-
pytorch-2 데이터로더딥러닝/pytorch 2023. 7. 3. 17:06
1. 데이터로더
데이터로더는 말 그대로 데이터를 따로 로드하는 객체이다. 파이토치에서 데이터로더를 따로 사용하는 이유는 데이터양이 방대해질수록 한 번에 불러와서 처리하는 것이 어렵고 해당 데이터를 내가 원하는 크기로 잘라서 사용할 수 있으므로 병렬처리 및 빠른 속도, 유연성 등의 장점이 있기 때문에 데이터로더를 분리하는 것이 이상적이다.
2. 데이터셋의 함수
사용자 정의 데이터셋은 반드시 __init__, __len__, __getitem__ 3개의 함수를 가진다.
__init__은 데이터셋 객체가 생성될 때 한 번만 실행된다. 일반적으로 데이터셋을 로드하고 전처리하는 데 필요한 변수를 설정한다.
__len__은 데이터셋의 샘플 개수를 반환한다.
__getitem__은 데이터를 로드하고 전처리를 실행한다.
3. 커스텀 데이터셋
from typing import Any import torch import os import glob from PIL import Image from torch.utils.data import Dataset, DataLoader from torchvision import transforms def is_grayscale(img): return img.mode == 'L' class CustomImageDataset(Dataset): def __init__(self, image_paths, transform = None): self.image_paths = glob.glob(os.path.join(image_paths, "*", "*", "*.jpg")) self.transform = transform self.label_dict = {"dew": 0, "fogsmog": 1, "frost": 2, "glaze": 3, "hail": 4, "lightning": 5, "rain": 6, "rainbow": 7, "rime": 8, "sandstorm": 9, "snow": 10} def __getitem__(self, index): image_path: str = self.image_paths[index] image = Image.open(image_path).convert("RGB") if not is_grayscale(image): folder_name = image_path.split("\\") folder_name = folder_name[2] label = self.label_dict[folder_name] if self.transform: image = self.transform(image) return image, label else: print(f"{image_path} 파일은 흑백 이미지입니다.") def __len__(self): return len(self.image_paths) if __name__ == "__main__": transform = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor() ]) image_paths = './data/sample_data_01' dataset = CustomImageDataset(image_paths, transform=transform) data_loader = DataLoader(dataset, 32, shuffle=True) for images, labels in data_loader: print(f"Data and label : {images}, {labels}") ................................................. [[0.3333, 0.3529, 0.3961, ..., 0.4706, 0.4588, 0.4549], [0.3373, 0.3569, 0.4000, ..., 0.4784, 0.4667, 0.4627], [0.3451, 0.3647, 0.4078, ..., 0.4863, 0.4706, 0.4627], ..., [0.8510, 0.8549, 0.8392, ..., 0.5843, 0.5882, 0.6235], [0.8824, 0.8902, 0.8706, ..., 0.5725, 0.6039, 0.6235], [0.8863, 0.8902, 0.8784, ..., 0.5843, 0.6000, 0.6196]], [[0.1922, 0.2000, 0.2235, ..., 0.3333, 0.3373, 0.3412], [0.1961, 0.2039, 0.2314, ..., 0.3373, 0.3451, 0.3490], [0.2039, 0.2196, 0.2431, ..., 0.3333, 0.3412, 0.3451], ..., [0.5255, 0.5529, 0.5843, ..., 0.3490, 0.3020, 0.3098], [0.5137, 0.5255, 0.5098, ..., 0.2667, 0.2784, 0.2863], [0.5490, 0.5451, 0.5176, ..., 0.2745, 0.2941, 0.3137]]]]), tensor([1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0])
이미지 데이터들을 불러와서 텐서로 변환하고 라벨을 지정하는 코드이다.
is_grayscale 함수를 만들어서 이미지를 넣었을 때 img.mode의 값이 L인 경우 true를 반환하며 그렇지 않을 경우 false를 반환한다.
CustomImageDataset 클래스를 만들어서 데이터를 처리한다. init함수에서 데이터셋에 사용되는 이미지 경로와 파일을 변형하기 위한 transform, label_dict 함수를 정의한다.
getitem 함수에서는 imag_paths에 들어온 경로를 순차적으로 image_path에 str 타입으로 담는다. 또한 rgb타입으로 이미지들을 변환시켜서 통일하여 처리한다. if문을 사용하여 grayscale이 아니라면 이미지 경로를 분해하고 두 번째 인자를 folder_name 변수에 저장하고 label_dict에서 매칭되는 숫자로 label에 할당한다.
만약 transform이 있다면 transform을 진행하고 grayscale 조건에 부합하지 않는다면 다른 메시지를 출력한다. 마지막으로 len 함수를 사용하여 데이터셋의 길이를 알 수 있게 한다.
파이썬 스크립트를 모듈로서 사용하는 경우에는 __name__에 모듈이름이 설정된다. 하지만 해당 스크립트를 직접 실행했을 때만 __main__으로 이름이 설정되기 때문에 if __name__ =="__main__" 코드를 통해 직접 실행했을 때만 코드가 실행될 수 있도록 한다.
transforms.Compse 함수를 통해 데이터셋을 원하는 대로 변형할 수 있다. 이미지의 resize와 totensor를 진행한다. 이미지 경로에는 이미지의 경로를 넣고 join 함수에서 ,"*" , "*", 별두 개를 사용하였기 때문에 하위 두 개 폴더에서 jpg로 끝나는 모든 파일을 가져온다.
CustomImageDataset 클래스에 이미지 경로를 넣고 transform을 넣어서 실행시킨다. 해당 이미지 경로에서 jpg로 끝나는 모든 파일을 찾아서 rgb 파일로 오픈한 뒤 그레이스케일인지 확인하고 아니라면 경로를 분리할 것이다. 두 번째 인자의 값을 label_dict에 매칭시켜서 숫자로 반환받고 이미지를 transform 하여 이미지와 라벨값을 리턴 받을 것이다.
마지막으로 이 반환받은 값을 DataLoader 함수에 넣어서 자르는 작업을 한다. 32개로 자르며 shuffle 값을 true로 설정하여 무작위로 섞이게 한다. 이외에도 num_workers 인자는 병렬처리를 설정하며 pin_mempry 인자는 cpu와 gpu 간 데이터 전송 속도를 향상하고 drop_last는 마지막 배치를 삭제할 것인지 결정한다.
이 data_loader에서 image와 label값을 for문으로 출력해서 확인해 보면 이미지와 라벨값이 32개씩 잘린 채로 tensor 형태로 출력되는 것을 확인할 수 있다.
4. csv 데이터셋
import torch from torch.utils.data import Dataset, DataLoader class HeightWeightDataset(Dataset): def __init__(self, csv_path): self.data = [] with open(csv_path, 'r', encoding='utf-8') as f: next(f) for line in f: _, height, weight = line.strip().split(",") height = float(height) weight = float(weight) convert_to_kg_data = round(self.convert_to_kg(weight), 2) convert_to_cm_data = round(self.inch_to_cm(height), 1) self.data.append([convert_to_cm_data, convert_to_kg_data]) def __getitem__(self, index): data = torch.tensor(self.data[index], dtype=torch.float) return data def __len__(self): return len(self.data) def convert_to_kg(self, weight_lb): return weight_lb * 0.453592 def inch_to_cm(self, inch): return inch * 2.54 if __name__ == "__main__": dataset = HeightWeightDataset("./data/hw_200.csv") dataloader = DataLoader(dataset, batch_size = 1, shuffle=True) for batch in dataloader: x = batch[:, 0].unsqueeze(1) y = batch[:, 1].unsqueeze(1) print(x, y) tensor([[168.4000]]) tensor([[54.4400]]) tensor([[176.4000]]) tensor([[55.6100]]) tensor([[174.4000]]) tensor([[58.4000]]) tensor([[173.1000]]) tensor([[61.9400]]) tensor([[176.9000]]) tensor([[63.3500]]) tensor([[170.2000]]) tensor([[58.4300]]) tensor([[173.3000]]) tensor([[58.2000]]) tensor([[172.8000]]) tensor([[56.0100]]) tensor([[168.6000]]) tensor([[54.1500]])
키와 몸무게가 차례대로 저장되어 있는 csv 데이터 값을 변환하고 텐서로 변환한다.
읽기 모드로 csv파일을 읽어와서 f에 할당한다. next를 사용하여 첫 행을 건너뛸 수 있으며 f객체에서 인덱스와 높이 무게를 strip(). split(", ")을 사용하여 좌우 공백을 제거한 후 쉼표 구분자로 분할할 수 있다.
이것을 실수로 변환해 주고 밑에서 정의한 convert_to_kg, inch_to_cm 함수를 사용하여 각각 값을 변환하고 data 리스트에 추가한다. getitem에서 해당 데이터를 텐서로 변환한다.
데이터로더에서는 배치사이즈와 셔플을 설정하고 키와 몸무게 값을 dataloader 변수에서 추출하여 unsqueeze(1) 함수로 2차원으로 변경한다. 값을 프린트해 보면 2차원으로 변경된 키와 몸무게의 텐서값을 확인할 수 있다.
5. json 데이터셋
import json from PIL import Image import torch from torch.utils.data import Dataset, DataLoader import os class JsonCustomDataset(Dataset): def __init__(self, json_path, transform=None): self.transform = transform with open(json_path, 'r', encoding='utf-8') as f: self.data = json.load(f) def __getitem__(self, index): img_path = self.data[index]['filename'] img_path = os.path.join("이미지 폴더", img_path) bboxes = self.data[index]['ann']['bboxes'] labels = self.data[index]['ann']['labels'] return img_path, {'boxes': bboxes, 'labels': labels} def __len__(self): return len(self.data) if __name__ == "__main__": dataset = JsonCustomDataset("./data/test.json", transform=None) for item in dataset: print(f"Data of dataset : {item}") Data of dataset : ('이미지 폴더\\image_001.jpg', {'boxes': [[10, 10, 50, 50], [100, 100, 200, 200]], 'labels': [0, 1]}) Data of dataset : ('이미지 폴더\\image_002.jpg', {'boxes': [[20, 20, 60, 60], [300, 300, 400, 400]], 'labels': [1, 2]}) Data of dataset : ('이미지 폴더\\image_003.jpg', {'boxes': [[30, 30, 60, 60], [300, 300, 400, 400]], 'labels': [1, 2]}) Data of dataset : ('이미지 폴더\\image_004.jpg', {'boxes': [[10, 10, 60, 60], [300, 300, 400, 400]], 'labels': [1, 2]})
json 형식으로 저장된 파일을 출력해 본다.
init에서 read 형식으로 with open을 하여 json파일을 f에 저장하고 f값을 data에 할당한다.
getitem에서 json 내부의 filename과 ann의 bboxes, labels 항목을 각각 img_path, bboxes, labels 변수에 할당한다. 이 변수를 따로 데이터로더처리나 transform 처리를 하지 않고 출력하면 json내부 내용을 잘 읽어온 것을 확인할 수 있다.
6. cache 재사용
import torch import os import glob import time from PIL import Image from torch.utils.data import Dataset, DataLoader from torchvision import transforms from custom_dataset_prac import CustomImageDataset def is_grayscale(img): return img.mode == 'L' class CachedCustomImageDataset(Dataset): def __init__(self, image_paths, transform = None): self.image_paths = glob.glob(os.path.join(image_paths, "*", "*", "*.jpg")) self.transform = transform self.label_dict = {"dew": 0, "fogsmog": 1, "frost": 2, "glaze": 3, "hail": 4, "lightning": 5, "rain": 6, "rainbow": 7, "rime": 8, "sandstorm": 9, "snow": 10} self.cache = {} def __getitem__(self, index): if index in self.cache: image, label = self.cache[index] else: image_path: str = self.image_paths[index] image = Image.open(image_path).convert("RGB") if not is_grayscale(image): folder_name = image_path.split("\\") folder_name = folder_name[-2] label = self.label_dict[folder_name] self.cache[index] = (image, label) else: print(f"{image_path} 파일은 흑백 이미지입니다.") return None, None if self.transform: image = self.transform(image) return image, label def __len__(self): return len(self.image_paths) if __name__ == "__main__": tf = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor() ]) image_paths = "./data/sample_data_01/" cached_dataset = CachedCustomImageDataset(image_paths, tf) cached_dataloader = DataLoader(cached_dataset, batch_size = 64, shuffle=True) not_cached_dataset = CustomImageDataset(image_paths, tf) not_cached_dataloader = DataLoader(not_cached_dataset, batch_size = 64, shuffle = True) c_start_time = time.time() for images, labels in cached_dataloader: pass print(f"캐시된 클래스 : {time.time() - c_start_time} 초 소모") nc_start_time = time.time() for images, labels in not_cached_dataloader: pass print(f"캐시되지 않은 클래스 : {time.time() - nc_start_time} 초 소모") c_reuse_start_time = time.time() for images, labels in cached_dataloader: pass print(f"캐시된 클래스 재사용 : {time.time() - c_reuse_start_time} 초 소모") nc_reuse_start_time = time.time() for images, labels in not_cached_dataloader: pass print(f"캐시되지 않은 클래스 재사용 : {time.time() - nc_reuse_start_time} 초 소모") 캐시된 클래스 : 2.5715527534484863 초 소모 캐시되지 않은 클래스 : 2.359811544418335 초 소모 캐시된 클래스 재사용 : 1.0229148864746094 초 소모 캐시되지 않은 클래스 재사용 : 2.3245720863342285 초 소모
캐시 된 데이터를 사용해서 데이터 로드 속도를 개선할 수 있다.
가장 처음에 사용한 커스텀데이터셋을 이용하여 데이터를 캐싱한다. 캐시는 데이터를 미리 저장하는 것을 의미하며 데이터를 캐싱하여 사용하면 똑같은 데이터를 재사용하는 경우에 빠른 속도를 가져올 수 있다.
init 함수에 cache 변수를 만들고 클래스 실행 시 반환하는 이미지, 라벨 값을 cache 변수에 저장한다.
커스텀 데이터셋 모듈을 임포트 하여 위의 클래스와 같이 실행하여 비교한다. time.time 함수로 시간을 측정하여 확인하면 캐시 된 클래스는 재사용 시 속도가 확연히 빨라졌으며 캐시 하지 않은 클래스는 거의 동일한 속도인 것을 알 수 있다.
7. gpu utill
이렇게 데이터의 로딩 속도를 높이는 이유는 gpu util을 높이기 위함이다. gpu util은 gpu의 이용률을 의미하며 gpu util을 높여서 모델을 최대한 학습시키는 것이 중요하다. gpu util에 가장 큰 영향을 주는 것이 데이터를 로드하고 적용하는 과정이기 때문에 데이터 로더의 설계가 효율적이어야 한다.
ai 모델이 거대해지면 거대해질수록 학습시간은 방대해지며 gpu util이 100프로일 때 학습 시간이 한 달이라면 50프로일 때는 두 달이 소비되는 것이다. 따라서 이것은 더 좋은 성능의 모델을 생성하는 데에는 직접적인 연관이 없지만 gpu util이 높을수록 모델을 학습할 수 있는 시간이 확보되고 반복 횟수가 증가할수록 더 좋은 성능의 모델을 생성할 가능성이 높아진다.
디스크에서 데이터를 메모리로 올리고 cpu가 데이터를 전처리하면 gpu에서 학습을 하는 과정을 거치며 느린 작업은 데이터를 올리는 작업이다. 따라서 데이터를 모두 올려놓지 않는다면 cpu에서는 남은 데이터를 전처리하는 작업을 해야 하기 때문에 gpu util이 낮아진다.
8. gpu util을 높이는 방법
1. multi process data loading(데이터 병렬 로딩)
gpu의 util을 높이기 위해서 gpu util의 성능을 떨어뜨리는 작업인 데이터 전처리 작업을 미리 하는 것이다. 여러 개의 cpu에서 데이터를 batch 사이즈로 잘라놓은 다음 한 개의 배치가 끝날 때마다 순차적으로 batch를 하나씩 던져주어 딜레이를 없애는 방법이다. 이것을 담당하는 것이 num_worker 인자이며 어차피 cpu 코어개수는 물리적으로 한정되어 있고 학습환경이 조건에 따라 다르기 때문에 적절한 개수를 설정해야 한다.
2. 크기가 작은 데이터타입 사용
파이토치는 float32 타입을 가지고 있다. float32는 uint8보다 4배의 크기를 가지고 있으며 데이터를 전송할 때 4가 걸린다. 따라서 이미지의 경우 uint8로 가지고 있다가 모델에 넣어주기 직전에 float32로 변환하여 실행하는 것이 더 빠르다.
3. 청크 히트
hdf5의 청크 레이아웃을 사용하면 데이터를 작은 블록으로 분할하여 저장하여 효율성을 높일 수 있다. 각 청크는 독립적으로 관리되면 필요한 경우에만 로드되므로 대용량 데이터셋에서 효율적인 데이터 처리가 가능하다.
4. batch echoing
배치 정규화는 한 배치를 여러 번 사용하는 것이다. 이 방법은 학습의 속도를 증가시킬 수 있지만 무작위성이 감소한다.
5. 그 외
그 외에도 데이터 사전처리, 데이터 캐싱, 레이지 로딩, 데이터 압축 등의 방법을 사용할 수 있다.
'딥러닝 > pytorch' 카테고리의 다른 글
pytorch-5 CNN (0) 2023.07.09 pytorch-4 stride conv, dilated conv, 가중치 행렬 시각화 (0) 2023.07.09 pytorch-3 ANN, RBM (0) 2023.07.09 pytorch-1 기본 문법 (0) 2023.06.26