From 077346840b7255b2934577bb48a56911129ef083 Mon Sep 17 00:00:00 2001 From: Harshith K Date: Mon, 13 Jul 2026 20:30:34 +0530 Subject: [PATCH 1/7] Migrate EfficientNet fine-tuning tutorial to Keras 3 - Replace TensorFlow-specific code with backend-agnostic Keras 3 APIs - Remove tensorflow_datasets dependency, implement direct HTTP download from Stanford servers - Replace tf.data.Dataset pipeline with keras.utils.PyDataset classes - Replace tf.image.resize and tf.one_hot with keras.ops equivalents - Enhance unfreeze_model() function with parameterization (fixes #2385) - Add parameters: layers_to_unfreeze, learning_rate, loss, metrics - Support int (last N layers) and str (block name) patterns - Add return statement and update call site - Add comprehensive docstring with input validation - Update all documentation to reflect Keras 3 backend-agnostic approach - Standardize header format and update dates to 2026/07/13 - Apply changes consistently across .ipynb, .py, and .md files --- ...classification_efficientnet_fine_tuning.py | 443 +++++++++++++----- ...ssification_efficientnet_fine_tuning.ipynb | 442 ++++++++++++----- ...classification_efficientnet_fine_tuning.md | 428 ++++++++++++----- 3 files changed, 964 insertions(+), 349 deletions(-) diff --git a/examples/vision/image_classification_efficientnet_fine_tuning.py b/examples/vision/image_classification_efficientnet_fine_tuning.py index 2413b0c0de..23c45e20e1 100644 --- a/examples/vision/image_classification_efficientnet_fine_tuning.py +++ b/examples/vision/image_classification_efficientnet_fine_tuning.py @@ -2,7 +2,7 @@ Title: Image classification via fine-tuning with EfficientNet Author: [Yixing Fu](https://github.com/yixingfu) Date created: 2020/06/30 -Last modified: 2023/07/10 +Last modified: 2026/07/13 Description: Use EfficientNet with weights pre-trained on imagenet for Stanford Dogs classification. Accelerator: GPU """ @@ -123,10 +123,13 @@ ## Setup and data loading """ +import os +import tarfile +import urllib.request +from pathlib import Path import numpy as np -import tensorflow_datasets as tfds -import tensorflow as tf # For tf.data import matplotlib.pyplot as plt +from PIL import Image import keras from keras import layers from keras.applications import EfficientNetB0 @@ -139,40 +142,131 @@ """ ### Loading data -Here we load data from [tensorflow_datasets](https://www.tensorflow.org/datasets) -(hereafter TFDS). -Stanford Dogs dataset is provided in -TFDS as [stanford_dogs](https://www.tensorflow.org/datasets/catalog/stanford_dogs). -It features 20,580 images that belong to 120 classes of dog breeds +We download the Stanford Dogs dataset directly from Stanford's servers using the +built-in `urllib` and `tarfile` modules. +The dataset contains 20,580 images belonging to 120 classes of dog breeds (12,000 for training and 8,580 for testing). -By simply changing `dataset_name` below, you may also try this notebook for -other datasets in TFDS such as -[cifar10](https://www.tensorflow.org/datasets/catalog/cifar10), -[cifar100](https://www.tensorflow.org/datasets/catalog/cifar100), -[food101](https://www.tensorflow.org/datasets/catalog/food101), -etc. When the images are much smaller than the size of EfficientNet input, -we can simply upsample the input images. It has been shown in -[Tan and Le, 2019](https://arxiv.org/abs/1905.11946) that transfer learning -result is better for increased resolution even if input images remain small. +The dataset is downloaded, extracted, and loaded into lists of NumPy arrays. +Images have variable dimensions and will be resized to a uniform size in the data pipeline. + +**Note:** This direct download approach eliminates dependency conflicts that can +occur with `tensorflow_datasets` in some environments (particularly Google Colab +with protobuf version incompatibilities). """ -dataset_name = "stanford_dogs" -(ds_train, ds_test), ds_info = tfds.load( - dataset_name, split=["train", "test"], with_info=True, as_supervised=True -) -NUM_CLASSES = ds_info.features["label"].num_classes +# Download and extract Stanford Dogs dataset +import scipy.io +dataset_url = "http://vision.stanford.edu/aditya86/ImageNetDogs/images.tar" +lists_url = "http://vision.stanford.edu/aditya86/ImageNetDogs/lists.tar" +data_dir = Path("./stanford_dogs_data") +data_dir.mkdir(exist_ok=True) -""" -When the dataset include images with various size, we need to resize them into a -shared size. The Stanford Dogs dataset includes only images at least 200x200 -pixels in size. Here we resize the images to the input size needed for EfficientNet. -""" -size = (IMG_SIZE, IMG_SIZE) -ds_train = ds_train.map(lambda image, label: (tf.image.resize(image, size), label)) -ds_test = ds_test.map(lambda image, label: (tf.image.resize(image, size), label)) +def download_and_extract(url, extract_to): + filename = url.split("/")[-1] + filepath = data_dir / filename + if not filepath.exists(): + print(f"Downloading {filename}...") + urllib.request.urlretrieve(url, filepath) + print(f"Extracting {filename}...") + with tarfile.open(filepath, "r") as tar: + tar.extractall(extract_to) + return extract_to + + +# Download dataset +images_dir = download_and_extract(dataset_url, data_dir) +lists_dir = download_and_extract(lists_url, data_dir) + + +# Parse train/test splits +def load_file_list(filepath): + mat = scipy.io.loadmat(filepath) + return [item[0][0] for item in mat["file_list"]] + + +train_files = load_file_list(data_dir / "train_list.mat") +test_files = load_file_list(data_dir / "test_list.mat") + +# Build class name mapping +all_files = train_files + test_files +class_names = sorted(set([f.split("/")[0] for f in all_files])) +class_to_idx = {name: idx for idx, name in enumerate(class_names)} +NUM_CLASSES = len(class_names) + +print( + f"Found {NUM_CLASSES} classes, {len(train_files)} training images, {len(test_files)} test images" +) + + +# Load images and labels +def load_images_and_labels(file_list, base_dir): + images, labels = [], [] + for file_path in file_list: + class_name = file_path.split("/")[0] + img_path = base_dir / "Images" / file_path + try: + img = Image.open(img_path).convert("RGB") + images.append(np.array(img)) + labels.append(class_to_idx[class_name]) + except Exception as e: + print(f"Warning: Could not load {img_path}: {e}") + return images, np.array(labels) + + +print("Loading training images...") +train_images, train_labels = load_images_and_labels(train_files, data_dir) +print("Loading test images...") +test_images, test_labels = load_images_and_labels(test_files, data_dir) +print(f"Loaded {len(train_images)} train and {len(test_images)} test images") + + +""" +Each image can have a different shape, so we resize them to a shared input size +for EfficientNet. In Keras 3, we do this in a backend-agnostic `PyDataset` pipeline +using `keras.ops.image.resize`, which works across TensorFlow, JAX, and PyTorch backends, +rather than the TensorFlow-specific `tf.data` mapping steps. +""" + + +class ResizeOnlyDataset(keras.utils.PyDataset): + def __init__(self, images, labels, img_size, batch_size=1, **kwargs): + super().__init__(**kwargs) + self.images = images + self.labels = labels + self.img_size = img_size + self.batch_size = batch_size + self.indices = np.arange(len(labels)) + + def __len__(self): + return int(np.ceil(len(self.labels) / self.batch_size)) + + def __getitem__(self, idx): + batch_indices = self.indices[ + idx * self.batch_size : (idx + 1) * self.batch_size + ] + batch_images = np.stack( + [ + np.array( + keras.ops.image.resize( + np.array(self.images[i], dtype="float32"), + (self.img_size, self.img_size), + ) + ) + for i in batch_indices + ] + ) + batch_labels = self.labels[batch_indices] + if self.batch_size == 1: + return batch_images[0], batch_labels[0] + return batch_images, batch_labels + + +# Preview stream with resized images for visualization below +preview_train = ResizeOnlyDataset(train_images, train_labels, IMG_SIZE, batch_size=1) +preview_test = ResizeOnlyDataset(test_images, test_labels, IMG_SIZE, batch_size=1) """ ### Visualizing the data @@ -182,14 +276,15 @@ def format_label(label): - string_label = label_info.int2str(label) - return string_label.split("-")[1] + class_name = class_names[int(label)] + return class_name.split("-")[1] # Extract breed name from "n02085620-Chihuahua" -label_info = ds_info.features["label"] -for i, (image, label) in enumerate(ds_train.take(9)): +for i, (image, label) in enumerate(preview_train): + if i >= 9: + break ax = plt.subplot(3, 3, i + 1) - plt.imshow(image.numpy().astype("uint8")) + plt.imshow(np.array(image).astype("uint8")) plt.title("{}".format(format_label(label))) plt.axis("off") @@ -197,7 +292,8 @@ def format_label(label): """ ### Data augmentation -We can use the preprocessing layers APIs for image augmentation. +We can use Keras preprocessing layers for image augmentation. +These layers are backend-agnostic and can be used during both training and inference. """ img_augmentation_layers = [ @@ -215,64 +311,126 @@ def img_augmentation(images): """ -This `Sequential` model object can be used both as a part of -the model we later build, and as a function to preprocess -data before feeding into the model. Using them as function makes -it easy to visualize the augmented images. Here we plot 9 examples -of augmentation result of a given figure. +The `img_augmentation` function can be used both as a part of the model +we later build, and as a standalone function to preprocess data before feeding +into the model. Using it as a function makes it easy to visualize the augmentation +results. Here we plot 9 examples of augmentation applied to a single image. """ -for image, label in ds_train.take(1): - for i in range(9): - ax = plt.subplot(3, 3, i + 1) - aug_img = img_augmentation(np.expand_dims(image.numpy(), axis=0)) - aug_img = np.array(aug_img) - plt.imshow(aug_img[0].astype("uint8")) - plt.title("{}".format(format_label(label))) - plt.axis("off") +first_image, first_label = preview_train[0] + +for i in range(9): + ax = plt.subplot(3, 3, i + 1) + aug_img = img_augmentation(np.expand_dims(np.array(first_image), axis=0)) + aug_img = np.array(aug_img) + plt.imshow(aug_img[0].astype("uint8")) + plt.title("{}".format(format_label(first_label))) + plt.axis("off") """ ### Prepare inputs Once we verify the input data and augmentation are working correctly, -we prepare dataset for training. The input data are resized to uniform -`IMG_SIZE`. The labels are put into one-hot -(a.k.a. categorical) encoding. The dataset is batched. - -Note: `prefetch` and `AUTOTUNE` may in some situation improve -performance, but depends on environment and the specific dataset used. -See this [guide](https://www.tensorflow.org/guide/data_performance) -for more information on data pipeline performance. -""" - - -# One-hot / categorical encoding -def input_preprocess_train(image, label): - image = img_augmentation(image) - label = tf.one_hot(label, NUM_CLASSES) - return image, label - - -def input_preprocess_test(image, label): - label = tf.one_hot(label, NUM_CLASSES) - return image, label - - -ds_train = ds_train.map(input_preprocess_train, num_parallel_calls=tf.data.AUTOTUNE) -ds_train = ds_train.batch(batch_size=BATCH_SIZE, drop_remainder=True) -ds_train = ds_train.prefetch(tf.data.AUTOTUNE) +we prepare backend-agnostic datasets for training. + +The input images are resized to uniform `IMG_SIZE`, labels are converted to one-hot +(categorical) encoding, and batches are produced by `keras.utils.PyDataset`. + +Compared to the original `tf.data` version, this Keras 3 setup is backend-agnostic +and works seamlessly across TensorFlow, JAX, and PyTorch backends. +""" + + +class StanfordDogsDataset(keras.utils.PyDataset): + def __init__( + self, + images, + labels, + num_classes, + img_size, + batch_size, + augment=False, + shuffle=False, + **kwargs, + ): + super().__init__(**kwargs) + self.images = images + self.labels = labels + self.num_classes = num_classes + self.img_size = img_size + self.batch_size = batch_size + self.augment = augment + self.shuffle = shuffle + self.indices = np.arange(len(labels)) + self.on_epoch_end() + + def __len__(self): + # Match previous drop_remainder=True behavior. + return len(self.indices) // self.batch_size + + def on_epoch_end(self): + if self.shuffle: + np.random.shuffle(self.indices) + + def __getitem__(self, idx): + batch_indices = self.indices[ + idx * self.batch_size : (idx + 1) * self.batch_size + ] + + batch_images = np.stack( + [ + np.array( + keras.ops.image.resize( + np.array(self.images[i], dtype="float32"), + (self.img_size, self.img_size), + ) + ) + for i in batch_indices + ] + ) + + if self.augment: + batch_images = np.array(img_augmentation(batch_images)) + + batch_labels = np.array( + keras.ops.one_hot(self.labels[batch_indices], self.num_classes) + ) + + return batch_images, batch_labels + + +ds_train = StanfordDogsDataset( + train_images, + train_labels, + num_classes=NUM_CLASSES, + img_size=IMG_SIZE, + batch_size=BATCH_SIZE, + augment=True, + shuffle=True, + workers=2, + use_multiprocessing=False, +) -ds_test = ds_test.map(input_preprocess_test, num_parallel_calls=tf.data.AUTOTUNE) -ds_test = ds_test.batch(batch_size=BATCH_SIZE, drop_remainder=True) +ds_test = StanfordDogsDataset( + test_images, + test_labels, + num_classes=NUM_CLASSES, + img_size=IMG_SIZE, + batch_size=BATCH_SIZE, + augment=False, + shuffle=False, +) """ ## Training a model from scratch -We build an EfficientNetB0 with 120 output classes, that is initialized from scratch: +We build an EfficientNetB0 with 120 output classes, initialized from scratch +(no pretrained weights). -Note: the accuracy will increase very slowly and may overfit. +**Note:** Training from scratch typically shows slower convergence and may overfit +on smaller datasets like Stanford Dogs. """ model = EfficientNetB0( @@ -290,13 +448,13 @@ def input_preprocess_test(image, label): """ -Training the model is relatively fast. This might make it sounds easy to simply train EfficientNet on any -dataset wanted from scratch. However, training EfficientNet on smaller datasets, +Training the model is relatively fast (a few minutes per epoch on modern hardware). +However, training EfficientNet on smaller datasets, especially those with lower resolution like CIFAR-100, faces the significant challenge of overfitting. -Hence training from scratch requires very careful choice of hyperparameters and is -difficult to find suitable regularization. It would also be much more demanding in resources. +Training from scratch requires very careful choice of hyperparameters and +suitable regularization. It is also much more demanding in computational resources. Plotting the training and validation accuracy makes it clear that validation accuracy stagnates at a low value. """ @@ -317,10 +475,11 @@ def plot_hist(hist): plot_hist(hist) """ -## Transfer learning from pre-trained weights +## Transfer learning from pretrained weights -Here we initialize the model with pre-trained ImageNet weights, -and we fine-tune it on our own dataset. +Here we initialize the model with pretrained ImageNet weights +and fine-tune it on our own dataset. This is the recommended approach +for most applications. """ @@ -349,15 +508,15 @@ def build_model(num_classes): """ -The first step to transfer learning is to freeze all layers and train only the top +The first step in transfer learning is to freeze all base layers and train only the top layers. For this step, a relatively large learning rate (1e-2) can be used. -Note that validation accuracy and loss will usually be better than training + +**Note:** Validation accuracy and loss will usually be better than training accuracy and loss. This is because the regularization is strong, which only suppresses training-time metrics. -Note that the convergence may take up to 50 epochs depending on choice of learning rate. -If image augmentation layers were not -applied, the validation accuracy may only reach ~60%. +The convergence may take up to 50 epochs depending on the choice of learning rate. +If image augmentation layers were not applied, the validation accuracy may only reach ~60%. """ model = build_model(num_classes=NUM_CLASSES) @@ -367,14 +526,18 @@ def build_model(num_classes): plot_hist(hist) """ -The second step is to unfreeze a number of layers and fit the model using smaller -learning rate. In this example we show unfreezing all layers, but depending on -specific dataset it may be desireble to only unfreeze a fraction of all layers. +The second step is to unfreeze a number of layers and fine-tune the model using a smaller +learning rate. In this example we unfreeze the last 20 layers, but depending on the +specific dataset it may be desirable to only unfreeze a fraction of all layers. + +**Advanced usage:** The `unfreeze_model()` function also supports unfreezing by block name +(e.g., `unfreeze_model(model, layers_to_unfreeze="block7")`) to respect EfficientNet's +residual block boundaries. See the "Tips for fine-tuning EfficientNet" section below +for why this matters. -When the feature extraction with -pretrained model works good enough, this step would give a very limited gain on -validation accuracy. In our case we only see a small improvement, -as ImageNet pretraining already exposed the model to a good amount of dogs. +When feature extraction with the pretrained model works well enough, this step provides +only a limited gain in validation accuracy. In our case we only see a small improvement, +as ImageNet pretraining already exposed the model to a good amount of dog images. On the other hand, when we use pretrained weights on a dataset that is more different from ImageNet, this fine-tuning step can be crucial as the feature extractor also @@ -382,7 +545,7 @@ def build_model(num_classes): if choosing CIFAR-100 dataset instead, where fine-tuning boosts validation accuracy by about 10% to pass 80% on `EfficientNetB0`. -A side note on freezing/unfreezing models: setting `trainable` of a `Model` will +**Note on freezing/unfreezing models:** Setting `trainable` of a `Model` will simultaneously set all layers belonging to the `Model` to the same `trainable` attribute. Each layer is trainable only if both the layer itself and the model containing it are trainable. Hence when we need to partially freeze/unfreeze @@ -391,53 +554,103 @@ def build_model(num_classes): """ -def unfreeze_model(model): - # We unfreeze the top 20 layers while leaving BatchNorm layers frozen - for layer in model.layers[-20:]: +def unfreeze_model( + model, + layers_to_unfreeze=20, + learning_rate=1e-5, + loss="categorical_crossentropy", + metrics=None, +): + """Unfreeze part of `model` and recompile it for fine-tuning. + + Args: + model: A `keras.Model` instance to unfreeze in place. + layers_to_unfreeze: Either an `int` giving the number of layers, + counted from the end of `model.layers`, to unfreeze, or a `str` + substring to match against layer names -- the first matching + layer and every layer after it (in `model.layers` order) are + unfrozen. Use a string like `"block7"` to respect EfficientNet's + residual block boundaries instead of an arbitrary layer count. + Defaults to `20`. + learning_rate: Learning rate for the fine-tuning `Adam` optimizer. + Defaults to `1e-5`. + loss: Loss function passed to `model.compile()`. Defaults to + `"categorical_crossentropy"`. + metrics: List of metrics passed to `model.compile()`. Defaults to + `["accuracy"]`. + + Returns: + `model`, with the selected layers unfrozen (except + `BatchNormalization` layers, which are always kept frozen) and + recompiled with the new optimizer/loss/metrics. + """ + if metrics is None: + metrics = ["accuracy"] + + if isinstance(layers_to_unfreeze, str): + unfreeze_from = None + for i, layer in enumerate(model.layers): + if layers_to_unfreeze in layer.name: + unfreeze_from = i + break + if unfreeze_from is None: + raise ValueError(f"No layer name contains {layers_to_unfreeze!r}.") + layers_to_process = model.layers[unfreeze_from:] + elif isinstance(layers_to_unfreeze, int): + n_layers_to_unfreeze = min(layers_to_unfreeze, len(model.layers)) + layers_to_process = model.layers[-n_layers_to_unfreeze:] + else: + raise TypeError( + "layers_to_unfreeze must be an int or str, received: " + f"{type(layers_to_unfreeze)}" + ) + + # We keep BatchNorm layers frozen -- see "Tips for fine-tuning + # EfficientNet" in the tutorial for why. + for layer in layers_to_process: if not isinstance(layer, layers.BatchNormalization): layer.trainable = True - optimizer = keras.optimizers.Adam(learning_rate=1e-5) - model.compile( - optimizer=optimizer, loss="categorical_crossentropy", metrics=["accuracy"] - ) + optimizer = keras.optimizers.Adam(learning_rate=learning_rate) + model.compile(optimizer=optimizer, loss=loss, metrics=metrics) + return model -unfreeze_model(model) +model = unfreeze_model(model) epochs = 4 # @param {type: "slider", min:4, max:10} hist = model.fit(ds_train, epochs=epochs, validation_data=ds_test) plot_hist(hist) """ -### Tips for fine tuning EfficientNet +### Tips for fine-tuning EfficientNet -On unfreezing layers: +**On unfreezing layers:** - The `BatchNormalization` layers need to be kept frozen ([more details](https://keras.io/guides/transfer_learning/)). If they are also turned to trainable, the first epoch after unfreezing will significantly reduce accuracy. -- In some cases it may be beneficial to open up only a portion of layers instead of -unfreezing all. This will make fine tuning much faster when going to larger models like +- In some cases it may be beneficial to unfreeze only a portion of layers instead of +unfreezing all. This will make fine-tuning much faster when going to larger models like B7. - Each block needs to be all turned on or off. This is because the architecture includes a shortcut from the first layer to the last layer for each block. Not respecting blocks also significantly harms the final performance. -Some other tips for utilizing EfficientNet: +**Some other tips for utilizing EfficientNet:** - Larger variants of EfficientNet do not guarantee improved performance, especially for -tasks with less data or fewer classes. In such a case, the larger variant of EfficientNet +tasks with less data or fewer classes. In such a case, the larger the variant of EfficientNet chosen, the harder it is to tune hyperparameters. - EMA (Exponential Moving Average) is very helpful in training EfficientNet from scratch, but not so much for transfer learning. - Do not use the RMSprop setup as in the original paper for transfer learning. The momentum and learning rate are too high for transfer learning. It will easily corrupt the -pretrained weight and blow up the loss. A quick check is to see if loss (as categorical +pretrained weights and blow up the loss. A quick check is to see if loss (as categorical cross entropy) is getting significantly larger than log(NUM_CLASSES) after the same epoch. If so, the initial learning rate/momentum is too high. -- Smaller batch size benefit validation accuracy, possibly due to effectively providing +- Smaller batch sizes benefit validation accuracy, possibly due to effectively providing regularization. """ diff --git a/examples/vision/ipynb/image_classification_efficientnet_fine_tuning.ipynb b/examples/vision/ipynb/image_classification_efficientnet_fine_tuning.ipynb index 69b6a60ca5..f1a5b3b4a1 100644 --- a/examples/vision/ipynb/image_classification_efficientnet_fine_tuning.ipynb +++ b/examples/vision/ipynb/image_classification_efficientnet_fine_tuning.ipynb @@ -10,7 +10,7 @@ "\n", "**Author:** [Yixing Fu](https://github.com/yixingfu)
\n", "**Date created:** 2020/06/30
\n", - "**Last modified:** 2023/07/10
\n", + "**Last modified:** 2026/07/13
\n", "**Description:** Use EfficientNet with weights pre-trained on imagenet for Stanford Dogs classification." ] }, @@ -76,7 +76,7 @@ "use EfficientNetB0 for classifying 1000 classes of images from ImageNet, run:\n", "\n", "```python\n", - "from tensorflow.keras.applications import EfficientNetB0\n", + "from keras.applications import EfficientNetB0\n", "model = EfficientNetB0(weights='imagenet')\n", "```\n", "\n", @@ -111,15 +111,8 @@ "layer into prediction of the 1000 ImageNet classes. Replacing the top layer with custom\n", "layers allows using EfficientNet as a feature extractor in a transfer learning workflow.\n", "\n", - "Another argument in the model constructor worth noticing is `drop_connect_rate` which controls\n", - "the dropout rate responsible for [stochastic depth](https://arxiv.org/abs/1603.09382).\n", - "This parameter serves as a toggle for extra regularization in finetuning, but does not\n", - "affect loaded weights. For example, when stronger regularization is desired, try:\n", - "\n", - "```python\n", - "model = EfficientNetB0(weights='imagenet', drop_connect_rate=0.4)\n", - "```\n", - "The default value is 0.2.\n", + "A side note: in Keras 3, input preprocessing remains built into EfficientNet.\n", + "Inputs are expected as float pixel values in the `[0, 255]` range.\n", "\n", "## Example: EfficientNetB0 for Stanford Dogs.\n", "\n", @@ -140,16 +133,19 @@ }, { "cell_type": "code", - "execution_count": 0, + "execution_count": null, "metadata": { "colab_type": "code" }, "outputs": [], "source": [ + "import os\n", + "import tarfile\n", + "import urllib.request\n", + "from pathlib import Path\n", "import numpy as np\n", - "import tensorflow_datasets as tfds\n", - "import tensorflow as tf # For tf.data\n", "import matplotlib.pyplot as plt\n", + "from PIL import Image\n", "import keras\n", "from keras import layers\n", "from keras.applications import EfficientNetB0\n", @@ -167,37 +163,84 @@ "source": [ "### Loading data\n", "\n", - "Here we load data from [tensorflow_datasets](https://www.tensorflow.org/datasets)\n", - "(hereafter TFDS).\n", - "Stanford Dogs dataset is provided in\n", - "TFDS as [stanford_dogs](https://www.tensorflow.org/datasets/catalog/stanford_dogs).\n", - "It features 20,580 images that belong to 120 classes of dog breeds\n", + "We download the Stanford Dogs dataset directly from Stanford's servers using the\n", + "built-in `urllib` and `tarfile` modules.\n", + "The dataset contains 20,580 images belonging to 120 classes of dog breeds\n", "(12,000 for training and 8,580 for testing).\n", "\n", - "By simply changing `dataset_name` below, you may also try this notebook for\n", - "other datasets in TFDS such as\n", - "[cifar10](https://www.tensorflow.org/datasets/catalog/cifar10),\n", - "[cifar100](https://www.tensorflow.org/datasets/catalog/cifar100),\n", - "[food101](https://www.tensorflow.org/datasets/catalog/food101),\n", - "etc. When the images are much smaller than the size of EfficientNet input,\n", - "we can simply upsample the input images. It has been shown in\n", - "[Tan and Le, 2019](https://arxiv.org/abs/1905.11946) that transfer learning\n", - "result is better for increased resolution even if input images remain small." + "The dataset is downloaded, extracted, and loaded into lists of NumPy arrays.\n", + "Images have variable dimensions and will be resized to a uniform size in the data pipeline.\n", + "\n", + "**Note:** This direct download approach eliminates dependency conflicts that can\n", + "occur with `tensorflow_datasets` in some environments (particularly Google Colab\n", + "with protobuf version incompatibilities)." ] }, { "cell_type": "code", - "execution_count": 0, + "execution_count": null, "metadata": { "colab_type": "code" }, "outputs": [], "source": [ - "dataset_name = \"stanford_dogs\"\n", - "(ds_train, ds_test), ds_info = tfds.load(\n", - " dataset_name, split=[\"train\", \"test\"], with_info=True, as_supervised=True\n", - ")\n", - "NUM_CLASSES = ds_info.features[\"label\"].num_classes" + "# Download and extract Stanford Dogs dataset\n", + "import scipy.io\n", + "dataset_url = \"http://vision.stanford.edu/aditya86/ImageNetDogs/images.tar\"\n", + "lists_url = \"http://vision.stanford.edu/aditya86/ImageNetDogs/lists.tar\"\n", + "data_dir = Path(\"./stanford_dogs_data\")\n", + "data_dir.mkdir(exist_ok=True)\n", + "\n", + "def download_and_extract(url, extract_to):\n", + " filename = url.split(\"/\")[-1]\n", + " filepath = data_dir / filename\n", + " if not filepath.exists():\n", + " print(f\"Downloading {filename}...\")\n", + " urllib.request.urlretrieve(url, filepath)\n", + " print(f\"Extracting {filename}...\")\n", + " with tarfile.open(filepath, \"r\") as tar:\n", + " tar.extractall(extract_to)\n", + " return extract_to\n", + "\n", + "# Download dataset\n", + "images_dir = download_and_extract(dataset_url, data_dir)\n", + "lists_dir = download_and_extract(lists_url, data_dir)\n", + "\n", + "# Parse train/test splits\n", + "def load_file_list(filepath):\n", + " mat = scipy.io.loadmat(filepath)\n", + " return [item[0][0] for item in mat['file_list']]\n", + "\n", + "train_files = load_file_list(data_dir / \"train_list.mat\")\n", + "test_files = load_file_list(data_dir / \"test_list.mat\")\n", + "\n", + "# Build class name mapping\n", + "all_files = train_files + test_files\n", + "class_names = sorted(set([f.split(\"/\")[0] for f in all_files]))\n", + "class_to_idx = {name: idx for idx, name in enumerate(class_names)}\n", + "NUM_CLASSES = len(class_names)\n", + "\n", + "print(f\"Found {NUM_CLASSES} classes, {len(train_files)} training images, {len(test_files)} test images\")\n", + "\n", + "# Load images and labels\n", + "def load_images_and_labels(file_list, base_dir):\n", + " images, labels = [], []\n", + " for file_path in file_list:\n", + " class_name = file_path.split(\"/\")[0]\n", + " img_path = base_dir / \"Images\" / file_path\n", + " try:\n", + " img = Image.open(img_path).convert(\"RGB\")\n", + " images.append(np.array(img))\n", + " labels.append(class_to_idx[class_name])\n", + " except Exception as e:\n", + " print(f\"Warning: Could not load {img_path}: {e}\")\n", + " return images, np.array(labels)\n", + "\n", + "print(\"Loading training images...\")\n", + "train_images, train_labels = load_images_and_labels(train_files, data_dir)\n", + "print(\"Loading test images...\")\n", + "test_images, test_labels = load_images_and_labels(test_files, data_dir)\n", + "print(f\"Loaded {len(train_images)} train and {len(test_images)} test images\")" ] }, { @@ -206,9 +249,10 @@ "colab_type": "text" }, "source": [ - "When the dataset include images with various size, we need to resize them into a\n", - "shared size. The Stanford Dogs dataset includes only images at least 200x200\n", - "pixels in size. Here we resize the images to the input size needed for EfficientNet." + "Each image can have a different shape, so we resize them to a shared input size\n", + "for EfficientNet. In Keras 3, we do this in a backend-agnostic `PyDataset` pipeline\n", + "using `keras.ops.image.resize`, which works across TensorFlow, JAX, and PyTorch backends,\n", + "rather than the TensorFlow-specific `tf.data` mapping steps." ] }, { @@ -219,9 +263,40 @@ }, "outputs": [], "source": [ - "size = (IMG_SIZE, IMG_SIZE)\n", - "ds_train = ds_train.map(lambda image, label: (tf.image.resize(image, size), label))\n", - "ds_test = ds_test.map(lambda image, label: (tf.image.resize(image, size), label))" + "class ResizeOnlyDataset(keras.utils.PyDataset):\n", + " def __init__(self, images, labels, img_size, batch_size=1, **kwargs):\n", + " super().__init__(**kwargs)\n", + " self.images = images\n", + " self.labels = labels\n", + " self.img_size = img_size\n", + " self.batch_size = batch_size\n", + " self.indices = np.arange(len(labels))\n", + "\n", + " def __len__(self):\n", + " return int(np.ceil(len(self.labels) / self.batch_size))\n", + "\n", + " def __getitem__(self, idx):\n", + " batch_indices = self.indices[idx * self.batch_size : (idx + 1) * self.batch_size]\n", + " batch_images = np.stack(\n", + " [\n", + " np.array(\n", + " keras.ops.image.resize(\n", + " np.array(self.images[i], dtype=\"float32\"),\n", + " (self.img_size, self.img_size),\n", + " )\n", + " )\n", + " for i in batch_indices\n", + " ]\n", + " )\n", + " batch_labels = self.labels[batch_indices]\n", + " if self.batch_size == 1:\n", + " return batch_images[0], batch_labels[0]\n", + " return batch_images, batch_labels\n", + "\n", + "\n", + "# Preview stream with resized images for visualization below\n", + "preview_train = ResizeOnlyDataset(train_images, train_labels, IMG_SIZE, batch_size=1)\n", + "preview_test = ResizeOnlyDataset(test_images, test_labels, IMG_SIZE, batch_size=1)" ] }, { @@ -244,14 +319,15 @@ "outputs": [], "source": [ "def format_label(label):\n", - " string_label = label_info.int2str(label)\n", - " return string_label.split(\"-\")[1]\n", + " class_name = class_names[int(label)]\n", + " return class_name.split(\"-\")[1] # Extract breed name from \"n02085620-Chihuahua\"\n", "\n", "\n", - "label_info = ds_info.features[\"label\"]\n", - "for i, (image, label) in enumerate(ds_train.take(9)):\n", + "for i, (image, label) in enumerate(preview_train):\n", + " if i >= 9:\n", + " break\n", " ax = plt.subplot(3, 3, i + 1)\n", - " plt.imshow(image.numpy().astype(\"uint8\"))\n", + " plt.imshow(np.array(image).astype(\"uint8\"))\n", " plt.title(\"{}\".format(format_label(label)))\n", " plt.axis(\"off\")" ] @@ -264,7 +340,8 @@ "source": [ "### Data augmentation\n", "\n", - "We can use the preprocessing layers APIs for image augmentation." + "We can use Keras preprocessing layers for image augmentation.\n", + "These layers are backend-agnostic and can be used during both training and inference." ] }, { @@ -295,11 +372,10 @@ "colab_type": "text" }, "source": [ - "This `Sequential` model object can be used both as a part of\n", - "the model we later build, and as a function to preprocess\n", - "data before feeding into the model. Using them as function makes\n", - "it easy to visualize the augmented images. Here we plot 9 examples\n", - "of augmentation result of a given figure." + "The `img_augmentation` function can be used both as a part of the model\n", + "we later build, and as a standalone function to preprocess data before feeding\n", + "into the model. Using it as a function makes it easy to visualize the augmentation\n", + "results. Here we plot 9 examples of augmentation applied to a single image." ] }, { @@ -310,14 +386,15 @@ }, "outputs": [], "source": [ - "for image, label in ds_train.take(1):\n", - " for i in range(9):\n", - " ax = plt.subplot(3, 3, i + 1)\n", - " aug_img = img_augmentation(np.expand_dims(image.numpy(), axis=0))\n", - " aug_img = np.array(aug_img)\n", - " plt.imshow(aug_img[0].astype(\"uint8\"))\n", - " plt.title(\"{}\".format(format_label(label)))\n", - " plt.axis(\"off\")" + "first_image, first_label = preview_train[0]\n", + "\n", + "for i in range(9):\n", + " ax = plt.subplot(3, 3, i + 1)\n", + " aug_img = img_augmentation(np.expand_dims(np.array(first_image), axis=0))\n", + " aug_img = np.array(aug_img)\n", + " plt.imshow(aug_img[0].astype(\"uint8\"))\n", + " plt.title(\"{}\".format(format_label(first_label)))\n", + " plt.axis(\"off\")" ] }, { @@ -329,14 +406,13 @@ "### Prepare inputs\n", "\n", "Once we verify the input data and augmentation are working correctly,\n", - "we prepare dataset for training. The input data are resized to uniform\n", - "`IMG_SIZE`. The labels are put into one-hot\n", - "(a.k.a. categorical) encoding. The dataset is batched.\n", - "\n", - "Note: `prefetch` and `AUTOTUNE` may in some situation improve\n", - "performance, but depends on environment and the specific dataset used.\n", - "See this [guide](https://www.tensorflow.org/guide/data_performance)\n", - "for more information on data pipeline performance." + "we prepare backend-agnostic datasets for training.\n", + "\n", + "The input images are resized to uniform `IMG_SIZE`, labels are converted to one-hot\n", + "(categorical) encoding, and batches are produced by `keras.utils.PyDataset`.\n", + "\n", + "Compared to the original `tf.data` version, this Keras 3 setup is backend-agnostic\n", + "and works seamlessly across TensorFlow, JAX, and PyTorch backends." ] }, { @@ -347,24 +423,85 @@ }, "outputs": [], "source": [ - "# One-hot / categorical encoding\n", - "def input_preprocess_train(image, label):\n", - " image = img_augmentation(image)\n", - " label = tf.one_hot(label, NUM_CLASSES)\n", - " return image, label\n", - "\n", - "\n", - "def input_preprocess_test(image, label):\n", - " label = tf.one_hot(label, NUM_CLASSES)\n", - " return image, label\n", - "\n", - "\n", - "ds_train = ds_train.map(input_preprocess_train, num_parallel_calls=tf.data.AUTOTUNE)\n", - "ds_train = ds_train.batch(batch_size=BATCH_SIZE, drop_remainder=True)\n", - "ds_train = ds_train.prefetch(tf.data.AUTOTUNE)\n", + "class StanfordDogsDataset(keras.utils.PyDataset):\n", + " def __init__(\n", + " self,\n", + " images,\n", + " labels,\n", + " num_classes,\n", + " img_size,\n", + " batch_size,\n", + " augment=False,\n", + " shuffle=False,\n", + " **kwargs,\n", + " ):\n", + " super().__init__(**kwargs)\n", + " self.images = images\n", + " self.labels = labels\n", + " self.num_classes = num_classes\n", + " self.img_size = img_size\n", + " self.batch_size = batch_size\n", + " self.augment = augment\n", + " self.shuffle = shuffle\n", + " self.indices = np.arange(len(labels))\n", + " self.on_epoch_end()\n", + "\n", + " def __len__(self):\n", + " # Match previous drop_remainder=True behavior.\n", + " return len(self.indices) // self.batch_size\n", + "\n", + " def on_epoch_end(self):\n", + " if self.shuffle:\n", + " np.random.shuffle(self.indices)\n", + "\n", + " def __getitem__(self, idx):\n", + " batch_indices = self.indices[\n", + " idx * self.batch_size : (idx + 1) * self.batch_size\n", + " ]\n", + "\n", + " batch_images = np.stack(\n", + " [\n", + " np.array(\n", + " keras.ops.image.resize(\n", + " np.array(self.images[i], dtype=\"float32\"),\n", + " (self.img_size, self.img_size),\n", + " )\n", + " )\n", + " for i in batch_indices\n", + " ]\n", + " )\n", + "\n", + " if self.augment:\n", + " batch_images = np.array(img_augmentation(batch_images))\n", + "\n", + " batch_labels = np.array(\n", + " keras.ops.one_hot(self.labels[batch_indices], self.num_classes)\n", + " )\n", + "\n", + " return batch_images, batch_labels\n", + "\n", + "\n", + "ds_train = StanfordDogsDataset(\n", + " train_images,\n", + " train_labels,\n", + " num_classes=NUM_CLASSES,\n", + " img_size=IMG_SIZE,\n", + " batch_size=BATCH_SIZE,\n", + " augment=True,\n", + " shuffle=True,\n", + " workers=2,\n", + " use_multiprocessing=False,\n", + ")\n", "\n", - "ds_test = ds_test.map(input_preprocess_test, num_parallel_calls=tf.data.AUTOTUNE)\n", - "ds_test = ds_test.batch(batch_size=BATCH_SIZE, drop_remainder=True)" + "ds_test = StanfordDogsDataset(\n", + " test_images,\n", + " test_labels,\n", + " num_classes=NUM_CLASSES,\n", + " img_size=IMG_SIZE,\n", + " batch_size=BATCH_SIZE,\n", + " augment=False,\n", + " shuffle=False,\n", + ")" ] }, { @@ -375,9 +512,11 @@ "source": [ "## Training a model from scratch\n", "\n", - "We build an EfficientNetB0 with 120 output classes, that is initialized from scratch:\n", + "We build an EfficientNetB0 with 120 output classes, initialized from scratch\n", + "(no pretrained weights).\n", "\n", - "Note: the accuracy will increase very slowly and may overfit." + "**Note:** Training from scratch typically shows slower convergence and may overfit\n", + "on smaller datasets like Stanford Dogs." ] }, { @@ -408,13 +547,13 @@ "colab_type": "text" }, "source": [ - "Training the model is relatively fast. This might make it sounds easy to simply train EfficientNet on any\n", - "dataset wanted from scratch. However, training EfficientNet on smaller datasets,\n", + "Training the model is relatively fast (a few minutes per epoch on modern hardware).\n", + "However, training EfficientNet on smaller datasets,\n", "especially those with lower resolution like CIFAR-100, faces the significant challenge of\n", "overfitting.\n", "\n", - "Hence training from scratch requires very careful choice of hyperparameters and is\n", - "difficult to find suitable regularization. It would also be much more demanding in resources.\n", + "Training from scratch requires very careful choice of hyperparameters and\n", + "suitable regularization. It is also much more demanding in computational resources.\n", "Plotting the training and validation accuracy\n", "makes it clear that validation accuracy stagnates at a low value." ] @@ -449,10 +588,11 @@ "colab_type": "text" }, "source": [ - "## Transfer learning from pre-trained weights\n", + "## Transfer learning from pretrained weights\n", "\n", - "Here we initialize the model with pre-trained ImageNet weights,\n", - "and we fine-tune it on our own dataset." + "Here we initialize the model with pretrained ImageNet weights\n", + "and fine-tune it on our own dataset. This is the recommended approach\n", + "for most applications." ] }, { @@ -493,15 +633,15 @@ "colab_type": "text" }, "source": [ - "The first step to transfer learning is to freeze all layers and train only the top\n", + "The first step in transfer learning is to freeze all base layers and train only the top\n", "layers. For this step, a relatively large learning rate (1e-2) can be used.\n", - "Note that validation accuracy and loss will usually be better than training\n", + "\n", + "**Note:** Validation accuracy and loss will usually be better than training\n", "accuracy and loss. This is because the regularization is strong, which only\n", "suppresses training-time metrics.\n", "\n", - "Note that the convergence may take up to 50 epochs depending on choice of learning rate.\n", - "If image augmentation layers were not\n", - "applied, the validation accuracy may only reach ~60%." + "The convergence may take up to 50 epochs depending on the choice of learning rate.\n", + "If image augmentation layers were not applied, the validation accuracy may only reach ~60%." ] }, { @@ -525,14 +665,18 @@ "colab_type": "text" }, "source": [ - "The second step is to unfreeze a number of layers and fit the model using smaller\n", - "learning rate. In this example we show unfreezing all layers, but depending on\n", - "specific dataset it may be desireble to only unfreeze a fraction of all layers.\n", + "The second step is to unfreeze a number of layers and fine-tune the model using a smaller\n", + "learning rate. In this example we unfreeze the last 20 layers, but depending on the\n", + "specific dataset it may be desirable to only unfreeze a fraction of all layers.\n", + "\n", + "**Advanced usage:** The `unfreeze_model()` function also supports unfreezing by block name\n", + "(e.g., `unfreeze_model(model, layers_to_unfreeze=\"block7\")`) to respect EfficientNet's\n", + "residual block boundaries. See the \"Tips for fine-tuning EfficientNet\" section below\n", + "for why this matters.\n", "\n", - "When the feature extraction with\n", - "pretrained model works good enough, this step would give a very limited gain on\n", - "validation accuracy. In our case we only see a small improvement,\n", - "as ImageNet pretraining already exposed the model to a good amount of dogs.\n", + "When feature extraction with the pretrained model works well enough, this step provides\n", + "only a limited gain in validation accuracy. In our case we only see a small improvement,\n", + "as ImageNet pretraining already exposed the model to a good amount of dog images.\n", "\n", "On the other hand, when we use pretrained weights on a dataset that is more different\n", "from ImageNet, this fine-tuning step can be crucial as the feature extractor also\n", @@ -540,7 +684,7 @@ "if choosing CIFAR-100 dataset instead, where fine-tuning boosts validation accuracy\n", "by about 10% to pass 80% on `EfficientNetB0`.\n", "\n", - "A side note on freezing/unfreezing models: setting `trainable` of a `Model` will\n", + "**Note on freezing/unfreezing models:** Setting `trainable` of a `Model` will\n", "simultaneously set all layers belonging to the `Model` to the same `trainable`\n", "attribute. Each layer is trainable only if both the layer itself and the model\n", "containing it are trainable. Hence when we need to partially freeze/unfreeze\n", @@ -556,19 +700,69 @@ }, "outputs": [], "source": [ - "def unfreeze_model(model):\n", - " # We unfreeze the top 20 layers while leaving BatchNorm layers frozen\n", - " for layer in model.layers[-20:]:\n", + "def unfreeze_model(\n", + " model,\n", + " layers_to_unfreeze=20,\n", + " learning_rate=1e-5,\n", + " loss=\"categorical_crossentropy\",\n", + " metrics=None,\n", + "):\n", + " \"\"\"Unfreeze part of `model` and recompile it for fine-tuning.\n", + "\n", + " Args:\n", + " model: A `keras.Model` instance to unfreeze in place.\n", + " layers_to_unfreeze: Either an `int` giving the number of layers,\n", + " counted from the end of `model.layers`, to unfreeze, or a `str`\n", + " substring to match against layer names -- the first matching\n", + " layer and every layer after it (in `model.layers` order) are\n", + " unfrozen. Use a string like `\"block7\"` to respect EfficientNet's\n", + " residual block boundaries instead of an arbitrary layer count.\n", + " Defaults to `20`.\n", + " learning_rate: Learning rate for the fine-tuning `Adam` optimizer.\n", + " Defaults to `1e-5`.\n", + " loss: Loss function passed to `model.compile()`. Defaults to\n", + " `\"categorical_crossentropy\"`.\n", + " metrics: List of metrics passed to `model.compile()`. Defaults to\n", + " `[\"accuracy\"]`.\n", + "\n", + " Returns:\n", + " `model`, with the selected layers unfrozen (except\n", + " `BatchNormalization` layers, which are always kept frozen) and\n", + " recompiled with the new optimizer/loss/metrics.\n", + " \"\"\"\n", + " if metrics is None:\n", + " metrics = [\"accuracy\"]\n", + "\n", + " if isinstance(layers_to_unfreeze, str):\n", + " unfreeze_from = None\n", + " for i, layer in enumerate(model.layers):\n", + " if layers_to_unfreeze in layer.name:\n", + " unfreeze_from = i\n", + " break\n", + " if unfreeze_from is None:\n", + " raise ValueError(f\"No layer name contains {layers_to_unfreeze!r}.\")\n", + " layers_to_process = model.layers[unfreeze_from:]\n", + " elif isinstance(layers_to_unfreeze, int):\n", + " n_layers_to_unfreeze = min(layers_to_unfreeze, len(model.layers))\n", + " layers_to_process = model.layers[-n_layers_to_unfreeze:]\n", + " else:\n", + " raise TypeError(\n", + " \"layers_to_unfreeze must be an int or str, received: \"\n", + " f\"{type(layers_to_unfreeze)}\"\n", + " )\n", + "\n", + " # We keep BatchNorm layers frozen -- see \"Tips for fine tuning\n", + " # EfficientNet\" in the tutorial for why.\n", + " for layer in layers_to_process:\n", " if not isinstance(layer, layers.BatchNormalization):\n", " layer.trainable = True\n", "\n", - " optimizer = keras.optimizers.Adam(learning_rate=1e-5)\n", - " model.compile(\n", - " optimizer=optimizer, loss=\"categorical_crossentropy\", metrics=[\"accuracy\"]\n", - " )\n", + " optimizer = keras.optimizers.Adam(learning_rate=learning_rate)\n", + " model.compile(optimizer=optimizer, loss=loss, metrics=metrics)\n", + " return model\n", "\n", "\n", - "unfreeze_model(model)\n", + "model = unfreeze_model(model)\n", "\n", "epochs = 4 # @param {type: \"slider\", min:4, max:10}\n", "hist = model.fit(ds_train, epochs=epochs, validation_data=ds_test)\n", @@ -581,34 +775,34 @@ "colab_type": "text" }, "source": [ - "### Tips for fine tuning EfficientNet\n", + "### Tips for fine-tuning EfficientNet\n", "\n", - "On unfreezing layers:\n", + "**On unfreezing layers:**\n", "\n", "- The `BatchNormalization` layers need to be kept frozen\n", "([more details](https://keras.io/guides/transfer_learning/)).\n", "If they are also turned to trainable, the\n", "first epoch after unfreezing will significantly reduce accuracy.\n", - "- In some cases it may be beneficial to open up only a portion of layers instead of\n", - "unfreezing all. This will make fine tuning much faster when going to larger models like\n", + "- In some cases it may be beneficial to unfreeze only a portion of layers instead of\n", + "unfreezing all. This will make fine-tuning much faster when going to larger models like\n", "B7.\n", "- Each block needs to be all turned on or off. This is because the architecture includes\n", "a shortcut from the first layer to the last layer for each block. Not respecting blocks\n", "also significantly harms the final performance.\n", "\n", - "Some other tips for utilizing EfficientNet:\n", + "**Some other tips for utilizing EfficientNet:**\n", "\n", "- Larger variants of EfficientNet do not guarantee improved performance, especially for\n", - "tasks with less data or fewer classes. In such a case, the larger variant of EfficientNet\n", + "tasks with less data or fewer classes. In such a case, the larger the variant of EfficientNet\n", "chosen, the harder it is to tune hyperparameters.\n", "- EMA (Exponential Moving Average) is very helpful in training EfficientNet from scratch,\n", "but not so much for transfer learning.\n", "- Do not use the RMSprop setup as in the original paper for transfer learning. The\n", "momentum and learning rate are too high for transfer learning. It will easily corrupt the\n", - "pretrained weight and blow up the loss. A quick check is to see if loss (as categorical\n", + "pretrained weights and blow up the loss. A quick check is to see if loss (as categorical\n", "cross entropy) is getting significantly larger than log(NUM_CLASSES) after the same\n", "epoch. If so, the initial learning rate/momentum is too high.\n", - "- Smaller batch size benefit validation accuracy, possibly due to effectively providing\n", + "- Smaller batch sizes benefit validation accuracy, possibly due to effectively providing\n", "regularization." ] }, @@ -631,7 +825,7 @@ "toc_visible": true }, "kernelspec": { - "display_name": "Python 3", + "display_name": "keras_io_311_env (3.11.3)", "language": "python", "name": "python3" }, @@ -645,7 +839,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.7.0" + "version": "3.11.3" } }, "nbformat": 4, diff --git a/examples/vision/md/image_classification_efficientnet_fine_tuning.md b/examples/vision/md/image_classification_efficientnet_fine_tuning.md index c325b3175e..235d00d62e 100644 --- a/examples/vision/md/image_classification_efficientnet_fine_tuning.md +++ b/examples/vision/md/image_classification_efficientnet_fine_tuning.md @@ -2,7 +2,7 @@ **Author:** [Yixing Fu](https://github.com/yixingfu)
**Date created:** 2020/06/30
-**Last modified:** 2023/07/10
+**Last modified:** 2026/07/13
**Description:** Use EfficientNet with weights pre-trained on imagenet for Stanford Dogs classification. @@ -127,10 +127,13 @@ As an end-to-end example, we will show using pre-trained EfficientNetB0 on ```python +import os +import tarfile +import urllib.request +from pathlib import Path import numpy as np -import tensorflow_datasets as tfds -import tensorflow as tf # For tf.data import matplotlib.pyplot as plt +from PIL import Image import keras from keras import layers from keras.applications import EfficientNetB0 @@ -143,42 +146,130 @@ BATCH_SIZE = 64 ### Loading data -Here we load data from [tensorflow_datasets](https://www.tensorflow.org/datasets) -(hereafter TFDS). -Stanford Dogs dataset is provided in -TFDS as [stanford_dogs](https://www.tensorflow.org/datasets/catalog/stanford_dogs). -It features 20,580 images that belong to 120 classes of dog breeds +We download the Stanford Dogs dataset directly from Stanford's servers using the +built-in `urllib` and `tarfile` modules. +The dataset contains 20,580 images belonging to 120 classes of dog breeds (12,000 for training and 8,580 for testing). -By simply changing `dataset_name` below, you may also try this notebook for -other datasets in TFDS such as -[cifar10](https://www.tensorflow.org/datasets/catalog/cifar10), -[cifar100](https://www.tensorflow.org/datasets/catalog/cifar100), -[food101](https://www.tensorflow.org/datasets/catalog/food101), -etc. When the images are much smaller than the size of EfficientNet input, -we can simply upsample the input images. It has been shown in -[Tan and Le, 2019](https://arxiv.org/abs/1905.11946) that transfer learning -result is better for increased resolution even if input images remain small. +The dataset is downloaded, extracted, and loaded into lists of NumPy arrays. +Images have variable dimensions and will be resized to a uniform size in the data pipeline. + +**Note:** This direct download approach eliminates dependency conflicts that can +occur with `tensorflow_datasets` in some environments (particularly Google Colab +with protobuf version incompatibilities). ```python -dataset_name = "stanford_dogs" -(ds_train, ds_test), ds_info = tfds.load( - dataset_name, split=["train", "test"], with_info=True, as_supervised=True +# Download and extract Stanford Dogs dataset +import scipy.io + +dataset_url = "http://vision.stanford.edu/aditya86/ImageNetDogs/images.tar" +lists_url = "http://vision.stanford.edu/aditya86/ImageNetDogs/lists.tar" +data_dir = Path("./stanford_dogs_data") +data_dir.mkdir(exist_ok=True) + + +def download_and_extract(url, extract_to): + filename = url.split("/")[-1] + filepath = data_dir / filename + if not filepath.exists(): + print(f"Downloading {filename}...") + urllib.request.urlretrieve(url, filepath) + print(f"Extracting {filename}...") + with tarfile.open(filepath, "r") as tar: + tar.extractall(extract_to) + return extract_to + + +# Download dataset +images_dir = download_and_extract(dataset_url, data_dir) +lists_dir = download_and_extract(lists_url, data_dir) + +# Parse train/test splits +def load_file_list(filepath): + mat = scipy.io.loadmat(filepath) + return [item[0][0] for item in mat["file_list"]] + + +train_files = load_file_list(data_dir / "train_list.mat") +test_files = load_file_list(data_dir / "test_list.mat") + +# Build class name mapping +all_files = train_files + test_files +class_names = sorted(set([f.split("/")[0] for f in all_files])) +class_to_idx = {name: idx for idx, name in enumerate(class_names)} +NUM_CLASSES = len(class_names) + +print( + f"Found {NUM_CLASSES} classes, {len(train_files)} training images, {len(test_files)} test images" ) -NUM_CLASSES = ds_info.features["label"].num_classes + +# Load images and labels +def load_images_and_labels(file_list, base_dir): + images, labels = [], [] + for file_path in file_list: + class_name = file_path.split("/")[0] + img_path = base_dir / "Images" / file_path + try: + img = Image.open(img_path).convert("RGB") + images.append(np.array(img)) + labels.append(class_to_idx[class_name]) + except Exception as e: + print(f"Warning: Could not load {img_path}: {e}") + return images, np.array(labels) + + +print("Loading training images...") +train_images, train_labels = load_images_and_labels(train_files, data_dir) +print("Loading test images...") +test_images, test_labels = load_images_and_labels(test_files, data_dir) +print(f"Loaded {len(train_images)} train and {len(test_images)} test images") ``` -When the dataset include images with various size, we need to resize them into a -shared size. The Stanford Dogs dataset includes only images at least 200x200 -pixels in size. Here we resize the images to the input size needed for EfficientNet. +Each image can have a different shape, so we resize them to a shared input size +for EfficientNet. In Keras 3, we do this in a backend-agnostic `PyDataset` pipeline +using `keras.ops.image.resize`, which works across TensorFlow, JAX, and PyTorch backends, +rather than the TensorFlow-specific `tf.data` mapping steps. ```python -size = (IMG_SIZE, IMG_SIZE) -ds_train = ds_train.map(lambda image, label: (tf.image.resize(image, size), label)) -ds_test = ds_test.map(lambda image, label: (tf.image.resize(image, size), label)) +class ResizeOnlyDataset(keras.utils.PyDataset): + def __init__(self, images, labels, img_size, batch_size=1, **kwargs): + super().__init__(**kwargs) + self.images = images + self.labels = labels + self.img_size = img_size + self.batch_size = batch_size + self.indices = np.arange(len(labels)) + + def __len__(self): + return int(np.ceil(len(self.labels) / self.batch_size)) + + def __getitem__(self, idx): + batch_indices = self.indices[ + idx * self.batch_size : (idx + 1) * self.batch_size + ] + batch_images = np.stack( + [ + np.array( + keras.ops.image.resize( + np.array(self.images[i], dtype="float32"), + (self.img_size, self.img_size), + ) + ) + for i in batch_indices + ] + ) + batch_labels = self.labels[batch_indices] + if self.batch_size == 1: + return batch_images[0], batch_labels[0] + return batch_images, batch_labels + + +# Preview stream with resized images for visualization below +preview_train = ResizeOnlyDataset(train_images, train_labels, IMG_SIZE, batch_size=1) +preview_test = ResizeOnlyDataset(test_images, test_labels, IMG_SIZE, batch_size=1) ``` ### Visualizing the data @@ -189,14 +280,15 @@ The following code shows the first 9 images with their labels. ```python def format_label(label): - string_label = label_info.int2str(label) - return string_label.split("-")[1] + class_name = class_names[int(label)] + return class_name.split("-")[1] # Extract breed name from "n02085620-Chihuahua" -label_info = ds_info.features["label"] -for i, (image, label) in enumerate(ds_train.take(9)): +for i, (image, label) in enumerate(preview_train): + if i >= 9: + break ax = plt.subplot(3, 3, i + 1) - plt.imshow(image.numpy().astype("uint8")) + plt.imshow(np.array(image).astype("uint8")) plt.title("{}".format(format_label(label))) plt.axis("off") @@ -210,7 +302,8 @@ for i, (image, label) in enumerate(ds_train.take(9)): ### Data augmentation -We can use the preprocessing layers APIs for image augmentation. +We can use Keras preprocessing layers for image augmentation. +These layers are backend-agnostic and can be used during both training and inference. ```python @@ -229,22 +322,22 @@ def img_augmentation(images): ``` -This `Sequential` model object can be used both as a part of -the model we later build, and as a function to preprocess -data before feeding into the model. Using them as function makes -it easy to visualize the augmented images. Here we plot 9 examples -of augmentation result of a given figure. +The `img_augmentation` function can be used both as a part of the model +we later build, and as a standalone function to preprocess data before feeding +into the model. Using it as a function makes it easy to visualize the augmentation +results. Here we plot 9 examples of augmentation applied to a single image. ```python -for image, label in ds_train.take(1): - for i in range(9): - ax = plt.subplot(3, 3, i + 1) - aug_img = img_augmentation(np.expand_dims(image.numpy(), axis=0)) - aug_img = np.array(aug_img) - plt.imshow(aug_img[0].astype("uint8")) - plt.title("{}".format(format_label(label))) - plt.axis("off") +first_image, first_label = preview_train[0] + +for i in range(9): + ax = plt.subplot(3, 3, i + 1) + aug_img = img_augmentation(np.expand_dims(np.array(first_image), axis=0)) + aug_img = np.array(aug_img) + plt.imshow(aug_img[0].astype("uint8")) + plt.title("{}".format(format_label(first_label))) + plt.axis("off") ``` @@ -257,45 +350,105 @@ for image, label in ds_train.take(1): ### Prepare inputs Once we verify the input data and augmentation are working correctly, -we prepare dataset for training. The input data are resized to uniform -`IMG_SIZE`. The labels are put into one-hot -(a.k.a. categorical) encoding. The dataset is batched. +we prepare backend-agnostic datasets for training. -Note: `prefetch` and `AUTOTUNE` may in some situation improve -performance, but depends on environment and the specific dataset used. -See this [guide](https://www.tensorflow.org/guide/data_performance) -for more information on data pipeline performance. +The input images are resized to uniform `IMG_SIZE`, labels are converted to one-hot +(categorical) encoding, and batches are produced by `keras.utils.PyDataset`. +Compared to the original `tf.data` version, this Keras 3 setup is backend-agnostic +and works seamlessly across TensorFlow, JAX, and PyTorch backends. -```python - -# One-hot / categorical encoding -def input_preprocess_train(image, label): - image = img_augmentation(image) - label = tf.one_hot(label, NUM_CLASSES) - return image, label - - -def input_preprocess_test(image, label): - label = tf.one_hot(label, NUM_CLASSES) - return image, label +```python +class StanfordDogsDataset(keras.utils.PyDataset): + def __init__( + self, + images, + labels, + num_classes, + img_size, + batch_size, + augment=False, + shuffle=False, + **kwargs, + ): + super().__init__(**kwargs) + self.images = images + self.labels = labels + self.num_classes = num_classes + self.img_size = img_size + self.batch_size = batch_size + self.augment = augment + self.shuffle = shuffle + self.indices = np.arange(len(labels)) + self.on_epoch_end() + + def __len__(self): + # Match previous drop_remainder=True behavior. + return len(self.indices) // self.batch_size + + def on_epoch_end(self): + if self.shuffle: + np.random.shuffle(self.indices) + + def __getitem__(self, idx): + batch_indices = self.indices[ + idx * self.batch_size : (idx + 1) * self.batch_size + ] + + batch_images = np.stack( + [ + np.array( + keras.ops.image.resize( + np.array(self.images[i], dtype="float32"), + (self.img_size, self.img_size), + ) + ) + for i in batch_indices + ] + ) + + if self.augment: + batch_images = np.array(img_augmentation(batch_images)) + + batch_labels = np.array( + keras.ops.one_hot(self.labels[batch_indices], self.num_classes) + ) + + return batch_images, batch_labels + + +ds_train = StanfordDogsDataset( + train_images, + train_labels, + num_classes=NUM_CLASSES, + img_size=IMG_SIZE, + batch_size=BATCH_SIZE, + augment=True, + shuffle=True, + workers=2, + use_multiprocessing=False, +) -ds_train = ds_train.map(input_preprocess_train, num_parallel_calls=tf.data.AUTOTUNE) -ds_train = ds_train.batch(batch_size=BATCH_SIZE, drop_remainder=True) -ds_train = ds_train.prefetch(tf.data.AUTOTUNE) - -ds_test = ds_test.map(input_preprocess_test, num_parallel_calls=tf.data.AUTOTUNE) -ds_test = ds_test.batch(batch_size=BATCH_SIZE, drop_remainder=True) - +ds_test = StanfordDogsDataset( + test_images, + test_labels, + num_classes=NUM_CLASSES, + img_size=IMG_SIZE, + batch_size=BATCH_SIZE, + augment=False, + shuffle=False, +) ``` --- ## Training a model from scratch -We build an EfficientNetB0 with 120 output classes, that is initialized from scratch: +We build an EfficientNetB0 with 120 output classes, initialized from scratch +(no pretrained weights). -Note: the accuracy will increase very slowly and may overfit. +**Note:** Training from scratch typically shows slower convergence and may overfit +on smaller datasets like Stanford Dogs. ```python @@ -1155,13 +1308,13 @@ Epoch 40/40 ``` -Training the model is relatively fast. This might make it sounds easy to simply train EfficientNet on any -dataset wanted from scratch. However, training EfficientNet on smaller datasets, +Training the model is relatively fast (a few minutes per epoch on modern hardware). +However, training EfficientNet on smaller datasets, especially those with lower resolution like CIFAR-100, faces the significant challenge of overfitting. -Hence training from scratch requires very careful choice of hyperparameters and is -difficult to find suitable regularization. It would also be much more demanding in resources. +Training from scratch requires very careful choice of hyperparameters and +suitable regularization. It is also much more demanding in computational resources. Plotting the training and validation accuracy makes it clear that validation accuracy stagnates at a low value. @@ -1190,10 +1343,11 @@ plot_hist(hist) --- -## Transfer learning from pre-trained weights +## Transfer learning from pretrained weights -Here we initialize the model with pre-trained ImageNet weights, -and we fine-tune it on our own dataset. +Here we initialize the model with pretrained ImageNet weights +and fine-tune it on our own dataset. This is the recommended approach +for most applications. ```python @@ -1223,15 +1377,15 @@ def build_model(num_classes): ``` -The first step to transfer learning is to freeze all layers and train only the top +The first step in transfer learning is to freeze all base layers and train only the top layers. For this step, a relatively large learning rate (1e-2) can be used. -Note that validation accuracy and loss will usually be better than training + +**Note:** Validation accuracy and loss will usually be better than training accuracy and loss. This is because the regularization is strong, which only suppresses training-time metrics. -Note that the convergence may take up to 50 epochs depending on choice of learning rate. -If image augmentation layers were not -applied, the validation accuracy may only reach ~60%. +The convergence may take up to 50 epochs depending on the choice of learning rate. +If image augmentation layers were not applied, the validation accuracy may only reach ~60%. ```python @@ -1302,14 +1456,18 @@ Epoch 25/25 -The second step is to unfreeze a number of layers and fit the model using smaller -learning rate. In this example we show unfreezing all layers, but depending on -specific dataset it may be desireble to only unfreeze a fraction of all layers. +The second step is to unfreeze a number of layers and fine-tune the model using a smaller +learning rate. In this example we unfreeze the last 20 layers, but depending on the +specific dataset it may be desirable to only unfreeze a fraction of all layers. -When the feature extraction with -pretrained model works good enough, this step would give a very limited gain on -validation accuracy. In our case we only see a small improvement, -as ImageNet pretraining already exposed the model to a good amount of dogs. +**Advanced usage:** The `unfreeze_model()` function also supports unfreezing by block name +(e.g., `unfreeze_model(model, layers_to_unfreeze="block7")`) to respect EfficientNet's +residual block boundaries. See the "Tips for fine-tuning EfficientNet" section below +for why this matters. + +When feature extraction with the pretrained model works well enough, this step provides +only a limited gain in validation accuracy. In our case we only see a small improvement, +as ImageNet pretraining already exposed the model to a good amount of dog images. On the other hand, when we use pretrained weights on a dataset that is more different from ImageNet, this fine-tuning step can be crucial as the feature extractor also @@ -1317,7 +1475,7 @@ needs to be adjusted by a considerable amount. Such a situation can be demonstra if choosing CIFAR-100 dataset instead, where fine-tuning boosts validation accuracy by about 10% to pass 80% on `EfficientNetB0`. -A side note on freezing/unfreezing models: setting `trainable` of a `Model` will +**Note on freezing/unfreezing models:** Setting `trainable` of a `Model` will simultaneously set all layers belonging to the `Model` to the same `trainable` attribute. Each layer is trainable only if both the layer itself and the model containing it are trainable. Hence when we need to partially freeze/unfreeze @@ -1327,19 +1485,69 @@ to `True`. ```python -def unfreeze_model(model): - # We unfreeze the top 20 layers while leaving BatchNorm layers frozen - for layer in model.layers[-20:]: +def unfreeze_model( + model, + layers_to_unfreeze=20, + learning_rate=1e-5, + loss="categorical_crossentropy", + metrics=None, +): + """Unfreeze part of `model` and recompile it for fine-tuning. + + Args: + model: A `keras.Model` instance to unfreeze in place. + layers_to_unfreeze: Either an `int` giving the number of layers, + counted from the end of `model.layers`, to unfreeze, or a `str` + substring to match against layer names -- the first matching + layer and every layer after it (in `model.layers` order) are + unfrozen. Use a string like `"block7"` to respect EfficientNet's + residual block boundaries instead of an arbitrary layer count. + Defaults to `20`. + learning_rate: Learning rate for the fine-tuning `Adam` optimizer. + Defaults to `1e-5`. + loss: Loss function passed to `model.compile()`. Defaults to + `"categorical_crossentropy"`. + metrics: List of metrics passed to `model.compile()`. Defaults to + `["accuracy"]`. + + Returns: + `model`, with the selected layers unfrozen (except + `BatchNormalization` layers, which are always kept frozen) and + recompiled with the new optimizer/loss/metrics. + """ + if metrics is None: + metrics = ["accuracy"] + + if isinstance(layers_to_unfreeze, str): + unfreeze_from = None + for i, layer in enumerate(model.layers): + if layers_to_unfreeze in layer.name: + unfreeze_from = i + break + if unfreeze_from is None: + raise ValueError(f"No layer name contains {layers_to_unfreeze!r}.") + layers_to_process = model.layers[unfreeze_from:] + elif isinstance(layers_to_unfreeze, int): + n_layers_to_unfreeze = min(layers_to_unfreeze, len(model.layers)) + layers_to_process = model.layers[-n_layers_to_unfreeze:] + else: + raise TypeError( + "layers_to_unfreeze must be an int or str, received: " + f"{type(layers_to_unfreeze)}" + ) + + # We keep BatchNorm layers frozen -- see "Tips for fine-tuning + # EfficientNet" in the tutorial for why. + for layer in layers_to_process: if not isinstance(layer, layers.BatchNormalization): layer.trainable = True - optimizer = keras.optimizers.Adam(learning_rate=1e-5) - model.compile( - optimizer=optimizer, loss="categorical_crossentropy", metrics=["accuracy"] - ) + optimizer = keras.optimizers.Adam(learning_rate=learning_rate) + model.compile(optimizer=optimizer, loss=loss, metrics=metrics) + return model -unfreeze_model(model) +model = unfreeze_model(model) epochs = 4 # @param {type: "slider", min:4, max:10} hist = model.fit(ds_train, epochs=epochs, validation_data=ds_test) @@ -1364,34 +1572,34 @@ Epoch 4/4 -### Tips for fine tuning EfficientNet +### Tips for fine-tuning EfficientNet -On unfreezing layers: +**On unfreezing layers:** - The `BatchNormalization` layers need to be kept frozen ([more details](https://keras.io/guides/transfer_learning/)). If they are also turned to trainable, the first epoch after unfreezing will significantly reduce accuracy. -- In some cases it may be beneficial to open up only a portion of layers instead of -unfreezing all. This will make fine tuning much faster when going to larger models like +- In some cases it may be beneficial to unfreeze only a portion of layers instead of +unfreezing all. This will make fine-tuning much faster when going to larger models like B7. - Each block needs to be all turned on or off. This is because the architecture includes a shortcut from the first layer to the last layer for each block. Not respecting blocks also significantly harms the final performance. -Some other tips for utilizing EfficientNet: +**Some other tips for utilizing EfficientNet:** - Larger variants of EfficientNet do not guarantee improved performance, especially for -tasks with less data or fewer classes. In such a case, the larger variant of EfficientNet +tasks with less data or fewer classes. In such a case, the larger the variant of EfficientNet chosen, the harder it is to tune hyperparameters. - EMA (Exponential Moving Average) is very helpful in training EfficientNet from scratch, but not so much for transfer learning. - Do not use the RMSprop setup as in the original paper for transfer learning. The momentum and learning rate are too high for transfer learning. It will easily corrupt the -pretrained weight and blow up the loss. A quick check is to see if loss (as categorical +pretrained weights and blow up the loss. A quick check is to see if loss (as categorical cross entropy) is getting significantly larger than log(NUM_CLASSES) after the same epoch. If so, the initial learning rate/momentum is too high. -- Smaller batch size benefit validation accuracy, possibly due to effectively providing +- Smaller batch sizes benefit validation accuracy, possibly due to effectively providing regularization. --- From f4d234d6f470de5f998b6c610e0e287ae01f6d63 Mon Sep 17 00:00:00 2001 From: Harshith K Date: Mon, 13 Jul 2026 20:56:13 +0530 Subject: [PATCH 2/7] Address Gemini Code Assist feedback: fix unfreeze_model and implement lazy loading Critical fixes: - Fix unfreeze_model() to access nested EfficientNetB0 base model via model.get_layer('efficientnetb0') Previously accessed model.layers which only contains 6 top-level layers, not EfficientNet blocks This caused ValueError with string matching and incorrectly unfroze entire base model with int parameter High-priority fixes: - Implement lazy image loading to prevent OOM issues Replace load_images_and_labels() with prepare_paths_and_labels() Store file paths instead of loaded images (saves several GB of RAM) Load images on-demand in PyDataset.__getitem__() from disk Prevents memory crashes in resource-constrained environments like Colab Changes applied consistently across .ipynb, .py, and .md files --- ...classification_efficientnet_fine_tuning.py | 102 +++++++++--------- ...ssification_efficientnet_fine_tuning.ipynb | 102 +++++++++--------- ...classification_efficientnet_fine_tuning.md | 102 +++++++++--------- 3 files changed, 159 insertions(+), 147 deletions(-) diff --git a/examples/vision/image_classification_efficientnet_fine_tuning.py b/examples/vision/image_classification_efficientnet_fine_tuning.py index 23c45e20e1..bf8a373bdd 100644 --- a/examples/vision/image_classification_efficientnet_fine_tuning.py +++ b/examples/vision/image_classification_efficientnet_fine_tuning.py @@ -201,26 +201,22 @@ def load_file_list(filepath): ) -# Load images and labels -def load_images_and_labels(file_list, base_dir): - images, labels = [], [] +# Prepare image paths and labels (lazy loading - no images loaded into memory yet) +def prepare_paths_and_labels(file_list, base_dir): + image_paths, labels = [], [] for file_path in file_list: class_name = file_path.split("/")[0] img_path = base_dir / "Images" / file_path - try: - img = Image.open(img_path).convert("RGB") - images.append(np.array(img)) + if img_path.exists(): + image_paths.append(str(img_path)) labels.append(class_to_idx[class_name]) - except Exception as e: - print(f"Warning: Could not load {img_path}: {e}") - return images, np.array(labels) + return image_paths, np.array(labels) -print("Loading training images...") -train_images, train_labels = load_images_and_labels(train_files, data_dir) -print("Loading test images...") -test_images, test_labels = load_images_and_labels(test_files, data_dir) -print(f"Loaded {len(train_images)} train and {len(test_images)} test images") +print("Preparing dataset paths...") +train_image_paths, train_labels = prepare_paths_and_labels(train_files, data_dir) +test_image_paths, test_labels = prepare_paths_and_labels(test_files, data_dir) +print(f"Found {len(train_image_paths)} train and {len(test_image_paths)} test images") """ @@ -228,13 +224,16 @@ def load_images_and_labels(file_list, base_dir): for EfficientNet. In Keras 3, we do this in a backend-agnostic `PyDataset` pipeline using `keras.ops.image.resize`, which works across TensorFlow, JAX, and PyTorch backends, rather than the TensorFlow-specific `tf.data` mapping steps. + +Images are loaded lazily from disk in `__getitem__` to avoid loading all 20,580 images +into memory at once, which would consume several gigabytes of RAM and cause OOM issues. """ class ResizeOnlyDataset(keras.utils.PyDataset): - def __init__(self, images, labels, img_size, batch_size=1, **kwargs): + def __init__(self, image_paths, labels, img_size, batch_size=1, **kwargs): super().__init__(**kwargs) - self.images = images + self.image_paths = image_paths self.labels = labels self.img_size = img_size self.batch_size = batch_size @@ -247,17 +246,15 @@ def __getitem__(self, idx): batch_indices = self.indices[ idx * self.batch_size : (idx + 1) * self.batch_size ] - batch_images = np.stack( - [ - np.array( - keras.ops.image.resize( - np.array(self.images[i], dtype="float32"), - (self.img_size, self.img_size), - ) - ) - for i in batch_indices - ] - ) + batch_images = [] + for i in batch_indices: + img = Image.open(self.image_paths[i]).convert("RGB") + img_array = np.array(img, dtype="float32") + img_resized = keras.ops.image.resize( + img_array, (self.img_size, self.img_size) + ) + batch_images.append(np.array(img_resized)) + batch_images = np.stack(batch_images) batch_labels = self.labels[batch_indices] if self.batch_size == 1: return batch_images[0], batch_labels[0] @@ -265,8 +262,10 @@ def __getitem__(self, idx): # Preview stream with resized images for visualization below -preview_train = ResizeOnlyDataset(train_images, train_labels, IMG_SIZE, batch_size=1) -preview_test = ResizeOnlyDataset(test_images, test_labels, IMG_SIZE, batch_size=1) +preview_train = ResizeOnlyDataset( + train_image_paths, train_labels, IMG_SIZE, batch_size=1 +) +preview_test = ResizeOnlyDataset(test_image_paths, test_labels, IMG_SIZE, batch_size=1) """ ### Visualizing the data @@ -345,7 +344,7 @@ def img_augmentation(images): class StanfordDogsDataset(keras.utils.PyDataset): def __init__( self, - images, + image_paths, labels, num_classes, img_size, @@ -355,7 +354,7 @@ def __init__( **kwargs, ): super().__init__(**kwargs) - self.images = images + self.image_paths = image_paths self.labels = labels self.num_classes = num_classes self.img_size = img_size @@ -378,17 +377,16 @@ def __getitem__(self, idx): idx * self.batch_size : (idx + 1) * self.batch_size ] - batch_images = np.stack( - [ - np.array( - keras.ops.image.resize( - np.array(self.images[i], dtype="float32"), - (self.img_size, self.img_size), - ) - ) - for i in batch_indices - ] - ) + # Load images lazily from disk + batch_images = [] + for i in batch_indices: + img = Image.open(self.image_paths[i]).convert("RGB") + img_array = np.array(img, dtype="float32") + img_resized = keras.ops.image.resize( + img_array, (self.img_size, self.img_size) + ) + batch_images.append(np.array(img_resized)) + batch_images = np.stack(batch_images) if self.augment: batch_images = np.array(img_augmentation(batch_images)) @@ -401,7 +399,7 @@ def __getitem__(self, idx): ds_train = StanfordDogsDataset( - train_images, + train_image_paths, train_labels, num_classes=NUM_CLASSES, img_size=IMG_SIZE, @@ -413,7 +411,7 @@ def __getitem__(self, idx): ) ds_test = StanfordDogsDataset( - test_images, + test_image_paths, test_labels, num_classes=NUM_CLASSES, img_size=IMG_SIZE, @@ -587,18 +585,26 @@ def unfreeze_model( if metrics is None: metrics = ["accuracy"] + # Access the nested EfficientNetB0 base model + base_model = model.get_layer("efficientnetb0") + base_model.trainable = True + + # First, freeze all layers in the base model + for layer in base_model.layers: + layer.trainable = False + if isinstance(layers_to_unfreeze, str): unfreeze_from = None - for i, layer in enumerate(model.layers): + for i, layer in enumerate(base_model.layers): if layers_to_unfreeze in layer.name: unfreeze_from = i break if unfreeze_from is None: raise ValueError(f"No layer name contains {layers_to_unfreeze!r}.") - layers_to_process = model.layers[unfreeze_from:] + layers_to_process = base_model.layers[unfreeze_from:] elif isinstance(layers_to_unfreeze, int): - n_layers_to_unfreeze = min(layers_to_unfreeze, len(model.layers)) - layers_to_process = model.layers[-n_layers_to_unfreeze:] + n_layers_to_unfreeze = min(layers_to_unfreeze, len(base_model.layers)) + layers_to_process = base_model.layers[-n_layers_to_unfreeze:] else: raise TypeError( "layers_to_unfreeze must be an int or str, received: " diff --git a/examples/vision/ipynb/image_classification_efficientnet_fine_tuning.ipynb b/examples/vision/ipynb/image_classification_efficientnet_fine_tuning.ipynb index f1a5b3b4a1..2dee6fcf5f 100644 --- a/examples/vision/ipynb/image_classification_efficientnet_fine_tuning.ipynb +++ b/examples/vision/ipynb/image_classification_efficientnet_fine_tuning.ipynb @@ -222,25 +222,21 @@ "\n", "print(f\"Found {NUM_CLASSES} classes, {len(train_files)} training images, {len(test_files)} test images\")\n", "\n", - "# Load images and labels\n", - "def load_images_and_labels(file_list, base_dir):\n", - " images, labels = [], []\n", + "# Prepare image paths and labels (lazy loading - no images loaded into memory yet)\n", + "def prepare_paths_and_labels(file_list, base_dir):\n", + " image_paths, labels = [], []\n", " for file_path in file_list:\n", " class_name = file_path.split(\"/\")[0]\n", " img_path = base_dir / \"Images\" / file_path\n", - " try:\n", - " img = Image.open(img_path).convert(\"RGB\")\n", - " images.append(np.array(img))\n", + " if img_path.exists():\n", + " image_paths.append(str(img_path))\n", " labels.append(class_to_idx[class_name])\n", - " except Exception as e:\n", - " print(f\"Warning: Could not load {img_path}: {e}\")\n", - " return images, np.array(labels)\n", - "\n", - "print(\"Loading training images...\")\n", - "train_images, train_labels = load_images_and_labels(train_files, data_dir)\n", - "print(\"Loading test images...\")\n", - "test_images, test_labels = load_images_and_labels(test_files, data_dir)\n", - "print(f\"Loaded {len(train_images)} train and {len(test_images)} test images\")" + " return image_paths, np.array(labels)\n", + "\n", + "print(\"Preparing dataset paths...\")\n", + "train_image_paths, train_labels = prepare_paths_and_labels(train_files, data_dir)\n", + "test_image_paths, test_labels = prepare_paths_and_labels(test_files, data_dir)\n", + "print(f\"Found {len(train_image_paths)} train and {len(test_image_paths)} test images\")" ] }, { @@ -252,7 +248,10 @@ "Each image can have a different shape, so we resize them to a shared input size\n", "for EfficientNet. In Keras 3, we do this in a backend-agnostic `PyDataset` pipeline\n", "using `keras.ops.image.resize`, which works across TensorFlow, JAX, and PyTorch backends,\n", - "rather than the TensorFlow-specific `tf.data` mapping steps." + "rather than the TensorFlow-specific `tf.data` mapping steps.\n", + "\n", + "Images are loaded lazily from disk in `__getitem__` to avoid loading all 20,580 images\n", + "into memory at once, which would consume several gigabytes of RAM and cause OOM issues." ] }, { @@ -264,9 +263,9 @@ "outputs": [], "source": [ "class ResizeOnlyDataset(keras.utils.PyDataset):\n", - " def __init__(self, images, labels, img_size, batch_size=1, **kwargs):\n", + " def __init__(self, image_paths, labels, img_size, batch_size=1, **kwargs):\n", " super().__init__(**kwargs)\n", - " self.images = images\n", + " self.image_paths = image_paths\n", " self.labels = labels\n", " self.img_size = img_size\n", " self.batch_size = batch_size\n", @@ -277,17 +276,13 @@ "\n", " def __getitem__(self, idx):\n", " batch_indices = self.indices[idx * self.batch_size : (idx + 1) * self.batch_size]\n", - " batch_images = np.stack(\n", - " [\n", - " np.array(\n", - " keras.ops.image.resize(\n", - " np.array(self.images[i], dtype=\"float32\"),\n", - " (self.img_size, self.img_size),\n", - " )\n", - " )\n", - " for i in batch_indices\n", - " ]\n", - " )\n", + " batch_images = []\n", + " for i in batch_indices:\n", + " img = Image.open(self.image_paths[i]).convert(\"RGB\")\n", + " img_array = np.array(img, dtype=\"float32\")\n", + " img_resized = keras.ops.image.resize(img_array, (self.img_size, self.img_size))\n", + " batch_images.append(np.array(img_resized))\n", + " batch_images = np.stack(batch_images)\n", " batch_labels = self.labels[batch_indices]\n", " if self.batch_size == 1:\n", " return batch_images[0], batch_labels[0]\n", @@ -295,8 +290,8 @@ "\n", "\n", "# Preview stream with resized images for visualization below\n", - "preview_train = ResizeOnlyDataset(train_images, train_labels, IMG_SIZE, batch_size=1)\n", - "preview_test = ResizeOnlyDataset(test_images, test_labels, IMG_SIZE, batch_size=1)" + "preview_train = ResizeOnlyDataset(train_image_paths, train_labels, IMG_SIZE, batch_size=1)\n", + "preview_test = ResizeOnlyDataset(test_image_paths, test_labels, IMG_SIZE, batch_size=1)" ] }, { @@ -426,7 +421,7 @@ "class StanfordDogsDataset(keras.utils.PyDataset):\n", " def __init__(\n", " self,\n", - " images,\n", + " image_paths,\n", " labels,\n", " num_classes,\n", " img_size,\n", @@ -436,7 +431,7 @@ " **kwargs,\n", " ):\n", " super().__init__(**kwargs)\n", - " self.images = images\n", + " self.image_paths = image_paths\n", " self.labels = labels\n", " self.num_classes = num_classes\n", " self.img_size = img_size\n", @@ -459,17 +454,14 @@ " idx * self.batch_size : (idx + 1) * self.batch_size\n", " ]\n", "\n", - " batch_images = np.stack(\n", - " [\n", - " np.array(\n", - " keras.ops.image.resize(\n", - " np.array(self.images[i], dtype=\"float32\"),\n", - " (self.img_size, self.img_size),\n", - " )\n", - " )\n", - " for i in batch_indices\n", - " ]\n", - " )\n", + " # Load images lazily from disk\n", + " batch_images = []\n", + " for i in batch_indices:\n", + " img = Image.open(self.image_paths[i]).convert(\"RGB\")\n", + " img_array = np.array(img, dtype=\"float32\")\n", + " img_resized = keras.ops.image.resize(img_array, (self.img_size, self.img_size))\n", + " batch_images.append(np.array(img_resized))\n", + " batch_images = np.stack(batch_images)\n", "\n", " if self.augment:\n", " batch_images = np.array(img_augmentation(batch_images))\n", @@ -482,7 +474,7 @@ "\n", "\n", "ds_train = StanfordDogsDataset(\n", - " train_images,\n", + " train_image_paths,\n", " train_labels,\n", " num_classes=NUM_CLASSES,\n", " img_size=IMG_SIZE,\n", @@ -494,7 +486,7 @@ ")\n", "\n", "ds_test = StanfordDogsDataset(\n", - " test_images,\n", + " test_image_paths,\n", " test_labels,\n", " num_classes=NUM_CLASSES,\n", " img_size=IMG_SIZE,\n", @@ -733,25 +725,33 @@ " if metrics is None:\n", " metrics = [\"accuracy\"]\n", "\n", + " # Access the nested EfficientNetB0 base model\n", + " base_model = model.get_layer(\"efficientnetb0\")\n", + " base_model.trainable = True\n", + "\n", + " # First, freeze all layers in the base model\n", + " for layer in base_model.layers:\n", + " layer.trainable = False\n", + "\n", " if isinstance(layers_to_unfreeze, str):\n", " unfreeze_from = None\n", - " for i, layer in enumerate(model.layers):\n", + " for i, layer in enumerate(base_model.layers):\n", " if layers_to_unfreeze in layer.name:\n", " unfreeze_from = i\n", " break\n", " if unfreeze_from is None:\n", " raise ValueError(f\"No layer name contains {layers_to_unfreeze!r}.\")\n", - " layers_to_process = model.layers[unfreeze_from:]\n", + " layers_to_process = base_model.layers[unfreeze_from:]\n", " elif isinstance(layers_to_unfreeze, int):\n", - " n_layers_to_unfreeze = min(layers_to_unfreeze, len(model.layers))\n", - " layers_to_process = model.layers[-n_layers_to_unfreeze:]\n", + " n_layers_to_unfreeze = min(layers_to_unfreeze, len(base_model.layers))\n", + " layers_to_process = base_model.layers[-n_layers_to_unfreeze:]\n", " else:\n", " raise TypeError(\n", " \"layers_to_unfreeze must be an int or str, received: \"\n", " f\"{type(layers_to_unfreeze)}\"\n", " )\n", "\n", - " # We keep BatchNorm layers frozen -- see \"Tips for fine tuning\n", + " # We keep BatchNorm layers frozen -- see \"Tips for fine-tuning\n", " # EfficientNet\" in the tutorial for why.\n", " for layer in layers_to_process:\n", " if not isinstance(layer, layers.BatchNormalization):\n", diff --git a/examples/vision/md/image_classification_efficientnet_fine_tuning.md b/examples/vision/md/image_classification_efficientnet_fine_tuning.md index 235d00d62e..739effee04 100644 --- a/examples/vision/md/image_classification_efficientnet_fine_tuning.md +++ b/examples/vision/md/image_classification_efficientnet_fine_tuning.md @@ -205,26 +205,22 @@ print( ) -# Load images and labels -def load_images_and_labels(file_list, base_dir): - images, labels = [], [] +# Prepare image paths and labels (lazy loading - no images loaded into memory yet) +def prepare_paths_and_labels(file_list, base_dir): + image_paths, labels = [], [] for file_path in file_list: class_name = file_path.split("/")[0] img_path = base_dir / "Images" / file_path - try: - img = Image.open(img_path).convert("RGB") - images.append(np.array(img)) + if img_path.exists(): + image_paths.append(str(img_path)) labels.append(class_to_idx[class_name]) - except Exception as e: - print(f"Warning: Could not load {img_path}: {e}") - return images, np.array(labels) + return image_paths, np.array(labels) -print("Loading training images...") -train_images, train_labels = load_images_and_labels(train_files, data_dir) -print("Loading test images...") -test_images, test_labels = load_images_and_labels(test_files, data_dir) -print(f"Loaded {len(train_images)} train and {len(test_images)} test images") +print("Preparing dataset paths...") +train_image_paths, train_labels = prepare_paths_and_labels(train_files, data_dir) +test_image_paths, test_labels = prepare_paths_and_labels(test_files, data_dir) +print(f"Found {len(train_image_paths)} train and {len(test_image_paths)} test images") ``` Each image can have a different shape, so we resize them to a shared input size @@ -232,12 +228,15 @@ for EfficientNet. In Keras 3, we do this in a backend-agnostic `PyDataset` pipel using `keras.ops.image.resize`, which works across TensorFlow, JAX, and PyTorch backends, rather than the TensorFlow-specific `tf.data` mapping steps. +Images are loaded lazily from disk in `__getitem__` to avoid loading all 20,580 images +into memory at once, which would consume several gigabytes of RAM and cause OOM issues. + ```python class ResizeOnlyDataset(keras.utils.PyDataset): - def __init__(self, images, labels, img_size, batch_size=1, **kwargs): + def __init__(self, image_paths, labels, img_size, batch_size=1, **kwargs): super().__init__(**kwargs) - self.images = images + self.image_paths = image_paths self.labels = labels self.img_size = img_size self.batch_size = batch_size @@ -250,17 +249,15 @@ class ResizeOnlyDataset(keras.utils.PyDataset): batch_indices = self.indices[ idx * self.batch_size : (idx + 1) * self.batch_size ] - batch_images = np.stack( - [ - np.array( - keras.ops.image.resize( - np.array(self.images[i], dtype="float32"), - (self.img_size, self.img_size), - ) - ) - for i in batch_indices - ] - ) + batch_images = [] + for i in batch_indices: + img = Image.open(self.image_paths[i]).convert("RGB") + img_array = np.array(img, dtype="float32") + img_resized = keras.ops.image.resize( + img_array, (self.img_size, self.img_size) + ) + batch_images.append(np.array(img_resized)) + batch_images = np.stack(batch_images) batch_labels = self.labels[batch_indices] if self.batch_size == 1: return batch_images[0], batch_labels[0] @@ -268,8 +265,10 @@ class ResizeOnlyDataset(keras.utils.PyDataset): # Preview stream with resized images for visualization below -preview_train = ResizeOnlyDataset(train_images, train_labels, IMG_SIZE, batch_size=1) -preview_test = ResizeOnlyDataset(test_images, test_labels, IMG_SIZE, batch_size=1) +preview_train = ResizeOnlyDataset( + train_image_paths, train_labels, IMG_SIZE, batch_size=1 +) +preview_test = ResizeOnlyDataset(test_image_paths, test_labels, IMG_SIZE, batch_size=1) ``` ### Visualizing the data @@ -363,7 +362,7 @@ and works seamlessly across TensorFlow, JAX, and PyTorch backends. class StanfordDogsDataset(keras.utils.PyDataset): def __init__( self, - images, + image_paths, labels, num_classes, img_size, @@ -373,7 +372,7 @@ class StanfordDogsDataset(keras.utils.PyDataset): **kwargs, ): super().__init__(**kwargs) - self.images = images + self.image_paths = image_paths self.labels = labels self.num_classes = num_classes self.img_size = img_size @@ -396,17 +395,16 @@ class StanfordDogsDataset(keras.utils.PyDataset): idx * self.batch_size : (idx + 1) * self.batch_size ] - batch_images = np.stack( - [ - np.array( - keras.ops.image.resize( - np.array(self.images[i], dtype="float32"), - (self.img_size, self.img_size), - ) - ) - for i in batch_indices - ] - ) + # Load images lazily from disk + batch_images = [] + for i in batch_indices: + img = Image.open(self.image_paths[i]).convert("RGB") + img_array = np.array(img, dtype="float32") + img_resized = keras.ops.image.resize( + img_array, (self.img_size, self.img_size) + ) + batch_images.append(np.array(img_resized)) + batch_images = np.stack(batch_images) if self.augment: batch_images = np.array(img_augmentation(batch_images)) @@ -419,7 +417,7 @@ class StanfordDogsDataset(keras.utils.PyDataset): ds_train = StanfordDogsDataset( - train_images, + train_image_paths, train_labels, num_classes=NUM_CLASSES, img_size=IMG_SIZE, @@ -431,7 +429,7 @@ ds_train = StanfordDogsDataset( ) ds_test = StanfordDogsDataset( - test_images, + test_image_paths, test_labels, num_classes=NUM_CLASSES, img_size=IMG_SIZE, @@ -1518,18 +1516,26 @@ def unfreeze_model( if metrics is None: metrics = ["accuracy"] + # Access the nested EfficientNetB0 base model + base_model = model.get_layer("efficientnetb0") + base_model.trainable = True + + # First, freeze all layers in the base model + for layer in base_model.layers: + layer.trainable = False + if isinstance(layers_to_unfreeze, str): unfreeze_from = None - for i, layer in enumerate(model.layers): + for i, layer in enumerate(base_model.layers): if layers_to_unfreeze in layer.name: unfreeze_from = i break if unfreeze_from is None: raise ValueError(f"No layer name contains {layers_to_unfreeze!r}.") - layers_to_process = model.layers[unfreeze_from:] + layers_to_process = base_model.layers[unfreeze_from:] elif isinstance(layers_to_unfreeze, int): - n_layers_to_unfreeze = min(layers_to_unfreeze, len(model.layers)) - layers_to_process = model.layers[-n_layers_to_unfreeze:] + n_layers_to_unfreeze = min(layers_to_unfreeze, len(base_model.layers)) + layers_to_process = base_model.layers[-n_layers_to_unfreeze:] else: raise TypeError( "layers_to_unfreeze must be an int or str, received: " From d934281a0b9bea550bc61f907857e0a2c614c806 Mon Sep 17 00:00:00 2001 From: Harshith K Date: Mon, 24 Aug 2026 19:44:37 +0530 Subject: [PATCH 3/7] Address PR review feedback: improve unfreeze_model robustness and fix markdown formatting - Make base model lookup dynamic instead of hardcoded 'efficientnetb0' - Add validation for layers_to_unfreeze parameter (must be > 0) - Update docstring to reference 'base model's layers' instead of 'model.layers' - Remove excessive blank lines in markdown file - Fix empty line before in markdown fenced code block Addresses comments from @maitry63 and @kirisakow in PR #2393 --- ...classification_efficientnet_fine_tuning.py | 22 +++- ...ssification_efficientnet_fine_tuning.ipynb | 109 +++++++++++++----- ...classification_efficientnet_fine_tuning.md | 35 +++--- 3 files changed, 118 insertions(+), 48 deletions(-) diff --git a/examples/vision/image_classification_efficientnet_fine_tuning.py b/examples/vision/image_classification_efficientnet_fine_tuning.py index bf8a373bdd..334f245635 100644 --- a/examples/vision/image_classification_efficientnet_fine_tuning.py +++ b/examples/vision/image_classification_efficientnet_fine_tuning.py @@ -564,9 +564,9 @@ def unfreeze_model( Args: model: A `keras.Model` instance to unfreeze in place. layers_to_unfreeze: Either an `int` giving the number of layers, - counted from the end of `model.layers`, to unfreeze, or a `str` + counted from the end of the base model's layers, to unfreeze, or a `str` substring to match against layer names -- the first matching - layer and every layer after it (in `model.layers` order) are + layer and every layer after it (in the base model's layers order) are unfrozen. Use a string like `"block7"` to respect EfficientNet's residual block boundaries instead of an arbitrary layer count. Defaults to `20`. @@ -585,8 +585,18 @@ def unfreeze_model( if metrics is None: metrics = ["accuracy"] - # Access the nested EfficientNetB0 base model - base_model = model.get_layer("efficientnetb0") + # Access the nested EfficientNet base model by finding the first layer + # with 'efficientnet' in its name (case-insensitive) + base_model = None + for layer in model.layers: + if "efficientnet" in layer.name.lower(): + base_model = layer + break + if base_model is None: + raise ValueError( + "Could not find EfficientNet base model in the model. " + "Expected a layer with 'efficientnet' in its name." + ) base_model.trainable = True # First, freeze all layers in the base model @@ -603,6 +613,10 @@ def unfreeze_model( raise ValueError(f"No layer name contains {layers_to_unfreeze!r}.") layers_to_process = base_model.layers[unfreeze_from:] elif isinstance(layers_to_unfreeze, int): + if layers_to_unfreeze <= 0: + raise ValueError( + f"layers_to_unfreeze must be > 0, got {layers_to_unfreeze}" + ) n_layers_to_unfreeze = min(layers_to_unfreeze, len(base_model.layers)) layers_to_process = base_model.layers[-n_layers_to_unfreeze:] else: diff --git a/examples/vision/ipynb/image_classification_efficientnet_fine_tuning.ipynb b/examples/vision/ipynb/image_classification_efficientnet_fine_tuning.ipynb index 2dee6fcf5f..11d7d30fc4 100644 --- a/examples/vision/ipynb/image_classification_efficientnet_fine_tuning.ipynb +++ b/examples/vision/ipynb/image_classification_efficientnet_fine_tuning.ipynb @@ -76,7 +76,7 @@ "use EfficientNetB0 for classifying 1000 classes of images from ImageNet, run:\n", "\n", "```python\n", - "from keras.applications import EfficientNetB0\n", + "from tensorflow.keras.applications import EfficientNetB0\n", "model = EfficientNetB0(weights='imagenet')\n", "```\n", "\n", @@ -111,8 +111,15 @@ "layer into prediction of the 1000 ImageNet classes. Replacing the top layer with custom\n", "layers allows using EfficientNet as a feature extractor in a transfer learning workflow.\n", "\n", - "A side note: in Keras 3, input preprocessing remains built into EfficientNet.\n", - "Inputs are expected as float pixel values in the `[0, 255]` range.\n", + "Another argument in the model constructor worth noticing is `drop_connect_rate` which controls\n", + "the dropout rate responsible for [stochastic depth](https://arxiv.org/abs/1603.09382).\n", + "This parameter serves as a toggle for extra regularization in finetuning, but does not\n", + "affect loaded weights. For example, when stronger regularization is desired, try:\n", + "\n", + "```python\n", + "model = EfficientNetB0(weights='imagenet', drop_connect_rate=0.4)\n", + "```\n", + "The default value is 0.2.\n", "\n", "## Example: EfficientNetB0 for Stanford Dogs.\n", "\n", @@ -133,7 +140,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 0, "metadata": { "colab_type": "code" }, @@ -152,7 +159,8 @@ "\n", "# IMG_SIZE is determined by EfficientNet model choice\n", "IMG_SIZE = 224\n", - "BATCH_SIZE = 64" + "BATCH_SIZE = 64\n", + "" ] }, { @@ -178,7 +186,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 0, "metadata": { "colab_type": "code" }, @@ -186,11 +194,13 @@ "source": [ "# Download and extract Stanford Dogs dataset\n", "import scipy.io\n", + "\n", "dataset_url = \"http://vision.stanford.edu/aditya86/ImageNetDogs/images.tar\"\n", "lists_url = \"http://vision.stanford.edu/aditya86/ImageNetDogs/lists.tar\"\n", "data_dir = Path(\"./stanford_dogs_data\")\n", "data_dir.mkdir(exist_ok=True)\n", "\n", + "\n", "def download_and_extract(url, extract_to):\n", " filename = url.split(\"/\")[-1]\n", " filepath = data_dir / filename\n", @@ -202,14 +212,17 @@ " tar.extractall(extract_to)\n", " return extract_to\n", "\n", + "\n", "# Download dataset\n", "images_dir = download_and_extract(dataset_url, data_dir)\n", "lists_dir = download_and_extract(lists_url, data_dir)\n", "\n", + "\n", "# Parse train/test splits\n", "def load_file_list(filepath):\n", " mat = scipy.io.loadmat(filepath)\n", - " return [item[0][0] for item in mat['file_list']]\n", + " return [item[0][0] for item in mat[\"file_list\"]]\n", + "\n", "\n", "train_files = load_file_list(data_dir / \"train_list.mat\")\n", "test_files = load_file_list(data_dir / \"test_list.mat\")\n", @@ -220,7 +233,10 @@ "class_to_idx = {name: idx for idx, name in enumerate(class_names)}\n", "NUM_CLASSES = len(class_names)\n", "\n", - "print(f\"Found {NUM_CLASSES} classes, {len(train_files)} training images, {len(test_files)} test images\")\n", + "print(\n", + " f\"Found {NUM_CLASSES} classes, {len(train_files)} training images, {len(test_files)} test images\"\n", + ")\n", + "\n", "\n", "# Prepare image paths and labels (lazy loading - no images loaded into memory yet)\n", "def prepare_paths_and_labels(file_list, base_dir):\n", @@ -233,10 +249,12 @@ " labels.append(class_to_idx[class_name])\n", " return image_paths, np.array(labels)\n", "\n", + "\n", "print(\"Preparing dataset paths...\")\n", "train_image_paths, train_labels = prepare_paths_and_labels(train_files, data_dir)\n", "test_image_paths, test_labels = prepare_paths_and_labels(test_files, data_dir)\n", - "print(f\"Found {len(train_image_paths)} train and {len(test_image_paths)} test images\")" + "print(f\"Found {len(train_image_paths)} train and {len(test_image_paths)} test images\")\n", + "" ] }, { @@ -262,6 +280,7 @@ }, "outputs": [], "source": [ + "\n", "class ResizeOnlyDataset(keras.utils.PyDataset):\n", " def __init__(self, image_paths, labels, img_size, batch_size=1, **kwargs):\n", " super().__init__(**kwargs)\n", @@ -275,12 +294,16 @@ " return int(np.ceil(len(self.labels) / self.batch_size))\n", "\n", " def __getitem__(self, idx):\n", - " batch_indices = self.indices[idx * self.batch_size : (idx + 1) * self.batch_size]\n", + " batch_indices = self.indices[\n", + " idx * self.batch_size : (idx + 1) * self.batch_size\n", + " ]\n", " batch_images = []\n", " for i in batch_indices:\n", " img = Image.open(self.image_paths[i]).convert(\"RGB\")\n", " img_array = np.array(img, dtype=\"float32\")\n", - " img_resized = keras.ops.image.resize(img_array, (self.img_size, self.img_size))\n", + " img_resized = keras.ops.image.resize(\n", + " img_array, (self.img_size, self.img_size)\n", + " )\n", " batch_images.append(np.array(img_resized))\n", " batch_images = np.stack(batch_images)\n", " batch_labels = self.labels[batch_indices]\n", @@ -290,7 +313,9 @@ "\n", "\n", "# Preview stream with resized images for visualization below\n", - "preview_train = ResizeOnlyDataset(train_image_paths, train_labels, IMG_SIZE, batch_size=1)\n", + "preview_train = ResizeOnlyDataset(\n", + " train_image_paths, train_labels, IMG_SIZE, batch_size=1\n", + ")\n", "preview_test = ResizeOnlyDataset(test_image_paths, test_labels, IMG_SIZE, batch_size=1)" ] }, @@ -313,6 +338,7 @@ }, "outputs": [], "source": [ + "\n", "def format_label(label):\n", " class_name = class_names[int(label)]\n", " return class_name.split(\"-\")[1] # Extract breed name from \"n02085620-Chihuahua\"\n", @@ -324,7 +350,8 @@ " ax = plt.subplot(3, 3, i + 1)\n", " plt.imshow(np.array(image).astype(\"uint8\"))\n", " plt.title(\"{}\".format(format_label(label)))\n", - " plt.axis(\"off\")" + " plt.axis(\"off\")\n", + "" ] }, { @@ -358,7 +385,8 @@ "def img_augmentation(images):\n", " for layer in img_augmentation_layers:\n", " images = layer(images)\n", - " return images" + " return images\n", + "" ] }, { @@ -389,7 +417,8 @@ " aug_img = np.array(aug_img)\n", " plt.imshow(aug_img[0].astype(\"uint8\"))\n", " plt.title(\"{}\".format(format_label(first_label)))\n", - " plt.axis(\"off\")" + " plt.axis(\"off\")\n", + "" ] }, { @@ -418,6 +447,7 @@ }, "outputs": [], "source": [ + "\n", "class StanfordDogsDataset(keras.utils.PyDataset):\n", " def __init__(\n", " self,\n", @@ -459,7 +489,9 @@ " for i in batch_indices:\n", " img = Image.open(self.image_paths[i]).convert(\"RGB\")\n", " img_array = np.array(img, dtype=\"float32\")\n", - " img_resized = keras.ops.image.resize(img_array, (self.img_size, self.img_size))\n", + " img_resized = keras.ops.image.resize(\n", + " img_array, (self.img_size, self.img_size)\n", + " )\n", " batch_images.append(np.array(img_resized))\n", " batch_images = np.stack(batch_images)\n", "\n", @@ -493,7 +525,8 @@ " batch_size=BATCH_SIZE,\n", " augment=False,\n", " shuffle=False,\n", - ")" + ")\n", + "" ] }, { @@ -530,7 +563,8 @@ "model.summary()\n", "\n", "epochs = 40 # @param {type: \"slider\", min:10, max:100}\n", - "hist = model.fit(ds_train, epochs=epochs, validation_data=ds_test)" + "hist = model.fit(ds_train, epochs=epochs, validation_data=ds_test)\n", + "" ] }, { @@ -595,6 +629,7 @@ }, "outputs": [], "source": [ + "\n", "def build_model(num_classes):\n", " inputs = layers.Input(shape=(IMG_SIZE, IMG_SIZE, 3))\n", " model = EfficientNetB0(include_top=False, input_tensor=inputs, weights=\"imagenet\")\n", @@ -616,7 +651,8 @@ " model.compile(\n", " optimizer=optimizer, loss=\"categorical_crossentropy\", metrics=[\"accuracy\"]\n", " )\n", - " return model" + " return model\n", + "" ] }, { @@ -692,6 +728,7 @@ }, "outputs": [], "source": [ + "\n", "def unfreeze_model(\n", " model,\n", " layers_to_unfreeze=20,\n", @@ -704,9 +741,9 @@ " Args:\n", " model: A `keras.Model` instance to unfreeze in place.\n", " layers_to_unfreeze: Either an `int` giving the number of layers,\n", - " counted from the end of `model.layers`, to unfreeze, or a `str`\n", + " counted from the end of the base model's layers, to unfreeze, or a `str`\n", " substring to match against layer names -- the first matching\n", - " layer and every layer after it (in `model.layers` order) are\n", + " layer and every layer after it (in the base model's layers order) are\n", " unfrozen. Use a string like `\"block7\"` to respect EfficientNet's\n", " residual block boundaries instead of an arbitrary layer count.\n", " Defaults to `20`.\n", @@ -725,8 +762,18 @@ " if metrics is None:\n", " metrics = [\"accuracy\"]\n", "\n", - " # Access the nested EfficientNetB0 base model\n", - " base_model = model.get_layer(\"efficientnetb0\")\n", + " # Access the nested EfficientNet base model by finding the first layer\n", + " # with 'efficientnet' in its name (case-insensitive)\n", + " base_model = None\n", + " for layer in model.layers:\n", + " if \"efficientnet\" in layer.name.lower():\n", + " base_model = layer\n", + " break\n", + " if base_model is None:\n", + " raise ValueError(\n", + " \"Could not find EfficientNet base model in the model. \"\n", + " \"Expected a layer with 'efficientnet' in its name.\"\n", + " )\n", " base_model.trainable = True\n", "\n", " # First, freeze all layers in the base model\n", @@ -743,6 +790,10 @@ " raise ValueError(f\"No layer name contains {layers_to_unfreeze!r}.\")\n", " layers_to_process = base_model.layers[unfreeze_from:]\n", " elif isinstance(layers_to_unfreeze, int):\n", + " if layers_to_unfreeze <= 0:\n", + " raise ValueError(\n", + " f\"layers_to_unfreeze must be > 0, got {layers_to_unfreeze}\"\n", + " )\n", " n_layers_to_unfreeze = min(layers_to_unfreeze, len(base_model.layers))\n", " layers_to_process = base_model.layers[-n_layers_to_unfreeze:]\n", " else:\n", @@ -808,10 +859,12 @@ }, { "cell_type": "markdown", - "metadata": {}, + "metadata": { + "colab_type": "text" + }, "source": [ "## Relevant Chapters from Deep Learning with Python\n", - "- [Chapter 8: Image classification](https://deeplearningwithpython.io/chapters/chapter08_image-classification)\n" + "- [Chapter 8: Image classification](https://deeplearningwithpython.io/chapters/chapter08_image-classification)" ] } ], @@ -825,7 +878,7 @@ "toc_visible": true }, "kernelspec": { - "display_name": "keras_io_311_env (3.11.3)", + "display_name": "Python 3", "language": "python", "name": "python3" }, @@ -839,9 +892,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.3" + "version": "3.7.0" } }, "nbformat": 4, "nbformat_minor": 0 -} +} \ No newline at end of file diff --git a/examples/vision/md/image_classification_efficientnet_fine_tuning.md b/examples/vision/md/image_classification_efficientnet_fine_tuning.md index 739effee04..28dbc1e82a 100644 --- a/examples/vision/md/image_classification_efficientnet_fine_tuning.md +++ b/examples/vision/md/image_classification_efficientnet_fine_tuning.md @@ -469,9 +469,6 @@ hist = model.fit(ds_train, epochs=epochs, validation_data=ds_test)
Model: "efficientnetb0"
 
- - -
┏━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━┓
 ┃ Layer (type)         Output Shape       Param #  Connected to         ┃
 ┡━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━┩
@@ -1196,21 +1193,12 @@ hist = model.fit(ds_train, epochs=epochs, validation_data=ds_test)
 └─────────────────────┴───────────────────┴─────────┴──────────────────────┘
 
- - -
 Total params: 4,203,291 (16.03 MB)
 
- - -
 Trainable params: 4,161,268 (15.87 MB)
 
- - -
 Non-trainable params: 42,023 (164.16 KB)
 
@@ -1495,9 +1483,9 @@ def unfreeze_model( Args: model: A `keras.Model` instance to unfreeze in place. layers_to_unfreeze: Either an `int` giving the number of layers, - counted from the end of `model.layers`, to unfreeze, or a `str` + counted from the end of the base model's layers, to unfreeze, or a `str` substring to match against layer names -- the first matching - layer and every layer after it (in `model.layers` order) are + layer and every layer after it (in the base model's layers order) are unfrozen. Use a string like `"block7"` to respect EfficientNet's residual block boundaries instead of an arbitrary layer count. Defaults to `20`. @@ -1516,8 +1504,18 @@ def unfreeze_model( if metrics is None: metrics = ["accuracy"] - # Access the nested EfficientNetB0 base model - base_model = model.get_layer("efficientnetb0") + # Access the nested EfficientNet base model by finding the first layer + # with 'efficientnet' in its name (case-insensitive) + base_model = None + for layer in model.layers: + if "efficientnet" in layer.name.lower(): + base_model = layer + break + if base_model is None: + raise ValueError( + "Could not find EfficientNet base model in the model. " + "Expected a layer with 'efficientnet' in its name." + ) base_model.trainable = True # First, freeze all layers in the base model @@ -1534,6 +1532,10 @@ def unfreeze_model( raise ValueError(f"No layer name contains {layers_to_unfreeze!r}.") layers_to_process = base_model.layers[unfreeze_from:] elif isinstance(layers_to_unfreeze, int): + if layers_to_unfreeze <= 0: + raise ValueError( + f"layers_to_unfreeze must be > 0, got {layers_to_unfreeze}" + ) n_layers_to_unfreeze = min(layers_to_unfreeze, len(base_model.layers)) layers_to_process = base_model.layers[-n_layers_to_unfreeze:] else: @@ -1572,6 +1574,7 @@ Epoch 4/4 187/187 ━━━━━━━━━━━━━━━━━━━━ 79s 419ms/step - accuracy: 0.6625 - loss: 1.1775 - val_accuracy: 0.7701 - val_loss: 0.8284 ``` + ![png](/img/examples/vision/image_classification_efficientnet_fine_tuning/image_classification_efficientnet_fine_tuning_25_1.png) From fb3d23b10a6e33399e940b7481bf3ac3f48c740e Mon Sep 17 00:00:00 2001 From: Harshith K Date: Mon, 24 Aug 2026 20:33:11 +0530 Subject: [PATCH 4/7] Fix CI: update Keras version from 3.15.0 to 3.15.1 The autogen.py script had a hardcoded Keras version (v3.15.0) that didn't match the current installed package version (3.15.1), causing the CI build to fail. --- scripts/autogen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/autogen.py b/scripts/autogen.py index ac55133857..18828ddc6f 100644 --- a/scripts/autogen.py +++ b/scripts/autogen.py @@ -32,7 +32,7 @@ GUIDES_GH_LOCATION = Path("keras-team") / "keras-io" / "blob" / "master" / "guides" KERAS_TEAM_GH = "https://github.com/keras-team" PROJECT_URL = { - "keras": f"{KERAS_TEAM_GH}/keras/tree/v3.15.0/", + "keras": f"{KERAS_TEAM_GH}/keras/tree/v3.15.1/", "keras_tuner": f"{KERAS_TEAM_GH}/keras-tuner/tree/v1.4.8/", "keras_hub": f"{KERAS_TEAM_GH}/keras-hub/tree/v0.30.0/", "tf_keras": f"{KERAS_TEAM_GH}/tf-keras/tree/v2.20.0/", From 22813cd5cc9499835a1a9857ce6e82585e6b19e8 Mon Sep 17 00:00:00 2001 From: Harshith K Date: Mon, 24 Aug 2026 20:58:29 +0530 Subject: [PATCH 5/7] Fix CI: update keras_hub version from 0.30.0 to 0.31.1 --- scripts/autogen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/autogen.py b/scripts/autogen.py index 18828ddc6f..cfc8974075 100644 --- a/scripts/autogen.py +++ b/scripts/autogen.py @@ -34,7 +34,7 @@ PROJECT_URL = { "keras": f"{KERAS_TEAM_GH}/keras/tree/v3.15.1/", "keras_tuner": f"{KERAS_TEAM_GH}/keras-tuner/tree/v1.4.8/", - "keras_hub": f"{KERAS_TEAM_GH}/keras-hub/tree/v0.30.0/", + "keras_hub": f"{KERAS_TEAM_GH}/keras-hub/tree/v0.31.1/", "tf_keras": f"{KERAS_TEAM_GH}/tf-keras/tree/v2.20.0/", "keras_rs": f"{KERAS_TEAM_GH}/keras-rs/tree/v0.4.0/", } From 12c2c29bd6dbe3621f685ffcce0a55e472b13834 Mon Sep 17 00:00:00 2001 From: Harshith K Date: Thu, 27 Aug 2026 20:57:33 +0530 Subject: [PATCH 6/7] Fix CI: restore upstream's dynamic version detection in autogen.py The upstream refactored autogen.py to automatically detect package versions instead of hardcoding them. This restores the _build_project_url() function and _MODULE_TO_REPO mapping from upstream. --- scripts/autogen.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/scripts/autogen.py b/scripts/autogen.py index fa9aee5f98..78959b7a10 100644 --- a/scripts/autogen.py +++ b/scripts/autogen.py @@ -33,12 +33,14 @@ EXAMPLES_GH_LOCATION = Path("keras-team") / "keras-io" / "blob" / "master" / "examples" GUIDES_GH_LOCATION = Path("keras-team") / "keras-io" / "blob" / "master" / "guides" KERAS_TEAM_GH = "https://github.com/keras-team" -PROJECT_URL = { - "keras": f"{KERAS_TEAM_GH}/keras/tree/v3.15.1/", - "keras_tuner": f"{KERAS_TEAM_GH}/keras-tuner/tree/v1.4.8/", - "keras_hub": f"{KERAS_TEAM_GH}/keras-hub/tree/v0.31.1/", - "tf_keras": f"{KERAS_TEAM_GH}/tf-keras/tree/v2.20.0/", - "keras_rs": f"{KERAS_TEAM_GH}/keras-rs/tree/v0.4.0/", + +# Mapping from Python module name to GitHub repo name. +_MODULE_TO_REPO = { + "keras": "keras", + "keras_tuner": "keras-tuner", + "keras_hub": "keras-hub", + "tf_keras": "tf-keras", + "keras_rs": "keras-rs", } From 43d6683b43a9e80fb64985d3fefc3d5ebc182687 Mon Sep 17 00:00:00 2001 From: Harshith K Date: Mon, 31 Aug 2026 16:40:48 +0530 Subject: [PATCH 7/7] Address PR review feedback from @laxmareddyp MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Update code examples to use Keras 3 import paths (keras.applications instead of tensorflow.keras.applications) - Remove unused 'import os' - Move 'import scipy.io' from inline to top-level imports - Remove stale TensorFlow TPU link from documentation - Fix typo: 'arbitray' → 'arbitrary' - Add filter='data' to tar.extractall() for Python 3.12+ compatibility All changes applied consistently across .py, .ipynb, and .md files. --- ...e_classification_efficientnet_fine_tuning.py | 17 ++++------------- ...lassification_efficientnet_fine_tuning.ipynb | 17 ++++------------- ...e_classification_efficientnet_fine_tuning.md | 17 ++++------------- 3 files changed, 12 insertions(+), 39 deletions(-) diff --git a/examples/vision/image_classification_efficientnet_fine_tuning.py b/examples/vision/image_classification_efficientnet_fine_tuning.py index 334f245635..6014bb6612 100644 --- a/examples/vision/image_classification_efficientnet_fine_tuning.py +++ b/examples/vision/image_classification_efficientnet_fine_tuning.py @@ -25,12 +25,6 @@ efficiency-oriented base model (B0) to surpass models at every scale, while avoiding extensive grid-search of hyperparameters. -A summary of the latest updates on the model is available at -[here](https://github.com/tensorflow/tpu/tree/master/models/official/efficientnet), where various -augmentation schemes and semi-supervised learning approaches are applied to further -improve the imagenet performance of the models. These extensions of the model can be used -by updating weights without changing model architecture. - ## B0 to B7 variants of EfficientNet *(This section provides some details on "compound scaling", and can be skipped @@ -57,7 +51,7 @@ are hand-picked and proven to produce good results, though they may be significantly off from the compound scaling formula. Therefore, the keras implementation (detailed below) only provide these 8 models, B0 to B7, -instead of allowing arbitray choice of width / depth / resolution parameters. +instead of allowing arbitrary choice of width / depth / resolution parameters. ## Keras implementation of EfficientNet @@ -65,7 +59,7 @@ use EfficientNetB0 for classifying 1000 classes of images from ImageNet, run: ```python -from tensorflow.keras.applications import EfficientNetB0 +from keras.applications import EfficientNetB0 model = EfficientNetB0(weights='imagenet') ``` @@ -123,13 +117,13 @@ ## Setup and data loading """ -import os import tarfile import urllib.request from pathlib import Path import numpy as np import matplotlib.pyplot as plt from PIL import Image +import scipy.io import keras from keras import layers from keras.applications import EfficientNetB0 @@ -155,9 +149,6 @@ with protobuf version incompatibilities). """ -# Download and extract Stanford Dogs dataset -import scipy.io - dataset_url = "http://vision.stanford.edu/aditya86/ImageNetDogs/images.tar" lists_url = "http://vision.stanford.edu/aditya86/ImageNetDogs/lists.tar" data_dir = Path("./stanford_dogs_data") @@ -172,7 +163,7 @@ def download_and_extract(url, extract_to): urllib.request.urlretrieve(url, filepath) print(f"Extracting {filename}...") with tarfile.open(filepath, "r") as tar: - tar.extractall(extract_to) + tar.extractall(extract_to, filter="data") return extract_to diff --git a/examples/vision/ipynb/image_classification_efficientnet_fine_tuning.ipynb b/examples/vision/ipynb/image_classification_efficientnet_fine_tuning.ipynb index 11d7d30fc4..4a073567d8 100644 --- a/examples/vision/ipynb/image_classification_efficientnet_fine_tuning.ipynb +++ b/examples/vision/ipynb/image_classification_efficientnet_fine_tuning.ipynb @@ -36,12 +36,6 @@ "efficiency-oriented base model (B0) to surpass models at every scale, while avoiding\n", "extensive grid-search of hyperparameters.\n", "\n", - "A summary of the latest updates on the model is available at\n", - "[here](https://github.com/tensorflow/tpu/tree/master/models/official/efficientnet), where various\n", - "augmentation schemes and semi-supervised learning approaches are applied to further\n", - "improve the imagenet performance of the models. These extensions of the model can be used\n", - "by updating weights without changing model architecture.\n", - "\n", "## B0 to B7 variants of EfficientNet\n", "\n", "*(This section provides some details on \"compound scaling\", and can be skipped\n", @@ -68,7 +62,7 @@ "are hand-picked and proven to produce good results, though they may be significantly\n", "off from the compound scaling formula.\n", "Therefore, the keras implementation (detailed below) only provide these 8 models, B0 to B7,\n", - "instead of allowing arbitray choice of width / depth / resolution parameters.\n", + "instead of allowing arbitrary choice of width / depth / resolution parameters.\n", "\n", "## Keras implementation of EfficientNet\n", "\n", @@ -76,7 +70,7 @@ "use EfficientNetB0 for classifying 1000 classes of images from ImageNet, run:\n", "\n", "```python\n", - "from tensorflow.keras.applications import EfficientNetB0\n", + "from keras.applications import EfficientNetB0\n", "model = EfficientNetB0(weights='imagenet')\n", "```\n", "\n", @@ -146,13 +140,13 @@ }, "outputs": [], "source": [ - "import os\n", "import tarfile\n", "import urllib.request\n", "from pathlib import Path\n", "import numpy as np\n", "import matplotlib.pyplot as plt\n", "from PIL import Image\n", + "import scipy.io\n", "import keras\n", "from keras import layers\n", "from keras.applications import EfficientNetB0\n", @@ -192,9 +186,6 @@ }, "outputs": [], "source": [ - "# Download and extract Stanford Dogs dataset\n", - "import scipy.io\n", - "\n", "dataset_url = \"http://vision.stanford.edu/aditya86/ImageNetDogs/images.tar\"\n", "lists_url = \"http://vision.stanford.edu/aditya86/ImageNetDogs/lists.tar\"\n", "data_dir = Path(\"./stanford_dogs_data\")\n", @@ -209,7 +200,7 @@ " urllib.request.urlretrieve(url, filepath)\n", " print(f\"Extracting {filename}...\")\n", " with tarfile.open(filepath, \"r\") as tar:\n", - " tar.extractall(extract_to)\n", + " tar.extractall(extract_to, filter=\"data\")\n", " return extract_to\n", "\n", "\n", diff --git a/examples/vision/md/image_classification_efficientnet_fine_tuning.md b/examples/vision/md/image_classification_efficientnet_fine_tuning.md index 28dbc1e82a..ec8599c4c0 100644 --- a/examples/vision/md/image_classification_efficientnet_fine_tuning.md +++ b/examples/vision/md/image_classification_efficientnet_fine_tuning.md @@ -27,12 +27,6 @@ heuristics (compound-scaling, details see efficiency-oriented base model (B0) to surpass models at every scale, while avoiding extensive grid-search of hyperparameters. -A summary of the latest updates on the model is available at -[here](https://github.com/tensorflow/tpu/tree/master/models/official/efficientnet), where various -augmentation schemes and semi-supervised learning approaches are applied to further -improve the imagenet performance of the models. These extensions of the model can be used -by updating weights without changing model architecture. - --- ## B0 to B7 variants of EfficientNet @@ -60,7 +54,7 @@ As a result, the depth, width and resolution of each variant of the EfficientNet are hand-picked and proven to produce good results, though they may be significantly off from the compound scaling formula. Therefore, the keras implementation (detailed below) only provide these 8 models, B0 to B7, -instead of allowing arbitray choice of width / depth / resolution parameters. +instead of allowing arbitrary choice of width / depth / resolution parameters. --- ## Keras implementation of EfficientNet @@ -69,7 +63,7 @@ An implementation of EfficientNet B0 to B7 has been shipped with Keras since v2. use EfficientNetB0 for classifying 1000 classes of images from ImageNet, run: ```python -from tensorflow.keras.applications import EfficientNetB0 +from keras.applications import EfficientNetB0 model = EfficientNetB0(weights='imagenet') ``` @@ -127,13 +121,13 @@ As an end-to-end example, we will show using pre-trained EfficientNetB0 on ```python -import os import tarfile import urllib.request from pathlib import Path import numpy as np import matplotlib.pyplot as plt from PIL import Image +import scipy.io import keras from keras import layers from keras.applications import EfficientNetB0 @@ -160,9 +154,6 @@ with protobuf version incompatibilities). ```python -# Download and extract Stanford Dogs dataset -import scipy.io - dataset_url = "http://vision.stanford.edu/aditya86/ImageNetDogs/images.tar" lists_url = "http://vision.stanford.edu/aditya86/ImageNetDogs/lists.tar" data_dir = Path("./stanford_dogs_data") @@ -177,7 +168,7 @@ def download_and_extract(url, extract_to): urllib.request.urlretrieve(url, filepath) print(f"Extracting {filename}...") with tarfile.open(filepath, "r") as tar: - tar.extractall(extract_to) + tar.extractall(extract_to, filter="data") return extract_to