Ma Trận Nâng Cao Với Thư Viện Numpy

Ma Trận Nâng Cao Với Thư Viện Numpy

Như đã hứa, ở bài viết này, mình sẽ chia sẻ đến các bạn các vấn đề nâng cao hơn về ma trận cũng như các hàm xử lý chúng bằng thư viện Numpy của Python. Mọi người có thể theo dõi lại bài viết trước của mình tại đây nếu chưa đọc hoặc có lỡ quên phần kiến thức nào nha. Các vấn đề được đề cập trong bài này có lẽ sẽ hơi khó hiểu với những bạn chưa từng học về Đại số tuyến tính. Tuy vậy, mình sẽ cố gắng truyền tải đến các bạn một cách dễ hiểu nhất.

Thay đổi kích thước của ma trận với Reshape

Khi làm việc với ma trận, chúng ta thường xuyên phải sử dụng các thao tác thay đổi kích thước của chúng. Trong Numpy, ta có thể thực hiện qua hàm .reshape

Chuyển từ mảng một chiều thành ma trận hai chiều

A = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9])
print(A.reshape(3, 3))

Trong đoạn code trên, mình thử chuyển một mảng một chiều có 9 phần tử sang một ma trận hai chiều có 3 hàng và 3 cột. Kết quả thu được như sau:

[[1 2 3]
 [4 5 6]
 [7 8 9]]

Dĩ nhiên, khi chuyển từ mảng một chiều thành hai chiều, số lượng phần tử được giữ nguyên, do vậy ta cần chú ý đến số hàng và số cột của ma trận hai chiều để tránh lỗi nha.

Chuyển từ ma trận hai chiều về mảng một chiều

Ngược lại, ta có thể chuyển một ma trận hai chiều về mảng một chiều bằng phương thức .reshape(-1).

A = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print(A.reshape(-1))

Kết quả thu được là:

[1 2 3 4 5 6 7 8 9]

Thay đổi số hàng, số cột của ma trận

A = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]])
print(A.reshape(3, 4))

Trong đoạn code trên, mình thử chuyển ma trận A gồm 4 hàng và 3 cột thành 3 hàng và 4 cột. Cùng xem kết quả ra sao nhé:

[[ 1  2  3  4]
 [ 5  6  7  8]
 [ 9 10 11 12]]

Hạng của ma trận

Hạng của ma trận, được định nghĩa là cấp cao nhất của định thức con khác 0 của ma trận đó. Người ta còn định nghĩa hạng của ma trận là số vector độc lập tuyến tính tối đa khác 0 có trong A. Tuy nhiên, mình thấy cách định nghĩa  này hơi khó tiếp cận đối với những bạn chưa được học về Đại số tuyến tính.

Ký hiệu hạng của ma trận A là rank(A).

Có nhiều phương pháp để xác định hạng của ma trận. Ta có thể thực hiện các phép biến đổi sơ cấp để đưa ma trận ban đầu về một ma trận hình thang. Các phép biến đổi sơ cấp thường dùng đó là:

  • Đổi 2 hàng hoặc 2 cột cho nhau.
  • Nhân các phần tử của cùng 1 hàng hoặc cột với một số thực khác 0.
  • Cộng vào các phần tử của 1 hàng hoặc cột các phần tử tương ứng của hàng khác hoặc cột khác cùng nhân với một số.

Để mọi người dễ hình dung, mình sẽ thử biến đổi sơ cấp để tính hạng của ma trận sau:

Đến đây thì mọi người đã có cái nhìn rõ hơn về hạng của ma trận rồi nhỉ. Tất nhiên là có nhiều phương pháp để xác định hạng của ma trận, ngoài phương pháp biến đổi sơ cấp như trên. 

Trong Numpy, ta có thể sử dụng hàm .linalg.matrix_rank() để tìm hạng của ma trận như sau:

A = np.array([[-2, 1, 0, 2, 3, -1], [4, 2, -1, 0, -2, 1], [6, 5, -2, 2, -1, 1], [-6, -1, 1, 2, 5, -2]])
print(np.linalg.matrix_rank(A))

Kết quả trả về vẫn sẽ là 

Vết của ma trận

Vết (trace) của một ma trận vuông, được định nghĩa bằng tổng các phần tử trên đường chéo chính của ma trận đó. 

Trong Numpy, ta có thể sử dụng 2 cách sau để tính vết của ma trận:

A = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
# trace
print(np.trace(A))
print(A.trace())

Kết quả trả về sẽ bằng 15. 

Chuẩn của ma trận

Trong tọa độ Oxy, chúng ta thường sử dụng khoảng cách Euclide (thông qua định lý Pythagoras quen thuộc) để tính khoảng cách giữa 2 điểm, hay là tính độ dài của một vector,nhằm xem điểm này gần với điểm nào, 2 điểm có mối tương quan như nào với nhau. Mở rộng ra cho không gian nhiều chiều, tức là vector nhiều chiều, chúng ta cũng cần có một công thức để tính "khoảng cách" giữa chúng. Điều này dẫn tới sự ra đời của khái niệm về chuẩn (norm). Có nhiều chuẩn khác nhau ứng với các không gian khác nhau, tuy nhiên, trong bài viết này, mình sẽ đề cập tới ba chuẩn thường dùng đối với một ma trận m hàng n cột bất kì:

Các bạn thấy chuẩn Frobenius có quen thuộc không nào. Công thức tính chuẩn Frobenius của một ma trận khá tương đồng với các công thức tính khoảng cách giữa 2 điểm trong hệ tọa độ Oxy, hay độ dài của 1 vector mà mình đã học trong trương trình THPT. Đối với chuẩn này, mình còn có thể sử dụng vết của ma trận để tính như sau:

Đối với chuẩn hàng, ta dùng câu lệnh như sau:

A = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]])
print(np.linalg.norm(A, ord=np.inf))

Kết quả thu được là 33.0

Đối với chuẩn cột, ta dùng câu lệnh sau:

A = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]])
print(np.linalg.norm(A, ord=1))

Kết quả trả về là 30.0

Đối với chuẩn Frobenius, ta tính theo cách như sau:

A = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]])
print(np.linalg.norm(A))

Ta thử dùng công thức tính theo vết ma trận để tính chuẩn Frobenius như nào nhé:

import math as mt

A = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]])
print(mt.sqrt(np.trace(A.T @ A)))

Kết quả trả về đều sẽ là 25.495097567963924 

Chuẩn của ma trận được sử dụng rất nhiều để đánh giá sai số trong các thuật toán giải hệ phương trình, tìm ma trận nghịch đảo,... cũng như đánh giá tính đúng đắn trong các thuật toán Machine learning, đặc biệt là chuẩn Frobenius. Cách tính cũng rất đơn giản phải không nào, vậy nên các bạn hãy nắm chắc nha.

Trị riêng và vector riêng của ma trận

Cho ma trận A vuông cấp n. Ta có định nghĩa sau về trị riêng và vector riêng:

Từ công thức kia, ta có một cách biến đổi quen thuộc trong Đại số tuyến tính như sau:

Như vậy, để tồn tại các vector X khác 0, ta có điều kiện sau:

Giải phương trình (**) ta thu được các trị riêng, và ứng với đó, thay vào phương trình (*) ta được các vector riêng tương ứng. Tất nhiên, cách giải thủ công này chỉ có thể áp dụng được với các ma trận cỡ nhỏ thôi, vì cấp của ma trận càng lớn thì khối lượng phép tính cũng sẽ tăng lên rất nhiều. Có một vài phương pháp giúp tìm trị riêng và vector riêng mà các bạn có thể tham khảo như phương pháp Danilevsky, phương pháp lũy thừa hay phương pháp xuống thang, ... Việc tìm trị riêng và vector riêng có ý nghĩa vô cùng lớn, đặc biệt là ứng dụng trong việc nén hình ảnh, giảm dung lượng của hình ảnh (Các bạn học machine learning chắc sẽ gặp bài toán khai triển SVD -  Singular Value Decomposition).

Có một vài tính chất sau mà ta cần lưu ý:

Đặc biệt hơn, ta có mối liên hệ giữa định thức và trị riêng như sau:

Trong Numpy, ta có thể tính trị riêng và vector riêng theo cách như sau:

A = np.array([[2, 1, 0], [1, 3, 1], [0, 1, 2]])
w, v = np.linalg.eig(A)
print("Eigenvalues: \n", w)  # trị riêng
print("Eigenvectors: \n", v)  # vector riêng

Kết quả trả về sẽ là:

Eigenvalues: 
 [4. 2. 1.]
Eigenvectors: 
 [[-4.08248290e-01  7.07106781e-01  5.77350269e-01]
 [-8.16496581e-01 -3.45767522e-16 -5.77350269e-01]
 [-4.08248290e-01 -7.07106781e-01  5.77350269e-01]]

Ma trận đồng dạng

Cho hai ma trận vuông A và B cùng cấp n.  Ta nói ma trận A đồn dạng với ma trận B (A ~ B) nếu tồn tại một ma trận T không suy biến (có định thức khác 0) sao cho:

 

Một tính chất quan trọng mà chúng ta cần biết, đó là hai ma trận đồng dạng có cùng trị riêng, do đó chúng có cùng các vector riêng tương ứng với các trị riêng đó. Do vậy, thay vì đi tìm trị riêng của ma trận ban đầu (vốn dĩ có thể vô cùng phức tạp), chúng ta có thể tìm một ma trận đơn giản hơn và đồng dạng với nó. Các bạn có thể đọc thêm về phương pháp Danilevsky, ở đây người ta sẽ biến đổi về một ma trận đặc biệt, gọi là ma trận Frobenius.

Dạng của ma trận Frobenius

Một vài hàm thao tác với hàng, cột và trên toàn ma trận

Hàm np.sum()

Để tính tổng tất cả các phần tử của ma trận, ta dùng hàm np.sum() như sau:

A = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print(np.sum(A))

Kết quả trả về là 45

Để tính tổng các phần tử trên từng cột, ta làm như sau:

A = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print(np.sum(A, axis=0))

Kết quả thu được là:

[12 15 18]

Tương tự như vậy, ta có thể tính tổng các phần tử trên từng hàng:

A = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print(np.sum(A, axis=1))

Kết quả trả về là:

[ 6 15 24]

Hàm np.min()

Hàm np.min() để tính phần tử nhỏ nhất của một ma trận như sau:

A = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print(np.min(A))

Kết quả trả về là 1

Tương tự như trên, ta có thể thêm vào axis=0 để tính phần tử nhỏ nhất của từng cột, và axis=1 để tính phần tử nhỏ nhất của từng hàng.

Hàm np.max()

Chúng ta có thể đoán được tác dụng của hàm này rồi nhỉ, ngược với hàm np.min() trả về giá trị nhỏ nhất, thì hàm np.max() trả về phần tử lớn nhất trên cả ma trận, trên từng hàng hay từng cột.

Hàm np.mean()

Hàm np.mean() sẽ trả về trung bình cộng các phần tử của toàn bộ ma trận, trung bình cộng của từng hàng hay từng cột:

A = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print(np.mean(A))
print(np.mean(A, axis=0))
print(np.mean(A, axis=1))

Kết quả trả về là:

5.0
[4. 5. 6.]
[2. 5. 8.]

Tổng kết

Như vậy, qua bài viết này, các bạn đã biết nhiều hơn các kiến thức về ma trận cũng như các hàm xử lý chúng.. Tất nhiên, những gì mình chia sẻ ở trên không phải là quá đủ, nhưng  mình nghĩ đó là những gì cần thiết nhất về ma trận mà các bạn cần nắm vững trước khi bắt đầu học về Machine Learning. Mọi sai sót cũng như thắc mắc về bài viết của mình mong được các bạn để lại comment phía bên dưới.