求导链式法则+反向计算
训练过程:通过前向传播得到损失,反向传播得到梯度( ∂L∂w ),最后用梯度更新权重。
那么 ∂L∂x=∂L∂z∂z∂x , ∂L∂y=∂L∂z∂z∂y , ∂L∂w=∂L∂z∂z∂w 。通过这种方式,我们就可以从输出的节点开始,往输入节点的方向计算损失关于各个节点权重的导数值,从而用梯度下降法进行权重的迭代,利用最优化减少损失。
计算如下的一个式子:
通过添加中间变量,进行的正向传播。
x = 3 # example values
y = -4
# forward pass
sigy = 1.0 / (1 + math.exp(-y)) # sigmoid in numerator #(1)
num = x + sigy # numerator #(2)
sigx = 1.0 / (1 + math.exp(-x)) # sigmoid in denominator #(3)
xpy = x + y #(4)
xpysqr = xpy**2 #(5)
den = sigx + xpysqr # denominator #(6)
invden = 1.0 / den #(7)
f = num * invden # done! #(8)
反向传播计算各个变量权重的导数
# backprop f = num * invden
dnum = invden # gradient on numerator #(8)
dinvden = num #(8)
# backprop invden = 1.0 / den
dden = (-1.0 / (den**2)) * dinvden #(7)
# backprop den = sigx + xpysqr
dsigx = (1) * dden #(6)
dxpysqr = (1) * dden #(6)
# backprop xpysqr = xpy**2
dxpy = (2 * xpy) * dxpysqr #(5)
# backprop xpy = x + y
dx = (1) * dxpy #(4)
dy = (1) * dxpy #(4)
# backprop sigx = 1.0 / (1 + math.exp(-x))
dx += ((1 - sigx) * sigx) * dsigx # Notice += !! See notes below #(3)
# backprop num = x + sigy
dx += (1) * dnum #(2)
dsigy = (1) * dnum #(2)
# backprop sigy = 1.0 / (1 + math.exp(-y))
dy += ((1 - sigy) * sigy) * dsigy #(1)
# done! phew
向量化反向传播(层与层之间):
# forward pass
W = np.random.randn(5, 10)
X = np.random.randn(10, 3)
D = W.dot(X)
# now suppose we had the gradient on D from above in the circuit
dD = np.random.randn(*D.shape) # same shape as D
dW = dD.dot(X.T) #.T gives the transpose of the matrix
dX = W.T.dot(dD)
Tips.
激活函数在神经元的中间,将输入转化为输出。 out=f(XW+b) ,其中的f就是激活函数。
优缺点:
梯度弥散:由于很多原因,比如说层数太多等,导致反向传播的时候梯度变小,前面权重进行更新的时候变化非常小,以至于不能够从样本中进行有效的学习。
优缺点:
优缺点:
优缺点:同ReLU,但是改进了5和6,神经元就算在输入小于0的区域也不会梯度消失,不会有非激活状态
优缺点:
优缺点:统一了ReLU和Leaky ReLU,在神经元的计算上多了非线性的元素,但是计算参数数量多了一倍。
通常的网络按层来组织,每层中都有神经元,没有回路。这种层的组织结构可以很轻松的进行向量矩阵的运算,容易理解和实现,并且效率高。全连接层(fully-connected layer)是很常见的一种结构,表示相邻两层之间的神经元两两连接。下面是两个例子。
命名规则,N-层神经网络,N包括隐藏层和输出层,不包括输入层。
输出层不像神经网络的其他层,一般没有激活函数,因为最后输出层的输出在分类问题中是代表类的分数,在回归问题中是实值数。
神经网络的规模,通常用参数的数量,或者神经元的数量来衡量网络的规模。例如在图的例子中,第一个网络有 [3×4]+[4×2]=20 个权重,4+2个偏置,总共26个参数,或者有4+2个神经元(不包括输入层)。
网络规模越大,容量越大,能表示的不同的函数越多。如下是一个二分类的例子,一个隐藏层。
从上图中可以看到,随着隐藏神经元的增大,可以代表越复杂的函数,但是也有过拟合的风险(拟合太好,以至于把训练数据的噪声给拟合进去了),如上图中的右图,将异常点拟合进去,可能会导致模型的泛化能力差,也就是在测试集上表现差。
在实际使用中,尽量使用大的网络,因为过拟合可以通过正则化等方法修补,但是网络太小导致不能表达需要的函数是不能修补的。
对于一个原始数据X[N,D],其中N是样本数量,D是特征数量。可以做如下的操作:
0均值化
X -= np.mean(X, axis=0)
有两种处理方式,以CIFAR-10为例,其中的图片是[32,32,3]。
归一化(normalized data)。归一化的方法很多,不一定必须是如下的方法,比如最大最小值归一化。图像处理中很少使用归一化,因为像素的尺度是一致的,都是[0,255]
X /= np.std(X, axis=0)
根据协方差,可以使用PCA主成份分析,或者白化,降低特征之间的相关性,使特征具有相同的方差。在机器学习中比较常见,在图像处理中比较少
初始化为0显然不是一个很好的选择,因为这样那么对于每个神经元,输出和梯度都是一样的,那么他们做的操作都是一样的。
生成小的随机数来初始化权重,比如说N(0,0.01)
W = 0.01*np.random.randn(D, H)
这种方法对于小网络适用,但是对于层数比较深的网络不行。每次乘上W,权重太小(0.01),会导致输入越来越小,梯度也会越来越小,从而导致梯度弥散;权重过大(1),会导致输入越来越大,如果使用tanh或sigmoid,过大的输入会让导数接近0,从而梯度弥散,无法训练权重。
Xavier,确保在xw前后数据的方差一致,那么权重如下,其中n是输入的维度数 s=∑niwixi 。在tanh中有效,但是在ReLU中输入到0更快,需要加上因子2
w = np.random.randn(n) / sqrt(n)
w = np.random.randn(n) * sqrt(2.0/n) # ReLU
一般设置在全连接层或者卷积层的后面和非线性层(激活函数)的前面。
Batch Normalization论文地址
论文的中文整理博客
在测试的时候依然使用如下式子:
反向传播的导数:
作者在文章中说应该把BN放在激活函数之前,这是因为Wx+b具有更加一致和非稀疏的分布。但是也有人做实验表明放在激活函数后面效果更好。这是实验链接,里面有很多有意思的对比实验。
损失是为了量化预测值和实际值之间的差距,差距越大损失越大。数据损失L是所有样本损失的平均, L=1N∑iLi ,缩写f是网络的输出,我们的预测值。对于不同的问题有集中不同的输出。
只有一个正确答案的分类问题,常用的有svm和softmax损失函数。
大量类别的,比如说英文字典,或者ImageNet包含了22000个类别。可以使用分层的Softmaxpdf here。先创建一棵类别树,每一个节点用于softmax,树的结构对分类效果影响很大,需要具体问题具体分析。
属性分类,一个样本有好几个分类属性,也有可能一个也没有。有两种计算方法,其一是给每个属性单独的设置一个二元分类器(类似于svm)。
回归问题是预测实值,例如通过房子的面积来预测房子的价格。常见的通过L1或者L2范式来衡量预测值和实际值之间的差距。
Note:L2损失比Softmax损失更难优化,因为需要对样本的每个值都准确的预测一个正确值,包括异常值。在遇到一个回归问题的时候,首先考虑是否可以转化为分类问题,比如说给某个产品星级评分1-5星,用5个分类器可能比回归更好。
用于控制网络的容量,不让模型过于复杂,从而防止过拟合,增加模型的泛化能力。
最常用的正则化形式。在计算损失loss的时候,加上 12λW2 作为最后的损失,在反向传播计算权重w导数的时候,根据求导法则加上对正则项的导数 λ∗W 。乘上0.5是为了求导的时候格式好看。L2正则项是为了能够充分利用各个特征项。
给损失加上 λ|w| 。也可以同时加上L1和L2正则项。L1正则化项得到的权重会更加稀疏,起到一个类似于特征选择的效果,选择重要的特征进行预测,而滤去噪声。如果没有做过特征选择,那么用L1会有更好的效果。
最大范数约束主要思路是安照正常进行update,只不过每次更新之后检查其范数是否大约了设定值 ||w⃗ ||2<x 的经典取值是3或者4。
一种很有效、简单的正则化技术,Dropout: A Simple Way to Prevent Neural Networks from Overfitting,基本思想是训练的时候选择一部分的神经元工作。
神经元以P或者0的概率决定是否被使用,测试的时候全部的神经元都是可用的。
一个3层神经网络的例子
""" Vanilla Dropout: Not recommended implementation (see notes below) """
p = 0.5 # probability of keeping a unit active. higher = less dropout
def train_step(X):
""" X contains the data """
# forward pass for example 3-layer neural network
H1 = np.maximum(0, np.dot(W1, X) + b1)
U1 = np.random.rand(*H1.shape) < p # first dropout mask
H1 *= U1 # drop!
H2 = np.maximum(0, np.dot(W2, H1) + b2)
U2 = np.random.rand(*H2.shape) < p # second dropout mask
H2 *= U2 # drop!
out = np.dot(W3, H2) + b3
# backward pass: compute gradients... (not shown)
# perform parameter update... (not shown)
def predict(X):
# ensembled forward pass
H1 = np.maximum(0, np.dot(W1, X) + b1) * p # NOTE: scale the activations
H2 = np.maximum(0, np.dot(W2, H1) + b2) * p # NOTE: scale the activations
out = np.dot(W3, H2) + b3
上面我们没有对输入层进行dropout(也可以做),输出层加入了p这一乘积项,这是因为训练的时候某个神经单元被使用的概率是p,如果他的输出是x那么他训练时输出值的期望只是px,所以在预测阶段要乘以p,但是并不推荐这样做。因为在预测阶段进行操作无异于增加了预测时间。我们用inverted dropout来解决这个问题。在预测时不再乘以p,而是在预测时除以p。
"""
Inverted Dropout: Recommended implementation example.
We drop and scale at train time and don't do anything at test time.
"""
p = 0.5 # probability of keeping a unit active. higher = less dropout
def train_step(X):
# forward pass for example 3-layer neural network
H1 = np.maximum(0, np.dot(W1, X) + b1)
U1 = (np.random.rand(*H1.shape) < p) / p # first dropout mask. Notice /p!
H1 *= U1 # drop!
H2 = np.maximum(0, np.dot(W2, H1) + b2)
U2 = (np.random.rand(*H2.shape) < p) / p # second dropout mask. Notice /p!
H2 *= U2 # drop!
out = np.dot(W3, H2) + b3
# backward pass: compute gradients... (not shown)
# perform parameter update... (not shown)
def predict(X):
# ensembled forward pass
H1 = np.maximum(0, np.dot(W1, X) + b1) # no scaling necessary
H2 = np.maximum(0, np.dot(W2, H1) + b2)
out = np.dot(W3, H2) + b3
bias不与输入变量直接相乘,不能控制数据对最终目标的影响,所以表示一般不用regularization,但是它的数量相对于w很少,约束下也不会有太大的影响。
用的很少,对不同的层使用不同的正则化方法。
前面介绍了网络的连接结构、数据预处理以及损失函数,这一节介绍学习参数的过程和超参数调参。
通过比较网络中公式计算的梯度和数值法得到的梯度是否相似,来判断网络的梯度部分是否正确。
对于不同的问题,梯度的尺度不一样,所以用相对误差来衡量两者的误差。
需要注意的是网络越深误差越大,如果网络有10层,那么1e-2也可以接受。
有很多图可以实时查看网络的训练状态。下面的图的x轴单位是epochs,表示数据集迭代的次数。因为iterations(反向传播次数)取决于batch size,所以epochs更有意义。
学习率太小看着像是线性的,高的学习率刚开始是指数型的,太高的学习率最后的结果会不好。
通过验证集训练集的准确率,可以看模型是否过拟合。
模型过拟合可以增加正则化、增加数据。如果验证集准确率和训练集准确率差不多,那么表示模型规模不够,通过增加参数数量来增大模型规模。
观察权重更新的快慢可以知道学习速度learning rate 设定的大小是否合适,下面是权重更新率计算的过程,一般来说在值小于1e-3的时候说明学习效率设定的太小,大于的时候说明设置的太大。
# assume parameter vector W and its gradient vector dW
param_scale = np.linalg.norm(W.ravel())
update = -learning_rate*dW # simple SGD update
update_scale = np.linalg.norm(update.ravel())
W += update # the actual update
print update_scale / param_scale # want ~1e-3
初始化出错会降低甚至停止学习过程,可以通过网络中每一层的激活输出和梯度的直方图来查看。比如说看到所有激活函数输出都为0,或者对于tanh激活函数,输出不是-1就是1等。
如果是在处理图片可以将第一层可视化:
下图第二个是一个非常合理的结果:特征多样,比较干净、平滑。但是第一个图很粗糙,显示不出底层特征,可能是因为网络不收敛或者学习速率设置不好或者是因为惩罚因子设置的太小。
本节介绍使用梯度来对参数进行更新的方法。
Vanilla update:最简单的方法,往负梯度方向更新参数。
# Vanilla update
x += - learning_rate * dx
learning_rate学习率是一个超参数。
Momentum update:
# Momentum update
v = mu * v - learning_rate * dx # integrate velocity
x += v # integrate position
其中v初始值是0,mu是一个超参数,一般设为0.9,也可以通过交叉验证从 [0.5, 0.9, 0.95, 0.99]中选取,从上面的迭代过程中我们可以看到,相对于传统的沿梯度方向更新的方法,这里的更新是在之前的基础上的更新,如果最后dx变为0,经过多次乘以mu之后v也会变得非常小,也就是最后的停车,它保留了自然界中的惯性的成分,因此不容易在局部最优解除停止,而且中间有加速度所以会加速运算的过程。
Nesterov Momentum:动量法的改进版本,在凸函数收敛上有很好的理论保证,实际使用也比标准的动量法稍微好一点。之前我们采用 v=mu∗v−learningrate∗dx 的方法计算增量,其中的dx还是当前的x,但是我们已经知道了,下一刻的惯性将会带我们去的位置,所以现在我们要用加了mu*v之后的x,来更新位置,下面的图很形象:
x_ahead = x + mu * v
# evaluate dx_ahead (the gradient at x_ahead instead of at x)
v = mu * v - learning_rate * dx_ahead
x += v
用类似于前面SGD和动量的方法更新:
v_prev = v # back this up
v = mu * v - learning_rate * dx # velocity update stays the same
x += -mu * v_prev + (1 + mu) * v # position update changes form
在训练网络的时候,将学习速率进行退火衰减处理,一般会有些帮助。学习率衰减太快,会浪费太多时间,衰减太慢曲线波动大,而且达不到最优点。一般有3种衰减方法:
1比较多一点,因为超参数可以解释。如果有足够的资源,学习率可以小一点训练更多时间。
Second order methods是基于牛顿法的二次方法,它的迭代公式如下:
可是,hessian矩阵的计算成本太高,深度学习中的网络有比较大,如果有100万个参数,海森矩阵的size为 [1,000,000 x 1,000,000], 需要3725G的RAM.
虽然现在有近似于hessian矩阵的方法例如L-BFGS,但是它需要在整个训练集中进行训练, 这也使得L-BFGS or similar second-order methods等方法在大规模学习的今天不常见的原因。如何是l-bfgs能像sgd一样在mini-batches上比较好的应用也是现在一个比较热门的研究领域。
前面的方法有的是通过别的超参数来调整学习率,以下几种是自适应调整学习率的方法
Adagrad:借鉴L2正则化,只是不是调节W,而是梯度。
# Assume the gradient dx and parameter vector x
cache += dx**2
x += - learning_rate * dx / (np.sqrt(cache) + eps)
其中eps一般取值1e-4 到1e-8,以避免分母出现0。但是这种方法用在深度学习中往往会学习的波动较大,并且使过早的停止学习。
RMSprop:Adagrad的改进,通过移动平均来减小波动。
cache = decay_rate * cache + (1 - decay_rate) * dx**2
x += - learning_rate * dx / (np.sqrt(cache) + eps)
改变就是分母中的cache,其中decay_rate是一个超参数,典型取值是[0.9, 0.99, 0.999]。cache的改变使这种方法有adagrad根据参数梯度自调整的优点,也克服adagrad单调减小的缺点。
Adam:有点像RMSProp+momentum
简单实现
m = beta1*m + (1-beta1)*dx
v = beta2*v + (1-beta2)*(dx**2)
x += - learning_rate * m / (np.sqrt(v) + eps)
论文中推荐eps = 1e-8, beta1 = 0.9, beta2 = 0.999. 完整版的程序还包括了一个偏差修正值,以弥补开始时m,v初始时为零的现象。
实际使用中,推荐Adam,他会比RMSProp效果好些. 也可以尝试SGD+Nesterov Momentum。另外如果能够允许全局的更新时可以试试L-BFGS。
更新版本,一般是用这个
# t is your iteration counter going from 1 to infinity
m = beta1*m + (1-beta1)*dx
mt = m / (1-beta1**t)
v = beta2*v + (1-beta2)*(dx**2)
vt = v / (1-beta2**t)
x += - learning_rate * mt / (np.sqrt(vt) + eps)
常见的超参数:
还有其他很多相对不敏感的超参数,例如参数自适应学习方法、动量及其策略的设置。下面介绍一些超参数搜索的注意点:
在测试阶段,综合一些神经网络的结果,来提高性能。模型越多样,提升效果越好。
模型集成的缺点是在测试集上太耗时。
http://cs231n.github.io/neural-networks-case-study/
- 视频课程地址
- 官方资料网站
- 课后作业参考
- 神经网络激活函数
- 训练深度神经网络尽量使用zero-centered数据
- Vectorized、PCA和Whitening
- 讲义总结:梯度检验 参数更新 超参数优化 模型融合博客
- 参数更新方法博客
- Batch Normalization论文地址
- 论文的中文整理博客