本周主要目标在于实现一个可用的循环神经网络。主要内容包括:循环神经网络以及求导(BPTT)、多层循环神经网络、文本向量化以及一个小的文本分类实践。实现过程中使用了 Numpy 用于矩阵运算。文章目的在于脱离编程语言束缚去学习算法,因此可以使用其他编程语言,同时语言中应该包含以下组件:矩阵乘法、矩阵分片。
正文没有使用机器学习库,但由于 Python 语言的速度原因,为了更快获得结果,使用了 Tensorflow 进行了预训练,这个过程不是必须的。文末会附这部分代码。
本周推荐阅读时间:15min。
1. 阅读建议
- 推荐阅读时间:15min
- 推荐阅读文章:Hochreiter S, Schmidhuber J. Long short-term memory[J]. Neural Computation, 1997, 9(8):1735-1780.
2. 软件环境
- Python3
- Numpy
3. 前置基础
- 链式求导
4. 数据描述
- 数据来源:谭松波整理语料
- 数据地址:ChnSentiCorp
- 数据简介:语料之中包含:酒店、电脑(笔记本)与书籍等领域。是包含正负样本的平衡数据集。
5. 理论部分
5.1 循环神经网络正向传播过程
循环神经网络的输入是时序相关的,因此其输入与输入可以描述为 $h_1,\cdots,h_T$、$y_1,\cdots,y_T$,为了保持时序信息,最简单的 RNN 函数形式为:
$$\begin{matrix}h_t=f(x_t,h_{t-1})\\\rightarrow h_t=tanh(concat[x_t, h_{t-1}]\cdot W+b)\\\rightarrow tanh(x_t\cdot W1+h_{t-1}\cdot W2+b)\end{matrix}$$(1.1)
其中 $x_t$ 的形式为 [BATCHSIZE, Features1],$h_t$ 的形式为 [BatchSize, Features2]。多层 RNN 网络可以在输出的基础上继续加入 RNN 函数:
$$\begin{matrix}h^l_t=f(x_t,h^l_{t-1})\\h^{l+1}_t=f(h^l_t, h^{l+1}_{t-1})\end{matrix}$$(1.2)
对于简单的 RNN 来说,状态向量与输出相同。
5.2 文本向量化正向传播
循环神经网络(RNN)产生之初就是为了处理文本问题。但是神经网络本身只能处理浮点型数据,因此需要将文本转化为适合于神经网络处理的向量。对于 1.1 式之中的 $x_t$ 为向量化后的文本,其是固定长度的(本文设定为 128)。下面对文本向量化过程进行叙述:
对文本每个字符进行单独编号 -> 整形数字 -> OneHot 向量 -> 乘以降维矩阵 $W_{N, 128}$(N 为字符个数)
在处理文本过程之中我们需要对所有文本之中不重复的字符(或词)进行单独编号(整形数字),编号是从 0 开始连续的。处理文本过程之中将文本转化为相应的整形数字表示。处理完成后将整形数字使用 OneHot 向量的方式进行表示。此时向量的维度很高(中文几千个字符,OneHot 向量长度也是如此)。为了将输入转化为适合处理的向量长度,需要乘以一个降维矩阵 $W_{N, 128}$,从而形成 $x_t$。这是对于向量一种线性降维的方式。
5.2 反向传播过程
RNN 函数反向传播过程与全链接网络类似:
$$\begin{matrix}e^l_{t-1}=\frac{\partial loss}{\partial h^l_{t-1}}=\frac{\partial loss}{\partial h^l_{t}}\circ f'(x_t,h^l_{t-1})\frac{\partial(x_t\cdot W1+h_{t-1}\cdot W2+b)}{\partial h_{t-1}}=e^l_t f' W2&(a)\\\Delta W1=\sum_t (x_t)^T\cdot (e_t^lf')&(b)\\\Delta W2=\sum_t (h_{t-1})^T\cdot (e_t^lf')&(c)\\e^{l}_{t}=\frac{\partial loss}{\partial h^{l}_{t}}=\frac{\partial loss}{\partial h^{l+1}_{t}}\circ f'(h_t^l,h^{l+1}_{t-1})\frac{\partial(h_t^l\cdot W1+h_{t-1}\cdot W2+b)}{\partial h_{t}^l}=e^{l+1}_t f' W1&(d)\end{matrix}$$(1.3)
1.3-a 称之为时间反向传播算法 BPTT,1.3-c 为层间传播。可训练参数为 1.3-bc。实际上传统的 RNN 网络与全链接网络并无不同。只是添加了时间反向传播项。
6. 代码部分
6.1 循环神经网络层
import numpy as npclass NN(): def __init__(self): """ 定义可训练参数 """ self.value = [] self.d_value = [] self.outputs = [] self.layer = [] self.layer_name = [] def tanh(self, x, n_layer=None, layer_par=None): epx = np.exp(x) enx = np.exp(-x) return (epx-enx)/(epx+enx) def d_tanh(self, x, n_layer=None, layer_par=None): e2x = np.exp(2 * x) return 4 * e2x / (1 + e2x) ** 2 def _rnncell(self, X, n_layer, layer_par): """ RNN正向传播层 """ W = self.value[n_layer][0] bias = self.value[n_layer][1] b, h, c = np.shape(X) _, h2 = np.shape(W) outs = [] stats = [] s = np.zeros([b, h2]) for itr in range(h): x = X[:, itr, :] stats.append(s) inx = np.concatenate([x, s], axis=1) out = np.dot(inx, W) + bias out = self.tanh(out) s = out outs.append(out) outs = np.transpose(outs, (1, 0, 2)) stats= np.transpose(stats, (1, 0, 2)) return [outs, stats] def _d_rnncell(self, error, n_layer, layer_par): """ BPTT层,此层使用上一层产生的Error产生向前一层传播的error """ inputs = self.outputs[n_layer][0] states = self.outputs[n_layer + 1][1] b, h, insize = np.shape(inputs) back_error = [np.zeros([b, insize]) for itr in range(h)] W = self.value[n_layer][0] bias = self.value[n_layer][1] dw = np.zeros_like(W) db = np.zeros_like(bias) w1 = W[:insize, :] w2 = W[insize:, :] for itrs in range(h - 1, -1, -1): # 每一个时间步都要进行误差传播 if len(error[itrs]) == 0: continue else: err = error[itrs] for itr in range(itrs, -1, -1): h = states[:, itr, :] x = inputs[:, itr, :] inx = np.concatenate([x, h], axis=1) h1 = np.dot(inx, W) + bias d_fe = self.d_tanh(h1) err = d_fe * err # 计算可训练参数导数 dw[:insize, :] += np.dot(x.T, err) dw[insize:, :] += np.dot(h.T, err) db += np.sum(err, axis=0) # 计算传递误差 back_error[itr] += np.dot(err, w1.T) err = np.dot(err, w2.T) self.d_value[n_layer][0] = dw self.d_value[n_layer][1] = db return back_error def basic_rnn(self, w, b): self.value.append([w, b]) self.d_value.append([np.zeros_like(w), np.zeros_like(b)]) self.layer.append((self._rnncell, None, self._d_rnncell, None)) self.layer_name.append("rnn")
6.2 文本向量化层
def _embedding(self, inputs, n_layer, layer_par): W = self.value[n_layer][0] F, E = np.shape(W) B, L = np.shape(inputs) # 转换成one-hot向量 inx = np.zeros([B * L, F]) inx[np.arange(B * L), inputs.reshape(-1)] = 1 inx = inx.reshape([B, L, F]) # 乘以降维矩阵 embed = np.dot(inx, W) return [embed] def _d_embedding(self, in_error, n_layer, layer_par): inputs = self.outputs[n_layer][0] W = self.value[n_layer][0] F, E = np.shape(W) B, L = np.shape(inputs) inx = np.zeros([B * L, F]) inx[np.arange(B * L), inputs.reshape(-1)] = 1 error = np.transpose(in_error, (1, 0, 2)) _, _, C = np.shape(error) error = error.reshape([-1, C]) # 计算降维矩阵的导数 self.d_value[n_layer][0] = np.dot(inx.T, error) return [] def embedding(self, w): self.value.append([w]) self.d_value.append([np.zeros_like(w)]) self.layer.append((self._embedding, None, self._d_embedding, None)) self.layer_name.append("embedding")
6.3 使用 RNN 网络最后一层输出用于后续处理
def _last_out(self, inputs, n_layer, layer_par): return [inputs[:, -1, :]] def _d_last_out(self, in_error, n_layer, layer_par): X = self.outputs[n_layer][0] b, h, c = np.shape(X) error = [[] for itr in range(h)] error[-1] = in_error return error def last_out(self): self.value.append([]) self.d_value.append([]) self.layer.append((self._last_out, None, self._d_last_out, None)) self.layer_name.append("text_error")
6.4 网络其他部分
其他部分与前面所讲全链接网络、卷积神经网络类似:
def _matmul(self, inputs, n_layer, layer_par): W = self.value[n_layer][0] return [np.dot(inputs, W)] def _d_matmul(self, in_error, n_layer, layer_par): W = self.value[n_layer][0] inputs = self.outputs[n_layer][0] self.d_value[n_layer][0] = np.dot(inputs.T, in_error) error = np.dot(in_error, W.T) return error def matmul(self, filters): self.value.append([filters]) self.d_value.append([np.zeros_like(filters)]) self.layer.append((self._matmul, None, self._d_matmul, None)) self.layer_name.append("matmul") def _sigmoid(self, X, n_layer=None, layer_par=None): return [1/(1+np.exp(-X))] def _d_sigmoid(self, in_error, n_layer=None, layer_par=None): X = self.outputs[n_layer][0] return in_error * np.exp(-X)/(1 + np.exp(-X)) ** 2 def sigmoid(self): self.value.append([]) self.d_value.append([]) self.layer.append((self._sigmoid, None, self._d_sigmoid, None)) self.layer_name.append("sigmoid") def _relu(self, X, *args, **kw): return [(X + np.abs(X))/2.] def _d_relu(self, in_error, n_layer, layer_par): X = self.outputs[n_layer][0] drelu = np.zeros_like(X) drelu[X>0] = 1 return in_error * drelu def relu(self): self.value.append([]) self.d_value.append([]) self.layer.append((self._relu, None, self._d_relu, None)) self.layer_name.append("relu") def forward(self, X): self.outputs.append([X]) net = [X] for idx, lay in enumerate(self.layer): method, layer_par, _, _ = lay net = method(net[0], idx, layer_par) self.outputs.append(net) return self.outputs[-2][0] def backward(self, Y): error = self.layer[-1][2](Y, None, None) self.n_layer = len(self.value) for itr in range(self.n_layer-2, -1, -1): _, _, method, layer_par = self.layer[itr] #print("++++-", np.shape(error), np.shape(Y)) error = method(error, itr, layer_par) return error def apply_gradient(self, eta): for idx, itr in enumerate(self.d_value): if len(itr) == 0: continue for idy, val in enumerate(itr): self.value[idx][idy] -= val * eta def fit(self, X, Y, eta=0.1): self.forward(X) self.backward(Y) self.apply_gradient(eta) def predict(self, X): self.forward(X) return self.outputs[-2]
7. 程序运行
7.1 文本分类网络模型
搭建文本分类网络时,网络模型为两层 RNN 网络,网络输出的最后一个时间步携带了整个文本的信息,因此使用最后一个输出搭建多层全链接网络用以后续处理,最终使用向量距离作为 loss 函数:
...#搭建网络模型method = NN()method.embedding(ew)method.basic_rnn(w1, b1)method.basic_rnn(w2, b2)method.last_out()method.matmul(w3)method.bias_add(b3)method.relu()method.matmul(w4)method.bias_add(b4)method.sigmoid()method.loss_square()for itr in range(1000): # 获取数据 inx, iny = ... pred = method.forward(inx) method.backward(iny) method.apply_gradient(0.001) if itr% 20 == 0: # 获取测试数据 inx, iny = ... pred = method.forward(inx) prd1 = np.argmax(pred, axis=1) prd2 = np.argmax(iny, axis=1) print(np.sum(prd1==prd2)/len(idx))
8. 附录
8.1 使用 TensorFlow 验证程序正确性
使用 TensorFlow 作为验证程序,验证方法为输出计算导数:
# 搭建多层神经网络batch_size = 1max_time = 10indata = tf.placeholder(dtype=tf.float64, shape=[batch_size, 10, 3])# 两层RNN网络cell = rnn.MultiRNNCell([rnn.BasicRNNCell(3) for itr in range(2)], state_is_tuple=True)state = cell.zero_state(batch_size, tf.float64)outputs = []states = []# 获取每一步输出,与状态for time_step in range(max_time): (cell_output, state) = cell(indata[:, time_step, :], state) outputs.append(cell_output) states.append(state)y = tf.placeholder(tf.float64, shape=[batch_size, 3])# 定义loss函数loss = tf.square(outputs[-1]-y)opt = tf.train.GradientDescentOptimizer(1)# 获取可训练参数weights = tf.trainable_variables()# 计算梯度grad = opt.compute_gradients(loss, weights)sess = tf.Session()sess.run(tf.global_variables_initializer())# 获取变量值与梯度w1, b1, w2, b2 = sess.run(weights)dw1, db1, dw2, db2 = sess.run(grad, feed_dict={indata:np.ones([batch_size, 10, 3]), y:np.ones([batch_size, 3])})dw1 = dw1[0]db1 = db1[0]dw2 = dw2[0]db2 = db2[0]method = NN()method.basic_rnn(w1, b1)method.basic_rnn(w2, b2)method.last_out()method.loss_square()method.forward(np.ones([batch_size, 10, 3]))method.backward(np.ones([batch_size, 3]))print("TF Gradients", np.mean(dw1), np.mean(db1), np.mean(dw2), np.mean(db2))rnn.loss(np.ones([batch_size, 3]))for itr in method.d_value: if len(itr) == 0:continue print("NP Gradinets", np.mean(dw1, np.mean(db1), np.mean(dw2), np.mean(db2))
验证分程序仅用于演示思路。
8.2 TensorFlow 预训练网络
import tensorflow as tfinput_x = tf.placeholder(tf.int32, [None, 30], name='input_x')input_y = tf.placeholder(tf.float32, [None, 2], name='input_y')embedding = tf.get_variable('embedding', [vocab_size, 128])embedding_inputs = tf.nn.embedding_lookup(embedding, input_x)# 多层rnn网络cells = [tf.contrib.rnn.BasicRNNCell(128) for _ in range(2)]rnn_cell = tf.contrib.rnn.MultiRNNCell(cells, state_is_tuple=True)_outputs, _ = tf.nn.dynamic_rnn(cell=rnn_cell, inputs=embedding_inputs, dtype=tf.float32)last = _outputs[:, -1, :] # 取最后一个时序输出作为结果net = tf.layers.dense(last, self.config.hidden_dim, name='fc1')net = tf.nn.relu(net)# 分类器logits = tf.layers.dense(net, 2, name='fc2')loss = tf.square(logits - input_y)....训练过程....# 获取变量并保存name = []array = []for itra, itrb in zip(tf.global_variables(), session.run(tf.global_variables())): name.append(itra.name) array.append(itrb)np.savez("par.npz", name=name, data=array)
本文首发于GitChat,未经授权不得转载,转载需与GitChat联系。
阅读全文: http://gitbook.cn/gitchat/activity/5b0eb962d0b199202f42912b
您还可以下载 CSDN 旗下精品原创内容社区 GitChat App ,阅读更多 GitChat 专享技术内容哦。