🤖 AI cơ bản
Trí tuệ nhân tạo từ gốc: dữ liệu, hồi quy, phân loại, đánh giá mô hình, mạng nơ-ron tự viết bằng numpy, nhận dạng chữ số — và AI chạy trên ESP32.
?Khóa học dành cho ai?
Học sinh từ lớp 8 trở lên đã học Python cơ bản (biến, vòng lặp, list, hàm). Không cần toán cao cấp — mọi công thức được giải thích bằng hình và code; chỉ cần biết phương trình đường thẳng và định lý Pythago.
Điểm khác biệt: tự cài đặt các thuật toán cốt lõi (gradient descent, kNN, k-means, mạng nơ-ron, lan truyền ngược) bằng numpy để hiểu bản chất, sau đó dùng scikit-learn như trong thực tế — và kết thúc bằng dự án AI chạy trên ESP32.
📖 10 bài
Mục tiêu, lý thuyết kèm hình vẽ từ dữ liệu thật, lỗi hay gặp, bài tập.
▶ 11 chương trình
Kết quả trong trang là kết quả chạy thật; chạy lại được trên máy hoặc Google Colab.
🔌 AI + hệ nhúng
Hiệu chuẩn cảm biến, cây quyết định thành code C, phát hiện ngã bằng MPU6050.
⚙Chuẩn bị
Cách nhanh nhất: mở Google Colab, dán code và chạy (có sẵn numpy, scikit-learn). Hoặc trên máy: pip install numpy scikit-learn.
⌚Lộ trình gợi ý — 10 buổi
→Khóa liên quan
🐍 Python cơ bản
Học trước khóa này nếu chưa biết Python.
🔌 Arduino & ESP32
Thu dữ liệu cảm biến, chạy mô hình trên board.
⏱ FreeRTOS
Chạy song song đọc cảm biến, suy luận và gửi dữ liệu.
🤖 AI là gì? Lập trình truyền thống và học máy
Hiểu AI, học máy, học sâu khác nhau thế nào và vì sao “cho máy học từ dữ liệu” lại mạnh hơn tự viết luật trong nhiều bài toán.
- Phân biệt AI – Machine Learning – Deep Learning
- Hiểu học có giám sát / không giám sát
- Viết chương trình đầu tiên để máy tự tìm ngưỡng phân loại từ dữ liệu
1Trí tuệ nhân tạo, học máy, học sâu
Trí tuệ nhân tạo (AI) là mọi kỹ thuật giúp máy tính làm những việc tưởng như cần trí thông minh của con người: nhận ra khuôn mặt, hiểu câu nói, chơi cờ, lái xe. Học máy (Machine Learning – ML) là nhánh quan trọng nhất của AI hiện nay: thay vì lập trình từng luật, ta đưa cho máy rất nhiều ví dụ và để máy tự rút ra quy luật. Học sâu (Deep Learning) là học máy dùng mạng nơ-ron nhiều lớp — công nghệ đứng sau ChatGPT, nhận dạng ảnh, xe tự lái.
2Đổi cách nghĩ: từ “viết luật” sang “học từ dữ liệu”
Khi viết chương trình thông thường, con người phải nghĩ ra luật (if nhiệt độ > 30 thì bật quạt). Nhưng có những luật ta không thể viết ra: làm sao mô tả bằng if-else “thế nào là chữ số 7 viết tay”? Học máy đảo ngược: ta cho dữ liệu + đáp án, máy tìm ra luật — gọi là mô hình (model). Sau đó dùng mô hình để dự đoán dữ liệu mới.
| Loại học | Dữ liệu | Ví dụ |
|---|---|---|
| Có giám sát (supervised) | có nhãn (đáp án) | đoán giá nhà (hồi quy – ra số), lọc thư rác (phân loại – ra nhóm) |
| Không giám sát (unsupervised) | không có nhãn | chia khách hàng thành nhóm, tìm điểm bất thường |
| Học tăng cường (reinforcement) | phần thưởng / phạt | robot tự học đi, AI chơi game |
3Ví dụ: máy tự tìm ngưỡng phân biệt hai loài hoa
"""Bài 1 – Lập trình truyền thống vs học máy.
Bài toán: từ chiều dài cánh hoa, đoán loài hoa diên vĩ (Iris versicolor hay virginica)."""
from sklearn.datasets import load_iris
iris = load_iris()
X = iris.data[50:, 2] # chiều dài cánh hoa (cm) của 100 bông: 50 versicolor, 50 virginica
y = iris.target[50:] - 1 # 0 = versicolor, 1 = virginica
ten = ["versicolor", "virginica"]
# ----- Cách 1: LẬP TRÌNH TRUYỀN THỐNG – con người tự nghĩ ra luật -----
def luat_tu_viet(chieu_dai):
return 1 if chieu_dai > 5.5 else 0 # "cảm giác" hoa virginica to hơn 5.5 cm
dung = sum(luat_tu_viet(x) == t for x, t in zip(X, y))
print(f"Luật tự viết (ngưỡng 5.5 cm): đúng {dung}/{len(y)}")
# ----- Cách 2: HỌC MÁY – máy tự tìm ngưỡng tốt nhất từ DỮ LIỆU -----
tot_nhat, nguong_tot = 0, None
for nguong in sorted(set(X)): # thử mọi ngưỡng có thể
so_dung = sum((1 if x > nguong else 0) == t for x, t in zip(X, y))
if so_dung > tot_nhat:
tot_nhat, nguong_tot = so_dung, nguong
print(f"Ngưỡng máy học được: {nguong_tot} cm -> đúng {tot_nhat}/{len(y)}")
# ----- Dùng "mô hình" vừa học để dự đoán hoa MỚI -----
for x_moi in [3.9, 4.8, 5.6]:
print(f" Cánh hoa {x_moi} cm -> đoán là {ten[int(x_moi > nguong_tot)]}")
print("\nThống kê dữ liệu (máy 'nhìn thấy' gì):")
for k in (0, 1):
v = X[y == k]
print(f" {ten[k]:<10}: nhỏ nhất {v.min():.1f}, lớn nhất {v.max():.1f}, trung bình {v.mean():.2f} cm")
python 01-luat-va-hoc.pyLuật tự viết (ngưỡng 5.5 cm): đúng 75/100 Ngưỡng máy học được: 4.7 cm -> đúng 93/100 Cánh hoa 3.9 cm -> đoán là versicolor Cánh hoa 4.8 cm -> đoán là virginica Cánh hoa 5.6 cm -> đoán là virginica Thống kê dữ liệu (máy 'nhìn thấy' gì): versicolor: nhỏ nhất 3.0, lớn nhất 5.1, trung bình 4.26 cm virginica : nhỏ nhất 4.5, lớn nhất 6.9, trung bình 5.55 cm
Giải thích
- “Huấn luyện” ở đây chỉ là thử mọi ngưỡng và giữ ngưỡng đúng nhiều nhất — mọi thuật toán học máy đều là phiên bản thông minh hơn của ý tưởng này.
- Luật “cảm tính” 5.5 cm chỉ đúng 75%; máy nhìn dữ liệu nên tìm được 4.7 cm đúng 93%.
- Không đạt 100% vì hai loài có vùng chồng lấn — dùng thêm đặc trưng (chiều rộng cánh hoa…) sẽ tốt hơn (bài 4).
⚠Lỗi thường gặp
- Nghĩ AI là “phép màu” — mô hình chỉ tốt bằng dữ liệu đưa vào (rác vào → rác ra).
- Nghĩ mọi bài toán đều cần AI: nếu luật đơn giản và rõ ràng, if-else nhanh và chắc chắn hơn.
✎Bài tập về nhà
📊 Dữ liệu và thư viện numpy
Biểu diễn dữ liệu dưới dạng bảng/mảng, dùng numpy để thống kê, lọc, làm sạch và chuẩn hoá.
- Hiểu mẫu – đặc trưng – nhãn, ma trận X và vectơ y
- Tạo mảng numpy, shape, axis
- Tính trung bình, độ lệch chuẩn; lọc bằng điều kiện
- Chuẩn hoá min-max và z-score
1Dữ liệu trong học máy
Mọi dữ liệu (ảnh, âm thanh, số đo cảm biến) cuối cùng đều được biến thành bảng số. Quy ước: X là ma trận đặc trưng (mỗi hàng một mẫu), y là vectơ nhãn. Cài thư viện: pip install numpy scikit-learn (hoặc dùng Google Colab – có sẵn mọi thứ, chạy trên trình duyệt).
2numpy: tính toán trên cả mảng
numpy lưu dữ liệu trong mảng nhiều chiều và tính toán trên cả mảng một lúc (vectơ hoá) — nhanh hơn vòng lặp Python hàng chục lần vì phần lõi viết bằng C. axis=0 là tính dọc theo cột (gộp các hàng), axis=1 tính theo hàng.
"""Bài 2 – Dữ liệu và numpy: mảng, thống kê, chuẩn hoá, lọc."""
import numpy as np
# Nhiệt độ (°C) đo bởi trạm thời tiết trong 2 tuần, mỗi hàng 1 tuần, mỗi cột 1 ngày
nhiet = np.array([[29, 31, 30, 33, 34, 32, 28],
[27, 26, 30, 35, 36, 33, 31]])
print("Kích thước (shape):", nhiet.shape, "| số phần tử:", nhiet.size)
print("Trung bình cả 2 tuần:", nhiet.mean().round(2))
print("Trung bình mỗi tuần:", nhiet.mean(axis=1).round(2))
print("Nóng nhất mỗi ngày trong tuần:", nhiet.max(axis=0))
print("Độ lệch chuẩn:", nhiet.std().round(2))
# Phép toán trên CẢ mảng (không cần vòng lặp) – nhanh và gọn
do_f = nhiet * 9 / 5 + 32
print("Đổi sang °F (tuần 1):", do_f[0])
# Lọc bằng điều kiện
print("Các ngày trên 32°C:", nhiet[nhiet > 32])
print("Số ngày nóng:", (nhiet > 32).sum())
# Chuẩn hoá dữ liệu về 0..1 (min-max) – bước bắt buộc trước khi cho nhiều mô hình học
lo, hi = nhiet.min(), nhiet.max()
chuan_hoa = (nhiet - lo) / (hi - lo)
print("Chuẩn hoá min-max (tuần 1):", chuan_hoa[0].round(2))
# Chuẩn hoá z-score: trung bình 0, độ lệch chuẩn 1
z = (nhiet - nhiet.mean()) / nhiet.std()
print("z-score (tuần 2):", z[1].round(2))
# Dữ liệu có giá trị lỗi (cảm biến hỏng trả về -999) -> làm sạch
tho = np.array([28.5, 29.0, -999, 30.1, 29.8, -999, 31.2])
sach = tho[tho > -100]
print(f"Dữ liệu thô {len(tho)} mẫu, sau khi bỏ lỗi còn {len(sach)}, trung bình {sach.mean():.2f}")
# Ma trận ngẫu nhiên có seed – cho kết quả lặp lại được
rng = np.random.default_rng(42)
print("5 số ngẫu nhiên chuẩn:", rng.normal(0, 1, 5).round(3))
python 02-numpy-du-lieu.pyKích thước (shape): (2, 7) | số phần tử: 14 Trung bình cả 2 tuần: 31.07 Trung bình mỗi tuần: [31. 31.14] Nóng nhất mỗi ngày trong tuần: [29 31 30 35 36 33 31] Độ lệch chuẩn: 2.87 Đổi sang °F (tuần 1): [84.2 87.8 86. 91.4 93.2 89.6 82.4] Các ngày trên 32°C: [33 34 35 36 33] Số ngày nóng: 5 Chuẩn hoá min-max (tuần 1): [0.3 0.5 0.4 0.7 0.8 0.6 0.2] z-score (tuần 2): [-1.42 -1.77 -0.37 1.37 1.72 0.67 -0.02] Dữ liệu thô 7 mẫu, sau khi bỏ lỗi còn 5, trung bình 29.72 5 số ngẫu nhiên chuẩn: [ 0.305 -1.04 0.75 0.941 -1.951]
Giải thích
nhiet[nhiet > 32]: biểu thức điều kiện tạo mảng True/False, dùng làm “mặt nạ” để lọc.(nhiet > 32).sum()đếm số phần tử thoả mãn vì True = 1.- Cảm biến hỏng hay trả về giá trị vô lý (-999, 0, 85°C với DS18B20) – luôn làm sạch trước khi học.
default_rng(42): cùng seed → cùng dãy ngẫu nhiên, kết quả lặp lại được khi chạy lại.
3Vì sao phải chuẩn hoá?
Diện tích (30–120) và số phòng (1–5) có thang đo khác nhau rất nhiều. Các thuật toán dựa vào khoảng cách (kNN, k-means) hoặc gradient descent sẽ bị đặc trưng có số lớn “lấn át”. Chuẩn hoá đưa mọi cột về cùng thang:
- min-max:
(x − min) / (max − min)→ khoảng 0…1 (hay dùng cho ảnh, tín hiệu). - z-score:
(x − trung bình) / độ lệch chuẩn→ trung bình 0, độ lệch 1 (StandardScalertrong scikit-learn).
⚠Lỗi thường gặp
- Quên
axis→ tính trung bình của cả bảng thay vì từng cột. - Chuẩn hoá bằng min/max của toàn bộ dữ liệu rồi mới chia train/test — làm “rò rỉ” thông tin dữ liệu kiểm tra.
✎Bài tập về nhà
📈 Hồi quy tuyến tính và gradient descent
Hiểu mô hình đầu tiên — đường thẳng khớp dữ liệu — và cách máy “học” bằng cách giảm dần sai số.
- Mô hình y = w·x + b, hàm mất mát MSE
- Ý tưởng gradient descent và tốc độ học
- Tự cài bằng numpy và so sánh với LinearRegression
- Đánh giá bằng R²
1Mô hình và hàm mất mát
Ta giả sử giá nhà tỉ lệ với diện tích: giá ≈ w × diện_tích + b. “Học” là tìm w, b sao cho đường thẳng gần các điểm nhất. Đo độ “gần” bằng hàm mất mát (loss) — ở đây là MSE (Mean Squared Error):
MSE = trung bình của (dự_đoán − thật)²
Bình phương để sai âm/dương không triệt tiêu nhau và phạt nặng sai số lớn.
2Gradient descent – xuống dốc từng bước
Hãy tưởng tượng MSE là một thung lũng, vị trí của ta là giá trị w hiện tại. Đạo hàm (độ dốc) cho biết đi hướng nào thì mất mát tăng; ta bước ngược hướng đó. Với MSE, đạo hàm tính được bằng công thức: ∂MSE/∂w = 2·trung_bình(sai × x), ∂MSE/∂b = 2·trung_bình(sai).
"""Bài 3 – Hồi quy tuyến tính: dự đoán giá nhà theo diện tích.
Tự cài GRADIENT DESCENT bằng numpy, rồi so sánh với scikit-learn."""
import numpy as np
from sklearn.linear_model import LinearRegression
rng = np.random.default_rng(0)
dien_tich = rng.uniform(30, 120, 40) # m²
gia = 0.045 * dien_tich + 0.5 + rng.normal(0, 0.35, 40) # tỉ đồng (có nhiễu)
# Chuẩn hoá đầu vào giúp gradient descent hội tụ nhanh
x = (dien_tich - dien_tich.mean()) / dien_tich.std()
w, b = 0.0, 0.0 # mô hình: gia ≈ w*x + b
toc_do_hoc = 0.1
for vong in range(201):
du_doan = w * x + b
sai = du_doan - gia
mse = (sai ** 2).mean() # hàm mất mát: sai số bình phương trung bình
dw = 2 * (sai * x).mean() # đạo hàm của MSE theo w
db = 2 * sai.mean() # đạo hàm theo b
w -= toc_do_hoc * dw # bước xuống dốc
b -= toc_do_hoc * db
if vong in (0, 5, 20, 50, 200):
print(f"vòng {vong:>3}: MSE = {mse:.4f} w = {w:.4f} b = {b:.4f}")
# Quy đổi về đơn vị gốc: gia = a * dien_tich + c
a = w / dien_tich.std()
c = b - a * dien_tich.mean()
print(f"\nMô hình tự học : giá = {a:.4f} x diện tích + {c:.3f}")
mo_hinh = LinearRegression().fit(dien_tich.reshape(-1, 1), gia)
print(f"scikit-learn : giá = {mo_hinh.coef_[0]:.4f} x diện tích + {mo_hinh.intercept_:.3f}")
print(f"R² (độ khớp, 1 là tuyệt đối): {mo_hinh.score(dien_tich.reshape(-1, 1), gia):.3f}")
for s in (50, 80, 100):
print(f" Nhà {s} m² -> dự đoán {a * s + c:.2f} tỉ đồng")
python 03-hoi-quy-tuyen-tinh.pyvòng 0: MSE = 18.7960 w = 0.2535 b = 0.8258 vòng 5: MSE = 2.1442 w = 0.9352 b = 3.0466 vòng 20: MSE = 0.1437 w = 1.2558 b = 4.0909 vòng 50: MSE = 0.1412 w = 1.2675 b = 4.1289 vòng 200: MSE = 0.1412 w = 1.2675 b = 4.1290 Mô hình tự học : giá = 0.0468 x diện tích + 0.463 scikit-learn : giá = 0.0468 x diện tích + 0.463 R² (độ khớp, 1 là tuyệt đối): 0.919 Nhà 50 m² -> dự đoán 2.80 tỉ đồng Nhà 80 m² -> dự đoán 4.21 tỉ đồng Nhà 100 m² -> dự đoán 5.14 tỉ đồng
Giải thích
- Sau ~20 vòng MSE gần như không giảm nữa → đã tới đáy; kết quả trùng với scikit-learn (giải bằng công thức đại số).
- Chuẩn hoá x trước giúp thung lũng “tròn”, đi nhanh; nếu không, phải chọn tốc độ học rất nhỏ.
- R² = 0.919: mô hình giải thích được ~92% sự thay đổi của giá; 1 là hoàn hảo, 0 là không hơn đoán bằng trung bình.
- Mạng nơ-ron khổng lồ (ChatGPT) cũng học bằng đúng ý tưởng này, chỉ là có hàng tỉ tham số thay vì 2.
🔧Áp dụng trong lập trình nhúng
- Hiệu chuẩn cảm biến: đo cảm biến rẻ (ví dụ cảm biến độ ẩm đất điện dung) song song với thiết bị chuẩn, khớp đường thẳng
giá_trị_thật = w × ADC + b, rồi chỉ cần nạp 2 số w, b vào Arduino. - Dự báo xu hướng: khớp đường thẳng qua 10 phút số đo mực nước để báo trước khi bể tràn.
⚠Lỗi thường gặp
- Tốc độ học quá lớn → MSE tăng vọt (phân kỳ); quá nhỏ → hàng nghìn vòng chưa tới.
- Dùng mô hình dự đoán xa ngoài vùng dữ liệu (nhà 1000 m²) — mô hình không biết gì ở đó.
✎Bài tập về nhà
🌸 Phân loại với k láng giềng gần nhất (kNN)
Hiểu bài toán phân loại, cài thuật toán kNN và làm quen quy trình scikit-learn: chia dữ liệu → fit → predict → score.
- Khoảng cách Euclid giữa hai mẫu
- Cài kNN bằng numpy, ảnh hưởng của k
- Chia train/test với train_test_split
- API chung của scikit-learn
1Ý tưởng: “Gần mực thì đen”
Muốn biết bông hoa mới thuộc loài nào, tìm k bông đã biết loài giống nó nhất (khoảng cách nhỏ nhất) rồi cho chúng bỏ phiếu. Khoảng cách Euclid giữa hai điểm (a₁, a₂) và (b₁, b₂): √((a₁−b₁)² + (a₂−b₂)²) — chính là định lý Pythago, mở rộng được cho bất kỳ số đặc trưng nào.
kNN “lười”: không học gì lúc huấn luyện, chỉ nhớ dữ liệu; mọi công việc dồn vào lúc dự đoán.
2Chia dữ liệu học / kiểm tra
Mọi mô hình trong scikit-learn dùng chung 3 lệnh: mo_hinh.fit(X, y) (học), mo_hinh.predict(X_moi) (dự đoán), mo_hinh.score(X, y) (độ chính xác). Đổi thuật toán chỉ cần đổi 1 dòng tạo mô hình.
"""Bài 4 – Phân loại với k láng giềng gần nhất (kNN): tự viết bằng numpy và so sánh với scikit-learn.
Dữ liệu: hoa Iris, dùng 2 đặc trưng chiều dài và chiều rộng cánh hoa, 3 loài."""
import numpy as np
from collections import Counter
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.neighbors import KNeighborsClassifier
iris = load_iris()
X = iris.data[:, 2:4] # chiều dài, chiều rộng cánh hoa (cm)
y = iris.target
TEN = iris.target_names
X_hoc, X_thu, y_hoc, y_thu = train_test_split(X, y, test_size=0.3, random_state=1, stratify=y)
print(f"Dữ liệu học: {len(X_hoc)} bông, dữ liệu kiểm tra: {len(X_thu)} bông")
def knn_du_doan(x_moi, k=5):
khoang_cach = np.sqrt(((X_hoc - x_moi) ** 2).sum(axis=1)) # khoảng cách Euclid tới MỌI mẫu học
gan_nhat = np.argsort(khoang_cach)[:k] # chỉ số k mẫu gần nhất
phieu = Counter(y_hoc[gan_nhat]) # bỏ phiếu theo nhãn
return phieu.most_common(1)[0][0]
for k in (1, 3, 5, 15):
du_doan = np.array([knn_du_doan(x, k) for x in X_thu])
print(f"k = {k:>2}: độ chính xác tự viết = {(du_doan == y_thu).mean():.1%}")
mo_hinh = KNeighborsClassifier(n_neighbors=5).fit(X_hoc, y_hoc)
print(f"scikit-learn (k = 5): {mo_hinh.score(X_thu, y_thu):.1%}")
for hoa in ([1.5, 0.3], [4.5, 1.4], [5.8, 2.2], [4.9, 1.7]):
print(f" Cánh {hoa[0]} x {hoa[1]} cm -> {TEN[knn_du_doan(np.array(hoa))]}")
python 04-knn.pyDữ liệu học: 105 bông, dữ liệu kiểm tra: 45 bông k = 1: độ chính xác tự viết = 97.8% k = 3: độ chính xác tự viết = 97.8% k = 5: độ chính xác tự viết = 97.8% k = 15: độ chính xác tự viết = 97.8% scikit-learn (k = 5): 97.8% Cánh 1.5 x 0.3 cm -> setosa Cánh 4.5 x 1.4 cm -> versicolor Cánh 5.8 x 2.2 cm -> virginica Cánh 4.9 x 1.7 cm -> virginica
Giải thích
np.argsorttrả về chỉ số sắp theo khoảng cách tăng dần; lấy[:k]là k láng giềng.stratify=y: tỉ lệ các loài trong tập học và tập kiểm tra giống nhau.- Bông 4.9 × 1.7 cm nằm ngay ranh giới versicolor / virginica — dễ đoán sai nhất.
- k nhỏ dễ bị nhiễu bởi 1 điểm lạ; k quá lớn làm mờ ranh giới. Thường chọn k lẻ để tránh hoà phiếu.
⚠Lỗi thường gặp
- Không chuẩn hoá khi các đặc trưng khác thang đo — kNN gần như chỉ nhìn đặc trưng có số lớn nhất.
- Chấm điểm trên chính dữ liệu học (k = 1 sẽ luôn đúng 100%!).
✎Bài tập về nhà
🎯 Đánh giá mô hình: độ chính xác chưa đủ
Đánh giá đúng chất lượng mô hình bằng ma trận nhầm lẫn, precision, recall và nhận biết quá khớp.
- Đọc ma trận nhầm lẫn
- Hiểu precision và recall, khi nào ưu tiên cái nào
- Nhận biết underfit / overfit
- Dùng pipeline chuẩn hoá + mô hình
1Ma trận nhầm lẫn, precision, recall
Một mô hình chẩn đoán đoán “lành” cho mọi người vẫn có thể đạt accuracy 63% (vì 63% số ca là lành) — nhưng vô dụng. Cần nhìn loại lỗi: bỏ sót ca bệnh (âm tính giả) nguy hiểm hơn nhiều so với báo động nhầm (dương tính giả).
- Precision (độ chuẩn): trong những lần mô hình nói “có”, bao nhiêu % đúng. Quan trọng khi báo nhầm gây hại (lọc thư rác xoá nhầm thư thật).
- Recall (độ phủ): trong tất cả các ca “có” thật, mô hình tìm ra bao nhiêu %. Quan trọng khi bỏ sót gây hại (bệnh, cháy, ngã).
"""Bài 5 – Đánh giá mô hình: chia dữ liệu, độ chính xác, ma trận nhầm lẫn, precision / recall,
và hiện tượng QUÁ KHỚP (overfitting)."""
import numpy as np
from sklearn.datasets import load_breast_cancer
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import confusion_matrix, precision_score, recall_score
from sklearn.model_selection import train_test_split
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
# Bài toán y tế: dự đoán khối u lành (1) hay ác tính (0) từ 30 chỉ số ảnh siêu âm
du_lieu = load_breast_cancer()
X, y = du_lieu.data, du_lieu.target
X_hoc, X_thu, y_hoc, y_thu = train_test_split(X, y, test_size=0.25, random_state=0, stratify=y)
mo_hinh = make_pipeline(StandardScaler(), LogisticRegression(max_iter=1000)).fit(X_hoc, y_hoc)
du_doan = mo_hinh.predict(X_thu)
print(f"Số mẫu học {len(X_hoc)}, kiểm tra {len(X_thu)}")
print(f"Độ chính xác (accuracy): {(du_doan == y_thu).mean():.1%}")
tn, fp, fn, tp = confusion_matrix(y_thu, du_doan, labels=[0, 1]).ravel()
print("Ma trận nhầm lẫn (hàng = thật, cột = dự đoán):")
print(f" đoán ác tính đoán lành")
print(f" thật ác tính {tn:>4} {fp:>4} <- {fp} ca ác tính bị bỏ sót (nguy hiểm!)")
print(f" thật lành {fn:>4} {tp:>4}")
print(f"Precision (đoán 'lành' thì đúng bao nhiêu): {precision_score(y_thu, du_doan):.1%}")
print(f"Recall (tìm ra được bao nhiêu ca lành) : {recall_score(y_thu, du_doan):.1%}")
print(f"Recall cho lớp ác tính (quan trọng nhất) : {recall_score(y_thu, du_doan, pos_label=0):.1%}")
# ----- Quá khớp: mô hình đa thức bậc cao "học thuộc" dữ liệu -----
rng = np.random.default_rng(3)
x = np.sort(rng.uniform(0, 1, 15))
y_that = np.sin(2 * np.pi * x) + rng.normal(0, 0.2, 15)
x_moi = np.linspace(x.min(), x.max(), 50) # điểm MỚI nằm giữa các điểm đã học
y_moi = np.sin(2 * np.pi * x_moi)
print("\nĐa thức bậc d khớp 15 điểm (sai số trên dữ liệu học / dữ liệu mới):")
for d in (1, 3, 9):
he_so = np.polyfit(x, y_that, d)
sai_hoc = np.mean((np.polyval(he_so, x) - y_that) ** 2)
sai_moi = np.mean((np.polyval(he_so, x_moi) - y_moi) ** 2)
nhan_xet = "chưa khớp (underfit)" if d == 1 else ("vừa đẹp" if d == 3 else "QUÁ KHỚP (overfit)")
print(f" bậc {d:>2}: học {sai_hoc:.3f} | mới {sai_moi:.3f} -> {nhan_xet}")
python 05-danh-gia-mo-hinh.pySố mẫu học 426, kiểm tra 143
Độ chính xác (accuracy): 95.8%
Ma trận nhầm lẫn (hàng = thật, cột = dự đoán):
đoán ác tính đoán lành
thật ác tính 50 3 <- 3 ca ác tính bị bỏ sót (nguy hiểm!)
thật lành 3 87
Precision (đoán 'lành' thì đúng bao nhiêu): 96.7%
Recall (tìm ra được bao nhiêu ca lành) : 96.7%
Recall cho lớp ác tính (quan trọng nhất) : 94.3%
Đa thức bậc d khớp 15 điểm (sai số trên dữ liệu học / dữ liệu mới):
bậc 1: học 0.116 | mới 0.111 -> chưa khớp (underfit)
bậc 3: học 0.020 | mới 0.008 -> vừa đẹp
bậc 9: học 0.007 | mới 0.301 -> QUÁ KHỚP (overfit)Giải thích
make_pipeline(StandardScaler(), LogisticRegression()): chuẩn hoá được “học” chỉ từ dữ liệu train rồi áp cho test — tránh rò rỉ.- Logistic Regression là hồi quy tuyến tính + hàm sigmoid để ra xác suất 0…1 — một nơ-ron đơn (bài 8).
2Chưa khớp và quá khớp
Quá khớp (overfitting): mô hình quá phức tạp “học thuộc lòng” cả nhiễu của dữ liệu học, đúng tuyệt đối trên bài cũ nhưng sai trên bài mới. Chưa khớp (underfitting): mô hình quá đơn giản, sai cả trên dữ liệu học. Dấu hiệu nhận biết: sai số học thấp nhưng sai số kiểm tra cao → quá khớp.
Cách chống quá khớp: thêm dữ liệu, chọn mô hình đơn giản hơn, giới hạn độ sâu cây / số vòng huấn luyện, kiểm tra chéo (cross-validation).
⚠Lỗi thường gặp
- Chỉ báo accuracy cho dữ liệu lệch lớp (99% không cháy, 1% cháy).
- Chỉnh mô hình nhiều lần theo điểm tập test — dần dần tập test cũng bị “học thuộc”; nên tách thêm tập validation.
✎Bài tập về nhà
🌳 Cây quyết định – mô hình giải thích được
Huấn luyện cây quyết định, đọc các luật máy học được và biết mức độ quan trọng của từng đặc trưng.
- Cấu trúc cây: nút hỏi, nhánh, lá
- Độ sâu cây và quá khớp
- In luật bằng export_text, feature_importances_
1Máy tự đặt câu hỏi
Cây quyết định giống trò chơi “20 câu hỏi”: mỗi bước chọn một đặc trưng và một ngưỡng chia dữ liệu thành hai phần “thuần” nhất (ít lẫn lộn nhãn nhất, đo bằng chỉ số Gini). Lặp lại với từng phần tới khi đạt độ sâu tối đa. Ưu điểm lớn: con người đọc hiểu được, và dễ chuyển thành if-else cho vi điều khiển.
2Ví dụ: hệ thống tưới cây thông minh
"""Bài 6 – Cây quyết định: mô hình "dễ hiểu" – in được ra các câu hỏi Có/Không như con người."""
import numpy as np
from sklearn.tree import DecisionTreeClassifier, export_text
# Dữ liệu tự tạo: có nên tưới cây không? [độ ẩm đất %, nhiệt độ °C, khả năng mưa %]
rng = np.random.default_rng(7)
do_am = rng.uniform(10, 90, 200)
nhiet = rng.uniform(18, 40, 200)
mua = rng.uniform(0, 100, 200)
X = np.column_stack([do_am, nhiet, mua])
# "Luật thật" (máy không biết): đất khô dưới 35% và sắp không mưa -> tưới; hoặc rất nóng và đất dưới 50%
y = (((do_am < 35) & (mua < 60)) | ((nhiet > 35) & (do_am < 50))).astype(int)
cay = DecisionTreeClassifier(max_depth=3, random_state=0).fit(X, y)
print(f"Độ chính xác trên dữ liệu học: {cay.score(X, y):.1%}")
print("Cây quyết định máy học được:\n")
print(export_text(cay, feature_names=["do_am", "nhiet_do", "kha_nang_mua"], class_names=["khong_tuoi", "TUOI"]))
print("Mức độ quan trọng của từng đặc trưng:")
for ten, qt in zip(["độ ẩm đất", "nhiệt độ", "khả năng mưa"], cay.feature_importances_):
print(f" {ten:<14}{'█' * int(qt * 40)} {qt:.2f}")
for mau in ([25, 30, 20], [25, 30, 80], [45, 38, 10], [70, 25, 0]):
kq = cay.predict([mau])[0]
print(f" Đất {mau[0]}%, {mau[1]}°C, mưa {mau[2]}% -> {'TƯỚI' if kq else 'không tưới'}")
python 06-cay-quyet-dinh.pyĐộ chính xác trên dữ liệu học: 99.5% Cây quyết định máy học được: |--- do_am <= 34.69 | |--- kha_nang_mua <= 62.15 | | |--- class: TUOI | |--- kha_nang_mua > 62.15 | | |--- nhiet_do <= 35.53 | | | |--- class: khong_tuoi | | |--- nhiet_do > 35.53 | | | |--- class: TUOI |--- do_am > 34.69 | |--- nhiet_do <= 36.41 | | |--- nhiet_do <= 35.57 | | | |--- class: khong_tuoi | | |--- nhiet_do > 35.57 | | | |--- class: khong_tuoi | |--- nhiet_do > 36.41 | | |--- do_am <= 47.96 | | | |--- class: TUOI | | |--- do_am > 47.96 | | | |--- class: khong_tuoi Mức độ quan trọng của từng đặc trưng: độ ẩm đất ███████████████████████ 0.58 nhiệt độ ██████ 0.16 khả năng mưa ██████████ 0.27 Đất 25%, 30°C, mưa 20% -> TƯỚI Đất 25%, 30°C, mưa 80% -> không tưới Đất 45%, 38°C, mưa 10% -> TƯỚI Đất 70%, 25°C, mưa 0% -> không tưới
Giải thích
- Máy tìm lại gần đúng “luật thật” (độ ẩm 35%, mưa 60%, nhiệt độ 35°C) chỉ từ 200 mẫu dữ liệu.
- Có nút chia mà hai nhánh cùng ra
khong_tuoi— vì xác suất bên trong khác nhau, nhưng nhãn đa số như nhau; có thể cắt tỉa. max_depth=3giới hạn cây 3 tầng: dễ đọc và tránh quá khớp. Không giới hạn, cây có thể học thuộc từng mẫu.- Random Forest = trung bình hàng trăm cây khác nhau → chính xác và ổn định hơn một cây.
🔧Áp dụng trong lập trình nhúng
- Cây quyết định là mô hình lý tưởng cho Arduino: sau khi học, nó chỉ là vài câu
iflồng nhau, tốn vài chục byte và chạy trong vài micro giây. Bài 10 sẽ tự động sinh code C từ cây. - Thay vì tự “chỉnh tay” ngưỡng cho hệ tưới cây / báo cháy, thu dữ liệu thật vài ngày và để cây quyết định tìm ngưỡng.
✎Bài tập về nhà
🧩 Học không giám sát: phân cụm k-means
Tìm các nhóm tự nhiên trong dữ liệu không có nhãn bằng thuật toán k-means.
- Khác biệt giữa có / không giám sát
- Hai bước lặp của k-means
- Ứng dụng: chia nhóm, đặt trạm, nén màu ảnh
1Thuật toán k-means
- Chọn ngẫu nhiên k tâm cụm.
- Gán: mỗi điểm thuộc về tâm gần nó nhất.
- Cập nhật: dời mỗi tâm về trung bình các điểm của nó.
- Lặp bước 2–3 tới khi tâm không còn di chuyển.
"""Bài 7 – Học KHÔNG giám sát: phân cụm k-means tự viết bằng numpy.
Dữ liệu: vị trí (km) của 90 nhà học sinh – tìm 3 điểm đặt trạm xe buýt đưa đón."""
import numpy as np
rng = np.random.default_rng(5)
tam_that = np.array([[2, 3], [8, 8], [9, 2]])
X = np.vstack([rng.normal(t, 1.0, (30, 2)) for t in tam_that]) # 3 khu dân cư (máy không biết)
k = 3
tam = X[rng.choice(len(X), k, replace=False)] # bước 0: chọn ngẫu nhiên 3 tâm
for vong in range(1, 20):
kc = np.linalg.norm(X[:, None, :] - tam[None, :, :], axis=2) # khoảng cách mọi điểm tới mọi tâm
nhom = kc.argmin(axis=1) # bước 1: gán điểm vào tâm gần nhất
tam_moi = np.array([X[nhom == j].mean(axis=0) for j in range(k)]) # bước 2: dời tâm về trung bình nhóm
dich = np.linalg.norm(tam_moi - tam)
tam = tam_moi
print(f"vòng {vong}: tâm dịch chuyển {dich:.3f} km")
if dich < 1e-3:
print("-> hội tụ!")
break
print("\nVị trí 3 trạm xe buýt đề xuất:")
for j in np.argsort(tam[:, 0]):
print(f" Trạm tại ({tam[j, 0]:.2f}, {tam[j, 1]:.2f}) km – phục vụ {np.sum(nhom == j)} học sinh, "
f"xa nhất {np.max(np.linalg.norm(X[nhom == j] - tam[j], axis=1)):.2f} km")
python 07-kmeans.pyvòng 1: tâm dịch chuyển 5.827 km vòng 2: tâm dịch chuyển 2.279 km vòng 3: tâm dịch chuyển 0.000 km -> hội tụ! Vị trí 3 trạm xe buýt đề xuất: Trạm tại (1.87, 2.70) km – phục vụ 30 học sinh, xa nhất 2.73 km Trạm tại (7.89, 7.81) km – phục vụ 30 học sinh, xa nhất 2.10 km Trạm tại (9.06, 2.02) km – phục vụ 30 học sinh, xa nhất 2.60 km
Giải thích
X[:, None, :] - tam[None, :, :]: kỹ thuật broadcasting của numpy, tính 90 × 3 khoảng cách không cần vòng lặp.- k-means có thể hội tụ về kết quả kém nếu tâm ban đầu xấu → scikit-learn chạy nhiều lần (
n_init) và giữ kết quả tốt nhất. - Phải tự chọn k; “phương pháp khuỷu tay” vẽ tổng khoảng cách theo k và chọn chỗ đường cong gãy.
✎Bài tập về nhà
🧠 Nơ-ron nhân tạo và mạng nơ-ron
Hiểu cấu tạo một nơ-ron, vì sao cần nhiều lớp, và cách mạng học bằng lan truyền ngược — tự cài bằng numpy.
- Nơ-ron: tổng có trọng số + hàm kích hoạt
- Perceptron học AND / OR, thất bại với XOR
- Mạng 2 lớp, lan truyền xuôi / ngược
- Tự cài mạng học XOR
1Một nơ-ron
Lấy cảm hứng từ tế bào thần kinh: nhận nhiều tín hiệu, mỗi tín hiệu nhân với một trọng số (mức quan trọng), cộng lại với độ lệch b, rồi qua hàm kích hoạt quyết định “phát xung” hay không. Một nơ-ron thực chất vẽ ra một đường thẳng chia mặt phẳng làm hai.
"""Bài 8 – Nơ-ron nhân tạo (perceptron): học hàm AND, OR – và thất bại với XOR."""
import numpy as np
X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
BANG = {"AND": [0, 0, 0, 1], "OR": [0, 1, 1, 1], "XOR": [0, 1, 1, 0]}
def huan_luyen(y, so_vong=20, toc_do=0.2):
w = np.zeros(2)
b = 0.0
for _ in range(so_vong):
for xi, yi in zip(X, y):
ra = 1 if xi @ w + b > 0 else 0 # nơ-ron: tổng có trọng số -> hàm bước
loi = yi - ra
w += toc_do * loi * xi # luật học perceptron: chỉnh trọng số theo lỗi
b += toc_do * loi
return w, b
for ten, y in BANG.items():
w, b = huan_luyen(np.array(y))
ra = [1 if xi @ w + b > 0 else 0 for xi in X]
dung = sum(r == t for r, t in zip(ra, y))
print(f"{ten:<4}: w = {w.round(2)}, b = {b:.2f} -> đầu ra {ra} (mong muốn {y}) – đúng {dung}/4"
+ ("" if dung == 4 else " ✗ một nơ-ron KHÔNG làm được"))
print("\nXOR không chia được bằng MỘT đường thẳng -> cần nhiều nơ-ron xếp thành nhiều lớp (bài tiếp theo).")
python 08a-perceptron.pyAND : w = [0.4 0.2], b = -0.40 -> đầu ra [0, 0, 0, 1] (mong muốn [0, 0, 0, 1]) – đúng 4/4 OR : w = [0.2 0.2], b = 0.00 -> đầu ra [0, 1, 1, 1] (mong muốn [0, 1, 1, 1]) – đúng 4/4 XOR : w = [-0.2 0. ], b = 0.20 -> đầu ra [1, 1, 0, 0] (mong muốn [0, 1, 1, 0]) – đúng 2/4 ✗ một nơ-ron KHÔNG làm được XOR không chia được bằng MỘT đường thẳng -> cần nhiều nơ-ron xếp thành nhiều lớp (bài tiếp theo).
Giải thích
- Luật học perceptron: nếu đoán sai, cộng/trừ trọng số theo hướng làm giảm lỗi.
- AND, OR học được trong vài vòng; XOR thì không bao giờ — kết quả lịch sử khiến AI “ngủ đông” cuối thập niên 1960.
2Vì sao cần nhiều lớp?
XOR cần hai đường thẳng để tách. Nhiều nơ-ron ở lớp ẩn mỗi cái vẽ một đường, nơ-ron lớp ra kết hợp chúng thành ranh giới cong bất kỳ. Thêm lớp, thêm nơ-ron → mô hình biểu diễn được quan hệ càng phức tạp: đó là học sâu.
3Lan truyền ngược (backpropagation)
Huấn luyện mạng nhiều lớp vẫn là gradient descent (bài 3). Khó khăn là tính đạo hàm của mất mát theo trọng số ở lớp sâu bên trong — giải bằng quy tắc chuỗi, đi ngược từ lớp ra về lớp vào. Đạo hàm sigmoid đẹp: σ'(z) = σ(z)(1 − σ(z)).
"""Bài 8 – Mạng nơ-ron 2 lớp tự viết bằng numpy, huấn luyện bằng LAN TRUYỀN NGƯỢC (backpropagation) để học XOR."""
import numpy as np
X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]], dtype=float)
y = np.array([[0], [1], [1], [0]], dtype=float)
def sigmoid(z):
return 1 / (1 + np.exp(-z))
rng = np.random.default_rng(1)
W1 = rng.normal(0, 1, (2, 4)) # lớp ẩn: 2 đầu vào -> 4 nơ-ron
b1 = np.zeros(4)
W2 = rng.normal(0, 1, (4, 1)) # lớp ra: 4 -> 1
b2 = np.zeros(1)
toc_do = 1.0
for vong in range(5001):
# ----- lan truyền xuôi -----
h = sigmoid(X @ W1 + b1)
ra = sigmoid(h @ W2 + b2)
mat_mat = np.mean((ra - y) ** 2)
# ----- lan truyền ngược: đạo hàm theo quy tắc chuỗi -----
d_ra = 2 * (ra - y) / len(X) * ra * (1 - ra)
d_W2 = h.T @ d_ra
d_b2 = d_ra.sum(axis=0)
d_h = d_ra @ W2.T * h * (1 - h)
d_W1 = X.T @ d_h
d_b1 = d_h.sum(axis=0)
# ----- cập nhật trọng số -----
W2 -= toc_do * d_W2; b2 -= toc_do * d_b2
W1 -= toc_do * d_W1; b1 -= toc_do * d_b1
if vong in (0, 500, 1000, 2000, 5000):
print(f"vòng {vong:>4}: mất mát = {mat_mat:.4f}")
print("\nKết quả sau huấn luyện:")
for xi, ri in zip(X, sigmoid(sigmoid(X @ W1 + b1) @ W2 + b2)):
print(f" {int(xi[0])} XOR {int(xi[1])} -> {ri[0]:.3f} -> {int(ri[0] > 0.5)}")
print(f"Số tham số của mạng: {W1.size + b1.size + W2.size + b2.size}")
python 08b-mang-no-ron.pyvòng 0: mất mát = 0.2749 vòng 500: mất mát = 0.1879 vòng 1000: mất mát = 0.0135 vòng 2000: mất mát = 0.0023 vòng 5000: mất mát = 0.0006 Kết quả sau huấn luyện: 0 XOR 0 -> 0.019 -> 0 0 XOR 1 -> 0.976 -> 1 1 XOR 0 -> 0.978 -> 1 1 XOR 1 -> 0.030 -> 0 Số tham số của mạng: 17
Giải thích
- Trong 500 vòng đầu mất mát giảm chậm (mạng đang “tìm hướng”), sau đó giảm rất nhanh.
- Chỉ 17 tham số đã học được XOR. Mạng nhận dạng ảnh có hàng triệu, mô hình ngôn ngữ lớn có hàng trăm tỉ.
- Thư viện thực tế (PyTorch, TensorFlow/Keras) tính đạo hàm tự động — nhưng bên trong làm đúng như đoạn code này.
⚠Lỗi thường gặp
- Khởi tạo mọi trọng số bằng 0 → các nơ-ron ẩn giống hệt nhau, mạng không học được; phải khởi tạo ngẫu nhiên.
- Quên chuẩn hoá đầu vào → sigmoid bão hoà, đạo hàm ≈ 0, mạng “đứng yên”.
✎Bài tập về nhà
✍️ Nhận dạng chữ số viết tay
Dùng mạng nơ-ron MLP của scikit-learn để nhận dạng ảnh, hiểu ảnh được biến thành số như thế nào.
- Ảnh = ma trận độ sáng điểm ảnh
- Duỗi ảnh thành vectơ đặc trưng
- Huấn luyện MLPClassifier, đọc ma trận nhầm lẫn 10×10
- Giới thiệu mạng tích chập (CNN)
1Máy tính “nhìn” ảnh thế nào?
Ảnh xám là bảng số độ sáng. Bộ dữ liệu digits (có sẵn trong scikit-learn) gồm 1797 ảnh 8×8 điểm, mỗi điểm 0…16. Ảnh màu có 3 bảng (R, G, B); ảnh camera ESP32-CAM 320×240 là 230 400 con số.
2Mạng nơ-ron nhận dạng chữ số
"""Bài 9 – Nhận dạng chữ số viết tay (ảnh 8x8 điểm) bằng mạng nơ-ron MLP của scikit-learn."""
import numpy as np
from sklearn.datasets import load_digits
from sklearn.metrics import confusion_matrix
from sklearn.model_selection import train_test_split
from sklearn.neural_network import MLPClassifier
so = load_digits() # 1797 ảnh, mỗi ảnh 8x8 = 64 điểm ảnh, độ sáng 0..16
print(f"Số ảnh: {len(so.images)}, kích thước mỗi ảnh: {so.images[0].shape}")
KY_TU = " .:-=+*#%@"
def ve(anh): # in ảnh ra chữ để "nhìn" dữ liệu
return ["".join(KY_TU[int(v / 16 * 9)] * 2 for v in hang) for hang in anh]
print("\nẢnh đầu tiên (nhãn = %d):" % so.target[0])
for dong in ve(so.images[0]):
print(" " + dong)
X = so.data / 16.0 # chuẩn hoá về 0..1
X_hoc, X_thu, y_hoc, y_thu = train_test_split(X, so.target, test_size=0.25, random_state=0, stratify=so.target)
mlp = MLPClassifier(hidden_layer_sizes=(64,), max_iter=400, random_state=0).fit(X_hoc, y_hoc)
print(f"\nMạng 64 -> 64 -> 10 nơ-ron, huấn luyện {mlp.n_iter_} vòng")
print(f"Độ chính xác trên {len(X_thu)} ảnh chưa từng thấy: {mlp.score(X_thu, y_thu):.1%}")
du_doan = mlp.predict(X_thu)
cm = confusion_matrix(y_thu, du_doan)
print("\nMa trận nhầm lẫn (hàng: số thật 0-9, cột: số dự đoán 0-9):")
for i, hang in enumerate(cm):
print(f" {i} | " + " ".join(f"{v:>2}" if v else " ." for v in hang))
sai = np.where(du_doan != y_thu)[0][:2]
for i in sai:
print(f"\nẢnh bị nhận sai: thật là {y_thu[i]}, máy đoán {du_doan[i]}")
for dong in ve(X_thu[i].reshape(8, 8) * 16):
print(" " + dong)
python 09-nhan-dang-chu-so.pySố ảnh: 1797, kích thước mỗi ảnh: (8, 8)
Ảnh đầu tiên (nhãn = 0):
::##++
##%%++%%::
..%%.. **==
::** ====
::== ++==
::** **--
..##::++**
--##++
Mạng 64 -> 64 -> 10 nơ-ron, huấn luyện 343 vòng
Độ chính xác trên 450 ảnh chưa từng thấy: 97.3%
Ma trận nhầm lẫn (hàng: số thật 0-9, cột: số dự đoán 0-9):
0 | 45 . . . . . . . . .
1 | . 45 . . . . . . 1 .
2 | . 1 43 . . . . . . .
3 | . . . 46 . . . . . .
4 | . . . . 44 . . . 1 .
5 | . . . 1 . 45 . . . .
6 | . 1 . . . . 44 . . .
7 | . . . . . . . 44 . 1
8 | . 2 . . . 1 . . 40 .
9 | . . . 2 . 1 . . . 42
Ảnh bị nhận sai: thật là 9, máy đoán 3
..::++
..%%==##..
==-- ++--
..**==%%**
--..##..
--++
++-- @@
..**@@@@..
Ảnh bị nhận sai: thật là 4, máy đoán 8
**--
::@@....##..
++%% **%%
--@@%%@@::
--%%**
@@::
++**
##++Giải thích
- Lớp ra có 10 nơ-ron, mỗi nơ-ron là “độ tin” cho một chữ số; chọn nơ-ron lớn nhất.
- 97% trên ảnh chưa từng thấy. Những ảnh sai thường viết xấu thật — người cũng có thể nhầm.
- Ma trận nhầm lẫn cho thấy số 8 hay bị nhầm thành 1 — gợi ý cần thêm dữ liệu số 8.
3Mạng tích chập (CNN) – bước tiếp theo
Duỗi ảnh thành vectơ làm mất thông tin “điểm nào cạnh điểm nào”. Mạng tích chập (CNN) trượt các bộ lọc nhỏ 3×3 trên ảnh để phát hiện cạnh, góc, rồi hình dạng — giống cách mắt người nhìn. CNN là nền tảng của nhận dạng khuôn mặt, xe tự lái. Học tiếp với Keras trên Google Colab và bộ dữ liệu MNIST (ảnh 28×28) — đạt trên 99%.
Không cần code vẫn có thể thử ngay: Teachable Machine của Google cho phép dùng webcam dạy máy nhận dạng đồ vật trong vài phút và xuất mô hình cho web/vi điều khiển.
✎Bài tập về nhà
🏁 Dự án: AI trên vi điều khiển (TinyML) – phát hiện ngã
Đi trọn quy trình một dự án AI nhúng: thu dữ liệu cảm biến, trích đặc trưng, huấn luyện, xuất mô hình thành code C chạy trên ESP32.
- Quy trình TinyML
- Trích đặc trưng từ tín hiệu thời gian
- Xuất cây quyết định thành hàm C
- Biết các công cụ: Edge Impulse, TensorFlow Lite Micro
1TinyML là gì?
TinyML là chạy mô hình AI trên vi điều khiển chỉ vài trăm KB RAM, tiêu thụ vài mW: không cần Internet, phản hồi tức thì, bảo mật dữ liệu. Ví dụ: đồng hồ phát hiện ngã, cảm biến rung dự đoán hỏng máy, nhận dạng từ khoá “OK Google”. Việc huấn luyện (nặng) làm trên máy tính; vi điều khiển chỉ suy luận (nhẹ).
2Đặc trưng từ cảm biến gia tốc
Đưa nguyên 50 mẫu thô vào mô hình sẽ tốn bộ nhớ và cần nhiều dữ liệu. Thay vào đó tính vài đặc trưng đơn giản trên mỗi cửa sổ 1 giây: độ lệch chuẩn (mức rung lắc), giá trị lớn nhất (cú va đập), biến thiên trung bình (thay đổi nhanh hay chậm). Các phép tính này dễ viết lại trên Arduino.
3Huấn luyện và xuất code C
"""Bài 10 – DỰ ÁN AI trên vi điều khiển (TinyML): nhận biết hoạt động (đứng yên / đi bộ / chạy / ngã)
từ cảm biến gia tốc MPU6050 gắn trên người. Huấn luyện trên máy tính -> xuất thành CODE C cho ESP32."""
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
rng = np.random.default_rng(11)
NHAN = ["dung_yen", "di_bo", "chay", "nga"]
def tao_cua_so(loai):
"""Giả lập 1 giây dữ liệu (50 mẫu) độ lớn gia tốc |a| (đơn vị g)."""
t = np.arange(50) / 50
if loai == 0: a = 1 + rng.normal(0, 0.02, 50)
elif loai == 1: a = 1 + 0.25 * np.sin(2 * np.pi * 2 * t + rng.uniform(0, 6)) + rng.normal(0, 0.05, 50)
elif loai == 2: a = 1 + 0.8 * np.abs(np.sin(2 * np.pi * 3 * t + rng.uniform(0, 6))) + rng.normal(0, 0.1, 50)
else:
a = 1 + rng.normal(0, 0.05, 50)
k = rng.integers(10, 35)
a[k:k + 3] += rng.uniform(2.0, 3.0) # cú va đập mạnh
a[k + 3:] = rng.normal(0.95, 0.03, 50 - k - 3)
return a
def dac_trung(a): # rút gọn 50 mẫu thành 3 con số – tính được trên ESP32
return [a.std(), a.max(), np.abs(np.diff(a)).mean()]
X, y = [], []
for loai in range(4):
for _ in range(150):
X.append(dac_trung(tao_cua_so(loai)))
y.append(loai)
X, y = np.array(X), np.array(y)
X_hoc, X_thu, y_hoc, y_thu = train_test_split(X, y, test_size=0.3, random_state=0, stratify=y)
cay = DecisionTreeClassifier(max_depth=3, random_state=0).fit(X_hoc, y_hoc)
print(f"Độ chính xác trên dữ liệu kiểm tra: {cay.score(X_thu, y_thu):.1%}")
# ----- Xuất cây quyết định thành hàm C (chạy được trên Arduino / ESP32, không cần thư viện AI) -----
TEN_DT = ["do_lech", "gia_tri_max", "bien_thien"]
t = cay.tree_
def sinh_c(nut, thut):
pad = " " * thut
if t.children_left[nut] == -1:
return f"{pad}return {int(np.argmax(t.value[nut]))}; // {NHAN[int(np.argmax(t.value[nut]))]}\n"
s = f"{pad}if ({TEN_DT[t.feature[nut]]} <= {t.threshold[nut]:.4f}f) {{\n"
s += sinh_c(t.children_left[nut], thut + 1)
s += f"{pad}}} else {{\n" + sinh_c(t.children_right[nut], thut + 1) + f"{pad}}}\n"
return s
print("\n// ===== Dán vào sketch ESP32 =====")
print("// 0 = đứng yên, 1 = đi bộ, 2 = chạy, 3 = NGÃ")
print("int nhanDangHoatDong(float do_lech, float gia_tri_max, float bien_thien) {")
print(sinh_c(0, 1), end="")
print("}")
print(f"// Kích thước mô hình: {t.node_count} nút – chỉ vài trăm byte Flash")
python 10-du-an-tinyml.pyĐộ chính xác trên dữ liệu kiểm tra: 100.0%
// ===== Dán vào sketch ESP32 =====
// 0 = đứng yên, 1 = đi bộ, 2 = chạy, 3 = NGÃ
int nhanDangHoatDong(float do_lech, float gia_tri_max, float bien_thien) {
if (gia_tri_max <= 1.6107f) {
if (bien_thien <= 0.0414f) {
return 0; // dung_yen
} else {
return 1; // di_bo
}
} else {
if (do_lech <= 0.3927f) {
return 2; // chay
} else {
return 3; // nga
}
}
}
// Kích thước mô hình: 7 nút – chỉ vài trăm byte FlashGiải thích
- Dữ liệu ở đây được giả lập cho dễ chạy; dự án thật cần thu dữ liệu từ MPU6050 với nhiều người, nhiều tư thế (xem khóa Arduino & ESP32 bài cảm biến và Serial).
- Hàm sinh code duyệt cây đệ quy: nút trong →
if/else, lá →return nhãn. - Mô hình 7 nút: vài trăm byte Flash, chạy trong vài micro giây — ESP32 có thể kiểm tra 50 lần/giây mà không tốn sức.
Trên ESP32, mỗi giây tính 3 đặc trưng từ bộ đệm 50 mẫu rồi gọi hàm vừa sinh:
float buf[50]; // |a| đọc từ MPU6050 mỗi 20 ms
void kiemTra() {
float tb = 0, mx = 0, bt = 0, dl = 0;
for (int i = 0; i < 50; i++) { tb += buf[i]; mx = max(mx, buf[i]); if (i) bt += fabs(buf[i] - buf[i - 1]); }
tb /= 50; bt /= 49;
for (int i = 0; i < 50; i++) dl += (buf[i] - tb) * (buf[i] - tb);
dl = sqrt(dl / 50);
if (nhanDangHoatDong(dl, mx, bt) == 3) guiCanhBao(); // còi + MQTT tới điện thoại
}4Công cụ cho dự án lớn hơn
| Công cụ | Dùng khi |
|---|---|
| Edge Impulse | nền tảng web: thu dữ liệu trực tiếp từ board, huấn luyện, xuất thư viện Arduino – phù hợp nhất cho học sinh |
| TensorFlow Lite Micro | chạy mạng nơ-ron (kể cả CNN nhỏ) trên ESP32, Arduino Nano 33 BLE |
| micromlgen / m2cgen | chuyển mô hình scikit-learn (cây, SVM, random forest) thành code C |
| ESP32-CAM + ESP-WHO | nhận diện khuôn mặt, phát hiện người |
✓Tóm tắt
- Học máy = tìm tham số mô hình để giảm hàm mất mát trên dữ liệu; đánh giá trên dữ liệu chưa thấy.
- Quy trình: dữ liệu → làm sạch, chuẩn hoá → chọn mô hình → huấn luyện → đánh giá (precision/recall, quá khớp) → triển khai.
- Mô hình nhỏ (cây, mạng nơ-ron nhỏ) có thể chạy ngay trên ESP32 — kết hợp AI với hệ nhúng là hướng rất mạnh cho các cuộc thi KHKT, STEM.