paint-brush
다중 클래스 분류: 신경망의 활성화 및 손실 함수 이해~에 의해@owlgrey
1,345 판독값
1,345 판독값

다중 클래스 분류: 신경망의 활성화 및 손실 함수 이해

~에 의해 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 아키텍처에서 광범위하게 사용됩니다. 예를 들어, 소프트맥스는 입력 값을 확률 분포로 변환하는 기능으로 인해 Transformer 모델( Attention Is All You Need 참조)에 사용되는 다중 헤드 어텐션 블록의 핵심입니다(자세한 내용은 나중에 참조).


Scaled Dot-Product Attention(Multi-Head attention 모듈에서 가장 일반적으로 사용됨)



다중 클래스 분류 문제를 해결하기 위해 소프트맥스 활성화 및 CE 손실을 적용하는 동기를 알고 있다면 훨씬 더 복잡한 NN 아키텍처 및 손실 기능을 이해하고 구현할 수 있습니다.


2 다중 클래스 분류 문제 공식화

다중 클래스 분류 문제는 샘플 집합 {(x_1, y_1), (x_2, y_2),...,(x_n, y_n)} 으로 표현될 수 있습니다. 여기서 x_i 는 샘플의 특징을 포함하는 m차원 벡터입니다. iy_ix_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_ix_i가 속한 클래스이고 k 값 중 하나를 취할 수 있습니다. 여기서 k>2 는 클래스 수입니다.


확률 분류기로 다중 클래스 분류 신경망을 구축하려면 다음이 필요합니다.

  • 크기가 k 인 출력 완전 연결 레이어
  • 출력 값은 [0,1] 범위에 있어야 합니다.
  • 출력 값의 합은 1과 같아야 합니다. 다중 클래스 분류에서 각 입력 x는 하나의 클래스(상호 배타적 클래스)에만 속할 수 있으므로 모든 클래스의 확률 합계는 1이어야 합니다. SUM(p_0,…,p_k )=1 .
  • 예측과 실제값이 동일할 때 가장 낮은 값을 갖는 손실 함수


3.1 소프트맥스 활성화 함수

신경망의 최종 선형 레이어는 "원시 출력 값"의 벡터를 출력합니다. 분류의 경우 출력 값은 입력이 k 클래스 중 하나에 속한다는 모델의 신뢰도를 나타냅니다. 앞에서 논의한 것처럼 출력 레이어는 크기 k를 가져야 하며 출력 값은 k 클래스 각각에 대한 확률 p_iSUM(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

[-10, 10] 범위의 지수 플롯


값이 [-10, 10]이고 크기가 21인 벡터의 소프트맥스


소프트맥스로 작업할 때 알아야 할 한 가지 사항이 있습니다. 출력 값 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


3.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을 훈련하는 과정


코드의 대부분은 이진 분류에 관한 이전 기사의 코드를 기반으로 합니다.


변경된 부분은 (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


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 데이터세트 스케일러

최소 최대 스케일러를 사용하여 데이터 세트 기능 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 데이터 세트를 두 개의 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]인 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


4.5 다중 클래스 분류 모델 생성 및 훈련

이 섹션에서는 이진 분류 모델을 훈련하는 데 필요한 모든 기능의 구현을 보여줍니다.


4.5.1 소프트맥스 활성화(변경됨)

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. 1단계에서 [-10, 11] 범위의 test_input numpy 배열을 생성합니다.

  2. 모양이 [7,3]인 텐서로 모양을 변경합니다.

  3. 구현된 소프트맥스 기능과 PyTorch 기본 구현으로 test_input을 처리합니다 .torch.nn.function.softmax

  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 손실 함수: 교차 엔트로피(변경됨)

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 구현:


  1. 모양이 [10,5]이고 범위가 [0,1)인 test_input 배열을 생성합니다. 토치랜드

  2. 모양이 [10,]이고 값이 [0,4] 범위인 test_target 배열을 생성합니다.

  3. 원-핫 인코딩 test_target 배열

  4. 구현된 cross_entropy 함수와 PyTorch 구현으로 손실 계산 torch.nn.function.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 정확도 측정항목(변경됨)

나는 사용할 것이다 토치메트릭스 모델 예측 및 실제 정보를 기반으로 정확도를 계산하기 위한 구현입니다.


다중 클래스 분류 정확도 측정항목을 생성하려면 두 개의 매개변수가 필요합니다.

  • 작업 유형 "멀티클래스"

  • 클래스 수 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개로 설정하여 이진 분류 문제를 해결할 수 있습니다.