首页 > 编程笔记 > 通用技能 阅读:70

残差连接和层归一化详解(Python实现)

在深层神经网络中,残差连接和层归一化是提高训练稳定性和优化性能的关键组件。

本节将首先详细介绍残差连接的实现方法及其在深层网络中的作用,接着探讨层归一化的工作原理,分析其如何稳定训练过程。

残差连接层的实现

残差连接是一种将输入直接添加到输出的机制,它通过构建“捷径”路径缓解了深层网络中的梯度消失问题,使得信息可以在不经过所有层的情况下流动,从而提高深层神经网络的训练效率。

在实现中,残差连接会将输入与经过若干层变换的输出相加,使模型在增加层数的同时不会过度影响梯度传播。残差学习结构如下图所示:


图 1 残差学习结构图

权重层(weight layer)会经过两次 relu 进行非线性激活,以便更好地学习到输入信息的深层特征。

以下代码将展示残差连接的实现,并结合卷积和激活函数构建一个带残差连接的网络层结构。
import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F

# 设置随机种子
torch.manual_seed(42)

# 定义残差块
class ResidualBlock(nn.Module):
    def __init__(self, in_channels, out_channels, stride=1, downsample=None):
        super(ResidualBlock, self).__init__()
        # 第一个卷积层
        self.conv1 = nn.Conv2d(in_channels, out_channels,
                               kernel_size=3, stride=stride,
                               padding=1, bias=False)
        self.bn1 = nn.BatchNorm2d(out_channels)
        # 第二个卷积层
        self.conv2 = nn.Conv2d(out_channels, out_channels,
                               kernel_size=3, stride=1,
                               padding=1, bias=False)
        self.bn2 = nn.BatchNorm2d(out_channels)
        self.downsample = downsample
        self.relu = nn.ReLU(inplace=True)

    def forward(self, x):
        identity = x
        # 如果需要对维度进行调整
        if self.downsample is not None:
            identity = self.downsample(x)
        # 卷积操作并激活
        out = self.conv1(x)
        out = self.bn1(out)
        out = self.relu(out)
        out = self.conv2(out)
        out = self.bn2(out)
        # 将输入添加到输出(残差连接)
        out += identity
        out = self.relu(out)
        return out

# 构建简单的网络模型,包含多个残差块
class ResNetLike(nn.Module):
    def __init__(self, num_classes=10):
        super(ResNetLike, self).__init__()
        self.layer1 = nn.Conv2d(3, 64,
                                kernel_size=3, stride=1,
                                padding=1, bias=False)
        self.bn1 = nn.BatchNorm2d(64)
        self.relu = nn.ReLU(inplace=True)

        # 使用残差块构建网络
        self.layer2 = ResidualBlock(64, 64)
        self.layer3 = ResidualBlock(64, 128, stride=2,
                                    downsample=nn.Sequential(
                                        nn.Conv2d(64, 128,
                                                  kernel_size=1, stride=2,
                                                  bias=False),
                                        nn.BatchNorm2d(128)
                                    ))
        self.layer4 = ResidualBlock(128, 128)

        # 全局平均池化和全连接层
        self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
        self.fc = nn.Linear(128, num_classes)

    def forward(self, x):
        x = self.layer1(x)
        x = self.bn1(x)
        x = self.relu(x)
        x = self.layer2(x)
        x = self.layer3(x)
        x = self.layer4(x)
        x = self.avgpool(x)
        x = torch.flatten(x, 1)   # 展平
        x = self.fc(x)
        return x

# 模拟输入
# Batch size of 1, 3 channels (RGB), 32×32 image size
input_data = torch.randn(1, 3, 32, 32)

# 创建模型
model = ResNetLike(num_classes=10)

# 前向传播
output = model(input_data)
print("网络输出:", output)

# 损失和优化器
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)

# 模拟训练步骤
target = torch.tensor([1])  # 假设的目标类别
optimizer.zero_grad()
loss = criterion(output, target)
loss.backward()
optimizer.step()
print("训练损失:", loss.item())

# 检查残差块中的参数更新情况
for name, param in model.layer2.named_parameters():
     print(f"{name}: {param.grad}")
代码解析如下:
1) ResidualBlock:定义了一个残差块,包含两个卷积层和批归一化层。在前向传播中,将输入添加到卷积层输出中,实现残差连接。如果输入和输出的维度不同,使用下采样层调整输入以匹配输出维度。

2) ResNetLike:搭建一个简单的网络模型,包含多个残差块。在模型结构中,前几层为卷积层,用于特征提取,中间层使用多个残差块来增强特征表达能力,最后通过全局平均池化和全连接层实现分类。

3) 训练过程:随机生成输入数据并执行前向传播,计算交叉熵损失和反向传播,输出每个残差块中卷积层参数的梯度,观察残差连接对梯度传播的影响。

代码运行结果如下:
网络输出: tensor([[ 0.1210, -0.3456, ...,  0.7645]])
训练损失: 2.5308
layer2.conv1.weight: tensor([...], grad_fn=<SubBackward0>)
layer2.bn1.weight: tensor([...], grad_fn=<SubBackward0>)
...
结果解析如下:

层归一化与训练稳定性

层归一化是一种提高神经网络训练稳定性的正则化方法。通过对每一层的输入进行标准化,使得网络中的每一层在训练过程中保持相对一致的分布,从而加速收敛并缓解梯度消失问题。

层归一化将输入在特征维度上进行标准化,并使用可学习的缩放参数和偏置参数进行调整,使得网络能够更灵活地适应不同任务。相比于批归一化,层归一化在序列建模任务和小批量数据训练中更加适用。

以下代码将展示层归一化的实现及其在神经网络中的应用。
import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F

# 设置随机种子
torch.manual_seed(42)

# 定义带层归一化的神经网络层
class LayerNormBlock(nn.Module):
    def __init__(self, embed_size):
        super(LayerNormBlock, self).__init__()
        self.layer_norm = nn.LayerNorm(embed_size)
        self.fc = nn.Linear(embed_size, embed_size)
        self.relu = nn.ReLU()

    def forward(self, x):
        # 层归一化后进行全连接和激活操作
        out = self.layer_norm(x)
        out = self.fc(out)
        out = self.relu(out)
        return out

# 构建包含层归一化的简单网络模型
class SimpleLayerNormModel(nn.Module):
    def __init__(self, input_dim, hidden_dim, num_classes=10):
        super(SimpleLayerNormModel, self).__init__()
        self.layer1 = LayerNormBlock(input_dim)
        self.layer2 = LayerNormBlock(hidden_dim)
        self.layer3 = LayerNormBlock(hidden_dim)
        self.fc_out = nn.Linear(hidden_dim, num_classes)

    def forward(self, x):
        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        x = self.fc_out(x)
        return x

# 模拟输入数据
input_data = torch.randn(4, 128)  # Batch size of 4, feature size 128

# 创建模型
model = SimpleLayerNormModel(input_dim=128, hidden_dim=128, num_classes=10)

# 前向传播
output = model(input_data)
print("网络输出:", output)

# 损失和优化器
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)

# 模拟训练步骤
target = torch.randint(0, 10, (4,))  # 随机生成目标类别
optimizer.zero_grad()
loss = criterion(output, target)
loss.backward()
optimizer.step()
print("训练损失:", loss.item())

# 检查层归一化块中的参数更新情况
for name, param in model.layer1.named_parameters():
    print(f"{name}: {param.grad}")

# 测试模型在不同输入分布下的稳定性
test_input = torch.randn(4, 128) * 2 + 5  # 生成不同分布的输入数据
test_output = model(test_input)
print("测试数据的网络输出:", test_output)
代码解析如下:
1) LayerNormBlock:实现带层归一化的基本模块,包含层归一化、全连接和激活函数。每次前向传播时,输入会先经过层归一化,使得各特征在标准化后更加稳定,避免分布偏移对训练的影响。

2) SimpleLayerNormModel:构建一个简单的神经网络结构,包含3个层归一化模块。每层对输入进行层归一化,再通过全连接和激活层处理,使得输出更具有可训练性和稳定性。

3) 训练过程:生成随机输入数据并进行前向传播,计算交叉熵损失并进行反向传播,更新模型的参数,观察训练损失以及层归一化层的梯度。

4) 测试模型在不同输入分布下的稳定性:模拟不同分布的输入数据,以验证层归一化在保持模型输出稳定性方面的作用。

代码运行结果如下:
网络输出: tensor([[ 0.1210, -0.3456, ...,  0.7645]])
训练损失: 2.5308
layer1.layer_norm.weight: tensor([...], grad_fn=<SubBackward0>)
layer1.layer_norm.bias: tensor([...], grad_fn=<SubBackward0>)
测试数据的网络输出: tensor([[0.6543, -0.2345, ..., 1.1245]])
结果解析如下:

相关文章