Monte Carlo Dropout: đo độ bất định của dự đoán bằng PyTorch

Monte Carlo Dropout: đo độ bất định của dự đoán bằng PyTorch

Monte Carlo Dropout (MC Dropout) giữ Dropout bật khi suy luận và chạy cùng một đầu vào qua mạng nhiều lần. Mỗi lượt lấy một mặt nạ ngẫu nhiên. Trung bình các đầu ra là dự đoán gộp, còn độ phân tán cho biết mô hình nhạy đến đâu với những mặt nạ đã lấy mẫu.

Trong thí nghiệm hồi quy dưới đây, sai số trung bình ở vùng ngoại suy cao gấp 19 lần vùng có dữ liệu huấn luyện, trong khi độ lệch chuẩn của các dự đoán chỉ tăng khoảng 12%. Nếu dùng mức dao động này để quyết định khi nào nên tin kết quả, ta cần đo riêng độ phân tán, sai số và độ bao phủ.

Một mô hình, nhiều lượt dự đoán

Khi huấn luyện với Dropout để giảm overfitting, mỗi lượt chạy ngẫu nhiên đặt một phần giá trị ở tầng ẩn về 0. Suy luận thông thường dùng model.eval(), lúc đó nn.Dropout truyền nguyên đầu vào. Với MC Dropout, ta giữ nguyên trọng số đã học và chủ động bật lại các tầng Dropout để lấy mẫu. Tài liệu PyTorch về Dropout

Với một đầu vào $x$, ký hiệu $f_t(x)$ là dự đoán của lượt thứ $t$. Từ $T$ lượt, ta tính trung bình và độ lệch chuẩn thực nghiệm của các đầu ra:

$$ \hat{\mu}(x) = \frac{1}{T}\sum_{t=1}^{T} f_t(x), \qquad \hat{\sigma}_{\mathrm{MC}}(x) = \sqrt{\frac{1}{T}\sum_{t=1}^{T}\bigl(f_t(x)-\hat{\mu}(x)\bigr)^2}. $$

Gal và Ghahramani (2016) đưa ra cách diễn giải huấn luyện với Dropout như một phép xấp xỉ suy luận Bayes. Trong bài này, $\hat{\sigma}_{\mathrm{MC}}$ đo mức các dự đoán dao động theo mặt nạ. Với mô hình hồi quy dùng MSE và một đầu ra duy nhất như ví dụ dưới đây, phép tính chưa cộng thành phần nhiễu quan sát. Các phân vị của đầu ra lấy mẫu vì thế cần được kiểm chứng trước khi dùng để lập khoảng dự đoán cho một giá trị quan sát mới. Gal và Ghahramani, ICML 2016

Bật Dropout và giữ BatchNorm ở chế độ eval

Ba thao tác trong PyTorch giải quyết ba việc khác nhau.

Thao tác Tác dụng trong ví dụ
model.eval() Đưa tất cả tầng về chế độ suy luận. Dropout tắt, BatchNorm dùng thống kê tích lũy trong lúc train.
layer.train() trên từng nn.Dropout Bật lại việc lấy mặt nạ ở đúng các tầng Dropout.
torch.inference_mode() Tắt cơ chế theo dõi gradient phục vụ huấn luyện. Chế độ này độc lập với train() và eval().

Nếu gọi model.train() trên toàn mạng, BatchNorm1d với mặc định track_running_stats=True sẽ dùng thống kê của batch hiện tại và cập nhật các bộ đệm ngay cả trong inference_mode(). Với track_running_stats=False, BatchNorm vẫn dùng thống kê của batch ở chế độ eval. Hãy kiểm tra cấu hình tầng nếu cần dự đoán cho một mẫu mà kết quả không phụ thuộc vào những mẫu khác trong cùng batch. Tài liệu BatchNorm1d, tài liệu inference_mode

Hàm mc_predict trong mã cuối bài dành cho mô hình suy luận có các tầng nn.Dropout được khai báo thành module. Nó đặt mô hình về eval, bật từng tầng cần lấy mẫu rồi trả chúng về eval khi xong, kể cả khi forward gặp lỗi. Nếu mô hình gọi torch.nn.functional.dropout trực tiếp trong forward, hãy kiểm tra đối số training của lời gọi đó. Duyệt model.modules() chỉ tìm được các module đã đăng ký. Vì cờ training là trạng thái dùng chung, hãy dành riêng instance mô hình cho lượt gọi này hoặc khóa truy cập trong lúc lấy mẫu.

Thí nghiệm hồi quy với một vùng bị bỏ trống

Dữ liệu huấn luyện gồm 256 điểm chia đều trên hai đoạn $[-2, -0.75]$ và $[0.75, 2]$. Hàm sinh dữ liệu là $g(x)=\sin(1.5x)+0.2x$, cộng nhiễu Gauss có độ lệch chuẩn 0,08. Mạng có hai tầng ẩn rộng 64, BatchNorm sau tầng tuyến tính đầu tiên và Dropout với $p=0.10$ sau mỗi Tanh. Mỗi seed huấn luyện 800 bước Adam trên toàn bộ 256 mẫu. Ba seed dùng cùng một bộ dữ liệu, với khởi tạo và chuỗi mặt nạ ngẫu nhiên riêng.

Lưới đánh giá có 281 điểm từ −3,5 đến 3,5. Tại mỗi điểm, mã so sánh một lượt eval thông thường với trung bình của 100 lượt MC. Cả hai cùng được chấm theo đường $g(x)$ đã biết, trước khi cộng nhiễu. Bảng là trung bình qua ba seed 11, 22 và 33. Cột cuối đếm tỉ lệ điểm mà đường $g(x)$ nằm trong khoảng phân vị 5%–95% của 100 đầu ra lấy mẫu.

Vùng đánh giá Số điểm MAE eval MAE trung bình MC Độ lệch chuẩn MC trung bình Độ bao phủ đường $g(x)$
Có dữ liệu huấn luyện, $0.75 \le \lvert x\rvert \le 2$ 102 0,0305 0,0328 0,0866 99,3%
Khoảng trống, $\lvert x\rvert < 0.75$ 59 0,1498 0,1425 0,0830 53,7%
Ngoại suy, $\lvert x\rvert > 2$ 120 0,6246 0,6248 0,0968 1,7%

Ở khoảng trống giữa hai cụm huấn luyện, sai số tuyệt đối trung bình (MAE) tăng hơn bốn lần, còn độ lệch chuẩn MC giảm nhẹ. Ra ngoài khoảng huấn luyện, các lượt lấy mẫu vẫn cho kết quả tương đối gần nhau dù trung bình lệch xa đường thật. Các mặt nạ có thể tạo ra những dự đoán đồng thuận mà cùng sai. Trên bài toán này, việc lấy trung bình có giảm nhẹ MAE ở khoảng trống, nhưng sai số ngoại suy hầu như giữ nguyên.

Khoảng phân vị 5%–95% chứa phần giữa của các đầu ra MC. Độ bao phủ của nó với giá trị thật là câu hỏi riêng, và ở đây chênh mạnh giữa các vùng. Những tỉ lệ trong bảng đo độ bao phủ của đường sinh dữ liệu trên lưới cố định. Muốn đánh giá độ bao phủ của một khoảng dự đoán cho quan sát mới có nhiễu, hãy dùng tập giữ lại có nhãn quan sát và kiểm tra riêng theo vùng hoặc nhóm dữ liệu mà hệ thống sẽ gặp.

Kiểm tra BatchNorm bằng một đối chứng

Với cách bật từng Dropout, mã kiểm tra running_mean, running_var và num_batches_tracked trước và sau 100 lượt. Cả ba giữ nguyên trên mọi seed. Mã còn so lại dự đoán eval sau khi lấy mẫu với dự đoán eval ban đầu.

Phép đối chứng dùng bản sao của mô hình seed 11, gọi train() cho cả mạng và đưa cùng một batch ngoại suy vào 100 lần. Dù chạy trong inference_mode(), bộ đếm BatchNorm tăng thêm 100 batch và phần tử thay đổi nhiều nhất trong running_mean dịch 3,3614. Nếu dùng cách đó trên mô hình đang phục vụ, dữ liệu truy vấn sẽ cập nhật các bộ đệm dùng cho những lần suy luận eval sau.

Khi nào tín hiệu này có ích?

MC Dropout cho phép lấy thêm tín hiệu từ một mô hình đã được huấn luyện với Dropout. Với $T$ lượt forward tuần tự, số lượt tính toán tăng theo $T$. Hãy thử vài giá trị $T$ và đo xem trung bình hoặc độ phân tán đã đủ ổn định cho tác vụ chưa, rồi cân đối với ngân sách độ trễ.

Nếu dùng độ phân tán để chuyển một số dự đoán cho người kiểm tra, hãy chọn ngưỡng trên tập validation phù hợp. Có thể đo sai số của các dự đoán được giữ lại theo tỉ lệ dự đoán chuyển đi. Kiểm tra riêng những vùng thiếu dữ liệu hoặc có phân bố thay đổi. Trong ví dụ này, xếp hạng theo độ lệch chuẩn trung bình của vùng sẽ đặt khoảng trống dưới cả vùng huấn luyện, dù dự đoán ở khoảng trống kém hơn rõ rệt.

Mã tái lập thí nghiệm

Mã chỉ cần PyTorch. Bảng được tạo trên Python 3.10.11, PyTorch 2.8.0, CPU arm64, với một luồng tính toán. Lưu khối dưới đây thành mc_dropout.py, rồi chạy python3 mc_dropout.py. Chương trình in kết quả từng seed, đối chứng BatchNorm và trung bình ba seed.

  1import copy
  2import platform
  3
  4import torch
  5from torch import nn
  6
  7
  8def signal(x):
  9    return torch.sin(1.5 * x) + 0.2 * x
 10
 11
 12def fit_model(x, y, seed):
 13    torch.manual_seed(seed)
 14    model = nn.Sequential(
 15        nn.Linear(1, 64),
 16        nn.BatchNorm1d(64),
 17        nn.Tanh(),
 18        nn.Dropout(0.10),
 19        nn.Linear(64, 64),
 20        nn.Tanh(),
 21        nn.Dropout(0.10),
 22        nn.Linear(64, 1),
 23    )
 24    optimizer = torch.optim.Adam(model.parameters(), lr=0.005)
 25    model.train()
 26    for _ in range(800):
 27        optimizer.zero_grad(set_to_none=True)
 28        loss = nn.functional.mse_loss(model(x), y)
 29        loss.backward()
 30        optimizer.step()
 31    return model.eval()
 32
 33
 34@torch.inference_mode()
 35def mc_predict(model, x, draws=100):
 36    model.eval()
 37    dropout_layers = [layer for layer in model.modules() if isinstance(layer, nn.Dropout)]
 38    for layer in dropout_layers:
 39        layer.train()
 40    try:
 41        return torch.stack([model(x) for _ in range(draws)])
 42    finally:
 43        for layer in dropout_layers:
 44            layer.eval()
 45
 46
 47@torch.inference_mode()
 48def measure(model, x, y, seed, groups):
 49    baseline = model(x)
 50    batch_norm = model[1]
 51    mean_before = batch_norm.running_mean.clone()
 52    variance_before = batch_norm.running_var.clone()
 53    batches_before = batch_norm.num_batches_tracked.item()
 54    torch.manual_seed(1000 + seed)
 55    samples = mc_predict(model, x)
 56    assert torch.equal(batch_norm.running_mean, mean_before)
 57    assert torch.equal(batch_norm.running_var, variance_before)
 58    assert batch_norm.num_batches_tracked.item() == batches_before
 59    assert not any(layer.training for layer in model.modules())
 60    assert torch.equal(baseline, model(x))
 61
 62    prediction = samples.mean(dim=0)
 63    spread = samples.std(dim=0, unbiased=False)
 64    low, high = torch.quantile(samples, torch.tensor([0.05, 0.95]), dim=0)
 65    in_band = (y >= low) & (y <= high)
 66    metrics = torch.stack(
 67        [(baseline - y).abs(), (prediction - y).abs(), spread, in_band.float()],
 68        dim=0,
 69    )
 70    return torch.stack([metrics[:, mask].mean(dim=1) for mask in groups])
 71
 72
 73@torch.inference_mode()
 74def batch_norm_control(model):
 75    test_x = torch.linspace(2.5, 3.5, 64).unsqueeze(1)
 76    control = copy.deepcopy(model).train()
 77    batch_norm = control[1]
 78    batches_before = batch_norm.num_batches_tracked.item()
 79    mean_before = batch_norm.running_mean.clone()
 80    for _ in range(100):
 81        control(test_x)
 82    batches_added = batch_norm.num_batches_tracked.item() - batches_before
 83    shift = (batch_norm.running_mean - mean_before).abs().max().item()
 84    return batches_added, shift
 85
 86
 87def main():
 88    torch.set_num_threads(1)
 89    generator = torch.Generator().manual_seed(2026)
 90    x_train = torch.cat([torch.linspace(-2, -0.75, 128), torch.linspace(0.75, 2, 128)])
 91    x_train = x_train.unsqueeze(1)
 92    y_train = signal(x_train) + 0.08 * torch.randn(x_train.shape, generator=generator)
 93    x_test = torch.arange(-140, 141).float().unsqueeze(1) / 40
 94    y_test = signal(x_test)
 95    abs_x = x_test.abs()
 96    groups = ((abs_x >= 0.75) & (abs_x <= 2), abs_x < 0.75, abs_x > 2)
 97
 98    print(f"Python {platform.python_version()}, PyTorch {torch.__version__}, CPU {platform.machine()}")
 99    print("seed   region       n    eval_MAE    MC_MAE     mean_MC_std   curve_in_band")
100    all_results = []
101    for seed in (11, 22, 33):
102        model = fit_model(x_train, y_train, seed)
103        results = measure(model, x_test, y_test, seed, groups)
104        all_results.append(results)
105        for name, mask, row in zip(("support", "gap", "exterior"), groups, results):
106            print(f"{seed:>4}   {name:<8}   {mask.sum():>3}    {row[0]:.4f}      "
107                  f"{row[1]:.4f}     {row[2]:.4f}        {row[3]:.1%}")
108        if seed == 11:
109            batches, shift = batch_norm_control(model)
110            print(f"BatchNorm control: +{batches} batches, max running_mean shift {shift:.4f}")
111
112    for name, mask, row in zip(("support", "gap", "exterior"), groups, torch.stack(all_results).mean(0)):
113        print(f"mean   {name:<8}   {mask.sum():>3}    {row[0]:.4f}      "
114              f"{row[1]:.4f}     {row[2]:.4f}        {row[3]:.1%}")
115
116
117if __name__ == "__main__":
118    main()

Đọc thêm trên blog

Bình luận