本节将基于CIFAR-10
数据,阐述卷积神经网络的构建过程。之所以选择CIFAR-10
数据,是因为其数据集很小,而且其中的图片也很小,方便开发阶段的快捷测试。
classes = ('plane', 'car', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck')
stats = (np.array([ 0.4914 , 0.48216, 0.44653]), np.array([ 0.24703, 0.24349, 0.26159]))
def get_data(sz,bs):
tfms = tfms_from_stats(stats, sz, aug_tfms=[RandomFlipXY()], pad=sz//8)
return ImageClassifierData.from_paths(PATH, val_name='test', tfms=tfms, bs=bs)
bs=256
其中classes
是CIFAR-10
数据的分类;stats
是图像数据的三个通道的均值和标准差。之前我们生成数据时,往往需要构造符合某个预先训练好的模型的数据,调用的函数时tfms_from_model()
,这个函数会使用预训练的模型里的参数,对数据做相应的归一化处理。而现在我们从零开始训练网络,需要实现这个处理步骤。具体而言,就是将stats
参数传入tfms_from_stats()
函数中。该函数的后面两个参数:aug_tfms
指明了对数据所做的修饰,RandomFlipXY()
为随机的水平翻转,pad
为对数据的周边进行补零填充。
class SimpleNet(nn.Module):
def __init__(self, layers):
super().__init__()
self.layers = nn.ModuleList([
nn.Linear(layers[i], layers[i + 1]) for i in range(len(layers) - 1)])
def forward(self, x):
x = x.view(x.size(0), -1)
for l in self.layers:
l_x = l(x)
x = F.relu(l_x)
return F.log_softmax(l_x, dim=-1)
SimpleNet
对象中layers
成员是一个nn.ModuleList
实例,这是Pytorch
的规则:定义一个网络层序列,需要将之放在nn.ModuleList
对象中。在self.layers
中,按照每层的输入输出尺寸生成nn.Linear
实例。而在SimpleNet
的前向传播函数forward()
中,使用nn.Linear
对象对输入向量x
进行线性加权,然后使用ReLU
做非线性操作,最后使用log_softmax()
实现输出。
以上述定义模型的实例和数据构造分类器:
learn = ConvLearner.from_model_data(SimpleNet([32*32*3, 40,10]), data)
这一结构的神经网络大概需要12万个参数:
(32∗32∗3∗40+40)+(40∗10+10) ( 32 ∗ 32 ∗ 3 ∗ 40 + 40 ) + ( 40 ∗ 10 + 10 ) ,其中加数40和10是隐藏层的偏置项。
最终可以得到的分类准确率约为46%
。
将全连接层替换为卷积层:
class ConvNet(nn.Module):
def __init__(self, layers, c):
super().__init__()
self.layers = nn.ModuleList([
nn.Conv2d(layers[i], layers[i + 1], kernel_size=3, stride=2)
for i in range(len(layers) - 1)])
self.pool = nn.AdaptiveMaxPool2d(1)
self.out = nn.Linear(layers[-1], c)
def forward(self, x):
for l in self.layers: x = F.relu(l(x))
x = self.pool(x)
x = x.view(x.size(0), -1)
return F.log_softmax(self.out(x), dim=-1)
其中,全连接网络中的nn.Linear
层替换成了nn.Conv2d
层;self.pool
为输出层之前的池化层,nn.AdaptiveMaxPool2d(1)
表示池化为1个数值;最终输出层会输出c
个类别。
生成分类器:
learn = ConvLearner.from_model_data(ConvNet([3, 20, 40, 80], 10), data)
分类器参数个数约为3万个:
(3∗3∗3∗20+20)+(3∗3∗20∗40+40)+(3∗3∗40∗80+80)+(80∗10+10) ( 3 ∗ 3 ∗ 3 ∗ 20 + 20 ) + ( 3 ∗ 3 ∗ 20 ∗ 40 + 40 ) + ( 3 ∗ 3 ∗ 40 ∗ 80 + 80 ) + ( 80 ∗ 10 + 10 ) 。经训练后,可获得60%
的准确率。
BN
,Batch Normalization
)考虑输入数据。事实上,在其传递给输出层时,我们会做归一化,使数据变为均值为0、方差为1的分布。这是由于大部分机器学习算法都假设数据服从独立同分布,而且大部分机器学习算法都是基于标准正态分布来分析的。考虑隐含层的输入。由于层层操作,隐含层的输入已经偏离这一假设;更不利的是,隐含层的系数矩阵可能使得每层的输出越来越大或越来越小。
针对这一问题,Batch Normalization
方法被提出。一个简单的版本是使每一层的输出的均值为零、方差为1。然而,这样做的效果不大。因为下一层在训练时,可能又使得其偏离所预想的数值范围。这其实是网络在反抗过分预设的条件。为了不让网络过分淘气,又不至于让网络信马由缰的瞎搞参数,我们额外增加两个参数m
、a
,作为BN
之后数据的标准差和均值。可以这样理解:如果网络想要调整参数矩阵造成的数据差异化(方差)和数据漂移(均值),就让它去调整m
和a
,而不是去调整整个参数矩阵。从这个角度来看,这也是一种正则化(Regularization
,即使得参数不过度复杂)。因此,采用BN
方法的网络,可以省略随机丢弃(Drop Out
)和权值衰减(weight decay
)。
既然BN
可以看做一种正则化方法,其和Drop Out
一样,应设置为只在训练时有效,而在泛化时无效。在Pytorch中,可以通过nn.Module
(注意:自定义的BN
层是派生自这一对象的)对象的training
参数判断是否处在训练过程中。自定义的BN
层实现代码如下:
class BnLayer(nn.Module):
def __init__(self, ni, nf, stride=2, kernel_size=3):
super().__init__()
self.conv = nn.Conv2d(ni, nf, kernel_size=kernel_size,
stride=stride, bias=False, padding=1)
self.a = nn.Parameter(torch.zeros(nf,1,1))
self.m = nn.Parameter(torch.ones(nf,1,1))
def forward(self, x):
x = F.relu(self.conv(x))
x_chan = x.transpose(0,1).contiguous().view(x.size(1), -1)
if self.training:
self.means = x_chan.mean(1)[:,None,None]
self.stds = x_chan.std (1)[:,None,None]
return (x-self.means) / self.stds *self.m + self.a
ResNet
)事实上,即使引入BN
策略,当网络层数达到一定的数目后,网络的性能就会达到饱和,再增加网络深度反而会引起性能下降。而采用残差网络的架构,可以有效增加网络层数,且能获得较好的性能。
残差网络派生自BnLayer
,只不过是将BnLayer
中的前向传播函数的返回值更改为return x + super().forward(x)
,即我们将预测值 y y 表示为 y=x+f(x) y = x + f ( x ) 。原先直接预测 y y ,现在改为预测差值 f(x)=y−x f ( x ) = y − x 。
在Dogs vs. Cats
分类网络中,我们采用ResNet34
的网络结构,调用Fast.AI的API生成了二分类网络。那么Fast.AI对已有的网络结构做了哪些调整呢?
事实上,ResNet34
网络的最后一层,是利用512维特征进行1000类的划分,与Dogs vs. Cats
的应用场景不符(只需两类),因此这一层将会被删除。另外,ResNet34
的倒数第二层是池化层,而我们若要修改输出特征数目,也需删除这一层。
在删除上述两层后,Fast.AI
增加了一个卷积层、池化层、输出层。其中卷积层将512维特征进一步压缩至2维。而在ResNet34
的网络层被锁定的情形下,需要训练的就是这个卷积层。