1962年,Hubel和Wiesel通过对猫视觉皮层细胞的研究,提出了 感受野 (Receptive Field) 的概念。通俗地说,就是他们发现视觉皮层的神经元是局部接受信息的,只受某些特定区域刺激的响应,而不是对全局图像进行感知。
1984年,日本学者Fukushima基于感受野概念提出 神经认知机 (Neocognitron)。这个神经认知机是感受野概念在人工神经网络领域的首次应用。简单地说,神经认知机将一个视觉模式分解为许多子模式,它试图将视觉系统模型化。所以卷积神经网络可以说是全连接网络和感受野概念的结合应用。
卷积神经网络最近几年大热,但是不要误以为卷积神经网络是近几年才提出的。它既不是06年也不是12年提出的,早在1989年的时候,就已经提出了卷积神经网络,并在1995年应用于支票上的手写数字识别,获得了非常好的应用效果。
卷积网络 (LeCun, 1989),也叫 卷积神经网络 (Convolutional Neural Network, CNN),是一种专门用来处理具有类似网格结构的数据的神经网络,例如时间序列数据和图像数据。它在深度学习的历史中发挥了重要作用,将研究大脑得到的深刻理解成功应用于机器学习应用的关键例子。卷积神经网络是计算机视觉应用几乎都在使用的一种深度学习模型,也是 第一批 能使用反向传播进行有效训练,解决重要商业应用的神经网络,并且仍然处于当今深度学习商业应用的前沿。

在用全连接前馈网络(FNN)来处理图像时,会存在以下两个问题:

目前的卷积神经网络一般是由 卷积层、汇聚层 和 全连接层 交叉堆叠而成的前馈神经网络。全连接层一般在卷积神经网络有三个结构上的特性:局部连接、权重共享以及 汇聚。这些特性使得卷积神经网络具有一定程度上的平移、缩放和旋转不变性。和前馈神经网络相比,卷积神经网络所使用的参数更少。
”卷积神经网络“一词表明该网络使用了 卷积 (Convolution) 这种特殊的数学线性运算,换而言之,卷积网络是指那些至少在网络的一层中使用卷积运算来替代一般的矩阵乘法运算的神经网络。但是,通常来说卷积神经网络中用到的卷积运算和其他领域 (例如工程以及纯数学领域) 中的定义并不完全一致,后续卷积详解中会说明如何在多种不同维度的数据上使用卷积运算。
同时卷积神经网络使用了 池化 (Pooling) 技术对输入特征图进行降维压缩。池化层也称为下采样层,会压缩输入的特征图,一方面减少了特征,导致了参数减少,进而简化了卷积网络计算的复杂度;另一方面也保证了特征的某种不变性 (旋转、平移、伸缩等)
虽然全连接神经网络和卷积神经网在直观上看差异比较大,但它们的整体架构是比较相似的。在结构方面,两者的输入层、全连接层和Softmax层都是一样的。
除了在结构上相似,全连接神经网络的损失函数、参数的优化过程等都同样适用于卷积神经网络。两者唯一的区别就在于:神经网络中,相邻两层的连接方式不同。体现在网络结构上就是卷积层和降采样层。
密集连接层 (Dense Layer/Full Connected Layer) 和卷积层的根本区别在于,Dense层从输入特征空间中学到的是全局模型,而卷积层学到的是 局部模型 。这个重要特性使卷积神经网络具有以下两个特质:
在通常形式中,卷积是对两个实变函数的一种 数学运算,其本质是通过对序列数据赋予不同的 权重,进行数据增强,同时降低数据的噪音。这种运算就叫做 卷积 (Convolution),通常用星号表示:
s(t) = (x * w)(t)s(t)=(x∗w)(t)
其中 ww 必须是一个有效的加权参数,否则输出就不再是一个加权平均。在卷积网络的术语中,卷积的第一个参数,函数 xx 通常叫做 输入 (input), 第二个参数,函数 ww 通常叫做 核函数 (Kernel Function), 输出 ss 被称为 特征映射 (Feature Map)
在机器学习的应用中,输入通常是多维数组的数据,而核通常是有学习算法优化得到的多维数组的参数,这些多维数组叫做张量。因为在输入与核中的每个元素都必须明确地分开存储,通常假设在存储了数值地有限点集以外,这些函数地值都为零,这意味着可以通过对有限个数组元素地求和来实现无限求和。
在机器学习的应用中,卷积运算通常运行在多个维度上。例如,如果把一张二维的图像 II 作为输入,就需要使用一个二维的核 KK:
S_{(i,j)} = (I * K)(i,j) = \sum_m\sum_nI(m,n)K(i-m,j-n)S(i,j)=(I∗K)(i,j)=∑m∑nI(m,n)K(i−m,j−n)
卷积是可交换的(Commutative), 所以可以等价地写作:
S_{(i,j)} = (K * I)(i,j) = \sum_m\sum_nI(i-m,j-n)K(m,n)S(i,j)=(K∗I)(i,j)=∑m∑nI(i−m,j−n)K(m,n)
卷积运算可交换性的出现是因为有时需要将核相对输入进行 翻转 (Flip), 但在神经网络的应用中却不是一个重要的性质。与之不同的是,许多神经网络库会实现一个相关的函数,叫做 互相关函数 (Cross-Correlation), 和卷积运算一样但是并没有对核进行翻转。许多机器学习的库实现是互相关函数但是也能称之为卷积:
S_{(i,j)} = (I * K)(i,j) = \sum_m\sum_nI(i+m,j+n)K(m,n)S(i,j)=(I∗K)(i,j)=∑m∑nI(i+m,j+n)K(m,n)
接下来会演示一个二维卷积 (没有进行核翻转) 的例子。在此点积运算中,将 5\times55×5 输入矩阵中 3\times33×3 深蓝色区域中每个元素分别与其对应位置的权值(右下角的红色数字)相乘,然后再相加,所得到的值作为 3\times33×3 输出矩阵(绿色)的元素。
x =
s = 3\times0+3\times1+2\times2+0\times2+0\times2+1\times0+3\times0+1\times1+2\times2=12s=3×0+3×1+2×2+0×2+0×2+1×0+3×0+1×1+2×2=12

将3×3权值矩阵(深蓝色区域)向右移动一个格(即步长为1)重复运算操作即为使用 滑动窗口 (Silde Window) 的算法。卷积核在二维输入数据上“滑动”,对当前输入部分的元素进行矩阵乘法,然后将结果汇为单个输出的像素值,重复这个过程直到遍历整张图像,这个过程就叫做卷积。权值矩阵(红色的数字)就是卷积核。卷积操作后的图像(绿色矩阵)称为特征映射或特征图。

卷积实际上是一个提供权重的模版,用邻域的点按一定的权重去重新定义这一点的运算,简单来说用一个卷积核滑动地与图像相应位置的像素点相乘,汇总之后得到一个值,填充到新的位置上。

在上面的例子中,我们输入的特征是 5\times5=255×5=25 ,输出的大小是 3\times3=93×3=9,如果我们使用标准的全连接层,就会产生一个 25\times9=22525×9=225 个参数的权值矩阵,因为后面一层中每个节点都是前面一层中所有节点输入的加权求和。而卷积操作中,每个深绿色的格子只与前面一层中 3\times33×3 大小的深蓝色区域相连,而不用查看 5\times55×5 的特征矩阵中所有的数值,这就是 局部连接。卷积核在图像上滑动的过程中,红色的权值矩阵是不变的,这就是 权值共享。因此,卷积神经网络只需要九个参数就可以实现从 5\times55×5 到 3\times33×3 的变换。
上例中输入层使用了一个 5\times55×5 的特征图,其中每 9 个方块可以作为中心放入一个 3\times33×3 的窗口,这 9 个方块形成一个 3\times33×3 的网格。因此,输出特征图的尺寸是 3\times33×3,比输入尺寸小了一点,这就是 边界效应 (Marginal Effect)。
观察上面的卷积示例,我们会发现一个现象:在卷积核滑动的过程中图像的边缘会被裁剪掉,5×5特征矩阵经过卷积之后转换为3×3的特征矩阵,这里免不了会丢失一部分信息。如何使得输出尺寸与输入保持一致呢?这里就用到了一个很重要的技术 填充 (Padding)。填充是在输入特征图的每一遍添加适当数目的行和列,使得每个输入防控都能作为卷积窗口的中心。通常用额外的“假”像素(通常值为0)填充边缘。这样,在滑动时的卷积核可以允许原始边缘像素位于卷积核的中心,同时延伸到边缘之外的假像素,从而产生与输入相同大小的输出。

如果未填充零,则网络每一层的宽度会逐层递减。根据卷积的性质,网络每一层宽度减少的数量等于卷积核的宽度减 1。
对输入有三种填充零的方式:
valid 填充:不使用零来填充输入,卷积核只允许访问那些图像中能完全包含整个核的位置。
same 填充:使用足够的零来填充,使得输出和输入保持相同的大小。这是最常见的填充方式。
full 填充:在输入的两端各填充 k-1k−1 个零,使得每个输入单元都恰好被卷积核访问 kk 次。其中 kk 为卷积核的宽度。

每个卷积核都会将图像生成为另一幅特征映射图,即一个卷积核提取一种特征。为了使特征提取更充分,我们可以添加多个卷积核以提取不同的特征,也就是多通道卷积。

卷积是图像矩阵与权值矩阵的点乘。接下来基于Numpy的示例,展示不同的卷积核对图像进行卷积操作之后会产生的不同效果。首先,定义 conv() 函数,这是一个进行卷积操作的函数。
import numpy as np import matplotlib.pyplot as plt import matplotlib.cm as cm from PIL import Image def conv(image_array,kernel): ''' input: - image_array:输入层灰度图像array矩阵 - kernel: 卷积核 output: - 卷积运算后的特征映射矩阵 ''' image_copy = image_array.copy() height,width = image_copy.shape h,w = kernel.shape h_x = int((h-1)/2) w_x = int((w-1)/2) # padding填充临时图片 temp = np.zeros([height + h_x*2, width + w_x*2]) # 将原图拷贝到临时图片的中央 temp[h_x:h_x + height, w_x:w_x + width] = image_copy[:,:] # 初始化一张同样大小的图片作为输出图片 output = np.zeros_like(a=temp) # 进行卷积运算 for i in range(h_x, h_x + height): for j in range(w_x, w_x + width): output[i][j] = int(np.sum(temp[i-h_x:i+h_x+1,j-w_x:j+w_x+1] * kernel)) return output[h_x:height + h_x, w_x:width + w_x]
然后定义三个不同的卷积核便于对比:
# 提取垂直特征 kernel_1 = np.array([[-1,0,1], [-2,0,2], [-1,0,1]]) # 提取水平特征 kernel_2 = np.array([[-1,-2,-1], [0,0,0], [1,2,1]]) # Laplace扩展算子 kernel_3 = np.array([[1,1,1], [1,-8,1], [1,1,1]])

# 打开图像并转换为灰度图像 image = Image.open('photos/example.jpg').convert('L') # 将图像转换为数组 image_array = np.array(image) # 卷积运算 image_vert = conv(image_array,kernel_1) image_horz = conv(image_array,kernel_2) image_lapc = conv(image_array,kernel_3)
# 展示原图像 plt.imshow(image_array,cmap=cm.gray) plt.axis('off') plt.show()

# 展示纵向卷积核灰度图像 plt.imshow(image_vert,cmap=cm.gray) plt.axis('off') plt.show()

# 展示横向卷积核灰度图像 plt.imshow(image_horz,cmap=cm.gray) plt.axis('off') plt.show()

# 展示Laplace卷积核灰度图像 plt.imshow(image_lapc,cmap=cm.gray) plt.axis('off') plt.show()

对于图像中没有边缘的区域,比如背景里的大海,大部分像素值几乎是相同的,所以卷积核在这些点,输出几乎也是相同的;而对于有边缘的区域,由于边缘两侧的像素是不一样的,卷积核计算结果因此也是不同的,从而能够检测到边缘,这就是 边缘检测 的原理。对图像用一个卷积核进行卷积运算,实际上是一个滤波的过程。每个卷积核都是一种特征提取方式,就像是一个筛子,将图像中符合条件的部分筛选出来。
在卷积层之后常常紧接着一个降采样层,通过减小矩阵的长和宽,从而达到减少参数的目的。而降采样是降低特定信号的采样率的过程。池化 (Pooling),思想来自于视觉机制,是对信息进行抽象的过程。池化就是计算图像一个区域上的某个特定特征的平均值或最大值。比如下图中,把四个点聚合成一个点,从而达到了降维的目的。

常用的池化方法有 均值池化 (Average Pooling) 和 最大池化 (Max Pooling),均值池化和最大池化适用于不同的场景。
另外比较少用到的池化策略为:
降采样同样可以通过卷积计算中的 步长 (Stride) 来实现,它表示卷积核在图片上移动的格数。通过步长的变换,可以得到不同尺寸的卷积输出结果。

步长大于 1 的卷积操作也是降维的一种方式。步长为 2 意味着特征图的宽度和高度都被做了 2 倍下采样(除了边界效应引起的变化)。虽然步进卷积对某种类型的模型有用,但在实践中很少使用,CNN模型通常使用 最大池化 运算。
池化操作把四个点平均成一个点,通过相似语义的聚合,达到降维的目的,同时不丢失过多的有效信息。但另一方面,为了检测更多的特征信息、形成更多不同通道特征的组合,从而形成更复杂的特征,需要逐渐增加每层所含的平面数。基于池化或卷积步长实现的降采样操作满足了:
增大感受野: 所谓感受野,即一个像素对应回原图的区域大小,假如没有池化运算,一个 3\times33×3,步长为 1 的卷积,那么输出的一个像素的感受野就是 3\times33×3 的区域,再加一个 stride=1stride=1 的 3\times33×3 卷积,则感受野为 5\times55×5。在每一个卷积中间加上 3\times33×3 的池化运算之后,很明显感受野会迅速增大。感受野的增加对于模型的能力的提升是必要的。
平移不变性: 目标的些许位置的移动,能得到相同的结果。因为池化不断地抽象了区域的特征而不关心位置,所以一定程度上增加了平移不变性。
降低优化难度和参数: 用步长大于1的卷积来替代池化,但是池化每个特征通道单独做降采样,与基于卷积的降采样相比,不需要参数,更容易优化。全局池化更是可以大大降低模型的参数量和优化工作量。池化层同时没有任何附加参数。
降采样层本质上是一个特征选择,信息过滤的过程。实际池化操作过程中不可避免地损失一部分信息,这也是一个和计算性能的一个妥协。随着运算速度的不断提高,现代神经网络开始逐步减少降采样层的使用频率来试图追求更高的精度。
# %load cnn.py import math import torch import torch.nn as nn class ConvolutionalNN(nn.Module): ''' size 图像大小 input_size 输入通道数 output_size 输出通道数 num_classes 类别数 ''' def __init__(self,input_size,output_size,num_classes): super(ConvolutionalNN, self).__init__() self.layer1 = nn.Sequential( nn.Conv2d(in_channels=1, out_channels=input_size, kernel_size=3, padding=1), nn.BatchNorm2d(input_size), nn.ReLU(), nn.MaxPool2d(2) ) self.layer2 = nn.Sequential( nn.Conv2d(in_channels=input_size, out_channels=output_size, kernel_size=3), nn.BatchNorm2d(output_size), nn.ReLU(), nn.MaxPool2d(2) ) # 全连接层,类别数为num_classes total_features = output_size * 36 ratio = int(math.sqrt(total_features/num_classes)) floor = math.floor(math.log2(ratio)) trans_size = int(math.pow(2,math.log2(total_features)-floor)) self.fc = nn.Sequential( nn.Linear(total_features, trans_size), nn.Dropout(0.25), nn.ReLU(inplace=True), nn.Linear(trans_size, num_classes) ) def forward(self, x): out = self.layer1(x) out = self.layer2(out) out = out.view(out.size(0), -1) out = self.fc(out) return out def build_cnn(phase, input_size=32,output_size=64,num_classes=10): if phase != "test" and phase != "train": print("ERROR: Phase: " + phase + " not recognized") return return ConvolutionalNN(input_size, output_size, num_classes)
from torchsummary import summary net = build_cnn('train') net.cuda() summary(net,(1,28,28))
- C:\ProgramData\Anaconda3\lib\site-packages\torch\nn\functional.py:718: UserWarning: Named tensors and all their associated APIs are an experimental feature and subject to change. Please do not use them for anything important until they are released as stable. (Triggered internally at ..\c10/core/TensorImpl.h:1156.)
- return torch.max_pool2d(input, kernel_size, stride, padding, dilation, ceil_mode)
-
-
- ----------------------------------------------------------------
- Layer (type) Output Shape Param #
- ================================================================
- Conv2d-1 [-1, 32, 28, 28] 320
- BatchNorm2d-2 [-1, 32, 28, 28] 64
- ReLU-3 [-1, 32, 28, 28] 0
- MaxPool2d-4 [-1, 32, 14, 14] 0
- Conv2d-5 [-1, 64, 12, 12] 18,496
- BatchNorm2d-6 [-1, 64, 12, 12] 128
- ReLU-7 [-1, 64, 12, 12] 0
- MaxPool2d-8 [-1, 64, 6, 6] 0
- Linear-9 [-1, 287] 661,535
- Dropout-10 [-1, 287] 0
- ReLU-11 [-1, 287] 0
- Linear-12 [-1, 10] 2,880
- ================================================================
- Total params: 683,423
- Trainable params: 683,423
- Non-trainable params: 0
- ----------------------------------------------------------------
- Input size (MB): 0.00
- Forward/backward pass size (MB): 0.86
- Params size (MB): 2.61
- Estimated Total Size (MB): 3.47
- ----------------------------------------------------------------
import keras from keras.layers import Conv2D, MaxPool2D, Dropout, Dense, Flatten from keras.models import Sequential model = Sequential() # 32个滤波器,每个滤波器的大小都是3x3,步长为1 model.add(Conv2D(32,(3,3),activation='relu',padding='same',input_shape=(28,28,1))) model.add(Conv2D(32,(3,3),activation='relu',padding='same')) # 2x2的最大池优化,步长为2 model.add(MaxPool2D(pool_size=(2,2))) model.add(Dropout(0.25)) # 64个滤波器,每个滤波器的大小都是3x3,步长为1 model.add(Conv2D(64,(3,3),activation='relu',padding='same')) model.add(Conv2D(64,(3,3),activation='relu',padding='same')) # 2x2的最大池优化,步长为2 model.add(MaxPool2D(pool_size=(2,2))) model.add(Dropout(0.25)) # 压平层与全连接层 model.add(Flatten()) model.add(Dense(256,activation='relu')) model.add(Dropout(0.25)) # 输出分类层 model.add(Dense(10,activation='softmax'))
model.summary()
- Model: "sequential_5"
- _________________________________________________________________
- Layer (type) Output Shape Param #
- =================================================================
- conv2d_11 (Conv2D) (None, 28, 28, 32) 320
- _________________________________________________________________
- conv2d_12 (Conv2D) (None, 28, 28, 32) 9248
- _________________________________________________________________
- max_pooling2d_4 (MaxPooling2 (None, 14, 14, 32) 0
- _________________________________________________________________
- dropout_6 (Dropout) (None, 14, 14, 32) 0
- _________________________________________________________________
- conv2d_13 (Conv2D) (None, 14, 14, 64) 18496
- _________________________________________________________________
- conv2d_14 (Conv2D) (None, 14, 14, 64) 36928
- _________________________________________________________________
- max_pooling2d_5 (MaxPooling2 (None, 7, 7, 64) 0
- _________________________________________________________________
- dropout_7 (Dropout) (None, 7, 7, 64) 0
- _________________________________________________________________
- flatten_2 (Flatten) (None, 3136) 0
- _________________________________________________________________
- dense_4 (Dense) (None, 256) 803072
- _________________________________________________________________
- dropout_8 (Dropout) (None, 256) 0
- _________________________________________________________________
- dense_5 (Dense) (None, 10) 2570
- =================================================================
- Total params: 870,634
- Trainable params: 870,634
- Non-trainable params: 0
- _________________________________________________________________
import tensorflow.compat.v1 as tf tf.disable_v2_behavior() # 定义权值 def weight(shape): # 在构建模型时,需要使用tf.Variable来创建一个变量 # 在训练时,这个变量不断更新 # 使用函数tf.truncated_normal(截断的正态分布)生成标准差为0.1的随机数来初始化权值 return tf.Variable(tf.truncated_normal(shape, stddev=0.1), name ='W') # 定义偏置 # 初始化为0.1 def bias(shape): return tf.Variable(tf.constant(0.1, shape=shape), name = 'b') # 定义卷积操作 # 步长为1,padding为'SAME' def conv2d(x, W): # tf.nn.conv2d(input, filter, strides, padding, use_cudnn_on_gpu=None, name=None) return tf.nn.conv2d(x, W, strides=[1,1,1,1], padding='SAME') # 定义池化操作 # 步长为2,即原尺寸的长和宽各除以2 def max_pool_2x2(x): # tf.nn.max_pool(value, ksize, strides, padding, name=None) return tf.nn.max_pool(x, ksize=[1,2,2,1], strides=[1,2,2,1], padding='SAME')
# 输入层 # 28x28图像,通道为1(Gray Image) with tf.name_scope('input_layer'): x = tf.placeholder('float',shape=[None, 28, 28, 1],name="x") # 第1个卷积层 # 输入通道:1,输出通道:32,卷积后图像尺寸不变,依然是28x28 with tf.name_scope('conv_1'): W1 = weight([3,3,1,32]) # [k_width, k_height, input_chn, output_chn] b1 = bias([32]) # 与output_chn 一致 conv_1=conv2d(x, W1)+ b1 conv_1 = tf.nn.relu(conv_1 ) # 第1个池化层 # 将28x28图像缩小为14x14,池化不改变通道数量,因此依然是32个 with tf.name_scope('pool_1'): pool_1 = max_pool_2x2(conv_1) # 第2个卷积层 # 输入通道:32,输出通道:64,卷积后图像尺寸不变,依然是14x14 with tf.name_scope('conv_2'): W2 = weight([3,3,32,64]) b2 = bias([64]) conv_2=conv2d(pool_1, W2)+ b2 conv_2 = tf.nn.relu(conv_2) # 第2个池化层 # 将14x14图像缩小为7x7,池化不改变通道数量,因此依然是64个 with tf.name_scope('pool_2'): pool_2 = max_pool_2x2(conv_2) # 全连接层 # 将池第2个池化层的64个7x7的图像转换为一维的向量,长度是 64*7*7=3136 # 256个神经元 with tf.name_scope('fc'): W3= weight([3136, 256]) #有256个神经元 b3= bias([256]) flat = tf.reshape(pool_2, [-1, 3136]) h = tf.nn.relu(tf.matmul(flat, W3) + b3) h_dropout= tf.nn.dropout(h, keep_prob=0.8) # 输出层 # 输出层共有10个神经元,对应到0-9这10个类别 with tf.name_scope('output_layer'): W4 = weight([256,10]) b4 = bias([10]) pred= tf.nn.softmax(tf.matmul(h_dropout, W4)+b4)
加载同级目录下 train.py 程序代码
# %load train.py import os os.environ["KMP_DUPLICATE_LIB_OK"]="TRUE" import time import argparse import sys import torch import torch.nn as nn import torch.optim as optim import torch.backends.cudnn as cudnn from torchvision import transforms from torch.autograd import Variable import matplotlib as mpl import matplotlib.pyplot as plt mpl.rc('axes', labelsize = 14) mpl.rc('xtick', labelsize = 12) mpl.rc('ytick', labelsize = 12) sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) from cnn import build_cnn from datasets.mnist import MNIST from datasets.fashion_mnist import FASHION_MNIST from datasets.config import FASHION_MNIST_ROOT, MNIST_ROOT def str2bool(v): return v.lower() in ("yes", "true", "t", "1") parser = argparse.ArgumentParser( description='Convolutional Neural Network Training With Pytorch') train_set = parser.add_mutually_exclusive_group() parser.add_argument('--dataset', default='MNIST', choices=['MNIST', 'FASHION_MNIST'], type=str, help='MNIST or FASHION_MNIST') parser.add_argument('--dataset_root', default=MNIST_ROOT, help='Dataset root directory path') parser.add_argument('--input_dim', default=1, choices=[1, 3], type=int, help='Grey or RGB image input') parser.add_argument('--input_size', default=32, type=int, help='Convolutional layer hidden size') parser.add_argument('--output_size', default=64, type=int, help='Convolutional layer output size') parser.add_argument('--batch_size', default=32, type=int, help='Batch size for training') parser.add_argument('--num_workers', default=0, type=int, help='Number of workers used in dataloading') parser.add_argument('--epoch_size', default=30, type=int, help='Number of Epoches for training') parser.add_argument('--cuda', default=True, type=str2bool, help='Use CUDA to train model') parser.add_argument('--lr', '--learning-rate', default=1e-3, type=float, help='initial learning rate') parser.add_argument('--save_folder', default='weights/', help='Directory for saving checkpoint models') parser.add_argument('--photo_folder', default='results/', help='Directory for saving photos') args = parser.parse_args() if not os.path.exists(args.save_folder): os.mkdir(args.save_folder) if not os.path.exists(args.photo_folder): os.mkdir(args.photo_folder) def train(): if args.dataset == 'MNIST': if args.dataset_root == MNIST_ROOT: if not os.path.exists(MNIST_ROOT): parser.error('Must specify dataset_root if specifying dataset') args.dataset_root = MNIST_ROOT dataset = MNIST(root=args.dataset_root,folder='train', transform=transforms.ToTensor()) elif args.dataset == 'FASHION_MNIST': if args.dataset_root == MNIST_ROOT: print("Using default FASHION_MNIST dataset_root") args.dataset_root = FASHION_MNIST_ROOT dataset = FASHION_MNIST(root=args.dataset_root,folder='train', transform=transforms.ToTensor()) net = build_cnn(phase='train', dimension=args.input_dim, input_size=args.input_size, output_size=args.output_size, num_classes=len(dataset.classes)) if args.cuda and torch.cuda.is_available(): net = torch.nn.DataParallel(net) cudnn.benchmark = True net.cuda() optimizer = optim.Adam(net.parameters(), lr=args.lr) criterion = nn.CrossEntropyLoss() epoch_size = args.epoch_size print('Loading the dataset...') data_loader = torch.utils.data.DataLoader(dataset, args.batch_size, num_workers=args.num_workers, shuffle=True, pin_memory=True) print('Training on:', dataset.name) print('Using model: CNN') print('Using the specified args:') print(args) loss_list = [] acc_list = [] for epoch in range(epoch_size): net.train() train_loss = 0.0 correct = 0 total = len(dataset) t0 = time.perf_counter() for step, data in enumerate(data_loader, start=0): images, labels = data if args.cuda: images = Variable(images.cuda()) labels = Variable(labels.cuda()) else: images = Variable(images) labels = Variable(labels) # forward outputs = net(images) # backprop optimizer.zero_grad() loss = criterion(outputs, labels) loss.backward() optimizer.step() # print statistics train_loss += loss.item() _, predicted = outputs.max(1) correct += predicted.eq(labels).sum().item() # print train process rate = (step + 1) / len(data_loader) a = "*" * int(rate * 50) b = "." * int((1 - rate) * 50) print("\rEpoch {}: {:^3.0f}%[{}->{}]{:.3f}".format(epoch+1, int(rate * 100), a, b, loss), end="") print(' Running time: %.3f' % (time.perf_counter() - t0)) acc = 100.*correct/ total loss = train_loss / step print('train loss: %.6f, acc: %.3f%% (%d/%d)' % (loss, acc, correct, total)) loss_list.append(loss) acc_list.append(acc/100) torch.save(net.state_dict(),args.save_folder + args.dataset + '.pth') plt.plot(range(epoch_size), loss_list, range(epoch_size), acc_list) plt.xlabel('Epoches') plt.ylabel('Sparse CrossEntropy Loss | Accuracy') plt.savefig(os.path.join( os.path.dirname( os.path.abspath(__file__)), args.photo_folder, args.dataset + "_train_details.png")) if __name__ == '__main__': train()
训练采用的数据集,目前提供MNIST ,FASHION_MNIST供选择。 点击查看数据集加载Demo
数据集读取地址, default已设置为数据集相对路径,部署在云端可能需要修改
FFN网络隐藏层个数, default为500层
单次训练所抓取的数据样本数量,default为32,MNIST数据集推荐参考为128
加载数据所使用线程个数,default为0,n\in (2,4,8,12\dots)n∈(2,4,8,12…)
训练次数, default为30
是否调用GPU训练
超参数学习率,FNN采用Adam优化函数,default为 0.0010.001
模型权重保存地址
print 于 python console, 包括单个epoch训练时间、训练集损失值、准确率
模型保存路径为 ./weight/%.pth
图片保存路径为 ./photos/%_train_details.png
加载同级目录下 test.py 程序代码
# %load test.py import sys import os os.environ["KMP_DUPLICATE_LIB_OK"]="TRUE" import argparse import torch import torch.nn as nn import torch.backends.cudnn as cudnn import torchvision.transforms as transforms from torch.autograd import Variable import itertools import numpy as np import matplotlib as mpl import matplotlib.pyplot as plt mpl.rc('axes', labelsize = 14) mpl.rc('xtick', labelsize = 12) mpl.rc('ytick', labelsize = 12) sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) from cnn import build_cnn from datasets.mnist import MNIST from datasets.fashion_mnist import FASHION_MNIST from datasets.config import FASHION_MNIST_ROOT, MNIST_ROOT parser = argparse.ArgumentParser( description='Convolutional Neural Network Testing With Pytorch') parser.add_argument('--dataset', default='MNIST', choices=['MNIST', 'FASHION_MNIST'], type=str, help='MNIST or FASHION_MNIST') parser.add_argument('--dataset_root', default=MNIST_ROOT, help='Location of MNIST root directory') parser.add_argument('--input_dim', default=1, choices=[1, 3], type=int, help='Grey or RGB image input') parser.add_argument('--input_size', default=32, type=int, help='Convolutional layer input size') parser.add_argument('--output_size', default=64, type=int, help='Convolutional layer output size') parser.add_argument('--batch_size', default=32, type=int, help='Batch size for training') parser.add_argument('--num_workers', default=0, type=int, help='Number of workers used in dataloading') parser.add_argument('--trained_model', default='weights/{}.pth', type=str, help='Trained state_dict file path to open') parser.add_argument('--cuda', default=True, type=bool, help='Use cuda to train model') parser.add_argument('-f', default=None, type=str, help="Dummy arg so we can load in Jupyter Notebooks") args = parser.parse_args() args.trained_model = args.trained_model.format(args.dataset) def confusion_matrix(preds, labels, conf_matrix): for p, t in zip(preds, labels): conf_matrix[p, t] += 1 return conf_matrix def save_confusion_matrix(cm, classes, normalize=False, title='Confusion matrix', cmap=plt.cm.Blues): plt.imshow(cm, interpolation='nearest', cmap=cmap) plt.title(title) plt.colorbar() tick_marks = np.arange(len(classes)) plt.xticks(tick_marks, classes, rotation=90) plt.yticks(tick_marks, classes) plt.axis("equal") ax = plt.gca() left, right = plt.xlim() ax.spines['left'].set_position(('data', left)) ax.spines['right'].set_position(('data', right)) for edge_i in ['top', 'bottom', 'right', 'left']: ax.spines[edge_i].set_edgecolor("white") thresh = cm.max() / 2. for i, j in itertools.product(range(cm.shape[0]), range(cm.shape[1])): num = '{:.2f}'.format(cm[i, j]) if normalize else int(cm[i, j]) plt.text(j, i, num, verticalalignment='center', horizontalalignment="center", color="white" if num > thresh else "black") plt.ylabel('True label') plt.xlabel('Predicted label') plt.savefig(os.path.join( os.path.dirname( os.path.abspath(__file__)), "results", args.dataset + '_confusion_matrix.png')) def test(): # load data if args.dataset == 'MNIST': if args.dataset_root == MNIST_ROOT: if not os.path.exists(MNIST_ROOT): parser.error('Must specify dataset_root if specifying dataset') args.dataset_root = MNIST_ROOT dataset = MNIST(root=args.dataset_root,folder='test', transform=transforms.ToTensor()) elif args.dataset == 'FASHION_MNIST': if args.dataset_root == MNIST_ROOT: print("Using default FASHION_MNIST dataset_root") args.dataset_root = FASHION_MNIST_ROOT dataset = FASHION_MNIST(root=args.dataset_root,folder='test', transform=transforms.ToTensor()) data_loader = torch.utils.data.DataLoader(dataset, args.batch_size, num_workers=args.num_workers, shuffle=True, pin_memory=True) # load net net = build_cnn(phase='test', dimension=args.input_dim, input_size=args.input_size, output_size=args.output_size, num_classes=len(dataset.classes)) if args.cuda and torch.cuda.is_available(): net = torch.nn.DataParallel(net) cudnn.benchmark = True net.cuda() net.load_state_dict(torch.load(args.trained_model)) print('Finish loading model: ', args.trained_model) net.eval() print('Training on:', dataset.name) print('Using model: Basic CNN') print('Using the specified args:') print(args) # evaluation criterion = nn.CrossEntropyLoss() test_loss = 0 correct = 0 total = 0 conf_matrix = torch.zeros(10, 10) class_correct = list(0 for i in range(10)) class_total = list(0 for i in range(10)) with torch.no_grad(): for step, data in enumerate(data_loader): images, labels = data if args.cuda: images = Variable(images.cuda()) labels = Variable(labels.cuda()) else: images = Variable(images) labels = Variable(labels) # forward outputs = net(images) loss = criterion(outputs, labels) test_loss += loss.item() _, predicted = outputs.max(1) conf_matrix = confusion_matrix(predicted, labels=labels, conf_matrix=conf_matrix) total += labels.size(0) correct += predicted.eq(labels).sum().item() c = (predicted.eq(labels)).squeeze() for i in range(c.size(0)): label = labels[i] class_correct[label] += c[i].item() class_total[label] += 1 acc = 100.* correct / total loss = test_loss / step print('test loss: %.6f, acc: %.3f%% (%d/%d)' % (loss, acc, correct, total)) for i in range(len(dataset.classes)): print('accuracy of %s : %.3f%% (%d/%d)' % ( str(dataset.classes[i]), 100 * class_correct[i] / class_total[i], class_correct[i], class_total[i])) save_confusion_matrix(conf_matrix.numpy(), classes=dataset.classes, normalize=False, title = 'Normalized confusion matrix') if __name__ == '__main__': test()
训练采用的数据集,目前提供MNIST ,FASHION_MNIST供选择。 点击查看数据集加载Demo
数据集读取地址, default已设置为数据集相对路径,部署在云端可能需要修改
FFN网络隐藏层个数, default为500层
单次训练所抓取的数据样本数量,default为32,MNIST数据集推荐参考为128
加载数据所使用线程个数,default为0,n\in (2,4,8,12\dots)n∈(2,4,8,12…)
模型权重保存路径,default为 train.py 生成的ptb文件路径
是否调用GPU训练
print 于 python console 第一行
print 于 python console 后续列表
图片保存路径为 ./photos/%_confusion_matrix.png

MNIST 来自美国国家标准与技术研究所, National Institute of Standards and Technology (NIST). 训练集 (training set) 由来自 250 个不同人手写的数字构成, 其中 50% 是高中学生, 50% 来自人口普查局 (the Census Bureau) 的工作人员,测试集(test set) 也是同样比例的手写数字数据
MNIST 是机器学习领域中非常经典的一个数据集,由60000个训练样本和10000个测试样本组成,每个样本都是一张 28\times2828×28 像素的灰度手写数字图片
MNIST 已经是一个被”嚼烂”了的数据集, 作为机器学习在视觉领域的hello world,很多教程都会对它”下手”, 几乎成为一个 “典范”
%run train.py --batch_size=128 --epoch_size=20
- Loading the dataset...
- Training on: MNIST
- Using model: CNN
- Using the specified args:
- Namespace(batch_size=128, cuda=True, dataset='MNIST', dataset_root='C:\\Users\\sbzy\\Documents/GitHub/dl_algorithm/datasets\\MNIST', epoch_size=20, input_dim=1, input_size=32, lr=0.001, num_workers=0, output_size=64, photo_folder='results/', save_folder='weights/')
- Epoch 1: 100%[**************************************************->]0.096 Running time: 4.268
- train loss: 0.142464, acc: 95.505% (57303/60000)
- Epoch 2: 100%[**************************************************->]0.048 Running time: 4.162
- train loss: 0.052557, acc: 98.390% (59034/60000)
- Epoch 3: 100%[**************************************************->]0.034 Running time: 4.199
- train loss: 0.041116, acc: 98.717% (59230/60000)
- Epoch 4: 100%[**************************************************->]0.064 Running time: 4.045
- train loss: 0.030572, acc: 99.033% (59420/60000)
- Epoch 5: 100%[**************************************************->]0.040 Running time: 4.029
- train loss: 0.027046, acc: 99.142% (59485/60000)
- Epoch 6: 100%[**************************************************->]0.006 Running time: 4.047
- train loss: 0.022617, acc: 99.275% (59565/60000)
- Epoch 7: 100%[**************************************************->]0.025 Running time: 4.025
- train loss: 0.019697, acc: 99.365% (59619/60000)
- Epoch 8: 100%[**************************************************->]0.015 Running time: 4.078
- train loss: 0.018360, acc: 99.378% (59627/60000)
- Epoch 9: 100%[**************************************************->]0.030 Running time: 4.062
- train loss: 0.014056, acc: 99.545% (59727/60000)
- Epoch 10: 100%[**************************************************->]0.000 Running time: 4.033
- train loss: 0.015583, acc: 99.477% (59686/60000)
- Epoch 11: 100%[**************************************************->]0.080 Running time: 4.106
- train loss: 0.012131, acc: 99.602% (59761/60000)
- Epoch 12: 100%[**************************************************->]0.000 Running time: 4.096
- train loss: 0.012068, acc: 99.598% (59759/60000)
- Epoch 13: 100%[**************************************************->]0.000 Running time: 4.104
- train loss: 0.011510, acc: 99.622% (59773/60000)
- Epoch 14: 100%[**************************************************->]0.003 Running time: 4.095
- train loss: 0.010077, acc: 99.643% (59786/60000)
- Epoch 15: 100%[**************************************************->]0.000 Running time: 4.059
- train loss: 0.008377, acc: 99.728% (59837/60000)
- Epoch 16: 100%[**************************************************->]0.002 Running time: 4.093
- train loss: 0.007739, acc: 99.737% (59842/60000)
- Epoch 17: 100%[**************************************************->]0.000 Running time: 4.033
- train loss: 0.007553, acc: 99.747% (59848/60000)
- Epoch 18: 100%[**************************************************->]0.002 Running time: 4.163
- train loss: 0.008951, acc: 99.700% (59820/60000)
- Epoch 19: 100%[**************************************************->]0.000 Running time: 4.327
- train loss: 0.005622, acc: 99.805% (59883/60000)
- Epoch 20: 100%[**************************************************->]0.025 Running time: 4.270
- train loss: 0.006955, acc: 99.752% (59851/60000)

%run test.py --batch_size=128
- Finish loading model: weights/MNIST.pth
- Training on: MNIST
- Using model: Basic CNN
- Using the specified args:
- Namespace(batch_size=128, cuda=True, dataset='MNIST', dataset_root='C:\\Users\\sbzy\\Documents/GitHub/dl_algorithm/datasets\\MNIST', f=None, input_dim=1, input_size=32, num_workers=0, output_size=64, trained_model='weights/MNIST.pth')
- test loss: 0.042636, acc: 99.080% (9908/10000)
- accuracy of 0 : 99.592% (976/980)
- accuracy of 1 : 99.736% (1132/1135)
- accuracy of 2 : 98.450% (1016/1032)
- accuracy of 3 : 98.911% (999/1010)
- accuracy of 4 : 99.695% (979/982)
- accuracy of 5 : 99.439% (887/892)
- accuracy of 6 : 99.165% (950/958)
- accuracy of 7 : 99.319% (1021/1028)
- accuracy of 8 : 99.589% (970/974)
- accuracy of 9 : 96.928% (978/1009)


Fashion-MNIST 的大小、格式和训练集/测试集划分与原始的 MNIST 完全一致。60000/10000 的训练测试数据划分,28\times2828×28 的灰度图片。
Fashion-MNIST 的目的是要成为 MNIST 数据集的一个直接替代品。MNIST 太简单了,只需要一个像素就可以区分开,很多算法在测试集上的性能已经达到 99.6%。MNIST 数字识别的任务不代表现代机器学习。Fashion-MNIST 迁移到真正的机器视觉图像分类问题。
%run train.py --dataset=FASHION_MNIST --batch_size=128 --epoch_size=20
- Using default FASHION_MNIST dataset_root
- Loading the dataset...
- Training on: FASHION_MNIST
- Using model: CNN
- Using the specified args:
- Namespace(batch_size=128, cuda=True, dataset='FASHION_MNIST', dataset_root='C:\\Users\\sbzy\\Documents/GitHub/dl_algorithm/datasets\\FASHION_MNIST', epoch_size=20, input_dim=1, input_size=32, lr=0.001, num_workers=0, output_size=64, photo_folder='results/', save_folder='weights/')
- Epoch 1: 100%[**************************************************->]0.331 Running time: 4.152
- train loss: 0.407117, acc: 85.200% (51120/60000)
- Epoch 2: 100%[**************************************************->]0.246 Running time: 4.073
- train loss: 0.280310, acc: 89.793% (53876/60000)
- Epoch 3: 100%[**************************************************->]0.330 Running time: 4.053
- train loss: 0.239892, acc: 91.213% (54728/60000)
- Epoch 4: 100%[**************************************************->]0.180 Running time: 4.036
- train loss: 0.217020, acc: 92.027% (55216/60000)
- Epoch 5: 100%[**************************************************->]0.203 Running time: 4.151
- train loss: 0.197507, acc: 92.663% (55598/60000)
- Epoch 6: 100%[**************************************************->]0.185 Running time: 4.104
- train loss: 0.177419, acc: 93.398% (56039/60000)
- Epoch 7: 100%[**************************************************->]0.328 Running time: 4.065
- train loss: 0.160301, acc: 93.953% (56372/60000)
- Epoch 8: 100%[**************************************************->]0.121 Running time: 4.083
- train loss: 0.146904, acc: 94.577% (56746/60000)
- Epoch 9: 100%[**************************************************->]0.158 Running time: 4.088
- train loss: 0.131364, acc: 95.163% (57098/60000)
- Epoch 10: 100%[**************************************************->]0.064 Running time: 4.080
- train loss: 0.122862, acc: 95.435% (57261/60000)
- Epoch 11: 100%[**************************************************->]0.142 Running time: 4.179
- train loss: 0.109991, acc: 95.925% (57555/60000)
- Epoch 12: 100%[**************************************************->]0.201 Running time: 4.107
- train loss: 0.098736, acc: 96.317% (57790/60000)
- Epoch 13: 100%[**************************************************->]0.062 Running time: 4.060
- train loss: 0.089864, acc: 96.623% (57974/60000)
- Epoch 14: 100%[**************************************************->]0.083 Running time: 4.039
- train loss: 0.078398, acc: 97.030% (58218/60000)
- Epoch 15: 100%[**************************************************->]0.087 Running time: 4.094
- train loss: 0.073593, acc: 97.245% (58347/60000)
- Epoch 16: 100%[**************************************************->]0.039 Running time: 4.104
- train loss: 0.064772, acc: 97.568% (58541/60000)
- Epoch 17: 100%[**************************************************->]0.133 Running time: 4.093
- train loss: 0.062107, acc: 97.683% (58610/60000)
- Epoch 18: 100%[**************************************************->]0.159 Running time: 4.073
- train loss: 0.054286, acc: 97.910% (58746/60000)
- Epoch 19: 100%[**************************************************->]0.063 Running time: 4.070
- train loss: 0.050712, acc: 98.115% (58869/60000)
- Epoch 20: 100%[**************************************************->]0.047 Running time: 4.038
- train loss: 0.047520, acc: 98.203% (58922/60000)

%run test.py --dataset=FASHION_MNIST --batch_size=128
- Using default FASHION_MNIST dataset_root
- Finish loading model: weights/FASHION_MNIST.pth
- Training on: FASHION_MNIST
- Using model: Basic CNN
- Using the specified args:
- Namespace(batch_size=128, cuda=True, dataset='FASHION_MNIST', dataset_root='C:\\Users\\sbzy\\Documents/GitHub/dl_algorithm/datasets\\FASHION_MNIST', f=None, input_dim=1, input_size=32, num_workers=0, output_size=64, trained_model='weights/FASHION_MNIST.pth')
- test loss: 0.416567, acc: 90.470% (9047/10000)
- accuracy of T-shirt/top : 91.200% (912/1000)
- accuracy of Trouser : 98.000% (980/1000)
- accuracy of Pullover : 95.000% (950/1000)
- accuracy of Dress : 93.600% (936/1000)
- accuracy of Coat : 71.100% (711/1000)
- accuracy of Sandal : 98.500% (985/1000)
- accuracy of Shirt : 64.700% (647/1000)
- accuracy of Sneaker : 97.200% (972/1000)
- accuracy of Bag : 99.000% (990/1000)
- accuracy of Ankle boot : 96.400% (964/1000)

开始实验