diff --git a/neuroner/__main__.py b/neuroner/__main__.py index 9f2c6d5b..b211195e 100644 --- a/neuroner/__main__.py +++ b/neuroner/__main__.py @@ -45,6 +45,7 @@ def parse_arguments(arguments=None): parser.add_argument('--number_of_gpus', required=False, default=None, help='') parser.add_argument('--optimizer', required=False, default=None, help='') parser.add_argument('--output_folder', required=False, default=None, help='') + parser.add_argument('--output_scores', required=False, default=None, help='') parser.add_argument('--patience', required=False, default=None, help='') parser.add_argument('--plot_format', required=False, default=None, help='') parser.add_argument('--pretrained_model_folder', required=False, default=None, help='') diff --git a/neuroner/neuromodel.py b/neuroner/neuromodel.py index 99fa67e5..cf300c44 100644 --- a/neuroner/neuromodel.py +++ b/neuroner/neuromodel.py @@ -37,7 +37,7 @@ def fetch_model(name): """ Fetch a pre-trained model and copy to a local "trained_models" folder - If name is provided, fetch from the package folder. + If name is provided, fetch from the package folder. Args: name (str): Name of a model folder. @@ -49,11 +49,12 @@ def fetch_model(name): # model.ckpt.index # model.ckpt.meta # parameters.ini - _fetch(name,content_type="trained_models") + _fetch(name, content_type="trained_models") + def fetch_data(name): """ - Fetch a dataset. If name is provided, fetch from the package folder. If url + Fetch a dataset. If name is provided, fetch from the package folder. If url is provided, fetch from a remote location. Args: @@ -61,9 +62,10 @@ def fetch_data(name): url (str): URL of a model folder. """ # get content from package and write to local dir - _fetch(name,content_type="data") + _fetch(name, content_type="data") + -def _fetch(name,content_type=None): +def _fetch(name, content_type=None): """ Load data or models from the package folder. @@ -78,10 +80,10 @@ def _fetch(name,content_type=None): resource_path = '/'.join((content_type, name)) # get dirs - root_dir = os.path.dirname(pkg_resources.resource_filename(package_name, + root_dir = os.path.dirname(pkg_resources.resource_filename(package_name, '__init__.py')) src_dir = os.path.join(root_dir, resource_path) - dest_dir = os.path.join('.',content_type,name) + dest_dir = os.path.join('.', content_type, name) if pkg_resources.resource_isdir(package_name, resource_path): @@ -97,6 +99,7 @@ def _fetch(name,content_type=None): msg = "{} not found in {} package.".format(name,package_name) print(msg) + def _get_default_param(): """ Get the default parameters. @@ -146,6 +149,7 @@ def _get_default_param(): return param + def _get_config_param(param_filepath=None): """ Get the parameters from the config file. @@ -157,11 +161,13 @@ def _get_config_param(param_filepath=None): param_file_txt = configparser.ConfigParser() param_file_txt.read(param_filepath, encoding="UTF-8") nested_parameters = utils.convert_configparser_to_dictionary(param_file_txt) - for k,v in nested_parameters.items(): + + for k, v in nested_parameters.items(): param.update(v) return param, param_file_txt + def _clean_param_dtypes(param): """ Ensure data types are correct in the parameter dictionary. @@ -171,7 +177,7 @@ def _clean_param_dtypes(param): """ # Set the data type - for k,v in param.items(): + for k, v in param.items(): v = str(v) # If the value is a list delimited with a comma, choose one element at random. # NOTE: review this behaviour. @@ -180,28 +186,34 @@ def _clean_param_dtypes(param): param[k] = v # Ensure that each parameter is cast to the correct type - if k in ['character_embedding_dimension','character_lstm_hidden_state_dimension', - 'token_embedding_dimension','token_lstm_hidden_state_dimension','patience', - 'maximum_number_of_epochs','maximum_training_time','number_of_cpu_threads','number_of_gpus']: + if k in ['character_embedding_dimension', + 'character_lstm_hidden_state_dimension', 'token_embedding_dimension', + 'token_lstm_hidden_state_dimension', 'patience', + 'maximum_number_of_epochs', 'maximum_training_time', + 'number_of_cpu_threads', 'number_of_gpus']: param[k] = int(v) elif k in ['dropout_rate', 'learning_rate', 'gradient_clipping_value']: param[k] = float(v) - elif k in ['remap_unknown_tokens_to_unk', 'use_character_lstm', 'use_crf', - 'train_model', 'use_pretrained_model', 'debug', 'verbose', - 'reload_character_embeddings', 'reload_character_lstm', 'reload_token_embeddings', - 'reload_token_lstm', 'reload_feedforward', 'reload_crf', - 'check_for_lowercase', 'check_for_digits_replaced_with_zeros', - 'freeze_token_embeddings', 'load_only_pretrained_token_embeddings']: + elif k in ['remap_unknown_tokens_to_unk', 'use_character_lstm', + 'use_crf', 'train_model', 'use_pretrained_model', 'debug', 'verbose', + 'reload_character_embeddings', 'reload_character_lstm', + 'reload_token_embeddings', 'reload_token_lstm', + 'reload_feedforward', 'reload_crf', 'check_for_lowercase', + 'check_for_digits_replaced_with_zeros', 'output_scores', + 'freeze_token_embeddings', 'load_only_pretrained_token_embeddings']: param[k] = distutils.util.strtobool(v) return param + def load_parameters(**kwargs): ''' - Load parameters from the ini file if specified, take into account any command - line argument, and ensure that each parameter is cast to the correct type. + Load parameters from the ini file if specified, take into account any + command line argument, and ensure that each parameter is cast to the + correct type. - Command line arguments take precedence over parameters specified in the parameter file. + Command line arguments take precedence over parameters specified in the + parameter file. ''' param = {} param_default = _get_default_param() @@ -228,6 +240,10 @@ def load_parameters(**kwargs): if k not in param: param[k] = param_default[k] + # clean the data types + param = _clean_param_dtypes(param) + print(param) + # if loading a pretrained model, set to pretrain hyperparameters if param['use_pretrained_model']: @@ -236,13 +252,14 @@ def load_parameters(**kwargs): if os.path.isfile(pretrain_path): pretrain_param, _ = _get_config_param(pretrain_path) + pretrain_param = _clean_param_dtypes(pretrain_param) pretrain_list = ['use_character_lstm', 'character_embedding_dimension', 'character_lstm_hidden_state_dimension', 'token_embedding_dimension', 'token_lstm_hidden_state_dimension', 'use_crf'] for name in pretrain_list: - if str(param[name]) != str(pretrain_param[name]): + if param[name] != pretrain_param[name]: msg = """WARNING: parameter '{0}' was overwritten from '{1}' to '{2}' for consistency with the pretrained model""".format(name, param[name], pretrain_param[name]) @@ -252,19 +269,10 @@ def load_parameters(**kwargs): msg = """Warning: pretraining parameter file not found.""" print(msg) - # clean the data types - param = _clean_param_dtypes(param) - - # # if running tests - # if param['experiment_name'] == 'test': - # param_file_txt = configparser.ConfigParser() - # test_param_path = os.path.join('test', 'test-parameters-training.ini') - # param_file_txt.read(test_param_path) - # update param_file_txt to reflect the overriding param_to_section = utils.get_parameter_to_section_of_configparser(param_file_txt) for k, v in param.items(): - try: + try: param_file_txt.set(param_to_section[k], k, str(v)) except: pass @@ -274,6 +282,7 @@ def load_parameters(**kwargs): return param, param_file_txt + def get_valid_dataset_filepaths(parameters): """ Get valid filepaths for the datasets. @@ -346,6 +355,7 @@ def get_valid_dataset_filepaths(parameters): return dataset_filepaths, dataset_brat_folders + def check_param_compatibility(parameters, dataset_filepaths): """ Check parameters are compatible. @@ -380,6 +390,13 @@ def check_param_compatibility(parameters, dataset_filepaths): if parameters['gradient_clipping_value'] < 0: parameters['gradient_clipping_value'] = abs(parameters['gradient_clipping_value']) + if parameters['output_scores'] and parameters['use_crf']: + warn_msg = """Warning when use_crf is True, scores are decoded + using the crf. As a result, the scores cannot be directly interpreted + in terms of class prediction. + """ + warnings.warn(warn_msg) + class NeuroNER(object): """ @@ -407,6 +424,7 @@ class NeuroNER(object): number_of_gpus (type): description optimizer (type): description output_folder (type): description + output_scores (bool): description patience (type): description plot_format (type): description reload_character_embeddings (type): description @@ -435,6 +453,7 @@ def __init__(self, **kwargs): # Set parameters self.parameters, self.conf_parameters = load_parameters(**kwargs) + self.dataset_filepaths, self.dataset_brat_folders = self._get_valid_dataset_filepaths(self.parameters) self._check_param_compatibility(self.parameters, self.dataset_filepaths) @@ -445,11 +464,11 @@ def __init__(self, **kwargs): # Launch session. Automatically choose a device # if the specified one doesn't exist session_conf = tf.ConfigProto( - intra_op_parallelism_threads=self.parameters['number_of_cpu_threads'], - inter_op_parallelism_threads=self.parameters['number_of_cpu_threads'], - device_count={'CPU': 1, 'GPU': self.parameters['number_of_gpus']}, - allow_soft_placement=True, - log_device_placement=False) + intra_op_parallelism_threads=self.parameters['number_of_cpu_threads'], + inter_op_parallelism_threads=self.parameters['number_of_cpu_threads'], + device_count={'CPU': 1, 'GPU': self.parameters['number_of_gpus']}, + allow_soft_placement=True, + log_device_placement=False) self.sess = tf.Session(config=session_conf) with self.sess.as_default(): @@ -459,10 +478,10 @@ def __init__(self, **kwargs): self.sess.run(tf.global_variables_initializer()) if self.parameters['use_pretrained_model']: - self.transition_params_trained = self.model.restore_from_pretrained_model(self.parameters, + self.transition_params_trained = self.model.restore_from_pretrained_model(self.parameters, self.modeldata, self.sess, token_to_vector=token_to_vector) else: - self.model.load_pretrained_token_embeddings(self.sess, self.modeldata, + self.model.load_pretrained_token_embeddings(self.sess, self.modeldata, self.parameters, token_to_vector) self.transition_params_trained = np.random.rand(len(self.modeldata.unique_labels)+2, len(self.modeldata.unique_labels)+2) @@ -483,7 +502,7 @@ def _create_stats_graph_folder(self, parameters): stats_graph_folder = os.path.join(parameters['output_folder'], model_name) utils.create_folder_if_not_exists(stats_graph_folder) return stats_graph_folder, experiment_timestamp - + def _get_valid_dataset_filepaths(self, parameters, dataset_types=['train', 'valid', 'test', 'deploy']): """ Get paths for the datasets. @@ -502,21 +521,21 @@ def _get_valid_dataset_filepaths(self, parameters, dataset_types=['train', 'vali dataset_type) dataset_compatible_with_brat_filepath = os.path.join(parameters['dataset_text_folder'], '{0}_compatible_with_brat.txt'.format(dataset_type)) - + # Conll file exists if os.path.isfile(dataset_filepaths[dataset_type]) \ and os.path.getsize(dataset_filepaths[dataset_type]) > 0: # Brat text files exist if os.path.exists(dataset_brat_folders[dataset_type]) and \ len(glob.glob(os.path.join(dataset_brat_folders[dataset_type], '*.txt'))) > 0: - + # Check compatibility between conll and brat files brat_to_conll.check_brat_annotation_and_text_compatibility(dataset_brat_folders[dataset_type]) if os.path.exists(dataset_compatible_with_brat_filepath): dataset_filepaths[dataset_type] = dataset_compatible_with_brat_filepath conll_to_brat.check_compatibility_between_conll_and_brat_text(dataset_filepaths[dataset_type], dataset_brat_folders[dataset_type]) - + # Brat text files do not exist else: # Populate brat text and annotation files based on conll file @@ -524,7 +543,7 @@ def _get_valid_dataset_filepaths(self, parameters, dataset_types=['train', 'vali dataset_compatible_with_brat_filepath, dataset_brat_folders[dataset_type], dataset_brat_folders[dataset_type]) dataset_filepaths[dataset_type] = dataset_compatible_with_brat_filepath - + # Conll file does not exist else: # Brat text files exist @@ -540,23 +559,23 @@ def _get_valid_dataset_filepaths(self, parameters, dataset_types=['train', 'vali brat_to_conll.brat_to_conll(dataset_brat_folders[dataset_type], dataset_filepath_for_tokenizer, parameters['tokenizer'], parameters['spacylanguage']) dataset_filepaths[dataset_type] = dataset_filepath_for_tokenizer - + # Brat text files do not exist else: del dataset_filepaths[dataset_type] del dataset_brat_folders[dataset_type] continue - + if parameters['tagging_format'] == 'bioes': # Generate conll file with BIOES format - bioes_filepath = os.path.join(parameters['dataset_text_folder'], + bioes_filepath = os.path.join(parameters['dataset_text_folder'], '{0}_bioes.txt'.format(utils.get_basename_without_extension(dataset_filepaths[dataset_type]))) - utils_nlp.convert_conll_from_bio_to_bioes(dataset_filepaths[dataset_type], + utils_nlp.convert_conll_from_bio_to_bioes(dataset_filepaths[dataset_type], bioes_filepath) dataset_filepaths[dataset_type] = bioes_filepath - + return dataset_filepaths, dataset_brat_folders - + def _check_param_compatibility(self, parameters, dataset_filepaths): """ Check parameters are compatible. @@ -566,7 +585,7 @@ def _check_param_compatibility(self, parameters, dataset_filepaths): dataset_filepaths (type): description. """ check_param_compatibility(parameters, dataset_filepaths) - + def fit(self): """ Fit the model. @@ -598,7 +617,7 @@ def fit(self): with open(os.path.join(model_folder, 'parameters.ini'), 'w') as parameters_file: conf_parameters.write(parameters_file) pickle.dump(modeldata, open(os.path.join(model_folder, 'dataset.pickle'), 'wb')) - + tensorboard_log_folder = os.path.join(stats_graph_folder, 'tensorboard_logs') utils.create_folder_if_not_exists(tensorboard_log_folder) tensorboard_log_folders = {} @@ -606,7 +625,7 @@ def fit(self): tensorboard_log_folders[dataset_type] = os.path.join(stats_graph_folder, 'tensorboard_logs', dataset_type) utils.create_folder_if_not_exists(tensorboard_log_folders[dataset_type]) - + # Instantiate the writers for TensorBoard writers = {} for dataset_type in dataset_filepaths.keys(): @@ -614,7 +633,7 @@ def fit(self): graph=sess.graph) # embedding_writer has to write in model_folder, otherwise TensorBoard won't be able to view embeddings - embedding_writer = tf.summary.FileWriter(model_folder) + embedding_writer = tf.summary.FileWriter(model_folder) embeddings_projector_config = projector.ProjectorConfig() tensorboard_token_embeddings = embeddings_projector_config.embeddings.add() @@ -646,7 +665,7 @@ def fit(self): # Start training + evaluation loop. Each iteration corresponds to 1 epoch. # number of epochs with no improvement on the validation test in terms of F1-score - bad_counter = 0 + bad_counter = 0 previous_best_valid_f1_score = 0 epoch_number = -1 try: @@ -666,19 +685,19 @@ def fit(self): sequence_number, model, parameters) step += 1 if step % 10 == 0: - print('Training {0:.2f}% done'.format(step/len(sequence_numbers)*100), + print('Training {0:.2f}% done'.format(step/len(sequence_numbers)*100), end='\r', flush=True) epoch_elapsed_training_time = time.time() - epoch_start_time - print('Training completed in {0:.2f} seconds'.format(epoch_elapsed_training_time), + print('Training completed in {0:.2f} seconds'.format(epoch_elapsed_training_time), flush=True) - y_pred, y_true, output_filepaths = train.predict_labels(sess, model, - transition_params_trained, parameters, modeldata, epoch_number, + y_pred, y_true, output_filepaths = train.predict_labels(sess, model, + transition_params_trained, parameters, modeldata, epoch_number, stats_graph_folder, dataset_filepaths) # Evaluate model: save and plot results - evaluate.evaluate_model(results, modeldata, y_pred, y_true, stats_graph_folder, + evaluate.evaluate_model(results, modeldata, y_pred, y_true, stats_graph_folder, epoch_number, epoch_start_time, output_filepaths, parameters) if parameters['use_pretrained_model'] and not parameters['train_model']: @@ -700,7 +719,7 @@ def fit(self): if valid_f1_score > previous_best_valid_f1_score: bad_counter = 0 previous_best_valid_f1_score = valid_f1_score - conll_to_brat.output_brat(output_filepaths, dataset_brat_folders, + conll_to_brat.output_brat(output_filepaths, dataset_brat_folders, stats_graph_folder, overwrite=True) self.transition_params_trained = transition_params_trained else: @@ -712,7 +731,8 @@ def fit(self): results['execution_details']['early_stop'] = True break - if epoch_number >= parameters['maximum_number_of_epochs']: break + if epoch_number >= parameters['maximum_number_of_epochs']: + break except KeyboardInterrupt: results['execution_details']['keyboard_interrupt'] = True @@ -734,11 +754,11 @@ def predict(self, text): text (str): Description. """ self.prediction_count += 1 - + if self.prediction_count == 1: self.parameters['dataset_text_folder'] = os.path.join('.', 'data', 'temp') self.stats_graph_folder, _ = self._create_stats_graph_folder(self.parameters) - + # Update the deploy folder, file, and modeldata dataset_type = 'deploy' @@ -761,23 +781,25 @@ def predict(self, text): f.write(text) # Update deploy filepaths - dataset_filepaths, dataset_brat_folders = self._get_valid_dataset_filepaths(self.parameters, + dataset_filepaths, dataset_brat_folders = self._get_valid_dataset_filepaths(self.parameters, dataset_types=[dataset_type]) self.dataset_filepaths.update(dataset_filepaths) self.dataset_brat_folders.update(dataset_brat_folders) # Update the dataset for the new deploy set self.modeldata.update_dataset(self.dataset_filepaths, [dataset_type]) - + # Predict labels and output brat output_filepaths = {} - prediction_output = train.prediction_step(self.sess, self.modeldata, - dataset_type, self.model, self.transition_params_trained, self.stats_graph_folder, - self.prediction_count, self.parameters, self.dataset_filepaths) + prediction_output = train.prediction_step(self.sess, self.modeldata, + dataset_type, self.model, self.transition_params_trained, + self.stats_graph_folder, self.prediction_count, self.parameters, + self.dataset_filepaths) + _, _, output_filepaths[dataset_type] = prediction_output conll_to_brat.output_brat(output_filepaths, self.dataset_brat_folders, self.stats_graph_folder, overwrite=True) - + # Print and output result text_filepath = os.path.join(self.stats_graph_folder, 'brat', 'deploy', os.path.basename(dataset_brat_deploy_filepath)) @@ -787,12 +809,12 @@ def predict(self, text): annotation_filepath, verbose=True) assert(text == text2) return entities - + def get_params(self): return self.parameters - + def close(self): self.__del__() - + def __del__(self): self.sess.close() diff --git a/neuroner/train.py b/neuroner/train.py index 0f45a839..e4c183ee 100644 --- a/neuroner/train.py +++ b/neuroner/train.py @@ -2,11 +2,12 @@ import os import pkg_resources import pickle +import warnings import numpy as np import sklearn.metrics import tensorflow as tf -#from tensorflow.python.tools.inspect_checkpoint import print_tensors_in_checkpoint_file +# from tensorflow.python.tools.inspect_checkpoint import print_tensors_in_checkpoint_file from neuroner.evaluate import remap_labels from neuroner import utils_tf @@ -32,12 +33,12 @@ def train_step(sess, dataset, sequence_number, model, parameters): model.dropout_keep_prob: 1-parameters['dropout_rate']} _, _, loss, accuracy, transition_params_trained = sess.run( - [model.train_op, model.global_step, model.loss, model.accuracy, + [model.train_op, model.global_step, model.loss, model.accuracy, model.transition_parameters],feed_dict) return transition_params_trained -def prediction_step(sess, dataset, dataset_type, model, transition_params_trained, +def prediction_step(sess, dataset, dataset_type, model, transition_params_trained, stats_graph_folder, epoch_number, parameters, dataset_filepaths): """ Predict. @@ -62,10 +63,12 @@ def prediction_step(sess, dataset, dataset_type, model, transition_params_traine model.input_label_indices_vector: dataset.label_vector_indices[dataset_type][i], model.dropout_keep_prob: 1. } - unary_scores, predictions = sess.run([model.unary_scores, model.predictions], feed_dict) + + unary_scores, predictions = sess.run([model.unary_scores, + model.predictions], feed_dict) if parameters['use_crf']: - predictions, _ = tf.contrib.crf.viterbi_decode(unary_scores, + predictions, _ = tf.contrib.crf.viterbi_decode(unary_scores, transition_params_trained) predictions = predictions[1:-1] else: @@ -75,14 +78,16 @@ def prediction_step(sess, dataset, dataset_type, model, transition_params_traine output_string = '' prediction_labels = [dataset.index_to_label[prediction] for prediction in predictions] + unary_score_list = unary_scores.tolist()[1:-1] + gold_labels = dataset.labels[dataset_type][i] if parameters['tagging_format'] == 'bioes': prediction_labels = utils_nlp.bioes_to_bio(prediction_labels) gold_labels = utils_nlp.bioes_to_bio(gold_labels) - for prediction, token, gold_label in zip(prediction_labels, - dataset.tokens[dataset_type][i], gold_labels): + for prediction, token, gold_label, scores in zip(prediction_labels, + dataset.tokens[dataset_type][i], gold_labels, unary_score_list): while True: line = original_conll_file.readline() @@ -99,11 +104,16 @@ def prediction_step(sess, dataset, dataset_type, model, transition_params_traine gold_label_original = split_line[-1] - assert(token == token_original and gold_label == gold_label_original) + assert(token == token_original and gold_label == gold_label_original) break split_line.append(prediction) + if parameters['output_scores']: + # space separated scores + scores = ' '.join([str(i) for i in scores]) + split_line.append('{}'.format(scores)) output_string += ' '.join(split_line) + '\n' + output_file.write(output_string+'\n') all_predictions.extend(predictions) @@ -119,12 +129,12 @@ def prediction_step(sess, dataset, dataset_type, model, transition_params_traine # run perl evaluation script in python package # conll_evaluation_script = os.path.join('.', 'conlleval') package_name = 'neuroner' - root_dir = os.path.dirname(pkg_resources.resource_filename(package_name, + root_dir = os.path.dirname(pkg_resources.resource_filename(package_name, '__init__.py')) conll_evaluation_script = os.path.join(root_dir, 'conlleval') - + conll_output_filepath = '{0}_conll_evaluation.txt'.format(output_filepath) - shell_command = 'perl {0} < {1} > {2}'.format(conll_evaluation_script, + shell_command = 'perl {0} < {1} > {2}'.format(conll_evaluation_script, output_filepath, conll_output_filepath) os.system(shell_command) @@ -133,7 +143,7 @@ def prediction_step(sess, dataset, dataset_type, model, transition_params_traine print(classification_report) else: - new_y_pred, new_y_true, new_label_indices, new_label_names, _, _ = remap_labels(all_predictions, + new_y_pred, new_y_true, new_label_indices, new_label_names, _, _ = remap_labels(all_predictions, all_y_true, dataset, parameters['main_evaluation_mode']) print(sklearn.metrics.classification_report(new_y_true, new_y_pred, @@ -142,7 +152,7 @@ def prediction_step(sess, dataset, dataset_type, model, transition_params_traine return all_predictions, all_y_true, output_filepath -def predict_labels(sess, model, transition_params_trained, parameters, dataset, +def predict_labels(sess, model, transition_params_trained, parameters, dataset, epoch_number, stats_graph_folder, dataset_filepaths): """ Predict labels using trained model @@ -155,8 +165,8 @@ def predict_labels(sess, model, transition_params_trained, parameters, dataset, if dataset_type not in dataset_filepaths.keys(): continue - prediction_output = prediction_step(sess, dataset, dataset_type, model, - transition_params_trained, stats_graph_folder, epoch_number, + prediction_output = prediction_step(sess, dataset, dataset_type, model, + transition_params_trained, stats_graph_folder, epoch_number, parameters, dataset_filepaths) y_pred[dataset_type], y_true[dataset_type], output_filepaths[dataset_type] = prediction_output diff --git a/parameters.ini b/parameters.ini index c5f0ea67..ce39faf7 100644 --- a/parameters.ini +++ b/parameters.ini @@ -66,6 +66,9 @@ number_of_gpus = 0 [advanced] experiment_name = test +# Append the scores to a column in the output file +output_scores = False + # tagging_format should be either 'bioes' or 'bio' tagging_format = bioes diff --git a/setup.py b/setup.py index 25745fbf..b7087279 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ # Versions should comply with PEP440. For a discussion on single-sourcing # the version across setup.py and the project code, see # https://packaging.python.org/en/latest/single_source_version.html - version='1.0.4', + version='1.0.6', description='NeuroNER', long_description=long_description,