经过我们之前的介绍,我们发现对卷积神经网络而言,“越深越好”,这是有道理的,因为这样模型的能力应该更强(它们适应任何空间的灵活性都会提高,因为它们有更大的参数空间可以探索)。然而,很多实验发现,在一定深度之后,性能就会下降。 这是 VGG 的瓶颈之一,因为当神经网络中使用特定激活函数的层数越多,损失函数的梯度就会趋近于零,导致梯度消失和梯度爆炸问题,从而使网络难以训练。
最简单的解决方案是使用其他激活函数,例如 ReLU,它不会导致导数很小。而残差神经网络(ResNet) 是另一种有效的解决方案。
ResNet(Residual Network) 通过使用残差块(Residual Block)来解决深度神经网络中的梯度消失和梯度爆炸问题。ResNet架构在ImageNet图像分类竞赛中取得了很好的成绩,并且在许多计算机视觉任务中都得到广泛应用。
它的核心思想是引入了跳跃连接(skip connection),允许信息直接在网络中跳过一层或多层。这种跳跃连接使得梯度可以更快地向后传播,从而解决了深度网络中的梯度问题。如下图所示,残差连接直接将块开头的值 x 添加到块的末尾 (F(x)+x)。这种残差连接不会通过“压缩”导数的激活函数,从而导致块的整体导数更高。
因此,我们可以利用跳跃连接能够构建深度网络的ResNets,有时深度能够超过100层
ResNets是由残差块(Residual block)构建的,首先我解释一下什么是残差块,最常见的残差块构成方式如下:
在每个残差块中,残差层的输入会先通过卷积层进行特征提取,并与原始输入进行相加操作,然后再经过ReLU激活函数处理。这种结构使得网络可以学习到原始输入上的残差,进而提升了模型的性能。根据网络的深度,可以开发出不同类型的 ResNet,如 ResNet-50 或 ResNet-152。ResNet 末尾的数字表示网络的层数或网络的深度。我们可以使用 ResNet 的基本构件设计任意深度的 ResNet。ResNet 也可以看成是VGG 架构的升级版,它们之间的区别在于 ResNet 中使用了跳转连接。
下图给出了VGG-19和ResNet-34的架构图:
ResNet和VGG有很多相似之处,都大量的使用了3×3卷积,而ResNet在最后使用了全局平均池化层,这和我们之前介绍的GoogleNet一样。
ResNet的核心还是引入了残差块,每个残差块包含两个或三个3×3卷积层,其中至少有一个卷积层的输出与输入进行跳跃连接(skip connection)。这种跳跃连接允许信息直接在深层网络中跨层传递,避免了梯度消失和信息丢失。
下图总结了各种ResNet系列每层的输出大小以及结构中每个点的卷积核的尺寸。
为了和之前的各个网络比较,我们还是使用Fashion-MNIST数据集,相对而言数据量不大,为了节约时间,下面构建ResNet18为例
在ResNet-18中,每个残差快都包含2个3×3的卷积层
class Residual(nn.Module):
def __init__(self, input_channels, num_channels,
use_1x1conv=False, strides=1):
super().__init__()
self.conv1 = nn.Conv2d(input_channels, num_channels,
kernel_size=3, padding=1, stride=strides)
self.conv2 = nn.Conv2d(num_channels, num_channels,
kernel_size=3, padding=1)
if use_1x1conv:
self.conv3 = nn.Conv2d(input_channels, num_channels,
kernel_size=1, stride=strides)
else:
self.conv3 = None
self.bn1 = nn.BatchNorm2d(num_channels)
self.bn2 = nn.BatchNorm2d(num_channels)
def forward(self, X):
Y = F.relu(self.bn1(self.conv1(X)))
Y = self.bn2(self.conv2(Y))
if self.conv3:
X = self.conv3(X)
Y += X
return F.relu(Y)
ResNet的前两层跟之前介绍的GoogLeNet中的一样: 在输出通道数为64、步幅为2的卷积层后,接步幅为2的的最大汇聚层。 不同之处在于ResNet每个卷积层后增加了批量归一化层。
b1 = nn.Sequential(nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3),
nn.BatchNorm2d(64), nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1))
GoogLeNet在后面接了4个由Inception块组成的模块。 ResNet则使用4个由残差块组成的模块,每个模块使用若干个同样输出通道数的残差块。 第一个模块的通道数同输入通道数一致。在后面的每个残差块中,如果是第一个卷积层,则引用带有1×1的残差块
def resnet_block(input_channels, num_channels, num_residuals,
first_block=False):
blk = []
for i in range(num_residuals):
if i == 0 and not first_block:
blk.append(Residual(input_channels, num_channels,
use_1x1conv=True, strides=2))
else:
blk.append(Residual(num_channels, num_channels))
return blk
在后面增加所有残差块,这里所有的残差块都是2个卷积层
b2 = nn.Sequential(*resnet_block(64, 64, 2, first_block=True)) # *解包
b3 = nn.Sequential(*resnet_block(64, 128, 2))
b4 = nn.Sequential(*resnet_block(128, 256, 2))
b5 = nn.Sequential(*resnet_block(256, 512, 2))
#加入全连接层
net = nn.Sequential(b1, b2, b3, b4, b5,
nn.AdaptiveAvgPool2d((1,1)),
nn.Flatten(), nn.Linear(512, 10))
# 加载Fashion-MNIST数据集
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Resize((96,96)),
transforms.Normalize((0.5,), (0.5,))
])
trainset = torchvision.datasets.FashionMNIST(root='./data', train=True, download=True, transform=transform)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=128, shuffle=True, num_workers=2)
testset = torchvision.datasets.FashionMNIST(root='./data', train=False, download=True, transform=transform)
testloader = torch.utils.data.DataLoader(testset, batch_size=128, shuffle=False, num_workers=2)
Xavier初始化:防止梯度爆炸,这是CNN常用的操作,ResNet这种已经算比较深的网络而言,特别有效,之前我们也介绍过他的具体公式。
# Xavier初始化:防止梯度爆炸,这是CNN常用的操作
def init_weights(m):
if type(m) == nn.Linear or type(m) == nn.Conv2d: #对全连接层和卷积层初始化
nn.init.xavier_uniform_(m.weight)
net.apply(init_weights)
# 检查是否有可用的GPU
device = torch.device('cuda'if torch.cuda.is_available() else 'cpu')
model = net.to(device)
# 定义损失函数和优化器
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=0.05)
# 训练模型
num_epochs = 10
train_losses = []
test_losses = []
train_accs = []
test_accs = []
for epoch in range(num_epochs):
train_loss = 0.0
train_total = 0
train_correct = 0
model.train()
for images, labels in trainloader:
images, labels = images.to(device), labels.to(device)
optimizer.zero_grad()
outputs = model(images)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
train_loss += loss.item()
_, predicted = torch.max(outputs.data, 1)
train_total += labels.size(0)
train_correct += (predicted == labels).sum().item()
train_loss /= len(trainloader)
train_accuracy = 100*train_correct / train_total
train_losses.append(train_loss)
train_accs.append(train_accuracy)
test_loss = 0.0
test_total = 0
test_correct = 0
model.eval()
with torch.no_grad():
for images, labels in testloader:
images, labels = images.to(device), labels.to(device)
outputs = model(images)
loss = criterion(outputs, labels)
test_loss += loss.item()
_, predicted = torch.max(outputs.data, 1)
test_total += labels.size(0)
test_correct += (predicted == labels).sum().item()
test_loss /= len(testloader)
test_accuracy = 100*test_correct / test_total
test_losses.append(test_loss)
test_accs.append(test_accuracy)
print(f"Epoch {epoch+1}/{num_epochs}: Train Loss: {train_loss:.4f}, Train Acc: {train_accuracy:.2f}%, Test Loss: {test_loss:.4f}, Test Acc: {test_accuracy:.2f}%")
# 绘制训练误差和测试误差曲线
plt.figure(figsize=(10, 5))
plt.plot(range(1, num_epochs+1), train_losses, label='Train Loss')
plt.plot(range(1, num_epochs+1), test_losses, label='Test Loss')
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.title('Training and Testing Loss')
plt.legend()
plt.show()
# 绘制训练准确率和测试准确率曲线
plt.figure(figsize=(10, 5))
plt.plot(range(1, num_epochs+1), train_accs, label='Train Acc')
plt.plot(range(1, num_epochs+1), test_accs, label='Test Acc')
plt.xlabel('Epoch')
plt.ylabel('Accuracy')
plt.title('Training and Testing Accuracy')
plt.legend()
plt.show()
Epoch 1/10: Train Loss: 0.4473, Train Acc: 84.47%, Test Loss: 0.3017, Test Acc: 88.82%
Epoch 2/10: Train Loss: 0.2442, Train Acc: 90.99%, Test Loss: 0.3066, Test Acc: 88.10%
Epoch 3/10: Train Loss: 0.1862, Train Acc: 93.07%, Test Loss: 0.2971, Test Acc: 88.83%
Epoch 4/10: Train Loss: 0.1436, Train Acc: 94.71%, Test Loss: 0.4492, Test Acc: 85.70%
Epoch 5/10: Train Loss: 0.1071, Train Acc: 96.12%, Test Loss: 0.2633, Test Acc: 91.12%
Epoch 6/10: Train Loss: 0.0806, Train Acc: 97.14%, Test Loss: 0.2641, Test Acc: 91.58%
Epoch 7/10: Train Loss: 0.0575, Train Acc: 98.01%, Test Loss: 0.4005, Test Acc: 88.51%
Epoch 8/10: Train Loss: 0.0430, Train Acc: 98.51%, Test Loss: 0.3064, Test Acc: 91.63%
Epoch 9/10: Train Loss: 0.0278, Train Acc: 99.12%, Test Loss: 0.3101, Test Acc: 92.04%
Epoch 10/10: Train Loss: 0.0192, Train Acc: 99.40%, Test Loss: 0.4140, Test Acc: 91.00%
从结果可以看出,存在较严重的过拟合,但即便这样,测试集上的精度还是达到了0.92,比之前介绍的网络架构都更有效。
ResNet
架构通过引入残差模块和跳跃连接
来解决深层神经网络中的梯度问题,使得可以训练更深的网络。这种架构在计算机视觉任务中取得了很好的性能,并且对于大多数图像分类、目标检测和语义分割等任务都非常有效。其中,在残差网络中添加恒等映射并没有引入任何额外的参数。因此,网络的计算复杂度不会增加。随着深度的增加,ResNet 的准确率增益会更高。这产生的结果比之前的其他网络(例如 VGG 网络)要好得多。
阅读量:1580
点赞量:0
收藏量:0