1. Giới thiệu
PyTorch là một thư viện Máy học mã nguồn mở, được phát triển bởi nhóm nghiên cứu của Facebook. Bản phát hành đầu tiên của PyTorch là vào tháng 10 năm 2016.
Đặc biệt, trong các lĩnh vực nghiên cứu, nhiều tác giả hiện nay sử dụng PyTorch để triển khai bài toán của mình. PyTorch cho thấy lợi thế của nó trong lĩnh vực nghiên cứu bởi việc rất dễ dàng debug model.
PyTorch dễ sử dụng, được viết theo Python-based nên dễ tiếp cận đối với những người dùng có kiến thức lập trình cơ bản.
Trong phần giới thiệu từ Github PyTorch, PyTorch có 2 tính năng chính:
- Tính toán dựa vào Tensor (tương tự như NumPy), có thể sử dụng với sức mạnh của GPU
- Tự động tính toán đạo hàm khi triển khai/xây dựng/huấn luyện mô hình Neural Networks (Autograd, sẽ được trình bày ở phần sau)
2. Cài đặt
Ở trang chủ PyTorch có hướng dẫn cài đặt phiên bản mới nhất:
- Phiên bản PyTorch:
- Stable (phiên bản hiện tại là 1.8.1): phiên bản ổn định, được hỗ trợ và thử nghiệm hiện tại của PyTorch, phù hợp với nhiều đối tượng sử dụng.
- Preview (Nightly): phiên bản xem trước nếu bạn muốn phiên bản mới nhất (1.9), chưa được kiểm tra và hỗ trợ đầy đủ.
- Hệ điều hành: Linux, Mac, Windows
- Package: Conda, Pip, LibTorch (dùng cho C++/Java), Source
- Ngôn ngữ: Python, C++/Java
- Nền tảng tính toán: dựa trên nền tảng tính toán thì PyTorch hỗ trợ CPU hoặc GPU (với PyTorch-GPU có thể vừa sử dụng GPU và CPU, người dùng có thể tùy chỉnh)
- CUDA xy.z: với GPU của NVIDIA, chú ý phiên bản xy.z phải tương thích với CUDA Toolkit và CUDA Driver
- ROCm xy.z: với GPU của AMD
- CPU: chỉ sử dụng với CPU Sau khi lựa chọn xong thì chạy command theo hướng dẫn để cài đặt.
Để cài đặt PyTorch theo phiên bản mong muốn xem thêm tại đây. Ví dụ phiên bản 1.4.0 ở hệ điều hành Linux, CUDA=10.1:
$ conda install pytorch==1.4.0 torchvision==0.5.0 cudatoolkit=10.1 -c pytorch
hoặc
$ pip install torch==1.4.0 torchvision==0.5.0
Thông tin thêm:
- CUDA: là một kiến trúc tính toán song song phổ biến được phát triển bới NVIDIA cho GPUs.
- Để biết phiên bản của CUDA đang dùng, chạy command line $ cat /usr/local/cuda/version.txt hoặc $ nvcc --version. nvcc chỉ có khi cài CUDA Toolkit từ Ubuntu hoặc NVIDIA.
- "CUDA version" hiển thị khi chạy command line $ nvidia-smi không phải là phiên bản CUDA đã được cài đặt, mà là của phiên bản CUDA tương thích nhất cho CUDA driver.
- Khi cài đặt PyTorch-gpu (TensorFlow-gpu cũng vậy) với Conda, phiên bản CUDA và CuDNN được cài đặt cùng với PyTorch trong môi trường đó, không ảnh hưởng đến các phiên bản CUDA/CuDNN ở môi trường khác hay ở cấp độ toàn hệ thống. Đây cũng là một lợi ích khi không cần phải cài đặt CUDA/CuDNN bằng tay, mất nhiều năng lượng để cài đặt, và ở các Servers hiện tại mà team mình đang sử dụng thì cũng không cài được mà phải nhờ SysAdmin.
3. Tensors
Tensors là một cấu trúc dữ liệu tương tự như mảng, ma trận. Tensors trong PyTorch giống với ndarrays của NumPy. NumPy chỉ sử dụng được CPU, Tensors còn có thể dùng với GPU để tối ưu tốc độ tính toán.
3.1. Khởi tạo
Có thể khởi tạo Tensor từ mảng, mảng NumPy, ma trận với số chiều cho trước và giá trị ngẫu nhiên hoặc hằng số,...
import torch
import numpy as np
t_1 = torch.tensor([[1,2,3], [3,4,5]])
numpy_arr = np.array([[1,2,3], [3,4,5]])
t_2 = torch.from_numpy(numpy_arr)
t_3 = torch.randn(2, 3)
t_4 = torch.ones(2, 3)
print(f"t_1 = {t_1}")
print(f"t_2 = {t_2}")
print(f"t_3 = {t_3}")
print(f"t_4 = {t_4}")
> Kết quả
t_1 = tensor([[1, 2, 3],
[3, 4, 5]])
t_2 = tensor([[1, 2, 3],
[3, 4, 5]])
t_3 = tensor([[-0.3857, -0.7349, 0.4409],
[-0.1547, 1.1787, 0.2354]])
t_4 = tensor([[1., 1., 1.],
[1., 1., 1.]])
3.2. Thuộc tính
Tensor có các thuộc tính là kích thước, loại dữ liệu, nền tảng tính toán (CPU hoặc GPU, mặc định Tensor khởi tạo với CPU):
tensor = torch.rand(3,4)
print(f"Shape of tensor: {tensor.shape}")
print(f"Datatype of tensor: {tensor.dtype}")
print(f"Device tensor is stored on: {tensor.device}")
> Kết quả
Shape of tensor: torch.Size([3, 4])
Datatype of tensor: torch.float32
Device tensor is stored on: cpu
Để chuyển đổi Tensor từ CPU sang GPU, giả sử cpu_tensor đã được khởi tạo với CPU:
cpu_tensor = torch.rand(3,4)
gpu_tensor_1 = cpu_tensor.to('cuda')
gpu_tensor_2 = cpu_tensor.cuda()
print(f"cpu_tensor = {cpu_tensor}")
print(f"gpu_tensor_1 = {gpu_tensor_1}")
print(f"gpu_tensor_2 = {gpu_tensor_2}")
> Kết quả
cpu_tensor = tensor([[0.9522, 0.1746, 0.9270, 0.2694],
[0.5580, 0.8964, 0.2764, 0.0628],
[0.5281, 0.1664, 0.5416, 0.4946]])
gpu_tensor_1 = tensor([[0.9522, 0.1746, 0.9270, 0.2694],
[0.5580, 0.8964, 0.2764, 0.0628],
[0.5281, 0.1664, 0.5416, 0.4946]], device='cuda:0')
gpu_tensor_2 = tensor([[0.9522, 0.1746, 0.9270, 0.2694],
[0.5580, 0.8964, 0.2764, 0.0628],
[0.5281, 0.1664, 0.5416, 0.4946]], device='cuda:0')
Cách tạo một Tensor sử dụng GPU như trên không tối ưu, tại vì phải tạo một Tensor với CPU, sau đó mới chuyển sang GPU. Cách tốt hơn để khởi tạo một Tensor sử dụng GPU là khởi tạo trực tiếp với tham số 'device' là nền tảng tính toán muốn sử dụng:
gpu_tensor = torch.tensor([1,2,3], device=torch.device('cuda'))
print(f"gpu_tensor = {gpu_tensor}")
> Kết quả
gpu_tensor = tensor([1, 2, 3], device='cuda:0')
3.3. Các phương thức
Có hơn 100 các phương thức với Tensor (transposing, indexing, slicing, mathematical operations, linear algebra, random sampling,... xem thêm ở đây. Sau đây là một số phương thức thường được sử dụng:
Khởi tạo
A = torch.randn(2,4)
print(f"A = {A}")
print(f"Shape of A: {A.shape}")
> Kết quả
A = tensor([[-0.6354, 0.4472, -2.3416, -2.0236],
[-0.0466, -0.1864, -0.5805, 0.8206]])
Shape of A: torch.Size([2, 4])
Ma trận chuyển vị
t = A.t()
print(f"Shape of t: {t.shape}")
> Kết quả
Shape of t: torch.Size([4, 2])
Bình phương mỗi giá trị trong Tensor
t = A**2
print(f"t = {t}")
> Kết quả
t = tensor([[4.0368e-01, 2.0003e-01, 5.4829e+00, 4.0949e+00],
[2.1761e-03, 3.4762e-02, 3.3696e-01, 6.7344e-01]])
Thay đổi kích thước của Tensor
Ba phương thức thường được sử dụng là view(), reshape(), flatten(). flatten() thường dùng để giảm số chiều của Tensor. view() và reshape() có chức năng tương đối giống nhau, vừa có thể tăng hoặc giảm số chiều của Tensor.
# torch.Size([A, B]) -> torch.Size([A*B]):
t = A.flatten() # hoặc A.view(-1), A.reshape(-1)
print(f"t = {t}")
print(f"Shape of t: {t.shape}")
> Kết quả
t = tensor([-0.6354, 0.4472, -2.3416, -2.0236, -0.0466, -0.1864, -0.5805, 0.8206])
Shape of t: torch.Size([8])
# torch.Size([A, B]) -> torch.Size([A, 1, B]):
t = A.view(2, 1, 4) # hoặc A.reshape(2, 1, 4)
print(f"t = {t}")
print(f"Shape of t: {t.shape}")
# Tương tự với torch.Size([A, B, 1]), torch.Size([A, B, 1, 1]), torch.Size([A, B/2, 2]), torch.Size([A/2, B/2, -1]), ...
> Kết quả
t = tensor([[[-0.6354, 0.4472, -2.3416, -2.0236]],
[[-0.0466, -0.1864, -0.5805, 0.8206]]])
Shape of t: torch.Size([2, 1, 4])
Thêm/giảm 1 chiều của Tensor
Ví dụ sử dụng khi inference một input, nhưng model cần đầu vào là một batch, cần thêm chiều để tạo ra input có batch_size=1.
t = A.unsqueeze(0) # dim=0, thêm vào chiều 0 của Tensor
print(f"Shape of t: {t.shape}")
t = t.squeeze(0)
print(f"Shape of t: {t.shape}")
> Kết quả
Shape of t: torch.Size([1, 2, 4])
Shape of t: torch.Size([2, 4])``
Thay đổi chiều của Tensor
torch.transpose(dim0, dim1): thay đổi chiều giữa dim0 và dim1. torch.permute(*dim): thay đổi chiều giữa các hoán vị của các dim. torch.transpose() chỉ thay đổi chiều giữa 2 dim, torch.permute() có thể thay đổi nhiều chiều.
x = torch.randn(2, 3, 5)
t_1 = x.transpose(0,1)
t_2 = x.permute(2,0,1)
print(f"Shape of t_1: {t_1.shape}")
print(f"Shape of t_2: {t_2.shape}")
> Kết quả
Shape of t_1: torch.Size([3, 2, 5])
Shape of t_2: torch.Size([5, 2, 3])
Nối/ghép các Tensors thành một Tensor
torch.stack() hay torch.cat() thường được sử dụng để nối các Tensors có cùng kích thước:
- torch.stack(): Nối các Tensors dọc theo một chiều mới.
- torch.cat(): Nối các Tensors theo một chiều có sẵn.
x_1, x_2 = torch.randn(2, 5), torch.randn(2, 5)
t_1 = torch.cat((x_1, x_2), 1)
t_2 = torch.stack((x_1, x_2))
print(f"Shape of t_1: {t_1.shape}")
print(f"Shape of t_2: {t_2.shape}")
> Kết quả
Shape of t_1: torch.Size([2, 10])
Shape of t_2: torch.Size([2, 2, 5])
Các phương thức trên có nhiều cách sử dụng và nhiều ứng dụng, trên đây là một số ứng dụng cho các phương thức đó.
(Những phần sau đây đang được bổ sung, chỉnh sửa)
4. Autograd
Để huấn luyện một mô hình Neural network cần 2 bước là Forward và Backward. Hàm Backward trong PyTorch được tự động định nghĩa dựa vào cơ chế tự động tính toán đạo hàm - Autograd.
Autograd lưu giá trị và các phép tính toán của các tensors trong một độ thị tính toán. Đồ thị tính toán trong PyTorch (directed acyclic graph - DAG) là một đồ thị có hướng và không có chu kỳ. Đồ thị này có các đỉnh và cạnh (hay cung), mỗi cạnh nối giữa 2 đỉnh và có hướng, các hướng tạo ra không tạo thành một vòng lặp. - nguồn . Trong đồ thị này, input là leaves và output là root. Bằng cách đi từ root đến leaves, autograd tự động tính toán đạo hàm dựa vào chain rule.
Forward:
- Tính toán các phép tính và lưu các kết quả
- Lưu các phép tính toán đạo hàm vào đồ thị tính toán
Backward:
- Tính toán đạo hàm (hàm đạo hàm được lưu trong tensor - .grad_fn)
- Lan truyền ngược với chain rule
Xem ví dụ autograd trong phần 2. Autograd của Google Colab.
5. Neural Networks
Một model có được khởi tạo bằng cách kế thừa lớp torch.nn.Module.
Có 2 phương thức chính bắt buộc phải có trong lớp này là __init__() để khởi tạo và chỉ định các tham số của mô hình, forward() để tính toán đầu ra với dữ liệu đầu vào bằng cách lần lượt chạy qua các layer trong model.
Một số phép tính toán mà không liên quan đến các thông số huấn luyện (trainable parameters) như các phép Activation hay phép Pooling thì thường sử dụng mô-đun torch.nn.functional.
Một phần quan trọng để giúp Pytorch được sử dụng rộng rãi nếu không cung cấp nhiều layers có sẵn được sử dụng thường xuyên. Một số layers như : nn.Linear, nn.Conv2d, nn.MaxPool2d, nn.ReLU, nn.BatchNorm2d, nn.Dropout, nn.Embedding, nn.GRU/nn.LSTM, nn.Softmax, nn.LogSoftmax, nn.MultiheadAttention, nn.TransformerEncoder, nn.TransformerDecoder
Dưới đây là một ví dụ về mô hình NN đơn giản với các lớp Convolutional, Pooling, Fully Connected:
Ngoài các Layers được định nghĩa sẵn, PyTorch cũng cung cấp các mô hình nổi tiếng như VGG, ResNet, DenseNet,... và các pretrained. Một ví dụ khác về việc sử dụng phương pháp transfer learning với pretrained trên ImageNet dataset:
nn.Sequential(*list(backbone.children())[:-1]) sử dụng các lớp để trích xuất đặc trưng. *list(backbone.children())[:-1] loại bỏ lớp FC cuối cùng trong mô hình pretrained (FC(Number_features, 1000)).
6. Dataset, DataLoaders
Dataset
PyTorch cung cấp sẵn các Dataset nổi tiếng trong torch.utils.data.Dataset như CelebA, CIFAR, COCO, EMNIST, Fashion-MNIST, Flickr, HMDB51, ImageNet, MNIST, VOC, WIDERFACE,...
Ví dụ sử dụng Dataset CIFAR có sẵn:
transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])
train_set = torchvision.datasets.CIFAR10(root='./data', train=True, download=True, transform=transform)
test_set = torchvision.datasets.CIFAR10(root='./data', train=False, download=True, transform=transform)
Trong thực tế các dataset có format khác với dataset có sẵn trong thư viện. Việc định nghĩa lại lớp Dataset được sử dụng phổ biến, lớp này kế thừa torch.utils.data.Dataset. Lớp này bắt buộc có 3 lớp là:
- __init__(): khởi tạo các tham số
- __len__(): số lượng dữ liệu
- __getitem__(index): index là chỉ số nằm trong số lượng dữ liệu. Hàm này để đọc, xử lí dữ liệu và trả về input, output của model. Thường các phương pháp Augmentation dữ liệu được thực hiện ở đây.
Ví dụ:
DataLoader
Class Dataset chỉ sinh ra từng sample dữ liệu. Model khi huấn luyện thưởng sử dụng Minibatch. Để dữ liệu thành dạng batch thì sử dụng hàm torch.utils.data.Dataloader:
torch.utils.data.DataLoader(dataset, batch_size=1, shuffle=False, sampler=None, batch_sampler=None, num_workers=0, collate_fn=None, pin_memory=False, drop_last=False, timeout=0, worker_init_fn=None, multiprocessing_context=None, generator=None)
Trong đó:
- dataset: nhận vào class Dataset đã khởi tạo ở trên.
- batch_size: dữ liệu được sinh ra mỗi batch bao nhiêu sample.
- num_workers: khi bạn muốn chạy nhiều tiến trình cùng một lúc.
- collate_fn: Hàm này để định nghĩa cách sắp xếp và kết nối dữ liệu và nhãn tương ứng theo từng lô dữ liệu.
7. Loss, Optimizer
Pytorch hỗ trợ rất nhiều các hàm loss như các bài toán hồi quy, phân loại,... Một số hàm loss phổ biến như là nn.MSE(),nn.CrossEntropyLoss(), nn.KLDivLoss(), nn.BCELoss, nn.NLLLoss().
Ta có thể định nghĩa các hàm Loss khác hoặc kết hợp các hàm Loss lại với nhau.
Pytorch cũng hỗ trợ rất nhiều các hàm tối ưu để tự động Backward khi huấn luyện mô hình Neural network như là torch.optim.Adadelta, torch.optim.Adagrad, torch.optim.RMSprop.
Ví dụ:
optimizer = torch.optim.Adam(params=model.parameters(), lr=0.0001)
Trong đó, tham số params là các tham số sẽ được cập nhật. Đối với trường hợp sử dụng Transfer Learning thì các tham số ở các layers đã được đóng băng (requires_grad=False) không phải cập nhật. Khi đó, sử dụng
params=filter(lambda p: p.requires_grad, model.parameters())
Khi mà loss.backward() được gọi để tính toán đạo hàm, optimizer.step() cập nhật các tham số bằng hàm tối ưu đa khởi tạo.
8. Huấn luyện mô hình
Ở code trên, mô hình chạy 5 epochs và ở mỗi epoch:
- Sử dụng model.train() để thiết lập trạng thái huấn luyện cho mô hình
- Sử dụng vòng for để lấy từng cặp dữ liệu theo từng batch size
- Ở mỗi vòng lặp, chúng ta forward dữ liệu bằng cách sủ dụng model(x_batch), trả về output tương ứng.
- Thực hiện tính toán hàm mất mát đã được định nghĩa trước đó, về hàm loss thì mình sẽ nói ở phần dưới đây, thực hiện bằng cách truyền vào hàm đó hàm mục tiêu và hàm dự đoán, hàm này trả về giá trị mất mát và mình cần tối ưu nó về nhỏ nhất có thể.
- Thực hiện Back-propagation bằng cách sử dụng loss.backward().
- Cập nhật trọng số bằng cách sử dụng optimizer.step().
- Sau khi chạy xong vòng lặp dữ liệu huấn luyện thì sẽ đến với việc đánh giá mô hình bằng cách sử dụng dữ liệu validation.
- Trước tiên thực hiện model.eval() để chuyển trạng thái mô hình sang đánh giá chạy vòng lặp dữ liệu đánh giá
- Ở mỗi vòng lặp ta chỉ forward dữ liệu qua model và tính giá trị loss tại mỗi vòng lặp.
9. Save và Load Model
Có 2 cách lưu model cơ bản trong PyTorch, với mỗi cách lưu model thì có cách load khác nhau:
- Toàn bộ model (kiến trúc + tham số):
torch.save(model, ‘model_path_name.pth’)
---------------------------------------------------
model = torch.load(‘model_path_name.pth')
Khi lưu toàn bộ model, nó lưu tất cả từ nn.Module đến requires_grad của toàn bộ các tham số.
- Chỉ tham số model:
torch.save(model.state_dict(), ‘model_path_name.pth’)
-----------------------------------------------------
model = DefinedModel()
weight = torch.load(‘model_path_name.pth’)
model.load_state_dict(weight)
Ngoài state_dict, có thể lưu thêm optimizer hay scheduler,... để tiếp tục việc huấn luyện:
state = {
'epoch': epoch,
'state_dict': model.state_dict(),
'optimizer': optimizer.state_dict(),
...
}
torch.save(state, ‘model_path_name.pth’)
-------------------------------------------------------
model = DefinedModel()
state = torch.load(‘model_path_name.pth’)
model.load_state_dict(state['state_dict'])
optimizer.load_state_dict(state['optimizer'])
Tài liệu tham khảo:
- https://cs230.stanford.edu/blog/pytorch/
- https://viblo.asia/p/huong-dan-tat-tan-tat-ve-pytorch-de-lam-cac-bai-toan-ve-ai-YWOZrNkNZQ0
- https://learnopencv.com/pytorch-for-beginners-basics/
- https://medium.com/@haophancs/pytorch-c%C6%A1-b%E1%BA%A3n-gi%E1%BB%9Bi-thi%E1%BB%87u-v%E1%BB%81-pytorch-cda2b15086c
- https://github.com/jcjohnson/pytorch-examples
- https://github.com/pytorch/pytorch