I have written a custom callback Dwell that achieves what you wish to accomplish and have used this callback on a large number of image classification tasks.
This callback enables you to select the option to continue or halt training. After training for ask_epoch number of epochs the callback queries the user to either enter H to halt traning or enter an integer N. If an integer is entered training will continue for N more epochs then the user is queried again. It also enables you to set a parameter called dwell. If dwell is set to True, the callback monitors validation loss. If at the end of an epoch the validation loss increases above the validation loss of the previous epoch , the model's weights are reset to the weights of the previous epoch and the learning rate is reduced by next_lr=current_lr * factor where factor is a user specified float value less than 1.0. The idea is that if validation loss increased the model has moved to a location in N space (N is the number of trainable weights) that is less favorable than the position in N space of the previous epoch. So why go there. Instead restore the weights of the previous epoch, then reduce the learning rate. The form of the callback is DWELL(model, factor, dwell, verbose, ask_epoch) where:
model is the name of your compiled model
factor is a float between 0.0 and 1.0. If the validation loss increased the learning rate for the next epoch is determined by next_lr = current_lr * factor. dwell is a boolean. If set to True, validation loss is monitored. If it increased then the model weights are set to those of the previous epoch and the learning rate is reduced. verbose is a boolean. If True the callback prints out the new lr at the end of an epoch where the validation loss increased
ask_epoch is an integer. At the start of training, training will take place for ask_epoch number of epochs. At that point the user is queried to enter H to halt training, or enter an integer N where N specifies how many more epochs to run than be queried again
class DWELL(keras.callbacks.Callback):
def __init__(self,model, factor,dwell, verbose,ask_epoch):
super(DWELL, self).__init__()
self.model=model
self.initial_lr=float(tf.keras.backend.get_value(model.optimizer.lr)) # get the initiallearning rate and save it
self.lowest_vloss=np.inf # set lowest validation loss to infinity initially
self.best_weights=self.model.get_weights() # set best weights to model's initial weights
self.verbose=verbose
self.best_epoch=0
self.ask_epoch=ask_epoch
self.ask=True
self.dwell=dwell
def on_train_begin(self, logs=None): # this runs on the beginning of training
print('Training will proceed until epoch', ask_epoch,' then you will be asked to')
print('enter H to halt training or enter an integer for how many more epochs to run then be asked again')
self.start_time= time.time() # set the time at which training started
def on_epoch_end(self, epoch, logs=None): # method runs on the end of each epoch
if self.ask: # are the conditions right to query the user?
if epoch + 1 ==self.ask_epoch: # is this epoch the one for querying the user?
print('\n Enter H to end training or an integer for the number of additional epochs to run then ask again')
ans=input()
if ans == 'H' or ans =='h' or ans == '0': # quit training for these conditions
print ('you entered ', ans, ' Training halted on epoch ', epoch+1, ' due to user input\n', flush=True)
self.model.stop_training = True # halt training
else: # user wants to continue training
self.ask_epoch += int(ans)
print ('you entered ', ans, ' Training will continue to epoch ', self.ask_epoch, flush=True)
if self.dwell:
lr=float(tf.keras.backend.get_value(self.model.optimizer.lr)) # get the current learning rate
vloss=logs.get('val_loss') # get the validation loss for this epoch
if vloss>self.lowest_vloss:
self.model.set_weights(self.best_weights)
new_lr=lr * factor
tf.keras.backend.set_value(self.model.optimizer.lr, new_lr)
if self.verbose:
print( '\n model weights reset to best weights from epoch ', self.best_epoch+1, ' and reduced lr to ', new_lr, flush=True)
else:
self.lowest_vloss=vloss
self.best_weights=self.model.get_weights()
self.best_epoch= epoch
Here is an example of use
# model is the variable name of your compiled model
ask_epoch=5 # query user at end of epoch 5 to halt or continue training
factor=.5 # if validation loss increased next_lt = current_lr * factor
dwell=True
verbose=True # print out new lr if validation loss increased
dwell=DWELL(model,factor, verbose, ask_epoch)
callbacks=[ DWELL(model,factor,dwell, verbose, ask_epoch)]
In model.fit set callbacks=callbacks. Below is an example of the training output where I purposefully set a large initial learning rate (.02) to cause the DWELL callback
to reduce the learning rate fairly early in the training.
Training will proceed until epoch 5 then you will be asked to
enter H to halt training or enter an integer for how many more epochs to run then be asked again
Epoch 1/40
313/313 [==============================] - 62s 153ms/step - loss: 6.2284 - accuracy: 0.1456 - val_loss: 2.9476 - val_accuracy: 0.2458
Epoch 2/40
313/313 [==============================] - 44s 141ms/step - loss: 2.1466 - accuracy: 0.2686 - val_loss: 8.4516 - val_accuracy: 0.3502
model weights reset to best weights from epoch 1 and reduced lr to 0.009999999776482582
Epoch 3/40
313/313 [==============================] - 46s 146ms/step - loss: 2.0746 - accuracy: 0.2628 - val_loss: 1.7664 - val_accuracy: 0.4072
Epoch 4/40
313/313 [==============================] - 45s 144ms/step - loss: 1.8257 - accuracy: 0.3944 - val_loss: 1.3599 - val_accuracy: 0.6120
Epoch 5/40
313/313 [==============================] - 45s 144ms/step - loss: 1.5230 - accuracy: 0.5530 - val_loss: 1.0913 - val_accuracy: 0.6901
Enter H to end training or an integer for the number of additional epochs to run then ask again
2
you entered 2 Training will continue to epoch 7
Epoch 6/40
313/313 [==============================] - 44s 141ms/step - loss: 1.2793 - accuracy: 0.6745 - val_loss: 0.8224 - val_accuracy: 0.8284
Epoch 7/40
313/313 [==============================] - 45s 142ms/step - loss: 1.0747 - accuracy: 0.7442 - val_loss: 0.7990 - val_accuracy: 0.8271
Enter H to end training or an integer for the number of additional epochs to run then ask again
4
you entered 4 Training will continue to epoch 11
Epoch 8/40
313/313 [==============================] - 45s 144ms/step - loss: 0.9850 - accuracy: 0.7770 - val_loss: 1.5557 - val_accuracy: 0.8688
model weights reset to best weights from epoch 7 and reduced lr to 0.004999999888241291
Epoch 9/40
313/313 [==============================] - 45s 143ms/step - loss: 0.8708 - accuracy: 0.7911 - val_loss: 0.5515 - val_accuracy: 0.8643
Epoch 10/40
313/313 [==============================] - 45s 144ms/step - loss: 0.8346 - accuracy: 0.8047 - val_loss: 0.4961 - val_accuracy: 0.9129
Epoch 11/40
313/313 [==============================] - 45s 144ms/step - loss: 0.7811 - accuracy: 0.8364 - val_loss: 0.5186 - val_accuracy: 0.9526
Enter H to end training or an integer for the number of additional epochs to run then ask again
h
you entered h Training halted on epoch 11 due to user input
I have made a number of test with dwell set to True and dwell set to False on the same data set. Because of inherent tensorflow randomness it is difficult to tell but it appears when dwell=True the model seems to converge a bit faster. I have had no issues to date with problems of convergence to local minimums with dwell=True I achieve as good or better validation loss than with dwell = False