机器学习数学基础专题【左扬精讲】—— 深度学习正则化技术详解:从 L1/L2 到参数共享

机器学习数学基础专题【左扬精讲】—— 深度学习正则化技术详解:从 L1/L2 到参数共享

在深度学习的实践中,过拟合(Overfitting)是一个几乎每个学习者都会遇到的问题。当模型在训练集上表现优异但在测试集上表现不佳时,就说明模型过拟合了。

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

本文将系统讲解深度学习中常用的正则化技术:L1 正则化、L2 正则化、数据增强、提前停止、批标准化、弹性网正则化、噪声注入和参数共享。这些技术各有特点,适用于不同的场景。

  torch.optim.SGD(weight_decay=...)      ← L2 正则化(weight decay)
  torch.nn.L1Loss                        ← L1 损失函数
  torchvision.transforms                 ← 数据增强
  torch.nn.Dropout                       ← Dropout 正则化
  torch.nn.BatchNorm2d                   ← 批标准化
  torch.nn.GaussianNoise                 ← 高斯噪声注入(需自定义)
  torch.nn.Parameter                     ← 可学习参数(参数共享基础)

PyTorch正则化L1正则化L2正则化数据增强Dropout批标准化

学习重点

  • 必须掌握
  • L1 正则化和 L2 正则化的数学原理与区别
  • PyTorch 中 weight_decay 参数的使用方法
  • 数据增强的常用方法:随机裁剪、翻转、旋转、颜色变换
  • Dropout 的训练/推理行为差异及其正则化原理
  • 理解即可
  • 批标准化的均值方差统计与移动平均
  • 参数共享的实现方式和应用场景

目录


一、L1 与 L2 正则化

What — L1 和 L2 正则化是什么?

L1 正则化和 L2 正则化是最基础也是最常用的两种参数正则化方法。它们通过在损失函数中添加一个正则项来约束模型的参数,从而防止过拟合。

  • L1 正则化(Lasso):在损失函数中加入参数绝对值之和,产生稀疏权重
  • L2 正则化(Ridge/Weight Decay):在损失函数中加入参数平方和,使权重趋近于零但不会完全为零

正则化后的损失函数形式为:

# 总损失 = 原始损失 + 正则项
  # L1 正则化: L_total = L_original + λ * Σ|w_i|
  # L2 正则化: L_total = L_original + λ * Σ(w_i^2)

Why — 为什么需要 L1 和 L2 正则化?

问题一:过拟合是如何产生的? 当模型参数过多、训练数据不足时,模型会学习到训练数据中的噪声和细节,而不是数据的一般规律。正则化通过惩罚大的参数值,约束模型的复杂度。

问题二:L1 和 L2 正则化有什么区别?

  • L1 正则化:产生稀疏权重,可以用于特征选择(将不重要特征的权重置零)
  • L2 正则化:使权重平滑分布,不会完全置零,适合大多数场景

问题三:weight_decay 参数是什么?

在 PyTorch 的优化器中,weight_decay 参数实现的就是 L2 正则化。它通过在梯度更新时额外减去 weight_decay * parameters 来实现权重衰减。

没有正则化会发生什么?

  • 模型在训练集上表现很好,但在测试集上表现差(过拟合)
  • 权重值可能变得非常大,导致数值不稳定
  • 模型可能学习到训练数据中的噪声
标准写法:PyTorch 中的 L2 正则化(weight_decay)

在 PyTorch 优化器中,L2 正则化通过 weight_decay 参数实现:

  import torch
  import torch.nn as nn
  import torch.optim as optim
  
  # 创建模型
  model = nn.Linear(10, 5)
  
  # 方法1:使用 weight_decay 参数(L2 正则化)
  # weight_decay 的值通常在 1e-4 到 1e-2 之间
  optimizer = optim.SGD(model.parameters(), lr=0.01, weight_decay=1e-4)
  # 或使用 Adam 优化器
  optimizer = optim.Adam(model.parameters(), lr=0.001, weight_decay=1e-4)
  
  # weight_decay=0.01 表示 L2 正则化系数 λ = 0.01
  # 每次参数更新时,会额外执行:parameters = parameters - lr * (gradient + λ * parameters)
  # 等价于:parameters = (1 - lr * λ) * parameters - lr * gradient
标准写法:L1 正则化的实现

L1 正则化在 PyTorch 中没有直接的 API,需要手动添加到损失函数中:

def l1_regularization(model, l1_lambda=0.001):
      """计算 L1 正则化项"""
      l1_loss = 0.0
      for param in model.parameters():
          # 计算参数绝对值之和
          l1_loss += torch.sum(torch.abs(param))
      return l1_lambda * l1_loss
  
  # 在训练循环中使用
  for data, target in dataloader:
      optimizer.zero_grad()
      output = model(data)
      
      # 计算原始损失(如交叉熵损失)
      ce_loss = nn.CrossEntropyLoss()(output, target)
      
      # 计算 L1 正则化损失
      regularization_loss = l1_regularization(model, l1_lambda=0.001)
      
      # 总损失 = 原始损失 + L1 正则化项
      total_loss = ce_loss + regularization_loss
      
      total_loss.backward()
      optimizer.step()

L1 和 L2 正则化的对比

  • 稀疏性:L1 可以产生零权重(特征选择),L2 权重趋近于零但不会完全为零
  • 解的稳定性:L2 的解更稳定,L1 在高度相关的特征上可能不稳定
  • 计算效率:L2 有解析解,计算高效;L1 的优化问题需要次梯度方法

本章小结

  • L1 正则化:产生稀疏权重,可用于特征选择
  • L2 正则化:使权重平滑,是 PyTorch 中的 weight_decay
  • PyTorch APIoptim.SGD(weight_decay=λ) 实现 L2 正则化
  • L1 实现:需手动计算 Σ|w| 并添加到损失函数

二、数据增强

What — 数据增强是什么?

数据增强(Data Augmentation)是一种通过人为扩充训练数据来提高模型泛化能力的技术。它通过对原始训练数据应用一系列随机变换,生成新的训练样本。

常用的图像数据增强方法:

  • 几何变换:随机裁剪、翻转、旋转、缩放
  • 颜色变换:亮度调整、对比度调整、颜色抖动
  • 噪声注入:添加高斯噪声
  • 混合增强:MixUp、CutMix 等

Why — 为什么数据增强有效?

问题一:深度学习需要大量数据。深度神经网络参数量巨大,通常需要数万甚至数百万的训练样本。数据增强可以在不收集新数据的情况下扩充训练集。

问题二:增强后的数据是否有效?。如果变换后的图像仍然是"有效"的(即属于同一类别),那么增强后的样本可以提供额外的训练信号。例如,将猫的图片水平翻转后,仍然是一只猫。

问题三:数据增强是否适用于所有任务?。对于图像分类、目标检测等任务,数据增强非常有效。但对于需要精确位置信息的任务(如关键点检测),需要谨慎使用裁剪等变换。

没有数据增强会发生什么?

  • 训练数据不足时,模型容易过拟合
  • 模型对数据变化的鲁棒性差
  • 泛化能力受限
标准写法:torchvision.transforms 数据增强

PyTorch 的 torchvision.transforms 提供了丰富的数据增强功能:

import torchvision.transforms as transforms
  from torchvision import datasets
  
  # 定义训练数据的数据增强管道
  train_transform = transforms.Compose([
      # 1. 随机水平翻转(50%概率)
      transforms.RandomHorizontalFlip(p=0.5),
      # 2. 随机垂直翻转(可选,用于某些场景)
      transforms.RandomVerticalFlip(p=0.2),
      # 3. 随机旋转(-15度到+15度)
      transforms.RandomRotation(degrees=15),
      # 4. 随机仿射变换(包含平移、旋转、缩放)
      transforms.RandomAffine(degrees=0, translate=(0.1, 0.1), scale=(0.9, 1.1)),
      # 5. 随机裁剪并调整到原始尺寸
      transforms.RandomResizedCrop(size=224, scale=(0.8, 1.0)),
      # 6. 颜色抖动(亮度、对比度、饱和度、色调)
      transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2, hue=0.1),
      # 7. 转换为张量
      transforms.ToTensor(),
      # 8. 标准化
      transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
  ])
  
  # 测试数据只做基础变换(不增强)
  test_transform = transforms.Compose([
      transforms.Resize(256),
      transforms.CenterCrop(224),
      transforms.ToTensor(),
      transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
  ])
  
  # 应用到数据集
  train_dataset = datasets.CIFAR10(root='./data', train=True, transform=train_transform)
  test_dataset = datasets.CIFAR10(root='./data', train=False, transform=test_transform)
高级数据增强:MixUp 和 CutMix

MixUp 和 CutMix 是两种更高级的数据增强方法:

def mixup_data(x, y, alpha=1.0):
      """MixUp 数据增强
      
      MixUp 通过线性插值混合两个样本及其标签来创建新的训练样本。
      """
      if alpha > 0:
          lam = torch.distributions.beta.Beta(alpha, alpha).sample()
      else:
          lam = 1.0
      
      batch_size = x.size(0)
      index = torch.randperm(batch_size).to(x.device)
      
      # 混合图像
      mixed_x = lam * x + (1 - lam) * x[index]
      
      # 混合标签(one-hot 编码)
      y_a, y_b = y, y[index]
      
      return mixed_x, y_a, y_b, lam
  
  def mixup_criterion(criterion, pred, y_a, y_b, lam):
      """MixUp 损失的加权计算"""
      return lam * criterion(pred, y_a) + (1 - lam) * criterion(pred, y_b)
  
  # 在训练循环中使用 MixUp
  for batch_idx, (images, labels) in enumerate(dataloader):
      images, labels = images.to(device), labels.to(device)
      
      # MixUp 增强
      images, labels_a, labels_b, lam = mixup_data(images, labels, alpha=0.2)
      
      optimizer.zero_grad()
      outputs = model(images)
      
      # 使用混合损失
      loss = mixup_criterion(criterion, outputs, labels_a, labels_b, lam)
      loss.backward()
      optimizer.step()

本章小结

  • 数据增强:通过变换扩充训练数据,提高泛化能力
  • 几何变换:翻转、旋转、裁剪、缩放
  • 颜色变换:亮度、对比度、饱和度调整
  • 高级方法:MixUp、CutMix 等混合增强技术

三、提前停止

What — 提前停止是什么?

提前停止(Early Stopping)是一种简单而有效的正则化技术。它通过监控模型在验证集上的性能,在模型开始过拟合之前停止训练。

提前停止的核心思想:

  • 随着训练进行,验证集损失会先下降后上升(过拟合)
  • 我们保存验证集损失最低时的模型参数
  • 当验证集损失连续多次不再改善时,停止训练

Why — 为什么要提前停止?

问题一:训练 loss 持续下降是否意味着模型在变好?。不一定。训练 loss 下降只说明模型在训练集上拟合得更好,但如果验证集 loss 开始上升,说明模型开始过拟合了。

问题二:为什么不等训练完全收敛再停止?。如果训练过度,模型会记忆训练数据的噪声和细节,导致泛化能力下降。提前停止可以在过拟合开始前停止,找到泛化能力和训练程度的平衡点。

问题三:patience 参数是什么?。patience 表示"耐心"程度,即验证集 loss 连续多少个 epoch 没有改善后才停止训练。较大的 patience 可以容忍短期波动,但可能导致过度训练。

没有提前停止会发生什么?

  • 模型可能过度训练,泛化能力下降
  • 需要手动监控训练过程,容易错过最佳停止点
  • 训练时间可能不必要地延长
标准写法:PyTorch 实现 EarlyStopping
class EarlyStopping:
      """提前停止回调类"""
      def __init__(self, patience=10, min_delta=0.0001, verbose=True):
          """
          Args:
              patience: 验证损失连续多少个 epoch 没有改善则停止
              min_delta: 被认为是改善的最小变化量
              verbose: 是否打印信息
          """
          self.patience = patience
          self.min_delta = min_delta
          self.verbose = verbose
          self.counter = 0  # 未改善的 epoch 计数
          self.best_loss = None
          self.early_stop = False
          self.best_model_state = None
      
      def __call__(self, val_loss, model):
          """在每个 epoch 后调用"""
          if self.best_loss is None:
              # 第一个 epoch,保存最佳状态
              self.best_loss = val_loss
              self.best_model_state = model.state_dict().copy()
              if self.verbose:
                  print(f"初始验证损失: {val_loss:.6f}")
          
          elif val_loss < self.best_loss - self.min_delta:
              # 验证损失有改善
              self.best_loss = val_loss
              self.best_model_state = model.state_dict().copy()
              self.counter = 0  # 重置计数器
              if self.verbose:
                  print(f"验证损失改善: {val_loss:.6f}")
          
          else:
              # 验证损失没有改善
              self.counter += 1
              if self.verbose:
                  print(f"验证损失未改善: {self.counter}/{self.patience}")
              
              if self.counter >= self.patience:
                  self.early_stop = True
                  if self.verbose:
                      print("触发提前停止!恢复最佳模型参数。")
                  # 恢复最佳模型
                  model.load_state_dict(self.best_model_state)
  
  # 使用提前停止
  early_stopping = EarlyStopping(patience=10, min_delta=0.001)
  
  for epoch in range(num_epochs):
      # 训练
      train_loss = train_epoch(model, train_loader, criterion, optimizer)
      # 验证
      val_loss = validate(model, val_loader, criterion)
      
      # 检查是否提前停止
      early_stopping(val_loss, model)
      
      if early_stopping.early_stop:
          print(f"在第 {epoch+1} 个 epoch 提前停止")
          break

本章小结

  • 提前停止:监控验证集损失,在过拟合前停止训练
  • patience:连续多少个 epoch 无改善则停止
  • min_delta:被认定为改善的最小变化量
  • 最佳模型:保存验证集表现最好的模型参数

四、批标准化

What — 批标准化是什么?

批标准化(Batch Normalization,简称 BatchNorm)是一种通过对每一层的输入进行标准化来处理神经网络训练不稳定问题的技术。它在 2015 年由 Google 研究人员提出,是深度学习领域最重要的技术突破之一。

批标准化的核心操作:

# 对于一个 batch 的数据 (batch_size, features)
  # 计算均值: μ_B = (1/m) Σ x_i
  # 计算方差: σ_B² = (1/m) Σ (x_i - μ_B)²
  # 标准化: x̂_i = (x_i - μ_B) / √(σ_B² + ε)
  # 线性变换: y_i = γ * x̂_i + β
  # 其中 γ 和 β 是可学习的参数

Why — 为什么批标准化有效?

问题一:什么是内部协变量偏移(Internal Covariate Shift)?。在训练过程中,由于前一层参数的更新,导致后一层的输入分布不断变化。这使得每一层都需要不断适应新的输入分布,学习变得困难。BatchNorm 通过标准化使每一层的输入分布保持稳定。

问题二:为什么需要 γ 和 β 参数?。如果简单地标准化到均值 0、方差 1,可能会改变该层能表示的信号。例如,Sigmoid 激活函数在均值 0 附近几乎是线性的,标准化可能限制网络的表达能力。γ 和 β 允许网络学习恢复它需要的数据分布。

问题三:BatchNorm 在训练和推理时行为有何不同?。训练时,BatchNorm 使用当前 batch 的均值和方差进行标准化。推理时,使用训练阶段统计的移动平均均值和方差。

没有 BatchNorm 会发生什么?

  • 深层网络训练困难,需要小心初始化学习率
  • 梯度消失/爆炸问题更严重
  • 训练不稳定,可能发散
标准写法:PyTorch BatchNorm2d 的使用
import torch
  import torch.nn as nn
  
  # BatchNorm2d 用于 2D 特征图(卷积层输出)
  # 输入: (batch_size, num_features, height, width)
  # num_features 通常等于通道数
  
  class CNNWithBatchNorm(nn.Module):
      def __init__(self):
          super(CNNWithBatchNorm, self).__init__()
          # 卷积层
          self.conv1 = nn.Conv2d(3, 64, kernel_size=3, padding=1)
          # BatchNorm2d:num_features = 64
          self.bn1 = nn.BatchNorm2d(64)
          self.relu = nn.ReLU()
          
          self.conv2 = nn.Conv2d(64, 128, kernel_size=3, padding=1)
          self.bn2 = nn.BatchNorm2d(128)
          
          # 自适应平均池化
          self.pool = nn.AdaptiveAvgPool2d((1, 1))
          self.fc = nn.Linear(128, 10)
      
      def forward(self, x):
          # Conv -> BN -> ReLU
          x = self.conv1(x)
          x = self.bn1(x)    # 标准化到均值0、方差1,然后线性变换
          x = self.relu(x)
          
          x = self.conv2(x)
          x = self.bn2(x)
          x = self.relu(x)
          
          x = self.pool(x)
          x = x.view(x.size(0), -1)
          x = self.fc(x)
          return x
  
  # BatchNorm1d 用于全连接层
  # 输入: (batch_size, num_features)
  linear_bn = nn.Sequential(
      nn.Linear(256, 128),
      nn.BatchNorm1d(128),  # 标准化特征维度
      nn.ReLU()
  )
BatchNorm 的重要参数和行为
# BatchNorm2d 的主要参数
  bn = nn.BatchNorm2d(
      num_features=64,       # 通道数
      eps=1e-05,            # 防止除零的小常数,默认 1e-5
      momentum=0.1,         # 移动平均的动量,默认 0.1
      affine=True,           # 是否使用可学习的 γ 和 β,默认 True
      track_running_stats=True  # 是否跟踪运行统计量(用于推理),默认 True
  )
  
  # 重要:设置模型模式
  model.train()  # 训练模式:使用当前 batch 的统计量
  model.eval()   # 推理模式:使用运行平均的统计量
  
  # 在训练和推理切换时,BatchNorm 的行为会自动变化
  # 这就是为什么推理前需要调用 model.eval()

BatchNorm 的变体

  • LayerNorm:对单个样本的所有特征标准化,适用于 NLP 任务
  • InstanceNorm:对每个样本的每个通道独立标准化,适用于风格迁移
  • GroupNorm:将通道分组后再标准化,不依赖 batch size

本章小结

  • 批标准化:对层的输入进行标准化,稳定训练
  • γ 和 β:可学习的缩放和偏移参数
  • 训练/推理:使用当前 batch 统计量 vs 运行平均统计量
  • PyTorch APInn.BatchNorm2d(2D)、nn.BatchNorm1d(1D)

五、弹性网正则化

What — 弹性网正则化是什么?

弹性网正则化(Elastic Net Regularization)是 L1 正则化和 L2 正则化的组合,同时具有两种正则化的优点。它在损失函数中加入 L1 和 L2 正则项的加权组合。

# 弹性网正则化的损失函数
  # L_total = L_original + λ1 * Σ|w_i| + λ2 * Σ(w_i^2)
  
  # 等价于同时使用 L1 和 L2 正则化
  # 可以通过设置不同的 λ1 和 λ2 来控制两者的相对强度

Why — 为什么要使用弹性网?

问题一:L1 和 L2 各有优缺点,弹性网如何结合两者?。L1 产生稀疏权重但可能在特征高度相关时不稳定;L2 稳定但不会产生零权重。弹性网同时使用两者,既能进行特征选择,又能保持稳定性。

问题二:什么情况下弹性网比单独的 L1 或 L2 更好?。当特征数量远大于样本数量,或特征之间存在高度相关性时,弹性网通常表现更好。

问题三:如何选择 λ1 和 λ2?。通常通过验证集或交叉验证来选择最佳参数组合。实践中,可以先固定一个比例(如 λ1/(λ1+λ2) = 0.5),然后搜索 λ1+λ2 的值。

标准写法:PyTorch 实现弹性网正则化
def elastic_net_regularization(model, l1_lambda=0.001, l2_lambda=0.001):
      """计算弹性网正则化项(L1 + L2)"""
      l1_loss = 0.0
      l2_loss = 0.0
      
      for param in model.parameters():
          # L1 正则化:参数绝对值之和
          l1_loss += torch.sum(torch.abs(param))
          # L2 正则化:参数平方和
          l2_loss += torch.sum(param ** 2)
      
      return l1_lambda * l1_loss + l2_lambda * l2_loss
  
  # 在训练循环中使用
  for data, target in dataloader:
      optimizer.zero_grad()
      output = model(data)
      
      # 原始损失
      ce_loss = nn.CrossEntropyLoss()(output, target)
      
      # 弹性网正则化
      l1_lambda = 0.001  # L1 正则化系数
      l2_lambda = 0.001  # L2 正则化系数(也可以用 weight_decay)
      reg_loss = elastic_net_regularization(model, l1_lambda, l2_lambda)
      
      # 总损失
      total_loss = ce_loss + reg_loss
      
      total_loss.backward()
      optimizer.step()
  
  # 另一种方式:L2 部分用 weight_decay,L1 部分手动添加
  optimizer = optim.Adam(model.parameters(), lr=0.001, weight_decay=l2_lambda)
  
  for data, target in dataloader:
      optimizer.zero_grad()
      output = model(data)
      loss = nn.CrossEntropyLoss()(output, target)
      
      # 只添加 L1 正则化
      l1_loss = sum(torch.sum(torch.abs(p)) for p in model.parameters())
      loss = loss + l1_lambda * l1_loss
      
      loss.backward()
      optimizer.step()

本章小结

  • 弹性网:L1 + L2 正则化的组合
  • 优点:兼具稀疏性和稳定性
  • 适用场景:特征高度相关或特征数大于样本数的场景

六、噪声注入

What — 噪声注入是什么?

噪声注入(Noise Injection)是一种通过向输入数据或模型参数添加随机噪声来提高模型鲁棒性的正则化技术。噪声的存在迫使模型学习更平滑的决策边界,从而提高泛化能力。

常见的噪声注入方式:

  • 输入噪声:向输入数据添加高斯噪声或椒盐噪声
  • 权重噪声:向模型权重添加噪声
  • Dropout:随机丢弃神经元(相当于对激活添加噪声)
  • 标签噪声:以一定概率翻转标签

Why — 噪声注入为什么有效?

问题一:向数据添加噪声如何帮助学习?。如果训练数据在某个点附近有噪声,模型被迫学习一个在该点附近变化不大的函数。这相当于隐式地约束了模型的 Lipschitz 常数,使模型更平滑。

问题二:Dropout 和噪声注入有什么关系?。Dropout 可以看作是对激活值的乘法噪声。具体来说,被丢弃的神经元输出为 0,未丢弃的神经元输出乘以 1/(1-p),其中 p 是丢弃率。

问题三:什么类型的噪声最常用?。高斯噪声是最常用的,因为它在数学上易于处理,且中心极限定理保证了大量小噪声的叠加趋近于高斯分布。

没有噪声注入会发生什么?

  • 模型可能对训练数据的微小变化过于敏感
  • 决策边界可能过于复杂,不够平滑
  • 对对抗样本的鲁棒性较差
标准写法:Dropout 正则化

Dropout 是最常用的噪声注入技术:

import torch.nn.functional as F
  
  class DropoutModel(nn.Module):
      def __init__(self):
          super(DropoutModel, self).__init__()
          self.fc1 = nn.Linear(784, 256)
          self.dropout1 = nn.Dropout(p=0.2)  # 丢弃率 20%
          self.fc2 = nn.Linear(256, 128)
          self.dropout2 = nn.Dropout(p=0.3)  # 丢弃率 30%
          self.fc3 = nn.Linear(128, 10)
      
      def forward(self, x):
          x = F.relu(self.fc1(x))
          x = self.dropout1(x)  # 训练时随机丢弃部分神经元
          
          x = F.relu(self.fc2(x))
          x = self.dropout2(x)
          
          x = self.fc3(x)  # 输出层通常不使用 Dropout
          return x
  
  # 使用时需要注意模式切换
  model = DropoutModel()
  
  # 训练模式
  model.train()  # Dropout 生效
  for data, target in dataloader:
      output = model(data)  # Dropout 在前向传播时随机丢弃
  
  # 推理模式
  model.eval()   # Dropout 不生效,所有神经元参与计算
  with torch.no_grad():
      output = model(data)  # 所有神经元参与
标准写法:高斯噪声注入
def add_gaussian_noise(x, mean=0.0, std=0.1):
      """向输入添加高斯噪声"""
      noise = torch.randn_like(x) * std + mean
      return x + noise
  
  class GaussianNoiseLayer(nn.Module):
      """可学习的高斯噪声层"""
      def __init__(self, mean=0.0, std=0.1):
          super(GaussianNoiseLayer, self).__init__()
          # 将均值和标准差设为可学习参数
          self.mean = nn.Parameter(torch.tensor(mean))
          self.std = nn.Parameter(torch.tensor(std))
      
      def forward(self, x):
          if self.training:  # 只在训练时添加噪声
              noise = torch.randn_like(x) * self.std + self.mean
              return x + noise
          return x
  
  # 在模型中使用
  class NoisyCNN(nn.Module):
      def __init__(self):
          super(NoisyCNN, self).__init__()
          self.noise = GaussianNoiseLayer(mean=0.0, std=0.1)
          self.conv1 = nn.Conv2d(3, 64, 3, padding=1)
          self.bn1 = nn.BatchNorm2d(64)
          # ... 其他层
      
      def forward(self, x):
          x = self.noise(x)  # 添加噪声
          x = F.relu(self.bn1(self.conv1(x)))
          return x

本章小结

  • Dropout:随机丢弃神经元,相当于对激活添加乘法噪声
  • 高斯噪声:向输入或权重添加加法噪声
  • 训练/推理:Dropout 只在训练时生效,推理时需要 model.eval()
  • PyTorch API:nn.Dropoutnn.Dropout2dnn.AlphaDropout

七、参数共享

What — 参数共享是什么?

参数共享(Parameter Sharing)是一种通过让多个神经元或层使用相同参数来减少模型参数量和过拟合风险的技术。CNN 中的卷积核权重共享就是最典型的例子。

参数共享的常见形式:

  • 卷积权重共享:同一卷积核在整张图像上滑动,共享权重
  • Siamese 网络:两个网络共享权重,应用于度量学习
  • 多任务学习:不同任务共享底层特征提取器
  • RNN 时间步共享:RNN 在不同时间步使用相同的循环权重

Why — 参数共享为什么有效?

问题一:参数共享如何减少过拟合?。如果某个特征在图像的不同位置都是重要的,使用共享参数意味着只需要学习一次这个特征,而不是为每个位置单独学习。这大大减少了需要学习的参数数量,从而减少过拟合风险。

问题二:什么时候参数共享是合理的?。当不同位置或不同上下文需要学习相同的模式时,参数共享是合理的。例如,边缘检测器在图像的左上角和右下角应该是一样的。

问题三:完全共享和部分共享有什么区别?。完全共享意味着所有位置使用完全相同的参数。部分共享(如 MoE 专家混合)允许某些参数在不同位置有不同值,只共享部分参数。

没有参数共享会发生什么?

  • 参数量巨大,训练困难
  • 模型可能过拟合到每个位置的特殊性,而忽略通用模式
  • 泛化能力差,对位置变化敏感
标准写法:PyTorch 中的参数共享
import torch.nn as nn
  
  # 方法1:使用 torch.nn.Parameter 显式共享参数
  class SharedLayerModel(nn.Module):
      def __init__(self):
          super(SharedLayerModel, self).__init__()
          # 创建一个共享参数
          self.shared_weight = nn.Parameter(torch.randn(128, 256))
          
          # 多个层使用同一个参数
          self.fc1 = nn.Linear(256, 128)
          self.fc2 = nn.Linear(256, 128)
          self.fc3 = nn.Linear(256, 128)
      
      def forward(self, x1, x2, x3):
          # 不同输入使用共享权重
          out1 = torch.matmul(x1, self.shared_weight)
          out2 = torch.matmul(x2, self.shared_weight)
          out3 = torch.matmul(x3, self.shared_weight)
          
          out1 = self.fc1(out1)
          out2 = self.fc2(out2)
          out3 = self.fc3(out3)
          
          return out1, out2, out3
  
  # 方法2:使用 register_buffer 共享非可学习参数
  class BufferSharingModel(nn.Module):
      def __init__(self):
          super(BufferSharingModel, self).__init__()
          # 注册一个共享的缓冲区(不参与梯度计算)
          self.register_buffer('shared_mean', torch.zeros(256))
          self.register_buffer('shared_std', torch.ones(256))
          
          self.layer1 = nn.Linear(256, 128)
          self.layer2 = nn.Linear(256, 128)
      
      def forward(self, x1, x2):
          # 两个输入共享统计量
          out1 = (x1 - self.shared_mean) / (self.shared_std + 1e-8)
          out2 = (x2 - self.shared_mean) / (self.shared_std + 1e-8)
          
          return self.layer1(out1), self.layer2(out2)
  
  # 方法3:使用 nn.Sequential 共享层
  shared_layers = nn.Sequential(
      nn.Linear(256, 128),
      nn.ReLU(),
      nn.Linear(128, 64)
  )
  
  class SequentialSharingModel(nn.Module):
      def __init__(self):
          super(SequentialSharingModel, self).__init__()
          # 多个地方使用同一个序列层
          self.branch1 = shared_layers
          self.branch2 = shared_layers
          self.branch3 = shared_layers
      
      def forward(self, x1, x2, x3):
          return self.branch1(x1), self.branch2(x2), self.branch3(x3)
应用场景:Siamese 网络
class SiameseNetwork(nn.Module):
      """Siamese 网络:两个分支共享权重"""
      def __init__(self):
          super(SiameseNetwork, self).__init__()
          # 共享的特征提取器
          self.feature_extractor = nn.Sequential(
              nn.Conv2d(1, 64, 3),
              nn.ReLU(),
              nn.MaxPool2d(2),
              nn.Conv2d(64, 128, 3),
              nn.ReLU(),
              nn.AdaptiveAvgPool2d(1)
          )
          
          # 共享的全连接层
          self.fc = nn.Sequential(
              nn.Linear(128, 64),
              nn.ReLU(),
              nn.Linear(64, 1)
          )
      
      def forward_once(self, x):
          """对单个输入提取特征"""
          features = self.feature_extractor(x)
          features = features.view(features.size(0), -1)
          output = self.fc(features)
          return output
      
      def forward(self, x1, x2):
          """计算两个输入的相似度"""
          out1 = self.forward_once(x1)
          out2 = self.forward_once(x2)
          # 计算相似度(可以使用各种度量方式)
          distance = torch.abs(out1 - out2)
          similarity = self.fc(distance)
          return similarity

本章小结

  • 参数共享:多个位置使用相同参数,减少参数量
  • CNN 权重共享:卷积核在整张图像上滑动
  • PyTorch 实现nn.Parameterregister_buffer、共享 nn.Module
  • 应用场景:Siamese 网络、多任务学习、迁移学习

八、FAQ(20 组)

FAQ — 精选 20 问,深入理解正则化技术

Q1. L1 正则化和 L2 正则化的核心区别是什么?

L1 正则化产生稀疏权重(可置零),L2 正则化使权重趋近于零但不会完全为零。L1 的正则项是参数的绝对值之和,梯度是常数;L2 的正则项是参数的平方和,梯度与参数值成正比。L1 可以用于特征选择,L2 计算更稳定。

Q2. weight_decay 在 PyTorch 中是如何实现的?

weight_decay 在参数更新时额外减去 lr * weight_decay * parameters。对于 SGD,这等价于 L2 正则化。但对于 Adam 优化器,由于动量等机制,weight_decay 的行为与纯 L2 正则化略有不同。AdamW 是权重衰减与 Adam 的正确解耦实现。

Q3. Dropout 在训练和推理时的行为有何不同?

训练时随机丢弃神经元(输出置零),推理时所有神经元参与但输出要缩放。PyTorch 中通过 model.train()model.eval() 切换模式。默认情况下,推理时 Dropout 层会跳过,但输出不会额外缩放。

Q4. BatchNorm 的 running_mean 和 running_var 是如何计算的?

使用指数移动平均(EMA)累积统计量。每个 batch 计算均值 μ 和方差 σ² 后,用 running_mean = (1-momentum) * running_mean + momentum * μ 更新。默认 momentum=0.1,表示新 batch 占 10% 权重。

Q5. 为什么 BatchNorm 在推理时不需要 batch?

因为推理时使用预计算的 running_mean 和 running_var。训练时每个 batch 的均值方差可能波动较大,推理时用整个训练集统计的移动平均值更稳定。这就是为什么推理前要调用 model.eval()

Q6. 数据增强是否在所有任务上都有效?

不是,增强后的样本必须保持正确的标签语义。对于图像分类,水平翻转通常安全,但旋转可能对某些数字(如 6 和 9)造成混淆。对于目标检测,需要保证目标在变换后仍被完整保留在图像中。

Q7. 提前停止的 patience 如何选择?

patience 应略大于验证损失正常波动的周期。如果训练曲线波动较大,patience 应设大一些(如 15-20)。如果训练稳定,patience 可以小一些(如 5-10)。过大的 patience 可能导致过度训练,过小的 patience 可能提前停止。

Q8. L1 正则化为什么能产生稀疏解?

L1 正则项的次梯度是常数,会将参数推向零。当参数接近零时,梯度仍然是非零常数(符号函数),参数会继续减小直到零。而 L2 的梯度与参数值成正比,参数越小梯度也越小,最终只会趋近于零而不会完全为零。

Q9. Dropout 和 BatchNorm 可以同时使用吗?

可以,但 BatchNorm 放在 Dropout 之后效果通常更好。研究表明,如果 Dropout 在 BatchNorm 之前,它会改变 batch 的均值和方差统计,影响 BatchNorm 的标准化效果。实践中建议 BN -> Activation -> Dropout 的顺序。

Q10. 噪声注入的噪声强度如何选择?

噪声强度需要根据任务和数据规模调整,通常通过验证集选择。过大的噪声会淹没真实信号,过小的噪声正则化效果不明显。一般从较小的值(如 std=0.01)开始尝试,根据验证集性能调整。

Q11. Elastic Net 相比单独 L1 或 L2 的优势是什么?

Elastic Net 兼具特征选择和稳定性,在特征高度相关时表现更好。当特征存在高度相关性时,L1 可能随机选择其中一个,Elastic Net 通过 L2 项提供稳定性,同时保留 L1 的稀疏性。

Q12. 为什么 BatchNorm 需要 affine=True?

affine=True 允许网络学习恢复输入的原始分布,保持表达能力。如果不使用 affine 参数,BatchNorm 只能将数据标准化到固定分布,可能限制网络能表示的函数类型。γ 和 β 参数让网络决定是否需要这种标准化。

Q13. 什么是谱归一化( Spectral Normalization)?

谱归一化通过限制权重矩阵的谱范数为 1 来约束网络的 Lipschitz 常数。它主要用于 GAN 的判别器训练,防止判别器过强导致训练不稳定。PyTorch 中有 torch.nn.utils.spectral_norm 实现。

Q14. 数据增强的"在線"和"离线"有什么区别?

在线增强在训练时实时变换,离线增强预先生成增强后的数据集。在线增强不占用额外存储空间,但训练时计算开销大。离线增强训练时更快,但占用更多存储空间。

Q15. 标签平滑(Label Smoothing)是一种正则化吗?

是的,标签平滑通过软化标签分布来防止模型过度自信。将硬标签(如 0 和 1)替换为软标签(如 0.1 和 0.9),减少模型对训练标签的完全依赖,提高泛化能力。交叉熵损失函数可以直接使用标签平滑。

Q16. Dropout 的丢弃率 p 如何选择?

丢弃率通常在 0.1 到 0.5 之间,过大的丢弃率可能导致欠拟合。对于大型网络或强正则化需求,可以用较大的 p(如 0.5)。对于小型网络或轻微正则化,p 可以小一些(如 0.1-0.2)。输出层通常不使用 Dropout。

Q17. 为什么深度可分离卷积比普通卷积更抗过拟合?

深度可分离卷积的参数量大幅减少(约为普通卷积的 1/K²),从根源上降低过拟合风险。参数越少,模型能学习的函数越受限,泛化能力越强。这是结构化正则化的一种形式。

Q18. 什么是 MixUp 和 CutMix 的区别?

MixUp 线性混合两幅图像和标签,CutMix 将一幅图像的部分区域替换为另一幅。MixUp 产生平滑的图像混合,CutMix 保留更多局部特征。两者都能提高模型对输入扰动的鲁棒性。

Q19. 为什么 BatchNorm 对 batch size 敏感?

BatchNorm 需要足够的 batch size 来获得稳定的统计量。当 batch size 很小时(如 1-4),每个 batch 的均值方差波动很大,导致训练不稳定。GroupNorm 和 LayerNorm 不依赖 batch size,适合 batch size 较小的场景。

Q20. 参数共享在 RNN 中是如何体现的?

RNN 在不同时间步使用相同的权重矩阵,实现时间步间的参数共享。这意味着网络在 t=1 和 t=100 使用完全相同的变换,参数在所有时间步被复用。这大大减少了参数量,使 RNN 能处理任意长度的序列。

FAQ 总结

  • L1/L2 正则化:稀疏性 vs 平滑性,weight_decay 实现 L2
  • 数据增强:扩充训练数据,提高泛化能力
  • 提前停止:监控验证集,在过拟合前停止
  • BatchNorm:标准化 + 可学习参数,训练/推理行为不同
  • Dropout:训练时随机丢弃,推理时使用所有神经元
  • 参数共享:减少参数量,提高泛化能力
posted @ 2026-07-15 18:08  左扬  阅读(17)  评论(0)    收藏  举报