{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[],"dockerImageVersionId":28755,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"from pathlib import Path\nimport pandas as pd\nimport numpy as np\nimport json\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nfrom sklearn.model_selection import train_test_split\n\nDATA_ROOT = Path(\"/kaggle/input/competitions/asl-signs\")\nWORK_ROOT = Path(\"/kaggle/working\")\n\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(\"Using device:\", device)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-30T12:39:12.863813Z","iopub.execute_input":"2026-07-30T12:39:12.864204Z","iopub.status.idle":"2026-07-30T12:39:20.07843Z","shell.execute_reply.started":"2026-07-30T12:39:12.864153Z","shell.execute_reply":"2026-07-30T12:39:20.077758Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_df = pd.read_csv(DATA_ROOT / \"train.csv\")\n\nmy_signs = ['hello', 'yes', 'no', 'please', 'thankyou', 'wait', 'go']\nsubset_df = train_df[train_df['sign'].isin(my_signs)].reset_index(drop=True)\n\nsign_to_idx = {sign: i for i, sign in enumerate(sorted(subset_df['sign'].unique()))}\nidx_to_sign = {v: k for k, v in sign_to_idx.items()}\n\nprint(subset_df.shape)\nprint(sign_to_idx)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-30T12:39:20.080265Z","iopub.execute_input":"2026-07-30T12:39:20.080588Z","iopub.status.idle":"2026-07-30T12:39:20.256114Z","shell.execute_reply.started":"2026-07-30T12:39:20.080566Z","shell.execute_reply":"2026-07-30T12:39:20.255363Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"HAND_LANDMARKS = 21\nPOSE_LANDMARKS = 33\nFIXED_LEN = 40\n\ndef load_landmarks(path, data_root):\n    df = pd.read_parquet(data_root / path)\n    frames = df['frame'].unique()\n    n_frames = len(frames)\n    output = np.zeros((n_frames, HAND_LANDMARKS + HAND_LANDMARKS + POSE_LANDMARKS, 2), dtype=np.float32)\n\n    for i, frame_num in enumerate(frames):\n        frame_df = df[df['frame'] == frame_num]\n        left = frame_df[frame_df['type'] == 'left_hand'].sort_values('landmark_index')[['x', 'y']].values\n        right = frame_df[frame_df['type'] == 'right_hand'].sort_values('landmark_index')[['x', 'y']].values\n        pose = frame_df[frame_df['type'] == 'pose'].sort_values('landmark_index')[['x', 'y']].values\n\n        if len(left) == HAND_LANDMARKS:\n            output[i, 0:21] = left\n        if len(right) == HAND_LANDMARKS:\n            output[i, 21:42] = right\n        if len(pose) == POSE_LANDMARKS:\n            output[i, 42:75] = pose\n\n    return np.nan_to_num(output, nan=0.0)\n\ndef pad_or_truncate(seq, fixed_len=FIXED_LEN):\n    n_frames = seq.shape[0]\n    if n_frames == fixed_len:\n        return seq\n    elif n_frames > fixed_len:\n        indices = np.linspace(0, n_frames - 1, fixed_len).astype(int)\n        return seq[indices]\n    else:\n        pad_width = fixed_len - n_frames\n        padding = np.zeros((pad_width, seq.shape[1], seq.shape[2]), dtype=np.float32)\n        return np.concatenate([seq, padding], axis=0)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-30T12:39:20.257024Z","iopub.execute_input":"2026-07-30T12:39:20.257342Z","iopub.status.idle":"2026-07-30T12:39:20.266116Z","shell.execute_reply.started":"2026-07-30T12:39:20.257304Z","shell.execute_reply":"2026-07-30T12:39:20.265262Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"X_path = WORK_ROOT / \"X.npy\"\ny_path = WORK_ROOT / \"y.npy\"\n\nif X_path.exists() and y_path.exists():\n    X = np.load(X_path)\n    y = np.load(y_path)\n    print(\"Loaded cached X, y\")\nelse:\n    from tqdm import tqdm\n    X = np.zeros((len(subset_df), FIXED_LEN, 75, 2), dtype=np.float32)\n    y = np.zeros(len(subset_df), dtype=np.int64)\n    failed_indices = []\n\n    for i, row in tqdm(subset_df.iterrows(), total=len(subset_df)):\n        try:\n            seq = load_landmarks(row['path'], DATA_ROOT)\n            X[i] = pad_or_truncate(seq)\n            y[i] = sign_to_idx[row['sign']]\n        except Exception as e:\n            failed_indices.append(i)\n            print(f\"Failed at {i}: {e}\")\n\n    np.save(X_path, X)\n    np.save(y_path, y)\n    with open(WORK_ROOT / \"sign_to_idx.json\", 'w') as f:\n        json.dump(sign_to_idx, f)\n    print(f\"Processed and saved. Failed: {len(failed_indices)}\")\n\nprint(\"X shape:\", X.shape, \"y shape:\", y.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-30T12:39:20.267126Z","iopub.execute_input":"2026-07-30T12:39:20.267417Z","iopub.status.idle":"2026-07-30T12:44:51.069206Z","shell.execute_reply.started":"2026-07-30T12:39:20.267384Z","shell.execute_reply":"2026-07-30T12:44:51.06825Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def augment_landmarks(seq, training=True):\n    if not training:\n        return seq\n    seq = seq.copy()\n\n    if np.random.rand() < 0.8:\n        noise_level = np.random.uniform(0.005, 0.02)\n        seq = seq + np.random.normal(0, noise_level, seq.shape)\n\n    if np.random.rand() < 0.3:\n        n_frames = seq.shape[0]\n        drop_count = np.random.randint(1, max(2, n_frames // 8))\n        drop_indices = np.random.choice(n_frames, drop_count, replace=False)\n        seq[drop_indices] = 0\n\n    if np.random.rand() < 0.5:\n        seq[:, :, 0] = 1 - seq[:, :, 0]\n        seq[:, [*range(0,21), *range(21,42)], :] = seq[:, [*range(21,42), *range(0,21)], :]\n\n    if np.random.rand() < 0.4:\n        n_frames = seq.shape[0]\n        stretch_factor = np.random.uniform(0.85, 1.15)\n        new_len = max(2, int(n_frames * stretch_factor))\n        indices = np.linspace(0, n_frames - 1, new_len).astype(int)\n        seq = pad_or_truncate(seq[indices], fixed_len=FIXED_LEN)\n\n    if np.random.rand() < 0.5:\n        shift = np.random.uniform(-0.03, 0.03, 2)\n        mask = ~(seq == 0).all(axis=2, keepdims=True)\n        seq = seq + np.where(mask, shift, 0)\n\n    if np.random.rand() < 0.5:\n        scale = np.random.uniform(0.9, 1.1)\n        mask = ~(seq == 0).all(axis=2, keepdims=True)\n        seq = np.where(mask, (seq - 0.5) * scale + 0.5, seq)\n\n    return np.clip(seq, 0, 1)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-30T12:44:51.070407Z","iopub.execute_input":"2026-07-30T12:44:51.070714Z","iopub.status.idle":"2026-07-30T12:44:51.079105Z","shell.execute_reply.started":"2026-07-30T12:44:51.070691Z","shell.execute_reply":"2026-07-30T12:44:51.078179Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class SignDataset(Dataset):\n    def __init__(self, X, y, training=True):\n        self.X = X\n        self.y = torch.tensor(y, dtype=torch.long)\n        self.training = training\n\n    def __len__(self):\n        return len(self.X)\n\n    def __getitem__(self, idx):\n        seq = augment_landmarks(self.X[idx], training=self.training)\n        seq_flat = seq.reshape(seq.shape[0], -1)\n        return torch.tensor(seq_flat, dtype=torch.float32), self.y[idx]\n\nX_train_raw, X_val_raw, y_train, y_val = train_test_split(\n    X, y, test_size=0.2, random_state=42, stratify=y\n)\n\ntrain_ds = SignDataset(X_train_raw, y_train, training=True)\nval_ds = SignDataset(X_val_raw, y_val, training=False)\n\ntrain_loader = DataLoader(train_ds, batch_size=32, shuffle=True)\nval_loader = DataLoader(val_ds, batch_size=32, shuffle=False)\n\nprint(\"Train:\", len(train_ds), \"Val:\", len(val_ds))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-30T12:44:51.080236Z","iopub.execute_input":"2026-07-30T12:44:51.080523Z","iopub.status.idle":"2026-07-30T12:44:51.146286Z","shell.execute_reply.started":"2026-07-30T12:44:51.08049Z","shell.execute_reply":"2026-07-30T12:44:51.145623Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class SignClassifier(nn.Module):\n    def __init__(self, input_size=150, hidden_size=128, num_layers=2, num_classes=7):\n        super().__init__()\n        self.lstm = nn.LSTM(\n            input_size=input_size, hidden_size=hidden_size,\n            num_layers=num_layers, batch_first=True,\n            bidirectional=True, dropout=0.3\n        )\n        self.fc = nn.Linear(hidden_size * 2, num_classes)\n\n    def forward(self, x):\n        out, _ = self.lstm(x)\n        out = out[:, -1, :]\n        return self.fc(out)\n\nmodel = SignClassifier(num_classes=len(sign_to_idx)).to(device)\nprint(model)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-30T12:44:51.147929Z","iopub.execute_input":"2026-07-30T12:44:51.148232Z","iopub.status.idle":"2026-07-30T12:44:51.576394Z","shell.execute_reply.started":"2026-07-30T12:44:51.148209Z","shell.execute_reply":"2026-07-30T12:44:51.575735Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"criterion = nn.CrossEntropyLoss()\noptimizer = optim.Adam(model.parameters(), lr=0.001, weight_decay=1e-4)\n\nEPOCHS = 100\nbest_val_acc = 0.0\nbest_model_path = WORK_ROOT / \"best_model.pth\"\n\nfor epoch in range(EPOCHS):\n    model.train()\n    train_loss, correct, total = 0, 0, 0\n    for X_batch, y_batch in train_loader:\n        X_batch, y_batch = X_batch.to(device), y_batch.to(device)\n        optimizer.zero_grad()\n        outputs = model(X_batch)\n        loss = criterion(outputs, y_batch)\n        loss.backward()\n        optimizer.step()\n        train_loss += loss.item()\n        _, predicted = outputs.max(1)\n        correct += predicted.eq(y_batch).sum().item()\n        total += y_batch.size(0)\n    train_acc = correct / total\n\n    model.eval()\n    val_correct, val_total = 0, 0\n    with torch.no_grad():\n        for X_batch, y_batch in val_loader:\n            X_batch, y_batch = X_batch.to(device), y_batch.to(device)\n            outputs = model(X_batch)\n            _, predicted = outputs.max(1)\n            val_correct += predicted.eq(y_batch).sum().item()\n            val_total += y_batch.size(0)\n    val_acc = val_correct / val_total\n\n    print(f\"Epoch {epoch+1}/{EPOCHS} | Train Loss: {train_loss/len(train_loader):.4f} | Train Acc: {train_acc:.4f} | Val Acc: {val_acc:.4f}\")\n\n    if val_acc > best_val_acc:\n        best_val_acc = val_acc\n        torch.save(model.state_dict(), best_model_path)\n        print(f\"  -> New best saved (val_acc: {val_acc:.4f})\")\n\nprint(f\"\\nDone. Best val accuracy: {best_val_acc:.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-30T12:47:22.272183Z","iopub.execute_input":"2026-07-30T12:47:22.27262Z","iopub.status.idle":"2026-07-30T12:49:39.530996Z","shell.execute_reply.started":"2026-07-30T12:47:22.272587Z","shell.execute_reply":"2026-07-30T12:49:39.530269Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\nimport json\nfrom pathlib import Path\n\nWORK_ROOT = Path(\"/kaggle/working\")\nFIXED_LEN = 40\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\nwith open(WORK_ROOT / \"sign_to_idx.json\") as f:\n    sign_to_idx = json.load(f)\nidx_to_sign = {v: k for k, v in sign_to_idx.items()}\n\nclass SignClassifier(nn.Module):\n    def __init__(self, input_size=150, hidden_size=128, num_layers=2, num_classes=7):\n        super().__init__()\n        self.lstm = nn.LSTM(\n            input_size=input_size, hidden_size=hidden_size,\n            num_layers=num_layers, batch_first=True,\n            bidirectional=True, dropout=0.3\n        )\n        self.fc = nn.Linear(hidden_size * 2, num_classes)\n\n    def forward(self, x):\n        out, _ = self.lstm(x)\n        out = out[:, -1, :]\n        return self.fc(out)\n\nmodel = SignClassifier(num_classes=len(sign_to_idx)).to(device)\nmodel.load_state_dict(torch.load(WORK_ROOT / \"best_model.pth\", map_location=device))\nmodel.eval()\nprint(\"Model loaded successfully\")\n\ndummy_input = torch.randn(1, FIXED_LEN, 150).to(device)\nonnx_path = WORK_ROOT / \"sign_model.onnx\"\n\ntry:\n    torch.onnx.export(\n        model,\n        dummy_input,\n        str(onnx_path),\n        input_names=[\"input\"],\n        output_names=[\"output\"],\n        dynamic_axes={\"input\": {0: \"batch_size\"}, \"output\": {0: \"batch_size\"}},\n        opset_version=13,\n        dynamo=False\n    )\n    print(\"Export succeeded\")\nexcept Exception as e:\n    print(\"Export FAILED:\", e)\n\nprint(\"File exists:\", onnx_path.exists())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-30T12:54:06.794551Z","iopub.execute_input":"2026-07-30T12:54:06.795408Z","iopub.status.idle":"2026-07-30T12:54:06.82294Z","shell.execute_reply.started":"2026-07-30T12:54:06.795372Z","shell.execute_reply":"2026-07-30T12:54:06.822254Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!pip install onnxscript --quiet\ndummy_input = torch.randn(1, FIXED_LEN, 150).to(device)\nonnx_path = WORK_ROOT / \"sign_model.onnx\"\n\ntry:\n    torch.onnx.export(\n        model,\n        dummy_input,\n        str(onnx_path),\n        input_names=[\"input\"],\n        output_names=[\"output\"],\n        dynamic_axes={\"input\": {0: \"batch_size\"}, \"output\": {0: \"batch_size\"}},\n        opset_version=13\n    )\n    print(\"Export succeeded\")\nexcept Exception as e:\n    print(\"Export FAILED:\", e)\n\nprint(\"File exists:\", onnx_path.exists())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-30T12:55:00.771097Z","iopub.execute_input":"2026-07-30T12:55:00.771365Z","iopub.status.idle":"2026-07-30T12:55:19.792406Z","shell.execute_reply.started":"2026-07-30T12:55:00.771344Z","shell.execute_reply":"2026-07-30T12:55:19.791636Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!pip install onnxruntime --quiet\n\nimport onnxruntime as ort\nimport numpy as np\n\nsession = ort.InferenceSession(str(WORK_ROOT / \"sign_model.onnx\"))\ntest_input = np.random.randn(1, FIXED_LEN, 150).astype(np.float32)\nresult = session.run(None, {\"input\": test_input})\nprint(\"ONNX output shape:\", result[0].shape)\nprint(\"Sample prediction:\", result[0])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-30T12:56:39.892357Z","iopub.execute_input":"2026-07-30T12:56:39.892937Z","iopub.status.idle":"2026-07-30T12:56:43.346185Z","shell.execute_reply.started":"2026-07-30T12:56:39.892843Z","shell.execute_reply":"2026-07-30T12:56:43.345256Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import json\n\nwith open(WORK_ROOT / \"idx_to_sign.json\", 'w') as f:\n    json.dump(idx_to_sign, f)\nprint(\"Saved idx_to_sign.json\")\n\nimport zipfile\nfiles_needed = [\"sign_model.onnx\", \"sign_model.onnx.data\", \"idx_to_sign.json\"]\n\nmissing = [f for f in files_needed if not (WORK_ROOT / f).exists()]\nif missing:\n    print(\"Missing files:\", missing)\nelse:\n    with zipfile.ZipFile(WORK_ROOT / \"deploy_export.zip\", 'w') as zf:\n        for fname in files_needed:\n            zf.write(WORK_ROOT / fname, arcname=fname)\n    print(\"Created deploy_export.zip with only needed files\")\n\n!ls -la /kaggle/working/deploy_export.zip","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-30T13:00:58.578566Z","iopub.execute_input":"2026-07-30T13:00:58.579658Z","iopub.status.idle":"2026-07-30T13:00:58.744825Z","shell.execute_reply.started":"2026-07-30T13:00:58.579608Z","shell.execute_reply":"2026-07-30T13:00:58.744093Z"}},"outputs":[],"execution_count":null}]}