Pytorch实现ResNet18

前言:ResNet是计算机视觉做人工智能分类的有力工具,也是后续深度学习开发超深网络的基础。笔者使用了一周的时间从全连接网络学习到了CNN及ResNet,并且使用pytorch手写了一版ResNet18对CIFAR-10数据集的识别,将在本篇中记录学习的心得。

What’s ResNet

首先引用一张图来说明为什么会诞生ResNet及其用处

Why

图片摘自发明该技术的何凯明原论文

正如上图中左部分,普通的神经网络当深度加深时,在同一数据集上训练的效果变差;但是从直觉和理论的角度来说,当网络加深时,对复杂特征函数的表达将更好,起码不能变差。

造成该现象的原因是,神经网络是基于梯度训练的,存在着梯度爆炸梯度消失问题,而深度网络越深则反向传播时深层获得的梯度激励更小,就越难训练,也就是说训练效果变差。

而采用ResNet后,如上图中右部分,深度网络变得如我们预期的那样——更深的网络带来更好的效果。那么这是怎么回事呢?下图是组成ResNet的基本元件,叫做Residual Unit(残差单元)

ResUnit

精髓就是右边的那一根短路线,简单的理解就是对这个部分求导会得到一个(1+dF),所以梯度传播到了深层最起码也有个1在,减小了梯度消失出现的概率。看这个块可能还有点愣,那么看一下具体实现的ResNet吧!

ResNet

图片摘自发明该技术的何凯明原论文,由于网络可视化的图片太长,为方便阅读仅截取一部分(后面都是类似的结构)

左图是普通的34层CNN,右图是使用了Residual的34层ResNet。可以看出,变动的是右图中每两个卷积层并联了一个短路线(实际上为了计算相加的残差单元输出,短路线是一个用来同步特征大小和数量的1x1Conv)。

在原论文中,作者给出了实验出的ResNet网络结构,如下图共有5种不同深度的网络,现在已经成为了大家开发自己ResNet的教科书级参考网络。

AllResNet

图中作者的网络输入是224x224分辨率图像,实际开发中应该根据自己的输入对网络各结构进行适当调整。

设计给CIFAR10使用的ResNet18

接下来,我们针对Cifar-10数据集设计一个残差卷积网络,模型在作者的ResNet18基础上进行修改,在此我们也称修改后的网络为ResNet-18

Cifar-10数据集中有10个种类的图片数据,每张样本图片都是32x32的分辨率,所以我们设计的网络适配后如下图所示:

NetDesign

修改的部分首先是第一个特征扩展3x3Conv层,将原本的图像扩展至64特征图,由于图像分辨率小,所以去掉了原版网络中的池化层;以及最后的全连接层的输入特征数量和输出分类个数。

pytorch具体实现

基于上面的网络设计, 编写代码如下。先实现残差块子类_ResBlock,然后在此之上构建ResNet18模型;接着使用标准pytorch的接口导入数据集和SGD算法训练网络。

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
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
# -*- coding: utf-8 -*-

import torch
import torchvision
import torchvision.transforms as transforms

import torch.nn as nn
import torch.nn.functional as F

import torch.optim as optim

import matplotlib.pyplot as plt
import numpy as np

import torch.utils.tensorboard as tensorboard


'''
@参数设置
'''
BATCH_SIZE_N = 64

PRINT_EACH_N_BATCH = 200

'''
@gpu
'''
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
# device = torch.device("cpu")
print(f"device is:{device}")

'''
@定义网络
'''
class _ResBlock(nn.Module):
def __init__(self, channelIn:int, channelOut:int, ResStride:int=1):
'''
残差块初始化函数
'''
super(_ResBlock,self).__init__()

self.convLayer = nn.Sequential(
# 第一个卷积层,特征数量变换和大小调整
nn.Conv2d(channelIn,
channelOut,
3,
padding=1,
stride= ResStride),

# 卷积输出激活,正则化+ReLU
nn.BatchNorm2d(channelOut),
nn.ReLU(),

# 第二个卷积层
nn.Conv2d(channelOut, channelOut,
3,
padding = 1,
stride = 1),

# 卷积输出,正则化
nn.BatchNorm2d(channelOut)
)

# 短路直线
self.shortCut = nn.Sequential()
# 如果特征数量变了,或者图像大小变了,那短路支线要做相应变换
if channelIn != channelOut or ResStride != 1:
self.shortCut = nn.Sequential(
nn.Conv2d(channelIn, channelOut,
1,
stride=ResStride),
nn.BatchNorm2d(channelOut)
)

def forward(self,inputTensor):
return F.relu( self.convLayer(inputTensor)
+
self.shortCut(inputTensor) )

class ResNet18(nn.Module):
def __init__(self,Wid,Hig,Cha,OutClass):
super(ResNet18,self).__init__()

# 先做一次卷积,增加特征图数量;本例不做池化!!!
self.preConv = nn.Conv2d(Cha, 64, 3,padding=1) # 64特征,图片大小不变

# 两次64特征
# 两次128特征
# 两次256特征
# 两次512特征
self.ResBlockNet = nn.Sequential(
_ResBlock(64,64), # 特征数量不变(64->64),图像大小不变
_ResBlock(64,64),

_ResBlock(64,128,2), # 特征翻倍(128),图像小一倍(W/2,H/2)
_ResBlock(128,128), #

_ResBlock(128,256,2), # 特征翻倍,图像小一倍(W/4,H/4)
_ResBlock(256,256),

_ResBlock(256,512,2), # 特征翻倍,图像小一倍(W/8,H/8)
_ResBlock(512,512),

nn.AvgPool2d(int(Wid/8)) # 平均池化为512个 1x1 特征
)

# 全连接层
self.FullConnect = nn.Sequential(
nn.Linear(512, 128),
nn.Linear(128, OutClass)
)

def forward(self,inputImg):
out = self.preConv(inputImg) # 特征增加
out = self.ResBlockNet(out) # 残差块
out = out.view(-1,512)
out = self.FullConnect(out) # 全连接

return out

if __name__ == "__main__":

'''
@Tensorboard工具
'''
writer = tensorboard.SummaryWriter('./log/ResNet-CIFAR10/')

'''
@加载数据
'''

transform = transforms.Compose(
[transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])

trainset = torchvision.datasets.CIFAR10(root='./data',
train=True, # 训练集
download=True, transform=transform)

trainloader = torch.utils.data.DataLoader(trainset, batch_size=BATCH_SIZE_N,
shuffle=True, num_workers=2)

testset = torchvision.datasets.CIFAR10(root='./data',
train=False, # 非训练集
download=True, transform=transform)
testloader = torch.utils.data.DataLoader(testset, batch_size=BATCH_SIZE_N,
shuffle=False, num_workers=2)

# 分类标签
classes = ('plane', 'car', 'bird', 'cat',
'deer', 'dog', 'frog', 'horse', 'ship', 'truck')

'''
@展示数据
'''
dataiter = iter(trainloader)
images, labels = dataiter.next()

images = images/2 + 0.5;
for i in range(4):
plt.figure(i)
plt.title( str( classes[ labels[i] ]
))

img = images[i].numpy()
plt.imshow(np.transpose(img,(1,2,0)))

'''
@建立网络
'''
net = ResNet18(32,32,3,10)
net.to(device)

criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.8)

'''
@模型训练
'''
for epoch in range(8): # loop over the dataset multiple times

running_loss = 0.0
for i, data in enumerate(trainloader, 0):
# get the inputs; data is a list of [inputs, labels]
# inputs, labels = data
inputs, labels = data[0].to(device), data[1].to(device)

# zero the parameter gradients
optimizer.zero_grad()

# forward + backward + optimize
outputs = net(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()

# print statistics
running_loss += loss.item()
if i % 20 == 19: # print every 20 mini-batches
print('[%d, %5d] loss: %.3f' %
(epoch + 1, i + 1, running_loss / 20))
running_loss = 0.0

# 计算epoch准确度
correct = 0
total = 0
with torch.no_grad():
for data in testloader:
images, labels = data[0].to(device), data[1].to(device)
outputs = net(images)
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
print(f'{epoch+1}次epoch训练'+'准确度: %d %%' % (100 * correct / total) )

# 添加准确度记录值到本地Tensorboard
writer.add_scalar("准确度", 100 * correct / total, epoch)

print('Finished Training')

使用上代码训练8次epoch后就可得到77%的测试集准确率。

Resault

该工程中,使用SGD算法训练网络、ReLU激活函数,并采用手动调整学习率等方法,都是最简单的神经网络工具,笔者将在后续博客中探讨各种训练时的优化算法和自动学习率调整算法等。

Donate
  • Copyright: Copyright is owned by the author. For commercial reprints, please contact the author for authorization. For non-commercial reprints, please indicate the source.
  • Copyrights © 2022-2023 RY.J
  • Visitors: | Views:

请我喝杯咖啡吧~

支付宝
微信