{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"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":[{"sourceType":"competition","sourceId":102335,"databundleVersionId":12518947},{"sourceType":"datasetVersion","sourceId":12867945,"datasetId":8112117,"databundleVersionId":13511553}],"dockerImageVersionId":31090,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# ==============================================\n# CMI BFRB — Submission (Inference Only)\n# Stage2+ artefaktlarıyla uyumlu\n#  - feature_cols.json/.npy varsa birebir kanal sırası\n#  - EMA/SWA/bundle fold ensemble + Gated Softmax\n#  - Otomatik mimari + depthwise tespiti (shape-aware)\n#  - Konvolüsyon öncesi pad maskesi (kenar sızıntısı yok)\n#  - Rerun'da pre-warm yapılmaz (900s limit)\n# ==============================================\nimport os, re, glob, json, numpy as np, pandas as pd, joblib, torch, torch.nn as nn\nimport polars as pl\nfrom scipy.spatial.transform import Rotation as R\nfrom torch.nn.utils.rnn import pack_padded_sequence, pad_packed_sequence\n\n# ---------------------------\n# Yollar / Ortam\n# ---------------------------\nARTIFACT_DIR = \"/kaggle/input/my-model-artifacts\"\nDATA_DIR     = \"/kaggle/input/cmi-detect-behavior-with-sensor-data\"\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\nIS_RERUN = bool(os.getenv(\"KAGGLE_IS_COMPETITION_RERUN\"))\nVERBOSE = not IS_RERUN\ndef vlog(msg):\n    if VERBOSE: print(msg)\n\n# ---------------------------\n# Yardımcı: dosya bulucu\n# ---------------------------\ndef find_in_artifacts(candidates, also_subdirs=(\"stage2_plus_export\",)):\n    for name in candidates:\n        p = os.path.join(ARTIFACT_DIR, name)\n        if os.path.exists(p): return p\n    for sub in also_subdirs:\n        for name in candidates:\n            p = os.path.join(ARTIFACT_DIR, sub, name)\n            if os.path.exists(p): return p\n    return None\n\ndef glob_in_artifacts(patterns, also_subdirs=(\"stage2_plus_export\",)):\n    found = []\n    for pat in patterns:\n        found += glob.glob(os.path.join(ARTIFACT_DIR, pat))\n    for sub in also_subdirs:\n        for pat in patterns:\n            found += glob.glob(os.path.join(ARTIFACT_DIR, sub, pat))\n    return sorted(found)\n\n# ---------------------------\n# Artefaktları yükle\n# ---------------------------\nclasses_path = find_in_artifacts([\n    \"stage2_plus_classes.npy\", \"classes.npy\",\n    \"stage3_classes.npy\", \"stage4_classes.npy\", \"stage5_classes.npy\"\n])\nif classes_path is None:\n    raise FileNotFoundError(\"Sınıf dosyası (stage2_plus_classes.npy / classes.npy ...) bulunamadı.\")\nclasses = np.load(classes_path, allow_pickle=True)\nnum_classes = int(len(classes))\n\nfold_stats_path = find_in_artifacts([\n    \"stage2_plus_fold_stats.pkl\", \"fold_stats.pkl\",\n    \"stage3_fold_stats.pkl\", \"stage4_fold_stats.pkl\", \"stage5_fold_stats.pkl\"\n])\nif fold_stats_path is None:\n    raise FileNotFoundError(\"Fold normalize istatistikleri (mu, sd) dosyası bulunamadı.\")\ncv_stats = joblib.load(fold_stats_path)\nIN_DIM_TARGET = int(len(cv_stats[0][0]))\n\nconfig_path = find_in_artifacts([\n    \"stage2_plus_config.json\", \"config.json\",\n    \"stage3_config.json\", \"stage4_config.json\", \"stage5_config.json\"\n])\nconfig = {}\nif config_path:\n    with open(config_path, \"r\", encoding=\"utf-8\") as f:\n        config = json.load(f)\n\n# ---------------------------\n# feature_cols (NPY/JSON)\n# ---------------------------\nfeature_cols = None\nfeature_cols_path_npy = find_in_artifacts([\n    \"stage2_plus_feature_cols.npy\", \"feature_cols.npy\"\n])\nif feature_cols_path_npy and os.path.exists(feature_cols_path_npy):\n    feature_cols = list(np.load(feature_cols_path_npy, allow_pickle=True))\n    vlog(f\"[INFO] feature_cols.npy bulundu ve kullanılacak. len={len(feature_cols)}\")\n\nif feature_cols is None:\n    ch = (config.get(\"channels\", {}) if isinstance(config, dict) else {})\n    json_name = ch.get(\"feature_cols_json\", None)\n    cand_json = []\n    if json_name: cand_json.append(json_name)\n    cand_json += [\"stage2_plus_feature_cols.json\", \"feature_cols.json\"]\n    feature_cols_path_json = find_in_artifacts(cand_json)\n    if feature_cols_path_json and os.path.exists(feature_cols_path_json):\n        with open(feature_cols_path_json, \"r\", encoding=\"utf-8\") as f:\n            feature_cols = json.load(f)\n        vlog(f\"[INFO] feature_cols.json bulundu ve kullanılacak. len={len(feature_cols)}\")\n\n# ---------------------------\n# Hiperparam/Config\n# ---------------------------\nMAX_LEN = int(config.get(\"max_len\", 127))\nhidden  = int(config.get(\"hyperparams\", {}).get(\"hidden\", 128))\nlayers  = int(config.get(\"hyperparams\", {}).get(\"layers\", 2))\ndropout = float(config.get(\"hyperparams\", {}).get(\"dropout\", 0.3))\n\n# ---------------------------\n# Gating (A-grubu)\n# ---------------------------\nDEFAULT_TARGET_LABELS = {\n    \"Above ear - pull hair\",\"Forehead - pull hairline\",\"Forehead - scratch\",\n    \"Eyebrow - pull hair\",\"Eyelash - pull hair\",\"Neck - pinch skin\",\n    \"Neck - scratch\",\"Cheek - pinch skin\"\n}\ncfg_gate = (config.get(\"gating\", {}) if isinstance(config, dict) else {})\ngroup_A = set(cfg_gate.get(\"group_A\", [])) if isinstance(cfg_gate, dict) else set()\nif not group_A:\n    group_A = DEFAULT_TARGET_LABELS\n\nmaskA = np.array([c in group_A for c in classes], dtype=bool)\nGATE_BIAS = float(cfg_gate.get(\"bias\", 0.0)) if isinstance(cfg_gate, dict) else 0.0\nGATE_TAU  = float(cfg_gate.get(\"tau\", 1.0))  if isinstance(cfg_gate, dict) else 1.0\n\ndef apply_gated_softmax(pm, pb, maskA):\n    eps = 1e-9\n    logit = np.log(np.clip(pb, eps, 1 - eps)) - np.log(np.clip(1 - pb, eps, 1 - eps))\n    pb_adj = 1.0 / (1.0 + np.exp(-(logit + GATE_BIAS) / max(GATE_TAU, 1e-6)))\n    pb_adj = pb_adj.reshape(-1, 1)\n    out = pm.copy()\n    out[:, maskA]  *= pb_adj\n    out[:, ~maskA] *= (1.0 - pb_adj)\n    out /= (out.sum(axis=1, keepdims=True) + 1e-9)\n    return out\n\n# ---------------------------\n# FE yardımcıları\n# ---------------------------\nimu_raw_cols = ['acc_x','acc_y','acc_z','rot_x','rot_y','rot_z','rot_w']\nthm_raw_cols = ['thm_1','thm_2','thm_3','thm_4','thm_5']\n\ndef _safe_series(x):\n    return pd.Series(x).replace([np.inf,-np.inf], np.nan).fillna(0.0).astype(np.float32)\n\ndef remove_gravity_from_acc(acc_xyz: np.ndarray, quat_xyzw: np.ndarray):\n    g = np.array([0,0,9.81], dtype=np.float32)\n    out = np.zeros_like(acc_xyz, dtype=np.float32)\n    for i in range(len(acc_xyz)):\n        q = quat_xyzw[i]\n        if np.any(np.isnan(q)):\n            out[i] = acc_xyz[i]; continue\n        try:\n            Rq = R.from_quat(q)\n            g_sens = Rq.apply(g, inverse=True)\n            out[i] = acc_xyz[i] - g_sens\n        except ValueError:\n            out[i] = acc_xyz[i]\n    return out\n\ndef angular_velocity_from_quat(quat_xyzw: np.ndarray, dt: float = 1/200):\n    w = np.zeros((len(quat_xyzw), 3), dtype=np.float32)\n    for i in range(len(quat_xyzw)-1):\n        q1, q2 = quat_xyzw[i], quat_xyzw[i+1]\n        if np.any(np.isnan(q1)) or np.any(np.isnan(q2)): \n            continue\n        try:\n            r1, r2 = R.from_quat(q1), R.from_quat(q2)\n            d = r1.inv() * r2\n            w[i] = d.as_rotvec() / dt\n        except ValueError:\n            pass\n    return w\n\ndef angular_distance_from_quat(quat_xyzw: np.ndarray):\n    ang = np.zeros((len(quat_xyzw),), dtype=np.float32)\n    for i in range(len(quat_xyzw)-1):\n        q1, q2 = quat_xyzw[i], quat_xyzw[i+1]\n        if np.any(np.isnan(q1)) or np.any(np.isnan(q2)): \n            continue\n        try:\n            r1, r2 = R.from_quat(q1), R.from_quat(q2)\n            ang[i] = np.linalg.norm((r1.inv() * r2).as_rotvec())\n        except ValueError:\n            pass\n    return ang\n\ndef _rolling_feats(s: pd.Series, win=5):\n    m = s.rolling(win, min_periods=1).mean().astype(np.float32)\n    sd = s.rolling(win, min_periods=1).std(ddof=0).fillna(0.0).astype(np.float32)\n    return m, sd\n\ndef get_engineered_fallback_list(target_len: int):\n    base = [\n        \"acc_mag\",\"acc_mag_jerk\",\n        \"rot_angle\",\"rot_angle_vel\",\n        \"linear_acc_x\",\"linear_acc_y\",\"linear_acc_z\",\n        \"linear_acc_mag\",\"linear_acc_mag_jerk\",\n        \"angular_vel_x\",\"angular_vel_y\",\"angular_vel_z\",\n        \"angular_distance\",\n    ]  # 13\n    extra = [\n        \"acc_mag_ma5\",\"acc_mag_std5\",\n        \"linear_acc_mag_ma5\",\"linear_acc_mag_std5\",\n        \"rot_angle_ma5\",\"rot_angle_std5\",\n        \"acc_x_diff\",\"acc_y_diff\",\"acc_z_diff\",\n        \"linear_acc_x_diff\",\"linear_acc_y_diff\",\"linear_acc_z_diff\",\n        \"ang_speed_mag\",\"ang_speed_jerk\",\"ang_speed_ma5\",\"ang_speed_std5\",\n        \"thm_mean\",\"thm_std\",\"thm_range\",\n    ]  # +19 = 32\n    all_eng = base + extra\n    if target_len > len(all_eng):\n        raise RuntimeError(f\"Fallback engineered {len(all_eng)} < hedef {target_len}. Lütfen feature_cols.json/.npy ekleyin.\")\n    return all_eng[:target_len]\n\ndef compute_features_df(df_seq: pd.DataFrame, want_cols: list | None, max_len: int):\n    s = df_seq.copy()\n    if 'sequence_counter' in s.columns:\n        s = s.sort_values('sequence_counter')\n    for c in set(imu_raw_cols + thm_raw_cols):\n        if c not in s.columns: s[c] = 0.0\n\n    acc = s[[\"acc_x\",\"acc_y\",\"acc_z\"]].values.astype(np.float32)\n    quat = s[[\"rot_x\",\"rot_y\",\"rot_z\",\"rot_w\"]].values.astype(np.float32)\n\n    acc_mag = np.linalg.norm(acc, axis=1).astype(np.float32)\n    s[\"acc_mag\"] = acc_mag\n    s[\"acc_mag_jerk\"] = _safe_series(acc_mag).diff().fillna(0.0).astype(np.float32)\n\n    rot_w_clip = np.nan_to_num(s[\"rot_w\"].values, nan=0.0)\n    rot_angle = (2*np.arccos(np.clip(rot_w_clip, -1, 1))).astype(np.float32)\n    s[\"rot_angle\"] = rot_angle\n    s[\"rot_angle_vel\"] = _safe_series(rot_angle).diff().fillna(0.0).astype(np.float32)\n\n    lin = remove_gravity_from_acc(acc, quat)\n    s[\"linear_acc_x\"] = lin[:,0]; s[\"linear_acc_y\"] = lin[:,1]; s[\"linear_acc_z\"] = lin[:,2]\n    lin_mag = np.linalg.norm(lin, axis=1).astype(np.float32)\n    s[\"linear_acc_mag\"] = lin_mag\n    s[\"linear_acc_mag_jerk\"] = _safe_series(lin_mag).diff().fillna(0.0).astype(np.float32)\n\n    ang_vel = angular_velocity_from_quat(quat)\n    s[\"angular_vel_x\"] = ang_vel[:,0]; s[\"angular_vel_y\"] = ang_vel[:,1]; s[\"angular_vel_z\"] = ang_vel[:,2]\n    s[\"angular_distance\"] = angular_distance_from_quat(quat)\n\n    s[\"acc_mag_ma5\"], s[\"acc_mag_std5\"] = _rolling_feats(pd.Series(acc_mag), win=5)\n    s[\"linear_acc_mag_ma5\"], s[\"linear_acc_mag_std5\"] = _rolling_feats(pd.Series(lin_mag), win=5)\n    s[\"rot_angle_ma5\"], s[\"rot_angle_std5\"] = _rolling_feats(pd.Series(rot_angle), win=5)\n\n    s[\"acc_x_diff\"] = _safe_series(s[\"acc_x\"]).diff().fillna(0.0).astype(np.float32)\n    s[\"acc_y_diff\"] = _safe_series(s[\"acc_y\"]).diff().fillna(0.0).astype(np.float32)\n    s[\"acc_z_diff\"] = _safe_series(s[\"acc_z\"]).diff().fillna(0.0).astype(np.float32)\n    s[\"linear_acc_x_diff\"] = _safe_series(s[\"linear_acc_x\"]).diff().fillna(0.0).astype(np.float32)\n    s[\"linear_acc_y_diff\"] = _safe_series(s[\"linear_acc_y\"]).diff().fillna(0.0).astype(np.float32)\n    s[\"linear_acc_z_diff\"] = _safe_series(s[\"linear_acc_z\"]).diff().fillna(0.0).astype(np.float32)\n\n    ang_speed = np.linalg.norm(ang_vel, axis=1).astype(np.float32)\n    s[\"ang_speed_mag\"] = ang_speed\n    s[\"ang_speed_jerk\"] = _safe_series(ang_speed).diff().fillna(0.0).astype(np.float32)\n    s[\"ang_speed_ma5\"], s[\"ang_speed_std5\"] = _rolling_feats(pd.Series(ang_speed), win=5)\n\n    thm_mat = s[thm_raw_cols].astype(np.float32).values\n    s[\"thm_mean\"]  = np.nanmean(thm_mat, axis=1).astype(np.float32)\n    s[\"thm_std\"]   = np.nanstd(thm_mat,  axis=1).astype(np.float32)\n    s[\"thm_range\"] = (np.nanmax(thm_mat, axis=1) - np.nanmin(thm_mat, axis=1)).astype(np.float32)\n\n    if want_cols is not None and len(want_cols) > 0:\n        cols = []\n        for c in want_cols:\n            if c not in s.columns: s[c] = 0.0\n            cols.append(c)\n        all_cols = cols\n    else:\n        need_eng = IN_DIM_TARGET - len(imu_raw_cols) - len(thm_raw_cols)\n        if need_eng <= 0:\n            raise RuntimeError(\"IN_DIM_TARGET IMU+THM'den küçük/eşit görünüyor. Artefaktlar hatalı olabilir.\")\n        engineered_cols = get_engineered_fallback_list(need_eng)\n        all_cols = imu_raw_cols + thm_raw_cols + engineered_cols\n        vlog(f\"[INFO] feature_cols yok. Fallback FE seti kullanılıyor. (in_dim hedef={IN_DIM_TARGET}, engineered={len(engineered_cols)})\")\n\n    X = s[all_cols].astype(\"float32\").replace([np.inf, -np.inf], np.nan).fillna(0.0).values\n    L, C = X.shape\n    T = min(L, max_len)\n    x = np.zeros((max_len, C), dtype=np.float32)\n    m = np.zeros((max_len,), dtype=np.float32)\n    x[:T] = X[:T]\n    m[:T] = 1.0\n    return x, m, all_cols\n\n# ---------------------------\n# Model mimari parçaları\n# ---------------------------\nclass Rearrange(nn.Module):\n    def __init__(self, pat): super().__init__(); self.pat = pat\n    def forward(self, x):\n        if self.pat == \"b t c -> b c t\": return x.permute(0,2,1).contiguous()\n        if self.pat == \"b c t -> b t c\": return x.permute(0,2,1).contiguous()\n        raise ValueError(\"pattern?\")\n\nclass ConvBlock(nn.Module):\n    def __init__(self, in_ch, out_ch, k=5, s=1, p=2, dw=False):\n        super().__init__()\n        if dw:\n            self.net = nn.Sequential(\n                nn.Conv1d(in_ch, in_ch, k, s, p, groups=in_ch, bias=False),  # depthwise\n                nn.Conv1d(in_ch, out_ch, 1, bias=False),                     # pointwise\n                nn.BatchNorm1d(out_ch), nn.GELU()\n            )\n        else:\n            self.net = nn.Sequential(\n                nn.Conv1d(in_ch, out_ch, k, s, p, bias=False),\n                nn.BatchNorm1d(out_ch), nn.GELU()\n            )\n        self.res_proj = (in_ch != out_ch) or (s != 1)\n        if self.res_proj:\n            self.proj = nn.Conv1d(in_ch, out_ch, 1, stride=s, bias=False)\n    def forward(self, x):\n        y = self.net(x)\n        if self.res_proj: x = self.proj(x)\n        return y + x\n\ndef lengths_from_mask(mask_t: torch.Tensor):\n    return mask_t.sum(dim=1).long().cpu()\n\n# --- Yeni mimari (layernorm_in ayrık, conv1 dw=False, conv2 dw=True)\nclass BiLSTM_Multitask_New(nn.Module):\n    def __init__(self, input_dim, hidden=128, layers=2, dropout=0.3, num_classes=18, conv_dim=128):\n        super().__init__()\n        self.layernorm_in = nn.LayerNorm(input_dim)\n        self.fe = nn.Sequential(\n            Rearrange(\"b t c -> b c t\"),\n            ConvBlock(input_dim, 128, k=5, p=2, dw=False),\n            ConvBlock(128, 128, k=5, p=2, dw=True),\n            nn.Conv1d(128, conv_dim, 1, bias=False),\n            nn.BatchNorm1d(conv_dim), nn.GELU(),\n            Rearrange(\"b c t -> b t c\"),\n        )\n        self.lstm = nn.LSTM(input_size=conv_dim, hidden_size=hidden, num_layers=layers,\n                            batch_first=True, bidirectional=True,\n                            dropout=(dropout if layers > 1 else 0.0))\n        self.attn = nn.Sequential(nn.Linear(hidden*2, hidden), nn.Tanh(), nn.Linear(hidden, 1))\n        self.norm = nn.LayerNorm(hidden*2)\n        self.drop = nn.Dropout(dropout)\n        self.head_multi = nn.Linear(hidden*2, num_classes)\n        self.head_bin   = nn.Linear(hidden*2, 1)\n\n    def forward(self, x, mask=None):\n        if mask is not None:\n            x = x * mask.unsqueeze(-1)\n        x = self.layernorm_in(x)\n        x = self.fe(x)\n        if mask is not None:\n            lengths = lengths_from_mask(mask)\n            packed = pack_padded_sequence(x, lengths, batch_first=True, enforce_sorted=False)\n            packed_out, _ = self.lstm(packed)\n            out, _ = pad_packed_sequence(packed_out, batch_first=True, total_length=x.size(1))\n            scores = self.attn(out).squeeze(-1)\n            scores = scores.masked_fill(mask == 0, -1e9)\n            w = torch.softmax(scores, dim=1).unsqueeze(-1)\n            feat = (out * w).sum(dim=1)\n        else:\n            out, _ = self.lstm(x); feat = out.mean(dim=1)\n        feat = self.drop(self.norm(feat))\n        return self.head_multi(feat), self.head_bin(feat).squeeze(1)\n\n# --- Legacy mimari (LN fe.0 içinde; conv1/conv2 dw bayrakları şekilden tespit edilir)\nclass BiLSTM_Multitask_Legacy(nn.Module):\n    def __init__(self, input_dim, hidden=128, layers=2, dropout=0.3, num_classes=18,\n                 conv1_dw=False, conv2_dw=False, conv_dim=128):\n        super().__init__()\n        self.fe = nn.Sequential(\n            nn.LayerNorm(input_dim),                    # fe.0\n            Rearrange(\"b t c -> b c t\"),               # fe.1\n            ConvBlock(input_dim, 128, k=5, p=2, dw=conv1_dw),  # fe.2\n            ConvBlock(128, 128, k=5, p=2, dw=conv2_dw),        # fe.3\n            nn.Conv1d(128, conv_dim, 1, bias=False),   # fe.4\n            nn.BatchNorm1d(conv_dim), nn.GELU(),       # fe.5\n            Rearrange(\"b c t -> b t c\"),               # fe.6\n        )\n        self.lstm = nn.LSTM(input_size=conv_dim, hidden_size=hidden, num_layers=layers,\n                            batch_first=True, bidirectional=True,\n                            dropout=(dropout if layers > 1 else 0.0))\n        self.attn = nn.Sequential(nn.Linear(hidden*2, hidden), nn.Tanh(), nn.Linear(hidden, 1))\n        self.norm = nn.LayerNorm(hidden*2)\n        self.drop = nn.Dropout(dropout)\n        self.head_multi = nn.Linear(hidden*2, num_classes)\n        self.head_bin   = nn.Linear(hidden*2, 1)\n\n    def forward(self, x, mask=None):\n        if mask is not None:\n            x = x * mask.unsqueeze(-1)                 # konvolüsyon öncesi pad sıfırlama\n        x = self.fe(x)\n        if mask is not None:\n            lengths = lengths_from_mask(mask)\n            packed = pack_padded_sequence(x, lengths, batch_first=True, enforce_sorted=False)\n            packed_out, _ = self.lstm(packed)\n            out, _ = pad_packed_sequence(packed_out, batch_first=True, total_length=x.size(1))\n            scores = self.attn(out).squeeze(-1)\n            scores = scores.masked_fill(mask == 0, -1e9)\n            w = torch.softmax(scores, dim=1).unsqueeze(-1)\n            feat = (out * w).sum(dim=1)\n        else:\n            out, _ = self.lstm(x); feat = out.mean(dim=1)\n        feat = self.drop(self.norm(feat))\n        return self.head_multi(feat), self.head_bin(feat).squeeze(1)\n\n# ---------------------------\n# Ağırlık yükleme (auto-arch + shape-aware)\n# ---------------------------\ndef parse_fold_id(path: str):\n    m = re.search(r\"fold(\\d+)\", os.path.basename(path))\n    return int(m.group(1)) if m else None\n\ndef _extract_state_dict(sd):\n    if isinstance(sd, dict):\n        if \"state_dict\" in sd and isinstance(sd[\"state_dict\"], (dict,)):\n            return sd[\"state_dict\"]\n        keys = list(sd.keys())\n        if keys and isinstance(keys[0], str) and (keys[0].startswith(\"fe.\") or \"lstm.weight_ih_l0\" in keys or \"head_multi.weight\" in keys or \"layernorm_in.weight\" in keys):\n            return sd\n    return sd\n\ndef _has(sd, k): return (k in sd)\ndef _shape(sd, k): return tuple(sd[k].shape) if k in sd else None\n\ndef _detect_arch_params(sd, in_dim):\n    keys = sd.keys()\n    new_style = any(k.startswith(\"layernorm_in.\") for k in keys)\n    legacy_ln = any(k == \"fe.0.weight\" for k in keys) and (_shape(sd, \"fe.0.weight\") == (in_dim,))\n    if new_style and not legacy_ln:\n        # New: conv1 dw? (beklenen False), conv2 dw? (beklenen True), conv_dim from fe.3 or fe.4?\n        conv1_dw = (_shape(sd, \"fe.1.net.0.weight\") is not None and _shape(sd, \"fe.1.net.0.weight\")[1] == 1)\n        conv2_dw = (_shape(sd, \"fe.2.net.0.weight\") is not None and _shape(sd, \"fe.2.net.0.weight\")[1] == 1)\n        # conv1x1 is fe.3.weight likely\n        conv1x1_shape = _shape(sd, \"fe.3.weight\")\n        conv_dim = conv1x1_shape[0] if conv1x1_shape else 128\n        return (\"new\", conv1_dw, conv2_dw, conv_dim)\n    else:\n        # Legacy: LN fe.0 içinde; convblocklar fe.2 ve fe.3; 1x1 conv fe.4\n        conv1_dw = (_shape(sd, \"fe.2.net.0.weight\") is not None and _shape(sd, \"fe.2.net.0.weight\")[1] == 1)\n        conv2_dw = (_shape(sd, \"fe.3.net.0.weight\") is not None and _shape(sd, \"fe.3.net.0.weight\")[1] == 1)\n        conv1x1_shape = _shape(sd, \"fe.4.weight\")\n        conv_dim = conv1x1_shape[0] if conv1x1_shape else 128\n        return (\"legacy\", conv1_dw, conv2_dw, conv_dim)\n\ndef _build_single_model_for_state(sd, in_dim, hidden, layers, dropout, num_classes):\n    arch, conv1_dw, conv2_dw, conv_dim = _detect_arch_params(sd, in_dim)\n    if arch == \"new\":\n        m = BiLSTM_Multitask_New(in_dim, hidden=hidden, layers=layers, dropout=dropout, num_classes=num_classes, conv_dim=conv_dim)\n    else:\n        m = BiLSTM_Multitask_Legacy(in_dim, hidden=hidden, layers=layers, dropout=dropout, num_classes=num_classes,\n                                    conv1_dw=conv1_dw, conv2_dw=conv2_dw, conv_dim=conv_dim)\n    m.load_state_dict(sd, strict=True)\n    m.to(device).eval()\n    return m, arch, conv1_dw, conv2_dw, conv_dim\n\ndef _build_models_for_fold(weight_paths, in_dim, hidden, layers, dropout, num_classes):\n    models = []\n    for wp in weight_paths:\n        raw = torch.load(wp, map_location=\"cpu\")\n        sd = _extract_state_dict(raw)\n        m, arch, d1, d2, cd = _build_single_model_for_state(sd, in_dim, hidden, layers, dropout, num_classes)\n        models.append(m)\n        if VERBOSE:\n            print(f\"[INFO] {os.path.basename(wp)} -> arch={arch}, conv1_dw={d1}, conv2_dw={d2}, conv_dim={cd}\")\n    return models\n\nclass Ensemble:\n    def __init__(self, in_dim, hidden, layers, dropout, num_classes):\n        weight_patterns = [\n            \"stage2_plus_ema_fold*.pt\", \"stage2_plus_swa_fold*.pt\",\n            \"stage2_plus_fold*_bundle.pt\",\n            \"stage3_fold*.pt\", \"stage4_fold*.pt\", \"stage5_fold*.pt\",\n            \"model_fold*.pt\"\n        ]\n        paths = glob_in_artifacts(weight_patterns)\n        if not paths:\n            raise FileNotFoundError(\"Fold ağırlıkları (*.pt) bulunamadı.\")\n\n        fold_map = {}\n        for p in paths:\n            f = parse_fold_id(p)\n            if f is None: \n                continue\n            fold_map.setdefault(f, []).append(p)\n\n        self.folds = []\n        total_models = 0\n        for f in sorted(fold_map.keys()):\n            order = sorted(fold_map[f], key=lambda x: ((\"ema\" not in x), x))\n            ms = _build_models_for_fold(order, in_dim, hidden, layers, dropout, num_classes)\n            self.folds.append((f, ms))\n            total_models += len(ms)\n        vlog(f\"[INFO] Fold sayısı: {len(self.folds)} (EMA/SWA/bundle birlikte {total_models} model)\")\n\n    @torch.no_grad()\n    def predict_proba(self, x: np.ndarray, m: np.ndarray, mu: np.ndarray, sd: np.ndarray):\n        # Normalizasyon + PAD'leri sıfırla (ek güvenlik)\n        xn = (x - mu.reshape(1, -1)) / (sd.reshape(1, -1) + 1e-6)\n        xn *= m.reshape(-1, 1)\n\n        xb = torch.tensor(xn[None, ...], dtype=torch.float32, device=device)\n        mb = torch.tensor(m[None,  ...], dtype=torch.float32, device=device)\n        probs_folds = []\n        for f, ms in self.folds:\n            probs_models = []\n            for model in ms:\n                log_m, log_b = model(xb, mask=mb)\n                pm = torch.softmax(log_m, dim=1).cpu().numpy()\n                pb = torch.sigmoid(log_b).cpu().numpy()\n                pm_g = apply_gated_softmax(pm, pb, maskA)\n                probs_models.append(pm_g[0])\n            probs_folds.append(np.mean(probs_models, axis=0))\n        return np.mean(probs_folds, axis=0)\n\n# ---------------------------\n# Tek sequence tahmini\n# ---------------------------\n_models_cache = None\n_feature_cols_used = None\n\ndef _ensure_models(feature_cols_runtime: list, cv_stats):\n    global _models_cache, _feature_cols_used\n    if _models_cache is not None:\n        return _models_cache\n\n    first_mu, first_sd = cv_stats[0]\n    in_dim = int(len(first_mu))\n    for (mu, sd) in cv_stats:\n        assert len(mu) == in_dim and len(sd) == in_dim, \"cv_stats boyutları tutarsız!\"\n\n    if len(feature_cols_runtime) != in_dim:\n        raise AssertionError(\n            f\"FE kanal sayısı ({len(feature_cols_runtime)}) ile cv_stats in_dim ({in_dim}) eşit değil.\\n\"\n            \"-> Çözüm: Eğitimde 'feature_cols.json/.npy' kaydet ve artefakta koy; \"\n            \"veya fallback FE setinin in_dim hedefiyle üretildiğini doğrula.\"\n        )\n\n    ens = Ensemble(in_dim=in_dim, hidden=hidden, layers=layers, dropout=dropout, num_classes=num_classes)\n    _models_cache = ens\n    _feature_cols_used = feature_cols_runtime\n    vlog(f\"[INFO] in_dim={in_dim} | MAX_LEN={MAX_LEN} | hidden={hidden} | layers={layers} | dropout={dropout}\")\n    return ens\n\n@torch.no_grad()\ndef _predict_one(sequence_df: pd.DataFrame) -> np.ndarray:\n    x, m, cols = compute_features_df(sequence_df, feature_cols, MAX_LEN)\n    ens = _ensure_models(cols, cv_stats)\n    probs_by_fold = []\n    for i in range(len(cv_stats)):\n        mu, sd = cv_stats[i]\n        mu = np.asarray(mu, dtype=np.float32)\n        sd = np.asarray(sd, dtype=np.float32)\n        probs_by_fold.append(ens.predict_proba(x, m, mu, sd))\n    return np.mean(probs_by_fold, axis=0)\n\n# ---------------------------\n# Kaggle Inference Server imzası\n# ---------------------------\ndef predict(sequence: pl.DataFrame, demographics: pl.DataFrame) -> str:\n    df = sequence.to_pandas()\n    p = _predict_one(df)\n    idx = int(np.argmax(p))\n    return str(classes[idx])\n\nvlog(\"[INFO] predict() tanımlandı ve hazır.\")\n\n# ---------------------------\n# CMI Inference Server entegrasyonu\n# ---------------------------\nfrom kaggle_evaluation.cmi_inference_server import CMIInferenceServer\n\n# Pre-warm SADECE lokal gateway'de (rerun'da değil)\nif not IS_RERUN:\n    try:\n        dummy = pd.DataFrame({c: [0.0] for c in (imu_raw_cols + thm_raw_cols)})\n        x, m, cols = compute_features_df(dummy, feature_cols, MAX_LEN)\n        _ensure_models(cols, cv_stats)\n    except Exception as e:\n        vlog(\"[WARN] Ön ısıtma uyarısı: \" + repr(e))\n\ninference_server = CMIInferenceServer(predict)\n\nif IS_RERUN:\n    inference_server.serve()\nelse:\n    inference_server.run_local_gateway(\n        data_paths=(\n            os.path.join(DATA_DIR, \"test.csv\"),\n            os.path.join(DATA_DIR, \"test_demographics.csv\"),\n        )\n    )\n    try:\n        print(pd.read_parquet(\"submission.parquet\").head())\n    except Exception as e:\n        vlog(\"[WARN] submission.parquet okunamadı: \" + repr(e))\n","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-08-25T23:43:51.797286Z","iopub.execute_input":"2025-08-25T23:43:51.798360Z","iopub.status.idle":"2025-08-25T23:43:56.942835Z","shell.execute_reply.started":"2025-08-25T23:43:51.798328Z","shell.execute_reply":"2025-08-25T23:43:56.941709Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}