Stimulator

機械学習とか好きな技術話とかエンジニア的な話とかを書く

機械学習モデリングの広辞苑的書籍「Kaggleで勝つデータ分析の技術」が良かったので筆者に媚を売る

- はじめに -

当ブログでは恒例になっている、献本されたので筆者に媚を売るシリーズです。

今回は10/9に発売予定の「Kaggleで勝つデータ分析の技術」という書籍なんですが、既に発売前にしてAmazonベストセラー1位。豪華著者陣とKaggleにおいては日本有数の起業と言っても過言ではない、DeNA株式会社の豪華レビュワー。筆者がブログを書いていたりu++さんがめちゃくちゃ丁寧な書評を書いていたり、Kaggle Grand Master各位の薦めツイートも出てきた段階で、もう私が媚を売る必要すらないと思いますが、良かったので感想だけでも残しておければと思います。

Kaggleで勝つデータ分析の技術

Kaggleで勝つデータ分析の技術

端的に言えば、テーブルデータにおける機械学習モデリング、データ分析の広辞苑+αな書籍で、筆者らがブログやSNS等で述べている通り「暗黙知を洗い出す」ような良書でした。

 

 

- どんな内容だったか -

前置きとして「広辞苑的な書籍」と表現した通り、Kaggle、データ分析における重要なワードとその適切な範囲までの解説がセットで広く書かれた書籍です。

詳細なアルゴリズムや数理的な背景までは説明していませんが、その分幅広く適切な深さの解説、コラム、ソースコードや過去コンテストの実例がついています。

f:id:vaaaaaanquish:20191005205334j:plain:w400
数式を最小限にしつつも実例を交えて各評価指標やアルゴリズムを紹介

コードや背景については分量は少なめですが、私が読んだ技術書の中でもズバ抜けて参考文献を非常に丁寧にまとめており、気になる所が後追い出来るようになっているだけでなく、ソースコードGitHubで公開されるとの事なので、基本的に読んだ後に手を動かして体感する事も可能そうです。


また、「決定木の気持ち」「過去コンペで話題になった手法」といったKaggler同士の中でのミームについても解説が入っています。Kaggle自体のコミュニティ性から言っても、技術情報や共通のミーム、周囲の戦略の収集は、コンテストに挑むにあたって重要な事でもあるので、今からでも書籍を読んで把握しておいて損はないでしょう。

f:id:vaaaaaanquish:20191005210157j:plain:w400
基本的なコンテストでの分析フロー等が図付きで解説されている

 
私自身、機械学習やデータ分析のブログやSNS投稿、スライド、発表や同人誌などで情報収集をする事は多々ありますが、「あなたの言ってる定義微妙に違ってない…?」と思う事は少なくありません(私が毎回正解を言っている確信がある訳でもありません)。そういった機械学習モデルや統計モデルを活用するコンテストやプロジェクトが一般的に広まってきたという段階で、各ワードに対する認識の共通化を図れる良い書籍です。

この一冊とGoogle翻訳があれば、Kaggle上のDiscussionやKagger Slack、オフラインイベントでの会話をかなり的確に把握できるようになるとも感じます。そういった場で、今まで曖昧に流してきたワードがあるような方にオススメの一冊です。

 
書籍のまえがきには、「ビジネス的な側面について触れない」とも書かれており、事実内容もその通りではありましたが、弊社の機械学習チームではこの書籍に出てくるような単語を前提の共通ワード、コンテキストとして扱う場面が多くあるなと読んでいて思いましたし、コンペに出る事を目的としない場合でも自力を上げる知識が多く含まれています

 
個人的には、これほど「タスクの評価」「モデルの評価」「Validationの設計」について広く用語がまとまった書籍は他に見たことがありませんし、実務でモデルの評価について考えている人はここに書いてある事は前提として一通り把握しておくべきだとも思います。実務だとここに事業的な評価やシステムの話が入ってくると思いますが、まず前提として知っておかないとモデルも構築できないと思います。

 
私自身、これを読みながら以下のようなツイートをしており、書籍と公開されるGithubを合わせれば、自然言語処理100本ノック、画像処理100本ノックに続く、手を動かして機械学習を広く知るコンテンツの1つともなりそうです。


知の高速道路が整備されるのは良いことです。

 

- 読んだらKaggleで勝てるか -

読んだだけでは勝てそうにありません。

 
この書籍は広辞苑のように大きく知識を広げ、ともすればコンテスト中に逆引きできる良書籍ですが、実際には多くの知見、バックグラウンドがその背景に詰め込まれた書籍でもあります。

例えば、「基本的なライブラリ(numpy, pandas, sklearn, ..., etc)の使い方」から「予測確率」「次元削減」といったワード、果ては「教師あり、なし学習」辺りまで、機械学習、統計の教科書的な知識は前提知識として必要としてきます。プラスでモデルの背景が分かっていないと「ん?」となる部分もいくつかあります。これは、それらがきちんとしている著者らが書いたからこそ正確かつ客観性、再現性のある書籍になっているという話でもあります。「舟を編む」ではないですが、これ程の機械学習、データ分析の範囲を適切な表現と深さで書いている本はほとんどありませんし、流石と言わんばかりです。
 
そういった事もあって、前提知識がないと出来ない事、例えば「アルゴリズムわからないので機械学習モデルどれ選べばいいか分からない」「可視化の結果どの方向性に行くのが正しいか分からない」という課題を解決してくれる書籍では無いので、「これでKaggle始めよう!」という人には前提として他にもいくつかの書籍やブログと一緒に読んでいく必要があるかなと思いました


全体が薄い訳ではなく、「xgboostのアルゴリズム、チューニング」「ベイズ最適化」については大きく3~5ページが取られています。筆者としてもKaggleで重要なBoostingとチューニングに限っては丁寧に解説したかったのだとも思います。特に近年では業界内でも有名なブログや企業、学術機関からも、不要なパラメータのチューニングやLeakageを代表に、コンテストでは一般的にバッドプラクティスとされる話が多く出回るようになりましたし、注力して書かれていると感じます。

逆に言うと、このxgboostの丁寧な解説と同量またはそれ以上のバックグラウンドが、他全ての章に書かれた技術についても本来ある訳でもあります。この書籍から分からなかった単語をググって、勉強して、コンテストに出てという形を想定した足掛かり的な書籍でもあると感じました。

 
当たり前ですが、実際に手を動かして、体感して、コンテストに時間を捧げてsubmitしない事には勝てないので、やっていきの気持ちが大事です。というか知ってるだけで勝てるなら私だってk…

 

- どんな書籍と読むと良さそうか -

基本的には、書籍内の参考文献が非常に優れておりその中のスライドやブログ、書籍を参考にすると良いと思います。実際この付録に書籍の3割くらいの価値が詰まっていると思います。

実際この付録のURL一覧だけでGitHubスター100はあげたいレベルです。

 
中でも、Kaggleでプラスになると考えて抜粋すると、「特徴量エンジニアリング」辺り。「データ解析のための統計モデリング入門」は一緒に買っておくと間違いないと思います。

機械学習のための特徴量エンジニアリング ―その原理とPythonによる実践 (オライリー・ジャパン)

機械学習のための特徴量エンジニアリング ―その原理とPythonによる実践 (オライリー・ジャパン)

はじパタやプロフェッショナルシリーズも悪くないですが、上の2つは直接的にKaggleや分析に効くいい本だと思います。
 
参考文献外だと「仕事ではじめる機械学習」「機械学習のエッセンス」はこの本の支えになる知識が多いと感じました。持ってなければ是非一緒に。

仕事ではじめる機械学習

仕事ではじめる機械学習

 
最近は書籍も多い(献本も多い)ですが、低いレベルのまとめ的な書籍も少なくないです。そういう意味でも「Kaggleで勝つデータ分析の技術」の厳選された参考文献と過去コンテスト情報は読んでおくべきだと思います。


 

- おわりに -

 
数年前、偶然同僚にKaggle Grand Masterが居たので気になってKaggleを始めましたが、まさかこんな良質で立派な書籍が出たりする程大きくなるとは…正直想像していませんでした。しかも、Expertになってからボーッとしてたら、数年で幾人もの人に追い抜かれ、何も言えない人になってしまいました。悔しいです。

DeNA株式会社に蔓延する攻撃的なSNSアカウントから「Expertが書いてる書評だから無意味」と言われそうなので、ブログはこの辺にしておきます。

 
媚を売られる側になれるよう、私も頑張ります。


Kaggleで勝つデータ分析の技術

Kaggleで勝つデータ分析の技術

 

追記:2019/10/05
らしいです。これは失礼しました。

xonshのEnvironment Variablesの全て

- はじめに -

以下の記事でxonshのEnvironment Variablesの大体の日本語訳を書いた。

xonshrcを書く - Stimulator

しかし、上記の記事は2017年次の物かつ、要約的な記事のため、より詳細に見て開発に以降できるよう本体へのコードや掴みどころをメモした記事を書いておく。

本家ドキュメントは下記なので参照のこと。

xon.sh
 

 
 

- Windowsに関連するもの -

Windowsの人はまずここ見て設定。
xon.sh

$ANSICON

Windowsのcmd.exe(コマンドプロンプト)において、ANSI escape sequencesを表示するためのアプリケーションであるansicon*1を利用するかどうかの設定。ansiconを利用する場合にTrueにしておくと、$TITLEをプロンプトのタイトルに設定するようになる。実装でいうと以下のような感じ。

import ctypes
ctypes.windll.kernel32.SetConsoleTitleW(env.get("TITLE"))

 

$INTENSIFY_COLORS_ON_WIN

Windowsのcmd.exeを利用する時に、colorを見やすくするかどうかのフラグ。青色がシアンに置き換わったりする。
そもそも近年でcolor style自体がかなり柔軟になったので、不要かも。細かく指定しない場合には使える。

 

$WIN_UNICODE_CONSOLE

Trueであれば、WindowsでのUnicodeサポートを有効にする。win_unicode_consoleなる外部ライブラリをimportしているだけ。以下必須。

pip install win_unicode_console

 

$PATHEXT

環境変数のPATHを見に行った時に、実行可能ファイルとして判定する拡張子のリスト。
".EXE"のように大文字で指定する。xonsh上でWINDOWS判定されていれば、[".COM", ".EXE", ".BAT", ".CMD"]がデフォルトで入るので、それ以外に必要であれば追加する。

 

- xonsh本体の動作に関するもの -

$XONSHRC

rcファイルへのpathのリスト。['~/xonshrc', '~/.config/xonsh/rc.xsh']のように複数あれば前方から複数読み込まれるし、ファイルがなければ読み込まれず終わる。

$VIRTUAL_ENV

アクティブなPythonへのPath。pythonやpipコマンドが指すところでもあり、xonsh用のpythonバージョン管理ツールであるvoxも参照するpathになる。
デフォルトでは変数が設定されていない状態なので注意。

 

$XONSH_HISTORY_SIZE

historyのサイズの指定。「(8128, "commands")」「"8128 commands"」のようなsetか文字列で、保存するコマンド数(commands)、保持する履歴ファイル数(files)、許可される秒数(s)、バイト数(b)の単位のどれかと値を指定する。

$XONSH_HISTORY_BACKEND

historyの保存形式。デフォルトはjsonだが、sqlを設定すればsqliteをバックエンドに選べる。設定は以下を見ると良い。
https://xon.sh/tutorial_history_backend.html

 

$SUPPRESS_BRANCH_TIMEOUT_MESSAGE

Trueであれば、VC_BRANCH_TIMEOUTに設定された時間を過ぎてもPROMPTの文字列、branch名、色の生成が終わって無かった場合に強制的にタイムアウトとしメッセージを表示する。PROMPTに{curr_branch}でブランチ名を表示させようとするが、git repoが巨大で…という時に起こる。

 

$VC_BRANCH_TIMEOUT

gitのブランチ名などをPROMPTに表示する時にタイムアウトする秒数。デカいrepo触ったりする時に設定しておくと良い。

 

$VC_HG_SHOW_BRANCH

Trueであれば、Mercurialのブランチを表示する。gitでなくMercurial使っている場合はこちら。

 

$UPDATE_OS_ENVIRON

Trueであれば、xonshのEnvが追加、削除された場合にその変更をos側の環境変数にも加える。デフォルトはfalse。
中身は「os.environ.update」を少し工夫して呼んでいるだけなので、個別にos側に追加したい時は不要。WindowsとかOSの環境変数が影響の大きい環境では使える場合があるかも。

 

$VI_MODE

Trueであれば、シェルの操作がvi風になる。iでinsert、escでnormalモードになるなど。
実装自体はprompt toolkitの機能で、現行のモードを判定するfilterとして実装されている以下の部分から追っていくと良い。
https://github.com/prompt-toolkit/python-prompt-toolkit/blob/master/prompt_toolkit/filters/cli.py

 

$XDG_DATA_HOME

英語では「Open desktop standard data home dir」となっているが、普通にセッションログやスクリプトの実行ログが入る場所へのpathというだけ。以下のXONSH_DATA_DIRに近いが、こちらのPathはxonsh用ではなく、一般的な環境全体用。
すこし踏み入ればpygmentsを使った実装になっていることが分かるが、難しい事はほとんどしておらず整備もあまりされていない印象。pygmentsすごい。

 

$XONSH_DATA_DIR

セッションログやスクリプトの実行ログが入る場所へのpath。
デフォルトでは「$XDG_DATA_HOME + "/xonsh"」 が設定されている。

 

$XONSH_CACHE_EVERYTHING

Trueであれば、全てのコード(コマンド含め)をキャッシュに保存する。
ここで言っているキャッシュというのは、historyではなく、そのセッションにおけるキャッシュで、できるだけコンパイルして保存しておいて次回のコマンド実行を早めるというもの。

 

$XONSH_CACHE_SCRIPTS

Trueであれば、スクリプト実行をキャッシュに保存する。
機構自体は上記のXONSH_CACHE_EVERYTHINGと同じだが、こちらはexecを通るようなスクリプトを実行した場合のキャッシュ。

 

$XONSH_TRACEBACK_LOGFILE

XONSH_SHOW_TRACEBACKがTrueの場合に、ログファイルとして残す先。ファイル名か不要の場合はNoneにしておく。

 

$XONSH_STORE_*

Trueであればhistoryに標準入出力を保存する。

$XONSH_STORE_STDIN

xonshの!()や![]オペレータを使って実行されたものをhistoryに保存する。

$XONSH_STORE_STDOUT

stderrやstdoutをhistoryに保存する。

$XONSH_DATETIME_FORMAT

ログやhistoryなど多くの場所で使われるdatetimeのフォーマット。デフォルトでは「"%Y-%m-%d %H:%M"」。
利用先は多いが、実装はtools.pyの中に収まっているだけでシンプル。

 

$XONSH_DEBUG

デバッグモードの指定。1であればimport情報、2であれば入力変換、コマンド置換の情報、3以上であればPLY解析メッセージと情報が増えていく。
よくissueを立てると「$XONSH_DEBUG=3 で一回ログ出して見て」とよく言われたりする。
実装自体は、参照されている部分が多いので変更は大変そうだが、追加はlogging足すだけなので参考に。

 

$XONSH_ENCODING

xonshのサブプロセスで利用されるエンコーディング。デフォルトでは「sys.getdefaultencoding()」が入る。
I/O制御で使われているTeeクラス、入力を作るreadline辺りが主に利用している。

 

$XONSH_ENCODING_ERRORS

Pythonエンコーディングエラーが出た時の処理。多くの場所で共通して使われている。
入る値は以下を参考に。
https://docs.python.org/3/library/codecs.html#error-handlers

 

$XONSH_PROC_FREQUENCY

連続したパイプラインを実行する時、キューを読み込むためにxonshプロセススレッドがスリープする秒数。
スリープタイムというよりは、コマンドを連続で実行した時、スレッド同士のキューが詰まった場合にタイムアウトする時間のイメージ。

 

- コマンド補完に関するもの -

$UPDATE_COMPLETIONS_ON_KEYPRESS

Trueであれば、キー入力時に毎回補完候補を出すようになる。例えば補完であればTABを押さなくてもキー入力毎に候補を表示できるようになる。
PROMPTの表示を評価するUPDATE_PROMPT_ON_KEYPRESSも別にあるので混同に注意。

f:id:vaaaaaanquish:20180622142703g:plain
キー入力時評価の例

実装自体はprompt toolkitの機能で、xonshはこの変数をフラグとしてptkの引数となるcomplete_while_typingに投げているだけである。その仕組み自体を把握するにはptkを追う必要がある。詳細は以下。
https://python-prompt-toolkit.readthedocs.io/en/latest/pages/asking_for_input.html?highlight=complete_while_typing#complete-while-typing

 

$AUTO_SUGGEST

Trueの時にfish shellのようなグレーアウトの補完候補を出す*2

f:id:vaaaaaanquish:20190824001450p:plain
AUTO_SUGGEST

この機能はxonshのコアライブラリであるpython prompt toolkit内の実装なので、そちらを参考にすると良い。
https://github.com/prompt-toolkit/python-prompt-toolkit/blob/master/docs/pages/asking_for_input.rst#auto-suggestion

表示されている補完候補を確定するときは「右矢印」「ctrl+e」がデフォルトで設定されている。変更する場合は以下が参考になる。
xonsh[ptk]で、Suggestionを確定するキーバインドを設定する - Qiita

下記のAUTO_SUGGEST_IN_COMPLETIONSとも関連しており、両者がTrueになっている場合、AUTO_SUGGESTで表示される候補をTab補完から除く等の機能があるので設定はよしなに。

 

$AUTO_SUGGEST_IN_COMPLETIONS

Trueの時、Tabキーで補完候補を表示する。$UPDATE_COMPLETIONS_ON_KEYPRESSがTrueの時は、Tab以外のキーでもキー入力時に補完候補が動的に表示される。

AUTO_SUGGEST同様、xonshというよりはpython prompt toolkitの機能であり、そちらを呼び出すかどうかのフラグにあたる。
https://github.com/prompt-toolkit/python-prompt-toolkit/blob/master/docs/pages/asking_for_input.rst#autocompletion

 

$COMPLETIONS_DISPLAY

Pythonのコードの補完を表示するか、もしくは表示形式をどうするか。

none or false

表示しない。stringの"none"や"false"でも良いし、PythonのNoneでも、boolのFalseでも良い。

single

1列で補完候補を表示する。stringで"single"。

f:id:vaaaaaanquish:20190824214546p:plain
$COMPLETIONS_DISPLAY = "single"

multi or true

複数列で表示する。デフォルト値。stringで"multi"、"true"とするか、boolのTrueを指定する。

f:id:vaaaaaanquish:20190824214823p:plain
$COMPLETIONS_DISPLAY = "multi"

readline

GNU Readlineの動作を再現した表示にする。stringの"readline"。

f:id:vaaaaaanquish:20190824214905p:plain
$COMPLETIONS_DISPLAY = "readline"


引数の型が柔軟なのは歴史的経緯。こちらはPythonの補完のみでbashの補完はこのEnvに依存していないので注意。

 

$COMPLETIONS_MENU_ROWS

補完を出す行数の指定。stringの"multi"だと5行で横幅最大まで補完を省略して表示する。int値を指定しても良い。

 

$COMPLETION_IN_THREAD

補完をasync/awaitで表示する。
実装自体はprompt toolkitのAppの引数にフラグを入れているだけで、非同期補完の詳細を見るにはptkのasync_promptの実装を見ると良い。
https://python-prompt-toolkit.readthedocs.io/en/master/pages/asking_for_input.html#asynchronous-completion
https://github.com/prompt-toolkit/python-prompt-toolkit/blob/master/prompt_toolkit/shortcuts/prompt.py#L909

 

$COMPLETION_QUERY_LIMIT

補完メニューの表示件数。最大件数では無く、これを超えたら補完が表示される閾値のイメージ。
xonshやprompt toolkitの機能というよりは、GNU Readlineのrl_completion_query_itemsをglobal変数として入れておくという実装なので、詳しくは以下を読む。
GNU Readline Library - Programming with GNU Readline

 

$COMPLETIONS_CONFIRM

タブ補完メニューが表示されている時、FalseだとEnterでコマンドを直接実行、TrueだとEnterでコマンドを確定のみして確認状態にする。
実装はprompt toolkitのkeybindにfilterを設定しているので、理解したければptkのkeybindingsを理解してからが良い。

 

$BASH_COMPLETIONS

bashのcompletionを利用するために、bash_completionスクリプトへのPATHを設定するための変数。sshの補完やgitの補完が含まれるため、基本的には必須だと思う。
listかtupleで最初に有効だったものが利用される。

例えばMacではbrewbashの補完が導入できる。

brew install bash-completion2

こちらは、"/usr/local/share/bash-completion/bash_completion" にファイルが配置される。デフォルトでBASH_COMPLETIONSにこのpathが入っているはずなので、基本的にはインストールだけで設定できるはず。

デフォルトだといくつか設定されており、それらを走査する実装になっているため、もし一意に絞れるようであれば絞るかソートで前に持ってきておくと良い。

 

$COMPLETIONS_BRACKETS

Trueの時、シェル入力時にPythonの括弧の補間を有効にする。実装自体はシンプルなので読めば大体分かると思う。

 

$FUZZY_PATH_COMPLETION

Trueであれば、pathをTab補完する時にfuzzyに補完する。あいまい補完。

> $FUZZY_PATH_COMPLETION = True
> ls desko 
./Desktop

具体的な実装としてはlevenshteinを使っており、以下を見れば良い。
https://github.com/xonsh/xonsh/blob/master/xonsh/tools.py#L854

 

$GLOB_SORTED

Trueであれば補完の結果をソートしてから表示する。

 

$SUBSEQUENCE_PATH_COMPLETION

Trueであれば、pathの補完において ~/w/t のような入力でも拡大解釈されて ~/work/tmp も候補に上がるようになる。
ディレクトリが長い時に雑に打って移動する時に使える。

 

$SUGGEST_COMMANDS

Trueであれば、無効なコマンドを入力した時に「もしかして?」を表示する。

f:id:vaaaaaanquish:20190825222513p:plain
$SUGGEST_COMMANDS=True

 

$SUGGEST_MAX_NUM

SUGGEST_COMMANDSがTrueの時に表示するコマンド数。負の値を入れておくと制限なしになる。

 

$SUGGEST_THRESHOLD

SUGGEST_COMMANDSやFUZZY_PATH_COMPLETIONでレーベンシュタイン距離を計算して、その閾値として扱う値。デフォルトは3。

 

$XONSH_AUTOPAIR

Trueであれば、括弧、括弧、引用符を自動挿入する。
keybindingsのfilterとして実装されているので、prompt toolkitにおけるfilterが何かを追ってから読むと大体実装もわかる。

 

$XONSH_HISTORY_MATCH_ANYWHERE

Trueであれば、上矢印で履歴を参照する時、現在の入力を検索語として接頭語以外でも補完する。
実装自体はprompt toolkitのbufferにおけるhistory matchなので、以下を参照すると良い。
https://github.com/prompt-toolkit/python-prompt-toolkit/blob/master/prompt_toolkit/buffer.py#L911

 

- ファイル、ディレクトリ操作に関するもの -

$AUTO_CD

Trueにしておくとcdコマンドを打たなくてもディレクトリ名だけで移動できる。

 

$CDPATH

bash, zsh等のCDPATHと同じ。cdコマンドが利用する相対パス。CDPATHによく使うディレクトリを入れておけば、cd hogeでショートカットのように移動できるので便利。一方で補間含めて全ての相対パスがこちらを参照するようになるので、使う場合は意図しないディレクトリに移動しないよう注意が必要。

 

$AUTO_PUSHD

DOSで言う所のディレクトリスタックに対して、cd時に自動でpushdする。pushd、popd、dirsコマンド*3を良く使うのであれば良い。

 

$DIRSTACK_SIZE

DOSで言う所のディレクトリスタックの最大保存数。実装自体は至極簡単なので読めば分かるはず。

  

$PUSHD_MINUS

ディレクトリスタックの操作pushd、popdに関するフラグ。Falseが通常のシェルコマンドのpushdで、Trueの場合はpushdとpopdが逆になる。

 

$PUSHD_SILENT

Trueならディレクトリスタックにpushdした時に表示する。デフォルトのFalseなら表示しない。

 

$DOTGLOB

Trueなら「*」や「**」を使ってglobした時にドットで始まるドットファイルを含むようにする。
実装はシンプルにreplaceで変換しているだけなので、読めば分かるはず。

 

- 表示に関するもの -

$PROMPT

xonshというよりはpython prompt toolkitの機能であり、ptk内のAppに対する引数に与える文字列に当たる。
https://github.com/prompt-toolkit/python-prompt-toolkit/blob/master/docs/pages/asking_for_input.rst#adding-a-bottom-toolbar

背景色や文字色も変更できるので、以下のドキュメントを参考にすると良い。
Asking for input (prompts) — prompt_toolkit 3.0.0 documentation
More about styling — prompt_toolkit 3.0.0 documentation

$PROMPTの表示。詳しく設定したい場合は、以下のチュートリアルを見ながら設定を進めると良い。
https://xon.sh/tutorial.html#customizing-the-prompt

 

$PROMPT_FIELDS

$PROMPTに対してオリジナルのフォーマットを関数等で指定できる。詳しく設定したい場合は、以下のチュートリアルを見ながら設定を進めると良い。
https://xon.sh/tutorial.html#customizing-the-prompt

 

$RIGHT_PROMPT

xonshというよりはpython prompt toolkitの機能であり、ptk内のAppに対する引数に与える文字列に当たる。
https://github.com/prompt-toolkit/python-prompt-toolkit/blob/master/docs/pages/asking_for_input.rst#adding-a-bottom-toolbar

背景色や文字色も変更できるので、以下のドキュメントを参考にすると良い。
Asking for input (prompts) — prompt_toolkit 3.0.0 documentation
More about styling — prompt_toolkit 3.0.0 documentation

右側のPROMPTに表示する文字列。詳しく設定したい場合は、以下のチュートリアルを見ながら設定を進めると良い。
https://xon.sh/tutorial.html#customizing-the-prompt

 

$BOTTOM_TOOLBAR

bottom toolbarを表示する文字列。空ならbottom toolbar無しになる。

f:id:vaaaaaanquish:20190824172521p:plain
bottom toolbar


xonshというよりはpython prompt toolkitの機能であり、ptk内のAppに対する引数に与える文字列に当たる。
https://github.com/prompt-toolkit/python-prompt-toolkit/blob/master/docs/pages/asking_for_input.rst#adding-a-bottom-toolbar

背景色や文字色も変更できるので、以下のドキュメントを参考にすると良い。
Asking for input (prompts) — prompt_toolkit 3.0.0 documentation
More about styling — prompt_toolkit 3.0.0 documentation

詳しく設定したい場合は、以下のチュートリアルを見ながら設定を進めると良い。
https://xon.sh/tutorial.html#customizing-the-prompt

 

$XONSH_GITSTATUS_*

xonshのgit statusのシンボル。gitコマンドがない場合はそれぞれsetされないので注意。

 

$XONSH_GITSTATUS_HASH

ハッシュ値。「git describe --always」と「git rev-parse --short HEAD」の値が違っていたら、XONSH_GITSTATUS_HASH+後者の値をHASHとして表示する実装。以下参照。
https://github.com/xonsh/xonsh/blob/master/xonsh/prompt/gitstatus.py#L89
デフォルト値は":"。

$XONSH_GITSTATUS_BRANCH

ブランチ名。デフォルト値は"{CYAN}"。

$XONSH_GITSTATUS_OPERATION

rebase中だとかmerge中だとかの状態シンボル。
[https://github.com/xonsh/xonsh/blob/master/xonsh/prompt/gitstatus.py#L104[
デフォルト値は"{CYAN}"。

$XONSH_GITSTATUS_STAGED

stageに乗っている時のシンボル。デフォルト値は"{RED}●"。

$XONSH_GITSTATUS_CONFLICTS:

コンフリクトが置きている時のシンボル。デフォルト値は"{RED}×"。

$XONSH_GITSTATUS_CHANGED

変更がある状態のシンボル。デフォルト値は"{BLUE}+"。

$XONSH_GITSTATUS_UNTRACKED

gitをトラックしていない時のシンボル。デフォルト値は"…"。

$XONSH_GITSTATUS_STASHED

stashした状態のシンボル。デフォルト値は"⚑"。

$XONSH_GITSTATUS_CLEAN

git上記の状態以外でclearな時のシンボル。デフォルト値は"{BOLD_GREEN}✓"。

$XONSH_GITSTATUS_AHEAD

masterブランチとのズレがaheadな状態を示すシンボル。デフォルト値は"↑·"。

$XONSH_GITSTATUS_BEHIND

masterブランチとのズレがbehindな状態を示すシンボル。デフォルト値は"↓·"。

 

$UPDATE_PROMPT_ON_KEYPRESS

Trueであれば、キー入力ごとにPROMPT表示を評価する。PROMPTに時計を実装したり、gitの状態を表示する時に有用。
実装もprompt toolkitにprompt周りの設定を関数で渡すか、stringで渡すかの違いでしかないのでシンプル。
実際評価しているのはptkなので、以下辺りから追っていくと良い。
https://github.com/prompt-toolkit/python-prompt-toolkit/blob/master/prompt_toolkit/shortcuts/prompt.py

 

$PROMPT_REFRESH_INTERVAL

PROMPTを更新する秒数を入れる。実装自体はprompt toolkitにあり、以下のサンプルのようにbottom_toolbarやrpromptでリアルタイムに更新される時計を作ったりできる。
https://github.com/prompt-toolkit/python-prompt-toolkit/blob/2.0/examples/prompts/clock-input.py

私が追加した変数なのだが、もちろん画面全体が指定秒でリフレッシュされて更新される訳なので、シェルの動作が重くなってしまう。(rpromptだけとか出来るようになっていくと良いな…)

 

$COLOR_INPUT

入力しているコマンドにsyntax highlightingをつける。実装自体は、prompt toolkit及びpygmentsによる実装なので以下を参考にすると良い。
https://github.com/prompt-toolkit/python-prompt-toolkit/blob/master/prompt_toolkit/lexers/pygments.py#L145

 

$COLOR_RESULTS

出力結果にsyntax highlightingをつける。実装自体は、上記のCOLOR_INPUTと同じくprompt toolkit及びpygmentsによる実装なので以下を参考にすると良い。
https://github.com/prompt-toolkit/python-prompt-toolkit/blob/master/prompt_toolkit/lexers/pygments.py#L145

 

$PRETTY_PRINT_RESULTS

Tanaka Akira氏が作っているrubyのprettyprint.rbを使って、出力を表示するかどうかのフラグ。
以下のドキュメントの通り、単体でもapiとして使えるようにはなっている。
https://xon.sh/api/pretty.html

 

$DYNAMIC_CWD_WIDTH

$PROMPTに表示するディレクトリを省略する時の文字数閾値
(float, str)のtupleで、0番目に閾値を入れる。float('inf')を入れる事もできる。
1番目にはstringの"%"かそれ以外が入る。"%"を入れた場合はShellの画面サイズに対する0番目の値のパーセンテージになる。"%"以外を入れたらシンプルに0番目がintになって文字数で閾値になる。

 

$DYNAMIC_CWD_ELISION_CHAR

$PROMPTに表示するディレクトリ名が長い場合に省略形として出す文字列。省略する時の文字数の閾値はDYNAMIC_CWD_WIDTHに依存。

 

$INDENT

複数行入力のインデント文字列

f:id:vaaaaaanquish:20190825181526p:plain
$INDENT="...."
実装は、改行時やkeybindで指定の文字列を挿入しているだけ。

 

$LS_COLORS

bash, zsh同様、lsコマンドの出力の色指定。基本的には他のシェルのものが参考になるし、実装を見て色名でのstring指定もできる。

 

$XONSH_COLOR_STYLE

xonshのcolorを統一的に変更できるthemeのような機能。適応できるstyleは「xonfig styles」で確認できる。
最近はPTK_STYLE_OVERRIDESやPROMPT_TOOLKIT_COLOR_DEPTHで柔軟に変更できるようになったが、このtheme機能も簡単に良い感じにできるので良さはある。

 

$PTK_STYLE_OVERRIDES

補完の表示色や、背景色、プロンプトの色などあらゆる色を設定するところ。

f:id:vaaaaaanquish:20190825200508p:plain
$PTK_STYLE_OVERRIDES
設定できる項目とデフォルト値の対応は以下の通り

  • "completion-menu": "bg:ansigray ansiblack"
  • "completion-menu.completion": ""
  • "completion-menu.completion.current": "bg:ansibrightblack ansiwhite"
  • "scrollbar.background": "bg:ansibrightblack"
  • "scrollbar.arrow": "bg:ansiblack ansiwhite bold"
  • "scrollbar.button": "bg:ansiblack"
  • "auto-suggestion": "ansibrightblack"
  • "aborting": "ansibrightblack"

デフォルト値参考:https://github.com/laloch/xonsh/blob/master/xonsh/style_tools.py#L347

 

$PROMPT_TOOLKIT_COLOR_DEPTH

色の深度を決定できる変数。ドキュメントには書いていないが、コード上では以下の値が指定できる。

  • "DEPTH_1_BIT"
  • "MONOCHROME"
  • "DEPTH_4_BIT"
  • "ANSI_COLORS_ONLY"
  • "DEPTH_8_BIT"
  • "DEFAULT"
  • "DEPTH_24_BIT"
  • "TRUE_COLOR"

 

$MULTILINE_PROMPT

コマンドを改行した際に表示される幅寄せ用の文字。

f:id:vaaaaaanquish:20190825184844p:plain
$MULTILINE_PROMPT="@"

 

$TITLE

ターミナルのタイトルを設定する。実装自体は以下のように非常にシンプルで、これで設定できないターミナルエミュレータを使っている場合は無意味。

if ON_WINDOWS and "ANSICON" not in env:
    kernel32.SetConsoleTitleW(t)
else:
    with open(1, "wb", closefd=False) as f:
        f.write("\x1b]0;{0}\x07".format(t).encode())
        f.flush()

iTerm2でタブ名が変わる例

f:id:vaaaaaanquish:20190825234117p:plain
$TITLE="Hoge"

 

$XONSH_SHOW_TRACEBACK

Trueであれば、xonshコマンドがエラーの時にtracebackを全て表示する。長くなるのでデフォルトはFalse。

 

$XONSH_STDERR_*

stderrの出力に接頭語、接尾語を付ける。stderrの色を設定したい時などに使える。
stdio周りはxonsh独自にTeeクラスが実装されていて、そこを通るのでそちらも参考に。

$XONSH_STDERR_POSTFIX

stderrの出力に付く接頭語。

$XONSH_STDERR_PREFIX

stderrの出力に付く接尾語。

 

- コマンドやxonshスクリプトの動作、変数の扱いに関するもの -

$EXPAND_ENV_VARS

Trueであれば「$var」「${var}」「%var%」をサブプロセスモードで自動で展開する。
実装自体はbuiltins.__xonsh__.envの中にあれば変換するし、なければそのまま文字列で利用するとなっている。

> $EXPAND_ENV_VARS=True
> echo "my home is $HOME"
my home is /Users/shunsuke.kawai

> $EXPAND_ENV_VARS=False
> echo "my home is $HOME"
my home is $HOME

あまりFalseにする必要はないと思うが、format string等で不具合が出た時にはこちらの設定を確認すると良い。

 

$FOREIGN_ALIASES_OVERRIDE

zsh上でxonshコマンドでxonshを起動する等、外部シェルから呼び出されている場合にxonshrc等で設定するaliasを外部シェルのaliasに対してOverrideするかどうか。
扱いが難しく xonshが起動した時点でaliasesの読み込みは行われてしまうので、xonshrcやxonsh上のコマンドとして設定しても動作しない。
呼び出し元のbashzsh環境変数としてFOREIGN_ALIASES_OVERRIDEを設定しておく必要があるので注意。

 

$FOREIGN_ALIASES_SUPPRESS_SKIP_MESSAGE

外部シェルのaliasが既に存在する時、xonsh上でOverrideしようとすると警告を出すaliasコマンドが一部存在する。その時のメッセージを表示しないようにするためのフラグ。
基本的にはTrueにしておいて問題ないと思う。

 

$HISTCONTROL

履歴に保存するコマンドを選択する。
setの中にstringで"ignoreerr"を入れておくと、終了ステータスが0以外の正常終了しなかったコマンドを履歴に追加しない設定となる。
setの中にstringで"ignoreerr"を入れておくと、同じコマンドを連続で入力した時に保存しない設定となる。

 

$LANG

historyファイル等で利用する文字コード。あくまでxonsh内での文字コードなので、デフォルトのutf-8以外にする必要に迫られる場合は殆どないはず。

 

$SHELL_TYPE

シェルのコアライブラリを選択する。以下の4つのstringが指定できる。

  • "readline"
  • "prompt_toolkit"
  • "random": ランダムにptkかreadlineを起動する
  • "best": 環境に応じてptkかreadlineを起動する

基本的にというか"prompt_toolkit"でないとxonshの恩恵の殆どが受けられないので一択だと思う…readlineだとほぼbash…randomとかどういう時に使うんだろう…

 

$RAISE_SUBPROC_ERROR

Trueであれば、サブプロセス経由のコマンドが異常終了した場合にsubprocess.CalledProcessErrorを発生させる。デフォルトはFalse。
何も表示せず、ステータスコードを返さず終了するようなモジュールを使う時に役立つかも…?

 

$XONSH_APPEND_NEWLINE

Trueであれば、コマンドの結果の末尾に改行を追加する。
普通のシェルの動作として使う場合はTrueで、「xonsh -c hoge.py」のようにしてxonshを動かす場合に余計な改行が入ってしまうのでFalseにするためのもの。
デフォルト値にはXONSH_INTERACTIVEの値が入っており、基本的にインタラクティブシェルだったらTrueになるようになっている。

 

- キーバインドや操作に関するもの -

$IGNOREEOF

Trueであれば、「ctrl + d」でシェルを終了しない。
コマンド入力を待機し続けるループをbreakするかしないかという実装。
https://github.com/xonsh/xonsh/blob/master/xonsh/ptk2/shell.py#L186

 

$MOUSE_SUPPORT

Trueであれば、シェル上でマウス操作を許可する。
実装自体はprompt toolkitの機能で、ptkの方を見に行くと良い。以下のexampleを実行すれば分かるように、マウスクリックでカーソルを移動させたりできる。
https://github.com/prompt-toolkit/python-prompt-toolkit/blob/master/examples/prompts/mouse-support.py
エディタやセレクターっぽい機能を実装する時にも使えるかも。

 

- フラグや変数として利用するもの -

$LOADED_RC_FILES

xonshrcが読み込まれたかどうかのフラグをlistにしたもの。n番目が何のrcファイルなんだ…

 

$OLDPWD

一つ前に居たディレクトリの文字列が入る。起動時はcdするまでEnvがセットすらされてないので注意が必要。

 

$PATH

各シェルで言う所のいわゆるPATH。binとかスクリプトは個々に入っているディレクトリを見に行って実行する。
listなのでappendとかで追加する。bulitins.__xonsh__.env["PATH"]からもよく参照される。

 

$TERM

基本的にはユーザが設定する事はなく、ターミナルエミュレータが与える変数。以下のようにWindowsエミュレータの判定とかに使われる。
https://github.com/xonsh/xonsh/blob/master/xonsh/base_shell.py#L500

例えば以下のissueのように、Windows+AnacondaをWSL上から触っていてターミナルエミュレータが認識できない場合等に強制的に指定する場合に使う。
Unknown Terminal type using conda env (Windows 10 --> WSL) · Issue #2525 · xonsh/xonsh · GitHub
指定する時はドキュメントの通り、早い段階(xonsh起動時の引数、xonshrcの上部)で設定しないと他設定が読み込まれる。

 

$XONSH_INTERACTIVE

xonshがインタラクティブシェルとして起動しているか、「xonsh -c hoge.py」のようなスクリプト実行形式で起動しているかのフラグ。

 

$XONSH_LOGIN

xonshがログインシェルとして起動しているかのフラグ。

 

$XONSH_SOURCE

xonshスクリプト実行時、スクリプトへの絶対pathが入る変数。スクリプト実行時意外はセットされていない。

- おわりに -

大体これで1.0.0までカバーできるはず。

今回「XDG_CONFIG_HOME」「XONSH_CONFIG_DIR」「XONSH_HISTORY_FILE」など非推奨やもう機能していないものを抜いているので注意。PRを出して改修するのが良さそう。

 

「FINAL FANTASY XVの人工知能」がなかなか面白かったので書評

- はじめに -

このブログの人気シリーズの一つ、「献本されて読んだら良かったので書評で作者に媚を売ろうシリーズ」です。

今回は、あるきっかけで「FINAL FANTASY XV人工知能 - ゲームAIから見える未来」(以下 FF本)という本を頂く形になりました。

FINAL FANTASY XV の人工知能 - ゲームAIから見える未来

FINAL FANTASY XV の人工知能 - ゲームAIから見える未来

私自身の専門もあって、普段はこの手の「人工知能本」と私が認識しているジャンルの本、例えば「よくわかる人工知能」「人工知能ビジネス徹底解説」といった旨の本を手に取る事はほぼなく、ましてや誤った知識を流布する事が多いジャンルでもあると認識していたが、このFF本はまた別の楽しみ方が出来たので、広めても良さそう思ったので書く。


 

- どんな人が楽しめそうか -

ゲームの攻略本を読むのが好きかつ、プログラミングの素養があると楽しめる本だと感じた。
日々学んでいるアルゴリズムやプログラミングの考え方が、ゲーム開発という現場でどう活かされているのか。どういったツール、環境が使われているのか。
あまり外に出てこないゲーム業界の事情や考え方が知れる、オタク大興奮みたいな書籍。

読むと楽しそうなのは以下辺りか

  • CSの学生
  • ゲームが好きな人
  • ゲームの説明書や攻略本を読むのが好きな人

ただ前提として、昨今流行りの機械学習Deep Learningの本ではないし、アルゴリズムの本でもないので、これで人工知能を使ったゲームが作れるとかそういう話ではない。技術目的で買うのは厳しい。どちらかというFFの本家技術同人っぽい雰囲気。

筆者のチームの一人として多くの講演等を行っている三宅陽一郎氏の話を聞いた事があるが、AIという言葉の定義自体が、ゲーム業界の内で使われる場合と外で使われる場合で大きく違っている。歴史的背景もあるし、互いに歩み寄っている部分もあるが、統計、機械学習分野と言葉の使い方が違うという雰囲気を書籍内でもちらほら感じるし、念頭に置いて読んだ方が良いと感じる。

(例えば、幅広くルールベースなものもAIとしてまとめられているし、神経系の理解が危うかったりArtificialと書いてある訳に"人口"が充てられている等、ある視点ではつらい表記は実際にいくつかある)

 
一方で、FINAL FANTASY XVという壮大なゲームの中で使われる、アルゴリズムや意思決定、実際のプロジェクトの動き、データ解析の一片が外から見られるという機会は、そうそう無いだろうし、自分がコンピュータに触れ始めた段階でこれを読んでいれば、擦り切れる程読んでゲーム開発者を目指したかも知れないと思うほど濃く鮮やかな本だと思う。


 

- 概要 -

「FFの本家技術同人」と表現した通り、各章を別々の担当者(エンジニアからデザイナー、リーダーまで)が書いている。
そのため、粒度も違えば、各章で「AIとゲームのこれから」のような節があるのが気になるが、近年テック企業が出してる同人誌はそんな感じなので普通か。

全体の概要は以下のような感じ

  • 1章: ゲームAI入門
    • 大まかなゲームAIの概要と歴史、書籍全体へのpath
    • 短いけど読み物として分かりやすい
  • 2章: 意思決定ツール
    • ゲームキャラクターのAIを作成するために考える事
    • 社内で使っている意思決定ツール、デバッグシステムの写真が出てくるのがなかなか
  • 3章: ナビゲーションシステム
    • パス検索やブロッキングを実際活用してるんやでというお話で興奮する
  • 4章: AIとアニメーション
    • キャラクターを重点的にPSD等を活かして感情を表現している話
    • 2章で出てくるツールをアニメーターも利用しながらキャラクターを構成しているのが伺える
  • 5章: 位置検索システム
    • マップにグリッドを取って検索する話が簡単に書かれている
    • CSで学ぶアルゴリズムがこういう所で活きるって事が実感できて良いと思う
  • AI座談会 プログラマー
    • これだけで同人誌1冊いけるくらい面白い
  • 6章: 仲間AI
    • キャラクターの個性をユーザに感じさせていく工夫の話
    • 「設定を体験に」「仲間:最も近く最も心地よく」などのテーマをどう実現するか丁寧に書かれている
    • 3人のキャラクターが主人公を囲むように動くFF XVというゲームの多大なる工夫を感じて良い
  • 7章: モンスターAI
    • NPCやモンスターの制御の話を簡潔に
  • 8章: 兵士AI
    • 7章の続きみたいな話
  • 9章: アビエントAI
    • 7章の続きみたいな話, 街中のモブのスクリプトを中心に
  • 10章: 写真AI
    • ゲーム内で撮れる写真機能において、キャラの個性や映えを設計する話
    • なかなか他の場面で考える事ないので面白い設定だなと思った
  • AI座談会 デザイナー編
    • ゲームデザイナーがゲームやキャラの作り方について語っている
    • 体験の設計の話はゲーム業界進んでるよなあと思った
  • 11章: 会話AI
    • 会話をユーザに面白いと思ってもらえるようなスクリプト構成の話
  • 12章: AIモード
    • 自律的に動くモードを作ってモブに適応した話
    • 自律モードを使う事の良い点、悪い点が書かれてあってなるほどなって感じ
  • 13章: ロギングと可視化
    • ゲームのbackendアーキテクチャとかロギングの話
    • フレームレートに影響を与えないようログを落とす
    • 統計量を取り異常な会話等を検知する
    • ナビゲーションマップを可視化して意図していない壁などを出さないように
    • 別で本一冊書いて欲しいくらい興味深い
  • 14章: これからのゲームAI
  • AI座談会 チームリーダー編
    • 良かった所メインだったのが気になった
    • チームリーダーの考えや工夫、試行錯誤が見えるともうちょっと良かった気もする

私がエンジニアということもあるかもしれないが、個人的には開発視点の話が面白かった。
「あー、ここもうちょっと書いて欲しい」と思っている時点で、この書籍の目的は達成されているのだと思う。
これから先は、SQUARE ENIXの門を叩くしかないのだろう。


昔、ゲームの攻略本を読んだだけで、手に入れるかも分からない剣やアイテムのモチーフを見て興奮したものだが、ゲーム会社が使っているツールやキャラに貼られたメッシュで似た感覚になれたのがとても良かった。

f:id:vaaaaaanquish:20190803225036j:plainf:id:vaaaaaanquish:20190803224944j:plain
実際のツールの写真

私は「技術書、ブログを書く時は必ず参考文献を、引用を」と常々言っているが、各章で参考文献をしっかり記載している所も好感を持てた。気になる所を追って読めればと思う。


 

- おわりに -

帯の松尾豊氏のコメントも「ゲームの未来を感じられる」とあるが、本当にそう感じた。

ゲーム業界からこういった技術本が出る事はこれまで稀で、実際のゲーム開発について知る機会というのは(いわゆるWebサービス開発やアプリ開発に比べて)少なかったように思う。

この書籍に書かれているような技術ロマンが新しい火種を作り、またゲーム業界を盛り上げるのだとすると、素晴らしい情報発信である。

機械学習観点では、今後ゲームとの距離が近くなると思っており、ゲーム開発におけるAIの歴史や考え方の発信を享受する事で、また新しい接点に繋げていければ良いと感じた一冊だった。


FINAL FANTASY XV の人工知能 - ゲームAIから見える未来

FINAL FANTASY XV の人工知能 - ゲームAIから見える未来

 

python prompt toolkitの紹介と動作を理解するメモ

- はじめに -

本記事はpython prompt toolkit(以下 ptk)の動作を理解するメモです。

ptkで出来ること、その概要を書いています。

- ptkとは -

ptkは、Pythonで実装されているCLI用のツールキットである。

  • コンソール上でのフルスクリーンアプリケーションの作成
  • dialogs、progress-barの生成
  • シェルのようなインタラクティブな入出力
  • 補完
  • clipboardの管理
  • 出力テキスト色の変更
  • シンタックスハイライト
  • EmacsとViのキーバインディング
  • マウスカーソルによる操作
  • 各種非同期処理

上記のような機能がサポートされており、コンソール上で様々なツールをPythonを利用して作成する事ができる。

github.com

Pure Pythonで書かれている事から、UnixWindowsなどクロスプラットフォームで動作する事、数の多いPyhtonライブラリを資産として利用できる事が強いメリットとして挙げられる。

    
examplesに様々なCLIアプリケーションの例とtutorialが示されており、小さなCLIツールから作成を始める事ができる。

examples: python-prompt-toolkit/examples at master · prompt-toolkit/python-prompt-toolkit · GitHub

 
例えば、シンプルなプログレスバーは、以下程のスクリプトで実装する事ができるだけでなく、プログレスバーの色付けや非同期化、ネスト化も簡易に実装する事ができる。

import time
from prompt_toolkit.shortcuts import ProgressBar

with ProgressBar() as pb:
    for i in pb(range(800)):
        time.sleep(.01)

f:id:vaaaaaanquish:20190706152405p:plainf:id:vaaaaaanquish:20190706152410p:plain


 
入出力のサポートでは、公式の補完の図が分かりやすいため引用しておく。

f:id:vaaaaaanquish:20190706152545p:plain
https://github.com/prompt-toolkit/python-prompt-toolkit

これらの機能は、Jupyter Notebookのに代表されるjupyter_console等でも利用されており、utilityとして多くのツールに導入されている。「xonsh」と呼ばれるシェルもまたこのptkをコアなライブラリとして作成されている。キーバインドや補完、表示の多くをこのptkに頼っている。

github.com

こういったシェル上でのI/O、Validate、表示形式を様々な形でサポートする他、ウィンドウサイズの計算も考慮されており、CLI上でのフルスクリーンアプリケーションを作成することも可能である。



フルスクリーンアプリケーションでは、CLIを縦横に分割したり、WebサイトのようにAlignさせたりといった画面上の計算をサポートしてくれる。

f:id:vaaaaaanquish:20190706140310p:plainf:id:vaaaaaanquish:20190706140317p:plain
vertical-split, horizontal-align

他にも例えばコンソール上で以下のようなものが「ウィジェット」としてサポートされており、Pythonで簡単に実装する事ができる。

f:id:vaaaaaanquish:20190706135444p:plainf:id:vaaaaaanquish:20190706135528p:plain
左:入力やチェックボックスを含むフルスクリーンダイアログアプリケーション
右:プログレスバーを含むフルスクリーンアプリケーション


  

ptkで作成されたツール

フルスクリーンアプリケーションを作成する機能を利用して、Python REPLの立ち位置のptpython、pure pythonなtmuxであるpymux、pure pythonvimであるpyvimをptkと同じ人物が作成している。

github.com

f:id:vaaaaaanquish:20190706154214p:plainf:id:vaaaaaanquish:20190706154204p:plain
ptkを利用した画面分割やフロートウィンドウ

github.com

github.com

f:id:vaaaaaanquish:20190706154001p:plainf:id:vaaaaaanquish:20190706154011p:plain
pyvim, pymux

 
ref: Gallery — prompt_toolkit 2.0.9 documentation


 

- ptkの描画を理解する -

ptkのテキスト描画の根幹はprint_formatted_textにある。また、画面の管理、描画はApplicationクラスが担当しており、こちらも重要である。

print_formatted_textは、組み込み関数のprint関数と互換性をもたせながら、クロスプラットフォーム上で色やフォーマットを変更して出力する関数である。多くの機能がこの関数によって表示されている。

シンプルに利用するには、以下のように書く。

from __future__ import unicode_literals, print_function
from prompt_toolkit import print_formatted_text
from prompt_toolkit.formatted_text import FormattedText
from prompt_toolkit.styles import Style

text = FormattedText([
    ('class:aaa', 'Hello\n'),
    ('class:bbb', 'ITALIC\n'),
    ('class:hoge', 'under line\n'),
    ('class:piyo', 'This is bold.')
])

style = Style.from_dict({
    'aaa': '#ff0066',
    'bbb': '#44ff00 italic',
    'hoge': 'underline',
    'puyo': 'bold'
})

print_formatted_text(text, style=style)

f:id:vaaaaaanquish:20190706182334p:plain
print_formatted_text

上記のようなPythonらしい記法以外にも、HTML風の記法や、ANSI、Pygments tokenを利用した書き方ができ、print_formatted_textはそれらをよしなに変換してくれる。ptk 1.xまでは完全にPygmentsに依存していたが、2.xから幅広い記法をサポートしたため、2.xや3.xを使うと良い。

ref: Printing (and using) formatted text — prompt_toolkit 2.0.7 documentation

 
クロスプラットフォームに対応する場合や、Class nameについてより詳しく知りたい場合は以下Stylingのページ、もしくは実際のコードを確認する。
ref: More about styling — prompt_toolkit 2.0.7 documentation
ref: python-prompt-toolkit/defaults.py at master · prompt-toolkit/python-prompt-toolkit · GitHub


 

Application

Applicationクラスは、上記までで紹介したようなI/O、style、keybindingやwidgetを含めて動くアプリケーションを生成するためのクラスである。ptkで作成されたxonsh、pyvim、pymuxがそうであるように、ptkで定義されたツールを一つのCLIアプリケーションの形にする事ができる。

このApplicationクラスがScreenオブジェクトをレンダリングする過程を知る事で、ptkを利用したCLIツール開発について理解する事ができる。

 

Screenオブジェクトのレンダリング

ApplicationクラスはScreenクラスを生成し画面を描画する。描画に利用されるwrite_to_screenメソッドの動きを見る前に、用語をまとめておく。

  • Container
    • HSplit VSplit FloatContainerなど画面上の場所を確保してくれるやつ
    • 中でもWindowというよしなに色々やってくれる便利なアダプターが楽で便利
      • WindowはUIControlを中に埋め込めるのでよく使う
  • UIControl
    • UI周りの動的な情報をContainerに渡してくれるやつ
    • 以下のようなものがある
      • 編集可能でスクロール可能なバッファ表示のためのBufferControl
      • テキスト表示のためのFormattedTextControl
      • More about Control
  • widgets
    • UIの中に入れる物として抽象化された概念、簡単にレイアウトが作れる
    • TextArea, Button, Frame, VerticalLine
  • Layout
    • 上記らをラップすると折返しや表示をよしなにやってくれる
    • styleもここに入る

概念の多きで言うと以下のようになる
Application > Screen > Layout > Container, UIControl > widgets
それぞれ継承したものを、再帰的に利用する事もできる(Containerの中にContainer等)。中でもContainerオブジェクトとUIControlオブジェクトを組み合わせた時の相互作用を理解する事は、Applicationを作る上で非常に重要である。

 
Screenオブジェクトが持つRendererオブジェクトがContainerを特定の画面上にレンダリングする流れは、シンプルに書くと以下のようになる。

  1. Containerが持つwrite_to_screenメソッドが一定の大きさの長方形を生成
  2. UIContentがスペースを計算
  3. RendererがLayoutを参考にContainerを正しい位置に描画していく

ref: https://python-prompt-toolkit.readthedocs.io/en/2.0.9/pages/advanced_topics/rendering_flow.html

上記用語と流れを理解した上で、シンプルなApplicationを作成してみると分かりやすい。

from prompt_toolkit import Application
from prompt_toolkit.buffer import Buffer
from prompt_toolkit.layout.containers import VSplit, Window
from prompt_toolkit.layout.controls import BufferControl, FormattedTextControl
from prompt_toolkit.layout.layout import Layout

buffer1 = Buffer()
root_container = VSplit([
    Window(content=BufferControl(buffer=buffer1)),
    Window(width=1, char='|'),
    Window(content=FormattedTextControl(text='Hello world')),
])

layout = Layout(root_container)

app = Application(layout=layout, full_screen=True)
app.run()

Vsplit(縦に2分割するcontainer)の中にWindow(入出力可能な便利container)を3つ入れている。それをLayerにいれてApplicationとしている。結果はフルスクリーンで以下のように表示される。

f:id:vaaaaaanquish:20190706211619p:plain
左右に画面が分割され、左画面がテキスト入力可能なBufferControl、右画面がText表示になる様子

 
ref: Building full screen applications — prompt_toolkit 2.0.7 documentation
source: python-prompt-toolkit/application.py at master · prompt-toolkit/python-prompt-toolkit · GitHub


 

KeybindingとFilter

さらに画面の操作を円滑にするKeybindingsやbottom toolbarを設置する事もできる。また各動作や状態をhookして動かせるFilterクラスも存在する。
ここでは省略するが、各々設置する事ができるので把握しておくと良い。

keybindings: More about key bindings — prompt_toolkit 2.0.7 documentation
filter: Filters — prompt_toolkit 2.0.7 documentation


 

関連

私自身もptkへのコミット他、xonshなどの開発に寄与している。
その他、破壊的変更の多かったptk 1.xからptk 2.xへの移行のための記事を書いてあるので参考にできると思う。
vaaaaaanquish.hatenablog.com

フルスクリーンアプリケーションで雑に時計とかも作ったので参考に
f:id:vaaaaaanquish:20190706213406p:plain
github.com


おわりに

ptkは1.xから2.xへの破壊的変更こそ多かったものの、3.xに向けてpython 3.6から導入されたasync/awitを利用した非同期処理や、レンダラの強化、各クラスの相互作用の整理が進んでおり、非常に使いやすくなりつつある。

Pythonの資産が扱えるのが非常に大きく、MLエンジンを載せた補完やCLIと連動するWebアプリケーション等が出てくる事を期待している。


 

xonsh拡張のxontribをつくる

- xontribとは -

xontribはxonshの拡張ライブラリ群である。
公開されているxontribを導入するには以下のようにライブラリをインストールしロードする。

pip install xontrib-hoge
xontrib load hoge

xontrib loadをxonshrcに記述しておくことで、xonshセッション起動時にロードすることもできる。

1年程前に公開されていた各xontribの紹介は以下記事に記載している。
vaaaaaanquish.hatenablog.com

 
xontribはpythonスクリプトもしくはxshスクリプトによって記載されたmoduleで、xontrib以下のnamespaceに配置される。tabcomplete拡張や外部Pythonライブラリのサポート、xonshモジュールのoverride等が作成、公開されており、その作成方法については以下チュートリアルに概要が書かれている。
xon.sh
本記事はこちらを参考にし、xontribを作成していくものである。


 

- cookiecutterによるtemplete利用 -

xontribのテンプレートとしてcookiecutterを利用する事ができる。

github.com


以下コマンドで対話的にライブラリの雛形が作成できる。

pip install cookiecutter
cookiecutter https://github.com/laerus/cookiecutter-xontrib

対話の内容は以下のようになる

full_name [Your name]: vaaaaanquish
email [Your address email]: sample@exm.com
github_username [Your github username]: 6syun9
project_name [Name of the project]: xontrib-sample    # setup.pyに記載されるproject
project_slug [xontrib-samplelib]: sample              # xontrib loadするモジュール名
project_short_description [A short description of the project]: This is test
version [0.1.0]: 0.0.1

上記cookiecutterコマンドによってsampleディレクトリが作成される。

$ tree sample
.
├── LICENSE
├── README.rst
├── setup.py
└── xontrib
    └── sample.xsh

作成した後は、sample.xshを編集するだけである。
作成された時点ではsample.xshには内容がないため、以下のように変更する。

import xonsh

__all__ = ()
__version__ = '0.0.1'

print('hello xontrib sample')

xontribを読み込んだ時点でstringをoutputするサンプルとなる。
こちらを手元にinstallし、xontrib loadで読み込んでみる。
sampleディレクトリ内にあるsetup.pyを利用する。

$ python setup.py install
$ xontrib load sample
hello xontrib sample

ここから中身を拡充していくだけ。

 

- pypiで公開し使ってもらう -

pip installで導入できるようにpypiに登録する。
(アカウント作成が必要なので事前に行なう)
PyPI – the Python Package Index · PyPI

twinを利用したアップロード

distを作成し公開するためのツールを導入する

pip install wheel twine

先程の作成したパッケージのroot dirのsetup.pyを利用してパッケージを作成、アップロードする。

python setup.py sdist bdist_wheel
twine upload --repository-url https://upload.pypi.org/legacy/ dist/*

数分後くらいにちゃんと登録されて pip install xontrib-sample できるようになる(この記事は解説なので実際にはやってない)。

pypiのREADMEや必要な外部ライブラリを定義する

最低限以下のようにしておくとなんとかなる。
https://github.com/6syun9/xontrib-readable-traceback/blob/master/setup.py

rstじゃなくてmdじゃないとgithubレンダリングしてくれない場合があるので、私はmdで管理している。

 

xonshのDocumentationに追加してもらう

公開されているxontribはxonshのドキュメント上に載せてもらう事ができる。
xon.sh

また xontrib list コマンドでも表示されるようになる。

こちらに載せてもらうには、本家xonshにpull requestsを送る必要がある。
例えば、以下のように「新しいxontrib作ったよ!」と xonsh/xontribs.json を編集した旨を伝えるpull requestsを作成する。
github.com

xonshにコントリビュートする時は news/TEMPLETE.rst をコピーして news/{{ブランチ名}}.rst なるファイルを作って、更新ログを書くルールになっているのでそちらも追加して終了。詳しくは以下Developer’s Guideを参照。

Developer’s Guide — xonsh 0.9.6 documentation


 

- xontrib作成において参考にできる所 -

「こういうの作ってみたいなあ…」と思った時に参考にできるよう私のメモとして記載しておく。

あるタイミングでコマンドを発火させたい

tab補完を拡張したい

promptの見た目を弄りたい

コマンドの保存、読み込み、引数実装

コマンドの出力結果を使いたい

xonsh自体の機能をoverrideしたい

別threadの実装

画像やgraphを表示したい

サブウインドウやalertを表示したい

ネタがない


 

- おわりに -

このメモとGoogleが使えればxontribが作れると思うので、みなさんもxontributerになりましょう。


 

フォロワーを管理するためだけのWebアプリを作った

- はじめに -

フォロワーも増えてきて大体毎月n*100単位でフォロワーが変動するようになってきて、巷のフォロー管理ツールじゃ全然要求を満たせないので、自作する体でGWに友人と1日ハッカソンみたいなのを開いて勢いだけで作った話。

「100万人とかフォロワーが居る訳じゃないけど1000から1万くらいの "小さな界隈のアルファ" 」は多く居て、皆どうやってTwitterを見てるのか知りたいのでまず自分から。

自分がTwitterを見ている方法や、フォロバに関する個人的な意見など、偏見を含む記事。ほぼポエム


 

- 作ったもの -

自身の要件を満たす物を一旦作った。

  • 裏側はpandasが走る事でquery検索やsort、別途の分析、拡張が容易
  • 画面内でqueryの調整、閲覧、フォロー、アンフォロー、リスト管理ができる
  • tableに最低限の情報が羅列されている
  • queryを保存したりDB側にも更新機構がある

f:id:vaaaaaanquish:20190518145931p:plain:w400
つくったもの

formにpandasのquery条件やsort条件が入れられるようになっていて、その場でuserテーブルを見るのはもちろん、よしなにqueryを作ってURLパラメータで保存しておけば、同条件でいろいろ見られる。

例えば

# 直近のフォロワー100件をフォローされた順に表示
# http://127.0.0.1:5000?nq=followed&tcrq=yy-mm-dd&tltq=yy-mm-dd&fcrq=yy-mm-dd&fltq=yy-mm-dd&sorq=follower_number&asq=True&samq=100
# 片思いでフォローしてるけど最新のつぶやきが1年以上前の人をアカウント生成日順に表示
# http://127.0.0.1:5000?nq=~followed and following&tcrq=yy-mm-dd&tltq=2018-05-01&fcrq=yy-mm-dd&fltq=yy-mm-dd&sorq=created_at&asq=True&samq=100
# bioに機械学習と書いてあり2018年以降に作られたアカウントでフォロワーが1000以上の人をフォロワー数順に
# http://127.0.0.1:5000?nq=description.str.contains("機械学習") and followers_count > 1000&tcrq=yy-mm-dd&tltq=yy-mm-dd&fcrq=2018-01-01&fltq=yy-mm-dd&sorq=followers_count&asq=True&samq=100

これだけ色んな情報を持っておいていつでも見れるようにしておけば、完全にフィードアウトしてしまって定期botと化してしまったアカウントとか、フォロバ即フォロー解除でフォロワー増強するクソ野郎だとかを定期的に見つけてフォロー外していったりできると思う。ランダムサンプリングする事もできるので、定期的に見てれば長期的にフォロワーが整理されていくだろうという仕組み。


 

- フォロバ作業 -

実際にこのツールをVivaldiのWebパネルに登録して、擬似的な2画面でフォロバ作業をやってみている様子。

f:id:vaaaaaanquish:20190518153301g:plain:w500
なんか変なアカウントをフォロバして外している様子

ここからはかなり偏見が混じるが、私のフォローに関する話とかを書いておく。
多分これを書かないと、管理ツールの思想が理解しづらいと思う。

 

リスト管理とフォロー

たまに「めっちゃフォローしてますよね」と言われるが、私はただTwitterとリスト管理が好きなだけ。
エゴサは出来る限りの検索queryと正規表現を作ってあるし、リストは34つある。
フォロバすると、「ここ半年」という名前の「フォローしてから半年経ってない人が入るリスト」におうちサーバが自動的に入れてくれる。
そのリストは割と見ていて、半年後にはそのリスト内の様子から大体どこかのリストには振り分けられている仕組みでなんとかやっている。
ただそれを色んな方法で見ているだけ。

 
リスト分けは「つぶやく頻度」なんかがわかりやすくて、週一だとか月一ペースのフォロワーは「たまに見る」といったリストに入っている。

エンジニアは分かりやすく「フロントエンド」「インフラ」「アプリ」といった分野でも分けているし、「機械学習」のような自分の専門であれば興味度合いを大体で3段階に分けて「げきつよ」「つよい」「興味ある」みたいに分けている(もう2段階くらい必要だなと最近は思っている)。もちろん技術全体的にすごい人は「高レベル人材」「好きな神」なるリストがあるし、「CTO」とか「経営」とか「採用人事公式アカウント」とかにも分けてある。「オタク」とか「あったことある」とか「仕事したことある」とかもあって、それぞれ複数のリストにまたがって入っている人が居る形になっている。

幸せになってほしい人を入れる「幸せになってほしい人リスト」もあり、こちらは公開している。
昔いろいろ言われたリストだが幸せになってほしい。


今回作ったものを奥さんに見せたら「そんなアプリ作ったの…?暇じゃん…」と言われたし、自分でもアホだと思うけど、検索queryとか考えたり特定界隈のバーストを見るのが楽しいので仕方ない。
月に数百人にフォローされても返せるのには、その辺りが好きなのが起因してると思う。


 

どんな人をフォローしているか

リスト管理が好きなので、フォロバの基準はアホみたいに低い。
最近はリスト内で見れないものがあると悔しいのでブロックもミュートも0人という、一部から見たら異常な運用だと思う。

 
実際フォロバする作業をしていくと、低いとはいえ「フォロバしない人」の基準自体はある。
例えば「留学、英会話をマスター、世界一周、恋愛テクニック、FX、FXツール、独立、投資、Wワーク、小遣い稼ぎ、フリーランス、自由、起業、複業、未経験、相互フォロー、エロ、〜〜と繋がりたい、DM、LINE@」辺りの単語がbioに入ってると、注意視度合いが上がる。大体フォロバした瞬間にDMでスパムやアフィリエイトリンク、情報商材のリンクを自動で送ってくる経験則から来てると思う。数撃ちゃ当たると思いやがってと思いつつ、フォロバ即商材DMを貰うと「俺もまだまだだな…」ってなる。

中でもエロだとか「人気のツイート格言まとめ」「話題の画像、人気画像まとめ」といった転載、パクツイbotはそもそも信念としてフォローしていないが、仮にTwitter公式認証のついたアカウントでもパクツイや商材DMと同じことやってくるアカウントはあるので疑念が強い。

f:id:vaaaaaanquish:20190518175056p:plain:w400
すぐDM送ってくるタイプの
 
他にも「月間100万PVみたいなブログアピールがすごい」とか「〜〜の中の人です」系は、実際直近のツイートを見に行くと完全な宣伝botと化していて、見る必要性がないなと思ってフォローしないパターンが多い。その点、最新Tweetの投稿アプリがTweetbotだったりすると、ツイートを舐めなくても直感的に分かりやすい。

f:id:vaaaaaanquish:20190518175418p:plain:w400
amebloまで見に行くとわかる情報商材


bioという意味では「エンジニア」も怪しい単語に最近入りつつある。
「アプリ作ってます」と書いてあるのでフォロバしてリストに入れようかなと思うと一生自アプリを宣伝するbotだったり、「初心者エンジニアがプログラミング教室通って独立して月50万」みたいなリンクを送ってくるパターンは、体感でかなり増えている。後は、捨て垢で質問だけするためにフォローとか。QiitaとかTeratailとかQuoraとかでやると良いと思う。


酷い乱数IDだとか、ツイートが0~10、アイコンもヘッダーもプロフィールもなしとか、情報出さずに鍵垢とかは普通に判断に困る。
IDでは、「(@hoge)の移行垢、サブ垢」とかも危険で、実際はサブでも何でもないパターンがあってこれも判断に困る。
実際「ばんくし」で検索した時に出てくるアカウントはvaaaaanquish以外私ではないし…


「興味あり、憧れて、勉強中、初心者」辺りもレベル差が激しいだけでなく、「同じ初心者を釣って相互フォローになって商材を送るアカウント」である場合があるので、本当に勉強してるかとかは見ないと大体「俺もまだまだだな…」ってなる。

 
ただ、これらは私が経験的に警戒しているというだけで、「こうしているとフォロバされない」とかでもなければ「こうした方が良い」とか「こうあるべき」という話ではない。もちろん自動DMのアカウントを作るなんて、プログラミングを2日くらいやれば誰でも出来るだろうし、そういうTwitterアカウント運用ツールが存在する事も知っていて、それ自体が悪いとは一切思ってない。
どれだけ謎めいたアカウントでも、アカウント作成日が2009年とか、最新ツイートが完全に人に寄っていたりだとか、明らかに転載でない画像投稿があると「ん〜、まあいいか」となる場合もあるし、知ってる人だと何もなくてもフォローしたりする。警戒する単語があったからどうするというより、総合的に見て私がモデルになって判断するというだけである。


 
積極的な方で言うと、最近は「オタク全面出しアカウント」がむしろめちゃくちゃ信頼できる。
2010年〜2015年くらいのTwitterは、アニメアイコンのアカウントが溢れかえっていて色んな所で争っていたり騒動を起こしていた事もあって「え、あの人フォローしてるん?」みたいな事を言われる機会も多かったのでフォローを厳選する必要は正直あったけど、良くも悪くも年齢層の変化か頓珍漢な人が減ったか自身のアンテナの変化か、〇〇好きをアピールしてくれている人の方がリスト分けもしやすいし、まだ見ていられるので閾値がグッと下がる。政治や意識だけ高い人より好きな事喋ってる人は良いよね…という事かもしれない。

全部車!ガジェット!みたいな雰囲気だったり、アイコンとヘッダーとbioが統一的な(概念的に近いような)アカウントだったり、得意な事が統一的に書いてあったり、〇〇株式会社のエンジニアみたいな人もフォロバする時の安心感がすごいし、何よりリストに入れやすい。


安心感的なところでいうと、はてな等のブログ、プロフィールサイト、Amazon wishlist、Qiita、Kaggle、競プロ、…他サービスと共通のIDが見えているとスパムアカウントではないだろうなと思うし、IDと同じドメインを持って運用しているアカウントだと即フォロバできるくらいの勢いがある(最悪whoisできる)。画像投稿もあると人柄が見えて実は嬉しい。

f:id:vaaaaaanquish:20190518182440p:plain:w400
安心感があるアカウント

後は「bioが単発のダジャレで面白い」とか「幾何学模様アイコン」「数式、文字アイコン」「猫アイコン」とか「bioにvimと書いてある」とか感覚的に良いと思っている指標はいくつかあるけど、大体その辺が見えていれば一旦信頼できると思っている。

最悪今回作ったツールみたいなやつでフォロー解除していけばいいだけなので、まずはフォローから。


 
こんな感じの概念を元に、フォロワーを見るためだけに作ったのが今回のツールである。


 

- できたもの -

GitHubにコードを置いた。

github.com

近年では、TwitterAPI規制が激しく、こんなしょうもないアプリを公開してペイできるほど甘い世界じゃなくなりつつある。API有料化自体は賛成しているが、ある程度定期的に使われて、収入源があるAppじゃないと厳しいという現実はある。


なので、TwitterAPI keyとClient Secretを自身で取ってこれて、Python実行環境が作れる人に絞って一旦公開する運びとなった。
アホみたいなTwitter連携アプリがガンガンリリースされていたあの頃に戻りたいという気持ちもある。

 

概念

概念として3つ大事な要素を考えて作った。

  • バックエンド何も考えたくないのでdata_managerなる神を1つ作ってAPIやファイル操作、pandas I/Oを一任したい
  • 主目的であるWebAppでフォロバ作業はしたい
  • 先述のように自分がどんな基準でフォローするかを考慮しながら調整、分析のPDCAを回したい
    • ipynbで分析できると嬉しい
    • MLモデル作れるようになりたい
    • 更新、拡張可能な形式だと嬉しい

f:id:vaaaaaanquish:20190518191743p:plain:w500
ハッカソンで出した概念図的なやつ

pandasで管理されてるので、そのファイルさえ読み込めば直接ipynbで分析したりできるのが今回の目的に沿ってると思う。
私のfollow情報を用いた簡易分析例のipynbはこんな感じ
twitter_manager/簡単に見ていくやつ.ipynb at master · vaaaaanquish/twitter_manager · GitHub

f:id:vaaaaaanquish:20190518214250p:plain:w650
こういうのがやりたかったんだよな
f:id:vaaaaaanquish:20190518183254p:plain:w400
フォロワーbioとtwから生成したワードクラウド

jsonファイルの管理が適当なのでDB建てるなり徐々に綺麗になっていくと思う。

 

- おわりに -

書き始めてから気付いたけど、技術的な事はハッカソンでやり切ってるので特に目立った工夫もないし、技術記事にする必要はなかったかも…

邪念な文章な気がするけど、はてなブログは大体これで良いと思う。


2時間くらいフォロバ作業してたらソッコーAPI枯れたので悲しい図


 

Pytorchでpretrainモデルを用いた画像向けSiameseNetworkのメモ

- はじめに -

NIPS 2016のSiamese Neural Networks for One-shot Image Recognitionを参考に、画像の距離学習を行う。

Siamese Networkは、各クラスの画像量にバラつきがあり、一部クラスが数枚しかない学習データでも上手く学習させられるネットワークである。

「特徴量同士の距離が近い画像」を探す事で、分類や検索といった問題を解くために利用できる。
やりたい事としては以下のような感じ。

f:id:vaaaaaanquish:20190223205410p:plain
sample (food-101 datasetより)


本記事は、以下の記事で利用したcnn_finetuneライブラリを用いて、pnasnet5largeをpretrainとした、画像の距離学習のtrain, testを行うメモである。

vaaaaaanquish.hatenablog.com

また、最後にfood-101データセットを用いた結果の例を示す。

- train -

距離関数とSiamese Networkを定義し、trainする。

最も簡単な構成で、pre trainから実現する。


 

距離関数の設計

距離関数には、古典的なContrastiveLossを利用する。
より一般的には、この距離関数の設定がtest時の精度に大きく影響するため、対象となるデータやモデルの大きさに応じて、変更すると良い。

import torch

class ContrastiveLoss(torch.nn.Module):
    def __init__(self, margin=1.0):
        super(ContrastiveLoss, self).__init__()
        self.margin = margin

    def forward(self, x0, x1, y):
        diff = x0 - x1
        dist_sq = torch.sum(torch.pow(diff, 2), 1)
        dist = torch.sqrt(dist_sq)
        mdist = self.margin - dist
        dist = torch.clamp(mdist, min=0.0)
        loss = y * dist_sq + (1 - y) * torch.pow(dist, 2)
        loss = torch.sum(loss) / 2.0 / x0.size()[0]
        return loss

参考: Chopra, Sumit, Raia Hadsell, and Yann LeCun. ”Learning a similarity metric discriminatively, with application to face verification.”, CVPR 2005.


 

モデルの定義

前述した通り、cnn_finetuneライブラリを用いてpnasnet5largeのimagenet pretrainモデルを導入し、そのネットワークの中間層出力を用いてSiamese Networkを構築する。

f:id:vaaaaaanquish:20190223182330p:plain:w400
Siamese Neural Networks for One-shot Image Recognitionより

上図のInput, hidden layerをpnasnetの特徴量抽出部分に変更し、後段の層も深くしてみる。

from cnn_finetune import make_model
import torch.nn as nn

resize = (256, 256)  # 入力画像サイズ

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

    def forward(self, x):
        return x


def make_pnas():
    model = make_model('pnasnet5large', pretrained=True, input_size=resize)
    model.module._classifier = Identity()
    return model


class SiameseNetwork(nn.Module):
    def __init__(self):
        super(SiameseNetwork, self).__init__()
        self.cnn = nn.Sequential(
                make_pnas(),
                nn.Linear(4320,500),
                nn.ReLU(inplace=True),
                nn.Linear(500, 10),
                nn.Linear(10, 2))

    def forward(self, input1, input2):
        output1 = self.cnn(input1)
        output2 = self.cnn(input2)
        return output1, output2

SiameseNetworkは別のネットワークから2つの出力を行う形となる。


 

データ読み込み部の設計

学習のためのデータセットとしては、前回記事同様、以下のようなheader付きのhogehoge.csvなる「学習画像の名前(ImageName)」と「学習画像に対するラベル(ImageLabel)」がある想定。

ImageName,ImageLabel
0001.jpg,1
0002.jpg,3
0003.jpg,0
0004.jpg,3


画像の変形によるアップサンプリングを行いながら、等確率で「同じラベルの画像」「別ラベルの画像」を返す実装を以下に示す。
transformには、pretrainで利用されているImageNet画像の平均、分散値を利用する。

import torch
import os
import pandas as pd
import pickle
import torchvision.transforms as transforms
from torch.utils.data import Dataset
from PIL import Image


resize = (256, 256)  # 入力画像サイズ
trans= [transforms.Resize(resize),
            transforms.ColorJitter(brightness=0.3, contrast=0.3, saturation=0.3, hue=0.3),
            transforms.RandomHorizontalFlip(),
            transforms.RandomAffine(0.3, shear=0.3),
            transforms.RandomRotation(degrees=30),
            transforms.ToTensor(),
            transforms.Normalize((0.485, 0.456, 0.406), (0.229, 0.224, 0.225))]


class MyDataSet(Dataset):
    def __init__(self, root_dir):
        self.transform = transforms.Compose(trans)
        self.train_df = pd.read_csv('hogehoge.csv')
        self.root_dir = './train'
        self.images = list(self.train_df.ImageName.unique())
        self.labels = list(self.train_df.ImageLabel.unique())
      
    def __len__(self):
        return len(self.images)
    
    def image_open(self,t):
        image = Image.open( os.path.join(self.root_dir, t) )  
        return image.convert('RGB')

    def __getitem__(self, idx):
        # labelに対して画像を選択
        source_label = self.labels[idx]
        source_image_name = self.train_df.query('ImageLabel=="{}"'.format(source_label)).sample(1)['Image'].iloc[0]
        
        # labelに対して同じラベル、違うラベルをそれぞれ50%で返す
        if random.randint(0,100)<50:
            target_image_name = self.train_df.query('ImageLabel=="{}"'.format(source_label)).sample(1)['Image'].iloc[0]
            label = 1
        else:
            target_image_name = self.train_df.query('ImageLabel!="{}"'.format(source_label)).sample(1)['Image'].iloc[0]
            label = 0
        
        # 画像ロード
        source_image = self.image_open(source_image_name)
        target_image = self.image_open(target_image_name)
        
        return self.transform(target_image), self.transform(image), label

kwargs = {'num_workers': 1, 'pin_memory': True}    
train_set = MyDataSet()
train_loader = torch.utils.data.DataLoader(
    train_set, batch_size=32, shuffle=True, **kwargs)

実際は同じ画像を学習しないだとか、少ないラベルをなるべくサンプリングするだとか、現在のモデルの精度によって当たらないラベルを多めにサンプリングするなどすると良い。


 

optimizer

前回記事あまり考えずSGDを選択する。criterionには前述したContrastiveLossを利用する。

import torch.nn as nn
import torch.optim as optim
criterion = ContrastiveLoss()
optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9)

train run

学習を回す。

from torch.autograd import Variable

def train(epoch):
    model.train()
    for batch_idx, (x0, x1, labels) in enumerate(train_loader):
        labels = labels.float()
        # x0, x1, labels = x0.cuda(), x1.cuda(), labels.cuda()
        x0, x1, labels = Variable(x0), Variable(x1), Variable(labels)
        output1, output2 = model(x0, x1)
        loss = criterion(output1, output2, labels)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
    torch.save(model.state_dict(), '../model/model-epoch-%s.pth' % epoch)

model = SiameseNetwork()
# model.cuda()
for epoch in range(1, 100):
    train(epoch)


 

- test -

データが少なければ全てのデータを特徴量にして、全てのtrain, testデータに対して総当たりで距離を計算すれば良い。

高次元ベクトル検索ライブラリを利用して検索する。

もし保存したtrainのモデルを利用したい場合は、前回記事を参照。

データ読み込み部の設計

test用にデータを読み込むだけのクラスを定義する。

import os
import pandas as pd
import torchvision.transforms as transforms
from torch.utils.data import Dataset
from PIL import Image

tran = [transforms.Resize(resize),
            transforms.ToTensor(),
            transforms.Normalize((0.485, 0.456, 0.406), (0.229, 0.224, 0.225))]

class MyDataSet(Dataset):
    def __init__(self, root_path, csv, pg=False):
        self.df = pd.read_csv(csv)
        self.root_path = root_path
        self.images = list(self.df.ImageName.unique())
        self.transform = transforms.Compose(tran)
        
    def __len__(self):
        return len(self.images)
    
    def __getitem__(self, idx):
        image_name = self.images[idx]
        image = Image.open( os.path.join(self.root_path, image_name) )
        return self.transform(image), image_name
test_set = MyDataSet('./test/', './test.csv')
test_loader = torch.utils.data.DataLoader(test_set,batch_size=1, shuffle=False)
train_set = MyDataSet('./train/', './train.csv')
train_loader = torch.utils.data.DataLoader(train_set,batch_size=1, shuffle=False)

trainとtestどちらも特徴量化して、testの1画像をqueryとして、train内から近いものを探す形で、分類問題や検索問題を解く。
そのため、trainは全て特徴量にしておく。


 

test run

画像を特徴量にする。
もしデータが大量の場合は、DBに保存したり、後述する高次元ベクトル検索ライブラリを利用する。

import numpy as np
import torch.nn as nn

model = model.eval()
aap2d = nn.AdaptiveAvgPool2d(output_size=1)
# aap2d.cuda()

# - test images -
test_output = []
test_output_name = []
for batch_idx, (x,name) in enumerate(test_loader):
    # x = x.cuda()
    output = model.cnn[0].module.features(x)
    output = aap2d(output).squeeze()
    test_output.append(np.array(output.cpu().tolist()))
    test_output_name.append(name)

# - train images -
train_output = []
train_output_name = []
for batch_idx, (x,name) in enumerate(train_loader):
    # x = x.cuda()
    output = model.cnn[0].module.features(x)
    output = aap2d(output).squeeze()
    train_output.append(np.array(output.cpu().tolist()))
    train_output_name.append(name)


 

全てのデータから探索する

全てのデータ同士のユークリッド距離を計算してやるやつのサンプル。
kaggleなど全データが少ない時に使える。

for tx,tname in zip(test_output, test_output_name):
    dists = []
    for y in train_output:
        dists.append(np.linalg.norm(tx-y))
    # 小さい順に
    j = sorted(list(zip(dists, train_output_name)), key=lambda x: x[0])
    print(tname[0], j[:10])
    break


 

検索ライブラリに突っ込む

全部探索してたら時間がすごいかかるので、高次元ベクトル検索ライブラリを利用する方法がある。

検索では私は大体何も考えずにnmslibに突っ込む(導入が最も簡単)。

# pip install nmslib
# https://github.com/nmslib/nmslib/tree/master/python_bindings

import nmslib

index = nmslib.init(method='hnsw', space='cosinesimil')
index.addDataPointBatch(train_output)
index.createIndex({'post': 2}, print_progress=True)

ids, distances = index.knnQuery(test_output[0], k=19)
print(ids)
print(distances)


検索ライブラリの比較:qiita.com

その他、Yahoo! JAPAN社もNGTなるライブラリ出してるので要検討。

GitHub - yahoojapan/NGT: Neighborhood Graph and Tree for Indexing High-dimensional Data

food-101による例

一応例としてfood-101データセットを学習して、queryに対して近い画像を出したものを示す。

www.vision.ee.ethz.ch

記事上部のドーナッツは上手くいった例で実際はこんな感じになる。

f:id:vaaaaaanquish:20190223213304p:plain
実例 (food-101データセットより)

左がquery、近い順に左から画像が並んでいる。ドーナッツに関してはドーナッツが収集できており、似た画像が拾ってきていると言える。
2行目のアイスクリームをqueryにした場合、2番目の画像についたラベルはアップルパイであった。視覚的には似てるけど。
3行目はスパゲッティで検索しているが、実際に出てくる画像は「オムレツ、パエリア、リゾット、パスタ、ハンバーガー(イカリング)」となっている。気持ちはわかるけど。
まあ大体3行目のような感じで間違えるので、後処理なんかで色々やってやると良さそう。

 
food-101 datasetは画像の明暗が激しかったり、人がインスタに載せるような角度だったり、以下のような画像が混ざっていて厳しい所もあるので前処理も重要そうだという知見が得られた。

f:id:vaaaaaanquish:20190223213343p:plain
difficult images
上記は、左から「アイスクリーム」「アイスクリーム」「ハンバーガー」「ピザ」「ブレッドプディング」である。わからん。

 
パスタやステーキ、餃子、寿司、ティラミス、マカロン、…といった他物体や人が映る可能性の低いものもあるので、タスクに応じて、カテゴリを絞ったり細分化されている所をマージするなどすると良さそう。


 

- おわりに -

Pytorchのpretrainモデルを利用したSiamese Networkを構築した。

verification modelの拡張としてtripret lossを利用したり、partなモデルに拡張してより細かな物体同士の距離を用いたりできるので、いつか記事として書く。GitHubにも上げる。

特に他意はない。