{"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":"gpu","dataSources":[{"sourceId":102335,"databundleVersionId":12518947,"sourceType":"competition"}],"dockerImageVersionId":31040,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"from scipy.spatial.transform import Rotation as R\nimport os\nimport pandas as pd\nimport numpy as np\nfrom sklearn.model_selection import GroupKFold\nfrom sklearn.preprocessing import LabelEncoder\nfrom sklearn.metrics import f1_score\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nimport joblib\nimport warnings\nimport polars as pl\nimport kaggle_evaluation.cmi_inference_server\nfrom torch.nn.utils.rnn import pad_sequence\nfrom torch.utils.data import Dataset\nfrom torch.utils.data import DataLoader\nfrom sklearn.model_selection import GroupShuffleSplit\nfrom sklearn.model_selection import StratifiedKFold\nfrom sklearn.model_selection import GroupKFold\nimport torch.optim as optim\nimport random\nimport torch\nimport torch.nn as nn\nimport numpy as np\nfrom torch.nn.utils.rnn import pad_sequence\nimport torch.nn.functional as F\n\nDATA_DIR = \"/kaggle/input/cmi-detect-behavior-with-sensor-data/\"\nTRAIN_PATH = DATA_DIR + \"train.csv\"\nTRAIN_DEMO_PATH = DATA_DIR + \"train_demographics.csv\"\nTEST_PATH = DATA_DIR + \"test.csv\"\nTEST_DEMO_PATH = DATA_DIR + \"test_demographics.csv\"\n\ntrain_df = pd.read_csv(TRAIN_PATH)\ntest_df = pd.read_csv(TEST_PATH)\ntrain_demo_df = pd.read_csv(TRAIN_DEMO_PATH)\ntest_demo_df = pd.read_csv(TEST_DEMO_PATH)\n\nacc_cols = ['acc_x', 'acc_y', 'acc_z']\nrot_cols = ['rot_w', 'rot_x', 'rot_y', 'rot_z']\nthm_cols = ['thm_1', 'thm_2', 'thm_3', 'thm_4', 'thm_5']\nimu_cols = acc_cols + rot_cols\n\nimu_features = [\n    'acc_x', 'acc_y', 'acc_z',\n    'rot_w', 'rot_x', 'rot_y', 'rot_z',\n    'acc_mag', 'acc_mag_jerk',\n    'linear_acc_x', 'linear_acc_y', 'linear_acc_z',\n    'linear_acc_mag', 'linear_acc_mag_jerk',\n    'rot_angle', 'rot_angle_vel',\n    'angular_vel_x', 'angular_vel_y', 'angular_vel_z',\n]\n\ndef add_const_vector(acc, quat, vector):\n    N = acc.shape[0]\n    new_acc = np.zeros_like(acc)\n\n    for i in range(N):\n        quat_now = quat[i]\n        \n        if np.all(np.isnan(quat_now)) or np.all(np.isclose(quat_now, 0)):\n            new_acc[i, :] = acc[i, :]\n        try:\n            rotation = R.from_quat(quat_now)\n            vector_rotated = rotation.apply(vector, inverse=True)\n            new_acc[i, :] = acc[i, :] - vector_rotated\n        except ValueError:\n            new_acc[i, :] = acc[i, :]\n             \n    return new_acc\n\n\ndef remove_gravity_from_acc(acc_data, rot_data):\n    if isinstance(acc_data, pd.DataFrame):\n        acc = acc_data[['acc_x', 'acc_y', 'acc_z']].values\n    else:\n        acc = acc_data\n\n    if isinstance(rot_data, pd.DataFrame):\n        quat = rot_data[['rot_x', 'rot_y', 'rot_z', 'rot_w']].values\n    else:\n        quat = rot_data\n        \n    gravity_world = np.array([0, 0, 9.81])\n    return add_const_vector(acc, quat, gravity_world)\n\ndef calculate_angular_velocity_from_quat(rot_data, time_delta=1/50):\n    if isinstance(rot_data, pd.DataFrame):\n        quat_values = rot_data[['rot_x', 'rot_y', 'rot_z', 'rot_w']].values\n    else:\n        quat_values = rot_data\n\n    num_samples = quat_values.shape[0]\n    angular_vel = np.zeros((num_samples, 3))\n\n    for i in range(num_samples - 1):\n        q_t = quat_values[i]\n        q_t_plus_dt = quat_values[i+1]\n\n        if np.all(np.isnan(q_t)) or np.all(np.isclose(q_t, 0)) or \\\n           np.all(np.isnan(q_t_plus_dt)) or np.all(np.isclose(q_t_plus_dt, 0)):\n            continue\n\n        try:\n            rot_t = R.from_quat(q_t)\n            rot_t_plus_dt = R.from_quat(q_t_plus_dt)\n\n            # Calculate the relative rotation\n            delta_rot = rot_t.inv() * rot_t_plus_dt\n        \n            angular_vel[i, :] = delta_rot.as_rotvec() / time_delta\n        except ValueError:\n            # If quaternion is invalid, angular velocity remains zero\n            pass\n            \n    return angular_vel\n\n\ndef get_linear_accel(df):\n    res = remove_gravity_from_acc(\n        df[['acc_x', 'acc_y', 'acc_z']],\n        df[['rot_x', 'rot_y', 'rot_z', 'rot_w']]\n    )\n    res = pd.DataFrame(res, columns=['linear_acc_x', 'linear_acc_y', 'linear_acc_z'], index=df.index)\n    return res\n\ndef calc_angular_velocity(df):\n    res = calculate_angular_velocity_from_quat( df[['rot_x', 'rot_y', 'rot_z', 'rot_w']] )\n    res = pd.DataFrame(res, columns=['angular_vel_x', 'angular_vel_y', 'angular_vel_z'], index=df.index)\n    return res\n\ndef feature_engineering(df):\n    df = df[imu_cols + thm_cols + [\"sequence_id\", \"subject\", \"label\"]]\n\n    df['acc_mag'] = np.sqrt(df['acc_x']**2 + df['acc_y']**2 + df['acc_z']**2)\n    df['acc_mag_jerk'] = df.groupby('sequence_id')['acc_mag'].diff().fillna(0)\n    \n    linear_accel_df = df.groupby('sequence_id').apply(get_linear_accel, include_groups=False)\n    linear_accel_df = linear_accel_df.droplevel('sequence_id')\n    df = df.join(linear_accel_df)\n\n    df['linear_acc_mag'] = np.sqrt(df['linear_acc_x']**2 + df['linear_acc_y']**2 + df['linear_acc_z']**2)\n    df['linear_acc_mag_jerk'] = df.groupby('sequence_id')['linear_acc_mag'].diff().fillna(0)\n\n    df['rot_angle'] = 2 * np.arccos(df['rot_w'].clip(-1, 1))\n    df['rot_angle_vel'] = df.groupby('sequence_id')['rot_angle'].diff().fillna(0)\n    \n    angular_velocity_df = df.groupby('sequence_id').apply(calc_angular_velocity, include_groups=False)\n    angular_velocity_df = angular_velocity_df.droplevel('sequence_id')\n    df = df.join(angular_velocity_df)\n    \n    df[imu_features] = df[imu_features].ffill().bfill().fillna(0).values.astype('float32')\n    return df\n\nle_gesture = LabelEncoder()\ntrain_df[\"label\"] = le_gesture.fit_transform(train_df[\"gesture\"])\n\nseq_time = {}\nfor seq_id, group in train_df.groupby(\"sequence_id\"):\n    behavior_cnt = group['behavior'].value_counts()\n    move_to_target = behavior_cnt.get('Moves hand to target location', 0) + behavior_cnt.get('Relaxes and moves hand to target location', 0)\n    begin_gesture = move_to_target + behavior_cnt.get('Hand at target location', 0)\n    seq_time[seq_id] = (move_to_target, begin_gesture)\n\ntrain_df = feature_engineering(train_df)\nsequence_ids = train_df[\"sequence_id\"].unique()\n\nbfrb_gestures = [\n    'Above ear - pull hair',\n    'Cheek - pinch skin',\n    'Eyebrow - pull hair',\n    'Eyelash - pull hair',\n    'Forehead - pull hairline',\n    'Forehead - scratch',\n    'Glasses on/off',\n    'Neck - pinch skin',\n    'Neck - scratch',\n    'Pinch knee/leg skin',\n    'Scratch knee/leg skin'\n]\n\nbfrb_labels = le_gesture.transform(bfrb_gestures)\n\ndef process_df(df: pd.DataFrame):\n    df = df.ffill().bfill()\n    \n    label_map = {\n        seq_id: int(group[\"label\"].iloc[0])\n        for seq_id, group in df.groupby(\"sequence_id\")\n    }\n    \n    seq_map = {\n        seq_id: torch.tensor(group.drop(columns=[\"sequence_id\", \"subject\", \"label\"]).values, dtype=torch.float32)\n        for seq_id, group in df.groupby(\"sequence_id\")\n    }\n    return seq_map, label_map\n\nclass SensorDataset(Dataset):\n    def __init__(self, df: pd.DataFrame, transforms=None):\n        self.seq_map, self.label_map = process_df(df)\n        self.sequence_ids = list(df[\"sequence_id\"].unique())\n        self.transforms = transforms\n        \n    def __len__(self):\n        return len(self.sequence_ids)\n\n    def __getitem__(self, idx):\n        seq_id = self.sequence_ids[idx]\n        seq = self.seq_map[seq_id]\n        start = seq_time[seq_id][0]\n\n        if self.transforms:\n            seq = self.transforms(seq, start)\n            \n        label = self.label_map[seq_id]\n        return seq, label","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-08-27T13:44:02.223823Z","iopub.execute_input":"2025-08-27T13:44:02.224106Z","iopub.status.idle":"2025-08-27T13:46:17.946581Z","shell.execute_reply.started":"2025-08-27T13:44:02.224082Z","shell.execute_reply":"2025-08-27T13:46:17.945748Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"max_len = 50\n\nclass TransformerClassifier(nn.Module):\n    def __init__(self, input_dim=len(imu_features) + 5, d_model=300, nhead=4, num_layers=8, num_classes=18, max_len=max_len):\n        super().__init__()\n        \n        self.input_proj = nn.Linear(input_dim, d_model)\n        self.pos_embedding = nn.Embedding(max_len + 1, d_model)  # +1 для CLS\n        self.cls_token = nn.Parameter(torch.randn(1, 1, d_model))  # обучаемый CLS\n\n        encoder_layer = nn.TransformerEncoderLayer(\n            d_model=d_model, \n            nhead=nhead, \n            dim_feedforward=600, \n            dropout=0.1,\n            batch_first=True\n        )\n        self.transformer = nn.TransformerEncoder(encoder_layer, num_layers=num_layers)\n\n        self.fc = nn.Linear(d_model, num_classes)\n        \n    def forward(self, x, mask):\n        batch_size, seq_len, _ = x.size()\n        x = self.input_proj(x)\n\n        # добавляем CLS-токен в начало\n        cls_tokens = self.cls_token.expand(batch_size, -1, -1)  # (B, 1, d_model)\n        x = torch.cat([cls_tokens, x], dim=1)  # (B, L+1, d_model)\n\n        # позиции (0 для CLS, потом 1...L)\n        positions = torch.arange(seq_len + 1, device=x.device).unsqueeze(0).expand(batch_size, seq_len + 1)\n        pos_emb = self.pos_embedding(positions)\n        x = x + pos_emb\n\n        # расширяем маску (нужно вставить False для CLS)\n        cls_mask = torch.zeros((batch_size, 1), dtype=mask.dtype, device=mask.device)\n        mask = torch.cat([cls_mask, mask], dim=1)  # (B, L+1)\n\n        x = self.transformer(x, src_key_padding_mask=mask)\n\n        # берём выход CLS-токена\n        cls_out = x[:, 0, :]  # (B, d_model)\n\n        out = self.fc(cls_out)\n        return out\n\nclass Augmenter():        \n    def __call__(self, seq, start):\n        aug_seq = self.cut_sequence(seq, start)\n        return aug_seq\n\n    def cut_sequence(self, seq, start):\n        return seq[random.randint(0, start):]\n        \n        \ndef preprocess(seq, start):\n    return seq[start//2:]\n\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nmodel = TransformerClassifier().to(device)\noptimizer = optim.Adam(model.parameters(), lr=1e-4)\nlr_scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='max', factor=0.1, min_lr=1e-6, patience=20, verbose=True)\ncriterion = nn.CrossEntropyLoss(label_smoothing=0.1)\n\ndef collate_fn(batch): \n    sequences = [item[0][-max_len:] for item in batch]  # обрезаем, если длиннее max_len\n    labels = [item[1] for item in batch]\n\n    padded_sequences = pad_sequence(sequences, batch_first=True, padding_value=0.0)\n\n    lengths = [len(seq) for seq in sequences]\n    max_len_batch = padded_sequences.size(1)\n    mask = torch.zeros(len(sequences), max_len_batch, dtype=torch.bool)\n    for i, l in enumerate(lengths):\n        mask[i, l:] = True\n\n    labels = torch.tensor(labels)\n    return padded_sequences, labels, mask","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-27T13:58:47.187755Z","iopub.execute_input":"2025-08-27T13:58:47.188655Z","iopub.status.idle":"2025-08-27T13:58:47.232172Z","shell.execute_reply.started":"2025-08-27T13:58:47.188625Z","shell.execute_reply":"2025-08-27T13:58:47.231416Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"sum(p.numel() for p in model.parameters())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-27T13:58:50.165402Z","iopub.execute_input":"2025-08-27T13:58:50.166122Z","iopub.status.idle":"2025-08-27T13:58:50.172622Z","shell.execute_reply.started":"2025-08-27T13:58:50.166085Z","shell.execute_reply":"2025-08-27T13:58:50.171712Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"splitter = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=42)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-27T13:58:53.300217Z","iopub.execute_input":"2025-08-27T13:58:53.300512Z","iopub.status.idle":"2025-08-27T13:58:53.304307Z","shell.execute_reply.started":"2025-08-27T13:58:53.300492Z","shell.execute_reply":"2025-08-27T13:58:53.303584Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def calculate_score(y, preds):\n    f1_macro = f1_score(y, preds, average='macro')\n    \n    bin_y = np.isin(y, bfrb_labels).astype(int)\n    bin_preds = np.isin(preds, bfrb_labels).astype(int)\n    \n    f1_binary = f1_score(bin_y, bin_preds, average='binary')\n    \n    final_score = (f1_macro + f1_binary) / 2\n    return final_score\n\nfor train_idx, val_idx in splitter.split(train_df, groups=train_df['subject']):\n    tr_df = train_df.iloc[train_idx]\n    train_sequence_subjects = list(tr_df['subject'].unique())\n    tr_demo_df = train_demo_df[train_demo_df['subject'].isin(train_sequence_subjects)]\n    \n    val_df = train_df.iloc[val_idx]\n    val_sequence_subjects  = list(val_df['subject'].unique())\n    val_demo_df = train_demo_df[train_demo_df['subject'].isin(val_sequence_subjects)]\n\n    train_dataset = SensorDataset(tr_df, transforms=Augmenter())\n    val_dataset = SensorDataset(val_df, transforms=preprocess)\n    \n    train_loader = DataLoader(train_dataset, batch_size=256, collate_fn=collate_fn, shuffle=True)\n    val_loader = DataLoader(val_dataset, batch_size=256, collate_fn=collate_fn, shuffle=False)\n\n    train_acc = 0\n    val_acc = 0\n    max_val_acc = 0\n    max_score = 0\n    \n    for epoch in range(120):\n        model.train()\n        lr = lr_scheduler.get_last_lr()[0]\n        train_losses = []\n        for X, y, mask in train_loader:\n            X, y, mask = X.to(device), y.to(device), mask.to(device) \n            optimizer.zero_grad()\n            \n            logits = model(X, mask)\n            train_acc += (torch.argmax(logits, dim=1) == y).sum().item()\n            \n            loss = criterion(logits, y)\n            loss.backward()\n            train_losses.append(loss.item())\n            optimizer.step()\n            \n        train_loss = np.array(train_losses).mean()\n        \n        model.eval()\n        val_losses = []\n        all_preds = []\n        all_y = []\n        \n        with torch.no_grad():\n            for X, y, mask in val_loader:\n                X, y, mask = X.to(device), y.to(device), mask.to(device) \n                \n                logits = model(X, mask) \n                loss = criterion(logits, y) \n                val_losses.append(loss.item()) \n                \n                preds = torch.argmax(logits, dim=1).cpu().numpy() \n                \n                y = y.cpu().numpy() \n                \n                val_acc += (preds == y).sum().item() \n                \n                all_preds.extend(preds) \n                all_y.extend(y) \n                \n        score = calculate_score(all_y, all_preds)\n        val_loss = np.array(val_losses).mean()\n        train_acc /= len(train_dataset)\n        val_acc /= len(val_dataset)\n        \n        max_val_acc = max(max_val_acc, val_acc)\n        max_score = max(max_score, score)\n        print(f\"Epoch: {epoch} | Train loss: {train_loss:.4f} | Val loss: {val_loss:.4f} | Train acc: {train_acc:.2f} | Val acc: {val_acc:.2f} | Score: {score:.2f} | LR: {lr}\")\n\n        all_preds = []\n        all_true = []\n        \n        lr_scheduler.step(score)\n    \n    print(f\"Max Score: {np.mean(max_score):.4f} | Max acc: {max_val_acc:.2f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-27T13:58:54.919570Z","iopub.execute_input":"2025-08-27T13:58:54.920209Z","iopub.status.idle":"2025-08-27T14:06:57.026074Z","shell.execute_reply.started":"2025-08-27T13:58:54.920184Z","shell.execute_reply":"2025-08-27T14:06:57.025214Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\"\"\"\njoblib.dump(model, \"model.pkl\")\njoblib.dump(le_gesture, \"le_gesture.pkl\")\n\"\"\"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\"\"\"\nmodel = joblib.load(\"model.pkl\")\nle_gesture = joblib.load(\"le_gesture.pkl\")\n\"\"\"\n\ndef predict(sequence: pl.DataFrame, demographics: pl.DataFrame) -> str:\n    try:\n        sequence = sequence.to_pandas()\n        demographics = demographics.to_pandas()\n        dataset = SensorDataset(sequence, demographics, submit=True)\n        x_seq, x_demo, _ = dataset[0]\n        x_seq = x_seq.to(device)\n        x_demo = x_demo.to(device) \n        x_seq = x_seq.unsqueeze(0)\n        x_demo = x_demo.unsqueeze(0)\n        model.eval()\n        logits = model(x_seq, x_demo)\n        preds = torch.argmax(logits, dim=1).cpu().numpy()\n        return le_gesture.inverse_transform(preds)[0]\n        \n    except Exception as e:\n        #print(e)\n        return le_gesture.classes_[0]\n\n# Run inference\ninference_server = kaggle_evaluation.cmi_inference_server.CMIInferenceServer(predict)\n\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    )\n\n\"\"\"\ninference_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)\n\"\"\"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}