Bài viết trước của tôi xây dựng bài toán phân loại và chia nó thành 3 loại (nhị phân, nhiều lớp và nhiều nhãn) và trả lời câu hỏi “Bạn cần sử dụng hàm kích hoạt và hàm mất nào để giải quyết nhiệm vụ phân loại nhị phân?”.
Trong bài đăng này, tôi sẽ trả lời câu hỏi tương tự nhưng dành cho nhiệm vụ phân loại nhiều lớp và cung cấp cho bạn một
Bạn cần sử dụng chức năng kích hoạt và mất mát nào để giải quyết nhiệm vụ phân loại nhiều lớp?
Mã được cung cấp phần lớn dựa trên việc triển khai phân loại nhị phân vì bạn cần thêm rất ít sửa đổi vào mã và NN của mình để chuyển từ phân loại nhị phân sang nhiều lớp. Các khối mã đã sửa đổi được đánh dấu bằng (Đã thay đổi) để điều hướng dễ dàng hơn.
Như sẽ được trình bày sau, hàm kích hoạt được sử dụng để phân loại nhiều lớp là kích hoạt softmax. Softmax được sử dụng rộng rãi trong các kiến trúc NN khác nhau ngoài việc phân loại nhiều lớp. Ví dụ: softmax là cốt lõi của khối chú ý nhiều đầu được sử dụng trong các mô hình Transformer (xem Chú ý là tất cả những gì bạn cần ) do khả năng chuyển đổi các giá trị đầu vào thành phân bố xác suất (xem thêm về điều đó sau).
Nếu bạn biết động lực đằng sau việc áp dụng kích hoạt softmax và mất CE để giải quyết các vấn đề phân loại nhiều lớp, bạn sẽ có thể hiểu và triển khai các kiến trúc NN và hàm mất mát phức tạp hơn nhiều.
Bài toán phân loại nhiều lớp có thể được biểu diễn dưới dạng một tập hợp các mẫu {(x_1, y_1), (x_2, y_2),...,(x_n, y_n)} , trong đó x_i là một vectơ m chiều chứa các đặc điểm của mẫu i và y_i là lớp mà x_i thuộc về. Trong đó nhãn y_i có thể giả sử một trong các giá trị k , trong đó k là số lớp cao hơn 2. Mục tiêu là xây dựng mô hình dự đoán nhãn y_i cho mỗi mẫu đầu vào x_i .
Ví dụ về các nhiệm vụ có thể được coi là vấn đề phân loại nhiều lớp:
Trong phân loại nhiều lớp, bạn được cung cấp:
một tập hợp các mẫu {(x_1, y_1), (x_2, y_2),...,(x_n, y_n)}
x_i là vectơ m chiều chứa các đặc điểm của mẫu i
y_i là lớp mà x_i thuộc về và có thể nhận một trong các giá trị k , trong đó k>2 là số lượng lớp.
Để xây dựng mạng nơ ron phân loại nhiều lớp như một bộ phân loại xác suất, chúng ta cần:
Lớp tuyến tính cuối cùng của mạng nơ-ron tạo ra một vectơ "giá trị đầu ra thô". Trong trường hợp phân loại, các giá trị đầu ra thể hiện độ tin cậy của mô hình rằng đầu vào thuộc về một trong k lớp. Như đã thảo luận trước, lớp đầu ra cần phải có kích thước k và các giá trị đầu ra phải biểu thị xác suất p_i cho mỗi lớp k và SUM(p_i)=1 .
Bài viết về phân loại nhị phân sử dụng kích hoạt sigmoid để chuyển đổi giá trị đầu ra NN thành xác suất. Hãy thử áp dụng sigmoid cho k giá trị đầu ra trong phạm vi [-3, 3] và xem liệu sigmoid có thỏa mãn các yêu cầu được liệt kê trước đó hay không:
k giá trị đầu ra phải nằm trong phạm vi (0,1), trong đó k là số lớp
tổng của k giá trị đầu ra phải bằng 1
Bài viết trước cho thấy hàm sigmoid ánh xạ các giá trị đầu vào vào một phạm vi (0,1). Hãy xem liệu kích hoạt sigmoid có đáp ứng yêu cầu thứ hai hay không. Trong bảng ví dụ bên dưới, tôi đã xử lý một vectơ có kích thước k (k=7) với kích hoạt sigmoid và tính tổng tất cả các giá trị này - tổng của 7 giá trị này bằng 3,5. Một cách đơn giản để khắc phục điều đó là chia tất cả các giá trị k cho tổng của chúng.
Đầu vào | -3 | -2 | -1 | 0 | 1 | 2 | 3 | TỔNG |
---|---|---|---|---|---|---|---|---|
đầu ra sigmoid | 0,04743 | 0.11920 | 0,26894 | 0,50000 | 0,73106 | 0,88080 | 0,95257 | 3,5000 |
Một cách khác là lấy số mũ của giá trị đầu vào và chia cho tổng số mũ của tất cả các giá trị đầu vào:
Hàm softmax biến đổi vectơ số thực thành vectơ xác suất. Mỗi xác suất trong kết quả nằm trong phạm vi (0,1) và tổng các xác suất là 1.
Đầu vào | -3 | -2 | -1 | 0 | 1 | 2 | 3 | TỔNG |
---|---|---|---|---|---|---|---|---|
softmax | 0,00157 | 0,00426 | 0,01159 | 0,03150 | 0,08563 | 0,23276 | 0,63270 | 1 |
Có một điều bạn cần lưu ý khi làm việc với softmax: giá trị đầu ra p_i phụ thuộc vào tất cả các giá trị trong mảng đầu vào vì chúng ta chia nó cho tổng số mũ của tất cả các giá trị. Bảng dưới đây chứng minh điều này: hai vectơ đầu vào có 3 giá trị chung {1, 3, 4}, nhưng giá trị softmax đầu ra khác nhau do phần tử thứ hai khác nhau (2 và 4).
Đầu vào 1 | 1 | 2 | 3 | 4 |
---|---|---|---|---|
phần mềm tối đa 1 | 0,0321 | 0,0871 | 0,2369 | 0,6439 |
Đầu vào 2 | 1 | 4 | 3 | 4 |
softmax 2 | 0,0206 | 0,4136 | 0,1522 | 0,4136 |
Mất entropy chéo nhị phân được định nghĩa là:
Trong phân loại nhị phân, có hai xác suất đầu ra p_i và (1-p_i) và giá trị chân lý cơ bản y_i và (1-y_i).
Bài toán phân loại đa lớp sử dụng khái quát hóa tổn thất BCE cho N lớp: mất mát entropy chéo.
N là số lượng mẫu đầu vào, y_i là sự thật cơ bản và p_i là xác suất dự đoán của lớp i .
Để triển khai NN phân loại nhiều lớp theo xác suất, chúng ta cần:
Hầu hết các phần của mã đều dựa trên mã từ bài viết trước về phân loại nhị phân.
Các phần thay đổi được đánh dấu bằng (Đã thay đổi) :
Hãy mã hóa mạng lưới thần kinh để phân loại nhiều lớp bằng khung PyTorch.
Đầu tiên, cài đặt
# used for accuracy metric and confusion matrix !pip install torchmetrics
Nhập các gói sẽ được sử dụng sau này trong mã
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
Đặt biến toàn cục với số lượng lớp (nếu bạn đặt thành 2 và nhận NN phân loại nhị phân sử dụng softmax và mất Cross-Entropy)
number_of_classes=4
tôi sẽ sử dụng
n_samples - là số lượng mẫu được tạo
n_features - đặt số lượng kích thước của mẫu X được tạo
n_classes - số lượng lớp trong tập dữ liệu được tạo. Trong bài toán phân loại nhiều lớp cần có nhiều hơn 2 lớp
Tập dữ liệu được tạo sẽ có X có hình dạng [n_samples, n_features] và Y có hình dạng [n_samples, ] .
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
Xác định các chức năng để trực quan hóa và in ra số liệu thống kê về tập dữ liệu. sử dụng hàm 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()
Chia tỷ lệ tập dữ liệu có tính năng X thành phạm vi [0,1] với bộ chia tỷ lệ tối đa tối thiểu. Điều này thường được thực hiện để đào tạo nhanh hơn và ổn định hơn.
def scale(x_in): return (x_in - x_in.min(axis=0))/(x_in.max(axis=0)-x_in.min(axis=0))
Hãy in ra số liệu thống kê về tập dữ liệu đã tạo và trực quan hóa nó bằng các hàm ở trên.
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')
Các kết quả đầu ra bạn sẽ nhận được dưới đây.
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]
Tỷ lệ tối thiểu-tối đa không làm biến dạng các tính năng của tập dữ liệu, nó biến đổi tuyến tính chúng thành phạm vi [0,1]. Hình "tập dữ liệu sau khi chia tỷ lệ tối thiểu-tối đa" dường như bị biến dạng so với hình trước đó vì 20 chiều bị giảm xuống còn 2 bởi thuật toán PCA và thuật toán PCA có thể bị ảnh hưởng bởi tỷ lệ tối thiểu-tối đa.
Tạo trình tải dữ liệu 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
Kiểm tra trình tải dữ liệu 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}')
Đầu ra:
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])
Tạo các hàm tiền xử lý và hậu xử lý. Như bạn có thể đã lưu ý trước khi hình chữ Y hiện tại là [N], chúng ta cần nó là [N,number_of_classes]. Để làm được điều đó, chúng ta cần mã hóa một lần các giá trị trong vectơ Y.
Mã hóa một lần là một quá trình chuyển đổi các chỉ mục lớp thành biểu diễn nhị phân trong đó mỗi lớp được biểu thị bằng một vectơ nhị phân duy nhất.
Nói cách khác: tạo một vectơ 0 có kích thước [number_of_classes] và đặt phần tử ở vị trí class_id thành 1, trong đó 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.]
Các tensor Pytorch có thể được xử lý bằng torch.nn.function.one_hot và việc triển khai gọn gàng rất đơn giản. Vectơ đầu ra sẽ có dạng [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
Để chuyển đổi vectơ được mã hóa một nóng trở lại id lớp, chúng ta cần tìm chỉ mục của phần tử tối đa trong vectơ được mã hóa một nóng. Nó có thể được thực hiện với torch.argmax hoặc np.argmax bên dưới.
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)
Kiểm tra các chức năng tiền xử lý và hậu xử lý đã xác định.
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]}')
Đầu ra:
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
Phần này trình bày cách triển khai tất cả các chức năng cần thiết để huấn luyện mô hình phân loại nhị phân.
Việc triển khai công thức softmax dựa trên 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
Hãy kiểm tra softmax:
tạo mảng numpy test_input trong phạm vi [-10, 11] với bước 1
định hình lại nó thành một tensor có hình dạng [7,3]
xử lý test_input với hàm softmax đã triển khai và triển khai mặc định PyTorch torch.nn.function.softmax
so sánh kết quả (chúng phải giống hệt nhau)
xuất các giá trị softmax và tổng cho tất cả bảy tensor [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()}')
Đầu ra:
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]
Việc triển khai công thức CE dựa trên 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
Kiểm tra việc thực hiện CE:
tạo mảng test_input có hình dạng [10,5] và các giá trị trong phạm vi [0,1) với
tạo mảng test_target có hình dạng [10,] và các giá trị trong phạm vi [0,4].
mảng test_target mã hóa một lần nóng
tính toán tổn thất với hàm cross_entropy đã triển khai và triển khai PyTorch
so sánh kết quả (chúng phải giống hệt nhau)
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()}')
Sản lượng dự kiến:
test_input shape: [10, 5], test_target shape: [10, 5] Loss outputs are the same: True
tôi sẽ sử dụng
Để tạo số liệu độ chính xác phân loại nhiều lớp, cần có hai tham số:
loại nhiệm vụ "đa lớp"
số lớp 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 được sử dụng trong ví dụ này là NN sâu với 2 lớp ẩn. Lớp đầu vào và lớp ẩn sử dụng kích hoạt ReLU và lớp cuối cùng sử dụng chức năng kích hoạt được cung cấp làm đầu vào lớp (nó sẽ là chức năng kích hoạt sigmoid đã được triển khai trước đó).
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
Hình trên mô tả logic huấn luyện cho một đợt. Sau này, hàm train_epoch sẽ được gọi nhiều lần (số kỷ nguyên đã chọn).
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
Hàm đánh giá lặp lại trên trình tải dữ liệu PyTorch được cung cấp để tính toán độ chính xác của mô hình hiện tại và trả về tổn thất trung bình cũng như độ chính xác trung bình.
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
Hàm dự đoán lặp lại trên trình tải dữ liệu được cung cấp, thu thập các dự đoán mô hình được xử lý sau (được giải mã một lần) và đưa các giá trị chân lý vào mảng PyTorch [N,1] rồi trả về cả hai mảng. Sau này, hàm này sẽ được sử dụng để tính toán ma trận nhầm lẫn và trực quan hóa các dự đoán.
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
Để huấn luyện mô hình, chúng ta chỉ cần gọi hàm train_epoch N lần, trong đó N là số epoch. Hàm đánh giá được gọi để ghi lại độ chính xác của mô hình hiện tại trên tập dữ liệu xác thực. Cuối cùng, mô hình tốt nhất được cập nhật dựa trên độ chính xác xác thực. Hàm model_train trả về độ chính xác xác thực tốt nhất và lịch sử đào tạo.
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
Hãy tập hợp mọi thứ lại với nhau và huấn luyện mô hình phân loại nhiều lớp.
######################################### # 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))
Kết quả đầu ra dự kiến phải tương tự như kết quả được cung cấp bên dưới.
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')
Để phân loại nhiều lớp, bạn cần sử dụng kích hoạt softmax và mất entropy chéo. Có một số sửa đổi mã cần thiết để chuyển từ phân loại nhị phân sang phân loại nhiều lớp: các hàm tiền xử lý và xử lý hậu kỳ, kích hoạt và mất dữ liệu. Hơn nữa, bạn có thể giải quyết vấn đề phân loại nhị phân bằng cách đặt số lượng lớp thành 2 với mã hóa một nóng, softmax và mất entropy chéo.