이번에는 Hugging Face에 있는 사전 학습 모델을 불러와 실습을 진행해보겠다.
이전 timm을 이용했던 실습과 다르게 자연어처리를 실습해볼 것이다.
1. 라이브러리 import
import torch
import numpy as np
import warnings
import matplotlib.pyplot as plt
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
from tqdm.notebook import tqdm
warnings.filterwarnings('ignore')
import torch.nn as nn
import torch.optim as optim
import random
import torch.backends.cudnn as cudnn
def random_seed(seed_num):
torch.manual_seed(seed_num)
torch.cuda.manual_seed(seed_num)
torch.cuda.manual_seed_all(seed_num)
np.random.seed(seed_num)
cudnn.benchmark = False
cudnn.deterministic = True
random.seed(seed_num)
random_seed(624)
device = 'cuda:0'
2. 데이터 셋 다운로드
이번 실습의 경우 IMDB 데이터셋을 이용한 실습을 진행할 것이다. 데이터셋에 대하여 간단하게 설명하자면 50K개의 영화 리뷰 데이터에 대한 긍/부정 주석이 포함되어 있다. 데이터셋은 아래 링크에서 다운받을 수 있다.
데이터셋을 현재 디렉토리에 압축을 풀어주고 불러와준다.
data = pd.read_csv('IMDB Dataset.csv')
print(data.shape)
data.head()
2-1. 데이터 전처리
자연어의 특성상 전처리가 필요하기 때문에 우선 전처리를 진행할 것이다.
dic = {'positive':0, 'negative':1} # positive 면 0으로, negative면 1로 변환
data['sentiment'] = data['sentiment'].map(dic)
# 데이터 분할
train, test = train_test_split(data, test_size = .2, random_state = 624)
val, test = train_test_split(test, test_size = .5, random_state = 624)
print("Train set: ", len(train))
print("Validation set: ", len(val))
print("Test set: ", len(test))
우선 타겟의 라벨을 1과 0으로 변경해줬고, train, valid, test 데이터를 분리해줬다. 총 train은 40000개, valid는 5000개. test도 5000개로 나뉘어졌다.
2-1-1. [CLS], [SEP] 토큰 추가
이번 실습에서 사전학습 모델로 `BERT`를 이용할건데, 따라서 BERT의 INPUT형태를 맞춰줘야한다. BERT는 input의 맨 앞에 [CLS]토큰, 마지막에는 [SEP]토큰이 들어가야 한다.
따라서 데이터셋의 각 문장의 앞뒤에 해당 토큰들을 넣어준다.
# 문장의 앞뒤에 [CLS]와 [SEP] 삽입
train['review'] = train['review'].apply(lambda x: f'[CLS] {x} [SEP]')
val['review'] = val['review'].apply(lambda x: f'[CLS] {x} [SEP]')
test['review'] = test['review'].apply(lambda x: f'[CLS] {x} [SEP]')
train.head()
그다음 각 문장들과 target들을 분리해준다.
train_sentences = train['review'].values
val_sentences = val['review'].values
test_sentences = test['review'].values
train_label = train['sentiment'].values
val_label = val['sentiment'].values
test_label = test['sentiment'].values
2-1-2. Tokenizing
다음으로 이렇게 불러온 문장들을 사용하기 위해서는 `Tokenizing`을 해줘야한다. 이때 우리는 사전 학습 모델을 이용하기 때문에 해당 모델 `BERT`의 tokenizer을 불러와야한다.
tokenizer이란 텍스트를 정수형태로 변환한 리스트라고 이해할 수 있는데, 이를 통해 기존에 학습된 어휘사전을 사용할 수 있게 된다. 만약 BERT의 tokenizer이 아닌 다른 것을 불러온다면 BERT모델에서 인식하는 단어와 내 문장의 단어가 동일하지 않게 된다.(쉽게말하자면 BERT모델에 사과=1로 저장되어있는데, 다른 tokenizer을 이용해 내 단어를 tokenizeing하면 바나나=1로 저장되어 모델에 1이 들어갈 때 다르게 인식)
# BERT의 사전학습된 tokenizer로 문장을 토큰으로 분리
from transformers import BertTokenizer
tokenizer = BertTokenizer.from_pretrained('bert-base-cased')
이렇게 불러와진 tokenizer을 이용해서 각 train,valid,test의 문장들을 tokenizing해준다.
train_tokenized_texts = list(map(lambda x: tokenizer.tokenize(x), train_sentences))
val_tokenized_texts = list(map(lambda x: tokenizer.tokenize(x), val_sentences))
test_tokenized_texts = list(map(lambda x: tokenizer.tokenize(x), test_sentences))
2-1-3. Padding, mask
다음으로 모델의 input을 생성할 것이다. 우선 tokenizer을 이용해서 각 문장의 토큰들을 정수형으로 변경해주고, 문장의 input 길이가 동일하도록 padding을 이용해서 설정해준다.
# 최대 시퀀스
MAX_LEN = 128
# convert_tokens_to_ids로 토큰을 정수 형태로 변환
train_input_ids = list(map(lambda x: tokenizer.convert_tokens_to_ids(x), train_tokenized_texts))
val_input_ids = list(map(lambda x: tokenizer.convert_tokens_to_ids(x), val_tokenized_texts))
test_input_ids = list(map(lambda x: tokenizer.convert_tokens_to_ids(x), test_tokenized_texts))
# 문장을 MAX_LEN 길이에 맞게 자르고, 나머지 부분을 패딩 0
def zero_padding(id_list,max_len):
return np.array([i[:max_len] if len(i) >= max_len else i + [0] * (max_len - len(i)) for i in id_list])
train_input_ids = zero_padding(train_input_ids, MAX_LEN)
val_input_ids = zero_padding(val_input_ids, MAX_LEN)
test_input_ids = zero_padding(test_input_ids, MAX_LEN)
이 때 padding을 통해서 추가된 의미없는 0을 모델이 인식할 수 있도록, 해당 토큰이 패딩으로 추가된 것인지 아니면 실제 토큰인지를 인식하게 해주는 mask를 만들어준다. 패딩이면 0, 실제면 1로 구성된다.
# 마스크 만들기
# 패딩이 아닌 부분은 0보다 큰 값이 있으므로 flag를 통해서 마스크를 구성
train_masks = train_input_ids > 0
val_masks = val_input_ids > 0
test_masks = test_input_ids > 0
2-2. 데이터셋 만들기
이제 train input, label, mask를 이용하여 데이터셋을 만들어볼 것이다.
train_inputs = torch.tensor(train_input_ids)
train_labels = torch.tensor(train_label)
train_masks = torch.tensor(train_masks)
validation_inputs = torch.tensor(val_input_ids)
validation_labels = torch.tensor(val_label)
validation_masks = torch.tensor(val_masks)
test_inputs = torch.tensor(test_input_ids)
test_labels = torch.tensor(test_label)
test_masks = torch.tensor(test_masks)
우선 전부 tensor 형태로 변경한다음, 커스텀 데이터셋을 구성해준다. 이전에 배웠듯이 custom dataset에는 `__init__`, `__len__`,'__getitem__`이 필수적으로 들어가야 한다.
class EmotionData(torch.utils.data.Dataset):
def __init__(self, inputs, masks, labels):
self.inputs = inputs
self.masks = masks
self.labels = labels
def __len__(self):
return len(self.inputs)
def __getitem__(self,idx):
inputs_value = self.inputs[idx]
masks_value = self.masks[idx]
labels_value = self.labels[idx]
return inputs_value, masks_value, labels_value
train_dataset = EmotionData(train_inputs, train_masks, train_labels)
valid_dataset = EmotionData(validation_inputs, validation_masks, validation_labels)
test_dataset = EmotionData(test_inputs, test_masks, test_labels)
이렇게 만들어진 커스텀 데이터셋에 대하여 DataLoader을 이용하여 배치단위로 불러와준다.
BATCH_SIZE = 32
train_dataloader = torch.utils.data.DataLoader(train_dataset, batch_size = BATCH_SIZE, shuffle = True, drop_last = False, num_workers = 8)
valid_dataloader = torch.utils.data.DataLoader(valid_dataset, batch_size = BATCH_SIZE, shuffle = False, drop_last = False, num_workers = 8)
test_dataloader = torch.utils.data.DataLoader(test_dataset, batch_size = BATCH_SIZE, shuffle = False, drop_last = False, num_workers = 8)
3. 모델 불러오기
다음으로 Hugging Face에서 transformer 라이브러리에 있는 BERT 모델을 불러올 것이다.
이번에도 GPU를 이용하여 진행할 것이므로, 모델을 device에 올려준다.
model = BertForSequenceClassification.from_pretrained("bert-base-cased").to(device)
4. Fine Tuning
BERT 모델을 파인튜닝하여 분류를 진행할 것인데, BERT모델은 output으로 loss와 logits을 반환한다. 각각은 정답값과 예측의 차이, 해당 클래스의 확률을 의미한다. 또한 이전에 input길이를 맞춰주기 위해 padding으로 추가한 부분은 계산할 필요가 없기 때문에 해당부분을 처리하여 계산속도가 빠르게 해준다.
def training(model, dataloader, train_dataset, optimizer, device, epoch, num_epochs):
model.train()
train_loss = 0.0
train_accuracy = 0
tbar = tqdm(dataloader)
for batch in tbar:
input_ = batch[0].to(device)
mask = batch[1].to(device)
labels = batch[2].to(device)
# 순전파
output = model(input_,
attention_mask= mask,
labels=labels)
loss = output['loss']
# 역전파 및 가중치 업데이트
optimizer.zero_grad()
loss.backward()
optimizer.step()
# 손실과 정확도 계산
train_loss += loss.item()
# torch.max에서 dim 인자에 값을 추가할 경우, 해당 dimension에서 최댓값과 최댓값에 해당하는 인덱스를 반환
_, predicted = torch.max(output['logits'], 1)
train_accuracy += (predicted == labels).sum().item()
# tqdm의 진행바에 표시될 설명 텍스트를 설정
tbar.set_description(f"Epoch [{epoch+1}/{num_epochs}], Train Loss: {loss.item():.4f}")
# 에폭별 학습 결과 출력
train_loss = train_loss / len(dataloader)
train_accuracy = train_accuracy / len(train_dataset)
return model, train_loss, train_accuracy
def evaluation(model, dataloader, val_dataset, device, epoch, num_epochs):
model.eval() # 모델을 평가 모드로 설정
valid_accuracy = 0
with torch.no_grad(): # model의 업데이트 막기
tbar = tqdm(dataloader)
for batch in tbar:
input_ = batch[0].to(device)
mask = batch[1].to(device)
labels = batch[2].to(device)
# 순전파
output = model(input_,
attention_mask= mask,
labels=labels)
# torch.max에서 dim 인자에 값을 추가할 경우, 해당 dimension에서 최댓값과 최댓값에 해당하는 인덱스를 반환
_, predicted = torch.max(output['logits'], 1)
valid_accuracy += (predicted == labels).sum().item()
# tqdm의 진행바에 표시될 설명 텍스트를 설정
tbar.set_description(f"Epoch [{epoch+1}/{num_epochs}]")
valid_accuracy = valid_accuracy / len(val_dataset)
return model, valid_accuracy
def training_loop(model, train_dataloader, valid_dataloader, train_dataset, val_dataset, optimizer, device, num_epochs, model_name):
best_valid_loss = float('inf') # 가장 좋은 validation loss를 저장
valid_max_accuracy = -1
for epoch in range(num_epochs):
model, train_loss, train_accuracy = training(model, train_dataloader, train_dataset, optimizer, device, epoch, num_epochs)
model, valid_accuracy = evaluation(model, valid_dataloader, val_dataset, device, epoch, num_epochs)
if valid_accuracy > valid_max_accuracy:
valid_max_accuracy = valid_accuracy
torch.save(model.state_dict(), f"./model_{model_name}.pt")
print(f"Epoch [{epoch + 1}/{num_epochs}], Train Loss: {train_loss:.4f}, Train Accuracy: {train_accuracy:.4f}, Valid Accuracy: {valid_accuracy:.4f}")
return model, valid_max_accuracy
이전 실습과 마찬가지로 전체를 학습시키는 방법과 마지막 fc layer만 학습시키는 방법으로 나누어 진행해볼것이다.
4-1. 전체 학습
# 모델 전체 fine tuning
num_epochs = 2
model_name = 'bert1'
lr = 1e-5
optimizer = optim.Adam(model.parameters(), lr=lr)
model, valid_max_accuracy = training_loop(model, train_dataloader, valid_dataloader, train_dataset, valid_dataset, optimizer, device, num_epochs, model_name)
print('Valid max accuracy : ', valid_max_accuracy)
# >> Valid max accuracy : 0.889
model.load_state_dict(torch.load("./model_bert1.pt")) # 모델 불러오기
model = model.to(device)
model.eval()
total_labels = []
total_preds = []
total_probs = []
with torch.no_grad():
for batch in tqdm(test_dataloader):
input_ = batch[0].to(device)
mask = batch[1].to(device)
labels = batch[2].to(device)
output = model(input_,
attention_mask= mask,
labels=labels)
# torch.max에서 dim 인자에 값을 추가할 경우, 해당 dimension에서 최댓값과 최댓값에 해당하는 인덱스를 반환
_, predicted = torch.max(output['logits'], 1)
total_preds.extend(predicted.detach().cpu().tolist())
total_labels.extend(labels.tolist())
total_probs.append(output['logits'].detach().cpu().numpy())
total_preds = np.array(total_preds)
total_labels = np.array(total_labels)
total_probs = np.concatenate(total_probs, axis= 0)
acc = accuracy_score(total_labels, total_preds)
print("Full fine tuning model accuracy : ",acc)
# >> Full fine tuning model accuracy : 0.8814
위와 같이 모델을 학습시킨 후 추론까지 진행해본결과 사전학습 모델을 불러와 커스텀 데이터셋에 대하여 전체 학습 시켰을 때의 정확도는 0.8814가 나왔다.
4-2. 일부 학습
model = BertForSequenceClassification.from_pretrained("bert-base-cased").to(device)
for para in model.parameters(): # 모든 layer freeze 하기
para.requires_grad = False
for name, param in model.named_parameters(): # fc layer 만 학습하기
if name in 'classifier.weight':
param.requires_grad = True
num_epochs = 2
model_name = 'bert2'
optimizer = optim.Adam(model.parameters(), lr=lr)
model, valid_max_accuracy = training_loop(model, train_dataloader, valid_dataloader, train_dataset, valid_dataset, optimizer, device, num_epochs, model_name)
print('Valid max accuracy : ', valid_max_accuracy)
model.load_state_dict(torch.load("./model_bert2.pt")) # 모델 불러오기
model = model.to(device)
model.eval()
total_labels = []
total_preds = []
total_probs = []
with torch.no_grad():
for batch in tqdm(test_dataloader):
input_ = batch[0].to(device)
mask = batch[1].to(device)
labels = batch[2].to(device)
output = model(input_,
attention_mask= mask,
labels=labels)
# torch.max에서 dim 인자에 값을 추가할 경우, 해당 dimension에서 최댓값과 최댓값에 해당하는 인덱스를 반환
_, predicted = torch.max(output['logits'], 1)
total_preds.extend(predicted.detach().cpu().tolist())
total_labels.extend(labels.tolist())
total_probs.append(output['logits'].detach().cpu().numpy())
total_preds = np.array(total_preds)
total_labels = np.array(total_labels)
total_probs = np.concatenate(total_probs, axis= 0)
acc = accuracy_score(total_labels, total_preds)
print("Only FC layer tunning model accuracy : ", acc)
#>> Only FC layer tunning model accuracy : 0.5968
위에서 fc layer을 제외한 다른 layer들은 freeze하여 업데이트를 막고 학습을 진행하였다.
그 결과 정확도가 0.5968로 낮아진 모습을 확인할 수 있었다.
5. 결과 비교
이 역시 IMDB가 큰 데이터 셋이기 때문에 전체를 학습시켰던 결과가 더 우수했다는 것을 확인할 수 있었다.
'ML & DL > 개념정리' 카테고리의 다른 글
데이터 전처리(이상치&결측치) (0) | 2024.01.10 |
---|---|
PyTorch Lightning (0) | 2024.01.09 |
Timm을 이용한 Finetuning 실습 (0) | 2024.01.09 |
Pretrained Model (0) | 2024.01.09 |
PyTorch 프로세스 (1) | 2024.01.09 |