{"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":"gpu","dataSources":[{"sourceType":"competition","sourceId":21154,"databundleVersionId":1243559}],"dockerImageVersionId":31091,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# =========================================================\n# セル1：モデル定義クラス群（任意追加OK）\n# - ここに自作モデルをどんどん足していく\n# - すべてのモデルは「入力=(H,W,3)、出力=softmax(num_classes)」を満たすこと\n# - 下の MODEL_REGISTRY にクラスを登録しておくと、学習セルから名前指定で呼べる\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#ーーーーーーーーーーーーーーーーーーーーーーーー\n\nimport tensorflow as tf\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#   - MobileNetV2/V3系の基本モジュール\n#   - expand(1x1) → depthwise(3x3) → project(1x1) + 残差(条件付)\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        \"\"\"\n        Args:\n            in_ch        : 入力チャネル数\n            out_ch       : 出力チャネル数\n            stride       : Depthwise畳み込みのストライド（1 or 2）\n            expand_ratio : 中間の拡張係数（hidden = round(in_ch * expand_ratio)）\n        \"\"\"\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        # 1x1 畳み込み（拡張）— expand_ratio が 1 の場合はスキップ\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        # 3x3 Depthwise 畳み込み（ストライドあり/なし）\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        # 1x1 畳み込み（出力チャネルへ縮小）\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        \"\"\"\n        順伝播\n        - 残差接続できる条件（stride==1 かつ チャネル数一致）では x + F(x)\n        \"\"\"\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# ---------------------------------------------------------\n# MobileNetV3 ライト版（ベースライン用）\n#   - 入力：(H, W, 3)\n#   - 出力：softmax(num_classes)\n#   - 仕様：軽量＆拡張性重視の簡易版\n# ---------------------------------------------------------\nclass MobileNetV3Lite(tf.keras.Model):\n    def __init__(self, image_size=(224, 224), num_classes=104, in_channels=3):\n        \"\"\"\n        Args:\n            image_size : 入力画像サイズ (H, W)\n            num_classes: クラス数\n            in_channels: 入力チャネル数（RGB=3）\n        \"\"\"\n        super().__init__(name=\"MobileNetV3Lite\")\n\n        H, W = image_size\n        C = in_channels\n\n        # Stem（最初のConv）\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        # 逆残差ブロック列（軽量構成）\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        # Head（Conv → GAP → Dense）\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        \"\"\"\n        順伝播\n        \"\"\"\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 をラップして「入力=(H,W,3) -> softmax(num_classes)」にする\n# ---------------------------------------------------------\ndef _build_keras_backbone(backbone_cls, preprocess_fn, image_size, num_classes,\n                          in_channels=3, pretrained=False, freeze_backbone=True):\n    \"\"\"\n    backbone_cls: Keras Application クラス（例: ResNet50）\n    preprocess_fn: そのモデル用の preprocess_input 関数\n    image_size: (H, W)\n    num_classes: 出力クラス数\n    pretrained: bool (True -> weights='imagenet')\n    freeze_backbone: bool (True -> backbone.trainable = False)\n    \"\"\"\n    H, W = image_size\n    inputs = tf.keras.Input(shape=(H, W, in_channels), name=\"input_image\")\n    # Keras pretrained models expect images scaled to [0,255] (most of them),\n    # so multiply by 255.0 then call preprocess_input.\n    x = inputs * 255.0\n    # preprocess_fn expects numpy/tensor input in [0,255] and returns processed tensor\n    x = tf.keras.layers.Lambda(lambda z: preprocess_fn(z), name=f\"{backbone_cls.__name__}_preproc\")(x)\n\n    # instantiate backbone with include_top=False and using our preprocessed tensor as input\n    base = backbone_cls(include_top=False,\n                        weights=\"imagenet\" if pretrained else None,\n                        input_tensor=x,\n                        pooling=\"avg\")  # global average pool\n\n    # freeze or unfreeze backbone\n    base.trainable = not freeze_backbone\n\n    # add final classifier head\n    outputs = tf.keras.layers.Dense(num_classes, activation=\"softmax\", name=\"predictions\")(base.output)\n\n    model = tf.keras.Model(inputs=inputs, outputs=outputs, name=f\"{backbone_cls.__name__}_head\")\n    return model\n\n# ---------------------------------------------------------\n# モデルレジストリ\n#   - 学習セルから文字列でモデルを選べるようにする辞書\n#   - ここに自作モデルを追加登録すれば main 側で呼べます\n# ---------------------------------------------------------\nMODEL_REGISTRY = {\n    # 自作（既存）\n    \"mobilenet_v3_lite\": MobileNetV3Lite,\n\n    # Keras Applications: 事前学習済みモデルを簡単に呼べるようにラップ\n    # 使い方: get_model_by_name(\"resnet50\", ..., pretrained=True, freeze_backbone=True)\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\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\n# ---------------------------------------------------------\n# get_model_by_name を拡張（pretrained, freeze_backbone オプション追加）\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    \"\"\"\n    name: str (キー名)\n    image_size: (H,W)\n    num_classes: int\n    in_channels: int\n    pretrained: bool -> Keras Applications の pretrained weights を使う（weights='imagenet'）\n    freeze_backbone: bool -> 事前学習モデルの backbone を凍結するか（True 推奨で最初は特徴抽出）\n    \"\"\"\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    # constructor のシグネチャはモデルによって異なるため、呼び出しは try/except で柔軟に\n    try:\n        # まず、KerasAppラッパー形式（lambda で上書きしたもの）は (image_size, num_classes, in_channels, pretrained, freeze_backbone)\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        # 旧来のクラス（例: MobileNetV3Lite）はクラスで渡されているので instantiate\n        # e.g. MODEL_REGISTRY[\"mobilenet_v3_lite\"] = MobileNetV3Lite (class)\n        if isinstance(constructor, type):\n            model = constructor(image_size=image_size, num_classes=num_classes, in_channels=in_channels)\n        else:\n            # fallback: try simple call\n            model = constructor(image_size, num_classes, in_channels)\n    return model","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-03-18T06:54:11.357528Z","iopub.execute_input":"2026-03-18T06:54:11.357688Z","iopub.status.idle":"2026-03-18T06:54:24.488052Z","shell.execute_reply.started":"2026-03-18T06:54:11.357674Z","shell.execute_reply":"2026-03-18T06:54:24.487485Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================================================\n# セル2：学習・推論・提出（TTAなし）\n# - 上のセルで定義したモデルを「名前指定」で呼び出せる\n# - ハイパーパラメータは HParams に一元化（ここだけ変えればOK）\n# - CPU/GPU/TPU 自動対応、公式TFRecordのみ使用\n# =========================================================\n\n# ---------------------------------------------------------\n# 余計な出力を抑える設定（Kaggle向け）\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\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nfrom dataclasses import dataclass\nfrom typing import Tuple, List, Optional\n\nimport shutil\nfrom pathlib import Path\n\n# ←←← 重要：セル1を実行済みにしておくこと（get_model_by_name を使います）\n\n# Kaggleでよく使うエイリアス（tf.dataの自動最適化）\nAUTO = tf.data.AUTOTUNE\n\n\n# =========================================================\n# 1) 乱数シード\n# =========================================================\ndef set_seed(seed: int = 42) -> None:\n    \"\"\"\n    乱数シードを固定して、実験のばらつきを抑える。\n    ※完全な再現性は環境要因で保証されないことがある（TPU/GPU等）\n    \"\"\"\n    np.random.seed(seed)\n    tf.random.set_seed(seed)\n\n\n# =========================================================\n# 2) ハイパーパラメータ（ここだけ編集すれば実験OK）\n# =========================================================\n@dataclass\nclass HParams:\n    # -----------------\n    # データ\n    # -----------------\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\n    # -----------------\n    # 学習\n    # -----------------\n    batch_size: int = 64\n    epochs: int = 500\n    seed: int = 42\n    shuffle_buffer: int = 2000\n\n    # -----------------\n    # 最適化（学習率もここで）\n    # -----------------\n    optimizer: str = \"adamw\"           # \"adam\" | \"sgd\" | \"adamw\"\n    lr: float = 0.0001\n    weight_decay: float = 0.0001\n    momentum: float = 0.9\n    jit_compile: bool = False\n\n    # -----------------\n    # 出力\n    # -----------------\n    model_path: str = \"/kaggle/working/model_baseline.weights.h5\"\n    submission_path: str = \"submission.csv\"\n\n    # -----------------\n    # Augmentation params\n    # -----------------\n    prob_flip: float = 0.5   # 左右反転（確率）\n\n    # random crop / zoom\n    prob_zoom: float = 0\n    zoom_scale_min: float = 1.0  # 1.0=原寸\n    zoom_scale_max: float = 1.0\n\n    # color jitter\n    brightness_delta: float = 0.0\n    contrast_lower: float = 1.0\n    contrast_upper: float = 1.0\n    saturation_lower: float = 1.0\n    saturation_upper: float = 1.0\n    hue_delta: float = 0.0\n\n    # gaussian blur\n    blur_sigma_min: float = 0.0\n    blur_sigma_max: float = 0.0\n\n    # gaussian noise\n    noise_stddev_min: float = 0.0\n    noise_stddev_max: float = 0.0\n\n    # -----------------\n    # 追加：学習ログCSV（EpochLoggerのrecordsを保存したいとき）\n    # ※機能追加に見えるが「ログを保存するだけ」で学習挙動は不変\n    # -----------------\n    train_log_path: str = \"/kaggle/working/train_log.csv\"\n\n\n# 実験ごとの設定（必要に応じてここを変更）\nHP = HParams()\nMODEL_NAME = \"mobilenet_v3_lite\"\n\n\n# =========================================================\n# 3) デバイス初期化（TPU/GPU/CPU 自動選択）\n# =========================================================\ndef init_strategy() -> tf.distribute.Strategy:\n    \"\"\"\n    Kaggle向け: TPU→GPU→CPU の優先順で分散戦略を初期化して返す。\n    戻り値: tf.distribute.Strategy\n    \"\"\"\n    # 1) TPUをまず試す（接続できない場合は例外）\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\n    # 2) GPU\n    gpus = tf.config.list_physical_devices(\"GPU\")\n    if gpus:\n        print(f\"Accelerator: GPU (count={len(gpus)})\")\n        # 複数GPUで同期学習したければ下を有効化：\n        # return tf.distribute.MirroredStrategy()\n        return tf.distribute.get_strategy()\n\n    # 3) CPU\n    print(\"Accelerator: CPU\")\n    return tf.distribute.get_strategy()\n\n\n# =========================================================\n# 4) TFRecord 読み込み・前処理\n# =========================================================\ndef decode_image(image_bytes: tf.Tensor) -> tf.Tensor:\n    \"\"\"\n    JPEGバイト列 → RGBテンソル（float32, 0〜1）\n    ※ TFRecordの画像サイズは固定なので resize は不要\n    \"\"\"\n    x = tf.image.decode_jpeg(image_bytes, channels=3)\n    x = tf.cast(x, tf.float32) / 255.0\n    return x\n\n\ndef read_labeled_tfrec(example: tf.Tensor):\n    \"\"\"\n    学習/検証用: (image, label) を取り出す\n    \"\"\"\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: tf.Tensor):\n    \"\"\"\n    テスト用: (image, id) を取り出す\n    \"\"\"\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\n\ndef load_dataset(files: List[str], labeled: bool) -> tf.data.Dataset:\n    \"\"\"\n    TFRecordファイル群から tf.data.Dataset を構築する。\n    ※ここでは map まで（shuffle/batch/prefetch は build_datasets でやる）\n    \"\"\"\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\n\n# =========================================================\n# 5) Augmentation（元の機能を維持しつつ読みやすく整理）\n# =========================================================\ndef _gaussian_blur_image(img: tf.Tensor, sigma: tf.Tensor) -> tf.Tensor:\n    \"\"\"\n    ガウシアンブラーを depthwise_conv2d で適用する。\n    img: [H,W,C], float32 in [0,1]\n    sigma: scalar float32 (>=0). sigma<=0 なら何もしない\n    \"\"\"\n    def _apply_blur():\n        # kernel半径: ceil(3*sigma)\n        radius = tf.cast(tf.math.ceil(3.0 * sigma), tf.int32)\n        k = radius * 2 + 1  # odd\n\n        coords = tf.cast(tf.range(-radius, radius + 1), tf.float32)\n        sigma2 = sigma * sigma\n\n        # 1D gaussian\n        gauss_1d = tf.exp(-(coords * coords) / (2.0 * sigma2))\n        gauss_1d = gauss_1d / tf.reduce_sum(gauss_1d)\n\n        # 2D kernel\n        gauss_2d = tf.tensordot(gauss_1d, gauss_1d, axes=0)\n        gauss_2d = gauss_2d / tf.reduce_sum(gauss_2d)\n\n        # [k,k,1,1] -> [k,k,C,1]\n        gauss_2d = tf.reshape(gauss_2d, [k, k, 1, 1])\n        channels = tf.shape(img)[-1]\n        filt = tf.tile(gauss_2d, [1, 1, channels, 1])\n\n        img_b = tf.expand_dims(img, axis=0)  # [1,H,W,C]\n        blurred = tf.nn.depthwise_conv2d(img_b, filt, strides=[1,1,1,1], padding=\"SAME\")\n        return tf.squeeze(blurred, axis=0)\n\n    return tf.cond(tf.greater(sigma, 0.0), _apply_blur, lambda: img)\n\n\ndef build_datasets(hp: HParams):\n    \"\"\"\n    学習・検証・テストの Dataset を構築して返す（trainのみaugment適用）。\n    val は「順序固定」して、後付けname(連番)がズレないようにする。\n    \"\"\"\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\n    assert train_files and val_files and test_files, \\\n        \"TFRecordが見つかりません。右の「Add data」で公式データを追加してください。\"\n\n    # ★ 順序固定（重要）\n    train_files = sorted(train_files)\n    val_files   = sorted(val_files)\n    test_files  = sorted(test_files)\n\n    train_ds = load_dataset(train_files, labeled=True)\n    val_ds   = load_dataset(val_files,   labeled=True)\n    test_ds  = load_dataset(test_files,  labeled=False)\n\n    # ---------------------------------------------------------\n    # Augmentation（元コードの挙動を維持）\n    #  - zoom/crop は確率で適用\n    #  - flip は確率で適用\n    #  - rot90 は常に適用\n    #  - color jitter は指定が有効なら適用（元コード同様）\n    #  - blur/noise は max>0 の時のみ適用（元コード同様）\n    # ---------------------------------------------------------\n    def augment(x, y, hparams: HParams = HP):\n        x = tf.cast(x, tf.float32)  # 念のため\n\n        # ---- (A) Random zoom & crop (probabilistic) ----\n        do_zoom = tf.less(tf.random.uniform([], 0.0, 1.0), hparams.prob_zoom)\n\n        def _apply_zoom_crop():\n            scale = tf.random.uniform([], hparams.zoom_scale_min, hparams.zoom_scale_max, dtype=tf.float32)\n            new_h = tf.cast(tf.cast(hparams.IMAGE_SIZE, tf.float32) * scale, tf.int32)\n            new_w = tf.cast(tf.cast(hparams.IMAGE_SIZE, tf.float32) * scale, tf.int32)\n\n            x_scaled = tf.image.resize(x, size=[new_h, new_w], method=\"bicubic\")\n            x_crop = tf.image.random_crop(\n                x_scaled,\n                size=[hparams.IMAGE_SIZE, hparams.IMAGE_SIZE, hparams.in_channels]\n            )\n            return x_crop\n\n        x = tf.cond(do_zoom, _apply_zoom_crop, lambda: x)\n\n        # tf.cond の出力は shape が不定になりやすいので固定\n        x.set_shape([hparams.IMAGE_SIZE, hparams.IMAGE_SIZE, hparams.in_channels])\n\n        # ---- (B) Flip (probabilistic) ----\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        # ---- (C) rot90 (always) ----\n        k = tf.random.uniform([], minval=0, maxval=4, dtype=tf.int32)\n        x = tf.image.rot90(x, k)\n\n        # ---- (D) Color jitter (conditional like original) ----\n        if hparams.brightness_delta != 0.0:\n            x = tf.image.random_brightness(x, max_delta=hparams.brightness_delta)\n\n        if hparams.contrast_upper > hparams.contrast_lower:\n            x = tf.image.random_contrast(x, lower=hparams.contrast_lower, upper=hparams.contrast_upper)\n\n        if hparams.saturation_upper > hparams.saturation_lower:\n            x = tf.image.random_saturation(x, lower=hparams.saturation_lower, upper=hparams.saturation_upper)\n\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        # ---- (E) Gaussian blur ----\n        if hparams.blur_sigma_max > 0.0:\n            sigma = tf.random.uniform([], hparams.blur_sigma_min, 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        # ---- (F) Gaussian noise ----\n        if hparams.noise_stddev_max > 0.0:\n            std = tf.random.uniform([], hparams.noise_stddev_min, 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 = tf.clip_by_value(x + noise, 0.0, 1.0)\n\n        return tf.cast(x, tf.float32), y\n\n    train_ds = (\n        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    )\n\n    val_ds  = val_ds.batch(hp.batch_size).prefetch(AUTO)\n    test_ds = test_ds.batch(hp.batch_size).prefetch(AUTO)\n\n    # TPU の評価/推論シャーディングを安定化\n    options = tf.data.Options()\n    options.experimental_distribute.auto_shard_policy = tf.data.experimental.AutoShardPolicy.DATA\n\n    # ★ valの順序固定（重要）\n    options.experimental_deterministic = True\n\n    val_ds  = val_ds.with_options(options)\n    test_ds = test_ds.with_options(options)\n    \n    # --- export 用：auto_shard を無効化して「全件を単一プロセスで」回す ---\n    export_opt = tf.data.Options()\n    export_opt.experimental_deterministic = True\n    export_opt.experimental_distribute.auto_shard_policy = tf.data.experimental.AutoShardPolicy.OFF\n    \n    val_export_ds = load_dataset(val_files, labeled=True)   # ← ここで作り直すのが超重要\n    val_export_ds = val_export_ds.with_options(export_opt)\n    val_export_ds = val_export_ds.batch(hp.batch_size).prefetch(AUTO)\n    \n    return train_ds, val_ds, val_export_ds, test_ds, train_files, val_files\n\n# =========================================================\n# 6) 便利関数：サンプル数カウント（命名規則を利用）\n# =========================================================\ndef count_items(files: List[str]) -> int:\n    \"\"\"\n    tfrec ファイル名の末尾に入っている枚数（例: ...-798.tfrec）を合計して返す。\n    \"\"\"\n    total = 0\n    for f in files:\n        total += int(f.split(\"-\")[-1].split(\".\")[0])\n    return total\n\n\n# =========================================================\n# 7) Augmentation可視化（確認用）\n# =========================================================\ndef visualize_augmentations(\n    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):\n    \"\"\"\n    train_ds（augment済み）から画像を取り出して可視化する。\n    \"\"\"\n    assert grid_shape[0] * grid_shape[1] >= n, \"grid が n を収容できるようにしてください\"\n\n    ds_sampled = dataset.unbatch().take(max_samples)\n\n    imgs, labels = [], []\n    for item in ds_sampled:\n        try:\n            img, lab = item\n        except Exception:\n            continue\n        imgs.append(img.numpy())\n        labels.append(int(lab.numpy()))\n\n    if len(imgs) == 0:\n        print(\"データセットからサンプルが取れませんでした。dataset の形を確認してください。\")\n        return None\n\n    n_pick = min(n, len(imgs))\n    idxs = np.random.choice(len(imgs), size=n_pick, replace=False)\n\n    rows, cols = grid_shape\n    fig, axes = plt.subplots(rows, cols, figsize=figsize)\n    axes = axes.flatten()\n\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 = np.asarray(imgs[idxs[ax_idx]])\n            im = np.clip(im, 0.0, 1.0)\n            ax.imshow(im)\n            ax.set_title(f\"label: {labels[idxs[ax_idx]]}\", fontsize=9)\n        else:\n            ax.set_visible(False)\n\n    plt.suptitle(\"Train augmented samples (random)\", fontsize=14)\n    plt.tight_layout(rect=[0, 0.03, 1, 0.95])\n\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\n    print(f\"Saved augmentation samples to: {save_path} (showing {n_pick}/{len(imgs)} available samples)\")\n    return save_path\n\n\n# =========================================================\n# 8) Optimizer / Callbacks / EpochLogger\n# =========================================================\ndef make_optimizer(hp: HParams, steps_per_epoch: int):\n    \"\"\"\n    optimizer を作成（元コードの仕様通り）。\n    ※steps_per_epoch は将来スケジューラ導入などの拡張余地用（現状未使用）\n    \"\"\"\n    lr = hp.lr\n    opt_name = hp.optimizer.lower()\n\n    if opt_name == \"adam\":\n        return tf.keras.optimizers.Adam(learning_rate=lr)\n\n    if opt_name == \"sgd\":\n        return tf.keras.optimizers.SGD(learning_rate=lr, momentum=hp.momentum, nesterov=True)\n\n    if opt_name == \"adamw\":\n        return tf.keras.optimizers.AdamW(learning_rate=lr, weight_decay=hp.weight_decay)\n\n    raise ValueError(f\"未知のoptimizer: {hp.optimizer}\")\n\n\ndef make_callbacks(hp: HParams):\n    \"\"\"\n    コールバック作成（元コードの仕様通り）\n    - ReduceLROnPlateau\n    - ModelCheckpoint\n    \"\"\"\n    cbs = [\n        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        tf.keras.callbacks.ModelCheckpoint(\n            filepath=hp.model_path,\n            monitor=\"val_loss\",\n            save_best_only=True,\n            save_weights_only=True,\n            verbose=0\n        )\n    ]\n    return cbs\n\n\nclass EpochLogger(tf.keras.callbacks.Callback):\n    \"\"\"\n    エポックごとの train/val 指標を 1行で表示しつつ内部に記録する。\n    records は後で DataFrame 化してCSV保存できる。\n    \"\"\"\n    def __init__(self):\n        super().__init__()\n        self.records = []\n\n    def on_train_begin(self, logs=None):\n        print(\"epoch |  loss    acc     val_loss  val_acc\")\n\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\n        print(\n            f\"{row['epoch']:>5d} | \"\n            f\"{row['loss']:.4f}  {row['accuracy']:.4f}   \"\n            f\"{row['val_loss']:.4f}   {row['val_accuracy']:.4f}\"\n        )\n\n\ndef save_train_log(epoch_logger: EpochLogger, log_path: str) -> pd.DataFrame:\n    \"\"\"\n    EpochLogger.records をCSVに保存する（学習分析用）。\n    \"\"\"\n    df = pd.DataFrame(epoch_logger.records)\n    df.to_csv(log_path, index=False)\n    print(f\"Train log saved: {log_path}\")\n    return df\n\n\n# =========================================================\n# 9) 学習曲線プロット\n# =========================================================\ndef plot_history(history):\n    \"\"\"Keras 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\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\n    plt.tight_layout()\n    plt.show()\n\n\n# =========================================================\n# 10) 推論＆提出\n# =========================================================\ndef predict_and_submit(model: tf.keras.Model, test_ds: tf.data.Dataset, hp: HParams):\n    \"\"\"\n    テストデータで予測し、submission.csv を生成する。\n    \"\"\"\n    image_ids = []\n    for _, ids in test_ds:\n        image_ids.extend([i.numpy().decode(\"utf-8\") for i in ids])\n\n    preds = model.predict(test_ds, verbose=0)\n    labels = preds.argmax(axis=1)\n\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\n\n# =========================================================\n# 11) 検証：詳細評価CSV\n# =========================================================\ndef evaluate_and_export_csv(\n    model: tf.keras.Model,\n    val_ds: tf.data.Dataset,\n    hp: HParams,\n    csv_path: str = \"/kaggle/working/val_results.csv\",\n    digits: int = 6\n):\n    \"\"\"\n    val_ds に対して以下を全件保存（分析用）。\n    - name（後付け: 000000.png 形式）\n    - gt_label\n    - pred_label\n    - score（= confidence）\n    - correct（True/False）\n    \"\"\"\n    names, gt, pred, score = [], [], [], []\n\n    for x, y in val_ds:\n        preds = model.predict(x, verbose=0)\n        p = preds.argmax(axis=1)\n        s = preds.max(axis=1)\n\n        gt.extend(y.numpy())\n        pred.extend(p)\n        score.extend(s)\n\n    gt = np.array(gt)\n    pred = np.array(pred)\n    score = np.array(score)\n    correct = (gt == pred)\n\n    # name は val の固定順に 0..N-1 を振る（画像ファイル名と一致）\n    n = len(gt)\n    names = [f\"{i:0{digits}d}.png\" for i in range(n)]\n\n    df = pd.DataFrame({\n        \"name\": names,\n        \"gt_label\": gt,\n        \"pred_label\": pred,\n        \"score\": score,\n        \"correct\": correct\n    })\n    df.to_csv(csv_path, index=False)\n\n    print(f\"val_results saved: {csv_path}\")\n    print(f\"Accuracy: {correct.mean():.4f}\")\n    return df\n\ndef export_val_images(\n    val_ds: tf.data.Dataset,\n    save_dir: str = \"/kaggle/working/val_images\",\n    digits: int = 6,\n    skip_if_exists: bool = True\n) -> int:\n    \"\"\"\n    val を固定順で全件 1フォルダに保存する（教材用）。\n    ファイル名: 000000.png, 000001.png, ...\n    ※ val_ds は「順序固定」「全件」になっているものを渡す想定（val_export_ds 推奨）\n    \"\"\"\n    os.makedirs(save_dir, exist_ok=True)\n\n    # 既に出力済みならスキップ（教材素材は1回で十分）\n    if skip_if_exists:\n        existing = tf.io.gfile.glob(os.path.join(save_dir, \"*.png\"))\n        if len(existing) > 0:\n            print(f\"val images already exist -> skip export: {save_dir} ({len(existing)} files)\")\n            return len(existing)\n\n    saved = 0\n    for idx, (x, y) in enumerate(val_ds.unbatch()):\n        img = tf.clip_by_value(x, 0.0, 1.0)          # [0,1]\n        img_u8 = tf.cast(img * 255.0, tf.uint8)      # uint8\n        png = tf.image.encode_png(img_u8)\n        path = os.path.join(save_dir, f\"{idx:0{digits}d}.png\")\n        tf.io.write_file(path, png)\n        saved += 1\n\n    print(f\"val images exported: {saved} files -> {save_dir}\")\n    return saved\n\ndef visualize_low_confidence_images(\n    model: tf.keras.Model,\n    dataset: tf.data.Dataset,\n    n: int = 16,\n    class_names=None,\n    figsize=(12, 12)\n):\n    \"\"\"\n    予測確率が低いサンプルを可視化する（誤分類分析の入口）。\n    \"\"\"\n    images, true_labels, pred_labels, confidences, names = [], [], [], [], []\n\n    for idx, (x, y) in enumerate(dataset.unbatch()):\n        pred = model.predict(tf.expand_dims(x, 0), verbose=0)[0]\n        pred_label = int(pred.argmax())\n        conf = float(pred[pred_label])\n\n        images.append(x.numpy())\n        true_labels.append(int(y.numpy()))\n        pred_labels.append(pred_label)\n        confidences.append(conf)\n        names.append(f\"{idx:06d}.png\")  # 後付けname（画像ファイル名と一致）\n\n    idxs = np.argsort(confidences)[:n]\n\n    cols = int(np.sqrt(n))\n    rows = int(np.ceil(n / cols))\n    fig, axes = plt.subplots(rows, cols, figsize=figsize)\n    axes = axes.flatten()\n\n    for i, idx in enumerate(idxs):\n        ax = axes[i]\n        ax.imshow(np.clip(images[idx], 0, 1))\n        ax.axis(\"off\")\n\n        correct = true_labels[idx] == pred_labels[idx]\n        color = \"green\" if correct else \"red\"\n        mark = \"OK\" if correct else \"NG\"\n\n        true_name = true_labels[idx] if class_names is None else class_names[true_labels[idx]]\n        pred_name = pred_labels[idx] if class_names is None else class_names[pred_labels[idx]]\n\n        ax.set_title(\n            f\"{mark} name: {names[idx]}\\n\"\n            f\"true: {true_name}\\n\"\n            f\"pred: {pred_name}\\n\"\n            f\"conf: {confidences[idx]:.2f}\",\n            color=color,\n            fontsize=9\n        )\n\n    for j in range(i + 1, len(axes)):\n        axes[j].set_visible(False)\n\n    plt.tight_layout()\n    plt.show()\n    print(f\"{n} 枚の低信頼度画像を表示しました。\")\n\n\n# =========================================================\n# 12) 誤分類画像を保存\n# =========================================================\ndef export_misclassified_images(\n    model: tf.keras.Model,\n    dataset: tf.data.Dataset,\n    save_dir: str = \"/kaggle/working/misclassified\",\n    class_names=None,\n    max_images: int = 100\n):\n    \"\"\"\n    間違えた画像を保存する。\n    ファイル名に true / pred / confidence を含める。\n    \"\"\"\n    os.makedirs(save_dir, exist_ok=True)\n\n    saved = 0\n    for idx, (x, y) in enumerate(dataset.unbatch()):\n        if saved >= max_images:\n            break\n\n        pred = model.predict(tf.expand_dims(x, 0), verbose=0)[0]\n        pred_label = int(pred.argmax())\n        conf = float(pred[pred_label])\n        true_label = int(y.numpy())\n\n        if pred_label == true_label:\n            continue\n\n        true_name = true_label if class_names is None else class_names[true_label]\n        pred_name = pred_label if class_names is None else class_names[pred_label]\n\n        # name(=val画像ファイル名)を含める\n        name = f\"{idx:06d}.png\"\n        filename = f\"{idx:05d}_name-{name}_true-{true_name}_pred-{pred_name}_conf-{conf:.2f}.png\"\n        path = os.path.join(save_dir, filename)\n\n        img = np.clip(x.numpy(), 0, 1)\n        plt.imsave(path, img)\n        saved += 1\n\n    print(f\"Misclassified images saved: {saved} files -> {save_dir}\")\n    return saved\n\n\n# =========================================================\n# 13) 画像+予測の可視化（元コードの未定義変数を除去して修正）\n# =========================================================\ndef visualize_predictions(\n    model: tf.keras.Model,\n    dataset: tf.data.Dataset,\n    class_names=None,\n    n: int = 16,\n    figsize=(12, 12)\n):\n    \"\"\"\n    画像 + 正解 + 予測 + 正誤 + 予測確率 を表示する（勉強＆分析用）。\n    \"\"\"\n    images, true_labels, pred_labels, confidences, names = [], [], [], [], []\n\n    for idx, (x, y) in enumerate(dataset.unbatch().take(n)):\n        pred = model.predict(tf.expand_dims(x, 0), verbose=0)[0]\n        pred_label = int(pred.argmax())\n        conf = float(pred[pred_label])\n\n        images.append(x.numpy())\n        true_labels.append(int(y.numpy()))\n        pred_labels.append(pred_label)\n        confidences.append(conf)\n        names.append(f\"{idx:06d}.png\")  # 表示用（先頭n枚の連番）\n\n    cols = int(np.sqrt(n))\n    rows = int(np.ceil(n / cols))\n    fig, axes = plt.subplots(rows, cols, figsize=figsize)\n    axes = axes.flatten()\n\n    for i in range(len(images)):\n        ax = axes[i]\n        ax.imshow(np.clip(images[i], 0, 1))\n        ax.axis(\"off\")\n\n        correct = true_labels[i] == pred_labels[i]\n        color = \"green\" if correct else \"red\"\n        mark = \"OK\" if correct else \"NG\"\n\n        true_name = true_labels[i] if class_names is None else class_names[true_labels[i]]\n        pred_name = pred_labels[i] if class_names is None else class_names[pred_labels[i]]\n\n        ax.set_title(\n            f\"{mark} name: {names[i]}\\n\"\n            f\"true: {true_name}\\n\"\n            f\"pred: {pred_name}\\n\"\n            f\"conf: {confidences[i]:.2f}\",\n            color=color,\n            fontsize=9\n        )\n\n    for j in range(i + 1, len(axes)):\n        axes[j].set_visible(False)\n\n    plt.tight_layout()\n    plt.show()\n\ndef _safe_rmtree(path: str) -> None:\n    \"\"\"フォルダを安全に削除（存在しなくてもOK）\"\"\"\n    if tf.io.gfile.exists(path):\n        try:\n            shutil.rmtree(path)\n        except Exception:\n            # tf.io.gfile で作られた場合に備えて\n            for p in tf.io.gfile.glob(os.path.join(path, \"*\")):\n                tf.io.gfile.remove(p)\n            tf.io.gfile.rmtree(path)\n\ndef zip_artifacts(\n    zip_path: str = \"/kaggle/working/artifacts.zip\",\n    include_paths: list = None,\n    exclude_files: list = None,\n):\n    \"\"\"\n    include_paths: zipに入れたいパス（ファイル or フォルダ）\n    exclude_files: zipに入れたくないファイル名（例: [\"submission.csv\"]）\n    \"\"\"\n    if include_paths is None:\n        include_paths = []\n    if exclude_files is None:\n        exclude_files = []\n\n    zip_path = str(zip_path)\n    if tf.io.gfile.exists(zip_path):\n        tf.io.gfile.remove(zip_path)\n\n    # zip作成（標準ライブラリ）\n    import zipfile\n    with zipfile.ZipFile(zip_path, \"w\", compression=zipfile.ZIP_DEFLATED) as zf:\n        for p in include_paths:\n            if not tf.io.gfile.exists(p):\n                continue\n\n            # フォルダ\n            if tf.io.gfile.isdir(p):\n                for root, dirs, files in os.walk(p):\n                    for fn in files:\n                        if fn in exclude_files:\n                            continue\n                        full = os.path.join(root, fn)\n                        arc = os.path.relpath(full, \"/kaggle/working\")  # zip内の相対パス\n                        zf.write(full, arcname=arc)\n            else:\n                fn = os.path.basename(p)\n                if fn in exclude_files:\n                    continue\n                arc = os.path.relpath(p, \"/kaggle/working\")\n                zf.write(p, arcname=arc)\n\n    size_mb = os.path.getsize(zip_path) / (1024 * 1024)\n    print(f\"artifacts zip saved: {zip_path} ({size_mb:.1f} MB)\")\n    return zip_path\n\n\n# =========================================================\n# 14) メインフロー（元の処理順を維持）\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\n    # ---- device strategy ----\n    strategy = init_strategy()\n\n    # ---- build datasets ----\n    train_ds, val_ds, val_export_ds, test_ds, train_files, val_files = build_datasets(hp)\n\n    train_count = count_items(train_files)\n    val_count   = count_items(val_files)\n\n    steps_per_epoch  = max(1, train_count // hp.batch_size)\n    validation_steps = max(1, val_count   // hp.batch_size)\n\n    print(f\"Samples train={train_count:,}, val={val_count:,}\")\n    print(f\"Steps   train={steps_per_epoch}, val={validation_steps}\")\n\n    # ---- visualize augmentation samples ----\n    visualize_augmentations(\n        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    )\n\n    # ---- build & compile model ----\n    with strategy.scope():\n        model = get_model_by_name(\n            model_name,\n            image_size=hp.image_size,\n            num_classes=hp.num_classes,\n            in_channels=hp.in_channels\n        )\n        model.compile(\n            optimizer=make_optimizer(hp, steps_per_epoch),\n            loss=\"sparse_categorical_crossentropy\",\n            metrics=[\"accuracy\"],\n            jit_compile=hp.jit_compile\n        )\n\n    # ---- training (EpochLogger + callbacks) ----\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  # stepログは出さず、EpochLoggerだけ表示\n    )\n\n    # ---- ベスト重みの読み込み（custom layer 問題を回避）----\n    with strategy.scope():\n        best_model = get_model_by_name(\n            model_name,\n            image_size=hp.image_size,\n            num_classes=hp.num_classes,\n            in_channels=hp.in_channels\n        )\n        best_model.compile(\n            optimizer=make_optimizer(hp, steps_per_epoch),\n            loss=\"sparse_categorical_crossentropy\",\n            metrics=[\"accuracy\"],\n            jit_compile=hp.jit_compile\n        )\n    \n    # ★ 先にモデルを build する（サブクラスModel対策）\n    _ = best_model(tf.zeros([1, hp.IMAGE_SIZE, hp.IMAGE_SIZE, hp.in_channels], dtype=tf.float32), training=False)\n    \n    best_model.load_weights(hp.model_path)\n    print(f\"Loaded best weights from: {hp.model_path}\")\n\n    # 以降は best_model を使う\n    model = best_model\n\n    # ---- val画像を全件エクスポート ----\n    n_export = export_val_images(\n        val_ds=val_export_ds,\n        save_dir=\"/kaggle/working/val_images\",\n        digits=6,\n        skip_if_exists=False  # ← まずは検証のため False 推奨（毎回作り直す）\n    )\n    \n    # 画像ファイル数チェック\n    n_files = len(tf.io.gfile.glob(\"/kaggle/working/val_images/*.png\"))\n    print(\"val_export count:\", n_export, \" files:\", n_files)\n    \n    # ---- 学習ログCSV（EpochLogger.records を保存） ----\n    _ = save_train_log(epoch_logger, hp.train_log_path)\n\n    # ---- 詳細評価CSV ----\n    _ = evaluate_and_export_csv(\n        model=model,\n        val_ds=val_export_ds,\n        hp=hp,\n        csv_path=\"/kaggle/working/val_results.csv\",\n        digits=6\n    )\n\n    # ---- 低信頼度画像の可視化 ----\n    visualize_low_confidence_images(\n        model=model,\n        dataset=val_export_ds, \n        n=16,\n        class_names=None,\n        figsize=(12, 12)\n    )\n\n    #---- 任意：予測の可視化（ランダムに n 枚）----\n    # 勉強・確認用。必要なければコメントアウトOK。\n    visualize_predictions(model, val_export_ds, class_names=None, n=16, figsize=(12,12))\n\n    #---- 任意：誤分類画像の保存（最大 max_images 枚）----\n    # 保存先: /kaggle/working/misclassified\n    saved_cnt = export_misclassified_images(\n        model=model,\n        dataset=val_export_ds,\n        save_dir=\"/kaggle/working/misclassified\",\n        class_names=None,\n        max_images=100\n    )\n    print(f\"saved misclassified images: {saved_cnt}\")\n\n    # ---- 推論 & submission.csv 作成 ----\n    predict_and_submit(model, test_ds, hp)\n\n    # =========================================================\n    # 出力を zip にまとめる（submission.csv 以外）\n    # =========================================================\n\n    include = [\n        \"/kaggle/working/val_images\",                 # フォルダ\n        \"/kaggle/working/misclassified\",              # フォルダ（任意）\n        \"/kaggle/working/augment_samples.png\",        # 画像\n        \"/kaggle/working/train_log.csv\",              # 学習ログ\n        \"/kaggle/working/val_results.csv\",            # val評価\n        HP.model_path,                                # best weights\n    ]\n\n    # 「存在するものだけ」zipに入れるので、無いものがあってもOK\n    _ = zip_artifacts(\n        zip_path=\"/kaggle/working/artifacts.zip\",\n        include_paths=include,\n        exclude_files=[\"submission.csv\"]\n    )\n\n    # 動作確認（KaggleのOutputに出す用）\n    !ls -lh /kaggle/working\n\n    # ---- 学習曲線の表示（任意）----\n    plot_history(history)\n\n    # ---- 終了ログ ----\n    elapsed_min = (time.time() - start) / 60.0\n    print(f\"Done. Elapsed: {elapsed_min:.1f} min\")\n\n    return model, history, epoch_logger\n\n# =========================================================\n# 15) 実行エントリポイント\n# =========================================================\nif __name__ == \"__main__\":\n    model, history, epoch_logger = main(HP, MODEL_NAME)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T06:54:24.489508Z","iopub.execute_input":"2026-03-18T06:54:24.489979Z","execution_failed":"2026-03-18T06:55:17.808Z"}},"outputs":[],"execution_count":null}]}