1. 前言
本文编译自斯坦福大学的CS231n课程(2022) Module1课程中神经网络部分之一,原课件网页参见:
CS231n Convolutional Neural Networks for Visual Recognition
本文(本系列)不是对原始课件网页内容的完全忠实翻译,只是作为学习笔记的摘要,主要是自我参考,而且也可能夹带一些私货(自己的理解和延申,不保证准确性)。如果想要更准确地了解更具体的细节,还请服用原文。如果本摘要恰巧也对小伙伴们有所参考则纯属无心插柳概不认账^-^。
前面几篇:
CS231n-2022 Module1: 神经网络1:Setting Up the Architecture
CS231n-2022 Module1: 神经网络2
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)
![scores_{[N,K]} = X_{[N,D]} W_{[D,K]} + b_{[1,K]}](https://latex.codecogs.com/gif.latex?scores_%7B%5BN%2CK%5D%7D%20%3D%20X_%7B%5BN%2CD%5D%7D%20W_%7B%5BD%2CK%5D%7D%20+%20b_%7B%5B1%2CK%5D%7D)
张量下标中的“[]”用于表示张量的维度,以下同。
3.3 计算分类概率
基于softmax函数![f(x_i) = \frac{e^{x_i}}{\sum\limits_k e^{x_k}}](https://latex.codecogs.com/gif.latex?f%28x_i%29%20%3D%20%5Cfrac%7Be%5E%7Bx_i%7D%7D%7B%5Csum%5Climits_k%20e%5E%7Bx_k%7D%7D)将scores变换成对应于各class#k的概率:
![probs[k] = \frac{e^{scores[k]}}{\sum\limits_{k}{e^{scores[k]}}}](https://latex.codecogs.com/gif.latex?probs%5Bk%5D%20%3D%20%5Cfrac%7Be%5E%7Bscores%5Bk%5D%7D%7D%7B%5Csum%5Climits_%7Bk%7D%7Be%5E%7Bscores%5Bk%5D%7D%7D%7D)
3.4 计算交叉熵损失函数
Softmax分类器使用的损失函数为交叉熵损失函数(softmax loss, cross-entropy loss)。给定一个分布P和一个分布Q,交叉熵定义为([1]):
![H(P, Q) = -\sum\limits_{ x \in X} P(x) * log(Q(x))](https://latex.codecogs.com/gif.latex?H%28P%2C%20Q%29%20%3D%20-%5Csum%5Climits_%7B%20x%20%5Cin%20X%7D%20P%28x%29%20*%20log%28Q%28x%29%29)
其中,P(x)代表在分布P中x发生的概率,Q(x)代表在分布Q中x发生的概率。
以P代表真值的分布,显然应该是一个one-hot的分布。比如说在本例中有三种分类{0,1,2},假设样本xi的真值yi = 1,则应该有![p_i = [0,1,0]](https://latex.codecogs.com/gif.latex?p_i%20%3D%20%5B0%2C1%2C0%5D)。
以Q代表预测值的分布,即Q = [probs[0],probs[1],probs[2]].
则显而易见的是,样本i {xi, yi}的损失应该为:
![L_i = - \sum\limits_{k=0,1,2} P[k] log(probs[k]) = -log(probs[y_i])](https://latex.codecogs.com/gif.latex?L_i%20%3D%20-%20%5Csum%5Climits_%7Bk%3D0%2C1%2C2%7D%20P%5Bk%5D%20log%28probs%5Bk%5D%29%20%3D%20-log%28probs%5By_i%5D%29)
而整个数据集的总(平均)损失自然应该是:
![L_{data} =\frac{1}{N} \sum\limits_{i} L_i = -\frac{1}{N} \sum\limits_{i} log(probs[y_i])](https://latex.codecogs.com/gif.latex?L_%7Bdata%7D%20%3D%5Cfrac%7B1%7D%7BN%7D%20%5Csum%5Climits_%7Bi%7D%20L_i%20%3D%20-%5Cfrac%7B1%7D%7BN%7D%20%5Csum%5Climits_%7Bi%7D%20log%28probs%5By_i%5D%29)
进一步,还要加上正则化损失(这里考虑![L_2](https://latex.codecogs.com/gif.latex?L_2)正则化损失)得到:
![L = L_{data} + L_{reg} = -\frac{1}{N} \sum\limits_{i} log(probs[y_i]) + \frac{1}{2} \lambda \sum\limits_k \sum \limits_l W^2_{k,l}](https://latex.codecogs.com/gif.latex?L%20%3D%20L_%7Bdata%7D%20+%20L_%7Breg%7D%20%3D%20-%5Cfrac%7B1%7D%7BN%7D%20%5Csum%5Climits_%7Bi%7D%20log%28probs%5By_i%5D%29%20+%20%5Cfrac%7B1%7D%7B2%7D%20%5Clambda%20%5Csum%5Climits_k%20%5Csum%20%5Climits_l%20W%5E2_%7Bk%2Cl%7D)
其中,正则化损失的系数(1/2)仅仅是为了数学推导的方便(平方项求导后会产生2的因子,两者抵消会显得梯度的解析式更清爽一些)。
正则化系数![\lambda](https://latex.codecogs.com/gif.latex?%5Clambda)作为模型的一个超参数进行调节。
如上一篇所述,作为梯度检查的一项,在正则化系数置0,随机初始化后,loss的初始值应该为![1.1 = -log(1/3)](https://latex.codecogs.com/gif.latex?1.1%20%3D%20-log%281/3%29)(注意,本文中log均指自然对数,即ln())。可以据此判断loss的实现是否存在明显的错误。
3.5 基于反向传播计算解析梯度(analytic gradient)
上面我们已经得到了损失函数,神经网络训练的目的就是要使损失最小化。我们将采用梯度下降(gradient descent)方法。基本思路是,从随机参数出发,计算损失函数关于这些参数的梯度,并由此确定向哪个方向以多大步长进行参数调节。
为了简洁起见,重写一下分类概率和样本损失(scores --> f, probs --> p, []-->下标),并推导(单个样本![x_i](https://latex.codecogs.com/gif.latex?x_i)的损失![L_i](https://latex.codecogs.com/gif.latex?L_i)关于分类k的分数![f_k](https://latex.codecogs.com/gif.latex?f_k)的)偏导数如下(其中关键是求导的链式法则。偏导数是构成梯度的元素):
注意,以上推导是针对 ![x_i](https://latex.codecogs.com/gif.latex?x_i)的,所以,其中f应该视为关于样本{i}的。所以,![f_k](https://latex.codecogs.com/gif.latex?f_k)应该写作![f_{i,k}](https://latex.codecogs.com/gif.latex?f_%7Bi%2Ck%7D)(即样本i的识别为分类k的分数),![f_{y_i}](https://latex.codecogs.com/gif.latex?f_%7By_i%7D)应该写做![f_{i,y_i}](https://latex.codecogs.com/gif.latex?f_%7Bi%2Cy_i%7D),![p_k](https://latex.codecogs.com/gif.latex?p_k)应该写成![p_{i,k}](https://latex.codecogs.com/gif.latex?p_%7Bi%2Ck%7D)。。。
其中,I(x)表示Indicator function,也有写做1(x)的。softmax之所以令人喜欢就在于它所导致的梯度是如此简洁优雅(其根源又在于指数函数的魔力)。下面通过一个简单的例子来增进直观的理解。假设针对样本![x_i](https://latex.codecogs.com/gif.latex?x_i)通过计算得到
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。
有了![\nabla_{f_i} L_i](https://latex.codecogs.com/gif.latex?%5Cnabla_%7Bf_i%7D%20L_i)后,就可以进一步基于反向传播、链式法则,求得loss关于W、b的梯度。以下仅给出loss对于某个权重参数![W_{d,k}](https://latex.codecogs.com/gif.latex?W_%7Bd%2Ck%7D)的偏微分作为示例(由此扩充至![\nabla_W L](https://latex.codecogs.com/gif.latex?%5Cnabla_W%20L)以及![\nabla_b L](https://latex.codecogs.com/gif.latex?%5Cnabla_b%20L)是一个顺理成章水到渠成的过程):
![\begin{align} \frac{\partial{L}}{\partial{W_{d,k}}} &= \sum\limits_{i} \frac{\partial{L_i}}{\partial{W_{d,k}}} \\ &= \sum\limits_{i}\sum\limits_{k} \frac{\partial{L_i}}{\partial{f_{i,k}}} \frac{\partial{f_{i,k}}}{\partial{W_{d,k}}} \end{}](https://latex.codecogs.com/gif.latex?%5Cbegin%7Balign%7D%20%5Cfrac%7B%5Cpartial%7BL%7D%7D%7B%5Cpartial%7BW_%7Bd%2Ck%7D%7D%7D%20%26%3D%20%5Csum%5Climits_%7Bi%7D%20%5Cfrac%7B%5Cpartial%7BL_i%7D%7D%7B%5Cpartial%7BW_%7Bd%2Ck%7D%7D%7D%20%5C%5C%20%26%3D%20%5Csum%5Climits_%7Bi%7D%5Csum%5Climits_%7Bk%7D%20%5Cfrac%7B%5Cpartial%7BL_i%7D%7D%7B%5Cpartial%7Bf_%7Bi%2Ck%7D%7D%7D%20%5Cfrac%7B%5Cpartial%7Bf_%7Bi%2Ck%7D%7D%7D%7B%5Cpartial%7BW_%7Bd%2Ck%7D%7D%7D%20%5Cend%7B%7D)
以![f_{[N,K]}](https://latex.codecogs.com/gif.latex?f_%7B%5BN%2CK%5D%7D), ![p_{[N,K]}](https://latex.codecogs.com/gif.latex?p_%7B%5BN%2CK%5D%7D)分别代表整个数据集的
scores = np.dot(X, W) + b
和概率矩阵,则基于以上推导经过一些矩阵微积分(matrix calculus)运算可以得到:
![\nabla_W L = \{\nabla_f L\}_{[K,N]} X_{[N,D]}](https://latex.codecogs.com/gif.latex?%5Cnabla_W%20L%20%3D%20%5C%7B%5Cnabla_f%20L%5C%7D_%7B%5BK%2CN%5D%7D%20X_%7B%5BN%2CD%5D%7D)
同理可以得到总体损失关于向量b的梯度(但是其表达式稍微有点难写)。
由此可以得到以下梯度计算的实现代码。注意梯度计算的实现代码中的对应关系:probs--![p_{[N,K]}](https://latex.codecogs.com/gif.latex?p_%7B%5BN%2CK%5D%7D); dscores--![\{\nabla_{f_{[N,K]}} L \}^T](https://latex.codecogs.com/gif.latex?%5C%7B%5Cnabla_%7Bf_%7B%5BN%2CK%5D%7D%7D%20L%20%5C%7D%5ET)(加转置是为了维度匹配),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 参数更新
有了梯度![\nabla_W L](https://latex.codecogs.com/gif.latex?%5Cnabla_W%20L)和![\nabla_b L](https://latex.codecogs.com/gif.latex?%5Cnabla_b%20L)和另一个超参数![\mu](https://latex.codecogs.com/gif.latex?%5Cmu)(step_size),参数的更新就直截了当了:
![\begin{align} W_{new} &= W_{old} - \mu \nabla_W L \\ b_{new} &= b_{old} - \mu \nabla_b L\end{}](https://latex.codecogs.com/gif.latex?%5Cbegin%7Balign%7D%20W_%7Bnew%7D%20%26%3D%20W_%7Bold%7D%20-%20%5Cmu%20%5Cnabla_W%20L%20%5C%5C%20b_%7Bnew%7D%20%26%3D%20b_%7Bold%7D%20-%20%5Cmu%20%5Cnabla_b%20L%5Cend%7B%7D)
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.785249
training 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. 训练一个神经网络
未完待续
参考文献:
[1] https://machinelearningmastery.com/cross-entropy-for-machine-learning
版权归原作者 笨牛慢耕 所有, 如有侵权,请联系我们删除。