{"metadata":{"kernelspec":{"name":"python3","display_name":"Python 3","language":"python"},"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":"nvidiaTeslaT4","dataSources":[{"sourceId":102335,"databundleVersionId":12518947,"sourceType":"competition"},{"sourceId":12703912,"sourceType":"datasetVersion","datasetId":8028914},{"sourceId":12720474,"sourceType":"datasetVersion","datasetId":8040013}],"dockerImageVersionId":31090,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\nimport sys\nimport numpy as np\nimport pandas as pd\nimport polars as pl\nfrom tqdm import tqdm\nimport matplotlib.pyplot as plt\nimport kaggle_evaluation.cmi_inference_server\n\nis_kaggle_kernel = os.path.isdir(\"/kaggle/working/\")\nif is_kaggle_kernel:\n    folder_path = \"/kaggle/input/cmi-detect-behavior-with-sensor-data/\"\n    split_data_path = \"/kaggle/input/cmi2025-split-train-data/\"\nelse:\n    folder_path = \"../../../data/cmi-detect-behavior-with-sensor-data/\"\n    split_data_path = folder_path + \"split_train_data/\"\nsys.path.append(folder_path)\nimport kaggle_evaluation.cmi_inference_server\n\ntrain_demo = pd.read_csv(f\"{folder_path}/train_demographics.csv\")\ntrain = pd.read_csv(f\"{split_data_path}/train_1.csv\")\nval_demo = train_demo.copy()\nval = pd.read_csv(f\"{split_data_path}/val_1.csv\")\ntest_demo = pd.read_csv(f\"{folder_path}/test_demographics.csv\")\ntest = pd.read_csv(f\"{folder_path}/test.csv\")","metadata":{"execution":{"iopub.status.busy":"2025-08-09T20:38:09.513133Z","iopub.execute_input":"2025-08-09T20:38:09.513733Z","iopub.status.idle":"2025-08-09T20:38:38.994592Z","shell.execute_reply.started":"2025-08-09T20:38:09.513707Z","shell.execute_reply":"2025-08-09T20:38:38.993684Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nseed = 42\ntorch.manual_seed(seed)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-09T20:38:38.996126Z","iopub.execute_input":"2025-08-09T20:38:38.996418Z","iopub.status.idle":"2025-08-09T20:38:43.110849Z","shell.execute_reply.started":"2025-08-09T20:38:38.996376Z","shell.execute_reply":"2025-08-09T20:38:43.110246Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# DatasetとDataLoaderのインポート\nfrom torch.utils.data import DataLoader\nif is_kaggle_kernel:\n    sys.path.append('/kaggle/input/cmi2025-utils')\nfrom utils.dataset import SubjectDataset, custom_collate_fn\n\ntrain_dataset = SubjectDataset(train_demo, train)\nval_dataset = SubjectDataset(val_demo, val)\ntest_dataset = SubjectDataset(test_demo, test, have_labels=False)","metadata":{"execution":{"iopub.status.busy":"2025-08-09T20:38:43.111540Z","iopub.execute_input":"2025-08-09T20:38:43.111893Z","iopub.status.idle":"2025-08-09T20:38:49.534465Z","shell.execute_reply.started":"2025-08-09T20:38:43.111874Z","shell.execute_reply":"2025-08-09T20:38:49.533610Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch.nn.functional as F\n\nNUM_CLASSES = 18\n\ndef to_one_hot(labels, num_classes=NUM_CLASSES, device='cpu'):\n    # labels: (batch,), int64 tensor\n    return F.one_hot(labels, num_classes=num_classes).float().to(device)\n\ndef one_hot_collate_fn(batch):\n    # custom_collate_fn でテンソルを得る\n    demo_batch, seq_batch, label_batch = custom_collate_fn(batch)\n\n    demo_batch = demo_batch.float()\n    seq_batch = seq_batch.float()\n\n    # label_batch は整数ラベル (LongTensor) と仮定\n    # one-hot に変換\n    label_batch = to_one_hot(label_batch, device=demo_batch.device)\n\n    return demo_batch, seq_batch, label_batch","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-09T20:38:49.535882Z","iopub.execute_input":"2025-08-09T20:38:49.536130Z","iopub.status.idle":"2025-08-09T20:38:49.541092Z","shell.execute_reply.started":"2025-08-09T20:38:49.536111Z","shell.execute_reply":"2025-08-09T20:38:49.540505Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"BATCH_SIZE = 32\nNUM_WORKERS = 0\n\ntrain_loader = DataLoader(\n    train_dataset,\n    batch_size=BATCH_SIZE,\n    shuffle=True,\n    # collate_fn=custom_collate_fn,\n    collate_fn=lambda b: one_hot_collate_fn(b), # <-- 変更点\n    num_workers=NUM_WORKERS\n)\nval_loader = DataLoader(val_dataset, batch_size=BATCH_SIZE, shuffle=False, collate_fn=custom_collate_fn, num_workers=NUM_WORKERS)","metadata":{"execution":{"iopub.status.busy":"2025-08-09T20:38:49.541707Z","iopub.execute_input":"2025-08-09T20:38:49.541913Z","iopub.status.idle":"2025-08-09T20:38:49.556465Z","shell.execute_reply.started":"2025-08-09T20:38:49.541896Z","shell.execute_reply":"2025-08-09T20:38:49.555792Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# モデルのインポート\nfrom utils.models import HybridGestureClassifier","metadata":{"execution":{"iopub.status.busy":"2025-08-09T20:38:49.557101Z","iopub.execute_input":"2025-08-09T20:38:49.557256Z","iopub.status.idle":"2025-08-09T20:38:49.577059Z","shell.execute_reply.started":"2025-08-09T20:38:49.557242Z","shell.execute_reply":"2025-08-09T20:38:49.576474Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# スコアリング関数のインポート\nfrom utils.scoring_metric import score","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-09T20:38:49.577948Z","iopub.execute_input":"2025-08-09T20:38:49.578674Z","iopub.status.idle":"2025-08-09T20:38:50.689604Z","shell.execute_reply.started":"2025-08-09T20:38:49.578652Z","shell.execute_reply":"2025-08-09T20:38:50.688770Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch.optim as optim\nfrom torch import nn\n\nDEMO_INPUT_DIM = train_dataset[0][0].shape[0]\nSEQ_INPUT_CHANNELS = train_dataset[0][1].shape[0]\n\n\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\n# モデルのインスタンスを作成\nmodel = HybridGestureClassifier(\n    num_sequence_features=SEQ_INPUT_CHANNELS,\n    sequence_length=300,\n    num_subject_features=DEMO_INPUT_DIM,\n    num_classes=NUM_CLASSES\n    )\n\nif False:\n    model_save_path = 'best_gesture_classifier.pth' # 訓練ループで保存したファイル名と同じにする\n\n    try:\n        loaded_state_dict = torch.load(model_save_path, map_location=device)\n        model.load_state_dict(loaded_state_dict)\n        model.eval()\n\n    except FileNotFoundError:\n        print(f\"エラー: モデルの重みファイル '{model_save_path}' が見つかりません。\")\n        print(\"ファイルパスが正しいか、訓練が正常に完了し保存されたか確認してください。\")\n    except Exception as e:\n        print(f\"モデルの読み込み中にエラーが発生しました: {e}\")\n\n# 損失関数の定義 (多クラス分類のためCrossEntropyLoss)\n# 出力はロジット、ラベルはsoft one-hotなので\ndef mixup_criterion(preds, soft_targets):\n    log_probs = F.log_softmax(preds, dim=1)\n    loss = -(soft_targets * log_probs).sum(dim=1).mean()\n    return loss\ncriterion = nn.CrossEntropyLoss()\n\n# 最適化アルゴリズムの定義 (Adamオプティマイザ)\noptimizer = optim.Adam(model.parameters(), lr=0.001)\n\n# モデルをGPUに移動（利用可能な場合）\nmodel.to(device)\nprint(device)","metadata":{"execution":{"iopub.status.busy":"2025-08-09T20:38:50.690715Z","iopub.execute_input":"2025-08-09T20:38:50.691246Z","iopub.status.idle":"2025-08-09T20:38:55.305175Z","shell.execute_reply.started":"2025-08-09T20:38:50.691221Z","shell.execute_reply":"2025-08-09T20:38:55.304470Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# モデルの構造図を描画 (オプション)\nif False:\n    from torchview import draw_graph\n\n    model_graph = draw_graph(model, \n                            input_size=[(1, DEMO_INPUT_DIM), (1, SEQ_INPUT_CHANNELS, 300)], \n                            expand_nested=True, # サブモジュールを展開して詳細に表示\n                            hide_module_functions=False, # nn.ReLUなどの関数も表示\n                            save_graph=True, # グラフを保存\n                            filename=\"model_structure_graph\", # 保存ファイル名 (拡張子なし)\n                            directory=\".\") # 保存ディレクトリ\n\n    print(\"モデルの構造図を 'model_structure_graph.png' (または .gv) として保存しました。\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-09T20:38:55.305913Z","iopub.execute_input":"2025-08-09T20:38:55.306372Z","iopub.status.idle":"2025-08-09T20:38:55.310718Z","shell.execute_reply.started":"2025-08-09T20:38:55.306352Z","shell.execute_reply":"2025-08-09T20:38:55.309838Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport matplotlib.pyplot as plt  # グラフ描画用ライブラリ\nfrom tqdm import tqdm  # プログレスバー表示\n\n# モデル、DataLoader、criterion、optimizer、deviceは前のセルで定義済みと仮定\n\n# --- 訓練ループ ---\nNUM_EPOCHS = 200 # 訓練エポック数\n\nprint(\"\\n--- 訓練開始 ---\")\nbest_val_accuracy = 0.0 # 最も良い検証精度を記録\nmodel_save_path = 'best_gesture_classifier.pth' \n\n# 各エポックの履歴を記録するリスト\ntrain_losses = []\nval_losses = []\ntrain_accuracies = []\nval_accuracies = []\n\nfor epoch in range(NUM_EPOCHS):\n    model.train()\n    train_loss = 0\n    train_correct = 0\n    train_total = 0\n\n    for demo_data, seq_data, labels in tqdm(train_loader, desc=f\"Epoch {epoch+1}/{NUM_EPOCHS} Train\"):\n        # NaN/Infの補正\n        if torch.isnan(seq_data).any() or torch.isinf(seq_data).any():\n            seq_data[torch.isnan(seq_data)] = 0.0\n            seq_data[torch.isinf(seq_data)] = 0.0\n\n        demo_data, seq_data, labels = demo_data.to(device), seq_data.to(device), labels.to(device)\n\n        optimizer.zero_grad()\n        outputs = model(demo_data, seq_data)\n        loss = mixup_criterion(outputs, labels) # <-- 変更点\n        loss.backward()\n        optimizer.step()\n\n        train_loss += loss.item()\n        _, predicted = torch.max(outputs.data, 1)\n        true_labels = torch.argmax(labels, dim=1)  # one-hot → class id\n        train_total += labels.size(0)\n        train_correct += (predicted == true_labels).sum().item()\n\n    avg_train_loss = train_loss / len(train_loader)\n    train_accuracy = train_correct / train_total\n\n    # 検証フェーズ\n    model.eval()\n    val_loss = 0\n    val_correct = 0\n    val_total = 0\n    with torch.no_grad():\n        for demo_data, seq_data, labels in tqdm(val_loader, desc=f\"Epoch {epoch+1}/{NUM_EPOCHS} Val\"):\n            if torch.isnan(seq_data).any() or torch.isinf(seq_data).any():\n                seq_data[torch.isnan(seq_data)] = 0.0\n                seq_data[torch.isinf(seq_data)] = 0.0\n\n            demo_data, seq_data, labels = demo_data.to(device), seq_data.to(device), labels.to(device)\n\n            outputs = model(demo_data, seq_data)\n            loss = criterion(outputs, labels) # # <-- こっちはそのままでOK\n\n            val_loss += loss.item()\n            _, predicted = torch.max(outputs.data, 1)\n            val_total += labels.size(0)\n            val_correct += (predicted == labels).sum().item()\n\n    avg_val_loss = val_loss / len(val_loader)\n    val_accuracy = val_correct / val_total\n\n    print(f\"Epoch [{epoch+1}/{NUM_EPOCHS}], \"\n          f\"Train Loss: {avg_train_loss:.4f}, Train Acc: {train_accuracy:.4f} | \"\n          f\"Val Loss: {avg_val_loss:.4f}, Val Acc: {val_accuracy:.4f}\")\n\n    # グラフ用に記録\n    train_losses.append(avg_train_loss)\n    val_losses.append(avg_val_loss)\n    train_accuracies.append(train_accuracy)\n    val_accuracies.append(val_accuracy)\n\n    if val_accuracy > best_val_accuracy:\n        best_val_accuracy = val_accuracy\n        torch.save(model.state_dict(), model_save_path)\n        print(f\"  --> Best model saved with Val Acc: {best_val_accuracy:.4f}\")\n\nprint(\"--- 訓練終了 ---\")\n\n# --- グラフ描画 ---\nepochs = list(range(1, NUM_EPOCHS + 1))\n\nplt.figure(figsize=(12, 5))\n\n# 損失\nplt.subplot(1, 2, 1)\nplt.plot(epochs, train_losses, label='Train Loss')\nplt.plot(epochs, val_losses, label='Val Loss')\nplt.xlabel('Epoch')\nplt.ylabel('Loss')\nplt.title('Loss over Epochs')\nplt.legend()\n\n# 精度\nplt.subplot(1, 2, 2)\nplt.plot(epochs, train_accuracies, label='Train Acc')\nplt.plot(epochs, val_accuracies, label='Val Acc')\nplt.xlabel('Epoch')\nplt.ylabel('Accuracy')\nplt.title('Accuracy over Epochs')\nplt.legend()\n\nplt.tight_layout()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-09T20:38:55.312999Z","iopub.execute_input":"2025-08-09T20:38:55.313281Z","iopub.status.idle":"2025-08-09T20:39:11.407057Z","shell.execute_reply.started":"2025-08-09T20:38:55.313264Z","shell.execute_reply":"2025-08-09T20:39:11.406266Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Scoring","metadata":{}},{"cell_type":"code","source":"try:\n    loaded_state_dict = torch.load(model_save_path, map_location=device)\n    model.load_state_dict(loaded_state_dict)\n    model.eval()\n    print(f\"モデルの重みを '{model_save_path}' から読み込みました。\")\n\nexcept FileNotFoundError:\n    print(f\"エラー: モデルの重みファイル '{model_save_path}' が見つかりません。\")\n    print(\"ファイルパスが正しいか、訓練が正常に完了し保存されたか確認してください。\")\nexcept Exception as e:\n    print(f\"モデルの読み込み中にエラーが発生しました: {e}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-09T18:06:51.522351Z","iopub.execute_input":"2025-08-09T18:06:51.522897Z","iopub.status.idle":"2025-08-09T18:06:51.556124Z","shell.execute_reply.started":"2025-08-09T18:06:51.522873Z","shell.execute_reply":"2025-08-09T18:06:51.555413Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(val_dataset.gesture_to_id)\nid_to_gesture = {v: k for k, v in val_dataset.gesture_to_id.items()}\nprint(id_to_gesture)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-09T18:06:54.422376Z","iopub.execute_input":"2025-08-09T18:06:54.422657Z","iopub.status.idle":"2025-08-09T18:06:54.427764Z","shell.execute_reply.started":"2025-08-09T18:06:54.42263Z","shell.execute_reply":"2025-08-09T18:06:54.426918Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model.eval() # モデルを評価モードに設定 (Dropoutなどが無効になる)\nval_loss = 0\nval_correct = 0\nval_total = 0\nall_predictions = []\nall_labels = []\nwith torch.no_grad(): # 勾配計算を無効化\n    for demo_data, seq_data, labels in val_loader:\n        if torch.isnan(seq_data).any() or torch.isinf(seq_data).any():\n            seq_data[torch.isnan(seq_data)] = 0.0 # NaNを0で埋める\n            seq_data[torch.isinf(seq_data)] = 0.0 # Infを0で埋める\n            \n        demo_data, seq_data, labels = demo_data.to(device), seq_data.to(device), labels.to(device)\n\n        outputs = model(demo_data, seq_data)\n        loss = criterion(outputs, labels)\n        all_predictions.extend(outputs.argmax(dim=1).cpu().numpy())\n        all_labels.extend(labels.cpu().numpy())\n        val_loss += loss.item()\n        _, predicted = torch.max(outputs.data, 1)\n        val_total += labels.size(0)\n        val_correct += (predicted == labels).sum().item()\n\navg_val_loss = val_loss / len(val_loader)\nval_accuracy = val_correct / val_total\n\nsubmission = pd.DataFrame({\"gesture\": [id_to_gesture[pred] for pred in all_predictions]})\nsolution = pd.DataFrame({\"gesture\": [id_to_gesture[label] for label in all_labels]})\nsubmission[\"id\"] = submission.index\nsolution[\"id\"] = solution.index\ncompetition_score = score(solution, submission, row_id_column_name=\"id\")\n\nprint(f\"Val Loss: {avg_val_loss:.4f}, Val Acc: {val_accuracy:.4f}\")\nprint(f\"Score: {competition_score:.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-09T18:06:57.188933Z","iopub.execute_input":"2025-08-09T18:06:57.189309Z","iopub.status.idle":"2025-08-09T18:06:58.680701Z","shell.execute_reply.started":"2025-08-09T18:06:57.189277Z","shell.execute_reply":"2025-08-09T18:06:58.679951Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# submit prediction","metadata":{}},{"cell_type":"code","source":"import torch.nn.functional as F\n\ndef predict(seq, demo):\n    if type(seq) != pd.DataFrame:\n        seq = seq.to_pandas()\n        demo = demo.to_pandas()\n\n    test_ds = SubjectDataset(demo, seq, have_labels=False)\n    test_loader = DataLoader(test_ds, batch_size=BATCH_SIZE, shuffle=False, collate_fn=custom_collate_fn, num_workers=NUM_WORKERS)\n    _, demo_data, seq_data = next(iter(test_loader))\n    if torch.isnan(seq_data).any() or torch.isinf(seq_data).any():\n        seq_data[torch.isnan(seq_data)] = 0.0 # NaNを0で埋める\n        seq_data[torch.isinf(seq_data)] = 0.0 # Infを0で埋める\n\n    demo_data, seq_data = demo_data.to(device), seq_data.to(device)\n    outputs = model(demo_data, seq_data, )\n\n    # 1. ロジットをSoftmaxにかけて確率に変換\n    # dim=1 はクラスの次元を指定\n    probabilities = F.softmax(outputs, dim=1)\n    \n    # 2. 最も確率の高いクラスのインデックス（ID）を取得\n    # .argmax(dim=1) で各サンプルの最大値のインデックスを取得\n    # .item() でテンソルからPythonのスカラ値に変換\n    predicted_id = torch.argmax(probabilities, dim=1).item()\n    \n    # 3. 逆引き辞書を使って文字列に変換\n    predicted_gesture = id_to_gesture[predicted_id]\n\n    return predicted_gesture","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-09T18:07:02.334587Z","iopub.execute_input":"2025-08-09T18:07:02.334883Z","iopub.status.idle":"2025-08-09T18:07:02.341263Z","shell.execute_reply.started":"2025-08-09T18:07:02.334861Z","shell.execute_reply":"2025-08-09T18:07:02.340441Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\n\ninference_server = kaggle_evaluation.cmi_inference_server.CMIInferenceServer(predict)\n\nif os.getenv('KAGGLE_IS_COMPETITION_RERUN'):\n    inference_server.serve()\nelse:\n    inference_server.run_local_gateway(\n        data_paths=(\n            '/kaggle/input/cmi-detect-behavior-with-sensor-data/test.csv',\n            '/kaggle/input/cmi-detect-behavior-with-sensor-data/test_demographics.csv',\n        )\n    )","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-09T18:07:10.139936Z","iopub.execute_input":"2025-08-09T18:07:10.140532Z","iopub.status.idle":"2025-08-09T18:07:11.897564Z","shell.execute_reply.started":"2025-08-09T18:07:10.140507Z","shell.execute_reply":"2025-08-09T18:07:11.896964Z"}},"outputs":[],"execution_count":null}]}