-
2021 ML Dev-Matching | 미술 작품 분류하기 : 출제자가 추천한 우수 코드 B취업 이야기/데브매칭 문제 해설 2021. 7. 6. 09:31
목차
- Set Device, Fix seed
- Training Configs
- Define Training Function
- Define Dataset Class
- Inference
Set Device, Fix seed¶
In [1]:import random import numpy as np import torch device = torch.device("cuda" if torch.cuda.is_available() else "cpu") # 시드 고정. random_seed = 42 torch.manual_seed(random_seed) torch.cuda.manual_seed(random_seed) torch.cuda.manual_seed_all(random_seed) torch.backends.cudnn.deterministic = True torch.backends.cudnn.benchmark = False np.random.seed(random_seed) random.seed(random_seed)
Training Configs¶
In [2]:# 데이터가 많지 않기 때문에 학습해야 할 파라미터가 너무 많을 경우 모델 최적화가 제대로 되지 않을 가능성이 큼. # 실제로 efficientnet 사용했을 때보다 benchmark 기록은 낮지만 더 가벼운 모델을 썼을 때 리더보드 점수가 높았기 때문에, # 가벼우면서 benchmark 성능이 좋은 모델로 선택. model_name = "swsl_resnext50_32x4d" # 실험 결과 대부분 30 epoch 전에 수렴하기 때문에 최대 epoch 30으로 설정. epoch_size = 30 batch_size = 48 # 너무 낮으면 학습이 안되고, 너무 높으면 최적의 값으로 수렴하지 못함. # 실험 결과 최적의 learning rate가 1e-4에서 public 리더보드에서 가장 좋은 성능을 보여 # 해당 learning rate 사용. learning_rate = 1e-4 early_stop = 5 k_fold_num = 5
Define Training Function¶
In [3]:import timm import torch.nn as nn from torch.optim import AdamW from torch.optim.lr_scheduler import ReduceLROnPlateau, CosineAnnealingLR from sklearn.metrics import f1_score def train(data_loader): model = timm.create_model(model_name, pretrained=True, num_classes=7).to(device=device) # 클래스 별로 이미지 수가 다르기 때문에 imbalance 문제를 완화하기 위해 가장 많은 클래스 이미지 수 / 각 클래스 이미지 수로 나눈 값을 가중치로 사용. class_num = [329, 205, 235, 134, 151, 245, 399] class_weight = torch.tensor(np.max(class_num) / class_num).to(device=device, dtype=torch.float) criterion = nn.CrossEntropyLoss(weight=class_weight) # pretrained 모델을 fine-tuning 하므로 feature map을 추출하는 레이어는 learing rate 0.1 비율만 적용. # 일반화 성능을 조금 더 높이기 위해 Adam 대신 AdamW 사용. # 참고자료 : https://hiddenbeginner.github.io/deeplearning/paperreview/2019/12/29/paper_review_AdamW.html feature_extractor = [m for n, m in model.named_parameters() if "fc" not in n] classifier = [p for p in model.fc.parameters()] params = [ {"params": feature_extractor, "lr": learning_rate * 0.5}, {"params": classifier, "lr": learning_rate} ] optimizer = AdamW(params, lr=learning_rate) # ConsineAnnealing Scheduler 적용. scheduler = CosineAnnealingLR(optimizer, T_max=10, eta_min=0) result = { "train_loss": [], "valid_loss": [], "valid_acc": [], "valid_f1": [], } train_loader = data_loader["train_loader"] valid_loader = data_loader["valid_loader"] best_model_state = None best_f1 = 0 early_stop_count = 0 for epoch_idx in range(1, epoch_size + 1): model.train() iter_train_loss = [] iter_valid_loss = [] iter_valid_acc = [] iter_valid_f1 = [] for iter_idx, (train_imgs, train_labels) in enumerate(train_loader, 1): train_imgs, train_labels = train_imgs.to(device=device, dtype=torch.float), train_labels.to(device) optimizer.zero_grad() train_pred = model(train_imgs) train_loss = criterion(train_pred, train_labels) train_loss.backward() optimizer.step() iter_train_loss.append(train_loss.cpu().item()) print( f"[Epoch {epoch_idx}/{epoch_size}] model training iteration {iter_idx}/{len(train_loader)} ", end="\r", ) with torch.no_grad(): for iter_idx, (valid_imgs, valid_labels) in enumerate(valid_loader, 1): model.eval() valid_imgs, valid_labels = valid_imgs.to(device=device, dtype=torch.float), valid_labels.to(device) valid_pred = model(valid_imgs) valid_loss = criterion(valid_pred, valid_labels) iter_valid_loss.append(valid_loss.cpu().item()) valid_pred_c = valid_pred.argmax(dim=-1) iter_valid_acc.extend((valid_pred_c == valid_labels).cpu().tolist()) iter_f1_score = f1_score(y_true=valid_labels.cpu().numpy(), y_pred=valid_pred_c.cpu().numpy(), average="macro") iter_valid_f1.append(iter_f1_score) print( f"[Epoch {epoch_idx}/{epoch_size}] model validation iteration {iter_idx}/{len(valid_loader)} ", end="\r" ) epoch_train_loss = np.mean(iter_train_loss) epoch_valid_loss = np.mean(iter_valid_loss) epoch_valid_acc = np.mean(iter_valid_acc) * 100 epoch_valid_f1_score = np.mean(iter_valid_f1) result["train_loss"].append(epoch_train_loss) result["valid_loss"].append(epoch_valid_loss) result["valid_acc"].append(epoch_valid_acc) result["valid_f1"].append(epoch_valid_f1_score) scheduler.step() print( f"[Epoch {epoch_idx}/{epoch_size}] " f"train loss : {epoch_train_loss:.4f} | " f"valid loss : {epoch_valid_loss:.4f} | valid acc : {epoch_valid_acc:.2f}% | valid f1 score : {epoch_valid_f1_score:.4f}" ) if epoch_valid_f1_score > best_f1: best_f1 = epoch_valid_f1_score best_model_state = model.state_dict() early_stop_count = 0 else: early_stop_count += 1 if early_stop_count == early_stop: print("early stoped." + " " * 30) break return result, best_model_state
Define Dataset Class¶
In [4]:import os import cv2 import albumentations as A from sklearn.model_selection import train_test_split, StratifiedKFold from torch.utils.data import Dataset, DataLoader class_encoder = { 'dog': 0, 'elephant': 1, 'giraffe': 2, 'guitar': 3, 'horse': 4, 'house': 5, 'person': 6 } def img_gather_(img_path): class_list = os.listdir(img_path) file_lists = [] label_lists = [] for class_name in class_list: file_list = os.listdir(os.path.join(img_path, class_name)) file_list = list(map(lambda x: "/".join([img_path] + [class_name] + [x]), file_list)) label_list = [class_encoder[class_name]] * len(file_list) file_lists.extend(file_list) label_lists.extend(label_list) file_lists = np.array(file_lists) label_lists = np.array(label_lists) return file_lists, label_lists class TrainDataset(Dataset): def __init__(self, file_lists, label_lists, transforms=None): self.file_lists = file_lists.copy() self.label_lists = label_lists.copy() self.transforms = transforms def __getitem__(self, idx): img = cv2.imread(self.file_lists[idx], cv2.IMREAD_COLOR) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) if self.transforms: img = self.transforms(image=img)["image"] img = img.transpose(2, 0, 1) label = self.label_lists[idx] img = torch.tensor(img, dtype=torch.float) label = torch.tensor(label, dtype=torch.long) return img, label def __len__(self): assert len(self.file_lists) == len(self.label_lists) return len(self.file_lists) class TestDataset(Dataset): def __init__(self, file_lists, transforms=None): self.file_lists = file_lists.copy() self.transforms = transforms def __getitem__(self, idx): img = cv2.imread(self.file_lists[idx], cv2.IMREAD_COLOR) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) if self.transforms: img = self.transforms(image=img)["image"] img = img.transpose(2, 0, 1) img = torch.tensor(img, dtype=torch.float) return img def __len__(self): return len(self.file_lists)
In [5]:train_transforms = A.Compose([ A.Rotate(), A.HorizontalFlip(), # Vertical Flip은 대부분의 class가 위 아래가 있는 형태를 가지고 있기 때문에 적용 X. A.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2, hue=0.2), A.Normalize() ]) valid_transforms = A.Compose([ A.Normalize() ])
In [6]:data_lists, data_labels = img_gather_("./data/train") best_models = [] # 먼저 베이스라인 코드로 k-fold를 사용하지 않고 모델 성능이나 최적의 learning rate를 찾은 뒤, # 어느정도 파라미터가 잡혔을 때 k-fold를 사용해 최종 최적의 파라미터를 찾는 방식으로 구현. if k_fold_num == -1: train_lists, valid_lists, train_labels, valid_labels = train_test_split(data_lists, data_labels, train_size=0.8, shuffle=True, random_state=random_seed, stratify=data_labels) train_dataset = TrainDataset(file_lists=train_lists, label_lists=train_labels, transforms=train_transforms) valid_dataset = TrainDataset(file_lists=valid_lists, label_lists=valid_labels, transforms=valid_transforms) train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True) valid_loader = DataLoader(valid_dataset, batch_size=batch_size, shuffle=True) data_loader = {"train_loader": train_loader, "valid_loader": valid_loader} print("No fold training starts ... ") train_result, best_model = train(data_loader) best_models.append(best_model) else: skf = StratifiedKFold(n_splits=k_fold_num, random_state=random_seed, shuffle=True) print(f"{k_fold_num} fold training starts ... ") for fold_idx, (train_idx, valid_idx) in enumerate(skf.split(data_lists, data_labels), 1): print(f"- {fold_idx} fold -") train_lists, train_labels = data_lists[train_idx], data_labels[train_idx] valid_lists, valid_labels = data_lists[valid_idx], data_labels[valid_idx] train_dataset = TrainDataset(file_lists=train_lists, label_lists=train_labels, transforms=train_transforms) valid_dataset = TrainDataset(file_lists=valid_lists, label_lists=valid_labels, transforms=valid_transforms) train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True) valid_loader = DataLoader(valid_dataset, batch_size=batch_size, shuffle=True) data_loader = {"train_loader": train_loader, "valid_loader": valid_loader} train_result, best_model = train(data_loader) best_models.append(best_model)
5 fold training starts ... - 1 fold - [Epoch 1/30] train loss : 0.7207 | valid loss : 0.0957 | valid acc : 95.88% | valid f1 score : 0.9602 [Epoch 2/30] train loss : 0.1556 | valid loss : 0.1375 | valid acc : 97.94% | valid f1 score : 0.9170 [Epoch 3/30] train loss : 0.1204 | valid loss : 0.0720 | valid acc : 96.76% | valid f1 score : 0.9708 [Epoch 4/30] train loss : 0.0935 | valid loss : 0.1224 | valid acc : 96.18% | valid f1 score : 0.9049 [Epoch 5/30] train loss : 0.0687 | valid loss : 0.0692 | valid acc : 96.18% | valid f1 score : 0.9523 [Epoch 6/30] train loss : 0.0522 | valid loss : 0.0594 | valid acc : 97.65% | valid f1 score : 0.9790 [Epoch 7/30] train loss : 0.0269 | valid loss : 0.0484 | valid acc : 97.94% | valid f1 score : 0.9773 [Epoch 8/30] train loss : 0.0369 | valid loss : 0.0442 | valid acc : 97.35% | valid f1 score : 0.9666 [Epoch 9/30] train loss : 0.0283 | valid loss : 0.0753 | valid acc : 97.94% | valid f1 score : 0.9350 [Epoch 10/30] train loss : 0.0304 | valid loss : 0.0832 | valid acc : 98.24% | valid f1 score : 0.9861 [Epoch 11/30] train loss : 0.0221 | valid loss : 0.0497 | valid acc : 98.24% | valid f1 score : 0.9848 [Epoch 12/30] train loss : 0.0306 | valid loss : 0.0645 | valid acc : 98.24% | valid f1 score : 0.9474 [Epoch 13/30] train loss : 0.0357 | valid loss : 0.0487 | valid acc : 97.65% | valid f1 score : 0.9750 [Epoch 14/30] train loss : 0.0324 | valid loss : 0.0481 | valid acc : 97.65% | valid f1 score : 0.9759 [Epoch 15/30] train loss : 0.0215 | valid loss : 0.0486 | valid acc : 97.35% | valid f1 score : 0.9736 early stoped. - 2 fold - [Epoch 1/30] train loss : 0.7360 | valid loss : 0.1071 | valid acc : 97.35% | valid f1 score : 0.9624 [Epoch 2/30] train loss : 0.1601 | valid loss : 0.1234 | valid acc : 96.47% | valid f1 score : 0.9624 [Epoch 3/30] train loss : 0.1315 | valid loss : 0.1133 | valid acc : 97.94% | valid f1 score : 0.9138 [Epoch 4/30] train loss : 0.0921 | valid loss : 0.0681 | valid acc : 97.65% | valid f1 score : 0.9806 [Epoch 5/30] train loss : 0.0873 | valid loss : 0.0747 | valid acc : 97.35% | valid f1 score : 0.9767 [Epoch 6/30] train loss : 0.0568 | valid loss : 0.0678 | valid acc : 97.94% | valid f1 score : 0.9773 [Epoch 7/30] train loss : 0.0381 | valid loss : 0.0480 | valid acc : 98.53% | valid f1 score : 0.9824 [Epoch 8/30] train loss : 0.0304 | valid loss : 0.0469 | valid acc : 98.53% | valid f1 score : 0.9866 [Epoch 9/30] train loss : 0.0208 | valid loss : 0.0454 | valid acc : 98.53% | valid f1 score : 0.9889 [Epoch 10/30] train loss : 0.0174 | valid loss : 0.0432 | valid acc : 98.82% | valid f1 score : 0.9889 [Epoch 11/30] train loss : 0.0321 | valid loss : 0.0465 | valid acc : 98.53% | valid f1 score : 0.9840 [Epoch 12/30] train loss : 0.0277 | valid loss : 0.0425 | valid acc : 98.53% | valid f1 score : 0.9880 [Epoch 13/30] train loss : 0.0231 | valid loss : 0.0479 | valid acc : 98.53% | valid f1 score : 0.9827 [Epoch 14/30] train loss : 0.0223 | valid loss : 0.0738 | valid acc : 98.53% | valid f1 score : 0.9873 early stoped. - 3 fold - [Epoch 1/30] train loss : 0.7205 | valid loss : 0.0804 | valid acc : 96.76% | valid f1 score : 0.9633 [Epoch 2/30] train loss : 0.1752 | valid loss : 0.0577 | valid acc : 97.65% | valid f1 score : 0.9740 [Epoch 3/30] train loss : 0.1090 | valid loss : 0.0634 | valid acc : 96.76% | valid f1 score : 0.9720 [Epoch 4/30] train loss : 0.0881 | valid loss : 0.0464 | valid acc : 98.24% | valid f1 score : 0.9799 [Epoch 5/30] train loss : 0.0562 | valid loss : 0.0468 | valid acc : 98.53% | valid f1 score : 0.9878 [Epoch 6/30] train loss : 0.0427 | valid loss : 0.0435 | valid acc : 98.53% | valid f1 score : 0.9875 [Epoch 7/30] train loss : 0.0364 | valid loss : 0.0362 | valid acc : 97.94% | valid f1 score : 0.9820 [Epoch 8/30] train loss : 0.0312 | valid loss : 0.0464 | valid acc : 98.24% | valid f1 score : 0.9837 [Epoch 9/30] train loss : 0.0315 | valid loss : 0.0424 | valid acc : 98.82% | valid f1 score : 0.9911 [Epoch 10/30] train loss : 0.0266 | valid loss : 0.0347 | valid acc : 98.82% | valid f1 score : 0.9906 [Epoch 11/30] train loss : 0.0235 | valid loss : 0.0365 | valid acc : 98.82% | valid f1 score : 0.9747 [Epoch 12/30] train loss : 0.0255 | valid loss : 0.0386 | valid acc : 98.82% | valid f1 score : 0.9892 [Epoch 13/30] train loss : 0.0149 | valid loss : 0.0351 | valid acc : 99.12% | valid f1 score : 0.9948 [Epoch 14/30] train loss : 0.0294 | valid loss : 0.0345 | valid acc : 98.82% | valid f1 score : 0.9871 [Epoch 15/30] train loss : 0.0220 | valid loss : 0.0456 | valid acc : 98.53% | valid f1 score : 0.9852 [Epoch 16/30] train loss : 0.0219 | valid loss : 0.0495 | valid acc : 97.35% | valid f1 score : 0.9793 [Epoch 17/30] train loss : 0.0287 | valid loss : 0.0473 | valid acc : 97.94% | valid f1 score : 0.9848 [Epoch 18/30] train loss : 0.0283 | valid loss : 0.0927 | valid acc : 98.53% | valid f1 score : 0.9295 early stoped. - 4 fold - [Epoch 1/30] train loss : 0.7141 | valid loss : 0.0536 | valid acc : 98.53% | valid f1 score : 0.9872 [Epoch 2/30] train loss : 0.1478 | valid loss : 0.0664 | valid acc : 98.23% | valid f1 score : 0.9268 [Epoch 3/30] train loss : 0.1004 | valid loss : 0.0301 | valid acc : 99.12% | valid f1 score : 0.9926 [Epoch 4/30] train loss : 0.1004 | valid loss : 0.0303 | valid acc : 98.82% | valid f1 score : 0.9900 [Epoch 5/30] train loss : 0.0670 | valid loss : 0.0411 | valid acc : 98.53% | valid f1 score : 0.9890 [Epoch 6/30] train loss : 0.0483 | valid loss : 0.0312 | valid acc : 98.53% | valid f1 score : 0.9891 [Epoch 7/30] train loss : 0.0482 | valid loss : 0.0308 | valid acc : 99.12% | valid f1 score : 0.9946 [Epoch 8/30] train loss : 0.0305 | valid loss : 0.0496 | valid acc : 98.82% | valid f1 score : 0.9886 [Epoch 9/30] train loss : 0.0376 | valid loss : 0.0257 | valid acc : 99.12% | valid f1 score : 0.9925 [Epoch 10/30] train loss : 0.0215 | valid loss : 0.0267 | valid acc : 99.12% | valid f1 score : 0.9928 [Epoch 11/30] train loss : 0.0273 | valid loss : 0.0271 | valid acc : 98.53% | valid f1 score : 0.9839 [Epoch 12/30] train loss : 0.0217 | valid loss : 0.0272 | valid acc : 99.12% | valid f1 score : 0.9928 early stoped. - 5 fold - [Epoch 1/30] train loss : 0.7172 | valid loss : 0.1716 | valid acc : 95.58% | valid f1 score : 0.9426 [Epoch 2/30] train loss : 0.1514 | valid loss : 0.1462 | valid acc : 95.58% | valid f1 score : 0.9521 [Epoch 3/30] train loss : 0.1178 | valid loss : 0.0809 | valid acc : 97.05% | valid f1 score : 0.9677 [Epoch 4/30] train loss : 0.0833 | valid loss : 0.0903 | valid acc : 96.46% | valid f1 score : 0.9601 [Epoch 5/30] train loss : 0.0797 | valid loss : 0.1030 | valid acc : 97.05% | valid f1 score : 0.9592 [Epoch 6/30] train loss : 0.0769 | valid loss : 0.1118 | valid acc : 96.76% | valid f1 score : 0.9655 [Epoch 7/30] train loss : 0.0446 | valid loss : 0.1240 | valid acc : 95.87% | valid f1 score : 0.9620 [Epoch 8/30] train loss : 0.0241 | valid loss : 0.0985 | valid acc : 96.17% | valid f1 score : 0.9681 [Epoch 9/30] train loss : 0.0278 | valid loss : 0.0923 | valid acc : 96.76% | valid f1 score : 0.9726 [Epoch 10/30] train loss : 0.0296 | valid loss : 0.0901 | valid acc : 97.05% | valid f1 score : 0.9694 [Epoch 11/30] train loss : 0.0352 | valid loss : 0.0938 | valid acc : 96.76% | valid f1 score : 0.9669 [Epoch 12/30] train loss : 0.0239 | valid loss : 0.0999 | valid acc : 96.46% | valid f1 score : 0.9598 [Epoch 13/30] train loss : 0.0314 | valid loss : 0.0933 | valid acc : 97.35% | valid f1 score : 0.9728 [Epoch 14/30] train loss : 0.0262 | valid loss : 0.0891 | valid acc : 97.35% | valid f1 score : 0.9722 [Epoch 15/30] train loss : 0.0295 | valid loss : 0.0840 | valid acc : 97.35% | valid f1 score : 0.9721 [Epoch 16/30] train loss : 0.0195 | valid loss : 0.0832 | valid acc : 96.46% | valid f1 score : 0.9695 [Epoch 17/30] train loss : 0.0321 | valid loss : 0.1005 | valid acc : 97.35% | valid f1 score : 0.9739 [Epoch 18/30] train loss : 0.0384 | valid loss : 0.1309 | valid acc : 96.76% | valid f1 score : 0.9715 [Epoch 19/30] train loss : 0.0669 | valid loss : 0.1034 | valid acc : 95.87% | valid f1 score : 0.9562 [Epoch 20/30] train loss : 0.0512 | valid loss : 0.1180 | valid acc : 96.17% | valid f1 score : 0.9648 [Epoch 21/30] train loss : 0.0509 | valid loss : 0.1188 | valid acc : 96.46% | valid f1 score : 0.9620 [Epoch 22/30] train loss : 0.0372 | valid loss : 0.1529 | valid acc : 94.99% | valid f1 score : 0.9461 early stoped.
Inference¶
In [11]:test_transforms_ = A.Compose([ A.Normalize() ]) test_files = os.listdir("./data/test/0") test_files = sorted(test_files) test_files = list(map(lambda x: "/".join(["./data/test/0", x]), test_files)) test_dataset = TestDataset(file_lists=test_files, transforms=test_transforms) test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)
In [12]:import pandas as pd answer_logits = [] model = timm.create_model(model_name, pretrained=True, num_classes=7).to(device=device) # fold 별 best model로 prediction logit의 average 값을 argmax하여 class 예측. for fold_idx, best_model in enumerate(best_models, 1): model.load_state_dict(best_model) model.eval() fold_logits = [] with torch.no_grad(): for iter_idx, test_imgs in enumerate(test_loader, 1): test_imgs = test_imgs.to(device) test_pred = model(test_imgs) fold_logits.extend(test_pred.cpu().tolist()) print(f"[{fold_idx} fold] inference iteration {iter_idx}/{len(test_loader)}" + " " * 10, end="\r") answer_logits.append(fold_logits) answer_logits = np.mean(answer_logits, axis=0) answer_value = np.argmax(answer_logits, axis=-1) i = 0 while True: if not os.path.isfile(os.path.join("submissions", f"submission_{i}.csv")): submission_path = os.path.join("submissions", f"submission_{i}.csv") break i += 1 submission = pd.read_csv("test_answer_sample_.csv", index_col=False) submission["answer value"] = answer_value submission["answer value"].to_csv(submission_path) print("\nAll done.")
[5 fold] inference iteration 8/8 All done.
'취업 이야기 > 데브매칭 문제 해설' 카테고리의 다른 글
2021 ML Dev-Matching | 미술 작품 분류하기 : 우수 코드 공개 (0) 2021.07.06 2021 ML Dev-Matching | 미술 작품 분류하기 : 출제자가 추천한 우수 코드 C (0) 2021.07.06 2021 ML Dev-Matching | 미술 작품 분류하기 : 출제자가 추천한 우수 코드 A (0) 2021.07.06 '2021 Dev-Matching: 웹 백엔드 개발자(상반기)' 기출 문제 해설 (0) 2021.04.26 '2021 Dev-Matching: 웹 프론트엔드 개발자(상반기)' 기출 문제 해설 (10) 2021.04.15