반응형
사전 학습된 BERT 모델을 이용하여, 네이버 영화 리뷰데이터 분류하기
http://yonghee.io/bert_binary_classification_naver/
를 참고하여 진행하였다.
GPU가 있는 Ubuntu 서버, Conda 환경에서 실행하였다.
🦊네이버 영화 리뷰데이터 다운로드
git clone https://github.com/e9t/nsmc.git
🦊주요 패키지 불러오기 / train, test data 로드
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 확인
import os
n_devices = torch.cuda.device_count()
print('device count:',n_devices)
for i in range(n_devices):
print(i,':',torch.cuda.get_device_name(i))
## main 및 test 로드
train = pd.read_csv("./nsmc/ratings_train.txt", sep='\t')
test = pd.read_csv("./nsmc/ratings_test.txt", sep='\t')
print('train shape:',train.shape)
print('test shape:',test.shape)
train shape: (150000, 3)
test shape: (50000, 3)
전체의 25%가 테스트셋으로 배분
🦊전처리
전처리 : 데이터의 결측치 및 이상치를 확인하거나 제거하고 불일치되는 부분을 일관성 있는 데이터의 형태로 전환 하기도 하는 이 전 과정을 데이터의 전처리 라고 일컫는다
#문장별 전처리 - BERT 분류모델은 각 문장의 앞마다 [CLS]를 붙여 인식한다. 종료는 [SEP]
print('sentence processing..')
document_bert = ["[CLS]"+str(s)+"[SEP]" for s in train.document]
document_bert[:5]
print(document_bert[:5])
['[CLS] 아 더빙.. 진짜 짜증나네요 목소리 [SEP]',
'[CLS] 흠...포스터보고 초딩영화줄....오버연기조차 가볍지 않구나 [SEP]',
'[CLS] 너무재밓었다그래서보는것을추천한다 [SEP]',
'[CLS] 교도소 이야기구먼 ..솔직히 재미는 없다..평점 조정 [SEP]',
'[CLS] 사이몬페그의 익살스런 연기가 돋보였던 영화!스파이더맨에서 늙어보이기만 했던 커스틴 던스트가 너무나도 이뻐보였다 [SEP]']
🦊 토크나이징
토크나이징 : 토크나이징이란 텍스트에 대해 특정 기준 단위로 문장을 나누는 것을 의미한다. 예를 들면 문장을 단어 기준으로 나누거나 전체 글을 문장 단위로 나누는 것들이 토크나이징에 해당한다.
#사전 학습된 BERT multilingual 모델 내 포함되어있는 토크나이저 활용
print('tokenizeing..')
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]:',tokenized_texts[0])
tokenized_texts[0]: ['[CLS]', '아', '더', '##빙', '.', '.', '진', '##짜', '짜', '##증',
'##나', '##네', '##요', '목', '##소', '##리', '[SEP]']
🦊 패딩
패딩 : 자연어를 처리할 때 가변적 길이를 가지는 문장은 행렬로 처리하기 어렵다. 이때 문장을 같은 길이로 맞춰행렬로 한번에 처리할 수 있도록 해준다.
이 패딩 과정은 길이가 부족한 문장은 지정된 길이에 맞도록 0(zero pdding)을 채워주는데, 앞에서부터 패딩을 채우는 pre-padding과 뒤에서부터 패딩을 채우는 post-padding으로 나눌 수 있다.
#token들의 max length보다 크게 MAX_LEN을 설정한다. 설정한 MAX_LEN만큼 빈 공간을 0 이 채운다
print('padding')
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]
print('input_ids[0]:',input_ids[0])
padding
input_ids[0]: [ 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]
🦊어텐션 마스크
BERT가 어텐션 연산을 할 때, 패딩 토큰에 대해서 불필요하게 어텐션을 하지 않도록 실제 단어와 패딩 토큰을 구분할 수 있도록 알려주는 입력(0,1)
#학습 속도를 높이기 위해 실 데이터가 있는 곳과 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]:',attention_masks[0])
attention_masks[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, 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만 분리
print('split train - val')
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,lable,mask들을 torch tensor로 변환
print('convert data to tenser..')
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에 맞게 배치사이즈 설정(크게 설정후 부족메시지가 뜨면 8의 배수중 작은것으로 줄여나가기)
print('set batch and data loader')
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 셋 전처리와 동일
print('data split')
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 서버 8개 중 7번째 할당을 받아서 6으로 설정 ( 0부터 시작 )
#GPU 체크 및 할당
if torch.cuda.is_available():
device = torch.device("cuda:6")
print('There are %d GPU(s) available.' % torch.cuda.device_count())
print('We will use the GPU:', torch.cuda.get_device_name(6))
else:
device = torch.device("cpu")
print('No GPU available, using the CPU instead.')
## 분류를 위한 BERT 모델 생성
# transformers 의 BertForSequenceClassification 모듈 이용
# 이진분류 이므로 num_labels는 2로 설정
print('making BERT model for classification')
model = BertForSequenceClassification.from_pretrained("bert-base-multilingual-cased", num_labels=2)
#model.cuda()
model.to(device)
📍Runtime Error 발생시
더보기
Runtime Error : CUDA error: out of memory
Traceback (most recent call last):
File "main.py", line 132, in <module>
model.cuda()
File "/home/ubuntu/anaconda3/envs/dglee_py38/lib/python3.8/site-packages/torch/nn/modules/module.py", line 680, in cuda
return self._apply(lambda t: t.cuda(device))
File "/home/ubuntu/anaconda3/envs/dglee_py38/lib/python3.8/site-packages/torch/nn/modules/module.py", line 570, in _apply
module._apply(fn)
File "/home/ubuntu/anaconda3/envs/dglee_py38/lib/python3.8/site-packages/torch/nn/modules/module.py", line 570, in _apply
module._apply(fn)
File "/home/ubuntu/anaconda3/envs/dglee_py38/lib/python3.8/site-packages/torch/nn/modules/module.py", line 570, in _apply
module._apply(fn)
File "/home/ubuntu/anaconda3/envs/dglee_py38/lib/python3.8/site-packages/torch/nn/modules/module.py", line 593, in _apply
param_applied = fn(param)
File "/home/ubuntu/anaconda3/envs/dglee_py38/lib/python3.8/site-packages/torch/nn/modules/module.py", line 680, in <lambda>
return self._apply(lambda t: t.cuda(device))
RuntimeError: CUDA error: out of memory
CUDA kernel errors might be asynchronously reported at some other API call,so the stacktrace below might be incorrect.
For debugging consider passing CUDA_LAUNCH_BLOCKING=1.
model.cuda 대신 model.to(device) 로 변경
🦊학습 스케쥴링
# transformers에서 제공하는 옵티마이저 중 AdamW를 사용
# 총 훈련 스텝은 이터레이션 * 에폭 수로 설정
# 러닝 레잇 스케쥴러도 transformers에서 제공하는것을 사용
print('schedule start')
#옵티마이저 설정
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 와 시간 표시함수 정의
# 정확도 계산 함수
print('train start')
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를 활용
# 재현을 위해 랜덤시드 고정
# 모든 Epoch를 학습하면 학습이 종료
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!")
print("")
트레이닝 완료 후
======== Epoch 4 / 4 ========
Training...
Batch 500 of 4,219. Elapsed: 0:05:35.
Batch 1,000 of 4,219. Elapsed: 0:11:10.
Batch 1,500 of 4,219. Elapsed: 0:16:46.
Batch 2,000 of 4,219. Elapsed: 0:22:21.
Batch 2,500 of 4,219. Elapsed: 0:27:56.
Batch 3,000 of 4,219. Elapsed: 0:33:32.
Batch 3,500 of 4,219. Elapsed: 0:39:07.
Batch 4,000 of 4,219. Elapsed: 0:44:42.
Average training loss: 0.18
Training epcoh took: 0:47:08
Running Validation...
Accuracy: 0.87
Validation took: 0:01:44
Training complete!
🦊테스트 - 테스트셋 평가
print('test start')
#시작 시간 설정
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)))
print("test finished!")
test start
Batch 100 of 1,563. Elapsed: 0:00:22.
Batch 200 of 1,563. Elapsed: 0:00:44.
Batch 300 of 1,563. Elapsed: 0:01:06.
Batch 400 of 1,563. Elapsed: 0:01:29.
Batch 500 of 1,563. Elapsed: 0:01:51.
Batch 600 of 1,563. Elapsed: 0:02:13.
Batch 700 of 1,563. Elapsed: 0:02:35.
Batch 800 of 1,563. Elapsed: 0:02:58.
Batch 900 of 1,563. Elapsed: 0:03:20.
Batch 1,000 of 1,563. Elapsed: 0:03:42.
Batch 1,100 of 1,563. Elapsed: 0:04:04.
Batch 1,200 of 1,563. Elapsed: 0:04:26.
Batch 1,300 of 1,563. Elapsed: 0:04:48.
Batch 1,400 of 1,563. Elapsed: 0:05:11.
Batch 1,500 of 1,563. Elapsed: 0:05:33.
Accuracy: 0.87
Test took: 0:05:47
test finished!
반응형
'AI > NLP' 카테고리의 다른 글
샘플과 타깃의 인코딩 (0) | 2022.02.09 |
---|---|
Some weights of the model checkpoint at bert-base-multilingual-cased were not used when initializing (0) | 2022.02.04 |
NLP / 지도학습 (0) | 2022.01.24 |
자연어 처리 시작 (0) | 2022.01.24 |
댓글