PyTorch 1.1 : Getting Started : Seq2Seq モデルをハイブリッド・フロントエンドで配備する

PyTorch 1.1 : Getting Started : Seq2Seq モデルをハイブリッド・フロントエンドで配備する (翻訳/解説)
翻訳 : (株)クラスキャット セールスインフォメーション
作成日時 : 06/29/2019 (1.1.0)

* 本ページは、PyTorch 1.1 Tutorials の DEPLOYING A SEQ2SEQ MODEL WITH THE HYBRID FRONTEND を翻訳した上で適宜、補足説明したものです:

* サンプルコードの動作確認はしておりますが、必要な場合には適宜、追加改変しています。
* ご自由にリンクを張って頂いてかまいませんが、sales-info@classcat.com までご一報いただけると嬉しいです。

 

Getting Started : Seq2Seq モデルをハイブリッド・フロントエンドで配備する

このチュートリアルは PyTorch のハイブリッド・フロントエンドを使用して sequence-to-sequence モデルを Torch Script に移行する過程をウォークスルーします。変換するモデルは チャットボット・チュートリアル のチャットボット・モデルです。このチュートリアルをチャットボット・チュートリアルの「パート 2」として扱い貴方自身の事前訓練したモデルを配備することもできますし、このドキュメントから始めて私達がホストした事前訓練モデルを使用することもできます。後者の場合、データ処理、モデル理論と定義、そしてモデル訓練に関して詳細は元のチャットボット・チュートリアルを参照できます。

 

ハイブリッド・フロントエンドとは何でしょう?

深層学習ベースのプロジェクトの研究と開発段階の間、PyTorch のもののような先行評価の (= eager) 命令的なインターフェイスと相互作用することは有利です。これはユーザに馴染みのある慣用的な Python を書く能力を与え、Python データ構造、制御フロー演算、print ステートメントとデバッギング・ユティリティの使用を可能にします。eager インターフェイスは研究と実験アプリケーションのための有益なツールですが、プロダクション環境でモデルを配備するときになると、graph-based モデル表現を持つことは非常に有益です。遅延グラフ表現はアウト・オブ・オーダー実行のような最適化と非常に最適化されたハードウェアアーキテクチャをターゲットとする能力を可能にします。また、グラフ・ベースの表現はフレームワーク不可知論モデル exportation を可能にします。eager モード・コードをインクリメンタルに Torch Script に変換するためのメカニズムを提供します、これは Python ランライムからは独立的に深層学習プログラムを表現するために Torch が使用する、静的に解析可能で最適化可能な Python のサブセットです。

eager モード PyTorch プログラムを Torch Script に変換するための API は torch.jit モジュールで見つかります。このモジュールは eager モードモデルを Torch Script 表現に変換するために 2 つの中心的なモード (= modalities) を持ちます : tracingscripting です。torch.jit.trace 関数はモジュールか関数とサンプル入力のセットを取ります。それからそれは直面する計算ステップを追跡しながら、関数かモジュールを通してサンプル入力を実行します、そして追跡された演算を遂行するグラフベースの関数を出力します。tracing は、標準的な畳み込みニューラルネットワークのようなデータ依存制御フローを伴わない率直なモジュールと関数のためには素晴らしく良いです。けれども、データ依存な if ステートメントとループが追跡される場合、サンプル入力により取られる実行ルートに沿って呼び出された演算だけが記録されるでしょう。換言すれば、制御フローそのものが捕捉されません。データ依存制御フローを含むモジュールと関数を変換するために、scripting メカニズムが提供されます。scripting は総ての可能な制御フロー・ルートを含めて、モジュールと関数コードを Torch Script に明示的に変換します。script モードを使用するには、(torch.nn.Module の代わりに) torch.jit.ScriptModule 基底クラスから継承して貴方の Python 関数に torch.jit.script デコレータかモジュールのメソッドに torch.jit.script_method デコレータを追加することを確実にしてください。script を使用する際の一つの注意事項はそれは Python の制限されたサブセットだけをサポートすることです。サポートされる特徴に関する総ての詳細については、Torch Script 言語リファレンス を見てください。最大限の柔軟性を提供するために、Torch Script のモードは貴方のプログラム全体を表わすために構成することができて、これらのテクニックはインクリメンタルに適用できます。

 

 

Acknowledgements

このチュートリアルは次のソースからインスパイアされています :

  1. Yuan-Kuei Wu の pytorch-chatbot 実装: https://github.com/ywk991112/pytorch-chatbot
  2. Sean Robertson の practical-pytorch seq2seq-translation サンプル: https://github.com/spro/practical-pytorch/tree/master/seq2seq-translation
  3. FloydHub の Cornell Movie Corpus preprocessing code: https://github.com/floydhub/textutil-preprocess-cornell-movie-corpus

 

環境を準備する

最初に、必要なモジュールをインポートして幾つかの定数を設定します。貴方自身のモデルを使用することを計画している場合には、MAX_LENGTH 定数が正しく設定されることを確実にしてください。注意として、この定数は訓練の間に許容されるセンテンス長を定義しモデルが生成可能な最大長出力です。

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

# Default word tokens
PAD_token = 0  # Used for padding short sentences
SOS_token = 1  # Start-of-sentence token
EOS_token = 2  # End-of-sentence token

 

モデル概要

言及したように、使用しているモデルは sequence-to-sequence (seq2seq) モデルです。このタイプのモデルは入力が可変長シークエンスで、(入力の 1 対 1 対応では必ずしもない) 出力も可変長シークエンスである場合に使用されます。seq2seq モデルは 2 つのリカレント・ニューラルネットワーク (RNN) から成り、これらは協調的に動作します : エンコーダデコーダ です。

画像ソース: https://jeddy92.github.io/JEddy92.github.io/ts_seq2seq_intro/

 

エンコーダ

エンコーダ RNN は入力センテンスを通して一度に一つのトークン (e.g. 単語) iterate して、各時間ステップで「出力」ベクトルと「隠れ状態」ベクトルを出力します。それから隠れ状態ベクトルが次の時間ステップに渡され、その一方で出力ベクトルは記録されます。エンコーダはシークエンスの各ポイントでそれが見たコンテキストを高次元空間のポイントのセット内に変換し、デコーダは与えられたタスクに対して意味のある出力を生成するためにそれを使用します。

 

デコーダ

デコーダ RNN はトークン毎流儀で応答センテンスを生成します。それはシークエンスの次の単語を生成するためにエンコーダのコンテキスト・ベクトルと内部隠れ状態を使用します。それはそれが EOS_token を出力するまで単語を生成し続けます。出力を生成するとき (デコーダが) 入力のあるパートに「注意を払う (= pay attention)」ことを助けるために私達はデコーダで attention メカニズム を使用します。私達のモデルについては、Luong et al. の “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  # Count 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 # Count default tokens
        for word in keep_words:
            self.addWord(word)


# Lowercase and remove non-letter characters
def normalizeString(s):
    s = s.lower()
    s = re.sub(r"([.!?])", r" \1", s)
    s = re.sub(r"[^a-zA-Z.!?]+", r" ", s)
    return s


# Takes string sentence, returns sentence of word indexes
def indexesFromSentence(voc, sentence):
    return [voc.word2index[word] for word in sentence.split(' ')] + [EOS_token]

 

エンコーダを定義する

エンコーダの RNN を torch.nn.GRU モジュールで実装します、これにセンテンスのバッチ (単語埋め込みのベクトル) を供給してそしてそれは内部的に内部状態を計算しながら一度に一つのトークンをセンテンスを通して iterate します。モジュールを双方向に初期化します、これは 2 つの独立した GRU を持つことを意味します : シークエンスを通して時系列順に iterate する一つと、逆順に iterate するもう一つです。最終的には 2 つの GRU 出力の総計を返します。モデルはバッチ処理を使用して訓練されましたので、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_lengths リストを取ることに注意してください、これはバッチの各センテンスの長さを含みます。この入力はパディングするとき torch.nn.utils.rnn.pack_padded_sequence により使用されます。

 

ハイブリッド・フロントエンドのノート :

エンコーダの forward 関数はどのようなデータ依存制御フローも含まないので、それを script モードに変換するために tracing を使用します。モジュールを trace するとき、モジュール定義をそのままにできます。このドキュメントの終わりに向けて評価を実行する前に総てのモデルを初期化します。

class EncoderRNN(nn.Module):
    def __init__(self, hidden_size, embedding, n_layers=1, dropout=0):
        super(EncoderRNN, self).__init__()
        self.n_layers = n_layers
        self.hidden_size = hidden_size
        self.embedding = embedding

        # Initialize GRU; the input_size and hidden_size params are both set to 'hidden_size'
        #   because our input size is a word embedding with number of features == hidden_size
        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):
        # Convert word indexes to embeddings
        embedded = self.embedding(input_seq)
        # Pack padded batch of sequences for RNN module
        packed = torch.nn.utils.rnn.pack_padded_sequence(embedded, input_lengths)
        # Forward pass through GRU
        outputs, hidden = self.gru(packed, hidden)
        # Unpack padding
        outputs, _ = torch.nn.utils.rnn.pad_packed_sequence(outputs)
        # Sum bidirectional GRU outputs
        outputs = outputs[:, :, :self.hidden_size] + outputs[:, : ,self.hidden_size:]
        # Return output and final hidden state
        return outputs, hidden

 

デコーダの Attention モジュールを定義する

次に、attention モジュール (Attn) を定義します。このモジュールはデコーダモデルの副モジュールとして使用されることに注意してください。Luong et al. は様々な「スコア関数」を考えます、これは現在のデコーダ RNN 出力と全体的なエンコーダ出力を取り、そして attention “energies” を返します。この attention energies tensor はエンコーダ出力と同じサイズで、2 つは最終的に乗算され、重み付けられた tensor という結果になります、その最大値はデコーディングの特定の時間ステップにおける質問センテンスの最も重要なパートを表します。

# Luong attention layer
class Attn(torch.nn.Module):
    def __init__(self, method, hidden_size):
        super(Attn, self).__init__()
        self.method = method
        if self.method not in ['dot', 'general', 'concat']:
            raise ValueError(self.method, "is not an appropriate attention method.")
        self.hidden_size = hidden_size
        if self.method == 'general':
            self.attn = torch.nn.Linear(self.hidden_size, hidden_size)
        elif self.method == 'concat':
            self.attn = torch.nn.Linear(self.hidden_size * 2, hidden_size)
            self.v = torch.nn.Parameter(torch.FloatTensor(hidden_size))

    def dot_score(self, hidden, encoder_output):
        return torch.sum(hidden * encoder_output, dim=2)

    def general_score(self, hidden, encoder_output):
        energy = self.attn(encoder_output)
        return torch.sum(hidden * energy, dim=2)

    def concat_score(self, hidden, encoder_output):
        energy = self.attn(torch.cat((hidden.expand(encoder_output.size(0), -1, -1), encoder_output), 2)).tanh()
        return torch.sum(self.v * energy, dim=2)

    def forward(self, hidden, encoder_outputs):
        # Calculate the attention weights (energies) based on the given method
        if self.method == 'general':
            attn_energies = self.general_score(hidden, encoder_outputs)
        elif self.method == 'concat':
            attn_energies = self.concat_score(hidden, encoder_outputs)
        elif self.method == 'dot':
            attn_energies = self.dot_score(hidden, encoder_outputs)

        # Transpose max_length and batch_size dimensions
        attn_energies = attn_energies.t()

        # Return the softmax normalized probability scores (with added dimension)
        return F.softmax(attn_energies, dim=1).unsqueeze(1)

 

デコーダを定義する

EncoderRNN と同様に、デコーダ RNN のために torch.nn.GRU を使用します。けれども今回は一方向 GRU を使用します。エンコーダと違い、デコーダ RNN に一度に一つの単語を供給することに注意してください。現在の単語の埋め込みを得て dropout を適用することから始めます。次に、埋め込みと最後の隠れ状態を GRU に forward して現在の GRU 出力と隠れ状態を得ます。それから attention 重みを得るために Attn モジュールを層として使用します、それにエンコーダの出力を乗じて attention 付きの (= attended) エンコーダ出力を得ます。この attention 付きのエンコーダ出力をコンテキスト tensor として使用します、これはエンコーダ出力のどのパートに注意を払うべきかを示す重み付き総計を表します。ここから、出力シークエンスの次の単語を選択するために線形層と softmax 正規化を使用します。

 

ハイブリッド・フロントエンドのノート :

EncoderRNN と同様に、このモジュールはどのようなデータ依存制御フローも含みません。従って、このモデルをそれが初期化されてパラメータがロードされた後 Torch Script に変換するために再び tracing を使用できます。

class LuongAttnDecoderRNN(nn.Module):
    def __init__(self, attn_model, embedding, hidden_size, output_size, n_layers=1, dropout=0.1):
        super(LuongAttnDecoderRNN, self).__init__()

        # Keep for reference
        self.attn_model = attn_model
        self.hidden_size = hidden_size
        self.output_size = output_size
        self.n_layers = n_layers
        self.dropout = dropout

        # Define layers
        self.embedding = embedding
        self.embedding_dropout = nn.Dropout(dropout)
        self.gru = nn.GRU(hidden_size, hidden_size, n_layers, dropout=(0 if n_layers == 1 else dropout))
        self.concat = nn.Linear(hidden_size * 2, hidden_size)
        self.out = nn.Linear(hidden_size, output_size)

        self.attn = Attn(attn_model, hidden_size)

    def forward(self, input_step, last_hidden, encoder_outputs):
        # Note: we run this one step (word) at a time
        # Get embedding of current input word
        embedded = self.embedding(input_step)
        embedded = self.embedding_dropout(embedded)
        # Forward through unidirectional GRU
        rnn_output, hidden = self.gru(embedded, last_hidden)
        # Calculate attention weights from the current GRU output
        attn_weights = self.attn(rnn_output, encoder_outputs)
        # Multiply attention weights to encoder outputs to get new "weighted sum" context vector
        context = attn_weights.bmm(encoder_outputs.transpose(0, 1))
        # Concatenate weighted context vector and GRU output using Luong eq. 5
        rnn_output = rnn_output.squeeze(0)
        context = context.squeeze(1)
        concat_input = torch.cat((rnn_output, context), 1)
        concat_output = torch.tanh(self.concat(concat_input))
        # Predict next word using Luong eq. 6
        output = self.out(concat_output)
        output = F.softmax(output, dim=1)
        # Return output and final hidden state
        return output, hidden

 

評価を定義する

Greedy 探索デコーダ

チャットボット・チュートリアルのように、実際のデコーディング・プロセスを容易にするために GreedySearchDecoder モジュールを使用します。このモジュールは訓練されたエンコーダとデコーダモデルを属性として持ち、入力センテンス (単語インデックスのベクトル) のエンコードと出力応答シークエンスを一度に 1 単語 (単語インデックス) 反復的にデコードするプロセスを駆動します。

入力シークエンスのエンコードは率直です : 単純に全体のシークエンス tensor とその対応する長さベクトルをエンコーダに forward します。このモジュールはシークエンスの バッチではなく、一度に一つの入力シークエンスだけを扱うことに注意することは重要です。従って、tensor サイズの宣言に定数 1 が使用されるとき、これは 1 のバッチサイズに対応します。与えられたデコーダ出力をデコードするため、デコーダモデルを通して forward パスを反復的に実行しなければなりません、これはデコードされたセンテンスの各単語が正しい次の単語である確率に対応する softmax スコアを出力します。decoder_input を SOS_token を含む tensor に初期化します。デコーダを通す各パスの後、最も高い softmax 確率を持つ単語を greedily に decoded_words リストに付加します。この単語を次の反復のための decoder_input としても使用します。デコードするプロセスはdecoded_words リストが MAX_LENGTH の長さに達するか予測された単語が EOS_token である場合に終了します。

 

ハイブリッド・フロントエンドのノート :

このモジュールの forward メソッドは出力シークエンスを一度に 1 単語デコードするとき [0,max_length) の範囲に渡る反復を伴います。このため、このモジュールを Torch Script に変換するために scripting を使用するべきです。trace できる、エンコーダとデコーダモデルによる場合とは異なり、エラーなしにオブジェクトを初期化するためには GreedySearchDecoder モジュールに幾つかの必要な変更を行わなければなりません。換言すれば、モジュールは scripting メカニズムのルールに忠実で、Torch Script が含む Python のサブセット外のどのような言語特徴も利用しないことを確かなものにしなければなりません。

必要となるかもしれない幾つかの操作の考えを得るために、チャットボット・チュートリアルからの GreedySearchDecoder 実装と下のセルで使用する実装の間の diffs を調べます。赤でハイライトされた行は元の実装から除去された行で緑色でハイライトされた行が新しいことに注意してください。

変更 :

  • nn.Module -> torch.jit.ScriptModule
    • モジュール上で PyTorch の scripting メカニズムを使用するため、そのモジュールは torch.jit.ScriptModule から継承しなければなりません。
  • コンストラクタ引数に decoder_n_layers を追加しました。
    • この変更は、このモジュールに渡すエンコーダとデコーダモデルが TracedModule (not Module) のチャイルドであるという事実に由来します。従って、decoder.n_layers ではデコーダの層の数にアクセスできません。代わりに、このために立案します、そしてこの値をモジュール・コンストラクションの間に渡します。
  • 新しい属性を定数として保管します。
    • 元の実装では、GreedySearchDecoder の forward メソッドで surrounding (global) スコープから変数を自由に使用できました。けれども、scripting を使用している今、この自由を持ちません、何故ならば scripting に伴う仮定は Python オブジェクトを必ずしも持ち続けることができないからです、特にエクスポートしているとき。これへの容易な解法は global スコープからのこれらの値をコンストラクタでモジュールへの属性としてストアすることです、そしてそれらを __constants__ と呼称される特別なリストに追加します、その結果 forward メソッドでグラフを構築するときそれらがリテラル値として使用できます。この使用方法の例は NEW ライン 19 上です、そこでは device と SOS_token global 値を使用する代わりに、 constant 属性 self._device と self._SOS_token を使用します。
  • torch.jit.script_method デコレータを forward メソッドに追加する。
    • このデコレータの追加は JIT コンパイラにそれが修飾している関数が script 化されるべきであることを知らせます。
  • forward メソッド引数の型を強制する。
    • デフォルトでは、Torch Script 関数への総てのパラメータは Tensor であると仮定されています。異なる型の引数を渡す必要がある場合、PEP 3107 で導入された function 型アノテーションを使用できます。更に、MyPy-style 型アノテーション (doc 参照) を使用して異なる型の引数を宣言することも可能です。
  • decoder_input の初期化を変更します。
    • 元の実装では、decoder_input tensor を torch.LongTensor([[SOS_token]]) で初期化しました。scripting では、このようなリテラル流儀で tensor を初期化することは許容されません。代わりに、torch.ones のような明示的な torch 流儀で tensor を初期化できます。この場合、constant self._SOS_token にストアされる SOS_token 値を 1 に乗算することによりスカラー decoder_input tensor を容易に複製できます。
class GreedySearchDecoder(torch.jit.ScriptModule):
    def __init__(self, encoder, decoder, decoder_n_layers):
        super(GreedySearchDecoder, self).__init__()
        self.encoder = encoder
        self.decoder = decoder
        self._device = device
        self._SOS_token = SOS_token
        self._decoder_n_layers = decoder_n_layers

    __constants__ = ['_device', '_SOS_token', '_decoder_n_layers']

    @torch.jit.script_method
    def forward(self, input_seq : torch.Tensor, input_length : torch.Tensor, max_length : int):
        # Forward input through encoder model
        encoder_outputs, encoder_hidden = self.encoder(input_seq, input_length)
        # Prepare encoder's final hidden layer to be first hidden input to the decoder
        decoder_hidden = encoder_hidden[:self._decoder_n_layers]
        # Initialize decoder input with SOS_token
        decoder_input = torch.ones(1, 1, device=self._device, dtype=torch.long) * self._SOS_token
        # Initialize tensors to append decoded words to
        all_tokens = torch.zeros([0], device=self._device, dtype=torch.long)
        all_scores = torch.zeros([0], device=self._device)
        # Iteratively decode one word token at a time
        for _ in range(max_length):
            # Forward pass through decoder
            decoder_output, decoder_hidden = self.decoder(decoder_input, decoder_hidden, encoder_outputs)
            # Obtain most likely word token and its softmax score
            decoder_scores, decoder_input = torch.max(decoder_output, dim=1)
            # Record token and score
            all_tokens = torch.cat((all_tokens, decoder_input), dim=0)
            all_scores = torch.cat((all_scores, decoder_scores), dim=0)
            # Prepare current token to be next decoder input (add a dimension)
            decoder_input = torch.unsqueeze(decoder_input, 0)
        # Return collections of word tokens and scores
        return all_tokens, all_scores

 

入力を評価する

次に、入力を評価するために幾つかの関数を定義します。evaluate 関数は正規化された文字列センテンスを取り、それをその対応する単語インデックスの tensor に処理し、そしてエンコーディング/デコーディング・プロセスを処理するためにこの tensor を searcher と呼ばれる GreedySearchDecoder インスタンスに渡します。searcher は出力単語インデックス・ベクトルと各デコードされた単語トークンのための softmax スコアに対応するスコア tensor を返します。最後のステップは voc.index2word を使用して各単語インデックスをその文字列表現に変換し戻します。

入力センテンスを評価するために 2 つの関数もまた定義します。evaluateInput 関数はユーザに入力を促して、それを評価します。それはユーザが ‘q’ か ‘quit’ を入力するまでもう一つの入力を要求し続けます。

evaluateExample 関数は単純に引数として文字列入力センテンスを取り、それを正規化し、評価し、そして応答をプリントします。

def evaluate(encoder, decoder, searcher, voc, sentence, max_length=MAX_LENGTH):
    ### Format input sentence as a batch
    # words -> indexes
    indexes_batch = [indexesFromSentence(voc, sentence)]
    # Create lengths tensor
    lengths = torch.tensor([len(indexes) for indexes in indexes_batch])
    # Transpose dimensions of batch to match models' expectations
    input_batch = torch.LongTensor(indexes_batch).transpose(0, 1)
    # Use appropriate device
    input_batch = input_batch.to(device)
    lengths = lengths.to(device)
    # Decode sentence with searcher
    tokens, scores = searcher(input_batch, lengths, max_length)
    # indexes -> words
    decoded_words = [voc.index2word[token.item()] for token in tokens]
    return decoded_words


# Evaluate inputs from user input (stdin)
def evaluateInput(encoder, decoder, searcher, voc):
    input_sentence = ''
    while(1):
        try:
            # Get input sentence
            input_sentence = input('> ')
            # Check if it is quit case
            if input_sentence == 'q' or input_sentence == 'quit': break
            # Normalize sentence
            input_sentence = normalizeString(input_sentence)
            # Evaluate sentence
            output_words = evaluate(encoder, decoder, searcher, voc, input_sentence)
            # Format and print response sentence
            output_words[:] = [x for x in output_words if not (x == 'EOS' or x == 'PAD')]
            print('Bot:', ' '.join(output_words))

        except KeyError:
            print("Error: Encountered unknown word.")

# Normalize input sentence and call evaluate()
def evaluateExample(sentence, encoder, decoder, searcher, voc):
    print("> " + sentence)
    # Normalize sentence
    input_sentence = normalizeString(sentence)
    # Evaluate sentence
    output_words = evaluate(encoder, decoder, searcher, voc, input_sentence)
    output_words[:] = [x for x in output_words if not (x == 'EOS' or x == 'PAD')]
    print('Bot:', ' '.join(output_words))

 

事前訓練されたパラメータをロードする

Ok, its time to load our model!

 

hosted モデルを使用する

hosted モデルをロードするには :

  1. ここ でモデルをダウンロードします。
  2. ダウンロードしたチェックポイントファイルへのパスへの loadFilename 変数を設定する。
  3. checkpoint = torch.load(loadFilename) 行をアンコメントしたままにします、何故ならば hosted モデルは CPU 上で訓練されたからです。

 

貴方自身のモデルを使用する

貴方自身の事前訓練されたモデルをロードするには :

  1. 貴方がロードしたいチェックポイントファイルへのパスへの loadFilename 変数を設定する。もし貴方がチャットボット・チュートリアルかのモデルをセーブするために慣習に従ったのであれば、これは model_name, encoder_n_layers, decoder_n_layers, hidden_size そして checkpoint_iter の変更を伴うかもしれません (これらの値はモデルパスで使用されるため)。
  2. もし貴方がモデルを CPU 上で訓練した場合には、チェックポイントを checkpoint = torch.load(loadFilename) 行でオープンしていることを確実にしてください。もし貴方がモデルを GPU 上で訓練してこのチュートリアルを CPU 上で実行している場合には、checkpoint = torch.load(loadFilename, map_location=torch.device(‘cpu’)) 行をアンコメントしてください。

 

ハイブリッド・フロントエンドのノート :

私達は通常のようにパラメータを初期化してエンコーダとデコーダにロードすることに気付いてください。また、モデルを trace する前にモデルの device オプションを設定するために .to(device) をそして dropout 層をテストモードに設定するために .eval() を呼び出さなければなりません。TracedModule オブジェクトは to や eval メソッドを継承しません。

save_dir = os.path.join("data", "save")
corpus_name = "cornell movie-dialogs corpus"

# Configure models
model_name = 'cb_model'
attn_model = 'dot'
#attn_model = 'general'
#attn_model = 'concat'
hidden_size = 500
encoder_n_layers = 2
decoder_n_layers = 2
dropout = 0.1
batch_size = 64

# If you're loading your own model
# Set checkpoint to load from
checkpoint_iter = 4000
# loadFilename = os.path.join(save_dir, model_name, corpus_name,
#                             '{}-{}_{}'.format(encoder_n_layers, decoder_n_layers, hidden_size),
#                             '{}_checkpoint.tar'.format(checkpoint_iter))

# If you're loading the hosted model
loadFilename = 'data/4000_checkpoint.tar'

# Load model
# Force CPU device options (to match tensors in this tutorial)
checkpoint = torch.load(loadFilename, map_location=torch.device('cpu'))
encoder_sd = checkpoint['en']
decoder_sd = checkpoint['de']
encoder_optimizer_sd = checkpoint['en_opt']
decoder_optimizer_sd = checkpoint['de_opt']
embedding_sd = checkpoint['embedding']
voc = Voc(corpus_name)
voc.__dict__ = checkpoint['voc_dict']


print('Building encoder and decoder ...')
# Initialize word embeddings
embedding = nn.Embedding(voc.num_words, hidden_size)
embedding.load_state_dict(embedding_sd)
# Initialize encoder & decoder models
encoder = EncoderRNN(hidden_size, embedding, encoder_n_layers, dropout)
decoder = LuongAttnDecoderRNN(attn_model, embedding, hidden_size, voc.num_words, decoder_n_layers, dropout)
# Load trained model params
encoder.load_state_dict(encoder_sd)
decoder.load_state_dict(decoder_sd)
# Use appropriate device
encoder = encoder.to(device)
decoder = decoder.to(device)
# Set dropout layers to eval mode
encoder.eval()
decoder.eval()
print('Models built and ready to go!')
Building encoder and decoder ...
Models built and ready to go!

 

モデルを Torch スクリプトに変換する

エンコーダ

前に言及したように、エンコーダモデルを Torch Script に変換するために、tracing を使用します。任意のモジュールの tracing はモデルの forward メソッドを通してサンプル入力の実行を必要としてそしてデータが遭遇する計算グラフを trace します。エンコーダモデルは入力シークエンスと対応する lengths tensor を取ります。従って、サンプル入力シークエンス tensor test_seq を作成します、これは適切なサイズ (MAX_LENGTH, 1) で、適切な範囲 [0,voc.num_words) の数字を含み、そして適切な型 (int64) です。test_seq_length スカラーもまた作成します、これは test_seq に幾つの単語があるかに対応する値を現実的に含みます。次のステップはモデルを trace するために torch.jit.trace 関数を使用します。渡す最初の引数は trace したいモジュールで、2 番目はモデルの forward メソッドへの引数のタプルであることに気付いてください。

 

デコーダ

デコーダを tarce するためにエンコーダのために行なったことと同じプロセスを遂行します。デコーダのために必要な出力を得るために traced_encoder へのランダム入力のセット上で forward を呼び出すことに気付いてください。これは必要ではありません、何故ならば正しい shape, 型、そして値範囲の tensor を単純に製造することもできるからです。このメソッドは可能です、何故ならば私達のケースでは tensor の値にどのような制約も持たないからです、何故ならば out-of-range 入力の誤りを起こすようなどのような演算も持たないからです。

 

GreedySearchDecoder

データ依存制御フローの存在により searcher モジュールを script したことを思い出してください。scripting の場合、前もってデコレータを追加して実装が scripting ルールでコンパイルしていることを確かなものとすることにより変換ワークを行ないます。un-scripted variant を初期化するのと同じ方法で scripted searcher を初期化します。

### Convert encoder model
# Create artificial inputs
test_seq = torch.LongTensor(MAX_LENGTH, 1).random_(0, voc.num_words).to(device)
test_seq_length = torch.LongTensor([test_seq.size()[0]]).to(device)
# Trace the model
traced_encoder = torch.jit.trace(encoder, (test_seq, test_seq_length))

### Convert decoder model
# Create and generate artificial inputs
test_encoder_outputs, test_encoder_hidden = traced_encoder(test_seq, test_seq_length)
test_decoder_hidden = test_encoder_hidden[:decoder.n_layers]
test_decoder_input = torch.LongTensor(1, 1).random_(0, voc.num_words)
# Trace the model
traced_decoder = torch.jit.trace(decoder, (test_decoder_input, test_decoder_hidden, test_encoder_outputs))

### Initialize searcher module
scripted_searcher = GreedySearchDecoder(traced_encoder, traced_decoder, decoder.n_layers)

 

グラフをプリントする

私達のモデルが Torch Script 形式にある今、計算グラフを適切に捕捉したかを確かなものにするために各々のグラフをプリントできます。scripted_searcher は traced_encoder と traced_decoder を含むので、これらのグラフはインラインでプリントします。

print('scripted_searcher graph:\n', scripted_searcher.graph)
scripted_searcher graph:
 graph(%input_seq : Tensor,
      %input_length : Tensor,
      %max_length : int,
      %126 : Tensor,
      %127 : Tensor,
      %128 : Tensor,
      %129 : Tensor,
      %130 : Tensor,
      %131 : Tensor,
      %132 : Tensor,
      %133 : Tensor,
      %134 : Tensor,
      %135 : Tensor,
      %136 : Tensor,
      %137 : Tensor,
      %138 : Tensor,
      %139 : Tensor,
      %140 : Tensor,
      %141 : Tensor,
      %142 : Tensor,
      %143 : Tensor,
      %144 : Tensor,
      %145 : Tensor,
      %146 : Tensor,
      %147 : Tensor,
      %148 : Tensor,
      %149 : Tensor,
      %150 : Tensor,
      %151 : Tensor,
      %152 : Tensor,
      %153 : Tensor,
      %154 : Tensor,
      %155 : Tensor):
  %4 : bool? = prim::Constant()
  %5 : int? = prim::Constant()
  %6 : int = prim::Constant[value=9223372036854775807](), scope: EncoderRNN
  %7 : float = prim::Constant[value=0](), scope: EncoderRNN
  %8 : float = prim::Constant[value=0.1](), scope: EncoderRNN/GRU[gru]
  %9 : int = prim::Constant[value=2](), scope: EncoderRNN/GRU[gru]
  %10 : bool = prim::Constant[value=1](), scope: EncoderRNN/GRU[gru]
  %11 : int = prim::Constant[value=6](), scope: EncoderRNN/GRU[gru]
  %12 : int = prim::Constant[value=500](), scope: EncoderRNN/GRU[gru]
  %13 : int = prim::Constant[value=4](), scope: EncoderRNN
  %14 : Device = prim::Constant[value="cpu"](), scope: EncoderRNN
  %15 : bool = prim::Constant[value=0](), scope: EncoderRNN/Embedding[embedding]
  %16 : int = prim::Constant[value=-1](), scope: EncoderRNN/Embedding[embedding]
  %17 : int = prim::Constant[value=0]()
  %18 : int = prim::Constant[value=1]()
  %input.7 : Float(10, 1, 500) = aten::embedding(%155, %input_seq, %16, %15, %15), scope: EncoderRNN/Embedding[embedding]
  %lengths : Long(1) = aten::to(%input_length, %14, %13, %15, %15), scope: EncoderRNN
  %input.1 : Float(10, 500), %batch_sizes : Long(10) = aten::_pack_padded_sequence(%input.7, %lengths, %15), scope: EncoderRNN
  %43 : int[] = prim::ListConstruct(%13, %18, %12), scope: EncoderRNN/GRU[gru]
  %hx : Float(4, 1, 500) = aten::zeros(%43, %11, %17, %14, %15), scope: EncoderRNN/GRU[gru]
  %45 : Tensor[] = prim::ListConstruct(%154, %153, %152, %151, %150, %149, %148, %147, %146, %145, %144, %143, %142, %141, %140, %139), scope: EncoderRNN/GRU[gru]
  %46 : Float(10, 1000), %encoder_hidden : Float(4, 1, 500) = aten::gru(%input.1, %batch_sizes, %hx, %45, %10, %9, %8, %15, %10), scope: EncoderRNN/GRU[gru]
  %48 : int = aten::size(%batch_sizes, %17), scope: EncoderRNN
  %max_seq_length : Long() = prim::NumToTensor(%48), scope: EncoderRNN
  %50 : int = prim::Int(%max_seq_length), scope: EncoderRNN
  %outputs : Float(10, 1, 1000), %52 : Long(1) = aten::_pad_packed_sequence(%46, %batch_sizes, %15, %7, %50), scope: EncoderRNN
  %53 : Float(10, 1, 1000) = aten::slice(%outputs, %17, %17, %6, %18), scope: EncoderRNN
  %54 : Float(10, 1, 1000) = aten::slice(%53, %18, %17, %6, %18), scope: EncoderRNN
  %55 : Float(10, 1!, 500) = aten::slice(%54, %9, %17, %12, %18), scope: EncoderRNN
  %56 : Float(10, 1, 1000) = aten::slice(%outputs, %17, %17, %6, %18), scope: EncoderRNN
  %57 : Float(10, 1, 1000) = aten::slice(%56, %18, %17, %6, %18), scope: EncoderRNN
  %58 : Float(10, 1!, 500) = aten::slice(%57, %9, %12, %6, %18), scope: EncoderRNN
  %encoder_outputs : Float(10, 1, 500) = aten::add(%55, %58, %18), scope: EncoderRNN
  %decoder_hidden.1 : Tensor = aten::slice(%encoder_hidden, %17, %17, %9, %18)
  %61 : int[] = prim::ListConstruct(%18, %18)
  %62 : Tensor = aten::ones(%61, %13, %5, %14, %4)
  %decoder_input.1 : Tensor = aten::mul(%62, %18)
  %64 : int[] = prim::ListConstruct(%17)
  %all_tokens.1 : Tensor = aten::zeros(%64, %13, %5, %14, %4)
  %66 : int[] = prim::ListConstruct(%17)
  %all_scores.1 : Tensor = aten::zeros(%66, %5, %5, %14, %4)
  %all_scores : Tensor, %all_tokens : Tensor, %decoder_hidden : Tensor, %decoder_input : Tensor = prim::Loop(%max_length, %10, %all_scores.1, %all_tokens.1, %decoder_hidden.1, %decoder_input.1)
    block0(%72 : int, %73 : Tensor, %74 : Tensor, %75 : Tensor, %76 : Tensor):
      %input.2 : Float(1, 1, 500) = aten::embedding(%138, %76, %16, %15, %15), scope: LuongAttnDecoderRNN/Embedding[embedding]
      %input.3 : Float(1, 1, 500) = aten::dropout(%input.2, %8, %15), scope: LuongAttnDecoderRNN/Dropout[embedding_dropout]
      %97 : Tensor[] = prim::ListConstruct(%137, %136, %135, %134, %133, %132, %131, %130), scope: LuongAttnDecoderRNN/GRU[gru]
      %hidden : Float(1, 1, 500), %decoder_hidden.2 : Float(2, 1, 500) = aten::gru(%input.3, %75, %97, %10, %9, %8, %15, %15, %15), scope: LuongAttnDecoderRNN/GRU[gru]
      %100 : Float(10, 1, 500) = aten::mul(%hidden, %encoder_outputs), scope: LuongAttnDecoderRNN/Attn[attn]
      %101 : int[] = prim::ListConstruct(%9), scope: LuongAttnDecoderRNN/Attn[attn]
      %attn_energies : Float(10, 1) = aten::sum(%100, %101, %15), scope: LuongAttnDecoderRNN/Attn[attn]
      %input.4 : Float(1!, 10) = aten::t(%attn_energies), scope: LuongAttnDecoderRNN/Attn[attn]
      %104 : Float(1, 10) = aten::softmax(%input.4, %18), scope: LuongAttnDecoderRNN/Attn[attn]
      %attn_weights : Float(1, 1, 10) = aten::unsqueeze(%104, %18), scope: LuongAttnDecoderRNN/Attn[attn]
      %106 : Float(1!, 10, 500) = aten::transpose(%encoder_outputs, %17, %18), scope: LuongAttnDecoderRNN
      %context.1 : Float(1, 1, 500) = aten::bmm(%attn_weights, %106), scope: LuongAttnDecoderRNN
      %rnn_output : Float(1, 500) = aten::squeeze(%hidden, %17), scope: LuongAttnDecoderRNN
      %context : Float(1, 500) = aten::squeeze(%context.1, %18), scope: LuongAttnDecoderRNN
      %110 : Tensor[] = prim::ListConstruct(%rnn_output, %context), scope: LuongAttnDecoderRNN
      %input.5 : Float(1, 1000) = aten::cat(%110, %18), scope: LuongAttnDecoderRNN
      %112 : Float(1000!, 500!) = aten::t(%129), scope: LuongAttnDecoderRNN/Linear[concat]
      %113 : Float(1, 500) = aten::addmm(%128, %input.5, %112, %18, %18), scope: LuongAttnDecoderRNN/Linear[concat]
      %input.6 : Float(1, 500) = aten::tanh(%113), scope: LuongAttnDecoderRNN
      %115 : Float(500!, 7826!) = aten::t(%127), scope: LuongAttnDecoderRNN/Linear[out]
      %input : Float(1, 7826) = aten::addmm(%126, %input.6, %115, %18, %18), scope: LuongAttnDecoderRNN/Linear[out]
      %decoder_output : Float(1, 7826) = aten::softmax(%input, %18), scope: LuongAttnDecoderRNN
      %decoder_scores : Tensor, %decoder_input.2 : Tensor = aten::max(%decoder_output, %18, %15)
      %120 : Tensor[] = prim::ListConstruct(%74, %decoder_input.2)
      %all_tokens.2 : Tensor = aten::cat(%120, %17)
      %122 : Tensor[] = prim::ListConstruct(%73, %decoder_scores)
      %all_scores.2 : Tensor = aten::cat(%122, %17)
      %decoder_input.3 : Tensor = aten::unsqueeze(%decoder_input.2, %17)
      -> (%10, %all_scores.2, %all_tokens.2, %decoder_hidden.2, %decoder_input.3)
  %125 : (Tensor, Tensor) = prim::TupleConstruct(%all_tokens, %all_scores)
  return (%125)

 

評価を実行する

最後に Torch Script モデルを実行してチャットボット・モデルの評価を実行します。正しく変換されれば、モデルは eager モード表現におけるように正確に挙動します。

デフォルトでは、2, 3 の一般的な質問センテンスを評価します。ボットと貴方自身でチャットすることを望むばあい、evaluateInput 行をアンコメントして回転させてください。

# Evaluate examples
sentences = ["hello", "what's up?", "who are you?", "where am I?", "where are you from?"]
for s in sentences:
    evaluateExample(s, traced_encoder, traced_decoder, scripted_searcher, voc)

# Evaluate your input
#evaluateInput(traced_encoder, traced_decoder, scripted_searcher, voc)
> hello
Bot: hello .
> what's up?
Bot: i m going to get my car .
> who are you?
Bot: i m the owner .
> where am I?
Bot: in the house .
> where are you from?
Bot: south america .

 

モデルをセーブする

モデルを Torch Script に成功的に変換した今、非 Python 開発環境で使用するためにそれをシリアライズします。これを行なうため、単純に scripted_searcher モジュールをセーブできます、何故ならばこれはチャットボットモデルに対して推論を実行するための user-facinng インターフェイスだからです。Script モジュールをセーブするとき、torch.save(model, PATH) の代わりに script_module.save(PATH) を使用してください。

scripted_searcher.save("scripted_chatbot.pth")

 
以上