Logo ROOT  
Reference Guide
 
Loading...
Searching...
No Matches
TMVA_SOFIE_Models.py
Go to the documentation of this file.
1### \file
2### \ingroup tutorial_ml
3### \notebook -nodraw
4### Example of inference with SOFIE using a set of models trained with PyTorch
5### and exported to ONNX.
6### This tutorial shows how to store several models in a single header file and
7### the weights in a ROOT binary file.
8### The models are then evaluated using the RDataFrame
9###
10### The PyTorch export and ROOT's SOFIE parser are both linked against protobuf,
11### but usually against different versions, so loading them in the same process
12### leads to a symbol clash. We therefore run the PyTorch training and ONNX
13### export in a separate Python process and only use ROOT before and afterwards.
14###
15### \macro_code
16### \macro_output
17### \author Lorenzo Moneta
18
19import os
20import subprocess
21import sys
22
23import numpy as np
24import ROOT
25
26## generate and train PyTorch models with different architectures
27
28# The PyTorch training and ONNX export, as a small standalone script run in its
29# own process. It takes as arguments the .npz file with the training data and
30# the names of the models to train, and writes a <modelName>.onnx file for each
31# of them.
32TRAIN_SCRIPT = r"""
33import sys
34import inspect
35import warnings
36import contextlib
37
38import numpy as np
39import torch
40import torch.nn as nn
41
42dataFile = sys.argv[1]
43modelNames = sys.argv[2:]
44
45
46@contextlib.contextmanager
47def expect_warning(category, message):
48 # Silence a known third-party warning and raise if it stops firing.
49
50 # Notifies us to drop the workaround once the upstream library is fixed.
51 with warnings.catch_warnings(record=True) as caught:
52 warnings.simplefilter("always")
53 yield
54 seen = False
55 for w in caught:
56 if issubclass(w.category, category) and message in str(w.message):
57 seen = True
58 else:
59 warnings.warn_explicit(w.message, w.category, w.filename, w.lineno)
60 if not seen:
61 raise RuntimeError(
62 f"Expected {category.__name__} containing {message!r} was not "
63 "emitted. This tutorial's workaround can probably be removed."
64 )
65
66
67def CreateModel(nlayers=4, nunits=64):
68 layers = []
69 ninputs = 7
70 for i in range(nlayers):
71 layers += [nn.Linear(ninputs, nunits), nn.ReLU()]
72 ninputs = nunits
73 layers += [nn.Linear(ninputs, 1), nn.Sigmoid()]
74 model = nn.Sequential(*layers)
75 print(model)
76 return model
77
78
79def TrainModel(model, x, y, epochs=5, batch_size=50):
80 x = torch.from_numpy(x)
81 y = torch.from_numpy(y)
82 criterion = nn.BCELoss()
83 optimizer = torch.optim.Adam(model.parameters())
84 nbatches = x.shape[0] // batch_size
85 for epoch in range(epochs):
86 perm = torch.randperm(x.shape[0])
87 running_loss = 0.0
88 for i in range(nbatches):
89 idx = perm[i * batch_size : (i + 1) * batch_size]
90 optimizer.zero_grad()
91 loss = criterion(model(x[idx]), y[idx])
92 loss.backward()
93 optimizer.step()
94 running_loss += loss.item()
95 print(f"Epoch {epoch + 1}/{epochs} - average loss: {running_loss / nbatches:.4f}")
96
97
98def ExportModel(model, modelName):
99 # need to evaluate the model before exporting to ONNX
100 # and to provide a dummy input tensor to set the input model shape
101 # (the batch size is fixed to 1 for the SOFIE inference)
102 model.eval()
103
104 modelFile = modelName + ".onnx"
105 dummy_x = torch.randn(1, 7)
106 model(dummy_x)
107
108 # check for torch.onnx.export parameters
109 def filtered_kwargs(func, **candidate_kwargs):
110 sig = inspect.signature(func)
111 return {k: v for k, v in candidate_kwargs.items() if k in sig.parameters}
112
113 kwargs = filtered_kwargs(
114 torch.onnx.export,
115 input_names=["input"],
116 output_names=["output"],
117 external_data=False, # may not exist
118 dynamo=True, # may not exist
119 )
120 print("calling torch.onnx.export with parameters", kwargs)
121
122 try:
123 # torch.onnx.export (dynamo path) pickles its export program through
124 # copyreg, which still references the deprecated LeafSpec. The warning
125 # is emitted from inside PyTorch and cannot be avoided from user code.
126 with expect_warning(FutureWarning, "isinstance(treespec, LeafSpec)"):
127 torch.onnx.export(model, dummy_x, modelFile, **kwargs)
128 print("model exported to ONNX as", modelFile)
129 except TypeError:
130 print("Cannot export model from pytorch to ONNX - with version ", torch.__version__)
131 # leave no .onnx behind: which the parent process treats as a RuntimeError
132 sys.exit()
133
134
135data = np.load(dataFile)
136for modelName in modelNames:
137 model = CreateModel(4, 64)
138 TrainModel(model, data["x_train"], data["y_train"])
139 ExportModel(model, modelName)
140"""
141
142
143def PrepareData():
144 # get the input data
145 inputFile = str(ROOT.gROOT.GetTutorialDir()) + "/machine_learning/data/Higgs_data.root"
146
147 df1 = ROOT.RDataFrame("sig_tree", inputFile)
148 sigData = df1.AsNumpy(columns=["m_jj", "m_jjj", "m_lv", "m_jlv", "m_bb", "m_wbb", "m_wwbb"])
149 # print(sigData)
150
151 # stack all the 7 numpy array in a single array (nevents x nvars)
152 xsig = np.column_stack(list(sigData.values()))
153 data_sig_size = xsig.shape[0]
154 print("size of data", data_sig_size)
155
156 # make SOFIE inference on background data
157 df2 = ROOT.RDataFrame("bkg_tree", inputFile)
158 bkgData = df2.AsNumpy(columns=["m_jj", "m_jjj", "m_lv", "m_jlv", "m_bb", "m_wbb", "m_wwbb"])
159 xbkg = np.column_stack(list(bkgData.values()))
160 data_bkg_size = xbkg.shape[0]
161
162 ysig = np.ones(data_sig_size)
163 ybkg = np.zeros(data_bkg_size)
164 inputs_data = np.concatenate((xsig, xbkg), axis=0).astype(np.float32)
165 inputs_targets = np.concatenate((ysig, ybkg), axis=0).astype(np.float32)
166
167 # split data in training and test data
168 rng = np.random.default_rng(1234)
170 ntrain = inputs_data.shape[0] // 2
171
172 x_train = inputs_data[idx[:ntrain]]
173 y_train = inputs_targets[idx[:ntrain]].reshape(-1, 1)
174 x_test = inputs_data[idx[ntrain:]]
175 y_test = inputs_targets[idx[ntrain:]].reshape(-1, 1)
176
177 return x_train, y_train, x_test, y_test
178
179
180def TrainModels(x_train, y_train, modelNames):
181 # train the models with PyTorch and export them to ONNX
182 # (done in a separate process to avoid the protobuf clash, see above)
183 dataFile = "Higgs_Model_train_data.npz"
184 np.savez(dataFile, x_train=x_train, y_train=y_train)
185
186 subprocess.run([sys.executable, "-c", TRAIN_SCRIPT, dataFile] + modelNames, check=True)
187 os.remove(dataFile)
188
189 modelFiles = [name + ".onnx" for name in modelNames]
190 for modelFile in modelFiles:
191 if not os.path.exists(modelFile):
192 raise RuntimeError("ONNX model " + modelFile + " could not be exported")
193 return modelFiles
194
195
196### run the models
197
198x_train, y_train, x_test, y_test = PrepareData()
199
200## create models and train them
201
202# All three models use the same small architecture (4 layers of 64 units) to
203# keep the tutorial runtime under control, whatever their names suggest.
204modelNames = ["Higgs_Model_4L_50", "Higgs_Model_4L_200", "Higgs_Model_2L_500"]
205model1, model2, model3 = TrainModels(x_train, y_train, modelNames)
206
207# evaluate with SOFIE the 3 trained models
208
209
210def GenerateModelCode(modelFile, generatedHeaderFile):
212 model = parser.Parse(modelFile)
213
214 print("Generating inference code for the ONNX model from ", modelFile, "in the header ", generatedHeaderFile)
215 # Generating inference code using a ROOT binary file
217 # add option to append to the same file the generated headers (pass True for append flag)
218 model.OutputGenerated(generatedHeaderFile, True)
219 # model.PrintGenerated()
220 return generatedHeaderFile
221
222
223generatedHeaderFile = "Higgs_Model.hxx"
224# need to remove existing header file since we are appending on same one
225if os.path.exists(generatedHeaderFile):
226 print("removing existing file", generatedHeaderFile)
227 os.remove(generatedHeaderFile)
228
229weightFile = "Higgs_Model.root"
230if os.path.exists(weightFile):
231 print("removing existing file", weightFile)
232 os.remove(weightFile)
233
234GenerateModelCode(model1, generatedHeaderFile)
235GenerateModelCode(model2, generatedHeaderFile)
236GenerateModelCode(model3, generatedHeaderFile)
237
238# compile the generated code
239
240ROOT.gInterpreter.Declare('#include "' + generatedHeaderFile + '"')
241
242
243# run the inference on the test data
244session1 = ROOT.TMVA_SOFIE_Higgs_Model_4L_50.Session("Higgs_Model.root")
245session2 = ROOT.TMVA_SOFIE_Higgs_Model_4L_200.Session("Higgs_Model.root")
246session3 = ROOT.TMVA_SOFIE_Higgs_Model_2L_500.Session("Higgs_Model.root")
247
248hs1 = ROOT.TH1D("hs1", "Signal result 4L 50", 100, 0, 1)
249hs2 = ROOT.TH1D("hs2", "Signal result 4L 200", 100, 0, 1)
250hs3 = ROOT.TH1D("hs3", "Signal result 2L 500", 100, 0, 1)
251
252hb1 = ROOT.TH1D("hb1", "Background result 4L 50", 100, 0, 1)
253hb2 = ROOT.TH1D("hb2", "Background result 4L 200", 100, 0, 1)
254hb3 = ROOT.TH1D("hb3", "Background result 2L 500", 100, 0, 1)
255
256
257def EvalModel(session, x):
258 result = session.infer(x)
259 return result[0]
260
261
262for i in range(0, x_test.shape[0]):
263 result1 = EvalModel(session1, x_test[i, :])
264 result2 = EvalModel(session2, x_test[i, :])
265 result3 = EvalModel(session3, x_test[i, :])
266 if y_test[i] == 1:
267 hs1.Fill(result1)
268 hs2.Fill(result2)
269 hs3.Fill(result3)
270 else:
271 hb1.Fill(result1)
272 hb2.Fill(result2)
273 hb3.Fill(result3)
274
275
276def PlotHistos(hs, hb):
277 hs.SetLineColor("kRed")
278 hb.SetLineColor("kBlue")
279 hs.Draw()
280 hb.Draw("same")
281
282
283c1 = ROOT.TCanvas()
284c1.Divide(1, 3)
285c1.cd(1)
286PlotHistos(hs1, hb1)
287c1.cd(2)
288PlotHistos(hs2, hb2)
289c1.cd(3)
290PlotHistos(hs3, hb3)
291c1.Draw()
292
293## draw also ROC curves
294
295
296def GetContent(h):
297 n = h.GetNbinsX()
298 x = ROOT.std.vector["float"](n)
299 w = ROOT.std.vector["float"](n)
300 for i in range(0, n):
301 x[i] = h.GetBinCenter(i + 1)
302 w[i] = h.GetBinContent(i + 1)
303 return x, w
304
305
306def MakeROCCurve(hs, hb):
307 xs, ws = GetContent(hs)
308 xb, wb = GetContent(hb)
309 roc = ROOT.TMVA.ROCCurve(xs, xb, ws, wb)
310 print("ROC integral for ", hs.GetName(), roc.GetROCIntegral())
311 curve = roc.GetROCCurve()
313 return roc, curve
314
315
316c2 = ROOT.TCanvas()
317
318r1, curve1 = MakeROCCurve(hs1, hb1)
320curve1.Draw("AC")
321
322r2, curve2 = MakeROCCurve(hs2, hb2)
323curve2.SetLineColor("kBlue")
324curve2.Draw("C")
325
326r3, curve3 = MakeROCCurve(hs3, hb3)
327curve3.SetLineColor("kGreen")
328curve3.Draw("C")
329
330c2.Draw()
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 ,...