20180730:pyimageresearch的DeepLearning for ComputerVision电子书阅读笔记。
封面9. Chapter 9: Optimization Methods and Regularization 优化方法和调整
开篇有了前面章节的学习,我们对参数化学习有了直观的理解。并且了解了打分函数如何把输入的图像数据映射为标签和对应分数的过程。
打分函数定义了两个参数,分别是权重矩阵W和偏移向量b。我们的打分函数使用这两个参数,并以输入的图像数据x_i作为输入,输出不同标签和对应的分数。
我们还学习了两种应用普遍的损失函数:Multi-class SVM loss
多类SVM损失和cross-entropy loss
交叉熵损失。损失函数,从最基础的角度来看,它的作用是判断我们打分函数输出结果的“好”与“坏”。
有了这些基础,我们可以把学习重点转移到机器学习,神经网络,深度学习——优化。优化算法是神经网络从数据中学习模型的原始驱动力。通过前面的讨论,我们知道为了得到一个高精度的分类器,权重矩阵W和偏移向量b是其中的决定因素。
但是我们如何来确定合适的权重矩阵W和偏移向量b?我们是否可以随机的初始化它们的值,然后不断循环评价优化过程,最终拿到一份相对满意的参数呢?讲道理是可以的,但是需要考虑的问题是,数据集可能达到百万量级,那么随机数值作为初始值可能会耗费大量的时间来纠正。
为了代替这种通过随机取值获得权重矩阵W和偏移向量b初始值的办法,我们需要定义一个可以让我们逐字去优化权重矩阵W和偏移向量b的optimization algorithm
优化算法。这一章,我们会学习一些常用的优化算法去优化我们的神经网络和深度学习模型——gradient descent
梯度下降法。梯度下降法包含很多的参数,但是在我们的运算过程中,目的都是一致的:为了通过迭代的方式去评估我们的参数,计算损失,然后逐步用逼近的方式减少我们的损失。
9.1 Gradient Descent 梯度下降
梯度下降算法有两个特点:
- 1.一个标准的实现方式
- 2.一个被应用更广泛的计算方式
这里作者说的不是很清楚,用了“气味”来形容这两种方法,可以理解为梯度下降算法有两种实现。一种是标准的实现方式,但是不常用;一种是改良的实现方式,更常用。
这里我们首先从标准实现来学习梯度下降算法。当我们理解了标准的梯度下降算法之后,我们再去学习更常用的改良版本的梯度下降算法。
9.1.1 The Loss Landscape and Optimization Surface 损失表面和优化表面
梯度下降算法是一种iterative optimization algorithm
迭代优化算法,它通过loss landscape
损失表面来实现优化功能。我们可以通过下图来解释标准形式的梯度下降算法是如何进行优化的:
上面图像中是映射在2D坐标系中的结果,实际情况下,我们面对的是一个三维面。这里为了方便理解,只抽取了其中的一个2D切面。
我们可以看到,我们的损失折线(这里作者使用了landscap
,但是这里讨论的是从某个垂直角度竖切形成的2D切面,所以这里叫折线,实际上还是三维的面)由于参数的不同会形成很多波峰和波谷。每个波峰是局部最大值,这个最大值表示了就近的最大损失值。全局的波峰是表示了整个参数范围内最大的损失函数。那么近似的,也就有了局部最小损失和全局最小损失。理想情况是,我们要找到全局最小损失值对应的参数。从而就可以得到最优性能的分类器。
那么这时候有个问题,既然我们可以直观的用图像来表示损失值,为什么我们不直接取最小的损失值所对应的参数呢?
这里有个问题,我们的loss landscape
损失表面是不可见的。我们并不知道它是什么样子的。如果我们想象我们自己是一个优化函数,我们只能盲目的去求取不同数据的损失值,其实我们在运算之前是不知道损失表面的样子。那么我们需要做的就是尽量避免损失值逐渐爬升到最大值。
个人的角度,我不会采用上图中的可视化方式来表示损失表面——它丢失的太多信息,显得有些单薄。并且它还会误导读者我们找到的最小损失值不是局部最小就是全局最小。这种情况并不是我们面对问题的实际情况,接下来我们会在后面的章节中逐步介绍这个过程。
9.1.2 The "Gradient" in Gradient Descent 梯度下降中的梯度
为了直观的解释梯度下降,假设我们有个小机器人叫Chad,当我们运行梯度下降的时候,我们随机的把Chad放置在我们的损失平面上,这里假设我们的损失平面是一个碗(为了直观好理解)。
简单理解梯度下降图示那么现在来看,Chad的任务就是尽量的去逼近“碗”的底部,也就是最小损失。是不是看起来很容易呢?所有的Chad小机器人需要做的就是面向下降的方向,然后让新落下的Chad尽量的逼近最小损失。
但是这里有个问题:Chad并不足够聪明。可以认为Chad只含有一个传感器,这个传感器可以使用权重矩阵W和偏移向量b这两个参数去求得损失函数的值L。因此,Chad可以计算它在损失平面的相对位置,但是它并不具备感知哪个方向应该是下一次计算应该朝向的。
那么Chad到底做了什么?答案是:应用梯度下降。所有的Chad需要遵循偏移向量W的梯度。我们可以通过下面的公式来计算梯度:
\frac{df(x)}{dx} = lim_{h\rightarrow0}\frac{f(x + h) - f(x)}{h}
当ln > 1维度的时候,我们的梯度变成一个vector of partial derivatives
(直接翻译为偏导数向量,不知道准不准确)。那么这个时候,上面的公式存在两个问题:
- 1.他是对梯度的接近
- 2.运算速度非常慢
实际使用中,我们使用analytic gradient
解析梯度来代替,解析梯度的运算速度更快,但是难点在于如何实现偏导数和多变量计算。多变量的全倒数超出了本电子书的范围,不做讨论。如果有兴趣学习更多关于解析梯度的内容,作者建议学习:Zibulevsky, Andrew Ng's cs229 marchine learning notes and cs231 notes
这里需要了解矩阵的偏导数的内容,而且还不太清楚如何应用下降梯度法来优化W和b的值。关于上面图的理解也还不足,如果放在三维空间XYZ中,假设纵坐标Z是
loss
,那么X和Y是如何看作W和b的不同取值的,毕竟W是一个高纬矩阵,b是一个三维向量(假设还是用cats, dogs, pandas
数据集来考虑)。从数值计算的角度,不好理解。。。如果看最开始的2D图像,横坐标是W,而且W是个矩阵,可否先理解XY平面就是W的直接映射,但是这里没有说如何优化偏移向量b。继续阅读本章看如何优化。
关于这里我们讨论的目的,只需要了解下降梯度的大体概念:它尝试降低损失,提高分类准确度,从而优化我们的参数。优化参数的最终目的是为了最小化损失。
9.1.3 Treat It Like a Convex Problem(Even if it's Not) 把它当作一个凸问题
如果我们尝试用上面图中碗的概念来理解我们的损失平面,我们可以近似的把损失平面当作一个凸问题来看待,虽然实际上来看它并不是。如果一个函数F是一个凸函数,那么它的局部最小就是它的全局最小。这样理解看起来完美的契合了上面图中“碗”的例子。我们的优化算法就是取到“碗”底部对应的W。
那么问题来了,实际上我们的神经网络和深度学习问题都不是纯粹的凸问题。也就是说我们会发现很多的波峰波谷。
既然是一个非凸问题,为什么我们还要使用梯度下降算法?因为,虽然不是凸问题,但是梯度下降已经可以取得很好的效果。
梯度算法已经具备足够的能力去让我们完成优化9.1.4 The Bias Trick 偏差
在我们真正实现梯度下降算法之前,我们来花一些时间看一下Bias Trick
(偏差技巧),这个技巧可以把我们的权重矩阵W和偏差向量b合并为一个参数。回忆一下我们定义的打分函数:
f(x_i, W, b) = Wx_i + b
同时跟踪两个变量的变化是有些冗余的,从解释和实现的角度来看,都应该尽量避免这种情况。为了合并权重矩阵W和偏差向量b为一个参数,我们给输入数据X多增加一个维度,然后在新的W_{new}的最右侧增加一列用来承载原来的偏移向量b。
那么我们的新的打分函数形式如下:
f(x_i, W_{new}) = W_{new}x_i
用我们之前的cats, dogs, pandas
数据集来解释这个过程,我们的输入图像是32 X 32的,也就是x_i的维度是[3072 X 1],增加一个维度之后(收尾增加都可以,只是影响我们的计算过程中提取b的位置)变成了[3073 X 1],新增加的维度值为1
。对于我们的W也增加一个维度,从原来的[3 X 3072]变成了W_{new}的[3 X 3073]。这样我们把偏差也变成了一个可以通过梯度下降来求取的值。示意图如下:
9.1.5 Pseudocode for Gradient Descent 梯度下降的伪代码例子
下面是vanilla gradient descent algorithm
标准梯度下降算法的python伪代码,见cs231的第83页。
伪代码第一行:表示我们通过一定的条件来遍历数据集中的所有图像数据。这个条件包括:
- 我们已经遍历了数据集中所有图像数据N次
- 我们的损失已经下降的非常缓慢,或者我们的准确度已经足够高
- 损失没有继续下降经过了M次数据集的遍历
伪代码第二行:我们调用一个名为evaluate_gradient
的函数。这个函数包含了三个参数,分别为:
- 损失:函数通过当前的W值来计算输入的图像的损失
- 数据:我们的当前用于计算的图像数据或者是图像的特征向量
- 权重矩阵:我们当前需要优化的权重矩阵W_{curr}。我们的目标是找到损失最小的W
其中evaluate_gradient
返回的是一个维度为K的矩阵,其中K的维度是输入图像数据或者特征向量的维度。Wgradient
表示了当前的梯度,也就是我们拿到了每个数据的梯度。
这里不太明白,每个数据的梯度是几个意思,
Wgradient
是一个矩阵,它表示了梯度,是否可以理解为矩阵中的每个值是当前输入数据的损失的梯度???这里可能涉及了矩阵偏导数的内容,之后查看后补充
伪代码第三行:我们应用梯度下降算法。我们通过一个参数\alpha来控制我们学习的步长。
在实际训练过程中,你可能需要花费大量的时间寻找一个合适的\alpha值。到目前为止,\alpha是我们整个训练过程最重要的参数。
如果\alpha太大,甚至只是在损失面周围取值,甚至都不会遇到下降的过程。
如果\alpha太小,那么达到足够下降值需要非常长的计算过程。
如果能找到一个合适的\alpha值,将会很容易设置训练的整体次数。
9.1.6 Implementing Basic Gradient Descent in Python 基本梯度下降算法的python实现
现在我们知道了梯度下降的基础,我们来使用python实现一个数据分类器。创建一个名为gradient_descent.py
的文件,完整代码如下:
# USAGE
# python gradient_descent.py
# import the necessary packages
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report
from sklearn.datasets import make_blobs
import matplotlib.pyplot as plt
import numpy as np
import argparse
def sigmoid_activation(x):
# compute the sigmoid activation value for a given input
return 1.0 / (1 + np.exp(-x))
def predict(X, W):
# take the dot product between our features and weight matrix
preds = sigmoid_activation(X.dot(W))
# apply a step function to threshold the outputs to binary
# class labels
preds[preds <= 0.5] = 0
preds[preds > 0] = 1
# return the predictions
return preds
# construct the argument parse and parse the arguments
ap = argparse.ArgumentParser()
ap.add_argument("-e", "--epochs", type=float, default=100,
help="# of epochs")
ap.add_argument("-a", "--alpha", type=float, default=0.01,
help="learning rate")
args = vars(ap.parse_args())
# generate a 2-class classification problem with 1,000 data points,
# where each data point is a 2D feature vector
(X, y) = make_blobs(n_samples=1000, n_features=2, centers=2,
cluster_std=1.5, random_state=1)
y = y.reshape((y.shape[0], 1))
# insert a column of 1's as the last entry in the feature
# matrix -- this little trick allows us to treat the bias
# as a trainable parameter within the weight matrix
X = np.c_[X, np.ones((X.shape[0]))]
# partition the data into training and testing splits using 50% of
# the data for training and the remaining 50% for testing
(trainX, testX, trainY, testY) = train_test_split(X, y,
test_size=0.5, random_state=42)
# initialize our weight matrix and list of losses
print("[INFO] training...")
W = np.random.randn(X.shape[1], 1)
losses = []
# loop over the desired number of epochs
for epoch in np.arange(0, args["epochs"]):
# take the dot product between our features `X` and the weight
# matrix `W`, then pass this value through our sigmoid activation
# function, thereby giving us our predictions on the dataset
preds = sigmoid_activation(trainX.dot(W))
# now that we have our predictions, we need to determine the
# `error`, which is the difference between our predictions and
# the true values
error = preds - trainY
loss = np.sum(error ** 2)
losses.append(loss)
# the gradient descent update is the dot product between our
# features and the error of the predictions
gradient = trainX.T.dot(error)
# in the update stage, all we need to do is "nudge" the weight
# matrix in the negative direction of the gradient (hence the
# term "gradient descent" by taking a small step towards a set
# of "more optimal" parameters
W += -args["alpha"] * gradient
# check to see if an update should be displayed
if epoch == 0 or (epoch + 1) % 5 == 0:
print("[INFO] epoch={}, loss={:.7f}".format(int(epoch + 1),
loss))
# evaluate our model
print("[INFO] evaluating...")
preds = predict(testX, W)
print(classification_report(testY, preds))
# plot the (testing) classification data
plt.style.use("ggplot")
plt.figure()
plt.title("Data")
plt.scatter(testX[:, 0], testX[:, 1], marker="o", c=testY.ravel(), s=30)
# need to consider the size of matrix in python 3.5.5 with matplot
# you need to change `c=testY` to `c=testY.ravel()`
# construct a figure that plots the loss over time
plt.style.use("ggplot")
plt.figure()
plt.plot(np.arange(0, args["epochs"]), losses)
plt.title("Training Loss")
plt.xlabel("Epoch #")
plt.ylabel("Loss")
plt.show()
这里需要注意的是运行环境,我的实际运行环境是Anaconda, python 3.5.5, matplotlib-2.2.2
,在使用作者提供的原始gradient_descent.py
运行的时候会提示:
查找StackOverflow发现问题在于第95行代码,使用matplot输出图像的时候,testY
不是一维向量,所以使用T.ravel()
函数降维。
关于T.ravel()
函数的理解,可以参考CSDN,描述如下:
下面逐行分析代码:
程序4 ~ 14行:导入需要的程序包,这里比之前新加入的是matplotlib
,用于绘制可视化图像,类似matlab里的matplot。方便的把数据输出为可视化图形。还有就是make_blobs
函数库,从sklearn.datasets
导入的,我们使用这个函数库来快速生成待分类的数据集,由于是简单的梯度下降分类器,我们首先使用这个数据集来作为入门,不直接使用图像数据集。
之后我们定义了一个名为sigmoid_activation
函数,这个函数接收一个输入数据x
,然后做1.0 / (1 + np.exp(-x))
运算。我们取X \in [-10, 10],步长为0.01,得到的sigmoid
函数图像如下:
我们把这个函数叫做activation function,从图像可以看出当x > 0的时候y > 0.5,我们称作函数处于ON状态。相反的,当x < 0的时候y < 0.5,我们称作函数处于OFF状态。
程序16 ~ 26行:我们把输入数据X和权重矩阵W的运算结果输入sigmoid_activation
函数,得到了我们的预测值。然后我们使用阈值0.5
来过滤我们的preds
。大于0.5,我们认为预测结果为1;小于0.5,我们认为预测结果为0。然后在程序26行返回我们的预测结果preds
。
当然我们可以选择很多其他形式的activation function
,但是我们选区的这种简单形式的激活函数已经可以很好的帮我们去学习神经网络,深度学习,梯度下降优化。我们在第十章会学习其他形式的激活函数。这里只需要简单的把激活函数看作一个非线性的函数用来过滤我们的预测结果。
程序28 ~ 34行:这部分代码用于设置参数,例如在terminal
中调用gradient_descent.py
文件,我们可以使用命令gradient_descent.py -e 100 -a 0.01
来调用python文件,并且给标签为epochs
的参数赋值为100
,给标签为alpha
的参数赋值为0.01
。
这两个参数的含义分别为:
-
epochs
:设置训练梯度下降分类器的循环次数 -
alpha
:这个参数用于设置训练速度,一般我们设置这个参数为0.1
,0.01
,0.001
。但是需要注意的是,这是一个超参数,我们需要根据实际的分类问题进行调整。
程序36 ~ 50行:首先我们定义了一个函数make_blobs
来生成1000个分为个类型的二维点数据。我们可以看作它们是一个长度为2的特征向量。每个数据都含有一个标签,分别是0
和1
。那么我们有个这样的数据,我们的目标就可以设定为:用我们的分类器去准确的预测并分类这1000个数据。
第45行代码使用了我们的处理偏差向量的小技巧,把偏差向量融合到新的权重矩阵,避免了同时追踪两个变量的尴尬。具体做法是给数据集中的每个数据增加一列,设置值为1
。然后给原来的权重矩阵增加一行,增加的内容就是偏差向量。
程序段的最后我们使用函数train_test_split
把生成的1000个数据分为训练数据和测试数据。
程序52 ~ 55行:用随机的方法初始化增益后的W_{new}参数矩阵,里面包含了随机生成的权重矩阵W和偏移向量b。同时还初始化了一个空数组用于存储每次计算的loss。
程序57 ~ 69行:从第58行开始,我们使用循环来迭代运算得到每个数据的预测值。接下来具体分析里面的计算过程。
首先trainX
矩阵的维度是[500, 3]
,因为我们把生成的1000个数据平分为500个训练数据和500个测试数据。然后每个数据包含有随机生成的x,y,还有一个增益之后的1
。所以trainX
的大小是[500, 3]
。
然后在第62行preds = sigmoid_activation(trainX.dot(W))
计算训练数据集和W_{new}的矩阵乘积,得到的preds
尺寸是[500, 1]
,分别表示了500组训练数据经过打分函数之后得到的预测概率。
这里现在还没想明白,trainX.dot(W)
可以看作是打分函数,输出值应该是分数,但是这里却直接把输出结果当作概率来看待?
尝试理解如下,上一章中我们通过打分函数(矩阵乘法运算)得到了每个标签对应的分数值,然后把分数最大所对应的标签作为我们的预测结果。那么这里的情况可以理解为:preds
承载了每个数据经过打分函数所输出的分数。
使用sigmoid_activation()
函数处理得到的分数,得到了预测的数值结果,且概率范围为(0, 1)
,为什么范围是(0, 1)
,可以参考上面的函数图像来看。
或者说这里通过函数sigmoid_activation()
函数得到的不是概率,而是对于输入数据的预测值?也不对。。。之后回头来看这里为什么可以使用这个所谓的激活函数。
看了程序67行之后的理解:由于标签是0
和1
,那么经过激活函数可以把打分函数输出的值映射到[0, 1]
中,也就是说,打分函数输出的结果越大,那么代表这个数据越逼近1
,也就是说这里拿到的preds其实就是概率值。
接下来,程序67行:计算了训练数据经过打分函数处理之后的错误error
,然后对输出的error
做平方运算,得到了loss
损失,然后在69行把这次得到的500个训练数据的损失加入到这次训练的整体损失数组中,方便在最后绘制出loss
曲线。
程序71 ~ 84行:这里作者的示例程序设计的听巧妙,因为标签是0
和1
,trainY
就是数据的标签,大小为[500, 1]
,里面承载了0
和1
,也就是我们的概率直接表示了和预测结果的错误程度。所以在得到了每次预测的error
错误之后,就可以使用梯度下降法来优化我们的W_{new}参数了。
注意,程序中使用的是
error
错误,而不是loss
损失来优化W_{new},这里需要对照上一章来理解为什么这样计算?
程序73行使用当前的训练数据(数据内容是500个二维向量,实际矩阵的尺寸为[500, 3]
,由于有一列全为1
用于承载我们的W_{new}参数),转置之后的尺寸为[3, 500]
,然后和error
错误(尺寸[500, 1]
)来做向量乘法,得到了这次训练的梯度矩阵gradient
,矩阵尺寸为[3, 1]
。
程序79行,这一步是本章内容最重要的运算。这里我们使用步长的负值来迭代我们的W_{new}参数。这个运算可以让我们更接近损失面的底部。
程序81行到84行做一些log输出,方便我们在termianl中查看训练过程。
程序86 ~106行:这里重要的是第88行,前面经过迭代之后,我们拿到了我们的W_{new}参数,88行preds = predict(testX, W)
来测试我们得到的参数(也就是训练结果)是否足够好。并且在程序89行调用函数classification_report()
输出测试结果,如下图:
再接下来的91行到106行是使用matplotlib输出两个图像,一个是loss曲线,一个是数据集分布图像。如下图:
matplotlib图像输出
9.1.7 Simple Gradient Descent Result 简单梯度下降结果
运行我们的gradient_descent.py
脚本,可以在控制台得到下面的输出:
从输出可以看出,我们执行了100次训练,也就是更新了100次W的值。每次都会输出当前W所对应的loss
损失值。
下面的截图是电子书中作者给出的运行截图:
图9.5中,我们可以用以条红色线段来完成数据的分类,并且损失函数从最开始的400左右很快的下降到0.1左右。最终的loss大概在0.04左右。
从最后的评价结果来看,我们的分类器对于0
标签实现了近乎100%的正确分类,但是对于1
标签实现的分类是99%。这里需要注意的是vanilla gradient descent
分类器在更新W的时候,每次运算只更新一次,也就是说,100次训练过程中只更新了100次W。
由于我们使用随机的方法初始化W和b,而且手动设置了训练次数为100和训练步长为0.01,那么很有可能因为训练次数和训练步长的缘故,我们不能对数据做出完整的分类。
对于一般的梯度下降方法,最好设置较大的训练次数和较小的训练步长。下一章我们将会介绍具备局部更新权重矩阵W的梯度下降算法Stochastic Gradient Descent
,SGD可以让我们在一次训练过程中多次更新W的值。
9.2 Stochastic Gradient Descent(SGD) 随机梯度下降
在前面的部分,我们讨论了梯度下降法,一种初级的可以用于在参数化学习中设置分类器权重的优化算法。但是这种初级的梯度下降实现在较大的数据集上会运行的十分缓慢。或者可以称作是浪费计算资源的。
作为替代,我们应该换用Stochastic Gradient Descent随机梯度下降法,一种对于标准梯度下降算法的简单修改,这种修改主要体现在,使用一小部分数据集来更行W的值,而不是每次循环数据集的所有数据才更新一次。虽然这种更新W的方式会带来更多的“噪声更新“,但是它也让我们可以在梯度方向”走“更多步。(一步每个数据部分 和 一步所有数据)。基本上会提升更快的训练速度,并且不会对精度造成损失。
SGD可以被看作是最终要的神经网络训练算法。虽然最初提出SGD已经快要57年了,但它仍旧是从大规模数据中训练神经网络并学习数据模型的关键环节。
未完待续
网友评论