1- from typing import Optional , Callable
1+ import tempfile
2+ import math
3+ from typing import Optional , Callable , Tuple , List
4+ from contextlib import suppress
25
36import tensorflow as tf
47import numpy as np
@@ -159,7 +162,7 @@ def _set_scale_scheme(self):
159162 """
160163
161164 # Check for supported scale schemes
162- if self .scale_scheme not in {'triangular' , 'triangular2' , 'exp_rage ' }:
165+ if self .scale_scheme not in {'triangular' , 'triangular2' , 'exp_range ' }:
163166 raise ValueError ('{} is not a supported scale scheme' .format (self .scale_scheme ))
164167
165168 # Set scheme
@@ -172,3 +175,140 @@ def _set_scale_scheme(self):
172175 elif self .scale_scheme == 'exp_range' :
173176 self .scale_fn = lambda x : self .gamma ** x
174177 self .scale_mode = 'iterations'
178+
179+
180+ class LearningRateFinder :
181+ """
182+ Learning rate finding utility for conducting the "LR range test", see article reference for more information
183+
184+ Use the ``scan`` method for finding the loss values for learning rates in the given range
185+
186+ Arguments
187+ ---------
188+
189+ - `model` (``tf.keras.Model``): Model for conduct test for. Must call ``model.compile`` before using this utility
190+
191+ Examples
192+ --------
193+
194+ Run an learning rate range test in the domain ``[0.0001, 1.0]``
195+
196+ .. code-block:: python3
197+
198+ import tensorflow as tf
199+ import tavolo as tvl
200+
201+ train_data = ...
202+ train_labels = ...
203+
204+ # Build model
205+ model = tf.keras.Sequential([tf.keras.layers.Input(shape=(784,)),
206+ tf.keras.layers.Dense(128, activation=tf.nn.relu),
207+ tf.keras.layers.Dense(10, activation=tf.nn.softmax)])
208+
209+ # Must call compile with optimizer before test
210+ model.compile(optimizer=tf.keras.optimizers.SGD(), loss=tf.keras.losses.CategoricalCrossentropy())
211+
212+ # Run learning rate range test
213+ lr_finder = tvl.learning.LearningRateFinder(model=model)
214+
215+ learning_rates, losses = lr_finder.scan(train_data, train_labels, min_lr=0.0001, max_lr=1.0, batch_size=128)
216+
217+ ### Plot the results to choose your learning rate
218+
219+
220+ References
221+ ----------
222+ - `Cyclical Learning Rates for Training Neural Networks`_
223+
224+ .. _`Cyclical Learning Rates for Training Neural Networks`: https://arxiv.org/abs/1506.01186
225+
226+ """
227+
228+ def __init__ (self , model : tf .keras .Model ):
229+ """
230+ :param model: Model for conduct test for. Must call ``model.compile`` before using this utility
231+ """
232+
233+ self ._model = model
234+ self ._lr_range = None
235+ self ._iteration = None
236+ self ._learning_rates = None
237+ self ._losses = None
238+
239+ def scan (self , x , y ,
240+ min_lr : float = 0.0001 ,
241+ max_lr : float = 1.0 ,
242+ batch_size : Optional [int ] = None ,
243+ steps : int = 100 ) -> Tuple [List [float ], List [float ]]:
244+ """
245+ Scans the learning rate range ``[min_lr, max_lr]`` for loss values
246+
247+ :param x: Input data. It could be:
248+ - A Numpy array (or array-like), or a list of arrays (in case the model has multiple inputs)
249+ - A TensorFlow tensor, or a list of tensors (in case the model has multiple inputs)
250+ - A dict mapping input names to the corresponding array/tensors, if the model has named inputs
251+ - A ``tf.data`` dataset or a dataset iterator. Should return a tuple of either ``(inputs, targets)`` or
252+ ``(inputs, targets, sample_weights)``
253+ - A generator or ``keras.utils.Sequence`` returning ``(inputs, targets)`` or ``(inputs, targets, sample weights)``
254+ :param y: Target data. Like the input data `x`,
255+ it could be either Numpy array(s) or TensorFlow tensor(s).
256+ It should be consistent with ``x`` (you cannot have Numpy inputs and
257+ tensor targets, or inversely). If ``x`` is a dataset, dataset
258+ iterator, generator, or ``tf.keras.utils.Sequence`` instance, ``y`` should
259+ not be specified (since targets will be obtained from ``x``).
260+ :param min_lr: Minimum learning rate
261+ :param max_lr: Maximum learning rate
262+ :param batch_size: Number of samples per gradient update.
263+ Do not specify the ``batch_size`` if your data is in the
264+ form of symbolic tensors, dataset, dataset iterators,
265+ generators, or ``tf.keras.utils.Sequence`` instances (since they generate batches)
266+ :param steps: Number of steps to scan between min_lr and max_lr
267+ :return: Learning rates, losses documented
268+
269+ """
270+
271+ # Prerequisites
272+ self ._iteration = 0
273+ self ._learning_rates = list ()
274+ self ._losses = list ()
275+
276+ # Save initial values
277+ initial_checkpoint = tempfile .NamedTemporaryFile ()
278+ self ._model .save_weights (initial_checkpoint .name ) # Save original weights
279+ initial_learning_rate = tf .keras .backend .get_value (self ._model .optimizer .lr ) # Save original lr
280+
281+ # Build range
282+ self ._lr_range = np .linspace (start = min_lr , stop = max_lr , num = steps )
283+
284+ # Scan
285+ tf .keras .backend .set_value (self ._model .optimizer .lr , self ._lr_range [self ._iteration ])
286+ scan_callback = tf .keras .callbacks .LambdaCallback (
287+ on_batch_end = lambda batch , logs : self ._on_batch_end (batch , logs ))
288+
289+ self ._model .fit (x , y , batch_size = batch_size , epochs = 1 , steps_per_epoch = steps ,
290+ verbose = 0 , callbacks = [scan_callback ])
291+
292+ # Restore initial values
293+ self ._model .load_weights (initial_checkpoint .name ) # Restore original weights
294+ tf .keras .backend .set_value (self ._model .optimizer .lr , initial_learning_rate ) # Restore original lr
295+
296+ return self ._learning_rates , self ._losses
297+
298+ def _on_batch_end (self , batch : tf .Tensor , logs : dict ):
299+ # Save learning rate and corresponding loss
300+ self ._learning_rates .append (
301+ tf .keras .backend .get_value (self ._model .optimizer .lr ))
302+
303+ self ._losses .append (
304+ logs ['loss' ])
305+
306+ # Stop on exploding gradient
307+ if math .isnan (logs ['loss' ]):
308+ self ._model .stop_training = True
309+ return
310+
311+ # Apply next learning rate
312+ with suppress (IndexError ):
313+ tf .keras .backend .set_value (self ._model .optimizer .lr , self ._lr_range [self ._iteration + 1 ])
314+ self ._iteration += 1
0 commit comments