softmax函数通常用在机器学习的分类问题中,作为输出层的激活函数。它的输入是一个实数向量,输出向量的长度与输入向量相同(也与分类的数目相同),但所有元素的取值范围为(0,1),且所有元素的和为1。输出向量的各个元素值表示的是属于某个分类的可能性。
softmax函数的数学表达式为:
s o f t m a x ( x ) i = e x i ∑ j e x j softmax(\boldsymbol x)_i=\frac{e^{x_i}}{\sum_je^{x_j}} softmax(x)i=∑jexjexi
由于输入x是实数向量,e的x次方的计算结果可能非常大甚至溢出,不利于计算的稳定性。所以通常将x减去一个常数c再输入到softmax。下面证明了x的常数加减是不会影响softmax结果的:
s o f t m a x ( x − c ) i = e x i − c ∑ j e x j − c = e x i ∗ e − c ∑ j e x j ∗ e − c = e x i ∗ e − c e − c ∗ ∑ j e x j = e x i ∑ j e x j = s o f t m a x ( x ) i softmax(\boldsymbol x-c)_i=\frac{e^{x_i-c}}{\sum_je^{x_j-c}} \\~ \\=\frac{e^{x_i}*e^{-c}}{\sum_je^{x_j}*e^{-c}} \\~ \\=\frac{e^{x_i}*e^{-c}}{e^{-c}*\sum_je^{x_j}} \\~ \\=\frac{e^{x_i}}{\sum_je^{x_j}} \\~ \\=softmax(\boldsymbol x)_i softmax(x−c)i=∑jexj−cexi−c =∑jexj∗e−cexi∗e−c =e−c∗∑jexjexi∗e−c =∑jexjexi =softmax(x)i
由于e的x次方的图像如下所示,如果常数c的取值为向量x中值最大的元素,则可以保证e的x次方取值范围为(0,1]。
所以,softmax的python实现应该为(假设输入为一个向量,省略参数检查):
import numpy as np
def softmax(x):
x = x - max(x)
exp_x = np.exp(x)
return exp_x/np.sum(exp_x)
这一题考的是神经网络算法的正反向推导,其中隐藏层使用sigmoid激活函数,输出层使用softmax激活函数。
为什么要在隐藏层使用激活函数?因为
如果不用激活函数,每一层输出都是上层输入的线性函数,无论神经网络有多少层,输出都是输入的线性组合。
如果使用的话,激活函数给神经元引入了非线性因素,使得神经网络可以任意逼近任何非线性函数,这样神经网络就可以应用到众多的非线性模型中。
上文引用自此处,由于转发者没有提供原文链接,所以只能提供转发的文章链接。文章里也对多种激活函数做了介绍并进行对比,我这里就不展开了。
由于反向传播计算过程需要用到导数,所以需要对sigmoid函数求导,sigmoid函数的数学表达式为:
σ ( x ) = 1 1 + e − x \sigma(x)=\frac{1}{1+e^{-x}} σ(x)=1+e−x1
其关于x的导数为:
σ ′ ( x ) = ( 1 1 + e − x ) ′ = − ( 1 + e − x ) − 2 ∗ e − x ∗ ( − 1 ) = e − x ( 1 + e − x ) 2 = 1 1 + e − x ∗ e − x 1 + e − x = 1 1 + e − x ∗ ( 1 − 1 1 + e − x ) = σ ( x ) ∗ ( 1 − σ ( x ) ) \sigma'(x)=(\frac{1}{1+e^{-x}})' \\~ \\=-(1+e^{-x})^{-2} * e^{-x} * (-1) \\~ \\=\frac{e^{-x}}{(1+e^{-x})^2} \\~ \\=\frac{1}{1+e^{-x}} * \frac{e^{-x}}{1+e^{-x}} \\~ \\=\frac{1}{1+e^{-x}} * (1-\frac{1}{1+e^{-x}}) \\~ \\=\sigma(x)*(1-\sigma(x)) σ′(x)=(1+e−x1)′ =−(1+e−x)−2∗e−x∗(−1) =(1+e−x)2e−x =1+e−x1∗1+e−xe−x =1+e−x1∗(1−1+e−x1) =σ(x)∗(1−σ(x))
故sigmoid导数的python实现为:
# 参数s为sigmoid的输出,返回值是sigmoid关于输入x的导数。
def sigmoid_grad(s):
return s*(1-s)
cross entropy交叉熵函数一般用于计算softmax输出的代价(cost/loss),交叉熵函数的数学表达式为:
C E ( y , y ^ ) = − ∑ i y i ∗ l o g ( y ^ i ) CE(\boldsymbol y,\boldsymbol{\hat y})=-\sum_i{\boldsymbol y_i*log(\boldsymbol{\hat y}_i)} CE(y,y^)=−i∑yi∗log(y^i)
其中y_hat为模型的预测结果,此处假设y_hat为softmax的输出,即
y ^ = s o f t m a x ( x ) \boldsymbol{\hat y}=softmax(\boldsymbol x) y^=softmax(x)
x为softmax的输入,则cross entropy相对于输入x的求导过程如下:
所以本次求导在python代码中的实现简写应该为:
import numpy as np
y_hat = softmax(x)
cost = np.sum(-y*log(y_hat))
grad_x = y_hat - y
习题中的神经网络模型为2层模型,隐藏层采用sigmoid激活,输出层采用softmax激活,这两个函数我们在前面已经完成了求导,接下来我们将直接使用上面的求导结果进行反向传播的求解。
从前面的推导可知,y_hat第i项的梯度为:
∂ C E ( y , y ^ ) ∂ y ^ i = − y i y ^ i \frac{\partial CE(\boldsymbol y,\boldsymbol{\hat y})} {\partial \boldsymbol{\hat y}_i} =-\frac{\boldsymbol y_i}{\boldsymbol{\hat y}_i} ∂y^i∂CE(y,y^)=−y^iyi
由于隐藏层到输出层是全连接网络,即y_hat的值实际上隐藏层的所有输出都有贡献,贡献的大小取决于连接的边的权重,这些权重保存在隐藏层到输出层的权重矩阵W2中。假设隐藏层h的第j个神经元h_j到输出层o第i个神经元的连接权重表示为(公式下方的2表示该矩阵是神经网络第1层到第2层的连接矩阵): W 2 j i \bold W^{ji}_2 W2ji
即W2矩阵的第j行第i列个元素。
则h层第j+1个神经元hj(j从0递增)的输出的偏导数为:
∂ C E ( y , y ^ ) ∂ h ^ j = ∑ i − y i y ^ i ∗ W 2 j i \frac{\partial CE(\boldsymbol y,\boldsymbol{\hat y})} {\partial \boldsymbol{\hat h}_j} =\sum_i-\frac{\boldsymbol y_i}{\boldsymbol{\hat y}_i}*\bold W_2^{ji} ∂h^j∂CE(y,y^)=i∑−y^iyi∗W2ji
若y和y_hat为列向量,要求的h层输出的偏导数为列向量,则有:
∂ C E ( y , y ^ ) ∂ h = W 2 ⋅ y y ^ \frac{\partial CE(\boldsymbol y,\boldsymbol{\hat y})} {\partial \boldsymbol{h}} =\bold W_2 \cdot \frac{\boldsymbol y}{\boldsymbol{\hat y}} ∂h∂CE(y,y^)=W2⋅y^y
根据上面关于sigmoid的推导,可得h层输入h_hat的偏导数为:
∂ C E ( y , y ^ ) ∂ h ^ = ∂ C E ( y , y ^ ) ∂ h ∗ ( 1 − ∂ C E ( y , y ^ ) ∂ h ) = W 2 ⋅ y y ^ ∗ ( 1 − W 2 ⋅ y y ^ ) \frac{\partial CE(\boldsymbol y,\boldsymbol{\hat y})} {\partial \boldsymbol{\hat h}}= \frac{\partial CE(\boldsymbol y,\boldsymbol{\hat y})} {\partial \boldsymbol{h}}*(1-\frac{\partial CE(\boldsymbol y,\boldsymbol{\hat y})} {\partial \boldsymbol{h}}) \\~ \\=\bold W_2 \cdot \frac{\boldsymbol y}{\boldsymbol{\hat y}}*(1-\bold W_2 \cdot \frac{\boldsymbol y}{\boldsymbol{\hat y}}) ∂h^∂CE(y,y^)=∂h∂CE(y,y^)∗(1−∂h∂CE(y,y^)) =W2⋅y^y∗(1−W2⋅y^y)
而h层任意一个神经元的输入也有输入层所有元素的贡献,故第n+1个输入x_n(n从0递增)的偏导数为(假设h和h_hat都是列向量):
∂ C E ( y , y ^ ) ∂ x n = ∑ j ∂ C E ( y , y ^ ) ∂ h ^ j ∗ W 1 n j \frac{\partial CE(\boldsymbol y,\boldsymbol{\hat y})} {\partial \boldsymbol{x}_n}= \sum_j\frac{\partial CE(\boldsymbol y,\boldsymbol{\hat y})} {\partial \boldsymbol{\hat h}_j}*\bold W_1^{nj} ∂xn∂CE(y,y^)=j∑∂h^j∂CE(y,y^)∗W1nj
故:
∂ C E ( y , y ^ ) ∂ x = W 1 ⋅ ∂ C E ( y , y ^ ) ∂ h ^ = W 1 ⋅ [ W 2 ⋅ y y ^ ∗ ( 1 − W 2 ⋅ y y ^ ) ] \frac{\partial CE(\boldsymbol y,\boldsymbol{\hat y})} {\partial \boldsymbol{x}} =\bold W_1 \cdot \frac{\partial CE(\boldsymbol y,\boldsymbol{\hat y})} {\partial \boldsymbol{\hat h}} \\~ \\=\bold W_1 \cdot [\bold W_2 \cdot \frac{\boldsymbol y}{\boldsymbol{\hat y}}*(1-\bold W_2 \cdot \frac{\boldsymbol y}{\boldsymbol{\hat y}})] ∂x∂CE(y,y^)=W1⋅∂h^∂CE(y,y^) =W1⋅[W2⋅y^y∗(1−W2⋅y^y)]
梯度gradient即我们前面所求的偏导数/斜率,由于偏导数要用于梯度下降算法,所以通常在机器学习中,偏导数也称为梯度。在梯度下降算法中,我们求得的梯度最终将用于更新算法的参数。如果我们算出来的梯度有误,将很可能导致算法无法获得想要的结果,会给排查问题带来困难。所以在求得梯度之后进行梯度检查,可以检测梯度计算错误,将排查问题的精力集中在模型上而不是计算上。
梯度检查通常采用一种近似算法,英文名称是central difference approximations。也有forward difference approximations和backward difference approximations,但是central difference的误差最小,具体可以查看这个页面。
其公式分别为:
central difference approximations:
f ′ ( x ) ≈ f ( x + h ) − f ( x − h ) 2 ∗ h f'(x)\approx\frac{f(x+h)-f(x-h)}{2*h} f′(x)≈2∗hf(x+h)−f(x−h)
forward difference approximations:
f ′ ( x ) ≈ f ( x + h ) − f ( x ) h f'(x)\approx\frac{f(x+h)-f(x)}{h} f′(x)≈hf(x+h)−f(x)
backward difference approximations:
f ′ ( x ) ≈ f ( x ) − f ( x − h ) h f'(x)\approx\frac{f(x)-f(x-h)}{h} f′(x)≈hf(x)−f(x−h)
由于梯度值和central difference值是近似值,所以不能直接检查它们是否相等。本习题提供的代码中,通过判断的是它们的差值的绝对值是否小于1e-5来判断这两个值是否近似。从而判断我们计算的梯度是否正确。
word2vec是谷歌提出的用于将自然词汇转化成向量表示的模型,它包括skip gram模型和CBOW模型。skip gram模型用center word去预测周边的词,而CBOW用center word周边的词去预测center word。宏观来说,word2vec是非监督学习模型,它不需要人工标注数据就能学习。但是它又可以拆分成多个监督学习问题,来解决表面上的无监督学习问题,数学式表示为:
p ( x ) = ∏ i = 1 n p ( x i ∣ x 1 , . . . , x i − 1 ) p(\boldsymbol x)=\prod^n_{i=1}p(\boldsymbol x_i|\boldsymbol x_1,...,\boldsymbol x_{i-1}) p(x)=i=1∏np(xi∣x1,...,xi−1)
在学习word2vec前,需要先掌握余弦相似度这个概念。
余弦相似度,又称为余弦相似性,是通过计算两个向量的夹角余弦值来评估他们的相似度。
《百度百科》
也就是说,两个向量之间的夹角越小,这两个向量越相似。由于word2vec最终将自然语言词汇转化成向量,所以可以通过余弦相似度来判断两个词有多相似。
cs224n中采用向量内积作为向量相似度,即两个向量内积越大越相似,类似余弦相似度但是与余弦相似度有区别,下文介绍了内积相似度和余弦相似度的区别。大概意思是余弦相似度只考虑向量之间的夹角,而内积考虑了向量的夹角和大小。至于为什么选用向量内积作为向量相似度,我找不到相关文档。但是事实证明,word2vec中采用内积作为向量的相似度结果不错,可能采用内积相似度是一个实践的选择吧。
Think geometrically. Cosine similarity only cares about angle difference, while dot product cares about angle and magnitude. If you normalize your data to have the same magnitude, the two are indistinguishable. Sometimes it is desirable to ignore the magnitude, hence cosine similarity is nice, but if magnitude plays a role, dot product would be better as a similarity measure.
《引用自此处》
对于两个列向量,它们的内积在数学上表示为:
i n n e r ( x , y ) = x T ⋅ y inner(\boldsymbol x,\boldsymbol y)=\boldsymbol x^T\cdot \boldsymbol y inner(x,y)=xT⋅y
python代码中可用numpy的函数inner实现:
import numpy as np
x = np.array([1,2,3])
y = np.array([4,5,6])
print(np.inner(x,y))
skip gram计算center word和某个单词o的相似度的概率的公式为:
y ^ o = p ( o ∣ c ) = e u o T ⋅ v c ∑ w = 1 W e u w T ⋅ v c \boldsymbol{\hat y}_o=p(\boldsymbol o|\boldsymbol c) =\frac{e^{\boldsymbol u_o^T \cdot \boldsymbol v_c}} {\sum_{w=1}^W e^{\boldsymbol u_w^T \cdot \boldsymbol v_c}} y^o=p(o∣c)=∑w=1WeuwT⋅vceuoT⋅vc
其中u_0是目标词向量,在output vector中。v_c是center word向量,在input vector中。
但skip gram的目的不是通过学习去逼近最相似的词(因为我们没有标注数据,如果有,说不定可以这么做),而是通过学习使center word和它周边的词的相似度的和最大,这样做的结果是可以使得周边词相同的center word越来越相似。例如“我 爱吃 苹果”和“我 爱吃 梨”,由于苹果和梨周边出现的词通常非常相似,所以学习算法能使“苹果”和“梨”这两个词向量越来越相似,最终得到我们想要的结果。
要使得两个词向量的相似度越大,则要使预测结果的代价cost越小。
即skip gram的目标是(其中k表示center word周边词的集合,Y_hat和Y表示所有单词的预测值矩阵和实际值矩阵):
m i n i m i z e ∑ k C o s t ( Y ^ k , Y k ) minimize\sum_kCost(\bold {\hat Y}_k,\bold Y_k) minimizek∑Cost(Y^k,Yk)
假设采用cross entropy作为代价函数,设y=Y_k则有:
C o s t ( Y ^ k , Y k ) = C E ( y ^ , y ) Cost(\bold {\hat Y}_k,\bold Y_k)=CE(\boldsymbol{\hat y},\boldsymbol y) Cost(Y^k,Yk)=CE(y^,y)
由于要更新词向量,故需要求各个向量的梯度(设x为列向量):
center word的梯度
∂ C E ( y ^ , y ) ∂ v c = ∂ C E ( y ^ , y ) ∂ y ^ ∗ ∂ y ^ ∂ x ∗ ∂ x ∂ v c = ( y ^ − y ) ∗ ∂ x ∂ v c = ( y ^ − y ) ∗ [ ∂ x 1 ∂ v c , . . . , ∂ x w ∂ v c ] = ( y ^ − y ) ∗ [ u 1 , . . . , u w ] = ( y ^ − y ) ∗ U \frac{\partial CE(\boldsymbol{\hat y},\boldsymbol y) }{\partial \boldsymbol v_c}= \frac{\partial CE(\boldsymbol{\hat y},\boldsymbol y) }{\partial \boldsymbol {\hat y}}* \frac{\partial \boldsymbol {\hat y}}{\partial \boldsymbol x}* \frac{\partial \boldsymbol {x}}{\partial \boldsymbol v_c} \\~ \\=(\boldsymbol {\hat y}-\boldsymbol y)* \frac{\partial \boldsymbol {x}}{\partial \boldsymbol v_c} \\~ \\=(\boldsymbol {\hat y}-\boldsymbol y)* [\frac{\partial \boldsymbol {x}_1}{\partial \boldsymbol v_c}, ... , \frac{\partial \boldsymbol {x}_w}{\partial \boldsymbol v_c}] \\~ \\=(\boldsymbol {\hat y}-\boldsymbol y)*[\boldsymbol u_1,...,\boldsymbol u_w] \\~ \\=(\boldsymbol {\hat y}-\boldsymbol y)*U ∂vc∂CE(y^,y)=∂y^∂CE(y^,y)∗∂x∂y^∗∂vc∂x =(y^−y)∗∂vc∂x =(y^−y)∗[∂vc∂x1,...,∂vc∂xw] =(y^−y)∗[u1,...,uw] =(y^−y)∗U
u_w的梯度(包括u_o)
∂ C E ( y ^ , y ) ∂ u w = ∂ C E ( y ^ , y ) ∂ y ^ ∗ ∂ y ^ ∂ x ∗ ∂ x ∂ u w = ( y ^ − y ) ∗ v c \frac{\partial CE(\boldsymbol{\hat y},\boldsymbol y) }{\partial \boldsymbol u_w}= \frac{\partial CE(\boldsymbol{\hat y},\boldsymbol y) }{\partial \boldsymbol {\hat y}}* \frac{\partial \boldsymbol {\hat y}}{\partial \boldsymbol x}* \frac{\partial \boldsymbol {x}}{\partial \boldsymbol u_w} \\~ \\=(\boldsymbol {\hat y}-\boldsymbol y)*\boldsymbol v_c ∂uw∂CE(y^,y)=∂y^∂CE(y^,y)∗∂x∂y^∗∂uw∂x =(y^−y)∗vc
由于cross entropy loss在计算loss时需要计算center word与所有output verctor的相似度,迭代次数是T(语料库的大小)。而负采样loss只计算center word和k个output vector的相似度,迭代次数是k,每次迭代都要快得多。
负采样的计算公式为:
J n e g − s a m p l e ( o , v c , U ) = − l o g ( σ ( u o T ⋅ v c ) ) − ∑ k = 1 K l o g ( σ ( − u k T ⋅ v c ) ) \boldsymbol J_{neg-sample}(\boldsymbol o,\boldsymbol v_c,\boldsymbol U)=-log(\sigma(\boldsymbol u_o^T\cdot \boldsymbol v_c))-\sum^K_{k=1}log(\sigma(-\boldsymbol u_k^T\cdot \boldsymbol v_c)) Jneg−sample(o,vc,U)=−log(σ(uoT⋅vc))−k=1∑Klog(σ(−ukT⋅vc))
其中各向量的梯度如下。
v_c的梯度(需要注意的是在初始化时,cs224n代码中将inputVector初始化为随机数,但是将outputVector初始化为0,在语料库T比较大且k比较小的情况下,很多轮迭代得到的v_c的梯度都为0,因为从outputVector中取到的u_o总是为0)
∂ J n e g − s a m p l e ( o , v c , U ) ∂ v c = − ∂ l o g ( σ ( u o T ⋅ v c ) ) ∂ v c − ∂ ∑ k = 1 K l o g ( σ ( − u k T ⋅ v c ) ) ∂ v c = − 1 σ ( u o T ⋅ v c ) ∗ [ σ ( u o T ⋅ v c ) ) ∗ ( 1 − σ ( u o T ⋅ v c ) ) ] ∗ u o − ∂ ∑ k = 1 K l o g ( σ ( − u k T ⋅ v c ) ) ∂ v c = − ( 1 − σ ( u o T ⋅ v c ) ) ∗ u o − ∑ k = 1 K ( 1 − σ ( − u k T ⋅ v c ) ) ∗ u k \frac{\partial \boldsymbol J_{neg-sample}(\boldsymbol o,\boldsymbol v_c,\boldsymbol U)}{\partial \boldsymbol v_c} =-\frac{\partial log(\sigma(\boldsymbol u_o^T\cdot \boldsymbol v_c))} {\partial \boldsymbol v_c}- \frac{\partial \sum^K_{k=1}log(\sigma(-\boldsymbol u_k^T\cdot \boldsymbol v_c))} {\partial \boldsymbol v_c} \\~ \\=-\frac{1}{\sigma(\boldsymbol u_o^T\cdot \boldsymbol v_c)}* [\sigma(\boldsymbol u_o^T\cdot \boldsymbol v_c))*(1-\sigma(\boldsymbol u_o^T\cdot \boldsymbol v_c))]* \boldsymbol u_o- \frac{\partial \sum^K_{k=1}log(\sigma(-\boldsymbol u_k^T\cdot \boldsymbol v_c))} {\partial \boldsymbol v_c} \\~ \\=-(1-\sigma(\boldsymbol u_o^T\cdot \boldsymbol v_c))* \boldsymbol u_o- \sum^K_{k=1}(1-\sigma(-\boldsymbol u_k^T\cdot \boldsymbol v_c))* \boldsymbol u_k ∂vc∂Jneg−sample(o,vc,U)=−∂vc∂log(σ(uoT⋅vc))−∂vc∂∑k=1Klog(σ(−ukT⋅vc)) =−σ(uoT⋅vc)1∗[σ(uoT⋅vc))∗(1−σ(uoT⋅vc))]∗uo−∂vc∂∑k=1Klog(σ(−ukT⋅vc)) =−(1−σ(uoT⋅vc))∗uo−k=1∑K(1−σ(−ukT⋅vc))∗uk
u_o的梯度(o不属于1…k)
∂ J n e g − s a m p l e ( o , v c , U ) ∂ u o = − ∂ l o g ( σ ( u o T ⋅ v c ) ) ∂ u o − ∂ ∑ k = 1 K l o g ( σ ( − u k T ⋅ v c ) ) ∂ u o = − ( 1 − σ ( u o T ⋅ v c ) ) ∗ v c \frac{\partial \boldsymbol J_{neg-sample}(\boldsymbol o,\boldsymbol v_c,\boldsymbol U)}{\partial \boldsymbol u_o} =-\frac{\partial log(\sigma(\boldsymbol u_o^T\cdot \boldsymbol v_c))} {\partial \boldsymbol u_o}- \frac{\partial \sum^K_{k=1}log(\sigma(-\boldsymbol u_k^T\cdot \boldsymbol v_c))} {\partial \boldsymbol u_o} \\~ \\=-(1-\sigma(\boldsymbol u_o^T\cdot \boldsymbol v_c))* \boldsymbol v_c ∂uo∂Jneg−sample(o,vc,U)=−∂uo∂log(σ(uoT⋅vc))−∂uo∂∑k=1Klog(σ(−ukT⋅vc)) =−(1−σ(uoT⋅vc))∗vc
u_k的梯度
∂ J n e g − s a m p l e ( o , v c , U ) ∂ u k = − ∂ l o g ( σ ( u o T ⋅ v c ) ) ∂ u k − ∂ ∑ k = 1 K l o g ( σ ( − u k T ⋅ v c ) ) ∂ u k = − ( 1 − σ ( − u o T ⋅ v c ) ) ∗ ( − v c ) = ( 1 − σ ( − u o T ⋅ v c ) ) ∗ v c \frac{\partial \boldsymbol J_{neg-sample}(\boldsymbol o,\boldsymbol v_c,\boldsymbol U)}{\partial \boldsymbol u_k} =-\frac{\partial log(\sigma(\boldsymbol u_o^T\cdot \boldsymbol v_c))} {\partial \boldsymbol u_k}- \frac{\partial \sum^K_{k=1}log(\sigma(-\boldsymbol u_k^T\cdot \boldsymbol v_c))} {\partial \boldsymbol u_k} \\~ \\=-(1-\sigma(-\boldsymbol u_o^T\cdot \boldsymbol v_c))* (-\boldsymbol v_c) \\~ \\=(1-\sigma(-\boldsymbol u_o^T\cdot \boldsymbol v_c))* \boldsymbol v_c ∂uk∂Jneg−sample(o,vc,U)=−∂uk∂log(σ(uoT⋅vc))−∂uk∂∑k=1Klog(σ(−ukT⋅vc)) =−(1−σ(−uoT⋅vc))∗(−vc) =(1−σ(−uoT⋅vc))∗vc