Chương 4·Bài 4.1

Sử dụng model pretrained

Muốn dùng lại một checkpoint, ta cần giữ đủ bốn thứ: trọng số, kiến trúc, cách tiền xử lý đầu vào, và thứ tự các nhãn. Thiếu một thứ là kết quả có thể sai mà không báo lỗi. Các ví dụ dưới đây đi từ một model nhỏ được học và lưu ngay tại chỗ, tới hai lỗi mà shape đúng vẫn không bắt được.

Ví dụ 1: Học một bài nguồn, lưu lại, rồi mở ra dùng

Hai cảm biến đo nhiệt độ, theo °C, và độ ẩm, theo %. Sáu hàng dữ liệu nguồn được gán nhãn mat hoặc nong. Một mạng tuyến tính học từ sáu hàng này, rồi được đóng gói và lưu vào bộ nhớ. Đây là model nhỏ ta tự train trong ví dụ, không phải một checkpoint pretrained được công bố, cũng không phải model nhận ảnh. Mục đích là thấy rõ một gói model cần chứa những gì.

Python
import io
import torch
from torch import nn

torch.set_num_threads(1)
torch.manual_seed(41)
raw = torch.tensor([[10., 40.], [12., 60.], [14., 50.],
                    [26., 50.], [28., 40.], [30., 60.]])
mean = torch.tensor([20., 50.])
scale = torch.tensor([10., 10.])
X = (raw - mean) / scale
y = torch.tensor([0, 0, 0, 1, 1, 1])
source = nn.Linear(2, 2)
optimizer = torch.optim.SGD(source.parameters(), lr=0.2)
source.train()
for _ in range(120):
    optimizer.zero_grad(set_to_none=True)
    loss = nn.functional.cross_entropy(source(X), y)
    loss.backward()
    optimizer.step()

package = {
    "architecture": {"input_dim": 2, "classes": 2},
    "state_dict": source.state_dict(),
    "processor": {"mean": mean, "scale": scale},
    "labels": ["mat", "nong"],
    "origin": "local-sensor-source-v1",
}
stream = io.BytesIO()
torch.save(package, stream)
stream.seek(0)
saved = torch.load(stream, map_location="cpu", weights_only=True)
config = saved["architecture"]
restored = nn.Linear(config["input_dim"], config["classes"])
restored.load_state_dict(saved["state_dict"], strict=True)
restored.eval()
processor = saved["processor"]
source.eval()
new_raw = torch.tensor([[13., 50.], [27., 50.]])
new_X = (new_raw - processor["mean"]) / processor["scale"]
with torch.inference_mode():
    prediction = restored(new_X).argmax(dim=1)
    source_prediction = source(X).argmax(dim=1)
    same = torch.equal(restored(new_X), source(new_X))
assert same and prediction.tolist() == [0, 1]
print("nguon:", saved["origin"])
print("dung tren 6 hang nguon:", int((source_prediction == y).sum()))
print("nhan moi:", [saved["labels"][i] for i in prediction.tolist()])
print("nap lai giu output:", same)

Gói lưu có năm phần. Kiến trúc cho biết phải dựng lại lớp có shape nào. state_dict là trọng số. Processor là mean và scale để chuẩn hóa đầu vào. labels cho biết cột 0 là mat, cột 1 là nong. origin ghi lại model này từ đâu ra.

Khi mở ra, ta dựng một lớp mới theo kiến trúc đã lưu, nạp trọng số, và chuẩn hóa hai mẫu mới bằng đúng processor trong gói. Model mở lại cho output y hệt model gốc. Hai mẫu 13°C và 27°C được đọc thành mat và nong. Dòng “đúng 6 trên 6” chỉ xác nhận model đã học được dữ liệu nguồn. Nó không phải điểm validation, và chưa nói gì về cảm biến thật.

Ví dụ 2: Cùng trọng số, khác cách tiền xử lý, ra nhãn khác

Để thấy lỗi bằng tay, ta đặt trọng số sao cho hai logit lần lượt là −z và +z, với z là nhiệt độ đã chuẩn hóa, còn độ ẩm không được dùng. Với 15°C, chuẩn hóa đúng cho z = (15 − 20) / 10 = −0.5, nên lớp mat thắng. Nếu quên trừ mean, z thành 15 / 10 = 1.5, và kết luận bị đảo ngược. Tensor vẫn có đúng hai cột, nên không có gì báo lỗi.

Python
import numpy as np
import torch
from torch import nn

torch.set_num_threads(1)
model = nn.Linear(2, 2, bias=False)
with torch.no_grad():
    model.weight.copy_(torch.tensor([[-1., 0.], [1., 0.]]))
model.eval()
raw = torch.tensor([[15., 50.], [25., 50.]])
mean = torch.tensor([20., 50.])
scale = torch.tensor([10., 10.])
with torch.inference_mode():
    correct = model((raw - mean) / scale)
    wrong = model(raw / scale)  # Shape vẫn đúng, nhưng nghĩa của đầu vào đã khác.
reference = np.array([[0.5, -0.5], [-0.5, 0.5]])
assert np.array_equal(correct.numpy(), reference)
labels = ["mat", "nong"]
print("logits dung:", correct.tolist())
print("processor dung:", [labels[i] for i in correct.argmax(1).tolist()])
print("quen tru mean:", [labels[i] for i in wrong.argmax(1).tolist()])

try:
    nn.Linear(2, 3, bias=False).load_state_dict(model.state_dict(), strict=True)
except RuntimeError:
    print("head 3 lop:", "tu choi checkpoint 2 lop")

Mẫu 15°C có logit [0.5, −0.5], khớp với tính tay. Quên trừ mean, cả hai mẫu đều bị đọc thành nong.

Phần cuối thử nạp trọng số của model hai lớp vào một lớp đầu ra có ba lớp. Với strict=True, PyTorch từ chối ngay vì shape không khớp. Nhưng strict=True chỉ bắt được sai shape. Nó không thể biết ta tiền xử lý sai công thức. Muốn bắt lỗi kiểu đó, cần vài mẫu nhỏ có đáp án biết trước, như hai mẫu 15°C và 25°C ở đây.

Ví dụ 3: Năm ID chạy qua các batch 2, 2 và 1

Mỗi ID phải nhận đúng một dòng kết quả, theo đúng thứ tự đầu vào. Lớp đầu ra vẫn đặt bằng tay và dùng nhiệt độ đã chuẩn hóa. Hàm suy luận chạy theo batch, phải xử lý được cả khi danh sách đầu vào rỗng. Cuối cùng ta đếm số byte của trọng số.

Python
import torch
from torch import nn

torch.set_num_threads(1)
model = nn.Linear(2, 2, bias=False)
with torch.no_grad():
    model.weight.copy_(torch.tensor([[-1., 0.], [1., 0.]]))
model.eval()
ids = ["S9", "S2", "S7", "S1", "S4"]
raw = torch.tensor([[12., 50.], [28., 50.], [14., 60.],
                    [26., 40.], [13., 45.]])

def infer(values, batch_size):
    if batch_size < 1:
        raise ValueError("batch_size phai duong")
    parts = []
    with torch.inference_mode():
        for start in range(0, len(values), batch_size):
            batch = values[start:start + batch_size]
            z = (batch - torch.tensor([20., 50.])) / 10
            parts.append(model(z))
    return torch.cat(parts) if parts else torch.empty((0, 2))

logits = infer(raw, 2)
assert torch.equal(logits, infer(raw, 5))
assert infer(raw[:0], 2).shape == (0, 2)
expected = [int(float(row[0]) > 20) for row in raw]
predicted = logits.argmax(1).tolist()
assert predicted == expected
print("kich thuoc batch:", [len(raw[i:i + 2]) for i in range(0, len(raw), 2)])
print("ID, lop:", list(zip(ids, predicted)))
print("tap rong:", tuple(infer(raw[:0], 2).shape))
print("bytes trong so:", sum(p.numel() * p.element_size() for p in model.parameters()))

Batch cuối chỉ có một hàng, nhưng ID S4 vẫn nhận được kết quả. Chạy với batch 2 hay batch 5 đều cho cùng logit, dòng assert đầu đã kiểm điều đó. Danh sách rỗng trả về một bảng (0, 2) thay vì báo lỗi khi ghép.

Model có bốn trọng số float32, mỗi số 4 byte, tổng 16 byte. Con số này chưa tính đầu vào, activation, và bộ nhớ tạm khi chạy forward. Kiểm batch và kiểm ID là hai việc riêng. Phép tính đúng hết mà gán kết quả nhầm ID thì bài nộp vẫn sai.

Mục lục