BƯỚC 01
AI nền tảng / Cơ bản / 12 PHÚT ĐỌC
Khi thu nhỏ ảnh đặc trưng, ta giữ lại điều gì và bỏ mất điều gì? Bạn sẽ so sánh cách lấy giá trị lớn nhất với cách lấy trung bình.
Biên soạn: LearnDo · Ngày hiển thị theo lộ trình
BƯỚC 01
Một bộ lọc (filter) tạo ra bản đồ đáp ứng (response map): mỗi vị trí cho biết đặc trưng xuất hiện mạnh hay yếu ở vùng đó. Giữ mọi vị trí qua nhiều tầng khiến phép tính phía sau tốn bộ nhớ. Pooling gom một vùng nhỏ thành một giá trị đại diện để giảm số vị trí.
Max pooling chọn giá trị lớn nhất, average pooling lấy trung bình. Max chú ý đáp ứng mạnh nhất, còn average giữ mức đáp ứng chung. Cả hai đều làm mất một phần thông tin vị trí bên trong vùng, vì nhiều số đã được thay bằng một số.
Gộp đặc trưng (pooling) thay một vùng bằng giá trị đại diện. Gộp cực đại (max pooling) lấy giá trị lớn nhất. Gộp trung bình (average pooling) lấy trung bình của vùng. Bước trượt (stride) quy định khoảng dịch cửa sổ.
BƯỚC 02
Dùng NumPy để tự tính pooling và Matplotlib để vẽ các ô số. Ví dụ dùng bản đồ 4 × 4 đã có sẵn, không cần mô hình hoặc GPU.
py -m venv .venv
.venv\Scripts\python -m pip install numpy matplotlibNumPy thực hiện phép gom vùng. Matplotlib vẽ giá trị từng ô để có thể kiểm tra đầu ra bằng tay.
BƯỚC 03
Chuẩn bị đường dẫn ket_qua và đặt font hỗ trợ tiếng Việt. Đây chỉ là phần thiết lập cho hình kết quả, phép pooling nằm trong hàm ở các bước sau.
from pathlib import Path
import numpy as np
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
plt.rcParams.update({"font.family": "DejaVu Sans", "font.size": 12})
output_dir = Path(__file__).resolve().parent / "ket_qua"
output_dir.mkdir(exist_ok=True)Đoạn đầu chọn cách vẽ không cần màn hình và tạo thư mục lưu kết quả cạnh tệp mã. Chưa có phép pooling nào được thực hiện ở bước này.
BƯỚC 04
Bản đồ dưới gồm các đáp ứng không âm, có thể hình dung là đầu ra sau ReLU. Vùng trên trái là [[1, 7], [3, 5]]. Giá trị lớn nhất bằng 7, còn trung bình là (1+7+3+5)/4 = 4.
feature_map = np.array([
[1, 7, 2, 4],
[3, 5, 0, 6],
[8, 2, 1, 3],
[4, 9, 5, 2],
], dtype=float)
patch = feature_map[:2, :2]
print("Vùng đầu tiên — max:", patch.max(), "| average:", patch.mean())feature_map là bản đồ 4 × 4 biết trước giá trị. Lát cắt [:2, :2] lấy vùng đầu tiên gồm 1, 7, 3, 5. Giá trị lớn nhất là 7 và trung bình là 4.
Một giá trị max bằng 7 không cho biết số 7 nằm ở ô nào trong vùng 2 × 2.
BƯỚC 05
Hàm pool2d nhận data, kích thước vùng size, bước trượt (stride) stride và phép gom reduce. Khi không thêm viền (padding), số vị trí trên mỗi chiều là (kích thước đầu vào − size) // stride + 1. Với chiều dài 4, size=2 và stride=2, kết quả có 2 vị trí.
def pool2d(data, size=2, stride=2, reduce=np.max):
height, width = data.shape
output_height = (height - size) // stride + 1
output_width = (width - size) // stride + 1
output = np.zeros((output_height, output_width), dtype=float)Công thức kích thước đếm số vị trí đặt vừa cửa sổ, không thêm viền. size là cạnh cửa sổ, stride là số ô di chuyển giữa hai vị trí liên tiếp.
BƯỚC 06
Phần này tiếp tục trong pool2d, giữ nguyên bốn dấu cách đầu dòng. row, col là tọa độ đầu ra, top, left là góc bắt đầu của cửa sổ trên đầu vào. reduce có thể là np.max hoặc np.mean, nên cùng một cách trượt dùng được cho hai loại pooling.
for row in range(output_height):
for col in range(output_width):
top, left = row * stride, col * stride
patch = data[top:top + size, left:left + size]
output[row, col] = reduce(patch)
return outputrow và col là vị trí ở đầu ra. Nhân với stride cho góc trên trái của vùng nguồn. reduce gom các phần tử vùng thành một số, mặc định dùng np.max.
BƯỚC 07
Gọi hàm ba lần với cùng feature_map. Hai kết quả đầu dùng bước trượt 2 và có kích thước 2 × 2. Lần thứ ba dùng bước trượt 1: các cửa sổ chồng nhau và cho đầu ra 3 × 3.
max_output = pool2d(feature_map)
average_output = pool2d(feature_map, reduce=np.mean)
dense_output = pool2d(feature_map, stride=1)
print("Max pooling:\n", max_output)
print("Average pooling:\n", average_output)
print("Stride 1:\n", dense_output)Ba lời gọi giữ nguyên đầu vào nhưng thay cách gom hoặc bước trượt. Stride 2 tạo đầu ra 2 × 2, stride 1 tạo đầu ra 3 × 3 vì các cửa sổ chồng nhau.
BƯỚC 08
Mỗi bản đồ được vẽ ở một ô hình. vmin và vmax cố định miền màu từ 0 đến 9, nhờ đó cùng màu có cùng ý nghĩa ở mọi ô. np.ndenumerate duyệt từng vị trí để ghi chính giá trị đã tính.
panels = [("Đầu vào · 4 × 4", feature_map),
("Max · bước 2", max_output),
("Average · bước 2", average_output),
("Max · bước 1", dense_output)]
fig, axes = plt.subplots(2, 2, figsize=(10, 9), layout="constrained")
for ax, (title, data) in zip(axes.flat, panels):
ax.imshow(data, cmap="YlGnBu", vmin=0, vmax=9, interpolation="nearest")
for (row, col), value in np.ndenumerate(data):
color = "white" if value >= 5 else "#16302c"
ax.text(col, row, f"{value:g}", ha="center", va="center", color=color, fontsize=20)
ax.set_title(title, pad=12)
ax.set_xticks([])
ax.set_yticks([])Cả bốn bản đồ dùng thang từ 0 đến 9. Con số được vẽ ngay trong ô để màu không phải nguồn thông tin duy nhất. Màu chữ đổi theo độ tối để giữ tương phản.
BƯỚC 09
Thêm tiêu đề và ghi pooling.png vào ket_qua. Phần hiển thị không tham gia phép pooling, thay bảng màu sẽ không đổi kết quả số.
fig.suptitle("Cùng một bản đồ · ba cách gom vùng", fontsize=19)
fig.savefig(output_dir / "pooling.png", dpi=160)
plt.close(fig)
print("Ảnh kết quả:", output_dir / "pooling.png")savefig lưu hình bốn bản đồ. close kết thúc đối tượng đồ thị, rồi dòng in cho biết đường dẫn để mở ảnh kiểm tra.
BƯỚC 10
Chạy tệp hoàn chỉnh và so hình dưới với các mảng được in. Với bước 2, 16 giá trị đầu vào còn 4 giá trị: số vị trí giảm bốn lần, mỗi chiều giảm một nửa. Với bước 1, đầu ra có 9 giá trị vì cửa sổ dịch qua nhiều vị trí hơn.
Giá trị 9 ở vùng dưới trái được max giữ nguyên nhưng bị average hòa với 8, 2 và 4 thành 5.75. Đây là khác biệt về phép gom, không phải một cách luôn tốt hơn cách kia.
.venv\Scripts\python pooling_numpy.pyLệnh chạy pooling_numpy.py. Đối chiếu ô đầu ra đầu tiên với phép tính tay trước khi tin toàn bộ ma trận kết quả.
BƯỚC 11
Trong mạng CNN, pooling thường được áp dụng riêng trên từng kênh của bản đồ đặc trưng (feature map). Nếu có 16 kênh và dùng pooling 2 × 2, bước 2 trên bản đồ 32 × 32, đầu ra là 16 × 16 trên mỗi kênh, số kênh vẫn bằng 16.
Dịch một đáp ứng mạnh trong cùng cửa sổ có thể không đổi max, nhưng dịch qua ranh giới cửa sổ có thể đổi đầu ra. Vì vậy không nên hiểu pooling là bảo đảm bất biến với mọi dịch chuyển. Khi cần định vị chính xác từng pixel, việc thu nhỏ phải được cân nhắc cùng phần khôi phục độ phân giải (resolution).
Ở ví dụ không padding này, nếu kích thước không chia vừa, những hàng hoặc cột cuối không thuộc cửa sổ trọn vẹn sẽ không được dùng. Đó là lựa chọn tính toán đã nêu của hàm, không phải lỗi dữ liệu.
Pooling giảm chi phí và giữ một kiểu thông tin đại diện, đổi lại, vị trí chi tiết có thể bị mất.
ĐẾN LƯỢT BẠN