개발자로 후회없는 삶 살기
Pytorch PART.데이터 로더, 폴더 활용(Collate, Sampler 등) 본문
서론
모델 학습에서 가장 중요하다고 할 수 있는 데이터, 그러한 데이터를 불러오고 배치 사이즈만큼 feeding 할 수 있는 데이터셋과 데이터 로더를 깊게 다뤄봅니다.
본론
- 데이터 셋과 데이터 로더를 극한으로 공부해보자
1) 데이터 초기화하여(대입) x, y 정의하기
2) 데이터를 읽기
데이터 셋의 역할은 다음과 같습니다.
1. __init__
데이터의 타입(자연어, 이미지)에 따라 다르지만 보통 이미지의 경우 학습할 데이터의 경로를 적습니다. 또한 증강 정의, 컬럼 삭제 등 초기화 작업을 수행합니다.
이처럼 정형 데이터라면 데이터를 읽어서 x 변수 y 변수를 초기화할 수 있고
이미지 데이터라면 데이터를 읽어서 증강을 선언하는 것이 일반적입니다.
2) __len__
이것은 단순하게 총 길이를 나타내면 끝입니다. 최대 요소 수를 정의해 두면 데이터 로더가 길이에 맞게 읽고 로딩을 종료합니다.
3) __getitem__
정형 데이터라면 loc으로 접근해서 데이터를 가져올 수 있을 것이고, 이미지라면 데이터를 idx 파라미터로 접근해서 __init__에서 정의한 증강을 적용하고 반환하는 것이 일반적입니다. 한가지 주의할 점은 getitem의 반환 값으로는 반드시 tensor나 numpy여야 합니다.
- 심화로 알아보기
지금까지는 이미지를 텐서로 바로 활용할 수 있고 라벨은 처음부터 숫자로 제공되는 상황에서 모델 학습을 돌렸었습니다. 하지만 실세계에서는 직접 텐서화와 라벨링을 해야합니다. 이를 심화로 다양한 상황에 적용할 수 있도록 코드로 작성해 보겠습니다. 하지만 어렵지는 않습니다. 우리는 데이터 셋에 넣고 x, y 형태로만 바꿔주면 되기 때문입니다.
예를들어서 mnist 데이터를 보면 jpg 이미지를 읽어서 0~255 넘파이 형태로 바꿔주고 라벨을 읽어서 숫자로 바꿔준 상태에서
초기화하여 __init__에서 가지고 있습니다. 우리는 모든 데이터를 이 포멧으로 맞춰주기만 하면 됩니다.
1. 이미지와 라벨만 있는 경우
이 경우는 이미지만 jpg 형태로 있는 것이라고 볼 수 있습니다. 대신 이미지 명에 라벨이(mask, normal) 적혀있습니다. 이러한 데이터는 이미지와 라벨을 순서대로 나열하고 이미지는 텐서 형테로 바꾸고 라벨은 숫자로 바꿔줘야 합니다. 그 후 데이터 셋에서 idx로 접근하면 될 것입니다.
1) __init__
class ImageDataset(BaseDataset):
def __init__(self, data_dir):
# 이미지 데이터 초기화
self.file_path = glob.glob(data_dir + "/*.jpg")
self.X = np.array([plt.imread(self.file_path[idx]) for idx in range(len(self.file_path))])
y = np.array([self.file_path[idx].split('\\')[-1].split('.')[0] for idx in range(len(self.file_path))])
self.y = self._convert_to_numeric(y)
self.transform = self._transforms()
def _convert_to_numeric(self, class_array):
class_to_number = {class_value : idx for idx, class_value in enumerate(np.unique(class_array))}
return np.vectorize(class_to_number.get)(class_array)
메모리 문제로 이미지 7장으로 테스트하겠습니다. 궁극적으로 우리는 이미지는 텐서, 라벨은 숫자로 바꾸고 순서대로 가지고 있기만 하면 됩니다. (train_test_split도 동일한 개념입니다. 이미지 텐서와 라벨 텐서가 동일한 인덱스 ex) 6번째 인덱스의 7이미지, 6번째 인덱스의 7라벨이 다른 배열에 있는 상태) 하지만 위 처럼 넘파이 배열로 작성하면 메모리를 많이 먹어서 거대한 데이터는 불가합니다.
2) __len__
동일하게 길이를 반환합니다.
3) __getitem__
def __getitem__(self, index):
X = self.X[index] / 255.
X = self.transform(X)
y = self.y[index]
return X, torch.tensor(y, dtype=torch.long)
이미지의 경우 255를 0과 1 사이의 실수로 변환해주는 것과 정규화하는 것이 기본입니다. init에서 정의한 넘파이 형태의 데이터를 텐서로 바꾸면서 라벨은 정수 타입임을 명시합니다.
- 테스트
앞서 정의한 Base 코드와 Config 파서로 config.json만 변경하면 새롭게 만든 데이터 셋을 바로 사용할 수 있습니다.
generate로 dataset에서 데이터 한개를 가져와 출력해보면, ImageDataset을 정확히 잡고 있는 것을 확인할 수 있습니다.
=> 메모리 문제를 해결한 방식
하지만 위 방식은 전체 이미지를 다 불러오기 때문에 메모리 문제가 발생한다고 하였고, 이를 해결하는 방법이 있습니다.(물론 데이터의 개수가 적은 경우에는 데이터 전체를 로드해서 사용해도 됩니다.)
1) __init__
def __init__(self, data_dir):
# 이미지 데이터 초기화
self.image_list = glob.glob(data_dir + "/*.jpg")
self.data_len = len(self.image_list)
y = np.array([self.image_list[idx].split('\\')[-1].split('.')[0] for idx in range(len(self.image_list))])
self.y = self._convert_to_numeric(y)
self.transform = self._transforms()
이전 데이터 셋과 달리 이미지의 경로만 리스트로 가지고 있습니다.
2) __getitem__
def __getitem__(self, index):
single_image_path = self.image_list[index]
x_img = Image.open(single_image_path)
X = np.asarray(x_img) / 255.
X = self.transform(X)
y = self.y[index]
return X, torch.tensor(y, dtype=torch.long)
그리고 init에서 이미지를 불러오는 것이 아니라, get에서 idx에 해당하는 이미지의 경로에 접근해서 이미지 하나만 로드한다면 메모리 문제를 피해갈 수 있습니다.
2. 폴더 명을 클래스로 나뉘어져 있는 경우
이번에는 폴더 구조로 나뉘어진 상황을 알아봅니다. 파이토치의 ImageFolder를 사용하여 쉽게 이미지와 라벨을 텐서, 숫자로 바꿀 수 있습니다.
이미지 폴더 데이터 셋을 구현합니다. 여기서는 데이터 셋 클래스를 직접 정의할 필요 없이, 폴더만 있으면 전처리와 증강을 할 수 있습니다.
class ImageFolderDataset(BaseDataset):
# 이미지 RGB 값 평균으로 데이터 전처리
def __init__(self, data_dir):
self.transform = self._init_transforms()
dataset = datasets.ImageFolder(data_dir, self._init_transforms())
이처럼 ImageFolder 모듈로 데이터 셋을 정의할 수 있으며 이 데이터셋을 로더에 인자로 넣으면 바로 사용할 수 있습니다.
# 픽셀별 정규화를 위한 증강
def _init_transforms(self):
return transforms.Compose([
transforms.Resize((1024, 1024)),
transforms.ToTensor()
])
# 메인 증강
def _main_transfomrs(self):
return transforms.Compose([
transforms.RandomHorizontalFlip(), # 좌우반전
transforms.RandomVerticalFlip(), # 상하반전
transforms.Resize((1024, 1024)), # 알맞게 변경하세요
transforms.ToTensor(), # 이 과정에서 [0, 255]의 범위를 갖는 값들을 [0.0, 1.0]으로 정규화, torch.FloatTensor로 변환
transforms.Normalize([self.meanR, self.meanG, self.meanB],
[self.stdR, self.stdG, self.stdB]) # 정규화(normalization)
])
저는 데이터에 RGB 평균, 표준편차로 정규화를 해주기 위해 init, main 증강을 구분하여 정의하였습니다. init은 RGB 평균을 구하기 위해 이미지에 resize를 하고 텐서로 바꿔주는 역할이고 main 증강에는 모델 학습을 위한 증강을 정의해 줍니다.
def __init__(self, data_dir):
self.transform = self._init_transforms()
dataset = datasets.ImageFolder(data_dir, self._init_transforms())
meanRGB = [np.mean(x.numpy(), axis=(1,2)) for x,_ in dataset]
stdRGB = [np.std(x.numpy(), axis=(1,2)) for x,_ in dataset]
self.meanR = np.mean([m[0] for m in meanRGB])
self.meanG = np.mean([m[1] for m in meanRGB])
self.meanB = np.mean([m[2] for m in meanRGB])
self.stdR = np.mean([s[0] for s in stdRGB])
self.stdG = np.mean([s[1] for s in stdRGB])
self.stdB = np.mean([s[2] for s in stdRGB])
print("평균",self.meanR, self.meanG, self.meanB)
print("표준편차",self.stdR, self.stdG, self.stdB)
self.train_data = datasets.ImageFolder(os.path.join(data_dir),
self._main_transfomrs())
def getDataset(self):
return self.train_data
초기화할 때 가져온 데이터로 RGB 평균을 구하고 새로 데이터 셋을 선언하여 정규화를 적용한 후 최종 데이터 셋을 생성합니다. 이제 이 데이터 셋을 데이터 로더의 인자로 넣으면 바로 사용할 수 있습니다.
- 테스트
config 파일을 변경한 후 테스트 해보면
이미지도 잘 불러오고 데이터 셋도 정확히 반환되는 것을 확인할 수 있습니다.
- 데이터 로더
1. 다중 프로세스 fetching : num workers
2. sampling data as small batches
3. collate_fn을 활용한 데이터 변환
4. 핀 메모리
데이터 로더는 데이터를 샘플링하는 데 도움이 되는 주요 수단입니다. collate_fn은 사용할 목적에 맞게 데이터를 변환하면 되므로 이번 시간에는 샘플러 종류와 각각의 수행 결과를 알아봅니다.
우선 샘플러를 사용하려면 shuffle을 false로 해야합니다. shuffle은 가져올 데이터를 섞는 것인데 샘플러를 사용한다는 것은 인덱스에 제약을 걸겠다는 것이기 때문입니다.
1. sequential sampler
A Sampler that returns indices sequentially.
늘 같은 순서대로 데이터를 샘플링합니다.
샘플러를 선언할 때는 샘플러마다 필요한 파라미터가 다르며, 샘플러가 데이터를 샘플링하는 것이기 때문에 데이터셋이나 인덱스를 주로 사용합니다.
수행 결과, 시퀀셜 샘플러는 배치 사이즈에 맞게 순서대로 데이터를 가져옵니다.
2. RandomSampler
랜덤 샘플러 역시 데이터 셋을 인자로 받습니다.replacement=True를 설정하면, 복원 추출을 진행합니다.
RandomSampler는 복원 추출을 할 수 있으며, False로 하면 비복원 추출을 수행합니다.
3. SubsetRandomSampler
SubsetRandomSampler은 이름 그래도 부분집합을 만듭니다. 인덱스 범위를 인자로 받아 부분집합을 만들고 분리된 상태에서 랜덤 샘플링을 합니다. 인덱스 범위를 줄 수 있으니 train / val를 구분할 수 있습니다.
따라서 train / val 데이터 로더를 따로 분리하여 만들 때 자주 사용됩니다.
예제를 만들어보겠습니다. train / val를 split만큼 분리하고 train 데이터 셋에서 랜덤 샘플링을 해보겠습니다.
전체 7개의 데이터 중에서 split을 '2'로 하였고 그 중 랜덤으로 2개가 val [6, 2]에 할당되고 나머지 5개가 train [1, 3, 0, 5, 4]에 할당됩니다. 분리될 때도 랜덤이고 분리된 데이터 셋에서 샘플링을 할 때도 랜덤으로 샘플링합니다. 이때 중복은 허용되지 않습니다.
4. WeightedRandomSampler
Weighted random sampling은 클래스 불균형 문제를 해결하기 위한 방법 중 하나입니다. 개별 이미지 한 장이 뽑힐 확률은 1/전체 개수라서 이미지를 많이 가지고 있는 클래스가 뽑힐 확률이 더 높습니다. 이를 보완하고자 더 적은 이미지를 갖는 클래스의 이미지가 뽑힐 확률은 높히도록 큰 가중치를 곱하는 방식으로 확률을 맞춥니다. 7:3 비율을 갖는 데이터가 있다면 배치에서는 5:5로 뽑아주게 됩니다.
1. 가중 확률 정의
2. 가중 확률을 기반으로 Sampler가 데이터 선택
3. 매 배치마다 balanced batch set 생성
위 이론을 수행하여 샘플러가 데이터 로더 내부에서 동작합니다.
-> 인자
1. weights
각 데이터 샘플에 대한 가중치를 나타내는 숫자 리스트입니다. 리스트의 길이는 전체 데이터셋의 크기와 일치해야 합니다. 가중치가 높을수록 해당 샘플이 선택될 확률이 높아집니다.
확률을 동일하게 주면
동일한 확률로 샘플링을 합니다.
0번 데이터의 확률을 높이 주면
복원 추출 시에 0번 데이터가 나올 확률이 올라가는 것을 볼 수 있습니다. 1번 데이터의 확률을 낮추니 0번 데이터가 더 확률이 높아 한 번 더 추출되었습니다.
2. num_samples
한 번에 샘플링할 데이터의 개수를 나타냅니다.
데이터 로더를 사용하면 데이터 셋의 __len__에 정의된 길이만큼 데이터를 로드하게 되는 데 데이터의 길이와 num_samples를 맞추면 전체 데이터를 가져오게 되고
num_samples를 3으로 하면
3개만 가져오고 데이터 로더의 작동이 끝납니다. 따라서 데이터 전체 길이로 명시하는 것이 일반적입니다.
=> 불균형 데이터에 적용
# 타겟 클래스의 분포와 타겟 클래스 리스트 추출
target_dist, y_train = train_dataset.target_class_distribution()
타겟 클래스의 분포를 구합니다.
# 타겟 클래스 분포를 타겟 번호 기준으로 정렬
sort_target_dist = sorted(target_dist.items(), key = lambda x:x[0])
정렬 결과 8, 14번 타겟이 매우 부족한 것을 확인할 수 있고 더 높은 가중치를 주어 많이 선택되도록 해야합니다.
# 가중치 계산
class_weights = [num_samples / sort_target_dist[i][1] for i in range(len(sort_target_dist))]
각 타겟 클래스 개수를 전체 샘플 수로 나누어 가중치를 구합니다.
# y 라벨마다 가중치를 적용
weights = [class_weights[y_train[i]] for i in range(num_samples)]
y 라벨마다 가중치를 적용합니다. 4.13은 4번째 타겟으로 가중치 50개만 봐도 상당히 많은 것을 알 수 있고 가중치를 작게 준 것을 확인할 수 있습니다.
로더에 구현한 샘플러를 적용하면 완료입니다.
-> 가중치 샘플러 사용 결과
inceptionV3 모델을 사용한 마스크 착용 여부 모델 학습에서 가중치 샘플러를 사용하기 전 모델 성능은 f1 score 34에 acc 45 였습니다.
가중치 샘플러를 적용한 후 30% 가까이 성능이 올랐습니다.
하이퍼 파라미터 튜닝을 한 최종 모델은 각각 0.02, 4%가 증가하는 것을 체감하였습니다.
결론
지금까지 데이터 셋을 데이터에 fit 하게 작성하는 방법과 데이터 로더의 샘플러에 대해 깊게 다뤄봤습니다. BatchSampler도 있지만 데이터 로더에 sequential sampler를 사용한 것과 같은 결과를 수행합니다. 앞으로 데이터의 특성에 맞게 적절하게 데이터셋과 로더를 구현하여 사용할 수 있을 것 같습니다.
'[AI] > [딥러닝 | 이슈해결]' 카테고리의 다른 글
Augmentation PART.albumentation 활용 (2) | 2023.12.19 |
---|---|
[최적화] GPU 풀 구현기 1 (0) | 2023.12.09 |
[최적화] 유연하게 확장 가능한 AI 학습 환경 구축 (0) | 2023.12.04 |
Pytorch PART.논문을 코드로 구현하는 능력(ResNet) (0) | 2023.11.28 |
PyTorch PART.PyTorch 탬플릿 Config 파일 활용 (0) | 2023.11.19 |