从线性到非线性的本质

深度学习的核心能力在于学习极其复杂的数据模式,例如图像中的物体、语音中的语义或推荐系统中的用户兴趣。这些模式本质上都是高度非线性的。而一个模型如果只能进行线性变换,它就永远无法捕捉到这些非线性的关系。激活函数的引入,正是为了赋予神经网络进行非线性变换的能力。

让我们定义一个无激活函数的网络

我们先构建一个包含一个隐藏层的多层感知机(MLP),但暂时不使用任何激活函数

  • 输入层: 一个小批量数据 ,其中 是批量大小, 是每个样本的特征维度。
  • 隐藏层: 包含 个神经元。其权重为 ,偏置为
  • 输出层: 包含 个神经元。其权重为 ,偏置为

根据线性代数的规则,从输入到输出的计算过程如下:

  1. 计算隐藏层输出

  2. 计算最终输出

这里的每次操作(矩阵乘法和加法)都是线性变换

线性叠加的失效

现在,我们来分析这个两层网络的最终数学形式。我们将第一步的 代入第二步的公式中:

利用矩阵乘法的分配律,展开上式:

现在,让我们定义两个新的矩阵:

  • 。由于 ,所以
  • 。由于 ,所以 。再加上 ,最终

将新定义的 代回原式,我们得到:

这个形式与一个单层的线性模型完全等价。无论我们堆叠多少个没有激活函数的线性层,通过简单的矩阵合并,最终总能化简为一个单层的线性网络。这意味着增加网络的“深度”并没有带来任何表示能力的提升,只是增加了冗余的参数。

引入非线性激活函数

为了打破这种线性叠加的局限,我们在隐藏层的线性变换之后,应用一个非线性的激活函数 ,例如 ReLU、Sigmoid 或 Tanh。

现在,模型的计算过程变为:

  1. 计算隐藏层输出 (加入了激活函数 ):

  2. 计算最终输出

代入,最终的表达式为:

由于 是一个非线性函数,我们无法再像之前那样将 合并。非线性函数 如同一道屏障,阻止了线性变换的直接传递和化简。正是这个“屏障”的存在,使得每一层都能学习到前一层输出的不同层次、不同方面的抽象特征,赋予了深度网络学习复杂非线性模式的能力。

代码验证 线性模型的坍缩

我们用 PyTorch 来清晰地展示这个“坍缩”过程。

我们将构建两个模型:

  1. LinearMLP: 一个没有激活函数的两层网络。
  2. 一个等效的单层网络,其权重由 LinearMLP 的权重计算得来。

然后我们将证明,对于相同的输入,它们的输出完全相同。

import torch
import torch.nn as nn
 
# 定义超参数
n_samples = 4   # 批量大小
d_in = 10       # 输入维度
h_hidden = 20   # 隐藏层维度
q_out = 5       # 输出维度
 
# 1. 构建一个没有激活函数的两层MLP
class LinearMLP(nn.Module):
    """一个简单的两层线性网络"""
    def __init__(self, d_in: int, h_hidden: int, q_out: int):
        super().__init__()
        self.layer1 = nn.Linear(d_in, h_hidden)
        self.layer2 = nn.Linear(h_hidden, q_out)
 
    def forward(self, x: torch.Tensor) -> torch.Tensor:
        # x shape: [n, d_in]
        # 注意:这里没有激活函数!
        h = self.layer1(x)  # h shape: [n, h_hidden]
        o = self.layer2(h)  # o shape: [n, q_out]
        return o
 
# 2. 实例化模型并提取权重
# 为了结果可复现,固定随机种子
torch.manual_seed(42)
two_layer_model = LinearMLP(d_in, h_hidden, q_out)
 
# 提取W1, b1, W2, b2
# PyTorch的nn.Linear中,权重是转置存储的,所以需要 .T
W1 = two_layer_model.layer1.weight.T # Shape: [d_in, h_hidden]
b1 = two_layer_model.layer1.bias     # Shape: [h_hidden]
W2 = two_layer_model.layer2.weight.T # Shape: [h_hidden, q_out]
b2 = two_layer_model.layer2.bias     # Shape: [q_out]
 
# 3. 根据我们的数学推导,计算等效的单层网络的权重和偏置
# W_equiv = W1 @ W2
# b_equiv = b1 @ W2 + b2
W_equiv = W1 @ W2                    # Shape: [d_in, q_out]
b_equiv = b1 @ W2 + b2               # Shape: [q_out]
 
# 4. 构建等效的单层网络,并手动设置其权重
single_layer_model = nn.Linear(d_in, q_out)
 
# 使用 with torch.no_grad() 因为我们不是在训练,只是在修改参数
with torch.no_grad():
    # 注意,PyTorch权重需要转置后赋值
    single_layer_model.weight.copy_(W_equiv.T)
    single_layer_model.bias.copy_(b_equiv)
 
# 5. 创建随机输入并比较两个模型的输出
input_tensor = torch.randn(n_samples, d_in) # Shape: [n_samples, d_in]
 
output_two_layer = two_layer_model(input_tensor)
output_single_layer = single_layer_model(input_tensor)
 
# 验证输出是否几乎完全相等 (使用 allclose 处理浮点数精度问题)
are_outputs_equal = torch.allclose(output_two_layer, output_single_layer)
 
# 打印结果
print(f"两层线性网络的输出:\n{output_two_layer}\n")
print(f"等效单层网络的输出:\n{output_single_layer}\n")
print(f"两个模型的输出是否相等? -> {are_outputs_equal}")
 

当我们运行这段代码,你会看到输出 True。这证明了,一个堆叠的线性网络在功能上等同于一个单层网络。

两层线性网络的输出:
tensor([[ 0.0222, -0.0984, -0.3281,  0.1727, -0.2270],
        [ 0.3905,  0.2054,  0.3526,  0.2433,  0.6196],
        [-0.0098, -0.4050, -0.0006, -0.0012, -0.3073],
        [-0.0512, -0.3356, -0.4761, -0.1166, -0.4144]],
       grad_fn=<AddmmBackward0>)
 
等效单层网络的输出:
tensor([[ 0.0222, -0.0984, -0.3281,  0.1727, -0.2270],
        [ 0.3905,  0.2054,  0.3526,  0.2433,  0.6196],
        [-0.0098, -0.4050, -0.0006, -0.0012, -0.3073],
        [-0.0512, -0.3356, -0.4761, -0.1166, -0.4144]],
       grad_fn=<AddmmBackward0>)
 
两个模型的输出是否相等? -> True

结论 激活函数的根本价值

总结一下,激活函数的根本价值在于:

  1. 引入非线性:它是神经网络能够学习和表示非线性关系的唯一途径。没有它,深度网络就失去了其核心优势。
  2. 打破线性叠加:它使得多层网络的堆叠成为可能,每一层都可以学习到更深层、更抽象的特征,从而构建起强大的表示能力。

因此,从“线性”到“非线性”的跃迁,是深度学习模型从简单的线性分类器/回归器转变为能够解决复杂现实世界问题的强大工具的关键一步。