决策树是一种简单而强大的预测建模技术,但它存在高方差的问题。
这意味着在不同的训练数据下,决策树可能会得到非常不同的结果。
一种使决策树更稳健并实现更好性能的技术称为自举聚合,简称 bagging。
在本教程中,您将学习如何使用 Python 从零开始实现带有决策树的 bagging 过程。
完成本教程后,您将了解:
- 如何创建数据集的自举样本。
- 如何使用自举模型进行预测。
- 如何将 bagging 应用于您自己的预测建模问题。
通过我的新书《从零开始的机器学习算法》启动您的项目,其中包括逐步教程和所有示例的 Python 源代码文件。
让我们开始吧。
- 更新于 2017 年 1 月:更改了 `cross_validation_split()` 中 `fold_size` 的计算,使其始终为整数。修复了 Python 3 的问题。
- 2017 年 2 月更新:修复了 build_tree 中的一个 bug。
- 2017 年 8 月更新:修复了 Gini 计算中的一个 bug,添加了按组大小对组 Gini 分数进行加权的功能(感谢 Michael!)。
- 2018 年 8 月更新:测试并更新以与 Python 3.6 配合使用。

如何用 Python 从零开始实现 Bagging
图片来源:Michael Cory,保留部分权利。
描述
本节简要介绍了自举聚合和本教程中将使用的声纳数据集。
自举聚合算法
自举是对带替换数据集的采样。
这意味着从现有数据集的随机样本中创建一个新数据集,其中给定行可以被选择并多次添加到样本中。
当您只有有限的数据集可用时,这是一种用于估计更广泛数据集(例如平均值)的值的有用方法。通过创建数据集的样本并从这些样本中估计平均值,您可以取这些估计值的平均值,并更好地了解底层问题的真实平均值。
同样的方法也可以用于具有高方差的机器学习算法,例如决策树。每个数据自举样本都训练一个单独的模型,并使用这些模型的平均输出进行预测。这种技术称为自举聚合,简称 bagging。
方差意味着算法的性能对训练数据敏感,高方差表明训练数据变化越大,算法的性能变化越大。
通过训练许多树并取其预测的平均值,可以提高未剪枝决策树等高方差机器学习算法的性能。结果通常优于单个决策树。
除了提高性能之外,bagging 的另一个好处是袋装决策树不会过拟合问题。可以继续添加树,直到达到最大性能。
声纳数据集
本教程中将使用的数据集是声纳数据集。
这是一个描述声纳脉冲从不同表面反弹的数据集。60 个输入变量是不同角度的回波强度。这是一个二元分类问题,需要一个模型来区分岩石和金属圆柱体。共有 208 个观测值。
这是一个众所周知的数据集。所有变量都是连续的,通常在 0 到 1 的范围内。输出变量是一个字符串,“M”代表水雷,“R”代表岩石,需要转换为整数 1 和 0。
通过预测数据集中观测值最多的类别(M 或地雷),零规则算法可以达到 53% 的准确率。
您可以在 UCI 机器学习存储库了解有关此数据集的更多信息。
免费下载数据集,并将其放置在您的工作目录中,文件名为 **sonar.all-data.csv**。
教程
本教程分为 2 个部分
- 自举重采样。
- 声纳数据集案例研究。
这些步骤为您提供了将自举聚合与决策树应用于您自己的预测建模问题所需的基础。
1. 自举重采样
让我们首先深入了解自举方法的工作原理。
我们可以通过从数据集中随机选择行并将它们添加到新列表中来创建数据集的新样本。我们可以对固定数量的行重复此操作,或者直到新数据集的大小与原始数据集大小的比例匹配。
我们可以通过不删除已选择的行来允许带替换采样,以便它可用于将来的选择。
下面是一个名为 subsample() 的函数,它实现了这个过程。random 模块中的 randrange() 函数用于在循环的每次迭代中选择一个随机行索引添加到样本中。样本的默认大小是原始数据集的大小。
1 2 3 4 5 6 7 8 |
# 从数据集中创建带替换的随机子样本 def subsample(dataset, ratio=1.0): sample = list() n_sample = round(len(dataset) * ratio) while len(sample) < n_sample: index = randrange(len(dataset)) sample.append(dataset[index]) return sample |
我们可以使用此函数来估计一个虚构数据集的平均值。
首先,我们可以创建一个包含 20 行和一列 0 到 9 之间随机数的数据集,并计算其平均值。
然后我们可以对原始数据集进行自举样本,计算平均值,并重复此过程直到我们得到一个平均值列表。对这些样本平均值取平均值应该能给我们一个对整个数据集平均值的稳健估计。
完整的示例如下所示。
每个自举样本都是作为原始 20 个观测数据集的 10% 样本(或 2 个观测值)创建的。然后我们通过创建 1、10、100 个原始数据集的自举样本进行实验,计算它们的平均值,然后对所有这些估计的平均值取平均值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
from random import seed from random import random from random import randrange # 从数据集中创建带替换的随机子样本 def subsample(dataset, ratio=1.0): sample = list() n_sample = round(len(dataset) * ratio) while len(sample) < n_sample: index = randrange(len(dataset)) sample.append(dataset[index]) return sample # 计算数字列表的平均值 def mean(numbers): return sum(numbers) / float(len(numbers)) seed(1) # 真实平均值 dataset = [[randrange(10)] for i in range(20)] print('True Mean: %.3f' % mean([row[0] for row in dataset])) # 估计平均值 ratio = 0.10 for size in [1, 10, 100]: sample_means = list() for i in range(size): sample = subsample(dataset, ratio) sample_mean = mean([row[0] for row in sample]) sample_means.append(sample_mean) print('Samples=%d, Estimated Mean: %.3f' % (size, mean(sample_means))) |
运行示例会打印我们旨在估计的原始平均值。
然后我们可以看到来自不同数量自举样本的估计平均值。我们可以看到,使用 100 个样本,我们获得了平均值的良好估计。
1 2 3 4 |
真实平均值:4.450 样本数=1,估计平均值:4.500 样本数=10,估计平均值:3.300 样本数=100,估计平均值:4.480 |
我们可以从每个子样本中创建一个模型,而不是计算平均值。
接下来,让我们看看如何组合来自多个自举模型的预测。
2. 声纳数据集案例研究
在本节中,我们将把随机森林算法应用于声纳数据集。
该示例假设数据集的 CSV 副本位于当前工作目录中,文件名为 sonar.all-data.csv。
首先加载数据集,将字符串值转换为数字,并将输出列从字符串转换为 0 到 1 的整数值。这通过辅助函数 load_csv()、str_column_to_float() 和 str_column_to_int() 来实现,用于加载和准备数据集。
我们将使用 k 折交叉验证来估计学习模型在未见数据上的性能。这意味着我们将构建和评估 k 个模型,并将性能估计为平均模型误差。分类精度将用于评估每个模型。这些行为由 cross_validation_split()、accuracy_metric() 和 evaluate_algorithm() 辅助函数提供。
我们还将使用经过修改以用于装袋的分类和回归树(CART)算法实现,其中包括辅助函数 test_split() 用于将数据集分成组,gini_index() 用于评估分割点,get_split() 用于找到最佳分割点,to_terminal()、split() 和 build_tree() 用于创建单个决策树,predict() 用于使用决策树进行预测,以及前一步中描述的 subsample() 函数用于创建训练数据集的子样本。
开发了一个名为 bagging_predict() 的新函数,负责使用每棵决策树进行预测,并将预测结果组合成一个单一的返回值。这是通过从袋装树所做的预测列表中选择最常见的预测来实现的。
最后,开发了一个名为 bagging() 的新函数,负责创建训练数据集的样本,对每个样本训练一个决策树,然后使用袋装树列表对测试数据集进行预测。
完整的示例如下所示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 |
# 声纳数据集上的 Bagging 算法 from random import seed from random import randrange from csv import reader # 加载 CSV 文件 def load_csv(filename): dataset = list() with open(filename, 'r') as file: csv_reader = reader(file) for row in csv_reader: if not row: continue dataset.append(row) return dataset # 将字符串列转换为浮点数 def str_column_to_float(dataset, column): for row in dataset: row[column] = float(row[column].strip()) # 将字符串列转换为整数 def str_column_to_int(dataset, column): class_values = [row[column] for row in dataset] unique = set(class_values) lookup = dict() for i, value in enumerate(unique): lookup[value] = i for row in dataset: row[column] = lookup[row[column]] return lookup # 将数据集分成 k 折 def cross_validation_split(dataset, n_folds): dataset_split = list() dataset_copy = list(dataset) fold_size = int(len(dataset) / n_folds) for i in range(n_folds): fold = list() while len(fold) < fold_size: index = randrange(len(dataset_copy)) fold.append(dataset_copy.pop(index)) dataset_split.append(fold) return dataset_split # 计算准确率百分比 def accuracy_metric(actual, predicted): correct = 0 for i in range(len(actual)): if actual[i] == predicted[i]: correct += 1 return correct / float(len(actual)) * 100.0 # 使用交叉验证分割评估算法 def evaluate_algorithm(dataset, algorithm, n_folds, *args): folds = cross_validation_split(dataset, n_folds) scores = list() for fold in folds: train_set = list(folds) train_set.remove(fold) train_set = sum(train_set, []) test_set = list() for row in fold: row_copy = list(row) test_set.append(row_copy) row_copy[-1] = None predicted = algorithm(train_set, test_set, *args) actual = [row[-1] for row in fold] accuracy = accuracy_metric(actual, predicted) scores.append(accuracy) 返回 分数 # 根据属性和属性值分割数据集 def test_split(index, value, dataset): left, right = list(), list() for row in dataset: if row[index] < value: left.append(row) else: right = list() return left, right # 计算分割数据集的基尼指数 def gini_index(groups, classes): # 计算分割点处的所有样本 n_instances = float(sum([len(group) for group in groups])) # 对每个组的加权基尼指数求和 gini = 0.0 for group in groups: size = float(len(group)) # 避免除以零 if size == 0: continue score = 0.0 # 根据每个类别的分数对组进行评分 for class_val in classes: p = [row[-1] for row in group].count(class_val) / size score += p * p # 根据其相对大小对组分数进行加权 gini += (1.0 - score) * (size / n_instances) return gini # 为数据集选择最佳分割点 def get_split(dataset): class_values = list(set(row[-1] for row in dataset)) b_index, b_value, b_score, b_groups = 999, 999, 999, None for index in range(len(dataset[0])-1): for row in dataset: # for i in range(len(dataset)): # row = dataset[randrange(len(dataset))] groups = test_split(index, row[index], dataset) gini = gini_index(groups, class_values) if gini < b_score: b_index, b_value, b_score, b_groups = index, row[index], gini, groups return {'index':b_index, 'value':b_value, 'groups':b_groups} # 创建一个终端节点值 def to_terminal(group): outcomes = [row[-1] for row in group] return max(set(outcomes), key=outcomes.count) # 为节点创建子分割或使其成为终端 def split(node, max_depth, min_size, depth): left, right = node['groups'] del(node['groups']) # 检查是否没有分割 if not left or not right: node['left'] = node['right'] = to_terminal(left + right) return # 检查是否达到最大深度 if depth >= max_depth: node['left'], node['right'] = to_terminal(left), to_terminal(right) return # 处理左子节点 if len(left) <= min_size: node['left'] = to_terminal(left) else: node['left'] = get_split(left) split(node['left'], max_depth, min_size, depth+1) # 处理右子节点 if len(right) <= min_size: node['right'] = to_terminal(right) else: node['right'] = get_split(right) split(node['right'], max_depth, min_size, depth+1) # 构建决策树 def build_tree(train, max_depth, min_size): root = get_split(train) split(root, max_depth, min_size, 1) return root # 使用决策树进行预测 def predict(node, row): if row[node['index']] < node['value']: if isinstance(node['left'], dict): return predict(node['left'], row) else: return node['left'] else: if isinstance(node['right'], dict): return predict(node['right'], row) else: return node['right'] # 从数据集中创建带替换的随机子样本 def subsample(dataset, ratio): sample = list() n_sample = round(len(dataset) * ratio) while len(sample) < n_sample: index = randrange(len(dataset)) sample.append(dataset[index]) return sample # 用一个袋装树列表进行预测 def bagging_predict(trees, row): predictions = [predict(tree, row) for tree in trees] return max(set(predictions), key=predictions.count) # 自举聚合算法 def bagging(train, test, max_depth, min_size, sample_size, n_trees): trees = list() for i in range(n_trees): sample = subsample(train, sample_size) tree = build_tree(sample, max_depth, min_size) trees.append(tree) predictions = [bagging_predict(trees, row) for row in test] return(predictions) # 在声纳数据集上测试 Bagging seed(1) # 加载并准备数据 filename = 'sonar.all-data.csv' dataset = load_csv(filename) # 将字符串属性转换为整数 for i in range(len(dataset[0])-1): str_column_to_float(dataset, i) # 将类别列转换为整数 str_column_to_int(dataset, len(dataset[0])-1) # 评估算法 n_folds = 5 max_depth = 6 min_size = 2 sample_size = 0.50 for n_trees in [1, 5, 10, 50]: scores = evaluate_algorithm(dataset, bagging, n_folds, max_depth, min_size, sample_size, n_trees) print('Trees: %d' % n_trees) print('Scores: %s' % scores) print('Mean Accuracy: %.3f%%' % (sum(scores)/float(len(scores)))) |
k 值为 5 用于交叉验证,每折有 208/5 = 41.6 或略多于 40 条记录进行每次迭代的评估。
构建了深度为 6、每个节点最小训练行数为 2 的深层树。训练数据集的样本大小为原始数据集的 50%。这是为了强制在用于训练每棵树的数据集子样本中产生一些差异。Bagging 的默认设置是样本数据集的大小与原始训练数据集的大小匹配。
评估了 4 种不同数量的树,以显示算法的行为。
打印了每次折叠的精度和每次配置的平均精度。我们可以看到,随着树的数量增加,性能有小幅提升的趋势。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
树:1 分数:[87.8048780487805, 65.85365853658537, 65.85365853658537, 65.85365853658537, 73.17073170731707] 平均精度:71.707% 树:5 分数:[60.97560975609756, 80.48780487804879, 78.04878048780488, 82.92682926829268, 63.41463414634146] 平均精度:73.171% 树:10 分数:[60.97560975609756, 73.17073170731707, 82.92682926829268, 80.48780487804879, 68.29268292682927] 平均精度:73.171% 树:50 分数:[63.41463414634146, 75.60975609756098, 80.48780487804879, 75.60975609756098, 85.36585365853658] 平均精度:76.098% |
这种方法的难点在于,即使构建了深层树,所创建的袋装树也高度相似。反过来,这些树做出的预测也相似,我们希望在训练数据集的不同样本上训练的树之间存在的高方差被削弱了。
这是因为树的构建中使用的贪婪算法选择相同或相似的分割点。
本教程试图通过限制用于训练每棵树的样本大小来重新注入这种方差。一种更稳健的技术是在创建每个分割点时限制可以评估的特征。这是随机森林算法中使用的方法。
扩展
- 调整示例。探索不同数量的树甚至单个树的配置,看看是否可以进一步改善结果。
- 袋装其他算法。其他算法也可以与 bagging 一起使用。例如,k 值较低的 k 最近邻算法将具有高方差,并且是 bagging 的良好候选者。
- 回归问题。Bagging 可以与回归树一起使用。您不再预测预测集中最常见的类别值,而是返回袋装树预测的平均值。在回归问题上进行实验。
你尝试过这些扩展吗?
在下面的评论中分享您的经验。
回顾
在本教程中,您学习了如何使用 Python 从零开始实现自举聚合。
具体来说,你学到了:
- 如何创建子样本并估计自举量。
- 如何创建决策树集成并使用它们进行预测。
- 如何将 bagging 应用于实际的预测建模问题。
你有什么问题吗?
在下面的评论中提出你的问题,我会尽力回答。
感谢这个从零开始的 python 示例。我认为您的分数保持不变的原因是您使用整个数据集来选择分割属性。这导致了相似的树,从而导致集成集的方差很小。如果您以某种方式更改您的代码,使树的方差更大,您将看到集成性能的提高。一个解决方案是修改此代码片段
# 构建决策树
def build_tree(train, max_depth, min_size)
root = get_split(train)
split(root, max_depth, min_size, 1)
return root
谢谢您的提示!
嗨,Jason,
只是想知道您是否根据他的建议修改了您的代码?(看不到您的代码和他的建议之间有什么区别)
是的,已经修复了。它以前使用整个数据集。
感谢您精彩的教程!
我已使用 sklearn 重写此脚本,以便轻松实现
https://gist.github.com/JovanSardinha/2c58bd1e7e3aa4c02affedfe7abe8a29
干得好!
嗨,Jason,
小提示 – 在字符串到整数的转换中 – 我发现在重复运行此函数和其他使用此函数的脚本时,唯一集是以某种随机顺序创建的。为避免这种情况,我将该行更改为:
unique = sorted(set(class_values))
这导致每次都创建相同的查找字典。我是在使用查找字典创建中间数据的精美打印输出时偶然发现的。
很棒的提示,谢谢 Alex!
嘿,这个链接不起作用
看这里
https://machinelearning.org.cn/bagging-ensemble-with-python/
我正在尝试将您的工作移植到 R 中。
然而,我在翻译上遇到了困难。具体来说,您“计算分割数据集的基尼指数”函数中的一行代码很难翻译成 R 语言。
p = [row[-1] for row in group].count(class_val) / size
您能帮我一下吗?或者考虑发布一个这个代码的 R 实现?
提前感谢!
抱歉,我没有能力为您翻译代码或调试新的 R 实现。
我遇到的问题是使用网格搜索调整参数时得到了不同的结果,我该如何解决这个问题,谢谢
这是预料之中的
https://machinelearning.org.cn/faq/single-faq/why-do-i-get-different-results-each-time-i-run-the-code
我使用随机森林回归器在 Python 中实现了 bagging,并调整了参数,但结果始终不收敛。训练精度为 95%,测试精度为 55%。
X = np.array([Depth, Dist, Soft, Stiff, sand, Sand, Stiff_C, Invert, FP, Penetr, Pitching, GP, GF]).T
Y = np.array(Settlement)
Y = Y.reshape(len(Y),)
data = [X, Y]
data = np.random.shuffle(data)
rf = result = BaggingRegressor(RandomForestRegressor(), n_estimators = 270,
bootstrap = True, oob_score = True, random_state = 42, max_features = 6)
rf.fit(X,Y)
pred = rf.predict(X)
r2 = r2_score(Y, pred)
plt.scatter(x=Y, y=pred)
plt.show()
print(“Train Accuracy : ” + str(r2))
print(“Test Accuracy : ” + str(rf.oob_score_))
也许可以尝试 scikit-learn 对算法的实现?
嗨 Jason,教程很棒。但是,我想知道 bagging 与交叉验证有什么不同?对于大型数据集,它们做的是“相同类型”的过程。您能解释一下吗?
相似之处在于它们都执行重采样,但目的不同。
在 bagging 中,我们寻求高方差,因为我们正在创建一个用于预测的集成模型。
在 CV 中,我们寻求偏差和方差之间的平衡,以获得对未见数据模型性能的可靠估计。
谢谢你的澄清
您好,如何使用 sklearn 分类器绘制自举的精度?
您是指使用自举法估计分类器的性能吗?
这个教程会有帮助
https://machinelearning.org.cn/a-gentle-introduction-to-the-bootstrap-method/
你好,当我们使用 bagging 技术创建一个集成模型时,每个模型(假设是一个 CNN)都在不同的数据集上训练,测试集对于每个模型和集成模型都是相同的,但是用于训练模型的验证集呢?我可以使用相同的数据集来验证所有模型,还是需要为每个模型使用不同的数据集,因为每个模型都在不同的数据集上训练。提前感谢!
您可以将袋外样本用作每个袋装模型的验证数据集。
非常感谢!!
不客气!
感谢您的本教程。我有一个问题。
我相信这行代码
max(set(predictions), key=predictions.count)
与以下代码相同
max(predictions, key=predictions.count)
这正确吗?
不,set() 用于查找唯一值。
我对此相当陌生,所以很可能是用户错误,我使用的是 PyCharm Community 版
=== 我是不是错过了什么?? ===
============================= 测试会话开始 =============================
正在收集…已收集 1 项
ex_bagging_from_scratch.py::test_split 错误 [100%]
测试设置失败
文件 D:\DomiPy\venv\ex_bagging_from_scratch.py,第 81 行
def test_split(index, value, dataset)
E 夹具“index”未找到
> 可用夹具:cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, monkeypatch, pytestconfig, record_property, record_testsuite_property, record_xml_attribute, recwarn, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory
> 有关它们的帮助,请使用“pytest --fixtures [testpath]”。
D:\DomiPy\venv\ex_bagging_from_scratch.py:81
很抱歉听到这个消息,也许这些提示会有帮助。
https://machinelearning.org.cn/faq/single-faq/why-does-the-code-in-the-tutorial-not-work-for-me
嗨,Jason,
非常感谢您提供的丰富信息教程!
我是一个初学者,所以请多多包涵!🙂
据我理解,这个实现与这篇论文 https://doi.org/10.1016/S0031-3203(02)00121-8 中的算法类似,我正在尝试将其实现用于回归问题,其中数据集很小(230 个数据点)。
我的问题是:如何修改代码以使用带有 L2 正则化的线性回归?
嗨 Lima,
我很乐意帮忙,但我没有能力根据你的具体需求定制代码。
我收到很多这样的请求。我相信你能理解我的理由。
我有一些可能有所帮助的想法
也许我有一个关于您所要求更改的教程?请在博客中搜索。
也许你可以自己尝试进行更改?
也许你可以在博文下方添加评论,说明你需要进行的更改,我和其他读者可以提出建议?
也许你可以雇佣承包商或程序员进行更改?
也许你可以在 stackoverflow.com 上发布所需代码的描述?