SyntaxHighlighter

2023年11月27日月曜日

SQLite3 のトランザクションと Rails

最近は開発よりもマネージメントの仕事が多いのですが、久しぶりに技術的な話で書くネタができたので書いておきます。

SQLite3 のトランザクションは他のデータベースと少し異なり特殊なので、色々とはまりがちですが、その内容についてまとめておきます。
Rails での話も少し書いておきます。

SQLite3 の分離レベル

SQL の分離レベルは、 Wikipedia の記事にあるように、 Read Uncommitted, Read Committed, Repeatable Read, Serializable の4種類があります。

SQLite3 では、 Read Uncommitted になる例外もありますが、基本的には常に Serializable です。
後述するトランザクションのモードとして、 Deferred, Immediate, Exclusive が指定できますが、それとは関係なく、常に Serializable になるようです。
つまり、どのトランザクションのモードを選んでも、 Dirty Read や Phantom Read などは起こらず、トランザクション中は、常に整合性の取れた読み取りができるかエラーになるかのいずれかになります。

SQLite3 のトランザクションのモードと、エラーになるケース

SQLite3 では、トランザクションのモードを BEGIN TRANSACTION の際に指定することができます。

公式のページに書かれていますが、 Deferred, Immediate, Exclusive の3つを指定でき、デフォルトでは Deferred が使われます。
細かな定義については公式のページに記載があるので割愛しますが、それぞれで、「ロックを取得するタイミング」、「取得するロックの種類」が異なります。
これらは、既に実行中のトランザクションが別で存在する場合に、「相手のトランザクションの終了をどのタイミングで待つか」や「何をしたときにエラーにするか」が異なります。

例えば、標準の設定である Deferred を選んだ場合、 BEGIN DEFERRED TRANSACTION を実行したタイミングでは何のロックも取得されず、その後に続く SELECTINSERT などが実行された際に、初めて読み取り用の Shared ロックや書き込み用の Reserved ロックが取得されます。
なお、 Shared ロックと異なり、同時に複数のセッションが Reserved ロックを取得することはできません。
そのため、例えば下記のようなコードをほぼ同時に二つ実行すると、後から実行した方は INSERT 実行のタイミングで Reserved ロックの取得に失敗することになります。

これは、 SELECT 実行の時点で Shared ロックが取得され、 INSERT 実行の時点で Reserved ロックの取得が試みられることによります。
Reserved ロックは同時に一つのセッションしか取得できないので、後から実行した方のコードでは、 Reserved ロックの取得ができません。
このタイミングで、後から実行した方のコードは、トランザクションの実行に失敗します。
勘違いしやすいですが、自動のリトライは走らず Busy Timeout の設定も意味を持ちません。
これは、後から実行した方のコードでも、既に SELECT を発行しているために、このトランザクションの Serializable の分離レベルを保証できなくなるからです。
よって、 INSERT 実行のタイミングで、デッドロックにより失敗します。

一方で、後から実行した方のコードで、 SELECT などの読み取りのみを行うのであれば、トランザクションは最後まで問題なく実行されます。
おそらくですが、 Deferred のトランザクションが最も効果的に動作するのはこのような読み取りの場合で、この場合は Phantom Read などの影響を受けずに読み取れることが保証できます。

なお、 Immediate を指定したトランザクションの場合、 BEGIN IMMEDIATE TRANSACTION を実行したタイミングで即座に Reserved ロックが取得されるので、同時に二つのコードを実行してもエラーは発生しません。
ただし、 SELECT も待たされてしまうので、並列性は下がることになります。
同時に読み込みや書き込みがあってもエラーとなる可能性は下がるので、こちらが適しているケースも多々あるように思います。

Ruby on Rails での挙動

Ruby on Rails では、標準では SQLite3 がデータベースとして使われるように設定されています。

一方で、 SQLite3 独自のトランザクションのモードである Deferred や Immediate は、 Rails の世界からは容易には指定することはできません。
原則として常に Deferred になります。
なお、分離レベルとしては、 Read Uncommitted は指定することができるようになったようです。

いずれにしても、 SQLite3 は基本的には分離レベルは Serializable であり、その上で Deferred, Immediate などのトランザクションの種類を指定することになるので、他のデータベースも考慮して、汎用的な Rails のコードを書こうと思うと、トランザクションのモードを指定するのは難しくなります。

sqlite3 ライブラリでの吸収

こちらについて少し考えた結果、 SQLite3 へアクセスするためのライブラリ sqlite3 で吸収するのがよいのではないかと考えました。

そこで、こちらの Pull Request で、「標準で使うトランザクションのモード」をコンストラクタで指定できるようにしてもらいました。

これにより、トランザクション実行時に、どのようなモードにするかの既定値を指定することができるようになります。
Rails では、何も指定せずにトランザクションが開始されるので、そういった状況で使われるモードを指定できることになります。

Ruby on Rails での設定

以上を踏まえ、 sqlite3 のバージョン 1.6.9以降を使った上で、以下の設定値を config/database.yml に書くと、トランザクションが Immediate モードで実行されるようになります。

2023年4月3日月曜日

PyTorch の TorchScript のデバッグメッセージ (PYTORCH_JIT_LOG_LEVEL によるログ出力)

PyTorch の挙動を調べる必要があった際に、ログ出力の設定を調べたことを書いておこうと思います。

PyTorch の TorchScript のログ出力

PyTorch のモデルを保存する際などに PyTorch 内部で利用される TorchScript には、標準でデバッグ用のログ出力の機能が備わっています。

なお、私は ONNX へのエクスポートの際の挙動のデバッグの際にこのログ出力の機能を用いました。

PYTORCH_JIT_LOG_LEVEL 環境変数の設定

基本的に、 jit_log.h の冒頭のコメント部分に書かれていることに従って環境変数を設定します。

この環境変数には、以下のようにログ出力したいファイルのファイル名を指定します。

  • PyTorch のソースコードを見て、ログを出力したいファイルを探します。
    例えば、 torch/csrc/jit/passes/onnx/shape_type_inference.cpp のログを出力したい場合、 shape_type_inference.cpp が設定すべきファイル名になります。
  • 出力したいログのレベルは現時点では 3段階あり、その値に応じて > をいくつか付与します。
  • 複数のファイルのログを出力したい場合は、 : で区切って連結します。
  • 結果として、 shape_type_inference.cpp:>subgraph_matcher.cpp のような文字列を PYTORCH_JIT_LOG_LEVEL に設定します。

結果として、 Bash であれば export 'PYTORCH_JIT_LOG_LEVEL=shape_type_inference.cpp:>subgraph_matcher.cpp' のように設定します。
Windows 上のコマンドプロンプトを使う場合は、 set "PYTORCH_JIT_LOG_LEVEL=shape_type_inference.cpp:>subgraph_matcher.cpp" のように設定します。

> を含む場合、 '" で囲む必要があるのに注意してください。

ログ出力の例

例として、以下のようなスクリプトを実行する際のログを出力することを考えます。

このスクリプトを実行したときの shape_type_inference.cpp のログをすべて出力する場合、 >>shape_type_inference.cppPYTORCH_JIT_LOG_LEVEL 環境変数に設定します。
この状態で上記のスクリプトを実行すると、以下のようなログが出力されます。

Windows 上でログ出力する際の注意事項

実際には、先の例の通りにやっても、 Windows ではログが出力されません。
これは、 v2.0.0 の時点での (おそらく) バグで、 PYTORCH_JIT_LOG_LEVEL に設定されたファイル名と __FILE__ を比較する際の不具合によるものです。

StripBasename 関数の中でファイルのフルパスからファイル名部分を抽出していますが、この部分はディレクトリの区切り文字として / しか見ていません。
そのため、 Windows で __FILE__C:\actions-runner\_work\pytorch\pytorch\builder\windows\pytorch\torch\csrc\jit\passes\onnx\shape_type_inference.cpp のようなファイル名が設定されている際には、ファイル名部分として shape_type_inference.cpp がうまく抽出されず、 PYTORCH_JIT_LOG_LEVELshape_type_inference.cpp が設定されていてもうまくログが出力されませんでした。

この場合、 StripBasename を修正してビルドし直すか、 site-packages\torch\lib\torch_python.dll をバイナリエディタで直接修正し、 DLL に含まれる C:\actions-runner\_work\pytorch\pytorch\builder\windows\pytorch\torch\csrc\jit\passes\onnx\shape_type_inference.cpp の部分を C:/actions-runner/_work/pytorch/pytorch/builder/windows/pytorch/torch/csrc/jit/passes/onnx/shape_type_inference.cpp に変更することで、ログを出力できるようになります。

なお、このプルリクエストで修正されました!

このログを使ってデバッグした話の詳細については、次回以降に書くかもしれません。

2022年5月5日木曜日

SQLAlchemy を用いた Snowflake へのアクセス

SQLAlchemy を用いた Snowflake へのアクセス

今回は、Snowflake SQLAlchemy ツールキットおよびPythonコネクターの使用にあるような、 SQLAlchemy を用いた Snowflake データベースへのアクセスについてまとめようと思います。

また、 Snowflake の JSON データへアクセスするのを容易にするために開発したライブラリである snowflake-sqlalchemy-json についても触れようと思います。

Snowflake と半構造化データ

Snowflake は、いわゆるクラウドでビッグデータを処理するためのデータプラットフォームです。
細かく最適化を考えなくても、それなりによいパフォーマンスでデータの保存/読み出しができ、 SQL でのアクセスもサポートされています。

2022年5月時点では、30日の無料トライアルを受け付けており、どんな雰囲気かは無料で試すことができます。

Snowflake は、一般的なデータ型をサポートしており、他のデータベースを使っていた人であれば、違和感なく使えるように思います。
なお、ビッグデータ向けのデータベースでよくあるように、 Unique 制約などは強制されないので、そこには注意が必要です。

少し特徴的な型として、半構造化データ型というものがあります。
これは、 VARIANT などの型をカラムに指定することで、カラム内に JSON, Avro などの半構造化データを入れておき、 SQL から直接アクセスできるようにするものです。

例えば SELECT column_name:key1 FROM table; のように記述することで、 table テーブルの column_name カラムにある {"key1": "value1", "key2": "value2"} の中の "value1" にアクセスすることができます。
WHERE の条件部分で指定することも可能で、通常のカラムと同様にアクセスすることができます。

SQLAlchemy による SQL の生成

話は変わりますが、 Python のライブラリで SQLAlchemy というものがあります。
このライブラリは ORM として使うこともできますが、生の SQL をラップし Python から扱いやすくするという、 core と呼ばれる使い方もあります。

これは、以下のように Python のコードとして SQL を組み立てられるものです。

SQLite の場合、上記の query は、以下のような SQL に変換されます。

このように SQLAlchemy を用いると、 Python のコードとして SQL を作ることができます。

他にも JOIN や CTE (Common Table Expression) を用いた例を書いておくと以下のようになります。
この例では、テーブルの定義に declarative_base を使っています。この場合は、 Table を使う場合と異なり、 .c の部分が不要になり、補完も効きやすくなるので、書きやすいかもしれません。
なお、例自体には特に意味はありません。

Python で記述することで、 CTE 部分を変数として保持でき、それなりに分かりやすく記述できているように思います。

上記の query からは、下記の SQL が生成されます。

snowflake-sqlalchemy

Snowflake でも、上述した SQLAlchemy を使うことができます。

公式のページで使い方がまとめられているので、こちらのページを参考にするのがよいと思います。

なお、 Snowflake の場合、テーブルを指定する際には DATABASE.SCHEMA.TABLE のように、データベース名とスキーマ名でテーブル名を修飾することで、一意にテーブルを指定することができます。
SQLAlchemy で、このようにデータベース名やスキーマ名も含めて修飾したテーブル名を指定する場合、以下のように quoted_name を用いてテーブル名を記述することで、実現できます。

snowflake-sqlalchemy-json

上述したように、 Snowflake では VARIANT 型のカラムに対しては、 JSON 形式などの値に対して SQL 内から所定の書式でアクセスすることができます。

snowflake-sqlalchemy では、その辺りの処理は 2022年5月時点では実装されていないようだったので、 snowflake-sqlalchemy-json でいくつかを実装してみました。

現時点では読み込みのみしかサポートしておらず、機能も限定的ですが、それなりに使えそうに思うので、このライブラリを使った例を挙げておきます。
ここでは、 Snowflake のアカウント作成時に例として提供されていた SNOWFLAKE_SAMPLE_DATA.WEATHER.DAILY_14_TOTAL テーブルに含まれているデータを読み込む際の例について挙げます。データのサンプルは、末尾に挙げておきます。

VARIANT 型を SQLAlchemy の JSON にマッピングしアクセス

上記の query で、 V カラムに入っているデータの中の city キーで取得できるオブジェクト内の要素にアクセスすることができます。
基本的に、 Python の dict と同様のアクセス方法になり、直感的に書けているのが分かると思います。

lateralflatten を用いたアクセス

Snowflake の VARIANT 型へのアクセスで重要な概念である lateral 結合を用いたアクセスについては、下記のように記述できます。
なお、 lateral 結合については結局 LATERAL とは何者なのか?の記事が分かりやすいように思います。

flatten は、あくまで通常の関数なので、 SQLAlchemy で関数を呼び出す際の形式である func.flatten(xxx) として記載します。
また、 lateral は SQLAlchemy がサポートしているので、公式のチュートリアルの通りに呼び出します。
lateral() で返されるテーブルの value カラムにアクセスすると、 flatten で展開された要素にアクセスすることができます。

なお、上記は FROM 句に FROM DAILY_14_TOTAL, LATERAL FLATTEN(xxx) のように lateral を記述する方法ですが、この書き方では Warning が出るかもしれません。
その場合、以下のように、 JOIN 句として独立させて書くこともできます。
私はこちらの方が分かりやすいように思います。

用いたデータの形式

最後に、 DAILY_14_TOTAL テーブルの V カラムに入っている JSON データの形式を挙げておきます。

以上のようにすることで、 Snowflake の VARIANT 型へのアクセスを SQLAlchemy を用いて記載することができるようになります。

2022年3月7日月曜日

Azure DevOps 向けの拡張機能 PublishMarkdownReports

Azure DevOps 向けの拡張機能 PublishMarkdownReports

Microsoft の統合開発環境である Azure DevOps 向けに、拡張機能 Publish Markdown Reports を作成したので、その紹介です。

拡張機能自体の作り方は、 Web 拡張機能の開発のページに詳しい方法が載っているので、こちらを参照してみてください。

やりたかったこと

ソースコードから自動生成した Markdown の API 仕様書を、 Azure DevOps の CI/CD である Azure Pipelines のページで閲覧できるようにしたい、というのがやりたかったことでした。
Azure Pipeline から Azure DevOps Wiki を更新する方法もあるのですが、こちらの記事のように Wiki 用の Git Repository を自動で更新する方法では、「同じぐらいのタイミングで CI/CD が走ったとき」や「CI/CD タスクが Re-queue され、古いコミットに対するタスクが後で実行されたとき」に、古い内容で上書きしてしまわないような仕組みが必要になります。

今回は、そういったことを考慮しなくてすむように、 Publish Markdown Reports では Azure Pipelines のビルド結果の画面に任意の Markdown ドキュメントを表示できるようにすることにしました。
このようにすることで、別の Wiki 用の Git Repository は不要になり、 CI/CD タスクの実行順序などを考えなくてもよくなります。
ただし、ビルド結果は設定によっては古い結果が削除されるので、正式なドキュメントとして残す場合は別途対応が必要です。

完成した表示画面は下記になります。
screenshot

Publish Markdown Reports について

まずは、 Publish Markdown Reports の概要について説明しようと思います。
なお、ソースコード全体は GitHub に置いてあります。

表示用 Markdown ファイルのアップロード

この拡張機能では、 Markdown として表示するファイルをビルドタスクの中でアップロードします。
下記のような YAML の記述を Azure Pipelines に加えることで、指定されたディレクトリのファイルをアップロードします。

上記の場合、ソースコードのディレクトリの下の markdown ディレクトリの下にあるファイルすべてをアップロードします。
また、既定のページとして main_page.md を指定しています。このファイルは contentPath で指定したディレクトリからの相対パスで指定します。
headingId は、 Markdown の # 見出し のような章のタイトル (<h1> などのタグで囲まれた部分) に対して、どのような ID を付与するかを指定します。
現時点では完全ではないのですが、 Python の Markdown の HTML 変換ライブラリである Python-Markdown や、 Doxygen の結果を Markdown に変換する Doxybook2 の ID のつけ方に対応しています。
なお、 Pydoc-Markdown などのツールでは、 <a> タグでリンク先の場所が定義されているので、 headingId の指定は不要です。

内部的には、これらのファイルはビルド中に Attachment として、ビルド結果に対する添付ファイルの形でアップロードされています。
このファイルは API でしか取得できないもので、ビルド生成物として登録する Artifact とは異なるものです。

Markdown ファイルの表示

この拡張機能では、アップロードされた Markdown ファイルを Marked で HTML 変換して表示しています。

ただし、 Azure DevOps の拡張機能の仕組み上、少し面倒ですが、下記のように表示を実現しています。

  1. 拡張機能に含まれる frame.html をビルド結果のタブ内に表示
  2. frame.html から Azure DevOps の SDK を用いて Attachment として保存されている Markdown を JavaScript で取得
  3. 取得した Markdown ファイルを Marked ライブラリを用いて HTML に変換
    その際に、リンクを処理する際には、 frame.html?page=/link-to-page.md のように、 frame.html に対して page パラメータとして Markdown のファイルのパスを指定するようにします。
    また、画像を表示するために、画像に対するファイル名に対して Attachment を取得し、得られたデータを Blob 変換した上で、 URL.createObjectURL で生成した URL に差し替えます。
  4. 生成された HTML をタブに表示

高速化

上述した方法では、表示されるまでに少しタイムラグが発生します。これは、 Azure DevOps の SDK の性質上、ロード時の初期化に 1~2 秒かかることが多いためです。
これを解消するために、少し高速化を行います。

上述した frame.html をラップする main.html を用意し、 frame.htmlmain.html の中で iframe として表示するようにします。

その上で、それぞれのファイルで以下のような処理を行うことで、 Azure DevOps の SDK の初期化処理がページ遷移のたびに動作しないようにしています。

main.html
  • Azure DevOps の SDK である VSS.SDK.js をロードし、初期化する。
  • onmessage ハンドラで frame.html からのメッセージを待ち受け、メッセージに応じて Attachment をダウンロードし、 frame.html に送る。
frame.html
  • 表示すべき Markdown ファイルを main.html に要望する。
  • main.html から送り返されたデータを処理し、 Marked ライブラリで HTML に変換する。

このようにすることで、画面が切り替わる際は frame.html だけがロードされ、 main.html 側は破棄されないため、 VSS.SDK.js による初期化処理は動作しないことになります。
そのため、 Markdown ファイルの切り替えによる画面遷移時には、 Markdown ファイルや画像のロードのみで動作が完結し、高速な動作が可能となります。

このようにして、それなりに実用に耐えうる拡張機能ができました。

2022年2月7日月曜日

Bash 向けの tcsh 同様な history-search

Bash 向けの tcsh 同様な history-search

Bash のシェルスクリプトの勉強がてら、 tcsh 同様な history-search-backward のようなものを実装してみました。

Bash の history-search-backward

Bash には、元々 history-search-backward というコマンドが用意されています。
しかし、このコマンドは tcsh のコマンドとは異なり、カーソル位置が行の末尾に移動しません。
私は Solaris の tcsh で Unix 系の環境に初めて触れたこともあり、 tcsh のようにカーソルが末尾に移動するという挙動の方が馴染みがあるので、そのようなコマンドを作ってみようと思いました。

なお、あくまで私が Bash のシェルスクリプトの練習をするというのも重要な目的の一つなので、例えば zsh をなど他の解は、今回は気にしていません。

ポイント

今回は、 Bash のシェルスクリプトの勉強を兼ねていたので、以下を守ることにしました。

  • Bash 以外での動作は気にしない。
  • Bash の組み込みコマンド以外は使わない。
  • Windows 版の Git に含まれている Bash でも動作させる。

コードの概要

コードは GitHub に置いてあります。

おおまかな流れとしては、以下のようになっています。

  1. fc コマンドでコマンド履歴の一覧を取ってくる。
  2. 一行ずつ read コマンドで読み込む。
  3. 特定の文字列で始まるコマンドを抜き出し、それを READLINE_LINE, READLINE_POINT に設定することで、ターミナルの文字列を置換する。

coproc の使い方

fc コマンドを実行し、その結果を read コマンドで処理する際に、 coproc コマンドを使うことにしました。
Bash の while でパイプから読み込んだテキストを一行ずつ処理をする場合、 Qiita の記事にあるように、 Process Substitution などを使えばよいようですが、 Windows 版の Git に含まれている Bash などでは実行することができません。
そのため、今回はこの方法は諦め、 coproc を使って読み込むことにしました。

概要

coproc に関連する部分をシンプルにして抜き出すと以下のようになっています。

coprocfc を実行するプロセスを作り、その出力を whileread で一行ずつ処理しています。

ちなみに、よく言われるように下記では while 内がサブシェルで実行されるので、 HISTORY_ARRAY に期待した通りに値が保存されません。

また、上述したように、下記のように Process Substitution を使う方法では、私の手元の Windows の bash.exe では動作しませんでした。

ジョブ情報の出力の抑制

そのまま使ってしまうと、[1] 1859[1]+ Done のような、ジョブの情報が出力されてしまうので、それらを抑制する必要がありました。

開始時は以下のようにします。
coproc の標準エラー出力を捨てればよいのですが、波括弧で囲ってリダイレクトすると指定しやすいです。

終了時の出力は、 coproc 呼び出し後に以下の disown を呼んでおくことで、抑制することができます。

coproc からの読み込み

coproc で返されるファイルディスクリプタから read コマンドでテキストを読み込むには、 -u オプションを使うことで実現できます。
この辺りは Web 上で情報がたくさん見つかるので省略します。

一方で、ただ coproc FC_FD { fc -lnr -${HISTSIZE} ; } として実行した coproc を read -u ${FC_FD[0]} のように読むだけでは、場合によってはうまくいきません。

Stack Overflow に書かれているように、 Bash の coproc では、 coproc で実行したプロセスが先に完了してしまうと、プロセスからの出力をすべて処理する前に出力が読めなくなります。

そのため、 coproc のプロセスの出力を読み終わるまで、プロセスが終了しないように工夫する必要があります。
また、 coproc のプロセスの出力をバッファリングせずに出力しきるために、以下のようにします。

このように、 coproc の中で read -s で入力を待ち、 exec 1>&- で出力のファイルディスクリプタを閉じることで出力を確実に処理させ、 coproc での出力をすべて処理できるようにします。

というような感じで、 Bash の組み込みコマンドのみで tcsh のような history-search-backward を実装することができました。
私はこれまでにほとんどシェルスクリプトを書く機会がなかったのですが、 Bash 4 以降は coproc や連想配列もあり、それなりにいろいろなことができるようです。

2022年1月28日金曜日

API による Microsoft Teams のステータスメッセージの取得/設定

API による Microsoft Teams のステータスメッセージの取得/設定

Microsoft Teams では、ステータスメッセージを設定することができます。

今回は、これをスクリプト経由で設定する方法について記載しようと思います。

Graph API でできる範囲

2022年1月現在、 Teams の Microsoft Graph REST API では、 ユーザーのプレゼンス情報の取得/設定ができます。
この API では、 availability, activity の取得/設定がサポートされており、 Python スクリプトなどで自由に設定することができます。

しかし、 beta 版も含め、ステータスメッセージの取得/設定は現状サポートされていないようです。

Teams アプリ、Teams Web アプリと同様の方法でのアクセス

そこで、公開されていない API を用いてアクセスすることになります。

結論としては、下記の情報で OAuth2 のアクセストークンを取得し、後述する URL に POST, PUT することで、ステータスメッセージの取得/設定することができます。
これは、原則として、 Teams アプリや Teams Web アプリが行っている方法と同様と思われます。

OAuth2 情報

OAuth2 のアクセストークンは、下記の情報で取得を行います。

Client ID
1fec8e78-bce4-4aaf-ab1b-5451cc387264
Client Secret
Public Client なので不要
Redirect URI
https://login.microsoftonline.com/common/oauth2/nativeclient
Scope
https://api.spaces.skype.com//.default

なお、上記の Client ID は、 Teams のデスクトップアプリの ID になっています。
また、 Client Secret, Redirect URI は、いずれも Public Client の設定になっています。
Scope の .default の前にスラッシュが二つありますが、 Microsoft のページによると、これで正しいようです。
(ただ、スラッシュが一つでも問題なく動いてはいるようでした。)

ステータスメッセージの取得

ステータスメッセージの取得は、下記の URL に JSON データを POST することで実現できます。

送信するデータの中の "mri" に設定している GUID は、 Graph API で取得できるユーザー情報の中の id のことです。
8:orgidPublicDirectoryParticipantPrefix と呼ばれるもののようで、固定値でよいようです。

URL
https://presence.teams.microsoft.com/v1/presence/getpresence/
HTTP Method
POST
Body として送信する JSON データ

上記を送ると、指定したユーザー分のステータスメッセージを含むユーザー情報が返ってきます。

ステータスメッセージの設定

自分のステータスメッセージの設定は、下記の URL に JSON データを PUT することで実現できます。

メッセージと有効期限を指定し、設定することができます。

URL
https://presence.teams.microsoft.com/v1/me/publishnote
HTTP Method
PUT
Body として送信する JSON データ

なお、 "message" の末尾に <pinnednote></pinnednote> を追加すると、「他のユーザーが自分にメッセージを送るときに表示する」にチェックした状態になるようです。

Python での実装例

最後に、 Python を用いてステータスメッセージを設定する例を書いておこうと思います。

Windows 上で簡易的に OAuth2 を扱えるようにした msal-interactive-token-acquirer と、 HTTP アクセスで広く使われている requests を用いた場合、以下のようになります。

なお、これは個人の Microsoft アカウントではなく、組織の Microsoft アカウントで行う必要があります。
個人で試す場合は、 Microsoft 365 開発者プログラムなどを活用するとよいと思います。

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 を出してみようかと思います。