革専門の小売店で革を買ってデスクマットにした話

デスクマットを購入しようとした経緯

作業用のデスクを半年前に買い替えたところタイピング時にパームレストがぐらつくようになりました。

パームレストは木材を買ってヤスリがけした自作のものでゴム製の滑り止めをつけていたのですがあまり効果がなく、安定したタイピングに支障が出ていたので滑らないようにデスクマットをひくことにしました。

デスクマットに求める条件

探す方針を決めるためにデスクマットに求める条件を書き出してみました。

  • サイズは横100cm、縦35cm前後
    • マット部分と机部分の段差を気にしなくて済むように机と同じ横幅のもの
    • 机の天板を挟み込むモニターアームに干渉しないように奥行きは机よりやや短いもの
  • 滑り止めの効果がしっかりしている本革
  • メモを取るのに支障がない程度にざらつきが少ないもの
  • 机の上に飲み物を常備しているのでできれば水染みに強い素材
  • 色は黒か茶色

上の条件であれば耐水性のあるゴムや人工革(PUレザー)のほうが実用性は高いのですが、せっかくなら使っていて気分が上がり風合いの変化を楽しめる本革にすることにしました。また、レザーのデスクマットを購入した方のツイートを見てうらやましくなったというのもあります。

なぜ革専門の小売店に行こうと思ったのか

大手のECサイトやハンドクラフトの通販サイトで探したところ100cmのものが少なく、あったとしても縦幅が30cmしかなかったりと条件に合うものがなかなか見つかりませんでした。

そこで店で革を購入して自分で裁断する方針に切り替えました。

裁断することを考えると元の条件のサイズより大きな革でなければいけません。また、自分で革を選べるならいろいろ見比べたいので革の専門店が集まる浅草エリアで探すことにしました。

店探しはこちらのサイトが参考になりました。

kumosha.com

店内で購入するまでの流れ

店内にはところ狭しと革が並んでいます。お店の方に許可をいただき撮影した写真はこちらです。実際にはこの10倍くらいの広さがあります。

f:id:kuromt:20210104160024j:plain

店の方に探している革の条件を伝えて相談したところ100cmというのは成牛であってもなかなか取れないサイズなのでより大きいものから裁断して購入したほうがいいとアドバイスをもらい、このとき初めて気に入った革で指定したサイズで購入することができることを知りました。

他にも革についていろいろ教えていただきました。たとえば革は10cm x 10cmを1ds(デシ)とする規格が定められていて購入価格は革の面積とdsあたりの値段で決まる(1dsが100円だと30dsで3千円、60dsで6千円)ことであったり、革の染め方には顔料を使うものと染料を使うものがあり顔料で染めた革の方が水に強いなどです。

話を聞いてから店内を見てまわり他の革に比べて厚みがあり触っていて気持ちがよかった牛のヌバック(表革をヤスリ掛け加工して起毛させたもの)に決めました。もともと挙げていた水染みに強いという条件には合わないのですがぬれてオイルが抜けても革靴用オイルでメンテナンスすれば多少は長持ちするそうです。

下の写真は実際に裁断されている様子です。分厚い革がいい音を立てながらはさみで裁断されているのは見ていて面白かったです。

f:id:kuromt:20210104160015j:plain

帰宅後にも床に置いて写真を撮ってみました。

f:id:kuromt:20210104160013j:plain

使ってみて

実際に机に置いた様子がこちらです。

f:id:kuromt:20210104175930j:plainf:id:kuromt:20210104193147j:plain
左: before 右: after

f:id:kuromt:20210104184452j:plain

タイピングしてみるとパームレストのぐらつきがなくなり手首の位置が安定するようになりました。ヌバックは起毛しているためか摩擦が強く多少の力ではびくともしません。

値段は税込で5千円以下でした。既製品で本革の大型デスクマットを買った場合に比べればかなり安く済んだと思っています。

新しい分野の勉強ができたことを含め総じて良い買い物ができました。

革を買うときの注意点

ここまで良い点ばかりを挙げてきましたが買った革をデスクマットにすることはメリットだけではありません。とはいえデメリットというほどではないと感じたので注意すべき点として紹介します。

たわみがある

先ほどの写真でも目立ちましたが革の端になるほど強いたわみがありました。素人考えですが小さな革(15ds以下)ではたわみの印象はなかったので大きな革の特徴なのかもしれません。

とりあえずはヘッドホンスタンドで挟んだり重りを乗せて伸ばすことにしています*1。キーボードやパームレストを置く中心部分は平らなので実用上は困っていません。

f:id:kuromt:20210104175922j:plain

革には傷があるので購入前によく確認する

これは文字通りです。元々知識としてはあったのですが実際にたくさんの革を見ていると想像よりも目立つ傷が多く、手に取った革には傷があって当たり前と思うくらいでちょうどよいと思いました。

今回購入した革はヌバックで起毛しており多少の傷は手でこすれば目立たなくすることができます。

匂いをたしかめる

このご時世なので店内でもずっとマスクをつけていたのですが、革には匂いがつきものなので購入前に確認すべきだったと帰宅してから気づきました。幸い購入したものは匂いはほとんどありませんでしたが匂いが気になるほどだったら買い直しになっていたので反省しています。

*1:SRE本シリーズや詳解システムパフォーマンスを積み上げています

MLflow Pluginの作り方

この記事はMLOps Advent Calender 2020の21日目の記事です.

日本でもMLflowの利用事例が増えてきていて特に実験管理に使われることが多いです. 特に2020年はMLflowを使った記事がいくつも公開されています.

MLflowは実験管理以外にもモデルの管理機能やモデルのサービング, 処理の実行機能があるのですがそれらはあまり使われていない印象です. モデルのサービングはTensorflowやPyTorchといった学習フレームワーク自身がサポートしはじめていていくつかMLOpsの事例を見てもそれらを使うケースが多いようです. 一方で今年のData + AI Summit Europe 2020ではMLflowのモデルサービングをプロダクション環境で使った発表もあり今後増えていくこともあるだろうと思っています.

メタデータ管理としてのMLflow

MLflowの実験管理はGoogleMLOps: 機械学習における継続的デリバリーと自動化のパイプラインでいえばMLOps Level1のメタデータ管理の機能に相当します. メタデータ管理が記録するのはMLの再現性の担保やデバッグに活用される実行時の情報です.

大手クラウドプロバイダや海外のベンチャー企業が様々なMLOpsのためのサービスを提供していますが単一のマネージドサービスで全ての要件が賄えることは稀です. 実際に使ってみると足りない機能が次々に見えてきて別のシステムと繋ぎ合わせることで補うということが多くあります. これらのシステムでも実行時の情報が必要になることもあり, MLflowをメタデータの管理として扱うのであれば連携するシステムで必要な情報もMLflowに保存したいと思うのは自然な流れです.

実際に導入するには追加で保存したい情報を整理しチームや組織の間でそれらに対応するタグを決めて mlflow.set_tag で保存する方法が考えられます. しかし人間は間違えるものなのでコードの中で一つ一つ指定するのではなく自動的に情報を記録する仕組みを作るべきです.

このような場合にMLflow Pluginが有効です.

MLflow Plugin

MLflowにはpluginの仕組みがあります.

仕組みは単純でPythonのEntrypointパッケージを使い特定のエントリーポイントを持つPythonパッケージをロードしてMLflowのpluginの抽象クラスを継承したクラスの特定のメソッドを実行すること実現されています.

pluginの抽象化クラスは用途ごとに分けられています. MLflowの実験管理で特定の情報を自動的に保存するには mlflow.run_context_provider をエントリーポイントとするPythonパッケージで mlflow.tracking.context.abstract_context.RunContextProviderを継承したクラスを実装します.

RunContextProviderクラスは二つのメソッドを持ちます.

  • def in_context(self):
    • Boolを返す. Trueを返すとmlflowのクライアントが tags メソッドを実行しFalseを返すと何もしない.
    • 特定の状況下ではpluginの処理を実行したくない場合に使う.
  • def tags(self):
    • MLflowに記録させたいtagをDictで返す.

pluginのexampleとしてmlflowのクライアントのバージョンを client.version タグに記録する mlflow-plugin-example プラグインを用意しました. (kuromt/mlflow-plugin-example)

実装はシンプルです.

from mlflow.tracking.context.abstract_context import RunContextProvider
import mlflow

class LoggingExample(RunContextProvider):
    def in_context(self):
        return True

    def tags(self):
        return { 'client.version': mlflow.__version__ }

このpluginを実装するときは二つ注意があります.

一つ目は独自に定義するタグの名前付けです. MLflowのタグには利用者が任意に指定するタグとMLflowが内部で予約しているシステムタグがあるのですがpluginはMLflowのシステムタグも上書きできてしまうので意図的に上書きしたくない場合はタグの名付けに注意してください.

二つ目はRunContextProviderを継承したpluginの処理が実行されるタイミングです. このpluginは mlflow.start_run() を実行するタイミングで処理されます. そのため自動的に保存できるのは mlflow.start_run() を実行する前までに確定した情報のみです.

import mlflow

# ここまでの処理内容と環境情報は自動的に保存できる

with mlflow_start_run():
  # ここの処理は自動保存できない

実際に実行してみる

MLflowとpluginの動作確認のためにJupyterhubとMLflow+MySQLの環境をDockerコンテナで構築するためのdocker-composeを含むデモ用のリポジトリを用意しました.

まずはデモ用のリポジトリからcloneして事前準備をします.

$ git clone https://github.com/kuromt/mlops_advent_calendar2020_demo.git
$ cd mlops_advent_calendar2020_demo/docker-compose/

移動したディレクトリにMySQLのパスワードを記述したファイルを作ります.

$ cat << EOF > .env
> MYSQL_ROOT_PASSWORD="mlflow"
> MYSQL_DATABASE="mlflow"
> MYSQL_USER="mlflow"
> MYSQL_PASSWORD="mlflow"
> EOF

続いてJupyterhubとMLflow + MySQLのコンテナを立ち上げます. 初めて立ち上げるときは自動でイメージのビルドが走ります.

$ docker-compose up -d

コンテナが立ち上がるとlocalhostの80ポートでMLflowに, 8080ポートでJupyterhubにアクセスできるようになります.

まずはpluginなしで実行してみます. JupyterのNotebookで以下を実行します.

import mlflow

tracking_server = "http://mlflow"
mlflow.set_tracking_uri(tracking_server)

with mlflow.start_run():
    mlflow.log_param("param1", 1)

MLflowのUIにアクセスすると "default" の Experimentsにrunが追加されています.

f:id:kuromt:20201220174353p:plain
mlflow1

次にJupyterhubの環境に先ほどのpluginをインストールします.

$ pip install git+https://github.com/kuromt/mlflow-plugin-example

インストールが終わればあとは自動的にmlflowのクライアントのバージョン情報を保存するpluginが有効になります. pluginの動作を確認するために先ほどのNotebookを再実行してみましょう. MLflowのpluginは import mlflow のタイミングで登録されるので先ほどの実行で使ったカーネルをrestartが必要です.

f:id:kuromt:20201220174901p:plain

カーネルをrestartした後に先ほどと同じコードを実行しました.

MLflowのUIを確認してみると先ほど登録されていなかった client.version タグが記録されており, 自動的に実行時の情報が記録されていることが分かります.

f:id:kuromt:20201220175017p:plain
mlflow_with_plugin

このようにMLflow Pluginを使うことで独自に決めた実行情報のメタデータの保存を自動化できるようになります.

また, 保存したい情報が増えても実験用のコードを修正するのではなくpluginの機能を拡張するだけで済むので拡張性も高くなるというメリットもあります.

まとめ

この記事ではMLflowの実験管理を拡張するpluginの実装方法を紹介しました.

他のpluginを作りたい場合は Writing Your Own MLflow Plugins を参考にしながら実装してください.

MLOpsの事例やツールの情報収集

この記事はMLOps Advent Calendar 2020の6日目の記事です。

MLOps関連の情報を入手するのに大変お世話になっている便利なサイト等を紹介します。 情報収集のために毎朝見ているサイトや購読しているメルマガからMLだけに興味があるという人向けに次の条件を満たすものを選びました。

日本語

ML-News

何か新しい話がないかと思ったときにまず見に行くのがこのサイトです。おそらくTwitterのアクティビティを見て掲載する記事が決まっており、Twitterを見ていなくてもここを見るだけで注目度が高い記事を知ることができます。

取り上げられるのは日本語の記事が多いですが海外の記事でも注目度が高いものは掲載されます。一度掲載された内容は1日近く残るので頻繁に確認しなくても取りこぼすことはなくとても便利です。

このサイトを運営している方にはとても感謝しています。

Weekly Kaggle News

週に一度更新されるメルマガです。名前のとおりKaggleのコンペの話題を扱っているのが他にはない特徴です。メルマガを配信されている方はKaggleコンペに参加されている方で配信される記事のコメントも参加者ならではの視点でためになります。Kaggle以外にもMLを使った事例紹介やツールの話が取り上げられることもあります。

英語

Databricks Blog

SparkやHadoop、その他データ分析のOSSを使ったシステムの事例紹介や各種OSSの仕組みや新機能が取り上げブログとしてまとめられています。DatabricksはSparkやMLflowをリードしている企業でそれらの最新情報も得られます。

分散処理が必要ないわゆるビッグデータを扱う事例・技術が中心であるためか取り上げられる事例は大規模なシステムであることが多く読んでいて面白いです。

Venturebeat

主に企業が取り組んでいる研究内容や自社サービスに適用したAI事例の紹介です。

これを読み始めて面白い事例だったり研究なのに日本のメディアでは取り上げられないものもあるんだなと思うようになりました。

ML in Production

weeklyに届くメルマガです。タイトルの通り実環境や実際のサービスにMLを適用する際に気を付けるべきノウハウや実際の事例を中心に紹介しています。自分はMLOpsに万能な仕組みやアーキテクチャは存在せず、基本を押さえたあとは自分たちの環境に合わせた仕組みを適用することが大事だと感じているのでこういった実際の事例は参考になります。

AI Weekly

ML in Productionに似ていますがツールの紹介も含むことがあるのが特徴です。個人的にはML-Newsに近い位置づけで読んでいます。

Project to Know

タイトルにはついていませんがこれもweekly系です。毎回3つのPerpersと3つのProjects、3つのContentsを紹介しています。アカデミックな分野と実務の両方のニュースを配信してくれています。研究者にとってもサービス開発者にとっても有用な情報があります。

お願い

他にも面白いトピックを扱っているサイトやメルマガがあればどんどん増やしていきたいので、これは見ておいた方がいいというサイトやweeklyなどがあればTwitterのつぶやきでもこのブログのコメントでもどこでもいいので教えてください。

MLOps好き仲間の研究者やエンジニアにこんなサイトやメルマガがあったのかと思えてもらえることほどこの記事を書いて良かったと思うことはありません。

お手軽で欲しい機能が揃っている実験管理ツールGuild AIの紹介

機械学習の実験管理ツールにGuild AIというものがあります。特に大きな特徴はコード追加なしで実験管理ができるというものです。

試しに触ってみたところ、まさにコード追加なしで簡単に試せる、ちょっとした条件を変えた実行も簡単、結果の可視化はシンプルなCLIもリッチなGUIもどちらも用意されている、ローカルだけではなくS3にもデータを保存できる、しかもWebサーバを別に立てる必要がなく手元の環境で完結します。

ただ、Guild AIは一部の方にSNS上で言及されているものの日本語で書かれたドキュメントやブログは見つかりませんでした。良いツールが埋もれるのはもったいないと思いGuild AIの記事を書くことにしました。

この記事の前半では実行条件を変えながら実行して結果を可視化するまでの流れを紹介します。Guild AIのお手軽さをお伝えすることを意識しました。

後半はある程度実務での状況を想定し複数の人が作業していても欲しい結果のみを参照する方法を紹介しています。

他にも紹介しきれない機能はあるのですが、最後にある資料からドキュメントを読んでみてください。簡単にですがここに書いていない機能にどんなものがあるかをリストにしてあります。

目次

この記事に書いていること、書いていないこと

  • 書いていること
    • Guild AIの手軽さを体験する
    • 条件を変えながら実行する
    • Optimizerを使って複数の実験条件をバッチ実行する
    • CLIを使った実行結果の管理、検索
    • GUIを使った実行結果の管理、検索
    • 他の実験と区別した実行、検索
  • 書いていないこと
    • パイプライン実行
    • 実験のパッケージング
    • 実験情報をS3に保存する
    • リモート環境で実行
    • 他の実験管理ツールとの機能比較

ここで紹介しきれない機能は ドキュメントexamples をご覧ください。

GuildAIの紹介

Guild AIを手軽さを体験する

まず、guildaiパッケージをインストールしましょう。これで guild コマンドがインストールされます。

$ pip install guildai

次に実行する度に違う値を返すrun_random.pyを用意します。これを実験用スクリプトとみなしてguildの手軽さを体験していきます。

import random

start = 0
end = 10

print("result: %f" % random.uniform(start, end))

さっそく用意したスクリプトguild runで実行します。

$ guild run run_random.py 
Refreshing flags...
You are about to run /home/jovyan/work/run_random.py
  end: 10
  start: 0
Continue? (Y/n) y
result: 4.966394

元コードに手を加えていないのにguildコマンドがスクリプト中の startend 変数を認識していることがわかります。これはguildaiがスクリプトグローバル変数の定義を読み込みflagとして認識するためです。

このまま何度か乱数を出力するスクリプトを実行してみます。

$ guild run run_random.py  -y
result: 7.314514
$ guild run run_random.py  -y
result: 2.449769
$ guild run run_random.py  -y
result: 1.361825
$ guild run run_random.py  -y
result: 6.973784

合計5回実行しました。それでは実行した結果を見てみましょう。 resultが小さいものから上位3つを取り出してみます。

$ guild compare --table --min result --top 3
run       operation      started              time     status     label  end  start  step  result
9d9b1c1d  run_random.py  2020-09-10 12:08:02  0:00:00  completed         10   0      0     1.361824
e0f6d49c  run_random.py  2020-09-10 12:08:01  0:00:00  completed         10   0      0     2.449769
99a5dbbd  run_random.py  2020-09-10 11:58:22  0:00:00  completed         10   0      0     4.966393

4番目に実行した1.361824が一番上に表示されています。

guildaiはkey: valueのフォーマットで標準出力に出された数値を認識します。これをguildaiではScalarsといいます。このフォーマットは設定で変更することもできます。

先程のスクリプトでは"result"をScalarsのkeyにしていたので--min result を引数に指定することで生成した乱数を降順に取り出せました。機械学習の実験の場合は評価関数の結果を出力することでモデルの最適化に応用できます。

ここまででguildaiがどれだけ簡単に実験管理できるか雰囲気を感じることができたと思います。とはいえ乱数を出力するスクリプトを叩いただけではつまらないので次はもう少し実験らしいことをしてみます。

乱数を生成する条件を変えながら実行する

先程は0から10の間の数値を生成していましたがもう少し大きな値を取り出したくなったとしましょう。

endを1000にして実行してみます。

$ guild run run_random.py end=1000 
You are about to run /home/jovyan/work/run_random.py
  end: 1000
  start: 0
Continue? (Y/n) y
result: 258.577733

先程と同じく何度か実行してみます。

$ guild run run_random.py end=1000 -y
result: 584.182183
$ guild run run_random.py end=1000 -y
result: 111.938457
$ guild run run_random.py end=1000 -y
result: 430.517918

実行した結果を確認しましょう。先程はCLIを使いました。CLIを使った確認は手軽ですが今回はguildaiの独自GUIであるViewで結果を確認してみましょう。

guild viewコマンドを実行します。実行したらブラウザから http://localhost:10000 にアクセスします。

$ guild view --port 10000 --host 0.0.0.0
Running Guild View at http://localhost:10000

ブラウザにアクセスすると個別の実験内容の詳細を確認できるダッシュボードが表示されます。

f:id:kuromt:20200910212807p:plain

guildaiはこのView以外にもTensorBoardを組み込んでいます。Viewはguildaiの独自概念であるrunの可視化のために使い、パラメータの可視化は高機能なTensorBoardを使うという役割分担になっています。

Viewの左上のボタンからTensorBoardを開いて end で与えたパラメータによって result がどのように変化したか確認できます。

f:id:kuromt:20200910213558p:plain

次はオープンデータのサンフランシスコの住宅価格データを使って実際にモデルを作る実験管理をしていきます。

オープンデータを使った実行

サンフランシスコの住宅価格データをダウンロードして保存します。

from sklearn.datasets import fetch_california_housing
import pandas as pd

data = fetch_california_housing()

data_df = pd.DataFrame(data.data, columns=data.feature_names)
target_df = pd.DataFrame(data.target, columns=["target"])
full_df = pd.concat([data_df, target_df], axis=1)

from sklearn.model_selection import train_test_split

def split_and_save(dataframe=None, target_path=None):
    train, validate = train_test_split(dataframe, test_size=0.3)
    train.to_csv(target_path + "/train.csv")
    validate.to_csv(target_path + "/validate.csv")

split_and_save(full_df, "./data")

次に、住宅価格をxgboostの回帰で学習するスクリプト train_validate.pyを用意します。

import pandas as pd
import xgboost as xgb

train_data_path = "/home/jovyan/work/data/train.csv"
validation_data_path = "/home/jovyan/work/data/validate.csv"

train_df = pd.read_csv(train_data_path)
validation_df = pd.read_csv(validation_data_path)

x_train = train_df.drop("target", axis=1)
y_train = train_df["target"]

max_depth = 1
min_child_weight = 0.1
learning_rate = 0.1

clf = xgb.XGBRegressor(max_depth=max_depth, min_child_weight=min_child_weight, learning_rate=learning_rate)

xg_model = clf.fit(x_train, y_train)

x_predict = validation_df.drop("target", axis=1)
y_predict = validation_df["target"]

predict = xg_model.predict(x_predict)

from sklearn.metrics import mean_squared_error
mse = mean_squared_error(predict, y_predict)

print("mse: %f" % mse)

乱数生成のスクリプトで試したようにmax_depthlearning_weightなどのパラメータを外部から与えてguild runを何度も実行しパラメータ最適化をするんだろうと想像がつくかと思います。

しかし、この環境には乱数生成のスクリプトを使った実行結果が残っています。このまま実行すると今回の実行と前の実行結果が混ざって表示されてしまい確認したい結果に集中できません。 また、毎回違うパラメータを指定して何度も実行することは大変です。

そこで、サンフランシスコの住宅の予測モデルの生成スクリプトの実行にpredict-house-priceタグを付与して乱数生成の実行と区別がつくようにします。

さらにguildaiのパラメータ最適化の機能を使いそれぞれのパラメータの探索範囲を指定し最大20回のトライアルでランダムサーチを実行します。

guildaiはランダムサーチの他にグリッドサーチ、ベイジアン最適化をサポートしているので目的に合わせた使い分けができます。

$ guild run train_validate.py learning_rate=[0.01:0.1] max_depth=[2:4] min_child_weight=[0.1:0.3]  --max-trials 20 --label predict-house-price
Refreshing flags...
You are about to run /home/jovyan/work/train_validate.py with random search (max 20 trials)
  learning_rate: [0.01:0.1]
  max_depth: [2:4]
  min_child_weight: [0.1:0.3]
  train_data_path: /home/jovyan/work/data/train.csv
  validation_data_path: /home/jovyan/work/data/validate.csv
Continue? (Y/n) y
INFO: [guild] Initialized trial 669896de (learning_rate=0.028297447038768214, max_depth=4, min_child_weight=0.17322408721578914, train_data_path=/home/jovyan/work/data/train.csv, validation_data_path=/home/jovyan/work/data/validate.csv)
INFO: [guild] Running trial 669896de: train_validate.py (learning_rate=0.028297447038768214, max_depth=4, min_child_weight=0.17322408721578914, train_data_path=/home/jovyan/work/data/train.csv, validation_data_path=/home/jovyan/work/data/validate.csv)
INFO: [numexpr.utils] NumExpr defaulting to 2 threads.
mse: 0.348852
INFO: [guild] Initialized trial 2ce684f7 (learning_rate=0.07674815292169297, max_depth=2, min_child_weight=0.15809164243532572, train_data_path=/home/jovyan/work/data/train.csv, validation_data_path=/home/jovyan/work/data/validate.csv)
INFO: [guild] Running trial 2ce684f7: train_validate.py (learning_rate=0.07674815292169297, max_depth=2, min_child_weight=0.15809164243532572, train_data_path=/home/jovyan/work/data/train.csv, validation_data_path=/home/jovyan/work/data/validate.csv)
INFO: [numexpr.utils] NumExpr defaulting to 2 threads.
mse: 0.332837

パラメータ探索の実行が終わったらguild compareで結果を確認しましょう。-l predict-house-price を指定することで結果を絞りこんでいます。

$ guild compare --table --min mse --top 5 -l predict-house-price
run       operation          started              time     status     label                learning_rate  max_depth  min_child_weight  train_data_path                    validation_data_path                  step  mse
1f2013fe  train_validate.py  2020-09-11 12:30:39  0:00:02  completed  predict-house-price  0.095549       4          0.206510          /home/jovyan/work/data/train.csv  /home/jovyan/work/data/validate.csv  0     0.242914
1a13f5bb  train_validate.py  2020-09-11 12:30:35  0:00:02  completed  predict-house-price  0.069911       4          0.254429          /home/jovyan/work/data/train.csv  /home/jovyan/work/data/validate.csv  0     0.256736
673398fc  train_validate.py  2020-09-11 12:30:10  0:00:02  completed  predict-house-price  0.098693       3          0.163653          /home/jovyan/work/data/train.csv  /home/jovyan/work/data/validate.csv  0     0.264573
8e83cb7b  train_validate.py  2020-09-11 12:30:19  0:00:02  completed  predict-house-price  0.098743       3          0.133931          /home/jovyan/work/data/train.csv  /home/jovyan/work/data/validate.csv  0     0.266335
274f5aa2  train_validate.py  2020-09-11 12:29:56  0:00:02  completed  predict-house-price  0.087953       3          0.184524          /home/jovyan/work/data/train.csv  /home/jovyan/work/data/validate.csv  0     0.273010

TensorBoardでも確認してみます。ここでもラベルを指定して表示するrunを絞り込んでいます。

$ guild view -p 10000 -h 0.0.0.0 -l predict-house-price
Running Guild View at http://localhost:10000

f:id:kuromt:20200911213710p:plain

まとめ

guildaiをインストールしてコード追加なしにパラメータを変えた実行と結果の可視化、ラベルを使った実験管理の方法を紹介しました。

Kaggle用に良さそうなツールを探していたのですがguildaiが自分にいちばんハマる感覚があったので当分使ってみようと思います。

ちなみにguildaiは実験情報をS3に保存する機能があるのでminioを用意してそこに実験情報を保存してみようとしたのですがS3へのデータ保存が aws-cli を直接叩く実装*1になっていて保存先をminioに向けるための--endpoint-url を外から与える手段が見つからなかったため諦めました。*2

資料

*1:https://github.com/guildai/guildai/blob/5d32cdf6eca60ad4195c7432612e5a86dc48f8e8/guild/commands/s3_sync_impl.py#L85-97

*2:/etc/hostsで名前解決を捻じ曲げればできそうな気もしますが...

データ分析のための数理モデル入門

paypaymall.yahoo.co.jp

この本を買うきっかけ

TwitterのTLに著者の方のツイートが流れてきて興味をもったのがきっかけです。

本を買う前の印象

そのままTwitterで検索したりAmzonの口コミを見て

  • 初学者にも分かりやすいように数式を使わず数理モデルを平易に解説している
  • 網羅的に描かれていて辞書のように使える
  • 図が多くしかもフルカラー

といった特徴に惹かれて購入しました。

全体の感想

実際に読んでみると数式がまったくでないというわけではありませんが、微積を知っていれば問題ないものばかりです。数理モデルを理論をベースにして式変形で導き出すのではなく、最初から式を提示したあとに各項ごとの意味を解説してくれています。おかげで、頭の中で式変形を考えなくてもサラサラと読み進めていくことができました。

著者の方がたびたび書かれているように、データ分析を行うときにどの数理モデルを使えばよいかを考えるための指標を学ぶことができました。これからデータ分析の理論を学ぶ入門書として素晴らしい本だと思います。

データ指向アプリケーションデザインのまとめ

www.oreilly.co.jp

昨年夏から続けてきたデータ指向アプリケーションデザインの輪読がほぼ終わったので、書かれていた内容を咀嚼するために理解した内容をアウトプットすることにしました。

それぞれの技術レイヤで一般的な選択肢と選択基準を文章化して、後から見返したときにデータシステムを設計の参考になるように基準となる軸が明確になるよう意識しています。

詳しい方から見ると私の理解が甘く間違いを書いてしまっているところもあるかもしれません。見つけられたらご指摘下さい。

整理した観点

単一のシステム

データモデルの選択

選択肢

選択基準

スキーマに柔軟性を持たせるかとアプリケーションにデータのマージが必要かどうかで判断する。

パフォーマンスを考えると、スキーマを変更することがなければスキーマオンライト(RDB)、スキーマに柔軟性を求めるならスキーマオンリードのデータモデル。

スキーマオンリードを選択したときの観点。ドキュメントデータベースは結合のサポートが弱い。グラフデータベースはドキュメントベースに比べてデータの持ち方が複雑。単一のデータだけを扱えればいいならパフォーマンスが高いドキュメントデータベースを選び, 関係性の抽出を行うパフォーマンスを重視するならグラフデータベースを選ぶ。

ストレージエンジンの選択

選択肢

  • データの持ち方
    • 行指向
    • 列指向
  • インデックスの有無
    • インデックス無し
    • インデックス有り
      • インデックスはメモリ上に保持
        • ハッシュインデックス
      • インデックスをファイルに保持可能
        • SSTable
        • LSMツリー(SSTableにlog-structuredを取り入れたもの)
        • Bツリー

選択基準

データを扱うアプリケーションのワークロードがトランザクション処理(OLTP)なのか分析(OLAP)なのかで選択する。

OLTPは全てのデータの中から一部を取り出して扱うのでランダムアクセスに強くなるようにインデックスが有る方がよい。データの持ち方は行指向。

OLAPは一度に大量のデータをスキャンするのでデータのアクセスができるだけシーケンシャルになるように列指向でデータを保持するのがよい。インデックスの有無は必要に応じて決める。

インデックスを有りにするとデータの読み込みとクエリ実行のパフォーマンスが上がるがデータ追記のパフォーマンスが下がる。

インデックスを有りにすると決めたのならほとんどの場合でBツリーを選べば良い。Bツリーはコンタントにパフォーマンスが高くなる。LSMツリーを採用するのはコンパクション中にパフォーマンスが落ちることを許容できてそれ以外のときにBツリーよりも高いスループットを求めるとき。

ただし、OLAPで列指向のデータの持ち方をして、かつインデックスを有りにするならLSMツリーを採用する。Bツリーは使えない。

データシステム間の通信

選択肢

  • エンコード(デコード)方式
    • 言語バインド有り
      • バイナリフォーマット
        • 言語固有のフォーマット
    • 言語バインド無し
  • データフローの形式
    • データベース経由
    • サービス呼び出し経由
      • REST
      • RPC
    • 非同期のメッセージパッシング経由

選択基準

テキストフォーマットはバイナリフォーマットに比べてデータサイズが大きくなる。ただし、人間が読みやすいメリットがある。

バイナリフォーマットにスキーマが有ると、そのデータを扱うアプリケーションの前方互換性と後方互換性の保証が可能になる。ただし、それらを保証できるかはフィールドに必須フィールドをいれるのか、フィールドに初期値を設定するかといった設計による。

ThriftとProctocol Buffersに比べてAvroはデータサイズが小さい。その代わり、Avroはスキーマ中のフィールドの順番がライターとリーダーで一致しない。フィールドの値の扱いはリーダーが決める。

エンコード方式で言語固有のフォーマットを選択することはまずない。

  

データフローの形式を考える。通信相手に送信したあとレスポンスを返して欲しいときはREST。レスポンスが不要な場合は非同期のメッセージパッシング経由。後者は送信相手が一時的に応答負荷になっていたとしても正常に戻った後にメッセージを受け取ってもらえることが期待できる。ただし、メッセージが届くことが保証されているわけではない。(exactry-onceのメッセージブローカがれば保証される?)

データベース経由とRPCについては力不足で選択基準を整理できていなかった。データベースの扱いを除外するのはローカルのデータベースなのかリモートのデータベースなのかで扱いが変わるため。リモートのデータベースの場合は分散システムの話が関わってくる。レプリケーションやパーティショニングの有無やトランザクションの種類, 求める一貫性の強さ, 必要なパフォーマンスでデータベース経由を採用できるか決まる。RPCを除外するのは完全に自分の力不足。*1

高負荷への対応

水平スケールを選択肢にいれており続く大規模環境とも範囲が重なるが、垂直スケールとの比較のために小規模環境の節にいれることにした.

選択肢

  • 垂直スケール
  • 水平スケール

選択基準

コストをかけることができて現実に構築できるマシンスペックでデータシステムを運用できるなら垂直スケール。コストを抑える代わりに分散システムの様々な問題に対処していくことができるなら水平スケール。

一区切り

「高負荷への対応」で水平スケールを採用するならば 単一システムで扱った範囲に加えてこのあとに続く分散システムについても設計を行う.

分散システム

パーティショニング

選択肢

  • パーティショニング無し
  • パーティショニング有り
    • パーティショニング方式(キーバリュー型のデータの場合のみを考える)
      • レンジパーティショニング
      • ハッシュパーティショニング
    • セカンダリインデックス
    • リバランスの戦略

選択基準

分散システムでデータやクエリの負荷をある程度均等に分配したいときにパーティショニング有りを選ぶ。

ここではデータがキーバリュー型であることを想定する。

パーティショニングの方式はレンジパーティショニングとハッシュパーティショニングが基本になる。どちらを選ぶかはデータの偏りによる。パーティションに含まれるデータができるだけ均等になるように選ぶ。

セカンダリインデックスが必要になるのはプライマリーキーに加えて別の条件でも検索したくなるとき。セカンダリインデックスを採用すると読み取りのパフォーマンスが上がる一方、セカンダリインデックスの更新が必要になるので書き込みのパフォーマンスは下がる。

セカンダリインデックスはローカルインデックスとグローバルインデックスの二種類。全体に対する検索ではグローバルインデックスがある方が効率的だが、データを追加するときに発生するセカンリインデックスの更新処理はローカルインデックスと比べて重い。一方で、絞り込みの検索ならローカルインデックスの方が効率が良い。グローバルインデックスが必要になる代表例は全文検索システム。

リバランスの戦略はデータ量の変動とノード数の変動が発生したときにパーティションを均一に保つかどうかで決める。均一に保つことを諦めるかわりにシンプルに運用できるのがパーティション数固定。均一に保つことを目指すならデータ量に合わせてパーティション数を増やす。一方、データ量が増えるときにパーティションごとのデータ量を均等にするだけではなく、ノードあたりのデータ量も均等にすることを目指すならノード数による変動を選ぶ。

レプリケーション

選択肢

選択基準

分散システムで耐障害性を持たせない選択肢はないので実質レプリケーション無しは選択肢に入らない.

レプリケーションアーキテクチャはデータの一貫性と可用性のバランスで決める。書き込みの衝突を考慮せずに済むようにしたいならシングルリーダーを採用する。ただし、ノードやネットワーク障害、レイテンシのスパイクに弱い。マルチデータセンタを考えるならシングルリーダだと耐障害性に不安があったりレイテンシの問題が出てくるのでマルチリーダ。どのレプリケーションアーキテクチャあっても読み取りの整合性の考慮は必要。

リーダレスが選択肢に入る基準は整理できていない。単一データセンタに閉じていてノード障害に強くしたい場合?

レプリケーション方式は単純だが決定性に欠けるステートメントベース、決定性はあるがストレージフォーマットのバージョンが必ずリーダーとフォロワーで一致していないといけないため運用が難しいwrite-aheadログの転送、ストレージフォーマットのバージョンに後方互換性を持たせることができる論理(行ベース)ログ、バグを仕込んでしまう可能性が他より高いが自由度が高いトリガベースから選ぶ。

データ処理のエンジン

選択肢

選択基準

有限のデータを処理した結果が欲しいならバッチ処理。データの量が不定でその都度入ってくるデータを処理して結果が欲しいならストリーム処理。

一区切り

レプリケーションの一貫性とトランザクションの一貫性の覚書を書いておく。アプリケーションの要件から具体的なデータベースを選ぶときの参考としての覚書。

レプリケーションによって発生する読み取り(R)と書き込み(W)の問題

ここで扱う問題は全てレプリケーション間のコピーが非同期、あるいはリーダーと1つのフォロワーは同期的にコピーされるが残りのフォロワーとは非同期でコピーされる準同期であることに起因したもの。クライアントが一つしかなくリクエストに並行性がなかったとしてもレプリケーションの方法やタイミングによって整合性が取れなくなることがある。

リーダーと全てのフォロワーでデータのレプリケーションが同期的に行われることはパフォーマンスを考えると採用されない。

リーダーのアーキテクチャ 一貫性の性質 防げるトラブル
シングルリーダー read-after-write一貫性 「自分が」書いた内容をすぐにまた読み込むと値が空っぽ、あるいは上書きする前の結果が返ってくる
モノトニックな読み取り レプリケーションのコピーを実行中に複数のクライアントから読み込み処理があったとき、発生順序が先だったクライアントの読み込みがコピー後の正しい結果で、発生順序が後だったクライアントの読み込みがコピー前のものになる
一貫性のあるプレフィックス読み取り 因果関係がある書き込みがあったとき、その書き込み結果を読み込むと因果関係が無視した順序で読み込んでしまう(e.g. 質問とそれに対する答えの会話の書き込みがあったのに、答えの文言を先に読んで後から質問の文言を読んでしまう)
マルチリーダー シングルリーダーであったこと全部 シングルリーダーであったこと全部
LWWなど 書き込みの衝突
リーダーレス クオラム(条件付き) 読み取り内容が最新である保証がない
読み取り修復 レプリカ間のデータが等しい保証がない
エントロピー処理 レプリカ間のデータが等しい保証がない
LWWなど リーダーレスには順序制御がない
バージョンベクトル 複数クライアントが書き込んでいるとき、上書きなのか並行書き込みなのかを判断できない

トランザクションの一貫性と分離レベル

この話は分散システムもレプリケーションも前提ではない。*2

クライアントが複数あって、それぞれが単一オブジェクト、あるいは複数オブジェクトを操作するときの一貫性の強さ。

  • 直列化可能性は最も強い一貫性。並行して実行された複数のトランザクションがあったとしても、順番に行儀良く実行されたときと同じ結果になる。悲観的ロックである2PLで実現する。
  • 直列化可能なスナップショット分離。楽観的ロックで実現する

ただし、2PLがパフォーマンスを悪化させるため現実的ではない。現実的には分離性を確立して並行したトランザクションを受け入れる。*3

分離性には以下のような例がある。

対応策 防げるトラブル
スナップショット分離(SQL標準で定義される概念で一番違いのはリピータブルリード) 読み取りスキュー
アトミックな書き込み 更新のロスト
ロック*4 更新のロスト
compare-and-set*5 更新のロスト
衝突の実体化(ロック) 書き込みのスキュー*6
read-committed ダーティリードとダーティライト

メモ

  • ハッシュインデックスとBツリーの比較は?
  • もしも大量の画像を扱うならオブジェクトストレージを選択することになると思うが、まとめた内容で関連するのは耐障害性/高可用性に関する部分のみ?
  • レプリケーション方式は 論理(行ベース)ログを選んでおけば良いように見えるがデメリットは?
  • レプリケーションの有無とトランザクションに関係はない。
  • マルチリーダーとリーダーレスを選択する境界は?
  • ダーティライトの回避策の一般解は行レベルのロックと紹介されていた。分散DBでロックを取る方法は合意の利用なので、read-committedを実現するには合意が必要?
  • 線形化可能と直列化可能に関連があるのはなんとなく分かったが、関係性を言語化するのが難しい。今言語化できるのは下記が精一杯。

*1:RPC恐い

*2:こう書いたが正直自身がない

*3:Twitterで@tzkbさんからこのスレッドを紹介されてトランザクションの一貫性と分離レベルは別の概念だということを理解した

*4:レプリケーションがあると使えない

*5:レプリケーションがあると使えない

*6:ファントムによって引き起こされる

機械学習基盤をKubernetesで運用してきて

※ この記事は別のサイトで2018年12月08日に公開していた記事をコピーしたものです.

 この記事について

普段からオンプレのKubernetesクラスタを使った研究開発に携わっており、LANケーブルの配線から新バージョンのKubernetesやエコシステムの検証、構築、運用保守、独自ツールの開発に加えてフロントエンドのGUIをmaterial-uiで作ったりとほぼ全レイヤをやっています。

今回は機械学習の実行基盤としてKubernetesを運用してきたなかで得られた知見について書いていきます。

運用中のKubernetesクラスタについて

運用しているKubernetesクラスタはオンプレミスのCPUサーバとGPUサーバの混合構成で、さらにGPUサーバは複数の世代のGPUが混ざっています。 データサイエンティスト向けに内製した機能を持つことが大きな特徴ではありますが、用途は機械学習に限定しておらず内部向けのサービスを動かしていたりBotを動かしたりと様々な目的で使われています。

内製した仕組みは独自のWebAPIインターフェースを持っており、Dockerを知っているけれどKubernetesを知らないエンジニアでもKubernetes上にコンテナを作ることができるようになっています。 この仕組みにより、わざわざ複雑なKubernetesマニフェストを覚えなくてもデータサイエンティストはKubernetesのリソース管理の恩恵を受けることができます。 また、このインターフェースはKubernetesクラスタの運用側が開発しており、全体の利用状況を見て安定して利用できるように利用者のPodに様々な設定を追加、変更できる仕組みになっています。

この記事では運用中に実際にあったトラブルに対処してきた経験から得られた知見について書いて行きます。

知見

Jupyterの扱いは難しい

データサイエンティストにとって試行錯誤しながら分析できるJupyterは必須のツールでしょう。

ただ、リソース管理者であるKubernetesの運用者としてはJupyterの扱いは頭を悩ませる原因の一つです。 業務時間中に次々と思いついたことをあれこれ試し続けて数日のうちにいい結果が得られるなんてことはまずなく、定例打ち合わせに参加したり雑務で数時間別のことをしたり、横から新しい仕事を振られた結果Jupyterコンテナを立てたことも忘れて貴重なGPUを確保したまま一週間放置されるなんてこともあります。

今はモニタリングしているPodのリソース状況から、GPUを使うPodを作りながらも一週間程度利用していないと思われるPodの作成者に必要がないならデータを退避して一度リソースを空けてもらうように直接相談するようにしています。 システマチックにPodを削除することも可能なのですが、人それぞれの事情があるので直近ではそういった仕組みを導入することはないと思います。

PersistentVolumeをマウントしてチェックポイントを実施する

機械学習を扱うとGPUを使っても学習に1週間かかることが少なくありません。それなのに、あと少しで学習終わるというときにPodがkillされると悲惨です。

学習中のモデルはPersistentVolume上に定期的にチェックポイントしましょう。

自分だけではなく、他の人のためにもメモリの最大量(limits)を設定する

先ほどコンテナがkillされる可能性を挙げましたが、同一Node上のPodがメモリを使いすぎたためKubernetesによってkillされることが理由の半数以上を占めています。

KubernetesはPodをkillする優先度として各PodにQoSを与えています。 QoSは低い順に"Best Effort" < "Burstable" < "Guaranteed"となっており、雑に言うならばそれぞれ「リソースについてまったく宣言していないPod」「CPU、メモリのどちらか、または両方が必要な最少量だけが宣言されているか最少量と最大量に差があるPod」、「CPUとメモリの両方で必要な最少量と最大量が同じ値で宣言されているPod」になります。 分かりづらければ、リソースに対して何も設定しない"Best Effort"と厳密に設定する"Guatanteed"とそれ以外の"Burstable"と考えると覚えやすいです。

実際の環境で、メモリの最少量が宣言されているが最大量が宣言されていないQoSが"Burstable"のPodとQoSが"Best Effort"の学習用のコンテナが同じNode上で同居した際に"Burstable"のPodのメモリが膨らみ続けてしまった結果、"Burstable"のPodがNodeのメモリを全て使ってしまいQoSが"Best Effort"設定の学習用のPodを停止させてしまうことがありました。

これを防ぐには"Burstable"のPodがメモリの最大量(limits)を指定しておき、想定以上のメモリを使うようになればそのコンテナだけが停止するように振る舞うべきでした。

メモリのlimitsを設定するのは同じNodeで実行している他の人のコンテナを守ることにつながります。

PodをGuaranteedの設定で起動する

先ほどの例では"Burstable"のPodがlimitsを設定すべきと書きました。 しかし、"Best Effort"の学習用のPodが大量のメモリを必要とする場合、"Burstable"のPodがlimitsを指定していたとしてもいずれ"Best Effort"の学習用のPodがNode上のメモリを食い潰すことになればQoSの低さから優先的にkillされてしまいます。

このような時はこの2つのPodが同じNodeにスケジュールされるべきではありません。スケジュールによって安全を確保するためにも全てのPodが"Guaranteed"の設定であることが理想です。

limitsを指定できないなら、せめてbackoffLimitに0を指定したJobとして実行する

初めて実行するので必要なメモリの最大量の目安が分からないが何度も試行錯誤して見積もりをしている暇がないといった事情でどうしてもメモリのlimitsを指定できないこともあると思います。そういう時は実行に失敗してもRestartしないように.spec.backoffLimitを0に設定したJobとして実行しましょう。

経験上、機械学習でコンテナがメモリ不足になるのはこれまで何度も実行してきた学習用のプロセスに今までよりも大きなデータを与えたときに起こりやすいです。

何も考えずに極端に大きなデータを与えてしまうとそのサーバのメモリを食い潰し(同じNode上の"Best Effort"のPodを停止させてから)killされた後に他のNodeでリスタートします。このようなPodはたいてい次のNodeでもメモリ不足を起こし他のPodを巻き込みながらkillされることになり、例えるならNodeを渡り歩く爆弾 になります。 backoffLimitの設定を忘れると悲惨です。現時点の最新版であるv1.13ではAPI ReferenceによるとbackoffLimitの初期値は6に設定されており、(初期の起動を含めると backoffLimit+1 回起動するので)最大で7つのNodeを渡り歩くことからその恐ろしさは推して知るべしです。

GPUサーバにGPUを使わないコンテナを割り当てない

GPUの資源は希少なのでGPUが余っているのにCPUやメモリが不足しているという状況を作らないことが大事です。 我々の環境ではnodeSelectorを使ってGPUが必要ないPodはGPUサーバに割り当てないようにしています。

実際の運用では副次的な効果もありました。 ある時、GPUを必要としないDB系のPodが複数回リスタートする設定で作られた際にリソースの見積もりがあまかったために数百GBのメモリを要求した結果、Nodeを渡り歩く爆弾になりNode上のPodを殺してまわったことがありましたが、GPUを必要としないDB系のPodはGPUサーバにスケジュールされることがなかったため長時間モデルを学習プロセスを守ることができました。

GPUの種類を指定できるようにする

複数の世代のGPUをまぜて運用すると、計算性能やメモリ量の違いからどうしても特定のGPU上で実行したいという要望が出てきます。 しかし、kubeletはサーバに積まれているGPUの数を認識するものの積まれているGPUの種類までは認識してくれません(要DevicePlugin)。

そこで、GPUを持つNodeにGPUの種類を区別できるラベルをつけ、nodeSelectorでスケジューリングされるサーバを指定できるようにしています。 公式ドキュメントのClusters containing different types of NVIDIA GPUsに具体的な方法が書かれている方法をそのまま採用しています。

GPUの空き状況をモニタリングして公開する

機械学習GPUが必要になる場合、同じ人でも試行錯誤を繰り返して特徴量を設計しているフェーズの時もあればパラメータ探索のために並行に大量の学習モデルを作りたいフェーズの時もあるのでユーザに対してQuotaを設けることはしていません。

その代わり、誰がどれだけGPUを使っているかをGrafanaで公開しており、ユーザが他のユーザの利用状況を確認できるようにしています。 自分が使いたいけれど特定の人が長期間GPUを占有している場合はユーザ間で調整をしてもらうようにお願いしていて、現状はそれでうまく回っています。

まとめ

機械学習基盤としてKubernetesを運用してきた知見をまとめてみました。 振り返るとほとんどはPodのリソース関係で安定してPodを動かし続ける知見になりました。

Kubernetesが想定する障害への対処策である「コンテナをステートレスに作ったうえでレプリカを作っておき、Podがダウンしたら生き残っているPodが処理を継続しつつ他のNodeに新しくPodを作り直して復帰する」という戦略を、希少なハードウェア資源を使って単一のプロセスが長時間かけて実行するような機械学習の学習用コンテナに適用することはできません。一度実行が中断されてやり直すコストが高かったり、リソースが限られているためレプリカなどで投機的な実行をする余裕がないからです。

分散処理ならタスクを分割した上で失敗したタスクをリトライする方式をとれるのですが、単一のコンテナで長時間かかる学習をマルチテナントのKubernetesクラスタで安定して動かせるようにするのは難しいですね。