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()
Bình luận