Skip to content

Commit 870f463

Browse files
committed
feat: change Lipschitz cst estimation to Jacobian method
To estimate the Lipschitz constant of a model, we now use the Jacobian method that is more robust than random perturbations (subject to numerical errors). The Jacobian method computes Jacobians for a batch of inputs and gets the max singular values of each Jacobian matrix, corresponding to the local Lipschitz constant. The function then returns the greatest Lipschitz constant estimation in the batch.
1 parent 96539db commit 870f463

File tree

2 files changed

+28
-32
lines changed

2 files changed

+28
-32
lines changed

deel/lip/utils.py

Lines changed: 26 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -13,64 +13,60 @@
1313

1414

1515
def evaluate_lip_const_gen(
16-
model: Model,
17-
generator: Generator[Tuple[np.ndarray, np.ndarray], Any, None],
18-
eps=1e-4,
19-
seed=None,
16+
model: Model, generator: Generator[Tuple[np.ndarray, np.ndarray], Any, None]
2017
):
2118
"""
22-
Evaluate the Lipschitz constant of a model, with the naive method.
23-
Please note that the estimation of the lipschitz constant is done locally around
24-
input sample. This may not correctly estimate the behaviour in the whole domain.
19+
Evaluate the Lipschitz constant of a model, using the Jacobian of the model.
20+
Please note that the estimation of the Lipschitz constant is done locally around
21+
input samples. This may not correctly estimate the behaviour in the whole domain.
2522
The computation might also be inaccurate in high dimensional space.
2623
2724
This is the generator version of evaluate_lip_const.
2825
2926
Args:
3027
model: built keras model used to make predictions
3128
generator: used to select datapoints where to compute the lipschitz constant
32-
eps (float): magnitude of noise to add to input in order to compute the constant
33-
seed (int): seed used when generating the noise ( can be set to None )
3429
3530
Returns:
3631
float: the empirically evaluated lipschitz constant.
3732
3833
"""
3934
x, _ = generator.send(None)
40-
return evaluate_lip_const(model, x, eps, seed=seed)
35+
return evaluate_lip_const(model, x)
4136

4237

43-
def evaluate_lip_const(model: Model, x, eps=1e-4, seed=None):
38+
def evaluate_lip_const(model: Model, x):
4439
"""
45-
Evaluate the Lipschitz constant of a model, with the naive method.
40+
Evaluate the Lipschitz constant of a model, using the Jacobian of the model.
4641
Please note that the estimation of the lipschitz constant is done locally around
47-
input sample. This may not correctly estimate the behaviour in the whole domain.
42+
input samples. This may not correctly estimate the behaviour in the whole domain.
4843
4944
Args:
5045
model: built keras model used to make predictions
5146
x: inputs used to compute the lipschitz constant
52-
eps (float): magnitude of noise to add to input in order to compute the constant
53-
seed (int): seed used when generating the noise ( can be set to None )
5447
5548
Returns:
56-
float: the empirically evaluated lipschitz constant. The computation might also
49+
float: the empirically evaluated Lipschitz constant. The computation might also
5750
be inaccurate in high dimensional space.
5851
5952
"""
60-
y_pred = model.predict(x)
61-
# x = np.repeat(x, 100, 0)
62-
# y_pred = np.repeat(y_pred, 100, 0)
63-
x_var = x + K.random_uniform(
64-
shape=x.shape, minval=eps * 0.25, maxval=eps, seed=seed
65-
)
66-
y_pred_var = model.predict(x_var)
67-
dx = x - x_var
68-
dfx = y_pred - y_pred_var
69-
ndx = K.sqrt(K.sum(K.square(dx), axis=range(1, len(x.shape))))
70-
ndfx = K.sqrt(K.sum(K.square(dfx), axis=range(1, len(y_pred.shape))))
71-
lip_cst = K.max(ndfx / ndx)
72-
print(f"lip cst: {lip_cst:.3f}")
73-
return lip_cst
53+
batch_size = x.shape[0]
54+
x = tf.constant(x, dtype=model.input.dtype)
55+
56+
# Get the jacobians of the model w.r.t. the inputs
57+
with tf.GradientTape() as tape:
58+
tape.watch(x)
59+
y_pred = model(x, training=False)
60+
batch_jacobian = tape.batch_jacobian(y_pred, x)
61+
62+
# Reshape the jacobians (in case of multi-dimensional input/output like in conv)
63+
xdim = tf.reduce_prod(x.shape[1:])
64+
ydim = tf.reduce_prod(y_pred.shape[1:])
65+
batch_jacobian = tf.reshape(batch_jacobian, (batch_size, ydim, xdim))
66+
67+
# Compute the spectral norm of the jacobians and return the maximum
68+
b = tf.norm(batch_jacobian, ord=2, axis=[-2, -1]).numpy()
69+
return tf.reduce_max(b)
7470

7571

7672
def _padding_circular(x, circular_paddings):

tests/test_layers.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ def train_k_lip_model(
224224
linear_generator(batch_size, input_shape, kernel),
225225
steps=10,
226226
)
227-
empirical_lip_const = evaluate_lip_const(model=model, x=x, seed=42)
227+
empirical_lip_const = evaluate_lip_const(model=model, x=x)
228228
# save the model
229229
model_checkpoint_path = os.path.join(logdir, "model.keras")
230230
model.save(model_checkpoint_path, overwrite=True)
@@ -237,7 +237,7 @@ def train_k_lip_model(
237237
linear_generator(batch_size, input_shape, kernel),
238238
steps=10,
239239
)
240-
from_empirical_lip_const = evaluate_lip_const(model=model, x=x, seed=42)
240+
from_empirical_lip_const = evaluate_lip_const(model=model, x=x)
241241
# log metrics
242242
file_writer = tf.summary.create_file_writer(os.path.join(logdir, "metrics"))
243243
file_writer.set_as_default()

0 commit comments

Comments
 (0)