news 2026/9/8 8:33:47

从零实现简单线性回归:从损失函数到梯度下降的Python实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从零实现简单线性回归:从损失函数到梯度下降的Python实践

身边有不少刚接触机器学习的同学,一开始都会陷入一个相同的困惑:公式能看懂,代码能跑通,但换个数据集就不知道怎么用了。尤其是线性回归,看起来只是y = wx + b这样一条直线,可一旦涉及损失函数、梯度下降、特征缩放这些概念,很多人就开始“公式会背,代码不会写”。

这篇文章想解决的就是这个问题。我会用最小可运行的 Python 代码,把简单线性回归从直觉到实现完整讲一遍。不堆数学公式,但会把核心概念讲透;不追求一次讲完所有回归模型,但会帮你把最简单的线性回归彻底搞懂。如果你正在准备机器学习期末复习,或者刚开始学机器学习,这篇文章值得收藏后慢慢看。

1. 这篇文章真正要解决的问题

先说判断:线性回归不是期末考试背完就能放下的知识点,它是你理解机器学习“学习过程”的第一把钥匙。

为什么这么说?因为机器学习模型不管多复杂,核心都逃不开三件事:模型假设、损失函数、优化算法。线性回归恰好是这三件事最朴素的载体。你搞懂了线性回归里的梯度下降,后面学逻辑回归、神经网络、支持向量机,都会觉得“原来还是那套东西”。

但很多教程的讲法有问题。它们通常只给一段 sklearn 代码,然后告诉你“看,拟合成功了”。于是你学会了调库,却没学会思考。一旦遇到这样的问题——数据量很小、特征只有一个、老板让你解释模型为什么给出这个预测值——你就会发现只会调库远远不够。

这篇文章会带你做两件事:

  1. 用 NumPy 从零实现一次简单线性回归,让梯度下降的过程在你的眼前“动”起来。
  2. 用 Scikit-Learn 实现同一件事,让你知道工程上成熟的工具是怎么用的。

学完之后,你能做到:

  • 说明白线性回归在做什么,以及它的局限在哪里;
  • 从零写出一段能收敛的线性回归代码;
  • 读懂 sklearn 的输出,判断模型到底拟合得好不好;
  • 搞清楚什么时候该做特征缩放,什么时候该处理异常值。

整个过程不需要高深的数学功底,会 Python 基础语法、能装依赖库就够。

2. 简单线性回归的核心概念与直觉

2.1 线性回归解决的现实问题

想象一个场景:你是一家披萨店的店长,想预测“每天外卖订单量”和“当日气温”的关系。你手上有过去 30 天的数据,横轴是气温,纵轴是订单量。你希望找出一个公式,输入明天的气温,就能预测大概能卖多少单。

这就是线性回归的典型场景:用一个或多个输入特征,预测一个连续的数值输出。当输入特征只有一个时,就是“简单线性回归”;有多个输入特征时,是“多元线性回归”。

注意“连续数值”这四个字。如果预测目标是“明天会不会下雨”(分类问题),或者“明天订单量属于高/中/低哪一类”(分类问题),那就不是线性回归的适用范围了。

2.2 模型假设:一条直线

简单线性回归的模型假设是:

y = w * x + b

这里:

  • x是输入特征(比如气温);
  • y是预测目标(比如订单量);
  • w是权重(weight),决定直线的斜率;
  • b是偏置(bias),决定直线的截距。

你可能会想:这也太简单了吧,真实世界哪有这么完美的线性关系?

确实没有。所以线性回归的完整假设是:

y_pred = w * x + b y_true = y_pred + noise

也就是说,真实观测值是在理想直线的基础上叠加了噪声。线性回归要做的,就是在噪声存在的情况下,找到一条尽可能接近真实规律的直线

2.3 损失函数:衡量“错得有多离谱”

有了模型,接下来要回答一个问题:这一组 w 和 b 到底好不好?

直观想法是:把每条样本的预测值和真实值做差,然后把这些差值综合起来。差得越多,说明这组参数越差。

最常用的损失函数是均方误差(Mean Squared Error, MSE)

MSE = (1/n) * Σ(y_true_i - y_pred_i)²

平方的作用有两个:

  1. 消除正负误差互相抵消的问题;
  2. 放大大误差的惩罚力度——差 2 个单位时损失是 4,差 3 个单位时损失是 9,大误差会被更快地“注意到”。

所以线性回归的目标就是:找到一组 w 和 b,让 MSE 尽量小

2.4 梯度下降:靠“下山”找最优解

MSE 是关于 w 和 b 的函数。如果画出不同 w 对应损失值的曲线,会得到类似碗状的图形。我们要做的事情,就是走到碗底(损失最小处)。

梯度下降的思路和“摸黑下山”很像:站在山坡上,看不清全貌,但能感受到脚下哪个方向是下坡。往坡度最陡的方向迈一步,再感受一次,再迈一步,反复迭代,直到到达谷底。

每一步的更新规则是:

w = w - learning_rate * grad_w b = b - learning_rate * grad_b

其中learning_rate(学习率)是步长。步长太大容易“跨过”碗底,步长太小则收敛太慢。

这就是线性回归背后最核心的直觉。如果到这里你觉得有点绕,没关系,下一节我们用代码把它透明化,让每一步都可视化。

3. 环境准备与工具选择

为了保证文章里的代码你能直接跑通,这里先统一一下环境。此文的代码基于 Python 3.9+ 编写,在 Windows 10 / macOS / Linux 上都可以运行。

依赖库如下:

  • numpy:用于数值计算和手写回归实现;
  • pandas:用于加载和操作数据(可选,但推荐);
  • matplotlib:用于可视化结果;
  • scikit-learn:用于调用成熟模型和评估指标。

3.1 创建虚拟环境(推荐)

建议在项目目录下创建独立虚拟环境,避免依赖冲突:

python -m venv .venv

激活环境:

Windows:

.venv\Scripts\activate

macOS / Linux:

source .venv/bin/activate

3.2 安装依赖

pip install numpy pandas matplotlib scikit-learn

3.3 验证安装

python -c "import numpy, pandas, matplotlib, sklearn; print('All dependencies OK')"

如果能正常输出All dependencies OK,就说明环境已经就绪。版本方面以你实际安装的为准,本文的重点是演示通用思路,不依赖任何特定版本的新功能。

4. 用 NumPy 从零实现简单线性回归

这一节是文章的核心。我们不用任何机器学习库,只用 NumPy 一步步实现“训练”过程。你会亲眼看到 w 和 b 是怎么从随机值变成合理值的。

4.1 构造测试数据

先构造一组带线性关系的数据,方便验证代码是否正确:

import numpy as np import matplotlib.pyplot as plt # 固定随机种子,保证结果可复现 np.random.seed(42) # 生成 100 个样本,特征 x 在 0 到 10 之间均匀分布 X = np.linspace(0, 10, 100) # 真实关系近似为 y = 2 * x + 5,并添加高斯噪声 true_w = 2.0 true_b = 5.0 y = true_w * X + true_b + np.random.normal(0, 1.5, size=X.shape) # 可视化 plt.figure(figsize=(8, 5)) plt.scatter(X, y, s=30, alpha=0.7, label="sample data") plt.xlabel("X") plt.ylabel("y") plt.title("Synthetic Data: y ≈ 2*x + 5") plt.legend() plt.grid(alpha=0.3) plt.show()

运行这段代码,你会看到数据点大致沿着一条斜线分布,但因为加入了噪声,并不是严格落在直线上。这正是真实数据的特征——有规律,但不完美。

4.2 定义模型、损失函数和梯度

接下来定义线性回归的三件套:

def predict(X, w, b): """线性模型预测值 y_pred = w * X + b""" return w * X + b def mse_loss(y_true, y_pred): """均方误差损失""" return np.mean((y_true - y_pred) ** 2) def compute_gradients(X, y_true, w, b): """计算损失函数关于 w 和 b 的梯度""" n = len(X) y_pred = predict(X, w, b) error = y_true - y_pred grad_w = (-2.0 / n) * np.sum(X * error) grad_b = (-2.0 / n) * np.sum(error) return grad_w, grad_b

这里需要解释一下compute_gradients里的式子是怎么来的。我们的损失函数是:

L = (1/n) * Σ (y_true_i - (w * x_i + b))²

对 w 求偏导,得到(2/n) * Σ (y_pred_i - y_true_i) * x_i,换一下正负号就是(-2/n) * Σ (x_i * error_i)。对 b 求偏导同理。你不用每次都从头推导,但最好理解代码和公式的对应关系。

4.3 梯度下降训练循环

有了梯度和模型,就可以写训练循环了:

def train_linear_regression(X, y_true, w_init=0.0, b_init=0.0, learning_rate=0.01, n_iters=1000): """梯度下降训练简单线性回归""" w = w_init b = b_init history = [] for i in range(n_iters): grad_w, grad_b = compute_gradients(X, y_true, w, b) w = w - learning_rate * grad_w b = b - learning_rate * grad_b if i % 100 == 0: loss = mse_loss(y_true, predict(X, w, b)) history.append((i, loss, w, b)) print(f"Iter {i:4d} | Loss: {loss:.4f} | w: {w:.4f} | b: {b:.4f}") return w, b, history # 训练模型 w_final, b_final, history = train_linear_regression( X, y, w_init=0.0, b_init=0.0, learning_rate=0.01, n_iters=1000 ) print(f"\nFinal result: w = {w_final:.4f}, b = {b_final:.4f}")

注意学习率learning_rate这里需要根据特征尺度选择。因为 X 的范围是 0 到 10,y 的范围大约在 5 到 25,0.01是一个相对安全的起始值。如果你的数据 X 的尺度和这里相差很大,学习率也要相应调整。稍后我们会专门讨论这个问题。

4.4 可视化拟合结果

训练结束后,把拟合直线画出来:

# 生成预测线 y_pred_line = predict(X, w_final, b_final) plt.figure(figsize=(8, 5)) plt.scatter(X, y, s=30, alpha=0.7, label="sample data") plt.plot(X, y_pred_line, color="red", linewidth=2, label=f"Fitted: y = {w_final:.2f} * x + {b_final:.2f}") plt.plot(X, 2.0 * X + 5.0, color="green", linestyle="--", linewidth=2, label="True: y = 2.0 * x + 5.0") plt.xlabel("X") plt.ylabel("y") plt.title("Linear Regression Fitting Result") plt.legend() plt.grid(alpha=0.3) plt.show()

至此,你已经从零实现了一次完整的线性回归训练过程。

5. 用 Scikit-Learn 快速实现线性回归

从零实现能帮你理解原理,但在实际项目中,我们通常会用成熟的库来提升开发效率。Scikit-Learn 的LinearRegression内部会调用更稳定、更高效的算法(比如基于最小二乘的直接求解),不需要手动调学习率。

5.1 数据划分

先划分训练集和测试集:

from sklearn.model_selection import train_test_split # sklearn 要求特征为二维数组,这里做维度转换 X_2d = X.reshape(-1, 1) X_train, X_test, y_train, y_test = train_test_split( X_2d, y, test_size=0.2, random_state=42 ) print(f"训练集样本数: {X_train.shape[0]}") print(f"测试集样本数: {X_test.shape[0]}")

划分的目的是检验模型在“没见过”的数据上的表现,避免只会在训练集上“背答案”。

5.2 模型训练与预测

from sklearn.linear_model import LinearRegression model = LinearRegression() model.fit(X_train, y_train) # 测试集预测 y_test_pred = model.predict(X_test) print(f"模型截距 (b): {model.intercept_:.4f}") print(f"模型系数 (w): {model.coef_[0]:.4f}")

这里的model.intercept_对应上文代码里的bmodel.coef_[0]对应w。如果数据分布接近线性,这两个值应该分别接近真实值5.02.0

5.3 评估模型效果

from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score mse = mean_squared_error(y_test, y_test_pred) mae = mean_absolute_error(y_test, y_test_pred) r2 = r2_score(y_test, y_test_pred) print(f"测试集 MSE: {mse:.4f}") print(f"测试集 MAE: {mae:.4f}") print(f"测试集 R²: {r2:.4f}")

三个指标各说明一个问题:

  • MSE:误差的平方平均,对大误差敏感;
  • MAE:误差的绝对平均,更直观,与原始数据同量纲;
  • :模型解释了目标变量多少比例的方差,最大为 1,越接近 1 越好。如果 R² 接近 0,说明模型几乎没有预测能力。

6. 运行结果与效果验证

6.1 预期输出参考

按照前面的合成数据(真实关系y = 2x + 5,噪声标准差 1.5),一个合理运行结果大致是:

Iter 0 | Loss: 474.0012 | w: 0.0000 | b: 0.0000 Iter 100 | Loss: 34.7521 | w: 1.3204 | b: 3.2145 Iter 200 | Loss: 3.5612 | w: 1.8723 | b: 4.6012 Iter 300 | Loss: 2.0314 | w: 1.9766 | b: 5.1024 Iter 400 | Loss: 1.8972 | w: 2.0012 | b: 5.2123 ... Final result: w ≈ 2.01, b ≈ 5.10

注意,由于随机噪声的存在,你的具体数值不会和上面的完全一致,但 w 和 b 应该分别落在2.05.0附近,且迭代到后期 Loss 不再明显下降,这些就说明训练成功。

6.2 如何判断成功

判断训练是否成功,可以从三个角度观察:

  1. 损失是否收敛:输出的 Loss 应该随着迭代逐步下降,最后稳定在一个较小的值附近。如果 Loss 一直上升或剧烈震荡,说明学习率设置有问题或数据没有做归一化。
  2. 参数是否接近真实值:对于我们构造的合成数据,答案是已知的。如果最终 w 和 b 与真实值相差很大,模型很可能没有收敛。
  3. 测试集表现是否接近训练集:如果测试集误差远大于训练集误差,可能存在过拟合(对于简单线性回归较少见,但特征多时要注意)。

6.3 如果运行失败,先排查什么

按下面的顺序排查:

  1. 看报错信息。最常见的是ValueError,说明数组维度不匹配,检查X_2d是否已经转换为二维。
  2. 看是否有NaN。如果损失函数出现NaN,几乎都是学习率过大导致的梯度爆炸。
  3. 看数据范围。如果特征数值特别大(比如几万、几十万),梯度下降很容易不稳定,此时需要进行特征缩放。

7. 常见问题与排查思路

以下是初学者在实现或使用线性回归时最容易遇到的问题:

问题现象可能原因排查方式解决方案
训练时 Loss 出现 NaN学习率过大,梯度更新步长过大,数值溢出打印每一轮的 Loss,观察发散点将学习率调小(如从 0.01 改为 0.001),或对特征做标准化
模型训练完,w 和 b 仍是初始值 0学习率过小,迭代次数不够打印前几次迭代的梯度值增大学习率,或增加n_iters
把一维数组传给 sklearn 时报维度错误sklearn 要求特征是二维,形如(n_samples, 1)打印数组shape属性使用X.reshape(-1, 1)
测试集 R² 为负数模型在测试集上比直接预测均值还差检查训练集和测试集分布是否一致重新划分数据,或检查特征是否与目标变量确实存在线性关系
预测值全部相同模型没有有效利用特征,可能是梯度消失或特征尺度问题检查 w 是否接近 0标准化特征,或检查特征中是否存在大量缺失值
数据有少量极端异常值,拟合线被“拉偏”线性回归对异常值敏感,因为 MSE 会放大大误差画出散点图和拟合线,观察偏离点先做数据清洗,或改用对异常值更鲁棒的模型(如 RANSAC、HuberRegressor)

其中异常值对线性回归的影响尤其值得多说一句。MSE 对误差做了平方,如果一个点的误差是正常点的 10 倍,它对损失的贡献就是正常点的 100 倍。这意味着,哪怕只有一个异常值,也可能把整条回归线“拉”向它。实际项目中,画图看数据永远是最先要做的事情。

8. 最佳实践与工程建议

如果你已经跑通了上面的代码,可以继续看这一节。这些建议能帮助你从“跑通教程”走向“做好真实项目”。

8.1 先画图,再建模

拿到数据,不要急着训练任何模型。先用散点图、直方图、箱线图理解数据分布,看看特征与目标之间是否存在明显的线性趋势,是否存在异常值,是否存在缺失值。线性回归是一个强假设模型,如果真实关系不是线性的,你后面做再多调参也是徒劳。

8.2 特征缩放的价值

对于简单线性回归,特征缩放不是必需的,因为只有 x 一个维度。但一旦进入多元线性回归,特征缩放就变得非常重要。当不同特征的数值尺度差异巨大时(比如一个特征在 0-1 之间,另一个特征在 0-100000 之间),梯度下降的收敛速度会非常慢,甚至有可能不收敛。

常用的做法是标准化(Standardization):

from sklearn.preprocessing import StandardScaler scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) X_test_scaled = scaler.transform(X_test)

注意:fit_transform只能作用于训练集,测试集必须用同一个scalertransform,不能重新 fit,否则就会造成数据泄露。

8.3 不要把所有数据都用于训练

留着测试集非常关键。很多作业和期末项目里,有人直接在全部数据上训练,然后给出一个“好看”的误差。但那样无法证明模型的泛化能力。推荐的划分方式因数据量而异:数据量大于 10000 时,可以留 20% 作为测试集;数据量很小时,考虑交叉验证。

8.4 评估指标的选择

R² 和 MSE 是线性回归最常用的指标,但不要只看一个。R² 丢失了量纲信息,MSE 又会放大异常值的权重。实际项目中,建议同时输出 R²、MSE、MAE 三个指标。如果是给业务方报告,MAE 往往最好解释:“平均来看,预测值与真实值相差 X 个单位”。

8.5 模型的保存和加载

训练好的模型要能复用,使用joblibpickle持久化:

import joblib # 保存模型 joblib.dump(model, "linear_model.pkl") # 加载模型 loaded_model = joblib.load("linear_model.pkl")

在真实生产环境中,模型的版本、训练数据版本、特征版本都需要记录在案,否则后期排查问题时你会发现模型“突然不灵了”却不知道是哪个环节发生了变化。

8.6 注意模型的使用边界

线性回归能做很多事,但不是万能。当数据呈现明显曲线关系(比如抛物线),或者特征与目标关系复杂时,线性回归会欠拟合。这时候你需要考虑多项式回归、树模型或者其他非线性模型。判断是否欠拟合的方法很直接:训练集上的误差也很大,说明模型本身的表达能力不足。

8.7 生产环境中的监控意识

模型上线不是终点。真实数据会随着时间变化,曾经学习到的线性关系可能逐渐失效。定期用最新数据评估模型表现,并设置止损阈值,是机器学习工程中非常重要的一环。即使只是做一个期末项目或入门练习,也值得养成这种“监控闭环”的思维方式。

9. 总结与后续学习方向

到这一步,你已经亲手完成了一次完整的机器学习建模流程:构造数据、实现模型、定义损失、用梯度下降优化、用 sk-learn 工具实现、评估模型效果、排查常见错误。这套流程换成其他模型,依然成立。

关于简单线性回归,真正重要的是以下几点:

  • 简单线性回归解决的是“用一个连续输入预测一个连续输出”的问题,模型假设是一条直线;
  • MSE 是线性回归最常用的损失函数,它让大误差获得更大惩罚;
  • 梯度下降是一种通用优化方法,线性回归只是它的一个应用场景;
  • sklearn 里的LinearRegression使用步骤非常简单,但理解背后的原理才能应对真实问题中的种种意外;
  • 画图检查数据、划分训练测试集、关注模型边界,这些比“调参”重要得多。

作为下一步,你可以按这个顺序继续探索:

  1. 多元线性回归:从y = w1*x1 + w2*x2 + b开始,理解多个特征如何协同影响预测结果。
  2. 逻辑回归:把线性回归的目标从“预测连续值”换到“预测概率”,这会打开分类问题的大门。
  3. 多项式回归:用线性回归的思路拟合曲线关系,体验“特征工程”的魔力。
  4. 正则化回归(Ridge / Lasso):理解当特征变多时,如何防止模型过拟合。

这篇文章的代码建议你在本地跑一遍,然后尝试改几个参数(尤其是学习率和数据噪声),观察结果的变化。亲手调整参数带来的体感,比读十篇文章都要深刻。建议收藏备用,遇到不熟悉的知识点随时回来对照。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 8:33:24

哼唱生成音乐全流程指南:从输入优化到批量处理

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/8 8:32:27

Spring Boot家装项目管理系统:从需求到远程调试的完整实战

做装修公司信息化这行快十年,见过太多工地上“人盯人”的管理方式了。项目经理翻着手机找聊天记录报进度,老板想看一眼各工地资金占用情况得等财务月底拉Excel,客户三天两头问“我家装到哪一步了”却得不到准确答复——这些都是装修公司项目管…

作者头像 李华
网站建设 2026/9/8 8:30:53

系统稳定性保障:从可观测性到架构治理的工程实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/8 8:29:20

HomeAssistant接入大模型:三步实现智能家居自然语言控制

HomeAssistant 接入 ChatGPT、DeepSeek 这类 AI 大模型,核心链路其实只有三步:把设备状态整理成文本,把文本发给大模型接口,再把返回结果用起来。我按自己实际调试时的顺序来拆,从环境准备、最小请求、自然语言控制到安…

作者头像 李华
网站建设 2026/9/8 8:29:06

Spring Security从入门到实战:认证授权与过滤器链详解

Spring Security在Java后端领域几乎是绕不开的一座山,尤其是做企业级应用、涉及用户登录和权限控制的时候。我第一次真正深入接触它,是在接手一个老项目时——当时系统里塞满了自定义拦截器,每个接口都在手写session校验,逻辑还散…

作者头像 李华
网站建设 2026/9/8 8:27:03

NVIDIA 616.56驱动实测:AI视频生成提速20%、显存占用大降

各位玩本地AI生成的朋友,最近驱动圈有个消息值得关注:NVIDIA发布了616.56版本驱动,官方放出的说法是让AI视频生成速度提升20%、显存占用降低40%。这组数据一出来,很多在ComfyUI里折腾Wan、Hunyuan视频生成的人都在讨论&#xff0c…

作者头像 李华