Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Machine Learning & Artificial Intelligence Plugin #995

Merged
merged 51 commits into from
Mar 3, 2022

Conversation

bpaul4
Copy link
Contributor

@bpaul4 bpaul4 commented Jan 4, 2022

Building off FOQUS's existing plugin to load custom Pymodel scripts, this tool allows users to import Tensorflow Keras models into FOQUS nodes. As a first draft, the tool enables new flowsheet capabilities using the same UI as the Pymodel plugin:

  • Users can save Sequential or Functional API Keras models as H5 files, and import into FOQUS nodes for model prediction. The tool will create default variable labels and bounds as needed
  • Users can follow the documentation workflow and examples to create a CustomLayer class, and the plugin will extract any custom labels/bounds into the Node Editor
  • Users can specify whether the training utilized data normalization, and the tool will handle needed scaling/unscaling internally (only linear scaling is currently supported)

@bpaul4 bpaul4 self-assigned this Jan 4, 2022
@ksbeattie ksbeattie added Priority:Normal Normal Priority Issue or PR and removed Priority:Normal Normal Priority Issue or PR labels Jan 11, 2022
@bpaul4 bpaul4 marked this pull request as ready for review February 22, 2022 21:18
@bpaul4 bpaul4 changed the title (draft) Machine Learning & Artificial Intelligence Plugin Machine Learning & Artificial Intelligence Plugin Feb 23, 2022
Add GUI tests for ML/AI plugin
@bpaul4
Copy link
Contributor Author

bpaul4 commented Feb 24, 2022

@lbianchi-lbl your tests and CI updates are very well written, and it appears to have resolved all remaining GitHub Actions issues - thank you for your continuous efforts on this task.

@anujad95 @jmorgan29 @MAZamarripa please provide any additional feedback you have on the Plugin code or the new tests so we may merge these updates soon; @anujad95 and I previously tested the tool on our local machines with no issues.

Copy link
Contributor

@lbianchi-lbl lbianchi-lbl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following the discussion today during the dev call, we came to the conclusion that making the exception handling more specific could be a low-effort way to make unexpected errors more visible. I've left review comments where I think it would make sense to do this, as well as a few suggestions on the implementation.

import tensorflow as tf

load = tf.keras.models.load_model
except:
Copy link
Contributor

@lbianchi-lbl lbianchi-lbl Mar 1, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
except:
except ImportError:
# if TensorFlow is not available, create a proxy function that will raise an exception
# whenever code tries to use `load()` at runtime
def load(*args, **kwargs):
raise RuntimeError(f"`load()` was called with args={args}, kwargs={kwargs} but `tensorflow` is not available")

for i in range(np.shape(self.model.inputs[0])[1]):
try:
input_label = self.model.layers[1].input_labels[i]
except:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestions to make exceptions more specific:

  • Use except MyExceptionType: instead of "bare except
  • Add a comment with more details on when that exception might occur (e.g. "this happens if the input labels in the model do not match those stored in the file" (just an example))

input_min = 0 # not necessarily a good default
try:
input_max = self.model.layers[1].input_bounds[input_label][1]
except:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: make except clause more specific (see above for suggestions)

for j in range(np.shape(self.model.outputs[0])[1]):
try:
output_label = self.model.layers[1].output_labels[j]
except:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: make except clause more specific (see above for suggestions)

output_label = "z" + str(j + 1)
try:
output_min = self.model.layers[1].output_bounds[output_label][0]
except:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: make except clause more specific (see above for suggestions)

output_min = 0 # not necessarily a good default
try:
output_max = self.model.layers[1].output_bounds[output_label][1]
except:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: make except clause more specific (see above for suggestions)

# check if user passed a model for normalized data - FOQUS will automatically scale/un-scale
try: # if attribute exists, user has specified a model form
self.normalized = self.model.layers[1].normalized
except: # otherwise user did not pass a normalized model
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: make except clause more specific (see above for suggestions)

str(self.modelName): getattr(module, str(self.modelName))
},
)
except: # try to load model without custom layer
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: make except clause more specific (see above for suggestions)

)
except: # try to load model without custom layer
self.model = load(str(self.modelName) + ".h5")
os.chdir(cwd) # reset to original working directory
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resetting the working dir inside a finally block ensures that it is always restored, regardless of what exceptions are thrown in the previous lines.

Suggested change
os.chdir(cwd) # reset to original working directory
finally:
os.chdir(cwd) # reset to original working directory

Comment on lines 1109 to 1111
except: # try to load model without custom layer
self.model = load(str(self.modelName) + ".h5")
os.chdir(cwd) # reset to original working directory
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above for exception and os.chdir() handling

@bpaul4 bpaul4 requested a review from lbianchi-lbl March 2, 2022 18:45
Copy link
Contributor

@lbianchi-lbl lbianchi-lbl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @bpaul4 for adding the more specific exceptions and especially the detailed messages, I think they're really helpful.

I've left two very minor corrections, and they're completely "take-it-or-leave-it", so feel free to dismiss them and I'm happy to approve either way.

# if TensorFlow is not available, create a proxy function that will raise
# an exception whenever code tries to use `load()` at runtime
def load(*args, **kwargs):
raise RuntimeError(f"`load()` was called with args={args},"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor suggestion, but in retrospective ModuleNotFoundError would be a better fit than RuntimeError here.

@@ -106,15 +109,33 @@ def __init__(self, model):
for i in range(np.shape(self.model.inputs[0])[1]):
try:
input_label = self.model.layers[1].input_labels[i]
except:
except AttributeError:
logging.getLogger("foqus." + __name__).info(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these extended messages are great, they add lots of useful context both for users and other developers.

One minor suggestion: instead of repeating the logging.getLogger("foqus." + __name__) each time, you could create a logger object at the module level, e.g. _logger = logging.getLogger("foqus." + __name__) after the import statements on Line 40, and then use that object throughout the rest of the module, i.e. _logger.info(...)

lbianchi-lbl
lbianchi-lbl previously approved these changes Mar 2, 2022
@bpaul4
Copy link
Contributor Author

bpaul4 commented Mar 2, 2022

Thanks @lbianchi-lbl, I agree with your final two suggestions. I'll make those changes and push again for final approval.

@bpaul4 bpaul4 requested a review from lbianchi-lbl March 2, 2022 20:40
Copy link
Contributor

@lbianchi-lbl lbianchi-lbl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good!

@lbianchi-lbl
Copy link
Contributor

I believe that the last round of reviews and changes covers what we discussed on Tuesday, so I'm going ahead and merge this in. Thanks to all those involved and @bpaul4 in particular for bringing this over the finish line!

@lbianchi-lbl lbianchi-lbl merged commit a3fcb81 into CCSI-Toolset:master Mar 3, 2022
@bpaul4 bpaul4 deleted the ml_ai_plugin branch August 18, 2023 16:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Priority:High High Priority Issue or PR
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants