連載
» 2020年06月12日 05時00分 公開

作って試そう! ディープラーニング工作室:プログラムを関数にまとめて、実行結果をグラフにプロットしよう (1/2)

ニューラルネットワークを使って学習や評価を行うコードを関数にまとめてみます。また、データセットを学習に使うものと精度評価に使うものに分割する方法、学習結果のグラフ化、過学習の抑制などについても簡単に見てみましょう。

[かわさきしんじ,Deep Insider編集部]
「作って試そう! ディープラーニング工作室」のインデックス

連載目次

 前回まではGoogle Colabのノートブックを使って、プログラムをベタに書いてきました。今回はCNNを使ってMNISTの手書き数字を認識するプログラムを幾つかのクラスと関数にまとめ、それらを実行し、実行結果をグラフにプロットしてみましょう。

今回作成するプログラム

 ここでは次のようなクラス/関数を作成します。

  • Netクラス:CNNを使って手書き数字を認識するクラス
  • train関数:Netクラス(と損失関数、最適化アルゴリズム)を用いて、学習を行う関数
  • validate関数:学習後のNetクラスのインスタンスの精度を評価する関数
  • do_train_and_validate関数:MNISTの手書き数字を受け取り、train関数とvalidate関数を呼び出す関数
  • get_loaders関数:訓練データを、訓練用/評価用のデータセットに分割して、それらを読み出すために使用するデータローダーを返す関数
  • plot_graph関数:1エポックごとの損失、精度をグラフにプロットするための関数

 これらの関係をざっくりと図にしたものを以下に示します。

今回作成するプログラムの構成 今回作成するプログラムの構成

 Netクラスは「CNNなんて怖くない! その基本を見てみよう」の最後で紹介したコードを(ほぼ)そのまま使用しています。

 学習や精度の評価を行うコードも基本的にはこれまでと同様で、それらを関数にまとめただけなので、コードを見て、「難しい……」と思うことはあまりないと思います。ただし、今回は合計6万個の学習用の手書き数字を訓練に使うものと、精度の評価に使うものに分割してみましょう。テスト用の手書き数字は、精度評価が終わったニューラルネットワークモデルに入力して、それが未知のデータに対しても汎用的に使えるかどうかの確認に使います。

 訓練データを分割して、それぞれに対応するデータローダーを作成するために、今回は上に示したget_loaders関数を定義することにしました。

 また、今回は1エポック(6万個のデータ)の学習と精度評価を複数回繰り返して、学習を進めるたびにニューラルネットワークモデルがどのくらい賢くなっていくかを調べることにしました。繰り返しの回数は50回です(この回数は筆者が適当に決めました)。そのため、全てが終わるまでには少々の時間がかかることは頭の中に入れておきましょう。

 それではこれらのクラスと関数、それらに付随するコードを見ていきましょう。

インポート文とデータセットの準備

 クラスや関数を見る前に、必要となるモジュールなどのインポートと、データセットを準備しておきます。これを行うコードが以下です。

import torch
import torchvision
import torchvision.transforms as transforms
import torch.optim as optim
import matplotlib.pyplot as plt
from torch import nn
from torchvision.datasets import MNIST
from torch.utils.data import DataLoader
from torch.utils.data import random_split

transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,))])

BATCH_SIZE = 20

trainset = MNIST(root='./data', train=True, transform=transform, download=True)
#trainloader = DataLoader(trainset, batch_size=BATCH_SIZE, shuffle=True)

testset = MNIST(root='./data', train=False, transform=transform, download=True)
testloader = DataLoader(testset, batch_size=BATCH_SIZE, shuffle=False)

impot文とデータセットの準備

 import文が多くなっていますが、これは今回使用するものを(コードを書いた後に筆者が整理して)全てここにまとめたからです。また、画面の都合でコードが横に長くなりすぎないように、明示的にインポートしているクラスもあります(MNISTクラスやDataLoaderクラスなど)。

 その後にはこれまでに何度も見てきたデータセットを取得するコードと(テストデータについては)データセットからデータローダーを作成するコードが続きます。先ほども述べた通り、今回は訓練データを訓練につかうものと精度の評価に使うものに分割するので、ここでは訓練データ用のデータローダーは作成しません(コードをあえてコメントアウトしてあります)。

Netクラス

 次にNetクラスを見てみましょう。そのコードを以下に示します。

class Net(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(1, 6, 5)
        self.pool = nn.MaxPool2d(2)
        self.conv2 = nn.Conv2d(6, 16, 5)
        self.fc1 = nn.Linear(16 * 16, 64)
        self.fc2 = nn.Linear(64, 10)
    def forward(self, x):
        x = self.conv1(x)
        x = nn.functional.relu(x)
        x = self.pool(x)
        x = self.conv2(x)
        x = self.pool(x)
        x = x.reshape(-1, 16 * 16)
        x = self.fc1(x)
        x = nn.functional.relu(x)
        x = self.fc2(x)
        return x

Netクラス

 既に見飽きた感もあるでしょうが、NetクラスではCNNの畳み込み層(Conv2dクラス)とプーリング層(MaxPool2dクラス)を使って、2次元データ(手書き数字)から特徴量を抽出し、それらを全結合層(Linearクラス)へ渡すようになっています。これらが実際にどのような振る舞いをするのかについては前回の記事を参照してください。

train関数とvalidate関数

 上記のNetクラスのインスタンスを使って、学習を実行するのがtrain関数です。そのコードは次のようになっています。

def train(net, dataloader, criterion, optimizer):
    net.train()

    total_loss = 0.0
    total_correct = 0
    for inputs, labels in dataloader:
        optimizer.zero_grad()
        outputs = net(inputs)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()

        total_loss += loss.item()
        _, predicted = torch.max(outputs, 1)
        total_correct += (predicted == labels).sum().item()

    avg_loss = total_loss / len(dataloader.dataset)
    accuracy = total_correct / len(dataloader.dataset)

    return avg_loss, accuracy

train関数

 この関数はニューラルネットワークモデル(net。この場合はNetクラスのインスタンス)、データローダー(dataloader。下で説明するget_loaders関数で得た学習で使用する分の訓練データを読み込むためのデータローダー)、損失関数(criterion)、最適化アルゴリズム(optimizer)をパラメーターに受け取ります。

 関数内のコードでは、まず「net.train()」呼び出しを行っています。これはニューラルネットワーククラスのインスタンスを「訓練モード」に設定するものです。PyTorchが提供するニューラルネットワークモジュールでは訓練モードと評価モードで動作が異なるものがあるため、今回は明示的にこれを設定しています。

 その後にある2つの変数total_lossとtotal_correctは、関数が受け取ったデータローダーに格納されている全ての要素を使って学習を行う際に算出される損失の総計(total_loss)と、ニューラルネットワークモデルからの出力と正解ラベルとを比較して正解だった数の総計(total_correct)を蓄積していくのに使っています。

 学習を行うコード自体はこれまでと同様です。

 最後に、損失の総計をデータローダーのデータ数で除算することで、今回の学習における損失の平均値を、また、正解数の総計を同じくデータローダーのデータ数で除算することで今回の学習における精度の平均を計算して、それらを戻り値としています。

 これらのデータは、後でエポックごとの損失と精度をグラフにプロットするために使用します。

 一方、上のコードで学習を行ったニューラルネットワークモデルを使用して、その精度を確認するために使用するのが以下に示すvalidate関数です。

def validate(net, dataloader, criterion):
    net.eval()

    with torch.no_grad():
        total_loss = 0.0
        total_correct = 0
        for inputs, labels in dataloader:
            outputs = net(inputs)
            loss = criterion(outputs, labels)

            total_loss += loss.item()
            _, predicted = torch.max(outputs, 1)
            total_correct += (predicted == labels).sum().item()

    avg_loss = total_loss / len(dataloader.dataset)
    avg_accuracy = total_correct / len(dataloader.dataset)

    return avg_loss, avg_accuracy

validate関数

 train関数とは異なり、validate関数は既存のニューラルネットワークモデルの精度を評価するのが目的で、学習を行うことはありません。そのため、この関数ではニューラルネットワークモデル(net)、データローダー(dataloader)、損失関数(criterion)をパラメーターに受け取るようにしています。

 内部のコードでは、最初に「net.eval()」呼び出しで、ニューラルネットワーククラスのインスタンスを「評価モード」に設定しています。これはtrain関数で行っていた「net.train()」呼び出しと対になるものです。

 その後は、「with torch.no_grad()」というwith文のブロックで「ここでは学習に必要な勾配計算は行わない」ことを明示した上で(こうすることでPyTorch内部での関連する処理を行わずに済むようになります)、データローダーに受け取った精度評価用のデータをニューラルネットワークモデルへと入力し、そこから損失と正解数を計算して、train関数と同様に2つの変数total_lossとtotal_correctに蓄積していきます。

 最後に損失の総計と正解数の総計からそれらの平均値を求めて、戻り値としています。これらを実際に呼び出しているのが次に示すdo_train_and_validate関数です。

do_train_and_validate関数

 この関数は、実質的に学習と精度評価を行うキモとなっています。ノートブックでは、Netクラス/損失関数/最適化アルゴリズムのインスタンスを作成して、最初に生成した訓練データ(データセット)と繰り返しの回数(エポック数)と共にこの関数を呼び出すことで、学習と精度評価を行うようになっています。

def do_train_and_validate(net, trainset, criterion, optimizer, epochs):

    trainloader, validloader = get_loaders(trainset)

    history = {}
    history['train_loss_values'] = []
    history['train_accuracy_values'] = []
    history['valid_loss_values'] = []
    history['valid_accuracy_values'] = []

    for epoch in range(1, epochs + 1):
        print(f'epoch: {epoch:2}')

        t_loss, t_accu = train(net, trainloader, criterion, optimizer)
        v_loss, v_accu = validate(net, validloader, criterion)

        print(f'train_loss: {t_loss:.6f}, train_accuracy: {t_accu:3.4%},',
              f'valid_loss: {v_loss:.6f}, valid_accuracy: {v_accu:3.4%}')

        history['train_loss_values'].append(t_loss)
        history['train_accuracy_values'].append(t_accu)
        history['valid_loss_values'].append(v_loss)
        history['valid_accuracy_values'].append(v_accu)

    return history

do_train_and_validate関数

 今も述べましたが、この関数はニューラルネットワークモデル(net)、訓練データ(trainset)、損失関数(criterion)、最適化アルゴリズム(optimizer)、繰り返し回数(epochs)をパラメーターに受け取ります。

 受け取った訓練データ(trainset)は、次に紹介するget_loaders関数に渡すことで、実際に学習に使用するデータセットを読み込むためのデータローダーと、その精度評価に使用するデータセットを読み込むためのデータローダーとが得られます。ここでは前者を変数trainloaderに、後者を変数validloaderに保存しています。

 その下にある辞書historyには、以下のfor文で実行される学習と訓練で得られた損失/正解率の平均値をリストの要素として蓄積していきます。

  • history['train_loss_values']:1エポックの学習で得られた平均損失を要素とするリスト
  • history['train_accuracy_values']:1エポックの学習で得られた平均正解率を要素とするリスト
  • history['valid_loss_values']:1エポックの精度評価で得られた平均損失を要素とするリスト
  • history['valid_accuracy_values']:1エポックの精度評価で得られた平均正解率を要素とするリスト

 その後は、パラメーターepochsに指定された回数だけfor文のループで、train関数およびvalidate関数を呼び出して、上記の辞書の要素(リスト)にそれぞれの値を追加していくようにしました。これにより、50回の繰り返しを指定すれば、学習によって得られた平均損失/平均正解率と、精度評価によって得られた平均損失/平均正解率が50回分たまります。今回はこれをグラフにプロットすることで、学習がどのように進められていくかを確認することにしましょう。

 そして、今回は訓練データを、学習に使用するものと精度評価に使用するものの2つに分割するのでした。これを行うのが、次に紹介するget_loaders関数です。

get_loaders関数

 既に述べたように、今回はMNISTの手書き数字の訓練データを学習に使うものと、精度評価に使うものの2つに分割します。データセットを分割するには、幾つかの方法がありますが、今回はシンプルにPyTorchのrandom_split関数を使用します。

 この関数はデータセットと、分割後のデータセットの要素数を格納するリストをパラメーターに受け取り、データセットを分割してくれます。分割後のデータセットの要素はランダムに並べ替えられます。MNISTの手書き数字(訓練データ)は6万個あるので、ここではそのうちの8割を学習に、残りの2割を精度評価に使うことにしましょう。後は分割後の2つのデータセットを基にDataLoaderクラスに渡して、データローダーを作成するだけです。

 実際のget_loaders関数のコードは次のようになります。

def get_loaders(dataset):
    train_size = int(len(dataset) * 0.8)
    valid_size = len(dataset) - train_size
    train_set, valid_set = random_split(dataset, (train_size, valid_size))
    trainloader = DataLoader(train_set, batch_size=BATCH_SIZE, shuffle=True)
    validloader = DataLoader(valid_set, batch_size=BATCH_SIZE, shuffle=False)
    return trainloader, validloader

get_loaders関数

 get_loaders関数では、最初に述べたようパラメーターdatasetに受け取ったデータセットの長さに0.8を乗じた値を学習に使用するデータセットの要素数として(48,000個)、残りを精度評価に使うデータセットの要素数としています(12,000個)。

 この2つの値と、データセットをrandom_split関数に渡した後は、得られたデータセットを使って2つのデータローダーを作成して、それを戻り値としています。

plot_graph関数

 最後に、do_train_and_validate関数の戻り値である平均損失/平均正解率をグラフにプロットするplot_graph関数を紹介します。

def plot_graph(values1, values2, rng, label1, label2):
    plt.plot(range(rng), values1, label=label1)
    plt.plot(range(rng), values2, label=label2)
    plt.legend()
    plt.grid()
    plt.show()

plot_graph関数

 ここでは2つの平均損失、2つの平均正解率を別個のグラフにプロットするものとして、2つの値とX軸の値となる繰り返し回数、それぞれの値のラベルをパラメーターに受け取るようにしています。

 これをMatplotlib.pyplotを使用して、グラフに描画しているだけです。

 以上で準備ができたので、次に実際にこれらを使って学習と精度評価を行ってみましょう。

       1|2 次のページへ

Copyright© Digital Advantage Corp. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。