文章目录
  1. 1. 工具
  2. 2. 实现
    1. 2.1. 初始要求
    2. 2.2. 问题A:预处理
    3. 2.3. 问题B:BASE模型
    4. 2.4. 问题C:Batch Normalization v.s BASE
    5. 2.5. 问题D: Dropout Layer
    6. 2.6. 问题E:SK model
    7. 2.7. 问题F:Change Number of channels
    8. 2.8. 问题G:Use different training set sizes
    9. 2.9. 问题H:Use different training sets
    10. 2.10. 问题I:Random Seed’s effects
    11. 2.11. 问题J:ReLU or Sigmoid?
  3. 3. 总结
  4. 4. References

本文缘起于一次CNN作业中的一道题,这道题涉及到了基本的CNN网络搭建,在MNIST数据集上的分类结果,Batch Normalization的影响,Dropout的影响,卷积核大小的影响,数据集大小的影响,不同部分数据集的影响,随机数种子的影响,以及不同激活单元的影响等,能够让人比较全面地对CNN有一个了解,所以想做一下,于是有了本文。

工具

实现

初始要求

首先建立基本的BASE网络,在Pytorch中有如下code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.conv1 = nn.Conv2d(1, 20, kernel_size=(5, 5), stride=(1, 1), padding=0)
self.conv2 = nn.Conv2d(20, 50, kernel_size=(5, 5), stride=(1, 1), padding=0)
self.fc1 = nn.Linear(4*4*50, 500)
self.fc2 = nn.Linear(500, 10)

def forward(self, x):
x = F.max_pool2d(self.conv1(x), 2)
x = F.max_pool2d(self.conv2(x), 2)
x = x.view(-1, 4*4*50)
x = F.relu(self.fc1(x))
x = self.fc2(x)
return F.log_softmax(x)

这部分代码见base.py

问题A:预处理

即要求将MNIST数据集按照规则读取并且tranform到适合处理的格式。这里读取的代码沿用了BigDL Python Support的读取方式,无需细说,根据MNIST主页上的数据格式可以很快读出,关键block有读取32位比特的函数:

1
2
3
def _read32(bytestream):
dt = numpy.dtype(numpy.uint32).newbyteorder('>') # 大端模式读取,最高字节在前(MSB first)
return numpy.frombuffer(bytestream.read(4), dtype=dt)[0]

读出后是(N, 1, 28, 28)的tensor,每个像素是0-255的值,首先做一下归一化,将所有值除以255,得到一个0-1的值,然后再Normalize,训练集和测试集的均值方差都已知,直接做即可。由于训练集和测试集的均值方差都是针对归一化后的数据来说的,所以刚开始没做归一化,所以forward输出和grad很离谱,后来才发现是这里出了问题。

这部分代码见preprocessing.py

问题B:BASE模型

将random seed设置为0,在前10000个训练样本上学习参数,最后看20个epochs之后的测试集错误率。最后结果为:

1
Test set: Average loss: 0.0014, Accuracy: 9732/10000 (97.3%)

可以看到,BASE模型准确率并不是那么的高。

问题C:Batch Normalization v.s BASE

在前三个block的卷积层之后加上Batch Normalization层,简单修改网络结构如下即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.conv1 = nn.Conv2d(1, 20, kernel_size=(5, 5), stride=(1, 1), padding=0)
self.conv2 = nn.Conv2d(20, 50, kernel_size=(5, 5), stride=(1, 1), padding=0)
self.fc1 = nn.Linear(4*4*50, 500)
self.fc2 = nn.Linear(500, 10)
self.bn1 = nn.BatchNorm2d(20)
self.bn2 = nn.BatchNorm2d(50)
self.bn3 = nn.BatchNorm1d(500)

def forward(self, x):
x = self.conv1(x)
x = F.max_pool2d(self.bn1(x), 2)
x = self.conv2(x)
x = F.max_pool2d(self.bn2(x), 2)
x = x.view(-1, 4*4*50)
x = self.fc1(x)
x = F.relu(self.bn3(x))
x = self.fc2(x)
return F.log_softmax(x)

同样的参数run一下,得出加了BN的结果为:

1
Test set: Average loss: 0.0009, Accuracy: 9817/10000 (98.2%)

由此可见,有明显的效果提升。
关于Batch Normalization的更多资料参见[2],[5]。

问题D: Dropout Layer

在最后一层即fc2层后加一个Dropout(p=0.5)后,在BASE和BN上的结果分别为:

1
2
BASE:Test set: Average loss: 0.0011, Accuracy: 9769/10000 (97.7%)
BN: Test set: Average loss: 0.0014, Accuracy: 9789/10000 (97.9%)

观察得知,dropout能够对BASE模型起到一定提升作用,但是对BN模型却效果不明显反而降低了。
原因可能在于,BN模型中本身即包含了正则化的效果,再加一层Dropout显得没有必要反而可能影响结果。

问题E:SK model

SK model: Stacking two 3x3 conv. layers to replace 5x5 conv. layer


如此一番改动后,搭建的SK模型如下:

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
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.conv1_1 = nn.Conv2d(1, 20, kernel_size=(3, 3), stride=(1, 1), padding=0)
self.conv1_2 = nn.Conv2d(20, 20, kernel_size=(3, 3), stride=(1, 1), padding=0)
self.conv2 = nn.Conv2d(20, 50, kernel_size=(3, 3), stride=(1, 1), padding=0)
self.fc1 = nn.Linear(5*5*50, 500)
self.fc2 = nn.Linear(500, 10)
self.bn1_1 = nn.BatchNorm2d(20)
self.bn1_2 = nn.BatchNorm2d(20)
self.bn2 = nn.BatchNorm2d(50)
self.bn3 = nn.BatchNorm1d(500)
self.drop = nn.Dropout(p=0.5)

def forward(self, x):
x = F.relu(self.bn1_1(self.conv1_1(x)))
x = F.relu(self.bn1_2(self.conv1_2(x)))
x = F.max_pool2d(x, 2)
x = self.conv2(x)
x = F.max_pool2d(self.bn2(x), 2)
x = x.view(-1, 5*5*50)
x = self.fc1(x)
x = F.relu(self.bn3(x))
x = self.fc2(x)
return F.log_softmax(x)

在20个epoch后,结果如下,

1
SK: Test set: Average loss: 0.0008, Accuracy: 9848/10000 (98.5%)

测试集准确率得到了少许的提高。
这里利用2个3x3的卷积核来代替大的5x5卷积核,参数个数由5x5=25变为了2x3x3=18。实践表明,这样使得计算更快了,并且小的卷积层之间的ReLU也很有帮助。
VGG中就使用了这种方法。

问题F:Change Number of channels

通过将特征图大小乘上一个倍数,再通过shell程序执行,得到如下结果:

1
2
3
4
5
SK0.2:  97.7%
SK0.5: 98.2%
SK1: 98.5%
SK1.5: 98.6%
SK2: 98.5% (max 98.7%)

在特征图分别为4,10, 30, 40时,最终的准确度基本是往上提升的。这在一定程度上说明,在没有达到过拟合前,增大特征图的个数,即相当于提取了更多的特征,提取特征数的增加有助于精度的提高。
这部分代码见SK_s.pyrunSK.sh

问题G:Use different training set sizes

同样通过脚本运行,增加参数

1
2
parser.add_argument('--usedatasize', type=int, default=60000, metavar='SZ',
help='use how many training data to train network')

表示使用的数据大小,从前往后取usebatchsize个数据。
这部分程序见SK_s.pyrunTrainingSize.sh
运行的结果如下:

1
2
3
4
5
6
7
500:   84.2%
1000: 92.0%
2000: 94.3%
5000: 95.5%
10000: 96.6%
20000: 98.4%
60000: 99.1%

由此可以明显地看出,数据越多,结果的精度越大。
太少的数据无法准确反映数据的整体分布情况,而且容易过拟合,数据多到一定程度效果也会不明显,不过,大多数时候我们总还是嫌数据太少,而且更多的数据获取起来也有一定难度。

问题H:Use different training sets

采用脚本完成,这部分程序见SK_0.2.pydiffTrainingSets.sh
运行结果如下:

1
2
3
4
5
6
    0-10000: 98.0%
10000-20000: 97.8%
20000-30000: 97.8%
30000-40000: 97.4%
40000-50000: 97.5%
50000-60000: 97.7%

由此可见,采用不同的训练样本集合训练出来的网络有一定的差异,虽不是很大,但是毕竟显示出了不稳定的结果。

问题I:Random Seed’s effects

采用runSeed.sh脚本完成,用到了全部60000个训练集。
运行的结果如下:

1
2
3
4
5
6
7
Seed      0:  98.9%
Seed 1: 99.0%
Seed 12: 99.1%
Seed 123: 99.0%
Seed 1234: 99.1%
Seed 12345: 99.0%
Seed 123456: 98.9%

事实上在用上整个训练集的时候,随机数生成器的种子设置对于最后结果的影响不大。

问题J:ReLU or Sigmoid?

将ReLU全部换成Sigmoid后,用全部60000个训练集训练,有对比结果如下:

1
2
   ReLU SK_0.2:  99.0%
Sigmoid SK_0.2: 98.6%

由此可以看出,在训练CNN时,使用ReLU激活单元比Sigmoid激活单元要更好一些。原因可能在于二者机制的差别,sigmoid在神经元输入值较大或者较小时,输出值会近乎0或者1,这使得许多地方的梯度几乎为0,权重几乎得不到更新。而ReLU虽然增加了计算的负担,但是它能够显著加速收敛过程,并且也不会有梯度饱和问题。

总结

本文利用PyTorch对几个CNN模型在MNIST数据集上的比较,以及一些参数的设置对模型效果的影响,从而对CNN的许多方面进行了一些详细的评估。
用过这么一次觉得PyTorch还是挺好用的,比较简单,其他模型不知道,反正卷积神经网络模型是如此。
项目具体代码见[7]。
由于笔者对CNN(卷积神经网络)研究不太深入,所以每个结果后的解释或有失偏颇,读者批判阅读即可。

References

深度学习 | Deep Learning