目录
在这一节中,我们将用模拟数据集(2个特征),实现一个完整的2层神经网络。我们将首先实现一个简单的线性分类器,然后将代码扩展到2层神经网络。我们将看到,这个扩展惊人地简单,只需要非常少的变化即可。
10.1 生成数据
让我们生成一个不容易线性分割的分类数据集。我们最喜欢的例子是螺旋数据集,代码如下:
import matplotlib.pyplot as plt
import numpy as np
#生成数据集
#每个分类100个点
N = 100
#2个维度
D = 2
#3个分类
K = 3
#数据矩阵(每行一个样本)
X = np.zeros((N*K,D))
#分类标签
y = np.zeros(N*K, dtype='uint8')
for j in range(K):
ix = range(N*j,N*(j+1))
#半径
r = np.linspace(0.0,1,N)
# theta
t = np.linspace(j*4,(j+1)*4,N) + np.random.randn(N)*0.2
X[ix] = np.c_[r*np.sin(t), r*np.cos(t)]
y[ix] = j
#可视化数据
plt.scatter(X[:, 0], X[:, 1], c=y, s=40, cmap=plt.cm.Spectral)
plt.show()
模拟的螺旋数据由三个类(蓝、红、黄)组成,它们不是线性可分离的
通常,我们希望对数据集进行预处理,以使每个特征具有零均值和单位标准偏差,但这是模拟数据,特征已经在从-1到1的良好范围内,所以我们跳过这一步。
10.2 训练Softmax线性分类器
10.2.1初始化参数
首先让我们在这个分类数据集上训练一个Softmax分类器。前面的章节讲过,Softmax分类器有一个线性分值函数,使用交叉熵损失。线性分类器的参数由权重矩阵W和每个类的偏置向量 b 组成。首先让这些参数初始化为随机数:
#随机初始化参数W
W = 0.01 * np.random.randn(D,K)
b = np.zeros((1,K))
D=2是维数,k=3是分类数。
10.2.2计算分类得分
由于这是一个线性分类器,我们可以非常简单地用户一个矩阵乘法计算所有类分数:
#为一个线性分类器计算分类得分
scores = np.dot(X, W) + b
在这个例子中,我们有300个2维点,因此在乘法之后,分数大小是[300×3 ],其中每行给出对应于3个类(蓝色、红色、黄色)的类得分。
10.2.3计算损失
我们需要的第二个关键因素是损失函数,它是一个可微分的目标,用计算的分类分数来量化我们的不爽。直观地说,我们希望正确的类具有比其他类更高的分数。在这种情况下,损失应该很低,否则损失会很高。有很多方法来量化这种直觉,但是在这个例子中,我们使用与Softmax 分类器相关联的交叉熵损失。回想一下,如果F是一个样本的类分数数组(例如这里就是3个数的数组),那么Softmax 分类器计算该样本的损失为:
我们可以看到,Softmax 分类器将F的每个元素解释为保持三个类的(非归一化)对数概率。我们对这些进行指数化以得到(非规范化)概率,然后将它们归一化以获得概率。因此,对数内的表达式是正确类的归一化概率。注意这个表达式是如何工作的:这个量总是在0到1之间。当正确类的概率非常小(接近0)时,损失将朝向(正)无穷大。相反,当正确的类概率接近1时,由于log(1)=0,损失将趋于零。因此,当正确的类概率较高时,Li的值较低,而当正确的类概率较低时非常高。
还记得,完整的Softmax 分类器损失,是定义在训练样本上的平均交叉熵损失和正则化:
考虑到上面计算的数组 scores
,我们可以计算损失。首先,获得概率的方法是直截了当的:
num_examples = X.shape[0]
#获得没有归一化的概率
exp_scores = np.exp(scores)
#归一化
probs = exp_scores / np.sum(exp_scores, axis=1, keepdims=True)
我们现在有一个数组probs
,大小为 300×3,其中每行现在包含类概率。因为我们已经将它们归一化,现在每一行总和为1。现在我们可以查询在每个样本中分配给正确类的对数概率:
correct_logprobs = -np.log(probs[range(num_examples),y])
correct_logprobs
是一个一维数组,为每个样本分配给正确的类的概率。完全损失是这些对数概率和正则化损失的平均值:
#L 计算损失: 平均交叉熵损失和正则化
data_loss = np.sum(correct_logprobs)/num_examples
reg_loss = reg*np.sum(W*W)
loss = data_loss + reg_loss
在该代码中,reg是
正则化强度λ。在开始时用随机参数来评估这一点可能会给我们 loss = 1.1
,这是 np.log(1.0/3)
,因为在初始初始随机权值的情况下,分配给所有类的所有概率都是三分之一左右。我们现在想把损失尽可能低,loss = 0
作为绝对下界。但是损失越低,分配给所有样本的正确类的概率就越高。
10.2.4 用反向传播算法计算解析梯度
我们有一种评估损失的方法,现在我们必须尽量减少损失。我们将采用梯度下降法。也就是说,我们从随机参数开始(如上文所示),并且评估损失函数相对于参数的梯度,这样我们就知道我们应该如何改变参数以减少损失。让我们介绍中间变量p,它是(归一化)概率的向量。一个样本的损失是:
现在我们希望理解 f 内的计算分数应该如何改变,以减少损失 Li,即这个样本对整个目标的贡献。换言之,我们要导出梯度 ∂Li/∂fk.。损失 Li 是由 p 计算的,p又取决于f。使用链式法则来推导梯度是一个有趣的练习,但是在很多事情被剔除后,它最终是非常简单和可解释的:
注意这个表达是多么的优雅和简单。假设我们计算的概率是p=(0.2,0.3,0.5),并且正确的类是中间类(概率0.3)。根据这个推导,得分函数上的梯度将是 df=[ 0.2,- 0.7,0.5 ]。回顾梯度的解释,我们看到这个结果是非常直观的:增加分数向量 f 的第一个或最后一个元素(不正确类的分数)导致增加损失(由于正符号 +0.2和 +0.5)-正如预期的一样,并且增加损失是不好的。而增加正确分类的分数对损失有负面影响。-0.7 的梯度告诉我们,增加正确的分类分数会导致损失的减少,这就对了。
所有这些都归结为下面的代码。probs
存储每个样本的所有分类(一行一个)的概率。计算得分函数的梯度,即 dscores
,代码如下:
dscores = probs
dscores[range(num_examples),y] -= 1
dscores /= num_examples
最后,公式 scores = np.dot(X, W) + b
携带了关于 scores
的梯度(存储在 dscores
中),我们现在可以反向传播到 W
和 b
:
#反向传播到 W 和 b:
dW = np.dot(X.T, dscores)
db = np.sum(dscores, axis=0, keepdims=True)
#正则化梯度
dW += reg*W
在这里我们看到我们通过矩阵乘法运算来支持,并且还增加了正则化的贡献。注意,正则化梯度具有非常简单的形式 reg*W
,因为我们使用常数0.5来表示其损失贡献(即)。这是简化梯度表达式的一种常见的便利技巧。
10.2.5 执行参数更新
现在我们已经评估了梯度,我们知道每个参数如何影响损失函数。现在我们将在负梯度方向上执行参数更新以减少损失:
#执行参数更新
W += -step_size * dW
b += -step_size * db
10.2.6 组装到一起:训练一个 Softmax 分类器
用梯度下降训练SoftMax分类器的完整代码:
#Train a Linear Classifier
# initialize parameters randomly
W = 0.01 * np.random.randn(D,K)
b = 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 xrange(200):
# evaluate class scores, [N x K]
scores = np.dot(X, W) + b
# 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
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)
# 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
运行结果:
iteration 0: loss 1.096956
iteration 10: loss 0.917265
iteration 20: loss 0.851503
iteration 30: loss 0.822336
iteration 40: loss 0.807586
iteration 50: loss 0.799448
iteration 60: loss 0.794681
iteration 70: loss 0.791764
iteration 80: loss 0.789920
iteration 90: loss 0.788726
iteration 100: loss 0.787938
iteration 110: loss 0.787409
iteration 120: loss 0.787049
iteration 130: loss 0.786803
iteration 140: loss 0.786633
iteration 150: loss 0.786514
iteration 160: loss 0.786431
iteration 170: loss 0.786373
iteration 180: loss 0.786331
iteration 190: loss 0.786302
我们看到,在大约190次迭代之后,我们已经收敛到某个位置。我们可以评估训练集的准确性:
# 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))
结果为49%。不太好,但也不足为奇,因为数据集是构造的,所以它不是线性可分的。我们也可以绘制学习的决策边界:
线性分类器无法学习模拟的螺旋数据集。
10.3 训练一个神经网络
显然,线性分类器不适合这个数据集,我们希望使用神经网络。一个额外的隐藏层将足以满足这个模拟数据。我们现在需要两组全职和偏值(对于第一层和第二层):
# initialize parameters randomly
h = 100 # size of hidden layer
W = 0.01 * np.random.randn(D,h)
b = np.zeros((1,h))
W2 = 0.01 * np.random.randn(h,K)
b2 = np.zeros((1,K))
计算得分的形式变为:
# evaluate class scores with a 2-layer Neural Network
hidden_layer = np.maximum(0, np.dot(X, W) + b) # note, ReLU activation
scores = np.dot(hidden_layer, W2) + b2
请注意,唯一变化是一个额外的代码行,在这里我们首先计算隐藏层表示,然后基于这个隐藏层的计算得分。最重要的是,我们还增加了非线性,用简单的Relu函数在零上激活隐藏层的阈值。
其他一切都是一样的。我们根据前面的分数来计算损失,并像以前一样得到 dscores
的梯度。然而,当然,我们将梯度反向传播到模型参数的方式也会发生变化。首先让反向传播神经网络的第二层。这看起来与我们对于Softmax 分类器的代码相同,除了我们用变量 hidden_layer
替换X(原始数据):
# 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)
但是,不像之前我们还没有完成的,因为 hidden_layer
层本身就是其他参数和数据的函数!我们需要通过这个变量继续反向传播。其梯度可以计算为:
dhidden = np.dot(dscores, W2.T)
现在我们拿到了隐藏层的输出上的梯度。接下来,我们必须反向传播Relu非线性。这是很容易的,因为在后传中的Relu实际上是一个开关。由于r=max(0,x),我们有dr/dx=1(x>0)。与链式法则相结合,我们看到,前向传播时,Relu单元让梯度不变,如果其输入大于0,但杀死它,如果它的输入小于零。因此,我们可以简单地将Relu反向传播:
# backprop the ReLU non-linearity
dhidden[hidden_layer <= 0] = 0
现在,我们终于可以计算第一层权重和偏值了:
# finally into W,b
dW = np.dot(X.T, dhidden)
db = np.sum(dhidden, axis=0, keepdims=True)
完毕!我们有梯度dW,db,dW2,db2
,可以执行参数更新了。其他一切都没有改变。完整的代码看起来非常相似:
# initialize parameters randomly
h = 100 # size of hidden layer
W = 0.01 * np.random.randn(D,h)
b = 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 xrange(10000):
# evaluate class scores, [N x K]
hidden_layer = np.maximum(0, np.dot(X, W) + b) # 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
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) + 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
dW = np.dot(X.T, dhidden)
db = np.sum(dhidden, axis=0, keepdims=True)
# add regularization gradient contribution
dW2 += reg * W2
dW += reg * W
# perform a parameter update
W += -step_size * dW
b += -step_size * db
W2 += -step_size * dW2
b2 += -step_size * db2
打印结果为:
iteration 0: loss 1.098744
iteration 1000: loss 0.294946
iteration 2000: loss 0.259301
iteration 3000: loss 0.248310
iteration 4000: loss 0.246170
iteration 5000: loss 0.245649
iteration 6000: loss 0.245491
iteration 7000: loss 0.245400
iteration 8000: loss 0.245335
iteration 9000: loss 0.245292
训练的准确性是:
# evaluate training set accuracy
hidden_layer = np.maximum(0, np.dot(X, W) + b)
scores = np.dot(hidden_layer, W2) + b2
predicted_class = np.argmax(scores, axis=1)
print 'training accuracy: %.2f' % (np.mean(predicted_class == y))
打印98%!我们也可以可视化决策边界:
神经网络分类器碾压螺旋数据集。
10.4 总结
我们模拟的2D数据集上,训练线性网络和2层神经网络。我们看到从线性分类器到神经网络的变化只需要对代码稍作改动。得分函数有1行代码差异,反向传播有变化是必须通过隐藏层对网络的第一层执行另一轮反向支柱。
- IPython Notebook 代码 rendered as HTML.
- 下载 ipynb file
斯坦福大学计算机视图课程,青星人工智能研究中心 翻译整理
原文地址 CS231n Convolutional Neural Networks for Visual Recognition