{"cells":[{"cell_type":"markdown","metadata":{},"source":"# CMI-BFRB CNN-LSTM推論ノートブック\n\nCNN-LSTMモデルを使用した推論。\n\n**Version**: v1.0.0\n\n**モデル性能**:\n- v6: CV Score ~0.60程度（5-Fold平均）\n- 特徴: CNN層で時系列特徴を抽出後、LSTMで時系列依存性を学習"},{"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 pickle\nimport torch\nimport torch.nn as nn\nfrom sklearn.preprocessing import StandardScaler\nimport warnings\nwarnings.filterwarnings('ignore')\n\n# デバイス設定\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nprint(f\"使用デバイス: {device}\")\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-v131')\n    MODEL_DIR = Path('/kaggle/input/cmi-bfrb-cnnlstm-models')\n    OUTPUT_DIR = Path('/kaggle/working')\nelse:\n    # ローカル環境（テスト用）\n    DATA_DIR = Path('../../experiments/data')\n    UTILS_DIR = Path('../../baseline/src')\n    MODEL_DIR = Path('../../output/cnn_lstm_v6')\n    OUTPUT_DIR = Path('../../submissions')\n\n# utilsディレクトリをパスに追加\nsys.path.insert(0, str(UTILS_DIR))\n\nprint(\"環境設定完了\")\nprint(f\"MODEL_DIR: {MODEL_DIR}\")"},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":"# モジュールのインポート\nfrom utils import (\n    load_data,\n    create_features,\n    TARGET_GESTURES,\n    ALL_GESTURES\n)\n\n# 評価APIのインポート\nimport kaggle_evaluation.cmi_inference_server\n\nprint(\"モジュール読み込み完了\")"},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":"# CNN-LSTMモデルの定義（学習時と同じ）\nclass CNNLSTMImproved(nn.Module):\n    def __init__(self, input_dim=334, num_classes=18, \n                 hidden_size=128, num_layers=2, dropout=0.3):\n        super(CNNLSTMImproved, self).__init__()\n        \n        # 1D CNN with BatchNorm for feature extraction\n        self.conv1 = nn.Conv1d(input_dim, 64, kernel_size=5, padding=2)\n        self.bn1 = nn.BatchNorm1d(64)\n        self.conv2 = nn.Conv1d(64, 128, kernel_size=3, padding=1)\n        self.bn2 = nn.BatchNorm1d(128)\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 with additional layer\n        self.fc1 = nn.Linear(hidden_size * 2, hidden_size)\n        self.bn3 = nn.BatchNorm1d(hidden_size)\n        self.fc2 = nn.Linear(hidden_size, 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 with BatchNorm\n        x = self.conv1(x)\n        x = self.bn1(x)\n        x = torch.relu(x)\n        x = self.pool(x)\n        \n        x = self.conv2(x)\n        x = self.bn2(x)\n        x = torch.relu(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        x = self.fc1(x)\n        x = self.bn3(x)\n        x = torch.relu(x)\n        x = self.dropout(x)\n        output = self.fc2(x)\n        \n        return output"},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":"# CNN-LSTMモデルの読み込み\nprint(\"CNN-LSTMモデル読み込み中...\")\n\n# モデル設定（学習時と同じ）\nSEQUENCE_LENGTH = 100\nINPUT_DIM = 334  # 7 (IMU) + 320 (ToF) + 5 (Thermopile) + 2 (others)\nNUM_CLASSES = 18\n\n# モデルのインスタンス化\nmodels = []\nscaler = None\n\nfor fold in range(5):\n    model_path = MODEL_DIR / f'cnnlstm_fold_{fold}.pth'\n    if model_path.exists():\n        model = CNNLSTMImproved(input_dim=INPUT_DIM, num_classes=NUM_CLASSES)\n        checkpoint = torch.load(model_path, map_location=device)\n        model.load_state_dict(checkpoint['model_state_dict'])\n        model.to(device)\n        model.eval()\n        models.append(model)\n        \n        # 最初のFoldからscalerを取得\n        if fold == 0 and 'scaler' in checkpoint and checkpoint['scaler'] is not None:\n            scaler = checkpoint['scaler']\n        \n        print(f\"  Fold {fold}: 読み込み成功 (CV Score: {checkpoint.get('cv_score', 'N/A')})\")\n\nprint(f\"\\n読み込んだモデル数: {len(models)}\")\n\n# scalerがない場合は新規作成\nif scaler is None:\n    print(\"Warning: Scalerが見つかりません。新しいScalerを作成します。\")\n    scaler = StandardScaler()"},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":"def create_sequences(df: pd.DataFrame, sequence_length: int = 100) -> np.ndarray:\n    \"\"\"データフレームからシーケンスを作成\"\"\"\n    \n    # センサー列を定義（学習時と同じ順序）\n    sensor_cols = []\n    \n    # IMU\n    sensor_cols.extend(['acc_x', 'acc_y', 'acc_z', 'rot_w', 'rot_x', 'rot_y', 'rot_z'])\n    \n    # ToF\n    for sensor_id in range(1, 6):\n        for value_id in range(64):\n            sensor_cols.append(f'tof_{sensor_id}_v{value_id}')\n    \n    # Thermopile\n    sensor_cols.extend([f'thm_{i}' for i in range(1, 6)])\n    \n    # データを取得\n    data_dict = {}\n    for col in sensor_cols:\n        if col in df.columns:\n            data_dict[col] = df[col].values\n        else:\n            data_dict[col] = np.zeros(len(df))\n    \n    sensor_df = pd.DataFrame(data_dict)\n    data = sensor_df.values\n    \n    # 欠損値を0で埋める\n    data = np.nan_to_num(data, nan=0.0)\n    \n    # パディングまたはトリミング\n    if len(data) < sequence_length:\n        # ゼロパディング\n        padding = np.zeros((sequence_length - len(data), data.shape[1]))\n        data = np.vstack([data, padding])\n    elif len(data) > sequence_length:\n        # 最後の部分を使用\n        data = data[-sequence_length:]\n    \n    return data"},{"cell_type":"markdown","metadata":{},"source":"## 推論関数の定義"},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":"def predict(sequence: pl.DataFrame, demographics: pl.DataFrame) -> str:\n    \"\"\"\n    CMI評価API用のCNN-LSTM推論関数\n    \n    Args:\n        sequence: センサーデータ（Polars DataFrame）\n        demographics: デモグラフィック情報（Polars DataFrame）\n        \n    Returns:\n        予測されたジェスチャー名\n    \"\"\"\n    try:\n        # Polars DataFrameをPandasに変換\n        test = sequence.to_pandas() if isinstance(sequence, pl.DataFrame) else sequence\n        \n        # デバッグ情報\n        if not hasattr(predict, 'call_count'):\n            predict.call_count = 0\n        predict.call_count += 1\n        \n        if predict.call_count == 1:\n            print(f\"最初の呼び出し - カラム数: {len(test.columns)}\")\n            print(f\"行数: {len(test)}\")\n        \n        # シーケンスデータを作成\n        sequence_data = create_sequences(test, SEQUENCE_LENGTH)\n        \n        # 正規化（scalerが利用可能な場合）\n        if hasattr(scaler, 'transform'):\n            sequence_data = scaler.transform(sequence_data)\n        \n        # テンソルに変換\n        x = torch.FloatTensor(sequence_data).unsqueeze(0).to(device)\n        \n        # 各モデルで予測\n        predictions = []\n        with torch.no_grad():\n            for model in models:\n                logits = model(x)\n                probs = torch.softmax(logits, dim=1)\n                predictions.append(probs.cpu().numpy())\n        \n        # 予測を平均\n        avg_predictions = np.mean(predictions, axis=0)\n        \n        # 最も確率の高いクラスを選択\n        pred_class = np.argmax(avg_predictions[0])\n        \n        # ジェスチャー名に変換\n        gesture = ALL_GESTURES[pred_class]\n        \n        # 100回ごとに進捗表示\n        if predict.call_count % 100 == 0:\n            print(f\"予測数: {predict.call_count}\")\n        \n        return gesture\n        \n    except Exception as e:\n        print(f\"\\n=== Prediction Error ===\")\n        print(f\"Error: {type(e).__name__}: {e}\")\n        import traceback\n        traceback.print_exc()\n        # エラー時は最頻値\n        return 'Text on phone'\n\nprint(\"✓ CNN-LSTM推論関数定義完了\")"},{"cell_type":"markdown","metadata":{},"source":"## 推論サーバーの実行"},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":"# システム情報の表示\nprint(\"\\n=== CNN-LSTMモデル情報 v1.0.0 ===\")\nprint(f\"モデルアーキテクチャ: CNN-LSTM Improved\")\nprint(f\"モデル数: {len(models)}\")\nprint(f\"シーケンス長: {SEQUENCE_LENGTH}\")\nprint(f\"入力次元: {INPUT_DIM}\")\nprint(f\"\")\nprint(f\"期待される性能:\")\nprint(f\"  - CV実績: ~0.60（v6の5-Fold平均）\")\nprint(f\"  - LB予想: 0.55-0.60\")"},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":"# CMI評価APIを使用して推論を実行\nprint(\"推論サーバーを作成中...\")\ninference_server = kaggle_evaluation.cmi_inference_server.CMIInferenceServer(predict)\n\nprint(\"CNN-LSTM推論サーバーを作成しました。\")\n\n# 環境に応じて実行\nif os.getenv('KAGGLE_IS_COMPETITION_RERUN'):\n    print(\"Kaggle競技環境を検出。推論サーバーを開始します。\")\n    inference_server.serve()\nelse:\n    print(\"推論サーバーを実行します...\")\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":{"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.10.0"}},"nbformat":4,"nbformat_minor":4}