[Impl. from Scratch Ep. 1] Dataset

__chyun__·2024년 3월 9일

사용하는 데이터셋의 구조를 파악하고, 학습 및 검증을 위해 Dataset 클래스를 구축하자!

Dataset Description

본 프로젝트에서는 이전에 리뷰했던 Anticipating Accidents in Dashcam Videos에서 구축한 데이터셋을 사용한다. 자율주행 등을 위해 주로 사용하는 Cityscapes, KITTI 데이터셋과 비교했을 때 복잡한 표지판, 더욱 많은 오브젝트 수, 다양한 사고 시나리오를 포함하고 있다는 점에서 장점을 가진다.

데이터셋 구성은 다음과 같다.

PositiveNegativeTotal
Training set4558291284
Testing set165301466
Total62011301730

추가적으로 앞서 언급한 논문에서는 데이터셋 외 추가로 수집된 데이터 일부에 대해 수동으로 annotation을 생성하여 구축한 데이터를 이용해 Faster R-CNN 모델을 자체적으로 학습시킨다. 이를 이용해 데이터셋에 포함된 비디오에 대한 Object Detection 결과 파일도 함께 제공한다. 다만 학습 데이터가 58개의 비디오로 구성되어 다소 적은 데이터로 훈련되었다는 점과 논문이 출간된 시점이 2016년이라는 점을 감안하여, YOLO 등 최신 Object Detection 모델들을 사용하는 방향도 함께 고려해보기로 했다.

TODO: Faster R-CNN 정리하기

Dataset Structure

앞에서 언급한 데이터셋의 구조는 다음과 같다. 학습과 검증으로 그리고 그 중에서도 positive와 negative 데이터로 분리되어 저장되어 있다.

Dashcam_dataset
└ videos
	└ training
    	└ positive
        	- 000001.mp4
            ...
            - 000455.mp4
        └ negative
        	- 000001.mp4
            ...
            - 000829.mp4
    └ testing
    	└ positive
        	- 000456.mp4
            ...
            - 000620.mp4
        └ negative
        	- 000830.mp4
            ...
            - 001130.mp4
    

Custom Dataset

이제 본격적으로 Dataset 클래스의 기본 틀을 잡고 시작해보자. torch가 정의하는 Dataset 클래스를 상속받아 우리에게 필요한 커스텀 데이터셋 클레스인 DashcamDataset을 구축하는 과정이다. 우리가 원하는 형태로 정의할 수 있다고 하더라도, 세 가지의 메소드를 필수적으로 가져야 한다.

import torch
from torch.utils.data import Dataset, DataLoader

class DashcamDataset(Dataset):
	def __init__(self, ...):
    	pass
	
    def __len__(self):
    	pass
    
    def __getitem__(self, idx):
    	pass
    
  1. __init__ : Dataset 클래스를 정의하고 초기화하는 부분으로, 데이터셋 내에서 지속적으로 사용하는 데이터 경로, Transform 함수 등을 정의한다.
  2. __len__ : 데이터셋의 길이를 반환하는 함수. 일반적으로 __init__ 메소드에서 정의한 객체변수(attribute)들 중 전체 데이터셋에 대한 경로 정보나 데이터 정보를 담고 있는 리스트의 길이를 반환한다.
  3. __getitem__ : 실제로 학습 파이프라인 상에서 데이터를 순차적으로 가져올때마다 인덱스에 해당하는 데이터를 원하는 형태로 반환하는 메소드다.

이외에도 추가적으로 필요한 함수가 있다면 추가적인 메소드로 정의해서 사용할 수 있다. 각 메소드에서 필요한 요소를 하나씩 채워나가보자.


__init__ method

데이터셋 기본 정보 가져오기

데이터셋에서 비디오를 읽어오기 위해서는 각 비디오가 저장된 경로를 알아야 한다. 위의 데이터셋 구조를 참고해서, Dataset 클래스가 사용자로부터 받아야 하는 argument가 무엇인지, 그리고 이를 이용해 데이터셋 파일 경로를 어떻게 저장할지 생각해보자.


Arguments (인자)

class DashcamDataset(Dataset):
  def __init__(self, 
               path, 
               train=True,
               video_res=[720, 1280], 
               transform=None, 
               ):
  • train bool
    먼저, 데이터셋을 불러와서 어떤 용도로 사용할지 알아야 한다. 지금 불러와야 할 데이터는 학습을 위한 것인가, 테스트를 위한 것인가? 이 인자의 값이 True라면 training, False라면 testing 모드로 동작하게 한다.
  • path str
    불러와야 하는 데이터셋이 무엇인지 알았다면, 최상위 디렉토리인 DashcamDataset이 어떤 위치에 저장되어 있는지 알아야 한다. 그로부터 시작해 하위 폴더와 비디오를 읽어올 수 있다. 최상위폴더의 위치를 받아온다.
  • video_res list
    원하는 비디오의 resolution을 [H, W] 리스트로 받는다.
  • transform callable, optional
    비디오에 적용할 다양한 augmentation, resizing 등의 transform을 실행하는 하나의 함수 형태로 받는다. 나중에 __getitem__ 메소드 내에서 이를 활용해 비디오를 처리한 후 반환한다.

Attributes (속성)

앞에서 받아온 인자를 이용해 필요한 속성을 정의해본다. 먼저, 비디오 경로를 저장해보자. 최상위 폴더 위치에서 총 두 번의 갈림길이 있다.

STEP 1. training / testing

if train == True:
    self.base_path = os.path.join(path, 'videos', 'training')
else: 
    self.base_path = os.path.join(path, 'videos', 'testing')

여기서 두 디렉토리 중 하나만을 base_path로 사용한다. os.path.join 함수를 통해 학습 또는 테스트 데이터에 접근하는 기본 디렉토리로 저장한다.

예를 들어 최상위 디렉토리 경로가 c:/data/Dashcam_dataset이라면, 학습 데이터를 불러오는 경우 self.base_pathc:/data/Dashcam_dataset/videos/training이 된다.


STEP 2. positive / negative

기본 디렉토리가 결정되면, 그 내부에 있는 positive와 negative 데이터 모두를 사용한다. 또한, 학습 시에는 데이터를 무작위로 셔플에서 사용하는 경우가 있다. 따라서 두 디렉토리 내의 비디오 정보를 모두 담은 하나의 리스트가 필요하다.

하나의 리스트를 만들며 발생했던 문제

우리가 사용하는 데이터셋을 자세히 살펴보면 다음과 같은 문제가 생긴다.

Training 데이터에서

첫 번째 positive 비디오의 경로:
./Dashcam_dataset/videos/training/positive/000001.mp4

첫 번째 negative 비디오의 경로:
./Dashcam_dataset/videos/training/negative/000001.mp4

각각을 os.path.listdir 함수를 사용하면 인자로 넘겨주는 디렉토리 내의 파일명만을 담은 리스트가 반환된다. 위에서 볼 수 있듯 positive와 negative 디렉토리 내의 비디오 이름이 겹치기 때문에, 이를 하나의 리스트로 만들면 나중에 각 비디오가 어디에서 왔는지 구분할 수 없다는 문제가 생긴다.

즉, GT (ground truth) 정보를 얻으려면 각 비디오가 어떤 디렉토리 내에 있는지도 알고 있어야 한다. 이를 위해 다양한 방법이 존재하지만, 각 리스트에 최상위 폴더로부터의 전체 경로를 모두 저장하는 방법을 사용한다. 나중의 사용성을 고려하여 positive, negative, positive+negative의 세 가지 리스트에 각각에 속하는 비디의 경로를 모두 저장한다.

이와 같이 커스텀 데이터셋을 만들 때는, 사용하고 있는 데이터셋의 특성을 잘 파악하고 구현해야 문제가 발생하지 않는다.

코드 상으로는 다음과 같다.

# positive, negative 디렉토리까지의 경로
self.positive_path = os.path.join(self.base_path, 'positive')
self.negative_path = os.path.join(self.base_path, 'negative')

# positive, negative 각각의 비디오 경로를 담은 리스트
self.positive_videos = [os.path.join(self.positive_path, v) for v in sorted(os.listdir(self.positive_path))]
self.negative_videos = [os.path.join(self.negative_path, v) for v in sorted(os.listdir(self.negative_path))]

# 두 개의 리스트를 합쳐 모든 비디오 경로를 가지고 있는 리스트
self.video_paths = self.positive_videos + self.negative_videos

STEP 3. 추가 정보 저장하기

self.video_res = video_res  # list: [H, W]
self.transform = transform

비디오 resolution, transform 등 추가적인 정보도 추가한다. 프로젝트를 진행하며 수정될 수 있다.


끝!🙂

최종 __init__ 메소드는 다음과 같이 정의된다.

class DashcamDataset(Dataset):
  def __init__(self, 
               path, 
               train=True,
               video_res=[720, 1280], 
               transform=None, 
               ):
               
    self.base_path = os.path.join(path, 'videos', 'training') if train==True else os.path.join(path, 'videos', 'testing')
    
    self.positive_path = os.path.join(self.base_path, 'positive')
    self.negative_path = os.path.join(self.base_path, 'negative')

    self.positive_videos = [os.path.join(self.positive_path, v) for v in sorted(os.listdir(self.positive_path))]
    self.negative_videos = [os.path.join(self.negative_path, v) for v in sorted(os.listdir(self.negative_path))]

    self.video_paths = self.positive_videos + self.negative_videos

    self.video_res = video_res  # list: [H, W]

    self.transform = transform

__len__ method

  def __len__(self):
    return len(self.video_paths)

이번 메소드는 추가 인자 없이 간단하다. 앞의 __init__ 메소드에서 정의한 속성 중 모든 비디오 경로를 답고 있는 self.video_paths 리스트의 길이를 반환한다.


__getitem__ method

Argument

idx: 학습을 할 때 뒤에 소개할 DataloaderDataset으로부터 idx를 통해 순차적으로 데이터를 가져온다. 우리는 이 인자를 사용해 적절한 데이터를 찾아 반환해야 한다. 데이터셋 내의 모든 비디오 경로가 저장되어 있는 self.video_paths의 인덱스로 사용하여 데이터를 가져오면 된다. 여기서는 1) 비디오의 경로, 2) torchvision.io.read_video를 이용해 불러온 비디오, 3) 이 비디오가 positive인지 negative인지 구분하는 레이블을 반환하도록 한다.

def __getitem__(self, idx):
  video_path = self.video_paths[idx]
  video = torchvision.io.read_video(video_path, output_format = 'THWC')[0]
  label = 1 if video_path in self.positive_videos else 0

  return video_path, video, label

이렇게 DashcamDataset 구현이 끝났다! 나중에 이렇게 구성한 커스텀 데이터셋에서 데이터를 불러오는 Dataloader를 정의해 사용할 수 있다. 학습/검증 코드를 작성하며 등장할 예정이다.

다음 글에서는 본격적으로 모델을 구현해보도록 하자.🙂

0개의 댓글