Stimulator

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

Python moduleがない場合に自動でpip installする

- はじめに -

この記事は、Xonsh Advent Calendar 2018 - Qiita 10日目の記事です。

複数のサーバを業務で利用おり、それぞれのサーバ環境設定のためpythonパッケージもインストールする作業が必要だが、別サーバからpip freezeするほどでもない(ちょっとした作業サーバなど)という時にjupyter_configや.xonshrc等の設定ファイルに自動インストールするように書いておけば良いのではと思って書いたメモです。

端的にはimportが失敗したらpip moduleを使ってインストールしてやる仕組みを作ります。
また加えて「PyPiに登録されているパッケージのリストを取得し、その中になければ諦めるという仕組み」「xontribの自動インストール」も示します。

 

- 動的なpip install -

python3にはpipモジュールが含まれているため、以下のような形で動的なインストールを実装します。
as構文を利用したい場合もあるかと思いますので、globalsに入れるように書いておきます。

from pip._internal import main as _main
import importlib

def _import(name, module, ver=None):
    try:
        globals()[name] = importlib.import_module(module)
    except ImportError:
        try:
            if ver is None:
                _main(['install', module])
            else:
                _main(['install', '{}=={}'.format(module, ver)])
            globals()[name] = importlib.import_module(module)
        except:
            print("can't import: {}".format(module))

_import('pd','pandas', '0.22.0')
print(pd)

pandasをアンインストールした状態でもスクリプト実行時にインストールが行われます。


試しに、上記スクリプトpythonが動くshellであるxonshの設定ファイルに書き込んでみます。
pandas 消してもxonsh起動時になければ新しくインストールされています。
f:id:vaaaaaanquish:20181202210419p:plain

たぶんjupyterもconfigに書けば同じことができると思います。


参考:pip - Installing python module within code - Stack Overflow

 

- PyPiに登録されているパッケージのリストを取得する -

上記に記載の仕組みに加え「自身が作成しているオリジナルのモジュールもpipで入れたい」などの要望と入り混じった時、PyPiに登録されているライブラリかどうかを判定する必要が稀に出てきます。
PyPiに登録されているライブラリ一覧を取得するスクリプトを加えたものを以下に示します。

if not os.path.exists('~/.pypilist'):
    import xmlrpc.client as xmlrpclib
    client = xmlrpclib.ServerProxy('https://pypi.python.org/pypi')
    packages = client.list_packages()
    with open('~/.pypilist', 'w') as f:
        f.write('\n'.join(packages))
pypilist = open('~/.pypilist', 'r').read().split('\n')
if module in pypilist:
     print('not found pypi')

また、月更新くらいでpypiにあるパッケージやバージョンを見るようにしておけば、rcファイル等に書く時にも便利になるでしょう。

参考:JSON API for PyPi - how to list packages? - Stack Overflow


 

- xonshrcに書くために -

xonshアドベントカレンダーの記事ですので、xonshに書くためのtipsも示します。

xontribをインストールする

xontribも先程と同様にインストールできますが、xontribの場合、xontrib loadなるコマンドが用意されています。

実装を見ると、xontribs_load関数で読み込めなかった場合、prompt_xontrib_install関数で生成した以下のような文字列を返す実装になっています。

The following xontribs are enabled but not installed:
   z
To install them run
    xpip install xontrib-z

参考:xonsh/xontribs.py at master · xonsh/xonsh · GitHub

文字列返しても仕方ないので、ロード部分となるupdate_contextをoverrideして自動でpipするようにしてみます。

from pip._internal import main as _main
import xonsh.xontribs
from xonsh.xontribs import xontrib_context, update_context

def _update_context(name, ctx=None):
    if ctx is None:
        ctx = builtins.__xonsh__.ctx
    if not hasattr(update_context, "bad_imports"):
        _update_context.bad_imports = []
    modctx = xontrib_context(name)
    if modctx is None:
        _main(['install', 'xontrib-{}'.format(name)])
        remodctx = xontrib_context(name)
        if remodctx is None:
            _update_context.bad_imports.append(name)
            return ctx
        return ctx.update(remodctx)
    return ctx.update(modctx)

xonsh.xontribs.update_context = _update_context

xonshにはxpipなるpip wrapperがあり、xonsh環境配下にpip packageをinstallし、他env環境などから切り離すことが出来ます。
そちらを利用する事でxonshのアップグレードにも対応できます。
そちらはpip._internal.mainからは利用できないため、その場合は以下のように_mainでなくsubprocessなどでインストールしてやれば良いでしょう。

import subprocess

cmd = "xonsh -c 'xpip install xontrib-{}'".format(name)
subprocess.call(cmd, shell=True)

 
これらを組み合わせ、xonshrcに書いた上でxontrib loadコマンドを実行すれば、勝手にpipでパッケージを取ってくることが出来ます。
f:id:vaaaaaanquish:20181202210543p:plain

xontrib-readable-tracebackは以前XonshのException発生時のtracebackを見やすくする - Stimulatorなる記事で私が作成したライブラリです。結果として1/0のException messageが短くキレイに色付けされ表示されています。



 

コマンドをxonshrc内でインストールする

コマンドの有無も確認し、なければインストールするようにしておくと便利です。
pythonではshutil.whichメソッドがそれらをサポートしてくれているので利用すると良いでしょう。

「コマンドが無ければ〜」というサンプルを以下に示します。

import shutil

if shutil.which( COMMAND_NAME ) is None:
    # install script


xonshではpython以外にshell scriptもほぼ同等に動くわけなので、何も考えず最新版をインストールシェルスクリプトを書くだけです。

例えば私がよく使っているpecoなら以下をそのまま書くだけ。
Linux に最新版の peco をインストールするシェルスクリプト - Qiita


 

- おわりに -

これでxonsh周りの実行環境作成は、xonshrcだけで完結すると思います。

個人的には以下のlazy load等も組み合わせながら、xonshの起動を高速化しつつ、最低限のラインとして使っています。
vaaaaaanquish.hatenablog.com


xonshアドベントカレンダーのほうはまだ空きがあるみたいなので、よければ是非。
qiita.com


 

macOSの通知リストをpython経由で取得しShellに流す

- はじめに -

macOSを使っていると、通知(Notifications)に色んな情報が集約される。
メールが来ただのSlackの更新だの、アプリのアップデート等がそれにあたる。
f:id:vaaaaaanquish:20181124204404p:plain
「OS通知を定期的に取得してシェルのbottom barとかに表示してやれば、作業中でも社内チャットやらメールの通知とかを厳選して気付けたりするしサイコーなのでは?」という所を目的にした作業ログを示すものである。

ちなみにこの記事は、Xonsh Advent Calendar 2018 - Qiitaの7日目の記事。

- macOSの通知の取得 -

2014年くらいにHigh Serriaになった時、ユーザ向けにもAPIが公開されたNotifications。それらを叩けるterminal-notifierや、そのPython wrapperとなるpyncなどが出てきて、ユーザが通知欄に任意のメッセージを送る事は難しくなくなった。
GitHub - julienXX/terminal-notifier: Send User Notifications on macOS from the command-line.
GitHub - SeTeM/pync: Python wrapper for Mac OS 10.8 Notification Center


macOSが提供するAPIでは「アプリXから通知を送る」「アプリXから送った通知を消す」「アプリXが送った通知のリストを取得する」が利用できるが、これらを利用しても「全てのアプリから送られた全ての通知を取得する」事はできない(記事執筆時、2018/11/24)。

完全な情報を取得する事はできないが、以下のpatreon記事によると、システムデータベースには通知の断片ログが残っているようである。patreon主はSignalや各所登壇で良く見るあのPatrick Wardle氏である。
www.patreon.com
Macのsystemのpythonに含まれるFoundationクラスを利用する事でこのログを読むことができる。*1

 
上記記事内のdumpNotificationDB.pyをベースに、通知毎のタイトルと内容と日付、アプリ名を取得し表示するスクリプトを以下に示す。

# -*- coding: utf-8 -*-
# /usr/bin/python (macOS system python)
import os
import sys
import sqlite3
import datetime
import tempfile
import Foundation


def parse_req(req):
    # reqをparseしtitleとbodyのみ取得する
    res = {}
    for x in str(req).split(';'):
        if 'body' in x or 'titl' in x:
            d = x.replace('{','').strip().split(' = ')
            res[d[0]] = d[1].replace('"','')
    return res


def get_notif_json():
    # DBファイルにアクセスしparseして返す
    notificationDB = os.path.realpath(
            tempfile.gettempdir() + '/../0/com.apple.notificationcenter/db2/db')
    conn = sqlite3.connect(notificationDB)
    conn.row_factory = sqlite3.Row
    cursor = conn.execute("SELECT data from record");

    res_j = []
    for row in cursor:
        plist, fmt, err = \
                Foundation.NSPropertyListSerialization.propertyListFromData_mutabilityOption_format_errorDescription_(
                buffer(row[0]),
                Foundation.NSPropertyListMutableContainers,
                None, None)
        if err is not None:
            continue

        notif_dic = {}
        for key, value in plist.iteritems() :
            if key == 'date':
                notif_dic['date'] = Foundation.NSDate.alloc().initWithTimeIntervalSinceReferenceDate_(value)
            if key == 'req':
                req = parse_req(value)
                if 'titl' in req.keys():
                    notif_dic['title'] = req['titl']
                notif_dic['body'] = req['body']
            elif key == 'app':
                notif_dic['app'] = value
        res_j.append(notif_dic)
    return res_j

# データ取得
d = get_notif_json()
# 日付順ソート
d = sorted(d, key=lambda x: x['date'])
# 表示
import codecs
for x in d:
    print x['date']
    print x['app']
    print codecs.decode(x.get('title','no title').lower(), 'unicode-escape')
    print codecs.decode(x['body'].lower(), 'unicode-escape')
    print ''

macOSのpython2系を利用するため、codecsを利用してunicodeをdecodeしたり、printがアレだったりするが仕方ない。

 
先述の通り、system-pyhtonのFoundationが必要なため、/usr/bin/pythonを利用して実行する。

$ /usr/bin/python get_notification.py

2018-11-14 14:58:05 +0000
_SYSTEM_CENTER_:com.apple.battery-monitor
バッテリー残量低下
電源コンセントに接続しない場合、お使いのmacは間もなくスリープ状態に入ります。

2018-11-16 23:27:59 +0000
com.adobe.acc.AdobeCreativeCloud
creative cloud
4 個のアプリのアップデートがあります

2018-11-21 10:08:50 +0000
com.tinyspeck.slackmacgap
teamy
miku : 完全に未来人扱いされてますね笑

2018-11-21 10:10:36 +0000
com.tinyspeck.slackmacgap
teamy
miku: オッケです!

あくまで通知時のログなので、既に通知欄から削除した、していないという情報は得られないが、概ね十分そうである。


 

- xonshを利用してshell上に表示する -

xonshのアドベントカレンダーなのでxonshを利用してshellに流し込む

上記スクリプトの出力部分を一行空白区切りで情報を出力するように修正します。

import codecs
for x in d:
    date = str(x['date']).split(' ')
    print date[0].encode('utf-8'), date[1].encode('utf-8'), x['app'].encode('utf-8'),\
            codecs.decode(x.get('title','no title').lower(), 'unicode-escape').replace(' ','').encode('utf-8'),\
            codecs.decode(x['body'].lower(), 'unicode-escape').replace('\n','').replace(' ', '').encode('utf-8')

そもそもコードも酷いというのもあるが、encodeしないと「UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-8: ordinal not in range(128)」みたいになるためさらに酷くなっているが仕方ない。
 
このpython2のスクリプトにおけるprintを標準出力から受け取り、xonshで表示できるような関数にする。
 
python3からsubprocess経由で/usr/bin/pythonを使い、上記ファイルを実行するスクリプトを以下に示す。

import subprocess

res = subprocess.run(['/usr/bin/python get_notification.py'], stdout=subprocess.PIPE, shell=True)
print(res.stdout)

これをxonshの関数にそのまま採用して情報を厳選して、xonsh toolbarに表示してみる。

 
python2と3を経由するかなり気持ち悪い感じだが出来たコマンドは以下のような感じ。

import subprocess

def get_last_notif():
    # 最後の通知を返す
    res = subprocess.run(['/usr/bin/python /path/to/get_notification.py'], stdout=subprocess.PIPE, shell=True)
    return res.stdout.decode('utf-8', 'replace').strip().split('\n')[-1]

# xonsh bottom toolbarに表示する
$BOTTOM_TOOLBAR = get_last_notif

f:id:vaaaaaanquish:20181124215447p:plain
(クソ…!よこせストレージを…!!)

 
このままだとUPDATE_COMPLETIONS_ON_KEYPRESSをTrueにしていれば、キー入力毎に評価され、DBを読みに行ってparseしてという処理がキー入力毎に発生してしまいますので、一度どこかにキャッシュして一定時間ごとに更新するなどの処理が必要そうである。

まあとりあえず目的は達成したので、キャッシュの話はまた別途という事にする。


 

- おわりに -

通知をキャッシュして、直近のものを順繰りで表示しておいたりすると便利そう。
ちゃんと書けばxontribみたいにできるかも…?

ただMacの通知取得の方法が若干アレなので、アップデートで使えなくなる可能性もあるし、素直にAppleが相応のAPIを公開してくれたほうが良さそうだなと思う。

 
xonshアドベントカレンダーの方がまだまだ後半ガラ空きという状態なので書いてほしいです。宜しくお願いします。
qiita.com

 
 

*1:記事内にもある通りsystem pythonが実行できる権限があれば過去の通知の一部を読めてしまうため脆弱性になり得る要素である

ptk1.xから2.xへの変更を選択コマンド実装から学ぶ

- はじめに -

この記事は、Xonsh Advent Calendar 2018 - Qiitaの5日目の記事です。


前年度、Xonsh Advent Calendar 2017 - Qiitaにおいて、私は以下のような2つの記事を投稿しています。
Python Prompt Toolkitで対話的な選択コマンドを作る - Stimulator
PythonでHatenaブックマークのホットエントリを取得して表示する - Stimulator
これは、xonshのコアライブラリとなるpython prompt-toolkit (以下、ptk)を利用したものです。

今回のアドベントカレンダー2018の1日目の記事にも書いた通り、xonshはptkのwrapperと言っても過言ではないレベルでptkは重要な役割を担っています。つまりptkを理解する事は、xonshを理解する事に繋がっています。
そして2018年、ptkは1.xから2.xへのメジャーアップデートにおいて多くの破壊的変更を含んでいます。
参考:Upgrading to prompt_toolkit 2.0 — prompt_toolkit 2.0.7 documentation


本記事は、前年度の以下のようなコマンドをptkの破壊的変更を逐一確認しながら進めるものです。
f:id:vaaaaaanquish:20171225155520g:plain:w400:h200
このコマンドにはptkの多くの要素が含まれており、これらを確認していくとおのずと詳細にふれる事ができます。
xonshがptk 2.xに対応した今年にぴったりの記事です。

 

- スクリプトの変更箇所を理解していく -

前年度実装したコマンドから変更点を抜き出しながら逐一確認していきます。
また、コマンドの全容が見たい場合は、こちらのgit repositoryにも公開しています。
GitHub - vaaaaanquish/select-command-using-ptk: select command using python prompt toolkit

 

MouseEventTypeのリネーム

マウス操作が入った時に落とすためのデコレータ部分です。
MouseEventTypeはMouseEventTypesにリネームされていますので変更が必要です。

- from prompt_toolkit.mouse_events import MouseEventTypes # ptk 1.x
+ from prompt_toolkit.mouse_events import MouseEventType # ptk 2.x

def if_mousedown(handler):
    def handle_if_mouse_down(app, mouse_event):
        if mouse_event.event_type == MouseEventType.MOUSE_DOWN:
            return handler(app, mouse_event)
        else:
            return NotImplemented
    return handle_if_mouse_down

また、詳細な理由を後述しますが前記事でhandle_if_mouse_down(cli, mouse_event)としていた第一引数の変数名をappに変更しています。cliという表記は後方互換性のためにptkの中でもいくつか残っているものの、今後無くなっていくかと思われますので、ここでもそれに習います。

 

KeyBindingManagerとKeybindings

キーバインディング周りは、KeyBindingManagerクラスが廃止され、KeyBindingsクラスによって全て賄う事ができるようになっています。これは、xonshrc等を書く場合でも同様です。
加えて、Keysクラスによって管理されていたキー入力周りですが、KeyBindingsでは「c-a」のようなstrを利用する事ができます。

その他、KeyBindingsのデコレータを付けた関数の引数に入るevent.cliはset_return_valueメソッドを持たなくなっています。
ptk 1.xではここにCommandLineInterfaceというクラスが入っていましたが、CommandLineInterfaceはapplication.Applicationクラスに統合されました。
上記MouseEventTypeの項でもcliという表記をappに変更したのは、そのためです(詳細は後述)。

Applicationクラスにset_return_valueメソッドはないため、exitを利用して同様の動作を実現します。
https://python-prompt-toolkit.readthedocs.io/en/master/pages/reference.html?highlight=Application#prompt_toolkit.application.Application

###### ptk 1.x ######
from prompt_toolkit.key_binding.manager import KeyBindingManager
from prompt_toolkit.keys import Keys
manager = KeyBindingManager.for_prompt()
@manager.registry.add_binding(Keys.ControlQ, eager=True)
@manager.registry.add_binding(Keys.ControlC, eager=True)
def _(event):
    event.cli.set_return_value(None)
@manager.registry.add_binding(Keys.Enter, eager=True)
def set_answer(event):
    ic.answered = True
    event.cli.set_return_value(None)

###### ptk 2.x ######
from prompt_toolkit.key_binding import KeyBindings
kb = KeyBindings()
@kb.add('c-q', eager=True)
@kb.add('c-c', eager=True)
def _(event):
    event.app.exit(None)
@kb.add('enter', eager=True)
def set_answer(event):
    ic.answered = True
    event.app.exit(None)

ガラッと変わっているのが目に見えてわかると思います。
前回の記事に記載のmove_cursor_down、move_cursor_upについてもKeyBindingsに対応させる事で動作させる事ができます。

 

Styleの設定

Style周りの変更は、かなり大きな影響を与えているように思います。
ptkは長らくPygrids、PygmentsにSyntaxColor、lexer等を依存させていましたが、ptk 2.xでは、Pygmentsのトークンのサポートを残しつつ、CSSのような独自コンポーネントが作成され、自由なクラス名を割り当てて自由なスタイリングを行う事が可能になりました。

以下がptk 1.xを利用してstyleを定義する部分です。

###### ptk 1.x ######
from prompt_toolkit.token import Token
from prompt_toolkit.styles import style_from_dict

inquirer_style = style_from_dict({
    Token.QuestionMark: '#5F819D',
    Token.Selected: '#FF9D00',
    Token.Instruction: '',
    Token.Answer: '#FF9D00 bold',
    Token.Question: 'bold',
})

 
同様の記載方法を利用したい場合はpygmentsのTokenクラスを利用する必要があります。

###### ptk 2.x ######
from pygments.token import Token
from prompt_toolkit.styles.pygments import style_from_pygments_dict

inquirer_style = style_from_pygments_dict({
    Token.QuestionMark: '#5F819D',
    Token.Selected: '#FF9D00',
    Token.Instruction: '',
    Token.Answer: '#FF9D00 bold',
    Token.Question: 'bold'
})

この記法を利用した場合、inquirer_styleの中では「Token.QuestionMark: '#5F819D'」が「'class:pygments.questionmark': '#5F819D'」のように取り扱われます。ptkの中でこのpygments.tokenを利用してStyleを適応させたい場合は、styles.pygments_token_to_classnameなるメソッドが用意されているのでそちらを使ってToken -> ptk classnameに変換して使う必要があります。

 
もちろん前述の通りptk2.xではpygemntsを使わない記法も可能です。

###### ptk 2.x ######
from prompt_toolkit.styles import Style
inquirer_style = Style.from_dict({
    'qu': '#5F819D',
    's': '#FF9D00',
    'instruction': '',
    'aaa': '#FF9D00 bold',
    'question': 'bold'
})

上記の例は大げさですが、他クラスに被らなければ自由なクラス名をつける事が可能です。これらを表示時に適応したい場合は「('class:qu', 'hogehoge')」「('instruction': 'piyo')」のようなtupleを作ってprint_formatted_textに投げてあげるとコンソール上にカラースタイルが適応された状態で表示する事ができます。

実際にxonshで表示してみます

###### ptk 2.x ######
from prompt_toolkit import print_formatted_text
from prompt_toolkit.formatted_text import FormattedText
print_formatted_text(
        FormattedText([('class:qu', 'hogehoge piyopiyo')]),
        style=inquirer_style)

f:id:vaaaaaanquish:20181121151916p:plain

より詳細には以下を参考にすると良いでしょう。pygmentsとprompt_toolkit classnameの比較表もあります。
https://python-prompt-toolkit.readthedocs.io/en/master/pages/printing_text.html

 

Styleの出力

前回の記事においては、get_prompt_tokens関数を定義して、promptに動的にテキストを渡すようにしています。

こちらも上記Styleの変更に対応するため、変更が必要です。
ptkのtokenの利用を止め、pygments.tokenを利用する事でほぼ同じ記述で同じ動作が実現できます。
最後returnする時にpygments_token_to_classnameで「class:pygments.hoge」な記法に変換してやれば良いです。

- from prompt_toolkit.token import Token
+ from pygments.token import Token
+ from prompt_toolkit.styles import pygments_token_to_classname

###### ptk 1.x #####
def get_prompt_tokens(cli):
###### ptk 2.x #####
def get_prompt_tokens():

    tokens = []
    T = Token
    tokens.append((Token.QuestionMark, '?'))
    tokens.append((Token.Question, string_query))
    if ic.answered:
        tokens.append((Token.Answer, ' ' + ic.get_selection()))
        selected_item(ic.get_selection())
    else:
        tokens.append((Token.Instruction, inst))

    ###### ptk 1.x #####
    return tokens    

    ###### ptk 2.x ######
    return [('class:'+pygments_token_to_classname(x[0]), str(x[1])) for x in tokens]

tokenとテキストのtupleをリストにして返す関数ですが、returnする時にtuple[0]のtokenを変換する処理を挟んでいます。
前述のStyleでも紹介した通り、pygmentsを使わない記法も使えますので、そちらも検討すると良いでしょう。

 

TokenListControlがFormattedTextControlにリネーム

前回の記事では、layout.controls.TokenListControlなるクラスを継承し、InquirerControlなる独自のセレクトアイテムを制御するClassを定義していました。
まず前提として、ptk 2.xではTokenListControlクラスはFormattedTextControlにリネームされています。

加えて、前述したStyle出力と同様に、pygments_token_to_classnameを使ってStyleの記法を変更する必要もあります。

- from prompt_toolkit.layout.controls import TokenListControl
- from prompt_toolkit.token import Token
+ from pygments.token import Token
+ from prompt_toolkit.layout.controls import FormattedTextControl
+ from prompt_toolkit.styles import pygments_token_to_classname

- class InquirerControl(TokenListControl):
+ class InquirerControl(FormattedTextControl):

    selected_option_index = 0
    answered = False

    def __init__(self, choices, **kwargs):
        self.choices = choices
        super(InquirerControl, self).__init__(self._get_choice_tokens, **kwargs)

    @property
    def choice_count(self):
        return len(self.choices)

    def get_selection(self):
        return self.choices[self.selected_option_index]

    def _get_choice_tokens(self):
        tokens = []
        T = Token

        def append(index, label):
            selected = (index == self.selected_option_index)
            def select_item(app, mouse_event):
                self.selected_option_index = index
                self.answered = True
                app.set_return_value(None)
            token = T.Selected if selected else T
            tokens.append((T.Selected if selected else T, ' > ' if selected else '   '))
            if selected:
                tokens.append((Token.SetCursorPosition, ''))
            tokens.append((T.Selected if selected else T, '%-24s' % label, select_item))
            tokens.append((T, '\n'))

        for i, choice in enumerate(self.choices):
            append(i, choice)
        tokens.pop()  # Remove last newline.
        
        ###### ptk 1.x #####
        return tokens    

        ###### ptk 2.x ######
        return [('class:'+pygments_token_to_classname(x[0]), str(x[1])) for x in tokens]
 
ic = InquirerControl(choices)

Tokenについては前述同様pygments.tokenを利用していますが、tokenを利用しない記法に変えても大丈夫です。

 

LayoutとContainer

前述の通り、TokenListControlはリネームされました。
その結果、引数のとり方が変わっているため注意が必要です。

加えて、ptk2.xでは表示アーキテクチャにContainerという概念が出てきました。
RendererオブジェクトはLayoutで定義されたオブジェクトを画面に表示します。
ptk 2.xにおいて、Layoutを構成する要素はContainerとUIControlの2つです。
Containerは水平、垂直分割を利用したContainerを再帰的に定義する事ができます。
UIControlはWindowやFormattedTextControl等を含んでおり、表示のサイズ、形式を定義する事ができます。

ptk1.xまでHSplitメソッドはContainerではなくLayoutを直接返していましたが、ptk2.xではContainerを再帰的に定義できるようにすることでより複雑なLayoutを実現できるようになったため、HSplitメソッドはContainerを返すようになりました。
よって以下のように、Layoutの定義の最後にLayoutクラスにする変更が必要です。

from prompt_toolkit.layout.containers import ConditionalContainer, ScrollOffsets, HSplit
from prompt_toolkit.layout.dimension import LayoutDimension as D
- from prompt_toolkit.layout.controls import TokenListControl
+ from prompt_toolkit.layout.controls import FormattedTextControl
+ from prompt_toolkit.layout.layout import Layout

###### ptk 1.x ######
layout = HSplit([
###### ptk 2.x ######
HSContainer = HSplit([

    Window(height=D.exact(1),

           ###### ptk 1.x ######
           content=TokenListControl(get_prompt_tokens, align_center=False)),
           ###### ptk 2.x ######
           content=FormattedTextControl(get_prompt_tokens)),

    ConditionalContainer(
        Window(
            ic,
            width=D.exact(43),
            height=D(min=3),
            scroll_offsets=ScrollOffsets(top=1, bottom=1)
        ),
        filter=~IsDone())])

###### ptk 2.x only ######
layout = Layout(HSContainer)

LayoutとContainerの関係については、以下を見ておくと良いでしょう。
https://python-prompt-toolkit.readthedocs.io/en/master/pages/advanced_topics/rendering_flow.html
https://python-prompt-toolkit.readthedocs.io/en/master/pages/full_screen_apps.html?highlight=Container#the-layout

また、アーキテクチャの全体像については(少し古いですが)以下が参考になります。
https://python-prompt-toolkit.readthedocs.io/en/master/pages/advanced_topics/architecture.html

 

CommandLineInterfaceがApplicationにマージ

MouseEventType、Keybindingsの項でも触れた通り、ptk 2.xではinterface.CommandLineInterfaceはapplication.Applicationにmergeされています。
後方互換のため、少なからずcliなる記載やメソッドが残っていますが)

最後に今までの項で作った設定や関数を利用してptkアプリを実行する部分のコードを以下に示します。

###### ptk 1.x ######
app = Application(
    layout=layout,
    key_bindings_registry=manager.registry,
    mouse_support=False,
    style=inquirer_style
)
eventloop = create_eventloop()
try:
    cli = CommandLineInterface(application=app, eventloop=eventloop)
    cli.run(reset_current_buffer=False)
finally:
    eventloop.close()

###### ptk 2.x ######
app = Application(
    layout=layout,
    key_bindings=kb,
    mouse_support=False,
    style=inquirer_style
)
app.run()

ApplicationはCommandLineInterfaceと違って適切にeventloopを設定してくれるようになっているため、eventloopを明示的に書く必要はありません。


 

- 全体のコード -

コード全体はptk 1.xと2.xのそれぞれを以下のrepositoryに入れてあります。
それぞれのdiffを見る事で違いを確認できます。
github.com


 

- おわりに -

ptkの理解はxonshの理解です。
他にもptkのリファレンスには様々なApplicationのサンプルがありますので、一読し是非ともxonshに活かして頂ければと思います。

アドベントカレンダーですが後半が空いてますので参加者を募集しています!!
宜しくお願いします!!
qiita.com

xonshの2018年の歩み

- はじめに -

この記事は、Xonsh Advent Calendar 2018 - Qiitaの1日目の記事です。
1日目という事ですので、2018年を振り返ってxonshにどんな動きがあったかの簡単な振り返りです。


私は社内チャットにおいても #xonsh なるxonshの情報を発信する部屋を運用しています(誰も発言してくれないので私しか発言してないですが)。そこでxonshのアップデート情報及び、xonsh関連の情報をまとめていまして、今回はそちらから引用しつつ2018年の節目とできればと思います。


 

- 2018年まとめ -

xonshの2018年の流れを順を追いながらまとめます。
重要なワードを節としていくので、気になるところは目次より飛んで頂けると良いかと思います。

2018年前半のpython shellとしての歩み

2018年の0.5.2から0.5.8

2018年のreleaseタグは少々混雑していまして3月に0.5.3から0.5.8までが一気に打たれました。
2018年、最初のreleaseは0.5.2で、そこから一気に0.5.8まで流れていきます。
 
 
0.5.3を軽く振り返ります。
Release 0.5.3 · xonsh/xonsh · GitHub

多くのbugfixに加えて、python-prompt-toolkit(以下、ptk)のpygments依存が解消しています。
2018年の多くのxonsh releaseにおいてこのptkとのやり取りは切っても切り離せません。

ptkはPythonにおける補完やサジェスト、Syntax highlightなどの機能を含んだPure Pythonなツールキットです。
github.com
xonshのshell機能における根幹を担っているパッケージで、他にもjupyterに含まれるIPython REPL、python実装のvimであるpyvimなど、多くの実装がこのptkを利用しています。

ここまでptkは、pygmentsというSyntax highlightのためのパッケージに完全に依存していました。pygmentsの別途installが必要かつ、環境によってはカラー設定が変化してしまう事から、ptkではansiカラーを利用してFormatted textベースなカラーリングもできるようになりました。
Welcome! — Pygments
Asking for input (prompts) — prompt_toolkit 2.0.7 documentation

また、このreleaseではTab completion xontribの追加、subprocessの結果のredirect等、shellとしての強化が多く成されています。
  
0.5.4ではXONSH_HISTORY_BACKENDやHistoryEntryなど、履歴関連周りが良くなりました。
Release 0.5.4 · xonsh/xonsh · GitHub
また、この時bash_completionが別プロジェクトとして切り離されました。
xonsh上でbash補完を利用したい場合は、bash_completionを別途導入する形になりました。
bash_completionについてはまたアドベントカレンダー内で書ければと思います)
  
0.5.5では、$UPDATE_COMPLETIONS_ON_KEYPRESSで補完出す出さないの管理が出来たり、configファイルとなるxonshrc関連が多く変更されています。0.5.6では、catやechoなどのaliasを追加するcoreutils xontribやjediベースのタブ補完が使えるjedi xontribが追加されています。
Release 0.5.6 · xonsh/xonsh · GitHub
Release 0.5.5 · xonsh/xonsh · GitHub
import文の前後で発火するイベントが増えたり、Windows上のコンソールにおけるbug fixなども入っています。
 
0.5.7です。
Release 0.5.7 · xonsh/xonsh · GitHub
subprocessのリスト、文字列のリダイレクトは非常に大きな改善だったかと思います。以下がやっと動くように。

echo hello world >/dev/null.

加えて、xonsh独特な@(...) syntax がgenerators、tuples をサポートするようになったり、python shellとしても改善が進みました。
 
0.5.8では_TeeStdクラスに変更が入っています。
Release 0.5.8 · xonsh/xonsh · GitHub
xonshでは標準入出力を独自のTeeStdクラスによって管理している事は知っておいた方が良いでしょう。
xonsh/base_shell.py at master · xonsh/xonsh · GitHub
前年度のアドベントカレンダーで、xonshの入出力をイジろうとした時、私もこのクラスでハマっています。
vaaaaaanquish.hatenablog.com

0.6.0に向けた怒涛のbugfixラッシュ

0.6.0に向けた細かなbugfixやrelease.xshの修正が続きます。
Release 0.5.9 · xonsh/xonsh · GitHub
Release 0.5.10 · xonsh/xonsh · GitHub
Release 0.5.11 · xonsh/xonsh · GitHub
Release 0.5.12 · xonsh/xonsh · GitHub

xonshにcontributeした時は、以下にあるように/newsディレクトリ配下に更新内容を書いた[branch名].rstを追加します。
(プルリク出すと「追加してくれ!」って言われます)
Developer’s Guide — xonsh 0.8.3 documentation

各プルリクで追加されたrstファイルがmergeされてreleaseタグの内容となる仕組みです。
自分出したプルリクで書いたrstがreleaseタグに書かれているとちょっと嬉しいですし、後で記録に残る良い習慣です。
開発者向けですが、それらが整備されているのも非常に大切なOSSの要素の一つだと感じます。

 

Python3.6以降もサポート0.6.xが出た!

0.6.0ではついにPython3.6のf-stringがサポートされました。
(といってもこの時はまだprint時に使えるかなくらいのレベルでしたが)
Releases · xonsh/xonsh · GitHub
またdeprecation用のデコレータ、xonsh関数のAssertion等が追加され、Python shellとしての機能拡張が進みます。
個人的には、この時追加されたTask Scheduler、cronの役割を担うschedule xontribも割と使っています。

0.6.1ではWindowsにおけるMSYS2のサポートや`~/.config/xonsh/rc.xsh`にあるrcファイルの読み込みが追加されています。
Release 0.6.1 · xonsh/xonsh · GitHub
0.6.xでの変更箇所は大きく、同僚とも「Linuxでlsできなくなった」「コメントが効かなくなった」等の会話をした記憶があります。0.6.0以降、毎週のreleaseにおいて数多くのbug fixが発生しています。ここでは書ききれないので省きますが、この辺りで多くのPython周りのbugが修正されています。

0.6.xのどこか辺りでimportも高速化されたんですが、ちょっと思い出せなかったです…
 

config.jsonの死

0.6.2より前では、xonshrcの他にconfig.jsonなるファイルを利用して設定できる項目がいくつか存在していました。
Release 0.6.2 · xonsh/xonsh · GitHub

ここでconfig.jsonは完全にサポートされなくなりました。
「Static configuration is dead (config.json), long live run control (xonshrc)!」
config.jsonに関する記載がWeb上には幾つかありますが、今はもう亡き存在として扱って大丈夫です。
 

Python3.7の問題児Async/Await対応

0.6.3でいくつかのbug fixとError入出力の変更を経て、0.6.4ではPython3系の革命児であり問題児であるAsync/Awaitをトークナイザーのキーワードに追加する対応をしています。
Release 0.6.3 · xonsh/xonsh · GitHub
Release 0.6.4 · xonsh/xonsh · GitHub
Python3.7で予約語に追加され、様々なフレームワークで課題になっているAsync/Await。
xonsh本体に大きな影響はなかったと捉えていますが、それでもやっぱり対策は大事です。

promptゾンビ問題の解消

0.6.5で設定ウィザードのI/Oが修正、0.6.7でEnvPathのメソッド拡充が行われた後、0.6.7でゾンビプロセス問題が解消しました。
Release 0.6.5 · xonsh/xonsh · GitHub
Release 0.6.6 · xonsh/xonsh · GitHub
Release 0.6.7 · xonsh/xonsh · GitHub
これは割と使う側から見て大きな改良かと思います。pythonのサブプロセスが結果を返せなかった時に発生していたissueで「xonsh使うとPCが重い」といった現象の元になっていました。この辺りから、不意にxonshが落ちる現象がほぼ無くなり、安定して常用できるshellになってきたように記憶しています。

 

2018年後半の山場 ptk2.xへの移行

ptk2.x対応スタート

大きな変更となったのが前述しているコアライブラリであるptkのバージョンアップです。
ptk 1.xから2.xにおいては、いくつかの破壊的変更が含まれており、それらを利用するjupyter等も順次対応していく形となったのが2018年の後半の動きです。

0.7.0がそろそろ出るか否かという時期の0.6.8で、ptk 2.x対応必要だよねという流れが出来てきました。
Release 0.6.9 · xonsh/xonsh · GitHub
ひとまずreadlineが起動する所まで、ここでfixされています。

その後小さなサブプロセス周りの拡充0.6.10を経て、ptk2.xに完全対応していく0.7.xが始まりました。
Release 0.6.10 · xonsh/xonsh · GitHub

怒涛のptk2.x対応

7月には0.7.0が出ました。

ptk2.xの対応は、ひとまず動くという状態でしたが、$SHELL_TYPEで切り替え可能になっていたりしたので一通り使えるようにはなっていました。

ユーザ目線では、prompt-toolkitに含まれるメソッドが大きく変更されたため、xonshrcの書き換えが必要になりました。
以下にも記載していますが、一番汎用的な例を出すとKeybindの書き方が変わります。
vaaaaaanquish.hatenablog.com
以下は複数行のコマンド入力中にctrl+vでvim編集できるようにする設定ですが、メソッド構成が変更になったことによって書き方を変える必要が出ました。

from prompt_toolkit.keys import Keys

@events.on_ptk_create
def custom_keybindings(bindings, **kw):

    # ptk 2.x
    @bindings.add('c-v')
    def edit_in_editor(event):
        event.current_buffer.tempfile_suffix = '.py'
        event.current_buffer.open_in_editor(event.cli)

    # ptk 1.x
    handler = bindings.registry.add_binding
    @handler(Keys.ControlV)
    def edit_in_editor(event):
        event.current_buffer.tempfile_suffix = '.py'
        event.current_buffer.open_in_editor(event.cli)

前年度のアドベントカレンダー等を見返してもptk 1.x形式で記載している場合があるため、注意が必要になってきます。


0.7.1では、その他ptk2.xで動かなくなった機能などの一斉fixが行われました。
Release 0.7.1 · xonsh/xonsh · GitHub
手前味噌ですが、私もここからcontributerになりました。最初は「コンソールが下までスクロールした時ptk2.xが補完出してくれない」というbugをptkのソース読みながら直しました。以降は、bugやドキュメントの整備などをやっています。

2018年の冒頭に出てきたpygmentsもptk2.xへの変更で更なる課題となってきました。
マルチプラットフォームに対応するだけでなく、新しいptkのcolor styleも取り入れながら0.7.3が登場しました。
Release 0.7.3 · xonsh/xonsh · GitHub

また、ここでjupyter notebookのxonsh Kernelも上手く動作するようになっていきます。
0.7.4でもxonsh-catなどjupyter kernel周り、ptk2.x移行時のcolors names、補完のバグが修正されています。
Release 0.7.4 · xonsh/xonsh · GitHub

0.7.4から数時間後には、ptk2におけるhistoryの順番がfixした0.7.5が出ました。爆速開発です。
Release 0.7.5 · xonsh/xonsh · GitHub

はじまるptk拡張

なんでこんな大きな変更を含んだptk2.xなんじゃいというのもあるんですが、ptk2.xの更新はデカいです。
例えば「補完を上下移動で選択」が「左右移動でも選択可能」になったのはptk2.xからです。

補完の拡張となるalias stackが0.7.6で出ました。メイン開発者のscopatzは個人的に使ってたみたいですが、乗っかる形ですね。補完魔改造時代のスタートです。
Release 0.7.6 · xonsh/xonsh · GitHub

def _f():
    def j():
        pass
    global aliases
    aliases['j'] = j
    def completions(pref, *args):
        return set(['hello', 'world'])
    completer add j completions "start"
_f()
del _f

f:id:vaaaaaanquish:20181118021840p:plain

続く0.7.7では、$XONSH_HISTORY_MATCH_ANYWHEREをTrueにする事で上矢印で履歴エントリを検索できるようになりました。
xonsh/xonsh https://github.com/xonsh/xonsh/releases/tag/0.7.7
これ自体はptk2.xの恩恵という訳ではないですが、shellとしてかなり一般的に使われている機能で、また一つ力強くなったように感じます。

opensource.comとクックパッド、そしてHacker News、GitHubトレンドへ

ちょうど0.7.7周辺でMoshe Zadka氏により「Why I love Xonsh」なる記事がOpensource.comに投稿されます。
opensource.com

また、0.7.8になる手前辺りでクックパッドインターンの体験記事に「インターン生がxonshを使っている」という文言が出てきました。
nanabyo.hatenablog.com
流石クックパッドさんですね。

少しずつxonshが話題にされていた9月頃、Hacker Newsに「Xonsh: Python-powered, cross-platform, Unix-gazing shell」なるスレッドが立ちました。
Xonsh: Python-powered, cross-platform, Unix-gazing shell | Hacker News
そこでは、メイン開発者であるscopatzが実際に投稿するなど盛り上がりを見せ、Hacker News内のトップスレッド20に入るなどしました。
世界を見た時にHacker Newsで話題になる事がどれだけ大きいか。

その後、9月のgithub trendにもxonshがランクインします。
https://github.motakasoft.com/trending/?d=2018-09-15&l=python
話題として今年最も躍進した月でした。

jupyterもptk2.xへ

この頃、ちょうどjupyter(より正確にはIPython)もptk2.xへの対応が進みました。

jupyterのptk2.x対応以前では、ptk2.xをインストールした状態でpython kernelを動かすと落ちるという課題を抱えており、xonshとjupyterを併用で利用するのが少々複雑な状態でした。私個人においても pip install prompt_toolkit==1.0.15 等を推奨していました。

github.com

現在、ptk2.xでの課題はほぼほぼ解消しています。この関係性によって、xonshにコミットしptkにコミットする事でjupyterプロジェクトにも貢献できるという所が非常に楽しいです。

2018年後半、そして0.8.xへ

増えるpython機能のサポート

0.7.8ではcollections.ChainDBがサポートされました。
Release 0.7.8 · xonsh/xonsh · GitHub

0.7.9では以下のように@(expr) syntaxが書けるようになり、かなり便利になりました。
Release 0.7.9 · xonsh/xonsh · GitHub

$ echo /path/to/@(['hello', 'world'])
/path/to/hello /path/to/world

0.7.10では、 f-stringを利用してf"{$HOME}" のように環境変数を参照できるようになりました。
Release 0.7.10 · xonsh/xonsh · GitHub

徐々にpython本体の便利なメソッドを吸収しつつ、shellとして進化していきます。

0.8.xが来た!Archにも来た!

いよいよ0.8.0の登場です。ここまで一部変更できず気になっていた色の問題も$PTK_STYLE_OVERRIDESの登場により、ほぼ完全にxonsh上の全ての色を完全に設定できるようになりました。
Release 0.8.0 · xonsh/xonsh · GitHub

0.8.1では、ついにforeign shellsからfishが消えました。また、この時からArch Linux official repositoriesからxonshが利用できるようになりました。Archは良いぞ。
Release 0.8.1 · xonsh/xonsh · GitHub

他にも以下のようにSubprocSpecにpipeline_indexなる属性が付くようになりました。
使い所は少々難しいですが、shellの結果をPythonに流すような場合に汎用性が高まったと言えるでしょう。

p = ![ls -l | grep x]
p.specs [0] .pipeline_index == 0

0.8.2ではf-stringsが完全に動作するようになりました。!を利用した構文とカニバるので!を犠牲にするわけですが、pythonの機能を優先するという意味で良い改修だったと思います。
Release 0.8.2 · xonsh/xonsh · GitHub

記事執筆時(2018/11/18)のlatest 0.8.3です。
Release 0.8.3 · xonsh/xonsh · GitHub
Annotation assignment statements (x : int = 42)がfix、from x import (y, z,)がfixするなど、Pythonの書き方をshell上で再現できるようになってきました。

あと私は使ってないんですがvirtualenvのサポートが含まれており、これは結構待望されてた感じなのではないでしょうか。


加えて、0.8.xから古い書き方に対して警告が出るようになり始めました。
徐々に移行していく必要があります。
(1.x.xの匂いが少しずつしている感じか…!?)

 

-おわりに -

急ぎ足で、主に追加された機能の多くを振り返っていきました。開発スピードも早く、2018年話題になった事で開発者も増えたxonshの動きをこれからも応援しcontributeしていければと思っております。

メジャーバージョン1.x.xも見えてきて、めちゃくちゃ楽しみな2019年になると思っています!
来年も良いxonshの年にしていきましょう!


明日はあの74thさんです!
アドベントカレンダーはまだまだ空きがありますので、ちょっと触ってみただけでも書いて頂けると助かります!!
qiita.com

宜しくお願いします!!

LeapMotionでPythonを使ってジェスチャーで家電を操作する

- はじめに -

近年、VRのブームのおかげもあってモーションキャプチャー分野も発展しつつあります。

本記事は、お手軽モーションキャプチャー端末であるLeap MotionPython 3.xから利用し、様々な家電やPC上の操作をジェスチャーで行おうという記事です。

都内某企業でLTする機会があったのでネタにしたところダダ滑りしたのでこの記事もボツにしようとしましたが、現状多分多くの人が詰まるので、正しくPython3系でLeap Motionが使えるように公開しておきます。


 

- Leap Motionとは -

前述したように、人体の動きをデータ化するモーションキャプチャー分野は、近年急速に発展しています。

キャプチャー端末分野では先日のbuild2018にてProject Kinect for Azure (Microsoft,2018)が発表されたりだとか、ハンドキャプチャー特化で見てもグローブを装着して高精度に手の動きを認識するNoitom Hi5 (Noitom, 2017)が話題になったりだとか、画像認識分野ではOpenPose(CMU Perceptual, 2017)が出たりとか、あらゆる方面から進展しています。

f:id:vaaaaaanquish:20180923155542p:plain:w350
LTスライドの一部:キャプチャー分野の発展

 
Leap Motionは、2012年よりLeap Motion社が発売しているハンドモーションキャプチャーバイスです。
光センサ、赤外線センサを搭載していて、なかなかの精度で手の骨格の動きを取得する事ができます。
Windows, Mac, Linuxに対応したドライバが存在するだけでなく、Unity, Unreal Engineへの対応、以下の言語に対応するSDKとライブラリが公開されています。

(ただこれはSDK v3の話で、v4ではc++, java, pythonがメインになっています)

 
価格はAmazon最安で9,900円。

近年IT企業に増えつつある「月1万技術向上に使って良いよ〜」みたいな枠にピッタリのガジェットです。(私もその枠組で購入しました)

サイズとしてはこれくらいで扱いやすいです。USBケーブルが専用かつ1mしかないのがちょっとだけネックです。

f:id:vaaaaaanquish:20180923222724j:plain:w350
Leap Motion実物


 
Leap MotionHPよりLeap Motion搭載のPCが発売されたり、公式WebStoreではOculus RiftやHTC Viveに装着できる機器が出ています。デベロッパーギャラリーもあり、世界の開発者が作成したアプリで遊んだり、自身の作ったものを公開できる環境も存在します。

f:id:vaaaaaanquish:20180923162152p:plain:w350
LTスライドの一部:Leap Motion周辺の土壌

手軽にモーションキャプチャーの世界に入り込める、最初に遊ぶに最適な製品です。


 

- Leap Motionで取得できる情報 -

Leap Motionでは、両手における、手のひら中央及び各指、各関節のstart_point, end_pointにおける空間座標(3次元)と方向ベクトル(3次元)が取得できます。

f:id:vaaaaaanquish:20180923164036p:plain:w350
LTスライドの一部:取得可能な情報

また、SDKの提供するAPIでは以下のGestureが定義されています。

  • circle: 指で円を描く動作
  • swipe: 手を左右に動かす動作
  • key tap: 指で物を選択するような動作
  • screen taps: スクリーンをTapする動作

f:id:vaaaaaanquish:20180923171502p:plain:w350
LTスライドの一部:定義済みGesture


 

- Leap Motionに関連する開発 -

Leap Motion周りの開発について調べると、古いバージョンのSDK v3を利用した情報が多く存在します。例えばPythonであれば以下等
PythonでLeapMotionのデータを取得する。 - Qiita
PythonでLeapMotionを使ってみる - Qiita
Leap Motion を Pythonから使う方法を調べた | Futurismo
LeapMotionとpythonで遊ぶ

また、公式HPから参照できる以下のページでは、SDK v3までのReferenceしか参照することができません。
Leap Motion SDK Reference : Python SDK Documentation — Leap Motion Python SDK v3.2 Beta documentation

これは、以下のような内容からきています。

  • SDK v4からLeapCxxという名前でGithub上でライブラリが管理されるようになった
  • それまでのLeapAPIは非推奨となりLeapCなるAPIが提供されはじめた(LeapCxxの一部)
  • v4は未だbeta版であり、MacLinuxはv2の利用を推奨されている

LeapCxxではswigで多言語対応される事を前提として高速化され、LeapCSharpやUnityModulesなどのBindingを切り離す事にも成功しています。
まとめると「Windowsで利用可能な言語であればLeapCxxを使ったほうが良いが、現状使えない場面の方が多い」という事になります。

LeapCxx : GitHub - leapmotion/LeapCxx: Implementation of older C++ API using LeapC
LeapCxx documentation : Leap Motion C API: Leap Concepts

 
Windowsならv4版、Windowsじゃ無くてもv3版使えば良いじゃんとなる訳ですが、私はWindowsが扱えないのと、v3以前の開発環境は以下のようになっています。

f:id:vaaaaaanquish:20180923202240p:plain
・・・。
一目で分かるかと思いますが古いです。

行列や機械学習に突っ込む事も考慮して、せめてMacbookのPython3で動くようにしていきます。

(本記事では、Mac, Python3での利用を想定しますが、今後Windowsでv4を利用した記事もがんばって公開する予定です。)


 

Leap Motion Controllerのインストール

以下からControllerをダウンロードします。
www.leapmotion.com

ダウンロードしたdmgファイルを使ってインストールすると、MacであればメニューにLeap Motionアイコンが追加されます。

f:id:vaaaaaanquish:20180923182551p:plain
Controller
このアイコンが緑になっている場合、Leap Motionが認識しているという形です。

Visualizerが付属していますので遊んでみます。

f:id:vaaaaaanquish:20180923182352g:plain
Visualizer
この時点で、Leap Motion App Homeなるアプリもインストールされており、そのアプリ経由で公式WebStoreに公開されているアプリで遊んだり、自身のアプリを公開する事が可能です。


 

Leap Motion SDK v2のインストール

Leap Motion SDKのv2を以下よりダウンロードします。
こちらをswigを用いてPython3系でも利用できるように改修していきます。
V2 Tracking — Leap Motion Developer

zipを解凍すると、配下にdmgファイルがあるはずなので、そちらを利用してSDKをインストールします。

また、解凍したファイル群の中には「./LeapSDK/samples/Sample.py」なるサンプルが存在します。
以降このサンプルを利用してもよいですが、このSample.pyはpython 2.xベースですので今回は使いません。


 

python 3系への対応

公式がswigでPython3系に対応する方法を公開してくれています。
support.leapmotion.com

雑に調べた所、以下2つのリポジトリがpython3に対応してそうに見えますが、中身は上記の記事をスクリプトに落とし込んだだけのようですので、自身で作業した方が良さげです。
GitHub - Nagasaki45/leap_python3: Build LeapMotion binaries for python3
GitHub - BlackLight/leap-sdk-python3: Leap Motion SDK - Python 3 module builder


先程解凍したLeap Motion SDK の配下に入り ./LeapSDK/ に移動して雑なディレクトリを作ります。

cd LeapSDK
mkdir work
ls
> docs/         head_sha.txt  include/      lib/          samples/      util/         version.txt   work/

必要なファイルを全てworkにコピーしてswigに投げます

cp include/Leap.i work
cp include/LeapMath.h work
cp include/Leap.h work
cp lib/libLeap.dylib work

cd work
swig -c++ -python -o LeapPython.cpp -interface LeapPython Leap.i

この時、以下のようなエラーが出るはずです。

Leap.i:991: Error: Line indented less than expected (line 3 of %pythoncode or %insert("python") block) as no line should be indented less than the indentation in line 1
Leap.i:1009: Error: Line indented less than expected (line 16 of %pythoncode or %insert("python") block) as no line should be indented less than the indentation in line 1
Leap.i:542: Error: Line indented less than expected (line 13 of %pythoncode or %insert("python") block) as no line should be indented less than the indentation in line 1
Leap.i:552: Error: Line indented less than expected (line 7 of %pythoncode or %insert("python") block) as no line should be indented less than the indentation in line 1
Leap.i:1115: Error: Line indented less than expected (line 6 of %pythoncode or %insert("python") block) as no line should be indented less than the indentation in line 1
Leap.i:1116: Error: Line indented less than expected (line 6 of %pythoncode or %insert("python") block) as no line should be indented less than the indentation in line 1
Leap.i:1117: Error: Line indented less than expected (line 6 of %pythoncode or %insert("python") block) as no line should be indented less than the indentation in line 1
Leap.i:1121: Error: Line indented less than expected (line 6 of %pythoncode or %insert("python") block) as no line should be indented less than the indentation in line 1
Leap.i:1118: Error: Line indented less than expected (line 6 of %pythoncode or %insert("python") block) as no line should be indented less than the indentation in line 1
Leap.i:1122: Error: Line indented less than expected (line 6 of %pythoncode or %insert("python") block) as no line should be indented less than the indentation in line 1
Leap.i:1123: Error: Line indented less than expected (line 6 of %pythoncode or %insert("python") block) as no line should be indented less than the indentation in line 1
Leap.i:1119: Error: Line indented less than expected (line 6 of %pythoncode or %insert("python") block) as no line should be indented less than the indentation in line 1
Leap.i:1120: Error: Line indented less than expected (line 6 of %pythoncode or %insert("python") block) as no line should be indented less than the indentation in line 1
Leap.i:565: Error: Line indented less than expected (line 10 of %pythoncode or %insert("python") block) as no line should be indented less than the indentation in line 1
Leap.i:1156: Error: Line indented less than expected (line 23 of %pythoncode or %insert("python") block) as no line should be indented less than the indentation in line 1

試しにLeap.iの991行目を見ると分かりますが、以下のようにpythonコードと言いつつdefの前にインデントが入ってます。

%extend Leap::Vector {
%pythoncode {
  def to_float_array(self): return [self.x, self.y, self.z]
  def to_tuple(self): return (self.x, self.y, self.z)
%}}

インデントが合うように、%pythoncodeで検索し、全ての行に存在する前空白2個を消していけば良いです。
1115行から1123行目のエラー原因も別箇所のpythoncodeにあります。私はvimで雑にやりました…

 
swigによって「LeapPython.cpp」なるファイルが生成出来ていれば成功です。
このファイルを利用しているPythonに紐付ける作業を行います。

clang++ -arch i386 -arch x86_64 -I/Library/Frameworks/Python.framework/Versions/3.6/include/python3.6m LeapPython.cpp libLeap.dylib /Library/Frameworks/Python.framework/Versions/3.6/lib/libpython3.6.dylib -shared -o LeapPython.so

pyenv等System以外のPythonを利用している場合は、python3.6mへのpathを、それぞれPythonの/include/pythonNNmディレクトリを指すようにします。
私は面倒なのでpyenv local systemでworkディレクトリではsystemのpython3.6を利用するようにしています。


 
LeapPython.soとLeap.pyが出力されていれば終わりです。

SampleはSwig解説記事の最後にある以下リンクからダウンロードしたものを使います。
https://leapmotion-assets-production.s3.amazonaws.com/Jsf1V_R5_kbRNHu3QKdenQ/LeapPython33.zip

解凍すると、SamplePython33.pyがあるのでそちらをworkディレクトリに移動します。(解凍したら3.3用のLeapPython.soが付いてくるけど基本無視で)
workの中でls

ls
> eap.h             Leap.i             Leap.py            LeapMath.h         LeapPython.cpp     LeapPython.h       LeapPython.so*     SamplePython33.py* libLeap.dylib*

そのままpython3.6でSamplePython33.pyを実行するとLeap.pyのimportエラーが出ます。

  File "/Users/vanquish/Documents/work/LeapDeveloperKit_2.3.1+31549_mac 2/LeapSDK/work/Leap.py", line 345
    %
    ^
SyntaxError: invalid syntax

Leap.pyを見てみると何箇所か%だけが記載されている行があるので、Leap.pyからその行を削除します。

 
これで多分動きます。

python3.6 SamplePython33

> Initialized
> Press Enter to quit...
> Connected
> Frame id: 46613, timestamp: 4268852938, hands: 0, fingers: 0, tools: 0
> Frame id: 46614, timestamp: 4268957238, hands: 0, fingers: 0, tools: 0
> Frame id: 46615, timestamp: 4269061570, hands: 0, fingers: 0, tools: 0
> Hand has 5 fingers with average tip position (38.1071, 111.769, -141.137)
> Palm position: (51.3712, 76.887, -72.069)
> Pitch: 31.787451 degrees,  Roll: 17.410023 degrees,  Yaw: -10.570597 degrees
> Hand curvature radius: 124.589699 mm
> Frame id: 49355, timestamp: 4485494804, hands: 1, fingers: 5, tools: 0
> Hand has 5 fingers with average tip position (37.3419, 113.345, -140.911)
> Palm position: (50.7582, 77.6839, -72.2254)
>  ...

後はSamplePython33.pyを見れば分かりそうですが、on_frame メソッドに手の情報が流れ込んでくるので、それに応じてGesture判定したり動作を書いてやれば良い感じです。


 

- 家電を動かす -

私の家のIoT環境です。ここにLeap Motionを足してみます。

f:id:vaaaaaanquish:20180923220036p:plain
Leap MotionをIoT環境に追加

 
実際にテレビの操作をしているGIFです。GIFに収めている関係上めっちゃ反応がよく見えます(実際は30秒くらいの動画です)。

f:id:vaaaaaanquish:20180923220340g:plain:w450
Leap Motionによるテレビの操作

 
電気やクーラー、テレビも「OK, Google全部消して」等で消せるようにしていますので、そちらのトリガーをLeap Motionにしてみます。

f:id:vaaaaaanquish:20180923220526g:plain:w450
家電の操作
これ引っ越し直前の家で実験してるのでちょっと汚いんですが、この端末が玄関にあればサッとジェスチャーキメるだけで全て消せるようになりそうではあります。


 
まあここまで書いてなんですが、実際他にも「ターミナルやTweetDeckを起動する」とか「じゃんけんアプリを作ってみる」とかやったものの、あんまりウケそうなものができずコレに落ち着きました…

実際やってみる中で、モーションキャプチャーで何するかと考えてもどれも微妙で「妻がキレてる時に声出してオーケーグーグルって言えない時」とかに使えるかも知れないなレベルの感想しか得られませんでした。


このクックパッドを手で操作するのは公開されているアプリで最初に試したんですが、唯一これが料理中とかに使い物になるかもしれないなあ…

f:id:vaaaaaanquish:20180923221012g:plain
クックパッドの操作


 

- おわりに -

普通にデバイスとしては面白いですし、多分VR機器買ったらもっと遊べるんだと思います。

高専に居た頃にKinectを触った事がありますが、手の認識に関してはKinectよりは精度良く安く出来て暇つぶしに良い端末です。


 

複数ノードDockerでChainerMNを動かすためのTips

- はじめに -

ChainerMNがついに本家Chainerにマージされました。分散深層学習への本気度が伺えます。

github.com


節目という事で、Dockerを利用して複数ノードでChainerMNするために行った事のメモをTips形式で残しておこうという記事です。

私は半年くらい前からこの記事の内容を使っているのでアップデートがあるかもしれません。
加えて、最近はkubernetesを使うのが流暢で、PFNさんも公式ブログ書いてるし多分k8sが良いと思います。
ChainerMN on Kubernetes with GPUs
(私もk8s挑戦してるけどあんまり上手くいってなくて放置中です…)

この記事はk8s使えないけどDocker使えるような分散環境はあるみたいなニッチな需要を満たすかもしれないなあというレベル感の記事です。

 

 

- 前提と参考文献 -

記事前提として以下を想定しています。

  • docker、nvidia-docker、cuda周りがインストールされており、1ノード1GPUでChainerのtrainを回せる環境がある
  • 複数ノード、複数GPU環境がある

また、私が環境構築した際に読んだ参考文献を示します。

ひとまず、上の5つに目を通せば動くような気がします。
加えて、細かなエラーでStackOverflowやOpenMPIの公式リファレンスを読んでいますがそちらは随時記載します。


Deep Learning分散の仕組みについては、秋葉さん、鈴木さんの記事とスライドに任せます。

(というかちゃんと論文読んだりしてる訳じゃないので自分はできない…)


 

- Docker環境構築 -

cuda周り以外はまっさらのUbuntu 16.04 のDocker想定。

apt-get upgrade
apt-get update
apt-get install python3-pip python3-dev
apt-get install python-pip python-dev
apt-get install wget git

バックエンドにOpenMPI、NCCLを利用する想定でインストールを進める。

OpenMPIのインストール

apt-get install infiniband-diags opensm ibverbs-utils infiniband-diags perftest

wget https://download.open-mpi.org/release/open-mpi/v3.0/openmpi-3.0.1.tar.gz
tar -zxvf openmpi-3.0.1.tar.gz
cd openmpi-3.0.1

./configure --with-cuda --prefix=$HOME/local/openmpi --with-openib
make -j4
make install

touch ~/.bashrc
echo 'export LD_LIBRARY_PATH=$HOME/local/openmpi/lib:${LD_LIBRARY_PATH}' >> ~/.bashrc
echo 'export PATH=$HOME/local/openmpi/bin:${PATH}' >> ~/.bashrc


NCCLなるGPU通信ライブラリのインストールは、以下のリンクからダウンロードする。
NVIDIA Developerの登録を済ませておく必要がある)
https://developer.nvidia.com/nccl/nccl-download

ライセンス読んで「I Agree~」をチェックした後、debファイルを以下からダウンロードしてくる。
f:id:vaaaaaanquish:20180917213514p:plain

Docker内にダウンロードしてきたファイルを設置する。

# コンテナ内に送る
sudo docker cp nccl-repo-ubuntu1604-2.2.12-ga-cuda9.0_1-1_amd64.deb [CONTAINER ID]:/hogehoge

# docker attach後
dpkg -i nccl-repo-ubuntu1604-2.2.12-ga-cuda9.0_1-1_amd64.deb
apt update
apt install libnccl2 libnccl-dev
echo 'export NCCL_ROOT=/usr/local/nccl' >> ~/.bashrc
echo 'export CPATH=$NCCL_ROOT/include:$CPATH' >> ~/.bashrc
echo 'export LD_LIBRARY_PATH=$NCCL_ROOT/lib/:$LD_LIBRARY_PATH' >> ~/.bashrc
echo 'export LIBRARY_PATH=$NCCL_ROOT/lib/:$LIBRARY_PATH' >> ~/.bashrc
source ~/.bashrc

追記:2018/09/19
エゴサしてたらNCCL情報が


 
この後一番陥りやすいのが相互にsshする環境の構築である。
複数ノードの場合「ノード間はssh-keyでパスワードなしでssh可」「dockerに入るにはパスワードなしで」を実現しなければならず、通信できない場合にOpenMPIが無反応だったりしてわからんってなるのでちゃんとやる。

apt-get install -y build-essential libssl-dev libreadline-dev zlib1g-dev language-pack-ja
apt-get -y install openssh-server ufw curl
mkdir /var/run/sshd

# ユーザを作って、そのユーザでsshできるようにする(vaaaaanquishの所をよしなに)
useradd -m vaaaaanquish && echo "vaaaaanquish:vaaaaanquish" | chpasswd && gpasswd -a vaaaaanquish sudo
mkdir -p /home/vaaaaanquish/.ssh
chmod 700 /home/vaaaaanquish/.ssh
passwd -d root
passwd -d vaaaaanquish

鍵はDockerコンテナ内では生成せず、ノードからマウントしてくる形を取る(結局ノード間自体もsshできないと意味ないので)。

 
/etc/ssh/sshd_configを以下のように変更する(なければ作る)
ここで指定するPortは、Dockerからもアクセスするので今後統一して選べる所にする。

Port 2223
PermitEmptyPasswords yes
PasswordAuthentication no
PubkeyAuthentication yes
UsePAM no

また、rootから~/.ssh/configの最期の方に以下も追記する(なければ作る)

Host *
    Port 2223
    StrictHostKeyChecking no
    UserKnownHostsFile /dev/null

 
最後にcythonとChanierMNをインストールする。
cupyのcudaNNのバージョン等を絶対間違えないよう確認する

pip3 install cython
pip3 install cupy-cuda90
pip3 install chainermn

# Chainer試すためのMNISTパッケージもここで
pip3 install python-mnist


 
dockerコンテナからデタッチする前に、以下のファイルをルート配下辺りに「init.sh」のような名前で置いておく。

#!/bin/bash

NCCL_SOCKET_IFNAME=^docker0
/usr/sbin/service ssh start
/bin/bash

NCCLがDockerの作る仮想ソケットを利用してしまうらしいのでNCCL_SOCKET_IFNAME=^docker0は必須
参考:http://tabisurubiker.hatenadiary.jp/entry/2017/10/02/092952
Dockerコマンドでrunする際に実行するプログラムとして、このinit.shを実行、sshサーバを立ち上げて、bashで待機させるようにする。

待機しているdockerコンテナに対してOpenMPIがプロセス振ってくれるイメージ。

 
これで多分できたのでcommitしてsaveしてDockerコンテナは終了。


 

- ノード間のsshの実現 -

前述したようにノード間はsshできないとダメ。

実現する方法はいくつかあるがが、私はアホなので全ノードでssh-keygenして全ノードに配ればええやんと思ってスクリプト書きました。これで作ったkeyをマウントする形を取っている。ssh_generator.pyなる名前で以下のファイルを作って実行するだけ。自身をfabricで読んで各サーバに送る動作をpexpectで実行するアホみたいなスクリプトです。

# -*- coding: utf-8 -*-
# must : pip install pexpect, fabric3
from fabric.api import *
import pexpect
import sys

HOSTS = ['hogehoge', piyopiyo]    # 利用予定のサーバ
env.user = 'vaaaaanquish'          # localからサーバにアクセスするユーザ
env.key_filename = 'local key'    # localからサーバにアクセスするためのkey
env.password = PASSWORD      # localからサーバにアクセスするためのkey pass


def ssh_gen():
    run('ssh-keygen -t rsa')

def ssh_copy_id(y):
    run('ssh-copy-id -i /home/{}/.ssh/id_rsa.pub {}'.format(USER, y))


if __name__ == '__main__':
    for x in HOSTS:
        # 作るやつ
        print('\n[Making key : {}]'.format(x))
        cmd= "fab -f ssh_generator.py -H {} ssh_gen".format(x)
        child = pexpect.spawn(cmd, encoding='utf-8')
        child.logfile = sys.stdout
        while 1:
            i=child.expect([
                r'^.*(Enter file in which to save the key).*',
                r'.*(Enter passphrase).*',
                r'.*(Enter same passphrase again).*',
                r'.*(Overwrite).*\?',
                pexpect.EOF, pexpect.TIMEOUT], timeout=5)
            if i in [0,1,2]:
                child.sendline('')
            if i == 3:
                child.sendline('y')
                child.sendline('')
            if i in [4, 5]:
                break

        # 配るやつ
        print('\n[Sending key : {}]'.format(x))
        for y in HOSTS:
            cmd= "fab -f ssh_generator.py -H {} ssh_copy_id:{}".format(x,y)
            child = pexpect.spawn(cmd, encoding='utf-8')
            child.logfile = sys.stdout
            while 1:
                i = child.expect([
                    r".*(password).*",
                    pexpect.EOF, pexpect.TIMEOUT], timeout=5)
                if i == 0:
                    child.sendline('{}'.format(PASSWORD))
                    child.sendline('')
                if i in [1,2]:
                    break

これをローカルPCで実行し、鍵配り作業とする。各サーバの~/.ssh/配下に鍵がそれぞれ登録されます。
(基本的に使い終わったらこの鍵を削除しておかないと危ういのでちゃんとやりましょう)

踏み台等を利用する環境でも「env.gateway = [GATEWAY_SERVER]」のようにする事で解決可能です。


 

- mpiexecを用いたChainerMNの実行 -

以下ChinerMNのチュートリアルpython-mnistで試していく。
Step 1: Communicators and Optimizers — ChainerMN 1.3.0 documentation

 

Dockerのrun

nvidia-docker runするがこの際には以下のようにオプションを付ける。

sudo nvidia-docker run -it -d \
    --net=host -p 2223:2223 \
    -v /tmp:/tmp \
    -v /etc/hosts:/etc/hosts:ro \
    -v /home/vaaaaanquish/.ssh:/home/vaaaaanquish/.ssh \
    -e LD_LIBRARY_PATH=/usr/local/nccl/lib/:/root/local/openmpi/lib:/usr/local/nvidia/lib:/usr/local/nvidia/lib64:/usr/local/cuda/lib64 \
    -e PATH=/root/local/openmpi/bin:/usr/local/nvidia/bin:/usr/local/cuda/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
    -e NCCL_ROOT=/usr/local/nccl \
    -e CPATH=/usr/local/nccl/include:/usr/local/cuda/targets/x86_64-linux/include \
    -e LIBRARY_PATH=/usr/local/nccl/lib/:/usr/local/cuda/lib64/stubs \
    -e PYTHONPATH=/usr/bin/python3\
    [docker_image_name] sh /init.sh

dockerのポートはssh_configに書いたものを指定する。
/etc/hostsはマウントしないとdockerから各ノード間で通信できないので必須。
また前述の通り、配布したsshもノード上の物をマウントして利用する。
環境変数はdocker内でbashrcに書いても良いし、個々で-eで指定しても良いですが今回はinit.shを最後に動かすようにしてるのでここで。

あとコレはHorovodなるpackageを使った時に起こりがちな症状ですが/tmpにファイル作る場合もあるので、/tmpも大人しくマウントしておくと良いです。

 
何か雑にこんなんで全ノードでdocker実行すればええんちゃうんですかね

from fabric.api import *

env.hosts = [HOSTS]
env.user = USER
env.key_filename = KEY_PATH
env.password = PASSWORD

DOCKER_ENV = '-e LD_LIBRARY_PATH=/usr/local/nccl/lib/:/root/local/openmpi/lib:/usr/local/nvidia/lib:/usr/local/nvidia/lib64:/usr/local/cuda/lib64 -e PATH=/root/local/openmpi/bin:/usr/local/nvidia/bin:/usr/local/cuda/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin -e NCCL_ROOT=/usr/local/nccl -e CPATH=/usr/local/nccl/include:/usr/local/cuda/targets/x86_64-linux/include -e LIBRARY_PATH=/usr/local/nccl/lib/:/usr/local/cuda/lib64/stubs -e PYTHONPATH=/usr/bin/python3'

@parallel
def run_docker():
        run("sudo nvidia-docker run -it -d --name {} --net=host -p 2223:2223 -v {}:{} -v /etc/hosts:/etc/hosts:ro -v /home/{}/.ssh:/home/{}/.ssh {} {} sh {}".format(P_NAME, LUSTERFS_MAUNT, LUSTERFS_HOME, USER, USER,DOCKER_ENV, DOCKER, INIT_SH))


 

mpiexecでChainerMNの実行

どこか1つのノードに対して「ssh root@hogehoge -p 2223」等としてdockerに入る。

docker内でmpiexecを実行する時、オプションで別のノードを指定することもできるが、 hostfile が あると便利なのでこれを用意する。
host.txtみたいな雑な名前で良い。書き方は以下の通り。

hogehoge_server_001 port=2223 cpu=2
hogehoge_server_002 port=2223 cpu=2
hogehoge_server_003 port=2223 cpu=2
hogehoge_server_004 port=2223 cpu=2

cpu=2って書いてるけど、これ実際はgpuの数っぽい。(プロセス数という意なのかも)
各サーバでport=2223とport=2224をそれぞれGPU割り当てて、Dockerマウントして使いたい!みたいなのはhostfileでは書けない。


いざ実行。前述example内のtrain.pyの画像path等をちょっと変更して動かしてみる。

mpiexec --allow-run-as-root --mca btl_tcp_if_include ib0 -x PATH=$PATH -x PYTHONPATH=$PYTHONOATH -x LD_LIBRARY_PATH=$LD_LIBRARY_PATH -x CPATH=$CPATH -x LIBRARY_PATH=$LIBRARY_PATH -x NCCL_ROOT=$NCCL_ROOT --hostfile host.txt -np 8 python3 train.py

dockerのrootで走ってもらう必要があるので--allow-run-as-rootが必須。
また、Infinibandを使っている時は--mca btl_tcp_if_include ib0して指定しないと掴んでくれなくて遅いので注意。
npの後ろにGPUと同じ数を書いて実行コマンドを叩くだけ。


学習が走ってそうだったら終わりです。


 

- エラー対応とか -

結構つまり所なエラーについて書いておきます。
OpenMPI、クソデコレーションされたエラー文吐く時もあれば、うんともすんとも言わないときもあるのでイライラせず対応していきましょう。

mpiexec実行しても全く何も表示されない!

→ 相互sshが上手くいってないので普通にsshで別ノードの指定ポートにパスワードなしで飛んでdockerコンテナ内に入れるかチェック

orterun was unable to launch the specified application as it could not access or execute an executable ~~~

→ 相互sshが上手くいってないので普通にsshで別ノードの指定ポートにパスワードなしで飛んでdockerコンテナ内に入れるかチェック

mpiexecが動いてそうだけど学習前に止まる!

→ chainerのサンプルでいうとGPU掴むところで止まってるんだと思います。
hostfileの書き方が怪しい場合と環境変数が上手く渡せてない場合が多いです。
この場合エラーも出ず止まったままになるのでサッサとctrl+cで抜けて確認。

ORTE does not know how to route a message to the specified daemon located on the indicated node ~~~

sshsshdのconfig間違えてデーモン起動できてないんだと思います

クソ遅い!received unexpected process identifier!

→ infiniband掴めてないのでmpiexec時にこれ忘れてる --mca btl_tcp_if_include ib0
もしくは環境とオプションが合ってない

It appears as if there is not enough space for (the shared-memory backing

→ /tmpとかファイルシステムに書き込む物を実行した時にそこマウントしてないと起こりがち
エラー文を読めばなんとか。

Failed, NCCL error nvidia-sample.cu:88 'unhandled system error

→ NCCLがDockerの作る仮想ソケットを利用してしまうので、上記init.shもしくはそれと同等のものが動いてるか確認


 

- おわりに -

最初にも書きましたが多分k8sとか色んなツールが出てきてるので、こんなコツコツやる必要もなくなって行くと思います。

ただこれ貯めといても仕方ないし書いときました。何かしらの参考になれば幸いです。


最近uberが出しているTF, Keras, PyTorchのOpenMPIトレーニングのwrapperとして扱えるhorovodも、以上の方法で使えます。
uber/horovod: Distributed training framework for TensorFlow, Keras, and PyTorch.
github.com

horovodとかdistTF、pytorch.distributedの知見も徐々にアウトプットしていこうと思っています。がんばります。


 

PyTorchで学習済みモデルを元に自前画像をtrainしてtestするまで

- はじめに -

最初のステップとなる「学習済みのDeep Learningモデルをpre-train modelとして自分が用意した画像に対して学習」する時のメモ。

多分これが一番簡単だと思います。

 

- 準備 -

バージョンはtorch (0.4.1)、torchvision (0.2.1)の話をする。

pip install torch
pip install torchvision

学習済みモデルはpytorchの画像向けパッケージとなるtorchvisionでもサポートされている。
torchvisionで扱えるモデルは以下(2018/09/15 時点)

  • AlexNet
  • VGG
  • ResNet
  • SqueezeNet
  • DenseNet
  • Inception v3

参考:torchvision.models — PyTorch master documentation

 
最近はすごいスピードで他の高精度モデルや、仕組みの違う学習済みモデルが出てきてるので、pytorchのpretrainモデルを使う場合のサポートpackageを使うと良さそう。
以下のどちらでも良い。
GitHub - creafz/pytorch-cnn-finetune: Fine-tune pretrained Convolutional Neural Networks with PyTorch
GitHub - Cadene/pretrained-models.pytorch: Pretrained ConvNets for pytorch: NASNet, ResNeXt, ResNet, InceptionV4, InceptionResnetV2, Xception, DPN, etc.

pip install cnn_finetune
pip install pretrainedmodels

上記のtorchvisionに加えて以下が簡易に扱えるようになる(2018/09/15 時点)

  • ResNeXt
  • NASNet-A Large
  • NASNet-A Mobile
  • Inception-ResNet v2
  • Dual Path Networks
  • Inception v4
  • Xception
  • Squeeze-and-Excitation Networks
  • PNASNet-5-Large
  • PolyNet

モデルは「どのモデルがどんな感じの精度なん?」というのは以下READMEにimagenetでの精度比較表が載ってるので参考に。
https://github.com/Cadene/pretrained-models.pytorch#evaluation-on-imagenet

それぞれのモデルへのリンクも以下に存在する。
https://github.com/Cadene/pretrained-models.pytorch#documentation


 

- pretrainモデルで簡易に学習する -

cnn_finetuneの方がちょっとばかり楽できるので今回はcnn_finetuneベースで薦める。

分類するクラス数とモデルの入力となる画像サイズ、pretrained=Trueを指定して実行すると、学習済みデータがダウンロードされて読み込まれる。

from cnn_finetune import make_model
import torch

# cnn_futureを使う場合
model = make_model('pnasnet5large', num_classes=2, pretrained=True, input_size=(384, 384))
# pretrainedmodelsを使う場合
# model = pretrainedmodels.__dict__[model_name](num_classes=10, pretrained='imagenet')

# 'cuda' or 'cpu'
device = torch.device('cuda')
model = model.to(device)

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

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

また「学習画像は/hogehoge/train/なる配下に全て入っている」想定。

学習にはDataset、DataLoaderというクラスを利用する必要がある。
今回は雑に2クラスのデータセットを想定して書く。

# must: pip install pillow, pandas
from PIL import Image
from torch.utils.data import Dataset
import pandas as pd
import os

class MyDataSet(Dataset):
    def __init__(self, csv_path, root_dir):
        self.train_df = pd.read_csv(csv_path)
        self.root_dir = root_dir
        self.images = os.listdir(self.root_dir)
        self.transform = transforms.Compose([transforms.ToTensor()])
        
    def __len__(self):
        return len(self.images)
    
    def __getitem__(self, idx):
        # 画像読み込み
        image_name = self.images[idx]
        image = Image.open( os.path.join(self.root_dir, img_name) )
        # label (0 or 1)
        label = self.train_df.query('ImageName=="'+image_name+'"')['ImageLabel'].iloc[0]
        return self.transform(image), int(label)

train_set = ShipDataSet('hogehoge_train.csv', '/hogehoge/train/')
train_loader = torch.utils.data.DataLoader(train_set, batch_size=32, shuffle=True)

シンプルに画像とラベルを返す__getitem__とデータの大きさを返す__len__を実装するだけです。

バッチサイズは画像の大きさに合わせて調整すると良いです。
デカすぎると皆大好き「RuntimeError: cuda runtime error: out of memory」になります。

 
Optimizerを選ぶ。学習済みモデル使うならSGDでええんちゃうんと思ってるけどベストプラクティスは謎。

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

 
これで後は学習回すだけ。

import datetime

def train(epoch):
    total_loss = 0
    total_size = 0
    model.train()
    for batch_idx, (data, target) in enumerate(train_loader):
        data, target = data.to(device), target.to(device)
        optimizer.zero_grad()
        output = model(data)
        loss = criterion(output, target)
        total_loss += loss.item()
        total_size += data.size(0)
        loss.backward()
        optimizer.step()
        if batch_idx % 1000 == 0:
            now = datetime.datetime.now()
            print('[{}] Train Epoch: {} [{}/{} ({:.0f}%)]\tAverage loss: {:.6f}'.format(
                now,
                epoch, batch_idx * len(data), len(train_loader.dataset),
                100. * batch_idx / len(train_loader), total_loss / total_size))

for epoch in range(1, 10 + 1):
    train(epoch)

以下examplesを参考にした。
https://github.com/creafz/pytorch-cnn-finetune/blob/master/examples/cifar10.py


 

- modelを保存する -

state_dictはモデルの構造だけ保存。
普通にsaveするとGPU等device関連情報も一緒に保存するため、別環境で動かす時面倒らしい。

torch.save(model.state_dict(), 'cnn_dict.model')
torch.save(model, 'cnn.model')

参考:https://pytorch.org/docs/master/notes/serialization.html


 

- predictする -

モデルを読み込む。
modelをそのままsaveした場合はloadで簡易に読み込めるが、state_dictした場合は以下のように。

import torch
from cnn_finetune import make_model

# モデル定義
model = make_model('pnasnet5large', num_classes=2, input_size=(384, 384))
# パラメータの読み込み
param = torch.load('cnn_dict.model')
model.load_state_dict(param)
# 評価モードにする
model = model.eval()

 
テスト時もDataLoaderが必要になるが、今回は上記train時に作成したMyDataSetクラスをそのまま使う。

test_set = MyDataSet('hogehoge_test.csv', '/hogehoge/test/')
test_loader = torch.utils.data.DataLoader(test_set, batch_size=32)

 
torch.no_gradとした上でmodelにデータを入力するだけ。
皆大好きclassification_reportを出す。

# must: pip install scikit-learn
from sklearn.metrics import classification_report

pred = []
Y = []
for i, (x,y) in enumerate(test_loader):
    with torch.no_grad():
        output = model(x)
    pred += [int(l.argmax()) for l in output]
    Y += [int(l) for l in y]

print(classification_report(Y, pred))

出力はSoftmax使えばクラス数分 [クラス1の予測値, クラス2の予測値, ...] となってるので、argmax取ってやれば予測クラスを出すことができる。


 

- おわりに -

最近インターン生にオススメされてPyTorch触り始めて「ええやん」ってなってるので書いた。

ちょっと複雑なモデル書く時の話や torch.distributed 使う話も気が向いたら書くと思うけど、TensorFlow資産(tensorbordとか)にも簡単に繋げられるし、分散時もバックエンド周りを意識しながら書きやすいので結構良い感じする。


 
追記:2018/09/16

以下の部分でtransforms内のメソッドを利用して入力正規化とかaugumentationも出来るのですが、今回省いています。

self.transform = transforms.Compose([transforms.ToTensor()])

そしたら「正規化はpretrainに合わせてやった方がいいのでは?」みたいな話がTwitterで発生しました。

結論としては、多分やったほうが良いみたいになったのですが、確証が今の所ないのでtransforms.Normalizeとかtransforms内の色々試して比較すべきみたいな感じです。