{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":21154,"databundleVersionId":1243559,"sourceType":"competition"},{"sourceId":948457,"sourceType":"datasetVersion","datasetId":514569},{"sourceId":13484865,"sourceType":"datasetVersion","datasetId":8561325},{"sourceId":13492898,"sourceType":"datasetVersion","datasetId":8566811},{"sourceId":13494284,"sourceType":"datasetVersion","datasetId":8567655}],"dockerImageVersionId":31153,"isInternetEnabled":true,"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","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================================================\n# セル1：モデル定義クラス群（任意追加OK）\n# =========================================================\n\n# 以下余計な出力を抑える設定\nimport os, time\nos.environ[\"TF_CPP_MIN_LOG_LEVEL\"] = \"2\"  # 0:all, 1:INFO非表示, 2:WARNING非表示, 3:ERROR以外非表示\nos.environ[\"TF_CPP_MAX_VLOG_LEVEL\"] = \"0\"\n\nimport warnings\nwarnings.filterwarnings(\"ignore\")\n\nimport tensorflow as tf\ntry:\n    from absl import logging as absl_logging\n    absl_logging.set_verbosity(absl_logging.ERROR)\nexcept Exception:\n    pass\n\nfrom tensorflow.keras.applications.resnet50 import ResNet50, preprocess_input as _resnet_preprocess\nfrom tensorflow.keras.applications.efficientnet import EfficientNetB0, preprocess_input as _effnet_preprocess\n\n# ---------------------------------------------------------\n# 逆残差ブロック（Inverted Residual）\n# ---------------------------------------------------------\nclass InvertedResidual(tf.keras.layers.Layer):\n    def __init__(self, in_ch: int, out_ch: int, stride: int, expand_ratio: float, name=None):\n        super().__init__(name=name)\n        self.stride = stride\n        self.use_res = (stride == 1 and in_ch == out_ch)\n        hidden = int(round(in_ch * expand_ratio))\n\n        layers = []\n        if expand_ratio != 1:\n            layers += [\n                tf.keras.layers.Conv2D(hidden, kernel_size=1, padding=\"same\", use_bias=False),\n                tf.keras.layers.BatchNormalization(),\n                tf.keras.layers.ReLU(max_value=6.0),\n            ]\n        layers += [\n            tf.keras.layers.DepthwiseConv2D(kernel_size=3, strides=stride, padding=\"same\", use_bias=False),\n            tf.keras.layers.BatchNormalization(),\n            tf.keras.layers.ReLU(max_value=6.0),\n        ]\n        layers += [\n            tf.keras.layers.Conv2D(out_ch, kernel_size=1, padding=\"same\", use_bias=False),\n            tf.keras.layers.BatchNormalization(),\n        ]\n        self.block = tf.keras.Sequential(layers)\n\n    def call(self, x, training=False):\n        out = self.block(x, training=training)\n        if self.use_res:\n            return tf.keras.layers.Add()([x, out])\n        return out\n\n# ---------------------------------------------------------\n# MobileNetV3 Lite\n# ---------------------------------------------------------\nclass MobileNetV3Lite(tf.keras.Model):\n    def __init__(self, image_size=(224, 224), num_classes=104, in_channels=3):\n        super().__init__(name=\"MobileNetV3Lite\")\n        H, W = image_size\n        C = in_channels\n\n        self.stem = tf.keras.Sequential([\n            tf.keras.layers.Conv2D(16, kernel_size=3, strides=2, padding=\"same\", use_bias=False,\n                                   input_shape=(H, W, C)),\n            tf.keras.layers.BatchNormalization(),\n            tf.keras.layers.ReLU(max_value=6.0),\n        ])\n\n        self.blocks = tf.keras.Sequential([\n            InvertedResidual(16,  24, stride=2, expand_ratio=4),\n            InvertedResidual(24,  24, stride=1, expand_ratio=3),\n            InvertedResidual(24,  40, stride=2, expand_ratio=3),\n            InvertedResidual(40,  40, stride=1, expand_ratio=3),\n            InvertedResidual(40,  80, stride=2, expand_ratio=6),\n            InvertedResidual(80,  80, stride=1, expand_ratio=2.5),\n            InvertedResidual(80, 112, stride=2, expand_ratio=6),\n            InvertedResidual(112,112, stride=1, expand_ratio=6),\n            InvertedResidual(112,160, stride=2, expand_ratio=6),\n            InvertedResidual(160,160, stride=1, expand_ratio=6),\n        ])\n\n        self.head = tf.keras.Sequential([\n            tf.keras.layers.Conv2D(960, kernel_size=1, use_bias=False),\n            tf.keras.layers.BatchNormalization(),\n            tf.keras.layers.ReLU(max_value=6.0),\n            tf.keras.layers.Conv2D(1280, kernel_size=1, use_bias=False),\n            tf.keras.layers.BatchNormalization(),\n            tf.keras.layers.ReLU(max_value=6.0),\n            tf.keras.layers.GlobalAveragePooling2D(),\n            tf.keras.layers.Dense(num_classes, activation=\"softmax\"),\n        ])\n\n    def call(self, x, training=False):\n        x = self.stem(x, training=training)\n        x = self.blocks(x, training=training)\n        x = self.head(x, training=training)\n        return x\n\n# ---------------------------------------------------------\n# Helper: Keras applications wrapper\n# ---------------------------------------------------------\ndef _build_keras_backbone(backbone_cls, preprocess_fn, image_size, num_classes,\n                          in_channels=3, pretrained=False, freeze_backbone=True):\n    H, W = image_size\n    inputs = tf.keras.Input(shape=(H, W, in_channels), name=\"input_image\")\n    x = inputs * 255.0\n    x = tf.keras.layers.Lambda(lambda z: preprocess_fn(z), name=f\"{backbone_cls.__name__}_preproc\")(x)\n\n    base = backbone_cls(include_top=False,\n                        weights=\"imagenet\" if pretrained else None,\n                        input_tensor=x,\n                        pooling=\"avg\")\n    base.trainable = not freeze_backbone\n    outputs = tf.keras.layers.Dense(num_classes, activation=\"softmax\", name=\"predictions\")(base.output)\n    model = tf.keras.Model(inputs=inputs, outputs=outputs, name=f\"{backbone_cls.__name__}_head\")\n    return model\n\n# ---------------------------------------------------------\n# MODEL_REGISTRY\n# ---------------------------------------------------------\nMODEL_REGISTRY = {\n    \"mobilenet_v3_lite\": MobileNetV3Lite,\n    \"resnet50\": lambda image_size, num_classes, in_channels=3, pretrained=False, freeze_backbone=True:\n        _build_keras_backbone(ResNet50, _resnet_preprocess, image_size, num_classes,\n                              in_channels=in_channels, pretrained=pretrained, freeze_backbone=freeze_backbone),\n    \"efficientnetb0\": lambda image_size, num_classes, in_channels=3, pretrained=False, freeze_backbone=True:\n        _build_keras_backbone(EfficientNetB0, _effnet_preprocess, image_size, num_classes,\n                              in_channels=in_channels, pretrained=pretrained, freeze_backbone=freeze_backbone),\n}\n\ndef get_model_by_name(name: str, image_size=(224,224), num_classes=104, in_channels=3,\n                      pretrained=False, freeze_backbone=True) -> tf.keras.Model:\n    key = name.lower()\n    if key not in MODEL_REGISTRY:\n        raise ValueError(f\"未知のモデル名です: {name}. 登録済み: {list(MODEL_REGISTRY.keys())}\")\n\n    constructor = MODEL_REGISTRY[key]\n    try:\n        model = constructor(image_size=image_size, num_classes=num_classes,\n                            in_channels=in_channels, pretrained=pretrained, freeze_backbone=freeze_backbone)\n    except TypeError:\n        if isinstance(constructor, type):\n            model = constructor(image_size=image_size, num_classes=num_classes, in_channels=in_channels)\n        else:\n            model = constructor(image_size, num_classes, in_channels)\n    return model\n\n# =========================================================\n# セル2：学習・推論・提出（TTAなし）\n# =========================================================\n\n# 以下余計な出力を抑える設定（セル2でも同様に安全策を入れておく）\nos.environ[\"TF_CPP_MIN_LOG_LEVEL\"] = \"2\"\nos.environ[\"TF_CPP_MAX_VLOG_LEVEL\"] = \"0\"\n\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nfrom dataclasses import dataclass\nfrom typing import Tuple, List\n\n# 乱数シード\ndef set_seed(seed: int = 42):\n    np.random.seed(seed)\n    tf.random.set_seed(seed)\n\n@dataclass\nclass HParams:\n    IMAGE_SIZE = 224\n    base_path: str = \"/kaggle/input/tpu-getting-started\"\n    folder: str = f\"tfrecords-jpeg-{IMAGE_SIZE}x{IMAGE_SIZE}\"\n    image_size: Tuple[int, int] = (IMAGE_SIZE, IMAGE_SIZE)\n    in_channels: int = 3\n    num_classes: int = 104\n    batch_size: int = 64\n    epochs: int = 50\n    seed: int = 42\n    shuffle_buffer: int = 2000\n    optimizer: str = \"adamw\"\n    lr: float = 0.0003\n    weight_decay: float = 0.0001\n    momentum: float = 0.9\n    jit_compile: bool = False\n    model_path: str = \"/kaggle/working/model_baseline.h5\"\n    submission_path: str = \"submission.csv\"\n    prob_flip: float = 0.5\n    prob_zoom: float = 0.5\n    zoom_scale_min: float = 1.1\n    zoom_scale_max: float = 1.3\n    brightness_delta: float = 0.15\n    contrast_lower: float = 1.0\n    contrast_upper: float = 1.3\n    saturation_lower: float = 1.0\n    saturation_upper: float = 1.3\n    hue_delta: float = 0.1\n    blur_sigma_min: float = 0.0\n    blur_sigma_max: float = 1.0\n    noise_stddev_min: float = 0.0\n    noise_stddev_max: float = 0.1\n\nHP = HParams()\nMODEL_NAME = \"mobilenet_v3_lite\"\n\ndef init_strategy():\n    try:\n        tpu = tf.distribute.cluster_resolver.TPUClusterResolver.connect()\n        strategy = tf.distribute.TPUStrategy(tpu)\n        print(\"✔ Accelerator: TPU\", tf.config.list_logical_devices('TPU'))\n        return strategy\n    except Exception:\n        pass\n    gpus = tf.config.list_physical_devices('GPU')\n    if gpus:\n        print(f\"✔ Accelerator: GPU (count={len(gpus)})\")\n        return tf.distribute.get_strategy()\n    print(\"✔ Accelerator: CPU\")\n    return tf.distribute.get_strategy()\n\nAUTO = tf.data.AUTOTUNE\n\ndef decode_image(image_bytes: tf.Tensor):\n    x = tf.image.decode_jpeg(image_bytes, channels=3)\n    x = tf.cast(x, tf.float32) / 255.0\n    return x\n\ndef read_labeled_tfrec(example):\n    spec = {\n        \"image\": tf.io.FixedLenFeature([], tf.string),\n        \"class\": tf.io.FixedLenFeature([], tf.int64)\n    }\n    ex = tf.io.parse_single_example(example, spec)\n    x = decode_image(ex[\"image\"])\n    y = tf.cast(ex[\"class\"], tf.int32)\n    return x, y\n\ndef read_unlabeled_tfrec(example):\n    spec = {\n        \"image\": tf.io.FixedLenFeature([], tf.string),\n        \"id\": tf.io.FixedLenFeature([], tf.string)\n    }\n    ex = tf.io.parse_single_example(example, spec)\n    x = decode_image(ex[\"image\"])\n    i = ex[\"id\"]\n    return x, i\n\ndef load_dataset(files: List[str], labeled: bool):\n    ds = tf.data.TFRecordDataset(files, num_parallel_reads=AUTO)\n    if labeled:\n        ds = ds.map(read_labeled_tfrec, num_parallel_calls=AUTO)\n    else:\n        ds = ds.map(read_unlabeled_tfrec, num_parallel_calls=AUTO)\n    return ds\n\ndef _gaussian_blur_image(img: tf.Tensor, sigma: tf.Tensor):\n    def _apply_blur():\n        radius = tf.cast(tf.math.ceil(3.0 * sigma), tf.int32)\n        kernel_size = radius * 2 + 1\n        coords = tf.cast(tf.range(-radius, radius + 1), tf.float32)\n        sigma2 = sigma * sigma\n        gauss_1d = tf.exp(-(coords * coords) / (2.0 * sigma2))\n        gauss_1d = gauss_1d / tf.reduce_sum(gauss_1d)\n        gauss_2d = tf.tensordot(gauss_1d, gauss_1d, axes=0)\n        gauss_2d = gauss_2d / tf.reduce_sum(gauss_2d)\n        k = kernel_size\n        gauss_2d = tf.reshape(gauss_2d, [k, k, 1, 1])\n        channels = tf.shape(img)[-1]\n        filter_tensor = tf.tile(gauss_2d, [1, 1, channels, 1])\n        img_b = tf.expand_dims(img, axis=0)\n        blurred = tf.nn.depthwise_conv2d(img_b, filter_tensor, strides=[1,1,1,1], padding='SAME')\n        blurred = tf.squeeze(blurred, axis=0)\n        return blurred\n    return tf.cond(tf.greater(sigma, 0.0), _apply_blur, lambda: img)\n\ndef build_datasets(hp: HParams):\n    train_files = tf.io.gfile.glob(f\"{hp.base_path}/{hp.folder}/train/*.tfrec\")\n    val_files   = tf.io.gfile.glob(f\"{hp.base_path}/{hp.folder}/val/*.tfrec\")\n    test_files  = tf.io.gfile.glob(f\"{hp.base_path}/{hp.folder}/test/*.tfrec\")\n    assert train_files and val_files and test_files, \"TFRecordが見つかりません。右の「Add data」で公式データを追加してください。\"\n\n    train_ds = load_dataset(train_files, True)\n    val_ds   = load_dataset(val_files,   True)\n    test_ds  = load_dataset(test_files,  False)\n\n    def augment(x, y, hparams: HParams = HP):\n        x = tf.cast(x, tf.float32)\n        do_zoom_crop = tf.less(tf.random.uniform([], 0.0, 1.0), hparams.prob_zoom)\n\n        def _apply_zoom_crop():\n            scale = tf.random.uniform([], minval=hparams.zoom_scale_min, maxval=hparams.zoom_scale_max, dtype=tf.float32)\n            new_height = tf.cast(tf.cast(hparams.IMAGE_SIZE, tf.float32) * scale, tf.int32)\n            new_width = tf.cast(tf.cast(hparams.IMAGE_SIZE, tf.float32) * scale, tf.int32)\n            x_scaled = tf.image.resize(x, size=[new_height, new_width], method='bicubic')\n            x_cropped = tf.image.random_crop(x_scaled, size=[hparams.IMAGE_SIZE, hparams.IMAGE_SIZE, hparams.in_channels])\n            return x_cropped\n\n        def _skip_zoom_crop():\n            return x\n\n        x = tf.cond(do_zoom_crop, _apply_zoom_crop, _skip_zoom_crop)\n        x.set_shape([hparams.IMAGE_SIZE, hparams.IMAGE_SIZE, hparams.in_channels])\n\n        do_flip = tf.less(tf.random.uniform([], 0.0, 1.0), hparams.prob_flip)\n        x = tf.cond(do_flip, lambda: tf.image.flip_left_right(x), lambda: x)\n\n        k = tf.random.uniform([], minval=0, maxval=4, dtype=tf.int32)\n        x = tf.image.rot90(x, k)\n\n        if hparams.brightness_delta != 0.0:\n            x = tf.image.random_brightness(x, max_delta=hparams.brightness_delta)\n        if hparams.contrast_upper > hparams.contrast_lower:\n            x = tf.image.random_contrast(x, lower=hparams.contrast_lower, upper=hparams.contrast_upper)\n        if hparams.saturation_upper > hparams.saturation_lower:\n            x = tf.image.random_saturation(x, lower=hparams.saturation_lower, upper=hparams.saturation_upper)\n        if hparams.hue_delta != 0.0:\n            x = tf.image.random_hue(x, max_delta=hparams.hue_delta)\n\n        x = tf.clip_by_value(x, 0.0, 1.0)\n\n        if hparams.blur_sigma_max > 0.0:\n            sigma = tf.random.uniform([], minval=hparams.blur_sigma_min, maxval=hparams.blur_sigma_max, dtype=tf.float32)\n            x = _gaussian_blur_image(x, sigma)\n            x = tf.clip_by_value(x, 0.0, 1.0)\n\n        if hparams.noise_stddev_max > 0.0:\n            std = tf.random.uniform([], minval=hparams.noise_stddev_min, maxval=hparams.noise_stddev_max, dtype=tf.float32)\n            noise = tf.random.normal(tf.shape(x), mean=0.0, stddev=std, dtype=tf.float32)\n            x = x + noise\n            x = tf.clip_by_value(x, 0.0, 1.0)\n\n        x = tf.cast(x, tf.float32)\n        return x, y\n\n    train_ds = (train_ds\n                .map(augment, num_parallel_calls=AUTO)\n                .shuffle(hp.shuffle_buffer, seed=hp.seed, reshuffle_each_iteration=True)\n                .batch(hp.batch_size)\n                .prefetch(AUTO))\n    val_ds = (val_ds\n              .batch(hp.batch_size)\n              .prefetch(AUTO))\n    test_ds = (test_ds\n               .batch(hp.batch_size)\n               .prefetch(AUTO))\n\n    options = tf.data.Options()\n    options.experimental_distribute.auto_shard_policy = tf.data.experimental.AutoShardPolicy.DATA\n    val_ds  = val_ds.with_options(options)\n    test_ds = test_ds.with_options(options)\n\n    return train_ds, val_ds, test_ds, train_files, val_files\n\ndef count_items(files: List[str]) -> int:\n    total = 0\n    for f in files:\n        total += int(f.split(\"-\")[-1].split(\".\")[0])\n    return total\n\ndef visualize_augmentations(dataset: tf.data.Dataset,\n                            hp: HParams,\n                            n: int = 16,\n                            max_samples: int = 256,\n                            grid_shape: tuple = (4, 4),\n                            figsize: tuple = (10, 10),\n                            save_path: str = \"/kaggle/working/augment_samples.png\"):\n    assert grid_shape[0] * grid_shape[1] >= n, \"grid が n を収容できるようにしてください\"\n    ds_sampled = dataset.unbatch().take(max_samples)\n    imgs = []\n    labels = []\n    for item in ds_sampled:\n        try:\n            img, lab = item\n        except Exception:\n            continue\n        imgs.append(img.numpy())\n        try:\n            labels.append(int(lab.numpy()))\n        except Exception:\n            labels.append(lab.numpy())\n    if len(imgs) == 0:\n        print(\"⚠ データセットからサンプルが取れませんでした。dataset の形を確認してください。\")\n        return None\n    n_available = len(imgs)\n    n_pick = min(n, n_available)\n    idxs = np.random.choice(n_available, size=n_pick, replace=False)\n    picked_imgs = [imgs[i] for i in idxs]\n    picked_labels = [labels[i] for i in idxs]\n    rows, cols = grid_shape\n    fig, axes = plt.subplots(rows, cols, figsize=figsize)\n    axes = axes.flatten()\n    for ax_idx in range(rows * cols):\n        ax = axes[ax_idx]\n        ax.axis(\"off\")\n        if ax_idx < n_pick:\n            im = picked_imgs[ax_idx]\n            im = np.asarray(im)\n            if im.dtype == np.float32 or im.dtype == np.float64:\n                im = np.clip(im, 0.0, 1.0)\n            else:\n                im = im.astype(np.float32)\n                if im.max() > 1.0:\n                    im = im / 255.0\n            ax.imshow(im)\n            ax.set_title(f\"label: {picked_labels[ax_idx]}\", fontsize=9)\n        else:\n            ax.set_visible(False)\n    plt.suptitle(\"Train augmented samples (random)\", fontsize=14)\n    plt.tight_layout(rect=[0, 0.03, 1, 0.95])\n    os.makedirs(os.path.dirname(save_path) or \".\", exist_ok=True)\n    plt.savefig(save_path, bbox_inches=\"tight\", dpi=150)\n    plt.show()\n    plt.close(fig)\n    print(f\"✔ Saved augmentation samples to: {save_path} (showing {n_pick}/{n_available} available samples)\")\n    return save_path\n\ndef make_optimizer(hp: HParams, steps_per_epoch: int):\n    lr = hp.lr\n    opt_name = hp.optimizer.lower()\n    if opt_name == \"adam\":\n        opt = tf.keras.optimizers.Adam(learning_rate=lr)\n    elif opt_name == \"sgd\":\n        opt = tf.keras.optimizers.SGD(learning_rate=lr, momentum=hp.momentum, nesterov=True)\n    elif opt_name == \"adamw\":\n        opt = tf.keras.optimizers.AdamW(learning_rate=lr, weight_decay=hp.weight_decay)\n    else:\n        raise ValueError(f\"未知のoptimizer: {hp.optimizer}\")\n    return opt\n\ndef make_callbacks(hp: HParams):\n    cbs = []\n    cbs.append(tf.keras.callbacks.ReduceLROnPlateau(\n        monitor=\"val_loss\",\n        factor=0.2,\n        patience=3,\n        min_delta=0.0001,\n        cooldown=1,\n        min_lr=0.000001,\n        verbose=0\n    ))\n    cbs.append(tf.keras.callbacks.ModelCheckpoint(\n        filepath=hp.model_path,\n        monitor=\"val_loss\",\n        save_best_only=True,\n        save_weights_only=False,\n        verbose=0\n    ))\n    return cbs\n\nclass EpochLogger(tf.keras.callbacks.Callback):\n    def __init__(self):\n        super().__init__()\n        self.records = []\n    def on_train_begin(self, logs=None):\n        print(\"epoch |  loss    acc     val_loss  val_acc\")\n    def on_epoch_end(self, epoch, logs=None):\n        logs = logs or {}\n        row = {\n            \"epoch\":       int(epoch + 1),\n            \"loss\":        float(logs.get(\"loss\", float(\"nan\"))),\n            \"accuracy\":    float(logs.get(\"accuracy\", float(\"nan\"))),\n            \"val_loss\":    float(logs.get(\"val_loss\", float(\"nan\"))),\n            \"val_accuracy\":float(logs.get(\"val_accuracy\", float(\"nan\"))),\n        }\n        self.records.append(row)\n        print(f\"{row['epoch']:>5d} | \"\n              f\"{row['loss']:.4f}  {row['accuracy']:.4f}   \"\n              f\"{row['val_loss']:.4f}   {row['val_accuracy']:.4f}\")\n\ndef plot_history(history):\n    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12,4))\n    ax1.plot(history.history.get(\"accuracy\", []), label=\"train\")\n    ax1.plot(history.history.get(\"val_accuracy\", []), label=\"valid\")\n    ax1.set_title(\"Accuracy\"); ax1.set_xlabel(\"Epoch\"); ax1.legend()\n    ax2.plot(history.history.get(\"loss\", []), label=\"train\")\n    ax2.plot(history.history.get(\"val_loss\", []), label=\"valid\")\n    ax2.set_title(\"Loss\"); ax2.set_xlabel(\"Epoch\"); ax2.legend()\n    plt.tight_layout(); plt.show()\n\ndef predict_and_submit(model: tf.keras.Model, test_ds: tf.data.Dataset, hp: HParams):\n    image_ids = []\n    for _, ids in test_ds:\n        image_ids.extend([i.numpy().decode(\"utf-8\") for i in ids])\n    preds = model.predict(test_ds, verbose=0)\n    labels = preds.argmax(axis=1)\n    sub = pd.DataFrame({\"id\": image_ids, \"label\": labels})\n    sub.to_csv(hp.submission_path, index=False)\n    print(f\"✔ submission.csv 作成: {len(sub)} 件\")\n\ndef main(hp: HParams, model_name: str):\n    start = time.time()\n    set_seed(hp.seed)\n    os.makedirs(os.path.dirname(hp.model_path) or \".\", exist_ok=True)\n    strategy = init_strategy()\n    train_ds, val_ds, test_ds, train_files, val_files = build_datasets(hp)\n    train_count = count_items(train_files)\n    val_count   = count_items(val_files)\n    steps_per_epoch  = max(1, train_count // hp.batch_size)\n    validation_steps = max(1, val_count   // hp.batch_size)\n    print(f\"Samples train={train_count:,}, val={val_count:,}\")\n    print(f\"Steps   train={steps_per_epoch}, val={validation_steps}\")\n    visualize_augmentations(train_ds, hp, n=16, max_samples=256,\n                            grid_shape=(4,4), figsize=(10,10),\n                            save_path=\"/kaggle/working/augment_samples.png\")\n    with strategy.scope():\n        model = get_model_by_name(model_name, image_size=hp.image_size,\n                                  num_classes=hp.num_classes, in_channels=hp.in_channels)\n        model.compile(optimizer=make_optimizer(hp, steps_per_epoch),\n                      loss=\"sparse_categorical_crossentropy\",\n                      metrics=[\"accuracy\"],\n                      jit_compile=hp.jit_compile)\n    epoch_logger = EpochLogger()\n    history = model.fit(\n        train_ds,\n        epochs=hp.epochs,\n        validation_data=val_ds,\n        steps_per_epoch=steps_per_epoch,\n        validation_steps=validation_steps,\n        callbacks=make_callbacks(hp) + [epoch_logger],\n        verbose=0\n    )\n    plot_history(history)\n    predict_and_submit(model, test_ds, hp)\n    best_val_acc = max(history.history.get(\"val_accuracy\", [float(\"nan\")]))\n    log_df = pd.DataFrame(epoch_logger.records)\n    log_path = \"/kaggle/working/train_log.csv\"\n    log_df.to_csv(log_path, index=False)\n    print(f\"✔ Best val acc: {best_val_acc:.4f}\")\n    print(f\"✔ Model saved : {hp.model_path}\")\n    print(f\"✔ Train log saved: {log_path}\")\n    print(f\"✔ Elapsed     : {(time.time()-start)/60:.1f} min\")\n\nif __name__ == \"__main__\":\n    _ = main(HP, MODEL_NAME)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-25T09:45:54.711899Z","iopub.execute_input":"2025-10-25T09:45:54.712275Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-25T02:10:32.271460Z","iopub.execute_input":"2025-10-25T02:10:32.272296Z","iopub.status.idle":"2025-10-25T02:10:32.409342Z","shell.execute_reply.started":"2025-10-25T02:10:32.272260Z","shell.execute_reply":"2025-10-25T02:10:32.408151Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-25T02:11:37.598222Z","iopub.execute_input":"2025-10-25T02:11:37.599124Z","iopub.status.idle":"2025-10-25T02:11:37.610007Z","shell.execute_reply.started":"2025-10-25T02:11:37.599090Z","shell.execute_reply":"2025-10-25T02:11:37.608671Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-25T02:12:04.101966Z","iopub.execute_input":"2025-10-25T02:12:04.102723Z","iopub.status.idle":"2025-10-25T02:12:05.848308Z","shell.execute_reply.started":"2025-10-25T02:12:04.102688Z","shell.execute_reply":"2025-10-25T02:12:05.847019Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-25T02:18:41.987327Z","iopub.execute_input":"2025-10-25T02:18:41.987781Z","iopub.status.idle":"2025-10-25T02:18:53.415529Z","shell.execute_reply.started":"2025-10-25T02:18:41.987751Z","shell.execute_reply":"2025-10-25T02:18:53.414479Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-25T05:50:42.445202Z","iopub.execute_input":"2025-10-25T05:50:42.446101Z","iopub.status.idle":"2025-10-25T06:56:38.511836Z","shell.execute_reply.started":"2025-10-25T05:50:42.446068Z","shell.execute_reply":"2025-10-25T06:56:38.509405Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-25T06:56:55.286349Z","iopub.execute_input":"2025-10-25T06:56:55.287244Z","iopub.status.idle":"2025-10-25T06:57:01.178533Z","shell.execute_reply.started":"2025-10-25T06:56:55.287213Z","shell.execute_reply":"2025-10-25T06:57:01.177557Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-25T06:57:08.025583Z","iopub.execute_input":"2025-10-25T06:57:08.025938Z","iopub.status.idle":"2025-10-25T07:02:48.819030Z","shell.execute_reply.started":"2025-10-25T06:57:08.025915Z","shell.execute_reply":"2025-10-25T07:02:48.817652Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-25T07:06:52.325720Z","iopub.execute_input":"2025-10-25T07:06:52.326123Z","iopub.status.idle":"2025-10-25T07:14:08.162061Z","shell.execute_reply.started":"2025-10-25T07:06:52.326100Z","shell.execute_reply":"2025-10-25T07:14:08.160567Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-25T07:14:15.004167Z","iopub.execute_input":"2025-10-25T07:14:15.004602Z","iopub.status.idle":"2025-10-25T07:14:15.040952Z","shell.execute_reply.started":"2025-10-25T07:14:15.004562Z","shell.execute_reply":"2025-10-25T07:14:15.039994Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-25T07:14:21.073257Z","iopub.execute_input":"2025-10-25T07:14:21.074319Z","iopub.status.idle":"2025-10-25T07:14:21.091640Z","shell.execute_reply.started":"2025-10-25T07:14:21.074234Z","shell.execute_reply":"2025-10-25T07:14:21.090515Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}