{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[],"dockerImageVersionId":28755,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Initial setup","metadata":{}},{"cell_type":"code","source":"import numpy as np\nimport pandas as pd\nimport tensorflow as tf\nfrom tensorflow.keras.preprocessing.image import ImageDataGenerator\nfrom tensorflow.keras import layers, models\nfrom tensorflow.keras.models import load_model\n\nSPEC = '/kaggle/input/datasets/kelbycraft/kwc-freesound-2018-melspec-128-magma/spectrograms'\nTRAIN_DIR = f'{SPEC}/train'\nTEST_DIR  = f'{SPEC}/test'          # contains one subfolder 'all'\nNUM_CLASSES = 41\nBATCH = 64\nSEED = 42\n\nprint('GPU:', tf.config.list_physical_devices('GPU'))\n\n# One place to collect every experiment's score.\nRESULTS = {}   # name -> {'val_map3': float, 'img': (H,W), 'path': saved_model_path}\n\ndef mapk_from_probs(probs, true_idx, k=3):\n    top_k = np.argsort(-probs, axis=1)[:, :k]\n    score = 0.0\n    for i, true in enumerate(true_idx):\n        hits = np.where(top_k[i] == true)[0]\n        if len(hits):\n            score += 1.0 / (hits[0] + 1)\n    return score / len(true_idx)","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Reusable harness","metadata":{}},{"cell_type":"code","source":"def make_generators(preprocess_fn, img_size):\n    gen = ImageDataGenerator(preprocessing_function=preprocess_fn, validation_split=0.2)\n    tr = gen.flow_from_directory(TRAIN_DIR, target_size=img_size, batch_size=BATCH,\n        class_mode='categorical', subset='training', shuffle=True, seed=SEED)\n    va = gen.flow_from_directory(TRAIN_DIR, target_size=img_size, batch_size=BATCH,\n        class_mode='categorical', subset='validation', shuffle=False, seed=SEED)\n    return tr, va\n\ndef callbacks(patience=6):\n    return [tf.keras.callbacks.EarlyStopping(patience=patience, restore_best_weights=True),\n            tf.keras.callbacks.ReduceLROnPlateau(patience=2, factor=0.5)]\n\ndef log_and_save(model, name, va, img_size):\n    score = mapk_from_probs(model.predict(va, verbose=0), va.classes)\n    path = f'/kaggle/working/{name}.keras'\n    model.save(path)\n    RESULTS[name] = {'val_map3': round(float(score), 4), 'img': img_size, 'path': path}\n    print(f'>>> {name}  val MAP@3 = {score:.4f}  (saved: {path})')\n    return score\n\n# ---- from-scratch CNN ----\ndef build_cnn(img_size):\n    m = models.Sequential([\n        layers.Input((img_size[0], img_size[1], 3)),\n        layers.Conv2D(32,3,activation='relu',padding='same'), layers.BatchNormalization(), layers.MaxPooling2D(),\n        layers.Conv2D(64,3,activation='relu',padding='same'), layers.BatchNormalization(), layers.MaxPooling2D(),\n        layers.Conv2D(128,3,activation='relu',padding='same'), layers.BatchNormalization(), layers.MaxPooling2D(),\n        layers.GlobalAveragePooling2D(),\n        layers.Dropout(0.3), layers.Dense(256, activation='relu'),\n        layers.Dropout(0.3), layers.Dense(NUM_CLASSES, activation='softmax'),\n    ])\n    m.compile('adam', 'categorical_crossentropy', metrics=['accuracy'])\n    return m\n\ndef run_cnn(name, img_size, epochs=25):\n    tr, va = make_generators(lambda x: x/255.0, img_size)\n    m = build_cnn(img_size)\n    print(f'\\n=== {name} (CNN {img_size}) ===')\n    m.fit(tr, validation_data=va, epochs=epochs, callbacks=callbacks())\n    log_and_save(m, name, va, img_size)\n    return m\n\n# ---- transfer-learning backbone (stage-1 frozen; stage 2 optional) ----\ndef run_backbone(name, backbone_fn, preprocess_fn, img_size, epochs=15, fine_tune=False):\n    tr, va = make_generators(preprocess_fn, img_size)\n    base = backbone_fn(include_top=False, weights='imagenet',\n                       input_shape=(img_size[0], img_size[1], 3), pooling='avg')\n    base.trainable = False\n    m = models.Sequential([base,\n        layers.Dropout(0.3), layers.Dense(256, activation='relu'),\n        layers.Dropout(0.3), layers.Dense(NUM_CLASSES, activation='softmax')])\n    m.compile('adam', 'categorical_crossentropy', metrics=['accuracy'])\n    print(f'\\n=== {name} ({backbone_fn.__name__} {img_size}) STAGE 1 ===')\n    m.fit(tr, validation_data=va, epochs=epochs, callbacks=callbacks())\n\n    if fine_tune:\n        # NOTE: keep BatchNorm layers frozen — unfreezing them destabilized training.\n        print(f'=== {name} STAGE 2 (fine-tune, BN frozen) ===')\n        base.trainable = True\n        for layer in base.layers:\n            if isinstance(layer, layers.BatchNormalization):\n                layer.trainable = False\n        m.compile(tf.keras.optimizers.Adam(1e-5), 'categorical_crossentropy', metrics=['accuracy'])\n        m.fit(tr, validation_data=va, epochs=10, callbacks=callbacks())\n\n    log_and_save(m, name, va, img_size)\n    return m","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Submission template","metadata":{}},{"cell_type":"code","source":"def make_submission(model, preprocess_fn, img_size, out_name='submission.csv'):\n    cls = ImageDataGenerator(validation_split=0.2).flow_from_directory(\n        TRAIN_DIR, target_size=img_size, batch_size=BATCH,\n        class_mode='categorical', subset='training', shuffle=True, seed=SEED).class_indices\n    idx_to_label = {v: k for k, v in cls.items()}\n    tf_ = ImageDataGenerator(preprocessing_function=preprocess_fn).flow_from_directory(\n        TEST_DIR, target_size=img_size, batch_size=BATCH, class_mode=None, shuffle=False)\n    probs = model.predict(tf_, verbose=1)\n    top3 = np.argsort(-probs, axis=1)[:, :3]\n    labels = [' '.join(idx_to_label[i] for i in row) for row in top3]\n    fnames = [f.split('/')[-1].replace('.png', '.wav') for f in tf_.filenames]\n    sub = pd.DataFrame({'fname': fnames, 'label': labels})\n    sub.to_csv(f'/kaggle/working/{out_name}', index=False)\n    print(f'wrote {out_name} | rows {len(sub)}'); print(sub.head())\n    return sub","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Achitectures","metadata":{}},{"cell_type":"code","source":"from tensorflow.keras.applications import MobileNetV2, EfficientNetB0\nfrom tensorflow.keras.applications.mobilenet_v2 import preprocess_input as mnv2_pre\nfrom tensorflow.keras.applications.efficientnet import preprocess_input as eff_pre\n\n# --- 100x150 ---\nrun_cnn('cnn_100x150', (100,150))\nrun_backbone('mnv2_100x150', MobileNetV2,    mnv2_pre, (100,150))\nrun_backbone('eff_100x150',  EfficientNetB0, eff_pre,  (100,150))","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- 160x240 ---\nrun_cnn('cnn_160x240', (160,240))\nrun_backbone('mnv2_160x240', MobileNetV2,    mnv2_pre, (160,240))\nrun_backbone('eff_160x240',  EfficientNetB0, eff_pre,  (160,240))","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Comparison Table","metadata":{}},{"cell_type":"code","source":"tbl = pd.DataFrame(RESULTS).T.sort_values('val_map3', ascending=False)\nprint(tbl[['img','val_map3','path']])","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Submission","metadata":{}},{"cell_type":"code","source":"# Example: submit the 160x240 baseline CNN.\nbest = load_model(RESULTS['cnn_160x240']['path'])\nmake_submission(best, lambda x: x/255.0, (160,240))   # writes submission.csv","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}