Mục lục
Mục tiêu bài học
Sau bài học, bạn sẽ:
- Biết NumPy là gì và vị trí của nó trong hệ sinh thái Python AI.
- Cài đặt NumPy và import đúng convention.
- Tạo một
ndarraytừ list Python. - Đọc các attribute
shape,ndim,size,dtype,itemsize,nbytes. - Giải thích vì sao ndarray nhanh hơn list Python.
- Biết các dtype thường gặp trong DL và cách convert.
Module 4 (bài 28 đến 33) là phần NumPy. Bài 28 mở đầu, đặt nền cho indexing, slicing, vectorization, reshape, broadcasting ở các bài sau.
NumPy là gì
NumPy (Numerical Python) là thư viện array số học của Python, được công bố lần đầu năm 2006 bởi Travis Oliphant. Lõi NumPy viết bằng C và Fortran, expose API Python để thao tác mảng đa chiều và đại số tuyến tính ở tốc độ gần native.
Hầu hết thư viện AI / data science trong Python đều xây trên NumPy hoặc tương thích với NumPy:
- pandas — Series và DataFrame backed bởi NumPy array.
- scikit-learn — input và output của mọi estimator là NumPy array.
- SciPy — tối ưu hoá, thống kê, xử lý tín hiệu trên array NumPy.
- matplotlib — vẽ trực tiếp từ array NumPy.
- PyTorch —
torch.Tensorchuyển qua lại vớinumpy.ndarraykhông copy bộ nhớ (khi cùng device CPU). - TensorFlow —
tf.Tensornhận và xuất NumPy array qua.numpy().
Phiên bản dùng trong bài này: NumPy >= 1.26 (release cuối 2023, hỗ trợ Python 3.9 đến 3.12). NumPy 2.0 ra tháng 06/2024 có vài breaking change về dtype và scalar; phần lớn API cơ bản trong bài này vẫn giữ nguyên.
Cài đặt và import
Trên máy local, dùng pip:
pip install numpy
Hoặc qua conda:
conda install numpy
Trên Google Colab và Kaggle, NumPy đã được cài sẵn — chỉ cần import.
Convention chuẩn (xuất hiện trong gần như mọi codebase):
import numpy as np
print(np.__version__) # ví dụ: 1.26.4
Alias np là quy ước cộng đồng, được dùng trong tài liệu chính thức của NumPy, pandas, scikit-learn, PyTorch. Không nên dùng tên khác.
Tạo array đầu tiên
Cách đơn giản nhất là gọi np.array() với một list Python:
import numpy as np
a = np.array([1, 2, 3])
print(a) # [1 2 3]
print(type(a)) # <class 'numpy.ndarray'>
Kết quả là một object numpy.ndarray — đây là kiểu dữ liệu cốt lõi của NumPy, viết tắt của N-dimensional array. Một ndarray giống tensor đã học ở bài 20 — có thể có 0, 1, 2 hoặc nhiều chiều.
Ví dụ ma trận 2D:
M = np.array([
[1, 2, 3],
[4, 5, 6],
])
print(M.shape) # (2, 3)
Khi list đầu vào có lẫn kiểu, NumPy promote (đẩy lên) kiểu rộng nhất chứa được tất cả phần tử:
np.array([1, 2, 3]).dtype # dtype('int64')
np.array([1, 2.0, 3]).dtype # dtype('float64') — có 1 float, mọi phần tử lên float
np.array([1, 2.0, 'x']).dtype # dtype('<U32') — chuỗi Unicode, mọi phần tử lên str
Đây là điểm khác biệt cơ bản so với list Python: mọi phần tử trong một ndarray có cùng kiểu.
Vì sao ndarray nhanh hơn list Python
Trên cùng một phép tính số học, ndarray thường nhanh hơn list Python từ 10 đến 100 lần. Có 4 lý do chính.
1. Contiguous memory — phần tử nằm liền nhau trong RAM
List Python là array of pointers: mỗi phần tử của list là một con trỏ tới một object Python riêng biệt nằm rải rác trong heap. Để đọc lst[i], CPU phải dereference con trỏ và nhảy tới một địa chỉ bất kỳ.
Ndarray lưu các phần tử dưới dạng buffer C liền kề — toàn bộ \( n \) phần tử nằm trong một khối bộ nhớ liên tục. Lợi ích:
- Cache-friendly: CPU đọc theo cache line 64 byte; nếu các phần tử nằm sát nhau, một lần load cache mang về nhiều phần tử cùng lúc.
- Prefetch dễ: CPU đoán được địa chỉ phần tử tiếp theo và load trước, giảm cache miss.
2. Cùng kiểu dữ liệu (homogeneous)
Mỗi phần tử của list Python là một object đầy đủ (PyObject), tốn khoảng 28 byte chỉ riêng cho int nhỏ — gồm reference count, type pointer, value. Khi thao tác, interpreter phải kiểm tra kiểu rồi mới dispatch toán tử phù hợp.
Ndarray chỉ lưu giá trị thuần (4 byte cho int32, 8 byte cho float64), không kèm metadata cho từng phần tử. Type check chỉ làm một lần cho cả array, không phải mỗi phần tử.
3. Loop ở C level
Cộng hai list bằng vòng for trong Python phải đi qua interpreter cho từng iteration — interpreter overhead lớn (bytecode dispatch, type check, object creation). Các phép tính NumPy như a + b gọi xuống vòng lặp C / Fortran đã biên dịch, không qua interpreter Python.
4. SIMD / vectorization ở CPU level
CPU hiện đại có các tập lệnh SIMD (Single Instruction Multiple Data) — AVX2, AVX-512 trên x86, NEON trên ARM — cho phép một lệnh xử lý 4, 8, hoặc 16 phần tử float32 cùng lúc. NumPy được biên dịch để tận dụng các lệnh này khi có thể. List Python không có cách nào dùng được.
Tóm lược 4 lý do trên bằng một bảng:
| Yếu tố | list Python | numpy.ndarray |
|---|---|---|
| Bộ nhớ | Pointer rải rác | Buffer liên tục |
| Kiểu phần tử | Đa kiểu, kèm metadata | Đồng nhất, giá trị thuần |
| Vòng lặp | Python interpreter | C / Fortran đã biên dịch |
| SIMD | Không | Có (AVX2, AVX-512, NEON) |
Attributes của ndarray
Mỗi ndarray có một số attribute đọc-only mô tả cấu trúc của nó:
| Attribute | Ý nghĩa |
|---|---|
shape | Tuple kích thước theo từng chiều, ví dụ (3, 4) |
ndim | Số chiều — bằng len(shape) |
size | Tổng số phần tử — bằng tích các kích thước trong shape |
dtype | Kiểu dữ liệu của các phần tử, ví dụ int64, float32 |
itemsize | Số byte mỗi phần tử chiếm |
nbytes | Tổng số byte của array — bằng size * itemsize |
Ví dụ:
import numpy as np
M = np.array([
[1, 2, 3],
[4, 5, 6],
])
print(M.shape) # (2, 3)
print(M.ndim) # 2
print(M.size) # 6
print(M.dtype) # int64
print(M.itemsize) # 8 — int64 chiếm 8 byte
print(M.nbytes) # 48 — 6 phần tử * 8 byte
Chú ý: shape là một tuple, kể cả khi array có 1 chiều — vector \( n \) phần tử có shape (n,) (có dấu phẩy ở cuối). Đây là điểm dễ nhầm khi mới học, đã nói ở bài 20.
Dtype và ý nghĩa trong DL
NumPy có nhiều dtype, nhưng trong AI / DL bạn sẽ gặp chủ yếu 5 loại sau:
| dtype | Số byte | Phạm vi giá trị | Ghi chú |
|---|---|---|---|
int32 | 4 | -2.1 tỷ đến 2.1 tỷ | Đếm, index nhỏ |
int64 | 8 | ~ ±9.2e18 | Mặc định cho integer trên Linux/macOS 64-bit |
float32 | 4 | ~ ±3.4e38, ~7 chữ số có nghĩa | Single precision — chuẩn DL trên GPU |
float64 | 8 | ~ ±1.8e308, ~15-17 chữ số có nghĩa | Double precision — mặc định của NumPy float |
bool_ | 1 | True / False | Kết quả phép so sánh, mask |
Tạo array với dtype cụ thể:
a = np.array([1, 2, 3], dtype=np.float32)
print(a.dtype) # float32
print(a.itemsize) # 4
b = np.array([1, 2, 3], dtype=np.int32)
print(b.dtype) # int32
print(b.itemsize) # 4
Vì sao float32 quan trọng trong DL
Mặc định khi tạo array số thực, NumPy dùng float64. Nhưng trong deep learning, người ta gần như luôn dùng float32:
- Tiết kiệm RAM / VRAM một nửa. Một model 100 triệu tham số ở
float32chiếm 400 MB; ởfloat64sẽ là 800 MB. - Nhanh hơn trên GPU. Phần lớn GPU consumer (RTX 30, RTX 40, A100, H100) có throughput
float32cao hơnfloat64từ vài lần đến vài chục lần. Tensor Core trên GPU NVIDIA tối ưu chofloat16,bfloat16,float32, không phảifloat64. - Precision đủ dùng. Gradient và weight của neural network không cần độ chính xác 15 chữ số; 7 chữ số của
float32đã đủ.
PyTorch và TensorFlow mặc định dùng float32 — khi convert NumPy array sang tensor để đưa vào model, thường phải cast về float32 nếu array gốc là float64.
Half precision: float16 và bfloat16
Để giảm thêm bộ nhớ và tăng tốc, các framework gần đây dùng float16 (half precision, 2 byte) hoặc bfloat16 (brain float, 2 byte) — gọi chung là mixed-precision training khi kết hợp với float32 cho các phép cần precision cao.
float16: 5 bit exponent, 10 bit mantissa — precision cao, range hẹp (~ ±65504). Có sẵn trong NumPynp.float16.bfloat16: 8 bit exponent, 7 bit mantissa — range bằngfloat32(~ ±3.4e38), precision thấp hơn. NumPy thuần chưa có; PyTorch và TensorFlow cótorch.bfloat16/tf.bfloat16.
Chi tiết về mixed-precision sẽ ở module Deep Learning. Ở đây chỉ cần nhớ: float32 là default của DL, float16 / bfloat16 dùng để tăng tốc khi đã quen.
Convert dtype với astype
Để chuyển kiểu của array, dùng method .astype(). Method này luôn trả về một array mới (copy), không sửa array gốc:
a = np.array([1, 2, 3]) # int64 mặc định
b = a.astype(np.float32)
print(b) # [1. 2. 3.]
print(b.dtype) # float32
print(a.dtype) # int64 — array gốc không đổi
Khi convert từ float xuống int, phần thập phân bị truncate (cắt về 0), không phải làm tròn:
x = np.array([1.7, 2.3, -1.8])
print(x.astype(np.int32)) # [ 1 2 -1] — không phải [2 2 -2]
Khi convert từ kiểu rộng xuống kiểu hẹp, có thể tràn số (overflow) mà NumPy không cảnh báo:
big = np.array([300], dtype=np.int32)
small = big.astype(np.int8) # int8 chỉ chứa -128..127
print(small) # [44] — wrap quanh, không phải 300
Lưu ý này quan trọng khi ép kiểu dữ liệu ảnh: pixel thường nằm trong uint8 [0, 255], nhưng sau các phép tính có thể vượt phạm vi — cần clip hoặc dùng kiểu rộng hơn trước khi astype.
Liên hệ ndarray và tensor
Bài 20 đã giới thiệu tensor — mảng đa chiều — ở mức khái niệm. numpy.ndarray là hiện thực hoá cụ thể nhất, phổ biến nhất của tensor trong Python.
So sánh nhanh với torch.Tensor:
| Đặc điểm | numpy.ndarray | torch.Tensor |
|---|---|---|
| Thiết bị | CPU | CPU + GPU (CUDA, MPS, ROCm) |
| Autograd | Không | Có (requires_grad) |
| Default float | float64 | float32 |
| API | np.zeros((2, 3)) | torch.zeros(2, 3) |
Chuyển qua lại không copy bộ nhớ (chỉ share buffer) khi cùng CPU:
import numpy as np
import torch
a = np.array([1.0, 2.0, 3.0], dtype=np.float32)
t = torch.from_numpy(a) # torch.Tensor, share buffer với a
t[0] = 99
print(a) # [99. 2. 3.] — a cũng đổi vì share bộ nhớ
b = t.numpy() # về lại ndarray, vẫn share buffer
Đặc tính share buffer này tiết kiệm bộ nhớ khi pipeline data NumPy → PyTorch nhưng cũng dễ gây bug nếu vô tình sửa một bên. Khi đưa lên GPU (t.cuda()) thì PyTorch phải copy sang VRAM nên không còn share nữa.
Benchmark list vs ndarray
Đo thử thời gian cộng từng phần tử của 1 triệu số trên hai cấu trúc:
import numpy as np
import time
N = 1_000_000
# 1. List Python
a = list(range(N))
b = list(range(N))
t0 = time.perf_counter()
c = [a[i] + b[i] for i in range(N)]
t1 = time.perf_counter()
print(f"list: {(t1 - t0) * 1000:.2f} ms")
# 2. NumPy ndarray
x = np.arange(N)
y = np.arange(N)
t0 = time.perf_counter()
z = x + y
t1 = time.perf_counter()
print(f"ndarray: {(t1 - t0) * 1000:.2f} ms")
Kết quả tham khảo trên một CPU Apple M2, Python 3.11, NumPy 1.26:
list: 62.31 ms
ndarray: 1.18 ms
Tỷ lệ khoảng 50 lần. Trên CPU x86 với AVX2 hoặc AVX-512 tỷ lệ này có thể lên 80–100 lần với cùng phép cộng int64; với float32 còn cao hơn vì SIMD xử lý được nhiều phần tử hơn mỗi lệnh.
Cần lưu ý: kết quả phụ thuộc vào kích thước \( N \), dtype, và CPU. Với \( N \) nhỏ (vài chục phần tử), overhead khởi tạo của NumPy có thể khiến nó chậm hơn list. NumPy chỉ thắng rõ rệt khi array đủ lớn để chi phí gọi C function được khấu hao.
Hãy chạy thử ở Google Colab và đối chiếu — phản xạ "với mảng số > vài nghìn phần tử, đừng dùng list" sẽ giúp bạn rất nhiều khi viết code AI sau này.
Bài tập
- Tạo một ndarray shape
(3, 4)toàn số 0. In rashape,ndim,size,dtypecủa nó. Gợi ý: dùngnp.zeros. - Cho
a = np.array([1, 2, 3, 4, 5]). Convertasangfloat32và initemsize,nbytescủa array kết quả. - Một array shape
(1000, 1000)dtypefloat32chiếm bao nhiêu byte? Bao nhiêu MB? Kiểm tra bằng code. - Chạy benchmark ở bước 10 trên máy của bạn với
N = 10_000_000. Báo cáo tỷ lệ tốc độ giữa list và ndarray. - Thử
np.array([1, 2, 'x'])— dtype kết quả là gì? Vì sao?
Đáp án
-
z = np.zeros((3, 4)) print(z.shape) # (3, 4) print(z.ndim) # 2 print(z.size) # 12 print(z.dtype) # float64 — np.zeros mặc định float64 -
a = np.array([1, 2, 3, 4, 5]) b = a.astype(np.float32) print(b.itemsize) # 4 print(b.nbytes) # 20 — 5 phần tử * 4 byte - 1000 * 1000 * 4 byte = 4_000_000 byte = ~3.81 MB (chia cho 1024^2). Kiểm tra:
arr = np.zeros((1000, 1000), dtype=np.float32) print(arr.nbytes) # 4000000 print(arr.nbytes / 1024 / 1024) # ~3.81 - Phụ thuộc CPU. Trên CPU desktop hiện đại, tỷ lệ thường 40–100 lần với phép cộng integer; với float32 còn cao hơn.
- Dtype là Unicode string (
'<U21'hoặc tương tự). Vì array phải đồng nhất kiểu, mọi phần tử bị promote lên kiểu rộng nhất chứa được cả'x'— string. Số bị chuyển thành'1','2'.
Tổng kết và bài tiếp theo
- NumPy là thư viện array số học nền tảng của Python; gần như mọi thư viện AI/data science đều xây trên hoặc tương thích với NumPy.
- Kiểu dữ liệu cốt lõi:
numpy.ndarray— mảng đa chiều, mọi phần tử cùng dtype, lưu trong buffer C liên tục. - Attributes hay dùng:
shape,ndim,size,dtype,itemsize,nbytes. - 4 lý do nhanh hơn list: contiguous memory, dtype đồng nhất, loop C / Fortran, SIMD. Benchmark thực tế cho thấy chênh 10–100 lần.
- Trong DL:
float32là default,float16/bfloat16cho mixed-precision. Convert bằng.astype(). ndarrayvàtorch.Tensorshare buffer khi cùng CPU — chuyển qua lại không tốn bộ nhớ.
Bài 29 đi tiếp vào cách tạo array bằng các hàm built-in (np.zeros, np.ones, np.arange, np.linspace, np.random) và cú pháp indexing phần tử của ndarray nhiều chiều.
