이번 글에서는 이미지(data), 차원(dimension), 그리고 분포(distribution)간의 관계에 대해서 설명해보도록 하겠습니다.
앞으로 AutoEncoder, VAE, GAN을 이해하기 위한 가장 기본적인 background이니 만큼 잘 설명해보도록 하겠습니다.
1. 이미지와 차원(dimension) 간의 관계
먼저, 한 가지 질문을 던지면서 글을 시작하도록 하겠습니다.
"색(color)을 표현(representation)하기 위해서는 몇 차원이 필요한가요?"
답부터 말씀드리면 "색을 표현하기 위해서는 3차원이 필요"합니다. 현실세계에서는 3차원 (R,G,B) 값을 다양하게 조합하여 색상을 표현할 수 있죠. 그리고, 이렇게 표현된 공간을 "색 공간 (color space)"라고 부릅니다.
"그렇다면, 색을 표현할 수 있는 모든 경우의 수는 어떻게 될까요?"
당연히 조합의 경우의 수를 구하면 되므로 \(255^{3}\) 이 될 것입니다.
"gray color를 표현하기 위해서 필요한 차원은 무엇일까요?"
gray color는 1차원 (0~255)으로 모두 표현 가능합니다. 또한, gray color를 표현할 수 있는 모든 경우의 수는 255가지가 되겠죠. 그래서, 흑백 이미지를 다루는 문제에서는 이미지를 1차원으로 가정하고 gray scale이라고 표현하기도 합니다.
"앞서 색상관련해서는 정해진 차원(1차원 or 3차원)이 필요하다고 했는데, 그렇다면 이미지를 표현하기 위해서도 정해진 차원이 필요한가요?"
결론부터 말씀드리면, 이미지에 대한 차원은 "이미지 크기에 따라 달라진다"입니다. 아래 gray scale의 튜링 이미지의 크기가 200×200이라고 한다면, 이러한 이미지를 표현하기 위해서는 40000 차원이 필요합니다. 왜 이렇게 고차원이 나오는지 설명해보도록 하겠습니다. (사실 엄밀히 말하면 색상도 R,G,B외에 다른 요소들로 표현할 수 있으면 색상의 차원은 또 달라질 수 있습니다)
200×200 gray scale 이미지를 표현한다고 하면 어떻게 생각해볼 수 있을까요? 먼저, 이미지라는 것은 pixel의 조합으로 이루어져 있습니다. 그리고, 하나의 픽셀에는 하나의 색 표현이 가능하죠. Gray scale인 경우에는 하나의 pixel에 0~255 사이 값 중 하나가 할당될 수 있습니다.
그럼 40000개의 pixel이 나올 것입니다. 이때, 각각의 pixel들은 0~255개의 값을 지니고 있죠. 이를 다른 관점에서 보면, 40000개의 독립된 변수로 볼 수 있고, 각각의 변수는 0~255 값의 범위를 갖고 있다고 할 수 있죠. 40000개의 독립된 변수로 하나의 이미지가 구성될 수 있기 때문에, 200×200 gray scale 이미지는 40000차원을 갖는다고 할 수 있습니다. 200×200 이미지는 40000 차원의 독립된 변수들이 갖는 고유의 값들에 의해 표현될 수 있는 것이죠.
"그렇다면, 200×200 gray scale 이미지를 표현할 수 있는 모든 경우의 수는 어떻게 될까요?"
정답은 아래에서 구한 것 처럼 \(10^{96329}\) 경우의 수가 됩니다.
2. 이미지와 분포간의 관계
앞서 200×200 gray scale 이미지의 차원은 40000차원이고, 표현할 수 있는 이미지의 개수(경우의 수)는 대략 \(10^{96329}\) 라고 했습니다.
"그렇다면, 200×200 gray scale 에서 표현될 수 있는 모든 경우의 수에 해당하는 이미지들은 의미가 있다고 할 수 을까요?"
예를 들어, \(10^{96329}\) 경우의 조합들 중에서는 아래 왼쪽과 같이 의미 없는 이미지(noise image)들도 있을 것이고, 오른쪽 같이 사람이 구별할 수 있는 의미 있는 이미지가 있을 수 있습니다.
여기서 우리는 또 한 가지 질문을 던져볼 수 있습니다.
"200×200 gray scale 즉, 40000차원 상에서 의미있는 이미지들은 고르게 분포(uniform distribution)하고 있을까요? 아니면 40000차원이라는 공간의 특정 영역에 몰려있거나 특정 패턴으로 분포(non-uniform distribution)하고 있을까요?"
이러한 질문에 답을 하는 방법 중 하나는 200×200 gray scale 이미지들을 uniform distribution으로 수 없이 샘플링 해봐서 경험적으로 보여주는 것입니다. 만약, uniform distribution을 전제로 수 없이 샘플링 했을 때, 의미있는 이미지들이 종종 보인다면 의미있는 이미지들이 40000차원 상에 고르게 분포한다고 볼 수 있겠죠.
(※ 직관적으로 이해하기 위해 편의상 40000차원을 아래 이미지 처럼 3차원으로 표현했습니다. (즉, 아래 그림은 '본래 40000차원이다'라고 간주하시면 될 것 같습니다))
"오토인코더의 모든 것"이라는 영상에서 이활석님은 20만번 샘플링한 결과 의미없는 이미지(noisy image)만 추출된 것을 확인 할 수 있었다고 합니다. 이러한 실험을 통해 의미있는 이미지들은 40000차원에서 특정 패턴 또는 특정 위치에 분포해 있다고 경험적으로 결론내릴 수 있게 되는 것이죠.
(↓↓↓아래 영상 1:01:25초 부터↓↓↓)
지금까지 AutoEncoder, VAE, GAN을 배우기 위한 기본 지식들을 정리해봤습니다.
다음 글에서는 AutoEncoder에 대해서 다루면서 dimension reduction에 대한 개념을 소개하도록 하겠습니다. 왜냐하면 VAE, GAN 같은 논문들을 살펴보려면 "latent"말을 이해하기 중요하기 때문이죠!
from torchvision import datasets, models, transforms
위의 코드를 보면 torchvision에서 transforms, datasets 이라는 패키지가 사용되었다는걸 확인할 수 있습니다. Pytorch에는 torchvision이라는 패키지를 이용해 다양한 computer vision 관련 task를 수행할 수 있습니다. 각각의 패키지들 (datasets, transforms, models) 에 대해서는 위의 코드를 분석하면서 같이 설명하도록 하겠습니다.
위의 코드를 살펴보면 compose 함수인자에 transforms 객체들의 list를 입력 받습니다.
이것이 의미하는 바가 무엇일까요? 계속해서 알아보도록 하겠습니다.
data_transforms 자체는 dictionary로 정의되었습니다.
그리고 각 key에 해당 하는 value 값은 클래스 객체 Compose인 것을 알 수 있습니다.
Compose 객체에 다양한 인자 값들이 포함되어 있는데, 이 인자 값들이 augmentation이 적용되는 순서라고 보시면 됩니다.
EX) data_tranforms['train']의 경우
RandomResizedCrop 수행
scale→ Specifies the lower and upper bounds for the random area of the crop, before resizing. The scale is defined with respect to the area of the original image.
ratio → lower and upper bounds for the random aspect ratio of the crop, before resizing.
RandomHorizontalFlip 수행
p=0.5 → 50%의 확률로 RandomHorizontalFlip 실행
ToTensor 수행
처음 로드되는 이미지 형태를 딥러닝 학습 format에 맞는 tensor 형태로 변경
Converts a PIL Image or numpy.ndarray (H x W x C) in the range [0, 255] to a torch.FloatTensor of shape (C x H x W) in the range [0.0, 1.0]
Normalize 수행
이미지 전처리 (Preprocessing)
Normalization에 들어가는 값들이 어떻게 계산되는지는 아래 PPT를 참고해주세요.
2. datasets(Feat. Data Augmentation)
datasets.ImageFolder를 이용하면 이미지 디렉터리에 존재하는 이미지 메타정보 (ex: 이미지 개수, augmentation 기법들, etc..) 등을 하나의 tuple 형태로 저장할 수 있습니다 ← ImageFolder의 return 형태는 tuple
나중에 DataLoader를 이용해 실제 이미지를 load 할 때, 이미지 메타정보(=tuple 형태)들을 이용해 쉽게 load할 수 있습니다 ← 뒤에서 설명
root: Root directory of dataset
transforms: A function/transform that takes in an PIL image and returns a transformed version (ex:transforms.RandomCrop)
dataset_size 변수는 딥러닝 모델(CNN)을 학습 시킬 때 epoch 단위의 loss or accuracy 산출을 위해 이용됩니다. (자세한 설명은 train을 구현하는 함수에서 설명하도록 하겠습니다)
train 폴더를 보면 총 5개의 class가 있다는걸 확인 할 수 있습니다.
image_datasets['train'].classes을 이용하면 아래 폴더를 기준으로 class 명들을 string 형태로 사용할 수 있게 됩니다.
※이번 코드에서는 포함되어 있진 않지만, datasets 패키지의 첫 번째 활용도는 cifar10, caltech101, imagenet 과 같이 범용적으로 쓰이는 데이터를 쉽게 다운로드 받아 이용할 수 있게 해준다는 점입니다. 상식으로 알아두시면 좋습니다!
3. DataLoader
At the heart of PyTorch data loading utility is the torch.utils.data.DataLoaderclass.
딥러닝에서 이미지를 load 하기 위해서는 batchsize, num_workers 등의 사전 정보들을 정의해야합니다. (※torch.utils.data.DataLoader가 이미지를 GPU에 업로드 시켜주는 코드가 아니라, 사전 정보들을 정의하는 역할만 한다는 것을 알아두세요!)
image_datasets[x]: 예를 들어, image_datasets['train']인 경우 train 폴더에 해당하는 클래스들의 이미지 경로를 설정해줍니다.
batch_size: 학습을 위한 batch size 크기를 설정해줍니다.
shuffle: 이미지 데이터를 불러들일 때, shuffle을 적용 여부를 설정해줍니다.
num_workers: 바로 아래에서 따로 설명
아래 코드 역시 dataloaders라는 dictionary 형태의 변수를 for 문으로 생성하는 코드 입니다.
[num_workers]
학습 이미지를 GPU에 할당시켜 주기 위해서는 CPU의 조력이 필요합니다. 즉, CPU가 학습 이미지를 GPU에 올려주는 역할을 하는 것이죠.
좀 더 구체적으로 말하자면, CPU에서 process 생성해 학습 이미지를 GPU에 업로드 시켜주는 방식입니다.
num_workers는 CPU가 GPU에 이미지를 업로드 할 때, 얼마나 많은 subprocess를 이용할지를 결정하는 인자(argument)라고 생각하시면 됩니다.
howmanysubprocessesto use for data loading. 0 means that the data will be loaded in the main process. (default: 0)
num_workers를 0으로 설정했을 때 (=a main process), CPU가 하나의 이미지를 GPU에 업로드 하는 시간을 1초라고 가정해보겠습니다.
이 때, 업로드된 이미지를 GPU에서 처리 (for CNN training) 를 하는 것이 0.1초라고 가정한다면, 다음 이미지를 업로드 할 때 까지 0.9초 동안 GPU는 놀게(idle)됩니다. (아래 "그림14"에서 빨간색 간격 → CPU에서 GPU로 이미지 데이터를 넘기기 위해 수행되는 전처리 과정)
결국 CPU에서 GPU로 이미지를 넘겨주는 작업을 빠르게 증가시키기 위해서는 여러 subprocess를 이용해 이미지들을 GPU에 업로드 시켜주어야 합니다. 다시 말해, 다수의 process를 이용해서 CPU에서 GPU로 이미지 데이터를 넘기기 위한 전처리 시간을 축소시켜주는 것이죠.
이렇게 num_workers를 증가하여 GPU에 이미지를 업로드 시키는 속도가 빨라지면, GPU가 놀지 않고 열심히 일하게 될 것입니다. → GPU 사용량(이용률)이 증가하게 된다고도 할 수 있습니다.
More num_workers would consume more memory usage but is helpful to speed up the I/O process.
하지만, CPU가 GPU에 이미지를 업로드 시켜주는 역할은 수 많은 CPU의 역할 중 하나입니다.
즉, num_workers를 너무 많이 설정해주어 CPU코어들을 모두 dataload에 사용하게 되면, CPU가 다른 중요한 일들을 못하게 되는 상황이 발생되는 것이죠.
보통 코어 개수의 절반 정도로 num_workers를 설정하는 경우도 있지만, 보통 batch size를 어떻게 설정해주냐에 따라서 num_workers를 결정해주는 경우가 많습니다.
경험적으로 batch_size가 클 때, num_workers 부분도 크게 잡아주면 out of memory 에러가 종종 발생해서, num_workers 부분을 줄이거나, batch_size 부분을 줄였던 것 같습니다. (이 부분은 차후에 다시 정리해보도록 하겠습니다.)
torch.utils.data.Dataloader를 통해 생성한 dataloaders 정보들을 출력하면 아래와 같습니다.
4. GPU에 업로드 될 이미지 확인하기
앞서 "torch.utils.data.DataLoader"를 통해 이미지를 GPU에 올릴 준비를 끝냈으니, 어떤 이미지들의 GPU에 올라갈 지 확인해보겠습니다.
inputs에 어떤 정보들이 저장되어 있는지 알아보도록 하겠습니다.
inputs 데이터에는 batch 단위로 이미지들이 저장되어 있는 것을 확인할 수 있습니다.
앞서 dataset_sizes['train'] 에서 확인 했듯이, train 원본 이미지 개수는 ‘2683’개 입니다.
transformer.compose()를 통해 생성된 train 이미지 개수도 ‘2683’개 이다.
imshow를 통해 생성된 이미지를 봤을 때, data augmentation에 의해 데이터 수가늘어난게아니라, 본래 2683개의 데이터 자체내에서 augmentation이 적용된 것 같습니다. 다시 말해, training dataset의 (절대)개수는 data augmentation을 적용했다고 해서 늘어나는 것이 아닙니다. (imshow 함수는 이 글 제일 아래 있습니다)
결국, (inputs, classes) 가 CNN 입장에서 (학습 이미지 데이터, 라벨) 정보를 담고 있다고 볼 수 있겠네요.
하지만, 딥러닝 입장에서 중복된 데이터는 학습을 위해 큰 의미가 없을 가능성이 높습니다. 그래서 이미지A 5장이 있는것과, 이미지 A에 5가지 augmentation 적용된 것을 학습한다고 했을 때, 딥러닝 입장에서는 후자의 경우 더 많은 데이터가 있다고 볼 수 있습니다.
그래서 위와 같이 첫 번째 epoch의 첫 번째 iteration에서 잡힌 4개(batch) 이미지에 적용된 augmentation과, 두 번째 iteration에서 잡힌 4개(batch) 이미지에 적용된 augmentation과 다르기 때문에 결국 epoch을 여러 번 진행해주게되면, 다양한 조합으로 data augmentation이 적용되기 때문에 데이터수가 늘어났다고 볼 수 있게 됩니다.
그래서 이와 같은 방식에서 data augmentation을 적용해 (딥러닝 관점에서 봤을 때) 데이터 개수를 늘리려면 epoch을 늘려줘야합니다. (왜냐하면 앞서 transforms.compose에서 봤듯이 augmetnation이 적용될 때 augmetnation의 정도가 random or 확률적으로 적용되기 때문입니다.)
5. Upload to GPU (Feat. torch.cuda)
앞서 "torch.utils.data.DataLoader"를 통해 이미지를 GPU에 올릴 준비를 끝냈으니, 이번에는 실제로 GPU에 이미지를 올리는 코드를 소개하겠습니다.
Pytorch는 torch.cuda 패키지를 제공하여 pytorch와 CUDA를 연동시켜 GPU를 이용할 수 있게 도와줍니다.
CUDA가 정상적으로 pytorch와 연동이 되어 있는지 확인하기 위해서는 아래 코드로 확인해봐야 합니다.
torch.cuda: "torch.cuda" is used to set up and run CUDA operations.
It keeps track of the currently selected GPU, and all CUDA tensors you allocate will by default be created on that device. ← 아래 그림(코드)에서 설명
torch.cuda.is_avialable(): CUDA 사용이 가능 하다면 true값을 반환해 줍니다.
메인보드에는 GPU slot이 있습니다. 만약 slot이 4개가 있으면 4개의 GPU를 사용할 수 있게 됩니다. 해당 slot의 번호는 0,1,2,3 인데, 어느 slot에 GPU를 설치하느냐에 따라 GPU 넘버가 정해집니다. (ex: GPU 0 → 0번째 slot에 설치된 GPU)
cuda:0 이 의미하는 바는 0번째 slot에 설치된 GPU를 뜻합니다.
if 문에 의해 CUDA 사용이 가능하다면 torch.device("cuda:0")으로 세팅됩니다.
torch.device: "torch.device" contains a device type ('cpu' or 'cuda') and optional device ordinal for the device type.
torch.device("cuda:0") 을 실행하면 0번째 slot에 있는 GPU를 사용하겠다고 선언하게 됩니다.
device
train 함수에 있는 일부 코드를 통해 미리 device의 쓰임새를 말씀드리겠습니다.
앞서 "inputs"이라는 변수에 (batch_size, 3, 224, 224) 정보가 있다는 걸 확인할 수 있었습니다.
inputs 변수를 GPU에 올리기 위해 아래와 같은 "to(device)" 함수를 이용하면 됩니다. (한 가지 더 이야기 하자면 학습을 위해 딥러닝 모델도 GPU에 올려야 하는데, 이때도 "to(device)" 함수를 이용합니다. 이 부분은 나중에 model 부분에서 설명하도록 하겠습니다)
torch.cuda.get_device_name(device): 현재 사용하고 있는 GPU 모델명
3) 사용할 GPU 변경하기 (or 사용한 GPU 수동 설정하기)
torch.cuda.set_device(device): 사용할 GPU 세팅
"그림23"과 비교하면, 사용중인 GPU index가 0번으로 변경된 것을 확인 할 수 있습니다 ← 첫 번째 slot에 있는 GPU 사용
지금까지 설명한 내용이 pytorch에서 이미지 데이터를 로드할 때 필요한 내용들이었습니다.
최종코드는 아래와 같습니다.
# Data augmentation and normalization for training
# Just normalization for validation
data_transforms = {
'train': transforms.Compose([
transforms.RandomResizedCrop(224),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
]),
'val': transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
]),
}
data_dir = 'data/hymenoptera_data'
image_datasets = {x: datasets.ImageFolder(os.path.join(data_dir, x),
data_transforms[x])
for x in ['train', 'val']}
dataset_sizes = {x: len(image_datasets[x]) for x in ['train', 'val']}
class_names = image_datasets['train'].classes
dataloaders = {x: torch.utils.data.DataLoader(image_datasets[x], batch_size=4,
shuffle=True, num_workers=4)
for x in ['train', 'val']}
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
def imshow(inp, title=None):
"""Imshow for Tensor."""
inp = inp.numpy().transpose((1, 2, 0))
mean = np.array([0.485, 0.456, 0.406])
std = np.array([0.229, 0.224, 0.225])
inp = std * inp + mean
inp = np.clip(inp, 0, 1)
plt.imshow(inp)
if title is not None:
plt.title(title)
plt.pause(0.001) # pause a bit so that plots are updated
# Get a batch of training data
inputs, classes = next(iter(dataloaders['train']))
# Make a grid from batch
out = torchvision.utils.make_grid(inputs)
imshow(out, title=[class_names[x] for x in classes])