筆者:川嶋 宏彰
本連載では、機械学習の基礎となるさまざまな手法の仕組みや、それらの手法のPythonでの利用方法を解説していきます。最終回となる今回は、ニューラルネットの仕組みと、基本的なモデルについて解説します。
シェルスクリプトマガジン Vol.77は以下のリンク先でご購入できます。
図3 単純パーセプトロンを学習するための関数を定義するPythonコード
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 |
import numpy as np def h_step(x): """ ステップ関数: 0より大きければ1、それ以外は0 """ return int(x > 0) def train_perceptron(X, y, lr=0.5, max_it=20, random_state=None): """ パーセプトロンの学習アルゴリズム lr: 学習率 (learning rate) max_it: 最大反復回数 (maximum number of iterations) """ N, d = X.shape # データ数x次元 X1 = np.c_[np.ones(N), X] # 1だけの列を1列目に追加 d1 = d + 1 # バイアス項込みの次元数 np.random.seed(random_state) w = np.random.rand(d1) # (w0, w1, w2) print('initial w', w) w_log = [w.copy()] # 初期の重み係数 info_log = [[-1] * 5] # [it, i, y, o, y-o] for it in range(max_it): # ① 反復 (iteration) print('--- iteration:', it) err = 0 for i in range(N): # ② 各データを入力 x = X1[i, :] y_pred = h_step(np.dot(w, x)) err += (y[i] - y_pred) ** 2 print('yhat:', y_pred, 'y:', y[i]) if y_pred != y[i]: w += lr * (y[i] - y_pred) * x # ③ wを更新 w_log.append(w.copy()) info_log.append([it, i, y[i], y_pred, y[i] - y_pred]) print('err:', err) if err == 0: print('## converged @ it=', it) break return w_log, info_log def get_fourpoints(xor_flag=False): """4点のデータを準備 xor_flag: 各データのラベルをどう設定するか True: 入力が異なる符号なら1, False: 入力が共に正なら1 """ X = np.array([[-1, -1], [-1, 1], [1, -1], [1, 1]]) y = np.array([0, 1, 1, 0]) if xor_flag else np.array([0, 0, 0, 1]) return X, y |
図4 単純パーセプトロンの学習過程を可視化する関数を定義するPythonコード
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 |
import matplotlib.pyplot as plt import seaborn as sns plt.rcParams['font.size'] = 14 def plot_2dline_with_data(X, y, w, title='', i=-1): """ データと w0 + w1 x1 + w2 x2 = 0 をプロット """ fig = plt.figure(figsize=(5, 5)) sns.scatterplot(x=X[:, 0], y=X[:, 1], hue=y, s=150) xlim = [-1.5, 1.5] ylim = [-1.5, 1.5] plt.xlim(xlim) plt.ylim(ylim) plt.xticks([-1, 1]) plt.yticks([-1, 1]) # w0 + w1*x1 + w2*x2 = 0 のプロット if w[2] == 0: # w2 == 0 のとき: x1 = -w0/w1 x2 = np.linspace(*ylim, 2) plt.plot([-w[0]/w[1]] * x2.size, x2, '-', linewidth=3, color='r') else: # w2 != 0 のとき: x2 = -(w0 + w1*x1)/w2 x1 = np.linspace(*xlim, 2) plt.plot(x1, -(w[0] + w[1]*x1)/w[2], '-', linewidth=3, color='r') if i >= 0: plt.scatter(X[i, 0], X[i, 1], s=300, facecolor='none', edgecolor='r', linewidth=2) plt.title(title) return fig |
図5 単純パーセプトロンの学習と結果表示をするPythonコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
# データの読み込みと学習 xor_flag = False # (★) True: XOR, False: AND X, y = get_fourpoints(xor_flag) # 学習データを取得 print(f' X:\n{X},\n y: {y}') w_log, info_log = train_perceptron(X, y, random_state=0) # 学習過程の表示 for step in range(len(w_log)): title = 'it: {}, i: {}, y: {}, y_pred: {}, y - y_pred:{}'\ .format(*info_log[step]) print(title) w = w_log[step] it = info_log[step][0] i = info_log[step][1] print('w:', w) plot_flag = False if (it >= 3) and (it % 5 != 0) else True if plot_flag: plot_2dline_with_data(X, y, w, title, i) plt.show() |
図12 決定境界を可視化するための関数を定義するPythonコード
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 |
import seaborn as sns import matplotlib as mpl def plot_decision_boundary(X, y, clf, xylabels=None, palette=None, fig=None, ngrid=50): """ 分類器 clf の決定境界を描画 """ if fig is None: fig = plt.figure(figsize=(5, 5)) else: plt.figure(fig.number) # 2次元空間にグリッド点を準備 xmin = X.min(axis=0) # 各列の最小値 xmax = X.max(axis=0) # 各列の最大値 xstep = [(xmax[j]-xmin[j]) / ngrid for j in range(2)] # グリッドのステップ幅 xmin = [xmin[j] - 8*xstep[j] for j in range(2)] # 少し広めに xmax = [xmax[j] + 8*xstep[j] for j in range(2)] aranges = [np.arange(xmin[j], xmax[j] + xstep[j], xstep[j]) for j in range(2)] x0grid, x1grid = np.meshgrid(*aranges) # 各グリッド点でクラスを判定 y_pred = clf.predict(np.c_[x0grid.ravel(), x1grid.ravel()]) y_pred = y_pred.reshape(x0grid.shape) # 2次元へ y_pred = np.searchsorted(np.unique(y_pred), y_pred) # 値をindexへ clist = palette.values() if type(palette) is dict else palette cmap = mpl.colors.ListedColormap(clist) if palette is not None else None plt.contourf(x0grid, x1grid, y_pred, alpha=0.3, cmap=cmap) sns.scatterplot(x=X[:, 0], y=X[:, 1], hue=y, palette=palette) plt.legend() plt.xlim([xmin[0], xmax[0]]) plt.ylim([xmin[1], xmax[1]]) if xylabels is not None: plt.xlabel(xylabels[0]) plt.ylabel(xylabels[1]) return fig |
図13 多層パーセプトロンによる学習をするPythonコード
1 2 3 4 5 6 7 8 9 10 |
from sklearn.neural_network import MLPClassifier clf = MLPClassifier(hidden_layer_sizes=3, learning_rate_init=0.05, random_state=0) # XORデータの準備 xor_flag = True # True: XOR, False: AND X, y = get_fourpoints(xor_flag) # 学習データを取得 print(f' X:\n{X},\n y: {y}') clf.fit(X, y) # 学習(誤差逆伝播法)の実行 plot_decision_boundary(X, y, clf) # 決定境界 plt.show() |
図15 学習曲線や結合荷重などを表示するPythonコード
1 2 3 4 5 6 7 8 9 10 11 12 |
# 学習曲線の表示 plt.plot(range(1, clf.n_iter_ + 1), clf.loss_curve_) plt.xlabel('Epoch') plt.ylabel('Loss') plt.ylim(0,) plt.show() # 構造を確認 print('n_outputs:', clf.n_outputs_) # 出力層の素子数 print('n_layers (with input layer):', clf.n_layers_) # 入力層を含めた層の数 # 結合荷重 print('coefs:\n', clf.coefs_) # バイアス項以外の結合荷重(行列のリスト) print('intercepts:\n', clf.intercepts_) # バイアス項(ベクトルのリスト) |
図17 Breast Cancerデータセットの準備をするPythonコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
from sklearn.datasets import load_breast_cancer from sklearn.model_selection import train_test_split from sklearn.metrics import roc_curve, roc_auc_score from sklearn.preprocessing import StandardScaler data = load_breast_cancer() # データセット読み込み X_orig = data['data'] y = 1 - data['target'] # 悪性を1、良性を0とする正解ラベル X = X_orig[:, :10] # 一部の特徴量を利用する # 訓練データとテストデータに2分割する(検証データは今回は切り分けない) X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=1) # ROCカーブの表示用に関数を準備 def plot_roc(fpr, tpr, marker=None): plt.figure(figsize=(5, 5)) plt.plot(fpr, tpr, marker=marker) plt.xlabel('False Positive Rate') plt.ylabel('True Positive Rate') plt.grid() |
図18 Breast Cancerデータセットによる学習と評価をするPythonコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
clf = MLPClassifier(hidden_layer_sizes=(5, 3), learning_rate_init=0.005, random_state=1) with_scaling = False # (★)標準化するか否か if with_scaling: # 標準化によるスケーリングあり scaler = StandardScaler().fit(X_train) # 訓練データで平均と標準偏差を求める Xscaled_train = scaler.transform(X_train) # X_train -> Xscaled_train Xscaled_test = scaler.transform(X_test) # X_test -> Xscaled_test clf.fit(Xscaled_train, y_train) # 学習 y_proba_train = clf.predict_proba(Xscaled_train) # 訓練データで事後確率予測 y_proba_test = clf.predict_proba(Xscaled_test) # テストデータ事後確率予測 else: # 標準化なし(そのまま) clf.fit(X_train, y_train) # 学習 y_proba_train = clf.predict_proba(X_train) # 訓練データで事後確率予測 y_proba_test = clf.predict_proba(X_test) # テストデータ事後確率予測 # テストデータに対するROCカーブ fpr, tpr, threshold = roc_curve(y_test, y_proba_test[:, 1]) plot_roc(fpr, tpr) plt.show() # 訓練・テストデータのAUC(Area Under the Curve)スコア print('AUC (train):', roc_auc_score(y_train, y_proba_train[:, 1])) print('AUC (test):', roc_auc_score(y_test, y_proba_test[:, 1])) |
図21 PyTorchによる画像認識の準備をするPythonコード
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 |
import torch import torch.nn as nn import torch.nn.functional as F import torchvision from torchvision import datasets from torchvision import transforms as transforms import matplotlib.pyplot as plt import numpy as np # GPUが使えるか否かでデータの転送先を指定 device = 'cuda:0' if torch.cuda.is_available() else 'cpu' img_mean, img_std = (0.1307, 0.3081) # 画素値の標準化用 # 標準化などの前処理を設定 trans = transforms.Compose([ transforms.ToTensor(), transforms.Normalize((img_mean,), (img_std,)) ]) # データセットのダウンロード rootdir = './data' # ダウンロード先 train_dataset = datasets.MNIST(root=rootdir, train=True, transform=trans, download=True) test_dataset = datasets.MNIST(root=rootdir, train=False, transform=trans, download=True) # データを読み込む専用のクラスDataLoaderを準備 batch_size = 32 train_loader = torch.utils.data.DataLoader( dataset=train_dataset, batch_size=batch_size, shuffle=True) test_loader = torch.utils.data.DataLoader( dataset=test_dataset, batch_size=batch_size, shuffle=False) # クラス情報 num_classes = len(train_dataset.classes) cls_names = [f'{i}' for i in range (num_classes)] # 画像表示用の関数を定義しておく def tensor_imshow(img, title=None): """ Tensorを画像として表示 """ img = img.numpy().transpose((1, 2, 0)) # (C,H,W)->(H,W,C) img = img_std * img + img_mean plt.imshow(np.clip(img, 0, 1)) if title is not None: plt.title(title) plt.show() def test_with_examples(model, loader): """ loaderのミニバッチの画像を予測結果と共にグリッド表示 model: 予測に用いるニューラルネット loader: DataLoader """ model.eval() # 推論モード with torch.no_grad(): # 推論のみ(勾配計算なし) imgs, labels = next(iter(loader)) # ミニバッチ取得 imgs_in = imgs.view(-1, imgs.shape[2]*imgs.shape[3]) outputs = model(imgs_in.to(device)) # 順伝播による推論 _, pred = torch.max(outputs, 1) # 予測ラベル grid = torchvision.utils.make_grid(imgs) # グリッド画像生成 title_str1 = '\n'.join( [', '.join([cls_names[y] for y in x]) for x in pred.view(-1, 8).tolist()]) title_str2 = '\n'.join( [', '.join([cls_names[y] for y in x]) for x in labels.view(-1, 8).tolist()]) tensor_imshow(grid, title='Predicted classes:\n' + f'{title_str1}\n\nTrue classes:\n{title_str2}\n') # 訓練データ例の表示 print('\n--- Training data (example) ---\n') imgs, labels = next(iter(train_loader)) grid = torchvision.utils.make_grid(imgs) title_str = '\n'.join([', '.join([cls_names[y] for y in x]) for x in labels.view(-1, 8).tolist()]) tensor_imshow(grid, title=f'True classes:\n{title_str}\n') # ニューラルネットの構造を定義 class Net(nn.Module): def __init__(self): super(Net, self).__init__() self.fc1 = nn.Linear(28*28, 512) self.fc2 = nn.Linear(512, 256) self.fc3 = nn.Linear(256, num_classes) def forward(self, x): x = F.relu(self.fc1(x)) x = F.relu(self.fc2(x)) x = self.fc3(x) # 活性化関数なし(損失関数側でsoftmax) return x net = Net() # インスタンス生成 net = net.to(device) # モデルをGPUへ転送(もしGPUがあれば) # 学習前にテストデータを入力してみる print('\n--- Result BEFORE training ---\n') test_with_examples(net, test_loader) |
図22 PyTorchによるニューラルネットの学習と結果表示をするPythonコード
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 |
# 損失関数(softmax + cross entropy) criterion = nn.CrossEntropyLoss() # 最適化手法の選択 optimizer = torch.optim.SGD(net.parameters(), lr=0.01) # 学習のループ num_epochs = 5 train_loss_log = [] train_acc_log = [] for epoch in range(num_epochs): print(f'Epoch {epoch + 1}/{num_epochs}\n', '-' * 10) # 訓練フェーズ sum_loss, sum_ok = [0.0, 0] for inputs, labels in train_loader: # ミニバッチ入力 inputs = inputs.view(-1, 28*28).to(device) labels = labels.to(device) # deviceに転送 outputs = net(inputs) # 順伝播 loss = criterion(outputs, labels) # 損失計算 optimizer.zero_grad() # 勾配をクリア loss.backward() # 誤差逆伝播 optimizer.step() # パラメータ更新 # ミニバッチの評価値(1バッチ分)を計算 _, preds = torch.max(outputs, 1) # 予測ラベル sum_loss += loss.item() * inputs.size(0) # 損失 sum_ok += torch.sum(preds == labels.data) # 正解数 # 1エポック分の評価値を計算 e_loss = sum_loss / len(train_dataset) # 平均損失 e_acc = sum_ok.cpu().double() / len(train_dataset) # 正解率 print(f'Loss: {e_loss:.4f} Acc: {e_acc:.4f}') train_loss_log.append(e_loss) train_acc_log.append(e_acc) # 学習曲線をプロット fig = plt.figure(figsize=(12, 6)) fig.add_subplot(121) # 損失 plt.plot(range(1, num_epochs + 1), train_loss_log, '.-') plt.title('Training data') plt.xlabel('Epoch') plt.ylabel('Loss') plt.ylim(0) fig.add_subplot(122) # 正解率 plt.plot(range(1, num_epochs + 1), train_acc_log, '.-') plt.title('Training data') plt.xlabel('Epoch') plt.ylabel('Accuracy') plt.show() # テストデータを入力してみる print('\n--- Result AFTER training ---\n') test_with_examples(net, test_loader) |