之前有过断断续续地学习深度学习的经历
对深度学习有一定的了解
包括激活函数,损失函数,卷积,池化这种基本概念
对CNN,RNN,ResNet都有一定的了解
去年参加的项目里还和队友一起做了个基于CNN的智能搜索引擎
(没记错的话还花里胡哨地用了点jieba分词)
不过当时才刚刚大二,知识体系漏洞很大,项目全靠带
现在再翻翻当时的源码都得费好大劲才能回想起来在写什么。。。
而想想自己到底学了点什么深度学习,又很难系统地总结出来,东一榔头西一棒,确实很多片面的知识点都会些,但又不深入
所以以此契机我决定从头好好梳理一遍深度学习,从最基础的概念开始补全知识漏洞,同时呢就当是对相关的知识也做一个复习(比如线性代数,概率论,python之类的)
话不多说,希望能在新一遍的学习中有所收获吧——
Logistic回归主要是适用于分类问题的算法
毕竟是入门笔记,这里就只对二元分类做简述并给出概念定义
其基本的线性回归形式为:
y = w T x + b y = w^{T}x + b y=wTx+b
注:该子标题中的Logistic回归与上述表达式略有差异,具体可以参考子标题向量化
当然,用最基础的数学知识来看,这里的 y 取得的是一系列的实值
甚至在理论上值域为 R
而我们期望得到的值域为
[ 0 , 1 ] [0, 1] [0,1]
即我们输入一个 x 后,我们需要知道一个概率区间
这就需要需要在外层嵌套函数,转变函数的值域
最理想的自然是单位阶跃函数,但单位阶跃函数一个缺点就是其不连续,不能保证可微的严格性,所以不能直接使用
所以这里需要对单位跃迁函数进行替换,
也就是sigmoid函数:
y ^ = 1 1 + e − x \widehat{y} = \frac{1}{1 + e^{-x}} y =1+e−x1
也就是说最后可以表示为:
y ^ = σ ( w T x + b ) \widehat{y} = \sigma (w^{T}x + b) y =σ(wTx+b)
其中
σ ( x ) = 1 1 + e − x \sigma (x) = \frac{1}{1 + e^{-x}} σ(x)=1+e−x1
此处我们定义:
y ^ = P ( y = 1 ∣ x ) \widehat{y} = P(y = 1 | x) y =P(y=1∣x)
而当我们在进行神经网络训练时,此时产生的 y’ 只能说是理论值,为了使这个理论值 y’ 和实际值 y 接近,我们需要定义一个损失函数loss去衡量 y’ 和 y 之间的误差:
注:推导过程可以参考周志华教授的《机器学习》,这里只写结论
L ( y ^ , y ) = − ( y l o g y ^ + ( 1 − y ) l o g ( 1 − y ^ ) ) L (\widehat{y}, y) = -(ylog\widehat{y} + (1-y)log(1 - \widehat{y})) L(y ,y)=−(ylogy +(1−y)log(1−y ))
函数的前一个参数为理论值, 后一个参数为实际期望值
(实际上这就是一个经典的交叉熵损失函数)
也许有人会觉得为什么不用误差平方进行求值
但实际上,至少我所接触的神经网络都是利用梯度下降法进行训练的
而在梯度下降的过程中会面临很多凸函数问题
到那时你就会发现误差平方并不精确,不能有效地找到局部最小值
所以综上,我们选择上述功能相近的loss函数作为替换
再回看loss函数:
L ( y ^ , y ) = − ( y l o g y ^ + ( 1 − y ) l o g ( 1 − y ^ ) ) L (\widehat{y}, y) = -(ylog\widehat{y} + (1-y)log(1 - \widehat{y})) L(y ,y)=−(ylogy +(1−y)log(1−y ))
这里并没有标明log的底数(并不是默认为10),而实际上 log 的底数并不影响函数的实际含义:
我们使用loss函数的目的是为了衡量理论值和实际值的误差
所以对上述函数进行分析
你会发现当理论值 y = 1 时, y’ 也需要趋于1,反之理论值 y = 0 时 y’ 趋于0亦成立,从而确保了理论值和实际期望值最大程度上的吻合
明确了loss之后,我们还需明确另一个概念cost
loss是神经网络在单个训练集上的表现
cost则是神经网络在整个训练集上的表现
若训练集为:
{ ( x ( 1 ) , y ( 1 ) ) , ( x ( 2 ) , y ( 2 ) ) , . . . , ( x ( m ) , y ( m ) ) } \left\{ (x^{(1)}, y^{(1)}), (x^{(2)}, y^{(2)}),...,(x^{(m)}, y^{(m)}) \right\} {(x(1),y(1)),(x(2),y(2)),...,(x(m),y(m))}
则cost为:
J ( w , b ) = 1 m ∑ i = 1 m L ( y ^ ( i ) , y ( i ) ) J(w, b) = \frac{1}{m}\sum_{i = 1}^{m}L (\widehat{y}^{(i)}, y^{(i)}) J(w,b)=m1i=1∑mL(y (i),y(i))
w 和 b 就是Logistic回归中的参数,在实际训练中应该是作为超参处理的
在上个子标题中我简单梳理了Logistic回归以及 loss 和 cost
那么在已知cost表达式的情况下,我们对初次训练的神经网络进行参数 w 和 b 的设定,显然,除非你运气够好,不然初次设定的参数一定是具有较大偏差的——
我们对 w , b , J(w, b) 建立空间坐标系,得到如下的空间曲线:
注:下图截取自吴恩达老师的深度学习课程
为了达到理论值和实际期望值最贴近的状态
我们需要 J(w, b) 取到局部最小值
而实际上对 w 和 b 的初次调参往往会有很大的误差
这时候我们就需要神经网络遵循一套规则渐进找到这个最低点
这里使用的就是梯度下降法
现在具体的解释梯度下降法的原理:
为了方便研究,我们先将上图的空间坐标系降维至平面坐标系
假定 b 是已知确定的
此时我们只需研究 w 和 J(w) 的图像:
注:下图截取自吴恩达老师的深度学习课程
在原图的基础上我添加了红点和蓝点
分别对应局部最小值和初次调参值
为了使神经网络能渐进地从 B 过渡至 A ,我们对 w 进行以下修正:
w = w − α d J ( w ) d w w = w - \alpha \frac{dJ(w)}{dw} w=w−αdwdJ(w)
这便是梯度下降法的核心思路
其中:
d J ( w ) d w \frac{dJ(w)}{dw} dwdJ(w)
是函数在当前点的斜率,对应了梯度下降的方向
而参数 α 是学习率,对应了沿当前点的斜率方向下降的深度
参数 α 非常重要,其取值决定了梯度下降的效率:
太大则容易错失最低点
太小则下降速率过慢,降低了程序执行的速度
当你将 B 点选定在 A 点左侧时,再从公式上理解时:
你会发现横坐标在增加
但从函数趋势上是在下降的,仍然对应了梯度下降
清楚理解平面坐标系的情形后,我们重新回归空间坐标系
实际上空间坐标系和平面坐标系建立在完全一致的数学规则上
只是需要对 w 和 b 同时进行修正:
w = w − α ∂ J ( w , b ) ∂ w w = w - \alpha \frac{\partial J(w, b)}{\partial w} w=w−α∂w∂J(w,b)
b = b − α ∂ J ( w , b ) ∂ b b = b - \alpha \frac{\partial J(w, b)}{\partial b} b=b−α∂b∂J(w,b)
在分别介绍了 Logistic回归 和 梯度下降法 后
我们将两者结合起来审视,看看具体的运作机制
首先我们假设我们的训练集为:
T = { ( x ( 1 ) , y ( 1 ) ) , ( x ( 2 ) , y ( 2 ) ) , . . . , ( x ( m ) , y ( m ) ) } T = \left\{ (x^{(1)}, y^{(1)}), (x^{(2)}, y^{(2)}),...,(x^{(m)}, y^{(m)}) \right\} T={(x(1),y(1)),(x(2),y(2)),...,(x(m),y(m))}
其中:
∣ T ∣ = m \left|T\right| = m ∣T∣=m
并假设在Logistic回归中,w 可表示为集合:
W = { w 1 , w 2 , w 3 , . . . , w n } W = \left\{w_{1}, w_{2}, w_{3},...,w_{n} \right\} W={w1,w2,w3,...,wn}
使得:
y = w 1 x 1 + w 2 x 2 + . . . + w n x n + b y = w_{1}x_{1} + w_{2}x_{2} +...+ w_{n}x_{n} + b y=w1x1+w2x2+...+wnxn+b
我们将此结果赋值给变量 z 以方便后续的变量区分
z = y = w 1 x 1 + w 2 x 2 + . . . + w n x n + b z = y = w_{1}x_{1} + w_{2}x_{2} +...+ w_{n}x_{n} + b z=y=w1x1+w2x2+...+wnxn+b
并在外层嵌套sigmoid函数,使得:
a = σ ( z ) a = \sigma (z) a=σ(z)
依照上文,这里的 a 就是理论值
我们再假设实际期望值为 y
进而求得:
L ( a , y ) = − ( y l o g a + ( 1 − y ) l o g ( 1 − a ) ) L (a, y) = -(yloga + (1-y)log(1 - a)) L(a,y)=−(yloga+(1−y)log(1−a))
运算至此,我们已经求得了Loss函数,接下来,我们只需要反推:
∂ L ( a , y ) ∂ w j \frac{\partial L(a, y)}{\partial w_{j}} ∂wj∂L(a,y)
以获取梯度下降中的关键参数
其实对于有一定微积分基础的人而言,这个反推过程并不复杂
(其实就是反向传播)
只需要对链式规则加以利用即可:
∂ L ( a , y ) ∂ w j = ∂ L ( a , y ) ∂ a × ∂ a ∂ z × ∂ z ∂ w j \frac{\partial L(a, y)}{\partial w_{j}} = \frac{\partial L(a, y)}{\partial a}\times \frac{\partial a}{\partial z}\times\frac{\partial z}{\partial w_{j}} ∂wj∂L(a,y)=∂a∂L(a,y)×∂z∂a×∂wj∂z
省略具体的偏导过程,我们最终求出结果:
∂ L ( a , y ) ∂ w j = x j × ( a − y ) \frac{\partial L(a, y)}{\partial w_{j}} = x_{j}\times(a - y) ∂wj∂L(a,y)=xj×(a−y)
而实际上,在上一个标题中,梯度下降公式中对应的参数为:
∂ J ( w , b ) ∂ w \frac{\partial J(w, b)}{\partial w} ∂w∂J(w,b)
也就意味着我们需要对整个训练集进行处理,从而求出该参数,
定义额外变量如下:
J = 0 ; w 1 = 0 ; w 2 = 0 ; . . . ; w n = 0 ; b = 0 ; J = 0; w_{1} = 0; w_{2} = 0;...;w_{n} = 0;b = 0; J=0;w1=0;w2=0;...;wn=0;b=0;
遍历整个训练集,有:
J + = L ( a ( i ) , y ( i ) ) J += L (a^{(i)}, y^{(i)}) J+=L(a(i),y(i))
w j + = x j ( i ) × ( a ( i ) − y ( i ) ) w_{j} +=x_{j}^{(i)}\times(a^{(i)} - y^{(i)}) wj+=xj(i)×(a(i)−y(i))
b + = ( a ( i ) − y ( i ) ) b +=(a^{(i)} - y^{(i)}) b+=(a(i)−y(i))
并对整个训练集取平均:
J = J m J = \frac{J}{m} J=mJ
对其余设定的额外变量做同样的取平均处理
由此我们可以求得:
w j = ∂ J ∂ w j w_{j} = \frac{\partial J}{\partial w_{j}} wj=∂wj∂J
同理:
b = ∂ J ∂ b b= \frac{\partial J}{\partial b} b=∂b∂J
并带入最终的梯度下降公式中
就可以对输入的各个参数作出一次修正了
神经网络的相关基础概念已经基本整理完毕
但是如果只按上述流程去编写机器学习算法
咳咳
那么你会发现整个程序的执行效率令人窒息
仔细分析上述流程
我们将发现设计的算法中将不可避免的有 for 循环
但在实际的机器学习过程中
显式的 for 循环会降低整个程序的执行效率
而实际上,真正在训练神经网络时训练集的容量是大到恐怖的
就拿我之前的做的搜索引擎举例
当时的训练集将近30G,涵盖了140w张图片
事实上神经网络会被投入比之更大的训练集,往往达到千万级甚至更大
(自己的笔记本默默无闻地跑了6个多小时才把训练集用掉。。。)
所以面对如此之大的训练集
即使是毫秒层面的时间效率都应该被予以重视
这里将使用向量化对上述正向传播的过程进行优化
这里我们假设对于每个训练集 x,都是一个 k 维的列向量(即 k 个特征)
那么对于整个训练集合 X ,我们可以设 X 为:
X = [ x ( 1 ) , x ( 2 ) , x ( 3 ) , . . . , x ( m ) ] X = [x^{(1)}, x^{(2)}, x^{(3)},...,x^{(m)}] X=[x(1),x(2),x(3),...,x(m)]
不难发现这是一个 k * m 的矩阵
而对于 k 维向量中每个特征所对应的 w,我们设 W 的转置为:
(从实际意义出发 W 应该是一个列向量)
W T = [ w 1 , w 2 , w 3 , . . . , w k ] W^{T} = [w_{1},w_{2}, w_{3},...,w_{k}] WT=[w1,w2,w3,...,wk]
此时我们会发现,在之前的正向传播过程中,
循环中所有的 Z 亦可被构造为向量:
Z = W T X + b Z = W^{T}X + b Z=WTX+b
显然 Z 也是一个 m 维向量
在矩阵加减的逻辑上, b 也必须是一个 m 维向量
但得益于python的广播机制
实际编写过程中只需将 b 赋值为常规整型即可
对于反向传播也是同理,对于:
∂ L ( a , y ) ∂ z \frac{\partial L(a, y)}{\partial z} ∂z∂L(a,y)
我们亦可以构建 m 维向量,使得:
Z ^ = [ ∂ L ( a ( 1 ) , y ( 1 ) ) ∂ z ( 1 ) , ∂ L ( a ( 2 ) , y ( 2 ) ) ∂ z ( 2 ) , . . . , ∂ L ( a ( m ) , y ( m ) ) ∂ z ( m ) ] = A − Y \widehat{Z} = [\frac{\partial L(a^{(1)}, y^{(1)})}{\partial z^{(1)}}, \frac{\partial L(a^{(2)}, y^{(2)})}{\partial z^{(2)}},...,\frac{\partial L(a^{(m)}, y^{(m)})}{\partial z^{(m)}}] = A - Y Z =[∂z(1)∂L(a(1),y(1)),∂z(2)∂L(a(2),y(2)),...,∂z(m)∂L(a(m),y(m))]=A−Y
其中:
Y = [ y ( 1 ) , y ( 2 ) , y ( 3 ) , . . . , y ( m ) ] Y = [y^{(1)}, y^{(2)}, y^{(3)},...,y^{(m)}] Y=[y(1),y(2),y(3),...,y(m)]
A = σ ( Z ) A = \sigma (Z) A=σ(Z)
而对于最终所要求的:
w j = ∂ J ∂ w j w_{j} = \frac{\partial J}{\partial w_{j}} wj=∂wj∂J
b = ∂ J ∂ b b= \frac{\partial J}{\partial b} b=∂b∂J
我们只需依照原反向传播过程稍加变化即可:
B ^ = 1 m ∑ Z ^ \widehat{B} = \frac{1}{m}\sum \widehat{Z} B =m1∑Z
W ^ = 1 m X Z ^ T \widehat{W} = \frac{1}{m}X\widehat{Z}^{T} W =m1XZ T
至于为什么采用显示的 for 循环会导致效率变低
我所使用的课程资源中并没有给出详细解释
通过查阅资料,我给出一些个人理解:
首先:
事实如此,当向量维度为100w时,写个程序就会发现存在400ms+的时间差距
其次:
我们在编写上述流程的算法时,其实需要嵌套三层循环
而在数学本质上
我们也正是模拟了一个矩阵乘法的过程
无非没有定义相应的矩阵
而常规的矩阵乘法都是三层循环,时间复杂度为O(n^3)
但python中的numpy库给出的应该是优化过的矩阵乘法
目前最优的矩阵乘法是2014年由François Le Gall简化的斯坦福方法,时间复杂度为O(n^2.3728639)
个人认为使用显式 for 循环所产生的细微时间差正源于此
整理完最基础的知识点之后
我们将上述的知识点放入一个实实在在的神经网络里进行深入理解:
如下是一个简单的神经网络:
从左至右依次是输入层,隐藏层和输出层
但输入层往往也会被视为一层常规神经网络
所以我们通常将这类神经网络命名为双层神经网络
此时我们将从左至右的每一层依次标记为(如上图):
a [ 1 ] , a [ 2 ] , a [ 3 ] a^{[1]}, a^{[2]}, a^{[3]} a[1],a[2],a[3]
依照上图的逻辑结构,我们在第 1 层神经网络(隐藏层)中
将会计算四次Logistic回归,从上至下依次为:
a 1 [ 1 ] , a 2 [ 1 ] , a 3 [ 1 ] , a 4 [ 1 ] a^{[1]}_{1}, a^{[1]}_{2}, a^{[1]}_{3}, a^{[1]}_{4} a1[1],a2[1],a3[1],a4[1]
而这些结果又将进一步被输出至输出层中
则在隐藏层中我们进行的计算可被表示为:
z i [ 1 ] = w i [ 1 ] T x + b i [ 1 ] , a i [ 1 ] = σ ( z i [ 1 ] ) z^{[1]}_{i} = w^{[1]T}_{i}x + b^{[1]}_{i}, a^{[1]}_{i} = \sigma (z^{[1]}_{i}) zi[1]=wi[1]Tx+bi[1],ai[1]=σ(zi[1])
但仅仅这样依旧会产生显式for循环
接下来我们尝试对该流程向量化:
上述流程中我们会得到 4 个Logistic回归单元
每个单元都是一个3维列向量
我们将其堆叠起来,从而组成一个 4 * 3 的矩阵:
W [ 1 ] = [ w 1 [ 1 ] T w 2 [ 1 ] T w 3 [ 1 ] T w 4 [ 1 ] T ] W^{[1]} = \left [ \begin{array}{c} w^{[1]T}_{1} \\ w^{[1]T}_{2} \\ w^{[1]T}_{3} \\ w^{[1]T}_{4} \end{array} \right ] W[1]=⎣⎢⎢⎢⎡w1[1]Tw2[1]Tw3[1]Tw4[1]T⎦⎥⎥⎥⎤
为验证其正确性,我们将该矩阵与:
x = [ x 1 x 2 x 3 ] x = \left [ \begin{array}{c} x_{1} \\ x_{2} \\ x_{3} \end{array} \right ] x=⎣⎡x1x2x3⎦⎤
相乘并与:
b [ 1 ] = [ b 1 [ 1 ] b 2 [ 1 ] b 3 [ 1 ] b 4 [ 1 ] ] b^{[1]} = \left [ \begin{array}{c} b^{[1]}_{1} \\ b^{[1]}_{2} \\ b^{[1]}_{3} \\ b^{[1]}_{4} \end{array} \right ] b[1]=⎣⎢⎢⎢⎡b1[1]b2[1]b3[1]b4[1]⎦⎥⎥⎥⎤
相加,显然我们会发现最终产生了一个4维列向量
而其中的每一个元素正是一次Logistic回归对应的结果
从而在输入层至隐藏层的正向传播中,上述向量化方法是合理的
对于隐藏层至输出层,我们亦进行同理的向量化
而对于:
x = [ x 1 x 2 x 3 ] x = \left [ \begin{array}{c} x_{1} \\ x_{2} \\ x_{3} \end{array} \right ] x=⎣⎡x1x2x3⎦⎤
我们仔细观察上图的神经网络结构
可以发现:
x = a [ 0 ] x =a^{[0]} x=a[0]
由此,向量化的神经网络正向传播过程可以表示为:
z [ i ] = w [ i ] T a [ i − 1 ] + b [ i ] z^{[i]} = w^{[i]T}a^{[i - 1]} + b^{[i]} z[i]=w[i]Ta[i−1]+b[i]
a [ i ] = σ ( z [ i ] ) a^{[i]} = \sigma (z^{[i]}) a[i]=σ(z[i])