Toàn bộ source code nằm ở: https://github.com/lenguyen1807/mnist-cpp
Neural Network là gì ?
Dựa vào ảnh trên, có thể thấy một Neural Network như là một đồ thị có hướng không chu trình. Trong đó sẽ gồm nhiều nút, mỗi nút được gọi là một Neuron và được sắp xếp theo từng layer, những layer mà không phải input hoặc output thì được gọi là hidden layer. Mỗi cạnh của Neural Network sẽ có trọng số hay weight.
Lưu ý là khi ta nói Neural Network có 1 layer, ta sẽ hiểu Neural Network ấy sẽ bao gồm 1 layer input và 1 layer output. Bởi vì khi nói đến layer, ta chỉ nói đến các layer có thể tính toán được, còn input layer chỉ có nhiệm vụ nhận giá trị chứ không tính toán giá trị ấy [null].
Neural Network chỉ có 1 layer (gồm input layer và output layer) được gọi là Perceptron. Trước khi đi vào Neural Network nhiều layer hơn, ta sẽ tìm hiểu một chút về Perceptron.
Neural Network mà chúng ta đang nói tới, có hướng, không chu trình được gọi là Feed Forward Neural Network. Ngoài ra mỗi neuron đều có cạnh nối tới nó, do đó có thể nghe đến term Fully-Connected Neural Network.
Perceptron
Ý tưởng mở rộng output neuron được mình lấy thẳng từ [null].
Ở hình 2 là ví dụ về một perceptron gồm input và 1 output do đó input layer sẽ gồm neuron và output layer chỉ có 1 neuron. Có thể thấy, neuron output sẽ được tính bằng tổ hợp tuyến tính của các input neuron (trong đó có các trọng số tương ứng với input neuron ), sau đó tổ hợp tuyến tính ấy sẽ đi qua một hàm :
Ngoài ra còn một thứ mà mình chưa nhắc đến đó là Bias, tức là ta sẽ cộng thêm một hằng số nữa, gọi là đi.
Nếu đặt thì ta sẽ có:
Vậy vector input lúc này trở thành (thêm vào đầu) và . Nhưng mình chỉ nhắc đến thôi, từ đây về cuối luôn mình sẽ không dùng đến bias (bởi vì nó làm mọi thứ rối hơn 😭).
Ta gọi hàm là hàm activation, ngoài ra phần tổ hợp tuyến tính có khi được gọi là pre-activation và kí hiệu là , do đó:
Hiện tại đã có rất nhiều hàm activation, ví dụ như:
- Hàm sigmoid (mình sẽ kí hiệu là ):
- Hàm tanh:
- Hàm ReLU 1:
- Hay có thể là hàm Linear 💀:
Nhìn hình 3 và 4 thì ta có thể đưa một Perceptron về dạng ma trận trọng số và vector input , vector output (các vector đều là vector cột):
trong đó sẽ có số dòng tương ứng với chiều của và số cột tương ứng với số chiều của . Nếu mở rộng hơn, số dòng sẽ tương ứng với chiều của layer hiện tại và số cột tương ứng với layer trước đó.
Deep Neural Network
Có thể thấy, deep neural network được tạo ra bằng cách thêm nhiều lớp hidden layer ở giữa input layer và output layer. Đây cũng là lý do mà nó còn tên khác là Multi-layer Perceptron (hay MLP) bởi vì đơn giản nó là Perceptron và có thêm nhiều layer ở giữa (Multi-layer).
Dựa vào hình 6, để tính được output của một Neural Network (gọi tắt của Deep Neural Network) ta sẽ thực hiện tính từng hidden layer bằng cách nhân với ma trận trọng số tương ứng. Để tính được output ta phải tính hidden layer , tương tự để tính phải tính được hidden layer . Vậy tất cả chỉ là nhân ma trận với vector thôi hay sao 😭. Thật ra là còn nhiều hơn nữa nhưng mà đơn giản vậy là được rồi.
Nếu để ý, việc tính được output thì cần bắt đầu từ input sau đó tiến đến hidden layer thứ nhất, thứ hai, ... cho đến hidden layer cuối cùng, do đó nó còn được gọi là bước Forward (tiến về phía trước ấy). Ta sẽ còn một bước nữa gọi là Backward mà mình và các bạn sẽ cùng tìm hiểu ở phần 2 nhé.
Một trong những dòng code đầu tiên khi code Neural Network đó chính là phải code được ma trận và tất cả phép toán của nó ví dụ như phép nhân, cộng, chuyển vị, ...
Những dòng code đầu tiên
Để hiểu rõ và làm cực hơn một tí thì mình với các bạn sẽ cùng code C++ 😈.
Ma trận
Đầu tiên ta phải có 1 class ma trận. Ở đây mình chọn std::vector
ví nó đơn giản, dễ sử dụng. Còn tại sao lại dùng size_t
mà không dùng int
, bởi vì nếu bạn gọi length()
của vector thì giá trị trả về là size_t
chứ không phải int
, nên nếu dùng int
tức là bạn đang ép kiểu từ size_t
về int
.
Kiểu
size_t
là một kiểu dữ liệu số nguyên không dấu và điều đặc biệt làsize_t
không có kích thước cố định, trong khiint
thì sẽ từ 2 cho đến 4 bytes 2.
Việc dùng const Matrix& mat
thay vì Matrix mat
sẽ làm được 2 thứ:
- Thứ nhất là
&
sẽ chỉ định compiler rằng ta sẽ truyền tham chiếu (hay pass by reference) nếu không có dấu&
thì compiler sẽ tạo một copy củamat
. - Thứ hai là
const
, bởi vì khi dùng&
ta có thể thay đổi được giá trị củamat
trong hàm nên dùngconst
để tránh điều này xảy ra.
Tại sao mình lại dùng inline
rồi code thêm operator +=
chi cho phức tạp vậy, chỉ cần code Matrix operator+(const Matrix& mat1, const Matrix& mat2)
là đủ rồi. Việc mình implement +=
trước sau đó dùng lại +=
ở +
thì sẽ tiết kiệm được việc phải implement 2 cái, ngoài ra dùng inline
sẽ tối ưu hơn. Tiếp theo việc truyền Matrix mat1
thay vì const Matrix& mat1
sẽ copy luôn mat1
và dùng +=
sẽ thay đổi mat1
trực tiếp, cuối cùng là return về copy của mat1
và ta xem nó như là kết quả của mat1 + mat2
3.
Để đơn giản hoá, khi nhân ma trận mình chỉ cần chạy từng dòng, đến từng cột, sau đó tích vô hướng lại, cái này thì dễ làm nhưng độ phức tạp sẽ rơi vào , rất là lớn ha, nếu các bạn muốn nhanh hơn thì có thể dùng các thuật toán khác và đặc biệt hơn là dùng GPU (đây là cái mà Pytroch, Tensorflow, ... đang làm).
Cuối cùng sẽ là implement các hàm còn lại của class hàm ma trận. Lưu ý khi khởi tạo vector lồng vector nhớ khởi tạo vector phía bên trong 4, nếu không nó sẽ dễ bị bug đấy.
Cái mà
:rows(rows),cols(cols)
ấy được gọi là initializer list 5. Đây là một cách để khởi tạo biến cho class trong C++.
Đối với ma trận chuyển vị, thì ta cũng dùng thuật toán đơn giản theo công thức sau:
Ngoài ra còn operator +=, -=, =
mà chúng ta cũng phải implement, mình nghĩ code nó khá dễ rồi nên sẽ không giải thích nhiều. Trước khi cộng, trừ hay so sánh hai ma trận, ta phải so sánh hai ma trận xem có cùng chiều với nhau không thông qua một hàm phụ là CheckDimension
.
Như vậy là gần xong class ma trận rồi, thế nhưng ta vẫn cần một số hàm phụ trợ nữa. Đầu tiên chính là Flatten
, tức là ta sẽ đưa ma trận 2 chiều về thành vector 1 chiều, cái này cực quan trọng nếu dữ liệu là ảnh, ảnh thông thường sẽ ở dạng 2 chiều, nhưng mà input của Neural Network lại là 1 chiều (trừ khi chúng ta dùng dạng neural network khác được gọi là Convolutional Neural Network). Mình implement hàm Flatten
như sau:
Việc mình chọn static
là bởi vì mình không muốn hàm Flatten
phải có object mới dùng được, kiểu như ta có một object Matrix mat
bất kì thì cứ gọi Matrix::Flatten(mat)
là được.
Tiếp theo là mình phải tìm cách để khởi tạo weight cho Neural Network một cách tối ưu nhất, theo 6 thì khởi tạo trọng số như sau: Với mỗi trọng số , ta lấy từ một phân phối chuẩn có trung bình là và phương sai là với là số chiều của vector input (hay đúng hơn là chiều của layer phía trước).
À còn cái cuối cùng, mình cần dùng được một hàm Element-wise trên class Matrix (ví dụ như dùng Activation lên class Matrix). Mình sẽ viết hàm Apply()
(giống tương tự như hàm apply
trong pandas
), std::function<double(double)>
nghĩa là một hàm có một tham số kiểu double (cái ngoặc tròn double ý) và kiểu trả về là double (phía ngoài), đây là kiểu dữ liệu của hàm (mình truyền hàm như tham số) của C++11 trở lên.
Các hàm activation
Các hàm activation mà chúng ta đã biết phía trên cũng không quá khó để code. Như đã biết thì mình dùng tham số std::function<double(double)>
ở phía trên nên do đó các hàm activation mà áp dụng element-wise cũng phải có kiểu trả về là double
và tham số là double
luôn.
Việc mình không làm hàm tanh là bởi vì hiện tại khá là ít người dùng, đa số chỉ dùng Sigmoid hoặc ReLU. Riêng hàm linear thì khá quan trọng, còn tại sao thì dưới đây nhé :>.
Trước khi kết thúc thì còn 1 hàm activation rất quan trọng nữa đó chính là hàm Softmax 7. Softmax cũng là một activation nhưng thay vì truyền vào một số thực như các hàm khác thì ta sẽ truyền 1 vector và output của Softmax cũng là một vector (do đó không còn chuyện element-wise như trên nữa).
Xét một vector bất kì gồm phần tử . Khi đó:
hay:
Có thể thấy các phần tử của vector luôn có giá trị nằm trong khoảng ngoài ra tổng các phần tử của vector bằng do đó vector này có thể được người ta xem như vector phân phối vậy, trong đó mỗi phần tử được xem như là xác suất. Ngoài ra hàm activation Softmax thường được áp dụng cho layer cuối, khi đó output cho ra sẽ là xác suất. Ví dụ như output layer cuối cùng gồm 10 phần tử, ta xem mỗi phần tử ấy là 1 lớp, sau khi qua Softmax thì mỗi lớp sẽ có giá trị xác suất tương ứng của mình.
Giờ đến lý do tại sao lại có hàm Linear nhé, đơn giản là vì mình muốn để Softmax ở layer cuối thế nhưng code mình chỉ hỗ trợ các hàm activation nhận tham số là số thực chứ không là vector do đó mình dùng Linear trước, rồi dùng Softmax lên sau.
Vậy là xong class Ma trận và các hàm phụ trợ rồi ấy, cảm ơn các bạn đã đọc đến đây hehe 🍉. Ở phần sau Cùng mình xây dựng Neural Network đơn giản (Phần 2) ta sẽ đi qua kỹ thuật Backpropagation (hay còn gọi là Backward) và cách train Deep Neural Network.
References
- An Introduction to Neural Networks, Neural Networks and Deep Learning: A Textbook,Aggarwal, Charu.Springer International Publishing, 2023,https://doi.org/10.1007/978-3-031-29642-0_1
- The Backpropagation Algorithm, Neural Networks and Deep Learning: A Textbook,Aggarwal, Charu.Springer International Publishing, 2023,https://doi.org/10.1007/978-3-031-29642-0_2