Back propagation là gì

    Backpropagation (Truyền ngược) là một thuật toán mà ta rất hay gặp trong các mô hình mạng học sâu (Deep Learning), nó tính toán đạo hàm thành phần phần trên các nút của mô hình (Ví dụ: Convnet, Neural Network). Các đạo hàm thành phần này được sử dụng trong suốt quá trình huấn luyện mạng. Để hiểu rõ hơn các định nghĩa và các cơ chế đi kèm, bạn đọc có thể tham khảo tại: //en.wikipedia.org/wiki/Backpropagation. Trong bài viết này, chúng ta sẽ cùng nhau xem xét cách thực hiện backpropagation trực quan và đơn giản nhất.

    Để thực hiện backpropagation một cách đơn giản,  ta sẽ biểu diễn mô hình như một đồ thị tính toán. Sau đó, ta sẽ tính forward propagation (Truyền xuôi) và đạo hàm trên mỗi block (khối).

    Các hình dưới đây minh họa cách tính forward và backward propagation trên block cơ bản như add (cộng), nhân (multiply), exp, và max. Đường màu đỏ thể hiện forward và đường màu xanh thể hiện backforward. Chúng ta sẽ tuân theo và áp dụng các quy tắc tính toán này cho các đồ thị tính toán phức tạp hơn.

Để ý rằng chúng ta có 2 đạo hàm thành phần (gradient) bởi vì chúng ta có 2 input (đầu vào). Cũng lưu ý rằng chúng ta cần phải lưu các input trước đó vào bộ nhớ cache.

    Giả sử rằng ta có một output y, là một hàm g, g lại là hàm hợp của f, f là một hàm của x. Nếu ta muốn biết g sẽ thay đổi như thế nào nếu như có một sự thay đổi nhỏ ở dx (dg/dx), ta sẽ sử dụng quy tắc dây chuyền. Đó là một quy tắc để tính toán đạo hàm của một hàm là hàm hợp của hai hay nhiều hàm khác.

    Để hiểu rõ hơn về quy tắc dây chuyền, ta hãy cùng xem hai hình bên dưới. Hình bên trái biểu diễn một nút f(x, y) sẽ tính một hàm f với 2 input x và y và cho ra một output (đầu ra) z = f(x, y). Ở hình bên phải, chúng ta có một nút tương tự nhận được đạo hàm dL/dz từ một hàm L nào đó (ví dụ hàm mất mát - loss function) với ý nghĩa: "L sẽ thay đổi như thế nào nếu có một sự thay đổi nhỏ ở z ?". Do nút có 2 input nên nó sẽ có 2 đạo hàm thành phần tương ứng. Một đạo hàm thể hiện sự thay đổi của L khi có một sự thay đổi nhỏ dx và đạo hàm còn lại tương ứng với sự thay đổi nhỏ dy.

    Để tính toán các đạo hàm thành phần, chúng ta cần đạo hàm dL/dz (dout), đạo hàm thành phần của hàm f(x, y) và sau đó ta nhân chúng lại với nhau. Chúng ta cũng cần bộ nhớ cache của input trước đó, được lưu trong suốt quá trình forward propagation.

    Trong phần này chúng ta sẽ thực hiện tính forward và backpropagation với 2 hàm đơn giản là nhân và cộng.

    Với những kiến thức cơ bản vừa trình bày, chúng ta hãy cùng tính đạo hàm thành phần của một số đồ thị.

    Dưới đây ta có đồ thị của hàm f(x, y, z) = (x + y).z

1. Bắt đầu ở nút output f, và giả sử đạo hàm của f liên quan đến một số tiêu chí là 1 (dout).

2. dq = dout(1) * z = 1 * (-4) = -4. 

3. dz = dout(1) * q = 1 * 3 = 3.

4. Ở cổng sum, như đã trình bày ở mục 2, ta có dx = dy = dq = -4.

    Đồ thị dưới đây biểu diễn forward và backpropagation của một neural network đơn giản với 2 input và 1 output layer với hàm kích hoạt (activation function) là hàm sigmoid.

1. Bắt đầu ở nút ouput, giả sử rằng dout = 1.

2. Đạo hàm tại input của 1/x là (-1/(1.37^2) ) * 1 = -0.53.

3. Nút "+1" không làm thay đổi đạo hàm tại input của nó, vì vậy nó bằng -0.53 * 1 = -0.53.

4. Nút exp có đạo hàm tại input là exp(cached input) * -0.53 = exp(-1) * -0.53 = -0.2.

5. Nút "*-1" có đạo hàm tại input là (-1 * -0.2) = 0.2.

6. Nút sum sẽ gồm có 2 thành phần đạo hàm, dw2 = 0.2 và nút sum ở thành phần còn lại có đạo hàm 0.2.

7. Tương tự như 6, nút sum có 2 thành phần đạo hàm bằng 0.2.

8. dw0 = (0.2 * -1) = -0.2, dx0 = (0.2 * 2) = 0.4.

9. dw1 = (0.2 * -2) = -0.4, dx1 = (0.2 * -3) = 0.6.

    Hi vọng với những kiến thức đã trình bày trên đây, bạn đọc đã có cái nhìn cơ bản nhất về backpropagation cũng như cách tính nó trong những đồ thị tính toán phức tạp hơn. Chúng ta sẽ còn gặp lại nó trong rất nhiều những bài viết về các mô hình học sâu mà tôi sẽ trình bày trong những bài sắp tới. 

Trong trang này:

Vì bài này sử dụng khá nhiều công thức toán, bạn đọc được khuyến khích đọc Lưu ý về ký hiệu toán học.

1. Giới thiệu

Bài toán Supervised Learning, nói một cách ngắn gọn, là việc đi tìm một hàm số để với mỗi input, ta sử dụng hàm số đó để dự đoán output. Hàm số này được xây dựng dựa trên các cặp dữ liệu \((\mathbf{x} _i, \mathbf{y}_i)\) trong training set. Nếu đầu ra dự đoán (predicted output) gần với đầu ra thực sự (ground truth) thì đó được gọi là một thuật toán tốt (nhưng khi đầu ra dự đoán quá giống với đầu ra thực sự thì không hẳn đã tốt, tôi sẽ đề cập kỹ về hiện tượng trong bài tiếp theo).

1.1. PLA cho các hàm logic cơ bản

Chúng ta cùng xét khả năng biểu diễn (representation) của Perceptron Learning Algorithm (PLA) cho các bài toán binary vô cùng đơn giản: biểu diễn các hàm số logic NOT, AND, OR, và XOR (output bằng 1 nếu và chỉ nếu hai input khác nhau). Để có thể sử dụng PLA (output là 1 hoặc -1), chúng ta sẽ thay các giá trị bằng 0 của output của các hàm này bởi -1. Trong hàng trên của Hình 1 dưới đây, các điểm hình vuông màu xanh là các điểm có label bằng 1, các điểm hình tròn màu đỏ là các điểm có label bằng -1. Hàng dưới của Hình 1 là các mô hình perceptron với các hệ số tương ứng.

Hình 1: PLA biểu diễn các hàm logic đơn giản.

Nhận thấy rằng với các bài toán OR, AND, và OR, dữ liệu là linearly separable, vì vậy ta có thể tìm được các hệ số cho perceptron giúp biểu diễn chính xác mỗi hàm số. Xem ví dụ với hàm NOT, khi \(x_1 = 0\), ta có \(a = \text{sgn}(-2 \times 0+1) = 1\). Khi \(x_1 = 1\), \(a = \text{sgn}(-2\times 1 + 1) = -1\). Trong cả hai trường hợp, predicted output đều giống với ground truth. Bạn đọc có thể tự kiểm chứng các hệ số trong hình với hàm AND và OR.

1.2. Biểu diễn hàm XOR với Neural Network.

Với hàm XOR, vì dữ liệu không linearly separable, tức không thể tìm được 1 đường thằng giúp phân chia hai lớp xanh đỏ, nên bài toán vô nghiệm. Nếu thay PLA bằng Logistic Regression, tức thay hàm activation function từ sgn sang sigmoid, ta cũng không tìm được các hệ số thỏa mãn, vì về bản chất, Logistic Regression cũng chỉ tạo ra các đường biên có dạng tuyến tính. Như vậy là các mô hình Neural Network chúng ta đã biết không thể biểu diễn được hàm số logic đơn giản này.

Nhận thấy rằng nếu cho phép sử dụng hai đường thẳng, bài toán biểu diễn hàm XOR sẽ được giải quyết như Hình 2 (trái) dưới đây:

Hình 2: Multilayer Perceptron biểu diễn hàm XOR

Các hệ số tương ứng với hai đường thẳng trong Hình 2 (trái) được minh họa trên Hình 2 (phải) tại các node màu xanh (có hai loại màu xanh). Đầu ra \(a_1^{(1)}\) bằng 1 với các điểm nằm về phía (+) của đường thẳng \(-2x_1 -2x_2 +3 = 0\), bằng -1 với các điểm nằm về phía (-) của đường thẳng này. Tương tự, đầu ra \(a_2^{(1)}\) bằng 1 với các điểm nằm về phía (+) của đường thẳng \(2x_1 + 2x_2 - 1 = 0\). Như vậy, hai đường thằng này tạo ra hai đầu ra tại các node \(a_1^{(1)}, a_2^{(1)}\). Vì hàm XOR chỉ có một đầu ra nên ta cần làm thêm một bước nữa: coi \(a_1, a_2\) như là input của một PLA khác. Trong PLA mới này, input là các node màu lam (đừng quên node bias có giá trị bằng 1), output là các node màu đỏ. Các hệ số được cho trên Hình 2 (phải). Kiểm tra lại một chút, với các điểm hình vuông xanh (hình trái), \(a^{(1)}_1 = a^{(1)}_2 = 1\), khi đó \(a^{(2)} = \text{sgn}(1 + 1 - 1) = 1\). Với các điểm hình tròn đỏ, \(a^{(1)}_1 = -a^{(1)}_2\), vậy nên \(a^{(2)} = \text{sgn}(a^{(1)}_1 + a^{(1)}_2 - 1) = \text{sgn}(-1) = -1\). Trong cả hai trường hợp, predicted ouput đều giống với ground truth. Vậy, nếu ta sử dụng 3 PLA tương ứng với các output \(a^{(1)}_1, a^{(1)}_2, a^{(2)}\), ta sẽ biểu diễn được hàm XOR.

Ba PLA kể trên được xếp vào hai layers. Layer thứ nhất: input - lục, output - lam. Layer thứ hai: input - lam, output - đỏ. Ở đây, output của layer thứ nhất chính là input của layer thứ hai. Tổng hợp lại ta được một mô hình mà ngoài layer input (lục) và output (đỏ), ta còn có một layer nữa (lam). Mô hình này có tên gọi là Multi-layer Perceptron (MLP). Layer trung gian ở giữa còn được gọi là hidden layer.

Một vài lưu ý:

  • Perceptron Learing Algorithm là một trường hợp của single-layer neural network với activation fucntion là hàm sgn. Trong khi đó, Perceptron là tên chung để chỉ các Neural Network với chỉ một input layer và một output tại output layer, không có hidden layer.

  • Các activation function có thể là các nonlinear function khác, ví dụ như sigmoid function hoặc tanh function. Các activation function phải là nonlinear (phi tuyến), vì nếu không, nhiều layer hay một layer cũng là như nhau. Ví dụ với hai layer trong Hình 2, nếu activation function là một hàm linear (giả sử hàm \(f(s) = s\)), thì cả hai layer có thể được thay bằng một layer với ma trận hệ số \(\mathbf{W} = \mathbf{W}^{(1)}\mathbf{W}^{(2)}\) (tạm bỏ qua biases).

  • Để cho đơn giản, tôi đã sử dụng ký hiệu \(\mathbf{W}^{(l)T}\) để thay cho \((\mathbf{W}^{(l)})^T\) (ma trận chuyển vị). Trong Hình 2 (phải), tôi sử dụng ký hiệu ma trận \(\mathbf{W}^{(2)}\), mặc dù đúng ra nó phải là vector, để biểu diễn tổng quát cho trường hợp output layer có thể có nhiều hơn 1 node. Tương tự với bias \(\mathbf{b}^{(2)}\).

  • Khác với các bài trước về Neural Networks, khi làm việc với MLP, ta nên tách riêng phần biases và ma trận hệ số ra. Điều này đồng nghĩa với việc vector input \(\mathbf{x}\) là vector KHÔNG mở rộng.

2. Các ký hiệu và khái niệm

2.1. Layers

Ngoài Input layers và Output layers, một Multi-layer Perceptron (MLP) có thể có nhiều Hidden layers ở giữa. Các Hidden layers theo thứ tự từ input layer đến output layer được đánh số thứ thự là Hidden layer 1, Hidden layer 2, … Hình 3 dưới đây là một ví dụ với 2 Hidden layers.

Hình 3: MLP với hai hidden layers (các biases đã bị ẩn).

Số lượng layer trong một MLP được tính bằng số hidden layers cộng với 1. Tức là khi đếm số layers của một MLP, ta không tính input layers. Số lượng layer trong một MLP thường được ký hiệu là \(L\). Trong Hình 3 trên đây, \(L = 3\).

2.2. Units

Một node hình tròn trong một layer được gọi là một unit. Unit ở các input layer, hidden layers, và output layer được lần lượt gọi là input unit, hidden unit, và output unit. Đầu vào của các hidden layer được ký hiệu bởi \(z\), đầu ra của mỗi unit thường được ký hiệu là \(a\) (thể hiện activation, tức giá trị của mỗi unit sau khi ta áp dụng activation function lên \(z\)). Đầu ra của unit thứ \(i\) trong layer thứ \(l\) được ký hiệu là \(a_i^{(l)}\). Giả sử thêm rằng số unit trong layer thứ \(l)\) (không tính bias) là \(d^{(l)}\). Vector biểu diễn output của layer thứ \(l\) được ký hiệu là \(\mathbf{a}^{(l)} \in \mathbb{R}^{d^{(l)}}\).

Khi làm việc với những Neural Networks phức tạp, cách tốt nhất để hạn chế lỗi là viết cụ thể chiều của mỗi ma trận hay vector ra, bạn sẽ thấy rõ hơn trong phần sau.

Hình 4: Các ký hiệu sử dụng trong MLP.

2.3. Weights và Biases

Có \(L\) ma trận trọng số cho một MLP có \(L\) layers. Các ma trận này được ký hiệu là \(\mathbf{W}^{(l)} \in \mathbb{R}^{d^{(l-1)}\times d^{(l)}}, l = 1, 2, \dots, L\) trong đó \(\mathbf{W}^{(l)}\) thể hiện các kết nối từ layer thứ \(l-1\) tới layer thứ \(l\) (nếu ta coi input layer là layer thứ \(0\)). Cụ thể hơn, phần tử \(w^{(l)}_{ij}\) thể hiện kết nối từ node thứ \(i\) của layer thứ \((l-1)\) tới node từ \(j\) của layer thứ \((l)\). Các biases của layer thứ \((l)\) được ký hiệu là \(\mathbf{b}^{(l)} \in \mathbb{R}^{d^{(l)}}\). Các trọng số này được ký hiệu như trên Hình 4. Khi tối ưu một MLP cho một công việc nào đó, chúng ta cần đi tìm các weghts và biases này.

Tập hợp các weights và biases lần lượt được ký hiệu là \(\mathbf{W}\) và \(\mathbf{b}\).

2.4. Activation functions

(Phần này chú yếu được dịch lại từ: //cs231n.github.io/neural-networks-1/ )

Mỗi output của một unit (trừ các input units) được tính dựa vào công thức: \[ a_i^{(l)} = f(\mathbf{w}_i^{(l)T}\mathbf{a}^{(l-1)} + b_i^{(l)}) \]

Trong đó \(f(.)\) là một (nonlinear) activation function. Ở dạng vector, biểu thức bên trên được viết là:

\[ \mathbf{a}^{(l)} = f(\mathbf{W}^{(l)T}\mathbf{a}^{(l-1)} + \mathbf{b}^{(l)}) \]

Khi activation function \(f(.)\) được áp dụng cho một ma trận (hoặc vector), ta hiểu rằng nó được áp dụng cho từng thành phần của ma trận đó. Sau đó các thành phần này được sắp xếp lại đúng theo thứ tự để được một ma trận có kích thước bằng với ma trận input. Trong tiếng Anh, việc áp dụng lên từng phần tử như thế này được gọi là element-wise.

2.4.1. Hàm sgn không được sử dụng trong MLP

Hàm sgn (còn gọi là hard-threshold) chỉ được sử dụng trong PLA, mang mục đích giáo dục nhiều hơn. Trong thực tế, hàm sgn không được sử dụng vì hai lý do: đầu ra là discrete, và đạo hàm tại hầu hết các điểm bằng 0 (trừ điểm 0 không có đạo hàm). Việc đạo hàm bằng 0 này khiến cho các thuật toán gradient-based (ví dụ như Gradient Descent) không hoạt động!

2.4.1 Sigmoid và tanh

Hàm sigmoid có dạng \(f(s) = 1/(1 + \exp(-s))\) với đồ thị như trong Hình 5 (trái). Nếu đầu vào lớn, hàm số sẽ cho đầu ra gần với 1. Với đầu vào nhỏ (rất âm), hàm số sẽ cho đầu ra gần với 0. Hàm số này được sử dụng nhiều trong quá khứ ví có đạo hàm rất đẹp. Những năm gần đây, hàm số này ít khi được sử dụng. Nó có một nhược điểm cơ bản:

  • Sigmoid saturate and kill gradients: Một nhược điểm dễ nhận thấy là khi đầu vào có trị tuyệt đối lớn (rất âm hoặc rất dương), gradient của hàm số này sẽ rất gần với 0. Điều này đồng nghĩa với việc các hệ số tương ứng với unit đang xét sẽ gần như không được cập nhật. Bạn đọc sẽ hiểu rõ hơn phần này trong phần Backpropagation.

Hàm tanh cũng có nhược điểm tương tự về việc gradient rất nhỏ với các đầu vào có trị tuyệt đối lớn.

2.4.3. ReLU

Hình 5: Hàm ReLU và tốc độ hội tụ khi so sánh với hàm tanh.

ReLU (Rectified Linear Unit) được sử dụng rộng rãi gần đây vì tính đơn giản của nó. Đồ thị của hàm ReLU được minh họa trên Hình 5 (trái)). Nó có công thức toán học \(f(s) = \max(0, s)\) - rất đơn giản. Ưu điểm chính của nó là:

  • ReLU được chứng minh giúp cho việc training các Deep Networks nhanh hơn rất nhiều (theo Krizhevsky et al.). Hình 5 (phải) so sánh sự hội tụ của SGD khi sử dụng hai activation function khác nhau: ReLU và tanh. Sự tăng tốc này được cho là vì ReLU được tính toán gần như tức thời và gradient của nó cũng được tính cực nhanh với gradient bằng 1 nếu đầu vào lớn hơn 0, bằng 0 nếu đầu vào nhỏ hơn 0.

  • Mặc dù hàm ReLU không có đạo hàm tại \(s = 0\), trong thực nghiệm, người ta vẫn thường định nghĩa \(\text{ReLU}’(0) = 0\) và khẳng định thêm rằng, xác suất để input của một unit bằng 0 là rất nhỏ.

Hàm ReLU có nhiều biến thể khác như Noisy ReLU, Leaky ReLu, ELUs. Tôi xin phép dừng phần này ở đây vì chưa có ý định đi sâu vào Deep Neural Networks.

2.4.4. Một vài lưu ý

  • Output layer nhiều khi không có activation function mà sử dụng trực tiếp giá trị đầu vào \(z_i^{(l)}\) của mỗi unit. Hoặc nói một cách khác, activation function chính là hàm identity, tức đầu ra bằng đầu vào. Với các bài toán classification, output layer thường là một Softmax Regression layer giúp tính xác suất để một điểm dữ liệu rơi vào mỗi class.

  • Mặc dù activation function cho mỗi unit có thể khác nhau, trong cùng một network, activation như nhau thường được sử dụng. Điều này giúp cho việc tính toán được đơn giản hơn.

3. Backpropagation

Phần này khá nặng về Đại Số Tuyến Tính, bạn đọc không muốn hiểu backpropagation có thể bỏ qua để đọc tiếp phần Ví dụ với Python.

Phương pháp phổ biến nhất để tối ưu MLP vẫn là Gradient Descent (GD). Để áp dụng GD, chúng ta cần tính được gradient của hàm mất mát theo từng ma trận trọng số \(\mathbf{W}^{(l)}\) và vector bias \(\mathbf{b}^{(l)}\). Trước hết, chúng ta cần tính predicted output \( \mathbf{\hat{y}}\) với một input \(\mathbf{x}\): \[ \begin{eqnarray} \mathbf{a}^{(0)} &=& \mathbf{x} \ z_{i}^{(l)} &=& \mathbf{w}_i^{(l)T}\mathbf{a}^{(l-1)} + b_i^{(l)} \ \mathbf{z}^{(l)} &=& \mathbf{W}^{(l)T}\mathbf{a}^{(l-1)} + \mathbf{b}^{(l)},~~ l = 1, 2, \dots, L \ \mathbf{a}^{(l)} &=& f(\mathbf{z}^{(l)}), ~~ l = 1, 2, \dots, L \

\mathbf{\hat{y}} &=& \mathbf{a}^{(L)} \end{eqnarray} \]

Bước này được gói là feedforward vì cách tính toán được thực hiện từ đầu đến cuối của network. MLP cũng được gọi

Giả sử \(J(\mathbf{W, b, X, Y})\) là một hàm mất mát của bài toán, trong đó \(\mathbf{W, b}\) là tập hợp tất cả các ma trận trọng số giữa các layers và biases của mỗi layer. \(\mathbf{X, Y}\) là cặp dữ liệu huấn luyện với mỗi cột tương ứng với một điểm dữ liệu. Để có thể áp dụng các gradient-based methods (mà Gradient Descent là một ví dụ), chúng ta cần tính được: \[ \frac{\partial J}{\partial \mathbf{W}^{(l)}} ; \frac{\partial J}{\partial \mathbf{b}^{(l)}},~~ l = 1, 2, \dots, L \]

Một ví dụ của hàm mất mát là hàm Mean Square Error (MSE) tức trung bình của bình phương lỗi. \[ \begin{eqnarray} J(\mathbf{W, b, X, Y}) &=& \frac{1}{N}\sum_{n=1}^N || \mathbf{y}_n - \mathbf{\hat{y}}_n||_2^2 \
&=&\frac{1}{N}\sum_{n=1}^N || \mathbf{y}_n - \mathbf{a}_n^{(L)}||_2^2 \end{eqnarray} \] Với \(N\) là số cặp dữ liệu \((\mathbf{x}, \mathbf{y})\) trong tập training.

Theo những công thức ở trên, việc tính toán trực tiếp giá trị này là cực kỳ phức tạp vì hàm mất mát không phụ thuộc trực tiếp vào các hệ số. Phương pháp phổ biến nhất được dùng có tên là Backpropagation giúp tính gradient ngược từ layer cuối cùng đến layer đầu tiên. Layer cuối cùng được tính toán trước vì nó gần gũi hơn với predicted outputs và hàm mất mát. Việc tính toán gradient của các layer trước được thực hiện dựa trên một quy tắc quen thuộc có tên là chain rule, tức đạo hàm của hàm hợp.

Stochastic Gradient Descent có thể được sử dụng để tính gradient cho các ma trận trọng số và biases dựa trên một cặp điểm training \(\mathbf{x, y}\). Để cho đơn giản, ta coi \(J\) là hàm mất mát nếu chỉ xét cặp điểm này, ở đây \(J\) là hàm mất mát bất kỳ, không chỉ hàm MSE như ở trên.

Đạo hàm của hàm mất mát theo chỉ một thành phần của ma trận trọng số của lớp cuối cùng:

\[ \begin{eqnarray} \frac{\partial J}{\partial w_{ij}^{(L)}} &=& \frac{\partial J}{\partial z_j^{(L)}}. \frac{\partial z_j^{(L)}}{\partial w_{ij}^{(L)}} \
&=& e_j^{(L)} a_i^{(L-1)} \end{eqnarray} \]

Trong đó \(e_j^{(L)} = \frac{\partial J}{\partial z_j^{(L)}} \) thường là một đại lượng dễ tính toán và \(\frac{\partial z_j^{(L)}}{\partial w_{ij}^{(L)}} = a_i^{(L-1)}\) vì \(z_j^{(L)} = \mathbf{w}_j^{(L)T}\mathbf{a}^{(L-1)} + b_j^{(L)}\).

Tương tự như thế, đạo hàm của hàm mất mát theo bias của layer cuối cùng là: \[ \frac{\partial J}{\partial b_{j}^{(L)}} = \frac{\partial J}{\partial z_j^{(L)}}. \frac{\partial z_j^{(L)}}{\partial b_{j}^{(L)}} = e_j^{(L)} \]

Với đạo hàm theo hệ số ở các lớp \(l\) thấp hơn, chúng ta hay xem hình dưới đây. Ở đây, tại mỗi unit, tôi đã viết riêng đầu vào \(z\) và đầu ra \(a\) để các bạn tiện theo dõi.

Hình 6: Mô phỏng cách tính backpropagation. Layer cuối có thể là output layer.

Dựa vào hính trên, ta có thể tính được:

\[ \begin{eqnarray} \frac{\partial J}{\partial w_{ij}^{(l)}} &=& \frac{\partial J}{\partial z_j^{(l)}}. \frac{\partial z_j^{(l)}}{\partial w_{ij}^{(l)}} \
&=& e_j^{(l)} a_i^{(l-1)} \end{eqnarray} \] với:

\[ \begin{eqnarray} e_j^{(l)} &=& \frac{\partial J}{\partial z_j^{(l)}} = \frac{\partial J}{\partial a_j^{(l)}} . \frac{\partial a_j^{(l)}}{\partial z_j^{(l)}} \ &=& \left( \sum_{k = 1}^{d^{(l+1)}} \frac{\partial J}{\partial z_k^{(l+1)}} .\frac{\partial z_k^{(l+1)}}{\partial a_j^{(l)}} \right) f’(z_j^{(l)}) \ &=&\left( \sum_{k = 1}^{d^{(l+1)}} e_k^{(l+1)} w_{jk}^{(l+1)} \right) f’(z_j^{(l)}) \ &=&\left( \mathbf{w}_{j:}^{(l+1)} \mathbf{e}^{(l+1)} \right) f’(z_j^{(l)}) \

\end{eqnarray} \]

trong đó \(\mathbf{e}^{(l+1)} = [e_1^{(l+1)}, e_2^{(l+1)}, …, e_{d^{(l+1)}}^{(l+1)}]^T \in \mathbb{R}^{d^{(l+1)}\times 1} \) và \(\mathbf{w}_{j:}^{(l+1)}\) được hiểu là hàng thứ \(j\) của ma trận \(\mathbf{W}^{(l+1)}\) (Chú ý dấu hai chấm, khi không có dấu này, tôi mặc định ký hiệu nó cho vector cột).

Dấu sigma tính tổng ở hàng thứ hai trong phép tính trên xuất hiện vì \(a_{j}^{(l)}\) đóng góp vào việc tính tất cả các \(z_k^{(l+1)}, k = 1, 2, \dots, d^{(l+1)}\). Biểu thức đạo hàm ngoài dấu ngoặc lớn là vì \(a_j^{(l)} = f(z_j^{(l)})\). Tới đây, ta có thể thấy rằng việc activation function có đạo hàm đơn giản sẽ có ích rất nhiều trong việc tính toán.

Với cách làm tương tự, bạn đọc có thể suy ra: \[ \frac{\partial J}{\partial b_j^{(l)}} = e_j^{(l)} \]

Nhận thấy rằng trong các công thức trên đây, việc tính các \(e_j^{(l)}\) đóng một vài trò quan trọng. Hơn nữa, để tính được giá trị này, ta cần tính được các \(e_j^{(l+1)}\). Nói cách khác, ta cần tính ngược các giá trị này từ cuối. Cái tên backpropagation cũng xuất phát từ việc này.

Việc tính toán các đạo hàm khi sử dụng SGD có thể tóm tắt như sau:

3.1. Backpropagation cho Stochastic Gradient Descent

3.1.1. Đạo hàm theo từng hệ số \(w_{ij}^{(l)}, b_{i}^{(l)}\)

  1. Bước feedforward: Với 1 giá trị đầu vào \(\mathbf{x}\), tính giá trị đầu ra của network, trong quá trình tính toán, lưu lại các activation \(\mathbf{a}^{(l)}\) tại mỗi layer.
  2. Với mỗi unit \(j\) ở output layer, tính \[e_j^{(L)} = \frac{\partial J}{\partial z_j^{(L)}}\]
  3. Từ đó suy ra: \[ \begin{eqnarray} \frac{\partial J}{\partial w_{ij}^{(L)}} &=& a_i^{(L-1)}e_j^{(L)} \
    \frac{\partial J}{\partial b_{j}^{(L)}} &=& e_j^{(L)} \end{eqnarray} \]
  4. Với \(l = L-1, L-2, …, 1\), tính: \[ e_j^{(l)} = \left( \mathbf{w}_{j:}^{(l+1)} \mathbf{e}^{(l+1)} \right) f’(z_j^{(l)}) \]
  5. Cập nhật đạo hàm cho từng hệ số: \[ \begin{eqnarray} \frac{\partial J}{\partial w_{ij}^{(l)}} &=& a_i^{(l-1)} e_j^{(l)} \
    \frac{\partial J}{\partial b_{j}^{(l)}} &=& e_j^{(l)} \end{eqnarray} \]

3.1.2. Đạo hàm theo ma trận \(\mathbf{W}^{(l)}, \mathbf{b}^{(l)}\)

Việc tính toán theo từng hệ số như trên chỉ phù hợp cho việc hiểu nguyên lý tính toán, trong khi lập trình, ta cần tìm cách thu gọn chúng về dạng vector và ma trận để tăng tốc độ cho thuật toán. Đặt \(\mathbf{e}^{(l)} = [e_1^{(l)}, e_2^{(l)}, …, e_{d^{(l)}}^{(l)}]^T \in \mathbb{R}^{d^{(l)}\times 1} \). Ta sẽ có quy tắc tính như sau:

  1. Bước feedforward: Với 1 giá trị đầu vào \(\mathbf{x}\), tính giá trị đầu ra của network, trong quá trình tính toán, lưu lại các activation \(\mathbf{a}^{(l)}\) tại mỗi layer.
  2. Với output layer, tính: \[\mathbf{e}^{(L)} = \frac{\partial J}{\partial \mathbf{z}^{(L)}}\]
  3. Từ đó suy ra: \[ \begin{eqnarray} \frac{\partial J}{\partial \mathbf{W}^{(L)}} &=& \mathbf{a}^{(L-1)}\mathbf{e}^{(L)T}\
    \frac{\partial J}{\partial \mathbf{b}^{(L)}} &=& \mathbf{e}^{(L)} \end{eqnarray} \]
  4. Với \(l = L-1, L-2, …, 1\), tính: \[ \mathbf{e}^{(l)} = \left( \mathbf{W}^{(l+1)} \mathbf{e}^{(l+1)} \right) \odot f’(\mathbf{z}^{(l)}) \] trong đó \(\odot\) là element-wise product hay Hadamard product tức lấy từng thành phần của hai vector nhân với nhau để được vector kết quả.
  5. Cập nhật đạo hàm cho ma trận trọng số và vector biases: \[ \begin{eqnarray} \frac{\partial J}{\partial \mathbf{W}^{(l)}} &=& \mathbf{a}^{(l-1)}\mathbf{e}^{(l)T}\
    \frac{\partial J}{\partial \mathbf{b}^{(l)}} &=& \mathbf{e}^{(l)} \end{eqnarray} \]

Chú ý: Biểu thức tính đạo hàm trong dòng trên của bước 3 có thể khiến bạn đặt câu hỏi: tại sao lại là \(\mathbf{a}^{(L-1)}\mathbf{e}^{(L)T}\) mà không phải là \(\mathbf{a}^{(L-1)T}\mathbf{e}^{(L)}\), \(\mathbf{e}^{(L)T}\mathbf{a}^{(L-1)}\), hay \(\mathbf{e}^{(L)}\mathbf{a}^{(L-1)T}\)? Quy tắc bỏ túi cần nhớ là chiều của hai ma trận ở hai vế phải như nhau. Thử một chút, vế trái là đạo hàm theo \(\mathbf{W}^{(L)}\) là một đại lượng có chiều (dimension, not afternoon) bằng chiều của ma trận này, tức chiều là \(\mathbb{R}^{d^{(L-1)}\times d^{(L)}}\). Vế phải, \(\mathbf{e}^{(L)} \in \mathbf{R}^{d^{(L)} \times 1}\), \(\mathbf{a}^{(L-1)} \in \mathbb{R}^{d^{(L-1)} \times 1}\). Để hai vế có chiều bằng nhau thì ta phải lấy \(\mathbf{a}^{(L-1)} \mathbf{e}^{(L)T}\). Cũng chú ý thêm rằng đạo hàm theo một ma trận của một hàm số nhận giá trị thực (scalar) sẽ có chiều bằng với chiều của ma trận đó!!

3.2. Backpropagation cho Batch (mini-batch) Gradient Descent

Nếu chúng ta muốn thực hiện Batch hoặc mini-batch Gradient Descent thì sao? Trong thực tế, mini-batch GD được sử dụng nhiều nhất. Nếu lượng dữ liệu là nhỏ, Batch GD trực tiếp được sử dụng.

Khi đó, cặp (input, output) sẽ ở dạng ma trận \((\mathbf{X, Y})\). Giả sử rằng mỗi lần tính toán, ta lấy \(N\) dữ liệu để tính toán. Khi đó, \(\mathbf{X} \in \mathbb{R}^{d^{(0)} \times N}, \mathbf{Y} \in \mathbb{R}^{d^{(L)}\times N}\). Với \(d^{(0)} = d\) là chiều của dữ liệu đầu vào (không tính bias).

Khi đó các activation sau mỗi layer sẽ có dạng \(\mathbf{A}^{(l)} \in \mathbb{R}^{d^{(l)} \times N}\). Tương tự thế, \(\mathbf{E}^{(l)} \in \mathbb{R}^{d^{(l)}\times N}\). Và ta cũng có thể suy ra công thức cập nhật như sau.

  1. Bước feedforward: Với toàn bộ dữ liệu (batch) hoặc một nhóm dữ liệu (mini-batch) đầu vào \(\mathbf{X}\), tính giá trị đầu ra của network, trong quá trình tính toán, lưu lại các activation \(\mathbf{A}^{(l)}\) tại mỗi layer. Mỗi cột của \(\mathbf{A}^{(l)}\) tương ứng với một cột của \(\mathbf{X}\), tức một điểm dữ liệu đầu vào.
  2. Với output layer, tính: \[\mathbf{E}^{(L)} = \frac{\partial J}{\partial \mathbf{Z}^{(L)}}\]
  3. Từ đó suy ra: \[ \begin{eqnarray} \frac{\partial J}{\partial \mathbf{W}^{(L)}} &=& \mathbf{A}^{(L-1)}\mathbf{E}^{(L)T}\
    \frac{\partial J}{\partial \mathbf{b}^{(L)}} &=& \sum_{n=1}^N\mathbf{e}_n^{(L)} \end{eqnarray} \]
  4. Với \(l = L-1, L-2, …, 1\), tính: \[ \mathbf{E}^{(l)} = \left( \mathbf{W}^{(l+1)} \mathbf{E}^{(l+1)} \right) \odot f’(\mathbf{Z}^{(l)}) \] trong đó \(\odot\) là element-wise product hay Hadamard product tức lấy từng thành phần của hai ma trận nhân với nhau để được ma trận kết quả.
  5. Cập nhật đạo hàm cho ma trận trọng số và vector biases: \[ \begin{eqnarray} \frac{\partial J}{\partial \mathbf{W}^{(l)}} &=& \mathbf{A}^{(l-1)}\mathbf{E}^{(l)T}\
    \frac{\partial J}{\partial \mathbf{b}^{(l)}} &=& \sum_{n=1}^N\mathbf{e}_n^{(l)} \end{eqnarray} \]

Mặc dù khi làm thực nghiệm, các công cụ có hỗ trợ việc tự động tính Backpropagation, tôi vẫn không muốn bỏ qua phần này. Hiểu backpropagation rất quan trọng! Xem thêm Yes you should understand backprop.

4. Ví dụ trên Python

Source code cho ví dụ này có thể được xem tại đây.

Ví dụ tôi nêu trong mục này mang mục đích giúp các bạn hiểu thực sự cách lập trình cho backpropagation. Khi làm thực nghiệm, chúng ta sử dụng các thư viện sẵn có giúp tính backpropagation. Ví dụ như Sklearn cho MLP.

Để kiểm chứng lại những gì tôi viết trên đây có đúng không, chúng ta cùng xem một ví dụ. Ý tưởng trong ví dụ này được lấy từ CS231n Convolutional Neural Networks for Visual Recognition, phần code dưới đây tôi viết lại cho phù hợp với những tính toán và ký hiệu phía trên.

4.1. Tạo dữ liệu giả

Trước hết, ta tạo dữ liệu cho 3 classes mà không có hai class nào là linearly separable:

# To support both python 2 and python 3 from __future__ import division, print_function, unicode_literals import math import numpy as np import matplotlib.pyplot as plt N = 100 # number of points per class d0 = 2 # dimensionality C = 3 # number of classes X = np.zeros((d0, N*C)) # data matrix (each row = single example) y = np.zeros(N*C, dtype='uint8') # class labels for j in xrange(C): ix = range(N*j,N*(j+1)) r = np.linspace(0.0,1,N) # radius t = np.linspace(j*4,(j+1)*4,N) + np.random.randn(N)*0.2 # theta X[:,ix] = np.c_[r*np.sin(t), r*np.cos(t)].T y[ix] = j # lets visualize the data: # plt.scatter(X[:N, 0], X[:N, 1], c=y[:N], s=40, cmap=plt.cm.Spectral) plt.plot(X[0, :N], X[1, :N], 'bs', markersize = 7); plt.plot(X[0, N:2*N], X[1, N:2*N], 'ro', markersize = 7); plt.plot(X[0, 2*N:], X[1, 2*N:], 'g^', markersize = 7); # plt.axis('off') plt.xlim([-1.5, 1.5]) plt.ylim([-1.5, 1.5]) cur_axes = plt.gca() cur_axes.axes.get_xaxis().set_ticks([]) cur_axes.axes.get_yaxis().set_ticks([]) plt.savefig('EX.png', bbox_inches='tight', dpi = 600) plt.show()

Hình 7: Phân bố dữ liệu theo class.

Với dữ liệu được phân bố thế này, Softmax Regression không thể thực hiện được vì Bounray giữa các class tạo bởi Softmax Regression có dạng linear. Chúng ta hãy làm một thí nghiệm nhỏ bằng cách thêm một Hidden layer vào giữa Input layer vả output layer của Softmax Regression. Activation function của Hidden layer là hàm ReLU: \(f(s) = \max(s, 0)\), \(f’(s) = 0 ~~\text{if}~ s \leq 0\), \(f’(s) = 1 ~\text{otherwise}\).

Hình 8: 2-layer Neural Networks.

Bây giờ chúng ta sẽ áp dụng Batch Gradient Descent cho bài toán này (vì lượng dữ liệu là nhỏ). Trước hết cần thực tìm công thức tính các activation và output.

4.2. Tính toán Feedforward

\[ \begin{eqnarray} \mathbf{Z}^{(1)} &=& \mathbf{W}^{(1)T}\mathbf{X} \ \mathbf{A}^{(1)} &=& \max(\mathbf{Z}^{(1)}, \mathbf{0}) \ \mathbf{Z}^{(2)} &=& \mathbf{W}^{(2)T}\mathbf{A}^{(1)} \

\mathbf{\hat{Y}} = \mathbf{A}^{(2)} &=& \text{softmax}(\mathbf{Z}^{(2)}) \end{eqnarray} \]

Hàm mất mát được tính như sau:

\[ J \triangleq J(\mathbf{W, b}; \mathbf{X, Y}) = -\frac{1}{N}\sum_{i = 1}^N \sum_{j = 1}^C y_{ji}\log(\hat{y}_{ji}) \] Ở đây, tôi đã cho thêm thừa số \(\frac{1}{N}\) để tránh hiện tượng tổng quá lớn với Batch GD. Về mặt toán học, thừa số này không làm thay đổi nghiệm của bài toán.

4.3. Tính toán Backpropagation

Áp dụng quy tắc như đã trình bày ở trên và và đạo hàm theo ma trận trọng số của Softmax Regression, ta có:

\[ \begin{eqnarray} \mathbf{E}^{(2)} &=& \frac{\partial J}{\partial \mathbf{Z}^{(2)}} =\frac{1}{N}(\mathbf{\hat{Y}} - \mathbf{Y}) \ \frac{\partial J}{\partial \mathbf{W}^{(2)}} &=& \mathbf{A}^{(1)} \mathbf{E}^{(2)T} \ \frac{\partial J}{\partial \mathbf{b}^{(2)}} &=& \sum_{n=1}^N\mathbf{e}_n^{(2)} \ \mathbf{E}^{(1)} &=& \left(\mathbf{W}^{(2)}\mathbf{E}^{(2)}\right) \odot f’(\mathbf{Z}^{(1)}) \ \frac{\partial J}{\partial \mathbf{W}^{(1)}} &=& \mathbf{A}^{(0)} \mathbf{E}^{(1)T} = \mathbf{X}\mathbf{E}^{(1)T}\ \frac{\partial J}{\partial \mathbf{b}^{(1)}} &=& \sum_{n=1}^N\mathbf{e}_n^{(1)} \

\end{eqnarray} \]

Từ đó ta có thể bắt đầu lập trình như sau:

4.4. Một số hàm phụ trợ

def softmax(V): e_V = np.exp(V - np.max(V, axis = 0, keepdims = True)) Z = e_V / e_V.sum(axis = 0) return Z ## One-hot coding from scipy import sparse def convert_labels(y, C = 3): Y = sparse.coo_matrix((np.ones_like(y), (y, np.arange(len(y)))), shape = (C, len(y))).toarray() return Y # cost or loss function def cost(Y, Yhat): return -np.sum(Y*np.log(Yhat))/Y.shape[1]

4.4. Phần chương trình chính

d0 = 2 d1 = h = 100 # size of hidden layer d2 = C = 3 # initialize parameters randomly W1 = 0.01*np.random.randn(d0, d1) b1 = np.zeros((d1, 1)) W2 = 0.01*np.random.randn(d1, d2) b2 = np.zeros((d2, 1)) Y = convert_labels(y, C) N = X.shape[1] eta = 1 # learning rate for i in xrange(10000): ## Feedforward Z1 = np.dot(W1.T, X) + b1 A1 = np.maximum(Z1, 0) Z2 = np.dot(W2.T, A1) + b2 Yhat = softmax(Z2) # print loss after each 1000 iterations if i %1000 == 0: # compute the loss: average cross-entropy loss loss = cost(Y, Yhat) print("iter %d, loss: %f" %(i, loss)) # backpropagation E2 = (Yhat - Y )/N dW2 = np.dot(A1, E2.T) db2 = np.sum(E2, axis = 1, keepdims = True) E1 = np.dot(W2, E2) E1[Z1 <= 0] = 0 # gradient of ReLU dW1 = np.dot(X, E1.T) db1 = np.sum(E1, axis = 1, keepdims = True) # Gradient Descent update W1 += -eta*dW1 b1 += -eta*db1 W2 += -eta*dW2 b2 += -eta*db2

iter 0, loss: 1.098815 iter 1000, loss: 0.150974 iter 2000, loss: 0.057996 iter 3000, loss: 0.039621 iter 4000, loss: 0.032148 iter 5000, loss: 0.028054 iter 6000, loss: 0.025346 iter 7000, loss: 0.023311 iter 8000, loss: 0.021727 iter 9000, loss: 0.020585

4.5. Kết quả

Như vậy hàm mất mát giảm dần khi số vòng lặp tăng lên. Bây giờ chúng ta cùng áp dụng ngược network này vào phân loại dữ liệu training:

Z1 = np.dot(W1.T, X) + b1 A1 = np.maximum(Z1, 0) Z2 = np.dot(W2.T, A1) + b2 predicted_class = np.argmax(Z2, axis=0) print('training accuracy: %.2f %%' % (100*np.mean(predicted_class == y)))

training accuracy: 99.33 %

Vậy là trong 300 điểm, chỉ có 2 điểm bị phân loại sai! Dưới đây là hình minh hoạ khu vực của mỗi class:

Hình 9: Kết quả khi sử dụng 1 hidden layer với 100 units.

Hai điểm bị phân lớp sai có lẽ nằm gần khu vực trung tâm.

Vậy là chỉ thêm 1 hidden layer, Neural Network đã có thể xây dựng được boundary phi tuyến. Kết luận đầu tiên ở đây là khả năng biểu diễn của MLP tốt hơn rất nhiều so với 1-layer Neural Network.

Kết quả bên trên được thực hiện khi số lượng units trong hidden layer là d1 = 100. Chúng ta thử thay đổi giá trị này bởi d1 = 5, 10, 15, 20 xem kết quả khác nhau như thế nào. Dưới đây là hình mình họa:

Hình 10: Kết quả với số lượng units trong hidden layer là khác nhau.

Có một vài nhận xét như sau:

  • Khi số lượng hidden units tăng lên, độ chính xác của mô hình tạo được cũng tăng lên.

  • Với d1 = 5, đường phân định giữa ba classes gần như là đường thẳng.

  • Với d1 = 15, mặc dù kết quả đã đạt 99.33%, vẫn có một vùng đỏ nhỏ nằm giữa nhánh màu lục và màu lam, và một vùng màu lam khá lớn giữa màu đỏ và lục. Khi một điểm dữ liệu test rơi vào những vùng này, nó sẽ bị phân loại sai.

  • Với d1 = 20, kết quả nhận được đã tương đối giống với d1 = 100. Mặc dù các đường boundary không được trơn tru cho lắm.

5. Thảo luận

  • Người ta đã chứng minh được rằng, với một hàm số liên tục bất kỳ \(f(x)\) và một số \(\varepsilon >0\), luôn luôn tồn tại một Neural Network với predicted output có dạng \(g(x)\) với một hidden layer (với số hidden units đủ lớn và nonlinear activation function phù hợp) sao cho với mọi \(x, |f(x) - g(x)| < \varepsilon\). Nói một cách khác, Neural Network có khả năng xấp xỉ hầu hết các hàm liên tục.

  • Trên thực tế, việc tìm ra số lượng hidden units và nonlinear activation function nói trên nhiều khi bất khả thi. Thay vào đó, thực nghiệm chứng minh rằng Neural Networks với nhiều hidden layers kết hợp với các nonlinear activation function (đơn giản như ReLU) có khả năng xấp xỉ (khả năng biểu diễn) training data tốt hơn.

  • Khi số lượng hidden layers lớn lên, số lượng hệ số cần tối ưu cũng lớn lên và mô hình sẽ trở nên phức tạp. Sự phức tạp này ảnh hưởng tới hai khia cạnh. Thứ nhất, tốc độ tính toán sẽ bị chậm đi rất nhiều. Thứ hai, nếu mô hình quá phức tạp, nó có thể biểu diễn rất tốt training data, nhưng lại không biểu diễn tốt test data. Hiện tượng này gọi là Overfitting, tôi sẽ trình bày trong bài sau.

  • Nếu mọi units của một layer được kết nối với mọi unit của layer tiếp theo (như chúng ta đang xét trong baì này), ta gọi đó là fully connected layer (kết nối hoàn toàn). Neural Networks với toàn fully connected layer ít được sử dụng trong thực tế. Thay vào đó, có nhiều phương pháp giúp làm giảm độ phức tạp của mô hình bằng cách giảm số lượng kết nối bằng cách cho nhiều kết nối bằng 0 (ví dụ, sparse autoencoder), hoặc các hệ số được ràng buộc giống nhau (để giảm số hệ số cần tối ưu) (ví dụ, Convolutional Neural Networks (CNNs / ConvNets)). Bạn đọc muốn tìm hiểu thêm có thể bắt đầu tại đây.

  • Đây là bài cuối cùng trong chuỗi bài về Neural Networks. Viết một bài về Deep Learning sẽ tốn thời gian hơn rất nhiều, trong 1 tuần tôi không đủ khả năng hoàn thành được. Bài tiếp theo tôi sẽ nói về Overfitting, sau đó chuyển sang một phương pháp classification rất phổ biến khác: Support Vector Machine.

  • Về backpropagation, có rất nhiều điều phải nói nữa. Nếu có thể, tôi xin phép được trình bày sau. Bài này cũng đã đủ dài.

6. Tài liệu tham khảo

[1] Neural Networks Part 1: Setting up the Architecture - Andrej Karpathy

[2] Neural Networks, Case study - Andrej Karpathy

[3] Lecture Notes on Sparse Autoencoders - Andrew Ng

[4] Yes you should understand backprop

[5] Backpropagation, Intuitions - Andrej Karpathy

[6] How the backpropagation algorithm works - Michael Nielsen

Page 2

Trong trang này:

Overfitting không phải là một thuật toán trong Machine Learning. Nó là một hiện tượng không mong muốn thường gặp, người xây dựng mô hình Machine Learning cần nắm được các kỹ thuật để tránh hiện tượng này.

1. Giới thiệu

Đây là một câu chuyện của chính tôi khi lần đầu biết đến Machine Learning.

Năm thứ ba đại học, một thầy giáo có giới thiệu với lớp tôi về Neural Networks. Lần đầu tiên nghe thấy khái niệm này, chúng tôi hỏi thầy mục đích của nó là gì. Thầy nói, về cơ bản, từ dữ liệu cho trước, chúng ta cần tìm một hàm số để biến các các điểm đầu vào thành các điểm đầu ra tương ứng, không cần chính xác, chỉ cần xấp xỉ thôi.

Lúc đó, vốn là một học sinh chuyên toán, làm việc nhiều với đa thức ngày cấp ba, tôi đã quá tự tin trả lời ngay rằng Đa thức Nội suy Lagrange có thể làm được điều đó, miễn là các điểm đầu vào khác nhau đôi một! Thầy nói rằng “những gì ta biết chỉ là nhỏ xíu so với những gì ta chưa biết”. Và đó là những gì tôi muốn bắt đầu trong bài viết này.

Nhắc lại một chút về Đa thức nội suy Lagrange: Với \(N\) cặp điểm dữ liệu \((x_1, y_1), (x_2, y_2), \dots, (x_N, y_N)\) với các \(x_i\) kháu nhau đôi một, luôn tìm được một đa thức \(P(.)\) bậc không vượt quá \(N-1\) sao cho \(P(x_i) = y_i, ~\forall i = 1, 2, \dots, N\). Chẳng phải điều này giống với việc ta đi tìm một mô hình phù hợp (fit) với dữ liệu trong bài toán Supervised Learning hay sao? Thậm chí điều này còn tốt hơn vì trong Supervised Learning ta chỉ cần xấp xỉ thôi.

Sự thật là nếu một mô hình quá fit với dữ liệu thì nó sẽ gây phản tác dụng! Hiện tượng quá fit này trong Machine Learning được gọi là overfitting, là điều mà khi xây dựng mô hình, chúng ta luôn cần tránh. Để có cái nhìn đầu tiên về overfitting, chúng ta cùng xem Hình dưới đây. Có 50 điểm dữ liệu được tạo bằng một đa thức bậc ba cộng thêm nhiễu. Tập dữ liệu này được chia làm hai, 30 điểm dữ liệu màu đỏ cho training data, 20 điểm dữ liệu màu vàng cho test data. Đồ thị của đa thức bậc ba này được cho bởi đường màu xanh lục. Bài toán của chúng ta là giả sử ta không biết mô hình ban đầu mà chỉ biết các điểm dữ liệu, hãy tìm một mô hình “tốt” để mô tả dữ liệu đã cho.

Underfitting và Overfitting với Polynomial Regression (Source code).

Với những gì chúng ta đã biết từ bài Linear Regression, với loại dữ liệu này, chúng ta có thể áp dụng Polynomial Regression. Bài toán này hoàn toàn có thể được giải quyết bằng Linear Regression với dữ liệu mở rộng cho một cặp điểm \((x, y)\) là \((\mathbf{x}, y)\) với \(\mathbf{x} = [1, x, x^2, x^3, \dots, x^d]^T\) cho đa thức bậc \(d\). Điều quan trọng là chúng ta cần tìm bậc \(d\) của đa thức cần tìm.

Rõ ràng là một đa thức bậc không vượt quá 29 có thể fit được hoàn toàn với 30 điểm trong training data. Chúng ta cùng xét vài giá trị \(d = 2, 4, 8, 16\). Với \(d = 2\), mô hình không thực sự tốt vì mô hình dự đoán quá khác so với mô hình thực. Trong trường hợp này, ta nói mô hình bị underfitting. Với \(d = 8\), với các điểm dữ liệu trong khoảng của training data, mô hình dự đoán và mô hình thực là khá giống nhau. Tuy nhiên, về phía phải, đa thức bậc 8 cho kết quả hoàn toàn ngược với xu hướng của dữ liệu. Điều tương tự xảy ra trong trường hợp \(d = 16\). Đa thức bậc 16 này quá fit dữ liệu trong khoảng đang xét, và quá fit, tức không được mượt trong khoảng dữ liệu training. Việc quá fit trong trường hợp bậc 16 không tốt vì mô hình đang cố gắng mô tả nhiễu hơn là dữ liệu. Hai trường hợp đa thức bậc cao này được gọi là Overfitting.

Nếu bạn nào biết về Đa thức nội suy Lagrange thì có thể hiểu được hiện tượng sai số lớn với các điểm nằm ngoài khoảng của các điểm đã cho. Đó chính là lý do phương pháp đó có từ “nội suy”, với các trường hợp “ngoại suy”, kết quả thường không chính xác.

Với \(d = 4\), ta được mô hình dự đoán khá giống với mô hình thực. Hệ số bậc cao nhất tìm được rất gần với 0 (xem kết quả trong source code), vì vậy đa thức bậc 4 này khá gần với đa thức bậc 3 ban đầu. Đây chính là một mô hình tốt.

Overfitting là hiện tượng mô hình tìm được quá khớp với dữ liệu training. Việc quá khớp này có thể dẫn đến việc dự đoán nhầm nhiễu, và chất lượng mô hình không còn tốt trên dữ liệu test nữa. Dữ liệu test được giả sử là không được biết trước, và không được sử dụng để xây dựng các mô hình Machine Learning.

Về cơ bản, overfitting xảy ra khi mô hình quá phức tạp để mô phỏng training data. Điều này đặc biệt xảy ra khi lượng dữ liệu training quá nhỏ trong khi độ phức tạp của mô hình quá cao. Trong ví dụ trên đây, độ phức tạp của mô hình có thể được coi là bậc của đa thức cần tìm. Trong Multi-layer Perceptron, độ phức tạp của mô hình có thể được coi là số lượng hidden layers và số lượng units trong các hidden layers.

Vậy, có những kỹ thuật nào giúp tránh Overfitting?

Trước hết, chúng ta cần một vài đại lượng để đánh giá chất lượng của mô hình trên training data và test data. Dưới đây là hai đại lượng đơn giản, với giả sử \(\mathbf{y}\) là đầu ra thực sự (có thể là vector), và \(\mathbf{\hat{y}}\) là đầu ra dự đoán bởi mô hình:

Train error: Thường là hàm mất mát áp dụng lên training data. Hàm mất mát này cần có một thừa số \(\frac{1}{N_{\text{train}}} \) để tính giá trị trung bình, tức mất mát trung bình trên mỗi điểm dữ liệu. Với Regression, đại lượng này thường được định nghĩa: \[ \text{train error}= \frac{1}{N_{\text{train}}} \sum_{\text{training set}} \|\mathbf{y} - \mathbf{\hat{y}}\|_p^2 \] với \(p\) thường bằng 1 hoặc 2.

Với Classification, trung bình cộng của cross entropy có thể được sử dụng.

Test error: Tương tự như trên nhưng áp dụng mô hình tìm được vào test data. Chú ý rằng, khi xây dựng mô hình, ta không được sử dụng thông tin trong tập dữ liệu test. Dữ liệu test chỉ được dùng để đánh giá mô hình. Với Regression, đại lượng này thường được định nghĩa: \[ \text{test error}= \frac{1}{N_{\text{test}}} \sum_{\text{test set}} \|\mathbf{y} - \mathbf{\hat{y}}\|_p^2 \]

với \(p\) giống như \(p\) trong cách tính train error phía trên.

Việc lấy trung bình là quan trọng vì lượng dữ liệu trong hai tập hợp training và test có thể chênh lệch rất nhiều.

Một mô hình được coi là tốt (fit) nếu cả train error và test error đều thấp. Nếu train error thấp nhưng test error cao, ta nói mô hình bị overfitting. Nếu train error cao và test error cao, ta nói mô hình bị underfitting. Nếu train error cao nhưng test error thấp, tôi không biết tên của mô hình này, vì cực kỳ may mắn thì hiện tượng này mới xảy ra, hoặc có chỉ khi tập dữ liệu test quá nhỏ.

Chúng ta cùng đi vào phương pháp đầu tiên

2. Validation

2.1. Validation

Chúng ta vẫn quen với việc chia tập dữ liệu ra thành hai tập nhỏ: training data và test data. Và một điều tôi vẫn muốn nhắc lại là khi xây dựng mô hình, ta không được sử dụng test data. Vậy làm cách nào để biết được chất lượng của mô hình với unseen data (tức dữ liệu chưa nhìn thấy bao giờ)?

Phương pháp đơn giản nhất là trích từ tập training data ra một tập con nhỏ và thực hiện việc đánh giá mô hình trên tập con nhỏ này. Tập con nhỏ được trích ra từ training set này được gọi là validation set. Lúc này, training set là phần còn lại của training set ban đầu. Train error được tính trên training set mới này, và có một khái niệm nữa được định nghĩa tương tự như trên validation error, tức error được tính trên tập validation.

Việc này giống như khi bạn ôn thi. Giả sử bạn không biết đề thi như thế nào nhưng có 10 bộ đề thi từ các năm trước. Để xem trình độ của mình trước khi thi thế nào, có một cách là bỏ riêng một bộ đề ra, không ôn tập gì. Việc ôn tập sẽ được thực hiện dựa trên 9 bộ còn lại. Sau khi ôn tập xong, bạn bỏ bộ đề đã để riêng ra làm thử và kiểm tra kết quả, như thế mới “khách quan”, mới giống như thi thật. 10 bộ đề ở các năm trước là “toàn bộ” training set bạn có. Để tránh việc học lệch, học tủ theo chỉ 10 bộ, bạn tách 9 bộ ra làm training set thật, bộ còn lại là validation test. Khi làm như thế thì mới đánh giá được việc bạn học đã tốt thật hay chưa, hay chỉ là học tủ. Vì vậy, Overfitting còn có thể so sánh với việc Học tủ của con người.

Với khái niệm mới này, ta tìm mô hình sao cho cả train error và validation error đều nhỏ, qua đó có thể dự đoán được rằng test error cũng nhỏ. Phương pháp thường được sử dụng là sử dụng nhiều mô hình khác nhau. Mô hình nào cho validation error nhỏ nhất sẽ là mô hình tốt.

Thông thường, ta bắt đầu từ mô hình đơn giản, sau đó tăng dần độ phức tạp của mô hình. Tới khi nào validation error có chiều hướng tăng lên thì chọn mô hình ngay trước đó. Chú ý rằng mô hình càng phức tạp, train error có xu hướng càng nhỏ đi.

Hính dưới đây mô tả ví dụ phía trên với bậc của đa thức tăng từ 1 đến 8. Tập validation bao gồm 10 điểm được lấy ra từ tập training ban đầu.

Hình 2: Lựa chọn mô hình dựa trên validation (Source code).

Chúng ta hãy tạm chỉ xét hai đường màu lam và đỏ, tương ứng với train error và validation error. Khi bậc của đa thức tăng lên, train error có xu hướng giảm. Điều này dễ hiểu vì đa thức bậc càng cao, dữ liệu càng được fit. Quan sát đường màu đỏ, khi bậc của đa thức là 3 hoặc 4 thì validation error thấp, sau đó tăng dần lên. Dựa vào validation error, ta có thể xác định được bậc cần chọn là 3 hoặc 4. Quan sát tiếp đường màu lục, tương ứng với test error, thật là trùng hợp, với bậc bằng 3 hoặc 4, test error cũng đạt giá trị nhỏ nhất, sau đó tăng dần lên. Vậy cách làm này ở đây đã tỏ ra hiệu quả.

Việc không sử dụng test data khi lựa chọn mô hình ở trên nhưng vẫn có được kết quả khả quan vì ta giả sử rằng validation data và test data có chung một đặc điểm nào đó. Và khi cả hai đều là unseen data, error trên hai tập này sẽ tương đối giống nhau.

Nhắc lại rằng, khi bậc nhỏ (bằng 1 hoặc 2), cả ba error đều cao, ta nói mô hình bị underfitting.

2.2. Cross-validation

Trong nhiều trường hợp, chúng ta có rất hạn chế số lượng dữ liệu để xây dựng mô hình. Nếu lấy quá nhiều dữ liệu trong tập training ra làm dữ liệu validation, phần dữ liệu còn lại của tập training là không đủ để xây dựng mô hình. Lúc này, tập validation phải thật nhỏ để giữ được lượng dữ liệu cho training đủ lớn. Tuy nhiên, một vấn đề khác nảy sinh. Khi tập validation quá nhỏ, hiện tượng overfitting lại có thể xảy ra với tập training còn lại. Có giải pháp nào cho tình huống này không?

Câu trả lời là cross-validation.

Cross validation là một cải tiến của validation với lượng dữ liệu trong tập validation là nhỏ nhưng chất lượng mô hình được đánh giá trên nhiều tập validation khác nhau. Một cách thường đường sử dụng là chia tập training ra \(k\) tập con không có phần tử chung, có kích thước gần bằng nhau. Tại mỗi lần kiểm thử , được gọi là run, một trong số \(k\) tập con được lấy ra làm validate set. Mô hình sẽ được xây dựng dựa vào hợp của \(k-1\) tập con còn lại. Mô hình cuối được xác định dựa trên trung bình của các train error và validation error. Cách làm này còn có tên gọi là k-fold cross validation.

Khi \(k\) bằng với số lượng phần tử trong tập training ban đầu, tức mỗi tập con có đúng 1 phần tử, ta gọi kỹ thuật này là leave-one-out.

Sklearn hỗ trợ rất nhiều phương thức cho phân chia dữ liệu và tính toán scores của các mô hình. Bạn đọc có thể xem thêm tại Cross-validation: evaluating estimator performance.

3. Regularization

Một nhược điểm lớn của cross-validation là số lượng training runs tỉ lệ thuận với \(k\). Điều đáng nói là mô hình polynomial như trên chỉ có một tham số cần xác định là bậc của đa thức. Trong các bài toán Machine Learning, lượng tham số cần xác định thường lớn hơn nhiều, và khoảng giá trị của mỗi tham số cũng rộng hơn nhiều, chưa kể đến việc có những tham số có thể là số thực. Như vậy, việc chỉ xây dựng một mô hình thôi cũng là đã rất phức tạp rồi. Có một cách giúp số mô hình cần huấn luyện giảm đi nhiều, thậm chí chỉ một mô hình. Cách này có tên gọi chung là regularization.

Regularization, một cách cơ bản, là thay đổi mô hình một chút để tránh overfitting trong khi vẫn giữ được tính tổng quát của nó (tính tổng quát là tính mô tả được nhiều dữ liệu, trong cả tập training và test). Một cách cụ thể hơn, ta sẽ tìm cách di chuyển nghiệm của bài toán tối ưu hàm mất mát tới một điểm gần nó. Hướng di chuyển sẽ là hướng làm cho mô hình ít phức tạp hơn mặc dù giá trị của hàm mất mát có tăng lên một chút.

Một kỹ thuật rất đơn giản là early stopping.

3.1. Early Stopping

Trong nhiều bài toán Machine Learning, chúng ta cần sử dụng các thuật toán lặp để tìm ra nghiệm, ví dụ như Gradient Descent. Nhìn chung, hàm mất mát giảm dần khi số vòng lặp tăng lên. Early stopping tức dừng thuật toán trước khi hàm mất mát đạt giá trị quá nhỏ, giúp tránh overfitting.

Vậy dừng khi nào là phù hợp?

Một kỹ thuật thường được sử dụng là tách từ training set ra một tập validation set như trên. Sau một (hoặc một số, ví dụ 50) vòng lặp, ta tính cả train error và validation error, đến khi validation error có chiều hướng tăng lên thì dừng lại, và quay lại sử dụng mô hình tương ứng với điểm và validation error đạt giá trị nhỏ.

Hình 3: Early Stopping. Đường màu xanh là train error, đường màu đỏ là validation error. Trục x là số lượng vòng lặp, trục y là error. Mô hình được xác định tại vòng lặp mà validation error đạt giá trị nhỏ nhất. (Overfitting - Wikipedia).

Hình trên đây mô tả cách tìm điểm stopping. Chúng ta thấy rằng phương pháp này khá giống với phương pháp tìm bậc của đa thức ở phần trên của bài viết.

3.2. Thêm số hạng vào hàm mất mát

Kỹ thuật regularization phổ biến nhất là thêm vào hàm mất mát một số hạng nữa. Số hạng này thường dùng để đánh giá độ phức tạp của mô hình. Số hạng này càng lớn, thì mô hình càng phức tạp. Hàm mất mát mới này thường được gọi là regularized loss function, thường được định nghĩa như sau: \[ J_{\text{reg}}(\theta) = J(\theta) + \lambda R(\theta) \]

Nhắc lại rằng \(\theta\) được dùng để ký hiệu các biến trong mô hình, chẳng hạn như các hệ số \(\mathbf{w}\) trong Neural Networks. \(J(\theta)\) ký hiệu cho hàm mất mát (loss function) và \(R(\theta)\) là số hạng regularization. \(\lambda\) thường là một số dương để cân bằng giữa hai đại lượng ở vế phải.

Việc tối thiểu regularized loss function, nói một cách tương đối, đồng nghĩa với việc tối thiểu cả loss function và số hạng regularization. Tôi dùng cụm “nói một cách tương đối” vì nghiệm của bài toán tối ưu loss function và regularized loss function là khác nhau. Chúng ta vẫn mong muốn rằng sự khác nhau này là nhỏ, vì vậy tham số regularization (regularization parameter) \(\lambda\) thường được chọn là một số nhỏ để biểu thức regularization không làm giảm quá nhiều chất lượng của nghiệm.

Với các mô hình Neural Networks, một số kỹ thuật regularization thường được sử dụng là:

3.3. \(l_2\) regularization

Trong kỹ thuật này: \[ R(\mathbf{w}) = \|\mathbf{w}\|_2^2 \] tức norm 2 của hệ số.

Nếu bạn đọc chưa quen thuộc với khái niệm norm, bạn được khuyến khích đọc phần phụ lục này.

Hàm số này có một vài đặc điểm đang lưu ý:

  • Thứ nhất, \(\|\mathbf{w}\|_2^2\) là một hàm số rất mượt, tức có đạo hàm tại mọi điểm, đạo hàm của nó đơn giản là \(\mathbf{w}\), vì vậy đạo hàm của regularized loss function cũng rất dễ tính, chúng ta có thể hoàn toàn dùng các phương pháp dựa trên gradient để cập nhật nghiệm. Cụ thể: \[ \frac{\partial J_{\text{reg}} }{\partial \mathbf{w}} = \frac{\partial J}{\partial \mathbf{w}} + \lambda \mathbf{w} \]
  • Thứ hai, việc tối thiểu \(\|\mathbf{w}\|_2^2\) đồng nghĩa với việc khiến cho các giá trị của hệ số \(\mathbf{w}\) trở nên nhỏ gần với 0. Với Polynomial Regression, việc các hệ số này nhỏ có thể giúp các hệ số ứng với các số hạng bậc cao là nhỏ, giúp tránh overfitting. Với Multi-layer Perceptron, việc các hệ số này nhỏ giúp cho nhiều hệ số trong các ma trận trọng số là nhỏ. Điều này tương ứng với việc số lượng các hidden units hoạt động (khác không) là nhỏ, cũng giúp cho MLP tránh được hiện tượng overfitting.

\(l_2\) regularization là kỹ thuật được sử dụng nhiều nhất để giúp Neural Networks tránh được overfitting. Nó còn có tên gọi khác là weight decay. Decay có nghĩa là tiêu biến.

Trong Xác suất thống kê, Linear Regression với \(l_2\) regularization được gọi là Ridge Regression. Hàm mất mát của Ridge Regression có dạng: \[ J(\mathbf{w}) = \frac{1}{2} \|\mathbf{y} - \mathbf{Xw}\|_2^2 + \lambda \|\mathbf{w}\|_2^2 \] trong đó, số hạng đầu tiên ở vế phải chính là hàm mất mát của Linear Regression. Số hạng thứ hai chính là phần regularization.

Ví dụ về Weight Decay với MLP

Chúng ta sử dụng mô hình MLP giống như bài trước nhưng dữ liệu có khác đi đôi chút.

# To support both python 2 and python 3 from __future__ import division, print_function, unicode_literals import math import numpy as np import matplotlib.pyplot as plt np.random.seed(4) means = [[-1, -1], [1, -1], [0, 1]] cov = [[1, 0], [0, 1]] N = 20 X0 = np.random.multivariate_normal(means[0], cov, N) X1 = np.random.multivariate_normal(means[1], cov, N) X2 = np.random.multivariate_normal(means[2], cov, N)

Dữ liệu được tạo là ba cụm tuân theo phân phối chuẩn có tâm ở [[-1, -1], [1, -1], [0, 1]].

Trong ví dụ này, chúng ta sử dụng số hạng regularization: \[ \lambda R(\mathbf{W}) = \lambda \sum_{l=1}^L \|\mathbf{W}^{(l)}\|_F^2 \]

với \(\|.\|_F\) là Frobenius norm, là căn bậc hai của tổng bình phường các phẩn tử của ma trận.

(Bạn đọc được khuyến khích đọc bài MLP để hiểu các ký hiệu).

Chú ý rằng weight decay ít khi được áp dụng lên biases. Tôi thay đổi tham số regularization \(\lambda\) và nhận được kết quả như sau:

Multi-layer Perceptron với Weight Decay (Source code).

Khi \(\lambda = 0\), tức không có regularization, ta nhận thấy gần như toàn bộ dữ liệu trong tập training được phân lớp đúng. Việc này khiến cho các class bị phân làm nhiều mảnh không được tự nhiên. Khi \(\lambda = 0.001\), vẫn là một số nhỏ, các đường phân chia trông tự nhiên hơn, nhưng lớp màu xanh lam vẫn bị chia làm hai bởi lớp màu xanh lục. Đây chính là biểu hiện của overfitting.

Khi \(\lambda\) tăng lên, tức sự ảnh hưởng của regularization tăng lên (xem hàng dưới), đường ranh giới giữa các lớp trở lên tự nhiên hơn. Nói cách khác, với \(\lambda\) đủ lớn, weight decay có tác dụng hạn chế overfitting trong MLP.

Bạn đọc hãy thử vào trong Source code, thay \(\lambda = 1\) bằng cách thay dòng cuối cùng:

rồi chạy lại toàn bộ code, xem các đường phân lớp trông như thế nào. Gợi ý: underfitting.

Khi \(\lambda\) quá lớn, tức ta xem phần regularization quan trọng hơn phần loss fucntion, một hiện tượng xấu xảy ra là các phần tử của \(\mathbf{w}\) tiến về 0 để thỏa mãn regularization là nhỏ.

Sklearn có cung cấp rất nhiều chức năng cho MLP, trong đó ta có thể lựa chọn số lượng hidden layers và số lượng hidden units trong mỗi layer, activation functions, weight decay, learning rate, hệ số momentum, nesterovs_momentum, có early stopping hay không, lượng dữ liệu được tách ra làm validation set, và nhiều chức năng khác.

3.4. Tikhonov regularization

\[ \lambda R(\mathbf{w}) = \|\Gamma \mathbf{w}\|_2^2 \]

Với \(\Gamma\) (viết hoa của gamma) là một ma trận. Ma trận \(\Gamma\) hay được dùng nhất là ma trận đường chéo. Nhận thấy rằng \(l_2\) regularization chính là một trường hợp đặc biệt của Tikhonov regularization với \(\Gamma = \lambda \mathbf{I}\) với \(\mathbf{I}\) là ma trận đơn vị (the identity matrix), tức các phần tử trên đường chéo của \(\Gamma\) là như nhau.

Khi các phần tử trên đường chéo của \(\Gamma\) là khác nhau, ta có một phiên bản gọi là weighted \(l_2\) regularization, tức đánh trọng số khác nhau cho mỗi phần tử trong \(\mathbf{w}\). Phần tử nào càng bị đánh trọng số cao thì nghiệm tương ứng càng nhỏ (để đảm bảo rằng hàm mất mát là nhỏ). Với Polynomial Regression, các phần tử ứng với hệ số bậc cao sẽ được đánh trọng số cao hơn, khiến cho xác suất để chúng gần 0 là lớn hơn.

3.5. Regularizers for sparsity

Trong nhiều trường hợp, ta muốn các hệ số thực sự bằng 0 chứ không phải là nhỏ gần 0 như \(l_2\) regularization đã làm phía trên. Lúc đó, có một regularization khác được sử dụng, đó là \(l_0\) regularization: \[ R(\mathbf{W}) = \|\mathbf{w}\|_0 \]

Norm 0 không phải là một norm thực sự mà là giả norm. (Bạn được khuyến khích đọc thêm về norms (chuẩn)). Norm 0 của một vector là số các phần tử khác không của vector đó. Khi norm 0 nhỏ, tức rất nhiều phần tử trong vector đó bằng 0, ta nói vector đó là sparse.

Việc giải bài toán tổi thiểu norm 0 nhìn chung là khó vì hàm số này không convex, không liên tục. Thay vào đó, norm 1 thường được sử dụng: \[ R(\mathbf{W}) = \|\mathbf{w}\|_1 = \sum_{i=0}^d |w_i| \]

Norm 1 là tổng các trị tuyệt đối của tất cả các phần tử. Người ta đã chứng minh được rằng tối thiểu norm 1 sẽ dẫn tới nghiệm có nhiều phần tử bằng 0. Ngoài ra, vì norm 1 là một norm thực sự (proper norm) nên hàm số này là convex, và hiển nhiên là liên tục, việc giải bài toán này dễ hơn việc giải bài toán tổi thiểu norm 0. Về \(l_1\) regularization, bạn đọc có thể đọc thêm trong lecture note này. Việc giải bài toán \(l_1\) regularization nằm ngoài mục đích của tôi trong bài viết này. Tôi hứa sẽ quay lại phần này sau. (Vì đây là phần chính trong nghiên cứu của tôi).

Trong Thống Kê, việc sử dụng \(l_1\) regularization còn được gọi là LASSO (Least Absolute Shrinkage and Selection Operator).

Khi cả \(l_2\) và \(l_1\) regularization được sử dụng, ta có mô hình gọi là Elastic Net Regression.

3.6. Regularization trong sklearn

Trong sklearn, ví dụ Logistic Regression, bạn cũng có thể sử dụng các \(l_1\) và \(l_2\) regularizations bằng cách khai báo biến penalty='l1' hoặc penalty = 'l2' và biến C, trong đó C là nghịch đảo của \(\lambda\). Trong các bài trước khi chưa nói về Overfitting và Regularization, tôi có sử dụng C = 1e5 để chỉ ra rằng \(\lambda\) là một số rất nhỏ.

4. Các phương pháp khác

Ngoài các phương pháp đã nêu ở trên, với mỗi mô hình, nhiều phương pháp tránh overfitting khác cũng được sử dụng. Điển hình là Dropout trong Deep Neural Networks mới được đề xuất gần đây. Một cách ngắn gọn, dropout là một phương pháp tắt ngẫu nhiên các units trong Networks. Tắt tức cho các unit giá trị bằng không và tính toán feedforward và backpropagation bình thường trong khi training. Việc này không những giúp lượng tính toán giảm đi mà còn làm giảm việc overffitng. Tôi xin được quay lại vấn đề này nếu có dịp nói sâu về Deep Learning trong tương lai.

Bạn đọc có thể tìm đọc thêm với các từ khóa: pruning (tránh overfitting trong Decision Trees), VC dimension (đo độ phức tạp của mô hình, độ phức tạp càng lớn thì càng dễ bị overfitting).

5. Tóm tắt nội dung

  • Một mô hình mô tốt là mộ mô hình có tính tổng quát, tức mô tả được dữ liệu cả trong lẫn ngoài tập training. Mô hình chỉ mô tả tốt dữ liệu trong tập training được gọi là overfitting.

  • Để tránh overfitting, có rất nhiều kỹ thuật được sử dụng, điển hình là cross-validationregularization. Trong Neural Networks, weight decaydropout thường được dùng.

6. Tài liệu tham khảo

[1] Overfitting - Wikipedia

[2] Cross-validation - Wikipedia

[3] Pattern Recognition and Machine Learning

[4] Krogh, Anders, and John A. Hertz. “A simple weight decay can improve generalization.” NIPS. Vol. 4. 1991.

[5] Srivastava, Nitish, et al. “Dropout: A Simple Way to Prevent Neural Networks from Overfitting” Journal of Machine Learning Research 15.1 (2014): 1929-1958.

Video liên quan

Chủ đề