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

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

Pythonの環境を管理・再現する

皆さんこんにちは
お元気ですか。3月末というのに、雪積もっていてすごかったです。

さて、本日はPythonでの環境の管理方法を紹介します。
今まではAnacondaを利用しており、それを利用してimport/exportする方法もあります。
これに加えて最近はPipenvも増えたようでもあり、それら紹介をします。

なぜ、環境の管理をする必要があるのか?

誰もが同じ環境を再現し、環境による問題を起こさないためです。
仕事やOSSで複数人でのプロジェクトで環境を統一しないと、依存ライブラリのバージョンによる問題が発生することもあります。
そのため、環境を統一することは非常に重要です。

環境構築の方法

本章では、大きく分けて3つの方法を紹介します。

1. requirement.txt
2. Anacondaの仮想環境
3. Pipenv

requirement.txt

古来からある方式であるrequirement.txtです。
未だにライブラリでも多く採用されている方式の一つです。
requeirement.txtはライブラリの名前が一覧で書いており、それを読み込むことで必要なライブラリをインストールできます。

requirement.txtの作り方は次の通りです。

pip freeze > requirement.txt

読み込み方は次の通りです。

pip install -r requirement.txt

この方法ですが、pipコマンドが実行できれば、実行可能なのがメリットです。
AnacondaやPipenvでもpipが利用できるので使えます。
ただ、Pythonのバージョンは管理できていないので、そこは一つネックになるポイントです。

Anacondaの仮想環境

Anacondaを用いて、仮想環境を作成できます。
基本的な利用方法は次の通りです。
nonbiri-tereka.hatenablog.com

Anacondaによる仮想環境の構築は次のコマンドで作成可能です。

conda create -n py38 python=3.8

その仮想環境の情報を出力するには次のコマンドを実行します。

conda env export > condatest.yml

出力されたファイルを読み込む方法は次の通りです。

conda env create -n py38_re -f condatest.yml

仮想環境ごと環境構築ができるのでPythonのバージョンも管理できるのが強みです。
ただし、Anacondaそのものをインストールせねばならず、ディスクの容量も必要とする点がネックでしょうか。

Pipenv

Pipenvが最近は利用されるようになっています。
まずは、pipenvをインストールします。

pip install pipenv

次にpipenvを利用した環境構築を行います。

pipenv --python 3.7 # python3.7での環境構築
pipenv install numpy scipy matplotlib

逆にPipenvの環境を取り込み、構築する場合は次の通りです。
Pipfileがあるディレクトリで、次のコマンドを入力します。

pipenv install 

pipenvは導入までの時間が非常に早く、手軽に構築できるのが良い点だと感じました。

正直どれが一番なのか

どれも一長一短ですが、Pipenvが楽そうです。
Anacondaだとそのインストール自体も必要になり、導入コストが非常に高いと感じています。
また、requirement.txtのみだと、同一PC内での複数環境での管理が難しくなるので、避けたほうが良いと思っています。

最後に

これらそれぞれに利点欠点もあるため、ケースバイケースで考慮すべきでしょう。
このあたりは使いながら検討する必要があると感じています。

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研究会さんありがとうございました。
また参加したいと思います!