ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

线性回归手写实现指南:从梯度下降到模型评估的机器学习入门实践

线性回归手写实现指南:从梯度下降到模型评估的机器学习入门实践 每次带学弟学妹做山东大学的机器学习实验一我都会先问一句你们觉得线性回归是不是太简单了大部分人会点头然后就被波士顿房价数据集教做人了。这个实验表面上看就是拟合一条直线但真正做下来你会发现它把机器学习最核心的几件事全串起来了——数据预处理、模型假设、损失函数设计、梯度下降求解、评估与调参。可以说搞定线性回归你就搞定了机器学习入门阶段80%的思维模式。这篇就把我做这个实验的完整思路、踩过的坑、调试技巧全部摊开讲清楚。不管你是刚接触机器学习的小白还是想回头夯实基础的老手都可以直接照着做。文章里没有花里胡哨的框架全部用numpy手写实现保证你能理解每一步在干什么。1. 从问题出发这个实验到底要做什么很多同学拿到实验题第一反应是上网搜代码搜到一堆用sklearn几行搞定的版本然后复制粘贴交差。这样做最大的问题是你根本不知道模型内部发生了什么后面学到神经网络、SVM这些更复杂的模型时会越来越吃力。1.1 线性回归的核心思想先说人话。线性回归就是找一条“最合适”的直线或者超平面来描述自变量和因变量之间的关系。比如你要预测波士顿地区的房价输入特征有房间数、犯罪率、高速公路可达性等十几个维度输出是一个连续的房价数值。线性回归假设房价可以用这些特征的线性组合来近似表达[ \hat{y} w_1 x_1 w_2 x_2 \cdots w_n x_n b ]这里的 (w) 是权重每个特征对房价的影响程度(b) 是偏置基础房价。训练过程就是不断调整 (w) 和 (b)让预测值 (\hat{y}) 尽量接近真实值 (y)。这个“尽量接近”怎么量化我们一般用均方误差MSE作为损失函数[ L(w,b) \frac{1}{m}\sum_{i1}^{m}(\hat{y}^{(i)} - y^{(i)})^2 ]为什么用平方而不是绝对值因为平方误差是凸函数有唯一全局最小值方便用梯度下降法求解。绝对值误差虽然对异常值更鲁棒但在零点不可导优化起来麻烦很多。这个选择本身就是机器学习里“模型设计要兼顾数学性质和实际需求”的典型案例。1.2 实验任务与数据集选择山东大学这个实验一般会提供波士顿房价数据集如果新版课程没有内置数据可以用sklearn的load_boston或者去UCI找原始版本。这个数据集有506个样本13个特征非常适合做回归入门实验。实验任务通常包括几个层次数据探索与可视化、手写线性回归模型、评估模型效果、对比不同实现方式。有些版本还会要求你加入正则化项或者尝试多项式特征扩展我当时做的就是基础版加多项式回归对比。我觉得这个实验最有价值的地方在于数据量不大506个样本特征维度适中13维计算速度快非常适合你反复跑实验、修改参数、观察结果变化。如果一上来就上一个百万级数据的大项目你根本没法调试。2. 动手之前先把数据看清楚我见过太多人拿到数据集直接开训结果模型效果差得出奇又不知道问题出在哪。数据探索这一步省掉后面就要花十倍时间补坑。2.1 数据探索与预处理波士顿房价数据集的13个特征包括特征含义潜在影响CRIM城镇人均犯罪率高犯罪率地区房价通常偏低ZN占地超过25万平方英尺的住宅用地比例与社区规划有关INDUS非零售商业用地比例工商业发达地区不一定宜居CHAS是否临河虚拟变量景观因素NOX一氧化氮浓度环境污染指标RM每栋住宅平均房间数直接影响居住面积AGE1940年以前建成的自住房比例房龄影响DIS到波士顿就业中心加权距离通勤因素RAD径向高速公路可达性指数交通便利程度TAX每万美元房产税率持有成本PTRATIO师生比例教育资源B黑人比例相关指标社会因素这个特征在后续研究中常被质疑LSTAT低收入人群比例社会经济地位注意B这个特征的原始定义在今天看来有争议你在做数据可视化分析的时候可以提一嘴这属于数据集的背景知识但正式的模型分析不用围绕它过度展开。第一步永远是检查数据完整性。波士顿房价数据集比较干净没有缺失值但你要养成习惯拿到数据先执行df.isnull().sum()看看有没有空值。如果有处理方法一般是删除、填充均值/中位数或者用插值法。第二步是看数据分布。我习惯用seaborn的pairplot画几个关键特征和目标值的关系图你会发现RM房间数和LSTAT低收入人群比例跟房价的相关性特别明显一个是正相关一个是负相关。这一步能帮你建立直观感受后面做特征工程时心里有底。第三步是检查量纲差异。注意一下CRIM的数值范围是0到88左右而TAX的范围是187到711PTRATIO是12.6到22。不同特征的数值范围差了上百倍直接影响梯度下降的收敛速度。这就是为什么后面要做特征缩放下面会详细说。2.2 特征相关性分析我建议每个同学都画一张相关系数热力图这一步对理解模型帮助巨大。你会发现RM和MEDV房价的相关系数大约是0.7最强的正相关。LSTAT和MEDV的相关系数大约是-0.74最强的负相关。特征之间也存在强相关比如RAD和TAX的相关系数高达0.91这意味着它们携带的信息高度重复后续做特征选择时可以考虑只保留其中一个。为什么相关系数热力图重要因为线性回归有一个基本假设特征之间不应该存在严重的多重共线性。如果两个特征高度相关模型给它们分配权重时会出现不稳定的情况——可能一个权重大、一个权重小但换一组数据结果就完全反过来了模型泛化能力会受影响。对于基础实验我们通常不用太纠结多重共线性但你可以做一个加分项在实验报告里分析一下RAD和TAX的共线性问题并使用适当的处理方式比如去掉一个对比前后效果。这会让老师觉得你真的理解了这个模型。3. 核心实现从零写线性回归好了数据看完了终于到了核心环节。我强烈建议这个实验的前半部分一定要手写实现不要直接调sklearn。只有自己推一遍公式、写一遍代码你才能真正理解梯度下降和损失函数到底是怎么回事。3.1 数学原理解析线性回归的求解有两种经典方式正规方程和梯度下降。正规方程基于最小二乘法直接对损失函数求导并令导数为零解出最优权重[ w (X^T X)^{-1} X^T y ]这个方法的优点是一步到位不用调学习率不用迭代。缺点是计算复杂度是 (O(n^3))n是特征数当特征维度很高时计算代价巨大。波士顿房价只有13个特征用小数据演示正规方程完全没问题。梯度下降是另一种思路。它把损失函数想象成一个山谷你要从某个随机位置出发沿着最陡峭的下坡方向一步步走到谷底。每一步的方向是损失函数对参数的负梯度步长就是学习率。核心更新公式[ w_j : w_j - \alpha \frac{\partial L}{\partial w_j} ][ \frac{\partial L}{\partial w_j} \frac{1}{m} \sum_{i1}^{m}(\hat{y}^{(i)} - y^{(i)}) x_j^{(i)} ]这里的 (\alpha) 就是学习率决定了每一步走多远。走得太小收敛慢走得太快可能在谷底附近来回震荡甚至发散。我建议把两种方法都实现一遍对比结果。你会发现正规方程直接得到精确解析解梯度下降则是一个逐渐逼近的过程。3.2 梯度下降法实现下面给你一份完整可跑的实现代码。我用面向对象的方式封装了一个线性回归类方便复用import numpy as np class LinearRegression: def __init__(self, lr0.01, n_iterations1000, methodgd): self.lr lr self.n_iterations n_iterations self.method method self.weights None self.bias None self.loss_history [] def fit(self, X, y): m, n X.shape # 初始化参数 self.weights np.zeros(n) self.bias 0.0 # 正规方程求解 if self.method normal: X_with_bias np.c_[np.ones(m), X] theta np.linalg.inv(X_with_bias.T X_with_bias) X_with_bias.T y self.bias theta[0] self.weights theta[1:] return self # 梯度下降求解 for i in range(self.n_iterations): y_pred X self.weights self.bias error y_pred - y # 计算梯度 dw (1/m) * (X.T error) db (1/m) * np.sum(error) # 更新参数 self.weights - self.lr * dw self.bias - self.lr * db # 记录损失 loss np.mean(error ** 2) self.loss_history.append(loss) # 每100次迭代打印一次损失 if (i1) % 100 0: print(fIteration {i1}: loss {loss:.4f}) return self def predict(self, X): return X self.weights self.bias def score(self, X, y): y_pred self.predict(X) u np.sum((y - y_pred) ** 2) v np.sum((y - np.mean(y)) ** 2) return 1 - u / v注意一个细节我在代码里用了X self.weights这种矩阵乘法写法而不是用for循环逐个特征求梯度。在实际工程项目中向量化计算是所有数值计算库的基本优化手段效率远高于循环写法。这也是为什么numpy在机器学习领域这么重要——它把底层向量化计算封装好了你只需要关心数学表达式怎么写。3.3 正规方程实现正规方程的实现更简洁核心就一行theta_best np.linalg.inv(X_b.T X_b) X_b.T y其中X_b是在原始特征前面加了一列全1的矩阵用来对应偏置项。但要注意np.linalg.inv在矩阵不可逆时会抛异常更稳健的做法是用np.linalg.pinv伪逆theta_best np.linalg.pinv(X_b.T X_b) X_b.T y伪逆的好处是即使特征之间存在多重共线性矩阵接近奇异它依然能给出一个合理的解。我在实验对比中发现正规方程算出来的权重和梯度下降收敛后的权重高度一致差距基本在10的负6次方级别这验证了两种方法的正确性。这里有个有意思的对比值得在报告里写正规方程是一次性计算出全局最优解而梯度下降是渐进逼近但两者的结果几乎相同。这说明只要学习率和迭代次数设置合理梯度下降完全可以收敛到接近解析解的位置。3.4 学习率对收敛的影响学习率是整个实验里最值得玩味的超参数。我建议你做一个实验把这几个学习率分别代入梯度下降观察损失函数的收敛曲线学习率收敛行为原因分析0.001收敛极慢3000次迭代还不够步长太小参数每次只移动一点点0.01收敛正常约800次后趋于平稳步长合适能快速下降但不会震荡0.1前期快速下降后期在最小值附近震荡步长偏大无法精确收敛到最优点1.0损失直接爆炸成NaN步长太大参数更新越过山谷梯度越来越大我实测过在未做特征缩放的波士顿房价数据上学习率设置为0.01时损失函数需要约800次迭代才慢慢收敛设置为0.001时需要几千次设置为0.1的时候前100次迭代损失下降很猛但之后在某个值附近来回跳动无法继续下降。设置为1.0以上抱歉直接NaN。这也是为什么特征缩放如此重要。在做特征缩放之后你会发现即使学习率设置为0.1模型也能快速且稳定地收敛。缩放的本质是把所有特征的数值范围统一到差不多的量级这样损失函数的地形接近一个圆形碗梯度指向碗底的方向更准确更新参数时不会出现某个维度走太快、另一个维度走太慢的尴尬局面。4. 评估、可视化与结果调优模型训练出来了怎么评估好不好实验报告里需要哪些图这是很多同学忽略但老师非常看重的地方。4.1 模型评估指标回归任务的核心评估指标有三个MAE、MSE、R²。MAE平均绝对误差预测值和真实值差的绝对值的平均。优点是对异常值不敏感解释性强单位跟目标变量一致。MSE均方误差预测值和真实值差的平方的平均。因为平方的存在大误差会得到更大的惩罚所以它对异常值比较敏感。R²决定系数表示模型解释了目标变量多少比例的方差。取值范围通常在0到1之间越接近1说明模型拟合效果越好。举个例子假设真实房价是20万、22万、25万模型预测是21万、23万、24万那么MAE就是1万MSE大约是2.33万²R²则根据模型的解释力计算。R²的计算公式[ R^2 1 - \frac{\sum(y_i - \hat{y}_i)^2}{\sum(y_i - \bar{y})^2} ]分子是模型预测的残差平方和分母是目标变量的总方差。如果模型预测完美分子为0R²等于1。如果模型预测跟直接用均值预测差不多R²会接近0。如果模型比用均值预测还差R²甚至可能为负。波士顿房价数据集上标准线性回归的R²大约在0.71到0.74之间这属于相当不错的效果考虑到特征本身噪音不小。如果你用多项式特征扩展R²能提到0.8以上但要小心过拟合风险。4.2 可视化分析实验报告里至少要有三张图损失下降曲线横轴是迭代次数纵轴是损失值。这条曲线应该呈现“快速下降然后趋于平稳”的形状。如果曲线锯齿状上下跳动说明学习率太大如果一直平缓下降没有平台期说明迭代次数不够或者学习率太小。预测值vs真实值散点图横轴是真实房价纵轴是预测房价。理想情况下所有点应该集中在yx这条直线附近。如果出现系统性偏差比如低房价区域预测偏高、高房价区域预测偏低说明模型存在欠拟合或者非线性关系没有捕捉到。残差分布图横轴是预测值纵轴是残差预测值减真实值。理想的残差图应该呈现随机分布在0附近的形态没有明显的漏斗状或曲带状模式。如果残差随着预测值增大而增大说明模型存在异方差性——不同价格区间内的误差不稳定。这三张图不仅是为了交作业它们是你判断模型状态的重要工具。我调模型的时候习惯先画残差图一眼就能看出问题出在哪。4.3 特征缩放与多项式扩展特征缩放常用两种方法标准化Standardization和归一化MinMaxScaler。标准化把数据转化为均值为0、标准差为1的标准正态分布[ x_{scaled} \frac{x - \mu}{\sigma} ]归一化把数据缩放到[0,1]区间[ x_{scaled} \frac{x - x_{min}}{x_{max} - x_{min}} ]对于线性回归标准化更常用因为它在缩放后保持了数据的分布形状且对存在异常值的特征更鲁棒。我用标准化处理波士顿房价数据后梯度下降的收敛速度显著提升学习率可以放心调大一些。多项式扩展是一个很有意思的方向。线性回归假设变量之间是线性关系但真实世界往往不是这样。比如房间数和房价之间可能存在边际递减效应——房间数从2加到3房价涨很多但从8加到9涨幅就很小了。这时候线性假设就不够用你可以增加特征的高次项来拟合这种非线性关系。from sklearn.preprocessing import PolynomialFeatures poly PolynomialFeatures(degree2, include_biasFalse) X_poly poly.fit_transform(X)度数越高模型表达能力越强但也越容易过拟合——训练集效果很好测试集一塌糊涂。我建议你实验一下degree1、2、3三种情况画出训练集和测试集R²的对比图表直观感受偏差-方差权衡。5. 常见问题与排查技巧实录这个章节是我最想分享的。以下这些问题几乎每个做实验的同学都会遇到我把排查思路给你列出来能省下半天调试时间。5.1 训练震荡发散怎么办现象损失函数输出NaN或者一开始很小但迅速飙升到天文数字。原因学习率过大梯度更新跨度过大导致参数越过最优点误差震荡加剧最终溢出为NaN。排查顺序先检查学习率从0.01降到0.001如果问题仍在先做特征标准化看是否恢复正常最后检查输入数据是否有缺失值或者无穷大值。我遇到过一次很诡异的情况数据里某一行特征数值特别大导致矩阵乘法结果溢出。用np.isinf()扫一遍数据就能排查。5.2 训练集效果差预测全是均值水平现象R²接近0模型预测值基本都在一个固定值附近。原因模型欠拟合。可能是特征太少、模型表达能力不足或者训练没有收敛梯度下降还停在半路。排查顺序先看损失曲线是否还在下降如果还在下降说明需要加大迭代次数或者学习率。然后尝试加入多项式特征提升模型容量。最后检查特征是否做完标准化未标准化的特征会让损失函数地形变得非常狭长梯度下降很难前进。5.3 训练集R²比测试集R²高一大截现象训练集R²超过0.85测试集只有0.6左右。原因过拟合。模型把训练集噪声也学进去了失去了泛化能力。排查顺序降低多项式扩展的度数或者增加样本量。也可以尝试正则化岭回归、Lasso在损失函数中增加参数惩罚项限制模型权重过大。具体可以看第4小节的正则化实验部分。5.4 特征之间的量纲差异过大现象梯度下降收敛极慢损失函数下降像蜗牛爬。原因没有做特征缩放。数值范围大的特征在计算梯度时会主导更新方向其他特征几乎学不到东西。处理方式对所有特征做标准化处理。记住一个原则梯度下降类模型几乎都需要特征缩放树模型则不需要。6. 正则化与交叉验证实验扩展基础实验做完如果你想在报告里加分我建议增加正则化对比实验。这是机器学习非常核心的概念也是很多面试都会问的考点。6.1 正则化原理正则化的思路是在损失函数后面加一个惩罚项限制模型的权重不能太大。为什么要这么做因为权重过大通常意味着模型过度依赖某些特征对训练数据的噪声过于敏感导致过拟合。岭回归L2正则化的损失函数[ L MSE \lambda \sum_{j1}^{n} w_j^2 ]Lasso回归L1正则化的损失函数[ L MSE \lambda \sum_{j1}^{n} |w_j| ]区别在于L2正则化把权重向0压缩但不会正好变成0L1正则化则会让一部分权重精确变成0相当于自动做特征选择。当你有几十个特征担心某些特征没用时Lasso会帮你筛掉它们。6.2 手动实现岭回归用梯度下降实现岭回归非常简单在梯度更新时加一个惩罚项的导数class RidgeRegression: def __init__(self, lr0.01, n_iterations1000, lambda_val0.1): self.lr lr self.n_iterations n_iterations self.lambda_val lambda_val def fit(self, X, y): m, n X.shape self.weights np.zeros(n) self.bias 0.0 for _ in range(self.n_iterations): y_pred X self.weights self.bias error y_pred - y # 梯度计算加了正则项 dw (1/m) * (X.T error) (self.lambda_val / m) * self.weights db (1/m) * np.sum(error) self.weights - self.lr * dw self.bias - self.lr * db return self实现sklearn版本对比代码也可以参考但手动实现一遍会让你对惩罚项到底怎么起作用有更直觉的理解。6.3 交叉验证选择正则化强度你肯定要问(\lambda) 设多少合适答案是用交叉验证选。把训练数据分成k份每次拿k-1份训练、1份验证轮流做k次取平均验证分数最高的 (\lambda)。我常用的做法是设置一个候选列表比如[0.001, 0.01, 0.1, 1.0, 10.0]分别训练模型并记录交叉验证R²画一张“R²随lambda变化”的图。你会发现lambda太小正则化效果不明显lambda太大模型被压得过于简单欠拟合了。最佳点在中间某个位置。这种网格搜索的思路在你以后做深度学习调参时也会反复用到。机器学习实验的意义就在于此——把最底层的套路练熟了上层用的任何花哨模型都万变不离其宗。7. 写在最后的实操总结按照山东大学这个实验的评分标准一份完整的实验报告至少应该包含数据探索过程、模型原理推导、代码实现与实验结果、不同参数对比分析、问题与解决方案。我带的学弟学妹里凡是认真做了梯度下降实现和参数对比实验的最后对机器学习的理解深度明显比直接调库的同学高一个档次。最后再分享一个小技巧做参数实验时把每次运行的关键结果学习率、迭代次数、R²、MSE记录在表格里。别嫌麻烦写实验报告的时候你就知道这个习惯多香了。而且我发现一个省事方法——每次调整完参数以后顺手写一行注释说明这次改了什么避免跑了一堆实验后忘了哪个参数组合对应哪个结果。这个习惯后来做深度学习调参时救了我无数次。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进