Skip to content

Commit cdae54b

Browse files
committed
trainer: Enable resuming from checkpoint
1 parent 671f61e commit cdae54b

1 file changed

Lines changed: 55 additions & 11 deletions

File tree

trainer/train_model.py

Lines changed: 55 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,12 @@ def get_args():
9191
default=1,
9292
help='How many dataset chunks to pool, shuffle, and train on in a single fit call (default: 1)'
9393
)
94+
parser.add_argument(
95+
'--resume-from-checkpoint',
96+
type=str,
97+
default=None,
98+
help='Path to .keras checkpoint file to resume training from (default: None)'
99+
)
94100
args, _ = parser.parse_known_args()
95101
return args
96102

@@ -278,20 +284,58 @@ def categorical_crossentropy_2d_gene_ml(y_true, y_pred):
278284
except Exception:
279285
pass
280286

281-
if N_GPUS > 1:
282-
# https://keras.io/guides/distributed_training/
283-
strategy = tensorflow.distribute.MirroredStrategy()
284-
tee('Number of devices: {}'.format(strategy.num_replicas_in_sync))
285-
with strategy.scope():
287+
if args.resume_from_checkpoint:
288+
tee(f"\033[1mLoading model from checkpoint: {args.resume_from_checkpoint}\033[0m")
289+
# Load with custom objects
290+
custom_objects = {
291+
'categorical_crossentropy_2d_gene_ml': categorical_crossentropy_2d_gene_ml,
292+
'WarmupSchedule': WarmupSchedule,
293+
'DecayOnPlateauSchedule': DecayOnPlateauSchedule,
294+
}
295+
model = keras.models.load_model(args.resume_from_checkpoint, custom_objects=custom_objects)
296+
297+
# Extract the decay_factor from the saved optimizer config
298+
# The learning rate schedule is saved in the optimizer, and we need to extract
299+
# the decay_factor value to continue modifying it if needed
300+
try:
301+
lr_config = model.optimizer.learning_rate.get_config()
302+
saved_decay_factor = lr_config.get('decay_factor', 1.0)
303+
tee(f"Loaded decay_factor from checkpoint: {saved_decay_factor}")
304+
except AttributeError:
305+
saved_decay_factor = 1.0
306+
tee("Could not extract decay_factor from checkpoint, assuming 1.0 (no decay applied yet)")
307+
308+
# Create a new decay_factor variable initialized to the saved value
309+
decay_factor = tensorflow.Variable(saved_decay_factor, trainable=False, dtype=tensorflow.float32, name='lr_decay_factor')
310+
311+
# Get current learning rate
312+
current_lr = model.optimizer.learning_rate
313+
if callable(current_lr):
314+
lr_value = float(tensorflow.keras.backend.get_value(current_lr(model.optimizer.iterations)))
315+
else:
316+
lr_value = float(tensorflow.keras.backend.get_value(current_lr))
317+
tee(f"Resumed from checkpoint. Current learning rate: {lr_value:.5f}")
318+
319+
# Extract starting epoch from checkpoint filename (e.g., "ep10" -> start at 11)
320+
match = re.search(r'_ep(\d+)\.keras', args.resume_from_checkpoint)
321+
start_epoch = int(match.group(1)) + 1 if match else 1
322+
tee(f"\033[1mResuming from epoch {start_epoch}\033[0m")
323+
else:
324+
if N_GPUS > 1:
325+
# https://keras.io/guides/distributed_training/
326+
strategy = tensorflow.distribute.MirroredStrategy()
327+
tee('Number of devices: {}'.format(strategy.num_replicas_in_sync))
328+
with strategy.scope():
329+
model = GeneML(L, W, AR, num_classes)
330+
model.compile(loss=loss,
331+
optimizer=keras.optimizers.Adam(learning_rate=lr_schedule,
332+
weight_decay=WEIGHT_DECAY))
333+
else:
286334
model = GeneML(L, W, AR, num_classes)
287335
model.compile(loss=loss,
288336
optimizer=keras.optimizers.Adam(learning_rate=lr_schedule,
289337
weight_decay=WEIGHT_DECAY))
290-
else:
291-
model = GeneML(L, W, AR, num_classes)
292-
model.compile(loss=loss,
293-
optimizer=keras.optimizers.Adam(learning_rate=lr_schedule,
294-
weight_decay=WEIGHT_DECAY))
338+
start_epoch = 1
295339
# model.summary()
296340

297341
###############################################################################
@@ -374,7 +418,7 @@ def print_performance_metrics(indices, max_eval):
374418

375419
return acceptor_score, donor_score
376420

377-
for epoch_num in range(1, args.num_epochs + 1):
421+
for epoch_num in range(start_epoch, args.num_epochs + 1):
378422
# Shuffle indices for this epoch (no replacement)
379423
# Use epoch-based seed for deterministic but different permutation per epoch
380424
np.random.seed(SEED + epoch_num)

0 commit comments

Comments
 (0)