随着深度学习技术的不断更新,应用越来越广泛。为了方便开发,各大公司都开源了自己深度学习框架,比如Google的Tensorflow,Facebook的Pytorch,百度的PaddlePaddle飞桨,里边各种函数接口API我们调用到手软,非常方便,而且上手也很快。但是在面试的时候,面试官为了考察应聘者对深度学习原理的理解程度,有时会让手撕源码,今天我们就来练习一下。
在本文中,我们先简单介绍一下卷积层和池化层,然后开始写代码。
卷积层介绍
在卷积层中,我们会设置一个或多个固定 (或不固定) 大小(3 * 3, 5 * 5, 7 * 7…)和形状的算子,即卷积核。每个卷积核分别在图像或feature map上滑动,进行点乘相加运算,也就是进行局部空间信息的融合交互感知。每个卷积核滑动遍历整张图像或feature map得到一张新的feature map (也就是一个通道),即有多少个卷积核,便可以得到多少通道。卷积核的设计就是模仿了人的眼睛在感受事物时的状态,我们观察事物的时候便是先获取局部细节信息,然后结合得到全局信息。看图~,一个卷积核的工作原理。
在CNN中,卷积运算有诸多参数,比如 input, out_channels, kernel_size, strides, padding等。
- input: 输入图像/feature map
- out_channels: 输出图像/feature map的通道数
- kernel_size: 卷积核的大小
- strides: 卷积核每次滑动的步长
- padding: 扩充边缘像素的方式
代码
import numpy as np
import math
import cv2
import matplotlib.pyplot as plt
class Conv2D(object):
def __init__(self, shape, output_channels, kernel_size=3, stride=1, method='VALID'):
self.input_shape = shape
self.output_channels = output_channels
self.input_channels = shape[-1]
self.batch_size = shape[0]
self.stride = stride
self.ksize = kernel_size
self.method = method
weights_scale = math.sqrt(kernel_size * kernel_size * self.input_channels / 2)
# 卷积核以及偏置参数初始化,标准正态分布
self.weights = np.random.standard_normal((kernel_size, kernel_size, self.input_channels, self.output_channels)) // weights_scale
self.bias = np.random.standard_normal(self.output_channels) // weights_scale
# 设置卷积后图像的大小,如果选择“VALID”,输出图像会根据卷积核以及步长改变;如果选择“SAME”,输出图像尺寸不变
if method == 'VALID':
self.eta = np.zeros((shape[0], (shape[1] - kernel_size) // self.stride + 1, (shape[2] - kernel_size) // self.stride + 1,self.output_channels))
if method == 'SAME':
self.eta = np.zeros((shape[0], shape[1]//self.stride, shape[2]//self.stride, self.output_channels))
# 初始化权重和偏置的梯度
self.w_gradient = np.zeros(self.weights.shape)
self.b_gradient = np.zeros(self.bias.shape)
self.output_shape = self.eta.shape
# 前向计算方法
def forward(self,x):
# 首先对卷积核进行reshape, 之后直接进行矩阵运算,提高计算效率, [in_channels, kernel_size, kernel_size, out_channels] ---> [in_channels * kernel_size * kernel_size, out_channels]
col_weights = self.weights.reshape([-1, self.output_channels])
# 如果保持输出feature map的shape保持不变,那么对边缘进行zero填充
if self.method == 'SAME':
x = np.pad(x, ((0, 0), (self.ksize // 2, self.ksize // 2), (self.ksize // 2, self.ksize // 2), (0, 0)), 'constant', constant_values=0)
self.col_image = []
conv_out = np.zeros(self.eta.shape)
# 对batch里每个数据进行单独循环处理
for i in range(self.batch_size):
# 取batch中第i个数据进行维度扩展
img_i = x[i][np.newaxis, ...]
# 对该数据进行矩阵化,方便进行向量化运算, [1, height, width, channels]
self.col_image_i = self.im2col(img_i, self.ksize, self.stride)
# 使用矩阵点乘得到卷积后的结果
conv_out[i] = np.reshape(np.dot(self.col_image_i, col_weights) + self.bias, self.eta[0].shape)
return conv_out
# 将图像取与卷积核大小相同的patch,patch的大小为k_size*k_size*3,将patch reshape一行为(k_size*k_size*3,1),若有col个patch,则整个图像转换为[col, k_size*k_size*3]
def im2col(self, image, k_size, stride):
image_col = []
for i in range(0, image.shape[1] - k_size+1, stride):
for j in range(0, image.shape[2]-k_size+1, stride):
col = image[:, i:i+k_size, j:j+k_size, :].reshape([-1]) # 一个patchreshape成一个向量
image_col.append(col)
image_col = np.array(image_col)
return image_col
if __name__ == '__main__':
image = cv2.imread(r'C:\Users\11468\Desktop\sea_wind.jpg')
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
# 添加batch维度
image = image[np.newaxis, ...]
print("input:", image.shape)
# 输出通道设为6, 卷积核尺寸3*3, 步长为1, 输出图像尺寸变化
conv2d = Conv2D(image.shape, 6, 3, 1, 'VALID')
conv_out = conv2d.forward(image)
print("output:", conv_out.shape)
# 卷积结果可视化
fig = plt.figure()
for i in range(6):
ax = plt.subplot(2, 3, i + 1)
plt.imshow(conv_out[0][:, :, i])
plt.axis('off')
plt.title('The {}-th Channel of Feature Map'.format(i + 1), fontsize=8)
plt.show()
运行结果
输入图像
输出特征图