{"cells":[{"cell_type":"markdown","metadata":{},"source":"# CMI-BFRB CNN-LSTM Training\n\nCNN-LSTMモデルによる時系列センサーデータからの行動分類。\n\n**環境**: Kaggle Notebook (GPU)\n\n**Version**: v2.0.0"},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":"# 環境確認\nimport torch\nprint(f\"PyTorch version: {torch.__version__}\")\nprint(f\"CUDA available: {torch.cuda.is_available()}\")\nif torch.cuda.is_available():\n    print(f\"CUDA device: {torch.cuda.get_device_name(0)}\")\n    print(f\"CUDA version: {torch.version.cuda}\")"},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":"import os\nimport sys\nimport pandas as pd\nimport numpy as np\nimport polars as pl\nfrom pathlib import Path\nimport warnings\nwarnings.filterwarnings('ignore')\n\n# Kaggle環境でのパス設定\nif os.path.exists('/kaggle/input'):\n    DATA_DIR = Path('/kaggle/input/cmi-detect-behavior-with-sensor-data')\n    UTILS_DIR = Path('/kaggle/input/cmi-bfrb-utils-v120')\n    OUTPUT_DIR = Path('/kaggle/working')\nelse:\n    # ローカル環境（テスト用）\n    DATA_DIR = Path('../../experiments/data')\n    UTILS_DIR = Path('../../baseline/src')\n    OUTPUT_DIR = Path('../../models/cnnlstm')\n\nsys.path.append(str(UTILS_DIR))\n\n# utilsのインポート\nfrom utils import (\n    load_data,\n    create_features,\n    preprocess_data,\n    evaluate_predictions,\n    LABEL_MAPPING,\n    BFRB_LABELS\n)\n\nprint(\"環境設定完了\")"},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":"# PyTorch関連のインポート\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nfrom sklearn.model_selection import GroupKFold\nfrom sklearn.preprocessing import StandardScaler\nimport time\nfrom tqdm.auto import tqdm\nimport gc"},{"cell_type":"markdown","metadata":{},"source":"## データ読み込みと前処理"},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":"# データ読み込み\nprint(\"データ読み込み中...\")\ntrain_data, test_data = load_data(DATA_DIR)\nprint(f\"Train shape: {train_data.shape}\")\nprint(f\"Test shape: {test_data.shape}\")\n\n# 時系列データの準備\n# CNN-LSTMでは生のセンサーデータを使用\nsensor_cols = [col for col in train_data.columns \n               if col.startswith(('acc_', 'rot_', 'tof_', 'thm_'))]\nprint(f\"センサー数: {len(sensor_cols)}\")"},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":"# シーケンスデータセットクラス\nclass SequenceDataset(Dataset):\n    def __init__(self, data, sensor_cols, label_col='label', \n                 sequence_length=100, overlap=50, scaler=None, is_train=True):\n        self.data = data\n        self.sensor_cols = sensor_cols\n        self.label_col = label_col\n        self.sequence_length = sequence_length\n        self.overlap = overlap\n        self.is_train = is_train\n        self.scaler = scaler\n        \n        # シーケンスの作成\n        self.sequences = []\n        self.labels = []\n        self.subjects = []\n        \n        for seq_id in data['sequence_id'].unique():\n            seq_data = data[data['sequence_id'] == seq_id]\n            if len(seq_data) < sequence_length:\n                continue\n                \n            # スライディングウィンドウでシーケンスを作成\n            for start in range(0, len(seq_data) - sequence_length + 1, \n                             sequence_length - overlap):\n                end = start + sequence_length\n                seq_slice = seq_data.iloc[start:end]\n                \n                # センサーデータを抽出\n                sensor_data = seq_slice[sensor_cols].values\n                \n                # ラベル（最頻値）\n                if is_train:\n                    label = seq_slice[label_col].mode()[0]\n                    self.labels.append(LABEL_MAPPING[label])\n                \n                self.sequences.append(sensor_data)\n                self.subjects.append(seq_slice['subject_id'].iloc[0])\n        \n        # NumPy配列に変換\n        self.sequences = np.array(self.sequences, dtype=np.float32)\n        if is_train:\n            self.labels = np.array(self.labels, dtype=np.int64)\n        self.subjects = np.array(self.subjects)\n        \n        # 正規化\n        if self.scaler is None and is_train:\n            self.scaler = StandardScaler()\n            n_samples, n_timesteps, n_features = self.sequences.shape\n            sequences_reshaped = self.sequences.reshape(-1, n_features)\n            self.scaler.fit(sequences_reshaped)\n        \n        if self.scaler is not None:\n            n_samples, n_timesteps, n_features = self.sequences.shape\n            sequences_reshaped = self.sequences.reshape(-1, n_features)\n            sequences_normalized = self.scaler.transform(sequences_reshaped)\n            self.sequences = sequences_normalized.reshape(n_samples, n_timesteps, n_features)\n    \n    def __len__(self):\n        return len(self.sequences)\n    \n    def __getitem__(self, idx):\n        if self.is_train:\n            return self.sequences[idx], self.labels[idx]\n        else:\n            return self.sequences[idx]"},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":"# CNN-LSTMモデルの定義（シンプル版）\nclass CNNLSTMSimple(nn.Module):\n    def __init__(self, input_dim=334, num_classes=18, \n                 hidden_size=128, num_layers=2, dropout=0.3):\n        super(CNNLSTMSimple, self).__init__()\n        \n        # 1D CNN for feature extraction\n        self.conv1 = nn.Conv1d(input_dim, 64, kernel_size=5, padding=2)\n        self.conv2 = nn.Conv1d(64, 128, kernel_size=3, padding=1)\n        self.pool = nn.MaxPool1d(2)\n        self.dropout = nn.Dropout(dropout)\n        \n        # LSTM\n        self.lstm = nn.LSTM(\n            input_size=128,\n            hidden_size=hidden_size,\n            num_layers=num_layers,\n            batch_first=True,\n            dropout=dropout if num_layers > 1 else 0,\n            bidirectional=True\n        )\n        \n        # Classifier\n        self.fc = nn.Linear(hidden_size * 2, num_classes)\n        \n    def forward(self, x):\n        # CNN expects (batch, channels, length)\n        x = x.transpose(1, 2)  # (batch, input_dim, seq_len)\n        \n        # CNN layers\n        x = torch.relu(self.conv1(x))\n        x = self.pool(x)\n        x = torch.relu(self.conv2(x))\n        x = self.pool(x)\n        x = self.dropout(x)\n        \n        # Prepare for LSTM: (batch, seq_len, features)\n        x = x.transpose(1, 2)\n        \n        # LSTM\n        lstm_out, _ = self.lstm(x)\n        \n        # Use the last output\n        x = lstm_out[:, -1, :]\n        \n        # Classification\n        output = self.fc(x)\n        \n        return output"},{"cell_type":"markdown","metadata":{},"source":"## 学習関数"},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":"def train_epoch(model, dataloader, criterion, optimizer, device):\n    model.train()\n    running_loss = 0.0\n    correct = 0\n    total = 0\n    \n    for batch_idx, (data, targets) in enumerate(tqdm(dataloader, desc='Training')):\n        data, targets = data.to(device), targets.to(device)\n        \n        optimizer.zero_grad()\n        outputs = model(data)\n        loss = criterion(outputs, targets)\n        loss.backward()\n        optimizer.step()\n        \n        running_loss += loss.item()\n        _, predicted = outputs.max(1)\n        total += targets.size(0)\n        correct += predicted.eq(targets).sum().item()\n    \n    epoch_loss = running_loss / len(dataloader)\n    accuracy = 100. * correct / total\n    \n    return epoch_loss, accuracy\n\n\ndef validate_epoch(model, dataloader, criterion, device):\n    model.eval()\n    running_loss = 0.0\n    correct = 0\n    total = 0\n    all_preds = []\n    all_targets = []\n    \n    with torch.no_grad():\n        for batch_idx, (data, targets) in enumerate(tqdm(dataloader, desc='Validation')):\n            data, targets = data.to(device), targets.to(device)\n            \n            outputs = model(data)\n            loss = criterion(outputs, targets)\n            \n            running_loss += loss.item()\n            _, predicted = outputs.max(1)\n            total += targets.size(0)\n            correct += predicted.eq(targets).sum().item()\n            \n            all_preds.extend(predicted.cpu().numpy())\n            all_targets.extend(targets.cpu().numpy())\n    \n    epoch_loss = running_loss / len(dataloader)\n    accuracy = 100. * correct / total\n    \n    # カスタムF1スコアの計算\n    all_preds = np.array(all_preds)\n    all_targets = np.array(all_targets)\n    \n    # ラベル名に変換\n    inv_mapping = {v: k for k, v in LABEL_MAPPING.items()}\n    pred_labels = [inv_mapping[p] for p in all_preds]\n    true_labels = [inv_mapping[t] for t in all_targets]\n    \n    _, f1_score = evaluate_predictions(true_labels, pred_labels)\n    \n    return epoch_loss, accuracy, f1_score"},{"cell_type":"markdown","metadata":{},"source":"## Cross-Validation学習"},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":"# 学習設定\nSEQUENCE_LENGTH = 100\nOVERLAP = 50\nBATCH_SIZE = 32\nNUM_EPOCHS = 30\nLEARNING_RATE = 0.001\nWEIGHT_DECAY = 0.0001\nN_SPLITS = 5\n\n# デバイス設定\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nprint(f\"使用デバイス: {device}\")\n\n# Cross-validation\ngkf = GroupKFold(n_splits=N_SPLITS)\ncv_scores = []\nfold_models = []\n\nfor fold, (train_idx, val_idx) in enumerate(gkf.split(train_data, \n                                                      train_data['label'], \n                                                      train_data['subject_id'])):\n    print(f\"\\n===== Fold {fold + 1}/{N_SPLITS} =====\")\n    \n    # データ分割\n    train_fold = train_data.iloc[train_idx].reset_index(drop=True)\n    val_fold = train_data.iloc[val_idx].reset_index(drop=True)\n    \n    print(f\"Train subjects: {train_fold['subject_id'].nunique()}\")\n    print(f\"Val subjects: {val_fold['subject_id'].nunique()}\")\n    \n    # データセット作成\n    train_dataset = SequenceDataset(\n        train_fold, sensor_cols, sequence_length=SEQUENCE_LENGTH, \n        overlap=OVERLAP, is_train=True\n    )\n    val_dataset = SequenceDataset(\n        val_fold, sensor_cols, sequence_length=SEQUENCE_LENGTH,\n        overlap=SEQUENCE_LENGTH,  # バリデーションではオーバーラップなし\n        scaler=train_dataset.scaler, is_train=True\n    )\n    \n    print(f\"Train sequences: {len(train_dataset)}\")\n    print(f\"Val sequences: {len(val_dataset)}\")\n    \n    # DataLoader\n    train_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, \n                            shuffle=True, num_workers=2)\n    val_loader = DataLoader(val_dataset, batch_size=BATCH_SIZE, \n                          shuffle=False, num_workers=2)\n    \n    # モデル初期化\n    model = CNNLSTMSimple(input_dim=len(sensor_cols), num_classes=18)\n    model = model.to(device)\n    \n    # 損失関数とオプティマイザ\n    criterion = nn.CrossEntropyLoss()\n    optimizer = optim.Adam(model.parameters(), lr=LEARNING_RATE, \n                          weight_decay=WEIGHT_DECAY)\n    scheduler = optim.lr_scheduler.ReduceLROnPlateau(\n        optimizer, mode='max', factor=0.5, patience=3, verbose=True\n    )\n    \n    # 学習\n    best_f1 = 0\n    best_model_state = None\n    patience = 7\n    patience_counter = 0\n    \n    for epoch in range(NUM_EPOCHS):\n        print(f\"\\nEpoch {epoch + 1}/{NUM_EPOCHS}\")\n        \n        # 学習\n        train_loss, train_acc = train_epoch(\n            model, train_loader, criterion, optimizer, device\n        )\n        \n        # 検証\n        val_loss, val_acc, val_f1 = validate_epoch(\n            model, val_loader, criterion, device\n        )\n        \n        # 学習率調整\n        scheduler.step(val_f1)\n        \n        print(f\"Train Loss: {train_loss:.4f}, Train Acc: {train_acc:.2f}%\")\n        print(f\"Val Loss: {val_loss:.4f}, Val Acc: {val_acc:.2f}%, Val F1: {val_f1:.4f}\")\n        \n        # ベストモデルの保存\n        if val_f1 > best_f1:\n            best_f1 = val_f1\n            best_model_state = model.state_dict().copy()\n            patience_counter = 0\n            print(f\"New best model! F1: {best_f1:.4f}\")\n        else:\n            patience_counter += 1\n            \n        # Early stopping\n        if patience_counter >= patience:\n            print(f\"Early stopping at epoch {epoch + 1}\")\n            break\n    \n    # ベストモデルを復元\n    model.load_state_dict(best_model_state)\n    fold_models.append(model)\n    cv_scores.append(best_f1)\n    \n    print(f\"\\nFold {fold + 1} Best F1: {best_f1:.4f}\")\n    \n    # メモリクリア\n    del train_dataset, val_dataset, train_loader, val_loader\n    gc.collect()\n    torch.cuda.empty_cache()"},{"cell_type":"markdown","metadata":{},"source":"## 結果サマリー"},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":"# CV結果\nprint(\"\\n===== Cross-Validation Results =====\")\nprint(f\"CV Scores: {cv_scores}\")\nprint(f\"Mean CV Score: {np.mean(cv_scores):.4f} (+/- {np.std(cv_scores):.4f})\")\n\n# モデル保存\nprint(\"\\nモデル保存中...\")\nfor i, model in enumerate(fold_models):\n    torch.save({\n        'model_state_dict': model.state_dict(),\n        'cv_score': cv_scores[i],\n        'input_dim': len(sensor_cols),\n        'sequence_length': SEQUENCE_LENGTH,\n        'scaler': train_dataset.scaler if i == 0 else None\n    }, OUTPUT_DIR / f'cnnlstm_fold_{i}.pth')\n\nprint(\"学習完了！\")"},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":"# 実験結果の保存\nexperiment_results = {\n    'model_type': 'CNN-LSTM Simple',\n    'version': 'v2.0.0',\n    'cv_scores': cv_scores,\n    'mean_cv_score': np.mean(cv_scores),\n    'std_cv_score': np.std(cv_scores),\n    'sequence_length': SEQUENCE_LENGTH,\n    'input_dim': len(sensor_cols),\n    'num_epochs': NUM_EPOCHS,\n    'batch_size': BATCH_SIZE,\n    'learning_rate': LEARNING_RATE,\n    'device': str(device),\n    'training_environment': 'Kaggle GPU (Tesla P100)'\n}\n\nimport json\nwith open(OUTPUT_DIR / 'experiment_results.json', 'w') as f:\n    json.dump(experiment_results, f, indent=2)\n\nprint(\"実験結果を保存しました。\")"}],"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"codemirror_mode":{"name":"ipython","version":3},"file_extension":".py","mimetype":"text/x-python","name":"python","nbconvert_exporter":"python","pygments_lexer":"ipython3","version":"3.8.0"}},"nbformat":4,"nbformat_minor":4}