吴恩达机器学习补充:决策树和随机森林
数据集:通过网盘分享的文件:sonar-all-data.csv
链接: https://pan.baidu.com/s/1D3vbcnd6j424iAwssYzDeQ?pwd=12gr 提取码: 12gr
学习来源:https://github.com/cabin-w/MLBeginnerHub
定义
决策树的定义
决策树(Decision Tree)是一种基于树状结构进行决策的监督学习算法,其核心思想是通过对数据特征的逐步划分,模拟人类决策过程,最终实现对目标变量的预测(分类或回归)。
-
结构组成:
-
根节点:代表整个数据集,是决策树的起点。
-
内部节点:每个内部节点对应一个特征,用于对数据进行划分(如 “年龄是否大于 30 岁”“收入是否高于 5 万元”)。
-
分支:每个分支表示基于特征划分的结果(如 “是” 或 “否”)。
-
叶节点:树的末端,代表最终的决策结果(如分类问题中的类别,回归问题中的数值)。
-
-
工作原理: 从根节点开始,根据特征的重要性(如通过信息增益、基尼系数等指标衡量)选择最优特征,将数据划分为不同子集;对每个子集重复这一过程,直到子集中的样本属于同一类别(分类)或差异足够小(回归),形成叶节点。
-
示例: 预测 “用户是否购买商品” 时,决策树可能先按 “年龄” 划分,再按 “是否有优惠券” 进一步划分,最终叶节点输出 “购买” 或 “不购买”。
随机森林的定义
随机森林(Random Forest)是一种集成学习算法,通过构建多个决策树并综合其结果来提高预测性能,减少过拟合风险。
-
核心思想: 基于 “集成思想”—— 多个弱分类器(决策树)的组合可以形成强分类器。具体通过两个随机性实现:
-
样本随机:从原始数据中通过bootstrap 抽样(有放回抽样)生成多个不同的训练子集,每个子集用于训练一棵决策树。
-
特征随机:在每棵决策树的每个节点划分时,从所有特征中随机选择部分特征(如总特征数的平方根),仅基于这些特征进行最优划分。
-
由以上两个随机,我们就可以发现单棵决策树决策树的问题在于结果不稳定。
-
-
预测规则:
-
分类问题:多数投票法(所有决策树的预测结果中,出现次数最多的类别为最终结果)。
-
回归问题:平均值法(所有决策树的预测结果的平均值为最终结果)。
-
-
优势: 相比单一决策树,随机森林泛化能力更强,对噪声数据更稳健,且不易过拟合。
总结
算法 | 本质 | 核心特点 | 应用场景 |
决策树 | 单一树状模型 | 直观易懂,易过拟合 | 简单分类 / 回归、规则提取 |
随机森林 | 多棵决策树的集成 | 性能更优,抗过拟合,稳定性高 | 复杂数据预测、特征重要性评估 |
代码实现
加载数据
def loadCSV(filename): # 定义loadCSV函数,参数为filenamedataSet = [] # 创建空列表dataSetwith open(filename, 'r') as file: # 用只读方式打开filename文件并赋值给filecsvReader = csv.reader(file) # 用csv模块的reader方法读取file文件并赋值给csvReader(分割处理)for line in csvReader: # 遍历csvReader中的每一行dataSet.append(line) # 将每一行添加到dataSet列表中return dataSet # 返回dataSet列表
预处理
# 除了判别列,其他列都转换为float类型
def column_to_float(dataSet): # 定义column_to_float函数,参数为dataSetfeatLen = len(dataSet[0]) - 1 # 获取dataSet中最后一列的索引for data in dataSet: # 遍历dataSet中的每一行for column in range(featLen): # 遍历每一行中除最后一列之外的每一列data[column] = float(data[column].strip()) # 将每一列的字符串转换为浮点数并赋值回原位置
将数据集分成N块,方便交叉验证
def spiltDataSet(dataSet, n_folds):fold_size = int(len(dataSet) / n_folds) # 计算每份数据的大小dataSet_copy = list(dataSet) # 复制数据集dataSet_spilt = [] # 创建用于存储分割后数据集的列表for i in range(n_folds): # 循环n_folds次,将数据集分割成n份fold = [] # 创建一个空列表用于存储每份数据while len(fold) < fold_size: # 当该份数据未达到应有的大小时index = randrange(len(dataSet_copy) - 1) # 随机选择数据集中的索引(不放回)fold.append(dataSet_copy.pop(index)) # 将随机选择的数据加入该份数据,并从数据集中移除dataSet_spilt.append(fold) # 将该份数据添加到分割后数据集列表中return dataSet_spilt # 返回分割后的数据集列表
构造数据子集
# 从原始数据集中随机抽取指定比例的样本,构建一个数据子集,
#且采用的是有放回抽样(即同一个样本可能被多次选中)
def get_subsample(dataSet, ratio):subdataSet = [] # 创建空列表用于存储子样本lenSubdata = round(len(dataSet) * ratio) # 计算子样本的大小while len(subdataSet) < lenSubdata: # 当子样本未达到应有大小时index = randrange(len(dataSet) - 1) # 随机选择数据集中的索引(有放回)subdataSet.append(dataSet[index]) # 将随机选择的数据加入子样本return subdataSet # 返回子样本
分割数据集
#根据指定特征索引和分割值,将数据集划分为两个子集,是决策树等算法中用于划分节点的核心操作
# 分割数据集
def data_spilt(dataSet, index, value):left = [] # 创建存储左子集的列表right = [] # 创建存储右子集的列表for row in dataSet: # 遍历数据集中的每一行if row[index] < value: # 如果行中指定索引的值小于给定值left.append(row) # 将该行添加到左子集else:right.append(row) # 否则将该行添加到右子集return left, right # 返回左右的子集
损失函数
#计算数据集分割后的 “不纯度”(Impurity),
# 用于衡量分割的优劣。它实现的是基尼不纯度(Gini Impurity) 的计算逻辑,是分类问题中评估决策树分割质量的常用指标。
def spilt_loss(left, right, class_values):loss = 0.0 # 初始化分割代价为0for class_value in class_values: # 遍历每个类别的取值left_size = len(left) # 计算左子集的大小if left_size != 0: # 防止除数为零prop = [row[-1] for row in left].count(class_value) / float(left_size) # 计算左子集中该类别的占比loss += (prop * (1.0 - prop)) # 根据占比计算损失right_size = len(right) # 计算右子集的大小if right_size != 0: # 防止除数为零prop = [row[-1] for row in right].count(class_value) / float(right_size) # 计算右子集中该类别的占比loss += (prop * (1.0 - prop)) # 根据占比计算损失return loss # 返回分割代价
选取分割时的最优特征
def get_best_spilt(dataSet, n_features):features = [] # 用于存储随机选择的特征的列表class_values = list(set(row[-1] for row in dataSet)) # 获取数据集中的类别值b_index, b_value, b_loss, b_left, b_right = 999, 999, 999, None, None # 初始化最佳分割的特征索引、值、损失和左右子集while len(features) < n_features: # 当特征列表中的特征数量小于n_features时index = randrange(len(dataSet[0]) - 1) # 随机选择一个特征索引if index not in features: # 如果选择的特征索引不在特征列表中features.append(index) # 将特征索引添加到特征列表中for index in features: # 遍历随机选择的特征for row in dataSet: # 遍历数据集中的每一行left, right = data_spilt(dataSet, index, row[index]) # 根据特征和特征值将数据集分割成左右子集loss = spilt_loss(left, right, class_values) # 计算分割代价if loss < b_loss: # 如果当前分割代价比最小代价还小b_index, b_value, b_loss, b_left, b_right = index, row[index], loss, left, right # 更新最佳分割信息return {'index': b_index, 'value': b_value, 'left': b_left, 'right': b_right} # 返回最佳分割特征的索引、值、左右子集
确定最终的输出标签
#这个函数 decide_label 的作用是从输入数据中确定最终的输出标签,采用的是 “多数投票” 原则 —— 即选择数据中出现次数最多的标签作为结果
# 这在决策树等模型中常用于叶子节点的标签确定(当数据集无法再分割或达到停止条件时,用多数样本的标签作为该节点的预测结果)
def decide_label(data):output = [row[-1] for row in data] # 获取数据中所有标签的列表return max(set(output), key=output.count) # 返回出现次数最多的标签
子分割,不断地构建叶节点的过程
def sub_spilt(root, n_features, max_depth, min_size, depth):left = root['left']right = root['right']del (root['left'])del (root['right'])if not left or not right:root['left'] = root['right'] = decide_label(left + right) # 如果左节点或右节点为空,则将左右节点设为出现次数最多的标签returnif depth > max_depth:root['left'] = decide_label(left) # 如果深度超过最大深度,则将左节点设为出现次数最多的标签root['right'] = decide_label(right) # 将右节点设为出现次数最多的标签returnif len(left) < min_size:root['left'] = decide_label(left) # 如果左节点数据量小于最小数据量,则将左节点设为出现次数最多的标签else:root['left'] = get_best_spilt(left, n_features) # 否则,根据最佳分割点构建左节点sub_spilt(root['left'], n_features, max_depth, min_size, depth + 1) # 递归构造左子树if len(right) < min_size:root['right'] = decide_label(right) # 如果右节点数据量小于最小数据量,则将右节点设为出现次数最多的标签else:root['right'] = get_best_spilt(right, n_features) # 否则,根据最佳分割点构建右节点sub_spilt(root['right'], n_features, max_depth, min_size, depth + 1) # 递归构造右子树
构造决策树
def build_tree(dataSet, n_features, max_depth, min_size):root = get_best_spilt(dataSet, n_features) # 找到最佳分割点作为根节点sub_spilt(root, n_features, max_depth, min_size, 1) # 构建整棵决策树return root # 返回根节点
预测
def predict(tree, row):predictions = [] # 初始化一个存储预测结果的列表if row[tree['index']] < tree['value']: # 如果数据的某个特征值小于当前节点的划分值if isinstance(tree['left'], dict): # 如果左子树仍然是一个字典(即非叶子节点)return predict(tree['left'], row) # 递归地对左子树进行预测else: # 如果左子树是叶子节点return tree['left'] # 返回左子树的预测结果else: # 如果数据的某个特征值大于或等于当前节点的划分值if isinstance(tree['right'], dict): # 如果右子树仍然是一个字典(即非叶子节点)return predict(tree['right'], row) # 递归地对右子树进行预测else: # 如果右子树是叶子节点return tree['right'] # 返回右子树的预测结果
得到最终的预测值
#集成学习中 Bagging( bootstrap aggregating)方法的预测函数,用于结合多棵决策树的预测结果,得到最终的预测值。
# 其核心思想是 **“多数投票”**—— 即综合多棵树的预测结果,选择出现次数最多的结果作为最终预测。
def bagging_predict(trees, row):predictions = [predict(tree, row) for tree in trees] # 使用每棵树对数据进行预测,并将预测结果存储在列表中# 找到预测结果中出现次数最多的值作为最终预测结果return max(set(predictions), key=predictions.count) # 找到预测结果中出现次数最多的值作为最终预测结果
创建随机森林
def random_forest(train, test, ratio, n_features, max_depth, min_size, n_trees):trees = [] # 用于存储每棵树的列表for i in range(n_trees):train = get_subsample(train, ratio) # 从训练集中进行子采样tree = build_tree(train, n_features, max_depth, min_size) # 构建一棵树print('tree %d: '%i,tree)trees.append(tree) # 将构建好的树添加到列表中# predict_values = [predict(trees,row) for row in test]predict_values = [bagging_predict(trees, row) for row in test] # 对测试集中的每一行数据进行预测return predict_values # 返回预测结果列表
计算准确率
def accuracy(predict_values, actual):correct = 0 # 初始化正确预测的数量for i in range(len(actual)): # 遍历每一个预测结果if actual[i] == predict_values[i]: # 如果预测结果与实际结果相符correct += 1 # 正确预测数量加一return correct / float(len(actual)) # 返回正确预测的比例
主函数
if __name__ == '__main__':seed(1) # 设置随机种子,以确保结果的可重复性dataSet = loadCSV('sonar-all-data.csv') # 载入数据集column_to_float(dataSet) # 将数据集中的数据类型转换为浮点型n_folds = 5 # 设置交叉验证的折数max_depth = 15 # 决策树的最大深度min_size = 1 # 叶子节点的最小样本数ratio = 1 # 子采样比例n_features = 15 # 特征数量n_trees = 20 # 随机森林中树的数量folds = spiltDataSet(dataSet, n_folds) # 将数据集划分为指定折数的子集scores = [] # 用于存储每次交叉验证的准确率# 实现了k折交叉验证(k - foldcross - validation) 的核心逻辑,用于评估随机森林模型的性能。它将数据集分成多个子集(fold),# 每次用其中一个子集作为测试集,其余作为训练集,循环验证并计算模型准确率,最终得到多个准确率分数用于评估模型稳定性。for fold in folds: # 对每个子集进行交叉验证train_set = folds[:] # 复制数据集train_set.remove(fold) # 从训练集中移除当前折的数据train_set = sum(train_set, []) # 将多个fold列表组合成一个train_set列表test_set = [list(row)[:-1] for row in fold] # 从当前折中提取测试数据actual = [row[-1] for row in fold] # 获取当前折的实际结果predict_values = random_forest(train_set, test_set, ratio, n_features, max_depth, min_size,n_trees) # 使用随机森林进行预测accur = accuracy(predict_values, actual) # 计算准确率scores.append(accur) # 将准确率添加到列表中print('Trees is %d' % n_trees) # 打印树的数量print('scores:%s' % scores) # 打印每次交叉验证的准确率print('mean score:%s' % (sum(scores) / float(len(scores)))) # 打印平均准确率