Hugging Face Transformers
허깅페이스 트랜스포머는 다양한 형태의 트랜스포머 모델을 통일된 인터페이스에서 사용할 수 있도록 지원하는 오픈 소스 라이브러리이다.
허깅페이스에서는 트랜스포머 모델과 토크나이저를 활용할 때 사용하는 transformers 라이브러리, 원하는 데이터셋을 가져다 쓸 수 있도록 하는 datasets 라이브러리를 제공한다.
- BERT와 GPT-2 모델을 활용하기 위한 허깅페이스 트랜스포머 코드
from transformers import AutoModel, AutoTokenizer
text = "What's the goal of transformer models?"
# BERT
bert_model = AutoModel.from_pretrained("bert-base-uncased") # BERT 모델 불러오기
bert_tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased") # 토크나이저 불러오기
encoded_input = bert_tokenizer(text, return_tensors="pt") # 입력 text 토큰화
bert_output = bert_model(**encoded_input) # BERT 모델에 입력
# GPT-2
gpt_model = AutoModel.from_pretrained("gpt2") # GPT-2 모델 불러오기
gpt_tokenizer = AutoTokenizer.from_pretrained("gpt2") # 토크나이저 불러오기
encoded_input = gpt_tokenizer(text, return_tensors="pt") # 입력 text 토큰화
gpt_output = gpt_model(**encoded_input)
Hugging Face Library 사용해보기
1. 모델 활용하기
허깅페이스 허브에서 원하는 모델과 데이터셋을 찾고 활용해보자.
허깅페이스 트랜스포머 라이브러리를 사용하면 허깅페이스 모델 허브의 모델을 불러와서 사용할 수 있다.
허깅페이스에서는 모델을 바디(Body)와 헤드(Head)로 구분한다.
동일한 바디를 가져와서 사용하되, 본인의 태스크에 맞게 헤드를 선택해 사용할 수 있다.
예를 들어, 동일한 BERT 모델의 바디를 가져온 뒤, 텍스트 분류나 토큰 분류와 같은 서로 다른 태스크에 맞는 헤드를 가져와 사용할 수 있다.
- (1) 모델 바디만 불러오기
from transformers import AutoModel
model_id = 'klue/roberta-base'
model = AutoModel.from_pretrained(model_id)
* AutoModel: 모델의 바디를 불러오는 클래스
* KLUE/RoBERTa: RoBERTa 모델을 한국어로 학습한 모델
- (2) 분류 헤드가 붙어 있는 모델 불러오기
from transformers import AutoModelForSequenceClassification
model_id = 'SamLove/roberta-base-go_emotions'
Classification_model = AutoModelForSequenceClassification.from_pretrained(model_id)
* SamLove/roberta-base-go_emotions: 입력 문장이 어떤 감성을 나타내는지 분류하는 분류 헤드가 포함된 RoBERTa 모델
2. 토크나이저 활용하기
토크나이저는 텍스트를 토큰 단위로 나누고 각 토큰을 대응하는 토큰 아이디로 변환하는 역할을 수행한다.
필요한 경우에는 특수 토큰을 추가할 수도 있다.
토크나이저도 모델을 불러올 때처럼 모델의 아이디로 불러올 수 있다.
- 토크나이저 불러오기
from transformers import AutoTokenizer
model_id = 'klue/roberta-base'
tokenizer = AutoTokenizer.from_pretrained(model_id)
토크나이저에 텍스트를 입력하면 다음을 반환하게 된다.
- input_ids: 토큰 아이디 리스트
-> 각 토큰이 토크나이저 사전의 몇 번째 항목인지 나타낸다.
ex. input_ids가 0인 경우 [CLS] 토큰에 대응
- attention_mask: 토큰이 실제 텍스트인지 아니면 길이를 맞추기 위해 추가한 패딩(Padding)인지 알려준다.
-> ex. attention_mask 값이 1인 경우 패딩이 아닌 실제 토큰
- token_type_ids: 토큰이 속한 문장의 아이디
-> ex. token_type_ids가 0이면 첫 번째 문장임을 의미한다.
- 토크나이저 사용하기
tokenized = tokenizer("토크나이저는 텍스트를 토큰 단위로 나누는 역할을 합니다.")
print(tokenized)
print(tokenizer.convert_ids_to_tokens(tokenized['input_ids']))
print(tokenizer.decode(tokenized['input_ids']))
decode 메서드를 사용하면 토큰 아이디를 다시 텍스트로 변환할 수 있다.
print(tokenizer.decode(tokenized['input_ids'], skip_special_tokens=True))
skip_special_tokens 옵션을 True로 설정하면 [CLS], [SEP]와 같은 특수 토큰을 제외한 채 텍스트로 변환한다.
- 토크나이저에 여러 문장 넣기
tokenized = tokenizer(['첫 번째 문장입니다.', '두 번째 문장입니다.'])
print(tokenized)
만약 2개의 문장이 서로 원인과 결과 관계인지 학습시키고자 한다면 두 문장을 한 번에 모델의 입력으로 넣어주어야 한다.
이 때는 2개의 문장이 하나의 데이터라는 것을 표현하기 위해 아래 예제와 같이 한 번 더 리스트로 감싸주어야 한다.
tokenized = tokenizer([['첫 번째 문장입니다.', '두 번째 문장입니다.']])
print(tokenized)
여러 문장을 넣는 첫 번째 예제가 2개의 리스트를 반환하는 결과값과 다르게, 결과가 하나로 반환되는 것을 확인할 수 있다.
그렇다면 두 가지 예제 토큰을 다시 문자열로 복원하면 어떻게 될까?
first_tokenized_result = tokenizer(['첫 번째 문장입니다.', '두 번째 문장입니다.'])['input_ids']
first_decode_results = tokenizer.batch_decode(first_tokenized_result)
print(first_decode_results)
second_tokenized_result = tokenizer([['첫 번째 문장입니다.', '두 번째 문장입니다.']])['input_ids']
second_decode_results = tokenizer.batch_decode(second_tokenized_result)
print(second_decode_results)
2개의 문장을 각각 모델의 입력으로 넣어준 첫 번째 예제의 경우, 각 문장마다 [CLS], [SEP] 토큰이 붙어 있는 것을 확인할 수 있다.
반면 2개의 문장을 하나의 입력으로 넣어준 두 번째 예제의 경우, [SEP] 토큰으로 두 문장을 구분한다는 것을 알 수 있다.
- BERT 토크나이저와 RoBERTa 토크나이저 비교하기
bert_tokenizer = AutoTokenizer.from_pretrained('klue/bert-base')
bert_results = bert_tokenizer([['첫 번째 문장입니다.', '두 번째 문장입니다.']])
print(bert_results)
roberta_tokenizer = AutoTokenizer.from_pretrained('klue/roberta-base')
roberta_results = roberta_tokenizer([['첫 번째 문장입니다.', '두 번째 문장입니다.']])
print(roberta_results)
BERT 토크나이저로 토큰화한 결과를 보면 [2, .... , 3] 인 것을 확인할 수 있다.
이 때 [2]는 문장의 시작을 나타내는 CLS 토큰, [3]은 문장의 끝을 나타내는 SEP 토큰이다.
RoBERTa 토크나이저로 토큰화한 결과를 보면 [0, ..., 2] 인 것을 확인할 수 있다.
이 때 [0]은 문장의 시작을 나타내는 BOS 토큰, [2]는 문장의 끝을 나타내는 EOS 토큰이다.
BERT 토크나이저로 토큰화한 결과를 보면 token_type_ids가 0과 1로 나누어져 있는 것을 알 수 있다.
첫 번째 문장은 0, 두 번째 문장은 1이 된다.
RoBERTa의 경우 token_type_ids를 사용하지 않고 문장 구분을 자동으로 처리하기 때문에 모두 0인 것을 알 수 있다.
-> BERT는 문장 분리를 명확하게 하기 위해 token_type_ids를 사용하지만, RoBERTa는 이를 제거해 더 단순한 토큰화를 수행한다는 차이를 확인할 수 있다.
해당 부분은 BERT가 2개의 문장이 이어지는지 맞추는 학습을 할 때 NSP(Next Sentence Prediction)을 사용한다는 사실을 기억한다면 빠르게 이해할 수 있다!
attention_mask 값은 두 모델 모두 동일하다.
attention_mask가 1이면 해당 토큰이 패딩이 아니며, 모델이 해당 토큰을 고려해야한다는 것을 의미한다.
3. 데이터셋 활용하기
datasets 라이브러리를 사용하면 허깅페이스 허브에서 데이터셋을 코드로 불러올 수 있다.
- KLUE MRC 데이터셋 다운로드
from datasets import load_dataset
klue_mrc_dataset = load_dataset('klue', 'mrc')
print(klue_mrc_dataset)
klue_mrc_dataset의 내용을 확인하면 train, validation 데이터가 각각 17554, 5841 개가 있는 것을 확인할 수 있다.
또한 title, context, question, answers와 같은 컬럼이 존재한다는 것을 확인할 수 있다.
허깅페이스의 데이터셋 저장소에 있는 데이터만 불러올 수 있는 것이 아니라, 로컬에 있는 파일이나 파이썬 객체를 데이터셋으로 변환해 사용할 수도 있다!
from datasets import load_dataset
from datasets import Dataset
# 로컬의 csv 데이터 파일 활용하기
dataset = load_dataset("csv", data_files="my_file.csv")
# 파이썬 딕셔너리 활용하기
my_dict = {"a": [1, 2, 3]}
dataset = Dataset.from_dict(my_dict)
# Pandas 데이터프레임 활용하기
import pandas as pd
df = pd.DataFrame({"a": [1, 2, 3]})
dataset = Dataset.from_pandas(df)
모델 학습시키기
[한국어 기사 제목을 바탕으로 기사의 카테고리를 분류하는 텍스트 분류 모델 학습]
- 데이터 준비
klue_tc_train = load_dataset('klue', 'ynat', split='train')
klue_tc_eval = load_dataset('klue', 'ynat', split='validation')
train 데이터를 확인해보면 뉴스 제목(title), 카테고리(label) 등 컬럼으로 이루어진 45678개의 데이터가 존재함을 확인할 수 있다.
train 데이터의 첫 번째 데이터를 출력해보면 위와 같다.
label 값이 숫자로 되어 있는데 각 숫자가 의미하는 카테고리명이 무엇인지 출력해보자.
klue_tc_train.features['label'].names
데이터셋의 정보를 저장하고 있는 features 속성으로 label 컬럼의 항목별 이름을 확인할 수 있다!
출력 결과를 통해 첫 번째 데이터의 label은 '생활문화' 카테고리였음을 알 수 있다.
- 불필요한 컬럼 제거하기
분류 모델을 학습시킬 때 title, label을 제외한 컬럼들은 필요하지 않기 때문에 제거해주어야 한다.
klue_tc_train = klue_tc_train.remove_columns(['guid', 'url', 'date'])
klue_tc_eval = klue_tc_eval.remove_columns(['guid', 'url', 'date'])
- 카테고리 매핑하기
카테고리가 숫자로 되어 있어 확인하기 어려우므로 label_str 컬럼을 추가해보자.
features 속성에서 label 컬럼을 확인해보면 레이블 ID와 카테고리를 연결할 수 있는 ClassLabel 객체가 있다.
ClassLabel 객체에는 ID를 카테고리로 변환하는 int2str 메서드가 있는데, 이를 활용해 아이디 3을 입력하면 '생활문화' 카테고리를 뱉어내도록 변환할 수 있다.
print(klue_tc_train.features['label'])
print(klue_tc_train.features['label'].int2str(3))
klue_tc_label = klue_tc_train.features['label']
def make_str_label(batch):
batch['label_str'] = klue_tc_label.int2str(batch['label'])
return batch
klue_tc_train = klue_tc_train.map(make_str_label, batched=True, batch_size=1000)
print(klue_tc_train[0])
- train/validation/test 데이터셋 분할하기
train_test_split 메서드를 사용해 test_size에 맞춰 학습 데이터셋과 테스트 데이터셋을 분리한다.
학습 데이터 10000개, 검증 데이터 1000개, 테스트 데이터 1000개로 분리했다.
train_dataset = klue_tc_train.train_test_split(test_size=10000, shuffle=True, seed=0)['test']
dataset = klue_tc_eval.train_test_split(test_size=1000, shuffle=True, seed=0)
test_dataset = dataset['test']
valid_dataset = dataset['train'].train_test_split(test_size=1000, shuffle=True, seed=0)['test']
- Trainer API를 사용한 학습: (1) 준비
허깅페이스는 학습에 필요한 다양한 기능(데이터 로더 준비, logging, 평가, 저장) 등을 학습 인자(TrainingArguments)만으로 쉽게 활용할 수 있는 Trainer API를 제공한다.
import torch
import numpy as np
from transformers import Trainer, TrainingArguments, AutoModelForSequenceClassification, AutoTokenizer
def tokenize_function(examples):
return tokenizer(examples['title'], padding="max_length", truncation=True)
model_id = "klue/roberta-base"
model = AutoModelForSequenceClassification.from_pretrained(model_id, num_labels=len(train_dataset.features['label'].names))
tokenizer = AutoTokenizer.from_pretrained(model_id)
train_dataset =train_dataset.map(tokenize_function, batched=True)
valid_dataset = valid_dataset.map(tokenize_function, batched=True)
test_dataset = test_dataset.map(tokenize_function, batched=True)
학습에 사용할 분류 모델을 불러오기 위해 AutoModelForSequenceClassification 클래스로 klue/roberta-base 모델을 불러왔다.
그리고 분류 헤드의 분류 클래스 수를 지정하기 위해 num_labels 인자에 데이터셋의 레이블 수를 사용했다.
- Trainer API를 사용한 학습: (2) 학습 인자와 평가 함수 준비
training_args = TrainingArguments(
output_dir="./results",
num_train_epochs=1,
per_device_train_batch_size=8,
per_device_eval_batch_size=8,
evaluation_strategy="epoch",
learning_rate=5e-5,
push_to_hub=False,
report_to="none",
)
def compute_metrics(eval_pred):
logits, labels = eval_pred
predictions = np.argmax(logits, axis=-1)
return {"accuracy": (predictions == labels).mean()}
학습에 사용할 인자를 설정하는 TrainingArguments에 학습 인자를 입력한다.
에포크 수는 3, 배치 크기는 8, 결과는 results 폴더에 저장하고 한 에포크 학습이 끝날 때마다 검증 데이터셋에 대한 평가를 수행하도록 evaluation_strategy를 epoch로 설정했다.
compute_metrics를 정의해 학습에 대한 평가 지표를 정의했다.
모델의 예측 결과인 eval_pred를 입력으로 받아 예측 결과 중 가장 큰 값을 갖는 클래스를 np.argmax 함수로 뽑아 predictions에 저장하고, predictions와 정답이 저장된 labels가 같은 값을 갖는 결과의 비율을 정확도로 반환하도록 했다.
report_to='none' 인자는Trainer의 wandb를 사용하지 않도록 하기 위함이다.
- Trainer API를 사용한 학습: (3) 학습 진행
trainer = Trainer(
model=model,
args=training_args,
train_dataset=train_dataset,
eval_dataset=valid_dataset,
tokenizer=tokenizer,
compute_metrics=compute_metrics,
)
trainer.train()
trainer.evaluate(test_dataset)
Trainer에 준비한 데이터셋과 설정을 인자로 전달한 뒤 train() 으로 학습을 진행한다.
학습이 끝나면 evaluate() 로 테스트 데이터셋에 대한 평가를 수행한다.
출력 결과를 확인한 결과 약 86%의 정확도로 기사의 카테고리를 분류한 것을 알 수 있다!
Trainer API는 추상화를 통해 간편하게 학습을 할 수 있다는 장점이 있으나, 내부 동작을 파악하기 어렵다는 단점이 있다.
이번에는 Trainer API를 사용하지 않고 직접 학습 코드를 작성해보고, 두 가지 방식의 차이를 이해해보려고 한다.
- Trainer API를 사용하지 않는 학습: (1) 모델과 토크나이저 준비
import torch
from tqdm.auto import tqdm
from torch.utils.data import DataLoader
from transformers import AdamW
def tokenizer_function(examples):
return tokenizer(examples["title"], padding="max_length", truncation=True)
# 모델과 토크나이저 불러오기
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model_id = "klue/roberta-base"
model = AutoModelForSequenceClassification.from_pretrained(model_id, num_labels=len(train_dataset.features['label'].names))
tokenizer = AutoTokenizer.from_pretrained(model_id)
model.to(device)
Trainer를 사용한 방법과 비슷하게 모델과 토크나이저를 불러오고 토큰화 함수를 정의했다.
차이점은, Trainer에서는 내부적으로 수행하던 GPU로의 모델 이동을 직접 수행한다는 것이다!
- Trainer API를 사용하지 않는 학습: (2) 데이터 준비
def make_dataloader(dataset, batch_size, shuffle=True):
dataset = dataset.map(tokenize_function, batched=True).with_format("torch")
# 데이터셋에 토큰화 수행
dataset = dataset.rename_column("label", "labels") # 컬럼 이름 변경
dataset = dataset.remove_columns(column_names=["title"]) # 불필요한 컬럼 제거
return DataLoader(dataset, batch_size=batch_size, shuffle=shuffle)
# 데이터 로더 생성
train_dataloader = make_dataloader(train_dataset, batch_size=8, shuffle=True)
valid_dataloader = make_dataloader(valid_dataset, batch_size=8, shuffle=False)
test_dataloader = make_dataloader(test_dataset, batch_size=8, shuffle=False)
데이터로더를 생성하는 함수를 정의해주었다.
rename_column 메서드를 통해 컬럼 이름을 labels로 변경하고, remove_columns 메서드를 사용해 토큰화 후 불필요해진 title 컬럼을 제거한다.
마지막으로 파이토치에서 제공하는 DataLoader 클래스를 사용해 데이터셋을 배치 데이터로 만들었다.
- Trainer API를 사용하지 않는 학습: (3) 학습 함수 정의
def train_epoch(model, data_loader, optimizer):
model.train()
total_loss=0
for batch in tqdm(data_loader):
optimizer.zero_grad()
input_ids = batch['input_ids'].to(device) # 모델에 입력할 토큰 아이디
attention_mask = batch['attention_mask'].to(device) # 모델에 입력할 attention mask
labels = batch['labels'].to(device) # 모델에 입력할 label
outputs = model(input_ids, attention_mask=attention_mask, labels=labels)
loss = outputs.loss # 손실
loss.backward # 역전파
optimizer.step() # 모델 업데이트
total_loss += loss.item()
avg_loss = total_loss / len(data_loader)
return avg_loss
학습을 수행하는 함수 train_epoch를 정의하고 train() 메서드를 사용해 모델을 학습 모드로 변경한다.
위에서 생성한 데이터로더에서 배치 데이터를 가져와 모델에 입력으로 전달하고, input_ids, attention_mask, labels 키를 각각 model에 인자로 전달해 계산을 수행한다.
모델의 계산을 거친 결과에는 레이블과의 차이를 통해 계산된 손실이 있는데, 이 손실값을 이용해 역전파를 수행한다.
옵티마이저의 step()을 호출하면 역천파 결과를 바탕으로 모델을 업데이트하게 된다.
total_loss를 통해 학습이 잘 되고 있는지 집계한다.
- Trainer API를 사용하지 않는 학습: (4) 평가 함수 정의
def evaluate(model, data_loader):
model.eval()
total_loss = 0
predictions = []
true_labels = []
with torch.no_grad():
for batch in tqdm(data_loader):
input_ids = batch['input_ids'].to(device)
attention_mask = batch['attention_mask'].to(device)
labels = batch['labels'].to(device)
outputs = model(input_ids, attention_mask=attention_mask, labels=labels)
logits = outputs.logits
loss = outputs.loss
total_loss += loss.item()
preds = torch.argmax(logits, dim=-1)
predictions.extend(preds.cpu().numpy())
true_labels.extend(labels.cpu().numpy())
avg_loss = total_loss / len(data_loader)
accuracy = np.mean(np.array(predictions) == np.array(true_labels))
return avg_loss, accuracy
- Trainer API를 사용하지 않는 학습: (5) 학습 수행
num_epochs = 1
optimizer = AdamW(model.parameters(), lr=5e-5)
# 학습 루프
for epoch in range(num_epochs):
print(f"Epoch {epoch+1}/{num_epochs}")
train_loss = train_epoch(model, train_dataloader, optimizer)
print(f"Training loss: {train_loss}")
valid_loss, valid_accuracy = evaluate(model, valid_dataloader)
print(f"Validation loss: {valid_loss}")
print(f"Validation accuracy: {valid_accuracy}")
# 테스트
_, test_accuracy = evaluate(model, test_dataloader)
print(f"Test accuracy: {test_accuracy}")
Trainer API vs. PyTorch Custom Training
Trainer API
장점: - 학습, 평가, 저장 등 주요 기능을 자동화할 수 있다.
- 간결한 코드로 모델을 쉽게 학습시킬 수 있다.
- save_strategy="epoch" 설정을 통해 체크포인트를 자동으로 저장할 수 있다.
- torch.nn.DataParallel과 torch.nn.DistributedDataParallel을 내부적으로 지원해 쉽게 다중 GPU를 활용할 수 있다.
- tokenizer 및 data_collator와 결합해 배치 크기를 자동으로 맞출 수 있다.
단점: - PyTorch의 torch.optim 처럼 직접 optimizer를 설정하려면 별도로 Trainer의 compute_loss()를 오버라이딩해야 한다.
- Trainer는 학습 과정 대부분을 자동화하므로 특정 연산을 추가하는 것이 어려울 수 있다.
PyTorch Custom Training
장점: - Gradient Clipping, Custom Loss 등의 학습 과정을 세부적으로 조정할 수 있다.
- PyTorch에서 지원하는 모든 옵티마이저와 스케줄러를 사용할 수 있다.
- 데이터 로더 및 배치 커스텀이 가능하다.
단점: - 코드가 길고 복잡하며 직접 구현해야한다는 어려움이 있다.
- 체크 포인트를 저장하기 위해서는 직접 구현해야 한다.
- 멀티 GPU를 사용하기 위해서는 직접 구현해야 한다.
'Deep Learning' 카테고리의 다른 글
[NLP/RF] Markov Decision Process (MDP)에 대해 알아보자 (1) | 2024.11.04 |
---|---|
[NLP] InstructGPT와 RLHF 학습 방법에 대해 알아보자 (5) | 2024.11.04 |
[NLP] GPT의 구조와 입출력 방식을 알아보자 (1) | 2024.11.01 |
[NLP] BERT의 MLM 개념을 이용해 문법 교정을 해보자 (0) | 2024.10.31 |
[NLP] BERT 모델의 사전 학습 방법인 MLM과 NSP를 알아보자 (1) | 2024.10.31 |