{"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":"none","dataSources":[{"sourceId":102335,"databundleVersionId":12518947,"isSourceIdPinned":false,"sourceType":"competition"},{"sourceId":248861620,"sourceType":"kernelVersion"}],"dockerImageVersionId":31040,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nfrom tqdm import tqdm\n\n\nfolder_path = \"/kaggle/input/cmi-detect-behavior-with-sensor-data\"\n\ntrain_demo = pd.read_csv(f\"{folder_path}/train_demographics.csv\")\ntrain = pd.read_csv(f\"{folder_path}/train.csv\")\ntest_demo = pd.read_csv(f\"{folder_path}/test_demographics.csv\")\ntest = pd.read_csv(f\"{folder_path}/test.csv\")","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-07-05T11:12:41.934585Z","iopub.execute_input":"2025-07-05T11:12:41.935365Z","iopub.status.idle":"2025-07-05T11:13:19.387979Z","shell.execute_reply.started":"2025-07-05T11:12:41.935330Z","shell.execute_reply":"2025-07-05T11:13:19.387198Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nfrom torch.utils.data import Dataset, DataLoader\n\nclass SubjectDataset(Dataset):\n    def __init__(self, data_demo, data, have_labels=True):\n        self.have_labels = have_labels\n        self.unique_seequence_ids = sorted(data[\"sequence_id\"].unique())\n        self.sequence_id_to_subject = data.groupby(\"sequence_id\")[\"subject\"].first().to_dict()\n        \n        numerical_demo_cols = data_demo.select_dtypes(include=np.number).columns.tolist()\n        if 'subject' in numerical_demo_cols:\n            numerical_demo_cols.remove('subject')\n        \n        self.subject_to_demographics = {\n            subject: torch.tensor(row[numerical_demo_cols].values.astype(np.float32))\n            for subject, row in data_demo.set_index('subject').iterrows()\n        }\n\n        self.labels = None\n        \n        if have_labels:\n            all_unique_gestures = sorted(data[\"gesture\"].unique())\n            self.gesture_to_id = {gesture: i for i, gesture in enumerate(all_unique_gestures)}\n            sequence_labels_map_str = data.groupby(\"sequence_id\")[\"gesture\"].first().to_dict()\n            self.labels = torch.tensor(\n                [self.gesture_to_id[sequence_labels_map_str[sid]] for sid in self.unique_seequence_ids], \n                dtype=torch.long\n            )\n\n        cols_to_drop = [\"row_id\", \"subject\", \"sequence_id\", \"sequence_type\", \"orientation\", \"behavior\", \"phase\", \"gesture\"]\n        sequence_feature_cols = [col for col in data.columns if col not in cols_to_drop]\n        \n        self.preprocessed_sequences = {\n            seq_id: torch.tensor(\n                data[data[\"sequence_id\"] == seq_id][sequence_feature_cols].values.astype(np.float32)\n            ).T\n            for seq_id in tqdm(self.unique_seequence_ids)\n        }\n\n    def __len__(self):\n        return len(self.unique_seequence_ids)\n\n    def __getitem__(self, idx):\n        sequence_id = self.unique_seequence_ids[idx]\n        \n        subject_data_tensor = self.subject_to_demographics[self.sequence_id_to_subject[sequence_id]]\n        sequence_data_tensor = self.preprocessed_sequences[sequence_id]\n\n        if self.have_labels:\n            return subject_data_tensor, sequence_data_tensor, self.labels[idx]\n        else:\n            return sequence_id, subject_data_tensor, sequence_data_tensor","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-05T11:13:19.389264Z","iopub.execute_input":"2025-07-05T11:13:19.389620Z","iopub.status.idle":"2025-07-05T11:13:24.019120Z","shell.execute_reply.started":"2025-07-05T11:13:19.389587Z","shell.execute_reply":"2025-07-05T11:13:24.018210Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"np.shape(train)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-05T11:13:24.020217Z","iopub.execute_input":"2025-07-05T11:13:24.020728Z","iopub.status.idle":"2025-07-05T11:13:24.027409Z","shell.execute_reply.started":"2025-07-05T11:13:24.020705Z","shell.execute_reply":"2025-07-05T11:13:24.026569Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"val_dataset = SubjectDataset(train_demo, train[:10000])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-05T11:13:24.029191Z","iopub.execute_input":"2025-07-05T11:13:24.029440Z","iopub.status.idle":"2025-07-05T11:13:24.469340Z","shell.execute_reply.started":"2025-07-05T11:13:24.029411Z","shell.execute_reply":"2025-07-05T11:13:24.468594Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport torch.nn.functional as F # F.pad のために必要\n\n# --- custom_collate_fn の定義 ---\ndef custom_collate_fn(batch):\n    # バッチ内の最初の要素の長さで、ラベルがあるか（3つ要素）ないか（2つ要素）を判別\n    has_labels = type(batch[0][0]) != str\n    if has_labels:\n        # ラベルがある場合、(デモデータ, シーケンスデータ, ラベル) のタプルを受け取る\n        demo_list, sequence_list, label_list = zip(*batch)\n    else:\n        # ラベルがない場合、(sequence_id, デモデータ, シーケンスデータ) のタプルを受け取る\n        sequence_id_list, demo_list, sequence_list = zip(*batch)\n\n    # 1. デモグラフィックデータは固定長と仮定し、そのままスタックしてバッチを作成\n    demographics_batch = torch.stack(demo_list)\n\n    # 2. シーケンスデータのパディング\n    # sequence_listは既に(C, L)のテンソルリストになっていることを前提\n    \n    # バッチ内で最も長いシーケンスの長さを取得\n    max_len = max(s.shape[1] for s in sequence_list) # s.shape[1] がシーケンス長 (L)\n\n    # 各シーケンスを最長シーケンスの長さにパディング\n    padded_sequences = []\n    for s in sequence_list:\n        # s.shape[1]は現在のシーケンス長、max_lenはバッチ内の最長シーケンス長\n        padding_needed = max_len - s.shape[1] \n        if padding_needed > 0:\n            # torch.nn.functional.pad を使って (C, L_new) にパディング\n            # pad=(0, padding_needed) は「最後の次元（ここではL）の右側にpadding_neededだけゼロを追加」\n            padded_s = F.pad(s, (0, padding_needed))\n        else:\n            padded_s = s # パディングが不要な場合はそのまま\n        padded_sequences.append(padded_s)\n    \n    # パディングされたテンソルを結合し、1つのバッチテンソルにする\n    sequences_batch = torch.stack(padded_sequences)\n\n\n    # 3. ラベルの処理\n    if has_labels:\n        # ラベルは全て同じ形状なので、そのままスタック\n        labels_batch = torch.stack(label_list)\n        return demographics_batch, sequences_batch, labels_batch\n    else:\n        # ラベルがない場合、sequence_idもバッチとして返す\n        # sequence_id_listは文字列なのでstackできないことに注意（リストのまま返す）\n        return sequence_id_list, demographics_batch, sequences_batch","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-05T11:13:24.470155Z","iopub.execute_input":"2025-07-05T11:13:24.470490Z","iopub.status.idle":"2025-07-05T11:13:24.478102Z","shell.execute_reply.started":"2025-07-05T11:13:24.470467Z","shell.execute_reply":"2025-07-05T11:13:24.477269Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"BATCH_SIZE = 32\nNUM_WORKERS = 0\n\nval_loader = DataLoader(val_dataset, batch_size=BATCH_SIZE, shuffle=False, collate_fn=custom_collate_fn, num_workers=NUM_WORKERS)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-05T11:13:24.478917Z","iopub.execute_input":"2025-07-05T11:13:24.479193Z","iopub.status.idle":"2025-07-05T11:13:24.504165Z","shell.execute_reply.started":"2025-07-05T11:13:24.479173Z","shell.execute_reply":"2025-07-05T11:13:24.503079Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch.nn as nn\nimport torch.nn.functional as F\n\nclass HybridGestureClassifier(nn.Module):\n    def __init__(self, num_sequence_features: int, sequence_length: int, num_subject_features: int, num_classes: int = 18):\n        super().__init__()\n\n        # CNNブロックをさらに深く、広くする\n        self.conv_block = nn.Sequential(\n            nn.Conv1d(in_channels=num_sequence_features, out_channels=128, kernel_size=3, padding=1),\n            nn.ReLU(),\n            nn.BatchNorm1d(128), # BatchNormを追加\n            nn.MaxPool1d(kernel_size=2),\n\n            nn.Conv1d(in_channels=128, out_channels=256, kernel_size=3, padding=1),\n            nn.ReLU(),\n            nn.BatchNorm1d(256), # BatchNormを追加\n            nn.MaxPool1d(kernel_size=2),\n\n            nn.Conv1d(in_channels=256, out_channels=512, kernel_size=3, padding=1), # 新しい層: 256 -> 512\n            nn.ReLU(),\n            nn.BatchNorm1d(512), # BatchNormを追加\n            nn.MaxPool1d(kernel_size=2) # 最終的なシーケンス長は元の1/8になる\n        )\n        \n        # MaxPool1dが3回になるので、シーケンス長は元の1/8になる\n        self.cnn_output_length = sequence_length // 8 \n        self.lstm_input_size = 512 # CNNの最終出力チャンネル数に合わせる\n\n        # LSTMをさらに深く、広くする\n        self.lstm = nn.LSTM(\n            input_size=self.lstm_input_size, \n            hidden_size=512, # 隠れ層のサイズをさらに増やす: 256 -> 512\n            num_layers=4,    # 層数をさらに増やす: 3 -> 4\n            batch_first=True, \n            dropout=0.4      # ドロップアウト率も調整\n        )\n        \n        # 被験者データ処理器を深くする\n        self.subject_fc = nn.Sequential(\n            nn.Linear(num_subject_features, 128), \n            nn.ReLU(),\n            nn.Dropout(0.3),\n            nn.Linear(128, 256), # 新しい層: 128 -> 256\n            nn.ReLU(),\n            nn.Dropout(0.3)\n        )\n\n        # 結合される特徴量の次元が増える\n        combined_feature_dim = 512 + 256 # LSTMのhidden_size + subject_fcの出力 \n        \n        # 分類器を深く、広くする\n        self.classifier = nn.Sequential(\n            nn.Linear(combined_feature_dim, 512), # 入力を増やす\n            nn.ReLU(),\n            nn.Dropout(0.5), # ドロップアウト率を調整\n            nn.Linear(512, 256), # 新しい層: 512 -> 256\n            nn.ReLU(),\n            nn.Dropout(0.5), # ドロップアウト率を調整\n            nn.Linear(256, num_classes) # 最終出力\n        )\n\n    def forward(self, subject_data, sequence_data):\n        cnn_out = self.conv_block(sequence_data)\n        lstm_input = cnn_out.transpose(1, 2) \n        lstm_out, (h_n, c_n) = self.lstm(lstm_input)\n        sequence_features = h_n[-1] \n\n        subject_features = self.subject_fc(subject_data)\n\n        combined_features = torch.cat((sequence_features, subject_features), dim=1)\n\n        logits = self.classifier(combined_features)\n        return logits","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-05T11:13:24.505483Z","iopub.execute_input":"2025-07-05T11:13:24.505740Z","iopub.status.idle":"2025-07-05T11:13:24.523195Z","shell.execute_reply.started":"2025-07-05T11:13:24.505721Z","shell.execute_reply":"2025-07-05T11:13:24.522280Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport torch.optim as optim\n\nDEMO_INPUT_DIM = val_dataset[0][0].shape[0]\nSEQ_INPUT_CHANNELS = val_dataset[0][1].shape[0]\nNUM_CLASSES = 18\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\n# 損失関数の定義 (多クラス分類のためCrossEntropyLoss)\ncriterion = nn.CrossEntropyLoss()\n\n# 最適化アルゴリズムの定義 (Adamオプティマイザ)\noptimizer = optim.Adam(model.parameters(), lr=0.001)\n\n# モデルをGPUに移動（利用可能な場合）\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nmodel.to(device)\n\nmodel_save_path = '/kaggle/input/pytorch-baseline-train/best_gesture_classifier.pth' # 訓練ループで保存したファイル名と同じにする\n\ntry:\n    loaded_state_dict = torch.load(model_save_path, map_location=device)\n    model.load_state_dict(loaded_state_dict)\n    model.eval()\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-07-05T11:13:24.524003Z","iopub.execute_input":"2025-07-05T11:13:24.524257Z","iopub.status.idle":"2025-07-05T11:13:28.428834Z","shell.execute_reply.started":"2025-07-05T11:13:24.524222Z","shell.execute_reply":"2025-07-05T11:13:28.427955Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model.eval() # モデルを評価モードに設定 (Dropoutなどが無効になる)\nval_loss = 0\nval_correct = 0\nval_total = 0\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\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\nprint(f\"Val Loss: {avg_val_loss:.4f}, Val Acc: {val_accuracy:.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-05T11:13:28.429608Z","iopub.execute_input":"2025-07-05T11:13:28.429977Z","iopub.status.idle":"2025-07-05T11:13:29.357523Z","shell.execute_reply.started":"2025-07-05T11:13:28.429957Z","shell.execute_reply":"2025-07-05T11:13:29.356738Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# submit prediction","metadata":{}},{"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-07-05T11:13:29.359596Z","iopub.execute_input":"2025-07-05T11:13:29.359856Z","iopub.status.idle":"2025-07-05T11:13:29.365199Z","shell.execute_reply.started":"2025-07-05T11:13:29.359836Z","shell.execute_reply":"2025-07-05T11:13:29.364519Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport pandas as pd\nimport polars as pl\nimport kaggle_evaluation.cmi_inference_server","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-05T11:13:29.366151Z","iopub.execute_input":"2025-07-05T11:13:29.366802Z","iopub.status.idle":"2025-07-05T11:13:30.349992Z","shell.execute_reply.started":"2025-07-05T11:13:29.366778Z","shell.execute_reply":"2025-07-05T11:13:30.349248Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def 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-07-05T11:13:30.350930Z","iopub.execute_input":"2025-07-05T11:13:30.351968Z","iopub.status.idle":"2025-07-05T11:13:30.358600Z","shell.execute_reply.started":"2025-07-05T11:13:30.351933Z","shell.execute_reply":"2025-07-05T11:13:30.357756Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"predict(test[test[\"sequence_id\"] == \"SEQ_000001\"], test_demo[1:])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-05T11:13:30.359692Z","iopub.execute_input":"2025-07-05T11:13:30.360010Z","iopub.status.idle":"2025-07-05T11:13:30.485734Z","shell.execute_reply.started":"2025-07-05T11:13:30.359990Z","shell.execute_reply":"2025-07-05T11:13:30.484779Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"inference_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-07-05T11:13:30.486864Z","iopub.execute_input":"2025-07-05T11:13:30.487201Z","iopub.status.idle":"2025-07-05T11:13:32.136704Z","shell.execute_reply.started":"2025-07-05T11:13:30.487178Z","shell.execute_reply":"2025-07-05T11:13:32.135564Z"}},"outputs":[],"execution_count":null}]}