资讯动态

红酒数据集线性回归实战:OLS、批量梯度下降与SGD对比

发布时间:2026/10/18 12:10:52 来源:尧图企业网站定制
简介面向数据分析与机器学习初学者的线性回归实战项目以经典红酒数据集为对象演示如何用Python完成从数据加载、清洗、可视化探索到特征选择、模型训练与评估的完整流程。压缩包内共6个文件包括3个Python脚本分别实现普通最小二乘、梯度下降与随机梯度下降三种建模方式、2个CSV格式的红白葡萄酒数据集以及1个辅助交换文件整体大小仅89KB轻量易用。已有1715人学习适合希望快速上手sklearn回归建模、理解线性回归原理及系数含义的读者。该压缩包目录简洁脚本与数据分离便于直接运行与对照修改通过它可获得可直接运行的代码、真实的葡萄酒质量数据集以及残差分析、R²评分、交叉验证等模型诊断与优化方法的实践示范便于对照学习与二次改进。1. 线性回归在红酒数据集上的三种实现从OLS走到梯度下降多数人卡在第二步很多初学者第一次接触线性回归是在红酒质量数据集上跑通一行LinearRegression().fit()然后看着 R² 不到 0.4 陷入沉思。这套资源包不像普通教程只给一个结果而是同时提供了 OLS、批量梯度下降、随机梯度下降三份独立脚本外加红、白两份标准 CSV。它能回答的不只是“怎么把模型跑出来”还包括“同一个线性回归三种求解方式在参数收敛、运行速度、最终精度上到底差多少”。如果你正在学习回归理论或准备课程实验这个包可以直接当脚手架用如果你已经会用 sklearn我更建议直接从 GD 和 SGD 脚本入手那份源码里有真正值得抠的细节。2. 红酒数据集和五份文件先摸清包里的东西再决定跑哪份脚本2.1 数据集的列结构与质量评分分布包里的winequality-red.csv是经典的红酒质量数据集共 1599 行、12 列。前 11 列是理化指标固定酸度、挥发酸度、柠檬酸、残糖、氯化物、游离二氧化硫、总二氧化硫、密度、pH、硫酸盐、酒精最后一列quality是品酒师给出的 0–10 整数评分在这个数据集中实际分布在 3 到 8 之间。另一份winequality-white.csv是白葡萄酒版本行数更多约 4898 行列结构完全一致。需要注意这份 CSV 的分隔符是分号;不是逗号直接pd.read_csv(winequality-red.csv)会把整行读成一列。第一次处理时我在这里翻过车白读了半天数据形状。用下面的代码可以正确加载并快速查看分布import pandas as pd df pd.read_csv(winequality-red.csv, sep;) print(df.shape) print(df.head()) print(df[quality].value_counts().sort_index())sep;是这里最关键的参数少写它df.shape会得到(1599, 1)而不是(1599, 12)。value_counts().sort_index()用来观察质量评分的频数分布你会发现评分集中在 5、6 两档两头的 3 和 8 很少这种不均衡会直接影响模型对极端评分的预测能力。后期做回归评估时RMSE 会被这些样本拉大但不代表模型完全不可用。白葡萄酒数据同样用分号分隔列名一致。两份数据可以共用一套特征处理逻辑但不要直接合并训练原因见第 5 章的避坑说明。2.2 三份脚本的分工OLS、GD、SGD 分别解决什么问题压缩包里的三份 Python 脚本实现了同一种模型、三种不同的求解路线这是这份资源最有价值的地方脚本求解方式更新频率适用场景收敛特点linear_regression_ols.py最小二乘法封闭解一次计算全部特征少、样本不过万的基线模型直接得到最优解无收敛过程linear_regression_gd.py批量梯度下降每轮全量样本算一次梯度理解优化过程、学习收敛曲线稳定下降但每轮成本高linear_regression_sgd.py随机梯度下降每个样本或小批量更新一次数据量大、内存受限快速但震荡需要调学习率OLS 脚本通常直接调sklearn.linear_model.LinearRegression底层走的是最小二乘的闭式解或 SVD 分解。它的优点是稳定、无需调参、结果可复现缺点是无法体现“训练”过程也不适合特征维度极高的场景。GD 脚本把梯度下降的每一步都显式写出来适合对照公式看权重怎么逐步逼近最优值。SGD 脚本则把更新频率打散每个样本都拉动一次权重配合shuffle后在数据量大的环境下内存压力更小。我在教学和自学的过程中通常建议先把 OLS 结果当作“标准答案”存好再用 GD 和 SGD 去逼近它。如果手写算法和 sklearn 的系数差距大到不可接受问题几乎都出在特征标准化或学习率上而不是推导本身。2.3 运行环境与依赖安装三份脚本都基于 Python 3 编写依赖库包括numpy、pandas、matplotlib、scikit-learn。如果本地环境没装齐可以集中安装pip install numpy pandas matplotlib scikit-learn装好之后先跑 OLS 脚本验证环境python linear_regression_ols.py脚本通常会输出训练集、测试集的 R² 和 RMSE以及各特征的回归系数。如果输出正常再逐个运行 GD 和 SGD。环境上只有一个小坑包里的.winequality-red.csv.swp是 vim 编辑时留下的交换文件不是数据文件直接忽略或删除即可别把它读进 pandas否则会得到一堆乱码。3. 用 OLS 建立红酒质量的第一个基线sklearn 一行 fit 背后的四个细节3.1 从 CSV 到训练矩阵特征选择与数据分割OLS 脚本的第一步是从 CSV 构建特征矩阵X和目标向量y。这里涉及到回归建模中的一个基本决策是全量使用 11 个特征还是只挑几个相关性高的特征。线性回归对特征数量并不敏感但在样本量只有 1599 的情况下特征全上通常不会出大问题如果想做对照实验也可以只保留alcohol和volatile acidity两个特征模型更简单残差图更容易解读。标准做法是先做一次训练测试切分保持评估口径一致import pandas as pd from sklearn.model_selection import train_test_split from sklearn.linear_model import LinearRegression from sklearn.metrics import mean_squared_error, r2_score df pd.read_csv(winequality-red.csv, sep;) X df.drop(quality, axis1) y df[quality] X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42 )test_size0.2表示拿 20% 的数据做测试剩下 80% 训练random_state42固定随机种子保证每次运行切分一致。这个种子值可以随便改但建议在实验记录里固定下来否则不同跑次之间的评估指标不可比。drop(quality, axis1)是把质量列从特征矩阵中拿掉axis1表示按列删除漏写的话会连目标值一起带进模型。3.2 拟合与评估R² 之外还要看 RMSE拟合过程只有两行model LinearRegression() model.fit(X_train, y_train) y_pred model.predict(X_test) print(R2:, r2_score(y_test, y_pred)) print(RMSE:, mean_squared_error(y_test, y_pred, squaredFalse))fit是训练过程OLS 在内部通过最小二乘求解权重。预测完成后用r2_score看决定系数用mean_squared_error的squaredFalse参数直接输出 RMSE均方根误差单位与质量评分一致。在这个数据集上R² 通常在 0.3 到 0.4 之间RMSE 大约 0.65 到 0.75。很多初学者看到这个 R² 会怀疑写错了其实这是正常现象红酒质量评分是人工主观打分本身噪声大且绝大多数特征与质量之间是弱线性关系。与其盯着 R² 的绝对值不如把它作为后续特征工程的对照基线。如果你用白葡萄酒数据跑R² 可能略高一点这取决于评分分布的差别。3.3 系数解读volatile acidity 和 alcohol 是影响质量的两个关键方向模型训练完系数存在coef_里。把它们呈现为特征名对应的序列coef pd.Series(model.coef_, indexX.columns) print(coef.sort_values(ascendingFalse))在我的多次运行结果中alcohol的系数通常是最大的正值说明酒精含量越高质量评分偏向越高volatile acidity是明显的负值挥发酸度越高质量越差这符合酿造常识。density的系数符号有时不稳定因为它和其他特征存在相关性。这里要特别强调一点由于各特征的单位不同density在 0.99 附近波动total sulfur dioxide在几十到几百之间系数绝对值大小不能直接用来比较特征重要性。要比较重要性必须先把特征标准化。OLS 脚本如果没做标准化系数解读只能看符号和相对趋势不能比绝对大小。3.4 三种脚本的输出对齐为什么建议先跑 OLS我拿到这个包后的习惯是先跑一次 OLS 脚本把输出保存为ols_baseline.json再跑 GD 和 SGD用同一份测试集对比三个模型的 R² 和 RMSE。为什么要先跑 OLS因为它是标准答案的锚点。梯度下降如果调好了最终的损失函数值应该逼近 OLS 的目标函数最小值如果两者差距大说明梯度计算、学习率或特征缩放有问题而不是算法本身有毛病。这种“先锚定再复现”的实验步骤比直接跳到优化算法更能帮你定位问题。4. 手写梯度下降与随机梯度下降从损失函数到更新公式的完整落地4.1 批量梯度下降的数学基础与代码骨架线性回归的目标是最小化均方误差L(w) 1/n * sum(sample i: (y_i - x_i^T w)^2)对权重向量w求梯度得到grad L(w) -2/n * X^T (y - Xw)更新规则就是沿负梯度方向前进w - w - lr * grad这个公式看起来简单但落地时有两个容易写错的地方一是截距项要作为一列 1 拼进X否则模型没有偏置二是矩阵形状要对齐X^T和(y - Xw)的维度必须匹配。下面的代码是一个可靠的批量梯度下降实现import numpy as np def mse_loss(y_true, y_pred): return np.mean((y_true - y_pred) ** 2) def compute_gradient(X, y, w): n len(y) grad -2 / n * X.T (y - X w) return grad def gradient_descent(X, y, lr0.01, epochs1000): n_features X.shape[1] w np.zeros(n_features) history [] for epoch in range(epochs): grad compute_gradient(X, y, w) w - lr * grad loss mse_loss(y, X w) history.append(loss) if epoch % 100 0: print(fepoch {epoch}, loss {loss:.4f}) return w, historycompute_gradient里X.T (y - X w)是向量化计算一次得到所有特征的梯度比 for 循环快得多也符合 NumPy 的写法。history列表记录每一轮的损失值用于后面画收敛曲线。lr是学习率epochs是迭代轮数这两个参数直接决定收敛质量。4.2 特征标准化与学习率的配合直接用原始特征跑这个 GD大概率会发散或收敛极慢。原因很简单红酒数据里alcohol是 9 到 14 的尺度density是 0.99 左右的尺度total sulfur dioxide是 6 到 289 的尺度各维度量纲差异巨大时梯度下降的等高线是狭长椭圆形更新路径会来回震荡。处理办法是先做标准化让每个特征均值归零、方差归一。sklearn.preprocessing.StandardScaler可以直接用from sklearn.preprocessing import StandardScaler scaler StandardScaler() X_scaled scaler.fit_transform(X) X_b np.c_[np.ones(len(X_scaled)), X_scaled] # 加截距列 y df[quality].valuesnp.c_把一列 1 和标准化后的特征矩阵按列拼接这列 1 对应的权重就是截距项。标准化之后所有特征尺度统一到 0 附近学习率可以安全地设在 0.01 甚至 0.1。如果不做标准化用同样的学习率可能直接导致梯度爆炸。我的调参习惯是从lr0.01, epochs1000开始观察 loss 曲线如果前 100 轮 loss 下降明显、之后趋平说明参数合理如果 loss 忽上忽下说明学习率偏大降到 0.001 再试如果 loss 一路缓慢下降但还没收敛说明 epochs 不够加大到 5000。4.3 SGD 的实现与 mini-batch 变体SGD 和批量 GD 的核心区别在于梯度计算使用的样本数量。批量 GD 每轮用全部 1599 个样本计算一次梯度稳定但慢SGD 每步随机抽一个样本算梯度更新频繁但方向噪声大。代码核心如下def stochastic_gradient_descent(X, y, lr0.01, epochs50, batch_size1): n_samples, n_features X.shape w np.zeros(n_features) for epoch in range(epochs): indices np.random.permutation(n_samples) for i in indices: xi X[i:i1] yi y[i:i1] grad -2 * xi.T (yi - xi w) w - lr * grad return wnp.random.permutation(n_samples)每次迭代前打乱样本顺序避免模型按固定顺序学习到序列相关性。X[i:i1]保持二维形状这样xi.T (yi - xi w)的矩阵运算不会出错。batch_size1表示纯 SGD改成 32 或 64 就是 mini-batch SGD一次用一小批样本算梯度兼具稳定性和速度这也是深度学习中实际使用的默认方案。SGD 的学习率通常要比批量 GD 小一个数量级。批量 GD 用 0.01 能收敛的话SGD 用 0.001 起步比较稳否则单个样本的噪声梯度会把权重震飞。epochs 也要相应加大因为每个 epoch 实际上更新了 n_samples 次看起来总步数很多但这个条件下收敛仍然需要多跑几轮。常见的做法是让学习率随训练轮次衰减比如每 20 轮乘 0.9可以在脚本里加一行调度逻辑。4.4 三种算法在红酒数据上的效果对比在同样的特征标准化条件下三种算法最终得到的模型精度差别不大。OLS 由于是解析解必然得到 R² 最高或接近最高的结果批量 GD 在充分收敛后权重会落在和 OLS 几乎相同的位置SGD 由于每步的随机性最终权重会在最优解附近小幅波动R² 比 OLS 低 0.01 到 0.02 属于正常现象。我的建议是对比时不要只看 R²还要看训练时间和收敛轨迹。红酒数据只有 1599 行批量 GD 每轮计算很轻反而比 SGD 更容易观察收敛换成几十万行数据时SGD 每轮只处理一个样本内存优势和速度优势才会真正体现。这份资源的价值就在于让你在小数据集上把三种机制跑透将来切到真实工业数据时知道该选哪种。5. 避坑指南红酒线性回归最常见的 5 个问题5.1 现象R² 只有 0.3怀疑代码写错了很多学员第一次跑完看到 R² 不到 0.4第一反应是数据加载错了或模型没训练。实际上红酒质量评分是人工主观打分本身噪声很大大多数特征与质量之间只是弱线性关系R² 低是数据特性不是代码 bug。解决方法是不要只看 R²把 RMSE 和残差分布也打印出来同时用交叉验证做对照如果交叉验证的分数和单次切分接近说明模型是稳定的。用多项式特征或岭回归可以小幅提升但很难突破 0.5这一点要建立合理预期。5.2 现象损失函数输出 nan 或 inf批量梯度下降过程中loss 打印出现nan几乎可以肯定是数值爆炸了。原因通常是两个一是学习率太大导致权重更新步长越过最优点后梯度越来越大二是特征未标准化某个特征的值域差异极端梯度方向被量纲大的维度主导。解决方法是先对特征做StandardScaler然后把学习率从 0.001 开始调每跑一次看 loss 前 200 轮的曲线。如果第一轮就nan十有八九是特征标准化这一步漏了。5.3 现象SGD 的结果和 OLS 差距很大SGD 的权重在最优解附近震荡是正常的但如果 R² 比 OLS 低 0.1 以上通常不是随机性问题而是训练不充分。原因有三个epochs 太少模型还没走完足够多的样本遍历学习率恒定为 0.01 或更大后期权重在最优解附近跳跃无法稳定没有打乱样本顺序模型学到了数据原始排列的偏差。解决方法是把 epochs 加到 100 以上学习率从 0.001 开始每轮用permutation打乱数据或者在训练后半段把学习率衰减一半。5.4 现象OLS 系数的符号和业务直觉相反靠直觉判断酸度高的酒通常质量不会太好但线性回归跑出来volatile acidity是负号时符合预期有些特征如density却出现正负摇摆。原因可能是特征之间高度相关存在多重共线性导致系数估计不稳定。解决方法是先看特征的相关矩阵删掉相关性超过 0.8 的冗余特征或者改用带 L2 正则的岭回归岭回归能缩小系数方差符号会更稳定。不要看到符号不对就急着删特征先做相关性诊断。5.5 现象把红、白两份数据合并训练两份 CSV 列名相同有人图省事直接pd.concat后一起训练。这样做会引入数据分布偏差红、白葡萄酒的理化指标分布差异明显混合训练会让模型学到一个“两种酒平均”的关系对哪一类都拟合不好。解决方法是分开建模或者明确加一列type标志作为特征。这个包本身提供了两份独立 CSV目的不是让你合并而是做对比实验。如果真要做统一模型至少也要把type作为类别特征加进去再检查它在系数中是否显著。6. 进阶用法交叉验证、多项式特征和收敛曲线让红酒回归模型经得起推敲6.1 用交叉验证替代单次切分单次train_test_split的结果依赖于随机种子换一个种子可能差 0.02 的 R²。更稳健的做法是用 5 折交叉验证把模型评估方差摊平from sklearn.model_selection import cross_val_score scores cross_val_score(LinearRegression(), X, y, cv5, scoringr2) print(scores, scores.mean())cv5表示把数据切成 5 份轮流拿一份做验证其余 4 份训练最终得到 5 个 R² 和它们的均值。如果 5 个分数标准差很大说明模型在某些子集上不稳定这时候要回看数据分布是否均匀而不是继续调参。6.2 多项式特征与岭回归的组合如果觉得线性模型的 R² 上限太低可以试试加上二次项。线性回归只能捕捉直线关系但红酒质量与特征之间可能存在非线性关系比如alcohol对质量的边际作用会递减。用管道把特征扩展、标准化和岭回归串起来避免数据泄露from sklearn.preprocessing import PolynomialFeatures from sklearn.pipeline import make_pipeline from sklearn.linear_model import Ridge pipe make_pipeline( PolynomialFeatures(degree2, include_biasFalse), StandardScaler(), Ridge(alpha1.0) ) print(cross_val_score(pipe, X, y, cv5, scoringr2).mean())degree2会生成所有特征的平方项和两两交叉项特征维度从 11 扩大到 70 多这时必须配合正则化Ridge(alpha1.0)的 L2 惩罚能抑制膨胀系数。在我的测试里这种方式通常能让 R² 比纯线性提高 0.03 到 0.05代价是可解释性变差。如果你做的是课程作业建议在报告里说明多项式特征提升了拟合度但不代表业务上真的有二次关系。6.3 一个值得养成的习惯把收敛曲线画出来我拿到任何手写梯度下降代码第一件事不是看最终 R²而是把 loss 曲线打出来。批量 GD 和 SGD 都应该可视化收敛过程光打印几个 epoch 的数字不够直观。import matplotlib.pyplot as plt plt.plot(history) plt.xlabel(epoch) plt.ylabel(loss) plt.title(GD convergence curve) plt.show()如果曲线是单调下降并趋平说明学习率和 epochs 搭配正常如果曲线震荡调低学习率如果结束还在明显下降加大 epochs。我第一次跑红酒数据时只看结果不看过程某导师指出“你的 GD 其实没收敛只是碰巧停在了一个不算差的地方”。从那以后我每次调梯度相关代码都强制走一遍“先画曲线、再报指标”的流程这个习惯帮我绕开了大量隐藏的翻车现场。这个资源包的 GD 脚本里已经预留了 history 列表直接复用即可。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑