Numpy 80%复现LeNet-5

(一)简介

1. 简简介

寒假看了一下Yann LeCun等大牛在1998年发表的论文《Gradient-Based Learning Applied to Document Recognition》,就是这篇论文确定了现代CNN的基本架构。论文里介绍了著名的LeNet-5网络结构和训练方法,还对比了很多当时其他的机器学习方法,限于个人知识水平后面很多没看下去,只看了关于LeNet-5的部分,并按照文中所述尽量还原训练过程。

2. 网络结构

在这里插入图片描述
绝大多深度学习的书都有这幅图,从图上直观地可以看出输入(nx32x32x1)—>C1卷积(nx28x28x6)—>S2(池化)(nx14x14x6)—>C3(nx10x10x16) —> S4(nx5x5x16)—> C5(nx1x1x120) —> F6全连接 (nx84)—> RBF7径向基 (nx10)
但从网上下载的数据集(MNIST)却都是28x28的,对此有两个方法:

  1. 把28x28的图像pad成32x32。
  2. 稍微修改一下C5的卷积核大小(本人就是使用这种方法,论文怎么搞的没看到,懂的高人请指教)

对LeNet-5网络结构和参数详细解析可见:link

(二)卷积层

1. C1、C5

C1、C5层卷积方法都是一样的,它们卷积核的深度都是和输入特征图(feature map)的深度一样,符合:输入(N,H,W,C)—> 通过(FN,FH,FW,FC)卷积核卷积 —> 输出(N, OH, OW, FN)

编写卷积层前向传播时有多种方法可供选择,但卷积这个操作就非常影响训练效率。一开始我使用大多数网站介绍卷积时用的滑窗法写,效率感人,batch size 为32时5mins+才迭代了10次,十分耗时。然后再深入搜搜才发现了众多流行框架都采用的im2col卷积方法 :此方法先把卷积核框中的窗的元素展开成1维向量,最终把输入的特征图张成2维矩阵;再把每个卷积核直接摊开展成2维矩阵,最后一次性做矩阵乘法。下面结合卷积分析以下上述过程的维度变化:
im2col
(在描述维度的时候我使用了tensorflow的(个数,高度,宽度,通道数),但编程的时候使用的顺序是(个数,通道数,高度,宽度)!!

输入(N,C,H,W) —> 输入im2col展开(NxOHxOW, CxFHxFW) —> 卷积核(FN,FC,FH,FW) —> 卷积核im2col展开(FCxFHxFW, FN) —> 矩阵乘法(NxOHxOW, FN) —> 加上偏置(bias: (1,FN)) —> reshape一下(N,FN,OH,OW)卷积完成!!

有了上述的较快速的卷积实现,我们就可以用此来实现卷积层的前向传播和误差反向传播的过程了。在代码之前,先回顾一波数学推导,规定如下符号:
ll 层到第 l+1l+1 层卷积,ll 层输入为 zlz^{l} ,输出为ala^{l},该层经过的卷积核为klk^{l},偏置为blb^{l},灵敏度(sensitivity)为δl\delta ^{l}。损失函数 EE,卷积操作 *。 前向传播表示为:

前向传播表示为: zl+1=alkl+bz^{l+1} = a^{l} * k^{l} + b
灵敏度递推: δl=δl+1rot180(kl)\delta^{l} = \delta^{l+1}*rot180(k^{l})
权重梯度: Ek=alδl+1\frac{\partial E}{\partial k}=a^{l}*\delta^{l+1}
偏置梯度: Eb=δl+1\frac{\partial E}{\partial b}=\delta^{l+1}

下面举个具体的例子,为不失一般性,该例子的参数尽量揭示一般规律(维度遵循(N,C,H,W))。
输入(2x2x2x2),卷积核(2x2x2x2)。卷积方式:对输入的pad=1,stride=2。输出为(2x2x2x2)。输入和卷积核内容为假设(基于计算方便)。
在这里插入图片描述
把前向传播得到的输出直接当作 l+1l+1 层的误差灵敏度反向传播如下图。
在这里插入图片描述
由于位置限制,上述误差反传的过程只呈现了第一张图的误差反传,第二张图的过程与上图完全一样。记住一点:前向传播和反向传播的各层维度一致
值得注意的是,误差反传的过程有几个细节是和公式不同的:

  1. 灵敏度递推的卷积中,从上图可以看出,δl+1\delta^{l+1} 要向外 pad(FH-1,FW-1) 的0,并且在行和列中插入stride行(列)0,得到的特征图再和经旋转180°的卷积核卷积。这种插入0再卷积的操作有一个专门的名称:空洞卷积(dilated convolution)。而为什么要这样卷积,其一当然是为了保证维度一致,其二经手工推导确实是这样的,由于公式过多,有机会展示一张手写的!!
  2. 上述空洞卷积的stride一定是1,不管前向传播的stride是多少!!
  3. 误差反传得到的输出维度可能与原输入维度不一致,这是因为前向传播过程若有pad的操作那反向得到的输出肯定维度是和pad后的维度一致的。所以反向得到的输出还要把pad多出来的地方裁掉!!
#卷积操作
def conv2(inputs, kernel, bias, stride=1, pad=(0, 0)):
    N, C, H, W = inputs.shape
    FN, FC, FH, FW = kernel.shape
    # [NxOHxOW, CxFHxFW]
    col, OH, OW = im2col(inputs, [FH, FW], stride, pad) # C must be equal to FC
    ker = kernel.reshape((FN, -1)).T  # [FCxFHxFW, FN]
    dot = np.dot(col, ker)  # [NxOHxOW, FN]
    # bias [1, FN]
    result = dot + bias
    result = result.T.reshape((FN, N, OH, OW))
    result = result.transpose(1, 0, 2, 3)	# [N, FN, OH, OW]
    return result
#把卷积核反转180°
def flip180(arr):
    FC, FN, FH, FW = arr.shape
    new_arr = arr.reshape((FC, FN, -1))
    new_arr = new_arr[..., ::-1]
    new_arr = new_arr.reshape((FC, FN, FH, FW))
    return new_arr
#反卷积,就是误差反传的那个卷积
def reconv2(delta_in, kernel, stride=1, pad=(0, 0)):
    N, FN, OH, OW = delta_in.shape
    FN, FC, FH, FW = kernel.shape

    kernel0 = kernel.transpose(1, 0, 2, 3)  # FCxFNxFHxFW
    kernel0 = flip180(kernel0)

	# insert the 0s, as the dilation rate(行列插入0)
    if stride > 1:
        hid = np.repeat(np.arange(1,OH), stride-1)
        wid = np.repeat(np.arange(1,OW), stride-1)
        delta_in = np.insert(delta_in, hid, 0, axis=2)
        delta_in = np.insert(delta_in, wid, 0, axis=3)	#[N,FN,DH,DW]
	# [N,FN,DH,DW] conv2 [FC,FN,FH,FW] --> [N,FC,OH,OW]
    delta_out = conv2(delta_in, kernel0, 0, pad=(FH-1, FW-1))
    N, C, H1, W1 = delta_out.shape
    delta_out = delta_out[..., pad[0]:H1 - pad[0], pad[1]:W1 - pad[1]] 		#clip the pad partition
    #裁去pad的部分
	#[N,FC,H,W]
    return delta_out
#构建卷积模块
class Conv2:
    def __init__(self, kernel_size, stride=1, pad=(0,0)):
        self.pad = pad
        self.stride = stride
        self.kernel_size = kernel_size
	#初始化方法,最好使用LeCun提到的初始化方法,即令init_kind='Le'
    def init_weights(self, init_kind):
        FN, FC, FH, FW = self.kernel_size
        # lenet-5 C1 and C3 have no activation, therefor init_kind may choose randomly
        if init_kind == 'Gaussian':
            std = 0.01
            kernel = np.random.normal(0, std, (FN, FC, FH, FW))
            bias = np.random.normal(0, 0.01, (1, FN))
        elif init_kind == 'He':  # for relu activation
            std = np.sqrt(2 / (FN*FC*FH*FW))
            kernel = np.random.normal(0, std, (FN, FC, FH, FW))
            bias = np.random.normal(0, 0.01, (1, FN))
        elif init_kind == 'Le': #such initialation method is applied in the lenet-5
            fin = FC*FH*FW		#according to the essay
            low = -2.4 / fin
            high = -low
            kernel = np.random.uniform(low, high, (FN, FC, FH, FW))
            bias = np.random.uniform(low, high ,(1, FN))
            
        self.kernel = kernel
        self.bias = bias

    def forward(self, inputs):
        self.inputs = inputs
        self.outputs = conv2(self.inputs, self.kernel, self.bias, self.stride, self.pad)

    def update(self, delta_in, learning_rate):
        # [NxFNxOHxOW]
        if len(delta_in.shape) < 4:
            delta_in = np.expand_dims(delta_in, axis=(2,3))
        self.delta_in = delta_in
        self.delta_out = reconv2(self.delta_in, self.kernel, self.stride, self.pad)
        # [1, FN]
        temp = np.sum(self.delta_in, axis=(0,2,3))
        temp = temp.reshape((1,-1))
        self.bias -= learning_rate * temp
        delta_in0 = self.delta_in.swapaxes(0,1) #[FNxNxOHxOW]
        inputs0 = self.inputs.swapaxes(0,1)     #[CxNxHxW]
        kernel_gra = conv2(inputs0, delta_in0, 0, self.stride, self.pad)    #[CxFNxFHxFW]
        kernel_gra = kernel_gra.swapaxes(0,1)	#[FNxFCxFHxFW]
        self.kernel = self.kernel - learning_rate * kernel_gra

2. C3

C1和C5都可以使用上述的Conv2模块构建,但C3却不行,因为在论文中提到的C3是作者精心设计的。S2(池化)(nx14x14x6)—>C3(nx10x10x16) 。S2层输出的6个通道的特征图并不是都用于每个卷积核的卷积。看一下论文:
在这里插入图片描述
0~15为16个卷积核,但它们的通道并不是固定的,打叉表示该卷积核卷积该通道。可见有6个卷积核取了输出的其中3个通道,9个卷积核取了其中4个通道,最后一个卷积核才用了6个通道,因此为这个C3特地写了一个模块。

# C3
class Leconv2:
    def __init__(self):
        pos = np.zeros((6, 16))
        temp0 = [np.array([0, 1, 2]), np.array([0, 1, 2, 3]),
                 np.array([0, 1, 3, 4]), np.arange(6)]
        j = 0
        for i in range(16):
            if i == 6 or i == 12 or i == 15:
                j += 1
            pos[temp0[j], i] = 1
            temp0[j] += 1
            temp0[j][temp0[j] == 6] = 0
        self.pos = pos.astype(np.bool) #构建LeCun提出的卷积表

        block = []
        k_size = [(1, 3, 5, 5), (1, 4, 5, 5), (1, 6, 5, 5)]
        j = 0
        for i in range(16):
            if i == 6 or i == 15:
                j += 1
            block.append(Conv2(k_size[j], stride=1, pad=(0,0)))
        self.block = block

    def init_weights(self, init_kind):
        for each in self.block:
            each.init_weights(init_kind)

    def forward(self, inputs):
        self.inputs = inputs
        outputs = []
        for k, each in enumerate(self.block):
            inputs0 = inputs[:,self.pos[:, k],...]
            each.forward(inputs0)
            outputs.append(each.outputs)
        self.outputs = np.concatenate(outputs, axis=1)
        
    def update(self, delta_in, learning_rate):
        self.delta_in = delta_in    # Nx16xOHxOW
        self.delta_out = np.zeros(self.inputs.shape)
        for k, each in enumerate(self.block):
            delta = delta_in[:,k,...]
            delta = np.expand_dims(delta, axis=1)
            each.update(delta, learning_rate)
            self.delta_out[:,self.pos[:, k],:,:] += each.delta_out

(三)池化层

在LeNet-5中,池化层S2、S4都把输入的特征图长宽缩小到原来的一半。池化一般有两种类型:均值池化和最大值池化。而在论文中貌似并没有提及作者使用了哪种池化,而我在某个科普网上看到好像是S2使用均值池化而S4采用最大值池化(忘在哪看的了),于是就采用了这种(但个人认为训练效果差异可忽略不计)。

下面推导简单推导一下池化的公式:假设 down()down(\cdot) 表示池化,up()up(\cdot) 表示池化的逆操作,第 ll 层第 ii 个通道输出 aila_{i}^{l} , βi\beta_{i}bib_{i} 为第i通道的训练参数,σ()\sigma(\cdot) 为sigmoid激活函数,zil+1z_{i}^{l+1} 为第 l+1l+1 层的输入,δil+1\delta_{i}^{l+1}l+1l+1 层的灵敏度,\circ哈达玛积, 则:
前向传播 :
zil+1=βdown(ail)+biz_{i}^{l+1}=\beta down(a_{i}^{l}) + b_{i}
ail+1=σ(zil+1)a_{i}^{l+1}=\sigma(z_{i}^{l+1})
反向传播:
上一层传入的梯度并非灵敏度,而是 Eail+1\frac{\partial{E}}{\partial{a_{i}^{l+1}}} ,而灵敏度 δil+1=Ezil+1\delta_{i}^{l+1}=\frac{\partial{E}}{\partial{z_{i}^{l+1}}},所以:
δil+1=Eail+1ail+1zil+1=Eail+1σ(zil+1)\delta_{i}^{l+1}=\frac{\partial{E}}{\partial{a_{i}^{l+1}}}\frac{\partial{a_{i}^{l+1}}}{\partial{z_{i}^{l+1}}}=\frac{\partial{E}}{\partial{a_{i}^{l+1}}} \circ \sigma'(z_{i}^{l+1})
δil=βiup(δil+1)\delta_{i}^{l} = \beta_{i}\cdot up(\delta_{i}^{l+1})
Ebi=δl+1\frac{\partial E}{\partial b_{i}}=\delta^{l+1}
Eβi=δl+1down(ail)\frac{\partial E}{\partial \beta_{i}}=\delta^{l+1}\cdot down(a_{i}^{l})
要注意的是:对上面公式维度不一致的情况,在多出来的维度上求和即可!!

看最大值池化的例子:
在这里插入图片描述
而为了提高效率,这里同样采用了im2col的池化实现,因为im2col已经把每个滑窗的元素展成2维矩阵,对此矩阵的行求最大值或均值后,再reshape维度就能得到最大值池化或均值池化的结果。而反向传播则要逆推会原二维矩阵,然后通过col2im还原。

#池化
def pool2(inputs, kernel_size, kind, stride=1, pad=(0,0)):
    N, C, H, W = inputs.shape
    FH, FW = kernel_size[:2]

    if kind == 'mean':
        col, out_h, out_w = im2col(inputs, (FH, FW), stride, pad)
        col = col.reshape((-1, FH * FW))	#(N*OH*OW*C, FH*FW)
        ccol = 1.0 * np.ones(col.shape) / (FH * FW)
        col = np.mean(col, axis=1)
        col = col.reshape((N,out_h,out_w,C)).transpose(0,3,1,2)	#(N,C,OH,OW)

    elif kind == 'max':
        col, out_h, out_w = im2col(inputs, (FH, FW), stride, pad)
        col = col.reshape((-1, FH*FW))	#(N*OH*OW*C, FH*FW)
        col0 = np.max(col, axis=1, keepdims=True)
        #记录最大值的位置,用于反向传播col2im还原
        ccol = 1.0 * (col == col0)	#keep the position of the max value
        col = col0.reshape((N, out_h, out_w, C)).transpose(0,3,1,2)	#(N,C,OH,OW)

    out_shape = (out_h, out_w)
    return col, ccol, out_shape   #ccol [NxOHxOWxC, FHxFW]
#反池化,池化的反向传播
def repool2(delta_in, ccol, inputs_shape, kernel_shape, out_shape, stride=1, pad=(0,0)):
    N, C, OH, OW = delta_in.shape
    delta_in = delta_in.transpose(0,2,3,1)  # NxOHxOWxC
    delta_in = delta_in.reshape((delta_in.size, 1))    # [NxOHxOWxC, 1]
    delta_out = ccol * delta_in
    delta_out = delta_out.reshape((N*OH*OW, -1)) # [NxOHxOW,C]
    delta_out = col2im(delta_out, inputs_shape, kernel_shape, out_shape, stride, pad)

    return delta_out #[N,C,H,W]
#池化模块
class Pool2:
    def __init__(self, kernel_size, kind, act_kind, stride, pad=(0,0)):
        self.kind = kind
        self.pad = pad
        self.stride = stride
        self.kernel_size = kernel_size  #[h, w, d]
        self.act_kind = act_kind

    def init_weights(self, init_kind):
        FH, FW, n = self.kernel_size
        if init_kind == 'Gaussian':
            std = 0.01
            beta = np.random.normal(0, std, (1, n))
            bias = np.random.normal(0, 0.01, (1, n))  
        elif init_kind == 'He':  # for relu activation
            std = np.sqrt(2 / n)
            beta = np.random.normal(0, std, (1, n))
            bias = np.random.normal(0, 0.01, (1, n))  
        elif init_kind == 'Le':
            fin = n
            low = -2.4 / fin
            high = -low
            beta = np.random.uniform(low, high, (1,n))
            bias = np.random.uniform(low, high ,(1, n))

        self.beta = beta
        self.bias = bias

    def forward(self, inputs):
        self.inputs = inputs
        self.outputu, self.output0, self.out_shape = pool2(self.inputs,
                        self.kernel_size, self.kind, self.stride, self.pad)
        beta = np.expand_dims(self.beta, axis=(2,3))
        bias = np.expand_dims(self.bias, axis=(2,3))
        outputs = beta * self.outputu + bias
        self.outputu = outputs
        self.outputs = activate(outputs, self.act_kind)

    def update(self, delta_in, learning_rate):
        self.delta_in = delta_in * deactivate(self.outputu, self.act_kind)
        delta_out = repool2(self.delta_in, self.output0, self.inputs.shape,
                            self.kernel_size[:2], self.out_shape, self.stride, self.pad)	# [NxCxHxW]
        beta = np.expand_dims(self.beta, axis=(2, 3))
        self.delta_out = beta * delta_out
        temp = np.sum(np.sum(self.delta_in, axis=(2,3)), axis=0, keepdims=True)
        self.bias = self.bias - learning_rate * temp
        temp = np.sum(np.sum(self.delta_in * self.outputu, axis=(2, 3)), axis=0, keepdims=True)
        self.beta = self.beta - learning_rate * temp

(四)全连接层

全连接层F6则非常简单了,前向传播和反向传播的原理和BP神经网络原理是一样的,下面就不赘述了,贴一个写的比较详细的link

(五)RBF层

径向基(RBF)层是LeNet-5中最后的一层,也是非常有意思的一层,个人认为这个层把神经网络的可解释性提高了很多。RBF的设置也是大有来头的,84个输入是由于,作者初始化RBF层权重的时候,使用了字符的bitmap来初始化,bitmap是7x12大小的小图,其中背景为-1,字符填充部分为+1,而7x12就是84。所以作者的意图就非常的明显了,作者把RBF层之前的层作为特征提取器,提取出一个bitmap,然后再把这个bitmap和RBF层输出的10个bitmap比较,看它和哪个bitmap最接近,就判断为属于哪一类。而每个输出自然是使用欧氏距离去评判他们的相近程度最直观。
在这里插入图片描述
下面推导一下RBF的式子,假设第 ll 层第 ii 个神经元输出 xilx_{i}^{l} , 第 l+1l+1 层第 jj 个神经元输出 yil+1y_{i}^{l+1}wijw_{ij} 为第 ll 层到第 l+1l+1 层的权重,δjl+1\delta_{j}^{l+1}l+1l+1 层的第 jj 个神经元灵敏度,则:
前向传播:
yjl+1=i(xiwij)2y_{j}^{l+1} = \sum_{i} (x_{i} - w_{ij})^2
反向传播:
δjl+1=Eyjl+1\delta_{j}^{l+1} = \frac{\partial{E}}{\partial{y_{j}^{l+1}}}
δil=Exil=j2δjl+1(xiwij)\delta_{i}^{l} =\frac{\partial{E}}{\partial{x_{i}^{l}}}= \sum_{j} 2\delta_{j}^{l+1}(x_{i} - w_{ij})
Ewij=2(xilwij)\frac{\partial{E}}{\partial{w_{ij}}}=-2(x_{i}^{l} - w_{ij})

#径向基模块
class Rbfcon2:
    def __init__(self, weights):
        self.weights = weights

    def forward(self, inputs):
        self.inputs = inputs  # num * dim
        n, d = self.inputs.shape
        # n_in, n_out = self.weights.shape
        temp0 = np.expand_dims(inputs, axis=2)  #nx84x1
        weights = np.expand_dims(self.weights, axis=0).repeat(n, axis=0)    #nx84x10
        temp0 = (temp0 - weights) ** 2
        self.outputs = temp0.sum(axis=1)    # num * 10

    def update(self, delta_in, learning_rate):
        self.delta_in = delta_in    #mx10
        n, d = self.inputs.shape
        inputs = np.expand_dims(self.inputs, axis=2)    # mx84x1
        weights = np.expand_dims(self.weights, axis=0).repeat(n, axis=0)    #mx84x10
        delta = np.expand_dims(delta_in, axis=1)    #mx1x10
        delta_out = 2 * (inputs - weights) * delta   # mx84x10 x mx1x10
        kernel_gra = np.sum(-1.0 * delta_out, axis=0)   #84x10
        self.delta_out = np.sum(delta_out, axis=-1) # mx84
        self.weights = self.weights - learning_rate * kernel_gra

(六)损失函数

损失函数的选择在神经网络训练中是非常讲究的,LeCun在论文中推荐了两个损失函数,用于不同的训练场合。
第一个称作极大似然估计判据:
E=1niyitureE = \frac{1}{n}\sum_{i}y_{i}^{ture}
其中,nn 为批样本个数,yiturey_{i}^{ture} 为第 ii 个样本真实类别的输出值。但使用这个损失函数有以下缺点:1. 没有对错判类别进行惩罚,用原文的话来说就是:no competition between the class。2. 会造成不可接受的collapsing phenomenon,也就是径向基网络的权重向量(对每一类别)会聚集在一起,即每个参考的bitmap都会随着训练而变为一致,这样得到的结果就是每一类都是一样的。
对于以上现象,作者给出了两种修正方法,其一就是设置RBF的权重是不可训练的;其二就是修正损失函数(但其实作者在后面说RBF的权重向量最好不要训练,或至少在最初几次迭代设置为不可训练)
E=1n(iyiture+ln(ej+jeyi))E = \frac{1}{n}(\sum_{i}y_{i}^{ture} + ln(e^{-j} + \sum_{j}e^{-y_{i}}))
其中,eje^{-j} 这一项是为了保证 EE 最终为正。

#损失函数
def Lossfun(outputs, labels, kind):
    # outputs nxd
    # labels  nxd
    n, d = labels.shape
    if kind == 'MSE':
        y0 = np.sum(outputs, axis=-1)
        y = outputs / y0
        temp = y - labels
        loss = np.sum(temp ** 2) / (2*n)
        delta_in = 2 * temp * (1 - y) / y0
    elif kind == 'CE':
        # softmax
        exp_o = np.exp(outputs)
        exp_o1 = np.sum(exp_o, axis=-1, keepdims=True)
        softmax = exp_o / exp_o1
        softmax = np.clip(softmax, 1e-10, 1)
        # outputs0[outputs0 == 0] = 1e-10
        ce = -1.0 * np.log(softmax) * labels
        loss = np.sum(ce) / n
        delta_in = softmax - labels
    elif kind == 'LeLoss':
        j = np.exp(-10)
        e = 1.0
        loss0 = np.sum(outputs * labels)
		#若用第一种损失函数,取消以下注释
        # loss = loss0 / n
        # delta_in = 1.0 * labels
		#若使用修正的损失函数,取消以下注释
        # exp_o0 = np.exp(-1.0 * outputs)
        # exp_o1 = np.sum(exp_o0, axis=-1, keepdims=True)
        # exp_o2 = np.log(j + exp_o1)
        # loss = (loss0 + e * np.sum(exp_o2)) / n
        # delta_in = (labels - e * exp_o0 / (exp_o1 + j))

    return loss, delta_in

(七)训练细节

搭建的网络结构如下:

class LeNet:
    def __init__(self,rbf_w):
        self.net = [bk.Conv2([6,1,5,5]),                     #nx24x24x6
                    bk.Pool2([2,2,6], 'mean', 'sigmoid', 2),    #nx12x12x6
                    bk.Leconv2(),                                            #nx8x8x16
                    bk.Pool2([2,2,16], 'max', 'sigmoid', 2),    #nx4x4x16
                    bk.Conv2([120,16,4,4]),                  #nx1x1x120
                    bk.Fullycon2([120, 84], 'Letanh'),
                    # bk.Rbfcon2([84, 10], 'relu')]   #nx84
                    bk.Rbfcon2(rbf_w)]                                       #nx10
        self.learning_rate = [1e-2,1e-2,1e-3,1e-3,1e-4,1e-4] + [0]   #Rbfcon is not training

    def init_weights(self):
        self.net[0].init_weights('Le')
        self.net[1].init_weights('Le')
        self.net[2].init_weights('Le')
        self.net[3].init_weights('Le')
        self.net[4].init_weights('Le')
        self.net[5].init_weights('Xavier')

    def forward(self, inputs):
        for k, each in enumerate(self.net):
            if k == 0:
                each.forward(inputs)
            else:
                each.forward(self.net[k-1].outputs)
        outputs = self.net[-1].outputs

        return outputs

    def update(self, delta_in):
        idx = [i for i in range(len(self.net))]
        idx.reverse()

        for i in idx:
            if i == len(self.net) - 1:
                self.net[i].update(delta_in, self.learning_rate[i])
            else:
                self.net[i].update(self.net[i+1].delta_out, self.learning_rate[i])

    def train(self, images, labels):
        outputs = self.forward(images)
        loss, delta_in = bk.Lossfun(outputs, labels, 'LeLoss')
        acc = bk.get_accuracy_lenet(outputs, labels)
        self.update(delta_in)
        return loss, acc

    def test(self, images, labels):
        outputs = self.forward(images)
        acc = bk.get_accuracy_lenet(outputs, labels)
        return acc
  1. 学习率这样设置是为了加快网络的训练,由于论文中激活函数使用sigmoid,但该激活函数会导致反向传播的误差减少至原来的四分之一或者更多,容易导致梯度消失,因此在每经过一个sigmoid函数我就把学习率调大来抗衡这种影响。
  2. 前面也说过,为了适应28x28的图像,这里把C5的卷积核大小改为4x4的。

(八)训练结果

在batch size为32,训练2个epochs后结果如下:
在这里插入图片描述
验证集为:1st epoch:0.85,2rd epoch:0.95

Complete codes available at : github

引用

[1] https://blog.csdn.net/Jason_yyz/article/details/80003271
[2] https://www.cnblogs.com/pinard/p/6494810.html
[3] https://blog.csdn.net/weixin_42398658/article/details/84392845
[4] https://www.jianshu.com/p/3286d4a061ca

发布了2 篇原创文章 · 获赞 0 · 访问量 1718
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 大白 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览