5.2 卷积神经网络的基础算子
我们先实现卷积网络的两个基础算子:卷积层算子和汇聚层算子。
5.2.1 卷积算子
卷积层是指用卷积操作来实现神经网络中一层。
为了提取不同种类的特征,通常会使用多个卷积核一起进行特征提取。
5.2.1.1 多通道卷积
5.2.1.2 多通道卷积层算子
-
多通道卷积卷积层的代码实现
-
Pytorch:torch.nn.Conv2d()代码实现
-
比较自定义算子和框架中的算子
import torch.nn as nn
import torch
class Conv2D(nn.Module):
def __init__(self, in_channels, out_channels, kernel_size, stride=1, padding=0):
super(Conv2D, self).__init__()
# 创建卷积核
self.weight = nn.Parameter(torch.ones(size=[out_channels, in_channels, kernel_size,kernel_size]))
self.bias = nn.Parameter(torch.ones(size=[out_channels,1]))
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,
axis=[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), axis=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.as_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)
# 比较与torch API运算结果
conv2d_torch = nn.Conv2d(in_channels=2, out_channels=3, kernel_size=2)
conv2d_torch.weight.data = nn.Parameter(torch.ones(size=[3, 2, 2, 2]))
conv2d_torch.bias.data = nn.Parameter(torch.ones(size=[3]))
outputs_torch = conv2d_torch(inputs)
# 自定义算子运算结果
print('Conv2D outputs:', outputs)
# torch API运算结果
print('nn.Conv2D outputs:', outputs_torch)
inputs shape: torch.Size([1, 2, 3, 3])
Conv2D outputs shape: torch.Size([1, 3, 2, 2])
Conv2D outputs: tensor([[[[21., 29.],
[45., 53.]],
[[21., 29.],
[45., 53.]],
[[21., 29.],
[45., 53.]]]], grad_fn=<StackBackward0>)
nn.Conv2D outputs: tensor([[[[21., 29.],
[45., 53.]],
[[21., 29.],
[45., 53.]],
[[21., 29.],
[45., 53.]]]], grad_fn=<ThnnConv2DBackward0>)
5.2.1.3 卷积算子的参数量和计算量
参数量:
输入特征图为cxhxw,输出特征图为oxhxw, (由于padding导致输入输出特征图不变)
普通卷积而言,卷积核的尺寸为k x k x c x o, 一共有o个kxkxc的卷积核
权重:k x k x c x o
偏置: o
BN:2 x o
总的参数数目:k x k x c x o + 3 x o
计算量
输入特征图为cxhxw,输出特征图为oxhxw, (由于padding导致输入输出特征图不变)
普通卷积而言,卷积核的尺寸为k x k x c x o, 一共有o个kxkxc的卷积核
权重:k x k x c x o
偏置: o
BN:2 x o
总的参数数目:k x k x c x o + 3 x o
5.2.2 汇聚层算子
汇聚层的作用是进行特征选择,降低特征数量,从而减少参数数量。
由于汇聚之后特征图会变得更小,如果后面连接的是全连接层,可以有效地减小神经元的个数,节省存储空间并提高计算效率。
常用的汇聚方法有两种,分别是:平均汇聚、最大汇聚。
-
代码实现一个简单的汇聚层。
-
torch.nn.MaxPool2d();torch.nn.avg_pool2d()代码实现
-
比较自定义算子和框架中的算子
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],
)
return output
inputs = torch.as_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与torch API运算结果
maxpool2d_torch = nn.MaxPool2d(kernel_size=(2, 2), stride=2)
outputs_torch = maxpool2d_torch(inputs)
# 自定义算子运算结果
print('Maxpool2D outputs:', outputs)
# torch API运算结果
print('nn.Maxpool2D outputs:', outputs_torch)
# 比较Avgpool2D与torch API运算结果
avgpool2d_torch = nn.AvgPool2d(kernel_size=(2, 2), stride=2)
outputs_torch = avgpool2d_torch(inputs)
pool2d = Pool2D(mode='avg', stride=2)
outputs = pool2d(inputs)
# 自定义算子运算结果
print('Avgpool2D outputs:', outputs)
# torch API运算结果
print('nn.Avgpool2D outputs:', outputs_torch)
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]]]])
选做题:使用pytorch实现Convolution Demo
- 翻译以下内容
卷积演示。下面是一个CONV层的运行演示。由于3D体积难以可视化,所以所有体积(输入体积(蓝色)、权重体积(红色)、输出体积(绿色))都用按行堆叠的每个深度切片来可视化,输入体积的大小为W =5,H=5,D1 = 3,CONV层参数为K=2,F=3,S=2,P= 1。也就是说,我们有两个大小为3×3的过滤器,它们的步长为2。因此,输出体积大小具有空间大小(5 -3 +2)/2 +1 = 3。此外,请注意,P= 1的添加应用于输入体积,使输入体积的外部边界为零。下面的可视化迭代输出激活(绿色),并显示每个元素是通过以下方式计算的:将高亮显示的输入(蓝色)与过滤器(红色)相乘,求和,然后用偏差抵消结果。
总结
这次试验体会了自定义算子的流程,了解了池化和卷积操作,了解了框架算子的使用方法,以及与自定义算子的区别。