import argparse import os import sys from datetime import datetime from pathlib import Path root = Path(__file__).resolve().parents[1] sys.path.append(str(root)) import tensorflow as tf from src.data import get_datasets, prepare_dataset, get_augmentation_layer from src.models import get_model from src.utils import save_history, plot_training_history try: from src.config_loader import set_yaml_defaults except Exception: # pragma: no cover - pyyaml optional set_yaml_defaults = None def parse_args(): parser = argparse.ArgumentParser(description='Train brain tumor detection models') parser.add_argument('--model', choices=['cnn', 'transfer', 'vit'], default='cnn') parser.add_argument('--dataset', default='dataset') parser.add_argument('--epochs', type=int, default=10) parser.add_argument('--batch_size', type=int, default=32) parser.add_argument('--learning_rate', type=float, default=1e-4) parser.add_argument('--validation_split', type=float, default=0.15) parser.add_argument('--output', default='artifacts') parser.add_argument('--fine_tune_transfer', action='store_true', help='Unfreeze the upper layers of the transfer backbone.') parser.add_argument('--transfer_fine_tune_at', type=int, default=140, help='Layer index where transfer fine-tuning starts.') parser.add_argument('--augment', action='store_true', help='Apply random flip/rotation/zoom/contrast augmentation on the train split.') parser.add_argument('--config', default=None, help='Optional path to config.yaml to use for default values.') # YAML defaults: read the [training] section of config.yaml and apply as # parser defaults. CLI flags still win. Mapping below is explicit since the # YAML keys don't all match argparse attribute names. pre_args, _ = parser.parse_known_args() if set_yaml_defaults is not None: try: set_yaml_defaults( parser, 'training', mapping={ 'epochs': 'epochs', 'batch_size': 'batch_size', 'learning_rate': 'learning_rate', }, path=pre_args.config, ) except FileNotFoundError: pass return parser.parse_args() def main(): args = parse_args() model_name = args.model train_ds, val_ds, test_ds = get_datasets( args.dataset, batch_size=args.batch_size, validation_split=args.validation_split, ) # Optional train-time augmentation. The aug layer also rescales to [0,1] so # we keep the in-model Rescaling unchanged: aug layer outputs float [0,1], # the in-model Rescaling(1/255) gets a near-no-op since inputs are already # small floats. Pass through the float tensor; for transfer/vit the # ResNet50 preprocess_input remains valid (it accepts floats). if args.augment: aug = get_augmentation_layer(image_size=(224, 224)) train_ds = train_ds.map( lambda x, y: (aug(x, training=True) * 255.0, y), num_parallel_calls=tf.data.AUTOTUNE, ) train_ds = prepare_dataset(train_ds) val_ds = prepare_dataset(val_ds) model = get_model( model_name, fine_tune_transfer=args.fine_tune_transfer, transfer_fine_tune_at=args.transfer_fine_tune_at, ) model.compile( optimizer=tf.keras.optimizers.Adam(learning_rate=args.learning_rate), loss='binary_crossentropy', metrics=['accuracy', tf.keras.metrics.Precision(name='precision'), tf.keras.metrics.Recall(name='recall')], ) timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') model_dir = os.path.join(args.output, model_name) os.makedirs(model_dir, exist_ok=True) checkpoint_path = os.path.join(model_dir, 'best_weights.weights.h5') callbacks = [ tf.keras.callbacks.ModelCheckpoint( filepath=checkpoint_path, monitor='val_accuracy', save_best_only=True, save_weights_only=True, verbose=1, ), tf.keras.callbacks.EarlyStopping(monitor='val_loss', patience=3, restore_best_weights=True, verbose=1), ] history = model.fit( train_ds, validation_data=val_ds, epochs=args.epochs, callbacks=callbacks, ) history_path = os.path.join(model_dir, f'history_{timestamp}.npz') save_history(history, history_path) plot_training_history(history, model_dir) print(f'Model training complete. Weights and history saved to {model_dir}') if __name__ == '__main__': main()