Info
- Author: 천용희 (Yonghee Cheon)
- Description: 레퍼런스들을 참고하여 분류 태스크를 수행한 후 내용을 정리한 것입니다.
- Reference
준비
패키지 및 데이터 다운로드
- sklearn, torch, keras, transformers, numpy, pandas가 필요합니다.
- transformers: BERT 이용 메인 API
- torch: 파인튜닝 실제 학습용 딥러닝 프레임워크
- numpy 및 pandas: 전처리 목적
- sklearn: train_test_split 사용 목적
- keras: pad_sequences 사용 목적
- 위 항목 중 bold한 항목을 제외하고는 쓰임새에 맞게 다른 라이브러리로 구현해도 상관없습니다.
- 이 중 자신의 컴퓨터에 없는 패키지를 설치합니다.
- google colab으로 실습하고 있다면 나머지는 이미 설치되어 있으므로 transformers만 설치합니다.
!pip install transformers
주요 패키지 불러오기
필요한 패키지를 import해줍니다.
제일 중요한 건 파이토치와 트랜스포머입니다.
import torch
from transformers import BertTokenizer
from transformers import BertForSequenceClassification, AdamW, BertConfig
from transformers import get_linear_schedule_with_warmup
from torch.utils.data import TensorDataset, DataLoader, RandomSampler, SequentialSampler
from keras.preprocessing.sequence import pad_sequences
from sklearn.model_selection import train_test_split
import pandas as pd
import numpy as np
import random
import time
import datetime
GPU 확인
Torch에서 GPU가 사용되는지 확인하는 코드입니다.
Colab 사용시에도 어떤 GPU를 쓰는지 알 수 있습니다.
import os
n_devices = torch.cuda.device_count()
print(n_devices)
for i in range(n_devices):
print(torch.cuda.get_device_name(i))
네이버 영화 리뷰데이터 다운로드
!git clone https://github.com/e9t/nsmc.git
git clone을 하면 현재 파이썬 파일 혹은 Jupyter notebook이 있는 자리에 nsmc라는 이름의 디렉토리가 생성되었을 것입니다.
train, test 각각 로드
해당 디렉토리 내의 train, test 텍스트 파일을 불러옵니다.
train = pd.read_csv("nsmc/ratings_train.txt", sep='\t')
test = pd.read_csv("nsmc/ratings_test.txt", sep='\t')
print(train.shape)
print(test.shape)
출력하면 output으로 각각 (150000, 3), (50000, 3)이 출력됩니다.
전체의 25%가 테스트셋으로 배분되어있는 데이터입니다.
전처리
문장별 전처리
- BERT 분류모델은 각 문장의 앞마다 [CLS]를 붙여 인식합니다.
- 문장 종료는 [SEP]으로 알립니다.
- 저는 list comprehension을 선호하여 그렇게 처리했지만, for loop을 명시적으로 사용해도 상관없습니다.
document_bert = ["[CLS] " + str(s) + " [SEP]" for s in train.document]
document_bert[:5]
- output
['[CLS] 아 더빙.. 진짜 짜증나네요 목소리 [SEP]',
'[CLS] 흠...포스터보고 초딩영화줄....오버연기조차 가볍지 않구나 [SEP]',
'[CLS] 너무재밓었다그래서보는것을추천한다 [SEP]',
'[CLS] 교도소 이야기구먼 ..솔직히 재미는 없다..평점 조정 [SEP]',
'[CLS] 사이몬페그의 익살스런 연기가 돋보였던 영화!스파이더맨에서 늙어보이기만 했던 커스틴 던스트가 너무나도 이뻐보였다 [SEP]']
토크나이징
- 사전학습된 BERT multilingual 모델 내 포함되어있는 토크나이저를 활용하여 토크나이징합니다.
tokenizer = BertTokenizer.from_pretrained('bert-base-multilingual-cased', do_lower_case=False)
tokenized_texts = [tokenizer.tokenize(s) for s in document_bert]
print(tokenized_texts[0])
- output
['[CLS]', '아', '더', '##빙', '.', '.', '진', '##짜', '짜', '##증', '##나', '##네', '##요', '목', '##소', '##리', '[SEP]']
패딩
- token들의 max length보다 크게 MAX_LEN을 설정합니다.
- 설정한 MAX_LEN 만큼 빈 공간을 0이 채웁니다.
MAX_LEN = 128
input_ids = [tokenizer.convert_tokens_to_ids(x) for x in tokenized_texts]
input_ids = pad_sequences(input_ids, maxlen=MAX_LEN, dtype='long', truncating='post', padding='post')
input_ids[0]
- output
array([ 101, 9519, 9074, 119005, 119, 119, 9708, 119235,
9715, 119230, 16439, 77884, 48549, 9284, 22333, 12692,
102, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0])
어텐션 마스크
- 학습속도를 높이기 위해 실 데이터가 있는 곳과 padding이 있는 곳을 attention에게 알려줍니다.
attention_masks = []
for seq in input_ids:
seq_mask = [float(i>0) for i in seq]
attention_masks.append(seq_mask)
print(attention_masks[0])
- output
[1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
train - validation set 분리
- input과 mask가 뒤섞이지 않도록 random_state를 일정하게 고정합니다.
- test set은 위에서 이미 분리되었기에 train과 validation set만을 분리합니다.
random state는 42가 국룰입니다.
train_inputs, validation_inputs, train_labels, validation_labels = \
train_test_split(input_ids, train['label'].values, random_state=42, test_size=0.1)
train_masks, validation_masks, _, _ = train_test_split(attention_masks,
input_ids,
random_state=42,
test_size=0.1)
파이토치 텐서로 변환
- numpy ndarray로 되어있는 input, label, mask들을 torch tensor로 변환합니다.
train_inputs = torch.tensor(train_inputs)
train_labels = torch.tensor(train_labels)
train_masks = torch.tensor(train_masks)
validation_inputs = torch.tensor(validation_inputs)
validation_labels = torch.tensor(validation_labels)
validation_masks = torch.tensor(validation_masks)
배치 및 데이터로더 설정
- 현재 쓰고 있는 GPU의 VRAM에 맞도록 배치사이즈를 설정합니다.
- 우선 배치사이즈를 크게 넣어보고 VRAM 부족 메시지가 나오면 8의 배수 중 더 작은 것으로 줄여나가는 것이 일반적인 방법입니다.
BATCH_SIZE = 32
train_data = TensorDataset(train_inputs, train_masks, train_labels)
train_sampler = RandomSampler(train_data)
train_dataloader = DataLoader(train_data, sampler=train_sampler, batch_size=BATCH_SIZE)
validation_data = TensorDataset(validation_inputs, validation_masks, validation_labels)
validation_sampler = SequentialSampler(validation_data)
validation_dataloader = DataLoader(validation_data, sampler=validation_sampler, batch_size=BATCH_SIZE)
테스트셋 전처리
- 위의 train-val 셋 전처리와 동일합니다.
- train 전처리시 함께 처리해도 무방합니다.
sentences = test['document']
sentences = ["[CLS] " + str(sentence) + " [SEP]" for sentence in sentences]
labels = test['label'].values
tokenized_texts = [tokenizer.tokenize(sent) for sent in sentences]
input_ids = [tokenizer.convert_tokens_to_ids(x) for x in tokenized_texts]
input_ids = pad_sequences(input_ids, maxlen=MAX_LEN, dtype="long", truncating="post", padding="post")
attention_masks = []
for seq in input_ids:
seq_mask = [float(i>0) for i in seq]
attention_masks.append(seq_mask)
test_inputs = torch.tensor(input_ids)
test_labels = torch.tensor(labels)
test_masks = torch.tensor(attention_masks)
test_data = TensorDataset(test_inputs, test_masks, test_labels)
test_sampler = RandomSampler(test_data)
test_dataloader = DataLoader(test_data, sampler=test_sampler, batch_size=BATCH_SIZE)
모델 학습
GPU 체크 및 할당
- GPU 8개가 있고 이 중 0번째 GPU TITAN X를 사용합니다.
- CPU를 사용해도 학습 가능합니다.
(거짓말)
if torch.cuda.is_available():
device = torch.device("cuda")
print('There are %d GPU(s) available.' % torch.cuda.device_count())
print('We will use the GPU:', torch.cuda.get_device_name(0))
else:
device = torch.device("cpu")
print('No GPU available, using the CPU instead.')
- output
Found GPU at: /device:GPU:0
There are 8 GPU(s) available.
We will use the GPU: TITAN X (Pascal)
분류를 위한 BERT 모델 생성
- transformers의 BertForSequenceClassification 모듈을 이용합니다.
- 이진분류이기 때문에 num_labels는 2로 둡니다.
model = BertForSequenceClassification.from_pretrained("bert-base-multilingual-cased", num_labels=2)
model.cuda()
- output
BertForSequenceClassification(
(bert): BertModel(
(embeddings): BertEmbeddings(
(word_embeddings): Embedding(119547, 768, padding_idx=0)
(position_embeddings): Embedding(512, 768)
(token_type_embeddings): Embedding(2, 768)
(LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
(dropout): Dropout(p=0.1, inplace=False)
)
# 이하 출력은 생략합니다.
학습스케쥴링
- transformers에서 제공하는 옵티마이저 중 AdamW를 사용합니다.
- 총 훈련 스텝은 이터레이션 * 에폭수로 설정해둡니다.
- 러닝 레잇 스케쥴러는 역시 transformers에서 제공하는 것을 사용합니다.
# 옵티마이저 설정
optimizer = AdamW(model.parameters(),
lr = 2e-5, # 학습률
eps = 1e-8 # 0으로 나누는 것을 방지하기 위한 epsilon 값
)
# 에폭수
epochs = 4
# 총 훈련 스텝
total_steps = len(train_dataloader) * epochs
# lr 조금씩 감소시키는 스케줄러
scheduler = get_linear_schedule_with_warmup(optimizer,
num_warmup_steps = 0,
num_training_steps = total_steps)
학습
accuracy와 시간 표시함수 정의
# 정확도 계산 함수
def flat_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 format_time(elapsed):
# 반올림
elapsed_rounded = int(round((elapsed)))
# hh:mm:ss으로 형태 변경
return str(datetime.timedelta(seconds=elapsed_rounded))
학습 실행부분
- 데이터로더에서 배치만큼 가져온 후 forward, backward pass를 수행합니다.
- gradient update는 명시적으로 하지 않고 위에서 로드한 optimizer를 활용합니다.
# 재현을 위해 랜덤시드 고정
seed_val = 42
random.seed(seed_val)
np.random.seed(seed_val)
torch.manual_seed(seed_val)
torch.cuda.manual_seed_all(seed_val)
# 그래디언트 초기화
model.zero_grad()
# 에폭만큼 반복
for epoch_i in range(0, epochs):
# ========================================
# Training
# ========================================
print("")
print('======== Epoch {:} / {:} ========'.format(epoch_i + 1, epochs))
print('Training...')
# 시작 시간 설정
t0 = time.time()
# 로스 초기화
total_loss = 0
# 훈련모드로 변경
model.train()
# 데이터로더에서 배치만큼 반복하여 가져옴
for step, batch in enumerate(train_dataloader):
# 경과 정보 표시
if step % 500 == 0 and not step == 0:
elapsed = format_time(time.time() - t0)
print(' Batch {:>5,} of {:>5,}. Elapsed: {:}.'.format(step, len(train_dataloader), elapsed))
# 배치를 GPU에 넣음
batch = tuple(t.to(device) for t in batch)
# 배치에서 데이터 추출
b_input_ids, b_input_mask, b_labels = batch
# Forward 수행
outputs = model(b_input_ids,
token_type_ids=None,
attention_mask=b_input_mask,
labels=b_labels)
# 로스 구함
loss = outputs[0]
# 총 로스 계산
total_loss += loss.item()
# Backward 수행으로 그래디언트 계산
loss.backward()
# 그래디언트 클리핑
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
# 그래디언트를 통해 가중치 파라미터 업데이트
optimizer.step()
# 스케줄러로 학습률 감소
scheduler.step()
# 그래디언트 초기화
model.zero_grad()
# 평균 로스 계산
avg_train_loss = total_loss / len(train_dataloader)
print("")
print(" Average training loss: {0:.2f}".format(avg_train_loss))
print(" Training epcoh took: {:}".format(format_time(time.time() - t0)))
# ========================================
# Validation
# ========================================
print("")
print("Running Validation...")
#시작 시간 설정
t0 = time.time()
# 평가모드로 변경
model.eval()
# 변수 초기화
eval_loss, eval_accuracy = 0, 0
nb_eval_steps, nb_eval_examples = 0, 0
# 데이터로더에서 배치만큼 반복하여 가져옴
for batch in validation_dataloader:
# 배치를 GPU에 넣음
batch = tuple(t.to(device) for t in batch)
# 배치에서 데이터 추출
b_input_ids, b_input_mask, b_labels = batch
# 그래디언트 계산 안함
with torch.no_grad():
# Forward 수행
outputs = model(b_input_ids,
token_type_ids=None,
attention_mask=b_input_mask)
# 로스 구함
logits = outputs[0]
# CPU로 데이터 이동
logits = logits.detach().cpu().numpy()
label_ids = b_labels.to('cpu').numpy()
# 출력 로짓과 라벨을 비교하여 정확도 계산
tmp_eval_accuracy = flat_accuracy(logits, label_ids)
eval_accuracy += tmp_eval_accuracy
nb_eval_steps += 1
print(" Accuracy: {0:.2f}".format(eval_accuracy/nb_eval_steps))
print(" Validation took: {:}".format(format_time(time.time() - t0)))
print("")
print("Training complete!")
출력부분
- 모든 Epoch를 학습하면 학습이 종료됩니다.
======== Epoch 1 / 4 ========
Training...
/pytorch/torch/csrc/utils/python_arg_parser.cpp:749: UserWarning: This overload of add_ is deprecated:
add_(Number alpha, Tensor other)
Consider using one of the following signatures instead:
add_(Tensor other, Number alpha)
Batch 500 of 4,219. Elapsed: 0:03:01.
Batch 1,000 of 4,219. Elapsed: 0:06:03.
Batch 1,500 of 4,219. Elapsed: 0:09:04.
Batch 2,000 of 4,219. Elapsed: 0:12:07.
Batch 2,500 of 4,219. Elapsed: 0:15:09.
Batch 3,000 of 4,219. Elapsed: 0:18:12.
Batch 3,500 of 4,219. Elapsed: 0:21:15.
Batch 4,000 of 4,219. Elapsed: 0:24:18.
Average training loss: 0.38
Training epcoh took: 0:25:37
.
.
.
Training complete!
테스트
테스트셋 평가
#시작 시간 설정
t0 = time.time()
# 평가모드로 변경
model.eval()
# 변수 초기화
eval_loss, eval_accuracy = 0, 0
nb_eval_steps, nb_eval_examples = 0, 0
# 데이터로더에서 배치만큼 반복하여 가져옴
for step, batch in enumerate(test_dataloader):
# 경과 정보 표시
if step % 100 == 0 and not step == 0:
elapsed = format_time(time.time() - t0)
print(' Batch {:>5,} of {:>5,}. Elapsed: {:}.'.format(step, len(test_dataloader), elapsed))
# 배치를 GPU에 넣음
batch = tuple(t.to(device) for t in batch)
# 배치에서 데이터 추출
b_input_ids, b_input_mask, b_labels = batch
# 그래디언트 계산 안함
with torch.no_grad():
# Forward 수행
outputs = model(b_input_ids,
token_type_ids=None,
attention_mask=b_input_mask)
# 로스 구함
logits = outputs[0]
# CPU로 데이터 이동
logits = logits.detach().cpu().numpy()
label_ids = b_labels.to('cpu').numpy()
# 출력 로짓과 라벨을 비교하여 정확도 계산
tmp_eval_accuracy = flat_accuracy(logits, label_ids)
eval_accuracy += tmp_eval_accuracy
nb_eval_steps += 1
print("")
print("Accuracy: {0:.2f}".format(eval_accuracy/nb_eval_steps))
print("Test took: {:}".format(format_time(time.time() - t0)))
테스트 출력
Batch 100 of 1,563. Elapsed: 0:00:11.
Batch 200 of 1,563. Elapsed: 0:00:22.
Batch 300 of 1,563. Elapsed: 0:00:34.
Batch 400 of 1,563. Elapsed: 0:00:45.
Batch 500 of 1,563. Elapsed: 0:00:56.
Batch 600 of 1,563. Elapsed: 0:01:07.
Batch 700 of 1,563. Elapsed: 0:01:18.
Batch 800 of 1,563. Elapsed: 0:01:29.
Batch 900 of 1,563. Elapsed: 0:01:40.
Batch 1,000 of 1,563. Elapsed: 0:01:52.
Batch 1,100 of 1,563. Elapsed: 0:02:03.
Batch 1,200 of 1,563. Elapsed: 0:02:14.
Batch 1,300 of 1,563. Elapsed: 0:02:25.
Batch 1,400 of 1,563. Elapsed: 0:02:36.
Batch 1,500 of 1,563. Elapsed: 0:02:47.
Accuracy: 0.87
Test took: 0:02:54
문장 앞뒤에 구분자 토큰을 넣는 것 외에 별다른 전처리를 하지 않고도 epoch 4회만에 테스트셋 acc 0.87이 나왔습니다.
BERT 임베딩의 강력함을 알 수 있습니다.
multilingual model이 아닌 kobert 등 한국어 코퍼스를 중점으로 학습한 모델을 사용하면 95% 이상의 결과도 낼 수 있다고 합니다.
이 다음으로는 유사한 태스크인 IMDB 데이터도 실습해보겠습니다.