- Ubuntu 16.04
- Conda 4.5.13
- Python 2.7.13 / 3.6.3
- PyTorch 0.4.0 / 1.0.0
- CUDA Version 9.1.85
步骤如下:
-
使用torchvision加载并预处理CIFAR-10数据集
-
自定义卷积神经网络,结构如下图所示
class DaiNet7(nn.Module): # 定义卷积神经网络 def __init__(self): super(DaiNet7, self).__init__() self.layer1 = nn.Sequential( nn.Conv2d(3, 64, kernel_size = 3, padding = 1), # 输出 32*32*64 nn.ReLU(), nn.BatchNorm2d(64), nn.Dropout(0.1)) self.layer2 = nn.Sequential( nn.Conv2d(64, 64, kernel_size = 3, padding = 1), # 输出 32*32*64 nn.ReLU(), nn.BatchNorm2d(64), nn.MaxPool2d(2, 2), # 输出 16*16*64 nn.Dropout(0.1)) self.layer3 = nn.Sequential( nn.Conv2d(64, 128, kernel_size=3, padding=1), # 输出 16*16*128 nn.ReLU(), nn.BatchNorm2d(128)) self.layer4 = nn.Sequential( nn.Conv2d(128, 128, kernel_size = 3, padding=1), # 输出 16*16*128 nn.ReLU(), nn.BatchNorm2d(128), nn.MaxPool2d(2, 2), # 输出 8*8*128 nn.Dropout(0.5)) self.layer5 = nn.Sequential( nn.Conv2d(128, 256, kernel_size = 3, padding=1), # 输出 8*8*256 nn.ReLU(), nn.BatchNorm2d(256), nn.Dropout(0.5)) self.layer6 = nn.Sequential( nn.Conv2d(256, 256, kernel_size = 3, padding=1), # 输出 8*8*256 nn.ReLU(), nn.BatchNorm2d(256), nn.AvgPool2d(8, 8)) # 输出 1*1*256 self.fc1 = nn.Linear(1 * 1 * 256, 10) def forward(self, x): x = self.layer1(x) x = self.layer2(x) x = self.layer3(x) x = self.layer4(x) x = self.layer5(x) x = self.layer6(x) x = x.view(-1, 1 * 1 * 256) x = self.fc1(x) return x
-
定义损失函数和优化器
# 定义损失函数和优化器 criterion = nn.CrossEntropyLoss() # 损失函数为交叉熵,多用于多分类问题 optimizer = optim.Adam(net.parameters(), lr=args.lr, betas=(0.9, 0.99), weight_decay=5e-4)
-
训练网络并更新网络参数
-
测试网络
经过多种网络的搭建测试,迭代了大概八九个版本,最终网络结构如下:
-
修改CNN网络的定义形式 PyTorch中文文档
给的样例中的CNN网络定义形式如下:
class Net(nn.Module): def __init__(self): super(Net, self).__init__() self.conv1 = nn.Conv2d(3, 6, 5) self.pool = nn.MaxPool2d(2, 2) self.conv2 = nn.Conv2d(6, 16, 5) self.fc1 = nn.Linear(16 * 5 * 5, 120) self.fc2 = nn.Linear(120, 84) self.fc3 = nn.Linear(84, 10) def forward(self, x): x = self.pool(F.relu(self.conv1(x))) x = self.pool(F.relu(self.conv2(x))) x = x.view(-1, 16 * 5 * 5) x = F.relu(self.fc1(x)) x = F.relu(self.fc2(x)) x = self.fc3(x) return x
这种定义方式很简单,但是对于层数多一点的网络可能看起来就不那么直观了,所以后期使用了
nn.Sequential
将多个层写在一起,如下:self.layer1 = nn.Sequential( nn.Conv2d(3, 12, kernel_size = 5, padding=2), nn.Dropout(0.5), nn.BatchNorm2d(12), nn.ReLU(), nn.MaxPool2d(2, 2))
-
增加卷积核的个数(Feature Map通道个数增加),增加Dropout(0.5)随机失活
紫红色为train_accuracy、train_loss 蓝绿色为test_accuracy、test_loss epoch:200
test_accuracy可以达到59.68%,存在问题:test_loss比train_loss的值要小
-
增加了一层卷积,并且修改学习率,在上一个网络参数不变的情况下,将学习率由0.1调整为0.001
紫红色为train_accuracy、train_loss 蓝绿色为test_accuracy、test_loss epoch:200
学习率调整为0.001后 灰色为train_accuracy、train_loss 橙色为test_accuracy、test_loss
test_accuracy可以达到77.89%,test_loss与train_loss的差距不大
-
Data Augmentation数据增强
transform_train = transforms.Compose([ # 通过compose将各个变换串联起来 transforms.RandomCrop(32, padding=4), # 先四周填充0,再把图像随机裁剪成32*32 transforms.RandomHorizontalFlip(), # 以0.5的概率水平翻转给定的PIL图像 # transforms.RandomAffine(5.0), # python2.7 好像没有 # transforms.RandomGrayscale(p=0.1), # 依概率 p 将图片转换为灰度图 # transforms.ColorJitter(brightness=0.3, contrast=0.3, saturation=0.4, hue=0.4), transforms.ToTensor(), transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010)), #R,G,B每层的归一化用到的均值和方差 ])
这一部分代码有对数据进行了更多的处理,比如以0.5的概率水平翻转、RandomAffine仿射变换、RandomGrayscale转化为灰度图、ColorJitter修改亮度、对比度、饱和度等等,如上边代码所示,当我增加了三个注释行之后,效果好像变得更差了……所以后期又注释掉了。在最终版的DaiNet7上再次进行了测试,发现两者可达到的精度类似,没有显著的提高,同时也说明了数据集对于这两种变换不是很敏感。
-
按1中的方式整改网络结构,添加Batch Normalization 参考文章 参考文章2
def __init__(self): super(DaiNet1, self).__init__() self.layer1 = nn.Sequential( nn.Conv2d(3, 12, kernel_size = 5, padding=2), nn.Dropout(0.5), nn.BatchNorm2d(12), nn.ReLU(), nn.MaxPool2d(2, 2)) self.layer2 = nn.Sequential( nn.Conv2d(12, 24, kernel_size=3, padding=1), nn.BatchNorm2d(24), nn.ReLU(), # nn.Dropout(0.5), nn.MaxPool2d(2, 2)) self.layer3 = nn.Sequential( nn.Conv2d(24, 12, kernel_size=3, padding=1), nn.BatchNorm2d(12), nn.ReLU(), ## nn.Dropout(0.5), nn.MaxPool2d(2, 2)) self.fc1 = nn.Linear(12 * 4 * 4, 128) self.fc2 = nn.Linear(128, 84) self.fc3 = nn.Linear(84, 10)
这个时候,就出现了一个问题,BatchNorm2d、ReLU、Dropout、MaxPool2d等层的顺序及是否添加Dropout,添加Dropout后失活的概率设置为多少合适0.1 or 0.5
上边两张图应该是每个Sequential都设置Dropout时的曲线,蓝色为train,紫色为test epoch:200
下面两张图为取消了后两层Sequential的Dropout,调整了一些顺序之后,绿色为train,灰色为test
还有一些其他的图,由于文章篇幅有限不再展示,可以看出,调节顺序,增加或取消一些层,都有一些影响
test_accuary可以达到77.76%,test_loss与train_loss的差距不大
-
更改优化器,将SGD改为Adam 参考文章-pytorch中使用torch.optim优化神经网络以及优化器的选择
所有的优化器Optimizer都实现了step()方法来对所有的参数进行更新,
optimizer.step()
按理论上来说Adam要比SGD效果好一点,所以更换了优化器,但是没有存下训练和测试的对比图
Adam的特点有:
- 结合了Adagrad善于处理稀疏梯度和RMSprop善于处理非平稳目标的优点
- 对内存需求较小
- 为不同的参数计算不同的自适应学习率
- 也适用于大多非凸优化-适用于大数据集和高维空间
SGD 是最普通的优化器, 也可以说没有加速效果, 而 Momentum 是 SGD 的改良版, 它加入了动量原则. 后面的 RMSprop 又是 Momentum 的升级版. 而 Adam 又是 RMSprop 的升级版. 不过从下面结果中我们看到, Adam 的效果似乎比 RMSprop 要差一点. 所以说并不是越先进的优化器, 结果越佳. 在试验中可以尝试不同的优化器, 找到那个最适合数据/网络的优化器
-
是否添加softmax层
这个论述可以参考文章-pytorch 计算 CrossEntropyLoss 需要先经 softmax 层激活吗
简言之就是,在CrossEntropyLoss计算交叉熵时候,自己计算了一个softmax,所以不需要自己额外增加一层,否则就相当于是有两个softmax层
-
减少全连接层的数量
某篇博客中提到了在做CNN分类问题的时候,全连接层数量过多可能会导致效果不太好,所以将原来样例中的的3个全连接层减小为1个全连接层。
好像是因为:卷积层其实能够更多的体现图像的信息,而全连接层可能导致信息丢失
-
batch_size大小的调节 参考链接
一般来说,在合理的范围之内,越大的 batch size 使下降方向越准确,震荡越小;batch size 如果过大,则可能会出现局部最优的情况。小的 bath size 引入的随机性更大,难以达到收敛,极少数情况下可能会效果变好。
如上图,对于DaiNet7,分别使用
batch_size=128
和batch_size=1024
,其他参数保持不变,粉色和绿色(长的)分别为batch_size=1024
时的train和test,深红色和青蓝色(短的)分别为batch_size=128
时的train和test。当batch_size变大之后,train_loss变小了,但是test_loss变大了;train_accuracy变大了,但是test_accuracy变小了,即两个的Generlization Gap变大了。
-
激活函数 Sigmoid,Tanh,ReLu,softplus,softmax
因为上课老师讲到过ReLU会出现下图的这种情况,所以想到了换激活函数, 有一个Softplus函数,公式如下:$f(x) = log(1+e^x)$,如右图所示, 但是更换之后效果不是很明显,且增加了计算量,所以又换回了ReLU。
-
动态调整学习率Learning Rate
具体实现如下:
def adjust_learning_rate(optimizer, epoch):
if epoch < 50 :
lr = 0.001
elif epoch < 100 :
lr = 0.0005
elif epoch < 150 :
lr = 0.0003
elif epoch < 200 :
lr = 0.0001
elif epoch < 230 :
lr = 0.00001
else :
lr = 0.000001
for param_group in optimizer.param_groups:
param_group['lr'] = lr
print('\nlr:====>',lr)
for epoch in range(start_epoch, start_epoch+1000):
adjust_learning_rate(optimizer,epoch)
train(epoch)
test(epoch)
据查阅,pytorch有自带的学习率调节函数,但是我还是没有采用它,本实验中仍然使用的是上段代码中的if - else
判断,在下边的结果展示中,同样可以看到,accuracy曲线突然增高,即学习率发生改变
torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='min', factor=0.1, patience=10, verbose=True, threshold=0.0001, threshold_mode='rel', cooldown=0, min_lr=0, eps=1e-08)
- 该网络在
batch_size=128
的情况下,test_accuracy可以达到91.91%,如下图,紫色为train,绿色为test
- 该网络在
batch_size=1024
的情况下,结果在上一章节3.9 batch_size调节中有做对比,这里不在重复展示
- ResNet50在我的一些参数下,结果如下图
上图中,灰色的为train,橙色的为test,不得不说Resnet是真的强大,对于训练数据的拟合,train_accuracy可以达到99.94%,test_accuracy可以达到92.50%,不过这个结果并没有完全展示RestNet的实力,在网站看到有的博客可以使用ResNet达到95.5%左右的正确率,所以除了网络的结构,一些其他的参数对于结果也是比较重要的,比如学习率,图像增广等或者一些其他的Track。
ResNet50模型训练过程- ResNet50和我的网络Dainet7对比图所示
如上图所示,灰色和橙色分别为ResNet50的train和test,紫红色和绿色分别为DaiNet7的train和test,在train_accuracy方面两个有较大的差距,大概能差7个百分点左右,但是两者在test_accuracy方面相差不大,大概0.5个百分点左右,但是在训练速度方面,DaiNet7要比ResNet更占优势,在使用GTX 1080Ti显卡时,DaiNet7一个epoch平均大概需要23s左右,ResNet50则需要73s左右,两者的差距还是挺明显的。
Tensorboard 是 TensorFlow 的一个附加工具,可以记录训练过程的数字、图像等内容,以方便研究人员观察神经网络训练过程。 参考文章-详解PyTorch项目使用TensorboardX进行训练可视化
pip install tensorboardX
from tensorboardX import SummaryWriter
# Creates writer1 object.
# The log will be saved in 'runs/exp'
writer1 = SummaryWriter('runs/exp')
# Creates writer2 object with auto generated file name
# The log directory will be something like 'runs/Aug20-17-20-33'
writer2 = SummaryWriter()
# Creates writer3 object with auto generated file name, the comment will be appended to the filename.
# The log directory will be something like 'runs/Aug20-17-20-33-resnet'
writer3 = SummaryWriter(comment='resnet')
以上展示了三种初始化 SummaryWriter 的方法:
- 提供一个路径,将使用该路径来保存日志
- 无参数,默认将使用
runs/日期时间
路径来保存日志 - 提供一个
comment
参数,将使用runs/日期时间-comment
路径来保存日志
接下来,我们就可以调用 SummaryWriter 实例的各种 add_something 方法向日志中写入不同类型的数据了:
from tensorboardX import SummaryWriter
writer = SummaryWriter('runs/scalar_example')
for i in range(10):
writer.add_scalar('quadratic', i**2, global_step=i)
writer.add_scalar('exponential', 2**i, global_step=i)
writer2 = SummaryWriter('runs/another_scalar_example')
for i in range(10):
writer2.add_scalar('quadratic', i**3, global_step=i)
writer2.add_scalar('exponential', 3**i, global_step=i)
想要在浏览器中查看可视化这些数据,只要在命令行中开启tensorboard
即可:
tensorboard --logdir <your_log_dir>
# 例如:
tensorboard --logdir runs
其中的<your_log_dir>
既可以是单个run
的路径,如上面writer1
生成的 runs/exp
;也可以是多个run
的父目录,如runs/
下面可能会有很多的子文件夹,每个文件夹都代表了一次实验,我们令--logdir=runs/
就可以在 tensorboard 可视化界面中方便地横向比较runs/
下不同次实验所得数据的差异。
import torch
import torch.nn as nn
import torch.nn.functional as F
import torchvision
from torch.autograd import Variable
from tensorboardX import SummaryWriter
dummy_input = (torch.zeros(1, 3),)
# torch.rand(1, 3, 32, 32) 在DaiNet7中使用的
class LinearInLinear(nn.Module):
def __init__(self):
super(LinearInLinear, self).__init__()
self.l = nn.Linear(3, 5)
def forward(self, x):
return self.l(x)
with SummaryWriter(comment='LinearInLinear') as w:
w.add_graph(LinearInLinear(), dummy_input, True)
参考Github官方样例链接
- 绘制Graph如下图所示(图片太长 为方便添加 旋转90°)
- 样例运行时候报错找不到models
python main.py
Traceback (most recent call last):
File "main.py", line 16, in <module>
from models import *
ImportError: bad magic number in 'models': b'\x03\xf3\r\n'
# 解决办法
cd models
ls -a # 会看到一些pyc文件
rm *.pyc
在定义卷积神经网络的时候,需要计算一下每层的数据维度,保证各个连接起来的时候,大小正确且一致
......
self.layer6 = nn.Sequential(
nn.Conv2d(256, 256, kernel_size = 3, padding=1),
nn.ReLU(),
nn.BatchNorm2d(256),
nn.AvgPool2d(8, 8)
)
self.fc1 = nn.Linear(1 * 1 * 256, 10) # 这里的维度
......
x = x.view(-1, 1 * 1 * 256) # 还有这里的维度 需要计算正确
- train很稳定,但是test忽大忽小,波动很大
如下图,没找到问题的原因,不过后来直接换网络了,也就没这个问题了