본문 바로가기

책/파이토치 트랜스포머를 활용한 자연어 처리와 컴퓨터비전 심층학습

07 트랜스포머 (6) T5

 

위 그림은 가장 대표적인 트랜스포머 아키텍처를 보여준다. 지금까지 공부한 디코더 기반의 GPT, 인코더 기반의 BERT, ELECTRA, 그리고 seq2seq 구조의 BART를 확인할 수 있다. 

 

T5(Text-to-Text Transfer Transformer)는 2019년 구글에서 발표한 트랜스포머 구조를 기반으로한 모델이다.

 

T5는 GLUE, SuperGLUE, CNN/DM(Cable News Network/Daily Mail) 등에서 SOTA를 달성했으며 다양한 자연어 처리 작업에서 높은 성능을 보이는 모델이다.

 

T5는 입출력을 모두 토큰 시퀀스로 처리하는 Text-to-Text 구조다. 따라서 입출력의 형태를 자유롭게 다룰 수 있으며, 모델 구조상 유연성과 확장성이 뛰어나기 때문에 새로운 자연어 처리 작업에서도 쉽게 적용할 수 있다. 이러한 구조를 사용하면 여러 NLP 작업에서 동일한 모델, 손실 함수, 하이퍼 파라미터를 사용할 수 있다.

 

T5 모델 학습 유형

 

대표적인 학습 작업으로 문장 번역, 요약, 질의응답, 텍스트 분류 등이 있다.

 

사전 학습은 C4(Colossal Clean Crawled Corpus) 데이터세트를 활용했는데, 이는 규모는 크지만 데이터의 품질이 낮은 Common Crawl 데이터세트를 연구진이 정제한 것이다. 적용된 정제 과정은 완성되지 않은 문장 제외, 중복 데이터 제외, offensive 혹은 noisy 데이터 제외 등.

 

 

사전 학습

사전 학습 방식은 비지도 학습 방식으로 입력 문장의 일부 구간을 마스킹해 입력 시퀀스를 처리하며, 출력 시퀀스는 실제 마스킹된 토큰과 마스크 토큰의 연결로 구성된다.

 

이때 문장마다 유일한 마스크 토큰을 의미하는 센티널 토큰(Sentinel Token)이 사용된다. 센티널 토큰은 <extra_id_0>, <extra_id_1>과 같이 0~99까지 100개의 기본값을 사용한다.

 

예를 들어 '인코더-디코더 모델 구조'라는 문장에서 '인코더'와 '디코더'를 마스킹해 처리하는 경우

입력 토큰의 센티널 토큰 : <extra_id_0>, -, <extra_id_1>, 모델 구조

출력 토큰 : 인코더, <extra_id_0>, 디코더, <extra_id_1>

 

T5는 이러한 마스킹 토큰을 예측하는 것을 목적으로 사전 학습되며, 사전 학습이 완료된 후 미세 조정은 지도 학습 방식으로 학습된다. 미세 조정을 할 때는 문장이 인코딩 되게 전에 'translate English to German:' 또는 'summerize:'와 같은 작업 토큰을 문장 앞에 추가한다. 이러한 방식은 작업 토큰도 함께 학습해 다양한 NLP 작업에서 높은 성능을 발휘할 수 있게 한다.

 

 

모델 실습

허깅페이스의 T5 인코더-디코더 모델을 학습해 문장 요약 작업을 수행해본다.

 

!pip install datasets sentencepiece transformers[sentencepiece] -U

 

datasets : 허깅페이스에서 제공하는 데이터셋 관련 라이브러리

sentencepiece : Google에서 개발한 텍스트 토크나이저 라이브러리

transformers[sentencepiece] : transformers 라이브러리에 옵션으로 sentencepiece 토크나이저를 함께 설치

 

 

뉴스 요약 데이터셋 불러오기

import numpy as np
from datasets import load_dataset


news = load_dataset("argilla/news-summary", split="test")
df = news.to_pandas().sample(5000, random_state=42)[["text", "prediction"]]
df["text"] = "summarize: " + df["text"]
df["prediction"] = df["prediction"].map(lambda x: x[0]["text"])
train, valid, test = np.split(
    df.sample(frac=1, random_state=42), [int(0.6 * len(df)), int(0.8 * len(df))]
)

print(f"Source News : {train.text.iloc[0][:200]}")
print(f"Summarization : {train.prediction.iloc[0][:50]}")
print(f"Training Data Size : {len(train)}")
print(f"Validation Data Size : {len(valid)}")
print(f"Testing Data Size : {len(test)}")
Source News : summarize: DANANG, Vietnam (Reuters) - Russian President Vladimir Putin said on Saturday he had a normal dialogue with U.S. leader Donald Trump at a summit in Vietnam, and described Trump as civil, we
Summarization : Putin says had useful interaction with Trump at Vi
Training Data Size : 3000
Validation Data Size : 1000
Testing Data Size : 1000

 

df["text"]는 뉴스 본문을 뜻하며 본문 앞에 "summarize: "를 붙여 요약 작업이라는 정보를 모델에 전달한다.

 

 

데이터셋 전처리

import torch
from transformers import T5Tokenizer
from torch.utils.data import TensorDataset, DataLoader
from torch.utils.data import RandomSampler, SequentialSampler
from torch.nn.utils.rnn import pad_sequence


def make_dataset(data, tokenizer, device):
    source = tokenizer(
        text=data.text.tolist(),
        padding="max_length",
        max_length=128,
        pad_to_max_length=True,
        truncation=True,
        return_tensors="pt"
    )

    target = tokenizer(
        text=data.prediction.tolist(),
        padding="max_length",
        max_length=128,
        pad_to_max_length=True,
        truncation=True,
        return_tensors="pt"
    )

    source_ids = source["input_ids"].squeeze().to(device)
    source_mask = source["attention_mask"].squeeze().to(device)
    target_ids = target["input_ids"].squeeze().to(device)
    target_mask = target["attention_mask"].squeeze().to(device)
    return TensorDataset(source_ids, source_mask, target_ids, target_mask)

# BART의 입력 텐서 생성
# def make_dataset(data, tokenizer, device):
#     tokenized = tokenizer(
#         text=data.text.tolist(),
#         padding="longest",
#         truncation=True,
#         return_tensors="pt"
#     )
#     labels = []
#     input_ids = tokenized["input_ids"].to(device)
#     attention_mask = tokenized["attention_mask"].to(device)
#     for target in data.prediction:
#         labels.append(tokenizer.encode(target, return_tensors="pt").squeeze())
#     labels = pad_sequence(labels, batch_first=True, padding_value=-100).to(device)
#     return TensorDataset(input_ids, attention_mask, labels)

 

주석 부분은 이전에 BART 실습에서 같은 데이터셋을 전처리한 방법이다. 다음과 같은 차이점이 있다.

 

토크나이저 설정

BART : tokenizer 함수에 padding="longest"를 사용하여 가장 긴 시퀀스 길이에 맞춰서 패딩

T5 : padding="max_length", max_length=128, pad_to_max_length=True을 사용하여 모든 시퀀스를 고정된 길이 128로 패딩한다.

 

라벨 생성 방식

BART : data.prediction을 이터레이터로 불러와서 tokenizer.encode를 통해 인코딩한 후 리스트에 append 한 뒤 패딩을 수행

T5 : tokenizer로 한 번에 처리

 

다음은 dataloader를 만들기 위한 나머지 코드이다.

def get_datalodader(dataset, sampler, batch_size):
    data_sampler = sampler(dataset)
    dataloader = DataLoader(dataset, sampler=data_sampler, batch_size=batch_size)
    return dataloader


epochs = 5
batch_size = 8
device = "cuda" if torch.cuda.is_available() else "cpu"
tokenizer = T5Tokenizer.from_pretrained(
    pretrained_model_name_or_path="t5-small"
)

train_dataset = make_dataset(train, tokenizer, device)
train_dataloader = get_datalodader(train_dataset, RandomSampler, batch_size)

valid_dataset = make_dataset(valid, tokenizer, device)
valid_dataloader = get_datalodader(valid_dataset, SequentialSampler, batch_size)

test_dataset = make_dataset(test, tokenizer, device)
test_dataloader = get_datalodader(test_dataset, SequentialSampler, batch_size)

print(next(iter(train_dataloader)))
[tensor([[21603,    10,   549,  ..., 13644,  2770,     1],
        [21603,    10,   454,  ...,     9,  2493,     1],
        [21603,    10,   301,  ...,    13,     3,     1],
        ...,
        [21603,    10,   549,  ...,  1291,    13,     1],
        [21603,    10,   205,  ...,    32,     3,     1],
        [21603,    10,   549,  ...,     3,     9,     1]], device='cuda:0'), tensor([[1, 1, 1,  ..., 1, 1, 1],
        [1, 1, 1,  ..., 1, 1, 1],
        [1, 1, 1,  ..., 1, 1, 1],
        ...,
        [1, 1, 1,  ..., 1, 1, 1],
        [1, 1, 1,  ..., 1, 1, 1],
        [1, 1, 1,  ..., 1, 1, 1]], device='cuda:0'), tensor([[ 1008,   127,     7,  ...,     0,     0,     0],
        [24967,  6470,    57,  ...,     0,     0,     0],
        [ 1029, 17354,    12,  ...,     0,     0,     0],
        ...,
        [ 3128,    76,  2482,  ...,     0,     0,     0],
        [ 5308,    31,     7,  ...,     0,     0,     0],
        [  412,     5,   134,  ...,     0,     0,     0]], device='cuda:0'), tensor([[1, 1, 1,  ..., 0, 0, 0],
        [1, 1, 1,  ..., 0, 0, 0],
        [1, 1, 1,  ..., 0, 0, 0],
        ...,
        [1, 1, 1,  ..., 0, 0, 0],
        [1, 1, 1,  ..., 0, 0, 0],
        [1, 1, 1,  ..., 0, 0, 0]], device='cuda:0')]

 

토크나이저는 transformers의 T5Tokenizer 클래스로 사전 학습된 t5-small 모델의 토크나이저를 로드해 사용한다.

 

토큰 인덱스 앞에 반복되는 21603과 10은 각각 '_summarize'와 ':'를 의미한다.

print(tokenizer.convert_ids_to_tokens(21603))
print(tokenizer.convert_ids_to_tokens(10))
▁summarize
:

 

 

T5 모델

from torch import optim
from transformers import T5ForConditionalGeneration


model = T5ForConditionalGeneration.from_pretrained(
    pretrained_model_name_or_path="t5-small",
).to(device)
optimizer = optim.AdamW(model.parameters(), lr=1e-5, eps=1e-8)

 

T5ForConditionalGeneration은 미세 조정을 위한 클래스다. small은 트랜스포머와 동일한 구조를 갖는다.

 

 

학습

from torch import nn


def calc_accuracy(preds, labels):
    pred_flat = np.argmax(preds, axis=1).flatten()
    labels_flat = labels.flatten()
    return np.sum(pred_flat == labels_flat) / len(labels_flat)


def train(model, optimizer, dataloader):
    model.train()
    train_loss = 0.0

    for source_ids, source_mask, target_ids, target_mask in dataloader:
        decoder_input_ids = target_ids[:, :-1].contiguous()
        labels = target_ids[:, 1:].clone().detach()
        labels[target_ids[:, 1:] == tokenizer.pad_token_id] = -100

        outputs = model(
            input_ids=source_ids,
            attention_mask=source_mask,
            decoder_input_ids=decoder_input_ids,
            labels=labels,
        )

        loss = outputs.loss
        train_loss += loss.item()

        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

    train_loss = train_loss / len(dataloader)
    return train_loss


def evaluation(model, dataloader):
    with torch.no_grad():
        model.eval()
        val_loss = 0.0

        for source_ids, source_mask, target_ids, target_mask in dataloader:
            decoder_input_ids = target_ids[:, :-1].contiguous()
            labels = target_ids[:, 1:].clone().detach()
            labels[target_ids[:, 1:] == tokenizer.pad_token_id] = -100

            outputs = model(
                input_ids=source_ids,
                attention_mask=source_mask,
                decoder_input_ids=decoder_input_ids,
                labels=labels,
            )

            loss = outputs.loss
            val_loss += loss

    val_loss = val_loss / len(dataloader)
    return val_loss


best_loss = 10000
for epoch in range(epochs):
    train_loss = train(model, optimizer, train_dataloader)
    val_loss = evaluation(model, valid_dataloader)
    print(f"Epoch {epoch + 1}: Train Loss: {train_loss:.4f} Val Loss: {val_loss:.4f}")

    if val_loss < best_loss:
        best_loss = val_loss
        torch.save(model.state_dict(), "../T5ForConditionalGeneration.pt")
        print("Saved the model weights")
Epoch 1: Train Loss: 4.3293 Val Loss: 3.3523
Saved the model weights
Epoch 2: Train Loss: 3.4422 Val Loss: 2.9363
Saved the model weights
Epoch 3: Train Loss: 3.1539 Val Loss: 2.7773
Saved the model weights
Epoch 4: Train Loss: 3.0102 Val Loss: 2.6799
Saved the model weights
Epoch 5: Train Loss: 2.9007 Val Loss: 2.6199
Saved the model weights

 

T5 모델은 inputs_ids, attention_mask, decoder_input_ids, labels로 모델을 학습한다. 

 

decoder_input_ids = target_ids[:, :-1].contiguous() : target_ids의 마지막 토큰을 제외하고 decoder의 입력을 만든다. contiguous() 메서드는 텐서의 메모리 레이아웃을 연속적으로 만들어주는 역할을 한다. 이 메서드를 사용하는 이유 중 하나는 일부 연산이 연속적인 텐서에 대해서만 적용될 수 있기 때문이다.

 

labels = target_ids[:, 1:].clone().detach() : target_ids의 첫 번째 토큰을 제외하고 label을 만든다. 이러한 입력와 라벨을 만드는 방식은 트랜스포머 디코더가 다음 토큰을 예측하는 방식으로 학습하기 위한 전형적인 방법이다.

clone()은 원본 텐서를 복사하여 완전히 독립적인 새로운 텐서를 생성한다.

detach()는 새로운 텐서를 반환하는데, 이 텐서는 원본 텐서와의 계산 그래프 연결을 끊는다. 따라서 labels를 그래디언트 추적을 하지 않게 된다.

 

labels[target_ids[:, 1:] == tokenizer.pad_token_id] = -100 : padding토큰에 대해서 -100으로 설정해 손실 값 계산 시 무시되게 한다.

 

출력 결과를 보면 3000개의 데이터를 가지고 학습했음에도 불구하고 학습 및 검증 손실이 점차 감소하는 것을 확인할 수 있다.

 

 

평가

model.eval()
with torch.no_grad():
    for source_ids, source_mask, target_ids, target_mask in test_dataloader:
        generated_ids = model.generate(
            input_ids=source_ids,
            attention_mask=source_mask,
            max_length=128,
            num_beams=3,
            repetition_penalty=2.5,
            length_penalty=1.0,
            early_stopping=True,
        )

        for generated, target in zip(generated_ids, target_ids):
            pred = tokenizer.decode(
                generated, skip_special_tokens=True, clean_up_tokenization_spaces=True
            )
            actual = tokenizer.decode(
                target, skip_special_tokens=True, clean_up_tokenization_spaces=True
            )
            print("Generated Headline Text:", pred)
            print("Actual Headline Text   :", actual)
        break
Generated Headline Text: Clinton leads Trump by 4 percentage points in four-war race for Nov. 8 election
Actual Headline Text   : Clinton leads Trump by 4 points in Washington Post: ABC News poll
Generated Headline Text: U.S. senators sharpen potential line of attack against Gorsuch's nomination to Supreme Court
Actual Headline Text   : Democrats question independence of Trump Supreme Court nominee
Generated Headline Text: U.S. warns Saudi Arabia over humanitarian situation in Yemen could constrain U.S. aid.
Actual Headline Text   : In push for Yemen aid, U.S. warned Saudis of threats in Congress
Generated Headline Text: Romanian anti-corruption prosecutors open investigation into Liviu Dragnea on suspicion of forming criminal group to siphon off cash from state projects
Actual Headline Text   : Romanian ruling party leader investigated over 'criminal group'
Generated Headline Text: environmental activist endorsed Hillary Clinton for U.S. president
Actual Headline Text   : Billionaire environmental activist Tom Steyer endorses Clinton
Generated Headline Text: the 74-year-old grandmother delivers news of Pyongyang nuclear test with her usual gusto.
Actual Headline Text   : Voice of triumph or doom: North Korean presenter back in limelight for nuclear test
Generated Headline Text: Delson Guarate and Yon Goicoechea among nearly 400 jailed anti-Maduro activists.
Actual Headline Text   : Venezuela frees two anti-Maduro activists; scores still jailed
Generated Headline Text: House Majority Leader says he still troubled by Clinton email server
Actual Headline Text   : House No. 2 Republican says still questions Clinton's judgment in email matter

 

generate 메서드는 입력 문장(입력 시퀀스)에 대한 요약문(출력 시퀀스)를 생성한다. 

 

num_beams : Beam Search 알고리즘의 빔 크기를 의미한다. 이는 디코더 모델이 생성한 다수의 후보 단어 시퀀스 중에서 가장 높은 확률을 가진 시퀀스를 선택해 출력한다.

 

repetition_penalty : 중복 토큰 생성을 제어하는 값이다. 이 값이 높을수록 중복 토큰 생성이 억제된다.

 

length_penalty : 생성된 시퀀스 길이에 대한 보상을 제어한다. 이 값이 높을수록 더욱 긴 시퀀스가 생성된다.

 

early_stopping : 최대 길이에 도달하기 전에 eos 토큰이 생성되면 중단한다.

 

tokenizer의 decode 메서드에서 skip_special_tokens와 clean_up_tokenization_spaces는 디코딩된 텍스트에서 특수 토큰과 불필요한 공백을 제거한다.

 

실습을 위해 3000개의 데이터만 가지고 학습하여 실제와 생성된 요약문의 차이가 있다. 학습 데이터를 늘리고 하이퍼파라미터 튜닝으로 모델 성능을 개선할 수 있다.