Ở 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:

  1. 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.
  2. Đạ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.
🧠 CPU vs GPU — Bản chất song song hóa phần cứng
Tại sao các mô hình học sâu bắt buộc phải huấn luyện trên GPU?

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:

tensor_device.py
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)
⚠️ Cạm bẫy: Tính toán chéo thiết bị (Device Mismatch)
PyTorch nghiêm cấm thực hiện các phép toán giữa các Tensor nằm ở các thiết bị phần cứng khác nhau. Nếu bạn thực hiện cộng một Tensor trên CPU với một Tensor trên GPU, chương trình sẽ crash ngay lập tức với lỗi: 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()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:

⚠️ Cạm bẫy: Tensor CPU và mảng NumPy CHIA SẺ CHUNG một vùng nhớ
tensor.numpy()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.
tensor_numpy_shared_memory.py
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().reshape().

🧠 Khái niệm Memory Contiguity (Tính liên tục của bộ nhớ)
Một Tensor được gọi là contiguous (liên tục) nếu các phần tử của nó được xếp đúng theo thứ tự dòng-cột liên tiếp nhau trong RAM vật lý.

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ỗi RuntimeError: 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:

tensor_reshape.py
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í dim chỉ đị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:

$$\frac{dz}{dx} = \frac{dz}{dy} \cdot \frac{dy}{dx} = 2 \cdot 2x = 4x$$
autograd_demo.py
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 đồ thị tính toán với torch.no_grad()
Trong quá trình chạy suy luận (Inference - dự đoán) hoặc cập nhật trọng số thủ công, chúng ta không cần tính đạo hàm. Việc PyTorch liên tục xây dựng đồ thị tính toán động sẽ gây lãng phí bộ nhớ RAM/VRAM cực lớn.

Để 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.
⚠️ Cạm bẫy: Đạo hàm tự động cộng dồn (Accumulating Gradients)
Mặc định, PyTorch sẽ cộng dồn (accumulate) giá trị đạo hàm mới vào thuộc tính .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):

backward_scalar_only.py
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:

autograd_estimation.py
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()
💡 Khối with torch.no_grad() dùng để làm gì?
Khi thực hiện cập nhật trọng số ở đoạn 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

🔑 Bài học đạt đượ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):

Tải về autograd_estimation.py

📖 Tài liệu tham khảo

Bài viết liên quan trong series

Bài 3: Làm việc với Dữ liệu lớn: NumPy & Pandas chuyên sâu Bài 5: Mạng Nơ-ron Đơn Giản (Perceptron & MLP) Quay lại Lộ trình Kỹ Sư AI Thực Chiến

Bình luận