Skip to content

Commit ec3e459

Browse files
authored
Version 0.3.0 - LearningRateFinder #5
LearningRateFinder
2 parents d32b53d + 74cfd7a commit ec3e459

8 files changed

Lines changed: 211 additions & 7 deletions

File tree

README.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ Tavolo
2828
| tavolo gathers implementations of these useful ideas from the community (by contribution, from `Kaggle`_ etc.)
2929
and makes them accessible in a single PyPI hosted package that compliments the `tf.keras`_ module.
3030
|
31-
| *Notice: tavolo is developed for TensorFlow 2.0 (right now on alpha), most modules will work with earlier versions but some won't (like LayerNorm)*
31+
| *Notice: tavolo is developed for TensorFlow 2.0 (right now on beta), most modules will work with earlier versions but some won't (like LayerNorm)*
3232
3333
Documentation
3434
-------------

docs/source/index.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ Welcome to tavolo's documentation!
1111
1212
.. warning::
1313

14-
tavolo is developed for TensorFlow 2.0 (right now on alpha), most modules will work with earlier versions but some won't (like LayerNorm)
14+
tavolo is developed for TensorFlow 2.0 (right now on beta), most modules will work with earlier versions but some won't (like LayerNorm)
1515

1616
Showcase
1717
--------
@@ -43,7 +43,7 @@ Showcase
4343

4444

4545
.. toctree::
46-
:maxdepth: 1
46+
:maxdepth: 2
4747
:caption: Modules
4848

4949
embeddings

docs/source/learning.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,15 @@ Modules for altering the learning process
1616
++++++++++++++++++++++++++++++
1717

1818
.. automodule:: learning.CyclicLearningRateCallback
19+
20+
-------
21+
22+
.. _`learning_rate_finder`:
23+
24+
``LearningRateFinder``
25+
++++++++++++++++++++++
26+
27+
.. automodule:: learning.LearningRateFinder
28+
29+
.. automethod:: learning.LearningRateFinder::scan
30+

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from setuptools import setup
22

3-
VERSION = '0.2.1'
3+
VERSION = '0.3.0'
44

55
setup(name='tavolo',
66
version=VERSION,

tavolo/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
__name__ = 'tavolo'
2-
__version__ = '0.2.1'
2+
__version__ = '0.3.0'
33

44
from . import embeddings
55
from . import normalization

tavolo/learning.py

Lines changed: 142 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
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

36
import tensorflow as tf
47
import 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

tests/learning/cyclic_learning_rate_callback_test.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
def test_logic():
1010
""" Test logic on known input """
1111

12+
# -------- TRIANGULAR --------
13+
1214
# Input
1315
input_2d = np.random.normal(size=(1000, 20))
1416
labels = np.random.randint(low=0, high=2, size=1000)
@@ -28,3 +30,27 @@ def test_logic():
2830
model.fit(input_2d, labels, batch_size=10, epochs=5, callbacks=[clr], verbose=0)
2931

3032
assert all(math.isclose(a, b, rel_tol=0.001) for a, b in zip(clr.history['lr'], expected_lr_values))
33+
34+
# -------- TRIANGULAR2 --------
35+
36+
# Create model
37+
model = tf.keras.Sequential([tf.keras.layers.Input(shape=(20,)),
38+
tf.keras.layers.Dense(10, activation=tf.nn.relu),
39+
tf.keras.layers.Dense(1, activation=tf.nn.sigmoid)])
40+
model.compile(optimizer=tf.keras.optimizers.SGD(), loss='binary_crossentropy')
41+
42+
clr = CyclicLearningRateCallback(scale_scheme='triangular2')
43+
44+
model.fit(input_2d, labels, batch_size=10, epochs=5, callbacks=[clr], verbose=0)
45+
46+
# -------- EXPONENT RANGE --------
47+
48+
# Create model
49+
model = tf.keras.Sequential([tf.keras.layers.Input(shape=(20,)),
50+
tf.keras.layers.Dense(10, activation=tf.nn.relu),
51+
tf.keras.layers.Dense(1, activation=tf.nn.sigmoid)])
52+
model.compile(optimizer=tf.keras.optimizers.SGD(), loss='binary_crossentropy')
53+
54+
clr = CyclicLearningRateCallback(scale_scheme='exp_range')
55+
56+
model.fit(input_2d, labels, batch_size=10, epochs=5, callbacks=[clr], verbose=0)
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import tensorflow as tf
2+
import numpy as np
3+
4+
from tavolo.learning import LearningRateFinder
5+
6+
7+
def test_logic():
8+
""" Test logic on known input """
9+
10+
# Input
11+
input_2d = np.random.normal(size=(10000, 20))
12+
labels = np.random.randint(low=0, high=2, size=10000)
13+
14+
# Create model
15+
model = tf.keras.Sequential([tf.keras.layers.Input(shape=(20,)),
16+
tf.keras.layers.Dense(10, activation=tf.nn.relu),
17+
tf.keras.layers.Dense(1, activation=tf.nn.sigmoid)])
18+
model.compile(optimizer=tf.keras.optimizers.SGD(), loss='binary_crossentropy')
19+
20+
# Learning rate range test
21+
lr_finder = LearningRateFinder(model=model)
22+
23+
# Run model
24+
lrs, losses = lr_finder.scan(input_2d, labels, batch_size=50)
25+
26+
assert len(lrs) == len(losses)

0 commit comments

Comments
 (0)