numpy手写NLP模型(四)———— RNN
1. 模型介绍
首先介绍一下RNN,RNN全程为循环神经网络,主要用来解决一些序列化具有顺序的输入的问题。普通的前馈神经网络的输入单一决定输出,输出只由输入决定,比如一个单调函数的拟合,一个x决定一个y,前馈神经网络可以直接拟合出一条曲线并得到不错的拟合效果。再举一个例子,给出sin(x)的值,假设x属于[0, 2π),让你输出x的值是多少,前馈神经网络还能直接做到吗?显然是不可以的,因为一个sin(x)在一个周期内对应着多个值,单一的信息是不能直接确定x的值的,因此无法简单地通过前馈神经网络做到很好的拟合。那么这个时候要推测x的值,就不仅需要sin(x)的值,还需要sin(x)的值在前几个时刻的值来共同确定,放在连续函数中来说就是不仅需要函数在这个点的值,还需要函数在这个点的导数才行。而RNN正是一个可以解决这样问题的神经网络,它不仅考虑当前的输入,还同时考虑之前多个时刻的输入,并由多个时刻的状态共同决定输出。
2. 模型
2.1 模型的输入
首先看看这个模型的结构图:
假设模型的原始输入是一段文本,这段文本显然是由一个一个单词挨个连接而成的,是有序的,比如一句话"I love you"和另一句话"You love me",意思就是不同的,尽管组成这两句话的单词是完全一样的。而RNN网络的输入就是这样一个有序的序列输入。同时每一个状态的输出不仅由当前节点的输入决定,同时还受上一时刻的状态所影响,这个理念和理解一句话中的某个单词很相似,当前单词的意思不仅由这个单词本身决定,还要受到之前的词,也就是当前单词所处的语境的影响。回到之前的例子,
2.2 模型的前向传播
首先,前文一直提到当前状态的输出不仅和当前的输入有关,还和上一个状态有关,体现在公式上就是:
2.3 模型的反向传播
模型的反向传播部分的话我就不多加阐述了,我参考的仍然是刘建平老师的博客,原博客讲得很好。其实自己也完完全全推导了一遍RNN的反向传播求导的公式,求出了最后每个元素求导的公式,奈何数学功底不够,没能强行变换成一个整体的公式,因为这个diag(1-h(t)^2)之前完全没想到可以这么整,可能你现在已经听不懂我在说啥了,但是如果你先自己推导一遍,然后卡住了之后再去看那个博客,你就会懂的(斜眼笑)
3. 模型的代码实现
目前代码还存在一些问题(也可能是很多问题),后续会修改
首先我们来看初始化部分:
window_size:表示输入单词的个数,也可以理解成 t
hidden_size:就是每个节点隐藏层的维数
embed_size:就是每个单词embedding的维数
n_class:就是分类的类数,比如单词预测中问题中,n_class就是词典的大小
# model.py
class RNN:
def __init__(self, window_size, hidden_size, embed_size, n_class):
self.hidden_size = hidden_size
self.window_size = window_size
self.n_classs = n_class
self.embed_size = embed_size
self.w = np.random.random((hidden_size, hidden_size))
self.u = np.random.random((hidden_size, embed_size))
self.v = np.random.random((n_class, hidden_size))
self.b = np.random.random((hidden_size, 1))
self.c = np.random.random((n_class, 1))
self.x = []
self.hidden_node_list = []
# window_size means how many words in each input batch
# for example, if i input "I love apple" into the network, then the window_size is 3
for i in range(window_size):
# initialize all the nodes in the hidden layer
# then the net will calculate some related value in the forward part
new_node = np.random.random((hidden_size, 1))
self.hidden_node_list.append(new_node)
然后看一下前向传播:
# model.py
def forward(self, x):
self.x = x
# default h0 = 0
h0 = np.zeros((self.hidden_size, 1))
# before calculate, we need to clear the node list of the net
self.hidden_node_list.clear()
# calculate some intermediate variables
for i in range(self.window_size):
new_node = Node()
if i == 0:
new_node.h = np.tanh(np.dot(self.u, x[0]) + np.dot(self.w, h0) + self.b)
else:
new_node.h = np.tanh(np.dot(self.u, x[i]) + np.dot(self.w, self.hidden_node_list[i - 1].h))
new_node.d_tanh = 1 - new_node.h ** 2
new_node.o = np.dot(self.v, new_node.h) + self.c
self.hidden_node_list.append(new_node)
明白了RNN前向传播的公式的话,forward这部分应该很好懂。
接着看RNN的反向传播,建议数学推导搞明白后再看代码,那样会很明了:
# model.py
def backward(self, target_output, lr):
h0 = np.zeros((self.hidden_size, 1))
# then calculate the gradient in each hidden layer
predict = softmax(self.hidden_node_list[-1].o)
for i in range(self.window_size):
j = self.window_size - i - 1
if i == 0:
self.hidden_node_list[j].e = np.dot(self.v.T, predict - target_output)
else:
temp = np.dot(self.w.T, diag(self.hidden_node_list[j + 1].d_tanh))
self.hidden_node_list[j].e = np.dot(temp, self.hidden_node_list[j + 1].e)
dloss_w = np.zeros((self.hidden_size, self.hidden_size))
dloss_u = np.zeros((self.hidden_size, self.embed_size))
dloss_b = np.zeros((self.hidden_size, 1))
dloss_c = predict - target_output
dloss_v = np.dot(predict-target_output, self.hidden_node_list[-1].h.T)
for i in range(self.window_size):
temp = np.dot(diag(self.hidden_node_list[i].d_tanh), self.hidden_node_list[i].e)
if i == 0:
# seem unnecessary~
dloss_w += 0
else:
dloss_w += np.dot(temp, self.hidden_node_list[i - 1].h.T)
dloss_b += temp
dloss_u += np.dot(temp, self.x[i].T)
self.w -= lr * dloss_w
self.u -= lr * dloss_u
self.v -= lr * dloss_v
self.b -= lr * dloss_b
self.c -= lr * dloss_c
最后附上本博客的github代码