从线性到非线性的本质
深度学习的核心能力在于学习极其复杂的数据模式,例如图像中的物体、语音中的语义或推荐系统中的用户兴趣。这些模式本质上都是高度非线性的。而一个模型如果只能进行线性变换,它就永远无法捕捉到这些非线性的关系。激活函数的引入,正是为了赋予神经网络进行非线性变换的能力。
让我们定义一个无激活函数的网络
我们先构建一个包含一个隐藏层的多层感知机(MLP),但暂时不使用任何激活函数。
- 输入层: 一个小批量数据 ,其中 是批量大小, 是每个样本的特征维度。
- 隐藏层: 包含 个神经元。其权重为 ,偏置为 。
- 输出层: 包含 个神经元。其权重为 ,偏置为 。
根据线性代数的规则,从输入到输出的计算过程如下:
-
计算隐藏层输出 :
-
计算最终输出 :
这里的每次操作(矩阵乘法和加法)都是线性变换。
线性叠加的失效
现在,我们来分析这个两层网络的最终数学形式。我们将第一步的 代入第二步的公式中:
利用矩阵乘法的分配律,展开上式:
现在,让我们定义两个新的矩阵:
- 令 。由于 且 ,所以 。
- 令 。由于 且 ,所以 。再加上 ,最终 。
将新定义的 和 代回原式,我们得到:
这个形式与一个单层的线性模型完全等价。无论我们堆叠多少个没有激活函数的线性层,通过简单的矩阵合并,最终总能化简为一个单层的线性网络。这意味着增加网络的“深度”并没有带来任何表示能力的提升,只是增加了冗余的参数。
引入非线性激活函数
为了打破这种线性叠加的局限,我们在隐藏层的线性变换之后,应用一个非线性的激活函数 ,例如 ReLU、Sigmoid 或 Tanh。
现在,模型的计算过程变为:
-
计算隐藏层输出 (加入了激活函数 ):
-
计算最终输出 :
将 代入,最终的表达式为:
由于 是一个非线性函数,我们无法再像之前那样将 和 合并。非线性函数 如同一道屏障,阻止了线性变换的直接传递和化简。正是这个“屏障”的存在,使得每一层都能学习到前一层输出的不同层次、不同方面的抽象特征,赋予了深度网络学习复杂非线性模式的能力。
代码验证 线性模型的坍缩
我们用 PyTorch 来清晰地展示这个“坍缩”过程。
我们将构建两个模型:
LinearMLP
: 一个没有激活函数的两层网络。- 一个等效的单层网络,其权重由
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
结论 激活函数的根本价值
总结一下,激活函数的根本价值在于:
- 引入非线性:它是神经网络能够学习和表示非线性关系的唯一途径。没有它,深度网络就失去了其核心优势。
- 打破线性叠加:它使得多层网络的堆叠成为可能,每一层都可以学习到更深层、更抽象的特征,从而构建起强大的表示能力。
因此,从“线性”到“非线性”的跃迁,是深度学习模型从简单的线性分类器/回归器转变为能够解决复杂现实世界问题的强大工具的关键一步。