Mở đầu: khi không ai chấm điểm đúng/sai cho bạn

Ba bài trước đều có một thứ xa xỉ: đáp án đúng đi kèm mỗi điểm dữ liệu — giá nhà thật (Bài 1), nhãn đậu/rớt thật (Bài 3). Nhưng phần lớn dữ liệu một doanh nghiệp thu thập được — log truy cập, lịch sử mua hàng, ảnh chưa gắn thẻ — không có đáp án nào đi kèm cả. Không ai ngồi gắn nhãn "khách hàng nhóm A" cho hàng triệu giao dịch. Câu hỏi đổi hẳn: không còn là "dự đoán con số/nhãn cho trước", mà là "tự tìm cấu trúc ẩn trong đống dữ liệu thô".

Bài này học hai công cụ nền tảng nhất của học không giám sát. K-means tự động gom điểm dữ liệu thành các nhóm (cụm) mà không cần ai chỉ trước nhóm nào là nhóm nào — và bạn sẽ thấy nó chắc chắn hội tụ nhưng không chắc chắn đúng, một phân biệt tinh tế sẽ ám ảnh mọi thuật toán lặp bạn gặp sau này. PCA giải bài toán ngược lại: dữ liệu có hàng chục cột, làm sao nén còn vài cột mà không mất thông tin quan trọng? Cả hai đều được xây từ số 0, kết thúc bằng demo k-means chạy từng bước bạn tự bấm, và một đám mây điểm 3D bạn tự xoay để thấy PCA "nhìn thấy" mặt phẳng ẩn bên trong.


📚 Điều kiện tiên quyết
Bắt buộc: Bài 1 (chỉ cần khái niệm khoảng cách Euclid — không cần lại Bài 2/Bài 3, bài này độc lập với gradient descent). Tài nguyên ngoài: Google ML Crash Course — Clustering, Setosa — PCA trực quan hoá tương tác.

1. Ba nhánh học máy nhìn từ dữ liệu

Điểm phân biệt ba nhánh học máy không nằm ở thuật toán — nó nằm ở dữ liệu cho máy được gì:

Nhánh Dữ liệu có gì Mục tiêu Gặp ở đâu trong series
Học có giám sát Từng ví dụ kèm đáp án đúng (nhãn) Học ánh xạ đầu vào → đáp án, dự đoán cho dữ liệu mới Bài 1–3 (giá nhà, đậu/rớt)
Học không giám sát Chỉ có dữ liệu, không nhãn Tự tìm cấu trúc ẩn: nhóm giống nhau, hướng biến thiên chính Bài này (k-means, PCA)
Học tăng cường Không có đáp án đúng — chỉ có phần thưởng thưa sau một chuỗi hành động Học chính sách hành động tối đa hoá phần thưởng dài hạn Bài 17 (Q-learning)

Vì sao học không giám sát quan trọng đến vậy: phần lớn dữ liệu doanh nghiệp thực tế không có nhãn. Gắn nhãn tay cho hàng triệu bản ghi là công việc tốn kém, chậm, và thường bất khả thi ở quy mô lớn — trong khi dữ liệu thô (không nhãn) gần như miễn phí, sinh ra liên tục từ mọi tương tác. Học không giám sát là cách duy nhất để khai thác phần dữ liệu áp đảo này.

2. K-means từng bước: gán rồi cập nhật, lặp lại

Bài toán: cho $N$ điểm dữ liệu và một số $k$ (số cụm muốn có), tìm $k$ "tâm cụm" (centroid) sao cho mỗi điểm ở gần tâm cụm của nó hơn mọi tâm cụm khác. Thuật toán K-means giải bài này bằng một vòng lặp hai bước cực đơn giản, lặp tới khi ổn định:

  1. Assign (gán): mỗi điểm dữ liệu được gán vào cụm có centroid gần nó nhất (khoảng cách Euclid).
  2. Update (cập nhật): mỗi centroid dời tới đúng vị trí trung bình của mọi điểm vừa được gán vào cụm đó.

Lặp lại 2 bước cho tới khi không còn điểm nào đổi cụm. Đại lượng theo dõi mức độ "gọn" của cách chia hiện tại gọi là inertia — tổng bình phương khoảng cách từ mỗi điểm tới centroid của nó:

$$\text{inertia} = \sum_{i=1}^{N} \|x_i - c_{\text{gan}(i)}\|^2$$

kmeans_core.js (đúng vòng lặp Demo A gọi, self-test đối chiếu)
function kmeansStep(points, centroids) {
  // 1. Assign: moi diem ve centroid gan nhat (khoang cach Euclid binh phuong)
  const assign = points.map(p => {
    let best = -1, bd = Infinity;
    centroids.forEach((c, k) => {
      const d = (p.x-c.x)**2 + (p.y-c.y)**2;
      if (d < bd) { bd = d; best = k; }
    });
    return best;
  });
  // 2. Update: centroid moi = trung binh cong cac diem vua duoc gan
  const sums = centroids.map(() => ({x:0, y:0, n:0}));
  points.forEach((p, i) => { const s=sums[assign[i]]; s.x+=p.x; s.y+=p.y; s.n++; });
  const newCentroids = sums.map((s, k) => s.n ? {x: s.x/s.n, y: s.y/s.n} : centroids[k]);
  return { assign, centroids: newCentroids };
}
// Goi lap lai toi khi assign khong doi nua -> inertia CHAC CHAN da hoi tu

Sự thật toán học đẹp và quan trọng nhất bài: mỗi bước assign-rồi-update không bao giờ làm inertia tăng — assign luôn chọn centroid gần nhất (không thể làm tổng khoảng cách tệ hơn), và update dời centroid về đúng điểm cực tiểu hoá tổng bình phương khoảng cách của cụm đó (trung bình cộng chính là điểm tối ưu này). Vì inertia giảm dần (hoặc đứng yên) và có cận dưới ($\geq 0$), thuật toán chắc chắn dừng lại sau hữu hạn bước. Chạy trên bộ dữ liệu 24 điểm — 3 cụm Gaussian thật quanh $(2,2)$, $(8,2)$, $(5,8)$ — với centroid khởi tạo gần đúng 3 cụm, self-test đối chiếu:

Bước Inertia
1 (sau assign lần đầu) $76{,}600$
2 (hội tụ) $25{,}170$

Chỉ 2 bước để hội tụ, inertia giảm đơn điệu đúng như lý thuyết dự đoán. Nhưng "chắc chắn dừng" và "chắc chắn tìm được cách chia TỐT NHẤT" là hai điều hoàn toàn khác nhau.

⚠️ Cạm bẫy: khởi tạo xấu → kẹt ở nghiệm tệ, dù thuật toán chạy đúng 100%

K-means chỉ đảm bảo hội tụ về một cực tiểu CỤC BỘ của inertia — không đảm bảo đó là cực tiểu toàn cục (cách chia tốt nhất có thể). Kết quả phụ thuộc vị trí khởi tạo centroid. Trên đúng bộ dữ liệu 3 cụm ở trên, thử khởi tạo cả 3 centroid cùng nằm lệch về một phía (gần cụm bên phải) thay vì rải đều 3 vùng:

Cách khởi tạo Số bước hội tụ Inertia cuối
Rải đều 3 vùng (tốt) 2 $25{,}170$
Cả 3 lệch về 1 phía (xấu) 5 $191{,}537$ — tệ hơn 7,6 lần

Thuật toán vẫn chạy đúng tuyệt đối từng bước (inertia vẫn giảm đơn điệu: $335 \to 199 \to 194 \to 192 \to 192$) — nó chỉ hội tụ về một cách chia tệ: 2 centroid tranh nhau trong cùng một cụm bên phải, còn centroid thứ ba phải "gánh" gần hết phần còn lại của dữ liệu. Đây KHÔNG phải lỗi code — đây là bản chất toán học của thuật toán lặp cực tiểu cục bộ.

Giải pháp thực tế: k-means++ — thay vì chọn centroid khởi tạo hoàn toàn ngẫu nhiên, chọn centroid tiếp theo với xác suất tỉ lệ thuận với bình phương khoảng cách tới centroid gần nhất đã chọn. Trực giác: một centroid đã nằm trong cụm A rồi thì centroid tiếp theo có xu hướng bị "đẩy" ra xa A — nhiều khả năng rơi vào cụm B hoặc C, tránh đúng lỗi "cả 3 dồn 1 chỗ" ở trên. Thư viện công nghiệp (sklearn KMeans) dùng k-means++ làm mặc định, không phải khởi tạo thuần ngẫu nhiên.

🔢 k-means++ có thực sự tránh được cực tiểu cục bộ tệ không?
Đã kiểm chứng bằng cách chạy đúng bộ dữ liệu 24 điểm ở trên: khởi tạo "xấu" (như pitfall trên) luôn cho inertia $\approx 191{,}54$. Thử khởi tạo bằng k-means++ với 10 hạt giống ngẫu nhiên khác nhau: cả 10 lần đều hội tụ đúng về inertia $\approx 25{,}17$ — cách chia TỐT NHẤT, không lần nào rơi vào bẫy "dồn 1 phía" như khởi tạo ngẫu nhiên thuần tuý. Cơ chế "đẩy xa centroid đã chọn" hoạt động đúng như lý thuyết mô tả: xác suất chọn centroid tiếp theo tỉ lệ với bình phương khoảng cách khiến việc rơi cả 3 centroid vào cùng 1 vùng gần như không thể xảy ra một cách ngẫu nhiên. Đây là bằng chứng số cụ thể cho lý do sklearn chọn k-means++ làm mặc định thay vì khởi tạo ngẫu nhiên thuần.

Câu hỏi còn bỏ ngỏ: chọn $k$ bao nhiêu? Vẽ inertia tốt nhất tìm được (nhiều lần khởi tạo lại) theo từng giá trị $k$ — phương pháp elbow (khuỷu tay) — trên đúng bộ dữ liệu 3 cụm:

$k$ 1 2 3 4 5 6
Inertia tốt nhất $338{,}4$ $169{,}0$ $25{,}2$ $18{,}3$ $16{,}1$ $12{,}5$

Từ $k=2 \to 3$, inertia rơi mạnh ($169 \to 25$, giảm gần $7$ lần) — đúng lúc số cụm chạm số cụm THẬT của dữ liệu. Từ $k=3$ trở đi, mỗi lần tăng $k$ chỉ gọt thêm chút ít ($25 \to 18 \to 16 \to 12$) vì đang chia nhỏ những cụm vốn đã hợp lý — đó là "khuỷu tay" của đồ thị, điểm mà lợi ích biên giảm hẳn tốc độ. Giới hạn thật của elbow method: với dữ liệu đời thực (không sạch như 3 cụm Gaussian tách biệt), khuỷu tay hiếm khi rõ ràng — đường cong tụt dần đều đặn, người dùng phải tự phán đoán chủ quan, và hai người nhìn cùng một đồ thị có thể chọn hai giá trị $k$ khác nhau.

3. K-means thất bại khi nào

K-means chia không gian bằng mặt phẳng phân giới giữa các centroid (mỗi điểm thuộc về centroid gần nhất) — hình dạng cụm nó tìm được luôn có xu hướng hình cầu/lồi. Với dữ liệu thật sự có cấu trúc dạng đó, k-means gần như hoàn hảo, như 3 cụm Gaussian ở trên. Nhưng thử áp k-means ($k=2$) lên hai vòng tròn đồng tâm (vòng trong bán kính $\approx 1$, vòng ngoài bán kính $\approx 4$, cùng tâm) — về mặt thị giác con người, đây là 2 nhóm rõ ràng:

ring_failure.js (chạy đúng trong self-test file tải về)
// 20 diem vong trong (r~1) + 20 diem vong ngoai (r~4), cung tam
const result = kmeans(ringPoints, [{x:1,y:0}, {x:-1,y:0}]);
// Centroid cuoi: (~1.57, 0.24) va (~-1.57, -0.24) - CHIA THEO TRAI/PHAI
// Purity moi cum: 10 vong trong + 10 vong ngoai (50/50) - THAT BAI HOAN TOAN

K-means chia dữ liệu thành nửa trái / nửa phải (centroid cuối cùng nằm đối xứng qua tâm theo trục ngang) — hoàn toàn phớt lờ cấu trúc "trong/ngoài" mà mắt người thấy ngay lập tức. Mỗi cụm kết quả chứa đúng $50\%$ điểm vòng trong và $50\%$ điểm vòng ngoài — độ tinh khiết (purity) chỉ ngang mức đoán ngẫu nhiên. Lý do sâu xa: hai vòng tròn không thể tách bằng một đường thẳng/mặt phẳng nào — mà k-means, giống hệt decision boundary tuyến tính của Bài 3, về bản chất chỉ vẽ được ranh giới thẳng giữa các centroid.

Dạng cụm K-means Vì sao
Các khối hình cầu, tách biệt rõ (như 3 cụm Gaussian Mục 2) ✅ Gần hoàn hảo Đúng giả định của thuật toán: cụm lồi, phân giới thẳng đủ dùng
Vòng tròn đồng tâm, hình trăng lưỡi liềm, xoắn ốc ❌ Thất bại hoàn toàn Không có đường thẳng/mặt phẳng nào tách đúng cấu trúc
Cụm chênh lệch mật độ hoặc kích thước rất lớn ❌ Thiên vị cụm lớn, "nuốt" bớt cụm nhỏ gần đó Inertia ưu tiên tổng bình phương nhỏ nhất, không cân bằng số điểm/cụm

Bài học rút ra: k-means là baseline nhanh và dễ hiểu, không phải câu trả lời vạn năng cho mọi bài toán phân cụm. Dữ liệu có cấu trúc phi tuyến (vòng tròn, xoắn ốc) cần thuật toán khác (DBSCAN, spectral clustering — ngoài phạm vi series này) hoặc cần biến đổi dữ liệu trước (ví dụ chuyển toạ độ Cartesian sang toạ độ cực biến bán kính thành một trục riêng).

4. PCA: nén dữ liệu theo hướng giữ nhiều thông tin nhất

Bài toán ngược của k-means: thay vì nhóm các điểm, PCA (phân tích thành phần chính) nén các cột (đặc trưng). Với dữ liệu nhiều chiều, câu hỏi là: nếu chỉ được giữ lại 1 hoặc 2 hướng chiếu, hướng nào giữ được nhiều thông tin nhất? "Thông tin" ở đây được đo bằng phương sai (độ trải rộng) — hướng nào dữ liệu trải rộng nhất chính là hướng phân biệt các điểm rõ nhất, nên là hướng đáng giữ lại nhất khi phải bỏ bớt chiều.

Hướng phương sai lớn nhất chính là eigenvector ứng với eigenvalue lớn nhất của ma trận hiệp phương sai (covariance matrix) của dữ liệu. Không cần đại số tuyến tính hình thức để tìm nó — power iteration làm được bằng một phép lặp cực đơn giản: nhân lặp đi lặp lại một vector với ma trận rồi chuẩn hoá về độ dài 1:

$$v_{t+1} = \frac{Av_t}{\|Av_t\|}$$

power_iteration.js (đúng hàm self-test và Demo B dùng)
function powerIteration(cov, iters) {
  let v = [1, 0];                          // vector khoi dau bat ky, khac 0
  for (let i = 0; i < iters; i++) {
    const Av = [cov.cxx*v[0] + cov.cxy*v[1], cov.cxy*v[0] + cov.cyy*v[1]];
    const norm = Math.sqrt(Av[0]**2 + Av[1]**2);
    v = [Av[0]/norm, Av[1]/norm];          // chuan hoa ve do dai 1 moi buoc
  }
  return v;                                // hoi tu ve eigenvector eigenvalue LON NHAT
}

Trực giác: mỗi lần nhân, thành phần của $v$ nằm dọc hướng eigenvalue lớn nhất được "khuếch đại" nhiều hơn mọi hướng khác — lặp đủ nhiều, $v$ bị kéo về gần sát hướng đó. Tính tay trên ma trận đối xứng $A=\begin{pmatrix}2&1\\1&2\end{pmatrix}$ (eigenvector đúng đã biết trước: $(0{,}7071,\ 0{,}7071)$ ứng với eigenvalue $3$), xuất phát từ $v_0=(1,0)$:

Bước $v$ (đã chuẩn hoá) Rayleigh quotient ($v^TAv$, ước lượng eigenvalue)
1 $(0{,}8944,\ 0{,}4472)$ $2{,}8000$
2 $(0{,}7809,\ 0{,}6247)$ $2{,}9756$
3 $(0{,}7328,\ 0{,}6805)$ $2{,}9973$
4 $(0{,}7158,\ 0{,}6983)$ $2{,}9997$
5 $(0{,}7100,\ 0{,}7042)$ $3{,}0000$
6 $(0{,}7081,\ 0{,}7061)$ $3{,}0000$

Chỉ sau 6 bước nhân-rồi-chuẩn-hoá, $v$ đã bám sát $(0{,}7071,\ 0{,}7071)$ và Rayleigh quotient khớp chính xác eigenvalue $3$ — không cần công thức đại số nào phức tạp hơn phép nhân ma trận-vector. Áp đúng kỹ thuật này lên một bộ 40 điểm dữ liệu 2D tương quan thật (kéo dài theo hướng $\approx 30°$), power iteration cho ra vector $(0{,}8831,\ 0{,}4692)$ — góc thực tế $\approx 28°$, khớp hướng đã tạo dữ liệu — với eigenvalue $7{,}4104$, chiếm $94{,}4\%$ tổng phương sai. Giữ lại đúng 1 chiều (thay vì 2) mà vẫn giữ được hơn $94\%$ thông tin — đó là sức mạnh của PCA.

⚠️ Cạm bẫy: quên chuẩn hoá đơn vị — một cột "nuốt chửng" mọi cột khác

PCA tìm hướng phương sai LỚN NHẤT theo đúng nghĩa đen — nếu các cột đo bằng đơn vị khác nhau, cột nào có con số tuyệt đối lớn hơn sẽ có phương sai áp đảo, dù về mặt ý nghĩa nó không "quan trọng hơn". Ví dụ cụ thể: dữ liệu nhân viên với cột tuổi (22–55, đơn vị năm) và cột lương (12–95 triệu, ghi bằng đồng thay vì triệu đồng — tức $12\,000\,000$ tới $95\,000\,000$):

  Phương sai tuổi Phương sai lương (VND) Tỉ lệ chênh lệch
Giá trị $113{,}6$ $8{,}17 \times 10^{14}$ khoảng $7 \times 10^{12}$ lần

PCA không chuẩn hoá cho ra $PC1 = (0{,}0000,\ 1{,}0000)$ — $100\%$ là lương, $0\%$ là tuổi, dù trực giác con người thấy cả hai đặc trưng đều đáng giữ. Nguyên nhân thuần tuý là đơn vị đo: đổi lương từ "đồng" sang "triệu đồng" (chia $10^6$) sẽ đổi hẳn kết quả PCA dù dữ liệu về bản chất không đổi gì — một dấu hiệu rõ ràng rằng đang đo sai cách. Khắc phục bằng chuẩn hoá z-score (trừ trung bình, chia độ lệch chuẩn) từng cột trước khi tính PCA — sau chuẩn hoá, $PC1 \approx (0{,}7071,\ 0{,}7071)$, cân bằng đúng giữa hai đặc trưng như trực giác kỳ vọng. Quy tắc thực hành: luôn chuẩn hoá dữ liệu trước PCA trừ khi mọi cột đã cùng đơn vị và có ý nghĩa so sánh trực tiếp (ví dụ toàn bộ là pixel ảnh).

5. Thực hành: K-means từng bước & PCA xoay 3D

Demo A — đúng bộ 3 cụm Gaussian của Mục 2. Chọn cách khởi tạo (Tốt/Xấu), rồi bấm Bước để chạy từng vòng assign–update một, xem inertia giảm dần (hoặc mắc kẹt) qua từng lần bấm:

🔵 Demo A — K-means từng bước: khởi tạo quyết định số phận
Đang tải…

Khởi tạo Tốt hội tụ sau 2 bước về inertia ≈ 25,17. Khởi tạo Xấu (cả 3 centroid dồn về 1 cụm) hội tụ sau 5 bước về inertia ≈ 191,54 — tệ hơn 7,6 lần, dù thuật toán chạy hoàn toàn đúng (hành vi đúng, không phải lỗi — đây chính là cạm bẫy cực tiểu cục bộ của Mục 2).

Demo B — 60 điểm 3D gần như nằm phẳng trên một mặt phẳng nghiêng (không trùng trục nào), cộng thêm chút nhiễu vuông góc mặt phẳng đó. Kéo chuột để xoay đám mây điểm, quan sát nó gần như "phẳng" từ đúng góc nhìn; bấm Chiếu xuống 2D để xem PCA lột bỏ chiều nhiễu:

🟣 Demo B — PCA 3D→2D: xoay để thấy mặt phẳng ẩn, rồi chiếu xuống
Đang tải…
Con số thật đằng sau "giữ 98,42% phương sai" (đã kiểm chứng bằng code): chiếu 60 điểm từ 3D xuống 2D (PC1+PC2) rồi tái dựng lại 3D, sai số bình phương trung bình mỗi điểm chỉ $\approx 0{,}139$ — so với tổng biến động gốc mỗi điểm $\approx 8{,}81$. Tỉ lệ lỗi còn lại đúng bằng $1 - 0{,}9842 = 0{,}0158$ — không phải trùng hợp: phần trăm phương sai MẤT ĐI (do bỏ PC3) luôn bằng đúng phần trăm sai số tái dựng, đây là hai cách diễn đạt của cùng một con số toán học.

3 mũi tên là PC1 (đỏ, phương sai lớn nhất) · PC2 (xanh lá) · PC3 (xanh dương, gần như luôn ngắn — đây chính là hướng nhiễu bị PCA phát hiện và loại bỏ khi chiếu xuống 2D).

Đối chiếu công nghiệp: hệ sinh thái Python cho học máy cổ điển (k-means, PCA, hồi quy phi-deep-learning) là scikit-learn (sklearn) — khác với PyTorch chuyên cho deep learning mà series dùng từ Bài 6 trở đi. Bản 1-1 với hai demo trên:

kmeans_pca.py (chạy ngoài browser — cần cài scikit-learn)
# Doi chieu 1-1 voi Demo A/B - cai dat: pip install scikit-learn
from sklearn.cluster import KMeans
from sklearn.decomposition import PCA
import numpy as np

# Demo A: k-means (sklearn dung k-means++ mac dinh - tranh dung cach khoi tao XAU)
X = np.array([...])  # 24 diem, 3 cum Gaussian
km = KMeans(n_clusters=3, init="k-means++", n_init=10, random_state=7)
km.fit(X)
print(km.inertia_)          # ~25.17 - khop Demo A voi khoi tao TOT

# Demo B: PCA giu lai 2 chieu nhieu thong tin nhat trong du lieu 3D
X3d = np.array([...])  # 60 diem gan phang trong khong gian 3D
pca = PCA(n_components=2)
X2d = pca.fit_transform(X3d)
print(pca.explained_variance_ratio_.sum())  # ~0.9842 - khop Demo B (giu 98.42%)

Tóm lược

  • 3 nhánh học máy phân biệt bởi dữ liệu cho máy được gì: có nhãn, không nhãn, hay chỉ phần thưởng thưa — phần lớn dữ liệu doanh nghiệp thuộc nhánh không nhãn.
  • K-means hội tụ CHẮC CHẮN (inertia không bao giờ tăng, có cận dưới $0$) nhưng không chắc chắn tối ưu toàn cục — khởi tạo xấu khiến kết quả tệ hơn tới $7{,}6$ lần dù thuật toán chạy hoàn toàn đúng; k-means++ và nhiều lần khởi tạo lại là cách giảm rủi ro.
  • Elbow method chọn $k$ bằng cách tìm điểm lợi ích biên giảm mạnh — hữu ích nhưng chủ quan trên dữ liệu đời thực không sạch.
  • K-means thất bại trên cụm phi lồi (vòng tròn đồng tâm cho purity $50/50$, ngang đoán ngẫu nhiên) vì nó chỉ vẽ được ranh giới thẳng — baseline nhanh, không phải câu trả lời vạn năng.
  • PCA tìm hướng phương sai lớn nhất bằng power iteration (nhân lặp ma trận-vector, không cần đại số tuyến tính hình thức) — 6 bước hội tụ chính xác trên ví dụ tính tay; giữ 2/3 chiều mà vẫn giữ $98{,}42\%$ thông tin trên dữ liệu 3D gần phẳng.
  • Quên chuẩn hoá trước PCA khiến cột đơn vị lớn (lương tính bằng đồng) nuốt chửng hoàn toàn cột đơn vị nhỏ (tuổi) — luôn z-score hoá trước khi chạy PCA trên dữ liệu đa đơn vị.

Tải file code thực hành minh họa bài học

File JavaScript gồm K-means (assign–update, inertia) và PCA (power iteration, deflation cho PCA đa chiều) của cả 2 demo, với self-test đối chiếu đúng mọi con số trích dẫn trong bài (chạy node kmeans_pca.js):

Tải về kmeans_pca.js

📖 Tài liệu tham khảo

Bài viết liên quan trong series

Bài 3: Phân loại & hồi quy logistic Bài 5: Tensor engine mini Quay lại Lộ trình Series Trí Tuệ Nhân Tạo

Bình luận