paint-brush
マルチクラス分類: ニューラル ネットワークの活性化関数と損失関数を理解する@owlgrey
2,871 測定値
2,871 測定値

マルチクラス分類: ニューラル ネットワークの活性化関数と損失関数を理解する

Dmitrii Matveichev 25m2024/01/24
Read on Terminal Reader

長すぎる; 読むには

マルチクラス分類ニューラル ネットワークを構築するには、最終層でソフトマックス アクティベーション関数をクロスエントロピー損失とともに使用する必要があります。最終的な層のサイズは k である必要があります。k はクラスの数です。クラス ID はワンホット エンコーディングで前処理する必要があります。このようなニューラル ネットワークは、入力がクラス i に属する確率 p_i を出力します。予測されたクラス ID を見つけるには、最大確率のインデックスを見つける必要があります。
featured image - マルチクラス分類: ニューラル ネットワークの活性化関数と損失関数を理解する
Dmitrii Matveichev  HackerNoon profile picture


前回の投稿では、分類問題を定式化して 3 つのタイプ (バイナリ、マルチクラス、マルチラベル) に分割し、「バイナリ分類タスクを解決するにはどのような活性化関数と損失関数を使用する必要がありますか?」という質問に答えました。


この投稿では、マルチクラス分類タスクを除いて同じ質問に答え、次の情報を提供します。 Google colab での pytorch 実装の例


マルチクラス分類タスクを解決するには、どのような活性化関数と損失関数を使用する必要がありますか?


バイナリ分類からマルチクラスに切り替えるには、コードと NN にほとんど変更を加える必要がないため、提供されるコードは主にバイナリ分類の実装に基づいています。変更されたコード ブロックには、ナビゲーションが容易になるように(変更) のマークが付いています。


1 複数クラス分類に使用される活性化関数と損失を理解することが重要なのはなぜですか?

後で示すように、マルチクラス分類に使用される活性化関数はソフトマックス活性化です。 Softmax は、マルチクラス分類以外のさまざまな NN アーキテクチャで広く使用されています。たとえば、softmax は、入力値を確率分布に変換する機能により、Transformer モデルで使用されるマルチヘッド アテンション ブロックの中核となります ( 「アテンションだけで十分です」を参照)。


スケーリングされたドット積アテンション (マルチヘッド アテンション モジュールで最も一般的)



マルチクラス分類問題を解決するためにソフトマックス アクティベーションと CE 損失を適用する背後にある動機を知っていれば、より複雑な NN アーキテクチャと損失関数を理解して実装できるようになります。


2 マルチクラス分類問題の定式化

多クラス分類問題は、サンプルのセット{(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 を予測するモデルを構築することです。

マルチクラス分類問題として扱うことができるタスクの例:

  • 医療診断 - 提供されたデータ (病歴、検査結果、症状) に基づいて、いくつかの病気のいずれかを患っている患者を診断すること
  • 製品分類 - 電子商取引プラットフォーム向けの自動製品分類
  • 天気予報 - 将来の天気を晴れ、曇り、雨などに分類します。
  • 映画、音楽、記事をさまざまなジャンルに分類する
  • オンライン顧客レビューを製品フィードバック、サービスフィードバック、苦情などのカテゴリに分類する


3 マルチクラス分類のための活性化関数と損失関数


マルチクラス分類では次のようになります。

  • サンプルのセット{(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の出力全結合層
  • 出力値は [0,1] の範囲内である必要があります
  • 出力値の合計は 1 に等しくなければなりません。マルチクラス分類では、各入力x は1 つのクラス (相互に排他的なクラス) にのみ属することができるため、すべてのクラスの合計確率は 1 になる必要があります: SUM(p_0,…,p_k) )=1
  • 予測とグランドトゥルースが同じ場合に最小値を持つ損失関数


3.1 ソフトマックス活性化関数

ニューラル ネットワークの最後の線形層は、「生の出力値」のベクトルを出力します。分類の場合、出力値は、入力が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

[-10, 10] 範囲の指数のプロット


サイズ 21、値 [-10, 10] のベクトルのソフトマックス


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


3.2クロスエントロピー損失

バイナリ相互エントロピー損失は次のように定義されます。

バイナリクロスエントロピー損失


バイナリ分類では、2 つの出力確率p_iおよび(1-p_i)とグランド トゥルース値y_iおよび(1-y_i) があります。


マルチクラス分類問題では、N クラスの BCE 損失、つまりクロスエントロピー損失の一般化が使用されます。


クロスエントロピー損失


N は入力サンプルの数、 y_iはグラウンド トゥルース、 p_iはクラスiの予測確率です。


4 PyTorch を使用したマルチクラス分類 NN の例

確率的マルチクラス分類 NN を実装するには、以下が必要です。

  • グラウンド トゥルースと予測は次元[N,k]を持つ必要があります。ここで、 Nは入力サンプルの数、 kはクラスの数です - クラス ID はサイズkのベクトルにエンコードされる必要があります
  • 最終的な線形層のサイズはkでなければなりません
  • 最終層からの出力は、出力確率を取得するためにソフトマックスアクティベーションで処理する必要があります。
  • CE損失は、予測されたクラス確率とグランド トゥルース値に適用する必要があります
  • サイズkの出力ベクトルから出力クラス ID を見つけます



マルチクラス分類 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


4.1 データセットの作成

グローバル変数にクラスの数を設定します (2 に設定し、ソフトマックスとクロスエントロピー損失を使用するバイナリ分類 NN を取得する場合)


 number_of_classes=4


私が使用しますsklearn.datasets.make_classificationバイナリ分類データセットを生成するには:

  • 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


4.2 データセットの視覚化

データセット統計を視覚化して出力する関数を定義します。 show_dataset 関数は使用しますPCA 2D プロットでの入力データ X の視覚化を簡素化するために、X の次元を任意の数から 2 まで削減します。


 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()



4.3 データセットスケーラー

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 データローダーを作成します。 sklearn.datasets.make_classificationデータセットを 2 つの numpy 配列として生成します。 PyTorch データローダーを作成するには、torch.utils.data.TensorDataset を使用して numpy データセットを torch.tensor に変換する必要があります。


 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])


4.4 データセットの前処理と後処理 (変更)

前処理関数と後処理関数を作成します。前に気づいたかもしれませんが、現在の 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


4.5 マルチクラス分類モデルの作成とトレーニング

このセクションでは、二項分類モデルをトレーニングするために必要なすべての関数の実装を示します。


4.5.1 ソフトマックスの有効化 (変更)

PyTorch ベースのソフトマックス公式の実装

Softmax アクティベーション定義


 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. ステップ 1 で [-10, 11] の範囲でtest_input numpy 配列を生成します

  2. それを、shape [7,3] のテンソルに再形成します。

  3. 実装されたSoftmax関数と PyTorch のデフォルト実装torch.nn.Functional.softmaxを使用してtest_inputを処理します。

  4. 結果を比較します(結果は同一である必要があります)

  5. 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]


4.5.2 損失関数: クロスエントロピー (変更)

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 実装のテスト:


  1. 形状 [10,5] と範囲 [0,1) のtest_input配列を生成します。トーチランド

  2. 形状 [10,] と範囲 [0,4] の値を持つtest_target配列を生成します。

  3. ワンホット エンコードtest_target配列

  4. 実装されたcross_entropy関数とPyTorch実装を使用して損失を計算するtorch.nn.Functional.binary_cross_entropy

  5. 結果を比較します (結果は同一である必要があります)


 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


4.5.3 精度メトリクス (変更)

私が使用しますトーチメトリクスモデルの予測とグラウンド トゥルースに基づいて精度を計算するための実装。


マルチクラス分類精度メトリックを作成するには、次の 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))


4.5.4 NNモデル

この例で使用される 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


4.5.5 トレーニング、評価、予測

マルチクラス分類 NN をトレーニングするプロセス


上の図は、単一バッチのトレーニング ロジックを示しています。その後、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


4.5.6 データセットの取得、モデルの作成、トレーニング (変更)

すべてをまとめて、マルチクラス分類モデルをトレーニングしましょう。

 ######################################### # 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%


4.5.7 トレーニング履歴のプロット

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() 

トレーニングと検証の損失と精度の履歴


4.6 モデルの評価


4.6.1 トレーニングと検証の精度の計算

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


4.6.2 混同行列の印刷 (変更)

 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() 

検証データセットの混同行列


4.6.3 プロットの予測とグランドトゥルース

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 に設定して、ワンホット エンコーディング、ソフトマックス、クロスエントロピー ロスを使用することで、二値分類問題を解くことができます。