实验目录
选做题:使用pytorch实现Convolution Demo
2. Pytorch:torch.nn.Conv2d()代码实现
前言
这次真的是弄点细了,这次研究bias格式研究了半天,研究max的那个问题研究了一天,而且这次写的真有点赶,但是也算是研究出来,以后努力吧,还是我太菜了,要不然就不会研究这么长时间了
我写的不太好,请老师和各位大佬多教教我。
一、卷积神经网络的基础算子
积神经网络是目前计算机视觉中使用最普遍的模型结构,如图5.8 所示,由M个卷积层和b个汇聚层组合作用在输入图片上,在网络的最后通常会加入K个全连接层。
从上图可以看出,卷积网络是由多个基础的算子组合而成。下面我们先实现卷积网络的两个基础算子:卷积层算子和汇聚层算子。
5.2.1卷积算子
卷积层是指用卷积操作来实现神经网络中一层。为了提取不同种类的特征,通常会使用多个卷积核一起进行特征提取。
5.2.1.1 多通道卷积
在前面介绍的二维卷积运算中,卷积的输入数据是二维矩阵。但实际应用中,一幅大小为M×N的图片中的每个像素的特征表示不仅仅只有灰度值的标量,通常有多个特征,可以表示为D维的向量,比如RGB三个通道的特征向量。因此,图像上的卷积操作的输入数据通常是一个三维张量,分别对应了图片的高度M、宽度N和深度D,其中深度D通常也被称为输入通道数D。如果输入如果是灰度图像,则输入通道数为1;如果输入是彩色图像,分别有R、G、B三个通道,则输入通道数为3。
此外,由于具有单个核的卷积每次只能提取一种类型的特征,即输出一张大小为U×V的特征图(Feature Map)。而在实际应用中,我们也希望每一个卷积层能够提取多种不同类型的特征,所以一个卷积层通常会组合多个不同的卷积核来提取特征,经过卷积运算后会输出多张特征图,不同的特征图对应不同类型的特征。输出特征图的个数通常将其称为输出通道数P。
一般情况下,我们会 使用多了filters同时卷积,比如,如果我们同时使用4个filter的话,那么输出的维度则会变为(6,6,4)。
同时有4个filter,图中的输入图像是(8,8,3),filter有4个,大小均为(3,3,3),得到的输出为(6,6,4)。我觉得这个图已经画的很清晰了,而且给出了3和4这个两个关键数字是怎么来的,所以我就不啰嗦了(这个图画了我起码40分钟)。
在前面的图中,我加一个激活函数,给对应的部分标上符号,就是这样的:
多张输出特征图的计算
5.2.1.2 多通道卷积层算子
1. 多通道卷积卷积层的代码实现
2. Pytorch:torch.nn.Conv2d()代码实现
3. 比较自定义算子和框架中的算子
参考代码:
# coding=gbk
import torch
import torch.nn as nn
from torch.nn.init import constant_, normal_, uniform_
class Conv2D(nn.Module):
def __init__(self, in_channels, out_channels, kernel_size, stride=1, padding=0):
weight_attr = constant_(torch.empty(size=(out_channels, in_channels, kernel_size, kernel_size)),val=1.0)
bias_attr = constant_(torch.empty(size=(out_channels,1)), val=0.0)
super(Conv2D, self).__init__()
# 创建卷积核
self.weight = torch.nn.parameter.Parameter(weight_attr,requires_grad=True)
# 创建偏置
self.bias = torch.nn.parameter.Parameter(bias_attr,requires_grad=True)
self.stride = stride
self.padding = padding
# 输入通道数
self.in_channels = in_channels
# 输出通道数
self.out_channels = out_channels
# 基础卷积运算
def single_forward(self, X, weight):
# 零填充
new_X = torch.zeros([X.shape[0], X.shape[1] + 2 * self.padding, X.shape[2] + 2 * self.padding])
new_X[:, self.padding:X.shape[1] + self.padding, self.padding:X.shape[2] + self.padding] = X
u, v = weight.shape
output_w = (new_X.shape[1] - u) // self.stride + 1
output_h = (new_X.shape[2] - v) // self.stride + 1
output = torch.zeros([X.shape[0], output_w, output_h])
for i in range(0, output.shape[1]):
for j in range(0, output.shape[2]):
output[:, i, j] = torch.sum(
new_X[:, self.stride * i:self.stride * i + u, self.stride * j:self.stride * j + v] * weight,
dim=[1, 2])
return output
def forward(self, inputs):
"""
输入:
- inputs:输入矩阵,shape=[B, D, M, N]
- weights:P组二维卷积核,shape=[P, D, U, V]
- bias:P个偏置,shape=[P, 1]
"""
feature_maps = []
# 进行多次多输入通道卷积运算
p = 0
for w, b in zip(self.weight, self.bias): # P个(w,b),每次计算一个特征图Zp
multi_outs = []
# 循环计算每个输入特征图对应的卷积结果
for i in range(self.in_channels):
single = self.single_forward(inputs[:, i, :, :], w[i])
multi_outs.append(single)
# print("Conv2D in_channels:",self.in_channels,"i:",i,"single:",single.shape)
# 将所有卷积结果相加
feature_map = torch.sum(torch.stack(multi_outs), dim=0) + b # Zp
feature_maps.append(feature_map)
# print("Conv2D out_channels:",self.out_channels, "p:",p,"feature_map:",feature_map.shape)
p += 1
# 将所有Zp进行堆叠
out = torch.stack(feature_maps, 1)
return out
inputs = torch.tensor([[[[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]],
[[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]]]])
conv2d = Conv2D(in_channels=2, out_channels=3, kernel_size=2)
print("inputs shape:", inputs.shape)
outputs = conv2d(inputs)
print("Conv2D outputs shape:", outputs.shape)
# 比较与paddle API运算结果
conv2d_paddle = nn.Conv2d(in_channels=2, out_channels=3, kernel_size=2)
conv2d_paddle.weight=torch.nn.parameter.Parameter(constant_(conv2d_paddle.weight,val=1.0))
conv2d_paddle.bias=torch.nn.parameter.Parameter(constant_(conv2d_paddle.bias,val=0.0))
outputs_paddle = conv2d_paddle(inputs)
# 自定义算子运算结果
print('Conv2D outputs:', outputs)
# paddle API运算结果
print('nn.Conv2D outputs:', outputs_paddle)
运行结果为:
inputs shape: torch.Size([1, 2, 3, 3])
Conv2D outputs shape: torch.Size([1, 3, 2, 2])
Conv2D outputs: tensor([[[[20., 28.],
[44., 52.]],[[20., 28.],
[44., 52.]],[[20., 28.],
[44., 52.]]]], grad_fn=<StackBackward0>)
nn.Conv2D outputs: tensor([[[[20., 28.],
[44., 52.]],[[20., 28.],
[44., 52.]],[[20., 28.],
[44., 52.]]]], grad_fn=<ThnnConv2DBackward0>)
这里必须要说一下这个bias的格式,这个研究了好久。
并且咱们主要看bias的格式,我一开始看了半天以为是哪算法出错了,但是我发现下边的。
上边的weight咱们经常传卷积核,但是bais,咱们要是传的话必须传一个二维的,一般正常的思路是(1,out_channels),而如果要是用conv2d函数的话,确实是这样的。
但是以这样的思路来处理咱们自定义的那个conv2d函数的话,是不太对的,会报错。
RuntimeError: The size of tensor a (2) must match the size of tensor b (3) at non-singleton dimension 2
很明显这个是格式错了,但是为啥不对呢,原因就在下边。
for w, b in zip(self.weight, self.bias): # P个(w,b),每次计算一个特征图Zp
multi_outs = []
# 循环计算每个输入特征图对应的卷积结果
for i in range(self.in_channels):
single = self.single_forward(inputs[:, i, :, :], w[i])
multi_outs.append(single)
这个是因为zip函数的特性,来进行的,只有写成(out_channel,1),才能变成每个卷积核对应一个偏置。
5.2.1.3 卷积算子的参数量和计算量
5.2.2 汇聚层算子
汇聚层的作用是进行特征选择,降低特征数量,从而减少参数数量。由于汇聚之后特征图会变得更小,如果后面连接的是全连接层,可以有效地减小神经元的个数,节省存储空间并提高计算效率。
常用的汇聚方法有两种,分别是:平均汇聚和最大汇聚。
- 平均汇聚:将输入特征图划分为2×22×2大小的区域,对每个区域内的神经元活性值取平均值作为这个区域的表示;
- 最大汇聚:使用输入特征图的每个子区域内所有神经元的最大活性值作为这个区域的表示。
汇聚层输出的计算尺寸与卷积层一致,对于一个输入矩阵X∈RM×N和一个运算区域大小为U×V的汇聚层,步长为S,对输入矩阵进行零填充,那么最终输出矩阵大小则为
由于过大的采样区域会急剧减少神经元的数量,也会造成过多的信息丢失。目前,在卷积神经网络中比较典型的汇聚层是将每个输入特征图划分为2×22×2大小的不重叠区域,然后使用最大汇聚的方式进行下采样。
由于汇聚是使用某一位置的相邻输出的总体统计特征代替网络在该位置的输出,所以其好处是当输入数据做出少量平移时,经过汇聚运算后的大多数输出还能保持不变。比如:当识别一张图像是否是人脸时,我们需要知道人脸左边有一只眼睛,右边也有一只眼睛,而不需要知道眼睛的精确位置,这时候通过汇聚某一片区域的像素点来得到总体统计特征会显得很有用。这也就体现了汇聚层的平移不变特性。
汇聚层的参数量和计算量
由于汇聚层中没有参数,所以参数量为00;最大汇聚中,没有乘加运算,所以计算量为00,而平均汇聚中,输出特征图上每个点都对应了一次求平均运算。
使用飞桨实现一个简单的汇聚层,代码实现如下:
class Pool2D(nn.Module):
def __init__(self, size=(2, 2), mode='max', stride=1):
super(Pool2D, self).__init__()
# 汇聚方式
self.mode = mode
self.h, self.w = size
self.stride = stride
def forward(self, x):
output_w = (x.shape[2] - self.w) // self.stride + 1
output_h = (x.shape[3] - self.h) // self.stride + 1
output = torch.zeros([x.shape[0], x.shape[1], output_w, output_h])
# 汇聚
for i in range(output.shape[2]):
for j in range(output.shape[3]):
# 最大汇聚
if self.mode == 'max':
output[:, :, i, j] = torch.max(
x[:, :, self.stride * i:self.stride * i + self.w, self.stride * j:self.stride * j + self.h])
# 平均汇聚
elif self.mode == 'avg':
output[:, :, i, j] = torch.mean(
x[:, :, self.stride * i:self.stride * i + self.w, self.stride * j:self.stride * j + self.h],
dim=[2, 3])
return output
inputs = torch.tensor([[[[1., 2., 3., 4.], [5., 6., 7., 8.], [9., 10., 11., 12.], [13., 14., 15., 16.]]]])
pool2d = Pool2D(stride=2)
outputs = pool2d(inputs)
print("input: {}, \noutput: {}".format(inputs.shape, outputs.shape))
# 比较Maxpool2D与paddle API运算结果
maxpool2d_paddle = nn.MaxPool2d(kernel_size=(2, 2), stride=2)
outputs_paddle = maxpool2d_paddle(inputs)
# 自定义算子运算结果
print('Maxpool2D outputs:', outputs)
# paddle API运算结果
print('nn.Maxpool2D outputs:', outputs_paddle)
# 比较Avgpool2D与paddle API运算结果
avgpool2d_paddle = nn.AvgPool2d(kernel_size=(2, 2), stride=2)
outputs_paddle = avgpool2d_paddle(inputs)
pool2d = Pool2D(mode='avg', stride=2)
outputs = pool2d(inputs)
# 自定义算子运算结果
print('Avgpool2D outputs:', outputs)
# paddle API运算结果
print('nn.Avgpool2D outputs:', outputs_paddle)
运行结果为:
input: torch.Size([1, 1, 4, 4]),
output: torch.Size([1, 1, 2, 2])
Maxpool2D outputs: tensor([[[[ 6., 8.],
[14., 16.]]]])
nn.Maxpool2D outputs: tensor([[[[ 6., 8.],
[14., 16.]]]])
Avgpool2D outputs: tensor([[[[ 3.5000, 5.5000],
[11.5000, 13.5000]]]])
nn.Avgpool2D outputs: tensor([[[[ 3.5000, 5.5000],
[11.5000, 13.5000]]]])
这个需要注意max的问题,我会在最后专门说一下的,因为,这个真的研究的很细,所以大家如果有不明白的,可以看一看。
选做题:使用pytorch实现Convolution Demo
首先,咱先说一下,这是啥东西,这是斯坦福大学李飞飞视觉识别课程,Convolutional Neural Networks for Visual Recognition,即面向视觉识别的卷积神经网络。该课程是斯坦福大学计算机视觉实验室推出的课程。
关于该课程:
本课程将深入讲解深度学习框架的细节问题,聚焦面向视觉识别任务(尤其是图像分类任务)的端到端学习模型。在10周的课程中,学生们将会学习如何实现、训练和调试他们自己的神经网络,并建立起对计算机视觉领域的前沿研究方向的细节理解。最终的作业将包括训练一个有几百万参数的卷积神经网络,并将其应用到最大的图像分类数据库(ImageNet)上。我们将会聚焦于教授如何确定图像识别问题,学习算法(比如反向传播算法),对网络的训练和精细调整(fine-tuning)中的工程实践技巧,指导学生动手完成课程作业和最终的课程项目。本课程的大部分背景知识和素材都来源于ImageNet Challenge竞赛。
这一段,比较官方的解释是
卷积演示。下面是一个CONV层的运行演示。由于三维体积很难可视化,所有的体积(输入体积(蓝色),权重体积(红色),输出体积(绿色))被可视化,每个深度切片堆叠成行。输入体积的大小是W1=5,H1=5,D1=3,CONV层的参数是K=2,F=3,S=2,P=1。也就是说,我们有两个大小为3×3的滤波器,它们的跨度为2。因此,输出体积的空间大小为(5-3+2)/2+1=3。此外,注意到P=1的填充被应用于输入体积,使输入体积的外边界为零。下面的可视化图对输出激活(绿色)进行了迭代,并显示每个元素都是通过将突出显示的输入(蓝色)与滤波器(红色)进行元素相乘,然后相加,再将结果与偏置相抵消来计算的。
这个翻译,是我找到的,有大神看了他的课,翻译出来的,我发现我自己翻译的和这个一比,狗屁不是,所以,我就不发我的翻译了。
2. 代码实现下图
1. 多通道卷积卷积层的代码实现
# coding=gbk
import torch
import torch.nn as nn
from torch.nn.init import constant_, normal_, uniform_
class Conv2D(nn.Module):
def __init__(self, in_channels, out_channels, kernel_size, stride=1, padding=0):
weight_attr = x=torch.tensor([[[[-1,1,0]
,[0,1,0]
,[0,1,1]]
,[[-1,-1,0]
,[0,0,0]
,[0,-1,0]]
,[[0,0,-1]
,[0,1,0]
,[1,-1,-1]]],
[[[1, 1, -1],
[-1, -1, 1],
[0, -1, 1]],
[[0, 1, 0],
[-1, 0, -1],
[-1, 1, 0]],
[[-1, 0, 0],
[-1, 0, 1],
[-1, 0, 0]]]],dtype=torch.float32)
bias_attr = torch.tensor([1.0,0.0])
super(Conv2D, self).__init__()
# 创建卷积核
self.weight = torch.nn.parameter.Parameter(weight_attr,requires_grad=True)
# 创建偏置
self.bias = torch.nn.parameter.Parameter(bias_attr,requires_grad=True)
self.stride = stride
self.padding = padding
# 输入通道数
self.in_channels = in_channels
# 输出通道数
self.out_channels = out_channels
# 基础卷积运算
def single_forward(self, X, weight):
# 零填充
new_X = torch.zeros([X.shape[0], X.shape[1] + 2 * self.padding, X.shape[2] + 2 * self.padding])
new_X[:, self.padding:X.shape[1] + self.padding, self.padding:X.shape[2] + self.padding] = X
u, v = weight.shape
output_w = (new_X.shape[1] - u) // self.stride + 1
output_h = (new_X.shape[2] - v) // self.stride + 1
output = torch.zeros([X.shape[0], output_w, output_h])
for i in range(0, output.shape[1]):
for j in range(0, output.shape[2]):
output[:, i, j] = torch.sum(
new_X[:, self.stride * i:self.stride * i + u, self.stride * j:self.stride * j + v] * weight,
dim=[1, 2])
return output
def forward(self, inputs):
"""
输入:
- inputs:输入矩阵,shape=[B, D, M, N]
- weights:P组二维卷积核,shape=[P, D, U, V]
- bias:P个偏置,shape=[P, 1]
"""
feature_maps = []
# 进行多次多输入通道卷积运算
p = 0
for w, b in zip(self.weight, self.bias): # P个(w,b),每次计算一个特征图Zp
multi_outs = []
# 循环计算每个输入特征图对应的卷积结果
for i in range(self.in_channels):
single = self.single_forward(inputs[:, i, :, :], w[i])
multi_outs.append(single)
# print("Conv2D in_channels:",self.in_channels,"i:",i,"single:",single.shape)
# 将所有卷积结果相加
feature_map = torch.sum(torch.stack(multi_outs), dim=0) + b # Zp
feature_maps.append(feature_map)
# print("Conv2D out_channels:",self.out_channels, "p:",p,"feature_map:",feature_map.shape)
p += 1
# 将所有Zp进行堆叠
out = torch.stack(feature_maps, 1)
return out
inputs = torch.tensor([[[[0.0, 1.0, 1.0,0.0,2.0]
, [2.0, 2.0, 2.0,2.0,1.0]
, [1.0, 0.0, 0.0,2.0,0.0]
,[0.0,1.0,1.0,0.0,0.0]
,[1.0,2.0,0.0,0.0,2.0]]
,[[1.0, 0.0, 2.0,2.0,0.0]
, [0.0, 0.0, 0.0,2.0,0.0]
, [1.0, 2.0, 1.0,2.0,1.0]
,[1.0,0.0,0.0,0.0,0.0]
,[1.0,2.0,1.0,1.0,1.0]]
,[[2.0,1.0,2.0,0.0,0.0]
,[1.0,0.0,0.0,1.0,0.0]
,[0.0,2.0,1.0,0.0,1.0]
,[0.0,1.0,2.0,2.0,2.0]
,[2.0,1.0,0.0,0.0,1.0]]]],dtype=torch.float32)
conv2d = Conv2D(in_channels=3, out_channels=2, kernel_size=3,padding=1,stride=2)
print("inputs shape:", inputs.shape)
outputs = conv2d(inputs)
print("Conv2D outputs shape:", outputs.shape)
运行结果为:
inputs shape: torch.Size([1, 3, 5, 5])
Conv2D outputs shape: torch.Size([1, 2, 3, 3])
Conv2D outputs: tensor([[[[ 6., 7., 5.],
[ 3., -1., -1.],
[ 2., -1., 4.]],[[ 2., -5., -8.],
[ 1., -4., -4.],
[ 0., -5., -5.]]]], grad_fn=<StackBackward0>)
2. Pytorch:torch.nn.Conv2d()代码实现
# 比较与paddle API运算结果
conv2d_paddle = nn.Conv2d(in_channels=3, out_channels=2, kernel_size=3,padding=1,stride=2)
x=torch.tensor([[[[-1,1,0]
,[0,1,0]
,[0,1,1]]
,[[-1,-1,0]
,[0,0,0]
,[0,-1,0]]
,[[0,0,-1]
,[0,1,0]
,[1,-1,-1]]],
[[[1, 1, -1],
[-1, -1, 1],
[0, -1, 1]],
[[0, 1, 0],
[-1, 0, -1],
[-1, 1, 0]],
[[-1, 0, 0],
[-1, 0, 1],
[-1, 0, 0]]]],dtype=torch.float32)
conv2d_paddle.weight=torch.nn.parameter.Parameter(x)
conv2d_paddle.bias=torch.nn.parameter.Parameter(torch.tensor([1.0,0.0]))
outputs_paddle = conv2d_paddle(inputs)
# 自定义算子运算结果
print('Conv2D outputs:', outputs)
# paddle API运算结果
print('nn.Conv2D outputs:', outputs_paddle)
运行结果为:
nn.Conv2D outputs: tensor([[[[ 6., 7., 5.],
[ 3., -1., -1.],
[ 2., -1., 4.]],[[ 2., -5., -8.],
[ 1., -4., -4.],
[ 0., -5., -5.]]]], grad_fn=<ThnnConv2DBackward0>)
这个就是上边我说过的问题,记得看好bais的定义形状,剩下的就是这个是3通道变2通道,然后卷积核的形状是3,记得卷积核的形状就是之前写过的四位。
关于池化中遇到的max函数的问题
首先,说一下问题出在哪一行。
for i in range(output.shape[2]):
for j in range(output.shape[3]):
# 最大汇聚
if self.mode == 'max':
output[:, :, i, j] = torch.max(
x[:, :, self.stride * i:self.stride * i + self.w, self.stride * j:self.stride * j + self.h])
问题出在这个代码的max函数,一开始paddle的代码,dim是一个列表,但是torch中并不能这么用,所以这个的问题就是,这个是啥意思。
我研究了好长时间,包括max中dim选择的意思,最后终于研究出来了,
Pytorch中tensor维度和torch.max()函数中dim参数的理解_twelve13的博客-优快云博客_torch.max(dim)
Pytorch笔记:维度dim的定义及其理解使用_Activewaste的博客-优快云博客_深度学习dim
这两个看了就明白了,
然后说一下,意思就是下边的图
从前两维的遍历可以看出这个就是,相当于对每个二维的进行操作,然后再合起来,也就是上边的图,所以直接删了,相当于求二维图片中数值的最大值。所以直接删了维度选择就行了。直接求全部最大就行了。
总结
首先,这次,真有点赶,主要也是研究的有点细了,因为,研究max的问题研究了一天,然后格式的问题研究好长时间,研究的有点细了,以后不能这么细了。
其次,是手动实现了一下卷积层和池化层,真学到了好多,去查了官方文档,知道了为啥自己写和官方那个文档的bias正好相反,学到了好多。
其次,是真的好好弄了弄通道的含义,之前都是有点一直半解,这次写作业好好掠了掠,尤其是最后一个选做,才真的是清晰了好多。
其次,是学了一下dim的含义,感觉这个真的有用,以后会用到。
最后,当然是谢谢老师,谢谢老师在学习和生活上的关心。