1.关于图像的基础知识
灰度图
这里以MINST数据集为例,对于一个手写数字的图像,其由像素为28*28的灰度图构成,如图所示:
根据每一个像素点的一个具体的数值,可以将其构造出一副如上述所示的灰度图,由于灰度图的通道只有1,也就是没有其他的通道堆叠,所以对于0-255的数值,其实可以将每一个像素点都除以一个255,然后所得到的就是一个0-1的范围之间的二维数组,这堆小数点排列的二维数组数据便可以构成一幅灰度图片。
彩色图
而对于一张彩色的图片,就代表了其有三个通道,按我个人的理解是由于三种颜色可以组合成其他的任意一种颜色,所以这三通道二维数组便可以构造成一张彩色的图片。也就说,对于红黄蓝三个通道,每一个通道都一个类似灰度图的一个二维数组,来代表每一个通道的数据构成,如下所示:
而一般来说,一张彩色的图片,就是这三个通道的叠加组成,主观体现为:
2.卷积操作与过滤器
由于全连接层所设计的参数过大,在当时的年代硬件设备提供的能力不足,而卷积神经网络的出现可以较好的降低了参数的总数量,参考了人视野的局部相关性特点。
而其中的卷积概念,其具体的操作就是对于一个行列维数相同的矩阵,他们之间的卷积运算就相同位置的数据相乘,然后对这些全部相乘的的数据进行一个累加的操作,这个过程就是卷积,信号处理中的数学公式为:
而对于卷积神经网络来说,做卷积运算的是图片样本与一个个的卷积核(这些卷积核也称为过滤器)。卷积核可以由一个标量拓展成一个3x3的矩阵,然后用卷积核中的每一个元素,乘以对应输入矩阵中的对应元素,每计算出一次,就移动一格,或者几格。这样,卷积核作为输入矩阵上的一个移动窗口。
其中,每一种这样的卷积核就代表其会提取出图片的某一种特征,比如
- 锐化过滤器
- 边缘检测过滤器
3.卷积神经网络的具体操作
卷积过程
对于简单的一张灰度图片来说,以手写数据集的数据为例,其是一个通道的灰度图,而且像素点的组成是28*28的,现在假设有一张这样的照片,将其表示为(B,C,H,W),其中的B代表数据的数量,C代表通道数,H与W代表图片的像素组成。
对于这样的一张图片,现在挑选一个过滤器(卷积核)与其进行一个卷积运算,便会得到一个新的矩阵,称为feature map
如果卷积矩阵是3x3,步长设置为1,这这个新的feature map就会是2626,如果在原来的minst图的上下左右加0变成一个3030的矩阵,则卷积出来feature map就同样是28*28的大小
而如果使用n个卷积核(过滤器),然后将得到的feature map进行叠加,便会得到一个n通道的卷积结果
使用了多个卷积核,就代表了对原来的图片进行检测n种不同的属性,所以这个维度是会增加的,这个维度的大小取决于使用kernel的数量。
以下区分一下概念:
Input_channels:原始照片的颜色通道数,灰度图片是1,彩色图片是3
Kernel_channels:所使用的卷积核的数量
Kernel_size:卷积核的矩阵大小
Stride:卷积核的步长,也就是卷积核一次移动的距离
Padding:在原始照片周围打0的圈数
- 对于更常见的现象是,对于彩色的照片做卷积运算
对于一些参数的解释:
x:[b,3,28,28]
对于彩色的图片,其颜色的通道数是3,而对于minst图片其像素是28*28,其中的b代表的是b张minst图片
one kernel:[3,3,3]
对于一个卷积核,[3,3,3]最后的两个3表示的是卷积核矩阵的大小,而第一个3是与图片的颜色通道相对应,由于彩色的图片是拥有rgb三种通道,如上图所示。对于每一个通道对应一个kernel,这三个通道加起来就是一个Kernel_channels,也就是第一个参数需要完全和输入的通道数匹配起来
multi-kernel:[16,3,3,3]
而对于多卷积核,也就是使用了多个过滤器来对原始的图片作为特征提取,第一个参数就表示假设使用了16个过滤器。
bias:[16]
每一个Kernel_channels都有一个偏置,所以如果用了16个过滤器来进行特征提取,就会有16个偏置
out:[b,16,28,28]
原始的输入数据是b张照片,所以输出也会是b个内容,16代表的是经过了16个过滤器得到的16个特征叠加在一起,由于设置了Padding,所以还是28*28
- 对卷积结果的解释:
根据以下经验的解释是,低层维度的提取是一些低层的特征
对于中层提取的小维的概念,开始无法解释,高层就是一些更高维的概念等等,越来越抽象,也就是特征的不断提取的过程
代码实现
- nn.Conv2d的使用
# 第一个例子 # F.conv2d = nn.Conv2d # F.conv2d是一个函数的操作,而nn.Conv2d是一个类操作 layer = nn.Conv2d(in_channels=1,out_channels=3,kernel_size=3,stride=1,padding=0) # Conv2d:函数名字含义就是做一个2d的函数卷积运算 # in_channels=1:表示输入图片的通道数,由于是灰度的图片,所以设置为1 # out_channels=3:表示使用过滤器的数目为3 # kernel_size=3:表示卷积核的矩阵大小是3x3 # stride=1:表示补步长设置为1 # padding=0:表示没有对图片进行补偶读拓展 x = torch.rand(1,1,28,28) # 输入是2张minst数据图片,通道数为1 out = layer.forward(x) # out.shape输出为:torch.Size([1, 3, 26, 26]) # 由于没有设置padding与设置了stride=1,所以由原来的28*28变成了26*26 # 由于只输入了1张图片,所以最高位是1,而且由于使用了3个过滤器,所以第二维度是3 # 而且这句可以被替换为 # out = layer(x) # 第二个例子 layer = nn.Conv2d(in_channels=1,out_channels=3,kernel_size=3,stride=1,padding=1) # 这次设置了padding=1,也就是进行了一圈的补0操作 x = torch.rand(1,1,28,28) layer(x).shape # out.shape输出为:torch.Size([1, 3, 28, 28]) # 由于原图是28*28,还进行了补0操作,所以输出还是28*28 # 第三个例子 layer = nn.Conv2d(in_channels=1,out_channels=3,kernel_size=3,stride=2,padding=1) # 步长为2,也就是输出的矩阵将会减半操作 x = torch.rand(3,1,28,28) # 输入设置为3张 layer(x).shape # out.shape输出为:torch.Size([3, 3, 14, 14]) # 第四个例子 layer = nn.Conv2d(in_channels=3,out_channels=3,kernel_size=3,stride=2,padding=1) # 设置输入的通道数是3 x = torch.rand(10,3,28,28) # 过滤器的通道数一定要和输入图片的通道数相等,否则会报错处理 layer(x).shape # out.shape输出为:torch.Size([10, 3, 14, 14])
- F.conv2d的使用
# F.conv2d的使用 x = torch.rand(1,3,28,28) # 图片样本数是1,通道是3,大小是28*28 w = torch.rand(16,3,3,3) # 过滤器数量是16,通道是3,卷积核大小是3*3 b = torch.rand(16) # 偏置数量与卷积核数量一致都是16 out = F.conv2d(input=x,weight=w,bias=b,stride=1,padding=1) # 步长是1,填充1圈 out.shape # 输出为:torch.Size([1, 16, 28, 28]) # 输出1个结果因为只有1张图片是输入,卷积后的通道数是16由于使用了16个卷积核,大小不变
- 网络结构参数的查看
# 观察网络结构的参数 layer = nn.Conv2d(in_channels=1,out_channels=3,kernel_size=3,stride=2,padding=1) x = torch.rand(3,1,28,28) layer(x).shape # out.shape输出为:torch.Size([3, 3, 14, 14]) # 查看weight layer.weight # Parameter containing: # tensor([[[[-0.3117, 0.1637, 0.1690], # [ 0.0798, -0.1686, -0.0726], # [-0.2899, 0.1273, 0.3168]]], # # # [[[ 0.2656, 0.1425, -0.2783], # [ 0.1055, 0.2423, 0.0910], # [ 0.2852, -0.2426, -0.0128]]], # # # [[[ 0.2536, 0.0558, -0.2349], # [ 0.0722, 0.0460, -0.1613], # [ 0.2438, -0.0269, 0.0463]]]], requires_grad=True) layer.weight.shape # 输出为:torch.Size([3, 1, 3, 3]) # 由于kernel的大小是3*3,所以最后两个参数是3,3,而由于设置了kernel只有一个通道,所以第二个参数是1 # 第一个参数是3,原因是使用了3个过滤器提取特征 layer.bias.shape # 输出为:torch.Size([3]) # 有一个卷积核就有几个bias,使用了3个过滤器提取特征所以shape是3
4.池化层(Pooling Sampling)
下采样
- 下采样操作
也就是隔行采样操作来实现降维
- 最大池化操作(Max pooling)
对于单位的窗口中挑选出数值最高的来作为输出值,除了Max pooling还有Avg pooling,就是对于单位的窗口的数值求均值来作为输出值
测试代码:
# nn.MaxPool2d使用例子 # 其中的2d表示是对二维的图片进行处理 x = out # x.shape为:torch.Size([1, 16, 14, 14]) # 卷积之后的结果作为池化层的输入 layer = nn.MaxPool2d(kernel_size=2,stride=2) # kernel_size=2:表达滑动窗口是2*2 # stride=2:表示移动的步长是2 layer(x).shape # 输出为:torch.Size([1, 16, 7, 7]) # 由于设置了步长是2,所以实现了降维操作,维度也从14*14变成7*7 # F.avg_pool2d的使用例子 # 其中的2d表示是对二维的图片进行处理 out = F.avg_pool2d(input=x,kernel_size=2,stride=2) # x是输入,步长为2,窗口大小是2 out.shape # 输出为:torch.Size([1, 16, 7, 7]) # F.avg_pool2d功能与nn.MaxPool2d是一样的
上采样
- 上采样操作
其实numpy或者其他的很多工具包都是有这样的操作的,但是pytorch为了满足自身的tensor数据类型还是封装了这样的一个功能。
上采用操作就是实现放大的功能
测试代码:
# 上采样操作 out.shape # 原本的输出为:torch.Size([1, 16, 7, 7]) # 进行上采样也就是放大操作 out = F.interpolate(input=out,scale_factor=2,mode='nearest') # scale_factor=2:表示输出结果放大2倍 # mode='nearest':表示使用某一个模式,nearest是近邻插值 out.shape # 输出为:torch.Size([1, 16, 14, 14]) # 可以结果放大了1倍 # 进一步放大 out = F.interpolate(input=out,scale_factor=3,mode='nearest') # scale_factor=2:表示输出结果放大3倍 out.shape # 输出为:torch.Size([1, 16, 42, 42]) # 可见,在原来的基础上又放大了3倍
5.批处理规范(Batch Norm)
Image Normalization
由于Sigmoid函数大于一定数值或者小于一定数值,也就是不在一段有效区间范围内时或出现梯度弥散情况,对于这种情况的梯度会很多时候得不到更新,所以希望输入的值可以能控制在一个有效的范围之内
这个方法的主要思路就是将数据的大小最好集中在0附近,然后方差就有一个比较小的范围。而对于彩色的3通道图像来说,对于没一个通道都有一个均值与方差,然后根据计算公式就可以分布在一个接近于0的区域。
Batch Normalization
- Batch Norm
假设输入的数据是[6,3,784],这表示有6张彩色的图片,每张图片的像素分布是28*28,Batch Norm是对通道进行统计处理,去全部实例上的通道1,通道2,通道3的均值,也就是根据3个通道统计出3个均值,也就有3个方差。
- Layer Norm
简单的来说Batch Norm是根据通道数来计算均值,而Layer Norm就是根据实例数来计算均值,所以对于输入的数据是[6,3,784],由于有6张照片,所以会有6个均值与6个方差。
- Instance Norm
Instance Norm统计的是当前的实例的当前的均值,同样的,假设输入的数据是[6,3,784],那么根据6个照片就算6个实例,3个通道,所以其会用6*3=18个均值,还有18个方差。
Batch Norm规范化的过程:
测试代码:
- nn.BatchNorm1d操作
# nn.BatchNorm1d的使用例子 x = torch.rand(100,16,784) # x是来至0-1的均匀分布,表示的是100张图片,16个通道,大小是28*28 layer = nn.BatchNorm1d(16) # BatchNorm1d的原因是现在已经将28*28的数据表示成1维的了,所以需要用1d # 而由于Batch Norm是根据通道数来做均值和方差的,所以参数是16 out = layer(x) # out.shape输出为:torch.Size([100, 16, 784]) layer.running_mean # 由于x是0-1的均匀分布,所以均值是0.5左右,验证一下 # tensor([0.0499, 0.0499, 0.0499, 0.0500, 0.0500, 0.0500, 0.0500, 0.0500, 0.0501, # 0.0501, 0.0500, 0.0499, 0.0502, 0.0500, 0.0501, 0.0502]) layer.running_var # 方差应该是1,验证一下 # tensor([0.9084, 0.9084, 0.9083, 0.9083, 0.9083, 0.9083, 0.9083, 0.9083, 0.9084, # 0.9084, 0.9084, 0.9084, 0.9084, 0.9083, 0.9083, 0.9084]) # 其中的running_u与running_θ根据当前的μ与θ来更新
- nn.BatchNorm2d操作
# nn.BatchNorm2d的使用例子 x.shape # 输出为:torch.Size([1, 16, 7, 7]) layer = nn.BatchNorm2d(16) # 由于现在是输入x没有将维度变成1维,这个照片还是用2维表示的,所以是2d # 而参数必须要与输入通道数相同,输入的通道是16,所以这里的参数也必须是16 out = layer(x) # out.shape输出为:torch.Size([1, 16, 7, 7]) # 因为进行批处理是不会改变维度的 layer.weight # 可以输出相关参数的信息 # Parameter containing: # tensor([1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.], # requires_grad=True) layer.weight.shape # 输出为:torch.Size([16]) layer.bias.shape # 输出为:torch.Size([16]) # 将layer当前的参数全部打印出来 vars(layer) # {'training': True, # '_parameters': OrderedDict([('weight', Parameter containing: # tensor([1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.], # requires_grad=True)), # ('bias', # Parameter containing: # tensor([0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], # requires_grad=True))]), # '_buffers': OrderedDict([('running_mean', # tensor([0.0477, 0.0495, 0.0464, 0.0551, 0.0555, 0.0441, 0.0553, 0.0496, 0.0551, # 0.0480, 0.0546, 0.0461, 0.0454, 0.0484, 0.0478, 0.0562])), # ('running_var', # tensor([0.9099, 0.9076, 0.9070, 0.9073, 0.9073, 0.9079, 0.9089, 0.9092, 0.9087, # 0.9077, 0.9074, 0.9085, 0.9096, 0.9076, 0.9070, 0.9089])), # ('num_batches_tracked', tensor(1))]), # '_non_persistent_buffers_set': set(), # '_backward_hooks': OrderedDict(), # '_forward_hooks': OrderedDict(), # '_forward_pre_hooks': OrderedDict(), # '_state_dict_hooks': OrderedDict(), # '_load_state_dict_pre_hooks': OrderedDict(), # '_modules': OrderedDict(), # 'num_features': 16, # 'eps': 1e-05, # 'momentum': 0.1, # 'affine': True, # 'track_running_stats': True} # running_mean就是是μ,而running_var就是σ;weight相当于是γ,bias相当于是β # 'affine': False表示不适应β与γ的变换,而且不会自动更新
注意,Batch Norm与Drop Out的使用例子一样,train与test的使用例子是不一样的,test是不需要跟新权值的,因为其只是作为测试效果,没有β与γ可以更新,而实现这种行为需要将test的行为调整过来之后,再使用batchnorm进行变换,如图所示
- 使用Batch Norm的效果:
1)收敛速度更快
2)得到更好的结果
3)更加的稳定
6.经典神经网络的介绍
LeNet-5
这个卷积神经网络是80年代的产物。其有两个卷积层与一个池化层,最后再连接两个全连接层所组成。大概有5-6层组成。
AlexNet
由于当时的显卡性能不算出色,不能在一块上面完成任务,所以必须分为两个部分去训练,来讲显存分配开来。输入图像为3通道224*224。
模型特点:
1)由5个卷积层与3个全连接层组成
2)使用了Max pooling的操作
3)使用ReLU激活函数
4)使用Dropout来防止过拟合
5)具备一些很好的训练技巧,包括数据增广,学习率策略,Weight Decay等
VGG
VGG-Nets是有牛津大学VGG(Visual Geometry Group)提出,层数高达16或19层,一共有六个版本。
模型特点:
1)具有更深的网络结构
2)使用了较小的3x3的卷积核或者是1x1的小窗口
ps:1x1的卷积核实现了维度的改变,这是因为输出的维度只取决于使用使用多少个卷积核,而去输入的维度是没有关系的,输入的维度只有使用卷积核的通道数有关系。
GoogLeNet
模型特点:
1)使用了大小更小的卷积和,33或者甚至是11
2)采用全局平均池化层
3)使用了多个不同size的卷积核去提取特征,然后将结果聚合
ResNet
层数越高的网络其实在当时并没有提升准确率,有时时候甚至网络的层数越多,准确率越低,而ResNet可以解决这个问题,使其使用更深层次的网络的时候不至于比浅层次的网络的准确率要低。
其结构如下图,也就是ResNet是可以简化成VGG-19或者是其他的网络的
DenseNet
DenseNet是在ResNet的基础上拓展的,也就是后面的每一层都不仅仅与前一层有联系,还可以与前面的所有层有联系,结构图改变如下:
这样改变之后,通道数会越来越大,这一点需要注意