머신러닝

skin cancer HAM10000

content0474 2024. 10. 29. 09:56

이번에는 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을 썼다.

 


이제 사진을 넣어서 확인해보자..!

melanoma도 nevus로 보는 돌팔이

 

붉은색 hemangioma 줘도 nevus

 

검정색도 아닌 bcc를 줬는데도 nevus

 

sebok도 nevus...

 

이렇게 되는 이유는 데이터셋 중 nevus의 개수가 압도적으로 많기 때문일 것으로 보인다.

대부분의 병변은 몇백장 가장 많아도 1천장 내외인데, nevus 사진은 6천장이 넘는다.

데이터셋의 불균형을 해결하기 위해 crossentropyloss에 가중치를 적용할 수 있다고 하니 그 방법을 시도해봐야겠다.


ResNet을 이용하여 성능 개선을 기대해봤다.

코드는 너무나 간단해서 simpleCNN 모델 정의한 부분 다 지우고 이 네 줄을 쓰면 된다.

가장 마지막 선형층만 변화시킨 것이다.

 

ResNet으로 학습한 결과.. simpleCNN이랑 별 차이가 없는데..?

 

사진을 다시 넣어보자

ㅎ.. 여전히 hemangioma를 nevus라고

 

!!! 그래도 bcc는 제대로 봤다. 색깔이 일단 검지 않아서 그런가?

 

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