深度学习-线性回归

参考:《动手学深度学习》,https://zh-v2.d2l.ai/index.html

引用自《动手学深度学习》:回归 (regression)是能为一个或多个自变量与因变量之间关系建模的一类方法。在自然科学和社会科学领域,回归经常用来表示输入和输出之间的关系。在机器学习领域中的大多数任务通常都与预测(prediction)有关。当我们想预测一个数值时,就会涉及到回归问题。常见的例子包括:预测价格(房屋、股票等)、预测住院时间(针对住院病人等)、预测需求(零售销量等)。

线性回归 (linear regression)可以追溯到19世纪初,它在回归的各种标准工具中最简单而且最流行。线性回归基于几个简单的假设:首先,假设自变量x\mathbf{x}x和因变量yyy之间的关系是线性的,即yyy可以表示为x\mathbf{x}x中元素的加权和,这里通常允许包含观测值的一些噪声;其次,我们假设任何噪声都比较正常,如噪声遵循正态分布。

一些题外话,如今有了各种深度学习框架,model.fit() 敲一下,模型就训练好了。但不知道你有没有好奇过------这背后到底发生了什么?梯度是怎么算出来的?参数又是怎么一步步更新的?这篇笔记,我们抛开所有"黑盒"工具,从最基础的数学公式出发,从零开始实现线性回归

本文脉络

  1. 问题定义:我们要解决什么问题?为什么挑这个?
  2. 数据准备:造一批带噪声的样本数据
  3. 数学推导:前向传播、损失计算、反向传播------手把手推导每一步公式
  4. 代码实现与训练:NumPy 手写实现,跑通训练并可视化
  5. 换个角度:用 PyTorch 优雅实现同样的功能

预计阅读时间:15 分钟 | 难度:入门 | 前置知识:线性代数基础、Python 基础

一、问题定义:我们要拟合什么?

1.1 目标函数

为了方便说明,我们假设目标函数具有如下表达式:

y=4x+0.8y = 4x + 0.8 y=4x+0.8

你可能觉得,这不就是一次函数吗?初中就学过了。但别忘了:所有复杂的深度学习模型,归根结底都是线性变换和非线性激活的层层叠加。把这个最基础的情况吃透了,后面再复杂的架构也不怕。

1.2 构建模型

在这个简单的例子里,我们已经充分理解了输入和输出之间的变换关系。带着这些先验知识,仅需构建一个一维线性变换模型就能很好地拟合目标函数,如下所示:

y^=wx+b\hat{y} = wx + by^=wx+b

拆开来说:

  • www:权重(weight) ,一开始随机给个值,训练的目标是让它慢慢靠近 444
  • bbb:偏置(bias) ,同样从随机值出发,目标是逼近 0.80.80.8
  • y^\hat{y}y^:模型跑出来的预测值

换个角度想 :从神经网络的视角看,这就是一个不带激活函数的单层全连接层。别嫌它简单,把这个搞明白了,后面加隐藏层、加激活函数,自然就水到渠成了。

二、数据准备:造一批训练样本

2.1 数据怎么来?

现实世界中的数据哪有那么完美,老老实实落在目标函数曲线上的情况几乎不存在。所以我们的做法是:

  1. 在区间 [−2,2][-2, 2][−2,2] 上均匀采样N个点 ,生成输入 XXX,X=[x0,x1,......,xN−1]⊤X=[x_0,x_1,......,x_{N-1}]^\topX=[x0,x1,......,xN−1]⊤
  2. 套入理想公式,算出理论输出 Yideal=4X+0.8Y_{\text{ideal}} = 4X + 0.8Yideal=4X+0.8
  3. 撒点高斯噪声 ϵ∼N(0,σ2)\epsilon \sim \mathcal{N}(0, \sigma^2)ϵ∼N(0,σ2),模拟真实场景中的测量误差,得到输出 YYY,Y=[y0,y1,......,yN−1]⊤Y=[y_0,y_1,......,y_{N-1}]^\topY=[y0,y1,......,yN−1]⊤

最终拿到手的数据长这样:

Y=Yideal+ϵ=4X+0.8+ϵ,ϵ∼N(0,0.32)Y = Y_{\text{ideal}} + \epsilon = 4X + 0.8 + \epsilon, \quad \epsilon \sim \mathcal{N}(0, 0.3^2)Y=Yideal+ϵ=4X+0.8+ϵ,ϵ∼N(0,0.32)

python 复制代码
# 导入必要的库
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.gridspec import GridSpec
import matplotlib
import matplotlib.font_manager as fm

# ==================== 中文字体配置 ====================
# 重要:必须在 plt.style.use() 之后设置,否则会被覆盖!
# macOS 可用中文字体(按优先级):
# STHeiti - 系统黑体 | Songti SC - 宋体 | PingFang HK - 苹方

# 方法1:使用 rcParams 设置字体
plt.rcParams['font.sans-serif'] = ['STHeiti', 'Songti SC', 'PingFang HK', 'Kaiti SC', 'Heiti TC']
plt.rcParams['axes.unicode_minus'] = False  # 正常显示负号

# 方法2:使用 FontProperties 确保字体生效(备用方案)
# 查找系统中可用的中文字体
def get_chinese_font():
    """获取可用的中文字体名称"""
    chinese_fonts = ['STHeiti', 'Songti SC', 'PingFang HK', 'Kaiti SC', 'Heiti TC']
    available_fonts = [f.name for f in fm.fontManager.ttflist]
    for font in chinese_fonts:
        if font in available_fonts:
            return font
    return None

CHINESE_FONT = get_chinese_font()
if CHINESE_FONT:
    print(f"找到中文字体: {CHINESE_FONT}")
    # 使用找到的字体覆盖默认设置
    plt.rcParams['font.family'] = 'sans-serif'
    plt.rcParams['font.sans-serif'] = [CHINESE_FONT]
else:
    print("未找到中文字体,尝试使用默认字体")

print("环境就绪!NumPy版本:", np.__version__)
print("当前字体配置:", plt.rcParams['font.sans-serif'][:3])
# 🎲 设置随机种子保证可复现性
np.random.seed(42)

# ==================== 数据生成参数 ====================
N = 100              # 样本数量
true_w = 4.0         # 真实权重
true_b = 0.8         # 真实偏置
noise_std = 0.3      # 噪声标准差

# 生成输入数据 X: 形状 (N, 1) - 在 [-2, 2] 区间均匀采样
X = np.random.uniform(-2, 2, size=(N, 1)).astype(np.float32)

# 生成目标数据 Y = 4X + 0.8 + 噪声: 形状 (N, 1)
noise = np.random.normal(0, noise_std, size=(N, 1)).astype(np.float32)
Y = true_w * X + true_b + noise

print(f"✅ 数据生成完成!")
print(f"   X 形状: {X.shape},范围: [{X.min():.2f}, {X.max():.2f}]")
print(f"   Y 形状: {Y.shape},范围: [{Y.min():.2f}, {Y.max():.2f}]")
print(f"   真实参数: w={true_w}, b={true_b}")
复制代码
找到中文字体: STHeiti
环境就绪!NumPy版本: 2.2.6
当前字体配置: ['STHeiti']
✅ 数据生成完成!
   X 形状: (100, 1),范围: [-1.98, 1.95]
   Y 形状: (100, 1),范围: [-6.96, 8.82]
   真实参数: w=4.0, b=0.8

2.2 先看看数据长什么样

python 复制代码
# 📈 可视化数据分布
fig, ax = plt.subplots(figsize=(9, 6))

# 绘制散点图(含噪声的观测数据)
ax.scatter(X, Y, alpha=0.6, edgecolors='white', linewidth=0.5, s=50, 
           label='含噪声的观测数据', color='steelblue')

# 绘制真实函数(不含噪声)
x_line = np.linspace(-2.2, 2.2, 100).reshape(-1, 1)
y_line = true_w * x_line + true_b
ax.plot(x_line, y_line, 'r--', linewidth=2.5, label=f'真实函数: $y = {true_w}x + {true_b}$', alpha=0.8)

ax.set_xlabel('$x$', fontsize=14, fontweight='bold')
ax.set_ylabel('$y$', fontsize=14, fontweight='bold')
ax.set_title('数据集分布:目标函数与观测数据', fontsize=16, fontweight='bold', pad=15)
ax.legend(fontsize=12, loc='upper left')
ax.grid(True, alpha=0.3)

# 添加文本标注
ax.text(0.5, 7.5, f'样本数: {N}\n噪声标准差: {noise_std}', 
        fontsize=11, bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.5))

plt.tight_layout()
plt.show()

2.3 维度这件事,一开始就要说清楚

做矩阵运算,最怕维度对不上。所以我们从一开始就把每个量的形状标得明明白白:

符号 含义 维度 备注
XXX 输入特征矩阵 (N,1)(N, 1)(N,1) 行数表示样本数,表示NNN 个样本;列数表示特征维度,这里每个样本 1 个特征
YYY 目标标签 (N,1)(N, 1)(N,1) 行数表示样本数,表示NNN 个样本;列数表示输出维度,这里是1表示每个样本只有一个输出值
WWW 权重矩阵 (1,1)(1, 1)(1,1) 输入维度 1 → 输出维度 1
bbb 偏置 (1,1)(1, 1)(1,1) 广播到 (N,1)(N, 1)(N,1),与每个样本相加
ϵ\epsilonϵ 噪声 (N,1)(N, 1)(N,1) 独立同分布的高斯噪声

多嘴一句 :为什么要用 (N,1)(N, 1)(N,1) 的二维数组,而不是干脆用 (N,)(N,)(N,) 的一维数组?

因为二维形状能让后续的矩阵乘法维度对齐更加直观。NumPy 的广播机制虽然方便,但偶尔也会让人摸不着头脑,明确维度能少踩很多坑。

三、数学推导:从问题到解法的完整链路

接下来是这篇笔记的重头戏。我们会像剥洋葱一样,一层层把前向传播、损失函数、反向传播的数学过程掰开揉碎,保证每一步都能跟得上。

3.1 前向传播(Forward Pass)

先看单个样本

假设现在只有一个样本 (x,y)(x, y)(x,y),模型要做的事情很简单:

y^=wx+b\hat{y} = wx + by^=wx+b

这里的每个符号代表什么:

  • x∈Rx \in \mathbb{R}x∈R:输入,一个实数
  • w∈Rw \in \mathbb{R}w∈R:权重,模型要学的参数
  • b∈Rb \in \mathbb{R}b∈R:偏置,也是模型要学的参数
  • y^∈R\hat{y} \in \mathbb{R}y^∈R:模型给出的预测值

扩展到批量样本(矩阵形式)

实际训练时,我们一般不会一个一个样本地喂,而是一批一起算。假设这批有 NNN 个样本,公式就升级成了:

Y^=XW+b\hat{Y} = XW + bY^=XW+b

维度拆解

X⏟(N,1)⋅W⏟(1,1)+b⏟(1,1)=Y^⏟(N,1)\underbrace{X}{(N, 1)} \cdot \underbrace{W}{(1, 1)} + \underbrace{b}{(1, 1)} = \underbrace{\hat{Y}}{(N, 1)}(N,1) X⋅(1,1) W+(1,1) b=(N,1) Y^

一个一个来看:

  • X∈RN×1X \in \mathbb{R}^{N \times 1}X∈RN×1:输入矩阵,NNN 行 1 列,每个样本占一行
  • W∈R1×1W \in \mathbb{R}^{1 \times 1}W∈R1×1:权重矩阵,本质上就是个标量
  • b∈R1×1b \in \mathbb{R}^{1 \times 1}b∈R1×1:偏置,靠 NumPy 的广播机制自动加到每个样本上
  • Y^∈RN×1\hat{Y} \in \mathbb{R}^{N \times 1}Y^∈RN×1:预测输出,维度跟输入保持一致

一个小技巧 :做矩阵运算的时候,养成随手检查维度的习惯。(N×1)×(1×1)(N \times 1) \times (1 \times 1)(N×1)×(1×1) 的结果是 (N×1)(N \times 1)(N×1),刚好跟目标 YYY 的维度对齐。维度能对得上,公式基本就没问题了。

3.2 损失函数(Loss Function)

模型预测得准不准,得有个衡量标准。这里我们选的是均方误差(Mean Squared Error, MSE)

L=1N∑i=1N(y^i−yi)2L = \frac{1}{N}\sum_{i=1}^{N}(\hat{y}_i - y_i)^2L=N1i=1∑N(y^i−yi)2

写成矩阵的形式更简洁:

L=1N(Y^−Y)⊤(Y^−Y)L = \frac{1}{N}(\hat{Y} - Y)^\top(\hat{Y} - Y)L=N1(Y^−Y)⊤(Y^−Y)

为什么偏偏选 MSE?

  • 误差越大,惩罚越狠(平方项的作用)
  • 导数好算,数学上很"乖"
  • 回归任务的标配,大家都用它

3.3 反向传播(Backward Pass)

好戏来了! 我们需要知道损失函数对参数 WWW 和 bbb 分别有多"敏感",也就是求这两个梯度:

∂L∂W和∂L∂b\frac{\partial L}{\partial W} \quad \text{和} \quad \frac{\partial L}{\partial b}∂W∂L和∂b∂L

第一步:引入中间变量

先把误差向量定义出来:

E=Y^−Y∈RN×1E = \hat{Y} - Y \in \mathbb{R}^{N \times 1}E=Y^−Y∈RN×1

有了它,损失函数可以重新写成:

L=1N∑i=1NEi2=1NE⊤EL = \frac{1}{N}\sum_{i=1}^{N}E_i^2 = \frac{1}{N}E^\top EL=N1i=1∑NEi2=N1E⊤E

第二步:损失对预测值的梯度

对 Y^\hat{Y}Y^ 求导:

∂L∂Y^=∂∂Y^[1N(Y^−Y)⊤(Y^−Y)]\frac{\partial L}{\partial \hat{Y}} = \frac{\partial}{\partial \hat{Y}}\left[\frac{1}{N}(\hat{Y} - Y)^\top(\hat{Y} - Y)\right]∂Y^∂L=∂Y^∂[N1(Y^−Y)⊤(Y^−Y)]

展开后得到:

∂L∂Y^=2N(Y^−Y)=2NE\frac{\partial L}{\partial \hat{Y}} = \frac{2}{N}(\hat{Y} - Y) = \frac{2}{N}E∂Y^∂L=N2(Y^−Y)=N2E

维度检查 :∂L∂Y^∈RN×1\frac{\partial L}{\partial \hat{Y}} \in \mathbb{R}^{N \times 1}∂Y^∂L∈RN×1,跟 Y^\hat{Y}Y^ 一样,没毛病。

第三步:预测值对参数的梯度

回到前向传播的公式 Y^=XW+b\hat{Y} = XW + bY^=XW+b:

对 WWW 求导

∂Y^∂W=∂∂W(XW+b)=X\frac{\partial \hat{Y}}{\partial W} = \frac{\partial}{\partial W}(XW + b) = X∂W∂Y^=∂W∂(XW+b)=X

对 bbb 求导

∂Y^∂b=∂∂b(XW+b)=1N×1\frac{\partial \hat{Y}}{\partial b} = \frac{\partial}{\partial b}(XW + b) = \mathbf{1}_{N \times 1}∂b∂Y^=∂b∂(XW+b)=1N×1

这里的 1N×1\mathbf{1}_{N \times 1}1N×1 是个全 1 向量,因为偏置 bbb 是通过广播加到每个样本上的,每个样本都分到了同样的 bbb。

第四步:链式法则,算出最终梯度

权重梯度

∂L∂W=∂L∂Y^⋅∂Y^∂W=(2NE)⊤X=2NE⊤X\frac{\partial L}{\partial W} = \frac{\partial L}{\partial \hat{Y}} \cdot \frac{\partial \hat{Y}}{\partial W} = \left(\frac{2}{N}E\right)^\top X = \frac{2}{N}E^\top X∂W∂L=∂Y^∂L⋅∂W∂Y^=(N2E)⊤X=N2E⊤X

维度对一对

  • E⊤∈R1×NE^\top \in \mathbb{R}^{1 \times N}E⊤∈R1×N
  • X∈RN×1X \in \mathbb{R}^{N \times 1}X∈RN×1
  • E⊤X∈R1×1E^\top X \in \mathbb{R}^{1 \times 1}E⊤X∈R1×1,跟 WWW 的维度一致

写成分量的形式,看得更清楚:

∂L∂W=2N∑i=1N(y^i−yi)⋅xi\frac{\partial L}{\partial W} = \frac{2}{N}\sum_{i=1}^{N}(\hat{y}_i - y_i) \cdot x_i∂W∂L=N2i=1∑N(y^i−yi)⋅xi

偏置梯度

∂L∂b=∂L∂Y^⋅∂Y^∂b=(2NE)⊤1=2N∑i=1NEi\frac{\partial L}{\partial b} = \frac{\partial L}{\partial \hat{Y}} \cdot \frac{\partial \hat{Y}}{\partial b} = \left(\frac{2}{N}E\right)^\top \mathbf{1} = \frac{2}{N}\sum_{i=1}^{N}E_i∂b∂L=∂Y^∂L⋅∂b∂Y^=(N2E)⊤1=N2i=1∑NEi

维度检查 :∂L∂b∈R1×1\frac{\partial L}{\partial b} \in \mathbb{R}^{1 \times 1}∂b∂L∈R1×1,跟 bbb 一致。

分量形式:

∂L∂b=2N∑i=1N(y^i−yi)\frac{\partial L}{\partial b} = \frac{2}{N}\sum_{i=1}^{N}(\hat{y}_i - y_i)∂b∂L=N2i=1∑N(y^i−yi)

3.4 参数更新(梯度下降)

梯度算出来了,接下来用学习率 η\etaη(learning rate)控制步长,沿着梯度的反方向更新参数:

Wnew=Wold−η⋅∂L∂WW_{\text{new}} = W_{\text{old}} - \eta \cdot \frac{\partial L}{\partial W}Wnew=Wold−η⋅∂W∂L

bnew=bold−η⋅∂L∂bb_{\text{new}} = b_{\text{old}} - \eta \cdot \frac{\partial L}{\partial b}bnew=bold−η⋅∂b∂L

3.5 训练流程一览

把上面的步骤串起来,一个 Epoch 的训练流程长这样:

复制代码
+---------------------------------------------------------+
|                    训练一个 Epoch                         |
+---------------------------------------------------------+
|                                                         |
|  1. 前向传播:    Y_hat = XW + b                         |
|                 维度:(N,1) = (N,1)×(1,1) + (1,1)       |
|                                                         |
|  2. 计算误差:    E = Y_hat - Y                          |
|                 维度:(N,1) = (N,1) - (N,1)             |
|                                                         |
|  3. 计算损失:    L = (1/N)×E^T×E                        |
|                 维度:标量 = (1/N)×(1,N)×(N,1)          |
|                                                         |
|  4. 反向传播:    dL/dW = (2/N)×E^T×X                    |
|                 dL/db = (2/N)×Sum(E)                    |
|                                                         |
|  5. 参数更新:    W ← W - η×(dL/dW)                      |
|                 b ← b - η×(dL/db)                        |
|                                                         |
+---------------------------------------------------------+

四、代码实现:把数学公式翻译成 NumPy

4.1 模型定义

推导完了,接下来动手。我们先把前面一步步推出来的公式,原封不动地用 NumPy 翻译成一个完整的类。代码里我会逐行标注维度,让数据的来龙去脉一目了然。

python 复制代码
class LinearMLP:
    """
    单层线性 MLP(多层感知机)- 纯 NumPy 实现
    ========================================
    
    模型结构:
    ---------
    前向传播:Ŷ = XW + b
    
    维度说明:
    ---------
    X       : (N, 1)     - 输入特征矩阵,N 个样本,每个样本 1 个特征
    W       : (1, 1)     - 权重矩阵
    b       : (1, 1)     - 偏置项
    Ŷ (Y_pred) : (N, 1)  - 预测输出
    Y       : (N, 1)     - 真实标签
    E       : (N, 1)     - 误差向量 E = Ŷ - Y
    
    梯度维度:
    ---------
    ∂L/∂W   : (1, 1)     - 权重梯度 = (2/N) × Eᵀ × X
    ∂L/∂b   : (1, 1)     - 偏置梯度 = (2/N) × ΣE
    
    """
    
    def __init__(self, seed=42):
        """
        初始化参数
        
        初始化策略:
        - 权重 W:给个小随机数,避免一开始梯度过大
        - 偏置 b:从 0 开始就行
        """
        np.random.seed(seed)
        
        # 权重初始化:(1, 1) 矩阵,从标准正态分布采样后乘以 0.1
        # 维度:(1, 1)
        self.W = np.random.randn(1, 1).astype(np.float32) * 0.1
        
        # 偏置初始化:(1, 1) 零矩阵
        # 维度:(1, 1)
        self.b = np.zeros((1, 1), dtype=np.float32)
        
        # 保存训练历史,方便后面画图
        self.loss_history = []
        self.W_history = []
        self.b_history = []
        self.grad_W_history = []
        self.grad_b_history = []
        
        print("参数初始化完成:")
        print(f"   W = {self.W[0,0]:.4f}  (目标值:4.0)")
        print(f"   b = {self.b[0,0]:.4f}  (目标值:0.8)")
        print(f"   W 维度:{self.W.shape}")
        print(f"   b 维度:{self.b.shape}")
    
    def forward(self, X):
        """
        前向传播(Forward Pass)
        ========================
        
        输入:
        -----
        X : (N, 1) - 输入特征矩阵
        
        输出:
        -----
        Y_pred : (N, 1) - 预测输出
        
        公式:Ŷ = XW + b
        
        维度验证:
        ---------
        X     : (N, 1)
        W     : (1, 1)
        X @ W : (N, 1)  ← 矩阵乘法 (N×1) × (1×1) = (N×1)
        b     : (1, 1)
        XW+b  : (N, 1)  ← 广播机制:(N×1) + (1×1) = (N×1)
        """
        # 保存输入 X,后面反向传播要用
        self.X = X  # shape: (N, 1)
        
        # 线性变换:Ŷ = XW + b
        # np.dot(X, W): (N,1) @ (1,1) → (N,1)
        # + b: 广播机制 (N,1) + (1,1) → (N,1)
        self.Y_pred = np.dot(X, self.W) + self.b  # shape: (N, 1)
        
        return self.Y_pred
    
    def compute_loss(self, Y_true, Y_pred):
        """
        计算均方误差损失(MSE Loss)
        ============================
        
        输入:
        -----
        Y_true  : (N, 1) - 真实标签
        Y_pred  : (N, 1) - 预测输出
        
        输出:
        -----
        loss : 标量 - MSE 损失值
        
        公式:L = (1/N) × Σ(Ŷ - Y)² = (1/N) × EᵀE
        """
        # 计算误差向量:(N,1) - (N,1) → (N,1)
        self.E = Y_pred - Y_true  # shape: (N, 1)
        
        # 计算 MSE:(1/N) × ΣE²
        # E**2: (N,1) 逐元素平方 → (N,1)
        # np.mean: (N,1) → 标量
        N = Y_true.shape[0]
        loss = np.mean(self.E ** 2)  # 标量
        
        return loss
    
    def backward(self, Y_true, Y_pred):
        """
        反向传播计算梯度(Backward Pass)
        ==================================
        
        输入:
        -----
        Y_true  : (N, 1) - 真实标签
        Y_pred  : (N, 1) - 预测输出
        
        梯度计算(根据前面的数学推导):
        -------------------------
        ∂L/∂W = (2/N) × Eᵀ × X      维度:(1,1) = (1,N) × (N,1)
        ∂L/∂b = (2/N) × ΣE          维度:(1,1) = 标量求和
        
        维度详细分析:
        -------------
        E = Ŷ - Y             : (N, 1)  - 误差向量
        Eᵀ                    : (1, N)  - 转置
        X                     : (N, 1)  - 输入
        Eᵀ × X                : (1, 1)  - 矩阵乘法
        (2/N) × EᵀX           : (1, 1)  - 权重梯度
        
        ΣE (对 N 个样本求和)  : 标量
        (2/N) × ΣE            : 标量   - 偏置梯度
        """
        N = Y_true.shape[0]
        
        # 误差向量已经在 compute_loss 中算好了:E = Ŷ - Y, shape: (N, 1)
        # self.E: (N, 1)
        
        # ========== 计算权重梯度 ∂L/∂W ==========
        # 公式:(2/N) × Eᵀ × X
        # E.T: (N,1) → (1,N)
        # X: (N,1)
        # np.dot(E.T, X): (1,N) × (N,1) → (1,1)
        grad_W = (2.0 / N) * np.dot(self.E.T, self.X)  # shape: (1, 1)
        
        # ========== 计算偏置梯度 ∂L/∂b ==========
        # 公式:(2/N) × ΣE
        # np.sum(self.E): (N,1) → 标量
        # np.sum 返回的是标量,需要包装成 (1,1) 保持维度一致
        grad_b = (2.0 / N) * np.sum(self.E)  # 标量
        grad_b = np.array([[grad_b]], dtype=np.float32)  # 转换为 (1, 1)
        
        # 保存梯度
        self.grad_W = grad_W  # shape: (1, 1)
        self.grad_b = grad_b  # shape: (1, 1)
        
        return grad_W, grad_b
    
    def update_parameters(self, learning_rate=0.01):
        """
        参数更新(梯度下降)
        ===================
        
        输入:
        -----
        learning_rate : 标量 - 学习率 η
        
        更新公式:
        --------
        W_new = W_old - η × (∂L/∂W)
        b_new = b_old - η × (∂L/∂b)
        
        维度验证:
        --------
        W       : (1, 1)
        η       : 标量
        ∂L/∂W   : (1, 1)
        η×∂L/∂W : (1, 1)
        W - η×∂L/∂W : (1, 1)
        """
        # 权重更新:(1,1) - 标量 × (1,1) → (1,1)
        self.W = self.W - learning_rate * self.grad_W
        
        # 偏置更新:(1,1) - 标量 × (1,1) → (1,1)
        self.b = self.b - learning_rate * self.grad_b
    
    def train_epoch(self, X, Y, learning_rate=0.01):
        """
        训练一个完整的 Epoch
        ====================
        
        流程:
        -----
        1. 前向传播:Ŷ = XW + b
        2. 计算损失:L = (1/N) × Σ(Ŷ - Y)²
        3. 反向传播:计算 ∂L/∂W 和 ∂L/∂b
        4. 参数更新:W ← W - η×∂L/∂W, b ← b - η×∂L/∂b
        
        输出:
        -----
        loss : 标量 - 当前 Epoch 的损失值
        """
        # 第 1 步:前向传播
        Y_pred = self.forward(X)  # shape: (N, 1)
        
        # 第 2 步:计算损失
        loss = self.compute_loss(Y, Y_pred)  # 标量
        
        # 第 3 步:反向传播
        grad_W, grad_b = self.backward(Y, Y_pred)
        # grad_W: (1, 1), grad_b: (1, 1)
        
        # 第 4 步:参数更新
        self.update_parameters(learning_rate)
        
        return loss

# 实例化模型
model = LinearMLP(seed=42)

# 设置训练超参数
epochs = 200          # 训练轮数
learning_rate = 0.01  # 学习率
复制代码
参数初始化完成:
   W = 0.0497  (目标值:4.0)
   b = 0.0000  (目标值:0.8)
   W 维度:(1, 1)
   b 维度:(1, 1)

4.2 超参数设置与模型实例化

万事俱备,接下来设置训练所需的超参数并实例化模型。

参数 说明
Epochs 200 完整跑 200 轮
Learning Rate 0.01 学习率,步子不能迈太大,也不能太小
Batch Size 100 全批量梯度下降,一次用全部数据

调参小经验:学习率是最敏感的超参数。设大了,训练容易来回震荡;设小了,收敛又慢得让人着急。好在咱们这个问题足够简单,0.01 是个稳妥的选择。

python 复制代码
print("=" * 70)
print("开始训练!")
print("=" * 70)

# ============================================================
# 训练循环
# ============================================================
for epoch in range(epochs):
    # 训练一个epoch
    loss = model.train_epoch(X, Y, learning_rate)
    
    # 记录历史
    model.loss_history.append(loss)
    model.W_history.append(model.W[0, 0])
    model.b_history.append(model.b[0, 0])
    model.grad_W_history.append(model.grad_W[0, 0])
    model.grad_b_history.append(model.grad_b[0, 0])
    
    # 打印关键epoch的信息
    if (epoch + 1) % 20 == 0 or epoch == 0:
        print(f"Epoch {epoch+1:3d}/{epochs} | "
              f"Loss: {loss:.6f} | "
              f"W: {model.W[0,0]:.4f} (目标: 4.0) | "
              f"b: {model.b[0,0]:.4f} (目标: 0.8) | "
              f"|梯度|: {abs(model.grad_W[0,0]):.4f}")

print("=" * 70)
print("训练完成!")
print(f"最终参数: W = {model.W[0,0]:.6f}, b = {model.b[0,0]:.6f}")
print(f"最终损失: {model.loss_history[-1]:.8f}")
print("=" * 70)
复制代码
======================================================================
开始训练!
======================================================================
Epoch   1/200 | Loss: 21.676088 | W: 0.1587 (目标: 4.0) | b: 0.0066 (目标: 0.8) | |梯度|: 10.9004
Epoch  20/200 | Loss: 7.598458 | W: 1.7346 (目标: 4.0) | b: 0.1463 (目标: 0.8) | |梯度|: 6.3410
Epoch  40/200 | Loss: 2.612014 | W: 2.6887 (目标: 4.0) | b: 0.2947 (目标: 0.8) | |梯度|: 3.5969
Epoch  60/200 | Loss: 0.955393 | W: 3.2308 (目标: 4.0) | b: 0.4225 (目标: 0.8) | |梯度|: 2.0483
Epoch  80/200 | Loss: 0.390340 | W: 3.5403 (目标: 4.0) | b: 0.5242 (目标: 0.8) | |梯度|: 1.1718
Epoch 100/200 | Loss: 0.191368 | W: 3.7177 (目标: 4.0) | b: 0.6014 (目标: 0.8) | |梯度|: 0.6739
Epoch 120/200 | Loss: 0.118721 | W: 3.8200 (目标: 4.0) | b: 0.6584 (目标: 0.8) | |梯度|: 0.3898
Epoch 140/200 | Loss: 0.091160 | W: 3.8794 (目标: 4.0) | b: 0.6995 (目标: 0.8) | |梯度|: 0.2270
Epoch 160/200 | Loss: 0.080304 | W: 3.9141 (目标: 4.0) | b: 0.7288 (目标: 0.8) | |梯度|: 0.1332
Epoch 180/200 | Loss: 0.075879 | W: 3.9345 (目标: 4.0) | b: 0.7494 (目标: 0.8) | |梯度|: 0.0788
Epoch 200/200 | Loss: 0.074021 | W: 3.9467 (目标: 4.0) | b: 0.7638 (目标: 0.8) | |梯度|: 0.0470
======================================================================
训练完成!
最终参数: W = 3.946665, b = 0.763773
最终损失: 0.07402119
======================================================================

4.3 训练过程可视化

数字会说话,但图表更直观。咱们用四张图来见证模型从"一无所知"到"精准命中"的全过程。

python 复制代码
# 创建2×2子图布局
fig, axes = plt.subplots(2, 2, figsize=(15, 11))
fig.suptitle('训练过程可视化', fontsize=18, fontweight='bold', y=0.98)

# ========== 图1: 损失曲线 ==========
axes[0, 0].plot(model.loss_history, linewidth=2.5, color='steelblue')
axes[0, 0].set_title('训练损失曲线 (MSE)', fontsize=14, fontweight='bold', pad=12)
axes[0, 0].set_xlabel('Epoch', fontsize=12)
axes[0, 0].set_ylabel('Loss', fontsize=12)
axes[0, 0].set_yscale('log')  # 对数坐标更好展示收敛趋势
axes[0, 0].grid(True, alpha=0.3)
axes[0, 0].text(0.02, 0.95, f'最终Loss: {model.loss_history[-1]:.6f}', 
                transform=axes[0, 0].transAxes, fontsize=10,
                verticalalignment='top',
                bbox=dict(boxstyle='round', facecolor='lightblue', alpha=0.7))

# ========== 图2: 权重W的收敛过程 ==========
axes[0, 1].plot(model.W_history, linewidth=2.5, color='forestgreen', label='W 的学习轨迹')
axes[0, 1].axhline(y=true_w, color='crimson', linestyle='--', linewidth=2.5, 
                   label=f'真实值 ({true_w})', alpha=0.8)
axes[0, 1].set_title('权重 W 的收敛过程', fontsize=14, fontweight='bold', pad=12)
axes[0, 1].set_xlabel('Epoch', fontsize=12)
axes[0, 1].set_ylabel('W', fontsize=12)
axes[0, 1].legend(fontsize=11, loc='lower right')
axes[0, 1].grid(True, alpha=0.3)
axes[0, 1].text(0.02, 0.95, f'最终W: {model.W[0,0]:.6f}\n误差: {abs(model.W[0,0]-true_w):.6f}', 
                transform=axes[0, 1].transAxes, fontsize=10,
                verticalalignment='top',
                bbox=dict(boxstyle='round', facecolor='lightgreen', alpha=0.7))

# ========== 图3: 偏置b的收敛过程 ==========
axes[1, 0].plot(model.b_history, linewidth=2.5, color='darkorange', label='b 的学习轨迹')
axes[1, 0].axhline(y=true_b, color='crimson', linestyle='--', linewidth=2.5, 
                   label=f'真实值 ({true_b})', alpha=0.8)
axes[1, 0].set_title('偏置 b 的收敛过程', fontsize=14, fontweight='bold', pad=12)
axes[1, 0].set_xlabel('Epoch', fontsize=12)
axes[1, 0].set_ylabel('b', fontsize=12)
axes[1, 0].legend(fontsize=11, loc='lower right')
axes[1, 0].grid(True, alpha=0.3)
axes[1, 0].text(0.02, 0.95, f'最终b: {model.b[0,0]:.6f}\n误差: {abs(model.b[0,0]-true_b):.6f}', 
                transform=axes[1, 0].transAxes, fontsize=10,
                verticalalignment='top',
                bbox=dict(boxstyle='round', facecolor='moccasin', alpha=0.7))

# ========== 图4: 梯度绝对值变化 ==========
axes[1, 1].plot(np.abs(model.grad_W_history), linewidth=2.5, color='purple', label='|∂L/∂W|')
axes[1, 1].plot(np.abs(model.grad_b_history), linewidth=2.5, color='brown', label='|∂L/∂b|')
axes[1, 1].set_title('梯度绝对值变化', fontsize=14, fontweight='bold', pad=12)
axes[1, 1].set_xlabel('Epoch', fontsize=12)
axes[1, 1].set_ylabel('|梯度|', fontsize=12)
axes[1, 1].set_yscale('log')
axes[1, 1].legend(fontsize=11, loc='upper right')
axes[1, 1].grid(True, alpha=0.3)
axes[1, 1].text(0.02, 0.95, '梯度趋近于0\n表示收敛', 
                transform=axes[1, 1].transAxes, fontsize=10,
                verticalalignment='top',
                bbox=dict(boxstyle='round', facecolor='lavender', alpha=0.7))

plt.tight_layout(rect=[0, 0, 1, 0.96])
plt.show()

4.4 训练前 vs 训练后:直观对比拟合效果

来看看模型从"啥也不会"到"精准拟合"的蜕变。

python 复制代码
# 打印最终结果对比
print("\n" + "="*65)
print("最终结果对比")
print("="*65)
print(f"{'参数':<10} | {'真实值':<12} | {'学习值':<12} | {'绝对误差':<12}")
print("-" * 65)
print(f"{'W':<10} | {true_w:<12.4f} | {model.W[0,0]:<12.6f} | {abs(model.W[0,0] - true_w):<12.6f}")
print(f"{'b':<10} | {true_b:<12.4f} | {model.b[0,0]:<12.6f} | {abs(model.b[0,0] - true_b):<12.6f}")
print("="*65)
print(f"\n模型已成功拟合目标函数!")
print(f"   学习到的函数: y = {model.W[0,0]:.4f}x + {model.b[0,0]:.4f}")
print(f"   目标函数:     y = {true_w}x + {true_b}")
复制代码
=================================================================
最终结果对比
=================================================================
参数         | 真实值          | 学习值          | 绝对误差        
-----------------------------------------------------------------
W          | 4.0000       | 3.946665     | 0.053335    
b          | 0.8000       | 0.763773     | 0.036227    
=================================================================

模型已成功拟合目标函数!
   学习到的函数: y = 3.9467x + 0.7638
   目标函数:     y = 4.0x + 0.8

五、换个角度看问题:PyTorch 的优雅实现

前面我们用纯 NumPy 手撸了一遍完整的线性回归,从数据生成到反向传播,每一步都掰开揉碎地讲解。现在你可能会问:如果用深度学习框架来实现,能省多少事?

答案是:非常多。

PyTorch 这样的现代框架帮我们封装了自动微分、梯度计算、参数更新等底层细节,让我们能更专注于模型设计本身。但别担心------有了前面 NumPy 版本的基础,你现在看 PyTorch 代码时会有一种"原来如此"的通透感,而不是只会调 API 的黑盒玩家。

5.1 PyTorch 实现的优势

对比维度 纯 NumPy 手写 PyTorch 实现
梯度计算 手动推导链式法则 自动微分,一键搞定
参数更新 手动写 W = W - lr * grad 优化器封装,支持多种算法
矩阵运算 np.dot(), 手动控制维度 torch.Tensor,GPU 加速开箱即用
代码可读性 细节繁琐,但适合学习 简洁优雅,生产环境首选

重要提醒 :别因为框架方便就跳过底层原理。能手写 NumPy 版本的人,用框架是降维打击;只会调 model.fit() 的人,出了 bug 连排查方向都找不到。

5.2 实现思路

我们的目标是用 PyTorch 复现完全相同的功能:

  1. 数据准备 :同样的目标函数 y=4x+0.8y = 4x + 0.8y=4x+0.8,同样的噪声分布
  2. 模型构建 :一个不带激活函数的线性层(等价于 wx+bwx + bwx+b)
  3. 损失函数:均方误差 MSE
  4. 优化器:SGD(随机梯度下降),学习率 0.01
  5. 训练循环:前向传播 → 计算损失 → 反向传播 → 参数更新

接下来我们一步步来实现。

python 复制代码
# 导入 PyTorch 相关模块
import torch
import torch.nn as nn
import torch.optim as optim

print(f"PyTorch 版本: {torch.__version__}")

# ============================================================
# 第一步:准备数据(与 NumPy 版本保持一致)
# ============================================================
# 注意:PyTorch 使用 torch.Tensor,而不是 numpy.ndarray
# 维度说明:
#   X_tensor : (N, 1) - 输入特征,N 个样本,每个样本 1 个特征
#   Y_tensor : (N, 1) - 目标标签,维度与输入一致

N = 100              # 样本数量
true_w = 4.0         # 真实权重
true_b = 0.8         # 真实偏置
noise_std = 0.3      # 噪声标准差

# 设置随机种子保证可复现性
torch.manual_seed(42)

# 生成输入数据 X: 形状 (N, 1)
X_tensor = torch.rand(N, 1) * 4 - 2  # 均匀分布 [-2, 2],shape: (N, 1)

# 生成目标数据 Y = 4X + 0.8 + 噪声: 形状 (N, 1)
noise = torch.randn(N, 1) * noise_std  # 高斯噪声,shape: (N, 1)
Y_tensor = true_w * X_tensor + true_b + noise

print(f"✅ 数据准备完成!")
print(f"   X_tensor 形状: {X_tensor.shape},范围: [{X_tensor.min():.2f}, {X_tensor.max():.2f}]")
print(f"   Y_tensor 形状: {Y_tensor.shape},范围: [{Y_tensor.min():.2f}, {Y_tensor.max():.2f}]")
复制代码
PyTorch 版本: 2.11.0
✅ 数据准备完成!
   X_tensor 形状: torch.Size([100, 1]),范围: [-1.98, 1.92]
   Y_tensor 形状: torch.Size([100, 1]),范围: [-7.34, 8.25]
python 复制代码
# ============================================================
# 第二步:定义模型
# ============================================================
# 在 PyTorch 中,我们使用 nn.Linear 来定义线性层
# nn.Linear(in_features, out_features) 会自动创建权重和偏置
#
# 维度说明:
#   in_features=1  : 输入维度为 1(单个特征 x)
#   out_features=1 : 输出维度为 1(预测值 ŷ)
#
# 模型内部参数:
#   weight (W): (1, 1) - 权重矩阵
#   bias (b):   (1,)   - 偏置向量
#
# 前向传播公式:Ŷ = X @ W^T + b
#   X: (N, 1) @ (1, 1)^T + (1,) → (N, 1) + (1,) → (N, 1)

model_torch = nn.Linear(in_features=1, out_features=1)

# 查看初始化后的参数
print("📦 模型结构:")
print(f"   输入维度: 1")
print(f"   输出维度: 1")
print(f"\n🔍 参数详情:")
print(f"   权重 W (weight): {model_torch.weight.shape}")
print(f"      当前值: W = {model_torch.weight.item():.4f}")
print(f"   偏置 b (bias):   {model_torch.bias.shape}")
print(f"      当前值: b = {model_torch.bias.item():.4f}")
复制代码
📦 模型结构:
   输入维度: 1
   输出维度: 1

🔍 参数详情:
   权重 W (weight): torch.Size([1, 1])
      当前值: W = 0.1447
   偏置 b (bias):   torch.Size([1])
      当前值: b = -0.2590
python 复制代码
# ============================================================
# 第三步:定义损失函数和优化器
# ============================================================

# 损失函数:均方误差 (MSE)
# 等价于 NumPy 版本中的 L = (1/N) * Σ(Ŷ - Y)²
criterion = nn.MSELoss()

# 优化器:SGD(随机梯度下降)
# 学习率 0.01,与 NumPy 版本保持一致
# 优化器会自动管理 model_torch 的参数,并在 step() 时更新
learning_rate = 0.01
optimizer = optim.SGD(model_torch.parameters(), lr=learning_rate)

print("⚙️ 训练配置:")
print(f"   损失函数: MSELoss")
print(f"   优化器: SGD")
print(f"   学习率: {learning_rate}")
复制代码
⚙️ 训练配置:
   损失函数: MSELoss
   优化器: SGD
   学习率: 0.01

5.3 训练循环

PyTorch 的训练循环有一个关键步骤:梯度清零。这是因为 PyTorch 默认会累积梯度,如果不清零,每次迭代的梯度会叠加。

训练流程与 NumPy 版本对比:

步骤 NumPy 版本 PyTorch 版本
1. 梯度清零 不需要(手动计算) optimizer.zero_grad()
2. 前向传播 Y_pred = X @ W + b Y_pred = model(X)
3. 计算损失 loss = mean((Y_pred - Y)²) loss = criterion(Y_pred, Y)
4. 反向传播 手动推导 dL/dW, dL/db loss.backward()自动微分
5. 参数更新 W = W - lr * grad_W optimizer.step()自动更新

自动微分的魔力loss.backward() 这行代码背后,PyTorch 会利用计算图自动追踪所有运算,然后通过链式法则计算出每个参数的梯度。这正是我们在 NumPy 版本中手动推导的第三步和第四步!

python 复制代码
# ============================================================
# 第四步:训练循环
# ============================================================
epochs = 200

# 记录训练历史,方便后续可视化
loss_history_torch = []
W_history_torch = []
b_history_torch = []

print("=" * 70)
print("开始训练(PyTorch 版本)!")
print("=" * 70)

for epoch in range(epochs):
    # ---- 第 1 步:梯度清零 ----
    # PyTorch 默认会累积梯度,所以每次迭代前必须清零
    # 等价于 NumPy 版本中我们每次重新计算梯度
    optimizer.zero_grad()
    
    # ---- 第 2 步:前向传播 ----
    # 模型内部执行:Y_pred = X @ W^T + b
    #   X: (N, 1), W: (1, 1), b: (1,) → Y_pred: (N, 1)
    Y_pred = model_torch(X_tensor)
    
    # ---- 第 3 步:计算损失 ----
    # 计算 MSE: loss = mean((Y_pred - Y)²)
    #   Y_pred: (N, 1), Y: (N, 1) → loss: 标量
    loss = criterion(Y_pred, Y_tensor)
    
    # ---- 第 4 步:反向传播 ----
    # 自动微分!PyTorch 通过计算图自动计算梯度
    # 等价于 NumPy 版本中手动推导的:
    #   dL/dW = (2/N) * E^T * X
    #   dL/db = (2/N) * sum(E)
    # 梯度会存储在 model_torch.weight.grad 和 model_torch.bias.grad 中
    loss.backward()
    
    # ---- 第 5 步:参数更新 ----
    # 优化器自动执行:W = W - lr * dL/dW, b = b - lr * dL/db
    optimizer.step()
    
    # 记录历史
    loss_history_torch.append(loss.item())
    W_history_torch.append(model_torch.weight.item())
    b_history_torch.append(model_torch.bias.item())
    
    # 打印关键 epoch 的信息
    if (epoch + 1) % 20 == 0 or epoch == 0:
        print(f"Epoch {epoch+1:3d}/{epochs} | "
              f"Loss: {loss.item():.6f} | "
              f"W: {model_torch.weight.item():.4f} (目标: 4.0) | "
              f"b: {model_torch.bias.item():.4f} (目标: 0.8) | "
              f"|梯度|: {abs(model_torch.weight.grad.item()):.4f}")

print("=" * 70)
print("训练完成!")
print(f"最终参数: W = {model_torch.weight.item():.6f}, b = {model_torch.bias.item():.6f}")
print(f"最终损失: {loss_history_torch[-1]:.8f}")
print("=" * 70)
复制代码
======================================================================
开始训练(PyTorch 版本)!
======================================================================
Epoch   1/200 | Loss: 21.813667 | W: 0.2513 (目标: 4.0) | b: -0.2330 (目标: 0.8) | |梯度|: 10.6663
Epoch  20/200 | Loss: 7.542290 | W: 1.7972 (目标: 4.0) | b: 0.1541 (目标: 0.8) | |梯度|: 6.2326
Epoch  40/200 | Loss: 2.495776 | W: 2.7357 (目标: 4.0) | b: 0.4034 (目标: 0.8) | |梯度|: 3.5389
Epoch  60/200 | Loss: 0.854131 | W: 3.2684 (目标: 4.0) | b: 0.5549 (目标: 0.8) | |梯度|: 2.0084
Epoch  80/200 | Loss: 0.319364 | W: 3.5707 (目标: 4.0) | b: 0.6475 (目标: 0.8) | |梯度|: 1.1392
Epoch 100/200 | Loss: 0.144833 | W: 3.7421 (目标: 4.0) | b: 0.7045 (目标: 0.8) | |梯度|: 0.6457
Epoch 120/200 | Loss: 0.087724 | W: 3.8392 (目标: 4.0) | b: 0.7398 (目标: 0.8) | |梯度|: 0.3657
Epoch 140/200 | Loss: 0.068972 | W: 3.8942 (目标: 4.0) | b: 0.7618 (目标: 0.8) | |梯度|: 0.2069
Epoch 160/200 | Loss: 0.062785 | W: 3.9253 (目标: 4.0) | b: 0.7756 (目标: 0.8) | |梯度|: 0.1169
Epoch 180/200 | Loss: 0.060731 | W: 3.9428 (目标: 4.0) | b: 0.7843 (目标: 0.8) | |梯度|: 0.0660
Epoch 200/200 | Loss: 0.060043 | W: 3.9527 (目标: 4.0) | b: 0.7899 (目标: 0.8) | |梯度|: 0.0372
======================================================================
训练完成!
最终参数: W = 3.952723, b = 0.789894
最终损失: 0.06004309
======================================================================

5.4 可视化 PyTorch 训练过程

我们同样可以画出损失曲线和参数收敛过程,看看 PyTorch 版本的表现如何。

python 复制代码
# 可视化 PyTorch 训练过程
fig, axes = plt.subplots(1, 3, figsize=(18, 5))
fig.suptitle('PyTorch 训练过程可视化', fontsize=18, fontweight='bold', y=1.02)

# ========== 图1: 损失曲线 ==========
axes[0].plot(loss_history_torch, linewidth=2.5, color='steelblue')
axes[0].set_title('训练损失曲线 (MSE)', fontsize=14, fontweight='bold', pad=12)
axes[0].set_xlabel('Epoch', fontsize=12)
axes[0].set_ylabel('Loss', fontsize=12)
axes[0].set_yscale('log')
axes[0].grid(True, alpha=0.3)
axes[0].text(0.02, 0.95, f'最终Loss: {loss_history_torch[-1]:.6f}', 
             transform=axes[0].transAxes, fontsize=10,
             verticalalignment='top',
             bbox=dict(boxstyle='round', facecolor='lightblue', alpha=0.7))

# ========== 图2: 权重W的收敛过程 ==========
axes[1].plot(W_history_torch, linewidth=2.5, color='forestgreen', label='W 的学习轨迹')
axes[1].axhline(y=true_w, color='crimson', linestyle='--', linewidth=2.5, 
                label=f'真实值 ({true_w})', alpha=0.8)
axes[1].set_title('权重 W 的收敛过程', fontsize=14, fontweight='bold', pad=12)
axes[1].set_xlabel('Epoch', fontsize=12)
axes[1].set_ylabel('W', fontsize=12)
axes[1].legend(fontsize=11, loc='lower right')
axes[1].grid(True, alpha=0.3)
axes[1].text(0.02, 0.95, f'最终W: {model_torch.weight.item():.6f}\n误差: {abs(model_torch.weight.item()-true_w):.6f}', 
             transform=axes[1].transAxes, fontsize=10,
             verticalalignment='top',
             bbox=dict(boxstyle='round', facecolor='lightgreen', alpha=0.7))

# ========== 图3: 偏置b的收敛过程 ==========
axes[2].plot(b_history_torch, linewidth=2.5, color='darkorange', label='b 的学习轨迹')
axes[2].axhline(y=true_b, color='crimson', linestyle='--', linewidth=2.5, 
                label=f'真实值 ({true_b})', alpha=0.8)
axes[2].set_title('偏置 b 的收敛过程', fontsize=14, fontweight='bold', pad=12)
axes[2].set_xlabel('Epoch', fontsize=12)
axes[2].set_ylabel('b', fontsize=12)
axes[2].legend(fontsize=11, loc='lower right')
axes[2].grid(True, alpha=0.3)
axes[2].text(0.02, 0.95, f'最终b: {model_torch.bias.item():.6f}\n误差: {abs(model_torch.bias.item()-true_b):.6f}', 
             transform=axes[2].transAxes, fontsize=10,
             verticalalignment='top',
             bbox=dict(boxstyle='round', facecolor='moccasin', alpha=0.7))

plt.tight_layout()
plt.show()
python 复制代码
# 打印最终结果对比
print("\n" + "="*65)
print("最终结果对比(PyTorch 版本)")
print("="*65)
print(f"{'参数':<10} | {'真实值':<12} | {'学习值':<12} | {'绝对误差':<12}")
print("-" * 65)
print(f"{'W':<10} | {true_w:<12.4f} | {model_torch.weight.item():<12.6f} | {abs(model_torch.weight.item() - true_w):<12.6f}")
print(f"{'b':<10} | {true_b:<12.4f} | {model_torch.bias.item():<12.6f} | {abs(model_torch.bias.item() - true_b):<12.6f}")
print("="*65)
print(f"\n模型已成功拟合目标函数!")
print(f"   学习到的函数: y = {model_torch.weight.item():.4f}x + {model_torch.bias.item():.4f}")
print(f"   目标函数:     y = {true_w}x + {true_b}")
复制代码
=================================================================
最终结果对比(PyTorch 版本)
=================================================================
参数         | 真实值          | 学习值          | 绝对误差        
-----------------------------------------------------------------
W          | 4.0000       | 3.952723     | 0.047277    
b          | 0.8000       | 0.789894     | 0.010106    
=================================================================

模型已成功拟合目标函数!
   学习到的函数: y = 3.9527x + 0.7899
   目标函数:     y = 4.0x + 0.8

5.5 两种实现的对比总结

通过 PyTorch 版本的实现,我们能明显感受到框架带来的便利:

步骤 NumPy 版本(纯手写) PyTorch 版本
模型定义 手动写类,定义 forward() 等方法 nn.Linear(1, 1) 一行搞定
梯度计算 手动推导 dL/dW = (2/N)×Eᵀ×X loss.backward() 自动完成
参数更新 手动写 W = W - lr * grad_W optimizer.step() 自动更新
梯度清零 不需要 optimizer.zero_grad()
代码行数 ~150 行 ~80 行

关键洞察:虽然 PyTorch 帮我们封装了大量细节,但理解底层原理的价值在于------

  1. 调试模型时能更快定位问题(比如梯度消失/爆炸)
  2. 设计新架构时能做出更合理的决策
  3. 阅读论文时能理解各种 trick 的本质

两种实现的学习结果非常接近:

实现方式 学习到的 W 学习到的 b 最终 Loss
NumPy 3.9467 0.7638 0.0740
PyTorch 3.9527 0.7899 0.0600

由于随机种子不同,两种实现的数据采样略有差异,导致最终结果有微小差别,但都成功收敛到了目标参数附近!


总结:从 NumPy 到 PyTorch,我们看到了同一个问题的两种解法。前者是"知其然,更知其所以然",后者是"站在巨人的肩膀上"。掌握了前者,用后者自然如鱼得水。

相关推荐
a752066281 小时前
AI 智能体 OpenClaw 飞书插件安装配置 全程命令行实操
人工智能·飞书·openclaw部署·ai 办公自动化
霸道流氓气质1 小时前
Spring AI Advisor 完全指南:拦截器机制与实战全解
java·人工智能·spring
qyr67891 小时前
全球罐底扫描仪市场前景分析与行业报告
大数据·人工智能·数据分析·市场报告·罐底扫描仪
智享食事1 小时前
一个配置,多平台使用:飞书/企微/钉钉/QQ全接入
人工智能
zhoupenghui1681 小时前
如何设置PyTorch程序在 GPU上运行
人工智能·pytorch·python·gru·cuda
泰迪智能科技012 小时前
从课堂到产业:数据挖掘平台如何破解高校实战教学难题?
人工智能·数据挖掘
Jahport2 小时前
当量子计算时代进入倒计时,智能汽车的安全体系该如何重构?
人工智能·安全·重构·架构·量子计算·物联网安全
Raink老师9 小时前
【AI面试临阵磨枪-48】GraphRAG、多模态 RAG、自适应 RAG 原理
人工智能·ai 面试题
波动几何9 小时前
模式驱动的学术选题方法论——四种AI模式处理能力的系统建构与论证
人工智能