From cecdd7d5a136ca80beed5ddb31f5995e71d65811 Mon Sep 17 00:00:00 2001 From: Devasy Patel <110348311+Devasy23@users.noreply.github.com> Date: Sat, 23 Nov 2024 19:01:12 +0530 Subject: [PATCH 1/9] renamed folder --- {deeptune => deeptuner}/architectures/siamese.py | 0 {deeptune => deeptuner}/backbones/resnet.py | 0 {deeptune => deeptuner}/callbacks/finetune_callback.py | 0 {deeptune => deeptuner}/datagenerators/triplet_data_generator.py | 0 {deeptune => deeptuner}/losses/arcface_loss.py | 0 {deeptune => deeptuner}/losses/triplet_loss.py | 0 {deeptune => deeptuner}/run.py | 0 {deeptune => deeptuner}/siamese_network.py | 0 {deeptune => deeptuner}/tripletdatagenerator.py | 0 {deeptune => deeptuner}/utils.py | 0 {deeptune => deeptuner}/utils/helpers.py | 0 11 files changed, 0 insertions(+), 0 deletions(-) rename {deeptune => deeptuner}/architectures/siamese.py (100%) rename {deeptune => deeptuner}/backbones/resnet.py (100%) rename {deeptune => deeptuner}/callbacks/finetune_callback.py (100%) rename {deeptune => deeptuner}/datagenerators/triplet_data_generator.py (100%) rename {deeptune => deeptuner}/losses/arcface_loss.py (100%) rename {deeptune => deeptuner}/losses/triplet_loss.py (100%) rename {deeptune => deeptuner}/run.py (100%) rename {deeptune => deeptuner}/siamese_network.py (100%) rename {deeptune => deeptuner}/tripletdatagenerator.py (100%) rename {deeptune => deeptuner}/utils.py (100%) rename {deeptune => deeptuner}/utils/helpers.py (100%) diff --git a/deeptune/architectures/siamese.py b/deeptuner/architectures/siamese.py similarity index 100% rename from deeptune/architectures/siamese.py rename to deeptuner/architectures/siamese.py diff --git a/deeptune/backbones/resnet.py b/deeptuner/backbones/resnet.py similarity index 100% rename from deeptune/backbones/resnet.py rename to deeptuner/backbones/resnet.py diff --git a/deeptune/callbacks/finetune_callback.py b/deeptuner/callbacks/finetune_callback.py similarity index 100% rename from deeptune/callbacks/finetune_callback.py rename to deeptuner/callbacks/finetune_callback.py diff --git a/deeptune/datagenerators/triplet_data_generator.py b/deeptuner/datagenerators/triplet_data_generator.py similarity index 100% rename from deeptune/datagenerators/triplet_data_generator.py rename to deeptuner/datagenerators/triplet_data_generator.py diff --git a/deeptune/losses/arcface_loss.py b/deeptuner/losses/arcface_loss.py similarity index 100% rename from deeptune/losses/arcface_loss.py rename to deeptuner/losses/arcface_loss.py diff --git a/deeptune/losses/triplet_loss.py b/deeptuner/losses/triplet_loss.py similarity index 100% rename from deeptune/losses/triplet_loss.py rename to deeptuner/losses/triplet_loss.py diff --git a/deeptune/run.py b/deeptuner/run.py similarity index 100% rename from deeptune/run.py rename to deeptuner/run.py diff --git a/deeptune/siamese_network.py b/deeptuner/siamese_network.py similarity index 100% rename from deeptune/siamese_network.py rename to deeptuner/siamese_network.py diff --git a/deeptune/tripletdatagenerator.py b/deeptuner/tripletdatagenerator.py similarity index 100% rename from deeptune/tripletdatagenerator.py rename to deeptuner/tripletdatagenerator.py diff --git a/deeptune/utils.py b/deeptuner/utils.py similarity index 100% rename from deeptune/utils.py rename to deeptuner/utils.py diff --git a/deeptune/utils/helpers.py b/deeptuner/utils/helpers.py similarity index 100% rename from deeptune/utils/helpers.py rename to deeptuner/utils/helpers.py From 6031898296dc7d85bb82ea602ae3dd2ebe39c6f9 Mon Sep 17 00:00:00 2001 From: Devasy Patel <110348311+Devasy23@users.noreply.github.com> Date: Sat, 23 Nov 2024 19:01:55 +0530 Subject: [PATCH 2/9] Updated package to version 0.1.1 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 6badba8..55fdfdf 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setup( name='DeepTuner', - version='0.1.0', + version='0.1.1', author='Devasy Patel', author_email='patel.devasy.23@gmail.com', description='A package for fine-tuning deep learning models with Siamese architecture and triplet loss', From 0ad60e7408cda40544b6c5647a57a99e6ac526b8 Mon Sep 17 00:00:00 2001 From: Devasy Patel <110348311+Devasy23@users.noreply.github.com> Date: Sat, 23 Nov 2024 19:11:38 +0530 Subject: [PATCH 3/9] reappeared the publish.yml file --- .github/workflows/publish.yml | 39 +++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 .github/workflows/publish.yml diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..b71ded5 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,39 @@ +name: Publish Package + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.8' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install setuptools wheel + - name: Build package + run: python setup.py sdist bdist_wheel + + - name: Publish package + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} + run: | + pip install twine + twine upload dist/* + - name: Clean up + run: rm -rf dist build *.egg-info \ No newline at end of file From 308da48d7d64fe729cf5682a029206e0516149c4 Mon Sep 17 00:00:00 2001 From: Devasy Patel <110348311+Devasy23@users.noreply.github.com> Date: Sun, 24 Nov 2024 14:25:40 +0530 Subject: [PATCH 4/9] Reapply "Modularize code" This reverts commit 86bae9256733e1ed91faaf51c05f4a744456bd83. --- .devcontainer/devcontainer.json | 6 + .github/workflows/publish.yml | 2 +- README.md | 156 ++++++++++++++++-- deeptune/architectures/siamese.py | 19 +++ deeptune/backbones/resnet.py | 27 +++ deeptune/callbacks/finetune_callback.py | 32 ++++ .../datagenerators/triplet_data_generator.py | 71 ++++++++ deeptune/losses/arcface_loss.py | 16 ++ deeptune/losses/triplet_loss.py | 11 ++ deeptune/utils/helpers.py | 60 +++++++ deeptuner/run.py | 110 +++++------- examples/train_siamese.py | 146 ++++++++++++++++ requirements.txt | 2 +- 13 files changed, 571 insertions(+), 87 deletions(-) create mode 100644 .devcontainer/devcontainer.json create mode 100644 deeptune/architectures/siamese.py create mode 100644 deeptune/backbones/resnet.py create mode 100644 deeptune/callbacks/finetune_callback.py create mode 100644 deeptune/datagenerators/triplet_data_generator.py create mode 100644 deeptune/losses/arcface_loss.py create mode 100644 deeptune/losses/triplet_loss.py create mode 100644 deeptune/utils/helpers.py create mode 100644 examples/train_siamese.py diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..f4da4a6 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,6 @@ +{ + "tasks": { + "build": "pip install -r requirements.txt", + "launch": "python -m deeptuner.run" + } +} \ No newline at end of file diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index b71ded5..16c8026 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -36,4 +36,4 @@ jobs: pip install twine twine upload dist/* - name: Clean up - run: rm -rf dist build *.egg-info \ No newline at end of file + run: rm -rf dist build *.egg-info diff --git a/README.md b/README.md index d0d5edb..a07a88a 100644 --- a/README.md +++ b/README.md @@ -19,25 +19,151 @@ pip install DeepTuner Here is an example of how to use the package for fine-tuning models with Siamese architecture and triplet loss: ```python -import DeepTuner -from DeepTuner import triplet_loss, backbones, data_preprocessing, evaluation_metrics +import os +import json +from sklearn.model_selection import train_test_split +from tensorflow.keras.optimizers import Adam +from tensorflow.keras.metrics import Mean +from tensorflow.keras.callbacks import ReduceLROnPlateau, EarlyStopping, ModelCheckpoint +from wandb.integration.keras import WandbMetricsLogger +import wandb -# Load and preprocess data -data = data_preprocessing.load_data('path/to/dataset') -triplets = data_preprocessing.create_triplets(data) +from deeptuner.backbones.resnet import ResNetBackbone +from deeptuner.architectures.siamese import SiameseArchitecture +from deeptuner.losses.triplet_loss import triplet_loss +from deeptuner.datagenerators.triplet_data_generator import TripletDataGenerator +from deeptuner.callbacks.finetune_callback import FineTuneCallback -# Initialize model backbone -model = backbones.get_model('resnet') +# Load configuration from JSON file +with open('config.json', 'r') as config_file: + config = json.load(config_file) -# Compile model with triplet loss -model.compile(optimizer='adam', loss=triplet_loss.triplet_loss) +data_dir = config['data_dir'] +image_size = tuple(config['image_size']) +batch_size = config['batch_size'] +margin = config['margin'] +epochs = config['epochs'] +initial_epoch = config['initial_epoch'] +learning_rate = config['learning_rate'] +patience = config['patience'] +unfreeze_layers = config['unfreeze_layers'] -# Train model -model.fit(triplets, epochs=10, batch_size=32) +# Initialize W&B +wandb.init(project=config['project_name'], config=config) -# Evaluate model -metrics = evaluation_metrics.evaluate_model(model, triplets) -print(metrics) +# Load and preprocess the data +image_paths = [] +labels = [] + +for label in os.listdir(data_dir): + label_dir = os.path.join(data_dir, label) + if os.path.isdir(label_dir): + for image_name in os.listdir(label_dir): + image_paths.append(os.path.join(label_dir, image_name)) + labels.append(label) + +# Debugging output +print(f"Found {len(image_paths)} images in {len(set(labels))} classes") + +# Split the data into training and validation sets +train_paths, val_paths, train_labels, val_labels = train_test_split( + image_paths, labels, test_size=0.2, stratify=labels, random_state=42 +) + +# Check if the splits are non-empty +print(f"Training on {len(train_paths)} images") +print(f"Validating on {len(val_paths)} images") + +# Create data generators +num_classes = len(set(labels)) +train_generator = TripletDataGenerator(train_paths, train_labels, batch_size, image_size, num_classes) +val_generator = TripletDataGenerator(val_paths, val_labels, batch_size, image_size, num_classes) + +# Check if the generators have data +assert len(train_generator) > 0, "Training generator is empty!" +assert len(val_generator) > 0, "Validation generator is empty!" + +# Create the embedding model and freeze layers +backbone = ResNetBackbone(input_shape=image_size + (3,)) +embedding_model = backbone.create_model() + +# Freeze all layers initially +for layer in embedding_model.layers: + layer.trainable = False +# Unfreeze last few layers +for layer in embedding_model.layers[-unfreeze_layers:]: + layer.trainable = True + +# Create the siamese network +siamese_architecture = SiameseArchitecture(input_shape=image_size + (3,), embedding_model=embedding_model) +siamese_network = siamese_architecture.create_siamese_network() + +# Initialize the Siamese model +loss_tracker = Mean(name="loss") +siamese_model = SiameseModel(siamese_network, margin, loss_tracker) + +# Set up callbacks +reduce_lr = ReduceLROnPlateau(monitor='val_loss', factor=0.1, patience=3, min_lr=1e-7, verbose=1) +early_stopping = EarlyStopping(monitor='val_loss', patience=10, restore_best_weights=True, verbose=1) +model_checkpoint = ModelCheckpoint( + "models/best_siamese_model.weights.h5", + save_best_only=True, + save_weights_only=True, + monitor='val_loss', + verbose=1 +) +embedding_checkpoint = ModelCheckpoint( + "models/best_embedding_model.weights.h5", + save_best_only=True, + save_weights_only=True, + monitor='val_loss', + verbose=1 +) +fine_tune_callback = FineTuneCallback(embedding_model, patience=patience, unfreeze_layers=unfreeze_layers) + +# Create models directory if it doesn't exist +os.makedirs('models', exist_ok=True) + +# Compile the model +siamese_model.compile(optimizer=Adam(learning_rate=learning_rate), loss=triplet_loss(margin=margin)) + +# Train the model +history = siamese_model.fit( + train_generator, + validation_data=val_generator, + epochs=epochs, + initial_epoch=initial_epoch, + callbacks=[ + reduce_lr, + early_stopping, + model_checkpoint, + embedding_checkpoint, + fine_tune_callback, + WandbMetricsLogger(log_freq=5) + ] +) + +# Save the final embedding model +embedding_model.save('models/final_embedding_model.h5') +``` + +### Using Configuration Files + +To make it easier to experiment with different hyperparameter settings, you can use a configuration file (e.g., JSON) to store hyperparameters. Here is an example of a configuration file (`config.json`): + +```json +{ + "data_dir": "path/to/your/dataset", + "image_size": [224, 224], + "batch_size": 32, + "margin": 1.0, + "epochs": 50, + "initial_epoch": 0, + "learning_rate": 0.001, + "patience": 5, + "unfreeze_layers": 10, + "project_name": "DeepTuner" +} ``` -For more detailed usage and examples, please refer to the documentation. +You can then load this configuration file in your code as shown in the usage example above. diff --git a/deeptune/architectures/siamese.py b/deeptune/architectures/siamese.py new file mode 100644 index 0000000..3c76e90 --- /dev/null +++ b/deeptune/architectures/siamese.py @@ -0,0 +1,19 @@ +from tensorflow.keras import layers, Model, Input + +class SiameseArchitecture: + def __init__(self, input_shape, embedding_model): + self.input_shape = input_shape + self.embedding_model = embedding_model + + def create_siamese_network(self): + anchor_input = Input(name="anchor", shape=self.input_shape) + positive_input = Input(name="positive", shape=self.input_shape) + negative_input = Input(name="negative", shape=self.input_shape) + + anchor_embedding = self.embedding_model(anchor_input) + positive_embedding = self.embedding_model(positive_input) + negative_embedding = self.embedding_model(negative_input) + + outputs = [anchor_embedding, positive_embedding, negative_embedding] + model = Model(inputs=[anchor_input, positive_input, negative_input], outputs=outputs, name="siamese_network") + return model diff --git a/deeptune/backbones/resnet.py b/deeptune/backbones/resnet.py new file mode 100644 index 0000000..984f6e2 --- /dev/null +++ b/deeptune/backbones/resnet.py @@ -0,0 +1,27 @@ +from tensorflow.keras.applications import ResNet50 +from tensorflow.keras.layers import GlobalAveragePooling2D, Dense, Dropout, BatchNormalization +from tensorflow.keras.models import Model +from tensorflow.keras import Input + +class ResNetBackbone: + def __init__(self, input_shape=(224, 224, 3), weights='imagenet'): + self.input_shape = input_shape + self.weights = weights + + def create_model(self): + inputs = Input(shape=self.input_shape) + base_model = ResNet50(weights=self.weights, include_top=False, input_tensor=inputs) + x = base_model.output + x = GlobalAveragePooling2D()(x) + x = Dense(1024, activation='relu')(x) + x = Dropout(0.2)(x) + x = BatchNormalization()(x) + x = Dense(512, activation='relu')(x) + x = Dropout(0.2)(x) + x = BatchNormalization()(x) + x = Dense(256, activation='relu')(x) + x = Dropout(0.2)(x) + x = BatchNormalization()(x) + outputs = Dense(128)(x) + model = Model(inputs, outputs, name='resnet_backbone') + return model diff --git a/deeptune/callbacks/finetune_callback.py b/deeptune/callbacks/finetune_callback.py new file mode 100644 index 0000000..2586c01 --- /dev/null +++ b/deeptune/callbacks/finetune_callback.py @@ -0,0 +1,32 @@ +import tensorflow as tf +from tensorflow.keras.callbacks import Callback +from tensorflow.keras.optimizers import Adam + +class FineTuneCallback(Callback): + def __init__(self, base_model, patience=5, unfreeze_layers=10): + super(FineTuneCallback, self).__init__() + self.base_model = base_model + self.patience = patience + self.unfreeze_layers = unfreeze_layers + self.best_weights = None + self.best_loss = float('inf') + self.wait = 0 + + def on_epoch_end(self, epoch, logs=None): + current_loss = logs.get('val_loss') + if current_loss < self.best_loss: + self.best_loss = current_loss + self.best_weights = self.model.get_weights() + self.wait = 0 + else: + self.wait += 1 + if self.wait >= self.patience: + # Restore the best weights + self.model.set_weights(self.best_weights) + self.wait = 0 + # Unfreeze the last few layers + for layer in self.base_model.layers[-self.unfreeze_layers:]: + if hasattr(layer, 'trainable'): + layer.trainable = True + # Recompile the model to apply the changes + self.model.compile(optimizer=Adam(learning_rate=1e-5)) diff --git a/deeptune/datagenerators/triplet_data_generator.py b/deeptune/datagenerators/triplet_data_generator.py new file mode 100644 index 0000000..18523e7 --- /dev/null +++ b/deeptune/datagenerators/triplet_data_generator.py @@ -0,0 +1,71 @@ +import tensorflow as tf +from tensorflow.keras.preprocessing.image import ImageDataGenerator, load_img, img_to_array +from sklearn.preprocessing import LabelEncoder +import numpy as np +from tensorflow.keras.applications import resnet50 as resnet + +class TripletDataGenerator(tf.keras.utils.Sequence): + def __init__(self, image_paths, labels, batch_size, image_size, num_classes): + self.image_paths = image_paths + self.labels = labels + self.batch_size = batch_size + self.image_size = image_size + self.num_classes = num_classes + self.label_encoder = LabelEncoder() + self.encoded_labels = self.label_encoder.fit_transform(labels) + self.image_data_generator = ImageDataGenerator(preprocessing_function=resnet.preprocess_input) + self.on_epoch_end() + print(f"Initialized TripletDataGenerator with {len(self.image_paths)} images") + + def __len__(self): + return max(1, len(self.image_paths) // self.batch_size) # Ensure at least one batch + + def __getitem__(self, index): + batch_image_paths = self.image_paths[index * self.batch_size:(index + 1) * self.batch_size] + batch_labels = self.encoded_labels[index * self.batch_size:(index + 1) * self.batch_size] + return self._generate_triplet_batch(batch_image_paths, batch_labels) + + def on_epoch_end(self): + # Shuffle the data at the end of each epoch + combined = list(zip(self.image_paths, self.encoded_labels)) + np.random.shuffle(combined) + self.image_paths[:], self.encoded_labels[:] = zip(*combined) + + def _generate_triplet_batch(self, batch_image_paths, batch_labels): + anchor_images = [] + positive_images = [] + negative_images = [] + + for i in range(len(batch_image_paths)): + anchor_path = batch_image_paths[i] + anchor_label = batch_labels[i] + + positive_path = np.random.choice( + [p for p, l in zip(self.image_paths, self.encoded_labels) if l == anchor_label] + ) + negative_path = np.random.choice( + [p for p, l in zip(self.image_paths, self.encoded_labels) if l != anchor_label] + ) + + anchor_image = load_img(anchor_path, target_size=self.image_size) + positive_image = load_img(positive_path, target_size=self.image_size) + negative_image = load_img(negative_path, target_size=self.image_size) + + anchor_images.append(img_to_array(anchor_image)) + positive_images.append(img_to_array(positive_image)) + negative_images.append(img_to_array(negative_image)) + + # Convert lists to numpy arrays + anchor_array = np.array(anchor_images) + positive_array = np.array(positive_images) + negative_array = np.array(negative_images) + + # Return inputs and dummy targets (zeros) since the loss is computed in the model + return ( + { + "anchor": anchor_array, + "positive": positive_array, + "negative": negative_array + }, + np.zeros((len(batch_image_paths),)) # Dummy target values + ) diff --git a/deeptune/losses/arcface_loss.py b/deeptune/losses/arcface_loss.py new file mode 100644 index 0000000..64ac43f --- /dev/null +++ b/deeptune/losses/arcface_loss.py @@ -0,0 +1,16 @@ +import tensorflow as tf +from tensorflow.keras import backend as K + +def arcface_loss(y_true, y_pred, scale=64.0, margin=0.5): + y_true = tf.cast(y_true, dtype=tf.int32) + y_true = tf.one_hot(y_true, depth=y_pred.shape[-1]) + + cos_theta = y_pred + theta = tf.acos(K.clip(cos_theta, -1.0 + K.epsilon(), 1.0 - K.epsilon())) + target_logits = tf.cos(theta + margin) + + logits = y_true * target_logits + (1 - y_true) * cos_theta + logits *= scale + + loss = tf.nn.softmax_cross_entropy_with_logits(labels=y_true, logits=logits) + return tf.reduce_mean(loss) diff --git a/deeptune/losses/triplet_loss.py b/deeptune/losses/triplet_loss.py new file mode 100644 index 0000000..2863f7d --- /dev/null +++ b/deeptune/losses/triplet_loss.py @@ -0,0 +1,11 @@ +import tensorflow as tf + +def triplet_loss(margin=1.0): + def loss(y_true, y_pred): + anchor, positive, negative = y_pred[0], y_pred[1], y_pred[2] + pos_dist = tf.reduce_sum(tf.square(anchor - positive), axis=-1) + neg_dist = tf.reduce_sum(tf.square(anchor - negative), axis=-1) + basic_loss = pos_dist - neg_dist + margin + loss = tf.reduce_mean(tf.maximum(basic_loss, 0.0), axis=0) + return loss + return loss diff --git a/deeptune/utils/helpers.py b/deeptune/utils/helpers.py new file mode 100644 index 0000000..62e8359 --- /dev/null +++ b/deeptune/utils/helpers.py @@ -0,0 +1,60 @@ +from tensorflow.keras.applications import resnet +from tensorflow.keras import layers +from tensorflow import keras +import tensorflow as tf + +def get_embedding_module(imageSize): + """ + Creates an embedding module based on ResNet50 + """ + # construct the input layer and pass the inputs through a + # pre-processing layer + inputs = keras.Input(imageSize + (3,)) + x = resnet.preprocess_input(inputs) + + # fetch the pre-trained resnet 50 model and freeze the weights + baseCnn = resnet.ResNet50(weights="imagenet", include_top=False) + baseCnn.trainable=False + + # pass the pre-processed inputs through the base cnn and get the + # extracted features from the inputs + extractedFeatures = baseCnn(x) + # pass the extracted features through a number of trainable layers + x = layers.GlobalAveragePooling2D()(extractedFeatures) + x = layers.Dense(units=1024, activation="relu")(x) + x = layers.Dropout(0.2)(x) + x = layers.BatchNormalization()(x) + x = layers.Dense(units=512, activation="relu")(x) + x = layers.Dropout(0.2)(x) + x = layers.BatchNormalization()(x) + x = layers.Dense(units=256, activation="relu")(x) + x = layers.Dropout(0.2)(x) + outputs = layers.Dense(units=128)(x) + # build the embedding model and return it + embedding = keras.Model(inputs, outputs, name="embedding") + return embedding + +def get_siamese_network(imageSize, embedding_model): + """ + Creates a siamese network using the provided embedding module + Args: + imageSize: tuple of image dimensions (height, width) + embedding_model: pre-trained embedding model to use + """ + # build the anchor, positive and negative input layer + anchorInput = keras.Input(name="anchor", shape=imageSize + (3,)) + positiveInput = keras.Input(name="positive", shape=imageSize + (3,)) + negativeInput = keras.Input(name="negative", shape=imageSize + (3,)) + + # embed the anchor, positive and negative images + anchorEmbedding = embedding_model(anchorInput) + positiveEmbedding = embedding_model(positiveInput) + negativeEmbedding = embedding_model(negativeInput) + + # build the siamese network and return it + outputs = [anchorEmbedding, positiveEmbedding, negativeEmbedding] + return keras.Model( + inputs=[anchorInput, positiveInput, negativeInput], + outputs=outputs, + name="siamese_network" + ) diff --git a/deeptuner/run.py b/deeptuner/run.py index 45296ae..1a32432 100644 --- a/deeptuner/run.py +++ b/deeptuner/run.py @@ -1,39 +1,35 @@ -from wandb.integration.keras import WandbMetricsLogger, WandbModelCheckpoint -import wandb import os +import json from sklearn.model_selection import train_test_split -from keras.optimizers import Adam -from keras.metrics import Mean -from keras.layers import Input -from keras.models import Model -from keras.applications.resnet50 import ResNet50 -from keras.applications.resnet50 import preprocess_input -from keras.callbacks import ReduceLROnPlateau, EarlyStopping, ModelCheckpoint -from deeptuner.utils import get_embedding_module, get_siamese_network -from deeptuner.siamese_network import SiameseModel -from deeptuner.tripletdatagenerator import TripletDataGenerator - -from tensorflow import keras -from tensorflow.keras import layers -from tensorflow.keras.layers import Input, Lambda, Dense, Flatten -from tensorflow.keras.models import Model - -data_dir = 'datasets/lfw_processed' -image_size = (224, 224) -batch_size = 2 # Adjust the batch size for the small dataset -margin = 10.0 +from tensorflow.keras.optimizers import Adam +from tensorflow.keras.metrics import Mean +from tensorflow.keras.callbacks import ReduceLROnPlateau, EarlyStopping, ModelCheckpoint +from wandb.integration.keras import WandbMetricsLogger +import wandb + +from deeptuner.backbones.resnet import ResNetBackbone +from deeptuner.architectures.siamese import SiameseArchitecture +from deeptuner.losses.triplet_loss import triplet_loss +from deeptuner.datagenerators.triplet_data_generator import TripletDataGenerator +from deeptuner.callbacks.finetune_callback import FineTuneCallback + +# Load configuration from JSON file +with open('config.json', 'r') as config_file: + config = json.load(config_file) + +data_dir = config['data_dir'] +image_size = tuple(config['image_size']) +batch_size = config['batch_size'] +margin = config['margin'] +epochs = config['epochs'] +initial_epoch = config['initial_epoch'] +learning_rate = config['learning_rate'] +patience = config['patience'] +unfreeze_layers = config['unfreeze_layers'] # Initialize W&B -wandb.init(project="FaceRec", config={ - "learning_rate": 0.001, - "epochs": 20, - "batch_size": 2, - "optimizer": "Adam", - "architecture": "ResNet50", - "dataset": "lfw", - "loss": "TripletLoss", - "margin": 10.0 -}) +wandb.init(project=config['project_name'], config=config) + # Load and preprocess the data image_paths = [] labels = [] @@ -66,46 +62,20 @@ assert len(train_generator) > 0, "Training generator is empty!" assert len(val_generator) > 0, "Validation generator is empty!" -class FineTuneCallback(keras.callbacks.Callback): - def __init__(self, base_model, patience=5, unfreeze_layers=10): - super(FineTuneCallback, self).__init__() - self.base_model = base_model - self.patience = patience - self.unfreeze_layers = unfreeze_layers - self.best_weights = None - self.best_loss = float('inf') - self.wait = 0 - - def on_epoch_end(self, epoch, logs=None): - current_loss = logs.get('val_loss') - if current_loss < self.best_loss: - self.best_loss = current_loss - self.best_weights = self.model.get_weights() - self.wait = 0 - else: - self.wait += 1 - if self.wait >= self.patience: - # Restore the best weights - self.model.set_weights(self.best_weights) - self.wait = 0 - # Unfreeze the last few layers - for layer in self.base_model.layers[-self.unfreeze_layers:]: - if hasattr(layer, 'trainable'): - layer.trainable = True - # Recompile the model to apply the changes - self.model.compile(optimizer=Adam(learning_rate=1e-5)) - # Create the embedding model and freeze layers -embedding_model = get_embedding_module(image_size) +backbone = ResNetBackbone(input_shape=image_size + (3,)) +embedding_model = backbone.create_model() + # Freeze all layers initially for layer in embedding_model.layers: layer.trainable = False -# Unfreeze last 20 layers -for layer in embedding_model.layers[-20:]: +# Unfreeze last few layers +for layer in embedding_model.layers[-unfreeze_layers:]: layer.trainable = True # Create the siamese network -siamese_network = get_siamese_network(image_size, embedding_model) +siamese_architecture = SiameseArchitecture(input_shape=image_size + (3,), embedding_model=embedding_model) +siamese_network = siamese_architecture.create_siamese_network() # Initialize the Siamese model loss_tracker = Mean(name="loss") @@ -128,20 +98,20 @@ def on_epoch_end(self, epoch, logs=None): monitor='val_loss', verbose=1 ) -fine_tune_callback = FineTuneCallback(embedding_model, patience=5, unfreeze_layers=10) +fine_tune_callback = FineTuneCallback(embedding_model, patience=patience, unfreeze_layers=unfreeze_layers) # Create models directory if it doesn't exist os.makedirs('models', exist_ok=True) # Compile the model -siamese_model.compile(optimizer=Adam()) +siamese_model.compile(optimizer=Adam(learning_rate=learning_rate), loss=triplet_loss(margin=margin)) # Train the model history = siamese_model.fit( train_generator, validation_data=val_generator, - epochs=40, - initial_epoch=20, + epochs=epochs, + initial_epoch=initial_epoch, callbacks=[ reduce_lr, early_stopping, @@ -153,4 +123,4 @@ def on_epoch_end(self, epoch, logs=None): ) # Save the final embedding model -embedding_model.save('models/final_embedding_model.h5') \ No newline at end of file +embedding_model.save('models/final_embedding_model.h5') diff --git a/examples/train_siamese.py b/examples/train_siamese.py new file mode 100644 index 0000000..085733a --- /dev/null +++ b/examples/train_siamese.py @@ -0,0 +1,146 @@ +import os +import json +from sklearn.model_selection import train_test_split +from tensorflow.keras.optimizers import Adam +from tensorflow.keras.metrics import Mean +from tensorflow.keras.callbacks import ReduceLROnPlateau, EarlyStopping, ModelCheckpoint +from wandb.integration.keras import WandbMetricsLogger +import wandb + +from deeptuner.backbones.resnet import ResNetBackbone +from deeptuner.architectures.siamese import SiameseArchitecture +from deeptuner.losses.triplet_loss import triplet_loss +from deeptuner.datagenerators.triplet_data_generator import TripletDataGenerator +from deeptuner.callbacks.finetune_callback import FineTuneCallback + +# Load configuration from JSON file +with open('config.json', 'r') as config_file: + config = json.load(config_file) + +data_dir = config['data_dir'] +image_size = tuple(config['image_size']) +batch_size = config['batch_size'] +margin = config['margin'] +epochs = config['epochs'] +initial_epoch = config['initial_epoch'] +learning_rate = config['learning_rate'] +patience = config['patience'] +unfreeze_layers = config['unfreeze_layers'] + +# Initialize W&B +wandb.init(project=config['project_name'], config=config) + +# Load and preprocess the data +image_paths = [] +labels = [] + +for label in os.listdir(data_dir): + label_dir = os.path.join(data_dir, label) + if os.path.isdir(label_dir): + for image_name in os.listdir(label_dir): + image_paths.append(os.path.join(label_dir, image_name)) + labels.append(label) + +# Debugging output +print(f"Found {len(image_paths)} images in {len(set(labels))} classes") + +# Split the data into training and validation sets +train_paths, val_paths, train_labels, val_labels = train_test_split( + image_paths, labels, test_size=0.2, stratify=labels, random_state=42 +) + +# Check if the splits are non-empty +print(f"Training on {len(train_paths)} images") +print(f"Validating on {len(val_paths)} images") + +# Create data generators +num_classes = len(set(labels)) +train_generator = TripletDataGenerator(train_paths, train_labels, batch_size, image_size, num_classes) +val_generator = TripletDataGenerator(val_paths, val_labels, batch_size, image_size, num_classes) + +# Check if the generators have data +assert len(train_generator) > 0, "Training generator is empty!" +assert len(val_generator) > 0, "Validation generator is empty!" + +# Create the embedding model and freeze layers +backbone = ResNetBackbone(input_shape=image_size + (3,)) +embedding_model = backbone.create_model() + +# Freeze all layers initially +for layer in embedding_model.layers: + layer.trainable = False +# Unfreeze last few layers +for layer in embedding_model.layers[-unfreeze_layers:]: + layer.trainable = True + +# Create the siamese network +siamese_architecture = SiameseArchitecture(input_shape=image_size + (3,), embedding_model=embedding_model) +siamese_network = siamese_architecture.create_siamese_network() + +# Initialize the Siamese model +loss_tracker = Mean(name="loss") +siamese_model = SiameseModel(siamese_network, margin, loss_tracker) + +# Set up callbacks +reduce_lr = ReduceLROnPlateau(monitor='val_loss', factor=0.1, patience=3, min_lr=1e-7, verbose=1) +early_stopping = EarlyStopping(monitor='val_loss', patience=10, restore_best_weights=True, verbose=1) +model_checkpoint = ModelCheckpoint( + "models/best_siamese_model.weights.h5", + save_best_only=True, + save_weights_only=True, + monitor='val_loss', + verbose=1 +) +embedding_checkpoint = ModelCheckpoint( + "models/best_embedding_model.weights.h5", + save_best_only=True, + save_weights_only=True, + monitor='val_loss', + verbose=1 +) +fine_tune_callback = FineTuneCallback(embedding_model, patience=patience, unfreeze_layers=unfreeze_layers) + +# Create models directory if it doesn't exist +os.makedirs('models', exist_ok=True) + +# Compile the model +siamese_model.compile(optimizer=Adam(learning_rate=learning_rate), loss=triplet_loss(margin=margin)) + +# Train the model +history = siamese_model.fit( + train_generator, + validation_data=val_generator, + epochs=epochs, + initial_epoch=initial_epoch, + callbacks=[ + reduce_lr, + early_stopping, + model_checkpoint, + embedding_checkpoint, + fine_tune_callback, + WandbMetricsLogger(log_freq=5) + ] +) + +# Save the final embedding model +embedding_model.save('models/final_embedding_model.h5') + +# Function to load LFW dataset +def load_lfw_dataset(data_dir): + image_paths = [] + labels = [] + + for label in os.listdir(data_dir): + label_dir = os.path.join(data_dir, label) + if os.path.isdir(label_dir): + for image_name in os.listdir(label_dir): + image_paths.append(os.path.join(label_dir, image_name)) + labels.append(label) + + return image_paths, labels + +# Example usage +if __name__ == "__main__": + data_dir = "path/to/your/dataset" + image_paths, labels = load_lfw_dataset(data_dir) + print(f"Loaded {len(image_paths)} images from LFW dataset") diff --git a/requirements.txt b/requirements.txt index ad87b1e..b58735c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,4 +6,4 @@ wandb>=0.15.0 efficientnet>=1.0.0 facenet-pytorch>=2.5.0 requests>=2.31.0 -tqdm>=4.65.0 \ No newline at end of file +tqdm>=4.65.0 From 106a789d0052c8fd44aedbabe63246d9a75dd849 Mon Sep 17 00:00:00 2001 From: Devasy Patel <110348311+Devasy23@users.noreply.github.com> Date: Sun, 24 Nov 2024 14:27:37 +0530 Subject: [PATCH 5/9] updated code --- deeptune/architectures/siamese.py | 19 ----- deeptune/backbones/resnet.py | 27 ------- deeptune/callbacks/finetune_callback.py | 32 --------- .../datagenerators/triplet_data_generator.py | 71 ------------------- deeptune/losses/arcface_loss.py | 16 ----- deeptune/losses/triplet_loss.py | 11 --- deeptune/utils/helpers.py | 60 ---------------- 7 files changed, 236 deletions(-) delete mode 100644 deeptune/architectures/siamese.py delete mode 100644 deeptune/backbones/resnet.py delete mode 100644 deeptune/callbacks/finetune_callback.py delete mode 100644 deeptune/datagenerators/triplet_data_generator.py delete mode 100644 deeptune/losses/arcface_loss.py delete mode 100644 deeptune/losses/triplet_loss.py delete mode 100644 deeptune/utils/helpers.py diff --git a/deeptune/architectures/siamese.py b/deeptune/architectures/siamese.py deleted file mode 100644 index 3c76e90..0000000 --- a/deeptune/architectures/siamese.py +++ /dev/null @@ -1,19 +0,0 @@ -from tensorflow.keras import layers, Model, Input - -class SiameseArchitecture: - def __init__(self, input_shape, embedding_model): - self.input_shape = input_shape - self.embedding_model = embedding_model - - def create_siamese_network(self): - anchor_input = Input(name="anchor", shape=self.input_shape) - positive_input = Input(name="positive", shape=self.input_shape) - negative_input = Input(name="negative", shape=self.input_shape) - - anchor_embedding = self.embedding_model(anchor_input) - positive_embedding = self.embedding_model(positive_input) - negative_embedding = self.embedding_model(negative_input) - - outputs = [anchor_embedding, positive_embedding, negative_embedding] - model = Model(inputs=[anchor_input, positive_input, negative_input], outputs=outputs, name="siamese_network") - return model diff --git a/deeptune/backbones/resnet.py b/deeptune/backbones/resnet.py deleted file mode 100644 index 984f6e2..0000000 --- a/deeptune/backbones/resnet.py +++ /dev/null @@ -1,27 +0,0 @@ -from tensorflow.keras.applications import ResNet50 -from tensorflow.keras.layers import GlobalAveragePooling2D, Dense, Dropout, BatchNormalization -from tensorflow.keras.models import Model -from tensorflow.keras import Input - -class ResNetBackbone: - def __init__(self, input_shape=(224, 224, 3), weights='imagenet'): - self.input_shape = input_shape - self.weights = weights - - def create_model(self): - inputs = Input(shape=self.input_shape) - base_model = ResNet50(weights=self.weights, include_top=False, input_tensor=inputs) - x = base_model.output - x = GlobalAveragePooling2D()(x) - x = Dense(1024, activation='relu')(x) - x = Dropout(0.2)(x) - x = BatchNormalization()(x) - x = Dense(512, activation='relu')(x) - x = Dropout(0.2)(x) - x = BatchNormalization()(x) - x = Dense(256, activation='relu')(x) - x = Dropout(0.2)(x) - x = BatchNormalization()(x) - outputs = Dense(128)(x) - model = Model(inputs, outputs, name='resnet_backbone') - return model diff --git a/deeptune/callbacks/finetune_callback.py b/deeptune/callbacks/finetune_callback.py deleted file mode 100644 index 2586c01..0000000 --- a/deeptune/callbacks/finetune_callback.py +++ /dev/null @@ -1,32 +0,0 @@ -import tensorflow as tf -from tensorflow.keras.callbacks import Callback -from tensorflow.keras.optimizers import Adam - -class FineTuneCallback(Callback): - def __init__(self, base_model, patience=5, unfreeze_layers=10): - super(FineTuneCallback, self).__init__() - self.base_model = base_model - self.patience = patience - self.unfreeze_layers = unfreeze_layers - self.best_weights = None - self.best_loss = float('inf') - self.wait = 0 - - def on_epoch_end(self, epoch, logs=None): - current_loss = logs.get('val_loss') - if current_loss < self.best_loss: - self.best_loss = current_loss - self.best_weights = self.model.get_weights() - self.wait = 0 - else: - self.wait += 1 - if self.wait >= self.patience: - # Restore the best weights - self.model.set_weights(self.best_weights) - self.wait = 0 - # Unfreeze the last few layers - for layer in self.base_model.layers[-self.unfreeze_layers:]: - if hasattr(layer, 'trainable'): - layer.trainable = True - # Recompile the model to apply the changes - self.model.compile(optimizer=Adam(learning_rate=1e-5)) diff --git a/deeptune/datagenerators/triplet_data_generator.py b/deeptune/datagenerators/triplet_data_generator.py deleted file mode 100644 index 18523e7..0000000 --- a/deeptune/datagenerators/triplet_data_generator.py +++ /dev/null @@ -1,71 +0,0 @@ -import tensorflow as tf -from tensorflow.keras.preprocessing.image import ImageDataGenerator, load_img, img_to_array -from sklearn.preprocessing import LabelEncoder -import numpy as np -from tensorflow.keras.applications import resnet50 as resnet - -class TripletDataGenerator(tf.keras.utils.Sequence): - def __init__(self, image_paths, labels, batch_size, image_size, num_classes): - self.image_paths = image_paths - self.labels = labels - self.batch_size = batch_size - self.image_size = image_size - self.num_classes = num_classes - self.label_encoder = LabelEncoder() - self.encoded_labels = self.label_encoder.fit_transform(labels) - self.image_data_generator = ImageDataGenerator(preprocessing_function=resnet.preprocess_input) - self.on_epoch_end() - print(f"Initialized TripletDataGenerator with {len(self.image_paths)} images") - - def __len__(self): - return max(1, len(self.image_paths) // self.batch_size) # Ensure at least one batch - - def __getitem__(self, index): - batch_image_paths = self.image_paths[index * self.batch_size:(index + 1) * self.batch_size] - batch_labels = self.encoded_labels[index * self.batch_size:(index + 1) * self.batch_size] - return self._generate_triplet_batch(batch_image_paths, batch_labels) - - def on_epoch_end(self): - # Shuffle the data at the end of each epoch - combined = list(zip(self.image_paths, self.encoded_labels)) - np.random.shuffle(combined) - self.image_paths[:], self.encoded_labels[:] = zip(*combined) - - def _generate_triplet_batch(self, batch_image_paths, batch_labels): - anchor_images = [] - positive_images = [] - negative_images = [] - - for i in range(len(batch_image_paths)): - anchor_path = batch_image_paths[i] - anchor_label = batch_labels[i] - - positive_path = np.random.choice( - [p for p, l in zip(self.image_paths, self.encoded_labels) if l == anchor_label] - ) - negative_path = np.random.choice( - [p for p, l in zip(self.image_paths, self.encoded_labels) if l != anchor_label] - ) - - anchor_image = load_img(anchor_path, target_size=self.image_size) - positive_image = load_img(positive_path, target_size=self.image_size) - negative_image = load_img(negative_path, target_size=self.image_size) - - anchor_images.append(img_to_array(anchor_image)) - positive_images.append(img_to_array(positive_image)) - negative_images.append(img_to_array(negative_image)) - - # Convert lists to numpy arrays - anchor_array = np.array(anchor_images) - positive_array = np.array(positive_images) - negative_array = np.array(negative_images) - - # Return inputs and dummy targets (zeros) since the loss is computed in the model - return ( - { - "anchor": anchor_array, - "positive": positive_array, - "negative": negative_array - }, - np.zeros((len(batch_image_paths),)) # Dummy target values - ) diff --git a/deeptune/losses/arcface_loss.py b/deeptune/losses/arcface_loss.py deleted file mode 100644 index 64ac43f..0000000 --- a/deeptune/losses/arcface_loss.py +++ /dev/null @@ -1,16 +0,0 @@ -import tensorflow as tf -from tensorflow.keras import backend as K - -def arcface_loss(y_true, y_pred, scale=64.0, margin=0.5): - y_true = tf.cast(y_true, dtype=tf.int32) - y_true = tf.one_hot(y_true, depth=y_pred.shape[-1]) - - cos_theta = y_pred - theta = tf.acos(K.clip(cos_theta, -1.0 + K.epsilon(), 1.0 - K.epsilon())) - target_logits = tf.cos(theta + margin) - - logits = y_true * target_logits + (1 - y_true) * cos_theta - logits *= scale - - loss = tf.nn.softmax_cross_entropy_with_logits(labels=y_true, logits=logits) - return tf.reduce_mean(loss) diff --git a/deeptune/losses/triplet_loss.py b/deeptune/losses/triplet_loss.py deleted file mode 100644 index 2863f7d..0000000 --- a/deeptune/losses/triplet_loss.py +++ /dev/null @@ -1,11 +0,0 @@ -import tensorflow as tf - -def triplet_loss(margin=1.0): - def loss(y_true, y_pred): - anchor, positive, negative = y_pred[0], y_pred[1], y_pred[2] - pos_dist = tf.reduce_sum(tf.square(anchor - positive), axis=-1) - neg_dist = tf.reduce_sum(tf.square(anchor - negative), axis=-1) - basic_loss = pos_dist - neg_dist + margin - loss = tf.reduce_mean(tf.maximum(basic_loss, 0.0), axis=0) - return loss - return loss diff --git a/deeptune/utils/helpers.py b/deeptune/utils/helpers.py deleted file mode 100644 index 62e8359..0000000 --- a/deeptune/utils/helpers.py +++ /dev/null @@ -1,60 +0,0 @@ -from tensorflow.keras.applications import resnet -from tensorflow.keras import layers -from tensorflow import keras -import tensorflow as tf - -def get_embedding_module(imageSize): - """ - Creates an embedding module based on ResNet50 - """ - # construct the input layer and pass the inputs through a - # pre-processing layer - inputs = keras.Input(imageSize + (3,)) - x = resnet.preprocess_input(inputs) - - # fetch the pre-trained resnet 50 model and freeze the weights - baseCnn = resnet.ResNet50(weights="imagenet", include_top=False) - baseCnn.trainable=False - - # pass the pre-processed inputs through the base cnn and get the - # extracted features from the inputs - extractedFeatures = baseCnn(x) - # pass the extracted features through a number of trainable layers - x = layers.GlobalAveragePooling2D()(extractedFeatures) - x = layers.Dense(units=1024, activation="relu")(x) - x = layers.Dropout(0.2)(x) - x = layers.BatchNormalization()(x) - x = layers.Dense(units=512, activation="relu")(x) - x = layers.Dropout(0.2)(x) - x = layers.BatchNormalization()(x) - x = layers.Dense(units=256, activation="relu")(x) - x = layers.Dropout(0.2)(x) - outputs = layers.Dense(units=128)(x) - # build the embedding model and return it - embedding = keras.Model(inputs, outputs, name="embedding") - return embedding - -def get_siamese_network(imageSize, embedding_model): - """ - Creates a siamese network using the provided embedding module - Args: - imageSize: tuple of image dimensions (height, width) - embedding_model: pre-trained embedding model to use - """ - # build the anchor, positive and negative input layer - anchorInput = keras.Input(name="anchor", shape=imageSize + (3,)) - positiveInput = keras.Input(name="positive", shape=imageSize + (3,)) - negativeInput = keras.Input(name="negative", shape=imageSize + (3,)) - - # embed the anchor, positive and negative images - anchorEmbedding = embedding_model(anchorInput) - positiveEmbedding = embedding_model(positiveInput) - negativeEmbedding = embedding_model(negativeInput) - - # build the siamese network and return it - outputs = [anchorEmbedding, positiveEmbedding, negativeEmbedding] - return keras.Model( - inputs=[anchorInput, positiveInput, negativeInput], - outputs=outputs, - name="siamese_network" - ) From d54ab469102074f325e5e7668599f637c69a43cb Mon Sep 17 00:00:00 2001 From: Devasy Patel <110348311+Devasy23@users.noreply.github.com> Date: Mon, 25 Nov 2024 20:16:53 +0530 Subject: [PATCH 6/9] updated code --- MANIFEST.in | 8 ++++++++ config.json | 12 ++++++++++++ deeptuner/__init__.py | 5 +++++ deeptuner/architectures/__init__.py | 7 +++++++ deeptuner/backbones/__init__.py | 7 +++++++ deeptuner/callbacks/finetune_callback.py | 11 ++++++++--- deeptuner/losses/__init__.py | 8 ++++++++ deeptuner/utils/__init__.py | 0 pyproject.toml | 14 ++++++++++++++ deeptuner/run.py => run.py | 23 ++++++----------------- setup.py | 2 +- 11 files changed, 76 insertions(+), 21 deletions(-) create mode 100644 MANIFEST.in create mode 100644 config.json create mode 100644 deeptuner/__init__.py create mode 100644 deeptuner/architectures/__init__.py create mode 100644 deeptuner/backbones/__init__.py create mode 100644 deeptuner/losses/__init__.py create mode 100644 deeptuner/utils/__init__.py create mode 100644 pyproject.toml rename deeptuner/run.py => run.py (87%) diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..13faa8d --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,8 @@ +include LICENSE +include README.md +include requirements.txt +include config.json +recursive-include examples * +recursive-include deeptuner * +global-exclude __pycache__ +global-exclude *.py[cod] diff --git a/config.json b/config.json new file mode 100644 index 0000000..919150d --- /dev/null +++ b/config.json @@ -0,0 +1,12 @@ +{ + "data_dir": "datasets/lfw_processed", + "image_size": [224, 224], + "batch_size": 32, + "margin": 10.0, + "epochs": 50, + "initial_epoch": 0, + "learning_rate": 0.001, + "patience": 5, + "unfreeze_layers": 10, + "project_name": "DeepTuner" +} \ No newline at end of file diff --git a/deeptuner/__init__.py b/deeptuner/__init__.py new file mode 100644 index 0000000..af6ab6c --- /dev/null +++ b/deeptuner/__init__.py @@ -0,0 +1,5 @@ +""" +DeepTuner - A package for fine-tuning deep learning models with Siamese architecture and triplet loss +""" + +__version__ = '0.1.5' diff --git a/deeptuner/architectures/__init__.py b/deeptuner/architectures/__init__.py new file mode 100644 index 0000000..805ed7b --- /dev/null +++ b/deeptuner/architectures/__init__.py @@ -0,0 +1,7 @@ +""" +Neural network architectures for DeepTuner +""" + +from .siamese import SiameseArchitecture + +__all__ = ['SiameseArchitecture'] diff --git a/deeptuner/backbones/__init__.py b/deeptuner/backbones/__init__.py new file mode 100644 index 0000000..81e4cfc --- /dev/null +++ b/deeptuner/backbones/__init__.py @@ -0,0 +1,7 @@ +""" +Backbone models for DeepTuner +""" + +from .resnet import ResNetBackbone + +__all__ = ['ResNetBackbone'] diff --git a/deeptuner/callbacks/finetune_callback.py b/deeptuner/callbacks/finetune_callback.py index 2586c01..8df0fb1 100644 --- a/deeptuner/callbacks/finetune_callback.py +++ b/deeptuner/callbacks/finetune_callback.py @@ -3,11 +3,12 @@ from tensorflow.keras.optimizers import Adam class FineTuneCallback(Callback): - def __init__(self, base_model, patience=5, unfreeze_layers=10): + def __init__(self, base_model, patience=5, unfreeze_layers=10, margin=1.0): super(FineTuneCallback, self).__init__() self.base_model = base_model self.patience = patience self.unfreeze_layers = unfreeze_layers + self.margin = margin self.best_weights = None self.best_loss = float('inf') self.wait = 0 @@ -28,5 +29,9 @@ def on_epoch_end(self, epoch, logs=None): for layer in self.base_model.layers[-self.unfreeze_layers:]: if hasattr(layer, 'trainable'): layer.trainable = True - # Recompile the model to apply the changes - self.model.compile(optimizer=Adam(learning_rate=1e-5)) + # Recompile the model with both optimizer and loss function + from deeptuner.losses.triplet_loss import triplet_loss + self.model.compile( + optimizer=Adam(learning_rate=1e-6), + loss=triplet_loss(margin=self.margin) + ) diff --git a/deeptuner/losses/__init__.py b/deeptuner/losses/__init__.py new file mode 100644 index 0000000..c4c1a15 --- /dev/null +++ b/deeptuner/losses/__init__.py @@ -0,0 +1,8 @@ +""" +Loss functions for DeepTuner +""" + +from .triplet_loss import triplet_loss +from .arcface_loss import arcface_loss + +__all__ = ['triplet_loss', 'arcface_loss'] diff --git a/deeptuner/utils/__init__.py b/deeptuner/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..3c4345a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,14 @@ +[build-system] +requires = ["setuptools>=42", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "DeepTuner" +version = "0.1.5" +description = "A package for fine-tuning deep learning models with Siamese architecture and triplet loss" +readme = "README.md" +requires-python = ">=3.7" +license = {file = "LICENSE"} +authors = [ + {name = "Devasy Patel", email = "patel.devasy.23@gmail.com"} +] diff --git a/deeptuner/run.py b/run.py similarity index 87% rename from deeptuner/run.py rename to run.py index 1a32432..75da51a 100644 --- a/deeptuner/run.py +++ b/run.py @@ -7,6 +7,7 @@ from wandb.integration.keras import WandbMetricsLogger import wandb +from deeptuner.siamese_network import SiameseModel from deeptuner.backbones.resnet import ResNetBackbone from deeptuner.architectures.siamese import SiameseArchitecture from deeptuner.losses.triplet_loss import triplet_loss @@ -82,23 +83,11 @@ siamese_model = SiameseModel(siamese_network, margin, loss_tracker) # Set up callbacks -reduce_lr = ReduceLROnPlateau(monitor='val_loss', factor=0.1, patience=3, min_lr=1e-7, verbose=1) -early_stopping = EarlyStopping(monitor='val_loss', patience=10, restore_best_weights=True, verbose=1) -model_checkpoint = ModelCheckpoint( - "models/best_siamese_model.weights.h5", - save_best_only=True, - save_weights_only=True, - monitor='val_loss', - verbose=1 -) -embedding_checkpoint = ModelCheckpoint( - "models/best_embedding_model.weights.h5", - save_best_only=True, - save_weights_only=True, - monitor='val_loss', - verbose=1 -) -fine_tune_callback = FineTuneCallback(embedding_model, patience=patience, unfreeze_layers=unfreeze_layers) +reduce_lr = ReduceLROnPlateau(monitor='val_loss', factor=0.2, patience=5, min_lr=1e-6) +early_stopping = EarlyStopping(monitor='val_loss', patience=10, restore_best_weights=True) +model_checkpoint = ModelCheckpoint('models/best_model.h5', monitor='val_loss', save_best_only=True) +embedding_checkpoint = ModelCheckpoint('models/best_embedding_model.h5', monitor='val_loss', save_best_only=True) +fine_tune_callback = FineTuneCallback(embedding_model, patience=patience, unfreeze_layers=unfreeze_layers, margin=margin) # Create models directory if it doesn't exist os.makedirs('models', exist_ok=True) diff --git a/setup.py b/setup.py index 55fdfdf..94b3602 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setup( name='DeepTuner', - version='0.1.1', + version='0.1.5', author='Devasy Patel', author_email='patel.devasy.23@gmail.com', description='A package for fine-tuning deep learning models with Siamese architecture and triplet loss', From a4d30b7d8906231d853fb7c13bde2c38906aac93 Mon Sep 17 00:00:00 2001 From: Devasy Patel <110348311+Devasy23@users.noreply.github.com> Date: Mon, 25 Nov 2024 21:26:43 +0530 Subject: [PATCH 7/9] updated version and code --- deeptuner/__init__.py | 2 +- deeptuner/datagenerators/__init__.py | 0 pyproject.toml | 4 ++-- setup.py | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) create mode 100644 deeptuner/datagenerators/__init__.py diff --git a/deeptuner/__init__.py b/deeptuner/__init__.py index af6ab6c..ac461e5 100644 --- a/deeptuner/__init__.py +++ b/deeptuner/__init__.py @@ -2,4 +2,4 @@ DeepTuner - A package for fine-tuning deep learning models with Siamese architecture and triplet loss """ -__version__ = '0.1.5' +__version__ = '0.1.6' diff --git a/deeptuner/datagenerators/__init__.py b/deeptuner/datagenerators/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pyproject.toml b/pyproject.toml index 3c4345a..5145d72 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,8 +3,8 @@ requires = ["setuptools>=42", "wheel"] build-backend = "setuptools.build_meta" [project] -name = "DeepTuner" -version = "0.1.5" +name = "deeptuner" +version = "0.1.6" description = "A package for fine-tuning deep learning models with Siamese architecture and triplet loss" readme = "README.md" requires-python = ">=3.7" diff --git a/setup.py b/setup.py index 94b3602..dcac6b8 100644 --- a/setup.py +++ b/setup.py @@ -1,8 +1,8 @@ from setuptools import setup, find_packages setup( - name='DeepTuner', - version='0.1.5', + name='deeptuner', + version='0.1.6', author='Devasy Patel', author_email='patel.devasy.23@gmail.com', description='A package for fine-tuning deep learning models with Siamese architecture and triplet loss', From 3b1c3eca390bffe68bc502c341f01c8c8c4a8bca Mon Sep 17 00:00:00 2001 From: Devasy Patel <110348311+Devasy23@users.noreply.github.com> Date: Mon, 25 Nov 2024 21:29:59 +0530 Subject: [PATCH 8/9] updated initializers --- deeptuner/__init__.py | 2 +- deeptuner/callbacks/__init__.py | 0 pyproject.toml | 2 +- setup.py | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) create mode 100644 deeptuner/callbacks/__init__.py diff --git a/deeptuner/__init__.py b/deeptuner/__init__.py index ac461e5..e07ab48 100644 --- a/deeptuner/__init__.py +++ b/deeptuner/__init__.py @@ -2,4 +2,4 @@ DeepTuner - A package for fine-tuning deep learning models with Siamese architecture and triplet loss """ -__version__ = '0.1.6' +__version__ = '0.1.7' diff --git a/deeptuner/callbacks/__init__.py b/deeptuner/callbacks/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pyproject.toml b/pyproject.toml index 5145d72..f8cc060 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "deeptuner" -version = "0.1.6" +version = "0.1.7" description = "A package for fine-tuning deep learning models with Siamese architecture and triplet loss" readme = "README.md" requires-python = ">=3.7" diff --git a/setup.py b/setup.py index dcac6b8..fcbc2db 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setup( name='deeptuner', - version='0.1.6', + version='0.1.7', author='Devasy Patel', author_email='patel.devasy.23@gmail.com', description='A package for fine-tuning deep learning models with Siamese architecture and triplet loss', From 65dc6607d3f8385014891b7e747fa5aa35709e5e Mon Sep 17 00:00:00 2001 From: Devasy Patel <110348311+Devasy23@users.noreply.github.com> Date: Tue, 26 Nov 2024 20:42:09 +0530 Subject: [PATCH 9/9] patch to fix triplet data generator warning --- deeptuner/datagenerators/triplet_data_generator.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/deeptuner/datagenerators/triplet_data_generator.py b/deeptuner/datagenerators/triplet_data_generator.py index 18523e7..d23f96b 100644 --- a/deeptuner/datagenerators/triplet_data_generator.py +++ b/deeptuner/datagenerators/triplet_data_generator.py @@ -5,7 +5,8 @@ from tensorflow.keras.applications import resnet50 as resnet class TripletDataGenerator(tf.keras.utils.Sequence): - def __init__(self, image_paths, labels, batch_size, image_size, num_classes): + def __init__(self, image_paths, labels, batch_size, image_size, num_classes, **kwargs): + super().__init__(**kwargs) self.image_paths = image_paths self.labels = labels self.batch_size = batch_size