开源项目地址
神经网络识别手写数字
基础知识
如何理解感知机 激活函数是阶跃函数的神经元
sigmoid作用 阶跃函数的升级,平滑了阶跃函数,阶跃函数不容易稳定,sigmoid克服了此缺点
多层感知机 可以理解为多层sigmoid激活函数神经元连接的网络
前馈神经网络 指上个神经元输出会作为下个神经元输入的网络。意味着神经网络中没有环路
循环神经网络 带环路的神经网络。rnn应用面和fnn应用面都很广,fnn更广一些
梯度下降 一种最小化损失函数的方法。对于复杂情况,难以通过导数运算计算损失函数最小值。此时想象一个山谷,找到最低谷底,通过多次采样本计算最终值,每次步进一点,按一定步进方法寻找最终结果
随机梯度下降 先选取少量样本估算梯度,估算的梯度可以加速梯度下降
代码
核心类为network类
import numpy as np
class Network:
def __init__(self, sizes):
# size is the nodes number im every layer, like [2, 3, 1]
self.num_layers = len(sizes)
self.sizes = sizes
self.bias = [np.random.randn(y, 1) for y in sizes[1:]]
self.weights = [np.random.randn(y, x) for x, y in zip(sizes[:-1], sizes[1:])]
注意,输入的数据是先列再行,比如一个一维数组,它在网络运算中应该是列向量存在而不是行向量,如果是行向量,不方便表示多维数组,所以应该是先列吧
其中,偏置和权重是均值为0标准差为1的标准正态分布随机数
用np写一个sigmoid,支持基本数值类型也支持矩阵sigmoid计算
def sigmoid(z):
return 1.0 / (1.0 + np.exp(-z))
给网络添加前馈运算
def feedforward(self, a):
for b, w in zip(self.bias, self.weights):
a = sigmoid(np.dot(w, a) + b)
return a
添加随机梯度下降
def SGD(self, training_data, epochs, batch_size, learn_rate, test_data):
if test_data:
n_test = len(test_data)
train_data_len = len(training_data)
for j in range(epochs):
random.shuffle(training_data)
batchs = [training_data[k: k + batch_size] for k in range(0, train_data_len, batch_size)]
for one_batch in batchs:
self.update_batch(one_batch, learn_rate)
if test_data:
print('epoch {0}:{1} {2}'.format(j, self.evaluate(test_data), n_test))
else:
print('epoch {0} completed'.format(j))
training_data是二维数组,第二个维度是手写数字图像转化为一维向量的长度,第一个维度是手写图像的个数
epoch是训练轮数。逻辑是每轮先将训练数据随机打乱,分成几个batch,batch长度为batch_size,每个batch会用本batch计算梯度并更新权重,在update_batch方法实现
单次更新权重函数update_batch
def update_batch(self, one_batch, learn_rate):
delta_b = [np.zeros(b.shape) for b in self.bias]
delta_w = [np.zeros(w.shape) for w in self.weights]
for x, y in one_batch:
delta_b_sample, delta_w_sample = self.backprop(x, y)
delta_b = [db + dnb for db, dnb in zip(delta_b, delta_b_sample)]
delta_w = [dw + dnw for dw, dnw in zip(delta_w, delta_w_sample)]
self.weights = [w - (learn_rate/len(one_batch)) * nw for w, nw in zip(self.weights, delta_w)]
self.bias = [b - (learn_rate/len(one_batch)) * nb for b, nb in zip(self.bias, delta_b)]
self.backprop代码下章看,主要逻辑是self.backprop实现,它对每个单独sample计算梯度,然后在update_batch里对batch梯度加和
训练得当的话,神经网络会是最好的模型
一个训练公式:复杂算法<=简单算法+好的训练数据
拿数据的话,写如下函数进行处理
import pickle
import gzip
import numpy as np
# usage: train_data, validata_data, test_data = mnist_loader.load_data_wrapper()
def load_data():
f = gzip.open('../data/mnist.pkl.gz', 'rb')
training_data, validation_data, test_data = pickle.load(f)
f.close()
return training_data, validation_data, test_data
def load_data_wrapper():
tr_d, va_d, te_d = load_data()
training_inputs = [np.reshape(x, (784, 1)) for x in tr_d[0]]
training_results = [vectorized_result(y) for y in tr_d[1]]
training_data = zip(training_inputs, training_results)
validation_inputs = [np.reshape(x, (784, 1)) for x in va_d[0]]
validation_data = zip(validation_inputs, va_d[1])
test_inputs = [np.reshape(x, (784, 1)) for x in te_d[0]]
test_data = zip(test_inputs, te_d[1])
return training_data, validation_data, test_data
def vectorized_result(j):
e = np.zeros((10, 1))
e[j] = 1.0
return e
反向传播代码(完整代码见开源项目)
损失函数使用均方误差
def backprop(self, x, y):
nabla_b = [np.zeros(b.shape) for b in self.bias]
nabla_w = [np.zeros(w.shape) for w in self.weights]
# feedforward operation
activation = x
activations = [x]
zs = []
for b, w in zip(self.bias, self.weights):
z = np.dot(w, activation) + b
zs.append(z)
activation = sigmoid(z)
activations.append(activation)
# back forward operation
delta = self.cost_derivative(activations[-1], y) * sigmoid_prime(zs[-1])
nabla_b[-1] = delta
nabla_w[-1] = np.dot(delta, activations[-2].transpose())
for layer_number in range(2, self.num_layers):
z = zs[-layer_number]
sp = sigmoid_prime(z)
delta = np.dot(self.weights[-layer_number + 1].transpose(), delta) * sp
nabla_b[-layer_number] = delta
nabla_w[-layer_number] = np.dot(delta, activations[-layer_number - 1].transpose())
return nabla_b, nabla_w
def cost_derivative(self, output_activations, y):
return output_activations - y
效果
准确率能达到95%左右
开始深度学习
上面的多层感知机即使最后取得了好的结果,但权重和偏置很难用人类可以理解的东西去解释,网络为什么可以达到很高的准确率
一种思路是将复杂的问题分解为多个,多层的简单问题,分解的结果是深度学习,可以理解为深度神经网络
反向传播算法工作原理
本章数学较多,可以跳过
为啥用反向传播?因为比传统算法更快
阿达马积
类似向量内积,计算结果不加和,结果仍是一个向量,只是向量逐元素相乘
反向传播算法四个基本方程
反向传播目标是计算出代价函数关于权重和偏置的导数。计算这个最终结果需要的其中一个中间结果是每个神经元上的误差,用这个可以计算最终的导数。可以吧每个神经元的微小误差定义为代价函数的微分
总结
理解:方程1和2求出各层误差,方程3和4算的是损失函数对权重和偏置的导数,算出了1和2,再算3和4,用复合函数求导法即可算出。方程1规定了每层误差是损失函数的倒数乘以
改进神经网络的学习方法
引入交叉熵代价函数
对于sigmoid激活函数,梯度下降开始时,损失函数对权重和偏置导数会比较小,这也是sigmoid函数刚开始梯度下降速度会比较慢的原因。
交叉熵可以作为代价函数 但是为什么
1 根据交叉熵表达式,其值大于0
2 交叉熵可以避免sigmoid的训练速度两边缓慢的问题
优点:二次函数也有1和2特点,但交叉熵还避免了学习速度下降的问题,因为当损失函数是交叉熵时,损失函数对权重或偏执求导,最后会约掉sigmoid倒数项,剩余sigmoid函数本身,所以不存在学习速度下降现象
交叉熵可以理解为:当出现严重错误时,也往往是人是学习速度最快的时候,但sigmoid函数存在学习速度慢的现象,而交叉熵避免了这种现象
关于学习率:如果使用不同的代价函数,不能直接使用相同的学习率
对于多个输出神经元的网络,交叉熵形如
什么时候使用交叉熵:当代价函数是sigmoid或二次函数时,可以考虑用交叉熵
因为如果用交叉熵为损失函数,损失函数对权重和偏置求导的最终化简结果不包含激活值,也就避免了sigmoid两边学习慢现象
交叉熵含义和起源
为了避免梯度下降过慢,考虑假定希望找到一个满足下列公式的损失函数,这样的损失函数可以避免梯度下降缓慢,然后积分求导,可以得出损失函数的原函数是形如交叉熵的函数
softmax
避免学习缓慢另一个简单方法是softmax。softmax用在输出层,其公式为
公式可以看出,softmax对于多输出神经元来说,输出神经元值总和为1,可视为概率分布
softmax作为输出函数,对权重和偏置求导结果如下
从求导结果可以看出,a和y都不会收到学习速度下降的影响
小结,如果用sigmoid作为激活层,可以用交叉熵作为损失函数避免学习下降,也可以用softmax作为输出层避免学习缓慢
过拟合与正则化
思考 已经有了训练集和测试集 为什么还需要验证集?因为加入不用验证集,那么实际会用过测试集的预测结果来对超参数进行调整,这其实也是一种过拟合,因为用了测试集的结果反过来在调整超参数。所以如果加入验证集,就可以解决这个问题。所以总结一下,使用验证集可以避免过拟合。
另一种防止过拟合的方法是保证样本数量足够多。因为样本数量过少,会较快出现过拟合,且测试集和训练集准确率差距会很大。
正则化
一种避免过拟合的方法是缩小网络规模,但缩小网络,也缩小了网络的能力上限。
另一种方法是正则化,分l1和l2正则化。l2是权重衰减,l2正则化思想是对代价函数加一项正则化项。对于交叉熵代价函数来说,形如下
其中,新增项是所有权重的平方和,乘以正则化参数,除以2n,n是所有样本数量。注意到正则化项中不包含偏置
直观从公式理解其作用,先求代价函数对权重和偏置的导数,发现只有对权重导数发生了变化,最后反映到梯度下降,会将大权重变小,同时可以避免代价函数局部梯度下降缓慢,因为正则化梯度下降的权重公式,极大降低了大权重的下降速度
为何正则化可以减小过拟合
如何理解正则化减小过拟合原理?
假设存在一个x和y的关系,我们希望准确预测y关于x的表达式
给出两种方案,1是y=2x,用简单的线性模型,2是用多项式拟合
拟合结果:多项式拟合理论可以模拟任何曲线,多项式对现有样本拟合结果也比线性模型更准确,但多项式随着x增大,y的大小会收到x最高次幂项主导,从而忽略其他项,这可能导致繁华结果不够理想。而线性模型虽然对现有样本拟合不如多项式好,但线性模型足够简单,不像多项式会受到最高次幂项影响,从而泛化能力可能更好。
正则化对神经网络的影响 神经网络如果使用正则化,大权重会快速下降,最后大多权重差距不会太大,这提升了网络的抗噪能力,使网络适用于希望抗噪的情况。而没使用正则化的网络,则适合拟合带有噪声的数据。相比之下,抗噪的网络,泛化性能会更好,也就是说,正则化的网络,泛化性能比无正则化的网络可能更好
总结 正则化可以减少过拟合,但只是从经验得出,难以具体科学解释为什么正则化减少了过拟合
其他正则化技术
介绍三种:1 l1正则化 2 dropout 3 人为扩展数据
l1正则化
类似l2正则化,只是正则化项不一样
求导后,正则化项多了一个常数,所以梯度下降时,权重下降速度多了一个常数项,最终向0前进。所以l1会使一些权重容易变成0从而不起作用,而l2会避免大权重存在,提高抗噪能力,提高泛化能力
dropout
想必你应该听过,和正则化修改代价函数不同,dropout是随机丢弃神经元,可以控制丢弃神经元的数量,简化网络避免过拟合。
前馈时随机disable某个比例数量的隐单元,最后预测时会把所有神经元打开,同时也会按比例降低输出单元比例的权重。思想是平均
人为扩展数据
以识别mnist手写数字为例,通过对手写数字的像素图片进行平移,旋转,拉伸等操作丰富训练集,从而提升识别精度。虽然旋转看起来没啥区别,但对像素图片和网络还是有区别的
权重初始化
前述手写数字识别的权重初始化是随机高斯分布,有没有更好的初始权重?
考虑一个例子,激活函数为sigmoid,不使用正则化,输出函数使用交叉熵避免学习缓慢。交叉熵仅对输出层有作用,对隐藏层神经元如果饱和则学习缓慢
解决方法 缩小初始化权重的分布,默认是均值0标准差1的分布,将分布的标准差改小,改为sqrt(1/n),这样损失函数对权重求导则不会陷入饱和,从而不会发生学习缓慢的现象。而偏置可以不用管,因为偏置只有一项,影响不大,可以不用管
总结 权重初始化可以避免学习缓慢,加速学习速度,但最终准确率不会有太大影响
代码优化手写数字识别
将前述各种优化方案加入到第一版网络代码,继承第一版网络代码
使用交叉熵作为默认代价函数
import json
import numpy as np
from network import sigmoid, sigmoid_prime, Network
import mnist_loader
class CrossEntropyCost:
@staticmethod
def fn(a, y):
return np.sum(np.nan_to_num(-y * np.log(a) - (1 - y) * np.log(1 - a)))
@staticmethod
def delta(z, a, y):
return a - y
class QuadraticCost:
@staticmethod
def fn(a, y):
return 0.5 * (np.linalg.norm(a - y) ** 2)
@staticmethod
def delta(z, a, y):
return (a - y) * sigmoid_prime(z)
class Network2(Network):
def __init__(self, sizes, cost=CrossEntropyCost):
self.num_layers = len(sizes)
self.sizes = sizes
self.default_weight_initializer()
self.cost = cost
def default_weight_initializer(self):
self.bias = [np.random.randn(y, 1) for y in self.sizes[1:]]
self.weights = [np.random.randn(y, x) / np.sqrt(x) for x, y in zip(self.sizes[:-1], self.sizes[1:])]
def SGD(self, tr, epochs, batch_size, learn_rate, lmbda=0,
evaluation_data=None,
monitor_evaluation_cost=False,
monitor_evaluation_accuracy=False,
monitor_training_cost=False,
monitor_training_accuracy=False):
n = len(tr[0])
tr = [(tr[0][index], tr[1][index]) for index in range(len(tr[0]))]
if evaluation_data:
n_data = len(evaluation_data[0])
evaluation_data = [(evaluation_data[0][index], evaluation_data[1][index]) for index in range(len(evaluation_data[0]))]
evaluation_cost, evaluation_accuracy = [], []
training_cost, training_accuracy = [], []
for j in range(epochs):
# mini_batches = [[(tr[0][i], tr[1][i]) for i in range(k, k + batch_size)] for k in range(0, n, batch_size)]
mini_batches = [tr[k: k + batch_size] for k in range(0, n, batch_size)]
for mini_batch in mini_batches:
self.update_batch(mini_batch, learn_rate, lmbda, len(tr))
print('epoch %s training complete' % j)
if monitor_training_cost:
cost = self.total_cost(tr, lmbda)
training_cost.append(cost)
print('cost on training data: {}'.format(cost))
if monitor_training_accuracy:
accuracy = self.accuracy(tr, convert=True)
training_accuracy.append(accuracy)
print('accuracy on training data: {}/ {}'.format(accuracy, n))
if monitor_evaluation_cost:
cost = self.total_cost(evaluation_data, lmbda, convert=True)
evaluation_cost.append(cost)
print('cost on evaluation data: {}'.format(cost))
if monitor_evaluation_accuracy:
accuracy = self.accuracy(evaluation_data)
evaluation_accuracy.append(accuracy)
print('accuracy on evaluation data: {} / {}'.format(self.accuracy(evaluation_data), n_data))
print()
return evaluation_cost, evaluation_accuracy, training_cost, training_accuracy
def update_batch(self, one_batch, learn_rate, lmbda, n):
delta_b = [np.zeros(b.shape) for b in self.bias]
delta_w = [np.zeros(w.shape) for w in self.weights]
for x, y in one_batch:
# method backprop is to calculate derivative of loss for weight and bias
delta_b_sample, delta_w_sample = self.backprop(x, y)
delta_b = [db + dnb for db, dnb in zip(delta_b, delta_b_sample)]
delta_w = [dw + dnw for dw, dnw in zip(delta_w, delta_w_sample)]
# regularization param lmbda to adjust weight except bias
self.weights = [(1 - learn_rate * (lmbda / n)) * w - (learn_rate/len(one_batch)) * nw for w, nw in zip(self.weights, delta_w)]
self.bias = [b - (learn_rate/len(one_batch)) * nb for b, nb in zip(self.bias, delta_b)]
def accuracy(self, data, convert=False):
if convert:
results = [(np.argmax(self.feedforward(x)), np.argmax(y)) for x, y in data]
else:
results = [(np.argmax(self.feedforward(x)), y) for x, y in data]
return sum(int(x == y) for x, y in results)
def total_cost(self, data, lmbda, convert=False):
cost = 0
data_len = len(data)
for x, y in data:
a = self.feedforward(x)
if convert:
y = vectorized_result(y)
cost += self.cost.fn(a, y) / data_len
cost += 0.5 * (lmbda / len(data)) * sum(np.linalg.norm(w) ** 2 for w in self.weights)
return cost
def save(self, filename):
data = {'sizes': self.sizes,
'weights': [w.tolist() for w in self.weights],
'biases': [b.tolist() for b in self.bias],
'cost': str(self.cost.__name__)}
with open(filename, 'w') as f:
json.dump(data, f)
def vectorized_result(j):
e = np.zeros((10, 1))
e[j] = 1
return e
if __name__ == '__main__':
tr, vd, te = mnist_loader.load_data_wrapper()
net = Network2([784, 30, 10], cost=CrossEntropyCost)
net.SGD(tr, 30, 10, .5,
lmbda=.8,
evaluation_data=vd,
monitor_evaluation_accuracy=True,
monitor_evaluation_cost=True,
monitor_training_accuracy=True,
monitor_training_cost=True)
上述代码和第一版相比,修改了初始化权重的标准差,按输入规模进行了压缩以减小学习缓慢;还默认使用了交叉熵代价函数。交叉熵的delta用来计算反向传播误差
如何选择神经网络超参数
策略1-宽泛策略:
简化网络,加快训练速度,快速发现问题。可以通过减少隐含层或减少神经元数量实现
减少训练结果集y的项数。比如mnist手写数字识别,可以只选择为0和1的样本,这样可以加快5倍速度学习
适度减少测试,验证,训练集大小,加快训练速度
注意 样本量减少时 同时需要视情况调小正则化参数值
优先对实验规模等进行修改,优先考虑从实验获取最直观的感受,即感受超参数对性能的影响
其他技术
随机梯度下降的变化形式
hessian技术
将代价函数泰勒展开,保留二阶导数
将代价函数对权重的二阶导数写为H矩阵
每次更新权重步长公式为
可以通过学习率改变每次步长变化大小
优点 hessian技术让代价函数收敛速度比一般梯度下降算法更快
缺点 由于保留了二阶导数,hessian矩阵太大,每次计算权重更新步长麻烦。
怎么优化缺点 针对hessian技术,在此基础上诞生了很多改进的方法
基于动量的梯度下降算法
在原有梯度下降算法和hessian算法基础上,舍弃运算二阶导数,从而避免引入的超大矩阵;引入 速度 的概念。公式如下
u表示摩擦力。当u为0,速度仅由梯度决定,权重运算公式退化为一般梯度下降。当u为1,表示没有摩擦力。摩擦力和速度的引入,使基于动量的梯度下降更快收敛,且不会跨过山谷谷底。
代价函数其他方法
共轭梯度下降
BFGS方法
nesterov
其他人工神经元模型
修正线性单元(RELU)
正切激活函数tanh
神经网络故事
为何深度网络很难训练
梯度消失问题
对于深度网络 不是隐藏层越多 越准确
一个现象 一个多层感知机,后面隐含层学习速度比前面隐含层学习速度快,这个学习速度是指每个层的损失函数梯度向量的模。这会导致在反向传播时,梯度到前面的隐含层时,出现梯度消失问题。这个问题可以被解决,但解决方法不完美
梯度消失的原因
看一个四层,每层只有一个神经元的模型。根据嵌套函数求导法,可得损失函数对第一个神经元偏置的导数为
为何出现梯度消失
根据损失函数对偏置导数公式可发现,sigmoid函数导数值域在(0, 0.25),而权重绝对值小于1(如果使用标准高斯分布给全局权重随机赋值),所以多个项都比1小,所以多个项相互乘法会越乘越小,这就是 梯度消失的原因
梯度爆炸问题
类似梯度消失问题,当权重和sigmoid函数的导数乘积比1大时,多项相乘的结果会指数增加,导致梯度爆炸
梯度不稳定问题
可以理解成梯度爆炸和梯度消失都存在,事实上这是可能发生的,条件就是权重大小和sigmoid函数导数乘积是否比1大
梯度消失问题普遍存在
当使用sigmoid激活函数时,由于函数的特点,梯度消失比梯度爆炸更为常见
复杂神经网络中的梯度不稳定
上一节只讨论了每层只有一个神经元的网络,当每层有多个神经元时,结论也是类似的,甚至梯度消失会更明显
深度网络
卷积神经网络入门
共享权重
与全连接层相比的优点 参数数量小 因为cnn每个卷积核的权重共享,而全连接层是笛卡积,这个层次看,参数数量会小一些
池化层
常用的池化层有最大值池化层
cnn应用
修正线性单元
将sigmoid改为RELU效果会更好些
扩展训练数据
即对已有图像进行平移,旋转,甚至是放缩等
全连接层+dropout
cnn最后加一层全连接再加dropout,会再提升一些性能。注意dropout只对全连接层使用。
集成神经网络
训练多个相同结构神经网络,然后进行投票选举等选出一个最好的
如何解决梯度消失梯度不稳定等问题
1 使用卷积层减少参数数量
2 使用正则化技术,尤其是dropout和卷积层
3 使用ReLU而不是sigmoid
4 使用gpu
提高训练准确度
见前述章节
采用对图像随即裁剪减少过拟合(尤其是对大型深度网络)(随机裁剪可以扩展基础数据集)
l2正则化减少过拟合
dropout减少过拟合
增加训练速度
基于动量的梯度下降