每次有人问我“机器学习从哪开始”,我给的答案都是线性回归。不是因为它简单,而是因为它足够透明——模型怎么学、学得怎么样、哪里出了问题,每一步都能拆开看清楚。很多人用Python调一行LinearRegression就完事了,但真到面试、写论文、做业务分析的时候,被问到“梯度下降更新公式怎么推的”“为什么用R²而不是MSE评估”,一下子就卡壳了。这篇就来手把手把线性回归从原理到代码过一遍,重点放在手写实现和实战中真正会踩的坑上,适合刚入门Python想学机器学习的读者,也适合那些调包调腻了、想搞清楚模型内部逻辑的人。
1. 线性回归:值得从调包回归到手写的第一课
1.1 为什么线性回归值得手写一遍
线性回归的历史可以追溯到19世纪,但直到今天它依然是很多工业场景的首选模型。原因很简单:可解释性强、计算成本低、对中小规模数据效果稳定。在银行做评分卡、在电商做销量预测、在工厂做良率分析,线性回归都是那个“先跑一遍看看”的模型。
但正因为sklearn一行就能调出来,很多人忽略了它内部的数学原理。线性回归本质上是在求解一个最小化残差平方和的参数估计问题,背后的数学工具是最小二乘法,而求解方式又分为闭式解和梯度下降两条路线。这两条路线分别对应了数值计算和优化理论两个方向,是后续学习岭回归、Lasso、逻辑回归、神经网络的基础。
我见过太多人跳过原理直接调包,结果遇到问题完全不知道从哪里排查。比如模型系数出现了异常的符号,比如预测结果对输入特征的尺度极其敏感,比如训练集表现很好但测试集一塌糊涂。这些问题如果你理解线性回归的推导过程,往往一眼就能看出来原因。所以这篇文章我想做的一件事,就是让原理和代码一一对应起来,让你看完之后不仅会调fit,还能自己在黑板上把公式写出来。
1.2 一个例子:房价预测里的线性回归
线性回归的经典例子是房价预测,我们用这个场景来建立直觉。假设你收集了一些房屋数据,包括面积、卧室数量、房龄等特征,目标是预测房价。最简单的线性回归模型长这样:
[
\hat{y} = w_1 x_1 + w_2 x_2 + w_3 x_3 + b
]
其中(x_1, x_2, x_3)是面积、卧室数量、房龄,(w_1, w_2, w_3)是每个特征的权重,(b)是偏置项(截距)。模型要做的,就是找到一组((w, b)),让预测值(\hat{y})和真实值(y)之间的误差尽可能小。
这里的“误差尽可能小”落到数学上,就是最小化误差平方和(SSE)。为什么用平方而不是绝对值?因为平方误差处处可导,方便求导优化;而且它对大误差的惩罚呈二次增长,会让模型更积极地减小大偏差。当然这也带来了对异常值敏感的缺点,后面我会谈到这个问题。
提示:线性回归的“线性”指的不是特征必须是一次的,而是指参数是线性的。你可以加入(x_1^2)、(x_1 x_2)这样的多项式特征,只要模型对参数(w)是线性的,它依然是线性回归。这个理解对后面做多项式回归和特征工程很重要。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 准备环境与样例数据:别在环境上浪费一小时
2.1 Python环境与依赖安装
做线性回归,Python环境必备的库其实就四个:numpy(数值计算)、pandas(数据处理)、matplotlib(可视化)、scikit-learn(用于和手写结果对比验证)。如果你还没装Python,去官网下载安装包时记得在第一步勾选“Add Python to PATH”,这个选项不勾,后面在命令行里输python会提示找不到命令,非常容易劝退新手。
装依赖我建议直接用pip,在终端里执行:
bash复制pip install numpy pandas matplotlib scikit-learn
如果你用的是Anaconda,那这几个库基本都预装了,打开终端或者Anaconda Prompt直接确认一下版本就行:
bash复制python -c "import numpy, pandas, matplotlib, sklearn; print('ok')"
一个常见的问题是环境冲突。我之前见过有人用pip install装了一堆包,后来跑深度学习框架时发现numpy版本不对,整个环境崩了。建议从开始就养成建虚拟环境的习惯:
bash复制python -m venv ml-env
# Windows
ml-env\Scripts\activate
# macOS/Linux
source ml-env/bin/activate
这样即使环境弄坏了,删掉ml-env目录重来就行,不影响系统Python。
2.2 生成一份能练手的样例数据
为了把原理讲透,我先用代码生成一份带噪声的线性数据。这样我们在“已知真实规律”的前提下来训练模型,就能直观地检验模型学得对不对。
python复制import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
np.random.seed(42) # 固定随机种子,保证结果可复现
X = 2 * np.random.rand(100, 1) # 100个样本,特征范围0~2
# 真实关系:y = 4 + 3 * x + 噪声
y = 4 + 3 * X + np.random.randn(100, 1)
plt.scatter(X, y)
plt.xlabel('x')
plt.ylabel('y')
plt.show()
这里我设置了随机种子42,是为了保证每次运行生成的噪声数据一样,方便你复现我的结果。噪声np.random.randn(100, 1)服从标准正态分布,它模拟的是真实场景中那些我们没收集到的、或者本身就无法用特征解释的随机因素。
这份数据的真实参数是(w=3)、(b=4)。如果你的模型训练完之后,学到的权重接近3、截距接近4,说明实现是正确。这比一上来就用真实业务数据去跑要稳妥得多——真实数据没有标准答案,代码错了你都不知道错在哪。
3. 两种手写方案从零实现:闭式解与梯度下降
3.1 最小二乘法的闭式解:一行式实现
线性回归最直接的求解方式是用正规方程(Normal Equation)。它的推导思路不复杂:把损失函数写成矩阵形式,对参数向量求导,令导数为零,就能求出最优解的解析表达式。
[
\theta = (X^T X)^{-1} X^T y
]
其中(\theta)是包含截距项和权重的参数向量。注意这里需要在原特征矩阵(X)前加一列全1,用来对应截距项。代码实现是这样的:
python复制X_b = np.c_[np.ones((100, 1)), X] # 在X左边拼接一列1
theta_best = np.linalg.inv(X_b.T.dot(X_b)).dot(X_b.T).dot(y)
print(theta_best)
# 输出大概是 [[3.98], [2.92]],和真实值4、3接近
这里用np.linalg.inv求逆矩阵,简单直观。但实际项目中我更推荐用np.linalg.lstsq,它用的是奇异值分解,数值稳定性更好,尤其在(X^T X)接近奇异(即特征之间存在多重共线性)的时候,直接求逆会得到非常离谱的结果,而lstsq还能凑合给出一个解。
python复制theta_best_svd, residuals, rank, s = np.linalg.lstsq(X_b, y, rcond=None)
print(theta_best_svd)
两种写法结果基本一致(在不共线的前提下)。从工程角度讲,直接用lstsq更稳,这也是为什么我不太建议你在代码里手写inv(X_b.T.dot(X_b))——它能帮你理解公式,但不适合直接上生产。
3.2 梯度下降:更接近工程实践的训练循环
闭式解虽然优雅,但它的时间复杂度是(O(n^3))级别的,因为要算矩阵乘法和逆矩阵。当特征数量超过几万甚至更大时,计算量会非常恐怖。这时候就需要用梯度下降来逼近最优解。
梯度下降的思路可以这样理解:你站在一座山的某一点,想走到山谷最低处,但你蒙着眼睛,只能靠脚底的感觉判断哪个方向是下坡。梯度就是那个“坡度最大的下坡方向”,学习率就是迈步的幅度。每一步都沿着梯度方向调整参数,反复迭代,就能逼近最低点。
对线性回归的损失函数:
[
J(\theta) = \frac{1}{2m} \sum_{i=1}^m (\hat{y}^{(i)} - y^{(i)})^2
]
求梯度后得到参数更新公式:
[
\theta_j := \theta_j - \alpha \cdot \frac{1}{m} \sum_{i=1}^m (\hat{y}^{(i)} - y^{(i)}) x_j^{(i)}
]
这里的(\alpha)是学习率,(m)是样本数量。代码实现如下:
python复制m = len(X_b)
theta = np.random.randn(2, 1) # 随机初始化参数
learning_rate = 0.1
n_iterations = 1000
for iteration in range(n_iterations):
y_pred = X_b.dot(theta)
gradients = (2 / m) * X_b.T.dot(y_pred - y)
theta = theta - learning_rate * gradients
print(theta)
# 输出应该也接近 [[3.98], [2.92]]
这段代码的关键在第4到第6行:先算出预测值,再算梯度,最后沿负梯度方向更新。循环1000次后,参数会收敛到和闭式解几乎一样的结果。
为了更直观,我在实战里习惯把每次迭代的损失记录下来,画一条损失下降曲线。如果曲线一直下降然后持平,说明收敛正常;如果曲线震荡甚至上升,说明学习率太大或者数据没做标准化。
4. 在真实业务数据上验证:指标选了才不会被问住
4.1 评估指标:MSE、RMSE、MAE与R²怎么选
很多人跑完模型只看一眼准确率,这对回归任务来说是不够的。回归模型常用的评估指标有四个:
| 指标 | 公式 | 业务解读 | 特点 |
|---|---|---|---|
| MSE | (\frac{1}{m}\sum (y-\hat{y})^2) | 预测误差的平方均值 | 对大误差惩罚重,对异常值敏感 |
| RMSE | (\sqrt{MSE}) | 平均误差的量级(与原单位一致) | 最常用,能直观反映“平均差多少” |
| MAE | (\frac{1}{m}\sum |y-\hat{y}|) | 平均绝对误差 | 对异常值不如MSE敏感 |
| R² | (1 - \frac{SSE}{SST}) | 模型解释了百分之多少的方差 | 无单位,适合跨场景比较 |
举个例子,你在做房价预测,RMSE算出来是3.5万元,意思就是“平均而言,预测值和真实值之间差3.5万元”。而R²如果是0.86,意思是“房价的变动中有86%可以被模型里的特征解释”。R²越接近1越好,但如果R²为负,说明你的模型比直接用平均值预测还要差,这时候就需要检查是不是特征工程或者模型选择出了问题。
4.2 用sklearn的数据集验证手写实现
为了不让教程停留在虚构数据上,我用sklearn自带的加利福尼亚房价数据集(California Housing)来验证一下手写实现。这个数据集比波士顿房价数据要新,也更适合用来演示。
python复制from sklearn.datasets import fetch_california_housing
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import mean_squared_error, r2_score
housing = fetch_california_housing()
X_housing, y_housing = housing.data, housing.target
# 划分训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(
X_housing, y_housing, test_size=0.2, random_state=42
)
# 特征标准化:梯度下降必需的一步
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
然后我用自己写的梯度下降函数去训练,并用闭式解做一个对照。结果两个方法学到的参数几乎一致,测试集上的R²都在0.6左右,RMSE在0.7左右。这个效果对于不含任何交叉特征和多项式特征的纯线性模型来说,属于正常水平。
训练完成后可以用下面的代码做预测和评估:
python复制y_pred = X_b_test.dot(theta)
rmse = np.sqrt(mean_squared_error(y_test, y_pred))
r2 = r2_score(y_test, y_pred)
print(f'RMSE: {rmse:.4f}, R²: {r2:.4f}')
这里有个重要细节:对训练集做StandardScaler.fit_transform,对测试集只用transform,千万不能在测试集上重新fit。因为标准化用的均值和方差应该只来自训练集,如果测试集也参与计算,就会造成数据泄露,导致评估结果虚高,这在真实业务中会让模型上线后表现远不如预期。
4.3 画图看拟合效果,别只看数字
我见过太多只看指标就收工的人。回归模型有一个特别好的优势:低维情况下可以直接可视化。对于单个特征,画出散点和拟合直线,一眼就能看出模型是不是有系统性偏差。对于高维数据,画预测值和真实值的散点图也很管用——点越靠近对角线,预测越准。
用matplotlib画一个简单版本:
python复制plt.scatter(y_test, y_pred, alpha=0.5)
plt.plot([y_test.min(), y_test.max()], [y_test.min(), y_test.max()], 'r--')
plt.xlabel('True Values')
plt.ylabel('Predictions')
plt.show()
如果散点在对角线附近且分布均匀,说明模型表现健康。如果出现明显的曲线形状(比如真实值小时预测偏大、真实值大时预测偏小),说明存在非线性关系,可以考虑加多项式特征。
5. 实战高频坑与排错思路:比模型本身更值钱的细节
5.1 特征尺度不一致导致梯度下降不收敛
这是手写梯度下降时最容易踩的坑。假设房价预测里一个特征是房龄(范围0到50),另一个特征是面积(范围50到300),两个特征的数值量级差异可能达到10倍以上。如果不做标准化,梯度下降的更新路径会非常曲折,甚至出现振荡,导致很长时间不收敛。
解决办法就是前面提到的StandardScaler,把每个特征变成均值为0、标准差为1的分布。注意,这个操作只对基于梯度的方法(梯度下降、神经网络)是必须的;对于正规方程的闭式解,标准化不是必须的,因为矩阵求逆能直接把不同尺度的特征一并处理掉。但如果你在闭式解里也做标准化,并不影响最终解,只是预测时需要把标准化过程同步到新数据上。
5.2 多重共线性让系数完全失去解释意义
多重共线性是指两个或多个特征之间存在高度线性相关。比如同时放入“房屋面积”和“房间数量”,这两个变量本身就强相关,模型会很难区分它们各自对房价的贡献。
表现是什么?你可能会发现模型权重数值非常大、符号也反直觉(比如面积的权重变成负的),但整体的预测效果还说得过去。这是因为模型在用这些大系数相互抵消,来凑出一个稳定的预测值,但单个系数的解释意义已经被破坏了。
检测多重共线性常用VIF(方差膨胀因子),一般VIF超过10就认为存在严重共线性。处理手段包括删除相关特征、做主成分分析降维,或者改用岭回归(L2正则化)来稳定系数。岭回归的实现也很简单:
python复制from sklearn.linear_model import Ridge
ridge = Ridge(alpha=1.0)
ridge.fit(X_train_scaled, y_train)
这里alpha是正则化强度,越大对系数的压缩越强。关于怎么选alpha,可以用交叉验证,RidgeCV会自动帮你搜索最优值。
5.3 过拟合问题:当模型记住了训练集而不是学到了规律
线性回归的过拟合不像深度神经网络那么夸张,但确实存在。尤其是当你加入了很多多项式特征(比如(x^2, x^3, ...))之后,模型自由度大幅增加,完全可能在训练集上拟合得极好,但在测试集上一塌糊涂。
在训练集R²很高但测试集R²明显下降,基本就是过拟合的信号。解决办法有三个方向:
- 减少特征数量或多项式阶数,从源头上降低模型复杂度;
- 加入正则化,用Lasso(L1)或Ridge(L2)约束系数大小;
- 增加训练数据量,让模型看到更多样本,减少对噪音的过度拟合。
Lasso的一个额外好处是它能自动做特征选择,它会把不重要的特征系数压缩到0,相当于帮你筛特征。不过Lasso的代价是它在线性回归中会使用不同的优化算法,而且对特征尺度更敏感,使用前一定要先做标准化。
5.4 评估时的数据泄露:一个隐蔽但致命的错误
前面提到的测试集不做fit_transform,属于数据泄露的一种常见形式。但还有一种更隐蔽的情况:你在做特征选择的时候,先在整个数据集上选了部分特征,再做训练测试划分。这个操作看似没什么问题,但特征选择过程中模型已经“偷看”了测试集的信息,最终评估结果会偏乐观。
最稳妥的标准做法是把特征选择、标准化等所有数据处理步骤,全部放进一个Pipeline里,让交叉验证的每一折都在训练集上独立执行这些步骤。sklearn的Pipeline正是为此设计的:
python复制from sklearn.pipeline import Pipeline
from sklearn.preprocessing import PolynomialFeatures
pipe = Pipeline([
('poly', PolynomialFeatures(degree=2, include_bias=False)),
('scaler', StandardScaler()),
('ridge', Ridge(alpha=1.0))
])
pipe.fit(X_train, y_train)
这样从特征生成到标准化的所有操作,都会在每一折的训练数据内部独立完成,从而避免数据泄露。
5.5 把线性回归当成唯一解,忽略数据本身的非线性
最后一个坑更像思路问题。线性回归有它的适用范围,如果数据本身存在明显的非线性关系(比如销量随季节呈周期性波动、广告投入产出呈现边际递减),线性模型无论怎么调都很难有好效果。
我在做电商销量预测时就遇到过这个情况:纯线性模型的R²只能做到0.4左右,怎么加特征都上不去。后来画出特征和标签的散点图,发现广告投入和销量存在明显的对数关系,于是把广告投入做了个log变换,R²一下跳到了0.7。
所以在用线性回归之前,先画图、先做探索性数据分析,这个习惯比熟悉sklearn的接口重要得多。线性回归只是一个工具,知道什么时候用它、什么时候不用它,才是真正的能力。
6. 一些零散但有用的实操体会
最后再分享几个我在实际使用中总结的小技巧。
验证手写代码是否正确时,我习惯先用一份人肉可算的小数据做测试。比如3个样本、1个特征的极简数据,手算出每个中间变量的值,然后打印代码里的每个中间变量,逐一对照。这比直接拿大数据跑出来后看效果要高效得多,能帮你快速定位是求导算错了、矩阵拼错了,还是初始化问题。
关于学习率,我在项目里常用的做法是先用0.01试跑,然后把损失曲线画出来。如果收敛太慢就调大到0.1,如果震荡就调到0.001。调参这事儿没有银弹,但曲线可视化能让你对“该往哪个方向调”心里有数。
如果你想把模型部署到生产环境,强烈建议不要直接序列化整个sklearn Pipeline就完事。更可靠的做法是,把训练好的权重数值导出来,比如存成JSON或者数据库表,然后在生产服务里直接用numpy做矩阵乘法得到预测结果。这样不仅省去加载大模型的成本,还避免了环境依赖问题——一个几十KB的权重文件和一个几百MB的模型包,维护难度完全是两个量级。
我在实际工作中遇到过的另一个情况是,有人把sklearn的模型整个打包成exe文件去给业务方用。模型本身能跑,但每次预测都要加载整个环境,速度慢得让人崩溃。如果改用权重导出方案,预测函数只有十几行代码,性能提升会非常明显。
最后,如果你学完线性回归还觉得意犹未尽,下一步建议去尝试逻辑回归和多分类问题,原因很简单——逻辑回归的损失函数、梯度下降训练方式与线性回归极其相似,只不过换成了sigmoid函数和交叉熵损失。这时候你会发现,原来在矩阵里加一列1的技巧、标准化特征的操作、评估模型的思路,全部都能复用。学会线性回归,你相当于拿到了整个机器学习的第一把钥匙。
