문제
앞에서 pytorch의 기본 train에 대해서 알아봤습니다.
이번에는 영상에 관해 알아보겠습니다.
당신은 영상을 통해 불량을 검출하는 임무를 받았습니다. 영상을 촬영하고 정지 영상으로 화면을 캡춰하고 그 이후에는 이미지 분석을 통하여 정상인지 불량인지 검출하는 방법을 사용하게 될것 입니다.
이것은 어디까지나 고전적인 방법입니다. ML을 배운 입장에서는 불량와 정상을 구별해 내는것을 이미지 분류기라고 생각하면 됩니다.
준비해야 하는 것은 많은 불량 사진과 정상 상태의 사진입니다.
정상과 불량 사진을 구하기 힘들어서 고양이와 개를 하나는 정상과 불량으로 가정하고 이 둘을 구분하는 작업을 할 예정입니다.
개 고양이 분류
캐글에서 있던 부분이고 좀 더 자세한 내용은 아래 링크를 참고 하시면 됩니다.
https://www.kaggle.com/competitions/dogs-vs-cats
여기에서 사용된 자료는 아래 링크로부터 받았습니다.
https://storage.googleapis.com/mledu-datasets/cats_and_dogs_filtered.zip
훈련 데이터 이미지 가져오기
zip파일을 다운로드 받고 폴더에 압축을 풉니다. 실행시켜도 폴더가 존재하거나 하면 다시 다운로드 하지 않습니다.
from requests import get from os.path import exists import zipfile def download(url, file_name=None): if not file_name: file_name = url.split('/')[-1] with open(file_name, "wb") as file: response = get(url) file.write(response.content) if __name__ == '__main__': zip_folder = r"cats_and_dogs_filtered" url = "https://storage.googleapis.com/mledu-datasets/cats_and_dogs_filtered.zip" if not exists(zip_folder+".zip"): print("download data") download(url) if not exists(zip_folder): print("unzip") zip_ref = zipfile.ZipFile(zip_folder+".zip", 'r') zip_ref.extractall() zip_ref.close()
배치(batch)와 데이터 로딩
배치란 물건을 놓는것을 말하는건 아니고 일괄 작업의 단위를 말하는 것입니다.
자세한 내용은 아래 링크에서 확인해보시고, 여기에서는 해당 소스를 바탕으로 이미지 부분만 추가 작업 하였습니다.
https://swlock.blogspot.com/2022/07/pytorch-dataloader.html
기존과 달라지는 부분은 jpg파일을 읽어야 하는 부분입니다. 이미 이미지용으로 준비된 dataloader가 존재하긴 하지만 직접 만들어서 사용해봤습니다.
이미지는 아래 폴더에 존재합니다.
cats_and_dogs_filtered/train/cats cats_and_dogs_filtered/train/dogs cats_and_dogs_filtered/validation/cats cats_and_dogs_filtered/validation/dogs
훈련이나 검증이냐에 따라서 base_path를 지정해서 class를 호출해 줄겁니다.
즉 cats_and_dogs_filtered/train , cats_and_dogs_filtered/validation 여기까지는 호출하는 쪽에서 경로를 알주고 cats, dogs라는 경로를 붙여 파일 목록을 구해냅니다.
데이터를 넘겨야 하는 __getitem__ 에서는 cats+dogs 이미지가 포함되어 있으므로 하위순위에서는 cats 이미지를 상위 숫자에서는 dogs 이미지를 넘기도록 합니다. 목표값도 개는 0,1을 고양이는 1,0을 리턴 하도록 구현 하였습니다.
def __getitem__(self, index):
if index >= len(self.cat_fnames): # dog
else: # cat
transform 처리가 있는데, 학습을 위해서 이미지 그대로 학습을 할 수 없습니다. 이미지를 Tensor로 변환하는 작업도 해야하고 또 다른 중요한 이유는 학습 데이터를 늘리기 위해 이미지에 변형을 하는 작업을 합니다.
class CustomDataset(Dataset): def __init__(self, base_path, transform=None, target_transform=None): self.base_dir = base_path self.transform = transform self.target_transform = target_transform # 훈련에 사용되는 고양이/개 이미지 경로 self.cats_dir = os.path.join(self.base_dir, 'cats') self.dogs_dir = os.path.join(self.base_dir, 'dogs') ''' cats_and_dogs_filtered/train/cats cats_and_dogs_filtered/train/dogs cats_and_dogs_filtered/validation/cats cats_and_dogs_filtered/validation/dogs ''' # os.listdir() 경로 내에 있는 파일의 이름을 리스트의 형태로 반환합니다. # [...'cat.102.jpg', 'cat.103.jpg', .... ] self.cat_fnames = os.listdir(self.cats_dir) self.dog_fnames = os.listdir(self.dogs_dir) self.total_len = len(self.cat_fnames) + len(self.dog_fnames) print(f"total images:{self.total_len}") def __getitem__(self, index): """ 주어진 인덱스 index 에 해당하는 샘플을 데이터셋에서 불러오고 반환합니다. cat[xxx] cat[xxx] dog[xxx] dog[xxx] """ if index >= len(self.cat_fnames): # dog x_data = self.dog_fnames[index - len(self.cat_fnames)] y_data = np.array([0.0, 1.0]) img_path = os.path.join(self.dogs_dir, x_data) image = Image.open(img_path) # plt.imshow(image) # print(y_data) # plt.show() else: # cat x_data = self.cat_fnames[index] y_data = np.array([1.0, 0.0]) img_path = os.path.join(self.cats_dir, x_data) image = Image.open(img_path) # plt.imshow(image) # print(y_data) # plt.show() if self.transform: image = self.transform(image) # plt.imshow(image) # plt.show() if self.target_transform: y_data = self.target_transform(y_data) return image, y_data def __len__(self): """ 데이터셋의 샘플 개수를 반환합니다. """ return self.total_len
이미지 변형
이미지는 PIL.Image 패키지를 사용하여 로딩을 하고 from torchvision import transforms 에 의해서 transforms.Compose() 함수에 list 형태로 넘겨 주면 순서대로 처리를 해줍니다.
여기 예제에서는 다음과 같은 코드입니다.
transforms.Compose([
transforms.RandomVerticalFlip(),
transforms.RandomHorizontalFlip(),
transforms.RandomResizedCrop(150),
transforms.ToTensor(),
transforms.Normalize((0.485, 0.456, 0.406), (0.229, 0.224, 0.225))
사실 여기까지만 해도 되긴하는데...
검증시에는 변형없는 그대로 사용을 원하기 때문에 검증용을 위해서 랜덤 요소가 빠진 transform도 준비해 두었습니다.
transforms.Compose([
transforms.Resize(150),
transforms.CenterCrop(150),
transforms.ToTensor(),
transforms.Normalize((0.485, 0.456, 0.406), (0.229, 0.224, 0.225))
]
그리고 이것을 한번에 묶어 두었습니다.
data_transforms = {
'train': transforms.Compose([
transforms.RandomVerticalFlip(),
transforms.RandomHorizontalFlip(),
transforms.RandomResizedCrop(150),
transforms.ToTensor(),
transforms.Normalize((0.485, 0.456, 0.406), (0.229, 0.224, 0.225))
]),
'validation': transforms.Compose([
transforms.Resize(150),
transforms.CenterCrop(150),
transforms.ToTensor(),
transforms.Normalize((0.485, 0.456, 0.406), (0.229, 0.224, 0.225))
]),
}
실제 변형에서 어떻게 보이는지 아래 링크에서 좀 더 자세한 정보를 확인해 보시기 바랍니다.
이미지 모델
이미지 모델에서 빠질 수 없는 것은 컨벌루션(convolution) 연산입니다. 간단하게 ML이 없다고 생각해보면 2차원에서 영상을 처리할때 어떤 물체의 외곽선을 판단해서 어떤 도형인지 물건인지 판단하게 될것 입니다. 다시 1차원으로 돌아와서 생각해보면, edge(갑자기 값이 급격히 변화되는 구간)를 판별하려면 어떻게 하면 될까요?
아마 특별히 생각해 본적이 없을겁니다. 우리가 수학시간에 배웠던 미분이 필요합니다. 미분을 배울때 어떤 함수의 기울기라고 배웠습니다. 즉 기울기가 0이라는 것은 변화가 없다는 뜻이 되고 함수에서 값이 급격히 증가하면 기울기 값이 커지게 됩니다. 이 커지는 구간이 물체에서는 edge가 되는 구간입니다.
https://ko.wikipedia.org/wiki/%ED%8E%B8%EB%AF%B8%EB%B6%84
다시 2차원으로 올라와서 영상을 분석하는데 도형의 특징 추출이 필요 -> edge 검출 필요 -> 미분이 연산이 필요하게 됩니다. 2차원에서 이러한 복잡한 처리를 할 수 있는 것을 컨벌루션(convolution) 연산이며, 아래 링크를 보시면 convolution matrix 를 어떻게 하느냐에 따라 이미지가 어떻게 되는지 예제가 있습니다.
https://en.wikipedia.org/wiki/Kernel_(image_processing)
pytorch에서 친절하게도 nn.Conv2d() 함수를 준비해 두었습니다.
in_channels=3은 입력영상이 RGB 3개 영역이 있어서 Tensor로 변환하면 3개의 채널을 가지게 됩니다. 입력 이미지크기는 transform 에서 resize나 crop정보를 150으로 설정해뒀기 때문에 150*150 크기의 이미지가 들어온다고 생각하면 됩니다.
좀 더 많은 정보는 여기 링크를 통해서 알아 보시기 바랍니다.
https://pytorch.org/docs/stable/nn.html
여기에서는 여러 시행 착오를 통해서 모델을 만들었습니다. 여러개의 layer를 겹친다고 해서 성능이 좋아지는 것도 아니고 적절한 model을 만드는 것이 중요합니다. 여기에 정답은 없습니다. 이것 저것 해보면서 성능을 개선해 나가야 합니다.
훈련을 하면서 lr(learing rate) 값도 조절해보고 입력 값도 변경해보면서 이것 저것 조절해봐야 합니다. 참고로 해당 모델을 변경하고 이것저것 바꾸는데 약 일주일 정도 소모 되었습니다.
class CustomModel(nn.Module): def __init__(self): super().__init__() self.main = nn.Sequential( nn.Conv2d(in_channels=3, out_channels=16, kernel_size=3), nn.ReLU(), nn.MaxPool2d(8, 8), nn.Flatten(), nn.Linear(5184, 512), nn.ReLU(), nn.Linear(512, 2), nn.ReLU(), ) def forward(self, x): return self.main(x) def make_train_step(model, loss_fn, optimizer): # Builds function that performs a step in the train loop def train_step(x, y): # Sets model to TRAIN mode model.train() # Makes predictions yhat = model(x) # Computes loss # print("yhat",yhat) # print("y", y) loss = loss_fn(yhat, y) # zeroes gradients optimizer.zero_grad() # Computes gradients loss.backward() # Updates parameters optimizer.step() # Returns the loss # print("loss",loss) return loss.item() # Returns the function that will be called inside the train loop return train_step
LOSS함수와 optim
Loss함수도 많긴 하지만 클래스 분류에는 일반적으로 BCELoss 나 CrossEntropyLoss 함수를 많이 사용하는데 꼭 해당 함수를 사용해야 하는것은 아닙니다. 여기에서는 이전 기초에서 했던것도 있어서 단순하게 출력단을 2가지 0,1 을 목표값으로 설정해서 진행했습니다.
"""
https://pytorch.org/docs/stable/nn.html#loss-functions
"""
loss_fn = nn.L1Loss()
"""
https://pytorch.org/docs/stable/optim.html
"""
optimizer = optim.SGD(model.parameters(), lr=lr)
전체소스
import torch import torch.optim as optim import torch.nn as nn import numpy as np from torch.utils.data import Dataset from torch.utils.data import DataLoader import matplotlib.pyplot as plt import os from torchvision import transforms from PIL import Image batch_size = 20 lr = 1e-3 n_epochs = 200 device = 'cuda' if torch.cuda.is_available() else 'cpu' # 학습 과정에 문제가 발생하는 경우 중지시킨다 torch.autograd.set_detect_anomaly(True) # fig = plt.gcf() class CustomDataset(Dataset): def __init__(self, base_path, transform=None, target_transform=None): self.base_dir = base_path self.transform = transform self.target_transform = target_transform # 훈련에 사용되는 고양이/개 이미지 경로 self.cats_dir = os.path.join(self.base_dir, 'cats') self.dogs_dir = os.path.join(self.base_dir, 'dogs') ''' cats_and_dogs_filtered/train/cats cats_and_dogs_filtered/train/dogs cats_and_dogs_filtered/validation/cats cats_and_dogs_filtered/validation/dogs ''' # os.listdir() 경로 내에 있는 파일의 이름을 리스트의 형태로 반환합니다. # [...'cat.102.jpg', 'cat.103.jpg', .... ] self.cat_fnames = os.listdir(self.cats_dir) self.dog_fnames = os.listdir(self.dogs_dir) self.total_len = len(self.cat_fnames) + len(self.dog_fnames) print(f"total images:{self.total_len}") def __getitem__(self, index): """ 주어진 인덱스 index 에 해당하는 샘플을 데이터셋에서 불러오고 반환합니다. cat[xxx] cat[xxx] dog[xxx] dog[xxx] """ if index >= len(self.cat_fnames): # dog x_data = self.dog_fnames[index - len(self.cat_fnames)] y_data = np.array([0.0, 1.0]) img_path = os.path.join(self.dogs_dir, x_data) image = Image.open(img_path) # plt.imshow(image) # print(y_data) # plt.show() else: # cat x_data = self.cat_fnames[index] y_data = np.array([1.0, 0.0]) img_path = os.path.join(self.cats_dir, x_data) image = Image.open(img_path) # plt.imshow(image) # print(y_data) # plt.show() if self.transform: image = self.transform(image) # plt.imshow(image) # plt.show() if self.target_transform: y_data = self.target_transform(y_data) return image, y_data def __len__(self): """ 데이터셋의 샘플 개수를 반환합니다. """ return self.total_len # https://pytorch.org/vision/stable/auto_examples/plot_transforms.html#sphx-glr-auto-examples-plot-transforms-py data_transforms = { 'train': transforms.Compose([ transforms.RandomVerticalFlip(), transforms.RandomHorizontalFlip(), transforms.RandomResizedCrop(150), transforms.ToTensor(), transforms.Normalize((0.485, 0.456, 0.406), (0.229, 0.224, 0.225)) ]), 'validation': transforms.Compose([ transforms.Resize(150), transforms.CenterCrop(150), transforms.ToTensor(), transforms.Normalize((0.485, 0.456, 0.406), (0.229, 0.224, 0.225)) ]), } train_dataset = CustomDataset("cats_and_dogs_filtered/train", transform=data_transforms['train']) val_dataset = CustomDataset("cats_and_dogs_filtered/validation", transform=data_transforms['validation']) train_loader = DataLoader(dataset=train_dataset, batch_size=batch_size, shuffle=True) val_loader = DataLoader(dataset=val_dataset, batch_size=batch_size, shuffle=True) class CustomModel(nn.Module): def __init__(self): super().__init__() # https://pytorch.org/docs/stable/nn.html self.main = nn.Sequential( nn.Conv2d(in_channels=3, out_channels=16, kernel_size=3), nn.ReLU(), nn.MaxPool2d(8, 8), nn.Flatten(), nn.Linear(5184, 512), nn.ReLU(), nn.Linear(512, 2), nn.ReLU(), ) def forward(self, x): return self.main(x) def make_train_step(model, loss_fn, optimizer): # Builds function that performs a step in the train loop def train_step(x, y): # Sets model to TRAIN mode model.train() # Makes predictions yhat = model(x) # Computes loss # print("yhat",yhat) # print("y", y) loss = loss_fn(yhat, y) # zeroes gradients optimizer.zero_grad() # Computes gradients loss.backward() # Updates parameters optimizer.step() # Returns the loss # print("loss",loss) return loss.item() # Returns the function that will be called inside the train loop return train_step model = CustomModel().to(device) """ https://pytorch.org/docs/stable/nn.html#loss-functions """ loss_fn = nn.L1Loss() """ https://pytorch.org/docs/stable/optim.html """ optimizer = optim.SGD(model.parameters(), lr=lr) print(model.parameters()) print(model) if __name__ == '__main__': train_step = make_train_step(model, loss_fn, optimizer) losses = [] val_losses = [] for epoch in range(n_epochs): epoch_loss = 0.0 epoch_val_loss = 0.0 print(f"epoch ##{epoch}") for x_batch, y_batch in train_loader: # print(x_batch.shape) # print(x_batch,y_batch) x_batch = x_batch.to(device) y_batch = y_batch.to(device) epoch_loss += train_step(x_batch, y_batch) losses.append(epoch_loss) with torch.no_grad(): for x_val, y_val in val_loader: x_val = x_val.to(device) y_val = y_val.to(device) model.eval() yhat = model(x_val) loss_f_ret = loss_fn(y_val, yhat) epoch_val_loss += loss_f_ret.item() val_losses.append(epoch_val_loss) print("yhat", yhat) print("y", y_val) print(f"epoch_loss:{epoch_loss} epoch_val_loss:{epoch_val_loss}") torch.save(model.state_dict(), "model.pt") print(model.state_dict()) fig, ax = plt.subplots() ax.plot(losses) ax.plot(val_losses) plt.savefig("losses.png")
훈련 결과
아래와 같은 형태로 출력됩니다.
각 epoch이 끝날때 마다 test 했던 결과와 목표값을 같이 출력하도록 해봤습니다.
즉 y, yhat이 같아야 하는게 최종 목표입니다.
total images:2000 total images:1000 <generator object Module.parameters at 0x00000170F59F1820> CustomModel( (main): Sequential( (0): Conv2d(3, 16, kernel_size=(3, 3), stride=(1, 1)) (1): ReLU() (2): MaxPool2d(kernel_size=8, stride=8, padding=0, dilation=1, ceil_mode=False) (3): Flatten(start_dim=1, end_dim=-1) (4): Linear(in_features=5184, out_features=512, bias=True) (5): ReLU() (6): Linear(in_features=512, out_features=2, bias=True) (7): ReLU() ) ) epoch ##0 yhat tensor([[0.0000, 0.3994], [0.0000, 0.2350], [0.0000, 0.1967], [0.0000, 0.1090], [0.0000, 0.3204], [0.0000, 0.4554], [0.0000, 0.4258], [0.0000, 0.2223], [0.0000, 0.1527], [0.0000, 0.4054], [0.1221, 0.2172], [0.0264, 0.1598], [0.0000, 0.2727], [0.0000, 0.2253], [0.0000, 0.4849], [0.0403, 0.2851], [0.1353, 0.2561], [0.0314, 0.4062], [0.0000, 0.2969], [0.0000, 0.2825]]) y tensor([[1., 0.], [1., 0.], [1., 0.], [1., 0.], [0., 1.], [1., 0.], [0., 1.], [1., 0.], [1., 0.], [0., 1.], [0., 1.], [1., 0.], [0., 1.], [0., 1.], [0., 1.], [0., 1.], [1., 0.], [0., 1.], [1., 0.], [0., 1.]], dtype=torch.float64) epoch_loss:49.88235554099083 epoch_val_loss:24.440080739697432 epoch ##1 yhat tensor([[0.0805, 0.4619], [0.0408, 0.1896], [0.3308, 0.0345], [0.0628, 0.4683], [0.2434, 0.2937], [0.1710, 0.4539], [0.0000, 0.3918], [0.0000, 0.4909], [0.0000, 0.4868], [0.0622, 0.6183], [0.2874, 0.3312], [0.2024, 0.4399], [0.0000, 0.5700], [0.3397, 0.5381], [0.1793, 0.6647], [0.3155, 0.3843], [0.3735, 0.5708], [0.1659, 0.3201], [0.0661, 0.2593], [0.1464, 0.5680]]) y tensor([[0., 1.], [1., 0.], [0., 1.], [1., 0.], [0., 1.], [1., 0.], [1., 0.], [1., 0.], [0., 1.], [1., 0.], [1., 0.], [0., 1.], [0., 1.], [0., 1.], [1., 0.], [0., 1.], [1., 0.], [0., 1.], [0., 1.], [1., 0.]], dtype=torch.float64) epoch_loss:49.14801982045174 epoch_val_loss:23.88168753646314
...(생략)...
최종까지 실행되고 나면 model.pt 파일이 저장됩니다.
파란색은 훈련 데이터이고 주황색은 검정용 데이터입니다. 그래프를 봤을때 점점 감소하는것으로 봤을때 lr크기가 조금 작아 보이기도 하고 epoch을 늘려도 될것 같기도 한데 이런 부분들은 주관적인 판단을 해서 적절한 훈련 결과를 얻으면 됩니다.Test
import trainer import torch from PIL import Image if __name__ == '__main__': model = trainer.CustomModel() model.load_state_dict(torch.load("model.pt")) print(model.state_dict()) # test test_images = ["cats_and_dogs_filtered/train/cats/cat.1.jpg", "cats_and_dogs_filtered/train/cats/cat.2.jpg", "cats_and_dogs_filtered/train/dogs/dog.1.jpg", "cats_and_dogs_filtered/train/dogs/dog.2.jpg", "cats_and_dogs_filtered/validation/cats/cat.2001.jpg", "cats_and_dogs_filtered/validation/dogs/dog.2001.jpg", "cats_and_dogs_filtered/validation/cats/cat.2004.jpg", "cats_and_dogs_filtered/validation/dogs/dog.2004.jpg", ] model.eval() print(model) for test_image in test_images: print(test_image) x_prd = Image.open(test_image) x_prd = trainer.data_transforms['validation'](x_prd) print(x_prd.shape) x_prd = x_prd.unsqueeze(0) print(x_prd.shape) with torch.no_grad(): x_prd = x_prd.to(trainer.device) y = model(x_prd) print("result:", y) if y[0][0] > y[0][1]: print("cat") else: print("dog")
값이 큰 것을 기준으로 dog인지 cat인지에 따라 출력 해놓았습니다 입력 jpg와 비교를 해보면 두번째 훈련 데이터 cat.2.jpg 입력이 dog로 출력된 오류가 보이고 나머지는 모두 정답임을 알 수 있습니다.
cats_and_dogs_filtered/train/cats/cat.1.jpg torch.Size([3, 150, 150]) torch.Size([1, 3, 150, 150]) result: tensor([[0.6571, 0.1703]]) cat cats_and_dogs_filtered/train/cats/cat.2.jpg torch.Size([3, 150, 150]) torch.Size([1, 3, 150, 150]) result: tensor([[0.2023, 0.3759]]) dog cats_and_dogs_filtered/train/dogs/dog.1.jpg torch.Size([3, 150, 150]) torch.Size([1, 3, 150, 150]) result: tensor([[0.0000, 0.9106]]) dog cats_and_dogs_filtered/train/dogs/dog.2.jpg torch.Size([3, 150, 150]) torch.Size([1, 3, 150, 150]) result: tensor([[0.0000, 0.9987]]) dog cats_and_dogs_filtered/validation/cats/cat.2001.jpg torch.Size([3, 150, 150]) torch.Size([1, 3, 150, 150]) result: tensor([[0.8002, 0.0244]]) cat cats_and_dogs_filtered/validation/dogs/dog.2001.jpg torch.Size([3, 150, 150]) torch.Size([1, 3, 150, 150]) result: tensor([[0.0000, 1.0920]]) dog cats_and_dogs_filtered/validation/cats/cat.2004.jpg torch.Size([3, 150, 150]) torch.Size([1, 3, 150, 150]) result: tensor([[0.4062, 0.2524]]) cat cats_and_dogs_filtered/validation/dogs/dog.2004.jpg torch.Size([3, 150, 150]) torch.Size([1, 3, 150, 150]) result: tensor([[0.0041, 0.6446]]) dog
github 소스 해당 소스는 해당 블로그 내용과 조금 차이가 발생할 수 있습니다.
sourcecode/pytorch/03_image_class at main · donarts/sourcecode · GitHub
Pytorch 링크
pytorch 파일 기반(file based) DataLoader
Machine Learning(머신러닝) 기초, Pytorch train 기본
Machine Learning(머신러닝) 기초, Pytorch 영상 분류, 영상 불량 검출, 개 고양이 분류 (image classification)
댓글 없음:
댓글 쓰기