神戸のデータ活用塾!KDL Data Blog

KDLが誇るデータ活用のプロフェッショナル達が書き連ねるブログです。

【第1回 実験結果登録編】Azure Machine LearningでMNIST!

こんにちは!株式会社神戸デジタル・ラボ DataIntelligenceチームの原口です。

今回はMicrosoftのAzure Machine LearningとAzure Machine Learningn SDKを利用して、MNIST学習の実験管理に取り組みます!

今回の目的

皆さん、機械学習は好きですか?私は大好きです。物体検出や画像生成、いろんなことが出来て夢がありますよね!そんな夢のある機械学習ですが、その実験管理はどのようにしていますか?

Excel、テキストベース・・・、ふむふむ。分かります。難しいですよね。

今回は難しい実験管理から機械学習の準備まで簡単に出来ちゃうAzure Machine Learningをご紹介します!

実験管理とは?

そもそも実験管理とは何でしょうか?実験管理とは、実験に関する「いつ・どこで・何を・どんなふうに・何のために・どんな結果が出たのか」を詳細に記した結果を管理することを指します。

なぜそんな管理をするのでしょうか?答えは比較を行うためです。実験管理を行っていない場合、過去の実験と比較することができないため、精度が上がった要因を考察することができません。

一方で実験管理を行うと、過去の実験に関する詳細な情報が記されているため、前回と今回でどこに差があるか分かるので、精度が上がった要因を容易に考察できます。

このように、実験管理は実験の影響を測るうえで非常に重要な役割を持っています

Azure Machine Learningとは?

Azure Machine Learning
Azure Machine Learningとは、Microsoftが提供している機械学習プラットフォームです。データセットの作成やモデルの学習・評価、学習プログラムの構築も行えるオールインワンなサービスです。

Azure Machine Learningはブラウザ上で操作する方法とSDKによって操作する方法があります。

ブラウザ上では、グラフィカルインターフェイスによる直感的な操作が可能です。一方のSDKは一般的に使われる方法で、開発者やスクリプト操作に慣れた方はこちらを使うと良いでしょう。

今回はAzure Machine Learning SDKと手書き数字データセットであるMNISTを利用して、手書き数字分類AIの構築及びその実験管理に取り組みます!

Azure Machine Learningの準備

まずはAzure Machine Learningの準備をしましょう。こちらからAzure Machine Learningのページに遷移します。

サインインまたは無料アカウントを選択
Azureのアカウントを所有されていない方は「無料アカウント」を、すでに持たれている方は「サインイン」を選択し、Azure Portalページにアクセスします。

Azure Portal画面

画面上部の検索欄に「Azure Machine Learning」を検索します。するとAzure Machine Learningが表示されますので、選択します。

新規作成

表示された画面上部にある「作成」をクリックすると、ワークスペース作成画面が表示されます。ワークスペース作成画面が表示されたら、各種設定をしましょう。Azure Machine Learningの設定方法はこちらの記事でご紹介しています。

ワークスペース作成画面

最後に構成ファイルをダウンロードします。構成ファイルにはワークスペースの情報が記載されていますので、取り扱いには注意が必要です。

構成ファイルのダウンロード

Azure Machine Learning SDKのインストール

ローカルマシンでする場合は、Azure Machine Learning SDKの利用準備をします。以下のコマンドを実行すると、必要なライブラリがインストールされます。

pip install azureml-sdk azureml-widgets

これで準備完了です!

数字分類の学習コードを作成

Azure Machine Learningに移る前に、まずはMNISTデータセットを用いて数字分類ができるコードを作成しましょう。

以下のコードはこちらのページの内容を参考に構築しています。各コードの意味などを確認したい方は、参考にしてください。

はじめに必要なライブラリをインストールします。

import os
import random
import math

import numpy as np
import matplotlib.pyplot as plt
from PIL import Image
from sklearn.metrics import accuracy_score
from tqdm import tqdm
import torch
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader
from torchvision import datasets, transforms

os.makedirs('outputs', exist_ok=True)

続いて乱数を固定します。固定することで、比較実験時に結果が乱数の影響を受けなくなります。

def torch_fix_seed(seed=2022):
    random.seed(seed)
    np.random.seed(seed)
    torch.manual_seed(seed)
    torch.cuda.manual_seed(seed)
    torch.backends.cudnn.deterministic = True
    torch.use_deterministic_algorithms = True

データセット周りの準備もしましょう。

class CustomDataset(Dataset):
    def __init__(self, data, label):
        self.data = data
        self.labels = label
        self.prep = transform = transforms.Compose([])

    def __getitem__(self, idx):
        data = self.prep(self.data[idx]).to(torch.float32).unsqueeze(0)
        label = self.labels[idx].to(torch.long)

        return data, label

    def __len__(self):
        return self.data.shape[0]


dataset = datasets.MNIST('./data', train=True, download=True)
data = dataset.data
label = dataset.targets

split_rate = 0.8
batch_size = 512
train_data = data[:math.ceil(data.shape[0] * split_rate)]
train_label = label[:math.ceil(data.shape[0] * split_rate)]

val_data = data[math.ceil(data.shape[0] * split_rate):]
val_label = label[math.ceil(data.shape[0] * split_rate):]


test_dataset = datasets.MNIST('./data', train=False, download=True)
test_data = test_dataset.data
test_label = test_dataset.targets

train_dataset = CustomDataset(train_data, train_label)
val_dataset = CustomDataset(val_data, val_label)
test_dataset = CustomDataset(test_data, test_label)

train_loader = DataLoader(train_dataset, batch_size=batch_size, num_workers=0)
val_loader = DataLoader(val_dataset, batch_size=batch_size, num_workers=0)
test_loader = DataLoader(test_dataset, batch_size=batch_size, num_workers=0)

続いてCNNモデルを構築します。今回構築するモデルは筆者が構築した独自モデルです。

class CustomModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.block_1 = nn.Sequential(
            nn.Conv2d(
                in_channels=1,
                out_channels=4,
                kernel_size=3,
                stride=1,
                padding=1),
            nn.ReLU(),
        )
        self.avepool_1 = nn.AvgPool2d(kernel_size=2, stride=2)
        self.block_2 = nn.Sequential(
            nn.Conv2d(
                in_channels=4,
                out_channels=8,
                kernel_size=3,
                stride=1,
                padding=1),
            nn.ReLU(),
        )
        self.avepool_2 = nn.AvgPool2d(kernel_size=2, stride=2)
        self.block_3 = nn.Sequential(
            nn.Conv2d(
                in_channels=8,
                out_channels=16,
                kernel_size=3,
                stride=1,
                padding=1),
            nn.ReLU(),
        )
        self.block_4 = nn.Sequential(
            nn.Conv2d(
                in_channels=16,
                out_channels=10,
                kernel_size=3,
                stride=1,
                padding=1),
            nn.ReLU(),
        )

    def forward(self, x):
        x = self.block_1(x)
        x = self.avepool_1(x)
        x = self.block_2(x)
        x = self.avepool_2(x)
        x = self.block_3(x)
        x = self.block_4(x)
        x = F.avg_pool2d(x, kernel_size=x.size()[2:])
        return x

最後に学習コードを作成します。

def calc_acc(y_pred, y_label):
    y_pred = torch.argmax(y_pred, dim=1)
    return torch.sum(y_pred == y_label)

device = 'cuda' if torch.cuda.is_available() else 'cpu'

torch_fix_seed()

num_epochs = 3
model = CustomModel().to(device)
opt = optim.Adam(model.parameters())
criterion = nn.CrossEntropyLoss()

train_loss = []
val_loss = []
best_loss = None
for epoch in range(num_epochs):
    with tqdm(train_loader) as pbar:
        total_acc = 0
        total_data = 0
        total_loss = 0
        for n, (data, y_label) in enumerate(pbar):
            data = data.to(device)
            y_label = y_label.to(device)
            opt.zero_grad()
            y_pred = model(data)
            loss = criterion(y_pred[:, :, 0, 0], y_label)
            loss.backward()
            opt.step()
            total_acc += calc_acc(y_pred[:, :, 0, 0], y_label)
            total_data += data.shape[0]
            total_loss += loss.item()
            acc = total_acc / total_data
            pbar.set_description(
                f"[train] Epoch {epoch+1:04}/{num_epochs:04} loss {total_loss/total_data:1.4f} Acc {acc:1.4f}")
    train_loss += [total_loss / total_data]
    with tqdm(val_loader) as pbar:
        total_acc = 0
        total_data = 0
        total_loss = 0
        for n, (data, y_label) in enumerate(pbar):
            with torch.no_grad():
                data = data.to(device)
                y_label = y_label.to(device)
                y_pred = model(data)
                loss = criterion(y_pred[:, :, 0, 0], y_label)
                total_acc += calc_acc(y_pred[:, :, 0, 0], y_label)
                total_data += data.shape[0]
                total_loss += loss.item()
                acc = total_acc / total_data
            pbar.set_description(
                f"[ val ] Epoch {epoch+1:04}/{num_epochs:04} loss {total_loss/total_data:1.4f} Acc {acc:1.4f}")
        if best_loss is None or best_loss > total_loss / total_data:
            best_loss = total_loss / total_data
            torch.save(model.state_dict(), "./outputs/model.pth")
    val_loss += [total_loss / total_data]

with tqdm(test_loader) as pbar:
    total_acc = 0
    total_data = 0
    label = []
    pred = []
    data_list = []
    for n, (data, y_label) in enumerate(pbar):
        with torch.no_grad():
            data = data.to(device)
            data_list.append(data.cpu().numpy())
            y_label = y_label.to(device)
            y_pred = model(data)
            label += y_label.cpu().numpy().tolist()
            pred += torch.argmax(y_pred[:, :, 0, 0],
                                 dim=1).cpu().numpy().tolist()
data = np.concatenate(data_list, axis=0)
print(accuracy_score(label, pred))

これで一通り学習ができるようになりました。では学習してみましょう!

[train] Epoch 0001/0003 loss 0.0033 Acc 0.4809: 100%|██████████| 94/94 [00:08<00:00, 10.62it/s]
[ val ] Epoch 0001/0003 loss 0.0018 Acc 0.7732: 100%|██████████| 24/24 [00:00<00:00, 26.22it/s]
[train] Epoch 0002/0003 loss 0.0013 Acc 0.8260: 100%|██████████| 94/94 [00:08<00:00, 11.00it/s]
[ val ] Epoch 0002/0003 loss 0.0009 Acc 0.8730: 100%|██████████| 24/24 [00:00<00:00, 25.20it/s]
[train] Epoch 0003/0003 loss 0.0008 Acc 0.8780: 100%|██████████| 94/94 [00:07<00:00, 11.94it/s]
[ val ] Epoch 0003/0003 loss 0.0007 Acc 0.8985: 100%|██████████| 24/24 [00:00<00:00, 24.34it/s]
100%|██████████| 20/20 [00:00<00:00, 29.12it/s]

0.8987

上のような結果が出れば学習の確認はOKです。Azure Machine Learningで実験管理をしましょう。

Azure Machine Learning仕様にチェンジ!

ではAzure Machine Learning仕様にチェンジしましょう。

Step1:下準備

まずはAzure Machine Learningのワークスペースと接続する必要があります。以下のコードを学習コードの前に記述してワークスペースと接続しましょう!

from azureml.core import Workspace
ws = Workspace.from_config()

続いて実験を定義します。この定義はAzure Machine Learningに「”test”という名前で実験します!」と宣言しているイメージに近いです。

from azureml.core import Experiment

experiment = Experiment(workspace=ws, name="test")

最後にAzure Machine Learningに実験開始を宣言しましょう。

run = experiment.start_logging()

これで下準備は完了です。

Step2:実験結果の登録

続いて実験結果を登録しましょう。

まずは学習・検証時の損失の経過を登録します。

学習コードの下に次のコードを追加します。

run.log_list("train_loss", train_loss)
run.log_list("val_loss", val_loss)

これによって、現在実行している実験のログをAzure Machine Learningに保存することができます。

特にリスト上のデータを保存する場合はrun.log_listを利用します。第一引数が登録名、第二引数が実際のデータです。

今回の例ですと、"train_loss"という名前でtrain_lossのデータが格納されたことになります。

続いてモデルの精度を保存しましょう。

run.log("Accuracy", accuracy_score(label, pred))

定数を保存する場合はrun.logを利用します。引数はrun.log_listと変化ありません。

さらにモデルの出力先を変更します。

torch.save(model.state_dict(), "./outputs/model.pth")

outputsに保存したデータはAzure Machine Learningに保存されます。とりあえずアップロードしておきたいと思ったものはoutputsに保存しましょう。

最後に実験終了をAzure Machine Learningに伝えましょう。

run.complete()

以上で準備完了です!実際に実行しましょう!

Step3:実験結果の確認

実行後、Azure Machine Learningのジョブを選択すると先ほどの実験結果が一番上に登録されていますので、選択しましょう。

実験後のAzure Machine Learningの画面

すると下図のような画面が表示されます。この画面では、実験が行われた日時や誰が行った実験か、さらには実験時に用いた引数も登録されています。どんな引数で実験したっけな・・・・。と悩む自分はもういません!

実験結果

ではメトリックタブを選択しましょう。メトリックでは、学習時に登録したデータの確認ができます。特にリストで保存したものに関しては、グラフとして描画されるので非常に便利です。

メトリック

最後に出力が保存されているか確認しましょう。出力とログのタブを選択すると次のような画面が表示されます。

出力とログ

ちゃんと保存されていますね!

Step4:モデルの登録

最後にモデルを登録しましょう!機械学習においてよく起こる問題が、どのモデルが一番精度が良かったのか分からなくなること・このモデルは何のモデルだったか忘れることです。

Azure Machine Learningにはモデルを登録する機能があるので、そこに登録しておきましょう!

そうすることで、「いつ・誰が・何を学習したモデルなのか・バージョン違いがあるのか」などの細かな情報を簡単に管理することができます。

run.complete()の下に、以下のコードを追加しましょう。

run.register_model(
    model_name="mnist-model",
    model_path="./outputs/model.pth",
    tags={
        "Dataset": "Mnist"},
    properties={
        "Accuracy": accuracy_score(
            label,
            pred)})

各引数について説明します。

  • model_name:登録時のモデル名
  • model_path:登録するモデルまでのパス
  • tags:モデルに付与するタグ
  • properties:モデルの精度や損失などの情報を付与できる

上記を実行後、Azure Machine Learningのトップページからモデルへアクセスしましょう。設定した名前で登録されていることが確認できます。

モデル一覧画面

Azure Machine Learningはモデルのバージョン管理もできます!なので、「あ!!間違えて上書きしてしまった!」「過去のデータ消しちゃった・・・」なんていう問題ともおさらばです!

まとめ

今回はAzure Machine Learningを用いてMNISTの実験管理をしました。Azure Machine Learningは精度や損失の履歴の管理はもちろん、モデルのバージョン管理も出来てしまう優れたツールです!

機会があればぜひご利用ください!

次回は、今回作成したコードをクラウドマシンで実行できるよう変更を加えます!お楽しみに!