{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":59093,"databundleVersionId":7469972,"sourceType":"competition"}],"dockerImageVersionId":31234,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"pip uninstall -y scikit-learn","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-19T17:00:10.349117Z","iopub.execute_input":"2025-12-19T17:00:10.349521Z","iopub.status.idle":"2025-12-19T17:00:12.095790Z","shell.execute_reply.started":"2025-12-19T17:00:10.349479Z","shell.execute_reply":"2025-12-19T17:00:12.094079Z"}},"outputs":[{"name":"stdout","text":"\u001b[33mWARNING: Skipping scikit-learn as it is not installed.\u001b[0m\u001b[33m\n\u001b[0mNote: you may need to restart the kernel to use updated packages.\n","output_type":"stream"}],"execution_count":1},{"cell_type":"code","source":"pip install scikit-learn","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-19T17:00:12.097575Z","iopub.execute_input":"2025-12-19T17:00:12.098079Z","iopub.status.idle":"2025-12-19T17:00:20.576757Z","shell.execute_reply.started":"2025-12-19T17:00:12.098031Z","shell.execute_reply":"2025-12-19T17:00:20.574620Z"}},"outputs":[{"name":"stdout","text":"Collecting scikit-learn\n  Using cached scikit_learn-1.8.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl.metadata (11 kB)\nRequirement already satisfied: numpy>=1.24.1 in /usr/local/lib/python3.12/dist-packages (from scikit-learn) (2.0.2)\nRequirement already satisfied: scipy>=1.10.0 in /usr/local/lib/python3.12/dist-packages (from scikit-learn) (1.15.3)\nRequirement already satisfied: joblib>=1.3.0 in /usr/local/lib/python3.12/dist-packages (from scikit-learn) (1.5.3)\nRequirement already satisfied: threadpoolctl>=3.2.0 in /usr/local/lib/python3.12/dist-packages (from scikit-learn) (3.6.0)\nUsing cached scikit_learn-1.8.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl (8.9 MB)\nInstalling collected packages: scikit-learn\nSuccessfully installed scikit-learn-1.8.0\nNote: you may need to restart the kernel to use updated packages.\n","output_type":"stream"}],"execution_count":2},{"cell_type":"code","source":"pip uninstall scikit-learn imblearn --yes\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-19T17:00:20.580444Z","iopub.execute_input":"2025-12-19T17:00:20.581379Z","iopub.status.idle":"2025-12-19T17:00:22.407871Z","shell.execute_reply.started":"2025-12-19T17:00:20.581335Z","shell.execute_reply":"2025-12-19T17:00:22.406721Z"}},"outputs":[{"name":"stdout","text":"Found existing installation: scikit-learn 1.8.0\nUninstalling scikit-learn-1.8.0:\n  Successfully uninstalled scikit-learn-1.8.0\n\u001b[33mWARNING: Skipping imblearn as it is not installed.\u001b[0m\u001b[33m\n\u001b[0mNote: you may need to restart the kernel to use updated packages.\n","output_type":"stream"}],"execution_count":3},{"cell_type":"code","source":"pip install scikit-learn==1.2.2 imbalanced-learn==0.10.1\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-19T17:00:22.409519Z","iopub.execute_input":"2025-12-19T17:00:22.409956Z"}},"outputs":[{"name":"stdout","text":"Collecting scikit-learn==1.2.2\n  Using cached scikit-learn-1.2.2.tar.gz (7.3 MB)\n  Installing build dependencies ... \u001b[?25l\u001b[?25hdone\n  Getting requirements to build wheel ... \u001b[?25l\u001b[?25hdone\n  Preparing metadata (pyproject.toml) ... \u001b[?25l\u001b[?25hdone\nCollecting imbalanced-learn==0.10.1\n  Using cached imbalanced_learn-0.10.1-py3-none-any.whl.metadata (8.2 kB)\nRequirement already satisfied: numpy>=1.17.3 in /usr/local/lib/python3.12/dist-packages (from scikit-learn==1.2.2) (2.0.2)\nRequirement already satisfied: scipy>=1.3.2 in /usr/local/lib/python3.12/dist-packages (from scikit-learn==1.2.2) (1.15.3)\nRequirement already satisfied: joblib>=1.1.1 in /usr/local/lib/python3.12/dist-packages (from scikit-learn==1.2.2) (1.5.3)\nRequirement already satisfied: threadpoolctl>=2.0.0 in /usr/local/lib/python3.12/dist-packages (from scikit-learn==1.2.2) (3.6.0)\nUsing cached imbalanced_learn-0.10.1-py3-none-any.whl (226 kB)\nBuilding wheels for collected packages: scikit-learn\n","output_type":"stream"}],"execution_count":null},{"cell_type":"code","source":"# ==================== TCN ON 50K SAMPLES ====================\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport numpy as np\nimport pandas as pd\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import accuracy_score, classification_report, confusion_matrix\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nimport time\nimport os\nimport warnings\nwarnings.filterwarnings('ignore')\nfrom tqdm import tqdm\n\nprint(\"=\"*80)\nprint(\"⏱️  TCN TRAINING ON 50,000 SAMPLES\")\nprint(\"=\"*80)\n\n# 1. DATA LOADING\nprint(\"\\n📂 LOADING DATA...\")\ncsv_path = '/kaggle/input/hms-harmful-brain-activity-classification/train.csv'\ntrain_csv = pd.read_csv(csv_path)\nprint(f\"✅ Loaded {len(train_csv):,} samples\")\n\n# Process labels - FIXED: Handle all-zero rows properly\nlabel_cols = ['seizure_vote', 'lpd_vote', 'gpd_vote', 'lrda_vote', 'grda_vote', 'other_vote']\nvotes = train_csv[label_cols].values.astype(float)\n\n# Check for rows with all zeros\nzero_rows = np.all(votes == 0, axis=1)\nif zero_rows.any():\n    print(f\"⚠️  Found {zero_rows.sum():,} rows with all zero votes. Setting to uniform distribution.\")\n    votes[zero_rows] = 1.0  # Set to uniform distribution\n\nrow_sums = votes.sum(axis=1, keepdims=True)\nrow_sums[row_sums == 0] = 1  # Avoid division by zero\nprobabilities = votes / row_sums\ntrain_csv['dominant_class'] = np.argmax(probabilities, axis=1)\n\n# Select 50K samples\nn_samples = 50000\nif len(train_csv) > n_samples:\n    _, sample_indices = train_test_split(\n        range(len(train_csv)),\n        test_size=n_samples,\n        random_state=42,\n        stratify=train_csv['dominant_class']\n    )\n    selected_df = train_csv.iloc[sample_indices].copy()\nelse:\n    selected_df = train_csv.copy()\n    n_samples = len(selected_df)\n\nprint(f\"Selected {n_samples:,} samples\")\n\n# 2. SEQUENCE DATA EXTRACTION\nprint(\"\\n🔧 EXTRACTING SEQUENCE DATA...\")\n\nDATA_DIR = '/kaggle/input/hms-harmful-brain-activity-classification/train_eegs'\n\ndef extract_tcn_sequence(eeg_data):\n    \"\"\"Extract sequence data for TCN (channels first)\"\"\"\n    if eeg_data.size == 0 or eeg_data.shape[0] == 0:\n        return np.zeros((4, 100), dtype=np.float32)\n    \n    # Downsample to 100 timesteps\n    if eeg_data.shape[0] > 100:\n        indices = np.linspace(0, eeg_data.shape[0]-1, 100).astype(int)\n        eeg_data = eeg_data[indices, :]\n    elif eeg_data.shape[0] < 100:\n        # Pad if shorter than 100\n        pad_width = ((0, 100 - eeg_data.shape[0]), (0, 0))\n        eeg_data = np.pad(eeg_data, pad_width, mode='constant')\n    \n    # Use first 4 channels\n    if eeg_data.shape[1] > 4:\n        eeg_data = eeg_data[:, :4]\n    elif eeg_data.shape[1] < 4:\n        eeg_data = np.pad(eeg_data, ((0, 0), (0, 4 - eeg_data.shape[1])), mode='constant')\n    \n    # Normalize per channel with robust normalization\n    for i in range(eeg_data.shape[1]):\n        channel = eeg_data[:, i]\n        # Check for constant channel\n        if np.std(channel) < 1e-8:\n            eeg_data[:, i] = np.random.normal(0, 1e-8, size=channel.shape)\n        else:\n            # Robust normalization\n            median = np.median(channel)\n            iqr = np.percentile(channel, 75) - np.percentile(channel, 25)\n            if iqr < 1e-8:\n                eeg_data[:, i] = (channel - median) / (np.std(channel) + 1e-8)\n            else:\n                eeg_data[:, i] = (channel - median) / (iqr + 1e-8)\n    \n    # Transpose to [channels, timesteps] for TCN\n    return eeg_data.T.astype(np.float32)\n\n# Process samples\nsequence_list = []\nlabels_list = []\neeg_ids_list = []\nfailed = 0\n\nprocessing_start = time.time()\n\nfor idx in tqdm(range(n_samples), desc=\"Processing EEGs\"):\n    try:\n        sample_id = selected_df.iloc[idx]['eeg_id']\n        eeg_path = f\"{DATA_DIR}/{sample_id}.parquet\"\n        \n        if os.path.exists(eeg_path):\n            eeg_df = pd.read_parquet(eeg_path)\n            if not eeg_df.empty and eeg_df.shape[0] > 10:  # Reduced minimum length\n                eeg_data = eeg_df.values.astype(np.float32)\n                \n                # Handle NaN values\n                if np.any(np.isnan(eeg_data)):\n                    # Fill NaN with forward fill then backward fill\n                    eeg_df_clean = eeg_df.fillna(method='ffill').fillna(method='bfill').fillna(0)\n                    eeg_data = eeg_df_clean.values.astype(np.float32)\n                \n                sequence = extract_tcn_sequence(eeg_data)\n                \n                # Check if sequence is valid\n                if not np.any(np.isnan(sequence)) and not np.any(np.isinf(sequence)):\n                    sequence_list.append(sequence)\n                    labels_list.append(selected_df.iloc[idx]['dominant_class'])\n                    eeg_ids_list.append(sample_id)\n                else:\n                    failed += 1\n            else:\n                failed += 1\n        else:\n            failed += 1\n    except Exception as e:\n        failed += 1\n        # Uncomment for debugging\n        # print(f\"Error processing sample {sample_id}: {e}\")\n\nprocessing_time = time.time() - processing_start\nprint(f\"\\n✅ Processed {len(sequence_list):,}/{n_samples} sequences\")\nprint(f\"   Failed: {failed:,}\")\nprint(f\"   Time: {processing_time:.1f}s\")\n\n# Check if we have enough data\nif len(sequence_list) < 1000:\n    raise ValueError(f\"Only {len(sequence_list)} sequences processed successfully. Need at least 1000.\")\n\n# 3. DATA PREPARATION\ny = np.array(labels_list, dtype=np.int64)  # Changed to int64 for PyTorch compatibility\n\n# Split data\ntrain_idx, val_idx = train_test_split(\n    range(len(sequence_list)),\n    test_size=0.1,\n    random_state=42,\n    stratify=y\n)\n\nprint(f\"\\n📊 DATASET SIZES:\")\nprint(f\"   Training: {len(train_idx):,} sequences\")\nprint(f\"   Validation: {len(val_idx):,} sequences\")\nprint(f\"   Sequence shape: {sequence_list[0].shape}\")\n\n# 4. TCN MODEL\nclass EEG_TCN(nn.Module):\n    \"\"\"Enhanced TCN with residual connections and stability improvements\"\"\"\n    def __init__(self, input_channels=4, num_classes=6, num_filters=64):\n        super().__init__()\n        \n        # First residual block\n        self.tcn1 = nn.Conv1d(input_channels, num_filters, kernel_size=3, padding=1, bias=False)\n        self.tcn2 = nn.Conv1d(num_filters, num_filters, kernel_size=3, padding=1, bias=False)\n        self.bn1 = nn.BatchNorm1d(num_filters)\n        self.bn2 = nn.BatchNorm1d(num_filters)\n        self.downsample1 = nn.Conv1d(input_channels, num_filters, kernel_size=1, bias=False) if input_channels != num_filters else nn.Identity()\n        \n        # Second residual block\n        self.tcn3 = nn.Conv1d(num_filters, num_filters, kernel_size=3, padding=2, dilation=2, bias=False)\n        self.tcn4 = nn.Conv1d(num_filters, num_filters, kernel_size=3, padding=2, dilation=2, bias=False)\n        self.bn3 = nn.BatchNorm1d(num_filters)\n        self.bn4 = nn.BatchNorm1d(num_filters)\n        \n        # Global pooling\n        self.global_pool = nn.AdaptiveAvgPool1d(1)\n        \n        # Classifier with better stability\n        self.classifier = nn.Sequential(\n            nn.Linear(num_filters, 32),\n            nn.BatchNorm1d(32),\n            nn.ReLU(),\n            nn.Dropout(0.3),\n            nn.Linear(32, 16),\n            nn.BatchNorm1d(16),\n            nn.ReLU(),\n            nn.Dropout(0.2),\n            nn.Linear(16, num_classes)\n        )\n        \n        # Initialize weights\n        self._initialize_weights()\n    \n    def _initialize_weights(self):\n        for m in self.modules():\n            if isinstance(m, nn.Conv1d):\n                nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')\n                if m.bias is not None:\n                    nn.init.constant_(m.bias, 0)\n            elif isinstance(m, nn.BatchNorm1d):\n                nn.init.constant_(m.weight, 1)\n                nn.init.constant_(m.bias, 0)\n            elif isinstance(m, nn.Linear):\n                nn.init.xavier_normal_(m.weight)\n                if m.bias is not None:\n                    nn.init.constant_(m.bias, 0)\n    \n    def forward(self, x):\n        # First residual block\n        identity = x\n        out = F.relu(self.bn1(self.tcn1(x)), inplace=True)\n        out = self.bn2(self.tcn2(out))\n        \n        identity = self.downsample1(identity)\n        out += identity\n        out = F.relu(out, inplace=True)\n        \n        # Second residual block\n        identity = out\n        out = F.relu(self.bn3(self.tcn3(out)), inplace=True)\n        out = self.bn4(self.tcn4(out))\n        out += identity\n        out = F.relu(out, inplace=True)\n        \n        # Global pooling\n        out = self.global_pool(out).squeeze(2)\n        \n        # Classifier\n        out = self.classifier(out)\n        return F.log_softmax(out, dim=1)\n\n# 5. TRAINING WITH LOSS CURVES\nprint(f\"\\n{'='*60}\")\nprint(\"⏱️  TRAINING TCN WITH LOSS CURVES\")\nprint(f\"{'='*60}\")\n\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nprint(f\"Using device: {device}\")\n\n# Create datasets with validation\nclass TCNDataset(torch.utils.data.Dataset):\n    def __init__(self, sequences, labels):\n        self.sequences = sequences\n        self.labels = labels\n    \n    def __len__(self):\n        return len(self.sequences)\n    \n    def __getitem__(self, idx):\n        # Add small noise for stability during training\n        seq = self.sequences[idx].copy()\n        label = self.labels[idx]\n        return torch.FloatTensor(seq), torch.tensor(label, dtype=torch.long)\n\ntrain_dataset = TCNDataset(\n    [sequence_list[i] for i in train_idx],\n    [labels_list[i] for i in train_idx]\n)\nval_dataset = TCNDataset(\n    [sequence_list[i] for i in val_idx],\n    [labels_list[i] for i in val_idx]\n)\n\ntrain_loader = torch.utils.data.DataLoader(\n    train_dataset, batch_size=64, shuffle=True, num_workers=2, pin_memory=True\n)\nval_loader = torch.utils.data.DataLoader(\n    val_dataset, batch_size=64, shuffle=False, num_workers=2, pin_memory=True\n)\n\n# Initialize model\nmodel = EEG_TCN(input_channels=4, num_classes=6, num_filters=64)\nmodel = model.to(device)\n\n# Weighted loss for class imbalance - FIXED: Handle class counts properly\nall_labels = torch.tensor(labels_list, dtype=torch.long)\nclass_counts = torch.bincount(all_labels)\nprint(f\"Class counts: {class_counts.tolist()}\")\n\n# Calculate weights with smoothing\nclass_weights = 1.0 / (class_counts.float() + 1e-8)\nclass_weights = class_weights / class_weights.sum()\nprint(f\"Class weights: {class_weights.tolist()}\")\ncriterion = nn.NLLLoss(weight=class_weights.to(device))\n\n# Use gradient clipping and weight decay\noptimizer = torch.optim.AdamW(model.parameters(), lr=0.001, weight_decay=1e-4)\nscheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(\n    optimizer, mode='max', factor=0.5, patience=3, verbose=True\n)\n\n# Track metrics\ntrain_losses = []\nval_losses = []\ntrain_accuracies = []\nval_accuracies = []\nlearning_rates = []\n\nnum_epochs = 15\nbest_val_acc = 0\npatience_counter = 0\nmax_patience = 5\n\nprint(f\"\\nTraining for {num_epochs} epochs...\")\n\nstart_time = time.time()\n\nfor epoch in range(num_epochs):\n    # Training phase\n    model.train()\n    train_loss = 0\n    train_correct = 0\n    train_total = 0\n    \n    train_pbar = tqdm(train_loader, desc=f\"Epoch {epoch+1}/{num_epochs} [Train]\", leave=False)\n    for sequences, labels in train_pbar:\n        sequences, labels = sequences.to(device), labels.to(device)\n        \n        optimizer.zero_grad()\n        outputs = model(sequences)\n        loss = criterion(outputs, labels)\n        \n        # Gradient clipping\n        loss.backward()\n        torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)\n        optimizer.step()\n        \n        train_loss += loss.item()\n        _, predicted = torch.max(outputs.data, 1)\n        train_total += labels.size(0)\n        train_correct += (predicted == labels).sum().item()\n        \n        # Update progress bar\n        train_pbar.set_postfix({'loss': loss.item(), 'acc': train_correct/train_total})\n    \n    avg_train_loss = train_loss / len(train_loader)\n    train_acc = train_correct / train_total\n    train_losses.append(avg_train_loss)\n    train_accuracies.append(train_acc)\n    \n    # Validation phase\n    model.eval()\n    val_loss = 0\n    val_correct = 0\n    val_total = 0\n    all_preds = []\n    all_labels_val = []\n    \n    val_pbar = tqdm(val_loader, desc=f\"Epoch {epoch+1}/{num_epochs} [Val]\", leave=False)\n    with torch.no_grad():\n        for sequences, labels in val_pbar:\n            sequences, labels = sequences.to(device), labels.to(device)\n            outputs = model(sequences)\n            loss = criterion(outputs, labels)\n            \n            val_loss += loss.item()\n            _, predicted = torch.max(outputs.data, 1)\n            val_total += labels.size(0)\n            val_correct += (predicted == labels).sum().item()\n            \n            all_preds.extend(predicted.cpu().numpy())\n            all_labels_val.extend(labels.cpu().numpy())\n            \n            # Update progress bar\n            val_pbar.set_postfix({'loss': loss.item(), 'acc': val_correct/val_total})\n    \n    avg_val_loss = val_loss / len(val_loader)\n    val_acc = val_correct / val_total\n    val_losses.append(avg_val_loss)\n    val_accuracies.append(val_acc)\n    learning_rates.append(optimizer.param_groups[0]['lr'])\n    \n    # Update scheduler\n    scheduler.step(val_acc)\n    \n    print(f\"\\n  Epoch {epoch+1}:\")\n    print(f\"    Train Loss: {avg_train_loss:.4f}, Train Acc: {train_acc:.4f}\")\n    print(f\"    Val Loss: {avg_val_loss:.4f}, Val Acc: {val_acc:.4f}\")\n    print(f\"    LR: {optimizer.param_groups[0]['lr']:.6f}\")\n    \n    # Check for NaN\n    if np.isnan(avg_train_loss) or np.isnan(avg_val_loss):\n        print(f\"  ⚠️  NaN detected! Stopping training.\")\n        break\n    \n    # Early stopping\n    if val_acc > best_val_acc:\n        best_val_acc = val_acc\n        patience_counter = 0\n        # Save best model\n        torch.save({\n            'epoch': epoch,\n            'model_state_dict': model.state_dict(),\n            'optimizer_state_dict': optimizer.state_dict(),\n            'val_acc': val_acc,\n        }, 'best_tcn_model.pth')\n        print(f\"    💾 Saved best model with val_acc: {val_acc:.4f}\")\n    else:\n        patience_counter += 1\n        if patience_counter >= max_patience:\n            print(f\"  ⏹️  Early stopping at epoch {epoch+1}\")\n            break\n\ntotal_time = time.time() - start_time\n\n# Load best model\nif os.path.exists('best_tcn_model.pth'):\n    checkpoint = torch.load('best_tcn_model.pth')\n    model.load_state_dict(checkpoint['model_state_dict'])\n    print(f\"Loaded best model from epoch {checkpoint['epoch']} with val_acc: {checkpoint['val_acc']:.4f}\")\n\n# Final evaluation\nmodel.eval()\nall_preds = []\nall_labels_final = []\n\nwith torch.no_grad():\n    for sequences, labels in val_loader:\n        sequences, labels = sequences.to(device), labels.to(device)\n        outputs = model(sequences)\n        _, predicted = torch.max(outputs.data, 1)\n        all_preds.extend(predicted.cpu().numpy())\n        all_labels_final.extend(labels.cpu().numpy())\n\nfinal_val_acc = accuracy_score(all_labels_final, all_preds)\n\nprint(f\"\\n✅ TCN Training Complete!\")\nprint(f\"   Best Validation Accuracy: {best_val_acc:.4f}\")\nprint(f\"   Final Validation Accuracy: {final_val_acc:.4f}\")\nprint(f\"   Total Training Time: {total_time:.1f}s\")\n\n# Save final model\ntorch.save({\n    'model_state_dict': model.state_dict(),\n    'config': {\n        'input_channels': 4,\n        'num_classes': 6,\n        'num_filters': 64\n    },\n    'val_acc': final_val_acc\n}, 'tcn_50k_model.pth')\nprint(\"   Model saved as 'tcn_50k_model.pth'\")\n\n# 6. CLASSIFICATION REPORT\nprint(f\"\\n{'='*60}\")\nprint(\"📊 CLASSIFICATION REPORT\")\nprint(f\"{'='*60}\")\n\nclass_names = ['Seizure', 'LPD', 'GPD', 'LRDA', 'GRDA', 'Other']\n\nprint(\"\\n📋 Detailed Classification Report:\")\nreport = classification_report(all_labels_final, all_preds, target_names=class_names, digits=4)\nprint(report)\n\n# Confusion Matrix\ncm = confusion_matrix(all_labels_final, all_preds)\n\nplt.figure(figsize=(12, 10))\nsns.heatmap(cm, annot=True, fmt='d', cmap='Oranges', \n            xticklabels=class_names, yticklabels=class_names)\nplt.title('TCN - Confusion Matrix (50K Samples)', fontsize=14, fontweight='bold')\nplt.xlabel('Predicted Label', fontsize=12)\nplt.ylabel('True Label', fontsize=12)\nplt.tight_layout()\nplt.savefig('tcn_confusion_matrix.png', dpi=300, bbox_inches='tight')\nplt.show()\n\n# 7. LOSS CURVES VISUALIZATION\nprint(f\"\\n{'='*60}\")\nprint(\"📈 LOSS AND ACCURACY CURVES\")\nprint(f\"{'='*60}\")\n\nfig, axes = plt.subplots(2, 2, figsize=(15, 12))\n\n# Loss Curves\nepochs_range = range(1, len(train_losses) + 1)\n\nax1 = axes[0, 0]\nax1.plot(epochs_range, train_losses, 'o-', linewidth=2, markersize=6, label='Training Loss', color='blue')\nax1.plot(epochs_range, val_losses, 's-', linewidth=2, markersize=6, label='Validation Loss', color='red')\nax1.set_xlabel('Epoch', fontsize=12)\nax1.set_ylabel('Loss', fontsize=12)\nax1.set_title('Loss Curves', fontsize=14, fontweight='bold')\nax1.grid(True, alpha=0.3)\nax1.legend()\n\n# Accuracy Curves\nax2 = axes[0, 1]\nax2.plot(epochs_range, train_accuracies, 'o-', linewidth=2, markersize=6, label='Training Accuracy', color='blue')\nax2.plot(epochs_range, val_accuracies, 's-', linewidth=2, markersize=6, label='Validation Accuracy', color='red')\nax2.set_xlabel('Epoch', fontsize=12)\nax2.set_ylabel('Accuracy', fontsize=12)\nax2.set_title('Accuracy Curves', fontsize=14, fontweight='bold')\nax2.grid(True, alpha=0.3)\nax2.legend()\n\n# Learning Rate Curve\nax3 = axes[1, 0]\nax3.plot(epochs_range, learning_rates[:len(epochs_range)], '^-', linewidth=2, markersize=8, label='Learning Rate', color='green')\nax3.set_xlabel('Epoch', fontsize=12)\nax3.set_ylabel('Learning Rate', fontsize=12)\nax3.set_title('Learning Rate Schedule', fontsize=14, fontweight='bold')\nax3.grid(True, alpha=0.3)\nax3.set_yscale('log')\nax3.legend()\n\n# Training Summary\nax4 = axes[1, 1]\nax4.axis('off')\nsummary_text = f\"\"\"\nTraining Summary:\n────────────────\nBest Val Acc: {best_val_acc:.4f}\nFinal Val Acc: {final_val_acc:.4f}\nTotal Epochs: {len(train_losses)}\nTotal Time: {total_time:.1f}s\n────────────────\nClass Distribution:\n{chr(10).join([f'{class_names[i]}: {class_counts[i].item()}' for i in range(6)])}\n\"\"\"\nax4.text(0.1, 0.5, summary_text, fontsize=11, verticalalignment='center',\n         bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.5))\n\nplt.tight_layout()\nplt.savefig('tcn_training_curves.png', dpi=300, bbox_inches='tight')\nplt.show()\n\nprint(f\"\\n{'='*80}\")\nprint(\"🎯 TCN TRAINING COMPLETE!\")\nprint(f\"{'='*80}\")","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}