tf-extra 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tf_extra/__init__.py +19 -0
- tf_extra/callbacks/__init__.py +13 -0
- tf_extra/callbacks/custom_callbacks.py +104 -0
- tf_extra/layers/__init__.py +27 -0
- tf_extra/layers/custom_layers.py +361 -0
- tf_extra/losses/__init__.py +19 -0
- tf_extra/losses/custom_losses.py +196 -0
- tf_extra/optimizers/__init__.py +5 -0
- tf_extra/optimizers/multi_optimizer.py +234 -0
- tf_extra-1.0.0.dist-info/LICENSE +21 -0
- tf_extra-1.0.0.dist-info/METADATA +213 -0
- tf_extra-1.0.0.dist-info/RECORD +14 -0
- tf_extra-1.0.0.dist-info/WHEEL +5 -0
- tf_extra-1.0.0.dist-info/top_level.txt +1 -0
tf_extra/__init__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TF-Extra: A powerful extension library for TensorFlow/Keras.
|
|
3
|
+
|
|
4
|
+
Provides MultiOptimizer, custom layers, losses, and callbacks
|
|
5
|
+
that extend TensorFlow's functionality.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
import tf_extra
|
|
9
|
+
opt = tf_extra.optimizers.MultiOptimizer(optimizers_and_layers)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
__version__ = "1.0.0"
|
|
13
|
+
__author__ = "Your Name"
|
|
14
|
+
__license__ = "MIT"
|
|
15
|
+
|
|
16
|
+
from tf_extra.tf_extra import optimizers
|
|
17
|
+
from tf_extra.tf_extra import layers
|
|
18
|
+
from tf_extra.tf_extra import losses
|
|
19
|
+
from tf_extra.tf_extra import callbacks
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""TF-Extra Custom Callbacks Module."""
|
|
2
|
+
|
|
3
|
+
from tf_extra.callbacks.custom_callbacks import (
|
|
4
|
+
CosineAnnealingScheduler,
|
|
5
|
+
GradientAccumulationCallback,
|
|
6
|
+
TimeLimitCallback,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"CosineAnnealingScheduler",
|
|
11
|
+
"GradientAccumulationCallback",
|
|
12
|
+
"TimeLimitCallback",
|
|
13
|
+
]
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Custom Keras Callbacks for TF-Extra.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import tensorflow as tf
|
|
6
|
+
import math
|
|
7
|
+
import time
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CosineAnnealingScheduler(tf.keras.callbacks.Callback):
|
|
11
|
+
"""
|
|
12
|
+
Cosine Annealing Learning Rate Scheduler with Warm Restarts.
|
|
13
|
+
|
|
14
|
+
Implements SGDR: Stochastic Gradient Descent with Warm Restarts.
|
|
15
|
+
The learning rate follows a cosine curve from eta_max to eta_min,
|
|
16
|
+
resetting every T_0 epochs (multiplied by T_mult after each restart).
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
eta_min: Minimum learning rate. Default 1e-7.
|
|
20
|
+
eta_max: Maximum learning rate. Default 1e-3.
|
|
21
|
+
T_0: Number of epochs for the first restart. Default 10.
|
|
22
|
+
T_mult: Factor to increase T_0 after each restart. Default 2.
|
|
23
|
+
verbose: Whether to print learning rate changes. Default False.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, eta_min=1e-7, eta_max=1e-3, T_0=10, T_mult=2, verbose=False, **kwargs):
|
|
27
|
+
super().__init__(**kwargs)
|
|
28
|
+
self.eta_min = eta_min
|
|
29
|
+
self.eta_max = eta_max
|
|
30
|
+
self.T_0 = T_0
|
|
31
|
+
self.T_mult = T_mult
|
|
32
|
+
self.verbose = verbose
|
|
33
|
+
self.T_i = T_0
|
|
34
|
+
self.T_cur = 0
|
|
35
|
+
|
|
36
|
+
def on_epoch_begin(self, epoch, logs=None):
|
|
37
|
+
lr = self.eta_min + 0.5 * (self.eta_max - self.eta_min) * (
|
|
38
|
+
1 + math.cos(math.pi * self.T_cur / self.T_i)
|
|
39
|
+
)
|
|
40
|
+
tf.keras.backend.set_value(self.model.optimizer.learning_rate, lr)
|
|
41
|
+
|
|
42
|
+
if self.verbose:
|
|
43
|
+
print(f"\nEpoch {epoch + 1}: CosineAnnealing LR = {lr:.8f}")
|
|
44
|
+
|
|
45
|
+
self.T_cur += 1
|
|
46
|
+
if self.T_cur >= self.T_i:
|
|
47
|
+
self.T_cur = 0
|
|
48
|
+
self.T_i = int(self.T_i * self.T_mult)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class GradientAccumulationCallback(tf.keras.callbacks.Callback):
|
|
52
|
+
"""
|
|
53
|
+
Simulates larger batch sizes by accumulating gradients over multiple steps.
|
|
54
|
+
|
|
55
|
+
Note: This callback works best with a custom training loop. For
|
|
56
|
+
model.fit(), it adjusts the effective learning rate to simulate
|
|
57
|
+
accumulation.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
accumulation_steps: Number of steps to accumulate before updating.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def __init__(self, accumulation_steps=4, **kwargs):
|
|
64
|
+
super().__init__(**kwargs)
|
|
65
|
+
self.accumulation_steps = accumulation_steps
|
|
66
|
+
|
|
67
|
+
def on_train_begin(self, logs=None):
|
|
68
|
+
# Scale learning rate by accumulation steps
|
|
69
|
+
current_lr = float(tf.keras.backend.get_value(self.model.optimizer.learning_rate))
|
|
70
|
+
self._original_lr = current_lr
|
|
71
|
+
scaled_lr = current_lr * self.accumulation_steps
|
|
72
|
+
tf.keras.backend.set_value(self.model.optimizer.learning_rate, scaled_lr)
|
|
73
|
+
print(f"GradientAccumulation: Scaled LR from {current_lr:.6f} to {scaled_lr:.6f}")
|
|
74
|
+
|
|
75
|
+
def on_train_end(self, logs=None):
|
|
76
|
+
tf.keras.backend.set_value(self.model.optimizer.learning_rate, self._original_lr)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class TimeLimitCallback(tf.keras.callbacks.Callback):
|
|
80
|
+
"""
|
|
81
|
+
Stops training after a specified number of seconds.
|
|
82
|
+
|
|
83
|
+
Useful for Kaggle competitions or cloud instances with time limits.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
max_seconds: Maximum training time in seconds.
|
|
87
|
+
verbose: Whether to print when stopping. Default True.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
def __init__(self, max_seconds, verbose=True, **kwargs):
|
|
91
|
+
super().__init__(**kwargs)
|
|
92
|
+
self.max_seconds = max_seconds
|
|
93
|
+
self.verbose = verbose
|
|
94
|
+
|
|
95
|
+
def on_train_begin(self, logs=None):
|
|
96
|
+
self._start_time = time.time()
|
|
97
|
+
|
|
98
|
+
def on_epoch_end(self, epoch, logs=None):
|
|
99
|
+
elapsed = time.time() - self._start_time
|
|
100
|
+
if elapsed >= self.max_seconds:
|
|
101
|
+
if self.verbose:
|
|
102
|
+
print(f"\nTimeLimitCallback: Stopping training after {elapsed:.1f}s "
|
|
103
|
+
f"(limit: {self.max_seconds}s)")
|
|
104
|
+
self.model.stop_training = True
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""TF-Extra Custom Layers Module."""
|
|
2
|
+
|
|
3
|
+
from tf_extra.layers.custom_layers import (
|
|
4
|
+
SpatialDropout1D,
|
|
5
|
+
ChannelAttention,
|
|
6
|
+
SpatialAttention,
|
|
7
|
+
CBAM,
|
|
8
|
+
SqueezeExcitation,
|
|
9
|
+
MultiHeadSelfAttention,
|
|
10
|
+
PositionalEncoding,
|
|
11
|
+
GeM,
|
|
12
|
+
AdaptiveConcatPool2D,
|
|
13
|
+
Mish,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"SpatialDropout1D",
|
|
18
|
+
"ChannelAttention",
|
|
19
|
+
"SpatialAttention",
|
|
20
|
+
"CBAM",
|
|
21
|
+
"SqueezeExcitation",
|
|
22
|
+
"MultiHeadSelfAttention",
|
|
23
|
+
"PositionalEncoding",
|
|
24
|
+
"GeM",
|
|
25
|
+
"AdaptiveConcatPool2D",
|
|
26
|
+
"Mish",
|
|
27
|
+
]
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Custom Keras Layers for TF-Extra.
|
|
3
|
+
|
|
4
|
+
Provides production-ready layers that are commonly needed but not
|
|
5
|
+
included in the default tf.keras.layers module.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import tensorflow as tf
|
|
9
|
+
import numpy as np
|
|
10
|
+
import math
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SpatialDropout1D(tf.keras.layers.Layer):
|
|
14
|
+
"""
|
|
15
|
+
Spatial 1D Dropout: drops entire 1D feature maps instead of individual elements.
|
|
16
|
+
Useful for NLP embeddings and 1D convolutions.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
rate: Float between 0 and 1. Fraction of feature maps to drop.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, rate=0.5, **kwargs):
|
|
23
|
+
super().__init__(**kwargs)
|
|
24
|
+
self.rate = rate
|
|
25
|
+
self.dropout = tf.keras.layers.Dropout(rate)
|
|
26
|
+
|
|
27
|
+
def call(self, inputs, training=None):
|
|
28
|
+
# inputs shape: (batch, timesteps, channels)
|
|
29
|
+
# We want to drop entire channels, so we reshape
|
|
30
|
+
input_shape = tf.shape(inputs)
|
|
31
|
+
noise_shape = (input_shape[0], 1, input_shape[2])
|
|
32
|
+
return tf.nn.dropout(inputs, rate=self.rate, noise_shape=noise_shape) if training else inputs
|
|
33
|
+
|
|
34
|
+
def get_config(self):
|
|
35
|
+
config = super().get_config()
|
|
36
|
+
config["rate"] = self.rate
|
|
37
|
+
return config
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ChannelAttention(tf.keras.layers.Layer):
|
|
41
|
+
"""
|
|
42
|
+
Channel Attention Module (from CBAM).
|
|
43
|
+
|
|
44
|
+
Applies both average-pooling and max-pooling, passes each through
|
|
45
|
+
a shared MLP, then combines them with a sigmoid gate.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
reduction_ratio: Integer, reduction ratio for the bottleneck. Default 16.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(self, reduction_ratio=16, **kwargs):
|
|
52
|
+
super().__init__(**kwargs)
|
|
53
|
+
self.reduction_ratio = reduction_ratio
|
|
54
|
+
|
|
55
|
+
def build(self, input_shape):
|
|
56
|
+
channels = input_shape[-1]
|
|
57
|
+
reduced = max(1, channels // self.reduction_ratio)
|
|
58
|
+
self.fc1 = tf.keras.layers.Dense(reduced, activation="relu", use_bias=False)
|
|
59
|
+
self.fc2 = tf.keras.layers.Dense(channels, use_bias=False)
|
|
60
|
+
super().build(input_shape)
|
|
61
|
+
|
|
62
|
+
def call(self, inputs):
|
|
63
|
+
avg_pool = tf.reduce_mean(inputs, axis=[1, 2], keepdims=True)
|
|
64
|
+
max_pool = tf.reduce_max(inputs, axis=[1, 2], keepdims=True)
|
|
65
|
+
|
|
66
|
+
avg_out = self.fc2(self.fc1(avg_pool))
|
|
67
|
+
max_out = self.fc2(self.fc1(max_pool))
|
|
68
|
+
|
|
69
|
+
attention = tf.nn.sigmoid(avg_out + max_out)
|
|
70
|
+
return inputs * attention
|
|
71
|
+
|
|
72
|
+
def get_config(self):
|
|
73
|
+
config = super().get_config()
|
|
74
|
+
config["reduction_ratio"] = self.reduction_ratio
|
|
75
|
+
return config
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class SpatialAttention(tf.keras.layers.Layer):
|
|
79
|
+
"""
|
|
80
|
+
Spatial Attention Module (from CBAM).
|
|
81
|
+
|
|
82
|
+
Concatenates average-pooled and max-pooled features along the channel
|
|
83
|
+
axis, then applies a convolution with sigmoid activation.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
kernel_size: Integer, kernel size for the convolution. Default 7.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
def __init__(self, kernel_size=7, **kwargs):
|
|
90
|
+
super().__init__(**kwargs)
|
|
91
|
+
self.kernel_size = kernel_size
|
|
92
|
+
|
|
93
|
+
def build(self, input_shape):
|
|
94
|
+
self.conv = tf.keras.layers.Conv2D(
|
|
95
|
+
1,
|
|
96
|
+
kernel_size=self.kernel_size,
|
|
97
|
+
padding="same",
|
|
98
|
+
activation="sigmoid",
|
|
99
|
+
use_bias=False,
|
|
100
|
+
)
|
|
101
|
+
super().build(input_shape)
|
|
102
|
+
|
|
103
|
+
def call(self, inputs):
|
|
104
|
+
avg_pool = tf.reduce_mean(inputs, axis=-1, keepdims=True)
|
|
105
|
+
max_pool = tf.reduce_max(inputs, axis=-1, keepdims=True)
|
|
106
|
+
concat = tf.concat([avg_pool, max_pool], axis=-1)
|
|
107
|
+
attention = self.conv(concat)
|
|
108
|
+
return inputs * attention
|
|
109
|
+
|
|
110
|
+
def get_config(self):
|
|
111
|
+
config = super().get_config()
|
|
112
|
+
config["kernel_size"] = self.kernel_size
|
|
113
|
+
return config
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class CBAM(tf.keras.layers.Layer):
|
|
117
|
+
"""
|
|
118
|
+
Convolutional Block Attention Module.
|
|
119
|
+
|
|
120
|
+
Sequentially applies Channel Attention then Spatial Attention.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
reduction_ratio: Reduction ratio for channel attention. Default 16.
|
|
124
|
+
kernel_size: Kernel size for spatial attention conv. Default 7.
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
def __init__(self, reduction_ratio=16, kernel_size=7, **kwargs):
|
|
128
|
+
super().__init__(**kwargs)
|
|
129
|
+
self.channel_attention = ChannelAttention(reduction_ratio)
|
|
130
|
+
self.spatial_attention = SpatialAttention(kernel_size)
|
|
131
|
+
self.reduction_ratio = reduction_ratio
|
|
132
|
+
self.kernel_size = kernel_size
|
|
133
|
+
|
|
134
|
+
def call(self, inputs):
|
|
135
|
+
x = self.channel_attention(inputs)
|
|
136
|
+
x = self.spatial_attention(x)
|
|
137
|
+
return x
|
|
138
|
+
|
|
139
|
+
def get_config(self):
|
|
140
|
+
config = super().get_config()
|
|
141
|
+
config["reduction_ratio"] = self.reduction_ratio
|
|
142
|
+
config["kernel_size"] = self.kernel_size
|
|
143
|
+
return config
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class SqueezeExcitation(tf.keras.layers.Layer):
|
|
147
|
+
"""
|
|
148
|
+
Squeeze-and-Excitation block.
|
|
149
|
+
|
|
150
|
+
Recalibrates channel-wise feature responses by explicitly modelling
|
|
151
|
+
interdependencies between channels.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
reduction_ratio: Reduction ratio for the bottleneck. Default 16.
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
def __init__(self, reduction_ratio=16, **kwargs):
|
|
158
|
+
super().__init__(**kwargs)
|
|
159
|
+
self.reduction_ratio = reduction_ratio
|
|
160
|
+
|
|
161
|
+
def build(self, input_shape):
|
|
162
|
+
channels = input_shape[-1]
|
|
163
|
+
reduced = max(1, channels // self.reduction_ratio)
|
|
164
|
+
self.fc1 = tf.keras.layers.Dense(reduced, activation="relu", use_bias=False)
|
|
165
|
+
self.fc2 = tf.keras.layers.Dense(channels, activation="sigmoid", use_bias=False)
|
|
166
|
+
super().build(input_shape)
|
|
167
|
+
|
|
168
|
+
def call(self, inputs):
|
|
169
|
+
se = tf.reduce_mean(inputs, axis=[1, 2], keepdims=True)
|
|
170
|
+
se = self.fc1(se)
|
|
171
|
+
se = self.fc2(se)
|
|
172
|
+
return inputs * se
|
|
173
|
+
|
|
174
|
+
def get_config(self):
|
|
175
|
+
config = super().get_config()
|
|
176
|
+
config["reduction_ratio"] = self.reduction_ratio
|
|
177
|
+
return config
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class MultiHeadSelfAttention(tf.keras.layers.Layer):
|
|
181
|
+
"""
|
|
182
|
+
Multi-Head Self-Attention layer.
|
|
183
|
+
|
|
184
|
+
A clean implementation of scaled dot-product multi-head attention
|
|
185
|
+
that can be used in custom transformer architectures.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
embed_dim: Dimensionality of the embedding.
|
|
189
|
+
num_heads: Number of attention heads.
|
|
190
|
+
dropout_rate: Dropout rate for attention weights. Default 0.0.
|
|
191
|
+
"""
|
|
192
|
+
|
|
193
|
+
def __init__(self, embed_dim, num_heads, dropout_rate=0.0, **kwargs):
|
|
194
|
+
super().__init__(**kwargs)
|
|
195
|
+
self.embed_dim = embed_dim
|
|
196
|
+
self.num_heads = num_heads
|
|
197
|
+
self.dropout_rate = dropout_rate
|
|
198
|
+
|
|
199
|
+
if embed_dim % num_heads != 0:
|
|
200
|
+
raise ValueError(
|
|
201
|
+
f"embed_dim ({embed_dim}) must be divisible by "
|
|
202
|
+
f"num_heads ({num_heads})."
|
|
203
|
+
)
|
|
204
|
+
self.head_dim = embed_dim // num_heads
|
|
205
|
+
|
|
206
|
+
def build(self, input_shape):
|
|
207
|
+
self.wq = tf.keras.layers.Dense(self.embed_dim)
|
|
208
|
+
self.wk = tf.keras.layers.Dense(self.embed_dim)
|
|
209
|
+
self.wv = tf.keras.layers.Dense(self.embed_dim)
|
|
210
|
+
self.dense = tf.keras.layers.Dense(self.embed_dim)
|
|
211
|
+
self.dropout = tf.keras.layers.Dropout(self.dropout_rate)
|
|
212
|
+
super().build(input_shape)
|
|
213
|
+
|
|
214
|
+
def _split_heads(self, x, batch_size):
|
|
215
|
+
x = tf.reshape(x, (batch_size, -1, self.num_heads, self.head_dim))
|
|
216
|
+
return tf.transpose(x, perm=[0, 2, 1, 3])
|
|
217
|
+
|
|
218
|
+
def call(self, inputs, mask=None, training=None):
|
|
219
|
+
batch_size = tf.shape(inputs)[0]
|
|
220
|
+
|
|
221
|
+
q = self._split_heads(self.wq(inputs), batch_size)
|
|
222
|
+
k = self._split_heads(self.wk(inputs), batch_size)
|
|
223
|
+
v = self._split_heads(self.wv(inputs), batch_size)
|
|
224
|
+
|
|
225
|
+
scale = tf.math.sqrt(tf.cast(self.head_dim, tf.float32))
|
|
226
|
+
scores = tf.matmul(q, k, transpose_b=True) / scale
|
|
227
|
+
|
|
228
|
+
if mask is not None:
|
|
229
|
+
scores += mask * -1e9
|
|
230
|
+
|
|
231
|
+
weights = tf.nn.softmax(scores, axis=-1)
|
|
232
|
+
weights = self.dropout(weights, training=training)
|
|
233
|
+
|
|
234
|
+
attention = tf.matmul(weights, v)
|
|
235
|
+
attention = tf.transpose(attention, perm=[0, 2, 1, 3])
|
|
236
|
+
attention = tf.reshape(attention, (batch_size, -1, self.embed_dim))
|
|
237
|
+
|
|
238
|
+
return self.dense(attention)
|
|
239
|
+
|
|
240
|
+
def get_config(self):
|
|
241
|
+
config = super().get_config()
|
|
242
|
+
config["embed_dim"] = self.embed_dim
|
|
243
|
+
config["num_heads"] = self.num_heads
|
|
244
|
+
config["dropout_rate"] = self.dropout_rate
|
|
245
|
+
return config
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class PositionalEncoding(tf.keras.layers.Layer):
|
|
249
|
+
"""
|
|
250
|
+
Sinusoidal Positional Encoding layer.
|
|
251
|
+
|
|
252
|
+
Adds positional information to embeddings using sine and cosine
|
|
253
|
+
functions of different frequencies.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
max_len: Maximum sequence length. Default 5000.
|
|
257
|
+
embed_dim: Embedding dimensionality. If None, inferred from input.
|
|
258
|
+
"""
|
|
259
|
+
|
|
260
|
+
def __init__(self, max_len=5000, embed_dim=None, **kwargs):
|
|
261
|
+
super().__init__(**kwargs)
|
|
262
|
+
self.max_len = max_len
|
|
263
|
+
self.embed_dim = embed_dim
|
|
264
|
+
|
|
265
|
+
def build(self, input_shape):
|
|
266
|
+
d_model = self.embed_dim or int(input_shape[-1])
|
|
267
|
+
pe = np.zeros((self.max_len, d_model), dtype=np.float32)
|
|
268
|
+
position = np.arange(0, self.max_len)[:, np.newaxis]
|
|
269
|
+
div_term = np.exp(np.arange(0, d_model, 2) * -(math.log(10000.0) / d_model))
|
|
270
|
+
pe[:, 0::2] = np.sin(position * div_term)
|
|
271
|
+
pe[:, 1::2] = np.cos(position * div_term[: d_model // 2 + d_model % 2][:pe[:, 1::2].shape[1]])
|
|
272
|
+
self.pe = tf.constant(pe[np.newaxis, :, :]) # (1, max_len, d_model)
|
|
273
|
+
super().build(input_shape)
|
|
274
|
+
|
|
275
|
+
def call(self, inputs):
|
|
276
|
+
seq_len = tf.shape(inputs)[1]
|
|
277
|
+
return inputs + self.pe[:, :seq_len, :]
|
|
278
|
+
|
|
279
|
+
def get_config(self):
|
|
280
|
+
config = super().get_config()
|
|
281
|
+
config["max_len"] = self.max_len
|
|
282
|
+
config["embed_dim"] = self.embed_dim
|
|
283
|
+
return config
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
class GeM(tf.keras.layers.Layer):
|
|
287
|
+
"""
|
|
288
|
+
Generalized Mean Pooling (GeM).
|
|
289
|
+
|
|
290
|
+
Computes the generalized mean of each feature map. When p=1 it
|
|
291
|
+
becomes average pooling; as pโโ it becomes max pooling.
|
|
292
|
+
|
|
293
|
+
Args:
|
|
294
|
+
p_init: Initial value of the learnable exponent p. Default 3.0.
|
|
295
|
+
eps: Small constant for numerical stability. Default 1e-6.
|
|
296
|
+
"""
|
|
297
|
+
|
|
298
|
+
def __init__(self, p_init=3.0, eps=1e-6, **kwargs):
|
|
299
|
+
super().__init__(**kwargs)
|
|
300
|
+
self.p_init = p_init
|
|
301
|
+
self.eps = eps
|
|
302
|
+
|
|
303
|
+
def build(self, input_shape):
|
|
304
|
+
self.p = self.add_weight(
|
|
305
|
+
name="p", shape=(1,), initializer=tf.keras.initializers.Constant(self.p_init), trainable=True
|
|
306
|
+
)
|
|
307
|
+
super().build(input_shape)
|
|
308
|
+
|
|
309
|
+
def call(self, inputs):
|
|
310
|
+
x = tf.clip_by_value(inputs, clip_value_min=self.eps, clip_value_max=tf.float32.max)
|
|
311
|
+
x = tf.pow(x, self.p)
|
|
312
|
+
x = tf.reduce_mean(x, axis=[1, 2], keepdims=False)
|
|
313
|
+
x = tf.pow(x, 1.0 / self.p)
|
|
314
|
+
return x
|
|
315
|
+
|
|
316
|
+
def get_config(self):
|
|
317
|
+
config = super().get_config()
|
|
318
|
+
config["p_init"] = self.p_init
|
|
319
|
+
config["eps"] = self.eps
|
|
320
|
+
return config
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
class AdaptiveConcatPool2D(tf.keras.layers.Layer):
|
|
324
|
+
"""
|
|
325
|
+
Adaptive Concatenated Pooling (from fastai).
|
|
326
|
+
|
|
327
|
+
Concatenates the results of adaptive average pooling and
|
|
328
|
+
adaptive max pooling, giving the model access to both statistics.
|
|
329
|
+
|
|
330
|
+
Args:
|
|
331
|
+
output_size: Target spatial size (H, W). Default (1, 1).
|
|
332
|
+
"""
|
|
333
|
+
|
|
334
|
+
def __init__(self, output_size=(1, 1), **kwargs):
|
|
335
|
+
super().__init__(**kwargs)
|
|
336
|
+
self.output_size = output_size
|
|
337
|
+
|
|
338
|
+
def call(self, inputs):
|
|
339
|
+
avg = tf.reduce_mean(inputs, axis=[1, 2], keepdims=True)
|
|
340
|
+
mx = tf.reduce_max(inputs, axis=[1, 2], keepdims=True)
|
|
341
|
+
return tf.concat([avg, mx], axis=-1)
|
|
342
|
+
|
|
343
|
+
def get_config(self):
|
|
344
|
+
config = super().get_config()
|
|
345
|
+
config["output_size"] = self.output_size
|
|
346
|
+
return config
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
class Mish(tf.keras.layers.Layer):
|
|
350
|
+
"""
|
|
351
|
+
Mish activation function: x * tanh(softplus(x)).
|
|
352
|
+
|
|
353
|
+
A self-regularized non-monotonic activation function that often
|
|
354
|
+
outperforms ReLU and Swish.
|
|
355
|
+
"""
|
|
356
|
+
|
|
357
|
+
def call(self, inputs):
|
|
358
|
+
return inputs * tf.math.tanh(tf.math.softplus(inputs))
|
|
359
|
+
|
|
360
|
+
def get_config(self):
|
|
361
|
+
return super().get_config()
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""TF-Extra Custom Losses Module."""
|
|
2
|
+
|
|
3
|
+
from tf_extra.losses.custom_losses import (
|
|
4
|
+
FocalLoss,
|
|
5
|
+
DiceLoss,
|
|
6
|
+
TverskyLoss,
|
|
7
|
+
LabelSmoothingCrossEntropy,
|
|
8
|
+
ContrastiveLoss,
|
|
9
|
+
TripletLoss,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"FocalLoss",
|
|
14
|
+
"DiceLoss",
|
|
15
|
+
"TverskyLoss",
|
|
16
|
+
"LabelSmoothingCrossEntropy",
|
|
17
|
+
"ContrastiveLoss",
|
|
18
|
+
"TripletLoss",
|
|
19
|
+
]
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Custom Loss Functions for TF-Extra.
|
|
3
|
+
|
|
4
|
+
Provides commonly used loss functions not included in tf.keras.losses.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import tensorflow as tf
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class FocalLoss(tf.keras.losses.Loss):
|
|
11
|
+
"""
|
|
12
|
+
Focal Loss for addressing class imbalance.
|
|
13
|
+
|
|
14
|
+
Down-weights well-classified examples and focuses on hard negatives.
|
|
15
|
+
FL(p_t) = -alpha_t * (1 - p_t)^gamma * log(p_t)
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
alpha: Balancing factor. Default 0.25.
|
|
19
|
+
gamma: Focusing parameter. Default 2.0.
|
|
20
|
+
from_logits: Whether predictions are logits. Default False.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, alpha=0.25, gamma=2.0, from_logits=False, **kwargs):
|
|
24
|
+
super().__init__(**kwargs)
|
|
25
|
+
self.alpha = alpha
|
|
26
|
+
self.gamma = gamma
|
|
27
|
+
self.from_logits = from_logits
|
|
28
|
+
|
|
29
|
+
def call(self, y_true, y_pred):
|
|
30
|
+
y_true = tf.cast(y_true, tf.float32)
|
|
31
|
+
if self.from_logits:
|
|
32
|
+
y_pred = tf.nn.sigmoid(y_pred)
|
|
33
|
+
|
|
34
|
+
y_pred = tf.clip_by_value(y_pred, 1e-7, 1.0 - 1e-7)
|
|
35
|
+
p_t = y_true * y_pred + (1 - y_true) * (1 - y_pred)
|
|
36
|
+
alpha_t = y_true * self.alpha + (1 - y_true) * (1 - self.alpha)
|
|
37
|
+
focal_weight = alpha_t * tf.pow(1.0 - p_t, self.gamma)
|
|
38
|
+
|
|
39
|
+
bce = -(y_true * tf.math.log(y_pred) + (1 - y_true) * tf.math.log(1 - y_pred))
|
|
40
|
+
return tf.reduce_mean(focal_weight * bce)
|
|
41
|
+
|
|
42
|
+
def get_config(self):
|
|
43
|
+
config = super().get_config()
|
|
44
|
+
config.update({"alpha": self.alpha, "gamma": self.gamma, "from_logits": self.from_logits})
|
|
45
|
+
return config
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class DiceLoss(tf.keras.losses.Loss):
|
|
49
|
+
"""
|
|
50
|
+
Dice Loss for segmentation tasks.
|
|
51
|
+
|
|
52
|
+
Measures the overlap between predicted and ground truth masks.
|
|
53
|
+
DL = 1 - (2 * |X โฉ Y| + smooth) / (|X| + |Y| + smooth)
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
smooth: Smoothing constant to avoid division by zero. Default 1.0.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def __init__(self, smooth=1.0, **kwargs):
|
|
60
|
+
super().__init__(**kwargs)
|
|
61
|
+
self.smooth = smooth
|
|
62
|
+
|
|
63
|
+
def call(self, y_true, y_pred):
|
|
64
|
+
y_true = tf.cast(tf.reshape(y_true, [-1]), tf.float32)
|
|
65
|
+
y_pred = tf.cast(tf.reshape(y_pred, [-1]), tf.float32)
|
|
66
|
+
intersection = tf.reduce_sum(y_true * y_pred)
|
|
67
|
+
return 1.0 - (2.0 * intersection + self.smooth) / (
|
|
68
|
+
tf.reduce_sum(y_true) + tf.reduce_sum(y_pred) + self.smooth
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
def get_config(self):
|
|
72
|
+
config = super().get_config()
|
|
73
|
+
config["smooth"] = self.smooth
|
|
74
|
+
return config
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class TverskyLoss(tf.keras.losses.Loss):
|
|
78
|
+
"""
|
|
79
|
+
Tversky Loss โ generalization of Dice Loss.
|
|
80
|
+
|
|
81
|
+
Allows controlling the penalty for false positives vs false negatives
|
|
82
|
+
via alpha and beta.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
alpha: Weight for false positives. Default 0.5.
|
|
86
|
+
beta: Weight for false negatives. Default 0.5.
|
|
87
|
+
smooth: Smoothing constant. Default 1.0.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
def __init__(self, alpha=0.5, beta=0.5, smooth=1.0, **kwargs):
|
|
91
|
+
super().__init__(**kwargs)
|
|
92
|
+
self.alpha = alpha
|
|
93
|
+
self.beta = beta
|
|
94
|
+
self.smooth = smooth
|
|
95
|
+
|
|
96
|
+
def call(self, y_true, y_pred):
|
|
97
|
+
y_true = tf.cast(tf.reshape(y_true, [-1]), tf.float32)
|
|
98
|
+
y_pred = tf.cast(tf.reshape(y_pred, [-1]), tf.float32)
|
|
99
|
+
tp = tf.reduce_sum(y_true * y_pred)
|
|
100
|
+
fp = tf.reduce_sum((1 - y_true) * y_pred)
|
|
101
|
+
fn = tf.reduce_sum(y_true * (1 - y_pred))
|
|
102
|
+
tversky = (tp + self.smooth) / (tp + self.alpha * fp + self.beta * fn + self.smooth)
|
|
103
|
+
return 1.0 - tversky
|
|
104
|
+
|
|
105
|
+
def get_config(self):
|
|
106
|
+
config = super().get_config()
|
|
107
|
+
config.update({"alpha": self.alpha, "beta": self.beta, "smooth": self.smooth})
|
|
108
|
+
return config
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class LabelSmoothingCrossEntropy(tf.keras.losses.Loss):
|
|
112
|
+
"""
|
|
113
|
+
Cross Entropy with Label Smoothing.
|
|
114
|
+
|
|
115
|
+
Replaces hard 0/1 targets with smoothed targets to prevent
|
|
116
|
+
overconfidence and improve generalization.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
smoothing: Label smoothing factor. Default 0.1.
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
def __init__(self, smoothing=0.1, **kwargs):
|
|
123
|
+
super().__init__(**kwargs)
|
|
124
|
+
self.smoothing = smoothing
|
|
125
|
+
|
|
126
|
+
def call(self, y_true, y_pred):
|
|
127
|
+
num_classes = tf.cast(tf.shape(y_pred)[-1], tf.float32)
|
|
128
|
+
y_true = tf.cast(y_true, tf.float32)
|
|
129
|
+
y_true = y_true * (1.0 - self.smoothing) + self.smoothing / num_classes
|
|
130
|
+
return tf.keras.losses.categorical_crossentropy(y_true, y_pred)
|
|
131
|
+
|
|
132
|
+
def get_config(self):
|
|
133
|
+
config = super().get_config()
|
|
134
|
+
config["smoothing"] = self.smoothing
|
|
135
|
+
return config
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class ContrastiveLoss(tf.keras.losses.Loss):
|
|
139
|
+
"""
|
|
140
|
+
Contrastive Loss for Siamese Networks.
|
|
141
|
+
|
|
142
|
+
L = y * d^2 + (1-y) * max(margin - d, 0)^2
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
margin: Margin for dissimilar pairs. Default 1.0.
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
def __init__(self, margin=1.0, **kwargs):
|
|
149
|
+
super().__init__(**kwargs)
|
|
150
|
+
self.margin = margin
|
|
151
|
+
|
|
152
|
+
def call(self, y_true, y_pred):
|
|
153
|
+
y_true = tf.cast(y_true, tf.float32)
|
|
154
|
+
square_pred = tf.square(y_pred)
|
|
155
|
+
margin_square = tf.square(tf.maximum(self.margin - y_pred, 0))
|
|
156
|
+
return tf.reduce_mean(y_true * square_pred + (1 - y_true) * margin_square)
|
|
157
|
+
|
|
158
|
+
def get_config(self):
|
|
159
|
+
config = super().get_config()
|
|
160
|
+
config["margin"] = self.margin
|
|
161
|
+
return config
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class TripletLoss(tf.keras.losses.Loss):
|
|
165
|
+
"""
|
|
166
|
+
Triplet Loss for metric learning.
|
|
167
|
+
|
|
168
|
+
L = max(d(anchor, positive) - d(anchor, negative) + margin, 0)
|
|
169
|
+
|
|
170
|
+
Expects y_pred to be a concatenation of [anchor, positive, negative]
|
|
171
|
+
embeddings along the last axis, each of size embed_dim.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
margin: Margin for the triplet loss. Default 1.0.
|
|
175
|
+
"""
|
|
176
|
+
|
|
177
|
+
def __init__(self, margin=1.0, **kwargs):
|
|
178
|
+
super().__init__(**kwargs)
|
|
179
|
+
self.margin = margin
|
|
180
|
+
|
|
181
|
+
def call(self, y_true, y_pred):
|
|
182
|
+
# y_pred: (batch, 3 * embed_dim)
|
|
183
|
+
embed_dim = tf.shape(y_pred)[-1] // 3
|
|
184
|
+
anchor = y_pred[:, :embed_dim]
|
|
185
|
+
positive = y_pred[:, embed_dim : 2 * embed_dim]
|
|
186
|
+
negative = y_pred[:, 2 * embed_dim :]
|
|
187
|
+
|
|
188
|
+
pos_dist = tf.reduce_sum(tf.square(anchor - positive), axis=-1)
|
|
189
|
+
neg_dist = tf.reduce_sum(tf.square(anchor - negative), axis=-1)
|
|
190
|
+
loss = tf.maximum(pos_dist - neg_dist + self.margin, 0.0)
|
|
191
|
+
return tf.reduce_mean(loss)
|
|
192
|
+
|
|
193
|
+
def get_config(self):
|
|
194
|
+
config = super().get_config()
|
|
195
|
+
config["margin"] = self.margin
|
|
196
|
+
return config
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MultiOptimizer: Assign different optimizers to different layers of a Keras model.
|
|
3
|
+
|
|
4
|
+
This is a drop-in replacement for the deprecated tensorflow_addons MultiOptimizer,
|
|
5
|
+
fully compatible with TensorFlow 2.x (including TF 2.16+ / Keras 3).
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
import tensorflow as tf
|
|
9
|
+
import tf_extra
|
|
10
|
+
|
|
11
|
+
optimizers = [
|
|
12
|
+
tf.keras.optimizers.Adam(learning_rate=1e-6),
|
|
13
|
+
tf.keras.optimizers.Adam(learning_rate=1e-5),
|
|
14
|
+
tf.keras.optimizers.Adam(learning_rate=1e-4),
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
optimizers_and_layers = [
|
|
18
|
+
(optimizers[0], model.layers[:100]),
|
|
19
|
+
(optimizers[1], model.layers[100:200]),
|
|
20
|
+
(optimizers[2], model.layers[-1]),
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
opt = tf_extra.optimizers.MultiOptimizer(optimizers_and_layers)
|
|
24
|
+
model.compile(optimizer=opt, loss="binary_crossentropy")
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import tensorflow as tf
|
|
28
|
+
import numpy as np
|
|
29
|
+
from typing import List, Tuple, Union, Any, Optional, Sequence
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class MultiOptimizer(tf.keras.optimizers.Optimizer):
|
|
33
|
+
"""
|
|
34
|
+
An optimizer that applies different optimizers to different sets of layers.
|
|
35
|
+
|
|
36
|
+
Each (optimizer, layers) pair assigns that optimizer to the trainable
|
|
37
|
+
variables of the given layer(s). Variables not covered by any pair
|
|
38
|
+
will use the fallback_optimizer (default: Adam with lr=1e-3).
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
optimizers_and_layers: A list of (optimizer, layers) tuples.
|
|
42
|
+
- optimizer: a tf.keras.optimizers.Optimizer instance.
|
|
43
|
+
- layers: a single tf.keras.layers.Layer, or a list of layers.
|
|
44
|
+
fallback_optimizer: Optional optimizer for any trainable variables
|
|
45
|
+
not covered by the pairs above. If None, uncovered variables
|
|
46
|
+
will raise a warning and use Adam(lr=1e-3).
|
|
47
|
+
name: Name for this optimizer. Default "MultiOptimizer".
|
|
48
|
+
**kwargs: Additional keyword arguments passed to the base Optimizer.
|
|
49
|
+
|
|
50
|
+
Raises:
|
|
51
|
+
ValueError: If optimizers_and_layers is empty.
|
|
52
|
+
TypeError: If an element is not a valid (optimizer, layers) tuple.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
optimizers_and_layers: List[
|
|
58
|
+
Tuple[tf.keras.optimizers.Optimizer, Union[tf.keras.layers.Layer, List[tf.keras.layers.Layer]]]
|
|
59
|
+
],
|
|
60
|
+
fallback_optimizer: Optional[tf.keras.optimizers.Optimizer] = None,
|
|
61
|
+
name: str = "MultiOptimizer",
|
|
62
|
+
**kwargs,
|
|
63
|
+
):
|
|
64
|
+
super().__init__(name=name, **kwargs)
|
|
65
|
+
|
|
66
|
+
if not optimizers_and_layers:
|
|
67
|
+
raise ValueError(
|
|
68
|
+
"`optimizers_and_layers` must be a non-empty list of "
|
|
69
|
+
"(optimizer, layers) tuples."
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
self._optimizers_and_layers = []
|
|
73
|
+
self._optimizer_list = []
|
|
74
|
+
|
|
75
|
+
for idx, pair in enumerate(optimizers_and_layers):
|
|
76
|
+
if not isinstance(pair, (list, tuple)) or len(pair) != 2:
|
|
77
|
+
raise TypeError(
|
|
78
|
+
f"Element {idx} of optimizers_and_layers must be a "
|
|
79
|
+
f"(optimizer, layers) tuple, got {type(pair)}."
|
|
80
|
+
)
|
|
81
|
+
optimizer, layer_or_layers = pair
|
|
82
|
+
|
|
83
|
+
if not isinstance(optimizer, tf.keras.optimizers.Optimizer):
|
|
84
|
+
raise TypeError(
|
|
85
|
+
f"Element {idx}: first item must be a "
|
|
86
|
+
f"tf.keras.optimizers.Optimizer, got {type(optimizer)}."
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Normalize layers to a list
|
|
90
|
+
if isinstance(layer_or_layers, tf.keras.layers.Layer):
|
|
91
|
+
layers_list = [layer_or_layers]
|
|
92
|
+
elif isinstance(layer_or_layers, (list, tuple)):
|
|
93
|
+
layers_list = list(layer_or_layers)
|
|
94
|
+
else:
|
|
95
|
+
raise TypeError(
|
|
96
|
+
f"Element {idx}: second item must be a Layer or list of "
|
|
97
|
+
f"Layers, got {type(layer_or_layers)}."
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
for layer in layers_list:
|
|
101
|
+
if not isinstance(layer, tf.keras.layers.Layer):
|
|
102
|
+
raise TypeError(
|
|
103
|
+
f"Expected tf.keras.layers.Layer, got {type(layer)}."
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
self._optimizers_and_layers.append((optimizer, layers_list))
|
|
107
|
+
if optimizer not in self._optimizer_list:
|
|
108
|
+
self._optimizer_list.append(optimizer)
|
|
109
|
+
|
|
110
|
+
# Fallback optimizer
|
|
111
|
+
if fallback_optimizer is not None:
|
|
112
|
+
self._fallback_optimizer = fallback_optimizer
|
|
113
|
+
else:
|
|
114
|
+
self._fallback_optimizer = tf.keras.optimizers.Adam(learning_rate=1e-3)
|
|
115
|
+
|
|
116
|
+
if self._fallback_optimizer not in self._optimizer_list:
|
|
117
|
+
self._optimizer_list.append(self._fallback_optimizer)
|
|
118
|
+
|
|
119
|
+
# Build variable-to-optimizer mapping (populated in _build_mapping)
|
|
120
|
+
self._var_optimizer_map = None
|
|
121
|
+
|
|
122
|
+
# ------------------------------------------------------------------
|
|
123
|
+
# Internal: build a dict variable.ref() -> optimizer
|
|
124
|
+
# ------------------------------------------------------------------
|
|
125
|
+
def _build_mapping(self, var_list):
|
|
126
|
+
"""Build mapping from variable refs to their assigned optimizer."""
|
|
127
|
+
self._var_optimizer_map = {}
|
|
128
|
+
assigned_refs = set()
|
|
129
|
+
|
|
130
|
+
for optimizer, layers_list in self._optimizers_and_layers:
|
|
131
|
+
for layer in layers_list:
|
|
132
|
+
for var in layer.trainable_variables:
|
|
133
|
+
ref = var.ref()
|
|
134
|
+
if ref in assigned_refs:
|
|
135
|
+
import warnings
|
|
136
|
+
warnings.warn(
|
|
137
|
+
f"Variable '{var.name}' is assigned to multiple "
|
|
138
|
+
f"optimizers. The last assignment wins."
|
|
139
|
+
)
|
|
140
|
+
self._var_optimizer_map[ref] = optimizer
|
|
141
|
+
assigned_refs.add(ref)
|
|
142
|
+
|
|
143
|
+
# Assign fallback to any unassigned variables
|
|
144
|
+
for var in var_list:
|
|
145
|
+
ref = var.ref()
|
|
146
|
+
if ref not in self._var_optimizer_map:
|
|
147
|
+
self._var_optimizer_map[ref] = self._fallback_optimizer
|
|
148
|
+
|
|
149
|
+
# ------------------------------------------------------------------
|
|
150
|
+
# Public API expected by Keras
|
|
151
|
+
# ------------------------------------------------------------------
|
|
152
|
+
def apply_gradients(self, grads_and_vars, **kwargs):
|
|
153
|
+
"""
|
|
154
|
+
Group gradients by their assigned optimizer and apply each group.
|
|
155
|
+
"""
|
|
156
|
+
grads_and_vars = list(grads_and_vars)
|
|
157
|
+
all_vars = [v for _, v in grads_and_vars]
|
|
158
|
+
|
|
159
|
+
if self._var_optimizer_map is None:
|
|
160
|
+
self._build_mapping(all_vars)
|
|
161
|
+
|
|
162
|
+
# Bucket gradients by optimizer
|
|
163
|
+
optimizer_grads = {} # optimizer -> list of (grad, var)
|
|
164
|
+
for grad, var in grads_and_vars:
|
|
165
|
+
if grad is None:
|
|
166
|
+
continue
|
|
167
|
+
opt = self._var_optimizer_map.get(var.ref(), self._fallback_optimizer)
|
|
168
|
+
optimizer_grads.setdefault(id(opt), (opt, []))
|
|
169
|
+
optimizer_grads[id(opt)][1].append((grad, var))
|
|
170
|
+
|
|
171
|
+
# Apply each optimizer's gradients
|
|
172
|
+
results = []
|
|
173
|
+
for _, (opt, gv_list) in optimizer_grads.items():
|
|
174
|
+
results.append(opt.apply_gradients(gv_list, **kwargs))
|
|
175
|
+
|
|
176
|
+
return results
|
|
177
|
+
|
|
178
|
+
# ------------------------------------------------------------------
|
|
179
|
+
# Convenience: allow model.compile(optimizer=multi_opt) to work
|
|
180
|
+
# ------------------------------------------------------------------
|
|
181
|
+
def get_config(self):
|
|
182
|
+
"""Return serializable config dict."""
|
|
183
|
+
config = super().get_config()
|
|
184
|
+
opt_layer_configs = []
|
|
185
|
+
for optimizer, layers_list in self._optimizers_and_layers:
|
|
186
|
+
opt_layer_configs.append(
|
|
187
|
+
{
|
|
188
|
+
"optimizer": tf.keras.optimizers.serialize(optimizer),
|
|
189
|
+
"layer_names": [l.name for l in layers_list],
|
|
190
|
+
}
|
|
191
|
+
)
|
|
192
|
+
config["optimizers_and_layers"] = opt_layer_configs
|
|
193
|
+
config["fallback_optimizer"] = tf.keras.optimizers.serialize(
|
|
194
|
+
self._fallback_optimizer
|
|
195
|
+
)
|
|
196
|
+
return config
|
|
197
|
+
|
|
198
|
+
@property
|
|
199
|
+
def learning_rate(self):
|
|
200
|
+
"""Return the learning rate of the first optimizer (for logging)."""
|
|
201
|
+
return self._optimizers_and_layers[0][0].learning_rate
|
|
202
|
+
|
|
203
|
+
@learning_rate.setter
|
|
204
|
+
def learning_rate(self, value):
|
|
205
|
+
"""Set learning rate on ALL sub-optimizers."""
|
|
206
|
+
for opt, _ in self._optimizers_and_layers:
|
|
207
|
+
opt.learning_rate = value
|
|
208
|
+
self._fallback_optimizer.learning_rate = value
|
|
209
|
+
|
|
210
|
+
@property
|
|
211
|
+
def sub_optimizers(self):
|
|
212
|
+
"""Access list of (optimizer, layers) pairs."""
|
|
213
|
+
return list(self._optimizers_and_layers)
|
|
214
|
+
|
|
215
|
+
@property
|
|
216
|
+
def optimizers(self):
|
|
217
|
+
"""Return flat list of unique optimizers."""
|
|
218
|
+
return list(self._optimizer_list)
|
|
219
|
+
|
|
220
|
+
# ------------------------------------------------------------------
|
|
221
|
+
# Required abstract methods (no-ops; real work delegated to sub-opts)
|
|
222
|
+
# ------------------------------------------------------------------
|
|
223
|
+
def _create_slots(self, var_list):
|
|
224
|
+
pass
|
|
225
|
+
|
|
226
|
+
def _resource_apply_dense(self, grad, handle, apply_state=None):
|
|
227
|
+
pass
|
|
228
|
+
|
|
229
|
+
def _resource_apply_sparse(self, grad, handle, indices, apply_state=None):
|
|
230
|
+
pass
|
|
231
|
+
|
|
232
|
+
def get_gradients(self, loss, params):
|
|
233
|
+
"""Compute gradients โ delegates to tf.GradientTape compatible path."""
|
|
234
|
+
return super().get_gradients(loss, params)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Your Name
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: tf-extra
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A powerful extension library for TensorFlow/Keras โ MultiOptimizer, custom layers, losses, and callbacks.
|
|
5
|
+
Home-page: https://github.com/yourusername/tf-extra
|
|
6
|
+
Author: Your Name
|
|
7
|
+
Author-email: Your Name <your.email@example.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
Project-URL: Homepage, https://github.com/yourusername/tf-extra
|
|
10
|
+
Project-URL: Repository, https://github.com/yourusername/tf-extra
|
|
11
|
+
Project-URL: Issues, https://github.com/yourusername/tf-extra/issues
|
|
12
|
+
Keywords: tensorflow,keras,deep-learning,multi-optimizer,attention,focal-loss
|
|
13
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: Science/Research
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
20
|
+
Requires-Python: >=3.8
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: tensorflow>=2.10.0
|
|
24
|
+
Requires-Dist: numpy>=1.21.0
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
27
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
28
|
+
Requires-Dist: flake8>=5.0; extra == "dev"
|
|
29
|
+
Requires-Dist: black>=22.0; extra == "dev"
|
|
30
|
+
Requires-Dist: isort>=5.0; extra == "dev"
|
|
31
|
+
Requires-Dist: mypy>=0.990; extra == "dev"
|
|
32
|
+
|
|
33
|
+
# ๐ TF-Extra
|
|
34
|
+
|
|
35
|
+
**A powerful extension library for TensorFlow/Keras** โ the spiritual successor to `tensorflow_addons`.
|
|
36
|
+
|
|
37
|
+
[](https://pypi.org/project/tf-extra/)
|
|
38
|
+
[](https://www.python.org/downloads/)
|
|
39
|
+
[](https://opensource.org/licenses/MIT)
|
|
40
|
+
[](https://www.tensorflow.org/)
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## โจ Features
|
|
45
|
+
|
|
46
|
+
| Module | Components |
|
|
47
|
+
|--------|-----------|
|
|
48
|
+
| **๐ง Optimizers** | `MultiOptimizer` โ assign different optimizers to different layers |
|
|
49
|
+
| **๐ Layers** | `CBAM`, `SqueezeExcitation`, `MultiHeadSelfAttention`, `GeM`, `Mish`, `PositionalEncoding`, `AdaptiveConcatPool2D` |
|
|
50
|
+
| **๐ Losses** | `FocalLoss`, `DiceLoss`, `TverskyLoss`, `LabelSmoothingCrossEntropy`, `ContrastiveLoss`, `TripletLoss` |
|
|
51
|
+
| **๐ Callbacks** | `CosineAnnealingScheduler`, `GradientAccumulationCallback`, `TimeLimitCallback` |
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## ๐ฆ Installation
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pip install tf-extra
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Or install from source:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
git clone https://github.com/yourusername/tf-extra.git
|
|
65
|
+
cd tf-extra
|
|
66
|
+
pip install -e .
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## ๐ Quick Start
|
|
72
|
+
|
|
73
|
+
### MultiOptimizer (Drop-in replacement for `tfa.optimizers.MultiOptimizer`)
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
import tensorflow as tf
|
|
77
|
+
import tf_extra
|
|
78
|
+
|
|
79
|
+
# Load a pre-trained model
|
|
80
|
+
base_model = tf.keras.applications.ResNet50(weights='imagenet', include_top=False)
|
|
81
|
+
x = tf.keras.layers.GlobalAveragePooling2D()(base_model.output)
|
|
82
|
+
x = tf.keras.layers.Dense(1)(x)
|
|
83
|
+
model = tf.keras.Model(base_model.input, x)
|
|
84
|
+
|
|
85
|
+
# Define different learning rates for different parts of the model
|
|
86
|
+
optimizers = [
|
|
87
|
+
tf.keras.optimizers.Adam(learning_rate=1e-6), # Very slow for early layers
|
|
88
|
+
tf.keras.optimizers.Adam(learning_rate=1e-5), # Medium for middle layers
|
|
89
|
+
tf.keras.optimizers.Adam(learning_rate=1e-4), # Fast for the head
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
optimizers_and_layers = [
|
|
93
|
+
(optimizers[0], base_model.layers[:100]),
|
|
94
|
+
(optimizers[1], base_model.layers[100:]),
|
|
95
|
+
(optimizers[2], model.layers[-1]),
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
opt = tf_extra.optimizers.MultiOptimizer(optimizers_and_layers)
|
|
99
|
+
model.compile(
|
|
100
|
+
optimizer=opt,
|
|
101
|
+
loss=tf.keras.losses.BinaryCrossentropy(from_logits=True),
|
|
102
|
+
metrics=[tf.keras.metrics.BinaryAccuracy(threshold=0, name='accuracy')]
|
|
103
|
+
)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Focal Loss (for imbalanced datasets)
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
import tf_extra
|
|
110
|
+
|
|
111
|
+
model.compile(
|
|
112
|
+
optimizer='adam',
|
|
113
|
+
loss=tf_extra.losses.FocalLoss(alpha=0.25, gamma=2.0),
|
|
114
|
+
metrics=['accuracy']
|
|
115
|
+
)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### CBAM Attention Layer
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
import tf_extra
|
|
122
|
+
|
|
123
|
+
x = tf.keras.layers.Conv2D(64, 3, padding='same')(inputs)
|
|
124
|
+
x = tf_extra.layers.CBAM(reduction_ratio=16)(x)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Cosine Annealing with Warm Restarts
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
import tf_extra
|
|
131
|
+
|
|
132
|
+
scheduler = tf_extra.callbacks.CosineAnnealingScheduler(
|
|
133
|
+
eta_min=1e-7,
|
|
134
|
+
eta_max=1e-3,
|
|
135
|
+
T_0=10,
|
|
136
|
+
T_mult=2
|
|
137
|
+
)
|
|
138
|
+
model.fit(X, y, callbacks=[scheduler])
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## ๐ Full API Reference
|
|
144
|
+
|
|
145
|
+
### `tf_extra.optimizers.MultiOptimizer`
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
MultiOptimizer(
|
|
149
|
+
optimizers_and_layers, # List of (optimizer, layers) tuples
|
|
150
|
+
fallback_optimizer=None, # Optional: optimizer for unassigned variables
|
|
151
|
+
name="MultiOptimizer"
|
|
152
|
+
)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### `tf_extra.layers`
|
|
156
|
+
|
|
157
|
+
| Layer | Description |
|
|
158
|
+
|-------|-------------|
|
|
159
|
+
| `CBAM(reduction_ratio=16, kernel_size=7)` | Convolutional Block Attention Module |
|
|
160
|
+
| `SqueezeExcitation(reduction_ratio=16)` | SE-Net attention block |
|
|
161
|
+
| `MultiHeadSelfAttention(embed_dim, num_heads)` | Transformer-style self-attention |
|
|
162
|
+
| `GeM(p_init=3.0)` | Generalized Mean Pooling |
|
|
163
|
+
| `Mish()` | Mish activation function |
|
|
164
|
+
| `PositionalEncoding(max_len=5000)` | Sinusoidal positional encoding |
|
|
165
|
+
| `AdaptiveConcatPool2D()` | Concatenated average + max pooling |
|
|
166
|
+
| `SpatialDropout1D(rate=0.5)` | Spatial dropout for 1D data |
|
|
167
|
+
|
|
168
|
+
### `tf_extra.losses`
|
|
169
|
+
|
|
170
|
+
| Loss | Description |
|
|
171
|
+
|------|-------------|
|
|
172
|
+
| `FocalLoss(alpha=0.25, gamma=2.0)` | Focal loss for class imbalance |
|
|
173
|
+
| `DiceLoss(smooth=1.0)` | Dice loss for segmentation |
|
|
174
|
+
| `TverskyLoss(alpha=0.5, beta=0.5)` | Generalized Dice loss |
|
|
175
|
+
| `LabelSmoothingCrossEntropy(smoothing=0.1)` | Label smoothing |
|
|
176
|
+
| `ContrastiveLoss(margin=1.0)` | For Siamese networks |
|
|
177
|
+
| `TripletLoss(margin=1.0)` | For metric learning |
|
|
178
|
+
|
|
179
|
+
### `tf_extra.callbacks`
|
|
180
|
+
|
|
181
|
+
| Callback | Description |
|
|
182
|
+
|----------|-------------|
|
|
183
|
+
| `CosineAnnealingScheduler(eta_min, eta_max, T_0, T_mult)` | SGDR scheduler |
|
|
184
|
+
| `GradientAccumulationCallback(accumulation_steps=4)` | Gradient accumulation |
|
|
185
|
+
| `TimeLimitCallback(max_seconds)` | Stop training after time limit |
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## ๐งช Testing
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
pip install -e ".[dev]"
|
|
193
|
+
pytest tests/ -v
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## ๐ค Contributing
|
|
199
|
+
|
|
200
|
+
Contributions are welcome! Please read the contributing guidelines and submit a PR.
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## ๐ License
|
|
205
|
+
|
|
206
|
+
MIT License โ see [LICENSE](LICENSE) for details.
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## ๐ Acknowledgements
|
|
211
|
+
|
|
212
|
+
Inspired by `tensorflow_addons` which is no longer maintained.
|
|
213
|
+
Built to provide the same functionality with modern TensorFlow compatibility.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
tf_extra/__init__.py,sha256=tl7OZ4sEFDPOZ1-NL7lshGqMCi7dEBHMzDBcic_xLKY,492
|
|
2
|
+
tf_extra/callbacks/__init__.py,sha256=OxhzxY0CnjrUyI49IJBMzfnr1Vql3z8TtcoXAsWMXQc,288
|
|
3
|
+
tf_extra/callbacks/custom_callbacks.py,sha256=AsiGF7nopKC7bAsyc4pnZlSmdhPuMcjtinV2ur6xTbU,3658
|
|
4
|
+
tf_extra/layers/__init__.py,sha256=xSrCBxNg0GA7tso-lW8TngmBilWVR7_F01J8C1qZxnY,511
|
|
5
|
+
tf_extra/layers/custom_layers.py,sha256=PB8ktQT86AjgcXTAlHSMyIZXTI2jI2Hs7DPf8CWBMZk,11731
|
|
6
|
+
tf_extra/losses/__init__.py,sha256=pjMbfeqnqpceOLmEQD6HY2aVuahp3sMyc2zP1w5scrI,343
|
|
7
|
+
tf_extra/losses/custom_losses.py,sha256=fDDaxCwo_NcXK-51NH7mGLInmA7IHtwqvRNwbgpdgeQ,6250
|
|
8
|
+
tf_extra/optimizers/__init__.py,sha256=M1eLL7IFb6SQXZEHpTz2abXdjby8TFus2nDRfPoEe-k,128
|
|
9
|
+
tf_extra/optimizers/multi_optimizer.py,sha256=oI02Stbg067wyBWU0AdLyLtPlQGMfeALcNKOKNigupg,9199
|
|
10
|
+
tf_extra-1.0.0.dist-info/LICENSE,sha256=F-4b93u0OVrVwGXgMwBRq6MlGyUT9zmre1oh5Gft5Ts,1066
|
|
11
|
+
tf_extra-1.0.0.dist-info/METADATA,sha256=AgpScl2oJkMvnMp3AFfE4XozUaY2wmCge2zkfCLRL8c,6625
|
|
12
|
+
tf_extra-1.0.0.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
|
|
13
|
+
tf_extra-1.0.0.dist-info/top_level.txt,sha256=o7AvNlkeqL9f3EDtsjgPLEBsCmm08OsrI-MMb2DBe4A,9
|
|
14
|
+
tf_extra-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
tf_extra
|