CS231n-2022 Module1: Minimal Neural Network case study

发布于:2023-01-04 ⋅ 阅读:(316) ⋅ 点赞:(0)

目录

1. 前言

2. 数据生成

3. 训练一个Softmax线性分类器

3.1 参数初始化

3.2 计算分类分数(class scores)

3.3 计算分类概率

3.4 计算交叉熵损失函数

3.5 基于反向传播计算解析梯度(analytic gradient)

3.6 参数更新

3.7 组装到一起进行训练

3.7.1 处理框图

3.7.2 代码

4. 训练一个神经网络

4.1 参数以及前向计算

4.2 loss computation

4.3 梯度计算

4.3.1 dscores计算

4.3.2 dW2, db2计算

4.3.3 dhidden

4.3.4 dW1, db1计算

4.4 参数更新

4.5 训练 


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]}

        张量下标中的“[]”用于表示张量的维度,以下同。


3.3 计算分类概率

        基于softmax函数f(x_i) = \frac{e^{x_i}}{\sum\limits_k e^{x_k}}将scores变换成对应于各class#k的概率:

                probs[k] = \frac{e^{scores[k]}}{\sum\limits_{k}{e^{scores[k]}}}

3.4 计算交叉熵损失函数

        Softmax分类器使用的损失函数为交叉熵损失函数(softmax loss, cross-entropy loss)。给定一个分布P和一个分布Q,交叉熵定义为([1]):

                H(P, Q) = -\sum\limits_{ x \in X} P(x) * log(Q(x))

        其中,P(x)代表在分布P中x发生的概率,Q(x)代表在分布Q中x发生的概率。

        以P代表真值的分布,显然应该是一个one-hot的分布。比如说在本例中有三种分类{0,1,2},假设样本xi的真值yi = 1,则应该有p_i = [0,1,0]

        以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])

        而整个数据集的总(平均)损失自然应该是:

                L_{data} =\frac{1}{N} \sum\limits_{i} L_i = -\frac{1}{N} \sum\limits_{i} log(probs[y_i])

        进一步,还要加上正则化损失(这里考虑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}

        其中,正则化损失的系数(1/2)仅仅是为了数学推导的方便(平方项求导后会产生2的因子,两者抵消会显得梯度的解析式更清爽一些)。

        正则化系数\lambda作为模型的一个超参数进行调节。

        如上一篇所述,作为梯度检查的一项,在正则化系数置0,随机初始化后,loss的初始值应该为1.1 = -log(1/3)(注意,本文中log均指自然对数,即ln())。可以据此判断loss的实现是否存在明显的错误。


3.5 基于反向传播计算解析梯度(analytic gradient)

        上面我们已经得到了损失函数,神经网络训练的目的就是要使损失最小化。我们将采用梯度下降(gradient descent)方法。基本思路是,从随机参数出发,计算损失函数关于这些参数的梯度,并由此确定向哪个方向以多大步长进行参数调节。

        为了简洁起见,重写一下分类概率和样本损失(scores --> f, probs --> p, []-->下标),并推导(单个样本x_i的损失L_i关于分类k的分数f_k的)偏导数如下(其中关键是求导的链式法则。偏导数是构成梯度的元素): 

        注意,以上推导是针对 x_i的,所以,其中f应该视为关于样本{i}的。所以,f_k应该写作f_{i,k}(即样本i的识别为分类k的分数),f_{y_i}应该写做f_{i,y_i}p_k应该写成p_{i,k}。。。

        其中,I(x)表示Indicator function,也有写做1(x)的。softmax之所以令人喜欢就在于它所导致的梯度是如此简洁优雅(其根源又在于指数函数的魔力)。下面通过一个简单的例子来增进直观的理解。假设针对样本x_i通过计算得到​​​​​p = [0.2, 0.3, 0.5], 并且假定正确的分类是k=1(中间那个,其概率为0.3)。根据以上梯度公式可以得到(这里为简洁起见,以df表示梯度向量\nabla_{f_i} L_i) 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后,就可以进一步基于反向传播、链式法则,求得loss关于W、b的梯度。以下仅给出loss对于某个权重参数W_{d,k}的偏微分作为示例(由此扩充至\nabla_W L以及\nabla_b L是一个顺理成章水到渠成的过程):

                \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{}

         以f_{[N,K]}, p_{[N,K]}分别代表整个数据集的scores = np.dot(X, W) + b和概率矩阵,则基于以上推导经过一些矩阵微积分(matrix calculus)运算可以得到:

                \nabla_W L = \{\nabla_f L\}_{[K,N]} X_{[N,D]}                 

        同理可以得到总体损失关于向量b的梯度(但是其表达式稍微有点难写)。 

        由此可以得到以下梯度计算的实现代码。注意梯度计算的实现代码中的对应关系:probs--p_{[N,K]}; dscores--\{\nabla_{f_{[N,K]}} L \}^T(加转置是为了维度匹配),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\nabla_b L和另一个超参数\mu(step_size),参数的更新就直截了当了:

                \begin{align} W_{new} &= W_{old} - \mu \nabla_W L \\ b_{new} &= b_{old} - \mu \nabla_b L\end{}


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. 训练一个神经网络

        在上一章所述的softmax classifier的基础上追加一层隐藏层(后面我们将会看到,对于以上这个toy dataset,一层隐藏层足以获得充分好的分类性能)。以下各节分别描述追加一层隐藏层所带来的变化。

4.1 参数以及前向计算

        追加一层隐藏层后,将有两个参数层(隐藏层,输出层),我们其参数分别记为W^{(1)},b^{(1)},W^{(2)},b^{(2)}(我们用上标来标识它所对应的层序号),前向计算过程变为如下所示:

                \begin{align} s^{(1)} &= X^{(0)} W^{(1)} + b^{(1)} \\ X^{(1)} &= g(s^{(1)}) \\ s^{(2)} &= X^{(1)} W^{(2)} + b^{(2)} \end{align}

        其中,X^{(l)}表示layer#(l+1)的输入,而X^{(0)}即是原始输入数据。g(x)表示隐藏层的激励函数,最常用的是ReLU,s^{(k)}则表示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(即现在的s^{(2)})的梯度,即\nabla_{s^{(2)}} L,其中s^{(2)}代表(输出层的)scores,而该梯度在代码中仍用dscores表示。

        好吧,显而易见的是,由于scores的计算与前面有多少隐藏层没有关系,所以dscores的计算不受追加一层隐藏层的影响,没有任何变化! 

4.3.2 dW2, db2计算

        W2和b2是输出层的权重参数,同样,loss关于它们的梯度的计算不受追加的隐藏层的影响(想一想,反向传播的方向,在传播路径上居于前面的不会受到后面的影响。在反向传播路径上,输出层在新追加的隐藏层的前面)。

        当然,由于dW2和db2的解析式中包含上一层输出的score。在上一章的Softmax分类其中,输出层的上一层(即是输入层)的输出数据就是原始的X,即现在的X^{[0]};而现在变成的隐藏层的输出即s^{(1)}

        所以dW2和db2的计算代码的修改只需要:(1)名字的改变(dW->dW2,db->db2);(2)用s^{(1)}(在以下代码中用hidden_layer表示)替换原来的X。

4.3.3 dhidden

        与dW2、db2的计算需要先计算dscores一样,dW1、db1的计算同样需要先计算loss关于layer#1的输出s^{(1)}的梯度,即\nabla_{s^{(1)}} L,它可以计算如下(公式推导待补充):

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


网站公告

今日签到

点亮在社区的每一天
去签到