{"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":112509,"databundleVersionId":14254895,"sourceType":"competition"}],"dockerImageVersionId":31154,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\nimport pandas as pd\nimport numpy as np\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\nimport timm  # Thư viện tuyệt vời cho các backbone\nimport cv2  # OpenCV để đọc ảnh\nfrom tqdm import tqdm # Thanh tiến trình\nimport matplotlib.pyplot as plt\n\n# --- LỚP CẤU HÌNH TRUNG TÂM ---\n# Quản lý tất cả các siêu tham số (hyperparameters) tại đây\nclass CFG:\n    # --- Đường dẫn (Paths) ---\n    # (Hãy điều chỉnh các đường dẫn này cho đúng với môi trường của bạn)\n    BASE_PATH = '/kaggle/input/csiro-biomass'\n    TRAIN_CSV = os.path.join(BASE_PATH, 'train.csv')\n    IMAGE_DIR = os.path.join(BASE_PATH, 'train')\n    \n    # --- Cài đặt Mô hình ---\n    MODEL_NAME = 'convnext_tiny' # Bạn có thể đổi sang 'resnet50'\n    PRETRAINED = True\n    IMG_SIZE = 768 # Kích thước ảnh đầu vào (từ 1000x1000 nén xuống)\n    \n    # --- Cài đặt Huấn luyện ---\n    DEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n    BATCH_SIZE = 2   # Bắt đầu với số nhỏ (vì 2 luồng ảnh 768x768 sẽ tốn VRAM)\n    EPOCHS = 30\n    LEARNING_RATE = 1e-4      # LR cho Giai đoạn 1 (huấn luyện heads)\n    \n    # --- Cài đặt Giai đoạn 2 (Fine-tuning) ---\n    FREEZE_EPOCHS = 15         # Số epochs chỉ huấn luyện 'heads'\n    FINETUNE_LR = 1e-5        # LR thấp hơn cho Giai đoạn 2 (toàn bộ mô hình)\n    \n    NUM_WORKERS = 2  # Số luồng tải dữ liệu\n    \n    # --- Mục tiêu & Loss ---\n    TARGET_COLS = ['Dry_Total_g', 'GDM_g', 'Dry_Green_g']\n    LOSS_WEIGHTS = {\n        'total_loss': 0.5,\n        'gdm_loss': 0.2,\n        'green_loss': 0.1\n    }\n    # 5 MỤC TIÊU ĐỂ TÍNH ĐIỂM (theo đúng thứ tự của cuộc thi)\n    ALL_TARGET_COLS = ['Dry_Green_g', 'Dry_Dead_g', 'Dry_Clover_g', 'GDM_g', 'Dry_Total_g']\n    \n    # Trọng số R2 (theo thứ tự của ALL_TARGET_COLS)\n    R2_WEIGHTS = [0.1, 0.1, 0.1, 0.2, 0.5]\n\n# --- In ra để xác nhận ---\nprint(f\"Sử dụng thiết bị: {CFG.DEVICE}\")\nprint(f\"Backbone mô hình: {CFG.MODEL_NAME}\")\nprint(f\"Kích thước ảnh huấn luyện: {CFG.IMG_SIZE}x{CFG.IMG_SIZE}\")","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-10-30T11:32:23.477943Z","iopub.execute_input":"2025-10-30T11:32:23.478160Z","iopub.status.idle":"2025-10-30T11:32:28.935116Z","shell.execute_reply.started":"2025-10-30T11:32:23.478127Z","shell.execute_reply":"2025-10-30T11:32:28.934442Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- Tải dữ liệu và Pivot ---\n\nprint(f\"Đang tải {CFG.TRAIN_CSV}...\")\ntry:\n    # 1. Đọc file CSV gốc (dạng long)\n    df_long = pd.read_csv(CFG.TRAIN_CSV)\n    print(f\"Bảng 'long' gốc có {len(df_long)} hàng.\")\n    \n    # 2. Thực hiện Pivot\n    # Chúng ta dùng 'image_path' làm chỉ mục (index) duy nhất\n    # Xoay cột 'target_name' thành các cột mới\n    # Lấy giá trị từ cột 'target'\n    df_wide = df_long.pivot(\n        index='image_path',\n        columns='target_name',\n        values='target'\n    )\n    \n    # 3. Dọn dẹp DataFrame\n    # Sau khi pivot, 'image_path' trở thành index.\n    # 'reset_index()' sẽ biến nó trở lại thành một cột bình thường.\n    df_wide = df_wide.reset_index()\n    df_wide.columns.name = None # Xóa tên 'target_name' khỏi trục cột\n    \n    print(f\"Đã pivot! Bảng 'wide' mới có {len(df_wide)} hàng (mỗi hàng 1 ảnh).\")\n    \n    # 4. Hiển thị 5 hàng đầu tiên của bảng 'wide' mới\n    print(\"\\n--- 5 hàng đầu của df_wide ---\")\n    print(df_wide.head())\n    \n    # 5. (Tùy chọn) Kiểm tra xem các cột mục tiêu của chúng ta có ở đó không\n    print(\"\\n--- Các cột có trong df_wide ---\")\n    print(df_wide.columns.tolist())\n    \nexcept FileNotFoundError:\n    print(f\"LỖI: Không tìm thấy file {CFG.TRAIN_CSV}\")\n    print(\"Vui lòng kiểm tra lại CFG.TRAIN_CSV\")\n    # Giả lập df_wide nếu không tìm thấy file để các bước sau không bị lỗi\n    df_wide = pd.DataFrame(columns=['image_path'] + CFG.TARGET_COLS)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-30T11:32:28.938287Z","iopub.execute_input":"2025-10-30T11:32:28.938596Z","iopub.status.idle":"2025-10-30T11:32:28.959306Z","shell.execute_reply.started":"2025-10-30T11:32:28.938567Z","shell.execute_reply":"2025-10-30T11:32:28.958540Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from albumentations import (\n    Compose, \n    Resize, \n    Normalize,\n    HorizontalFlip, \n    VerticalFlip,\n    RandomRotate90,  # Chỉ xoay 90, 180, 270\n    ColorJitter\n)\n\n# --- Định nghĩa Augmentations ---\n\n# Chúng ta sẽ dùng pipeline NÀY cho CẢ hai ảnh (trái và phải) MỘT CÁCH ĐỘC LẬP\ndef get_train_transforms():\n    \"\"\"\n    Tăng cường dữ liệu cho huấn luyện.\n    Sẽ được áp dụng độc lập cho img_left và img_right.\n    \"\"\"\n    return Compose([\n        # 1. Tăng cường hình học (Geometric)\n        HorizontalFlip(p=0.5),\n        VerticalFlip(p=0.5),\n        RandomRotate90(p=0.5), # Tự động xoay 90, 180, hoặc 270\n\n        # 2. Tăng cường màu sắc (Photometric)\n        ColorJitter(\n            brightness=0.2, \n            contrast=0.2, \n            saturation=0.2, \n            hue=0.1, \n            p=0.75\n        ),\n\n        # 3. Chuẩn hóa (Normalize)\n        # Giá trị mean/std tiêu chuẩn của ImageNet\n        Normalize(\n            mean=[0.485, 0.456, 0.406],\n            std=[0.229, 0.224, 0.225]\n        ),\n        \n        # 4. Nén ảnh và Chuyển sang Tensor\n        # (Nén sau khi augment để đảm bảo chất lượng)\n        Resize(CFG.IMG_SIZE, CFG.IMG_SIZE),\n        ToTensorV2()\n    ])\n\ndef get_valid_transforms():\n    \"\"\"\n    Chỉ chuẩn hóa và nén ảnh cho tập validation (không augment).\n    \"\"\"\n    return Compose([\n        Normalize(\n            mean=[0.485, 0.456, 0.406],\n            std=[0.229, 0.224, 0.225]\n        ),\n        Resize(CFG.IMG_SIZE, CFG.IMG_SIZE),\n        ToTensorV2()\n    ])\n\n# --- In ra để xác nhận ---\nprint(\"Đã định nghĩa các hàm Augmentation.\")\nprint(f\"Ảnh huấn luyện sẽ được augment và nén xuống: {CFG.IMG_SIZE}x{CFG.IMG_SIZE}\")\nprint(f\"Ảnh validation sẽ chỉ được nén xuống: {CFG.IMG_SIZE}x{CFG.IMG_SIZE}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-30T11:32:28.961563Z","iopub.execute_input":"2025-10-30T11:32:28.961972Z","iopub.status.idle":"2025-10-30T11:32:28.968888Z","shell.execute_reply.started":"2025-10-30T11:32:28.961954Z","shell.execute_reply":"2025-10-30T11:32:28.968207Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# (Đây là phiên bản MỚI của lớp BiomassDataset)\n\nclass BiomassDataset(Dataset):\n    \"\"\"\n    Dataset tùy chỉnh cho chiến lược \"Hai luồng\".\n    \n    Sẽ trả về:\n    (img_left, img_right, train_targets (3), all_targets (5))\n    \"\"\"\n    def __init__(self, df, transforms_fn, image_dir, train_target_cols, all_target_cols):\n        self.df = df\n        self.transforms_fn = transforms_fn\n        self.image_dir = image_dir\n        \n        # Lưu trữ sẵn để truy cập nhanh\n        self.image_paths = df['image_path'].values\n        # 3 mục tiêu cho việc tính loss\n        self.train_targets = df[train_target_cols].values\n        # 5 mục tiêu cho việc tính điểm R2\n        self.all_targets = df[all_target_cols].values\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        # 1. Lấy thông tin\n        img_path_suffix = self.image_paths[idx]\n        train_target_vals = self.train_targets[idx]\n        all_target_vals = self.all_targets[idx]\n        \n        # 2. Đọc ảnh gốc (2000x1000)\n        filename = os.path.basename(img_path_suffix)\n        full_path = os.path.join(self.image_dir, filename)\n        image = cv2.imread(full_path)\n        if image is None:\n            raise FileNotFoundError(f\"Không thể đọc ảnh: {full_path}\")\n        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n        \n        # 3. Cắt (Crop) thành 2 ảnh (Trái và Phải)\n        height, width, _ = image.shape\n        mid_point = width // 2\n        img_left = image[:, :mid_point]\n        img_right = image[:, mid_point:]\n        \n        # 4. Áp dụng Augmentations ĐỘC LẬP\n        transforms = self.transforms_fn()\n        img_left_tensor = transforms(image=img_left)['image']\n        \n        # Lấy pipeline MỚI cho ảnh phải (để augment độc lập)\n        transforms_2 = self.transforms_fn()\n        img_right_tensor = transforms_2(image=img_right)['image']\n        \n        # 5. Lấy mục tiêu (Targets)\n        train_target_tensor = torch.tensor(train_target_vals, dtype=torch.float32)\n        all_targets_tensor = torch.tensor(all_target_vals, dtype=torch.float32)\n        \n        # 6. Trả về\n        return img_left_tensor, img_right_tensor, train_target_tensor, all_targets_tensor","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-30T11:32:28.969682Z","iopub.execute_input":"2025-10-30T11:32:28.969924Z","iopub.status.idle":"2025-10-30T11:32:28.983342Z","shell.execute_reply.started":"2025-10-30T11:32:28.969906Z","shell.execute_reply":"2025-10-30T11:32:28.982521Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- Thử nghiệm Dataset và DataLoader ---\n\nprint(\"\\nĐang kiểm tra Dataset & DataLoader...\")\n\ntry:\n    # 1. Tạo một dataset (dùng augmentation của tập train)\n    # Lấy 10 hàng đầu tiên của df_wide để thử nghiệm\n    test_df_subset = df_wide.head(10) \n    \n    train_dataset = BiomassDataset(\n        df=test_df_subset,\n        transforms_fn=get_train_transforms, # Truyền tên HÀM\n        image_dir=CFG.IMAGE_DIR,\n        target_cols=CFG.TARGET_COLS\n    )\n    \n    # 2. Tạo DataLoader\n    train_loader = DataLoader(\n        train_dataset,\n        batch_size=CFG.BATCH_SIZE,\n        shuffle=True,\n        num_workers=CFG.NUM_WORKERS\n    )\n    \n    # 3. Lấy một batch dữ liệu\n    print(f\"Đang tải 1 batch (batch_size={CFG.BATCH_SIZE})...\")\n    img_left_batch, img_right_batch, targets_batch = next(iter(train_loader))\n    \n    # 4. In ra kích thước (shape)\n    print(\"\\n--- Kích thước (Shape) của Batch đầu ra ---\")\n    print(f\"  Ảnh trái (Left): {img_left_batch.shape}\")\n    print(f\"  Ảnh phải (Right): {img_right_batch.shape}\")\n    print(f\"  Mục tiêu (Targets): {targets_batch.shape}\")\n    \n    print(\"\\n--- Kích thước mong đợi ---\")\n    print(f\"  Ảnh (Kỳ vọng):   [Batch, Channels, Height, Width] -> [{CFG.BATCH_SIZE}, 3, {CFG.IMG_SIZE}, {CFG.IMG_SIZE}]\")\n    print(f\"  Mục tiêu (Kỳ vọng): [Batch, Num_Targets] -> [{CFG.BATCH_SIZE}, {len(CFG.TARGET_COLS)}]\")\n    \n    # (Tùy chọn) Hiển thị một ảnh\n    plt.imshow(img_left_batch[0].permute(1, 2, 0) * 0.229 + 0.485) # Đã chuẩn hóa\n    plt.title(\"Một ảnh mẫu từ Batch (Trái)\")\n    plt.show()\n\nexcept Exception as e:\n    print(f\"\\nLỖI khi kiểm tra DataLoader: {e}\")\n    print(\"Hãy kiểm tra lại đường dẫn trong CFG và cấu trúc file ảnh.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-30T11:32:28.984203Z","iopub.execute_input":"2025-10-30T11:32:28.984447Z","iopub.status.idle":"2025-10-30T11:32:28.997499Z","shell.execute_reply.started":"2025-10-30T11:32:28.984423Z","shell.execute_reply":"2025-10-30T11:32:28.996803Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class BiomassModel(nn.Module):\n    \"\"\"\n    Mô hình Hai luồng, Ba đầu ra chuyên biệt.\n    \n    1. Một backbone (ví dụ: ConvNeXt) dùng chung.\n    2. Backbone được áp dụng cho img_left và img_right.\n    3. Hai vector đặc trưng được ghép (concatenate) lại.\n    4. Vector hợp nhất được đưa vào 3 \"đầu\" MLP riêng biệt.\n    \"\"\"\n    def __init__(self, model_name, pretrained, n_targets=3):\n        super(BiomassModel, self).__init__()\n        \n        # 1. Tải Backbone (Thân) dùng chung\n        # num_classes=0: Loại bỏ lớp phân loại gốc.\n        # global_pool='avg': Thêm lớp Global Average Pooling\n        #                      để lấy ra một vector đặc trưng 1D.\n        self.backbone = timm.create_model(\n            model_name,\n            pretrained=pretrained,\n            num_classes=0,\n            global_pool='avg'\n        )\n        \n        # 2. Lấy số chiều đặc trưng (feature dimension) từ backbone\n        # Ví dụ: convnext_tiny là 768, resnet50 là 2048\n        self.n_features = self.backbone.num_features\n        \n        # 3. Tính số chiều sau khi hợp nhất (fusion)\n        # (features_left + features_right)\n        self.n_combined_features = self.n_features * 2\n        \n        # 4. Định nghĩa Ba (3) \"Đầu\" MLP riêng biệt\n        # Mỗi \"đầu\" là một mạng nơ-ron nhỏ chuyên biệt\n        \n        # --- Đầu cho Dry_Total_g ---\n        self.head_total = nn.Sequential(\n            nn.Linear(self.n_combined_features, self.n_combined_features // 2),\n            nn.ReLU(),\n            nn.Dropout(0.3),\n            nn.Linear(self.n_combined_features // 2, 1) # Đầu ra 1 giá trị\n        )\n        \n        # --- Đầu cho GDM_g ---\n        self.head_gdm = nn.Sequential(\n            nn.Linear(self.n_combined_features, self.n_combined_features // 2),\n            nn.ReLU(),\n            nn.Dropout(0.3),\n            nn.Linear(self.n_combined_features // 2, 1) # Đầu ra 1 giá trị\n        )\n        \n        # --- Đầu cho Dry_Green_g ---\n        self.head_green = nn.Sequential(\n            nn.Linear(self.n_combined_features, self.n_combined_features // 2),\n            nn.ReLU(),\n            nn.Dropout(0.3),\n            nn.Linear(self.n_combined_features // 2, 1) # Đầu ra 1 giá trị\n        )\n\n    def forward(self, img_left, img_right):\n        # 1. Chạy Luồng 1 (Trái)\n        features_left = self.backbone(img_left) # Shape: [batch, n_features]\n        \n        # 2. Chạy Luồng 2 (Phải)\n        features_right = self.backbone(img_right) # Shape: [batch, n_features]\n        \n        # 3. Hợp nhất (Fusion)\n        # Ghép 2 vector đặc trưng lại\n        combined = torch.cat([features_left, features_right], dim=1) # Shape: [batch, n_combined_features]\n        \n        # 4. Cho qua các \"Đầu\" riêng biệt\n        out_total = self.head_total(combined)\n        out_gdm = self.head_gdm(combined)\n        out_green = self.head_green(combined)\n        \n        # 5. Trả về 3 giá trị dự đoán\n        # Chúng ta trả về 3 tensor riêng biệt để hàm loss dễ xử lý\n        return out_total, out_gdm, out_green","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-30T11:32:28.998128Z","iopub.execute_input":"2025-10-30T11:32:28.998360Z","iopub.status.idle":"2025-10-30T11:32:29.009267Z","shell.execute_reply.started":"2025-10-30T11:32:28.998336Z","shell.execute_reply":"2025-10-30T11:32:29.008545Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- Thử nghiệm Kiến trúc Mô hình ---\nprint(\"\\nĐang kiểm tra kiến trúc mô hình...\")\n\ntry:\n    # 1. Tạo một mô hình\n    model = BiomassModel(\n        model_name=CFG.MODEL_NAME,\n        pretrained=False # Không cần tải trọng số, chỉ kiểm tra kiến trúc\n    ).to(CFG.DEVICE)\n    \n    # 2. Tạo dữ liệu giả (dummy data)\n    # (Batch size, channels, height, width)\n    dummy_left = torch.randn(CFG.BATCH_SIZE, 3, CFG.IMG_SIZE, CFG.IMG_SIZE).to(CFG.DEVICE)\n    dummy_right = torch.randn(CFG.BATCH_SIZE, 3, CFG.IMG_SIZE, CFG.IMG_SIZE).to(CFG.DEVICE)\n    \n    print(f\"Đang đưa 2 batch {dummy_left.shape} vào mô hình...\")\n    \n    # 3. Chạy forward pass\n    out_total, out_gdm, out_green = model(dummy_left, dummy_right)\n    \n    # 4. In ra kích thước đầu ra\n    print(\"\\n--- Kích thước (Shape) của Đầu ra Mô hình ---\")\n    print(f\"  Đầu ra Total: {out_total.shape}\")\n    print(f\"  Đầu ra GDM:   {out_gdm.shape}\")\n    print(f\"  Đầu ra Green: {out_green.shape}\")\n    \n    print(\"\\n--- Kích thước mong đợi (mỗi đầu ra) ---\")\n    print(f\"  Kỳ vọng: [Batch, 1] -> [{CFG.BATCH_SIZE}, 1]\")\n\nexcept Exception as e:\n    print(f\"\\nLỖI khi kiểm tra mô hình: {e}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-30T11:32:29.009840Z","iopub.execute_input":"2025-10-30T11:32:29.010022Z","iopub.status.idle":"2025-10-30T11:32:30.352297Z","shell.execute_reply.started":"2025-10-30T11:32:29.010007Z","shell.execute_reply":"2025-10-30T11:32:30.351559Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class WeightedBiomassLoss(nn.Module):\n    \"\"\"\n    Hàm loss tổng hợp có trọng số.\n    \n    Tính toán 3 hàm MSELoss riêng biệt và cộng chúng lại\n    với các trọng số từ CFG.\n    \"\"\"\n    def __init__(self, loss_weights_dict):\n        super(WeightedBiomassLoss, self).__init__()\n        # Chúng ta có thể dùng một (1) hàm MSELoss\n        # và gọi nó 3 lần\n        self.criterion = nn.SmoothL1Loss()\n        \n        # Lưu trữ các trọng số\n        self.weights = loss_weights_dict\n\n    def forward(self, predictions, targets):\n        \"\"\"\n        predictions: Một tuple (out_total, out_gdm, out_green) từ mô hình\n        targets: Một tensor [batch_size, 3] từ dataloader\n        \"\"\"\n        \n        # 1. Tách các dự đoán\n        pred_total, pred_gdm, pred_green = predictions\n        \n        # 2. Tách các mục tiêu (ground truth)\n        # targets shape là [batch, 3]\n        # Cột 0: Dry_Total_g\n        # Cột 1: GDM_g\n        # Cột 2: Dry_Green_g\n        true_total = targets[:, 0].unsqueeze(-1) # Shape [batch, 1]\n        true_gdm   = targets[:, 1].unsqueeze(-1) # Shape [batch, 1]\n        true_green = targets[:, 2].unsqueeze(-1) # Shape [batch, 1]\n        \n        # 3. Tính 3 hàm loss riêng biệt\n        loss_total = self.criterion(pred_total, true_total)\n        loss_gdm   = self.criterion(pred_gdm, true_gdm)\n        loss_green = self.criterion(pred_green, true_green)\n        \n        # 4. Áp dụng trọng số và tính tổng\n        total_loss = (\n            self.weights['total_loss'] * loss_total +\n            self.weights['gdm_loss'] * loss_gdm +\n            self.weights['green_loss'] * loss_green\n        )\n        \n        return total_loss","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-30T11:32:30.352998Z","iopub.execute_input":"2025-10-30T11:32:30.353246Z","iopub.status.idle":"2025-10-30T11:32:30.360285Z","shell.execute_reply.started":"2025-10-30T11:32:30.353226Z","shell.execute_reply":"2025-10-30T11:32:30.359523Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- Khởi tạo và Kiểm tra Hàm Loss ---\n\nprint(\"\\nĐang khởi tạo hàm loss...\")\n\n# 1. Khởi tạo\ncriterion = WeightedBiomassLoss(loss_weights_dict=CFG.LOSS_WEIGHTS)\ncriterion.to(CFG.DEVICE)\n\nprint(f\"Hàm loss đã được tạo với các trọng số: {CFG.LOSS_WEIGHTS}\")\n\n# 2. Kiểm tra\nprint(\"\\nĐang kiểm tra hàm loss...\")\ntry:\n    # Tạo dữ liệu giả\n    # (Giống như output của mô hình)\n    dummy_preds = (\n        torch.randn(CFG.BATCH_SIZE, 1).to(CFG.DEVICE),\n        torch.randn(CFG.BATCH_SIZE, 1).to(CFG.DEVICE),\n        torch.randn(CFG.BATCH_SIZE, 1).to(CFG.DEVICE)\n    )\n    \n    # (Giống như output của dataloader)\n    dummy_targets = torch.randn(CFG.BATCH_SIZE, len(CFG.TARGET_COLS)).to(CFG.DEVICE)\n    \n    # Tính loss\n    loss_value = criterion(dummy_preds, dummy_targets)\n    \n    print(f\"  Dữ liệu giả: {CFG.BATCH_SIZE} mẫu\")\n    print(f\"  Giá trị loss tính được: {loss_value.item():.4f}\")\n    print(\"✓ Hàm loss hoạt động chính xác.\")\n    \nexcept Exception as e:\n    print(f\"\\nLỖI khi kiểm tra hàm loss: {e}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-30T11:32:30.361224Z","iopub.execute_input":"2025-10-30T11:32:30.361496Z","iopub.status.idle":"2025-10-30T11:32:30.391997Z","shell.execute_reply.started":"2025-10-30T11:32:30.361473Z","shell.execute_reply":"2025-10-30T11:32:30.391485Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.model_selection import StratifiedKFold\nimport numpy as np\n\n# --- Thêm vào lớp CFG ---\nCFG.N_FOLDS = 5  # Chúng ta sẽ dùng 5-Fold Cross-Validation\nCFG.RANDOM_STATE = 42 # Để đảm bảo kết quả có thể tái lập\n\nprint(f\"\\nĐang chuẩn bị {CFG.N_FOLDS}-Fold Cross-Validation...\")\n\n# 1. Tạo một cột 'fold' mới trong df_wide, mặc định là -1\ndf_wide['fold'] = -1\n\n# 2. Tạo Bins (Nhóm) cho mục tiêu quan trọng nhất (Dry_Total_g)\n# 'pd.cut' sẽ chia các giá trị liên tục thành 10 nhóm\n# (q=10: quantile-based, đảm bảo mỗi nhóm có số lượng mẫu tương đương)\nnum_bins = int(np.floor(1 + np.log2(len(df_wide)))) # Công thức Sturges (hoặc dùng 10)\nif len(df_wide) > 100: # Dùng 10 bins nếu đủ dữ liệu\n    num_bins = 10\n    \nprint(f\"Sử dụng {num_bins} bins để phân tầng (stratify) trên 'Dry_Total_g'\")\n\ndf_wide['total_bin'] = pd.cut(\n    df_wide['Dry_Total_g'], \n    bins=num_bins, \n    labels=False # Chỉ cần nhãn số\n)\n\n# 3. Khởi tạo StratifiedKFold\n# Chúng ta chia dựa trên các 'bins' vừa tạo\nskf = StratifiedKFold(\n    n_splits=CFG.N_FOLDS, \n    shuffle=True, \n    random_state=CFG.RANDOM_STATE\n)\n\n# 4. Gán số 'fold' cho mỗi hàng\n# 'skf.split' trả về (train_indices, valid_indices)\n# Chúng ta chỉ cần valid_indices để gán số fold\nfor fold_num, (train_idx, valid_idx) in enumerate(skf.split(df_wide, df_wide['total_bin'])):\n    # Gán số fold (0, 1, 2, 3, 4) cho các hàng trong tập validation\n    df_wide.loc[valid_idx, 'fold'] = fold_num\n\n# 5. Kiểm tra kết quả\nprint(\"\\n--- Phân phối số lượng mẫu trong mỗi Fold ---\")\nprint(df_wide['fold'].value_counts().sort_index())\n\nprint(\"\\n--- df_wide sau khi thêm cột 'fold' (5 hàng đầu) ---\")\nprint(df_wide.head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-30T11:32:30.392722Z","iopub.execute_input":"2025-10-30T11:32:30.392956Z","iopub.status.idle":"2025-10-30T11:32:30.736136Z","shell.execute_reply.started":"2025-10-30T11:32:30.392935Z","shell.execute_reply":"2025-10-30T11:32:30.735378Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.metrics import r2_score\n\ndef calculate_competition_score(all_preds_3, all_targets_5):\n    \"\"\"\n    Tính điểm R^2 có trọng số của cuộc thi.\n    \n    Args:\n        all_preds_3 (dict): Dict chứa 3 mảng numpy dự đoán từ model.\n        all_targets_5 (np.array): Mảng [N, 5] chứa 5 giá trị ground truth.\n    \n    Returns:\n        float: Điểm R^2 cuối cùng.\n    \"\"\"\n    \n    # 1. Tái cấu trúc 5 dự đoán từ 3 đầu ra\n    pred_total = all_preds_3['total']\n    pred_gdm = all_preds_3['gdm']\n    pred_green = all_preds_3['green']\n    \n    # Đảm bảo không có giá trị âm\n    pred_clover = np.maximum(0, pred_gdm - pred_green)\n    pred_dead = np.maximum(0, pred_total - pred_gdm)\n    \n    # Ghép 5 dự đoán (phải đúng thứ tự của CFG.ALL_TARGET_COLS)\n    # ['Dry_Green_g', 'Dry_Dead_g', 'Dry_Clover_g', 'GDM_g', 'Dry_Total_g']\n    y_preds = np.stack([\n        pred_green,\n        pred_dead,\n        pred_clover,\n        pred_gdm,\n        pred_total\n    ], axis=1) # Shape: [N, 5]\n    \n    y_true = all_targets_5 # Shape: [N, 5]\n\n    # 2. Tính R^2 cho từng mục tiêu\n    # (r2_score của sklearn có thể xử lý multi-output)\n    r2_scores = r2_score(y_true, y_preds, multioutput='raw_values')\n    \n    # 3. Áp dụng trọng số\n    weighted_r2_total = 0.0\n    for i, weight in enumerate(CFG.R2_WEIGHTS):\n        weighted_r2_total += r2_scores[i] * weight\n        \n    return weighted_r2_total","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-30T11:32:30.736863Z","iopub.execute_input":"2025-10-30T11:32:30.737334Z","iopub.status.idle":"2025-10-30T11:32:30.742921Z","shell.execute_reply.started":"2025-10-30T11:32:30.737315Z","shell.execute_reply":"2025-10-30T11:32:30.742209Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch.optim as optim\nfrom tqdm import tqdm\nimport time\nimport gc\n\ndef train_one_epoch(model, loader, criterion, optimizer, device):\n    \"\"\"\n    Chạy 1 epoch huấn luyện.\n    (Đã cập nhật để xử lý 4 đầu ra từ Dataset)\n    \"\"\"\n    model.train()  # Chuyển mô hình sang chế độ .train()\n    epoch_loss = 0.0\n    \n    pbar = tqdm(loader, desc=\"Training\", leave=False)\n    \n    # *** THAY ĐỔI CHÍNH Ở ĐÂY ***\n    # Dataloader giờ trả về 4 giá trị.\n    # Chúng ta dùng dấu gạch dưới (_) để bỏ qua giá trị thứ 4 (all_targets).\n    for (img_left, img_right, train_targets, _all_targets_ignored) in pbar:\n        \n        # 1. Chuyển 3 dữ liệu cần thiết sang device (GPU/CPU)\n        img_left = img_left.to(device)\n        img_right = img_right.to(device)\n        targets = train_targets.to(device) # Chỉ 3 mục tiêu cho loss\n        \n        # 2. Xóa gradients cũ\n        optimizer.zero_grad()\n        \n        # 3. Forward pass (Lan truyền tiến)\n        predictions = model(img_left, img_right)\n        \n        # 4. Tính toán Loss (với 3 mục tiêu)\n        loss = criterion(predictions, targets)\n        \n        # 5. Backward pass (Lan truyền ngược)\n        loss.backward()\n        \n        # 6. Cập nhật trọng số\n        optimizer.step()\n        \n        epoch_loss += loss.item()\n        pbar.set_postfix(loss=f'{loss.item():.4f}') # Hiển thị loss hiện tại\n        \n    # Trả về loss trung bình của epoch\n    return epoch_loss / len(loader)\n\ndef validate_one_epoch(model, loader, criterion, device):\n    \"\"\"\n    Chạy 1 epoch đánh giá VÀ tính điểm R^2.\n    \"\"\"\n    model.eval()\n    epoch_loss = 0.0\n    \n    # List để thu thập tất cả dự đoán và nhãn\n    all_preds_3 = {'total': [], 'gdm': [], 'green': []}\n    all_targets_list = []\n\n    with torch.no_grad():\n        pbar = tqdm(loader, desc=\"Validating\", leave=False)\n        # Chú ý: Dataloader giờ trả về 4 giá trị\n        for (img_left, img_right, train_targets, all_targets) in pbar:\n            \n            img_left = img_left.to(device)\n            img_right = img_right.to(device)\n            train_targets = train_targets.to(device) # 3 mục tiêu cho loss\n            # all_targets vẫn ở CPU, chúng ta sẽ xử lý sau\n            \n            # 1. Forward pass\n            pred_total, pred_gdm, pred_green = model(img_left, img_right)\n            \n            # 2. Tính toán Loss (chỉ dựa trên 3 mục tiêu)\n            predictions_tuple = (pred_total, pred_gdm, pred_green)\n            loss = criterion(predictions_tuple, train_targets)\n            epoch_loss += loss.item()\n            \n            # 3. Thu thập kết quả để tính R^2\n            # Chuyển về CPU và lưu dưới dạng numpy\n            all_preds_3['total'].append(pred_total.cpu().numpy())\n            all_preds_3['gdm'].append(pred_gdm.cpu().numpy())\n            all_preds_3['green'].append(pred_green.cpu().numpy())\n            all_targets_list.append(all_targets.cpu().numpy())\n\n    # --- Kết thúc Epoch ---\n    \n    # 4. Ghép tất cả các batch lại\n    # Ghép các list mảng numpy thành một mảng numpy lớn\n    preds_dict_np = {\n        'total': np.concatenate(all_preds_3['total']).flatten(),\n        'gdm':   np.concatenate(all_preds_3['gdm']).flatten(),\n        'green': np.concatenate(all_preds_3['green']).flatten()\n    }\n    targets_np_5 = np.concatenate(all_targets_list) # Shape [N, 5]\n    \n    # 5. Tính điểm R^2\n    competition_score = calculate_competition_score(preds_dict_np, targets_np_5)\n    \n    avg_epoch_loss = epoch_loss / len(loader)\n    \n    return avg_epoch_loss, competition_score","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-30T11:32:30.745287Z","iopub.execute_input":"2025-10-30T11:32:30.745497Z","iopub.status.idle":"2025-10-30T11:32:30.761909Z","shell.execute_reply.started":"2025-10-30T11:32:30.745481Z","shell.execute_reply":"2025-10-30T11:32:30.761328Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import time\nimport torch.optim as optim\nimport gc\n\ndef run_training(fold_to_run):\n    \"\"\"\n    Hàm chính đã được cập nhật với chiến lược 2 giai đoạn (Freeze/Unfreeze).\n    \"\"\"\n    print(f\"\\n{'='*50}\")\n    print(f\"🚀 BẮT ĐẦU HUẤN LUYỆN FOLD {fold_to_run} (Chiến lược 2 giai đoạn) 🚀\")\n    print(f\"{'='*50}\")\n    \n    start_time = time.time()\n    \n    # 1. Chia dữ liệu (Giữ nguyên)\n    print(f\"Đang chia dữ liệu cho Fold {fold_to_run}...\")\n    train_df = df_wide[df_wide['fold'] != fold_to_run].reset_index(drop=True)\n    valid_df = df_wide[df_wide['fold'] == fold_to_run].reset_index(drop=True)\n    \n    # 2. Tạo Datasets (Giữ nguyên)\n    # (Giả sử bạn đang dùng BiomassDataset trả về 4 giá trị)\n    train_dataset = BiomassDataset(\n        df=train_df, transforms_fn=get_train_transforms, image_dir=CFG.IMAGE_DIR,\n        train_target_cols=CFG.TARGET_COLS, all_target_cols=CFG.ALL_TARGET_COLS\n    )\n    valid_dataset = BiomassDataset(\n        df=valid_df, transforms_fn=get_valid_transforms, image_dir=CFG.IMAGE_DIR,\n        train_target_cols=CFG.TARGET_COLS, all_target_cols=CFG.ALL_TARGET_COLS\n    )\n    \n    # 3. Tạo DataLoaders (Giữ nguyên)\n    train_loader = DataLoader(\n        train_dataset, batch_size=CFG.BATCH_SIZE, shuffle=True,\n        num_workers=CFG.NUM_WORKERS, pin_memory=True\n    )\n    valid_loader = DataLoader(\n        valid_dataset, batch_size=CFG.BATCH_SIZE * 2, shuffle=False,\n        num_workers=CFG.NUM_WORKERS, pin_memory=True\n    )\n    \n    # 4. Khởi tạo Mô hình & Loss (Hỗ trợ Multi-GPU)\n    print(f\"Đang tải backbone '{CFG.MODEL_NAME}'...\")\n    model_base = BiomassModel(CFG.MODEL_NAME, CFG.PRETRAINED)\n    \n    if torch.cuda.device_count() > 1:\n        print(f\"Sử dụng {torch.cuda.device_count()} GPU với nn.DataParallel.\")\n        model = nn.DataParallel(model_base)\n    else:\n        model = model_base\n    model.to(CFG.DEVICE)\n    \n    criterion = WeightedBiomassLoss(CFG.LOSS_WEIGHTS).to(CFG.DEVICE)\n    \n    # =================================================================\n    # ✨ GIAI ĐOẠN 1: HUẤN LUYỆN \"HEADS\" (ĐÓNG BĂNG BACKBONE)\n    # =================================================================\n    print(f\"\\n--- GIAI ĐOẠN 1: Đóng băng Backbone (Training Heads) ---\")\n    print(f\"Epochs: 1 đến {CFG.FREEZE_EPOCHS} | LR: {CFG.LEARNING_RATE}\")\n    \n    # Đóng băng tất cả các tham số của backbone\n    # (Dùng .module. để truy cập mô hình gốc bên trong DataParallel)\n    for param in model.module.backbone.parameters():\n        param.requires_grad = False\n        \n    # Tạo optimizer CHỈ cho các \"heads\" (tham số có requires_grad=True)\n    optimizer = optim.Adam(\n        filter(lambda p: p.requires_grad, model.parameters()), \n        lr=CFG.LEARNING_RATE\n    )\n    scheduler = optim.lr_scheduler.ReduceLROnPlateau(\n        optimizer, mode='min', factor=0.1, patience=2 # Giảm patience\n    )\n    \n    best_score = -float('inf') # Theo dõi điểm R^2 tốt nhất qua CẢ 2 GIAI ĐOẠN\n\n    for epoch in range(1, CFG.FREEZE_EPOCHS + 1):\n        print(f\"\\n--- Epoch {epoch}/{CFG.EPOCHS} (Giai đoạn 1) ---\")\n        \n        train_loss = train_one_epoch(model, train_loader, criterion, optimizer, CFG.DEVICE)\n        valid_loss, competition_score = validate_one_epoch(model, valid_loader, criterion, CFG.DEVICE)\n        \n        scheduler.step(valid_loss)\n        \n        print(f\"Epoch {epoch} - Train Loss: {train_loss:.4f} | Valid Loss: {valid_loss:.4f} | Score (R^2): {competition_score:.4f}\")\n        \n        if competition_score > best_score:\n            best_score = competition_score\n            print(f\"✨ Score R^2 cải thiện! Đang lưu mô hình 'best_model_fold{fold_to_run}.pth'...\")\n            torch.save(model.module.state_dict() if isinstance(model, nn.DataParallel) else model.state_dict(), \n                       f'best_model_fold{fold_to_run}.pth')\n\n    # =================================================================\n    # ✨ GIAI ĐOẠN 2: HUẤN LUYỆN TOÀN BỘ (RÃ ĐÔNG BACKBONE)\n    # =================================================================\n    print(f\"\\n--- GIAI ĐOẠN 2: Rã đông Backbone (Fine-tuning) ---\")\n    print(f\"Epochs: {CFG.FREEZE_EPOCHS + 1} đến {CFG.EPOCHS} | LR: {CFG.FINETUNE_LR}\")\n\n    # Rã đông toàn bộ mô hình\n    for param in model.module.backbone.parameters():\n        param.requires_grad = True\n        \n    # Tạo một optimizer MỚI cho TOÀN BỘ mô hình với LR thấp\n    optimizer = optim.Adam(\n        model.parameters(), \n        lr=CFG.FINETUNE_LR # Sử dụng LR mới\n    )\n    scheduler = optim.lr_scheduler.ReduceLROnPlateau(\n        optimizer, mode='min', factor=0.2, patience=3\n    )\n    \n    # Tiếp tục vòng lặp epoch\n    for epoch in range(CFG.FREEZE_EPOCHS + 1, CFG.EPOCHS + 1):\n        print(f\"\\n--- Epoch {epoch}/{CFG.EPOCHS} (Giai đoạn 2) ---\")\n        \n        train_loss = train_one_epoch(model, train_loader, criterion, optimizer, CFG.DEVICE)\n        valid_loss, competition_score = validate_one_epoch(model, valid_loader, criterion, CFG.DEVICE)\n        \n        scheduler.step(valid_loss)\n        \n        print(f\"Epoch {epoch} - Train Loss: {train_loss:.4f} | Valid Loss: {valid_loss:.4f} | Score (R^2): {competition_score:.4f}\")\n        \n        if competition_score > best_score:\n            best_score = competition_score\n            print(f\"✨ Score R^2 cải thiện! Đang lưu mô hình 'best_model_fold{fold_to_run}.pth'...\")\n            torch.save(model.module.state_dict() if isinstance(model, nn.DataParallel) else model.state_dict(), \n                       f'best_model_fold{fold_to_run}.pth')\n            \n    # --- KẾT THÚC HUẤN LUYỆN ---\n    end_time = time.time()\n    print(f\"\\n🎉 Hoàn thành Fold {fold_to_run} sau {(end_time - start_time)/60:.2f} phút.\")\n    print(f\"Điểm R^2 tốt nhất: {best_score:.4f}\")\n    \n    # Dọn dẹp\n    del model, train_loader, valid_loader, train_dataset, valid_dataset\n    gc.collect()\n    torch.cuda.empty_cache()\n\n# --- BẮT ĐẦU CHẠY HUẤN LUYỆN ---\n# (Giữ nguyên)\ntry:\n    for i in range(CFG.N_FOLDS):\n        run_training(fold_to_run=i)\nexcept Exception as e:\n    gc.collect()\n    torch.cuda.empty_cache()\n    raise e","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-30T11:32:30.762780Z","iopub.execute_input":"2025-10-30T11:32:30.763025Z","execution_failed":"2025-10-30T11:32:58.837Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}