本笔记主要是我研读《神经网络与深度学习》一书之后,对重要知识点的整理和公式的推导。这里讲的神经网络是最简单的前馈神经网络,学习算法采用基于误差反向传播的(随机)梯度下降算法。
一个三层的神经网络结构(包含输入层)如下:
注意:输入层节点没有运算功能,直接将输入信号传递给隐藏层,而隐藏层和输出层将输入首先进行线性变换,然后再经过激活函数映射到输出。
神经网络中的符号定义
L L L : 神经网络层数(包含输入层)
x = ( x 1 , x 2 , . . . , x m ) T x=(x_1,x_2,...,x_m)^T x=(x1,x2,...,xm)T : 输入
y ^ = ( y ^ 1 , y ^ 2 , . . . , y ^ n ) T \widehat{y}=(\widehat{y}_1,\widehat{y}_2,...,\widehat{y}_n)^T y =(y 1,y 2,...,y n)T : 输出
a l = ( a 1 l , a 2 l , . . . ) T a^l=(a^l_1,a^l_2,...)^T al=(a1l,a2l,...)T : 第 l l l 层输出,特别地, a 1 = x , a L = y ^ a^1=x, a^L = \widehat{y} a1=x,aL=y
z l = ( z 1 l , z 2 l , . . . ) T z^l=(z^l_1,z^l_2,...)^T zl=(z1l,z2l,...)T : 第 l l l 层带权输入
W l W^l Wl : 第 l l l 层与第 l − 1 l-1 l−1 层之间的权重矩阵, w i j l w^l_{ij} wijl : 第 l l l 层第 i i i 个节点与第 l − 1 l-1 l−1 层第 j j j 个节点之间的权重.
b l b^l bl : 第 l l l 层偏置向量, b j l b^l_j bjl : 第 l l l 层第 j j j 个节点偏置
假设损失函数采用二次代价函数(均方差),激活函数采用 s i g m o i d sigmoid sigmoid 函数。
二次代价函数:
C x = 1 2 ∥ a L − y ∥ 2 C_x=\frac{1}{2}\left\|a^L-y\right\|^2 Cx=21∥∥aL−y∥∥2
s i g m o i d sigmoid sigmoid 函数定义:
σ ( x ) = 1 1 + e − x \sigma{(x)}=\frac{1}{1+e^{-x}} σ(x)=1+e−x1
s i g m o i d sigmoid sigmoid 函数导数有:
σ ′ ( x ) = σ ( x ) ( 1 − σ ( x ) ) \sigma^{'}{(x)}=\sigma{(x)}(1-\sigma{(x))} σ′(x)=σ(x)(1−σ(x))
信号前向传播公式:
{ a 1 = x z l = W l a l − 1 + b l , l ≥ 2 a l = σ ( z l ) , l ≥ 2 y ^ = a L \begin{cases} a^1=x \\ z^l=W^la^{l-1}+b^l , l\ge{2}\\ a^l=\sigma{(z^l)} , l\ge{2}\\ \widehat{y}=a^L \end{cases} ⎩⎪⎪⎪⎨⎪⎪⎪⎧a1=xzl=Wlal−1+bl,l≥2al=σ(zl),l≥2y =aL
误差的反向传播公式:
{ δ L = ∂ C ∂ a L ⊙ σ ′ ( z L ) = ( a L − y ) ⊙ σ ′ ( z L ) δ l = [ ( W l + 1 ) T δ l + 1 ] ⊙ σ ′ ( z l ) , l < L ∂ C ∂ b l = δ l ∂ C ∂ W l = δ l ( a l − 1 ) T \begin{cases} \delta^L=\frac{\partial C}{\partial a^L}\odot \sigma^{'}(z^L) =(a^L-y) \odot \sigma^{'}(z^L) \\ \delta^l=[(W^{l+1})^T\delta^{l+1}] \odot \sigma{'}(z^l), l<L \\ \frac{\partial C}{\partial b^l}=\delta^l\\ \frac{\partial C}{\partial W^l}=\delta ^l(a^{l-1})^T \end{cases} ⎩⎪⎪⎪⎨⎪⎪⎪⎧δL=∂aL∂C⊙σ′(zL)=(aL−y)⊙σ′(zL)δl=[(Wl+1)Tδl+1]⊙σ′(zl),l<L∂bl∂C=δl∂Wl∂C=δl(al−1)T
误差反向传播公式中引入中间变量 δ l \delta^l δl , 定义为 ∂ C ∂ z l \frac{\partial C}{\partial z^l} ∂zl∂C .
公式推导的基本思想是“链式求导法则”,证明时直接进行矩阵求导不易,可先证明分量形式,最后在写成矩阵或向量形式。
上面的误差反向传播公式是为梯度下降算法而服务的,梯度下降算法是神经网络最常用的学习算法。具体来讲又分为:
参数更新公式:
{ W l ← W l − η ∂ C ∂ W l b l ← b l − η ∂ C ∂ b l \begin{cases} W^l \leftarrow W^l - \eta \frac{\partial C}{\partial W^l} \\ b^l \leftarrow b^l - \eta \frac{\partial C}{\partial b^l} \end{cases} {Wl←Wl−η∂Wl∂Cbl←bl−η∂bl∂C
其中, η \eta η 为学习速率.
{ W l ← W l − η ∂ C ∂ W l b l ← b l − η ∂ C ∂ b l \begin{cases} W^l \leftarrow W^l -\eta\frac{\partial C}{\partial W^l} \\ b^l \leftarrow b^l - \eta\frac{\partial C}{\partial b^l} \end{cases} {Wl←Wl−η∂Wl∂Cbl←bl−η∂bl∂C
神经网络学习过程的流程图:
经典的神经网络采用的激活函数是 s i g m o i d sigmoid sigmoid 函数,代价采用二次代价函数,两者配合使用共同导致在输出误差较大时学习的速度反而很慢,随着误差的逐渐减小,学习速度出现先增大后又减小的现象。(如下图) 为什么会出现这种反常识的现象呢?按照人类的学习经验,不应该是误差越大学习速度越大吗?
要解释这个问题我们首先看看 s i g m o i d sigmoid sigmoid 函数的输入输出曲线:
s i g m o i d sigmoid sigmoid 函数将输入 ( − ∞ , + ∞ ) (-\infty, +\infty) (−∞,+∞)的数值挤压到 ( 0 , 1 ) (0, 1) (0,1) 之间。当输入的绝对值很大时, s i g m o i d sigmoid sigmoid 函数的导数趋近于0,再来看看上面的误差反向传播公式, δ L \delta^L δL 的公式中恰好含有 σ ′ ( z L ) \sigma^{'} (z^L) σ′(zL), 这就是原因所在。
改进措施之一:采用交叉熵代价函数,效果是将 δ L \delta^L δL 的公式中的 σ ′ ( z L ) \sigma^{'} (z^L) σ′(zL) 项约掉。其对应的误差反向传播公式为:
{ δ L = ∂ C ∂ a L ⊙ σ ′ ( z L ) = a L − y δ l = [ ( W l + 1 ) T δ l + 1 ] ⊙ σ ′ ( z l ) , l < L ∂ C ∂ b l = δ l ∂ C ∂ W l = δ l ( a l − 1 ) T \begin{cases} \delta^L=\frac{\partial C}{\partial a^L}\odot \sigma^{'}(z^L) = a^L-y \\ \delta^l=[(W^{l+1})^T\delta^{l+1}] \odot \sigma{'}(z^l), l<L \\ \frac{\partial C}{\partial b^l}=\delta^l\\ \frac{\partial C}{\partial W^l}=\delta ^l(a^{l-1})^T \end{cases} ⎩⎪⎪⎪⎨⎪⎪⎪⎧δL=∂aL∂C⊙σ′(zL)=aL−yδl=[(Wl+1)Tδl+1]⊙σ′(zl),l<L∂bl∂C=δl∂Wl∂C=δl(al−1)T
可见将代价函数变为交叉熵之后,对比两组公式,只有 δ L \delta^L δL 发生了改变。
推导 δ L \delta ^L δL 的过程(先证明分量形式):
改进措施之二:输出层采用** s o f t m a x softmax softmax激活函数和对数代价函数**。
softmax 定义如下:
s o f t m a x ( x j ) = e x j ∑ k e x k softmax(x_j)=\frac{e^{x_j}}{\sum_{k}{e^{x_k}}} softmax(xj)=∑kexkexj
特点:对每层神经元的输出值进行归一化(之和为1),因此,最终的输出值可以看作是“概率”。
与sigmoid函数类似,其导数也有类似性质:
∂ s o f t m a x ( x i ) ∂ x j = { s o f t m a x ( x i ) ( 1 − s o f t m a x ( x i ) ) , i = j − s o f t m a x ( x i ) s o f t m a x ( x j ) , i ≠ j \frac{\partial softmax(x_i)}{\partial x_j} = \begin{cases} softmax(x_i)(1-softmax(x_i)), i=j \\ -softmax(x_i)softmax(x_j), i\ne j \end{cases} ∂xj∂softmax(xi)={softmax(xi)(1−softmax(xi)),i=j−softmax(xi)softmax(xj),i̸=j
对数代价函数的定义:
C x = − l n ( a y L ) C_x = -ln (a^L_y) Cx=−ln(ayL)
巧妙的是输出层采用softmax激活函数和对数代价函数与sigmoid激活函数和交叉熵代价函数的反向传播公式是一样的。
下面推导采用softmax激活函数和对数代价函数的 δ L \delta^L δL 的计算式:
δ L = a L − y \delta^{L}=a^L-y δL=aL−y
推导过程:
有了这样的相似性,你应该使一个具有交叉熵代价的 sigmoid 型输出层,还是一个具有对数似然
代价的柔性最大值输出层呢?柔性最大值加上对数似然的组合更加适合于那些需要将输出激活值解释为概率的场景。
过度拟合(overfit)是指神经网络在训练过程中过分追求较高的分类准确度,学习到“噪声”等非本质特征的信号,而丧失泛化能力,在训练样本之外表现的很差。一般出现在训练样本很少的情况下。
解决过度拟合有以下几种策略:
规范化中的L2规范化是最常用的手段。基本思想是在原来的代价函数的基础上引入网络所有权重的平方和项。即,
C = C 0 + λ 2 n ∑ w w 2 C=C_0+\frac{\lambda}{2n}\sum_{w}{w^2} C=C0+2nλw∑w2
其中, C 0 C_0 C0是原来的代价函数, λ > 0 \lambda>0 λ>0 是规范化参数。
则,权重的更新公式变成
w ← w − η ( ∂ C 0 ∂ w + λ n w ) = ( 1 − η λ n ) w − η ∂ C 0 ∂ w w \leftarrow w - \eta( \frac{\partial C_0}{\partial w}+\frac{\lambda}{n}w) \\ =(1-\frac{\eta \lambda}{n})w-\eta \frac{\partial C_0}{\partial w} w←w−η(∂w∂C0+nλw)=(1−nηλ)w−η∂w∂C0
这种调整有时被称为权重衰减,因为它使得权重变小。
熵、交叉熵属于信息论中的概念。首先明确几个概念:
信息量:与事件空间中的某一事件相对应。刻画某一事件发生的不确定行。定义为 I ( x ) = − l o g ( p ( x ) ) I(x)=-log(p(x)) I(x)=−log(p(x)) , 事件发生的概率越小,信息量越大。
熵:与某一随机变量相对应。刻画某一随机变量的不确定性。定义为 H X = E [ I ] = − ∑ k p ( x k ) l o g ( p ( x k ) ) H_X=E[I]=-\sum_{k}{p(x_k)log(p(x_k))} HX=E[I]=−∑kp(xk)log(p(xk))
当某一随机变量服从均匀分布时,该随机变量的熵最大。
交叉熵:刻画两个随机变量分布的相似性。定义为 C E H ( p , q ) = − ∑ k p ( x k ) l o g ( q ( x k ) ) CEH(p,q)=-\sum_k{p(x_k)log(q(x_k))} CEH(p,q)=−∑kp(xk)log(q(xk)) , 其中,p, q分别是两个分布函数。p是真实样本分布,q是待估计样本分布。交叉熵越小,反映两个分布越接近。
# -*- coding:utf-8 -*-
"""全连接前馈神经网络训练和测试实现。学习算法采用小批量随机梯度下降算法。
输出层采用softmax函数,代价函数采用对数似然函数,有正则化"""
import numpy as np
import random
import mnist_loader
import matplotlib.pyplot as plt
class Network(object):
def __init__(self, struct, w=None, b=None, batsize=5, i_max=30, c_min=1e-3, rate=1.0, lam=10):
# 初始化神经网络
self.struct = struct
self.batsize = batsize
self.nlayer = len(struct) # 神经网络层数,不包含输入层
self.rate = rate
self.lam = lam
self.c = []
if w is None:
w, b = self.initwb()
self.w = w
self.b = b
self.i_max = i_max
self.c_min = c_min
self.nin = struct[0]
self.nout = struct[-1]
def initwb(self):
# 初始化权重和偏置
w = [None] + [np.random.randn(a,b)/np.sqrt(b) for a,b in zip(self.struct[1:], self.struct[:-1])]
b = [None] + [np.random.randn(a, 1) for a in self.struct[1:]]
# for i in range(0, self.nlayer-1):
# w.append(np.ones((self.struct[i+1], self.struct[i]))/self.struct[i])
# b.append(np.zeros((self.struct[i+1], 1)))
return w, b
def batpro(self, num):
# 随机分组
ind = list(range(num))
# random.shuffle(ind)
self.nbat = num//self.batsize+1
t = self.batsize-num%self.batsize
tt = random.sample(ind, t)
ind_2 = ind + tt
random.shuffle(ind_2)
return [ind_2[i:i+self.batsize] for i in range(0, num, self.batsize)]
# return np.array(ind_2).reshape((self.nbat, self.batsize))
def train(self, x, y, x_t, y_t):
# 训练
num = x.shape[1]
self.t = 1 - self.rate*self.lam/num
epoch = 0
c = float('inf')
while epoch < self.i_max:
bat_inds = self.batpro(num)
for inds in bat_inds:
x_bat = x[:, inds]
y_bat = y[:, inds]
self.update(x_bat, y_bat)
epoch += 1
_, aa = self.forword(x)
al = aa[-1]
c = self.calcc(al, y)
print('c', c)
self.c.append(c)
print('Epoch:', epoch)
self.test(x_t, y_t)
plt.plot(self.c)
plt.show()
def test(self, x, y):
# 测试
a = x
for i in range(1, self.nlayer):
z = self.w[i]@a+self.b[i]
a = sigmoid(z)
ind_p = np.argmax(a, 0)
ind = np.argmax(y, 0)
print(sum(ind_p == ind)/y.shape[1])
def forword(self, x_bat):
aa = [x_bat] + [np.zeros((t.shape[0], self.batsize)) for t in self.b[1:]]
zz = [None] + [np.zeros((t.shape[0], self.batsize)) for t in self.b[1:]]
for i in range(1, self.nlayer - 1):
zz[i] = self.w[i] @ aa[i - 1] + self.b[i]
aa[i] = sigmoid(zz[i])
zz[-1] = self.w[-1] @ aa[-2] + self.b[-1]
aa[-1] = softmax(zz[-1])
return zz, aa
def backward(self, zz, aa, y_bat):
# 对小批量数据计算各个参数的平局梯度,反向传播算法的关键所在
delt = [None] + [np.zeros((t.shape[0], self.batsize)) for t in self.b[1:]]
dw = [None] + [np.zeros(w.shape) for w in self.w[1:]]
db = [None] + [np.zeros((t.shape[0], self.batsize)) for t in self.b[1:]]
delt[-1] = aa[-1] - y_bat # 采用对数似然函数时的 delta_L
db[-1] = np.mean(delt[-1], 1)
db[-1].shape = (len(db[-1]), 1)
dw[-1] = delt[-1]@(aa[-2].T)/self.batsize
for i in range(self.nlayer-2, 0, -1):
d_l = ((self.w[i+1].T)@delt[i+1])*sigmoid_d(zz[i])
delt[i] = d_l
b_l = np.mean(d_l, 1)
b_l.shape = (len(b_l), 1)
db[i] = b_l
w_l = d_l@(aa[i-1].T)/self.batsize
dw[i] = w_l
return dw, db
def update(self, x_bat, y_bat):
# 更新参数
zz, aa = self.forword(x_bat)
dw, db = self.backward(zz, aa, y_bat)
self.w = [None]+[self.t*w1-self.rate*w2 for w1, w2 in zip(self.w[1:], dw[1:])]
self.b = [None]+[b1-self.rate*b2 for b1, b2 in zip(self.b[1:], db[1:])]
def calcc(self, al, y):
ind = np.argmax(y, 0)
return - np.mean(np.log(al[ind, list(range(y.shape[1]))]))
def softmax(z):
return np.exp(z)/sum(np.exp(z))
def sigmoid(z):
# 激活函数
return 1.0/(1.0 + np.exp(-z))
def sigmoid_d(z):
# 激活函数导数
return sigmoid(z) * (1 - sigmoid(z))
def parse_data(data):
# 解析数据
x_list = [sample[0] for sample in data]
y_list = [sample[1] for sample in data]
x = np.array(x_list)
y = np.array(y_list)
x = x.T
y = y.T
x.shape = x.shape[1:]
y.shape = y.shape[1:]
return x, y
if __name__ == '__main__':
training_data, validation_data, test_data = mnist_loader.load_data_wrapper()
training_data = list(training_data)
test_data = list(test_data)
test_data1 = []
for i, data in enumerate(test_data):
t = np.zeros((10, 1))
t[data[1], 0] = 1.0
test_data1.append([data[0], t])
x, y = parse_data(training_data)
x_t, y_t = parse_data(test_data1)
nn = Network([784, 100, 10], batsize=10, rate=0.5, i_max=30, lam=1)
nn.train(x, y, x_t, y_t)
数据集和详细代码参考:https://github.com/Daibingh/network-based-on-numpy