深度学习模型,凭借其强大的特征学习和复杂模式识别能力,在诸多领域取得了显著成就。然而,这些模型的复杂性也使其在训练数据量不足或噪声干扰较大时,极易出现过拟合(Overfitting)现象。过拟合指的是模型在训练集上表现优异,但在未见过的测试集上性能显著下降,这表明模型学习到了训练数据中的噪声和特有模式,而非普适的规律。为了缓解过拟合,提升模型的泛化能力,正则化(Regularization)技术应运而生。正则化通过向模型的损失函数中引入额外的惩罚项,对模型的复杂度进行约束,引导模型学习更简单、更平滑的参数。在众多正则化方法中,L1 范数正则化(Lasso 回归)、L2 范数正则化(Ridge 回归)以及它们的组合 Elastic Net 正则化,是最为经典和广泛应用的权重正则化技术

基本知识

L1 范数

  • 定义: L1 范数也称为曼哈顿范数或绝对值范数, 是向量中各个元素的绝对值之和

  • 数学表示: 对于一个实数向量 $x = (x_1, x_2, x_3, …, x_n)$, 其 L1 范数被定义如下:

    \[\|x\|_1 = \sum_{i=0}^n |x_i|\]
  • 性质:

    • L1 范数始终的非负的, 即 $|x|_1 \ge 0$

    • 对于任何标量 $c$ 和向量 $x$, 有 $|c \cdot x|_1 = |c| \cdot |x|_1$

    • 对于任意两个向量 $x$ 和 $y$, 有 $|x + y|_1 \le |x|_1 + |y|_1$

    • L1 范数倾向于产生稀疏解, 即解中的许多元素为 0. 因为在优化问题中, L1 范数会促使权值不断向 0 靠拢, 从而使不重要的特征对应的权值变为 0, 实现特征的选择

    • L1 范数是一个凸函数, 这意味着对于任何两个向量 $x$ 和 $y$ 以及 $0 \le t \le 1$, 都有 $|t \cdot x + (1 - t) \cdot y|_1 \le t|x|_1 + (1 - t)|y|_1$, 凸函数性质使其能够在优化过程中找到全局最优解

      凸函数 (Convex function) 是一种函数类型,它描述了函数图像的形状特性。简单来说,如果一个函数的图像在任意两点之间的线段都在函数图像的上方或刚好与图像重合,那么这个函数就是凸函数。举个例子,考虑常见的二次函数 $y=x^2$,其图像是开口向上的抛物线。当你在抛物线上取任意两个点,并连接这两个点形成一条线段时,这条线段会位于抛物线的上方或与抛物线重合。这说明 $y=x^2$ 是一个凸函数

      凸函数有以下重要性质:

      • 凸函数在其定义域内导数是单调递增的。这意味着随着输入值的增加,函数的斜率会变得越来越陡峭或保持不变
      • 凸函数在其定义域内如果有极小值,那么这个极小值是全局的。也就是说,如果找到一个局部极小值点,那么它就是整个函数的最小点
      • 凸函数的非负线性组合仍然是凸函数。比如,如果有两个凸函数 $f(x)$ 和 $g(x)$,以及非负系数 $a$ 和 $b$,那么函数 $h(x)=a \cdot f(x)+b \cdot g(x)$ 也是凸函数
    • L1 范数的单位球是一个 $n$ 维超立方体, 其边长为 2

  • 应用场景:

    • 鲁棒性模型训练/特征选择: 由于 L1 范数对异常值具有一定的鲁棒性,因此在训练机器学习模型时,若数据中存在噪声或异常值,使用 L1 范数作为损失函数的一部分,可以使模型对这些异常数据不那么敏感

    • 稀疏信号恢复: 在信号处理领域,如压缩感知中,通过 L1 范数可以实现对稀疏信号的精确恢复,仅需少量的测量值就能重建出原始信号

L2 范数

  • 定义: L2 范数也叫欧几里得范数或平方和范数, 是向量中各元素的平方和的平方根

  • 数学表示: 对于一个实数向量 $x$, 其 L2 范数定义如下:

    \[\|x\|_2 = \sqrt{x_1^2 + x_2^2 + ... + x_n^2}\]
  • 性质:

    • L2 范数是非负的,且仅当向量的所有元素都为零时,其范数为零

    • 对于任何标量 $c$ 和向量 $x$, 有 $|c \cdot x|_2 = |c| \cdot |x|_2$

    • 对于任意两个向量 $x$ 和 $y$, 有 $|x + y|_2 \le |x|_2 + |y|_2$

    • L2 范数的平方在求导时较为方便,优化过程中容易求解极值问题,因此在机器学习和最优化领域应用广泛。其平方被称为平方模或平方范数

    • L2 范数的最小化解是唯一的,这在优化问题中具有重要意义,能够保证问题有明确的最优解

    • 在几何上,L2 范数表示向量对应点到原点的欧几里得距离, 在二维平面上,向量 $(x, y)$ 的 L2 范数就是该点到原点的距离

  • 应用场景:

    • 机器学习: L2 范数常被用作正则化项,如岭回归(Ridge Regression)中,通过在损失函数中添加 L2 范数项,可以限制模型参数的大小,防止过拟合,提高模型的泛化能力

    • 优化问题: 在优化问题中,L2 范数可以作为目标函数或约束条件,用于衡量解的质量或可行性。例如在最小二乘法中,通过最小化残差的 L2 范数来求解线性方程组的最佳近似解

    • 距离度量: 由于 L2 范数具有直观的几何意义,因此在计算两个向量或点之间的距离时,常使用 L2 范数, 也就是欧几里得距离

L1 范数与 L2 范数在深度学习中的应用

L1 正则化 (Lasso 回归)

  • 定义: L1 正则化,也被称为 Lasso (Least Absolute Shrinkage and Selection Operator, 最小绝对收缩和选择算子) 回归,是一种通过对模型权重的绝对值之和施加惩罚来防止过拟合的技术 。它在深度学习中因其产生稀疏权重 (Sparse Weights) 的能力而备受关注,其通过将不重要的特征权重推向 0 实现了特征选择

  • 损失函数的数学表示: L1 正则化是在原始损失函数 $L_{orig}$ 的基础上,增加一个与模型权重矩阵 $w$ 的 L1 范数成正比的惩罚项:

    \[L(w) = L_{orig}(w) + \lambda \sum_{j=1}^n\|w_j\|\]

    其中,$w_j$ 是模型的第 $j$ 个权重,$\lambda$ 是一个非负的正则化超参数,用于控制 L1 正则化的强度

  • 性质和意义: L1 正则化的显著特点是它能够产生稀疏解,即模型中的许多权重会变为精确的零 。这种稀疏性源于 L1 范数惩罚项的几何形状。在优化过程中,L1 惩罚项在权重接近零的区域具有恒定的梯度 (对于 $w_j \ne 0$, $\frac{\partial(\lambda \cdot |w_j|)}{\partial w_j} = sgn(w_j)$, 即 +1 或 -1)。这意味着即使权重已经很小,反向传播仍然会给出一个恒定的梯度将其推向 0,如果某个特征对减少原始损失的贡献不足以抵消这个恒定的惩罚梯度,其对应的权重就会被优化到 0

  • 算法实现:

      class BaseNeuralNetwork(nn.Module):
          def __init__(self): ...
    
          def l1(self, _lambda: float = 1e-4) -> torch.Tensor:
              def _l1() -> torch.Tensor:
                  l2_reg = torch.tensor(0.)
                  for param in self.parameters():
                      l2_reg += (torch.abs(param)).sum()
                  return l2_reg
              return _lambda * _l1()
    

L2 正则化 (Ridge 回归,或岭回归)

  • 定义: L2 正则化,也称为 Ridge 回归权重衰减 (Weight Decay),是另一种广泛用于防止深度学习模型过拟合的技术 。它通过惩罚模型权重的平方和来约束模型的复杂度

  • 损失函数的数学表示: L2 正则化是在原始损失函数 $L_{orig}$ 上增加一个与模型权重矩阵 $w$ 的 L2 范数的平方成正比的惩罚项:

    \[L(w) = L_{orig}(w) + \frac{\lambda \sum_{j=1}^n w_j^2}{2}\]

    其中,$w_j$ 是模型的第 $j$ 个权重,$\lambda$ 是一个非负的正则化超参数,用于控制 L2 正则化的强度

    L2 正则化使用 L2 范数的平方而不是 L2 范数本身,主要是为了可微性:L2 范数的平方是一个处处可微的简单二次函数,而 L2 范数在原点是不可微的。由于梯度下降算法高度依赖损失函数的可微性,因此通常使用 L2 范数的平方以稳定权重参数优化过程

    当 L2 正则化项使用 $\lambda \sum_{j=1}^n w_j^2$ 时, 其梯度为 $2 \lambda w_j$, 通过除以 2 操作, 其梯度表示将变为 $\lambda w_j$, 这种形式的梯度更加简洁, 方便计算实现

  • 性质和意义: L2 正则化的主要作用是抑制模型产生过大的权重值。通过惩罚权重的平方,L2 正则化会有效地将所有权重向零进行收缩 (Shrinkage) 。与 L1 正则化不同,L2 正则化通常不会将权重精确地压缩到零,而是使它们趋近于零 。这是因为在反向传播过程中,L2 正则化惩罚项的梯度是 $2\lambda \cdot w_j$,即 $\frac{\partial (\lambda \cdot w_j^2)}{\partial w_j} = 2\lambda \cdot w_j$。这意味着当权重 $w_j$ 较小时,其梯度也会随之减小,对权重向 0 推动的推力较弱。当某个特征对减小原始损失的贡献与 L2 惩罚梯度达成平衡时,权重会最终稳定下来,而不是被直接优化到 0。这种权重收缩机制使得模型倾向于学习到更小、更分散的权重分布。这通常会导致更平滑的模型响应和决策边界,从而降低模型对训练数据中噪声的敏感性,提高其泛化能力。L2 正则化在处理多重共线性(即特征之间高度相关)问题时也表现良好,它倾向于将相关特征的权重一起缩小,而不是像 L1 那样从中选择一个

  • 算法实现:

      class BaseNeuralNetwork(nn.Module):
          def __init__(self): ...
    
          def l2(self, _lambda: float = 1e-4) -> torch.Tensor:
              def _l2() -> torch.Tensor:
                  l2_reg = torch.tensor(0.)
                  for param in self.parameters():
                      l2_reg += (torch.pow(param, exponent=2.)).sum()
                  return l2_reg
              return _lambda * _l2() / 2.
    

弹性网络 (ElasticNet)

  • 定义: 弹性网络 (Elastic Net) 是 L1 正则化和 L2 正则化的结合。它既具有 L1 正则化的稀疏性特点,能够实现特征选择,又具备 L2 正则化防止权重过大的优势,并且在处理特征之间存在多重共线性(即特征高度相关)的问题时,比单独的 L1 或 L2 正则化更具优势。通过在损失函数中同时引入 L1 和 L2 正则项,弹性网络能够在特征选择和权重收缩之间取得平衡

  • 损失函数的数学表示: 弹性网络的损失是在原有损失函数 $L_{orig}$ 的基础上, 同时增加 L1 正则化项和 L2 正则化项:

    \[L(w) = L_{\text{orig}}(w) + \alpha \left( \rho \sum_{j=1}^n |w_j| + \frac{1 - \rho}{2} \sum_{j=1}^n w_j^2 \right)\]

    其中, $w_j$ 是模型的第 $j$ 个权重, $\alpha$ 是正则化强度超参数, 控制正则化对总损失的影响, $\rho$ 是 L1 比例超参数, 取值范围在 $[0, 1]$, 用于平衡 L1 和 L2 正则化对弹性网络的贡献, 当 $\rho = 1$ 时, 弹性网络退化为 Lasso 回归, 而当 $\rho = 0$ 时, 弹性网络则退化为 Ridge 回归

  • 性质和意义: 弹性网络继承了 L1 正则化产生稀疏解的能力,同时利用 L2 正则化防止权重被过度压缩到零。这种组合使得模型既能选择重要特征(通过 L1),又能处理特征之间的相关性(通过 L2),从而在特征选择和权重稳定性之间取得平衡。在特征高度相关的情况下,L1 正则化可能随机选择其中一个特征而忽略其他相关特征,而 L2 正则化会倾向于将相关特征的权重平均分配。弹性网络通过混合两种正则化方式,能够在相关特征组中选择代表性特征,同时对其他相关特征的权重进行适当收缩,从而提高模型的稳定性和解释性。 通过调整 $\alpha$ 和 $\rho$,可以灵活控制正则化的强度和 L1/L2 的比例,以适应不同的数据特性和建模需求。这种灵活性使得弹性网络在各种复杂场景下都能表现出较好的泛化能力。

  • 算法实现:

      class BaseNeuralNetwork(nn.Module):
          def __init__(self): ...
    
          def l1(_lambda: float) -> torch.Tensor: ...
    
          def l2(_lambda: float) -> torch.Tensor: ...
            
          def elastic_net(self, alpha: float = 1e-4, rho: float = 0.5) -> torch.Tensor:
              return alpha * (rho * self.l1(_lambda=1.) + (1 - rho) * self.l2(_lambda=1.))