深度学习-线性回归

参考:《动手学深度学习》,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,我们看到了同一个问题的两种解法。前者是"知其然,更知其所以然",后者是"站在巨人的肩膀上"。掌握了前者,用后者自然如鱼得水。

相关推荐
fai厅的秃头姐!20 分钟前
OpenCV实战篇
人工智能·opencv·计算机视觉
TMT星球1 小时前
虎鲸文娱春苗编剧计划全面升级,已孵化20个剧本进入前期开发
大数据·人工智能
奔跑中的小象7 小时前
统信UOS + 天数AI卡部署SGLang服务手册
人工智能·uos·sglang·天数智芯
DevSecOps选型指南8 小时前
中国版Mythos,为何是悬镜安全灵脉CodeAI?
人工智能·安全
Shockang8 小时前
LangGraph 状态机实战
人工智能
刹那芳华19928 小时前
循环神经网络的从零开始实现(RNN)
人工智能·rnn·深度学习
阿童木写作8 小时前
跨境图片翻译工具多合一,批量图片视频字幕翻译加智能抠图
人工智能·python·音视频·语音识别
科技之门8 小时前
AI3D从建模到贴图、绑骨和动画的完整流程怎么做?V2Fun完整工作流指南
人工智能·3d·贴图
宇宙第一小趴菜8 小时前
二、机器学习的应用领域和发展史
人工智能·机器学习
前端开发江鸟8 小时前
我写过 MCP Server,却一直以为 MCP 只有 Tool
人工智能