Frozen-weight transfer learning is a training choice: you take a net pre-trained on ImageNet, you either lock those weights and train only a new head, or you let the whole net move on your medical set. This page is one 2023 PYCAD Team run of that choice on Kvasir endoscopy stills. It is not a definition of machine learning for imaging. It is not an iNeuron course. It is not a PYCAD product.
If you meant what imaging ML actually returns (mark / mask / score) → machine learning for medical imaging. If you meant CNN vs ViT on X-ray PNGs → CNNs or ViT for medical imaging. If you meant what a CNN is → convolutional neural networks explained. If you meant the radiology product job → AI in radiology.
The code is public: amine0110/medical-imaging-classification. The slug still says iNeuron because that is the URL; the dead course marketing is not the job.
The question
ImageNet weights already know edges and textures. Medical images are not ImageNet. Two common recipes:
- Freeze the backbone.
layer.trainable = Falseon the imported net. Train only GlobalAveragePooling + a softmax head. Cheap. The features stay ImageNet features. - Do not freeze. Same starting weights, every layer can move. You are fine-tuning, not just fitting a linear probe. More GPU, more chance to overfit a small set.
The default blog advice is “freeze, then maybe unfreeze later.” This run asked whether freezing was actually better on this set. It was not.
The set and the loaders
Kvasir is a public GI-endoscopy set (Kaggle mirrors exist). Classes are anatomical landmarks and findings, not a chest X-ray. Still RGB JPEGs, not DICOM. That matters for how far you should generalise the table below.

image_dataset_from_directory does the 80/20 split. Input size follows the backbone (224 for VGG16 / DenseNet121, 299 if you switch to Xception). Batch 16 train, 8 val.
def return_ds(input_dir):
train_ds = tf.keras.preprocessing.image_dataset_from_directory(
input_dir,
validation_split=0.2,
subset="training",
seed=42,
image_size=(cfg.input_dim, cfg.input_dim),
batch_size=16,
label_mode="categorical",
)
val_ds = tf.keras.preprocessing.image_dataset_from_directory(
input_dir,
validation_split=0.2,
subset="validation",
seed=42,
image_size=(cfg.input_dim, cfg.input_dim),
batch_size=8,
label_mode="categorical",
)
return train_ds, val_ds
The model
Four Keras applications, ImageNet weights, top off, a GAP + softmax head. freeze=True locks the backbone. Config picks the name and the learning rate (this run used SGD at 0.01).
def return_model(input_dim, nb_classes, freeze=False, head=None):
heads = {
"xception": Xception,
"vgg16": VGG16,
"inceptionv3": InceptionV3,
"densenet121": DenseNet121,
}
if head is None or head.lower() not in heads:
raise ValueError("choose head: xception, vgg16, inceptionv3, densenet121")
ctor = heads[head.lower()]
base_model = ctor(
include_top=False,
weights="imagenet",
input_shape=(input_dim, input_dim, 3),
)
if freeze:
for layer in base_model.layers:
layer.trainable = False
x = GlobalAveragePooling2D()(base_model.output)
predictions = Dense(nb_classes, activation="softmax")(x)
return Model(inputs=base_model.inputs, outputs=predictions)
The live 2023 snippet used if head == 'inceptionv3' or 'Inceptionv3', which is always true in Python. That is a real bug; the dict above is the same four models without it. The repo is the source of truth if the two diverge.
model.compile(
loss="categorical_crossentropy",
optimizer=tf.keras.optimizers.SGD(learning_rate=cfg.lr),
metrics=["accuracy"],
)
save_weights = ModelCheckpoint(
filepath="models/my_model.h5",
monitor="val_accuracy",
save_best_only=True,
mode="max",
)
That run
Same recipe, frozen vs not, VGG16 and Xception. Numbers from the 2023 table (one run, this set, this LR):
| Frozen backbone | Not frozen | |||
|---|---|---|---|---|
| Model | VGG16 | Xception | VGG16 | Xception |
| Loss | 222.78 | 190.12 | 0.43 | 0.43 |
| Accuracy | 0.55 | 0.56 | 0.87 | 0.88 |

A loss of ~200 on a softmax classifier is a model that did not fit. Frozen ImageNet features plus SGD at 0.01 on this endoscopy set were not enough. Letting the backbone move dropped the loss three orders of magnitude and gained ~30 accuracy points. That is this run. It is not “never freeze.”
Why it is not a law: RGB endoscopy, not DICOM; ImageNet pretrain; one LR; no site hold-out; no class-balanced report. A chest X-ray net, a 3D U-Net, or a two-stage freeze-then-unfreeze schedule can look different. The 2023 note already said so. If the frozen head is bad, try a lower LR on the backbone, or unfreeze from the top blocks down, before you declare transfer learning dead.
Inference
One image, or a directory via model.evaluate. Classes come from the annotation list in the repo.
def predict_one_image(img_path, model_path):
classes = return_classes(cfg.classes_path)
model = load_model(model_path)
img = image.load_img(img_path, target_size=(cfg.input_dim, cfg.input_dim))
img_array = np.expand_dims(image.img_to_array(img), axis=0)
prediction = model.predict(img_array)
idx = int(np.argmax(prediction))
return classes[idx], float(np.max(prediction))
What this page is not
- Not 673. The imaging-ML method layer already exists.
- Not 308. CNN vs ViT is a different 2023 run, different repo.
- Not an iNeuron / PYCAD course. The slug is leftover. Empty Gutenberg and diary first-person are gone. The Keras snippets stayed.
If the classifier has to land next to a DICOM study in a clinic app, that is the imaging piece. Case studies.