深度学习——手动实现残差网络 辛普森一家人物识别
目标
通过深度学习,训练模型识别辛普森一家人动画中的14个角色
最终实现92%-94%的识别准确率。
数据
ResNet介绍
论文地址 https://arxiv.org/pdf/1512.03385.pdf
残差网络(ResNet)是微软亚洲研究院的何恺明、孙剑等人2015年提出的,它解决了深层网络训练困难的问题。利用这样的结构我们很容易训练出上百层甚至上千层的网络。
残差网络的提出,有效地缓解了深度学习两个大问题
-
梯度消失:当使用深层的网络时(例如> 100)反向传播时会产生梯度消。由于参数初始化一般更靠近0,这样在训练的过程中更新浅层网络的参数时,很容易随着网络的深入而导致梯度消失,浅层的参数无法更新。
-
退化问题:层数越多,训练错误率与测试错误率反而升高。举个例子,假设已经有了一个最优化的网络结构,是18层。当我们设计网络结构的时候,我们并不知道具体多少层次的网络时最优化的网络结构,假设设计了34层网络结构。那么多出来的16层其实是冗余的,我们希望训练网络的过程中,模型能够自己训练这16层为恒等映射,也就是经过这层时的输入与输出完全一样,这样子最终的结果和18层是一致的最优的。但是往往模型很难将这16层恒等映射的参数学习正确,那么就一定会不比最优化的18层网络结构性能好,所以随着网络深度增加,模型会产生退化现象。它不是由过拟合产生的,而是由冗余的网络层学习了不是恒等映射的参数造成的。
ResNet使用了一个新的思想,ResNet的思想是假设我们的网络,存在最优化的网络层次,那么往往我们设计的深层次网络是有很多网络层为冗余层的。那么我们希望这些冗余层能够完成恒等映射,保证经过该恒等层的输入和输出完全相同。
这是残差网络的基本单元,和普通的网络结构多了一个直接到达输出前的连线(shortcut)。开始输入的X是这个残差块的输入,F(X)是经过第一层线性变化并激活后的输出。在第二层输出值激活前加入X,这条路径称作shortcut连接,然后再进行激活后输出。
这个残差怎么理解呢?大家可以这样理解在线性拟合中的残差说的是数据点距离拟合直线的函数值的差,那么这里我们可以类比,这里的X就是我们的拟合的函数,而H(x)的就是具体的数据点,那么我通过训练使的拟合的值加上F(x)的就得到具体数据点的值,因此这 F(x)的就是残差了,还是画个图吧,如下图:
引用:https://blog.csdn.net/weixin_42398658/article/details/84627628
ResNet就是在网络中添加shortcut,来构成一个个的残差块,从而解决梯度爆炸和网络退化。
ResNet18网络结构
这是一个ResNet18的网络结构,在我的实现中,我根据这个结构搭建网络,并根据自己的实际情况进行调整。
引用:https://www.researchgate.net/figure/ResNet-18-model-architecture-10_fig2_342828449
项目思路
首先,ResNets利用恒等映射帮助解决渐变消失的问题。我开始尝试使用简单的MLP模型,例如一个输入层、一个输出层和三个卷积层。但是它的训练表现很差,只有45%的准确率。所以我需要更多的神经网络层,但是如果太多的神经网络层会导致梯度消失的问题。最后我想到了ResNet。
其次,网络深度决定了savedModel.pth(训练好的模型)的文件大小,该数据集总共有15,000张图像和14个类别,并不是很多。所以我选择了ResNet18,因为我们的数据集不是特别大。
而且不需要更深层次的网络模型。
ResNet18是一个卷积神经网络。它的架构有18层。它在图像分类中是非常有用和有效的。首先是一个卷积层,内核大小为3x3,步幅为1。
在标准的ResNet18模型中,这一层使用7*7的内核大小和步幅2。我在这里做了一些改变。
根据实际情况,因为原始模型中输入的文件大小是224 * 224,而我们的图像大小是64 * 64,7*7的内核大小对于这个任务来说太大了。标准ResNet18模型的精度为大约89%,而我修改的模型的准确率大约为94%。
输入层之后是由剩余块组成的四个中间层。ResNet的残余块是由两个33卷积层,包括一个shortcut,使用11卷积层直接添加的输入前一层到另一层的输出。最后,average pooling应用于的输出,将最终的残块和接收到的特征图赋给全连通层。
此外,模型中的卷积结果采用ReLu激活函数和归一化处理。
Data Transform:
为了减少过拟合,我使用图像变换进行数据增强。并对输入图像进行归一化处理。这样可以保证所有图像的分布是相似的,即在训练过程中更容易收敛,训练速度更快,效果更好。
我还尝试将图像的大小调整为224224,并在输入层使用77的卷积核,但我发现图像放大得太多,导致特征模糊,模型性能变差。
归一化:我使用下面的脚本来计算所有数据的均值和标准差。
data = torchvision.datasets.ImageFolder(root=student.dataset,
transform=student.transform('train'))
trainloader = torch.utils.data.DataLoader(data,
batch_size=1, shuffle=True)
mean = torch.zeros(3)
std = torch.zeros(3)
for batch in trainloader:
images, labels = batch
for d in range(3):
mean[d] += images[:, d, :, :].mean()
std[d] += images[:, d, :, :].std()
mean.div_(len(data))
std.div_(len(data))
print(list(mean.numpy()), list(std.numpy()))
超参数及其他设定
Epochs, batch_size, learning rate:
epochs = 120
, 如果太小,收敛可能不会结束。
batch_size = 256
, 如果batch_size太小,可能会导致收敛速度过慢或损失不会减少
lr = 0.001
,当学习率设置过大时,梯度可能会围绕最小值振荡,甚至无法收敛
Loss function:
torch.nn.CrossEntropyLoss()
是很适合图像作业的
Aptimiser:
我尝试过SGD
, RMSprop
, Adadelta
等,但Adam
是最适合我的。
Dropout and weight_decay: not use them
当我试图设置它们来减少过拟合问题时,效果并不好。这种设置使loss无法减少或精度降低。
代码
图像增强代码
def transform(mode):
"""
Called when loading the data. Visit this URL for more information:
https://pytorch.org/vision/stable/transforms.html
You may specify different transforms for training and testing
"""
if mode == 'train':
return transforms.Compose([
# transforms.Grayscale(num_output_channels=1),
transforms.RandomHorizontalFlip(),
transforms.RandomVerticalFlip(),
transforms.ToTensor(),
transforms.Normalize([0.42988312, 0.42988312, 0.42988312],
[0.17416202, 0.17416202, 0.17416202])
])
elif mode == 'test':
return transforms.Compose([
# transforms.Grayscale(num_output_channels=1),
transforms.RandomHorizontalFlip(),
transforms.RandomVerticalFlip(),
transforms.ToTensor(),
transforms.Normalize([0.42988312, 0.42988312, 0.42988312],
[0.17416202, 0.17416202, 0.17416202])
])
ResNet18手工搭建
class Network(nn.Module):
def __init__(self, num_classes=14):
super().__init__()
self.inchannel = 64
self.conv1 = nn.Sequential(
nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1, bias=False),
nn.BatchNorm2d(64),
nn.ReLU()
# nn.MaxPool2d(3, 1, 1)
)
self.layer1 = self.make_layer(ResBlock, 64, 2, stride=1)
self.layer2 = self.make_layer(ResBlock, 128, 2, stride=2)
self.layer3 = self.make_layer(ResBlock, 256, 2, stride=2)
self.layer4 = self.make_layer(ResBlock, 512, 2, stride=2)
self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
self.fc = nn.Linear(512, num_classes)
def make_layer(self, block, channels, num_blocks, stride):
layers = []
for i in range(num_blocks):
if i == 0:
layers.append(block(self.inchannel, channels, stride))
else:
layers.append(block(channels, channels, 1))
self.inchannel = channels
return nn.Sequential(*layers)
def forward(self, x):
out = self.conv1(x)
out = self.layer1(out)
out = self.layer2(out)
out = self.layer3(out)
out = self.layer4(out)
out = self.avgpool(out)
out = out.reshape(out.size(0), -1)
out = self.fc(out)
return out
class ResBlock(nn.Module):
def __init__(self, inchannel, outchannel, stride=1):
super(ResBlock, self).__init__()
# two 3*3 kenerl size conv layers
self.left = nn.Sequential(
nn.Conv2d(inchannel, outchannel, kernel_size=3, stride=stride, padding=1, bias=False),
nn.BatchNorm2d(outchannel),
nn.ReLU(inplace=True),
nn.Conv2d(outchannel, outchannel, kernel_size=3, stride=1, padding=1, bias=False),
nn.BatchNorm2d(outchannel)
)
self.shortcut = nn.Sequential()
if stride != 1 or inchannel != outchannel:
# shortcut,1*1 kenerl size
# shortcut,这里为了跟2个卷积层的结果结构一致,要做处理
self.shortcut = nn.Sequential(
nn.Conv2d(inchannel, outchannel, kernel_size=1, stride=stride, bias=False),
nn.BatchNorm2d(outchannel)
)
def forward(self, x):
out = self.left(x)
# 将2个卷积层的输出跟处理过的x相加,实现ResNet的基本结构
out = out + self.shortcut(x)
out = F.relu(out)
return out
参考
论文:
https://arxiv.org/pdf/1512.03385.pdf
参考博客:
https://www.cnblogs.com/gczr/p/10127723.html
https://blog.csdn.net/weixin_42398658/article/details/84627628