Danh sách bài viết

Bài 32: Shape và reshape array — biến đổi hình dạng dữ liệu

Bài học trình bày cách đọc và sửa hình dạng (shape) của NumPy array: thuộc tính .shape, hàm reshape kèm -1 magic, phân biệt flatten và ravel (copy vs view), transpose / .T để hoán vị trục, swapaxes và moveaxis để di chuyển trục cụ thể, expand_dims và squeeze để thêm / bỏ chiều size 1, cùng nhóm hàm concatenate, stack, vstack, hstack, split để ghép và tách array. Mọi thao tác chỉ thay đổi cách nhìn dữ liệu chứ không tính toán lại giá trị.

24/05/2026
13 phút đọc
0 lượt xem
1

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.

2

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õ ý đồ.

3

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().

4

-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.

5

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()).

6

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 transposeview, 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.

7

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.

8

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.

9

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)

Nonenp.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.

10

concatenate, stack, vstack, hstack

Bốn hàm này hay bị nhầm. Quy tắc:

  • np.concatenate nối các array dọc theo trục có sẵn — không tạo trục mới.
  • np.stack nối các array theo trục mới — tăng ndim lên 1.
  • np.vstack / np.hstack là 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.

11

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.

12

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ằng moveaxis(arr, -1, 1) hoặc transpose(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ết features.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ằng img[None, ...] hoặc np.expand_dims(img, 0).
  • Ghép batch: stack N sample shape (C, H, W) thành batch (N, C, H, W) bằng np.stack(samples, axis=0).
  • Split train / val / test: chia dataset 1D index bằng np.split theo 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,).
13

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)
14

Bài tập

  1. Cho array a = np.arange(24) shape (24,). Reshape thành (2, 3, 4).
  2. Tạo ảnh fake img = np.zeros((224, 224, 3)). Hoán vị trục để được shape (3, 224, 224) theo chuẩn PyTorch.
  3. Cho ma trận M = np.arange(16).reshape(4, 4). Flatten thành 1D bằng ravel().
  4. Cho 3 vector v1, v2, v3 mỗi cái shape (10,). Stack thành ma trận shape (3, 10).
Đáp án
  1. 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)
  2. 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)
  3. 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().
  4. 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.
15

Tổng kết và bài tiếp theo

  • .shape là tuple kích thước; có thể gán trực tiếp nhưng reshape linh 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) / .T hoán vị trục; swapaxes đổi 2 trục; moveaxis đưa 1 trục về vị trí mới.
  • expand_dims / None / np.newaxis thêm chiều size 1; squeeze bỏ chiều size 1.
  • concatenate nối theo trục có sẵn; stack tạo trục mới; vstack / hstack là shortcut.
  • split / array_split chia 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.