文章目录
上一个章节主要讲的是神经网络的前向传播,即在一直参数的条件下,计算出输出值,并且根据输出值的大小进行分类(对于分类问题)。本章主要讲通过梯度下降法,进行参数的更新
神经网络中的学习指的是:模型能够从训练数据中自动获取最优权重参数的过程。
介绍几个其中的概念:
- 泛化能力:指处理未被观察过的数据(未包含在训练数据汇总的数据)的能力。
- 训练数据和测试数据:模型通过对训练数据进行训练,不断更新参数,然后通过测试数据进行检验,看训练后的模型是否具有泛化能力。(一个很生动形象的例子,训练数据相当于高考的模拟卷,通过对模拟卷的练习,不断提高写卷子的准确性,然后高考就是测试数据,只有高考能够考好,你才算是成功的,类比到神经网络中,只有对测试数据进行检测,效果好,才能够说明这个模型是好的)
- 过拟合:就是模型的泛化能力不够,对训练数据能够有很好的效果,对测试数据检测的效果不好。
4.1 损失函数
4.1.1损失函数定义
损失函数: 神经网络以某一个指标为线索,寻找最优的权重参数,这个指标叫做损失函数,损失函数越小的参数,即是最优的参数。
分类: 损失函数一般由两种,一种是均方误差,另一种是交叉熵误差。
损失函数的引入原因: 如何判断一个神经网络模型的优劣性?有人可能说用识别精度,但是如果以识别精度为指标,则参数的导数在绝大部分地方都会变为0(仅仅微调参数,不能够改变精度值,而且精度值不是连续变化的)。在寻找最优权重参数值,要选用使得损失函数最小的权重参数。损失函数是连续可导的,通过使用梯度下降法,能够不断地更新权重参数,使得损失函数越小,神经网络的模型也会更加优化。
4.1.2损失函数的实现
损失函数一般分为:均方误差和交叉熵误差,用来评估预测的结果与实际结果的差异性。
- 均方误差
均方误差: 损失函数中,均方误差用的最多,方差的求取也是相似的公式。
图1:均方误差表达式
其中y(k)表示神经网络的输出;t(k)表示监督数据(实际标签数据),k表示数据的维度。
- 交叉熵误差
交叉熵误差: 公式形式如下:
图2:交叉熵误差表达式
- 两种损失函数的实现代码与例子:
import matplotlib.pyplot as plt
import numpy as np
'''
两种类型的损失函数:均方误差和交叉熵误差
'''
#损失函数:均方误差
def mean_square_error(y,t):
return 0.5*np.sum((y-t)**2)
#损失函数:交叉熵误差(为了是计算能够继续,加了一个delta)
def cross_entropy_error(y,t):
delta=1e-7
return -np.sum(t*np.log(y+delta))
y=[0.1,0.05,0.6,0.0,0.05,0.1,0.0,0.1,0.0,0.0];
#softmax函数的输出,可以表示为概率
t=[0,0,1,0,0,0,0,0,0,0];
#该正确标签的表示法,称为one-hot表示,正确标签置1,其余均为0
y1=mean_square_error(np.array(y),np.array(t))
y2=cross_entropy_error(np.array(y),np.array(t))
print('均方误差=',y1,'y交叉熵误差=',y2)
#输出值越小,越准确。
输出:
均方误差= 0.09750000000000003
交叉熵误差= 0.510825457099338
4.2 导数与梯度
根据上一节所说,可以对损失函数使用梯度下降法,来进行权重的更新,使得损失函数最小,进而得到最优的权重参数。梯度下降法就需要求出目标函数的梯度(梯度又是由各个未知数的导数所构成的)
4.2.1导数
求导的方式有两种,一种是数值微分求导,一种是解析性求导。梯度下降法主要用的是数值微分求导,因为目标函数常常很复杂。
数值微分求导: 利用微小的差分求导数的过程称为数值微分。有一定的误差。
解析性求导: 基于数学式的推导求导数的过程,称为解析性求导,没有误差。
图1:均方误差表达式
- 代码实现数值微分求导
#前向差分(不太好)
def numerical_diff0(f,x):
h=1e-4
return (f(x+h)-f(x))/h
#数值微分求导(中心差分) 比较好
def numerical_diff(f,x):
h=1e-4
return (f(x+h)-f(x-h))/(2*h)
#切线的曲线
def function_qiexian(f,x,x0):
k=numerical_diff(f,x0)
return k*(x-x0)+f(x0)
- 举例说明求导过程
问题描述:首先有一个函数y=0.01x^2+0.1x,通过数值微分法,求这个函数在x=5和x=10的导数
#函数的定义
def function_1(x):
y=0.01*x**2+0.1*x
return y
#数值微分求导(中心差分) 比较好
def numerical_diff(f,x):
h=1e-4
return (f(x+h)-f(x-h))/(2*h)
#库的引用
import numpy as np
#调用函数numerical_diff进行数值微分求导
#在x=5和x=10的导数
d1=numerical_diff(function_1,5)
print('x=5的导数:',d1)
d2=numerical_diff(function_1,10)
print('x=10的导数:',d2)
输出:
x=5的导数: 0.20000099999917254
x=10的导数: 0.3000009999976072
- 追加问题
求出上面函数y=0.01x^2+0.1x在x=5时的导数,并且用图像绘制出函数曲线和切线。
#函数的定义
def function_1(x):
y=0.01*x**2+0.1*x
return y
#数值微分求导(中心差分) 比较好
def numerical_diff(f,x):
h=1e-4
return (f(x+h)-f(x-h))/(2*h)
#切线的曲线
def function_qiexian(f,x,x0):
k=numerical_diff(f,x0)
return k*(x-x0)+f(x0)
#库的引用
import numpy as np
import matplotlib.pylab as plt
#原函数和切线函数的定义
x=np.arange(0,20,0.1)
y=function_1(x)
y1=function_qiexian(function_1,x,5)
#坐标轴的标签
plt.xlabel('x')
plt.ylabel('f(x)')
#绘图与显示
plt.plot(x,y)
plt.plot(x,y1)
plt.show()
输出:
图3:原函数和在x=5处切线图
4.2.2 偏导数和梯度
偏导数: 导数一般是对只含有一个变量的函数进行求导;对于含有多个变量的函数,对其中一个变量进行求导,即为偏导数。
梯度: 梯度相当于是偏导数的集合,即对函数中每一个变量进行求偏导,由全部变量的偏导数汇总而成的向量称为梯度。
- 问题:对于函数y = x 1 2 + x 2 2 y = {x_1}^2 + {x_2}^2y=x12+x22,求其在(3,4)点处的偏导数
#求梯度函数的定义
def numerical_gradient(f,x):
h=1e-4 #0.0001
grad=np.zeros_like(x) #生成和x形状相同的数组
for idx in range(x.size):
tmp_val=x[idx] #一个一个进行求导
#f(x+h)的计算
x[idx]=tmp_val+h
fxh1=f(x)
#f(x+h)的计算
x[idx]=tmp_val-h
fxh2=f(x)
grad[idx]=(fxh1-fxh2)/(2*h)
x[idx]=tmp_val #还原值
return grad
#目标函数的两种写法
def function_2(x):
return x[1]**2+x[2]**2
def function_3(x):
if x.ndim == 1:
return np.sum(x**2)
else:
return np.sum(x**2, axis=1) #列方向求和
#主函数,引用库,求梯度函数逇调用
import numpy as np
d3=numerical_gradient(function_3,np.array([3.0 ,4.0]))
print(d3)
输出:
[6. 8.]
4.3 梯度下降法
- 机器学习的主要任务是在学习时寻找最优的参数,即让损失函数取得最小值的权重参数即为最优。
- 一个函数在某一点的梯度是目标函数增长最快的方向,负梯度也就成了在改点下降最快的方向。
- 通过梯度下降法能够求得目标函数的最小值。
4.3.1 梯度下降法数学表示:
图4:梯度下降法数学表示
其中η \etaη表示学习率,也可以叫做是步长,以多大的位移向下降速度最快的方向移动。(不能过大,也不能过小,认为选取,是一个超参数)
4.3.2 代码显示梯度下降法:
- 问题: 对目标函数y = x 1 2 + x 2 2 y = {x_1}^2 + {x_2}^2y=x12+x22,通过梯度下降法求解其最小值,并且绘制出其迭代的路径,用图像显示出来
下面这串代码有点长,主要有一下四部分:
1 .库的申明部分
2 .子函数的定义,其中包括求梯度函数定义,梯度下降法的函数定义和目标函数的定义。
3 .主函数部分,其中包括初始值的确定;调用子函数,获取历史梯度信息;绘制历史梯度信息的迭代变化和等高线的绘制。
'''
库的申明
'''
import numpy as np
#import math
import matplotlib.pylab as plt
'''
子函数的定义
'''
#求梯度
def numerical_gradient(f,x):
h=1e-4 #0.0001
grad=np.zeros_like(x) #生成和x形状相同的数组
for idx in range(x.size):
tmp_val=x[idx] #一个一个进行求导
#f(x+h)的计算
x[idx]=tmp_val+h
fxh1=f(x)
#f(x+h)的计算
x[idx]=tmp_val-h
fxh2=f(x)
grad[idx]=(fxh1-fxh2)/(2*h)
x[idx]=tmp_val #还原值
return grad
#梯度下降法
def gradient_decent(f,init_x,lr,step_num=1000):
x=init_x
for i in range(step_num):
grad=numerical_gradient(f,x)
x -=lr*grad
return x
#梯度下降法(带历史记录)
def gradient_decent1(f,init_x,lr,step_num):
x=init_x
x_history=[]
for i in range(step_num):
x_history.append( x.copy() ) #复制保存每一次迭代的梯度值;最后一次的数据存入不了
grad=numerical_gradient(f,x)
x -=lr*grad
return x, np.array(x_history)
#目标函数
def function_2(x):
return x[0]**2+x[1]**2
'''
主函数
'''
#初始值的定义
step_num=100 #迭代次数
lr=0.2 #学习率
init_x=np.array([-3.0,4.0]) #迭代初始点
#调用函数,得到历史梯度值
#x,x_history =gradient_decent(function_2,init_x,lr=0.4,step_num=100)
x,x_history= gradient_decent1(function_2, init_x, lr, step_num)
#print(x)
#print(x_history)
print('最后的迭代点',x)
#开始绘制图像
#先画两条虚线
plt.plot([-5,5],[0,0],'--b')
plt.plot([0,0],[-5,5],'--b')
x=np.arange(-5,5,0.1)
#绘制历史梯度信息点
plt.plot(x_history[:,0], x_history[:,1], 'ro')
#绘制等高线
x1 = y1 = np.arange(-4, 4, 0.1)
x1, y1 = np.meshgrid(x1,y1) #将原始数据变为网格数据形式
for i in range(6):
plt.contour(x1, y1, x1**2 + y1**2, [3*i],) #x**2 + y**2 = 9 的圆形
#坐标轴的尺度
plt.xlim(-3.5, 3.5)
plt.ylim(-4.5, 4.5)
#坐标轴的标签
plt.xlabel("X0")
plt.ylabel("X1")
#plt.axis('scaled') #使坐标轴是标准的(即x轴与y轴的比例尺一致)
#显示图像
plt.show()
结果:
最后的迭代点 [-2.14405215e-21 6.35274710e-21]
图5:结果图
4.3.3 学习率对迭代的影响
学习率:学习率是一个超参数,决定着迭代点移动方向的步长,过大和过小都不合适。
以上面的为题为例,上面的学习率设置的为0.2,迭代次数为100,较为合适
当固定迭代次数,设置偏大或者偏小的学习率,迭代的效果都不太一样。
测试结果如下:
- 学习率过大:学习率设置为1
结果:
最后的迭代点 [-6.11110793e-10 8.14814391e-10]
图6:结果图
- 学习率过小:学习率设置为0.01
最后的迭代点 [-0.39785867 0.53047822]
图6:结果图
- 分析
一般情况下,迭代的次数都是一定的,迭代次数越多,需要的计算时间就会越长,只能够通过调整学习率来优化模型。学习率过大,就不能够精细的调整迭代点,很容易直接就跨过了最优值点;学习率过小,在有限的迭代次数中,移动不了多少位置,可能在最后都走不到最优值点的位置,所以设置一个合适的学习率非常重要。
前一个博客中介绍了损失函数,梯度,梯度下降等关键词,这一个博客,结合前面的知识,编写程序,使得整个深度学习算法能够进行权值迭代,进行更新。
4.5 学习算法的实现
神经网络的学习步骤如下:
- 前提
神经网络存在适合的权重和偏置,调整权重和偏置以便你和训练数据的过程称之为"学习",神经网络的学习分为下面四个步骤: - 步骤一(mini-batch)
从训练数据中随机选出一部分数据,这部分数据称为mini-batch,我们的目标是减小mini-batch的损失函数的值 - 步骤二(计算梯度)
为了减小mini-batch的损失函数的值,需要求出各种权重参数的梯度。梯度表示损失函数的值减少最多的方向。 - 步骤三(更新参数)
将权重参数沿着梯度方向进行微小更新 - 步骤四(重复)
重复步骤一,二,三,训练数据中的值全部训练完,使得训练后的模型精准。
注: 在进行参数更新的时候,使用的方法是随机梯度下降法,即对随机选择的数据进行的梯度下降法,简称SGD。
4.5.1 定义一个两层网络的类
这个类之中包含个子函数,如下:
图1: 子函数中的具体函数
文件1:Two_Layer_Net.py
#库和函数的导入
import sys,os
import numpy as np
sys.path.append(os.pardir) #前面这两行代码,纯粹是为了跨文件夹调用文档中的函数
from TLN_function import * #调用TLN_function文件中所有子函数
class TwoLayerNet:
def __init__(self,input_size,hidden_size,output_size,weight_init_std=0.01):
#初始化权重,并且定义几个实例变量,(也就是在类中的局部变量)
self.params={} #初始化实例变量params,里面有四个变量,分别是W1 ,W2,b1,b2
#W1和W2使用符合高斯分布的随机数进行初始化
self.params['W1']=weight_init_std*np.random.randn(input_size,hidden_size)
self.params['W2']=weight_init_std*np.random.randn(hidden_size, output_size)
#b1和b2使用0进行初始化
self.params['b1']=np.zeros(hidden_size) #初始值全部设为0
self.params['b2']=np.zeros(output_size) #初始值全部设为0
#x:图像数据 ;t:正确解标签
#前向传播算法
def predict(self,x):
#赋值变量
W1,W2=self.params['W1'],self.params['W2']
b1,b2=self.params['b1'],self.params['b2']
#两层网络的前向传播算法
a1=np.dot(x,W1)+b1
z1=sigmoid(a1)
a2=np.dot(z1,W2)+b2
y=softmax(a2)
return y
#损失函数(交叉熵误差函数u,求出损失值)
def loss(self,x,t):
y=self.predict(x)
loss_x=cross_entropy_error(y,t)
return loss_x
#计算准确率函数
def accuracy(self,x,t):
y=self.predict(x)
y=np.argmax(y,axis=1) #求出y中每一列中的最大值
t=np.argmax(t,axis=1) #求出t中每一列中的最大值
accuracy=np.sum(y==t)/float(x.shape[0]) #强制类型转换
return accuracy
#x:图像数据 ;t:正确解标签
#计算权重参数的梯度
#负梯度法求解权重参数梯度值
def numerical_gradient(self,x,t):
loss_W=lambda W:self.loss(x,t) #看不懂,什么意思
grads={} #定义参数的梯度信息,存取权重参数的梯度信息值
#求取四个权重参数的梯度信息值,并且存入grad中
grads['W1']=numerical_gradient(loss_W,self.params['W1'])
grads['b1']=numerical_gradient(loss_W,self.params['b1'])
grads['W2']=numerical_gradient(loss_W,self.params['W2'])
grads['b2']=numerical_gradient(loss_W,self.params['b2'])
return grads
#误差反向传播法求解梯度值
def gradient(self, x, t):
W1, W2 = self.params['W1'], self.params['W2']
b1, b2 = self.params['b1'], self.params['b2']
grads = {}
batch_num = x.shape[0]
# forward
a1 = np.dot(x, W1) + b1
z1 = sigmoid(a1)
a2 = np.dot(z1, W2) + b2
y = softmax(a2) #自己定义,可以叠加多层
# backward
dy = (y - t) / batch_num
grads['W2'] = np.dot(z1.T, dy)
grads['b2'] = np.sum(dy, axis=0)
da1 = np.dot(dy, W2.T)
dz1 = sigmoid_grad(a1) * da1
grads['W1'] = np.dot(x.T, dz1)
grads['b1'] = np.sum(dz1, axis=0)
return grads
4.5.2 子函数
在定义这一个两层网络的时候,需要用到一些子函数,如下:
- 求解梯度的函数:numerical_gradient(f, x)
- 激活函数:sigmoid(x),softmax(x),sigmoid_grad(x)
- 损失函数(交叉熵误差):cross_entropy_error(y, t)
文件2:TLN_function.py
#TwoLayerNet中的子函数
import numpy as np
#求梯度函数
def numerical_gradient(f, x):
h = 1e-4 # 0.0001
grad = np.zeros_like(x)
it = np.nditer(x, flags=['multi_index'], op_flags=['readwrite'])
while not it.finished:
idx = it.multi_index
tmp_val = x[idx]
x[idx] = float(tmp_val) + h
fxh1 = f(x) # f(x+h)
x[idx] = tmp_val - h
fxh2 = f(x) # f(x-h)
grad[idx] = (fxh1 - fxh2) / (2*h)
x[idx] = tmp_val # 还原值
it.iternext()
return grad
#激活函数(层与层之间使用)
def sigmoid(x):
return 1 / (1 + np.exp(-x))
#激活函数(输出层使用)
def softmax(x):
if x.ndim == 2:
x = x.T
x = x - np.max(x, axis=0)
y = np.exp(x) / np.sum(np.exp(x), axis=0)
return y.T
x = x - np.max(x) # 溢出对策
return np.exp(x) / np.sum(np.exp(x))
#损失函数的求取
def cross_entropy_error(y, t):
if y.ndim == 1:
t = t.reshape(1, t.size)
y = y.reshape(1, y.size)
# 监督数据是one-hot-vector的情况下,转换为正确解标签的索引
if t.size == y.size:
t = t.argmax(axis=1)
batch_size = y.shape[0]
return -np.sum(np.log(y[np.arange(batch_size), t] + 1e-7)) / batch_size
#误差反向传播算法要用的子函数
def sigmoid_grad(x):
return (1.0 - sigmoid(x)) * sigmoid(x)
4.5. 3 主函数
最后就是主函数了,包括以下几个小模块:
- 手写数据集mnist的数据获取
- 超参数的定义
- 深度学习的四个步骤(见本文的开篇)
- 图像的绘制
文件:TLN_main.py
'''#ini-batch 进行算法的实现'''
#相关库的调用
import time
import numpy as np
import matplotlib.pyplot as plt
import sys,os
sys.path.append(os.pardir) #前面这两行代码,纯粹是为了跨文件夹调用文档中的函数
from dataset.mnist import load_mnist #调用加载mnist数据集的函数
from Two_Layer_Net import TwoLayerNet #调用编辑的两层神经网络构成的类
#from two_layer_net import TwoLayerNet
#手写数据集mnist的数据获取
(x_train,t_train),(x_test,t_test)=load_mnist(flatten=True,normalize=True,one_hot_label=True)
#load_mnist以(训练图片,训练标签),(测试图片,测试标签)的形式读入数据集
#normalize:是否将输入图片正规化为0.0-1.0的值,这里没有正规化,图片像素仍然为0-255
#flatten:是否展开输入图像(变为一维数组),这里是用一维数组显示的
#one_hot_label:仅正确标签为1,其他均为0的数组,形如[0,0,0,1,0,0],如果为False,则仅保存2,7等正确解的标签
#平均每一个epoch重复的次数
#超参数的定义
iters_num=10000
train_size=x_train.shape[0] #总训练集的大小
batch_size=100 #mini_batch的大小
learning_rate=0.5 #学习率,也相当于是步长
network= TwoLayerNet(input_size=784,hidden_size=100,output_size=10)
#给两层神经网络的基本参数进行定义:
#神经网络:两层,输入为784个,隐藏神经元为50个,输出为10个。
#定义一些计算损失函数和精确度值的矩阵
train_loss_list=[] #训练损失函数
train_acc_list=[] #训练数据的准确率
test_acc_list=[] #测试数据的准确率
iter_per_epoch=max(train_size/batch_size,1)
#主函数
start = time.clock() #计时开始
for i in range(iters_num):
#获取随机mini——batch
batch_mask=np.random.choice(train_size,batch_size) #从总的训练集数中选取batch_size个随机数,不带重复的
x_batch=x_train[batch_mask]
t_batch=t_train[batch_mask]
#计算梯度
#grad=network.numerical_gradient(x_batch,t_batch)
#采用误差反向传播法:
grad=network.gradient(x_batch,t_batch)
#更新参数
for key in ('W1','b1','W2','b2'):
network.params[key] -= learning_rate*grad[key]
#记录学习过程
loss=network.loss(x_batch,t_batch)
train_loss_list.append(loss) #不懂代码意思
#每经过一个epoch(参数更新后),就对所有的训练数据和测试数据计算识别精度
if i % iter_per_epoch==0:
#训练数据和测试数据的记录
train_acc=network.accuracy(x_train,t_train)
test_acc=network.accuracy(x_test,t_test)
train_acc_list.append(train_acc)
test_acc_list.append(test_acc)
# print('train_acc,test_acc| '+str(train_acc)+' , '+test_acc)
print('train_acc,test_acc|', train_acc,',',test_acc)
#增加一个计时功能
end = time.clock() #计时结束
print ('Running time:',str(end-start)) #显示所耗时间
#绘制图形
x = np.arange(len(train_acc_list)) #绘制图像的最主要的三行,变量与因变量
plt.plot(x, train_acc_list, label='train acc', marker='o')
plt.plot(x, test_acc_list, label='test acc', marker='x',linestyle='--')
plt.xlabel("epochs") #显示横纵坐标的标签
plt.ylabel("accuracy")
plt.ylim(0, 1.0)
plt.legend(loc='lower right') #显示图例,在右下角
plt.savefig('./test2.jpg') #保存显示的图片
plt.show()