1 打开深度学习的大门:神经网络概述
人工神经网络(Artificial Neural Network,ANN),通常简称为神经网络,它是机器学习当中独树一帜的,最强大的强学习器没有之一。机器学习是研究如何让计算机学习的学科,是研究如何赋予计算机学习能力的学科。在过去的十一周中,我们介绍了各种各样能够让计算机“学习”到知识并自行做出判断的手段(算法),他们之中大多数是从“如何做出判断”或者“如何提取规则”的角度去探求出让计算机学习的方式,但却没有任何一种算法使用了和人类相似的学习方式。
二十世纪后半,神经学家们发现人脑主要通过”生物神经网络“来进行学习。人类大脑主要由称为神经元的神经细胞组成,通过名为轴突的纤维束与其他神经元连接在一起。每当神经元接受到信号,神经元便会受到刺激,此时纤维束会将信号从一个神经元传递到另一个神经元上,而人类正是通过在神经元上传入、传出信息来进行学习。
不难发现,这个过程其实可以和机器学习类比起来:在传统机器学习中,我们建立模型,对模型输入特征矩阵,模型将特征矩阵转化为判断结果后为我们输出,如果将模型看作是单个神经元,特征矩阵看作是神经元接受的”信号“,模型对特征矩阵的处理过程看作神经元受到的刺激,最后输出的结果看成是神经元通过轴突传递出的信号,那人类学习的模式是可以一定程度上被算法模仿的。
人脑通过构建复杂的网络可以进行逻辑,语言,图像的学习,而传统机器学习算法不具备和人类相似的学习能力。机器学习研究者们相信,模拟大脑结构可以让机器的学习能力更上一层楼,于是人工神经网络算法应运而生,现在基本都简称为”神经网络“。有了神经网络,基于其算法延申出来的机器学习分枝学科——深度学习也从此走入了人们的视野,成为所有让世人惊叹的人工智能技术的根基。在深度学习中,我们使用圆来表示神经元,使用线表示数据流动的方向。我们从神经元左侧输入特征,让神经元处理数据,并从右侧输出预测结果,以此来模拟人脑的学习过程。
在人脑中,大约存在8-10兆个如上所述的神经元,一次学习中调用的神经元越多,人类可以处理和学习的信息也越多。神经网络算法也是如此,神经元越多,算法对数据的处理能力和学习能力越强。所以大家经常会看见的神经网络结构可能是这样的:
到这里,许多人可能会好奇:神经元是怎样处理数据的呢?数据在众多神经元中是怎么传递的呢?如果神经元的数量影响神经网络的学习能力,那我们该如何去确认这个数量呢?这些问题都是神经网络算法十分核心的问题,你可以在今天的课程中读到所有的答案。
思考
神经元越多,神经网络对数据的学习能力越强。如果神经元太多,可能会发生什么机器学习中普遍存在的现象呢?
现在,机器学习能够在逻辑运算上比人类表现得更好已经是一个通识了——AlphaGo
战胜世界围棋冠军,视频通话中进行实时字幕和翻译,火车站飞机场的人脸识别系统,都是人工智能技术落地的经典例子。这些技术应用的背后全部都是以神经网络为核心的深度学习技术。因此,神经网络作为机器学习课程中的最后一个算法,是链接了机器学习和深度学习之间的桥梁。
在这里不得不提到几个要点。传统机器学习领域有三大追求:算法效果,运算速度和可解释性,而这三个追求都是为了业务而服务。其中算法效果与剩下的两大特征:运算速度、可解释性是一定程度上相互矛盾的。神经网络作为追求算法效果到极致的算法,几乎完全放弃了可解释性和运算速度,因此神经网络算法落地时的核心与其他我们已经学过的机器学习算法都不同:除了理解原理,了解提高识别精度的参数调优过程之外,还需要了解如何使用GPU将神经网络高速化,如何处理非结构化数据等等。
然而sklearn是专注于机器学习的库,它不是专用于深度学习的平台,也不具备处理大型数据的能力,所以sklearn中的神经网络和其他机器学习算法比起来显得有些不受重视,它不具备做真正的深度学习的功能,甚至对一些在深度学习中非常关键的点缺乏关注。因此,我们的课程也将不会包括:
1. Pytorch,Caffe,TensorFlow,Keras等深度学习框架的使用方法
2. 为了实现深度学习的高速化而进行的GPU、dropout相关的实现
3. 自然语言处理,图像识别,语音识别,视频识别的例子
4. 基本神经网络之外的,如卷积神经网络,循环神经网络,LSTM等更高级的深度学习算法
另外,同样也非常重要的是,我假设在观看课程的你已经具备了扎实的机器学习基础,了解机器学习中的基本概念,并对机器学习中常见算法比较熟悉(或者,当你不熟悉或忘记一些内容的时候,你知道从哪里能够获得这些机器学习算法的详细资料)。
sklearn中的神经网络课程将会注重向熟练使用机器学习算法、但对于深度学习尚不了解的技术人员讲解神经网络核心的工作原理。在sklearn中,神经网络只包含了如下的三个类:最初的神经网络玻尔兹曼机,以及已经成熟的以多层感知机为基础的神经网络分类和神经网络回归。我们的重点将会放在后两个类:MLPClassifier和MLPRegressor上。
#首先使用numpy来创建数据
import numpy as np
X = np.array([[0,0],[1,0],[0,1],[1,1]])
z_reg = np.array([-0.2, -0.05, -0.05, 0.1])
X
X.shape
z_reg
#定义实现简单线性回归的函数
def LinearR(x1,x2):
w1, w2, b = 0.15, 0.15,-0.2 #给定一组系数w和b
z = x1*w1 + x2*w2 + b #z是系数*特征后加和的结果
return z
LinearR(X[:,0],X[:,1])
可以看到,只要能够给到适合的w和b,回归神经网络其实非常容易实现。从这样的一个简单回归神经网络,我们很容易就可以把它推广到分类模型上。
2.2 二分类单层神经网络:sigmoid与阶跃函数
在过去我们学习逻辑回归时,我们了解到sigmoid函数可以帮助我们将线性回归连续型的结果转化为0-1之间的概率值,从而帮助我们将回归类算法转变为分类算法逻辑回归。对于神经网络来说我们也可以使用相同的方法。首先先来复习一下Sigmoid函数的的公式和性质:
#重新定义数据中的标签
y_and = [0,0,0,1]
#根据sigmoid公式定义sigmoid函数
def sigmoid(z):
return 1/(1 + np.exp(-z))
def AND_sigmoid(x1,x2):
w1, w2, b = 0.15, 0.15,-0.2 #给定的系数w和b不变
z = x1*w1 + x2*w2 + b
o = sigmoid(z) #使用sigmoid函数将回归结果转换到(0,1)之间
y = [int(x) for x in o >= 0.5] #根据阈值0.5,将(0,1)之间的概率转变
为分类0和1
return o, y
#o:sigmoid函数返回的概率结果
#y:对概率结果按阈值进行划分后,形成的0和1,也就是分类标签
o, y_sigm = AND_sigmoid(X[:,0],X[:,1])
o
y_sigm
y_sigm == y_and
可见,这里得到了与我们期待的结果一致的结果,这就将回归算法转变为了二分类。这个过程在神经网络中的表示图如下:
def AND(x1,x2):
w1, w2, b = 0.15, 0.15, -0.23 #和sigmoid相似的w和b
z = x1*w1 + x2*w2 + b
y = [int(x) for x in z >= 0]
return y
AND(X[:,0],X[:,1])
y_and
2.3 多分类单层神经网络:softmax回归
在了解二分类后,我们可以继续将神经网络推广到多分类。在sklearn中,我们曾经学习过逻辑回归做多分类的做法。逻辑回归通过Many-vs-Many(多对多)和One-vs-Rest(一对多)模式来进行多分类。其中,OvR是指将多个标签类别中的一类作为类别1,其他所有类别作为类别0,分别建立多个二分类模型,综合得出多分类结果的方法。MvM是指把好几个标签类作为1,剩下的几个标签类别作为0,同样分别建立多个二分类模型来得出多分类结果的方法。这两种方法非常有效,尤其是在逻辑回归做多分类的问题上能够解决很多问题,但是对于神经网络却不奏效。理由非常简单:
1. 逻辑回归是一个非常快速的算法,在使用OvR和MvM这样需要同时建立多个模型的方法时,运算速度不会成为太大的问题。但神经网络本身是一个计算量庞大
的算法,建立一个模型就会耗费很多时间,因此必须建立很多个模型来求解的方法对神经网络来说就不够高效。
2. 我们有更好的方法来解决这个问题,那就是softmax回归。Softmax函数是深度学习基础中的基础,它是神经网络进行多分类时,默认放在输出层中处理数据的函数。假设现在神经网络是用于三分类数据,且三个分类分别是苹果,柠檬和百香果,序号则分别是分类1、分类2和分类3。则使用softmax函数的神经网络的模型会如下所示:
#假定一组巨大的z
z = np.array([1010,1000,990])
np.exp(z) / np.sum(np.exp(z)) # softmax函数的运算
#定义softmax函数
def softmax(z):
c = np.max(z)
exp_z = np.exp(z - c) #溢出对策
sum_exp_z = np.sum(exp_z)
o = exp_z / sum_exp_z
return o
#导入刚才定义的z
softmax(z)
来看看刚才求出的softmax函数的结果加和之后会显示怎样的效果:
sum(softmax(z))
#定义数据
X = np.array([[0,0],[1,0],[0,1],[1,1]])
y_and = [0,0,0,1]
def AND(x1,x2):
w1, w2, b = 0.15, 0.15, -0.23 #和sigmoid相似的w和b
z = x1*w1 + x2*w2 + b
y = [int(x) for x in z >= 0]
return y
AND(X[:,0],X[:,1])
y_and
考虑到这一组数据只有两维,我们可以通过以下代码将数据可视化,其中,紫色点代表了类别0,红色点代表类别1。
import matplotlib.pyplot as plt
import seaborn as sns
plt.style.use('seaborn-whitegrid') #设置图像的风格
sns.set_style("white")
plt.figure(figsize=(5,3)) #设置画布大小
plt.title("AND GATE",fontsize=16) #设置图像标题
plt.scatter(X[:,0],X[:,1],c=y_and,cmap="rainbow") #绘制散点图
plt.xlim(-1,3) #设置横纵坐标尺寸
plt.ylim(-1,3)
plt.grid(alpha=.4,axis="y") #显示背景中的网格
plt.gca().spines["top"].set_alpha(.0) #让上方和右侧的坐标轴被隐藏
plt.gca().spines["right"].set_alpha(.0);
import numpy as np
x = np.arange(-1,3,0.5)
plt.plot(x,(0.23-0.15*x)/0.15 #这里是从直线的表达式变型出的x2 = 的式子
,color="k",linestyle="--")
#定义数据
X = np.array([[0,0],[1,0],[0,1],[1,1]])
#或门、或门的图像
y_or = [0,1,1,1]
def OR(x1,x2):
w1, w2, b = 0.15, 0.15, -0.08 #为了拟合不同的标签,重新定义一组w和b
z = x1*w1 + x2*w2 + b
y = [int(x) for x in z >= 0]
return y
OR(X[:,0],X[:,1])
#绘制直线划分散点的图像
x = np.arange(-1,3,0.5)
plt.figure(figsize=(5,3))
plt.title("OR GATE",fontsize=16)
plt.scatter(X[:,0],X[:,1],c=y_or,cmap="rainbow")
plt.plot(x,(0.08-0.15*x)/0.15,color="k",linestyle="--")
plt.xlim(-1,3)
plt.ylim(-1,3)
plt.grid(alpha=.4,axis="y")
plt.gca().spines["top"].set_alpha(.0)
plt.gca().spines["right"].set_alpha(.0)
#非与门、非与门的图像
y_nand = [1,1,1,0]
def NAND(x1,x2):
w1, w2, b = -0.15, -0.15, 0.23 #同样为了拟合不同的标签,重新定义一组
w和b
z = x1*w1 + x2*w2 + b
y = [int(x) for x in z >= 0]
return y
NAND(X[:,0],X[:,1])
#图像
x = np.arange(-1,3,0.5)
plt.figure(figsize=(5,3))
plt.title("NAND GATE",fontsize=16)
plt.scatter(X[:,0],X[:,1],c=y_nand,cmap="rainbow")
plt.plot(x,(0.23-0.15*x)/0.15,color="k",linestyle="--")
plt.xlim(-1,3)
plt.ylim(-1,3)
plt.grid(alpha=.4,axis="y")
plt.gca().spines["top"].set_alpha(.0)
plt.gca().spines["right"].set_alpha(.0)
#定义数据
X = np.array([[0,0],[1,0],[0,1],[1,1]])
y_and = [0,0,0,1]
def AND(x1,x2):
w1, w2, b = 0.15, 0.15, -0.23 #和sigmoid相似的w和b
z = x1*w1 + x2*w2 + b
y = [int(x) for x in z >= 0]
return y
AND(X[:,0],X[:,1])
y_and
y_xor = [0,1,1,0]
plt.figure(figsize=(5,3))
plt.title("XOR GATE",fontsize=16)
plt.scatter(X[:,0],X[:,1],c=y_xor,cmap="rainbow")
plt.xlim(-1,3)
plt.ylim(-1,3)
plt.grid(alpha=.4,axis="y")
plt.gca().spines["top"].set_alpha(.0)
plt.gca().spines["right"].set_alpha(.0)
#回忆一下XOR数据的真实标签
y_xor = [0,1,1,0]
def AND(x1,x2):
w1, w2, b = 0.15, 0.15, -0.23
z = x1*w1 + x2*w2 + b
#下面这一行就是阶跃函数的表达式,注意AND函数是在输出层,所以保留输出层的阶
跃函数g(z)
y = [int(x) for x in z >= 0]
return y
def OR(x1,x2):
w1, w2, b = 0.15, 0.15, -0.08
z = x1*w1 + x2*w2 + b
#y = [int(x) for x in z >= 0] #注释掉阶跃函数,相当于h(z)是恒等函数
return z
def NAND(x1,x2):
w1, w2, b = -0.15, -0.15, 0.23
z = x1*w1 + x2*w2 + b
#y = [int(x) for x in z >= 0] #注释掉阶跃函数,相当于h(z)是恒等函数
return z
def XOR(x1,x2):
z_nand = NAND(X[:,0],X[:,1])
z_or = OR(X[:,0],X[:,1])
y_and = AND(np.array(z_nand),np.array(z_or))
return y_and
XOR(X[:,0],X[:,1])
很明显,此时XOR函数的预测结果与真实的y_xor不一致。当隐藏层的 h(z)是恒等函数或不存在时,叠加层并不能够解决XOR这样的非线性问题。从数学上来看,这也非常容易理解。
#根据sigmoid公式定义sigmoid函数
def sigmoid(z):
return 1/(1 + np.exp(-z))
def AND_sigmoid(x1,x2):
w1, w2, b = 0.15, 0.15,-0.23
z = x1*w1 + x2*w2 + b
#AND函数是位于输出层的,这里是g(z)而非h(z),因此不会受到将阶跃函数更换为
sigmoid的影响
#g(z)依然是阶跃函数
y = [int(x) for x in z >= 0]
return y
def OR_sigmoid(x1,x2):
w1, w2, b = 0.15, 0.15, -0.075
z = x1*w1 + x2*w2 + b
o = sigmoid(z) #这里是h(z),我们使用sigmoid函数
return o
def NAND_sigmoid(x1,x2):
w1, w2, b = -0.15, -0.15, 0.23
z = x1*w1 + x2*w2 + b
o = sigmoid(z) #这里是h(z),我们使用sigmoid函数
return o
def XOR_sigmoid(x1,x2):
o_nand = NAND_sigmoid(X[:,0],X[:,1])
o_or = OR_sigmoid(X[:,0],X[:,1])
y_and = AND_sigmoid(np.array(o_nand),np.array(o_or))
return y_and
XOR_sigmoid(X[:,0],X[:,1])
#如果g(z)是sigmoid函数,而h(z)是阶跃函数
#根据sigmoid公式定义sigmoid函数
def sigmoid(z):
return 1/(1 + np.exp(-z))
def AND_sigmoid(x1,x2):
w1, w2, b = 0.15, 0.15,-0.23
z = x1*w1 + x2*w2 + b
o = sigmoid(z) #输出层是sigmoid函数
y = [int(x) for x in o >= 0.5] #按0.5为阈值将结果化为0和1
return o,y
def OR(x1,x2):
w1, w2, b = 0.15, 0.15, -0.075
z = x1*w1 + x2*w2 + b
o = [int(x) for x in z >= 0] #这里是h(z),我们使用阶跃函数
return o
def NAND(x1,x2):
w1, w2, b = -0.15, -0.15, 0.23
z = x1*w1 + x2*w2 + b
o = [int(x) for x in z >= 0] #这里是h(z),我们使用阶跃函数
return o
def XOR(x1,x2):
o_nand = NAND(X[:,0],X[:,1])
o_or = OR(X[:,0],X[:,1])
y_and = AND_sigmoid(np.array(o_nand),np.array(o_or))
return y_and
XOR(X[:,0],X[:,1])
y_xor
def relu(z):
import numpy as np
return np.maximum(0,z)
maximum函数会从输入的数值中选择较大的那个值进行输出,以达到保留正数元素,将负元素清零的作用。ReLU的图像如下所示:
相对的,ReLU函数导数的图像如下:
对tanh求导后可以得到如下公式和导数图像:
5 neural_network.MLPClassifier
MLP是多层感知机multilayer perception的简写,所以MLPClassifier直译就是多层感知机分类器。为什么神经网络类会被叫做多层感知机分类器呢?实际上,感知机是最古老的机器学习分类算法之一,在1957年就已经被提出了,现代神经网络的原理都是基于感知机提出的。经过之前的学习,我们可以这样来定义感知机:
不过现在,多层感知机的概念更多是代表了一层一层嵌套下去的这种神经网络结构,所以现在多层感知机和多层神经网络基本已经是一致的概念了。这也是sklearn直接使用MLP这个名字来代表神经网络的原因。当然了,除了MLP之外还有别的神经网络,如RBM(伯努利限制玻尔兹曼机),只不过在机器学习领域我们接触得甚少,这里就不去讨论MLP以外的神经网络了。
以下是sklearn中的MLP分类器类的详情:
class sklearn.neural_network.MLPClassifier (hidden_layer_sizes=(100, ),activation=’relu’, solver=’adam’, alpha=0.0001, batch_size=’auto’,learning_rate=’constant’,learning_rate_init=0.001,power_t=0.5,max_iter=200,shuffle=True,random_state=None,tol=0.0001,verbose=False,warm_start=False,momentum=0.9,nesterovs_momentum=True,early_stopping=False,validation_fraction=0.1, beta_1=0.9, beta_2=0.999, epsilon=1e-08,n_iter_no_change=10)
这些参数涉及到神经网络概念、训练和提升的方方面面,接下来我们就来介绍关于神经网络基础的两个重要参数。
import numpy as np
from sklearn.neural_network import MLPClassifier as DNN
from sklearn.model_selection import cross_val_score as cv
from sklearn.datasets import load_breast_cancer
from sklearn.ensemble import RandomForestClassifier as RFC
from time import time
import datetime
#先使用机器学习中的数据来试试看神经网络的效果
data = load_breast_cancer()
X = data.data
y = data.target
y #二分类
2. 建模,使用交叉验证导出分数
dnn = DNN(hidden_layer_sizes=(200,)
,random_state=420 #random_state控制着神经网络上的某些随机性,
你能够猜到一些吗?
)
#这样,一个简单的神经网络就实例化完毕了
cv(dnn,X,y,cv=5).mean()
#接口predict:预测出结果,由于之前在交叉验证中的训练不会被记录,因此需要重新训
练
dnn.fit(X,y).predict(X)
#接口predict_proba:返回预测的概率
dnn.fit(X,y).predict_proba(X)
#看看运行时间如何
times = time()
dnn = DNN(hidden_layer_sizes=(200,),random_state=420)
print(cv(dnn,X,y,cv=5).mean())
print(time() - times)
#使用随机森林进行一个对比
times = time()
clf_rfc = RFC(n_estimators=200,random_state=420)
print(cv(clf_rfc,X,y,cv=5).mean())
print(time() - times)
3. 使用参数hidden_layer_sizes
times = time()
dnn = DNN(hidden_layer_sizes=(50,),random_state=420)
print(cv(dnn,X,y,cv=5).mean())
print(time() - times)
#试试看不用的神经元个数组合:(50,50),(50,100),(100,50),(100,100,100)
#不断调整hidden_layer_sizes中输入的内容,你发现了什么?
#来试试看,(70,)
这个时候你可能想起了我们之前提到过,神经网络是一个“黑箱“,这既是因为我们无法了解神经网络是如何得出预测结果的,也是因为我们很难总结出神经网络的隐藏层数与隐藏层上神经元的个数如何影响其结果,所以调整神经网络的参数很困难。当层数和神经元个数太少时,神经网络可能会处于严重的欠拟合状态,而神经元太多的时候,神经网络又会过拟合(实际上神经网络非常容易过拟合)。在业界,神经网络的层数与神经元个数的调整基本靠经验、穷举或者AutoML来完成。不过我们还是能够找得到一些基本的规律。关于隐藏层的数量,业界认为:如果能在较少的隐藏层下完成任务,则我们不优先选择增加层数。可以先看看一层隐藏层能否解决问题,在非工业问题上最多不超过3~5层,记住层数越多过拟合的可能性越大。关于隐藏层上的神经元的数量,有以下三个规则:
1、隐藏层的神经元数量一般在输入层的神经元数量与输出层神经元的数量之间
2、隐藏层神经元的数量应该是输入层神经元数量的2/3加上输出层神经元的数量
3、隐藏层神经元的数量应该少于输入层神经元数量的两倍
三条规则大家都可以使用,但这不是绝对的。如果你发现了更好的、不会导致过拟合的层与神经元的组合,请忽略以上的规则。
#使用重要属性n_layers_,显示神经网络的层数
dnn.fit(X,y).n_layers_
#这里返回的层数将输入层和输出层也考虑在其中
#重要属性classes_,查看返回结果中一共有多少个类别
dnn.fit(X,y).classes_
#重要属性n_outputs_,显示输出层上神经元的个数(即输出结果的个数)
#试想下二分类神经网络对应的g(z)和我们之前总结的规律,应该很容易就能够判断这里会
输出几
dnn.fit(X,y).n_outputs_
#如果更换数据,n_outputs会返回多少?
from sklearn.datasets import load_digits
dnn.fit(load_digits().data, load_digits().target).n_outputs_
np.unique(load_digits().target)
4. 提升神经网络效果的有效方法:归一化
from sklearn.preprocessing import StandardScaler as SS
X_ = SS().fit_transform(X)
times = time()
dnn = DNN(hidden_layer_sizes=(50,50),random_state=420)
print(cv(dnn,X_,y,cv=5).mean())
print(time() - times)
#最终的结果显示,(200,50)是一个比较有效的组合
for activef in ["identity","logistic","tanh","relu"]:
times = time()
dnn = DNN(hidden_layer_sizes=(200,50)
,activation = activef
#,max_iter = 2000
,random_state=420)
print(activef, cv(dnn,X_,y,cv=5).mean())
print(time() - times)
#重要属性out_activation_,猜猜它会返回什么内容?
dnn.fit(X,y).out_activation_
#如果更换数据,out_activation_会返回什么结果?
dnn.fit(load_digits().data, load_digits().target).out_activation_
#很明显,属性out_activation_返回的是g(z),而参数activation控制的是h(z)。
有了神经网络的基本结构和激活函数,就可以实现简单的网络了。在sklearn API下,我们很容易就可以得出一个预测结果,但是如何提升神经网络的预测表现,是深度学习中的难题,即便是战斗在一线的深度学习工程师,大部分时候也是依赖于经验、尝试和自动调参的手段。现在,先回到我们最初见到的XOR的网络结构。
4.1 神经网络学习的基本思想
学习是指模型从训练数据中自动获取最优权重参数的过程。这一过程的本质是使用优化算法不断迭代模型参数以降低模型损失函数的值,并最终找出损失函数最小时对应的参数向量w和b。神经网络也是遵循这一基本思想来进行学习。在sklearn中,我们可以通过参数solver来控制神经网络使用的优化算法,并使用max_iter这一熟悉的参数来控制最大迭代次数。
for solver_ in ["adam","lbfgs","sgd"]:
times = time()
dnn = DNN(hidden_layer_sizes=(200,50)
,solver = solver_
,activation = "relu"
,max_iter = 2000 #如果设置较小的max_iter会发生什么?
,random_state=420)
print(solver_, cv(dnn,X_,y,cv=5).mean())
print(time() - times)
max_iter非常容易理解,在这里我们更关注solver参数带来的变化。可以看到,adam拥有最好的结果(同时也是现在许多前沿技术人偏好的优化算法),lbfgs拥有最快的速度,而小批量随机梯度下降位于两者中间。一种最理想状态是,我们能够在课程中将adam优化算法的细节都梳理清楚,然而课时内容所限,无法完整地讲解adam,但幸运的是,adam其实就是在mini-batch SGD上改进而来的。因此,我将为大家详细讲解和剖析mini-batch SGD的细节,并提供了解adam的各种途径和手段,以便大家能够从课程中获得扎实基础,然后自发地学习adam的相关知识。
幸运的是,在学习逻辑回归时,我们深入地学习了普通的梯度下降算法。因此我们可以从普通梯度下降入手来学习神经网络的训练过程。如果你已经对梯度下降感觉到陌生,或之前没有学习过逻辑回归的篇章,请回到逻辑回归查看详细的梯度下降讲解和推导过程。
极端情况下,当我们每次随机选取的批量中只有一个样本时,梯度下降的迭代轨迹就会变得异常不稳定(如上图所示)。我们称这样的梯度下降为随机梯度下降(stochastic gradient descent,SGD)。mini-batch SGD的优势是算法不会轻易陷入局部最优,由于每次梯度向量的方向都会发生巨大变化,因此一旦有机会,算法就能够跳出局部最优,走向全局最优(当然也有可能是跳出一个局部最优,走向另一个局部最优)。不过缺点是,需要的迭代次数变得不明。如果最开始就在全局最优的范围内,那可能只需要非常少的迭代次数就收敛,但是如果最开始落入了局部最优的范围,或全局最优与局部最优的差异很小,那可能需要花很长的时间、经过很多次迭代才能够收敛,毕竟不断改变的方向会让迭代的路线变得曲折。
【深入】使用momentum的梯度下降
momentum是让梯度下降的梯度的方向持续变化,同时让其迭代路线变得平缓的手段,目前没有对momentum一词特别统一的中文翻译,不过许多深度学习书称其为“动量”。动量法通过为权重迭代添加”速度V”的概念,结合速度与步长共同调节权重迭代的细节,是一种数学不难、但原理很难说明得透彻的梯度下降改进法。其不透彻的地方主要在于:
1) 这样的数学变化为何能够让迭代路线变得平缓?
2) 这样的迭代变化如何保证梯度方向的变化不受影响,如果它不能保证,只是单纯让迭代路线变得平缓,那动量法的手段岂不是和单纯的增大batch_size的量有一样的效果?
在sklearn中的MLPClassifier一样,存在参数momentum和nesterovs_momentum与动量法有关,但由于课程篇幅有限,我们将不会就这一点来展开讨论。详细的内容可以在谷歌搜索如“gradient descent with momentum”这样的句子来获取,最简单的讲解方式可以直接进入该链接进行查看(英文): https://engmrk.com/gradient-descent-with-momentum/(注
意,该链接并不能解决上面提出的两个问题)
从整体来看,为了mini-batch SGD这“不会轻易被局部最优困住”的优点,我们在神经网络中使用它作为优化算法(或优化算法的基础)。当然,还有另一个流传更广、更为认知的理由支持我们使用mini-batch SGD。
4.1.2 关于mini-batch SGD提升模型速度的讨论
mini-batch SGD被推崇的第二个理由是,它可以提升神经网络的计算效率,让神经网络计算更快。
在深度学习中,神经网络的训练对象往往是图像、文字、语音、视频等非结构化数据,这些数据的特点之一就是特征矩阵一般都是大量高维的数据。比如在深度学习教材中总是被用来做例子的入门级数据MNIST,其训练集的结构为(60000,784)。对比机器学习中的入门级数据鸢尾花(结构为(150,4)),两者完全不在一个量级上。在深度学习中,如果梯度下降的每次迭代都使用全部数据,将会非常耗费计算资源,且样本量越大,计算开销越高。
许多深度学习的教材、书籍都声明,为了解决计算开销大的问题,我们要使用mini-batch SGD。考虑到可以从全部数据中选出一部分作为全部数据的“近似估计”,然后用选出的这一部分数据来进行迭代,每次迭代需要计算的数据量就会更少,计算消耗也会更少,因此神经网络的速度会提升。当然了,这并不是说使用1001个样本进行迭代一定比使用1000个样本进行迭代速度要慢,而是指每次迭代中计算上十万级别的数据,会比迭代中只计算一千个数据慢得多。
事实上,如果数据量不够大,我们很难直接地从神经网络的使用中看出“速度提升”这一点。在上一节中我们讨论过,虽然mini-batch SGD每次迭代时需要的数据量不大,但需要的迭代次数却因为变化巨大的梯度方向而变得不明确。如果每次迭代的时间变短,但是却需要更多次的迭代,也有可能导致神经网络的整体运行时间变长。理论上来说,若迭代次数确定,训练的样本量变小时,神经网络的计算应该会变得更快。但遗憾的是,在sklearn中真实使用小批量随机梯度下降的时候,计算时间会受到除了批量大小之外许多因素的影响,因此不一定能够展现出绝对的“小批量更快”的效果。从sklearn的实际使用结果来看,当样本量过小时,还会出现小批量计算时间更长的问题,这可能是因为sklearn底层用来进行矩阵计算的numpy和scipy比起小矩阵,更擅长处理大矩阵的缘故。但整体来看,小批量更容易快速产生结果。
关于这一点,我们可以自己来实验一下。在sklearn中,有两个参数与mini-batchSGD在迭代前的采样有关:
其中参数batch_size是我们一直称呼“sgd”为小批量随机梯度下降的原因。
batch_size的默认值是“auto”,它被设定为在样本量和200中选择一个较小的数。无论是机器学习还是深度学习,样本量小于200的情况非常少见,因此一般来说我们可以认为batch_size的默认值就是200。也就是说,当我们在solver参数中设定”sgd“的时候,其实我们是设置了随机抽样的批量尺寸为200的。真正的随机梯度下降是每次迭代时只抽样一个样本的随机梯度下降,而我们抽的是200个,所以在不将batch_size设置为1的情况下,“sgd”其实指的是小批量随机梯度下降。我们可以在之前的乳腺癌数据集试用这两个参数,来观察mini-batch SGD让神经网络运算更快的效果。
#看看乳腺癌数据集的结构,注意到这是一个非常小的数据集
X_.shape
#建立神经网络
dnn = DNN(hidden_layer_sizes=(20,)
,activation="relu"
,batch_size=20 #批量大小N_B
,shuffle=True #是否随机选取
,max_iter=500 #从参数的样子来看是最大迭代次数的意思,和逻辑回归中
相似,暂时不用去管
,random_state=420)
dnn = dnn.fit(X_,y)
#接口score
dnn.score(X_,y)
#重要属性n_iter_
#注意,只有训练完毕的模型才存在该属性,使用交叉验证时无法获取模型的实际迭代次数
dnn.n_iter_
import datetime
#试试看不同batch_size下的测试分数结果
for size in [450,200,50]:
dnn = DNN(hidden_layer_sizes=(20,)
,activation="relu"
,batch_size=size
,max_iter=500
,random_state=420)
times = time()
cvresult = cv(dnn,X_,y,cv=5) #交叉验证
usedtime = time() - times
acc, var = cvresult.mean(), cvresult.var()
print("batch_size:",size)
print("\t
time:",datetime.datetime.fromtimestamp(usedtime).strftime("%M:%S:
%f"))
print("\t cv value:",acc, var) #返回交叉验证的均值和方差
print("")
在这里第一个batch_size为什么设置为450呢?这其实是在模拟普通的梯度下降。乳腺癌数据集总共有569条样本,当使用5折交叉验证后训练集约455条样本,一次性使用几乎全部的数据,几乎就是普通梯度下降了。可以看到,运行得出的结果很有趣,比如batch_size=450时运行时间最短,准确率相当高,而且交叉验证的方差最小,反而是batch_size=50时的运行时间最长,准确率最低,batch_size=200时模型准确率最高,但相对的交叉验证的方差最大。准确率的实际情况也许和梯度下降最初随机选择的系数等很多因素有关,不过就运行时间来看,现在的确出现了之前说明过的,当数据集很小的时候,sklearn中的神经网络反而表现出batch_size越小计算速度越慢的情况。可见,不是所有的数据都适用于mini-batch SGD(这是可想而知的),mini-batch SGD不会在任何时候都表现出提升运算速度的能力。当我们更换成更大的数据集,就更容易得出“小批量计算速度更快迭代时间更短”结论了:
import pandas as pd
#导入神经网络入门数据MNIST
#这是手写数字数据集,我们以前经常使用的sklearn中的load_digits就是从MNIST中
提取出来的1700多条数据
data = pd.read_csv(r"C:\work\micro-class\sklearn\week 12 神经网络
\mnist-in-csv\mnist_train.csv")
data.shape
data.head()
datay = data.iloc[:,0]
datax = data.iloc[:,1:]
from sklearn.preprocessing import StandardScaler as SS
datax_ = SS().fit_transform(datax)
#========【TIME WARNING: 20+mins】========#
#在该数据集的基础上,建立不同batch_size的神经网络
for size in [60000,10000,5000,1000,200]:
dnn = DNN(hidden_layer_sizes=(20,)
,activation="relu"
,batch_size=size
,max_iter=3000
,random_state=420)
times = time()
model = dnn.fit(datax_,datay) #为了更快的计算速度而不使用交叉验证
result = model.score(datax_,datay)
print("batch_size:",size)
print("\t time:",datetime.datetime.fromtimestamp(time() -
times).strftime("%M:%S:%f")) #运行时间
print("\t acc:",result) #运行结果
print("\t actual iter:",model.n_iter_) #在这里由于没有使用交叉验
证,可以查看模型实际的迭代次数
#迭代次数*每次迭代时使用的数据,就可以看到真实的“算法遍历了多少数据”
print("\t actual seen data amount:", model.n_iter_ * size)
print("")
由于代码运行时间过长,在这里我将结果展示出来:
可以看得出,当batch_size=60000时,就是在模拟普通的梯度下降,此时的运算速度是最慢的。随着batch_size逐渐减小,算法所花的时间逐渐变短,并且算法的效果逐渐提升。当batch_size从1000转到200的时候,算法运算的时间又变长了,而精度提高到了99.97%。这里算法的运算时间会变长,也是因为对于60000条样本的数据来说,处理200条样本的批次反而更加费力。而这里的精度看起来明显比之前的乳腺癌数据集高,这并不是因为神经网络更擅长MNIST数据集,而是因为我们在MNIST数据集计算时没有使用交叉验证(时间过长不利于展示)。如果使用交叉验证,可以发现神经网络对MNIST数据集的学习是过拟合的,实际上神经网络在两个数据集上的表现是差不多的。
思考:batch_size与实际迭代次数的关系?
当数据量足够大的时候,出现了batch_size越小,实际迭代次数也越小的趋势。目前没有有说服力的理论能够解释这个现象。你怎么看待这个现象呢?是巧合还是确有其事?为什么数据量小时这个趋势没有表现出来?有条件的小伙伴请使用深度学习框架进行你的研究,然后把结果分享给大家吧。
4.2 神经网络的学习流程
作为用优化算法追求损失函数最小值的模型,神经网络也需要执行和普通梯度下降一致的学习流程,如定义损失函数、计算梯度表达式、迭代梯度等。由于普通梯度下降与mini-batch SGD在迭代时使用的数据不同,两者在数学细节与sklearn实现上都有众多差异。接下来,让我们以仅有两个权重,一个偏差的数据为例,来看看两者在具体学习过程中的异同。
注意,仅有两个权重的神经网络的结构是这样的:
在这里我们简化了多层神经网络的嵌套过程,实际上是因为神经网络每次迭代时是一次性求解出所有层上的所有权重,而权重之间的迭代不会互相影响(上一次迭代的结果会对本次迭代的结果造成影响,但本次迭代的时候所有权重是独立迭代)。因此求一层和求多层在“迭代”这一点来看其实是一样的。
for size in [450,200,50]:
dnn = DNN(hidden_layer_sizes=(20,)
,activation="relu"
,batch_size=size
,max_iter=500
,random_state=420)
times = time()
cvresult = cv(dnn,X_,y,cv=5) #交叉验证
usedtime = time() - times
acc, var = cvresult.mean(), cvresult.var()
print("batch_size:",size)
print("\t
time:",datetime.datetime.fromtimestamp(usedtime).strftime("%M:%S:
%f"))
print("\t cv value:",acc, var) #返回交叉验证的均值和方差
print("\t 完成一个epoch所需要的迭代次数:",X_.shape[0]*0.8/size)
print("")
for size in [450,200,50]:
dnn = DNN(hidden_layer_sizes=(20,)
,activation="relu"
,batch_size=size
,max_iter=200
,random_state=420)
times = time()
dnn = dnn.fit(X_,y) #为了调用真实的n_iter所以不使用交叉验证
usedtime = time() - times
print("batch_size:",size)
print("\t
time:",datetime.datetime.fromtimestamp(usedtime).strftime("%M:%S:
%f"))
print("\t result:",dnn.score(X_,y)) #返回结果
print("\t actual iter:",dnn.n_iter_) #调用实际的迭代次数
print("\t 完成一个epoch所需要的迭代次数:",X_.shape[0]*0.8/size)
print("")
我们会发现,无论我们怎样调整batch_size,实际迭代的次数上一直返回200,这说明max_iter的确是限制了优化算法的实际迭代次数(梯度的步数)。当然,epoch的数量也会因此受到影响,因为batch_size*n_iteration决定了模型究竟可以使用多少数据,也就间接决定了epoch的数量。所以这句说明应该算是sklearn官方不太严谨的地方,正确的说法应该是:max_iter)决定了实际迭代次数的上限,而实际迭代次数会与batch_size共同决定epoch的数量。
for t in [1,2,5,10,20]: #t是迭代次数
eta0, power_t = 0.1,0.2 #可以试着更换一下原始的eta0和设置的衰减用指数
eta = eta0/np.power(t,power_t)
print("iter:",t,"eta:",eta)
for eta_strategy in ['constant','invscaling','adaptive']:
dnn = DNN(hidden_layer_sizes=(20,)
,activation="relu"
,solver="sgd"
,learning_rate_init = 0.5 #初始学习率
,learning_rate = eta_strategy #学习率的变化策略
,power_t = 0.1 #衰减指数
,batch_size=200
,max_iter=3000
,random_state=420)
times = time()
cvresult = cv(dnn,X_,y,cv=5)
usedtime = time() - times
acc, var = cvresult.mean(), cvresult.var()
print("eta_strategy:",eta_strategy)
print("\t
time:",datetime.datetime.fromtimestamp(usedtime).strftime("%M:%S:
%f"))
print("\t cv value:",acc, var)
print("")
#完成一次epoch所需要的迭代次数
iter_ = X_.shape[0]*0.8/200
print(iter_)
dnn = DNN(hidden_layer_sizes=(20,)
,activation="relu"
,solver="sgd"
,learning_rate_init = 0.5 #初始学习率
,learning_rate = "adaptive" #学习率的变化策略
,n_iter_no_change = 5 #设定允许的最大无效epochs为5
,verbose = True
,batch_size=200
,max_iter=3000
,random_state=420)
times = time()
usedtime = time() - times
dnn = dnn.fit(X_,y)
print("eta_strategy:adaptive")
print("\t
time:",datetime.datetime.fromtimestamp(usedtime).strftime("%M:%S:
%f"))
print("\t score:",dnn.score(X_,y))
#试试看将允许的最大无效epochs定义为1,会发生什么?
#如果定义为2呢?
dnn = DNN(hidden_layer_sizes=(20,)
,activation="relu"
,solver="sgd"
,learning_rate_init = 0.5 #初始学习率
,learning_rate = "invscaling" #学习率的变化策略
,power_t = 0.1 #衰减指数
,batch_size=200
,max_iter=3000
,random_state=420).fit(X_,y)
#查看最终生成的系数w
dnn.coefs_
#怎么看w的结构?
type(dnn.coefs_)
for item in dnn.coefs_:
print(item.shape)
#结果有怎样的含义?含有一层隐藏层的神经网络,会有两层连接:输入层-隐藏层,隐藏
层-输出层,输入层对隐藏层中有(输入层神经元个数*隐藏层神经元个数)个系数w,而隐
藏层到输出层有(隐藏层神经元个数 * 输出层神经元个数)个系数w。
dnn.coefs_[0][0] #这就是输入层上的第一个神经元连接到隐藏层上的所有神经元的系
数w
#查看最终生成的b以及b的结构
dnn.intercepts_
for item in dnn.intercepts_:
print(item.shape)
#查看现在的损失函数
dnn.loss_
究竟迭代到什么状况才算是迭代完毕,才会停止迭代呢?下一节我们来看让算法停止迭代的方法。
4.2.6 收敛、停止神经网络的迭代
优化算法以寻找损失函数的全局最小值作为目的,理想状态下,当算法找到了全局最优时神经网络就“收敛”了,迭代就会停止。然而遗憾的是,我们并不知道真正的全局最小值是多少,所以无法判断算法是否真正找到了全局最小值(许多人甚至相信,真正的全局最小值是人类不可获得的“暗知识”,只有算法才知道真相)。其次,一种经常发生的情况可可能是,算法真实能够获取的最小值为0.5,且优化算法可能在很短的时间内就锁定了(0.500001, 0.49999)之间的范围,但由于学习率等超参数的设置问
题,始终无法到达最小值0.5。这两种情况下优化算法都会持续(无效地)迭代下去,因此我们会需要人为来停止神经网络。我们只会在两种情况下停止神经网络的迭代:
1. 神经网络已经达到了足够好的效果(非常接近收敛状态),持续迭代下去不会有助于算法效果
2. 神经网络的训练时间太长了,即便我们知道它还没有找到最优结果
这两种情况中的第二种非常容易理解,就是我们设置参数max_iter来控制迭代次数。就像我们在之前的章节尝试的那样,限制max_iter为某个整数,当迭代次数达到max_iter的时候迭代就会停止。在数据量巨大的时候我们常使用这样的参数来避免陷入几天几夜的训练流程。
第一种情况就比较复杂了。首先,我们必须先定义什么是”足够好的效果”。就像我们之前提过多次的,神经网络通过降低损失函数上的值来求解参数 和 ,所以只要损失函数的值持续减小,我们就认为神经网络的效果还有提升的空间。当这种提升空间、即连续n_iter_no_change个epochs的损失函数的减小量都非常小的时候,我们就可以认为神经网络已经“收敛”,就可以将神经网络停下。除了损失函数的减小量之外,还有另一种验证“效果足够好”的方法。损失函数反映的是算法在训练集上的拟合效果,但我们在学习时真正追求的其实是算法的“泛化能力”,而在训练集上拟合效果优秀(损失函数的值非常小)不能代表模型的泛化能力强。所以,为了验证模型的泛化能力,我们需要在数据集中留出一部分验证集,当模型完成对训练集的学习,我们可以用验证集来验证模型的效果,并且用验证集上效果的提升来衡量神经网络是否已经“足够好”。early_stopping就是使用这种思路的方法。
Comments NOTHING