nykergoto’s blog

機械学習とpythonをメインに

解釈可能な機械学習モデルを作るライブラリ Interpret を Docker で動かすときのメモ

解釈可能なモデリングを目的としたライブラリ interpret を使うときに Docker で利用していると動かないという現象があったのでその解決方法です。

github.com

環境

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)

f:id:dette:20200928135625p:plain
show を call しても何も出てこない

原因

interpret は output として静的なコンテンツを生成しておらず、逐次 interpret がバックグラウンドで起動している API からデータを取得しているようです。試しに F12 の検証から開いてみると http://127.0.0.1:7001/140350020331408/ に対してリクエストを送っていることがわかります。

f:id:dette:20200928135652p:plain
リクエストが失敗している様子

interpert は docker 内部で実行されているため, 内部での localhost:7001 と jupyter の実行環境の localhost が一致していないためリクエストが通らず、エラーになっています。

解決方法

  1. docker 起動時に interpret のための port を開放しておく (例えば 7001番など、これをAとします)
  2. 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))

これを実行すると画面が表示されます。やったね。

f:id:dette:20200928135600p:plain
リクエストが200OKになって画面も表示される。

リモートサーバーの時

適当なリモートサーバーで 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 / 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つです。

  1. キャッシュは消す
  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/*

順番に

  1. autoremove で必要がないライブラリを削除して
  2. clean でキャッシュされているすべてのライブラリを消して
  3. インストールに使ったソースコードがある場合消す

ということをやっています。

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 ということをやっています。 内容に関しては昔詳しく書いたことがあるので、こちらも参考にしてみてください。

nykergoto.hatenablog.jp

ドキュメントもあるよ!

nyker510.gitlab.io

こうしたほうがいい! 自分はこういうことをやっている とかあればコメント貰えると嬉しいです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点がポイント