meowの覚え書き

write to think, create to understand

『PyData.Tokyo Meetup #23 MLOps〜AIを社会に届ける技術』 聴講メモ

f:id:meow_memow:20210708213552j:plain

目次

5月にPyData.Tokyo Meetup #23 MLOps〜AIを社会に届ける技術がオンラインで開催されました。
https://pydatatokyo.connpass.com/event/210654/

PyData.TokyoがテーマにMLOpsを扱うのは2年ぶり1で、個人的に期待が高かったです。

今回はサイバーエージェント(CA社)のMLOpsにまつわるご発表2件でした。 トピックはML実験管理基盤、プロダクト化にあたってのパフォーマンス向上やトラブルシューティングでした。
Ops関係の直接的な話題はなかったものの、現場ならではのノウハウが豊富にふくまれていました。

私は勉強のために、スピーカーごとにメモ(個人的所感含む)を取っていました。 スライドや動画が公開されているので、そちらをご覧いただいた方がいいというのは百も承知の上、以下で共有します。


スポンサードリンク

1人目『CyberAgent AI Labを支えるCloud実験環境』

スピーカー: 岩崎 祐貴 (Yuki Iwasaki)

概要: CyberAgent AI Labでは、Computer VisionNLP・経済学・ハイパーパラメータ最適化・Human Computer Interactionなど分野の異なるResearcherが広く在籍しています。このような分野違いのデータや実験を一元管理するために、AI Labで開発・運用しているデータローダーやモデルサーバーを通じて、高速かつパワフルなCloud実験環境をご紹介します。

スライド: CyberAgent AI Labを支えるCloud実験環境 - Speaker Deck https://speakerdeck.com/chck/cyberagent-ai-labwozhi-erucloudshi-yan-huan-jing

動画: https://www.youtube.com/watch?v=E2s5NP6B8cs

本題

  • 所属組織であるAI Lab
    • 人数35人。Researcher 31名、Research Engineer 4名(岩崎さんはこちら)。リサーチャーが多い。
    • OKR: 学会発表がメイン。
    • プロダクト側ではないので、顧客データを貯めていない。しかし実験で使用したい時はプロダクト側と連携する。
  • AI Labのエコシステム
    1. 課題、仮説、アイディア
    2. データ選定し、収集する
    3. 前処理
    4. 解きたいタスクに対するモデリング
    5. 評価(実験結果のレポート作成)
    6. 論文、プロダクション化
  • 研究生産性にまつわる課題
    • 使うライブラリは、リサーチャーや分野によってばらつく。
      • → そこで岩崎さんはR&Dを加速させるために、実験の共通部分を洗い出して実験サポートツールを作った。
  • 今回の報告内容は、エコシステムにおけるサポートツール3つの紹介
    1. (ideaに関するものは無し)
    2. データ収集支援→ ailab-datasets
    3. (前処理: numpy, tfrecord, pandas, beam)
    4. モデリング支援→ ailab-model-zoo
    5. 実験支援→ ailab-mlflow
    6. (論文: overleaf, GCP, AWS)

データ選択: ailab-datasets

  • 前提知識: tensorflow-datasets
    • publicなデータセットを一元管理してくれるOSSのdata loader
      • データセット名 を指定し、ロードするだけですぐに使える。
        • 226のデータセットが提供されている。mnistとかの軽いものだけでなく Celeb AやMS COCOなどの大容量のものもカバーしている
        • 画像だけでなくテキストや音声データもカバーしている
    • 分析データを扱うためのインタフェースを統一
      • tensorflowとついているが、numpy, pandas形式で出力できる。pytorch派でも使える。
        • ただしdfはiterableではないので、メモリに展開されてしまうことに注意。
    • オリジナルのデータセットの定義方法
      • tfds cliという専用のCLIがあるので、それ経由でpythonのテンプレートを生成する。
        • テンプレート
          • メタデータにデータの型やdescription
            • データ参照時に型チェック可能
          • _info()に、データ本体ののURLや、train/val/test といったデータセットの分割方法を定義
          • _generate_example()
            • 参照したデータに対する処理を書くジェネレータ関数
        • データセット用のテストケースも書くことができる。
    • 動画のような入力データサイズが巨大な場合はapache beamの形式で並列分散も処理可能
      • もとのテンプレートをちょっとapache beam用に変えるだけで動かすことができる。
      • apache beamのおかげでクラウドでスケールしやすい
  • private(社内)なデータを扱う上での課題
    1. データの管理にルールがない
      • CA社のプロダクトは並列に大量に立ち上がるため、データ管理が難しい
        • プロダクトサイドのデータ集約基盤はリサーチャー向けではない
      • → リサーチャーごとのオレオレJupyterlab上で実験コードやデータを管理している
    2. リーダビリティの低いSQL、冪等性の無い前処理のため、リサーチャー間の再現が困難
    3. privateデータは使うのにドメイン知識が必要で、プロジェクト新規参入者の学習コストが高い
  • この課題を解決するのがailab-datasetsという社内ツール
    • 説明: private データを前処理ごと管理できるDataloader。前述のtensorflow-datasetsのラッパー
    • 利点
      • Data追加が簡単
      • プロダクトやタスクごとにデータをバージョニングできる
      • apache beamのランナーを使うことにより、高速に前処理が可能
    • post-process(共通の前処理の後の処理)を選択可能
      • 特徴としては、TensorflowTransform(TFデータのapache beam実装)が選択可能。前処理-後処理で分散処理のパイプを組める。

モデリング部分でのサポート: ailab-model-zoo

  • privateなmodelにおける課題
    • 異なるタスクで似たようなモデルの再発明が起きている
    • リサーチャー間でpretrain済みモデルのシェアしたい場合がある
      • 社内データでpretrainした重みがほしい。
      • しかし、誰がどんなモデルを作成しているか把握しづらい
  • ailab-model-zoo(WIP)
    • AI hub(GCPのマネジド版tensorflow-hub)を使用
    • 次の3つをprivateに管理可能: kubeflowパイプライン, jupyter notebook, 学習済みモデル
    • 名にtensorflowと関しているが、オブジェクトストレージとしても使える
      • メタデータやタグを打っておけば、所定のリソースをテキスト検索できて、探しやすい
    • (個人メモ; 単純にAI hub使うのと何が異なるのだろうか)

実験管理部分でのサポート: ailab-mlflow

  • 実験管理ツールにおける課題
    • MLflowはサーバ構築が面倒。セキュリティのことを考える必要がある。
    • SaaS(Nepture.ai, Comet.mlなど)は無料枠を越えると高コスト
  • MLflowでの実験管理
    • 1つの共用のMLflowサーバをたててチームで利用という案はやりたくない
      • アクセス制御ができない
      • 個人の実験管理を制限してしまう
    • ailab-mlflow
      • AILab内で共通で使えるMLflow ClusterをGCP上にGKEで構築
        • ユーザごとにMLflowのPodが立っている。podのurlをユーザに教える。urlにアクセスしたリサーチャーからは個人のMLflowのように見える。
        • アクセス制御はgoogleアカウント単位(Cloud IAP)。共同研究者はgmailをもっていればokなので楽。
        • MLflowのバックエンドDBやオブジェクトストレージは共通のものを指定
          • 区分けはテーブルやprefixで行い、ユーザごとの保存場所を用意
      • 実験バッチからどう、MLflowサーバへログを飛ばすのか
        • リサーチャーがMLflowの使用申請をailab-mlflow管理者に出す。管理者は新しいMLflow podを立ち上げる。
        • リサーチャーは予め持っているクライアントidをキーに、MLflowへアクセスするためのOAuth Tokenを発行する
        • 環境変数MLFLOW_TRACKING_TOKENにTokenをセットすると、MLflowクライアントはこれをつかって、MLFlowサーバにアクセスしてくれる。
        • あとは、普通のMLflowの使い方と一緒
      • 構築方法はAIlabのテックブログにて公開予定

おわりに

  • 今後の課題
    • 論文執筆後のコード、デモページのジェネレータの開発
  • ツールをリサーチャーにどう布教させるか
    • ツールのチュートリアルを開催
    • 地道に一人ひとり使用者を増やしていく
    • 共著で入るときに、こっそりツールが使える環境を用意してしまう
  • 研究の仕組み全体を支援するpipelineよりも、要所要所で使えるモジュールを開発をし、リサーチャーに使うモジュールを取捨選択してもらったほうがよい

質疑

  • privateなデータセットの認証周りはどうしているのか
    • private githubで管理しているため、github上で行っている
    • BigQueryからロードする場合は、BQのread権限も必要
  • ailab-model-zooをリサーチャーに使ってもらう時のインセンティブはあるか
    • ない。強いて言うならば、ailab-model-zooで実験負荷を軽減できたり、モデルを公開したことで思わぬバグを発見できたという成功体験をしてもらうこと。
  • ailab-mlflowのインフラ管理は誰が行っているのか
    • 岩崎さん+もう1名
  • ailabの研究成果をCA社のプロダクト内で使用することはあるか。使用するならば、どこの部隊がMLコードを運用管理するか。
    • 使用する。プロダクト側が実運用を担当する。理由は、AIlabの目的が研究なので、プロダクトの目的(売上を伸ばす)とギャップがあるため。

個人メモ

  • 基盤の設計。あまり意識されない部分にしっかり生産性を上げる機能を提供しているという印象でした。
    • 環境と成果の切り分け
      • 実験は個人専用の環境で、成果は全員が使えるようになっていて、研究者のニーズが考慮されています。
    • 計算資源のスケールが容易
    • 共同研究に対応した基盤
      • 外部の人がnotebookにアクセスできるようにしているのは便利。
  • 実験ツールの標準化 と 研究の自由 のトレードオフ
    • 岩崎さんは以前会社ブログやPyConJPで『小さく始めて大きく育てるMLOps2020』 https://cyberagent.ai/blog/research/12898/という発表をしていましたが、私はこれを読んだ当時これに沿って社内の実験手順を統一しているのかと思ってました。しかし、実際には研究者の個人のやりかたは自由ということでした。
    • 社内ツールを開発しても、使ってもらえないということは私にもあるので、いかに相手に使用を強制させずに普及させていくかは参考になりました。
  • プロダクト側への実験成果物(モデルなど)の引き継ぎはどのように行っているのだろう
    • 関数I/Oは共通化できるものの、内部ロジックはリサーチャーが実装しているはず。そこはおそらく実験のコードのままだと思うのでプロダクト側にわたすとき、どう品質を担保するのか、誰が今後メンテを行うのかというのは気になりました。

スポンサードリンク


2人目 『サイバーエージェントにおけるMLOpsに関する取り組み

スピーカー: 芝田 将(Masashi Shibata)

概要: サイバーエージェント社内の3つのプロダクトのMLシステム構成を紹介しながら、そのプロダクトで行ってきたMLOps周りの取り組みをお話します。

スライド https://www.slideshare.net/c-bata/mlops-248545368

動画 https://www.youtube.com/watch?v=oluIfLSsq8w

芝田さんは社内の(高度な)技術サポートの仕事を行っており、今回は社内プロダクトの改良やトラブルシューティングに関して3点報告されました。

  1. DynalystにおけるMLモデルの推論の高速化
  2. AirTrackにおけるMLモデル学習の高速化
  3. AI Messanger Voicebotにおいてシステムが固まる問題の調査

1. Dynalystにおける機械学習

  • Dynalyst:CA社のプロダクト。スマートフォンアプリに特化したダイナミックリターゲティング広告をしてくれるDemand Side Platform。
    • スマホユーザの行動ログを元に、提示する広告を決める。ユーザに広告を提示した時、コンバージョン(広告主の期待する行動をとること; 商品を購買、アプリのインストールなど)する確率を予測するのにMLを使っている。
    • 使用しているCVR予測アルゴリズム: field-aware Factorization Machine
  • C++実装でCLIのみを提供している。 そのままだとPythonで実行できない。サードパーティpythonバインディングもあるが、自前でpythonバインディングを実装した。
    • 自前のpythonバインディング 理由1
      • libffmに予測性能を上げるためのパッチを当てたいため。具体的には損失関数をカスタマイズしたかった。
      • カスタマイズすると、サードパーティpythonバインディングが想定するlibffmと違ってしまう。そのため、自前でバインディングを実装する必要がある。
    • 自前のpythonバインディング 理由2
      • 予測速度の高速化したかった。
      • ユーザに提示する広告をリアルタイムに決める。リアルタイム性が求められる。広告の表示のレスポンスタイム(100ms以内)。

以下では高速化したlibffmのpythonバインディングを開発した際のtipsを説明する。

Cythonによる推論サーバの高速化

  • Cython
    • Pythonライクなコードを書くとCの最適化したコードを生成してくれるプログラミング言語
      • Pythonのスーパセットなので、CythonのコードとPythonのコードが混ざっていてもいい
    • 変数に明示的にデータ型を与えるほど高速化する
      • 手書きのCコードより高速になることも珍しくないとのこと
      • コードアノテーションが用意されており、ハイライトされている場所(Python/C APIを読んでしまってる遅い箇所)をCythonのコードに書き直すことで処理が速くなっていく。
    • C/C++Pythonのインタフェースとしても利用可能で、バインディングを作る際にも使える
  • CythonにおけるGILの解放
    • C言語のコード部分(Python/C APIを読んでいない部分)はGILの解放が可能で、マルチスレッドの恩恵を受けられる。
  • 安全性を犠牲にした高速化もおこなっている
    • CとPythonで異なる部分に対してPython/C APIがチェックが呼ばれてしまう
      • 例えばゼロ除算チェック、配列に負値のインデックス-1を与えることなど
    • 高速化のために、Compiler directivesの設定でチェックを切る
  • 推論処理をCythonに直した効果
    • 推論時間が改善前の10%に短縮
    • 推論サーバの全体のレスポンスが元の60%に短縮
    • スループットが1.35倍なったため、稼働させておくサーバを減らしコスト削減にもつながる。

LIBFFMバインディングの実装 参照カウントとNumPy C-API

Pythonコードの高速化をしたいだけならば、Cythonに書き直せばOK。これはお手軽。しかし、Cythonを使ってPythonバインディングを作るにはCython, C/C++の深い理解が必要。その一例としてメモリ管理にまつわる話をする。

  • Cライブラリをラップするには
    • Cython側から参照したいCの関数や構造体の宣言をする
    • 関数の引数に与えるオブジェクトを生成する
      • やってることは構造体のメモリ領域の確保
    • 関数の呼び出し
    • 使用したメモリ領域の解放
  • Pythonと連動したメモリ管理がしたい
    • Cで確保したメモリ領域をPythonで触りたいというケースがある。
      • libffmの例だと、メモリ領域内にモデルの重みがあり、その重みで推論がしたい。
    • その場合、Cでメモリ領域確保→Cythonのラッパーがメモリ領域をラップしPythonで参照できるようにするnumpyオブジェクトにする
    • 問題なのは、numpyオブジェクトを破棄しても、cで定義したメモリ領域は自動的には解放されない(Cにはガーベージコレクタがない)
    • したがって自分で開放する必要があるが面倒。
    • numpyオブジェクトを破棄したら連動してメモリ領域が解放されるようにしたい。
  • CPythonのメモリ管理機構は参照カウント
    • 参照カウントが0になるとPythonオブジェクトのメモリ領域は解放される
  • どうやって連動させるか
    • ラップしたNumpy配列に対してBaseObjectを指定する
      • このBaseObjectは、Numpy配列の実体となるCの配列のポインタを所有するオブジェクト。雛形のクラスには__dealloc__メソッドを定義し、このクラスのオブジェクトが破棄されたときは、実体となるCの配列のメモリ領域を解放する処理を書く。
    • これにより、Numpy配列が破棄されると、連動してBaseObjectも破棄され、BaseObjectが破棄されるときにCの領域が解放される。
  • 以上のように、Cで確保したメモリ領域をPythonで触れるようにするのは一苦労。pythonで操作する必要がない (Cython内でメモリ領域の操作が閉じている場合は型付きメモリビューを使うことを推奨する。

2. AirTrackの事例紹介 OptunaとMLflowを使ったハイパーパラメータ最適化の転移学習

  • AirTrackとは
    • 位置情報を活用した来店計測や広告配信を行うCA社製SDK
    • ユーザやエリアの属性推定、店舗への来店予測に機械学習技術を利用
  • MLモデル学習のパイプライン
    • メトリクス、アーティファクトの管理にMLflowを利用
    • ハイパーパラメータの探索にはOptunaを使用
      • 学習バッチを実行するごとに毎回ハイパパラメータも探索しているとのこと
      • この時、ハイパラ探索の工夫をしている。それについて説明する。

Optunaの基礎知識とWarm Starting CMA-ES

  • Optunaの強み
    • 使える探索アルゴリズムが豊富
      • ただし、芝田さんの印象では殆どのユーザはデフォルトのアルゴリズムしか使っていないと思っている
  • Optunaのデフォルトのアルゴリズム: 単変量TPE
    • 欠点: ハイパラ間の相関関係を考慮しない
  • Optunaで使える、相関関係を考慮した探索アルゴリズム
    • 多変量TPE
    • CMA Evolution Strategy(CMA-ES)
      • AirTrackのモデル学習で使用
  • Warm Starting CMA-ES でハイパラ探索を高速化

3. AI Messanger Voicebotの事例紹介 軽量スレッドとWebSocket

  • AI Messanger Voicebot
    • AIによる電話自動応対サービス
  • システム構成
    • twillioを経由して、websocketで音声データをやり取り
    • エンドポイントとなる部分(WebSocketサーバ)が送られてきた音声を
      1. Google CloudのSpeech-to-Textで音声認識し、
      2. 音声認識結果を元に対話エンジンが応答内容を生成し、
      3. Google Cloud Text-to-Speechで応答内容を音声合成する
  • トラブル
    • 開発初期、WebSocketサーバ(Flaskで実装)が特定の条件下で固まって動かなくなるという相談が芝田さんに寄せられたので原因を調査した

WSGIと軽量スレッド

  • PythonでWebsocketを扱うことについて
    • WSGIに則ったデータのやり取りだと、そのままでは不可能。
    • WSGIであってもwebsocketで通信するには何らかの工夫が必要
      • 工夫の例 WSGIのcallableなオブジェクトにWebsocketオブジェクトをわたす
        • Flask-socketsを用いると、このようにしてWebsocket通信ができるようになる。
  • 工夫してWebsockets通信を行おうとする方式の問題点(WSGIの制限について)
    • WSGIアプリケーションを呼び出したスレッドは、そのアプリケーションの処理が終わるまで別の処理を行えないこと。Websocketのコネクションが張られているうちは処理を離せない。
    • 対応策としてはマルチスレッドが考えられるが、OSが管理するスレッド(threading.Thread)を使うのは避けたい
      • 理由はパフォーマンスが悪いため
        1. コンテキストスイッチが重たい
          • スレッドの切り替わりのタイミングで、レジスタの内容をメモリにダンプし別のスレッドの状態をメモリからレジスタに読み出して処理を再開する。切り替える度にこれが発生してしまう。
        2. スレッドのスタックサイズが大きく(2MBなど)、1つスレッドを作るだけでもコストがかかる
      • したがって、user landで動作するスレッドのようなもの(軽量スレッド)を使ってマルチスレッドを実現する
        • (個人メモ: アプリケーション側で生成した仮想的なスレッドだから生成が早いということだろうか)
    • 先述のFlask-socketsはGevent-websocketを内部で使用しており、これが軽量スレッドを用いたマルチスレッド処理を実現してくれる

Gevent-websocketの仕組み

  • Geventのmonkeyというモジュール
    • monkey.patch_all()を 呼び出すとPythonの標準ライブラリに対してモンキーパッチを当てる。通常ならばスレッドをブロックするような操作をGevent用のものに置き換える。置き換えると軽量スレッドベースで動作させることができる。
    • この状態でthreading.Thread(本来は、OS経由でのスレッド生成)を呼び出すと、内部的にはGeventの軽量スレッドを生成する操作(gevent.Greenlet)に置き換わっており、シングルスレッドでありながら仮想的に並列処理を行うことができる。
    • Flask-socketsはGevent-websocketを利用して1つのスレッドで複数のWebSocketのコネクションをさばいている
  • ただし、monkey.patch_all()が置換できないものがコードの中に含まれていて、そこを呼び出してしまうとブロックが発生。 Gevent(イベント駆動)のイベントループがそこで止まってしまうためプログラム全体が固まってしまう
    • 置換できないコードの例としては、サードパーティが独自実装した通信部分など。
    • CA社の例だと、GCPのText-to-SpeechAPIのSDKが内部でgRPC双方向ストリーミングを呼び出してブロックが発生
      • gRPC通信部分はC++実装のため、Geventが置き換えられなかった。
  • 【余談】ASGI(Asynchronous Server Gateway Interface)
    • 上の問題は同期処理を想定しているWSGIで無理やりWebsocket通信と非同期処理をやろうとしたことが起因
    • WebSocketもサポートできるような非同期インタフェースの仕様を策定(中)

質疑

  • Cythonのコードアノテーションのハイライト(Python/C API呼び出し)部分はどうやっているのか
    • コードアノテーションはCythonが提供している機能。コンパイルコマンドにオプションをつけるとコードハイライトしたhtmlファイルを吐いてくれる。
  • メモリ確保したい時、PyMem_Mallocで確保するのとC++のスマートポインタで確保するのでは何が違うか
    • メモリをどこで管理したいかの違い。PyMem_MallocはCythonでメモリ管理したい場合に使う。
  • (コメント) Cythonを使った実装のパートについて、話は理解できるけど自分で実装できる気がしない
    • CA社のMLエンジニアもCythonに苦手意識がある。Cythonは実際難しい。デバッグ時オブジェクトファイルにデバッグコード埋め込んでgdb起動しないといけなかったり、ハマると大変。そのあたりは頑張ってください。
  • Optunaに関して、ハイパラ探索時、過去のbestハイパラより悪くなることはないか。
    • 悪くなる場合がある。それを見越して、探索パラメータ候補に過去のbestハイパラを追加しておくことで精度の下界を保証する。
  • Warm Starting CMA-ESによって、どれだけ学習効率が上がったか
    • オフライン性能検証の結果では、単変量TPEに比べて評価回数が半減した
      • 単変量TPEだと100回評価してたどり着けるハイパパラメータがあるとして、Warm Starting CMA-ESだと50回でたどり着ける。
  • SageMakerではなくMLflowを使っている理由は
    • 自分はサポートとしてぱっと入っただけで、プロダクトの設計には関わっていない。したがってMLflowを選定した細かい背景はわからない。
    • 他のプロダクトでシステム設計のディスカッションをした時の話になるが、MLflowがむかないケースが結構あると思っている。メトリクス集めたい時、MLflowのTracking APIを使うせいで柔軟性が減ることがある。
  • AI Messanger Voicebotに関してpythonで非同期処理を扱うのは大変そうだが、何でPythonで実装したのか。
    • あくまで推察だが、対話エンジンがPythonで実装されている手前、Pythonで統一しないとシステムの複雑さがあがるからというのがあったのではないかと思う。
      • また、Webscocket通信周りを実装していたのもMLエンジニアだったというのもあるかもしれない。
    • 自分がやるとすれば、Websocketの部分はPythonより得意な言語、例えばGo langを使うだろう
      • Goにはgoroutinesという優れた軽量スレッドの実装がある

個人メモ

  • Dynalystの事例紹介に関して
    • 速度要件の求められるアドテクで、Pythonを使うことはかなりディスアドバンテージのはず。そこを技術でカバーしたのはすごい。
    • Pythonバインディング書ける人は尊敬します2
  • AirTrackの事例紹介に関して
    • 岩崎さんの方はGCPを利用していたが、こちらはAWSを利用しているようだ。どういう基準でクラウドベンダを選定しているのだろう。
    • アーキテクチャを見ると、Client(エンドユーザ)がMLflow serverにリクエストを送っているように見える。推論サーバとしてMLflowサーバが使えるのだろうかと気になりました。
  • AI Messanger Voicebotの事例紹介に関して
    • 問題は複雑。
      • 強いエンジニアが高度な技術的課題を解決して現場を救ったという見方はできるものの、最下流で尻ぬぐいした感じもする。もう少し設計段階で芝田さんが加わっていた方がトラブル解決にかかった工数が少なかったかもしれない。
        • とはいえ、GeventとgRPCの相性が起因であると予見するのはさすがに難しいか。
      • そもそもの問題はPythonでプロダクトを開発しようとしたことだが、MLエンジニアがプロダクトを全部設計しようとすると技術選定がPython中心になってしまうのは仕方がない。
        • 別にMLエンジニアもスキル不足というわけではないだろう。MLエンジニアはGoも実装できるようにならないといけないかというと違うわけで。
      • 未来に同じようなことが起きたときにまた強いエンジニアが必要になってしまう事態をどう避けるか。結論出ない。MLOpsの責任境界問題の議論のケーススタディとしては面白い。
  • その他
    • 内容が高度だった。発表内容を理解するために、数多くの参考文献にあたる必要があったが、いい勉強になった。
    • 岩崎さんはAI Labはプロダクト部隊ではないと言っていたが、今回の芝田さんの取り組みはどのような位置づけなのだろう
    • スペルを大文字含めて正確に書いていて丁寧なスライド。
      • 私は面倒なので、pythonのように 表記の正確さを若干サボって書くのですが、スライドを見る限り、全て正確だったので驚きました。

おわりに

最後になりますが、改めてPyData.Tokyoの運営の皆様、会をアレンジしていただきありがとうございました。
おかげ様でまた1つ勉強になりました。

参考文献


スポンサードリンク


  1. MLOpsという名前が定着していなかった頃だったので、SysMLというカテゴリ名でした。

  2. 私はバインディングを書けないので過去にsubprocessでrubyバインディングを実行するというなんちゃってPythonラッパーを作っていました…

初心者向けOSINT CTF『Cyber Detective CTF』のwrite-up

f:id:meow_memow:20210430232146j:plain

目次

はじめに

Cyber Detective CTFはOSINT系のCTFです。すなわち、公開情報やWebサービスを駆使してフラグを見つけるタイプのCTFです。このCTFが開催されたのは2020年ですが、常設の状態で公開されているため、現在でも解くことができます。

私は下記のブログにて初心者向けのCTFとして紹介されていた記事を読んだことをきっかけに、2ヶ月間ほど取り組んでいました。

『オススメの初級者向けCTF - 好奇心の足跡』 https://tech.kusuwada.com/entry/2020/12/02/065100

「初心者向け」と紹介されているだけあって、OSINTという言葉自体知らなくても、自力で解けるため、解いていて楽しいです。そのため、可能な限り自力で解いていただきたいです。しかし、中にはどうあがいても解けない問題もあるかもしれません1。 したがって「どうしてもわからなかった時用」という立ち位置でwrite-upを公開します。

以下よりwrite-upの内容ですが、留意事項が1点あります。解答のテキストを画像化しています
理由はOSINT問はWeb検索を多用するためです。この記事が公開されたことで、答えがWeb検索でヒットしてしまうと、後にこのCTFを解く方々の楽しみが薄れてしまいます。
したがって、そうなることを回避するために画像にしています。そのためリンクが踏めません。また、画像は自分でスクリプトを書いて生成したため、60文字で改行しています。そのため変な折返しが発生しており読みにくい部分もあるかもしれません。ご不便をおかけしますが、予めご了承ください。


スポンサードリンク

write-up

Life Online

Twitterアカウントへのインテリジェンスを行ってフラグを取得する問題です。

f:id:meow_memow:20210430234356j:plain

f:id:meow_memow:20210430234402j:plain

Evidence Investigation

様々なOSINTを行う問題です。

f:id:meow_memow:20210430234417j:plain

f:id:meow_memow:20210430234423j:plain

General Knowledge

知識問題です。知っているか否かなのでサクサク説明します。

f:id:meow_memow:20210430234440j:plain

おわりに

Cyber Detective CTFのwrite-upを共有しました。
「初心者向け」として紹介されていた通り、知識ゼロからでも始められる点がよいですね。
これを足がかりに、その他のOSINT系CTFも解いていきたいです。

姉妹記事の紹介

同内容は、勉強会にてスライド形式で発表いたしました。上で解けていない問題も解く過程を紹介しています。

初心者向けOSINT CTF! Cyber Detective CTFに挑んだ!/cyber_detective_ctf - Speaker Deck


  1. 私も解けない問題が2問ありました。

PyTorchのnn.ConvTranspose2dに与えるパラメータは畳み込みから逆算して考える

f:id:meow_memow:20201229144751j:plain

(畳み込みの画像はこちらのもの)

この記事では、転置畳み込み層のPyTorch実装であるnn.ConvTranspose2dの出力サイズを自分が狙った通りに生成できるように、パラメータを与える知見を共有する。

画像を生成するDNNモデルにおいてアップサンプリングは不可欠な要素である。アップサンプリングを行う時は転置畳み込み層が用いられることが多い。PyTorchで2次元の転置畳み込み層を扱いたい時はnn.ConvTranspose2dを使う。
しかし、いざnn.ConvTranspose2dを使って自分で画像生成のモデリングをしようとした時、公式ドキュメントを見ても引数の値に何を設定すればわからなかった。正確に言うと、動かして処理の内容を推測しようとしたが、挙動が意味不明だった。例えば出力の特徴マップの幅を増やしたいのでpaddingを増やすと逆に幅が小さくなるなどである。
そこで、調べ物をして、どうパラメータを設定すれば想定サイズの特徴マップを生成できるか調査した。

結論としては「畳み込みの逆問題を解く」なのだが、そこに至るまでの過程を省略して理解するのはおそらく難しいので、下記で導入を交えながら説明をする。


スポンサードリンク

導入: 畳み込み層における逆伝播

調査の結果、パラメータを想定通りに設定するには、畳み込み層の逆伝播で何が行われているかの理解が必要であるという結論に至った。 その根拠としては、参考文献[2]によると、TensorFlowに関して、畳み込み層の逆伝播で転置畳み込み層が使われていることを述べている。PyTorchで同様の実装が行われているかはわからなかったが、転置畳み込み層クラスの引数の意味は共通している。

「畳み込み層の逆伝播で転置畳み込みが行われる」とはどういうことか、例を交えて説明する。

畳み込み層の順伝播

例えば下図のようなCNNの一部があったとして、ある畳み込み層Conv1に着目する。このConv1はConv0の特徴マップを入力として受け取る。また、Conv1の出力の特徴マップはConv2へ渡すとする。

f:id:meow_memow:20201229145612j:plain

順伝播ではご存知の通り、特徴マップXカーネルFで畳み込みの計算を行い、新たな特徴マップOを生成するという処理である。 このConv1では、Fが2x2の時、padding=0, stride=1とする。 簡単のためにchannelはinput,outputともに1とし、biasはなく、活性化関数もないとする。Xのサイズが3x3のとき、Oのサイズは2x2となる

f:id:meow_memow:20201229150108j:plain

畳み込み層の逆伝播

f:id:meow_memow:20201229150345j:plain

次に、この畳み込み層の逆伝播を考える。
逆伝播での勾配計算というと、重み(畳み込み層ではカーネルとバイアス)に対する勾配をまずイメージするかもしれない。 しかし、勾配は重みに対してだけでなく、入力の特徴マップに対しても求める必要がある。何故ならば、Conv1にとっての畳み込み層の入力の特徴マップの勾配\frac{\partial  L}{\partial X}は、1つ手前の畳み込み層Conv0にとっての局所的な誤差として逆伝播するからである。 この、入力の特徴マップの勾配\frac{\partial  L}{\partial X}の求め方を簡単に説明する。詳しくは文献[1],[2]を参照いただきたい。

上図のように、逆伝播時はConv2層から誤差\frac{\partial  L}{\partial  O}が入ってくる。

ここで、Xの最小要素であるピクセル1X _ iが誤差Lに及ぼした影響\frac{\partial  L}{\partial X _ i}は、多変数関数の連鎖律[5]より、

\frac{\partial  L}{\partial X _ i} = \sum_{k} \frac{\partial  L}{\partial  O _ k} \cdot \frac{\partial  O _ k}{\partial  X _ i}

と計算できる。

この内、右辺の\frac{\partial  L}{\partial  O _ k} は、Conv2からの逆伝播した勾配の各ピクセルである。
また、右辺の\frac{\partial O _ k}{\partial X _ i}は、O _ kの式をX _ i偏微分したものである。
O _ kは次の式で表される。

したがって、これらを例えばX _ 1偏微分し、元の式に代入すると、

[tex: \begin{align} \frac{\partial L}{\partial X _ 1} &= \sum_{k} \frac{\partial L}{\partial O _ k} \cdot \frac{\partial O _ k}{\partial X _ 1} \ &= F _ 1 \cdot O _ 1 + 0 \cdot O _ 2 + 0 \cdot O _ 3 + 0 \cdot O _ 4 \ &= F _ 1 O _ 1 \end{align}

これを\frac{\partial  L}{\partial X _ 9} まで 求めて、shapeを整えたものが、\frac{\partial  L}{\partial X}である。

畳み込み計算で表現

この勾配\frac{\partial  L}{\partial X _ i}を求める過程は、実は畳み込み計算で表現できる。

f:id:meow_memow:20201229153537j:plain

\frac{\partial O}{\partial X _ i}が特定のフィルタor0であり、これが\frac{\partial  L}{\partial  O}の各ピクセルと掛け合わされることから大まかに把握できるかと思う。微分して0になる場所は、入力\frac{\partial  L}{\partial  O}に0をpaddingすることで対応する。
畳み込み計算と1つ違う点としては、フィルタを180°回転させることである(実際の行列計算ではフィルタの行列を転置する操作にあたる[4])。

畳み込みをしながら\frac{\partial  L}{\partial X _ i}を求めていくイメージを下図に示す

\frac{\partial  L}{\partial X _ 1}は、

\frac{\partial  L}{\partial X _ 2}は、

最後までフィルターをスライドすると、\frac{\partial  L}{\partial X _ 9} まで求まる。

このように、入力の特徴マップに対する勾配\frac{\partial  L}{\partial X}を求める上で、転置畳み込みが使用される。

パラメータの謎の解明

ここで冒頭、nn.ConvTranspose2dのパラメータ名と処理の整合性が合っていないと思った話に戻る。

実はnn.ConvTranspose2dの引数のpadding,strideは、Convの順伝播のパラメータを与えなければいけないのである。
この理由は、TransConvの入力としてConvの出力の勾配(例だと\frac{\partial  L}{\partial  O})がくることを想定しているためだと考えられる。
もともとは勾配を求める用途のものを、(Convの文脈とは独立して)単なるアップサンプリング用途に使おうとしているから私は挙動を理解できなかったのである。

パラメータをどう指定するのか

seq2seq型のモデルだったらencoderのConv層のパラメータがあると思うので、decoder側のnn.ConvTranspose2dのパラメータにはそれを与えてやればよい。
しかし、seq2seqモデルではない場合などで、アップサンプリングにnn.ConvTranspose2dを使いたい場合、出力の特徴マップを所要のサイズにするにはパラメータをどう与えるか。

公式ドキュメント[3]によると、サイズは関して下記の計算式で求まると書かれている2
H _ {out} = (H _ {in} −1)×stride[0 −2×padding[0]+dilation[0]×(kernel_size[0]−1)+output_padding[0]+1]

しかし、これを覚えるのは面倒である。

したがって、このH_outの式を覚えるよりも、畳み込みの逆問題を解いた方が早いと私は考える。
例えば、3x3の特徴マップを5x5にアップサンプリングしたい場合、逆問題として5x5の特徴マップを畳み込みで3x3にするにはどうすればいいかを考える。
答えの一例としては、カーネルを2x2、stride=2, padding=0にすればよいので、これをnn.ConvTranspose2dのパラメータに与えるといった具合である。

まとめ

nn.ConvTranspose2dで所要の出力サイズにするためのパラメータ設定方法に関して述べた。

調べ物をする中で、引数と挙動の謎が解けた。文献[1][2]にかかれている通り、畳み込みの逆伝播さえつかめれば転置畳み込みは理解できることがわかった。

逆に考えると、転置畳み込みを理解していないということは、畳み込みも理解していない、ということになる。

参考文献

ふろく: デバッグ用のコード

一応、nn.ConvTranspose2dをかけた時のshapeを確認するためのコードを用意した。


  1. 特徴マップの最小要素を正式には何と呼ぶのかわからなかった。間違っているかもしれないが、本稿ではピクセルと呼ぶことにした。

  2. これはHeightの例であるが、Widthも同様の計算が行われる。

スパイ養成コンテスト!! Open xINT CTF in AVTokyo 2020 write-up と 参加した感想

(私のスコアボード。ブルートフォースしすぎました...)

目次

Open xINT CTFはコンピューターセキュリティのオフラインカンファレンスイベントAVTokyo内の1イベントで、インテリジェンス能力を競うコンテストです。主催はTeam pinjaです。
今年は10月31日に開催されました。前年度に初参加してみてとてつもなく面白かったので、次回開催される日を心より待っていました。
出題ジャンルは例年は現地ならではのSIGINT問やHUMINT問がありますが、今年はAVTokyoがオンライン開催だったこともあり、全問OSINTからの出題でした。

私はチーム名test、ユーザ名emowtetでソロ参加しました。今年は3位に入賞するのが目標で、ふざけたHNが入賞していたら面白いかなと思ってこんな名前にしました。
その結果、700ptで17位(/152チーム)でした。入賞しませんでしたし、HN芸は1位の方がやってのけた感があるので、次回があるならば普通のHNで登録します。

本記事では、私が解けた問題の答案(write-up)を共有します。付録として私の事前対策と他の方々のwrite-up一覧を載せました。


スポンサードリンク

お知らせ(2021年10月23日更新)

xINT CTFのサーバが公開されました!

2021年度のOpen xINT CTFは10/23(土)に開催されます!

  • 参考: AVtokyo2021内のアナウンス
    • 2020年と同じく、参加費無料、オンライン、全てOSINT問、3人までチーム可能です。
  • 参加方法:
    • Open xINT CTFにご興味のある方は、10/23(土曜日) 9:00ごろまでに、
    • AVTokyoに参加登録(AVTokyoのDiscordサーバへ参加)し、
    • Discordに参加した後は、#xint-ctf-generalという名前のチャットルームに集まります(2021/10/21の時点ではまだチャットルームはできていません。)
    • xINT CTFのサーバにアクセスします http://133.242.156.137:2021/

なお、過去の参加報告は↓にまとめています。

meow-memow.hatenablog.com

write-up

問題は7カテゴリ23問あります。私はそのうち5問正解しました。
全問題はスコアサーバにアカウントを作成すれば閲覧、解答できます。スコアサーバのURLはavtokyoのdiscodexint-ctf-generalチャンネルのヘッダを御覧ください。サーバはおそらく11月までの稼働だと思うのでお早めに試されることを推奨します。
(2021/01/23追記)
xINT CTFの問題サーバ3年分が公開されました! 2018年: http://49.212.212.72:2018/
2019年: http://49.212.212.72:2019/
2020年: http://49.212.212.72:2020/
2021/04/04現在、サーバにアクセスできなくなっています。

それでは答案を1つずつ見ていきます1

[BUS] Easy Bus 200

とりあえず、手がかりになるものはないか画像内に映っているものをズームします。

ポールに「山形県」、看板に「ふくの湯」とかいてあるので、検索しました。
すると、場所がヒットしました。あとは、バス停を探すのですが、ふくの湯の近くをストリートビューで調べてもバス停が見つかりませんでした。したがって別の情報を使って解くしかないと判断しました。
尾花沢市のバスを調べると、尾花沢市路線バス時刻表が見つかりました。多分、ここのpdfのバス停のどれかだと踏みました。
そこで、画像内に〇〇温泉というカーペイントがされているバスがあるので、「山形県 温泉」で検索すると銀山温泉というものがあることがわかります。
あとは、銀山温泉へ行くバスを調べればよいのですが、ちょうど銀山線というものがあるようです。 あとは、pdf内のバス停名を総当りでsubmitし、答えを見つけました。

flag: 西原

余談: 解答の後の運営の方のコメント

すみません、少しでも早くポイントが欲しかったので余裕がなかったです^^;

[DarkWeb] Easy Onion 100

私のダークウェブの理解度はほぼ皆無で、耳にしたことこそあれど、実際は何なのか全くわからないレベルです。
ポイントは100ptと少なめなので、頑張れば解けるかもしれないと思い、とりあえずTorをインストールしてみました。

u6ra3igsfurmotk2.onionをTorのフォームに入力すると、一応ページが出てきます。このサーバのipアドレスを突き止めるわけですが、どうやればいいかわかりませんでした。

とりあえず見出しのBIANCO - Plateforme de dénonciation anonyme sécuriséeという単語でchromeで検索すると、大元のhttps://bianco-mg.org/というフランスのサイトが見つかります。内部告発を支援する組織のようです。そのページの中に匿名フォームを用意する主旨で上述のダークウェブへアクセスする手順の載ったページが見つかりました。
このwebサーバがそうなのではないかと思って、$ nslookup bianco-mg.orgで出てくるipをフラグとして提出しましたが不正解でした。

その後は、u6ra3igsfurmotk2.onionnslookup方法をしらべたり、Tor Circuitのipを打ち込んだりといった無知ムーブを繰り返しました。

考えあぐねていましたが、chromeの翻訳機能を使いながらhttps://bianco-mg.org/ブラウジングしていた所、このページからhttps://doleances.bianco-mg.org/というページに行けることがわかりました。このページはTorで見たのと同じレイアウトをしています。

試しに、$ nslookup doleances.bianco-mg.orgした結果を打ち込んだ所、正解しました。

flag: 51.15.97.245

[BASIC] WHOIS 100

問題名の通りですが、$ whois pinja.xyzと叩けばすぐに解けます。
whoisコマンドを使う問題はいつも出題されており、xintctfにおけるwelcome問題の位置付けですね。

フラグ: 2020-10-27T03:28:00.0Z

[BASIC] Aaron 200

まず、兄弟の名前"Aaron Ruben Mordechai"で検索をします。するとBarnett finally closes on $11M Noho portfolioという記事が見つかります。これによると父親のportfolioを巡って3人が争っていたことがわかります。したがって、本名はAaron Muschelということがわかります。
続いて、詐欺事件を検索したいので、"Aaron Muschel fraud New Jersey"で検索します。
すると、Convicted Ponzi-Schemer Indicted On New Fraud And Other Chargesというのがトップに見つかります。ここでテック企業というのがFacebookだというのがわかります。このページに出てくる弁護士の名前をフラグとして提出してみましたが、どれも不正解でした。
2番目の検索結果として、同じような内容のFBIのページが見つかります。しかし、ここにも弁護士の名前は見つかりませんでした。しかし、このページの最後にThis content has been reproduced from its original source.と書いてあり、ニュージャージー州の司法省のサイトへ飛びます。ここの検索フォームで、"Aaron Muschel"と検索すると、Three Arrested, Charged In New Jersey For Multimillion-Dollar Fraud Offering Phony “pre-ipo” Facebook Sharesというページがヒットします。ページ一番下にDefense counselが載っており、そこのAaron Muschel担当の人がフラグでした。

フラグ: James T. Moriarty

[MAP] CG 100

画像リンクはこちら

画像を見た感じ、北朝鮮っぽいなと感じたのですが、URLのドメインがkpなので北朝鮮で確定です。問題は、背景に見えるパフェのような塔と、円盤のついた建物がどこか、なのですが、全く検討が付きませんでした。
とりあえず、ポータルのhttp://www.friend.com.kp/に行き、写真を紹介している場所を探しました。もとのURLからおそらく2019年7月だろうと思い画像ライブラリのバックナンバーをたどりました。その結果、このページで画像が使われていることがわかりました。"Mirae Scientists Street"というタイトルでした。
とりあえず、Mirae Scientists Streetで画像検索すると、パフェのような塔が検索に引っかかり、wikipediaから塔の名前がMirae Unha Towerという名前であることがわかります。
あとはGoogle Mapで"Mirae Unha Tower"で検索して近くの建物を探して終了と思ったのですが、北朝鮮なのでGoogle Mapにインデキシングされていません。

どうするか考えたのですが、Mirae Unha Towerのwikipediaページにはmapが記載されていました。このポイントをもとにしていけばいいと思いました。しかし位置座標のとり方がわからなかったので2wikipedia mapとGoogle Earthを左右に並べて、位置を照らし合わせる行為を行いました。

Mirae Unha Towerの位置をGoogle Earthで特定した後は、円盤のついた建物を見つけ、後は撮影されてそうな位置を総当りで試して正解しました。

フラグ: N38.995 E125.736

参加しての所感

  • 競争の激化
    • 前年度までは、CTFの参加条件がAVTokyo参加者だったこともあり、オンサイト(@渋谷)かつ入場料7,000円という参入障壁がありました。それに加えて、会場のwifiが繋がりにくかったり、パソコンの充電が切れないようにという心配をしながら解く必要がありました。また、オンサイトでアーカイブが残るかわからない中だと、メインホールの講演を聞きながら解かざるを得ず、パフォーマンスを思う以上に挙げられないということもありました。そのため、競争はxINT CTF専門で参加した方たち内の戦いだった気がします。
    • 一方今回は誰でも参加することができ、解くデバイスも用意し放題、また講演もweb配信なのでキャプチャなど使えば保存できることもあり、自分のリソースをCTFに十分に割くことができた方が多かったのではないかと思います。
    • そういう公平な条件でOSINT力が試されるので、必然的に上級者の方々が続々と上位に登られました。特にDarkWebカテゴリは顕著だったと思います。結果として当初楽観的に3位以内に入れると見込んでいた当てが外れました。
  • write-upが充実
    • 競争が激化した反面、競技人口が増えたメリットとしてwrite-upが大量に増えました。
      • 答案の共有があることで、参加者は他人の解答から学ぶことができ、スキルアップに繋げることができます。
    • 過去のxintctfのwrite-upは実はそれほどは多くみつかりません(付録の「事前対策」の項目参照)。したがって、問題の中には解き方はおろか答えすらわからないものもあります。一方今年は、多様な解答が数多く共有されていてOSINTの理解を広げることができました。
    • Discodeで振り返りの時間があったことも大きかったと思います3。テキストチャットに答えが続々投下されました。
  • チームを組んでみたかった
    • 今回はチーム参加が可能だったのですが、総ユーザ数は165で、総チーム数は152だったそうです。1.09人/チームなのでほぼソロ戦ですね。
      • 私はせっかくなので、ツイッターでメンバーをFF内外問わず募集してみましたが、うまくいきませんでした。
    • 各ユーザの回答状況を見てみると、解き方や解くジャンルがバラけていることがわかるので、組むと大きなインパクトがありそうだと思いました。
    • 今回は確かにソロが多かったですが、次回があるならば、オンサイトでの開催であったとしてもチーム参加OKであってほしいと思いました。
  • 問題数が多い
    • 去年は10問だったのですが、チームOKだったからか今年は倍以上になりました。1人では解ききれないと思いますが、問題が多い分だけ解ける問題も増えました。
  • バス問題ガチ勢の存在感
    • 去年もそうでしたが、獲得ポイント最高(故に最難の)バス問題を解くことを至上命題とするかのような猛者が大勢参加されたように見受けられます。時間内に解けた人が9人もいました。他の問題でも3人くらいしか解答できていないのですが...戦慄しました。
  • 総評
    • 初心者でも楽しめて、かつ技術介入度も高く順当に(=変にまぐれ当たりで上位にくることはない)順位が決まるので、よく練られた問題設計だなぁと感じました。
    • 私はセキュリティの専門ではないですが、そのような者から見てxINT CTFは知識無しでもとっつきやすいと思っています。他のCTFジャンルだと前提知識(例えばweb問はwebサーバーの知識、binaryはアセンブリ言語)と問題を解くための知識運用力を身につける必要があり、実際のコンテストで問題が解けるまでに修練を要します。一方で、xINT CTFは検索さえできれば始めることができます。
    • もちろん、「OSINT技術を知っていないと絶対に解けないだろう」という問題もありますが、一方で頑張ればこじ開けられる問題もあるので「難しい、けど解ける。だから楽しい。」といった成功体験を得やすいです。
    • 誰にでも門戸が開かれつつ、強い人のwrite-upを見てOSINTの奥の深さを学べるので、説明文にある"スパイ養成コンテスト"は言い得て妙だと思いました。

改めて運営チームの方々に今年も開催していただけたことを感謝申し上げます。次回の開催を願ってOSINTのスキルアップに励みます!

付録

事前対策

①まず、過去問を見直しました

②また、運営メンバーはTwitterでときどきヒントのようなつぶやきを非公式にあげるので、下記の方々からなるTwitterのリストを用意しました。

③加えて、開催の数週間前に、luminさんがSecurity Days2020にて"ビジネスOSINTでわかる様々なこと"という題で講演されていたので、そちらも視聴しました。

④あと、今年の時事問題が来るかなと予想し、Twitter裏アカウント特定問題が来た時にどうするかのイメトレをしていました。

2020年度の他の方々のwrite-up

私が確認できた範囲でwrite-upを書かれている記事を紹介します。見づらくすみません。o印は記事内で扱われている問題。discodeのチャンネルにも解答が投下されているので併せてご覧いただければと思います。

HN(敬称略) 記事URL
Easy Bus INSIDE BUS Easy Onion 🍳Kyoto-Cool #1 🍳Kyoto-Cool #2 🍳Kyoto-Cool #3 RAMEN DATETIME SUN No.1 No.2 No.3 No.4 No.5 WHOIS Need WiFi Speed Lover Super Easy Aaron Speed Lover Newbie made a mistake CG Star Gazer GATE
solve count→ 69 9 27 5 3 3 3 0 2 6 1 0 0 1 107 4 2 3 2 3 40 1 2
point→ 200 500 100 300 400 400 300 200 300 100 100 200 200 300 100 200 200 200 300 300 100 200 200
o o o o o
chappie https://chapp1e.hatenablog.com/entry/2020/10/31/233739 o o o o o o o
ak1t0 https://medium.com/@ak1t0/open-xint-ctf-2020-darkweb-write-up-17969075228e o o o
_(:3」∠)_ https://st98.github.io/diary/posts/2020-11-01-open-xint-ctf-2020.html o o o o o o o o o
Cyber_ken https://oniyan.hatenablog.com/entry/2020/11/01/085354 o o o o
taru https://github.com/tarugrimoire/Open-xInt-CTF-2020-Writeups o o
f0reachARR https://qiita.com/f0reachARR/items/1044d92b9a071ad564e8 o

同じ問題でも解き方が異なっていて面白いですね。


・『Open xINT CTF 関連ページ 非公式まとめ』へ戻る


  1. 答案だけ見るとスマートに答えを割り出しているように見えますが、裏では試行錯誤を繰り返した末にたどり着いていることを補足します。

  2. 記事を書いている時に気づいたが、普通に右下の"External maps"ボタンから座標を取得できる。

  3. ただ、私はDiscodeの使い勝手がわかっていなかったため、ずっとミュートになっていたようで、気がつけば音声チャンネルでの運営の方々による振り返りが終わっていたのが惜しい所でした。

connpassの開催前イベントのキャンセル人数を粗く見積もる方法

f:id:meow_memow:20201102212947j:plain

connpassはIT勉強会のイベント管理機能や参加者管理するサービスなどを提供するプラットフォームです。
イベント主催者は開催日まで参加者を募りますが、途中で参加申込者のキャンセルも出ます。
主催者側としては、イベント開催前にキャンセル人数を見積もれると役に立つことがあるので、粗く見積もる方法を考えました。
手作業でも求められますが非常に手間がかかるので、urlを入れると見積もったキャンセル人数が出るpythonスクリプトを実装しました。

キャンセル人数を見積もるアイディア

現行の参加エントリーしている人(抽選中、キャンセル待ちを含む)を参加予定者と呼ぶことにします。
参加予定者は、参加するかキャンセルするかを確率的に決めていると仮定します。例えばコイントスで決める人ならば1/2ですし、3本ある くじ のうち1本だけある当たりくじを引いたら参加すると決めている人ならば1/3、トランプでハートが出た時だけ参加すると決めている人ならば1/4です。
キャンセル(予定)人数は、各参加予定者の確率の総和を求めた後、参加予定者の総和から引けば求められます。

キャンセル人数を見積もる例として上記のコイントス、くじ、トランプで決めている人達が参加予定者であることを考えます。
確率の総和は13/12=1.08です。参加予定者の総和は3です。したがって、キャンセル予定人数は3-1.08=1.92です。
このことから、約2名がキャンセルしそうだということがわかります。

ただし現実問題として参加する確率はわからないので、過去の参加傾向から推定します。すなわち過去のイベントのうち何回参加しているかの割合を参加予定者の確率とします。例えば、過去に10回イベントに参加登録したうち9回参加していたら確率は0.9とします。

アイディアの詳細

なぜ、このようになるのかの説明をします。 現時点でN人が参加エントリーをしているとします。i番目の参加予定者が参加したら1,キャンセルしたら0として定義した確率変数X _ iがあるとします。N個の確率変数X _ iはそれぞれ独立とします。
そして、参加する確率をp _ iとした時、X _ i \in \left\{ 1, 0 \right\}, P(X _ i=1)= p _ i, P(X _ i=0) = 1 - p _ iと表せます。

(現時点での)参加予定者が参加する総数もまた確率変数となり、X = \Sigma ^ {N} _ {i=1} X _ iと表すことができます。そして、この確率変数Xポアソン二項分布 に従うとみなせます。 このポアソン二項分布の平均は\mu = \Sigma ^ {N} _ {i=1} p _ iです。これは(現時点での)参加人数の期待値とみなせます。上述の例で参加予定者の参加確率の和を計算していたのは、この部分に当たります。

あとは、Nをこの\muで引けば、キャンセル人数となります。
ちなみに何故、参加予定者数ではなくキャンセル人数としたのかですが、参加者の期待値としてしまうと、未来に登録する参加者も変数として考慮しなければならず、問題が複雑化するためです。そのため、キャンセル人数としました。

以上でアイディアの説明は終わりです。大量に仮定を置いていることがわかります。この仮定によって生じる見積もりの限界は後述します。

実装

上述のアイディアを実装しました。イベントのURLを入力すると、キャンセル人数を出力するpython scriptです。なお、動かすのにconnpassアカウントは不要です。コードは下記のリポジトリに配置しています。

github.com

各ユーザの参加確率を求めるにはイベントの参加予定者リストと、参加予定ユーザの過去のイベント参加履歴が必要です。connpassはWebAPIを提供していますが、ユーザに関して取得できるものはありませんでした。したがって、必要な2つの情報はhtmlページのスクレイピングで収集します。connpass.com利用規約の第7条【禁止事項】にスクレイピング行為は載っていないので、https://connpass.com/robots.txt を遵守する形で機械的に情報取得させていただきます。もちろん、マナーとして、リクエストを連続で送る場合には1秒のsleepを入れています。

参加予定ユーザ一覧の取得

conpassのイベントページのurlから参加者一覧を取り出してリスト化する関数は下記です。
イベントページのhtmlデータを取得後、applicant_areaクラス下のparticipation_table_areaクラスのついたテーブルを取得します。
ユーザ情報へのURL内にユーザidがあるので、それを抽出してリストに格納して返しています。

def get_participants_id_list(event_url: str) -> IdList:
    """イベント参加予定者のユーザidのリストを取得

    Args:
        event_url (str): イベントページのURL

    Returns:
        IdList: ユーザidのリスト
    """

    # 申込みをしたユーザのページを取得
    participants_url = f'{event_url}/participation'
    r = requests.get(participants_url)
    soup = BeautifulSoup(r.text, 'html.parser')

    # ユーザ一覧のテーブルからユーザidを取得
    participants_id_list = []
    for participation_table_list in soup.select('.applicant_area .participation_table_area'):
        for participating_user in participation_table_list.select('.user'):
            user_url = participating_user.select('.display_name a')[0]['href']
            m = re.match('https://connpass.com/user/(.*)/', user_url)
            participants_id_list.append(m.group(1))

    return participants_id_list

現行のコードには実装漏れがあります。参加申込数が100人を超えると、次のページが発行されますが、それの対応をしていません。したがって、参加申込数が100人を超えるイベントに対してはエラーを出して終了しています。

ユーザのイベント参加割合を求める

取得した全ユーザに対し、ユーザのページから過去のイベントの参加履歴を見て、「終了」ステータスのイベントを「参加」したか「キャンセル」したかの情報をすべて収集し、最後に割合として求めます。もしもイベントが補欠だった場合、参加の意思があったかわからないので、カウント対象から除外します。
新規ユーザは、参加するかわからないので開発者が指定できるようにします。デフォルトでは0.5とします。

def calc_participation_rate(user_id: str, new_user_participation_prob: float = 0.5) -> float:
    """イベント参加予定者の過去のイベントに実際に参加した割合を求め、参加確率とする

    Args:
        user_id (str): ユーザid
        new_user_participation_prob (float, optional): 新規ユーザの場合の確率. Defaults to 0.5.

    Returns:
        float: イベント参加確率
    """
    user_url = f'https://connpass.com/user/{user_id}/'
    r = requests.get(user_url)
    soup = BeautifulSoup(r.text, 'html.parser')

    # イベントのテーブル部分を取得
    participating_total_num = int(
        soup.select('.square_tab .num')[0].get_text())

    if participating_total_num == 0:
        return new_user_participation_prob

    # connpassはユーザページにはイベントを10件ずつしか表示しないので、ページ数を予め取得する必要がある
    page_total_num = math.ceil(participating_total_num / 10.)

    total_register_num = 0
    participate_num = 0

    for page_idx in range(1, page_total_num + 1):
        if page_idx > 1:
            # 2ページ目以降はリクエストを送る間隔を設ける
            time.sleep(1)
            user_url = f'https://connpass.com/user/{user_id}/?page={page_idx}'
            r = requests.get(user_url)
            soup = BeautifulSoup(r.text, 'html.parser')

        # ページごとのユーザのイベントの参加状況を収集する
        results = soup.select('div.event_area .event_list')
        for result in results:
            event_status = result.select(
                '.event_thumbnail .label_status_event')

            # イベントの開催がすでに終わっているもののみカウントする
            if event_status[0].get_text() == '終了':
                # ユーザの参加状況を取得
                participate_status = result.select(
                    '.label_status_tag')[0].get_text()

                # 補欠のまま終了したイベントは参加イベントとしてノーカウント
                if participate_status == '補欠':
                    continue
                total_register_num += 1

                if participate_status == 'キャンセル':
                    continue
                else:
                    participate_num += 1

    participation_rate = participate_num / total_register_num

    print(f"ユーザID: {user_id}, 参加確率: {participation_rate}, 参加回数: {participate_num}, イベント申し込み数: {total_register_num}")

    return participation_rate

スクリプトの実行例

以上の内容の関数をestimate_number_of_cancellation.pyモジュールに入れました。コマンドライン実行できるようにしています。したがって、実行方法は、

  • $ python estimate_number_of_cancellation.py <event_url>
    • event_url: connpassのイベントページのURL

実行結果は下記のような形で標準出力されます。

イベントに参加予定のユーザIDリスト
['user1', 'user2']

全2ユーザー分の参加確率を算出中...
ユーザID: user1, 参加確率: 1.0, 参加回数: 119, イベント申し込み数: 119
ユーザID: user2, 参加確率: 0.8080808080808081, 参加回数: 80, イベント申し込み数: 99

見積もったキャンセル人数: 0.19191919191919204, 現在の参加登録人数: 2, 現在の参加人数の期待値:1.808080808080808

注意点

  • [重要]処理結果が返ってくるまでにとても時間がかかります。
    • ユーザのイベント参加ページの取得時1秒のスリープを入れています。そして、1ページあたりのユーザのイベント参加情報は10つまでしか表示されません。したがって(ユーザ数 * ユーザの過去のイベント参加数)/10秒時間がかかります。
      • たとえば1人のユーザが200イベント過去に参加していた場合は収集に20秒かかります。それがユーザ分繰り返されるので、実行に10分以上かかることもあると思います。
  • 先述の通り、イベントの参加予定者の数が100人を超えている場合の実装ができていないので、100人以上の場合は処理を中断しています。申し訳ございません。

この手法の限界

  • ユーザのイベント参加回数が少ない場合、参加確率が統計的に信頼できない
    • 例えばイベント参加回数が1回の場合、確率が1.0or0.0のどちらかになり、全体の数値の足を引っ張る可能性があります。
  • 無断欠席者はわからない
    • コロナ禍真っ只中の現在は特にオンライン開催が多く、イベントに申し込んで実際には参加しなかった、という数が多くなっている可能性が高いです。なので、確率がやはり信頼できない可能性があります。
  • 確率変数間の独立を仮定
    • 「あの人が行くから私も行こう」という可能性を見逃しています。別の見方をすると、誰かがキャンセルしたことで別の人がキャンセルすることを考慮していません。

まとめ

connpassのイベントに対し、参加をキャンセルする人数を大雑把に見積もる方法を考え、pythonスクリプトを開発しました。
実装には、速度面、数値の信頼性の問題があります。あくまで、概算用という位置づけです。
イベント運営者の勘で人数当てをした時の結果と比較すると面白いかもしれません。

参考文献

機械学習のTrainerのクラス図を写経して高解像度にした

f:id:meow_memow:20201010163642p:plain

最近、mediumでWriting a Production-Level Machine Learning Framework: Lessons Learnedという記事を読んだ。
こちらは(PyTorchを使った)機械学習を本番適用する上で心がけるべき6つのポイントを説明した記事である。

その中の1つ"1. Do not reinvent the wheel"で、Trainerの典型的な構成をクラス図(著者いわくhigh-level sketch)としてまとめたものがあるのだが、解像度が低く文字は拡大してやっと読める程度につぶれてしまっていた。
したがって、自分のTrainerに対する知識の整理も込めてこのクラス図をplantumlで写経して、なるべく高解像度な画像した1。なお、元のpumlコードはこちら

コンポネントの位置がオリジナルの画像とずれている理由は、plantumlのコンポネントの位置関係を制御するのが難しかったためである。技術不足で申し訳ない。

EngineクラスがTrainerに当たるものである。論文実装でコードがこの形式で用意されていると嬉しい。 図の中にはPyTorchが提供するクラスもありやや冗長かもしれない。

所感

私は実験でTrainerを作る時は、jupyter notebookで実験したものをリファクタリングする形で作っているが、実験ごとに毎回毎回作ってしまっている。まさしく、元の記事で警告している車輪の再発明状態である。
なので、こういう抽象的な構成のテンプレートコードを持っておいてjupyter notebookのコードをそのテンプレートに適用していったほうが長期的な研究生産性が高まりそうだと思った。世には既にpytorch-lightningみたいな学習ラッパーがあるのでそこら辺を触っていきたい。


  1. なお、この記事トップの画像も同じ画像をアップロードしたのだが、はてなブログ側で勝手に縮小されてしまった。

GANベースの画像変換手法『ACL-GAN』を顔写真→アニメ顔変換タスクを中心に理解する

f:id:meow_memow:20201006215223p:plain

またSelfie2Animeデータセットを扱ったunpairedな画像変換手法が出てきたので「顔写真 → アニメ顔変換」タスクを中心に手法を理解する。おまけで、コードの簡単な実行方法も説明する。
念の為断っておくが、名にACLと冠しているが自然言語処理のトップカンファレンスとは一切関係ない

論文の基本情報

ECCV2020 に採択された論文である。今年の採択率は26%。

論文の概要

ソースドメイン(ここでは人の顔写真)の画像からターゲットドメイン(ここではアニメキャラクターの顔)の画像へ変換することに取り組んでいる。GANベースで、unpairedでも(画像の間で対応が取れていなくても)変換できるようにGeneratorを学習させる手法を提案している。

論文が解決したのは下記のCycle GANの欠点である。

  1. 再構成という制約のために、画像変換時に変換元の画像の情報(ピクセル)を変換先の画像へ埋め込んでしまっている
    • 男性→女性変化の時にひげが残っていたりする
  2. 形状変換能力が弱い
    • 顔写真→アニメ変換だと輪郭を変える必要がある
  3. Cycle Consistency Lossでは決定的な変換しか学習されない
    • ある画像をGeneratorに変換するとき同じ画像からは同じ変換しかされない。これは多様性がない。

そこで、入力画像をピクセルではなく特徴として保持しつつも、多様性が生まれるようなlossであるAdversarial-Consistency Lossを提案している。

lossの全体

トータルの損失関数は下記。

f:id:meow_memow:20201006215558j:plain

\lambdaはlossの重み付けを行うためのハイパーパラメータである。 第1項目がAdversal-Translation Loss、第2項目がAdversarial-Consistency Loss、第3項目がIdentity Loss、第4項目がBounded focus maskのためのlossである。 説明順はメインの第2項目のAdversarial-Consistency Loss、第1項目のAdversal-Translation Loss、第3項目のIdentity Lossである。なお、第4項目は入力画像を変換する部分を制限するためのlossであるが、selfie2animeタスクにおいては\lambda_{mask}は0に設定されているので、説明を割愛する。

Adversarial-Consistency Loss

論文のキーポイントとなるAdversarial-Consistency Loss(ACL)に関して説明をする。
このlossがある意図は2つ。

  1. 1つの画像に対して、多様な変換ができるようにする
  2. ソース → ターゲット変換のGeneratorに対して、ソースドメインの特徴を保持できるようにする

概念図は下図。元のドメインと別ドメインへ行って返ってきた画像を比較するあたりぱっと見Cycle GANと同じ見た目をしている。

f:id:meow_memow:20201006215713j:plain:w400

ACLの具体的な式は下記の通り。

f:id:meow_memow:20201006215814p:plain:w400

式の構成要素の説明

\hat{D}(\cdot, \cdot)ACLのために用意されたDiscriminatorである。画像に対して2値分類する分類器である。引数が2つある件は後述する。
x_Sは入力画像となるソースドメイン側の画像である。これがオリジナルの画像となる。
\bar{x}_T=G_T(x_S,z_1)は、ソースドメインの画像をターゲットドメインへ変換したフェイク画像である。
\hat{x} _ {S} = G_S(\bar{x} _ {T}, z_2)は、ターゲット側のフェイク画像を入力として、ソース側に変換したフェイク画像である。
\tilde{x} _ S=G _ S(x _ {S}, z _ {3})は少しややこしいが、オリジナルのソースドメインの画像に対してターゲットドメインからソースドメインへの画像変換をかけた結果である。Cycle GANでは、これはGeneratorが恒等写像となるように学習されるが、ACL-GANでは下図のように、若干の変化が加わる。このような\tilde{x} _ Sを設ける理由は、データオーグメンテーションの役割ではないかと考える。オリジナルの画像x_Sばかり使うのではなく、そこからノイズでちょっと変換をかけた画像を使う方が多様な変換を獲得できる可能性が高いと著者は考えたのではないだろうか。

f:id:meow_memow:20201006220151p:plain:h400

G_T(x, z)は入力画像をターゲット画像へ変換するGenerator、G_S(x, z)は画像をソース画像へ変換するGeneratorである。画像のGeneratorはMUNITを踏襲している。すなわち、画像をcontentとstyleに分けるエンコーダーをソースとターゲットの2つのドメイン分用意して、変換時に、ターゲット側のスタイルを混ぜてデコーダーに通して画像を生成している。
zN(0, 1)に従うノイズであり、生成器にノイズを混ぜることで画像のスタイルを多様に変換するための乱数である。これにより、同じ画像を入力してもノイズが異なれば生成される画像も異なるようにできる。

ACLの役割

\mathcal{L} _ {ACL}の大雑把な意味としては、ソースドメインの画像(にSrc→Tgt変換をかけた画像)\tilde{x} _ {S}と、ソース→ターゲット→ソースと変換2回行ってソースドメインに戻ってきた画像\hat{x} _ Sの差異をDiscriminator \hat{D}(\cdot, \cdot)で測っているような形である。\hat{D}(\cdot, \cdot)としては、\tilde{x} _ S\hat{x} _ Sとを見分けようとするし、Generatorは\hat{D}(\cdot, \cdot)を騙したいので、これら2つの画像の差異がなくなるように学習していく。
Cycle GANではここを再構成誤差としていたため、1つの画像に対する変換が決定的になってしまう。一方こちらの手法では、識別器\hat{D}で差を測ることで、ソースドメインの画像\tilde{x} _ Sと変換して戻ってきた画像\hat{x} _ Sが異なる画像であっても、同じドメインになっていればOKなのでGeneratorが多様な変換を獲得できるようになる。

\hat{D}(\cdot, \cdot)はどのように実装されているか

\hat{D}(\cdot, \cdot)がxを2つ引数に取る件であるが、これはオリジナルの画像x_Sを連結している。 実際のコードでは、画像をチャネルを軸にして連結している。
例えば画像の入力が[B, C, H, W]として、 一方がtorch.Size([2, 3, 256, 256])で、もう一方もtorch.Size([2, 3, 256, 256])の時、結合時の次元はtorch.Size([2, 6, 256, 256])となる。
画像を連結する意図は、Generatorがソースドメインの元画像の特徴を保持しながら変換する能力を獲得するためのお手本として使用したとのことである(p.6,l.6〜)。
ただ、個人的にはここの設計が不可解である。というのも、G_S(\cdot)x_Sを混ぜるのではなく、Discriminatorで混ぜているからである。確かにDiscriminatorの分類結果をGeneratorが学習するが、やや間接的ではないかと思ってしまう。

Adversarial-Translation Loss

\mathcal{L} _ {adv}=\mathcal{L} _ {adv} ^ {T}+\mathcal{L} _ {adv} ^ {S}

Adversarial-Translation Loss \mathcal{L} _ {adv}はいわゆるGANのloss部分である。すなわちGeneratorとDiscriminatorがいたちごっこしている部分である。
ターゲットドメイン側のloss \mathcal{L} _ {adv} ^ {T}は、下記の通りで、分類器D_Tがrealなアニメ画像x_Tと生成器G_Tが生成したfake画像\bar{x} _ Tを見分ける。

f:id:meow_memow:20201006221048p:plain

ソースドメイン側のloss \mathcal{L} _ {adv} ^ {S}はちょっとだけ違う。fake画像が\tilde{x} _ S\hat{x} _ Sの2つがあるのでlossを折半しているような形。

f:id:meow_memow:20201006221102p:plain

Identity loss

f:id:meow_memow:20201006221137p:plain

ここで x _ {S} ^ {idt}=G _ {S}(x _ {S}, E _ {S} ^ {z} (x _ S)), x _ {T} ^ {idt}=G _ {T}(x _ {T}, E _ {T} ^ {z} (x _ T))

ACLでは、ノイズを加えることで多様な画像を生成できるようにすると言ったが、自分の画像由来で生成したノイズに対しては恒等写像になってほしいようにloss \mathcal{L} _ {idt}で制御する。

これにより、期待できることとして下記の4点を上げている。

  1. 特徴を保持
  2. 変換画像の質を向上させ、
  3. 学習プロセスを安定させることを期待する。
  4. mode collapse(?)を避ける(p.6, 3.3)
    • 学習データセットすべてを変換できるようにしようとしてバランスが取れなくて破滅することだろうか?

Generatorのノイズ引数に入れられているE^{z}(\cdot)は画像をノイズに変換するネットワークとのことらしい。実際のコードを見てみると、下記の手順でMUNITのStyelをAdainのパラメータにしていることを表していた。

  1. GeneratorであるMUNITのEncoder側でStyleとContentを生成
  2. Style情報を多層レイヤパーセプトロンに通してAdaINのパラメータを生成
  3. Decoder側でAdaINの正規化をかけながら解像度を増やして画像にする
  4. Generatorの画像とオリジナルの画像を、ピクセル差でL1ノルムをとって誤差としている。

実験

U-GAT-IT論文を出した研究グループ(NCSOFT社?)が独自に集めた、Selfie2anime dataset(どこからか収集してきた人の自撮り画像とアニメ画像)で評価する。
比べる手法は、Unpairedな画像変換では有名な所5つ。CouncilGANが最も最近の手法である。Council GANについて興味があれば解説記事を書いたので読んでいただけると幸いである。

定性的な評価は下図。CycleGANと比べると輪郭のデフォルメや顔の追従ができていると主張している。個人的には、他の手法の生成画像とそんなに大差ないという印象。

f:id:meow_memow:20201006222052j:plain:w500

定量評価結果としてはFID, KIDでどの既存手法にも勝っている。

f:id:meow_memow:20201006222107j:plain:h300

さらに、U-GAT-ITはパラメータが670.8Mある一方ACL-GANは54.9Mであると主張している。パラメータ数が少ないけど定量評価で勝っていることを強調している。

コードの実行方法

pretrained weightは提供されていない模様。

追記: 私がモデル学習させた時のpretrained weightを下記に置きました。ただし論文とは異なるパラメータで学習させたため、論文の再現には至りませんでした。 github.com

所感

unpairedな画像変換手法の研究としては着実な進歩を進んでいるが、顔写真→アニメ変換に絞って見ると、限界を迎えていそう。そもそものselfie2animeデータセット自体、データがそんなにが良くない可能性がある(鏡越しの自撮りで顔の前にスマホがあったりする)ので進歩がわかりにくい。だから、selfie2animeタスクに絞って手法を研究開発してほしいところである。
また、目や顔や顔が大幅にデフォルメされなければならない以上、もとの特徴を保持することが好ましくないかもしれない。そう思うと、アニメ調に変換といった時、どんなふうなアニメのスタイルに変換することを期待しているのかを決めて手法を設計した方がいいと思った。


  1. ただし有志の方のissueによると学習の再現に失敗している模様。著者が原因を調べるといってcloseされている。 https://github.com/hyperplane-lab/ACL-GAN/issues/3