“今天,我不准备多讲。原因在哪里,同志们自己回去想。嘴巴,人人都有,无非是吃饭、说话。到底哪个重要,还要看实践。我们老祖宗有一句话:‘光说不练,假把式;光练不说,傻把式;连说带练~~真!把!式!’”
—— 傅明老人,《我爱我家》
现在,由于加入了矩阵计算的算子,我们的计算图框架( VectorSlow )现在该改叫 MatrixSlow 了。也许哪一天我们会将它改进成 TensorSlow ,但总之 Slow 这个特性我们是不会放弃的。
在之前的博文中,我们介绍了计算图和自动求导的原理及实现(这里),用 MatrixSlow 搭建了一些可以被纳入“非全连接神经网络”范畴的若干模型(这里),还搭建了一些深度不一的多层全连接神经网络,并用动画展示了它们的训练过程(这里)。现在,我们用 MatrixSlow 搭建卷积神经网络(CNN)并用来识别 MNIST 。关于 CNN 的介绍,可见这里。
(作者对本文代码保留后续修改的权利,完整代码请见码云)
一、卷积
我们首先实现卷积算子,代码如下(node.py):
class Convolve(Node):
"""
以第二个父节点的值为卷积核,对第一个父节点的值做二维离散卷积
"""
def __init__(self, *parents):
assert len(parents) == 2
Node.__init__(self, *parents)
self.padded = None
def compute(self):
data = self.parents[0].value # 输入特征图
kernel = self.parents[1].value # 卷积核
w, h = data.shape # 输入特征图的宽和高
kw, kh = kernel.shape # 卷积核尺寸
hkw, hkh = int(kw / 2), int(kh / 2) # 卷积核长宽的一半
# 补齐数据边缘
pw, ph = tuple(np.add(data.shape, np.multiply((hkw, hkh), 2)))
self.padded = np.mat(np.zeros((pw, ph)))
self.padded[hkw:hkw + w, hkh:hkh + h] = data
self.value = np.mat(np.zeros((w, h)))
# 二维离散卷积
for i in np.arange(hkw, hkw + w):
for j in np.arange(hkh, hkh + h):
self.value[i - hkw, j - hkh] = np.sum(
np.multiply(self.padded[i - hkw:i - hkw + kw, j - hkh:j - hkh + kh], kernel))
def get_jacobi(self, parent):
data = self.parents[0].value # 输入特征图
kernel = self.parents[1].value # 卷积核
w, h = data.shape # 输入特征图的宽和高
kw, kh = kernel.shape # 卷积核尺寸
hkw, hkh = int(kw / 2), int(kh / 2) # 卷积核长宽的一半
# 补齐数据边缘
pw, ph = tuple(np.add(data.shape, np.multiply((hkw, hkh), 2)))
jacobi = []
mask = np.mat(np.zeros((pw, ph)))
if parent is self.parents[0]:
for i in np.arange(hkw, hkw + w):
for j in np.arange(hkh, hkh + h):
mask *= 0
mask[i - hkw:i - hkw + kw, j - hkh:j - hkh + kh] = kernel
jacobi.append(mask[hkw:hkw+w, hkh:hkh+h].A1)
elif parent is self.parents[1]:
for i in np.arange(hkw, hkw + w):
for j in np.arange(hkh, hkh + h):
jacobi.append(self.padded[i - hkw:i - hkw + kw, j - hkh:j - hkh + kh].A1)
else:
raise Exception("You're not my father")
return np.mat(jacobi)
Convolve 接受两个父节点:图像(或叫特征图)节点,它是一个二维矩阵;卷积核节点也是一个二维矩阵。Convolve 暂时不支持设置步幅(stride)和填充方式(padding),步幅一律为 1 ,使用补零填充。compute 以第二个父节点的值为滤波器,对第一个父节点的值做二维离散卷积(滤波)。get_jacobi 函数返回当前本节点对特征图或卷积核的雅克比矩阵。
二、最大值池化
我们来实现最大值池化算子,代码如下(node.py):