deploying a seq2seq model with the Hybrid Frontend(施工中止)

这篇教程将介绍使用PyTorch的Hybrid Frontend将序列到序列模型转换为PyTorch脚本的过程。我们要转换的模型是一个Chatbot模型。你既可以把本篇教程视作Chatbot教程的第二节,并部署你自己训练好的模型;也可以从这篇文档开始,并使用教程提供的训练好的模型。在第二种情况下,你可以参考那篇Chatbot教程以获得包括数据处理、模型理论和定义以及模型训练在内的细节。

Hybrid Frontend是什么?

在基于深度学习的项目的研究和发展阶段,与eager这样类似PyTorch的必要接口进行交互是很有利的。这给了用户书写熟悉、符合语言习惯的Python的能力,包括使用Python的数据结构、流程控制操作、打印声明以及debug功能。尽管eager接口是一个对于研究和实验应用来说很好用的工具,但当在生产环境部署模型时,使用基于图形的模型表示非常有益。延迟图表示容许进行无序执行这样的优化,还拥有针对高度优化的硬件体系结构的能力。同时,基于图的表示使得框架无关模型的导出成为可能。PyTorch提供了将eager-mode代码逐步转换为Torch脚本的机制,Torch脚本是一个静态可分析和优化的Python子集,Torch使用了它就可以不需要Python runtime也能够表示深度学习程序。

用于将eager模式的PyTorch代码转换为Torch脚本的脚本在torch.jit模块里。该模块有两个核心的模式来将eager模式的模型转换为Torch脚本的图形表示:tracing和scripting。torch.jit.trace函数将模型或函数以及一组示例输入作为输入。 然后,它在追踪聚集的计算步骤时通过函数或模块来运行示例输入,并输出一个执行追踪操作的基于图的函数。追踪非常适合不涉及数据依赖(data-dependent)控制流的简单模块和函数,比如标准卷积神经网络。然而,如果追踪具有数据依赖的if语句和循环结构的函数的话,则只会记录沿示例输入所采用的执行路径上被调用的操作。换句话说,不捕获控制流本身。为了将包含数据依赖的控制流转化为模块和函数,我们提供了脚本机制。脚本显示地将模块或函数代码转换为Torch脚本,包含了所有可能的控制流路径。为了使用脚本模式,请确保继承了torch.jit.ScriptModule的基类(而不是torch.nn.Module),并把torch.jit.script的装饰器(decorator)添加到你的Python函数中;或把torch.jit_script_method装饰器添加到你的模块的方法中。使用脚本的一个警告是,它只支持Python的受限子集。关于支持的功能所有细节,请参阅Torch脚本语言参考。为了提供最大的灵活性,Torch脚本的模式可以组合起来来表示你的整个程序,且这些技术可以被逐步应用。

在这里插入图片描述

致谢

本教程受到以下资源的启发:

准备环境

首先,我们将会引入必要的模块,并设置一些常量。如果你打算使用你自己的模块,确保你正确设置了MAX_LENGTH常量。作为提醒,该常量定义了在训练时容许的最长句子长度以及模型能够输出的最大长度。

## 在python2.4之前,如果你想引用系统中的模块而非当前路径下的其同名模块,则可以使用下列语句
## 见 https://blog.csdn.net/caiqiiqi/article/details/51050800
from __future__ import absolute_import

from __future__ import division
from __future__ import print_function

## 在python2和3里关于字符串的定义有一些差异,在2里要在字符串前加u表示为unicode
## 否则为str,而在3里所有字符串都默认为unicode,只有前面加了b的才被视作str
## 见 https://www.cnblogs.com/ccorz/p/python-zhong-defutruemo-kuai-yi-jiunicodeliterals-.html
from __future__ import unicode_literals

import torch
import torch.nn as nn
import torch.nn.functional as F

## 包含了一些对字符串进行处理的正则表达式模块
## 见 https://blog.csdn.net/m0_37564426/article/details/82534652
import re

import os

## 该模块提供对Unicode字符数据库(Unicode Character DataBase,UCD)的访问
## 该数据库提供所有字符属性的定义,见 https://cloud.tencent.com/developer/section/1371917
import unicodedata

import numpy as np

device = torch.device("cpu")

MAX_LENGTH = 10

## 用于填充短句
PAD_token = 0

## 句子的开头标记
SOS_token = 1

## 句子的结束标记
EOS_token = 2

模型总览

像之前提到的,我们使用的模型为序列到序列(seq2seq)模型。这类模型被用于输入为可变长度的序列,且输出为不必与输入一对一映射的可变长度序列。seq2seq序列由两个合作的RNN网络构成:encoder和decoder。

在这里插入图片描述

Encoder

编码RNN(encoder RNN)一次迭代输入语句的一个标记(比如说一个字),并在每个时间步输出"输出"矢量和"隐藏态"矢量。隐藏态矢量随后被传递至下个时间步,与此同时,输出结果也被记录。编码器把它在序列中的每个点上看到的上下文转换为高维空间的一系列点,而解码器将会使用这些系列点来得到对于给定任务来说有意义的结果。

Decoder

解码RNN(decoder RNN)以逐个标记(token)的方式生成相应语句。它使用编码器的上下文矢量,以及内部隐藏态来生成序列中的下一个字。它持续的生成文字,直到它输出了表示语句结束的EOS_token。我们在解码器里使用注意力机制来帮助解码器在生成输出时对输入的特定部分"保持注意"。对我们的模型而言,我们部署Luong的"全局注意力(Global attention)"模型,并把它作为我们编码器模型的一个子模型。

数据处理

虽然我们的模型在概念上处理标记的序列,但实际上,它们就像其它机器学习模型那样来处理数字。在这种情况下,在训练前就建立好的模型的词汇表中的每个字都被映射到一个整型索引上。我们使用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
        
        def addSentence(self,sentence):
            for word in sentence:
                self.addWord(word)
        
        def addWord(self,word):
            if word not in self.word2index:
                self.word2index[word] = seld.num_words
                self.word2count[word] = 1
                self.index2word[self.new_words] = word
                self.num_words += 1
            else:
                self.word2count[word] += 1
        
        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(word2index)))
            
            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):
            ## 
            print([voc.word2index[word] for word in sentence.split(' ')] ,
                  "\n", [EOS_token],"\n",
                  [voc.word2index[word] for word in sentence.split(' ')] + [EOS_token])
            return [voc.word2index[word] for word in sentence.split(' ')] + [EOS_token]

定义编码器

我们使用torch.nn.GRU模块部署了编码器RNN,我们提供一批句子(嵌入字的矢量),torch.nn.GRU模块就会在内部一次一个标记地迭代计算隐藏态。我们将该模块初始化为双向地,这意味着我们有两个独立的GRU:一个按照时间顺序迭代序列,另一个按相反的顺序迭代。我们最终返回这两个GRU输出的总和。 由于我们的模型已经使用批处理训练过了,我们的EncoderRNN模型的forward函数需要填充的批输入。对于批量变长语句,我们设置语句中最多有MAX_LENGTH个标记,而且批量中小于MAX_LENGTH个标记的句子的末尾都被我们专用的PAD_token标记填充。为了使用带有PyTorch RNN模块的填充批次,我们必须使用torch.nn.utils.rnn.pack_padder_sequence和torch.nn.utile.rnn.pad_packed_sequence数据转换来包装前向调用。注意forward函数也可以将input_lengths列表作为输入,其中input_lengths包含了批量中每个句子的长度。该输入在填充时被torch.nn.utils.rnn.pack_padded_sequence函数使用。

Hybrid Frontend Notes:

由于编码器的forward函数并不包括任何数据依赖的控制流,我们将会使用tracing来将它转化为脚本模式。当追踪模块时,我们可以保留模块当前的定义。在进行评估之前,我们将在本文的末尾初始化所有模型。

class EncoderRNN(nn.Module):
    def __init__(self,hidden_size,embedding,n_layers=1,dropout=0):
        ## super(EncoderRNN,self).__init__(),该语句在python3中可以如下书写
        super().__init__()
        self.n_layers = n_layers
        self.hidden_size = hidden_size
        self.embedding = embedding
        
        ## GRU()将一个多层的门循环单元(gated recurrent unit,GRU)RNN应用于输入序列
        ## 见 https://pytorch.org/docs/stable/nn.html GRU部分
        ## 关于GRU,见 https://www.cnblogs.com/jiangxinyang/p/9376021.html
        self.gru = nn.GRU(hidden_size,hidden_size,n_layers,
                         dropout=(0 if n_layers == 1 else dropout),bidirectional = True)
        
    def forward(self,input_seq,input_lengths,hidden=None):
        ## 将word索引转换为embeddings,该类型用于比较word的相似程度
        ## 见 https://my.oschina.net/earnp/blog/1113896
        embedded = self.embedding(input_seq)
        
        ## 将embedded压紧,这是为了方便将其传入GRU
        ## 见 https://www.cnblogs.com/sbj123456789/p/9834018.html
        packed = torch.nn.utils.rnn.pack_padded_sequence(embedded,input_length)
        
        ## 前向传播
        outputs,hidden = self.gru(packed,hidden)
        
        ## 将输出结果解压缩
        outputs,_ = torch.nn.utils.rnn.pad_packed_sequence(outputs)
        
        ## 将正向和反向的结果相加作为最终结果
        outputs = outputs[:,:,:self.hidden_size] + outputs[:,:,self.hidden_size:]
        return outputs,hidden

定义解码器的注意力模型

接下来,我们将要定义我们的注意力模型(Attn)。注意,该模型将会作为我们的解码器的子模型。Luong等人

发布了74 篇原创文章 · 获赞 14 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/JachinMa/article/details/95181955