PyTorch là thư viện dùng để xây và huấn luyện mạng neural trong khóa học. Dữ liệu trong PyTorch là tensor: mảng nhiều chiều giống mảng NumPy, nhưng chạy được trên GPU. Điểm mới quan trọng hơn là autograd: PyTorch ghi lại các phép tính đã làm để tự tính đạo hàm của loss theo từng trọng số, thay cho việc tính tay như ở bài 0.6. Bài này tính một gradient bằng tay trước, rồi kiểm rằng autograd cho cùng kết quả.
Từ mảng C++ tới tensor
Tensor là một khối số có shape, kiểu dữ liệu (dtype) và nơi lưu (device). Một số có shape (), một vector ba số có shape (3,), bảng hai hàng ba cột có shape (2, 3). Phép tính nào cũng đòi ba thứ này khớp nhau, giống kích thước mảng và kiểu số trong một lời giải CP. Tensor không tự biết chiều nào là học sinh, token hay đặc trưng; người viết code phải tự nhớ.
Trọng số và đặc trưng thường dùng float32; mã lớp hoặc token ID dùng int64, còn gọi là torch.long. Tensor số nguyên không thể đặt requires_grad=True, vì đạo hàm chỉ có nghĩa trên số thực. torch.tensor suy dtype từ dữ liệu, nên chỉ một dấu chấm thập phân cũng đổi kiểu của cả tensor. Ví dụ dưới dùng số nhỏ để tính tay được: một bảng hai mẫu, chiếu sang hai đặc trưng mới.
import torch
torch.set_num_threads(1)
torch.manual_seed(7)
X = torch.tensor([[1., 2., 3.], [4., 5., 6.]])
W = torch.tensor([[1., 0.], [0., 1.], [1., 1.]])
b = torch.tensor([0.5, -0.5])
Y = X @ W + b
print(tuple(X.shape), X.dtype, X.device)
print(Y.tolist())
print(torch.tensor([1, 2, 3]).dtype, torch.tensor([1., 2., 3.]).dtype)
(2, 3) torch.float32 cpu
[[4.5, 4.5], [10.5, 10.5]]
torch.int64 torch.float32
Dòng đầu cho cả shape, dtype và device. Dòng thứ hai gồm hai hàng vì ta vẫn có hai mẫu. Với hàng đầu, đầu ra thứ nhất là ; đầu ra thứ hai là . Bias (2,) được cộng vào từng hàng. @ gộp chiều đặc trưng, còn X * X chỉ bình phương từng ô. Dòng cuối cho thấy dtype đi theo dữ liệu: [1, 2, 3] thành int64, [1., 2., 3.] thành float32.
| Đại lượng | Ý nghĩa | Shape |
|---|---|---|
X |
mẫu, mỗi mẫu đặc trưng | (B, d) |
W |
đầu vào thành đầu ra | (d, h) |
b |
Bias cho từng đầu ra | (h,) |
X @ W + b |
Đặc trưng mới của từng mẫu | (B, h) |
Hỏi nhanh
X có shape (8, 3), W có shape (3, 5). X @ W có shape gì?
Nhân ma trận gộp mất chiều 3 ở giữa, giữ số hàng 8 của X và số cột 5 của W. Chiều batch không biến mất. (3, 3) là lấy nhầm hai chiều bị gộp. (5, 8) là shape của W.T @ X.T. (8, 3, 5) là kết quả của X[:, :, None] * W khi chưa cộng theo chiều 3.
Shape đúng phải đi cùng ý nghĩa đúng
Khi xử lý chuỗi, tensor thường là (B, L, d): batch, độ dài, đặc trưng. reshape chia lại cách nhìn một dãy phần tử; nó không đổi thứ tự logic của dãy. transpose hoặc permute mới hoán đổi các trục. Đây giống khác biệt giữa đánh lại chỉ số của mảng một chiều và chuyển vị thật một bảng hai chiều.
A = torch.arange(6).reshape(2, 3)
print(A.tolist())
print(A.reshape(3, 2).tolist())
print(A.transpose(0, 1).tolist())
print(A.unsqueeze(0).shape, A.sum(dim=1).shape)
[[0, 1, 2], [3, 4, 5]]
[[0, 1], [2, 3], [4, 5]]
[[0, 3], [1, 4], [2, 5]]
torch.Size([1, 2, 3]) torch.Size([2])
So hai bảng (3, 2): reshape gom hai phần tử liên tiếp thành một hàng; transpose lấy các cột cũ làm hàng mới. Shape bằng nhau không bảo đảm dữ liệu mang cùng nghĩa.
unsqueeze(0) thêm chiều batch kích thước 1; sum(dim=1) gộp ba cột của mỗi hàng. squeeze(0) chỉ xóa chiều 0 nếu nó bằng 1. Tránh dùng squeeze() không chỉ định chiều khi batch có thể bằng 1: chiều batch có thể mất ngoài ý muốn.
Vài quy ước gặp liên tục:
- Một batch ảnh màu có shape
(N, C, H, W): 32 ảnh RGB là(32, 3, 64, 64). Thư viện ảnh thường trả(H, W, 3), nên phảipermute(2, 0, 1)trước khi thêm chiều batch. - Gộp theo chiều nào thì chiều đó biến mất: logits
(32, 10)lấy nhãn bằngargmax(dim=1), còndim=0cho 10 số, mỗi số là ảnh có điểm cao nhất của một lớp. torch.catnối theo một chiều có sẵn: hai tensor(3, 4)thành(6, 4).torch.stacktạo chiều mới: thành(2, 3, 4).
Hỏi nhanh
Bảng A có shape (2, 3). Muốn đổi thành (3, 2) sao cho các cột cũ thành hàng mới, ta dùng gì?
reshape và view chỉ chia lại dãy phần tử theo đúng thứ tự cũ, nên với bảng arange(6), hàng đầu vẫn là [0, 1]. transpose đổi chỗ hai trục, nên hàng đầu là [0, 3], đúng là cột cũ. unsqueeze(0) chỉ thêm một chiều dài 1, ra (1, 2, 3).
Broadcasting cũng cần kiểm. Phép cộng (B, h) với (h,) có ý nghĩa bias; phép trừ dự đoán (B, 1) với nhãn (B,) lại tạo (B, B). Máy không hiểu ta định so từng mẫu. Đặt assert prediction.shape == target.shape trước loss hồi quy là một kiểm tra rẻ nhưng hữu ích.
Hỏi nhanh
Dự đoán pred = [[1.], [2.]], shape (2, 1), khớp đúng nhãn target = [1., 2.], shape (2,). Vậy mà ((pred - target) ** 2).mean() lại in ra 0.5. Vì sao?
Hiệu có shape (2, 2), bằng [[0, -1], [1, 0]]: mỗi dự đoán bị trừ cho mọi nhãn. Trung bình bình phương là , trong khi MSE đúng phải bằng 0. Số 1 và 2 được biểu diễn chính xác trong float32, và đổi kiểu hay cách chia cũng không tạo ra chiều thứ hai. Sửa bằng pred.squeeze(-1) kèm một dòng assert. Gặp hai shape này, F.mse_loss chỉ cảnh báo rồi vẫn trả về 0.5.
Autograd lưu đường đi của phép tính
Trong bài linear regression, ta tự viết gradient. Với mạng nhiều lớp, biểu thức dài hơn nhưng mỗi phép cục bộ vẫn nhỏ: nhân, cộng, bình phương, ReLU. Autograd ghép đạo hàm cục bộ bằng quy tắc hàm hợp. Nó tính đạo hàm của phép tính đã thực hiện, không tìm công thức tối ưu và không tự sửa trọng số. So với sai phân hữu hạn ở bài 2.1, autograd cho đạo hàm chính xác và chỉ cần một lượt ngược cho mọi tham số, thay vì mỗi tham số một lượt chạy.
Hình dung phép tính như một DAG trong CP. Forward tính các đỉnh theo thứ tự topo, lưu giá trị cần thiết. Backward đi ngược DAG, truyền độ nhạy của loss về các đầu vào. Nếu một biến góp mặt ở nhiều nhánh, các đóng góp gradient được cộng, giống cộng số đường đi từ các nhánh tới một đỉnh.
PyTorch dựng DAG ngay trong lúc forward chạy và dựng lại ở mỗi lần forward (đồ thị động). Vì vậy if hay vòng lặp Python trong forward không cản autograd: backward đi theo đúng nhánh đã thực sự chạy.
Mỗi phép lưu đúng thứ nó cần cho backward. Sigmoid lưu chính đầu ra vì đạo hàm là , nên sửa tại chỗ như y.add_(1) sau forward sẽ làm backward báo lỗi.
Thử trên một phép tính nhỏ. Đặt , , . Forward cho , . Nếu tăng một lượng rất nhỏ, tăng ba lần lượng đó. Còn nhạy với một lượng . Vì vậy
Gradient dương nói rằng tăng một chút sẽ làm loss tăng tại điểm đang xét. Muốn đi xuống, gradient descent dùng . Với , một bước cho ; đó là phép cập nhật riêng, không thuộc backward().
Với nhiều biến, gradient gom các đạo hàm riêng: tại có gradient .
w = torch.tensor(2., requires_grad=True)
u = 3 * w
J = (u - 1) ** 2
J.backward()
print(round(J.item(), 2), round(w.grad.item(), 2))
with torch.no_grad():
w -= 0.01 * w.grad
print(round(w.item(), 2))
25.0 30.0
1.7
Hai số ở dòng đầu khớp và đã tính tay; dòng sau kiểm phép cập nhật . Vài điều đọc từ đoạn code:
requires_grad=Trueyêu cầu PyTorch ghi lại các phép tính dùng tớiw.wlà tensor lá do ta tạo trực tiếp; gradient được cộng vàow.grad. Tensor trung gian nhưukhông giữ.grad, nhưng đạo hàm vẫn đi qua nó bình thường.Jlà một số (scalar) nênbackward()biết bắt đầu từ độ nhạy 1. Với đầu ra là vector, phải gộp thành một số trước, ví dụ lấy tổng hoặc trung bình.
Hỏi nhanh
Vẫn , nhưng tại . Gradient bằng bao nhiêu, và một bước gradient descent đẩy theo hướng nào?
Tại thì , nên . Gradient âm nghĩa là tăng thì loss giảm. Bước làm tăng, với thì . Hai phương án ra là quên dấu của . Đi theo dấu gradient là gradient ascent, làm loss tăng.
Hỏi nhanh
Cho x = torch.tensor(-3., requires_grad=True), rồi y = 2 * x if x > 0 else x ** 2 và y.backward(). x.grad bằng bao nhiêu?
Với , điều kiện sai, nên chỉ phép được chạy và ghi vào graph. Đạo hàm là . Nhánh 2 * x không chạy nên không có trong graph. Autograd không lấy trung bình các nhánh có thể xảy ra. Đồ thị được dựng trong lúc chạy, nên dùng if ngay trong forward là được.
Gradient được cộng dồn
PyTorch cộng gradient mới vào .grad. Cách này cho phép tích lũy nhiều microbatch, nhưng trong training loop bình thường ta phải xóa trước mỗi bước. Sau backward(), PyTorch giải phóng các giá trị trung gian của graph, nên mỗi lượt dưới đây có forward riêng:
v = torch.tensor(2., requires_grad=True)
((3 * v - 1) ** 2).backward()
print(v.grad.item())
((3 * v - 1) ** 2).backward()
print(v.grad.item())
v.grad = None
((3 * v - 1) ** 2).backward()
print(v.grad.item())
30.0
60.0
30.0
Dãy cho thấy gradient cộng dồn và phép xóa đã hoạt động. Trong training loop, việc xóa do optimizer.zero_grad(set_to_none=True) đảm nhận. Gọi backward() hai lần trên cùng một graph sẽ báo lỗi; đừng sửa bằng retain_graph=True: giữ graph tốn bộ nhớ, không xóa gradient cũ, và graph cũ vẫn mô tả trọng số cũ. Mỗi bước huấn luyện cần forward mới rồi backward mới.
Hỏi nhanh
Một bạn tính loss một lần trước vòng lặp, rồi mỗi epoch gọi loss.backward() và optimizer.step(). Sang epoch thứ hai thì báo RuntimeError: Trying to backward through the graph a second time. Sửa thế nào?
Graph của loss bị giải phóng sau lần backward đầu tiên. Kể cả giữ lại bằng retain_graph=True, graph ấy vẫn được tính từ trọng số ban đầu, nên không cho gradient tại trọng số mới. zero_grad() chỉ xóa .grad, không dựng lại graph. loss.detach() không còn nối với tham số, nên backward báo một lỗi khác. Forward lại ở mỗi bước thì vừa có graph mới, vừa dùng đúng trọng số vừa cập nhật.
Detach, no_grad và bản sao
Khi lưu dự đoán để tính metric, ta không muốn giữ cả đường tính toán. Có ba công cụ:
detach()tạo tensor không nối gradient về nguồn. Nó vẫn dùng chung vùng nhớ với tensor gốc.detach().clone()tạo bản sao độc lập, dùng khi muốn lưu lại giá trị lúc đó.with torch.no_grad():tạm ngừng ghi graph cho các phép trong khối, dùng khi suy luận hoặc cập nhật trọng số bằng tay.
no_grad() không tự chuyển model sang chế độ đánh giá eval() (bài 3.4 và 3.6); ngược lại, eval() cũng không tắt gradient. Lúc đánh giá nên dùng cả hai.
Ta kiểm ý nghĩa “cắt đường gradient” bằng biểu thức . Nếu giữ cả hai nhánh, đạo hàm tại là . Nếu tính rồi detach, hệ số 4 ấy được coi như hằng số:
v.grad = None
square = v ** 2
frozen_square = square.detach()
(frozen_square * v).backward()
print(v.grad.item(), frozen_square.requires_grad)
snapshot = square.detach().clone()
print(snapshot.item())
4.0 False
4.0
Gradient 4 là đúng với graph đã bị cắt, dù giá trị forward vẫn bằng 8. Vì vậy detach không phải cách sửa lỗi backward: nó đổi bài toán, và tham số nằm trước điểm cắt sẽ không học được. Chuyển tensor cần gradient sang NumPy cũng nên viết tensor.detach().cpu().numpy() khi chỉ phục vụ phân tích. Với một số đơn lẻ như loss, loss.item() trả về số Python; ghi log bằng số này không giữ graph.
Cập nhật tham số bằng tay cũng đặt trong no_grad(): with torch.no_grad(): w -= lr * w.grad; viết thẳng thì PyTorch báo lỗi sửa tại chỗ một tensor lá cần gradient. Ngược lại, chạy forward trong no_grad() rồi gọi loss.backward() sẽ báo loss không có grad_fn. Detach còn được dùng có chủ đích: mang trạng thái ẩn của RNN sang đoạn văn bản sau bằng hidden.detach() (bài 3.8).
Hỏi nhanh
w bằng 2 và đã có w.grad bằng 30. Chạy snap = w.detach(), rồi with torch.no_grad(): w -= 0.1 * w.grad. snap.item() in ra gì?
detach() chỉ tách khỏi graph, không sao chép dữ liệu. snap nhìn vào đúng vùng nhớ của w, nên phép trừ tại chỗ hiện ra ở cả hai. Muốn giữ lại giá trị 2 thì phải viết w.detach().clone(). no_grad chỉ ngừng ghi graph, phép cập nhật vẫn chạy. Tensor đã detach vẫn đọc và tính toán bình thường.
Thiết bị và nơi lưu dữ liệu
GPU thực hiện nhiều phép số song song, nhưng mọi tensor trong một phép tính phải cùng device:
- Với tensor,
.to(device)trả về tensor mới trên device đã chọn, nên phải gán lại:X = X.to(device). - Với model,
model.to(device)chuyển tham số của model tại chỗ. - Tensor tạo mới trong
forward, nhưtorch.ones(...), mặc định nằm trên CPU; viếtdevice=x.devicehoặctorch.ones_likeđể nó theo dữ liệu, đừng ghi cứng'cuda'.
Đoạn sau chọn CUDA khi có, nếu không thì dùng CPU; các dòng còn lại không phụ thuộc máy:
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
X_device = X.to(device)
linear = torch.nn.Linear(3, 2).to(device)
with torch.no_grad():
result = linear(X_device)
print(str(device), tuple(result.shape))
print(result.detach().cpu().numpy().shape)
cpu (2, 2)
(2, 2)
Máy chạy bài không có CUDA nên dòng đầu in cpu; trên máy có CUDA, cùng đoạn code chọn cuda mà không phải sửa dòng nào. Dòng hai là đường thường dùng khi lấy kết quả ra phân tích: ngắt graph, chép về CPU, rồi mới sang NumPy.
GPU cũng không tự nhanh hơn: với dữ liệu rất nhỏ, thời gian chép dữ liệu và khởi động có thể lớn hơn phần tính. Bài 3.7 có cách đo thời gian mỗi bước trên máy CUDA.
Hỏi nhanh
Model đã gọi model.to('cuda'). Trong vòng lặp có dòng X.to('cuda'), không gán lại, rồi model(X), và PyTorch báo các tensor nằm trên hai device khác nhau. Vì sao?
Với tensor, .to không đổi tại chỗ, nên phải viết X = X.to('cuda'). Với module, .to chuyển tham số tại chỗ và trả về chính module, nên model.to('cuda') không gán lại vẫn đúng. .to('cuda') đổi được cả device lẫn dtype, và về device thì giống .cuda(). PyTorch không tự giữ batch nhỏ ở CPU.
Khi đi thi
- In shape ở ranh giới dataset, model và loss; đặt
assert pred.shape == target.shape. - Nhãn lớp cho cross-entropy là
long; nhãn hồi quy và nhãn BCE làfloat. - Đặt
deviceở một chỗ và chuyển bằng.to(device), đừng gõ cứng.cuda().
Tóm tắt
- Tensor là mảng kèm shape, dtype và device.
reshapechia lại dãy phần tử,transposeđổi trục; broadcasting có thể lặng lẽ tạo shape sai. - Autograd ghi DAG của forward, rồi cộng gradient theo các đường ở backward.
backward()chưa cập nhật trọng số;.gradcộng dồn nên phải xóa trước mỗi bước.detach()cắt graph,clone()tạo bản sao,.item()lấy ra số Python,no_grad()ngừng ghi graph cho một khối.
Kiểm tra cuối bài
Câu 1
Batch embedding E có shape (4, 6, 8): 4 câu, mỗi câu 6 token, mỗi token 8 đặc trưng. Muốn mỗi câu có một vector bằng trung bình các token, ta gọi E.mean(dim=?), và nhận được shape gì?
Chiều token là chiều 1. Lấy trung bình theo chiều ấy thì còn (4, 8), mỗi câu một vector 8 số. dim=0 trộn 4 câu khác nhau. dim=2 trộn các đặc trưng của từng token. (4, 1, 8) chỉ xuất hiện khi thêm keepdim=True.
Câu 2
a = torch.tensor([1, 2, 3]) và b = torch.Tensor([1, 2, 3]). Hai tensor này có dtype gì?
torch.tensor, viết thường, suy dtype từ dữ liệu: toàn số nguyên thì thành int64. torch.Tensor, viết hoa, là hàm tạo của lớp, luôn dùng dtype mặc định float32. Vì vậy gọi a.mean() hay đặt requires_grad=True cho a đều báo lỗi. Khi cần số thực, hãy viết [1., 2., 3.] hoặc thêm dtype=torch.float32.
Câu 3
x = torch.tensor([1., -2., 3.], requires_grad=True), loss = (x ** 2).mean(), rồi loss.backward(). x.grad bằng bao nhiêu, làm tròn hai chữ số?
, nên . [2, -4, 6] là gradient của tổng bình phương: đổi từ tổng sang trung bình thì gradient bị chia cho số phần tử. [0.33, 1.33, 3.00] là , nhầm giá trị với đạo hàm. 4.67 là chính loss, còn .grad thì luôn cùng shape với x.
Câu 4
Một model khai báo ba lớp fc1, fc2, fc3 trong __init__, nhưng forward chỉ gọi fc1 rồi fc2. Sau loss.backward(), fc3.weight.grad là gì?
Đồ thị tính toán ghi lại các phép đã chạy trong forward, không phải sơ đồ kiến trúc khai báo trong __init__. Backward chỉ đi qua các đỉnh của đồ thị, nên không chạm tới fc3. .grad của nó chưa từng được tạo, nên vẫn là None, không phải tensor 0. Có mặt trong model.parameters() chỉ có nghĩa là optimizer quản lý tham số ấy, và optimizer bỏ qua tham số có .grad bằng None. PyTorch không báo lỗi khi có tham số không dùng tới, nên lỗi quên nối một lớp rất dễ lọt. Kiểm .grad is None sau một batch thử là bắt được.
Câu 5
Huấn luyện full-batch trên dữ liệu cố định. Mỗi epoch gọi optimizer.zero_grad(), tính loss, gọi loss.backward(), nhưng quên optimizer.step(). Loss in ra qua các epoch sẽ thế nào?
backward() tính đạo hàm rồi cộng vào .grad, còn tham số chỉ thay đổi ở optimizer.step(). Không có step, mỗi epoch tính lại cùng một loss trên cùng dữ liệu và cùng trọng số, nên số in ra lặp lại y hệt. Gradient không cộng dồn vì đã có zero_grad(), và graph được giải phóng sau mỗi lần backward. Loss đứng yên tuyệt đối ngay từ epoch đầu là dấu hiệu nên kiểm vòng lặp trước khi đổi learning rate.
Câu 6
Một bạn khởi tạo w = torch.randn(3, requires_grad=True) * 0.01, tính loss rồi gọi backward(). w.grad là None, và PyTorch đưa ra cảnh báo. Vì sao?
requires_grad=True được đặt cho tensor do randn tạo ra. Còn w là tensor trung gian sinh từ phép nhân: w.is_leaf là False, dù w.requires_grad vẫn là True. Gradient chỉ được giữ ở tensor lá, nên w.grad là None, và torch.optim cũng từ chối những tham số không phải lá. Sửa bằng w = (torch.randn(3) * 0.01).requires_grad_(). Gradient nhỏ thì vẫn là một con số, không thành None.
Câu 7
loss là tensor 0 chiều, logits là tensor (32, 10), cả hai còn nối với graph. Ta muốn (a) ghi loss vào log dưới dạng số Python, và (b) giữ logits ở dạng tensor nhưng không giữ graph. Gọi gì?
.item() đổi một tensor chỉ có một phần tử thành số Python, hợp với loss. Gọi trên logits có 320 phần tử sẽ báo lỗi. .detach() trả về tensor cùng shape, không nối với graph và dùng chung vùng nhớ, hợp với logits. loss.detach() thì vẫn là tensor, chưa phải số Python.
Câu 8
Hàm dự đoán tập test chạy preds.append(model(xb)) cho 2000 batch trên GPU. Bộ nhớ tăng dần rồi báo out of memory, dù batch nào cũng rất nhỏ. Sửa thế nào?
Không có no_grad, mỗi output còn nối với graph và các activation trung gian của batch ấy trên GPU. Danh sách preds giữ lại cả 2000 graph. no_grad không ghi graph, còn .cpu() đưa kết quả ra khỏi GPU. zero_grad() chỉ xóa .grad của tham số. Giảm batch chỉ làm chậm thời điểm hết bộ nhớ. empty_cache() chỉ trả lại phần bộ nhớ đệm chưa dùng, không giải phóng được tensor còn đang được giữ.
Câu 9
X có shape (64, 10): 64 mẫu, mỗi mẫu 10 đặc trưng. Để trừ mỗi hàng cho trung bình của chính hàng đó, một bạn viết m = X.mean(dim=1) rồi X - m. Kết quả thế nào?
Broadcasting so shape từ chiều cuối: (64, 10) với (64,) đặt 10 đối diện 64. Hai số khác nhau và không số nào bằng 1, nên phép trừ báo lỗi. Sửa bằng X.mean(dim=1, keepdim=True), có shape (64, 1), để chiều 1 được kéo ra thành 10 cột. (64, 64) chỉ xuất hiện khi một vế có chiều bằng 1, như bẫy (B, 1) trừ (B,). Nguy hiểm hơn là khi số mẫu bằng số đặc trưng, chẳng hạn X có shape (10, 10): phép trừ chạy mà không báo lỗi, nhưng cột lại bị trừ trung bình của hàng .
Câu 10
Model có tham số trên CUDA, X đang ở CPU, và y là nhãn lớp kiểu long dùng cho cross-entropy. Muốn train một batch trên CUDA, ta làm gì?
Input, target và tham số trong cùng một phép tính phải nằm trên cùng device: X = X.to(device) và y = y.to(device). Hàm loss không tự chuyển y. eval() đổi chế độ của các lớp, không đổi device. Đổi y sang float không đổi device, mà với cross-entropy theo chỉ số lớp còn làm sai kiểu: nhãn phải giữ ở long.
Đọc thêm
- PyTorch — Automatic Differentiation: đọc về graph động, gradient cộng dồn và cách tắt theo dõi.
- D2L — Forward/Backward Propagation: xem đồ thị tính toán như cầu nối từ đạo hàm một biến sang mạng.
- PyTorch 2.9 — Tensor: tra shape, dtype, device và các thao tác tensor trước khi ghép model lớn.
Nguồn ảnh: ảnh hoa là ảnh mẫu flower.jpg kèm scikit-learn (vultilion, Flickr, CC BY 2.0).