Ở Bài 3, chúng ta đã nắm vững nguyên lý vector hóa và cơ chế phát thanh chiều (Broadcasting) bằng thư viện NumPy. Tuy nhiên, NumPy chỉ hoạt động trên CPU và không hỗ trợ tính toán đạo hàm tự động. Để huấn luyện những kiến trúc học sâu khổng lồ như Transformer hay mạng CNN phức tạp, chúng ta cần một công cụ mạnh mẽ hơn: PyTorch.
Bài học này sẽ hướng dẫn bạn tiếp cận thư viện học sâu PyTorch thông qua cấu trúc dữ liệu cốt lõi Tensor, cách thức kiểm soát chiều không gian dữ liệu bằng cơ chế Reshaping và tìm hiểu chi tiết động cơ tính đạo hàm tự động Autograd - xương sống của quá trình lan truyền ngược (Backpropagation).
4.1 Tensor là gì? Khác gì mảng NumPy?
Về mặt toán học và cấu trúc lưu trữ bộ nhớ, một Tensor của PyTorch tương đồng đến 99% với
mảng ndarray của NumPy. Nó cũng là một mảng đa chiều chứa các phần tử có cùng kiểu dữ liệu
được sắp xếp liên tục trong RAM. Tuy nhiên, Tensor của PyTorch được trang bị hai vũ khí siêu việt chuyên
biệt cho học sâu:
- Tăng tốc phần cứng bằng GPU: PyTorch Tensor có thể dễ dàng chuyển đổi tính toán từ CPU sang các dòng chip đồ họa GPU hiệu năng cao như NVIDIA (thông qua CUDA) hoặc chip Apple Silicon (thông qua MPS) để song song hóa hàng triệu phép toán ma trận cùng lúc.
- Đạo hàm tự động (Autograd): Mỗi Tensor có thể tự theo dõi lịch sử tính toán của chính nó và tự động tính đạo hàm (Gradient) tại bất kỳ mắt xích nào trong đồ thị tính toán.
Bộ vi xử lý CPU được thiết kế để xử lý tuần tự các tác vụ phức tạp với số lượng nhân (Core) rất ít (thường từ 4 đến 64 nhân) nhưng tốc độ mỗi nhân cực kỳ cao và có bộ đệm cache lớn. Trái lại, card đồ họa GPU được thiết kế cho xử lý song song khối lượng cực lớn dữ liệu hình học đơn giản. Nó chứa hàng ngàn nhân tính toán nhỏ (ALU - Arithmetic Logic Unit).
Phép toán cốt lõi của mạng nơ-ron là nhân ma trận lớn, vốn được phân rã thành hàng triệu phép nhân và cộng số học độc lập. GPU có thể phân chia hàng triệu phép toán này cho hàng ngàn nhân ALU chạy đồng thời trong một chu kỳ nhịp, tăng tốc độ huấn luyện mô hình lên gấp hàng chục, hàng trăm lần so với CPU.
Để khởi tạo một Tensor và di chuyển nó sang thiết bị tăng tốc (GPU/MPS), chúng ta sử dụng thuộc tính
device:
import torch
# Khởi tạo Tensor từ danh sách
x = torch.tensor([[1.0, 2.0], [3.0, 4.0]])
# Tự động phát hiện phần cứng tăng tốc
device = (
"cuda" if torch.cuda.is_available()
else "mps" if torch.backends.mps.is_available()
else "cpu"
)
print(f"Thiết bị đang sử dụng: {device}")
# Di chuyển Tensor sang thiết bị đích
x_gpu = x.to(device)
print(x_gpu)
RuntimeError: Expected all tensors to be on the same device.... Hãy luôn nhớ gọi
.to(device) đồng bộ cho mọi biến đầu vào và bộ trọng số trước khi tính toán.
Vì cả hai đều lưu dữ liệu liên tục trong RAM theo cùng cách, PyTorch cho phép chuyển đổi qua lại giữa
Tensor (trên CPU) và mảng NumPy gần như miễn phí bằng .numpy() và
torch.from_numpy() — nhưng "gần như miễn phí" ở đây ẩn chứa một cạm bẫy quan trọng khác:
tensor.numpy() và torch.from_numpy(array) KHÔNG sao chép dữ liệu — chúng chỉ
tạo ra một "cửa sổ nhìn" khác trên CÙNG một vùng RAM (chỉ áp dụng cho Tensor đang ở CPU). Nếu bạn sửa
đổi mảng NumPy, Tensor gốc cũng bị thay đổi theo NGAY LẬP TỨC mà không có bất kỳ cảnh báo nào — đây là
nguồn gốc của rất nhiều bug "dữ liệu tự nhiên bị sai" cực kỳ khó truy trong pipeline tiền xử lý dữ liệu.
import torch
import numpy as np
t = torch.ones(3)
n = t.numpy() # KHÔNG sao chép — n và t trỏ chung 1 vùng RAM
n[0] = 99.0 # Sửa mảng NumPy...
print(t) # ...Tensor gốc CŨNG đổi theo: tensor([99., 1., 1.])
# Muốn tách biệt hoàn toàn, phải chủ động sao chép:
n_independent = t.numpy().copy()
n_independent[0] = -1.0
print(t) # Tensor gốc KHÔNG đổi lần này
4.2 Biến đổi hình dạng (Tensor Reshaping & View)
Trong thiết kế mạng nơ-ron, việc biến đổi hình dạng (Shape/Dimension) của dữ liệu xảy ra liên tục ở từng
tầng (Layer). PyTorch cung cấp hai phương thức biến đổi chiều quan trọng nhất là .view() và
.reshape().
Khi chúng ta gọi phép toán chuyển vị
transpose() hoặc t() hoặc
permute(), PyTorch không hề sắp xếp lại dữ liệu trong RAM (vì sẽ làm chậm hệ thống). Nó chỉ
thay đổi cách tính toán chỉ số tọa độ (metadata). Điều này làm Tensor bị mất tính liên tục
(non-contiguous).
-
.view(): Chỉ tạo ra một "góc nhìn" mới trên cùng vùng RAM gốc cũ. Do không sao chép dữ liệu,view()bắt buộc Tensor gốc phải contiguous. Nếu không, nó sẽ báo lỗiRuntimeError: view size is not compatible with input tensor's size and stride. -
.reshape(): Hoạt động an toàn hơn. Nếu Tensor gốc contiguous, nó trả về một view (không sao chép). Nếu Tensor gốc non-contiguous, nó tự động gọi ngầm hàm sao chép.clone().contiguous()để tạo ra một vùng nhớ liên tục mới rồi mới thay đổi chiều.
Hãy phân tích ví dụ đổi chiều Tensor sau:
import torch
# Khởi tạo Tensor kích thước (2, 3)
a = torch.tensor([[1, 2, 3], [4, 5, 6]])
# Đổi chiều sang (6, 1) bằng .view() (Không tốn thêm RAM)
b = a.view(6, 1)
# Thử nghiệm phép chuyển vị .t() -> Kích thước (3, 2)
# Chuyển vị làm đảo lộn cách xếp hàng dữ liệu trong RAM -> Non-contiguous
a_t = a.t()
# Sử dụng .view() ở đây sẽ crash! Ta phải dùng .reshape() hoặc gọi .contiguous() trước
# Cách 1: dùng .reshape()
c = a_t.reshape(6, 1)
# Cách 2: chuyển contiguous trước rồi view
d = a_t.contiguous().view(6, 1)
Bên cạnh đó, ta có hai hàm hỗ trợ thêm/bớt chiều ảo vô cùng tiện lợi:
-
.squeeze(): Loại bỏ mọi chiều có kích thước bằng 1. Ví dụ mảng shape(1, 5, 1)sau khi squeeze sẽ thành(5,). -
.unsqueeze(dim): Thêm một chiều ảo kích thước bằng 1 tại vị trídimchỉ định. Ví dụ mảng shape(5,)gọi.unsqueeze(1)sẽ thành(5, 1).
4.3 Cơ chế tự động tính đạo hàm (Autograd & Đồ thị tính toán)
Trong học máy, để tối ưu hóa trọng số theo hướng giảm sai số, ta bắt buộc phải tính đạo hàm riêng (Partial Derivatives) của hàm sai số đối với từng tham số. PyTorch giải quyết triệt để bài toán này bằng động cơ Autograd.
Khi bạn khởi tạo một Tensor và đặt thuộc tính requires_grad=True, PyTorch sẽ bắt đầu theo dõi
mọi phép toán tác động lên Tensor đó. Trong quá trình chạy xuôi (Forward pass), PyTorch sẽ tự động dựng
lên một Đồ thị tính toán động (Dynamic Computation Graph) dưới dạng đồ thị có hướng không
chu trình (DAG).
Mỗi biến được sinh ra từ các phép toán sẽ lưu giữ thuộc tính grad_fn (trỏ tới toán tử đã sinh
ra nó, ví dụ <PowBackward0> hay <AddBackward0>). Các nút lá (Leaf
nodes) chính là các Tensor đầu vào tự định nghĩa không do phép toán nào tạo ra sẽ có
grad_fn = None.
Hãy xem đồ thị tính toán đơn giản sau:
$$y = x^2 \quad \implies \quad z = 2y + 3$$
Khi ta gọi phương thức z.backward(), PyTorch sẽ tự động duyệt ngược đồ thị từ kết quả $z$
quay trở lại $x$ để tính đạo hàm theo quy tắc chuỗi (Chain Rule) trong giải tích:
import torch
# Bật tính năng theo dõi đạo hàm cho x
x = torch.tensor(3.0, requires_grad=True)
# forward pass
y = x ** 2
z = 2 * y + 3
# backward pass (Lan truyền ngược tự động)
z.backward()
# In kết quả đạo hàm dz/dx tại x=3 (Mong đợi: 4*3 = 12)
print(f"Đạo hàm dz/dx tại x=3 là: {x.grad.item():.1f}")
Để vô hiệu hóa cơ chế này, chúng ta sử dụng khối lệnh
with torch.no_grad(): hoặc gọi phương
thức tensor.detach() để cắt đứt liên kết của Tensor khỏi đồ thị tính toán hiện tại. Điều
này giải phóng bộ nhớ và tăng tốc độ xử lý lên tối đa.
.grad sau mỗi lần gọi .backward(). Điều này hữu ích trong huấn luyện mạng quy
mô lớn, nhưng đối với các vòng lặp tối ưu hóa thông thường, nó sẽ làm sai lệch hoàn toàn hướng đi của
Gradient Descent. Bạn bắt buộc phải gọi .grad.zero_() hoặc
optimizer.zero_grad() để reset đạo hàm về 0 sau mỗi vòng cập nhật.
Một lỗi runtime khác cũng cực kỳ phổ biến với người mới: gọi .backward() trên một Tensor
KHÔNG PHẢI số vô hướng (scalar):
import torch
x = torch.tensor([1.0, 2.0, 3.0], requires_grad=True)
y = x * 2 # y là Tensor 3 phần tử, KHÔNG phải 1 số đơn lẻ
try:
y.backward() # ❌ Lỗi!
except RuntimeError as e:
print("Lỗi:", e)
# Lỗi: grad can be implicitly created only for scalar outputs
# ✅ Cách sửa 1: rút gọn về scalar trước khi backward (cách phổ biến nhất — mọi
# hàm Loss trong huấn luyện thật đều làm việc này, ví dụ .sum() hoặc .mean())
y.sum().backward()
print(x.grad) # tensor([2., 2., 2.])
# ✅ Cách sửa 2: nếu thực sự cần gradient riêng cho từng phần tử, truyền
# một "vector trọng số" cùng shape với y vào backward()
x.grad.zero_()
y2 = x * 2
y2.backward(torch.tensor([1.0, 1.0, 1.0]))
print(x.grad) # tensor([2., 2., 2.]) — tương đương .sum().backward() ở đây
Đây là lý do vì sao mọi hàm Loss trong huấn luyện thật (MSE, Cross-Entropy...) đều trả về đúng 1 con số vô
hướng đại diện cho toàn bộ batch — không phải ngẫu nhiên mà PyTorch yêu cầu vậy, mà vì
.backward() mặc định chỉ biết cách lan truyền ngược từ 1 điểm gốc duy nhất.
4.4 Dự án thực hành bài 4: Xấp xỉ hàm số đa thức bằng Autograd
Để kiểm nghiệm sức mạnh của Autograd, chúng ta sẽ tự xây dựng một ứng dụng tối ưu hóa: Sử dụng đa thức bậc 3 để xấp xỉ (fitting) đường hình Sin.
Công thức đa thức bậc 3 cần học của chúng ta là:
$$\hat{y} = w_1 \cdot x + w_2 \cdot x^2 + w_3 \cdot x^3 + b$$Trong đó $w_1, w_2, w_3, b$ là các tham số sẽ được cập nhật liên tục ngược chiều Gradient để giảm thiểu sai số.
Bạn có thể tải file Python hoàn chỉnh chứa mã nguồn thực hành chi tiết ở khung bên dưới:
import torch
import math
# Tạo dữ liệu giả lập cho hàm Sin trong khoảng [-pi, pi]
# Dữ liệu sẽ được sinh ngẫu nhiên kèm nhiễu (noise)
# Sử dụng công thức bậc 3: y = w1*x + w2*x^2 + w3*x^3 + b
def generate_data(num_samples=2000):
# Khởi tạo x phân bố đều trong khoảng [-pi, pi]
x = torch.linspace(-math.pi, math.pi, num_samples, dtype=torch.float32)
# y = sin(x) + nhiễu Gaussian ngẫu nhiên
y = torch.sin(x) + 0.1 * torch.randn(num_samples)
return x, y
def train_autograd():
# 1. Khởi tạo dữ liệu
x, y = generate_data()
# 2. Khởi tạo ngẫu nhiên các trọng số cần học và đặt requires_grad=True để bật Autograd
# w1, w2, w3, b đại diện cho các trọng số của hàm đa thức bậc 3
w1 = torch.randn((), dtype=torch.float32, requires_grad=True)
w2 = torch.randn((), dtype=torch.float32, requires_grad=True)
w3 = torch.randn((), dtype=torch.float32, requires_grad=True)
b = torch.randn((), dtype=torch.float32, requires_grad=True)
# Siêu tham số tối ưu hóa
learning_rate = 1e-6
epochs = 2000
print("=== Bắt đầu tối ưu hóa đa thức bằng PyTorch Autograd ===")
print(f"Trọng số ban đầu: w1={w1.item():.4f}, w2={w2.item():.4f}, w3={w3.item():.4f}, b={b.item():.4f}\n")
for epoch in range(1, epochs + 1):
# Forward pass: Tính toán giá trị y dự đoán từ đa thức bậc 3
# y_pred = w1*x + w2*x^2 + w3*x^3 + b
y_pred = w1 * x + w2 * (x ** 2) + w3 * (x ** 3) + b
# Tính toán sai số bình phương trung bình (Mean Squared Error - MSE) làm hàm loss
loss = (y_pred - y).pow(2).sum()
# Backward pass: Tự động tính gradient của loss đối với tất cả tensor có requires_grad=True
# PyTorch sẽ tự xây dựng Computation Graph và thực hiện quy tắc Chain Rule
loss.backward()
# Cập nhật trọng số thủ công (không lưu vết gradient trong bước cập nhật này)
with torch.no_grad():
w1 -= learning_rate * w1.grad
w2 -= learning_rate * w2.grad
w3 -= learning_rate * w3.grad
b -= learning_rate * b.grad
# QUAN TRỌNG: Giải phóng bộ đệm gradient (zero grad) sau mỗi lần cập nhật
# Nếu không xóa, PyTorch sẽ tự cộng dồn gradient mới vào gradient cũ
w1.grad.zero_()
w2.grad.zero_()
w3.grad.zero_()
b.grad.zero_()
if epoch % 200 == 0:
print(f"Epoch {epoch:4d} | Loss: {loss.item():.4f}")
print(f"\n=== Kết quả tối ưu tìm được ===")
print(f"Hàm đa thức xấp xỉ: y_pred = {w1.item():.4f}*x + {w2.item():.4f}*x^2 + {w3.item():.4f}*x^3 + {b.item():.4f}")
print(f"Hàm đích thực tế: y = sin(x)")
# Đo đếm độ chính xác
with torch.no_grad():
final_y_pred = w1 * x + w2 * (x ** 2) + w3 * (x ** 3) + b
final_loss = (final_y_pred - y).pow(2).mean()
print(f"Sai số trung bình cuối cùng (MSE Mean): {final_loss.item():.6f}")
if __name__ == "__main__":
train_autograd()
w1 -= learning_rate * w1.grad phía trên, ta đang
thực hiện các phép toán số học thông thường chứ không phải các lớp tính toán của mạng. Nếu không bọc
trong with torch.no_grad():, PyTorch sẽ tiếp tục theo dõi các phép toán này và tự vẽ thêm
các nhánh vô nghĩa vào Đồ thị tính toán, làm tràn bộ nhớ GPU và gây lỗi logic gradient.
Tóm tắt bài học & Cầu nối kiến thức
- Đạt được: Hiểu bản chất cấu trúc dữ liệu Tensor của PyTorch và cách nó tăng tốc song song trên GPU.
- Đạt được: Nắm vững cơ chế đồ thị tính toán (Computation Graph) và tính đạo hàm tự động Autograd để lan truyền lỗi.
Cầu nối bài tiếp theo: Từ các viên gạch Tensor đơn lẻ này, ở Bài số 5 chúng ta sẽ chính thức lắp ráp chúng lại để tạo nên mô hình Mạng Nơ-ron đa lớp đầu tiên (MLP) hoàn chỉnh.
Tải file code thực hành minh họa bài học
File Python autograd_estimation.py — mã nguồn tối ưu hóa đa thức bậc 3 để khớp đồ thị đường
hình Sin sử dụng động cơ đạo hàm tự động Autograd của PyTorch (chạy
python autograd_estimation.py, yêu cầu cài đặt thư viện torch):
Bình luận