{"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":"nvidiaTeslaT4","dataSources":[{"sourceId":97984,"databundleVersionId":14096757,"sourceType":"competition"}],"dockerImageVersionId":31153,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"ls /kaggle/input/physionet-ecg-image-digitization/train/1006427285","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-10-27T17:41:56.941088Z","iopub.execute_input":"2025-10-27T17:41:56.94197Z","iopub.status.idle":"2025-10-27T17:41:57.297488Z","shell.execute_reply.started":"2025-10-27T17:41:56.941944Z","shell.execute_reply":"2025-10-27T17:41:57.296569Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\nimport pandas as pd","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-27T17:41:57.299233Z","iopub.execute_input":"2025-10-27T17:41:57.299497Z","iopub.status.idle":"2025-10-27T17:41:57.303841Z","shell.execute_reply.started":"2025-10-27T17:41:57.299475Z","shell.execute_reply":"2025-10-27T17:41:57.302966Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train = pd.read_csv('/kaggle/input/physionet-ecg-image-digitization/train.csv')\ntest = pd.read_csv('/kaggle/input/physionet-ecg-image-digitization/test.csv')\nsubmission = pd.read_parquet('/kaggle/input/physionet-ecg-image-digitization/sample_submission.parquet')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-27T17:41:57.304758Z","iopub.execute_input":"2025-10-27T17:41:57.305048Z","iopub.status.idle":"2025-10-27T17:41:57.374297Z","shell.execute_reply.started":"2025-10-27T17:41:57.305024Z","shell.execute_reply":"2025-10-27T17:41:57.373575Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nimport cv2\nimport pandas as pd\nimport numpy as np\nimport os\nfrom tqdm import tqdm\nimport matplotlib.pyplot as plt","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-27T17:41:57.376098Z","iopub.execute_input":"2025-10-27T17:41:57.376345Z","iopub.status.idle":"2025-10-27T17:41:57.381071Z","shell.execute_reply.started":"2025-10-27T17:41:57.376327Z","shell.execute_reply":"2025-10-27T17:41:57.38041Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def get_image_type(filename):\n    \"\"\"Determine image type based on filename\"\"\"\n    type_mapping = {\n        '0001': 'original_color',\n        '0003': 'printed_scanned_color', \n        '0004': 'printed_scanned_bw',\n        '0005': 'mobile_photo_color',\n        '0006': 'mobile_photo_screen',\n        '0009': 'stained_soaked',\n        '0010': 'extensive_damage',\n        '0011': 'mold_color',\n        '0012': 'mold_bw'\n    }\n\n    image_id = filename.split('-')[1].split('.')[0]\n    return type_mapping.get(image_id, 'unknown')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-27T17:41:57.381891Z","iopub.execute_input":"2025-10-27T17:41:57.382168Z","iopub.status.idle":"2025-10-27T17:41:57.396825Z","shell.execute_reply.started":"2025-10-27T17:41:57.382149Z","shell.execute_reply":"2025-10-27T17:41:57.396151Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"**Dataset Transformer class**","metadata":{}},{"cell_type":"code","source":"# ============================================================\n# DATASET : Image → 12 Leads\n# ============================================================\nimport random\nclass MultiLeadECGDataset(Dataset):\n    \"\"\"\n    Dataset pour entraîner le modèle :\n    Input : Image ECG\n    Output : 12 signaux (I, II, III, aVR, aVL, aVF, V1-V6)\n    \"\"\"\n    \n    def __init__(self, train_csv, train_dir, img_size=(512, 512)):\n        \"\"\"\n        Args:\n            train_csv: Fichier train.csv avec id, fs, sig_len\n            train_dir: Dossier contenant les images et signaux\n            img_size: Taille de redimensionnement des images\n        \"\"\"\n        self.metadata = pd.read_csv(train_csv)\n        self.train_dir = train_dir\n        self.img_size = img_size\n        self.lead_names = ['I', 'II', 'III', 'aVR', 'aVL', 'aVF', 'V1', 'V2', 'V3', 'V4', 'V5', 'V6']\n        self.image_used_types = ['original_color', 'printed_scanned_color', 'mobile_photo_color']\n        \n        # Déterminer la longueur maximale des signaux\n        self.max_signal_length = self._get_max_signal_length()\n        \n    def _get_max_signal_length(self):\n        \"\"\"Trouve la longueur maximale des signaux dans le dataset\"\"\"\n        max_len = 0\n        for _, row in self.metadata.iterrows():\n            csv_path = os.path.join(self.train_dir, str(row['id']), f\"{row['id']}.csv\")\n            if os.path.exists(csv_path):\n                df = pd.read_csv(csv_path)\n                max_len = max(max_len, len(df))\n        return max_len\n    \n    def __len__(self):\n        return len(self.metadata)\n    \n    def _get_images_path(self, sample_id):\n        \"\"\"Trouve les images disponible pour cet ID\"\"\"\n        base_dir = os.path.join(self.train_dir, str(sample_id))\n        images_path = []\n        for filename in os.listdir(base_dir):\n            if not filename.lower().endswith(('.png', '.jpg', '.jpeg')):\n                continue\n            image_type = get_image_type(filename)\n            if image_type in self.image_used_types:\n                image_path = os.path.join(base_dir, filename)\n                images_path.append(image_path)\n        return images_path\n\n    \n    def __getitem__(self, idx):\n        sample_id = self.metadata.iloc[idx]['id']\n        fs = self.metadata.iloc[idx]['fs']\n        sig_len = self.metadata.iloc[idx]['sig_len']\n        \n        # 1. Charger l'image\n        img_paths = self._get_images_path(sample_id)\n        if not img_paths:\n            # Image non trouvée, retourner des données vides\n            image = np.zeros((3, *self.img_size), dtype=np.float32)\n            signals = np.zeros((12, self.max_signal_length), dtype=np.float32)\n            mask = np.zeros((12, self.max_signal_length), dtype=np.float32)\n            return {\n                'image': torch.from_numpy(image),\n                'signals': torch.from_numpy(signals),\n                'mask': torch.from_numpy(mask),\n                'id': sample_id\n            }\n        img_idx = random.randint(0, len(img_paths) -1)\n        img_path = img_paths[img_idx]\n        image = cv2.imread(img_path)\n        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n        image = cv2.resize(image, self.img_size)\n        \n        # Normalisation [0, 1]\n        image = image.astype(np.float32) / 255.0\n        \n        # Transpose pour PyTorch (H, W, C) → (C, H, W)\n        image = image.transpose(2, 0, 1)\n        \n        # 2. Charger les signaux (labels)\n        csv_path = os.path.join(self.train_dir, str(sample_id), f\"{sample_id}.csv\")\n        signal_df = pd.read_csv(csv_path)\n        \n        # Initialiser les signaux et le masque\n        signals = np.zeros((12, self.max_signal_length), dtype=np.float32)\n        mask = np.zeros((12, self.max_signal_length), dtype=np.float32)\n        \n        # Remplir chaque lead\n        for i, lead in enumerate(self.lead_names):\n            if lead in signal_df.columns:\n                lead_signal = signal_df[lead].values\n                \n                # Supprimer les NaN\n                valid_indices = ~np.isnan(lead_signal)\n                valid_signal = lead_signal[valid_indices]\n                \n                if len(valid_signal) > 0:\n                    # Stocker le signal\n                    length = min(len(valid_signal), self.max_signal_length)\n                    signals[i, :length] = valid_signal[:length]\n                    mask[i, :length] = 1.0  # Marquer comme valide\n        \n        return {\n            'image': torch.from_numpy(image),\n            'signals': torch.from_numpy(signals),\n            'mask': torch.from_numpy(mask),\n            'id': sample_id,\n            'fs': fs,\n            'sig_len': sig_len\n        }","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-27T17:41:57.39785Z","iopub.execute_input":"2025-10-27T17:41:57.398127Z","iopub.status.idle":"2025-10-27T17:41:57.419546Z","shell.execute_reply.started":"2025-10-27T17:41:57.398098Z","shell.execute_reply":"2025-10-27T17:41:57.418654Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"**Model Definition**","metadata":{}},{"cell_type":"code","source":"# ============================================================\n# MODÈLE U-NET MULTI-OUTPUT\n# ============================================================\n\nclass UNetEncoder(nn.Module):\n    \"\"\"Encoder U-Net pour extraction de features\"\"\"\n    \n    def __init__(self):\n        super().__init__()\n        \n        # Encoder blocks\n        self.enc1 = self._make_encoder_block(3, 64)\n        self.enc2 = self._make_encoder_block(64, 128)\n        self.enc3 = self._make_encoder_block(128, 256)\n        self.enc4 = self._make_encoder_block(256, 512)\n        \n        self.pool = nn.MaxPool2d(2, 2)\n        \n        # Bottleneck\n        self.bottleneck = self._make_encoder_block(512, 1024)\n        \n    def _make_encoder_block(self, in_channels, out_channels):\n        return nn.Sequential(\n            nn.Conv2d(in_channels, out_channels, 3, padding=1),\n            nn.BatchNorm2d(out_channels),\n            nn.ReLU(inplace=True),\n            nn.Conv2d(out_channels, out_channels, 3, padding=1),\n            nn.BatchNorm2d(out_channels),\n            nn.ReLU(inplace=True)\n        )\n    \n    def forward(self, x):\n        # Encoder path avec skip connections\n        e1 = self.enc1(x)        # [B, 64, H, W]\n        e2 = self.enc2(self.pool(e1))  # [B, 128, H/2, W/2]\n        e3 = self.enc3(self.pool(e2))  # [B, 256, H/4, W/4]\n        e4 = self.enc4(self.pool(e3))  # [B, 512, H/8, W/8]\n        \n        bottleneck = self.bottleneck(self.pool(e4))  # [B, 1024, H/16, W/16]\n        \n        return bottleneck, [e4, e3, e2, e1]\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-27T17:41:57.420409Z","iopub.execute_input":"2025-10-27T17:41:57.42061Z","iopub.status.idle":"2025-10-27T17:41:57.43653Z","shell.execute_reply.started":"2025-10-27T17:41:57.420595Z","shell.execute_reply":"2025-10-27T17:41:57.435738Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"**Signal decoder**","metadata":{}},{"cell_type":"code","source":"\nclass MultiLeadDecoder(nn.Module):\n    \"\"\"\n    Decoder qui produit 12 signaux 1D\n    \"\"\"\n    \n    def __init__(self, signal_length=5000):\n        super().__init__()\n        self.signal_length = signal_length\n        \n        # Global pooling pour réduire la dimensionnalité spatiale\n        self.gap = nn.AdaptiveAvgPool2d(1)\n        \n        # Shared layers\n        self.shared_fc = nn.Sequential(\n            nn.Linear(1024, 2048),\n            nn.ReLU(inplace=True),\n            nn.Dropout(0.3),\n            nn.Linear(2048, 4096),\n            nn.ReLU(inplace=True),\n            nn.Dropout(0.2)\n        )\n        \n        # 12 têtes de sortie (une pour chaque lead)\n        self.lead_heads = nn.ModuleList([\n            nn.Sequential(\n                nn.Linear(4096, 2048),\n                nn.ReLU(inplace=True),\n                nn.Linear(2048, signal_length)\n            ) for _ in range(12)\n        ])\n        \n    def forward(self, x):\n        # x: [B, 1024, H, W]\n        \n        # Global pooling\n        x = self.gap(x)  # [B, 1024, 1, 1]\n        x = x.view(x.size(0), -1)  # [B, 1024]\n        \n        # Shared features\n        shared_features = self.shared_fc(x)  # [B, 4096]\n        \n        # Prédire chaque lead séparément\n        lead_outputs = []\n        for head in self.lead_heads:\n            lead_signal = head(shared_features)  # [B, signal_length]\n            lead_outputs.append(lead_signal)\n        \n        # Empiler les 12 leads\n        outputs = torch.stack(lead_outputs, dim=1)  # [B, 12, signal_length]\n        \n        return outputs\n\n\nclass MultiLeadECGModel(nn.Module):\n    \"\"\"Modèle complet : Image → 12 signaux\"\"\"\n    \n    def __init__(self, signal_length=5000):\n        super().__init__()\n        self.encoder = UNetEncoder()\n        self.decoder = MultiLeadDecoder(signal_length)\n        \n    def forward(self, image):\n        # Encoder\n        features, skips = self.encoder(image)\n        \n        # Decoder\n        signals = self.decoder(features)  # [B, 12, signal_length]\n        \n        return signals","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-27T17:41:57.437341Z","iopub.execute_input":"2025-10-27T17:41:57.43805Z","iopub.status.idle":"2025-10-27T17:41:57.454237Z","shell.execute_reply.started":"2025-10-27T17:41:57.438024Z","shell.execute_reply":"2025-10-27T17:41:57.453545Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n# ============================================================\n# LOSS FUNCTION AVEC MASQUE\n# ============================================================\n\nclass MaskedMSELoss(nn.Module):\n    \"\"\"MSE Loss qui ignore les valeurs NaN (masquées)\"\"\"\n    \n    def __init__(self):\n        super().__init__()\n    \n    def forward(self, predictions, targets, mask):\n        \"\"\"\n        Args:\n            predictions: [B, 12, signal_length]\n            targets: [B, 12, signal_length]\n            mask: [B, 12, signal_length] - 1 pour valide, 0 pour NaN\n        \"\"\"\n        # Calculer l'erreur seulement sur les valeurs valides\n        squared_error = (predictions - targets) ** 2\n        masked_error = squared_error * mask\n        \n        # Moyenne sur les valeurs valides\n        num_valid = mask.sum()\n        if num_valid > 0:\n            loss = masked_error.sum() / num_valid\n        else:\n            loss = torch.tensor(0.0, device=predictions.device)\n        \n        return loss","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-27T17:41:57.455035Z","iopub.execute_input":"2025-10-27T17:41:57.455303Z","iopub.status.idle":"2025-10-27T17:41:57.475634Z","shell.execute_reply.started":"2025-10-27T17:41:57.455275Z","shell.execute_reply":"2025-10-27T17:41:57.474665Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n# ============================================================\n# TRAINING FUNCTION\n# ============================================================\n\ndef train_multilead_model(model, train_loader, val_loader, num_epochs=50, device='cuda'):\n    \"\"\"Entraîne le modèle multi-lead\"\"\"\n    \n    criterion = MaskedMSELoss()\n    optimizer = optim.Adam(model.parameters(), lr=0.001)\n    scheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='min', patience=5, factor=0.5)\n    \n    history = {'train_loss': [], 'val_loss': []}\n    best_val_loss = float('inf')\n    \n    for epoch in range(num_epochs):\n        # ===== TRAINING =====\n        model.train()\n        train_loss = 0.0\n        \n        pbar = tqdm(train_loader, desc=f'Epoch {epoch+1}/{num_epochs} [Train]')\n        for batch in pbar:\n            images = batch['image'].to(device)\n            signals = batch['signals'].to(device)\n            mask = batch['mask'].to(device)\n            \n            # Forward\n            optimizer.zero_grad()\n            predictions = model(images)\n            \n            # Ajuster la longueur si nécessaire\n            min_len = min(predictions.shape[2], signals.shape[2])\n            predictions = predictions[:, :, :min_len]\n            signals = signals[:, :, :min_len]\n            mask = mask[:, :, :min_len]\n            \n            # Loss\n            loss = criterion(predictions, signals, mask)\n            \n            # Backward\n            loss.backward()\n            optimizer.step()\n            \n            train_loss += loss.item()\n            pbar.set_postfix({'loss': loss.item()})\n        \n        train_loss /= len(train_loader)\n        history['train_loss'].append(train_loss)\n        \n        # ===== VALIDATION =====\n        model.eval()\n        val_loss = 0.0\n        \n        with torch.no_grad():\n            pbar = tqdm(val_loader, desc=f'Epoch {epoch+1}/{num_epochs} [Val]')\n            for batch in pbar:\n                images = batch['image'].to(device)\n                signals = batch['signals'].to(device)\n                mask = batch['mask'].to(device)\n                \n                predictions = model(images)\n                \n                min_len = min(predictions.shape[2], signals.shape[2])\n                predictions = predictions[:, :, :min_len]\n                signals = signals[:, :, :min_len]\n                mask = mask[:, :, :min_len]\n                \n                loss = criterion(predictions, signals, mask)\n                val_loss += loss.item()\n                pbar.set_postfix({'loss': loss.item()})\n        \n        val_loss /= len(val_loader)\n        history['val_loss'].append(val_loss)\n        \n        # Scheduler\n        scheduler.step(val_loss)\n        \n        # Save best model\n        if val_loss < best_val_loss:\n            best_val_loss = val_loss\n            torch.save({\n                'epoch': epoch,\n                'model_state_dict': model.state_dict(),\n                'optimizer_state_dict': optimizer.state_dict(),\n                'val_loss': val_loss,\n            }, 'best_multilead_ecg_model.pth')\n            print(f'✓ Nouveau meilleur modèle ! Val Loss: {val_loss:.6f}')\n        \n        print(f'Epoch {epoch+1}/{num_epochs} - Train Loss: {train_loss:.6f}, Val Loss: {val_loss:.6f}\\n')\n    \n    return history\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-27T17:41:57.478827Z","iopub.execute_input":"2025-10-27T17:41:57.479558Z","iopub.status.idle":"2025-10-27T17:41:57.495873Z","shell.execute_reply.started":"2025-10-27T17:41:57.479538Z","shell.execute_reply":"2025-10-27T17:41:57.495058Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n\n# ============================================================\n# INFERENCE POUR TEST SET\n# ============================================================\n\ndef predict_test_set(model, test_df, test_dir, device='cuda', img_size=(512, 512)):\n    \"\"\"Prédire sur le test set\"\"\"\n    \n    model.eval()\n    predictions = {}\n    \n    lead_names = ['I', 'II', 'III', 'aVR', 'aVL', 'aVF', 'V1', 'V2', 'V3', 'V4', 'V5', 'V6']\n    \n    with torch.no_grad():\n        for _, row in tqdm(test_df.iterrows(), total=len(test_df), desc='Predicting'):\n            image_id = row['id']\n            requested_lead = row['lead']\n            target_length = row['number_of_rows']\n            \n            # Charger l'image\n            img_path = os.path.join(test_dir, f\"{image_id}.png\")\n            \n            if not os.path.exists(img_path):\n                # Image non trouvée\n                predictions[(image_id, requested_lead)] = np.zeros(target_length)\n                continue\n            \n            # Préprocessing\n            image = cv2.imread(img_path)\n            image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n            image = cv2.resize(image, img_size)\n            image = image.astype(np.float32) / 255.0\n            image = image.transpose(2, 0, 1)\n            \n            # Convertir en tensor\n            image_tensor = torch.from_numpy(image).unsqueeze(0).to(device)\n            \n            # Prédire\n            all_signals = model(image_tensor)  # [1, 12, signal_length]\n            \n            # Extraire le lead demandé\n            lead_idx = lead_names.index(requested_lead)\n            signal = all_signals[0, lead_idx, :].cpu().numpy()\n            \n            # Rééchantillonner à la longueur demandée\n            if len(signal) != target_length:\n                x_old = np.linspace(0, 1, len(signal))\n                x_new = np.linspace(0, 1, target_length)\n                signal = np.interp(x_new, x_old, signal)\n            \n            predictions[(image_id, requested_lead)] = signal\n    \n    return predictions","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-27T17:41:57.496571Z","iopub.execute_input":"2025-10-27T17:41:57.496875Z","iopub.status.idle":"2025-10-27T17:41:57.510909Z","shell.execute_reply.started":"2025-10-27T17:41:57.496855Z","shell.execute_reply":"2025-10-27T17:41:57.510227Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n# ============================================================\n# PIPELINE COMPLET\n# ============================================================\n\ndef main_pipeline():\n    \"\"\"Pipeline complet d'entraînement\"\"\"\n    \n    print(\"=\"*70)\n    print(\"🚀 TRAINING MULTI-LEAD ECG MODEL\")\n    print(\"=\"*70)\n    \n    # Configuration\n    BATCH_SIZE = 4\n    NUM_EPOCHS = 50\n    IMG_SIZE = (512, 512)\n    DEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n    \n    print(f\"\\n📱 Device: {DEVICE}\")\n    print(f\"🖼️  Image size: {IMG_SIZE}\")\n    print(f\"📦 Batch size: {BATCH_SIZE}\")\n    \n    # Dataset\n    print(\"\\n📊 Chargement des données...\")\n    dataset = MultiLeadECGDataset(\n        train_csv='/kaggle/input/physionet-ecg-image-digitization/train.csv',\n        train_dir='/kaggle/input/physionet-ecg-image-digitization/train',\n        img_size=IMG_SIZE\n    )\n    \n    print(f\"   Max signal length: {dataset.max_signal_length}\")\n    \n    # Split\n    train_size = int(0.8 * len(dataset))\n    val_size = len(dataset) - train_size\n    train_dataset, val_dataset = torch.utils.data.random_split(dataset, [train_size, val_size])\n    \n    train_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True, num_workers=2)\n    val_loader = DataLoader(val_dataset, batch_size=BATCH_SIZE, shuffle=False, num_workers=2)\n    \n    print(f\"   Train: {len(train_dataset)} samples\")\n    print(f\"   Val: {len(val_dataset)} samples\")\n    \n    # Modèle\n    print(\"\\n🏗️  Construction du modèle...\")\n    model = MultiLeadECGModel(signal_length=dataset.max_signal_length).to(DEVICE)\n    \n    num_params = sum(p.numel() for p in model.parameters() if p.requires_grad)\n    print(f\"   Paramètres: {num_params:,}\")\n    \n    # Training\n    print(\"\\n🎯 Début du training...\\n\")\n    history = train_multilead_model(model, train_loader, val_loader, num_epochs=NUM_EPOCHS, device=DEVICE)\n    \n    # Plot history\n    plt.figure(figsize=(10, 5))\n    plt.plot(history['train_loss'], label='Train Loss')\n    plt.plot(history['val_loss'], label='Val Loss')\n    plt.xlabel('Epoch')\n    plt.ylabel('Loss')\n    plt.title('Training History')\n    plt.legend()\n    plt.grid(True, alpha=0.3)\n    plt.savefig('training_history.png')\n    plt.show()\n    \n    print(\"\\n✅ Training terminé !\")\n    print(\"📁 Modèle sauvegardé : best_multilead_ecg_model.pth\")\n\n# Pour lancer\n# main_pipeline()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-27T17:41:57.511784Z","iopub.execute_input":"2025-10-27T17:41:57.512063Z","iopub.status.idle":"2025-10-27T17:41:57.53084Z","shell.execute_reply.started":"2025-10-27T17:41:57.512037Z","shell.execute_reply":"2025-10-27T17:41:57.530019Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Training","metadata":{}},{"cell_type":"code","source":"# main_pipeline()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-27T17:41:57.531682Z","iopub.execute_input":"2025-10-27T17:41:57.532031Z","iopub.status.idle":"2025-10-27T17:41:57.548283Z","shell.execute_reply.started":"2025-10-27T17:41:57.532012Z","shell.execute_reply":"2025-10-27T17:41:57.547485Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import cv2\nimport matplotlib.pyplot as plt\n\nimg = cv2.imread(\"/kaggle/input/physionet-ecg-image-digitization/train/1006867983/1006867983-0001.png\")\nb, g, r = cv2.split(img)\n\nplt.figure(figsize=(10,3))\nplt.subplot(131); plt.imshow(r, cmap='gray'); plt.title(\"Canal Rouge\")\nplt.subplot(132); plt.imshow(g, cmap='gray'); plt.title(\"Canal Vert\")\nplt.subplot(133); plt.imshow(b); plt.title(\"Canal Bleu\")\nplt.show()\n\ncv2.imwrite('image.png', b)\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-27T17:41:57.549214Z","iopub.execute_input":"2025-10-27T17:41:57.549768Z","iopub.status.idle":"2025-10-27T17:41:58.721272Z","shell.execute_reply.started":"2025-10-27T17:41:57.549739Z","shell.execute_reply":"2025-10-27T17:41:58.720565Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def get_image_type(filename):\n    \"\"\"Determine image type based on filename\"\"\"\n    type_mapping = {\n        '0001': 'original_color',\n        '0003': 'printed_scanned_color', \n        '0004': 'printed_scanned_bw',\n        '0005': 'mobile_photo_color',\n        '0006': 'mobile_photo_screen',\n        '0009': 'stained_soaked',\n        '0010': 'extensive_damage',\n        '0011': 'mold_color',\n        '0012': 'mold_bw'\n    }\n\n    image_id = filename.split('-')[1].split('.')[0]\n    return type_mapping.get(image_id, 'unknown')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-27T17:41:58.722053Z","iopub.execute_input":"2025-10-27T17:41:58.722258Z","iopub.status.idle":"2025-10-27T17:41:58.727087Z","shell.execute_reply.started":"2025-10-27T17:41:58.722243Z","shell.execute_reply":"2025-10-27T17:41:58.726267Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\noriginal_count = 0\ntotal_count = 0\ndef check_all_uniform(train_directory):\n    global original_count, total_count\n    for root, _, files in os.walk(train_directory):\n        for file in files:\n            if file.lower().endswith(('.png', '.jpg', '.jpeg', 'JPG')):\n                image_type = get_image_type(file)\n                if image_type == 'original_color':\n                    original_count += 1\n                total_count += 1\n\ncheck_all_uniform('/kaggle/input/physionet-ecg-image-digitization/train')\nprint('original_count', original_count, 'total_count', total_count)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-27T17:41:58.728094Z","iopub.execute_input":"2025-10-27T17:41:58.728624Z","iopub.status.idle":"2025-10-27T17:42:00.01999Z","shell.execute_reply.started":"2025-10-27T17:41:58.728597Z","shell.execute_reply":"2025-10-27T17:42:00.019145Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\n\ntrain = pd.read_csv('/kaggle/input/physionet-ecg-image-digitization/train.csv')\ntest = pd.read_csv('/kaggle/input/physionet-ecg-image-digitization/test.csv')\nsubmission = pd.read_parquet('/kaggle/input/physionet-ecg-image-digitization/sample_submission.parquet')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-27T17:42:00.020813Z","iopub.execute_input":"2025-10-27T17:42:00.021033Z","iopub.status.idle":"2025-10-27T17:42:00.067901Z","shell.execute_reply.started":"2025-10-27T17:42:00.021016Z","shell.execute_reply":"2025-10-27T17:42:00.067174Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(train.info())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-27T17:42:00.06882Z","iopub.execute_input":"2025-10-27T17:42:00.069038Z","iopub.status.idle":"2025-10-27T17:42:00.080919Z","shell.execute_reply.started":"2025-10-27T17:42:00.069022Z","shell.execute_reply":"2025-10-27T17:42:00.079959Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(test.info())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-27T17:42:00.081931Z","iopub.execute_input":"2025-10-27T17:42:00.082342Z","iopub.status.idle":"2025-10-27T17:42:00.101102Z","shell.execute_reply.started":"2025-10-27T17:42:00.08231Z","shell.execute_reply":"2025-10-27T17:42:00.099975Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"label_1_path = \"/kaggle/input/physionet-ecg-image-digitization/train/1006867983/1006867983.csv\"\nlabel = pd.read_csv(label_1_path)\nprint(label.head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-27T17:42:00.102351Z","iopub.execute_input":"2025-10-27T17:42:00.103371Z","iopub.status.idle":"2025-10-27T17:42:00.126628Z","shell.execute_reply.started":"2025-10-27T17:42:00.10334Z","shell.execute_reply":"2025-10-27T17:42:00.125706Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def _get_images_path(sample_id):\n    \"\"\"Trouve les images disponible pour cet ID\"\"\"\n    train_dir = '/kaggle/input/physionet-ecg-image-digitization/train'\n    base_dir = os.path.join(train_dir, str(sample_id))\n    images_path = []\n    for filename in os.listdir(base_dir):\n        \n        if not filename.lower().endswith(('.png', '.jpg', '.jpeg')):\n            continue\n        print(filename)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-27T17:42:00.127348Z","iopub.execute_input":"2025-10-27T17:42:00.127549Z","iopub.status.idle":"2025-10-27T17:42:00.133028Z","shell.execute_reply.started":"2025-10-27T17:42:00.127534Z","shell.execute_reply":"2025-10-27T17:42:00.132194Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n_get_images_path('1006427285')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-27T17:42:00.133813Z","iopub.execute_input":"2025-10-27T17:42:00.134092Z","iopub.status.idle":"2025-10-27T17:42:00.149895Z","shell.execute_reply.started":"2025-10-27T17:42:00.134072Z","shell.execute_reply":"2025-10-27T17:42:00.149167Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"ls /kaggle/working","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-27T17:42:00.150915Z","iopub.execute_input":"2025-10-27T17:42:00.151166Z","iopub.status.idle":"2025-10-27T17:42:00.496007Z","shell.execute_reply.started":"2025-10-27T17:42:00.151148Z","shell.execute_reply":"2025-10-27T17:42:00.494893Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\n\n# Charger le modèle\nmodel = MultiLeadECGModel(signal_length=10250)\nmodel = model.to('cuda')\ncheckpoint = torch.load(\"/kaggle/working/best_multilead_ecg_model.pth\", map_location='cuda')\n\n# Charger les poids\nmodel.load_state_dict(checkpoint['model_state_dict'])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-27T17:42:00.497506Z","iopub.execute_input":"2025-10-27T17:42:00.498412Z","iopub.status.idle":"2025-10-27T17:42:09.685132Z","shell.execute_reply.started":"2025-10-27T17:42:00.498385Z","shell.execute_reply":"2025-10-27T17:42:09.684228Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def create_submission_from_multilead(predictions, test_df, filename='submission.csv'):\n    \"\"\"\n    Crée le fichier de soumission au format requis\n    \n    Format attendu :\n    id, value\n    {image_id}_{row_idx}_{lead}, {signal_value}\n    \n    Args:\n        predictions: dict {(image_id, lead): signal_array}\n        test_df: DataFrame du test\n        filename: Nom du fichier de sortie\n    \"\"\"\n    submission_data = []\n    \n    for _, row in test_df.iterrows():\n        image_id = row['id']\n        lead = row['lead']\n        n_rows = row['number_of_rows']\n        \n        # Récupérer le signal prédit\n        signal = predictions.get((image_id, lead), np.zeros(n_rows))\n        \n        # Créer les entrées pour chaque point du signal\n        for i in range(n_rows):\n            submission_id = f\"{image_id}_{i}_{lead}\"\n            value = float(signal[i])\n            \n            submission_data.append({\n                'id': submission_id,\n                'value': value\n            })\n    \n    # Créer le DataFrame\n    submission_df = pd.DataFrame(submission_data)\n    \n    # Statistiques\n    print(f\"\\n📊 Statistiques de la soumission :\")\n    print(f\"   Total d'entrées : {len(submission_df):,}\")\n    print(f\"   Range : [{submission_df['value'].min():.4f}, {submission_df['value'].max():.4f}]\")\n    print(f\"   Moyenne : {submission_df['value'].mean():.4f}\")\n    print(f\"   Std : {submission_df['value'].std():.4f}\")\n    \n    # Vérifier NaN\n    if submission_df['value'].isna().any():\n        print(f\"⚠️  {submission_df['value'].isna().sum()} valeurs NaN, remplacement par 0\")\n        submission_df['value'].fillna(0, inplace=True)\n    \n    # Sauvegarder\n    submission_df.to_csv(filename, index=False)\n    print(f\"\\n✅ Soumission sauvegardée : {filename}\")\n    print(f\"📏 Shape : {submission_df.shape}\")\n    \n    return submission_df\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-27T17:42:09.686153Z","iopub.execute_input":"2025-10-27T17:42:09.686776Z","iopub.status.idle":"2025-10-27T17:42:09.694746Z","shell.execute_reply.started":"2025-10-27T17:42:09.686747Z","shell.execute_reply":"2025-10-27T17:42:09.693767Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"device = 'cuda'\ndef submission_from_pretrained():\n    \"\"\"Créer soumission depuis un modèle pré-entraîné\"\"\"\n    \n    print(\"=\"*70)\n    print(\"🎯 SOUMISSION DEPUIS MODÈLE PRÉ-ENTRAÎNÉ\")\n    print(\"=\"*70)\n    \n    # Charger test\n    test = pd.read_csv('/kaggle/input/physionet-ecg-image-digitization/test.csv')\n    test_dir = '/kaggle/input/physionet-ecg-image-digitization/test/'\n    \n    # Prédire\n    predictions = predict_test_set(model, test, test_dir, device=device)\n    \n    # Créer soumission\n    submission = create_submission_from_multilead(predictions, test, 'submission.csv')\n    \n    print(\"\\n✅ Soumission prête !\")\n    \n    return submission","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-27T17:42:09.695842Z","iopub.execute_input":"2025-10-27T17:42:09.696194Z","iopub.status.idle":"2025-10-27T17:42:09.714856Z","shell.execute_reply.started":"2025-10-27T17:42:09.696175Z","shell.execute_reply":"2025-10-27T17:42:09.713893Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"submission_from_pretrained()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-27T17:42:09.715871Z","iopub.execute_input":"2025-10-27T17:42:09.716169Z","iopub.status.idle":"2025-10-27T17:42:13.029191Z","shell.execute_reply.started":"2025-10-27T17:42:09.716144Z","shell.execute_reply":"2025-10-27T17:42:13.028499Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(next(model.parameters()).device)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-27T17:42:13.03237Z","iopub.execute_input":"2025-10-27T17:42:13.032605Z","iopub.status.idle":"2025-10-27T17:42:13.037233Z","shell.execute_reply.started":"2025-10-27T17:42:13.032587Z","shell.execute_reply":"2025-10-27T17:42:13.036391Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}