データサイエンティストのたまご育成日記 vol.17 ディープラーニングの基礎 ― 後編 ―

f:id:yuzuhiko_persol:20200601103256p:plain
みなさん、こんにちは!デジタルテクノロジー統括部に新卒入社した長谷川智彦です。 データサイエンティスト未経験の新卒社員がデジタルテクノロジー統括部でどんなことをやっているのか、どのように成長していくのかの学びの過程を記録していくこの企画。今回はPytorchで実際に簡易的なNNモデルを作成したいと思います!


前回の記事では、AIってどういうものなのかやニューラルネットワーク(以下、NN)の概要に関してざっくりと書かせていただきましたが、今回はそのNNを実際にPytorchコードを交えながら見ていきたいと思います!
コードは小川 雄太郎著 『つくりながら学ぶ!深層強化学習 Pytorchによる実践プログラミング』(マイナビ出版)のコードをだいぶ参考にさせていただきました。こちら強化学習を始めるのにわかりやすくておすすめです。


Pytorchってなに?

前回の記事からPytorchという単語を連発していますが、これが何なのかを説明させてもらいます。
まず、コンピュータに何か動作を指示する際にはプログラミング言語を使用します。英語圏の人と会話する際に日本語だと話が通じないことが多いので英語で会話するのと概念は同じ感じです。

データ分析をする際にはこのプログラミング言語の中でもPythonというプログラミング言語がよく使われます。このPythonにはデータ分析をするのに便利なライブラリがたくさんそろっています。ライブラリは感覚としてはお料理キットみたいなものだと感じていて、それを使えば目的の品が作りやすくなるキットだと認識してます。
そのPythonライブラリの中にディープラーニングモデルを作るのに適したものが何個かありPytorchはそのうちの1つです(ライブラリにはほかにもKerasやTensorFlow、Chainerなどがあります)。

PytorchはFacebook社が開発したライブラリで、特徴としては、入力するデータサイズや次元数に合わせて調整できることや、Pytorchを使ってモデルを実装した最新研究が多いので、Pytorchを使えると自分でその公開されたモデルをすぐに実験できることがあげられます。

個人的にはKerasというライブラリが慣れているので使いやすいのですが、最近Pytorchによる実装が増えていて、勉強をしているので今回の記事もPytorchを使って頑張ってみます。


実装作業の概要

さて、これからNNの実装を行っていくのですが、大きな流れは以下のようになります。

f:id:yuzuhiko_persol:20210315200515p:plain

ざっくりといえば、データを用意して、データを入れるAIモデルを作り、そこにデータを入れて学習させ、学習させたAIモデルに予測をさせるといった流れです。これからコードを交えて説明していきますが、コードを見慣れてない方はなんとなくここではこんなことしてるんだなと思いながら最後までお付き合いください。


データの準備と前処理

f:id:yuzuhiko_persol:20210315201354p:plain

それでは、まず初めにデータを準備していきたいと思います。今回はMINST(エムニスト)というデータセットを使います。これは0-9までの手書きの数字が書かれた7万個の画像とその画像の書かれている数字の答えがセットになっているデータセットです(おそらくAIを勉強し始めた方にはなじみ深いデータセットです)。

f:id:yuzuhiko_persol:20210318104059p:plain:w300:h250
(MNISTの写真)

このMNISTをNNに学習させ、新たに画像を入れたときにそこに写っている数字を当てれる(分類できる)ようにしていきたいと思います。
それではまず、MNISTデータを以下のコードで揃えます。

#必要なライブラリのインポート
import torch #Pytorchのライブラリ
import numpy as np
import matplotlib.pyplot as plt #可視化のためインポート
%matplotlib inline
from sklearn.datasets import fetch_openml #ここにMINSTのデータが揃っています

#画像データとそのラベルをダウンロード
mnist_X, mnist_y = fetch_openml('mnist_784', version=1, data_home=".", return_X_y=True)

#画像データは0-255段階のグレースケールで構成されているので0-1に正規化しておきます
X = mnist_X.astype(np.float32) / 255
Y = mnist_y.astype(np.int32)

#データの確認(上にあげた画像が出てくるかと思います)
plt.imshow(X[0].reshape(28,28),cmap='gray')
print("ラベル:{:.0f}".format(Y[0]))


DataLoaderの作成

f:id:yuzuhiko_persol:20210318111248p:plain

では次に、DataLoaderを作成していこうと思います。DataLoaderは上で作成したデータセットをPytorchで使えるデータの形に変える目的で作成します。
今回は1. 訓練データとテストデータに分ける、2. NumPyのデータをTensorの形式に変える、3. バッチごとにデータを分けるといった操作を行います。

ちょっと用語が出たのでざっくりとだけ説明します(今回はモデルを組む全体の流れを知って欲しいので詳細は他のサイトや書籍で調べてみてください)。
訓練データは簡単にいえば今回NNに学習させるために使うデータで、テストデータは訓練データで学習させたモデルが他の同様のデータにどれだけ適用できるかを評価する際に使うデータだと思っていただきたいです(なので、訓練データとテストデータの精度が離れすぎると良くないです)。
次にTensorとパッと出しましたが、これはPytorchではデータをPytorchで使えるデータ形式に直す必要があるようで、それがTensor(テンソル)と呼ばれています。
最後のバッチに関しては用意したデータを一気にまとめてNNに学習させるのではなく、小分けにして学習させるために行うのだと認識いただければと思います。
簡易的なイメージは以下の感じです。

f:id:yuzuhiko_persol:20210319101348p:plain

以下はDataLoaderの作成のコードです。

#必要なライブラリのインポート
from torch.utils.data import TensorDataset, DataLoader #Pytorchには Tensor型に変えるためのライブラリやDataLoader作成のライブラリがあります。
from sklearn.model_selection import train_test_split #訓練データとテストデータを分けるため

X_train, X_test, y_train, y_test = train_test_split(X,Y,test_size=1/4,random_state=0) #訓練データ(train)とテストデータ(test)に分ける

#Tensor型に変換
X_train = torch.Tensor(X_train)
X_test = torch.Tensor(X_test)
y_train = torch.LongTensor(y_train)
y_test = torch.LongTensor(y_test)

train_set = TensorDataset(X_train, y_train)
test_set = TensorDataset(X_test, y_test)

#バッチのサイズを決め(これだと32分割、DataLoaderに入れる)
train_loader = DataLoader(train_set,batch_size=32, shuffle=True)
test_loader = DataLoader(test_set, batch_size=32, shuffle=False)

ここまでで、概要にあったデータの準備が終わりました。ここからはモデルの構築に移っていきましょう。

モデルの構築

f:id:yuzuhiko_persol:20210322104542p:plain

さて、ここからはモデルを構築していきます。
前回、以下のNNの構造を示しました。この図とコードを踏まえながら見ていきたいと思います。

f:id:yuzuhiko_persol:20210322101605p:plain

改めてNNの構造を見てみると入力層、中間層、出力層の3段階になってました。
以下のコードの部分で各層を用意します。

def __init__(self, n_in, n_mid, n_out,p):
    super(Net,self).__init__()
    self.fc1 = nn.Linear(n_in,n_mid) #入力層
    self.fc2 = nn.Linear(n_mid,n_mid) #中間層
    self.fc3 = nn.Linear(n_mid,n_out) #出力層
    self.drop = nn.Dropout(p=p) #ドロップアウト層

次のコードで層の順番に書いています(間にドロップアウト層を挟みましたが入力層、中間層、出力層の順になってます)。

def forward(self, x): #上から順に層を重ねていくイメージで記述していきます。
    h1 = F.relu(self.fc1(x)) #ここでデータを入力層に入れる
    h2 = self.drop(h1)
    h3 = F.relu(self.fc2(h2)) #入力層から中間層に伝わる
    h4 = self.drop(h3)
    output = self.fc3(h4) #中間層から出力層に伝わる
    return output

NNの図解で層ごとに分かれていましたが、実際のコードでも同様に層を用意し、重ねる順番を調整していきます。この総数やどんな層を重ねるかには答えはないので、これを時間をかけて探していきます。
ここまでで一旦モデルの全体のコードも載せておきます。

#必要なライブラリのインポート
import torch.nn as nn
import torch.nn.functional as F

#Chainer風に記述してます(こうしているコードをよく見るのでこちらで慣れるのがいいのかもです)
class Net(nn.Module):
    
    def __init__(self, n_in, n_mid, n_out,p): #ここは初期設定みたいな感じです。ここで層を用意します。
        super(Net,self).__init__()
        self.fc1 = nn.Linear(n_in,n_mid) #入力層
        self.fc2 = nn.Linear(n_mid,n_mid) #中間層
        self.fc3 = nn.Linear(n_mid,n_out) #出力層
        self.drop = nn.Dropout(p=p) #ドロップアウト層
        
        
    def forward(self, x):
        h1 = F.relu(self.fc1(x)) #ここでデータを入力層に入れる
        h2 = self.drop(h1)
        h3 = F.relu(self.fc2(h2)) #入力層から中間層に伝わる
        h4 = self.drop(h3)
        output = self.fc3(h4) #中間層から出力層に伝わる
        return output
    
model = Net(n_in = 28*28*1, n_mid = 100, n_out = 10,p=0.1) #パラメータやニューロン数を設定します。

コード内の入力層にあるn_inが入力するデータの次元で今回は画像なので28*28*1(縦×横×RGBがないのでグレースケールの1)になります。
次に中間層のn_midは間にあるニューロンの数を表していてここでは28*28*1の次元を100にまで圧縮しているイメージです。
最後の出力層は中間層の入力を受け、それを分類したい0-9の10個のノードで出力をしています(n_out)。
こういった形で層を設定し、その層の持つニューロン数をいじったりして精度のいいモデルを作っていきます。
途中にあるLinear()は上のNNのモデルの横に書いた線形結合の計算を行ってくれ、ReLUは活性化関数の一つです。
Dropoutは過学習を防ぐために学習に使うニューロンの割合を決めるのですが、これらを説明し始めると記事がとてつもなく長くなるので割愛させていただきます。
前回の記事でも書きましたが、この記事で目指すのはAIに関して全く知らなかった方がなんとなくわかった気になれる状態を目指します!一先ずAIの精度を上げるためにいろんな工夫をするのだなといった認識でいていただければ幸いです。


誤差関数と最適化手法の選択

ここでは誤差関数と最適化を設定していきます。

f:id:yuzuhiko_persol:20210322134733p:plain

誤差関数の方だけを簡単に説明します。いきなりですが皆さん、勉強した時に自分に知識がついたかをどうかをどのように調べますか?恐らく何かしら関連する問題を解いてみて、答え合わせをするやり方を選ぶ方が多いのではないかなと思います。
自分の答えが実際の答えと合っていれば問題はなく、間違ってたら振り返りますよね。AIも似たような概念で学習で出した答えと実際の答えを照らし合わせて(差分や一致しているかを確認して)、それを元に振り返ります(正確には重みの調整をします。詳しくは大学レベルの微分の知識がいるのでここでは扱いませんが、気になる方は逆勾配伝播法を調べてみてください)。その時に全体の学習で予測した答えと実際の答えとの誤差を計算するのに使用するのが誤差関数です(損失関数とも言ったりします)。
以下のコードでは誤差関数と最適化手法を設定しています。

#必要なライブラリのインポート
from torch import optim

loss = nn.CrossEntropyLoss() # 誤差関数を定義。今回は複数の分類問題なのでクロスエントロピー関数を使いました。

optimizer = optim.Adam(model.parameters(), lr = 0.01) #重みの調整方法を定義、lrは学習率

ここであげたクロスエントロピー損失関数(誤差関数と言っているのはこれです)は複数の分類の際によく使われる誤差関数で以下の式で表されます。

H(p,q)=−∑p(x)log(q(x)) (pは真の確率分布、qは推定した確率分布)

少しややこしく見えるかもしれませんが実際の確率の分布と予測した確率の分布が近ければ値が小さくなり、分布が近くなければ値が大きくなります。
例えば(ゆず、りんご、安納芋)があって答えがゆずの場合、真の確率は(1,0,0)となります。
予測が真の確率と近い時(0.9,0.1,0.1)と近くない時(0.1,0.9,0.7)の二つを比較すると、近い時のH(p,q)の値は0.105、近くない時の値は2.303なので真の確率に近い方が値が小さいことを確認できるかと思います。
この値が小さくなれば誤差、つまり間違いが小さいということになってくるので、重みがうまく調整出来ていくことになります。
Optimizerと途中コードがありますがこちらはどんなペースで学習していきますか?といったものです。


学習と評価

f:id:yuzuhiko_persol:20210322164324p:plain

さて、いよいよ実際に学習させてみましょう。
以下がコードなのですが、modelにdataを入れ、誤差関数で誤差を見ながら学習していきます。
epochは何回その操作を繰り返したを表します。

def train(epoch):
    model.train() #訓練モード
    
    for data, targets in train_loader:
        optimizer.zero_grad()
        outputs = model(data) #ここで作成したモデルにデータを投入します。
        loss_ = loss(outputs, targets) #誤差関数(損失関数)にモデルの予測と答えを入れます。
        
        loss_.backward() #バックプロパゲーション
        optimizer.step()
        
    print("epoch{}回目".format(epoch))

どれだけ予測が答えと合っているかを以下のtestで見ています。

def test():
    model.eval() #評価モード
    correct = 0 #初期値
    
    with torch.no_grad():
        for data, targets in test_loader:
            outputs = model(data)
            
            _,predicted = torch.max(outputs.data, 1)
            correct += predicted.eq(targets.data.view_as(predicted)).sum()
            
      
    data_lenght = len(test_loader.dataset) #全体の数
    print('正解割合:{}/{}'.format(correct,data_lenght)) #合っていた数/全体の数

上で作成したモデルを学習して、正答割合を出した結果は、、、。

正解率:16776/17500 *100= 95.9

ということで約96%の精度を持つみたいです(高すぎて怖いですが)。
本当はエポック数を変えながら正答率を見ていく方がいいのですが今回は10回で固定して見ています。


推論

それでは最後に推論のプチ実験をしてみます。

f:id:yuzuhiko_persol:20210322200921p:plain

コードは以下の通りで、2021年なんでindex番号が2021の画像の数字の推論をさせています。

index = 2021 #番号の指定

model.eval()#評価モード
data = X_test[index] #テストデータから取ってくる
output = model(data)
_,predicted = torch.max(output.data,0)
print("予測結果{}".format(predicted))

#画像の可視化
X_test_show = (X_test[index]).numpy()
plt.imshow(X_test_show.reshape(28,28),cmap='gray')
print("正解ラベル{:.0f}".format(y_test[index]))

結果はこんな感じになりました。

予測結果6
正解ラベル6

f:id:yuzuhiko_persol:20210322201358p:plain:w300:h300

実際の答えである6に対してしっかりと6を予測してくれてます。ビジネスに使う場合はもっと推論用のデータを用意して、実測値と予測値を照らし合わせて使えるかどうかを検討したりします。


総括

途中途中専門的な用語の説明を省いたり、操作を簡略化してしまったのでデータサイエンティストの方には物足りなかったかもしれませんが、ざっくりとNN構築の概要を記事にさせていただきました。AIをあまり知らなかった方になんとなく、AIのモデルを作るのってこんなことをするんだというのが伝わってくれると幸いです。

さて、この記事で僕の1年目でのデータサイエンスのたまご育成日記は終わりになります。ですが、来期以降もペースは確実に落ちるとしても、その分専門性を高めて発信して行きたいと思うので次回のデータサイエンティストのたまご育成日記をお楽しみにしていてください!(もしかしたらアイキャッチとタイトルが変わるかもです!)


オススメの書籍

今回、NNのPytorchでの記述にあたって小川 雄太郎さんが執筆したつくりながら学ぶ!深層強化学習 Pytorchによる実践プログラミング』(マイナビ出版)を参考にさせていただきました。強化学習の入りから深層強化学習や強化学習の変遷など強化学習を学ぶにあたって勉強になるのでオススメです。



alt


長谷川 智彦 Tomohiko Hasegawa


デジタルテクノロジー統括部 データ&テクノロジー ソリューション部 アナリティクスグループ

大学時代の専攻は植物学・分子生物学。最近趣味でデザインをかじり出した社会人1年目。植物の実験データを正しく解釈するために統計を勉強し始め、データ分析に興味をもつ。データサイエンスはただいま必死に勉強中。

※2021年3月現在の情報です。

▶パーソルキャリアの求人ページはこちらから