{"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":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session\n\n# Use the kagglehub client library to attach Kaggle resources like competitions, datasets, and models to your session\n# Learn more about kagglehub: https://github.com/Kaggle/kagglehub/blob/main/README.md\n\nimport kagglehub\n# kagglehub.dataset_download('<owner>/<dataset-slug>')","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-07-23T08:31:16.061340Z","iopub.execute_input":"2026-07-23T08:31:16.061971Z","iopub.status.idle":"2026-07-23T08:31:16.094177Z","shell.execute_reply.started":"2026-07-23T08:31:16.061940Z","shell.execute_reply":"2026-07-23T08:31:16.093379Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import warnings, os\nwarnings.filterwarnings(\"ignore\")\nos.environ[\"TF_CPP_MIN_LOG_LEVEL\"] = \"3\"\n\nimport math, re\nimport numpy as np\nimport pandas as pd\nimport tensorflow as tf\nimport matplotlib.pyplot as plt\nfrom kaggle_datasets import KaggleDatasets\nfrom sklearn.metrics import f1_score\n\nprint(\"TF version:\", tf.__version__)\nstrategy = tf.distribute.get_strategy()\nprint(\"GPU:\", tf.config.list_physical_devices('GPU'))\nAUTOTUNE = tf.data.AUTOTUNE","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-23T08:31:16.095487Z","iopub.execute_input":"2026-07-23T08:31:16.096082Z","iopub.status.idle":"2026-07-23T08:31:16.102157Z","shell.execute_reply.started":"2026-07-23T08:31:16.096060Z","shell.execute_reply":"2026-07-23T08:31:16.101398Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"GCS_DS_PATH = KaggleDatasets().get_gcs_path()\n\nIMAGE_SIZE = [331, 331]\nNUM_CLASSES = 104\nBATCH_SIZE = 16\nGCS_PATH = GCS_DS_PATH + '/tfrecords-jpeg-331x331'\n\nTRAIN_FILES = tf.io.gfile.glob(GCS_PATH + '/train/*.tfrec')\nVAL_FILES   = tf.io.gfile.glob(GCS_PATH + '/val/*.tfrec')\nTEST_FILES  = tf.io.gfile.glob(GCS_PATH + '/test/*.tfrec')\nprint(\"files:\", len(TRAIN_FILES), len(VAL_FILES), len(TEST_FILES))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-23T08:31:16.103345Z","iopub.execute_input":"2026-07-23T08:31:16.104057Z","iopub.status.idle":"2026-07-23T08:31:16.761515Z","shell.execute_reply.started":"2026-07-23T08:31:16.104024Z","shell.execute_reply":"2026-07-23T08:31:16.760676Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Decode a JPEG string into a normalized float image\ndef decode_image(image_data):\n    image = tf.image.decode_jpeg(image_data, channels=3)\n    image = tf.cast(image, tf.float32) / 255.0\n    image = tf.reshape(image, [*IMAGE_SIZE, 3])\n    return image\n\n# Parse a labeled example (image + class)\ndef read_labeled(example):\n    fmt = {\"image\": tf.io.FixedLenFeature([], tf.string),\n           \"class\": tf.io.FixedLenFeature([], tf.int64)}\n    ex = tf.io.parse_single_example(example, fmt)\n    return decode_image(ex[\"image\"]), tf.cast(ex[\"class\"], tf.int32)\n\n# Parse an unlabeled example (image + id)\ndef read_unlabeled(example):\n    fmt = {\"image\": tf.io.FixedLenFeature([], tf.string),\n           \"id\": tf.io.FixedLenFeature([], tf.string)}\n    ex = tf.io.parse_single_example(example, fmt)\n    return decode_image(ex[\"image\"]), ex[\"id\"]\n\n# Count images from the number encoded in each filename\ndef count_items(filenames):\n    n = [int(re.compile(r\"-([0-9]*)\\.\").search(f).group(1)) for f in filenames]\n    return int(np.sum(n))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-23T08:31:16.762646Z","iopub.execute_input":"2026-07-23T08:31:16.763170Z","iopub.status.idle":"2026-07-23T08:31:16.769527Z","shell.execute_reply.started":"2026-07-23T08:31:16.763147Z","shell.execute_reply":"2026-07-23T08:31:16.768795Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Build a TFRecord dataset, optionally deterministic\ndef load_dataset(filenames, labeled=True, ordered=False):\n    ds = tf.data.TFRecordDataset(filenames, num_parallel_reads=AUTOTUNE)\n    if ordered:\n        opt = tf.data.Options(); opt.deterministic = True\n        ds = ds.with_options(opt)\n    ds = ds.map(read_labeled if labeled else read_unlabeled, num_parallel_calls=AUTOTUNE)\n    return ds\n\n# Training set: shuffled, repeated, batched\ndef get_training_dataset():\n    ds = load_dataset(TRAIN_FILES, labeled=True)\n    return ds.repeat().shuffle(2048).batch(BATCH_SIZE).prefetch(AUTOTUNE)\n\n# Validation set: ordered, batched\ndef get_validation_dataset():\n    return load_dataset(VAL_FILES, labeled=True, ordered=True).batch(BATCH_SIZE).prefetch(AUTOTUNE)\n\n# Test set: ordered, batched, no labels\ndef get_test_dataset():\n    return load_dataset(TEST_FILES, labeled=False, ordered=True).batch(BATCH_SIZE).prefetch(AUTOTUNE)\n\nNUM_TRAIN, NUM_VAL, NUM_TEST = count_items(TRAIN_FILES), count_items(VAL_FILES), count_items(TEST_FILES)\nSTEPS_PER_EPOCH = NUM_TRAIN // BATCH_SIZE\nprint(\"train/val/test:\", NUM_TRAIN, NUM_VAL, NUM_TEST, \"| steps/epoch:\", STEPS_PER_EPOCH)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-23T08:31:16.771170Z","iopub.execute_input":"2026-07-23T08:31:16.772750Z","iopub.status.idle":"2026-07-23T08:31:16.783674Z","shell.execute_reply.started":"2026-07-23T08:31:16.772728Z","shell.execute_reply":"2026-07-23T08:31:16.782979Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"with strategy.scope():\n    augment = tf.keras.Sequential([\n        tf.keras.layers.RandomFlip(\"horizontal_and_vertical\"),\n        tf.keras.layers.RandomRotation(0.15),\n        tf.keras.layers.RandomZoom(0.2),\n        tf.keras.layers.RandomTranslation(0.1, 0.1)\n    ], name=\"augment\")\n\n    base = tf.keras.applications.EfficientNetB5(\n        input_shape=[*IMAGE_SIZE, 3], include_top=False, weights='imagenet')\n    base.trainable = True\n\n    # EfficientNet expects 0-255 input, so scale back up before the base\n    inputs = tf.keras.Input([*IMAGE_SIZE, 3])\n    x = augment(inputs)\n    x = tf.keras.layers.Rescaling(255.0)(x)\n    x = base(x)\n    x = tf.keras.layers.GlobalAveragePooling2D()(x)\n    x = tf.keras.layers.Dropout(0.3)(x)\n    outputs = tf.keras.layers.Dense(NUM_CLASSES, activation='softmax')(x)\n    model = tf.keras.Model(inputs, outputs)\n\n    model.compile(optimizer=tf.keras.optimizers.Adam(1e-4),\n                  loss='sparse_categorical_crossentropy', metrics=['accuracy'])\nprint(\"params:\", model.count_params())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-23T08:31:16.784556Z","iopub.execute_input":"2026-07-23T08:31:16.784726Z","iopub.status.idle":"2026-07-23T08:31:19.361713Z","shell.execute_reply.started":"2026-07-23T08:31:16.784710Z","shell.execute_reply":"2026-07-23T08:31:19.360812Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Warmup 3 epochs then exponential decay\ndef lrfn(epoch):\n    start, mx, mn, warm = 1e-5, 2e-4, 1e-6, 3\n    if epoch < warm:\n        return start + (mx - start) * epoch / warm\n    return mn + (mx - mn) * (0.8 ** (epoch - warm))\n\nlr_cb = tf.keras.callbacks.LearningRateScheduler(lrfn, verbose=1)\n\n# Keep the best weights by validation accuracy\nckpt_cb = tf.keras.callbacks.ModelCheckpoint(\n    \"best.weights.h5\", monitor=\"val_accuracy\",\n    save_best_only=True, save_weights_only=True, verbose=1)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-23T08:31:19.362748Z","iopub.execute_input":"2026-07-23T08:31:19.363498Z","iopub.status.idle":"2026-07-23T08:31:19.368683Z","shell.execute_reply.started":"2026-07-23T08:31:19.363466Z","shell.execute_reply":"2026-07-23T08:31:19.367995Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"EPOCHS = 12\nhistory = model.fit(\n    get_training_dataset(),\n    steps_per_epoch=STEPS_PER_EPOCH,\n    epochs=EPOCHS,\n    validation_data=get_validation_dataset(),\n    callbacks=[lr_cb, ckpt_cb])\n\nmodel.load_weights(\"best.weights.h5\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-23T08:31:19.369577Z","iopub.execute_input":"2026-07-23T08:31:19.369876Z","iopub.status.idle":"2026-07-23T11:09:42.978925Z","shell.execute_reply.started":"2026-07-23T08:31:19.369842Z","shell.execute_reply":"2026-07-23T11:09:42.978245Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"val_ds = get_validation_dataset()\nval_true = np.concatenate([lbl.numpy() for _, lbl in val_ds])\nval_prob = model.predict(val_ds.map(lambda image, label: image))\nval_pred = np.argmax(val_prob, axis=1)\nprint(\"macro F1:\", round(f1_score(val_true, val_pred, average='macro'), 4))\nprint(\"accuracy:\", round((val_pred == val_true).mean(), 4))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-23T11:09:42.980231Z","iopub.execute_input":"2026-07-23T11:09:42.980520Z","iopub.status.idle":"2026-07-23T11:10:32.735674Z","shell.execute_reply.started":"2026-07-23T11:09:42.980497Z","shell.execute_reply":"2026-07-23T11:10:32.734939Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def tta_predict(dataset, n=4):\n    probs = None\n    for i in range(n):\n        def aug(img):\n            if i == 1: img = tf.image.flip_left_right(img)\n            elif i == 2: img = tf.image.flip_up_down(img)\n            elif i == 3: img = tf.image.flip_left_right(tf.image.flip_up_down(img))\n            return img\n        ds = dataset.map(lambda image, idnum: (tf.map_fn(aug, image), idnum))\n        p = model.predict(ds.map(lambda image, idnum: image))\n        probs = p if probs is None else probs + p\n    return probs / n\n\ntest_ds = get_test_dataset()\ntest_prob = tta_predict(test_ds, n=4)\ntest_pred = np.argmax(test_prob, axis=1)\ntest_ids = np.concatenate([idnum.numpy() for _, idnum in test_ds]).astype('U')\n\nsub = pd.DataFrame({'id': test_ids, 'label': test_pred})\nsub.to_csv('submission.csv', index=False)\nprint(sub.head())\nprint(\"rows:\", len(sub), \"expected:\", NUM_TEST)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-23T11:10:32.736597Z","iopub.execute_input":"2026-07-23T11:10:32.736892Z","iopub.status.idle":"2026-07-23T11:16:26.149620Z","shell.execute_reply.started":"2026-07-23T11:10:32.736869Z","shell.execute_reply":"2026-07-23T11:16:26.148681Z"}},"outputs":[],"execution_count":null}]}