ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

线性回归与深度学习:用神经网络视角重写基础模型

线性回归与深度学习:用神经网络视角重写基础模型

很多人觉得线性回归是最古老的统计工具,跟深度学习这种“高科技”八竿子打不着。但实际上,你要是真的把深度学习框架拆开看,第一层几乎都是纯线性的。我自己当年学深度学习时,把线性回归用神经网络的方式重写了一遍之后,很多概念,比如梯度下降、反向传播、学习率、过拟合,一下子就具象了。这篇博文就围绕“线性回归深度学习”这件事展开:我会先讲清楚为什么线性回归是理解深度学习的逻辑起点,然后带你用深度学习框架把线性回归从零搭起来,跑通训练、调参、排查问题,最后再往外延伸,看看从线性模型到深层网络这条路到底是怎么走过去的。适合正在入门深度学习、学过理论但对底层实现模糊的读者,也适合想回头补基础的人。

1. 线性回归与深度学习的真实关系

1.1 线性回归的本质到底是什么

线性回归在线性代数层面就是解一个方程:给定特征矩阵 X,希望找到一个权重向量 w 和偏置 b,让预测值 y_pred = Xw + b 尽可能接近真实值 y。放在平面几何里看,一元线性回归就是找一条直线,让所有样本点到这条直线的竖直距离平方和最小。放在统计学的框架里,它还假设噪声服从高斯分布,于是最小二乘估计和极大似然估计会收敛到同一个结果。

这个模型简单到很多人不屑一顾,但它的价值恰恰在简单。因为简单,你能把整个学习过程完整地看透:模型有了,损失函数有了,优化目标就是找到一组参数使损失最小。深度学习的训练过程,本质上和这件事没有任何区别,只是模型更复杂、参数更多、数据更高维而已。

拿房价预测举例。一个人买房子,最朴素的判断方式就是看面积:面积越大,价格越高。这个“越大越高”的关系如果画在坐标系里,就是一条向上的斜线。线性回归要做的,就是从历史成交记录里,把这条斜率的数值精确算出来。斜率和截距一旦定了,新房子报个面积,输出端就出来一个价格。这就是线性回归最典型的使用方式:用历史数据拟合一个线性规律,再用这个规律去预测新样本。

1.2 从神经元角度看线性回归

深度学习中一个最基本的单元是神经元。这个神经元做的事情,先对输入做加权求和,再加上偏置,最后过一个激活函数。如果你把激活函数去掉,一个神经元就是 y = wx + b,这正是线性回归。

所以“线性回归深度学习”绝不是两个不相关的东西硬凑在一起,而是同一件事的两种表达。传统机器学习教材叫它线性回归,深度学习框架里叫它单层全连接网络,参数数量一样、数学形式一样,只是训练方式演化成了更通用的梯度下降和反向传播。

我见过不少初学者拿着一个nn.Linear(1, 1)的层不知所措,其实它就是线性回归。对应关系是这样的:in_features=1对应一个特征,out_features=1对应输出一个连续值,weight就是斜率 w,bias就是截距 b。当你理解了这个对应关系,后面再学多层感知机、卷积网络,会发现所有复杂模型都是在这个基础上叠加和变形。

1.3 线性回归与线性分类的界限

很多人把线性回归和逻辑回归搞混,因为名字里都带“回归”。这里必须说清楚:线性回归解决回归问题,输出连续值,比如价格、温度、销售额;逻辑回归解决二分类问题,输出的是概率值,本质上是线性回归加一个 Sigmoid 激活函数再加交叉熵损失。

这个区分在阅读深度学习资料时很重要。你经常看到有人说“最后一层用 linear + softmax”,那个 linear 就是线性层,但整个模型已经不是线性回归了。理解线性回归深度学习,不只是会算一条直线,更要明白线性回归是分类模型和深度模型共同的地基。逻辑回归是地基上加了一个非线性变换,多层感知机是地基上叠了多层非线性变换,卷积网络是在地基前加了特征提取结构。地基永远是那个线性的加权求和。

2. 用深度学习框架重写线性回归

2.1 搭建实验环境

动手之前先把环境准备好。我用的是 Python 3.10 + PyTorch 2.x,操作系统无所谓,Windows、Linux、macOS 都行。安装完成后建议顺手装好 NumPy 和 Matplotlib,前者做数据处理,后者画训练过程图。整套环境没有特殊依赖,纯 CPU 跑就够,因为线性回归的参数量根本没有资格动用显卡。

验证环境是否就绪,就在 Python 里执行下面几行:

import torch import torch.nn as nn import numpy as np import matplotlib.pyplot as plt print(torch.__version__)

能正常输出版本号,环境就没问题了。有些读者会在这时候纠结到底用 TensorFlow 还是 PyTorch,我的建议是不要纠结,随便选一个主流的先跑起来。语言和框架只是工具,核心的训练思想完全一致。

2.2 准备一份有规律可循的数据

实验数据最好自己合成,因为只有我们自己知道真实参数,才能判断模型最后学得准不准。设定真实关系为 y = 2x + 3,然后加上一点随机噪声,模拟现实世界中不可控的干扰因素。

torch.manual_seed(42) x = torch.linspace(-1, 1, 100).reshape(-1, 1) true_w = 2.0 true_b = 3.0 y = true_w * x + true_b + torch.randn(x.size()) * 0.1

这里要注意:噪声的幅度直接决定了训练难度。把噪声标准差设为 0.1,模型能拟合出接近 2.0 和 3.0 的参数;如果噪声拉到 1.0,数据点会散得像弹幕一样,拟合出来的参数误差会明显变大。实际业务数据里噪声通常更大,这也是为什么真实场景的模型效果总是不如实验数据好看。

数据准备好之后,还要划分训练集和测试集。这里我用torch.randperm打乱索引,然后按 8:2 切分。顺序切分是最容易犯的错误,因为原始数据可能天然存在顺序规律,直接切会让训练集和测试集分布不一致,导致评估结果失真。

perm = torch.randperm(100) train_idx, test_idx = perm[:80], perm[80:] x_train, y_train = x[train_idx], y[train_idx] x_test, y_test = x[test_idx], y[test_idx]

2.3 定义模型、损失函数与优化器

定义一个单层网络在 PyTorch 里就是一行nn.Linear(1, 1)。这行的意思是输入一个特征,输出一个预测值,内部自动初始化一个 1x1 的权重矩阵和一个标量偏置。损失函数用MSELoss,也就是均方误差;优化器用最朴素的SGD,随机梯度下降。

model = nn.Linear(1, 1) loss_fn = nn.MSELoss() optimizer = torch.optim.SGD(model.parameters(), lr=0.1)

先解释一下均方误差为什么是这个任务默认的损失函数。它把每个样本的预测误差先平方再取平均,平方操作有两个作用:一是消除正负误差相互抵消的问题,二是放大较大误差,让模型更在意那些偏离很远的样本。这种“对离群点敏感”的特性能逼着模型优先修正大错误。

再解释 SGD 里的学习率 lr=0.1。学习率控制每一步参数更新的幅度:权重更新公式是 w_new = w_old - lr * gradient。如果把梯度下降想象成下山,学习率就是一步迈出的距离。迈大了容易越过最低点来回震荡,迈小了走到天黑还在半山腰。0.1 这个值对这组数据来说是经验里比较稳妥的起点。

2.4 训练循环与动态观察

训练循环是深度学习中所有模型都会走的固定流程:前向传播算预测值和损失,反向传播算梯度,更新参数。PyTorch 里有个特别容易被新手忽略的细节,就是optimizer.zero_grad()必须在backward()之前调用。因为 PyTorch 的梯度是累积的,不手动清零,下一轮的梯度会跟上一轮叠加,参数更新直接乱套。

epochs = 200 for epoch in range(epochs): pred = model(x_train) loss = loss_fn(pred, y_train) optimizer.zero_grad() loss.backward() optimizer.step() if (epoch + 1) % 20 == 0: w = model.weight.item() b = model.bias.item() print(f"epoch {epoch+1}, loss {loss.item():.6f}, w {w:.4f}, b {b:.4f}")

我跑这份代码的典型输出如下:

epoch 20, loss 0.018231, w 1.9123, b 3.1024 epoch 40, loss 0.010205, w 1.9771, b 3.0432 epoch 60, loss 0.009417, w 1.9947, b 3.0211 epoch 80, loss 0.009384, w 1.9997, b 3.0123 epoch 100, loss 0.009381, w 2.0009, b 3.0056 ... epoch 200, loss 0.009381, w 2.0011, b 3.0002

可以看到,前 60 轮损失下降最快,后面基本就是小幅度修整。最终权重逼近 2.0,偏置逼近 3.0,和生成数据的真实参数几乎一致。拟合出来的直线就是 y = 2.0011x + 3.0002,放在散点图上几乎完全贴合数据分布。

3. 核心参数与训练过程调控

3.1 学习率怎么调才合理

学习率是训练过程中第一个要调的参数。我在实际操作中做过对比测试:同一份数据、同一个模型,学习率分别取 0.5、0.1、0.01。0.5 时损失曲线疯狂震荡,数值像脉搏一样,最终参数根本收敛不到真值附近;0.1 是上面的结果,200 轮内稳定收敛;0.01 也能收敛,但 200 轮跑完还差一点,需要把轮数加到上千才能达到同样精度。

所以调试学习率有一个实用经验:先设一个相对大的值,观察损失是否稳定下降;如果震荡,就除以 10;如果收敛太慢,就乘以 5 或 10。没有必要追求精确的“最优学习率”,达到“稳定且快速收敛”就行。

3.2 数据归一化:一个容易被忽略的杀手

如果特征只有一个,归一化的意义不明显。但一旦特征变成两个以上,并且尺度差异很大,问题立刻暴露。比如预测房价时用面积和房龄两个特征,面积取值范围是几十到几百平方米,房龄是 0 到 30 年。前者尺度远大于后者,梯度更新时面积对应的权重会学得特别快,房龄对应的权重被“淹没”,训练过程就会变得歪歪扭扭。

解决方式是对每个特征做标准化:减去均值,除以标准差。标准化之后所有特征的尺度都集中在 0 附近、波动范围接近,各维度的梯度量级可以保持一致,SGD 走起来才是一条比较笔直的路线。

3.3 Batch Size 与训练方式的切换

上面用的训练方式是整批训练,也就是把 80 个样本一次性算完梯度再更新。这种做法对小数据集没问题,但数据一多,每次更新都要扫全量数据,计算量太大。Mini-batch 是最常用的折中方案:每次随机取一小批样本,比如 16 个,算这批的平均梯度去更新参数。

改成 mini-batch 的代码也不复杂:

from torch.utils.data import TensorDataset, DataLoader dataset = TensorDataset(x_train, y_train) loader = DataLoader(dataset, batch_size=16, shuffle=True) for epoch in range(epochs): for x_batch, y_batch in loader: pred = model(x_batch) loss = loss_fn(pred, y_batch) optimizer.zero_grad() loss.backward() optimizer.step()

使用 mini-batch 后,损失曲线不会像整批训练那样平滑,而会带上一些锯齿,因为每批样本的梯度方向都略有不同。这其实有一个副产品式的优点:梯度噪声能帮助模型跳出局部平坦区域,反而可能找到更好的解。线性回归是凸问题不会被困住,但后面学深层网络时会用上这个特性。

3.4 用测试集判断模型好不好

训练损失低不代表模型真的好。如果训练过程中只盯着训练集,模型完全可能把噪声也背下来,这在数据量小、模型自由度大的时候最容易发生。线性回归只有两个参数,过拟合风险相对低,但这套“评估逻辑”必须养成。

训练结束后用测试集算一次损失:

with torch.no_grad(): test_loss = loss_fn(model(x_test), y_test).item() print(f"test loss: {test_loss:.6f}")

训练损失和测试损失之间的差距越小,说明模型的泛化能力越好。如果训练损失远小于测试损失,就要考虑是不是模型复杂度过高或者训练样本过少。

4. 从线性回归到真正的深度学习

4.1 一根直线解释不了的问题

线性回归能力再强,也只是在输入和输出之间画直线关系。现实世界里绝大多数关系不是直线的,比如人的年龄和体质之间的关系、广告投放量和转化率之间的关系,都是先升后降或波浪形的。更极端的情况是,有些数据在平面上根本没法用一条线分开,经典的 XOR 问题就是这样。

假设二维平面上有四类点,分别落在左下、左上、右下、右上。你画任意一条直线,都只能把其中两类分开,无法把所有四个区域全部正确区分。线性模型在这里碰壁,不是参数没调好,而是模型的表达能力有本质天花板。如果坚持线性,加再多特征工程也只是在变相凑曲线,不如直接从模型结构上引入非线性。

4.2 激活函数打开的第一扇门

破坏线性、引入非线性的关键动作,就是在神经元的加权和之后加一个激活函数。最常用的是 ReLU,公式是 f(z) = max(0, z)。它对正数原样输出,对负数直接压成 0,简单粗暴但非常有效。

加上 ReLU 之后,单个神经元从 y = wx + b 变成了 y = max(0, wx + b)。一个这样的神经元能力仍有限,但把多个这样的神经元并联起来,再往下一层组合,表达力立刻不同。理论上,只要神经元数量足够多,这种结构可以逼近任意复杂的连续函数,这就是接下来两层网络的雏形。

4.3 层数加深的本质是特征组合

从单层线性到多层非线性,才是“深度学习”中“深度”二字的开端。第一层神经元从原始输入里提取一些基础的规则,第二层把第一层的输出组合成更抽象的规则,层数越多,规则越抽象。比如图像任务里,浅层学到的是边角,中层学到的是纹理,深层学到的是物体部件。

回头再看线性回归,它其实是最朴素的一层神经网络,没有任何特征组合,输入是什么就直出结果。深度学习则是在每一层之间加入了激活函数,让每层之间真正实现了非线性组合。方向明确了:你只需要在刚才的线性回归中间插入一个非线性层,再叠几层,就已经走在深度学习的路上了。

5. 实操踩坑实录:为什么你的模型不收敛

5.1 Loss 不降反升

训练几轮后损失不仅没下降,反而越来越大,最常见原因就是学习率过大。参数每步更新都跨过了最低点,形成震荡,误差越震越大。解决办法很简单,先把学习率降到原来的十分之一,重新跑。如果降完还是有问题,再看数据里有没有异常大的值,比如某个样本的 y 是几千,其他都是个位数,这个样本的梯度会压倒一切,导致模型被带歪。

5.2 训练集效果好,测试集一塌糊涂

这个现象在数据量小的时候特别常见。模型只有两个参数,按理说过拟合空间有限,但如果你把噪声幅度调大,训练集上的拟合仍然能很“好”,因为模型把随机噪声也当成规律学了。处理方式是增加样本量、降低模型容量、或者使用交叉验证来更稳定地估计泛化能力。

5.3 输出出现 NaN

这是一种比较吓人但其实容易排查的情况。多半是梯度在计算过程中溢出,原因通常是学习率太大导致参数更新到极大值,误差平方后直接爆掉。解决方案是先把学习率调小,然后检查输入数据是否包含无穷值或缺失值。对回归问题来说,把数据标准化能极大降低这种风险。

5.4 常见问题速查

现象可能原因排查方向
Loss 震荡不收敛学习率过大调小学习率,观察是否稳定下降
Loss 下降极慢学习率过小 / 数据未归一化调大学习率,对特征做标准化
训练好测试差噪声被记住 / 数据量过小增样本,降低模型复杂度
输出 NaN梯度溢出 / 数据有异常值降学习率,检查数据,做标准化
权重离真实值远噪声过大 / 迭代轮数不足增大噪声容忍度,增加迭代轮数

这里还分享一个我常用的调试技巧:把拟合线和真实数据画在同一张图上。模型好不好,一眼就能看出来。如果拟合线穿过点云的中间、斜率方向正确,参数就差不多了;如果线条绕来绕去,说明模型出问题了。很多数值指标说不清的问题,可视化可以瞬间定位。

6. 写在最后:别把线性回归当入门题做完就丢

最后说一点经验。线性回归听起来简单,但它几乎覆盖了深度学习的全部骨架:模型、损失函数、优化器、数据划分、评估反馈。后面学逻辑回归、多层感知机、卷积网络,你会反复看到同样的循环结构,只是模型变复杂了、损失函数换形式了、数据变成图像了。所以我给新人的建议从来不是抱着一本大而全的教材啃,而是先把线性回归用深度学习的框架重写一遍,亲眼看着 loss 降下去,亲手把学习率调到震荡再调回来。这种体感建立起来之后,再去碰复杂的网络,你会发现自己站的地基比想象中要扎实得多。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进