Bỏ qua nội dung

Neural Network

Giả sử bạn muốn dự đoán điểm thi từ số giờ ôn tập. Ở bài Linear Regression, ta dùng một đường thẳng: cứ học thêm một giờ thì điểm dự đoán tăng thêm một lượng cố định.

Nhưng dữ liệu thực tế có thể không đi theo một đường thẳng như vậy. Lúc mới bắt đầu, học thêm một giờ có thể giúp tiến bộ nhiều; khi đã gần đạt điểm tối đa, học thêm một giờ có thể chỉ cải thiện rất ít. Hai học sinh học cùng số giờ cũng có thể đạt kết quả khác nhau vì số bài luyện tập và kiến thức nền khác nhau.

Neural Network giúp ta học những mối quan hệ phức tạp hơn. Thay vì đi thẳng từ input đến dự đoán bằng một phép tính, mạng cho dữ liệu đi qua nhiều bước tính toán. Mỗi bước kết hợp các thông tin đã có, rồi đưa kết quả sang bước tiếp theo.

Trong bài này, ta đi từ một khối tính toán nhỏ nhất, ghép các khối thành mạng, rồi xem mạng học cách dự đoán ra sao.

Định nghĩa: Neural Network là gì?

Neural Network, hay mạng nơ-ron nhân tạo, là một mô hình ghép nhiều khối tính toán nhỏ với nhau. Mỗi khối nhận vào các con số và trả ra một con số. Kết quả của khối này có thể trở thành đầu vào của khối tiếp theo.

Mỗi khối đó được gọi là neuron. Các neuron thường được xếp thành từng layer, tức là từng tầng. Khi học từ dữ liệu, mạng điều chỉnh các con số bên trong neuron để dự đoán tốt hơn.

Neuron: Một khối tính toán: nhận các input, kết hợp chúng theo một công thức rồi tạo ra một output. Đây là phép tính trong máy, không phải tế bào thần kinh thật.

Layer (tầng): Một nhóm neuron nằm ở cùng một bước xử lý. Các neuron trong tầng đó nhận dữ liệu từ tầng trước.

Mạng cơ bản nhất: một neuron

Tiếp tục bài toán điểm thi. Ta cung cấp cho mô hình hai thông tin: số giờ học và số bài luyện tập. Một neuron nhận cả hai thông tin này để tính ra một kết quả.

Đầu vào Một neuron Đầu ra
z=w1x1+w2x2+bz = w_1x_1 + w_2x_2 + b
x₁ x₂ × w₁× w₂ z Activation function
f(z)f(z)
a
a=f(z)a = f(z)
2 giá trị1 giá trị
Neuron nhân từng input với weight tương ứng rồi cộng cùng bias để được z. Hàm activation f trên mũi tên biến tổng z thành đầu ra a.

Vuốt ngang để xem trọn sơ đồ →

Hãy đọc hình từ trái sang phải. Một neuron làm hai việc:

  1. Nhân rồi cộng: nhân từng input với một trọng số riêng, cộng các kết quả và thêm bias. Gọi tổng vừa tính là zz.
  2. Xử lý tổng vừa có: đưa zz qua một hàm gọi là activation, rồi nhận output aa.

Feature (đặc trưng): Một thông tin đầu vào. Trong ví dụ này, giờ học là một feature, số bài luyện tập là một feature khác.

Weight (trọng số): Mỗi input được nhân với một weight riêng trước khi cộng vào tổng zz. Weight quyết định input đó làm zz thay đổi như thế nào: nếu w=2w = 2, input tăng 11 thì zz tăng 22; nếu w=−2w = -2, input tăng 11 thì zz giảm 22, khi các input khác và tham số giữ nguyên.

Bias: Con số bb được cộng vào sau khi đã nhân từng input với weight và cộng các kết quả lại. Bias giúp tăng hoặc giảm tổng zz: nếu giữ nguyên inputs và weights rồi tăng bb từ 11 lên 33, tổng zz tăng thêm 22; nếu giảm bb đi 22, tổng zz cũng giảm 22. Khi tất cả input bằng 00, ta có z=bz = b.

Activation function: Hàm nhận tổng zz và biến nó thành output của neuron. Chúng ta sẽ nói về activation function sau.

Viết hai bước ấy bằng công thức:

z=w1x1+w2x2+ba=f(z)\begin{aligned} z &= w_1x_1+w_2x_2+b \\ a &= f(z) \end{aligned}

Ở đây, x1,x2x_1,x_2 là hai input; w1,w2w_1,w_2 là hai weights; bb là bias. Ký hiệu ff chỉ hàm activation ta chọn. zz là kết quả trước activation, còn aa là kết quả sau activation.

Vậy aa có ý nghĩa gì? aa chính là output mà neuron tạo ra bằng cách kết hợp các features đầu vào rồi áp dụng activation. Trong ví dụ điểm thi, neuron nhận số giờ học và số bài luyện tập, nhân chúng với các weights tương ứng, cộng bias rồi đưa tổng qua hàm ff. Kết quả là một con số mới: aa.

Output của neuron (aa): Giá trị neuron trả về sau khi xử lý các input. Giá trị này có thể được dùng để đưa ra dự đoán, hoặc tiếp tục làm input cho neuron khác.

Điểm quan trọng là aa không nhất thiết là kết quả cuối cùng của cả mạng. Với neuron vừa tính, aa là output; nhưng khi ta đưa con số ấy sang một neuron tiếp theo, nó lại đóng vai trò input. Neuron tiếp theo có thể kết hợp aa với output của các neuron khác để tạo ra một kết quả mới. Nối tiếp các bước như vậy, ta có mạng nhiều layer.

Khi có nhiều layer

Bây giờ, ta áp dụng ý tưởng dùng output làm input cho tầng tiếp theo. Thay vì chỉ có một neuron, ta cho ba neuron cùng nhìn vào hai input. Mỗi neuron có weights và bias riêng, nên tạo ra một output aa riêng.

Ta nhận được ba con số mới từ hai features ban đầu. Cả ba con số trở thành inputs cho nhóm neuron ở tầng tiếp theo. Tầng này lại kết hợp các inputs để tạo output mới, rồi truyền tiếp cho đến tầng tạo dự đoán cuối cùng:

Input layer 2 thông tin đầu vào x₁x₂ Hidden layer 1 3 neuron h₁h₂h₃ Hidden layer 2 2 neuron g₁g₂ Output layer 1 neuron ŷ Kết quả tầng trước → đầu vào tầng sau
Mỗi vòng tròn ở hidden/output layer là một neuron có weights và bias riêng. Hai vòng tròn ở input layer chỉ biểu diễn dữ liệu đầu vào. Mỗi mũi tên mang kết quả sang tầng kế tiếp.

Vuốt ngang để xem trọn sơ đồ →

Theo dõi một lượt đi qua hình:

  1. Input layer: đưa hai thông tin ban đầu vào mạng.
  2. Hidden layer 1: ba neuron tính riêng rẽ và tạo ra ba kết quả.
  3. Hidden layer 2: mỗi neuron nhận cả ba kết quả vừa có, kết hợp chúng để tạo ra kết quả mới. Tầng này có hai neuron nên trả ra hai con số.
  4. Output layer: nhận hai con số đó và tạo một dự đoán cuối cùng.

Input layer (tầng đầu vào): Nơi đưa dữ liệu vào mạng. Các nút ở đây chỉ biểu diễn features, chưa làm phép tính của neuron.

Hidden layer (tầng ẩn): Tầng nằm giữa input và output.

Output layer (tầng đầu ra): Tầng tạo kết quả ta cần, chẳng hạn điểm thi dự đoán.

Mũi tên trong hình cho biết neuron nhận dữ liệu từ đâu. Ở mạng này, mỗi neuron nhận tất cả kết quả của tầng ngay trước nó. Kiểu kết nối đó gọi là fully connected.

Fully connected (kết nối đầy đủ): Mỗi neuron nối với tất cả output của tầng liền trước. Mỗi kết nối có một weight; mỗi neuron có một bias riêng.

Hình minh họa có hai hidden layer, nhưng mạng có thể chỉ có một hoặc có nhiều hơn. Phần tính tay phía sau sẽ dùng mạng nhỏ hơn: 2 input → 3 hidden neuron → 1 output, để bạn dễ theo dõi từng con số.

Activation function

Có nhiều neuron và nhiều layer chưa đủ để giải quyết vấn đề ở đầu bài. Nếu mọi neuron chỉ nhân rồi cộng, cả mạng vẫn có thể thu gọn về một mô hình tuyến tính.

Xem ví dụ đơn giản với một input. Tầng thứ nhất tính ra hh, tầng thứ hai dùng hh để dự đoán:

h=2x+1y^=3h−2=3(2x+1)−2=6x+1\begin{aligned} h &= 2x+1 \\ \hat{y} &= 3h-2 \\ &= 3(2x+1)-2 \\ &= 6x+1 \end{aligned}

Ta đã đi qua hai tầng, nhưng kết quả cuối vẫn là một đường thẳng theo xx. Thêm tầng chỉ thay đổi hệ số, chưa tạo được quan hệ phức tạp hơn.

Activation phi tuyến làm cho mạng không còn bị giới hạn như vậy. Nó thay đổi cách xử lý tùy giá trị nhận được. Chẳng hạn, hàm ReLU dưới đây xử lý số âm khác với số dương.

ReLU

ReLU là activation function phổ biến nhất, và nó có một quy tắc rất dễ nhớ: số âm đổi thành 0; số dương giữ nguyên; số 0 vẫn là 0.

ReLU: Tên một activation function. Công thức max⁡(0,z)\max(0,z) có nghĩa là chọn số lớn hơn giữa 0 và zz.

ReLU⁡(z)=max⁡(0,z)\operatorname{ReLU}(z)=\max(0,z)

Thử lần lượt ba giá trị:

ReLU⁡(−3)=0,ReLU⁡(0)=0,ReLU⁡(2)=2\operatorname{ReLU}(-3)=0,\qquad \operatorname{ReLU}(0)=0,\qquad \operatorname{ReLU}(2)=2 0zReLU(z)Giữ ở 0Giữ nguyên z

Đường trên hình nằm ngang ở phần âm và đi lên ở phần dương: nó có một chỗ gãy, thay vì là một đường thẳng xuyên suốt. Khi nhiều neuron dùng ReLU với các weights khác nhau, mạng có thể ghép nhiều đoạn đơn giản để mô tả quan hệ phức tạp hơn.

Chọn activation theo vai trò: Hidden layer thường dùng ReLU. Ở output, regression có thể giữ nguyên tổng để dự đoán một con số; classification nhị phân dùng sigmoid để chuyển tổng thành xác suất.

Forward propagation

Ta đã biết mạng được nối như thế nào. Bây giờ hãy đưa một bộ input vào và tính từ trái sang phải: tầng đầu tính xong thì chuyển kết quả cho tầng sau, cho đến khi có dự đoán. Toàn bộ lượt tính đó gọi là forward propagation.

Forward propagation: Một lượt tính từ input qua các layer để ra dự đoán.

Ta dùng mạng 2 → 3 → 1: hai input, một hidden layer có ba neuron dùng ReLU và một output neuron. Bộ weights dưới đây được chọn để tính tay dễ, chưa phải tham số đã train.

Đi từng bước qua Neural Network

2 input → 3 hidden neuron → 1 output. Bấm Next để theo dõi phép tính; weights và bias giữ cố định.

Input layer Hidden layer · ReLU Output layer x₁2x₂1 h1?h2?h3? Output? Đường nối sáng: tín hiệu đang truyền qua layer

Vuốt ngang sơ đồ để xem các layer →

Bước 1/5 · Đầu vào
x₁ = 2, x₂ = 1. Các neuron chưa được tính.
Xem weights và bias cố định

h₁: w = (1, 1), b = −1
h₂: w = (−1, 1), b = 0
h₃: w = (0.5, 1), b = 0
Output: v = (0.5, −1, 1), b = −1

Đây là tham số chọn để minh họa phép tính, chưa được học từ dữ liệu. Các số hiển thị được làm tròn đến 4 chữ số thập phân.

Tự tính lại ví dụ mặc định

Bước 1 — Đưa input vào: x1=2x_1 = 2, x2=1x_2 = 1. Lúc này chưa có dự đoán; ta mới chỉ cung cấp hai con số cho mạng.

Bước 2 — Tính tổng tại từng hidden neuron. Mỗi neuron có bộ weights và bias riêng:

z1=1x1+1x2−1=1×2+1×1−1=2z2=−1x1+1x2=−1×2+1×1=−1z3=0.5x1+1x2=0.5×2+1×1=2\begin{aligned} z_1 &= 1x_1 + 1x_2 - 1 = 1\times2 + 1\times1 - 1 = 2 \\ z_2 &= -1x_1 + 1x_2 = -1\times2 + 1\times1 = -1 \\ z_3 &= 0.5x_1 + 1x_2 = 0.5\times2 + 1\times1 = 2 \end{aligned}

Bước 3 — Áp dụng ReLU:

h1=ReLU⁡(2)=2h2=ReLU⁡(−1)=0h3=ReLU⁡(2)=2\begin{aligned} h_1 &= \operatorname{ReLU}(2)=2 \\ h_2 &= \operatorname{ReLU}(-1)=0 \\ h_3 &= \operatorname{ReLU}(2)=2 \end{aligned}

Hidden layer trả ra ba con số (2,0,2)(2,0,2). Ta đặt tên chúng là h1,h2,h3h_1,h_2,h_3 để dễ theo dõi. Neuron ở output sẽ dùng ba con số này, thay vì dùng trực tiếp hai input ban đầu.

Bước 4 — Tính tổng ở output. Gọi weights ở đây là v1,v2,v3v_1,v_2,v_3 để dễ phân biệt với weights ở hidden layer:

zout=v1h1+v2h2+v3h3+bout=0.5×2+(−1)×0+1×2−1=2\begin{aligned} z_{\mathrm{out}} &= v_1h_1+v_2h_2+v_3h_3+b_{\mathrm{out}} \\ &= 0.5\times2+(-1)\times0+1\times2-1 \\ &= 2 \end{aligned}

Bước 5 — Chuyển thành dự đoán phù hợp với bài toán:

Bài toánXử lý outputVí dụ
RegressionGiữ nguyên tổng: y^=zout\hat{y}=z_{\mathrm{out}}y^=2\hat{y}=2, một giá trị liên tục.
Binary classificationĐưa tổng qua sigmoid: p=σ(zout)p=\sigma(z_{\mathrm{out}})p=1/(1+e−2)≈0.8808p=1/(1+e^{-2})\approx0.8808; threshold 0.5 cho nhãn 1.

Hidden features có ý nghĩa gì?

Hãy tưởng tượng x1x_1 là giờ học, x2x_2 là số bài luyện tập, và output dùng để dự đoán kết quả thi. Sau khi mạng học từ nhiều ví dụ, ta có thể hình dung ý nghĩa của từng output ở hidden layer như sau:

  • h1h_1 có thể phản ánh “mức độ chuẩn bị”: giá trị cao khi học sinh vừa dành nhiều thời gian học, vừa làm nhiều bài luyện tập.
  • h2h_2 có thể phản ánh “thiên về luyện tập”: giá trị cao khi học sinh làm nhiều bài so với thời gian học.
  • h3h_3 có thể phản ánh “học nhiều nhưng ít luyện tập”: giá trị cao khi học sinh dành nhiều giờ học nhưng làm ít bài.

Đây là những ví dụ để hình dung cách mạng kết hợp hai thông tin ban đầu thành các thông tin mới, không phải ý nghĩa đã được xác nhận của ba neuron trong công cụ phía trên.

Bạn nhập vào hai thông tin: giờ học và số bài. Mạng tự tính ra ba thông tin mới là h1,h2,h3h_1,h_2,h_3. Những thông tin mới này gọi là hidden features. Tầng sau dùng chúng như “nguyên liệu” để tính tiếp.

Hidden feature: Đặc trưng trung gian do mạng tính ra; giá trị của nó biết được, nhưng ý nghĩa đối với con người không có nhãn cố định.

Nhưng ta không đưa cho mạng yêu cầu “neuron thứ nhất phải đo mức độ chuẩn bị”. Ta chỉ yêu cầu nó dự đoán kết quả cuối cho tốt. Vì vậy, những tên trên chỉ là phỏng đoán về ý nghĩa. Ta tính được chính xác giá trị của hidden feature, nhưng không thể chỉ nhìn vào con số đó rồi khẳng định nó mang ý nghĩa gì.

Khả năng tự học những feature mới như vậy là một điểm khác biệt quan trọng giữa deep learning và nhiều cách làm machine learning truyền thống. Với Linear Regression và Logistic Regression trong hai bài trước, ta đưa các features đã chọn vào công thức để dự đoán. Nếu muốn bổ sung thông tin như “số bài luyện tập trên mỗi giờ học”, ta thường phải tự nghĩ ra cách kết hợp này rồi tính nó thành một feature mới cho mô hình.

Với deep learning, ta vẫn cung cấp dữ liệu đầu vào, nhưng mạng có thể tự học cách kết hợp và biến đổi các features qua nhiều tầng. Tầng đầu tạo các hidden features từ dữ liệu ban đầu; tầng sau lại dùng chúng làm input để tạo những features phức tạp hơn. Trong lúc huấn luyện, weights và biases được điều chỉnh để những cách kết hợp này phục vụ việc dự đoán. Ta không cần định nghĩa sẵn ý nghĩa cho từng hidden feature.

Quay lại ví dụ điểm thi: từ giờ học và số bài luyện tập, mạng tạo ra h1,h2,h3h_1,h_2,h_3, rồi có thể tiếp tục kết hợp chúng ở tầng sau trước khi dự đoán điểm. Mạng vừa học cách tạo features, vừa học cách dùng các features đó để dự đoán. Những features học được có thể hữu ích hơn việc chỉ dùng trực tiếp thông tin ban đầu, nhưng điều này vẫn cần được kiểm tra trên dữ liệu mới.

Deep learning và machine learning: Deep learning là một nhánh của machine learning, không phải hai lĩnh vực tách biệt. Điểm nổi bật của deep learning là dùng nhiều tầng để tự học các cách biểu diễn dữ liệu; nhiều phương pháp machine learning truyền thống phụ thuộc nhiều hơn vào các features do con người lựa chọn hoặc thiết kế.

Hàm mất mát (Loss function)

Sau khi mạng đưa ra dự đoán, ta cần so sánh với đáp án thật để biết mô hình sai bao nhiêu. Neural Network có thể dùng lại hai hàm loss đã gặp: MSE cho dự đoán giá trị liên tục và BCE cho phân loại nhị phân.

Loss function: Cách đo dự đoán sai bao nhiêu bằng một con số để hướng dẫn việc học.

MSE cho regression

Giống bài Linear Regression, khi dự đoán điểm thi, ta lấy dự đoán trừ điểm thật, bình phương sai số rồi tính trung bình:

MSE: Trung bình bình phương sai số, thường dùng khi dự đoán giá trị liên tục.

LMSE=1n∑i=1n(y^i−yi)2L_{\mathrm{MSE}} = \frac{1}{n}\sum_{i=1}^{n}(\hat y_i-y_i)^2

Ở đây, nn là số mẫu; yiy_i là giá trị thật và y^i\hat{y}_i là dự đoán của mẫu thứ ii.

Ví dụ hai dự đoán là (2,5)(2,5), đáp án thật là (3,3)(3,3):

MSE⁡=(2−3)2+(5−3)22=1+42=2.5\operatorname{MSE}=\frac{(2-3)^2+(5-3)^2}{2}=\frac{1+4}{2}=2.5

Bình phương giúp lỗi âm và dương không triệt tiêu nhau, đồng thời phạt lỗi lớn mạnh hơn. MSE càng nhỏ thì dự đoán càng gần đáp án thật trên các mẫu đang xét.

BCE cho binary classification

Giống bài Logistic Regression, khi dự đoán đậu hay trượt, ta dùng Binary Cross-Entropy để so sánh xác suất dự đoán với nhãn thật:

BCE: Loss cho nhãn 0/1, phạt mạnh khi xác suất dự đoán nghiêng về lớp sai.

LBCE=−1n∑i=1n[yilog⁡(pi)+(1−yi)log⁡(1−pi)]L_{\mathrm{BCE}} = -\frac{1}{n}\sum_{i=1}^{n}\left[y_i\log(p_i)+(1-y_i)\log(1-p_i)\right]

Ở đây, nn là số mẫu; yiy_i là nhãn thật (0 hoặc 1), pip_i là xác suất dự đoán lớp 1 và log⁡\log là logarit tự nhiên. Với một mẫu, công thức rút gọn thành:

  • Nếu y=1y=1: loss là −log⁡(p)-\log(p). Xác suất lớp 1 càng cao thì loss càng nhỏ.
  • Nếu y=0y=0: loss là −log⁡(1−p)-\log(1-p). Xác suất lớp 1 càng thấp thì loss càng nhỏ.

Ví dụ, học sinh thực sự đậu (y=1y=1) và mạng dự đoán xác suất đậu p=0.9p=0.9:

L=−log⁡(0.9)≈0.1054L=-\log(0.9)\approx0.1054

Nếu mạng chỉ dự đoán p=0.1p=0.1, loss tăng lên −log⁡(0.1)≈2.3026-\log(0.1)\approx2.3026. Dự đoán càng tự tin vào lớp sai, BCE phạt càng nặng. Ta tính BCE từ xác suất, chưa đổi xác suất thành nhãn bằng threshold.

Backpropagation

Ở hai bài trước, bạn đã biết cách dùng Gradient Descent để giảm loss: tính gradient của loss theo từng tham số, rồi cập nhật tham số theo hướng ngược gradient. Neural Network vẫn học theo ý tưởng đó.

Chẳng hạn, để cập nhật một weight ww, ta vẫn dùng công thức quen thuộc:

wnew=wold−α∂L∂ww_{\mathrm{new}} = w_{\mathrm{old}} - \alpha\frac{\partial L}{\partial w}

Trong đó, α\alpha là learning rate, còn ∂L/∂w\partial L/\partial w cho biết loss thay đổi thế nào khi ww thay đổi một chút. Quy tắc cập nhật không đổi. Điều cần giải quyết là tính gradient cho tất cả weights và biases trong một mạng nhiều tầng.

Mạng có nhiều tầng nên việc tính gradient không còn gọn như ở hai mô hình trước. Với backpropagation, ta tính lần lượt: bắt đầu từ loss để tính gradient ở tầng cuối, rồi dùng kết quả đó tính ngược về tầng ngay trước nó. Cứ tiếp tục như vậy, ta tính được gradient cho các weights và biases ở những tầng đầu mạng.

Nói đơn giản, gradient ở tầng trước cần thông tin đã tính từ tầng sau. Chính vì đi từ cuối mạng ngược về đầu mạng nên cách tính này được gọi là backpropagation — lan truyền ngược.

Backpropagation (lan truyền ngược): Cách tính gradient của loss theo các weights và biases bằng cách đi ngược qua các phép tính của mạng. Các kết quả gradient ở bước sau được dùng lại để tính gradient ở bước trước.

Forward: input → hidden → output → loss
Backward: gradient từ loss → output → hidden → các weights đầu vào

Backprop dựa trên chain rule: nếu weight làm hidden output thay đổi, và hidden output làm dự đoán thay đổi, ta ghép các mức ảnh hưởng ấy để biết weight làm loss thay đổi thế nào.

Chain rule (quy tắc dây chuyền): Quy tắc tính đạo hàm qua nhiều phép tính nối tiếp.

Backpropagation cung cấp gradient; Gradient Descent dùng gradient đó để cập nhật tham số. Hai bước phối hợp với nhau: chạy backward mới chỉ tính được thông tin cần để sửa weights, chưa thay đổi weights.

Tính tay với chính mạng vừa xem

Ta dùng chế độ regression, với input x1=2x_1=2, x2=1x_2=1 và đáp án thật y=3y=3. Trước khi tính ngược, hãy nhắc lại kết quả forward:

Thành phầnGiá trị đang dùng
Tổng ở ba hidden neuronz1=2, z2=−1, z3=2z_1=2,\ z_2=-1,\ z_3=2
Output sau ReLUh1=2, h2=0, h3=2h_1=2,\ h_2=0,\ h_3=2
Weights từ hidden đến outputv1=0.5, v2=−1, v3=1v_1=0.5,\ v_2=-1,\ v_3=1
Bias của outputbout=−1b_{\mathrm{out}}=-1

Dự đoán và loss của một mẫu là:

y^=0.5×2+(−1)×0+1×2−1=2L=(y^−y)2=(2−3)2=1\begin{aligned} \hat y&=0.5\times2+(-1)\times0+1\times2-1=2\\ L&=(\hat y-y)^2=(2-3)^2=1 \end{aligned}

Ta sẽ tính gradient cho v1v_1 ở output, rồi đi ngược về w11w_{11}, weight nối input x1x_1 với hidden neuron thứ nhất. Giá trị hiện tại của w11w_{11} là 11. Trong lúc tính gradient, ta giữ nguyên tất cả tham số, chưa cập nhật gì cả.

Sơ đồ dưới đây tách riêng đường đi qua neuron thứ nhất. Hãy đọc hàng trên từ trái sang phải để theo dõi forward, rồi đọc hàng dưới từ phải sang trái để theo dõi gradient. Các bước sẽ được giải thích ngay sau hình.

Forward → Đọc hàng trên từ trái sang phải để tính loss.

Weight cần tính gradientw11=1w_{11}=1
∂L∂w11=−2\frac{\partial L}{\partial w_{11}}=-2
Nhân input và cộng biasz1=2w11+1−1=2z_1=2w_{11}+1-1=2
∂L∂z1=−1\frac{\partial L}{\partial z_1}=-1
Qua activationh1=ReLU⁡(z1)=2h_1=\operatorname{ReLU}(z_1)=2
∂L∂h1=−1\frac{\partial L}{\partial h_1}=-1
Tính dự đoány^=0.5h1+1=2\hat y=0.5h_1+1=2
∂L∂y^=−2\frac{\partial L}{\partial\hat y}=-2
So với đáp án thậtL=(y^−3)2=1L=(\hat y-3)^2=1
LL

← Backward Đọc hàng dưới từ phải sang trái: bắt đầu từ loss, rồi lần lượt tính gradient theo dự đoán, hidden output, tổng và weight.

Sơ đồ chỉ tách đường đi qua neuron thứ nhất. Đóng góp của các nhánh còn lại vào dự đoán đang bằng 1 và được giữ cố định khi xét đường này.

Bước 1: Tính gradient từ loss về dự đoán

Tạm chưa nghĩ đến weights. Ta chỉ hỏi: nếu dự đoán tăng một chút, loss thay đổi thế nào?

Ta có L=(y^−3)2L=(\hat y-3)^2. Đạo hàm của biểu thức bình phương bằng hai lần biểu thức đó, nhân với đạo hàm bên trong. Đạo hàm của y^−3\hat y-3 theo y^\hat y bằng 11, nên:

∂L∂y^=2(y^−3)×1=2(2−3)=−2\frac{\partial L}{\partial\hat y} =2(\hat y-3)\times1 =2(2-3) =-2

Cách đọc đạo hàm: ∂L/∂y^=−2\partial L/\partial\hat y=-2 nghĩa là tại dự đoán hiện tại, nếu y^\hat y tăng một lượng rất nhỏ thì loss giảm xấp xỉ hai lần lượng đó. Đây là mức thay đổi cục bộ, không có nghĩa tăng dự đoán bao nhiêu cũng làm loss giảm.

Thử tăng dự đoán từ 22 lên 2.012.01:

Lnew=(2.01−3)2=0.9801L_{\mathrm{new}}=(2.01-3)^2=0.9801

Loss giảm từ 11 xuống 0.98010.9801, tức giảm 0.01990.0199, gần với 2×0.01=0.022\times0.01=0.02. Dấu âm phù hợp với trực giác: dự đoán đang thấp hơn đáp án, tăng lên một chút sẽ giảm sai số.

Bước 2: Tính gradient cho weight ở output

Xét v1v_1, weight nhân với h1h_1. Giữ nguyên các giá trị khác, ta viết lại công thức dự đoán:

y^=v1h1+v2h2+v3h3+bout=v1×2+(−1)×0+1×2−1=2v1+1\begin{aligned} \hat y&=v_1h_1+v_2h_2+v_3h_3+b_{\mathrm{out}}\\ &=v_1\times2+(-1)\times0+1\times2-1\\ &=2v_1+1 \end{aligned}

Nếu v1v_1 tăng 0.010.01, dự đoán tăng 0.020.02. Đạo hàm của dự đoán theo v1v_1 chính là hệ số 22:

∂y^∂v1=2\frac{\partial\hat y}{\partial v_1}=2

Ta đã biết hai mắt xích: v1v_1 làm dự đoán thay đổi, rồi dự đoán làm loss thay đổi. Nhân hai đạo hàm để nối chúng:

∂L∂v1=∂L∂y^×∂y^∂v1=(−2)×2=−4\begin{aligned} \frac{\partial L}{\partial v_1} &=\frac{\partial L}{\partial\hat y}\times\frac{\partial\hat y}{\partial v_1}\\ &=(-2)\times2=-4 \end{aligned}

Đó chính là chain rule. Kết quả −4-4 cho biết tăng v1v_1 một chút sẽ làm loss giảm ở bộ tham số hiện tại.

Các weights còn lại ở output được tính cùng cách: lấy gradient −2-2 nhân với input tương ứng. Với bias, đạo hàm bằng 11 vì bias được cộng trực tiếp vào dự đoán:

∂L∂v2=(−2)×h2=(−2)×0=0∂L∂v3=(−2)×h3=(−2)×2=−4∂L∂bout=(−2)×1=−2\begin{aligned} \frac{\partial L}{\partial v_2}&=(-2)\times h_2=(-2)\times0=0\\ \frac{\partial L}{\partial v_3}&=(-2)\times h_3=(-2)\times2=-4\\ \frac{\partial L}{\partial b_{\mathrm{out}}}&=(-2)\times1=-2 \end{aligned}

Đây là phần được dùng lại: ta tính ∂L/∂y^=−2\partial L/\partial\hat y=-2 một lần, rồi dùng nó cho cả ba weights và bias của output.

Bước 3: Tính ngược về output của hidden neuron

Để đi đến w11w_{11}, ta cần đi qua h1h_1 trước. Giữ nguyên weights và các nhánh khác:

y^=0.5h1+(−1)×0+1×2−1=0.5h1+1\hat y=0.5h_1+(-1)\times0+1\times2-1=0.5h_1+1

Nếu h1h_1 tăng một chút, dự đoán tăng bằng một nửa lượng đó. Vì vậy:

∂y^∂h1=0.5\frac{\partial\hat y}{\partial h_1}=0.5

Dùng lại gradient từ loss về dự đoán:

∂L∂h1=∂L∂y^×∂y^∂h1=(−2)×0.5=−1\frac{\partial L}{\partial h_1} =\frac{\partial L}{\partial\hat y}\times\frac{\partial\hat y}{\partial h_1} =(-2)\times0.5=-1

Ta đã biết loss thay đổi thế nào khi output của hidden neuron thứ nhất thay đổi. Để tới weight đầu vào, ta còn phải đi ngược qua ReLU và phép nhân với input.

Bước 4: Đi ngược qua ReLU

Ta có h1=ReLU⁡(z1)h_1=\operatorname{ReLU}(z_1) và z1=2z_1=2. Với số dương, ReLU giữ nguyên giá trị: z1=2z_1=2 thì h1=2h_1=2; z1=2.01z_1=2.01 thì h1=2.01h_1=2.01. Hai giá trị thay đổi cùng một lượng, nên:

∂h1∂z1=1\frac{\partial h_1}{\partial z_1}=1

Nhân với gradient ở bước 3:

∂L∂z1=∂L∂h1×∂h1∂z1=(−1)×1=−1\frac{\partial L}{\partial z_1} =\frac{\partial L}{\partial h_1}\times\frac{\partial h_1}{\partial z_1} =(-1)\times1=-1

ReLU ở nhánh này giữ nguyên gradient vì tổng đang dương.

Bước 5: Tính gradient cho weight đầu vào

Cuối cùng, ta tới phép tính chứa w11w_{11}:

z1=w11x1+1x2−1=w11×2+1×1−1=2w11z_1=w_{11}x_1+1x_2-1 =w_{11}\times2+1\times1-1 =2w_{11}

Vì x1=2x_1=2, tăng w11w_{11} một chút làm z1z_1 tăng gấp đôi lượng đó. Do vậy:

∂z1∂w11=2\frac{\partial z_1}{\partial w_{11}}=2

Dùng gradient ở bước 4:

∂L∂w11=∂L∂z1×∂z1∂w11=(−1)×2=−2\frac{\partial L}{\partial w_{11}} =\frac{\partial L}{\partial z_1}\times\frac{\partial z_1}{\partial w_{11}} =(-1)\times2=-2

Gom lại, ta đã lần lượt tính:

Tính gradient theoDùng kết quả bước trướcKết quả
Dự đoán y^\hat yĐạo hàm của loss−2-2
Hidden output h1h_1(−2)×0.5(-2)\times0.5−1-1
Tổng z1z_1(−1)×1(-1)\times1−1-1
Weight w11w_{11}(−1)×2(-1)\times2−2-2

Mỗi bước lấy gradient vừa tính ở phía sau, nhân với đạo hàm của phép tính hiện tại. Đó là cách backprop đi từ loss về một weight ở đầu mạng.

Nếu gặp nhánh ReLU âm thì sao?

Neuron thứ hai có z2=−1z_2=-1, nên h2=0h_2=0. Nếu z2z_2 tăng nhẹ lên −0.99-0.99, ReLU vẫn trả về 00. Thay đổi nhỏ ở z2z_2 chưa làm output thay đổi, nên đạo hàm tại đây bằng 00.

Weight nối nhánh này với output là v2=−1v_2=-1. Ta tính ngược:

∂L∂h2=(−2)×(−1)=2∂L∂z2=2×0=0\begin{aligned} \frac{\partial L}{\partial h_2}&=(-2)\times(-1)=2\\ \frac{\partial L}{\partial z_2}&=2\times0=0 \end{aligned}

Từ z2z_2 đi tiếp về weights và bias của neuron thứ hai, gradient đều bằng 00 vì nhân với 00. Các tham số đó không nhận cập nhật từ mẫu đang xét. Mẫu khác làm z2>0z_2>0 vẫn có thể cho gradient khác 00. Tại đúng z2=0z_2=0, ReLU không có đạo hàm duy nhất; code bên dưới chọn 00.

Từ gradient đến cập nhật

Bây giờ ta mới dùng Gradient Descent: lấy tham số cũ trừ learning rate nhân với gradient.

θnew=θold−α∂L∂θ\theta_{\mathrm{new}}=\theta_{\mathrm{old}}-\alpha\frac{\partial L}{\partial\theta}

θ\theta là weight hoặc bias đang cập nhật. Chọn learning rate α=0.01\alpha=0.01 để thử một bước nhỏ.

Learning rate: Hệ số điều chỉnh độ lớn của bước cập nhật. Với α=0.01\alpha=0.01, ta nhân gradient với 0.010.01 rồi trừ kết quả khỏi tham số hiện tại.

Với weight output v1=0.5v_1=0.5, gradient là −4-4:

v1,new=0.5−0.01×(−4)=0.5−(−0.04)=0.54\begin{aligned} v_{1,\mathrm{new}}&=0.5-0.01\times(-4)\\ &=0.5-(-0.04)=0.54 \end{aligned}

Trừ một số âm tức là cộng, nên weight tăng 0.040.04. Điều này hợp lý vì gradient âm cho biết tăng weight một chút sẽ giúp giảm loss.

Với weight đầu vào w11=1w_{11}=1, gradient là −2-2:

w11,new=1−0.01×(−2)=1−(−0.02)=1.02\begin{aligned} w_{11,\mathrm{new}}&=1-0.01\times(-2)\\ &=1-(-0.02)=1.02 \end{aligned}

Phải tính xong các gradient bằng cùng bộ tham số cũ rồi mới cập nhật. Khi tính ngược về h1h_1 ở bước 3, ta dùng v1=0.5v_1=0.5 của lượt forward, chưa được thay bằng 0.540.54.

Để kiểm tra, giả sử ta chỉ cập nhật hai weights vừa tính, giữ nguyên mọi tham số khác, rồi chạy forward lại:

z1,new=1.02×2+1×1−1=2.04h1,new=ReLU⁡(2.04)=2.04y^new=0.54×2.04+(−1)×0+1×2−1=2.1016Lnew=(2.1016−3)2=0.80712256\begin{aligned} z_{1,\mathrm{new}}&=1.02\times2+1\times1-1=2.04\\ h_{1,\mathrm{new}}&=\operatorname{ReLU}(2.04)=2.04\\ \hat y_{\mathrm{new}}&=0.54\times2.04+(-1)\times0+1\times2-1=2.1016\\ L_{\mathrm{new}}&=(2.1016-3)^2=0.80712256 \end{aligned}

Dự đoán đã gần đáp án 33 hơn, và loss giảm từ 11 xuống khoảng 0.80710.8071. Ta chỉ cập nhật hai weights ở đây để dễ kiểm tra bằng tay; khi train thực tế, optimizer cập nhật tất cả tham số được huấn luyện bằng gradient tương ứng.

Nhớ hai nhiệm vụ: Backpropagation tính gradient; optimizer dùng gradient để cập nhật tham số. Bước cập nhật quá lớn vẫn có thể làm loss tăng, nên cần chọn learning rate phù hợp.

Training loop: ghép mọi thứ lại

Hãy ghép lại như cách làm bài rồi sửa bài: mạng đưa ra dự đoán, so với đáp án thật, tính xem các tham số ảnh hưởng đến lỗi ra sao, rồi sửa tham số một chút. Làm lại với dữ liệu nhiều lần chính là training loop.

Training loop: Vòng lặp dự đoán → đo loss → tính gradient → cập nhật tham số, lặp lại để mô hình học.

Initialize weights
for each epoch:
Forward propagation
Calculate loss
Backpropagation
Calculate gradients
Update weights

Ở đây, Calculate gradients là kết quả của Backpropagation, không phải một lượt đạo hàm riêng sau backward. Bản trên coi mỗi epoch dùng cả tập train. Khi chia dữ liệu thành mini-batch, vòng lặp thực tế là:

Initialize weights và bias
for each epoch:
Shuffle training data
for each mini-batch:
Clear old gradients
Forward propagation
Calculate loss
Backpropagation → calculate gradients
Optimizer updates weights và bias
Evaluate on validation data (không update)

Các thuật ngữ cần biết

Thuật ngữHiểu đơn giản
EpochMột lượt đi qua toàn bộ tập train.
BatchNhóm mẫu được xử lý cùng nhau; trong code, “batch” thường chỉ một mini-batch.
Full batchDùng toàn bộ tập train cho một lần tính gradient và update.
Mini-batchDùng một phần tập train cho mỗi lần update.
Batch sizeSố mẫu trong một batch, ví dụ 32. Batch cuối có thể nhỏ hơn.
IterationMột lượt xử lý batch; trong loop này tương ứng một lần update.
Learning rateĐiều khiển độ lớn bước cập nhật; quá lớn dễ bất ổn, quá nhỏ học chậm.
OptimizerThuật toán quyết định cách dùng gradient để cập nhật tham số.

Epoch: Một lượt đi qua toàn bộ tập train.

Batch / mini-batch / batch size: Batch là nhóm mẫu xử lý cùng nhau; mini-batch dùng một phần dữ liệu; batch size là số mẫu trong nhóm.

Iteration: Một lượt xử lý batch; trong ví dụ này có một lần cập nhật tham số.

Optimizer: Thuật toán dùng gradient để quyết định cách cập nhật weights và bias.

Ví dụ có 1.000 mẫu, batch size 32, giữ batch cuối: một epoch có ⌈1000/32⌉=32\lceil1000/32\rceil=32 iterations. Trong đó 31 batch có 32 mẫu và batch cuối có 8 mẫu. Train 10 epoch sẽ có 320 lần update. Không nhầm 10 epoch với 10 lần cập nhật.

Gradient Descent → SGD → Momentum → Adam

Gradient Descent đi ngược hướng gradient. Nếu tính gradient trên toàn bộ tập train, mỗi bước có thể tốn nhiều bộ nhớ và thời gian.

Gradient Descent: Điều chỉnh tham số ngược hướng gradient để giảm loss từng bước.

Stochastic Gradient Descent (SGD) theo nghĩa chặt dùng một mẫu mỗi bước; thực tế người ta thường gọi cả cách dùng mini-batch là SGD. Gradient của batch chỉ là ước lượng gradient toàn tập, nên loss từng bước có thể dao động. Đổi lại, ta cập nhật được thường xuyên và xử lý dữ liệu lớn hơn.

SGD: Ước lượng gradient từ một mẫu hoặc mini-batch để cập nhật, thay vì tính trên cả tập train mỗi bước.

Momentum tích lũy một phần hướng di chuyển từ các bước trước. Hãy tưởng tượng đẩy một quả bóng xuống dốc: nó có quán tính, thay vì đổi hướng hoàn toàn theo từng gợn nhỏ. Momentum giúp giảm dao động và tiến nhanh hơn theo hướng nhất quán.

Momentum: Giữ một phần hướng cập nhật từ các bước trước, giống quán tính giúp chuyển động bớt đổi hướng liên tục.

Adam cũng nhớ các bước trước, nhưng còn theo dõi độ lớn gradient để điều chỉnh bước đi riêng cho từng tham số. Bạn có thể hiểu rằng các weights không nhất thiết cần được sửa với cùng một mức độ. Bên trong, Adam dùng các giá trị trung bình của gradient và bình phương gradient. Khi mới học, chỉ cần nắm vai trò đó để đọc được phần code; Adam không đảm bảo tốt nhất cho mọi bài toán.

Adam: Optimizer dùng lịch sử gradient và bình phương gradient để điều chỉnh bước đi cho từng tham số.

Trong thực tế, ta hiếm khi tự viết phép cập nhật cho từng weight. Mạng có thể có hàng triệu tham số; thư viện giúp tính gradient và cập nhật chúng theo cùng quy tắc, giảm việc viết code lặp lại và sai sót. Với PyTorch, loss.backward() tính gradient và optimizer.step() cập nhật tham số; xem tài liệu optimizer chính thức. Phần NumPy phía sau sẽ tự viết các bước này để bạn thấy thư viện đang làm gì.

Các vấn đề khi train Neural Network

Overfitting và underfitting

Underfitting giống như chưa nắm được cách giải bài: ngay những ví dụ đã dùng để học, mạng vẫn dự đoán kém. Có thể mô hình quá đơn giản, input thiếu thông tin cần thiết hoặc quá trình học chưa hiệu quả. Ta cần xem cả kết quả trên tập train và trên dữ liệu giữ riêng.

Underfitting: Mô hình chưa học đủ: dự đoán trên chính tập train cũng chưa tốt.

Overfitting giống như học quá sát bộ bài đã luyện: làm tốt trên dữ liệu train nhưng gặp dữ liệu mới lại kém. Mạng có thể đã khớp cả những chi tiết ngẫu nhiên trong tập train. Một dấu hiệu thường gặp là training loss tiếp tục giảm trong khi validation loss có xu hướng tăng qua nhiều epoch.

Overfitting: Mô hình khớp tốt với dữ liệu đã học nhưng dự đoán kém trên dữ liệu mới.

Training loss / validation loss: Training loss đo trên tập học; validation loss đo trên tập giữ riêng để theo dõi và chọn cấu hình.

LossEpochTrain (liền)Validation (nét đứt)Vùng loss val thấp nhất

Đồ thị trên là minh họa xu hướng, không phải kết quả một lần train cụ thể. Một epoch validation loss tăng chưa đủ để kết luận. Khi so sánh, cần dùng cùng cách tính loss; dropout và các chế độ train/eval có thể làm hai đường khác nhau.

Giải phápVì sao có ích?
More dataThêm dữ liệu đại diện giúp mô hình bớt phụ thuộc vào chi tiết của một tập nhỏ.
Data augmentationTạo biến thể vẫn giữ đúng nhãn, như dịch nhẹ ảnh. Không áp biến đổi tùy tiện làm đổi ý nghĩa hoặc nhãn.
DropoutKhi train, ngẫu nhiên đưa một phần activation về 0 để mạng bớt phụ thuộc vào một vài neuron. Khi eval, không thả ngẫu nhiên nữa.
Weight decayKhuyến khích weights nhỏ hơn, hạn chế độ phức tạp. Cách thực hiện phụ thuộc optimizer; AdamW tách weight decay khỏi bước thích nghi của Adam.
Early stoppingDừng khi validation không cải thiện đủ lâu và khôi phục checkpoint có validation loss tốt nhất.

Data augmentation: Tạo biến thể dữ liệu vẫn giữ đúng nhãn, như dịch nhẹ một ảnh.

Dropout: Tạm đưa ngẫu nhiên một phần activation về 0 khi train để giảm phụ thuộc vào vài neuron.

Weight decay: Cơ chế khuyến khích weights nhỏ hơn để hạn chế độ phức tạp của mô hình.

Early stopping / checkpoint: Dừng khi validation không cải thiện đủ lâu; checkpoint là bản tham số đã lưu để có thể khôi phục.

Với underfitting, không nên mặc định tăng dropout. Hãy kiểm tra dữ liệu, khả năng biểu diễn, thời gian train, learning rate và mức regularization. Validation dùng để chọn cấu hình; test để đánh giá cuối cùng, không dùng test để quyết định lúc dừng.

Vanishing gradient và exploding gradient

Trong backprop, gradient đi qua nhiều phép nhân. Nếu các hệ số thường nhỏ, tích có thể co lại: 0.110=0.00000000010.1^{10}=0.0000000001. Layer đầu nhận tín hiệu rất yếu và học chậm — đó là vanishing gradient.

Vanishing gradient: Gradient nhỏ dần khi truyền ngược, khiến một số layer học rất chậm.

Ngược lại, nếu tích tăng mạnh như 210=10242^{10}=1024, gradient có thể trở nên rất lớn — exploding gradient. Dấu hiệu có thể là loss nhảy mạnh, tham số tăng vọt hoặc xuất hiện NaN. Đây là trực giác đơn giản; gradient thực tế còn có ma trận và tổng qua nhiều đường.

Exploding gradient: Gradient tăng quá lớn, có thể làm cập nhật mất ổn định hoặc xuất hiện giá trị không hợp lệ.

  • ReLU: ở phía dương, đạo hàm bằng 1 nên tránh một nguồn co gradient do activation bão hòa. Nhưng phía âm có gradient bằng 0; neuron luôn âm trên dữ liệu có thể bị “chết”. ReLU không giải quyết mọi vấn đề gradient.
  • Proper initialization: chọn thang weights phù hợp để tín hiệu không bị phóng đại hoặc thu nhỏ quá mức ngay từ đầu.
  • Normalization: điều chỉnh thang các activation theo một nhóm giá trị, thường giúp tối ưu ổn định hơn.
  • Residual connections: thêm đường tắt output=x+F(x)\mathrm{output}=x+F(x) khi kích thước phù hợp. Gradient có thêm đường đi trực tiếp qua xx, thay vì chỉ qua mọi phép biến đổi trong FF.
  • Gradient clipping: giới hạn norm của gradient trước update để giảm tác động của bước quá lớn; thường hữu ích với exploding gradient.

Residual connection: Đường tắt cộng input vào kết quả của một nhóm layer, giúp thông tin và gradient có thêm đường đi trực tiếp.

Gradient clipping: Giới hạn độ lớn gradient trước khi cập nhật để giảm tác động của bước quá mạnh.

Weight initialization

Trước lượt forward đầu tiên, weights phải có giá trị ban đầu. Nếu ba neuron cùng bắt đầu với bộ weights và bias giống nhau, chúng tính ra cùng kết quả và có thể tiếp tục được cập nhật giống nhau. Ta có ba neuron nhưng chúng lại làm cùng một việc.

Vì vậy, ta thường chọn weights ngẫu nhiên để các neuron bắt đầu khác nhau. Đó là ý nghĩa của “phá đối xứng” trong initialization. Bias có thể bắt đầu bằng 0; điều cần tránh là cho mọi neuron cùng một bộ weights và bias như nhau.

Weight initialization: Cách chọn weights ban đầu trước khi train; khởi tạo ngẫu nhiên giúp các neuron không bắt đầu giống hệt nhau.

Nhưng random không có nghĩa là chọn độ lớn tùy ý:

Cách khởi tạoTrực giác và cách dùng
Random với thang tùy ýPhá đối xứng, nhưng quá nhỏ hoặc quá lớn có thể làm tín hiệu/gradient yếu hoặc bất ổn.
Xavier / GlorotCân nhắc cả số kết nối vào và ra, thường phù hợp với tanh hoặc các phép biến đổi gần tuyến tính.
He / KaimingCân nhắc việc ReLU loại phần âm; thường dùng cho layer đi kèm ReLU.

Xavier / He initialization: Hai cách chọn thang weights ban đầu dựa trên số kết nối; Xavier thường hợp với tanh, He thường dùng cùng ReLU.

Thang khởi tạo thường dùng ⚠️ Có toán

Gọi fan_in là số input vào một neuron và fan_out là số output của layer. Một dạng Xavier normal cơ bản dùng variance 2/(fan_in+fan_out)2/(\mathrm{fan\_in}+\mathrm{fan\_out}); He normal với ReLU dùng variance 2/fan_in2/\mathrm{fan\_in}. Standard deviation là căn bậc hai của variance.

Ví dụ hidden layer có hai input: He normal lấy weights từ phân phối chuẩn có độ lệch chuẩn 2/2=1\sqrt{2/2}=1. Đây là quy tắc giữ thang tín hiệu dưới các giả định nhất định, không đảm bảo mọi mạng đều train tốt. Các hàm thư viện còn có lựa chọn gain, distribution và mode; xem tài liệu khởi tạo PyTorch.

Normalization: BatchNorm và LayerNorm

Hãy tưởng tượng một nhóm giá trị trong mạng có số chỉ quanh 0.1, trong khi một nhóm khác thường lên đến hàng trăm. Sự khác biệt về độ lớn có thể khiến việc cập nhật khó ổn định. Normalization điều chỉnh các giá trị về một thang đo dễ xử lý hơn.

Bạn đã gặp việc chuẩn hóa input trước khi train. BatchNorm và LayerNorm đưa ý tưởng chuẩn hóa vào bên trong mạng, áp dụng cho các kết quả trung gian. Sau bước chuẩn hóa, chúng thường còn có hệ số nhân và phần cộng thêm để mạng học cách điều chỉnh lại nếu cần.

Normalization: Điều chỉnh thang của các giá trị bằng thống kê như trung bình và độ lệch chuẩn.

Cách tính cơ bản là lấy mỗi giá trị trừ đi trung bình của nhóm, rồi chia cho thước đo độ phân tán của nhóm đó. Một số rất nhỏ được thêm vào phép tính để tránh chia cho 0. Khi mới đọc, hãy tập trung vào câu hỏi: chọn những giá trị nào vào cùng một nhóm để chuẩn hóa?

BatchNorm trong mạng fully connectedLayerNorm trên vector hidden
Nhóm tính thống kêCùng một feature qua các mẫu trong mini-batch.Các features được chọn trong từng mẫu riêng biệt.
Khi trainThường dùng thống kê batch và cập nhật trung bình chạy.Tính thống kê trên từng mẫu.
Khi evalMặc định dùng thống kê chạy đã tích lũy.Vẫn tính thống kê trên từng mẫu, không dùng trung bình chạy từ batch.
Điều cần chú ýBatch quá nhỏ có thể cho thống kê kém ổn định.Không phụ thuộc các mẫu khác trong batch theo cách của BatchNorm.

BatchNorm: Trong mạng dạng bảng, chuẩn hóa từng feature theo thống kê qua các mẫu trong batch khi train.

LayerNorm: Chuẩn hóa các features được chọn trong từng mẫu riêng biệt, không cần lấy thống kê từ các mẫu khác.

Ví dụ, đưa 32 học sinh qua một hidden layer có 3 neuron. Kết quả là một bảng 32 hàng × 3 cột: mỗi hàng là một học sinh, mỗi cột là kết quả của một neuron. BatchNorm xét từng cột qua 32 học sinh; LayerNorm trong ví dụ này xét 3 giá trị trên từng hàng. Cách hình dung theo cột và hàng giúp phân biệt hai loại mà chưa cần nhớ API.

Normalization không thay thế việc chia dữ liệu đúng và không tự chữa overfitting. Mạng nhỏ trong phần code dưới đây chưa cần thêm BatchNorm hay LayerNorm để bạn tập trung vào forward/backward. Đối chiếu hành vi mặc định tại BatchNorm1d và LayerNorm.

Code Neural Network với PyTorch

Ta tạo dữ liệu hai features có quan hệ phi tuyến, rồi train mạng 2 → 3 → 1 để phân loại nhị phân. Đây là dữ liệu giả lập, không cần tải dataset. Cài các thư viện bằng pip install torch numpy scikit-learn nếu môi trường chưa có.

import copy
import numpy as np
import torch
from torch import nn
from torch.utils.data import DataLoader, TensorDataset
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import accuracy_score, f1_score, roc_auc_score
# 1. Dữ liệu giả lập: hai feature, nhãn 0/1
torch.manual_seed(42)
rng = np.random.default_rng(42)
X = rng.normal(size=(1000, 2)).astype(np.float32)
score = X[:, 0] + 0.8 * np.maximum(X[:, 1], 0) - 0.4
y = (score + rng.normal(0, 0.25, len(X)) > 0).astype(np.float32)
# 2. Train/validation/test = 60/20/20; giữ tỉ lệ lớp
X_train, X_other, y_train, y_other = train_test_split(
X, y, test_size=0.4, random_state=42, stratify=y
)
X_val, X_test, y_val, y_test = train_test_split(
X_other, y_other, test_size=0.5, random_state=42, stratify=y_other
)
scaler = StandardScaler().fit(X_train) # chỉ fit trên train!
def tensors(features, labels):
x = torch.tensor(scaler.transform(features), dtype=torch.float32)
y = torch.tensor(labels, dtype=torch.float32).reshape(-1, 1)
return x, y
xtr, ytr = tensors(X_train, y_train)
xv, yv = tensors(X_val, y_val)
xt, yt = tensors(X_test, y_test)
loader = DataLoader(TensorDataset(xtr, ytr), batch_size=32, shuffle=True)
# 3. Logits ở output: KHÔNG thêm Sigmoid trước BCEWithLogitsLoss
model = nn.Sequential(nn.Linear(2, 3), nn.ReLU(), nn.Linear(3, 1))
nn.init.kaiming_normal_(model[0].weight, nonlinearity="relu")
nn.init.zeros_(model[0].bias)
nn.init.xavier_normal_(model[2].weight)
nn.init.zeros_(model[2].bias)
loss_fn = nn.BCEWithLogitsLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
# 4. Train; chọn checkpoint bằng validation, không dùng test
best_val = float("inf")
best_state = copy.deepcopy(model.state_dict())
patience, stale = 25, 0
history = []
model.eval()
with torch.no_grad():
print(f"Initial train loss: {loss_fn(model(xtr), ytr).item():.4f}")
for epoch in range(300):
model.train()
for xb, yb in loader:
optimizer.zero_grad() # xóa gradient từ batch trước
logits = model(xb) # forward: (batch_size, 1)
loss = loss_fn(logits, yb) # nhãn cũng (batch_size, 1)
loss.backward() # backprop tính gradient
optimizer.step() # cập nhật weights và bias
model.eval()
with torch.no_grad():
train_loss = loss_fn(model(xtr), ytr).item()
val_loss = loss_fn(model(xv), yv).item()
history.append((train_loss, val_loss))
if epoch % 25 == 0:
print(f"Epoch {epoch + 1}: train={train_loss:.4f}, val={val_loss:.4f}")
if val_loss < best_val - 1e-4:
best_val = val_loss
best_state = copy.deepcopy(model.state_dict())
stale = 0
else:
stale += 1
if stale >= patience:
break
# 5. Khôi phục model tốt nhất rồi mới đánh giá test
model.load_state_dict(best_state)
model.eval()
with torch.no_grad():
test_logits = model(xt)
test_loss = loss_fn(test_logits, yt).item()
p = torch.sigmoid(test_logits).squeeze(1).numpy()
pred = (p >= 0.5).astype(int)
print(f"Test BCE: {test_loss:.4f}")
print(f"Accuracy: {accuracy_score(y_test, pred):.3f}")
print(f"F1: {f1_score(y_test, pred, zero_division=0):.3f}")
print(f"ROC-AUC: {roc_auc_score(y_test, p):.3f}")

Autograd: Cơ chế tự theo dõi phép tính và tính gradient của thư viện.

Tensor / shape: Tensor là mảng số; shape là kích thước theo từng chiều, như (32, 2) nghĩa là 32 mẫu với 2 features.

Các điểm cần đọc kỹ:

  • BCEWithLogitsLoss kết hợp sigmoid và BCE theo cách ổn định số học. Input của loss là logits, không phải xác suất đã qua sigmoid; xem tài liệu loss chính thức.
  • zero_grad() tránh cộng dồn gradient ngoài ý muốn. PyTorch mặc định tích lũy gradient sau các lần backward.
  • model.eval() đổi hành vi các layer như Dropout/BatchNorm; torch.no_grad() tắt ghi nhận phép tính cho autograd. Hai lệnh có mục đích khác nhau. Mạng này chưa có Dropout/BatchNorm nhưng giữ thói quen đúng từ đầu.
  • history lưu train/validation loss tính ở cuối epoch với cùng chế độ eval, có thể dùng để vẽ learning curves.
  • deepcopy giữ bản weights tốt nhất, không giữ tham chiếu tiếp tục thay đổi khi train.
  • Accuracy và F1 dùng nhãn; ROC-AUC dùng xác suất. Khi dữ liệu lệch lớp, cần xem thêm confusion matrix, precision và recall.

Chuyển sang regression: giữ output cuối không có activation, đổi nhãn thành giá trị liên tục có shape (n, 1) và dùng nn.MSELoss(). Bỏ sigmoid/threshold lúc dự đoán; đánh giá bằng MAE, RMSE hoặc R². Với target liên tục, bỏ stratify=y trong cách split ở trên. Không lấy nhãn 0/1 của ví dụ này rồi gọi đó là bài toán dự đoán điểm thi.

Code Neural Network từ đầu bằng NumPy

Bản này dùng cùng kiến trúc và quy luật tạo dữ liệu, tự tính gradient rồi update bằng full-batch Gradient Descent. Dùng cả tập train mỗi bước giúp ta đọc công thức rõ hơn; bản PyTorch phía trên đã minh họa mini-batch. Hai ví dụ chạy độc lập.

import numpy as np
rng = np.random.default_rng(42)
X = rng.normal(size=(1000, 2))
score = X[:, 0] + 0.8 * np.maximum(X[:, 1], 0) - 0.4
y = (score + rng.normal(0, 0.25, len(X)) > 0).astype(float)[:, None]
indices = rng.permutation(len(X))
train_ids, test_ids = indices[:800], indices[800:]
mean, std = X[train_ids].mean(axis=0), X[train_ids].std(axis=0)
std = np.maximum(std, 1e-8)
X_train, X_test = (X[train_ids] - mean) / std, (X[test_ids] - mean) / std
y_train, y_test = y[train_ids], y[test_ids]
W1 = rng.normal(size=(2, 3)) * np.sqrt(2 / 2) # He cho ReLU
b1 = np.zeros((1, 3))
W2 = rng.normal(size=(3, 1)) * np.sqrt(2 / (3 + 1))
b2 = np.zeros((1, 1))
def sigmoid(s):
# exp luôn nhận số không dương, tránh overflow
e = np.exp(-np.abs(s))
return np.where(s >= 0, 1 / (1 + e), e / (1 + e))
def forward(x):
z1 = x @ W1 + b1
h = np.maximum(0, z1)
s = h @ W2 + b2
return z1, h, s
def bce(s, target):
# BCE từ logits: max(s, 0) - y*s + log(1 + exp(-abs(s)))
return np.mean(np.maximum(s, 0) - target * s + np.log1p(np.exp(-np.abs(s))))
lr = 0.05
for epoch in range(2000):
z1, h, s = forward(X_train)
loss = bce(s, y_train)
# Backprop; mean loss nên chia số mẫu đúng một lần
ds = (sigmoid(s) - y_train) / len(X_train)
dW2 = h.T @ ds
db2 = ds.sum(axis=0, keepdims=True)
dz1 = (ds @ W2.T) * (z1 > 0)
dW1 = X_train.T @ dz1
db1 = dz1.sum(axis=0, keepdims=True)
# Tất cả gradient đã được tính bằng weights cũ
W1 -= lr * dW1
b1 -= lr * db1
W2 -= lr * dW2
b2 -= lr * db2
if epoch % 400 == 0:
print(f"Epoch {epoch}: train BCE={loss:.4f}")
_, _, train_s = forward(X_train)
_, _, test_s = forward(X_test)
pred = sigmoid(test_s) >= 0.5
print(f"Final train BCE: {bce(train_s, y_train):.4f}")
print(f"Test BCE: {bce(test_s, y_test):.4f}")
print(f"Test accuracy: {(pred == y_test).mean():.3f}")

X_train có shape (800, 2), W1 là (2, 3), nên hidden features có shape (800, 3). Nhân tiếp W2 có shape (3, 1) cho logits (800, 1), khớp với shape nhãn. Viết nhãn thành (800,) ở đây có thể gây broadcasting sai và tạo ra một ma trận loss không mong muốn.

Bản NumPy cố định số epoch để minh họa; không dùng test để chỉnh learning rate hay chọn epoch. Khi thử nhiều cấu hình, hãy tách thêm validation như bản PyTorch. Loss thường giảm theo xu hướng với cấu hình phù hợp, không phải lời hứa giảm ở mọi iteration hoặc mọi seed.

Dùng AI để code Neural Network

Bạn có thể nhờ AI viết phần code lặp lại, nhưng cần hiểu forward/backward để kiểm tra kết quả. Trước khi tin một con số đẹp, hãy rà soát:

  • Có chia train/validation/test trước khi fit scaler không?
  • Output và loss có khớp không: logits + BCEWithLogitsLoss, hay output tuyến tính + MSE?
  • Shape và dtype của nhãn có đúng không? Có broadcasting ngoài ý muốn không?
  • Có zero_grad(), backward() và step() đúng thứ tự không?
  • Có dùng train(), eval() và no_grad() đúng mục đích không?
  • Có theo dõi validation và khôi phục checkpoint tốt nhất không? Có dùng test để chọn model không?
  • Metric có phù hợp, có so sánh với baseline đơn giản không?
  • Có seed, kiểm tra loss hữu hạn và quan sát learning curves không?

Data leakage: Thông tin từ validation/test lọt vào quá trình học, khiến kết quả đánh giá thiếu tin cậy.

Baseline: Cách dự đoán đơn giản dùng làm mốc so sánh cho neural network.

Một prompt mẫu:

Hãy viết code Python dùng PyTorch cho binary classification với hai features.
Dùng mạng Linear(2, 3) → ReLU → Linear(3, 1), BCEWithLogitsLoss và Adam.
Tạo dữ liệu giả lập phi tuyến, chia train/validation/test trước khi fit scaler.
Train theo mini-batch, theo dõi train/validation loss, early stopping và khôi phục
checkpoint tốt nhất. Đánh giá test bằng accuracy, F1, ROC-AUC và confusion matrix.
Giải thích shape ở mỗi layer, zero_grad/backward/step, train/eval/no_grad,
và vì sao không thêm sigmoid trước BCEWithLogitsLoss.
Cuối cùng chỉ ra các lỗi thường gặp và cách kiểm tra gradient bằng finite differences.

Mini project

Project 1: Dự đoán điểm từ dữ liệu giả lập. Tạo hai features giờ học và số bài luyện tập, target có quan hệ phi tuyến và nhiễu. Train mạng regression, so sánh RMSE với Linear Regression trên cùng split. Vẽ predicted vs actual; giải thích tại sao hidden features không có sẵn tên gọi chắc chắn.

Project 2: Quan sát overfitting. Dùng bài classification ở trên, giảm số mẫu train và tăng hidden neurons. Vẽ train/validation loss, sau đó thử từng thay đổi: weight decay, dropout, early stopping. Chọn cấu hình bằng validation và chỉ đánh giá test cuối cùng. Đừng thay tất cả cùng lúc vì sẽ khó biết yếu tố nào tạo khác biệt.

Tóm tắt

  1. Neuron tính tổng có trọng số cộng bias rồi áp dụng activation. Nhiều layer học các biểu diễn trung gian.
  2. Activation phi tuyến giúp mạng vượt khỏi một phép biến đổi tuyến tính; ReLU giữ phần dương và đưa phần âm về 0.
  3. Forward tạo dự đoán; loss đo sai; backprop tính gradient; optimizer cập nhật tham số.
  4. Output và loss phụ thuộc bài toán: output tuyến tính + MSE cho regression; sigmoid + BCE về mặt toán học cho binary classification.
  5. Train cần theo dõi validation, initialization, gradient và regularization, không chỉ tăng số epoch.
  6. Thư viện tự làm phép tính, nhưng người viết code vẫn phải kiểm tra dữ liệu, shape, loss và cách đánh giá.

Ôn tập

Làm 4 câu trắc nghiệm, 1 câu điền số và 2 câu tự luận ngắn. Trắc nghiệm và câu điền số được kiểm tra tự động; câu tự luận có đáp án gợi ý để bạn tự đối chiếu.

Câu 1 · Trắc nghiệmVì sao cần activation phi tuyến giữa các layer?

Câu 2 · Trắc nghiệmPhát biểu nào phân biệt đúng backpropagation và optimizer?

Câu 3 · Trắc nghiệmKhi dùng BCEWithLogitsLoss trong PyTorch, output đưa vào loss nên là gì?

Câu 4 · Trắc nghiệmTrain loss tiếp tục giảm nhưng validation loss tăng qua nhiều epoch thường gợi ý điều gì?

Câu 5 · Điền sốCó 1.000 mẫu train, batch size 32, giữ batch cuối và update một lần mỗi batch. Một epoch có bao nhiêu lần update?

Câu 6 · Tự luận ngắnMột hidden neuron có giá trị trước ReLU là −1. Output và gradient đi qua ReLU của neuron đó là gì đối với mẫu này?

Câu 7 · Tự luận ngắnVì sao không thể khẳng định một hidden neuron chắc chắn biểu diễn “mức độ chuẩn bị” của học sinh?

Chưa kiểm tra câu nào. Có 5 câu chấm tự động và 2 câu tự đối chiếu.