Logo ROOT   6.10/00
Reference Guide
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Macros Groups Pages
MethodPyKeras.cxx
Go to the documentation of this file.
1 // @(#)root/tmva/pymva $Id$
2 // Author: Stefan Wunsch, 2016
3 
4 #include <Python.h>
5 #include "TMVA/MethodPyKeras.h"
6 
7 #define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION
8 #include <numpy/arrayobject.h>
9 
10 #include "TMVA/Types.h"
11 #include "TMVA/Config.h"
12 #include "TMVA/ClassifierFactory.h"
13 #include "TMVA/Results.h"
16 
17 using namespace TMVA;
18 
19 REGISTER_METHOD(PyKeras)
20 
22 
23 MethodPyKeras::MethodPyKeras(const TString &jobName, const TString &methodTitle, DataSetInfo &dsi, const TString &theOption)
24  : PyMethodBase(jobName, Types::kPyKeras, methodTitle, dsi, theOption) {
25  fNumEpochs = 10;
26  fBatchSize = 100;
27  fVerbose = 1;
28  fContinueTraining = false;
29  fSaveBestOnly = true;
30  fTriesEarlyStopping = -1;
31  fLearningRateSchedule = ""; // empty string deactivates learning rate scheduler
32  fFilenameTrainedModel = ""; // empty string sets output model filename to default (in weights/)
33 }
34 
35 MethodPyKeras::MethodPyKeras(DataSetInfo &theData, const TString &theWeightFile)
36  : PyMethodBase(Types::kPyKeras, theData, theWeightFile) {
37  fNumEpochs = 10;
38  fBatchSize = 100;
39  fVerbose = 1;
40  fContinueTraining = false;
41  fSaveBestOnly = true;
43  fLearningRateSchedule = ""; // empty string deactivates learning rate scheduler
44  fFilenameTrainedModel = ""; // empty string sets output model filename to default (in weights/)
45 }
46 
48 }
49 
51  if (type == Types::kRegression) return kTRUE;
52  if (type == Types::kClassification && numberClasses == 2) return kTRUE;
53  if (type == Types::kMulticlass && numberClasses >= 2) return kTRUE;
54  return kFALSE;
55 }
56 
57 ///////////////////////////////////////////////////////////////////////////////
58 
60  DeclareOptionRef(fFilenameModel, "FilenameModel", "Filename of the initial Keras model");
61  DeclareOptionRef(fFilenameTrainedModel, "FilenameTrainedModel", "Filename of the trained output Keras model");
62  DeclareOptionRef(fBatchSize, "BatchSize", "Training batch size");
63  DeclareOptionRef(fNumEpochs, "NumEpochs", "Number of training epochs");
64  DeclareOptionRef(fVerbose, "Verbose", "Keras verbosity during training");
65  DeclareOptionRef(fContinueTraining, "ContinueTraining", "Load weights from previous training");
66  DeclareOptionRef(fSaveBestOnly, "SaveBestOnly", "Store only weights with smallest validation loss");
67  DeclareOptionRef(fTriesEarlyStopping, "TriesEarlyStopping", "Number of epochs with no improvement in validation loss after which training will be stopped. The default or a negative number deactivates this option.");
68  DeclareOptionRef(fLearningRateSchedule, "LearningRateSchedule", "Set new learning rate during training at specific epochs, e.g., \"50,0.01;70,0.005\"");
69 }
70 
72  // Set default filename for trained model if option is not used
73  if (fFilenameTrainedModel.IsNull()) {
74  fFilenameTrainedModel = GetWeightFileDir() + "/TrainedModel_" + GetName() + ".h5";
75  }
76  // Setup model, either the initial model from `fFilenameModel` or
77  // the trained model from `fFilenameTrainedModel`
78  if (fContinueTraining) Log() << kINFO << "Continue training with trained model" << Endl;
80 }
81 
82 void MethodPyKeras::SetupKerasModel(bool loadTrainedModel) {
83  /*
84  * Load Keras model from file
85  */
86 
87  // Load initial model or already trained model
88  TString filenameLoadModel;
89  if (loadTrainedModel) {
90  filenameLoadModel = fFilenameTrainedModel;
91  }
92  else {
93  filenameLoadModel = fFilenameModel;
94  }
95  PyRunString("model = keras.models.load_model('"+filenameLoadModel+"')",
96  "Failed to load Keras model from file: "+filenameLoadModel);
97  Log() << kINFO << "Load model from file: " << filenameLoadModel << Endl;
98 
99  /*
100  * Init variables and weights
101  */
102 
103  // Get variables, classes and target numbers
104  fNVars = GetNVariables();
107  else Log() << kFATAL << "Selected analysis type is not implemented" << Endl;
108 
109  // Init evaluation (needed for getMvaValue)
110  fVals = new float[fNVars]; // holds values used for classification and regression
111  npy_intp dimsVals[2] = {(npy_intp)1, (npy_intp)fNVars};
112  PyArrayObject* pVals = (PyArrayObject*)PyArray_SimpleNewFromData(2, dimsVals, NPY_FLOAT, (void*)fVals);
113  PyDict_SetItemString(fLocalNS, "vals", (PyObject*)pVals);
114 
115  fOutput.resize(fNOutputs); // holds classification probabilities or regression output
116  npy_intp dimsOutput[2] = {(npy_intp)1, (npy_intp)fNOutputs};
117  PyArrayObject* pOutput = (PyArrayObject*)PyArray_SimpleNewFromData(2, dimsOutput, NPY_FLOAT, (void*)&fOutput[0]);
118  PyDict_SetItemString(fLocalNS, "output", (PyObject*)pOutput);
119 
120  // Mark the model as setup
121  fModelIsSetup = true;
122 }
123 
125  if (!PyIsInitialized()) {
126  Log() << kFATAL << "Python is not initialized" << Endl;
127  }
128  _import_array(); // required to use numpy arrays
129 
130  // Import Keras
131  PyRunString("import keras", "Import Keras failed");
132 
133  // Set flag that model is not setup
134  fModelIsSetup = false;
135 }
136 
138  if(!fModelIsSetup) Log() << kFATAL << "Model is not setup for training" << Endl;
139 
140  /*
141  * Load training data to numpy array
142  */
143 
144  UInt_t nTrainingEvents = Data()->GetNTrainingEvents();
145 
146  float* trainDataX = new float[nTrainingEvents*fNVars];
147  float* trainDataY = new float[nTrainingEvents*fNOutputs];
148  float* trainDataWeights = new float[nTrainingEvents];
149  for (UInt_t i=0; i<nTrainingEvents; i++) {
150  const TMVA::Event* e = GetTrainingEvent(i);
151  // Fill variables
152  for (UInt_t j=0; j<fNVars; j++) {
153  trainDataX[j + i*fNVars] = e->GetValue(j);
154  }
155  // Fill targets
156  // NOTE: For classification, convert class number in one-hot vector,
157  // e.g., 1 -> [0, 1] or 0 -> [1, 0] for binary classification
159  for (UInt_t j=0; j<fNOutputs; j++) {
160  trainDataY[j + i*fNOutputs] = 0;
161  }
162  trainDataY[e->GetClass() + i*fNOutputs] = 1;
163  }
164  else if (GetAnalysisType() == Types::kRegression) {
165  for (UInt_t j=0; j<fNOutputs; j++) {
166  trainDataY[j + i*fNOutputs] = e->GetTarget(j);
167  }
168  }
169  else Log() << kFATAL << "Can not fill target vector because analysis type is not known" << Endl;
170  // Fill weights
171  // NOTE: If no weight branch is given, this defaults to ones for all events
172  trainDataWeights[i] = e->GetWeight();
173  }
174 
175  npy_intp dimsTrainX[2] = {(npy_intp)nTrainingEvents, (npy_intp)fNVars};
176  npy_intp dimsTrainY[2] = {(npy_intp)nTrainingEvents, (npy_intp)fNOutputs};
177  npy_intp dimsTrainWeights[1] = {(npy_intp)nTrainingEvents};
178  PyArrayObject* pTrainDataX = (PyArrayObject*)PyArray_SimpleNewFromData(2, dimsTrainX, NPY_FLOAT, (void*)trainDataX);
179  PyArrayObject* pTrainDataY = (PyArrayObject*)PyArray_SimpleNewFromData(2, dimsTrainY, NPY_FLOAT, (void*)trainDataY);
180  PyArrayObject* pTrainDataWeights = (PyArrayObject*)PyArray_SimpleNewFromData(1, dimsTrainWeights, NPY_FLOAT, (void*)trainDataWeights);
181  PyDict_SetItemString(fLocalNS, "trainX", (PyObject*)pTrainDataX);
182  PyDict_SetItemString(fLocalNS, "trainY", (PyObject*)pTrainDataY);
183  PyDict_SetItemString(fLocalNS, "trainWeights", (PyObject*)pTrainDataWeights);
184 
185  /*
186  * Load validation data to numpy array
187  */
188 
189  // NOTE: In TMVA, test data is actually validation data
190 
191  UInt_t nValEvents = Data()->GetNTestEvents();
192 
193  float* valDataX = new float[nValEvents*fNVars];
194  float* valDataY = new float[nValEvents*fNOutputs];
195  float* valDataWeights = new float[nValEvents];
196  for (UInt_t i=0; i<nValEvents; i++) {
197  const TMVA::Event* e = GetTestingEvent(i);
198  // Fill variables
199  for (UInt_t j=0; j<fNVars; j++) {
200  valDataX[j + i*fNVars] = e->GetValue(j);
201  }
202  // Fill targets
204  for (UInt_t j=0; j<fNOutputs; j++) {
205  valDataY[j + i*fNOutputs] = 0;
206  }
207  valDataY[e->GetClass() + i*fNOutputs] = 1;
208  }
209  else if (GetAnalysisType() == Types::kRegression) {
210  for (UInt_t j=0; j<fNOutputs; j++) {
211  valDataY[j + i*fNOutputs] = e->GetTarget(j);
212  }
213  }
214  else Log() << kFATAL << "Can not fill target vector because analysis type is not known" << Endl;
215  // Fill weights
216  valDataWeights[i] = e->GetWeight();
217  }
218 
219  npy_intp dimsValX[2] = {(npy_intp)nValEvents, (npy_intp)fNVars};
220  npy_intp dimsValY[2] = {(npy_intp)nValEvents, (npy_intp)fNOutputs};
221  npy_intp dimsValWeights[1] = {(npy_intp)nValEvents};
222  PyArrayObject* pValDataX = (PyArrayObject*)PyArray_SimpleNewFromData(2, dimsValX, NPY_FLOAT, (void*)valDataX);
223  PyArrayObject* pValDataY = (PyArrayObject*)PyArray_SimpleNewFromData(2, dimsValY, NPY_FLOAT, (void*)valDataY);
224  PyArrayObject* pValDataWeights = (PyArrayObject*)PyArray_SimpleNewFromData(1, dimsValWeights, NPY_FLOAT, (void*)valDataWeights);
225  PyDict_SetItemString(fLocalNS, "valX", (PyObject*)pValDataX);
226  PyDict_SetItemString(fLocalNS, "valY", (PyObject*)pValDataY);
227  PyDict_SetItemString(fLocalNS, "valWeights", (PyObject*)pValDataWeights);
228 
229  /*
230  * Train Keras model
231  */
232 
233  // Setup parameters
234 
235  PyObject* pBatchSize = PyLong_FromLong(fBatchSize);
236  PyObject* pNumEpochs = PyLong_FromLong(fNumEpochs);
237  PyObject* pVerbose = PyLong_FromLong(fVerbose);
238  PyDict_SetItemString(fLocalNS, "batchSize", pBatchSize);
239  PyDict_SetItemString(fLocalNS, "numEpochs", pNumEpochs);
240  PyDict_SetItemString(fLocalNS, "verbose", pVerbose);
241 
242  // Setup training callbacks
243  PyRunString("callbacks = []");
244 
245  // Callback: Save only weights with smallest validation loss
246  if (fSaveBestOnly) {
247  PyRunString("callbacks.append(keras.callbacks.ModelCheckpoint('"+fFilenameTrainedModel+"', monitor='val_loss', verbose=verbose, save_best_only=True, mode='auto'))", "Failed to setup training callback: SaveBestOnly");
248  Log() << kINFO << "Option SaveBestOnly: Only model weights with smallest validation loss will be stored" << Endl;
249  }
250 
251  // Callback: Stop training early if no improvement in validation loss is observed
252  if (fTriesEarlyStopping>=0) {
253  TString tries;
254  tries.Form("%i", fTriesEarlyStopping);
255  PyRunString("callbacks.append(keras.callbacks.EarlyStopping(monitor='val_loss', patience="+tries+", verbose=verbose, mode='auto'))", "Failed to setup training callback: TriesEarlyStopping");
256  Log() << kINFO << "Option TriesEarlyStopping: Training will stop after " << tries << " number of epochs with no improvement of validation loss" << Endl;
257  }
258 
259  // Callback: Learning rate scheduler
260  if (fLearningRateSchedule!="") {
261  // Setup a python dictionary with the desired learning rate steps
262  PyRunString("strScheduleSteps = '"+fLearningRateSchedule+"'\n"
263  "schedulerSteps = {}\n"
264  "for c in strScheduleSteps.split(';'):\n"
265  " x = c.split(',')\n"
266  " schedulerSteps[int(x[0])] = float(x[1])\n",
267  "Failed to setup steps for scheduler function from string: "+fLearningRateSchedule,
268  Py_file_input);
269  // Set scheduler function as piecewise function with given steps
270  PyRunString("def schedule(epoch, model=model, schedulerSteps=schedulerSteps):\n"
271  " if epoch in schedulerSteps: return float(schedulerSteps[epoch])\n"
272  " else: return float(model.optimizer.lr.get_value())\n",
273  "Failed to setup scheduler function with string: "+fLearningRateSchedule,
274  Py_file_input);
275  // Setup callback
276  PyRunString("callbacks.append(keras.callbacks.LearningRateScheduler(schedule))",
277  "Failed to setup training callback: LearningRateSchedule");
278  Log() << kINFO << "Option LearningRateSchedule: Set learning rate during training: " << fLearningRateSchedule << Endl;
279  }
280 
281  // Train model
282  PyRunString("history = model.fit(trainX, trainY, sample_weight=trainWeights, batch_size=batchSize, nb_epoch=numEpochs, verbose=verbose, validation_data=(valX, valY, valWeights), callbacks=callbacks)",
283  "Failed to train model");
284 
285  /*
286  * Store trained model to file (only if option 'SaveBestOnly' is NOT activated,
287  * because we do not want to override the best model checkpoint)
288  */
289 
290  if (!fSaveBestOnly) {
291  PyRunString("model.save('"+fFilenameTrainedModel+"', overwrite=True)",
292  "Failed to save trained model: "+fFilenameTrainedModel);
293  Log() << kINFO << "Trained model written to file: " << fFilenameTrainedModel << Endl;
294  }
295 
296  /*
297  * Clean-up
298  */
299 
300  delete[] trainDataX;
301  delete[] trainDataY;
302  delete[] trainDataWeights;
303  delete[] valDataX;
304  delete[] valDataY;
305  delete[] valDataWeights;
306 }
307 
310 }
311 
313  // Cannot determine error
314  NoErrorCalc(errLower, errUpper);
315 
316  // Check whether the model is setup
317  // NOTE: unfortunately this is needed because during evaluation ProcessOptions is not called again
318  if (!fModelIsSetup) {
319  // Setup the trained model
320  SetupKerasModel(true);
321  }
322 
323  // Get signal probability (called mvaValue here)
324  const TMVA::Event* e = GetEvent();
325  for (UInt_t i=0; i<fNVars; i++) fVals[i] = e->GetValue(i);
326  PyRunString("for i,p in enumerate(model.predict(vals)): output[i]=p\n",
327  "Failed to get predictions");
328 
330 }
331 
332 std::vector<Double_t> MethodPyKeras::GetMvaValues(Long64_t firstEvt, Long64_t lastEvt, Bool_t) {
333  // Check whether the model is setup
334  // NOTE: Unfortunately this is needed because during evaluation ProcessOptions is not called again
335  if (!fModelIsSetup) {
336  // Setup the trained model
337  SetupKerasModel(true);
338  }
339 
340  // Load data to numpy array
342  if (firstEvt > lastEvt || lastEvt > nEvents) lastEvt = nEvents;
343  if (firstEvt < 0) firstEvt = 0;
344  nEvents = lastEvt-firstEvt;
345 
346  float* data = new float[nEvents*fNVars];
347  for (UInt_t i=0; i<nEvents; i++) {
348  Data()->SetCurrentEvent(i);
349  const TMVA::Event *e = GetEvent();
350  for (UInt_t j=0; j<fNVars; j++) {
351  data[j + i*fNVars] = e->GetValue(j);
352  }
353  }
354 
355  npy_intp dimsData[2] = {(npy_intp)nEvents, (npy_intp)fNVars};
356  PyArrayObject* pDataMvaValues = (PyArrayObject*)PyArray_SimpleNewFromData(2, dimsData, NPY_FLOAT, (void*)data);
357  if (pDataMvaValues==0) Log() << "Failed to load data to Python array" << Endl;
358 
359  // Get prediction for all events
360  PyObject* pModel = PyDict_GetItemString(fLocalNS, "model");
361  if (pModel==0) Log() << kFATAL << "Failed to get model Python object" << Endl;
362  PyArrayObject* pPredictions = (PyArrayObject*) PyObject_CallMethod(pModel, (char*)"predict", (char*)"O", pDataMvaValues);
363  if (pPredictions==0) Log() << kFATAL << "Failed to get predictions" << Endl;
364  delete[] data;
365 
366  // Load predictions to double vector
367  // NOTE: The signal probability is given at the output
368  std::vector<double> mvaValues(nEvents);
369  float* predictionsData = (float*) PyArray_DATA(pPredictions);
370  for (UInt_t i=0; i<nEvents; i++) {
371  mvaValues[i] = (double) predictionsData[i*fNOutputs + TMVA::Types::kSignal];
372  }
373 
374  return mvaValues;
375 }
376 
377 std::vector<Float_t>& MethodPyKeras::GetRegressionValues() {
378  // Check whether the model is setup
379  // NOTE: unfortunately this is needed because during evaluation ProcessOptions is not called again
380  if (!fModelIsSetup){
381  // Setup the model and load weights
382  SetupKerasModel(true);
383  }
384 
385  // Get regression values
386  const TMVA::Event* e = GetEvent();
387  for (UInt_t i=0; i<fNVars; i++) fVals[i] = e->GetValue(i);
388  PyRunString("for i,p in enumerate(model.predict(vals)): output[i]=p\n",
389  "Failed to get predictions");
390 
391  // Use inverse transformation of targets to get final regression values
392  Event * eTrans = new Event(*e);
393  for (UInt_t i=0; i<fNOutputs; ++i) {
394  eTrans->SetTarget(i,fOutput[i]);
395  }
396 
397  const Event* eTrans2 = GetTransformationHandler().InverseTransform(eTrans);
398  for (UInt_t i=0; i<fNOutputs; ++i) {
399  fOutput[i] = eTrans2->GetTarget(i);
400  }
401 
402  return fOutput;
403 }
404 
405 std::vector<Float_t>& MethodPyKeras::GetMulticlassValues() {
406  // Check whether the model is setup
407  // NOTE: unfortunately this is needed because during evaluation ProcessOptions is not called again
408  if (!fModelIsSetup){
409  // Setup the model and load weights
410  SetupKerasModel(true);
411  }
412 
413  // Get class probabilites
414  const TMVA::Event* e = GetEvent();
415  for (UInt_t i=0; i<fNVars; i++) fVals[i] = e->GetValue(i);
416  PyRunString("for i,p in enumerate(model.predict(vals)): output[i]=p\n",
417  "Failed to get predictions");
418 
419  return fOutput;
420 }
421 
423 }
424 
426 // typical length of text line:
427 // "|--------------------------------------------------------------|"
428  Log() << Endl;
429  Log() << "Keras is a high-level API for the Theano and Tensorflow packages." << Endl;
430  Log() << "This method wraps the training and predictions steps of the Keras" << Endl;
431  Log() << "Python package for TMVA, so that dataloading, preprocessing and" << Endl;
432  Log() << "evaluation can be done within the TMVA system. To use this Keras" << Endl;
433  Log() << "interface, you have to generate a model with Keras first. Then," << Endl;
434  Log() << "this model can be loaded and trained in TMVA." << Endl;
435  Log() << Endl;
436 }
Double_t GetMvaValue(Double_t *errLower, Double_t *errUpper)
const TString & GetWeightFileDir() const
Definition: MethodBase.h:474
Long64_t GetNTestEvents() const
Definition: DataSet.h:80
MsgLogger & Endl(MsgLogger &ml)
Definition: MsgLogger.h:158
Singleton class for Global types used by TMVA.
Definition: Types.h:73
long long Long64_t
Definition: RtypesCore.h:69
const char * GetName() const
Definition: MethodBase.h:318
UInt_t GetNClasses() const
Definition: DataSetInfo.h:136
OptionBase * DeclareOptionRef(T &ref, const TString &name, const TString &desc="")
const Event * GetTestingEvent(Long64_t ievt) const
Definition: MethodBase.h:759
DataSet * Data() const
Definition: MethodBase.h:393
EAnalysisType
Definition: Types.h:125
UInt_t GetNTargets() const
Definition: DataSetInfo.h:111
TransformationHandler & GetTransformationHandler(Bool_t takeReroutedIfAvailable=true)
Definition: MethodBase.h:378
bool Bool_t
Definition: RtypesCore.h:59
Double_t GetWeight() const
return the event weight - depending on whether the flag IgnoreNegWeightsInTraining is or not...
Definition: Event.cxx:382
static int PyIsInitialized()
Check Python interpreter initialization status.
Float_t GetValue(UInt_t ivar) const
return value of i&#39;th variable
Definition: Event.cxx:237
std::vector< Float_t > & GetRegressionValues()
Types::EAnalysisType GetAnalysisType() const
Definition: MethodBase.h:421
void PyRunString(TString code, TString errorMessage="Failed to run python code", int start=Py_single_input)
Execute Python code from string.
TString fFilenameTrainedModel
Definition: MethodPyKeras.h:88
void SetCurrentEvent(Long64_t ievt) const
Definition: DataSet.h:99
Class that contains all the data information.
Definition: DataSetInfo.h:60
Bool_t HasAnalysisType(Types::EAnalysisType type, UInt_t numberClasses, UInt_t)
TString fLearningRateSchedule
Definition: MethodPyKeras.h:81
UInt_t GetNVariables() const
Definition: MethodBase.h:329
const int nEvents
Definition: testRooFit.cxx:42
const Event * GetTrainingEvent(Long64_t ievt) const
Definition: MethodBase.h:753
unsigned int UInt_t
Definition: RtypesCore.h:42
const Event * GetEvent() const
Definition: MethodBase.h:733
void SetTarget(UInt_t itgt, Float_t value)
set the target value (dimension itgt) to value
Definition: Event.cxx:360
std::vector< Float_t > & GetMulticlassValues()
void GetHelpMessage() const
const Bool_t kFALSE
Definition: RtypesCore.h:92
void SetupKerasModel(Bool_t loadTrainedModel)
#define ClassImp(name)
Definition: Rtypes.h:336
double Double_t
Definition: RtypesCore.h:55
std::vector< Double_t > GetMvaValues(Long64_t firstEvt, Long64_t lastEvt, Bool_t logProgress)
get all the MVA values for the events of the current Data type
int type
Definition: TGX11.cxx:120
Long64_t GetNEvents(Types::ETreeType type=Types::kMaxTreeType) const
Definition: DataSet.h:215
virtual void TestClassification()
initialization
MsgLogger & Log() const
Definition: Configurable.h:122
you should not use this method at all Int_t Int_t Double_t Double_t Double_t e
Definition: TRolke.cxx:630
DataSetInfo & DataInfo() const
Definition: MethodBase.h:394
UInt_t GetClass() const
Definition: Event.h:81
Float_t GetTarget(UInt_t itgt) const
Definition: Event.h:97
#define REGISTER_METHOD(CLASS)
for example
PyObject * fLocalNS
Definition: PyMethodBase.h:143
Long64_t GetNTrainingEvents() const
Definition: DataSet.h:79
MethodPyKeras(const TString &jobName, const TString &methodTitle, DataSetInfo &dsi, const TString &theOption="")
const Bool_t kTRUE
Definition: RtypesCore.h:91
virtual void TestClassification()
initialization
std::vector< float > fOutput
Definition: MethodPyKeras.h:85
_object PyObject
Definition: TPyArg.h:20
void NoErrorCalc(Double_t *const err, Double_t *const errUpper)
Definition: MethodBase.cxx:829
const Event * InverseTransform(const Event *, Bool_t suppressIfNoTargets=true) const