23暑假深度学习week3

本文最后更新于:2023年7月30日 晚上

ResNet

深度残差网络(Deep residual network, ResNet)的提出是CNN图像史上的一件里程碑事件,让我们先看一下ResNet在ILSVRC和COCO 2015上的战绩:

ResNet取得了5项第一,并又一次刷新了CNN模型在ImageNet上的历史。ResNet的作者Kaiming He - FAIR也因此摘得CVPR2016最佳论文奖。

针对随着网络训练加深导致准确度下降的问题,ResNet提出了残差学习(Residual learning)方法来减轻训练深层网络的困难。在已有设计思路(BN, 小卷积核,全卷积网络)的基础上,引入了残差模块。每个残差模块包含两条路径,其中一条路径是输入特征的直连通路,另一条路径对该特征做两到三次卷积操作得到该特征的残差,最后再将两条路径上的特征相加。

残差模块如下图所示,左边是基本模块连接方式,由两个输出通道数相同的3x3卷积组成。右边是瓶颈模块(Bottleneck)连接方式,之所以称为瓶颈,是因为上面的1x1卷积用来降维(图示例即256->64),下面的1x1卷积用来升维(图示例即64->256),这样中间3x3卷积的输入和输出通道数都较小(图示例即64->64)。

残差模块

深度网络的退化问题

从经验来看,网络的深度对模型的性能至关重要,当增加网络层数后,网络可以进行更加复杂的特征模式的提取,所以当模型更深时理论上可以取得更好的结果。但是更深的网络其性能一定会更好吗?

实验发现深度网络出现了退化问题(Degradation problem):网络深度增加时,网络准确度出现饱和,甚至出现下降。这个现象可以在下图中直观看出来:56层的网络比20层网络效果还要差。这不会是过拟合问题,因为56层网络的训练误差同样高。我们知道深层网络存在着梯度消失或者爆炸的问题,这使得深度学习模型很难训练。但是现在已经存在一些技术手段如BatchNorm来缓解这个问题。因此,出现深度网络的退化问题是非常令人诧异的。

残差学习

深度网络的退化问题至少说明深度网络不容易训练。但是我们考虑这样一个事实:现在你有一个浅层网络,你想通过向上堆积新层来建立深层网络,一个极端情况是这些增加的层什么也不学习,仅仅复制浅层网络的特征,即这样新层是恒等映射(Identity mapping)。在这种情况下,深层网络应该至少和浅层网络性能一样,也不应该出现退化现象。好吧,你不得不承认肯定是目前的训练方法有问题,才使得深层网络很难去找到一个好的参数。

这个有趣的假设让何凯明灵感爆发,他提出了残差学习来解决退化问题。对于一个堆积层结构(几层堆积而成)当输入为$x$时其学习到的特征记为$H(x)$,现在我们希望其可以学习到残差$F(x)=H(x)-x$,这样其实原始的学习特征是$F(x)+x$。之所以这样是因为残差学习相比原始特征直接学习更容易。当残差为0时,此时堆积层仅仅做了恒等映射,至少网络性能不会下降,实际上残差不会为0,这也会使得堆积层在输入特征基础上学习到新的特征,从而拥有更好的性能。残差学习的结构如下图所示。这有点类似与电路中的“短路”,所以是一种短路连接(shortcut connection)。

残差单元

为什么残差学习相对更容易,从直观上看残差学习需要学习的内容少,因为残差一般会比较小,学习难度小点。不过我们可以从数学的角度来分析这个问题,首先残差单元可以表示为:

$y_l=F(x_l,W_i)+x_l$

$x_{l+1}=ReLu(y_l)$

其中$x_l$和$x_{l+1}$分别表示的是第$l$个残差单元的输入和输出,注意每个残差单元一般包含多层结构。$F$是残差函数,表示学习到的残差。基于上式,我们求得从浅层$l$到深层$L$的学习特征为:

$x_L=x_l+\sum_{i=1}^{L-1} F(x_i,W_i)$

利用链式规则,可以求得反向过程的梯度:

$\frac{\partial \text { loss }}{\partial x_{l}}=\frac{\partial l o s s}{\partial x_{L}} \cdot \frac{\partial x_{L}}{\partial x_{l}}=\frac{\partial \text { loss }}{\partial x_{L}} \cdot\left(1+\frac{\partial}{\partial x_{l}} \sum_{i=l}^{L-1} F\left(x_{i}, W_{i}\right)\right)$

式子的第一个因子$\frac{\partial l o s s}{\partial x_{L}}$表示的损失函数到达$L$的梯度,括号中的1表明短路机制可以无损地传播梯度,而另外一项残差梯度则需要经过带有weights的层,梯度不是直接传递过来的。残差梯度不会那么巧全为-1,而且就算其比较小,有1的存在也不会导致梯度消失。所以残差学习会更容易。要注意上面的推导并不是严格的证明。

ResNet使用两种残差单元,如下图所示。左图对应的是浅层网络,而右图对应的是深层网络。对于短路连接,当输入和输出维度一致时,可以直接将输入加到输出上。但是当维度不一致时(对应的是维度增加一倍),这就不能直接相加。

有两种策略:

  1. 采用zero-padding增加维度,此时一般要先做一个downsamp,可以采用strde=2的pooling,这样不会增加参数;
  2. 采用新的映射(projection shortcut),一般采用1x1的卷积,这样会增加参数,也会增加计算量。短路连接除了直接使用恒等映射,当然都可以采用projection shortcut。

残差单元

作者对比18-layer和34-layer的网络效果,如下图所示。可以看到普通的网络出现退化现象,但是ResNet很好的解决了退化问题。

对比

作者在[1603.05027] Identity Mappings in Deep Residual Networks (arxiv.org)中又对不同的残差单元做了细致的分析与实验,这里我们直接抛出最优的残差结构,如下图(b)所示。改进前后一个明显的变化是采用pre-activation,BN和ReLU都提前了。而且作者推荐短路连接采用恒等变换,这样保证短路连接不会有阻碍。

改进残差单元

ResNeXt

ResNeXt是ResNet和Inception的结合体,不同于Inception v4的是,ResNext不需要人工设计复杂的Inception结构细节,而是每一个分支都采用相同的拓扑结构。ResNeXt的本质是分组卷积(Group Convolution),通过变量基数(Cardinality)来控制组的数量。组卷机是普通卷积和深度可分离卷积的一个折中方案,即每个分支产生的Feature Map的通道数为$n(n>1)$。

Inception的模式:split-transform-merge

如下图所示,先将输入分配到多路,然后每一路进行转换,最后再把所有支路的结果融合。

Inception

Inception的缺点,太复杂了,人工设计的痕迹太重了。

然后,站得更高,分析了神经网络的标准范式就符合这样的split-transform-merge模式。以一个最简单的普通神经元为例(比如FC中的每个神经元):

神经元

就是先对输入的m个元素,分配到m个分支,进行权重加权,然后merge求和,最后经过一个激活。

基本结构

如下图,左边是ResNet的基本结构,右边是ResNeXt的基本结构:

对比

可以看到,旁边的residual connection就是公式中的x直接连过来,然后剩下的是32组独立的同样结构的变换,最后再进行融合,符合split-transform-merge的模式。

作者进一步指出,split-transform-merge是通用的神经网络的标准范式,前面已经提到,基本的神经元符合这个范式,而如下图所示:

ResNeXt最抽象

(a)是ResNeXt基本单元,如果把输出那里的1x1合并到一起,得到等价网络(b)拥有和Inception-ResNet相似的结构,而进一步把输入的1x1也合并到一起,得到等价网络(c)则和通道分组卷积的网络有相似的结构。

到这里,可以看到本文的野心很大,相当于在说,Inception-ResNet和通道分组卷积网络,都只是ResNeXt这一范式的特殊形式而已,进一步说明了split-transform-merge的普遍性和有效性,以及抽象程度更高,更本质一点。

猫狗大战

LeNet版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
!wget http://fenggao-image.stor.sinaapp.com/dogscats.zip
!unzip dogscats.zip

import torch

import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torchvision import datasets,transforms
import torchvision as tv
from torch.autograd import Variable
from torch.utils.data import DataLoader

from tqdm import tqdm, tqdm_notebook

# 设置全局参数
lr = 0.0001
batchsize = 64
epochs = 100
device = torch.device('cuda:1' if torch.cuda.is_available() else 'cpu')

# 数据预处理,数据增广
transform = transforms.Compose([
transforms.RandomResizedCrop(28),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])

])
transform_test = transforms.Compose([
transforms.RandomResizedCrop(28),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])

# 读取数据
dataset_train = datasets.ImageFolder(r'/content/dogscats/train', transform)
#训练集标签对齐cat对应0,dog对应1
for i in tqdm(range(len(dataset_train))):
str_tmp = dataset_train.imgs[i][0]
# print(str_tmp[24:27]+'\n')
if str_tmp[24:27]=='dog':
dataset_train.imgs[i]=(str_tmp,1)
else:
dataset_train.imgs[i]=(str_tmp,0)
dataset_test = datasets.ImageFolder('/content/dogscats/valid', transform_test)
# 对应文件夹的label
#训练集标签对齐cat对应0,dog对应1
for i in tqdm(range(len(dataset_test))):
str_tmp = dataset_test.imgs[i][0]
# print(str_tmp[24:27]+'\n')
if str_tmp[24:27]=='dog':
dataset_test.imgs[i]=(str_tmp,1)
else:
dataset_test.imgs[i]=(str_tmp,0)

# 导入数据
train_loader = torch.utils.data.DataLoader(dataset_train, batch_size=batchsize, shuffle=True)
test_loader = torch.utils.data.DataLoader(dataset_test, batch_size=batchsize, shuffle=False)

class LeNet(nn.Module):
def __init__(self):
super(LeNet, self).__init__()
self.conv1 = nn.Sequential( #input_size=(1*28*28)
nn.Conv2d(3, 6, 5, 1, 2), #padding=2保证输入输出尺寸相同
nn.BatchNorm2d(6),
nn.ReLU(), #input_size=(6*28*28)
nn.MaxPool2d(kernel_size=2, stride=2),#output_size=(6*14*14)
)
self.conv2 = nn.Sequential(
nn.Conv2d(6, 16, 5),
nn.BatchNorm2d(16),
nn.ReLU(), #input_size=(16*10*10)
nn.MaxPool2d(2, 2) #output_size=(16*5*5)
)
self.fc1 = nn.Sequential(
nn.Linear(400, 120),
nn.ReLU()
)
self.fc2 = nn.Sequential(
nn.Linear(120, 84),
nn.ReLU()
)
self.fc3 = nn.Linear(84, 2)

# 定义前向传播过程,输入为x
def forward(self, x):
x = self.conv1(x)
x = self.conv2(x)
# nn.Linear()的输入输出都是维度为一的值,所以要把多维度的tensor展平成一维
x = x.view(x.size()[0], -1)
x = self.fc1(x)
x = self.fc2(x)
x = self.fc3(x)
return x

model=LeNet().to(device)
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(),lr)

def train(model, device, train_loader, optimizer, epoch):
best_loss = 100
model.train()
sum_loss = 0
total_num = len(train_loader.dataset)
# print(total_num, len(train_loader))
for batch_idx, (data, target) in enumerate(train_loader):
data, target = Variable(data).to(device), Variable(target).to(device)
output = model(data)
loss = criterion(output, target)
optimizer.zero_grad()
loss.backward()
optimizer.step()
print_loss = loss.data.item()
sum_loss += print_loss
ave_loss = sum_loss / len(train_loader)
print('epoch:{}, loss:{}'.format(epoch, ave_loss))
if ave_loss < best_loss:
best_loss = ave_loss
# print('Find better model in Epoch {0}, saving model.'.format(epoch))
torch.save(model, '\model.pkl')#将最优模型保存

def val(model, device, test_loader):
model.eval()
with torch.no_grad():
correct = 0
total = 0
for images,labels in test_loader:
images = images.to(device)
labels = labels.to(device)
outputs = model(images)
_,predicted = torch.max(outputs.data,1)
total+=labels.size(0) #数据总数
correct +=(predicted == labels).sum().item() #总的准确个数
print('Acc:{}%'.format(100*correct/total))

for epoch in tqdm(range(1, epochs + 1)):
train(model, device, train_loader, optimizer, epoch)
val(model, device, test_loader)
torch.save(model, 'model.pth')

结果

ResNet版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
!wget http://fenggao-image.stor.sinaapp.com/dogscats.zip
!unzip dogscats.zip

import torch.optim as optim
import torch
import torch.nn as nn
import torch.nn.parallel
import torch.optim
import torch.utils.data
import torch.utils.data.distributed
import torchvision.transforms as transforms
import torchvision.datasets as datasets
import torchvision.models
from torch.autograd import Variable

# 设置超参数
BATCH_SIZE = 64
EPOCHS = 100
DEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

# 数据预处理
transforms = transforms.Compose([
transforms.RandomResizedCrop(224), # 图片将被整理成 224 × 224 × 3 的大小
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) # 归一化
])
# 读取数据
dataset_train = datasets.ImageFolder('/content/dogscats/train', transforms)
dataset_test = datasets.ImageFolder('/content/dogscats/valid', transforms)

# 导入数据
train_loader = torch.utils.data.DataLoader(dataset_train, batch_size=BATCH_SIZE, shuffle=True)
test_loader = torch.utils.data.DataLoader(dataset_test, batch_size=BATCH_SIZE, shuffle=False)
modellr = 0.0001

# 实例化模型并且移动到GPU
criterion = nn.CrossEntropyLoss()
model = torchvision.models.resnet18(weights=None)
num_ftrs = model.fc.in_features
model.fc = nn.Linear(num_ftrs, 2)
model.to(DEVICE)
# 选择简单暴力的Adam优化器,学习率调低
optimizer = optim.Adam(model.parameters(), lr=modellr)

# 定义训练过程
def train(model, device, train_loader, optimizer, epoch):
best_loss = 100
model.train()
sum_loss = 0
total_num = len(train_loader.dataset)
# print(total_num, len(train_loader))
for batch_idx, (data, target) in enumerate(train_loader):
data, target = Variable(data).to(device), Variable(target).to(device)
output = model(data)
loss = criterion(output, target)
optimizer.zero_grad()
loss.backward()
optimizer.step()
print_loss = loss.data.item()
sum_loss += print_loss
ave_loss = sum_loss / len(train_loader)
print('epoch: {}, loss: {}'.format(epoch, ave_loss))
if ave_loss < best_loss:
best_loss = ave_loss
torch.save(model, '\model.pkl')#将最优模型保存

def val(model, device, test_loader):
model.eval()
test_loss = 0
correct = 0
total_num = len(test_loader.dataset)
# print(total_num, len(test_loader))
with torch.no_grad():
for data, target in test_loader:
data, target = Variable(data).to(device), Variable(target).to(device)
output = model(data)
loss = criterion(output, target)
_, pred = torch.max(output.data, 1)
correct += torch.sum(pred == target)
print_loss = loss.data.item()
test_loss += print_loss
correct = correct.data.item()
acc = correct / total_num
avgloss = test_loss / len(test_loader)
print('Val set: Average loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format(
avgloss, correct, len(test_loader.dataset), 100 * acc))

# 训练
for epoch in range(1, EPOCHS + 1):
train(model, DEVICE, train_loader, optimizer, epoch)
val(model, DEVICE, test_loader)
torch.save(model, 'model.pth')

结果

思考

Residual learning 的基本原理?

Residual Learning(残差学习)是一种深度神经网络的学习方法,其基本原理是通过添加残差连接(residual connection)来改进传统的深度神经网络结构。传统的深度神经网络通常采用层次结构,即每一层接收前一层的输出,并将其作为输入传递给下一层。但是,这种结构在深度网络中容易产生梯度消失或梯度爆炸的问题,从而导致难以优化网络。

残差学习的基本思想是避免在深度网络中丢失信息的积累,通过添加残差连接,将输入信息直接传递到网络的输出层。这样,网络不仅学习到特征表示,还学习到输入信息的残差表示。

具体来说,残差连接将前向传播中的输入加到后续层的特征表示中,形式化表示为:

$y_l=F(x_l,W_i)+x_l$

$x_{l+1}=ReLu(y_l)$

其中$x_l$和$x_{l+1}$分别表示的是第$l$个残差单元的输入和输出,注意每个残差单元一般包含多层结构。$F$是残差函数,表示学习到的残差。残差连接的形式可以是直接连接、跳跃连接、卷积层等。

通过添加残差连接,残差学习能够更好地保留输入信息的特征,避免梯度消失或梯度爆炸的问题,从而提高网络的训练效率和泛化能力。

Batch Normailization 的原理,思考 BN、LN、IN 的主要区别。

Batch Normalization(批标准化)是一种深度神经网络的正则化技术,它通过对每一批数据中的每个神经元的输入进行标准化,来调整神经网络的权重和偏置,以加速训练并提高泛化能力。

具体来说,Batch Normalization是对每一批数据中的每个神经元的输入进行标准化,将其转换为零均值和单位方差的形式。这个过程通过计算每个神经元的均值和方差,使用一个缩放因子和一个平移因子对输入进行转换。这个转换可以表示为:

BN

Batch Normalization的作用是使网络更加稳定,可以减少梯度消失的问题,并提高模型的训练速度和泛化能力。它还可以减少模型对初始权重的敏感性,使得随机初始化后的权重能够更快地达到一个好的点,从而提高训练的效率。

Batch Normalization与Layer Normalization(层标准化)和Instance Normalization(实例标准化)的主要区别在于它们的计算方式和应用场景。Batch Normalization是针对每一批数据中的每个神经元进行标准化,需要存储大量的均值和方差参数,计算开销较大,不适合处理小批量数据。Layer Normalization是针对每一层神经元进行标准化,不需要存储大量参数,计算开销较小,适合处理小批量数据。Instance Normalization是针对每个输入样本进行标准化,不需要存储任何参数,计算开销最小,适合处理风格转换等任务。

总的来说,Batch Normalization是一种有效的正则化技术,通过标准化神经元的输入,可以提高网络的训练速度和泛化能力。但它需要存储大量参数和计算开销较大,不适合处理小批量数据。对于小批量数据,可以选择Layer Normalization或Instance Normalization来进行标准化。

为什么分组卷积可以提升准确率?即然分组卷积可以提升准确率,同时还能降低计算量,分数数量尽量多不行吗?

分组卷积可以提升准确率的原因是因为它能够减少模型的参数数量,从而提高模型的泛化能力。这是因为分组卷积可以将多个卷积层合并成一组卷积层,从而减少模型的参数量和计算量,同时保持模型的深度和宽度不变。

虽然分组卷积可以减少模型的参数量和计算量,但是它并不是将所有的卷积层都合并成一组卷积层。相反,它只是将多个卷积层合并成一组卷积层,从而减少参数量和计算量。这样,模型仍然具有足够的卷积层来提取特征和传递信息。


23暑假深度学习week3
https://jialiangz.github.io/2023/07/28/23暑假深度学习week3/
作者
爱吃菠萝
发布于
2023年7月28日
更新于
2023年7月30日
许可协议