{"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":"none","dataSources":[{"sourceId":102335,"databundleVersionId":12518947,"sourceType":"competition"}],"dockerImageVersionId":31089,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# CMI-利用传感器数据检测行为","metadata":{}},{"cell_type":"markdown","source":"#### 部分代码参考：https://www.kaggle.com/code/shigekii/pytorch-baseline-train","metadata":{}},{"cell_type":"markdown","source":"### 1.数据加载","metadata":{}},{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nfrom tqdm import tqdm\n\ntrain_demo = pd.read_csv(f\"/kaggle/input/cmi-detect-behavior-with-sensor-data/train_demographics.csv\")\ntrain = pd.read_csv(f\"/kaggle/input/cmi-detect-behavior-with-sensor-data/train.csv\")\ntest_demo = pd.read_csv(f\"/kaggle/input/cmi-detect-behavior-with-sensor-data/test_demographics.csv\")\ntest = pd.read_csv(f\"/kaggle/input/cmi-detect-behavior-with-sensor-data/test.csv\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-08T01:06:37.312772Z","iopub.execute_input":"2025-09-08T01:06:37.313170Z","iopub.status.idle":"2025-09-08T01:07:26.300584Z","shell.execute_reply.started":"2025-09-08T01:06:37.313130Z","shell.execute_reply":"2025-09-08T01:07:26.299361Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 2.数据集类","metadata":{}},{"cell_type":"code","source":"print(\"测试集基本信息：\")\ntest.info()\n\n# 查看数据集行数和列数\nrows, columns = test.shape\n\nif rows < 5:\n    # 样本行数少于5则查看全量数据信息\n    print(\"测试集全部内容信息：\")\n    print(test.to_csv(sep='\\t', na_rep='nan'))\nelse:\n    # 样本行数多于5则查看数据前几行信息\n    print(\"测试集前几行内容信息：\")\n    print(test.head().to_csv(sep='\\t', na_rep='nan'))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-08T01:07:26.301776Z","iopub.execute_input":"2025-09-08T01:07:26.302055Z","iopub.status.idle":"2025-09-08T01:07:26.356636Z","shell.execute_reply.started":"2025-09-08T01:07:26.302033Z","shell.execute_reply":"2025-09-08T01:07:26.355390Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nfrom tqdm import tqdm\nfrom sklearn.ensemble import RandomForestClassifier\nfrom sklearn.metrics import classification_report\nfrom sklearn.preprocessing import StandardScaler\nfrom scipy import signal\nimport matplotlib.pyplot as plt\nfrom datasets import load_dataset, load_from_disk","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-08T01:07:26.358901Z","iopub.execute_input":"2025-09-08T01:07:26.359439Z","iopub.status.idle":"2025-09-08T01:07:30.428961Z","shell.execute_reply.started":"2025-09-08T01:07:26.359372Z","shell.execute_reply":"2025-09-08T01:07:30.427679Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-08T01:07:30.434491Z","iopub.execute_input":"2025-09-08T01:07:30.434899Z","iopub.status.idle":"2025-09-08T01:07:30.488299Z","shell.execute_reply.started":"2025-09-08T01:07:30.434867Z","shell.execute_reply":"2025-09-08T01:07:30.487157Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-08T01:07:30.489444Z","iopub.execute_input":"2025-09-08T01:07:30.489750Z","iopub.status.idle":"2025-09-08T01:07:30.517305Z","shell.execute_reply.started":"2025-09-08T01:07:30.489727Z","shell.execute_reply":"2025-09-08T01:07:30.515756Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_demo.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-08T01:07:30.518077Z","iopub.execute_input":"2025-09-08T01:07:30.518349Z","iopub.status.idle":"2025-09-08T01:07:30.548420Z","shell.execute_reply.started":"2025-09-08T01:07:30.518328Z","shell.execute_reply":"2025-09-08T01:07:30.547368Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(np.shape(train_demo), np.shape(train))\nprint(np.shape(test_demo), np.shape(test))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-08T01:07:30.549502Z","iopub.execute_input":"2025-09-08T01:07:30.549864Z","iopub.status.idle":"2025-09-08T01:07:30.572317Z","shell.execute_reply.started":"2025-09-08T01:07:30.549836Z","shell.execute_reply":"2025-09-08T01:07:30.571351Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print([col for col in train.columns if col not in test.columns])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-08T01:07:30.575772Z","iopub.execute_input":"2025-09-08T01:07:30.576078Z","iopub.status.idle":"2025-09-08T01:07:30.598952Z","shell.execute_reply.started":"2025-09-08T01:07:30.576053Z","shell.execute_reply":"2025-09-08T01:07:30.597737Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 3.自定义Dataset类","metadata":{}},{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nimport torch\nfrom torch.utils.data import Dataset\nfrom tqdm import tqdm\n\nclass SubjectDataset(Dataset):\n    def __init__(self, data_demo, data, have_labels=True):\n        self.have_labels = have_labels\n        # 获取所有唯一序列ID（修正原拼写错误）\n        self.unique_sequence_ids = sorted(data[\"sequence_id\"].unique())\n        # 建立“序列ID → 受试者ID”的映射\n        self.sequence_id_to_subject = data.groupby(\"sequence_id\")[\"subject\"].first().to_dict()\n        \n        # 提取人口统计数据的数值特征列（排除subject自身）\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        # 构建“受试者ID → 人口统计特征张量”的映射\n        self.subject_to_demographics = {}\n        for subject, row in data_demo.set_index('subject').iterrows():\n            demo_values = row[numerical_demo_cols].values.astype(np.float32)\n            self.subject_to_demographics[subject] = torch.tensor(demo_values)\n\n        # 处理标签（仅训练集需要）\n        self.labels = None\n        self.gesture_to_id = None\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 = data.groupby(\"sequence_id\")[\"gesture\"].first().to_dict()\n            self.labels = torch.tensor(\n                [self.gesture_to_id[sequence_labels_map[sid]] for sid in self.unique_sequence_ids],\n                dtype=torch.long\n            )\n\n        # 筛选传感器特征列（排除非特征列）\n        cols_to_drop = [\"row_id\", \"subject\", \"sequence_id\", \"sequence_type\", \"orientation\", \"behavior\", \"phase\", \"gesture\"]\n        self.sequence_feature_cols = [col for col in data.columns if col not in cols_to_drop]\n        \n        # 预处理每个序列的传感器数据（转置为“特征数 × 时间步”格式，适配后续1D卷积）\n        self.preprocessed_sequences = {}\n        print(\"正在处理序列数据...\")\n        for seq_id in tqdm(self.unique_sequence_ids):\n            seq_data = data[data[\"sequence_id\"] == seq_id][self.sequence_feature_cols].values.astype(np.float32)\n            self.preprocessed_sequences[seq_id] = torch.tensor(seq_data).T  # 转置后形状：(特征数, 时间步)\n\n    def __len__(self):\n        return len(self.unique_sequence_ids)\n\n    def __getitem__(self, idx):\n        seq_id = self.unique_sequence_ids[idx]\n        subject = self.sequence_id_to_subject[seq_id]\n        # 获取人口统计特征张量\n        demo_tensor = self.subject_to_demographics[subject]\n        # 获取传感器序列张量\n        seq_tensor = self.preprocessed_sequences[seq_id]\n\n        if self.have_labels:\n            # 训练/验证集：返回“人口特征 + 传感器序列 + 标签”\n            label = self.labels[idx]\n            return demo_tensor, seq_tensor, label\n        else:\n            # 测试集：返回“序列ID + 人口特征 + 传感器序列”（用于预测）\n            return seq_id, demo_tensor, seq_tensor","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-08T01:07:30.600156Z","iopub.execute_input":"2025-09-08T01:07:30.600476Z","iopub.status.idle":"2025-09-08T01:07:36.445337Z","shell.execute_reply.started":"2025-09-08T01:07:30.600448Z","shell.execute_reply":"2025-09-08T01:07:36.443677Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_dataset = SubjectDataset(train_demo, train)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-08T01:07:36.446879Z","iopub.execute_input":"2025-09-08T01:07:36.447234Z","iopub.status.idle":"2025-09-08T01:14:23.806441Z","shell.execute_reply.started":"2025-09-08T01:07:36.447202Z","shell.execute_reply":"2025-09-08T01:14:23.805433Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test_dataset = SubjectDataset(test_demo, test, have_labels=False)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-08T01:14:23.807415Z","iopub.execute_input":"2025-09-08T01:14:23.807696Z","iopub.status.idle":"2025-09-08T01:14:23.827374Z","shell.execute_reply.started":"2025-09-08T01:14:23.807676Z","shell.execute_reply":"2025-09-08T01:14:23.825987Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"seed = 42\ntorch.manual_seed(seed)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-08T01:14:23.828773Z","iopub.execute_input":"2025-09-08T01:14:23.829142Z","iopub.status.idle":"2025-09-08T01:14:23.849821Z","shell.execute_reply.started":"2025-09-08T01:14:23.829108Z","shell.execute_reply":"2025-09-08T01:14:23.848629Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from torch.utils.data import random_split\ntrain_size = int(0.8 * len(train_dataset))\nval_size = len(train_dataset) - train_size\ntrain_dataset, val_dataset = random_split(train_dataset, [train_size, val_size])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-08T01:14:23.850855Z","iopub.execute_input":"2025-09-08T01:14:23.851492Z","iopub.status.idle":"2025-09-08T01:14:23.882698Z","shell.execute_reply.started":"2025-09-08T01:14:23.851454Z","shell.execute_reply":"2025-09-08T01:14:23.881441Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 4.自定义collate函数","metadata":{}},{"cell_type":"code","source":"import torch\nimport torch.nn.functional as F\n\ndef custom_collate_fn(batch):\n    \"\"\"\n    自定义批处理函数：统一批次内变长传感器序列的长度，并适配有/无标签场景\n    \n    参数:\n        batch: 原始数据批次，格式为：\n            - 训练/验证集：(人口统计特征张量, 传感器序列张量, 标签张量) 的元组列表\n            - 测试集：(序列ID, 人口统计特征张量, 传感器序列张量) 的元组列表\n    \n    返回:\n        批量化数据，格式为：\n            - 有标签：(人口统计特征批次, 填充后的序列批次, 标签批次)\n            - 无标签：(序列ID列表, 人口统计特征批次, 填充后的序列批次)\n    \"\"\"\n    # 判断当前批次是否包含标签（训练/验证集特征数为3，测试集为2）\n    has_labels = len(batch[0]) == 3\n\n    # 拆分批次中的不同组件\n    if has_labels:\n        demo_tensors, seq_tensors, label_tensors = zip(*batch)\n    else:\n        seq_ids, demo_tensors, seq_tensors = zip(*batch)\n\n    # 1. 人口统计特征批量化（长度固定，直接堆叠）\n    demo_batch = torch.stack(demo_tensors)\n\n    # 2. 传感器序列填充（统一到批次内最长序列长度）\n    # 获取批次中最长序列的时间步长度\n    max_seq_len = max(seq.shape[1] for seq in seq_tensors)\n    \n    # 对每个序列进行填充\n    padded_seqs = []\n    for seq in seq_tensors:\n        # 计算需要填充的长度（时间维度右侧补0）\n        pad_length = max_seq_len - seq.shape[1]\n        if pad_length > 0:\n            padded_seq = F.pad(seq, (0, pad_length))  # 仅在时间维度补0\n        else:\n            padded_seq = seq  # 长度足够时直接使用原序列\n        padded_seqs.append(padded_seq)\n    \n    # 堆叠填充后的序列形成批次\n    seq_batch = torch.stack(padded_seqs)\n\n    # 3. 按场景返回对应格式的数据\n    if has_labels:\n        label_batch = torch.stack(label_tensors)\n        return demo_batch, seq_batch, label_batch\n    else:\n        return seq_ids, demo_batch, seq_batch","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-08T01:14:23.883660Z","iopub.execute_input":"2025-09-08T01:14:23.883938Z","iopub.status.idle":"2025-09-08T01:14:23.894930Z","shell.execute_reply.started":"2025-09-08T01:14:23.883918Z","shell.execute_reply":"2025-09-08T01:14:23.893696Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 补充必要的导入\nimport torch\nfrom torch.utils.data import DataLoader  # 关键：导入DataLoader类\n\n# 数据加载器配置参数\nBATCH_SIZE = 32\nNUM_WORKERS = 0  # Windows系统建议设为0，避免多进程问题\n\n# 创建训练集数据加载器\ntrain_loader = DataLoader(\n    dataset=train_dataset,\n    batch_size=BATCH_SIZE,\n    shuffle=True,  # 训练集打乱顺序\n    collate_fn=custom_collate_fn,\n    num_workers=NUM_WORKERS,\n    pin_memory=torch.cuda.is_available()  # 若有GPU，自动启用加速\n)\n\n# 创建验证集数据加载器\nval_loader = DataLoader(\n    dataset=val_dataset,\n    batch_size=BATCH_SIZE,\n    shuffle=False,  # 验证集不打乱顺序\n    collate_fn=custom_collate_fn,\n    num_workers=NUM_WORKERS,\n    pin_memory=torch.cuda.is_available()\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-08T01:14:23.896123Z","iopub.execute_input":"2025-09-08T01:14:23.896614Z","iopub.status.idle":"2025-09-08T01:14:23.930022Z","shell.execute_reply.started":"2025-09-08T01:14:23.896553Z","shell.execute_reply":"2025-09-08T01:14:23.928876Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 5.混合神经网络模型","metadata":{}},{"cell_type":"code","source":"import os\nimport pandas as pd\nimport numpy as np\nimport math  # 新增：导入math模块用于检查浮点数\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader, random_split\nfrom tqdm import tqdm, trange\nimport torch.nn.functional as F\nimport time\n\n# --------------------------\n# 0. 全局配置\n# --------------------------\nDATA_FOLDER = \"/kaggle/input/cmi-detect-behavior-with-sensor-data\"\nBATCH_SIZE = 16\nNUM_EPOCHS = 10\nLEARNING_RATE = 1e-5\nRANDOM_SEED = 42\nNUM_WORKERS = 0\n\n\n# --------------------------\n# 1. 工具函数\n# --------------------------\ndef print_status(message):\n    tqdm.write(f\"[{time.strftime('%H:%M:%S', time.localtime())}] {message}\")\n\n\ndef check_file_exists(file_path):\n    if not os.path.exists(file_path):\n        raise FileNotFoundError(f\"数据文件不存在，请检查路径：{file_path}\")\n\n\ndef clean_nan_inf(data: pd.DataFrame) -> pd.DataFrame:\n    data = data.replace([np.inf, -np.inf], np.nan)\n    for col in data.columns:\n        if pd.api.types.is_numeric_dtype(data[col]):\n            fill_val = data[col].mean()\n        else:\n            fill_val = data[col].mode()[0] if not data[col].mode().empty else \"\"\n        data[col] = data[col].fillna(fill_val)\n    return data\n\n\n# --------------------------\n# 2. 数据集类\n# --------------------------\nclass SubjectDataset(Dataset):\n    def __init__(self, data_demo: pd.DataFrame, data: pd.DataFrame, have_labels: bool = False):\n        self.have_labels = have_labels\n        self.data = clean_nan_inf(data)\n        self.unique_sequence_ids = sorted(self.data[\"sequence_id\"].unique())\n        self.seq_to_subj = self.data.groupby(\"sequence_id\")[\"subject\"].first().to_dict()\n        \n        # 人口特征：明确指定float32\n        self.data_demo = clean_nan_inf(data_demo)\n        self.numerical_demo_cols = [c for c in self.data_demo.select_dtypes(include=np.number).columns \n                                    if c != \"subject\"]\n        self.demo_mean = self.data_demo[self.numerical_demo_cols].mean()\n        self.demo_std = self.data_demo[self.numerical_demo_cols].std() + 1e-8\n        self.subj_demo = {\n            subj: torch.tensor(\n                ((row[self.numerical_demo_cols] - self.demo_mean) / self.demo_std).clip(-5, 5).values,\n                dtype=torch.float32\n            ) for subj, row in self.data_demo.set_index(\"subject\").iterrows()\n        }\n\n        # 传感器特征：过滤非数值列\n        self.drop_cols = [\"row_id\", \"subject\", \"sequence_id\", \"sequence_type\", \n                         \"orientation\", \"behavior\", \"phase\", \"gesture\"]\n        self.feat_cols = [c for c in self.data.columns if c not in self.drop_cols]\n        self.feat_cols = [col for col in self.feat_cols if pd.api.types.is_numeric_dtype(self.data[col])]\n        print_status(f\"传感器特征列数量：{len(self.feat_cols)}（列名示例：{self.feat_cols[:3]}...）\")\n        \n        # 预计算传感器特征均值/标准差\n        self.seq_mean = self.data[self.feat_cols].mean()\n        self.seq_std = self.data[self.feat_cols].std() + 1e-8\n        \n        print_status(f\"开始处理 {len(self.unique_sequence_ids)} 个序列（无去噪+数据清洗）\")\n        self.sequences = self._process_all_sequences()\n\n    def _process_all_sequences(self):\n        sequences = {}\n        with tqdm(self.unique_sequence_ids, position=0, leave=False, desc=\"处理序列\") as pbar:\n            for seq_id in pbar:\n                try:\n                    sequences[seq_id] = self._process_single_sequence(seq_id)\n                    pbar.set_postfix({\"当前序列\": seq_id}, refresh=False)\n                except Exception as e:\n                    tqdm.write(f\"\\n警告：序列 {seq_id} 处理失败（{str(e)}），使用均值填充\")\n                    fake_seq = np.full((30, len(self.feat_cols)), self.seq_mean.values, dtype=np.float32)\n                    sequences[seq_id] = torch.tensor(fake_seq.T, dtype=torch.float32)\n        return sequences\n\n    def _process_single_sequence(self, seq_id: str):\n        # 提取序列数据：强制转为float32\n        seq_data = self.data[self.data[\"sequence_id\"] == seq_id][self.feat_cols].values.astype(np.float32)\n        original_len = seq_data.shape[0]\n        \n        # 标准化+裁剪\n        seq_data = (seq_data - self.seq_mean.values) / self.seq_std.values\n        seq_data = np.clip(seq_data, -5, 5)\n        \n        # 长度对齐\n        target_len = min(max(original_len, 30), 500)\n        seq_data = self._align_sequence_length(seq_data, target_len)\n        \n        # 强制float32\n        return torch.tensor(seq_data.T, dtype=torch.float32)\n\n    @staticmethod\n    def _align_sequence_length(signal: np.ndarray, target_len: int) -> np.ndarray:\n        if len(signal) == target_len:\n            return signal\n        elif len(signal) > target_len:\n            return signal[:target_len]\n        else:\n            pad_val = np.mean(signal, axis=0).astype(np.float32)\n            return np.pad(signal, ((0, target_len - len(signal)), (0, 0)), mode=\"constant\", constant_values=pad_val)\n\n    def __len__(self) -> int:\n        return len(self.unique_sequence_ids)\n\n    def __getitem__(self, idx: int) -> tuple:\n        seq_id = self.unique_sequence_ids[idx]\n        subj_id = self.seq_to_subj[seq_id]\n        demo_feat = self.subj_demo[subj_id]\n        seq_feat = self.sequences[seq_id]\n        \n        if self.have_labels:\n            label = self.data[self.data[\"sequence_id\"] == seq_id][\"gesture\"].iloc[0]\n            return (seq_id, demo_feat, seq_feat, label)\n        return (seq_id, demo_feat, seq_feat)\n\n\n# --------------------------\n# 3. Collate函数\n# --------------------------\ndef custom_collate(batch: list) -> tuple:\n    has_labels = len(batch[0]) == 4\n\n    if has_labels:\n        seq_ids, demo_list, seq_list, label_list = zip(*batch)\n    else:\n        seq_ids, demo_list, seq_list = zip(*batch)\n\n    # 人口特征：已是float32，直接堆叠\n    demo_batch = torch.stack(demo_list)\n\n    # 序列填充：填充值强制float32\n    max_seq_len = max(seq.shape[1] for seq in seq_list)\n    padded_seqs = []\n    for seq in seq_list:\n        pad_length = max_seq_len - seq.shape[1]\n        if pad_length > 0:\n            pad_val = torch.tensor(np.mean(seq.numpy(), axis=1), dtype=torch.float32).unsqueeze(1)\n            padded_seq = torch.cat([seq, pad_val.repeat(1, pad_length)], dim=1)\n        else:\n            padded_seq = seq\n        padded_seqs.append(padded_seq)\n    seq_batch = torch.stack(padded_seqs)\n\n    if has_labels:\n        unique_labels = sorted(set(label_list))\n        label_to_id = {label: idx for idx, label in enumerate(unique_labels)}\n        label_batch = torch.tensor([label_to_id[label] for label in label_list], dtype=torch.long)\n        return (seq_ids, demo_batch, seq_batch, label_batch)\n    \n    return (seq_ids, demo_batch, seq_batch)\n\n\n# --------------------------\n# 4. 模型\n# --------------------------\nclass HybridClassifier(nn.Module):\n    def __init__(self, seq_feat_dim: int, subj_feat_dim: int, num_classes: int):\n        super().__init__()\n        self.cnn = nn.Sequential(\n            nn.Conv1d(seq_feat_dim, 64, kernel_size=3, padding=1),\n            nn.ReLU(),\n            nn.BatchNorm1d(64),\n            nn.MaxPool1d(2),\n            \n            nn.Conv1d(64, 128, kernel_size=3, padding=1),\n            nn.ReLU(),\n            nn.BatchNorm1d(128),\n            nn.MaxPool1d(2),\n            \n            nn.Conv1d(128, 256, kernel_size=3, padding=1),\n            nn.ReLU(),\n            nn.BatchNorm1d(256),\n            nn.MaxPool1d(2)\n        )\n\n        # 修复LSTM dropout警告：num_layers=1时dropout无效，设为0\n        self.lstm = nn.LSTM(\n            input_size=256,\n            hidden_size=256,\n            num_layers=1,\n            batch_first=True,\n            dropout=0.0,\n            bidirectional=False\n        )\n\n        self.subj_encoder = nn.Sequential(\n            nn.Linear(subj_feat_dim, 64),\n            nn.ReLU(),\n            nn.Dropout(0.2),\n            nn.Linear(64, 128),\n            nn.ReLU(),\n            nn.Dropout(0.2)\n        )\n\n        self.classifier = nn.Sequential(\n            nn.Linear(256 + 128, 256),\n            nn.ReLU(),\n            nn.Dropout(0.3),\n            nn.Linear(256, 128),\n            nn.ReLU(),\n            nn.Dropout(0.3),\n            nn.Linear(128, num_classes)\n        )\n\n        self._initialize_weights()\n\n    def _initialize_weights(self):\n        for m in self.modules():\n            if isinstance(m, nn.Conv1d) or isinstance(m, nn.Linear):\n                nn.init.normal_(m.weight, mean=0.0, std=0.005)\n                if m.bias is not None:\n                    nn.init.constant_(m.bias, 0.01)\n            elif isinstance(m, nn.LSTM):\n                for name, param in m.named_parameters():\n                    if 'weight' in name:\n                        nn.init.normal_(param, mean=0.0, std=0.005)\n                    elif 'bias' in name:\n                        nn.init.constant_(param, 0.01)\n\n    def forward(self, demo_feat: torch.Tensor, seq_feat: torch.Tensor) -> torch.Tensor:\n        cnn_out = self.cnn(seq_feat)\n        cnn_out = torch.clip(cnn_out, -10, 10)\n        \n        lstm_in = cnn_out.transpose(1, 2)\n        lstm_out, (hn, _) = self.lstm(lstm_in)\n        seq_final_feat = torch.clip(hn[-1], -10, 10)\n        \n        subj_final_feat = self.subj_encoder(demo_feat)\n        subj_final_feat = torch.clip(subj_final_feat, -10, 10)\n        \n        combined_feat = torch.cat([seq_final_feat, subj_final_feat], dim=1)\n        combined_feat = torch.clip(combined_feat, -10, 10)\n        \n        return self.classifier(combined_feat)\n\n\n# --------------------------\n# 5. 训练函数（核心修复：用math.isfinite检查浮点数）\n# --------------------------\ndef train_model(model: nn.Module, train_loader: DataLoader, val_loader: DataLoader, \n                num_epochs: int, device: torch.device, lr: float = 1e-5) -> nn.Module:\n    criterion = nn.CrossEntropyLoss()\n    optimizer = optim.Adam(model.parameters(), lr=lr, weight_decay=1e-6)\n    scheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer, 'min', patience=3, factor=0.5)\n    \n    model.to(device)\n    best_val_loss = float(\"inf\")\n    best_model_state = model.state_dict().copy()\n    torch.save(best_model_state, \"initial_model.pth\")\n\n    with trange(num_epochs, position=0, leave=True, desc=\"训练总进度\") as epoch_pbar:\n        for epoch in epoch_pbar:\n            model.train()\n            train_total_loss = 0.0\n            train_total_acc = 0.0\n            train_total_samples = 0\n\n            with tqdm(train_loader, position=1, leave=False, \n                      desc=f\"Epoch {epoch+1}/{num_epochs} [训练]\") as batch_pbar:\n                for batch in batch_pbar:\n                    _, demo_batch, seq_batch, label_batch = batch\n                    demo_batch = demo_batch.to(device)\n                    seq_batch = seq_batch.to(device)\n                    label_batch = label_batch.to(device)\n\n                    optimizer.zero_grad()\n                    outputs = model(demo_batch, seq_batch)\n                    \n                    if torch.isnan(outputs).any():\n                        tqdm.write(\"警告：模型输出含NaN，跳过本轮更新\")\n                        continue\n                    \n                    loss = criterion(outputs, label_batch)\n                    torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=0.5)\n                    \n                    loss.backward()\n                    optimizer.step()\n\n                    # 修复1：用math.isfinite检查float类型的loss\n                    if not math.isfinite(loss.item()):\n                        tqdm.write(f\"警告：第{epoch+1}轮损失异常，跳过\")\n                        continue\n\n                    batch_size = demo_batch.size(0)\n                    train_total_loss += loss.item() * batch_size\n                    preds = outputs.argmax(dim=1)\n                    correct = (preds == label_batch).sum().item()\n                    train_total_acc += correct\n                    train_total_samples += batch_size\n\n                    batch_pbar.set_postfix({\n                        \"batch_loss\": f\"{loss.item():.4f}\",\n                        \"batch_acc\": f\"{correct/batch_size:.4f}\"\n                    }, refresh=False)\n\n            if train_total_samples == 0:\n                tqdm.write(\"警告：训练集样本数为0，跳过本轮\")\n                continue\n\n            train_avg_loss = train_total_loss / train_total_samples\n            train_avg_acc = train_total_acc / train_total_samples\n\n            # 验证阶段\n            model.eval()\n            val_total_loss = 0.0\n            val_total_acc = 0.0\n            val_total_samples = 0\n\n            with torch.no_grad():\n                with tqdm(val_loader, position=1, leave=False, \n                          desc=f\"Epoch {epoch+1}/{num_epochs} [验证]\") as batch_pbar:\n                    for batch in batch_pbar:\n                        _, demo_batch, seq_batch, label_batch = batch\n                        demo_batch = demo_batch.to(device)\n                        seq_batch = seq_batch.to(device)\n                        label_batch = label_batch.to(device)\n\n                        outputs = model(demo_batch, seq_batch)\n                        if torch.isnan(outputs).any():\n                            tqdm.write(\"警告：验证输出含NaN，用0填充\")\n                            outputs = torch.zeros_like(outputs, dtype=torch.float32)\n                        \n                        loss = criterion(outputs, label_batch)\n                        # 修复2：用math.isfinite检查float类型的loss\n                        if not math.isfinite(loss.item()):\n                            loss = torch.tensor(10.0, device=device, dtype=torch.float32)\n\n                        batch_size = demo_batch.size(0)\n                        val_total_loss += loss.item() * batch_size\n                        preds = outputs.argmax(dim=1)\n                        correct = (preds == label_batch).sum().item()\n                        val_total_acc += correct\n                        val_total_samples += batch_size\n\n                        batch_pbar.set_postfix({\n                            \"val_batch_loss\": f\"{loss.item():.4f}\",\n                            \"val_batch_acc\": f\"{correct/batch_size:.4f}\"\n                        }, refresh=False)\n\n            if val_total_samples == 0:\n                tqdm.write(\"警告：验证集样本数为0，跳过本轮\")\n                continue\n\n            val_avg_loss = val_total_loss / val_total_samples\n            val_avg_acc = val_total_acc / val_total_samples\n\n            scheduler.step(val_avg_loss)\n\n            # 修复3：用math.isfinite检查float类型的val_avg_loss\n            if val_avg_loss < best_val_loss and math.isfinite(val_avg_loss):\n                best_val_loss = val_avg_loss\n                best_model_state = model.state_dict().copy()\n                torch.save(best_model_state, \"best_hybrid_classifier.pth\")\n                print_status(f\"Epoch {epoch+1}：保存最佳模型（验证损失：{val_avg_loss:.4f}）\")\n\n            epoch_pbar.set_postfix({\n                \"train_loss\": f\"{train_avg_loss:.4f}\",\n                \"train_acc\": f\"{train_avg_acc:.4f}\",\n                \"val_loss\": f\"{val_avg_loss:.4f}\",\n                \"val_acc\": f\"{val_avg_acc:.4f}\",\n                \"lr\": f\"{optimizer.param_groups[0]['lr']:.8f}\"\n            }, refresh=True)\n\n            print_status(f\"Epoch {epoch+1}/{num_epochs} 总结：\"\n                         f\"训练损失 {train_avg_loss:.4f}（acc {train_avg_acc:.4f}）| \"\n                         f\"验证损失 {val_avg_loss:.4f}（acc {val_avg_acc:.4f}）\")\n\n    model.load_state_dict(best_model_state)\n    return model\n\n\n# --------------------------\n# 6. 预测函数\n# --------------------------\ndef predict_test(model: nn.Module, test_loader: DataLoader, device: torch.device, \n                 label_map: dict) -> pd.DataFrame:\n    model.eval()\n    model.to(device)\n    all_seq_ids = []\n    all_preds = []\n\n    with torch.no_grad(), tqdm(test_loader, position=0, leave=True, \n                               desc=\"测试集预测\") as pbar:\n        for batch in pbar:\n            seq_ids, demo_batch, seq_batch = batch\n            demo_batch = demo_batch.to(device)\n            seq_batch = seq_batch.to(device)\n\n            outputs = model(demo_batch, seq_batch)\n            if torch.isnan(outputs).any():\n                tqdm.write(\"警告：预测输出含NaN，用多数类填充\")\n                outputs = torch.zeros_like(outputs, dtype=torch.float32)\n                outputs[:, 0] = 1.0\n            \n            preds = outputs.argmax(dim=1).cpu().numpy()\n\n            all_seq_ids.extend(seq_ids)\n            all_preds.extend(preds)\n\n            pbar.set_postfix({\"已处理序列\": len(all_seq_ids)}, refresh=False)\n\n    submission_df = pd.DataFrame({\n        \"sequence_id\": all_seq_ids,\n        \"predicted_gesture\": [label_map[pred_id] for pred_id in all_preds]\n    }).drop_duplicates(subset=\"sequence_id\")\n\n    submission_df.to_parquet(\"cmi_behavior_prediction_submission.parquet\", index=False)\n    print_status(f\"预测完成！提交文件已保存（共 {len(submission_df)} 条记录）\")\n    print_status(f\"提交文件前5行示例：\\n{submission_df.head().to_string(index=False)}\")\n\n    return submission_df\n\n\n# --------------------------\n# 7. 主程序\n# --------------------------\ndef main():\n    try:\n        torch.manual_seed(RANDOM_SEED)\n        np.random.seed(RANDOM_SEED)\n\n        # 1. 加载数据\n        print_status(\"=== 开始加载数据 ===\")\n        train_demo_path = os.path.join(DATA_FOLDER, \"train_demographics.csv\")\n        train_data_path = os.path.join(DATA_FOLDER, \"train.csv\")\n        test_demo_path = os.path.join(DATA_FOLDER, \"test_demographics.csv\")\n        test_data_path = os.path.join(DATA_FOLDER, \"test.csv\")\n\n        check_file_exists(train_demo_path)\n        check_file_exists(train_data_path)\n        check_file_exists(test_demo_path)\n        check_file_exists(test_data_path)\n\n        train_demo = pd.read_csv(train_demo_path)\n        train_data = pd.read_csv(train_data_path)\n        test_demo = pd.read_csv(test_demo_path)\n        test_data = pd.read_csv(test_data_path)\n\n        print_status(f\"数据加载完成：训练集序列数 {train_data['sequence_id'].nunique()} | \"\n                     f\"测试集序列数 {test_data['sequence_id'].nunique()} | \"\n                     f\"行为类别数 {train_data['gesture'].nunique()}\")\n\n        # 2. 创建数据集\n        print_status(\"\\n=== 开始创建数据集 ===\")\n        full_train_dataset = SubjectDataset(\n            data_demo=train_demo,\n            data=train_data,\n            have_labels=True\n        )\n        \n        train_size = int(0.8 * len(full_train_dataset))\n        val_size = len(full_train_dataset) - train_size\n        train_dataset, val_dataset = random_split(\n            full_train_dataset,\n            [train_size, val_size],\n            generator=torch.Generator().manual_seed(RANDOM_SEED)\n        )\n\n        test_dataset = SubjectDataset(\n            data_demo=test_demo,\n            data=test_data,\n            have_labels=False\n        )\n\n        print_status(f\"数据集拆分完成：训练集 {len(train_dataset)} 样本 | \"\n                     f\"验证集 {len(val_dataset)} 样本 | \"\n                     f\"测试集 {len(test_dataset)} 样本\")\n\n        # 3. 创建数据加载器\n        print_status(\"\\n=== 开始创建数据加载器 ===\")\n        train_loader = DataLoader(\n            dataset=train_dataset,\n            batch_size=BATCH_SIZE,\n            shuffle=True,\n            collate_fn=custom_collate,\n            num_workers=NUM_WORKERS,\n            pin_memory=False,\n            drop_last=False\n        )\n\n        val_loader = DataLoader(\n            dataset=val_dataset,\n            batch_size=BATCH_SIZE,\n            shuffle=False,\n            collate_fn=custom_collate,\n            num_workers=NUM_WORKERS,\n            pin_memory=False,\n            drop_last=False\n        )\n\n        test_loader = DataLoader(\n            dataset=test_dataset,\n            batch_size=BATCH_SIZE,\n            shuffle=False,\n            collate_fn=custom_collate,\n            num_workers=NUM_WORKERS,\n            pin_memory=False,\n            drop_last=False\n        )\n\n        # 4. 初始化模型\n        print_status(\"\\n=== 开始初始化模型 ===\")\n        train_sample = train_dataset[0]\n        _, demo_sample, seq_sample, _ = train_sample\n        seq_feat_dim = seq_sample.shape[0]\n        subj_feat_dim = demo_sample.shape[0]\n        num_classes = train_data[\"gesture\"].nunique()\n\n        model = HybridClassifier(\n            seq_feat_dim=seq_feat_dim,\n            subj_feat_dim=subj_feat_dim,\n            num_classes=num_classes\n        )\n\n        print_status(f\"模型初始化完成：传感器特征维度 {seq_feat_dim} | \"\n                     f\"人口特征维度 {subj_feat_dim} | 类别数 {num_classes}\")\n        print_status(f\"模型总参数量：{sum(p.numel() for p in model.parameters() if p.requires_grad):,}\")\n\n        # 5. 训练设备\n        device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n        print_status(f\"\\n=== 训练设备：{device} ===\")\n        if torch.cuda.is_available():\n            print_status(f\"GPU型号：{torch.cuda.get_device_name(0)}\")\n\n        # 6. 训练模型\n        print_status(\"\\n=== 开始训练模型 ===\")\n        model = train_model(\n            model=model,\n            train_loader=train_loader,\n            val_loader=val_loader,\n            num_epochs=NUM_EPOCHS,\n            device=device,\n            lr=LEARNING_RATE\n        )\n\n        # 7. 预测\n        print_status(\"\\n=== 开始测试集预测 ===\")\n        unique_gestures = sorted(train_data[\"gesture\"].unique())\n        id_to_gesture = {idx: gesture for idx, gesture in enumerate(unique_gestures)}\n        submission_df = predict_test(\n            model=model,\n            test_loader=test_loader,\n            device=device,\n            label_map=id_to_gesture\n        )\n\n        print_status(\"\\n=== 全部流程完成 ===\")\n        return submission_df\n\n    except Exception as e:\n        print_status(f\"\\n程序运行出错：{str(e)}\")\n        raise\n \n\nif __name__ == \"__main__\":\n    submission = main()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-08T02:00:16.237507Z","iopub.execute_input":"2025-09-08T02:00:16.237977Z","iopub.status.idle":"2025-09-08T03:27:34.090527Z","shell.execute_reply.started":"2025-09-08T02:00:16.237949Z","shell.execute_reply":"2025-09-08T03:27:34.089123Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nfrom sklearn.metrics import classification_report, confusion_matrix, accuracy_score, f1_score\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader, random_split  # 修正：Dataset从正确模块导入\nfrom tqdm import tqdm\nimport math\n\n# --------------------------\n# 1. 全局配置（与训练代码完全一致，避免参数 mismatch）\n# --------------------------\n# 请根据你的实际文件路径修改！！！\nPRED_FILE_PATH = \"cmi_behavior_prediction_submission.parquet\"  # 训练代码生成的预测文件\nMODEL_WEIGHT_PATH = \"best_hybrid_classifier.pth\"  # 训练代码保存的最佳模型权重\nDATA_FOLDER = \"/kaggle/input/cmi-detect-behavior-with-sensor-data\"  # 数据根路径\n\n# 训练相关参数（与训练代码保持一致）\nBATCH_SIZE = 16\nRANDOM_SEED = 42\nDEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(f\"评估设备：{DEVICE}\")\n\n\n# --------------------------\n# 2. 工具函数（与训练代码一致，确保数据处理逻辑统一）\n# --------------------------\ndef clean_nan_inf(data: pd.DataFrame) -> pd.DataFrame:\n    \"\"\"清洗数据中的NaN/Inf，与训练时一致\"\"\"\n    data = data.replace([np.inf, -np.inf], np.nan)\n    for col in data.columns:\n        if pd.api.types.is_numeric_dtype(data[col]):\n            fill_val = data[col].mean()\n        else:\n            fill_val = data[col].mode()[0] if not data[col].mode().empty else \"\"\n        data[col] = data[col].fillna(fill_val)\n    return data\n\n\ndef process_anomalies(tensor: torch.Tensor) -> torch.Tensor:\n    \"\"\"处理张量中的异常值，与训练时一致\"\"\"\n    tensor[torch.isnan(tensor)] = 0.0\n    tensor[torch.isinf(tensor)] = 0.0\n    return tensor\n\n\n# --------------------------\n# 3. 数据集类（修正继承错误，与训练代码完全一致）\n# --------------------------\nclass SubjectDataset(Dataset):  # 关键修正：继承Dataset而非nn.Module\n    def __init__(self, data_demo: pd.DataFrame, data_sensor: pd.DataFrame, have_labels: bool = False):\n        self.have_labels = have_labels\n        self.data_sensor = clean_nan_inf(data_sensor)  # 与训练一致：先清洗数据\n        self.unique_sequence_ids = sorted(self.data_sensor[\"sequence_id\"].unique())\n        self.seq_to_subj = self.data_sensor.groupby(\"sequence_id\")[\"subject\"].first().to_dict()\n        \n        # 1. 人口统计特征：标准化+float32（与训练一致）\n        self.data_demo = clean_nan_inf(data_demo)\n        self.numerical_demo_cols = [c for c in self.data_demo.select_dtypes(include=np.number).columns \n                                    if c != \"subject\"]\n        # 计算均值/标准差（与训练一致的标准化逻辑）\n        self.demo_mean = self.data_demo[self.numerical_demo_cols].mean()\n        self.demo_std = self.data_demo[self.numerical_demo_cols].std() + 1e-8\n        self.subj_demo = {\n            subj: torch.tensor(\n                ((row[self.numerical_demo_cols] - self.demo_mean) / self.demo_std).clip(-5, 5).values,\n                dtype=torch.float32\n            ) for subj, row in self.data_demo.set_index(\"subject\").iterrows()\n        }\n\n        # 2. 传感器特征：过滤非数值列+预计算全局均值（与训练一致）\n        self.drop_cols = [\"row_id\", \"subject\", \"sequence_id\", \"sequence_type\", \n                         \"orientation\", \"behavior\", \"phase\", \"gesture\"]\n        self.feat_cols = [c for c in self.data_sensor.columns if c not in self.drop_cols]\n        self.feat_cols = [col for col in self.feat_cols if pd.api.types.is_numeric_dtype(self.data_sensor[col])]\n        self.seq_mean = self.data_sensor[self.feat_cols].mean()\n        self.seq_std = self.data_sensor[self.feat_cols].std() + 1e-8\n\n        # 3. 标签处理（仅训练/验证集）\n        self.labels = None\n        if have_labels:\n            # 按序列ID获取标签，与训练一致\n            seq_label_map = self.data_sensor.groupby(\"sequence_id\")[\"gesture\"].first().to_dict()\n            # 生成类别映射（确保与训练时排序一致）\n            global gesture_to_id, id_to_gesture\n            gestures = sorted(self.data_sensor[\"gesture\"].unique())\n            gesture_to_id = {g: i for i, g in enumerate(gestures)}\n            id_to_gesture = {i: g for g, i in gesture_to_id.items()}\n            # 转换为tensor标签\n            self.labels = torch.tensor(\n                [gesture_to_id[seq_label_map[sid]] for sid in self.unique_sequence_ids],\n                dtype=torch.long\n            )\n\n        # 4. 预处理所有传感器序列（与训练一致：标准化+长度对齐）\n        print(f\"加载 {len(self.unique_sequence_ids)} 个序列...\")\n        self.sequences = self._process_all_sequences()\n\n    def _process_all_sequences(self):\n        sequences = {}\n        for seq_id in tqdm(self.unique_sequence_ids, desc=\"处理传感器序列\"):\n            try:\n                sequences[seq_id] = self._process_single_sequence(seq_id)\n            except Exception as e:\n                tqdm.write(f\"警告：序列 {seq_id} 处理失败（{str(e)}），用均值填充\")\n                fake_seq = np.full((30, len(self.feat_cols)), self.seq_mean.values, dtype=np.float32)\n                sequences[seq_id] = torch.tensor(fake_seq.T, dtype=torch.float32)\n        return sequences\n\n    def _process_single_sequence(self, seq_id: str):\n        # 提取序列数据\n        seq_data = self.data_sensor[self.data_sensor[\"sequence_id\"] == seq_id][self.feat_cols].values.astype(np.float32)\n        # 标准化+裁剪（与训练一致）\n        seq_data = (seq_data - self.seq_mean.values) / self.seq_std.values\n        seq_data = np.clip(seq_data, -5, 5)\n        # 长度对齐（30-500步，与训练一致）\n        target_len = min(max(seq_data.shape[0], 30), 500)\n        if seq_data.shape[0] > target_len:\n            seq_data = seq_data[:target_len]\n        elif seq_data.shape[0] < target_len:\n            pad_val = np.mean(seq_data, axis=0)\n            seq_data = np.pad(seq_data, ((0, target_len - seq_data.shape[0]), (0, 0)), mode=\"constant\", constant_values=pad_val)\n        # 转置为 [特征数, 时间步]（适配CNN输入）\n        return torch.tensor(seq_data.T, dtype=torch.float32)\n\n    def __len__(self) -> int:\n        return len(self.unique_sequence_ids)\n\n    def __getitem__(self, idx: int) -> tuple:\n        seq_id = self.unique_sequence_ids[idx]\n        demo_feat = self.subj_demo[self.seq_to_subj[seq_id]]\n        seq_feat = self.sequences[seq_id]\n        if self.have_labels:\n            return (demo_feat, seq_feat, self.labels[idx])\n        return (seq_id, demo_feat, seq_feat)\n\n\n# --------------------------\n# 4. 自定义Collate函数（与训练代码完全一致，处理变长序列）\n# --------------------------\ndef custom_collate(batch: list) -> tuple:\n    has_labels = len(batch[0]) == 3  # 训练/验证集（3元素）vs 测试集（3元素：seq_id+2特征）\n\n    if has_labels:\n        demo_list, seq_list, label_list = zip(*batch)\n    else:\n        seq_ids, demo_list, seq_list = zip(*batch)\n\n    # 1. 人口特征：直接堆叠（固定长度）\n    demo_batch = torch.stack(demo_list)\n\n    # 2. 传感器序列：按批次最长序列填充（均值填充，与训练一致）\n    max_seq_len = max(seq.shape[1] for seq in seq_list)\n    padded_seqs = []\n    for seq in seq_list:\n        pad_length = max_seq_len - seq.shape[1]\n        if pad_length > 0:\n            pad_val = torch.tensor(np.mean(seq.numpy(), axis=1), dtype=torch.float32).unsqueeze(1)\n            padded_seq = torch.cat([seq, pad_val.repeat(1, pad_length)], dim=1)\n        else:\n            padded_seq = seq\n        padded_seqs.append(padded_seq)\n    seq_batch = torch.stack(padded_seqs)\n\n    # 3. 标签处理\n    if has_labels:\n        return (demo_batch, seq_batch, torch.stack(label_list))\n    return (seq_ids, demo_batch, seq_batch)\n\n\n# --------------------------\n# 5. 模型结构（与最新训练代码完全一致，确保权重加载匹配）\n# --------------------------\nclass HybridClassifier(nn.Module):\n    def __init__(self, seq_feat_dim: int, subj_feat_dim: int, num_classes: int):\n        super().__init__()\n        # CNN模块（简化版，与训练一致）\n        self.cnn = nn.Sequential(\n            nn.Conv1d(seq_feat_dim, 64, kernel_size=3, padding=1),\n            nn.ReLU(),\n            nn.BatchNorm1d(64),\n            nn.MaxPool1d(2),\n            \n            nn.Conv1d(64, 128, kernel_size=3, padding=1),\n            nn.ReLU(),\n            nn.BatchNorm1d(128),\n            nn.MaxPool1d(2),\n            \n            nn.Conv1d(128, 256, kernel_size=3, padding=1),\n            nn.ReLU(),\n            nn.BatchNorm1d(256),\n            nn.MaxPool1d(2)\n        )\n\n        # LSTM模块（1层，与训练一致）\n        self.lstm = nn.LSTM(\n            input_size=256,\n            hidden_size=256,\n            num_layers=1,\n            batch_first=True,\n            dropout=0.0,  # 单层禁用dropout，与训练一致\n            bidirectional=False\n        )\n\n        # 人口特征编码器（简化版，与训练一致）\n        self.subj_encoder = nn.Sequential(\n            nn.Linear(subj_feat_dim, 64),\n            nn.ReLU(),\n            nn.Dropout(0.2),\n            nn.Linear(64, 128),\n            nn.ReLU(),\n            nn.Dropout(0.2)\n        )\n\n        # 分类器（简化版，与训练一致）\n        self.classifier = nn.Sequential(\n            nn.Linear(256 + 128, 256),\n            nn.ReLU(),\n            nn.Dropout(0.3),\n            nn.Linear(256, 128),\n            nn.ReLU(),\n            nn.Dropout(0.3),\n            nn.Linear(128, num_classes)\n        )\n\n        # 权重初始化（与训练一致）\n        self._initialize_weights()\n\n    def _initialize_weights(self):\n        for m in self.modules():\n            if isinstance(m, nn.Conv1d) or isinstance(m, nn.Linear):\n                nn.init.normal_(m.weight, mean=0.0, std=0.005)\n                if m.bias is not None:\n                    nn.init.constant_(m.bias, 0.01)\n            elif isinstance(m, nn.LSTM):\n                for name, param in m.named_parameters():\n                    if 'weight' in name:\n                        nn.init.normal_(param, mean=0.0, std=0.005)\n                    elif 'bias' in name:\n                        nn.init.constant_(param, 0.01)\n\n    def forward(self, demo_feat: torch.Tensor, seq_feat: torch.Tensor) -> torch.Tensor:\n        # 与训练一致的前向传播逻辑\n        cnn_out = self.cnn(seq_feat)\n        cnn_out = torch.clip(cnn_out, -10, 10)\n        \n        lstm_in = cnn_out.transpose(1, 2)\n        lstm_out, (hn, _) = self.lstm(lstm_in)\n        seq_final_feat = torch.clip(hn[-1], -10, 10)\n        \n        subj_final_feat = self.subj_encoder(demo_feat)\n        subj_final_feat = torch.clip(subj_final_feat, -10, 10)\n        \n        combined_feat = torch.cat([seq_final_feat, subj_final_feat], dim=1)\n        combined_feat = torch.clip(combined_feat, -10, 10)\n        \n        return self.classifier(combined_feat)\n\n\n# --------------------------\n# 6. 核心评估流程（分步骤：数据加载→模型加载→验证集评估→测试集分析）\n# --------------------------\ndef main():\n    # --------------------------\n    # 步骤1：加载基础数据（训练/验证/测试）\n    # --------------------------\n    print(\"\\n\" + \"=\"*60)\n    print(\"步骤1：加载数据并初始化数据集\")\n    print(\"=\"*60)\n    try:\n        # 加载训练集（用于划分验证集）和测试集\n        train_demo = pd.read_csv(f\"{DATA_FOLDER}/train_demographics.csv\")\n        train_sensor = pd.read_csv(f\"{DATA_FOLDER}/train.csv\")\n        test_demo = pd.read_csv(f\"{DATA_FOLDER}/test_demographics.csv\")\n        test_sensor = pd.read_csv(f\"{DATA_FOLDER}/test.csv\")\n        # 加载预测结果\n        pred_df = pd.read_parquet(PRED_FILE_PATH)\n        print(f\"✅ 数据加载完成：\")\n        print(f\"  - 训练集序列数：{train_sensor['sequence_id'].nunique()}\")\n        print(f\"  - 测试集序列数：{test_sensor['sequence_id'].nunique()}\")\n        print(f\"  - 预测结果序列数：{pred_df['sequence_id'].nunique()}\")\n    except Exception as e:\n        raise ValueError(f\"❌ 数据加载失败：{str(e)}，请检查文件路径！\")\n\n    # 初始化训练数据集（含标签），用于划分验证集\n    full_train_dataset = SubjectDataset(\n        data_demo=train_demo,\n        data_sensor=train_sensor,\n        have_labels=True\n    )\n    # 划分验证集（与训练代码完全一致：8:2拆分+固定种子）\n    torch.manual_seed(RANDOM_SEED)\n    train_size = int(0.8 * len(full_train_dataset))\n    val_dataset = random_split(full_train_dataset, [train_size, len(full_train_dataset)-train_size])[1]\n    # 初始化验证集加载器\n    val_loader = DataLoader(\n        val_dataset,\n        batch_size=BATCH_SIZE,\n        shuffle=False,\n        collate_fn=custom_collate,\n        num_workers=0,\n        pin_memory=False\n    )\n    print(f\"✅ 验证集初始化完成：{len(val_dataset)} 个样本\")\n\n    # --------------------------\n    # 步骤2：加载模型权重并初始化模型\n    # --------------------------\n    print(\"\\n\" + \"=\"*60)\n    print(\"步骤2：加载模型权重并验证\")\n    print(\"=\"*60)\n    try:\n        # 从验证集样本获取输入维度（与训练一致）\n        sample_demo, sample_seq, _ = val_dataset[0]\n        seq_feat_dim = sample_seq.shape[0]  # 传感器特征维度（如333）\n        subj_feat_dim = sample_demo.shape[0]  # 人口特征维度（如7）\n        num_classes = len(gesture_to_id)  # 类别数（如18）\n        \n        # 初始化模型（与训练一致的参数）\n        model = HybridClassifier(\n            seq_feat_dim=seq_feat_dim,\n            subj_feat_dim=subj_feat_dim,\n            num_classes=num_classes\n        )\n        # 加载权重\n        model.load_state_dict(torch.load(MODEL_WEIGHT_PATH, map_location=DEVICE))\n        model.to(DEVICE)\n        model.eval()  # 切换评估模式（禁用Dropout/BatchNorm更新）\n        print(f\"✅ 模型加载完成：\")\n        print(f\"  - 传感器特征维度：{seq_feat_dim}\")\n        print(f\"  - 人口特征维度：{subj_feat_dim}\")\n        print(f\"  - 类别数：{num_classes}\")\n        print(f\"  - 模型总参数量：{sum(p.numel() for p in model.parameters() if p.requires_grad):,}\")\n    except Exception as e:\n        raise ValueError(f\"❌ 模型加载失败：{str(e)}，请确保权重文件路径正确且模型结构与训练一致！\")\n\n    # --------------------------\n    # 步骤3：验证集评估（核心指标：准确率、F1、混淆矩阵）\n    # --------------------------\n    print(\"\\n\" + \"=\"*60)\n    print(\"步骤3：验证集性能评估\")\n    print(\"=\"*60)\n    # 初始化损失函数（与训练一致）\n    criterion = nn.CrossEntropyLoss()\n    # 存储所有真实标签和预测标签\n    all_true = []\n    all_pred = []\n    total_loss = 0.0\n\n    with torch.no_grad():  # 关闭梯度计算，节省内存\n        for batch in tqdm(val_loader, desc=\"评估验证集\"):\n            demo_batch, seq_batch, label_batch = batch\n            # 数据移至设备并处理异常值\n            demo_batch = demo_batch.to(DEVICE)\n            seq_batch = process_anomalies(seq_batch).to(DEVICE)\n            label_batch = label_batch.to(DEVICE)\n            \n            # 模型预测\n            outputs = model(demo_batch, seq_batch)\n            loss = criterion(outputs, label_batch)\n            total_loss += loss.item()\n            \n            # 转换为numpy格式，便于计算指标\n            preds = outputs.argmax(dim=1).cpu().numpy()\n            trues = label_batch.cpu().numpy()\n            all_true.extend(trues)\n            all_pred.extend(preds)\n\n    # 计算核心指标\n    val_acc = accuracy_score(all_true, all_pred)\n    val_f1_macro = f1_score(all_true, all_pred, average=\"macro\")\n    val_f1_micro = f1_score(all_true, all_pred, average=\"micro\")\n    avg_loss = total_loss / len(val_loader)\n\n    # 打印核心指标\n    print(f\"\\n📊 验证集核心指标：\")\n    print(f\"  - 平均损失：{avg_loss:.4f}\")\n    print(f\"  - 准确率（Accuracy）：{val_acc:.4f}\")\n    print(f\"  - 宏F1（Macro-F1）：{val_f1_macro:.4f}\")\n    print(f\"  - 微F1（Micro-F1）：{val_f1_micro:.4f}\")\n\n    # 打印分类报告（每类行为的详细指标）\n    print(f\"\\n📋 验证集分类报告（按行为类别）：\")\n    report = classification_report(\n        all_true, all_pred,\n        target_names=[id_to_gesture[i] for i in range(num_classes)],\n        output_dict=False,\n        zero_division=0\n    )\n    print(report)\n\n    # 绘制混淆矩阵（归一化，便于观察错误分布）\n    plt.figure(figsize=(16, 14))\n    cm = confusion_matrix(all_true, all_pred)\n    cm_norm = cm.astype(\"float\") / cm.sum(axis=1)[:, np.newaxis]  # 按真实标签归一化\n\n    sns.heatmap(\n        cm_norm,\n        annot=True,\n        fmt=\".2f\",\n        cmap=\"YlOrRd\",\n        xticklabels=[id_to_gesture[i] for i in range(num_classes)],\n        yticklabels=[id_to_gesture[i] for i in range(num_classes)],\n        annot_kws={\"fontsize\": 6, \"fontweight\": \"bold\"},\n        linewidths=0.3,\n        linecolor=\"white\"\n    )\n    plt.xticks(rotation=45, ha=\"right\", fontsize=8)\n    plt.yticks(rotation=0, fontsize=8)\n    plt.xlabel(\"预测行为（Predicted Gesture）\", fontsize=12, fontweight=\"bold\")\n    plt.ylabel(\"真实行为（True Gesture）\", fontsize=12, fontweight=\"bold\")\n    plt.title(\n        f\"验证集混淆矩阵（归一化）\\n\"\n        f\"准确率：{val_acc:.4f} | 宏F1：{val_f1_macro:.4f}\",\n        fontsize=14, pad=20, fontweight=\"bold\"\n    )\n    plt.tight_layout()\n    plt.show()\n\n    # --------------------------\n    # 步骤4：测试集预测合理性分析（类别分布对比）\n    # --------------------------\n    print(\"\\n\" + \"=\"*60)\n    print(\"步骤4：测试集预测合理性分析\")\n    print(\"=\"*60)\n    # 合并测试集与预测结果（仅保留匹配的序列）\n    test_with_pred = pd.merge(\n        test_sensor[[\"sequence_id\"]].drop_duplicates(),  # 仅保留测试集序列ID\n        pred_df[[\"sequence_id\", \"predicted_gesture\"]],\n        on=\"sequence_id\",\n        how=\"inner\"\n    )\n    # 检查匹配率\n    match_rate = len(test_with_pred) / test_sensor[\"sequence_id\"].nunique()\n    print(f\"✅ 测试集与预测结果匹配：\")\n    print(f\"  - 匹配序列数：{len(test_with_pred)}\")\n    print(f\"  - 匹配率：{match_rate:.2%}\")\n    if match_rate < 0.9:\n        print(f\"⚠️ 警告：匹配率低于90%，可能存在序列ID不匹配！\")\n\n    # 类别分布对比（训练集 vs 测试集预测）\n    # 训练集类别分布（按序列数计算，与测试集一致）\n    train_seq_dist = train_sensor.groupby(\"sequence_id\")[\"gesture\"].first().value_counts(normalize=True) * 100\n    # 测试集预测类别分布\n    test_pred_dist = test_with_pred[\"predicted_gesture\"].value_counts(normalize=True) * 100\n\n    # 合并分布数据（确保所有类别都显示）\n    all_gestures = sorted(gesture_to_id.keys())\n    dist_compare = pd.DataFrame({\n        \"训练集（序列占比）\": train_seq_dist.reindex(all_gestures, fill_value=0),\n        \"测试集预测（序列占比）\": test_pred_dist.reindex(all_gestures, fill_value=0)\n    })\n\n    # 绘制类别分布对比图\n    plt.figure(figsize=(16, 8))\n    dist_compare.plot(\n        kind=\"bar\",\n        width=0.8,\n        color=[\"#2E86AB\", \"#A23B72\"],\n        alpha=0.8\n    )\n    plt.title(\n        \"训练集与测试集预测类别分布对比（按序列数占比）\",\n        fontsize=14, pad=20, fontweight=\"bold\"\n    )\n    plt.xlabel(\"行为类别（Gesture）\", fontsize=12)\n    plt.ylabel(\"占比（%）\", fontsize=12)\n    plt.xticks(rotation=45, ha=\"right\", fontsize=8)\n    plt.legend(fontsize=11, loc=\"upper right\")\n    plt.grid(axis=\"y\", alpha=0.3, linestyle=\"--\")\n    plt.tight_layout()\n    plt.show()\n\n    # 计算KL散度（衡量分布相似度：值越小越相似）\n    def kl_div(p, q):\n        p = np.array(p) + 1e-10  # 避免log(0)\n        q = np.array(q) + 1e-10\n        return np.sum(p * np.log(p / q))\n\n    kl_value = kl_div(\n        dist_compare[\"训练集（序列占比）\"] / 100,\n        dist_compare[\"测试集预测（序列占比）\"] / 100\n    )\n    print(f\"\\n📊 分布相似度分析：\")\n    print(f\"  - KL散度（KL Divergence）：{kl_value:.4f}\")\n    print(f\"    （注：KL值<0.5表示分布高度相似，0.5~1.0表示中度相似，>1.0表示差异较大）\")\n\n    print(\"\\n\" + \"=\"*60)\n    print(\"所有评估步骤完成！\")\n    print(\"=\"*60)\n\n\nif __name__ == \"__main__\":\n    # 全局变量：类别映射（由Dataset初始化时生成）\n    gesture_to_id = {}\n    id_to_gesture = {}\n    main()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-08T03:34:55.611388Z","iopub.execute_input":"2025-09-08T03:34:55.612840Z","iopub.status.idle":"2025-09-08T03:45:51.586769Z","shell.execute_reply.started":"2025-09-08T03:34:55.612801Z","shell.execute_reply":"2025-09-08T03:45:51.585288Z"}},"outputs":[],"execution_count":null}]}