2.3. Đại số tuyến tính

Bây giờ bạn đã có thể lưu trữ và xử lý dữ liệu, hãy cùng ôn qua những kiến thức đại số tuyến tính cần thiết để hiểu và lập trình hầu hết các mô hình được nhắc tới trong quyển sách này. Dưới đây, chúng tôi giới thiệu các đối tượng toán học, số học và phép tính cơ bản trong đại số tuyến tính, biểu diễn chúng bằng cả ký hiệu toán học và cách triển khai lập trình tương ứng.

2.3.1. Số vô hướng

Nếu bạn chưa từng học đại số tuyến tính hay học máy, có lẽ bạn mới chỉ có kinh nghiệm làm toán với từng con số riêng lẻ. Và nếu bạn đã từng phải cân bằng sổ thu chi hoặc đơn giản là trả tiền cho bữa ăn, thì hẳn bạn đã biết cách thực hiện các phép tính cơ bản như cộng trừ nhân chia các cặp số. Ví dụ, nhiệt độ tại Palo Alto là \(52\) độ Fahrenheit. Chúng ta gọi các giá trị mà chỉ bao gồm một số duy nhất là số vô hướng (scalar). Nếu bạn muốn chuyển giá trị nhiệt độ trên sang độ Celsius (thang đo nhiệt độ hợp lý hơn theo hệ mét), bạn sẽ phải tính biểu thức \(c = \frac{5}{9}(f - 32)\) với giá trị \(f\) bằng \(52\). Trong phương trình trên, mỗi số hạng — \(5\), \(9\)\(32\) — là các số vô hướng. Các ký hiệu \(c\)\(f\) được gọi là biến và chúng biễu diễn các giá trị số vô hướng chưa biết.

Trong quyển sách này, chúng tôi sẽ tuân theo quy ước ký hiệu các biến vô hướng bằng các chữ cái viết thường (chẳng hạn \(x\), \(y\)\(z\)). Chúng tôi ký hiệu không gian (liên tục) của tất cả các số thực vô hướng là \(\mathbb{R}\). Vì tính thiết thực, chúng tôi sẽ bỏ qua định nghĩa chính xác của không gian. Nhưng bạn cần nhớ \(x \in \mathbb{R}\) là cách toán học để thể hiện \(x\) là một số thực vô hướng. Ký hiệu \(\in\) đọc là “thuộc” và đơn thuần biểu diễn việc phần tử thuộc một tập hợp. Tương tự, ta có thể viết \(x, y \in \{0, 1\}\) để ký hiệu cho việc các số \(x\)\(y\) chỉ có thể nhận giá trị \(0\) hoặc \(1\).

Trong mã nguồn MXNet, một số vô hướng được biễu diễn bằng một ndarray với chỉ một phần tử. Trong đoạn mã dưới đây, chúng ta khởi tạo hai số vô hướng và thực hiện các phép tính quen thuộc như cộng, trừ, nhân, chia và lũy thừa với chúng.

from mxnet import np, npx
npx.set_np()

x = np.array(3.0)
y = np.array(2.0)

x + y, x * y, x / y, x ** y
(array(5.), array(6.), array(1.5), array(9.))

2.3.2. Vector

Bạn có thể xem vector đơn thuần như một dãy các số vô hướng. Chúng ta gọi các giá trị đó là phần tử (thành phần) của vector. Khi dùng vector để biễu diễn các mẫu trong tập dữ liệu, giá trị của chúng thường mang ý nghĩa liên quan tới đời thực. Ví dụ, nếu chúng ta huấn luyện một mô hình dự đoán rủi ro vỡ nợ, chúng ta có thể gán cho mỗi ứng viên một vector gồm các thành phần tương ứng với thu nhập, thời gian làm việc, số lần vỡ nợ trước đó của họ và các yếu tố khác. Nếu chúng ta đang tìm hiểu về rủi ro bị đau tim của bệnh nhân, ta có thể biểu diễn mỗi bệnh nhân bằng một vector gồm các phần tử mang thông tin về dấu hiệu sinh tồn gần nhất, nồng độ cholesterol, số phút tập thể dục mỗi ngày, v.v. Trong ký hiệu toán học, chúng ta thường biểu diễn vector bằng chữ cái in đậm viết thường (ví dụ \(\mathbf{x}\), \(\mathbf{y}\), và \(\mathbf{z})\).

Trong MXNet, chúng ta làm việc với vector thông qua các ndarray \(1\)-chiều. Thường thì ndarray có thể có chiều dài bất kỳ, tùy thuộc vào giới hạn bộ nhớ máy tính.

x = np.arange(4)
x
array([0., 1., 2., 3.])

Một phần tử bất kỳ trong vector có thể được ký hiệu sử dụng chỉ số dưới. Ví dụ ta có thể viết \(x_i\) để ám chỉ phần tử thứ \(i\) của \(\mathbf{x}\). Lưu ý rằng phần tử \(x_i\) là một số vô hướng nên nó không được in đậm. Có rất nhiều tài liệu tham khảo xem vector cột là chiều mặc định của vector, và quyển sách này cũng vậy. Trong toán học, một vector có thể được viết như sau

(2.3.1)\[\begin{split}\mathbf{x} =\begin{bmatrix}x_{1} \\x_{2} \\ \vdots \\x_{n}\end{bmatrix},\end{split}\]

trong đó \(x_1, \ldots, x_n\) là các phần tử của vector. Trong mã nguồn, chúng ta sử dụng chỉ số để truy cập các phần tử trong ndarray.

x[3]
array(3.)

2.3.2.1. Độ dài, Chiều, và Kích thước

Hãy quay lại với những khái niệm từ Section 2.1. Một vector đơn thuần là một dãy các số. Mỗi vector, tương tự như dãy, đều có một độ dài. Trong ký hiệu toán học, nếu ta muốn nói rằng một vector \(\mathbf{x}\) chứa \(n\) các số thực vô hướng, ta có thể biểu diễn nó bằng \(\mathbf{x} \in \mathbb{R}^n\). Độ dài của một vector còn được gọi là số chiều của vector.

Cũng giống như một dãy thông thường trong Python, chúng ta có thể xem độ dài của của một ndarray bằng cách gọi hàm len() có sẵn của Python.

len(x)
4

Khi một ndarray biễu diễn một vector (với chính xác một trục), ta cũng có thể xem độ dài của nó qua thuộc tính .shape (kích thước). Kích thước là một tuple liệt kê độ dài (số chiều) dọc theo mỗi trục của ndarray. Với các ndarray có duy nhất một trục, kích thước của nó chỉ có một phần tử.

x.shape
(4,)

Ở đây cần lưu ý rằng, từ “chiều” là một từ đa nghĩa và khi đặt vào nhiều ngữ cảnh thường dễ làm ta bị nhầm lẫn. Để làm rõ, chúng ta dùng số chiều của một vector hoặc của một trục để chỉ độ dài của nó, tức là số phần tử trong một vector hay một trục. Tuy nhiên, chúng ta sử dụng số chiều của một ndarray để chỉ số trục của ndarray đó. Theo nghĩa này, chiều của một trục của một ndarray là độ dài của trục đó.

2.3.3. Ma trận

Giống như vector khái quát số vô hướng từ bậc \(0\) sang bậc \(1\), ma trận sẽ khái quát những vector từ bậc \(1\) sang bậc \(2\). Ma trận thường được ký hiệu với ký tự hoa và được in đậm (ví dụ: \(\mathbf{X}\), \(\mathbf{Y}\), và \(\mathbf{Z}\)); và được biểu diễn bằng các ndarray với \(2\) trục khi lập trình.

Trong ký hiệu toán học, ta dùng \(\mathbf{A} \in \mathbb{R}^{m \times n}\) để biểu thị một ma trận \(\mathbf{A}\) gồm \(m\) hàng và \(n\) cột các giá trị số thực. Về mặt hình ảnh, ta có thể minh họa bất kỳ ma trận \(\mathbf{A} \in \mathbb{R}^{m \times n}\) như một bảng biểu mà mỗi phần tử \(a_{ij}\) nằm ở dòng thứ \(i\) và cột thứ \(j\) của bảng:

(2.3.2)\[\begin{split}\mathbf{A}=\begin{bmatrix} a_{11} & a_{12} & \cdots & a_{1n} \\ a_{21} & a_{22} & \cdots & a_{2n} \\ \vdots & \vdots & \ddots & \vdots \\ a_{m1} & a_{m2} & \cdots & a_{mn} \\ \end{bmatrix}.\end{split}\]

Với bất kỳ ma trận \(\mathbf{A} \in \mathbb{R}^{m \times n}\) nào, kích thước của ma trận \(\mathbf{A}\) là (\(m\), \(n\)) hay \(m \times n\). Trong trường hợp đặc biệt, khi một ma trận có số dòng bằng số cột, dạng của nó là một hình vuông; như vậy, nó được gọi là một ma trận vuông (square matrix).

Ta có thể tạo một ma trận \(m \times n\) trong MXNet bằng cách khai báo kích thước của nó với hai thành phần \(m\)\(n\) khi sử dụng bất kỳ hàm khởi tạo ndarray nào mà ta thích.

A = np.arange(20).reshape(5, 4)
A
array([[ 0.,  1.,  2.,  3.],
       [ 4.,  5.,  6.,  7.],
       [ 8.,  9., 10., 11.],
       [12., 13., 14., 15.],
       [16., 17., 18., 19.]])

Ta có thể truy cập phần tử vô hướng \(a_{ij}\) của ma trận \(\mathbf{A}\) trong :eqref:eq_matrix_def bằng cách khai báo chỉ số dòng (\(i\)) và chỉ số cột (\(j\)), như là \([\mathbf{A}]_{ij}\). Khi những thành phần vô hướng của ma trận \(\mathbf{A}\), như trong :eqref:eq_matrix_defchưa được đưa ra, ta có thể sử dụng ký tự viết thường của ma trận \(\mathbf{A}\) với các chỉ số ghi dưới, \(a_{ij}\), để chỉ thành phần \([\mathbf{A}]_{ij}\). Nhằm giữ sự đơn giản cho các ký hiệu, dấu phẩy chỉ được thêm vào để phân tách các chỉ số khi cần thiết, như \(a_{2, 3j}\)\([\mathbf{A}]_{2i-1, 3}\).

Đôi khi, ta muốn hoán đổi các trục. Khi ta hoán đổi các dòng với các cột của ma trận, kết quả có được là chuyển vị (transpose) của ma trận đó. Về lý thuyết, chuyển vị của ma trận \(\mathbf{A}\) được ký hiệu là \(\mathbf{A}^\top\) và nếu \(\mathbf{B} = \mathbf{A}^\top\) thì \(b_{ij} = a_{ji}\) với mọi \(i\)\(j\). Do đó, chuyển vị của \(\mathbf{A}\) trong :eqref:eq_matrix_def là một ma trận \(n \times m\):

(2.3.3)\[\begin{split}\mathbf{A}^\top = \begin{bmatrix} a_{11} & a_{21} & \dots & a_{m1} \\ a_{12} & a_{22} & \dots & a_{m2} \\ \vdots & \vdots & \ddots & \vdots \\ a_{1n} & a_{2n} & \dots & a_{mn} \end{bmatrix}.\end{split}\]

Trong mã nguồn, ta lấy chuyển vị của một ma trận thông qua thuộc tính T.

A.T
array([[ 0.,  4.,  8., 12., 16.],
       [ 1.,  5.,  9., 13., 17.],
       [ 2.,  6., 10., 14., 18.],
       [ 3.,  7., 11., 15., 19.]])

Là một biến thể đặc biệt của ma trận vuông, ma trận đối xứng (symmetric matrix) \(\mathbf{A}\) có chuyển vị bằng chính nó: \(\mathbf{A} = \mathbf{A}^\top\).

B = np.array([[1, 2, 3], [2, 0, 4], [3, 4, 5]])
B
array([[1., 2., 3.],
       [2., 0., 4.],
       [3., 4., 5.]])
B == B.T
array([[ True,  True,  True],
       [ True,  True,  True],
       [ True,  True,  True]])

Ma trận là một cấu trúc dữ liệu hữu ích: chúng cho phép ta tổ chức dữ liệu có nhiều phương thức biến thể khác nhau. Ví dụ, các dòng trong ma trận của chúng ta có thể tượng trưng cho các căn nhà khác nhau (các điểm dữ liệu), còn các cột có thể tượng trưng cho những thuộc tính khác nhau của ngôi nhà. Bạn có thể thấy quen thuộc với điều này nếu đã từng sử dụng các phần mềm lập bảng tính hoặc đã đọc Section 2.2. Do đó, mặc dù một vector đơn lẻ có hướng mặc định là một vector cột, trong một ma trận biểu thị một tập dữ liệu bảng biểu, sẽ tốt hơn nếu ta xem mỗi điểm dữ liệu như một vector dòng trong ma trận. Chúng ta sẽ thấy ở những chương sau, quy ước này sẽ giúp dễ dàng áp dụng các kỹ thuật học sâu thông dụng. Ví dụ, với trục ngoài cùng của ndarray, ta có thể truy cập hay duyệt qua các batch nhỏ của những điểm dữ liệu hoặc chỉ đơn thuần là các điểm dữ liệu nếu không có batch nhỏ nào cả.

2.3.4. Tensor

Giống như vector khái quát hoá số vô hướng và ma trận khái quát hoá vector, ta có thể xây dựng những cấu trúc dữ liệu với thậm chí nhiều trục hơn. Tensor cho chúng ta một phương pháp tổng quát để miêu tả các ndarray với số trục bất kỳ. Ví dụ, vector là các tensor bậc một còn ma trận là các tensor bậc hai. Tensor được ký hiệu với ký tự viết hoa sử dụng một font chữ đặc biệt (ví dụ: \(\mathsf{X}\), \(\mathsf{Y}\), và \(\mathsf{Z}\)) và có cơ chế truy vấn (ví dụ: \(x_{ijk}\) and \([\mathsf{X}]_{1, 2i-1, 3}\)) giống như ma trận.

Tensor sẽ trở nên quan trọng hơn khi ta bắt đầu làm việc với hình ảnh, thường được biểu diễn dưới dạng ndarray với 3 trục tương ứng với chiều cao, chiều rộng và một trục kênh (channel) để xếp chồng các kênh màu (đỏ, xanh lá và xanh dương). Tạm thời, ta sẽ bỏ qua các tensor bậc cao hơn và tập trung vào những điểm cơ bản trước.

X = np.arange(24).reshape(2, 3, 4)
X
array([[[ 0.,  1.,  2.,  3.],
        [ 4.,  5.,  6.,  7.],
        [ 8.,  9., 10., 11.]],

       [[12., 13., 14., 15.],
        [16., 17., 18., 19.],
        [20., 21., 22., 23.]]])

2.3.5. Các thuộc tính Cơ bản của Phép toán Tensor

Số vô hướng, vector, ma trận và tensor với một số trục bất kỳ có một vài thuộc tính rất hữu dụng. Ví dụ, bạn có thể để ý từ định nghĩa của phép toán theo từng phần tử (elementwise), bất kỳ phép toán theo từng phần tử một ngôi nào cũng không làm thay đổi kích thước của toán hạng của nó. Tương tự, cho hai tensor bất kỳ có cùng kích thước, kết quả của bất kỳ phép toán theo từng phần tử hai ngôi sẽ là một tensor có cùng kích thước. Ví dụ, cộng hai ma trận có cùng kích thước sẽ thực hiện phép cộng theo từng phần tử giữa hai ma trận này.

A = np.arange(20).reshape(5, 4)
B = A.copy()  # Assign a copy of A to B by allocating new memory
A, A + B
(array([[ 0.,  1.,  2.,  3.],
        [ 4.,  5.,  6.,  7.],
        [ 8.,  9., 10., 11.],
        [12., 13., 14., 15.],
        [16., 17., 18., 19.]]),
 array([[ 0.,  2.,  4.,  6.],
        [ 8., 10., 12., 14.],
        [16., 18., 20., 22.],
        [24., 26., 28., 30.],
        [32., 34., 36., 38.]]))

Đặc biệt, phép nhân theo phần tử của hai ma trận được gọi là phép nhân Hadamard (Hadamard product – ký hiệu toán học là \(\odot\)). Xét ma trận \(\mathbf{B} \in \mathbb{R}^{m \times n}\) có phần tử dòng \(i\) và cột \(j\)\(b_{ij}\). Phép nhân Hadamard giữa ma trận \(\mathbf{A}\) (khai báo ở :eqref:eq_matrix_def) và \(\mathbf{B}\)

(2.3.4)\[\begin{split}\mathbf{A} \odot \mathbf{B} = \begin{bmatrix} a_{11} b_{11} & a_{12} b_{12} & \dots & a_{1n} b_{1n} \\ a_{21} b_{21} & a_{22} b_{22} & \dots & a_{2n} b_{2n} \\ \vdots & \vdots & \ddots & \vdots \\ a_{m1} b_{m1} & a_{m2} b_{m2} & \dots & a_{mn} b_{mn} \end{bmatrix}.\end{split}\]
A * B
array([[  0.,   1.,   4.,   9.],
       [ 16.,  25.,  36.,  49.],
       [ 64.,  81., 100., 121.],
       [144., 169., 196., 225.],
       [256., 289., 324., 361.]])

Nhân hoặc cộng một tensor với một số vô hướng cũng sẽ không thay đổi kích thước của tensor, mỗi phần tử của tensor sẽ được cộng hoặc nhân cho số vô hướng đó.

a = 2
X = np.arange(24).reshape(2, 3, 4)
a + X, (a * X).shape
(array([[[ 2.,  3.,  4.,  5.],
         [ 6.,  7.,  8.,  9.],
         [10., 11., 12., 13.]],

        [[14., 15., 16., 17.],
         [18., 19., 20., 21.],
         [22., 23., 24., 25.]]]),
 (2, 3, 4))

2.3.6. Rút gọn

Một phép toán hữu ích mà ta có thể thực hiện trên bất kỳ tensor nào là phép tính tổng các phần tử của nó. Ký hiệu toán học của phép tính tổng là \(\sum\). Ta biểu diễn phép tính tổng các phần tử của một vector \(\mathbf{x}\) với độ dài \(d\) dưới dạng \(\sum_{i=1}^d x_i\). Trong mã nguồn, ta chỉ cần gọi hàm sum.

x = np.arange(4)
x, x.sum()
(array([0., 1., 2., 3.]), array(6.))

Ta có thể biểu diễn phép tính tổng các phần tử của tensor có kích thước tùy ý. Ví dụ, tổng các phần tử của một ma trận \(m \times n\) có thể được viết là \(\sum_{i=1}^{m} \sum_{j=1}^{n} a_{ij}\).

A.shape, A.sum()
((5, 4), array(190.))

Theo mặc định, hàm sum sẽ rút gọn tensor dọc theo tất cả các trục của nó và trả về kết quả là một số vô hướng. Ta cũng có thể chỉ định các trục được rút gọn bằng phép tổng. Lấy ma trận làm ví dụ, để rút gọn theo chiều hàng (trục \(0\)) bằng việc tính tổng tất cả các hàng, ta đặt axis=0 khi gọi hàm sum.

A_sum_axis0 = A.sum(axis=0)
A_sum_axis0, A_sum_axis0.shape
(array([40., 45., 50., 55.]), (4,))

Việc đặt axis=1 sẽ rút gọn theo cột (trục \(1\)) bằng việc tính tổng tất cả các cột. Do đó, kích thước trục \(1\) của đầu vào sẽ không còn trong kích thước của đầu ra.

A_sum_axis1 = A.sum(axis=1)
A_sum_axis1, A_sum_axis1.shape
(array([ 6., 22., 38., 54., 70.]), (5,))

Việc rút gọn ma trận dọc theo cả hàng và cột bằng phép tổng tương đương với việc cộng tất cả các phần tử trong ma trận đó lại.

A.sum(axis=[0, 1])  # Same as A.sum()
array(190.)

Một đại lượng liên quan là trung bình cộng. Ta tính trung bình cộng bằng cách chia tổng các phần tử cho số lượng phần tử. Trong mã nguồn, ta chỉ cần gọi hàm mean với đầu vào là các tensor có kích thước tùy ý.

A.mean(), A.sum() / A.size
(array(9.5), array(9.5))

Giống như sum, hàm mean cũng có thể rút gọn tensor dọc theo các trục được chỉ định.

A.mean(axis=0), A.sum(axis=0) / A.shape[0]
(array([ 8.,  9., 10., 11.]), array([ 8.,  9., 10., 11.]))

2.3.6.1. Tổng không rút gọn

Tuy nhiên, việc giữ lại số các trục đôi khi là cần thiết khi gọi hàm sum hoặc mean, bằng cách đặt keepdims=True.

sum_A = A.sum(axis=1, keepdims=True)
sum_A
array([[ 6.],
       [22.],
       [38.],
       [54.],
       [70.]])

Ví dụ, vì sum_A vẫn giữ lại \(2\) trục sau khi tính tổng của mỗi hàng, chúng ta có thể chia A cho sum_A thông qua cơ chế lan truyền.

A / sum_A
array([[0.        , 0.16666667, 0.33333334, 0.5       ],
       [0.18181819, 0.22727273, 0.27272728, 0.3181818 ],
       [0.21052632, 0.23684211, 0.2631579 , 0.28947368],
       [0.22222222, 0.24074075, 0.25925925, 0.2777778 ],
       [0.22857143, 0.24285714, 0.25714287, 0.27142859]])

Nếu chúng ta muốn tính tổng tích lũy các phần tử của A dọc theo các trục, giả sử axis=0 (từng hàng một), ta có thể gọi hàm cumsum. Hàm này không rút gọn chiều của tensor đầu vào theo bất cứ trục nào.

A.cumsum(axis=0)
array([[ 0.,  1.,  2.,  3.],
       [ 4.,  6.,  8., 10.],
       [12., 15., 18., 21.],
       [24., 28., 32., 36.],
       [40., 45., 50., 55.]])

2.3.7. Tích vô hướng

Cho đến giờ, chúng ta mới chỉ thực hiện những phép tính từng phần tử tương ứng, như tổng và trung bình. Nếu đây là tất những gì chúng ta có thể làm, đại số tuyến tính có lẽ không xứng đáng để có nguyên một mục. Tuy nhiên, một trong nhưng phép tính căn bản nhất của đại số tuyến tính là tích vô hướng. Với hai vector \(\mathbf{x}, \mathbf{y} \in \mathbb{R}^d\) cho trước, tích vô hướng (dot product) \(\mathbf{x}^\top \mathbf{y}\) (hoặc \(\langle \mathbf{x}, \mathbf{y} \rangle\)) là tổng các tích của những phần tử có cùng vị trí: \(\mathbf{x}^\top \mathbf{y} = \sum_{i=1}^{d} x_i y_i\).

y = np.ones(4)
x, y, np.dot(x, y)
(array([0., 1., 2., 3.]), array([1., 1., 1., 1.]), array(6.))

Lưu ý rằng chúng ta có thể thể hiện tích vô hướng của hai vector một cách tương tự bằng việc thực hiện tích từng phần tử tương ứng rồi lấy tổng:

np.sum(x * y)
array(6.)

Tích vô hướng sẽ hữu dụng trong rất nhiều trường hợp. Ví dụ, với một tập các giá trị cho trước, biểu thị bởi vector \(\mathbf{x} \in \mathbb{R}^d\), và một tập các trọng số được biểu thị bởi \(\mathbf{w} \in \mathbb{R}^d\), tổng trọng số của các giá trị trong \(\mathbf{x}\) theo các trọng số trong \(\mathbf{w}\) có thể được thể hiện bởi tích vô hướng \(\mathbf{x}^\top \mathbf{w}\). Khi các trọng số không âm và có tổng bằng một(\(\left(\sum_{i=1}^{d} {w_i} = 1\right)\)), tích vô hướng thể hiện phép tính trung bình trọng số (weighted average). Sau khi được chuẩn hoá thành hai vector đơn vị, tích vô hướng của hai vector đó là giá trị cos của góc giữa hai vector đó. Chúng tôi sẽ giới thiệu khái niệm về độ dài ở các phần sau trong mục này.

2.3.8. Tích giữa Ma trận và Vector

Giờ đây, khi đã biết cách tính toán tích vô hướng, chúng ta có thể bắt đầu hiểu tích giữa ma trận và vector. Bạn có thể xem lại cách ma trận \(\mathbf{A} \in \mathbb{R}^{m \times n}\) và vector \(\mathbf{x} \in \mathbb{R}^n\) được định nghĩa và biểu diễn trong (2.3.2)(2.3.1). Ta sẽ bắt đầu bằng việc biểu diễn ma trận \(\mathbf{A}\) qua các vector hàng của nó.

(2.3.5)\[\begin{split}\mathbf{A}= \begin{bmatrix} \mathbf{a}^\top_{1} \\ \mathbf{a}^\top_{2} \\ \vdots \\ \mathbf{a}^\top_m \\ \end{bmatrix},\end{split}\]

Mỗi \(\mathbf{a}^\top_{i} \in \mathbb{R}^n\) là một vector hàng thể hiện hàng thứ \(i\) của ma trận \(\mathbf{A}\). Tích giữa ma trận và vector \(\mathbf{A}\mathbf{x}\) đơn giản chỉ là một vector cột với chiều dài \(m\), với phần tử thứ \(i\) là kết quả của phép tích vô hướng \(\mathbf{a}^\top_i \mathbf{x}\):

(2.3.6)\[\begin{split}\mathbf{A}\mathbf{x} = \begin{bmatrix} \mathbf{a}^\top_{1} \\ \mathbf{a}^\top_{2} \\ \vdots \\ \mathbf{a}^\top_m \\ \end{bmatrix}\mathbf{x} = \begin{bmatrix} \mathbf{a}^\top_{1} \mathbf{x} \\ \mathbf{a}^\top_{2} \mathbf{x} \\ \vdots\\ \mathbf{a}^\top_{m} \mathbf{x}\\ \end{bmatrix}.\end{split}\]

Chúng ta có thể nghĩ đến việc nhân một ma trận \(\mathbf{A}\in \mathbb{R}^{m \times n}\) với một vector như một phép biến hình, chiếu vector từ không gian \(\mathbb{R}^{n}\) thành \(\mathbb{R}^{m}\). Những phép biến hình này hóa ra lại trở nên rất hữu dụng. Ví dụ, chúng ta có thể biểu diễn phép xoay là tích với một ma trận vuông. Bạn sẽ thấy ở những chương tiếp theo, chúng ta cũng có thể sử dụng tích giữa ma trận và vector để thực hiện hầu hết những tính toán cần thiết khi tính các tầng trong một mạng nơ-ron dựa theo kết quả của tầng trước đó.

Khi lập trình, để thực hiện nhân ma trận với vector ndarray, chúng ta cũng sử dụng hàm dot giống như tích vô hướng. Việc gọi np.dot(A, x) với ma trận A và một vector x sẽ thực hiện phép nhân vô hướng giữa ma trận và vector. Lưu ý rằng chiều của cột A (chiều dài theo trục \(1\)) phải bằng với chiều của vector x (chiều dài của nó).

A.shape, x.shape, np.dot(A, x)
((5, 4), (4,), array([ 14.,  38.,  62.,  86., 110.]))

2.3.9. Phép nhân Ma trận

Nếu bạn đã quen với tích vô hướng và tích ma trận-vector, tích ma trận-ma trận cũng tương tự như thế.

Giả sử ta có hai ma trận \(\mathbf{A} \in \mathbb{R}^{n \times k}\)\(\mathbf{B} \in \mathbb{R}^{k \times m}\):

(2.3.7)\[\begin{split}\mathbf{A}=\begin{bmatrix} a_{11} & a_{12} & \cdots & a_{1k} \\ a_{21} & a_{22} & \cdots & a_{2k} \\ \vdots & \vdots & \ddots & \vdots \\ a_{n1} & a_{n2} & \cdots & a_{nk} \\ \end{bmatrix},\quad \mathbf{B}=\begin{bmatrix} b_{11} & b_{12} & \cdots & b_{1m} \\ b_{21} & b_{22} & \cdots & b_{2m} \\ \vdots & \vdots & \ddots & \vdots \\ b_{k1} & b_{k2} & \cdots & b_{km} \\ \end{bmatrix}.\end{split}\]

Đặt \(\mathbf{a}^\top_{i} \in \mathbb{R}^k\) là vector hàng biểu diễn hàng thứ \(i\) của ma trận \(\mathbf{A}\)\(\mathbf{b}_{j} \in \mathbb{R}^k\) là vector cột thứ \(j\) của ma trận \(\mathbf{B}\). Để tính ma trận tích \(\mathbf{C} = \mathbf{A}\mathbf{B}\), cách đơn giản nhất là viết các hàng của ma trận \(\mathbf{A}\) và các cột của ma trận \(\mathbf{B}\):

(2.3.8)\[\begin{split}\mathbf{A}= \begin{bmatrix} \mathbf{a}^\top_{1} \\ \mathbf{a}^\top_{2} \\ \vdots \\ \mathbf{a}^\top_n \\ \end{bmatrix}, \quad \mathbf{B}=\begin{bmatrix} \mathbf{b}_{1} & \mathbf{b}_{2} & \cdots & \mathbf{b}_{m} \\ \end{bmatrix}.\end{split}\]

Khi đó ma trận tích \(\mathbf{C} \in \mathbb{R}^{n \times m}\) được tạo với phần tử \(c_{ij}\) bằng tích vô hướng \(\mathbf{a}^\top_i \mathbf{b}_j\):

(2.3.9)\[\begin{split}\mathbf{C} = \mathbf{AB} = \begin{bmatrix} \mathbf{a}^\top_{1} \\ \mathbf{a}^\top_{2} \\ \vdots \\ \mathbf{a}^\top_n \\ \end{bmatrix} \begin{bmatrix} \mathbf{b}_{1} & \mathbf{b}_{2} & \cdots & \mathbf{b}_{m} \\ \end{bmatrix} = \begin{bmatrix} \mathbf{a}^\top_{1} \mathbf{b}_1 & \mathbf{a}^\top_{1}\mathbf{b}_2& \cdots & \mathbf{a}^\top_{1} \mathbf{b}_m \\ \mathbf{a}^\top_{2}\mathbf{b}_1 & \mathbf{a}^\top_{2} \mathbf{b}_2 & \cdots & \mathbf{a}^\top_{2} \mathbf{b}_m \\ \vdots & \vdots & \ddots &\vdots\\ \mathbf{a}^\top_{n} \mathbf{b}_1 & \mathbf{a}^\top_{n}\mathbf{b}_2& \cdots& \mathbf{a}^\top_{n} \mathbf{b}_m \end{bmatrix}.\end{split}\]

Ta có thể coi tích hai ma trận \(\mathbf{AB}\) như việc tính \(m\) phép nhân ma trận và vector, sau đó ghép các kết quả với nhau để tạo ra một ma trận \(n \times m\). Giống như tích vô hướng và phép nhân ma trận-vector, ta có thể tính phép nhân hai ma trận bằng cách sử dụng hàm dot. Trong đoạn mã dưới đây, chúng ta tính phép nhân giữa AB. Ở đây, A là một ma trận với \(5\) hàng \(4\) cột và B là một ma trận với 4 hàng 3 cột. Sau phép nhân này, ta thu được một ma trận với \(5\) hàng \(3\) cột.

B = np.ones(shape=(4, 3))
np.dot(A, B)
array([[ 6.,  6.,  6.],
       [22., 22., 22.],
       [38., 38., 38.],
       [54., 54., 54.],
       [70., 70., 70.]])

Phép nhân hai ma trận có thể được gọi đơn giản là phép nhân ma trận và không nên nhầm lẫn với phép nhân Hadamard.

2.3.10. Chuẩn

Một trong những toán tử hữu dụng nhất của đại số tuyến tính là chuẩn (norm). Nói dân dã thì, các chuẩn của một vector cho ta biết một vector lớn tầm nào. Thuật ngữ kích thước đang xét ở đây không nói tới số chiều không gian mà đúng hơn là về độ lớn của các thành phần.

Trong đại số tuyến tính, chuẩn của một vector là hàm số \(f\) ánh xạ một vector đến một số vô hướng, thỏa mãn các tính chất sau. Cho vector \(\mathbf{x}\) bất kỳ, tính chất đầu tiên phát biểu rằng nếu chúng ta co giãn toàn bộ các phần tử của một vector bằng một hằng số \(\alpha\), chuẩn của vector đó cũng co giãn theo giá trị tuyệt đối của hằng số đó:

(2.3.10)\[f(\alpha \mathbf{x}) = |\alpha| f(\mathbf{x}).\]

Tính chất thứ hai cũng giống như bất đẳng thức tam giác:

(2.3.11)\[f(\mathbf{x} + \mathbf{y}) \leq f(\mathbf{x}) + f(\mathbf{y}).\]

Tính chất thứ ba phát biểu rằng chuẩn phải không âm:

(2.3.12)\[f(\mathbf{x}) \geq 0.\]

Điều này là hợp lý vì trong hầu hết các trường hợp thì kích thước nhỏ nhất cho các vật đều bằng 0. Tính chất cuối cùng yêu cầu chuẩn nhỏ nhất thu được khi và chỉ khi toàn bộ thành phần của vector đó bằng 0.

(2.3.13)\[\forall i, [\mathbf{x}]_i = 0 \Leftrightarrow f(\mathbf{x})=0.\]

Bạn chắc sẽ để ý là các chuẩn có vẻ giống như một phép đo khoảng cách. Và nếu còn nhớ khái niệm khoảng cách Euclid (định lý Pythagoras) được học ở phổ thông, thì khái niệm không âm và bất đẳng thức tam giác có thể gợi nhắc lại một chút. Thực tế là, khoảng cách Euclid cũng là một chuẩn: cụ thể là \(\ell_2\). Giả sử rằng các thành phần trong vector \(n\) chiều \(\mathbf{x}\)\(x_1, \ldots, x_n\). Chuẩn \(\ell_2\) của \(\mathbf{x}\) là căn bậc hai của tổng các bình phương của các thành phần trong vector:

(2.3.14)\[\|\mathbf{x}\|_2 = \sqrt{\sum_{i=1}^n x_i^2},\]

Ở đó, chỉ số dưới \(2\) thường được lược đi khi viết chuẩn \(\ell_2\), ví dụ, \(\|\mathbf{x}\|\) cũng tương đương với \(\|\mathbf{x}\|_2\). Khi lập trình, ta có thể tính chuẩn \(\ell_2\) của một vector bằng cách gọi hàm linalg.norm.

u = np.array([3, -4])
np.linalg.norm(u)
array(5.)

Trong học sâu, chúng ta thường gặp chuẩn \(\ell_2\) bình phương hơn. Bạn cũng sẽ thường xuyên gặp chuẩn \(\ell_1\), chuẩn được biểu diễn bằng tổng các giá trị tuyệt đối của các thành phần trong vector:

(2.3.15)\[\|\mathbf{x}\|_1 = \sum_{i=1}^n \left|x_i \right|.\]

So với chuẩn \(\ell_2\), nó ít bị ảnh ưởng bởi các giá trị ngoại biên hơn. Để tính chuẩn \(\ell_1\), chúng ta dùng hàm giá trị tuyệt đối rồi lấy tổng các thành phần.

np.abs(u).sum()
array(7.)

Cả hai chuẩn \(\ell_2\)\(\ell_1\) đều là trường hợp riêng của một chuẩn tổng quát hơn, chuẩn \(\ell_p\):

(2.3.16)\[\|\mathbf{x}\|_p = \left(\sum_{i=1}^n \left|x_i \right|^p \right)^{1/p}.\]

Tương tự với chuẩn \(\ell_2\) của vector, chuẩn Frobenius của một ma trận \(\mathbf{X} \in \mathbb{R}^{m \times n}\) là căn bậc hai của tổng các bình phương của các thành phần trong ma trận:

(2.3.17)\[\|\mathbf{X}\|_F = \sqrt{\sum_{i=1}^m \sum_{j=1}^n x_{ij}^2}.\]

Chuẩn Frobenius thỏa mãn tất cả các tính chất của một chuẩn vector. Nó giống chuẩn \(\ell_2\) của một vector nhưng ở dạng của ma trận. Ta dùng hàm linalg.norm để tính toán chuẩn Frobenius của ma trận.

np.linalg.norm(np.ones((4, 9)))
array(6.)

2.3.10.1. Chuẩn và Mục tiêu

Tuy không muốn đi quá nhanh nhưng chúng ta có thể xây dựng phần nào trực giác để hiểu tại sao những khái niệm này lại hữu dụng. Trong học sâu, ta thường cố giải các bài toán tối ưu: cực đại hóa xác suất xảy ra của dữ liệu quan sát được; cực tiểu hóa khoảng cách giữa dự đoán và nhãn gốc. Gán các biểu diễn vector cho các đối tượng (như từ, sản phẩm hay các bài báo) để cực tiểu hóa khoảng cách giữa các đối tượng tương tự nhau và cực đại hóa khoảng cách giữa các đối tượng khác nhau. Mục tiêu, thành phần quan trọng nhất của một thuật toán học sâu (bên cạnh dữ liệu), thường được biễu diễn diễn theo chuẩn (norm).

2.3.11. Bàn thêm về Đại số Tuyến tính

Chỉ trong mục này, chúng tôi đã trang bị cho bạn tất cả những kiến thức đại số tuyến tính cần thiết để hiểu một lượng lớn các mô hình học máy hiện đại. Vẫn còn rất nhiều kiến thức đại số tuyến tính, phần lớn đều hữu dụng cho học máy. Một ví dụ là phép phân tích ma trận ra các thành phần, các phép phân tích này có thể tạo ra các cấu trúc thấp chiều trong các tập dữ liệu thực tế. Có cả một nhánh của học máy tập trung vào sử dụng các phép phân tích ma trận và tổng quát chúng lên cho các tensor bậc cao để khám phá cấu trúc trong các tập dữ liệu và giải quyết các bài toán dự đoán. Tuy nhiên, cuốn sách này chỉ tập trung vào học sâu. Và chúng tôi tin rằng bạn sẽ muốn học thêm nhiều về toán một khi đã có thể triển khai được các mô hình học máy hữu dụng cho các tập dữ liệu thực tế. Bởi vậy, trong khi vẫn còn nhiều kiến thức toán cần bàn thêm ở phần sau, chúng tôi sẽ kết thúc mục này ở đây.

Nếu bạn muốn học thêm về đại số tuyến tính, bạn có thể tham khảo sec_geometry-linear-algebric-ops hoặc các nguồn tài liệu xuất sắc tại [Strang, 1993][Kolter, 2008][Petersen et al., 2008].

2.3.12. Tóm tắt

  • Số vô hướng, vector, ma trận, và tensor là các đối tượng toán học cơ bản trong đại số tuyến tính.
  • Vector là dạng tổng quát của số vô hướng và ma trận là dạng tổng quát của vector.
  • Trong cách biểu diễn ndarray, các số vô hướng, vector, ma trận và tensor lần lượt có 0, 1, 2 và một số lượng tùy ý các trục.
  • Một tensor có thể thu gọn theo một số trục bằng summean.
  • Phép nhân theo từng phần tử của hai ma trận được gọi là tích Hadamard của chúng. Phép toán này khác với phép nhân ma trận.
  • Trong học sâu, chúng ta thường làm việc với các chuẩn như chuẩn \(\ell_1\), chuẩn \(\ell_2\) và chuẩn Frobenius.
  • Chúng ta có thể thực hiện một số lượng lớn các toán tử trên số vô hướng, vector, ma trận và tensor với các hàm của ndarray.

2.3.13. Bài tập

  1. Chứng minh rằng chuyển vị của một ma trận chuyển vị là chính nó: \((\mathbf{A}^\top)^\top = \mathbf{A}\).
  2. Cho hai ma trận \(\mathbf{A}\)\(\mathbf{B}\), chứng minh rằng tổng của chuyển vị bằng chuyển vị của tổng: \(\mathbf{A}^\top + \mathbf{B}^\top = (\mathbf{A} + \mathbf{B})^\top\).
  3. Cho một ma trận vuông \(\mathbf{A}\), liệu rằng \(\mathbf{A} + \mathbf{A}^\top\) có luôn đối xứng? Tại sao?
  4. Chúng ta đã định nghĩa tensor X với kích thước (\(2\), \(3\), \(4\)) trong mục này. Kết quả của len(X) là gì?
  5. Cho một tensor X với kích thước bất kỳ, liệu len(X) có luôn tương ứng với độ dài của một trục nhất định của X hay không? Đó là trục nào?
  6. Chạy A / A.sum(axis=1) và xem điều gì xảy ra. Bạn có phân tích được nguyên nhân không?
  7. Khi di chuyển giữa hai điểm ở Manhattan (đường phố hình bàn cờ), khoảng cách tính bằng tọa độ (tức độ dài các đại lộ và phố) mà bạn cần di chuyển là bao nhiêu? Bạn có thể đi theo đường chéo không? (Xem thêm bản đồ Manhattan, New York để trả lời câu hỏi này)
  8. Xét một tensor với kích thước (\(2\), \(3\), \(4\)). Kích thước của kết quả sau khi tính tổng theo trục \(0\), \(1\)\(2\) sẽ như thế nào?
  9. Đưa một tensor với 3 trục hoặc hơn vào hàm linalg.norm và quan sát kết quả. Hàm này thực hiện việc gì cho các ndarray với kích thước bất kỳ?

2.3.14. Thảo luận

2.3.15. Những người thực hiện

Bản dịch trong trang này được thực hiện bởi:

  • Đoàn Võ Duy Thanh
  • Lê Khắc Hồng Phúc
  • Phạm Minh Đức
  • Ngô Thế Anh Khoa
  • Nguyễn Lê Quang Nhật
  • Vũ Hữu Tiệp
  • Mai Sơn Hải
  • Phạm Hồng Vinh