深度学习——结构递归神经网络(Recursive NN)
1、递归神经网络介绍
目前,递归神经网络一共包含两种,一种是时间递归神经网络(Recurrent NN),另外一种是结构性递归神经网络(Recursive NN)。
1.1 神经网络处理变长序列
对于常规的神经网络而言,其输入节点的长度往往都是固定的。例如下面的一个普通的前馈神经网络。
(图片来源:https://image.baidu.com/search)
对于上述的前馈网络而言,其每次的输入就只有X1,X2,X3,X4四个。我们知道,在NLP中,句子序列的长度往往都是不固定的。那么如何处理变长的数据呢?
在这里,我们提出了采用递归神经网络的方式来处理不定长度的数据。由于输入单元个数的不确定,所以,必须采用循环,或者递归的方式来对数据进行输入,由此也就有了基于时间的递归神经网络和基于结构的递归神经网络。首先介绍基于时间的递归神经网络对于不定长序列的处理。
图片来源:https://www.sohu.com/a/128784058_487514
当在处理一个变长的句子的时候,将句子看作是不同词汇所构成的序列,每次向递归神经网络中输入一个词汇,知道所有的词汇输入完成,网络产生对应的输出。
下面,我们给出这样的一个序列,“两个计算机学院的学生”,从字面上来看,这句话可以有两种理解方式,第一种,是同一个计算机学院的两个学生,第二种,来自两个不同计算机学院的学生。当我们以基于时间的递归网络进行输入的时候,显然,网络是对无法对这两种语义进行辨析的。
上图是不同语义对应的语法树,根据不同语法树结构,我们自然而然的就可以想到使用树状的神经网络来对序列进行语义理解。这也就是基于结构的递归神经网络的应用。
基于结构的递归神经网络类似于一个自编码器,它将由句子构成的树或者图结构进行自编码,将其整个结构映射的向量空间中。语义相近的句子在向量空间中拥有更近的向量距离。同时,当把词汇,句子、段落映射到相同的向量空间中的时候,可以利用向量组合的方式,由词构成句子,由句子构成段落。不停地向上组合,最后将组合出来的语义树统一的表示成一个有意义的向量。
2、递归神经网络前向传播过程
2.1 前向计算过程
输入:两个或者多个子节点,
输出:两个子节点编码之后产生的父节点
下面两个节点表示子节点向量,上面表示的是父节点的向量,子节点和父节点组成一个全连接到的神经网络,也就是子节点和父节点上的神经单元进行全连接。计算公式为:
其中C1,C2分别表示的是两个子节点的向量,F表示父节点的向量,W表示权重,b表示偏重。f是激活函数。然后,我们将父节点的向量和其他子节点进行拼接,再次产生下一个父节点向量,不停的递归,直到生成最终的根节点向量。如下图所示:
2.2 前向传播的代码展示
#encoding=utf-8
import numpy as np
def sigmoid(x):
return 1/(1+np.exp(-x))
def IdentityActivator(x):
return x
#定义递归树的节点信息
class TreeNode(object):
def __init__(self,data,children=[],children_data=[]):
self.parent = None
self.children = children
self.children_data = children_data
self.data = data
for child in children:
child.parent = self
class RecursiveNetWork(object):
def __init__(self,node_dim,child_count,lr):
'''
node_dim : 每一个节点的维度
child_count : 每一个节点的子节点的个数
lr : 学习率
'''
self.node_dim = node_dim
self.child_count = child_count
self.lr = lr
self.W = np.ones((child_count * node_dim,node_dim))
self.B = np.ones((node_dim,1))
#生成最后的根节点
self.root = None
def concatenate(self,tree_nodes):
'''
定义向量拼接的方法
'''
concat = np.zeros((0,1))
concat = np.concatenate((concat,tree_nodes[0].data))
concat = np.concatenate((concat,tree_nodes[1].data))
#for node in tree_nodes:
# cocat = np.concatenate((concat,node.data))
return concat
def forward(self,*children):
#首先,将向量进行拼接
children_data = self.concatenate(children)
#前向传播,获取父节点的数据
parent_data = IdentityActivator(np.dot(self.W.T,children_data)+ self.B)
#生成根节点
self.root = TreeNode(parent_data,children,children_data)
3 反向传播
3.1 反向传播的过程计算(BPTS算法 )
3.1.1 少量节点的情况
对于基于结构的递归神经网络,其训练过程和基于时间的递归神经网络不同,基于时间的递归神经网络的反向传播是将当前时刻 t k t_k tk的误差不同的向前一个时刻 t k − 1 t_{k-1} tk−1进行反向传播,直到传播到初始时刻 t 0 t_0 t0。对于基于结构的递归神经网络而言,它的反向传播的方式是从根节点反向传播到所有的子节点,这种反向传播的方式成为BPTS算法。
我们先考虑三个节点的情况,为了便于说明,我们首先说明一个父节点,两个子节点的情况。具体结构如下图所示:
根据上图,我们构造出输入的节点矩阵X和权重W矩阵。输入包括两个子节点
X
1
,
X
2
X_1,X_2
X1,X2,每一个节点包含两个属性:
X
1
=
X
11
X
12
,
X
2
=
X
21
X
22
X_1= \begin{matrix} X_{11}\\ X_{12}\\ \end{matrix} ,X_2= \begin{matrix} X_{21}\\ X_{22}\\ \end{matrix}
X1=X11X12,X2=X21X22
将两者合并成一个向量之后,表示为X:
X
=
X
11
X
12
X
21
X
22
X= \begin{matrix} X_{11}\\ X_{12}\\ X_{21}\\ X_{22}\\ \end{matrix}
X=X11X12X21X22
下面构建出对应的W矩阵:
W
=
W
111
W
112
W
121
W
122
W
211
W
212
W
221
W
222
W= \begin{matrix} W_{111}&W_{112}\\ W_{121}&W_{122}\\ W_{211}&W_{212}\\ W_{221}&W_{222}\\ \end{matrix}
W=W111W121W211W221W112W122W212W222
根据前向传播的过程,设:
n
e
t
Y
=
W
T
X
,
Y
=
s
i
g
m
o
i
d
(
n
e
t
Y
)
net_Y=W^TX,Y=sigmoid(net_Y)
netY=WTX,Y=sigmoid(netY)
设J表示误差项,设
δ
F
δ_F
δF是对于父层节点的误差函数,则有:
δ
n
e
t
Y
=
∂
J
n
e
t
Y
=
∂
J
∂
n
e
t
Y
1
=
∂
J
∂
Y
1
∂
Y
1
∂
n
e
t
Y
1
=
∂
J
∂
Y
1
∗
Y
1
∗
(
1
−
Y
1
)
∂
J
∂
n
e
t
Y
2
=
∂
J
∂
Y
2
∂
Y
2
∂
n
e
t
Y
2
=
∂
J
∂
Y
2
∗
Y
2
∗
(
1
−
Y
2
)
δ_{net_Y}=\frac{∂J}{net_Y}= \begin{matrix} \frac{∂J}{∂net_{Y_1}}=\frac{∂J}{∂{Y_1}}\frac{∂Y_1}{∂{net_{Y_1}}}=\frac{∂J}{∂{Y_1}}*Y_1*(1-Y_1)\\ \frac{}{}\\ \frac{∂J}{∂net_{Y_2}}=\frac{∂J}{∂{Y_2}}\frac{∂Y_2}{∂{net_{Y_2}}}=\frac{∂J}{∂{Y_2}}*Y_2*(1-Y_2)\\ \end{matrix}
δnetY=netY∂J=∂netY1∂J=∂Y1∂J∂netY1∂Y1=∂Y1∂J∗Y1∗(1−Y1)∂netY2∂J=∂Y2∂J∂netY2∂Y2=∂Y2∂J∗Y2∗(1−Y2)
我们假设
δ
F
δ_F
δF已知,我们下面继续推导
∂
J
∂
x
11
\frac{∂J}{∂x_{11}}
∂x11∂J
∂
J
∂
x
11
=
∑
i
=
1
2
∂
J
∂
n
e
t
Y
i
∗
∂
n
e
t
Y
i
∂
X
11
=
∑
i
=
1
2
∂
J
∂
n
e
t
Y
i
∗
W
11
i
\frac{∂J}{∂x_{11}}=∑_{i=1}^2\frac{∂J}{∂net_{Y_i}}*\frac{∂net_{Y_i}}{∂X_{11}}= ∑_{i=1}^2\frac{∂J}{∂net_{Y_i}}*W_{11i}
∂x11∂J=i=1∑2∂netYi∂J∗∂X11∂netYi=i=1∑2∂netYi∂J∗W11i
同理,可以推导出关于
X
12
,
X
21
,
X
22
X_{12},X_{21},X_{22}
X12,X21,X22的导数为:
∂
J
∂
x
12
=
∑
i
=
1
2
∂
J
∂
n
e
t
Y
i
∗
∂
n
e
t
Y
i
∂
X
12
=
∑
i
=
1
2
∂
J
∂
n
e
t
Y
i
∗
W
12
i
\frac{∂J}{∂x_{12}}=∑_{i=1}^2\frac{∂J}{∂net_{Y_i}}*\frac{∂net_{Y_i}}{∂X_{12}}= ∑_{i=1}^2\frac{∂J}{∂net_{Y_i}}*W_{12i}
∂x12∂J=i=1∑2∂netYi∂J∗∂X12∂netYi=i=1∑2∂netYi∂J∗W12i
∂
J
∂
x
21
=
∑
i
=
1
2
∂
J
∂
n
e
t
Y
i
∗
∂
n
e
t
Y
i
∂
X
21
=
∑
i
=
1
2
∂
J
∂
n
e
t
Y
i
∗
W
21
i
\frac{∂J}{∂x_{21}}=∑_{i=1}^2\frac{∂J}{∂net_{Y_i}}*\frac{∂net_{Y_i}}{∂X_{21}}= ∑_{i=1}^2\frac{∂J}{∂net_{Y_i}}*W_{21i}
∂x21∂J=i=1∑2∂netYi∂J∗∂X21∂netYi=i=1∑2∂netYi∂J∗W21i
∂
J
∂
x
22
=
∑
i
=
1
2
∂
J
∂
n
e
t
Y
i
∗
∂
n
e
t
Y
i
∂
X
22
=
∑
i
=
1
2
∂
J
∂
n
e
t
Y
i
∗
W
22
i
\frac{∂J}{∂x_{22}}=∑_{i=1}^2\frac{∂J}{∂net_{Y_i}}*\frac{∂net_{Y_i}}{∂X_{22}}= ∑_{i=1}^2\frac{∂J}{∂net_{Y_i}}*W_{22i}
∂x22∂J=i=1∑2∂netYi∂J∗∂X22∂netYi=i=1∑2∂netYi∂J∗W22i
整理成向量的形式:
δ
X
=
∂
J
∂
X
=
W
δ
Y
=
∂
J
∂
X
11
∂
J
∂
X
12
∂
J
∂
X
21
∂
J
∂
X
22
δ_X=\frac{∂J}{∂X}=Wδ_Y= \begin{matrix} \frac{∂J}{∂X_{11}}\\ \frac{}{}\\ \frac{∂J}{∂X_{12}}\\ \frac{}{}\\ \frac{∂J}{∂X_{21}}\\ \frac{}{}\\ \frac{∂J}{∂X_{22}} \end{matrix}
δX=∂X∂J=WδY=∂X11∂J∂X12∂J∂X21∂J∂X22∂J
如,X也是采用sigmoid激活的,那么根据激活函数的求导,可以直接定义:
δ
n
e
t
X
=
∂
J
∂
n
e
t
X
=
δ
X
∗
X
∗
(
1
−
X
)
δ_{net_X}=\frac{∂J}{∂net_{X}}=δ_X*X*(1-X)
δnetX=∂netX∂J=δX∗X∗(1−X)
这里可以根据我们选择的激活函数进行实际的求导。,设激活函数为
f
(
x
)
f(x)
f(x),则最终获取的值为:
δ
n
e
t
X
=
∂
J
∂
n
e
t
X
=
δ
X
∗
f
′
(
X
)
δ_{net_X}=\frac{∂J}{∂net_{X}}=δ_X*f'(X)
δnetX=∂netX∂J=δX∗f′(X)
下面就是将
δ
n
e
t
X
δ_{net_X}
δnetX拆解成
δ
n
e
t
X
1
和
δ
n
e
t
X
2
δ_{net_{X_1}}和δ_{net_{X_2}}
δnetX1和δnetX2两个部分的误差。
δ
n
e
t
X
1
=
∂
J
∂
n
e
t
X
1
=
W
δ
n
e
t
X
1
=
∂
J
∂
n
e
t
X
11
∂
J
∂
X
n
e
t
12
,
δ
n
e
t
X
2
=
∂
J
∂
n
e
t
X
2
=
W
δ
n
e
t
X
2
=
∂
J
∂
n
e
t
X
21
∂
J
∂
n
e
t
X
22
δ_{net_{X_1}}=\frac{∂J}{∂net_{X_1}}=Wδ_{net_{X_1}}= \begin{matrix} \frac{∂J}{∂net_{X_{11}}}\\ \frac{}{}\\ \frac{∂J}{∂Xnet_{{12}}}\\ \end{matrix}, δ_{net_{X_2}}=\frac{∂J}{∂net_{X_{2}}}=Wδ_{net_{X_2}}= \begin{matrix} \frac{∂J}{∂net_{X_{21}}}\\ \frac{}{}\\ \frac{∂J}{∂net_{X_{22}}}\\ \end{matrix}
δnetX1=∂netX1∂J=WδnetX1=∂netX11∂J∂Xnet12∂J,δnetX2=∂netX2∂J=WδnetX2=∂netX21∂J∂netX22∂J
3.1.2 多节点的情况
当构造的递归树的层数较多的时候,我们可以根据上面的少量节点的推导公式,扩展到多层递归树的情况。
在上图中,已经将总的误差标出来了。假设
δ
r
o
o
t
δ_{root}
δroot已知,下面我们来逐层推导一下:
δ
n
e
t
X
=
W
δ
r
o
o
t
∗
f
′
(
X
)
,
δ
n
e
t
X
1
=
δ
n
e
t
X
[
0
:
n
o
d
e
d
i
m
,
:
]
,
δ
n
e
t
X
2
=
δ
n
e
t
X
[
n
o
d
e
d
i
m
:
,
:
]
δ_{net_X}=Wδ_{root}*f'(X),δ_{net{X_1}}=δ_{net_X}[0:nodedim,:],δ_{net_{X_2}}=δ_{net_X}[nodedim:,:]
δnetX=Wδroot∗f′(X),δnetX1=δnetX[0:nodedim,:],δnetX2=δnetX[nodedim:,:]
δ
n
e
t
m
=
W
δ
X
∗
f
′
(
m
)
,
δ
n
e
t
m
1
=
δ
n
e
t
m
[
0
:
n
o
d
e
d
i
m
,
:
]
,
δ
n
e
t
m
2
=
δ
n
e
t
m
[
n
o
d
e
d
i
m
:
,
:
]
δ_{net_m}=Wδ_{X}*f'(m),δ_{net{m_1}}=δ_{net_m}[0:nodedim,:],δ_{net_{m_2}}=δ_{net_m}[nodedim:,:]
δnetm=WδX∗f′(m),δnetm1=δnetm[0:nodedim,:],δnetm2=δnetm[nodedim:,:]
δ
n
e
t
k
=
W
δ
m
∗
f
′
(
k
)
,
δ
n
e
t
k
1
=
δ
n
e
t
k
[
0
:
n
o
d
e
d
i
m
,
:
]
,
δ
n
e
t
k
2
=
δ
n
e
t
k
[
n
o
d
e
d
i
m
:
,
:
]
δ_{net_k}=Wδ_{m}*f'(k),δ_{net{k_1}}=δ_{net_k}[0:nodedim,:],δ_{net_{k_2}}=δ_{net_k}[nodedim:,:]
δnetk=Wδm∗f′(k),δnetk1=δnetk[0:nodedim,:],δnetk2=δnetk[nodedim:,:]
其中,W是所有节点共用的。nodedim表示节点数据的维度。
4、权重W,B的更新
我们可以看到,权重W在树的每一层都出现了,我们在更新W的时候,就需要将每一层对于W的更新做一个累加和。下面,根据上面的例子进行逐层计算W,并累加。
4.1 逐层计算关于W的梯度
- 首先
X
1
,
X
2
,
r
o
o
t
X_1,X_2,root
X1,X2,root层的W,根据计算公式
n
e
t
r
o
o
t
=
W
T
X
+
B
net_{root}=W^TX+B
netroot=WTX+B有:
∂ n e t Y ∂ W 111 = ∂ n e t Y 1 ∂ W 111 = X 11 \frac{∂net_Y}{∂W_{111}}=\frac{∂net_{Y_1}}{∂W_{111}}=X_{11} ∂W111∂netY=∂W111∂netY1=X11
∂ n e t Y ∂ W 112 = ∂ n e t Y 2 ∂ W 112 = X 11 \frac{∂net_Y}{∂W_{112}}=\frac{∂net_{Y_2}}{∂W_{112}}=X_{11} ∂W112∂netY=∂W112∂netY2=X11
∂ n e t Y ∂ W 121 = ∂ n e t Y 1 ∂ W 121 = X 12 \frac{∂net_Y}{∂W_{121}}=\frac{∂net_{Y_1}}{∂W_{121}}=X_{12} ∂W121∂netY=∂W121∂netY1=X12
∂ n e t Y ∂ W 122 = ∂ n e t Y 2 ∂ W 122 = X 12 \frac{∂net_Y}{∂W_{122}}=\frac{∂net_{Y_2}}{∂W_{122}}=X_{12} ∂W122∂netY=∂W122∂netY2=X12
同理,可以计算关于第二个子节点 X 2 X_2 X2的梯度。将上述的计算过程总结成向量的形式可以看到:
∂ J ∂ W X = X δ n e t Y T \frac{∂J}{∂W_X}=Xδ_{net_Y}^T ∂WX∂J=XδnetYT - 对于上述的过程,扩展到其他层有:
∂ J ∂ W m = m δ n e t X T \frac{∂J}{∂W_m}=mδ_{net_X}^T ∂Wm∂J=mδnetXT
∂ J ∂ W k = k δ n e t m T \frac{∂J}{∂W_k}=kδ_{net_m}^T ∂Wk∂J=kδnetmT
4.2 权重B的更新
B的更新相对于W要简单很多。同样也是将每一层对于B的更新的梯度的求和。
- 首先
X
1
,
X
2
,
r
o
o
t
X_1,X_2,root
X1,X2,root层的B,根据计算公式
n
e
t
r
o
o
t
=
W
T
X
+
B
net_{root}=W^TX+B
netroot=WTX+B有:
∂ n e t Y ∂ B 1 = ∂ n e t Y 1 ∂ B 1 = 1 \frac{∂net_Y}{∂B_{1}}=\frac{∂net_{Y_1}}{∂B_{1}}=1 ∂B1∂netY=∂B1∂netY1=1
∂ n e t Y ∂ B 2 = ∂ n e t Y 2 ∂ B 2 = 1 \frac{∂net_Y}{∂B_{2}}=\frac{∂net_{Y_2}}{∂B_{2}}=1 ∂B2∂netY=∂B2∂netY2=1
则 n e t Y net_Y netY关于B的梯度值为: δ n e t Y δ_{net_Y} δnetY。
同理可以推导到其他层中:
δ n e t X δ_{net_X} δnetX
δ n e t m δ_{net_m} δnetm
4.2 求和更新
∂
J
∂
W
=
∂
J
∂
W
X
+
∂
J
∂
W
m
+
∂
J
∂
W
k
=
∑
i
=
X
,
m
,
k
∂
J
∂
W
i
\frac{∂J}{∂W}=\frac{∂J}{∂W_X}+\frac{∂J}{∂W_m}+\frac{∂J}{∂W_k}=∑_{i=X,m,k}\frac{∂J}{∂W_i}
∂W∂J=∂WX∂J+∂Wm∂J+∂Wk∂J=i=X,m,k∑∂Wi∂J
∂
J
∂
B
=
δ
n
e
t
Y
+
δ
n
e
t
X
+
δ
n
e
t
m
\frac{∂J}{∂B}=δ_{net_Y}+δ_{net_X}+δ_{net_m}
∂B∂J=δnetY+δnetX+δnetm
W
n
e
w
=
W
−
α
∂
J
∂
W
W_{new}=W-α\frac{∂J}{∂W}
Wnew=W−α∂W∂J
B
n
e
w
=
B
−
α
∂
J
∂
B
B_{new}=B-α\frac{∂J}{∂B}
Bnew=B−α∂B∂J
5、反向传播及误差更新的实现
def backward(self,parent_delta):
self.calc_delta(parent_delta,self.root)
self.W_grad,self.B_grad = self.calc_gradient(self.root)
def calc_delta(self,parent_delta,parent):
'''
计算δ值
'''
parent.delta = parent_delta
if parent.children:
children_delta = np.dot(self.W,parent_delta)
slices = [(i,i*self.node_dim,(i+1)*self.node_dim) for i in range(self.child_count)]
#针对每一个子节点,计算梯度
for s in slices:
self.calc_delta(children_delta[s[1]:s[2]],parent.children[s[0]])
def calc_gradient(self,parent):
'''
计算关于W和B的梯度的累加和
'''
W_grad = np.zeros((self.child_count*self.node_dim,self.node_dim))
B_grad = np.zeros((self.node_dim,1))
if not parent.children:
return W_grad,B_grad
parent.W_grad = np.dot(parent.children_data,parent.delta.T)
parent.B_grad = parent.delta
W_grad += parent.W_grad
B_grad += parent.B_grad
for child in parent.children:
W,B = self.calc_gradient(child)
W_grad += W
B_grad += B
return W_grad,B_grad
def update(self):
self.W -= self.lr * self.W_grad
self.B -= self.lr * self.B_grad
注意:在代码中,为了计算方便,选择了y=x的激活方式