ニューラルネットワーク(深層学習)を用いた分類機は、非線形分離性能があることが知られています。
非線形の分離性能とは、データに対して直線ではない、分類境界を引くことができる性能のことです。
例えば、次のような渦巻き上のデータを分類しようとします。
この時、どんな直線を引いても、このデータをきれいに分割することはできませんよね。このようなデータを分類するときには、非線形の分類器を用いる必要性があります。
ニューラルネットワークは、構造の中に非線形な活性化関数を通すことで、非線形な分類が実行できることが知られています。今回はpytorchを用いて、深層学習によって非線形の分離がうまくできるか確認していきます。
非線形なデータセット
今回は、最初に提示した渦巻き上のデータを利用して、深層学習の非線形な分離性能を確認します。この渦巻き上のデータセットの作成方法は次の記事に解説しているので、今回はコードだけを提示します。
ここで、渦巻き上のデータセットを作成する前に、動かすためのコード全体で必要なライブラリを先にインポートします。
import numpy as np
from numpy import pi
import matplotlib.pyplot as plt
import pandas as pd
# pytorch library
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch import nn,optim
from torch.utils.data import DataLoader, TensorDataset, Dataset
from sklearn.model_selection import train_test_split
今回は、pytorchで実装していくので、pytorch関連ライブラリなどをインポートしていきます。
続いて、渦巻き上のデータを作成します。
N = 200
theta = np.sqrt(np.random.rand(N)) * pi
scaler = 10 * theta + pi
data1 = np.array([np.cos(theta)*scaler, np.sin(theta)*scaler]).T
c1 = data1 + np.random.randn(N,2)
data2 = np.array([np.cos(theta - 0.7 * pi) * scaler, np.sin(theta - 0.7 * pi) * scaler]).T
c2 = data2 + np.random.randn(N,2)
data3 = np.array([np.cos(theta - 1.4 * pi) * scaler, np.sin(theta - 1.4 * pi) * scaler]).T
c3 = data3 + np.random.randn(N,2)
res1 = np.append(c1, np.zeros((N,1)), axis=1)
res2 = np.append(c2, np.ones((N,1)), axis=1)
res3 = np.append(c3, np.ones((N,1), dtype="int8") * 2, axis=1)
plt.scatter(res1[:,0], res1[:,1], label="-")
plt.scatter(res2[:,0], res2[:,1], label="x")
plt.scatter(res3[:,0], res3[:,1], label="o")
plt.show()
res = np.append(res1, res2, axis=0)
res = np.append(res, res3, axis=0)
np.random.shuffle(res)
np.savetxt("result.csv", res, delimiter=",", header="x,y,label", comments="", fmt='%.5f')
df = pd.read_csv("result.csv")
これで、渦巻き上のデータが出来ました。df変数にDataFrameオブジェクトが格納されています。
このデータを深層学習でうまく分類することができるのか、確認していきます。
データセットの加工
続いて、用意した渦巻きデータセットを訓練データと検証データに分割し、pytorchで利用しやすいように、DataLoaderの形式に変換してみます。コードは次のようになります。
# 訓練データと検証データに分割
df_train, df_test = train_test_split(df, test_size=0.2, shuffle=False)
# 正解データを準備
train = df_train[["x", "y"]].values
train_labels = pd.get_dummies(df_train["label"]).values
test = df_test[["x", "y"]].values
test_labels = pd.get_dummies(df_test["label"]).values
batch_size = 4
train = torch.tensor(train, dtype=torch.float)
labels = torch.tensor(train_labels, dtype=torch.float)
dataset = torch.utils.data.TensorDataset(train, labels)
train_loader = DataLoader(dataset, batch_size=batch_size, shuffle=True)
test = torch.tensor(test, dtype=torch.float)
test_labels = torch.tensor(test_labels, dtype=torch.float)
test_dataset = torch.utils.data.TensorDataset(test, test_labels)
test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=True)
ここで、正解ラベルが0,1,2 とカテゴリ変数でふられているので、pandasのget_dummies関数を用いて、One-Hot-Encodingに変換しています。
この辺りの処理は、下記の記事で解説しています。
pytorchで深層学習モデルを実装
では早速、pytorchを用いて簡単な深層学習モデルを実装していきましょう。
今回利用する渦巻きデータセットは全部で3つのカテゴリに属しているため、出力層の次元は3次元で、損失関数には、交差エントロピー誤差を利用するのが良いでしょう。またデータセットの次元数は、2次元なので、入力層の次元は2次元に設定します。
今回は渦巻き状(Spiral)のデータセットの分類を行うので、SpiralNetというクラスで深層学習モデルを定義しました。
# モデルを定義
class SpiralNet(nn.Module):
def __init__(self, input_dim, output_dim):
# モデルの定義
super(SpiralNet, self).__init__()
self.input_dim = input_dim
self.output_dim = output_dim
self.fc1 = nn.Linear(input_dim, 20)
self.fc2 = nn.Linear(20, 40)
self.fc3 = nn.Linear(40, output_dim)
def forward(self, x):
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
x = self.fc3(x)
return x
input_dim = 2
ouput_dim = 3
net = SpiralNet(input_dim, ouput_dim)
入力層1層、中間層2層で、中間層の出力の後の活性化関数にはReLU関数を利用しました。
今回はこのReLU関数がポイントです。ReLU関数のような非線形関数を利用することで、深層学習は非線形の分類性能を得ることができます。
ここまででモデルの定義ができたので、続いて学習に進んでいきます。
学習パラメータを設定します。
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(net.parameters(), lr=1.0e-3)
history = { 'train_loss': [], 'test_loss': [], 'test_acc': [] }
device = torch.device("cuda:0" if torch.cuda. is_available() else "cpu")
epochs = 500
net.to(device)
続いて、学習をおこなっていきます。
for i in range(epochs+1):
# Model Training
net.train()
tmp_loss = 0.0
for batch_idx, (data, target) in enumerate(train_loader):
optimizer.zero_grad()
y = net(data)
loss = criterion(y, target)
loss.backward()
optimizer.step()
tmp_loss += loss.item()
tmp_loss /= batch_idx+1
history['train_loss'].append(tmp_loss)
if i % 10 == 0:
print('Epoch: ', i, ', Loss_Train:', tmp_loss)
# Model Test
net.eval() # または net.train(False) でも良い
test_loss = 0
correct_cnt = 0
with torch.no_grad():
for data, target in test_loader:
y = net(data)
loss = criterion(y, target)
test_loss += loss.item()
pred = y.argmax(dim=1, keepdim=True)
correct_cnt += pred.eq(target.argmax(dim=1, keepdim=True)).sum().item()
data_num = len(test_loader.dataset)
test_loss /= data_num
if i % 10 == 0:
print('Test loss (avg): {}, Accuracy: {}'.format(test_loss, correct_cnt / data_num))
history['test_loss'].append(test_loss)
history['test_acc'].append(correct_cnt / data_num)
今回はひとまず500epochで学習をし、同時にテストデータでの検証をおこなっていきます。
収束の状況を見ていきます。
plt.plot(range(len(history["train_loss"])), history["train_loss"], label='train loss')
plt.plot(range(len(history["test_loss"])), history["test_loss"], label='test loss')
plt.legend()
plt.xlabel("epochs")
plt.ylabel("loss")
plt.show()
うまく学習が収束していますね。検証データの正答率を見ていきましょう。
plt.plot(range(len(history["test_acc"])), history["test_acc"], label='test accuracy')
plt.legend()
plt.xlabel("epochs")
plt.ylabel("loss")
plt.show()
正答率100%まで上げることができました。学習に利用していない検証データでこの正答率はすごいですね。
では、ちゃんと非線形分離ができているか確認していきましょう。正答率が100%なので分離できていることは明白ですが、、。
分類器の分類傾向を確認するには、分類決定境界を見ることが有効です。
決定境界を可視化するコードを実装します。 格子状のデータを用意して、格子状の点に対して推論を行い、その推論結果を等高線として表示しています。
h = 0.1
x_min, x_max = df["x"].min() - .1, df["x"].max()+ .1
y_min, y_max = df["y"].min()- .1, df["y"].max()+ .1
xx, yy = np.meshgrid(np.arange(x_min, x_max, h), np.arange(y_min, y_max, h))
X = np.c_[xx.ravel(), yy.ravel()]
X = torch.tensor(X, dtype=torch.float)
score = net(X)
score = score.to('cpu').detach().numpy().copy()
predict_cls = np.argmax(score, axis=1)
Z = predict_cls.reshape(xx.shape)
plt.contourf(xx, yy, Z)
plt.axis('off')
plt.scatter(res1[:,0], res1[:,1], marker="^")
plt.scatter(res2[:,0], res2[:,1], marker="x")
plt.scatter(res3[:,0], res3[:,1], marker="o")
結果はこのようになりました。明かに非線形な分類ができていますね!すごい!
非線形な活性化関数を利用しない場合で検証
先ほど、ReLUなどの活性化関数が非線形な分類を行う上で重要だと述べました。
試しに実験として、ReLU関数を利用しないモデルを実装して同様の決定境界を見てみましょう。
コード自体は次のように定義します。
# モデルを定義
class SpiralNet2(nn.Module):
def __init__(self, input_dim, output_dim):
# モデルの定義
super(SpiralNet2, self).__init__()
self.input_dim = input_dim
self.output_dim = output_dim
self.fc1 = nn.Linear(input_dim, 20)
self.fc2 = nn.Linear(20, 40)
self.fc3 = nn.Linear(40, output_dim)
def forward(self, x):
x = self.fc1(x)
x = self.fc2(x)
x = self.fc3(x)
return x
同様にモデルを学習して決定境界を描くとこのようになりました。
先ほどとはうって変わって、非線形な分離ができていないことがわかりますね。
この実験を通して、非線形な活性化関数が深層学習の学習に非常に重要な意味を持つことがわかります。