此专栏文章随时更新编辑,如果你看到的文章还没写完,那么多半是作者正在更新或者上一次没有更新完,请耐心等待,正常的频率是每天更新一篇文章。
该文章是“深度学习(Deep Learning)”系列文章的第一部分,首发于知乎的专栏“深度学习+自然语言处理(NLP)”。
该系列文章的目的在于理清利用深度学习进行自然语言处理的一些基本概念。
以下是相关文章的链接地址:
====================================================================
按照惯例先放出文章目录:推导 sigmoid函数的梯度
推导交叉熵函数关于softmax向量的梯度
推导包含一个隐藏层神经网络的梯度
神经网络参数的讨论
sigmoid函数及其梯度的python实现
梯度检查器的python实现
神经网络的正向和反向传递及其python实现
====================================================================
下面开始正文:
1. 推导sigmoid函数的梯度
我们想要推导sigmoid函数的梯度,并证明它的梯度的表达式可以用sigmoid函数表示(比较拗口,往下看)。
首先,sigmoid函数如下:
在我么开始求梯度之前,先弄清楚一个更重要的终极问题:sigmoid函数能干神马???
如果你把该函数在坐标轴上画出来,sigmoid函数长这个样子:
观察上面这张图,我们的问题迎刃而解:sigmoid函数能够把一个实数,转换成为[0-1]之间的数,也就是可以转换成概率!!!
了解了这个函数的意义比求梯度的意义重要一万倍!!!
下面求其梯度:
我们假设输入x是一个实数,那么对其求导得到
(这里我们只关注结果,忽略求导过程,如果读者读者感兴趣,可以自行计算),进而通过一些列计算,可以表示为
2. 推导交叉熵函数关于softmax向量的梯度
在自然语言处理(NLP)基础概念(一):词向量及其生成方法介绍的4.2节中,我们提到过连续袋模型,其中,为了评估预测值与真实值的差距,我们引入了测量函数:交叉熵(cross entropy)。
交叉熵就是计算真实值
,和预测值
之间的‘距离’。
其中y是one-hot向量。
那么如果我们能够得到交叉熵对于输入矩阵
的梯度,并使其最小,那么本质上我们就是求得“什么样的
能够使交叉熵最小”。
很显然,这对训练神经网络模型意义重大。
这里,我们表示交叉熵函数如下:
那么对其求导能够得到(这里省略了求导过程):
这个结果说明,当预测值等于真实值时,梯度最小。
3. 推导包含一个隐藏层神经网络的梯度
推导包含一个隐藏层神经网络的梯度,也就是说得到
,其中
,是这个神经网络的测量函数。这里的x是输入层的输入。
而上面第二部分,我们是对
求导,是输出层的输入。
为了说清楚,用下图举例说明:
首先,让我们弄清楚一件事,x是输入层的输入,那么从输入层到输出层,x经历了什么呢?
见以下分割线包含的部分。
====================================================================
首先经历从输入层到隐藏层:先给x加权重和偏差:
把
代入激活函数(这里sigmoid是隐藏层的激活函数(activation function)得到
然后,从隐藏层的输出h到输出层的输出
经历了如下过程:同样增加权重和偏差:
把
代入激活函数(这里是softmax函数,见自然语言处理(NLP)基础概念(二):Softmax介绍及其python实现)就得到了我们的预测值,也就是输出层的输出值
。
====================================================================
现在,我们要做的是,求测量函数对于x的梯度。而第二节中,我们求的是测量函数对于输出层的输入的梯度,也就是对于
的梯度。
现在我们开始推导:求
比较困难,而求
比较简单,所以我们利用链式法则(chain rule)得到:
但是这里又遇到了同样的问题,求
比较困难。我们可以继续应用链式法则,这样一直应用,最后得到:
下面分别求这4个偏导数:
其中
我们已经在第二部分求过了,是
。
根据矩阵求导法则
是
求 h相对于
的导数,这部分我们也已经得到了答案(见本文的第一部分),所以
同样根据矩阵求导法则,
是
问题迎刃而解!
这里,最重要的问题来了!!!求
的意义是什么???
问题的答案也显而易见,我们想知道,输入值是什么时,CE最小。
这也是测试函数CE的存在意义(回想一下测试函数的定义),输入等于什么的情况下,预测值
和真实值
之间的‘距离’最小。
神经网络也是利用这一点优化模型参数,从而得到最好的结果。
4. 神经网络参数的讨论
Dx代表输入的维度, H代表隐藏层的神经元数量, Dy代表输出的维度,那么下图的神经网络中参数数量是多少呢?
先给出答案:(Dx+1)*H+(H+1)*Dy
首先,这是一个只有一层隐藏层的神经网络。
输入的维度是Dx的话,那么输入层的数量就是Dx+1,这里为什么+1,因为1乘以一个数就是一个实数,代表
中的b。
我们想知道的是参数的数量,也就是有多少W,有多少b。
很显然,Dx是多少就有多少W,b只有一个,所以对于一个神经元来说参数数量是Dx+1。
现在有H个神经元,那么参数数量就是(Dx+1)*H。
同样的道理,我们可以得出,从隐藏层到输出层有(H+1)*Dy个参数。
所以参数的总数量就是(Dx+1)*H+(H+1)*Dy。
这里还可以再进一步推导,如果我们有N个隐藏层,每一层有(1~m)个神经元呢?读者可以安装上面的思路自行推导。
5. sigmoid函数及其梯度的python实现
下面是sigmoid函数以及求其梯度的python代码,代码包括三个函数。
其中sigmoid(x)用来对x进行sigmoid。
sigmoid_grad(x)用来计算sigmoid函数的梯度。这里有个技巧,就是利用了文章第一部分的结论。
test_sigmoid_basic()函数提供了几个例子用于测试前两个函数。
脚本可以被直接执行:
if __name__ == "__main__":
上面这行代码的作用是“让你写的脚本模块既可以导入到别的模块中用,另外该模块自己也可执行。”(“Make a script both importable and executable”)如果还是不太明白可以看这篇文章。
import numpy as np
def sigmoid(x):
"""Compute the sigmoid function for the input here.Arguments:x -- A scalar or numpy array.Return:s -- sigmoid(x)"""
s = 1./(1.+np.exp(-x))
return s
def sigmoid_grad(s):
"""Compute the gradient for the sigmoid function here. Note thatfor this implementation, the input s should be the sigmoidfunction value of your original input x.Arguments:s -- A scalar or numpy array.Return:ds -- Your computed gradient."""
ds = s*(1-s)
return ds
def test_sigmoid_basic():
"""Some simple tests to get you started.Warning: these are not exhaustive."""
print "Running basic tests..."
x = np.array([[1, 2], [-1, -2]])
f = sigmoid(x)
g = sigmoid_grad(f)
print f
f_ans = np.array([
[0.73105858, 0.88079708],
[0.26894142, 0.11920292]])
assert np.allclose(f, f_ans, rtol=1e-05, atol=1e-06)
print g
g_ans = np.array([
[0.19661193, 0.10499359],
[0.19661193, 0.10499359]])
assert np.allclose(g, g_ans, rtol=1e-05, atol=1e-06)
print "You should verify these results by hand!\n"
if __name__ == "__main__":
test_sigmoid_basic();
6. 梯度检查器的python实现
为了方便以后检查梯度是否正确,利用导数的定义对梯度进行验证,以下是python实现:
import numpy as np
import random
# First implement a gradient checker by filling in the following functions
def gradcheck_naive(f, x):
""" Gradient check for a function f.Arguments:f -- a function that takes a single argument and outputs thecost and its gradientsx -- the point (numpy array) to check the gradient at"""
rndstate = random.getstate()
random.setstate(rndstate)
fx, grad = f(x) # Evaluate function value at original point
h = 1e-4 # Do not change this!
# Iterate over all indexes in x
# flags=['multi_index'] can get all dimension index
# Set op_flags to make array readable and writable.
it = np.nditer(x, flags=['multi_index'], op_flags=['readwrite'])
while not it.finished:
ix = it.multi_index
# Try modifying x[ix] with h defined above to compute
# numerical gradients. Make sure you call random.setstate(rndstate)
# before calling f(x) each time. This will make it possible
# to test cost functions with built in randomness later.
random.setstate(rndstate)
tmp1 = np.copy(x)
tmp1[ix] = tmp1[ix] + h
f1, _ = f(tmp1)
random.setstate(rndstate)
tmp2 = np.copy(x)
tmp2[ix] = tmp2[ix] - h
f2, _ = f(tmp2)
numgrad = (f1 - f2) / (2 * h)
print(numgrad,grad[ix])
print(max(1,abs(numgrad), abs(grad[ix])))
# Compare gradients
reldiff = abs(numgrad - grad[ix]) / max(1, abs(numgrad), abs(grad[ix]))
if reldiff > 1e-5:
print "Gradient check failed."
print "First gradient error found at index%s" % str(ix)
print "Your gradient:%f\tNumerical gradient:%f" % (
grad[ix], numgrad)
return
it.iternext() # Step to next dimension
print "Gradient check passed!"
def sanity_check():
"""Some basic sanity checks."""
quad = lambda x: (np.sum(x ** 2), x * 2)
print "Running sanity checks..."
gradcheck_naive(quad, np.array(123.456)) # scalar test
gradcheck_naive(quad, np.random.randn(3,)) # 1-D test
gradcheck_naive(quad, np.random.randn(4,5)) # 2-D test
print ""
if __name__ == "__main__":
sanity_check()
这里包括两个函数,gradcheck_naive(f, x)用于检查梯度,def sanity_check()用于生成几个例子进行测试。
gradcheck_naive(f, x)的主要思想是‘用定义求出来的导数’和‘用我们自己写的函数求出来的导数’进行对比,如果结果不相等(代码中是比对结果> 1e-5)就报错。
这里我们利用一个简单的函数
进行测试,我们知道它的导数是
。
这个测试函数的python实现为:quad = lambda x: (np.sum(x ** 2), x * 2)
如果不明白lambda的作用,看这里。
在gradcheck_naive(f, x)函数中,我们首先定义一个极小的变量h = 1e-4
那么根据导数的定义,f(x)=(f(x+h)-f(x-h))/2h
上述定义的python实现是下面这段代码:
random.setstate(rndstate)
tmp1 = np.copy(x)
tmp1[ix] = tmp1[ix] + h
f1, _ = f(tmp1)
random.setstate(rndstate)
tmp2 = np.copy(x)
tmp2[ix] = tmp2[ix] - h
f2, _ = f(tmp2)
numgrad = (f1 - f2) / (2 * h)
print(numgrad,grad[ix])
print(max(1,abs(numgrad), abs(grad[ix])))
最后我们比较定义和函数求出来的值是否一致,代码如下:
reldiff = abs(numgrad - grad[ix]) / max(1, abs(numgrad), abs(grad[ix]))
注意,这里为什么有个max(1, abs(numgrad), abs(grad[ix]))?
因为如果 abs(numgrad), abs(grad[ix])是小于1的数的话,除以一个小于一的数使结果变得更大,即便这两个值真的很接近。
7. 神经网络的正向和反向传递及其python实现
#!/usr/bin/env python
import numpy as np
import random
from q1_softmax import softmax
from q2_sigmoid import sigmoid, sigmoid_grad
from q2_gradcheck import gradcheck_naive
def forward_backward_prop(data, labels, params, dimensions):
"""Forward and backward propagation for a two-layer sigmoidal networkCompute the forward propagation and for the cross entropy cost,and backward propagation for the gradients for all parameters.Arguments:data -- M x Dx matrix, where each row is a training example.labels -- M x Dy matrix, where each row is a one-hot vector.params -- Model parameters, these are unpacked for you.dimensions -- A tuple of input dimension, number of hidden unitsand output dimension"""
### Unpack network parameters (do not modify)
ofs = 0
Dx, H, Dy = (dimensions[0], dimensions[1], dimensions[2])
W1 = np.reshape(params[ofs:ofs+ Dx * H], (Dx, H))
ofs += Dx * H
b1 = np.reshape(params[ofs:ofs + H], (1, H))
ofs += H
W2 = np.reshape(params[ofs:ofs + H * Dy], (H, Dy))
ofs += H * Dy
b2 = np.reshape(params[ofs:ofs + Dy], (1, Dy))
### YOUR CODE HERE: forward propagation
h = sigmoid(np.dot(data,W1) +b1)
pred = sigmoid(np.dot(h, W2) + b2)
cost = (-1) * np.sum(labels * np.log(pred) + (1 - labels) * np.log(1 - pred))
### END YOUR CODE
### YOUR CODE HERE: backward propagation
dout = pred - labels
dh = np.dot(dout, W2.T) * sigmoid_grad(h)
gradW2 = np.dot(h.T, dout)
gradb2 = np.sum(dout, 0)
gradW1 = np.dot(data.T, dh)
gradb1 = np.sum(dh, 0)
### END YOUR CODE
### Stack gradients (do not modify)
grad = np.concatenate((gradW1.flatten(), gradb1.flatten(),
gradW2.flatten(), gradb2.flatten()))
return cost, grad
def sanity_check():
"""Set up fake data and parameters for the neural network, and test usinggradcheck."""
print "Running sanity check..."
N = 20
dimensions = [10, 5, 10]
data = np.random.randn(N, dimensions[0]) # each row will be a datum
# creat one-hot vector
labels = np.zeros((N, dimensions[2]))
for i in xrange(N):
labels[i, random.randint(0,dimensions[2]-1)] = 1
params = np.random.randn((dimensions[0] + 1) * dimensions[1] + (
dimensions[1] + 1) * dimensions[2], )
gradcheck_naive(lambda params:
forward_backward_prop(data, labels, params, dimensions), params)
if __name__ == "__main__":
sanity_check()