写在前面

这将是一篇长文,里面不仅仅包括karpathy的neural network的实现过程讲解,还有Andrew Ng的关于softmax的讲解。

Author : Jasperyang
School : BUPT

正文

从 neural-networks-case-study 开始

这个部分直接就是 neural-networks-case-study的翻译,我认为我加上了别的资料后对这方面较有理解,希望翻译后的版本能让更多人接受和快速理解。

英文版目录

  • Generating some data
  • Training a Softmax Linear Classifier

    • Initialize the parameters
    • Compute the class scores
    • Compute the loss
    • Computing the analytic gradient with backpropagation
    • Performing a parameter update
    • Putting it all together: Training a Softmax Classifier
  • Training a Neural Network
  • Summary

中文版目录

  • 生成数据(生成数据的方式很值得学习,很巧妙,利用了正余弦公式)
  • 训练softmax的线性分类器

    • 初始化参数
    • 计算每个类的得分
    • 计算loss
    • 计算反向传播(最重要的部分)
    • 参数更新
    • 整合代码
  • 训练一个神经网络(带有隐含层的)
  • 总结

生成数据

生成一些线性不可分的数据集,最有代表性的就是螺旋数据。

N = 100 # number of points per class
D = 2 # dimensionality
K = 3 # number of classes
X = np.zeros((N*K,D)) # data matrix (each row = single example)
y = np.zeros(N*K, dtype='uint8') # class labels
for j in xrange(K):
  ix = range(N*j,N*(j+1))
  r = np.linspace(0.0,1,N) # radius
  t = np.linspace(j*4,(j+1)*4,N) + np.random.randn(N)*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()

The toy spiral data consists of three classes (blue, red, yellow) that are not linearly separable

一般来讲,我们需要对数据进行服从标准正太的归一化处理,这样比较方便,幸运的是这个数据集已然如此。

训练softmax的线性分类器

初始化参数
# initialize parameters randomly
W = 0.01 * np.random.randn(D,K)
b = np.zeros((1,K))

D是维度,这里为2(x,y轴两个方向),K是类别数,这里为3。

计算每个类的得分
# compute class scores for a linear classifier
scores = np.dot(X, W) + b

X是个300x2的矩阵,W是个2x3的矩阵,计算出的scores是一个[300x3]的矩阵,3代表不同的类别[蓝,红,黄].

计算loss

接下来我们就需要一个损失函数,尽管有很多的做法,这里使用的是softmax,定义如下,f代表的是计算出来的scores也就是三种分类的得分,这里利用y来选择哪种分类,因为这是supervised training,我们在之前已经知道当前这个点应该属于哪一个类,所以,

$$L_i=−log(\frac{e^{f_{y_i}}}{∑_je^{f_j}})$$

然而,下面加了正则项的才是最终的形式。(现在也许还不能理解我上面说的什么意思,先仔细往下看,看到代码那段再思考一下应该就能明白)

$$L=\underbrace{\frac{1}{N}\sum_{i}L_i}_{data \space loss}+\underbrace{\frac{1}{2}λ\sum_{k}\sum_lW^{2}_{k,l}}_{regularization \space loss}$$

先是用代码复现上公式的 data loss


num_examples = X.shape[0]
# get unnormalized probabilities
exp_scores = np.exp(scores)
# normalize them for each example
probs = exp_scores / np.sum(exp_scores, axis=1, keepdims=True)

我们现在得到一个[300x3]的probs,然后下面这段代码就是我之前提醒大家要思考的地方,从y的定义可以看出y是代表这个点是哪个label的,所以,

corect_logprobs = -np.log(probs[range(num_examples),y])

完全的loss计算:


# compute the loss: average cross-entropy loss and regularization
data_loss = np.sum(corect_logprobs)/num_examples
reg_loss = 0.5*reg*np.sum(W*W)
loss = data_loss + reg_loss
计算反向传播(最重要的部分)

反向传播这里的代码是我理解最久的地方,后来我甚至发现了一个与我想法出入的地方。先不谈,反向传播的作用其实就是为了矫正,详细的讲解看本文的后半部分或是知乎

$$p_k=\frac{e^{f_k}}{\sum_{j}e^{f_j}}$$
$$L_i=−log(p_{y_i})$$

上公式中的p就是前面计算出来的probs。所以得到一个优雅的求导后公式:

$$\frac{∂L_i}{∂f_k}=p_k−1(y_i=k)$$

下面我们将求导后的变量叫dscores,则可以得到以下代码:


dscores = probs
dscores[range(num_examples),y] -= 1
dscores /= num_examples

最后,因为 scores = np.dot(X,W) + b ,所以求导可知 dscores = Xdw ,dsocres = db 所以代码如下,但是我其实在这里有个疑问,我认为 dw 不应该等于 X逆dscores吗?any way,code here.


dW = np.dot(X.T, dscores)
db = np.sum(dscores, axis=0, keepdims=True)
dW += reg*W # don't forget the regularization gradient

注意:

$$\frac{d}{dw}(\frac{1}{2}λw^2)=λw$$

所以dW += reg*W

参数更新

# perform a parameter update
W += -step_size * dW
b += -step_size * db
整合代码

#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
  corect_logprobs = -np.log(probs[range(num_examples),y])
  data_loss = np.sum(corect_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

我们可以得到最后的准确率是 49%


# 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))

然后我们可以画出最终分类的区分图:

# plot the resulting classifier
h = 0.02
x_min, x_max = X[:, 0].min() - 0.5, X[:, 0].max() + 0.5
y_min, y_max = X[:, 1].min() - 0.5, X[:, 1].max() + 0.5
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')
plt.show()

图片描述

可以看出效果其实很不好,为了做到非线性的分类,我们就需要加入一个隐含层,这个隐含层的参数设置只有个大小h需要设置,这个参数是可调节的,然后我们就会得到一个非常漂亮的分类效果图。

训练一个神经网络(带有隐含层的)

这里有个不太一样的地方就是加入了ReLU激活函数,这个函数很简单,但是却具备softmax不具备的好处。

Sigmoid 函数曾经被使用的很多,不过近年来,用它的人越来越少了。主要是因为它的缺点(输入较大或较小的时候,最后梯度会接近于0),最终导致网络学习困难。详细内容可以看知乎回答

这个就是Relu

$$r=max(0,x)$$
$$\frac{dr}{dx}=1(x>0)$$

因为其特性,一旦x大于0,才能通过,并且倒数是一个常数,所以当我们反向传播时很简单,小于零的置0,别的不变。


# backprop the ReLU non-linearity
dhidden[hidden_layer <= 0] = 0

最后得到整合代码。

# 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
  corect_logprobs = -np.log(probs[range(num_examples),y])
  data_loss = np.sum(corect_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

准确率高达98%


# 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))
# 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.maximum(0, np.dot(np.c_[xx.ravel(), yy.ravel()], W) + b), W2) + b2
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_net.png')
plt.show()

图片描述

总结

加入一个隐含层的改变是如此的巨大,细微之处可见神经网络的效果是多么惊人。

  • 可以直接下载ipynb,自己去跑跑。

第二部分,很懒,没写

其实是segmentfault的文章一旦写长了就非常卡了,原因并不知道,所以就写到这,贴一下传送门: Softmax详解


jasperyang
203 声望58 粉丝

Highest purpose is Hacking...