本教程将介绍如何是seq2seq模型转换为PyTorch可用的前端混合Torch脚本。我们要转换的模型来自于聊天机器人教程Chatbot tutorial。
1.混合前端
在一个基于深度学习项目的研发阶段, 使用像PyTorch这样即时eager、命令式的界面进行交互能带来很大便利。 这使用户能够在使用Python数据结构、控制流操作、打印语句和调试实用程序时通过熟悉的、惯用的Python脚本编写。
尽管即时性界面对于研究和试验应用程序是一个有用的工具,但是对于生产环境中部署模型时,使用基于图形graph-based的模型表示将更加适用的。 一个延迟的图型展示意味着可以优化,比如无序执行操作,以及针对高度优化的硬件架构的能力。 此外,基于图形的表示支持框架无关的模型导出。PyTorch提供了将即时模式的代码增量转换为Torch脚本的机制,Torch脚本是一个在Python中的静态可分析和可优化的子集,Torch使用它来在Python运行时独立进行深度学习。
在Torch中的torch.jit模块可以找到将即时模式的PyTorch程序转换为Torch脚本的API。 这个模块有两个核心模式用于将即时模式模型转换为Torch脚本图形表示: 跟踪tracing 以及 脚本化scripting。torch.jit.trace 函数接受一个模块或者一个函数和一组示例的输入,然后通过函数或模块运行输入示例,同时跟跟踪遇到的计算步骤,然后输出一个可以展示跟踪流程的基于图形的函数。跟踪Tracing对于不涉及依赖于数据的控制流的直接的模块和函数非常有用,就比如标准的卷积神经网络。
然而,如果一个有数据依赖的if语句和循环的函数被跟踪,则只记录示例输入沿执行路径调用的操作。换句话说,控制流本身并没有被捕获。要将带有数据依赖控制流的模块和函数进行转化,已提供了一个脚本化机制。脚本显式地将模块或函数代码转换为Torch脚本,包括所有可能的控制流路径。 如需使用脚本模式script mode, 要确定继承了 torch.jit.ScriptModule基本类 (取代torch.nn.Module) 并且增加 torch.jit.script 装饰器到你的Python函数或者 torch.jit.script_method 装饰器到你的模块方法。
使用脚本化的一个警告是,它只支持Python的一个受限子集。要获取与支持的特性相关的所有详细信息,请参考 Torch Script language reference。为了达到最大的灵活性,可以组合Torch脚本的模式来表示整个程序,并且可以增量地应用这些技术。
2.预备环境
首先,导入所需的模块以及设置一些常量。如果想使用自己的模型,需要保证MAX_LENGTH常量设置正确。 提醒:这个常量定义了在训练过程中允许的最大句子长度以及模型能够产生的最大句子长度输出。
source-python from __future__ import absolute_import from __future__ import division from __future__ import print_function from __future__ import unicode_literals import torch import torch.nn as nn import torch.nn.functional as F import re import os import unicodedata import numpy as np device = torch.device("cpu") MAX_LENGTH = 10 # Maximum sentence length # 默认的词向量 PAD_token = 0 # Used for padding short sentences SOS_token = 1 # Start-of-sentence token EOS_token = 2 # End-of-sentence token
3.模型概述
正如前文所言,我们使用的sequence-to-sequence (seq2seq) 模型。这种类型的模型用于输入是可变长度序列的情况,我们的输出也是一个可变长度序列它不一定是一对一输入映射。seq2seq 模型由两个递归神经网络(RNNs)组成:编码器 encoder和解码器decoder.
(1)编码器(Encoder)
编码器RNN在输入语句中每次迭代一个标记(例如单词),每次步骤输出一个“输出”向量和一个“隐藏状态”向量。”隐藏状态“向量在之后则传递到下一个步骤,同时记录输出向量。编码器将序列中每个坐标代表的文本转换为高维空间中的一组坐标,解码器将使用这些坐标为给定的任务生成有意义的输出。
(2)解码器(Decoder)
解码器RNN以逐个令牌的方式生成响应语句。它使用来自于编码器的文本向量和内部隐藏状态来生成序列中的下一个单词。它继续生成单词,直到输出表示句子结束的EOS语句。我们在解码器中使用专注机制attention mechanism来帮助它在输入的某些部分生成输出时”保持专注”。对于我们的模型,我们实现了 Luong et al等人的“全局关注Global attention”模块,并将其作为解码模型中的子模块。
4.数据处理
尽管我们的模型在概念上处理标记序列,但在现实中,它们与所有机器学习模型一样处理数字。在这种情况下,在训练之前建立的模型词汇表中的每个单词都映射到一个整数索引。我们使用Voc对象来包含从单词到索引的映射,以及词汇表中的单词总数。我们将在运行模型之前加载对象。
此外,为了能够进行评估,我们必须提供一个处理字符串输入的工具。normalizeString函数将字符串中的所有字符转换为小写,并删除所有非字母字符。indexesFromSentence函数接受一个单词的句子并返回相应的单词索引序列。
class Voc: def __init__(self, name): self.name = name self.trimmed = False self.word2index = {} self.word2count = {} self.index2word = {PAD_token: "PAD", SOS_token: "SOS", EOS_token: "EOS"} self.num_words = 3 # 统计SOS, EOS, PAD def addSentence(self, sentence): for word in sentence.split(' '): self.addWord(word) def addWord(self, word): if word not in self.word2index: self.word2index[word] = self.num_words self.word2count[word] = 1 self.index2word[self.num_words] = word self.num_words += 1 else: self.word2count[word] += 1 # Remove words below a certain count threshold def trim(self, min_count): if self.trimmed: return self.trimmed = True keep_words = [] for k, v in self.word2count.items(): if v >= min_count: keep_words.append(k) print('keep_words {} / {} = {:.4f}'.format( len(keep_words), len(self.word2index), len(keep_words) / len(self.word2index) )) # Reinitialize dictionaries self.word2index = {} self.word2count = {} self.index2word = {PAD_token: "PAD", SOS_token: "SOS", EOS_token: "EOS"} self.num_words = 3 # 统计默认的令牌 for word in keep_words: self.addWord(word) # 小写并删除非字母字符 def normalizeString(s): s = s.lower() s = re.sub(r"([.!?])", r" \1", s) s = re.sub(r"[^a-zA-Z.!?]+", r" ", s) return s # 使用字符串句子,返回单词索引的句子 def indexesFromSentence(voc, sentence): return [voc.word2index[word] for word in sentence.split(' ')] + [EOS_token]
5.定义编码器
通过torch.nn.GRU模块实现编码器的RNN。本模块接受一批语句(嵌入单词的向量)的输入,它在内部遍历这些句子,每次一个标记, 计算隐藏状态。我们将这个模块初始化为双向的,这意味着我们有两个独立的GRUs:一个按时间顺序遍历序列,另一个按相反顺序遍历序列。 我们最终返回这两个GRUs输出的和。由于我们的模型是使用批处理进行训练的,所以我们的EncoderRNN模型的forward函数需要一个填充 的输入批处理。为了批量处理可变长度的句子,我们通过MAX_LENGTH令牌允许一个句子中支持的最大长度,并且批处理中所有小于MAX_LENGTH 令牌的句子都使用我们专用的PAD_token令牌填充在最后。要使用带有PyTorch RNN模块的批量填充,我们必须把转发forward密令在 调用torch.nn.utils.rnn.pack_padded_sequence和torch.nn.utils.rnn.pad_packed_sequence数据转换时进行打包。注意,forward 函数还接受一个input_length列表,其中包含批处理中每个句子的长度。该输入在填充时通过torch.nn.utils.rnn.pack_padded_sequence使用。
- 混合前端笔记 由于编码器的转发函数forward不包含任何依赖于数据的控制流,因此我们将使用跟踪tracing将其转换为脚本模式script mode。在跟踪模块时, 我们可以保持模块定义不变。在运行评估之前,我们将在本文末尾初始化所有模型。
class EncoderRNN(nn.Module): def __init__(self, hidden_size, embedding, n_layers=1, dropout=0): super(EncoderRNN, self).__init__() self.n_