이번에는 CNN을 이용해 피부암 분류모델을 만들고, Resnet으로도 학습시켜서 두 모델의 성능을 비교해보겠다.
참고) skin cancer HAM10000
HAM10000: human against macine with 10000 training images
skin cancer 데이터셋은 피부병변사진과 진단명 정보가 있으며, 진단명은
#bkl=benigh keratosis-like lesion
#nv=nevus
#df=dermatofibroma
#mel=melanoma
#vasc=vascular lesion
#bcc=basal cell carcinoma
#akiec=actinic keratoses and intraepithelial carcinoma
다음 7개 중 하나이다.
전체코드
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
from torchvision import datasets, transforms
from torch.utils.data import DataLoader, random_split, Dataset
import pandas as pd
import os
from PIL import Image
images_dir='이미지폴더 주소'
csv_path = 'metadata.csv파일 주소'
transform = transforms.Compose([
transforms.Resize((224, 224)),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
])
class HAM10000Dataset(Dataset):
def __init__(self, images_dir, csv_path, transform=None):
self.images_dir = images_dir
self.transform = transform
# CSV 파일 읽고 이미지 파일과 병변 타입 매칭
metadata = pd.read_csv(csv_path)
self.image_labels = metadata[['image_id', 'dx']]
# 고유한 병변 타입을 정수형 라벨로 매핑
self.label_map = {label: idx for idx, label in enumerate(self.image_labels['dx'].unique())}
self.image_labels['label'] = self.image_labels['dx'].map(self.label_map)
def __len__(self):
return len(self.image_labels)
def __getitem__(self, idx):
img_name = self.image_labels.iloc[idx, 0]
label = self.image_labels.iloc[idx, 2] # 'label' 컬럼
img_path = os.path.join(self.images_dir, f"{img_name}.jpg") # 이미지 경로
image = Image.open(img_path).convert("RGB") # 이미지 로드
# 전처리
if self.transform:
image = self.transform(image)
return image, label
dataset = HAM10000Dataset(images_dir=images_dir, csv_path=csv_path, transform=transform)
train_size = int(0.8 * len(dataset))
test_size = len(dataset) - train_size
trainset, testset = random_split(dataset, [train_size, test_size], generator=torch.Generator().manual_seed(42))
trainloader = DataLoader(trainset, batch_size=32, shuffle=True)
testloader = DataLoader(testset, batch_size=32, shuffle=False)
#모델 정의
class SimpleCNN(nn.Module):
def __init__(self):
super(SimpleCNN, self).__init__()
self.conv1 = nn.Conv2d(3, 32, 3, padding=1)
self.pool = nn.MaxPool2d(2, 2)
self.conv2 = nn.Conv2d(32, 64, 3, padding=1)
self.adaptive_pool = nn.AdaptiveAvgPool2d((16, 16)) # 출력 크기를 16x16으로 축소
self.fc1 = nn.Linear(64 * 16 * 16, 512) # 64채널 * 16x16 크기
self.dropout = nn.Dropout(0.5)
self.fc2 = nn.Linear(512, 7)
def forward(self, x):
x = self.pool(torch.relu(self.conv1(x)))
x = self.pool(torch.relu(self.conv2(x)))
x = self.adaptive_pool(x) # 크기를 16x16으로 줄이기
x = x.view(x.size(0), -1)
x = torch.relu(self.fc1(x))
x = self.dropout(x)
x = self.fc2(x)
return x
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = SimpleCNN().to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001, weight_decay=0.001)
# 모델 학습
for epoch in range(5):
model.train()
running_loss = 0.0
for i, data in enumerate(trainloader, 0):
images, labels = data
images, labels = images.to(device), labels.to(device)
# 기울기 초기화
optimizer.zero_grad()
# 순전파 + 역전파 + 최적화
outputs = model(images)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
# 손실 출력 (매 배치마다 출력)
print(f'[Epoch {epoch + 1}, Batch {i + 1}] loss: {loss.item():.3f}')
print('Finished Training')
#정확도 평가
model.eval()
correct = 0
total = 0
with torch.no_grad():
for data in testloader:
images, labels = data
outputs = model(images)
_, predicted = torch.max(outputs, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
print(f'Accuracy of the network on the 10000 test images: {100 * correct / total:.2f}%')
#사진을 주고 테스트하기
preprocess = transforms.Compose([
transforms.Resize((224, 224)),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
])
# 2. 이미지 불러오기
image_path = "테스트 하고 싶은 이미지 주소"
img = Image.open(image_path)
# 3. 전처리 적용
input_tensor = preprocess(img)
# 4. 배치 차원을 추가 (모델은 [batch_size, channels, height, width] 형식의 입력을 기대함)
input_tensor = input_tensor.unsqueeze(0)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = model.to(device)
input_tensor = input_tensor.to(device)
model.eval()
with torch.no_grad():
output = model(input_tensor)
_, predicted = torch.max(output, 1)
if predicted.item() == 0:
print("benigh keratosis-like lesion")
elif predicted.item() == 1:
print("nevus")
elif predicted.item() == 2:
print("dermatofibroma")
elif predicted.item() == 3:
print("melanoma")
elif predicted.item() == 4:
print("vascular lesion")
elif predicted.item() == 5:
print("basal cell carcinoma")
else:
print("actinic keratoses and intraepithelial carcinoma")
kaggle에서 데이터 다운로드
다운받은 폴더를 열어보면 피부병변사진폴더와 metadata라는 csv파일이 있다.
metadata는 병변사진이름-진단이 써있다.
이 metadata에 따라 병변사진에 진단을 매칭시키는 것이 모델학습을 시작하기 위한 중요한 작업이다.
이렇게 커스텀클래스 방식을 사용하는 경우가 가장 흔하다.
class HAM10000Dataset(Dataset):
HAM10000Dataset은 dataset이므로 torch.utils.data에 포함된 Dataset클래스를 상속하고 있다.
def __init__(self, images_dir, csv_path, transform=None):
생성자 매서드
transform=none인 이유는 transform이 필요한 경우 transform을 하고 필요하지 않으면 건너뛰는 유연성을 주기 위해서이다. 나중에 모델에 전달할 dataset을 정의할 때 tranform 옵션을 변경시킬 수 있다.
metadata = pd.read_csv(csv_path)
self.image_labels = metadata[['image_id', 'dx']]
여기서 두 데이터프레임이 만들어진다.
df1: metadata #csv파일 그대로 만든 데이터프레임
df2: image_labels #df1에서 image_id 와 dx(diagnosis) 컬럼만 가져와서 만든 데이터프레임
self.label_map = {label: idx for idx, label in enumerate(self.image_labels['dx'].unique())}
label_map={label:idx}로 된 딕셔너리
enumerate: 리스트의 값과 인덱스를 튜플로 반환
self.image_labels['dx'].unique()
image_labels의 dx컬럼의 고유값(=unique()) =있는 값 다 들고와서 그 중 중복을 제거함
다 붙여보면 dx컬럼의 고유값을 {값:인덱스}의 딕셔너리로 만드는 코드
결국 label_map은 다음과 같다. {'mel': 0, 'nv': 1, 'bcc': 2, 'akiec': 3, 'bkl': 4, 'df': 5, 'vasc': 6}
self.image_labels['label'] = self.image_labels['dx'].map(self.label_map)
self.image_labels['label'] : image_labels 데이터프레임에 'label'이라는 컬럼을 추가
self.image_labels ['dx'].map(self.label_map)
image_labels의 dx컬럼에 있는 각 값을 순회하면서(=map함수) self.label_map의 규칙에 따라 값을 변환해줌
ex
image_id | dx | label | |
0 | ISIC_4235452 | mel | 0 |
1 | ISIC_4235453 | bcc | 2 |
2 | ISIC_4235454 | nv | 1 |
... | ... | ... | ... |
현재 작업으로 생성된 image_labels는 이처럼 세 개의 열로 되어있다.
라벨을 붙여준 이유는 모델이 학습할때 문자열이 아닌 숫자로 학습하기 때문이다.
mel-> 뭔지 모름 / 0->아하!
def __len__(self):
return len(self.image_labels)
전에도 봤던 데이터셋의 크기를 반환해주는 매서드
init이나 getitem도 마찬가지지만, 우리가 len 매서드를 직접 호출하는 일은 별로 없다.
dataloader가 데이터를 불러오고 모델에 전달할 때 알아서 클래스 내부 함수들을 호출해서 몇 번째 데이터인지 어떤 정보가 있는지 계산하고 알아낸다. 그래서 이게 없으면 오류가 난다.
def __getitem__(self, idx):
img_name = self.image_labels.iloc[idx, 0]
label = self.image_labels.iloc[idx, 2]
idx를 받으면 해당 idx의 파일이름과 라벨을 알려준다.
image_labels.iloc[idx, 0]: image_labels의 idx행의 0번째 열
image_labels.iloc[idx, 2]: image_labels의 idx행의 2번째 열
위의 예시표를 참고하면 HAM10000Dataset[2] == [ ISIC_4235454 , 1]
img_path = os.path.join(self.images_dir, f"{img_name}.jpg")
image = Image.open(img_path).convert("RGB")
이미지를 열어서 모델한테 가져다주는 코드
전에도 말했지만 단순 파일경로만 써두면 문자열이기 때문에 반드시 open으로 이미지를 로드해야 한다.
윗줄의 os.path.join은 파일경로를 하드코딩하지 않고 코드로 제공하고 있다.
if self.transform:
image = self.transform(image)\
return image, label
이 때 transform=True이면 transform한 이미지를 전달하고 있다.
dataset = HAM10000Dataset(images_dir=images_dir, csv_path=csv_path, transform=transform)
train_size = int(0.8 * len(dataset))
test_size = len(dataset) - train_size
trainset, testset = random_split(dataset, [train_size, test_size], generator=torch.Generator().manual_seed(42))
trainloader = DataLoader(trainset, batch_size=32, shuffle=True)
testloader = DataLoader(testset, batch_size=32, shuffle=False)
데이터셋 불러오고 데이터로더로 트레인셋과 테스트셋을 나누고 있다.
이 때 주목할 것은 random_split이다.
random_split(분할할 데이터셋, [사이즈1, 사이즈2], 난수생성기) 의 구조로
데이터셋을 두 가지 크기로 분할하고, 데이터를 임의로 나눌때 사용하는 난수를 생성하는데 seed값을 고정해서 코드를 다시 실행해도 재현성이 유지되도록 하고 있다.
위의 코드에서는 사이즈를 코딩해서 train size가 전체 데이터셋의 80%, 나머지는 test size로 주고 있다.
이 때 내가 모델한테 줄 데이터가 어떻게 생겼는지 알아보고 싶다면?
for images, labels in trainloader:
print(f"Batch of images shape: {images.shape}")
print(f"Batch of labels: {labels}")
break
->출력
Batch of images shape: torch.Size([32, 3, 224, 224])
Batch of labels: tensor([1, 1, 1, 5, 3, 1, 1, 1, 2, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 4, 4, 1, 1, 1,
1, 3, 6, 1, 1, 1, 1, 1])
images.shape 코드로 batch size=32, 채널=3(RGB), 224*224 크기의 텐서구조를 갖는다는 정보를 얻었다.
labels를 프린트하면 32개의 배치 안에 들어있는 라벨들이 나온다.
그런데 trainloader안에 있는 모든 미니배치마다 이걸 다 할 필요는 없으므로 break로 코드를 끝내고 있다.
for label, idx in dataset.label_map.items():
print(f"{label}: {idx}")
->출력
bkl: 0
nv: 1
df: 2
mel: 3
vasc: 4
bcc: 5
akiec: 6
label_map에서 매핑한 결과를 키-값 쌍으로 보고 있다.
기본CNN모델이나 cat vs dog에서 달라진 게 거의 없다.
옵티마이저로 adam을 썼다.
이제 사진을 넣어서 확인해보자..!
이렇게 되는 이유는 데이터셋 중 nevus의 개수가 압도적으로 많기 때문일 것으로 보인다.
대부분의 병변은 몇백장 가장 많아도 1천장 내외인데, nevus 사진은 6천장이 넘는다.
데이터셋의 불균형을 해결하기 위해 crossentropyloss에 가중치를 적용할 수 있다고 하니 그 방법을 시도해봐야겠다.
ResNet을 이용하여 성능 개선을 기대해봤다.
코드는 너무나 간단해서 simpleCNN 모델 정의한 부분 다 지우고 이 네 줄을 쓰면 된다.
가장 마지막 선형층만 변화시킨 것이다.
ResNet으로 학습한 결과.. simpleCNN이랑 별 차이가 없는데..?
사진을 다시 넣어보자
melanoma, sebok도 다 넣어봤지만 여전히 nevus로 보고 있다.
다음번에 가중치 적용하는 방법으로 다시 해보도록 하자
사담
병변 사진을 보고 있으니 잠깐 옛날 생활이 그립다는 생각이 들었다.
언제 돌아갈수있을까
'머신러닝' 카테고리의 다른 글
sentence generator with GPT-2 (0) | 2024.10.31 |
---|---|
skin cancer HAM10000(2) (0) | 2024.10.30 |
wordcloud (2) | 2024.10.28 |
CatDog (3) | 2024.10.25 |
리뷰 분석 (3) | 2024.10.24 |