☰
最简单的线性回归代码:手写梯度下降与sklearn实战
2026/10/5 3:37:09 网站建设 项目流程

先纠正一个小细节:"liner"大概率是"linear"(线性)的笔误,这个词在搜索框里出现的频率还挺高,很多人写代码时也会顺手打错。这篇笔记是"最简单的回归代码"系列的第4篇,前几篇拆过理论、讲过公式推导,这篇集中火力只聊代码本身:怎么用最短的代码把线性回归跑起来,手写和调库各来一遍,参数怎么设、数据怎么预处理、跑出结果怎么判断好坏。

适合谁看:刚上手机器学习、被各种教材里"最小二乘""梯度下降"绕晕的新手;已经会用sklearn但想搞清楚底层逻辑的进阶者;以及想快速把回归模型用在自己数据上的朋友。这篇尽量照顾到不同基础,能抄代码的地方直接抄,想深挖原理的地方也有解释。

1. 这篇笔记在聊什么:一个纯粹的"跑通"问题

1.1 先搞明白"最简单"到底指什么

很多人第一次写回归代码,最大的障碍不是公式,而是"代码不知道从哪一行开始写"。线性回归作为入门算法,它的代码量可以压缩到非常小:手动实现核心逻辑也就二十几行,调库的话三行就够。所谓"最简单",是指它的数学假设最简单——因变量和自变量之间是线性关系,一条直线或一个超平面去拟合;也是指代码结构最简单——没有复杂的网络层、没有繁琐的预处理流程,是把"Python + NumPy + 一份数据"组合起来就能跑的最小闭环。

但要提醒一句:代码短不代表可以闭眼抄。我在实际写代码过程中发现,线性回归的代码陷阱往往不在回归本身,而在数据。数据有没有空值、量纲差多少、有没有明显的异常点,这些直接决定你跑出来的系数是"可解释的"还是"看起来对但没卵用"的。所以这篇笔记表面上写代码,实际上把数据准备和结果验证也一并讲透。

1.2 为什么是"第4篇学习笔记"

这个系列前几篇大概覆盖了:什么是回归问题、误差怎么衡量、正规方程和梯度下降的推导。到了第4篇,默认读者已经知道"损失函数是均方误差""目标是找到让损失最小的w和b"。如果你还没看前几篇也没关系,这篇涉及到的公式都会用代码重新演示一遍——代码本身就是最好的公式解释器。我始终觉得,学机器学习有个笨但特别有效的路径:先照着敲,敲完改参数,改完再看结果变化,最后再回头推公式。这比上来啃一堆矩阵求导舒服得多,也记得牢得多。

2. 写代码之前的三件事:环境、数据、损失函数

2.1 环境依赖其实就三个

跑线性回归不需要深度学习框架,有Python就够。我建议的环境是:

  • Python 3.8及以上(低版本不是不行,但3.8以下有些新语法和库版本不好适配)
  • NumPy:所有矩阵运算的底子
  • Matplotlib:画散点图和回归线,一定要学,因为判断线性拟合好不好,肉眼比指标快
  • scikit-learn:调库版回归要用,但手写代码阶段用不到它

安装命令不写了,网上一搜一大把。重点提醒一下scikit-learn的版本问题:0.24之后和1.x版本的API有细微变化,如果你下载的教程代码报"AttributeError",先检查是不是版本不对。我踩过这个坑,一度以为是代码写错了,最后发现是版本混用。

2.2 造一份能用来练手的数据

没有真实数据的时候,自己造数据是最快的验证方式。我一般用这种形式:

import numpy as np import matplotlib.pyplot as plt np.random.seed(42) X = np.random.rand(100, 1) * 10 # 100个样本,特征取值范围0~10 true_w, true_b = 3.0, 5.0 # 真实系数:y = 3x + 5 y = true_w * X.squeeze() + true_b + np.random.randn(100) * 2 # 加一点噪声

这段代码背后的思路值得多说两句。第一,种子设成42,是为了保证每次随机出的数据一样,方便复现。第二,噪声系数2决定了数据集的"难度"——噪声调小,点几乎落在直线上,模型好拟合同时也学不到什么;噪声调大,点的分布很散,回归线的斜率会被噪声拉扯。做实验时我习惯先设一个中等噪声,等代码跑通了再逐步加大,观察模型抗干扰能力。第三,如果把np.random.rand(100,1)改成np.random.rand(100,2),就从一元线性回归变成了多元线性回归,代码逻辑几乎不变,但涉及的概念(特征维度、系数向量)会复杂一截。这篇先牢牢盯住一元。

画出散点图确认一下数据结构,这是流程里不能省的一步:

plt.scatter(X, y, alpha=0.6) plt.xlabel("X") plt.ylabel("y") plt.title("模拟数据分布") plt.show()

2.3 最小二乘法在代码里到底在算什么

你可能看过最小二乘的公式:要求的就是让每个点的预测值和真实值差的平方和最小。代码里的实现逻辑和公式推导是一致的,但有一个关键区别:推导时用矩阵形式写起来干净,代码实现时更常直接写"循环或向量化运算"。

具体到损失函数:

def compute_loss(y_true, y_pred): return np.mean((y_true - y_pred) ** 2)

这个函数返回的就是均方误差(MSE)。np.mean而不是np.sum,好处是把误差归一化到"每个样本平均误差"的量级,这样不管数据集有100条还是10000条记录,Loss的数值范围都会比较稳定,方便你判断模型是否收敛。

3. 最简单的回归代码两种写法:手写与调库

3.1 手写梯度下降版:二十几行看清回归的本质

梯度下降的思路用生活场景类比就是:你站在山坡上想走到谷底,每次左右看看哪个方向是下坡,迈出一步,再重复——步子太大可能跳过谷底,步子太小走得太慢。代码里的"方向"就是梯度,"步子"就是学习率。

下面是我常给学生演示的最小实现,核心就一个循环:

# 初始化参数 w, b = 0.0, 0.0 learning_rate = 0.01 epochs = 1000 n = len(X) for epoch in range(epochs): # 1. 预测 y_pred = w * X.squeeze() + b # 2. 计算损失 loss = np.mean((y_pred - y) ** 2) # 3. 计算梯度(对w求偏导、对b求偏导) dw = (2 / n) * np.dot(X.squeeze(), y_pred - y) db = (2 / n) * np.sum(y_pred - y) # 4. 更新参数 w -= learning_rate * dw b -= learning_rate * db if epoch % 100 == 0: print(f"Epoch {epoch}: loss={loss:.4f}, w={w:.2f}, b={b:.2f}") print(f"最终结果: w={w:.2f}, b={b:.2f}(真实值: w=3.00, b=5.00)")

代码理解拆成三层:

第一层,预测公式w * X + b对应线性回归的假设函数,这是所有代码的地基。第二层,梯度计算里的(2/n)来自均方误差对w和b求导的结果,如果你推过公式会发现手写代码没有绕过数学,只是把求导结果直接落地了。第三层,w -= learning_rate * dw是梯度下降的更新规则,负号表示往损失减小的方向走。

跑完这段代码,如果一切正常,w会在3.0附近,b在5.0附近——因为造数据时我们就是按y = 3x + 5生成的。这正是手写代码最大的好处:用已知答案的数据验证实现逻辑。如果跑出来w是3.5甚至4.0,说明梯度下降过程有问题,而不是模型有问题。

3.2 三段式调库:scikit-learn的正式用法

手写版跑通后,就该看生产环境下真正常用的写法了。实际项目中几乎没人手写梯度下降,都用现成库。用scikit-learn完成线性回归,核心代码就三行式的流程:

from sklearn.linear_model import LinearRegression from sklearn.model_selection import train_test_split from sklearn.metrics import mean_squared_error, r2_score # 1. 划分训练集和测试集 X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42 ) # 2. 训练 model = LinearRegression() model.fit(X_train, y_train) # 3. 预测并评估 y_pred = model.predict(X_test) mse = mean_squared_error(y_test, y_pred) r2 = r2_score(y_test, y_pred) print(f"系数w: {model.coef_[0]:.2f}") print(f"截距b: {model.intercept_:.2f}") print(f"测试集MSE: {mse:.4f}") print(f"测试集R²: {r2:.4f}")

有个细节特别容易让新手困惑:为什么手写版直接用了全部数据,调库版却要先划分训练集和测试集?原因在于评估方式的不同。手写版我们是在验证实现逻辑——数据是合成的、答案已知,不需要担心过拟合;调库版要模拟真实场景——模型得在没见过的数据上表现好才算有用,如果只用训练数据评估,哪怕模型完全记住数据点也能拿到很低的MSE,但这没有任何预测价值。train_test_split里test_size=0.2意味着80%数据训练、20%数据测试,这是最常见的一种划分比例。

3.3 两种结果对拍:用同一份数据相互验证

手写版和调库版之间应该是什么关系?答案是:结果应该高度接近,但不要求完全相等。手写版是梯度下降迭代求近似解,迭代1000次后收敛到的位置接近全局最优但可能有微小误差;调库版用的是最小二乘的闭式解,直接解方程得到精确解。以我的经验,学习率合理、迭代次数足够时,两者差距通常在0.01以内。如果差距大,先查学习率,再看迭代次数,最后检查是不是数据划分不一致。

对拍验证的脚本我长期保留,换个数据集就能用:

# 把两个模型的结果放在同一张图上看 plt.scatter(X_test, y_test, alpha=0.6, label="真实测试点") plt.plot(X_test, y_pred, "r-", label="sklearn回归线") # 手写模型的系数也画一条线 plt.plot(X_test, manual_w * X_test.squeeze() + manual_b, "g--", label="手写梯度下降回归线") plt.legend() plt.show()

两张线如果几乎重合,说明你既理解了原理,也会用工具,这一章就算真正过关了。

4. 实操中的五个典型问题与排查思路

4.1 数据没做归一化导致梯度震荡

如果数据里某个特征取值范围是0到100000,另一个特征是0到1,梯度下降就会出问题:大数值特征的梯度幅值很大,小数值特征的梯度很小,两者相差几个数量级,更新参数时要么大特征方向步子过猛,要么小特征方向基本不动,训练过程来回震荡。

我的排查习惯:一旦发现loss曲线像锯齿一样上下颠簸,先打印出w和b的变化值,看是不是某一个方向更新量特别大。解决办法很直接,标准化或归一化后重新跑:

from sklearn.preprocessing import StandardScaler scaler = StandardScaler() X_scaled = scaler.fit_transform(X)

注意一点,fit_transform之后做预测,新数据也要用同一个scaler做transform,不能用原来的原始值往模型里塞,否则特征分布对不上,预测结果直接飘掉。这个问题在博文评论区经常有人问,说是"练得好好的,一用真实数据就乱套",多半就是漏了这一步。

4.2 学习率设得不对:NaN和发散

学习率太大,参数更新直接越过最低点,甚至每一步都跳得比上一步更远,loss一路飙升变成inf,最终出现NaN。学习率太小,loss下降非常缓慢,1000次迭代后参数离最优值还差很远。每次换数据集,都要把学习率当作要重新调整的超参数来看,不要沿用上次的默认值。

我调试时的经验方法:先把学习率设成0.001跑一遍,观察loss是否稳定下降。如果下降太慢,改成0.01再看;如果发散了,改0.0001。用这种"十倍试错"的方式快速定位一个量级合适的区间。还有个偷懒技巧,给loss更新设一个打印阈值,连续50次迭代loss下降幅度小于1e-5就提前停止,省时间还避免过拟合到迭代次数上:

if epoch > 0 and abs(loss - prev_loss) < 1e-5: print(f"提前收敛,停止于 epoch {epoch}") break prev_loss = loss

4.3 数据里有NaN或无穷值

这个坑的隐蔽之处在于,大部分情况代码不会直接报错,而是默默返回一个nan或者离谱的系数。你看到loss变成nan的第一反应通常是"梯度爆炸了",但先别急着调学习率——先检查原始数据:

import pandas as pd df = pd.DataFrame({"X": X.squeeze(), "y": y}) print(df.isna().sum()) print(np.isinf(df).sum())

如果确有缺失,常用策略是删除或填充。小数据集直接删除缺失行,大数据集用均值/中位数填充更稳妥。fillna的mean和median在sklearn的SimpleImputer里也有封装,字段名差不多,别记混。这个排查思路对后面学任何模型都有用,因为数据清洗永远是第一步。

4.4 R²,MSE和"感觉不对"的判断

回归模型有两个最常用的评估指标:MSE和R²。MSE衡量的是预测误差的平均水平,数值越小越好,但它有量纲,不好跨数据集比较;R²是无量纲的,取值范围通常在0到1之间,可以通俗理解成"模型解释了百分之多少的方差",0.8意味着y的变化有八成被模型抓住了。

但这两套指标在真实数据上都有"失灵"的时候。如果测试集R²是负的,说明你的模型预测水平还不如直接拿y的均值去猜,基本可以断定模型结构有问题或数据预处理有问题。另外别只看单一指标,我遇到过一次MSE挺小但画图一看拟合线是歪的,原因是数据里有几个极端异常点把回归线拽偏了,MSE被多数正常点拉低,视觉上却明显不对。所以我的习惯永远是:指标看,图也画,两者参照着下结论。

4.5 一元回归没问题,但多元回归跑崩了

系列的下一篇可能就会涉及多元回归,这里先打一个预防针:一元回归里你能直接画散点图看线性关系,多元回归的维度超过3就没法直接可视化,这时候更依赖指标和诊断图。同时,多元回归最容易出问题的点是多重共线性——两个特征高度相关时,系数估计会非常不稳定,甚至符号都反常识。判断方法是看相关系数矩阵,相关系数超过0.8就要警惕。处理办法是删掉一个冗余特征,改用岭回归或Lasso,这个内容在下一节展开。

5. 从最简单起步:线性回归之后的扩展方向

5.1 正则化:面对"不对劲"的线性回归

真实业务数据很少像模拟数据那么规整,特征多、相关性高、噪声大,普通线性回归容易过拟合或系数解释性差。这时候岭回归(L2正则)和Lasso(L1正则)是线性回归最自然的升级。

正则化的原理一句话就能说清:在损失函数后面加一个对系数大小的惩罚项,逼着模型在"拟合得好"和"系数别太夸张"之间取平衡。代码上只是换了个模型名字:

from sklearn.linear_model import Ridge, Lasso ridge = Ridge(alpha=1.0) ridge.fit(X_train, y_train) lasso = Lasso(alpha=0.01) lasso.fit(X_train, y_train)

alpha越大,惩罚越强,系数被压得越小,但它不是越大越好——压得太狠模型就欠拟合了。alpha的调参需要用交叉验证(GridSearchCV或者RidgeCV这种带CV后缀的类),不能用测试集反复试,否则会信息泄露,后续评估虚高。

5.2 非线性怎么办:从线性回归到树模型

线性回归的局限性非常明确:它假设特征和标签之间是直线关系。实际场景里更多是非线性关系,比如年龄和收入的关系、温度和销量的关系,用线性模型硬拟合的效果很差。这时候有两个方向:

  • 方向一:对特征做变换,比如加平方项、交互项,把非线性关系"掰"成线性后再套线性回归。优点是可解释性好,缺点是变换公式需要人工设计和业务经验。
  • 方向二:换模型,比如决策树回归、随机森林回归。我现在遇到非线性关系明显的数据,第一反应就是直接上树模型,因为它不需要做特征缩放、能捕捉复杂交互关系,写代码也不麻烦:
from sklearn.ensemble import RandomForestRegressor rf = RandomForestRegressor(n_estimators=200, max_depth=10, random_state=42) rf.fit(X_train, y_train)

随机森林里有几个关键参数值得记住:n_estimators是树的数量,一般100到300之间效果差异不大但训练时间会线性增加;max_depth控制单棵树的深度,太深容易过拟合训练集;random_state固定之后保证结果可复现。关于随机森林的调参建议是:优先调max_depth和min_samples_leaf,而不是无脑加树的数量。

再往上走就是梯度提升树,XGBoost和LightGBM。这两个模型在工业界用得非常多,原因是它们把"多个弱学习器逐步提升"的思路落地成工程化工具,速度更快、精度更高。以LightGBM为例,一份可以跑的基础代码:

import lightgbm as lgb model = lgb.LGBMRegressor( n_estimators=500, learning_rate=0.05, max_depth=-1, num_leaves=31, random_state=42, ) model.fit(X_train, y_train)

LightGBM几个入门的参数常识:learning_rate决定了每棵树的贡献权重,调小以后需要更多树来弥补;num_leaves是核心复杂度控制参数,默认31在中小数据集上够用;max_depth设-1表示不限制,但配合num_leaves控制复杂度。建议先跑默认参数,再看特征重要性做筛选,最后调参,不要一开始就追求最优。

5.3 学习路径建议:从这篇笔记往后怎么走

以我自己的项目经验来看,回归模型的学习路径可以画成一条清晰的线:

  1. 线性回归(本文):搞懂损失、梯度下降、评估指标,这是后续所有模型的地基。
  2. 正则化线性模型(岭回归/Lasso):解决特征冗余和数据噪声问题。
  3. 决策树回归 → 随机森林:引入非线性能力,同时建立"集成学习"的直觉。
  4. XGBoost/LightGBM:工业界的默认选择,特征工程做好之后直接出成绩。
  5. 回看线性回归的假设诊断:残差图、Q-Q图、共线性诊断,让自己能从"会跑代码"进阶到"能判断模型是否可靠"。

这里必须强调一个容易被忽略的点:这套路径的价值不在于每个模型都学会调用API,而在于同一个数据集上做横向对比。同一个任务,用线性回归、岭回归、随机森林、LightGBM各跑一遍,记录各自的MSE、R²、训练时间,你才能直观感受"什么场景该用什么模型"。我每次带项目都会让组员做成一张对比表,这张表比任何理论讲解都更有冲击力。

6. 把线性回归写进自己的工具箱

这篇文章从"liner"这个拼写开头,一路写到岭回归、LightGBM,核心其实就是一个观点:线性回归不是一学完就可以丢掉的玩具,而是判断一切回归问题的起点。

我整理一下自己常用的代码片段,你可以直接当模板存一份:

# 1. 手写梯度下降(理解原理时用) # 2. sklearn LinearRegression(基线模型,永远先跑这个) # 3. Ridge / Lasso(遇到共线性或过拟合时替换) # 4. RandomForestRegressor / LGBMRegressor(非线性问题或追求精度时上)

每次拿到一份新数据集,我拿到手的第一反应永远是先跑线性回归做baseline。它不是效果最好的模型,但它能告诉我很多信息:特征和标签的大致关系、数据质量是否靠谱、评估流程是否通顺。等这些底都摸清了,再决定要不要上复杂模型,心里就有底了。

最后分享两个个人习惯。第一,模拟数据是调试代码最好的朋友:先造已知答案的数据,跑出来的系数对不对一目了然,等逻辑确认无误再换真实数据,能帮你省下一大半"为什么结果这么离谱"的排查时间。第二,写回归代码时始终带着一个疑问:这个预测结果,我能不能用一两句话说清楚它为什么是根据这些特征得出这个值——如果连自己都解释不了,那这个模型多半还没调到位。

希望这篇笔记能给你的回归学习之旅省点力气。代码量不大,重点是动手敲一遍,改几个参数看看会发生什么。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询