一、环境配置过程
Ubuntu18.04 + GeForce GTX 1050 + Anaconda + Pycharm + CUDA10.0 + cudnn 7.6.5。选择在Ubuntu下是因为在这里配置环境真的比Windows方便太多。
1.1安装Anaconda
Anaconda能很好很方便的配置环境,比如说便捷获取包,并且把配置的不同环境按需要隔离开来,更利于管理,可以说是配置深度学习环境必备武器。
1.在清华大学开源软件镜像站下载 Anaconda3-5.2.0-Linux-x86_64.sh
2.安装过程
cd Downloads/
bash Anaconda3-5.2.0-Linux-x86_64.sh
注意在该过程中有一个加到环境变量的选项,若安装成功后在终端输入 python,仍然会显示Ubuntu自带的python版本,可以直接修改./bashrc
。
sudo gedit ~/.bashrc
export PATH="/home/lynn/anaconda3/bin:$PATH"
source ~/.bashrc
在激活虚拟环境的时候可能会报错CommandNotFoundError: Your shell has not been properly configured to use 'conda activate'
,我尝试可以把激活指令换成 source activate
是可以成功激活,但这治标不治本,所以我继续查阅资料,发现可以直接修改./bashrc
后继续使用conda activate
。
sudo gedit ~/.bashrc
source ~/anaconda3/etc/profile.d/conda.sh
source ~/.bashrc
1.2安装显卡驱动
其实以前也在Ubuntu和Windows下装过显卡驱动,但是特别复杂,需要关闭图形界面等等操作,而且安装后还会出现循环登陆的情况,可以说以前的方法稍有不慎就崩了。这一次安装让我收获了一个特别厉害的方法,可能也是因为这是一个非常干净的Ubuntu系统,这次安装十分顺利,操作简单,并且目前看来没有任何异常。
1.识别显卡和查看推荐显卡驱动
ubuntu-drivers devices
这里我可以看到对于我的电脑nvidia-driver-465
是推荐的(这个是我已经把存储库加好了的)。
2.添加存储库ppa:graphics-drivers/ppa
到系统中
sudo add-apt-repository ppa:graphics-drivers/ppa
sudo apt install nvidia-driver-465
3.重启&&检查
sudo reboot
nvidia-smi
nvidia-settings
1.3安装cuda10.0和cudnn
在这个安装过程中我的收获是,其实很多东西别人官方都给出方法了,不要害怕是英文的就不去看,在有问题的时候,应该以官方为准。还有就是cuda、cudnn以及后续要安装的tensorflow-gpu有对应关系,一定要查阅官方文档确认。
当然在安装完成后,我发现其实Anaconda中安装tensorflow-gpu能够直接把cuda、cudnn安装好对应版本。我后来查阅到cudatoolkit只是Nvidia安装的cuda的部分文件, 为了让普通需要cuda的程序能够在系统没有安装cuda的时候也能运行,所以还是不得不再一次感慨Anaconda太强悍了!
1.安装和验证
cuda:执行成功后会出现很多选择,没什么大坑,按照字面意思判断即可。
sudo sh cuda_10.0.130_410.48_linux.run
nvcc -V
cudnn:对于下载好的对应系统的library:
sudo dpkg -i libcudnn7_7.6.5.32-1+cuda10.0_amd64.deb
sudo dpkg -i libcudnn7-dev_7.6.5.32-1+cuda10.0_amd64.deb
sudo dpkg -i libcudnn7-doc_7.6.5.32-1+cuda10.0_amd64.deb
利用下载时同时下载的例子来验证cuda+cudnn:
cd cudnn_samples_v7/mnistCUDNNmake clean && make./mnistCUDNN
2.错误排查
在安装cuda时报错显示Missing recommended library:
1)运行以下指令,并重新安装:
sudo apt-get install freeglut3-dev build-essential libx11-dev libxmu-dev libxi-dev libgl1-mesa-glx libglu1-mesa libglu1-mesa-devsudo sh cuda_10.0.130_410.48_linux.run
2)如果还是不成功,则需要配置环境变量:
sudo gedit ~/.bashrcexport PATH=$PATH:/usr/local/cuda-10.0/binexport LD_LIBRARY_PATH=/usr/local/cuda-10.0/lib64:$LD_LIBRARY_PATHsource ~/.bashrc
1.4创建虚拟环境ai
1.对于GPU版本:conda create --name ai tensorflow-gpu=1.14.0 python=3.6
查阅官方资料后知道tensorflow-gpu=1.14.0
与CUDA10.0 cudnn7.6.5
是兼容的。
2.安装相关包:
部分安装情况如下,具体还需要看代码需要什么则加什么
二、网络结构
2.1卷积神经网络基本知识:
正所谓磨刀不误砍柴工,以前学习卷积神经网络对很多概念都理解的是个大概,并且因为不怎么接触,所以也遗忘了很多。这一部分知识的学习帮助我更快地熟悉卷积神经网络结构,同时也让我对后续参数的选择有了更具体的认识。
2.1.1正向传播过程:
1)全连接层:许许多多神经元连接得来的,主要是起到分类器的作用。它将之前卷积层输出的特征映射到样本标记空间。在接下来的网络结构中最后一层可以得到很直观的认识,因为Fahion-Mnist有10类,最后一层全连接我们就是给10个节点。
2)卷积层:卷积就是用一个滑动窗口在图上滑动计算,目的是用于进行图像特征提取,具有局部感知、权值共享的特性,权值共享会减小网络的参数负担。
·输出的特征矩阵的channel(或者可以理解为厚度)由卷积核个数决定。vgg16中就是经历了从64-128-256-512。
·在滑动卷积核时,出现越界可以选择padding进行补零,vgg16中选择了该方法。
3)池化层:对特征图进行稀疏处理,减少数据运算量。特点是没有训练参数、不改变channel,一般pool_size(池化核)和stride(步距)一样,这样可以直接将特征矩阵的长宽缩小为1/2。
4)感受野:卷积神经网络特征所能看到输入图像的区域,这一概论主要是为了后续分析vgg中堆叠卷积核的效果,计算公式如下:
F
(
i
)
=
(
F
(
i
+
1
)
−
1
)
∗
S
t
r
i
d
e
+
K
s
i
z
e
F(i) = (F(i+1) - 1 )*Stride + Ksize
F(i)=(F(i+1)−1)∗Stride+Ksize
其中,F(i)为第i层感受野,Stride为步距,Ksize为卷积核和池化核大小。
2.1.2反向传播过程:
1)误差的计算:这里主要了解的是vgg16中使用的交叉熵损失,还要注意的是针对Fahion-Mnist的多分类问题(softmax输出)定义的公式如下所示:
H
=
−
∑
i
o
i
∗
l
o
g
(
o
i
)
H=-\sum\nolimits_{i}o_i^*log(o_i)
H=−∑ioi∗log(oi)
2)误差的反向传播:误差对每一个节点的损失梯度(grad)。
3)权重的更新:主要是利用刚刚算出的损失梯度进行更新。
w
n
e
w
=
w
o
l
d
−
l
r
∗
g
r
a
d
w_{new}=w_{old}-lr*grad
wnew=wold−lr∗grad
注意:、理论上这样会指向全局最优,但前提是我们使用整个样本集求解梯度损失,但是考虑到现实情况我们只能分批次进行训练,求取当前批次的最优。这也就引入了后续要设计的batch_size参数和优化器。
·SGD :就是最简单分批次训练,容易局部最优,受样本噪声影响大。
·SGD+Momentum:在SGD中,gradient类比成速度,learning rate类比成时间,不仅要看当前时所在位置的速度向量,还要看上一步的速度(梯度)。
·Adam:引入一阶动量/二阶动量。
2.2VGG16_bn网络结构:
VGG提出了可以通过重复使用简单的基础块来构建深度模型的思路。论文提到,可以通过堆叠两个3x3的卷积核来替代5x5的,3个3x3的卷积核替代7x7的,它们有相同的感受野,同时还可以减小参数,这也就是这个网络的一大亮点。
利用上述卷积神经网络的基本知识,能更加直观的理解论文中的推导:
1)利用感受野计算公式,考虑3层3x3卷积核(stride=1,ksize=3):
C
o
n
v
3
✖
3
(
3
)
=
(
F
(
1
−
1
)
∗
1
+
3
=
3
C
o
n
v
3
✖
3
(
2
)
=
(
F
(
3
−
1
)
∗
1
+
3
=
5
C
o
n
v
3
✖
3
(
1
)
=
(
F
(
5
−
1
)
∗
1
+
3
=
7
Conv3✖3(3) = (F(1 - 1 )*1+ 3 = 3 \\Conv3✖3(2) = (F(3 - 1 )*1+ 3 = 5 \\Conv3✖3(1) = (F(5 - 1 )*1+ 3 = 7
Conv3✖3(3)=(F(1−1)∗1+3=3Conv3✖3(2)=(F(3−1)∗1+3=5Conv3✖3(1)=(F(5−1)∗1+3=7
2)计算在输入输出channel均为C的情况下,3层3x3与1层7x7卷积核所需参数:
C
o
n
v
3
✖
3
:
3
∗
3
∗
C
∗
C
+
3
∗
3
∗
C
∗
C
+
3
∗
3
∗
C
∗
C
=
27
C
2
C
o
n
v
7
✖
7
:
7
∗
7
∗
C
∗
C
=
49
C
2
Conv3✖3:3*3*C*C + 3*3*C*C +3*3*C*C = 27C^2 \\Conv7✖7:7*7*C*C = 49C^2
Conv3✖3:3∗3∗C∗C+3∗3∗C∗C+3∗3∗C∗C=27C2Conv7✖7:7∗7∗C∗C=49C2
同时论文还分别给出了VGG(A-E)网络基本结构,本文采用的是在VGG16的基础上进行微调的VGG16_bn,它相对于前者网络结构基本一样,只是添加了用于防止过拟合的dropout层和BatchNormalization。
2.2.1VGG块
组成规律是:连续使用多个相同的padding为1,窗口形状为3×3的卷积层,后接一个步幅为2,窗口形状为2×2的最大池化层。
VGG中,conv的stride为1,padding为1,它将不会改变输出特征矩阵的长宽;maxpool的size为2,stride为2,它会将输出的特征矩阵的长宽都降低一半。
2.2.2VGG网络
现在介绍我们使用的VGG16_bn网络,它由卷积层模块后接全连接层模块构成,使用了13个卷积层和3个全连接层:它的输入层部分是28x28x3的RGB图像;它的卷积层部分串联了数个VGG块,具体来看是有5个卷积块,前2块使用双卷积层,而后3块使用三卷积层。第一块的输出通道是64,之后每次对输出通道数翻倍,直到变为512。同时正如之前所说的,每一块卷积之后增加了dropout层;它的全连接层中前两个神经元个数为512,剩下的全连接层神经元个数则是根据Fashion-Mnist数据集中分类个数决定的10个;最后部分则是由softmax的输出层。层与层之间使用最大化池分开,所有隐层的激活函数都采用relu。
对于网络中的参数相较于论文原版给出的参考值,本文针对所使用的数据集、训练设备等情况进行了修改,参数修改后具体如下图所示,选择的原理将在第三部分介绍。
三、参数说明
3.1卷积层部分:
之前介绍了VGG网络中很有特点的就是VGG块,它的复用性还是比较高的,并且不同块之间相似度也很高,本文采用的VGG16_bn网络的每个块中改变的就是卷积层数(2或3)和卷积个数(64或128或256或512)。因此着重理解部分,很容易就能推广到所有。
1.以第一个卷积块,即双层3✖3的64个卷积核为基础重点讲解。
self.c1 = Conv2D(filters=64, kernel_size=(3, 3), padding='same') # 卷积层1self.b1 = BatchNormalization() # BN层1self.a1 = Activation('relu') # 激活层1self.c2 = Conv2D(filters=64, kernel_size=(3, 3), padding='same', )self.b2 = BatchNormalization() # BN层1self.a2 = Activation('relu') # 激活层1self.p1 = MaxPool2D(pool_size=(2, 2), strides=2, padding='same')self.d1 = Dropout(0.2) # dropout层
1)Conv2D(filters=64, kernel_size=(3, 3), padding='same')
这个部分就是根据网络结构搭建的,即是3✖3的卷积核,共有64个,再加上padding的操作,就能够使得每一个卷积层与前一层保持相同的宽和高。需要注意的是,这里padding='same'
的参数,tensorflow中有’same’和’valid’。我们在卷积的时候卷积核的移动往往会跳出图片或者丢弃一小部分像素点。'same'
这种padding方法就是为了保证输入和输出的结构一致而进行的给图像矩阵四周都加上0。‘valid’ 就是没有padding,就是在卷积核不长不足或者超出的部分直接舍去,这样得到的输出相比输入尺寸较小。
2)self.b1 = BatchNormalization()
这一层可以加快模型的训练和防止模型训练过拟合。由于数据是分批次读入的,所以会有很多随机变异性,可能会产生很多问题。Batch Normalization是把每层神经网络任意神经元输入值的分布变为均值为0,方差为1的标准正态分布,以尽可能使在深度神经网络训练过程中,每一层神经网络的输入保持相同分布。
3)self.a1 = Activation('relu')
这部分参数比较简单,就是选择’relu’。
4)self.p1 = MaxPool2D(pool_size=(2, 2), strides=2, padding='same')
在完成了两层卷积后进行池化,同样是根据网络结构搭建的,即是2✖2的卷积核,滑动窗口为2。这里padding='same'
的参数选择原理与卷积部分一样,不再赘述。经过池化后,特征矩阵的长宽都降低一半。
5)self.d1 = Dropout(0.2)
随机让一定神经元停止参与运算。dropout rate是调参的关键,调好了就是加大模型鲁棒性,调不好就是过拟合,本文中尝试后选择的是0.2,效果很不错。
3.2全连接层部分:
self.flatten = Flatten()self.f1 = Dense(512, activation='relu')self.d6 = Dropout(0.2)self.f2 = Dense(512, activation='relu')self.d7 = Dropout(0.2)self.f3 = Dense(10, activation='softmax')
1)self.flatten = Flatten()
做了13层卷积和相应的池化操作后,使用Flatten(),将数据拉平,变成一维向量。根据我们设置的输入为28x28x3,因为是有padding的卷积所以卷积层不会改变特征矩阵长宽,而每次池化特征矩阵长宽都变为原来的1/2,故五次池化后变为1x1x512,经过 Flatten()变为1x1x512=512的一维向量。
这个部分可以断点调试查看,也可以利用model.summary()
输出查看,两种方法我都尝试了,都与理论匹配。
2)self.f1 = Dense(512, activation='relu')
前两个全连接层的神经元个数在VGG16论文里提供的参考值为4096,具体可以自己做测试修改。为了让训练的参数少一点,我的输入层就比较小,所以对于Flatten()后的一维向量,连接512个节点的全连接,加上偏置的考虑,那么参数训练是:1x1x512x512+512=262656。
3)self.f3 = Dense(10, activation='softmax')
VGG论文中网络里最后的全连接层是1000个神经元,因为本文中针对的Fashion-Mnist数据集是10分类问题,因此连接10个节点的全连接,加上偏置的考虑,那么参数训练是:1x1x512x10+10=5130。
同时针对多分类问题最后输出为activation='softmax'
也在第2部分有所分析,这里也只是指定为softmax。
3.3其余部分:
1.输入层数据增强处理部分:
fashion = tf.keras.datasets.fashion_mnist(x_train, y_train), (x_test, y_test) = fashion.load_data()x_train, x_test = x_train / 255.0, x_test / 255.0print("x_train.shape", x_train.shape)x_train = x_train.reshape(x_train.shape[0], 28, 28, 1) # 给数据增加一个维度x_test = x_test.reshape(x_test.shape[0], 28, 28, 1)print("x_train.shape", x_train.shape)
1)x_train, x_test = x_train / 255.0, x_test / 255.0
对输入网络的输入特征进行归一化,使原本0到255之间的灰度值,变为0到1之间的数值,把输入特征的数值变小更适合神经网络吸收。
x_train = x_train.reshape(x_train.shape[0], 28, 28, 1)
同时因为fit需要一个四维数据,所以进行了一个reshape,给数据增加一个维度,从 (60000, 28, 28) reshape 为 (60000, 28, 28, 1)。
在这之上,使用增强函数tf.keras.preprocessing.image.ImageDataGenerator()
进行数据增强,将图像进行修改,诸如旋转rotation_range
、偏移width_shift_range
,理论上能够增强模型的泛化能力,实验后发现训练效果得到更大改善:
image_gen_train = ImageDataGenerator(
rescale=1. / 1., # 图像,分母为255时,可归至 0 ~ 1
rotation_range=45, # 随机45度旋转
width_shift_range=0.15, # 宽度偏移
height_shift_range=0.15, # 高度偏移
horizontal_flip=False, # 水平翻转
zoom_range=0.5 # 将图像随机缩放阙量50%
)
2.训练部分:
model.compile(optimizer='adam',
loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=False),
metrics=['sparse_categorical_accuracy'])
1).compile方法确定模型训练结构,告知训练时用的优化器、损失函数和准确率评测标准。
optimizer='adam'
:这里指定优化器为adam,默认lr=0.001,训练效果比较好,也就没有再调参。后续修改优化器为optimizer=tf.keras.optimizers.SGD(lr=0.1, decay=1e-6, momentum=0.9, nesterov=False)
观察运行结果。
loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=False)
:这里用的是交叉熵损失函数,这个在第2部分也分析过,值得注意的是from_logits代表是否将输出转为概率分布的形式,为False时表示输入进来的y_pred已符合某种分布, 系统只是会帮你把概率归一化。
metrics=['sparse_categorical_accuracy'])
:真实标签中的值(也就是index)与预测标签中最大值对应的index是否相等。适用于当标签是具体的label index,而预测是向量形式。
2).fit方法使模型与训练数据“拟合”
history = model.fit(x_train, y_train, batch_size=32, epochs=10, validation_data=(x_test, y_test), validation_freq=1,callbacks=[cp_callback])
batch_size=32
:在第2部分分析了这一参数大小影响模型的优化程度和速度。如果对于很小的数据集肯定是全都放入最好,但是对于本文中的训练集为60000,直接影响到 GPU 内存的使用情况,因此最终选择32。
epochs=10
:整个输入数据的单次向前和向后传递,就是训练几轮的意思,epoch数量太少,有可能发生欠拟合;太多,则有可能发生过拟合。
四、结果展示
对于影响比较明显的几个参数进行了调整,可能是因为数据集不是很复杂的原因,即使我修改了vgg网络参数的数量,选择不同优化器的效果都很不错,具体情况如下:
1.Adam+epoch=5+batch_size=32+归一化预处理训练结果:
2.Adam+epoch=10+batch_size=32+归一化预处理训练结果:
3.Adam+epoch=10+batch_size=32+图像增强预处理训练结果:
4.SGD(Nesterov Momentum)+epoch=10+batch_size=32训练结果:
五、报错汇总
1.使用tensorflow训练模型时,报错Failed to get convolution algorithm. This is probably because cuDNN failed to initialize, so try looking to see if a warning log message was printed above.
分析:网上大多的教程是说tensorflow的版本过高,或者说cuda和cudnn的版本不对,但实测发现这样会很麻烦,后续会带来很多问题。其实显示了cudnn的问题,多半是由于显卡的显存爆了,因此加入如下代码:
import osos.environ["CUDA_DEVICE_ORDER"] = "PCI_BUS_ID"os.environ['CUDA_VISIBLE_DEVICES'] = "0"config = tf.ConfigProto()config.allow_soft_placement = Trueconfig.gpu_options.per_process_gpu_memory_fraction = 0.7config.gpu_options.allow_growth = Truesession = tf.Session(config=config)
六、扩展
这一部分的思想主要是修改网络,对比看看vgg16的效果是否超群。
1.Lenet5
通过model.ummary()
输出查看(在附录里)可以看到这是一个很小的网络,共有7层,训练效果比起有16层深的vgg16还是差了些。
2.Alexnet
与相对较小的LeNet相比,它包含8层变换,其中有5层卷积和2层全连接隐藏层,以及1个全连接输出层。而且它将sigmoid激活函数改成了简单的relu激活函数。同时它通过dropout来控制全连接层的模型复杂度。
该模型深度、参数数量比之vgg16还是有所差别,因而效果稍稍差了一点。
附录:
https://blog.csdn.net/weixin_41863685/article/details/80303963
https://blog.csdn.net/weixin_43096766/article/details/88659621
https://www.jianshu.com/p/59b0cbdaa38c
https://blog.csdn.net/weixin_41690483/article/details/89292666
学习vgg16:b占up霹雳吧啦(非常适合小白入门)
还有一些参考网站待放假补充。