TorchText Migration

GRoovAllstar·2023년 4월 17일
0

Dataset 객체 생성

Legacy

  • Field 클래스를 이용하여 dataset 객체를 생성함
  • 호출한 dataset의 여러 parameter를 통한 처리 과정이 있음
    • sequential: 순차 데이터 여부
    • use_vocab: Vocab 개체 사용 여부
    • tokenize: 사용될 토큰화 함수
    • batch_first: 미니 배치 차원을 맨 앞으로 하여 데이터를 불러올 것인지 여부
    • fix_length: 최대 허용 길이. 이 길이에 맞춰 패딩(Padding) 작업 진행
    • pad_token: 기본 패딩 토큰. default: <pad>
    • unk_token: OOV(Out Of Vocabulary) words. default: <unk>
from torchtext.legacy import data, datasets

TEXT = data.Field(sequential=True, batch_first=True, lower=True)
LABEL = data.Field(sequential=False, batch_first=True)

train_set, test_set = datasets.IMDB.splits(TEXT, LABEL)

New

  • 전처리 정보 없이 dataset에서 직접 분할된 학습/테스트 데이터세트를 반환
  • 분할된 DataPipe는 label과 text로 구성된 yield tuple 객체
from torchtext.datasets import IMDB
train_iter, test_iter = IMDB(root='.data', split=('train', 'test'))

Building Vocab

Legacy

  • build_vocab 을 이용하여 단어 집합 생성
    • min_freq : 5번 이상 등장한 단어를 단어 집합에 추가. 5번 미만으로 등장한 단어는 <unk> 토큰으로 대체됨
TEXT.build_vocab(train_set, min_freq=5)
LABEL.build_vocab(train_set)

print(TEXT.vocab.stoi)
>>> defaultdict(..., {'<unk>': 0, '<pad>': 1, 'the': 2, ... })

New

  • dataset의 tuple로 부터 iterator를 지정하여 vocab 생성
    • min_freq, specials token 지정 가능
from torchtext.data.utils import get_tokenizer
from torchtext.vocab import build_vocab_from_iterator

tokenizer = get_tokenizer('basic_english')
def yield_tokens(data_iter):
    for _, text in data_iter:
        # key= '1 or 2 or 3...', value : 'text sequence'
        yield tokenizer(text)  # 토큰화된 text 리턴

vocab = build_vocab_from_iterator(
    iterator=yield_tokens(train_iter), # must yield list or token iterator.
    min_freq=5,
    specials=['<unk>'],) # <unk> token을 지정
vocab.set_default_index(vocab['<unk>'])  # oov(out of vocabulary) 일때 반환되는 토큰

print((('<unk>') in vocab), (('<pad>') in vocab))
>>> True False
  • legacy 인터페이스와 달리 specials에 ‘<pad>’ token을 지정하지 않았다면 vocab 객체에 추가되지 않음

Batch Iterator 생성

Legacy

  • dataset 자체 분할 interface 지원
  • BucketIterator 객체를 통해 dataset를 batch size만큼 구성함
train_set, valid_set = train_set.split(split_ratio=0.8)
train_iter, val_iter, test_iter = data.BucketIterator.splits(
        (train_set, valid_set, test_set), batch_size=32,
        shuffle=True, repeat=False)

New

  • 별도의 util 함수를 통해 dataset의 iterator를 분할할 수 있음
  • 분할된 iterator를 torch에서 사용하는 DataLoader 객체에 연결함
    • 학습/검증 data를 분할 했으면 각자 DataLoader를 사용함
from torch.utils.data import random_split

def train_valid_split(train_iterator, split_ratio=0.8, seed=42):
    train_count = int(split_ratio * len(train_iterator))
    valid_count = len(train_iterator) - train_count
    generator = torch.Generator().manual_seed(seed)
    train_set, valid_set = random_split(
        train_iterator, lengths=[train_count, valid_count], generator=generator)
    return train_set, valid_set 

# iterable type에서 map style로 변환해야 length check 가능
train_iter = to_map_style_dataset(train_iter)
train_set, valid_set = split_train_valid(train_iter)

train_dataloader = DataLoader(
    train_set, batch_size=64, shuffle=True, collate_fn=collate_batch)
valid_dataloader = DataLoader(
    valid_set, batch_size=64, shuffle=True, collate_fn=collate_batch)
  • DataLoader의 collate_fn를 통해 mini batch의 data sample을 조합할 수 있음
    • data 가공
    • mini batch 차원 변경
    • Tensor type으로 변환
  • Legacy의 Field 객체를 통해 수행했던 작업들을 collate_fn 내부에서 수행할 수 있음
    • vocab build 시 추가했던 special token들을 text 문장에 추가
    • padding value 추가
    • batch_first parameter와 같은 Tensor 차원 변경
# 데이터 변환
text_pipeline = lambda x: vocab(tokenizer(x))
label_pipeline = lambda x: int(x)
'''
# dataset 형태에 따라 아래 방식처럼 생성 가능
text_pipeline = lambda x: \
    [vocab['<BOS>']] + [vocab[token] for token in tokenizer(x)] + [vocab['<EOS>']]
label_pipeline = lambda x: 1 if x == 'pos' else 0
'''
def collate_batch(batch):
    label_list, text_list = [], []
    for (_label, _text) in batch:
        label_list.append(label_pipeline(_label))
        processed_text = torch.tensor(text_pipeline(_text), dtype=torch.int64)
        text_list.append(processed_text)
    label_list = torch.tensor(label_list, dtype=torch.int64)
    text_tensor = pad_sequence(text_list, padding_value=1, batch_first=True)
    return text_tensor, label_list

학습/평가

Legacy

  • 분할된 iterator 객체를 학습/평가 로직에 사용함
def train(model, optimizer, train_iter):
    model.train()
    for batch in train_iter:
        x, y = batch.text.to(DEVICE), batch.label.to(DEVICE)
        y.data.sub_(1) # <unk>:0 인 token 값 제거
        optimizer.zero_grad()
        logit = model(x)
        loss = F.cross_entropy(logit, y)
        loss.backward()
        optimizer.step()

def evaluate(model, valid_iter):
    model.eval()
    corrects, total_loss = 0, 0
    for batch in valid_iter:
        x, y = batch.text.to(DEVICE), batch.label.to(DEVICE)
        y.data.sub_(1) # <unk>:0 인 token 값 제거
        logit = model(x)
        loss = F.cross_entropy(logit, y, reduction='sum')
        total_loss += loss.item()
        corrects += (logit.max(1)[1].view(y.size()).data == y.data).sum()
    size = len(valid_iter.dataset)
    avg_loss = total_loss / size
    avg_accuracy = 100.0 * corrects / size
    print(total_loss, 100.0 * corrects, size)
    return avg_loss, avg_accuracy

for epoch in range(1, EPOCHS+1):
    train(model, optimizer, train_iter)
    val_loss, val_accuracy = evaluate(model, val_iter)
    print("[Epoch: %d] val loss : %5.2f | val accuracy : %5.2f" % (
        epoch, val_loss, val_accuracy))

New

  • torch의 DataLoader의 객체를 학습 및 평가에 사용함
    • DataLoader의 collate_fn를 통해 Tensor에 대한 작업이 미리 가능함
      • Tensor 변수를 장치로 전달
      • label data에 <unk> token 제거 처리
  • 평가 시 전체 Loss 계산은 DataLoader 개수로 판단할 수 없음
    • DataLoader를 순회할 때의 개수는 mini batch size이므로 dataset를 분리할 때 valid data로 사용한 개수에 대해 전체 Loss를 계산해야함
def train(model, optimizer, train_iter):
    model.train()
    for x, y in train_iter:
        x, y = x.to(DEVICE), y.to(DEVICE)
        y.sub_(1) # <unk>:0 인 token 값 제거
        optimizer.zero_grad()

        logit = model(x)
        loss = F.cross_entropy(logit, y)
        loss.backward()
        optimizer.step()

def evaluate(model, valid_iter, total_valid_set_len):
    model.eval()
    corrects, total_loss, total_count = 0, 0, 0
    for x, y in valid_iter:
        x, y = x.to(DEVICE), y.to(DEVICE)
        y.sub_(1) # <unk>:0 인 token 값 제거
        logit = model(x)
        loss = F.cross_entropy(logit, y, reduction='sum')
        total_loss += loss.item()
        corrects += (logit.max(1)[1].view(y.size()).data == y.data).sum()
    
    size = total_valid_set_len
    avg_loss = total_loss / size
    avg_accuracy = 100.0 * corrects / size
    print(total_loss, 100.0 * corrects, size)
    return avg_loss, avg_accuracy

for epoch in range(1, EPOCHS+1):
    train(model, optimizer, train_dataloader)
    with torch.no_grad():
        val_loss, val_accuracy = evaluate(model, val_dataloader, len(val_set))
    print("[Epoch: %d] val loss : %5.2f | val accuracy : %5.2f" % (
        epoch, val_loss, val_accuracy))

Migration Code : https://github.com/groovallstar/pytorch_rnn_tutorial/blob/main/8_2_torchtext_migration.ipynb

profile
Keep on eye on the future :)

0개의 댓글