PyTorch 1.8 : PyTorch モデル配備 : TorchScript へのイントロダクション

PyTorch 1.8 チュートリアル : PyTorch モデル配備 : TorchScript へのイントロダクション (翻訳/解説)
翻訳 : (株)クラスキャット セールスインフォメーション
作成日時 : 04/29/2021 (1.8.1+cu102)

* 本ページは、PyTorch 1.8 Tutorials の以下のページを翻訳した上で適宜、補足説明したものです:

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

 

無料 Web セミナー開催中 クラスキャット主催 人工知能 & ビジネス Web セミナー

人工知能とビジネスをテーマに WEB セミナーを定期的に開催しています。
スケジュールは弊社 公式 Web サイト でご確認頂けます。
  • お住まいの地域に関係なく Web ブラウザからご参加頂けます。事前登録 が必要ですのでご注意ください。
  • ウェビナー運用には弊社製品「ClassCat® Webinar」を利用しています。
クラスキャットは人工知能・テレワークに関する各種サービスを提供しております :

人工知能研究開発支援 人工知能研修サービス テレワーク & オンライン授業を支援
PoC(概念実証)を失敗させないための支援 (本支援はセミナーに参加しアンケートに回答した方を対象としています。)

お問合せ : 本件に関するお問い合わせ先は下記までお願いいたします。

株式会社クラスキャット セールス・マーケティング本部 セールス・インフォメーション
E-Mail:sales-info@classcat.com  ;  WebSite: https://www.classcat.com/  ;  Facebook

 

 

PyTorch モデル配備 : TorchScript へのイントロダクション

このチュートリアルは TorchScript へのイントロダクションです、C++ のような高パフォーマンス環境内で実行可能な Python モデル (nn.Module のサブクラス) の中間表現です。

このチュートリアルでは以下をカバーします :

  1. 以下を含む、PyTorch のモデル・オーサリングの基本 :
    • モジュール
    • forward 関数を定義する
    • モジュールをモジュールの階層に構成する

  2. PyTorch モジュールを TorchScript、高パフォーマンス配備ランタイムに変換するための特定のメソッド
    • 既存のモジュールを追跡する
    • モジュールを直接コンパイルするためにスクリプティングを使用する
    • 両者のアプローチをどのように構成するか
    • TorchScript モジュールをセーブしてロードする

このチュートリアルを完了した後、C++ から TorchScript モデルを実際に呼び出すサンプルを一通り説明する 次の段階のチュートリアル を調べることに進むことを望みます。

import torch  # This is all you need to use both PyTorch and TorchScript!
print(torch.__version__)
1.8.1+cu102

 

PyTorch モデル・オーサリングの基本

単純な Module を定義することから始めましょう。Module は PyTorch の構成の基本ユニットです。それは以下を含みます :

  1. コンストラクタ、これは発動 (= invocation) のためにモジュールを準備します。
  2. パラメータとサブモジュールのセット。これらはコンストラクタにより初期化されて発動の間モジュールにより利用できます。
  3. forward 関数。これはモジュールが起動されたときに実行されるコードです。

小さいサンプルを調べましょう :

class MyCell(torch.nn.Module):
    def __init__(self):
        super(MyCell, self).__init__()

    def forward(self, x, h):
        new_h = torch.tanh(x + h)
        return new_h, new_h

my_cell = MyCell()
x = torch.rand(3, 4)
h = torch.rand(3, 4)
print(my_cell(x, h))
(tensor([[0.8791, 0.1063, 0.9373, 0.7739],
        [0.8209, 0.8623, 0.2297, 0.7261],
        [0.6958, 0.7173, 0.8353, 0.8942]]), tensor([[0.8791, 0.1063, 0.9373, 0.7739],
        [0.8209, 0.8623, 0.2297, 0.7261],
        [0.6958, 0.7173, 0.8353, 0.8942]]))

そこで私達は以下を行ないました :

  1. torch.nn.Module をサブクラス化するクラスを作成しました。

  2. コンストラクタを定義しました。コンストラクタは大きな働きはせず、単に super のためにコンストラクタを呼び出します。

  3. forward 関数を定義しました、これは 2 つの入力を取りそして 2 つの出力を返します。forward 関数の実際の内容は現実には重要ではありませんが、それは fake RNN セル の類 – つまり – それはループ上で適用される関数です。

モジュールをインスタンス化して、x と y を作成しました、これは単にランダム値の 3×4 行列です。それからセルを my_cell(x, h) で起動しました。これは forward 関数を順番に呼び出します。

もう少し興味深いことを行ないましょう :

class MyCell(torch.nn.Module):
    def __init__(self):
        super(MyCell, self).__init__()
        self.linear = torch.nn.Linear(4, 4)

    def forward(self, x, h):
        new_h = torch.tanh(self.linear(x) + h)
        return new_h, new_h

my_cell = MyCell()
print(my_cell)
print(my_cell(x, h))
MyCell(
  (linear): Linear(in_features=4, out_features=4, bias=True)
)
(tensor([[ 0.0076, -0.5203,  0.6029,  0.2598],
        [ 0.2583,  0.4240,  0.0408,  0.6245],
        [ 0.4987,  0.4963,  0.6609,  0.5512]], grad_fn=), tensor([[ 0.0076, -0.5203,  0.6029,  0.2598],
        [ 0.2583,  0.4240,  0.0408,  0.6245],
        [ 0.4987,  0.4963,  0.6609,  0.5512]], grad_fn=))

モジュール MyCell を再定義しましたが、今回は self.linear 属性を追加しました、そして forward 関数で self.linear を呼び出します。

ここで正確に何が起きているのでしょう?torch.nn.Linear は PyTorch 標準ライブラリからの Module です。丁度 MyCell のように、それは call シンタクスを使用して呼び出せます。Module の階層を構築しています。

Module 上の print は Module のサブクラス階層の視覚表現を与えます。私達のサンプルでは、Linear サブクラスとそのパラメータを見ることができます。

Module をこのように構成することにより、再利用可能なコンポーネントでモデルを簡潔に可読に作成できます。

出力上の grad_fn に気付いたかもしれません。これは autograd と呼ばれる自動微分の PyTorch のメソッドの詳細です。要するに、このシステムは潜在的に複雑なプログラムを通して導関数を計算することを可能にします。この設計はモデル・オーサリングにおいて莫大な量の柔軟性を可能にします。

さて柔軟性と呼ばれるものを調べましょう :

class MyDecisionGate(torch.nn.Module):
    def forward(self, x):
        if x.sum() > 0:
            return x
        else:
            return -x

class MyCell(torch.nn.Module):
    def __init__(self):
        super(MyCell, self).__init__()
        self.dg = MyDecisionGate()
        self.linear = torch.nn.Linear(4, 4)

    def forward(self, x, h):
        new_h = torch.tanh(self.dg(self.linear(x)) + h)
        return new_h, new_h

my_cell = MyCell()
print(my_cell)
print(my_cell(x, h))
MyCell(
  (dg): MyDecisionGate()
  (linear): Linear(in_features=4, out_features=4, bias=True)
)
(tensor([[0.5067, 0.6742, 0.6981, 0.1579],
        [0.2342, 0.8430, 0.3546, 0.2579],
        [0.5980, 0.8277, 0.5921, 0.7852]], grad_fn=), tensor([[0.5067, 0.6742, 0.6981, 0.1579],
        [0.2342, 0.8430, 0.3546, 0.2579],
        [0.5980, 0.8277, 0.5921, 0.7852]], grad_fn=))

再度 MyCell クラスを再定義しましたが、ここでは MyDecisionGate を定義しました。このモジュールは 制御フロー を利用しています。制御フローはループと if-ステートメントのようなものから成ります。

多くのフレームワークは完全なプログラム表現が与えられたときシンボリックな導関数を計算するアプローチを取ります。けれども、PyTorch では、勾配テープを利用します。演算をそれが発生するときに記録して、そして導関数を計算する間はそれらを後方に再生します。このように、フレームワークは言語の総ての構成要素のために導関数を明示的に定義しなくてもかまいません。

autograd がどのように動作するか

 

TorchScript の基本

さて実行サンプルを取り TorchScript をどのように適用できるかを見てみましょう。

要するに、TorchScript は PyTorch の柔軟で動的な性質の視点で、貴方のモデルの定義を捕捉するツールを提供します。tracing と呼ぶものを調べることから始めましょう。

 

モジュールをトレースする

class MyCell(torch.nn.Module):
    def __init__(self):
        super(MyCell, self).__init__()
        self.linear = torch.nn.Linear(4, 4)

    def forward(self, x, h):
        new_h = torch.tanh(self.linear(x) + h)
        return new_h, new_h

my_cell = MyCell()
x, h = torch.rand(3, 4), torch.rand(3, 4)
traced_cell = torch.jit.trace(my_cell, (x, h))
print(traced_cell)
traced_cell(x, h)
MyCell(
  original_name=MyCell
  (linear): Linear(original_name=Linear)
)

少し巻き戻して MyCell クラスの 2 番目のバージョンを取ります。前のように、それをインスタンス化しますが、今回は、torch.jit.trace を呼び出し、Module を渡し、そしてネットワークが見るかもしれないサンプル入力を渡します。

これは正確には何を成したのでしょう?それは Module を呼び出し、Module が実行されたときに発生した演算を記録し、そして torch.jit.ScriptModule のインスタンスを作成しました (TracedModule がそのインスタンスです)。

TorchScript は中間表現 (or IR) にあるその定義を記録します、一般には深層学習ではグラフとして参照されます。.graph プロパティでグラフを調べることができます :

print(traced_cell.graph)
graph(%self.1 : __torch__.MyCell,
      %input : Float(3, 4, strides=[4, 1], requires_grad=0, device=cpu),
      %h : Float(3, 4, strides=[4, 1], requires_grad=0, device=cpu)):
  %21 : __torch__.torch.nn.modules.linear.Linear = prim::GetAttr[name="linear"](%self.1)
  %23 : Tensor = prim::CallMethod[name="forward"](%21, %input)
  %14 : int = prim::Constant[value=1]() # /var/lib/jenkins/workspace/beginner_source/Intro_to_TorchScript_tutorial.py:188:0
  %15 : Float(3, 4, strides=[4, 1], requires_grad=1, device=cpu) = aten::add(%23, %h, %14) # /var/lib/jenkins/workspace/beginner_source/Intro_to_TorchScript_tutorial.py:188:0
  %16 : Float(3, 4, strides=[4, 1], requires_grad=1, device=cpu) = aten::tanh(%15) # /var/lib/jenkins/workspace/beginner_source/Intro_to_TorchScript_tutorial.py:188:0
  %17 : (Float(3, 4, strides=[4, 1], requires_grad=1, device=cpu), Float(3, 4, strides=[4, 1], requires_grad=1, device=cpu)) = prim::TupleConstruct(%16, %16)
  return (%17)

けれども、これは非常に低位な表現でグラフに含まれる情報の殆どはエンドユーザには有用ではありません。代わりに、コードの Python-シンタクス解釈を与えるために .code プロパティを使用できます :

print(traced_cell.code)
def forward(self,
    input: Tensor,
    h: Tensor) -> Tuple[Tensor, Tensor]:
  _0 = torch.add((self.linear).forward(input, ), h, alpha=1)
  _1 = torch.tanh(_0)
  return (_1, _1)

それでは 何故 この総てを行なったのでしょう?幾つかの理由があります :

  1. TorchScript はそれ自身のインタープリタで呼び出せます、これは基本的には制限された Python インタープリタです。このインタープリタは Global Interpreter Lock を獲得しませんので、非常に多くのリクエストが同じインスタンス上で同時に処理できます。

  2. この形式はモデル全体をディスクにセーブしてそれを Python 以外の言語で書かれたサーバ内のような、他の環境にロードすることを可能にします。

  3. TorchScript はより効率的な実行を提供するためにコード上のコンパイラ最適化を行なうことができる表現を与えます。

  4. TorchScript は個々の演算子よりもプログラムのより広いビューを要求する多くのバックエンド/デバイス・ランタイムと連動することを可能にします。

traced_cell の呼び出しは Python モジュールと同じ結果を生成することを見ることができます :

print(my_cell(x, h))
print(traced_cell(x, h))
(tensor([[-0.0247,  0.2107,  0.8478, -0.4302],
        [ 0.5998,  0.3156,  0.5910,  0.3392],
        [ 0.7202,  0.6929,  0.8065,  0.1688]], grad_fn=), tensor([[-0.0247,  0.2107,  0.8478, -0.4302],
        [ 0.5998,  0.3156,  0.5910,  0.3392],
        [ 0.7202,  0.6929,  0.8065,  0.1688]], grad_fn=))
(tensor([[-0.0247,  0.2107,  0.8478, -0.4302],
        [ 0.5998,  0.3156,  0.5910,  0.3392],
        [ 0.7202,  0.6929,  0.8065,  0.1688]], grad_fn=), tensor([[-0.0247,  0.2107,  0.8478, -0.4302],
        [ 0.5998,  0.3156,  0.5910,  0.3392],
        [ 0.7202,  0.6929,  0.8065,  0.1688]], grad_fn=))

 

モジュールを変換するためにスクリプティングを使用する

制御フローを積んだサブモジュールを持つ一つではなく、モジュールのバージョン 2 を使用したのには理由があります。それを今調べましょう :

class MyDecisionGate(torch.nn.Module):
    def forward(self, x):
        if x.sum() > 0:
            return x
        else:
            return -x

class MyCell(torch.nn.Module):
    def __init__(self, dg):
        super(MyCell, self).__init__()
        self.dg = dg
        self.linear = torch.nn.Linear(4, 4)

    def forward(self, x, h):
        new_h = torch.tanh(self.dg(self.linear(x)) + h)
        return new_h, new_h

my_cell = MyCell(MyDecisionGate())
traced_cell = torch.jit.trace(my_cell, (x, h))

print(traced_cell.dg.code)
print(traced_cell.code)
def forward(self,
    argument_1: Tensor) -> None:
  return None

def forward(self,
    input: Tensor,
    h: Tensor) -> Tuple[Tensor, Tensor]:
  _0 = self.dg
  _1 = (self.linear).forward(input, )
  _2 = (_0).forward(_1, )
  _3 = torch.tanh(torch.add(_1, h, alpha=1))
  return (_3, _3)

.code 出力を見ると、if-else 分岐はどこにも見つからないことを見ることができます!何故でしょう?tracing は正確に言ったことを行ないます、それは : コードを実行し、発生する演算を記録しそして正確にそれを行なう ScriptModule を構築します。不幸なことに、制御フローのようなものは消し去られます。

このモジュールを TorchScript でどのように忠実に表現できるのでしょうか?私達は スクリプトコンパイラ を提供します、これはそれを TorchScript に変換するために Python ソースコードの直接的解析を行ないます。スクリプトコンパイラを使用して MyDecisionGate を変換しましょう :

scripted_gate = torch.jit.script(MyDecisionGate())

my_cell = MyCell(scripted_gate)
scripted_cell = torch.jit.script(my_cell)

print(scripted_gate.code)
print(scripted_cell.code)
def forward(self,
    x: Tensor) -> Tensor:
  _0 = bool(torch.gt(torch.sum(x, dtype=None), 0))
  if _0:
    _1 = x
  else:
    _1 = torch.neg(x)
  return _1

def forward(self,
    x: Tensor,
    h: Tensor) -> Tuple[Tensor, Tensor]:
  _0 = (self.dg).forward((self.linear).forward(x, ), )
  new_h = torch.tanh(torch.add(_0, h, alpha=1))
  return (new_h, new_h)

Hooray! 今では TorchScript でプログラムの動作を忠実に補足しました。今はプログラムを実行してみましょう :

# New inputs
x, h = torch.rand(3, 4), torch.rand(3, 4)
traced_cell(x, h)

 

スクリプティングと tracing をミックスする

ある状況ではスクリプティングよりも tracing を使用することを必要とします (e.g. モジュールが TorchScript では現れて欲しくない定数 Python 値を基に作成される多くのアーキテクチャ決定を持つ (場合))。この場合、スクリプティングは tracing とともに組み合わせられます : torch.jit.script は traced モジュールのためにコードをインライン化できて、そして tracing は scripted モジュールのためにコードをインライン化できます。

最初のケースのサンプルは :

class MyRNNLoop(torch.nn.Module):
    def __init__(self):
        super(MyRNNLoop, self).__init__()
        self.cell = torch.jit.trace(MyCell(scripted_gate), (x, h))

    def forward(self, xs):
        h, y = torch.zeros(3, 4), torch.zeros(3, 4)
        for i in range(xs.size(0)):
            y, h = self.cell(xs[i], h)
        return y, h

rnn_loop = torch.jit.script(MyRNNLoop())
print(rnn_loop.code)
def forward(self,
    xs: Tensor) -> Tuple[Tensor, Tensor]:
  h = torch.zeros([3, 4], dtype=None, layout=None, device=None, pin_memory=None)
  y = torch.zeros([3, 4], dtype=None, layout=None, device=None, pin_memory=None)
  y0 = y
  h0 = h
  for i in range(torch.size(xs, 0)):
    _0 = (self.cell).forward(torch.select(xs, 0, i), h0, )
    y1, h1, = _0
    y0, h0 = y1, h1
  return (y0, h0)

そして 2 番目のケースのサンプルは :

class WrapRNN(torch.nn.Module):
    def __init__(self):
        super(WrapRNN, self).__init__()
        self.loop = torch.jit.script(MyRNNLoop())

    def forward(self, xs):
        y, h = self.loop(xs)
        return torch.relu(y)

traced = torch.jit.trace(WrapRNN(), (torch.rand(10, 3, 4)))
print(traced.code)
def forward(self,
    argument_1: Tensor) -> Tensor:
  _0, h, = (self.loop).forward(argument_1, )
  return torch.relu(h)

このように、スクリプティングと tracing は状況がそれらのいずれかを要求するとき一緒に利用できます。

 

モデルをセーブしてロードする

TorchScript モジュールをアーカイブ・フォーマットでディスクにセーブして (ディスクから) ロードするための API を提供します。このフォーマットはコード、パラメーター、属性そしてデバッグ情報を含みます、これはアーカイブが (完全に分離したプロセスにロード可能な) モデルの独立した表現であることを意味します。ラップされた RNN モジュールをセーブしてロードしましょう :

traced.save('wrapped_rnn.pt')

loaded = torch.jit.load('wrapped_rnn.pt')

print(loaded)
print(loaded.code)
RecursiveScriptModule(
  original_name=WrapRNN
  (loop): RecursiveScriptModule(
    original_name=MyRNNLoop
    (cell): RecursiveScriptModule(
      original_name=MyCell
      (dg): RecursiveScriptModule(original_name=MyDecisionGate)
      (linear): RecursiveScriptModule(original_name=Linear)
    )
  )
)
def forward(self,
    argument_1: Tensor) -> Tensor:
  _0, h, = (self.loop).forward(argument_1, )
  return torch.relu(h)

見れるように、シリアライゼーションはモジュール階層と通して調べたコードを保存します。モデルはまた例えば、python-free な実行のために C++ に ロードできます。

 

Further Reading

We’ve completed our tutorial! より込み入った複雑なデモについては、TorchScript を使用する機械翻訳モデルを変換するための NeurIPS デモを調べてください : https://colab.research.google.com/drive/1HiICg6jRkBnr5hvK2-VnMi88Vi9pUzEJ

 

以上