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

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

CMakeを使ってビルドのみしている人のためのCMake入門

皆さんこんにちは
お元気ですか。ついに自宅のパスタがなくなりそうです。

さて、本日のテーマはcmakeです。
大半の人のcmake、実はこんな感じなのではないでしょうか。

  • インストール時にお世話になる(おまじない)けど、何もわからない(文字通り)
  • 仮に見たとしても何をやっているのかわからない。

私もある程度、これらに該当する人なわけです。
しかし、このおまじない系はまると長時間はまってとても困るといった問題がありました。
ということで改めてcmakeをきちんと勉強しておこうと思った次第です。

CMake

CMakeが何かはWikipediaにも書いてあります。

CMakeはコンパイラに依存しないビルド自動化のためのフリーソフトウェアであり、様々なオペレーティングシステムで動作させることができる。CMakeは階層化ディレクトリや複数のライブラリを利用するアプリケーションをサポートするよう設計されている。

環境によってもコンパイル方法(g++/clangなど)が異なるため、それぞれで設定を書く必要があります。
また、C++特有の依存関係を記述していくのは大変です。
それらを一括してCMakeは管理してくれます。

インストール

パッケージ管理ソフト(apt-get/homebrew/yum)でインストール可能です。

Ubuntu
sudo apt-get install cmake
Mac(Homebrew)
brew install cmake
CentOS
sudo  yum install cmake

CMakeを使ってC++コンパイルを行ってみる。

構成の準備

次のサイトのサンプルを参考にします。
derekmolloy.ie

helloworld.cpp/CMakeList.txtは同じディレクトリに配置してください。

1. helloworld.cpp

#include<iostream>

int main(int argc, char *argv[]){
   std::cout << "Hello World!" << std::endl;
   return 0;
}

プログラムはただの「Hello World!」出力用のプログラムです。

2. CMakeLists.txt

cmake_minimum_required(VERSION 2.8.9)
project (hello)
add_executable(hello helloworld.cpp)

cmake_minimum_requiredはcmakeの最低要求のバージョン
projectはプロジェクト名を示します。(挙動には関係ありません。)
今回の場合、add_executableはhelloworld.cppからhelloのファイルを作ります。

CMakeを試す

試してみましょう、次のコマンドで実行します。

mkdir build && cd build
cmake ..

buildディレクトリ作成もおまじないのようにさらっと記述されています。
これは、cmakeした結果のファイルが生成されるので、それ用です。
構成管理ツールでの管理を考えた時に、buildディレクトリを無視すればよいだけなので、管理しやすそうです。

cmakeが完了するとCMakeCache.txtなど、様々なファイルが生成されます。
これらはmakeを使う時に必要なファイル群です。

最後にmakeを実行すれば、実行ファイルの生成がされます。
後は実行ファイルをそのまま実行できます。

その他CMakeについて

CMakeでよく利用されるOption

特によく利用されるOptionは-Dです。
cmakeで環境を示す変数を指定しますが、-D Optionを付与することで、それよりも優先させることができます。
複数の環境を利用していると、デフォルトよりも特定の環境を指定したいこともあります。

その場合に-D Optionを利用して強制的に書き換えることもあります。

どんな変数が設定できるのかは「cmake .. -L」を実行すれば、確認可能です。

CMakeで作成されるMakefileのコマンド

CMakeで作成されるMakefileでよく利用されるコマンドをここでは紹介します
CMakeで作られたMakefileのおまじないの一種として「make -j8」やら「make install」といったコマンドがあります。

「make」では、ソースコードのビルドが行われます。殆どの場合はC++コンパイルですね。
また、「make install」を行うことで、規定のディレクトリに配置されます。これによって外部からでも呼び出し可能になります。

途中までのビルド結果を消したい場合は「make clean」を利用すれば良いでしょう。

ちなみに、よく使うOption「make -j」は並列数を指定できます。許せる限りのCPUコア数を数値として入れると良いでしょう。
ただし、コア数に対してジョブが多すぎると逆効果になるため、慎重に。(一度メモリ不足か何かで誤ってえらい目にあったことがあります)

最後に

おまじない(※僕だけかもしれない)を真面目に解説してみました。
一度はまったときにあれっと思うので、このようなことを勉強しておくのも時には必要でしょう。

その場合にぜひ、見てほしいと思います。

NGBoostを使ってみた

皆さんこんにちは。
お元気ですか。自粛ムードはまだまだ続きそうですね。

本日は少し前の発表された勾配ブースティングの手法NGBoostを紹介しようと思います。

NGBoostについて

NGBoostとは?

NGBoostは予測の不確実性をも推定する勾配ブースティングのアルゴリズムです。
従来までは、例えば温度を推定する場合、30度しか出ませんでした。
しかし、このNGBoostでは、どの程度30度らしいかも推定できます。
Kaggleよりも業務でフォールバックの機能として使えるので、便利で使い方の幅も感じるところです。

github.com

インストール

他のライブラリ同様、pipコマンド一発でインストールできます。

pip install https://github.com/stanfordmlgroup/ngboost.git

使い方

分類

from ngboost import NGBClassifier

from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error
from ngboost.distns import k_categorical

X, Y = load_iris(True)
X_train, X_test, Y_train, Y_test = train_test_split(X, Y, test_size=0.2)

ngb = NGBClassifier(Dist=k_categorical(3)).fit(X_train, Y_train)
Y_preds = ngb.predict(X_test)
Y_dists = ngb.pred_dist(X_test)

Distの引数にk_categoricalを与える必要があります。
このk_categorialには、分類数と同じ値を与えることが必要です。
Y_distsで各クラスの確率値を獲得できます。

回帰

from ngboost import NGBRegressor

from sklearn.datasets import load_boston
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error

X, Y = load_boston(True)
X_train, X_test, Y_train, Y_test = train_test_split(X, Y, test_size=0.2)

ngb = NGBRegressor().fit(X_train, Y_train)
Y_preds = ngb.predict(X_test)
Y_dists = ngb.pred_dist(X_test)

# test Mean Squared Error
test_MSE = mean_squared_error(Y_preds, Y_test)
print('Test MSE', test_MSE)

# test Negative Log Likelihood
test_NLL = -Y_dists.logpdf(Y_test).mean()
print('Test NLL', test_NLL)

こちらもサンプル通りです。
NGBRegressorには特に引数を与えなくとも、実行ができます。
分類と同様、pred_distメソッドにより分布を計算できます。

最後に

NGBoostをとりあえず使ってみました。
ただ、使ったばかりもあり、性能や精度の比較はまだ検討できていないです。
そのため、有用であればコンペでも使いたいと思っています。

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