解釈可能な機械学習モデルを作るライブラリ Interpret を Docker で動かすときのメモ
解釈可能なモデリングを目的としたライブラリ interpret
を使うときに Docker で利用していると動かないという現象があったのでその解決方法です。
環境
python=3.7.7 / conda==4.8.3 で動作する docker を利用しています。interpret自体のversionは interpret==0.2.1 です。
FROM registry.gitlab.com/nyker510/analysis-template/cpu:1.0.4 RUN pip install -U pip && \ pip install \ interpret==0.2.1
現象
docker で起動している jupyter から interpret show を実行しても何も出てこない (本来は分析結果の画面が表示される)
from interpret import show from interpret.data import Marginal import pandas as pd df = pd.DataFrame(np.random.uniform(size=(10, 100))) marginal = Marginal().explain_data(df, df[0], name='train data') show(marginal)
原因
interpret は output として静的なコンテンツを生成しておらず、逐次 interpret がバックグラウンドで起動している API からデータを取得しているようです。試しに F12 の検証から開いてみると http://127.0.0.1:7001/140350020331408/
に対してリクエストを送っていることがわかります。
interpert は docker 内部で実行されているため, 内部での localhost:7001 と jupyter の実行環境の localhost が一致していないためリクエストが通らず、エラーになっています。
解決方法
- docker 起動時に interpret のための port を開放しておく (例えば 7001番など、これをAとします)
- interpret のサーバーを
0.0.0.0:A
に変更する (たとえば0.0.0.0:7001
)
変更は from interpret import set_show_addr
から行えます。例えば 7121
port でつなぐなら以下のような感じ
from interpret import set_show_addr set_show_addr(('0.0.0.0', 7121))
これを実行すると画面が表示されます。やったね。
リモートサーバーの時
適当なリモートサーバーで jupyter を起動している時もあると思います。その場合には set_show_addr
で指定するホストをそのサーバーの名前にしましょう。例えば https://www.example.com
で繋いているのであれば以下のような感じです。
from interpret import set_show_addr set_show_addr((https://www.example.com', 7121))
python: loggingの出力値を文字列として取得したい
python の logging で出力した info とかを文字列として取得したい! という場合の方法についてのメモです。
下準備
- 単純な logger と stream handler (コンソールへの出力のハンドラ) を用意します。
- 詳しくは https://docs.python.org/ja/3/howto/logging.html#logging-advanced-tutorial など参考にしてみてください。
今回は logger / hander 両方に INFO level をつけましたので info よりも重要度が高いものだけ console に output されるようになっています。
from logging import getLogger, StreamHandler, Formatter handler = StreamHandler() handler.setLevel('INFO') logger = getLogger('nyk.510') logger.setLevel('INFO') logger.addHandler(handler)
https://docs.python.org/ja/3/howto/logging.html#loggers
組み込みの深刻度の中では DEBUG が一番低く、 CRITICAL が一番高くなります。たとえば、深刻度が INFO と設定されたロガーは INFO, WARNING, ERROR, CRITICAL のメッセージしか扱わず、 DEBUG メッセージは無視します。
なるほど。というわけで、一旦試してみましょう。
logger.warning('warn') logger.info('foo') logger.debug('debug') # debug はでないよ warn foo
確かに debug は出ないようになっていますね。
loggingの出力値を文字列として取得
さて本題の logging の出力をテキストとして取得する、です。これは要するに上記の例で言うと warn / foo / debug みたいな文字列を取得したい、ということです。 結論をいうと StringIO を stream にもつような handler を作成して logger に付与すればOKです。 テキストとして取得するっていうのはだいたい log をどこかに保存したいとかいう気持ちがあると思いますので、ちょっとおしゃれな formatter にして時間等も取得できるようにしています。
log_capture_io = io.StringIO() stream_handler = StreamHandler(stream=log_capture_io) # オシャに formatting formatter = Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') stream_handler.setFormatter(formatter) stream_handler.setLevel('INFO') logger.addHandler(stream_handler)
この状態で logger に先と同じように log を記録します。
logger.warning('warn') logger.info('foo') logger.debug('debug') # debug は console にでないよ
log の取得
作成した StreamIO から getvalue
すればOKです。
- 通常のコンソールアウトプットは単に文字列だったが,
formatter
をリッチにしているので取得される文字列には何時 log が記録されたかなどの情報も入っている - コンソールの方も普通の
StreamHandler
に formatter を設定すれば時間も表示できる.
s = log_capture_io.getvalue() s.splitlines() ['2020-08-01 07:59:41,132 - nyk.510 - WARNING - warn', '2020-08-01 07:59:41,135 - nyk.510 - INFO - foo']
後片付け
ずっと handler が付いていると記録され続けるので、いらなくなったら消しましょう
- io の close
- handler のひも付けを logger から削除
removeHandler
# 終わったら消しましょう log_capture_io.close() logger.removeHandler(stream_handler) log_capture_io.closed # True
close してしまうと value はもう取れませんので注意
log_capture_io.getvalue() --------------------------------------------------------------------------- ValueError Traceback (most recent call last) <ipython-input-10-b5f4bf9c6e8d> in <module> ----> 1 log_capture_io.getvalue() ValueError: I/O operation on closed file
上記コードは gist にもありますので参考にしてください ;). Logging Recording · GitHub
機械学習なdockerfileを書くときに気をつけとくと良いこと
みなさん機械学習系の環境構築はどうやってますか? 僕は最近は Docker を使った管理を行っています。
特に師匠も居なかったので、ぐぐったり人のイメージを見たり手探りで docker をつかいつかいしている中で、最初からやっとけばよかったなーということがいくつかあるのでメモとして残しておきます。
大きく2つです。
- キャッシュは消す
- テストを書く
キャッシュは消す
ライブラリをいろいろと install すると大抵の場合ダウンロードしたファイルを保存されている場合が多いです。何かのタイミングで再びそのライブラリをインストールする際にはダウンロードしたファイルを使って、素早くインストールすることができます (この仕組みがキャッシュです)。
キャッシュがあると容量が重くなるという欠点があります。重たいイメージは pull に単に時間がかかりますから、システムとしてデプロイする時にトラフィックが多く発生してしまう + 更新に時間がかかるといったデメリットがあります。
ですのでライブラリを入れたあとにはキャッシュを削除すると吉です。
機械学習系だと基本的に ubuntu を base image として使うことが多いと思いますので、以下ではすべて ubuntu を使用していることを想定しています。 また、機械学習に必要なライブラリを入れるために使うコマンドは基本的に
- apt-get (or apt 絶対使う)
- pip (基本使う. conda only なら別.)
- conda (場合によって使う)
3つだと思いますので、それぞれ述べていきます。
apt-get の場合
ubuntu では apt-get
(もしくは単に apt
) でライブラリを入れますが、コマンドとしてキャッシュを消す機能がついていますので、それを使うのが便利です。
RUN apt-get autoremove -y &&\ apt-get clean &&\ rm -rf /usr/local/src/*
順番に
autoremove
で必要がないライブラリを削除してclean
でキャッシュされているすべてのライブラリを消して- インストールに使ったソースコードがある場合消す
ということをやっています。
pip の場合
pip には残念ながらキャッシュディレクトリを消すコマンドが用意されていません。一番簡単なのは pip install する時には cache を作らないようにする方法です。pip>=6 であれば no-cache-dir option をつけるとキャッシュせずに install が行われます。
pip install --no-cache-dir scipy
pip を最新版にしてからインストールすることが多いと思いますのでワンライナーで以下のように記述するといい感じです。
RUN pip install -U pip &&\ pip install --no-cache-dir scipy
インストールするライブラリはたくさんでてきくるので pip freeze で作れる requirements.txt
で管理する場合も多いと思います。その場合にも単に --no-cache-dir
をつけるだけです。
COPY /path/to/requirements.txt requirements.txt # あらかじめ requirements.txt を ADD or COPY などで # docker 内部に送っておくことが必要 RUN pip install -U pip &&\ pip install --no-cache-dir -r requrements.txt
もしくはインストールしたあとに cache directory を消すという方法もあります。Ubuntu なら場所は ~/.cache/pip
ですので全部ふっ飛ばせばよいです。
RUN pip install -r requirements.txt &&\ rm -rf ~/.cache/pip
Conda の場合
conda にはキャッシュ削除の clean
が用意されています。https://docs.conda.io/projects/conda/en/latest/commands/clean.html
いろいろインストールした最後に呼び出しましょう。
RUN conda install -y \ numpy \ scipy &&\ conda clean -i -t -y
その他
バイナリからインストールしたりする場合には使わないソースコードは消すようにしましょう。例えば noto font をインストールしたあとに元の .zip
を消すとかそういうことです。
テストを書く
スゴーク簡単でも良いので、いつも使うライブラリを import + 定型的な処理を行なうコードを .py
で書いておいて build 時にテストすると精神的に良いです。
例えばですが Mecab を使って文字列パースするような処理を書いていると、pip でインストールされていることはもちろんですが apt の方でもちゃんと mecab が入っていることが確認できます。
def test_mecab(): import MeCab tokenizer = MeCab.Tagger('-Ochasen') tokenizer.parse('おはようせかい')
具体的にはこれを build したあとのイメージに入れて pytest
を実行してあげます。
docker run -v ./test.py:/analysis/ my-image pytest ./
僕の場合はイメージの build は gitlab CI 上で行っているので CI で build したあとに registry に登録する前にテストを動かすような構成にしています。(gitlab ci の yaml 形式ですが script にあるコマンドが順番に実行されるということだけわかれば雰囲気はわかるかと思います。)
.build-docker: stage: build before_script: - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY script: - echo ${REGISTORY_URL} - export TAG=${REGISTORY_URL}/${IMAGE_NAME} - docker build -t ${TAG}:latest -f ${DOCKERFILE} ${CONTEXT} - docker tag ${TAG}:latest ${TAG}:${CI_COMMIT_SHA} - docker run -v ${PWD}:/analysis ${TAG}:latest pytest tests/${IMAGE_NAME} # < ここがそう - docker push ${TAG}:latest - docker push ${TAG}:${CI_COMMIT_SHA} variables: IMAGE_NAME: DOCKERFILE: # default values. if you use own values, override in variables scope. CONTEXT: . # ex.) registry.gitlab.com/atma_inc/analysis-template REGISTORY_URL: ${CI_REGISTRY}/${CI_PROJECT_NAMESPACE}/${CI_PROJECT_NAME}
こうしておくとテストに失敗すると登録自体が行われないので、登録されている == テストは通っているという安心感を得られます😃
例
自分がいつも使っているイメージ: https://gitlab.com/nyker510/analysis-template/-/blob/master/docker/cpu.Dockerfile
このリポジトリ analysis-template では dockerfile のコード管理 + master merge と tag を切った時に CI でビルド & registry に push ということをやっています。 内容に関しては昔詳しく書いたことがあるので、こちらも参考にしてみてください。
ドキュメントもあるよ!
こうしたほうがいい! 自分はこういうことをやっている とかあればコメント貰えると嬉しいですmm
JWT でのログインができるかどうかを確認するテスト
Django Rest Framework + allauth 使用時に JWT での Login ができているかどうかを念の為確認するテスト。
import pytest from django.urls import reverse from atma.shops.models import Shop from .factories import UserFactory @pytest.mark.django_db def test_authorized_by_jwt(client): """Json Web Token で login できる""" user = UserFactory() # type: User user.set_password('hogehoge') user.save() url = reverse('accounts_me') res = client.get(url) assert res.status_code == 401, res.json() auth_url = reverse('rest_login') res = client.post(auth_url, data={'email': user.email, 'password': 'hogehoge'}) assert res.status_code == 200, res.json() jwt_token = res.json().get('accessToken', None) assert jwt_token is not None header = {'HTTP_AUTHORIZATION': 'JWT {}'.format(jwt_token)} res = client.get(url, **header) assert res.status_code == 200, res.json()
- パスワードの設定は
user.set_password
で行なう - header を kwrgs で渡す
"HTTP_AUTHORIZATION"
を key にする
の3点がポイント