{"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":"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'))\nprint(\"REPLICAS:\", strategy.num_replicas_in_sync)\nAUTOTUNE = tf.data.AUTOTUNE","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-07-04T07:36:23.070343Z","iopub.execute_input":"2026-07-04T07:36:23.070568Z","iopub.status.idle":"2026-07-04T07:36:39.282803Z","shell.execute_reply.started":"2026-07-04T07:36:23.070546Z","shell.execute_reply":"2026-07-04T07:36:39.282075Z"}},"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-04T07:42:29.220105Z","iopub.execute_input":"2026-07-04T07:42:29.220939Z","iopub.status.idle":"2026-07-04T07:42:29.68958Z","shell.execute_reply.started":"2026-07-04T07:42:29.220906Z","shell.execute_reply":"2026-07-04T07:42:29.688958Z"}},"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) for train and val\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) for test\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 tfrec 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-04T07:42:32.467011Z","iopub.execute_input":"2026-07-04T07:42:32.467274Z","iopub.status.idle":"2026-07-04T07:42:32.474811Z","shell.execute_reply.started":"2026-07-04T07:42:32.467253Z","shell.execute_reply":"2026-07-04T07:42:32.473934Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Build a TFRecord dataset, optionally deterministic for id matching\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-04T07:42:34.829697Z","iopub.execute_input":"2026-07-04T07:42:34.830074Z","iopub.status.idle":"2026-07-04T07:42:34.837507Z","shell.execute_reply.started":"2026-07-04T07:42:34.830038Z","shell.execute_reply":"2026-07-04T07:42:34.836686Z"}},"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.DenseNet201(\n        input_shape=[*IMAGE_SIZE, 3], include_top=False, weights='imagenet')\n    base.trainable = True\n\n    inputs = tf.keras.Input([*IMAGE_SIZE, 3])\n    x = augment(inputs)\n    x = base(x)\n    x = tf.keras.layers.GlobalAveragePooling2D()(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-04T07:42:40.535047Z","iopub.execute_input":"2026-07-04T07:42:40.535857Z","iopub.status.idle":"2026-07-04T07:42:43.599862Z","shell.execute_reply.started":"2026-07-04T07:42:40.535829Z","shell.execute_reply":"2026-07-04T07:42:43.599059Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Warm up for 3 epochs then decay the learning rate exponentially\ndef lrfn(epoch):\n    start, mx, mn, warm = 1e-5, 3e-4, 1e-6, 3\n    if epoch < warm:\n        return start + (mx - start) * epoch / warm\n    return mn + (mx - mn) * (0.85 ** (epoch - warm))\n\nlr_cb = tf.keras.callbacks.LearningRateScheduler(lrfn, verbose=1)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-04T07:42:54.560255Z","iopub.execute_input":"2026-07-04T07:42:54.561064Z","iopub.status.idle":"2026-07-04T07:42:54.565492Z","shell.execute_reply.started":"2026-07-04T07:42:54.561034Z","shell.execute_reply":"2026-07-04T07:42:54.564678Z"}},"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])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-04T07:42:56.187134Z","iopub.execute_input":"2026-07-04T07:42:56.187553Z","iopub.status.idle":"2026-07-04T12:06:54.940533Z","shell.execute_reply.started":"2026-07-04T07:42:56.187525Z","shell.execute_reply":"2026-07-04T12:06:54.939922Z"}},"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-04T12:09:17.080894Z","iopub.execute_input":"2026-07-04T12:09:17.081721Z","iopub.status.idle":"2026-07-04T12:10:41.928374Z","shell.execute_reply.started":"2026-07-04T12:09:17.081649Z","shell.execute_reply":"2026-07-04T12:10:41.927725Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test_ds = get_test_dataset()\ntest_prob = model.predict(test_ds.map(lambda image, idnum: image))\ntest_pred = np.argmax(test_prob, axis=1)\ntest_ids = np.concatenate([idnum.numpy() for _, idnum in test_ds]).astype('U')\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-04T16:57:32.176262Z","iopub.execute_input":"2026-07-04T16:57:32.176491Z","iopub.status.idle":"2026-07-04T16:57:32.190756Z","shell.execute_reply.started":"2026-07-04T16:57:32.176468Z","shell.execute_reply":"2026-07-04T16:57:32.189798Z"}},"outputs":[],"execution_count":null}]}