Mở đầu: mọi lỗi PyTorch bạn từng gặp đều là lỗi shape

Nếu bạn từng nghịch PyTorch hay TensorFlow, gần như chắc chắn bạn từng dính lỗi đỏ chót RuntimeError: size mismatch hoặc tệ hơn — không lỗi gì cả, chỉ là model huấn luyện xong cho kết quả vô nghĩa. Cả hai tình huống đều bắt nguồn từ đúng một khái niệm: shape (hình dạng) của tensor. Bốn bài trước, mọi "dữ liệu" chỉ là vài con số hoặc cặp toạ độ $(x, y)$ — đủ đơn giản để không cần nghĩ về shape. Từ bài này trở đi, dữ liệu là tensor nhiều chiều, và school hiểu sai một chiều duy nhất có thể khiến cả pipeline chạy "bình thường" nhưng học ra thứ hoàn toàn sai.

Bài này tự xây một tensor engine tối giản từ số 0 — không dùng thư viện nào — để bạn thấy tận mắt shape chỉ là cách diễn giải một dải số phẳng trong bộ nhớ. Bạn sẽ tự tay tạo ra cạm bẫy nguy hiểm nhất của cả chương (một phép cộng sai shape "âm thầm" chạy được, không báo lỗi, và bạn sẽ tận mắt xem gradient descent vẫn giảm loss trong khi học ra tham số sai), rồi đo bằng số thật vì sao bỏ qua lớp trừu tượng tiện lợi lại nhanh hơn tới hơn chục lần. Kết thúc bài, mã nguồn viết ra chính là NeuroJS — engine sẽ được tái sử dụng nguyên vẹn ở mọi bài còn lại của series.


📚 Điều kiện tiên quyết
Bắt buộc: Bài 1 (khái niệm model là hàm số trên dữ liệu) và JavaScript căn bản (mảng, vòng lặp, class). Không cần Bài 2 hay Bài 3/Bài 4 — bài này độc lập, chỉ dùng lại đúng 1 công thức GD ở Mục 3 để minh hoạ cạm bẫy. Tài nguyên ngoài: NumPy — Broadcasting rules, PyTorch — Broadcasting semantics.

1. Vì sao mọi thứ đều là tensor

"Tensor" nghe huyền bí nhưng chỉ là một từ tổng quát cho: vô hướng (1 con số) → vector (dãy số 1 chiều) → ma trận (bảng số 2 chiều) → tensor bậc cao hơn (3, 4 chiều trở lên). Điểm chung: tất cả chỉ là số được sắp theo một số chiều (dimension) nào đó, và con số đó — gọi là bậc (rank) hay shape — chính là "ngôn ngữ chung" mà mọi framework deep learning dùng để mô tả dữ liệu VÀ mô tả model:

Bậc (rank) Tên gọi Ví dụ cụ thể Gặp ở đâu trong series
0 Vô hướng (scalar) Giá trị loss sau 1 epoch — đúng 1 con số Bài 1–4 (mọi loss)
1 Vector 1 dòng đặc trưng của 1 mẫu dữ liệu, hoặc 1 hàng trọng số Bài 1–4
2 Ma trận Cả batch dữ liệu (N mẫu × M đặc trưng), hoặc ma trận trọng số 1 lớp Bài 6 (MLP)
3 Tensor bậc 3 Batch câu văn: (batch × độ dài chuỗi × chiều embedding) Bài 13–14 (RNN, Transformer)
4 Tensor bậc 4 Batch ảnh: (batch × kênh màu × cao × rộng) Bài 11 (CNN)

Sự thật quan trọng nhất bài, đáng nhớ hơn mọi công thức: một model deep learning chỉ là một chuỗi phép toán trên tensor — nhân ma trận, cộng, phi tuyến, lặp lại. Không có phép màu nào khác. Và vì mọi phép toán đều yêu cầu shape đầu vào khớp theo quy tắc nhất định, hiểu rõ shape giải quyết được phần lớn lỗi khi debug deep learning — thói quen số 1 của người viết model thành thạo là in ra .shape sau MỌI phép toán khi có gì đó khả nghi, trước khi nghi ngờ bất cứ thứ gì khác.

2. Xây tensor trên Float32Array phẳng + shape/stride

Cách hiện thực ngây thơ nhất cho ma trận trong JS là mảng lồng mảng: [[1,2,3],[4,5,6]]. Vấn đề: mỗi hàng con là một object riêng nằm rải rác trong bộ nhớ (không liền mạch), và mỗi số là một giá trị "boxed" (bọc thêm metadata) thay vì số thô. Tensor thật sự (NumPy, PyTorch, và NeuroJS ở đây) dùng cách khác hẳn: một dải số PHẲNG duy nhất (Float32Array), cộng với 2 mảng nhỏ mô tả cách "đọc" dải số đó theo nhiều chiều — shape (kích thước từng chiều) và stride (bước nhảy trong bộ nhớ ứng với mỗi chiều).

Với layout row-major (chuẩn của NumPy/PyTorch — duyệt hết 1 hàng rồi mới sang hàng kế), công thức stride cực đơn giản: chiều cuối cùng luôn có stride $1$ (đi 1 bước trong chiều đó = đi 1 ô trong bộ nhớ), còn stride của mỗi chiều còn lại = stride chiều kế tiếp nhân với kích thước chiều kế tiếp:

$$\text{stride}[i] = \text{stride}[i+1] \times \text{shape}[i+1] \qquad (\text{stride cuối} = 1)$$

Shape Stride tính ra Ý nghĩa
$(2, 3)$ $(3, 1)$ Đi 1 hàng (chiều 0) = nhảy 3 ô; đi 1 cột (chiều 1) = nhảy 1 ô
$(2, 3, 4)$ $(12, 4, 1)$ 3 chiều lồng nhau: mỗi "khối" 2D con chiếm $3 \times 4 = 12$ ô
🔢 Tính tay stride cho đúng shape 4D "batch ảnh" của Bài 11
Bảng Mục 1 nhắc tới tensor bậc 4 (batch × kênh × cao × rộng) dùng cho CNN — thử tính stride cho shape cụ thể $(2, 3, 4, 5)$ (2 ảnh, 3 kênh màu, cao 4, rộng 5), áp đúng công thức đệ quy từ phải sang trái: $\text{stride}[3] = 1$ (chiều cuối luôn là $1$); $\text{stride}[2] = 1 \times 5 = 5$; $\text{stride}[1] = 5 \times 4 = 20$; $\text{stride}[0] = 20 \times 3 = 60$. Kết quả: $\text{stride} = (60, 20, 5, 1)$. Kiểm tra chéo: tổng phần tử $= \text{stride}[0] \times \text{shape}[0] = 60 \times 2 = 120$, đúng bằng $2 \times 3 \times 4 \times 5 = 120$ — công thức tự nhất quán. Đọc theo trực giác: đi 1 bước theo chiều batch (chọn ảnh tiếp theo) nhảy $60$ ô nhớ vì mỗi ảnh chiếm trọn $3 \times 4 \times 5 = 60$ số; đi 1 bước theo chiều kênh màu nhảy $20$ ô vì mỗi kênh chiếm $4 \times 5 = 20$ số.

Truy cập phần tử tại chỉ số $(i_0, i_1, \ldots)$ chỉ là một phép nhân-cộng: nhân từng chỉ số với stride tương ứng rồi cộng lại — chính là _flatIndex() trong file tải về cuối bài. Sức mạnh thật sự lộ ra ở phép transpose: đảo ngược shape THÌ CŨNG đảo ngược đúng stride tương ứng — không hề đụng vào dải số gốc:

transpose_view.js (đúng logic trong ai-neuro.js)
transpose(perm) {
  const p = perm || this.shape.map((_, i) => i).reverse();
  return new Tensor(
    this.data,                    // CUNG mot buffer - khong sao chep gi ca
    p.map(i => this.shape[i]),    // shape MOI: hoan doi thu tu
    p.map(i => this.strides[i]),  // stride MOI: hoan doi cung thu tu
    this.offset
  );
}
// shape (2,3) stride (3,1) -> transpose -> shape (3,2) stride (1,3)
// t.get(1,0) sau transpose == goc.get(0,1) — DUNG gia tri, KHONG di chuyen 1 byte nao

Vì chỉ đổi 2 mảng nhỏ (shape, stride) chứ không đụng dải số gốc, transpose có độ phức tạp $O(1)$ — tốn thời gian y hệt nhau dù tensor có 6 phần tử hay 1 triệu phần tử. Đo trực tiếp: transpose một tensor $1000 \times 1000$ (1 triệu phần tử) mất $0{,}016$ mili giây — chỉ là tạo 1 object mới, không lặp qua phần tử nào cả.

⚠️ Cạm bẫy: sau transpose, dữ liệu không còn "liền mạch" — reshape phải materialize lại

Cái giá của transpose "miễn phí": tensor kết quả không còn contiguous (liền mạch) — nghĩa là thứ tự stride không còn khớp layout row-major mặc định cho shape mới của nó. Nhiều phép toán (đặc biệt reshape) buộc phải giả định dữ liệu liền mạch để diễn giải đúng — gọi reshape() trên một tensor vừa transpose sẽ ném lỗi rõ ràng thay vì âm thầm cho kết quả sai:

reshape_pitfall.js
const t = Tensor.fromNested([[1,2,3],[4,5,6]]);   // shape (2,3), CONTIGUOUS
const tt = t.transpose();                          // shape (3,2), KHONG con contiguous
tt.reshape([6]);
// -> throw: "reshape() yeu cau tensor CONTIGUOUS - goi .contiguous() truoc"

const fixed = tt.contiguous().reshape([6]);        // dung: materialize roi moi reshape
// fixed = [1, 4, 2, 5, 3, 6] — DUNG thu tu sau transpose, khong phai [1,2,3,4,5,6] goc

.contiguous() sao chép dải số về đúng thứ tự row-major mới — đây là phép $O(n)$ thật sự, đối lập hẳn transpose $O(1)$: cùng tensor $1000 \times 1000$ ở trên, materialize mất $3{,}14$ mili giây — chậm hơn transpose gần 200 lần, vì giờ mới thật sự phải đọc và ghi lại 1 triệu phần tử. Bài học thực hành: transpose/view "gần như miễn phí", nhưng bất kỳ phép nào giả định bộ nhớ liền mạch phía sau nó đều phải trả giá $O(n)$ — biết phân biệt 2 loại chi phí này là kỹ năng tối ưu hoá cơ bản nhất khi làm việc với tensor.

3. Broadcasting: tiện lợi và cạm bẫy nguy hiểm nhất chương

Bài toán thực tế cực phổ biến: cộng bias (1 vector, vd shape $(3,)$) vào CẢ một batch dữ liệu (shape $(4,3)$ — 4 mẫu, 3 đặc trưng mỗi mẫu) mà không cần lặp tay từng dòng. Broadcasting giải đúng bài này bằng quy tắc căn phải (right-align): so 2 shape từ chiều cuối về đầu; hai chiều tương thích nếu bằng nhau HOẶC một trong hai bằng $1$ (chiều $1$ sẽ tự "nhân bản" để khớp chiều còn lại); chiều thiếu ở đầu coi như $1$. Cộng bias vào batch, hoạt động đúng như kỳ vọng:

Batch (4,3) + Bias (3,) = Kết quả (4,3)
[1,2,3] / [4,5,6] / [7,8,9] / [10,11,12] [10,20,30] (nhân bản 4 lần) [11,22,33] / [14,25,36] / [17,28,39] / [20,31,42]

Đúng như kỳ vọng — bias $(3,)$ được "trải" ra cộng vào cả 4 dòng. Nhưng cơ chế "chiều thiếu coi như 1" chính là con dao hai lưỡi:

⚠️ Cạm bẫy nguy hiểm nhất chương: broadcast ÂM THẦM che lỗi shape — không hề báo lỗi, loss vẫn giảm nhưng model học sai tham số

Giả sử bạn định cộng elementwise 2 vector cùng shape $(3,)$, nhưng một trong hai vô tình còn giữ 1 chiều thừa dạng cột — shape $(3,1)$ thay vì $(3,)$ (rất dễ xảy ra sau một phép slicing/reshape trước đó). Theo đúng quy tắc căn phải, $(3,1)$ và $(1,3)$ được coi là hoàn toàn hợp lệ — không ném lỗi — và cho ra kết quả shape $(3,3)$:

broadcast_silent_bug.js (chạy đúng trong self-test file tải về)
const col = Tensor.fromNested([[1],[2],[3]]);      // shape (3,1) - Y DINH la (3,)
const row = Tensor.fromNested([[10,20,30]]);       // shape (1,3) - Y DINH la (3,)
add(col, row);
// KHONG throw loi gi ca! Ket qua shape (3,3):
// [[11,21,31],
//  [12,22,32],
//  [13,23,33]]
// -- thay vi (3,) ma nguoi viet code DINH tinh: [11,22,33]

Không TypeError, không cảnh báo — chỉ là một kết quả có shape khác hoàn toàn ý định, và pipeline vẫn chạy tiếp "bình thường". Để chứng minh mức độ nguy hiểm bằng số thật, mô phỏng một vòng lặp gradient descent thật (công thức Bài 2) fit $\hat{y} = wx$ trên 3 điểm $(x,y) = (1,2), (2,4), (3,7)$, so sánh loss đúng (elementwise, shape $(3,)$) với loss lỗi (do $y$ vô tình mang shape $(3,1)$, broadcast ra $(3,3)$):

Bước GD Loss ĐÚNG (elementwise) Loss LỖI (outer-broadcast $3{\times}3$)
0 $23{,}000$ $23{,}000$
1 $6{,}627$ $11{,}483$
2 $1{,}970$ $8{,}207$
10 $0{,}119$ $6{,}905$
30 (hội tụ) $0{,}119$ $6{,}905$

Cả hai loss đều giảm đều đặn qua từng bước — nhìn biểu đồ loss, không có tín hiệu nào cho thấy có gì sai. Nhưng tham số hội tụ về hai giá trị KHÁC NHAU: $w = 2{,}2143$ cho loss đúng (khớp chính xác nghiệm giải tích least-squares) so với $w = 1{,}8571$ cho loss lỗi — lệch tới $16\%$. Model "lỗi" vẫn huấn luyện trơn tru, vẫn cho một con số loss giảm dần đẹp mắt, và âm thầm học ra một tham số sai hoàn toàn mà không có dấu hiệu cảnh báo nào ngoài việc tự tay kiểm tra shape.

Quy tắc thực hành sống còn: in .shape ra ngay sau MỌI phép toán khi viết pipeline mới, đặc biệt trước một phép reduction (mean, sum) — nếu shape trước reduction không đúng như bạn hình dung, dừng lại kiểm tra ngay, đừng đợi tới khi thấy kết quả cuối "trông sai sai".

4. Matmul: công thức, và vì sao lớp trừu tượng có giá của nó

Phép toán trung tâm của mọi mạng neural — nhân ma trận (matmul). Với $A$ shape $(m,k)$ và $B$ shape $(k,n)$, kết quả $C = AB$ có shape $(m,n)$:

$$C_{ij} = \sum_{p=1}^{k} A_{ip} B_{pj}$$

Ba vòng lặp lồng nhau ($i$, $j$, $p$) cho độ phức tạp $O(m \cdot n \cdot k)$ — thường viết gọn $O(n^3)$ khi các chiều cùng cỡ $n$. Câu hỏi thật sự không phải "công thức là gì" (chỉ 1 dòng) mà là viết code thế nào cho nhanh. Trực giác phổ biến: "dùng mảng phẳng (Float32Array) thay vì mảng lồng nhau sẽ nhanh hơn hẳn nhờ cache tốt hơn, không boxing". Đo thẳng bằng số thay vì tin trực giác — so mảng lồng nhau (nested array) với mảng phẳng cùng thứ tự vòng lặp, cùng cỡ $n=200$:

Cách hiện thực Thời gian (n=200)
Mảng lồng nhau (Array of Array) $10{,}47$ ms
Float32Array phẳng, cùng thứ tự vòng lặp $9{,}17$ ms — chỉ nhanh hơn $1{,}1$ lần

Bất ngờ: gần như không khác biệt. JIT compiler của V8 (engine chạy JS trong Chrome/Node) đủ thông minh để tối ưu mảng lồng nhau "packed" (toàn số, không lỗ hổng) gần bằng mảng phẳng trên phần cứng hiện đại — trực giác "mảng phẳng luôn nhanh hơn nhiều" không còn đúng như 10 năm trước. Vậy chi phí thật sự nằm ở đâu? Đo lại, lần này so API tiện lợi tensor.get(i,j)/.set(i,j,v) (dùng tham số rest + vòng lặp tính stride mỗi lần gọi — đúng API Mục 2) với việc đọc/ghi trực tiếp trên Float32Array bằng công thức data[i*n+j] đã tính sẵn:

Cách truy cập phần tử Thời gian (n=200, matmul đầy đủ)
Qua get()/set() (tham số rest, tính stride mỗi lần) $105{,}75$ ms
Chỉ số phẳng tính sẵn, đọc/ghi trực tiếp $9{,}16$ ms — nhanh hơn $11{,}5$ lần

Đây mới là khoảng cách "hàng chục lần" thật sự tồn tại — nhưng nó tới từ chi phí gọi hàm và tham số rest/spread lặp lại hàng triệu lần (với $n=200$, matmul gọi get() tới $200^3 \times 2 = 16$ triệu lần), không phải từ bản thân "mảng phẳng vs mảng lồng". Đây chính xác là lý do matmul() trong ai-neuro.js đọc/ghi trực tiếp trên Float32Array ở vòng lặp trong cùng, không đi qua get()/set() — và cũng là lý do PyTorch/NumPy hiện thực các phép "hot path" (matmul, convolution) bằng kernel C/C++/CUDA cấp thấp thay vì lớp Python/API tiện lợi mà người dùng thấy. Bài học tổng quát: lớp trừu tượng đẹp không miễn phí — nó chỉ đáng đánh đổi khi không nằm trên đường găng hiệu năng.

🧠 $O(n^3)$ trông trừu tượng thế nào khi $n$ tăng gấp đôi?
Đo trực tiếp cùng một hàm matmul chỉ-số-phẳng (không qua get()/set()) ở 3 cỡ $n$ khác nhau (đã kiểm chứng bằng Node, lấy thời gian tốt nhất trong 3 lần chạy):
  • $n = 100$: $\approx 1{,}64$ ms.
  • $n = 200$ (gấp đôi): $\approx 10{,}65$ ms — nhanh hơn dự đoán lý thuyết đôi chút do JIT warm-up, nhưng đã gấp $\approx 6{,}5$ lần.
  • $n = 400$ (gấp đôi tiếp): $\approx 83{,}89$ ms — gấp $\approx 7{,}9$ lần so với $n=200$, rất sát dự đoán lý thuyết $2^3 = 8$ lần của độ phức tạp $O(n^3)$.
Đây là hệ quả trực tiếp và cụ thể của công thức độ phức tạp: tăng gấp đôi kích thước mỗi chiều không làm chi phí tăng gấp đôi mà tăng theo lập phương — đúng lý do các mô hình deep learning hiện đại phải chạy trên GPU (hàng nghìn lõi tính song song) thay vì CPU một khi ma trận đủ lớn, và cũng lý giải vì sao tăng kích thước batch/hidden layer "gấp đôi cho chắc" trong thực hành có thể khiến thời gian huấn luyện tăng vọt ngoài dự kiến.
🚀 Muốn nhanh hơn NỮA thì sao?
$11{,}5$ lần đã là nhiều, nhưng vẫn chạy tuần tự trên 1 lõi CPU. Bước nhảy vọt thật sự tới từ song song hoá quy mô lớn: Series WebGPU dạy chạy matmul trên compute shader — hàng nghìn luồng GPU tính song song thay vì 1 luồng CPU tuần tự, nhanh hơn CPU thêm hàng chục tới hàng trăm lần nữa trên ma trận đủ lớn. NeuroJS ở đây chọn CPU/JS vì mục tiêu là hiểu cơ chế, không phải tốc độ tối đa.

5. Thực hành: khởi sinh NeuroJS

Toàn bộ engine viết trong bài này được đóng gói vào ai-neuro.js — file DUY NHẤT sẽ được import ở mọi bài học sau của series (Bài 7 thêm autograd, Bài 9 thêm optimizer, Bài 11 thêm Conv2D, Bài 14 thêm attention), không viết lại logic tensor từ đầu lần nào nữa:

API Chức năng
Tensor.zeros(shape) / .fromNested(arr) Tạo tensor mới (toàn 0, hoặc từ mảng lồng nhau)
t.get(...idx) / .set(...idx, v) Đọc/ghi 1 phần tử theo chỉ số nhiều chiều
t.transpose(perm?) Hoán vị chiều — trả về VIEW, $O(1)$
t.contiguous() Materialize thành buffer liền mạch mới — $O(n)$
t.reshape(newShape) Đổi shape (yêu cầu contiguous, ném lỗi nếu không)
add(a, b) / mul(a, b) Cộng/nhân elementwise, có broadcasting
matmul(a, b) Nhân ma trận 2D, tự materialize input nếu cần

Demo A — máy tính broadcast: nhập 2 shape (hoặc bấm nút mẫu), xem shape kết quả (hoặc thông báo lỗi nếu không tương thích), và với shape nhỏ, xem luôn bảng số outer-broadcast để cảm nhận trực tiếp cạm bẫy Mục 3:

🧮 Demo A — Máy tính Broadcast: shape nào hợp lệ, shape nào là cạm bẫy?
Đang tải…

Demo B — benchmark matmul sống: chọn cỡ ma trận $n$, bấm chạy để đo trực tiếp trên máy bạn cả hai cách (qua get()/set() và chỉ số phẳng trực tiếp) — số đo có thể khác máy tôi dùng viết bài, nhưng tỉ lệ chênh lệch phải cùng bậc độ lớn:

⚡ Demo B — Benchmark Matmul: get()/set() vs chỉ số phẳng trực tiếp
Chọn cỡ $n$ rồi bấm Chạy benchmark.

Cỡ càng lớn, vòng lặp get()/set() càng chậm rõ rệt vì số lần gọi hàm tăng theo $n^3$ — trong khi chỉ số phẳng trực tiếp chỉ tăng theo chi phí bộ nhớ thuần tuý.

Đối chiếu công nghiệp: PyTorch dùng đúng khái niệm tensor + shape + stride + broadcasting này (không phải phát minh riêng của bài — đây là chuẩn chung mọi framework), chỉ khác là các phép "hot path" chạy bằng kernel C++/CUDA thay vì JS:

tensor_engine.py (chạy ngoài browser — cần cài PyTorch)
# Doi chieu 1-1 voi ai-neuro.js - cai dat: pytorch.org/get-started/locally
import torch

batch = torch.tensor([[1.,2.,3.],[4.,5.,6.],[7.,8.,9.],[10.,11.,12.]])  # shape (4,3)
bias  = torch.tensor([10.,20.,30.])                                     # shape (3,)
print((batch + bias).shape)       # torch.Size([4, 3]) - broadcasting giong het NeuroJS

col = torch.tensor([[1.],[2.],[3.]])   # shape (3,1)
row = torch.tensor([[10.,20.,30.]])    # shape (1,3)
print((col + row).shape)          # torch.Size([3, 3]) - CUNG cam bay outer-broadcast!
                                   # PyTorch KHONG bao loi o day - giong het NeuroJS

A = torch.tensor([[1.,2.],[3.,4.]])
B = torch.tensor([[5.,6.],[7.,8.]])
print(A @ B)                      # toan tu @ la matmul - ket qua [[19,22],[43,50]]
print(A.T.is_contiguous())        # False - transpose la view, giong het Muc 2

Tóm lược

  • Mọi thứ là tensor: vô hướng → vector → ma trận → tensor bậc cao, phân biệt bởi shape; hiểu shape giải quyết được phần lớn lỗi debug deep learning.
  • Tensor = dữ liệu phẳng + shape + stride; row-major cho công thức stride đơn giản (chiều cuối luôn $=1$); transpose chỉ hoán đổi shape/stride nên là $O(1)$, không đụng dữ liệu gốc.
  • Materialize (contiguous) là $O(n)$ thật sự — chậm hơn transpose gần $200$ lần trên tensor $1$ triệu phần tử; reshape bắt buộc dữ liệu liền mạch, nên ném lỗi rõ ràng thay vì âm thầm sai.
  • Broadcasting căn phải, chiều $1$ tự nhân bản — tiện cho bias+batch, nhưng không hề báo lỗi khi shape sai kiểu $(3,1)$ vs $(1,3)$; verified bằng GD thật: loss cả hai đều giảm, nhưng tham số lệch $16\%$ so với nghiệm đúng.
  • Matmul $O(n^3)$; khoảng cách tốc độ thật không nằm ở "mảng phẳng vs mảng lồng" (chỉ $1{,}1$ lần) mà ở chi phí lớp trừu tượng get()/set() so với chỉ số phẳng trực tiếp ($11{,}5$ lần) — lý do các framework thật viết hot path bằng kernel cấp thấp.
  • NeuroJS khởi sinh (ai-neuro.js): Tensor, add/mul (broadcasting), matmul, transpose/contiguous/reshape — dùng lại nguyên vẹn từ Bài 7 trở đi.

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

ai-neuro.js — chính là NeuroJS, engine tensor dùng chung cho toàn bộ series từ đây trở đi. Chứa Tensor (shape/stride/transpose/contiguous/reshape), broadcasting, matmul, và self-test đối chiếu mọi con số trích dẫn trong bài (chạy node ai-neuro.js):

Tải về ai-neuro.js

📖 Tài liệu tham khảo

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

Bài 4: Học không giám sát: K-means & PCA Bài 6: Neuron & mạng MLP Quay lại Lộ trình Series Trí Tuệ Nhân Tạo

Bình luận