前回の投稿では、分類問題を定式化して 3 つのタイプ (バイナリ、マルチクラス、マルチラベル) に分割し、「バイナリ分類タスクを解決するにはどのような活性化関数と損失関数を使用する必要がありますか?」という質問に答えました。
この投稿では、マルチクラス分類タスクを除いて同じ質問に答え、次の情報を提供します。
マルチクラス分類タスクを解決するには、どのような活性化関数と損失関数を使用する必要がありますか?
バイナリ分類からマルチクラスに切り替えるには、コードと NN にほとんど変更を加える必要がないため、提供されるコードは主にバイナリ分類の実装に基づいています。変更されたコード ブロックには、ナビゲーションが容易になるように(変更) のマークが付いています。
後で示すように、マルチクラス分類に使用される活性化関数はソフトマックス活性化です。 Softmax は、マルチクラス分類以外のさまざまな NN アーキテクチャで広く使用されています。たとえば、softmax は、入力値を確率分布に変換する機能により、Transformer モデルで使用されるマルチヘッド アテンション ブロックの中核となります ( 「アテンションだけで十分です」を参照)。
マルチクラス分類問題を解決するためにソフトマックス アクティベーションと CE 損失を適用する背後にある動機を知っていれば、より複雑な NN アーキテクチャと損失関数を理解して実装できるようになります。
多クラス分類問題は、サンプルのセット{(x_1, y_1), (x_2, y_2),...,(x_n, y_n)}として表現できます。ここで、 x_iはサンプルの特徴を含む m 次元ベクトルです。 iおよびy_i は、 x_i が属するクラスです。ここで、ラベルy_i はk値のいずれかを想定できます。ここで、k は 2 より大きいクラスの数です。目標は、各入力サンプルx_iのラベル y_i を予測するモデルを構築することです。
マルチクラス分類問題として扱うことができるタスクの例:
マルチクラス分類では次のようになります。
サンプルのセット{(x_1, y_1), (x_2, y_2),...,(x_n, y_n)}
x_iは、サンプルiの特徴を含む m 次元ベクトルです。
y_iは、 x_i が属するクラスであり、 k値の 1 つを取ることができます。ここで、 k>2はクラスの数です。
確率的分類器としてマルチクラス分類ニューラル ネットワークを構築するには、次のものが必要です。
ニューラル ネットワークの最後の線形層は、「生の出力値」のベクトルを出力します。分類の場合、出力値は、入力がkクラスのいずれかに属するというモデルの信頼度を表します。前に説明したように、出力層はサイズkを持つ必要があり、出力値は k クラスのそれぞれの確率p_iおよびSUM(p_i)=1を表す必要があります。
二項分類に関する記事では、シグモイド活性化を使用して NN 出力値を確率に変換します。 [-3, 3] の範囲のk出力値にシグモイドを適用して、シグモイドが前述の要件を満たしているかどうかを確認してみましょう。
k の出力値は (0,1) の範囲内である必要があります。ここで、 kはクラスの数です。
k 個の出力値の合計は 1 に等しくなければなりません
前の記事では、シグモイド関数が入力値を範囲 (0,1) にマップすることを示しました。シグモイドのアクティベーションが 2 番目の要件を満たすかどうかを見てみましょう。以下のテーブル例では、サイズk (k=7) のベクトルをシグモイド アクティベーションで処理し、これらすべての値を合計します。これら 7 つの値の合計は 3.5 に等しくなります。これを修正する簡単な方法は、すべてのk値をその合計で除算することです。
入力 | -3 | -2 | -1 | 0 | 1 | 2 | 3 | 和 |
---|---|---|---|---|---|---|---|---|
シグモイド出力 | 0.04743 | 0.11920 | 0.26894 | 0.50000 | 0.73106 | 0.88080 | 0.95257 | 3.5000 |
別の方法は、入力値の指数を取得し、それをすべての入力値の指数の合計で割ることです。
Softmax 関数は、実数のベクトルを確率のベクトルに変換します。結果の各確率は (0,1) の範囲内にあり、確率の合計は 1 になります。
入力 | -3 | -2 | -1 | 0 | 1 | 2 | 3 | 和 |
---|---|---|---|---|---|---|---|---|
ソフトマックス | 0.00157 | 0.00426 | 0.01159 | 0.03150 | 0.08563 | 0.23276 | 0.63270 | 1 |
Softmax を使用するときに注意する必要があることが 1 つあります。出力値p_i は、すべての値の指数の合計で除算するため、入力配列内のすべての値に依存します。以下の表はこれを示しています。2 つの入力ベクトルには 3 つの共通値 {1, 3, 4} がありますが、2 番目の要素 (2 と 4) が異なるため、出力ソフトマックス値は異なります。
入力1 | 1 | 2 | 3 | 4 |
---|---|---|---|---|
ソフトマックス1 | 0.0321 | 0.0871 | 0.2369 | 0.6439 |
入力2 | 1 | 4 | 3 | 4 |
ソフトマックス2 | 0.0206 | 0.4136 | 0.1522 | 0.4136 |
バイナリ相互エントロピー損失は次のように定義されます。
バイナリ分類では、2 つの出力確率p_iおよび(1-p_i)とグランド トゥルース値y_iおよび(1-y_i) があります。
マルチクラス分類問題では、N クラスの BCE 損失、つまりクロスエントロピー損失の一般化が使用されます。
N は入力サンプルの数、 y_iはグラウンド トゥルース、 p_iはクラスiの予測確率です。
確率的マルチクラス分類 NN を実装するには、以下が必要です。
コードのほとんどの部分は、バイナリ分類に関する前の記事のコードに基づいています。
変更された部分には(変更) のマークが付けられます。
PyTorch フレームワークを使用して、マルチクラス分類用のニューラル ネットワークをコーディングしてみましょう。
まず、インストールします
# used for accuracy metric and confusion matrix !pip install torchmetrics
コードの後半で使用されるパッケージをインポートします
from sklearn.datasets import make_classification import numpy as np import torch import torchmetrics import matplotlib.pyplot as plt import seaborn as sn import pandas as pd from sklearn.decomposition import PCA
グローバル変数にクラスの数を設定します (2 に設定し、ソフトマックスとクロスエントロピー損失を使用するバイナリ分類 NN を取得する場合)
number_of_classes=4
私が使用します
n_samples - 生成されたサンプルの数です
n_features - 生成されたサンプル X の次元数を設定します。
n_classes - 生成されたデータセット内のクラスの数。マルチクラス分類問題では、2 つ以上のクラスが必要です
生成されたデータセットには、形状[n_samples, n_features]の X と形状[n_samples, ]の Y が含まれます。
def get_dataset(n_samples=10000, n_features=20, n_classes=2): # https://scikit-learn.org/stable/modules/generated/sklearn.datasets.make_classification.html#sklearn.datasets.make_classification data_X, data_y = make_classification(n_samples=n_samples, n_features=n_features, n_classes=n_classes, n_informative=n_classes, n_redundant=0, n_clusters_per_class=2, random_state=42, class_sep=4) return data_X, data_y
データセット統計を視覚化して出力する関数を定義します。 show_dataset 関数は使用します
def print_dataset(X, y): print(f'X shape: {X.shape}, min: {X.min()}, max: {X.max()}') print(f'y shape: {y.shape}') print(y[:10]) def show_dataset(X, y, title=''): if X.shape[1] > 2: X_pca = PCA(n_components=2).fit_transform(X) else: X_pca = X fig = plt.figure(figsize=(4, 4)) plt.scatter(x=X_pca[:, 0], y=X_pca[:, 1], c=y, alpha=0.5) # generate colors for all classes colors = plt.cm.rainbow(np.linspace(0, 1, number_of_classes)) # iterate over classes and visualize them with the dedicated color for class_id in range(number_of_classes): class_mask = np.argwhere(y == class_id) X_class = X_pca[class_mask[:, 0]] plt.scatter(x=X_class[:, 0], y=X_class[:, 1], c=np.full((X_class[:, 0].shape[0], 4), colors[class_id]), label=class_id, alpha=0.5) plt.title(title) plt.legend(loc="best", title="Classes") plt.xticks() plt.yticks() plt.show()
min max スケーラーを使用して、データセット フィーチャ X を範囲 [0,1] にスケールします。これは通常、トレーニングをより速く、より安定させるために行われます。
def scale(x_in): return (x_in - x_in.min(axis=0))/(x_in.max(axis=0)-x_in.min(axis=0))
生成されたデータセット統計を出力し、上記の関数を使用して視覚化してみましょう。
X, y = get_dataset(n_classes=number_of_classes) print('before scaling') print_dataset(X, y) show_dataset(X, y, 'before') X_scaled = scale(X) print('after scaling') print_dataset(X_scaled, y) show_dataset(X_scaled, y, 'after')
得られる出力は以下のとおりです。
before scaling X shape: (10000, 20), min: -9.549551632357336, max: 9.727761741276673 y shape: (10000,) [0 2 1 2 0 2 0 1 1 2]
after scaling X shape: (10000, 20), min: 0.0, max: 1.0 y shape: (10000,) [0 2 1 2 0 2 0 1 1 2]
最小-最大スケーリングはデータセットの特徴を歪めず、データセットの特徴を [0,1] の範囲に線形変換します。 「最小-最大スケーリング後のデータセット」の図は、PCA アルゴリズムによって 20 次元が 2 に削減され、PCA アルゴリズムが最小-最大スケーリングの影響を受ける可能性があるため、前の図と比較して歪んでいるように見えます。
PyTorch データローダーを作成します。
def get_data_loaders(dataset, batch_size=32, shuffle=True): data_X, data_y = dataset # https://pytorch.org/docs/stable/data.html#torch.utils.data.TensorDataset torch_dataset = torch.utils.data.TensorDataset(torch.tensor(data_X, dtype=torch.float32), torch.tensor(data_y, dtype=torch.float32)) # https://pytorch.org/docs/stable/data.html#torch.utils.data.random_split train_dataset, val_dataset = torch.utils.data.random_split(torch_dataset, [int(len(torch_dataset)*0.8), int(len(torch_dataset)*0.2)], torch.Generator().manual_seed(42)) # https://pytorch.org/docs/stable/data.html#torch.utils.data.DataLoader loader_train = torch.utils.data.DataLoader(train_dataset, batch_size=batch_size, shuffle=shuffle) loader_val = torch.utils.data.DataLoader(val_dataset, batch_size=batch_size, shuffle=shuffle) return loader_train, loader_val
PyTorch データローダーをテストする
dataloader_train, dataloader_val = get_data_loaders(get_dataset(n_classes=number_of_classes), batch_size=32) train_batch_0 = next(iter(dataloader_train)) print(f'Batches in the train dataloader: {len(dataloader_train)}, X: {train_batch_0[0].shape}, Y: {train_batch_0[1].shape}') val_batch_0 = next(iter(dataloader_val)) print(f'Batches in the validation dataloader: {len(dataloader_val)}, X: {val_batch_0[0].shape}, Y: {val_batch_0[1].shape}')
出力:
Batches in the train dataloader: 250, X: torch.Size([32, 20]), Y: torch.Size([32]) Batches in the validation dataloader: 63, X: torch.Size([32, 20]), Y: torch.Size([32])
前処理関数と後処理関数を作成します。前に気づいたかもしれませんが、現在の Y 形状は [N] なので、[N,number_of_classes] にする必要があります。これを行うには、Y ベクトルの値をワンホット エンコードする必要があります。
ワンホット エンコーディングは、クラス インデックスをバイナリ表現に変換するプロセスであり、各クラスは一意のバイナリ ベクトルで表されます。
言い換えると、サイズ [number_of_classes] のゼロ ベクトルを作成し、位置 class_id の要素を 1 に設定します。ここで、class_id は {0,1,…,number_of_classes-1} です。
0>>[1. 0.0.0.]
1>>[0. 1.0.0.】
2>>[0. 0.1.0.]
2>>[0. 0.0.1.]
Pytorch テンソルは torch.nn.function.one_hot で処理でき、numpy の実装は非常に簡単です。出力ベクトルの形状は [N,number_of_classes] になります。
def preprocessing(y, n_classes): ''' one-hot encoding for input numpy array or pytorch Tensor input: y - [N,] numpy array or pytorch Tensor output: [N, n_classes] the same type as input ''' assert type(y)==np.ndarray or torch.is_tensor(y), f'input should be numpy array or torch tensor. Received input is: {type(categorical)}' assert len(y.shape)==1, f'input shape should be [N,]. Received input shape is: {y.shape}' if torch.is_tensor(y): return torch.nn.functional.one_hot(y, num_classes=n_classes) else: categorical = np.zeros([y.shape[0], n_classes]) categorical[np.arange(y.shape[0]), y]=1 return categorical
ワンホット エンコードされたベクトルをクラス ID に変換するには、ワンホット エンコードされたベクトル内の最大要素のインデックスを見つける必要があります。これは、以下の torch.argmax または np.argmax を使用して実行できます。
def postprocessing(categorical): ''' one-hot to classes decoding with .argmax() input: categorical - [N,classes] numpy array or pytorch Tensor output: [N,] the same type as input ''' assert type(categorical)==np.ndarray or torch.is_tensor(categorical), f'input should be numpy array or torch tensor. Received input is: {type(categorical)}' assert len(categorical.shape)==2, f'input shape should be [N,classes]. Received input shape is: {categorical.shape}' if torch.is_tensor(categorical): return torch.argmax(categorical,dim=1) else: return np.argmax(categorical, axis=1)
定義された前処理関数と後処理関数をテストします。
y = get_dataset(n_classes=number_of_classes)[1] y_logits = preprocessing(y, n_classes=number_of_classes) y_class = postprocessing(y_logits) print(f'y shape: {y.shape}, y preprocessed shape: {y_logits.shape}, y postprocessed shape: {y_class.shape}') print('Preprocessing does one-hot encoding of class ids.') print('Postprocessing does one-hot decoding of class one-hot encoded class ids.') for i in range(10): print(f'{y[i]} >> {y_logits[i]} >> {y_class[i]}')
出力:
y shape: (10000,), y preprocessed shape: (10000, 4), y postprocessed shape: (10000,) Preprocessing does one-hot encoding of class ids. Postprocessing does one-hot decoding of one-hot encoded class ids. id>>one-hot encoding>>id 0 >> [1. 0. 0. 0.] >> 0 2 >> [0. 0. 1. 0.] >> 2 1 >> [0. 1. 0. 0.] >> 1 2 >> [0. 0. 1. 0.] >> 2 0 >> [1. 0. 0. 0.] >> 0 2 >> [0. 0. 1. 0.] >> 2 0 >> [1. 0. 0. 0.] >> 0 1 >> [0. 1. 0. 0.] >> 1 1 >> [0. 1. 0. 0.] >> 1 2 >> [0. 0. 1. 0.] >> 2
このセクションでは、二項分類モデルをトレーニングするために必要なすべての関数の実装を示します。
PyTorch ベースのソフトマックス公式の実装
def softmax(x): assert len(x.shape)==2, f'input shape should be [N,classes]. Received input shape is: {x.shape}' # Subtract the maximum value for numerical stability # you can find explanation here: https://www.deeplearningbook.org/contents/numerical.html x = x - torch.max(x, dim=1, keepdim=True)[0] # Exponentiate the values exp_x = torch.exp(x) # Sum along the specified dimension sum_exp_x = torch.sum(exp_x, dim=1, keepdim=True) # Compute the softmax return exp_x / sum_exp_x
ソフトマックスをテストしてみましょう:
ステップ 1 で [-10, 11] の範囲でtest_input numpy 配列を生成します
それを、shape [7,3] のテンソルに再形成します。
実装されたSoftmax関数と PyTorch のデフォルト実装torch.nn.Functional.softmaxを使用してtest_inputを処理します。
結果を比較します(結果は同一である必要があります)
7 つすべての [1,3] テンソルのソフトマックス値と合計を出力します。
test_input = torch.arange(-10, 11, 1, dtype=torch.float32) test_input = test_input.reshape(-1,3) softmax_output = softmax(test_input) print(f'Input data shape: {test_input.shape}') print(f'input data range: [{test_input.min():.3f}, {test_input.max():.3f}]') print(f'softmax output data range: [{softmax_output.min():.3f}, {softmax_output.max():.3f}]') print(f'softmax output data sum along axis 1: [{softmax_output.sum(axis=1).numpy()}]') softmax_output_pytorch = torch.nn.functional.softmax(test_input, dim=1) print(f'softmax output is the same with pytorch implementation: {(softmax_output_pytorch==softmax_output).all().numpy()}') print('Softmax activation changes values in the chosen axis (1) so that they always sum up to 1:') for i in range(softmax_output.shape[0]): print(f'\t{i}. Sum before softmax: {test_input[i].sum().numpy()} | Sum after softmax: {softmax_output[i].sum().numpy()}') print(f'\t values before softmax: {test_input[i].numpy()}, softmax output values: {softmax_output[i].numpy()}')
出力:
Input data shape: torch.Size([7, 3]) input data range: [-10.000, 10.000] softmax output data range: [0.090, 0.665] softmax output data sum along axis 1: [[1. 1. 1. 1. 1. 1. 1.]] softmax output is the same with pytorch implementation: True Softmax activation changes values in the chosen axis (1) so that they always sum up to 1: 0. Sum before softmax: -27.0 | Sum after softmax: 1.0 values before softmax: [-10. -9. -8.], softmax output values: [0.09003057 0.24472848 0.66524094] 1. Sum before softmax: -18.0 | Sum after softmax: 1.0 values before softmax: [-7. -6. -5.], softmax output values: [0.09003057 0.24472848 0.66524094] 2. Sum before softmax: -9.0 | Sum after softmax: 1.0 values before softmax: [-4. -3. -2.], softmax output values: [0.09003057 0.24472848 0.66524094] 3. Sum before softmax: 0.0 | Sum after softmax: 1.0 values before softmax: [-1. 0. 1.], softmax output values: [0.09003057 0.24472848 0.66524094] 4. Sum before softmax: 9.0 | Sum after softmax: 1.0 values before softmax: [2. 3. 4.], softmax output values: [0.09003057 0.24472848 0.66524094] 5. Sum before softmax: 18.0 | Sum after softmax: 1.0 values before softmax: [5. 6. 7.], softmax output values: [0.09003057 0.24472848 0.66524094] 6. Sum before softmax: 27.0 | Sum after softmax: 1.0 values before softmax: [ 8. 9. 10.], softmax output values: [0.09003057 0.24472848 0.66524094]
PyTorch ベースの CE 式の実装
def cross_entropy_loss(softmax_logits, labels): # Calculate the cross-entropy loss loss = -torch.sum(labels * torch.log(softmax_logits)) / softmax_logits.size(0) return loss
CE 実装のテスト:
形状 [10,5] と範囲 [0,1) のtest_input配列を生成します。
形状 [10,] と範囲 [0,4] の値を持つtest_target配列を生成します。
ワンホット エンコードtest_target配列
実装されたcross_entropy関数とPyTorch実装を使用して損失を計算する
結果を比較します (結果は同一である必要があります)
test_input = torch.rand(10, 5, requires_grad=False) test_target = torch.randint(0, 5, (10,), requires_grad=False) test_target = preprocessing(test_target, n_classes=5).float() print(f'test_input shape: {list(test_input.shape)}, test_target shape: {list(test_target.shape)}') # get loss with the cross_entropy_loss implementation loss = cross_entropy_loss(softmax(test_input), test_target) # get loss with the torch.nn.functional.cross_entropy implementation # !!!torch.nn.functional.cross_entropy applies softmax on input logits # !!!pass it test_input without softmax activation loss_pytorch = torch.nn.functional.cross_entropy(test_input, test_target) print(f'Loss outputs are the same: {(loss==loss_pytorch).numpy()}')
期待される出力:
test_input shape: [10, 5], test_target shape: [10, 5] Loss outputs are the same: True
私が使用します
マルチクラス分類精度メトリックを作成するには、次の 2 つのパラメーターが必要です。
タスクタイプ「マルチクラス」
クラス数 num_classes
# https://torchmetrics.readthedocs.io/en/stable/classification/accuracy.html#module-interface accuracy_metric=torchmetrics.classification.Accuracy(task="multiclass", num_classes=number_of_classes) def compute_accuracy(y_pred, y): assert len(y_pred.shape)==2 and y_pred.shape[1] == number_of_classes, 'y_pred shape should be [N, C]' assert len(y.shape)==2 and y.shape[1] == number_of_classes, 'y shape should be [N, C]' return accuracy_metric(postprocessing(y_pred), postprocessing(y))
この例で使用される NN は、2 つの隠れ層を持つ深い NN です。入力層と隠れ層は ReLU 活性化を使用し、最終層はクラス入力として提供される活性化関数を使用します (以前に実装されたシグモイド活性化関数になります)。
class ClassifierNN(torch.nn.Module): def __init__(self, loss_function, activation_function, input_dims=2, output_dims=1): super().__init__() self.linear1 = torch.nn.Linear(input_dims, input_dims * 4) self.linear2 = torch.nn.Linear(input_dims * 4, input_dims * 8) self.linear3 = torch.nn.Linear(input_dims * 8, input_dims * 4) self.output = torch.nn.Linear(input_dims * 4, output_dims) self.loss_function = loss_function self.activation_function = activation_function def forward(self, x): x = torch.nn.functional.relu(self.linear1(x)) x = torch.nn.functional.relu(self.linear2(x)) x = torch.nn.functional.relu(self.linear3(x)) x = self.activation_function(self.output(x)) return x
上の図は、単一バッチのトレーニング ロジックを示しています。その後、train_epoch 関数が複数回呼び出されます (選択されたエポック数)。
def train_epoch(model, optimizer, dataloader_train): # set the model to the training mode # https://pytorch.org/docs/stable/generated/torch.nn.Module.html#torch.nn.Module.train model.train() losses = [] accuracies = [] for step, (X_batch, y_batch) in enumerate(dataloader_train): ### forward propagation # get model output and use loss function y_pred = model(X_batch) # get class probabilities with shape [N,1] # apply loss function on predicted probabilities and ground truth loss = model.loss_function(y_pred, y_batch) ### backward propagation # set gradients to zero before backpropagation # https://pytorch.org/docs/stable/generated/torch.optim.Optimizer.zero_grad.html optimizer.zero_grad() # compute gradients # https://pytorch.org/docs/stable/generated/torch.Tensor.backward.html loss.backward() # update weights # https://pytorch.org/docs/stable/optim.html#taking-an-optimization-step optimizer.step() # update model weights # calculate batch accuracy acc = compute_accuracy(y_pred, y_batch) # append batch loss and accuracy to corresponding lists for later use accuracies.append(acc) losses.append(float(loss.detach().numpy())) # compute average epoch accuracy train_acc = np.array(accuracies).mean() # compute average epoch loss loss_epoch = np.array(losses).mean() return train_acc, loss_epoch
評価関数は、提供された PyTorch データローダーを反復処理して現在のモデルの精度を計算し、平均損失と平均精度を返します。
def evaluate(model, dataloader_in): # set the model to the evaluation mode # https://pytorch.org/docs/stable/generated/torch.nn.Module.html#torch.nn.Module.eval model.eval() val_acc_epoch = 0 losses = [] accuracies = [] # disable gradient calculation for evaluation # https://pytorch.org/docs/stable/generated/torch.no_grad.html with torch.no_grad(): for step, (X_batch, y_batch) in enumerate(dataloader_in): # get predictions y_pred = model(X_batch) # calculate loss loss = model.loss_function(y_pred, y_batch) # calculate batch accuracy acc = compute_accuracy(y_pred, y_batch) accuracies.append(acc) losses.append(float(loss.detach().numpy())) # compute average accuracy val_acc = np.array(accuracies).mean() # compute average loss loss_epoch = np.array(losses).mean() return val_acc, loss_epoch
予測関数は、提供されたデータローダーを反復処理し、後処理された (ワンホット デコードされた) モデル予測とグランド トゥルース値を [N,1] PyTorch 配列に収集し、両方の配列を返します。後で、この関数を使用して混同行列を計算し、予測を視覚化します。
def predict(model, dataloader): # set the model to the evaluation mode # https://pytorch.org/docs/stable/generated/torch.nn.Module.html#torch.nn.Module.eval model.eval() xs, ys = next(iter(dataloader)) y_pred = torch.empty([0, ys.shape[1]]) x = torch.empty([0, xs.shape[1]]) y = torch.empty([0, ys.shape[1]]) # disable gradient calculation for evaluation # https://pytorch.org/docs/stable/generated/torch.no_grad.html with torch.no_grad(): for step, (X_batch, y_batch) in enumerate(dataloader): # get predictions y_batch_pred = model(X_batch) y_pred = torch.cat([y_pred, y_batch_pred]) y = torch.cat([y, y_batch]) x = torch.cat([x, X_batch]) # print(y_pred.shape, y.shape) y_pred = postprocessing(y_pred) y = postprocessing(y) return y_pred, y, x
モデルをトレーニングするには、 train_epoch関数を N 回呼び出すだけです。N はエポック数です。評価関数は、現在のモデルの精度を検証データセットに記録するために呼び出されます。最後に、検証精度に基づいて最適なモデルが更新されます。 model_train関数は、最高の検証精度とトレーニング履歴を返します。
def model_train(model, optimizer, dataloader_train, dataloader_val, n_epochs=50): best_acc = 0 best_weights = None history = {'loss': {'train': [], 'validation': []}, 'accuracy': {'train': [], 'validation': []}} for epoch in range(n_epochs): # train on dataloader_train acc_train, loss_train = train_epoch(model, optimizer, dataloader_train) # evaluate on dataloader_val acc_val, loss_val = evaluate(model, dataloader_val) print(f'Epoch: {epoch} | Accuracy: {acc_train:.3f} / {acc_val:.3f} | ' + f'loss: {loss_train:.5f} / {loss_val:.5f}') # save epoch losses and accuracies in history dictionary history['loss']['train'].append(loss_train) history['loss']['validation'].append(loss_val) history['accuracy']['train'].append(acc_train) history['accuracy']['validation'].append(acc_val) # Save the best validation accuracy model if acc_val >= best_acc: print(f'\tBest weights updated. Old accuracy: {best_acc:.4f}. New accuracy: {acc_val:.4f}') best_acc = acc_val torch.save(model.state_dict(), 'best_weights.pt') # restore model and return best accuracy model.load_state_dict(torch.load('best_weights.pt')) return best_acc, history
すべてをまとめて、マルチクラス分類モデルをトレーニングしましょう。
######################################### # Get the dataset X, y = get_dataset(n_classes=number_of_classes) print(f'Generated dataset shape. X:{X.shape}, y:{y.shape}') # change y numpy array shape from [N,] to [N, C] for multi-class classification y = preprocessing(y, n_classes=number_of_classes) print(f'Dataset shape prepared for multi-class classification with softmax activation and CE loss.') print(f'X:{X.shape}, y:{y.shape}') # Get train and validation datal loaders dataloader_train, dataloader_val = get_data_loaders(dataset=(scale(X), y), batch_size=32) # get a batch from dataloader and output intput and output shape X_0, y_0 = next(iter(dataloader_train)) print(f'Model input data shape: {X_0.shape}, output (ground truth) data shape: {y_0.shape}') ######################################### # Create ClassifierNN for multi-class classification problem # input dims: [N, features] # output dims: [N, C] where C is number of classes # activation - softmax to output [,C] probabilities so that their sum(p_1,p_2,...,p_c)=1 # loss - cross-entropy model = ClassifierNN(loss_function=cross_entropy_loss, activation_function=softmax, input_dims=X.shape[1], output_dims=y.shape[1]) ######################################### # create optimizer and train the model on the dataset optimizer = torch.optim.Adam(model.parameters(), lr=0.001) print(f'Model size: {sum([x.reshape(-1).shape[0] for x in model.parameters()])} parameters') print('#'*10) print('Start training') acc, history = model_train(model, optimizer, dataloader_train, dataloader_val, n_epochs=20) print('Finished training') print('#'*10) print("Model accuracy: %.2f%%" % (acc*100))
予想される出力は、以下に示すものと同様になるはずです。
Generated dataset shape. X:(10000, 20), y:(10000,) Dataset shape prepared for multi-class classification with softmax activation and CE loss. X:(10000, 20), y:(10000, 4) Model input data shape: torch.Size([32, 20]), output (ground truth) data shape: torch.Size([32, 4]) Model size: 27844 parameters ########## Start training Epoch: 0 | Accuracy: 0.682 / 0.943 | loss: 0.78574 / 0.37459 Best weights updated. Old accuracy: 0.0000. New accuracy: 0.9435 Epoch: 1 | Accuracy: 0.960 / 0.967 | loss: 0.20272 / 0.17840 Best weights updated. Old accuracy: 0.9435. New accuracy: 0.9668 Epoch: 2 | Accuracy: 0.978 / 0.962 | loss: 0.12004 / 0.17931 Epoch: 3 | Accuracy: 0.984 / 0.979 | loss: 0.10028 / 0.13246 Best weights updated. Old accuracy: 0.9668. New accuracy: 0.9787 Epoch: 4 | Accuracy: 0.985 / 0.981 | loss: 0.08838 / 0.12720 Best weights updated. Old accuracy: 0.9787. New accuracy: 0.9807 Epoch: 5 | Accuracy: 0.986 / 0.981 | loss: 0.08096 / 0.12174 Best weights updated. Old accuracy: 0.9807. New accuracy: 0.9812 Epoch: 6 | Accuracy: 0.986 / 0.981 | loss: 0.07944 / 0.12036 Epoch: 7 | Accuracy: 0.988 / 0.982 | loss: 0.07605 / 0.11773 Best weights updated. Old accuracy: 0.9812. New accuracy: 0.9821 Epoch: 8 | Accuracy: 0.989 / 0.982 | loss: 0.07168 / 0.11514 Best weights updated. Old accuracy: 0.9821. New accuracy: 0.9821 Epoch: 9 | Accuracy: 0.989 / 0.983 | loss: 0.06890 / 0.11409 Best weights updated. Old accuracy: 0.9821. New accuracy: 0.9831 Epoch: 10 | Accuracy: 0.989 / 0.984 | loss: 0.06750 / 0.11128 Best weights updated. Old accuracy: 0.9831. New accuracy: 0.9841 Epoch: 11 | Accuracy: 0.990 / 0.982 | loss: 0.06505 / 0.11265 Epoch: 12 | Accuracy: 0.990 / 0.983 | loss: 0.06507 / 0.11272 Epoch: 13 | Accuracy: 0.991 / 0.985 | loss: 0.06209 / 0.11240 Best weights updated. Old accuracy: 0.9841. New accuracy: 0.9851 Epoch: 14 | Accuracy: 0.990 / 0.984 | loss: 0.06273 / 0.11157 Epoch: 15 | Accuracy: 0.991 / 0.984 | loss: 0.05998 / 0.11029 Epoch: 16 | Accuracy: 0.990 / 0.985 | loss: 0.06056 / 0.11164 Epoch: 17 | Accuracy: 0.991 / 0.984 | loss: 0.05981 / 0.11096 Epoch: 18 | Accuracy: 0.991 / 0.985 | loss: 0.05642 / 0.10975 Best weights updated. Old accuracy: 0.9851. New accuracy: 0.9851 Epoch: 19 | Accuracy: 0.990 / 0.986 | loss: 0.05929 / 0.10821 Best weights updated. Old accuracy: 0.9851. New accuracy: 0.9856 Finished training ########## Model accuracy: 98.56%
def plot_history(history): fig = plt.figure(figsize=(8, 4), facecolor=(0.0, 1.0, 0.0)) ax = fig.add_subplot(1, 2, 1) ax.plot(np.arange(0, len(history['loss']['train'])), history['loss']['train'], color='red', label='train') ax.plot(np.arange(0, len(history['loss']['validation'])), history['loss']['validation'], color='blue', label='validation') ax.set_title('Loss history') ax.set_facecolor((0.0, 1.0, 0.0)) ax.legend() ax = fig.add_subplot(1, 2, 2) ax.plot(np.arange(0, len(history['accuracy']['train'])), history['accuracy']['train'], color='red', label='train') ax.plot(np.arange(0, len(history['accuracy']['validation'])), history['accuracy']['validation'], color='blue', label='validation') ax.set_title('Accuracy history') ax.legend() fig.tight_layout() ax.set_facecolor((0.0, 1.0, 0.0)) fig.show()
acc_train, _ = evaluate(model, dataloader_train) acc_validation, _ = evaluate(model, dataloader_val) print(f'Accuracy - Train: {acc_train:.4f} | Validation: {acc_validation:.4f}')
Accuracy - Train: 0.9901 | Validation: 0.9851
val_preds, val_y, _ = predict(model, dataloader_val) print(val_preds.shape, val_y.shape) multiclass_confusion_matrix = torchmetrics.classification.ConfusionMatrix('multiclass', num_classes=number_of_classes) cm = multiclass_confusion_matrix(val_preds, val_y) print(cm) df_cm = pd.DataFrame(cm) plt.figure(figsize = (6,5), facecolor=(0.0,1.0,0.0)) sn.heatmap(df_cm, annot=True, fmt='d') plt.show()
val_preds, val_y, val_x = predict(model, dataloader_val) val_preds, val_y, val_x = val_preds.numpy(), val_y.numpy(), val_x.numpy() show_dataset(val_x, val_y,'Ground Truth') show_dataset(val_x, val_preds, 'Predictions')
マルチクラス分類の場合は、ソフトマックス アクティベーションとクロス エントロピー損失を使用する必要があります。バイナリ分類からマルチクラス分類に切り替えるには、データの前処理と後処理、アクティベーション関数、および損失関数など、いくつかのコードの変更が必要です。さらに、クラス数を 2 に設定して、ワンホット エンコーディング、ソフトマックス、クロスエントロピー ロスを使用することで、二値分類問題を解くことができます。