{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.11","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":5441,"databundleVersionId":38425,"sourceType":"competition"}],"dockerImageVersionId":31041,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"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,"execution":{"iopub.status.busy":"2025-07-06T12:13:43.531032Z","iopub.execute_input":"2025-07-06T12:13:43.531702Z","iopub.status.idle":"2025-07-06T12:13:43.539019Z","shell.execute_reply.started":"2025-07-06T12:13:43.531677Z","shell.execute_reply":"2025-07-06T12:13:43.538434Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## zipファイルの解凍と画像の振り分け","metadata":{}},{"cell_type":"code","source":"import os, zipfile, shutil, pathlib\n\n# 解凍先ディレクトリ\nWORK_DIR = '/kaggle/working'\n\n# train.zip の解凍\nwith zipfile.ZipFile('/kaggle/input/dogs-vs-cats-redux-kernels-edition/train.zip', 'r') as zip_ref:\n    zip_ref.extractall(f'{WORK_DIR}')\n\n# 犬・猫を分類するためのフォルダ作成\nbase_dir = pathlib.Path(f'{WORK_DIR}/dataset')\ncat_dir = base_dir / 'cats'\ndog_dir = base_dir / 'dogs'\ncat_dir.mkdir(parents=True, exist_ok=True)\ndog_dir.mkdir(parents=True, exist_ok=True)\n\n# trainフォルダの画像をcats/dogsに仕分け\ntrain_dir = pathlib.Path(f'{WORK_DIR}/train')\nfor file in train_dir.iterdir():\n    if 'cat' in file.name:\n        shutil.move(str(file), cat_dir / file.name)\n    elif 'dog' in file.name:\n        shutil.move(str(file), dog_dir / file.name)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-06T12:13:43.540275Z","iopub.execute_input":"2025-07-06T12:13:43.540506Z","iopub.status.idle":"2025-07-06T12:13:53.299372Z","shell.execute_reply.started":"2025-07-06T12:13:43.540490Z","shell.execute_reply":"2025-07-06T12:13:53.298617Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## データの読み込み","metadata":{}},{"cell_type":"code","source":"from tensorflow.keras.preprocessing.image import ImageDataGenerator\nfrom tensorflow.keras.applications.convnext import preprocess_input\n\n# 画像サイズなどの設定\nIMG_SIZE = (224, 224)\nBATCH_SIZE = 32\n\n# データ拡張＆正規化（validation_splitで分割）\ndatagen = ImageDataGenerator(\n    preprocessing_function=preprocess_input,\n    validation_split=0.2,  # 20%を検証用\n    horizontal_flip=True,\n    rotation_range=15,\n    zoom_range=0.2\n)\n\ntrain_gen = datagen.flow_from_directory(\n    directory='/kaggle/working/dataset',\n    target_size=IMG_SIZE,\n    batch_size=BATCH_SIZE,\n    class_mode='binary',\n    subset='training'\n)\n\nval_gen = datagen.flow_from_directory(\n    directory='/kaggle/working/dataset',\n    target_size=IMG_SIZE,\n    batch_size=BATCH_SIZE,\n    class_mode='binary',\n    subset='validation'\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-06T12:13:53.300234Z","iopub.execute_input":"2025-07-06T12:13:53.300528Z","iopub.status.idle":"2025-07-06T12:13:53.620338Z","shell.execute_reply.started":"2025-07-06T12:13:53.300501Z","shell.execute_reply":"2025-07-06T12:13:53.619810Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## ConvNeXt-Small の構築","metadata":{}},{"cell_type":"code","source":"from tensorflow.keras.applications import ConvNeXtSmall\nfrom tensorflow.keras import layers, models, optimizers\n\nbase_model = ConvNeXtSmall(\n    weights='imagenet',\n    include_top=False,\n    input_shape=(224, 224, 3)\n)\nbase_model.trainable = False  # 転移学習初期は凍結\n\nmodel = models.Sequential([\n    base_model,\n    layers.GlobalAveragePooling2D(),\n    layers.Dropout(0.3),\n    layers.Dense(1, activation='sigmoid')\n])\n\nmodel.compile(\n    optimizer=optimizers.Adam(learning_rate=1e-4),\n    loss='binary_crossentropy',\n    metrics=['accuracy']\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-06T12:13:53.621164Z","iopub.execute_input":"2025-07-06T12:13:53.621463Z","iopub.status.idle":"2025-07-06T12:14:02.226235Z","shell.execute_reply.started":"2025-07-06T12:13:53.621438Z","shell.execute_reply":"2025-07-06T12:14:02.225681Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## モデルの学習","metadata":{}},{"cell_type":"code","source":"from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint\n\ncallbacks = [\n    EarlyStopping(patience=3, restore_best_weights=True),\n    ModelCheckpoint('convnext_small_best.h5', save_best_only=True)\n]\n\nhistory = model.fit(\n    train_gen,\n    validation_data=val_gen,\n    epochs=10,\n    callbacks=callbacks\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-06T12:14:02.228140Z","iopub.execute_input":"2025-07-06T12:14:02.228373Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## グラフ描画","metadata":{}},{"cell_type":"code","source":"import matplotlib.pyplot as plt\n\n# 総エポック数\ntotal_epochs = len(history.history['loss'])\nif 'history_ft' in locals():\n    total_epochs += len(history_ft.history['loss'])\n\n# グラフ描画（2画面）\nplt.figure(figsize=(12, 5))\n\n# ========== 1. lossグラフ ==========\nplt.subplot(1, 2, 1)\n\n# 前半の学習（凍結中）\nplt.plot(range(1, len(history.history['loss']) + 1), history.history['loss'], '-o')\nplt.plot(range(1, len(history.history['val_loss']) + 1), history.history['val_loss'], '-o')\n\n# ========== 2. accuracyグラフ ==========\nplt.subplot(1, 2, 2)\n\nplt.plot(range(1, len(history.history['accuracy']) + 1), history.history['accuracy'], '-o')\nplt.plot(range(1, len(history.history['val_accuracy']) + 1), history.history['val_accuracy'], '-o')\n\nif 'history_ft' in locals():\n    start = len(history.history['accuracy']) + 1\n    plt.plot(range(start, start + len(history_ft.history['accuracy'])), history_ft.history['accuracy'], '-o')\n    plt.plot(range(start, start + len(history_ft.history['val_accuracy'])), history_ft.history['val_accuracy'], '-o')\n\nplt.title('Accuracy Transition')\nplt.ylabel('Accuracy')\nplt.xlabel('Epoch')\nplt.grid()\nplt.legend(['train_acc_1', 'val_acc_1', 'train_acc_2', 'val_acc_2'] if 'history_ft' in locals() else ['accuracy', 'val_accuracy'], loc='best')\n\n# 表示\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Grad-CAMの実装と誤分類画像の分析","metadata":{}},{"cell_type":"code","source":"import tensorflow as tf\nimport numpy as np\n\ndef make_gradcam_heatmap(img_array, model, last_conv_layer_name):\n    grad_model = tf.keras.models.Model(\n        [model.input],\n        [model.get_layer(last_conv_layer_name).output, model.output]\n    )\n    with tf.GradientTape() as tape:\n        conv_outputs, predictions = grad_model(img_array)\n        loss = predictions[:, 0]  # 2値分類の1クラス目の出力を使う\n\n    grads = tape.gradient(loss, conv_outputs)\n    pooled_grads = tf.reduce_mean(grads, axis=(0,1,2))\n\n    conv_outputs = conv_outputs[0]\n    heatmap = conv_outputs @ pooled_grads[..., tf.newaxis]\n    heatmap = tf.squeeze(heatmap)\n    heatmap = tf.maximum(heatmap, 0) / tf.math.reduce_max(heatmap)\n    return heatmap.numpy()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 予測結果取得\npreds = model.predict(val_gen)\npred_labels = (preds > 0.5).astype(int).flatten()\n\n# 正解ラベル、ファイル名\ntrue_labels = val_gen.classes\nfilenames = val_gen.filenames\n\n# 誤分類画像リスト\nmisclassified = []\nfor i in range(len(filenames)):\n    if pred_labels[i] != true_labels[i]:\n        misclassified.append((filenames[i], true_labels[i], pred_labels[i]))","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import cv2\nimport numpy as np\nimport matplotlib.pyplot as plt\nfrom tensorflow.keras.preprocessing import image\nfrom tensorflow.keras.applications.convnext import preprocess_input\nimport os\n\n# Grad-CAM heatmap を生成する関数\ndef make_gradcam_heatmap(img_array, model, last_conv_layer_name):\n    grad_model = tf.keras.models.Model(\n        [model.input],  # convnext_smallの入力\n        [model.get_layer(last_conv_layer_name).output, model.output]\n    )\n\n    with tf.GradientTape() as tape:\n        conv_outputs, predictions = grad_model(img_array)\n        loss = predictions[:, 0]\n\n    grads = tape.gradient(loss, conv_outputs)[0]  # 勾配\n    conv_outputs = conv_outputs[0]  # 出力特徴マップ\n\n    # 勾配をグローバル平均プーリング\n    weights = tf.reduce_mean(grads, axis=(0, 1))\n\n    # 重み付き合計\n    cam = np.zeros(conv_outputs.shape[:2], dtype=np.float32)\n    for i, w in enumerate(weights):\n        cam += w * conv_outputs[:, :, i]\n\n    # ReLU適用＆正規化\n    cam = np.maximum(cam, 0)\n    cam /= (np.max(cam) + 1e-8)\n    return cam\n\n# 前処理関数（画像読み込みとConvNeXt用の整形）\ndef load_and_preprocess_image(img_path):\n    img = image.load_img(img_path, target_size=(224, 224))\n    img_array = image.img_to_array(img)\n    return np.expand_dims(preprocess_input(img_array), axis=0), img\n\n# Grad-CAM 実行\nresults = []\n\n# convnext_small を model から取得\nconvnext_model = model.get_layer('convnext_small')\n\n# 最後のConv層名\nlast_conv_layer_name = \"convnext_small_stage_3_block_2_depthwise_conv\"\n\n# 可視化実行（最初の10件）\nfor img_rel_path, true, pred in misclassified[:10]:\n    img_path = os.path.join(val_gen.directory, img_rel_path)\n    img_array, original_img = load_and_preprocess_image(img_path)\n\n    heatmap = make_gradcam_heatmap(img_array, convnext_model, last_conv_layer_name)\n    heatmap = cv2.resize(heatmap, (224, 224))\n    heatmap = cv2.applyColorMap(np.uint8(255 * heatmap), cv2.COLORMAP_JET)\n\n    original = cv2.cvtColor(np.array(original_img), cv2.COLOR_RGB2BGR)\n    superimposed = cv2.addWeighted(original, 0.6, heatmap, 0.4, 0)\n\n    results.append((superimposed, img_rel_path, true, pred))","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"plt.figure(figsize=(12, 5 * ((len(results)+1)//2)))\nfor i, (img, path, true, pred) in enumerate(results):\n    plt.subplot((len(results)+1)//2, 2, i+1)\n    plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))\n    plt.title(f\"True:{true} Pred:{pred}\\n{os.path.basename(path)}\")\n    plt.axis('off')\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 学習済みモデルを保存\nmodel.save(\"/kaggle/working/best_model.h5\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 誤分類画像の分析結果から、背景ノイズに強くする","metadata":{}},{"cell_type":"code","source":"!pip install -q albumentations","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import albumentations as A\nfrom albumentations.pytorch import ToTensorV2\nimport cv2\n\nIMG_SIZE = (224, 224)\nheight, width = IMG_SIZE\n\ntrain_transform = A.Compose([\n    A.RandomResizedCrop(size=(224, 224), scale=(0.8, 1.0)), \n    A.HorizontalFlip(p=0.5),\n    A.ShiftScaleRotate(shift_limit=0.1, scale_limit=0.1, rotate_limit=30, p=0.5),\n    A.RandomBrightnessContrast(p=0.3),\n    A.OneOf([\n        A.GaussianBlur(p=0.3),\n        A.GaussNoise(p=0.3),\n        A.ISONoise(p=0.3),\n    ], p=0.3),\n    A.CoarseDropout(max_holes=1, max_height=40, max_width=40, p=0.5),\n    A.Normalize()\n])","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from tensorflow.keras.utils import Sequence\nimport numpy as np\nimport os\nfrom PIL import Image\n\nclass AlbumentationsDataGenerator(Sequence):\n    def __init__(self, image_paths, labels, batch_size, transform, shuffle=True):\n        self.image_paths = image_paths\n        self.labels = labels\n        self.batch_size = batch_size\n        self.transform = transform\n        self.shuffle = shuffle\n        self.on_epoch_end()\n    \n    def __len__(self):\n        return int(np.ceil(len(self.image_paths) / self.batch_size))\n\n    def on_epoch_end(self):\n        self.indices = np.arange(len(self.image_paths))\n        if self.shuffle:\n            np.random.shuffle(self.indices)\n\n    def __getitem__(self, idx):\n        batch_indices = self.indices[idx * self.batch_size:(idx + 1) * self.batch_size]\n        batch_images = []\n        batch_labels = []\n\n        for i in batch_indices:\n            img = np.array(Image.open(self.image_paths[i]).convert(\"RGB\"))\n            img = self.transform(image=img)['image']\n            batch_images.append(img)\n            batch_labels.append(self.labels[i])\n\n        return np.stack(batch_images), np.array(batch_labels)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from glob import glob\n\ncat_images = glob('/kaggle/working/dataset/cats/*.jpg')\ndog_images = glob('/kaggle/working/dataset/dogs/*.jpg')\n\nimage_paths = cat_images + dog_images\nlabels = [0] * len(cat_images) + [1] * len(dog_images)\n\n# データ分割\nfrom sklearn.model_selection import train_test_split\n\ntrain_paths, val_paths, train_labels, val_labels = train_test_split(\n    image_paths, labels, test_size=0.2, stratify=labels, random_state=42\n)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_gen = AlbumentationsDataGenerator(\n    image_paths=train_paths,\n    labels=train_labels,\n    batch_size=32,\n    transform=train_transform\n)\n\nval_transform = A.Compose([\n    A.Resize(height=height, width=width),\n    A.Normalize()\n])\n\nval_gen = AlbumentationsDataGenerator(\n    image_paths=val_paths,\n    labels=val_labels,\n    batch_size=32,\n    transform=val_transform,\n    shuffle=False\n)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"callbacks = [\n    EarlyStopping(patience=3, restore_best_weights=True),\n    ModelCheckpoint('convnext_small_best.h5', save_best_only=True)\n]\n\nhistory = model.fit(\n    train_gen,\n    validation_data=val_gen,\n    epochs=10,\n    callbacks=callbacks\n)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\n\nepochs = len(history.history['loss'])\n\nplt.figure(figsize=(10, 4))\n\nplt.subplot(1, 2, 1)\nplt.plot(range(1, epochs + 1), history.history['loss'], label='Train Loss')\nplt.plot(range(1, epochs + 1), history.history['val_loss'], label='Val Loss')\nplt.xlabel('Epoch'); plt.ylabel('Loss')\nplt.title('Loss over Epochs')\nplt.legend(); plt.grid()\n\nplt.subplot(1, 2, 2)\nplt.plot(range(1, epochs + 1), history.history['accuracy'], label='Train Acc')\nplt.plot(range(1, epochs + 1), history.history['val_accuracy'], label='Val Acc')\nplt.xlabel('Epoch'); plt.ylabel('Accuracy')\nplt.title('Accuracy over Epochs')\nplt.legend(); plt.grid()\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## テスト画像の読み込みと推論","metadata":{}},{"cell_type":"code","source":"# test.zip の解凍\nwith zipfile.ZipFile('/kaggle/input/dogs-vs-cats-redux-kernels-edition/test.zip', 'r') as zip_ref:\n    zip_ref.extractall('/kaggle/working/test')\n\n# ImageDataGeneratorでテスト画像を読み込み\ntest_gen = ImageDataGenerator(preprocessing_function=preprocess_input).flow_from_directory(\n    directory='/kaggle/working',\n    classes=['test'],\n    target_size=IMG_SIZE,\n    batch_size=1,\n    shuffle=False,\n    class_mode=None\n)\n\n# 推論（0〜1の確率）\npreds = model.predict(test_gen, verbose=1)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 提出用ファイル作成","metadata":{}},{"cell_type":"code","source":"import pandas as pd\n\n# sampleSubmission.csv 読み込み\nsubmission = pd.read_csv('/kaggle/input/dogs-vs-cats-redux-kernels-edition/sample_submission.csv')\n\n# 推論結果を代入\nsubmission['label'] = preds\nsubmission.to_csv('submission.csv', index=False)\n\n# 提出ファイルの読み込み\nsubmission = pd.read_csv('submission.csv')\n\n# 表示\nsubmission.head(10)","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}