本記事は「PyTorch チュートリアル Part1:XORゲートの実装を体験」の続きです。今回はより複雑なデータの分類に挑戦します。
本記事の構成は下記の通りです。
Contents
2クラスデータセットの生成とデータの確認
今回は、2クラスデータセットの分類に挑戦します。用いるデータセットは scikit-learn(sklearn) という Python 用の機械学習フレームワークの機能を使って生成します。sklearn の datasets モジュールには多数のデータセットが存在しますが、独自にデータセットを作り出す機能もあります。今回はデータセットを作り出す機能の方を使用して、プロットすると円形になるようなデータセットを作成します。
では、円形データセットを生成してプロットしてみます。クラスはクラス0とクラス1の2つ存在するので色分けしてプロットします。
上では、クラス0のデータは赤い点、クラス1の点は青い点で描画しています(以降本記事では、データセットの中のデータを「データ」、データセットの中で各データが属するクラスを「ラベル」と呼びますので注意してください)。今回はモデル自身に学習させることによってデータセットを上の図のように上手く分類させることが目標となります。
生成したデータセットについてより詳しく見てみましょう。データXの形状を出力してみます。
続いて、ラベルYの形状を見てみます。
よって、このデータセットは以下の図のような構造であることがわかります。
ディープラーニング:モデルの学習による2クラス分類
データセットの構造がわかったところで、モデルを構築して学習させていこうと思います。一般的に、ディープラーニングの流れは以下のようになります。
- 訓練用データセット、試験用データセットの用意
- モデルを用意
- 訓練用データセットでモデルに学習させる
- 試験用データセットで推論
訓練用データセット、試験用データセットの用意
ここで、データセットの分割方法には注意しましょう。Yを出力してみるとわかりますが、データセットはシャッフルされてない状態です。
最初の「訓練用データセット、試験用データセットの用意」ですが、我々はすでにsklearnの機能で円形データセットを生成しました。しかし、訓練用データセットと試験用データセットの二つのデータセットを用意したわけではないので、生成した円形データセットを訓練用データセットと試験用データセットに分割することにします。訓練用データセット : 試験用データセット = 8 : 2 の割合で分割します。
よって、データセットをシャッフルしてから分割する必要があります。sklearnの train_test_splitメソッドは、データセットをシャッフルしながら分割してくれます。
学習には訓練用データセットのみを用います。上のコードの出力結果より、分割後のデータセットの構造は以下のようになっています。
分割された後のtrain_Yを出力してみても、ちゃんとシャッフルされています。
これらはnumpy.ndarrayなのでtorch.tensorに変換しておきましょう。
#torch.tensorへ
train_X = torch.FloatTensor(train_X) #float32のtensorに変換
train_Y = torch.FloatTensor(train_Y)
test_X = torch.FloatTensor(test_X)
test_Y = torch.FloatTensor(test_Y)
さらに、次に説明するミニバッチ学習のために、訓練用データセットはデータとラベルをまとめておきます。
from torch.utils.data import TensorDataset
train = TensorDataset(train_X, train_Y) #train_X、train_Yを一つにまとめる
ニューラルネットワークに一度に全てのデータセットを入力して学習させることを「バッチ学習」と言います。バッチとは、全てのデータセットの塊のことです。データが少ないうちはこのバッチ学習でもよいのですが、データが多くなるとこの方法ではコンピューターに多大な負荷がかかります。そのため、一般的にはデータセットを小さなミニバッチに分けて、少しずつモデルに入力して学習させます。この方法を「ミニバッチ学習」と言います。今回はそれほどデータ数は多くないのですが、後々のためにこのミニバッチ学習で実装することにします。
PyTorchを用いればミニバッチ学習は容易に実装することができます。torch.utils.dataのDataloaderクラスは、データセットから任意のサイズのミニバッチを自動で作成してくれます。試験用データは学習には用いないため、訓練用データのみデータローダーを作成することにしました。ここでは、ミニバッチのサイズは8に設定しました(ミニバッチのサイズは大きすぎなければ任意の値で大丈夫です)。
from torch.utils.data import DataLoader
BATCH_SIZE = 8 #ミニバッチのサイズ
#訓練用データのDataloaderを作成
train_dataloader = DataLoader(
train,
batch_size=BATCH_SIZE,
shuffle=True
)
Dataloaderの引数には、用いるデータセット、ミニバッチのサイズ、各エポック毎にシャッフルしてからミニバッチを作成するか否かを示すbool値の三つを指定しておく必要があります。
これで訓練用、試験用のデータセットを用意できました。
モデルを用意
続いて、モデルを用意します。今回のモデルはnn.Moduleを継承したクラスとして定義します。
このモデルは入力層のサイズが2、隠れ層のサイズが8、出力層のサイズが1のニューラルネットワークです。隠れ層の活性化関数はReLU関数、出力層はシグモイド関数を用います。出力層のサイズが1なので、損失関数はBCE損失(Binary Cross Entropy Loss)を用います。出力層のサイズが1で、活性関数がシグモイド関数ならば一般的にはBCE損失が用いられます。今回は最適化手法についてはそこまで気を付ける必要がないです。よく用いられるSGD(確率的勾配降下法)を用います。
訓練用データセットでモデルに学習させる
import torch
import torch.nn as nn
from torch.nn import functional as F
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.fc1 = nn.Linear(2, 8)
self.fc2 = nn.Linear(8, 1)
self.sigmoid = torch.nn.Sigmoid() #出力層の活性化関数はsigmoid関数を使用
# 順伝播
def forward(self, x):
x = self.fc1(x)
x = F.relu(x)
x = self.fc2(x)
x = self.sigmoid(x)
return x
# インスタンス化
net = Net()
# 損失関数の設定(BCE損失)
criterion = nn.BCELoss()
# 最適化手法の選択(SGD)
optimizer = torch.optim.SGD(net.parameters(), lr=0.1)
では、ここまでのまとめを兼ねてモデルに学習させていきましょう。訓練ループを作成し、訓練用データで100エポック学習させます。訓練ループの内側に、データローダーによるループが入っていることに注意してください。データローダーからはデータがループ処理で取り出され、モデルに入力されるようになっています。
損失が一気に減少していっているのがよくわかります。上手く学習できているようです。
試験用データセットで推論
最後に、試験用データで推論させてみましょう。
推論させる前に、このニューラルネットワークの出力層における活性化関数はシグモイド関数なので、出力値は少数です。しかし出力がtest_Yとどれほど一致しているかを確認したいので、0か1のどちらかに出力値の離散化を行います。ここでは0.5より大きければ1、0.5より小さければ0という様に離散化を行いました。
import torch
# 離散化を行う関数
def discretize(proba):
threshold = torch.Tensor([0.5]) # 0か1かを分ける閾値を0.5に設定
discretized = (proba >= threshold).int() # 閾値未満で0、以上で1に変換
return discretized
試験用データをモデルに入力し、その出力を離散化します。さらにその結果をリストに格納します。
import torch
import numpy as np
with torch.no_grad():# 試験用データでは勾配を計算しない
pred_labels = [] # 各バッチごとの結果格納用
for x in test_X:
pred = net(x)
pred_label = discretize(pred) #離散化する
pred_labels.append(pred_label[0])
pred_labels = np.array(pred_labels) #numpy arrayに変換
また、推論の結果は以下のプログラムで描画します。
# 推論
X_red = test_X[pred_labels == 0]
X_blue = test_X[pred_labels == 1]
plt.scatter(X_red[:, 0], X_red[:, 1], color='red') #ラベルが0のデータ
plt.scatter(X_blue[:, 0], X_blue[:, 1], color='blue') #ラベルが1のデータ
では、実際に推論までさせてみましょう。
かなり上手く分類できていますね。
2クラス以上の分類のためのテクニック「ワンホットエンコーディング」の紹介
ここまでで2クラス分類は完了ですが、最後に少しだけ補足しておくことがあります。
今回のデータセットはsklearnのdatasets.make_circles関数を用い、ラベルはその機能で自動的に割り振られた0もしくは1という一つの数字でした。このように一つの数字でラベルを表すことをラベルエンコーディングなどと呼びます。例えば今回のように赤は0、青は1、といった具合です。
しかし他にもラベルの表し方があり、よく用いられるものにワンホットエンコーディングというものがあります。これは0と1の配列でラベルを表現するものです。この方法では各クラス毎に0と1の配列を作り、一か所だけ1にして他の値は0に設定し、その1の位置により区別を行います。言葉で説明されてもよくわからないかもしれませんが、下の図を見てください。三つのクラス、りんご、みかん、ももがあったとき、それぞれに番号を割り振るのがラベルエンコーディングです。それに対し、0と1のベクトルでラベルを表現するのがワンホットエンコーディングです。
今回は2クラス分類するときのラベルはラベルエンコーディングで表しましたが、ワンホットエンコーディングでラベルを表現する方法でも実装してみようと思います。
上のコードのワンホットエンコーディングのところでは、ラベル0は[1,0]に変換し、ラベル1は[0,1]に変換しています。わざわざラベルを一つの数字から配列に置き換えるのは無駄に思えるかもしれませんが、よりクラス数が増えたときにはこのワンホットエンコーディングが便利です。Yを出力してみた結果、実際に0と1の二次元配列が出力されていることもわかります。
ラベルをワンホットエンコーディングすることができたので、次に学習に入ります。流れはワンホットエンコーディングしなかったときと同じです。まず、データセットを訓練用データセットと試験用データセットに分割します。
from sklearn.model_selection import train_test_split
train_X, test_X, train_Y, test_Y = train_test_split(X, Y, test_size=0.2) #データを分割
また、これらをtorch.tensor型にしておきます。
#float32のtensorに変換
train_X = torch.FloatTensor(train_X)
train_Y = torch.FloatTensor(train_Y)
test_X = torch.FloatTensor(test_X)
test_Y = torch.FloatTensor(test_Y)
続いて、訓練用データセットのデータローダーの作成も同じです。
from torch.utils.data import TensorDataset
from torch.utils.data import DataLoader
#訓練用データセットをまとめる
train = TensorDataset(train_X, train_Y)
BATCH_SIZE = 8
#訓練用データセットのDataloader
train_dataloader = DataLoader(
train,
batch_size=BATCH_SIZE,
shuffle=True
)
次に、モデルの構築です。注意すべきはモデルの出力層と損失関数です。ワンホットエンコーディングしたことにより、ラベルが要素数2の配列として表されているためそれに合わせて出力層のサイズを2にし、損失関数はクロスエントロピー誤差を使用する必要があります。クロスエントロピー誤差は多クラス分類を行うときに良く用いられる損失関数です。さらに、出力層には活性化関数が存在しません。これは、PyTorchの仕様でnn.CrossEntropyLossが活性化関数と同じ処理もするようになっているためです。 本来は多クラス分類における出力層の活性化関数としてはソフトマックス関数という関数を用います。
import torch
import torch.nn as nn
from torch.nn import functional as F
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.fc1 = nn.Linear(2, 8)
self.fc2 = nn.Linear(8, 2) #pytorchの仕様のため、出力層の活性化関数は省略
# 順伝播
def forward(self, x):
x = self.fc1(x)
x = F.relu(x)
x = self.fc2(x)
return x
# インスタンス化
net = Net()
# 損失関数の設定(クロスエントロピー誤差)
criterion = nn.CrossEntropyLoss() #この中でソフトマックス関数と同じ処理をしている
# 最適化手法は変更なし(SGD)
optimizer = torch.optim.SGD(net.parameters(), lr=0.1)
では、ここまでのまとめも兼ねて学習させます。
しっかり損失が下がっています。推論を行わせてみましょう。ここでも、モデルの出力値が二つであるということに注意します。ソフトマックス関数の出力は0から1までの実数ですので、確率として解釈できます。よってあるデータを入力したときの二つの出力の内、値が大きい方のクラスにそのデータが分類される確率が高いということです。
よって、以下では二つの出力値の内、値が大きい方のインデックスを取得してその値が0か1かでラベルを判断しています。
with torch.no_grad():# 試験用データでは勾配を計算しない
pred_labels = [] # 各バッチごとの結果格納用
for x in test_X:
pred = net(x)
if torch.argmax(pred) == torch.tensor(0) :
pred_labels.append([1.,0.])
else:
pred_labels.append([0.,1.])
pred_labels = np.array(pred_labels) #numpy arrayに変換
推論の結果は次のプログラムで描画します。
pred_array = []
#ワンホットエンコーディングしたラベルを一つの数字に戻す
for i in pred_labels:
if i[0] == 1:
pred_array.append(0)
else:
pred_array.append(1)
pred_array = np.array(pred_array)
X_red = test_X[pred_array == 0] #ラベルにより色分け
X_blue = test_X[pred_array == 1]
#描画
plt.scatter(X_red[:, 0], X_red[:, 1], color='red') #ラベルが0のデータ
plt.scatter(X_blue[:, 0], X_blue[:, 1], color='blue') #ラベルが1のデータ
では、test_Xを推論させ、その結果を描画してみましょう。
上手く分類することができています。
まとめ
今回は、sklearnの機能を用いて生成した2クラスデータセットの分類に挑戦しました。本格的に深層学習らしいことをやれるようになってきたのではないかと思います。データセットの分割、データローダーの作成、ワンホットエンコーディングなど非常に重要な要素も盛りだくさんでした。忘れないようによく復習しておきましょう。
TensorFlow チュートリアル Part3 を公開!
本記事の続編を公開しました!!犬、猫、鳥といった3クラスの分類をPart3では行っています。より複雑になったデータを分類するために必要なテクニックを公開しています。是非見ていただければと思います。
参考文献
- Eli stevens, Luca Antiga, Thomas Viehmann 『Pytorch実践入門 ディープラーニングの基礎から実装へ』後藤勇輝, 小川雄太郎, 櫻井亮佑, 大串和正 訳. 株式会社マイナビ. 2021
- PYTORCH DOCUMENTATION , https://pytorch.org/docs/stable/index.html