动机
虽然主流的框架搭建简单得神经网络已经很方便了,但是总觉得用别人的黑箱没办法理解精髓,为了更深刻的理解CNN结构和梯度下降机制,自己花了两周时间只用python 和 numpy 实现了一个简单的CNN网络, 包括完整得梯度回传和更新, 数据加载部分用了pytorch的接口,真的很方便。
总结
网络可以自己定义一个类调用底层模块进行设计,并且按和前向计算相反的顺序调用对应层的梯度回传函数,就可以实现梯度下降,我写了一个简单的两层卷积层加两层全连接层的网络用于训练minist,可以参考。
整个实现过程最难的就是卷积层批量数据+多通道+padding 的梯度回传实现,参考了很多博客,自己推导了好几天才理清楚,整个过程下来,对于梯度回传和CNN网络的理解又更加深刻了。
代码还有很多可以优化的地方,比如没办法支持resnet式的结构,代码效率不高,因为大三课比较多,加上其它一堆事情要做,所以没办法对代码进行进一步的优化,还请大家多多包涵。 目前训练四个epoch准确率能达到98% 左右。
import numpy as np
import torch
import torch.nn.functional as F
import torchvision.datasets as datasets #加载数据
import torchvision.transforms as transforms #数据增强
import matplotlib.pyplot as plt
from tensorboardX import SummaryWriter
class Conv_2D():
def __init__(self, input_dim, output_dim, ksize=3,
stride=1, padding=(0,0), dilataion=None):
self.input_dim = input_dim
self.output_dim = output_dim
self.ksize = ksize
self.stride = stride
self.padding = padding #(1,2) 左边 和 上边 填充一列 右边和下边填充2列
self.dilatation = dilataion
self.output_h = None
self.output_w = None
self.patial_w = None
# 产生服从正态分布的多维随机随机矩阵作为初始卷积核
# OCHW
# self.conv_kernel = np.random.randn(self.output_dim, self.input_dim, self.kernelsize, self.kernelsize) # O*I*k*k
self.grad = np.zeros((self.output_dim, self.ksize, self.ksize, self.input_dim), dtype=np.float64)
# 产生服从正态分布的多维随机随机矩阵作为初始卷积核
self.input = None
# OCh,w
self.weights = np.random.normal(scale=0.1,
size= (output_dim, input_dim, ksize, ksize))
self.weights.dtype =np.float64
self.bias = np.random.normal(scale=0.1,size = output_dim)
self.bias.dtype = np.float64
self.weights_grad = np.zeros(self.weights.shape) # 回传到权重的梯度
self.bias_grad = np.zeros(self.bias.shape) # 回传到bias的梯度
self.Jacobi = None # 反传到输入的梯度
def forward(self, input):
'''
:param input: (N,C,H,W)
:return:
'''
assert len(np.shape(input)) == 4
input = np.pad(input, ((0, 0), (0, 0), (self.padding[0], self.padding[1]),
(self.padding[0], self.padding[1])), mode='constant', constant_values=0)
self.input = input
self.Jacobi = np.zeros(input.shape)
N, C, H, W = input.shape
# 输出大小
self.output_h = (H - self.ksize) / self.stride + 1
self.output_w = (W - self.ksize ) / self.stride + 1
# 检查是否是整数
assert self.output_h % 1 == 0
assert self.output_w % 1 == 0
self.output_h = int(self.output_h)
self.output_w = int(self.output_w)
imgcol = self.im2col(input, self.ksize, self.stride) # (N*X,C*H*W)
output = np.dot(imgcol,
self.weights.reshape(self.output_dim, -1).transpose(1, 0)) # (N*output_h*output_w,output_dim)
output += self.bias
output = output.reshape(N, self.output_w * self.output_h, self.output_dim). \
transpose(0, 2, 1).reshape(N, int(self.output_dim), int(self.output_h), int(self.output_w))
return output
def backward(self, last_layer_delta,lr):
'''
计算传递到上一层的梯度
计算到weights 和bias 的梯度 并更新参数
:param last_layer_delta: 输出层的梯度 (N,output_dim,output_h,output_w)
:return:
'''
def judge_h(x):
if x % 1 == 0 and x <= self.output_h-1 and x >= 0:
return int(x)
else:
return -1
def judge_w(x):
if x % 1 == 0 and x <= self.output_w - 1 and x >= 0:
return int(x)
else:
return -1
# 根据推到出的公司 找出索引 与卷积权重相乘
for i in range(self.Jacobi.shape[2]): # 遍历输入的高
for j in range(self.Jacobi.shape[3]): # W
mask = np.zeros((self.input.shape[0], self.output_dim,
self.ksize, self.ksize)) # (N,O,k,k)
index_h = [(i - k) / self.stride for k in range(self.ksize)]
index_w = [(j - k) / self.stride for k in range(self.ksize)]
index_h_ = list(map(judge_h, index_h))
index_w_ = list(map(judge_w, index_w))
for m in range(self.ksize):
for n in range(self.ksize):
if index_h_[m] != -1 and index_w_[n] != -1:
mask[:, :, m, n] = last_layer_delta[:, :, index_h_[m], index_w_[n]] # (N,O,1,1)
else:
continue
mask = mask.reshape(self.input.shape[0], 1, self.output_dim, self.ksize, self.ksize)