
【PyTorch入門】PyTorchで手書き数字(MNIST)を学習させる
2021.12.20
PyTorchで手書き数字(MNIST)を学習させる
前回は、PyTorch(パイトーチ)のインストールなどを行いました。
今回は、いよいよPyTorchで手書き数字(MNIST)データセットを学習させていきたいと思います!
前回の記事はこちら
早速実装してみる
それでは、実装を始めます。
入門編ということで、一つ一つ丁寧に解説していきます!
必要なモジュールのインポート
今回使うモジュールを先に公開しておきます。
1 2 3 4 5 | import torch import torch.nn.functional as f from torch.utils.data import DataLoader from torchvision import datasets, transforms import matplotlib.pyplot as plt |
今回は、これだけ使用します。
(つまり最低でも、前回インストールした2つのモジュールがあればOKです)
ネットワークの構築
まずは、今回使うネットワークを定義していきます。
PyTorchでは、 torch.nn.Module というクラスを継承して、オリジナルのネットワークを構築していきます。
今回は MyNet という名前でネットワークを作っていきますが、ネットワーク構成はシンプルに「入力層(784) - 中間層(1000) - 出力層(10)」の3層構造とします。
- 中間層の活性化関数に「シグモイド(sigmoid)関数」
- 出力は確率にしたいので「ソフトマックス(softmax)関数」
中間層の活性化関数に「シグモイド( sigmoid )関数」を、出力は確率にしたいので「ソフトマックス( softmax )関数」を使用します。
今回は、MNISTという簡単なタスクで、なおかつ畳み込み層はないので、よく使用される ReLU関数 は使いません。(もちろん使っても良いです笑)
PyTorchでは、以上のようなネットワークの場合以下のように定義していきます。
1 2 3 4 5 6 7 8 9 10 11 12 | class MyNet(torch.nn.Module): def __init__(self): super(MyNet, self).__init__() self.fc1 = torch.nn.Linear(28*28, 1000) self.fc2 = torch.nn.Linear(1000, 10) def forward(self, x): x = self.fc1(x) x = torch.sigmoid(x) x = self.fc2(x) return f.log_softmax(x, dim=1) |
とてもシンプルです。
最低限、コンストラクタ( def __init__() )と順伝播の関数( def forward() )を定義すればOKです。
データセット(MNIST)のロード
MNISTをロードする関数を作りましょう。
PyTorchでは、TorchVisionというモジュールでデータセットを管理しています。
まずは、出来上がった関数を見てみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | def load_MNIST(batch=128, intensity=1.0): train_loader = torch.utils.data.DataLoader( datasets.MNIST('./data', train=True, download=True, transform=transforms.Compose([ transforms.ToTensor(), transforms.Lambda(lambda x: x * intensity) ])), batch_size=batch, shuffle=True) test_loader = torch.utils.data.DataLoader( datasets.MNIST('./data', train=False, transform=transforms.Compose([ transforms.ToTensor(), transforms.Lambda(lambda x: x * intensity) ])), batch_size=batch, shuffle=True) return {'train': train_loader, 'test': test_loader} |
PyTorchでは、データローダーという形でデータを取り扱うことが大きな特徴の一つです。
このデータローダーには、バッチサイズごとにまとめられたデータとラベルがまとまっています。
さらにデータは、 torch.tensor というテンソルの形で扱いますが、データローダーにおけるデータの形は(batch, channel, dimension)という順番になっています。
これは後で、実際に見てみましょう。
また、 torch.utils.data.DataLoader() では、第一引数に「データセット」を取ります。
今回は、その第一引数に datasets.MNIST() というMNISTのデータを扱うためのクラスインスタンスが与えられていることが分かります。
このクラス( datasets.MNIST())では、コンストラクタとして第一引数にデータのダウンロード先を指定し、そのほかに訓練データか否か( train=True なら訓練データ、 train=False ならテストデータ)を指定したり、 transform= でデータを正規化したりできます。
今回は、画素値の最大値を intensity 倍するような形ですが、他によく見る形として、
1 2 3 4 5 6 7 8 9 10 | train_loader = torch.utils.data.DataLoader( datasets.MNIST('./data', train=True, download=True, transform=transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,)) # ここが違う ])), batch_size=batch, shuffle=True) |
のように、平均と分散を指定すると良い精度になる場合もあります。
今回用意した関数では、戻り値として各ローダーを辞書型変数にして返しています。
メイン処理部分を書く
下準備完了です!
早速学習させる部分を実装していきます。
まずは、ネットワークを構築して、データを取得するまでを示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | if __name__ == '__main__': # 学習回数 epoch = 20 # 学習結果の保存用 history = { 'train_loss': [], 'test_loss': [], 'test_acc': [], } # ネットワークを構築 net: torch.nn.Module = MyNet() # MNISTのデータローダーを取得 loaders = load_MNIST() |
これだけでOKです。
最適化
次は、学習率にどのような最適化を適用するかを決めます。
今回は、Adam という最適化手法を使ってみましょう。
1 | optimizer = torch.optim.Adam(params=net.parameters(), lr=0.001) |
初期学習率は、0.001 としました。
学習部分の実装
では、核となる学習部分の実装に移ります。
大枠としては、下記のように学習回数のループの中に、訓練データのループとテスト(検証)データのループを作ります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | for e in range(epoch): """ Training Part""" loss = None # 学習開始 (再開) net.train(True) # 引数は省略可能 for i, (data, target) in enumerate(loaders['train']): pass ########## 学習部分 ########## """ Test Part """ # 学習のストップ net.eval() # または net.train(False) でも良い with torch.no_grad(): # テスト部分では勾配は使わないのでこのように書く for data, target in loaders['test']: pass ########## テスト部分 ########## |
それでは、早速中身を書いていきましょう。
訓練部分の実装
訓練部分は、以下のようにコーディングしてみました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | """ Training Part""" loss = None # 学習開始 (再開) net.train(True) # 引数は省略可能 for i, (data, target) in enumerate(loaders['train']): # 全結合のみのネットワークでは入力を1次元に # print(data.shape) # torch.Size([128, 1, 28, 28]) data = data.view(-1, 28*28) # print(data.shape) # torch.Size([128, 784]) optimizer.zero_grad() output = net(data) loss = f.nll_loss(output, target) loss.backward() optimizer.step() if i % 10 == 0: print('Training log: {} epoch ({} / 60000 train. data). Loss: {}'.format(e+1, (i+1)*128, loss.item()) ) history['train_loss'].append(loss) |
ここで実際にデータの形を出力してみると、先ほど話をしたように(batch, channel, dimension)になっていることがわかります。
ネットワークにデータを入力して出力を得るまでは、 output = net(data) だけで済むのは簡単ですね!
今回入力は、1次元でグレースケールなので、 data = data.view(-1, 28*28) で形を調整します。
そのあとは、ロスを計算( loss = f.nll_loss(output, target) )して、そのロスを元に誤差を逆伝播( loss.backward() )しているだけです。
ログは、10batch 毎に出力するようにしてみました。
テスト部分の作成
これで訓練部分は完成したので、テスト部分を作ります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | """ Test Part """ # 学習のストップ net.eval() # または net.train(False) でも良い test_loss = 0 correct = 0 with torch.no_grad(): for data, target in loaders['test']: data = data.view(-1, 28 * 28) output = net(data) test_loss += f.nll_loss(output, target, reduction='sum').item() pred = output.argmax(dim=1, keepdim=True) correct += pred.eq(target.view_as(pred)).sum().item() test_loss /= 10000 print('Test loss (avg): {}, Accuracy: {}'.format(test_loss, correct / 10000)) history['test_loss'].append(test_loss) history['test_acc'].append(correct / 10000) |
これも先ほどのコードと似ている部分がたくさんありますね。
テスト部分のロスは全て足して、最後に平均を取ることで、その学習(epoch)でのロスとしています。
また、テスト部分では精度も測りたいので、softmaxの確率出力の中で一番大きいニューロンのインデックスを取得しています ( pred = output.argmax(dim=1, keepdim=True) )。
このあと、ラベルと比較して一致しているものを正解数として記録しています。
完成!最終的なコード
最後に、結果を描画する部分を加筆して完成です!
ちなみに今回書いたコードは、「これが正解・最適」というわけではなく、筆者の好みも現れていますので、適宜自分の理解しやすいようにコーディングしてください!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 | """ PyTorchでMNISTを学習させる :summary PyTorchで単純な多層パーセプトロンを構築してみる :author RightCode Inc. (https://rightcode.co.jp) """ import torch import torch.nn.functional as f from torch.utils.data import DataLoader from torchvision import datasets, transforms import matplotlib.pyplot as plt class MyNet(torch.nn.Module): def __init__(self): super(MyNet, self).__init__() self.fc1 = torch.nn.Linear(28*28, 1000) self.fc2 = torch.nn.Linear(1000, 10) def forward(self, x): x = self.fc1(x) x = torch.sigmoid(x) x = self.fc2(x) return f.log_softmax(x, dim=1) def load_MNIST(batch=128, intensity=1.0): train_loader = torch.utils.data.DataLoader( datasets.MNIST('./data', train=True, download=True, transform=transforms.Compose([ transforms.ToTensor(), transforms.Lambda(lambda x: x * intensity) ])), batch_size=batch, shuffle=True) test_loader = torch.utils.data.DataLoader( datasets.MNIST('./data', train=False, transform=transforms.Compose([ transforms.ToTensor(), transforms.Lambda(lambda x: x * intensity) ])), batch_size=batch, shuffle=True) return {'train': train_loader, 'test': test_loader} if __name__ == '__main__': # 学習回数 epoch = 20 # 学習結果の保存用 history = { 'train_loss': [], 'test_loss': [], 'test_acc': [], } # ネットワークを構築 net: torch.nn.Module = MyNet() # MNISTのデータローダーを取得 loaders = load_MNIST() optimizer = torch.optim.Adam(params=net.parameters(), lr=0.001) for e in range(epoch): """ Training Part""" loss = None # 学習開始 (再開) net.train(True) # 引数は省略可能 for i, (data, target) in enumerate(loaders['train']): # 全結合のみのネットワークでは入力を1次元に # print(data.shape) # torch.Size([128, 1, 28, 28]) data = data.view(-1, 28*28) # print(data.shape) # torch.Size([128, 784]) optimizer.zero_grad() output = net(data) loss = f.nll_loss(output, target) loss.backward() optimizer.step() if i % 10 == 0: print('Training log: {} epoch ({} / 60000 train. data). Loss: {}'.format(e+1, (i+1)*128, loss.item()) ) history['train_loss'].append(loss) """ Test Part """ # 学習のストップ net.eval() # または net.train(False) でも良い test_loss = 0 correct = 0 with torch.no_grad(): for data, target in loaders['test']: data = data.view(-1, 28 * 28) output = net(data) test_loss += f.nll_loss(output, target, reduction='sum').item() pred = output.argmax(dim=1, keepdim=True) correct += pred.eq(target.view_as(pred)).sum().item() test_loss /= 10000 print('Test loss (avg): {}, Accuracy: {}'.format(test_loss, correct / 10000)) history['test_loss'].append(test_loss) history['test_acc'].append(correct / 10000) # 結果の出力と描画 print(history) plt.figure() plt.plot(range(1, epoch+1), history['train_loss'], label='train_loss') plt.plot(range(1, epoch+1), history['test_loss'], label='test_loss') plt.xlabel('epoch') plt.legend() plt.savefig('loss.png') plt.figure() plt.plot(range(1, epoch+1), history['test_acc']) plt.title('test accuracy') plt.xlabel('epoch') plt.savefig('test_acc.png') |
動作確認
実際に動かしてみると、学習後に以下のような図が得られるはずです!
訓練ロスが若干バタついていますが、テスト精度は98%以上と、しっかり学習できていそうですね!
さいごに
今回は、PyTorchの入門編という立ち位置で「MNISTを単純なネットワークで学習」させてみました。
実際にコードを見てみると、機械学習初心者でも比較的馴染みやすい書き方だと思います。
これから機械学習を始める方、新しい機械学習ライブラリを探していた方、是非一度触ってみてください!
最初に言ったように、おそらく、これからどんどんホットになっていく機械学習ライブラリがPyTorchです!
こちらの記事もオススメ!
書いた人はこんな人

- 「好きを仕事にするエンジニア集団」の(株)ライトコードです!
ライトコードは、福岡、東京、大阪の3拠点で事業展開するIT企業です。
現在は、国内を代表する大手IT企業を取引先にもち、ITシステムの受託事業が中心。
いずれも直取引で、月間PV数1億を超えるWebサービスのシステム開発・運営、インフラの構築・運用に携わっています。
システム開発依頼・お見積もり大歓迎!
また、現在「WEBエンジニア」「モバイルエンジニア」「営業」「WEBデザイナー」「WEBディレクター」を積極採用中です!
インターンや新卒採用も行っております。
以下よりご応募をお待ちしております!
https://rightcode.co.jp/recruit
ライトコードの日常12月 1, 2023ライトコードクエスト〜東京オフィス歴史編〜
ITエンタメ10月 13, 2023Netflixの成功はレコメンドエンジン?
ライトコードの日常8月 30, 2023退職者の最終出社日に密着してみた!
ITエンタメ8月 3, 2023世界初の量産型ポータブルコンピュータを開発したのに倒産!?アダム・オズボーン