http://blog.csdn.net/Jerr__y/article/details/70471066

欢迎转载,但请务必注明原文出处及作者信息。

@author: huangyongye 
@creat_date: 2017-04-19

前言 
本例子主要介绍如何使用 TensorFlow 来一步一步构建双端 LSTM 网络(听名字就感觉好腻害的样子),并完成序列标注的问题。先声明一下,本文中采用的方法主要参考了【中文分词系列】 4. 基于双向LSTM的seq2seq字标注这篇文章。该文章用 keras 框架来实现的双端 LSTM,在本例中,实现思路和该文章基本上一样,但是用 TensorFlow 来实现的。这个例子中涉及到的知识点比较多,包括 word embedding, Viterbi 算法等,但是就算你对这些不是非常了解,依然能够很好地理解本文。

本例的主要目的是讲清楚基于 TensorFlow 如何来实现双端 LSTM。通过本例的学习,你可以知道 Bi-directional LSTM 是怎么样一步一步计算的。为了讲清楚这个,我把封装好的 static_bidirectional_rnn 接口进行展开,自己手写实现了一遍。如果你只是想急着用一下看看效果的话,我也提供了static_bidirectional_rnn 接口的用法(其实网上多了去)。但是既然用这个东西,当然还是希望把细节也理解透更好。否则的话,还不如直接用 keras 几行就把模型建好了,中间的变量维度也不需要你过多地考虑,keras 框架已经写好了自动匹配的功能。但是你用 keras 跑了几个网络以后,一问你细节,你啥也不知道。所以,抱着学习的心态,从 TensorFlow 这一比较底层的框架上手还是能有不少收获的。另外,因为比较底层,我们可以比较灵活的进行模型修改(假设已经到了要改模型这一步…)

由于这个例子的代码比较长,本文主要就网络结构部分进行分析。其余的比如数据处理这些在这里只是简单介绍,想理解具体内容的欢迎移步 鄙人 GitHub,代码,数据 什么的全都放上去了。

如果你还不知道什么是 LSTM 的话,建议先看一下 (译)理解 LSTM 网络 (Understanding LSTM Networks by colah) 这篇文章。在理解 LSTM 的基础上,再去理解 双端 LSTM (Bi-directional LSTM)还是非常容易的。关于双端 LSTM 的原理,这里不做详细解释,下面这张图显示了 双端 RNN 的结构。

 
fig.1 Bi-RNN 按时间展开的结构

Bi-LSTM大致的思路是这样的,看图中最下方的输入层,假设一个样本(句子)有10个 timestep (字)的输入 x1,x2,...,x10。 现在有两个相互分离的 LSTMCell:

  1. 对于前向 fw_cell ,样本按照x1,x2,...,x10 的顺序输入 cell 中,得到第一组状态输出 {h1,h2,...,h10} ;
  2. 对于反向 bw_cell ,样本按照 x10,x9,...,x1 的反序输入 cell 中,得到第二组状态输出 {h10,h9,...,h1 };
  3. 得到的两组状态输出的每个元素是一个长度为 hidden_size 的向量(一般情况下,h1和h1长度相等)。现在按照下面的形式把两组状态变量拼起来{[h1,h1], [h2,h2], … , [h10,h10]}。
  4. 最后对于每个 timestep 的输入 xt, 都得到一个长度为 2*hidden_size 的状态输出 Ht= [ht,ht]。然后呢,后面处理方式和单向 LSTM 一样。

1. 数据说明

下面大概说一下数据处理,但是这不影响对模型的理解,可以直接跳到

1.1 原始语料在txt文件中,长的下面这个样子

人/b  们/e  常/s  说/s  生/b  活/e  是/s  一/s  部/s  教/b  科/m  书/e  ,/s  而/s  血/s  与/s  火/s  的/s  战/b  争/e  更/s  是/s  不/b  可/m  多/m  得/e  的/s  教/b  科/m  书/e  ,/s  她/s  确/b  实/e  是/s  名/b  副/m  其/m  实/e  的/s  ‘/s  我/s  的/s  大/b  学/e  ’/s  。/s   心/s  静/s  渐/s  知/s  春/s  似/s  海/s  ,/s  花/s  深/s  每/s  觉/s  影/s
  • 1
  • 2

1.2 根据标点符号进行切分,下面是一个 sample

 人/b  们/e  常/s  说/s  生/b  活/e  是/s  一/s  部/s  教/b  科/m  书/e
  • 1
  • 2

1.3 把每个字和对应的tag转为一一对应的 id

在转为 id 之前,跟下表这样。这样的东西我们没办法塞到模型里去训练呀,必须先转为数值才行呀。

words tags sentence_len
[人, 们, 常, 说, 生, 活, 是, 一, 部, 教, 科, 书] [b, e, s, s, b, e, s, s, s, b, m, e] 12
[而, 血, 与, 火, 的, 战, 争, 更, 是, 不, 可, 多, 得, 的, 教, …] [s, s, s, s, s, b, e, s, s, b, m, m, e, s, b, …] 17

Table.1 按标点切分后的片段

因为一般情况下,我们训练网络的时候都喜欢把输入 padding 到固定的长度,这样子计算更快。可是切分之后的句子长短不一,因此我们取 32 作为句子长度,超过 32 个字的将把多余的字去掉,少于 32 个字的将用特殊字符填充。处理之前,每个字 word <-> tag,处理完后变成 X <-> y。长度不足 32 的补0填充。(下面编码问题显示就这样,凑合着看吧。)

words:  [u'\u4eba' u'\u4eec' u'\u5e38' u'\u8bf4' u'\u751f' u'\u6d3b' u'\u662f'  u'\u4e00' u'\u90e8' u'\u6559' u'\u79d1' u'\u4e66']
tags: [u'b' u'e' u's' u's' u'b' u'e' u's' u's' u's' u'b' u'm' u'e']
X: [ 8 43 320 88 36 198 7 2 41 163 124 245 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
y: [2 4 1 1 2 4 1 1 1 2 3 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
  • 1
  • 2
  • 3
  • 4
  • 5
跳到这里!!!

总之,上面这些都不重要,重要的是我们模型的输入 shape 是下面这样子的。第一维表示样本个数,第二维是 timestep_size。其实还有个第三维大小是 1, 因为每个时刻就只输入一个字,确切地说就是一个数字,因为我们已经把它转为数值 id 的形式了。

X_train.shape=(205780, 32), y_train.shape=(205780, 32);
X_valid.shape=(51446, 32), y_valid.shape=(51446, 32);
X_test.shape=(64307, 32), y_test.shape=(64307, 32)
  • 1
  • 2
  • 3
  • 4

2. Bi-directional lstm 模型

2.1 模型分析

import tensorflow as tf
config = tf.ConfigProto()
config.gpu_options.allow_growth = True
sess = tf.Session(config=config)
from tensorflow.contrib import rnn
import numpy as np '''
For Chinese word segmentation.
'''
# ##################### config ######################
decay = 0.85
max_epoch = 5
max_max_epoch = 10
timestep_size = max_len = 32 # 句子长度
vocab_size = 5159 # 样本中不同字的个数,根据处理数据的时候得到
input_size = embedding_size = 64 # 字向量长度
class_num = 5
hidden_size = 128 # 隐含层节点数
layer_num = 2 # bi-lstm 层数
max_grad_norm = 5.0 # 最大梯度(超过此值的梯度将被裁剪) lr = tf.placeholder(tf.float32)
keep_prob = tf.placeholder(tf.float32)
batch_size = tf.placeholder(tf.int32) # 注意类型必须为 tf.int32
model_save_path = 'ckpt/bi-lstm.ckpt' # 模型保存位置 def weight_variable(shape):
"""Create a weight variable with appropriate initialization."""
initial = tf.truncated_normal(shape, stddev=0.1)
return tf.Variable(initial) def bias_variable(shape):
"""Create a bias variable with appropriate initialization."""
initial = tf.constant(0.1, shape=shape)
return tf.Variable(initial) X_inputs = tf.placeholder(tf.int32, [None, timestep_size], name='X_input')
y_inputs = tf.placeholder(tf.int32, [None, timestep_size], name='y_input')
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42

如果你看过我上一篇文章 TensorFlow入门(五)多层 LSTM 通俗易懂版 的话,应该已经知道 LSTM 是怎么实现的了(如果不懂的话请先把上篇文章看懂再继续往下看了)。

下面重点终于来啦!!! 
在这里,为了那些不懂 embedding 的朋友能够看懂下面的代码,我必须啰嗦几句说明一下什么是 word embedding。这是自然语言处理的一个大杀器,我们平时口口声声说的词向量就是这东西。在这个例子中我们指的是字向量,原理完全是一样的。刚才我们已经说过,每个 timestep 输入的是一个字对应的 id, 也就是一个整数。经过 embedding 操作之后,就变成了一个长度为 embedding_size(我们可以自己指定字向量的长度) 的实数向量。具体它是怎么做的呢?如果你是做自然语言处理的应该已经知道了,如果你不是做自然语言处理的呢,那就不用管了。反正,真正输入到 LSTMCell 中的数据 shape 长这样 [ batchsize, timestep_size, input_size ]。 input_size 是每个 timestep 输入样本的特征维度,如上个例子中就是MNIST字符每行的28个点,那么就应该 input_size=28。把你要处理的数据整理成这样的 shape 就可以了,管它什么 embedding。


def bi_lstm(X_inputs):
"""build the bi-LSTMs network. Return the y_pred"""
# ** 0.char embedding,请自行理解 embedding 的原理!!做 NLP 的朋友必须理解这个
embedding = tf.get_variable("embedding", [vocab_size, embedding_size], dtype=tf.float32)
# X_inputs.shape = [batchsize, timestep_size] -> inputs.shape = [batchsize, timestep_size, embedding_size]
inputs = tf.nn.embedding_lookup(embedding, X_inputs)
# ** 1.LSTM 层
lstm_fw_cell = rnn.BasicLSTMCell(hidden_size, forget_bias=1.0, state_is_tuple=True)
lstm_bw_cell = rnn.BasicLSTMCell(hidden_size, forget_bias=1.0, state_is_tuple=True)
# ** 2.dropout
lstm_fw_cell = rnn.DropoutWrapper(cell=lstm_fw_cell, input_keep_prob=1.0, output_keep_prob=keep_prob)
lstm_bw_cell = rnn.DropoutWrapper(cell=lstm_bw_cell, input_keep_prob=1.0, output_keep_prob=keep_prob)
# ** 3.多层 LSTM
cell_fw = rnn.MultiRNNCell([lstm_fw_cell]*layer_num, state_is_tuple=True)
cell_bw = rnn.MultiRNNCell([lstm_bw_cell]*layer_num, state_is_tuple=True)
# ** 4.初始状态
initial_state_fw = cell_fw.zero_state(batch_size, tf.float32)
initial_state_bw = cell_bw.zero_state(batch_size, tf.float32) # 下面两部分是等价的
# **************************************************************
# ** 把 inputs 处理成 rnn.static_bidirectional_rnn 的要求形式
# ** 文档说明
# inputs: A length T list of inputs, each a tensor of shape
# [batch_size, input_size], or a nested tuple of such elements.
# *************************************************************
# Unstack to get a list of 'n_steps' tensors of shape (batch_size, n_input)
# inputs.shape = [batchsize, timestep_size, embedding_size] -> timestep_size tensor, each_tensor.shape = [batchsize, embedding_size]
# inputs = tf.unstack(inputs, timestep_size, 1)
# ** 5.bi-lstm 计算(tf封装) 一般采用下面 static_bidirectional_rnn 函数调用。
# 但是为了理解计算的细节,所以把后面的这段代码进行展开自己实现了一遍。
# try:
# outputs, _, _ = rnn.static_bidirectional_rnn(cell_fw, cell_bw, inputs,
# initial_state_fw = initial_state_fw, initial_state_bw = initial_state_bw, dtype=tf.float32)
# except Exception: # Old TensorFlow version only returns outputs not states
# outputs = rnn.static_bidirectional_rnn(cell_fw, cell_bw, inputs,
# initial_state_fw = initial_state_fw, initial_state_bw = initial_state_bw, dtype=tf.float32)
# output = tf.reshape(tf.concat(outputs, 1), [-1, hidden_size * 2])
# *********************************************************** # ***********************************************************
# ** 5. bi-lstm 计算(展开)
with tf.variable_scope('bidirectional_rnn'):
# *** 下面,两个网络是分别计算 output 和 state
# Forward direction
outputs_fw = list()
state_fw = initial_state_fw
with tf.variable_scope('fw'):
for timestep in range(timestep_size):
if timestep > 0:
tf.get_variable_scope().reuse_variables()
(output_fw, state_fw) = cell_fw(inputs[:, timestep, :], state_fw)
outputs_fw.append(output_fw) # backward direction
outputs_bw = list()
state_bw = initial_state_bw
with tf.variable_scope('bw') as bw_scope:
inputs = tf.reverse(inputs, [1])
for timestep in range(timestep_size):
if timestep > 0:
tf.get_variable_scope().reuse_variables()
(output_bw, state_bw) = cell_bw(inputs[:, timestep, :], state_bw)
outputs_bw.append(output_bw)
# *** 然后把 output_bw 在 timestep 维度进行翻转
# outputs_bw.shape = [timestep_size, batch_size, hidden_size]
outputs_bw = tf.reverse(outputs_bw, [0])
# 把两个oupputs 拼成 [timestep_size, batch_size, hidden_size*2]
output = tf.concat([outputs_fw, outputs_bw], 2)
# output.shape 必须和 y_input.shape=[batch_size,timestep_size] 对齐
output = tf.transpose(output, perm=[1,0,2])
output = tf.reshape(output, [-1, hidden_size*2])
# *********************************************************** softmax_w = weight_variable([hidden_size * 2, class_num])
softmax_b = bias_variable([class_num])
logits = tf.matmul(output, softmax_w) + softmax_b
return logits y_pred = bi_lstm(X_inputs)
# adding extra statistics to monitor
# y_inputs.shape = [batch_size, timestep_size]
correct_prediction = tf.equal(tf.cast(tf.argmax(y_pred, 1), tf.int32), tf.reshape(y_inputs, [-1]))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
cost = tf.reduce_mean(tf.nn.sparse_softmax_cross_entropy_with_logits(labels = tf.reshape(y_inputs, [-1]), logits = y_pred)) # ***** 优化求解 *******
# 获取模型的所有参数
tvars = tf.trainable_variables()
# 获取损失函数对于每个参数的梯度
grads, _ = tf.clip_by_global_norm(tf.gradients(cost, tvars), max_grad_norm)
# 优化器
optimizer = tf.train.AdamOptimizer(learning_rate=lr)
# 梯度下降计算
train_op = optimizer.apply_gradients( zip(grads, tvars),
global_step=tf.contrib.framework.get_or_create_global_step())
print 'Finished creating the bi-lstm model.'
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99

过多的解释也没有了,都在上面的代码中!但还是得说一声,在这个双端 LSTM 模型中,重点要理解两点:

1. 两个 LSTM (cell_fw, cell_bw)的计算是各自独立的,只是最后输出的时候把二者的状态向量结合起来。
2. 本例中每个 timestep 都有结果输出,而上篇的分类问题中我们只拿最后一个 h_state 来计算最后的输出(注意这不是 Bi-LSTM 和 单向 LSTM 的区别, 单向的也可以每个 timestep 都输出)。注意本例中的 y_input 也对应的每个 timestep 对应一个 tag(id)。
  • 1
  • 2
  • 3

2.2 实验结果

看一下结果吧,不放结果都是耍流氓 : 
(1) 先是分类准确率

**Test 64307, acc=0.948665, cost=0.139884
  • 1
  • 2

(2)实际分词

人们 / 思考 / 问题 / 往往 / 不是 / 从 / 零开始 / 的 / 。 / 就 / 好 / 像 / 你 / 现在 / 阅读 / 这 / 篇 / 文章 / 一样 / , / 你 / 对 / 每个 / 词 / 的 / 理解 / 都会 / 依赖 / 于 / 你 / 前面 / 看到 / 的 / 一些 / 词 / , /        / 而 / 不是 / 把 / 你 / 前面 / 看 / 的 / 内容 / 全部 / 抛弃 / 了 / , / 忘记 / 了 / , / 再去 / 理解 / 这个 / 单词 / 。 / 也 / 就 / 是 / 说 / , / 人们 / 的 / 思维 / 总是 / 会 / 有 / 延续 / 性 / 的 / 。 /
  • 1
  • 2

结论:本例子使用 Bi-directional LSTM 来完成了序列标注的问题。本例中展示的是一个分词任务,但是还有其他的序列标注问题都是可以通过这样一个架构来实现的,比如 POS(词性标注)、NER(命名实体识别)等。在本例中,单从分类准确率来看的话差不多到 95% 了,还是可以的。可是最后的分词效果还不是非常好,但也勉强能达到实用的水平 (详细请看全部代码及说明: https://github.com/yongyehuang/Tensorflow-Tutorial/blob/master/Tutorial_6%20-%20Bi-directional%20LSTM%20for%20sequence%20labeling%20(Chinese%20segmentation).ipynb),而且模型也只是粗略地跑了一遍,还没有进行任何的参数优化。最后的维特比译码中转移概率只是简单的用了等概分布,如果能根据训练语料以统计结果作为概率分布的话相信结果能够进一步提高。

3.reference

[1] 【中文分词系列】 4. 基于双向LSTM的seq2seq字标注 http://spaces.ac.cn/archives/3924/ 
[2] https://github.com/yongyehuang/TensorFlow-Examples/blob/master/examples/3_NeuralNetworks/bidirectional_rnn.py 
[3] https://github.com/yongyehuang/deepnlp/blob/master/deepnlp/pos/pos_model_bilstm.py

转:TensorFlow入门(六) 双端 LSTM 实现序列标注(分词)的更多相关文章

  1. LSTM + linear-CRF序列标注笔记

    CRF 许多随机变量组成一个无向图G = {V, E},V代表顶点,E代表顶点间相连的边, 每个顶点代表一个随机变量,边代表两个随机变量间存在相互影响关系(变量非独立), 如果随机变量根据图的结构而具 ...

  2. LSTM+CRF进行序列标注

    为什么使用LSTM+CRF进行序列标注 直接使用LSTM进行序列标注时只考虑了输入序列的信息,即单词信息,没有考虑输出信息,即标签信息,这样无法对标签信息进行建模,所以在LSTM的基础上引入一个标签转 ...

  3. TensorFlow入门(五)多层 LSTM 通俗易懂版

    欢迎转载,但请务必注明原文出处及作者信息. @author: huangyongye @creat_date: 2017-03-09 前言: 根据我本人学习 TensorFlow 实现 LSTM 的经 ...

  4. SPOJ - ADALIST,双端队列入门模板!

    ADALIST - Ada and List 这道题时限6.5s,激动人心啊,好多人STL一顿乱写AC,哈哈,如果熟悉双端队列的话这道题其实是很水的. 题意:n个数的数列,然后接下来Q次操作,每次可以 ...

  5. 六.基本数据结构-双端队列(Deque)

    一.双端队列(Deque) - 概念:deque(也称为双端队列)是与队列类似的项的有序集合.它有两个端部,首部和尾部,并且项在集合中保持不变. - 特性:deque 特殊之处在于添加和删除项是非限制 ...

  6. (转)TensorFlow 入门

        TensorFlow 入门 本文转自:http://www.jianshu.com/p/6766fbcd43b9 字数3303 阅读904 评论3 喜欢5 CS224d-Day 2: 在 Da ...

  7. 《算法实战策略》-chaper19-队列、栈和双端队列

    对于计算机专业的学生来说,他们一定会很熟悉一句话:程序设计 = 算法 + 数据结构.而根据笔者的理解,所谓程序设计其实就是为了编程解决实际问题,所谓算法是一种解决问题某种思维的方法,但是思维需要得到编 ...

  8. 脑残式网络编程入门(六):什么是公网IP和内网IP?NAT转换又是什么鬼?

    本文引用了“帅地”发表于公众号苦逼的码农的技术分享. 1.引言 搞网络通信应用开发的程序员,可能会经常听到外网IP(即互联网IP地址)和内网IP(即局域网IP地址),但他们的区别是什么?又有什么关系呢 ...

  9. 网络编程懒人入门(六):深入浅出,全面理解HTTP协议

    本文引用了自简书作者“涤生_Woo”的文章,内容有删减,感谢原作者的分享. 1.前言 HTTP(全称超文本传输协议,英文全称HyperText Transfer Protocol)是互联网上应用最为广 ...

随机推荐

  1. 【BZOJ-4530】大融合 线段树合并

    4530: [Bjoi2014]大融合 Time Limit: 10 Sec  Memory Limit: 256 MBSubmit: 280  Solved: 167[Submit][Status] ...

  2. Graph database_neo4j 底层存储结构分析(2)

    3       neo4j存储结构 neo4j 中,主要有4类节点,属性,关系等文件是以数组作为核心存储结构:同时对节点,属性,关系等类型的每个数据项都会分配一个唯一的ID,在存储时以该ID 为数组的 ...

  3. 企点微服务网关演进之路 IT大咖说 - 大咖干货,不再错过

      http://www.itdks.com/dakashuo/new/dakalive/detail/2297

  4. NodeJS下的Mongodb操作

    今天用Node写一个小程序中需要用到数据库操作,试用了一下,发现官方的驱动已经非常好用了,也支持async. 一个简单的实例如下: const MongoClient = require('mongo ...

  5. LPC-LINK 2 LPC4337 TQFP144 IO

  6. LPC18xx and LPC43xx 选型及差异

    Q LPC43xx和LPC18xx有相同的USB接口以及片内USB驱动吗?A 是的. Q LPC4300和LPC1800只是内核不同吗?外设和管脚配置都完全一致?A 为LPC18xx的映像可以直接在L ...

  7. 如何让PictureBox背景色透明

    winform程序中的PictureBox载入了一张带有透明度的PNG图片,悬浮于其他控件之上,但是它的背景不是透明的,即使把它的BackColor设置为Color.Transparent,或者是0x ...

  8. [Go] 如何正确地 抛出 错误 和 异常(error/panic/recover)?

    序言 错误 和 异常 是两个不同的概念,非常容易混淆.很多程序员习惯将一切非正常情况都看做错误,而不区分错误和异常,即使程序中可能有异常抛出,也将异常及时捕获并转换成错误.从表面上看,一切皆错误的思路 ...

  9. Programming 2D Games 读书笔记(第三章)

      示例一:DirectX Window Graphics类用于初始化Direct 3D 主流程: 仅需要粗体部分 try{ // Create Graphics object graphics = ...

  10. C#网络编程技术SuperSocket实战项目演练

    一.SuperSocket课程介绍 1.1.本期<C#网络编程技术SuperSocket实战项目演练>课程阿笨给大家带来三个基于SuperSocket通讯组件的实战项目演示实例: ● 基于 ...