Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
  • Loading branch information
amaiya committed May 26, 2020
2 parents 99cfaa9 + 95e4136 commit 7671f36
Show file tree
Hide file tree
Showing 11 changed files with 158 additions and 57 deletions.
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,26 @@ Most recent releases are shown at the top. Each release shows:
- **Fixed**: Bug fixes that don't change documented behaviour


## 0.15.3 (2020-05-28)

### New:
- N/A

### Changed
- [**breaking change**] The `multilabel` argument in `text.Transformer` constructor was moved to `Transformer.get_classifier` and now correctly allows
users to forcibly configure model for multilabel task regardless as to what data suggests. However, it is recommended to leave this value as `None`.
- The methods `predictor.save`, `ktrain.load_predictor`, `learner.save_model`, `learner.load_model` all now accept a path to folder where
all files (e.g., model file, `.preproc` file) will be saved. If path does not exist, it will be created.
This should not be a breaking change as the `load*` methods will still look for files in the old location if model or predictor was saved
using an older version of *ktrain*.

### Fixed:
- N/A





## 0.15.2 (2020-05-15)

### New:
Expand Down
5 changes: 4 additions & 1 deletion ktrain/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,11 @@ def get_learner(model, train_data=None, val_data=None,
val_data is instance of utils.Sequence.
default:32
workers (int): number of cpu processes used to load data.
only applicable if train_data is is a generator.
This is ignored unless train_data/val_data is an instance of
tf.keras.preprocessing.image.DirectoryIterator or tf.keras.preprocessing.image.DataFrameIterator.
use_multiprocessing(bool): whether or not to use multiprocessing for workers
This is ignored unless train_data/val_data is an instance of
tf.keras.preprocessing.image.DirectoryIterator or tf.keras.preprocessing.image.DataFrameIterator.
multigpu(bool): Lets the Learner know that the model has been
replicated on more than 1 GPU.
Only supported for models from vision.image_classifiers
Expand Down
49 changes: 38 additions & 11 deletions ktrain/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,11 +226,23 @@ def view_top_losses(self, n=4, preproc=None, val_data=None):
raise NotImplementedError('view_top_losses must be overriden by Learner subclass')


def _make_model_folder(self, fpath):
if os.path.isfile(fpath):
raise ValueError(f'There is an existing file named {fpath}. ' +\
'Please use dfferent value for fpath.')
elif os.path.exists(fpath):
#warnings.warn('model is being saved to folder that already exists: %s' % (fpath))
pass
elif not os.path.exists(fpath):
os.makedirs(fpath)


def save_model(self, fpath):
"""
a wrapper to model.save
"""
self.model.save(fpath, save_format='h5')
self._make_model_folder(fpath)
self.model.save(os.path.join(fpath, U.MODEL_NAME), save_format='h5')
return


Expand Down Expand Up @@ -1302,21 +1314,31 @@ def get_predictor(model, preproc, batch_size=U.DEFAULT_BS):
raise Exception('preproc of type %s not currently supported' % (type(preproc)))


def load_predictor(fname, batch_size=U.DEFAULT_BS):
def load_predictor(fpath, batch_size=U.DEFAULT_BS):
"""
Loads a previously saved Predictor instance
Args
fname(str): predictor path name (value supplied to predictor.save)
fpath(str): predictor path name (value supplied to predictor.save)
From v0.16.x, this is always the path to a folder.
Pre-v0.16.x, this is the base name used to save model and .preproc instance.
batch_size(int): batch size to use for predictions. default:32
"""

# load the preprocessor
preproc = None
with open(fname +'.preproc', 'rb') as f:
preproc = pickle.load(f)
try:
preproc_name = os.path.join(fpath, U.PREPROC_NAME)
with open(preproc_name, 'rb') as f: preproc = pickle.load(f)
except:
try:
preproc_name = fpath +'.preproc'
#warnings.warn('could not load .preproc file as %s - attempting to load as %s' % (os.path.join(fpath, U.PREPROC_NAME), preproc_name))
with open(preproc_name, 'rb') as f: preproc = pickle.load(f)
except:
raise Exception('Could not find a .preproc file in either the post v0.16.x loction (%s) or pre v0.16.x location (%s)' % (os.path.join(fpath. U.PRERPC_NAME), fpath+'.preproc'))

# load the model
model = _load_model(fname, preproc=preproc)
model = _load_model(fpath, preproc=preproc)


# preprocessing functions in ImageDataGenerators are not pickable
Expand Down Expand Up @@ -1374,12 +1396,12 @@ def release_gpu_memory(device=0):
return


def _load_model(fname, preproc=None, train_data=None, custom_objects=None):
def _load_model(fpath, preproc=None, train_data=None, custom_objects=None):
if not preproc and not train_data:
raise ValueError('Either preproc or train_data is required.')
if preproc and isinstance(preproc, TransformersPreprocessor):
# note: with transformer models, fname is actually a directory
model = preproc.get_model(fpath=fname)
model = preproc.get_model(fpath=fpath)
return model
elif (preproc and (isinstance(preproc, BERTPreprocessor) or \
type(preproc).__name__ == 'BERTPreprocessor')) or\
Expand Down Expand Up @@ -1407,10 +1429,15 @@ def _load_model(fname, preproc=None, train_data=None, custom_objects=None):
custom_objects['AdamWeightDecay'] = AdamWeightDecay
try:
try:
model = load_model(fname, custom_objects=custom_objects)
model = load_model(os.path.join(fpath, U.MODEL_NAME), custom_objects=custom_objects)
except:
# for bilstm models without CRF layer on TF2 where CRF is not supported
model = load_model(fname, custom_objects={'AdamWeightDecay':AdamWeightDecay})
try:
# pre-0.16: model fpath was file name of model not folder for non-Transformer models
#warnings.warn('could not load model as %s - attempting to load model as %s' % (os.path.join(fpath, U.MODEL_NAME), fpath))
model = load_model(fpath, custom_objects=custom_objects)
except:
# for bilstm models without CRF layer on TF2 where CRF is not supported
model = load_model(fpath, custom_objects={'AdamWeightDecay':AdamWeightDecay})
except Exception as e:
print('Call to keras.models.load_model failed. '
'Try using the learner.model.save_weights and '
Expand Down
42 changes: 35 additions & 7 deletions ktrain/predictor.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,45 @@ def get_classes(self, filename):
def explain(self, x):
raise NotImplementedError('explain is not currently supported for this model')

def save(self, fname):

if U.is_crf(self.model):
from .text.ner import crf_loss
self.model.compile(loss=crf_loss, optimizer=U.DEFAULT_OPT)
def _make_predictor_folder(self, fpath):
if os.path.isfile(fpath):
raise ValueError(f'There is an existing file named {fpath}. ' +\
'Please use dfferent value for fpath.')
elif os.path.exists(fpath):
#warnings.warn('predictor files are being saved to folder that already exists: %s' % (fpath))
pass
elif not os.path.exists(fpath):
os.makedirs(fpath)
return

self.model.save(fname, save_format='h5')
fname_preproc = fname+'.preproc'
with open(fname_preproc, 'wb') as f:

def _save_preproc(self, fpath):
with open(os.path.join(fpath, U.PREPROC_NAME), 'wb') as f:
pickle.dump(self.preproc, f)
return


def _save_model(self, fpath):
if U.is_crf(self.model): # TODO: fix/refactor this
from .text.ner import crf_loss
self.model.compile(loss=crf_loss, optimizer=U.DEFAULT_OPT)
model_path = os.path.join(fpath, U.MODEL_NAME)
self.model.save(model_path, save_format='h5')
return



def save(self, fpath):
"""
saves both model and Preprocessor instance associated with Predictor
Args:
fpath(str): path to folder to store model and Preprocessor instance (.preproc file)
Returns:
None
"""
self._make_predictor_folder(fpath)
self._save_model(fpath)
self._save_preproc(fpath)
return

9 changes: 4 additions & 5 deletions ktrain/text/learner.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,18 +165,17 @@ def save_model(self, fpath):
"""
save Transformers model
"""
if os.path.isfile(fpath):
raise ValueError(f'There is an existing file named {fpath}. ' +\
'Please use dfferent value for fpath.')
elif not os.path.exists(fpath):
os.mkdir(fpath)
self._make_model_folder(fpath)
self.model.save_pretrained(fpath)
return


def load_model(self, fpath, preproc=None):
"""
load Transformers model
Args:
fpath(str): path to folder containing model files
preproc(TransformerPreprocessor): a TransformerPreprocessor instance.
"""
if preproc is None or not isinstance(preproc, TransformersPreprocessor):
raise ValueError('preproc arg is required to load Transformer models from disk. ' +\
Expand Down
3 changes: 2 additions & 1 deletion ktrain/text/ner/learner.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,10 +142,11 @@ def save_model(self, fpath):
"""
a wrapper to model.save
"""
self._make_model_folder(fpath)
if U.is_crf(self.model):
from .anago.layers import crf_loss
self.model.compile(loss=crf_loss, optimizer=U.DEFAULT_OPT)
self.model.save(fpath, save_format='h5')
self.model.save(os.path.join(fpath, U.MODEL_NAME), save_format='h5')
return


Expand Down
13 changes: 2 additions & 11 deletions ktrain/text/predictor.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,19 +152,10 @@ def analyze_valid(self, val_tup, print_report=True, multilabel=None):
return cm


def save(self, fpath):

def _save_model(self, fpath):
if isinstance(self.preproc, TransformersPreprocessor):
if os.path.isfile(fpath):
raise ValueError(f'There is an existing file named {fpath}. ' +\
'Please use dfferent value for fpath.')
elif not os.path.exists(fpath):
os.mkdir(fpath)
self.model.save_pretrained(fpath)
fname_preproc = fpath+'.preproc'
with open(fname_preproc, 'wb') as f:
pickle.dump(self.preproc, f)
else:
super().save(fpath)
super()._save_model(fpath)
return

59 changes: 45 additions & 14 deletions ktrain/text/preprocessor.py
Original file line number Diff line number Diff line change
Expand Up @@ -376,12 +376,12 @@ class TextPreprocessor(Preprocessor):
Text preprocessing base class
"""

def __init__(self, maxlen, class_names, lang='en', multilabel=False):
def __init__(self, maxlen, class_names, lang='en', multilabel=None):

self.set_classes(class_names) # converts to list of necessary
self.maxlen = maxlen
self.lang = lang
self.multilabel = multilabel
self.multilabel = multilabel # currently, this is always initially set None until set by set_multilabel
self.preprocess_train_called = False
self.label_encoder = None # only set if y is in string format
self.c = self.c.tolist() if isinstance(self.c, np.ndarray) else self.c
Expand Down Expand Up @@ -421,9 +421,19 @@ def preprocess(self, texts):
raise NotImplementedError


def set_multilabel(self, data, mode):
def set_multilabel(self, data, mode, verbose=1):
if mode == 'train' and self.get_classes():
self.multilabel = U.is_multilabel(data)
original_multilabel = self.multilabel
discovered_multilabel = U.is_multilabel(data)
if original_multilabel is None:
self.multilabel = discovered_multilabel
elif original_multilabel is True and discovered_multilabel is False:
warnings.warn('The multilabel=True argument was supplied, but labels do not indicate '+\
'a multilabel problem (labels appear to be mutually-exclusive). Using multilabel=True anyways.')
elif original_multilabel is False and discovered_multilabel is True:
warnings.warn('The multilabel=False argument was supplied, but labels inidcate that '+\
'this is a multilabel problem (labels are not mutually-exclusive). Using multilabel=False anyways.')
U.vprint("Is Multi-Label? %s" % (self.multilabel), verbose=verbose)


def undo(self, doc):
Expand Down Expand Up @@ -527,7 +537,7 @@ class StandardTextPreprocessor(TextPreprocessor):
"""

def __init__(self, maxlen, max_features, class_names=[], classes=[],
lang='en', ngram_range=1, multilabel=False):
lang='en', ngram_range=1, multilabel=None):
class_names = self.migrate_classes(class_names, classes)
super().__init__(maxlen, class_names, lang=lang, multilabel=multilabel)
self.tok = None
Expand Down Expand Up @@ -706,7 +716,7 @@ class BERTPreprocessor(TextPreprocessor):
"""

def __init__(self, maxlen, max_features, class_names=[], classes=[],
lang='en', ngram_range=1, multilabel=False):
lang='en', ngram_range=1, multilabel=None):
class_names = self.migrate_classes(class_names, classes)


Expand Down Expand Up @@ -777,7 +787,7 @@ class TransformersPreprocessor(TextPreprocessor):

def __init__(self, model_name,
maxlen, max_features, class_names=[], classes=[],
lang='en', ngram_range=1, multilabel=False):
lang='en', ngram_range=1, multilabel=None):
class_names = self.migrate_classes(class_names, classes)

if maxlen > 512: raise ValueError('Transformer models only supports maxlen <= 512')
Expand Down Expand Up @@ -863,7 +873,7 @@ def preprocess_train(self, texts, y=None, mode='train', verbose=1):
pad_on_left=bool(self.name in ['xlnet']),
pad_token=self.tok.convert_tokens_to_ids([self.tok.pad_token][0]),
pad_token_segment_id=4 if self.name in ['xlnet'] else 0)
self.set_multilabel(dataset, mode)
self.set_multilabel(dataset, mode, verbose=verbose)
if mode == 'train': self.preprocess_train_called = True
return dataset

Expand Down Expand Up @@ -893,15 +903,35 @@ def _load_pretrained(self, mname, num_labels):



def get_classifier(self, fpath=None):
def get_classifier(self, fpath=None, multilabel=None):
"""
creates a model for classification
Args:
fpath(str): optional path to saved pretrained model. Typically left as None.
multilabel(bool): If None, multilabel status is discovered from data [recommended].
If True, model will be forcibly configured for multilabel task.
If False, model will be forcibly configured for non-multilabel task.
It is recommended to leave this as None.
"""
self.check_trained()
if not self.get_classes():
warnings.warn('no class labels were provided - treating as regression')
return self.get_regression_model()

# process multilabel task
multilabel = self.multilabel if multilabel is None else multilabel
if multilabel is True and self.multilabel is False:
warnings.warn('The multilabel=True argument was supplied, but labels do not indicate '+\
'a multilabel problem (labels appear to be mutually-exclusive). Using multilabel=True anyways.')
elif multilabel is False and self.multilabel is True:
warnings.warn('The multilabel=False argument was supplied, but labels inidcate that '+\
'this is a multilabel problem (labels are not mutually-exclusive). Using multilabel=False anyways.')

# setup model
num_labels = len(self.get_classes())
mname = fpath if fpath is not None else self.model_name
model = self._load_pretrained(mname, num_labels)
if self.multilabel:
if multilabel:
loss_fn = keras.losses.BinaryCrossentropy(from_logits=True)
else:
loss_fn = keras.losses.CategoricalCrossentropy(from_logits=True)
Expand Down Expand Up @@ -965,8 +995,7 @@ class Transformer(TransformersPreprocessor):
"""

def __init__(self, model_name, maxlen=128, class_names=[], classes=[],
batch_size=None, multilabel=False,
use_with_learner=True):
batch_size=None, use_with_learner=True):
"""
Args:
model_name (str): name of Hugging Face pretrained model
Expand All @@ -989,10 +1018,12 @@ def __init__(self, model_name, maxlen=128, class_names=[], classes=[],
return a ktrain TransformerDataset object for use with
ktrain.get_learner.
batch_size (int): batch_size - only required if use_with_learner=False
multilabel (int): if True, classifier will be configured for
multilabel classification.
"""
multilabel = None # force discovery of multilabel task from data in preprocess_train->set_multilabel
class_names = self.migrate_classes(class_names, classes)
if not use_with_learner and batch_size is None:
raise ValueError('batch_size is required when use_with_learner=False')
Expand Down
4 changes: 4 additions & 0 deletions ktrain/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ def get_default_optimizer(lr=0.001, wd=DEFAULT_WD):
DEFAULT_TRANSFORMER_LAYERS = [-2] # second-to-last hidden state
DEFAULT_TRANSFORMER_MAXLEN = 512
DEFAULT_TRANSFORMER_NUM_SPECIAL = 2
MODEL_BASENAME = 'tf_model'
MODEL_NAME = MODEL_BASENAME+'.h5'
PREPROC_NAME = MODEL_BASENAME+'.preproc'



#------------------------------------------------------------------------------
Expand Down
2 changes: 1 addition & 1 deletion ktrain/version.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
__all__ = ['__version__']
__version__ = '0.15.2'
__version__ = '0.15.3'

0 comments on commit 7671f36

Please sign in to comment.