尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

从零实现机器学习模型:线性回归、决策树与随机森林的核心原理与代码实战

从零实现机器学习模型:线性回归、决策树与随机森林的核心原理与代码实战 1. 项目概述从理论到代码打通预测模型的任督二脉在数学建模竞赛和实际数据分析项目中预测模型是当之无愧的“顶流”。无论是预测销量、股价还是分析用户行为、评估风险一个得心应手的预测模型库就是你的“瑞士军刀”。然而很多同学和初入行的朋友常常面临一个尴尬的断层理论课上听得头头是道公式推导也能看懂但一到自己动手面对空白的代码编辑器就两眼一抹黑不知道如何把那些优美的数学公式变成一行行能跑出结果的代码。更常见的是虽然能调用sklearn的几行接口跑出结果但对模型内部的参数调整、结果评估的深层逻辑、乃至不同模型间的核心差异依然是一知半解。这篇内容就是为你弥合这道鸿沟而准备的。我们不空谈理论也不只做简单的代码搬运。我将结合自己多年打比赛和做项目的经验带你深入线性回归、决策树、随机森林这几个最经典、最常用的预测模型内部。目标是让你不仅“知其然”知道代码怎么写更能“知其所以然”理解为什么这么写以及背后的数学和统计逻辑。我们会从最基础的模型假设和原理讲起然后立刻用Python代码实现其核心计算过程最后再与成熟的机器学习库如scikit-learn进行对比和衔接。你会发现自己动手实现一遍对于理解模型、调试参数、甚至创新改进有着不可替代的价值。无论你是正在备战数学建模竞赛的学生还是希望夯实基础的算法工程师这篇文章都能提供一条从理论直通实战的清晰路径。2. 基石模型线性回归的原理拆解与手动实现线性回归堪称预测世界的“Hello World”。它形式简单却蕴含着最小二乘估计、最大似然估计等重要的统计思想。很多人觉得它太简单而忽视其深度但恰恰是吃透线性回归才能为理解更复杂的模型打下坚实基础。2.1 模型核心最小二乘的几何与代数视角线性回归试图用一个线性方程来拟合数据y w1*x1 w2*x2 ... wn*xn b。其中y是因变量我们要预测的x1, x2,..., xn是自变量特征w1, w2,..., wn是权重系数b是截距。我们的目标是找到一组w和b使得预测值y_hat与真实值y之间的差距最小。这个“差距最小”在数学上通常定义为残差平方和RSS最小即最小二乘法。从代数角度看我们是在求解一个优化问题。从几何角度看我们是在高维空间中寻找一个超平面使得所有数据点到这个超平面的垂直距离残差的平方和最小。这个解有一个漂亮的闭式解解析解可以通过矩阵运算直接求得W (X^T * X)^(-1) * X^T * y。这里X是增加了全为1的列对应截距b的特征矩阵W是包含所有权重和截距的向量。这个公式很美但它隐藏了两个关键前提一是X^T * X矩阵必须是可逆的满秩这意味着特征之间不能存在完全的线性相关即多重共线性二是它假设误差项服从均值为0、方差恒定的正态分布且相互独立。在实际中这些假设经常被违反这就引出了岭回归Ridge、Lasso回归等正则化变体。2.2 从零实现梯度下降与矩阵求导虽然闭式解很优雅但在特征维度很高n很大时计算矩阵的逆(X^T * X)^(-1)会非常耗时甚至可能因为矩阵病态而无法计算。因此工业界和实际编程中更常用的是梯度下降法这是一种迭代逼近的数值优化方法。梯度下降的核心思想很直观想象你站在一座山上要最快下到山谷找到损失函数的最小值。你每走一步都沿着当前所在位置最陡峭的下山方向负梯度方向前进。步长就是学习率。对于线性回归损失函数是均方误差MSEL (1/m) * Σ(y_i - y_hat_i)^2。对其求关于权重W的偏导数得到梯度∇L -(2/m) * X^T * (y - X*W)。下面我们抛开sklearn用NumPy从头实现一个基于批量梯度下降的线性回归import numpy as np class LinearRegressionGD: def __init__(self, learning_rate0.01, n_iters1000): 初始化线性回归模型。 :param learning_rate: 学习率控制每一步更新的幅度。太大可能震荡太小收敛慢。 :param n_iters: 梯度下降迭代次数。 self.lr learning_rate self.n_iters n_iters self.weights None # 权重向量包含截距 self.loss_history [] # 记录每次迭代的损失用于可视化检查收敛 def _add_intercept(self, X): 在特征矩阵X前添加一列1用于计算截距项b。 intercept np.ones((X.shape[0], 1)) return np.hstack((intercept, X)) def fit(self, X, y): 训练模型使用梯度下降法拟合数据。 :param X: 训练特征形状 (m_samples, n_features) :param y: 训练标签形状 (m_samples,) # 添加截距项 X_b self._add_intercept(X) m, n X_b.shape # 初始化权重通常用小的随机数或零初始化 self.weights np.random.randn(n) * 0.01 for i in range(self.n_iters): # 计算当前预测值 y_pred X_b.dot(self.weights) # 计算误差 error y_pred - y # 计算梯度X_b.T.dot(error) / m gradients (2 / m) * X_b.T.dot(error) # 更新权重向负梯度方向移动 self.weights - self.lr * gradients # 计算并记录当前损失MSE loss np.mean(error ** 2) self.loss_history.append(loss) # 可选每100次迭代打印一次损失监控训练过程 if i % 100 0: print(fIteration {i}: Loss {loss:.4f}) def predict(self, X): 使用训练好的权重进行预测。 X_b self._add_intercept(X) return X_b.dot(self.weights) # 使用示例 if __name__ __main__: # 生成一些简单的线性数据加一点噪声 np.random.seed(42) m 100 X 2 * np.random.rand(m, 1) # 一个特征 y 4 3 * X np.random.randn(m, 1) # 真实关系: y 4 3x noise # 实例化并训练模型 lr_model LinearRegressionGD(learning_rate0.1, n_iters1000) lr_model.fit(X, y.flatten()) # y需要展平 # 查看学到的参数 (截距和权重) print(fLearned intercept (b): {lr_model.weights[0]:.4f}) print(fLearned coefficient (w): {lr_model.weights[1]:.4f}) # 预测新数据 X_new np.array([[1.5]]) print(fPrediction for X1.5: {lr_model.predict(X_new)[0]:.4f})注意手动实现梯度下降时学习率learning_rate的选择至关重要。上述代码用了0.1对于这个简单数据集可行。但对于不同尺度的数据可能需要先进行特征标准化如StandardScaler否则梯度下降可能难以收敛。这是手动实现教会我们的第一课数据预处理与模型训练密不可分。2.3 与Sklearn对比理解封装背后的逻辑运行上面的代码你会得到接近真实值4和3的参数。现在我们用sklearn做同样的事from sklearn.linear_model import LinearRegression from sklearn.metrics import mean_squared_error sk_model LinearRegression() sk_model.fit(X, y) print(fSklearn intercept: {sk_model.intercept_[0]:.4f}) print(fSklearn coefficient: {sk_model.coef_[0][0]:.4f}) y_pred_sk sk_model.predict(X) y_pred_manual lr_model.predict(X) print(fSklearn MSE: {mean_squared_error(y, y_pred_sk):.6f}) print(fManual GD MSE: {mean_squared_error(y, y_pred_manual):.6f})你会发现sklearn的结果通常更精确、更稳定因为它内部可能使用更高效的数值算法如基于奇异值分解SVD的解析解计算并且做了大量的数值稳定性处理。我们自己实现的梯度下降版本MSE可能会略高一点这是迭代逼近的特性。这里的核心收获是sklearn的.fit()和.predict()只是一个高度封装的黑箱。通过手动实现你明白了这个黑箱里在发生什么它可能在计算梯度、迭代更新权重、并监控损失。当你在使用sklearn的LinearRegression遇到奇异矩阵报错时你就能立刻想到可能是多重共线性问题从而考虑使用Ridge回归它在损失函数中加入了权重的L2范数惩罚项使得X^T * X αI总是可逆。这种从内到外的理解是调参和模型选择的底气所在。3. 树模型初探决策树的核心分裂逻辑与实现如果说线性回归是试图用一条直线或平面去拟合世界那么决策树则是用一系列“如果...那么...”的规则来划分世界。它更直观更容易解释也能处理非线性关系。决策树的核心在于“分裂”如何选择最佳的特征和分割点将数据分成更“纯”的子集。3.1 分裂准则信息增益、基尼系数与方差减少决策树的学习过程是一个递归的“选择特征-分割数据”的过程。关键问题是我们依据什么标准来选择“最好”的分割对于分类树常用指标有信息增益Information Gain基于信息熵。熵表示集合的混乱程度。信息增益是父节点的熵减去子节点熵的加权和。增益越大说明用该特征分割后子集的“纯度”提升越多。ID3算法使用此准则。基尼不纯度Gini Impurity衡量从数据集中随机抽取两个样本其类别标签不一致的概率。基尼系数越小纯度越高。CART分类树使用此准则。对于回归树预测连续值常用指标是 3.方差减少Variance Reduction选择那个能使分割后子节点目标值方差加权和减少最多的特征和分割点。直观理解就是让每个子组内的数据尽可能相似。以基尼系数为例其计算公式为Gini(p) 1 - Σ (p_i)^2其中p_i是第i类在节点中出现的概率。假设一个节点有10个样本6个属于A类4个属于B类。则该节点的基尼系数为1 - ((6/10)^2 (4/10)^2) 1 - (0.36 0.16) 0.48。如果我们找到一个分割点将数据分成两个子节点左节点8个样本6个A2个B和右节点2个样本0个A2个B。我们可以计算分割后的加权基尼系数并与父节点的0.48比较得出基尼系数的减少量即该分割的“增益”。3.2 手动构建一棵简单的CART分类树实现一棵完整的、支持剪枝的决策树比较复杂但我们可以实现其核心根据基尼系数寻找最佳分割。这能让你彻底理解.fit()方法在做什么。import numpy as np from collections import Counter class SimpleDecisionTree: 一个极简的CART分类树实现仅用于演示核心分裂逻辑。 def __init__(self, max_depth5, min_samples_split2): self.max_depth max_depth self.min_samples_split min_samples_split self.tree None def _gini(self, y): 计算一个节点中样本集合的基尼不纯度。 counter Counter(y) m len(y) if m 0: return 0 gini 1.0 for count in counter.values(): p count / m gini - p ** 2 return gini def _split(self, X_column, split_val): 根据特征列和分割值返回左右子集的索引。 left_indices np.where(X_column split_val)[0] right_indices np.where(X_column split_val)[0] return left_indices, right_indices def _best_split(self, X, y): 遍历所有特征和所有可能的分割点找到最佳分割。 best_gini_gain -1 best_feature_idx None best_split_val None best_left_idx, best_right_idx None, None m, n X.shape if m 1: return None, None, None, None # 无法继续分割 parent_gini self._gini(y) for feature_idx in range(n): # 获取该特征列的所有唯一值并排序取相邻值的中点作为候选分割点 feature_vals np.unique(X[:, feature_idx]) split_points (feature_vals[:-1] feature_vals[1:]) / 2 for split_val in split_points: left_idx, right_idx self._split(X[:, feature_idx], split_val) if len(left_idx) 0 or len(right_idx) 0: continue # 分割无效跳过 # 计算加权子节点基尼系数 left_gini self._gini(y[left_idx]) right_gini self._gini(y[right_idx]) weighted_gini (len(left_idx)/m)*left_gini (len(right_idx)/m)*right_gini # 计算基尼增益 gini_gain parent_gini - weighted_gini # 记录最佳增益 if gini_gain best_gini_gain: best_gini_gain gini_gain best_feature_idx feature_idx best_split_val split_val best_left_idx, best_right_idx left_idx, right_idx # 如果增益太小或为负则认为没有有效分割 if best_gini_gain 1e-7: return None, None, None, None return best_feature_idx, best_split_val, best_left_idx, best_right_idx def _build_tree(self, X, y, depth0): 递归构建决策树。 # 终止条件达到最大深度、样本数过少、或节点纯度已很高基尼系数为0 if (depth self.max_depth or len(y) self.min_samples_split or self._gini(y) 1e-7): # 返回叶子节点其值为该节点中最常见的类别 most_common Counter(y).most_common(1)[0][0] return {type: leaf, value: most_common} # 寻找最佳分割 feat_idx, split_val, left_idx, right_idx self._best_split(X, y) # 如果找不到有效分割也作为叶子节点 if feat_idx is None: most_common Counter(y).most_common(1)[0][0] return {type: leaf, value: most_common} # 递归构建左右子树 left_tree self._build_tree(X[left_idx], y[left_idx], depth1) right_tree self._build_tree(X[right_idx], y[right_idx], depth1) # 返回一个内部节点 return { type: internal, feature_idx: feat_idx, split_val: split_val, left: left_tree, right: right_tree } def fit(self, X, y): 训练决策树。 self.tree self._build_tree(np.array(X), np.array(y)) def _predict_one(self, x, node): 对单个样本进行预测。 if node[type] leaf: return node[value] # 根据节点的分裂规则决定走左子树还是右子树 if x[node[feature_idx]] node[split_val]: return self._predict_one(x, node[left]) else: return self._predict_one(x, node[right]) def predict(self, X): 对数据集进行预测。 return np.array([self._predict_one(x, self.tree) for x in np.array(X)]) # 使用示例鸢尾花数据集 from sklearn.datasets import load_iris from sklearn.model_selection import train_test_split from sklearn.metrics import accuracy_score iris load_iris() X, y iris.data, iris.target # 为了简化我们只取前两个特征和两个类别 X X[y ! 2, :2] y y[y ! 2] X_train, X_test, y_train, y_test train_test_split(X, y, test_size0.2, random_state42) simple_tree SimpleDecisionTree(max_depth3) simple_tree.fit(X_train, y_train) y_pred_simple simple_tree.predict(X_test) print(fSimple Tree Accuracy: {accuracy_score(y_test, y_pred_simple):.4f}) # 与sklearn的决策树对比 from sklearn.tree import DecisionTreeClassifier sk_tree DecisionTreeClassifier(max_depth3, criteriongini, random_state42) sk_tree.fit(X_train, y_train) y_pred_sk sk_tree.predict(X_test) print(fSklearn Tree Accuracy: {accuracy_score(y_test, y_pred_sk):.4f})这段代码实现了一个非常基础的决策树。它清晰地展示了递归分裂的过程从根节点开始计算所有可能分割的基尼增益选择增益最大的那个特征和分割点将数据分成两份然后对左右子集递归地重复这个过程直到满足停止条件深度、最小样本数、纯度。实操心得自己实现一遍后你会深刻理解决策树的两个关键超参数max_depth和min_samples_split的意义。max_depth控制树的复杂度防止过拟合min_samples_split规定了一个节点必须有多少样本才允许继续分裂避免产生只包含极少样本的无效节点。在数学建模中理解这些参数比盲目调参更重要。3.3 决策树的优势、劣势与可视化决策树最大的优点是可解释性极强。你可以直接把训练好的树画出来看到从根到叶子的每一条判断路径这非常符合人类的决策逻辑。在需要向非技术人员解释模型原因的场合如金融风控、医疗诊断辅助这是巨大的优势。但其劣势也很明显非常容易过拟合。一棵不加限制的树会一直分裂直到每个叶子节点都只包含一个样本纯度100%这相当于把训练数据完全背了下来对噪声极度敏感在新数据上表现会很差。这就是为什么我们需要剪枝Pruning或者使用它的集成版本——随机森林。你可以使用sklearn.tree.plot_tree或更强大的graphviz库来可视化你的树直观地检查模型的学习逻辑。4. 集成力量随机森林的“群众智慧”与并行实现“三个臭皮匠顶个诸葛亮。”随机森林就是这句话在机器学习中的完美体现。它通过构建多棵决策树并将它们的预测结果进行综合分类用投票回归用平均来获得比单棵决策树更稳定、更准确的模型。其核心思想是Bootstrap AggregatingBagging和随机特征子空间。4.1 Bagging与随机性为何有效Bagging的核心步骤是Bootstrap抽样从原始训练集中有放回地随机抽取m个样本通常m等于原始集大小形成一个自助采样集。由于是有放回一些样本可能被抽到多次一些样本可能一次都没被抽到。未被抽到的样本称为“袋外样本”Out-Of-Bag, OOB可用于模型验证。并行训练用这个采样集独立训练一个基学习器比如一棵决策树。重复与聚合重复以上过程n_estimators次得到n个基学习器。对于预测分类问题采用投票法回归问题采用平均法。随机森林在Bagging的基础上增加了一层随机性在每棵决策树进行节点分裂时不是从所有d个特征中选择最优特征而是先随机选取一个特征子集通常大小为sqrt(d)或log2(d)然后只在这个子集中寻找最优分裂特征。这两重随机性带来了巨大好处降低方差通过平均多棵树的预测平滑了单棵树因数据扰动而产生的方差有效抑制了过拟合。提升泛化能力特征随机性使得每棵树关注数据的不同侧面树之间的差异性多样性增大集成的效果更好。提供OOB估计无需额外的验证集即可用OOB样本评估模型性能这是一个非常方便的内置交叉验证。4.2 手动实现一个基础随机森林回归下面我们实现一个回归版本的随机森林重点展示Bagging和特征随机性的过程。为了简化我们使用sklearn的决策树作为基学习器但融入我们自己的随机采样逻辑。import numpy as np from sklearn.tree import DecisionTreeRegressor from sklearn.metrics import mean_squared_error from sklearn.model_selection import train_test_split from sklearn.datasets import make_regression class SimpleRandomForestRegressor: def __init__(self, n_estimators100, max_depthNone, max_featuressqrt, random_stateNone): 初始化随机森林回归器。 :param n_estimators: 森林中树的数量。 :param max_depth: 每棵树的最大深度。 :param max_features: 每棵树分裂时考虑的最大特征数。可以是int, float, sqrt, log2。 :param random_state: 随机种子用于复现结果。 self.n_estimators n_estimators self.max_depth max_depth self.max_features max_features self.random_state random_state self.trees [] self.feature_indices_for_trees [] # 记录每棵树使用的特征索引用于理解模型 self.oob_predictions None # 存储OOB预测用于评估 def _get_max_features(self, n_features): 根据参数确定每棵树实际使用的特征数量。 if isinstance(self.max_features, int): return min(self.max_features, n_features) elif isinstance(self.max_features, float): return max(1, int(self.max_features * n_features)) elif self.max_features sqrt: return int(np.sqrt(n_features)) elif self.max_features log2: return int(np.log2(n_features)) else: return n_features # 默认使用所有特征这时就退化为Bagging了 def fit(self, X, y): 训练随机森林。 核心为每棵树进行Bootstrap采样并记录其使用的特征子集。 X np.array(X) y np.array(y).flatten() n_samples, n_features X.shape rng np.random.RandomState(self.random_state) # 确定每棵树使用的特征数 n_features_per_tree self._get_max_features(n_features) # 初始化OOB预测容器一个列表的列表每个内列表存储一个样本的所有OOB预测 oob_pred_list [[] for _ in range(n_samples)] for i in range(self.n_estimators): # 1. Bootstrap采样 # 有放回地随机抽取样本索引 sample_indices rng.choice(n_samples, sizen_samples, replaceTrue) # 获取未被抽中的样本索引OOB样本 oob_indices np.setdiff1d(np.arange(n_samples), np.unique(sample_indices)) # 2. 随机选择特征子集 feature_indices rng.choice(n_features, sizen_features_per_tree, replaceFalse) feature_indices.sort() # 排序便于后续索引 self.feature_indices_for_trees.append(feature_indices) # 3. 准备该树的训练数据仅使用选中的特征 X_bootstrap X[sample_indices][:, feature_indices] y_bootstrap y[sample_indices] # 4. 训练一棵决策树回归树 tree DecisionTreeRegressor(max_depthself.max_depth, random_staterng) tree.fit(X_bootstrap, y_bootstrap) self.trees.append((tree, feature_indices)) # 5. 用这棵树对它的OOB样本进行预测并存储 if len(oob_indices) 0: X_oob X[oob_indices][:, feature_indices] y_oob_pred tree.predict(X_oob) for idx, pred in zip(oob_indices, y_oob_pred): oob_pred_list[idx].append(pred) # 计算每个样本的OOB预测平均所有包含该样本的树的预测 self.oob_predictions np.zeros(n_samples) self.oob_sample_count np.zeros(n_samples) for i in range(n_samples): if oob_pred_list[i]: # 如果该样本至少在一棵树的OOB中 self.oob_predictions[i] np.mean(oob_pred_list[i]) self.oob_sample_count[i] len(oob_pred_list[i]) def predict(self, X): 对输入X进行预测取所有树预测结果的平均值。 X np.array(X) n_samples X.shape[0] all_predictions np.zeros((self.n_estimators, n_samples)) for i, (tree, feat_idx) in enumerate(self.trees): # 每棵树只使用它训练时对应的特征子集 X_subset X[:, feat_idx] all_predictions[i, :] tree.predict(X_subset) # 对所有树的预测结果按行取平均 return np.mean(all_predictions, axis0) def score_oob(self, y_true): 使用OOB预测计算R^2分数仅针对有OOB预测的样本。 mask self.oob_sample_count 0 if np.sum(mask) 0: print(Warning: No OOB samples found. Consider increasing n_estimators.) return None y_true_masked y_true[mask] y_pred_masked self.oob_predictions[mask] # 计算R^2 ss_res np.sum((y_true_masked - y_pred_masked) ** 2) ss_tot np.sum((y_true_masked - np.mean(y_true_masked)) ** 2) r2 1 - (ss_res / ss_tot) return r2 # 生成一个回归数据集进行测试 X, y make_regression(n_samples500, n_features10, noise0.1, random_state42) X_train, X_test, y_train, y_test train_test_split(X, y, test_size0.2, random_state42) # 使用我们的简单随机森林 rf_manual SimpleRandomForestRegressor(n_estimators50, max_depth5, max_featuressqrt, random_state42) rf_manual.fit(X_train, y_train) # OOB评分 oob_r2 rf_manual.score_oob(y_train) print(fManual Random Forest OOB R^2 Score: {oob_r2:.4f}) # 测试集预测 y_pred_manual rf_manual.predict(X_test) test_mse_manual mean_squared_error(y_test, y_pred_manual) print(fManual Random Forest Test MSE: {test_mse_manual:.6f}) # 与sklearn的随机森林对比 from sklearn.ensemble import RandomForestRegressor rf_sk RandomForestRegressor(n_estimators50, max_depth5, max_featuressqrt, oob_scoreTrue, random_state42) rf_sk.fit(X_train, y_train) print(fSklearn Random Forest OOB R^2 Score: {rf_sk.oob_score_:.4f}) y_pred_sk rf_sk.predict(X_test) test_mse_sk mean_squared_error(y_test, y_pred_sk) print(fSklearn Random Forest Test MSE: {test_mse_sk:.6f})这个手动实现版本虽然功能上远不如sklearn的RandomForestRegressor完整和优化例如缺少对缺失值的处理、并行计算等但它清晰地揭示了随机森林的工作原理循环创建多棵树每棵树基于不同的数据子集和特征子集进行训练最终预测时集体投票或取平均。4.3 随机森林的调参要点与特征重要性理解了原理调参就不再是玄学。随机森林的关键参数包括n_estimators树的数量。越多越好但计算成本也越高。通常在一定数量后如100-200性能提升会趋于平缓。max_depth单棵树的最大深度。限制深度可以防止过拟合但可能欠拟合。通常通过交叉验证选择。max_features每棵树分裂时考虑的最大特征数。这是控制树之间相关性的关键参数。较小的值如sqrt或log2能增加多样性可能提升效果但太小也可能导致信息不足。min_samples_split/min_samples_leaf分裂节点所需的最小样本数/叶节点所需的最小样本数。增大这些值可以正则化模型防止过拟合。随机森林还有一个极其有用的副产品特征重要性。其计算方式通常基于基尼重要性或平均不纯度减少。简单说一个特征在所有树的所有分裂节点中被用于分裂时所带来的不纯度减少量的总和越大它的重要性就越高。sklearn通过model.feature_importances_属性直接提供。这在数学建模中用于特征筛选和理解问题驱动因素时价值连城。# 接上例使用sklearn模型获取特征重要性 importances rf_sk.feature_importances_ indices np.argsort(importances)[::-1] # 按重要性降序排列 print(Feature ranking:) for i, idx in enumerate(indices[:5]): # 打印前5个重要特征 print(f{i1}. Feature {idx} ({importances[idx]:.4f}))5. 模型评估与选择超越准确率的思考实现模型只是第一步如何评估和选择模型才是建模工作的核心。在数学建模竞赛和实际项目中切忌只看一个指标如准确率、R平方。5.1 回归任务评估矩阵对于回归问题常用的评估指标有均方误差MSE(1/n) * Σ(y_i - y_hat_i)^2。平方项放大了大误差的惩罚是最常用的损失函数。均方根误差RMSEsqrt(MSE)。其量纲与原始数据一致更易于解释。平均绝对误差MAE(1/n) * Σ|y_i - y_hat_i|。对异常值不如MSE敏感。决定系数R²1 - (SS_res / SS_tot)。表示模型解释的数据方差比例越接近1越好。但要注意在特征很多时R²会天然偏高。在sklearn中可以方便地调用这些指标from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score mse mean_squared_error(y_true, y_pred) rmse np.sqrt(mse) mae mean_absolute_error(y_true, y_pred) r2 r2_score(y_true, y_pred)5.2 分类任务评估矩阵对于分类问题准确率Accuracy常常具有误导性特别是在类别不平衡的数据集上比如99%的样本是A类1%是B类一个把所有样本都预测为A类的傻瓜模型也有99%的准确率。混淆矩阵Confusion Matrix这是所有评估的基础。它展示了真正例TP、假正例FP、真反例TN、假反例FN的数量。精确率PrecisionTP / (TP FP)。在所有预测为正的样本中真正为正的比例。关注预测的“准不准”。召回率RecallTP / (TP FN)。在所有真实为正的样本中被正确预测出来的比例。关注“找得全不全”。F1分数F1-Score精确率和召回率的调和平均数2 * (Precision * Recall) / (Precision Recall)。是综合考量。ROC曲线与AUC通过变化分类阈值绘制真正例率TPR vs. 假正例率FPR的曲线其下面积AUC用于评估模型整体排序能力对类别不平衡不敏感。from sklearn.metrics import confusion_matrix, classification_report, roc_auc_score # 对于二分类 print(confusion_matrix(y_true, y_pred)) print(classification_report(y_true, y_pred)) # 注意roc_auc_score通常需要预测的概率值而非类别标签 # auc roc_auc_score(y_true, y_pred_proba)5.3 交叉验证稳健评估的黄金准则永远不要只在一个固定的训练-测试集上评估模型这有很大的偶然性。K折交叉验证K-Fold Cross Validation是标准做法。它将数据分成K份轮流将其中一份作为验证集其余作为训练集重复K次最后取K次评估结果的平均值。from sklearn.model_selection import cross_val_score from sklearn.ensemble import RandomForestClassifier model RandomForestClassifier(n_estimators100, random_state42) # 使用准确率作为评估指标进行5折交叉验证 cv_scores cross_val_score(model, X, y, cv5, scoringaccuracy) print(fCross-validation scores: {cv_scores}) print(fMean CV accuracy: {cv_scores.mean():.4f} (/- {cv_scores.std()*2:.4f}))交叉验证的结果更能反映模型的泛化能力。在数学建模论文中汇报交叉验证的结果比单一划分的结果更有说服力。5.4 模型选择实战以波士顿房价数据集为例让我们用一个完整的流程对比线性回归、决策树和随机森林在一个经典数据集上的表现。import numpy as np import pandas as pd from sklearn.datasets import fetch_openml from sklearn.model_selection import train_test_split, cross_val_score, GridSearchCV from sklearn.preprocessing import StandardScaler from sklearn.linear_model import LinearRegression, Ridge from sklearn.tree import DecisionTreeRegressor from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import mean_squared_error, r2_score import warnings warnings.filterwarnings(ignore) # 加载波士顿房价数据集注意由于伦理问题sklearn已移除这里从openml获取 boston fetch_openml(nameboston, version1, as_frameTrue, parserpandas) X boston.data y boston.target # 数据划分 X_train, X_test, y_train, y_test train_test_split(X, y, test_size0.2, random_state42) # 特征标准化对线性模型很重要对树模型无所谓但做了也无害 scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test) # 初始化模型 models { Linear Regression: LinearRegression(), Ridge Regression (alpha1.0): Ridge(alpha1.0), Decision Tree (max_depth5): DecisionTreeRegressor(max_depth5, random_state42), Random Forest (n_estimators100): RandomForestRegressor(n_estimators100, random_state42) } # 训练、预测、评估 results {} for name, model in models.items(): model.fit(X_train_scaled, y_train) y_pred model.predict(X_test_scaled) mse mean_squared_error(y_test, y_pred) r2 r2_score(y_test, y_pred) results[name] {MSE: mse, R2: r2} print(f{name:30} - Test MSE: {mse:.4f}, Test R2: {r2:.4f}) # 使用交叉验证比较以Random Forest为例 rf RandomForestRegressor(random_state42) cv_scores_mse -cross_val_score(rf, X, y, cv5, scoringneg_mean_squared_error) cv_scores_r2 cross_val_score(rf, X, y, cv5, scoringr2) print(f\nRandom Forest 5-Fold CV:) print(f MSE: {cv_scores_mse.mean():.4f} (/- {cv_scores_mse.std()*2:.4f})) print(f R2: {cv_scores_r2.mean():.4f} (/- {cv_scores_r2.std()*2:.4f})) # 简单的网格搜索调参以决策树为例 param_grid { max_depth: [3, 5, 10, None], min_samples_split: [2, 5, 10], min_samples_leaf: [1, 2, 4] } tree DecisionTreeRegressor(random_state42) grid_search GridSearchCV(tree, param_grid, cv5, scoringneg_mean_squared_error, n_jobs-1) grid_search.fit(X_train_scaled, y_train) print(f\nBest Decision Tree Parameters: {grid_search.best_params_}) print(fBest CV MSE: {-grid_search.best_score_:.4f})运行这段代码你会直观地看到不同模型的表现差异。通常随机森林会表现最好因为它通过集成降低了方差。线性模型可能因为数据中的非线性关系而表现稍差但它的可解释性最强。决策树单独使用容易过拟合表现不稳定。关键经验没有“最好”的模型只有“最合适”的模型。选择模型时必须权衡预测精度、计算效率、模型复杂度和可解释性。在数学建模中如果问题要求对预测结果给出解释比如“哪些因素对房价影响最大”那么线性回归或带特征重要性的随机森林可能比一个黑箱的深度神经网络更合适。
返回列表