Chương 8·Bài 8.1

Chiến lược thi

Một bản dự đoán chỉ có giá trị khi nó đi trọn đường tới file nộp đúng yêu cầu của đề. Các ví dụ dưới đây kiểm lần lượt ba chuyện: file nộp, phần việc thật sự chạy trong giới hạn thời gian, và cách giữ thời gian cho lần chạy cuối.

Ví dụ 1: Ba xác suất đều đúng mà file vẫn có thể sai

Đề luyện giả định yêu cầu file CSV có hai cột id,probability, theo thứ tự ID là 001, 007, 010. Bảng dự đoán đang có đủ ba ID, nhưng sai thứ tự. Ta phải ghép theo ID trước khi ghi file. Sau đó thử xem bộ kiểm có bắt được một xác suất nằm ngoài khoảng 0 tới 1 không.

Python
from io import StringIO
import numpy as np
import pandas as pd

expected = ["001", "007", "010"]
predictions = pd.DataFrame({
    "id": ["010", "001", "007"], "probability": [0.55, 0.15, 0.80]
})
assert predictions["id"].is_unique
assert set(predictions["id"]) == set(expected)
ordered = predictions.set_index("id").loc[expected].reset_index()

def check_csv(text):
    frame = pd.read_csv(StringIO(text), dtype={"id": str})
    if frame.columns.tolist() != ["id", "probability"]:
        raise ValueError("sai cột")
    if frame["id"].tolist() != expected or not frame["id"].is_unique:
        raise ValueError("sai ID hoặc thứ tự")
    p = frame["probability"].to_numpy(dtype=float)
    if not np.isfinite(p).all() or not ((0 <= p) & (p <= 1)).all():
        raise ValueError("xác suất ngoài miền")
    return frame

csv_text = ordered.to_csv(index=False, lineterminator="\n")
checked = check_csv(csv_text)
print(csv_text.rstrip())
print("đọc lại:", checked.shape)
bad = ordered.copy()
bad.loc[1, "probability"] = 1.2
try:
    check_csv(bad.to_csv(index=False))
except ValueError as error:
    print("bắt lỗi:", error)
else:
    raise AssertionError("Bộ kiểm bỏ sót xác suất 1.2")

File ghi ra theo đúng thứ tự đề yêu cầu, và mỗi dòng mang đúng xác suất của ID ấy. ID 001 vẫn giữ số 0 ở đầu, nhờ đọc cột id dưới dạng chuỗi. Nếu để pandas tự đoán kiểu, 001 sẽ thành số 1.

Bộ kiểm đọc lại chính chuỗi CSV sẽ nộp, chứ không kiểm DataFrame trong bộ nhớ. Trường hợp xác suất 1.2 cho thấy: đủ số dòng, đúng tên cột, vẫn chưa đủ để file hợp lệ. Bộ kiểm phải xét cả miền giá trị.

Ví dụ 2: Đo đúng phần việc mà máy chấm sẽ chạy

Giả sử máy chấm chạy trên CPU, và chỉ làm hai việc: nạp model rồi suy luận. Việc train đã làm xong từ trước. Model ở đây chỉ là hai hệ số cho sẵn, không có bước học. Đồng hồ phải bấm từ lúc đọc file model, qua đọc dữ liệu, chia batch, cho tới khi ghi xong file CSV, chứ không chỉ bấm quanh một phép nhân ma trận.

Python
import json
from pathlib import Path
from tempfile import TemporaryDirectory
from time import perf_counter
import numpy as np
import pandas as pd

with TemporaryDirectory(prefix="ai81-") as directory:
    folder = Path(directory)
    (folder / "model.json").write_text(
        json.dumps({"weights": [0.5, -0.25], "bias": 0.0}), encoding="utf-8")
    pd.DataFrame({"id": ["001", "002", "003"],
                  "x0": [0.0, 2.0, -2.0], "x1": [0.0, 0.0, 0.0]}).to_csv(
        folder / "input.csv", index=False)

    start = perf_counter()
    config = json.loads((folder / "model.json").read_text(encoding="utf-8"))
    data = pd.read_csv(folder / "input.csv", dtype={"id": str})
    X = data[["x0", "x1"]].to_numpy(dtype=np.float64)
    w = np.array(config["weights"], dtype=np.float64)
    blocks = []
    for first in range(0, len(X), 2):
        logits = X[first:first + 2] @ w + config["bias"]
        blocks.append(1 / (1 + np.exp(-logits)))
    probabilities = np.concatenate(blocks)
    destination = folder / "submission.csv"
    pd.DataFrame({"id": data["id"], "probability": probabilities}).to_csv(
        destination, index=False, float_format="%.6f")
    elapsed_seconds = perf_counter() - start
    readback = pd.read_csv(destination, dtype={"id": str})
    assert elapsed_seconds >= 0
    assert readback["id"].tolist() == data["id"].tolist()
    assert np.allclose(probabilities, [0.5, 1 / (1 + np.exp(-1)),
                                      1 / (1 + np.exp(1))])
    print("batch:", len(blocks), "số mẫu:", len(probabilities))
    print("bytes X và w:", X.nbytes + w.nbytes)
    print("xác suất:", readback["probability"].tolist())

Ba mẫu chạy qua hai batch, batch sau chỉ có một mẫu. Con số 64 byte chỉ là bộ nhớ của hai mảng X và w: sáu số cộng hai số, mỗi số 8 byte. Nó chưa tính bộ nhớ của pandas, của Python, hay của các mảng tạm.

Biến elapsed_seconds chứa thời gian đo thật của lần chạy này. Ta không in nó ra, vì con số thay đổi theo máy và theo những gì đang chạy nền, nên không thể cố định làm kết quả mẫu. Nếu đề chấm cả một notebook có bước train, thì mốc bấm giờ phải đặt trước cả bước train.

Ví dụ 3: Một cải tiến cần một bước chuẩn bị trước

Theo lịch luyện giả định, còn 30 phút. Có ba việc có thể làm. Việc A mất 10 phút, dự kiến tăng 1 điểm. Việc B mất 20 phút, dự kiến tăng thêm 8 điểm, nhưng chỉ làm được sau khi xong A. Việc C độc lập, mất 20 phút, dự kiến tăng 5 điểm. Các con số này được đặt ra cho bài toán lập kế hoạch, không phải đo từ máy hay từ model nào. Ta duyệt mọi tổ hợp để tìm kế hoạch tốt nhất.

Python
from itertools import product

cost = {"A": 10, "B": 20, "C": 20}
gain = {"A": 1, "B": 8, "C": 5}
valid = []
for flags in product([False, True], repeat=3):
    chosen = {name for name, flag in zip(cost, flags) if flag}
    if "B" in chosen and "A" not in chosen:
        continue
    minutes = sum(cost[name] for name in chosen)
    if minutes <= 30:
        valid.append((sum(gain[name] for name in chosen), -minutes,
                      tuple(sorted(chosen))))
best_gain, negative_cost, plan = max(valid)
assert plan == ("A", "B") and best_gain == 9
print("kế hoạch:", plan)
print("phút giả định:", -negative_cost, "điểm tăng dự kiến:", best_gain)
print("nếu giữ 10 phút chạy cuối, phương án:",
      max(item for item in valid if -item[1] <= 20)[2])

Làm A rồi B cho 9 điểm, hơn hẳn C. Riêng A gần như vô ích, chỉ 1 điểm, nhưng nó mở đường cho B. Nhìn từng việc riêng lẻ, ta dễ bỏ qua A và vì thế bỏ lỡ B.

Nếu giữ lại 10 phút cho lần chạy cuối cùng, chỉ còn 20 phút để cải tiến. Khi đó A rồi B không còn vừa, và lựa chọn tốt nhất đổi thành C. Mười phút cho lần chạy cuối là một ràng buộc phải đặt ngay từ đầu kế hoạch, không phải thời gian dư có thể lấn vào khi đang hăng.