共计 5611 个字符,预计需要花费 15 分钟才能阅读完成。
激活函数:神经网络的开关与放大器
在神经网络中,激活函数就像一个个微小的决策单元,决定着一个神经元是否应该被激活。它的存在让神经网络具备了非线性表达能力,能够拟合复杂的函数关系。

1. 为什么需要激活函数?
想象一下,如果没有激活函数,神经网络就只是一堆线性变换的叠加,无论叠加多少层,最终的效果仍然等同于一个单层线性网络。这显然无法胜任复杂的任务,比如图像分类或语音识别。
- 线性 vs 非线性:激活函数引入了非线性因素,让网络可以学习更复杂的模式。
- 决定神经元输出:它决定了神经元的输出值,通常将输入映射到某个特定范围内。
2. 常见激活函数对比
2.1 Sigmoid 函数
数学表达式:
$$
\sigma(x) = \frac{1}{1 + e^{-x}}
$$
导数:
$$
\sigma'(x) = \sigma(x)(1 – \sigma(x))
$$
特点:
– 输出范围在 (0,1) 之间
– 容易导致梯度消失问题(当输入值较大或较小时,导数趋近于 0)
2.2 Tanh 函数
数学表达式:
$$
\tanh(x) = \frac{e^x – e^{-x}}{e^x + e^{-x}}
$$
导数:
$$
\tanh'(x) = 1 – \tanh^2(x)
$$
特点:
– 输出范围在 (-1,1) 之间
– 比 Sigmoid 函数有更强的梯度(因为均值在 0 附近)
– 但仍然存在梯度消失问题
2.3 ReLU 函数
数学表达式:
$$
\text{ReLU}(x) = \max(0, x)
$$
导数:
$$
\text{ReLU}'(x) = \begin{cases}
1 & \text{如果} x > 0 \
0 & \text{如果} x \leq 0
\end{cases}
$$
特点:
– 计算简单高效
– 解决了梯度消失问题(在正区间)
– 可能导致 ”Dead ReLU” 问题(神经元永远不被激活)
2.4 LeakyReLU 函数
数学表达式:
$$
\text{LeakyReLU}(x) = \begin{cases}
x & \text{如果} x > 0 \
\alpha x & \text{如果} x \leq 0
\end{cases}
$$
其中 α 是一个小的正数(如 0.01)。
特点:
– 解决了 ReLU 的 Dead 问题
– 在负区间也有小的梯度
3. 实战:MNIST 分类任务
下面我们用 PyTorch 实现一个简单的全连接网络,在 MNIST 数据集上测试不同激活函数的表现。
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
import matplotlib.pyplot as plt
# 1. 准备数据
transform = transforms.Compose([transforms.ToTensor(),
transforms.Normalize((0.5,), (0.5,))
])
trainset = torchvision.datasets.MNIST(root='./data', train=True, download=True, transform=transform)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=64, shuffle=True)
testset = torchvision.datasets.MNIST(root='./data', train=False, download=True, transform=transform)
testloader = torch.utils.data.DataLoader(testset, batch_size=64, shuffle=False)
# 2. 定义网络
class Net(nn.Module):
def __init__(self, activation='relu'):
super(Net, self).__init__()
self.fc1 = nn.Linear(28*28, 512)
self.fc2 = nn.Linear(512, 256)
self.fc3 = nn.Linear(256, 128)
self.fc4 = nn.Linear(128, 10)
# 初始化权重
for m in self.modules():
if isinstance(m, nn.Linear):
# 根据激活函数选择不同的初始化方法
if activation == 'relu' or activation == 'leaky_relu':
nn.init.kaiming_normal_(m.weight, mode='fan_in', nonlinearity='relu')
elif activation == 'sigmoid' or activation == 'tanh':
nn.init.xavier_normal_(m.weight, gain=nn.init.calculate_gain(activation))
nn.init.constant_(m.bias, 0.1)
# 选择激活函数
if activation == 'sigmoid':
self.activation = nn.Sigmoid()
elif activation == 'tanh':
self.activation = nn.Tanh()
elif activation == 'leaky_relu':
self.activation = nn.LeakyReLU(0.01)
else: # 默认使用 ReLU
self.activation = nn.ReLU()
def forward(self, x):
x = x.view(-1, 28*28) # 展平输入
x = self.activation(self.fc1(x))
x = self.activation(self.fc2(x))
x = self.activation(self.fc3(x))
x = self.fc4(x) # 最后一层不使用激活函数
return x
# 3. 训练函数
def train_model(activation='relu', lr=0.01, epochs=10):
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = Net(activation=activation).to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=lr, momentum=0.9)
# 学习率衰减
scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=5, gamma=0.1)
train_losses = []
train_accs = []
for epoch in range(epochs):
model.train()
running_loss = 0.0
correct = 0
total = 0
for i, (inputs, labels) in enumerate(trainloader, 0):
inputs, labels = inputs.to(device), labels.to(device)
optimizer.zero_grad()
outputs = model(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
running_loss += loss.item()
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
if i % 200 == 199:
print(f'[{epoch+1}, {i+1}] loss: {running_loss/200:.3f}')
running_loss = 0.0
scheduler.step()
train_loss = running_loss / len(trainloader)
train_acc = 100 * correct / total
train_losses.append(train_loss)
train_accs.append(train_acc)
print(f'Epoch {epoch+1}, Loss: {train_loss:.4f}, Accuracy: {train_acc:.2f}%')
# 测试
model.eval()
test_correct = 0
test_total = 0
with torch.no_grad():
for inputs, labels in testloader:
inputs, labels = inputs.to(device), labels.to(device)
outputs = model(inputs)
_, predicted = torch.max(outputs.data, 1)
test_total += labels.size(0)
test_correct += (predicted == labels).sum().item()
test_acc = 100 * test_correct / test_total
print(f'Test Accuracy: {test_acc:.2f}%')
return train_losses, train_accs, test_acc
# 4. 训练不同激活函数的模型
activations = ['sigmoid', 'tanh', 'relu', 'leaky_relu']
results = {}
for act in activations:
print(f'\nTraining with {act} activation function...')
losses, accs, test_acc = train_model(activation=act, epochs=15)
results[act] = {'losses': losses, 'accs': accs, 'test_acc': test_acc}
# 5. 可视化结果
plt.figure(figsize=(12, 5))
# 准确率曲线
plt.subplot(1, 2, 1)
for act in activations:
plt.plot(results[act]['accs'], label=act)
plt.title('Training Accuracy')
plt.xlabel('Epoch')
plt.ylabel('Accuracy (%)')
plt.legend()
# 损失曲线
plt.subplot(1, 2, 2)
for act in activations:
plt.plot(results[act]['losses'], label=act)
plt.title('Training Loss')
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.legend()
plt.tight_layout()
plt.show()
# 打印测试准确率
print('\nTest Accuracy Comparison:')
for act in activations:
print(f'{act}: {results[act]["test_acc"]:.2f}%')
4. 实验结果分析
通过上述实验,我们可以观察到:
- 训练速度:ReLU 和 LeakyReLU 明显快于 Sigmoid 和 Tanh
- 最终准确率:ReLU 类激活函数通常能达到更高的测试准确率
- 梯度消失:Sigmoid 和 Tanh 在深层网络中容易出现训练停滞(梯度消失)
- GPU 显存占用:不同激活函数对显存影响不大,主要取决于网络结构和 batch size
5. 避坑指南
5.1 Dead ReLU 问题
现象:使用 ReLU 时,某些神经元可能永远输出 0,不再参与学习。
解决方案:
– 使用 LeakyReLU 或 Parametric ReLU
– 调整学习率(通常减小学习率可以缓解)
– 使用更好的权重初始化方法(如 He 初始化)
5.2 权重初始化不当
现象:如果权重初始化不当,可能导致激活值过早饱和(对于 Sigmoid/Tanh)或 Dead ReLU。
解决方案:
– 对于 Sigmoid/Tanh:使用 Xavier/Glorot 初始化
– 对于 ReLU/LeakyReLU:使用 He/Kaiming 初始化
5.3 学习率设置不当
现象:学习率太大可能导致震荡不收敛,太小则训练缓慢。
解决方案:
– 使用学习率衰减策略
– 监控训练损失,如果波动太大则减小学习率
– 可以尝试自适应优化器(如 Adam)
6. 进阶思考
除了上述经典激活函数外,近年来还出现了许多新型激活函数:
- Swish:
$$
\text{Swish}(x) = x \cdot \sigma(\beta x)
$$ - 由 Google 提出,在多个任务上表现优于 ReLU
-
平滑非单调,有助于梯度流动
-
GELU(高斯误差线性单元):
$$
\text{GELU}(x) = x \Phi(x)
$$
其中 Φ(x)是标准正态分布的累积分布函数。 - 被 BERT、GPT 等 Transformer 模型采用
- 结合了 ReLU 和 Dropout 的思想
读者可以尝试修改上面的代码,用这些新型激活函数进行实验,观察它们的表现。
7. 总结
激活函数的选择对神经网络的训练和性能有重大影响。通过本文的实验和分析,我们可以得出以下结论:
- 对于大多数情况,ReLU 及其变体(如 LeakyReLU)是良好的默认选择
- Sigmoid 和 Tanh 适合某些特定场景(如需要输出在特定范围时),但要注意梯度消失问题
- 权重初始化和学习率设置需要与激活函数配合使用
- 新型激活函数(如 Swish、GELU)在某些任务上可能表现更好,值得尝试
希望这篇文章能帮助初学者理解激活函数的作用和选择策略。在实际应用中,建议多进行实验比较,找到最适合特定任务的激活函数。
