|
| 1 | +### \file |
| 2 | +### \ingroup tutorial_ml |
| 3 | +### \notebook -nodraw |
| 4 | +### Example of resampling when one class is underrepresented in the dataset. |
| 5 | +### |
| 6 | +### \macro_code |
| 7 | +### \macro_output |
| 8 | +### \author Jonah Ascoli |
| 9 | + |
| 10 | +import ROOT |
| 11 | +import torch |
| 12 | +from tqdm import tqdm |
| 13 | + |
| 14 | +seed = 42 |
| 15 | +torch.manual_seed(seed) |
| 16 | + |
| 17 | + |
| 18 | +# Create an imbalanced dataset with two classes, one of which is underrepresented. |
| 19 | +# Here, we'll create two files, one with even numbers and one with odd numbers, |
| 20 | +# and then merge them to form a dataset with underrepresented odd numbers. |
| 21 | +def make_df(b1_expr, num_events): |
| 22 | + return ROOT.RDataFrame(num_events).Define("b1", b1_expr).Define("b2", "(int) b1%2") |
| 23 | + |
| 24 | + |
| 25 | +df_major = make_df("(int) 2 * rdfentry_", 100000) |
| 26 | +df_minor = make_df("(int) 2 * rdfentry_ + 1", 1000) |
| 27 | + |
| 28 | +batch_size = 256 |
| 29 | +num_epochs = 10 |
| 30 | + |
| 31 | +loss_fn = torch.nn.BCEWithLogitsLoss() |
| 32 | + |
| 33 | + |
| 34 | +def train_model(model, optimizer, dataloader): |
| 35 | + train, val = dataloader.train_test_split(test_size=0.2) |
| 36 | + for _ in tqdm(range(num_epochs), desc="Training"): |
| 37 | + model.train() |
| 38 | + for X, y in train.as_torch(): |
| 39 | + optimizer.zero_grad() |
| 40 | + loss = loss_fn(model(X), y) |
| 41 | + loss.backward() |
| 42 | + optimizer.step() |
| 43 | + losses = [] |
| 44 | + for X, y in val.as_torch(): |
| 45 | + with torch.no_grad(): |
| 46 | + loss = loss_fn(model(X), y) |
| 47 | + losses.append(loss.item()) |
| 48 | + print(f"Validation Loss: {sum(losses) / len(losses)}") |
| 49 | + |
| 50 | + |
| 51 | +# First, let's try to create a dataloader without resampling and see how it handles the underrepresented class. |
| 52 | +dl = ROOT.Experimental.ML.RDataLoader( |
| 53 | + [df_major, df_minor], |
| 54 | + batch_size=batch_size, |
| 55 | + target="b2", |
| 56 | + set_seed=seed, |
| 57 | + load_eager=True, |
| 58 | +) |
| 59 | + |
| 60 | +basic_model = torch.nn.Linear(1, 1) # Simple linear model for binary classification |
| 61 | +basic_optimizer = torch.optim.Adam(basic_model.parameters()) |
| 62 | + |
| 63 | +print("Training without resampling:") |
| 64 | +train_model(basic_model, basic_optimizer, dl) |
| 65 | + |
| 66 | +# Now, let's try the same thing with oversampling |
| 67 | +# Strategy: more batches of the underrepresented class |
| 68 | +# Takes more time per epoch, but each epoch is more effective |
| 69 | +dl_oversampled = ROOT.Experimental.ML.RDataLoader( |
| 70 | + [df_major, df_minor], |
| 71 | + batch_size=batch_size, |
| 72 | + target="b2", |
| 73 | + set_seed=seed, |
| 74 | + load_eager=True, # Must be enabled for resampling |
| 75 | + sampling_type="oversampling", # Can also be "undersampling" |
| 76 | + sampling_ratio=0.1, # ~10% of the data will be from the underrepresented class |
| 77 | +) |
| 78 | + |
| 79 | +oversampling_model = torch.nn.Linear(1, 1) |
| 80 | +oversampling_optimizer = torch.optim.Adam(oversampling_model.parameters()) |
| 81 | + |
| 82 | +print("Training with oversampling:") |
| 83 | +train_model(oversampling_model, oversampling_optimizer, dl_oversampled) |
0 commit comments