Mục lục
- Vì sao cần reshape
- Thuộc tính .shape
- reshape(new_shape) — view hay copy
- -1 magic
- flatten() và ravel()
- transpose() và .T
- swapaxes(axis1, axis2)
- moveaxis(source, destination)
- expand_dims và squeeze
- concatenate, stack, vstack, hstack
- split và array_split
- Use case trong AI
- Code tổng hợp
- Bài tập
- Tổng kết và bài tiếp theo
Vì sao cần reshape
Mỗi loại layer trong model yêu cầu input có shape cố định:
- MLP (Dense / Linear): nhận tensor 2D
(batch, features). - CNN 2D: nhận tensor 4D
(batch, channels, height, width)trong PyTorch, hoặc(batch, height, width, channels)trong TensorFlow / Keras. - RNN / Transformer: nhận 3D
(batch, seq_len, features).
Dữ liệu thô thường có shape khác (ví dụ ảnh load bằng PIL là (H, W, C), batch ảnh là list các ảnh đó). Trước khi đưa vào model phải reshape hoặc hoán vị trục cho khớp.
Điểm quan trọng: các thao tác trong bài này không thay đổi dữ liệu, chỉ thay đổi cách NumPy diễn giải dãy số trong bộ nhớ. Tổng số phần tử trước và sau luôn bằng nhau.
Thuộc tính .shape
arr.shape trả về tuple kích thước từng trục, theo thứ tự outer → inner:
import numpy as np
a = np.arange(12) # 1D, 12 phần tử
print(a.shape) # (12,)
b = a.reshape(3, 4)
print(b.shape) # (3, 4) — 3 hàng, 4 cột
c = np.zeros((2, 3, 4))
print(c.shape) # (2, 3, 4)
print(c.ndim) # 3 — số chiều
print(c.size) # 24 — tổng phần tử (2*3*4)
Có thể gán trực tiếp vào .shape để đổi hình dạng tại chỗ, miễn là tổng phần tử không đổi:
a = np.arange(12)
a.shape = (3, 4) # đổi shape in-place
print(a.shape) # (3, 4)
Cách này nhanh (không tạo object mới) nhưng cứng — nếu memory layout không cho phép sẽ raise AttributeError. Trong code thông thường nên dùng reshape vì linh hoạt hơn và rõ ý đồ.
reshape(new_shape) — view hay copy
arr.reshape(new_shape) trả về array với shape mới. Quy tắc:
- Tích các chiều mới phải bằng tổng số phần tử cũ. Khác đi thì raise
ValueError. - Nếu memory của array là contiguous theo thứ tự cần thiết, NumPy trả về view — không copy dữ liệu, sửa view sẽ sửa array gốc.
- Nếu không (ví dụ array là kết quả của transpose hoặc slicing phức tạp), NumPy trả về copy.
a = np.arange(12) # shape (12,)
a.reshape(2, 6) # shape (2, 6)
a.reshape(3, 4) # shape (3, 4)
a.reshape(2, 2, 3) # shape (2, 2, 3)
a.reshape(12, 1) # shape (12, 1) — column vector
# Sai shape -> ValueError
# a.reshape(5, 3) # 5*3 = 15 != 12
Kiểm tra view hay copy qua thuộc tính .base: nếu .base is a thì là view của a; nếu .base is None thì là copy độc lập.
a = np.arange(12)
b = a.reshape(3, 4)
print(b.base is a) # True — b là view của a
b[0, 0] = 99
print(a[0]) # 99 — sửa b ảnh hưởng a
Nếu cần chắc chắn có dữ liệu độc lập, gọi a.reshape(3, 4).copy().
-1 magic
Khi truyền -1 ở một chiều, NumPy tự suy chiều đó từ tổng số phần tử:
a = np.arange(12)
a.reshape(2, -1) # -> shape (2, 6)
a.reshape(-1, 4) # -> shape (3, 4)
a.reshape(2, 2, -1) # -> shape (2, 2, 3)
a.reshape(-1) # -> shape (12,) — flatten
Tiện khi viết code không biết trước batch size hoặc số features. Ví dụ flatten feature map từ CNN: features.reshape(batch, -1) — không cần tính tay C*H*W.
Lưu ý: chỉ được dùng -1 ở đúng 1 chiều. Hai dấu -1 trở lên sẽ raise ValueError: can only specify one unknown dimension.
flatten() và ravel()
Cả hai cùng làm phẳng array nhiều chiều thành 1D:
m = np.array([[1, 2, 3],
[4, 5, 6]])
m.flatten() # array([1, 2, 3, 4, 5, 6]) — luôn là copy
m.ravel() # array([1, 2, 3, 4, 5, 6]) — view nếu được, copy nếu không
Khác biệt:
flatten()luôn cấp phát bộ nhớ mới và copy dữ liệu. An toàn nhưng tốn RAM.ravel()trả về view nếu memory cho phép; khi đó sửa kết quả sẽ ảnh hưởng array gốc.
Khuyến nghị mặc định dùng ravel() để tiết kiệm RAM, chỉ chuyển sang flatten() khi cần chắc chắn không đụng vào array gốc (hoặc gọi ravel().copy()).
transpose() và .T
Với 2D, .T đổi hàng thành cột:
A = np.array([[1, 2, 3],
[4, 5, 6]]) # shape (2, 3)
A.T # shape (3, 2)
# [[1, 4],
# [2, 5],
# [3, 6]]
Với array n-D, transpose(axes_order) permute (hoán vị) các trục theo thứ tự được chỉ định:
img = np.zeros((224, 224, 3)) # (H, W, C) — kiểu lưu của PIL / OpenCV
img_torch = img.transpose(2, 0, 1) # (C, H, W) — kiểu PyTorch nhận
print(img_torch.shape) # (3, 224, 224)
transpose(2, 0, 1) đọc là "lấy trục 2 cũ làm trục 0 mới, trục 0 cũ làm trục 1 mới, trục 1 cũ làm trục 2 mới". Nếu không truyền tham số, mặc định đảo ngược toàn bộ trục (giống .T nhưng tổng quát).
Kết quả của transpose là view, không copy — strides được thay đổi để duyệt dữ liệu theo thứ tự mới. Nếu sau đó gọi reshape trên view này, NumPy có thể phải copy vì memory layout không còn contiguous.
swapaxes(axis1, axis2)
Khi chỉ muốn đổi vị trí hai trục cụ thể, dùng swapaxes:
x = np.zeros((2, 3, 4))
y = x.swapaxes(0, 2)
print(y.shape) # (4, 3, 2) — trục 0 và 2 đổi chỗ, trục 1 giữ nguyên
Đây là trường hợp riêng của transpose: x.swapaxes(0, 2) tương đương x.transpose(2, 1, 0). Ưu điểm là rõ ý — đọc code biết ngay "swap hai trục", không phải tự nhẩm thứ tự permutation.
moveaxis(source, destination)
np.moveaxis(arr, source, destination) chuyển một trục về vị trí mong muốn, các trục còn lại giữ thứ tự tương đối:
img = np.zeros((224, 224, 3)) # (H, W, C)
# Đưa trục cuối (C) về đầu -> (C, H, W)
img_torch = np.moveaxis(img, -1, 0)
print(img_torch.shape) # (3, 224, 224)
# Có thể truyền tuple để chuyển nhiều trục cùng lúc
batch = np.zeros((8, 224, 224, 3)) # (B, H, W, C)
batch_torch = np.moveaxis(batch, -1, 1)
print(batch_torch.shape) # (8, 3, 224, 224) — (B, C, H, W)
So với transpose, moveaxis rõ ý hơn khi chỉ muốn "đưa trục X về vị trí Y" — không phải viết toàn bộ thứ tự permutation.
expand_dims và squeeze
np.expand_dims(arr, axis) thêm một chiều có size 1 ở vị trí chỉ định:
x = np.array([1, 2, 3]) # shape (3,)
np.expand_dims(x, axis=0).shape # (1, 3) — thêm batch dim ở trước
np.expand_dims(x, axis=1).shape # (3, 1) — column vector
# Cách viết tương đương với indexing
x[None, :].shape # (1, 3)
x[:, np.newaxis].shape # (3, 1)
None và np.newaxis là cùng một thứ (np.newaxis is None trả True). Dùng cái nào tuỳ style, kết quả như nhau.
Ngược lại, squeeze() bỏ tất cả chiều có size 1:
y = np.zeros((1, 3, 1, 4))
y.squeeze().shape # (3, 4) — bỏ cả hai chiều size 1
y.squeeze(axis=0).shape # (3, 1, 4) — chỉ bỏ trục 0
# squeeze trục không có size 1 -> ValueError
# y.squeeze(axis=1) # trục 1 có size 3, không squeeze được
Use case phổ biến: model output shape (1, num_classes) sau khi chạy 1 sample qua model(x[None, ...]) — gọi .squeeze(0) để bỏ batch dim trước khi xử lý tiếp.
concatenate, stack, vstack, hstack
Bốn hàm này hay bị nhầm. Quy tắc:
np.concatenatenối các array dọc theo trục có sẵn — không tạo trục mới.np.stacknối các array theo trục mới — tăngndimlên 1.np.vstack/np.hstacklà shortcut cho concatenate theo trục 0 / 1.
a = np.array([1, 2, 3])
b = np.array([4, 5, 6])
np.concatenate([a, b], axis=0)
# array([1, 2, 3, 4, 5, 6]) shape (6,)
np.stack([a, b], axis=0)
# array([[1, 2, 3],
# [4, 5, 6]]) shape (2, 3) — thêm trục 0 mới
np.stack([a, b], axis=1)
# array([[1, 4],
# [2, 5],
# [3, 6]]) shape (3, 2)
Với array 2D, vstack nối theo trục 0 (xếp chồng theo chiều dọc), hstack nối theo trục 1 (ghép sang chiều ngang):
A = np.array([[1, 2], [3, 4]]) # shape (2, 2)
B = np.array([[5, 6], [7, 8]]) # shape (2, 2)
np.vstack([A, B]).shape # (4, 2) — chồng dọc
np.hstack([A, B]).shape # (2, 4) — ghép ngang
Yêu cầu kích thước các trục không nối phải khớp nhau. Ví dụ concatenate axis=0 thì tất cả trục khác trục 0 phải cùng kích thước.
Khi không nhớ chính xác hành vi: dùng concatenate với axis tường minh — ít gây nhầm nhất.
split và array_split
Ngược lại của concatenate là split:
a = np.arange(12)
np.split(a, 3)
# [array([0, 1, 2, 3]), array([4, 5, 6, 7]), array([8, 9, 10, 11])]
# Split theo các vị trí cụ thể (chỉ số kết thúc của từng đoạn)
np.split(a, [3, 8])
# [array([0, 1, 2]), array([3, 4, 5, 6, 7]), array([8, 9, 10, 11])]
np.split yêu cầu chia đều — nếu không chia đều sẽ raise ValueError. Khi cần linh hoạt hơn, dùng np.array_split:
np.array_split(np.arange(10), 3)
# [array([0, 1, 2, 3]), array([4, 5, 6]), array([7, 8, 9])]
# Các đoạn đầu dài hơn 1 phần tử
Tương tự có np.vsplit (theo trục 0), np.hsplit (theo trục 1) — shortcut cho 2D.
Use case trong AI
- Chuyển layout ảnh giữa frameworks: TensorFlow / Keras dùng
(B, H, W, C)("channels last"), PyTorch dùng(B, C, H, W)("channels first"). Đổi bằngmoveaxis(arr, -1, 1)hoặctranspose(0, 3, 1, 2). - Flatten feature map sau CNN: output của conv là
(B, C, H, W), trước khi vào layer Dense / Linear cần làm phẳng thành(B, C*H*W)— viếtfeatures.reshape(features.shape[0], -1). - Thêm batch dim cho 1 sample: ảnh đơn
(C, H, W)→ batch 1 ảnh(1, C, H, W)bằngimg[None, ...]hoặcnp.expand_dims(img, 0). - Ghép batch: stack N sample shape
(C, H, W)thành batch(N, C, H, W)bằngnp.stack(samples, axis=0). - Split train / val / test: chia dataset 1D index bằng
np.splittheo các vị trí cụ thể. - Bỏ batch dim của output: sau khi inference 1 sample, kết quả
(1, num_classes); dùng.squeeze(0)để có vector(num_classes,).
Code tổng hợp
import numpy as np
# 1. Reshape 12 phần tử thành các shape khác nhau
a = np.arange(12)
print(a.reshape(3, 4).shape) # (3, 4)
print(a.reshape(2, 6).shape) # (2, 6)
print(a.reshape(2, 2, 3).shape) # (2, 2, 3)
print(a.reshape(-1, 4).shape) # (3, 4) — NumPy tự tính
# 2. Transpose ma trận 2D
A = np.array([[1, 2, 3],
[4, 5, 6]]) # (2, 3)
print(A.T.shape) # (3, 2)
# 3. Hoán vị trục cho ảnh (H, W, C) -> (C, H, W)
img = np.zeros((224, 224, 3))
img_t = img.transpose(2, 0, 1)
print(img_t.shape) # (3, 224, 224)
# 4. Concatenate 2 array dọc theo trục 0
u = np.array([[1, 2], [3, 4]])
v = np.array([[5, 6], [7, 8]])
print(np.concatenate([u, v], axis=0).shape) # (4, 2)
print(np.concatenate([u, v], axis=1).shape) # (2, 4)
# 5. Stack tạo trục mới
print(np.stack([u, v], axis=0).shape) # (2, 2, 2)
Bài tập
- Cho array
a = np.arange(24)shape(24,). Reshape thành(2, 3, 4). - Tạo ảnh fake
img = np.zeros((224, 224, 3)). Hoán vị trục để được shape(3, 224, 224)theo chuẩn PyTorch. - Cho ma trận
M = np.arange(16).reshape(4, 4). Flatten thành 1D bằngravel(). - Cho 3 vector
v1, v2, v3mỗi cái shape(10,). Stack thành ma trận shape(3, 10).
Đáp án
-
a = np.arange(24) b = a.reshape(2, 3, 4) print(b.shape) # (2, 3, 4) # Cách dùng -1 magic: # b = a.reshape(2, 3, -1) -
img = np.zeros((224, 224, 3)) img_torch = img.transpose(2, 0, 1) print(img_torch.shape) # (3, 224, 224) # Hoặc dùng moveaxis: # img_torch = np.moveaxis(img, -1, 0) -
M = np.arange(16).reshape(4, 4) flat = M.ravel() print(flat.shape) # (16,) # ravel() trả view nếu memory contiguous; sửa flat[0] sẽ sửa M[0, 0]. # Nếu cần copy độc lập, dùng M.flatten() hoặc M.ravel().copy(). -
v1 = np.arange(10) v2 = np.arange(10, 20) v3 = np.arange(20, 30) mat = np.stack([v1, v2, v3], axis=0) print(mat.shape) # (3, 10) # np.vstack([v1, v2, v3]) cũng cho kết quả tương đương với 1D inputs.
Tổng kết và bài tiếp theo
.shapelà tuple kích thước; có thể gán trực tiếp nhưngreshapelinh hoạt hơn.reshape(new_shape)giữ nguyên tổng phần tử, trả về view khi memory cho phép, copy khi không.-1ở 1 chiều để NumPy tự suy chiều đó (chỉ 1 chiều duy nhất).flatten()luôn copy;ravel()ưu tiên view — tiết kiệm RAM hơn.transpose(axes)/.Thoán vị trục;swapaxesđổi 2 trục;moveaxisđưa 1 trục về vị trí mới.expand_dims/None/np.newaxisthêm chiều size 1;squeezebỏ chiều size 1.concatenatenối theo trục có sẵn;stacktạo trục mới;vstack/hstacklà shortcut.split/array_splitchia array ngược lại của concatenate.
Bài 33 giới thiệu broadcasting — quy tắc giúp NumPy thực hiện phép toán giữa các array có shape khác nhau mà không cần lặp / tile thủ công.
