目录
3.5 基于反向传播计算解析梯度(analytic gradient)
1. 前言
本文编译自斯坦福大学的CS231n课程(2022) Module1课程中神经网络部分之一,原课件网页参见:
CS231n Convolutional Neural Networks for Visual Recognition
本文(本系列)不是对原始课件网页内容的完全忠实翻译,只是作为学习笔记的摘要,主要是自我参考,而且也可能夹带一些私货(自己的理解和延申,不保证准确性)。如果想要更准确地了解更具体的细节,还请服用原文。如果本摘要恰巧也对小伙伴们有所参考则纯属无心插柳概不认账^-^。
前面几篇:
CS231n-2022 Module1: 神经网络1:Setting Up the Architecture
CS231n-2022 Module1: 神经网络3:Learning and Evaluation
2. 数据生成
本实验的目的是通过对比来体现深度神经网络(当然本实验中其实只是有一个隐藏层,即2层神经网络)相对于浅层网络(没有隐藏层,比如说线性分类器,可以看作1层神经网络)的优势,所以我们需要一个线性不可分的数据集,线性分类器无法正确分类,而2层神经网络则能够进行正确分类(具体情况取决于分类问题本身的难易度)。
一般来说我们需要对数据做归一化预处理(比如说,使得数据集的每个feature都变成零均值、单位标准偏差),但是以上玩具数据集的范围已经在[-1, 1]范围内,所以我们可以跳过这一步。
在scikit-learn库中给出了一些常用(玩具)数据集的获取或者生成方法,有兴趣者可以参考:
机器学习笔记:常用数据集之scikit-learn生成分类和聚类数据集
3. 训练一个Softmax线性分类器
正如把大象装到冰箱里需要三步一样,训练一个机器学习模型也由基本固定的套路,以下分步骤说明。对应代码参考最后的3.7中的代码中对应章节号部分。
3.1 参数初始化
本Softmax分类器的参数包括权重参数W和偏置参数b。
W通常用零均值、小方差的高斯分布进行初始化,而b则初始化为全0即可。
3.2 计算分类分数(class scores)
张量下标中的“[]”用于表示张量的维度,以下同。
3.3 计算分类概率
基于softmax函数将scores变换成对应于各class#k的概率:
3.4 计算交叉熵损失函数
Softmax分类器使用的损失函数为交叉熵损失函数(softmax loss, cross-entropy loss)。给定一个分布P和一个分布Q,交叉熵定义为([1]):
其中,P(x)代表在分布P中x发生的概率,Q(x)代表在分布Q中x发生的概率。
以P代表真值的分布,显然应该是一个one-hot的分布。比如说在本例中有三种分类{0,1,2},假设样本xi的真值yi = 1,则应该有。
以Q代表预测值的分布,即Q = [probs[0],probs[1],probs[2]].
则显而易见的是,样本i {xi, yi}的损失应该为:
而整个数据集的总(平均)损失自然应该是:
进一步,还要加上正则化损失(这里考虑正则化损失)得到:
其中,正则化损失的系数(1/2)仅仅是为了数学推导的方便(平方项求导后会产生2的因子,两者抵消会显得梯度的解析式更清爽一些)。
正则化系数作为模型的一个超参数进行调节。
如上一篇所述,作为梯度检查的一项,在正则化系数置0,随机初始化后,loss的初始值应该为(注意,本文中log均指自然对数,即ln())。可以据此判断loss的实现是否存在明显的错误。
3.5 基于反向传播计算解析梯度(analytic gradient)
上面我们已经得到了损失函数,神经网络训练的目的就是要使损失最小化。我们将采用梯度下降(gradient descent)方法。基本思路是,从随机参数出发,计算损失函数关于这些参数的梯度,并由此确定向哪个方向以多大步长进行参数调节。
为了简洁起见,重写一下分类概率和样本损失(scores --> f, probs --> p, []-->下标),并推导(单个样本的损失
关于分类k的分数
的)偏导数如下(其中关键是求导的链式法则。偏导数是构成梯度的元素):
注意,以上推导是针对 的,所以,其中f应该视为关于样本{i}的。所以,
应该写作
(即样本i的识别为分类k的分数),
应该写做
,
应该写成
。。。
其中,I(x)表示Indicator function,也有写做1(x)的。softmax之所以令人喜欢就在于它所导致的梯度是如此简洁优雅(其根源又在于指数函数的魔力)。下面通过一个简单的例子来增进直观的理解。假设针对样本通过计算得到
p = [0.2, 0.3, 0.5]
, 并且假定正确的分类是k=1(中间那个,其概率为0.3)。根据以上梯度公式可以得到(这里为简洁起见,以df表示梯度向量)
df = [0.2, -0.7, 0.5]。由于正确的分类是k=1,因此如果增大p[0]或者p[2],应该会导致loss变大,df[0]和df[2]大于0正好与此相符,
同理如果增大p[1]则应该会导致loss变小,这对应着df[1]=-0.7<0。
有了后,就可以进一步基于反向传播、链式法则,求得loss关于W、b的梯度。以下仅给出loss对于某个权重参数
的偏微分作为示例(由此扩充至
以及
是一个顺理成章水到渠成的过程):
以,
分别代表整个数据集的
scores = np.dot(X, W) + b
和概率矩阵,则基于以上推导经过一些矩阵微积分(matrix calculus)运算可以得到:
同理可以得到总体损失关于向量b的梯度(但是其表达式稍微有点难写)。
由此可以得到以下梯度计算的实现代码。注意梯度计算的实现代码中的对应关系:probs--; dscores--
(加转置是为了维度匹配),dW和db分别表示loss关于W和b的梯度(最后还加上正则化损失部分的梯度):
dscores = probs
dscores[range(num_examples),y] -= 1
dscores /= num_examples
dW = np.dot(X.T, dscores)
db = np.sum(dscores, axis=0, keepdims=True)
dW += reg*W # don't forget the regularization gradient
注意正则化梯度( regularization gradient)的形式非常简单 reg*W
,这是因为,正如前面我们提到过,正则化损失中有个1/2的因子恰好被平方项的导数产生的因子2抵消了.
3.6 参数更新
有了梯度和
和另一个超参数
(step_size),参数的更新就直截了当了:
3.7 组装到一起进行训练
3.7.1 处理框图
将以上所有要素组装到一起,就得到了一个Softmax分类器,处理框图示意如下:
由以上框图可以看出, loss函数本身其实只是用于训练进度状况监测用的。训练(学习)本身并不要求显式地计算loss,而是跳过loss直接计算梯度了。
3.7.2 代码
基本上是原课件代码(网页内嵌代码以及minimal_net.ipynb),稍微有一些调整和修改(比如说python2-->python3的相关修改)、注释,在JupyterNotebook中运行验证过。
# A bit of setup
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
plt.rcParams['figure.figsize'] = (10.0, 8.0) # set default size of plots
plt.rcParams['image.interpolation'] = 'nearest'
plt.rcParams['image.cmap'] = 'gray'
# for auto-reloading extenrnal modules
# see http://stackoverflow.com/questions/1907993/autoreload-of-modules-in-ipython
%load_ext autoreload
%autoreload 2
# 2.数据生成
M = 100 # number of points per class。原示例中用N,容易混淆。通常用N表示数据集总的样本数。
D = 2 # dimensionality
K = 3 # number of classes
N = M*K # 总样本数
X = np.zeros((N,D)) # data matrix (each row = single example)
y = np.zeros(N, dtype='uint8') # class labels
for j in range(K):
ix = range(M*j,M*(j+1))
r = np.linspace(0.0,1,M) # radius
t = np.linspace(j*4,(j+1)*4,M) + np.random.randn(M)*0.2 # theta
X[ix] = np.c_[r*np.sin(t), r*np.cos(t)]
y[ix] = j
# lets visualize the data:
plt.scatter(X[:, 0], X[:, 1], c=y, s=40, cmap=plt.cm.Spectral)
plt.show()
# 3.0 some hyperparameters
step_size = 1e-0
reg = 1e-3 # regularization strength
# 3.1 initialize parameters randomly
W = 0.01 * np.random.randn(D,K)
b = np.zeros((1,K))
# gradient descent loop
num_examples = X.shape[0]
for i in range(200): # Each iteration corresponding to one epoch.
# 3.2 evaluate class scores, [N x K]
scores = np.dot(X, W) + b
# 3.3 compute the class probabilities
exp_scores = np.exp(scores)
probs = exp_scores / np.sum(exp_scores, axis=1, keepdims=True) # [N x K]
# 3.4 compute the loss: average cross-entropy loss and regularization
correct_logprobs = -np.log(probs[range(num_examples),y])
data_loss = np.sum(correct_logprobs)/num_examples
reg_loss = 0.5*reg*np.sum(W*W)
loss = data_loss + reg_loss
if i % 10 == 0:
print("iteration %d: loss %f" % (i, loss))
# 3.5 compute the gradient on scores
dscores = probs
dscores[range(num_examples),y] -= 1
dscores /= num_examples
# backpropate the gradient to the parameters (W,b)
dW = np.dot(X.T, dscores)
db = np.sum(dscores, axis=0, keepdims=True)
dW += reg*W # regularization gradient
# perform a parameter update
W += -step_size * dW
b += -step_size * db
# evaluate training set accuracy
scores = np.dot(X, W) + b
predicted_class = np.argmax(scores, axis=1)
print ('training accuracy: %.2f' % (np.mean(predicted_class == y)))
运行结果如下:
。。。
iteration 170: loss 0.785329 iteration 180: loss 0.785282 iteration 190: loss 0.785249training accuracy: 0.52
50%的准确度当然不能说好,但是考虑到数据集本来是非线性可分的,现在强行用一个线性分类器来分类,结果不好也是意料之内的事情。为了更直观地看到训练效果,用以下代码将所训练好的分类器的分类边界画出来,如下所示:
# plot the resulting classifier
h = 0.02
x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1
y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1
xx, yy = np.meshgrid(np.arange(x_min, x_max, h),
np.arange(y_min, y_max, h))
Z = np.dot(np.c_[xx.ravel(), yy.ravel()], W) + b
Z = np.argmax(Z, axis=1)
Z = Z.reshape(xx.shape)
fig = plt.figure()
plt.contourf(xx, yy, Z, cmap=plt.cm.Spectral, alpha=0.8)
plt.scatter(X[:, 0], X[:, 1], c=y, s=40, cmap=plt.cm.Spectral)
plt.xlim(xx.min(), xx.max())
plt.ylim(yy.min(), yy.max())
#fig.savefig('spiral_linear.png')
如上图所示,分类判决边界为直线,这正是线性分类器的特征。
下一节我们来针对以上模型追加一级隐藏层,使其从浅层(线性)网络变成深层(非线性) 网络,见识一下由于非线性带来的深度神经网络的威力。
4. 训练一个神经网络
在上一章所述的softmax classifier的基础上追加一层隐藏层(后面我们将会看到,对于以上这个toy dataset,一层隐藏层足以获得充分好的分类性能)。以下各节分别描述追加一层隐藏层所带来的变化。
4.1 参数以及前向计算
追加一层隐藏层后,将有两个参数层(隐藏层,输出层),我们其参数分别记为(我们用上标来标识它所对应的层序号),前向计算过程变为如下所示:
其中,表示layer#(l+1)的输入,而
即是原始输入数据。
表示隐藏层的激励函数,最常用的是ReLU,
则表示layer#(l)的输出的score(注意,前文用f表示score。感觉不妥,因为f经常用于表示函数,所以,这里改用s)。
4.2 loss computation
因为损失函数是基于输出层输出的scores进行计算(softmax, and then softmax loss, or cross-entropy loss,通常我们并不把这个softmax看作是输出层的激励函数),追加隐藏层对于loss的计算没有任何影响!
4.3 梯度计算
如前所述,梯度计算是基于链式法则、以反向传播的方式进行的。
4.3.1 dscores计算
首先是计算loss关于输出层的输出scores(即现在的)的梯度,即
,其中
代表(输出层的)scores,而该梯度在代码中仍用dscores表示。
好吧,显而易见的是,由于scores的计算与前面有多少隐藏层没有关系,所以dscores的计算不受追加一层隐藏层的影响,没有任何变化!
4.3.2 dW2, db2计算
W2和b2是输出层的权重参数,同样,loss关于它们的梯度的计算不受追加的隐藏层的影响(想一想,反向传播的方向,在传播路径上居于前面的不会受到后面的影响。在反向传播路径上,输出层在新追加的隐藏层的前面)。
当然,由于dW2和db2的解析式中包含上一层输出的score。在上一章的Softmax分类其中,输出层的上一层(即是输入层)的输出数据就是原始的X,即现在的;而现在变成的隐藏层的输出即
。
所以dW2和db2的计算代码的修改只需要:(1)名字的改变(dW->dW2,db->db2);(2)用(在以下代码中用hidden_layer表示)替换原来的X。
4.3.3 dhidden
与dW2、db2的计算需要先计算dscores一样,dW1、db1的计算同样需要先计算loss关于layer#1的输出的梯度,即
,它可以计算如下(公式推导待补充):
dhidden = np.dot(dscores, W2.T)
# backprop the ReLU non-linearity
dhidden[hidden_layer <= 0] = 0
以上第2行代码是因为,ReLU在输入小于0时直接输出0了,所以在输出为0时其梯度自然就是0了。所以在反向传播中ReLU其实就可以看成是一个开关!
4.3.4 dW1, db1计算
有了dhidden,dW1和db1就可以计算如下了(公式推导待追加):
# finally into W,b
dW = np.dot(X.T, dhidden)
db = np.sum(dhidden, axis=0, keepdims=True)
4.4 参数更新
有了以上各项梯度gradients dW,db,dW2,db2,就可以进行gradient descent参数更新了,更新式此处不再赘述,直接参考以下代码。
4.5 训练
# initialize parameters randomly
h = 100 # size of hidden layer
W1 = 0.01 * np.random.randn(D,h)
b1 = np.zeros((1,h))
W2 = 0.01 * np.random.randn(h,K)
b2 = np.zeros((1,K))
# some hyperparameters
step_size = 1e-0
reg = 1e-3 # regularization strength
# gradient descent loop
num_examples = X.shape[0]
for i in range(10000):
# evaluate class scores, [N x K]
hidden_layer = np.maximum(0, np.dot(X, W1) + b1) # note, ReLU activation
scores = np.dot(hidden_layer, W2) + b2
# compute the class probabilities
exp_scores = np.exp(scores)
probs = exp_scores / np.sum(exp_scores, axis=1, keepdims=True) # [N x K]
# compute the loss: average cross-entropy loss and regularization
corect_logprobs = -np.log(probs[range(num_examples),y])
data_loss = np.sum(corect_logprobs)/num_examples
reg_loss = 0.5*reg*np.sum(W1*W1) + 0.5*reg*np.sum(W2*W2)
loss = data_loss + reg_loss
if i % 1000 == 0:
print( "iteration %d: loss %f" % (i, loss))
# compute the gradient on scores
dscores = probs
dscores[range(num_examples),y] -= 1
dscores /= num_examples
# backpropate the gradient to the parameters
# first backprop into parameters W2 and b2
dW2 = np.dot(hidden_layer.T, dscores)
db2 = np.sum(dscores, axis=0, keepdims=True)
# next backprop into hidden layer
dhidden = np.dot(dscores, W2.T)
# backprop the ReLU non-linearity
dhidden[hidden_layer <= 0] = 0
# finally into W,b
dW1 = np.dot(X.T, dhidden)
db1 = np.sum(dhidden, axis=0, keepdims=True)
# add regularization gradient contribution
dW2 += reg * W2
dW1 += reg * W1
# perform a parameter update
W1 += -step_size * dW1
b1 += -step_size * db1
W2 += -step_size * dW2
b2 += -step_size * db2
# evaluate training set accuracy
hidden_layer = np.maximum(0, np.dot(X, W1) + b1)
scores = np.dot(hidden_layer, W2) + b2
predicted_class = np.argmax(scores, axis=1)
print( 'training accuracy: %.2f' % (np.mean(predicted_class == y)))
运行以上代码我们可得到99%的分类准确度!It is amazing, isn't it?
同样我们可以绘出分类边界示意图如下所示:
Summary
本实验在一个很简单的2D数据集上先是训练了一个线性分类器(单层神经网络),然后在线性分类器的基础上追加一层隐藏层得到一个深层(虽然只有2层!)神经网络。我们看到从单层网络扩充到2层网络,在代码上只有很少的一些修改,包括score function、反向传播等。
尽管只是追加了一层隐藏层,但是在分类准确度上得到了惊人的提高(52% --> 99%),非线性的威力由此可见一斑!
参考文献:
[1] https://machinelearningmastery.com/cross-entropy-for-machine-learning