版权声明:本文为博主原创文章,未经博主允许不得转载。
一、项目分析
1.1 项目介绍
本文介绍一个比较流行的网络,也是比较基础的序列网络,目前序列网络在很多应用场景中被用到,如:机器翻译、文本摘要提取、评价数据分析等,在NLP中传统的rnn效率太低,准确度也不够,Seq2seq的出现很好的解决了NLP使用的难点。下面以“利用Seq2seq对输入的字母排序”为例,进行讲解。
二、任务分析
2.1 目标
实现一个基础版的Seq2Seq:输入一个单词(字母序列),模型将返回一个对字母排序后的“单词”
2.2 任务
按字母顺序排序:输入:hello ——> 输出:ehllo
三、Seq2Seq(sequence-to-sequence)
3.1 RNN结构
根据输出和输入序列不同数量rnn可以有多种不同的结构,不同结构自然就有不同的引用场合。如下图所示:
- one to one 结构:仅仅只是简单的给一个输入得到一个输出,此处并未体现序列的特征,如图像分类场景。
- one to many 结构:给一个输入得到一系列输出,这种结构可用于生产图片描述的场景。
- many to one 结构:给一系列输入得到一个输出,这种结构可用于文本情感分析,对一系列的文本输入进行分类,看是消极还是积极情感。
- many to many 结构,给一系列输入得到一系列输出,这种结构可用于翻译或聊天对话场景,对输入的文本转换成另外一系列文本。
- 同步 many to many 结构,它是经典的rnn结构,前一输入的状态会带到下一个状态中,而且每个输入都会对应一个输出,我们最熟悉的就是用于字符预测了,同样也可以用于视频分类,对视频的帧打标签。
3.2 seq2seq网络架构
3.3 Seq2seq模型核心思想
seq2seq模型是在2014年,是由Google Brain团队和Yoshua Bengio 两个团队各自独立的提出来,他们发表的文章主要关注的是机器翻译相关的问题。而seq2seq模型,简单来说就是一个翻译模型,把一个语言序列翻译成另一种语言序列,整个处理过程是通过使用深度神经网络( LSTM :长短记忆网络),或RNN (递归神经网络) 将一个序列作为输入映射为另外一个输出序列,如下图所示:
上图已经是在时间维度上进行了展开,对于没有展开的情况下,一般左边使用一个神经网络,接收输入序列”A B C EOS ( EOS=End of Sentence,句末标记)”,在这个过程中每一个时间点接收一个词或者字,并在读取的EOS时终止接受输入,最后输出一个向量作为输入序列的语义表示向量,这一过程也被称为 编码(Encoder)过程,而第二个神经网络接收到第一个神经网络产生的输出向量后输出相应的输出语义向量,并且在这个时候每一个时刻输出词的概率都与前一个时刻的输出有关系,模型会将这些序列一次映射为”W X Y Z EOS”,这一过程也被称为解码 (Decoder)过程,这样就实现了句子的翻译过程。整个过程的结构如下图:
3.4 解码和编码过程
和3.4介绍相似,整个模型分为解码和编码的过程,编码的过程结束后悔输出一个语义向量C,之后整个解码过程根据C进行相应的学习输出。
对于整个编码的过程就是3.2介绍的RNN网络学习的的过程,最后输出一个向量C。而对于解码过程,对应的是另外一个RNN网络,其隐藏层状态在t时刻的更新根据如下方程进行更新:
除了新加了C变量以外,其它和RNN原本的函数关系是一样的。类似的条件概率公式可以写为:
对于整个输入编码和解码的过程中,使用梯度优化算法以及最大似然条件概率为损失函数去进行模型的训练和优化:
其中:θ为模型中的参数,(xn, yn)是输入和输出的序列。
3.5 Seq2seq组成
基础Seq2Seq主要包含3部分:
- Encoder(编码过程):-一个单独的rnn过程
- 隐层状态向量:连接Encoder和Decoder的中间状态向量,长度是固定的:中间状态维护
- Decoder(解码过程):一个单独的rnn过程
【注意】rnn不接收可变序列,所以序列的长度一定要一致
四、代码实现:分部实现
4.1 查看Tensorflow 版本
1.下载:Tensorflow离线安装包
2.安装:pip install tensorflow
from distutils.version import LooseVersion
from tensorflow.python.layers.core import Dense
import tensorflow as tf
# Check Tensorflow Version
assert LooseVersion(tf.__version__) >= LooseVersion('1.1'), 'Please use Tensorflow version 1.1 or newer'
# 打印 Tensorflow 版本
print('Tensorflow Version:{}'.format(tf.__version__))
【注意】我这里使用的是Tensorflow Version:1.10.0版本
4.2 加载/读取数据
数据集的准备:这里有两个文件分别是source.txt和target.txt,对应的分别是输入文件和输出文件
# 读取输入文件
with open('./data/letters_source.txt', 'r', encoding='UTF-8') as f:
source_data = f.read()
# 读取输出文件
with open('./data/letters_target.txt', 'r', encoding='UTF-8') as f:
target_data = f.read()
4.3 数据预处理
数据集的预处理:填充序列,序列字符和ID的转换
填充字符的含义:
- < PAD>:填充 / 补全字符 ((保证每次输入的大小都一样)
- < EOS>:解码器端的句子结束标识符
- < UNK>:低频词或者一些未遇到过的词等
- < GO>: 解码器端的句子 起始标识符。
【解释】
- .split()
- 通过指定分隔符为字符串进行切片,不指定第二个num参数的情况下,默认分隔所有
- data.split(’\n’)
- 将数据按照换行符进行切片,返回一个字符串列表
- for line in data.split(’\n’)
- 每次取字符串列表中的一个元素(即一个字符串)保存到line中
- for line in data.split(’\n’) for character in line
- 相当于一个嵌套循环,每次取字符串列表中的一个元素保存到line中,然后每次取line字符串的一个字符保存到character
- [character for line in data.split(’\n’) for character in line]
- 自动追加元素形成一个关于character的列表
- set([character for line in data.split(’\n’) for character in line])
- 将这个关于character的列表转化为集合(去掉重复的字符,便于后续每个数字对应一个字符,用数字来表示每个字符串)
- set_words = list(set([character for line in data.split(’\n’) for character in line]))
- 除去重复后的集合再次转化为列表,保存在列表set_words中
【解释】
- enumerate(special_words + set_words)
- enumerate() 函数用于将一个可遍历的数据对象(如列表、元组或字符串)组合为一个索引序列
- 将列表special_words + set_word连接成一个列表,将这个列表组合为一个索引序列
- int_to_cocab = {idx: word for idx, word in enumerate(special_words + set_words)}
- 字典解析,每次取出索引序列的一个索引和列表元素内容分别存到idx和word中,自动追加元素形成idx:word的字典
- vocab_to_int = {word: idx for idx, word in int_to_cocab.items()}
- 与上一条代码结构类似,内涵为每次取字典中key:value分别存储在idx,word中,自动追加元素word:idx形成字典vocab_to_int
- return int_to_cocab, vocab_to_int
- 最后函数返回两个字典:
int_to_vocab:idx:word(数字:字符)
vocab_to_int:word:idx(字符:数字)
注意,因为中间步骤set集合,所得的字符是无重复的
【解释】
- source_int = [[source_letter_to_int.get(letter, source_letter_to_int[’’])
for letter in line] for line in source_data.split(’\n’)]- 表达式比较长,不同于多重循环的列表解析,是包含列表解析的多重循环,建议从后往前看
- for line in source_data.split(’\n’)
- 对数据按照换行符进行切片,返回一个字符串的列表,循环编列列表,每次返回一个字符串赋值给line
- for letter in line
- 第二层循环,遍历上一步得到的line,每次返回一个字符赋值给letter
- [source_letter_to_int.get(letter, source_letter_to_int[’< UNK >’]) for letter in line]
- 将letter的值给表达式source_letter_to_int.get(letter, source_letter_to_int[’’]) 计算,字典的get函数用来返回指定键的值,如果值不在字典中返回默认值,这里每次返回一个字符串中一个字母的value值(数字),这里返回的是一个数字是因为字典source_letter_to_int的key是字母,value是对应的数字,如果没有呢就返回标签UNK对应的数字。这个表达式外面有一个列表的括号[],表示遍历完一个字符串,追加完成一个列表。整个表达式最外层还有一层列表括号,表示这一个字符串大列表转化为包含若干个字符串小列表的列表。
【解释】
- target_int = [[target_letter_to_int.get(letter, source_letter_to_int[’< UNK>’])
for letter in line] + [target_letter_to_int[’< EOS>’]] for line in target_data.split(’\n’)]- for line in target_data.split(’\n’)
- 将数据按照换行符进行切片,循环遍历字符串列表,每次返回一个字符串赋值给line
- [target_letter_to_int.get(letter, source_letter_to_int[’< UNK>’]) for letter in line]
- 这里将循环遍历line,每次返回一个字符赋值给letter,然后用letter计算get方法的表达式,这里get方法和上行代码一致,若在target_letter_to_int字典中找到字符对应的数字编码,那么返回value值,如果找不到,返回不常见字符的对应的数字编码
- [target_letter_to_int.get(letter, source_letter_to_int[’< UNK>’]) for letter in line] + [target_letter_to_int[’< EOS>’]
- 当把每个字符串转化为字典中对应的数字编码时,在每个字符串结尾追加一个结束字符对应的数字编码
def extract_character_vocab(data):
'''构造映射表'''
# 存在问题:特殊字符 / 数据量大 / 等问题:<UNK> <PAD>
# 使用特定的字符进行序列的填充
special_words = ['<PAD>', '<UNK>','<GO>', '<EOS>']
# 映射: 字符 ——> ID ——> 向量
set_words = list(set([character for line in data.split('\n') for character in line]))
# 这里要把4个特殊字符添加进字典
int_to_cocab = {
idx: word for idx, word in enumerate(special_words + set_words)} # 0: '<PAD>', ..., 7: 's'
vocab_to_int = {
word: idx for idx, word in int_to_cocab.items()} # '<PAD>': 0, ...., 's': 7
return int_to_cocab, vocab_to_int
# 构造映射表
source_int_to_letter, source_letter_to_int = extract_character_vocab(source_data)
target_int_to_letter, target_letter_to_int = extract_character_vocab(target_data)
# 对字母进行转换
source_int = [[source_letter_to_int.get(letter, source_letter_to_int['<UNK>'])
for letter in line] for line in source_data.split('\n')]
target_int = [[target_letter_to_int.get(letter, source_letter_to_int['<UNK>'])
for letter in line] + [target_letter_to_int['<EOS>']] for line in target_data.split('\n')]
# 查看一下转换结果:(对每一行中的每个词进行映射,注意有个终止符)
print(source_int[:5])
print(target_int[:5])
4.4 Encoder:创建编码层
在Encoder端,我们需要进行2步:
- 第一步:要对我们的输入进行Embedding
- 第二步:再把Embedding以后的向量传给RNN进行处理
在Embedding中,我们使用[tf.contrib.layers.embed_sequence],它会对每个batch执行Embedding操作。
tf.contrib.layers.embed_sequence
- 对序列数据执行Embedding操作,输入[batch_size, sequence_length]的tensor,返回[batch_size, sequence_length, embed_dim]的tensor.
- 如:
features = [[1,2,3], [4,5,6]]
outputs = tf.contrib.layers.embed_sequence(features, vocab_size, embed_dim)
如果embed_dim=4,输出结果为:
[
[[0.1,0.2,0.3,0.1],[0.2,0.5,0.7,0.2],[0.1,0.6,0.1,0.2]],
[[0.6,0.2,0.8,0.2],[0.5,0.6,0.9,0.2],[0.3,0.9,0.2,0.2]]
]
tf.contrib.rnn.MultiRNNCell
- 对RNN单元按序列堆叠。接受参数为一个由RNN cell组成的list。
- rnn_size:代表一个rnn单元中隐层节点数量,layer_nums代表堆叠的rnn cell个数
tf.nn.dynamic_rnn
- 构建RNN,接受动态输入序列。返回RNN的输出以及最终状态的tensor。
- dynamic_rnn与rnn的区别在于,dynamic_rnn对于不同的batch,可以接收不同的sequence_length。
- 如:第一个batch是[batch_size,10],第二个batch是[batch_size,20]。 而rnn只能接收定长的sequence_length。
构造Encoder层,参数说明:
- input_data:输入tensor
- rnn_size:rnn 隐层节点数量
- num_layers:堆叠的rnn cell数量
- source_sequence_length:源数据的序列长度
- source_vocab_size:源数据的词典大小
- encoding_embedding_size:embedding的大小
# 创建编码层
def get_encoder_layer(input_data, rnn_size, num_layers, source_sequence_length, source_vocab_size, encoding_embedding_size):
# Encoder embedding
encoder_embed_input = tf.contrib.layers.embed_sequence(input_data, source_vocab_size, encoding_embedding_size)
# RNN cell
def get_lstm_cell(rnn_size):
lstm_cell = tf.contrib.rnn.LSTMCell(rnn_size, initializer=tf.random_uniform_initializer(-0.1, 0.1, seed=2))
return lstm_cell
# 指定多个lstm
cell = tf.contrib.rnn.MultiRNNCell([get_lstm_cell(rnn_size) for _ in range(num_layers)])
# 返回output,state
encoder_output, encoder_state = tf.nn.dynamic_rnn(cell, encoder_embed_input, sequence_length=source_sequence_length, dtype=tf.float32)
return encoder_output, encoder_state
4.5 Decoder:创建解码层
对 target 数据进行预处理
(1)对编码之后的字符串进行处理,移除最后一个字符
def process_decoder_input(data, vocab_to_int, batch_size):
'''补充<GO>,并移除最后一个字符'''
# cut 掉最后一个字符
ending = tf.strided_slice(data, [0,0], [batch_size, -1], [1,1])
decoder_input = tf.concat([tf.fill([batch_size, 1], vocab_to_int['<GO>']), ending], 1)
return decoder_input
(2)创建解码层
构造Decoder层,参数说明:
- target_letter_to_int:target 数据的映射表
- decoding_embedding_size:embed 向量大小
- num_layers:堆叠的RNN单元数量
- rnn_size:RNN 单元的隐层节点数量
- target_sequence_length:target 数据序列长度
- max_target_sequence_length:target 数据序列最大长度
- encoder_state:encoder 端编码的状态向量
- decoder_input:decoder 端输入
def decoding_layer(target_letter_to_int, decoding_embedding_size, num_layers, rnn_size,
target_sequence_length, max_target_sequence_length, encoder_state, decoder_input):
# 1. Embedding:构建向量
# 目标词汇的长度
target_vocab_size = len(target_letter_to_int)
# 定义解码向量的维度大小
decoder_embeddings = tf.Variable(tf.random_uniform([target_vocab_size, decoding_embedding_size]))
# 解码之后向量的输出
decoder_embed_input = tf.nn.embedding_lookup