{"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":[{"sourceType":"competition","sourceId":25954,"databundleVersionId":2091745}],"dockerImageVersionId":31401,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session\n\n# Use the kagglehub client library to attach Kaggle resources like competitions, datasets, and models to your session\n# Learn more about kagglehub: https://github.com/Kaggle/kagglehub/blob/main/README.md\n\nimport kagglehub\n# kagglehub.dataset_download('<owner>/<dataset-slug>')","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!pip install -q transformers datasets evaluate accelerate librosa","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-28T08:22:58.258055Z","iopub.execute_input":"2026-05-28T08:22:58.258371Z","iopub.status.idle":"2026-05-28T08:23:04.340439Z","shell.execute_reply.started":"2026-05-28T08:22:58.258347Z","shell.execute_reply":"2026-05-28T08:23:04.339502Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELL 1: LỌC DATA, LẬP BẢN ĐỒ NHÃN & CHIA 70-15-15\n# ============================================================\nimport pandas as pd\nimport numpy as np\nfrom sklearn.model_selection import train_test_split\nfrom datasets import Dataset\nfrom collections import Counter\n\n\ndf = pd.read_csv('/kaggle/input/competitions/birdclef-2021/train_metadata.csv')\n\nTOP_N = 30\ntop_30_species = df['primary_label'].value_counts().head(TOP_N).index.tolist()\ndf_filtered = df[df['primary_label'].isin(top_30_species)].copy()\n\n\nlabels = sorted(df_filtered['primary_label'].unique())\nlabel2id = {label: i for i, label in enumerate(labels)}\nid2label  = {i: label for i, label in enumerate(labels)}\nnum_labels = len(labels)\n\ndf_filtered['label_id'] = df_filtered['primary_label'].map(label2id)\n\ndef make_absolute_path(example):\n    example['audio_path'] = (\n        f\"/kaggle/input/competitions/birdclef-2021/train_short_audio\"\n        f\"/{example['primary_label']}/{example['filename']}\"\n    )\n    return example\n\n\ntrain_val_df, test_df = train_test_split(\n    df_filtered, test_size=0.15, random_state=42,\n    stratify=df_filtered['label_id']\n)\ntrain_df, val_df = train_test_split(\n    train_val_df, test_size=round(0.15 / 0.85, 6), random_state=42,\n    stratify=train_val_df['label_id']\n)\n\ntrain_dataset = Dataset.from_pandas(train_df.reset_index(drop=True)).map(make_absolute_path)\nval_dataset   = Dataset.from_pandas(val_df.reset_index(drop=True)).map(make_absolute_path)\ntest_dataset  = Dataset.from_pandas(test_df.reset_index(drop=True)).map(make_absolute_path)\n\n\nlabel_counts = Counter(train_df['label_id'])\ntotal_train  = len(train_df)\nclass_weights_np = np.array([\n    total_train / (num_labels * label_counts.get(i, 1))\n    for i in range(num_labels)\n], dtype=np.float32)\n\nprint(f\"TRAIN: {len(train_dataset)} | VAL: {len(val_dataset)} | TEST: {len(test_dataset)}\")\nprint(f\"Số loài: {num_labels} | Class weight min/max: {class_weights_np.min():.2f}/{class_weights_np.max():.2f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-28T08:23:35.212567Z","iopub.execute_input":"2026-05-28T08:23:35.213349Z","iopub.status.idle":"2026-05-28T08:23:41.103800Z","shell.execute_reply.started":"2026-05-28T08:23:35.213312Z","shell.execute_reply":"2026-05-28T08:23:41.102830Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\nimport librosa\nimport random\nimport torch\nfrom transformers import AutoFeatureExtractor\n\n\n\nmodel_checkpoint  = \"MIT/ast-finetuned-audioset-10-10-0.4593\"\nfeature_extractor = AutoFeatureExtractor.from_pretrained(model_checkpoint)\n\nTARGET_SR     = 16000\nTARGET_FRAMES = 1024   # AST gốc dùng 1024 patches → giữ chuẩn\nAUDIO_DURATION = 5.0\nAUDIO_SAMPLES  = int(TARGET_SR * AUDIO_DURATION)\n\n\ndef load_audio(path: str) -> np.ndarray:\n    \"\"\"Load và chuẩn hóa file audio về 5 giây / 16kHz.\"\"\"\n    try:\n        audio, _ = librosa.load(path, sr=TARGET_SR, duration=AUDIO_DURATION, mono=True)\n    except Exception:\n        audio = np.zeros(AUDIO_SAMPLES, dtype=np.float32)\n    # Padding nếu ngắn hơn 5 giây\n    if len(audio) < AUDIO_SAMPLES:\n        audio = np.pad(audio, (0, AUDIO_SAMPLES - len(audio)))\n    else:\n        audio = audio[:AUDIO_SAMPLES]\n    # Peak normalization\n    peak = np.max(np.abs(audio))\n    if peak > 1e-6:\n        audio = audio / peak\n    return audio.astype(np.float32)\n\n\n\ndef augment_audio(audio: np.ndarray, sr: int = TARGET_SR) -> np.ndarray:\n    aug_type = random.choices(\n        ['noise', 'gain', 'stretch', 'pitch', 'none'],\n        weights=[0.25, 0.20, 0.10, 0.10, 0.35]  # none chiếm 35%\n    )[0]\n\n    if aug_type == 'noise':\n        # Gaussian noise nhẹ mô phỏng tiếng gió/môi trường\n        noise_level = random.uniform(0.001, 0.004)\n        audio = audio + (np.random.randn(len(audio)) * noise_level).astype(np.float32)\n\n    elif aug_type == 'gain':\n        # Random volume ±20% — augmentation an toàn nhất với audio chim\n        gain = random.uniform(0.80, 1.20)\n        audio = audio * gain\n\n    elif aug_type == 'stretch':\n        #  Biên độ rất nhỏ (±3%)\n        rate = random.uniform(0.97, 1.03)\n        audio = librosa.effects.time_stretch(audio, rate=rate)\n\n    elif aug_type == 'pitch':\n        #  dịch ±0.5 semitone \n        n_steps = random.uniform(-0.5, 0.5)\n        audio = librosa.effects.pitch_shift(audio, sr=sr, n_steps=n_steps)\n\n    # Đảm bảo đúng độ dài sau stretch\n    if len(audio) < AUDIO_SAMPLES:\n        audio = np.pad(audio, (0, AUDIO_SAMPLES - len(audio)))\n    else:\n        audio = audio[:AUDIO_SAMPLES]\n\n    # Renormalize sau augmentation\n    peak = np.max(np.abs(audio))\n    if peak > 1e-6:\n        audio = audio / peak\n\n    return audio.astype(np.float32)\n\n\ndef train_transforms(examples):\n    audio_arrays = []\n    for path in examples['audio_path']:\n        audio = load_audio(path)\n        audio = augment_audio(audio)\n        audio_arrays.append(audio)\n\n    inputs = feature_extractor(\n        audio_arrays,\n        sampling_rate=TARGET_SR,\n        padding=\"max_length\",\n        max_length=TARGET_FRAMES,\n        truncation=True,\n        return_tensors=\"pt\"\n    )\n    inputs[\"labels\"] = torch.tensor(examples[\"label_id\"], dtype=torch.long)\n    return inputs\n\n\ndef val_test_transforms(examples):\n    audio_arrays = [load_audio(path) for path in examples['audio_path']]\n\n    inputs = feature_extractor(\n        audio_arrays,\n        sampling_rate=TARGET_SR,\n        padding=\"max_length\",\n        max_length=TARGET_FRAMES,\n        truncation=True,\n        return_tensors=\"pt\"\n    )\n    inputs[\"labels\"] = torch.tensor(examples[\"label_id\"], dtype=torch.long)\n    return inputs\n\n\ntrain_dataset.set_transform(train_transforms)\nval_dataset.set_transform(val_test_transforms)\ntest_dataset.set_transform(val_test_transforms)\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-28T08:24:04.042148Z","iopub.execute_input":"2026-05-28T08:24:04.042595Z","iopub.status.idle":"2026-05-28T08:24:21.309552Z","shell.execute_reply.started":"2026-05-28T08:24:04.042564Z","shell.execute_reply":"2026-05-28T08:24:21.308949Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\nimport torch\nimport torch.nn as nn\nimport numpy as np\nimport evaluate\nfrom transformers import ASTForAudioClassification, Trainer\n\nprint(\"CELL 3: BUILD MODEL VÀ WEIGHTED LOSS TRAINER...\")\n\nmodel = ASTForAudioClassification.from_pretrained(\n    model_checkpoint,\n    num_labels=num_labels,\n    label2id=label2id,\n    id2label=id2label,\n    ignore_mismatched_sizes=True\n)\n\n# ✅ FIX: Freeze toàn bộ encoder trong 3 epoch đầu → tránh catastrophic forgetting\n# Chỉ train lớp classifier head trước để \"làm quen\" với task mới\nfor name, param in model.named_parameters():\n    if 'classifier' not in name:\n        param.requires_grad = False\n\nfrozen_params = sum(p.numel() for p in model.parameters() if not p.requires_grad)\ntotal_params  = sum(p.numel() for p in model.parameters())\nprint(f\"    Frozen: {frozen_params:,} params |  Trainable: {total_params - frozen_params:,} params\")\n\n\n# ✅ FIX: Custom Trainer với Weighted Cross-Entropy Loss\n# Giải quyết mất cân bằng dữ liệu giữa các loài chim\nclass WeightedTrainer(Trainer):\n    def __init__(self, class_weights: np.ndarray, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        # Đưa weights lên GPU nếu có\n        device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n        self.class_weights_tensor = torch.tensor(class_weights, dtype=torch.float32).to(device)\n\n    def compute_loss(self, model, inputs, return_outputs=False, **kwargs):\n        labels = inputs.pop(\"labels\")\n        outputs = model(**inputs)\n        logits  = outputs.logits\n\n        loss_fn = nn.CrossEntropyLoss(weight=self.class_weights_tensor)\n        loss    = loss_fn(logits, labels)\n\n        return (loss, outputs) if return_outputs else loss\n\n\n# Metrics: Accuracy + F1-macro (F1-macro quan trọng hơn với imbalanced classes)\naccuracy_metric = evaluate.load(\"accuracy\")\nf1_metric       = evaluate.load(\"f1\")\n\ndef compute_metrics(eval_pred):\n    predictions = np.argmax(eval_pred.predictions, axis=1)\n    references  = eval_pred.label_ids\n    acc = accuracy_metric.compute(predictions=predictions, references=references)\n    f1  = f1_metric.compute(predictions=predictions, references=references, average=\"macro\")\n    return {\"accuracy\": acc[\"accuracy\"], \"f1_macro\": f1[\"f1\"]}\n\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-28T08:25:54.440541Z","iopub.execute_input":"2026-05-28T08:25:54.441613Z","iopub.status.idle":"2026-05-28T08:26:01.905892Z","shell.execute_reply.started":"2026-05-28T08:25:54.441577Z","shell.execute_reply":"2026-05-28T08:26:01.905202Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELL 4: TOÀN BỘ TRAINING — PHASE 1 (FREEZE) + PHASE 2 (FINETUNE)\n# ============================================================\nimport math\nfrom transformers import TrainingArguments, EarlyStoppingCallback\n\nprint(\"🚀 CELL 4: BẮT ĐẦU HUẤN LUYỆN 2 PHASE...\")\n\nBATCH_SIZE      = 8\nGRAD_ACCUM      = 2\nEFFECTIVE_BATCH = BATCH_SIZE * GRAD_ACCUM  # = 16\n\n# ════════════════════════════════════════════════════════════\n# PHASE 1: CHỈ TRAIN HEAD — BACKBONE BỊ FREEZE\n# Mục tiêu: classifier head học \"bắt sóng\" task chim mới\n# mà không làm hỏng kiến thức AudioSet đã có sẵn\n# ════════════════════════════════════════════════════════════\nprint(\"\\n PHASE 1: Freeze backbone, chỉ train classifier head...\")\n\nfor name, param in model.named_parameters():\n    if 'classifier' not in name:\n        param.requires_grad = False\n\nfrozen    = sum(p.numel() for p in model.parameters() if not p.requires_grad)\ntrainable = sum(p.numel() for p in model.parameters() if p.requires_grad)\nprint(f\"   Frozen:    {frozen:,} params\")\nprint(f\"   Trainable: {trainable:,} params\")\n\nPHASE1_EPOCHS      = 3\nsteps_p1           = math.ceil(len(train_dataset) / EFFECTIVE_BATCH) * PHASE1_EPOCHS\nwarmup_p1          = int(steps_p1 * 0.10)\n\nargs_phase1 = TrainingArguments(\n    output_dir                  = \"/kaggle/working/birdclef-phase1\",\n    eval_strategy               = \"epoch\",\n    save_strategy               = \"epoch\",\n    remove_unused_columns       = False,\n    learning_rate               = 1e-4,        # LR cao vì chỉ train head nhỏ\n    num_train_epochs            = PHASE1_EPOCHS,\n    per_device_train_batch_size = BATCH_SIZE,\n    per_device_eval_batch_size  = BATCH_SIZE,\n    gradient_accumulation_steps = GRAD_ACCUM,\n    dataloader_num_workers      = 2,\n    weight_decay                = 0.01,\n    lr_scheduler_type           = \"cosine\",\n    warmup_steps                = warmup_p1,\n    fp16                        = True,\n    load_best_model_at_end      = True,\n    metric_for_best_model       = \"f1_macro\",\n    greater_is_better           = True,\n    report_to                   = \"none\",\n    logging_steps               = 50,\n)\n\ntrainer_phase1 = WeightedTrainer(\n    class_weights    = class_weights_np,\n    model            = model,\n    args             = args_phase1,\n    train_dataset    = train_dataset,\n    eval_dataset     = val_dataset,\n    processing_class = feature_extractor,\n    compute_metrics  = compute_metrics,\n    callbacks        = [EarlyStoppingCallback(early_stopping_patience=3)],\n)\n\nprint(f\"   Steps: {steps_p1} | Warmup: {warmup_p1}\")\nprint(\"   🔥 Bắt đầu Phase 1...\\n\")\ntrainer_phase1.train()\n\n# In kết quả Phase 1\np1_metrics = trainer_phase1.evaluate()\nprint(f\"\\n   ✅ Phase 1 xong — Val Accuracy: {p1_metrics.get('eval_accuracy', 0):.4f} | F1-Macro: {p1_metrics.get('eval_f1_macro', 0):.4f}\")\n\n# ════════════════════════════════════════════════════════════\n# PHASE 2: UNFREEZE TOÀN BỘ → FINE-TUNE END-TO-END\n# Mục tiêu: tinh chỉnh toàn bộ model với LR nhỏ hơn\n# để không xóa kiến thức đã học từ Phase 1 + AudioSet\n# ════════════════════════════════════════════════════════════\nprint(\"\\n PHASE 2: Unfreeze toàn bộ, fine-tune end-to-end...\")\n\nfor param in model.parameters():\n    param.requires_grad = True\n\ntotal_params = sum(p.numel() for p in model.parameters())\nprint(f\"   ✅ Toàn bộ {total_params:,} params đã unfreeze\")\n\nPHASE2_EPOCHS      = 8\nsteps_p2           = math.ceil(len(train_dataset) / EFFECTIVE_BATCH) * PHASE2_EPOCHS\nwarmup_p2          = int(steps_p2 * 0.05)   # Warmup ngắn hơn — model đã ấm\n\nargs_phase2 = TrainingArguments(\n    output_dir                  = \"/kaggle/working/birdclef-phase2\",\n    eval_strategy               = \"epoch\",\n    save_strategy               = \"epoch\",\n    remove_unused_columns       = False,\n    learning_rate               = 2e-5,        # Nhỏ hơn 5x Phase 1 — tránh catastrophic forgetting\n    num_train_epochs            = PHASE2_EPOCHS,\n    per_device_train_batch_size = BATCH_SIZE,\n    per_device_eval_batch_size  = BATCH_SIZE,\n    gradient_accumulation_steps = GRAD_ACCUM,\n    dataloader_num_workers      = 2,\n    weight_decay                = 0.01,\n    lr_scheduler_type           = \"cosine\",\n    warmup_steps                = warmup_p2,\n    fp16                        = True,\n    load_best_model_at_end      = True,\n    metric_for_best_model       = \"f1_macro\",\n    greater_is_better           = True,\n    report_to                   = \"none\",\n    logging_steps               = 50,\n)\n\ntrainer_phase2 = WeightedTrainer(\n    class_weights    = class_weights_np,\n    model            = model,\n    args             = args_phase2,\n    train_dataset    = train_dataset,\n    eval_dataset     = val_dataset,\n    processing_class = feature_extractor,\n    compute_metrics  = compute_metrics,\n    callbacks        = [EarlyStoppingCallback(early_stopping_patience=3)],\n)\n\nprint(f\"   Steps tối đa: {steps_p2} | Warmup: {warmup_p2}\")\nprint(\"  Bắt đầu Phase 2...\\n\")\ntrainer_phase2.train()\n\n# ── Lưu model tốt nhất ───────────────────────────────────────\ntrainer_phase2.save_model(\"/kaggle/working/birdclef-best-monster-final\")\nfeature_extractor.save_pretrained(\"/kaggle/working/birdclef-best-monster-final\")\n\np2_metrics = trainer_phase2.evaluate()\nprint(f\"\\n   Phase 2 xong — Val Accuracy: {p2_metrics.get('eval_accuracy', 0):.4f} | F1-Macro: {p2_metrics.get('eval_f1_macro', 0):.4f}\")\nprint(\"\\n Training hoàn tất! Model đã lưu tại /kaggle/working/birdclef-best-monster-final\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-28T08:29:36.554378Z","iopub.execute_input":"2026-05-28T08:29:36.555261Z","iopub.status.idle":"2026-05-28T12:48:35.691493Z","shell.execute_reply.started":"2026-05-28T08:29:36.555228Z","shell.execute_reply":"2026-05-28T12:48:35.690829Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELL 5: INFERENCE + METRICS TOÀN DIỆN\n# ============================================================\nimport numpy as np\nimport torch\nfrom sklearn.metrics import (\n    classification_report, confusion_matrix,\n    accuracy_score, f1_score, top_k_accuracy_score\n)\nfrom transformers import ASTForAudioClassification, AutoFeatureExtractor\n\nprint(\"CELL 5: LOAD MODEL & INFERENCE TRÊN TẬP TEST...\")\n\n# ── Load từ disk cho chắc ────────────────────────────────────\nMODEL_PATH        = \"/kaggle/working/birdclef-best-monster-final\"\nfeature_extractor = AutoFeatureExtractor.from_pretrained(MODEL_PATH)\nmodel_eval        = ASTForAudioClassification.from_pretrained(MODEL_PATH)\nmodel_eval.eval()\ndevice     = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nmodel_eval = model_eval.to(device)\nprint(f\"   ✅ Đã load model từ {MODEL_PATH}\")\nprint(f\"   ✅ Device: {device}\")\n\n# ── Re-apply transform cho test_dataset (phòng kernel reset) ─\ntest_dataset.set_transform(val_test_transforms)\n\n# ── Inference ────────────────────────────────────────────────\nall_preds  = []\nall_labels = []\nall_probs  = []\nINFER_BATCH = 16\n\nprint(f\"\\n  Chạy inference {len(test_dataset)} samples...\")\n\nfor i in range(0, len(test_dataset), INFER_BATCH):\n    batch        = test_dataset[i : i + INFER_BATCH]\n    input_values = batch[\"input_values\"].to(device)\n    labels_batch = batch[\"labels\"]\n\n    with torch.no_grad():\n        probs = torch.softmax(\n            model_eval(input_values=input_values).logits, dim=-1\n        ).cpu().numpy()\n\n    all_preds.append(np.argmax(probs, axis=1))\n    all_labels.append(\n        labels_batch.numpy() if torch.is_tensor(labels_batch)\n        else np.array(labels_batch)\n    )\n    all_probs.append(probs)\n\n    if i % (INFER_BATCH * 15) == 0 and i > 0:\n        print(f\"      [{min(i + INFER_BATCH, len(test_dataset))}/{len(test_dataset)}]...\")\n\nall_preds  = np.concatenate(all_preds)\nall_labels = np.concatenate(all_labels)\nall_probs  = np.concatenate(all_probs)\n\n# ── Metrics tổng quan ────────────────────────────────────────\nacc      = accuracy_score(all_labels, all_preds)\nf1_macro = f1_score(all_labels, all_preds, average=\"macro\")\nf1_w     = f1_score(all_labels, all_preds, average=\"weighted\")\ntop3_acc = top_k_accuracy_score(all_labels, all_probs, k=3)\ntop5_acc = top_k_accuracy_score(all_labels, all_probs, k=5)\n\nprint(\"\\n\" + \"=\"*55)\nprint(\"  KẾT QUẢ CUỐI — TẬP TEST\")\nprint(\"=\"*55)\nprint(f\"  ✅ Accuracy       : {acc:.4f}  ({acc*100:.2f}%)\")\nprint(f\"  ✅ F1 Macro       : {f1_macro:.4f}\")\nprint(f\"  ✅ F1 Weighted    : {f1_w:.4f}\")\nprint(f\"  ✅ Top-3 Accuracy : {top3_acc:.4f}  ({top3_acc*100:.2f}%)\")\nprint(f\"  ✅ Top-5 Accuracy : {top5_acc:.4f}  ({top5_acc*100:.2f}%)\")\nprint(\"=\"*55)\n\n# ── F1 từng loài — bảng đẹp sort theo F1 giảm dần ───────────\nspecies_names  = [id2label[i] for i in range(num_labels)]\nf1_per_class   = f1_score(all_labels, all_preds, average=None)\nsupport        = np.bincount(all_labels, minlength=num_labels)\n\nimport pandas as pd\ndf_f1 = pd.DataFrame({\n    \"Species\"  : species_names,\n    \"F1 Score\" : f1_per_class,\n    \"Support\"  : support,\n}).sort_values(\"F1 Score\", ascending=False).reset_index(drop=True)\n\ndf_f1.index += 1  # bắt đầu từ 1\ndf_f1[\"F1 Score\"] = df_f1[\"F1 Score\"].map(lambda x: f\"{x:.4f}\")\n\nprint(\"\\n📋 F1 SCORE TỪNG LOÀI (sort theo F1 giảm dần):\\n\")\nprint(f\"{'#':<4} {'Species':<22} {'F1 Score':>10} {'Support':>9}\")\nprint(\"-\" * 48)\nfor idx, row in df_f1.iterrows():\n    bar   = \"█\" * int(float(row[\"F1 Score\"]) * 20)\n    flag  = \"🟢\" if float(row[\"F1 Score\"]) >= 0.85 else (\"🟡\" if float(row[\"F1 Score\"]) >= 0.65 else \"🔴\")\n    print(f\"{idx:<4} {row['Species']:<22} {row['F1 Score']:>10}  {int(row['Support']):>6}   {flag} {bar}\")\n\nprint(f\"\\n{'─'*48}\")\nprint(f\"     {'MEAN':<22} {f1_macro:.4f}  {int(support.sum()):>6}\")\n\n# ── Full classification report ───────────────────────────────\nprint(\"\\n📋 CLASSIFICATION REPORT CHI TIẾT:\\n\")\nprint(classification_report(all_labels, all_preds, target_names=species_names, digits=3))\n\n# Lưu biến cho Cell 6, 7\ncm      = confusion_matrix(all_labels, all_preds)\ncm_norm = cm.astype(float) / cm.sum(axis=1, keepdims=True)\n\nprint(\"\\n✅ Cell 5 xong — chạy Cell 6 để vẽ Confusion Matrix\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-28T12:49:14.156886Z","iopub.execute_input":"2026-05-28T12:49:14.157513Z","iopub.status.idle":"2026-05-28T12:53:02.002791Z","shell.execute_reply.started":"2026-05-28T12:49:14.157476Z","shell.execute_reply":"2026-05-28T12:53:02.002135Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELL 6: CONFUSION MATRIX (ĐẸP + DỄ ĐỌC)\n# ============================================================\nimport matplotlib.pyplot as plt\nimport matplotlib.colors as mcolors\nimport seaborn as sns\nimport numpy as np\n\nprint(\"CELL 6: VẼ CONFUSION MATRIX...\")\n\nspecies_names = [id2label[i] for i in range(num_labels)]\n\ncm      = confusion_matrix(all_labels, all_preds)\ncm_norm = cm.astype(float) / cm.sum(axis=1, keepdims=True)  # Normalize theo hàng (recall)\n\n# ── Vẽ ───────────────────────────────────────────────────────\nfig, axes = plt.subplots(1, 2, figsize=(28, 13))\nfig.patch.set_facecolor(\"#0f0f0f\")\n\nCMAP_NORM = sns.color_palette(\"rocket_r\", as_cmap=True)\nCMAP_RAW  = sns.color_palette(\"mako_r\",   as_cmap=True)\n\nfor ax, data, title, fmt, cmap in [\n    (axes[0], cm_norm, \"Normalized (Recall per class)\", \".2f\", CMAP_NORM),\n    (axes[1], cm,      \"Raw counts\",                    \"d\",   CMAP_RAW),\n]:\n    ax.set_facecolor(\"#0f0f0f\")\n    sns.heatmap(\n        data,\n        annot=True,\n        fmt=fmt,\n        cmap=cmap,\n        linewidths=0.4,\n        linecolor=\"#1e1e1e\",\n        xticklabels=species_names,\n        yticklabels=species_names,\n        ax=ax,\n        cbar_kws={\"shrink\": 0.8, \"pad\": 0.02},\n        annot_kws={\"size\": 7.5, \"color\": \"white\"},\n    )\n\n    # Tô màu diagonal (đúng dự đoán) nổi bật\n    for i in range(len(species_names)):\n        ax.add_patch(plt.Rectangle(\n            (i, i), 1, 1,\n            fill=False, edgecolor=\"#00e5ff\", lw=1.5\n        ))\n\n    ax.set_title(title, color=\"white\", fontsize=13, pad=14, fontweight=\"bold\")\n    ax.set_xlabel(\"Predicted Label\", color=\"#aaaaaa\", fontsize=10, labelpad=8)\n    ax.set_ylabel(\"True Label\",      color=\"#aaaaaa\", fontsize=10, labelpad=8)\n    ax.tick_params(colors=\"#cccccc\", labelsize=8)\n    plt.setp(ax.get_xticklabels(), rotation=45, ha=\"right\")\n    plt.setp(ax.get_yticklabels(), rotation=0)\n\n    # Colorbar text màu trắng\n    cbar = ax.collections[0].colorbar\n    cbar.ax.yaxis.set_tick_params(color=\"white\")\n    plt.setp(cbar.ax.yaxis.get_ticklabels(), color=\"white\", fontsize=8)\n\nfig.suptitle(\n    f\"BirdCLEF-2021 — Confusion Matrix  |  Top-30 Species\\n\"\n    f\"Accuracy: {acc*100:.2f}%   F1-Macro: {f1_macro:.4f}   Top-3: {top3_acc*100:.2f}%\",\n    color=\"white\", fontsize=15, fontweight=\"bold\", y=1.01\n)\n\nplt.tight_layout(pad=2.0)\nplt.savefig(\n    \"/kaggle/working/confusion_matrix.png\",\n    dpi=150, bbox_inches=\"tight\",\n    facecolor=fig.get_facecolor()\n)\nplt.show()\nprint(\"Confusion matrix đã lưu tại /kaggle/working/confusion_matrix.png\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-28T12:54:01.552840Z","iopub.execute_input":"2026-05-28T12:54:01.553483Z","iopub.status.idle":"2026-05-28T12:54:08.670337Z","shell.execute_reply.started":"2026-05-28T12:54:01.553452Z","shell.execute_reply":"2026-05-28T12:54:08.669674Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\n\n# Đường dẫn tới file train_metadata.csv trong Kaggle\ncsv_path = '/kaggle/input/competitions/birdclef-2021/train_metadata.csv'\n\n# Đọc file CSV\ndf = pd.read_csv(csv_path)\n\n# 1. In thông tin tổng quan về cấu trúc bảng (tên cột, số lượng non-null, kiểu dữ liệu)\nprint(\"📌 CẤU TRÚC BẢNG DỮ LIỆU:\")\ndf.info()\n\n# 2. In danh sách các cột dưới dạng mảng để dễ copy\nprint(\"\\n📌 DANH SÁCH CÁC CỘT:\")\nprint(df.columns.tolist())\n\n# 3. In 5 dòng đầu tiên (sử dụng display() để bảng hiển thị đẹp trong Notebook)\nprint(\"\\n📌 DỮ LIỆU MẪU:\")\ndisplay(df.head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-29T16:32:42.283820Z","iopub.execute_input":"2026-05-29T16:32:42.284199Z","iopub.status.idle":"2026-05-29T16:32:42.865480Z","shell.execute_reply.started":"2026-05-29T16:32:42.284153Z","shell.execute_reply":"2026-05-29T16:32:42.864620Z"}},"outputs":[],"execution_count":null}]}