{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"pygments_lexer":"ipython3","nbconvert_exporter":"python","version":"3.6.4","file_extension":".py","codemirror_mode":{"name":"ipython","version":3},"name":"python","mimetype":"text/x-python"}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"!pip install -q timm\nimport timm\nimport cv2\nimport glob\nimport os\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nimport torchvision\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\nimport json\nimport matplotlib.pyplot as plt\nfrom tqdm import tqdm\nfrom sklearn.model_selection import StratifiedKFold\nimport time\nimport datetime","metadata":{"_uuid":"d629ff2d2480ee46fbb7e2d37f6b5fab8052498a","_cell_guid":"79c7e3d0-c299-4dcb-8224-4455121ee9b0","execution":{"iopub.status.busy":"2021-05-24T03:07:14.750149Z","iopub.execute_input":"2021-05-24T03:07:14.750462Z","iopub.status.idle":"2021-05-24T03:07:27.630274Z","shell.execute_reply.started":"2021-05-24T03:07:14.750433Z","shell.execute_reply":"2021-05-24T03:07:27.629354Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"LABEL_SMOOTHING = True\nLR_SCHEDULING = True\nTTA = True\nMODEL_ARCHITECTURE = 'vit_base_patch16_384'","metadata":{"execution":{"iopub.status.busy":"2021-05-24T03:07:27.632059Z","iopub.execute_input":"2021-05-24T03:07:27.632393Z","iopub.status.idle":"2021-05-24T03:07:27.641162Z","shell.execute_reply.started":"2021-05-24T03:07:27.632364Z","shell.execute_reply":"2021-05-24T03:07:27.640107Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## 1. Tìm hiểu dữ liệu (Exploratory data analysis)","metadata":{}},{"cell_type":"markdown","source":"Đọc các file dữ liệu cho bài toán:\n* `label_num_to_disease_map.json`: dictionary lưu id bệnh (nhãn) - tên bệnh.\n* `train.csv`: file csv chứa thông tin tên file ảnh và nhãn (id bệnh) tương ứng.\n* `train_images/*`: directory chứa các file ảnh.","metadata":{}},{"cell_type":"code","source":"with open('../input/cassava-leaf-disease-classification/label_num_to_disease_map.json', 'r') as f:\n    label_names = json.load(f)\nannotations = pd.read_csv('../input/cassava-leaf-disease-classification/train.csv')\ntrain_img_dir = '../input/cassava-leaf-disease-classification/train_images'","metadata":{"execution":{"iopub.status.busy":"2021-05-24T03:07:27.6461Z","iopub.execute_input":"2021-05-24T03:07:27.646692Z","iopub.status.idle":"2021-05-24T03:07:27.69986Z","shell.execute_reply.started":"2021-05-24T03:07:27.646653Z","shell.execute_reply":"2021-05-24T03:07:27.699203Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Có 4 loại bệnh khác nhau, label từ 0 -> 3, label 4 là để chỉ lá khỏe mạnh bình thường","metadata":{}},{"cell_type":"code","source":"label_names","metadata":{"execution":{"iopub.status.busy":"2021-05-24T03:07:27.705319Z","iopub.execute_input":"2021-05-24T03:07:27.705554Z","iopub.status.idle":"2021-05-24T03:07:27.713543Z","shell.execute_reply.started":"2021-05-24T03:07:27.70553Z","shell.execute_reply":"2021-05-24T03:07:27.712672Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"print(f'Tập train có {len(annotations)} ảnh.')\nannotations.head()","metadata":{"execution":{"iopub.status.busy":"2021-05-24T03:07:27.717047Z","iopub.execute_input":"2021-05-24T03:07:27.717714Z","iopub.status.idle":"2021-05-24T03:07:27.734532Z","shell.execute_reply.started":"2021-05-24T03:07:27.717674Z","shell.execute_reply":"2021-05-24T03:07:27.7336Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Độ phân giải của ảnh: tất cả các ảnh trong tập train đều có độ phân giải là $600 \\times 800 \\times 3$ ($h \\times w \\times c$)","metadata":{}},{"cell_type":"code","source":"def check_resolution(img_dir):\n    \"\"\"\n    Kiểm tra tất cả ảnh trong directory có những độ phân giải nào\n    Args:\n        img_dir: string - đường dẫn directory chứa file ảnh\n    Returns:\n        set of tuple - tập tất cả các độ phân giải khác nhau, \n            mỗi độ phân giải là 1 tuple (height, width, channels)\n    \"\"\"\n    resolutions = set()\n    img_paths = glob.glob(os.path.join(img_dir, '*.jpg'))\n    for img_path in tqdm(img_paths):\n        resolutions.add(cv2.imread(img_path).shape)\n    return resolutions","metadata":{"execution":{"iopub.status.busy":"2021-05-24T03:07:27.736221Z","iopub.execute_input":"2021-05-24T03:07:27.736578Z","iopub.status.idle":"2021-05-24T03:07:27.74378Z","shell.execute_reply.started":"2021-05-24T03:07:27.736542Z","shell.execute_reply":"2021-05-24T03:07:27.742644Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# resolutions = check_resolution(train_img_dir)\n# print(resolutions)","metadata":{"execution":{"iopub.status.busy":"2021-05-24T03:07:27.745502Z","iopub.execute_input":"2021-05-24T03:07:27.745938Z","iopub.status.idle":"2021-05-24T03:07:27.753162Z","shell.execute_reply.started":"2021-05-24T03:07:27.745901Z","shell.execute_reply":"2021-05-24T03:07:27.752147Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Kiểm tra phân phối các nhãn trong tập train","metadata":{}},{"cell_type":"code","source":"label_counts = annotations['label'].astype(str).map(label_names).value_counts()\nplt.figure(figsize=(8, 8))\nplt.pie(label_counts, labels=label_counts.index, autopct='%1.1f%%')\nplt.title('Phân phối nhãn trong tập train')\nplt.axis('equal')\nplt.show()","metadata":{"execution":{"iopub.status.busy":"2021-05-24T03:07:27.754957Z","iopub.execute_input":"2021-05-24T03:07:27.755357Z","iopub.status.idle":"2021-05-24T03:07:27.892474Z","shell.execute_reply.started":"2021-05-24T03:07:27.755321Z","shell.execute_reply":"2021-05-24T03:07:27.891637Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"$\\rightarrow$ Có thể thấy phân phối các nhãn không cân bằng. Cassava Mosaic Disease chiếm hơn một nửa trong bộ dữ liệu","metadata":{}},{"cell_type":"markdown","source":"Visualize một vài ảnh ví dụ trong tập train cùng với nhãn tương ứng","metadata":{}},{"cell_type":"code","source":"def visualize(label, img_dir, annotations, label_names):\n    \"\"\"\n    Visualize một ảnh trong tập train cùng với label tương ứng của nó\n    \"\"\"\n    img_id = annotations[annotations['label']==label].iloc[0]['image_id']\n    img_path = os.path.join(img_dir, img_id)\n    img = cv2.imread(img_path) # BGR format\n    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # BGR -> RGB\n    plt.figure(figsize=(8, 8))\n    plt.imshow(img)\n    plt.axis('off')\n    plt.title(label_names[str(label)])\n    plt.show()","metadata":{"execution":{"iopub.status.busy":"2021-05-24T03:07:27.893866Z","iopub.execute_input":"2021-05-24T03:07:27.894414Z","iopub.status.idle":"2021-05-24T03:07:27.903946Z","shell.execute_reply.started":"2021-05-24T03:07:27.894377Z","shell.execute_reply":"2021-05-24T03:07:27.903232Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"for label in range(5):\n    visualize(label, train_img_dir, annotations, label_names)","metadata":{"execution":{"iopub.status.busy":"2021-05-24T03:07:27.905702Z","iopub.execute_input":"2021-05-24T03:07:27.906304Z","iopub.status.idle":"2021-05-24T03:07:28.96992Z","shell.execute_reply.started":"2021-05-24T03:07:27.906261Z","shell.execute_reply":"2021-05-24T03:07:28.9691Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Có thể thấy dữ liệu có nhiễu (đánh nhãn sai). Ảnh cuối ở trên không thể nào thuộc nhãn `Healthy` được","metadata":{}},{"cell_type":"markdown","source":"## 2. Tiền xử lý dữ liệu (Data preprocess)","metadata":{}},{"cell_type":"markdown","source":"## a. Load dataset\nhttps://pytorch.org/tutorials/recipes/recipes/custom_dataset_transforms_loader.html\n\nTiền xử lý dữ liệu ảnh đầu vào:\n1. Đọc dữ liệu ảnh và lưu dưới dạng numpy array sử dụng opencv\n2. Resize về kích thước cố định (chẳng hạn 512x512)\n3. Rescale pixel values từ [0, 255] về [0, 1] rồi normalize với rgb mean và std của dataset này (hoặc của ImageNet dataset).\n4. Convert numpy array thành torch tensor làm input cho cnn model\n\nĐối với dữ liệu train có thể sử dụng data augmentation (làm giàu dữ liệu) để cải thiện hiệu quả:\n* Basic augmentation: random resize + crop, horizontal/vertical flip, shift + scale + rotate, adjust HSV/RGB values, adjust brightness/contrast, v.v.\n* Advanced augmentation: Cutout, Mixup, Cutmix, v.v. \n\nTest time augmentation: thực hiện nhiều augmentation lúc inference và lấy kết quả trung bình","metadata":{}},{"cell_type":"code","source":"class CassavaDataset(torch.utils.data.Dataset):\n    def __init__(self, img_dir, img_size, rgb_mean, rgb_std, \n                 augment=False, tta_idx=None, annotations=None):\n        \"\"\"\n        Args:\n            img_dir: string - đường dẫn directory chứa ảnh\n            img_size: int - kích thước ảnh muốn resize\n            rgb_mean: tuple (r, g, b) - kì vọng của từng channel (dùng để chuẩn hóa)\n            rgb_std: tuple (r, g, b) - độ lệch chuẩn của từng channel (dùng để chuẩn hóa)\n            augment: boolean - có làm giàu dữ liệu hay không\n            tta_idx: int 0->4 hoặc None - test time augmentation, thực hiện phép biến đổi nào,\n                                          =None thì biến đổi random (train)\n                                          chỉ có tác dụng khi augment=True\n            annotations: pd.DataFrame object - dataframe chứa thông tin label của ảnh\n                                               dùng cho tập train/validate (= None khi inference)\n        \"\"\"\n        self.img_dir = img_dir\n        self.annotations = annotations\n        self.img_ids = None\n        if self.annotations is None:\n            self.img_ids = os.listdir(self.img_dir)\n            \n        # 5 phép biến đổi tta:\n        # 0. Resize về img_size x img_size (dùng lúc validate)\n        # 1. Crop img_size x img_size ở chính giữa ảnh\n        # 2. Crop rồi lật ngang\n        # 3. Crop rồi lật dọc\n        # 4. Crop rồi chuyển vị\n        tta_transform_lists = [\n            [A.Resize(img_size, img_size)],\n            [A.CenterCrop(img_size, img_size, p=1.0)],\n            [\n                A.CenterCrop(img_size, img_size, p=1.0),\n                A.HorizontalFlip(p=1.0)\n            ],\n            [\n                A.CenterCrop(img_size, img_size, p=1.0),\n                A.VerticalFlip(p=1.0)\n            ],\n            [\n                A.CenterCrop(img_size, img_size, p=1.0),\n                A.Transpose(p=1.0)\n            ]\n        ]\n            \n        if augment:\n            # Test time augmentation cho lúc test\n            if tta_idx is not None:\n                transform_list = tta_transform_lists[tta_idx] + [\n                    A.Normalize(mean=rgb_mean, std=rgb_std, \n                                max_pixel_value=255.0, p=1.0),\n                    ToTensorV2(p=1.0) # (h, w, c) -> (c, h, w)\n                ]\n            # Random augmentation cho lúc train\n            else:\n                transform_list = [\n                    A.RandomResizedCrop(img_size, img_size,\n                                        ratio=(0.5, 2.0)),\n                    A.Transpose(p=0.5),\n                    A.HorizontalFlip(p=0.5),\n                    A.VerticalFlip(p=0.5),\n                    A.ShiftScaleRotate(p=0.5),\n                    A.HueSaturationValue(hue_shift_limit=0.2, sat_shift_limit=0.2, \n                                         val_shift_limit=0.2, p=0.5),\n                    A.RandomBrightnessContrast(brightness_limit=(-0.1, 0.1), \n                                               contrast_limit=(-0.1, 0.1), p=0.5),\n                    A.Normalize(mean=rgb_mean, std=rgb_std, \n                                max_pixel_value=255.0, p=1.0),\n                    A.CoarseDropout(p=0.5),\n                    A.Cutout(p=0.5),\n                    ToTensorV2(p=1.0) # (h, w, c) -> (c, h, w)\n                ]\n        else:\n            transform_list = tta_transform_lists[0] + [\n                A.Normalize(mean=rgb_mean, std=rgb_std, \n                            max_pixel_value=255.0, p=1.0),\n                ToTensorV2(p=1.0) # (h, w, c) -> (c, h, w)\n            ]\n        self.transform = A.Compose(transform_list)\n    \n    def __len__(self):\n        if self.annotations is not None:\n            return len(self.annotations)\n        else:\n            self.img_ids = os.listdir(self.img_dir)\n            return len(self.img_ids)\n        \n    def load_image(self, img_id):\n        img = cv2.imread(os.path.join(self.img_dir, img_id)) # (h, w, c) in BGR format\n        img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # BGR -> RGB\n        img = self.transform(image=img)['image'] # augment (optional), \n                                                 # normalized and rescaled to [0, 1], \n                                                 # shape (3, img_size, img_size)\n        return img\n    \n    def __getitem__(self, idx):\n        if self.annotations is not None:\n            row = self.annotations.iloc[idx]\n            img_id, label = row['image_id'], row['label']\n            img = self.load_image(img_id)\n            return img, label\n        else:\n            img_id = self.img_ids[idx]\n            img = self.load_image(img_id)\n            return img, img_id","metadata":{"execution":{"iopub.status.busy":"2021-05-24T03:07:28.971232Z","iopub.execute_input":"2021-05-24T03:07:28.971727Z","iopub.status.idle":"2021-05-24T03:07:28.998382Z","shell.execute_reply.started":"2021-05-24T03:07:28.971682Z","shell.execute_reply":"2021-05-24T03:07:28.997613Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## b. Stratified K-fold split\n\n* Chia tập dữ liệu train thành 5 fold\n* Số lượng dữ liệu của các class không cân bằng $\\rightarrow$ sử dụng stratified sampling\n* Stratified sampling: phương pháp chia đảm bảo tỉ lệ các class trong mỗi tập con vẫn giữ nguyên/gần giống so với tập gốc. https://en.wikipedia.org/wiki/Stratified_sampling","metadata":{}},{"cell_type":"code","source":"skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)\nfolds = skf.split(annotations['image_id'].values, annotations['label'].values)\nfor i, (train_indices, val_indices) in enumerate(folds):\n    annotations.loc[val_indices, 'fold'] = i\nannotations['fold'] = annotations['fold'].astype(int)","metadata":{"execution":{"iopub.status.busy":"2021-05-24T03:07:28.999757Z","iopub.execute_input":"2021-05-24T03:07:29.000132Z","iopub.status.idle":"2021-05-24T03:07:29.023357Z","shell.execute_reply.started":"2021-05-24T03:07:29.000094Z","shell.execute_reply":"2021-05-24T03:07:29.022679Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"dataframe bây giờ có thêm cột `fold` để chỉ ra mỗi sample thuộc fold nào","metadata":{}},{"cell_type":"code","source":"annotations.head()","metadata":{"execution":{"iopub.status.busy":"2021-05-24T03:07:29.026228Z","iopub.execute_input":"2021-05-24T03:07:29.026464Z","iopub.status.idle":"2021-05-24T03:07:29.037546Z","shell.execute_reply.started":"2021-05-24T03:07:29.02644Z","shell.execute_reply":"2021-05-24T03:07:29.036784Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# kích thước ảnh resize để cho vào mạng CNN\nimg_size = 384 # 512\n\n# RGB mean and std of this dataset\n# rgb_mean = (0.43032043, 0.49672632, 0.31342008)\n# rgb_std = (0.21909042, 0.22394303, 0.20059191)\n\n# RGB mean and std of Imagenet dataset\nrgb_mean = (0.485, 0.456, 0.406)\nrgb_std = (0.229, 0.224, 0.225)\n\n# chọn fold để làm tập validation, các fold còn lại là tập train\nfold = 1\n\n# chia dataframe thành train-validation\ntrain_annotations = annotations[annotations['fold']!=fold].reset_index(drop=True)\nval_annotations = annotations[annotations['fold']==fold].reset_index(drop=True)\n\n# Đối với tập train thì thực hiện làm giàu dữ liệu, còn validate thì không cần\ntrain_dataset = CassavaDataset(img_dir=train_img_dir, \n                               img_size=img_size,\n                               rgb_mean=rgb_mean, \n                               rgb_std=rgb_std,\n                               augment=True,\n                               tta_idx=None,\n                               annotations=train_annotations)\n\nval_dataset = CassavaDataset(img_dir=train_img_dir,\n                             img_size=img_size,\n                             rgb_mean=rgb_mean,\n                             rgb_std=rgb_std,\n                             augment=False,\n                             tta_idx=None,\n                             annotations=val_annotations)","metadata":{"execution":{"iopub.status.busy":"2021-05-24T03:07:29.038762Z","iopub.execute_input":"2021-05-24T03:07:29.039129Z","iopub.status.idle":"2021-05-24T03:07:29.053105Z","shell.execute_reply.started":"2021-05-24T03:07:29.039092Z","shell.execute_reply":"2021-05-24T03:07:29.05222Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## c. DataLoader\nThực hiện shuffle dữ liệu, gộp dữ liệu thành các batch, v.v.","metadata":{}},{"cell_type":"code","source":"batch_size = 16 # số sample trong một batch\n\n# Tập train thì cần xáo (shuffle) dữ liệu để đảm bảo tính ngẫu nhiên, còn tập validate thì không cần\ntrain_loader = torch.utils.data.DataLoader(train_dataset, batch_size=batch_size,\n                                           shuffle=True, num_workers=4)\nval_loader = torch.utils.data.DataLoader(val_dataset, batch_size=batch_size, \n                                         shuffle=False, num_workers=4)","metadata":{"execution":{"iopub.status.busy":"2021-05-24T03:07:29.05485Z","iopub.execute_input":"2021-05-24T03:07:29.055209Z","iopub.status.idle":"2021-05-24T03:07:29.061887Z","shell.execute_reply.started":"2021-05-24T03:07:29.055172Z","shell.execute_reply":"2021-05-24T03:07:29.060575Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## 3. Model\nTransfer learning: sử dụng mô hình resnet18 pretrained với Imagenet dataset để finetune cho bài toán phân loại bệnh lá sắn\nhttps://pytorch.org/tutorials/beginner/transfer_learning_tutorial.html#finetuning-the-convnet\n\nNote:\n* Để giữ output shape không thay đổi so với input shape khi đi qua convolution filter (\"same padding\") thì $$2 \\cdot padding = kernel\\_size - 1$$ (với điều kiện dilation và stride để mặc định bằng 1)\n* Hàm `torch.nn.CrossEntropyLoss()` không tính cross entropy giữa output và target ngay mà trước hết tính softmax của output để convert thành dạng probabilities, rồi sau đó mới tính cross entropy giữa softmax(output) và target. Do đó nếu dùng `torch.nn.CrossEntropyLoss()` thì lúc tạo CNN không cần phải dùng `torch.nn.Softmax()` ở layer cuối cùng. (Output raw của neural network chưa đi qua softmax còn hay được gọi là [\"logits\"](https://developers.google.com/machine-learning/glossary/#logits)). Tham khảo: https://stackoverflow.com/a/49839941","metadata":{}},{"cell_type":"code","source":"n_classes = len(label_names) # số class = 5 (4 loại bệnh + 1 bình thường)\n\n# Load mô hình với weights đã được pretrain trên ImageNet dataset\nmodel = timm.create_model(MODEL_ARCHITECTURE, \n                          pretrained=True, \n                          num_classes=n_classes,\n                          checkpoint_path='')","metadata":{"execution":{"iopub.status.busy":"2021-05-24T03:07:29.063577Z","iopub.execute_input":"2021-05-24T03:07:29.063953Z","iopub.status.idle":"2021-05-24T03:07:32.063607Z","shell.execute_reply.started":"2021-05-24T03:07:29.063918Z","shell.execute_reply":"2021-05-24T03:07:32.06268Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## 4. Optimizer, Loss function, Utilities","metadata":{}},{"cell_type":"markdown","source":"* Như đã đề cập ở trên, mô hình output ra logits (chưa đi qua lớp softmax) $\\rightarrow$ dùng loss function `nn.CrossEntropyLoss()`\n* Thuật toán tối ưu sử dụng là Stochastic Gradient Descent (hoặc Adam), các biến cần tối ưu là các tham số của mô hình\n* Learning rate scheduler `StepLR`: cứ mỗi `step_size` step thì nhân learning rate với `gamma`\n* Label smoothing: nhãn one-hot (1, 0, 0, 0, 0) $\\rightarrow$ (0.8, 0.05, 0.05, 0.05, 0.05), tránh overfit, hạn chế ảnh hưởng dữ liệu nhiễu","metadata":{}},{"cell_type":"code","source":"def smooth_one_hot(true_labels: torch.Tensor, classes: int, smoothing=0.0):\n    \"\"\"\n    if smoothing == 0, it's one-hot method\n    if 0 < smoothing < 1, it's smooth method\n\n    \"\"\"\n    assert 0 <= smoothing < 1\n    confidence = 1.0 - smoothing\n    label_shape = torch.Size((true_labels.size(0), classes))\n    with torch.no_grad():\n        true_dist = torch.empty(size=label_shape, device=true_labels.device)\n        true_dist.fill_(smoothing / (classes - 1))\n        true_dist.scatter_(1, true_labels.data.unsqueeze(1), confidence)\n    return true_dist\n\n\nclass LabelSmoothingLoss(nn.Module):\n    def __init__(self, classes, smoothing=0.0, dim=-1):\n        super(LabelSmoothingLoss, self).__init__()\n        self.smoothing = smoothing\n        self.cls = classes\n        self.dim = dim\n\n    def forward(self, pred, target):\n        pred = pred.log_softmax(dim=self.dim)\n        true_dist = smooth_one_hot(target, self.cls, self.smoothing)\n        return torch.mean(torch.sum(-true_dist * pred, dim=self.dim))\n\nif LABEL_SMOOTHING:\n    # label smoothing với hệ số smoothing 0.2\n    loss_func = LabelSmoothingLoss(classes=n_classes, smoothing=0.2)\nelse:\n    loss_func = nn.CrossEntropyLoss()\n    \n# optimizer = torch.optim.SGD(model.parameters(), lr=0.01)\noptimizer = torch.optim.Adam(model.parameters(), lr=5e-5)\nscheduler = None\nif LR_SCHEDULING:\n    # giảm lr 5 lần mỗi 5 epoch\n    scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=5, gamma=0.2)","metadata":{"execution":{"iopub.status.busy":"2021-05-24T03:07:32.065189Z","iopub.execute_input":"2021-05-24T03:07:32.065527Z","iopub.status.idle":"2021-05-24T03:07:32.089207Z","shell.execute_reply.started":"2021-05-24T03:07:32.065492Z","shell.execute_reply":"2021-05-24T03:07:32.088376Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Một số class và hàm dùng để track running loss/accuracy trong quá trình train:\n[(reference)](https://github.com/rwightman/pytorch-image-models/blob/master/timm/utils/metrics.py)","metadata":{}},{"cell_type":"code","source":"# https://github.com/rwightman/pytorch-image-models/blob/master/timm/utils/metrics.py\nclass AverageMeter:\n    \"\"\"\n    Computes and stores the average and current value\n    Use this for losses with reduction='mean'\n    \"\"\"\n    def __init__(self):\n        self.reset()\n\n    def reset(self):\n        self.val = 0\n        self.avg = 0\n        self.sum = 0\n        self.count = 0\n\n    def update(self, val, n=1):\n        self.val = val\n        self.sum += val * n\n        self.count += n\n        self.avg = self.sum / self.count\n        \ndef accuracy(output, target, topk=(1,)):\n    \"\"\"\n    Computes the accuracy over the k top predictions for the specified values of k\n    \"\"\"\n    maxk = max(topk)\n    batch_size = target.size(0)\n    _, pred = output.topk(maxk, 1, True, True)\n    pred = pred.t()\n    correct = pred.eq(target.reshape(1, -1).expand_as(pred))\n    return [correct[:k].reshape(-1).float().sum(0) * 100. / batch_size for k in topk]","metadata":{"execution":{"iopub.status.busy":"2021-05-24T03:07:32.091262Z","iopub.execute_input":"2021-05-24T03:07:32.091975Z","iopub.status.idle":"2021-05-24T03:07:32.103176Z","shell.execute_reply.started":"2021-05-24T03:07:32.091934Z","shell.execute_reply":"2021-05-24T03:07:32.102027Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## 5. Training loop\n\nNote\n* Một số loại layer như **dropout** hay **batchnorm** sẽ làm cho behavior của model khi train và khi test khác nhau. Sử dụng `model.train()` và `model.eval()` để thay đổi mode của model theo ý muốn.\nhttps://stackoverflow.com/a/51433411\n* Các bước để train một batch:\n 1. Cho batch ảnh đi qua model $\\rightarrow$ output là logits\n 2. Tính cross entropy loss của `softmax(logits)` và batch nhãn\n 3. Lan truyền ngược để tính gradient loss theo tất cả các tham số mô hình\n 4. Cập nhật tất cả tham số dựa trên gradient tính ở bước trên\n* Mặc định mỗi lần gọi `.backward()` để tính gradient thì pytorch sẽ **tích lũy** gradient ở các node lá. Do đó trước đấy ta cần phải gọi `zero_grad()` để reset tất cả gradient về 0. https://stackoverflow.com/questions/48001598/why-do-we-need-to-call-zero-grad-in-pytorch","metadata":{}},{"cell_type":"code","source":"def train_epoch(model, loader, device, loss_func, optimizer, verbose):\n    \"\"\"\n    Train mô hình một epoch\n    Args:\n        model: model cần train\n        loader: torch dataloader để load dữ liệu train\n        device: torch device (gpu hoặc cpu)\n        loss_func: hàm tổn thất\n        optimizer: thuật toán tối ưu sử dụng\n        verbose: boolean - có in đầy đủ thông tin trong quá trình train hay không\n    Returns:\n        summary_loss, summary_acc\n    \"\"\"\n    model.train() # chuyển mode sang 'train' (dùng cho dropout, batchnorm, v.v.)\n    summary_loss = AverageMeter() # track running loss trong quá trình train\n    summary_acc = AverageMeter() # track running accuracy trong quá trình train\n    start = time.time() # track thời gian train\n    \n    n = len(loader) # số batch một epoch\n    # mỗi batch của loader là một tuple (img_batch, label_batch)\n    # img_batch: torch tensor, shape (batch_size, 3, img_size, img_size)\n    # label_batch: torch tensor, shape (batch_size,)\n    for step, (img_batch, label_batch) in enumerate(loader):\n        batch_size = img_batch.size(0)\n        \n        # Chuyển dữ liệu đến gpu (nếu có)\n        img_batch = img_batch.to(device)\n        label_batch = label_batch.to(device)\n        \n        # output của model là logits\n        # shape là (batch_size, n_classes)\n        out_batch = model(img_batch)\n        # Tính loss của batch này\n        loss = loss_func(out_batch, label_batch)\n        \n        # Thực hiện lan truyền ngược (backpropagation)\n        # 1. Reset gradient của tất cả tham số mô hình về 0\n        optimizer.zero_grad()\n        # 2. Tính gradient loss đối với tất cả tham số mô hình\n        loss.backward()\n        # 3. Cập nhật tất cả các tham số mô hình dựa vào các gradient tính ở trên\n        optimizer.step()\n        \n        # Tính accuracy cho batch này (nếu ko cần track gradient thì gọi `torch.no_grad()`)\n        with torch.no_grad():\n            acc = accuracy(out_batch, label_batch)[0]\n        \n        # Update running loss và running accuracy\n        summary_loss.update(loss.item(), batch_size)\n        summary_acc.update(acc.item(), batch_size)\n        if verbose:\n            print('Train step {}/{}, loss: {:.5f}'.format(step + 1, n, summary_loss.avg), end='\\r')\n    train_time = str(datetime.timedelta(seconds=time.time() - start))\n    print('Train loss: {:.5f} - Train acc: {:.2f}% - time: {}'.format(summary_loss.avg, \n                                                                      summary_acc.avg,\n                                                                      train_time))\n    return summary_loss, summary_acc\n        \ndef evaluate_epoch(model, loader, device, loss_func, verbose):\n    \"\"\"\n    Evaluate mô hình\n    Args:\n        model: model cần evaluate\n        loader: torch dataloader để load dữ liệu validation\n        device: torch device (gpu hoặc cpu)\n        loss_func: hàm tổn thất\n        verbose: boolean - có in đầy đủ thông tin trong quá trình evaluate hay không\n    Returns:\n        summary_loss, summary_acc\n    \"\"\"\n    model.eval() # chuyển mode sang 'evaluate' (dùng cho dropout, batchnorm, v.v.)\n    summary_loss = AverageMeter() # track running loss trong quá trình evaluate\n    summary_acc = AverageMeter() # track running accuracy trong quá trình evaluate\n    start = time.time() # track thời gian evaluate\n    \n    n = len(loader) # số batch một epoch\n    # mỗi batch của loader là một tuple (img_batch, label_batch)\n    # img_batch: torch tensor, shape (batch_size, 3, img_size, img_size)\n    # label_batch: torch tensor, shape (batch_size,)\n    for step, (img_batch, label_batch) in enumerate(loader):\n        with torch.no_grad():\n            batch_size = img_batch.size(0)\n            \n            # Chuyển dữ liệu đến gpu (nếu có)\n            img_batch = img_batch.to(device)\n            label_batch = label_batch.to(device)\n\n            # output của model là logits\n            # shape là (batch_size, n_classes)\n            out_batch = model(img_batch)\n            # Tính loss của batch này\n            loss = loss_func(out_batch, label_batch)\n            # Tính accuracy cho batch này\n            acc = accuracy(out_batch, label_batch)[0]\n            \n            # Update running loss và running accuracy\n            summary_loss.update(loss.detach().item(), batch_size)\n            summary_acc.update(acc.detach().item(), batch_size)\n            if verbose:\n                print('Val step {}/{}'.format(step + 1, n), end='\\r')\n    eval_time = str(datetime.timedelta(seconds=time.time() - start))\n    print('Val loss: {:.5f} - Val acc: {:.2f}% - time: {}'.format(summary_loss.avg,\n                                                                  summary_acc.avg,\n                                                                  eval_time))\n    return summary_loss, summary_acc","metadata":{"execution":{"iopub.status.busy":"2021-05-24T03:07:32.105025Z","iopub.execute_input":"2021-05-24T03:07:32.105701Z","iopub.status.idle":"2021-05-24T03:07:32.127797Z","shell.execute_reply.started":"2021-05-24T03:07:32.105661Z","shell.execute_reply":"2021-05-24T03:07:32.126995Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Mỗi vòng lặp train thực hiện các công việc:\n1. Train 1 epoch trên training set\n2. Evaluate trên validation set $\\rightarrow$ tính loss, metrics trên tập validation\n3. Nếu performance trên tập validation (loss hoặc accuracy ...) đạt kỷ lục mới thì save model","metadata":{}},{"cell_type":"code","source":"# mặc định sử dụng GPU, nếu không thì sử dụng CPU\ndevice = torch.device(\"cuda:0\" if torch.cuda.is_available() else \"cpu\")\nmodel.to(device)\n\n# một số biến để track kỉ lục hiện tại\nbest_acc = -np.inf\nbest_epoch = 0\n\nverbose = False # có in đầy đủ thông tin không\nepochs = 15 # số epoch\n\n# lưu mô hình\nsave_path = '{}_{}'.format(MODEL_ARCHITECTURE, img_size)\nif LABEL_SMOOTHING:\n    save_path += '_ls'\nif LR_SCHEDULING:\n    save_path += '_lrs'\nsave_path += '_fold{}.pth'.format(fold)\n\nfor epoch in range(epochs):\n    print('Epoch {}/{}:'.format(epoch + 1, epochs))\n    print('-' * 10)\n    train_loss, train_acc = train_epoch(model, train_loader, device, loss_func, optimizer, verbose)\n    val_loss, val_acc = evaluate_epoch(model, val_loader, device, loss_func, verbose)\n    \n    # điều chỉnh learning rate\n    if LR_SCHEDULING:\n        scheduler.step()\n        \n    # nếu validation accuracy đạt kỉ lục mới thì save model\n    if val_acc.avg > best_acc:\n        best_acc = val_acc.avg\n        best_epoch = epoch\n        print('Saving model...')\n        torch.save(model.state_dict(),\n                   save_path)\nprint('Finished. Best val_acc achieved is {:.2f}% at epoch {}'.format(best_acc, best_epoch))","metadata":{"execution":{"iopub.status.busy":"2021-05-24T03:07:32.129554Z","iopub.execute_input":"2021-05-24T03:07:32.130154Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## 6. Inference","metadata":{}},{"cell_type":"markdown","source":"Kết quả inference cho mỗi ảnh: \n* `probabilities` là xác suất dự đoán từng nhãn\n* `label` là nhãn dự đoán (nhãn có xác suất cao nhất)","metadata":{}},{"cell_type":"code","source":"def inference(model, loader, device):\n    \"\"\"\n    Inference với mô hình đã train\n    Args:\n        model: model đã được train\n        loader: torch dataloader để load dữ liệu test\n        device: torch device (gpu hoặc cpu)\n    Returns:\n        dataframe chứa kết quả inference\n    \"\"\"\n    model.eval() # chuyển mode sang 'evaluate' (dùng cho dropout, batchnorm, v.v.)\n    preds = [] # lưu kết quả predict\n    img_ids = [] # lưu các image id\n    n = len(loader)\n    \n    # mỗi batch của loader là một tuple (img_batch, img_id_batch)\n    # img_batch: torch tensor, shape (batch_size, 3, img_size, img_size)\n    # img_id_batch: một list các string dưới dạng torch tensor (batch_size,)\n    for step, (img_batch, img_id_batch) in enumerate(loader):\n        # khi test không cần tính gradient\n        with torch.no_grad():\n            # Chuyển dữ liệu đến gpu (nếu có)\n            img_batch = img_batch.to(device)\n            \n            # output của model là logits\n            # sau khi cho đi qua lớp softmax sẽ thành probabilities\n            # shape là (batch_size, n_classes)\n            out_batch = nn.functional.softmax(model(img_batch), dim=1)\n            \n            # thêm các kết quả trong batch vào list tổng\n            preds.append(out_batch.detach().cpu().numpy())\n            img_ids += list(img_id_batch)\n            \n            print('Inference step {}/{}'.format(step + 1, n), end='\\r')\n    print('\\n')\n            \n    preds = np.concatenate(preds, axis=0) # convert list sang np.array, shape (n_samples, n_classes)\n    # lưu kết quả vào một dataframe\n    result = pd.DataFrame({'image_id': img_ids, \n                           'probabilities': list(preds)})\n    return result","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## a. Evaluate và visualize trên tập validation","metadata":{}},{"cell_type":"code","source":"# load best model weights\nmodel.load_state_dict(torch.load(save_path))\n\neval_loaders = []\n\nif TTA:\n    for tta_idx in range(5):\n        eval_dataset = CassavaDataset(img_dir=train_img_dir,\n                                      img_size=img_size,\n                                      rgb_mean=rgb_mean,\n                                      rgb_std=rgb_std,\n                                      augment=True,\n                                      tta_idx=tta_idx,\n                                      annotations=val_annotations)\n        eval_loaders.append(torch.utils.data.DataLoader(eval_dataset, batch_size=batch_size, \n                                                        shuffle=False, num_workers=4))\nelse:\n    eval_dataset = CassavaDataset(img_dir=train_img_dir,\n                                  img_size=img_size,\n                                  rgb_mean=rgb_mean,\n                                  rgb_std=rgb_std,\n                                  augment=False,\n                                  tta_idx=None,\n                                  annotations=val_annotations)\n    eval_loaders.append(torch.utils.data.DataLoader(eval_dataset, batch_size=batch_size, \n                                                    shuffle=False, num_workers=4))","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Visualize một số ảnh và dự đoán trên tập validation","metadata":{}},{"cell_type":"code","source":"eval_results = [inference(model, eval_loader, device)[['probabilities']]\n               for eval_loader in eval_loaders]\neval_results = [eval_result.rename(columns={'probabilities': f'probabilities_{i}'})\n               for i, eval_result in enumerate(eval_results)] ","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"eval_result = pd.concat(eval_results, axis=1)\neval_result['probabilities'] = eval_result.sum(axis=1) / len(eval_result.columns)\neval_result['pred'] = eval_result['probabilities'].map(lambda x: np.argmax(x)) # probability to class\neval_result = eval_result[['probabilities', 'pred']]\neval_result['image_id'] = val_annotations['image_id']","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"eval_result = pd.merge(eval_result, val_annotations[['image_id', 'label']], on='image_id')\neval_acc = (eval_result['pred'] == eval_result['label']).sum() / len(eval_result)\nprint('Validation accuracy: {:.2f}%'.format(100*eval_acc))","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"def visualize_prediction(img_dir, eval_result, label_names):\n    \"\"\"\n    Visualize một ảnh ngẫu nhiên trong tập validation và dự đoán tương ứng\n    \"\"\"\n    random_row = eval_result.sample()\n    img_id = random_row['image_id'].item() \n    label = random_row['label'].item() \n    pred = random_row['pred'].item()\n    probabilities = random_row['probabilities'].item()\n    \n    img_path = os.path.join(img_dir, img_id)\n    img = cv2.imread(img_path) # BGR format\n    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # BGR -> RGB\n    \n    plt.figure(figsize=(8, 8))\n    plt.imshow(img)\n    plt.axis('off')\n    title = 'Nhãn: {}\\nDự đoán: {}, xác suất: {:.2f}%'.format(label_names[str(label)],\n                                                              label_names[str(pred)],\n                                                              100*probabilities[pred])\n    plt.title(title)","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"for _ in range(5):\n    visualize_prediction(train_img_dir, eval_result, label_names)","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## b. Inference trên tập test","metadata":{}},{"cell_type":"markdown","source":"Đối với tập test:\n* `augment=False`: không làm giàu dữ liệu\n* `annotations=None`: không có nhãn trong tập test, mỗi sample trong tập test thay vì trả về `(ảnh, label)` thì sẽ trả về `(ảnh, id ảnh)`\n* dataloader `shuffle=False`: không cần phải xáo dữ liệu","metadata":{}},{"cell_type":"code","source":"test_img_dir = '../input/cassava-leaf-disease-classification/test_images'\n\ntest_loaders = []\n\nif TTA:\n    for tta_idx in range(5):\n        test_dataset = CassavaDataset(img_dir=test_img_dir,\n                                      img_size=img_size,\n                                      rgb_mean=rgb_mean,\n                                      rgb_std=rgb_std,\n                                      augment=True,\n                                      tta_idx=tta_idx,\n                                      annotations=None)\n        test_loaders.append(torch.utils.data.DataLoader(test_dataset, batch_size=batch_size, \n                                                        shuffle=False, num_workers=4))\nelse:\n    test_dataset = CassavaDataset(img_dir=test_img_dir,\n                                  img_size=img_size,\n                                  rgb_mean=rgb_mean,\n                                  rgb_std=rgb_std,\n                                  augment=False,\n                                  tta_idx=None,\n                                  annotations=None)\n    test_loaders.append(torch.utils.data.DataLoader(test_dataset, batch_size=batch_size, \n                                                    shuffle=False, num_workers=4))","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"test_results = [inference(model, test_loader, device).set_index('image_id')\n               for test_loader in test_loaders]\ntest_results = [test_result.rename(columns={'probabilities': f'probabilities_{i}'})\n               for i, test_result in enumerate(test_results)] \n\ntest_result = pd.concat(test_results, axis=1)\ntest_result['probabilities'] = test_result.sum(axis=1) / len(test_result.columns)\ntest_result['label'] = test_result['probabilities'].map(lambda x: np.argmax(x)) # probability to class\ntest_result","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## c. Submission","metadata":{}},{"cell_type":"markdown","source":"Kiểm tra lại format submission giống với file `sample_submission.csv`","metadata":{}},{"cell_type":"code","source":"sample_submission = pd.read_csv('../input/cassava-leaf-disease-classification/sample_submission.csv')\nsample_submission","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"test_result[['label']].reset_index()","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Sau khi kiểm tra cẩn thận, lưu file submission vào file `submission.csv` (phải đặt ở directory `/kaggle/working/`)","metadata":{}},{"cell_type":"code","source":"test_result[['label']].reset_index().to_csv('submission.csv', index=False)","metadata":{"trusted":true},"execution_count":null,"outputs":[]}]}