Repository: keras-team/keras

Issue:
ReduceLROnPlateau Callback behaves unexpectedly when cooldown == 0
In this case:

``` python
reduce_lr = ReduceLROnPlateau(monitor='val_loss', patience=4, cooldown=0)
model.fit(X_train, Y_train, callbacks=[reduce_lr])
```

After 4 epochs of `val_loss` not improving, we reduce the learning rate as expected. However, since `self.cooldown == 0` we don't take this [branch](https://github.com/fchollet/keras/blob/master/keras/callbacks.py#L620) and therefore never reset `self.wait`. Which causes:
1. Learning rate reduced after 4 epochs of no improvement.
2. Learning rate reduced again the next epoch since we don't reset `self.wait`.
3. Every single epoch we are reducing the learning rate after the first time we reduce it.
4. Once `val_loss` improves, we reset `self.wait` and now wait for `self.patience` epochs before reducing LR again.

In the case where `self.cooldown > 0` this code should work as expected.

Gist: https://gist.github.com/mjdietzx/3aaf9c58486f6e6ff310a0d960d8bb4e
@basveeling 



Discussion:
Thanks! I'll take a look this weekend and send a new PR.

On Fri, Oct 7, 2016, 17:20 Michael Dietz notifications@github.com wrote:

> In this case:
> 
> reduce_lr = ReduceLROnPlateau(monitor='val_loss', patience=4, cooldown=0)
> model.fit(X_train, Y_train, callbacks=[reduce_lr])
> 
> After 4 epochs of val_loss not improving, we reduce the learning rate as
> expected. However, since self.cooldown == 0 we don't take this branch
> https://github.com/fchollet/keras/blob/master/keras/callbacks.py#L620
> and therefore never reset self.wait. Which causes:
> 1. Learning rate reduced after 4 epochs of no improvement.
> 2. Learning rate reduced again the next epoch since we don't reset
>    self.wait.
> 3. Every single epoch we are reducing the learning rate after the
>    first time we reduce it.
> 4. Once val_loss improves, we reset self.wait and now wait for
>    self.patience epochs before reducing LR again.
> 
> In the case where self.cooldown > 0 this code should work as expected.
> 
> Gist: https://gist.github.com/mjdietzx/3aaf9c58486f6e6ff310a0d960d8bb4e
> @basveeling https://github.com/basveeling
> 
> —
> You are receiving this because you were mentioned.
> Reply to this email directly, view it on GitHub
> https://github.com/fchollet/keras/issues/3991, or mute the thread
> https://github.com/notifications/unsubscribe-auth/AAgxj2HOEFUjmqcsP9SydEM-8Vr7Aggnks5qxmNBgaJpZM4KRJf0
> .

Looks like it'll be an easy fix but if you're tight on time/need any help just let me know.


Resolving patch:
diff --git a/keras/callbacks.py b/keras/callbacks.py
--- a/keras/callbacks.py
+++ b/keras/callbacks.py
@@ -617,14 +617,14 @@ def on_epoch_end(self, epoch, logs={}):
             warnings.warn('Learning Rate Plateau Reducing requires %s available!' %
                           self.monitor, RuntimeWarning)
         else:
-            if self.cooldown_counter > 0:
+            if self.in_cooldown():
                 self.cooldown_counter -= 1
                 self.wait = 0
 
             if self.monitor_op(current, self.best):
                 self.best = current
                 self.wait = 0
-            elif self.cooldown_counter <= 0:
+            elif not self.in_cooldown():
                 if self.wait >= self.patience:
                     old_lr = float(K.get_value(self.model.optimizer.lr))
                     if old_lr > self.min_lr + self.lr_epsilon:
@@ -634,8 +634,12 @@ def on_epoch_end(self, epoch, logs={}):
                         if self.verbose > 0:
                             print('\nEpoch %05d: reducing learning rate to %s.' % (epoch, new_lr))
                         self.cooldown_counter = self.cooldown
+                        self.wait = 0
                 self.wait += 1
 
+    def in_cooldown(self):
+        return self.cooldown_counter > 0
+
 
 class CSVLogger(Callback):
     '''Callback that streams epoch results to a csv file.


Test patch: