BP神经网络激活函数:从Sigmoid到ReLU的演进与实战避坑指南

1次阅读
没有评论

共计 5611 个字符,预计需要花费 15 分钟才能阅读完成。

image.webp

激活函数:神经网络的开关与放大器

在神经网络中,激活函数就像一个个微小的决策单元,决定着一个神经元是否应该被激活。它的存在让神经网络具备了非线性表达能力,能够拟合复杂的函数关系。

BP 神经网络激活函数:从 Sigmoid 到 ReLU 的演进与实战避坑指南

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. 实验结果分析

通过上述实验,我们可以观察到:

  1. 训练速度:ReLU 和 LeakyReLU 明显快于 Sigmoid 和 Tanh
  2. 最终准确率:ReLU 类激活函数通常能达到更高的测试准确率
  3. 梯度消失:Sigmoid 和 Tanh 在深层网络中容易出现训练停滞(梯度消失)
  4. 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. 进阶思考

除了上述经典激活函数外,近年来还出现了许多新型激活函数:

  1. Swish
    $$
    \text{Swish}(x) = x \cdot \sigma(\beta x)
    $$
  2. 由 Google 提出,在多个任务上表现优于 ReLU
  3. 平滑非单调,有助于梯度流动

  4. GELU(高斯误差线性单元):
    $$
    \text{GELU}(x) = x \Phi(x)
    $$
    其中 Φ(x)是标准正态分布的累积分布函数。

  5. 被 BERT、GPT 等 Transformer 模型采用
  6. 结合了 ReLU 和 Dropout 的思想

读者可以尝试修改上面的代码,用这些新型激活函数进行实验,观察它们的表现。

7. 总结

激活函数的选择对神经网络的训练和性能有重大影响。通过本文的实验和分析,我们可以得出以下结论:

  1. 对于大多数情况,ReLU 及其变体(如 LeakyReLU)是良好的默认选择
  2. Sigmoid 和 Tanh 适合某些特定场景(如需要输出在特定范围时),但要注意梯度消失问题
  3. 权重初始化和学习率设置需要与激活函数配合使用
  4. 新型激活函数(如 Swish、GELU)在某些任务上可能表现更好,值得尝试

希望这篇文章能帮助初学者理解激活函数的作用和选择策略。在实际应用中,建议多进行实验比较,找到最适合特定任务的激活函数。

正文完
 0
评论(没有评论)