본문 바로가기
AI/NLP

BERT로 네이버 영화 리뷰데이터 분류하기 실습

by 동기 2022. 2. 4.
반응형

사전 학습된 BERT 모델을 이용하여, 네이버 영화 리뷰데이터 분류하기

 

 http://yonghee.io/bert_binary_classification_naver/

 

BERT로 네이버 영화 리뷰데이터 분류하기

사전학습된 BERT 모델만 이용하여 다른 전처리 없이 문장 긍부정 분류를 할 수 있을지, 귀추가 주목됩니다.

yonghee.io

를 참고하여 진행하였다.

 

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)

💻print

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])

💻print

['[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])

💻print

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])

💻print

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])

💻print

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("")

💻print

내가 할당된 6번째 GPU가 돌고 있다.

트레이닝 완료 후

======== 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!")

💻print

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!



 

출처 : http://yonghee.io/bert_binary_classification_naver/

반응형

댓글