이전 게시물에서는 분류 문제를 공식화하고 이를 3가지 유형(이진, 다중 클래스, 다중 레이블)으로 나누고 "이진 분류 작업을 해결하려면 어떤 활성화 및 손실 함수를 사용해야 합니까?"라는 질문에 답변했습니다.
이 게시물에서는 다중 클래스 분류 작업에 대한 동일한 질문에 답하고 다음을 제공하겠습니다.
다중 클래스 분류 작업을 해결하려면 어떤 활성화 및 손실 함수를 사용해야 합니까?
제공된 코드는 주로 이진 분류 구현을 기반으로 합니다. 이진 분류에서 다중 클래스로 전환하려면 코드와 NN에 거의 수정 사항을 추가해야 하기 때문입니다. 수정된 코드 블록은 더 쉽게 탐색할 수 있도록 (변경됨) 으로 표시됩니다.
나중에 설명하겠지만 다중 클래스 분류에 사용되는 활성화 함수는 소프트맥스 활성화입니다. Softmax는 다중 클래스 분류 이외의 다양한 NN 아키텍처에서 광범위하게 사용됩니다. 예를 들어, 소프트맥스는 입력 값을 확률 분포로 변환하는 기능으로 인해 Transformer 모델( Attention Is All You Need 참조)에 사용되는 다중 헤드 어텐션 블록의 핵심입니다(자세한 내용은 나중에 참조).
다중 클래스 분류 문제를 해결하기 위해 소프트맥스 활성화 및 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 값 중 하나를 취할 수 있습니다. 여기서 k>2 는 클래스 수입니다.
확률 분류기로 다중 클래스 분류 신경망을 구축하려면 다음이 필요합니다.
신경망의 최종 선형 레이어는 "원시 출력 값"의 벡터를 출력합니다. 분류의 경우 출력 값은 입력이 k 클래스 중 하나에 속한다는 모델의 신뢰도를 나타냅니다. 앞에서 논의한 것처럼 출력 레이어는 크기 k를 가져야 하며 출력 값은 k 클래스 각각에 대한 확률 p_i 및 SUM(p_i)=1을 나타내야 합니다.
이진 분류 에 관한 기사에서는 시그모이드 활성화를 사용하여 NN 출력 값을 확률로 변환합니다. [-3, 3] 범위의 k 출력 값에 시그모이드를 적용하고 시그모이드가 이전에 나열된 요구 사항을 충족하는지 확인해 보겠습니다.
k개의 출력 값은 (0,1) 범위 내에 있어야 합니다. 여기서 k 는 클래스 수입니다.
k개의 출력 값의 합은 1과 같아야 합니다.
이전 기사에서는 시그모이드 함수가 입력 값을 범위 (0,1)에 매핑하는 것을 보여주었습니다. 시그모이드 활성화가 두 번째 요구 사항을 충족하는지 살펴보겠습니다. 아래 예제 테이블에서는 시그모이드 활성화를 사용하여 크기가 k (k=7)인 벡터를 처리하고 이 모든 값을 합산했습니다. 이 7개 값의 합은 3.5와 같습니다. 이 문제를 해결하는 간단한 방법은 모든 k 값을 해당 합계로 나누는 것입니다.
입력 | -삼 | -2 | -1 | 0 | 1 | 2 | 삼 | 합집합 |
---|---|---|---|---|---|---|---|---|
시그모이드 출력 | 0.04743 | 0.11920 | 0.26894 | 0.50000 | 0.73106 | 0.88080 | 0.95257 | 3.5000 |
또 다른 방법은 입력 값의 지수를 취하여 모든 입력 값의 지수의 합으로 나누는 것입니다.
소프트맥스 함수는 실수 벡터를 확률 벡터로 변환합니다. 결과의 각 확률은 (0,1) 범위에 있고 확률의 합은 1입니다.
입력 | -삼 | -2 | -1 | 0 | 1 | 2 | 삼 | 합집합 |
---|---|---|---|---|---|---|---|---|
소프트맥스 | 0.00157 | 0.00426 | 0.01159 | 0.03150 | 0.08563 | 0.23276 | 0.63270 | 1 |
소프트맥스로 작업할 때 알아야 할 한 가지 사항이 있습니다. 출력 값 p_i는 모든 값의 지수 합계로 나누기 때문에 입력 배열의 모든 값에 따라 달라집니다. 아래 표는 이를 보여줍니다. 두 개의 입력 벡터에는 3개의 공통 값 {1, 3, 4}가 있지만 두 번째 요소(2와 4)가 다르기 때문에 출력 소프트맥스 값이 다릅니다.
입력 1 | 1 | 2 | 삼 | 4 |
---|---|---|---|---|
소프트맥스 1 | 0.0321 | 0.0871 | 0.2369 | 0.6439 |
입력 2 | 1 | 4 | 삼 | 4 |
소프트맥스 2 | 0.0206 | 0.4136 | 0.1522 | 0.4136 |
이진 교차 엔트로피 손실은 다음과 같이 정의됩니다.
이진 분류에는 두 가지 출력 확률 p_i 와 (1-p_i) 와 정답 값 y_i 와 (1-y_i)가 있습니다.
다중 클래스 분류 문제는 N 클래스에 대한 BCE 손실의 일반화인 교차 엔트로피 손실을 사용합니다.
N은 입력 샘플 수, y_i 는 실제값, p_i 는 클래스 i 의 예측 확률입니다.
확률적 다중 클래스 분류 NN을 구현하려면 다음이 필요합니다.
코드의 대부분은 이진 분류에 관한 이전 기사의 코드를 기반으로 합니다.
변경된 부분은 (Changed) 로 표시됩니다.
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()
최소 최대 스케일러를 사용하여 데이터 세트 기능 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]인 0 벡터를 만들고 class_id 위치의 요소를 1로 설정합니다. 여기서 class_ids는 {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로 변환하려면 원-핫 인코딩된 벡터에서 max 요소의 인덱스를 찾아야 합니다. 아래의 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
이 섹션에서는 이진 분류 모델을 훈련하는 데 필요한 모든 기능의 구현을 보여줍니다.
Softmax 공식의 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 배열을 생성합니다.
모양이 [7,3]인 텐서로 모양을 변경합니다.
구현된 소프트맥스 기능과 PyTorch 기본 구현으로 test_input을 처리합니다 .torch.nn.function.softmax
결과를 비교하십시오(동일해야 함).
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]
CE 공식의 PyTorch 기반 구현
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
나는 사용할 것이다
다중 클래스 분류 정확도 측정항목을 생성하려면 두 개의 매개변수가 필요합니다.
작업 유형 "멀티클래스"
클래스 수 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개로 설정하여 이진 분류 문제를 해결할 수 있습니다.