线性回归三种实现:OLS、批量梯度下降与SGD对比实战 简介以红酒数据集为切入点的线性回归实战资源面向刚接触机器学习、希望掌握回归分析实操的Python学习者也适合用于课程实验或项目练手。压缩包共6个文件3个Python脚本分别基于普通最小二乘法、批量梯度下降法与随机梯度下降法实现线性回归便于对照理解不同优化策略的差异2个CSV文件存放红葡萄酒与白葡萄酒的化学成分及质量评分另有1个辅助文件整包仅89KB非常轻量。目前已有1715人学习下载。配套代码和示例数据能够支撑从数据加载、清洗、探索性可视化如散点图、箱线图、特征选择到模型拟合、R²评估、残差分析、新样本预测的完整实践链路同时可借助模型系数解读各化学成分对酒品质的影响方向与强弱并对比三种算法的参数更新过程与收敛表现为后续学习岭回归、多项式回归打下扎实基础。1. 一个红酒数据集三个线性回归实现为什么说这不是重复造轮子拿到1.线性回归.zip的时候我第一反应也是一个红酒数据集而已为什么要塞linear_regression_ols.py、linear_regression_gd.py、linear_regression_sgd.py三个文件直接调 sklearn 的LinearRegression不就行了吗后来带一个学弟做课设才想明白——课程设计要的不是「跑出一个 R²」而是「把线性回归的三种求解路径讲清楚」。OLS 是闭式解GD 是批量迭代SGD 是单样本迭代三者处理的是同一个回归目标但数值行为完全不同。这个压缩包实际上是一份「算法对比实验」的完整素材目标是用红酒的化学成分预测质量评分同时把从数据清洗到模型评估的完整流程走一遍。适合两类人正在做回归类课设、需要同时展示理论推导和代码实现的学生以及想快速搭一个多算法对比 Demo 的从业者。接下来我按这套资源实际能落地的顺序把它拆成一份可以直接照着做的方案。2. 数据准备与探索先弄懂红酒数据集再谈建模2.1 加载数据与第一步清洗压缩包里给了winequality-red.csv和winequality-white.csv两个文件还有.winequality-red.csv.swp这个临时文件Vim 编辑时留下的直接忽略。核心操作对象是红葡萄酒那份白葡萄酒可以作为对照组跑同样的流程。数据加载用 pandas 的read_csv()就能读进来但读取之前建议先确认分隔符——这份数据集用的是分号不是逗号直接read_csv(winequality-red.csv)会把整行读成一个字段。正确写法是这样import pandas as pd df pd.read_csv(winequality-red.csv, sep;) print(df.head()) print(df.info()) print(df.isnull().sum())读取时指定sep;是这份数据最容易踩的第一个坑。df.info()会告诉你 11 个特征列加 1 个质量列共 1599 行特征包括固定酸度、挥发性酸度、柠檬酸、残糖、氯化物、游离二氧化硫、总二氧化硫、密度、pH、硫酸盐、酒精。isnull().sum()检查缺失值——很多网页教程会让你「删除缺失值」但实际这份数据集没有缺失值这一步的真正目的是确认数据完整性而不是机械地执行清洗动作。2.2 探索性分析看哪些特征真的和质量相关EDA 阶段用 matplotlib 和 seaborn 画三张图基本就够课设展示了特征分布直方图、特征与质量的散点图、特征间的相关性热力图。我的做法是先画相关性矩阵把和目标变量quality相关性绝对值超过 0.3 的特征圈出来。import matplotlib.pyplot as plt import seaborn as sns corr df.corr() plt.figure(figsize(12, 10)) sns.heatmap(corr, annotTrue, fmt.2f, cmapRdBu) plt.title(Feature Correlation Matrix) plt.tight_layout() plt.show() # 按与 quality 的相关性排序看哪些特征值得进模型 corr_target corr[quality].abs().sort_values(ascendingFalse) print(corr_target)通常画出来会发现酒精alcohol和硫酸盐sulphates与质量正相关挥发性酸度volatile acidity与质量负相关这几个是线性回归里最值得进模型的特征。密度density和质量相关度高但密度和酒精之间相关性也很强同时放进去会引入多重共线性这个在选特征时要留意。残糖这类特征在红酒里分布非常偏可视化之后会发现它和质量的关系基本不是线性的硬塞进线性回归只会增加噪声。2.3 特征选择的取舍不是相关性高就一定进模型EDA 之后进入特征选择这一步我会让学弟记住一句话「相关性高不等于线性关系好更不等于适合放进线性模型。」对于这个数据集一个比较稳的基线做法是选 4 到 5 个特征alcohol、volatile acidity、sulphates、citric acid再加一个total sulfur dioxide做对照。其中volatile acidity对质量是负向影响系数会是负的这个在答辩时能对应上「参数解读」这个知识点。features [volatile acidity, citric acid, sulphates, alcohol] X df[features] y df[quality] from sklearn.preprocessing import StandardScaler scaler StandardScaler() X_scaled scaler.fit_transform(X)我一般会建议先标准化再做后续建模原因后面在梯度下降那张会集中说。如果你在课设里想展示「特征工程」这个环节可以在这一步加一个对照实验只用alcohol一个特征的 R²和用四个特征的 R²两张结果对比放幻灯片里比任何文字描述都有说服力。3. 三种线性回归实现从 OLS 闭式解到两种梯度下降3.1 OLS 实现正规方程求解一行矩阵运算出结果linear_regression_ols.py走的是最小二乘的闭式解路线不依赖迭代。线性回归的目标是最小化残差平方和J(θ) (1/2m) ∑(h(x⁽ⁱ⁾) - y⁽ⁱ⁾)²对这个目标函数求导并令导数为零可以直接得到参数的最优解θ (XᵀX)⁻¹Xᵀy用 numpy 实现非常简洁import numpy as np import pandas as pd def ols_solve(X, y): # 加一列 1 作为截距项 X_with_intercept np.hstack([np.ones((X.shape[0], 1)), X]) # 正规方程theta (X^T X)^-1 X^T y theta np.linalg.inv(X_with_intercept.T X_with_intercept) X_with_intercept.T y return theta df pd.read_csv(winequality-red.csv, sep;) X df[[alcohol, volatile acidity, sulphates]].values y df[quality].values theta ols_solve(X, y) print(Intercept:, theta[0]) print(Coefficients:, theta[1:])这段代码里np.linalg.inv是求逆矩阵是矩阵乘法。正规方程的好处是一步到位、没有超参数坏处是当特征维度很高时求逆的计算量是 O(n³)会变得很慢。在红酒数据集这种 11 个特征、1600 个样本的规模下完全没问题但你要在答辩时能说清楚「什么场景下 OLS 闭式解不可用」——特征数上万时求逆矩阵的代价就让人难以接受了那时候得换梯度下降。3.2 批量梯度下降实现手动推导梯度观察收敛过程linear_regression_gd.py用的是批量梯度下降每一步用全部样本计算梯度然后更新参数。梯度推导是课设答辩的高频考点我建议在代码注释里把推导过程也写上import numpy as np import pandas as pd import matplotlib.pyplot as plt def gradient_descent(X, y, lr0.01, epochs1000): m, n X.shape X_with_intercept np.hstack([np.ones((m, 1)), X]) theta np.zeros(n 1) loss_history [] for epoch in range(epochs): # 预测值 y_pred X_with_intercept theta # 梯度 (1/m) * X^T (y_pred - y) gradient (1 / m) * (X_with_intercept.T (y_pred - y)) # 参数更新 theta - lr * gradient # 记录损失用于画收敛曲线 loss np.mean((y_pred - y) ** 2) / 2 loss_history.append(loss) if epoch % 100 0: print(fEpoch {epoch}, Loss: {loss:.4f}) return theta, loss_history这里梯度公式的推导过程是对 J(θ) 求偏导得到 (1/m) * Xᵀ(Xθ - y)。每个 epoch 更新一次参数learning rate 这里取 0.01。运行后你会看到 loss 在下降但很慢这是因为特征没有标准化。把lr调大到 0.1 可能就发散了——这就是为什么我在 2.3 节强烈建议建模前先做标准化标准化之后 0.01 的学习率在 1000 轮内就能收敛到和 OLS 一致的结果。3.3 随机梯度下降实现单样本更新看噪声与收敛的博弈linear_regression_sgd.py的逻辑和 GD 几乎一样唯一区别是每次迭代只随机选一个样本计算梯度并更新参数。实现上要注意每个 epoch 内要遍历所有样本且 epoch 之间要打乱顺序否则模型会学到数据排列顺序里的虚假模式。import numpy as np import pandas as pd def stochastic_gradient_descent(X, y, lr0.01, epochs50): m, n X.shape X_with_intercept np.hstack([np.ones((m, 1)), X]) theta np.zeros(n 1) loss_history [] for epoch in range(epochs): # 每个 epoch 打乱样本顺序 indices np.random.permutation(m) for i in indices: xi X_with_intercept[i:i1] yi y[i:i1] y_pred xi theta gradient xi.T (y_pred - yi) theta - lr * gradient # 计算整个 epoch 的平均损失 y_pred_all X_with_intercept theta loss np.mean((y_pred_all - y) ** 2) / 2 loss_history.append(loss) print(fEpoch {epoch}, Loss: {loss:.4f}) return theta, loss_history注意两个细节一是这里梯度没有除以 m因为只有一个样本所以是 xi.T * (y_pred - yi)二是学习率通常比 GD 要小因为单样本梯度噪声大步长太大容易在最优解附近震荡。实践中 SGD 的 loss 曲线不会像 GD 那样平滑下降而是带着抖动往下走。抖动不是 bug恰恰说明算法在探索。如果你想让曲线更平滑可以加一个学习率衰减每过一定 epoch 把学习率乘以 0.9 之类的系数这在代码里再加三行就能实现。3.4 三个模型的结果对比用同一份数据讲同一个结论三种方法最终得到的参数应该非常接近这是最好的验证方式。我在课设里一般会把这三种方式的输出汇总成一张对比表实现方式求解思路超参数迭代轮数参数结果R²OLS正规方程闭式解无一步到位θ_ols约 0.30GD全样本梯度下降lr0.01, epochs10001000θ_gd约 0.30SGD单样本梯度更新lr0.01, epochs5050×1599θ_sgd约 0.28R² 在 0.3 左右是这个数据集的正常水平——质量评分是 3 到 8 的离散整数受主观因素影响大能解释 30% 的变异已经不错了。如果有人做出 0.8 的 R²那大概率是数据泄露或者数据划分出了问题。三种方法参数一致性这个结论要重点写进报告里它是「算法实现正确」最硬的证据。4. 避坑指南红酒回归里最容易翻车的五个问题4.1 学习率设置不当导致 loss 输出 nan现象跑linear_regression_gd.py前几轮 loss 还在下降突然变成nan或者直接爆成天文数字。原因学习率太大参数更新时跨过了最优点梯度越来越大最后数值溢出。红酒数据集特征量纲不一致时这个问题尤其明显比如density是 0.99 量级residual sugar是 2 到 15 量级直接放进模型会让梯度方向被大数值特征主导。解决先StandardScaler标准化再训练或者把学习率从 0.01 降到 0.001 试试。标准化后0.01 在 1000 轮内稳定收敛不标准化0.01 在部分特征组合下就会炸。4.2 读数据时没有指定分隔符所有字段挤成一列现象df.head()显示只有一列整行数据用逗号或分号串在一起。原因winequality-red.csv使用的是分号分隔而 pandas 默认分隔符是逗号。这算这个数据集最经典的坑我见过好几个同学卡在这里。解决read_csv(winequality-red.csv, sep;)。读取后立刻print(df.shape)确认是 1599 行 × 12 列再继续往下走。4.3 用 sklearn 的 train_test_split 之前忘了打乱数据现象模型训练分数很高测试分数很低或者反过来。原因数据本身没有特殊排列顺序问题但如果直接按顺序切前 80% 训练、后 20% 测试碰到有些排序方式会让训练集合测试集分布不一致。标准做法是先train_test_split它内部会打乱数据。解决统一用from sklearn.model_selection import train_test_split然后train_test_split(X, y, test_size0.2, random_state42)。random_state设固定值保证每次运行结果一致——这个细节在答辩时很加分说明你关注可复现性。4.4 只报 R² 不看残差图现象R² 数值看着还行但画残差图发现明显的 U 形分布。原因R² 只衡量拟合优度不验证线性假设是否成立。如果残差随预测值增大呈现喇叭形或弯曲形说明模型结构有问题可能缺了非线性项或者存在异方差。解决模型评估阶段强制画residuals vs fitted图。如果残差图呈随机分布说明线性假设合理如果有明显趋势说明这个数据集某个特征可能需要做多项式变换比如把residual sugar取对数再放进模型。4.5 验证和优化时忽略了交叉验证现象训练集和测试集各跑一次R² 差不多就觉得模型没问题。原因单次划分的运气成分很大样本量只有 1599划分方式不同结果可能差 0.05 的 R²。解决用cross_val_score做 5 折交叉验证from sklearn.model_selection import cross_val_score from sklearn.linear_model import LinearRegression model LinearRegression() scores cross_val_score(model, X_scaled, y, cv5, scoringr2) print(CV scores:, scores) print(Mean R2:, scores.mean(), Std:, scores.std())交叉验证的均值和标准差比单次 R² 更有说服力。如果方差太大超过 0.05说明模型不稳定需要回头检查特征选择。5. 模型评估与验证R²、残差图和交叉验证怎么组合使用5.1 sklearn 线性回归与手写实现的对照压缩包里虽然没有直接给出 sklearn 版本的.py文件但作为对照实验是非常值得补上的一步。用sklearn.linear_model.LinearRegression跑一遍同样的特征拿它的结果和前面三个手写实现对比from sklearn.linear_model import LinearRegression from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test train_test_split( X_scaled, y, test_size0.2, random_state42 ) model LinearRegression() model.fit(X_train, y_train) print(Train R2:, model.score(X_train, y_train)) print(Test R2:, model.score(X_test, y_test)) print(Intercept:, model.intercept_) print(Coefficients:, model.coef_)sklearn 底层默认使用 SVD 求解最小二乘问题数值稳定性比直接求逆更好结果和手写 OLS 应该几乎一致。如果你的手写 OLS 和 sklearn 结果差很多回去检查特征是否标准化或者矩阵是否有多重共线性。这一步的目的是证明「手写实现没有 bug」不是证明谁更厉害。5.2 残差分析判断线性假设是否成立建模完成后必须做的三个可视化残差 vs 预测值散点图、残差直方图、QQ 图。第一个用来检查线性关系和异方差第二个用来检查残差是否近似正态第三个用来确认正态性假设。import matplotlib.pyplot as plt y_pred model.predict(X_test) residuals y_test - y_pred plt.figure(figsize(8, 5)) plt.scatter(y_pred, residuals, alpha0.5) plt.axhline(y0, colorred, linestyle--) plt.xlabel(Predicted Quality) plt.ylabel(Residuals) plt.title(Residuals vs Fitted Values) plt.show()红酒质量是 3 到 8 的离散整数所以你会看到残差图上有几条水平的「线」这是离散目标的正常表现不用慌。重点看残差点是否均匀分布在零线两侧、有没有明显的漏斗形。如果出现漏斗形说明预测值越大误差越大可以考虑对目标变量做处理或者尝试岭回归。5.3 验证阶段的优化方向从线性回归走向正则化如果交叉验证的 R² 不理想通常的优化路径有三条加特征、加多项式项、换正则化模型。在这个数据集上一个值得做的实验是岭回归对比from sklearn.linear_model import Ridge from sklearn.model_selection import cross_val_score ridge Ridge(alpha1.0) ridge_scores cross_val_score(ridge, X_scaled, y, cv5, scoringr2) print(Ridge Mean R2:, ridge_scores.mean())岭回归在特征间存在多重共线性时比普通线性回归更稳定。红酒数据里density和alcohol相关性高citric acid和fixed acidity相关性也高用岭回归做对照实验能自然引出「为什么要正则化」这个课设加分点。我在指导 A 同学做这套实验时他加了岭回归对比后报告的完整性明显上了一个档次——因为验收老师关心的不只是「你会跑线性回归」而是「你知道线性回归的局限在哪里」。6. 验收与答辩用一份可复现的实验报告收尾6.1 验收清单拿到这份方案怎么自检课设验收时老师通常不会一行行看代码而是看三样东西能不能复现、有没有图、能不能解释。我建议你自己先按下面这个清单过一遍验收项检查方法通过标准数据装载运行读取代码打印 shape1599 × 12无 NaN三种算法可跑通分别运行三个.py文件不报错输出收敛的 loss参数一致性对比 OLS 与 GD 的系数系数相差小于 0.01可视化齐全检查图片文件有 EDA、收敛曲线、残差图至少三种评价指标完整查看终端输出有 R²、交叉验证均值和标准差可复现性固定 random_state 后重跑两次结果完全一致最后一项最容易忽略。所有涉及随机性的地方——train_test_split、SGD 的np.random.permutation——都要固定random_state或np.random.seed(42)。答辩现场老师可能会说「你当场再跑一遍」如果不固定随机种子第二次跑出来的 R² 和报告里对不上场面会非常尴尬。6.2 答辩高频问题与应答策略答辩时老师最爱问的四个问题答案其实都已经在你这份实验里了为什么选线性回归而不是其他模型答因为要建立化学成分与质量评分之间的线性关系线性回归的参数有可解释性——volatile acidity系数为负说明挥发性酸度越高质量越差这在酿酒场景里是符合常识的。OLS 和梯度下降的区别是什么答OLS 是闭式解一步求出全局最优梯度下降是迭代逼近需要调学习率和迭代轮数。数据量大时 OLS 求逆代价高梯度下降更实际数据量小时 OLS 更精确。为什么你的 R² 只有 0.3答因为质量评分是一个范围 3 到 8 的离散整数且含有品酒师主观因素。0.3 说明化学特征解释了质量变异的三成剩余变异来自主观因素和未纳入的非线性特征。这个回答比强行辩解「0.3 很高」要诚实且专业得多。SGD 的 loss 曲线为什么在抖动答因为每次只用一个样本计算梯度单样本梯度是对全样本梯度的一个有偏估计噪声导致抖动。可以通过学习率衰减来缓解但保留少量抖动说明算法仍在探索参数空间。6.3 一个值得保留的验收习惯这套项目我前后带过几轮课设最深的感受是能留下来的不是模型本身而是「每次改动都记录一次结果」的习惯。后来我给自己定了一条规矩——每个版本的代码跑完立刻在项目目录下写一个results.txt包含时间、特征列表、学习率、epochs、R²、交叉验证均值。这样答辩时老师问「你试过什么」你可以直接翻出记录说试过单特征酒精、四特征组合、五特征加二氧化硫R² 从 0.21 涨到 0.30加上岭回归后标准差从 0.04 降到 0.03。这个回答比任何空泛的「我做了很多实验」都有说服力。从那以后我每次做完一个回归实验都会强制让结果记录走在报告前面。希望这套红酒数据集的方案能帮你把课设从「跑通代码」推进到「讲清楚原理」祝顺利。本文还有配套的精品资源点击获取