のんびりしているエンジニアの日記

ソフトウェアなどのエンジニア的な何かを書きます。

PyTorchで高精度・高性能のEfficientNetを利用する

皆さんこんにちは
お元気でしょうか。

本日はEfficientNetをPyTorchで利用します。
私も頻繁に利用しますが、時々忘れてしまうのでメモ

EfficnetNetについて

EfficientNetとは?

幅、深さや解像度に関係性を導き出されたニューラルネットワークアーキテクチャ
SoTAを出したが、従来より8.4倍小さく、6.1倍の高速化を実現しています。
EfficientB0-B7(今はそれ以上もあった気もしますが)まで存在し、精度・性能により使い分けできます。
Kagglerたちは最近このニューラルネットワークをソリューションに利用するようになりました。(ResNetより増えている気がする)
arxiv.org

ライブラリのインストール

使うライブラリのインストールは簡単です。

pip install efficientnet-pytorch

github.com

EfficientNet-Pytorchの使い方

モデルの初期化方法

モデルの初期化方法は学習済モデルの有無により、変わります。
学習済モデルがないバージョン

model = EfficientNet.from_name("efficientnet-b5")

学習済モデルを利用するバージョン

model = EfficientNet. from_pretrained("efficientnet-b5")

Fine-turning

ImageNetと同じクラスの問題を解く要望はほぼ皆無だと思います。
学習済のモデルを元にfineturningを行いましょう。
基本的には最終層を変更するのみなので、私は次のようにモデルを構築して学習を行っています。

n_class = 100
model = EfficientNet. from_pretrained("efficientnet-b5")
model._fc = nn.Linear(2048, n_class)

後は通常通りモデルを学習すれば良いです。

最後に

EfficientNetは精度も高く性能もそれなりに良いです。
PNASNetなどよりも圧倒的に利用しやすい感じがしているのでResNetなどとのアンサンブルに有効です。

MLFlow Trackingを使って、実験管理を効率化する

皆さんこんにちは
お元気でしょうか。COVIT-19起因で引きこもっているため、少しずつ自炊スキルが伸びていっています。

以前、実験管理に関していくつかのソフトウェアを紹介しました。
その中で、MLFlow Trackingが一番良さそうではあったのでパイプラインに取り込むことを考えています。
もう少し深ぼって利用方法を把握する必要があったので、メモ代わりに残しています。

nonbiri-tereka.hatenablog.com

MLFlow Trackingのおさらい

MLFlowとは

MLFlowはプラットフォームです。機械学習のデプロイやトラッキング、実装のパッケージングやデプロイなど幅広くサポートしています。
その中ではいくつかの機能があり、主にMLflow Trackingを実験管理に利用している人が増えています。
Trackingの機能については申し分がなさそうで、リモートサーバでの運用なども対応できるので、複数人で利用するにも有用でしょう。(大体以前と同じ)

github.com

基本的な利用方法

実装

実装はほぼ以前と同様ですが、以下の通りです。

import argparse
import os
import tempfile

import mlflow
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.autograd import Variable
from torchvision import datasets, transforms

# Command-line arguments
parser = argparse.ArgumentParser(description='PyTorch MNIST Example')
parser.add_argument('--batch-size', type=int, default=64, metavar='N',
                    help='input batch size for training (default: 64)')
parser.add_argument('--test-batch-size', type=int, default=1000, metavar='N',
                    help='input batch size for testing (default: 1000)')
parser.add_argument('--epochs', type=int, default=10, metavar='N',
                    help='number of epochs to train (default: 10)')
parser.add_argument('--lr', type=float, default=0.01, metavar='LR',
                    help='learning rate (default: 0.01)')
parser.add_argument('--momentum', type=float, default=0.5, metavar='M',
                    help='SGD momentum (default: 0.5)')
parser.add_argument('--enable-cuda', type=str, choices=['True', 'False'], default='True',
                    help='enables or disables CUDA training')
parser.add_argument('--seed', type=int, default=1, metavar='S',
                    help='random seed (default: 1)')
parser.add_argument('--log-interval', type=int, default=100, metavar='N',
                    help='how many batches to wait before logging training status')
args = parser.parse_args()

enable_cuda_flag = True if args.enable_cuda == 'True' else False

args.cuda = enable_cuda_flag and torch.cuda.is_available()

torch.manual_seed(args.seed)
if args.cuda:
    torch.cuda.manual_seed(args.seed)

kwargs = {'num_workers': 1, 'pin_memory': True} if args.cuda else {}
train_loader = torch.utils.data.DataLoader(
    datasets.MNIST('../data', train=True, download=True,
                   transform=transforms.Compose([
                       transforms.ToTensor(),
                       transforms.Normalize((0.1307,), (0.3081,))
                   ])),
    batch_size=args.batch_size, shuffle=True, **kwargs)
test_loader = torch.utils.data.DataLoader(
    datasets.MNIST('../data', train=False, transform=transforms.Compose([
        transforms.ToTensor(),
        transforms.Normalize((0.1307,), (0.3081,))
    ])),
    batch_size=args.test_batch_size, shuffle=True, **kwargs)


class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.conv1 = nn.Conv2d(1, 10, kernel_size=5)
        self.conv2 = nn.Conv2d(10, 20, kernel_size=5)
        self.conv2_drop = nn.Dropout2d()
        self.fc1 = nn.Linear(320, 50)
        self.fc2 = nn.Linear(50, 10)

    def forward(self, x):
        x = F.relu(F.max_pool2d(self.conv1(x), 2))
        x = F.relu(F.max_pool2d(self.conv2_drop(self.conv2(x)), 2))
        x = x.view(-1, 320)
        x = F.relu(self.fc1(x))
        x = F.dropout(x, training=self.training)
        x = self.fc2(x)
        return F.log_softmax(x, dim=0)


model = Net()
if args.cuda:
    model.cuda()

optimizer = optim.SGD(model.parameters(), lr=args.lr, momentum=args.momentum)


def train(epoch):
    model.train()
    for batch_idx, (data, target) in enumerate(train_loader):
        if args.cuda:
            data, target = data.cuda(), target.cuda()
        data, target = Variable(data), Variable(target)
        optimizer.zero_grad()
        output = model(data)
        loss = F.nll_loss(output, target)
        loss.backward()
        optimizer.step()
        if batch_idx % args.log_interval == 0:
            print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
                epoch, batch_idx * len(data), len(train_loader.dataset),
                       100. * batch_idx / len(train_loader), loss.data.item()))
            step = epoch * len(train_loader) + batch_idx
            log_scalar('train_loss', loss.data.item(), step)


def test(epoch):
    model.eval()
    test_loss = 0
    correct = 0
    with torch.no_grad():
        for data, target in test_loader:
            if args.cuda:
                data, target = data.cuda(), target.cuda()
            data, target = Variable(data), Variable(target)
            output = model(data)
            test_loss += F.nll_loss(output, target, reduction='sum').data.item()  # sum up batch loss
            pred = output.data.max(1)[1]  # get the index of the max log-probability
            correct += pred.eq(target.data).cpu().sum().item()

    test_loss /= len(test_loader.dataset)
    test_accuracy = 100.0 * correct / len(test_loader.dataset)
    print('\nTest set: Average loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format(
        test_loss, correct, len(test_loader.dataset), test_accuracy))
    step = (epoch + 1) * len(train_loader)
    log_scalar('test_loss', test_loss, step)
    log_scalar('test_accuracy', test_accuracy, step)


def log_scalar(name, value, step):
    """Log a scalar value to both MLflow and TensorBoard"""
    mlflow.log_metric(name, value)

active_run = mlflow.active_run()
for key, value in vars(args).items():
    mlflow.log_param(key, value)
# Create a SummaryWriter to write TensorBoard events locally
output_dir = dirpath = tempfile.mkdtemp()
# Perform the training
for epoch in range(1, args.epochs + 1):
    train(epoch)
    test(epoch)
# Upload the TensorBoard event logs as a run artifact
with tempfile.TemporaryDirectory()  as tmp:
    filename = os.path.join(tmp, "model.bin")
    torch.save(model.state_dict(), filename)
    mlflow.log_artifacts(tmp, artifact_path="results")

既存実装からそこまで改修を加える必要はありません。
mlflowの開始前には、active_run()を行います。
その後、mlflowの関数利用して、記録を残せます。記録を残すための関数は次の通りです。

項目 説明
log_param パラメータを記録する。batch_sizeなど
log_metric 時間で経過する数値を記録する。train_lossなど
log_artifacts 出力されたファイルを記録する。モデルなど

可視化

一覧画面

分析の一覧画面は次の通りです。
f:id:tereka:20200321195334p:plain

Searchの箇所にクエリを入力し、データを絞ることが可能です。
また、「Columns」の左にある表示のクリックによりテーブルのUIを変更できます。
更には各メトリクスの値をクリックすることでソートが可能です。

個別画面

一般的な実験条件です。こちらは特に目新しいこともなく、情報が記載されています。
f:id:tereka:20200321200048p:plain

各ステップごとのメトリクスや実験で保存したファイルをmlflowの画面から閲覧できます。
f:id:tereka:20200321200057p:plain

メトリクス

取得したメトリクスは次のようにグラフとして可視化できます。
Pointを点にしたり、対数でスケーリングしたりすることも可能です。
f:id:tereka:20200321200302p:plain

比較画面

一覧の画面から複数の実験を選択し、「Compare」を押すと次のような画面に進みます。
この画面で実験の比較を行えます。複数の実験が一覧で並んでいるため、左右見比べたりするのは容易です。

f:id:tereka:20200321214411p:plain

より使いやすくするために

これまで標準的な方式を紹介しましたが、一歩使いやすくするためのTipsを以下に紹介します。

リモートサーバへの送信

今回はリモートのサーバを利用することにします。
もともとローカルに出力されていますが、set_tracking_uriを実行することで、ファイルの配置先、リモートへの出力も可能とします。
まず、リモートにmlflowサーバの立ち上げを行います。

mlflow server -h 0.0.0.0 -p 5000

次にリモートサーバへの設定を行います。
active_run関数の前にset_tracking_url関数を実行します。
サーバのエンドポイントはログを見る限り、httpsで起動していると思われがちですが、httpで記述する必要があります。
(ここですが、httpsで記述していて、少しはまりました)

この状態でもUIは利用できるので、記録が成功しているかはアクセスすればわかります。

mlflow.set_tracking_uri("http://192.168.1.5:5000")

また、実装を変更したくない場合、MLFLOW_TRACKING_URI環境変数の設定による変更も可能です。

実験名の設定

通常だと、名前がなく、日付のみで管理し難いです。
mlflowは、<実験>/の階層で管理しています。
これらの指定により管理しやすくなります。例えば、実験とrun_idを指定するには次のコードを記載すれば良いでしょう。

eid = mlflow.create_experiment("experiment")
mlflow.start_run(experiment_id=eid, run_name="run01")

既に実験を作成した場合は、get_experiment_by_name関数を呼び出すとexperiment_idを取得できます。

セキュリティ

セキュリティはmlflow側で担保していないので、別のソフトウェア(例:nginx)を利用する必要があります。
例えば、dockerを利用する場合だと次のサイトが参考になります。
ymym3412.hatenablog.com

最後に

MLFlow Trackingの利用をすれば予想通り、リモートで集約するなども可能です。
自分の作りたいものが作れそうではあるので、これを使いこなしてより実験管理を効率化したいと感じています。

データ分析に役立つメモリ管理・削減方法

皆さんこんにちは
お元気ですか。最近自炊が少しずつ捗ってきました。

本日はデータ分析でよく起こる「Memory Error」の対策を書いていこうと思います。
今回のはGPUではなく、CPUです。

そもそもなぜ「Memory Error」と遭遇するのか

大量のデータを解析する、もしくは、大量の特徴量を扱うからです。
または、途中の巨大途中処理が原因で載らなくなったとかですね。
その結果、マシンが落ちることもデータ分析している人が陥るよくあることです。

その場合の処方箋を書いていこうと思います。

メモリ対策

不要な変数のメモリを開放する。

一番シンプルで、もういらないから消してしまえという方式です。
方法は単純です。変数をdelして、ガーベジコレクション(不要なメモリを回収し、空ける方式)を実行することです。

例えば、次の通りです。

import gc
import numpy as np

matrix = np.random.rand((10000, 10000)) # 適当に作成した巨大行列
# (処理)

del matrix #不要になったので削除
gc.collect() # ガーベジコレクションを実行

これにより要らなくなった段階で不要な変数を削除できるため、特にメモリがギリギリの場合に非常に便利です。

可能な限り型の精度を落とす

numpyやpandasを利用すると大半のカラムはfloat64になっていると思います。
しかし、解析する場合、float64で保持する必要はないでしょう。正直、float32もあれば十分です。
そのため、astypeなどでfloat32やそれ以下に状況に応じて削減すれば良いです。
型を判定し、メモリを削減する方式は次の箇所に記載されています。

www.kaggle.com


また、推論結果を保存したいと思ったこともよくあります。
その際にデータが非常に大きいとディスクを圧迫し、ディスクフルの戦いという不毛なことを行わなければなりません。
一つの対策として保存時に精度を落とすことで(float16や32に)ディスクサイズの削減ができます。
float64から16,32へ変換するのはastype(np.float32)とメソッドを実行すれば良いです。

少し特殊な事例ですが、0-1の場合は、255を乗算することで、uint8で管理できます。
メモリが限られている場合(Kernelなど)に利用可能です。

arr = np.random.rand(200000, 5000)
arr = arr * 255
arr = arr.astype(np.uint8)

一度に大量のデータを読まない

そもそも全てのデータを読み込みオンメモリで管理することは難しい場合もあります。
データを全て載せ、管理することを避けるべきです。

例えば、pandasには、chunkと呼ばれるメソッドがあり、そのメソッドを利用することで部分的に順番に読めます。
そのため、部分的に読みつつ、特徴量などを計算しましょう。

reader = pd.read_csv(fname, skiprows=[0, 1], chunksize=50)
for r in reader:
    print (r)

特徴量のみ保存する

一度に大量のデータを読まないに派生します。
特徴量のみ保存しておいて結合する手があります。
例えばログデータだとすれば、大半のログデータは分量が多いです。

部分的に読むのに加え、特徴量の計算をしておけば全てのデータをメモリで持つことはなくなります。
部分的に読み進め、特徴量を計算し、保存しておくことで、効率化できるでしょう。

最後に

メモリに載らないデータを扱うのはよくあることです。
そのため、このような方法を使ってうまく扱えるようになってもらえると幸いです。

atmaCup#4に参加して5位でした

皆さんこんにちは
お元気ですか。私は元気です。

atmaCup#4に参加して5位だったのでその報告を書きます!
前回より良く少しほっとしていますが、賞金圏内には届きませんでした。
f:id:tereka:20200309000912p:plain

概要

atmaさんと一般社団法人リテールAI研究会さんとの合同開催で開かれました。
お題はスーパー一回の購買で特定のカテゴリの商品を購入するかどうか。でした。
ログに今回推定するカテゴリではないものは含まれており、それらと同時にその特定のカテゴリの商品を購入するかを推定します。

開催期間は今までと異なり、一週間です。
今回から運営がSlackに#lb-liveチャンネルを作成していました。
このチャネルはTop10の順位が変わるとSlackのチャネルに投稿されるものです。
とてもおもしろいチャンネルですが、一応Top10以内にいた私としては、通知が来ると抜かれたと思って常にひやひやしていました。
スマホのブルブルが正直心臓には悪いと感じておりました。。。(あったほうが参加者的にも面白いと思います)

また、協賛企業さんより、おかしやドリンクの差し入れをいただきました。
差し入れかなりおいしかったので食べすぎないように注意しなければならないほどでした。

ソリューション

作戦

私自身は社会人のため、日中帯は稼働できません。
平日稼働中は計算を行い、夜に特徴量を作るといった作戦を立てていました。
また、評価指標がmacro AUCでもあったのでアンサンブルははじめからほぼ効果があるだろうと踏んでいました。
そのため、前日夜から最終日はモデルの計算に費やし、何もしないぐらいのお気持ちのはずでした。

しかし、予想よりもスコアが伸び悩んだこともあり、最終日は簡単なEDAしていました。
そのときに実装に致命的なバグを発見してしまったので、fix + 間に合う範囲でアンサンブルの再計算していました。
(一部間に合わなかったものがあるので頭を抱えました。)

最終日にスコアが上がって上位に来たのはアンサンブルを狙っており、Weightの比率を勘で変えるのを一日でやっていたからです。

解法

LightGBM, CatBoost, MLPの重み付き平均です。
ユーザのお気持ちになって、必要な特徴量を追加していきました。
事情により、詳細は伏せますが、こんな感じです。

・日付の特徴(年月日、給料日、祝日、祝日前日)
・不要なデータを削除
(詳細は聞かないでください。)
・ユーザx購入商品のカテゴリ、同時購入商品のカテゴリ
※NNのみSVDで次元圧縮
・ユーザの購入回数

感想

Home Credit以来のまともなテーブルデータ解析っぽいことをやりました。
かなりそのときの知見を活かしながら分析していましたが、なかなか苦しかったです。
一通りツールの使い方を改めて、学べたのはかなりの収穫だったと思っています。

正直、一日と比較して長いなんて最初は思っていましたが、データ量から見るとちょうど良かったと思います。
コンペの題材としては企業的にも有用で良かったのではないかと感じています。

改めて、運営のatmaさん、一般社団法人リテールAI研究会さんありがとうございました。
また参加したいと思います!

機械学習研究者&エンジニアが頭を抱える実験管理に役立つツールを比較した

皆さんこんにちは。
お元気でしょうか。GoogleQA20thで悔しいけど楽しかったです。
自然言語処理のみのコンペを真面目に挑んだのは初で、勉強になることが多かったです。
今回は実験管理ツールの紹介と比較をします。
特徴がわかる範囲で簡単に実装も書いているので、参考にしてみてください。

実験管理ツール

実験管理の必要性

コンペティションや研究では多くのハイパーパラメータや構造などに対して様々な変更を加えます。
私の場合の例ですが、コンペティションだと少なくとも100件ほどは実験します。
しかし、終盤になると実験した内容について忘れてしまい、あれこのハイパーパラメータのときの結果はなんだっけということになりかねません。

今回ご紹介するのはこちらです。

  • Excel
  • mag
  • Weights and Biases
  • MLFlow

Excelはツールなのか怪しいのですが、手動記録・BIとしてのベンチマークとして掲載しています。
自動記録以外は案外良いのでありな気もしています。

実験管理ツールの要件

実験管理に必要なものはなんでしょうか?個人的にはこのあたりです。

1. ハイパーパラメータの保存・・・実験した際にハイパーパラメータが記録として残せるか。
2. 学習曲線・・・ニューラルネットワークがメインですが、グラフとしての記述が可能か。
3. 出力物の保存・・・実験結果としてモデルやログなどが残せるか
4. 結果の可視化のリッチ度・・・その得られたものの結果が見やすいか
5. 集約しやすいか・・・リモートサーバにある場合に、1-4の保存が一つのUIなどで見れるかどうか。

実験管理ツールの紹介

Excel

Excelとは

Excelは言わずもがな、Microsoftが開発した表計算ソフトです。
Kaggleでは、ある種、最強のテーブルデータ解析ツールです。
データが少なければJupyter Notebookも必要なく、カラムのフィルタやグラフの作成も可能です。
ユーザが直感的に操作できる点もあるため、状況によってはJupyter Notebookよりも便利で、非常に汎用性の高いツールとなります。
全ての記録をExcelで残すには、全て手を動かし、ログの表を書いたり、出力物は紐付けるように保存したりなど、工夫が必要です。

良い点

1. Excelの機能が使える
Excelの機能をフル活用できます。
テーブルに対してフィルタをかける、表をかけるなど自由に記述が可能です。

欠点

1. 自動化が困難
自動化には、非常に困難な壁が立ちはだかっています。
Excelを解析するスクリプトを書いてしまえば、できるかもしれませんが、そこに時間をかけたいエンジニア・研究者はいないでしょう。
大量実験をする際には不便です。

mag

magとは

magはとても簡単で便利な実験管理ツールです。
GoogleQA 1stの方が利用されているのを見て、使ってみたくなりました。
導入までが早く、全てコンソールで実行可能なのは特徴です。

github.com

インストールには、次のコマンドを実行してください。

pip install git+https://github.com/ex4sperans/mag.git
サンプル実装

サンプルでirisで実行するならば、次のとおりです。(サンプルまま)

import argparse
import os
import pickle

from sklearn.datasets import load_iris
from sklearn.svm import SVC
from sklearn.model_selection import cross_val_score, StratifiedKFold
from mag.experiment import Experiment

parser = argparse.ArgumentParser(
    formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument(
    "--C", type=float, default=1.0,
    help="Regularization parameter for SVM")
parser.add_argument(
    "--gamma", type=float, default=0.01,
    help="Kernel parameter for SVM")
parser.add_argument(
    "--cv", type=int, default=5,
    help="Number of folds for cross-validation")
parser.add_argument(
    "--cv_random_seed", type=int, default=42,
    help="Random seed for cross-validation iterator")

args = parser.parse_args()

svm_config = {
    "model": {
        "C": args.C,
        "gamma": args.gamma
    },
    "crossval": {
        "n_folds": args.cv,
        "_random_seed": args.cv_random_seed
    }
} # ハイパーパラメータの定義(1)

iris = load_iris()

with Experiment(config=svm_config) as experiment: # ここからmag実験履歴の取得を開始

    config = experiment.config

    model = SVC(C=config.model.C, gamma=config.model.gamma)

    score = cross_val_score(
        model, X=iris.data, y=iris.target, scoring="accuracy",
        cv=StratifiedKFold(
            config.crossval.n_folds,
            shuffle=True,
            random_state=config.crossval._random_seed),
    ).mean()

    print("Accuracy is", round(score, 4))
    experiment.register_result("accuracy", score) # magのスコアの登録をする。

    with open(os.path.join(experiment.experiment_dir, "model.pkl"), "wb") as f: # 実験ディレクトリの中で、ファイルを保存する。(2)
        pickle.dump(model, f)

出力ディレクトリは次の通りです。

experiments/
└── 5-1.0-0.01
    ├── command
    ├── config.json
    ├── log
    ├── model.pkl
    └── results.json

特定のディレクトリの中で実験ごとにディレクトリが生成されます。
1. command・・・実行コマンド
2. config.json・・・ハイパーパラメータ(今回だとSVC, (1)に該当する部分)
3. log・・・標準出力の結果
4. model.pkl・・・学習済モデル
5. results.json・・・register_resultで残った記録

また、複数の実験を集約し、比較するためのコマンドも用意されています。

$ python -m mag.summarize experiments --metrics=accuracy

Results for <directory>:

            accuracy
5-10.0-1.0  0.953333
5-1.0-0.01  0.933333
良い点

1. 軽量・お手軽
コマンドラインで完結するため、簡単に実験を管理したい場合にちょうど良い塩梅です。

2. コマンド・標準出力の自動保存
実行コマンドや標準出力を勝手に拾ってきてくれるので、諸々printしておけば少なくとも消滅することはありません。
いざ、なにか残っていると後追いでログを確認できますが、それすらもないとどうしようもありません。

ここが少し残念

1. 一覧化できるUIとその機能
標準出力のUIがあまりイケていないため、少し見づらいと感じました。
ソート等も自分でしなければならないのは苦しいですね。

2. 複数サーバの運用は難しそう
ログがローカルに出力されるので、複数台ある場合は統合する仕組みが必要でしょう。

Weights and Biases

Weights and Biasesとは

実験の分析や運用をするためのツールです。
ローカルのクライアントツールを使い、サーバへデータを送信します。
クライアントツールは次のサイトから導入できます。

github.com

Webサイトはこちらです。UIで実験結果をこちらで確認できます。

www.wandb.com

サンプル実装

事前に利用にはコマンドラインからログインが必要です。次のコマンドを実行し、順を追って進めましょう。

wandb login

サンプルコードとして以下のものを用意しました。

import argparse
import os
import tempfile
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torchvision import datasets, transforms
from torch.autograd import Variable
import wandb

# Command-line arguments
parser = argparse.ArgumentParser(description='PyTorch MNIST Example')
parser.add_argument('--batch-size', type=int, default=64, metavar='N',
                    help='input batch size for training (default: 64)')
parser.add_argument('--test-batch-size', type=int, default=1000, metavar='N',
                    help='input batch size for testing (default: 1000)')
parser.add_argument('--epochs', type=int, default=10, metavar='N',
                    help='number of epochs to train (default: 10)')
parser.add_argument('--lr', type=float, default=0.01, metavar='LR',
                    help='learning rate (default: 0.01)')
parser.add_argument('--momentum', type=float, default=0.5, metavar='M',
                    help='SGD momentum (default: 0.5)')
parser.add_argument('--enable-cuda', type=str, choices=['True', 'False'], default='True',
                    help='enables or disables CUDA training')
parser.add_argument('--seed', type=int, default=1, metavar='S',
                    help='random seed (default: 1)')
parser.add_argument('--log-interval', type=int, default=100, metavar='N',
                    help='how many batches to wait before logging training status')
args = parser.parse_args()

enable_cuda_flag = True if args.enable_cuda == 'True' else False

args.cuda = enable_cuda_flag and torch.cuda.is_available()

torch.manual_seed(args.seed)
if args.cuda:
    torch.cuda.manual_seed(args.seed)

kwargs = {'num_workers': 1, 'pin_memory': True} if args.cuda else {}
train_loader = torch.utils.data.DataLoader(
    datasets.MNIST('../data', train=True, download=True,
                   transform=transforms.Compose([
                       transforms.ToTensor(),
                       transforms.Normalize((0.1307,), (0.3081,))
                   ])),
    batch_size=args.batch_size, shuffle=True, **kwargs)
test_loader = torch.utils.data.DataLoader(
    datasets.MNIST('../data', train=False, transform=transforms.Compose([
                       transforms.ToTensor(),
                       transforms.Normalize((0.1307,), (0.3081,))
                   ])),
    batch_size=args.test_batch_size, shuffle=True, **kwargs)

class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.conv1 = nn.Conv2d(1, 10, kernel_size=5)
        self.conv2 = nn.Conv2d(10, 20, kernel_size=5)
        self.conv2_drop = nn.Dropout2d()
        self.fc1 = nn.Linear(320, 50)
        self.fc2 = nn.Linear(50, 10)

    def forward(self, x):
        x = F.relu(F.max_pool2d(self.conv1(x), 2))
        x = F.relu(F.max_pool2d(self.conv2_drop(self.conv2(x)), 2))
        x = x.view(-1, 320)
        x = F.relu(self.fc1(x))
        x = F.dropout(x, training=self.training)
        x = self.fc2(x)
        return F.log_softmax(x, dim=0)

model = Net()
if args.cuda:
    model.cuda()

optimizer = optim.SGD(model.parameters(), lr=args.lr, momentum=args.momentum)

def train(epoch):
    model.train()
    for batch_idx, (data, target) in enumerate(train_loader):
        if args.cuda:
            data, target = data.cuda(), target.cuda()
        data, target = Variable(data), Variable(target)
        optimizer.zero_grad()
        output = model(data)
        loss = F.nll_loss(output, target)
        loss.backward()
        optimizer.step()
        if batch_idx % args.log_interval == 0:
            print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
                epoch, batch_idx * len(data), len(train_loader.dataset),
                100. * batch_idx / len(train_loader), loss.data.item()))
            step = epoch * len(train_loader) + batch_idx
            wandb.log({'train_loss': loss.data.item()})

def test(epoch):
    model.eval()
    test_loss = 0
    correct = 0
    with torch.no_grad():
        for data, target in test_loader:
            if args.cuda:
                data, target = data.cuda(), target.cuda()
            data, target = Variable(data), Variable(target)
            output = model(data)
            test_loss += F.nll_loss(output, target, reduction='sum').data.item() # sum up batch loss
            pred = output.data.max(1)[1] # get the index of the max log-probability
            correct += pred.eq(target.data).cpu().sum().item()

    test_loss /= len(test_loader.dataset)
    test_accuracy = 100.0 * correct / len(test_loader.dataset)
    print('\nTest set: Average loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format(
        test_loss, correct, len(test_loader.dataset), test_accuracy))
    step = (epoch + 1) * len(train_loader)

wandb.init(config=args, project="mnist-test")

output_dir = dirpath = tempfile.mkdtemp()
# Perform the training
for epoch in range(1, args.epochs + 1):
    train(epoch)
    test(epoch)
torch.save(model.state_dict(), os.path.join(wandb.run.dir, 'model.pt'))

次のコマンドで実行します。

python mnist.py

完了後に作成しておいたプロジェクトへ行くと、結果が一覧化されています。
クリックすると詳細画面にも遷移できます。

また、各個別の画面も揃っています。この画面でファイル等も管理されているので、複数台を利用するには良い環境ではないでしょうか。


良い点

1. 機能の追加に容易に対応できる。
UIはライブラリ管理していないので、常に最新の状態のものが利用できます。
ローカルのマシンに管理すると、新機能が追加された場合に、アップグレード操作が必要になります。

2. 複数台での管理が容易
複数台で実験を行っていても、管理が容易です。

ここが少し残念

1. ネットワーク環境がないと厳しい
ネットワークの環境がなければ当たり前ですが、動かせません。
そのため、環境によってはプログラムが利用できなくなります。

2. Standalone版がない
仕事ではクラウドやサービスを利用できない場面が多くあります。
その場合にうっかり利用してしまうと問題になります。
これはどうすることもできないので、その場合にはWeights and Biasesを利用すべきではないでしょう。

2023/8/2更新
最近知ったのですが、2020年10月頃より、Weights and BiasesのStandalone版が公開されているようです。
ぜひ、興味のある人は使ってみてください。
github.com

MLFlow

MLFlowはプラットフォームです。機械学習のデプロイやトラッキング、実装のパッケージングやデプロイなど幅広くサポートしています。
その中ではいくつかの機能があり、主にMLflow Trackingを実験管理に利用している人が増えています。
このツール、最も関心があり、使い方は調べていましたが、実は自分で触ったことがなく、イメージを持てていないツールの一つでした。

github.com

そのため、今回はMNISTでそのサンプルを試します。

サンプル実装

公式のサンプル実装に若干手を加えています。(公式だと、TensorBoardへの組み込みもあります)
実装は次の通りです。

import argparse
import os
import mlflow
import tempfile
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torchvision import datasets, transforms
from torch.autograd import Variable

# Command-line arguments
parser = argparse.ArgumentParser(description='PyTorch MNIST Example')
parser.add_argument('--batch-size', type=int, default=64, metavar='N',
                    help='input batch size for training (default: 64)')
parser.add_argument('--test-batch-size', type=int, default=1000, metavar='N',
                    help='input batch size for testing (default: 1000)')
parser.add_argument('--epochs', type=int, default=10, metavar='N',
                    help='number of epochs to train (default: 10)')
parser.add_argument('--lr', type=float, default=0.01, metavar='LR',
                    help='learning rate (default: 0.01)')
parser.add_argument('--momentum', type=float, default=0.5, metavar='M',
                    help='SGD momentum (default: 0.5)')
parser.add_argument('--enable-cuda', type=str, choices=['True', 'False'], default='True',
                    help='enables or disables CUDA training')
parser.add_argument('--seed', type=int, default=1, metavar='S',
                    help='random seed (default: 1)')
parser.add_argument('--log-interval', type=int, default=100, metavar='N',
                    help='how many batches to wait before logging training status')
args = parser.parse_args()

enable_cuda_flag = True if args.enable_cuda == 'True' else False

args.cuda = enable_cuda_flag and torch.cuda.is_available()

torch.manual_seed(args.seed)
if args.cuda:
    torch.cuda.manual_seed(args.seed)

kwargs = {'num_workers': 1, 'pin_memory': True} if args.cuda else {}
train_loader = torch.utils.data.DataLoader(
    datasets.MNIST('../data', train=True, download=True,
                   transform=transforms.Compose([
                       transforms.ToTensor(),
                       transforms.Normalize((0.1307,), (0.3081,))
                   ])),
    batch_size=args.batch_size, shuffle=True, **kwargs)
test_loader = torch.utils.data.DataLoader(
    datasets.MNIST('../data', train=False, transform=transforms.Compose([
                       transforms.ToTensor(),
                       transforms.Normalize((0.1307,), (0.3081,))
                   ])),
    batch_size=args.test_batch_size, shuffle=True, **kwargs)

class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.conv1 = nn.Conv2d(1, 10, kernel_size=5)
        self.conv2 = nn.Conv2d(10, 20, kernel_size=5)
        self.conv2_drop = nn.Dropout2d()
        self.fc1 = nn.Linear(320, 50)
        self.fc2 = nn.Linear(50, 10)

    def forward(self, x):
        x = F.relu(F.max_pool2d(self.conv1(x), 2))
        x = F.relu(F.max_pool2d(self.conv2_drop(self.conv2(x)), 2))
        x = x.view(-1, 320)
        x = F.relu(self.fc1(x))
        x = F.dropout(x, training=self.training)
        x = self.fc2(x)
        return F.log_softmax(x, dim=0)

model = Net()
if args.cuda:
    model.cuda()

optimizer = optim.SGD(model.parameters(), lr=args.lr, momentum=args.momentum)

def train(epoch):
    model.train()
    for batch_idx, (data, target) in enumerate(train_loader):
        if args.cuda:
            data, target = data.cuda(), target.cuda()
        data, target = Variable(data), Variable(target)
        optimizer.zero_grad()
        output = model(data)
        loss = F.nll_loss(output, target)
        loss.backward()
        optimizer.step()
        if batch_idx % args.log_interval == 0:
            print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
                epoch, batch_idx * len(data), len(train_loader.dataset),
                100. * batch_idx / len(train_loader), loss.data.item()))
            step = epoch * len(train_loader) + batch_idx
            log_scalar('train_loss', loss.data.item(), step)

def test(epoch):
    model.eval()
    test_loss = 0
    correct = 0
    with torch.no_grad():
        for data, target in test_loader:
            if args.cuda:
                data, target = data.cuda(), target.cuda()
            data, target = Variable(data), Variable(target)
            output = model(data)
            test_loss += F.nll_loss(output, target, reduction='sum').data.item() # sum up batch loss
            pred = output.data.max(1)[1] # get the index of the max log-probability
            correct += pred.eq(target.data).cpu().sum().item()

    test_loss /= len(test_loader.dataset)
    test_accuracy = 100.0 * correct / len(test_loader.dataset)
    print('\nTest set: Average loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format(
        test_loss, correct, len(test_loader.dataset), test_accuracy))
    step = (epoch + 1) * len(train_loader)
    log_scalar('test_loss', test_loss, step)
    log_scalar('test_accuracy', test_accuracy, step)

def log_scalar(name, value, step):
    """Log a scalar value to both MLflow and TensorBoard"""
    mlflow.log_metric(name, value)

with mlflow.start_run():
    # Log our parameters into mlflow
    for key, value in vars(args).items():
        mlflow.log_param(key, value)

    # Create a SummaryWriter to write TensorBoard events locally
    output_dir = dirpath = tempfile.mkdtemp()
    # Perform the training
    for epoch in range(1, args.epochs + 1):
        train(epoch)
        test(epoch)

    # Upload the TensorBoard event logs as a run artifact
    with tempfile.TemporaryDirectory()  as tmp:
        filename = os.path.join(tmp, "model.bin")
        torch.save(model.state_dict(), filename)
        mlflow.log_artifacts(tmp, artifact_path="results")

実行は次のコマンドを実行します。

python  mnist.py && python mnist.py --epochs 20 && python mnist.py --batch-size 32 && python mnist.py --momentum 0.9

mlrunsディレクトリが作られます。このディレクトリに、学習した結果やログが残されています。
この情報を可視化する場合は次のコマンドを実行します。

mlflow ui

そして、localhost:5000にアクセスすれば保存されているディレクトリの情報を可視化できます。
画面を開くと次の画面になり、パラメータとメトリクスが一覧化されて可視化されています。これだけでもかなり見やすい。
フィルタなども記載できるのでおすすめです。このライブラリ相当奥が深そうなので、細かいのは後々紹介します。

また、各実験の画面を開くと、次のようになります。
今回表示していませんが、この画面で実験の出力物も管理できます。

更にはそのログの遷移を確認できます。

一言言うなれば凄く便利

良い点

1. UIがリッチ
UIがリッチでWeb経由でアクセスできるならば文句ありません。
パラメータのカラムが横ならびになっておらず、各カラムで一覧化されているのは有効でしょう。

2. 多機能
これまでのと比較すると非常に高機能です。
実験において、モデルも併せて記録ができます。また、今回紹介していませんがREST APIも対応しているなど、かなり至れりつくせりです。

3. Standaloneで実行可能
Standaloneで実行できるので、仕事で利用する場合にもうっかりクラウドに情報が漏れ出ることはありません。
wandbにしろ、Comet(他の実験管理)もクラウドに情報が出てしまうのが辛いところです。

ここが少し残念

1. 認証機能
ここまで良いと何人かで共通して使いたいものですが、さすがに、認証機能はありません。
そのため、nginxなど別のOSSを利用して補完する必要があります。
個人で使うには何も問題ありません。

まとめ

正直、MLflow最強では?と思っています。
他のと比較してもどのような環境であっても使える面、UIのリッチさを踏まえると、基本、これを選択すると間違いはないと思っています。
ただ、局所的には他のももちろん良いとも感じています。

magはコンソールで簡単に実施したい場合に有用です。
また、Weights and Biasesはネットワーク環境があれば、Web上から参照でき、かつ、高機能なので選択肢の一つとしてありだとも感じています。

最初の指標に併せて○×つけると次の通りです。MLFlowが万能すぎる。

方式 ハイパーパラメータの保存 学習曲線 出力物の保存 結果の可視化のリッチ度 集約しやすいか 備考
Excel × × ほぼ手動で頑張る必要あり
mag × × × コンソールしかない。
Weights and Biases Standalone版がなく、ネットワーク環境必須
MLFlow 認証機能について、独自実装 or 他OSSが必要

最後に

これを書く一つのモチベーションとして実験管理のライブラリは多種多様ですが、何が良いか頭を悩ませていました。
そのため、私の自分のコンペライブラリの中に、ツールをまだ組み込むことは手が動かず、正直できていませんでした。
コンペの合間にようやく、自分の手で動かし、確認できました。少しずつで良いので組み込んでみようと感じています。