Huấn luyện mạng nơ-ron: vanishing/exploding gradient, BatchNorm, Dropout
This content is not available in your language yet.
Mục tiêu
Phần tiêu đề “Mục tiêu”Sau bài này, bạn sẽ:
- Tự dẫn được lan truyền ngược (backpropagation) cho MLP 2 lớp ở dạng ma trận, với đại lượng , và kiểm tra nó bằng kiểm tra gradient (gradient checking).
- Giải thích được vì sao gradient tiêu biến (vanishing gradient) hoặc bùng nổ (exploding gradient) khi mạng sâu, và vì sao khởi tạo Xavier và He chữa được phần lớn chuyện này.
- Hiểu BatchNorm và Dropout làm gì khi huấn luyện và khi dự đoán, và vì sao phải gọi
model.eval(). - Biết khi nào dùng weight decay, dừng sớm (early stopping) và cắt chuẩn gradient (gradient clipping).
Cần biết trước
Phần tiêu đề “Cần biết trước”- Neural Networks: Perceptron, MLP, Forward Propagation, Activation Functions, in Deep Learning(Not translated yet) (Learned)
- Advanced Optimizers: SGD, Momentum, RMSProp, Adam, in Machine Learning (Learned)
- Ridge, Lasso, and Elastic Net, in Machine Learning (Learned)
Trực giác
Phần tiêu đề “Trực giác”Bạn đã biết lan truyền xuôi: đưa vào, qua từng lớp, ra . Để học, gradient descent cần đạo hàm của hàm mất mát theo mọi tham số. Một mạng nhỏ cho ảnh chữ số 8×8, ba lớp ẩn 256 nơ-ron, đã có khoảng 150 nghìn tham số.
Cách ngây thơ: nhích từng tham số
Phần tiêu đề “Cách ngây thơ: nhích từng tham số”Ở bài Gradient Descent, bạn đã ước lượng độ dốc bằng cách nhích một tham số một chút rồi tính lại mất mát. Với 150 nghìn tham số, mỗi bước cập nhật cần ít nhất 150 nghìn lượt lan truyền xuôi. Quá chậm. Ta cần một cách tính cả 150 nghìn đạo hàm với chi phí chỉ cỡ vài lượt lan truyền xuôi. Đó là lan truyền ngược.
Trò chơi truyền tin
Phần tiêu đề “Trò chơi truyền tin”Lan truyền ngược chuyền “lời trách” từ lớp ra về lớp vào: lớp ra biết mình sai bao nhiêu, nó báo cho lớp trước, lớp trước báo tiếp. Mỗi lần chuyền, tin bị nhân với một hệ số của lớp đó.
Giống trò chơi truyền tin qua 10 người. Nếu ai cũng chỉ nói to bằng một phần tư người trước, người cuối nghe được âm lượng ban đầu: gần như im lặng. Nếu ai cũng nói to gấp đôi, người cuối bị hét vào tai to gấp lần. Chỉ khi mỗi người giữ đúng âm lượng, tin mới đi xa được.
Với mạng sâu cũng vậy. Các lớp đầu nhận gradient rất nhỏ thì gần như đứng yên (tiêu biến), nhận gradient rất lớn thì nhảy lung tung tới mức tràn số (bùng nổ). Thêm nữa, mạng lớn đủ sức học thuộc tập train, nên còn phải chống overfitting.
Bài toán của bài này là: tính gradient của mọi tham số một cách hiệu quả, giữ cho tín hiệu gradient không tắt cũng không bùng lên khi đi qua nhiều lớp, và giữ cho mạng lớn không học thuộc dữ liệu.
Lý thuyết
Phần tiêu đề “Lý thuyết”Quy tắc hàm hợp trên một chuỗi phép tính
Phần tiêu đề “Quy tắc hàm hợp trên một chuỗi phép tính”Bắt đầu với mạng chỉ có một nơ-ron mỗi lớp, mọi thứ là số vô hướng:
Muốn biết đổi bao nhiêu khi nhích một chút, ta lần theo chuỗi: đổi làm đổi, làm đổi, làm đổi, làm đổi. Quy tắc hàm hợp (chain rule, còn gọi quy tắc dây chuyền) nói tỉ lệ đổi của cả chuỗi là tích các tỉ lệ đổi của từng mắt xích:
Mỗi thừa số đọc ra từ đúng một mắt xích: nên ; nên ; nên .
Để ý: tính cũng cần . Phần đầu của chuỗi được dùng chung. Vì vậy ta tính từ cuối về đầu, lưu lại kết quả ở mỗi lớp, và lớp trước dùng lại kết quả của lớp sau. Đó là toàn bộ bí quyết của lan truyền ngược.
Lan truyền ngược cho MLP 2 lớp
Phần tiêu đề “Lan truyền ngược cho MLP 2 lớp”Giờ đến một MLP thật, với một mẫu , :
có phần tử, lớp ẩn có nơ-ron, nên có kích thước . Lớp ra có một nơ-ron sigmoid, nên có kích thước . Hàm mất mát là cross-entropy nhị phân:
Đại lượng trung tâm là : mất mát nhạy thế nào với giá trị trước hàm kích hoạt của lớp . Nó là “lời trách” được chuyền ngược. cùng kích thước với .
Bước 1: ở lớp ra. Đạo hàm của theo , rồi quy đồng:
Đạo hàm của sigmoid là , nên . Nhân hai thừa số, mẫu số triệt tiêu:
Gọn đến bất ngờ: dự đoán trừ nhãn. Sigmoid đi với cross-entropy (và softmax đi với cross-entropy nhiều lớp) đều cho kết quả này, và đó là một lý do chúng hay đi cặp.
Bước 2: gradient của lớp ra. Phần tử thứ của chỉ đi vào qua số hạng , nên . Nhân với theo quy tắc hàm hợp, rồi xếp lại thành hàng:
Bước 3: chuyền về lớp ẩn. Nơ-ron ẩn ảnh hưởng tới qua , mà đi vào với hệ số . Vậy . Khi lớp sau có nhiều nơ-ron, đi vào tất cả, và ta cộng các đường lại: , chính là phần tử của . Cuối cùng qua hàm kích hoạt, nhân với :
Dấu là nhân từng phần tử, vì mỗi nơ-ron có hàm kích hoạt riêng của nó, không trộn với nơ-ron khác.
Bước 4: gradient của lớp ẩn. Lặp lại Bước 2, với đóng vai :
là cột phần tử, là hàng phần tử, nên tích là ma trận , đúng kích thước của . Kiểm kích thước như vậy là cách nhanh nhất để bắt lỗi chuyển vị.
Chi phí của lan truyền ngược chỉ cỡ hai lần lan truyền xuôi, bất kể có bao nhiêu tham số. Đổi lại, phải giữ mọi trong bộ nhớ cho tới lúc đi ngược; đó là lý do huấn luyện tốn bộ nhớ hơn dự đoán nhiều. PyTorch, TensorFlow, JAX làm việc này tự động (autograd), nhưng chúng làm đúng các bước trên.
Kiểm tra gradient bằng sai phân
Phần tiêu đề “Kiểm tra gradient bằng sai phân”Lan truyền ngược tự viết rất dễ sai một chỗ chuyển vị hay một thừa số , mà mạng vẫn chạy, mất mát vẫn giảm chút ít. Cách bắt lỗi: tính lại từng đạo hàm bằng sai phân trung tâm (central difference), với một bước nhỏ :
Ở tử số, chỉ được nhích, mọi tham số khác giữ nguyên. Sai phân trung tâm có sai số cỡ , chính xác hơn nhiều so với sai phân một phía (sai số cỡ ). Với số thực 64 bit, là hợp lý: nhỏ hơn nữa thì sai số làm tròn ở phép trừ lại lớn lên.
So gradient lan truyền ngược với gradient sai phân bằng sai số tương đối, để không phụ thuộc gradient lớn hay nhỏ:
Cỡ hoặc nhỏ hơn là đúng; cỡ trở lên gần như chắc chắn có lỗi. Mỗi tham số cần hai lượt lan truyền xuôi, nên chỉ kiểm trên mạng nhỏ, vài mẫu, rồi tắt đi khi huấn luyện thật.
Gradient tiêu biến và bùng nổ
Phần tiêu đề “Gradient tiêu biến và bùng nổ”Viết công thức lặp lại từ lớp ra về lớp 1. Mỗi lần lùi một lớp, bị nhân với rồi với . Trở lại mạng một nơ-ron mỗi lớp cho dễ thấy:
Một tích gồm thừa số. Nếu mỗi thừa số cỡ , mười lớp cho ; nếu cỡ 2 thì . Tích của nhiều số hoặc co về 0, hoặc phình rất nhanh; chỉ khi các thừa số quanh 1 thì nó mới giữ được cỡ. Gradient của là , nên các lớp đầu chịu ảnh hưởng nặng nhất.
- Tiêu biến: các lớp đầu gần như không học. Mất mát giảm chậm hoặc dừng hẳn, dù các lớp cuối vẫn thay đổi.
- Bùng nổ: một bước cập nhật quá lớn đẩy tham số ra xa, mất mát nhảy vọt, rồi thành
infhoặcnan.
Vì sao sigmoid dễ gây tiêu biến. Đặt , thì . Đây là parabol theo , lớn nhất ở (tức ):
Vậy mỗi lớp sigmoid co ít nhất 4 lần, trừ khi trọng số lớn bù lại. Khi lớn, sigmoid bão hòa (saturate): gần 0 hoặc 1, gần 0, còn tệ hơn. Tanh có nên đỡ hơn, nhưng cũng bão hòa. ReLU có đạo hàm đúng bằng 1 ở phần dương, nên tự nó không co tín hiệu; lúc này trọng số quyết định tất cả, tức là cách khởi tạo.
Khởi tạo Xavier và He
Phần tiêu đề “Khởi tạo Xavier và He”Không khởi tạo toàn số 0. Nếu mọi trọng số của một lớp bằng nhau, mọi nơ-ron của lớp đó tính ra cùng một giá trị, nhận cùng một , nên được cập nhật y hệt nhau. Chúng mãi là bản sao của nhau, và lớp 256 nơ-ron chỉ làm việc của một nơ-ron. Khởi tạo ngẫu nhiên để phá vỡ đối xứng (symmetry breaking). Câu hỏi còn lại là ngẫu nhiên với độ lệch chuẩn bao nhiêu.
Giữ phương sai qua mỗi lớp. Xét , với là số nơ-ron vào. Giả sử các độc lập, trung bình 0, phương sai , và độc lập với các . Mỗi số hạng có phương sai , và phương sai của tổng các số hạng độc lập là tổng các phương sai:
Muốn tín hiệu không to lên cũng không nhỏ đi qua mỗi lớp, ta chọn để ở lớp này bằng ở lớp trước.
- Hàm kích hoạt gần tuyến tính quanh 0 (tanh): , nên , cần . Lượt lan truyền ngược nhân với nên đòi . Khởi tạo Xavier (Glorot và Bengio, 2010) lấy trung bình hai đòi hỏi:
- ReLU: với đối xứng quanh 0, ReLU đặt nửa số giá trị về 0, nên . Để bù nửa bị mất, khởi tạo He (He và cộng sự, 2015, còn gọi Kaiming) chọn:
Lấy lớp vuông 64 → 64 làm ví dụ. Mỗi lần lùi một lớp ReLU, phương sai của bị nhân với . Với là , tức độ lệch chuẩn nhân mỗi lớp: bùng nổ. Với He, : giữ nguyên. Ô code thứ hai sẽ đo đúng hai con số này.
Xavier dẫn ra cho hàm gần tuyến tính quanh 0, mà sigmoid quanh 0 có độ dốc chỉ , nên Xavier với sigmoid vẫn tiêu biến. Đây là một lý do mạng sâu ngày nay dùng ReLU (hoặc GELU) cùng He. nn.Linear của PyTorch mặc định lấy trọng số đều trong , tức phương sai , nhỏ hơn He; muốn đúng He thì gọi nn.init.kaiming_normal_.
BatchNorm
Phần tiêu đề “BatchNorm”Khởi tạo tốt chỉ giữ cỡ tín hiệu lúc đầu. Trong lúc huấn luyện, trọng số thay đổi, và phân phối của trôi đi. Chuẩn hóa theo batch (batch normalization, BatchNorm, BN; Ioffe và Szegedy, 2015) ép lại cỡ tín hiệu ở mỗi bước.
Xét một nơ-ron trong một mini-batch mẫu, với các giá trị . BatchNorm tính trung bình và phương sai của batch, rồi chuẩn hóa:
là một số rất nhỏ (PyTorch mặc định ) để không chia cho 0. Sau bước này, các có trung bình 0, phương sai gần 1. Cuối cùng co giãn và dịch:
Vì sao cần , . Ép mọi nơ-ron về trung bình 0, phương sai 1 có thể quá gò bó: một sigmoid sau đó sẽ chỉ làm việc ở vùng gần tuyến tính. và là tham số học được, mỗi nơ-ron một cặp, cho mạng tự chọn cỡ và vị trí phù hợp. Nếu cần, mạng có thể học , để trả lại đúng ban đầu. Vì BN trừ đi trung bình, hệ số tự do của lớp tuyến tính đứng trước bị triệt tiêu, nên thường bỏ nó; làm thay việc đó.
BN thường đặt sau lớp tuyến tính và trước hàm kích hoạt, như bài báo gốc. Mỗi nơ-ron có , , , riêng.
Khi huấn luyện và khi dự đoán. Lúc dự đoán, kết quả cho một ảnh không được phụ thuộc vào các ảnh khác tình cờ chung batch, và batch có thể chỉ có một ảnh. Vì vậy, trong lúc huấn luyện, BN giữ thêm trung bình động (running average) của và qua các batch:
và tương tự cho phương sai. PyTorch gọi là momentum, mặc định (khác momentum của bộ tối ưu). Lúc dự đoán, BN dùng , thay cho thống kê của batch. Chuyển giữa hai chế độ là việc của model.train() và model.eval().
BN giúp gì. Mạng có BN chịu được tốc độ học lớn hơn, ít nhạy với khởi tạo, và hội tụ nhanh hơn. Thống kê của batch có nhiễu, nên BN còn có tác dụng điều chuẩn nhẹ. Bài báo gốc giải thích bằng việc giảm “internal covariate shift” (phân phối đầu vào của mỗi lớp trôi đi), nhưng Santurkar và cộng sự (2018) cho thấy lý do chính có lẽ là BN làm mặt mất mát trơn hơn. Điểm yếu: batch nhỏ thì , nhiễu. Transformer vì vậy dùng chuẩn hóa lớp (layer normalization), chuẩn hóa trên các nơ-ron của một mẫu, xem bài Transformer.
Dropout
Phần tiêu đề “Dropout”Mạng lớn có thể “học thuộc” tập train, như đường bậc 15 trong bài Overfitting và underfitting. Một kiểu học thuộc đặc trưng của mạng nơ-ron: vài nơ-ron dựa vào nhau rất chặt, nơ-ron này chỉ có ích khi nơ-ron kia ở đúng chỗ (co-adaptation). Dropout (Srivastava và cộng sự, 2014) phá kiểu dựa dẫm này bằng một cách đơn giản đến lạ: ở mỗi bước huấn luyện, tắt ngẫu nhiên mỗi nơ-ron với xác suất .
Với một lớp có đầu ra , lấy một mặt nạ mà mỗi phần tử độc lập, bằng 1 với xác suất và bằng 0 với xác suất . Bản dùng phổ biến, inverted dropout, chia thêm cho :
Vì sao chia cho . Mỗi phần tử có kỳ vọng . Lớp sau nhận tín hiệu cùng cỡ trung bình như khi không có dropout. Nhờ vậy, lúc dự đoán, Dropout không làm gì cả: giữ nguyên , không tắt nơ-ron nào, không nhân gì. Bài báo gốc làm ngược lại (giữ nguyên lúc huấn luyện, nhân trọng số với lúc dự đoán); hai cách cho cùng kỳ vọng.
Khi lan truyền ngược, nơ-ron bị tắt có bằng 0, nên trọng số nối vào nó không đổi ở bước đó. Mỗi bước huấn luyện vì vậy là huấn luyện một mạng con ngẫu nhiên, và mạng lúc dự đoán giống như trung bình của rất nhiều mạng con. Giá trị hay dùng: cho lớp ẩn của MLP lớn (như bài báo gốc), khoảng trong Transformer.
Weight decay, dừng sớm, cắt chuẩn gradient
Phần tiêu đề “Weight decay, dừng sớm, cắt chuẩn gradient”Ba kỹ thuật ngắn nhưng gần như mạng nào cũng dùng.
Weight decay. Thêm phạt L2 như Ridge ở bài Ridge, Lasso, Elastic Net. Gradient của khoản phạt là , nên bước gradient descent thành:
Vế phải cho thấy tên gọi: trước mỗi bước, trọng số bị co lại theo một tỉ lệ cố định, “phân rã” dần về 0 nếu dữ liệu không kéo nó ra. Tham số weight_decay của các bộ tối ưu trong PyTorch cộng thẳng vào gradient, tức . Với Adam, cộng vào gradient không còn tương đương với co trọng số, vì Adam chia lại gradient; AdamW (Loshchilov và Hutter, 2019) tách phần co trọng số ra riêng. Thường không phạt , , .
Dừng sớm (early stopping). Sau mỗi epoch, đo mất mát trên tập kiểm định (validation set). Mất mát kiểm định thường giảm rồi tăng lại khi mạng bắt đầu học thuộc. Lưu lại tham số ở epoch tốt nhất; nếu sau một số epoch (gọi là độ kiên nhẫn, patience) vẫn không tốt hơn thì dừng, và dùng bản đã lưu.
Cắt chuẩn gradient (gradient clipping). Chặn bùng nổ trực tiếp: nếu chuẩn của cả vectơ gradient vượt ngưỡng , co nó lại:
Hướng giữ nguyên, chỉ độ dài bị giới hạn, nên một batch xấu không thể đẩy tham số đi quá xa. Trong PyTorch là torch.nn.utils.clip_grad_norm_(model.parameters(), c), gọi sau backward() và trước step(). Kỹ thuật này đặc biệt quan trọng với mạng hồi tiếp (Pascanu và cộng sự, 2013), xem bài Mạng nơ-ron hồi tiếp (RNN).
Giới hạn
Phần tiêu đề “Giới hạn”- Khởi tạo tốt và BN chưa đủ cho mạng rất sâu (hàng trăm lớp). Kết nối tắt (residual connection) của ResNet cho gradient một đường đi thẳng, xem bài Các kiến trúc CNN hiện đại.
- Bài này không bàn chọn bộ tối ưu và lịch tốc độ học. Momentum, Adam, warmup ở bài Các thuật toán tối ưu nâng cao.
- Phân tích phương sai dựa trên giả định độc lập. Nó cho đúng cỡ lúc khởi tạo, nhưng không đảm bảo gì sau khi huấn luyện đã chạy.
Minh họa
Phần tiêu đề “Minh họa”Minh họa dưới đây dựng một MLP sâu với dữ liệu ngẫu nhiên và vẽ, mỗi lớp một cột, độ lớn gradient của lớp đó, từ lớp đầu (trái) tới lớp cuối (phải). Trục đứng theo thang log: hai cột cách nhau một khoảng bằng nhau trên trục nghĩa là gấp nhau cùng một số lần, nên bạn đọc được “nhỏ đi bao nhiêu lần mỗi lớp”. Bạn chọn hàm kích hoạt (sigmoid, tanh, ReLU), cách khởi tạo (độ lệch chuẩn 1, Xavier, He) và số lớp (2 tới 20). Hãy thử:
- Sigmoid + Xavier, 10 lớp: các cột thấp dần rõ rệt về phía trái. Mỗi lần lùi một lớp, gradient nhỏ đi vài lần, đúng như giới hạn . Kéo số lớp lên 20: các cột đầu gần như biến mất. Đó là gradient tiêu biến.
- ReLU + độ lệch chuẩn 1: ngược lại, các cột vọt lên rất cao, hơn cấu hình ổn định nhiều bậc độ lớn, và càng nhiều lớp càng cao. Đó là gradient bùng nổ, do mỗi lớp nhân phương sai lên khoảng lần.
- ReLU + He: các cột gần như bằng nhau, dù tăng lên 20 lớp. Tín hiệu đi qua mỗi lớp mà giữ được cỡ.
- Đổi sang tanh, giữ Xavier: các cột đều hơn hẳn so với sigmoid ở bước 1, vì chứ không phải . Đây đúng là trường hợp Xavier được thiết kế cho.
Ví dụ code
Phần tiêu đề “Ví dụ code”Hai ô đầu chạy ngay trên trang, chỉ dùng NumPy; ô nào cũng tự tạo dữ liệu. Ô thứ ba dùng PyTorch, nên chạy trên máy bạn.
Ô đầu tiên cài đặt đúng bốn bước lan truyền ngược của mục Lý thuyết cho MLP 2 lớp (3 đầu vào, 4 nơ-ron ẩn sigmoid, 1 nơ-ron ra sigmoid), trên 5 mẫu ngẫu nhiên, rồi so với gradient sai phân trung tâm, .
Lan truyền ngược và kiểm tra gradient
import numpy as np
rng = np.random.default_rng(0)m, n, n1 = 5, 3, 4 # số mẫu, số đặc trưng, số nơ-ron lớp ẩnX = rng.normal(size=(m, n)) # mỗi hàng là một mẫu xy = np.array([0, 1, 1, 0, 1])W1, b1 = rng.normal(size=(n1, n)), np.zeros(n1)W2, b2 = rng.normal(size=(1, n1)), np.zeros(1)
def sigmoid(z): return 1 / (1 + np.exp(-z))
def loss(W1, b1, W2, b2): # cross-entropy nhị phân, trung bình m mẫu a2 = sigmoid(sigmoid(X @ W1.T + b1) @ W2.T + b2).ravel() return -np.mean(y * np.log(a2) + (1 - y) * np.log(1 - a2))
# Lan truyền ngược, từng mẫu một, cộng dồn gradientgrads = [np.zeros_like(p) for p in (W1, b1, W2, b2)]for x, t in zip(X, y): a1 = sigmoid(W1 @ x + b1) # lan truyền xuôi a2 = sigmoid(W2 @ a1 + b2) d2 = a2 - t # delta lớp ra d1 = (W2.T @ d2) * a1 * (1 - a1) # delta lớp ẩn for g, v in zip(grads, (np.outer(d1, x), d1, np.outer(d2, a1), d2)): g += v / m
# Gradient bằng sai phân trung tâm, từng tham số mộtparams, h = [W1, b1, W2, b2], 1e-5 # h: bước sai phânfor name, p, g in zip(["W1", "b1", "W2", "b2"], params, grads): num = np.zeros_like(p) for i in np.ndindex(p.shape): old = p[i] p[i] = old + h up = loss(*params) p[i] = old - h num[i] = (up - loss(*params)) / (2 * h) p[i] = old rel = np.linalg.norm(g - num) / (np.linalg.norm(g) + np.linalg.norm(num)) print(f"{name}: max |grad| = {np.abs(g).max():.4f}, rel err = {rel:.1e}")The first run downloads Python, about 10 MB.
Sai số tương đối của cả bốn nhóm tham số đều cực nhỏ, từ () tới (), dưới xa ngưỡng : lan truyền ngược đúng. Giờ hãy cố ý làm sai: xóa * a1 * (1 - a1) ở dòng tính d1 (quên thừa số ) rồi chạy lại. , vẫn đúng, còn sai số của và nhảy lên và . Kiểm tra gradient không chỉ báo có lỗi, nó còn chỉ ra lỗi nằm ở lớp nào.
Ô thứ hai dựng một MLP 10 lớp, mỗi lớp 64 nơ-ron, cho 32 mẫu ngẫu nhiên đi qua, rồi lan truyền ngược và in ở từng lớp với ba cấu hình. Để ba cấu hình so được với nhau, ở lớp cuối được đặt giống hệt nhau (một vectơ ngẫu nhiên cố định), nên hàng 10 của bảng là cùng một số.
Độ lớn gradient qua 10 lớp
import numpy as np
N_L, width, m = 10, 64, 32 # số lớp, số nơ-ron mỗi lớp, số mẫu
# Trả về ||delta^[l]|| = ||dL/dz^[l]|| của từng lớp l = 1..N_Ldef run(act, init_std): rng = np.random.default_rng(0) g = {"sigmoid": lambda z: 1 / (1 + np.exp(-z)), "relu": lambda z: np.maximum(z, 0)}[act] X = rng.normal(size=(m, width)) # mỗi hàng là một mẫu Ws, zs, acts = [], [], [X] for l in range(N_L): # lan truyền xuôi, lớp cuối không có g W = rng.normal(0, init_std(width, width), size=(width, width)) Ws.append(W) zs.append(acts[-1] @ W.T) acts.append(g(zs[-1]) if l < N_L - 1 else zs[-1]) # Mất mát giả L = trung bình của r . (đầu ra), r ngẫu nhiên cố định, # nên delta lớp cuối = r / m, giống nhau ở cả ba cấu hình d = rng.normal(size=(m, width)) / m norms = [np.linalg.norm(d)] for l in range(N_L - 1, 0, -1): # lan truyền ngược về lớp 1 a = acts[l] # kích hoạt của lớp l (đếm từ 1) dg = a * (1 - a) if act == "sigmoid" else (zs[l - 1] > 0) d = (d @ Ws[l]) * dg # delta^[l] = (W^[l+1]^T delta^[l+1]) * g' norms.append(np.linalg.norm(d)) return norms[::-1]
xavier = lambda n_in, n_out: np.sqrt(2 / (n_in + n_out))he = lambda n_in, n_out: np.sqrt(2 / n_in)one = lambda n_in, n_out: 1.0cols = {"sig+Xavier": run("sigmoid", xavier), "relu+std1": run("relu", one), "relu+He": run("relu", he)}print("layer" + "".join(f"{k:>12}" for k in cols))for l in range(N_L): print(f"{l + 1:>5}" + "".join(f"{v[l]:>12.1e}" for v in cols.values()))The first run downloads Python, about 10 MB.
Đọc từ dưới lên, tức theo chiều lan truyền ngược:
- Sigmoid + Xavier: từ 1.4 ở lớp 10 xuống ở lớp 1, nhỏ đi khoảng 4 lần mỗi lớp. Lớp đầu nhận gradient nhỏ hơn lớp cuối gần nửa triệu lần.
- ReLU + std 1: từ 1.4 lên , lớn lên khoảng 5.7 lần mỗi lớp, đúng con số đã tính ở mục Khởi tạo.
- ReLU + He: giữ quanh 1.3 tới 1.5 ở mọi lớp.
Gradient của trọng số, , còn nhân thêm với kích hoạt của lớp trước. Với std 1, kích hoạt cũng bùng nổ theo chiều xuôi, nên mọi lớp đều có gradient trọng số khổng lồ, không chỉ các lớp đầu.
Ô thứ ba huấn luyện hai MLP 3 lớp ẩn trên bộ chữ số viết tay load_digits của scikit-learn (1797 ảnh 8×8, nửa để train, nửa để test): một mạng thường, một mạng có BatchNorm và Dropout sau mỗi lớp ẩn. Cuối cùng, ô cố ý quên model.eval() để xem chuyện gì xảy ra. Để cài PyTorch bản chỉ chạy CPU, xem bài Xây dựng LLM đầu tiên.
Run on your machine
MLP có và không có BatchNorm, Dropout
- Install
pip install torch scikit-learn- Hardware
- CPU là đủ. Khoảng 5 tới 15 giây tùy máy
import torchimport torch.nn as nnfrom sklearn.datasets import load_digitsfrom sklearn.model_selection import train_test_split
torch.manual_seed(0)X, y = load_digits(return_X_y=True)X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.5, random_state=0, stratify=y)mean, std = X_train.mean(), X_train.std() + 1e-8X_tr = torch.tensor((X_train - mean) / std, dtype=torch.float32)y_tr = torch.tensor(y_train, dtype=torch.long)X_te = torch.tensor((X_test - mean) / std, dtype=torch.float32)y_te = torch.tensor(y_test, dtype=torch.long)
def make_mlp(use_bn_dropout=False): layers = [] dims = [64, 128, 128, 128, 10] for i in range(len(dims) - 1): layers.append(nn.Linear(dims[i], dims[i + 1])) if i < len(dims) - 2: if use_bn_dropout: layers.append(nn.BatchNorm1d(dims[i + 1])) layers.append(nn.ReLU()) if use_bn_dropout: layers.append(nn.Dropout(0.5)) return nn.Sequential(*layers)
for name, use_reg in [("Base MLP", False), ("BN + Dropout", True)]: torch.manual_seed(0) model = make_mlp(use_reg) opt = torch.optim.Adam(model.parameters(), lr=1e-3) loss_fn = nn.CrossEntropyLoss() for epoch in range(40): model.train() loss = loss_fn(model(X_tr), y_tr) opt.zero_grad() loss.backward() opt.step()
model.eval() with torch.no_grad(): acc = (model(X_te).argmax(dim=1) == y_te).float().mean().item() print(f"{name} (eval): test acc = {acc:.3f}")
if use_reg: model.train() # cố ý quên eval accs = [] for _ in range(3): with torch.no_grad(): acc_m = (model(X_te).argmax(dim=1) == y_te).float().mean().item() accs.append(f"{acc_m:.3f}") print(f"{name} (quen eval): test acc = {accs}")Sample output
From one real run. A model gives a different answer each time.
Base MLP (eval): test acc = 0.913BN + Dropout (eval): test acc = 0.917BN + Dropout (quen eval): test acc = ['0.830', '0.815', '0.816']Đọc kết quả:
- Khi có
model.eval(): Mạng có BatchNorm và Dropout đạt độ chính xác kiểm tra 0.917, nhỉnh hơn Base MLP (0.913) trên tập dữ liệu chữ số 8×8. Lúc này Dropout tắt hoàn toàn và BatchNorm dùng kỳ vọng, phương sai động tích lũy trong quá trình huấn luyện thay vì thống kê của batch kiểm tra. - Khi quên
model.eval(): Độ chính xác sụt giảm mạnh xuống khoảng 0.815–0.830 và thay đổi ngẫu nhiên qua từng lần chạy. Lý do: Dropout vẫn tiếp tục tắt ngẫu nhiên 50% nơ-ron khiến tín hiệu đầu ra bị khuyết tật, đồng thời BatchNorm chuẩn hóa theo trung bình của tập kiểm tra thay vì phân phối đã học.
Lỗi hay gặp
Phần tiêu đề “Lỗi hay gặp”Kiểm tra nhanh
Phần tiêu đề “Kiểm tra nhanh”Tóm tắt và đọc thêm
Phần tiêu đề “Tóm tắt và đọc thêm”Ý chính:
- Lan truyền ngược là quy tắc hàm hợp tính từ lớp ra về lớp vào, dùng lại kết quả trung gian: , . Kiểm tra bằng sai phân trung tâm; sai số tương đối cỡ trở xuống là đúng.
- Gradient ở lớp đầu là tích nhiều thừa số và , nên dễ tiêu biến (sigmoid: ) hoặc bùng nổ (trọng số quá lớn).
- Khởi tạo giữ phương sai qua mỗi lớp: Xavier cho tanh, He cho ReLU.
- BatchNorm chuẩn hóa mỗi nơ-ron theo batch rồi co giãn bằng , , và dùng trung bình động khi dự đoán; inverted dropout tắt nơ-ron với xác suất và chia cho khi huấn luyện, không làm gì khi dự đoán. Weight decay co trọng số, dừng sớm giữ mô hình tốt nhất trên tập kiểm định, cắt chuẩn gradient chặn bùng nổ.
Đọc thêm:
- Vũ Hữu Tiệp (2017). Machine Learning cơ bản, Bài 14: Multi-layer Perceptron và Backpropagation. Tiếng Việt, dẫn lan truyền ngược dạng ma trận. Ký hiệu khác: dữ liệu xếp theo cột, là số mẫu.
- Zhang, A., Lipton, Z. C., Li, M., Smola, A. J. Dive into Deep Learning, bản tiếng Việt Đắm mình vào Học Sâu (dịch từ bản 0.14.4): mục 4.7 Lan truyền xuôi, lan truyền ngược và đồ thị tính toán, mục 4.8 Ổn định số học và khởi tạo, mục 4.6 Dropout, mục 7.5 Chuẩn hóa theo batch. Có code chạy được.
- Goodfellow, I., Bengio, Y., Courville, A. (2016). Deep Learning, MIT Press, mục 6.5 (lan truyền ngược), chương 7 (điều chuẩn, có dropout và dừng sớm), mục 8.4 và 8.7.1 (khởi tạo, BatchNorm).
- Khóa CS231n, Đại học Stanford. Ghi chú bài giảng Neural Networks Part 3 (trang không ghi tác giả và năm), mục Gradient checks. Mẹo thực tế khi kiểm tra gradient: sai số tương đối, chỗ gãy, số thực 64 bit.
- Các bài gốc. Rumelhart, D. E., Hinton, G. E., Williams, R. J. (1986). Learning representations by back-propagating errors. Nature 323, 533–536. Glorot, X., Bengio, Y. (2010). Understanding the difficulty of training deep feedforward neural networks. AISTATS. He, K. và cộng sự (2015). Delving Deep into Rectifiers. arXiv:1502.01852. Ioffe, S., Szegedy, C. (2015). Batch Normalization. arXiv:1502.03167. Srivastava, N. và cộng sự (2014). Dropout: A Simple Way to Prevent Neural Networks from Overfitting. JMLR 15, 1929–1958.
Your browser is blocking storage. Your progress will be lost when you reload the page.
Content licensed under CC BY-NC-SA 4.0. Code under the MIT license.About