lenet5 实现了手写数字的识别。其关键在于CNN的使用。其结构如下:

convolution

通俗地理解卷积运算 数学表达式:
$$ (fg)(x) = \int_{-\infty}^\infty f(\tau)g(x-\tau)d\tau $$
可以把$f(x)$理解为信号,$g(x)$理解为发出信号的时机。那么卷积就代表了当前时刻该信号的叠加效果。在图像中卷积的意义。虽然卷积的过程看上去很像内积,但其实两者有很大的区别,两者的前进方向不同。为了方便计算,将g中的下标进行修改,使得卷积运算可以直接用内积来表示。(将g旋转$180\degree$)

卷积后得到的矩阵称为feature map

如果特征刚好在角落上,那么上面的卷积过程无法检测到。因此,可以在输入矩阵上填充padding。同时使得输入与输出的大小相同。 Zero-padding is used so that the resulting image doesn't shrink.
也可以控制stride来改变卷积核的移动步伐.这会导致feature map的尺寸变小。
设输入的尺寸为 $I_r \times I_c$ , 卷积核尺寸为$K_r \times K_c$, 则可训练参数为$K_r
K_c+1$,输出尺寸为$(I_r+1-K_r) \times (I_c+1-K_c)$.(无padding,stride=1)

Convolutional Layer

作为神经网络,每一个节点只有2种状态(激活或者未激活)。在感知机中使用sigmoid函数进行激活使得输出值在[0,1]。这一过程被称为Non-linearity

Pooling Layer

这一过程比较简单,它将原图像进行分割。再对每一个区域进行一次计算。与卷积不同的是,这里的每一块区域都是不重叠的。最终它使得输入尺寸成倍地减少。这一操作称为subsampling Max-pooling: Pooling using a "max" filter with stride equal to the kernel size
每一层含有2个参数,coefficient and bias。同样,作为神经网络中的一层,需要对subsampling后的数乘以coefficient+bias 再用sigmoid函数激活才能输出到网络中。

Fully-connected (Dense) Layer

这一层与多层神经网络相同,不同的是它的输入可能具有多个channel。不管怎么样,都可以将输入看作是一维的。

Practice

这里我遵照lenet5原始论文进行复现。材料:数据集+论文

预处理

背景色置为-0.1,前景色为1.175. (raw-20)/200

C1

这里有点奇怪,论文里说输入图像是32X32的,但是数据集是28X28的。
6个 zero-padding5X5的卷积层。有$655+6(bias)=156$个参数。$62828=4704$个神经元。
输出:$6\times28\times28$

S2

大小为:2X2.取区域平均值×系数+bias再sigmoid激活。 参数:$6\times 2$
输出:$6\times14\times14$

C3

这里卡了很久,不知道多个feature map 如何进行卷积。其实可以把多个feature map当作多个通道,每个通道上各自进行卷积再叠加在一起。或者说这个卷积具有3维结构(前面都是二维的),只不过其中一维的大小为3,因此正好被压回2维结构。3X14X14的输入,3X5X5的核。

参数:3X5X5 6个,4X5X5 6+3个,6X5X5 1个
输出:16X10X10

S4

大小为:2X2.取区域平均值×系数+bias再sigmoid激活。
输出:$16\times5\times5$

C5

大小为:16X5X5. 共120个。
输出:120X1

F6

使用正切函数激活。
$$ f(a)=A tanh(S*a) $$
A为1.7159.
输出:84

output

计算公式:
$$ y_i=\sum_j(x_j-w_{ij})^2 $$
如果模型有k个输出,即k个分类。那么$w_{k*}$代表了该类别在特征空间中的位置。显然,离该特征向量越远,输出越大。这称为distributed code

Loss function

关键代码

写了一通代码发现直接炸了,运算速度实在太慢,简直出不了结果。实验了一下发现python的for循环效率极低,要实现神经网络,必须全部使用专门优化过的工具。如numpy,最好全程使用矩阵运算并避免矩阵的复制。使用卷积的时候可以利用im2col

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
def use_time(f,c=1,*args):
import datetime
start = datetime.datetime.now()
for i in range(c):
f(*args)
return datetime.datetime.now()-start

def im2col(image, ksize, stride):
# 100 , 5,1 0:00:12.587087
# image is a 4d tensor([batchsize, width ,height, channel])
image_col = []
for i in range(0, image.shape[1] - ksize + 1, stride):
for j in range(0, image.shape[2] - ksize + 1, stride):
col = image[:, i:i + ksize, j:j + ksize, :].reshape([-1])
image_col.append(col)
image_col = np.array(image_col)
return image_col

def im2col1(image, ksize, stride):
# 0:00:02
shape_r =(image.shape[0] - ksize + 1)//stride
shape_c =(image.shape[1] - ksize + 1)//stride
channel=image.shape[3]
m = image.shape[0]
col = np.zeros([shape_r*shape_c,ksize*ksize*channel*m])
for i in range(shape_r):
for j in range(shape_c):
col[shape_r*i+j,:] = image[:, i:i + ksize, j:j + ksize, :].reshape([-1])
return col

def im2col2(image, ksize, stride):
# 100 , 5,1 0:00:12.587087
# image is a 4d tensor([batchsize, width ,height, channel])
image_col = []
for i in range(0, image.shape[1] - ksize + 1, stride):
for j in range(0, image.shape[2] - ksize + 1, stride):
col = image[:, i:i + ksize, j:j + ksize, :].reshape([-1])
image_col.append(col)

return np.concatenate(image_col)

images=np.random.rand(10,100,100,6)

print(use_time(im2col,100,images,5,1))
print(use_time(im2col1,100,images,5,1))
print(use_time(im2col2,100,images,5,1))

Appendix

implement

参考实现

sigmoid

wiki
它是一类具有形如”S”的函数。常指Logistic function

BP

Pooling池化操作的反向梯度传播
在计算卷积的偏导数时,可以先把不同单元的权值当作是不同的分开计算,最后再把他们加起来。这就好比分身术,先产生n个分身,然后分身再合体回一个。它其实就是一个全微分:
$$ df(x_1,x_2)=f’{x1} d{x1}+f’{x2}d{x2} $$
如果令$x_1=x_2=x$就得到了上面的方法。

基本问题

在一个线性可分的训练集中,求一条直线将2类数据分开,并使得其间隔最大化。

西瓜书P123.
$$ min \frac{1}{2}\left | w\right |^2 $$
$$ s.t. y_i(w^\top x_i+b)\geqslant 1 ,\: i =1,…,m. $$
其中 $y_i=1$ 表示正例,$y_i=-1$表示负例。

对偶问题

使用拉格朗日乘子法求得其等价的,也就是对偶问题:
$$ max \sum_{i=1}^m \alpha_i-\frac{1}{2} \sum_{i=1}^m\sum_{j=1}^m \alpha_i \alpha_j y_i y_j \mathbf{x_i^\top}\mathbf{x_j} $$
$$ s.t. \sum_{i=1}^m \alpha_i y_i=0,\: \alpha_i \geq 0 $$
n+1个参数的优化问题变成了m个参数的优化问题。
要完全理解SVM的计算,需要搞清楚下面两个问题:

  1. 原问题如何计算?
  2. 对偶问题又如何改进计算?

正则化

实际数据并不总是线性可分。即使找到了一个严格可分的平面,也有可能导致过拟合。

引入正则化后的SVM的损失函数(hinge损失)可以表示为:
$$ \min_\theta C\sum_{i=1}^m[y_i cost_1(\mathbf{\theta^\top x_i})+(1-y_i)cost_0(\mathbf{\theta^\top x_i})] + \frac{1}{2} \sum_{j=1}^m \theta_j^2$$
当$C$取无穷大时,等价于严格约束,即找到一条线性可分的直线;
当$C$取一个适当的值,可以排除一些不符合的数据。

较大的$C$的影响:Lower bias, high variance == small $\lambda$ 过拟合。
较小的$C$的影响:Higher bias,low variance == large $\lambda$ 欠拟合。

Logistic regression vs. SVMs

n = number of features , m = number of training examples 
If   n  is large (relative to   m  ): n>>m
    Use logistic regression, or SVM without a kernel (“linear kernel”) 
If   n  is small,     m  is intermediate: 
    Use SVM with Gaussian kernel 
If   n  is small,     m is large: 
    Create/add more features, then use logistic regression or SVM  without a kernel 
Neural network likely to work well for most of these secngs, but may be 
slower to train. 

高斯核的SVM模型复杂度能够随着数据集的增大而增大,但是随着数据增大计算量也快速地提高因此SVM最适合的范围为(n=1~1000, m=10~10000, Andrew)scikit learn中SVM的复杂度为$O(n_{features}m_{samples}^2)$到$O(n_{features}m_{samples}^3)$之间。

核函数

高斯核
核函数使得在原特征空间中线性不可分的数据映射到线性可分的特征空间中。可以将训练数据当成标记$l$,如果$x$在$l$附近,那么$f(x,l)$趋于1。这样就能把样本x映射到m维的空间中。在这个空间里描述了样本$x$到各个方向的距离。运用SVM算法,可以选择出有效的支持向量(处在边界上的点)。

import numpy as np
y = np.array([0])
X = np.array([[-6,-6]])
for i in range(-5,6):
    for j in range(-5,6):
        X = np.append(X,[[i,j]],axis=0)
        y = np.append(y,abs(i)<=3 and abs(j)<=3)

from sklearn import svm
clf = svm.SVC(gamma='scale')
clf.fit(X, y)
clf.support_vectors_

以上代码SVM在不同样本附近选取特征点,组成支持向量,而不是全部训练样本。 感觉SVM与KNN有一定的相似度。KNN是选择附近数目多的训练样本,而SVM则是对$x$附近的训练样本进行数值加权。

反向传播网络

反向传播是神经网络中求最优参数的一个运用梯度下降算法的应用。
简化模型
这是我总结的简化后的神经网络的模型。一个layer表示一个隐层。具体的计算过程可以参见andrew的ML,这里采用的符号z,a也是依据课程的介绍。
$$
a_1^{(2)} = g(\Theta_{10}^{(1)}x_0 + \Theta_{11}^{(1)}x_1 + \Theta_{12}^{(1)}x_2 + \Theta_{13}^{(1)}x_3) \newline a_2^{(2)} = g(\Theta_{20}^{(1)}x_0 + \Theta_{21}^{(1)}x_1 + \Theta_{22}^{(1)}x_2 + \Theta_{23}^{(1)}x_3) \newline a_3^{(2)} = g(\Theta_{30}^{(1)}x_0 + \Theta_{31}^{(1)}x_1 + \Theta_{32}^{(1)}x_2 + \Theta_{33}^{(1)}x_3) \newline h_\Theta(x) = a_1^{(3)} = g(\Theta_{10}^{(2)}a_0^{(2)} + \Theta_{11}^{(2)}a_1^{(2)} + \Theta_{12}^{(2)}a_2^{(2)} + \Theta_{13}^{(2)}a_3^{(2)}) \newline $$
在计算梯度的时候主要方法是 求偏导数和换元法。从上图可知$z$是连接不同层之间的节点。
在计算导数的时候,采用Z作为参考点会简化计算。先换元,转换为关于$z$的表达式$J(z^l)$.可知$\frac{∂J(Θ)}{∂z^{l}} =\frac{∂J(Θ)}{∂z^{l+1}}\frac{∂z^{l+1}}{∂z^{l}}$. 而这一部分$\frac{∂z^{l+1}}{∂z^{l}}=Θ^{k^T}z^{l+1}.g’(z^l)$正是反向传播的关键。 要求得参数的导数所需要的最后一步$\frac{∂z^{l+1}}{∂Θ^{l}_{ij}}=a^l_j$
由于:
$$ g′(z^{l})=a^l.∗ (1−a^l)$$
因此
$$\delta^l=\frac{∂z^{l+1}}{∂z^{l}}=Θ^{k^T}z^{l+1}.
a^l.∗ (1−a^l)$$
$$\frac{∂J}{∂Θ^{l}}=\delta^{l+1}a^{l^T}$$

参考

Neural Networks: Learning