SyntaxHighlighter

2021年8月16日月曜日

PyTorch における Deformable Convolution の ONNX 変換

PyTorch における Deformable Convolution の ONNX 変換

PyTorch では、何年か前から Deformable Convolution が DeformConv2d としてサポートされていました。

しかし、 ONNX 変換はサポートされておらず、 DeformConv2d を含むネットワークを ONNX にしようとするとエラーになっていました。

今回は、 DeformConv2d を PyTorch で ONNX に変換できるようにするモジュール deform_conv2d_onnx_exporter を作ったので、その説明です。

が、諸事情により、一旦ライブラリの公開は取りやめました。すみません。 → 再度公開しました。 PyPI のページからダウンロードできます。

Deformable Convolution の説明自体はここでは触れません。

ONNX 変換に必要なこと

DeformConv2d を ONNX に変換できないのは、内部で使われている deform_conv2d 関数を ONNX に変換する方法が定義されていないためです。

最終的に呼ばれる torch.ops.torchvision.deform_conv2d は、 CPU, CUDA 向けなどに C++ で実装されています。
そのため、 ONNX に変換するためには専用の変換関数を用意してやる必要があるのですが、現時点では実装されていません。

似たようなものとしては、 torch.ops.torchvision.roi_pool があるのですが、こちらは torchvision/ops/_register_onnx_ops.py の中で、以下のように ONNX 変換用の関数が登録されています。

こちらにあるように、 Graph object g を用いて、 ONNX の Operator である MaxRoiPool を使って ONNX のグラフを構築していくことになります。

ONNX Operator

ONNX の Operator 一覧は GitHub にあります。

今回やる必要があるのは、ここに定義されている Operator の一覧の中から適切なものを選び、組み合わせることで、 deform_conv2d の挙動を構築することになります。

なお、 ONNX の Operator は通常の手続き的な書き方ができるわけではなく、基本的には Tensor に対する処理の組み合わせで書いていくことになります。

deform_conv2d の ONNX Operator での実装

やることは、 Deformable ConvNets v2: More Deformable, Better Results の論文に書かれた内容を愚直に実装することになります。
注意点としては、 deform_conv2d の引数として渡されてくる Tensor のデータの格納順序が一部不明瞭なところがあるので、それを実動作ベースで確認しながら進めることになります。

大まかな流れとしては、下記になります。

  1. 各 Convolution の対象となる画素の位置を求める。
  2. その画素の位置座標が、画像外を指さないように丸める。
  3. 位置座標は小数になっているので、 Bilinear 補完により、周囲 4画素の値から重み付け平均を取り、画素の値とする。
    なお、このとき、画素の値は GatherND Operator を用いて取得をする。
  4. 得られた画素の値を基に、必要に応じて mask をかけ、 Convolution を行う。
  5. 必要に応じて bias を足す。

「愚直に実装する」と書きましたが、実際にはここはかなり苦労しました。
上記の各処理を ONNX の Operator のみで記述していくのは、 deform_conv2d の内部実装ドキュメントの少なさも相俟って、なかなか大変なものがありました。

気を付けたこと

Transpose を多用しない

上記の流れを実装する際に、 (論文に沿った愚直な方法では) GatherND を使って画素値を集めてくることになるかと思います。
似たような Operator の GatherElements でも実現はできますが、入力画像のチャネル数が大きくなった場合に、 (おそらく) とても非効率になります。

一方で、現時点の GatherND Operator の仕様上、複数のチャネルにまたがってデータを一括して集めてくる場合、そのチャネルがテンソルのランクの末尾の方に位置している必要があります。
つまり、 Tensor[batch, height, width, channel] のように、座標として指定される height, width よりも末尾に channel が位置している必要があります。

そのため、 Transpose Operator (PyTorch の Tensor の permute) で軸の順序を入れかえることになりますが、この処理は状況によっては負荷がかかるようです。

当初は Transpose を多用するコードになっていたのですが、なぜか速度がまったく出ず、 ONNX の profiling をすることで、無駄な Transpose が遅い要因の一つであることが分かりました。
そのため、必要最小限な部分に絞って Transpose を呼ぶようにすることで、効率化を図りました。

ただ、この辺りは ONNX のランタイムの実装にも強く依存する話なので、環境が変わると結果も異なるかもしれません。
不要な Operator を呼ばないようにするのはよいと思いますが。

GatherND で範囲外の座標値にアクセスしない

当然ですが、 GatherND では範囲外の座標値にアクセスしてはいけません。

当初は、これを防ぐために、範囲外なのかどうかを Greater, Less Operator を使って判定した mask を作り、これを活用することで範囲外へのアクセスを防いでいました。

しかし、この方法ではそれなりに時間がかかることから、 Clip Operator を使った方法に変更しました。

これは、まず入力画像の周囲に必ず 0 クリアされた Padding 領域が入るようにしておくことで、 Clip された座標値が Padding 領域を指すようにしておき、 Bilinear 補完の計算に入らないようにすることで実現しました。

これはそれなりに効率化がされたようで、処理速度も速くなりました。

これらを気を付けることで、 (遅いことは遅いのですが) それなりに使える範囲での実行速度を実現することができました。

よさそうなら PyTorch 本体にも Pull Request を出してみようかと思います。

2020年8月3日月曜日

nginx と Flask を使った Git LFS サーバー

nginx と Flask を使った Git LFS サーバー

Git LFS サーバーを自前で用意する必要が生じたのですが、手頃なものが見つからなかったので、手作りで作ってみることにしました。
Git LFS サーバーの仕様自体はシンプルで、とりあえず動作させるぐらいであればすぐに作れたので、今後のために記録を残しておこうと思います。

また、今さらですが Docker にも慣れておきたかったので、 Docker のイメージにしてみました。本来は docker-compose を使うようなケースのような気もしますが、とりあえず単一の Docker コンテナにしてあります。

なお、作成したサーバーのソースコードなどは、 gitlfs_with_nginx として GitHub 上に置いてあります。

Git LFS サーバー

Git LFS は、 Git で大きめのバイナリファイルを管理するために GitHub が中心となって策定した Git の拡張機能です。

概要としては、以下のようになるかと思います。

  • あらかじめ指定したファイルを、 Git リポジトリではなく、別に指定した Git LFS サーバーに保存する。
    ファイルは、ファイル固有の oid (現状ではファイルの SHA256 値) と紐付けて、 Git LFS サーバー内で Key-Value ストアのように保存される。
  • 元の Git リポジトリには、実体となるファイルの oid などが書かれたポインターファイルのみを保存しておき、 checkout などの際に、適宜 Git LFS サーバーからファイルの実体をダウンロードしてくる。
  • これらのファイルの保存やダウンロードの動作は、 Git のフックなどで実現されており、ユーザーはほとんど意識することなく、通常の Git リポジトリで作業する感覚で作業することができる。

ちなみに、私は Git LFS を最初に使ったとき、「Git LFS サーバーで管理されているファイルだけ、 Subversion のようなタイミングでダウンロードされるんだな」と感じました。

それはともかくとして、 Git LFS サーバーは比較的シンプルな仕様の HTTP サーバーです。
Batch APIBasic transfer をサポートするだけであれば、(どこまで異常系を考えるかによりますが) それほど悩まずに実装することができると思います。

Flask と nginx による Git LFS サーバー

そこで、 Git LFS サーバーの理解を深めるのを兼ねて、 Python の Flask フレームワークを使って実装してみました。
また、 nginx をリバースプロキシとして使うことで、パフォーマンスを向上させています。
以下に概要のみ、記します。 これらの処理は lfs_server.py に書かれています。

batch エンドポイント (batch メソッド)

Batch API の仕様に従って、 download, uploadoperation 要求に対し、指定された oid のファイルの実体をダウンロードもしくはアップロードするための URL を返します。

このとき、ファイルの実体をダウンロード/アップロードする際の GET/PUT リクエスト時に送信してもらうヘッダを同時に返します。
このヘッダには認証用のデータなどを含めることができます。
今回の実装でも、リポジトリ名や認証期限などを blake2b でハッシュをとったものをヘッダに追加しています。

ダウンロード用エンドポイント (download_file メソッド)

ダウンロードは、基本的に nginx に処理を任せます。
Flask 側では認証処理をしたあと、 X-Accel-Redirect ヘッダを指定し、 nginx 側で静的なファイルを配信するように構成することで、 Flask 側でファイル配信しなくてすむようにしています。

具体的には、 X-Accel-Redirect ヘッダに /repos/repository_name/oid のようなパスを指定することで、 nginx から静的なファイルを配信できるようにしています。

アップロード用エンドポイント (upload_file メソッド)

アップロードも、基本的に nginx に処理を任せます。
nginx 側で client_body_temp_path, client_body_in_file_only などを使うことで、クライアント側から PUT されてきたファイルを、 nginx がファイルに保存するようにします。
そして、そのファイル名を Flask 側に渡すことで、 Flask 側では渡されたファイルを oid で一意に決まる所定の場所に移動させます。
Flask 側はファイルの移動を行うだけなので、処理の負荷が少なくてすみます。

Docker での実装

上記のような仕組みの Flask サーバーの実装や nginx の設定を行い、 Docker イメージとして構築してみました。

env.list としていくつかのパラメータは変更できるようになっており、社内利用ぐらいであれば、 Basic 認証を追加するぐらいで使えなくはないのではないかと思います。

2019年12月31日火曜日

Windows 上の Python で OpenVINO を動かす

Windows 上の Python で OpenVINO を動かす

今回は、 Windows 上の Python で、 Intel からリリースされている AI 推論エンジン OpenVINO を動作させます。
といっても、OpenVINO ツールキットそのもののインストールは不要です。
Python の pip ライブラリをインストールするだけで試すことができます。

Intel OpenVINO

OpenVINO は Intel によって開発されている、エッジ端末上で AI 推論を行うためのライブラリです。
あくまで推論に特化しており、学習は別のツールで行うことが前提です。
また、推論エンジンは Intel CPU, GPU, GNA, VPU, FPGA などに対して高度に最適化されています。

事前準備

Python のインストール

Windows 上で、 Python 3.5, 3.6, 3.7 のいずれかをインストールしておきます。
64bit 版をインストールしておく必要があります。
現時点では、 Python 3.8 には対応していません。

OpenVINO の pip ライブラリのインストール

元々の OpenVINO は、現時点では pip ライブラリになっていないようです
そのため、私が Windows 向けに非公式な pip ライブラリ (wheel パッケージ) を作っておきました。
今回はこのライブラリをインストールします。
なお、後に公式ライブラリが公開される場合にややこしくなるので、公式の PyPI には登録していません。

PyPI に登録されていないので、下記のようにインストールします。

なお、 openvino-python をインストールすると、 cv2 モジュールも使えるようになります。そのため、別途 opencv-python などをインストールする必要はありません。

また、 numpy も必要なのでインストールしておきます。

パスの設定

openvino-python を動作させる際に必要な DLL は、 pip によるインストールの際に所定のディレクトリにダウンロードされています。
venv 環境の場合、 venv/Library/bin の下にダウンロードされます。
そのため、このディレクトリを PATH の先頭に追加しておきます。

venv の場合、 venv/Scripts/activate.bat の末尾を以下のように変更しておくとよいと思います。

動作確認

上記のように事前準備ができ、パスが通った状態であれば、下記のスクリプトがエラーなく実行できるはずです。

これで、 IENetwork など、種々の OpenVINO のクラスを使うことができます。

次回は、実際に Pre-trained なモデルを使い、簡単な動作サンプルを作ってみようと思います。

2018年11月24日土曜日

1バイナリの Python と exepy

1バイナリの Python と exepy

1バイナリにした Stackless Python の更新もそろそろ終わりなので、その中に含まれている exepy ライブラリについてまとめておこうと思います。

1バイナリの Python の仕組み

この Python は、 外部 DLL や *.py ファイルが不要になっています。
DLL については、ビルド時に tcl/tk, OpenSSL などを含め、静的リンクすることで解決しています。
*.py ファイルについては、 embeddedimport というモジュールを新規に開発することで対応しています。

これらについては Single Binary 版の Stackless Python 3.6.4Single Binary StacklessPython 3.6.4 L8 リリースに簡単にまとめてあります。

exepy の仕組み

2.0.0 以降の exepy では、以下のようにして exe を作ることができます。

上記で作られた exe は、一時フォルダにファイルを展開したりすることなく、単一の exe ファイルだけで input.py の処理を実行することができます。

さて、このとき、 sample.exe は python.exe とほぼ同じバイナリになっています。
唯一の違いは、 input.py に相当する内容が sample.exe のリソースとして組み込まれているところです。

embeddedimport は、実行している exe ファイル (sample.exe) のリソースに特定の ID のリソースが含まれている場合、その内容から *.py のデータをデコードしロードします。
つまり、 import input が実行された場合、 embeddedimport は以下の順で *.py を探します。

  1. 現在実行中の exe ファイルのリソース内
  2. exe 内に静的に持っている *.py のデータ (具体的には embeddedimport_data.c の内容)
そのため、元々の python.exe にリソースを埋め込むだけで、 *.py を含んだ exe ファイルを作ることができます。

exepy のサンプル

いくつかサンプルを示しておきます。

複数ファイルからなるサンプル

main.py と sub.py があり、 main.py が sub.py を import しているとします。

上記のファイルを下記のように exe 化します。

上記で作られた sample.exe の実行は、実質的に下のコマンドを実行したときとほぼ同等の動きになります。

画像ファイルなどの抱き込み

Single Binary StacklessPython 3.6.6 L16 に含まれる exepy 2.1.0 では、画像ファイルなどの抱き込みができるようになっています。
例えば、 picture.png を tkinter で表示する例を考えます。

以下のような main.py を用意します。

こちらを以下のコマンドで exe 化します。

これで picture.png も組み込まれた exe ができあがります。

__loader__.get_data(...) で、通常のファイルのようにデータを取得することができます。
あとはこれを用いて tkinter の Label, Widget などで表示すればよいです。

今回の変更で embeddedimporter が get_data をサポートしました。
これにより、任意のバイナリデータを組み込めるようになっています。

あとは、アイコンの変更機能ぐらいを追加して開発も終了です。

2018年6月26日火曜日

Single Binary StacklessPython 3.6.4 L8 リリース

Single Binary StacklessPython 3.6.4 L8 リリース

業務の効率化や Python インタプリタの勉強も兼ねて、 StacklessPython を 1 バイナリ化していましたが、バージョン L8 まできました。

GitHub のリリースページからダウンロードできます。

今回は、主に以下の変更をしました。

  • tkinter のサポート
  • EXE 化の実験的サポート
  • Windows Subsystem 版のサポート

tkinter のサポート

これまでに Python の標準ライブラリは、ほぼすべてサポートしていましたが、 tkinter はいくつかの理由でサポートしていませんでした。

tkinter をサポートし、1 バイナリで実行するには、 tcl/tk のライブラリを静的にリンクする必要があります。
また、 tcl/tk は、いくつかの *.tcl ファイルを必要とするので、こちらについても対応する必要があります。

静的リンクについては、 nmake -f makefile.vc core OPTS=static のように OPTS として static を指定すれば、静的リンク版をビルドできるので、こちらを使うことになります。

一方で、*.tcl ファイルの抱き込みについては、やや面倒です。

*.tcl ファイルの抱き込み

tcl では、Tcl_Filesystem という構造体で、複数のファイルシステムを持つことができます。
これは一言で言うと Python の path_hooks に相当するものです。
今回は embeddedFilesystem というファイルシステムを作成し、 embeddedfs:/tcl8.6/init.tcl のように embeddedfs:/ で始まる仮想的なパスが指定された場合に、バイナリの中に保持しているファイルの中身を返すようにしています。

詳しくは tclEmbeddedFilesystem.c を参照してください。

EXE 化の実験的サポート

せっかくなので、おまけの機能として EXE 化の機能を付けてみました。
なお、元々、標準ライブラリ外の機能は考慮していないので、py2exe や PyInstaller の代わりには使えません。あくまで、標準ライブラリだけを使って書けるスクリプトの EXE 化ができます。

例えば下記のように、 tk でウィンドウを作成し、クリックでカウントアップするだけのプログラムを EXE 化してみます。

今回の Single Binary StacklessPython では、上記のように tkinter を使うような場合も、実行することができます。

これを、単一の EXE にするには、下記のようにします。
ただし、これはまだ実験的な機能なので、将来は仕様を変更するかもしれません。

上記を実行すると sample.exe が生成されます。

サイズは 14MB ぐらいになりますが、テンポラリディレクトリにファイルを展開したりはしないので、かなり高速に実行されます。

Windows Subsystem 版のサポート

今回から pythonw.exe のように、コンソール画面が表示されないバイナリを作っておきました。

こちらは、主に GUI を使うスクリプトを EXE 化する際に使うと便利です。

Python, tcl/tk の実装の勉強にもなり、 AppVeyor の利用の勉強にもなりましたが、すべての標準ライブラリを 1 バイナリに収めたので、そろそろ終了にする予定です。

2018年6月13日水曜日

Single Binary 版の Stackless Python 3.6.4

Single Binary 版の Stackless Python 3.6.4

Stackless Python が 3.6.4 対応されていたので、 1 バイナリ化した Stackless Python も対応してみました。

GitHub のページから、ダウンロードしてご利用ください。

1 バイナリ化した Stackless Python

以前に Stackless Python 2.7 を 1 バイナリ化しましたがその当時の変更を Python 3.6 に対応させることになります。
今回は、その際の対応と、そもそも 1 バイナリ化した Stackless Python はどういうものかについて、まとめようと思います。

1 バイナリ化した Stackless Python 作成の目的

業務の都合上、「種々の PC 実機上で簡単に Python を実行したい」という動機がありました。
それに加えて、 pausable unittest を実行したかったので、通常の Python ではなく Stackless Python を使うことにしました。

また、極力、通常の Python と比べて、実行速度が遅くならないようにしたいということも考えていました。
前に Ruby を OCRA で固めたときは、実行前の展開にやや時間がかかるのがストレスだったので、今回はそういったことがないようにしたいと思っていました。

1 バイナリ化した Stackless Python の変更点

デフォルトの Stackless Python から以下の変更を加えました。

  • プログラム引数の変更
  • embeddedimport の実装
  • 3rd party ライブラリの同梱
  • その他の小変更

プログラム引数の変更

1 バイナリ化した Stackless Python は、引数の意味を二つだけ変更してあります。

  • -B: この引数は意味が逆になっており、-B が存在しないときに、 *.pyc ファイルを作成しないようになっています。
  • -E: この引数も意味が逆になっており、-E が存在しないときに、環境変数を参照しないようになっています。

これらの変更は、いずれも未知の環境で動作しやすいようにしておくためです。
時刻が正しくない複数の実機で実行した場合や、他に Python の処理系がインストールされていた場合にも、正しく動作させるためです。

embeddedimport の実装

通常、 Python の import 文は、 sys.path に登録されたパスを順に探し、その中から外部モジュールをロードします。
また、 sys.path には python.zipも 登録されており、 ZIP ファイルの中もサーチの対象になっています。
この ZIP ファイルからのロードは、 zipimport というモジュールが担っています。

今回は、「一つのバイナリにしたい」という目標があったので、 zipimport を参考にして、 embeddedimport を作り、実装しています。

挙動の概要としては、以下のようになります。

  1. ビルド時に、 Lib 以下の *.py ファイルをすべて C 言語の文字列に変換して、 python のバイナリに組み込んでおきます。
  2. 実行時には、 sys.pathC:/path_to/python.exe のように python.exe のフルパスを登録しておきます。また、 sys.path_hooksembeddedimporter モジュールを登録しておきます。
  3. これにより、 import pickle などが処理される際に embeddedimporter によってモジュールのインポートが試みられます。
  4. embeddedimporter では、自身が保持している *.py ファイルの中から指定されたファイルを探し、それを実行します。

3rd party ライブラリの同梱

快適に使えるように、いくつかのライブラリを同梱してあります。

  • comtypes
  • pycodestyle
  • pyflakes
  • pyreadline
  • PyYAML with libyaml

PyYAML に関しては、 libyaml をビルドしておき、 C 言語のサポートがあった方がずっと速いので、そのようにしてみました。

一点、注意としては、 libyaml とのブリッジライブラリである _yaml は、 Cython 形式になっており、 C 言語としてビルドするには前処理が必要でした。
Stackless Python の場合、 Fast Calll 時の関数呼び出しの処理が変更されているので、 CYTHON_FAST_PYCALL を 0 に設定し、 _yaml から Fast Call させないようにすることが必要でした。

その他の小変更

プログラムの引数を変更した関係で、いくつかのテストを変更しました。 -E を使っているテストは非常に多かったので、その辺りを中心に変更しました。

また、 C 言語で書かれた拡張ライブラリ *.pyd を、静的に Python にリンクするために、ビルド方法や config.c でのビルトインモジュールの定義部分を変更しました。

私の業務の都合上、 XML の属性をソートしたくなかったので、 xml.etree.ElementTree の出力部分に少し手を加えました。
具体的には ElementTree.writesort_attrib=True というキーワード引数を追加し、ソートを制御できるようにしておきました。

これらの対応を行い、テストがほぼ通ったので、ついにリリースになりました。
Windpws PE 上での作業や、多数のテスト実機上で Python を走らせる必要がある場合などに、ご利用いただければと思います。

2018年3月8日木曜日

Windows 上で一時的に Python を使って作業する際に使っているバッチファイル

Windows 上で一時的に Python を使って作業する際に使っているバッチファイル

私は、仕事でクリーンな (OS 展開直後の) Windows 実機や Windows PE 上で、Python を用いて作業をすることがしばしばあります。
今回は、その際によく使っているスクリプトを備忘録としてまとめておきます。

いずれも、 Python の -x オプションを使って、バッチファイル先頭行の goto 文をスキップしています。

1バイナリの Python

前提として、1バイナリ化した Python を使います。

以前に書いたように、ほぼすべての標準ライブラリを含んだ StacklessPython を、単一の EXE ファイルをコピーするだけで動作するようにしてあります。
こういった用途のために作ったので、インストールが不要かつコピーするだけで使えるようにしました。

管理者権限で実行する Python スクリプト

下記のようなバッチファイルを用意し、print("hoge") 部分を好きな Python スクリプトに変更すれば、ダブルクリックするだけで、管理者権限の Python スクリプトとして実行することができます。
なお、同じディレクトリに python.exe が必要です。

readline を有効にした状態で Python インタプリタを立ち上げるスクリプト

こちらも、下記のようなバッチファイルを用意し、ダブルクリックすれば、 Python のインタプリタが立ち上がります。
pyreadline を使っているので、 Ctrl-f などのキーバインドや、補完、ヒストリ保存もできます。

作業の内容によっては使えると思いますので、置いておきます。