Giáo trình tóm gọn · Đọc hiểu, không cần gõ code

Python từ cơ bản đến nâng cao
& nền tảng AI / Machine Learning

Mục tiêu: nắm toàn bộ khái niệm và quy tắc — biến, hàm, bộ nhớ, threads, async, typing, testing, packaging — rồi đi tiếp tới ML/DL/LLM. Mỗi chương gồm: ý cốt lõi kèm một ví dụ code ngắn để nhìn tận mắt, quy tắc phải nhớ, và sơ đồ minh hoạ cho khái niệm trừu tượng.

18 chương 3 phần: Cơ bản · Nâng cao · AI/ML Trọng tâm: hiểu cơ chế, không thuộc cú pháp Đọc xuôi 1 lượt ≈ 60–90 phút
Phần A · Chương 1–5

Python cơ bản

Nền móng để đọc hiểu mọi chương trình Python.

  • Biến, kiểu dữ liệu & bộ nhớ
  • Điều khiển & cấu trúc dữ liệu
  • Hàm & phạm vi
  • Lập trình hướng đối tượng
  • Module & hệ sinh thái
Phần B · Chương 6–12

Python nâng cao

Cơ chế bên trong và kỹ thuật của dân chuyên nghiệp.

  • Bộ nhớ sâu: refcount, GC
  • Generator, decorator, context manager
  • Threads · Process · Async & GIL
  • Typing, testing, packaging
  • Hiệu năng & quy tắc vàng
Phần C · Chương 13–18

AI / Machine Learning

Từ dữ liệu tới mô hình, tới LLM hiện đại.

  • Toán & dữ liệu cho ML
  • Pipeline ML, overfitting
  • Các họ thuật toán
  • Deep learning & huấn luyện
  • Transformer, LLM, RAG
  • Quy tắc thực hành & MLOps
Phần A

Python cơ bản

Năm chương nền móng. Nắm chắc phần này là đọc hiểu được 90% code Python ngoài đời.

1

Biến, kiểu dữ liệu & mô hình bộ nhớ

Khái niệm quan trọng nhất của Python: biến là cái nhãn, không phải cái hộp.

Ý cốt lõi

  • Mọi thứ trong Python đều là đối tượng (object) sống trên vùng nhớ heap. Mỗi đối tượng có 3 thứ: định danh (id — địa chỉ), kiểu (type) và giá trị (value).
  • Biến chỉ là một cái tên (nhãn) trỏ tới đối tượng — gọi là tham chiếu (reference). Gán biến này cho biến kia = hai nhãn cùng dán lên một đối tượng, không hề sao chép dữ liệu.
  • Python là kiểu động (dynamic typing): kiểu thuộc về đối tượng, không thuộc về biến. Cùng một tên có thể trỏ sang số, rồi sang chuỗi, bất cứ lúc nào.
  • Phân biệt sống còn: mutable (thay đổi được tại chỗ: list, dict, set) và immutable (không đổi được: int, float, str, tuple). "Thay đổi" một immutable thực ra là tạo đối tượng mới rồi dán lại nhãn.
Ví dụ · Hai nhãn, một đối tượng
a = [1, 2, 3]
b = a              # thêm MỘT NHÃN lên cùng list — không hề sao chép dữ liệu
b.append(4)
print(a)           # [1, 2, 3, 4] — sửa qua b mà a cũng đổi, vì là cùng một đối tượng

c = a.copy()       # muốn độc lập phải copy chủ động
print(a == c)      # True  — == so sánh GIÁ TRỊ
print(a is c)      # False — is kiểm tra có phải CÙNG một đối tượng không
Ví dụ · Phân biệt các biến: nhìn kiểu, không nhìn "mặt"
x = 42
y = "42"                      # trông giống hệt nhưng là HAI KIỂU khác nhau

print(type(x), type(y))       # int và str — kiểu nằm ở ĐỐI TƯỢNG, không nằm ở tên biến
print(x + 1)                  # 43   — cộng số
print(y + "1")                # "421" — nối chuỗi, KHÔNG phải 43

isinstance(x, int)            # True  — cách kiểm tra kiểu an toàn (tính cả kế thừa)
x is y                        # False — khác đối tượng; x == y cũng False — khác cả giá trị lẫn kiểu

x = "bốn mươi hai"            # cùng TÊN biến, gán lại = đổi sang đối tượng khác kiểu hẳn
print(type(x))                # str — bằng chứng của kiểu động: tên không "giữ" kiểu nào cả
TÊN (namespace) ĐỐI TƯỢNG (heap) a b c list · [1, 2, 3] mutable — sửa được tại chỗ int · 42 immutable — không sửa được a và b cùng trỏ một list → sửa qua a thì b cũng "thấy"
Mô hình bộ nhớ: tên biến là nhãn tham chiếu. Gán b = a chỉ thêm một nhãn lên cùng đối tượng — nguồn gốc của hầu hết bug "sửa list này mà list kia cũng đổi".

Các kiểu dữ liệu gốc

NhómKiểuĐặc điểm cần nhớ
Sốint · float · complex · boolint có độ lớn không giới hạn; float là số thực 64-bit nên có sai số (0.1+0.2 ≠ 0.3); bool là con của int (True = 1)
Văn bảnstrImmutable, là dãy ký tự Unicode; mọi phép "sửa chuỗi" đều tạo chuỗi mới
Dãy có thứ tựlist · tuple · rangelist mutable; tuple immutable (an toàn, nhanh hơn, dùng làm key dict được); range lười — không tạo sẵn dãy số
Tập hợpset · frozensetKhông trùng lặp, không thứ tự; kiểm tra "có thuộc" cực nhanh; dùng để khử trùng lặp, giao/hợp tập
Ánh xạdictCặp key→value; key phải immutable; giữ thứ tự chèn từ Python 3.7; tra cứu gần như O(1)
Rỗng / nhị phânNone · bytesNone là đối tượng duy nhất biểu thị "không có gì" — so sánh bằng is None, không dùng ==
Quy tắc phải nhớ
  • == so sánh giá trị; is so sánh cùng một đối tượng. Chỉ dùng is với None và các hằng duy nhất.
  • Gán không sao chép. Muốn bản sao độc lập phải copy chủ động (copy nông shallow chỉ sao lớp ngoài; copy sâu deep sao toàn bộ) — chi tiết ở Chương 6.
  • Đặt tên: snake_case cho biến/hàm, HẰNG_SỐ_VIẾT_HOA cho hằng, tên phải nói lên ý nghĩa — cấm x, tmp, data2 vô nghĩa trong code thật.
  • Số tiền và số đo chính xác: tránh float, dùng int (đơn vị nhỏ nhất) hoặc kiểu thập phân chính xác Decimal.
2

Cấu trúc điều khiển & cấu trúc dữ liệu

Luồng chạy của chương trình và 4 "thùng chứa" dùng suốt đời.

Ý cốt lõi

  • Thụt lề là cú pháp. Python dùng khoảng trắng để chia khối lệnh — thụt sai là sai chương trình. Chuẩn: 4 dấu cách, không trộn tab.
  • Rẽ nhánh: if / elif / else; so khớp mẫu match (3.10+) cho nhiều trường hợp theo cấu trúc.
  • Vòng lặp: for duyệt qua một dãy/đối tượng duyệt được (iterable); while lặp theo điều kiện. break thoát vòng, continue bỏ qua lượt này, else của vòng lặp chạy khi vòng kết thúc tự nhiên (không bị break).
  • Truthiness: mọi đối tượng đều có giá trị đúng/sai ngầm — 0, chuỗi rỗng, list rỗng, None đều là falsy. Nhờ vậy điều kiện viết gọn theo "có dữ liệu hay không".
  • Comprehension — cách viết gọn để biến đổi/lọc một dãy thành list/dict/set mới trong một dòng biểu thức. Là "khẩu vị" đặc trưng của Python: thay vòng lặp + append dài dòng.
  • Xử lý lỗi bằng try / except / else / finally: tách luồng bình thường khỏi luồng lỗi; finally luôn chạy (để dọn dẹp tài nguyên).
Ví dụ · Comprehension & tra cứu dict
# Cách vòng lặp tường minh
squares = []
for n in range(10):
    if n % 2 == 0:
        squares.append(n * n)

# Comprehension — cùng kết quả, đọc như một câu mô tả
squares = [n * n for n in range(10) if n % 2 == 0]

# Tra cứu bằng dict: O(1), thay cả chuỗi if/elif dài
menu = {"pho": 50_000, "com": 40_000}
total = menu.get("bun", 0)   # .get có giá trị mặc định → không văng lỗi KeyError khi thiếu món

Chọn cấu trúc dữ liệu theo việc

Nhu cầuChọnLý do
Dãy thay đổi, thêm/bớt cuốilistThêm cuối O(1); chèn/xoá giữa là O(n) vì phải dồn phần tử
Dữ liệu cố định, bản ghitupleImmutable → an toàn, dùng làm key được, tốn ít nhớ hơn list
Tra cứu theo khoádictHash map: tra/thêm/xoá trung bình O(1)
Kiểm tra tồn tại, khử trùng lặpsetTra cứu O(1); list kiểm tra tồn tại là O(n) — chậm hẳn với dữ liệu lớn
Hàng đợi hai đầudequeThêm/bớt cả hai đầu đều O(1); list.pop(0) là O(n)
Hàng đợi ưu tiên / top-KheapqHeap nhị phân: lấy phần tử nhỏ nhất O(log n)
key "phở" hash() ô số 3 50k 012345 dict: hash(key) cho ngay địa chỉ ô — 1 bước dù có 1 triệu khoá · list: duyệt từng phần tử tới khi gặp — O(n)
Vì sao dict/set tra cứu O(1): khoá được hàm băm biến thành địa chỉ ô, nhảy thẳng tới nơi chứa giá trị thay vì lục tung từng phần tử như list.
Quy tắc phải nhớ
  • Thuật toán tốt bắt đầu từ chọn đúng cấu trúc dữ liệu — khác biệt O(1) và O(n) quyết định chương trình chạy nổi hay không khi dữ liệu lớn.
  • Không sửa một list/dict trong lúc đang duyệt chính nó — duyệt trên bản sao hoặc tạo list mới bằng comprehension.
  • Bắt lỗi cụ thể (đúng loại exception), không bắt "trần" mọi lỗi — bắt hết sẽ nuốt cả bug thật và làm chương trình lỗi âm thầm.
  • Ưu tiên phong cách EAFP của Python: "cứ làm, lỗi thì bắt" thay vì kiểm tra trước mọi thứ (LBYL) — code gọn và tránh race condition.
3

Hàm & phạm vi (scope)

Hàm là đơn vị tổ chức code số một; scope quyết định tên nào "nhìn thấy" ở đâu.

Ý cốt lõi

  • Hàm trong Python là đối tượng hạng nhất (first-class): gán cho biến được, truyền làm tham số được, trả về từ hàm khác được, nhét vào list/dict được. Đây là nền của callback, decorator và lập trình hàm.
  • Tham số truyền theo cơ chế "truyền tham chiếu của đối tượng" (pass by object reference): hàm nhận cùng đối tượng với bên ngoài — sửa mutable bên trong hàm thì bên ngoài thấy; gán lại tên thì không.
  • Các loại tham số: vị trí, theo tên (keyword), giá trị mặc định, *args (gom số lượng tuỳ ý theo vị trí), **kwargs (gom theo tên), và ép buộc keyword-only / positional-only để API rõ ràng.
  • Scope LEGB — thứ tự tìm tên khi dùng một biến: Local (trong hàm) → Enclosing (hàm bao ngoài) → Global (cấp module) → Built-in (hàm sẵn có). Không thấy ở cả 4 nơi → lỗi NameError.
  • Closure: hàm con "nhớ" được biến của hàm cha ngay cả khi hàm cha đã chạy xong — cơ chế đứng sau decorator và callback giữ trạng thái.
Ví dụ · Cái bẫy giá trị mặc định mutable
def add(item, box=[]):   # BUG KINH ĐIỂN: giá trị mặc định chỉ tạo MỘT LẦN
    box.append(item)      # rồi dùng chung cho mọi lần gọi sau
    return box

add("a")   # ['a']
add("b")   # ['a', 'b'] — dữ liệu rò rỉ giữa các lần gọi!

def add(item, box=None):  # cách đúng: mặc định None, tạo mới bên trong
    if box is None:
        box = []
    box.append(item)
    return box
Ví dụ · Closure & LEGB: hàm con "nhớ" biến của hàm cha
def make_counter():
    count = 0                       # biến ENCLOSING — thuộc hàm cha, không phải global

    def step():                     # hàm con
        nonlocal count              # khai báo: tôi sửa biến của hàm CHA (tầng E trong LEGB)
        count += 1
        return count

    return step                     # trả hàm con ra ngoài

c = make_counter()
c()   # 1
c()   # 2 — hàm cha ĐÃ chạy xong từ lâu, nhưng biến count vẫn "sống"
      # trong closure của step. Mỗi lần make_counter() là một count độc lập riêng.
B — Built-in: print, len, range… (có sẵn) G — Global: biến cấp module (toàn file) E — Enclosing: biến của hàm bao ngoài (nơi sinh closure) L — Local: biến trong hàm hiện tại — tìm ở đây trước tiên
LEGB: khi gặp một tên, Python tìm từ trong ra ngoài L → E → G → B. Tìm thấy ở tầng nào thì dùng tầng đó ("che bóng" — shadowing — tầng ngoài).
make_counter() đã chạy xong count = 2 biến enclosing closure giữ c1 = make_counter() đếm riêng: 1, 2, 3… c2 = make_counter() đếm riêng: 1, 2… mỗi lần gọi hàm cha là một count độc lập — hai bộ đếm không đụng nhau
Closure: hàm cha kết thúc, khung biến của nó lẽ ra bị huỷ — nhưng hàm con còn giữ tham chiếu nên biến enclosing vẫn sống, và mỗi lần gọi hàm cha sinh một "ô nhớ riêng" độc lập.
Quy tắc phải nhớ
  • Không bao giờ dùng mutable làm giá trị mặc định của tham số (ví dụ mặc định là list rỗng) — giá trị mặc định chỉ tạo một lần và dùng chung cho mọi lần gọi, gây bug "dữ liệu rò rỉ giữa các lần gọi". Dùng mặc định None rồi tạo mới bên trong.
  • Hàm tốt: một việc, tên là động từ, ngắn, ít tham số (nhiều quá thì gom vào một đối tượng cấu hình/dataclass).
  • Ưu tiên hàm thuần (pure): cùng đầu vào → cùng đầu ra, không tác dụng phụ. Hàm thuần dễ test, dễ song song hoá, dễ suy luận.
  • Hạn chế tối đa global và nonlocal: sửa trạng thái toàn cục làm code khó lường — truyền tham số và trả kết quả là đường sạch.
  • Mỗi hàm công khai nên có docstring: một đoạn mô tả việc nó làm, tham số, giá trị trả về — đây là tài liệu sống của code.
4

OOP — Class, đối tượng & kế thừa

Đóng gói dữ liệu + hành vi vào một đơn vị có ranh giới rõ ràng.

Ý cốt lõi

  • Class là bản thiết kế; instance (thể hiện/đối tượng) là sản phẩm theo bản thiết kế đó. Thuộc tính của instance nằm trong từ điển riêng của từng đối tượng; thuộc tính của class dùng chung cho mọi instance.
  • Phương thức đặc biệt (dunder, hai gạch dưới) để đối tượng "hoà nhập" với ngôn ngữ: khởi tạo, biểu diễn chuỗi, so sánh, độ dài, toán tử, duyệt, gọi như hàm… Biết đọc dunder là đọc được ý đồ của mọi thư viện.
  • Kế thừa (inheritance): class con hưởng lại và mở rộng class cha. Thứ tự tìm phương thức khi có đa kế thừa gọi là MRO — Python tính bằng thuật toán C3, quyết định "bản nào thắng".
  • Đa hình (polymorphism) kiểu Python là duck typing: không quan trọng đối tượng "là gì", chỉ cần "làm được việc gì" (có đúng phương thức cần gọi). "Nếu nó đi như vịt, kêu như vịt…"
  • Đóng gói trong Python dựa trên quy ước, không cưỡng chế: tên bắt đầu bằng một gạch dưới = "nội bộ, đừng đụng"; hai gạch dưới = name mangling (đổi tên tránh trùng khi kế thừa).
  • Dataclass: sinh tự động các phương thức nhàm chán (khởi tạo, so sánh, in) cho class chủ yếu chứa dữ liệu — chuẩn hiện đại thay cho class viết tay dài dòng. Họ hàng: NamedTuple (bản ghi immutable), Enum (tập giá trị cố định có tên).
Ví dụ · Dataclass
from dataclasses import dataclass

@dataclass
class Robot:                     # dataclass tự sinh __init__, __repr__, __eq__
    name: str
    battery: float = 100.0       # thuộc tính có giá trị mặc định

    def move(self):              # phương thức: hành vi sống ở class, dùng chung
        return f"{self.name} đang di chuyển với pin {self.battery}%"

r = Robot("PuPu")                # mỗi instance giữ DỮ LIỆU RIÊNG (name, battery)
print(r.move())
CLASS «Robot» phương thức · thuộc tính chung instance · robot A dữ liệu riêng của A instance · robot B dữ liệu riêng của B instance · robot C dữ liệu riêng của C
Class giữ hành vi chung; mỗi instance giữ dữ liệu riêng. Gọi phương thức trên instance: Python tìm trong instance trước, không có mới leo lên class (rồi lên class cha theo MRO).
Quy tắc phải nhớ
  • Ưu tiên kết hợp (composition) hơn kế thừa: "có một cái" (has-a) linh hoạt hơn "là một cái" (is-a). Kế thừa sâu nhiều tầng là nguồn gốc của code khó sửa.
  • Kế thừa chỉ dùng khi quan hệ "là một" thật sự đúng và cần đa hình; còn lại dùng composition, hàm rời, hoặc Protocol.
  • Dữ liệu thuần tuý → dataclass/NamedTuple; hành vi là chính → class thường. Đừng biến mọi thứ thành class khi một hàm là đủ.
  • Nguyên lý SOLID áp dụng được cho Python: một trách nhiệm cho mỗi class, mở để mở rộng/đóng để sửa, thay thế được class cha bằng class con… (nhẹ nhàng theo duck typing, không cứng nhắc kiểu Java).
5

Module, import & hệ sinh thái thư viện

Cách Python tổ chức code lớn và "pin" của nó: thư viện chuẩn khổng lồ.

Ý cốt lõi

  • Module = một file .py; package = một thư mục các module. Import một module = chạy file đó một lần rồi ghi vào bộ nhớ đệm (sys.modules) — import lại lần hai không chạy lại.
  • Đoạn khởi động if __name__ == "__main__": phân biệt "file được chạy trực tiếp" với "file được import" — code để trong khối này chỉ chạy khi chạy trực tiếp.
  • Thứ tự tìm module khi import: thư mục hiện hành → các đường dẫn trong biến môi trường PYTHONPATH → thư viện chuẩn → site-packages (nơi pip cài). Hiểu thứ tự này là hiểu mọi lỗi "ModuleNotFoundError" và lỗi "import nhầm file trùng tên".
  • Import tuyệt đối (đường dẫn đầy đủ từ gốc package) rõ ràng hơn import tương đối (dấu chấm) — chuẩn cộng đồng ưu tiên tuyệt đối.
  • Thư viện chuẩn ("batteries included") cần biết mặt: xử lý file/đường dẫn (pathlib), thời gian (datetime), JSON, biểu thức chính quy (re), toán (math/statistics), bộ sưu tập nâng cao (collections), chạy lệnh hệ thống (subprocess), mạng (http/urllib), nén, log (logging)…
Ví dụ · Module & khối __main__
# File geometry.py — một module
import math

def circle_area(r):
    return math.pi * r * r

if __name__ == "__main__":
    # Khối này CHỈ chạy khi chạy trực tiếp:  python geometry.py
    # Khi file khác "import geometry", __name__ là "geometry" → khối này bị bỏ qua
    print(circle_area(2))
Quy tắc phải nhớ
  • Không đặt tên file trùng tên thư viện chuẩn (ví dụ math.py, email.py) — sẽ "che" thư viện thật và sinh lỗi khó hiểu.
  • Import ở đầu file, gom nhóm: chuẩn → bên thứ ba → nội bộ; không import "dấu sao" (*) vì làm mờ nguồn gốc tên.
  • Tránh import vòng (circular import): A import B, B import A. Khắc phục bằng tách phần chung ra module thứ ba hoặc import lười bên trong hàm.
  • Trước khi cài thư viện ngoài, kiểm tra thư viện chuẩn có sẵn chưa — và khi cài, luôn ở trong môi trường ảo (Chương 11).
Phần B

Python nâng cao

Đi vào cơ chế bên trong: bộ nhớ thật sự chạy thế nào, GIL là gì, async hoạt động ra sao, và bộ kỹ năng nghề nghiệp — typing, testing, packaging.

6

Bộ nhớ sâu: reference counting, GC, copy

Khi nào một đối tượng bị huỷ, ai dọn rác, và vì sao chương trình "ngốn RAM".

Ý cốt lõi

  • Mỗi đối tượng có một bộ đếm tham chiếu (refcount): có bao nhiêu tên/biến/container đang trỏ tới nó. Đếm về 0 → đối tượng bị huỷ ngay lập tức và trả bộ nhớ. Đây là cơ chế dọn rác chính của CPython.
  • Điểm mù của refcount: vòng tham chiếu (A trỏ B, B trỏ A) — đếm không bao giờ về 0 dù không ai dùng nữa. GC thế hệ (generational garbage collector) chạy định kỳ để phát hiện và dọn các vòng này. Vật chứa được chia 3 "thế hệ": càng sống lâu càng ít bị quét — vì đa số đối tượng chết trẻ.
  • Interning: số nguyên nhỏ (−5…256) và chuỗi ngắn phổ biến được tạo sẵn và dùng chung — vì vậy is đôi khi "đúng tình cờ" với số nhỏ/chuỗi ngắn. Không bao giờ dựa vào điều đó.
  • Copy: shallow copy sao lớp vỏ ngoài, các đối tượng con bên trong vẫn dùng chung; deep copy sao đệ quy toàn bộ cây đối tượng (chậm, nặng). Mặc định của gán là không copy gì cả.
  • Weak reference (tham chiếu yếu): trỏ tới đối tượng mà không tăng refcount — dùng cho cache, tránh vòng tham chiếu, tránh giữ đối tượng sống quá lâu.
  • Call stack vs heap: mỗi lần gọi hàm tạo một frame trên stack chứa biến cục bộ (là các tham chiếu); đối tượng thật nằm trên heap. Đệ quy sâu → tràn stack (RecursionError, giới hạn mặc định ~1000).
Ví dụ · Refcount & hai kiểu copy
import sys, copy

a = [1, 2, 3]
sys.getrefcount(a)   # có bao nhiêu nhãn/container đang trỏ tới list này
b = a                # refcount +1 — vẫn là MỘT list duy nhất
del b                # refcount −1; khi về 0, đối tượng bị huỷ ngay

shallow = copy.copy(a)      # shallow: sao lớp vỏ ngoài, phần tử con vẫn dùng chung
deep = copy.deepcopy(a)     # deep: sao đệ quy toàn bộ cây đối tượng
ĐẾM THAM CHIẾU (refcount) đối tượng X refcount = 2 biến a list chứa X Mất 1 tham chiếu → refcount 1 (còn sống). Mất nốt → refcount 0 → huỷ ngay, trả RAM. VÒNG THAM CHIẾU A · rc=1 B · rc=1 Không ai bên ngoài trỏ tới A/B nữa, nhưng rc không bao giờ về 0 → chỉ GC thế hệ mới dọn được.
Hai cơ chế dọn rác: refcount xử lý 99% trường hợp ngay tức thì; GC thế hệ quét định kỳ để xử lý vòng tham chiếu.
Quy tắc phải nhớ
  • Rò rỉ bộ nhớ trong Python thường không phải do quên "free" mà do giữ tham chiếu quá lâu: biến toàn cục, cache không giới hạn, list cứ append không xoá.
  • Đóng tài nguyên xác định (file, kết nối) bằng context manager — đừng trông chờ GC dọn đúng lúc.
  • Khi nghi ngờ tốn RAM: đo trước bằng công cụ profiling bộ nhớ; thủ phạm thường là dữ liệu nạp thừa, không phải "Python chậm".
7

Iterator, Generator, Decorator, Context Manager

Bốn cơ chế "ma thuật có kiểm soát" làm nên phong cách Python nâng cao.

Ý cốt lõi

  • Iterator: giao thức "lấy phần tử tiếp theo" — mọi vòng for thực chất gọi lặp lại next() cho đến khi nhận tín hiệu hết (StopIteration). Bất cứ thứ gì tuân thủ giao thức này đều duyệt được.
  • Generator: hàm dùng yield — không chạy một mạch mà đóng băng tại mỗi yield, trả một giá trị rồi chờ được gọi tiếp, giữ nguyên trạng thái cục bộ. Nhờ vậy tạo ra dãy dữ liệu "lười": sinh từng phần tử khi cần, tốn RAM hằng số dù dãy dài vô hạn.
  • Generator expression và nguyên lý lazy evaluation: chỉ tính khi bị ép (bị duyệt/hỏi len/chuyển thành list). Chuỗi xử lý lười = pipeline dữ liệu lớn không tràn RAM.
  • Decorator: một hàm nhận một hàm và trả về một hàm (thường là hàm bọc có thêm hành vi: đo thời gian, log, kiểm tra quyền, cache, retry). Cách áp dụng bằng ký hiệu @ chỉ là đường tắt của việc gán lại tên hàm qua hàm bọc. Decorator có tham số = một lớp bọc nữa bên ngoài.
  • Context manager (câu lệnh with): giao thức "vào thì mở — ra thì chắc chắn đóng/dọn", kể cả khi có lỗi giữa chừng. Nền tảng cho mở file, khoá thread, giao dịch CSDL, đo thời gian…
  • Descriptor & property: cơ chế phía sau thuộc tính "biết tự tính toán/tự kiểm tra" khi được truy cập; metaclass là tầng sâu hơn nữa (class của class) — biết tồn tại, hiếm khi cần tự viết.
Ví dụ · Generator + with
def big_lines(path):
    with open(path) as f:      # with: đóng file chắc chắn, kể cả khi có lỗi
        for line in f:
            yield line.strip() # yield: đóng băng tại đây, giữ nguyên trạng thái,
                               # chỉ sinh 1 dòng khi được gọi tiếp → RAM hằng số

for line in big_lines("server.log"):   # duyệt file vài GB cũng không tràn RAM
    ...
Ví dụ · Decorator đo thời gian
import functools, time

def timing(fn):                          # decorator = hàm nhận một hàm, trả về một hàm
    @functools.wraps(fn)                 # giữ nguyên tên + docstring của hàm gốc
    def wrapper(*args, **kwargs):
        t0 = time.perf_counter()
        result = fn(*args, **kwargs)     # chạy hàm gốc
        print(fn.__name__, "mất", round(time.perf_counter() - t0, 3), "giây")
        return result
    return wrapper

@timing                                  # tương đương: process = timing(process)
def process():
    ...
một hàm yield qua ba thời điểm — trạng thái đóng băng giữa hai lần gọi next(g) next(g) g = đếm() chỉ tạo object code CHƯA chạy chạy → yield 1 ĐÓNG BĂNG tại yield trạng thái giữ nguyên chạy tiếp → yield 2 tiếp đúng chỗ đã dừng hết → StopIteration list: tạo TẤT CẢ ngay — RAM tỉ lệ với n n phần tử = n ô nhớ generator: sinh TỪNG cái — RAM hằng số 1 phần tử tại một thời điểm
Generator không chạy một mạch: mỗi yield là một điểm đóng băng — biến cục bộ, vị trí đang chạy đều được giữ nguyên cho lần next() sau. Đó là cơ chế của tính "lười".
gọi process() wrapper — lớp bọc của @timing ① trước: ghi thời điểm bắt đầu process() gốc ② sau: in thời gian đã chạy kết quả + log thời gian tên process giờ trỏ tới wrapper — người gọi không hề biết có lớp bọc
Decorator là "bánh mì kẹp": hành vi cộng thêm (đo giờ, log, kiểm tra quyền…) bọc ngoài, hàm gốc nằm giữa nguyên vẹn. @timing chỉ là đường tắt của process = timing(process).
Quy tắc phải nhớ
  • Dữ liệu lớn/stream → nghĩ tới generator trước tiên; chỉ materialise thành list khi thật sự cần toàn bộ cùng lúc (sắp xếp, len, duyệt nhiều lần).
  • Generator chỉ duyệt được một lần — duyệt xong là rỗng. Đây là nguồn bug kinh điển "lần chạy thứ hai không có dữ liệu".
  • Decorator phải giữ nguyên chữ ký/tên hàm gốc (dùng functools.wraps) và không giấu tác dụng phụ nặng — người đọc phải đoán được decorator làm gì từ tên của nó.
  • Mọi tài nguyên có "mở" thì phải có "with": file, socket, kết nối DB, lock. Không có ngoại lệ.
8

Đồng thời: Threads · Multiprocessing · Async & GIL

Chương quan trọng nhất của phần nâng cao — và là nơi nhiều người hiểu sai nhất.

Phân biệt 3 khái niệm hay bị trộn

  • Concurrency (đồng thời): nhiều việc tiến triển xen kẽ trong cùng khoảng thời gian — không nhất thiết chạy cùng một tích tắc. Mục tiêu: không để thời gian chờ bị lãng phí.
  • Parallelism (song song): nhiều việc chạy đúng cùng lúc trên nhiều nhân CPU. Mục tiêu: tính nhanh hơn.
  • Phân loại việc: CPU-bound (giới hạn bởi tính toán: nén ảnh, train model) và I/O-bound (giới hạn bởi chờ: mạng, ổ đĩa, API). Chọn sai công cụ cho loại việc là sai lầm số một.

GIL — "ổ khoá toàn cục" của CPython

  • GIL (Global Interpreter Lock) là một ổ khoá duy nhất: tại một thời điểm, chỉ một thread được chạy bytecode Python trong một tiến trình. Các thread thay phiên nhau giữ khoá (chuyển đổi mỗi ~5ms hoặc khi thread chờ I/O).
  • Hậu quả: thread không tăng tốc việc CPU-bound bằng Python thuần (thậm chí chậm hơn vì tranh khoá), nhưng rất hiệu quả cho I/O-bound vì khi một thread chờ mạng/đĩa, nó nhả khoá cho thread khác chạy.
  • Ngoại lệ: thư viện viết bằng C như NumPy nhả GIL khi tính toán nặng → thread vẫn song song thật được trên phần tính đó. Python 3.13+ có bản dựng free-threaded (bỏ GIL) dạng tuỳ chọn — đang trưởng thành, chưa phải mặc định.
GIL — 1 ổ khoá chỉ 1 thread giữ được CPU chạy bytecode Thread 1đang giữ khoá → chạy Thread 2đang chờ khoá Thread 3đang chờ I/O (đã nhả khoá)
GIL: các thread dùng chung một tiến trình và một ổ khoá. Thread chờ I/O thì nhả khoá (tốt); thread tính toán thì tranh khoá (không nhanh hơn).
1 PROCESS — nhiều thread 3 PROCESS — mỗi process một nhà riêng Thread 1 Thread 2 Thread 3 HEAP DÙNG CHUNG mọi thread đọc/ghi cùng dữ liệu Process 1 — RAM riêng, GIL riêng Process 2 — RAM riêng, GIL riêng Process 3 — RAM riêng, GIL riêng thread: chung một nhà — giao tiếp rẻ nhưng dễ giẫm chân nhau, phải khoá (GIL + lock) process: mỗi nhà riêng — an toàn, nhưng gửi dữ liệu qua IPC phải đóng gói (pickle) và tốn RAM sao chép
Khác biệt gốc rễ của thread và process nằm ở bộ nhớ: thread là nhiều người ở chung một nhà (nhanh, rẻ, dễ va chạm); process là mỗi người một nhà (song song thật trên nhiều nhân, nhưng mọi trao đổi đều phải "đóng gói gửi qua cửa").

Ba công cụ, ba cơ chế

Threading

Cơ chế
Nhiều thread trong 1 tiến trình, chung bộ nhớ
Song song CPU thật
Không (bị GIL) — trừ phần thư viện C nhả GIL
Hợp nhất cho
I/O-bound, số tác vụ vừa, cần chia sẻ dữ liệu
Chi phí
Nhẹ; nhưng chia sẻ bộ nhớ → phải khoá chống race condition
Rủi ro điển hình
Race condition, deadlock

Multiprocessing

Cơ chế
Nhiều tiến trình, mỗi tiến trình bộ nhớ riêng + GIL riêng
Song song CPU thật
Có — dùng hết các nhân
Hợp nhất cho
CPU-bound nặng
Chi phí
Nặng (mỗi tiến trình một bản sao); dữ liệu qua lại phải tuần tự hoá (pickle) qua queue/pipe
Rủi ro điển hình
Tốn RAM, chi phí truyền dữ liệu, debug khó

Asyncio

Cơ chế
1 thread, 1 tiến trình; các tác vụ hợp tác nhường nhau tại điểm chờ
Song song CPU thật
Không
Hợp nhất cho
I/O-bound số lượng rất lớn (nghìn kết nối)
Chi phí
Nhẹ nhất; nhưng "lây màu": hàm async chỉ gọi được từ async, cả chuỗi thư viện phải hỗ trợ async
Rủi ro điển hình
Một tác vụ blocking sẽ treo cả vòng lặp; quên await = việc không chạy
Tính toán (CPU) Chờ I/O (mạng/đĩa) Đứng chờ

Vòng đời async — event loop chạy thế nào

  • Coroutine là hàm "tạm dừng được": gọi hàm async không chạy ngay mà tạo ra một đối tượng coroutine; nó chỉ chạy khi được await hoặc được giao cho event loop.
  • Event loop là người điều phối duy nhất: chạy một coroutine tới điểm await (chờ I/O) thì treo nó lại, chuyển sang coroutine khác đang sẵn sàng; khi I/O xong, I/O đánh thức coroutine cũ quay lại hàng đợi.
  • Toàn bộ diễn ra trên một thread: không tranh chấp bộ nhớ, không cần lock — đổi lại, một đoạn tính toán dài không có await sẽ "độc chiếm" vòng lặp và đóng băng mọi tác vụ khác.
EVENT LOOP 1 thread · quay vòng liên tục Coroutine A chạy → gặp await → treo Hàng đợi sẵn sàng B, C đang chờ tới lượt I/O hoàn tất mạng/đĩa xong → gọi A dậy Task / Future hộp chứa kết quả sẽ có
Vòng đời async: loop chạy coroutine đến điểm await → treo và chuyển việc khác → I/O xong thì đưa coroutine về hàng đợi sẵn sàng → chạy tiếp đúng chỗ đã dừng.

Đồng bộ hoá & lỗi kinh điển

  • Race condition: kết quả phụ thuộc thứ tự chạy không kiểm soát của các thread khi cùng sửa dữ liệu chung. Phòng bằng Lock (một thread vào vùng tới hạn tại một thời điểm), RLock, Semaphore (giới hạn số lượng), Event/Condition (chờ tín hiệu).
  • Deadlock: các thread chờ khoá của nhau thành vòng tròn — không ai nhả, tất cả treo vĩnh viễn. Phòng: luôn lấy nhiều khoá theo cùng một thứ tự cố định, giữ khoá càng ngắn càng tốt.
  • Queue là cách chia sẻ an toàn được ưa chuộng nhất giữa thread/process: mô hình nhà sản xuất → hàng đợi → người tiêu thụ, thay vì cùng sờ vào một biến.
  • API mức cao nên dùng trước: ThreadPoolExecutor / ProcessPoolExecutor (quản lý sẵn một bể thread/process, giao việc và nhận kết quả dạng Future) thay vì tự tạo/quản thread thủ công.
Ví dụ · Thread pool cho việc I/O
from concurrent.futures import ThreadPoolExecutor
import requests

urls = ["https://example.com"] * 20     # 20 việc chủ yếu là CHỜ mạng (I/O-bound)

def page_size(url):
    return len(requests.get(url).content)

with ThreadPoolExecutor(max_workers=10) as pool:
    sizes = list(pool.map(page_size, urls))
# Khi một thread chờ mạng, nó NHẢ GIL cho thread khác chạy → tổng thời gian rút mạnh.
# Nếu thay bằng tính toán thuần Python, thread KHÔNG nhanh hơn tuần tự vì GIL.
Ví dụ · Process pool cho việc CPU
from concurrent.futures import ProcessPoolExecutor

def heavy(n):
    return sum(i * i for i in range(n))   # tính toán THUẦN Python — việc CPU-bound

if __name__ == "__main__":                 # bắt buộc khi spawn tiến trình con (nhất là trên Windows)
    with ProcessPoolExecutor() as pool:
        results = list(pool.map(heavy, [10**7] * 4))

# Khác thread ở điểm sống còn: mỗi tiến trình có BỘ NHỚ RIÊNG + GIL RIÊNG
# → 4 việc chạy song song THẬT trên 4 nhân CPU. Đổi lại: tham số và kết quả
# phải được pickle (tuần tự hoá) để chuyển qua lại giữa các tiến trình.
Ví dụ · Asyncio: gather chạy đồng thời
import asyncio, aiohttp

async def fetch(session, url):
    async with session.get(url) as resp:   # gặp await → treo task này,
        return len(await resp.read())      # event loop chuyển sang task khác

async def main():
    async with aiohttp.ClientSession() as s:
        sizes = await asyncio.gather(*(fetch(s, u) for u in urls))  # cả loạt chạy đồng thời

asyncio.run(main())   # quên await = coroutine không bao giờ chạy — lỗi async kinh điển
Quy tắc chọn công cụ
  • Việc chờ mạng/đĩa/API, số lượng vừa → thread. Việc chờ, số lượng cực lớn (server nhiều kết nối) → async.
  • Việc tính toán nặng bằng Python thuần → multiprocessing (hoặc đẩy xuống thư viện C/GPU như NumPy/PyTorch).
  • Trộn lẫn: chương trình thật thường kết hợp — async ở tầng mạng + process pool cho phần tính nặng.
  • Trong code async: tuyệt đối không gọi hàm blocking (đọc file lớn đồng bộ, sleep thường, thư viện mạng đồng bộ) — phải await bản async hoặc đẩy sang thread phụ.
9

Typing — khai báo kiểu tĩnh

Python vẫn chạy động, nhưng kiểu khai báo giúp người và công cụ "bắt lỗi trước khi chạy".

Ý cốt lõi

  • Type hint là chú thích kiểu cho biến/tham số/giá trị trả về. Trình thông dịch bỏ qua chúng lúc chạy (trừ vài thư viện dùng để kiểm tra dữ liệu thật); giá trị của chúng là cho: công cụ kiểm tra tĩnh (mypy, pyright), IDE gợi ý, và người đọc code.
  • Các khối xây dựng: kiểu gốc, kiểu container có tham số (list của int, dict từ str sang float…), Optional (có thể là None), Union (một trong nhiều kiểu), Literal (chỉ nhận đúng vài giá trị), Any (tắt kiểm tra — dùng càng ít càng tốt).
  • Generic (tham số kiểu): viết hàm/class dùng được cho nhiều kiểu mà vẫn giữ liên hệ kiểu giữa đầu vào–đầu ra (ví dụ: hàm nhận list[T] trả về T).
  • Protocol = "kiểu cấu trúc": một đối tượng được coi là hợp lệ nếu có đủ phương thức cần thiết, không cần kế thừa khai báo — typing hoà hợp với duck typing nhờ cơ chế này. Ngược lại, ABC (lớp cơ sở trừu tượng) là kiểu danh nghĩa: phải kế thừa mới tính.
  • TypedDict / dataclass + thư viện kiểm định (như Pydantic): khi dữ liệu đến từ bên ngoài (API, file, người dùng), typing tĩnh không đủ — cần kiểm tra thật lúc chạy và ép kiểu tại "biên" của hệ thống.
Ví dụ · Type hint & Protocol
from typing import Optional, Protocol

def find_user(user_id: int) -> Optional[dict[str, str]]:
    # Trả thông tin user — hoặc None nếu không thấy.
    # None NẰM TRONG hợp đồng kiểu → người gọi buộc phải xử lý trường hợp đó.
    ...

class Drawable(Protocol):
    # Protocol = kiểu cấu trúc: đối tượng nào CÓ method draw() đều hợp lệ,
    # không cần kế thừa khai báo — duck typing nhưng có kiểm tra tĩnh
    def draw(self) -> None: ...
Quy tắc phải nhớ
  • Gõ kiểu cho mọi hàm công khai và chữ ký API; bên trong hàm ngắn có thể để công cụ tự suy luận.
  • Chạy kiểm tra kiểu trong CI như một cổng chất lượng; tăng độ nghiêm ngặt dần ở code mới thay vì "vá" toàn bộ một lúc.
  • None phải là một phần của hợp đồng kiểu: nếu hàm có thể trả None, khai báo rõ Optional — đa số bug "NoneType has no attribute" đến từ quên điều này.
  • Tránh Any lan tràn: một Any ở đầu vào sẽ "tắt điện" kiểm tra cho cả chuỗi phía sau.
10

Testing — kiểm thử

Test không chứng minh code đúng; test khoá hành vi đúng lại để không ai phá vỡ nó sau này.

Ý cốt lõi

  • Kim tự tháp test: nhiều unit test (nhỏ, nhanh, kiểm một hàm/class độc lập) ở đáy; ít hơn integration test (các phần ghép nhau, DB/API thật); rất ít end-to-end test (toàn hệ thống như người dùng thật) ở đỉnh — vì càng lên cao càng chậm, càng dễ vỡ vặt.
  • Mỗi test theo nhịp AAA: Arrange (chuẩn bị) → Act (thực hiện) → Assert (khẳng định kết quả). Một test kiểm một hành vi; tên test đọc như một câu mô tả hành vi.
  • Fixture (pytest): bộ dựng dữ liệu/môi trường dùng chung, tự dọn dẹp sau test. Parametrize: chạy cùng một test với nhiều bộ dữ liệu — thay cho việc viết nhiều test na ná nhau.
  • Mock/double: thay thế phụ thuộc ngoài (API, DB, thời gian, ngẫu nhiên) bằng bản giả có kiểm soát để test nhanh, ổn định, không phụ thuộc mạng. Quy tắc: mock ở biên hệ thống, đừng mock chính logic cần kiểm.
  • Coverage (độ phủ) chỉ cho biết code nào chưa từng được chạy bởi test — không đo chất lượng khẳng định. Phủ 100% vẫn có thể đầy bug.
  • TDD (viết test trước): đỏ → xanh → tái cấu trúc. Hữu ích nhất cho logic rõ ràng, thuật toán, sửa bug (viết test tái hiện bug trước, sửa sau).
Ví dụ · pytest: parametrize & fixture
import pytest

@pytest.mark.parametrize("n, expected", [(0, 0), (1, 1), (5, 120)])
def test_factorial(n, expected):   # parametrize: 1 test chạy với nhiều bộ dữ liệu
    assert factorial(n) == expected

@pytest.fixture                    # fixture: dựng môi trường dùng chung, tự dọn sau test
def sample_user():
    return {"name": "An", "age": 20}

def test_is_adult(sample_user):
    assert is_adult(sample_user) is True
E2E · rất ít Integration · vừa Unit · rất nhiều · nhanh · rẻ chậm, dễ vỡ, đắt chạy mỗi lần sửa code
Kim tự tháp test: đầu tư nặng vào unit test nhanh và ổn định; E2E chỉ dành cho vài luồng sống còn.
Quy tắc phải nhớ
  • Test phải độc lập và xác định: chạy riêng hay chạy chung, thứ tự nào cũng cùng kết quả; cấm phụ thuộc giờ thật/số ngẫu nhiên/mạng thật.
  • Mọi bug được sửa đều nên kèm một test tái hiện — đó là "bảo hiểm" chống tái phát.
  • Ưu tiên test hành vi nhìn thấy được từ ngoài (đầu vào → đầu ra) thay vì chi tiết cài đặt bên trong; test bám cài đặt sẽ vỡ mỗi lần refactor dù hành vi không đổi.
11

Packaging, môi trường & cấu trúc project

Từ "code chạy trên máy tôi" tới sản phẩm cài được, chạy được ở mọi nơi.

Ý cốt lõi

  • Môi trường ảo (venv): mỗi project một "hộp" riêng chứa Python + thư viện của riêng nó — tránh xung đột phiên bản giữa các project và với hệ điều hành. Nguyên tắc bất di bất dịch: không bao giờ cài thư viện vào Python hệ thống.
  • pip & PyPI: pip là trình cài; PyPI là kho thư viện công cộng. File khai báo phụ thuộc (requirements / pyproject) + lock file ghim đúng phiên bản để mọi máy cài ra y hệt nhau ("reproducible").
  • pyproject.toml là trung tâm cấu hình hiện đại của một project Python: metadata gói, phụ thuộc, cấu hình công cụ (format, lint, test) — thay cho rừng file rời ngày xưa.
  • Đóng gói: sdist (mã nguồn) và wheel (bản đã build, cài nhanh) là hai định dạng phát hành lên PyPI. Công cụ quản lý hiện đại đáng biết: uv (cài/gói siêu nhanh), Poetry, Hatch.
  • Cấu trúc project chuẩn: mã nguồn trong thư mục gói (hoặc layout src/ để tránh import nhầm bản chưa cài), test trong thư mục riêng, README, .gitignore, biến bí mật trong file .env (không commit lên git).
Ví dụ · pyproject.toml
# pyproject.toml — trung tâm cấu hình của một project Python
[project]
name = "my-package"
version = "1.2.0"                    # SemVer: MAJOR.MINOR.PATCH
requires-python = ">=3.11"
dependencies = ["requests>=2.31"]    # thư viện cần khi chạy thật

[project.optional-dependencies]
dev = ["pytest", "ruff"]             # chỉ cần khi phát triển
my_project/ src/my_package/ ← mã nguồn tests/ ← kiểm thử pyproject.toml ← cấu hình README.md · .gitignore .env ← bí mật (không commit) .venv/ — môi trường ảo riêng Python riêng + thư viện đã ghim phiên bản. Máy nào dựng lại cũng ra y hệt → hết cảnh "máy tôi chạy được". Phát hành: build → wheel/sdist → PyPI hoặc đóng gói Docker để deploy.
Xương sống của một project Python chuyên nghiệp: cấu trúc src, một file cấu hình trung tâm, môi trường ảo cô lập.
Quy tắc phải nhớ
  • Ghim phiên bản phụ thuộc và commit lock file; nâng cấp thư viện là việc có chủ đích, có test, không phải "cài bản mới nhất rồi cầu nguyện".
  • Bí mật (API key, mật khẩu) không bao giờ nằm trong code hay git — chỉ qua biến môi trường/.env; lỡ commit coi như đã lộ, phải thu hồi key.
  • Bộ ba vệ sinh tự động: formatter (định dạng thống nhất, vd Ruff/Black) + linter (bắt lỗi ngớ ngẩn, import thừa) + type checker — chạy trong CI để không ai phải tranh cãi style khi review.
  • Version theo SemVer: MAJOR (phá tương thích) . MINOR (thêm tính năng) . PATCH (sửa lỗi).
12

Hiệu năng, debug & quy tắc vàng

Phần "nghề": đo trước khi tối ưu, và những nguyên tắc sống còn của Python.

Ý cốt lõi

  • Độ phức tạp thuật toán (Big-O) quan trọng hơn mọi tinh chỉnh nhỏ: O(1) < O(log n) < O(n) < O(n log n) < O(n²). Một vòng lặp lồng nhau trên dữ liệu lớn sẽ giết hiệu năng trước mọi thứ khác.
  • Profiling trước, tối ưu sau: dùng profiler để tìm đúng điểm nóng (thường 10% code chiếm 90% thời gian). Đo bằng thời gian thực và bộ nhớ; không tối ưu theo cảm tính.
  • Các đòn tăng tốc đúng thứ tự ưu tiên: đổi thuật toán/cấu trúc dữ liệu → dùng thư viện C (NumPy, thư viện chuẩn viết bằng C) → cache (memoization cho hàm thuần đắt) → song song hoá (process pool) → cuối cùng mới tới viết lại bằng ngôn ngữ biên dịch (Cython/Rust qua PyO3).
  • Logging thay print: log có cấp độ (DEBUG/INFO/WARNING/ERROR), có thời gian và ngữ cảnh, tắt/bật được mà không sửa code. Exception nên kèm traceback đầy đủ; thông điệp lỗi hướng tới người xử lý, không phải "Error!!!".
  • PEP 8 & Zen of Python: chuẩn phong cách cộng đồng — dễ đọc quan trọng hơn ngắn; rõ ràng hơn ngầm ẩn; một cách làm chuẩn cho mỗi việc.
Ví dụ · Memoization bằng lru_cache
from functools import lru_cache

@lru_cache(maxsize=None)      # memoization: nhớ kết quả của hàm thuần theo đầu vào
def fib(n):
    return n if n < 2 else fib(n - 1) + fib(n - 2)

fib(100)   # không cache: ~2^100 lần gọi đệ quy — không bao giờ xong
           # có cache: chỉ 101 lần gọi — cùng thuật toán, khác hẳn nhờ "nhớ lại"
Quy tắc vàng — thuộc nằm lòng
  • Đo, đừng đoán — về hiệu năng, bộ nhớ, và cả lỗi: tái hiện được → đo được → sửa đúng gốc rễ, không vá triệu chứng.
  • Dữ liệu ngoài biên là không tin được: mọi đầu vào từ người dùng/API/file đều phải kiểm định tại biên hệ thống.
  • Lỗi phải ồn ào ở gần nguyên nhân: thà chương trình dừng ngay với traceback rõ ràng còn hơn chạy tiếp với dữ liệu sai âm thầm.
  • Code được đọc nhiều hơn viết gấp 10 lần: đặt tên tốt, hàm ngắn, ý đồ rõ — tối ưu cho người đọc.
  • "Đủ tốt và đo được" thắng "hoàn hảo và mơ hồ": phát hành sớm, đo thực tế, cải tiến có số liệu.
Phần C

AI / Machine Learning

ML là lập trình bằng dữ liệu: thay vì viết luật, ta cho máy tự rút ra luật. Sáu chương này đi từ nền toán tới LLM và cách vận hành mô hình thật.

13

Nền tảng: toán & dữ liệu cho ML

Không cần giỏi toán như nhà nghiên cứu — cần hiểu trực giác của 4 mảng này.

Bốn mảng toán cốt lõi

  • Đại số tuyến tính: dữ liệu = các vector/mảng; phép nhân ma trận là cách mọi mô hình "trộn" các đặc trưng. Trực giác: mỗi mẫu dữ liệu là một điểm trong không gian nhiều chiều.
  • Giải tích — đạo hàm & gradient: gradient là "hướng dốc lên nhanh nhất" của hàm lỗi; học máy đi ngược hướng đó để giảm lỗi (Chương 16).
  • Xác suất & thống kê: phân phối, kỳ vọng, phương sai; định lý Bayes (cập nhật niềm tin khi có bằng chứng); mọi dự đoán của ML về bản chất là xác suất.
  • Tối ưu: gần như mọi bài toán ML rút gọn thành "tìm tham số làm hàm mất mát nhỏ nhất".

Công cụ dữ liệu của Python

Công cụVai tròÝ niệm cốt lõi
NumPyTính toán mảng sốMảng n chiều đồng nhất kiểu, tính theo vector hoá (cả mảng một lệnh, chạy bằng C nên nhanh); broadcasting — quy tắc "kéo giãn" kích thước khi tính giữa các mảng khác cỡ
PandasDữ liệu bảngDataFrame = bảng có nhãn hàng/cột; lọc, gộp nhóm (groupby), nối bảng; nơi dành 80% thời gian làm sạch dữ liệu
Matplotlib / SeabornVẽ & khám pháNhìn dữ liệu trước khi mô hình hoá: phân phối, tương quan, ngoại lệ
scikit-learnML cổ điểnMột API thống nhất cho mọi thuật toán: fit → predict; kèm chia dữ liệu, tiền xử lý, đánh giá
PyTorch / TensorFlowDeep learningTensor + tự động tính đạo hàm (autograd) + chạy GPU
Ví dụ · NumPy vector hoá & Pandas groupby
import numpy as np
import pandas as pd

a = np.array([1, 2, 3])
a * 10                  # vector hoá: tính CẢ MẢNG bằng một lệnh (vòng lặp chạy trong C)
                        # → array([10, 20, 30])

df = pd.read_csv("sales.csv")
df.groupby("city")["revenue"].mean()   # groupby: chia nhóm → tính gộp — thao tác Pandas kinh điển
mỗi hàng của bảng = 1 căn nhà ma trận X — đặc trưng vector y — đáp án diện tíchsố phònggiá (tỉ) 5032.1 8043.4 3521.2 tách cột 503 804 352 3 mẫu × 2 đặc trưng 2.13.41.2 giá thật để học mỗi hàng = 1 mẫu = 1 điểm trong không gian đặc trưng · mỗi cột = 1 đặc trưng · mô hình học ánh xạ X → y
Mọi bài toán ML đều bắt đầu bằng bước "số hoá" này: bảng dữ liệu tách thành ma trận đặc trưng X và vector đáp án y. Từ đây, "học" nghĩa là tìm hàm biến X thành y sao cho khớp đáp án đã biết — rồi dùng cho hàng chưa có đáp án.
Quy tắc phải nhớ
  • Rác vào, rác ra (garbage in, garbage out): chất lượng dữ liệu quyết định trần chất lượng của mô hình — hơn mọi lựa chọn thuật toán.
  • Luôn khám phá dữ liệu trước (EDA): phân phối, giá trị thiếu, ngoại lệ, mất cân bằng lớp. Đa số "mô hình tệ" là "dữ liệu tệ chưa ai nhìn".
  • Chia thang đo (scaling) quan trọng với mô hình dựa trên khoảng cách/gradient; mô hình cây thì không cần.
14

Quy trình ML & overfitting

Pipeline chuẩn của mọi dự án ML, và căn bệnh số một của mô hình: học thuộc thay vì học hiểu.

Thu thậpdữ liệu thô Làm sạchthiếu · nhiễu · trùng Đặc trưngfeature engineering Huấn luyệnfit trên train Đánh giátrên test độc lập Deployphục vụ thật vòng lặp: theo dõi thực tế → dữ liệu mới → huấn luyện lại
Pipeline ML chuẩn. Hai bước tốn công nhất ngoài đời là thu thập/làm sạch dữ liệu và vòng lặp sau triển khai — huấn luyện thường là bước… dễ nhất.

Chia dữ liệu — luật bất khả xâm phạm

  • Train (học) · Validation (chọn mô hình/tinh chỉnh siêu tham số) · Test (chấm điểm cuối, chỉ mở ra một lần). Test bị "nhìn" nhiều lần trong lúc tinh chỉnh thì nó đã biến thành validation và điểm số không còn đáng tin.
  • Data leakage (rò rỉ dữ liệu) — lỗi nghiêm trọng nhất của ML thực hành: thông tin từ tương lai/tập test lọt vào quá trình học (ví dụ tính thống kê chuẩn hoá trên toàn bộ dữ liệu trước khi chia). Mô hình điểm cao chót vót trong phòng lab, chết ngay khi ra thực tế.
  • Cross-validation: chia train thành k phần, luân phiên học–kiểm để ước lượng ổn định hơn khi dữ liệu ít.
Ví dụ · Pipeline chống rò rỉ dữ liệu
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42)

pipe = Pipeline([
    ("scale", StandardScaler()),   # scaler CHỈ học thống kê trên train → không rò rỉ
    ("model", LogisticRegression()),
])
pipe.fit(X_train, y_train)
pipe.score(X_test, y_test)          # tập test chỉ mở ra để chấm điểm cuối
TRAIN ~70% — mô hình học từ đây VALIDATION ~15% TEST ~15% niêm phong
Tỉ lệ chia tham khảo 70/15/15. Với dữ liệu chuỗi thời gian: chia theo thứ tự thời gian, tuyệt đối không xáo trộn — không được "học từ tương lai".

Overfitting vs Underfitting — đánh đổi bias/variance

  • Underfitting (thiếu khớp, bias cao): mô hình quá đơn giản — tệ cả trên train lẫn test. Khắc phục: mô hình mạnh hơn, thêm đặc trưng, giảm regularization.
  • Overfitting (quá khớp, variance cao): mô hình học thuộc cả nhiễu của train — điểm train cao, điểm test thấp, khoảng cách hai đường lỗi nới rộng. Khắc phục: thêm dữ liệu, regularization (phạt độ phức tạp), giảm độ sâu/kích thước mô hình, dừng sớm (early stopping), cross-validation.
  • Điểm ngọt nằm giữa: đích đến không phải điểm train cao nhất mà là khả năng tổng quát hoá (generalization) — làm tốt trên dữ liệu chưa từng thấy.
lỗi (error) độ phức tạp của mô hình → lỗi test — tăng vọt khi quá khớp lỗi train — giảm mãi UNDERFIT điểm ngọt OVERFIT
Chữ ký của overfitting: lỗi train cứ giảm trong khi lỗi test quay đầu tăng. Khoảng cách giữa hai đường = mức học thuộc.

Đo bằng gì — metrics

Bài toánChỉ số chínhBẫy cần biết
Phân loạiAccuracy · Precision · Recall · F1 · ROC-AUCDữ liệu lệch lớp (99% một lớp) thì accuracy 99% là vô nghĩa — nhìn Precision/Recall và ma trận nhầm lẫn (confusion matrix)
Hồi quyMAE · RMSE · R²RMSE phạt lỗi lớn nặng hơn MAE — chọn theo việc lỗi lớn có "đắt" không
Xếp hạng / gợi ýPrecision@K · MAP · NDCGChỉ quan tâm thứ hạng đầu danh sách, không cần điểm tuyệt đối
Quy tắc phải nhớ
  • Luôn có một baseline ngây thơ để so (đoán lớp đông nhất, mô hình tuyến tính đơn giản): mô hình phức tạp phải thắng baseline rõ rệt mới đáng tồn tại.
  • Precision = "trong những cái mô hình báo dương, bao nhiêu đúng"; Recall = "trong những cái thật sự dương, mô hình bắt được bao nhiêu". Chọn ưu tiên theo cái giá của từng loại sai (bỏ sót bệnh ≠ báo nhầm thư rác).
  • Mọi bước tiền xử lý học từ dữ liệu (chuẩn hoá, điền thiếu, chọn đặc trưng) phải học chỉ trên train rồi áp cho phần còn lại — nếu không là rò rỉ.
15

Các họ thuật toán ML

Bốn paradigma học và "anh cả" của từng họ — biết khi nào dùng cái nào.

ParadigmaÝ tưởngThuật toán tiêu biểu & khi dùng
Học có giám sát — hồi quyDự đoán một con số từ dữ liệu có đáp ánLinear/Ridge/Lasso Regression — baseline, dễ giải thích; phạt L1 (Lasso) tự loại đặc trưng thừa, L2 (Ridge) thu nhỏ hệ số
Học có giám sát — phân loạiDự đoán nhãn/nhómLogistic Regression (baseline xác suất), Decision Tree (dễ hiểu, dễ overfit), Random Forest (nhiều cây bỏ phiếu — mạnh, ổn định), Gradient Boosting/XGBoost/LightGBM (cây cộng dồn sửa sai — quán quân dữ liệu bảng), SVM (lề tối đa), k-NN (lười, dựa khoảng cách)
Học không giám sátTự tìm cấu trúc trong dữ liệu không nhãnK-Means & phân cụm phân cấp (gom nhóm), PCA & t-SNE/UMAP (giảm chiều để nén/trực quan), phát hiện bất thường (isolation forest)
Học tăng cường (RL)Tác nhân thử–sai, tối đa hoá phần thưởng tích luỹQ-learning, policy gradient, PPO — robot, game, điều khiển; đánh đổi khám phá (explore) vs khai thác (exploit)
Học bán giám sát / tự giám sátTận dụng ít nhãn + nhiều dữ liệu thôTiền huấn luyện trên dữ liệu không nhãn rồi tinh chỉnh — chính là cách các LLM ra đời
Ví dụ · Gradient boosting cho dữ liệu bảng
from sklearn.ensemble import HistGradientBoostingClassifier

model = HistGradientBoostingClassifier(max_iter=300, learning_rate=0.06)
model.fit(X_train, y_train)   # dữ liệu bảng cỡ vừa: gradient boosting thường mạnh nhất
model.score(X_val, y_val)     # siêu tham số (max_iter, learning_rate…) chỉnh trên VALIDATION
                              # — tuyệt đối không chỉnh trên test
thu nhập ≥ 50 triệu? không có tuổi ≥ 30? DUYỆT VAY thu nhập đủ mạnh có không XEM XÉT THÊM TỪ CHỐI mỗi nút = 1 câu hỏi trên 1 đặc trưng mỗi lá = 1 dự đoán cây sâu quá → overfit (học thuộc)
Cây quyết định là mô hình dễ hình dung nhất: một chuỗi câu hỏi có/không học từ dữ liệu. Random Forest cho hàng trăm cây như vậy "bỏ phiếu"; gradient boosting cho các cây nhỏ sửa sai nối tiếp nhau — cả hai họ quán quân đều xây trên hình này.
Quy tắc phải nhớ
  • Định lý "không có bữa trưa miễn phí": không thuật toán nào thắng mọi bài toán — dữ liệu quyết định. Quy trình đúng: baseline đơn giản trước, tăng dần độ phức tạp khi có bằng chứng cần thiết.
  • Dữ liệu bảng (tabular) cỡ vừa: gradient boosting thường là lựa chọn mạnh nhất; đừng mặc định dùng deep learning cho mọi thứ.
  • Mô hình cây không cần scaling và chống ngoại lệ tốt; mô hình khoảng cách/tuyến tính/NN thì nhạy với thang đo.
  • Siêu tham số (hyperparameter — nút vặn đặt trước khi học, khác tham số mô hình tự học) được chỉnh trên validation bằng tìm kiếm lưới/ngẫu nhiên/Bayes — không bao giờ chỉnh trên test.
16

Deep Learning — mạng nơ-ron & cách huấn luyện

Học biểu diễn tự động: mô hình tự rút ra đặc trưng theo từng tầng.

Ý cốt lõi

  • Nơ-ron nhân tạo: một phép tính đơn giản — tổng có trọng số của đầu vào + độ lệch, qua một hàm kích hoạt phi tuyến (ReLU, sigmoid, tanh…). Không có phi tuyến, chồng bao nhiêu tầng cũng chỉ tương đương một phép tuyến tính.
  • Các tầng (layer) xếp chồng tạo thành mạng: tầng đầu học đặc trưng thô (cạnh, nét), tầng sâu học khái niệm trừu tượng (khuôn mặt, ngữ nghĩa). Đây là sức mạnh cốt lõi của deep learning: tự học đặc trưng, khỏi cần feature engineering tay.
  • Tensor là đơn vị dữ liệu: mảng nhiều chiều có mặt khắp nơi (ảnh = tensor chiều cao × rộng × kênh màu). Mọi tính toán DL là phép toán trên tensor, chạy song song hàng loạt trên GPU.
  • Các kiến trúc theo loại dữ liệu: MLP (dữ liệu bảng/véc-tơ), CNN (ảnh — bộ lọc trượt học hoa văn cục bộ), RNN/LSTM (chuỗi — nay lép vế), Transformer (chuỗi/ngôn ngữ — thống trị hiện nay, Chương 17), GAN & Diffusion (sinh dữ liệu/ảnh).
Tầng vào Tầng ẩn — học đặc trưng theo cấp độ trừu tượng tăng dần Tầng ra
Mạng nơ-ron: mỗi đường nối mang một trọng số (weight). "Học" = điều chỉnh hàng triệu trọng số này sao cho đầu ra khớp đáp án.

Vòng huấn luyện — trái tim của deep learning

  1. Forward pass: đưa một mẻ dữ liệu (batch) qua mạng → ra dự đoán.
  2. Tính loss: hàm mất mát đo độ sai giữa dự đoán và đáp án (ví dụ cross-entropy cho phân loại, MSE cho hồi quy).
  3. Backpropagation: lan truyền ngược đạo hàm của loss theo quy tắc dây chuyền → biết mỗi trọng số "góp phần gây sai" bao nhiêu (gradient của từng trọng số). Framework (PyTorch) tự làm việc này bằng autograd.
  4. Cập nhật: optimizer (SGD, Adam…) dịch mỗi trọng số một bước nhỏ ngược hướng gradient. Độ dài bước = learning rate — siêu tham số quan trọng nhất: quá lớn thì dao động/nổ, quá nhỏ thì học mãi không xong.
  5. Lặp lại qua nhiều epoch (lượt quét toàn bộ dữ liệu), theo dõi loss trên validation để dừng đúng lúc (early stopping chống overfitting).
Ví dụ · Vòng train PyTorch
for epoch in range(epochs):
    for x, y in train_loader:        # dữ liệu nạp theo từng mẻ (batch)
        pred = model(x)              # 1. forward pass: đưa dữ liệu qua mạng → dự đoán
        loss = loss_fn(pred, y)      # 2. tính loss: đo độ sai so với đáp án
        optimizer.zero_grad()
        loss.backward()              # 3. backprop: autograd tính gradient của mọi trọng số
        optimizer.step()             # 4. cập nhật: dịch mỗi trọng số ngược hướng gradient
                                     #    một bước dài bằng learning rate
điểm bắt đầu đáy — loss nhỏ nhất mỗi bước đi ngược hướng dốc nhất (gradient), cỡ bước = learning rate
Gradient descent hình dung như quả bóng lăn xuống thung lũng của hàm lỗi. Thực tế bề mặt lỗi có hàng triệu chiều — trực giác vẫn vậy: đi ngược gradient, bước vừa đủ nhỏ.
chiều xuôi — forward: dữ liệu chảy tới, sinh dự đoán và loss dữ liệu (batch) x và đáp án y forward pass đưa qua các tầng dự đoán pred loss — độ sai so với đáp án backprop — gradient chảy NGƯỢC qua từng tầng mỗi trọng số biết phần lỗi của mình → optimizer dịch ngược hướng gradient một bước bằng learning rate → lặp lại ở batch kế tiếp
Vòng huấn luyện là một vòng khép kín hai chiều: forward để "đo sai", backprop để "phân bổ trách nhiệm gây sai" cho từng trọng số. Hàng triệu vòng như vậy — mỗi vòng một batch — là toàn bộ quá trình "học" của mạng nơ-ron.
Quy tắc phải nhớ
  • Chuẩn hoá đầu vào + khởi tạo trọng số đúng cách là điều kiện để train ổn định; loss không giảm thường do learning rate sai trước mọi nguyên nhân khác.
  • Regularization cho DL: dropout (ngẫu nhiên tắt bớt nơ-ron khi train), weight decay, tăng cường dữ liệu (data augmentation), early stopping.
  • Batch size và learning rate đi cùng nhau; train không hội tụ thì kiểm tra dữ liệu & nhãn trước khi nghi ngờ kiến trúc (nhãn sai là bệnh thầm lặng phổ biến nhất).
  • Transfer learning là đường tắt chuẩn: lấy mô hình đã tiền huấn luyện trên dữ liệu khổng lồ, tinh chỉnh (fine-tune) phần cuối cho bài toán riêng — ít dữ liệu, ít tiền, hiệu quả cao.
17

Transformer, LLM, RAG & AI hiện đại

Kiến trúc đứng sau ChatGPT và mọi mô hình ngôn ngữ lớn ngày nay.

Ý cốt lõi

  • Token: văn bản được cắt thành các mảnh nhỏ (token — khoảng ¾ từ tiếng Anh, tiếng Việt tốn token hơn). Mô hình không đọc chữ cái hay từ nguyên vẹn, nó đọc token. Mọi giới hạn "context window" đều tính bằng token.
  • Embedding: mỗi token được ánh xạ thành một vector số; token gần nghĩa thì vector gần nhau. "Ngữ nghĩa thành hình học" là nền của tìm kiếm ngữ nghĩa và RAG.
  • Attention (cơ chế chú ý): khi xử lý một token, mô hình tự tính xem nên "nhìn" vào những token nào trước đó và với trọng lượng bao nhiêu (qua ba vai Query–Key–Value). Đây là phát minh cốt lõi của Transformer (2017): xử lý toàn bộ chuỗi song song và nắm được phụ thuộc xa, thay cho đọc tuần tự như RNN.
  • LLM học bằng cách dự đoán token tiếp theo trên kho văn bản khổng lồ (pretraining — tự giám sát, không cần nhãn tay). Dự đoán-từ-tiếp-theo tưởng đơn giản nhưng ở quy mô lớn sinh ra khả năng suy luận, dịch, tóm tắt, viết code…
  • Ba giai đoạn tạo nên một trợ lý AI: pretrain (học ngôn ngữ & tri thức) → instruction tuning/SFT (học làm theo lệnh từ ví dụ người viết) → RLHF/DPO (chỉnh theo sở thích con người: hữu ích, trung thực, an toàn).
  • Tham số & suy luận: kích thước mô hình tính bằng tỉ tham số (trọng số). Lúc dùng (inference), hai chỉ số sống còn là độ trễ token đầu và tốc độ token/giây; lượng tử hoá (quantization — giảm độ chính xác số của trọng số, ví dụ xuống 4-bit) giúp chạy mô hình lớn trên phần cứng nhỏ với hao hụt chất lượng chấp nhận được.
Văn bản cắt thành token Embedding token → vector × N khối Transformer Multi-Head Attention (ai nhìn ai) + Feed-Forward · chuẩn hoá · cộng dư Xác suất token kế tiếp Chọn 1 rồi lặp lại tự hồi quy (autoregressive): token vừa sinh được nối vào đầu vào, sinh tiếp token sau
Kiến trúc Transformer rút gọn: token hoá → embedding → chồng nhiều khối attention → dự đoán token tiếp theo, lặp lại từng token một.
trọng lượng chú ý của "nó": đường càng đậm — nhìn càng nhiều 6% 62% 23% Con mèo mệt nên nó nằm ngủ Query của "nó" khớp mạnh nhất với Key của "mèo" → nó lấy nhiều Value từ "mèo" → mô hình hiểu "nó" chính là con mèo (trọng số % chỉ là minh hoạ)
Self-attention là đây: mỗi token tự quyết định nên "nghe" mỗi token khác bao nhiêu phần trăm. Nhờ vậy đại từ "nó" gắn được về đúng danh từ ở xa — điều RNN đọc tuần tự rất khó làm. Nhiều "đầu" (head) attention chạy song song, mỗi đầu theo dõi một kiểu quan hệ khác nhau.

Dùng LLM đúng cách: prompt · fine-tune · RAG

CáchBản chấtDùng khi
Prompt engineeringĐiều khiển bằng hướng dẫn + ví dụ mẫu (few-shot) trong ngữ cảnhViệc chung, thay đổi nhanh, chưa có dữ liệu riêng
RAGTìm đoạn tài liệu liên quan → nhét vào prompt → mô hình trả lời dựa trên đóCần tri thức riêng/mới, cần trích nguồn, giảm bịa (hallucination)
Fine-tuningHuấn luyện thêm để đổi phong cách/hành vi/định dạng của mô hìnhCần hành vi ổn định lặp lại ở quy mô lớn; không phải để "nạp kiến thức mới" — kiến thức thì dùng RAG
Function calling / AgentMô hình gọi công cụ ngoài (API, CSDL, tìm kiếm) theo vòng: suy nghĩ → gọi tool → đọc kết quả → trả lờiViệc cần dữ kiện thật, hành động thật, nhiều bước
Câu hỏi của người dùng Embedding câu hỏi → vector Vector DB tìm đoạn gần nghĩa nhất LLM hỏi + ngữ cảnh Trả lời kèm nguồn chuẩn bị sẵn (offline): tài liệu → cắt đoạn (chunk) → embedding → lưu vào Vector DB
RAG: mô hình không cần "nhớ" tài liệu của bạn — nó được tra cứu và đưa đúng đoạn liên quan vào ngữ cảnh ngay lúc hỏi.
Ví dụ · Token & tìm kiếm ngữ nghĩa kiểu RAG
import numpy as np

tokens = tokenizer.encode("Học Python thật thú vị")
len(tokens)   # mọi giới hạn "context window" đều tính bằng token — không phải số từ

def cosine(u, v):   # độ tương đồng hướng của 2 vector: càng gần 1, càng gần nghĩa
    return u @ v / (np.linalg.norm(u) * np.linalg.norm(v))

# RAG rút gọn: nhúng câu hỏi thành vector, tìm đoạn tài liệu gần nghĩa nhất
q = embed("Robot học tiếng Anh giá bao nhiêu?")
best = max(chunks, key=lambda c: cosine(q, embed(c)))
# → đoạn "best" được nhét vào prompt để LLM trả lời dựa trên đó, kèm nguồn
Quy tắc phải nhớ
  • Hallucination là thuộc tính, không phải bug hiếm: LLM sinh văn bản "nghe hợp lý" chứ không truy vấn sự thật. Việc quan trọng phải có RAG trích nguồn, kiểm chứng, hoặc người duyệt.
  • Nhiệt độ (temperature) và cách lấy mẫu quyết định độ "sáng tạo vs nhất quán": việc cần chính xác → tham số thấp, việc sáng tạo → cao hơn.
  • Đánh giá hệ thống LLM bằng bộ câu hỏi chuẩn (eval set) và chấm theo tiêu chí — không đánh giá bằng "hỏi thử mấy câu thấy ổn".
  • Chi phí LLM tính theo token vào/ra: ngữ cảnh dài = đắt và chậm. Quản context là một kỹ năng kỹ thuật thật sự (chọn lọc, tóm tắt, cache prompt).
18

Thực hành ML & MLOps — đưa mô hình ra đời thật

Mô hình chạy trong notebook mới là 10% chặng đường; 90% còn lại là vận hành.

Ý cốt lõi

  • Phục vụ mô hình (serving): đóng gói mô hình thành API (thường FastAPI — chính là ứng dụng của async ở Chương 8), hoặc suy luận theo lô (batch). Cần quản phiên bản mô hình như quản phiên bản code.
  • Trôi dữ liệu (drift): thế giới thay đổi — phân phối dữ liệu thật lệch dần khỏi dữ liệu huấn luyện, mô hình âm thầm tệ đi. Bắt buộc theo dõi: chất lượng đầu vào, phân phối dự đoán, và chỉ số nghiệp vụ; có ngưỡng cảnh báo và kế hoạch huấn luyện lại.
  • Thí nghiệm có kỷ luật: ghi lại mọi lần train (dữ liệu phiên bản nào, siêu tham số gì, điểm bao nhiêu) bằng công cụ tracking (MLflow…) — không ghi = không tái lập được kết quả.
  • Công bằng & an toàn: kiểm tra mô hình có tệ bất thường với một nhóm người dùng nào không (bias từ dữ liệu lịch sử); dữ liệu cá nhân phải tuân thủ quy định bảo vệ dữ liệu; mô hình sinh nội dung cần rào chắn đầu vào/ra.
  • Đường ống tự động (pipeline): thu thập → kiểm định dữ liệu → train → đánh giá cổng chất lượng → triển khai từng phần (canary) → giám sát. Đây là MLOps: DevOps áp cho ML, cộng thêm quản trị dữ liệu và mô hình.
Ví dụ · Serving mô hình bằng FastAPI
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()
model = load_model("fraud-detector", version="1.4.2")  # mô hình cũng có PHIÊN BẢN như code

class Request(BaseModel):     # Pydantic kiểm định dữ liệu tại BIÊN: sai kiểu → từ chối ngay
    amount: float
    country: str

@app.post("/predict")
def predict(req: Request):
    features = to_features(req)        # (bước trích đặc trưng đã rút gọn)
    return {"fraud_score": float(model.predict_proba(features)[1])}
Lỗi chết người thường gặp: train trên dữ liệu rò rỉ đáp án · chấm điểm trên tập đã dùng để tinh chỉnh · triển khai không có giám sát · không lưu phiên bản dữ liệu · tin điểm offline mà không đo tác động nghiệp vụ thật (A/B test khi có thể).
1 · Train + tracking lưu phiên bản dữ liệu + tham số 2 · Cổng chất lượng đánh giá trên test độc lập 3 · Deploy canary ra mắt từng phần nhỏ 4 · Giám sát drift dữ liệu thật lệch dần? 5 · Huấn luyện lại khi drift vượt ngưỡng ở production vòng này quay mãi — deploy chỉ là điểm giữa, không phải đích đến
Vòng đời MLOps khác hẳn pipeline train-một-lần ở Chương 14: mô hình ra production rồi vẫn phải được giám sát liên tục, và dữ liệu mới thu về lại trở thành nhiên liệu cho vòng huấn luyện kế tiếp.
Quy tắc phải nhớ
  • Mô hình là một sản phẩm sống: ngân sách dự án phải gồm cả phần giám sát và huấn luyện lại định kỳ, không chỉ lần train đầu.
  • Mọi quyết định bằng mô hình quan trọng (tín dụng, y tế, tuyển dụng) cần: khả năng giải thích, nhật ký kiểm toán, và đường cho con người can thiệp.
  • Bắt đầu đơn giản ở production: mô hình nhỏ, rõ ràng, đo được — thắng mô hình khổng lồ không ai kiểm soát nổi.
✓

Checklist nắm vững toàn bộ giáo trình

Tự rà lại: giải thích được bằng lời của mình từng ý dưới đây là coi như nắm trọn.

  • Python lõi: biến là nhãn tham chiếu tới đối tượng; phân biệt mutable/immutable, ==/is, shallow/deep copy.
  • Hàm & scope: LEGB, closure, vì sao không dùng mutable làm giá trị mặc định, hàm là đối tượng hạng nhất.
  • OOP: class vs instance, MRO, duck typing, vì sao ưu tiên composition hơn kế thừa, khi nào dùng dataclass.
  • Bộ nhớ: refcount huỷ đối tượng thế nào, vòng tham chiếu và vai trò của GC thế hệ, interning là gì.
  • Cơ chế nâng cao: iterator/generator và tính lười, decorator là hàm-bọc-hàm, with và giao thức context manager.
  • Đồng thời: phân biệt CPU-bound/I/O-bound; GIL làm thread vô dụng cho tính toán nhưng tốt cho I/O; chọn đúng giữa thread/process/async và kể được vòng đời event loop.
  • Typing: type hint không chạy lúc runtime; Optional/Union/Generic/Protocol; kiểm định dữ liệu thật tại biên bằng công cụ riêng.
  • Testing: kim tự tháp test, AAA, fixture/parametrize/mock đúng chỗ, coverage không bằng chất lượng.
  • Packaging: venv cho mọi project, pyproject.toml + lock file, wheel/sdist, SemVer, bí mật không vào git.
  • ML quy trình: pipeline 6 bước, luật chia train/validation/test, data leakage là gì, overfitting chữa bằng cách nào.
  • ML thuật toán: chọn được baseline và thuật toán phù hợp cho hồi quy/phân loại/cụm/RL; đọc được precision–recall theo giá của sai lầm.
  • Deep learning: nơ-ron + hàm kích hoạt, vòng forward → loss → backprop → update, vai trò learning rate, transfer learning.
  • LLM: token, embedding, attention, dự đoán token tiếp theo; phân biệt prompt/RAG/fine-tune và khi nào dùng cái nào; hallucination và cách giảm.
  • MLOps: drift là gì, vì sao phải tracking thí nghiệm, giám sát và huấn luyện lại là phần việc bắt buộc sau deploy.