在这一节,我们对上一个程序(Network1.py)进行了优化
3.改进神经网络的学习方法
(1)交叉熵代价函数的引入
Network1程序采用了S型神经元,S型神经元存在一个问题,当输出层神经元的输出接近0,或者1的时候,sigmoid函数曲线相当平导致此时sigmoid函数的导数很小,当选择二次代价函数时,输出误差δL=(aL-y)σ‘(zL),∂C/∂ωL,∂C/∂bL就会非常小,使得神经网络学习变得缓慢。
因此我们引入了交叉熵代价函数
当选择交叉熵代价函数时,δL= aL-y。解决了输出层学习缓慢的问题,但是没有解决隐藏层的神经元在σ(z)接近1或者0的时候饱和的问题。
(2)柔性最大值的引入
有时候我们想把输出层第j个神经元的输出看做一种概率估计,因此引入了柔性最大值(softmax),第j个神经元的激活值是:
并定义对数代价函数
其中y为训练输入x对应的目标输出,aL为神经网络输出。如果我们训练的是MNIST图像,输入为7的图像,那么对应的对数代价就是-lna7L,当神经网络输出就是7的时候,他会估计一个对应的概率a7L和1很接近,所以代价就会很小,反之,神经网络表现的很糟,改良版a7L就变的很小,代价就随之增大,所以对数代价函数也是满足我们期望的代价函数的条件的。
(3)过度拟合和规范化
过度拟合可以理解为模型对已有的数据会表现的很好,但是对新的数据很难泛化,对一个模型真正的测验就是他对没有见过的场景的预测能力。
为了缓解过度拟合,我们主要采取以下措施:
- L1.规范化
- L2规范化
- L1规范化
- 弃权
- 人为增加训练样本
这里主要讲解以下L2规范化技术。二次代价函数,以及交叉熵代价函数,柔性最大值规范化后的形式如下:
其中C0为原始代价函数。第二项加入的就是所有权重(每个元素)的平方和,λ成为规范化参数,规范化可以当做一种寻找小的权重和最小原始代价函数之间的折中,λ越小,就越偏向于最小化原始代价函数,反之倾向于小的权重。
此时权重学习就变成了
(4)权重初始化
由于Network1程序中中我们在隐藏层和输出层激活函数选用的都是sigmoid函数,我们刚才考虑采用交叉熵代价函数,解决了输出层学习缓慢的问题,但是隐藏层的神经元在σ(z)接近1或者0的时候,也会存在饱和的问题,会导致学习缓慢。
在Network1中我们随机初始化权重和偏置为标准正态分布。假设我们使用一个有大量输入神经元的网络,比如有10000个,我们假设训练输入x,其中一半神经元的输入神经值为1,另一半输入为0。让我们考虑隐藏神经元输入的带权和 z = Σwjxj+b,所以z服从μ(0,501)正态分布,z是一个有非常宽的高斯分布,z>>1或者z<<-1的概率会很大,这样神经元的输出σ(z)会接近1或者0,这样我们的隐藏神经元会饱和,所以当出现这样的情况时,在权重中进⾏微⼩的调整仅仅会给隐藏神经元的激活值带来极其微弱的改变。⽽这种微弱的改变也会影响⽹络中剩下的神经元,然后会带来相应的代价函数的改变。结果就是,这些权重在我们进⾏梯度下降算法时会学习得⾮常缓慢。这其实和我们在前⾯所说的问题差不多,前⾯的情况是输出神经元在错误的值上饱和导致学习的下降。我们之前通过代价函数的选择解决了前⾯的问题。不幸的是,尽管那种⽅式在输出神经元上有效,但对于隐藏神经元的饱和却⼀点作⽤都没有。
假设我们有一个有nin个输入权重的神经元,因此我们可以通过初始化权重和偏置分布为μ(0,1/nin)解决这个问题。
(5)神经网络超参数的选择
(6)其他技术
(1)随机梯度下降的变化形式
主要介绍了以下Hessian技术和momentum技术。
(2)人工神经元的其他模型
双曲正切神经元(tanh),以及修正现行神经元(ReLU)
Network2.py程序较之前程序有了一些改进:
1.代价函数中加入了规范化项,缓解过度拟合
2.权重,偏置初始化进行了优化,服从N(0,1/nin)分布,缓解了隐藏层神经元饱和,加快训练速度
代码如下:
# -*- coding: utf-8 -*- """ Created on Tue Mar 13 18:13:30 2018 @author: Administrator """ ''' 书籍:神经网络与深度学习 第三章:改进版的神经网络 与Network.py文件中有如下改进 1.代价函数中加入了规范化项,缓解过度拟合 2.权重,偏置初始化进行了优化,服从N(0,1/nin)分布,缓解了隐藏层神经元饱和,加快训练速度 ''' import numpy as np import json import random import sys ''' 定义二次代价函数和交叉熵代价函数的类 ''' class QuadraticCost(object): #定义静态方法 @staticmethod def fn(a,y): ''' 计算一个样本的二次代价函数 C0 = 0.5∑j(yj - aj)^2 a:神经网络输出层值 y:目标值 ''' return 0.5*np.linalg.norm(a-y)**2 @staticmethod def delta(z,a,y): ''' 计算输出层误差δL z:输出层带权输入 a:神经网络输出层值 y:目标值 ''' return (a-y)*sigmod_prime(z) class CrossEntropyCost(object): #定义静态方法 @staticmethod def fn(a,y): ''' 计算一个样本的交叉熵代价函数 C0 = -∑j[yjln(ajL) + (1-yj)ln(1 - ajL)] a:神经网络输出层值 y:目标值 ''' #np.nan_to_num:Replace nan with zero and inf with finite numbers. #把np.nan(非常小的数字,接近0)用0取代 #np.inf,或者-np.inf(正负无穷大的数字)用有限数替代 return np.sum(-1*np.nan_to_num( y*np.log(a) + (1 - y)*np.log( 1 - a))) @staticmethod def delta(z,a,y): ''' 计算输出层误差δL z:输出层带权输入 a:神经网络输出层值 y:目标值 ''' return (a-y) ''' 定义神经网络的类 ''' class Network(object): def __init__(self,sizes,cost = CrossEntropyCost): ''' sizes:list类型 每个元素对应神经网络中每一层的神经元个数 cost:CrossEntropyCost或者QuadraticCost 指定代价函数 默认采用交叉熵代价函数 ''' self.num_layers = len(sizes) self.sizes = sizes self.default_weight_initializer() self.cost = cost def default_weight_initializer(self): ''' 默认权重初始化函数,改进之后的,w,b服从N(0,1/nin)正态分布 ''' self.biases = [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 large_weight_initializer(self): ''' 权重初始化函数,w,b服从N(0,1)正态分布 weights:权重,随机初始化,服从(0,1)高斯分布 weights[i]:是一个连接着第i层和第i+1层神经元权重的numpy矩阵 i=0,1... biases:偏置,随机初始化,服从(0,1)高斯分布 biases[i]:是第i+1层神经元偏置向量 i=0,1.... ''' self.biases = [np.random.randn(y,1) for y in self.sizes[1:]] self.weights = [np.random.randn(y,x) for x,y in zip(self.sizes[:-1],self.sizes[1:])] def feedforward(self,a): ''' 前向反馈函数,对于网络给定一个输入向量a,返回对应的输出 a:神经网络的输入 ''' for b,w in zip(self.biases,self.weights): #dot矩阵乘法 元素乘法使用* a = sigmod(np.dot(w,a) + b) return a def SGD(self,training_data,epochs,mini_batch_size,eta,lamda,evaluation_data=None, monitor_evaluation_cost = False, monitor_evaluation_accuracy = False, monitor_training_cost = False, monitor_training_accuracy = False): ''' 随机梯度下降算法:使用小批量训练样本来计算梯度(计算随机选取的小批量数据的梯度来估计整体的梯度) training_data:元素为(x,y)元组的列表 (x,y):表示训练输入以及对应的输出类别 这里的输出类别是二值化后的10*1维向量 epochs:迭代期数量 即迭代次数 mini_batch:小批量数据的大小 eta:学习率 lamda:规范化lamda参数 evaluation_data:评估数据集 validation_data或者test_data monitor_evaluation_cost:标志位 打印每次迭代评估数据集的代价 monitor_evaluation_accuracy:标志位 打印每次迭代评估数据集的预测准确的个数 monitor_training_cost 打印每次迭代训练数据集的代价 monitor_training_accuracy 打印每次迭代训练数据集的预测准确的个数 ''' if evaluation_data: #计算评估集样本个数 n_data = len(evaluation_data) #计算训练集样本个数 n = len(training_data) evaluation_cost = [] evaluation_accuracy = [] training_cost = [] training_accuracy = [] #进行迭代 for j in range(epochs): #将训练集数据打乱,然后将它分成多个适当大小的小批量数据 random.shuffle(training_data) mini_batches = [training_data[k:k+mini_batch_size] for k in range(0,n,mini_batch_size)] #训练神经网络 for mini_batch in mini_batches: self.update_mini_batch(mini_batch,eta,lamda,n) print('Epoch {0} training complete'.format(j)) #每一次迭代后 输出相应的测试信息,预测准确个数和代价 if monitor_evaluation_cost: cost = self.total_cost(evaluation_data,lamda,convert=True) evaluation_cost.append(cost) print('Cost on evaluation data:{0}'.format(cost)) if monitor_evaluation_accuracy: accuracy = self.accuracy(evaluation_data) evaluation_accuracy.append(accuracy) print('Accuracy on evaluation data:{0}/{1}'.format(accuracy,n_data)) if monitor_training_cost: cost = self.total_cost(training_data,lamda) training_cost.append(cost) print('Cost on training data:{0}'.format(cost)) if monitor_training_accuracy: accuracy = self.accuracy(training_data,convert=True) training_accuracy.append(accuracy) print('Accuracy on training data:{0}/{1}'.format(accuracy,n)) #返回的均为list 包含每次迭代后对应的代价值或者预测准确个数 return evaluation_cost,evaluation_accuracy,training_cost,training_accuracy def update_mini_batch(self,mini_batch,eta,lamda,n): ''' mini_batch:小批量数据 元素为(x,y)元组的列表 (x,y) eta:学习率 lamda:规范化lamda参数 n:训练集数据总长度 对每一个mini_batch应用梯度下降,更新权重和偏置 ''' #初始化为0 nabla_b = [np.zeros(b.shape) for b in self.biases] nabla_w = [np.zeros(w.shape) for w in self.weights] #依次对每一个样本求梯度,并求和 for x,y in mini_batch: #计算每一个样本代价函数的梯度 delta_nabla_b,delta_nabla_w = self.backprop(x,y) #梯度分量求和 Σ∂Cx/∂ω nabla_b = [nb + dnb for nb,dnb in zip(nabla_b,delta_nabla_b)] #梯度分量求和 Σ∂Cx/∂b nabla_w = [nw + dnw for nw,dnw in zip(nabla_w,delta_nabla_w)] #更新权重 w = w - η/m*Σ∂Cx/∂ω self.weights = [(1 - eta*lamda/n)*w - (eta/len(mini_batch))*nw for w,nw in zip(self.weights,nabla_w)] #更新偏置 b = b - η/m*Σ∂Cx/∂b self.biases = [b - (eta/len(mini_batch))*nb for b,nb in zip(self.biases,nabla_b)] def backprop(self,x,y): ''' 计算给定一个样本代价函数的梯度 单独训练样本x的二次代价函数 返回一个元组(nabla_b,nabla_w) = (∂Cx/∂ω,∂Cx/∂b) :和权重weights,偏置biases维数相同的numpy数组 ''' #初始化与self.baises,self.weights维数一样的两个数组 用于存放每个训练样本偏导数的累积和 nabla_b = [np.zeros(b.shape) for b in self.biases] nabla_w = [np.zeros(w.shape) for w in self.weights] #前向反馈 activation = x #保存除了输入层外所有层的σ(z)的值 activations = [x] #保存除了输入层外所有层的z的值 zs = [] #计算除了输入层外每一层z和σ(z)的值 for b,w in zip(self.biases,self.weights): z = np.dot(w,activation) + b zs.append(z) activation = sigmod(z) activations.append(activation) #反向传播 计算输出层误差δL,以及∂Cx/∂bL,∂Cx/∂ωL delta = self.cost.delta(zs[-1],activations[-1],y) nabla_b[-1] = delta nabla_w[-1] = np.dot(delta,activations[-2].transpose()) #计算隐藏层误差δl,以及 ∂Cx/∂bl,∂Cx/∂ωl
for l in range(2,self.num_layers): z = zs[-l] sp = sigmod_prime(z) delta = np.dot(self.weights[-l+1].transpose(),delta)*sp nabla_b[-l] = delta nabla_w[-l] = np.dot(delta,activations[-l-1].transpose()) return (nabla_b,nabla_w) def accuracy(self,data,convert=False): ''' 返回用神经网络预测data数据集中样本输出正确的个数 data:数据集[(x,y),...] convert:判断data中每个样本的输出是否是二值化后的向量?如果是True,否则False 这个与data数据格式有关 ''' 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,lamda,convert=False): ''' 返回用神经网络预测data数据集后的代价值 data:数据集[(x,y),...] lamda:规范化lamda参数 convert:判断data中每个样本的输出是否是二值化后的向量?如果是False,否则True 这个与data数据格式有关 ''' cost = 0.0 for x,y in data: if convert: #二值化 y = vectorized_result(y) cost += self.cost.fn(self.feedforward(x),y) cost /= len(data) #加入规范化项 cost += 0.5*lamda/len(data)*sum(np.linalg.norm(w)**2 for w in self.weights) return cost def save(self,filename): ''' 把神经网络的参数序列化后写到指定文件中 ''' data = {'sizes':self.sizes, 'weights':self.weights, 'biased':self.biases, 'cost':str(self.cost.__name__) } with open(filename,'w') as f: #写入序列化后的值 json.dump(data,f) def load(filename): ''' 从文件中读取字符串并反序列化,创建一个神经网络类对象,并恢复权重,偏置参数 ''' with open(filename,'r') as f: data = json.load(f) #从当前模块中找到名称data['cost']的对象,并赋值给cost cost = getattr(sys.modules[__name__],data['cost']) #创建神经网络 net = Network(data['sizes'],cost=cost) #初始化权重和偏置 net.weights = data['weights'] net.biases = data['biases'] return net def vectorized_result(j): """ 0-9 二值化为 10*1 Return a 10-dimensional unit vector with a 1.0 in the jth position and zeroes elsewhere. This is used to convert a digit (0...9) into a corresponding desired output from the neural network. """ e = np.zeros((10, 1)) e[j] = 1.0 return e def sigmod(z): ''' 定义S型函数 当输入z是一个list,tuple或者numpy数组时,numpy自动地按元素应用sigmod函数,即以向量形式 z:list,tuple或者numpy数组 ''' return 1.0/(1.0+np.exp(-z)) def sigmod_prime(z): ''' 定义S型函数的导数 ''' return sigmod(z)*(1-sigmod(z))
def network2_baseline():
#traning_data:[(784*1,10*1),...],50000个元素
#validation_data[(784*1,1*1),....],10000个元素
#test_data[(784*1,1*1),....],10000个元素
training_data,validation_data,test_data = mnist_loader.load_data_wrapper()
print('训练集数据长度',len(training_data))
print(training_data[0][0].shape) #训练集每一个样本的特征维数 (784,1)
print(training_data[0][1].shape) #训练集每一个样本对应的输出维数 (10,1)
print('测试集数据长度',len(test_data))
print(test_data[0][0].shape) #测试机每一个样本的特征维数,1,1 (784,1)
#print(test_data[0][1].shape) #测试机每一个样本对应的输出维数 () 这里与训练集的输出略有不同,这里输出是一个数 并不是二指化后的10*1维向量
print(test_data[0][1]) #7
#测试
net = Network2.Network([784,30,10])
'''
print(net.num_layers) #3
print(net.sizes)
print(net.weights)
print(net.biases)
'''
net.SGD(training_data,30,10,0.5,lamda=5,evaluation_data=validation_data,
monitor_evaluation_cost = True,
monitor_evaluation_accuracy=True,
monitor_training_cost=True,
monitor_training_accuracy=True)
#开始测试
network2_baseline()
参考文献