Bootcamp_zerobase/Pytorch
Pytorch #5 : 전이학습과 Pretrained Model
Loft_mind
2025. 3. 6. 23:30
2025.03.03
Chapter 6. Transfer Learning
- 43. 전이학습 - 개념
- 44. 전이학습 - 모델 불러오기
- 45. 전이학습 - 모델 수정 후 학습
- 46. 전이학습 - 평가
전이학습 개념
개념
전이학습 (Tranfer Learning) : 어떤 목적을 위해 학습된 모델(이미 있는 모델)을 다른 작업에 활용하는 것.
아래의 그림처럼 사물을 분류하는 모델을 학습한 이후, 고양이와 개를 분류하는 작업에 적용한다던가, 자동차나 사물을 분류하는 작업에 적용하는 것을 의미한다.
다만, 이미 완성되고, 학습된 모델을 사용하므로 Weights 역시 학습된 것을 사용한다. 다만 Output이나 Class의 개수에 차이가 있으므로 초기 일부 layer는 Freeze 하여 변경하지 않고 일부 layer만 학습시킨다.
Pretraind 모델 - 전처리
전이학습의 중요한 요소인, 이미 학습되어 있는 "Model"을 이용하는 방법
데이터 전처리가 필요함. 이미지 데이터의 경우 증강/전처리가 필요한 경우 있음.
데이터 전처리 Setting
# 데이터 전처리 Setting
import torch
import torchvision.transforms as transforms
import torch.utils
from torch.utils.data import DataLoader
from torchvision.datasets import ImageFolder
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
# 데이터 증강 = 과적합 방지, 데이터 다양성 추가, 데이터의 클래스별 불균형 해소
data_transforms = {
'train' : transforms.Compose([
transforms.Resize([64,64]),
transforms.RandomHorizontalFlip(), #이미지를 랜덤으로 좌우 반전 (50% 확률)
transforms.RandomVerticalFlip(), #이미지를 랜덤으로 상하 반전 (50% 확률)
transforms.RandomCrop(52), #52X52 크기로 랜덤 잘라내기
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.299, 0.224, 0.225]) #평균과 표준편차 정규화
]),
'val' : transforms.Compose([
transforms.Resize([64,64]),
transforms.RandomCrop(52),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.299, 0.224, 0.225])
# 이 값들은 ImageNet 데이터셋의 평균과 표준편차입니다.
# 즉, ImageNet 데이터셋을 기반으로 학습된 모델(VGG, ResNet 등)을 사용할 경우 일반적으로 사용되는 값.
])
}
앞서 말한 것처럼, 사진데이터의 경우 Class별 균형이 맞지 않거나 한 경우가 있다. 그렇기에 데이터 다양성을 추가하고 Class별 불균형을 해소하기 위해 데이터 증강을 진행한다. 물론 과적합 방지역할도 있다고 한다.
데이터셋 불러오기
import os
data_dir = './data/Sick_plant/splitted'
# 사진파일 저장된 경로. spltted 폴더 안에는 train, val 폴더가 있다.
batch_size = 30
image_datasets = {x : ImageFolder(root=os.path.join(data_dir, x),
transform=data_transforms[x]) for x in ['train', 'val']}
dataloaders = {x : DataLoader(image_datasets[x],
batch_size=batch_size,
shuffle=True,
num_workers=4) for x in ['train', 'val']}
dataset_sizes = {x : len(image_datasets[x]) for x in ['train', 'val']}
class_names = image_datasets['train'].classes
dataset_sizes 나 image_datasets에 무엇이 할당되었는지 확인해 보면...
dataset_sizes
>> {'train': 23989, 'val': 7989}
print(image_datasets['train'].class_to_idx)
>> {'Apple___Apple_scab': 0, 'Apple___Black_rot': 1, 'Apple___Cedar_apple_rust': 2....(생략)
print(image_datasets['train'].samples[:5])
>> [('./data/Sick_plant/splitted\\train\\Apple___Apple_scab\\image (1).JPG', 0), ('./data/Sick_plant/splitted\\train\\Apple___Apple_scab\\image (10).JPG', 0),....
또한, ImageFolder로 불러온 사진데이터에 대해서 확인을 하기 위해선..
import matplotlib.pyplot as plt
from torchvision.transforms import ToPILImage
os.environ['KMP_DUPLICATE_LIB_OK'] = 'True'
# 위 코드는 imshow 진행 시 자꾸 커널이 죽는 현상을 해결하기 위한 코드이다.
# 아직 임시방편인것 같지만 사용중이다. 특히 tensor 또는 pytorch 사용 시
# 커널이 죽는 오류가 난다.
# 첫 번째 샘플 가져오기
img, label = image_datasets['train'][0]
# Tensor → PIL 변환
to_pil = ToPILImage()
img = to_pil(img)
# 이미지 출력
plt.imshow(img)
plt.title(f"Label: {label}, Class: {image_datasets['train'].classes[label]}")
plt.axis("off") # 축 제거
plt.show()
참고로 Pretraind Model 사용 시 많이 사용되는 Model은 아래와 같다.
Pretraind 모델 - 모델 불러오기
이번 시간에는 유명한 resnet50 Model을 이용
USE_CUDA = torch.cuda.is_available()
DEVICE = torch.device("cuda")
from torchvision import models # torch에 있는 여러 모델 불러오기 가능
# ImageNet 데이터셋으로 학습된 가중치(Weight) 불러옴
resnet = models.resnet50(pretrained=True) # True : 학습된 Weight 불러옴 / False : 구조만 불러옴
# 불러온 resNet 모델의 class개수와 우리가 분석하려는 class 개수는 다름
# resNet의 class가 몇개인지 모르겠으나, 우리는 33개
# 따라서, resNet의 마지막 Fully Connected layer의 출력채널이 33으로 변경되어야 함.
num_ftrs = resnet.fc.in_features # 기존모델의 마지막 layer의 채널수
resnet.fc = nn.Linear(num_ftrs, 33)
resnet = resnet.to(DEVICE)
criterion = nn.CrossEntropyLoss()
optimizer_ft = optim.Adam(filter(lambda p: p.requires_grad, resnet.parameters()), lr=0.001)
# lr=0.001 → 학습률(learning rate) 설정
# p.requires_grad == True인 파라미터만 업데이트
# 사전 학습된 모델을 사용할 때 일부 층의 가중치를 고정하는 경우 필요함
from torch.optim import lr_scheduler
# 특정 epoch마다 learning rate을 강조
exp_lr_scheduler = lr_scheduler.StepLR(optimizer_ft, step_size=7, gamma=0.1)
'''
StepLR → 일정한 epoch마다 학습률 감소
step_size=7 → 7 epoch마다 학습률을 감소
gamma=0.1 → 학습률을 0.1배로 줄임
학습률 감소 이유?
처음에는 큰 학습률 → 빠르게 학습
후반부에는 작은 학습률 → 세밀한 조정
'''
torchvision 패키지를 통해서 여러 Model을 불러올 수 있다고 한다. 일단 resnet50을 불러와 다운로드하였다. 시간은 조금 걸린다.
앞서 말한 것처럼, Output이나 Class의 개수가 다르고 또 일부 Layer에서 학습을 진행해야 하니, 아래와 같이 학습할 Layer와 Freeze 할 Layer를 정한다.
ct = 0
for child in resnet.children():
# resnet = 10개 layer 존재
# .children()은 각 layer를 한개씩 받아오는 기능
# 반복문을 통해 5개 layer는 고정 (수정 X)
ct += 1
if ct < 6:
for param in child.parameters():
param.requires_grad = False #학습 미진행
수정 모델 학습
Train 함수를 아래와 같이 설정한다.
import time
import copy
def train_resnet(model, criterion, optimizer, scheduler, num_epochs=25):
best_model_wts = copy.deepcopy(model.state_dict())
# model에 저장되어 있는 가중치(weights)와 편향(bias) 반환
best_acc = 0.0
for epoch in range(num_epochs):
print('----------------------- epoch {} --------------------------' .format(epoch+1))
since = time.time()
for phase in ['train', 'val']:
if phase == 'train':
model.train() # train인 경우 학습모드 선언언
else:
model.eval() # val인 경우 평가모드 선언
running_loss = 0.0
running_corrects = 0
for inputs, labels in dataloaders[phase]: # dataloaders에서 미니배치 하나씩 반복복
inputs = inputs.to(DEVICE)
labels = labels.to(DEVICE)
optimizer.zero_grad() # 기울기(grad) 초기화
# train이 아니면 진행되지 않음음
# val 모드일땐, grad 계산 X
with torch.set_grad_enabled(phase == 'train'):
outputs = model(inputs) #예측 진행
_, preds = torch.max(outputs, 1) # 가장 높은 확률의 Class를 예측값으로 저장
loss = criterion(outputs, labels) # 손실 계산
if phase == 'train':
loss.backward() # train인 경우 loss의 역전파
optimizer.step() # 가중치 업데이트
running_loss += loss.item() * inputs.size(0)
running_corrects += torch.sum(preds == labels.data).item()
if phase == 'train':
scheduler.step() # 학습률 업데이트 (한 epoch 마다)
epoch_loss = running_loss / dataset_sizes[phase] # 평균 손실
epoch_acc = torch.tensor(running_corrects).double()/ dataset_sizes[phase] # 평균 정확도
print('{} Loss : {:.4f} ACC : {:.4f}' .format(phase, epoch_loss, epoch_acc))
# 최고의 모델 저장장
if phase == 'val' and epoch_acc > best_acc:
best_acc = epoch_acc
best_model_wts = copy.deepcopy(model.state_dict())
time_elapsed = time.time() - since
print('Completed in {:.04f}m {:.0f}s'.format(time_elapsed // 60, time_elapsed % 60))
print('Best Val Acc : {:.4f}'.format(best_acc))
model.load_state_dict(best_model_wts) #최적을 모델 가중치 로드
return model
model_resnet50 = train_resnet(resnet, criterion, optimizer_ft, exp_lr_scheduler, num_epochs=25)
출력된 결과는 아래와 같다
----------------------- epoch 1 --------------------------
train Loss : 0.8519 ACC : 0.7460
val Loss : 0.4049 ACC : 0.8617
Completed in 1.0000m 38s
----------------------- epoch 2 --------------------------
train Loss : 0.3732 ACC : 0.8799
val Loss : 0.2492 ACC : 0.9163
Completed in 0.0000m 54s
.
.
.
.
.
----------------------- epoch 23 --------------------------
train Loss : 0.0330 ACC : 0.9882
val Loss : 0.0303 ACC : 0.9889
Completed in 0.0000m 53s
----------------------- epoch 24 --------------------------
train Loss : 0.0323 ACC : 0.9881
val Loss : 0.0318 ACC : 0.9894
Completed in 1.0000m 10s
----------------------- epoch 25 --------------------------
train Loss : 0.0345 ACC : 0.9883
val Loss : 0.0315 ACC : 0.9892
Completed in 0.0000m 56s
Best Val Acc : 0.9902
항상 epoch가 반복될수록, 즉 반복학습을 할수록 성능이 좋아지는 것은 아니란 걸 알 수 있다. 참고로 위 Model에서 가장 좋은 Accuracy가 나온 epoch는 20번째이다.
평가하기
import torch.utils
transform_resNet = transforms.Compose([
transforms.Resize([64, 64]),
transforms.RandomCrop(52),
transforms.ToTensor(),
transforms.Normalize([0.458, 0.456, 0.406], [0.229, 0.224, 0.225])
])
test_resNet = ImageFolder(root='./data/Sick_plant/splitted/test',
transform=transform_resNet)
test_loader_resNet = torch.utils.data.DataLoader(
test_resNet,
batch_size=batch_size,
shuffle=True,
num_workers=4
)
import torch
import evaluate
from torch.utils.data import DataLoader
import torch.nn.functional as F
USE_CUDA = torch.cuda.is_available()
DEVICE = torch.device("cuda" if USE_CUDA else "cpu")
# 모델 불러오기
resnet50 = torch.load('resnet50_gh.pt')
resnet50 = resnet50.to(DEVICE)
resnet50.eval() # 평가모드
# 'accuracy' 메트릭 로드
metric = evaluate.load("accuracy")
# 테스트 데이터셋에 대해 정확도 계산
for inputs, labels in test_loader_resNet:
# 데이터도 GPU로 이동
inputs, labels = inputs.to(DEVICE), labels.to(DEVICE)
outputs = resnet50(inputs)
predictions = torch.argmax(outputs, dim=1)
# 정확도 계산
metric.add_batch(predictions=predictions, references=labels)
# 최종 정확도 계산
test_accuracy = metric.compute()
print(f'ResNet Test Accuracy: {test_accuracy["accuracy"]:.4f}')
>> ResNet Test Accuracy: 0.6519
??? 평가가 박살이 났는데.. 이유는 모르겠다. Train, Val에서 평가 시 Accuracy가 최소 98% 였는데... 나중에 조금 더 알아봐야 할 것 같다.