[Nhập môn Machine Learning] Bài 8: Giới thiệu các thư viện Numpy, Matplotlib, Pandas (P1: Numpy)


Trước khi có thể cài đặt thuật toán Machine Learning, chúng ta cần tìm hiểu về các công cụ trước đã. Trong bài viết này, mình muốn giới thiệu với các bạn "bộ ba quyền lực" trong các thư viện Machine Learning của ngôn ngữ Python đó là:
  • Numpy: thư viện hỗ trợ tính toán ma trận
  • Matplotlib: thư viện hỗ trợ biểu diễn đồ thị
  • Pandas: thư viện giúp thao tác với dữ liệu bảng biểu (hay còn có tên gọi khác là Excel của lập trình viên).
Chúng ta sẽ lần lượt đi qua cách cài đặt, các hàm và cách sử dụng cơ bản của Numpy, Matplotlib và Pandas cùng với code mẫu để các bạn có thể làm theo trong quá trình đọc bài viết. Nhưng mình sẽ nói nhiều hơn về numpy hơn là 2 thư viện còn lại vì tính quan trọng của thư viện này

Python và pip

Trong bài viết này, mình sử dụng Python 3.7 . Để kiểm tra máy bạn đã có Python chưa, hãy mở cmd/Terminal lên và gõ
python --version

hoặc
python3 --version

nếu lệnh trên không hoạt động. Nếu máy bạn đã có Python thì sẽ được trả lại kết quả như bên dưới.

Nếu kết quả khác thì bạn có thể tới trang chủ của Python và tải về.

Khi bạn tải Python về, đồng thời bạn cũng sẽ được cài đặt luôn pip. Pip là trình quản lý thư viện của Python và chúng ta sẽ sử dụng pip để cài đặt 3 thư viện trên. Tuy nhiên, để chắc chắc bạn cũng nên kiểm tra xem pip có được cài hay không. Tùy thuộc vào lệnh bạn gõ ở trên để kiểm tra là python hay python3 thì bạn cũng có thể sử dụng các lệnh tương ứng để gọi pip là pip hoặc pip3. Do trên máy của mình sử dụng python3 nên mình sẽ sử dụng lệnh dưới để kiểm tra.
pip3 --version

Và mình được kết quả:

Về cơ bản, chúng ta đã có thể bắt đầu nhưng để việc viết code dễ dàng, bạn có thể download thêm một text editor hoặc một IDE như PyCharm.

Cài đặt

Việc cài đặt 3 thư viện cùng một lúc được thực hiện bằng cách gọi pip cùng với lệnh
pip3 install numpy matplotlib pandas

hoặc có thể cài đặt từng thư viện một
pip3 install numpy

pip3 install matplotlib

pip3 install pandas
Chúc mừng, vậy là bạn đã cài đặt được 3 thư viện cần thiết để bắt đầu.

Một lưu ý nhỏ cho những bạn sử dụng Text Editor: Để có thể chạy chương trình python, bạn cần phải bật terminal tại vị trí file .py muốn chạy và gõ
python3 <filename>.py
hoặc
python <filename>.py 

Giới thiệu về Numpy

Tạo mảng đa chiều ndarray

Numpy là một thư viện quan trọng để tương tác với mảng nhiều chiều. Thư viện này lưu trữ dữ liệu trong mảng dưới dạng mảng đặc biệt gọi là ndarray. Để bắt đầu, hãy lấy một text editor/IDE bạn thích và tạo một file "basic.py" sau đó thêm vào
import numpy as np

để tải thư viện Numpy và gọi nó thông qua cái tên "np". Lúc này, bạn có thể tạo một mảng trong numpy bằng
a = np.array([1,2,3])
và khi in ra màn hình ta sẽ được kết quả
print(a)
"""
Output: [1 2 3]
"""
Chú ý rằng đây là một ndarray chứ không phải một mảng bình thường trong Python. Ta có thể kiểm chứng bằng lệnh
print(type(a))
"""
Output: <type 'numpy.ndarray'>
"""
Mảng ta vừa tạo chỉ là mảng một chiều, ta cũng có thể tạo mảng 2 chiều và 3 chiều bằng cách tương tự (để xác định số chiều, một mẹo nhỏ là bạn nên nhìn số kỹ tự '[' ở ngoài cùng)
b = np.array([[1,2,3],
              [4,5,6]])
print(b)
"""
Output:

[[1 2 3]
 [4 5 6]]
 
"""

c = np.array([[[1,2,3], [4,5,6]], 
              [[7,8,9], [10,11,12]]])
print(c)
"""
Output:

[[[ 1  2  3]
  [ 4  5  6]]

 [[ 7  8  9]
  [10 11 12]]]
  
"""
Ta có thể biết được kích thước của các mảng đa chiều bằng đoạn code sau.
print(a.shape) # Output: (3,)
print(b.shape) # Output: (2, 3)
print(c.shape) # Output: (2, 2, 3)
Như các bạn đã thấy, các mảng của chúng ta lần lượt là mảng 1 chiều với 3 phần tử, một ma trận với 2 dòng và 3 cột và cuối cùng là một tensor (tên của mảng nhiều chiều hơn 2) với kích thước (2, 2, 3).

Có một lỗi mình hay gặp khi thao tác với numpy đó là không để ý số chiều của mảng. Ví dụ như trong đoạn code sau
d = np.array([[1,2,3]])
nhìn có vẻ giống với mảng a chúng ta đã tạo
a = np.array([1,2,3])
nhưng thật ra d là mảng 2 chiều (hãy đếm số ký tự '[') còn a là mảng một chiều. Chúng ta cũng có thể kiểm chứng điều này bằng cách in ra kích thước của chúng.
print(a.shape) # Output: (3,)
print(d.shape) # Output: (1, 3)

Các ma trận khác

Numpy cũng cung cấp cho chúng ta những hàm để tạo một vài ma trận cụ thể như:
# Tạo một ma trận 2x2 với toàn 1
print(np.ones((2,2)))
"""
Output:
[[1. 1.]
 [1. 1.]]
"""

# Tạo một ma trận 2x2 với toàn 0
print(np.zeros((2,2)))
"""
Output:
[[0. 0.]
 [0. 0.]]
"""

# Tạo một ma trận đơn vị 3x3
print(np.eye(3))
"""
Output:
[[1. 0. 0.]
 [0. 1. 0.]
 [0. 0. 1.]]
"""

# Tạo một ma trận ngẫu nhiên với kích thước 3x2
print(np.random.rand(3,2))
"""
Output:
[[0.96600899 0.04178217]
 [0.94935854 0.90623781]
 [0.72550038 0.23328825]]
"""

Các phép toán trên ma trận

Nếu chúng ta có 2 ma trận cùng kích thước với nhau, ta có thể thực hiện cộng, trừ, nhân, chia theo phần tử giữa 2 ma trận này. Giả sử với tính cộng, phần tử ở vị trí (1,1) ở ma trận a sẽ được cộng với phần tử ở vị trí (1,1) ở ma trận b và cứ thế cho tới phần tử ở vị trí cuối cùng.
a = np.array([[1,0,2],
              [2,3,0]])
b = np.array([[2,1,-1],
              [2,1,2]])

print(a+b) # Output: [[3 1 1]
           #          [4 4 2]]
                                          
print(a-b) # Output: [[-1 -1  3]
           #          [ 0  2 -2]]
                                         
print(a*b) # Output: [[ 2  0 -2]
           #          [ 4  3  0]]
                                          
print(a/b) # Output: [[ 0.5  0.  -2. ]
           #          [ 1.    3.    0. ]]
Broadcasting
Tuy nhiên, không chỉ dừng ở đó, numpy còn hỗ trợ một tính năng gọi là broadcasting. Broadcasting giúp chúng ta có thể cộng, trừ, nhân, chia một ma trận với một số nguyên hoặc một ma trận khác mà kích thước của nó có ít nhất một chiều bằng với ma trận kia. Để hiểu rõ hơn, cách tốt nhất là đưa ra một ví dụ.
a = np.array([[1,0,2],
              [2,3,0]]) # shape: (2,3)
b = np.array([[1,2,1]]) # shape: (1,3)
c = np.array([[1],
              [2]])     # shape: (2,1)

print(a-1) # Output: [[ 0 -1  1]
           #          [ 1  2 -1]]
                      
print(a-b) # Output: [[ 0 -2  1]
           #          [ 1  1 -1]]
                      
print(a-c) # Output: [[ 0 -1  1]
           #          [ 0  1 -2]]
Có lẽ bạn đã hình dung được cách thức hoạt động của broadcasting, nếu chúng ta thiếu dữ liệu trên một chiều thì numpy sẽ tự động copy dữ liệu có sẵn đến khi nào đủ thì thôi. Đáng tiếc là broadcasting chỉ hoạt động khi giá trị của chiều còn thiếu là 1, nếu ma trận c ở trên có kích thước (2x2) thì chúng ta không thể làm điều tương tự. Để hiểu sâu hơn về broadcasting, bạn có thể đọc thêm tại trang chính thức của Numpy.
Chuyển vị ma trận
b = np.array([[2,1,-1],
              [2,1,2]])
print(b) # Output: [[ 2  1 -1]
         #          [ 2  1  2]]

print(b.T) # Output: [[ 2  2]
           #          [ 1  1]
           #          [-1  2]]
Nhân ma trận
a = np.array([[1,0,2],
              [2,3,0]])
b = np.array([[2,1,-1],
              [2,1,2]])
              
mul = np.matmul(a, b.T)
print(mul) # Output: [[0 6]
           #          [7 7]]

Truy cập một phần tử trong ma trận

Giống như với mảng 2 chiều thông thường, ta có thể truy cập phần tử trong mảng bằng cú pháp
arr[nrow][ncol]
"""
nrow: index của dòng cần truy cập
ncol: index của cột cần truy cập 
"""
Tuy nhiên, chúng ta cũng có thể truy cập phần tử trong mảng với cú pháp khác gọn gàng hơn
arr[nrow, ncol]
"""
nrow: index của dòng cần truy cập
ncol: index của cột cần truy cập 
"""
Ví dụ:
a = np.array([[1,0,2],
              [2,3,0]])
# Truy cập ----^
print(a[1,0]) # Output: 2
Như trên, ta đã truy cập phần tử nằm ở dòng 2 (có index 1) và cột 1 (có index 0).
Slicing
Slicing là cách để truy cập nhiều phần tử trong mảng cùng một lúc được hỗ trợ bởi mảng trong numpy. Cú pháp slice như sau:
start_index : end_index:step
"""
lấy từ start_index đến end_index-1 (không bao gồm end_index) với mỗi bước
khoảng cách là step
"""
Nếu start_index hay end_index được bỏ trống thì mặc định là lấy hết từ đầu (với start_index) hoặc đến cuối (với end_index). step nếu không đề cập thì mặc định là 1.

Ví dụ:
a = np.array([[1,0,2],
              [2,3,0]])
              
# Lấy nguyên dòng 2
print(a[1,:]) # Output: [2 3 0]

# Lấy nguyên cột 2
print(a[:,1]) # Output: [0 3]

# Lấy đến dòng 2 và đến cột 2
print(a[:2, :2]) # Output: [[1 0]
                 #          [2 3]]
Masking
Một cách để truy cập những phần tử trong numpy đó chính là sử dụng một mảng boolean với để xác định những phần tử cần lấy. Việc này rất có ích khi chúng ta có thể sử dụng masking để lấy ra những giá trị thỏa những tiêu chí cụ thể trong mảng.

Giả sử mình muốn lấy những phần tử lớn hơn hoặc bằng 2 trong mảng, mình có thể làm như sau:
a = np.array([[1,0,2],
              [2,3,0]])
mask = a>=2
print(a[mask]) # Output: [2 2 3]
Nếu bạn in ra giá trị của mask, bạn sẽ thấy mask chỉ là một mảng boolean có kích thước bằng với a và những vị trí (0,2), (1,0) và (1,1) đều có giá trị True tương ứng với vị trí các giá trị thỏa yêu cầu.

Thay đổi giá trị và kích thước của mảng

Thay đổi giá trị
a = np.array([[1,0,2],
              [2,3,0]])
              
# Normal
a[0,1] = 5
print(a) # Output: [[1 5 2]
         #          [2 3 0]]

# Slicing          
a[:,0] = np.zeros((2,)) # Kích thước mảng mới phải trùng với kích thước trả về
                        # Ở đây bạn có thể chỉ gán giá trị 0 cũng được,  
                        # broadcasting sẽ làm nhiện vụ của nó
print(a) # Output: [[0 5 2]
         #          [0 3 0]]

# Masking
a = np.array([[1,0,2],
              [2,3,0]])
a[mask] = -1
print(a) # Output: [[ 1  0 -1]
         #          [-1 -1  0]]
Thay đổi kích thước
Mỗi ndarray đều có một hàm reshape để thay đổi kích thước của mảng, hàm này lấy một giá trị là 2 kích thước mới. Hàm reshape này cũng có thể lấy 1 giá trị đặc biệt là -1 tức là "sao cũng được, tôi không quan tâm giá trị của nó là bao nhiêu".
a = np.array([[1,0,2],
              [2,3,0]])
print(a.reshape((3,2))) # Output: [[1 0]
                        #          [2 2]
                        #          [3 0]]
# Ta chỉ cần biết ta muốn 1 dòng và ta không quan tâm đến chiều còn lại
print(a.reshape((1,-1))) # Output: [[1 0 2 2 3 0]]

Tạm kết

Do bài viết đã dài (và mình đã mỏi tay) nên chúng ta sẽ tìm hiểu matplotlib và pandas ở bài sau. Nếu các bạn có ý kiến hoặc góp ý gì về bài viết của mình, mong các bạn để lại bình luận bên dưới để mình có thể phát triển bài viết hơn.

Mã nguồn đầy đủ

import numpy as np

# Tạo mảng
a = np.array([1,2,3])
print("1D array")
print(a)

# Xác định kiểu của mảng
print("Type: ", type(a))

# Tạo mảng 2 chiều (ma trận)
b = np.array([[1,2,3],
              [4,5,6]])
print("2D array")
print(b)

# Tạo mảng 3 chiều (tensor)
c = np.array([[[1,2,3], [4,5,6]], 
              [[7,8,9], [10,11,12]]])
print("3D array")
print(c)

# Mảng 2 chiều chứa 1 dòng, nhưng không phải mảng 1 chiều
d = np.array([[1,2,3]])

# In ra kích thước các mảng
print("Shape of a: ", a.shape)
print("Shape of a: ", b.shape)
print("Shape of a: ", c.shape)
print("Shape of a: ", d.shape)

# Các ma trận khác
print("Matrix of ones")
print(np.ones((2,2)))
print("Matrix of zeros")
print(np.zeros((2,2)))
print("Identity matrix")
print(np.eye(3))
print("Random matrix")
print(np.random.rand(3,2))
# Các toán tử cơ bản
a = np.array([[1,0,2],
              [2,3,0]])
b = np.array([[2,1,-1],
              [2,1,2]])

print("a+b: ", a+b)
print("a-b: ", a-b)
print("a*b: ", a*b)
print("a/b: ", a/b)

# Broadcasting
a = np.array([[1,0,2],
              [2,3,0]])
b = np.array([[1,2,1]])
c = np.array([[1],
              [2]])
print("Broadcasting")
print(a-1)
print(a-b)
print(a-c)
# Chuyển vị ma trận
b = np.array([[2,1,-1],
              [2,1,2]])
print("b: ", b)
print("b transpose: ", b.T)

# Nhân ma trận
mul = np.matmul(a, b.T)
print("a.bT: ", mul)

# Truy cập mảng
a = np.array([[1,0,2],
              [2,3,0]])
print(a[1,0])
print(a[1,:])
print(a[:,1])
print(a[:2, :2])

# Masking
a = np.array([[1,0,2],
              [2,3,0]])
mask = a>=2
print("Elements that greater than or equal 2")
print(a[mask])

# Thay đổi phần tử trong mảng
a = np.array([[1,0,2],
              [2,3,0]])
print("Changed (0,1)")
a[0,1] = 5
print(a)
print("Changed column 0")
a[:,0] = np.zeros((2,))
print(a)

print("Change with masking")
a = np.array([[1,0,2],
              [2,3,0]])
a[mask] = -1
print(a)

# Thay đổi kích thước mảng
a = np.array([[1,0,2],
              [2,3,0]])
print("Reshape to 3x2")
print(a.reshape((3,2)))
print("Reshape to one row (still 2D)")
print(a.reshape((1,-1)))

Nhận xét

Bài đăng phổ biến từ blog này

Phép phân tích ma trận A=LU

Độc lập tuyến tính và phụ thuộc tuyến tính

Thuật toán tính lũy thừa nhanh. Giải thích một cách đơn giản