参考资料:
deeplearning_ai_book
deeplearning.ai_JupyterNotebooks
GitHub另一个仓库
损失函数
又称误差函数,用于衡量算法的运行情况: L ( y ^ , y ) L(\hat y,y) L(y^,y),常用形式:
为什么不用平方差或者平方差的一半?
推导:
损失函数是在单个训练样本中定义的,衡量的是算法在单个训练样本中表现如何。为了衡量算法在全部训练样本上的表现如何,我们需要定义一个算法的代价函数,算法的代价函数是对个样本的损失函数求和然后除以m
梯度下降法怎么走到凸函数最小值点
(1)当代价函数 J ( w ) J(w) J(w)只有一个参数 w w w时,此时可用一维曲线代替多维曲线
其中, : = := :=表示更新参数; α \alpha α表示学习率,用来控制步长 d J ( w ) d w \dfrac{dJ(w)}{dw} dwdJ(w),步长 d J ( w ) d w \dfrac{dJ(w)}{dw} dwdJ(w)即为函数 J ( w ) J(w) J(w)对 w w w的求导。
当初始化 w 0 w_0 w0大于最优值 w ∗ w^* w∗:
此时函数 J ( w ) J(w) J(w)于 w 0 w_0 w0处求导结果 d J ( w ) d w \dfrac{dJ(w)}{dw} dwdJ(w)为正数(即该处函数曲线斜率为正值),而 w : = w − α d J ( w ) d w w := w- \alpha\dfrac{dJ(w)}{dw} w:=w−αdwdJ(w),即 w w w值变小,该点向左走,直至逼近最小点。
当初始化 w 0 w_0 w0小于最优值 w ∗ w^* w∗:
此时函数 J ( w ) J(w) J(w)于 w 0 w_0 w0处求导结果 d J ( w ) d w \dfrac{dJ(w)}{dw} dwdJ(w)为负数(即该处函数曲线斜率为负值,而 w : = w − α d J ( w ) d w w := w- \alpha\dfrac{dJ(w)}{dw} w:=w−αdwdJ(w),即 w w w值变大,该点向右走,直至逼近最小点。
(2)当代价函数 J ( w , b ) J(w,b) J(w,b)有两个参数 w , b w,b w,b时
计算图解释了为什么我们前向和反向传输的方式组织计算过程
在上图的例子中,我们确定了参数 a , b , c a,b,c a,b,c的值后,可得出中间值 u , v u,v u,v的值。
通过前向计算(蓝色箭头,从左往右),可计算出函数 J ( a , b , c ) J(a,b,c) J(a,b,c)的值;
通过反向计算(红色箭头,从右到左),可以计算中间值 u , v u,v u,v和参数 a , b , c a,b,c a,b,c的导数。
这里的计算过程也就是我们求多元函数偏导时所说的**“链式法则”**
但是,训练逻辑回归模型不仅仅只有一个训练样本,而是有个训练样本的整个训练集。
损失函数的定义:
即要对最后 J , w 1 , w 2 , b J,w_1,w_2,b J,w1,w2,b求平均:
代码流程:
J=0;dw1=0;dw2=0;db=0; # 初始化都为0
for i = 1 to m
z(i) = wx(i)+b;
a(i) = sigmoid(z(i));
J += -[y(i)log(a(i))+(1-y(i))log(1-a(i));
dz(i) = a(i)-y(i);
dw1 += x1(i)dz(i);
dw2 += x2(i)dz(i);
db += dz(i);
J/= m;
dw1/= m;
dw2/= m;
db/= m;
w=w-alpha*dw
b=b-alpha*db
但其中的for循环会使得算法效率非常低,故使用特征向量化来提高算法效率。
如上图所示,向量化版本花费了1.5毫秒,非向量化版本的for循环花费了大约几乎500毫秒,非向量化版本多花费了300倍时间。
当我们在写神经网络程序时,或者在写逻辑(logistic)回归,或者其他神经网络模型时,应该避免写循环(loop)语句。虽然有时写循环(loop)是不可避免的,但是我们可以使用比如numpy的内置函数或者其他办法去计算。当你这样使用后,程序效率总是快于循环(loop)。
例子(numpy中向量的相关操作)
r = np.dot(a,b)
r = np.exp(a)
在同一时间内如何完成一个所有 m个训练样本的前向传播向量化计算:
import numpy as np
Z = np.dot(w.T,x)+b # 此处 python 自动将 b 扩展为(1,m)的向量
使用 d w 1 = x 1 ∗ d z dw_1 = x_1*dz dw1=x1∗dz计算 d w 1 dw_1 dw1, d w 2 = x 2 ∗ d z dw_2 = x_2*dz dw2=x2∗dz计算, d b = d z db = dz db=dz来计算 d b db db
此处遍历训练集的循环需要去掉:
可发现 d b = 1 m ∑ i = 1 m d z ( i ) db=\dfrac{1}{m}\displaystyle\sum_{i=1}^{m}dz^{(i)} db=m1i=1∑mdz(i),使用代码实现:
db = (1/m) * np.sum(dZ)
而 d w = 1 m ∗ X ∗ d z T dw = \dfrac{1}{m}*X*dz^T dw=m1∗X∗dzT,其中, X X X是一个行向量,即
使用代码实现:
dw = (1/m) * np.dot(X,dz.T)
然后使用 w 1 : = w 1 − α d w 1 w_1 := w_1-\alpha dw_1 w1:=w1−αdw1 更新 w 1 w_1 w1, 使用 w 2 : = w 1 − α d w 2 w_2 := w_1-\alpha dw_2 w2:=w1−αdw2 更新 w 2 w_2 w2, 使用 b : = b − α d b b:= b-\alpha db b:=b−αdb 更新 b b b。
此处对 w , b w,b w,b的更新仍需要for循环
广播原理:
如果两个数组的后缘维度的轴长度相符或其中一方的轴长度为1,则认为它们是广播兼容的。广播会在缺失维度和轴长度为1的维度上进行。
如例: 当 m ∗ n m*n m∗n的矩阵和 1 ∗ n 1*n 1∗n 的矩阵相加。在执行加法操作时,其实是将 1 ∗ n 1*n 1∗n 的矩阵复制成为 m ∗ n m*n m∗n 的矩阵,然后两者做逐元素加法得到结果
但是当我们写代码时不确定矩阵维度的时候,通常会对矩阵进行重塑来确保得到我们想要的列向量或行向量。重塑操作reshape
是一个常量时间的操作,时间复杂度是,它的调用代价极低。
尽量不将一维数组当作向量来运算
每次创建一个数组时,都让它成为一个列向量(n,1)或者行向量(1,n),那么其后的向量运算行为更容易被理解,也不会出现向量运算上的奇怪bug。
当不完全确定一个向量的维度(dimension)时,扔进一个断言语句(assertion statement)。
这些断言语句实际上是要去执行的,并且它们也会有助于为你的代码提供信息。所以不论你要做什么,不要犹豫直接插入断言语句。
暂时留白
np.dot
和vector1*vector2
的区别np.dot(vector1*vector2)
:即矩阵乘法,若vector1的shape为(x1*y1)
,vector2的shape为(x2,y2)
,则需要y1=x2
才能进行运算(可广播的情况除外),否则报错。结果矩阵的shape为(x1,y2)
vector1*vector2
:两个向量中每个元素对应相乘,需要两个向量的shape一模一样,若vector1的shape为(x1*y1)
,vector2的shape为(x2,y2)
,则需要x1=x2,y1=y2
才能进行运算(可广播的情况除外)否则报错。最后得出的结果向量的shape与vector1和vector2的一样。a = np.random.randn(4,3)
a
array([[ 0.76779236, 0.18444005, 1.21712122],
[ 0.38977012, -0.56798278, 0.39299285],
[ 0.04380387, 0.25158324, 0.225215 ],
[-0.61341907, 0.74052763, -0.59685251]])
b = np.random.randn(3,2)
b
array([[-1.00104395, 0.26388446],
[-0.26633242, 0.44364925],
[ 0.40333879, -0.55962817]])
a*b
Traceback (most recent call last):
File "", line 1, in <module>
ValueError: operands could not be broadcast together with shapes (4,3) (3,2)
np.dot(a,b) # 矩阵乘法
array([[-0.32680406, -0.39670016],
[-0.08039554, -0.36906072],
[-0.02001642, -0.00286278],
[ 0.17609917, 0.50067824]])
c = np.random.randn(4,3)
a*c # 按对应元素相乘
array([[-0.91545714, 0.15368263, 0.08580373],
[-0.36117068, 0.0390558 , 0.37478451],
[-0.05269544, -0.09970869, 0.35626428],
[-1.26493901, 0.87666868, -0.65915348]])
d = np.random.randn(4,1)
a*d # 广播,d横向复制三次后shape为(4,3)
array([[-0.9078016 , -0.21807324, -1.43906692],
[-0.07042533, 0.10262555, -0.07100762],
[ 0.07332278, 0.42112225, 0.37698475],
[ 0.35215146, -0.4251219 , 0.34264093]])
神经网络的计算单元:
该神经元中的计算与逻辑回归一样,分为两步:
(1) 计算 z 1 [ 1 ] z_1^{[1]} z1[1]
(2) 通过激活函数计算 a 1 [ 1 ] a_1^{[1]} a1[1]
向量化计算
sigmoid 函数:
二分类问题中,因为预测值应为0或1,故需让输出值 y ^ \hat y y^数值介于0和1之间,此时需使用sigmoid函数。处理其他问题时,基本不用该函数。
tanh函数:
事实上,tanh函数是sigmoid的向下平移和伸缩后的结果。在训练一个算法模型时,如果使用tanh函数代替sigmoid函数中心化数据,因为它的值域在-1和+1,这使得数据的平均值更接近0而不是0.5,故其效果总是优于sigmoid函数。
sigmoid函数和tanh函数两者共同的缺点:在 z z z特别大或者特别小的情况下,导数的梯度或者函数的斜率会变得特别小,最后就会接近于0,导致降低梯度下降的速度很慢
修正线性单元的函数(ReLu):
在该函数中,只要 z z z是正值,导数恒等于1,当 z z z是负值时,导数恒等于0。 z z z在ReLu的梯度一半都是0,但是,有足够的隐藏层使得z值大于0,所以对大多数的训练数据来说学习过程仍然可以很快。
一些选择激活函数的经验法则:如果输出是0、1值(二分类问题),则输出层选择sigmoid函数,然后其它的所有单元都选择Relu函数。
在 z z z的区间变动很大的情况下,激活函数的导数或斜率都远大于0,在程序实现就是一个if-else语句,而sigmoid函数需要进行浮点四则运算,在实践中,使用ReLu激活函数神经网络通常会比使用sigmoid或者tanh激活函数学习的更快。
sigmoid和tanh函数的导数在正负饱和区的梯度都会接近于0,这会造成梯度弥散。而Relu和Leaky ReLu函数大于0的部分都为常数,不会产生梯度弥散现象。(同时应该注意到的是,Relu进入负半区的时候,梯度为0,神经元此时不会训练,产生所谓的稀疏性,而Leaky ReLu不会有这问题)
一般来说,如果不确定用哪个激活函数,就使用ReLu或者Leaky ReLu,但具体问题具体分析,这也不是绝对的。
对称问题:若将权重都初始化为0,则神经网络里所有的隐含单元计算的都是同一个函数,所有的隐含单元就会对输出单元有同样的影响,即隐含单元是对称的。不管训练网络多久时间隐含单元仍然计算的是相同的函数。
打破对称:随机初始化权重,使用np.random.randn(n,m)*0.01
;偏移量b可以初始化为0,即np.zeros(n,1)
0.01
,而不是10
或100
:如果初始化常数太大,权重w就会很大或很小,那么激活值z就会很大或者很小,即停在激活函数(sigmoid或tanh)的平坦处,这些地方的梯度很小,也就意味着梯度下降会很慢,因此学习就会很慢。深度学习符号
向量化前:
w [ l ] . s h a p e = d w [ l ] . s h a p e : ( n [ l ] , n [ l − 1 ] ) , 即 ( 该 层 维 数 , 前 一 层 维 数 ) b [ l ] . s h a p e = d b [ l ] . s h a p e : ( n [ l ] , 1 ) , 即 ( 该 层 维 数 , 1 ) z [ l ] = w [ l ] a [ l − 1 ] + b [ l ] , z [ l ] . s h a p e = d z [ l ] . s h a p e : ( n [ l ] , 1 ) a [ l ] = g [ l ] ( z [ l ] ) , a [ l ] . s h a p e = d a [ l ] . s h a p e : ( n [ l ] , 1 ) ( 输 入 x 即 为 a [ 0 ] ) w^{[l]}.shape = dw^{[l]}.shape:(n^{[l]},n^{[l-1]}),即(该层维数,前一层维数)\\ b^{[l]}.shape = db^{[l]}.shape:(n^{[l]},1),即(该层维数,1)\\ z^{[l]} = w^{[l]}a^{[l-1]}+b^{[l]},z^{[l]}.shape = dz^{[l]}.shape:(n^{[l]},1)\\ a^{[l]} = g^{[l]}(z^{[l]}),a^{[l]}.shape = da^{[l]}.shape:(n^{[l]},1)(输入x即为a^{[0]}) w[l].shape=dw[l].shape:(n[l],n[l−1]),即(该层维数,前一层维数)b[l].shape=db[l].shape:(n[l],1),即(该层维数,1)z[l]=w[l]a[l−1]+b[l],z[l].shape=dz[l].shape:(n[l],1)a[l]=g[l](z[l]),a[l].shape=da[l].shape:(n[l],1)(输入x即为a[0])
向量化后: w w w和 b b b的维度不变, z , a z,a z,a的维度变化
m 为 训 练 集 大 小 : Z [ l ] . s h a p e = d Z [ l ] . s h a p e : ( n [ l ] , m ) , A [ l ] . s h a p e = d A [ l ] . s h a p e : ( n [ l ] , m ) , A [ 0 ] = X . s h a p e : ( n [ l ] , m ) m为训练集大小:\\ Z^{[l]}.shape = dZ^{[l]}.shape : (n^{[l]},m),\\ A^{[l]}.shape = dA^{[l]}.shape : (n^{[l]},m),\\ A^{[0]} = X.shape: (n^{[l]},m) m为训练集大小:Z[l].shape=dZ[l].shape:(n[l],m),A[l].shape=dA[l].shape:(n[l],m),A[0]=X.shape:(n[l],m)
Small:隐藏单元的数量相对较少
Deep:隐藏层数目比较多
深层的网络隐藏单元数量相对较少,隐藏层数目较多,如果浅层的网络想要达到同样的计算结果则需要指数级增长的单元数量才能达到。
怎么找到一个低偏差,低方差的框架?注意以下两点:
lambda
在python中是一个关键字,所以编码时通常写作lambd
,以避免冲突。从公式分析:
如果 λ \lambda λ设置得足够大,权重矩阵 W W W被设置为接近于0的值,直观理解就是把多隐藏单元的权重设为0,可近似为消除了这些隐藏单元的影响。此时被大大简化了的神经网络会变成一个很小的网络,小到如同一个逻辑回归单元(其实不然,实际上是该神经网络的所有隐藏单元依然存在,但是它们的影响变得更小了),可是深度却很大,它会使这个网络从过拟合的状态更接近左图的高偏差状态(high bias)。也就是说, λ \lambda λ的增大能带来方差减小的效果。
从激活函数角度分析:
λ变大 -> W变小 -> |Z|变小
如果|Z|始终在这个范围内,激活函数最后可近似为线性函数,整个神经网络会计算离线性函数近的值,这个线性函数非常简单,并不是一个极复杂的高度非线性函数,不会发生过拟合。
使用正则化函数时,损失函数应使用:
如果你用的是原损失函数J(也就是第一个项),你可能看不到单调递减现象,为了调试梯度下降,请务必使用新定义的损失函数J(两项和),它包含第二个正则化项,否则函数可能不会在所有调幅范围内都单调递减。
1. 工作原理:dropout(随机失活)会遍历网络的每一层,并设置消除神经网络中节点的概率,之后我们会消除一些节点,然后删除掉从该节点进出的连线,最后得到一个节点更少,规模更小的网络,然后用backprop方法进行训练。
2. inverted dropout(反向随机失活)-- 较为常用:
keepProb = 0.8
# d3元素值为True或False
d3 = np.random.rand(a3.shape[0], a3.shape[1]) < keepProb
# 在相乘运算时,python会自动将True转化为1,False转化为0
# 所以可以选出概率大于keepProb的节点继续留下来进行计算
a3 = np.multiply(a3, d3)
a3 /= keepProb
第四行代码:我们假设网络的隐藏层,即a[2]有50个units,那么按照keepProb=0.8可以知道需要删除10个units,也就是说a[2]会减少20%,那么我们在计算下一层,即 z [ 3 ] = w [ 3 ] a [ 2 ] + b [ 3 ] z[3]=w[3]a[2]+b[3] z[3]=w[3]a[2]+b[3]时就会使得z[3]的期望值(均值)发生变化,为了不影响z[3]的期望值,我们需要用 w [ 3 ] a [ 2 ] ÷ 0.8 w[3]a[2]÷0.8 w[3]a[2]÷0.8来修正或弥补我们所需的20%。
3. 补充说明
为什么dropout可以起到正则化的作用呢:
应用情景:数据集有多维特征,而各个特征取值范围不同时,需要使用归一化加快算法运行。
实施步骤
如下例,假设数据集有两个特征x1,x2。其中 x 1 ∈ ( 0 , 5 ) , x 2 ∈ ( 1 , 2 ) x_1\in (0,5),x_2 \in (1,2) x1∈(0,5),x2∈(1,2)
零均值化: x : = x − μ x:=x- \mu x:=x−μ,即移动数据集。其中 μ = 1 m ∑ i = 1 m x ( i ) \mu = \dfrac{1}{m}\displaystyle\sum_{i=1}^{m}x^{(i)} μ=m1i=1∑mx(i)为一个向量。
归一化方差: x 1 x_1 x1方差比 x 2 x_2 x2的要大得多, x : = x / σ 2 x:=x/\sigma^2 x:=x/σ2,其中 σ = 1 m ∑ i = 1 m ( x ( i ) ) 2 \sigma = \dfrac{1}{m}\displaystyle\sum_{i=1}^{m}(x^{(i)})^2 σ=m1i=1∑m(x(i))2
注意:用相同的 μ , σ \mu,\sigma μ,σ来归一化测试集和训练集
假设每个权重矩阵
W [ l ] = [ 1.5 0 0 1.5 ] W^{[l]} = \begin{bmatrix} 1.5 & 0\\ 0 & 1.5 \end{bmatrix} W[l]=[1.5001.5]
最后计算结果就是 y ^ = 1. 5 ( L − 1 ) x \hat y = 1.5^{(L-1)}x y^=1.5(L−1)x,对于一个深度网络来说, L L L值越大,最后 y ^ \hat y y^呈指数爆炸式增长,增长比率为 1. 5 L 1.5^L 1.5L
相反,假设每个权重矩阵
W [ l ] = [ 0.5 0 0 0.5 ] W^{[l]} = \begin{bmatrix} 0.5 & 0\\ 0 & 0.5 \end{bmatrix} W[l]=[0.5000.5]
最后计算结果就是 y ^ = 0. 5 ( L − 1 ) x \hat y = 0.5^{(L-1)}x y^=0.5(L−1)x,对于一个深度网络来说, L L L值越大,最后 y ^ \hat y y^以指数级递减,递减比率为 0. 5 L 0.5^L 0.5L,此时梯度下降算法的步长会非常非常小,梯度下降算法将花费很长时间来学习。
为了预防z值过大或过小,n越大则需要w越小
步骤:
将参数 W [ l ] , b [ l ] , l ∈ ( 1 , L ) W^{[l]},b^{[l]},l \in (1,L) W[l],b[l],l∈(1,L)转换为向量 θ \theta θ,那么代价函数就是关于 θ \theta θ的一个函数 J ( θ ) J(\theta) J(θ)
将参数 d W [ l ] , d b [ l ] , l ∈ ( 1 , L ) dW^{[l]},db^{[l]},l \in (1,L) dW[l],db[l],l∈(1,L)转换为向量 d θ d\theta dθ,因为 d W [ l ] . s h a p e = W [ l ] . s h a p e , d b [ l ] . s h a p e = b [ l ] . s h a p e dW^{[l]}.shape = W^{[l]}.shape,db^{[l]}.shape = b^{[l]}.shape dW[l].shape=W[l].shape,db[l].shape=b[l].shape,所以 d θ . s h a p e = θ . s h a p e d\theta.shape = \theta.shape dθ.shape=θ.shape
怎么检验神经网络的梯度实施是否正确?该问题等价于“ d θ d\theta dθ和代价函数 J J J的梯度或坡度有什么关系”
使用双边误差计算 d θ a p p r o x [ i ] d\theta_{approx}[i] dθapprox[i]:
理论来说, d θ a p p r o x [ i ] ≈ d θ [ i ] = ∂ J ∂ θ i d\theta_{approx}[i] \approx d\theta[i] = \dfrac{\partial J}{\partial \theta_i} dθapprox[i]≈dθ[i]=∂θi∂J
验证这些向量是否彼此接近:
注意这里 ∣ ∣ d θ a p p r o x − d θ ∣ ∣ 2 ||d\theta_{approx}-d\theta||_2 ∣∣dθapprox−dθ∣∣2没有平方,它是误差平方之和,然后求平方根,得到欧式距离,然后用向量长度归一化,使用向量长度的欧几里得范数。分母只是用于预防这些向量太小或太大,分母使得这个方程式变成比率。
取 ϵ = 1 0 − 7 \epsilon = 10^{-7} ϵ=10−7来计算上一步的式子。