第一章:神经网络原理

第一章:神经网络原理

本章学习目标

  • 理解神经网络的基本组成单元------神经元与感知机的工作原理
  • 掌握主流激活函数的数学定义、特性及适用场景
  • 深入理解前向传播与反向传播算法的数学推导与数据流转过程
  • 掌握经典优化算法(SGD、Adam、AdamW)的核心思想与实现细节
  • 了解深度学习核心组件(损失函数、正则化、归一化)的设计哲学
  • 理解神经网络架构从MLP到Transformer的演进脉络

1.1 神经网络基础

1.1.1 神经元与感知机模型

概念引入:从生物神经元到人工神经元

人类大脑约由860亿个神经元组成,每个神经元通过突触与其他神经元相连,形成复杂的神经网络。人工神经网络(Artificial Neural Network, ANN)正是受此启发而设计。

类比理解:想象一个会议室决策场景------每个神经元是一位参会者,他们接收来自其他参会者的信息(输入信号),经过自己的判断(加权求和+激活函数),然后输出自己的意见(输出信号)。多位参会者相互连接,最终形成集体决策。

感知机模型(Perceptron)

感知机是神经网络的最基本单元,由Frank Rosenblatt于1957年提出,本质上是一个二分类线性分类器

数学定义

给定一个输入向量 x=x1,x2,...,xnT\mathbf{x} = x_1, x_2, \\dots, x_n^Tx=x1,x2,...,xnT,感知机的输出为:

y=f(wTx+b)=f(∑i=1nwixi+b) y = f(\mathbf{w}^T \mathbf{x} + b) = f\left(\sum_{i=1}^{n} w_i x_i + b\right) y=f(wTx+b)=f(i=1∑nwixi+b)

其中:

  • w=w1,w2,...,wnT\mathbf{w} = w_1, w_2, \\dots, w_n^Tw=w1,w2,...,wnT 为权重向量,表示每个输入特征的重要程度
  • bbb 为偏置项(bias),用于调整决策边界的偏移
  • f(⋅)f(\cdot)f(⋅) 为阶跃函数(早期感知机使用):

f(z)={1,z≥00,z<0 f(z) = \begin{cases} 1, & z \geq 0 \\ 0, & z < 0 \end{cases} f(z)={1,0,z≥0z<0

数据流转过程

复制代码
输入层 → 加权求和 → 加偏置 → 激活函数 → 输出
[x1, x2, ..., xn] → [w1*x1+...+wn*xn] → [+b] → [f(·)] → y
现代神经元模型

现代神经网络中的神经元在感知机基础上进行了扩展,主要区别在于使用可微的激活函数(如Sigmoid、ReLU等),使得可以通过梯度下降进行训练。

python 复制代码
import torch
import torch.nn as nn

# ===== 代码实战1.1:实现一个简单的神经元 =====
class Neuron(nn.Module):
    """
    单个神经元实现
    对应公式: y = activation(w^T * x + b)
    """
    def __init__(self, input_dim, activation='relu'):
        """
        初始化神经元
        
        Args:
            input_dim: 输入特征维度
            activation: 激活函数类型,可选 'relu', 'sigmoid', 'tanh'
        """
        super(Neuron, self).__init__()
        # 权重参数,使用正态分布初始化
        self.weights = nn.Parameter(torch.randn(input_dim, 1) * 0.01)
        # 偏置参数,初始化为0
        self.bias = nn.Parameter(torch.zeros(1))
        # 激活函数
        if activation == 'relu':
            self.activation = nn.ReLU()
        elif activation == 'sigmoid':
            self.activation = nn.Sigmoid()
        elif activation == 'tanh':
            self.activation = nn.Tanh()
        else:
            raise ValueError(f"不支持的激活函数: {activation}")
    
    def forward(self, x):
        """
        前向传播
        
        Args:
            x: 输入张量,形状为 (batch_size, input_dim)
        
        Returns:
            神经元的输出,形状为 (batch_size, 1)
        """
        # 加权求和: w^T * x + b
        # x @ self.weights: (batch_size, input_dim) @ (input_dim, 1) -> (batch_size, 1)
        linear_output = torch.matmul(x, self.weights) + self.bias
        # 激活函数
        output = self.activation(linear_output)
        return output


# 测试代码
if __name__ == "__main__":
    # 创建一个输入维度为3的ReLU神经元
    neuron = Neuron(input_dim=3, activation='relu')
    
    # 构造一批输入数据 (batch_size=2, input_dim=3)
    x = torch.tensor([[1.0, 2.0, 3.0], 
                      [0.5, -1.0, 1.5]])
    
    # 前向传播
    output = neuron(x)
    print(f"输入形状: {x.shape}")
    print(f"输出形状: {output.shape}")
    print(f"输出值:\n{output.detach().numpy()}")

输出解读

复制代码
输入形状: torch.Size([2, 3])
输出形状: torch.Size([2, 1])
输出值:
[[0.234]
 [0.   ]]  # 负值被ReLU置为0
多层感知机(MLP)

将多个神经元分层排列,就构成了多层感知机 (Multi-Layer Perceptron, MLP),也称为全连接神经网络(Fully Connected Neural Network)。

复制代码
输入层 (Input Layer)
    ↓
隐藏层1 (Hidden Layer 1) ------ 每个神经元与上一层所有神经元相连
    ↓
隐藏层2 (Hidden Layer 2)
    ↓
输出层 (Output Layer)

关键洞察 :单层感知机只能解决线性可分 问题(如AND、OR逻辑),而多层感知机通过引入隐藏层和非线性激活函数,理论上可以逼近任意连续函数(万能逼近定理,Universal Approximation Theorem)。


1.1.2 激活函数详解

为什么需要激活函数?

如果没有激活函数,无论神经网络有多少层,最终都等价于一个线性变换:

y=Wn(⋯(W2(W1x+b1)+b2)⋯ )+bn=Weqx+beq \mathbf{y} = W_n(\cdots(W_2(W_1\mathbf{x} + \mathbf{b}_1) + \mathbf{b}2)\cdots) + \mathbf{b}n = W{eq}\mathbf{x} + \mathbf{b}{eq} y=Wn(⋯(W2(W1x+b1)+b2)⋯)+bn=Weqx+beq

激活函数引入非线性,使得神经网络能够学习和表示复杂的非线性映射关系。

主流激活函数对比
激活函数 数学公式 输出范围 优点 缺点 适用场景
Sigmoid σ(x)=11+e−x\sigma(x) = \frac{1}{1+e^{-x}}σ(x)=1+e−x1 (0, 1) 平滑可导,可解释为概率 梯度消失,输出非零中心 二分类输出层(历史用法)
Tanh tanh⁡(x)=ex−e−xex+e−x\tanh(x) = \frac{e^x - e^{-x}}{e^x + e^{-x}}tanh(x)=ex+e−xex−e−x (-1, 1) 零中心,收敛快于Sigmoid 仍存在梯度消失 RNN隐藏层(历史用法)
ReLU ReLU(x)=max⁡(0,x)\text{ReLU}(x) = \max(0, x)ReLU(x)=max(0,x) [0,+∞)[0, +\infty)[0,+∞) 计算简单,缓解梯度消失 死亡ReLU问题 CNN隐藏层(最常用)
Leaky ReLU max⁡(αx,x),α=0.01\max(\alpha x, x), \alpha=0.01max(αx,x),α=0.01 (−∞,+∞)(-\infty, +\infty)(−∞,+∞) 解决死亡ReLU问题 负值区域梯度小 ReLU的替代方案
GELU xΦ(x)x\Phi(x)xΦ(x) 或近似公式 约(-0.17, +∞) 平滑非线性,性能优异 计算稍复杂 Transformer(GPT、BERT)
重点详解:ReLU与GELU

ReLU(Rectified Linear Unit)

python 复制代码
import matplotlib.pyplot as plt
import numpy as np

# ===== 代码实战1.2:可视化主流激活函数 =====
def plot_activation_functions():
    """绘制并对比主流激活函数的曲线"""
    x = np.linspace(-5, 5, 1000)
    
    # 定义各激活函数
    sigmoid = 1 / (1 + np.exp(-x))
    tanh = np.tanh(x)
    relu = np.maximum(0, x)
    leaky_relu = np.where(x > 0, x, 0.01 * x)
    # GELU近似公式: 0.5 * x * (1 + tanh(sqrt(2/pi) * (x + 0.044715 * x^3)))
    gelu = 0.5 * x * (1 + np.tanh(np.sqrt(2 / np.pi) * (x + 0.044715 * x**3)))
    
    # 绘制
    plt.figure(figsize=(12, 8))
    plt.plot(x, sigmoid, label='Sigmoid', linewidth=2)
    plt.plot(x, tanh, label='Tanh', linewidth=2)
    plt.plot(x, relu, label='ReLU', linewidth=2)
    plt.plot(x, leaky_relu, label='Leaky ReLU (α=0.01)', linewidth=2)
    plt.plot(x, gelu, label='GELU', linewidth=2)
    plt.axhline(y=0, color='gray', linestyle='--', alpha=0.5)
    plt.axvline(x=0, color='gray', linestyle='--', alpha=0.5)
    plt.grid(True, alpha=0.3)
    plt.legend(fontsize=12)
    plt.xlabel('输入 x', fontsize=12)
    plt.ylabel('输出 f(x)', fontsize=12)
    plt.title('主流激活函数对比', fontsize=14)
    plt.tight_layout()
    plt.savefig('activation_functions.png', dpi=150)
    plt.show()

plot_activation_functions()

GELU(Gaussian Error Linear Unit)

GELU是Transformer架构(包括BERT、GPT系列)的默认激活函数,其思想是将输入乘以一个依赖于输入值的随机正则化项。

精确公式 (计算成本高):

GELU(x)=x⋅Φ(x)=x⋅121+erf(x2) \text{GELU}(x) = x \cdot \Phi(x) = x \cdot \frac{1}{2}1 + \\text{erf}(\\frac{x}{\\sqrt{2}}) GELU(x)=x⋅Φ(x)=x⋅211+erf(2 x)

其中 Φ(x)\Phi(x)Φ(x) 是标准正态分布的累积分布函数。

常用近似公式 (实际实现使用):

GELU(x)≈0.5x(1+tanh⁡2/π(x+0.044715x3)) \text{GELU}(x) \approx 0.5x(1 + \tanh\\sqrt{2/\\pi}(x + 0.044715x\^3)) GELU(x)≈0.5x(1+tanh2/π (x+0.044715x3))

或更快的近似:

GELU(x)≈x⋅σ(1.702x) \text{GELU}(x) \approx x \cdot \sigma(1.702x) GELU(x)≈x⋅σ(1.702x)

其中 σ\sigmaσ 是Sigmoid函数。

python 复制代码
# ===== 代码实战1.3:PyTorch中激活函数的正确使用 =====
import torch
import torch.nn as nn

class MLPWithVariousActivations(nn.Module):
    """
    对比不同激活函数在MLP中的效果
    """
    def __init__(self, input_dim=784, hidden_dims=[256, 128], activation='relu'):
        super(MLPWithVariousActivations, self).__init__()
        
        layers = []
        prev_dim = input_dim
        
        for hidden_dim in hidden_dims:
            layers.append(nn.Linear(prev_dim, hidden_dim))
            # 根据选择添加激活函数
            if activation == 'relu':
                layers.append(nn.ReLU())
            elif activation == 'leaky_relu':
                layers.append(nn.LeakyReLU(negative_slope=0.01))
            elif activation == 'gelu':
                layers.append(nn.GELU())
            elif activation == 'tanh':
                layers.append(nn.Tanh())
            elif activation == 'sigmoid':
                layers.append(nn.Sigmoid())
            else:
                raise ValueError(f"不支持的激活函数: {activation}")
            prev_dim = hidden_dim
        
        # 输出层(无激活函数,用于回归任务)
        layers.append(nn.Linear(prev_dim, 10))  # 假设10分类任务
        
        self.network = nn.Sequential(*layers)
    
    def forward(self, x):
        # 将输入展平 (batch_size, features)
        x = x.view(x.size(0), -1)
        return self.network(x)


# 验证GELU与ReLU的差异
def compare_gelu_relu():
    """对比GELU和ReLU的梯度特性"""
    x = torch.linspace(-3, 3, 100, requires_grad=True)
    
    relu = nn.ReLU()
    gelu = nn.GELU()
    
    y_relu = relu(x)
    y_gelu = gelu(x)
    
    # 计算梯度(对输入x的梯度)
    y_relu.sum().backward(retain_graph=True)
    grad_relu = x.grad.clone()
    
    x.grad = None  # 清空梯度
    y_gelu.sum().backward()
    grad_gelu = x.grad.clone()
    
    print("GELU vs ReLU 梯度对比(部分值):")
    for i in [0, 25, 50, 75, 99]:
        print(f"  x={x[i].item():.2f}: ReLU梯度={grad_relu[i].item():.2f}, "
              f"GELU梯度={grad_gelu[i].item():.2f}")

compare_gelu_relu()

1.1.3 前向传播与反向传播算法

前向传播(Forward Propagation)

前向传播是指数据从输入层经过隐藏层,最终到达输出层的过程。

以两层神经网络为例

给定输入 x∈Rn\mathbf{x} \in \mathbb{R}^{n}x∈Rn,隐藏层维度 hhh,输出维度 mmm:

第一层(输入层→隐藏层)

z(1)=W(1)x+b(1),a(1)=f(1)(z(1)) \mathbf{z}^{(1)} = W^{(1)}\mathbf{x} + \mathbf{b}^{(1)}, \quad \mathbf{a}^{(1)} = f^{(1)}(\mathbf{z}^{(1)}) z(1)=W(1)x+b(1),a(1)=f(1)(z(1))

第二层(隐藏层→输出层)

z(2)=W(2)a(1)+b(2),a(2)=f(2)(z(2))=y^ \mathbf{z}^{(2)} = W^{(2)}\mathbf{a}^{(1)} + \mathbf{b}^{(2)}, \quad \mathbf{a}^{(2)} = f^{(2)}(\mathbf{z}^{(2)}) = \hat{\mathbf{y}} z(2)=W(2)a(1)+b(2),a(2)=f(2)(z(2))=y^

其中 f(1)f^{(1)}f(1) 和 f(2)f^{(2)}f(2) 分别为隐藏层和输出层的激活函数。

数据流转过程示意图

复制代码
输入 x ──→ [线性变换 W1*x+b1] ──→ [激活函数 f1] ──→ 隐藏表示 a1
                                                        │
                                                        ↓
                                                   [线性变换 W2*a1+b2] ──→ [激活函数 f2] ──→ 预测输出 ŷ
反向传播(Backpropagation)

反向传播是训练神经网络的核心算法,由Rumelhart、Hinton等人于1986年提出,基于链式法则(Chain Rule)计算损失函数对每一层参数的梯度。

核心思想:从输出层向输入层反向计算梯度,每一层的梯度计算都依赖于后一层的梯度结果。

数学推导(以均方误差损失为例)

对于单层网络,损失函数为:

L=12(y^−y)2 \mathcal{L} = \frac{1}{2}(\hat{y} - y)^2 L=21(y^−y)2

其中 y^=f(wTx+b)\hat{y} = f(\mathbf{w}^T\mathbf{x} + b)y^=f(wTx+b)。

根据链式法则:

∂L∂wi=∂L∂y^⋅∂y^∂z⋅∂z∂wi=(y^−y)⋅f′(z)⋅xi \frac{\partial \mathcal{L}}{\partial w_i} = \frac{\partial \mathcal{L}}{\partial \hat{y}} \cdot \frac{\partial \hat{y}}{\partial z} \cdot \frac{\partial z}{\partial w_i} = (\hat{y} - y) \cdot f'(z) \cdot x_i ∂wi∂L=∂y^∂L⋅∂z∂y^⋅∂wi∂z=(y^−y)⋅f′(z)⋅xi

∂L∂b=(y^−y)⋅f′(z) \frac{\partial \mathcal{L}}{\partial b} = (\hat{y} - y) \cdot f'(z) ∂b∂L=(y^−y)⋅f′(z)

对于多层网络 ,设损失函数为 L\mathcal{L}L,则第 lll 层的参数更新公式为:

∂L∂W(l)=∂L∂z(l)⋅∂z(l)∂W(l)=δ(l)⋅(a(l−1))T \frac{\partial \mathcal{L}}{\partial W^{(l)}} = \frac{\partial \mathcal{L}}{\partial \mathbf{z}^{(l)}} \cdot \frac{\partial \mathbf{z}^{(l)}}{\partial W^{(l)}} = \delta^{(l)} \cdot (\mathbf{a}^{(l-1)})^T ∂W(l)∂L=∂z(l)∂L⋅∂W(l)∂z(l)=δ(l)⋅(a(l−1))T

δ(l)={∇a(L)L⊙f′(z(L)),l=L (输出层)(W(l+1))Tδ(l+1)⊙f′(z(l)),l<L (隐藏层) \delta^{(l)} = \begin{cases} \nabla_{\mathbf{a}^{(L)}}\mathcal{L} \odot f'(\mathbf{z}^{(L)}), & l = L \text{ (输出层)} \\ (W^{(l+1)})^T \delta^{(l+1)} \odot f'(\mathbf{z}^{(l)}), & l < L \text{ (隐藏层)} \end{cases} δ(l)={∇a(L)L⊙f′(z(L)),(W(l+1))Tδ(l+1)⊙f′(z(l)),l=L (输出层)l<L (隐藏层)

其中 δ(l)\delta^{(l)}δ(l) 称为误差项 (Error Term),⊙\odot⊙ 表示逐元素乘法(Hadamard积)。

python 复制代码
# ===== 代码实战1.4:从零实现前向传播与反向传播 =====
import torch
import torch.nn as nn

class TwoLayerNetFromScratch:
    """
    从零实现两层神经网络(不使用PyTorch自动求导)
    用于理解前向传播和反向传播的底层原理
    """
    def __init__(self, input_dim, hidden_dim, output_dim):
        """
        初始化网络参数
        
        Args:
            input_dim: 输入维度
            hidden_dim: 隐藏层维度
            output_dim: 输出维度
        """
        # 使用Xavier初始化(后面会详细介绍)
        self.W1 = torch.randn(input_dim, hidden_dim) * np.sqrt(2.0 / (input_dim + hidden_dim))
        self.b1 = torch.zeros(hidden_dim)
        self.W2 = torch.randn(hidden_dim, output_dim) * np.sqrt(2.0 / (hidden_dim + output_dim))
        self.b2 = torch.zeros(output_dim)
    
    def relu(self, x):
        """ReLU激活函数"""
        return torch.maximum(x, torch.zeros_like(x))
    
    def relu_derivative(self, x):
        """ReLU的导数"""
        return (x > 0).float()
    
    def softmax(self, x):
        """Softmax函数(数值稳定版本)"""
        exp_x = torch.exp(x - torch.max(x, dim=1, keepdim=True).values)
        return exp_x / torch.sum(exp_x, dim=1, keepdim=True)
    
    def forward(self, x):
        """
        前向传播(保存中间结果用于反向传播)
        
        Returns:
            输出概率,以及中间结果缓存
        """
        # 第一层线性变换 + ReLU
        self.z1 = torch.matmul(x, self.W1) + self.b1  # (batch, hidden)
        self.a1 = self.relu(self.z1)                   # (batch, hidden)
        
        # 第二层线性变换 + Softmax
        self.z2 = torch.matmul(self.a1, self.W2) + self.b2  # (batch, output)
        self.a2 = self.softmax(self.z2)                      # (batch, output)
        
        return self.a2
    
    def backward(self, x, y_true, learning_rate=0.01):
        """
        反向传播(手动计算梯度)
        
        Args:
            x: 输入数据 (batch, input_dim)
            y_true: 真实标签 (batch,) 整数形式
            learning_rate: 学习率
        """
        batch_size = x.shape[0]
        
        # 将真实标签转换为one-hot编码
        y_one_hot = torch.zeros(batch_size, self.b2.shape[0])
        y_one_hot[torch.arange(batch_size), y_true] = 1
        
        # ===== 输出层梯度计算 =====
        # δ2 = a2 - y (对于Softmax + 交叉熵损失)
        delta2 = self.a2 - y_one_hot  # (batch, output)
        
        # ∂L/∂W2 = a1^T · δ2
        dW2 = torch.matmul(self.a1.T, delta2) / batch_size  # (hidden, output)
        db2 = torch.sum(delta2, dim=0) / batch_size          # (output,)
        
        # ===== 隐藏层梯度计算 =====
        # δ1 = (δ2 · W2^T) ⊙ ReLU'(z1)
        delta1 = torch.matmul(delta2, self.W2.T) * self.relu_derivative(self.z1)  # (batch, hidden)
        
        # ∂L/∂W1 = x^T · δ1
        dW1 = torch.matmul(x.T, delta1) / batch_size  # (input, hidden)
        db1 = torch.sum(delta1, dim=0) / batch_size   # (hidden,)
        
        # ===== 参数更新(梯度下降)=====
        self.W2 -= learning_rate * dW2
        self.b2 -= learning_rate * db2
        self.W1 -= learning_rate * dW1
        self.b1 -= learning_rate * db1
    
    def compute_loss(self, y_pred, y_true):
        """计算交叉熵损失"""
        # 避免log(0)
        y_pred = torch.clamp(y_pred, min=1e-8, max=1.0)
        batch_size = y_true.shape[0]
        y_one_hot = torch.zeros(batch_size, y_pred.shape[1])
        y_one_hot[torch.arange(batch_size), y_true] = 1
        loss = -torch.sum(y_one_hot * torch.log(y_pred)) / batch_size
        return loss


# 使用PyTorch自动求导验证手动实现的正确性
class TwoLayerNetPyTorch(nn.Module):
    """使用PyTorch实现的相同网络(用于对比验证)"""
    def __init__(self, input_dim, hidden_dim, output_dim):
        super(TwoLayerNetPyTorch, self).__init__()
        self.fc1 = nn.Linear(input_dim, hidden_dim)
        self.relu = nn.ReLU()
        self.fc2 = nn.Linear(hidden_dim, output_dim)
    
    def forward(self, x):
        x = self.relu(self.fc1(x))
        x = self.fc2(x)
        return x


if __name__ == "__main__":
    # 构造假数据
    torch.manual_seed(42)
    batch_size = 4
    input_dim = 3
    hidden_dim = 5
    output_dim = 2
    
    x = torch.randn(batch_size, input_dim)
    y = torch.randint(0, output_dim, (batch_size,))
    
    # 测试PyTorch版本
    model = TwoLayerNetPyTorch(input_dim, hidden_dim, output_dim)
    criterion = nn.CrossEntropyLoss()
    optimizer = torch.optim.SGD(model.parameters(), lr=0.01)
    
    # 前向传播
    logits = model(x)
    loss = criterion(logits, y)
    
    # 反向传播
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    
    print(f"PyTorch版本 - 初始损失: {loss.item():.4f}")
    print("PyTorch版本反向传播完成,参数已更新")

深度理解 :反向传播的效率来源于动态规划 思想------每一层的梯度计算只需一次前向传递的中间结果,避免了重复计算,使得训练深层网络成为可能。这也是为什么在代码实战1.4中,我们在forward()方法中保存了z1, a1, z2, a2等中间结果。


1.1.4 梯度下降与优化算法

批量梯度下降(Batch Gradient Descent)

最基础的优化算法,每次迭代使用全部训练数据计算梯度:

wt+1=wt−η∇wL(wt) \mathbf{w}_{t+1} = \mathbf{w}t - \eta \nabla{\mathbf{w}}\mathcal{L}(\mathbf{w}_t) wt+1=wt−η∇wL(wt)

其中 η\etaη 为学习率(Learning Rate)。

缺点

  • 数据量大时计算开销巨大
  • 无法在线更新(需要全部数据后才能更新)
随机梯度下降(Stochastic Gradient Descent, SGD)

每次迭代使用单个样本计算梯度:

wt+1=wt−η∇wL(wt;xi,yi) \mathbf{w}_{t+1} = \mathbf{w}t - \eta \nabla{\mathbf{w}}\mathcal{L}(\mathbf{w}_t; x_i, y_i) wt+1=wt−η∇wL(wt;xi,yi)

优点 :计算快,可在线学习

缺点:梯度估计噪声大,收敛路径震荡

小批量梯度下降(Mini-batch SGD)

实际工程中的标准做法,每次使用一小批数据(如32、64、128个样本):

wt+1=wt−η1m∑i=1m∇wL(wt;xi,yi) \mathbf{w}{t+1} = \mathbf{w}t - \eta \frac{1}{m}\sum{i=1}^{m}\nabla{\mathbf{w}}\mathcal{L}(\mathbf{w}_t; x_i, y_i) wt+1=wt−ηm1i=1∑m∇wL(wt;xi,yi)

其中 mmm 为批量大小(Batch Size)。

动量法(Momentum)

受物理学中动量概念启发,累积历史梯度方向以加速收敛并减少震荡:

vt=βvt−1+(1−β)∇wL(wt) \mathbf{v}t = \beta \mathbf{v}{t-1} + (1-\beta)\nabla_{\mathbf{w}}\mathcal{L}(\mathbf{w}_t) vt=βvt−1+(1−β)∇wL(wt)

wt+1=wt−ηvt \mathbf{w}_{t+1} = \mathbf{w}_t - \eta \mathbf{v}_t wt+1=wt−ηvt

其中 β\betaβ 为动量系数(通常取0.9)。

直观理解:想象一个小球在损失函数曲面上滚动,动量使得它能够越过局部最优的"小坑",继续向全局最优前进。

Adam优化器

Adam(Adaptive Moment Estimation)结合了动量法自适应学习率的优点,是深度学习中最常用的优化器之一。

算法步骤

  1. 计算梯度:gt=∇wL(wt)g_t = \nabla_{\mathbf{w}}\mathcal{L}(\mathbf{w}_t)gt=∇wL(wt)
  2. 更新一阶矩估计(动量):mt=β1mt−1+(1−β1)gtm_t = \beta_1 m_{t-1} + (1-\beta_1)g_tmt=β1mt−1+(1−β1)gt
  3. 更新二阶矩估计(自适应学习率):vt=β2vt−1+(1−β2)gt2v_t = \beta_2 v_{t-1} + (1-\beta_2)g_t^2vt=β2vt−1+(1−β2)gt2
  4. 偏差修正:m^t=mt1−β1t,v^t=vt1−β2t\hat{m}_t = \frac{m_t}{1-\beta_1^t}, \quad \hat{v}_t = \frac{v_t}{1-\beta_2^t}m^t=1−β1tmt,v^t=1−β2tvt
  5. 参数更新:wt+1=wt−ηm^tv^t+ϵw_{t+1} = w_t - \eta \frac{\hat{m}_t}{\sqrt{\hat{v}_t} + \epsilon}wt+1=wt−ηv^t +ϵm^t

默认超参数

  • β1=0.9\beta_1 = 0.9β1=0.9(一阶矩衰减率)
  • β2=0.999\beta_2 = 0.999β2=0.999(二阶矩衰减率)
  • ϵ=10−8\epsilon = 10^{-8}ϵ=10−8(数值稳定常数)
  • η=10−3\eta = 10^{-3}η=10−3(学习率)
python 复制代码
# ===== 代码实战1.5:不同优化器的收敛对比 =====
import torch
import torch.nn as nn
import matplotlib.pyplot as plt

def compare_optimizers():
    """
    对比SGD、SGD+Momentum、Adam在简单任务上的收敛速度
    """
    torch.manual_seed(42)
    
    # 构造一个简单的线性回归问题
    # 真实参数: y = 2x1 - 3x2 + 1 + noise
    n_samples = 1000
    X = torch.randn(n_samples, 2)
    true_w = torch.tensor([[2.0], [-3.0]])
    true_b = torch.tensor([1.0])
    y = X @ true_w + true_b + 0.1 * torch.randn(n_samples, 1)
    
    # 定义相同的模型(简单的线性模型)
    class LinearModel(nn.Module):
        def __init__(self):
            super(LinearModel, self).__init__()
            self.linear = nn.Linear(2, 1)
        
        def forward(self, x):
            return self.linear(x)
    
    # 测试不同的优化器
    optimizers_config = {
        'SGD': lambda model: torch.optim.SGD(model.parameters(), lr=0.01),
        'SGD+Momentum': lambda model: torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.9),
        'Adam': lambda model: torch.optim.Adam(model.parameters(), lr=0.001),
    }
    
    loss_history = {}
    
    for opt_name, opt_fn in optimizers_config.items():
        model = LinearModel()
        optimizer = opt_fn(model)
        criterion = nn.MSELoss()
        
        losses = []
        for epoch in range(200):
            # 前向传播
            pred = model(X)
            loss = criterion(pred, y)
            losses.append(loss.item())
            
            # 反向传播
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()
        
        loss_history[opt_name] = losses
        print(f"{opt_name}: 最终损失 = {losses[-1]:.6f}")
    
    # 绘制损失曲线
    plt.figure(figsize=(10, 6))
    for opt_name, losses in loss_history.items():
        plt.plot(losses, label=opt_name, linewidth=2)
    plt.xlabel('迭代次数', fontsize=12)
    plt.ylabel('损失值', fontsize=12)
    plt.title('不同优化器的收敛速度对比', fontsize=14)
    plt.legend(fontsize=12)
    plt.grid(True, alpha=0.3)
    plt.tight_layout()
    plt.savefig('optimizer_comparison.png', dpi=150)
    plt.show()

compare_optimizers()
AdamW:解耦权重衰减的Adam

在原始的Adam中,L2正则化(权重衰减)与梯度更新耦合,导致实际的正则化效果与预期不符。AdamW(Adam with Decoupled Weight Decay)将权重衰减与梯度更新解耦:

AdamW参数更新:{mt=β1mt−1+(1−β1)gtvt=β2vt−1+(1−β2)gt2m^t=mt/(1−β1t),v^t=vt/(1−β2t)wt+1=wt−η(m^tv^t+ϵ+λwt) \text{AdamW参数更新}: \begin{cases} m_t = \beta_1 m_{t-1} + (1-\beta_1)g_t \\ v_t = \beta_2 v_{t-1} + (1-\beta_2)g_t^2 \\ \hat{m}_t = m_t / (1-\beta_1^t), \quad \hat{v}t = v_t / (1-\beta_2^t) \\ w{t+1} = w_t - \eta(\frac{\hat{m}_t}{\sqrt{\hat{v}_t} + \epsilon} + \lambda w_t) \end{cases} AdamW参数更新:⎩ ⎨ ⎧mt=β1mt−1+(1−β1)gtvt=β2vt−1+(1−β2)gt2m^t=mt/(1−β1t),v^t=vt/(1−β2t)wt+1=wt−η(v^t +ϵm^t+λwt)

其中 λ\lambdaλ 为权重衰减系数(通常为 0.010.010.01 或 0.10.10.1)。

工程实践 :在Transformer模型(如BERT、GPT)的预训练中,AdamW是标准配置。PyTorch中实现方式:

python 复制代码
optimizer = torch.optim.AdamW(model.parameters(), lr=2e-5, weight_decay=0.01)

1.2 深度学习核心组件

1.2.1 损失函数设计

损失函数(Loss Function)衡量模型预测值与真实值之间的差距,是模型训练的"指南针"。

回归任务常用损失函数

1. 均方误差(Mean Squared Error, MSE)

LMSE=1N∑i=1N(yi−y^i)2 \mathcal{L}{\text{MSE}} = \frac{1}{N}\sum{i=1}^{N}(y_i - \hat{y}_i)^2 LMSE=N1i=1∑N(yi−y^i)2

特点:对异常值敏感(平方放大了大误差的影响)。

2. 平均绝对误差(Mean Absolute Error, MAE)

LMAE=1N∑i=1N∣yi−y^i∣ \mathcal{L}{\text{MAE}} = \frac{1}{N}\sum{i=1}^{N}|y_i - \hat{y}_i| LMAE=N1i=1∑N∣yi−y^i∣

特点:对异常值鲁棒,但零点不可导。

3. Huber Loss(平滑MAE)

Lδ(a)={12a2,∣a∣≤δδ(∣a∣−12δ),∣a∣>δ \mathcal{L}_{\delta}(a) = \begin{cases} \frac{1}{2}a^2, & |a| \leq \delta \\ \delta(|a| - \frac{1}{2}\delta), & |a| > \delta \end{cases} Lδ(a)={21a2,δ(∣a∣−21δ),∣a∣≤δ∣a∣>δ

其中 a=y−y^a = y - \hat{y}a=y−y^,δ\deltaδ 为超参数(通常取1.0或1.5)。

选择建议:数据含异常值时用MAE或Huber Loss;数据干净时用MSE(梯度更平滑,收敛更稳定)。

分类任务常用损失函数

1. 交叉熵损失(Cross Entropy Loss)

对于多分类问题,设真实标签为one-hot向量 y\mathbf{y}y,模型预测概率为 y^\hat{\mathbf{y}}y^:

LCE=−∑i=1Cyilog⁡(y^i) \mathcal{L}{\text{CE}} = -\sum{i=1}^{C} y_i \log(\hat{y}_i) LCE=−i=1∑Cyilog(y^i)

其中 CCC 为类别数。

2. 二元交叉熵损失(Binary Cross Entropy, BCE)

用于二分类问题:

LBCE=−1N∑i=1Nyilog⁡(y\^i)+(1−yi)log⁡(1−y\^i) \mathcal{L}{\text{BCE}} = -\frac{1}{N}\sum{i=1}^{N}y_i \\log(\\hat{y}_i) + (1-y_i)\\log(1-\\hat{y}_i) LBCE=−N1i=1∑Nyilog(y\^i)+(1−yi)log(1−y\^i)

3. Focal Loss(处理类别不平衡)

LFocal=−α(1−y^i)γyilog⁡(y^i)−(1−α)y^iγ(1−yi)log⁡(1−y^i) \mathcal{L}_{\text{Focal}} = -\alpha(1-\hat{y}_i)^\gamma y_i \log(\hat{y}_i) - (1-\alpha)\hat{y}_i^\gamma (1-y_i)\log(1-\hat{y}_i) LFocal=−α(1−y^i)γyilog(y^i)−(1−α)y^iγ(1−yi)log(1−y^i)

其中 α\alphaα 平衡正负样本权重,γ\gammaγ 降低易分类样本的损失贡献(通常 γ=2\gamma=2γ=2)。

企业级场景:在风控、推荐系统等正负样本极度不平衡的场景中,Focal Loss或加权交叉熵是必须的。

python 复制代码
# ===== 代码实战1.6:常用损失函数的PyTorch实现 =====
import torch
import torch.nn as nn
import torch.nn.functional as F

class FocalLoss(nn.Module):
    """
    Focal Loss实现
    用于解决类别不平衡问题
    """
    def __init__(self, alpha=0.25, gamma=2.0, reduction='mean'):
        """
        初始化Focal Loss
        
        Args:
            alpha: 平衡因子,用于平衡正负样本
            gamma: 聚焦因子,降低简单样本的权重
            reduction: 损失归约方式,'mean', 'sum', 或'none'
        """
        super(FocalLoss, self).__init__()
        self.alpha = alpha
        self.gamma = gamma
        self.reduction = reduction
    
    def forward(self, inputs, targets):
        """
        前向计算
        
        Args:
            inputs: 模型输出logits,形状为 (N, C)
            targets: 真实标签,形状为 (N,)
        
        Returns:
            Focal Loss值
        """
        # 计算交叉熵(不带reduction)
        ce_loss = F.cross_entropy(inputs, targets, reduction='none')
        # 计算概率
        pt = torch.exp(-ce_loss)
        # Focal Loss公式
        focal_loss = self.alpha * (1 - pt) ** self.gamma * ce_loss
        
        if self.reduction == 'mean':
            return focal_loss.mean()
        elif self.reduction == 'sum':
            return focal_loss.sum()
        else:
            return focal_loss


# 测试各类损失函数
def test_loss_functions():
    """测试并对比不同损失函数的数值特性"""
    torch.manual_seed(42)
    
    # 模拟模型输出(logits)和真实标签
    logits = torch.tensor([[2.0, 1.0, 0.1],   # 样本1:类别0概率高
                           [0.5, 2.0, 0.3],   # 样本2:类别1概率高
                           [0.2, 0.3, 3.0]])  # 样本3:类别2概率高
    targets = torch.tensor([0, 1, 2])
    
    # CrossEntropyLoss
    ce_loss = nn.CrossEntropyLoss()
    loss_ce = ce_loss(logits, targets)
    print(f"Cross Entropy Loss: {loss_ce.item():.4f}")
    
    # Focal Loss
    focal_loss = FocalLoss(alpha=0.25, gamma=2.0)
    loss_focal = focal_loss(logits, targets)
    print(f"Focal Loss (gamma=2.0): {loss_focal.item():.4f}")
    
    # 模拟不平衡场景:一个难样本(logits接近随机)
    hard_logits = torch.tensor([[0.8, 0.7, 0.9]])  # 模型预测不确定
    hard_target = torch.tensor([0])
    
    ce_hard = ce_loss(hard_logits, hard_target)
    focal_hard = focal_loss(hard_logits, hard_target)
    print(f"\n难样本 - CE: {ce_hard.item():.4f}, Focal: {focal_hard.item():.4f}")
    print("说明: Focal Loss会降低易分类样本的贡献,聚焦难样本")

test_loss_functions()

1.2.2 正则化技术

正则化(Regularization)是一系列用于防止过拟合、提高模型泛化能力的技术。

L1/L2正则化

通过在损失函数中添加参数的范数惩罚项来实现:

L2正则化(Ridge Regression / Weight Decay)

Ltotal=Ldata+λ2∥w∥22=Ldata+λ2∑iwi2 \mathcal{L}{\text{total}} = \mathcal{L}{\text{data}} + \frac{\lambda}{2}\|\mathbf{w}\|2^2 = \mathcal{L}{\text{data}} + \frac{\lambda}{2}\sum_{i} w_i^2 Ltotal=Ldata+2λ∥w∥22=Ldata+2λi∑wi2

L1正则化(Lasso Regression)

Ltotal=Ldata+λ∥w∥1=Ldata+λ∑i∣wi∣ \mathcal{L}{\text{total}} = \mathcal{L}{\text{data}} + \lambda\|\mathbf{w}\|1 = \mathcal{L}{\text{data}} + \lambda\sum_{i} |w_i| Ltotal=Ldata+λ∥w∥1=Ldata+λi∑∣wi∣

特性 L1正则化 L2正则化
惩罚项 ∣w∣1|\mathbf{w}|_1∣w∣1 ∣w∣22|\mathbf{w}|_2^2∣w∣22
参数稀疏性 产生稀疏解(部分参数为0) 参数趋近于0但不为0
特征选择 可进行特征选择 不适用特征选择
梯度特性 梯度为常数(需注意零点) 梯度与参数成正比

工程选择:L1适用于特征选择场景(如高维稀疏数据);L2是深度学习中的默认选择(配合AdamW使用)。

Dropout

Dropout是一种在训练过程中随机丢弃(置零)一部分神经元的技术,由Hinton等人于2012年提出。

工作原理

  • 训练时:每个神经元以概率 ppp 被保留(或以概率 1−p1-p1−p 被丢弃)
  • 测试时:所有神经元都被使用,但输出需要乘以 ppp 进行缩放(或使用倒置Dropout ,在训练时除以 ppp)

数学表达(倒置Dropout):

adropout=a⊙mp,m∼Bernoulli(p) \mathbf{a}_{\text{dropout}} = \frac{\mathbf{a} \odot \mathbf{m}}{p}, \quad \mathbf{m} \sim \text{Bernoulli}(p) adropout=pa⊙m,m∼Bernoulli(p)

python 复制代码
# ===== 代码实战1.7:Dropout的正确使用方式 =====
import torch
import torch.nn as nn

class MLPWithDropout(nn.Module):
    """
    带Dropout的MLP网络
    展示训练模式和评估模式的差异
    """
    def __init__(self, input_dim=784, hidden_dims=[512, 256], num_classes=10, dropout_rate=0.5):
        super(MLPWithDropout, self).__init__()
        
        layers = []
        prev_dim = input_dim
        
        for hidden_dim in hidden_dims:
            layers.append(nn.Linear(prev_dim, hidden_dim))
            layers.append(nn.ReLU())
            # Dropout层
            layers.append(nn.Dropout(p=dropout_rate))
            prev_dim = hidden_dim
        
        layers.append(nn.Linear(prev_dim, num_classes))
        self.network = nn.Sequential(*layers)
    
    def forward(self, x):
        x = x.view(x.size(0), -1)  # 展平
        return self.network(x)


def demonstrate_dropout_behavior():
    """演示Dropout在训练和评估模式下的不同行为"""
    model = MLPWithDropout(dropout_rate=0.5)
    
    # 构造固定输入
    x = torch.ones(1, 784)
    
    # 训练模式:Dropout激活,每次前向传播结果不同
    model.train()
    print("训练模式 (Dropout激活):")
    for i in range(3):
        output = model(x)
        # 检查中间层的激活值
        print(f"  前向传播 #{i+1}: 输出和 = {output.sum().item():.2f}")
    
    # 评估模式:Dropout关闭,结果确定
    model.eval()
    print("\n评估模式 (Dropout关闭):")
    with torch.no_grad():  # 评估时不需要计算梯度
        for i in range(3):
            output = model(x)
            print(f"  前向传播 #{i+1}: 输出和 = {output.sum().item():.2f}")

demonstrate_dropout_behavior()

⚠️ 企业级避坑1:Dropout在Transformer中的使用

在Transformer架构中,不要在注意力层和归一化层之间使用Dropout,这会导致训练不稳定。现代大模型(如BERT、GPT)通常在以下位置使用Dropout:

  • 嵌入层之后(Embedding Dropout)
  • 注意力权重之后(Attention Dropout,比例通常很小如0.1)
  • FFN的激活值之后(FFN Dropout)

另外,当使用大批量训练 时,应适当降低Dropout比例(如从0.5降至0.1),因为大批量本身已有正则化效果。

Batch Normalization(批归一化)

Batch Normalization(BN)由Sergey Ioffe和Christian Szegedy于2015年提出,通过归一化每一层的输入来加速训练并提高稳定性。

核心思想

对于一批输入 x={x1,x2,...,xm}\mathbf{x} = \{x_1, x_2, \dots, x_m\}x={x1,x2,...,xm},BN对 each feature 进行归一化:

μB=1m∑i=1mxi \mu_B = \frac{1}{m}\sum_{i=1}^{m}x_i μB=m1i=1∑mxi

σB2=1m∑i=1m(xi−μB)2 \sigma_B^2 = \frac{1}{m}\sum_{i=1}^{m}(x_i - \mu_B)^2 σB2=m1i=1∑m(xi−μB)2

x^i=xi−μBσB2+ϵ \hat{x}_i = \frac{x_i - \mu_B}{\sqrt{\sigma_B^2 + \epsilon}} x^i=σB2+ϵ xi−μB

yi=γx^i+β y_i = \gamma \hat{x}_i + \beta yi=γx^i+β

其中 γ\gammaγ 和 β\betaβ 是可学习的缩放和偏移参数,使得网络可以"恢复"原始表示(如果这样做是最优的)。

优点

  1. 允许使用更大的学习率(加速收敛)
  2. 起到正则化效果(减少对Dropout的依赖)
  3. 减轻对权重初始化的依赖

缺点

  1. 依赖于批量大小(小批量时统计量不准确)
  2. 在RNN等动态网络上难以应用
  3. 训练和推理时的行为不一致(需要使用滑动平均)

现代替代方案 :在计算机视觉中,BN仍广泛使用;但在NLP和Transformer中,Layer Normalization(将在3.2.4节详细介绍)是更常用的选择,因为它不依赖于批量统计量。

python 复制代码
# ===== 代码实战1.8:BatchNorm与LayerNorm的对比 =====
import torch
import torch.nn as nn

def compare_norm_layers():
    """对比BatchNorm和LayerNorm的归一化维度"""
    
    # 构造输入: (batch_size, seq_len, hidden_dim)
    # 以NLP任务为例: batch_size=2, seq_len=3, hidden_dim=4
    x = torch.tensor([
        [[1.0, 2.0, 3.0, 4.0],
         [2.0, 3.0, 4.0, 5.0],
         [3.0, 4.0, 5.0, 6.0]],
        [[4.0, 5.0, 6.0, 7.0],
         [5.0, 6.0, 7.0, 8.0],
         [6.0, 7.0, 8.0, 9.0]]
    ], dtype=torch.float32)
    
    print(f"输入形状: {x.shape}")
    print(f"输入:\n{x}")
    
    # BatchNorm: 在 batch 和 seq_len 维度上归一化(对每个特征独立)
    # 对于Conv网络,通常是对每个channel独立归一化
    bn = nn.BatchNorm1d(4)  # num_features=hidden_dim
    # 注意: BatchNorm1d期望 (N, C, L) 格式
    x_bn = x.transpose(1, 2)  # (2, 4, 3)
    out_bn = bn(x_bn)
    print(f"\nBatchNorm输出形状: {out_bn.shape}")
    
    # LayerNorm: 在最后一维(hidden_dim)上归一化(对每个样本独立)
    ln = nn.LayerNorm(4)
    out_ln = ln(x)
    print(f"LayerNorm输出形状: {out_ln.shape}")
    print(f"\nLayerNorm输出 (每个样本的特征被归一化到均值0方差1):")
    print(out_ln)

compare_norm_layers()

1.2.3 过拟合与欠拟合的识别与处理

识别方法
现象 判断 原因
训练误差低,验证误差高 过拟合 模型过于复杂,记住了训练数据的噪声
训练误差高,验证误差也高 欠拟合 模型过于简单,无法捕捉数据规律
训练误差持续下降,验证误差先降后升 过拟合开始 需要从该epoch开始早停

学习曲线(Learning Curve)诊断法

python 复制代码
# ===== 代码实战1.9:绘制学习曲线诊断过拟合/欠拟合 =====
import torch
import torch.nn as nn
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split

def generate_toy_data(n_samples=1000, n_features=20, noise=0.1):
    """生成一个简单的分类数据集"""
    torch.manual_seed(42)
    X = torch.randn(n_samples, n_features)
    # 真实决策边界: 仅前5个特征有用
    true_weights = torch.zeros(n_features)
    true_weights[:5] = torch.tensor([2.0, -1.5, 1.0, -0.5, 0.8])
    logits = X @ true_weights
    y = (logits > 0).float()
    return X, y

def train_and_plot_curves(model, X_train, y_train, X_val, y_val, epochs=200):
    """训练模型并绘制学习曲线"""
    optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
    criterion = nn.BCELoss()
    
    train_losses = []
    val_losses = []
    
    for epoch in range(epochs):
        # 训练模式
        model.train()
        optimizer.zero_grad()
        pred_train = model(X_train).squeeze()
        loss_train = criterion(pred_train, y_train)
        loss_train.backward()
        optimizer.step()
        train_losses.append(loss_train.item())
        
        # 评估模式
        model.eval()
        with torch.no_grad():
            pred_val = model(X_val).squeeze()
            loss_val = criterion(pred_val, y_val)
            val_losses.append(loss_val.item())
        
        if (epoch + 1) % 50 == 0:
            print(f"Epoch {epoch+1}: Train Loss = {loss_train.item():.4f}, "
                  f"Val Loss = {loss_val.item():.4f}")
    
    # 绘制学习曲线
    plt.figure(figsize=(10, 6))
    plt.plot(train_losses, label='训练损失', linewidth=2)
    plt.plot(val_losses, label='验证损失', linewidth=2)
    plt.xlabel('Epoch', fontsize=12)
    plt.ylabel('Loss', fontsize=12)
    plt.title('学习曲线(用于诊断过拟合/欠拟合)', fontsize=14)
    plt.legend(fontsize=12)
    plt.grid(True, alpha=0.3)
    plt.tight_layout()
    plt.savefig('learning_curve.png', dpi=150)
    plt.show()

# 主程序
if __name__ == "__main__":
    # 生成数据
    X, y = generate_toy_data(n_samples=2000, n_features=50)
    X_train, X_val, y_train, y_val = train_test_split(
        X, y, test_size=0.3, random_state=42
    )
    
    # 构建一个容易过拟合的模型(参数远多于样本)
    overparam_model = nn.Sequential(
        nn.Linear(50, 512),
        nn.ReLU(),
        nn.Linear(512, 256),
        nn.ReLU(),
        nn.Linear(256, 1),
        nn.Sigmoid()
    )
    
    print("=== 过拟合场景(大模型+小数据)===")
    train_and_plot_curves(overparam_model, X_train, y_train, X_val, y_val, epochs=500)
处理策略总结

处理过拟合

  1. 增加数据:数据增强、收集更多数据
  2. 正则化:L2正则化、Dropout、Early Stopping
  3. 简化模型:减少层数、减少每层神经元数量
  4. 数据清洗:去除异常值、噪声样本

处理欠拟合

  1. 增加模型复杂度:增加层数、增加每层神经元数量
  2. 减少正则化:降低L2系数、关闭Dropout
  3. 增加训练时间:更多epoch、调整学习率
  4. 特征工程:增加更有意义的特征

1.2.4 超参数调优策略

重要超参数分类
类别 超参数 典型范围 影响程度
优化相关 学习率 η\etaη 10−5∼10−110^{-5} \sim 10^{-1}10−5∼10−1 ⭐⭐⭐
优化相关 Batch Size 32, 64, 128, 256 ⭐⭐
架构相关 隐藏层数 2~24层 ⭐⭐⭐
架构相关 每层神经元数量 64~4096 ⭐⭐
正则化相关 Dropout比例 0.1~0.5 ⭐⭐
正则化相关 Weight Decay 10−5∼10−210^{-5} \sim 10^{-2}10−5∼10−2
调优方法论

1. 网格搜索(Grid Search)

遍历所有超参数组合,计算成本高,适用于少数量超参数。

2. 随机搜索(Random Search)

在超参数空间中随机采样,实践中比网格搜索更高效(因为有些超参数比其他更重要)。

3. 贝叶斯优化(Bayesian Optimization)

使用高斯过程建模超参数与验证集性能的关系,智能选择下一组超参数。

4. 学习率调度(Learning Rate Scheduling)

python 复制代码
# ===== 代码实战1.10:学习率调度策略 =====
import torch
import torch.optim as optim
import matplotlib.pyplot as plt

def demonstrate_lr_schedules():
    """演示不同的学习率调度策略"""
    model = nn.Linear(10, 1)  # 简单模型用于演示
    
    # 初始学习率
    base_lr = 0.1
    optimizer = optim.SGD(model.parameters(), lr=base_lr)
    
    # 定义不同的调度器
    schedulers = {
        'StepLR (每30步衰减0.1)': optim.lr_scheduler.StepLR(optimizer, step_size=30, gamma=0.1),
        'ExponentialLR (每步衰减0.95)': optim.lr_scheduler.ExponentialLR(optimizer, gamma=0.95),
        'CosineAnnealingLR': optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=100),
    }
    
    # 记录学习率变化
    lr_history = {name: [] for name in schedulers.keys()}
    
    for name, scheduler in schedulers.items():
        optimizer = optim.SGD(model.parameters(), lr=base_lr)
        scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=30, gamma=0.1) if 'Step' in name else \
                    optim.lr_scheduler.ExponentialLR(optimizer, gamma=0.95) if 'Exponential' in name else \
                    optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=100)
        
        lrs = []
        for epoch in range(100):
            # 模拟训练步骤
            lrs.append(optimizer.param_groups[0]['lr'])
            scheduler.step()
        lr_history[name] = lrs
    
    # 绘制
    fig, axes = plt.subplots(1, 3, figsize=(15, 4))
    for idx, (name, lrs) in enumerate(lr_history.items()):
        axes[idx].plot(lrs)
        axes[idx].set_title(name, fontsize=10)
        axes[idx].set_xlabel('Epoch')
        axes[idx].set_ylabel('Learning Rate')
        axes[idx].grid(True, alpha=0.3)
    plt.tight_layout()
    plt.savefig('lr_schedules.png', dpi=150)
    plt.show()

demonstrate_lr_schedules()

⚠️ 企业级避坑2:学习率warmup的重要性

在训练大模型(尤其是Transformer)时,不要从最大学习率开始训练 !应该使用学习率warmup策略:

python 复制代码
# PyTorch实现warmup
from torch.optim.lr_scheduler import LambdaLR

def get_linear_schedule_with_warmup(optimizer, num_warmup_steps, num_training_steps):
    def lr_lambda(current_step):
        if current_step < num_warmup_steps:
            return float(current_step) / float(max(1, num_warmup_steps))
        return max(0.0, float(num_training_steps - current_step) / 
                         float(max(1, num_training_steps - num_warmup_steps)))
    
    return LambdaLR(optimizer, lr_lambda)

原因:训练初期,模型参数是随机初始化的,如果立即使用大学习率,会导致训练不稳定甚至发散。Warmup让学习率从接近0逐渐增加到预设值,使训练更加稳定。


1.3 神经网络架构演进

1.3.1 从MLP到CNN:卷积神经网络原理

MLP的局限性

MLP(全连接网络)在处理图像等高维数据时有严重缺陷:

  1. 参数爆炸 :一张 224×224×3224 \times 224 \times 3224×224×3 的图片展平后有150,528个像素,如果第一层隐藏层有1000个神经元,则需要 1.5亿个参数
  2. 忽略空间结构:图像中相邻像素的相关性被完全忽略。
  3. 平移不变性缺失:同一物体出现在图像不同位置,MLP需要重新学习。
卷积神经网络(CNN)的核心思想

CNN通过以下三个关键设计解决上述问题:

1. 局部感受野(Local Receptive Field)

每个神经元只连接输入图像的局部区域(如 3×33\times33×3 或 5×55\times55×5 的局部块),而非全连接。

2. 权值共享(Weight Sharing)

同一个卷积核(filter)在整张图像上滑动,大大减少参数数量。

3. 平移不变性(Translation Invariance)

通过池化层(Pooling Layer)或步长卷积,使得网络对物体的位置变化不敏感。

卷积操作详解

给定输入 X∈RH×W×Cin\mathbf{X} \in \mathbb{R}^{H \times W \times C_{in}}X∈RH×W×Cin 和卷积核 K∈Rkh×kw×Cin×Cout\mathbf{K} \in \mathbb{R}^{k_h \times k_w \times C_{in} \times C_{out}}K∈Rkh×kw×Cin×Cout,输出特征图为:

Yi,j,cout=∑cin=1Cin∑m=0kh−1∑n=0kw−1Xi⋅sh+m,j⋅sw+n,cin⋅Km,n,cin,cout+bcout Y_{i,j,c_{out}} = \sum_{c_{in}=1}^{C_{in}}\sum_{m=0}^{k_h-1}\sum_{n=0}^{k_w-1} X_{i\cdot s_h+m, j\cdot s_w+n, c_{in}} \cdot K_{m,n,c_{in},c_{out}} + b_{c_{out}} Yi,j,cout=cin=1∑Cinm=0∑kh−1n=0∑kw−1Xi⋅sh+m,j⋅sw+n,cin⋅Km,n,cin,cout+bcout

其中 sh,sws_h, s_wsh,sw 为垂直和水平方向的步长(stride)。

输出尺寸计算公式

Hout=⌊Hin+2ph−khsh⌋+1 H_{out} = \left\lfloor\frac{H_{in} + 2p_h - k_h}{s_h}\right\rfloor + 1 Hout=⌊shHin+2ph−kh⌋+1

Wout=⌊Win+2pw−kwsw⌋+1 W_{out} = \left\lfloor\frac{W_{in} + 2p_w - k_w}{s_w}\right\rfloor + 1 Wout=⌊swWin+2pw−kw⌋+1

其中 ph,pwp_h, p_wph,pw 为填充(padding)大小。

python 复制代码
# ===== 代码实战1.11:从零实现2D卷积操作 =====
import torch
import torch.nn as nn
import torch.nn.functional as F

def manual_conv2d(x, kernel, bias=None, stride=1, padding=0):
    """
    手动实现2D卷积(用于理解底层原理)
    
    Args:
        x: 输入张量 (N, C_in, H, W)
        kernel: 卷积核 (C_out, C_in, kH, kW)
        bias: 偏置 (C_out,)
        stride: 步长
        padding: 填充大小
    
    Returns:
        卷积结果 (N, C_out, H_out, W_out)
    """
    N, C_in, H, W = x.shape
    C_out, _, kH, kW = kernel.shape
    
    # 添加padding
    if padding > 0:
        x = F.pad(x, (padding, padding, padding, padding))
    
    # 计算输出尺寸
    H_out = (H + 2 * padding - kH) // stride + 1
    W_out = (W + 2 * padding - kW) // stride + 1
    
    # 初始化输出
    output = torch.zeros(N, C_out, H_out, W_out)
    
    # 滑动窗口卷积
    for i in range(H_out):
        for j in range(W_out):
            # 当前窗口位置
            h_start = i * stride
            h_end = h_start + kH
            w_start = j * stride
            w_end = w_start + kW
            
            # 提取窗口 (N, C_in, kH, kW)
            window = x[:, :, h_start:h_end, w_start:w_end]
            
            # 卷积计算: (N, C_out, C_in, kH, kW) -> (N, C_out)
            for c_out in range(C_out):
                # 逐通道卷积并求和
                conv_result = (window * kernel[c_out:c_out+1, :, :, :]).sum(dim=(1, 2, 3))
                if bias is not None:
                    conv_result += bias[c_out]
                output[:, c_out, i, j] = conv_result
    
    return output

def compare_with_pytorch():
    """对比手动实现与PyTorch官方实现的卷积结果"""
    torch.manual_seed(42)
    
    # 构造输入 (N=1, C_in=3, H=5, W=5)
    x = torch.randn(1, 3, 5, 5)
    
    # 构造卷积核 (C_out=2, C_in=3, kH=3, kW=3)
    kernel = torch.randn(2, 3, 3, 3)
    bias = torch.randn(2)
    
    # 手动实现
    output_manual = manual_conv2d(x, kernel, bias, stride=1, padding=1)
    
    # PyTorch官方实现
    conv = nn.Conv2d(in_channels=3, out_channels=2, kernel_size=3, stride=1, padding=1, bias=True)
    # 手动设置权重
    conv.weight.data = kernel
    conv.bias.data = bias
    output_pytorch = conv(x)
    
    # 对比
    print(f"手动实现输出形状: {output_manual.shape}")
    print(f"PyTorch输出形状: {output_pytorch.shape}")
    print(f"最大差异: {torch.max(torch.abs(output_manual - output_pytorch)).item():.6f}")
    print("说明: 差异应接近0(数值精度范围内一致)")

compare_with_pytorch()
经典CNN架构演进
架构 年份 核心创新 应用场景
LeNet-5 1998 首个成功应用的CNN 手写数字识别
AlexNet 2012 ReLU+Dropout+GPU训练 图像分类(ImageNet)
VGGNet 2014 小卷积核(3×3)堆叠 图像分类、特征提取
ResNet 2015 残差连接(Skip Connection) 深度网络训练(152层+)
EfficientNet 2019 复合缩放(深度/宽度/分辨率) 高效图像分类

重点关注ResNet的残差连接

y=F(x)+x \mathbf{y} = \mathcal{F}(\mathbf{x}) + \mathbf{x} y=F(x)+x

这一简单设计解决了深度网络的梯度消失/爆炸问题 ,使得训练上百甚至上千层的网络成为可能。这也是为什么现代大模型(如Transformer)也广泛采用残差连接的原因。


1.3.2 RNN与LSTM:序列建模的突破

循环神经网络(RNN)原理

RNN通过在时间步之间共享参数,能够处理变长序列数据

RNN单元的数学表达

ht=tanh⁡(Whht−1+Wxxt+b) \mathbf{h}t = \tanh(W_h\mathbf{h}{t-1} + W_x\mathbf{x}_t + \mathbf{b}) ht=tanh(Whht−1+Wxxt+b)

yt=Wyht+by \mathbf{y}_t = W_y\mathbf{h}_t + \mathbf{b}_y yt=Wyht+by

其中 ht\mathbf{h}_tht 为时刻 ttt 的隐藏状态,编码了序列的历史信息。

数据流转过程

复制代码
x1 → [RNN单元] → h1 → y1
             ↓
x2 → [RNN单元] → h2 → y2
             ↓
x3 → [RNN单元] → h3 → y3
             ↓
           ...

局限性 :RNN存在长期依赖问题(Long-Term Dependency)------随着序列长度增加,早期信息在传递过程中逐渐"消失"(梯度消失),导致无法捕捉长距离依赖关系。

LSTM(长短期记忆网络)

LSTM通过精心设计的门控机制解决RNN的长期依赖问题。

LSTM的核心组件(每个LSTM单元包含以下门):

  1. 遗忘门(Forget Gate) :决定丢弃哪些历史信息

    ft=σ(Wfht−1,xt+bf) \mathbf{f}_t = \sigma(W_f\\mathbf{h}_{t-1}, \\mathbf{x}_t + \mathbf{b}_f) ft=σ(Wfht−1,xt+bf)

  2. 输入门(Input Gate) :决定写入哪些新信息

    it=σ(Wiht−1,xt+bi) \mathbf{i}_t = \sigma(W_i\\mathbf{h}_{t-1}, \\mathbf{x}_t + \mathbf{b}_i) it=σ(Wiht−1,xt+bi)

    C~t=tanh⁡(WCht−1,xt+bC) \tilde{\mathbf{C}}_t = \tanh(W_C\\mathbf{h}_{t-1}, \\mathbf{x}_t + \mathbf{b}_C) C~t=tanh(WCht−1,xt+bC)

  3. 细胞状态更新(Cell State Update)

    Ct=ft⊙Ct−1+it⊙C~t \mathbf{C}_t = \mathbf{f}t \odot \mathbf{C}{t-1} + \mathbf{i}_t \odot \tilde{\mathbf{C}}_t Ct=ft⊙Ct−1+it⊙C~t

  4. 输出门(Output Gate) :决定输出哪些信息

    ot=σ(Woht−1,xt+bo) \mathbf{o}_t = \sigma(W_o\\mathbf{h}_{t-1}, \\mathbf{x}_t + \mathbf{b}_o) ot=σ(Woht−1,xt+bo)

    ht=ot⊙tanh⁡(Ct) \mathbf{h}_t = \mathbf{o}_t \odot \tanh(\mathbf{C}_t) ht=ot⊙tanh(Ct)

python 复制代码
# ===== 代码实战1.12:从零实现LSTM单元 =====
import torch
import torch.nn as nn

class LSTMCellFromScratch(nn.Module):
    """
    从零实现LSTM单元(用于理解门控机制)
    """
    def __init__(self, input_dim, hidden_dim):
        """
        初始化LSTM单元
        
        Args:
            input_dim: 输入特征维度
            hidden_dim: 隐藏状态维度
        """
        super(LSTMCellFromScratch, self).__init__()
        
        # 将所有门的参数合并为一个大矩阵(提高效率)
        # 输入: [h_{t-1}, x_t],维度为 (hidden_dim + input_dim)
        # 输出: [i, f, g, o],维度为 (4 * hidden_dim)
        self.x2h = nn.Linear(input_dim, 4 * hidden_dim, bias=False)
        self.h2h = nn.Linear(hidden_dim, 4 * hidden_dim, bias=True)
    
    def forward(self, x, hidden):
        """
        前向传播
        
        Args:
            x: 当前时刻输入,形状为 (batch, input_dim)
            hidden: (h_{t-1}, C_{t-1}),每个都是 (batch, hidden_dim)
        
        Returns:
            h_t, C_t: 更新后的隐藏状态和细胞状态
        """
        h_prev, C_prev = hidden
        
        # 合并计算所有门
        # gates形状: (batch, 4 * hidden_dim)
        gates = self.x2h(x) + self.h2h(h_prev)
        
        # 分割为四个部分: i(输入门), f(遗忘门), g(候选值), o(输出门)
        i_gate, f_gate, g_gate, o_gate = gates.chunk(4, dim=1)
        
        # 应用激活函数
        i_t = torch.sigmoid(i_gate)       # 输入门
        f_t = torch.sigmoid(f_gate)       # 遗忘门
        g_t = torch.tanh(g_gate)          # 候选细胞状态
        o_t = torch.sigmoid(o_gate)       # 输出门
        
        # 更新细胞状态
        C_t = f_t * C_prev + i_t * g_t
        
        # 更新隐藏状态
        h_t = o_t * torch.tanh(C_t)
        
        return h_t, C_t


class LSTMFromScratch(nn.Module):
    """多层LSTM网络(简化版,仅支持序列处理)"""
    def __init__(self, input_dim, hidden_dim, num_layers=1):
        super(LSTMFromScratch, self).__init__()
        self.num_layers = num_layers
        self.hidden_dim = hidden_dim
        
        # 第一层
        self.cells = nn.ModuleList()
        self.cells.append(LSTMCellFromScratch(input_dim, hidden_dim))
        
        # 后续层
        for _ in range(1, num_layers):
            self.cells.append(LSTMCellFromScratch(hidden_dim, hidden_dim))
    
    def forward(self, x, hidden=None):
        """
        处理整个序列
        
        Args:
            x: 输入序列,形状为 (batch, seq_len, input_dim)
            hidden: 初始隐藏状态(可选)
        
        Returns:
            output: 所有时刻的输出,形状为 (batch, seq_len, hidden_dim)
            (h_T, C_T): 最后时刻的隐藏状态和细胞状态
        """
        batch_size, seq_len, _ = x.shape
        
        if hidden is None:
            h = [torch.zeros(batch_size, self.hidden_dim) for _ in range(self.num_layers)]
            C = [torch.zeros(batch_size, self.hidden_dim) for _ in range(self.num_layers)]
        else:
            h, C = hidden
        
        outputs = []
        
        for t in range(seq_len):
            x_t = x[:, t, :]  # (batch, input_dim)
            
            for layer in range(self.num_layers):
                h[layer], C[layer] = self.cells[layer](x_t, (h[layer], C[layer]))
                x_t = h[layer]  # 下一层的输入是当前层的输出
            
            outputs.append(h[-1])  # 取最后一层的输出
        
        # 堆叠所有时刻的输出
        output = torch.stack(outputs, dim=1)  # (batch, seq_len, hidden_dim)
        
        return output, (h, C)


# 测试LSTM实现
def test_lstm_implementation():
    """测试自定义LSTM与PyTorch官方LSTM的输出对比"""
    torch.manual_seed(42)
    
    batch_size = 2
    seq_len = 3
    input_dim = 5
    hidden_dim = 8
    
    # 输入数据
    x = torch.randn(batch_size, seq_len, input_dim)
    
    # 自定义LSTM
    custom_lstm = LSTMFromScratch(input_dim, hidden_dim, num_layers=1)
    output_custom, (h_custom, c_custom) = custom_lstm(x)
    
    print(f"自定义LSTM输出形状: {output_custom.shape}")
    print(f"自定义LSTM最后时刻隐藏状态形状: {h_custom.shape}")
    
    # PyTorch官方LSTM
    official_lstm = nn.LSTM(input_dim, hidden_dim, num_layers=1, batch_first=True)
    output_official, (h_official, c_official) = official_lstm(x)
    
    print(f"\nPyTorch LSTM输出形状: {output_official.shape}")
    print("\n注: 由于权重初始化不同,输出数值不直接可比")
    print("     但门控机制和前向传播逻辑应完全一致")

test_lstm_implementation()

GRU(门控循环单元) :LSTM的简化版本,只有更新门重置门两个门,参数更少,计算更快,在某些任务上性能与LSTM相当。


1.3.3 注意力机制的诞生与演进

注意力机制的核心思想

传统的序列模型(如RNN/LSTM)将整个输入序列压缩为一个固定长度的向量 ,这成为性能瓶颈。注意力机制 (Attention Mechanism)允许模型在生成每个输出时,动态地关注输入序列的不同部分

类比理解:想象你在阅读一篇长文章并需要回答一个问题。你不会一字不差地记住整篇文章,而是在回答每个具体问题时,有针对性地"回顾"文章中最相关的段落。这就是注意力机制的核心思想。

Bahdanau注意力(Additive Attention)

最早的注意力机制由Bahdanau等人于2014年在机器翻译中提出,用于解决Seq2Seq模型的瓶颈问题。

计算步骤

  1. 计算注意力分数(Attention Scores)

    et,i=vaTtanh⁡(Wast−1+Uahi) e_{t,i} = v_a^T \tanh(W_a \mathbf{s}_{t-1} + U_a \mathbf{h}_i) et,i=vaTtanh(Wast−1+Uahi)

    其中 st−1\mathbf{s}_{t-1}st−1 为解码器上一时刻的隐藏状态,hi\mathbf{h}_ihi 为编码器第 iii 个时刻的隐藏状态。

  2. 归一化为注意力权重

    αt,i=exp⁡(et,i)∑j=1Txexp⁡(et,j) \alpha_{t,i} = \frac{\exp(e_{t,i})}{\sum_{j=1}^{T_x}\exp(e_{t,j})} αt,i=∑j=1Txexp(et,j)exp(et,i)

  3. 计算上下文向量(Context Vector)

    ct=∑i=1Txαt,ihi \mathbf{c}t = \sum{i=1}^{T_x} \alpha_{t,i} \mathbf{h}_i ct=i=1∑Txαt,ihi

  4. 与解码器输入结合

    st=f(st−1,yt−1,ct) \mathbf{s}t = f(\mathbf{s}{t-1}, \mathbf{y}_{t-1}, \mathbf{c}_t) st=f(st−1,yt−1,ct)

Luong注意力(Dot-Product Attention)

Luong等人于2015年提出了更简单的点积注意力

et,i=st−1Thi e_{t,i} = \mathbf{s}_{t-1}^T \mathbf{h}_i et,i=st−1Thi

计算效率更高,成为后续Transformer架构的基础。

从注意力到Self-Attention

Self-Attention(自注意力)是注意力机制的一个特殊应用------在同一个序列内部计算注意力权重,使得序列中的每个位置都能"关注"到其他所有位置。

这一思想直接催生了Transformer架构(将在第三章详细介绍)。

python 复制代码
# ===== 代码实战1.13:从零实现注意力机制 =====
import torch
import torch.nn as nn
import torch.nn.functional as F

class BahdanauAttention(nn.Module):
    """
    Bahdanau注意力(Additive Attention)实现
    用于Seq2Seq模型
    """
    def __init__(self, hidden_dim):
        """
        初始化注意力层
        
        Args:
            hidden_dim: 隐藏状态维度(编码器和解码器必须相同)
        """
        super(BahdanauAttention, self).__init__()
        self.W_a = nn.Linear(hidden_dim, hidden_dim)
        self.U_a = nn.Linear(hidden_dim, hidden_dim)
        self.v_a = nn.Linear(hidden_dim, 1)
    
    def forward(self, decoder_hidden, encoder_outputs):
        """
        计算注意力权重和上下文向量
        
        Args:
            decoder_hidden: 解码器上一时刻隐藏状态 (batch, hidden_dim)
            encoder_outputs: 编码器所有时刻输出 (batch, src_len, hidden_dim)
        
        Returns:
            context: 上下文向量 (batch, hidden_dim)
            attention_weights: 注意力权重 (batch, src_len)
        """
        batch_size = decoder_hidden.size(0)
        src_len = encoder_outputs.size(1)
        
        # 扩展解码器隐藏状态以匹配编码器输出长度
        # decoder_hidden: (batch, hidden_dim) -> (batch, src_len, hidden_dim)
        decoder_hidden_expanded = decoder_hidden.unsqueeze(1).repeat(1, src_len, 1)
        
        # 计算注意力分数 (Bahdanau公式)
        # W_a(h_i) + U_a(s_{t-1})
        scores = self.v_a(
            torch.tanh(
                self.W_a(encoder_outputs) + self.U_a(decoder_hidden_expanded)
            )
        )  # (batch, src_len, 1)
        scores = scores.squeeze(2)  # (batch, src_len)
        
        # 归一化为注意力权重
        attention_weights = F.softmax(scores, dim=1)  # (batch, src_len)
        
        # 计算上下文向量: sum_i α_i * h_i
        context = torch.bmm(
            attention_weights.unsqueeze(1),  # (batch, 1, src_len)
            encoder_outputs                     # (batch, src_len, hidden_dim)
        ).squeeze(1)  # (batch, hidden_dim)
        
        return context, attention_weights


class DotProductAttention(nn.Module):
    """
    点积注意力(Luong注意力)实现
    计算效率更高,是现代Transformer的基础
    """
    def __init__(self, hidden_dim):
        super(DotProductAttention, self).__init__()
        self.hidden_dim = hidden_dim
        # 可选的缩放因子(当hidden_dim较大时建议使用)
        self.scale = torch.sqrt(torch.tensor(hidden_dim, dtype=torch.float32))
    
    def forward(self, query, key, value, mask=None):
        """
        点积注意力前向传播
        
        Args:
            query: 查询向量 Q (batch, seq_len_q, hidden_dim)
            key: 键向量 K (batch, seq_len_k, hidden_dim)
            value: 值向量 V (batch, seq_len_k, hidden_dim)
            mask: 可选掩码 (batch, seq_len_q, seq_len_k)
        
        Returns:
            output: 注意力输出 (batch, seq_len_q, hidden_dim)
            attention_weights: 注意力权重 (batch, seq_len_q, seq_len_k)
        """
        # 计算注意力分数: Q * K^T / sqrt(d_k)
        scores = torch.bmm(query, key.transpose(1, 2)) / self.scale  # (batch, seq_len_q, seq_len_k)
        
        # 应用掩码(可选,用于padding或未来信息遮挡)
        if mask is not None:
            scores = scores.masked_fill(mask == 0, -1e9)
        
        # 归一化为注意力权重
        attention_weights = F.softmax(scores, dim=-1)  # (batch, seq_len_q, seq_len_k)
        
        # 加权求和
        output = torch.bmm(attention_weights, value)  # (batch, seq_len_q, hidden_dim)
        
        return output, attention_weights


def demonstrate_attention():
    """演示两种注意力机制的使用"""
    torch.manual_seed(42)
    
    batch_size = 2
    src_len = 5
    tgt_len = 3
    hidden_dim = 8
    
    # ===== Bahdanau注意力演示 =====
    print("=== Bahdanau注意力 (用于Seq2Seq) ===")
    bahdanau_attn = BahdanauAttention(hidden_dim)
    
    # 模拟解码器隐藏状态 (通常是RNN/LSTM的隐藏状态)
    decoder_hidden = torch.randn(batch_size, hidden_dim)
    # 模拟编码器输出 (所有时刻的隐藏状态)
    encoder_outputs = torch.randn(batch_size, src_len, hidden_dim)
    
    context, attn_weights = bahdanau_attn(decoder_hidden, encoder_outputs)
    print(f"上下文向量形状: {context.shape}")
    print(f"注意力权重形状: {attn_weights.shape}")
    print(f"注意力权重和: {attn_weights[0].sum().item():.4f} (应为1.0)")
    
    # ===== 点积注意力演示 =====
    print("\n=== 点积注意力 (Transformer基础) ===")
    dot_attn = DotProductAttention(hidden_dim)
    
    # 模拟Q, K, V (自注意力场景中它们来自同一输入)
    query = torch.randn(batch_size, tgt_len, hidden_dim)
    key = torch.randn(batch_size, src_len, hidden_dim)
    value = key.clone()  # 简化:K和V相同
    
    output, attn_weights = dot_attn(query, key, value)
    print(f"注意力输出形状: {output.shape}")
    print(f"注意力权重形状: {attn_weights.shape}")
    print(f"第一个样本的注意力权重:\n{attn_weights[0].detach().numpy()}")

demonstrate_attention()

1.3.4 神经网络在大模型时代的定位

从专用模型到基础模型(Foundation Models)
时代 代表技术 训练范式 特点
2012-2017 CNN、RNN、LSTM 监督学习(任务特定数据) 专用模型,泛化能力有限
2018-2019 BERT、GPT-1 预训练+微调 迁移学习,基础模型雏形
2020-2022 GPT-3、CLIP 大规模预训练+Prompt 少样本/零样本学习
2023-至今 GPT-4、LLaMA、Gemini 预训练+RLHF/DPO 多模态、推理能力、Agent
大模型时代的神经网络核心组件

现代大模型(如GPT-4、LLaMA)虽然在架构上仍基于Transformer,但在以下方面进行了重要改进:

  1. 位置编码的演进:从绝对位置编码(Sinusoidal)到相对位置编码(RoPE,Rotary Position Embedding)
  2. 归一化层的位置:Post-Norm → Pre-Norm(稳定训练)
  3. 激活函数的选择:ReLU → GELU → SwiGLU(门控激活)
  4. 注意力机制的优化:Multi-Head → Multi-Query → Grouped-Query Attention
  5. 模型规模的扩展:从BERT-base的1.1亿参数到GPT-4的万亿级参数

关键洞察:神经网络的基本原理(反向传播、梯度下降、注意力机制)仍然是大模型训练的基石。理解这些基础原理,是深入掌握大模型技术的前提。


企业级考量与避坑指南

避坑1:梯度消失与梯度爆炸问题

问题描述

在深层网络中,反向传播时梯度需要经过多层连乘。如果每层的梯度都小于1(如Sigmoid的导数最大值为0.25),经过多层连乘后梯度会趋近于0(梯度消失 );反之,如果梯度都大于1,则会指数级增长(梯度爆炸)。

解决方案

  1. 使用ReLU/GELU等现代激活函数(导数常为0或1,缓解梯度消失)

  2. 使用残差连接 (Residual Connection):y=f(x)+xy = f(x) + xy=f(x)+x,梯度可以直接通过skip connection传播

  3. 使用Batch/Layer Normalization:归一化层使得激活值保持在合理范围内

  4. 使用梯度裁剪 (Gradient Clipping):

    python 复制代码
    torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
  5. 使用LSTM/GRU替代简单RNN(处理序列任务时)

避坑2:权重初始化不当导致训练不稳定

问题描述

如果权重初始化过小,信号在向前传播时会逐渐缩小;如果权重初始化过大,激活值会饱和(如Sigmoid)或导致梯度爆炸。

解决方案

使用合适的初始化策略:

激活函数 推荐初始化方法 公式
Sigmoid/Tanh Xavier初始化 W∼U−6nin+nout,6nin+noutW \sim U-\\frac{\\sqrt{6}}{\\sqrt{n_{in}+n_{out}}}, \\frac{\\sqrt{6}}{\\sqrt{n_{in}+n_{out}}}W∼U−nin+nout 6 ,nin+nout 6
ReLU/Leaky ReLU Kaiming初始化 W∼N0,2ninW \sim N0, \\sqrt{\\frac{2}{n_{in}}}W∼N0,nin2
GELU Kaiming初始化(He初始化) 同上
python 复制代码
# PyTorch中的正确初始化方式
def init_weights(m):
    """递归初始化模型权重"""
    if isinstance(m, nn.Linear):
        # 对于使用ReLU/GELU的层,使用Kaiming初始化
        nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
        nn.init.constant_(m.bias, 0)
    elif isinstance(m, nn.Conv2d):
        nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
        if m.bias is not None:
            nn.init.constant_(m.bias, 0)
    elif isinstance(m, nn.LayerNorm):
        nn.init.constant_(m.weight, 1)
        nn.init.constant_(m.bias, 0)

model = MyModel()
model.apply(init_weights)  # 递归应用初始化

避坑3:Batch Size选择不当影响模型泛化

问题描述

  • Batch Size太小:梯度估计噪声大,训练不稳定,难以利用硬件加速
  • Batch Size太大:泛化性能下降("Large Batch Size泛化差距"现象),显存不足

经验法则

  1. 在显存允许的情况下,尽可能使用较大的Batch Size(充分利用GPU并行能力)
  2. 使用学习率warmup和衰减策略配合大Batch Size训练
  3. 当增加Batch Size时,等比例增加学习率 (Linear Scaling Rule):
    KaTeX parse error: Expected 'EOF', got '' at position 65: ...rac{\text{batch_̲size}{\text{ne...
  4. 如果Batch Size太大导致泛化性能下降 ,可以使用:
    • 标签平滑(Label Smoothing)
    • 知识蒸馏(Knowledge Distillation)
    • 随机深度(Stochastic Depth,用于极深网络)

本章小结

核心Takeaways

  1. 神经网络的基础单元是神经元,通过加权求和、加偏置、激活函数三步完成一次前向传播。多层神经元堆叠形成深度网络。

  2. 激活函数引入非线性,是现代神经网络表达复杂函数的关键。ReLU因其简单高效成为CNN的标准选择,GELU则因平滑特性成为Transformer的首选。

  3. 反向传播算法基于链式法则,从输出层向输入层反向计算梯度,是训练神经网络的核心。理解其数据流转过程对于调试模型至关重要。

  4. 优化算法的选择直接影响收敛速度和模型性能。AdamW因解耦了权重衰减,成为训练Transformer模型的标准优化器。

  5. 正则化技术防止过拟合:Dropout、L1/L2正则化、Batch/Layer Normalization是深度学习的标配技术,但需要理解其适用场景。

  6. 神经网络架构经历了从MLP到CNN、RNN/LSTM,再到注意力机制和Transformer的演进。每一代架构都在解决上一代的局限性,最终催生了大模型时代。

  7. 企业级深度学习需要注意多个工程细节:梯度稳定性、权重初始化、学习率调度、Batch Size选择等,这些"小细节"往往决定了项目成败。


思考题

思考题1:为什么Transformer架构选择GELU而非ReLU作为激活函数?

参考答案

GELU相比ReLU有以下优势:

  1. 平滑性:GELU是平滑曲线(处处可导),而ReLU在零点不可导。平滑的激活函数有助于梯度传播,提高训练稳定性。
  2. 自适应保留概率:GELU可以理解为"随机正则化的期望",输入值越大,保留概率越高,这种自适应特性有助于模型学习。
  3. 实验性能:在BERT、GPT等Transformer模型中,GELU consistently outperforms ReLU。
  4. 理论支持:GELU基于高斯分布的累积分布函数,有更坚实的概率论基础。
思考题2:在训练深层网络时,为什么残差连接(Residual Connection)能有效缓解梯度消失问题?请结合反向传播的链式法则进行分析。

参考答案

对于普通网络,反向传播时梯度需要经过每一层的权重矩阵连乘:

∂L∂x=∂L∂y⋅WT⋅f′(⋅) \frac{\partial \mathcal{L}}{\partial \mathbf{x}} = \frac{\partial \mathcal{L}}{\partial \mathbf{y}} \cdot W^T \cdot f'(\cdot) ∂x∂L=∂y∂L⋅WT⋅f′(⋅)

当网络很深时,连乘效应导致梯度趋近于0。

而对于残差连接 y=f(x)+xy = f(x) + xy=f(x)+x,反向传播时:

∂L∂x=∂L∂y⋅(∂f(x)∂x+I) \frac{\partial \mathcal{L}}{\partial \mathbf{x}} = \frac{\partial \mathcal{L}}{\partial \mathbf{y}} \cdot \left(\frac{\partial f(\mathbf{x})}{\partial \mathbf{x}} + I\right) ∂x∂L=∂y∂L⋅(∂x∂f(x)+I)

其中 III 为单位矩阵。这意味着梯度可以直接通过skip connection传播 (∂L∂y⋅I\frac{\partial \mathcal{L}}{\partial \mathbf{y}} \cdot I∂y∂L⋅I 项),不受权重矩阵和激活函数导数的影响,从而有效缓解梯度消失问题。这也是ResNet能够成功训练上千层网络的关键原因。



参考文献

  1. Goodfellow, I., Bengio, Y., & Courville, A. (2016). Deep Learning. MIT Press.
  2. LeCun, Y., Bengio, Y., & Hinton, G. (2015). Deep learning. Nature, 521(7553), 436-444.
  3. Vaswani, A., et al. (2017). Attention is all you need. NeurIPS 2017.
  4. He, K., et al. (2016). Deep residual learning for image recognition. CVPR 2016.
  5. Ioffe, S., & Szegedy, C. (2015). Batch normalization: Accelerating deep network training by reducing internal covariate shift. ICML 2015.
  6. Kingma, D. P., & Ba, J. (2015). Adam: A method for stochastic optimization. ICLR 2015.
  7. Loshchilov, I., & Hutter, F. (2019). Decoupled weight decay regularization. ICLR 2019. (AdamW)