Phép toán ma trận¶
Các phép toán ma trận là động cơ tính toán của học sâu. File này bao quát phép cộng ma trận, nhân vô hướng, tích ma trận-vector, nhân ma trận, phép toán phần tử, tích Kronecker, và broadcasting — những phép toán đứng sau mọi lượt truyền xuôi và cập nhật gradient.
-
Ma trận có thể được cộng và co giãn giống như vector.
-
Để cộng, cả hai ma trận phải có cùng kích thước, và bạn cộng từng phần tử:
- Để nhân vô hướng, bạn nhân mọi phần tử với vô hướng:
- Điều đơn giản nhất bạn có thể làm với một ma trận là nhân nó với một vector. Phép nhân ma trận-vector \(A\mathbf{x}\) kết hợp các cột của \(A\) dùng các mục của \(\mathbf{x}\) làm trọng số:
-
Đây là phép toán cốt lõi trong ML. Mọi tầng mạng nơ-ron đều tính \(A\mathbf{x} + \mathbf{b}\): một ma trận nhân với vector đầu vào, cộng với độ lệch (bias).
-
Trường hợp tổng quát là nhân ma trận. Với \(A\) (\(m \times n\)) và \(B\) (\(n \times p\)), tích \(C = AB\) là ma trận \(m \times p\) với mỗi phần tử là một tích vô hướng:
-
Mỗi phần tử trong kết quả là tích vô hướng của một hàng từ \(A\) với một cột từ \(B\). Các chiều bên trong phải khớp (\(n\)), và kết quả lấy các chiều bên ngoài (\(m \times p\)).
-
Một cách khác để nhìn nhận: mỗi cột của kết quả là một tổng có trọng số của các cột của \(A\), với các trọng số đến từ cột tương ứng của \(B\).
-
Nếu \(B\) có cột \([2, 3]^T\), cột kết quả là \(2 \times (\text{cột 1 của } A) + 3 \times (\text{cột 2 của } A)\).
-
Một trường hợp đặc biệt hữu ích: nhân một ma trận với chuyển vị của nó luôn cho một ma trận vuông. \(AA^T\) là \(m \times m\) và \(A^TA\) là \(n \times n\):
-
Nhân ma trận có các quy tắc quan trọng:
- Không giao hoán: \(AB \neq BA\) nói chung. Thứ tự có ý nghĩa.
- Kết hợp: \((AB)C = A(BC)\). Bạn có thể nhóm các phép nhân tùy ý.
- Phân phối: \(A(B + C) = AB + AC\).
- Đơn vị: \(AI = IA = A\).
-
Tích Hadamard (tích phần tử) nhân hai ma trận cùng kích thước mục với mục, được viết \(A \odot B\):
-
Không giống như nhân ma trận tiêu chuẩn, tích Hadamard có tính giao hoán (\(A \odot B = B \odot A\)) và yêu cầu cả hai ma trận có cùng kích thước. Nó được dùng nhiều trong ML để định cổng (gating): nhân phần tử với một mặt nạ các giá trị giữa 0 và 1 điều khiển bao nhiêu phần trăm của mỗi phần tử "đi qua."
-
Tích ngoài (outer product) của hai vector \(\mathbf{u}\) và \(\mathbf{v}\) tạo ra một ma trận: \(\mathbf{u}\mathbf{v}^T\). Mỗi phần tử là tích của một phần tử từ \(\mathbf{u}\) và một từ \(\mathbf{v}\):
-
Kết quả luôn có hạng 1, vì mọi hàng là một phiên bản co giãn của \(\mathbf{v}^T\). Bất kỳ ma trận nào cũng có thể được viết dưới dạng tổng các tích ngoài hạng-1, điều chính xác mà SVD thực hiện (xem phần phân rã).
-
Nhân ma trận rất tốn kém về mặt tính toán. Nhân hai ma trận \(n \times n\) cần \(O(n^3)\) phép tính. Với ma trận \(1000 \times 1000\), đó là một tỷ phép nhân.
-
Khi ma trận thưa (hầu hết là không), phép nhân ngây thơ lãng phí thời gian nhân với không. Định dạng CSR (Compressed Sparse Row) chỉ lưu các phần tử khác không cùng với vị trí của chúng:
- Giá trị: các mục khác không theo thứ tự hàng
- Chỉ mục cột: mỗi giá trị thuộc cột nào
- Độ lệch hàng: mỗi hàng bắt đầu ở đâu trong danh sách giá trị
-
Ví dụ, ma trận:
-
Được lưu dưới dạng: values = [5, 2, 3, -1], columns = [0, 3, 2, 3], row offsets = [0, 2, 3, 4]. Cách này bỏ qua tất cả các số không và làm cho các phép toán thưa nhanh hơn nhiều.
-
Một ứng dụng cốt lõi của ma trận là giải hệ phương trình tuyến tính. Hệ \(A\mathbf{x} = \mathbf{b}\) hỏi: "vector \(\mathbf{x}\) nào, khi được biến đổi bởi \(A\), tạo ra \(\mathbf{b}\)?"
-
Ví dụ, giả sử bạn đang mua trái cây. Táo có giá \(x_1\) đôla mỗi quả và chuối có giá \(x_2\) đôla mỗi quả. Bạn biết rằng 2 quả táo và 1 quả chuối có giá $5, và 1 quả táo và 3 quả chuối có giá $10. Dưới dạng ma trận:
- Nhân ma trận với vector, từng hàng một (mỗi hàng tự nhân với \([x_1, x_2]^T\)) cho hai phương trình:
-
Từ hàng 1, \(x_2 = 5 - 2x_1\). Thay vào hàng 2: \(x_1 + 3(5 - 2x_1) = 10\), suy ra \(x_1 = 1\), rồi \(x_2 = 3\). Táo có giá $1 và chuối có giá $3.
-
Hãy xác minh - nó đúng:
-
Nếu \(A\) có nghịch đảo, lời giải đơn giản là \(\mathbf{x} = A^{-1}\mathbf{b}\). Nhưng tính toán nghịch đảo trực tiếp vừa đắt đỏ vừa không ổn định về số. Trong thực tế, ta dùng các phép phân rã để thay thế.
-
Không phải mọi ma trận đều vuông, và không phải mọi ma trận vuông đều khả nghịch. Giả nghịch đảo (pseudo-inverse) \(A^+\) tổng quát hóa nghịch đảo cho bất kỳ ma trận nào. Nó luôn tồn tại và cung cấp nghịch đảo "tốt nhất có thể":
-
Khi \(A\) là tam giác dưới, giải \(L\mathbf{x} = \mathbf{b}\) rất dễ bằng thế xuôi (forward substitution): giải cho \(x_1\) trước, rồi dùng nó để tìm \(x_2\), và cứ thế xuống dưới.
-
Khi \(A\) là tam giác trên, giải \(U\mathbf{x} = \mathbf{b}\) hoạt động bằng thế ngược (back substitution): giải cho biến cuối cùng trước, rồi làm ngược lên.
-
Đây là lý do tại sao phân rã một ma trận thành các nhân tử tam giác (như ta sẽ thấy ở phần phân rã) lại hữu ích như vậy. Nó biến một bài toán khó thành hai bài toán dễ.
Bài tập lập trình (dùng CoLab hoặc notebook)¶
-
Nhân hai ma trận và xác minh các kích thước. Sau đó hoán đổi thứ tự và quan sát kết quả thay đổi (hoặc nó thất bại nếu các kích thước không khớp).
-
Giải một hệ phương trình tuyến tính \(A\mathbf{x} = \mathbf{b}\) và xác minh lời giải bằng cách nhân lại. Thử thay đổi \(\mathbf{b}\) để xem lời giải dịch chuyển như thế nào.