Logo ROOT  
Reference Guide
 
Loading...
Searching...
No Matches
TMVA_SOFIE_PyTorch_HiggsModel.py
Go to the documentation of this file.
1### \file
2### \ingroup tutorial_ml
3### \notebook -nodraw
4### This macro trains a simple deep neural network on the Higgs dataset with
5### PyTorch, exports the model to ONNX and runs the SOFIE parser on it to
6### generate and compile C++ inference code.
7###
8### The trained model is saved as HiggsModel.onnx and is used as input by
9### other SOFIE tutorials (e.g. TMVA_SOFIE_RDataFrame.C), so this macro needs
10### to be run before them.
11###
12### The PyTorch export and ROOT's SOFIE parser are both linked against protobuf,
13### but usually against different versions, so loading them in the same process
14### leads to a symbol clash. We therefore run the PyTorch training and ONNX
15### export in a separate Python process and only use ROOT before and afterwards.
16###
17### \macro_code
18### \macro_output
19
20import os
21import subprocess
22import sys
23
24import numpy as np
25import ROOT
26
27# The PyTorch training and ONNX export, as a small standalone script run in its
28# own process. It takes as arguments the .npz file with the training data and
29# the model name, and writes <modelName>.onnx together with the PyTorch
30# predictions for the validation inputs in <modelName>_torch_output.npy.
31TRAIN_SCRIPT = r"""
32import sys
33import inspect
34import warnings
35import contextlib
36
37import numpy as np
38import torch
39import torch.nn as nn
40
41dataFile = sys.argv[1]
42modelName = sys.argv[2]
43
44
45@contextlib.contextmanager
46def expect_warning(category, message):
47 # Silence a known third-party warning and raise if it stops firing.
48
49 # Notifies us to drop the workaround once the upstream library is fixed.
50 with warnings.catch_warnings(record=True) as caught:
51 warnings.simplefilter("always")
52 yield
53 seen = False
54 for w in caught:
55 if issubclass(w.category, category) and message in str(w.message):
56 seen = True
57 else:
58 warnings.warn_explicit(w.message, w.category, w.filename, w.lineno)
59 if not seen:
60 raise RuntimeError(
61 f"Expected {category.__name__} containing {message!r} was not "
62 "emitted. This tutorial's workaround can probably be removed."
63 )
64
65
66def CreateModel(nlayers=4, nunits=64):
67 layers = []
68 ninputs = 7
69 for i in range(1, nlayers):
70 layers += [nn.Linear(ninputs, nunits), nn.ReLU()]
71 ninputs = nunits
72 layers += [nn.Linear(ninputs, 1), nn.Sigmoid()]
73 model = nn.Sequential(*layers)
74 print(model)
75 return model
76
77
78def TrainModel(model, x, y, epochs=5, batch_size=50):
79 x = torch.from_numpy(x)
80 y = torch.from_numpy(y)
81 criterion = nn.BCELoss()
82 optimizer = torch.optim.Adam(model.parameters())
83 nbatches = x.shape[0] // batch_size
84 for epoch in range(epochs):
85 perm = torch.randperm(x.shape[0])
86 running_loss = 0.0
87 for i in range(nbatches):
88 idx = perm[i * batch_size : (i + 1) * batch_size]
89 optimizer.zero_grad()
90 loss = criterion(model(x[idx]), y[idx])
91 loss.backward()
92 optimizer.step()
93 running_loss += loss.item()
94 print(f"Epoch {epoch + 1}/{epochs} - average loss: {running_loss / nbatches:.4f}")
95
96
97def ExportModel(model, modelName):
98 # need to evaluate the model before exporting to ONNX
99 # and to provide a dummy input tensor to set the input model shape
100 # (the batch size is fixed to 1 for the SOFIE inference)
101 model.eval()
102
103 modelFile = modelName + ".onnx"
104 dummy_x = torch.randn(1, 7)
105 model(dummy_x)
106
107 # check for torch.onnx.export parameters
108 def filtered_kwargs(func, **candidate_kwargs):
109 sig = inspect.signature(func)
110 return {k: v for k, v in candidate_kwargs.items() if k in sig.parameters}
111
112 kwargs = filtered_kwargs(
113 torch.onnx.export,
114 input_names=["input"],
115 output_names=["output"],
116 external_data=False, # may not exist
117 dynamo=True, # may not exist
118 )
119 print("calling torch.onnx.export with parameters", kwargs)
120
121 try:
122 # torch.onnx.export (dynamo path) pickles its export program through
123 # copyreg, which still references the deprecated LeafSpec. The warning
124 # is emitted from inside PyTorch and cannot be avoided from user code.
125 with expect_warning(FutureWarning, "isinstance(treespec, LeafSpec)"):
126 torch.onnx.export(model, dummy_x, modelFile, **kwargs)
127 print("model exported to ONNX as", modelFile)
128 except TypeError:
129 print("Cannot export model from pytorch to ONNX - with version ", torch.__version__)
130 # leave no .onnx behind: which the parent process treats as a RuntimeError
131 sys.exit()
132
133
134data = np.load(dataFile)
135
136# create dense model with 3 layers of 64 units and train it
137model = CreateModel(3, 64)
138TrainModel(model, data["x_train"], data["y_train"])
139ExportModel(model, modelName)
140
141# evaluate the trained model on the validation inputs, for comparison with SOFIE
142with torch.no_grad():
143 y = model(torch.from_numpy(data["x_check"])).numpy()
144np.save(modelName + "_torch_output.npy", y)
145"""
146
147
149 # get the input data
150 inputFile = str(ROOT.gROOT.GetTutorialDir()) + "/machine_learning/data/Higgs_data.root"
151
152 df1 = ROOT.RDataFrame("sig_tree", inputFile)
153 sigData = df1.AsNumpy(columns=["m_jj", "m_jjj", "m_lv", "m_jlv", "m_bb", "m_wbb", "m_wwbb"])
154 # print(sigData)
155
156 # stack all the 7 numpy array in a single array (nevents x nvars)
157 xsig = np.column_stack(list(sigData.values()))
158 data_sig_size = xsig.shape[0]
159 print("size of data", data_sig_size)
160
161 # make SOFIE inference on background data
162 df2 = ROOT.RDataFrame("bkg_tree", inputFile)
163 bkgData = df2.AsNumpy(columns=["m_jj", "m_jjj", "m_lv", "m_jlv", "m_bb", "m_wbb", "m_wwbb"])
164 xbkg = np.column_stack(list(bkgData.values()))
165 data_bkg_size = xbkg.shape[0]
166
167 ysig = np.ones(data_sig_size)
168 ybkg = np.zeros(data_bkg_size)
169 inputs_data = np.concatenate((xsig, xbkg), axis=0).astype(np.float32)
170 inputs_targets = np.concatenate((ysig, ybkg), axis=0).astype(np.float32)
171
172 # split data in training and test data
173 rng = np.random.default_rng(1234)
175 ntrain = inputs_data.shape[0] // 2
176
177 x_train = inputs_data[idx[:ntrain]]
178 y_train = inputs_targets[idx[:ntrain]].reshape(-1, 1)
179 x_test = inputs_data[idx[ntrain:]]
180 y_test = inputs_targets[idx[ntrain:]].reshape(-1, 1)
181
182 return x_train, y_train, x_test, y_test
183
184
186 # train the model with PyTorch and export it to ONNX
187 # (done in a separate process to avoid the protobuf clash, see above)
188 dataFile = name + "_train_data.npz"
189 np.savez(dataFile, x_train=x_train, y_train=y_train, x_check=x_check)
190
191 modelFile = name + ".onnx"
192 torchOutputFile = name + "_torch_output.npy"
193 subprocess.run([sys.executable, "-c", TRAIN_SCRIPT, dataFile, name], check=True)
194 os.remove(dataFile)
195 if not os.path.exists(modelFile) or not os.path.exists(torchOutputFile):
196 raise RuntimeError("ONNX model could not be exported")
197
198 ytorch = np.load(torchOutputFile)
199 os.remove(torchOutputFile)
200 return modelFile, ytorch
201
202
204
205 # check if the input file exists
206 if not os.path.exists(modelFile):
207 raise FileNotFoundError("Input model file is missing. The PyTorch training did not produce " + modelFile)
208
209 # parse the input ONNX model into an RModel object
211 model = parser.Parse(modelFile)
212
213 # Generating inference code
216
217 modelName = modelFile.replace(".onnx", "")
218 return modelName
219
220
221###################################################################
222## Step 1 : Create and train the model, export it to ONNX
223###################################################################
224
225x_train, y_train, x_test, y_test = PrepareData()
226# validate the exported model on the first test events
227x_check = x_test[:10]
229
230###################################################################
231## Step 2 : Parse model and generate inference code with SOFIE
232###################################################################
233
234modelName = GenerateCode(modelFile)
235modelHeaderFile = modelName + ".hxx"
236
237###################################################################
238## Step 3 : Compile the generated C++ model code
239###################################################################
240
241ROOT.gInterpreter.Declare('#include "' + modelHeaderFile + '"')
242
243###################################################################
244## Step 4: Evaluate the model
245###################################################################
246
247# get first the SOFIE session namespace
249session = sofie.Session()
250
251for i in range(x_check.shape[0]):
253 print("input to model is ", x_check[i], "\n\t -> output using SOFIE = ", y[0], " using PyTorch = ", ytorch[i, 0])
254 if abs(y[0] - ytorch[i, 0]) > 0.01:
255 raise RuntimeError("ERROR: Result is different between SOFIE and PyTorch")
256
257print("OK")
ROOT::Detail::TRangeCast< T, true > TRangeDynCast
TRangeDynCast is an adapter class that allows the typed iteration through a TCollection.
ROOT's RDataFrame offers a modern, high-level interface for analysis of data stored in TTree ,...
TrainModel(x_train, y_train, x_check, name)