※ 본 게시글은 필자의 개인 경험을 토대로 작성된 것으로, 잘못된 정보를 포함하고 있을 수 있습니다. ※
이미지(image) 분야의 딥러닝에서는 이미지넷(ImageNet) 1k 데이터 세트가 매우 많이 사용된다. 이 데이터 세트는 흔히 ILSVRC 2012 데이터세트라고도 하는데, 이 데이터 세트에는 약 1,200,000개의 학습 이미지(데이터) 포함되어 있으며, 검증(validation) 이미지(데이터)는 50,000개이다.
그리고 ImageNet-1k라는 이름에서부터 알 수 있듯이 클래스(class)의 개수는 총 1,000개다. ImageNet-1k 데이터 세트는 흔히 특정한 이미지 분류 모델의 성능(정확도)를 평가하기 위해 가장 많이 사용된다. 이미지 분류 모델의 성능을 평가하는 작업에서 거의 빠지지 않고 등장하는 데이터 세트가 바로 이 ImageNet라고 할 수 있다.
참고로 원래 오리지날(original) ImageNet 데이터 세트는 천만 개 이상의 데이터를 가지고 있는데, 이 중에서 1,000개의 클래스만 추려서 만든 것이 ILSVRC 2012 (ImageNet-1k)라고 이해할 수 있다.
이제 흔히 다양한 논문에서 베이스라인(baseline) 데이터 세트로 사용되는 ILSVRC2012 데이터 세트를 확인해 보자. 최근에 나오는 논문 혹은 포럼 등에서 단순히 ImageNet 데이터 세트라고 한다면, 그것은 ILSVRC2012를 가리키는 것이 일반적이다.
전체 데이터의 개수는 약 1,000,000개이기 때문에, 각 클래스당 약 1,000개의 데이터가 존재하는 것으로 알 수 있다. 한 논문 "ImageNet Large Scale Visual Recognition Challenge" 논문에서 언급된 내용을 확인해 보면, 다음의 표와 같다.
참고로 테스트 이미지 10만 장에 대한 정답(레이블)은 일반적인 연구자가 쉽게 접근할 수 없지만, 학습(training) 이미지와 검증용(validation) 이미지에 대해서는 모든 이미지 파일 및 정답 레이블(ground-truth) 정보에 접근할 수 있고, 해당 내용을 확인할 수 있다.
해당 논문에 나와 있는 문장으로는 다음과 같다. 쉽게 말 하면 ILSVRC2012 데이터 세트의 학습 데이터에서는 각 클래스마다 732개에서 1300개의 이미지가 포함된다고 한다.
"There are 1,281,167 images and 732~1300 images are belonging per class in the ILSVRC2012 training dataset."
그렇다면, 이러한 ImageNet-1k 데이터 세트에 어떻게 접근하고, 실제 이미지를 불러와 볼 수 있을까? 방법은 생각보다 아주 간단하다. Hugging Face 라이브러리를 사용하면 된다. Hugging Face에서 제공하는 ImageNet-1k 데이터 세트를 이용하면, 간단히 다운로드를 진행할 수 있다.
결과적으로 다음과 같이 한 줄의 코드로 간단히 ILSVRC 2012 데이터 세트를 불러올 수 있다. 학습 데이터의 개수는 약 1,200,000개로 많지만, 좋은 GPU 한 대만 있으면 충분히 학습시켜 볼 수 있는 수준이기도 하다.
참고로 전체 학습 이미지(training image)에 대한 전체 용량이 약 150GB 정도 되는 것을 확인할 수 있다.
다음과 같은 코드를 이용하여 간단히 ImageNet-1k 데이터 세트를 불러올 수 있다.
from datasets import load_dataset
imagenet_dataset = load_dataset("imagenet-1k")
참고로 PyTorch에서 불러오기 위해서는, 다음과 같이 CustomDataset을 만들어서 미니배치(mini-batch) 단위로 데이터를 불러올 수도 있다.
import torch
class CustomDataset(torch.utils.data.Dataset):
def __init__(self, train_mode=True, transforms=None):
if train_mode:
self.dataset = imagenet_dataset["train"]
else:
self.dataset = imagenet_dataset["validation"]
self.transforms = None
if transforms:
self.transforms = transforms
def __getitem__(self, index):
image = self.dataset[index]["image"]
label = self.dataset[index]["label"]
current = image.convert("RGB")
if self.transforms:
current = self.transforms(current)
return current, label
def __len__(self):
return len(self.dataset)
실제로 데이터 로더(data loader)를 불러오는 코드는 다음과 같다. 이를 이용하면 PyTorch를 이용해 간단히 매 미니배치(mini-batch) 단위로 데이터를 불러와 확인해 볼 수 있다.
간단히 학습 데이터의 개수를 확인해 보면, 총 1,281,167개인 것을 확인할 수 있다. 또한 전체 검증(validation) 데이터의 개수는 총 50,000개인 것을 확인할 수 있다. (각 클래스당 50장)
import torchvision.transforms as transforms
augment_train = transforms.Compose([
transforms.RandomResizedCrop(224),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
])
transform_test = transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
])
batch_size = 256
train_dataset = CustomDataset(train_mode=True, transforms=augment_train)
train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=batch_size, shuffle=True, num_workers=16)
test_dataset = CustomDataset(train_mode=False, transforms=transform_test)
test_loader = torch.utils.data.DataLoader(test_dataset, batch_size=batch_size, shuffle=False, num_workers=16)
print("Size of training dataset:", len(train_dataset))
print("Size of test dataset:", len(test_dataset))
실제로 다음과 같이 train_loader에서 매 배치 단위로 데이터를 불러온 결과는 다음과 같다. 실제로 실행해 보면 배치 크기(batch size)인 256에 맞게 매 배치마다 적절하게 데이터가 불러와지는 것을 확인할 수 있다.
for batch_idx, (images, labels) in enumerate(train_loader):
print(images.shape)
print(labels.shape)
break
가장 먼저, 다음과 같이 load_dataset을 이용하여 ImageNet-1k 데이터 세트를 불러오자.
코드를 실행했을 때, 다음과 같이 정상적으로 결과를 확인할 수 있다.
필자는 각 클래스당 100개의 이미지만 포함된 "ImageNet-1k 100 images per class"라는 데이터 세트를 만들었다. 다시 말해 1,000개의 클래스 각각 100개의 이미지만을 포함하기 때문에, 최종적으로 학습 이미지의 개수는 총 100,000개가 된다.
현실적으로 내가 어떠한 알고리즘을 설계한 뒤에, 이것이 실제로 잘 동작하는지 테스트해보고 싶을 수 있다. 이때, 각 클래스당 1,000개의 모든 이미지를 이용해 학습을 진행하는 것보다 클래스당 100개의 이미지만을 사용한다면 학습 속도가 약 10배 가량 빨라질 수 있다.
사실 이것은 코어셋(core-set) 알고리즘 연구 분야와도 연관성이 깊은데, 아무튼 동일한 GPU 리소스로 보다 빠르게 많은 실험을 돌려 볼 수 있기 때문에, 좋은 아키텍처를 찾기 위하여 효과적으로 사용할 수 있는 방식이다. 약 1,200,000개의 모든 이미지를 이용하여 매번 학습을 진행하면 1대의 GPU로 3~4일이 걸리기 때문이다. 반면에 학습 데이터를 총 100,000개만을 사용하는 경우 5시간 정도면 한 번의 실험 결과를 얻을 수 있다.
실제로 코어셋(core-set) 연구 논문인 DeepCore (2022)에서는 ImageNet에서 10%의 학습 데이터(약 10만 장)만을 랜덤으로 선택해 사용하더라도 Top-1 정확도(accuracy)로 약 52%를 얻을 수 있다는 점이 보고되었다. 참고로 전체 100만 개의 모든 학습 데이터를 사용하면 69% 정도의 정확도를 얻을 수 있다. (ResNet-18 기준)
다음과 같은 코드를 이용하여 총 1000개의 클래스(레이블)에 대하여 각 100장의 이미지만을 추출할 수 있다.
import os
import time
import numpy as np
train_dataset = CustomDataset(train_mode=True, transforms=None) # 데이터 세트 불러오기
train_dataset_size = len(train_dataset) # 전체 데이터의 개수
permutation = np.random.permutation(train_dataset_size) # 데이터의 인덱스 랜덤으로 섞기
n_classes = 1000 # 전체 클래스 개수
n_images_per_class = 100 # 클래스당 추출할 이미지 개수
target = n_classes * n_images_per_class
label_to_images = [[] for i in range(n_classes)] # 각 레이블마다 저장된 이미지들
# 최종 이미지 저장 폴더
for i in range(n_classes):
directory = f"imagenet_{n_images_per_class}_images_per_class/{i}"
os.makedirs(directory, exist_ok=True)
start_time = time.time() # 이미지 처리 시작 시각
processed = 0 # 저장된 이미지 수
for cnt, idx in enumerate(permutation):
image, label = train_dataset[int(idx)]
# 각 레이블(클래스)마다 저장될 최대 이미지 개수 한도 설정
if len(label_to_images[label]) < n_images_per_class:
# 이미지 저장
image.save(f"imagenet_{n_images_per_class}_images_per_class/{label}/{len(label_to_images[label])}.png")
label_to_images[label].append(image)
processed += 1
# 로깅(logging)
if (cnt + 1) % 1000 == 0:
print(f"({cnt + 1}), [{processed}/{target}] {time.time() - start_time:.2f} seconds elapsed.")
if processed == target:
break
실행 결과는 다음과 같다. 약 2시간 정도를 소요하여, 결과적으로 총 100,000장의 이미지가 저장되었다.
(1000), [1000/100000] 62.68 seconds elapsed.
(2000), [2000/100000] 120.75 seconds elapsed.
(3000), [3000/100000] 180.11 seconds elapsed.
(4000), [4000/100000] 244.28 seconds elapsed.
(5000), [5000/100000] 305.05 seconds elapsed.
(6000), [6000/100000] 368.30 seconds elapsed.
(7000), [7000/100000] 428.13 seconds elapsed.
(8000), [8000/100000] 491.33 seconds elapsed.
(9000), [9000/100000] 553.37 seconds elapsed.
(10000), [10000/100000] 610.46 seconds elapsed.
...
(30000), [30000/100000] 1866.69 seconds elapsed.
(31000), [31000/100000] 1932.57 seconds elapsed.
(32000), [32000/100000] 1996.23 seconds elapsed.
(33000), [33000/100000] 2049.75 seconds elapsed.
(34000), [34000/100000] 2114.50 seconds elapsed.
...
(50000), [50000/100000] 3177.90 seconds elapsed.
(51000), [51000/100000] 3241.96 seconds elapsed.
(52000), [52000/100000] 3309.74 seconds elapsed.
(53000), [53000/100000] 3371.50 seconds elapsed.
(54000), [54000/100000] 3429.78 seconds elapsed.
...
(65000), [65000/100000] 4141.32 seconds elapsed.
(66000), [66000/100000] 4200.57 seconds elapsed.
(67000), [67000/100000] 4268.17 seconds elapsed.
(68000), [67996/100000] 4328.20 seconds elapsed.
(69000), [68995/100000] 4384.93 seconds elapsed.
...
(76000), [75963/100000] 4818.34 seconds elapsed.
(77000), [76958/100000] 4875.96 seconds elapsed.
(78000), [77942/100000] 4939.35 seconds elapsed.
(79000), [78935/100000] 5002.03 seconds elapsed.
(80000), [79913/100000] 5062.91 seconds elapsed.
...
(95000), [92909/100000] 5893.68 seconds elapsed.
(96000), [93569/100000] 5936.18 seconds elapsed.
(97000), [94199/100000] 5981.79 seconds elapsed.
(98000), [94752/100000] 6024.19 seconds elapsed.
(99000), [95279/100000] 6055.75 seconds elapsed.
...
(120000), [99489/100000] 6400.83 seconds elapsed.
(121000), [99525/100000] 6407.42 seconds elapsed.
(122000), [99558/100000] 6413.48 seconds elapsed.
(123000), [99589/100000] 6419.44 seconds elapsed.
(124000), [99617/100000] 6425.66 seconds elapsed.
...
(160000), [99963/100000] 6618.42 seconds elapsed.
(161000), [99967/100000] 6623.48 seconds elapsed.
(162000), [99970/100000] 6628.30 seconds elapsed.
(163000), [99970/100000] 6633.49 seconds elapsed.
(164000), [99972/100000] 6638.33 seconds elapsed.
...
(176000), [99993/100000] 6697.06 seconds elapsed.
(177000), [99995/100000] 6702.08 seconds elapsed.
(178000), [99995/100000] 6706.99 seconds elapsed.
(179000), [99996/100000] 6711.80 seconds elapsed.
(180000), [99996/100000] 6716.41 seconds elapsed.
(181000), [99998/100000] 6721.46 seconds elapsed.
(182000), [99999/100000] 6726.27 seconds elapsed.
실질적으로 프로그램을 실행하여 총 100,000개의 이미지를 추출하여 새로운 데이터 세트를 만들 수 있는 것을 확인할 수 있다.
결과적으로 다음과 같이 최종적으로 만들어진 데이터 세트에 대하여 각 레이블마다 몇 개의 이미지가 포함되어 있는지 검증이 가능하다.
import os
# 각 클래스(class) 레이블 정보를 하나씩 확인하며
for i in range(n_classes):
directory = f"imagenet_{n_images_per_class}_images_per_class/{i}"
cnt = 0
# 폴더 내부의 경로를 하나씩 확인하며
for path in os.listdir(directory):
# 현재의 경로(path)가 파일인 경우 카운트
if os.path.isfile(os.path.join(directory, path)):
cnt += 1
# 각 레이블에 포함된 파일의 개수 출력
print(f"The number of file: {cnt} for the label {i}")
각 레이블마다 100개씩 잘 들어가 있는 것을 확인할 수 있다. 즉, 총 100,000개의 이미지가 존재하는 것을 확인할 수 있다.