Phần tiếp theo của chuỗi chủ đề về ma trận sẽ đề cập tới các phép toán của ma trận. Song song với việc lý giải các phép toán, ta cũng sẽ học sử dụng thư viện Numpy để lập trình với ma trận.

Mục lục

1. Numpy và ma trận

1.1. Giới thiệu về Numpy

Numpy là thư viện được viết bằng Python nhằm phục vụ cho việc tính toán khoa học. Trong gói phần mềm này gồm một số thứ cơ bản sau:

  • Tập các mảng đa nhiều mạnh mẽ
  • Tập các hàm tính toán tinh vi
  • Có thể tích hợp với C/C++ và Fortran
  • Thuận tiện khi làm việc với đại số tuyến tính
  • Hỗ trợ biến đổi Fourier
  • Khả năng tạo các số ngẫu nhiên mạnh mẽ

Có lẽ đây là thư viện được sử dụng phổ biến nhất hiện nay để làm việc với các phép toán khoa học, kĩ thuật bằng ngôn ngữ Python. Trong đó bao gồm cả lĩnh vực học máy, các gói phần mềm nền để xây dựng các bài toán học máy hầu hết được viết bằng Python và có sử dụng Numpy. Như vậy, việc nắm được cách sử dụng Numpy là một lợi thế để giúp bạn tiếp cận được với học máy. Không chỉ vậy, Numpy còn có nhiều kiểu dữ liệu đa chiều giúp cho việc tính toán, lập trình, làm việc với các hệ cơ sở dữ liệu cực kì thuận tiện. Nên ngoài tính toán khoa học ra, việc sử dụng chúng khi lập trình cũng rất hữu ích.

Trong phần này, tôi không đi chi tiết vào Numpy mà chỉ đề cập tới một số API có thể làm việc được trong bài viết này. Tôi khuyên các bạn nên tìm hiểu thêm về nó qua trang chủ của Numpy.

1.2. Sử dụng Numpy cho ma trận

Để tạo một ma trận ta có thể sử dụng ndarray (viết gọn là array) của Numpy. Lưu ý rằng mảng array của Numpy là khác với mảng thuần của Python. Mảng thuần của Python không có được nhiều tiện ích tính toán như của Numpy. Về cơ bản, array này là một đối tượng mảng đa chiều thuần nhất tức là mọi phần tử đều cùng 1 kiểu. Thường các phần tử của ta là dạng số và được đánh địa chỉ bằng 1 cặp số nguyên dương bắt đầu từ (0, 0). Ví dụ dưới đây sẽ tạo ra ma trận $ [A]_{mn} $:

numpy-matrix.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import numpy as np

# create a matrix A
A = np.array([(1, 2, 3), (4, 5, 6)])

# print out A type
print(type(A))
# <type 'numpy.ndarray'>

# print out A
print(A)
# [[1 2 3]
#  [4 5 6]]

# The number of dimensions (axes)
print(A.ndim)
# 2

# Shape of A (rows x colums)
print(A.shape)
# (2, 3)

# Number of elements
print(A.size)
# 6

# element's data type
print(A.dtype)
# dtype('int64')

Để tạo ma trận ta có thể sử dụng một số cách như sau:

Tạo mảng từ list hoặc tuple của Python bằng hàm array của Numpy, lúc này kiểu dữ liệu của mảng Numpy sẽ là kiểu dữ liệu của đầu vào cho mảng.

numpy-matrix.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import numpy as np

# from 1-dimesional array
a = np.array([1.5, 0.0, 0.8])
print(a)
# [1.5, 0., 0.8]
print(a.dtype)
# dtype('float64')

# from 2-dimensional array
b = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]])
print(b)
# [[1, 0, 0]
#  [0, 1, 0]
#  [0, 0, 1]]

# from array of tuple and array
c = np.array([(0, 0, 1), [0, 1, 0], [1, 0, 0]])
print(b)
# [[0, 0, 1]
#  [0, 1, 0]
#  [1, 0, 0]]

Tạo mảng từ một dãy số với arangelinspace:

numpy-matrix.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import numpy as np

# with integer step
a = np.arange(10, 15, 1)
print(a)
# [10, 11, 12, 13, 14]

b = np.arange(10, 15, 2)
print(b)
# [10, 12, 14]

c = np.arange(10)
print(c)
# [10, 12, 14]

# with non-integer step
a = np.linspace(2.0, 3.0, num=5)
print(a)
# [ 2.  ,  2.25,  2.5 ,  2.75,  3.  ]

Tạo mảng ngẫu nhiên:

numpy-matrix.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import numpy as np

# from uniform distribution
a = np.random.rand(3,2)
print(a)
# [[ 0.65539839  0.31324931]
#  [ 0.49746522  0.40978874]
#  [ 0.58603861  0.85287283]]

# from standard normal distribution
a = np.random.randn(3,2)
print(a)
# [[ 0.71564826,  0.78572601]
#  [-0.04637402, -0.58368628]
#  [-1.25782822,  1.00658686]]

# with N(5, 4)
b = 5 + 2 * np.random.randn(3,2)
print(b)
# [[ 3.53328499  5.15966328]
#  [ 3.38333745  4.522973  ]
#  [ 2.6586514   4.8483927 ]]

Tạo mảng với các giá trị mặc định:

numpy-matrix.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import numpy as np

# all elements is 0
a = np.zeros((3, 2))
print(a)
# [[ 0.  0.]
#  [ 0.  0.]
#  [ 0.  0.]]

# all elements is 1
a = np.ones((3, 2))
print(a)
# [[ 1.  1.]
#  [ 1.  1.]
#  [ 1.  1.]]

# all elements is vary (uninitialized)
a = np.empty((3, 2))
print(a)
# [[  1.72723371e-077   1.72723371e-077]
#  [  1.33397724e-322   1.27319747e-313]
#  [  1.27319747e-313   1.27319747e-313]]

Để truy cập các phần tử trong Numpy cũng tương tự như với lists của Python và có thêm một số mở rộng như truy cập qua 1 mảng địa chỉ.

numpy-matrix.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# vector
a = np.array([3, 4, 2, 8, 10, 5, 9, 8, 6])
# a_3
print(a[2])
# 2

# a_3 ~ a_6
print(a[2:6])
# [ 2  8 10  5]

# a_3 ~
print(a[2:])
# [ 2  8 10  5  9  8  6]

# matrix
a = np.array([[3, 4, 2, 8, 10, 5, 9, 8, 6], [1, 2, 3, 4, 5, 6, 7, 8, 9], [9, 8, 7, 6, 5, 4, 3, 2, 1]])

# a_1,:
print(a[0])
# [ 3  4  2  8 10  5  9  8  6]

# a_:,3
print(a[:, 2])
# [2 3 7]

# a_:,2~3
print(a[:, 1:3])
# [[4 2]
#  [2 3]
#  [8 7]]

# a_1,4
print(a[0, 3])
# 8

# using array of indices
i = np.array([0, 2])
print(a[i])
# [[ 3  4  2  8 10  5  9  8  6]
#  [ 9  8  7  6  5  4  3  2  1]]

j = np.array([3, 1])
print(a[i, j])
# [8 8]

2. Các phép toán ma trận

Các phép toán trên ma trận là những tính toán cơ bản khi làm việc với học máy. Trong phần này, tôi không đề cập đầy đủ tất cả các phép toán của nó, mà chỉ đề cập tới các phép toán cơ bản để có thể sài được với học máy cơ bản.

2.1. Nhân ma trận với một vô hướng

Nhân ma trận với một số (vô hướng) là phép nhân số đó với từng phần tử của ma trận.

$$ \alpha [A_{ij}]_{mn} = [\alpha . A_{ij}]_{mn} $$

Ví dụ: $$ 5 \begin{bmatrix} 1 & 2 & 3 \cr 4 & 5 & 6 \end{bmatrix} = \begin{bmatrix} 5 & 10 & 15 \cr 20 & 25 & 30 \end{bmatrix} $$

Các tính chất:

  • Tính giao hoán: $ \alpha A = A \alpha $
  • Tính kết hợp: $ \alpha(\beta A) = (\alpha \beta) A $
  • Tính phân phối: $ (\alpha + \beta) A = \alpha A + \beta A $

Ngoài ra, nhân ma trận với 1 sẽ không làm thay đổi ma trận: $ 1A = A $, còn nhân với 0 sẽ biến ma trận thành ma trận không $ 0A = \varnothing $.

Cách biểu diễn với Numpy:

numpy-matrix.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# create matrix a
a = np.array([(1, 2, 3), (4, 5, 6)])
print(a)
# [[1 2 3]
#  [4 5 6]]

# Multiplying a by 5
b = 5 * a
print(b)
# [[ 5 10 15]
#  [20 25 30]]

# Multiplying a by -1
b = -1 * a
print(b)
# [[-1 -2 -3]
#  [-4 -5 -6]]

2.2. Cộng 2 ma trận

Là phép cộng từng phần tử tương ứng của 2 ma trận cùng cấp với nhau.

$$ [A_{ij}]_{mn} + [B_{ij}]_{mn} = [A_{ij} + B_{ij}]_{mn} $$

Ví dụ: $$ \begin{bmatrix} 5 & 10 & 15 \cr 20 & 25 & 30 \end{bmatrix} + \begin{bmatrix} 1 & 2 & 3 \cr 4 & 5 & 6 \end{bmatrix} = \begin{bmatrix} 6 & 12 & 18 \cr 24 & 29 & 36 \end{bmatrix} $$

Các tính chất:

  • Tính giao hoán: $ A + B = B + A $
  • Tính kết hợp: $ A + (B + C) = (A + B) + C $
  • Tính phân phối: $ \alpha (A + B) = \alpha A + \alpha B $

Ngoài ra, dễ dàng thấy rằng cộng một ma trận với ma trận không thì không làm thay đổi ma trận đó: $ A + \varnothing = A $.

Từ phép nhân ma trận với một số, ta có thể định nghĩa được phép trừ ma trận là phép trừ từng phần tử tương ứng trong ma trận: $ A - \lambda B = A + (-\lambda)B, \lambda \in \mathbb{R} $.

Cách biểu diễn với Numpy:

numpy-matrix.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# create matrix a
a = np.array([(1, 2, 3), (4, 5, 6)])
print(a)
# [[1 2 3]
#  [4 5 6]]

# create matrix b
b = np.array([(0, 5, 25), (4, 9, 9)])
print(b)
# [[0 5 25]
#  [4 9  9]]

# sum of a and b
c = a + b
print(c)
# [[ 1  7 28]
#  [ 8 14 15]]

c = np.add(a, b)
print(c)
# [[ 1  7 28]
#  [ 8 14 15]]

# subtraction of a and b
c = np.subtract(a, b)
print(c)
# [[  1  -3 -22]
#  [  0  -4  -3]]

c = a - b
print(c)
# [[  1  -3 -22]
#  [  0  -4  -3]]

2.3. Nhân 2 ma trận

Nhân 2 ma trận là phép lấy tổng của tích từng phần tử của hàng tương ứng với cột tương ứng. Phép nhân này chỉ khả thi khi số cột của ma trận bên trái bằng với số hàng của ma trận bên phải. Cho 2 ma trận $ [A]mp $ và $ [B]pn $, tích chúng theo thứ tự đó sẽ là một ma trận có số hàng bằng với số hàng của $ A $ và số cột bằng với số cột của $ B $: $ [AB]mn $.

$$ C_{ij} = AB_{ij} = \sum_{k=1}^p{A_{ik} B_{kj}} ~~~, \forall{i = \overline{1,m}; j = \overline{1,n}} $$

Ví dụ: $$ \begin{bmatrix} 1 & 2 & 3 \cr 4 & 5 & 6 \end{bmatrix} \begin{bmatrix} 1 & 2 \cr 3 & 4 \cr 5 & 6 \end{bmatrix} = \begin{bmatrix} 22 & 28 \cr 49 & 64 \end{bmatrix} ~~~~~~~~~~~ (1) $$

$$ \begin{bmatrix} 1 & 2 & 3 \cr 4 & 5 & 6 \end{bmatrix} \begin{bmatrix} 1 & 0 & 0 \cr 0 & 1 & 0 \cr 0 & 0 & 1 \end{bmatrix} = \begin{bmatrix} 1 & 2 & 3 \cr 4 & 5 & 6 \end{bmatrix} ~~~ (2) $$

Các tính chất:

  • Tính kết hợp: $ A(BC) = (AB)C $
  • Tính phân phối: $ A(B+C) = AB + AC $, $ (A+B)C = AC + BC $.

Lưu ý là phép nhân 2 ma trận không có tính chất giao hoán: $ AB \not = BA $.

Nếu bạn để ý ở công thứ 2 phía trên thì sẽ thấy rằng việc nhân với ma trận đơn vị không làm thay đổi ma trận đó: $ AI = IA = A $.

Cách biểu diễn với Numpy:

numpy-matrix.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# create matrix A
A = np.array([(1, 2, 3), (4, 5, 6)])
print(A)
# [[1 2 3]
#  [4 5 6]]

# create matrix B
B = np.array([(0, 5), (4, 9), (9, 0)])
print(B)
# [[0 5]
#  [4 9]
#  [9 0]]

# product of A and B
C = A.dot(B)
print(C)
# [[35 23]
#  [74 65]]

C = np.dot(A, B)
# [[35 23]
#  [74 65]]

2.4. Chuyển vị ma trận

Chuyển vị là phép biến cột thành hàng và hàng thành cột của một ma trận. Cho ma trận $ [A]_{mn} $ thì chuyển vị của nó là $ [B_{ij}]_{nm} = [A_{ji}]_{mn}^\intercal $ ($ \intercal $ là kí hiệu của phép chuyển vị) có $ B_{ij} = A_{ji} ~~~, \forall i,j $. Ví dụ: $$ \begin{bmatrix} 1 & 2 \cr 3 & 4 \cr 5 & 6 \end{bmatrix} ^\intercal = \begin{bmatrix} 1 & 3 & 5 \cr 2 & 4 & 6 \end{bmatrix} $$

Các tính chất:

  • $ (A^\intercal)^\intercal = A $
  • $ (A + B)^\intercal = A^\intercal + B^\intercal $
  • $ (AB)^\intercal = B^\intercal A^\intercal $
  • $ (cA)^\intercal = cA^\intercal $

Ngoài ra, ta có thể thực hiện phép nhân số học với 2 véc-tơ để thu được 1 vô hướng bằng cách nhân với chuyển của 1 trong 2 véc-tơ: $ ab = a^\intercal b $. Phép nhân kiểu này còn được gọi là phép nhân vô hướng, tức là tổng của tích mỗi phần tử tương ứng của 2 ma trận.

numpy-matrix.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# create matrix a
A = np.array([[1, 2, 3], [4, 5, 6]])

# transpose matrix of A
B = A.transpose()
print(B)
# [[1 4]
#  [2 5]
#  [3 6]]

# vector inner product
a = np.arange(10)
b = np.ones(10)
c = a.dot(b)
print(c)
# 45.0

c = np.inner(a, b)
print(c)
# 45.0

Từ đoạn mã trên, ta có thể thấy rằng với Numpy, ta có thể thực hiện ngay được phép nhân vô hướng của 2 véc-tơ (inner product) mà không cần phải chuyển vị ma trận.

2.5. Ma trận nghịch đảo

Ma trận của ma trận vuông khả nghịch $ A $ cấp n là ma trận $ B $ sao cho tích của chúng là ma trận đơn vị cùng cấp: $ AB = I_n $. Ma trận khả nghịch được kí hiệu là: $ A^{-1} $. Tức là $ A A^{-1} = I_n $.

Các tính chất:

  • $ (A^{-1})^{-1} = A $
  • $ (kA)^{-1} = k^{-1} A^{-1} ~~~, \forall k \not = 0 $
  • $ (AB)^{-1} = B^{-1} A^{-1} $
  • $ (A^\intercal)^{-1} = (A^{-1})^\intercal $

Ngoài ra nếu để ý sẽ thấy ma trận đơn vị luôn có nghịch đảo là chính nó: $ I_n^{-1} = I_n $, còn ma trận không không tồn tại nghịch đảo - hay ta gọi nó là không khả nghịch.

Để xem một ma trận vuông có khả nghịch hay không và cách tìm ma trận nghịch đảo tương ứng của nó tôi sẽ trình bày trong bài viết tới. Tạm thời bạn cứ nắm được khái niệm của nó và cách lập trình đã nhé.

Cách biểu diễn với Numpy:

numpy-matrix.py
1
2
3
4
5
6
7
8
# create matrix a
A = np.array([[1., 2.], [3., 4.]])

# inverse matrix of A
B = np.linalg.inv(A)
print(B)
# [[-2.   1. ]
#  [ 1.5 -0.5]]

2.6. Phép nhân từng phần tử Hadamard

Là phép nhân từng phần tử tương ứng của 2 ma trận cùng cấp với nhau.

$$ [A_{ij}]_{mn} \circ [B_{ij}]_{mn} = [A_{ij} B_{ij}]_{mn} $$

Ví dụ: $$ \begin{bmatrix} 5 & 10 & 15 \cr 20 & 25 & 30 \end{bmatrix} \circ \begin{bmatrix} 1 & 2 & 3 \cr 4 & 5 & 6 \end{bmatrix} = \begin{bmatrix} 5 & 20 & 45 \cr 80 & 115 & 180 \end{bmatrix} $$

Các tính chất:

  • Tính giao hoán: $ A \circ B = B \circ A $
  • Tính kết hợp: $ A \circ (B \circ C) = (A \circ B) \circ C $
  • Tính phân phối: $ A \circ (B + C) = A \circ B + A \circ C $
numpy-matrix.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# create matrix a
a = np.array([(1, 2, 3), (4, 5, 6)])
print(a)
# [[1 2 3]
#  [4 5 6]]

# create matrix b
b = np.array([(0, 5, 25), (4, 9, 9)])
print(b)
# [[0 5 25]
#  [4 9  9]]

# Multiplying of a and b
c = a * b
print(c)
# [[ 0 10 75]
#  [16 45 54]]

c = np.multiply(a, b)
print(c)
# [[ 0 10 75]
#  [16 45 54]]

2.7. Các phép toán theo từng phần tử (Hadamard) khác

Ngoài phép nhân Hadamard theo từng phần tử, ta cũng có các phép biến đổi khác tương tự như:

Phép chia Hadamard: $ [A_{ij}]_{mn} \oslash [B_{ij}]_{mn} = [A_{ij} / B_{ij}]_{mn} $.

Phép lũy thừa Hadamard: $ [A_{ij}]_{mn}^p = [A_{ij}^p]_{mn} ~~~, \forall p \in \mathbb{R} $

Từ phép lũy thừa với số mũ phân số, ta có thể viết lại dưới dạng phép khai căn Hadamard: $ \sqrt[p]{[A_{ij}]_{mn}} = [\sqrt[p]{A_{ij}}]_{mn} ~~~, \forall p \in \mathbb{N} $

numpy-matrix.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# create matrix a
a = np.array([(1., 2., 3.), (4., 5., 6.)])
print(a)
# [[1. 2. 3.]
#  [4. 5. 6.]]

# create matrix b
b = np.array([(0., 5., 25.), (4., 9., 9.)])
print(b)
# [[0. 5. 25.]
#  [4. 9.  9.]]

# divide
c = b / a # c = np.divide(b, a)
print(c)
# [[ 0.          2.5         8.33333333]
#  [ 1.          1.8         1.5       ]]

# power of 2
c = a ** 2
print(c)
# [[  1.   4.   9.]
#  [ 16.  25.  36.]]

2.8. Norm

Trong không gian véc-tơ, Norm là một công cụ để đo độ dài của véc-tơ hay nói cách khác là đo khoảng cách giữa 2 điểm trong không gian. Đáng lẽ ta phải tìm hiểu không gian véc-tơ là gì các tính chất của nó ra sao để có thể hiểu rõ được độ dài của các véc-tơ và quan hệ của chúng ra sao, nhưng tạm thời lúc này bạn cứ hiểu nôm na nó là một tập chứa các véc-tơ và khoảng cách 2 điểm như một đường bay nối thẳng 2 điểm đó như ở không gian 2 chiều nhé. Nếu có dịp tôi sẽ trình bày kĩ hơn về không gian véc-tơ sau.

Thường một norm cấp $ p $ (kí hiệu: $ L^p $) hay sử dụng trong học máy được mô tả bằng công thức:

$$ \|x\|_p = \Bigg(\sum_i{|x_i|^p}\Bigg)^ \frac{1}{p} $$

Trong đó $ p \in \mathbb{R} $ và $ p \ge 1 $, còn $ x_i $ là phẩn tử thứ $ i $ của véc-tơ. Dễ nhận thấy rằng một norm không thể nào nhỏ hơn $ 0 $ được, vì làm gì có khoảng cách nào âm đúng không.

Ví dụ trong không gian Euclide n chiều quen thuộc, ta có độ dài của véc-tơ chính là một norm cấp 2 ($ L^2 $): $\displaystyle \|x\| = \sqrt{ \sum_{i=1}^n{x_i^2} } $. Trong đó $ x_i $ là phần tử thứ $ i $ của véc-tơ, hay nói cách khác là tọa độ trên trục thứ $ i $ tương ứng của véc-tơ trong không gian.

Một norm cấp 2 thường được kí hiệu đơn giản là $ \|x\| $ chứ không phải là $ \|x\|_2 $ nhé. Nguyên nhân là do chúng rất hay được sử dụng trong các bài toán học máy, nên phần định danh dưới được bỏ đi cho tiện làm việc. Nếu để ý sẽ thấy, với công thức trên ta có thể dễ dàng tính được $ L^2 $ của một véc-tơ bằng phép nhân vô hướng của chúng: $ \|x\| = x * x^\intercal $.

Đôi lúc ta cũng sử dụng cả norm cấp 1 để đo khoảng cách giữa các phần tử tại vị trí 0 và vì trị rất gần với 0 thay vì norm cấp 2. Vì norm cấp 2 lấy bình phương từng khoảng cách lên sẽ cho số rất nhỏ do khoảng cách giữa chúng đã rất nhỏ rồi, việc này dẫn tới sự triệt tiêu khoảng cách khi tính toán. Những lúc này ta sẽ sử dụng $ L ^ 1$ để tính toán, vì nó chỉ lấy trị tuyệt đối khoảng cách: $\displaystyle \|x\|_1 = \sum_i{|x_i|} $.

Cũng có khi ta phải dùng giả norm bậc 0 ($ L^0 $) để đo độ dài của véc-tơ khi nó quá bé để có thể thoải mái tính toán với các norm khác. Lưu ý rằng, ta không thực sự có $ L^0 $ nhé, mà khi nhắc tới nó ta phải hiểu ngầm với nhau rằng chúng là giả norm. $ L^0 $ được tính đơn giản bằng cách đếm số lượng các phần tử khác 0 của véc-tơ.

Một dạng norm khác cũng được sử dụng phổ biến trong học máy là $ L^\infty$ hay còn được gọi là norm lớn nhất (max norm) được đo bằng cách lấy trị tuyệt đối của phần tử lớn nhất: $\displaystyle \|x\|_\infty = \max_i{|x_i|} $.

Đó là với cách tính norm cho véc-tơ, thế còn norm áp dụng cho ma trận thì sao? Norm cho ma trận được tính bằng nhiều phương pháp khác nhau, nhưng trong học máy thường ta chỉ dùng tới chuẩn norm Frobenius - tương tự như $ L^2 $ cho véc-tơ, như sau:

$$ \|A\|_F = \sqrt{\sum_{i,j}{A_{ij}^2}} $$

Để thực hiện việc tính norm với Numpy, ta có thể sử dụng hàm norm trong gói linalg (gói đại số tuyến tính) như sau:

numpy-matrix.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# for vector
v = np.arange(10)
print(v)
# [0 1 2 3 4 5 6 7 8 9]
print(np.linalg.norm(v))
# 16.881943016134134

# for matrix
A = v.reshape(2, 5)
print(A)
# [[0 1 2 3 4]
#  [5 6 7 8 9]]
print(np.linalg.norm(A))
# 16.881943016134134

3. Numpy thường nhật

3.1. Biến hình

Các mảng trong Numpy đều định quy định bằng một khung (shape) với các chiều tương ứng với các chiều của mảng. Ví dụ như véc-tơ (mảng 1 chiều) có 10 phần tử sẽ có khung là (10,), ma trận (mảng 2 chiều) có 5 hàng và 3 cột sẽ có khung là (5, 3).

Đôi lúc ta cần biến đổi hình dạng của các mảng này cho phù hợp với bài toán của ta như biến đối mảng 1 chiều thành 2 chiều (véc-tơ thành ma trận). Để làm được việc này ta có thể sử dụng phép biến hình np.reshape(a, newshape, order='C') của Numpy cực dễ dàng:

numpy-matrix.py
1
2
3
4
5
6
7
a = np.eye(5, 3)
print(a.shape)
# (5, 3)

b = a.reshape(15)
print(b.shape)
# (15,)

Giờ câu hỏi đặt ra là quy tắc biến hình được thực hiện ra sao?

Đầu tiên, lượng phần tử cần phải được giữ nguyên sau khi biến hình, tức là hình mới được sinh ra phải đảm bảo được ràng buộc này. Ví dụ ở trên ta có một ma trận chéo kích thước (5, 3) có số phần tử là 15, nên khi biến nó thành véc-tơ thì véc-tơ này cũng phải có kích thước là 15 tương ứng.

Tiếp theo, là cách tổ chức các phần tử khi biến hình. Numpy cho phép ta thực hiện biến hình với 3 kiểu sắp xếp phần tử như sau:

  • C: Cho phép ta nhặt phần tử từ mảng cũ ra theo thứ tự ngôn ngữ lập trình C, tức là tọa độ sau sẽ thay đổi nhanh hơn tọa độ trước. Cụ thể là phần từ phía sau của A[0, 0]A[0, 1]. Lưu ý rằng, thứ tự này là giá trị mặc định của reshape.
  • F: Cho phép ta nhặt phần tử từ mảng cũ ra theo thứ tự ngôn ngữ lập trình Fortran, tức là tọa độ trước sẽ thay đổi nhanh hơn tọa độ sau. Cụ thể là phần từ phía sau của A[0, 1]A[1, 1]
  • A: Cho phép ta nhặt phần từ theo kiểu F nếu mảng cũ của ta được tổ chức dưới dạng bộ nhớ liên tục của Fortran. Còn các trường hợp khác nó sẽ nhặt theo kiểu C. Lưu ý rằng, kiểu FC chỉ đề cập tới thứ tự địa chỉ khi lập trình chứ không phải địa chỉ trong bộ nhớ máy tính.

Tổ chức dưới dạng bộ nhớ liên tục có nghĩa là thứ tự của phần tử trong mảng đúng với thứ tự liên tục trong bộ nhớ máy tính. Ví dụ, với Fortran hai phần tử A[0, 0]A[1, 0] sẽ có địa chỉ bộ nhớ cạnh nhau theo đúng thứ tự đó, còn với C thì A[0, 0]A[0, 1] sẽ có địa chỉ bộ nhớ cạnh nhau theo đúng thứ tự đó. Nếu bạn chưa hiểu thì có thể đọc thêm ở bài viết này.

3.2. Tự động mở rộng

Một trong những tính năng đặc biệt của Numpy là khả năng tự mở rộng mảng (broadcasting), nhờ có tính năng này mà ta có thể thực hiện được các phép toán của các hàm phổ cập (universal functions). Khi thực hiện phép toán giữa 2 mảng không cùng số chiều với nhau, Numpy sẽ tự mở rộng mảng có số chiều ít hơn lên cho bằng với mảng có số chiều lớn hơn. Việc này được thực hiện bằng cách lặp đi lặp lại 1 số lần nào đó của mảng có số chiều nhỏ hơn cho tới khi đạt được số chiều mảng kia.

Cụ thể bạn có thể coi 1 số ví dụ dưới đây:

numpy-matrix.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
A = np.array([(10, 8, 9), (5, 6, 4)])
b = np.arange(3)

# A + b or b + A
C = A + b
print(C)
# [[10  9 11]
#  [ 5  7  6]]

# b * A or A * b
C = b * A
print(C)
# [[ 0  8 18]
#  [ 0  6  8]]

Nhìn vào ví dụ trên ta có thể thấy véc-tơ b đã tự động mở rộng số hàng của mình cho bằng với số hàng của ma trận A để thực hiện các phép toán với A. Việc mở rộng này là được áp dụng cho tất cả các hàm phổ thông của Numpy, tức là không chỉ 2 phép cộng và nhân phía trên ta còn thực hiện được với nhiều phép toán khác như phép chia, phép trừ, phép lấy mũ,…

Các hàm phổ cập (universal functions) là các hàm tính toán số học thông dụng như sin, cos, exp của Numpy. Cụ thể các hàm ra sao bạn có tham khảo ở đây.

Ngoài các hàm tiện ích của Numpy phía trên, ta còn có nhiều hàm hữu dụng khác để thao tác với ma trận mà tôi không đề cập ở đây, nhưng các bạn có thể xem chi tiết trên trang hướng dẫn của Numpy tại đây.

Hi vọng qua bài viết này, bạn có thể nắm được các phép toán cơ bản của ma trận cùng với các lệnh thao tác tương ứng của Numpy. Nếu có thắc mắc hay góp ý gì thì hãy để lại bình luận phía dưới cho mình nhé.