Skip to content

First Layer

Giải thích chi tiết cho đoạn mã.

Chương trình chapter01.py

Tổng quan: Mục tiêu của chương trình là gì?

Hãy tưởng tượng chúng ta muốn xây dựng một "bộ não" nhân tạo đơn giản. Chương trình này thực hiện bước đầu tiên và cơ bản nhất:

  1. Xây dựng một "nơ-ron thần kinh": Tạo ra một đơn vị xử lý thông tin cơ bản.
  2. Chuẩn bị dữ liệu: Tạo ra một bộ dữ liệu mẫu để "bộ não" có cái để xử lý.
  3. Thực hiện một phép tính: Đưa dữ liệu qua "bộ não" và xem kết quả đầu ra là gì.

Đây là nền tảng của mọi mạng nơ-ron. Hiểu rõ từng dòng mã ở đây sẽ giúp bạn nắm vững các khái niệm phức tạp hơn sau này.


Phần 1: Chuẩn bị công cụ và nguyên liệu (Imports & Data)

Đây là bước chúng ta tập hợp các thư viện và dữ liệu cần thiết trước khi bắt đầu "xây dựng".

Python
import numpy as np
import nnfs
import matplotlib.pyplot as plt

from nnfs.datasets import spiral_data
nnfs.init()
Giải thích chi tiết từng dòng:
  • import numpy as np:

    • Nó là gì?: NumPy (Numerical Python) là thư viện cơ bản và quan trọng nhất cho khoa học dữ liệu trong Python. Nó cung cấp một cấu trúc dữ liệu cực kỳ hiệu quả gọi là mảng (array) và các công cụ để thực hiện các phép toán trên mảng đó, đặc biệt là toán ma trận.
    • Tại sao cần nó?: Mạng nơ-ron về bản chất là một chuỗi các phép toán ma trận. NumPy giúp chúng ta thực hiện các phép nhân, cộng ma trận này một cách nhanh chóng và hiệu quả hơn rất nhiều so với việc dùng list thông thường của Python. as np là một quy ước phổ biến để đặt tên viết tắt cho thư viện.
  • import nnfs:

    • Nó là gì?: nnfs (Neural Networks from Scratch) là một thư viện hỗ trợ được viết riêng cho cuốn sách cùng tên. Mục đích của nó là giúp người học tập trung vào khái niệm mạng nơ-ron thay vì bị sa đà vào các chi tiết phụ.
    • Tại sao cần nó?: Nó cung cấp các hàm tiện ích, như tạo dữ liệu mẫu (spiral_data) và khởi tạo môi trường (init) để đảm bảo kết quả của mọi người đều giống nhau, dễ dàng cho việc học và gỡ lỗi.
  • import matplotlib.pyplot as plt:

    • Nó là gì?: Matplotlib là thư viện trực quan hóa dữ liệu (vẽ đồ thị) phổ biến nhất trong Python. pyplot là một module trong Matplotlib cung cấp giao diện giống như MATLAB.
    • Tại sao cần nó?: "Trăm nghe không bằng một thấy". Thư viện này cho phép chúng ta vẽ dữ liệu lên biểu đồ để xem nó trông như thế nào. Việc nhìn thấy dữ liệu hình xoắn ốc giúp ta hiểu rõ hơn bài toán mà mạng nơ-ron đang cố gắng giải quyết.
  • from nnfs.datasets import spiral_data:

    • Nó là gì?: Đây là một lệnh import cụ thể. Thay vì nhập cả thư viện nnfs.datasets, chúng ta chỉ lấy riêng hàm spiral_data từ đó.
    • Tại sao cần nó?: spiral_data là một hàm giúp tạo ra bộ dữ liệu hình xoắn ốc nổi tiếng, một bài toán kinh điển để kiểm tra khả năng của các mô hình phân loại.
  • nnfs.init():

    • Nó là gì?: Lệnh này gọi hàm init từ thư viện nnfs.
    • Tại sao cần nó?: Hàm này thực hiện một số cài đặt nền, quan trọng nhất là cố định seed cho việc sinh số ngẫu nhiên của NumPy và thiết lập kiểu dữ liệu mặc định. Điều này đảm bảo rằng mỗi khi bạn chạy lại mã, các "trọng số ngẫu nhiên" và "dữ liệu" được tạo ra sẽ luôn giống hệt nhau, giúp việc học và tái tạo kết quả trở nên nhất quán.

Phần 2: Xây dựng "Bản thiết kế của một Vị Giám khảo" (class Layer_Dense)

Đây là trái tim của chương trình. Chúng ta không xây dựng một nơ-ron riêng lẻ, mà là một "bản thiết kế" (class) để có thể tạo ra cả một lớp/một ban giám khảo một cách dễ dàng.

Python
class Layer_Dense:
        def __init__(self, n_inputs, n_neurons):
            self.weights = 0.01 * np.random.randn(n_inputs, n_neurons)
            self.biases = np.zeros((1, n_neurons))
        def forward(self, inputs):
            self.output = np.dot(inputs, self.weights) + self.biases
Giải thích chi tiết từng phần:
  • class Layer_Dense:: Khai báo một "bản thiết kế" tên là Layer_Dense. Mọi thứ bên trong nó sẽ định nghĩa các thuộc tính và hành vi của một lớp nơ-ron dày đặc.

  • def __init__(self, n_inputs, n_neurons):: Hàm khởi tạo (constructor).

    • Nó làm gì?: Hàm này được tự động gọi mỗi khi một đối tượng mới được tạo ra từ bản thiết kế này (ví dụ dense1 = Layer_Dense(...)). Nó dùng để thiết lập các thuộc tính ban đầu.
    • self: Đại diện cho chính đối tượng sẽ được tạo ra. Khi bạn gọi dense1.weights, self chính là dense1.
    • n_inputs: Số lượng đặc trưng đầu vào mà lớp này sẽ nhận (ví dụ: 2 đặc trưng là "Độ đỏ" và "Độ tròn" của hoa quả).
    • n_neurons: Số lượng nơ-ron trong lớp này (ví dụ: 3 giám khảo, mỗi người cho một loại quả).
    • self.weights = 0.01 * np.random.randn(n_inputs, n_neurons): Đây là dòng cực kỳ quan trọng.
      • np.random.randn(n_inputs, n_neurons): Tạo một ma trận có kích thước (số_đầu_vào, số_nơ_ron) chứa đầy các số ngẫu nhiên theo phân phối chuẩn (phân phối Gauss, có giá trị trung bình là 0 và phương sai là 1). Đây chính là "sự ưu tiên" ban đầu, hoàn toàn ngẫu nhiên của các vị giám khảo.
      • * 0.01: Nhân tất cả các trọng số ngẫu nhiên với một số rất nhỏ. Đây là một kỹ thuật phổ biến để ngăn các giá trị đầu ra ban đầu quá lớn, giúp quá trình huấn luyện sau này ổn định hơn.
    • self.biases = np.zeros((1, n_neurons)):
      • np.zeros((1, n_neurons)): Tạo một ma trận hàng (vector) có kích thước (1, số_nơ_ron) chứa toàn số 0. Đây là "thành kiến" hay "tâm trạng" ban đầu của các vị giám khảo. Việc khởi tạo bằng 0 có nghĩa là ban đầu, họ không có bất kỳ thiên vị nào.
  • def forward(self, inputs):: Phương thức hành động.

    • Nó làm gì?: Định nghĩa hành vi chính của lớp: nhận dữ liệu đầu vào và tính toán đầu ra. Quá trình này được gọi là truyền xuôi (forward pass).
    • inputs: Dữ liệu đầu vào sẽ được đưa vào lớp (ví dụ: danh sách các đặc điểm của tất cả hoa quả).
    • self.output = np.dot(inputs, self.weights) + self.biases: Công thức toán học cốt lõi.
      • np.dot(inputs, self.weights): Phép nhân ma trận. Đây là lúc mỗi giám khảo "nhìn" vào các đặc điểm của hoa quả và nhân chúng với "sự ưu tiên" (trọng số) của mình để đưa ra một điểm số sơ bộ.
      • + self.biases: Cộng thêm "thành kiến" (thiên vị) của mỗi giám khảo vào điểm số của họ.
      • self.output = ...: Kết quả cuối cùng được lưu vào thuộc tính output của lớp.

Phần 3: Cuộc thi bắt đầu! (Sử dụng lớp và dữ liệu)

Bây giờ chúng ta sẽ sử dụng "bản thiết kế" và "nguyên liệu" đã chuẩn bị ở trên để tiến hành một cuộc thi thực sự.

Python
# Create dataset
X, y = spiral_data(samples=100, classes=3)
# Visualize dataset
plt.scatter(X[:,0], X[:,1], c=y, cmap='brg')
plt.show()
  • X, y = spiral_data(samples=100, classes=3): Gọi hàm đã nhập để tạo dữ liệu.
    • X: Sẽ là một mảng NumPy kích thước (300, 2). 300 là vì có 3 lớp (classes), mỗi lớp 100 mẫu (samples). 2 là vì mỗi mẫu có 2 đặc trưng (tọa độ x, y). Đây là "danh sách các thí sinh hoa quả và đặc điểm của chúng".
    • y: Sẽ là một mảng NumPy kích thước (300,) chứa các nhãn 0, 1, 2. Đây là "đáp án đúng" cho mỗi thí sinh (Táo, Cam, hay Chuối).
  • plt.scatter(X[:,0], X[:,1], c=y, cmap='brg'): Chuẩn bị vẽ đồ thị.
    • X[:,0]: Lấy tất cả các hàng, cột đầu tiên (tất cả tọa độ x).
    • X[:,1]: Lấy tất cả các hàng, cột thứ hai (tất cả tọa độ y).
    • c=y: c là viết tắt của color. Lệnh này bảo Matplotlib hãy tô màu cho mỗi điểm (x, y) dựa trên giá trị tương ứng trong mảng y. Các điểm có y=0 sẽ cùng màu, y=1 cùng màu khác,...
    • cmap='brg': Color map. Chọn bảng màu Xanh-Đỏ-Lá (Blue-Red-Green).
  • plt.show(): Hiển thị đồ thị đã chuẩn bị lên màn hình.
Python
# Create Dense layer with 2 input features and 3 output values
dense1 = Layer_Dense(2, 3)
  • Đây là lúc chúng ta tạo ra một đối tượng từ bản thiết kế Layer_Dense. Chúng ta đang "thuê một ban giám khảo".
  • dense1 = ...: Tạo một ban giám khảo cụ thể tên là dense1.
  • Layer_Dense(2, 3): Gọi hàm __init__.
    • n_inputs=2: Vì mỗi "thí sinh hoa quả" (X) có 2 đặc trưng (tọa độ x, y).
    • n_neurons=3: Vì chúng ta cần phân loại thành 3 loại quả (3 lớp trong y). Chúng ta cần 3 giám khảo, mỗi người chuyên về một loại.
Python
# Let's see initial weights and biases
print(">>> Initial weights and biases of the first layer:")
print(dense1.weights)
print(dense1.biases)
  • In ra các thuộc tính weightsbiases của đối tượng dense1 vừa tạo. Điều này cho chúng ta thấy "sự ưu tiên" và "thành kiến" ban đầu, hoàn toàn ngẫu nhiên của ban giám khảo trước khi họ chấm điểm bất kỳ thí sinh nào.
Python
# Perform a forward pass of our training data through this layer
dense1.forward(X)
  • Đây là khoảnh khắc hành động. Chúng ta gọi phương thức forward của dense1 và đưa toàn bộ "danh sách thí sinh" (X) vào. Phép tính np.dot(X, dense1.weights) + dense1.biases được thực thi. Ban giám khảo bắt đầu chấm điểm.
Python
# Let's see output of the first few samples:
print(">>> Output of the first few samples:")
print(dense1.output[:5])
  • Sau khi forward() chạy xong, kết quả được lưu trong dense1.output.
  • dense1.output[:5]: Chúng ta in ra kết quả chấm điểm cho 5 "thí sinh hoa quả" đầu tiên để xem thử. Mỗi hàng là một thí sinh, mỗi cột là điểm số từ một giám khảo. Các giá trị này được gọi là logits.

Phần 4: Diễn giải trừu tượng - Cuộc thi phân loại hoa quả

Hãy kể lại toàn bộ câu chuyện một cách liền mạch:

  1. Bối cảnh: Chúng ta tổ chức một cuộc thi để phân loại 3 loại quả: Táo, Cam, và Chuối.

  2. Các thí sinh (X, y): Có 300 quả tham gia. Với mỗi quả, chúng ta dùng máy đo được 2 đặc điểm: "Độ đỏ""Độ tròn" (đây là 2 cột của X). Chúng ta cũng biết trước đáp án mỗi quả là gì (đây là y).

  3. Thuê ban giám khảo (dense1 = Layer_Dense(2, 3)): Chúng ta thuê một ban giám khảo gồm 3 người:

    • Giám khảo 1: Chuyên gia về Táo.
    • Giám khảo 2: Chuyên gia về Cam.
    • Giám khảo 3: Chuyên gia về Chuối. Họ là những người mới vào nghề, nên "kiến thức" của họ ban đầu là ngẫu nhiên.
  4. Kiến thức của giám khảo (weightsbiases):

    • Sự ưu tiên (weights): Mỗi giám khảo có một bộ "ưu tiên" riêng cho 2 đặc điểm "Độ đỏ" và "Độ tròn". Ví dụ, chuyên gia Táo lý tưởng sẽ có ưu tiên cao cho "Độ đỏ" và "Độ tròn". Chuyên gia Chuối sẽ có ưu tiên âm cho "Độ tròn" (vì chuối dài). Nhưng vì họ là người mới, các ưu tiên này được gán ngẫu nhiên (ví dụ: chuyên gia Táo lại có thể thích quả không đỏ, chuyên gia Chuối lại thích quả tròn).
    • Tâm trạng (biases): Ban đầu, cả 3 giám khảo đều có tâm trạng trung lập (bằng 0).
  5. Quá trình chấm điểm (dense1.forward(X)):

    • Từng quả một được đưa ra trước ban giám khảo.
    • Mỗi giám khảo tính điểm của mình theo công thức: Điểm = (Độ đỏ * Ưu tiên cho độ đỏ) + (Độ tròn * Ưu tiên cho độ tròn) + Tâm trạng
    • Quá trình này diễn ra cho tất cả 300 quả.
  6. Bảng điểm (dense1.output):

    • Kết quả cuối cùng là một bảng điểm lớn. Mỗi hàng là một quả, mỗi cột là điểm số từ một giám khảo.
    • Ví dụ, dòng đầu tiên có thể là [0.0012, -0.0045, 0.0031]. Điều này có nghĩa là với kiến thức ngẫu nhiên hiện tại, Giám khảo Táo cho quả này 0.0012 điểm, Giám khảo Cam cho -0.0045 điểm, và Giám khảo Chuối cho 0.0031 điểm.

Kết luận quan trọng: Vì "kiến thức" (weights) của ban giám khảo là ngẫu nhiên, nên "bảng điểm" (output) này hoàn toàn vô nghĩa. Quá trình "huấn luyện" (training), không có trong mã này, chính là việc cho ban giám khảo xem đáp án đúng (y), chỉ ra lỗi sai của họ, và giúp họ điều chỉnh lại "sự ưu tiên" (weights) và "tâm trạng" (biases) qua hàng ngàn lần lặp, để cuối cùng bảng điểm của họ phản ánh đúng loại quả.


Phần 5: Sơ đồ minh họa (ASCII)

Sơ đồ cho một quả duy nhất đi qua ban giám khảo:

Text Only
        ĐẦU VÀO (1 quả)
        (2 đặc trưng)
        +----------------------+
        | Độ đỏ, Độ tròn       |
        +----------------------+
               |
               |                                 BAN GIÁM KHẢO (dense1)
               |                                 (3 Giám khảo/Nơ-ron)
               |
               |       Ưu tiên (w11, w21)      +--------------------+   (Điểm từ GK Táo)
               +----------------------------->|  Giám khảo TÁO   + b1|-----> output_1
               |                             +--------------------+
               |
               |       Ưu tiên (w12, w22)      +--------------------+   (Điểm từ GK Cam)
               +----------------------------->|  Giám khảo CAM   + b2|-----> output_2
               |                             +--------------------+
               |
               |       Ưu tiên (w13, w23)      +--------------------+   (Điểm từ GK Chuối)
               +----------------------------->|  Giám khảo CHUỐI + b3|-----> output_3
                                             +--------------------+


Công thức tính điểm của Giám khảo TÁO:
output_1 = (Độ đỏ * w11) + (Độ tròn * w21) + b1

Kết quả cuối cùng cho 1 quả là một bộ 3 điểm: [output_1, output_2, output_3]


Phụ Lục Giải Thích - Spiral Data :

Đây là phần giải thích về "Dữ liệu hình xoắn ốc" - nghe có vẻ trừu tượng, nhưng nó là một trong những bộ dữ liệu mẫu kinh điển và quan trọng nhất khi bắt đầu học về mạng nơ-ron.

Hãy cùng phân tích.

1. Định nghĩa đơn giản

Dữ liệu hình xoắn ốc (Spiral Data) là một bộ dữ liệu được tạo ra một cách nhân tạo, trong đó các điểm dữ liệu thuộc các lớp khác nhau được sắp xếp thành các hình xoắn ốc lồng vào nhau.

Hãy nhìn lại chính biểu đồ mà bạn đã tạo ra:

  • Bạn có 3 lớp (classes), tương ứng với 3 màu: Đỏ, Xanh lá, và Xanh dương.
  • Mỗi điểm có một vị trí (tọa độ x, y).
  • Các điểm cùng màu tạo thành một "cánh tay" xoắn ốc.
  • Các cánh tay này đan xen, quấn lấy nhau.

2. Tại sao nó lại quan trọng và nổi tiếng?

Lý do bộ dữ liệu này được sử dụng rộng rãi là vì nó là một thử thách hoàn hảo để chứng minh sức mạnh của mạng nơ-ron.

A. Nó "đánh bại" các mô hình đơn giản (Tuyến tính)

Hãy tưởng tượng bạn chỉ có một cây thước kẻ. Nhiệm vụ của bạn là kẻ một hoặc nhiều đường thẳng để phân chia 3 nhóm màu này ra, sao cho mỗi vùng chỉ chứa một màu duy nhất.

Bạn sẽ thấy ngay là bất khả thi.

Text Only
       /
      /    <-- Bạn không thể kẻ một đường thẳng nào
     /         để tách màu Đỏ (R) ra khỏi Xanh (G) và Xanh dương (B)
    /
   RRRRR
  G B R G
 B G R B G
B B G G B B
 R R B R R
  R G B R
   BBBBB

Một mô hình chỉ có thể kẻ các đường thẳng để phân loại được gọi là mô hình tuyến tính (linear model). Dữ liệu xoắn ốc là một ví dụ kinh điển của dữ liệu phi tuyến (non-linear), nơi ranh giới giữa các lớp không phải là đường thẳng mà là những đường cong phức tạp.

Nói cách khác, dữ liệu xoắn ốc được tạo ra để cố tình làm khó các thuật toán phân loại đơn giản.

B. Nó chứng tỏ sự cần thiết của Mạng Nơ-ron

Mạng nơ-ron, đặc biệt là các mạng có các lớp ẩn (hidden layers) và các hàm kích hoạt phi tuyến (sẽ học sau), có khả năng học được các ranh giới quyết định (decision boundaries) cực kỳ phức tạp và uốn lượn.

Một mạng nơ-ron được huấn luyện tốt có thể tạo ra một ranh giới trông giống như thế này:

Nó không dùng "thước kẻ", mà nó học cách "vẽ" ra những đường cong mềm mại để bao quanh từng nhóm dữ liệu một cách hoàn hảo.

Kết luận: Dữ liệu xoắn ốc là một bài kiểm tra "tốt nghiệp" cho một mô hình phân loại. Nếu mô hình của bạn có thể giải quyết được bài toán này, nó chứng tỏ rằng nó có khả năng xử lý các mối quan hệ phức tạp, phi tuyến trong dữ liệu, điều mà các mô hình đơn giản không làm được.

3. Hàm spiral_data tạo ra cái gì?

Khi bạn gọi X, y = spiral_data(samples=100, classes=3), hàm này sẽ tính toán và trả về hai thứ:

  1. X (Các đặc trưng - The Features):

    • Là một mảng NumPy chứa tọa độ [x, y] của tất cả các điểm.
    • Với samples=100classes=3, nó sẽ tạo ra 100 * 3 = 300 điểm.
    • Vì vậy, X sẽ có kích thước là (300, 2).
    • Trong câu chuyện "giám khảo hoa quả" của chúng ta, X tương đương với một danh sách 300 quả, mỗi quả có 2 đặc điểm là "Độ đỏ" và "Đ Độ tròn".
  2. y (Các nhãn - The Labels):

    • Là một mảng NumPy chứa nhãn lớp cho mỗi điểm tương ứng trong X.
    • Nó sẽ chứa 300 con số, bao gồm 100 số 0, 100 số 1, và 100 số 2.
    • y[i] là nhãn (đáp án đúng) cho điểm X[i].
    • Trong câu chuyện của chúng ta, y là danh sách đáp án đúng: quả nào là "Táo" (lớp 0), quả nào là "Cam" (lớp 1), quả nào là "Chuối" (lớp 2).

Vì vậy, spiral_data không chỉ là một bộ dữ liệu, mà nó là một bài toán phân loại phi tuyến kinh điển được đóng gói sẵn để bạn có thể nhanh chóng kiểm tra mô hình của mình.