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
TF2 porting: Enable early stopping + model save and load #739
Merged
w4nderlust
merged 37 commits into
ludwig-ai:tf2_porting
from
jimthompson5802:tf2_early_stopping
Jul 2, 2020
Merged
Changes from 7 commits
Commits
Show all changes
37 commits
Select commit
Hold shift + click to select a range
38910b6
feat: enabled early stopping and add early stopping unit test
jimthompson5802 6b10058
feat: enable model save and restore functions with unit test
jimthompson5802 522e702
Merge remote-tracking branch 'upstream/tf2_porting' into tf2_early_st…
jimthompson5802 d0eaf2b
Merge branch 'tf2_early_stopping' into tf2_early_stopping_model_save_…
jimthompson5802 9d6627c
refactor: eliminate warning pycharm warning message
jimthompson5802 a70d0e5
feat: add test for saving progress weights and final model
jimthompson5802 e45a155
feat: update restoring function
jimthompson5802 5a33a45
test: change assertion test for model save/resume
jimthompson5802 91bc289
refactor: re-enable resume_session() and restore() methods
jimthompson5802 acb8bed
fix: ValueError when saving model to disk
jimthompson5802 0264959
fix: resolve error when restoring saved model
jimthompson5802 037e3b7
fix: adapt test to directory for TF2 saved model file storage
jimthompson5802 1444c69
fix: syntax error
jimthompson5802 ddc7157
fix: TEMPORARY CODE FOR DEBUGGING PURPOSES - NEED TO BE REPLACED
jimthompson5802 48a9f41
feat: for model save/restore support dictionary of custom objects
jimthompson5802 0e5d1e3
test: VERSION USED FOR DEBUGGING
jimthompson5802 fccd4b1
refactor: change from savedmodel to save_weights approach
jimthompson5802 157caa3
refactor: remove hack for initializing weights
jimthompson5802 c0d28f2
fix: reporting metrics in wrong order when resuming model training
jimthompson5802 799406d
feat: initial working LudwigModel.predict() method with TF2
jimthompson5802 d4035fc
feat: allow specification of optimizer
jimthompson5802 0fdeed6
fix: restoration of saved model weights
jimthompson5802 182eea2
Added save and reload test using APIs
w4nderlust 0e88675
Fix: encoder creation in binary feature
w4nderlust fc809f9
Expanded the test_model_save_reload_API test
w4nderlust da1ef01
Fix: vector fature encoder return from dict of dict to dict
w4nderlust dbe7c33
Fix: bag feature_data when input is a dataframe
w4nderlust 88df360
Fix: image feature_data when input is a dataframe
w4nderlust 8593dd9
Fix: image feature_data cleanup
w4nderlust 64efb3a
Fix: audio feature_data when input is a dataframe
w4nderlust fe0245e
Fix: most input features now work in test_model_save_reload_API
w4nderlust 8e13c08
Fix: set output feature bugs (missing import for loss in metrics, mis…
w4nderlust eb13293
Fix: vector output feature bugs (missed kwargs and missing call to .n…
w4nderlust e66e3ef
Added additional outputs (ctegory, set, vector) to test_model_save_re…
w4nderlust d64953a
Merge branch 'tf2_porting' into tf2_early_stopping
w4nderlust fbfcb84
Added timeseries inputs to test_model_save_reload_API test
w4nderlust eedc838
fix: IndexError exception after model weights restore - work-in-progress
jimthompson5802 File filter
Filter by extension
Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,204 @@ | ||
import os.path | ||
import json | ||
from collections import namedtuple | ||
|
||
import pandas as pd | ||
import numpy as np | ||
from sklearn.model_selection import train_test_split | ||
from sklearn.metrics import mean_squared_error | ||
|
||
import pytest | ||
|
||
from ludwig.experiment import full_experiment | ||
from ludwig.predict import full_predict | ||
|
||
GeneratedData = namedtuple('GeneratedData', | ||
'train_df validation_df test_df') | ||
|
||
def get_feature_definitions(): | ||
input_features = [ | ||
{'name': 'x', 'type': 'numerical'}, | ||
] | ||
output_features = [ | ||
{'name': 'y', 'type': 'numerical', 'loss': {'type': 'mean_squared_error'}, | ||
'num_fc_layers': 5, 'fc_size': 64} | ||
] | ||
|
||
return input_features, output_features | ||
|
||
|
||
@pytest.fixture | ||
def generated_data(): | ||
# function generates simple training data that guarantee convergence | ||
# within 30 epochs for suitable model definition | ||
NUMBER_OBSERVATIONS = 500 | ||
|
||
# generate data | ||
np.random.seed(43) | ||
x = np.array(range(NUMBER_OBSERVATIONS)).reshape(-1, 1) | ||
y = 2*x + 1 + np.random.normal(size=x.shape[0]).reshape(-1, 1) | ||
raw_df = pd.DataFrame(np.concatenate((x, y), axis=1), columns=['x', 'y']) | ||
|
||
# create training data | ||
train, valid_test = train_test_split(raw_df, train_size=0.7) | ||
|
||
# create validation and test data | ||
validation, test = train_test_split(valid_test, train_size=0.5) | ||
|
||
return GeneratedData(train, validation, test) | ||
|
||
@pytest.mark.parametrize('early_stop', [3, 5]) | ||
def test_early_stopping(early_stop, generated_data, tmp_path): | ||
|
||
input_features, output_features = get_feature_definitions() | ||
|
||
model_definition = { | ||
'input_features': input_features, | ||
'output_features': output_features, | ||
'combiner': { | ||
'type': 'concat' | ||
}, | ||
'training': { | ||
'epochs': 30, | ||
'early_stop': early_stop, | ||
'batch_size': 16 | ||
} | ||
} | ||
|
||
# create sub-directory to store results | ||
results_dir = tmp_path / 'results' | ||
results_dir.mkdir() | ||
|
||
# run experiment | ||
exp_dir_name = full_experiment( | ||
data_train_df=generated_data.train_df, | ||
data_validation_df=generated_data.validation_df, | ||
data_test_df=generated_data.test_df, | ||
output_directory=str(results_dir), | ||
model_definition=model_definition, | ||
skip_save_processed_input=True, | ||
skip_save_progress=True, | ||
skip_save_unprocessed_output=True, | ||
skip_save_model=True, | ||
skip_save_log=True | ||
) | ||
|
||
# test existence of required files | ||
train_stats_fp = os.path.join(exp_dir_name, 'training_statistics.json') | ||
metadata_fp = os.path.join(exp_dir_name, 'description.json') | ||
assert os.path.isfile(train_stats_fp) | ||
assert os.path.isfile(metadata_fp) | ||
|
||
# retrieve results so we can validate early stopping | ||
with open(train_stats_fp,'r') as f: | ||
train_stats = json.load(f) | ||
with open(metadata_fp, 'r') as f: | ||
metadata = json.load(f) | ||
|
||
# get early stopping value | ||
early_stop_value = metadata['model_definition']['training']['early_stop'] | ||
|
||
# retrieve validation losses | ||
vald_losses = np.array(train_stats['validation']['combined']['loss']) | ||
last_epoch = vald_losses.shape[0] | ||
best_epoch = np.argmin(vald_losses) | ||
|
||
# confirm early stopping | ||
assert (last_epoch - best_epoch - 1) == early_stop_value | ||
|
||
@pytest.mark.parametrize('skip_save_progress', [False, True]) | ||
@pytest.mark.parametrize('skip_save_model', [False, True]) | ||
def test_model_progress_save( | ||
skip_save_progress, | ||
skip_save_model, | ||
generated_data, | ||
tmp_path | ||
): | ||
|
||
input_features, output_features = get_feature_definitions() | ||
|
||
model_definition = { | ||
'input_features': input_features, | ||
'output_features': output_features, | ||
'combiner': {'type': 'concat', 'fc_size': 14}, | ||
'training': {'epochs': 10} | ||
} | ||
|
||
# create sub-directory to store results | ||
results_dir = tmp_path / 'results' | ||
results_dir.mkdir() | ||
|
||
# run experiment | ||
exp_dir_name = full_experiment( | ||
data_train_df=generated_data.train_df, | ||
data_validation_df=generated_data.validation_df, | ||
data_test_df=generated_data.test_df, | ||
output_directory=str(results_dir), | ||
model_definition=model_definition, | ||
skip_save_processed_input=True, | ||
skip_save_progress=skip_save_progress, | ||
skip_save_unprocessed_output=True, | ||
skip_save_model=skip_save_model, | ||
skip_save_log=True | ||
) | ||
|
||
#========== Check for required result data sets ============= | ||
if skip_save_model: | ||
assert not os.path.isfile( | ||
os.path.join(exp_dir_name, 'model', 'model_weights.index') | ||
) | ||
else: | ||
assert os.path.isfile( | ||
os.path.join(exp_dir_name, 'model', 'model_weights.index') | ||
) | ||
|
||
if skip_save_progress: | ||
assert not os.path.isfile( | ||
os.path.join(exp_dir_name, 'model', 'model_weights_progress.index') | ||
) | ||
else: | ||
assert os.path.isfile( | ||
os.path.join(exp_dir_name, 'model', 'model_weights_progress.index') | ||
) | ||
|
||
|
||
# work-in-progress | ||
def test_model_save_resume(generated_data, tmp_path): | ||
|
||
input_features, output_features = get_feature_definitions() | ||
model_definition = { | ||
'input_features': input_features, | ||
'output_features': output_features, | ||
'combiner': {'type': 'concat', 'fc_size': 14}, | ||
'training': {'epochs': 30, 'early_stop': 5} | ||
} | ||
|
||
# create sub-directory to store results | ||
results_dir = tmp_path / 'results' | ||
results_dir.mkdir() | ||
|
||
exp_dir_name = full_experiment( | ||
model_definition, | ||
data_train_df=generated_data.train_df, | ||
data_validation_df=generated_data.validation_df, | ||
data_test_df=generated_data.test_df, | ||
output_directory=results_dir | ||
) | ||
|
||
full_experiment( | ||
model_definition, | ||
data_train_df=generated_data.train_df, | ||
model_resume_path=exp_dir_name | ||
) | ||
|
||
test_fp = os.path.join(str(tmp_path), 'data_to_predict.csv') | ||
generated_data.test_df.to_csv( | ||
test_fp, | ||
index=False | ||
) | ||
|
||
full_predict(os.path.join(exp_dir_name, 'model'), data_csv=test_fp) | ||
|
||
y_pred = np.load(os.path.join(exp_dir_name, 'y_predictions.npy')) | ||
|
||
mse = mean_squared_error(y_pred, generated_data.test_df['y']) | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what we can do here is that after the first full experiment we load the numpy predictions, and after the second experiment with resume we load the numpy predictions and then we assert that they are the same with
np.isclose(first_preds, second_preds)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I made the recommended change in the test. Here is the commit 5a33a45
I think either restore may not be saving the weights or the resume is not loading the weights correctly. The last epoch on the first
full_experiment
looks like thisOn the second
full_experiment
with the resume, the first epoch report is epoch 28, which I think makes sense but the values don't look correct