转:TensorFlow入门(六) 双端 LSTM 实现序列标注(分词)
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:
- 对于前向 fw_cell ,样本按照x1,x2,...,x10 的顺序输入 cell 中,得到第一组状态输出 {h1,h2,...,h10} ;
- 对于反向 bw_cell ,样本按照 x10,x9,...,x1 的反序输入 cell 中,得到第二组状态输出 {h10,h9,...,h1 };
- 得到的两组状态输出的每个元素是一个长度为 hidden_size 的向量(一般情况下,h1和h1长度相等)。现在按照下面的形式把两组状态变量拼起来{[h1,h1], [h2,h2], … , [h10,h10]}。
- 最后对于每个 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 实现序列标注(分词)的更多相关文章
- LSTM + linear-CRF序列标注笔记
CRF 许多随机变量组成一个无向图G = {V, E},V代表顶点,E代表顶点间相连的边, 每个顶点代表一个随机变量,边代表两个随机变量间存在相互影响关系(变量非独立), 如果随机变量根据图的结构而具 ...
- LSTM+CRF进行序列标注
为什么使用LSTM+CRF进行序列标注 直接使用LSTM进行序列标注时只考虑了输入序列的信息,即单词信息,没有考虑输出信息,即标签信息,这样无法对标签信息进行建模,所以在LSTM的基础上引入一个标签转 ...
- TensorFlow入门(五)多层 LSTM 通俗易懂版
欢迎转载,但请务必注明原文出处及作者信息. @author: huangyongye @creat_date: 2017-03-09 前言: 根据我本人学习 TensorFlow 实现 LSTM 的经 ...
- SPOJ - ADALIST,双端队列入门模板!
ADALIST - Ada and List 这道题时限6.5s,激动人心啊,好多人STL一顿乱写AC,哈哈,如果熟悉双端队列的话这道题其实是很水的. 题意:n个数的数列,然后接下来Q次操作,每次可以 ...
- 六.基本数据结构-双端队列(Deque)
一.双端队列(Deque) - 概念:deque(也称为双端队列)是与队列类似的项的有序集合.它有两个端部,首部和尾部,并且项在集合中保持不变. - 特性:deque 特殊之处在于添加和删除项是非限制 ...
- (转)TensorFlow 入门
TensorFlow 入门 本文转自:http://www.jianshu.com/p/6766fbcd43b9 字数3303 阅读904 评论3 喜欢5 CS224d-Day 2: 在 Da ...
- 《算法实战策略》-chaper19-队列、栈和双端队列
对于计算机专业的学生来说,他们一定会很熟悉一句话:程序设计 = 算法 + 数据结构.而根据笔者的理解,所谓程序设计其实就是为了编程解决实际问题,所谓算法是一种解决问题某种思维的方法,但是思维需要得到编 ...
- 脑残式网络编程入门(六):什么是公网IP和内网IP?NAT转换又是什么鬼?
本文引用了“帅地”发表于公众号苦逼的码农的技术分享. 1.引言 搞网络通信应用开发的程序员,可能会经常听到外网IP(即互联网IP地址)和内网IP(即局域网IP地址),但他们的区别是什么?又有什么关系呢 ...
- 网络编程懒人入门(六):深入浅出,全面理解HTTP协议
本文引用了自简书作者“涤生_Woo”的文章,内容有删减,感谢原作者的分享. 1.前言 HTTP(全称超文本传输协议,英文全称HyperText Transfer Protocol)是互联网上应用最为广 ...
随机推荐
- CSS选择符、属性继承、优先级算法以及CSS3新增伪类、新特性
CSS 选择符有哪些?哪些属性可以继承?优先级算法如何计算? CSS3新增伪类有那些?CSS新增了哪些特性?下面我整理了一些,仅供参考. CSS 选择符: 1) id选择器(# myid) ...
- [Java]类的生命周期(下)类的初始化[转]
上接深入java虚拟机——深入java虚拟机(二)——类加载器详解(上),在上一篇文章中,我们讲解了类的生命周期的加载和连接,这一篇我们接着上面往下看. 类的初始化:在类的生命周期执行完加载和连接之后 ...
- springmvc学习总结(二) -- maven+springmvc+spring+mybatis+mysql详细搭建整合过程讲解
@_@ 写在最前 之前分享过下面这几篇: mybatis学习笔记(五) -- maven+spring+mybatis从零开始搭建整合详细过程(上)(附demo和搭建过程遇到的问题解决方法) myba ...
- 2010-2011 ACM-ICPC, NEERC, Moscow Subregional Contest Problem H. Hometask 水题
Problem H. Hometask 题目连接: http://codeforces.com/gym/100714 Description Kolya is still trying to pass ...
- android adb命令 unable to connect to 192.168.1.155:5555
如果使用有线网络无法用adb connect命令连接设备的话,可以选择使用无线wifi来连接. 首先在android设备上装一个叫做Adb Wireless的软件,打开wifi,然后打开adb wir ...
- delphi Ribbon 111
Ribbon上包含以下一些元素,如图所示: 元素对应API: Element Ribbon API Quick Access Toolbar RibbonControl.ToolbarRibbonQu ...
- 一个button导致的慘案
Win8名存实亡,Win9未出已亡.Win10会如何呢? 微软于2014年北京时间10月1日凌晨在旧金山召开新品公布会,之前盛传所谓的"windows 9"变成了"Win ...
- canvas使用2
文字对齐方式 : 水平对齐 ? 1 2 3 4 //是用 textAlign 属性设置水平对齐方式(默认坐标点) ctx.textAlign = "start"; ctx.font ...
- C#编程(二十三)----------实现继承
原文链接:http://blog.csdn.net/shanyongxu/article/details/46593809 如果要声明派生自另一个类的一个类,可以使用下面的语法: class Deri ...
- WordPress基础:get_page_link获取页面地址
函数:get_page_link(页面id编号) 作用:获取指定页面的链接地址 用法: $link = get_page_link(2); 输出为:xxx/?page_id=2 如在循环里则不用填写i ...