TORCHNLP.SAMPLERS 패키지 파훼하기

데이터 샘플링에 활용되는 Sampler 예제 학습하기

Posted by devfon on June 2, 2020

PyTorch의 서드 파티인 torchnlp 라이브러리에는 데이터 샘플링에 활용되는 다양한 샘플러 클래스를 지원하고 있습니다. 샘플러는 데이터셋으로부터 각각의 샘플들을 어떠한 방식으로 내줄 것인지에 대해 정의하는 클래스입니다. 공식 문서에서 소개하고 있는 샘플러 클래스를 살펴보며, 각각의 샘플러가 어떠한 기능을 제공해주고 있는지 살펴보도록 하겠습니다.

RepeatSampler

1
torchnlp.samplers.RepeatSampler(sampler)

기구현된 샘플러를 영원히 반복하는 Wrapper 샘플러입니다.

기존 DataLoader는 한 epoch이 끝나면 데이터를 새로 읽어오기 위해 새로운 프로세스를 생성해야만 했습니다. 따라서 기존 프로세스가 데이터베이스 연결 혹은 인덱싱과 같은 내부 상태에 대한 캐시를 가지고 있는 상태였다면, epoch이 끝남에 따라 해당 캐시 정보들은 모두 사라지고 정보 재생성을 위해 오버헤드를 발생시켜야만 했습니다.

RepeatSampler는 이러한 DataLoader의 재사용 문제를 해결한 Wrapper 클래스이며, 더 자세한 내용은 이슈를 참고하시는 것이 좋습니다.

인자:

  • sampler (torch.data.utils.sampler.Sampler): PyTorch Sampler 클래스


SortedSampler

1
torchnlp.samplers.SortedSampler(data, sort_key=<function identity>)

키 함수에 의해 정렬된 리스트를 항상 동일한 순서의 시퀀스 원소로 샘플링해줍니다.

인자:

  • data (iterable): 이터러블 데이터
  • sort_key (callable): 리스트 내 원소를 정렬할 기준이 될 수 있는 키 함수
1
2
3
>>> from torchnlp.samplers import SortedSampler
>>> list(SortedSampler(range(10), sort_key=lambda i: -i))
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]


DeterministicSampler

1
torchnlp.samplers.DeterministicSampler(sampler, random_seed, cuda=False)

샘플러의 랜덤 스테이트를 유지해 매번 동일한 결과 값을 반환하도록 합니다.

인자:

  • sampler (torch.data.utils.sampler.Sampler): PyTorch Sampler 클래스
  • random_seed (int): 랜덤 시드


BalancedSampler

1
torchnlp.samplers.BalancedSampler(data_source, get_class=<function identity>, get_weight=<function BalancedSampler.<lambda>>, **kwargs)

데이터 인스턴스의 클래스에 따라 가중치를 적용한 샘플링을 해줍니다.

인자:

  • data (iterable): 이터러블 데이터
  • get_class (callable, optional): 각 원소의 클래스를 반환해주는 함수
  • get_weight (callable, optional): 각 원소에 적용할 가중치 함수
  • kwargs: WeightedRandomSampler에 활용될 추가 키워드 인자 e.g.) num_samples
1
2
3
4
5
6
>>> from torchnlp.samplers import BalancedSampler, DeterministicSampler
>>> data = ['a', 'b', 'c'] + ['c'] * 100
>>> sampler = BalancedSampler(data, num_samples=3)
>>> sampler = DeterministicSampler(sampler, random_seed=12)
>>> [data[i] for i in sampler]
['c', 'b', 'a']

위 예를 보시면 일반적인 샘플링에서는 c가 압도적으로 많이 나와야 하지만, BalancedSampler에서는 각 클래스 별 인스턴스를 활용한 개별 가중치가 계산된 후, 전체 클래스 수를 활용해 return iter(torch.multinomial(self.weights, self.num_samples, self.replacement).tolist()) 와 같이 torch.multinomial이 적용되므로 다항 분포에서 샘플링 된 원소들의 인덱스가 반환됩니다. 아래는 개별 가중치가 계산되는 로직입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
# 각 인스턴스의 클래스를 기록한 리스트 저장
classified = [get_class(item) for item in data_source]  # [a, b, c, c, c, ...]

# 각 인스턴스의 가중치 함수 적용: 디폴트는 1 반환
weighted = [float(get_weight(item)) for item in data_source]  # [1, 1, 1, 1, ...]

# 두 리스트를 돌며, 각 클래스 별 가중치의 합 계산
class_totals = {
	k: sum([w for c, w in zip(classified, weighted) if k == c]) for k in set(classified)
}  # {a: 1, b: 1, c: 101}

# 가중치 합을 활용해 개별 가중치 리스트 저장
weights = [w / class_totals[c] if w > 0 else 0.0 for c, w in zip(classified, weighted)]  # [1.0, 1.0, 0.009, 0.009, ...]


DistributedSampler

1
torchnlp.samplers.DistributedSampler(iterable, num_replicas=None, rank=None)

여러 개의 worker에 걸쳐 사용될 수 있도록 이터러블을 배분해줍니다.

인자:

  • iterable (iterable): 이터러블 데이터
  • num_replicas (int, optional): 병렬 훈련에 사용될 프로세스의 수
  • rank (int, optional): 현재 프로세스의 랭크 cf) num_replicas보다 작아야 함
1
2
3
4
>>> list(DistributedSampler(range(10), num_replicas=2, rank=0))
[0, 2, 4, 6, 8]
>>> list(DistributedSampler(range(10), num_replicas=2, rank=1))
[1, 3, 5, 7, 9]


DistributedBatchSampler

1
torchnlp.samplers.DistributedBatchSampler(batch_sampler, **kwargs)

여러 개의 worker에 걸쳐 사용될 수 있도록 BatchSampler배분해줍니다.

인자:

  • batch_sampler (torch.utils.data.sampler.BatchSampler): : PyTorch BatchSampler 클래스
  • num_replicas (int, optional): 병렬 훈련에 사용될 프로세스의 수
  • rank (int, optional): 현재 프로세스의 랭크 cf) num_replicas보다 작아야 함
1
2
3
4
5
6
7
8
9
10
>>> from torchnlp.samplers import DistributedBatchSampler
>>> from torch.utils.data.sampler import SequentialSampler, BatchSampler
>>> sampler = SequentialSampler(list(range(12)))
>>> batch_sampler = BatchSampler(sampler, batch_size=4, drop_last=False)
>>> list(batch_sampler)
[[0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10, 11]]
>>> list(DistributedBatchSampler(batch_sampler, num_replicas=2, rank=0))
[[0, 2], [4, 6], [8, 10]]
>>> list(DistributedBatchSampler(batch_sampler, num_replicas=2, rank=1))
[[1, 3], [5, 7], [9, 11]]


BPTTSampler

1
torchnlp.samplers.BPTTSampler(data, bptt_length, type_='source')

bptt_length 길이만큼 소스와 타겟 시퀀스를 슬라이스해 샘플링해줍니다. 주로 Language Modeling 태스크를 훈련시킬 때 활용됩니다.

Truncated backpropagation은 시계열 모델을 훈련시키기 위한 실용적인 기법입니다.

BPTT의 가장 큰 문제 중 하나는 하나의 파라미터를 업데이트 하기 위해 너무 많은 비용이 든다는 것입니다. 그리고 해당 문제는 긴 시퀀스에 대해 많은 이터레이션이 불가능하게 만드는 요소로 작용하게 됩니다.

이러한 훈련 비용은 단순히 1,000개의 토큰으로 구성된 긴 시퀀스를 각각 20개 토큰으로 구성된 50개의 시퀀스로 분할한 후, 각각의 시퀀스를 독립된 훈련 인스턴스로 취급함으로써 줄일 수 있습니다. 이는 실제로 꽤나 잘 동작하는 접근법이지만, 20개 타임 스텝을 넘어가는 순간 토큰 간 의존성을 학습하지 못한다는 단점이 있습니다.

인자:

  • data (iterable): 이터러블 데이터
  • bptt_length (int): 슬라이스 길이
  • type (str, optional): 슬라이스 타입 (source 혹은 target) cf. 타겟 슬라이스는 right_shifted를 고려해야 함으로 존재하는 옵션
1
2
3
>>> from torchnlp.samplers import BPTTSampler
>>> list(BPTTSampler(range(5), 2))
[slice(0, 2, None), slice(2, 4, None)]


BPTTBatchSampler

1
torchnlp.samplers.BPTTBatchSampler(data, bptt_length, batch_size, drop_last, type_='source')

bptt_length 길이만큼 소스와 타겟 시퀀스를 배치 단위로 슬라이스해 샘플링해줍니다. 마찬가지로 주로 Language Modeling 태스크를 훈련시킬 때 활용됩니다.

인자:

  • data (iterable): 이터러블 데이터
  • bptt_length (int): 슬라이스 길이
  • batch_size (int): 미니 배치의 사이즈
  • drop_last (bool): 참이라면, 마지막에 위치하게 될 Non-Full 배치를 활용하지 않음
  • type (str, optional): 슬라이스 타입 (source 혹은 target) cf. 타겟 슬라이스는 right_shifted를 고려해야 함으로 존재하는 옵션
1
2
3
4
5
6
7
8
9
>>> from torchnlp.samplers import BPTTBatchSampler
>>> sampler = BPTTBatchSampler(range(100), bptt_length=2, batch_size=3, drop_last=False)
>>> # (0 ~ 33), (34 ~ 66), (67 ~ 99)이 각 배치 버킷
>>> list(sampler)[0]  # First Batch
[slice(0, 2, None), slice(34, 36, None), slice(67, 69, None)]
>>> list(sampler)[1]  # Second Batch
[slice(2, 4, None), slice(36, 38, None), slice(69, 71, None)]
>>> list(sampler)[2]  # Third Batch
[slice(4, 6, None), slice(38, 40, None), slice(71, 73, None)]


BucketBatchSampler

1
torchnlp.samplers.BucketBatchSampler(sampler, batch_size, drop_last, sort_key=<function identity>, bucket_size_multiplier=100)

BucketBatchSampler는 기본적으로 배치 단위 샘플링소팅 샘플링을 함께 활용하는 샘플러입니다. 자세한 내용은 아래 예제를 살펴보시면 됩니다.

BucketBatchSamplerAllenNLPtorchtextBucketIterator와 유사한 샘플러입니다. BucketIterator의 경우, 배치 내 불필요한 패딩 작업을 줄이기 위해 비슷한 길이의 인스턴스들을 모아 버켓팅을 수행합니다.

인자:

  • sampler (torch.data.utils.sampler.Sampler): PyTorch Sampler 클래스
  • batch_size (int): 미니 배치의 사이즈
  • drop_last (bool): 참이라면, 마지막에 위치하게 될 Non-Full 배치를 활용하지 않음
  • sort_key (callable, optional): 리스트 내 원소를 정렬할 기준이 될 수 있는 키 함수
1
2
3
4
5
6
7
8
>>> from torchnlp.random import set_seed
>>> from torchnlp.samplers import BucketBatchSampler
>>> from torch.utils.data.sampler import SequentialSampler, BatchSampler
>>> set_seed(123)
>>> list(BucketBatchSampler(SequentialSampler(list(range(10))), batch_size=3, drop_last=False))
[[6, 7, 8], [0, 1, 2], [3, 4, 5], [9]]
>>> list(BatchSampler(SequentialSampler(range(10)), batch_size=3, drop_last=False))
[[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]]

위 예제를 보시면 BatchSampler의 경우, 0 부터 9까지 [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]]과 같이 정렬된 형태로 batchify가 진행됩니다. 그러나 BucketBatchSampler의 경우 배치 내부적으로는 정렬되었지만, 각각의 배치는 랜덤하게 분포된 상태로 샘플링이 진행됩니다.


NoisySortedSampler

1
torchnlp.samplers.NoisySortedSampler(data, sort_key=<function identity>, get_noise=<function _uniform_noise>)[source]

원소들에 노이즈를 가미해 시퀀스 형태로 샘플링해줍니다.

인자:

  • data (iterable): 이터러블 데이터
  • sort_key (callable): 리스트 내 원소를 정렬할 기준이 될 수 있는 키 함수
  • get_noise (callable): sort_key에 적용될 노이즈 함수
1
2
3
4
5
6
7
>>> import random
>>> from torchnlp.random import set_seed
>>> from torchnlp.samplers import NoisySortedSampler
>>> set_seed(123)
>>> get_noise = lambda i: round(random.uniform(-1, 1))
>>> list(NoisySortedSampler(range(10), sort_key=lambda i: i, get_noise=get_noise))
[0, 1, 2, 3, 5, 4, 6, 7, 9, 8]

get_noise 함수가 적용되는 과정은 아래와 같습니다. 기존의 sort_key 함수만 활용하는 SortedSampler와 달리 NoisySortedSamplerget_noise 함수를 함께 활용하기 때문에 노이즈를 적용한 정렬 시퀀스를 샘플링 할 수 있게 됩니다.

1
2
3
4
5
6
7
8
9
10
def __iter__(self):
  zip_ = []
  # sort_key 함수 값과 get_noise 함수 값을 더한 value를 지정합니다.
  for i, row in enumerate(self.data):
    value = self.get_noise(row) + self.sort_key(row)
    zip_.append(tuple([i, value]))
  # 앞서 구해진 value를 기준으로 정렬을 수행합니다.
  zip_ = sorted(zip_, key=lambda r: r[1])
  # value 기준으로 정렬된 시퀀스를 반환합니다.
  return iter([item[0] for item in zip_])

지금까지 torchnlp 라이브러리가 제공하는 여러 Sampler 클래스에 대해 알아보았습니다. 앞으로 여러 샘플링 전략을 위해 이처럼 기구현된 Sampler를 활용할 수도 있고, 자신이 직접 커스터마이즈한 Sampler 클래스를 활용할 수도 있겠습니다!