【ディープラーニング体験】PytorchでMNISTの数字画像認識をやってみよう
Contents
はじめに
HGAでは、初回のご利用登録から最初の3日間はご利用料無料で提供しております。ご利用無料期間終了が近づいたら、すべてのインスタンスを停止ないし削除をしていれば課金されることはありません。ところで、この3日間という期間、インスタンスにログインして色々触ってみるだけじゃきっと持て余してしまいますね。
この期間中に、ディープラーニングを体験してみましょう(唐突)!MNISTという下図のような手書き数字を学習させるための処理系について検討していきましょう。このデータセットでは下図の手書き数字一個一個に正解ラベル(target)と呼ばれる正解値が割り付けられています。例えば、下図一番左上の「0」には「0」という正解ラベルが貼られているはずです。このように学習用のデータに正解値が割り振られているようなデータセットを使った機械学習のことを教師あり学習と呼ぶことがあるので覚えておくことをおすすめします。

それでは、以下の「前提」のセクションを確認していただいた上で解説に入りたいと思います。
前提
この記事は下記の条件を満たしている方を対象としております。
- HGAでインスタンスを作成済であること(インスタンスタイプは何でも大丈夫です)
- 理系学部1年生相当の数学知識(できれば一般相対論よりはやばくない程度のテンソルに関する知識も)がある。ないしこれからそこら辺を勉強したいと思っている。
DAY1: まずはサクッと動作検証まで
アクセスサーバの立ち上げ
最初にアクセスサーバへの接続を行います。ターミナルから以下のコマンドを入力してください。
ssh -L 20122:[インスタンスIP]:22 user@[アクセスサーバIP] -p 30022 -i .\.ssh\ackey.txt

インスタンスへの接続
新規にターミナルを立ち上げ下記コマンドにてインスタンスに接続してください。
下図のような画面になれば接続完了です。
ssh user@localhost -p 20122 -i .\.ssh\mykey.txt

gitのインストール
デフォルトではgitが入っていないため下記コマンドでインストールしてください。
sudo apt install -y git
conda環境の切り替え
conda env list
で環境一覧を確認します。今回はPyTorchを使える環境にする必要があるので
conda activate torch16_py36
でPyTorch環境を有効化します(詳しくはこちらの記事を参考にしてください)。
実行コードをクローンする
こちらのGitHubリポジトリにコード一式を上げておきました。
git clone https://github.com/highreso/Lrning_DeepLearning_in3days.git
でインスタンスにコードをクローンし
cd Lrning_DeepLearning_in3days
でプロジェクトディレクトリに入ります。
学習の実行
ここまで来てしまえば学習の実行は簡単です。下記コマンドで実行できます。
python train.py
インスタンスタイプにもよりますが、筆者が「amd1dl」タイプのインスタンスで検証したところ処理は大体10分程で終わりました。下の画像は処理が終わった最後の方のログの様子をキャプチャしたものです。実行中はすごい勢いでログが流れます。全部追う必要はないですが、Lossの値の遷移とTest Accuracyの箇所にさらっと目を通しておくといいでしょう。

ここまでできた方お疲れさまでした!
DAY2: コードの解説
train.pyは何をしているのか
さて、ここからはコードの解説に入っていきます。gitクローンでコードをインスタンスに落とした時、python train.pyというコマンドで学習処理を実行しました。この110行ほどのコードが全てです。この中で、学習用データセットの収集も実際の学習もやっています。
上の画像のログの最終行を見ると、Accuracy: 0.9803という箇所があります(この値は多少変動します)。これは何を表しているのでしょうか。実は、これはMNISTのような手書き画像の識別精度が(概ね)98%であることを表しております。下図のような手書き数字が正確に認識される割合が98%とは、人でも間違えしておかしくない手書き数字が入っていることを考えるとまずまずといったところではないでしょうか。実は今回は用いていませんが、CNNという画像に対してディープラーニングを行う際はほぼ使われる手法を用いれば99%は軽く超えます(実は最適なパラメタの値さえググれば出てきたりします)。

結果についてのお話はこれくらいにして、具体的な実装を見ていきましょう。VSCodeが入っているのであれば、こちらの記事を参考に環境をセットアップすれば
code train.py
でVSCodeからコードの内容を確認することができてとても便利です!それでは、主要部を解説していきます。
train.py: 学習ネットワークの定義
さっそくですが、ここが一番重要な部分です。
class DLNetwork(torch.nn.Module):
def __init__(self):
super(DLNetwork, 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)
ここでは、学習に用いるネットワークを定義しています。入力データ(つまり、MNISTの画像)の一枚一枚をこのクラスの実体、インスタンスに通すことが学習の第一歩です。下図の青枠で囲った箇所がDLNetworkクラスで定義した部分に相当します。

このクラスが何を意味しているのかを理解できることが鍵です。このように、学習ネットワークをクラスで定義するのはPyTorchではよく見かけます(chainerのプラクティスを踏襲しているのかな?)。__init__では使う層(図オレンジの細長い四角)をもれなく定義し、forwardではその層を少なくとも部分的に使って図の青い枠のようなシステムを構成するという理解で問題ありません。
「実装レベルまでとやかく注文するなんて傲慢な!」と思われる方もいらっしゃるかもしれませんが、ここは守破離の精神でいきましょう。正直製品化はともかく、AIのアルゴリズム検証等の段でオブジェクト志向なんて仰々しいものわざわざ導入必要あるかな?って思うときもあるのですが、皆がやってるので筆者もまねっこしてます。慣れるとネットワーク部が見やすくてわかりやすく思えるはずですよ!
train.py: 学習の実行
day2はこのセクションで終わりです。あと少しです!
この節では前節で定義した、ネットワークを用いて実際に学習を行っていきます。まずは該当部をどうぞ:
# 学習回数
epoch = 20
# ネットワークを構築
model: torch.nn.Module = DLNetwork()
# MNISTのデータローダを取得
loaders = load_MNIST()
# 損失関数の最小値を探すアルゴリズムを指定します。Adamは現在メジャーです。
optimizer = torch.optim.Adam(params=model.parameters(), lr=0.001)
# GPUを使うための設定です。PyTorchではGPUを使用したい場合、明示的指定が必要です。
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
for e in range(epoch):
"""訓練(training)セッション"""
loss = None
# 学習開始
model.train(True) # 引数は省略可能
for i, (data, target) in enumerate(loaders['train']):
data = data.view(-1, 28*28) # 全結合層のノードに落とすため28*28の2次元行列を28*28個の要素からなるベクタに変換する
data.to(device) # GPU演算を行うためにdataの内容をVRAMにアサインするイメージ(厳密には異なる)
optimizer.zero_grad()
output = model(data) # ネットワークの出力値
output.to(device) # GPU演算を行うためにoutputの内容をVRAMにアサインするイメージ(厳密には異なる)
loss = f.nll_loss(output, target) # 損失関数
loss.backward() # back propagationというアルゴリズムを適用(図参考)
optimizer.step() # 極値探索(最小値探し)を次のステップへ
if i % 10 == 0: # MNISTデータの数だけループが回るわけであるが、10の倍数の箇所でログ出力。
print('Training log: {} epoch ({} / 60000 train. data). Loss: {}'.format(e+1, (i+1)*128, loss.item()))
抜粋とはいえ結構長いですね・・・。更に主要部に絞って解説します。
まず、model: torch.nn.Module = DLNetwork()という箇所では前節で解説したネットワークをインスタンス化してmodelという変数に格納します。つまりmodelとはネットワークそのものであるという理解で差し支えありません。
次に、loadersというディクショナリに学習に用いるデータを格納します。実は、データの前処理が命という言葉があるくらい本来はもっともこだわって解説すべき箇所だと思います。しかし、今回は具体論よりもディープラーニングアルゴリズムの解説にフォーカスを当てているので見送りました。解説を見送ったからといってプライオリティが低いという訳ではありません。パフォーマンスに直結します。ここでは、正規化(標準化)・バッチ処理・シャッフリング・学習用データとテスト用データの分離というフレーズに目を通しておくようにしてください(特に正規化は統計学の知識ーー正規分布を平行移動したり縮尺かけていい感じの形にするあの処理等ーーが学習にどう結びつくのか考察のしがいがあって個人的におもしろいです!え、そんなこと聞いてない?すみません・・・)。
loss = f.nll_loss(output, target) という箇所では損失関数という多変数関数の値を求めています。実は、学習の目的とは、この損失関数の値が(基本的には)最も小さくなるような層のパラメタ群の値を求めることに他なりません。さて、一変数関数の最小値(極値)は微分を用いて求めることができます。実は数学にはこれを多変数関数に拡張して偏微分という手法を使って同様に極値を求めることができます。ですがこれはしんどいです。手計算でしんどいのは言うに及ばず、コンピュータにとってもしんどいです。そこで、偏微分お化けの一種なのは間違いないのですが直計算よりずっとましなback propagation(誤差逆伝播法)という手法を用います。アルゴリズム導出しようと数式を追うとgradはともかくテンソル演算の箇所で死にかけたりするのですが、あまり重要ではないので飛ばしちゃいますね。
入力データをネットワークの層に渡せるよう変換して、ネットワークの出力値とラベルから損失関数を計算し、この値が最小となるような層のパラメタの値をもとめるよう色々がんばるという一連の処理をループ文で回していくのが学習となります。下の図とセットでもう一度該当箇所のコードを眺めていただければと思います。

これでday2のセクションは終わりです!ここまでで学習(訓練)処理の流れについて説明してきました。もう一度コードを眺めたり、解説中に出てきたフレーズをググったりしてしっかり復習されることをおすすめします。
day3: コードの解説の続き等
train.py: 学習効果のテスト
day2では学習を行うコード部の解説を行いました。さて、学習処理が終わったしめでたしめでたしと行きたいところなのですが、不十分です。なぜなら、学習効果の検証ができていないからです。
さて、検証と言ってもどのように検証すれば良いのでしょうか?すぐ思いつく方法としては、実際に手書きで書いた数字を学習した結果得られたパラメタ値を使って構成されたネットワーク(学習済モデル)に入れてその出力(0~9)が元の手書き数字と一致しているかどうかを検証することでしょう。このための処理のことを推論といいます。これを何枚か何種類かやって正答率を計算すれば実用的で良い検証になると思います。しかし、これは面倒です。論理的にも、あるデータセット(今回はMNIST)を用いて学習したのであれば、そのデータセット内のデータを用いて検証するのが筋だと考えられます。しかし、それでも不十分です。もし検証に使うデータと学習に使ったデータに重複があった場合、学習に使ったデータに対して高い正答率が出てしまい偏りが生じるからです。
結論を言います。現状ベストプラクティスとされている検証方法は、データセットをあらかじめ学習に用いるデータとテストに用いるデータとに分け、学習用データを用いた訓練が完了後、今度はテスト用データに対して推論を行い、その正答率を指標とする方法です。該当箇所のコードを見ていきましょう。
for data, target in loaders['test']:
data = data.view(-1, 28 * 28)
data.to(device)
output = model(data)
output.to(device)
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()
このコード部どこかに似てません?学習処理と似てますね!データをシェイプして学習工程で得られたパラメタで構成されたモデルに流し込むことで、確率値からなる要素数10個のベクタが得られます。最も確率値が大きいところのindex値が、手書き画像の数字(予測値)です。注意していただきたいのは、index値が予測値と一致するのはMNISTのデータセットの仕様でそうなっているからです。必ずしもそうならないことがほとんどなので、予測値を得る際には確認するようにしましょう。
ここであらためて、python train.pyの実行結果のログをみてみましょう:

Accuracy: 0.9803とありますが、これはテスト用データに対する正答率が98%程度であることを意味しています。この値を高めるためにあれこれ試行錯誤を施すことがディープラーニング(ひいてはAI)の目標の一つです。その方法には、データの前処理の洗練化やネットワークの改良、学習を回す回数(エポック数)の変更等が挙げられます。機械学習の世界へようこそ!!
GPUでの処理について
GPU処理を行う方法については、ディープラーニングの本質から外れますが、うちの商売はクラウドGPUサーバの提供なので、さくっと解説させていただきます(読み飛ばしていただいても当面差し支えありません)。
PyTorchでは、GPUで処理したい旨を明示的に書かないとGPU演算を行ってくれません(CPUで処理されちゃいます)。GPUで処理を行う行うためにtrain.pyにもあるように最初に次のような宣言を行います。
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
ここでは、GPUで動かせそうならGPU使っちゃおう!的な宣言をしています。次に以下に示す箇所で配列の格納値をGPU演算で使えるようVRAM上へ展開するための処理をしております(注意:あくまでイメージです)。
data.to(device)
### 中略 ####
output.to(device)
つまりdataとoutput配列に対する演算はGPUで行う旨を宣言しており、逆にこれだけ書けばPyTorchでGPU演算ができるのです。大きくて深い配列はもれなくGPUにぶち込む方針で特に問題ないと思います。メモリダンプを起こした場合、主要部のみをGPUで演算させる方針に切り替えれば良いでしょう。
day3の終わりに:無料期間の確認
ここまで、しっかり3日かけて追ってこられた方はHGAサービスの無料使用期間の終わりが近づいているはずです。本当にお疲れさまでした!無料ご利用期間中でHGAサービスの使用を終わらせたい場合は、インスタンスの管理画面から作成した全てのインスタンスの削除または停止を行うようにしてください(期間終了前にメールが届いていると思うので確認してくださるようお願いします)。
そのままHGAインスタンスをご利用いただける場合は(ありがとうございます!)、従量課金と月額課金プランの違いはありますが、特に何もせず、使わないインスタンスは停止させておく方針で大丈夫です。
ここまで大変お疲れさまでした!皆さんはここまででディープラーニングの主要な処理を一通り体験したことになります。これから皆さんがディープーラーニング処理系を開発する際、今まで述べた処理群の一部または全てがリプレースされることになりますが、基本的なストーリーは変わらないはずです。このストーリーの次の語り手はあなたです。
day x: 筆者の考えを聞いてください!
という声から始まるセクションですが大した話ではないです。ちょっと数学についての話をさせてください(あくまで持論です)。「前提」のセクションの箇所で理系学部初等レベルの知識を前提として持っていてほしい、ないしこれから持つ意欲が必要旨書いていますが、ついぞ数式なんて一つも出てきませんでしたね!これは初心者向けに記事だからではなく、数式を使う必要がないほど(まあまあうまく)言葉でディープラーニングの処理の流れを説明することができたからです(図さえごく少数です)。
これは筆者の価値観に過ぎないのですが、数式ベースの説明で書かれたドキュメントは、 数学基礎論を除いては、その時点では完成度としてはそれほど高くなく、やがて言葉を用いての説明を検討するに至り始めて完成度を高めて行く段に入ると考えています。実のところ、数式ベースの説明は、厳密なディスカッションには寄与しても、説明そのものには大して寄与しないと考えています。式変形の速さには個人差や熟練度の差がどうしても出てしまい、場合によっては式変形を追うだけに時間と労力が取られ説明システム全体の敷衍ができなくなる恐れがあるからです。
この記事は上記の価値観が反映されたものであるので、「あれ?実はDLとかAIってあまり数学いらなくね?」なんて考えはゆめゆめ持たぬようお願いします。それはとんでもない勘違いです。AI分野では次から次へとアルゴリズムの提案や発見があり、HGAサービス無料期間中の間にもどんどん論文やアーカイブが生産されています。それらが実装済みのコードとして提供されることは稀で、基本的には自分で実装していく必要があります(ライセンスの問題があるので自主検証以外の用途で使用する場合関連箇所を確認するようにしてください)。そして、自分で実装するとなったら、アルゴリズムをちゃんと理解する必要があります。そのためのトレーニングとして、先ほどさらっと上げてスルーしたback propagationについての議論をしっかり追えるようにすることをおすすめします。その過程で情報エントロピ(およびび確率論の期待値の概念に関する正確な理解)のやテンソル演算、ベクトル解析など様々な応用数学上の重要概念に出くわすはめになるを学ぶことになるでしょう。
ともあれ、この記事をday1からday3までしっかり追えた方(ありがとうございます!)ならきっと大丈夫です!これからも楽しくやっていきましょう!
[HGAoffcial]
[dot]
[/dot]