在Tensorflow中实现seq2seq模型

使用递归神经网络的数据生成正变得越来越流行,并且已在计算机科学的许多领域中使用。 自seq2seq概念于2014年问世以来,仅过去了五年,但全世界已经看到了许多应用,从经典的翻译和语音识别模型开始,到在照片中生成物体描述为止。


另一方面,随着时间的流逝,由Google专门为神经网络的开发而发布的Tensorflow库越来越受欢迎。 自然,Google开发人员不能忽略诸如seq2seq这样的流行范例,因此Tensorflow库提供了在该范例中进行开发的类。 本文介绍了此类系统。


循环网络


当前,递归网络是用于构建深度神经网络的最著名和最实际的形式主义之一。 递归网络旨在处理串行数据,因此,与普通单元(神经元)接收数据作为输入并输出计算结果不同,递归单元包含两个输入和两个输出。


输入中的一个表示序列的当前元素的数据,第二个输入称为状态,并作为序列的前一个元素的单元格计算结果进行传输。


图片

该图显示了单元格A,为其输入了序列元素的数据 xt以及这里未指出的情况 st1。 在输出时,单元格A给出状态 st和计算结果 ht


在实践中,数据序列通常分为一定固定长度的子序列,并通过整个子集(批)传递给计算。 换句话说,子序列是学习的例子。 递归网络的输入,输出和单元状态是实数序列。 用于输入计算 x1您必须使用非给定数据序列计算结果的状态。 这样的状态称为初始状态。 如果序列足够长,则有必要在每个子序列上保持计算的上下文。 在这种情况下,可以将先前序列中的最后计算的状态作为初始状态发送。 如果序列不是那么长,或者子序列是第一个片段,则可以用零初始化初始状态。


目前,为了在几乎所有地方训练神经网络,都使用了误差的反向传播算法。 对照预期结果(标记数据)检查已传输的示例集(在本例中为子序列集)的计算结果。 实际值与期望值之间的差称为误差,并且该误差沿相反的方向传播到网络权重。 因此,网络可以适应标记的数据,通常,这种适应的结果对于网络在初始训练示例中没有满足的数据(通用假设)非常有效。


在递归网络的情况下,我们有几种选择可以考虑错误的输出。 我们将在这里描述两个主要的:


  1. 您可以通过将子序列的最后一个单元格的输出与预期输出进行比较来考虑错误。 这对于分类任务非常有效。 例如,我们需要确定一条推文的情感色彩。 为此,我们选择鸣叫并将其标记为三类:负面,正面和中立。 单元格的输出将是三个数字-类别权重。 该推文还将标有三个数字-属于相应类别的推文概率。 计算完一部分数据的错误后,您可以根据需要在输出或状态中传播错误。
  2. 您可以立即在单元计算的输出中读取该子序列每个元素的错误。 这非常适合根据先前序列预测序列的下一个元素的任务。 例如,可以在确定数据时间序列异常的问题或预测文本中下一个字符的任务中使用此方法,以便随后生成它。 错误也可以通过状态或输出传播。

与常规的完全连接的神经网络不同,递归网络是深层的,因为误差不仅通过网络之间的连接从网络的输出向下传播到其权重,而且还向左传播。 因此,网络的深度由子序列的长度确定。 为了通过递归网络的状态传播错误,有一种特殊的算法 。 其特征是,当误差从右向左传播时,权重的梯度会彼此相乘。 如果初始误差大于1,则结果可能会变得非常大。 相反,如果初始误差小于1,则在序列开始的某个位置,误差可能会消失。 神经网络理论中的这种情况称为标准误差轮播。 为了避免训练中的这种情况,发明了不具有这种缺点的特殊细胞。 第一个这样的单元是LSTM ,现在有各种各样的替代方案,其中最流行的是GRU


这篇文章对递归网络有很好的介绍。 另一个著名的消息来源是Andrey Karpaty博客的一篇文章


Tensorflow库具有许多用于实现递归网络的类和功能。 这是一个基于GRU类型的单元创建动态递归网络的示例:


cell = tf.contrib.rnn.GRUCell(dimension) outputs, state = tf.nn.dynamic_rnn(cell, input, sequence_length=input_length, dtype=tf.float32) 

在此示例中,将创建一个GRU单元,然后将其用于创建动态递归网络。 输入数据张量和子序列的实际长度被传输到网络。 输入数据始终由实数向量指定。 对于单个值,例如符号代码或单词,所谓的 嵌入-将此代码映射到一些数字序列。 创建动态递归网络的功能返回一对值:该序列的所有值和最后计算的状态的网络输出列表。 作为输入,函数采用单元格,输入数据和子序列长度张量。


动态递归网络与静态递归网络的不同之处在于,它不预先为子序列创建网络单元网络(在确定计算图的阶段),而是在输入数据上图的计算过程中动态地在输入处启动单元。 因此,此功能需要知道输入数据的子序列的长度,以便在正确的时间停止。


基于递归网络生成模型


生成递归网络


之前,我们考虑了两种计算递归网络误差的方法:给定序列的最后一个输出或所有输出。 在这里,我们考虑生成序列的问题。 发电机网络培训基于上述第二种方法。


更详细地讲,我们正在尝试训练一个递归网络来预测序列的下一个元素。 如上所述,递归网络中单元的输出只是一个数字序列。 该向量对于学习不是很方便,因此,他们引入了另一个级别,该级别在输入端接收此向量,而在输出端给出预测的权重。 此级别称为“ 投影级别” ,使您可以将序列给定元素上单元格的输出与标记数据中的预期输出进行比较。


为了说明,请考虑生成以字符序列表示的文本的任务。 投影级别的输出向量的长度等于源文本字母的大小。 如果您计算俄语和英语语言的字符以及标点符号,则字母表的大小通常不超过150个字符。 投影级别的输出是一个具有字母长度的向量,其中每个符号对应于该向量中的某个位置-该符号的索引。 标记的数据也是由零组成的向量,其中一个位于序列后面字符的位置。


为了进行训练,我们使用两个数据序列:


  1. 源文本中的一系列字符,在其开头添加了一个特殊字符,该字符不属于源文本。 通常称为go
  2. 源文本的字符顺序保持不变,无需添加。

文本“妈妈洗框架”的示例:


 ['<go>', '', '', ', '', ' ', '', '', '', '', ' ', '', '', '', ''] ['', '', ', '', ' ', '', '', '', '', ' ', '', '', '', ''] 

为了训练,通常形成迷你批,包括少量示例。 在我们的情况下,这些字符串的长度可以不同。 下述代码使用以下方法来解决不同长度的问题。 在此微型封装中的许多行中,计算出最大长度。 所有其他行都填充有特殊字符(填充),因此迷你数据包中的所有示例的长度均相同。 在下面的代码示例中, 填充字符串用作此类字符。 另外,为了更好的生成,请在示例末尾添加句子符号的末尾-eos 。 因此,实际上,示例中的数据看起来会有些不同:


 ['<go>', '', '', ', '', ' ', '', '', '', '', ' ', '', '', '', '', '<eos>', '<pad>', '<pad>', '<pad>'] ['', '', ', '', ' ', '', '', '', '', ' ', '', '', '', '', '<eos>', '<pad>', '<pad>', '<pad>', '<pad>'] 

第一个序列被馈送到网络输入,第二个序列用作标记数据。 预测训练是基于将原始序列向左移动一个字符。


训练和产卵


培训课程


学习算法非常简单。 对于输入序列的每个元素,我们计算其投影级别的输出向量,并将其与标记的向量进行比较。 唯一的问题是如何计算误差。 您可以使用均方根误差,但是在这种情况下要计算误差,最好使用交叉熵 。 Tensorflow库提供了几个计算功能,尽管没有什么可以阻止在代码中直接执行计算公式。


为了清楚起见,我们引入一些符号。 通过symbol_id,我们将表示符号的标识符(字母的序列号)。 这里的术语符号相当随意,仅表示字母的一个元素。 字母可能不包含符号,但可能包含单词或什至更复杂的属性集。 术语symbol_embedding将用于表示与字母的给定元素相对应的数字向量。 通常,此类数字集存储在与字母大小匹配的大小表中。


Tensorflow提供了一项功能,允许您访问嵌入表并将字符索引替换为其嵌入向量。 首先,我们定义一个变量来存储表:


 embedding_table = tf.Variable(tf.random_uniform([alphabet_size, embedding_size])) 

之后,您可以将输入张量转换为嵌入张量:


 input_embeddings = tf.nn.embedding_lookup(embedding_table, input_ids) 

函数调用的结果是传递给输入的维数相同的张量,但是结果,所有字符索引都被相应的嵌入序列替换。


产生


为了进行计算,递归网络的像元需要状态和当前字符。 计算的结果是退出和新状态。 如果将投影级别应用于输出,我们可以得到一个权重向量,其中可以(非常有条件地)将对应位置的权重视为该符号出现在序列中下一个位置的概率。


基于投影级别生成的权重向量,可以使用多种策略来选择下一个符号:


  • 贪婪的搜索策略。 每次我们选择权重最高的符号,即 在这种情况下最有可能发生,但在整个序列中不一定是最合适的。
  • 选择最佳顺序的策略(光束搜索)。 我们不会一次选择一个符号,但会记住最可能的符号的几种变体。 在为生成的序列的所有元素计算完所有此类选项后,我们将考虑整个序列的上下文,选择最可能的字符序列。 通常,这是通过一个矩阵来实现的,该矩阵的宽度等于序列的长度,高度等于生成字符的变体数(波束搜索宽度)。 序列变体的生成完成后, 使用维特比算法的变体之一来选择最可能的序列。

Tensorflow库seq2seq类型系统


鉴于以上所述,很明显,基于递归网络的生成模型的实现对于编码而言是一项相当困难的任务。 因此,自然地,提出了分类系统以促进该问题的解决。 这些系统之一称为seq2seq,然后我们描述其主要类型的功能。


但是,首先,有关库名称的几句话。 名称seq2seq是序列到序列(从序列到序列)的缩写。 提出了产生序列的最初想法是为了实施翻译系统。 单词的输入序列被馈送到递归网络的输入,该系统在此系统中称为编码器。 该递归网络的输出是序列最后一个字符上的像元计算状态。 此状态被表示为第二个递归网络(解码器)的初始状态,该网络经过训练可以生成下一个单词。 该词在两个网络中均用作符号。 装饰器上的错误通过传输状态传播到编码器。 在此术语中,状态向量本身称为思想向量。 中间表示法在传统翻译模型中使用,并且通常是代表输入的翻译文本结构的图形。 翻译系统根据此中间结构生成输出文本。


实际上,在Tensorflow中seq2seq的实现属于解码器部分,而不会影响编码器。 因此,调用2seq库是正确的,但是传统的优势和思维的惯性显然优于常识。


seq2seq库中的两个主要亚型是:


  1. 助手类。
  2. 解码器

库开发人员基于以下考虑因素确定了这些类型。 让我们从稍微不同的角度考虑我们上面描述的学习过程和生成过程。


要进行培训,您需要:


  1. 对于每个字符,继续进行当前状态的计算并嵌入当前字符。
  2. 记住输出状态和为输出计算的投影。
  3. 获取序列中的下一个字符,然后转到步骤1。

之后,您可以通过将计算结果与序列的以下字符进行比较来开始计算错误。


要生成它,必须:


  1. 对于每个字符,继续进行当前状态的计算并嵌入当前字符。
  2. 记住输出状态和为输出计算的投影。
  3. 计算下一个字符作为投影级别索引的最大值,然后转到步骤1。

从描述中可以看出,算法非常相似。 因此,该库的开发人员决定将获取下一个字符的过程封装在Helper类中。 为了进行训练,这只是从序列中读取下一个字符,并为生成它而选择权重最大的字符(当然,对于贪婪的搜索)。


因此,Helper基类实现next_inputs方法以从当前和状态中获取下一个字符,以及示例方法以从投影级获取字符索引。 为了实现训练,提供了TrainingHelper类,为了实现通过贪婪搜索方法的生成, 提供GreedyEmbeddingHelper类。 不幸的是,光束搜索模型不适用于这种类型的系统,因此, 为此在库中实现了特殊的BeamSearchDecoder类。 不使用助手。


Decoder类提供用于实现解码器的接口。 实际上,该类提供了两种方法:


  1. 初始化在工作开始时进行初始化。
  2. 实施学习步骤或生成的步骤。 此步骤的内容由相应的Helper确定。

该库实现BasicDecoder类,该类可与TrainingHelper和GreedyEmbeddingHelper助手一起用于训练和繁殖。 这三个类通常足以实现基于递归网络的生成模型。


最后, dynamic_decode函数用于组织通过输入或生成的序列的段落。


接下来,我们将考虑一个说明性示例,该示例显示了用于构造各种类型seq2seq库的生成模型的方法。


说明性例子


首先,应该说所有示例都在Python 2.7中实现。 在requests.txt文件中可以找到其他库的列表。


作为说明性示例,请考虑由Google Kaggle在2017年举办的`` 文本标准化挑战-俄语''竞赛的部分数据。 这项比赛的目的是将俄语文本转换为适合阅读的形式。 比赛的文字被分解为带类型的表达式。 培训数据在以下格式的CSV文件中指定:


 "sentence_id","token_id","class","before","after" 0,0,"PLAIN","","" 0,1,"PLAIN","","" 0,2,"PLAIN","","" 0,3,"DATE","1862 ","    " 0,4,"PUNCT",".","." 1,0,"PLAIN","","" 1,1,"PLAIN","","" 1,2,"PLAIN","","" 1,3,"PLAIN","","" 1,4,"PLAIN","","" 1,5,"PLAIN","","" 1,6,"PLAIN","","" 1,7,"PLAIN","","" 1,8,"PLAIN","","" 1,9,"PUNCT",".","." ... 

在上面的示例中,类型DATE的表达式很有趣;在其中,“ 1862”被转换为“一千八百六十二年”。 为了说明这一点,我们仅将DATE类型的数据视为以下形式的对(表达式之前,表达式之后)。 数据文件的开始:


 before,after 1862 ,     1811 ,    12  2013,      15  2013,      1905 ,    17  2014,      7  2010 ,      1 ,  1843 ,     30  2007 ,      1846 ,     1996 ,     9 ,  ... 

我们将使用seq2seq库构建生成模型,在该库中,编码器将在符号级别实现(即,字母的元素是符号),而解码器将使用单词作为字母。 Github存储库中提供了示例代码(如数据)。


训练数据分为三个子集:train.csv,test.csv和dev.csv,分别用于训练,测试和再训练验证。 数据在数据目录中。 存储库中实现了三个模型:seq2seq_greedy.py,seq2seq_attention.py和seq2seq_beamsearch.py​​。 在这里,我们看一下基本贪婪搜索模型的代码。


所有模型都使用Estimator类来实现。 使用此类可以简化编码,而不会被非模型零件所干扰。 例如,无需实施数据传输周期进行培训,创建与Tensorflow一起使用的会话,考虑将数据传输至Tensorboard等。 估计器仅需实现两个功能即可:数据传输和构建模型。 这些示例还使用Dataset类传递数据进行处理。 这种现代的实现比传统字典更快地传输feed_dict形式的数据。


资料产生


考虑用于训练和生成的数据生成代码。


 def parse_fn(line_before, line_after): # Encode in Bytes for TF source = [c.encode('utf8') for c in line_before.decode('utf8').rstrip('\n')] t = [w.encode('utf8') for w in nltk.word_tokenize(line_after.decode('utf8').strip())] learn_target = t + ['<eos>'] + ['<pad>'] target = ['<go>'] + t + ['<eos>'] return (source, len(source)), (target, learn_target, len(target)) def generator_fn(data_file): with open(data_file, 'rb') as f: reader = csv.DictReader(f, delimiter=',', quotechar='"') for row in reader: yield parse_fn(row['before'], row['after']) def input_fn(data_file, params=None): params = params if params is not None else {} shapes = (([None], ()), ([None], [None], ())) types = ((tf.string, tf.int32), (tf.string, tf.string, tf.int32)) defaults = (('<pad>', 0), ('<pad>', '<pad>', 0)) dataset = tf.data.Dataset.from_generator(functools.partial(generator_fn, data_file), output_shapes=shapes, output_types=types) dataset = dataset.repeat(params['epochs']) return (dataset.padded_batch(params.get('batch_size', 50), shapes, defaults).prefetch(1)) 

input_fn函数用于创建数据集合,然后将Estimator传递给训练和生成。 首先设置数据类型。 这是一对格式((编码器序列,长度),(解码器序列,带前缀的解码器序列,长度))。 字符串“”用作前缀,每个编码器序列均以特殊词“”结尾。 另外,由于序列(输入和输出)的长度不相等,因此使用值“”的填充符号。

数据准备代码使用nltk库读取数据文件,将编码器字符串划分为字符,将解码器字符串划分为单词。 以这种方式处理的行是训练数据的示例。 生成的集合被分成小包,并根据训练时代的数量(每个时期是一次数据传递)克隆数据量。


使用字典


词典以列表形式存储在文件中,一个单词或一个字符一行。 要构建字典,请使用build_vocabs.py脚本。 生成的字典以vocab。*。Txt格式的文件位于数据目录中。


阅读字典的代码:


 # Read vocabs and inputs dropout = params['dropout'] source, source_length = features training = (mode == tf.estimator.ModeKeys.TRAIN) vocab_source = tf.contrib.lookup.index_table_from_file(vocabulary_file=params['source_vocab_file'], num_oov_buckets=params['num_oov_buckets']) with open(params['source_vocab_file']) as f: num_sources = sum(1 for _ in f) + params['num_oov_buckets'] vocab_target = tf.contrib.lookup.index_table_from_file(vocabulary_file=params['target_vocab_file'], num_oov_buckets=params['num_oov_buckets']) with open(params['target_vocab_file']) as f: num_targets = sum(1 for _ in f) + params['num_oov_buckets'] 

在这里,可能是index_table_from_file函数从一个文件中读取字典条目,这很有趣,它的参数num_oov_buckets是未使用的词汇篮数量。 默认情况下,此数字等于1,即 所有不在词典中的单词的索引等于词典的大小+1。我们有三个未知单词:“”,“”和“”,我们希望为其指定不同的索引。 因此,将此参数设置为数字三。 不幸的是,您必须再次读取输入文件才能获得字典中的单词数作为设置模型图的时间常数。

我们仍然需要创建一个表来实现嵌入-_source_embedding,以及将单词字符串转换为标识符字符串:


 # source embeddings matrix _source_embedding = tf.Variable(tf.random_uniform([num_sources, params['embedding_size']])) source_ids = vocab_source.lookup(source) source_embedding = tf.nn.embedding_lookup(_source_embedding, source_ids) 

编码器实施


对于编码器,我们将使用具有多个级别的双向递归网络。 , , .


 # add multilayer bidirectional RNN cell_fw = tf.contrib.rnn.MultiRNNCell([tf.contrib.rnn.GRUCell(params['dim']) for _ in range(params['layers'])]) cell_bw = tf.contrib.rnn.MultiRNNCell([tf.contrib.rnn.GRUCell(params['dim']) for _ in range(params['layers'])]) outputs, states = tf.nn.bidirectional_dynamic_rnn(cell_fw, cell_bw, source_embedding, sequence_length=source_length, dtype=tf.float32) # prepare output output = tf.concat(outputs, axis=-1) encoder_output = tf.layers.dense(output, params['dim']) # prepare state state_fw, state_bw = states cells = [] for fw, bw in zip(state_fw, state_bw): state = tf.concat([fw, bw], axis=-1) cells += [tf.layers.dense(state, params['dim'])] encoder_state = tuple(cells) 

GRU, MultiRNNCell, , rnn.Cell. ,
sequence_length — , , .


, , , . , 128, 256. , , 128. .


. 因为 , , bidirectional_dynamic_rnn, , . , . , .. . , , . , , .



, . .


  # decoder RNN cell decoder_cell = tf.contrib.rnn.MultiRNNCell([tf.contrib.rnn.GRUCell(params['dim']) for _ in range(params['layers'])]) decoder_initial_state = encoder_state # projection layer projection_layer = tf.layers.Dense(num_targets, use_bias=False) # embedding table for targets target_embedding = tf.Variable(tf.random_uniform([num_targets, params['embedding_size']])) 

培训课程


TrainingHelper + BasicDecoder.


  # target embeddings matrix target, learn_target, target_length = labels target_ids = vocab_target.lookup(target) target_learn_ids = vocab_target.lookup(learn_target) # train encoder _target_embedding = tf.nn.embedding_lookup(target_embedding, target_ids) train_helper = tf.contrib.seq2seq.TrainingHelper(_target_embedding, target_length) train_decoder = tf.contrib.seq2seq.BasicDecoder(decoder_cell, train_helper, decoder_initial_state, output_layer=projection_layer) train_outputs, _, _ = tf.contrib.seq2seq.dynamic_decode(train_decoder) train_output = train_outputs.rnn_output train_sample_id = train_outputs.sample_id 


.


 # prediction decoder prediction_helper = tf.contrib.seq2seq.GreedyEmbeddingHelper( embedding=target_embedding, start_tokens=tf.fill([batch_size], tf.to_int32(vocab_target.lookup(tf.fill([], '<go>')))), end_token=tf.to_int32(vocab_target.lookup(tf.fill([], '<eos>')))) prediction_decoder = tf.contrib.seq2seq.BasicDecoder(decoder_cell, prediction_helper, decoder_initial_state, output_layer=projection_layer) prediction_output, _, _ = tf.contrib.seq2seq.dynamic_decode(prediction_decoder, maximum_iterations=params['max_iters']) # prepare prediction reverse_vocab_target = tf.contrib.lookup.index_to_string_table_from_file(params['target_vocab_file']) pred_strings = reverse_vocab_target.lookup(tf.to_int64(prediction_output.sample_id)) predictions = { 'ids': prediction_output.sample_id, 'text': pred_strings } 

GreedyEmbeddingHelper "", "". . , , dynamic_decode . , , . , , .


, seq2seq.


 # loss masks = tf.sequence_mask(lengths=target_length, dtype=tf.float32) loss = tf.contrib.seq2seq.sequence_loss(logits=train_output, targets=target_learn_ids, weights=masks) 

, , sequence_mask.


Adam , .


 optimizer = tf.train.AdamOptimizer(learning_rate=params.get('lr', .001)) grads, vs = zip(*optimizer.compute_gradients(loss)) grads, gnorm = tf.clip_by_global_norm(grads, params.get('clip', .5)) train_op = optimizer.apply_gradients(zip(grads, vs), global_step=tf.train.get_or_create_global_step()) 


. 0.9 . , , , . , .


 24  1944                 1  2003              1992 .           11  1927               1969            1  2016             1047          1863            17      22  2014               

. — , — , — .


, — . . , ( ), . . , .


结论


seq2seq. , , . , .


. Tensorflow , , . , , . , . , , padding , embedding ? , , . — . , , . , , , . , . , , , , .

Source: https://habr.com/ru/post/zh-CN440472/


All Articles