AI/정책 댓글 반응 NLP

[4] 포털 댓글 감성 분석_2. BERT_1. 사전 작업

eraser 2020. 4. 21. 13:30
반응형

 자연어 처리 분야에서 가장 성능이 좋다고 알려진 모델은 transformer network 기반 구글의 BERT 모델(참고)이다. NLP 감성분석의 기본으로서 순환신경망 모델을 구현해 보았으니, BERT 모델에 분류층을 적용하여 결과를 비교할 것이다. Pytorch로 쉽게 BERT를 적용할 수 있게 한 Hugging Face의 transformers 라이브러리를 활용하여 작업을 진행했다. 구현을 위해 Chris McCormick의 블로그를 참고했다.

 


# 사용 라이브러리

 

  • Pytorch
  • Tensorflow
  • Keras
  • Transformers : 한국어는 bert-base-multilingual-cased 모델에 사전훈련되어 있다. 
  • pandas, numpy, tqdm

 


 

# 사전 작업

 

 데이터를 로드하는 단계까지는 이전과 비슷하므로 같은 함수를 사용하여 진행했다. 필요한 모듈을 불러 오고, 경로를 설정한 뒤, 데이터를 로드하는 단계까지의 과정이다.

 다만, Pytorch 라이브러리를 사용할 때 라벨이 0부터 시작하지 않으면 GPU 에러가 나기 때문에, 부정 라벨(-1)을 2로 바꾸어주었다. 원핫 인코딩은 진행하지 않아도 된다.

 

# module import
import tensorflow as tf
import torch
from transformers import BertTokenizer
from transformers import BertForSequenceClassification, AdamW, BertConfig
from transformers import get_linear_schedule_with_warmup
from transformers import save_pretrained # 모델 저장
from torch.utils.data import TensorDataset, DataLoader, RandomSampler, SequentialSampler
from tensorflow.keras.preprocessing.sequence import pad_sequences
from sklearn.model_selection import train_test_split
import os
from tqdm import tqdm
import pickle
from tqdm import tqdm_notebook as tn_
import warnings
warnings.filterwarnings(action='ignore')

# set path
my_path = 'PATH' # 개인 작업 경로
project_path = 'PATH' # 작업 결과 파일을 저장할 공유 프로젝트 경로
cur_path = f"{my_path}/SentimentAnalysis"
output_path = f"{cur_path}/Output"


# 데이터 로드 함수
def load_data(path, is_train, verbose=0):

    data = pd.read_pickle(path)    
    data = data.dropna()
    data = data.drop("content_morph", axis=1)
    data.index = pd.RangeIndex(len(data.index))
    data['content'] = data['content'].str.replace("[^가-힣 ]", "")    
    
    if is_train:
        splitter = StratifiedShuffleSplit(n_splits=1, test_size=0.3, random_state=42)
        for train_idx, test_idx in splitter.split(data, data['label']):
            train_data = data.loc[train_idx]
            test_data = data.loc[test_idx]
        train_data.index = pd.RangeIndex(len(train_data.index))
        test_data.index = pd.RangeIndex(len(test_data.index))
        if verbose > 0:
            print(f"train_shape : {train_data.shape}, test_shape: {test_data.shape}")
            print(f"train set label 비율 \n {train_data.label.value_counts() / len(train_data)}")
            print(f"test set label 비율 \n {test_data.label.value_counts() / len(test_data)}")
        
        return train_data, test_data
    
    else:
        if verbose > 0:
            print(f"predict_shape : {data.shape}")

        return data
    
# load data for train
train_raw, test_raw = load_data(TRAIN_PATH, is_train=True)

# label preprocessing : to prevent GPU torch error
for i in range(len(train_raw)):
    if train_raw['label'][i] == -1:
        train_raw['label'][i] = 2
for i in range(len(test_raw)):
    if test_raw['label'][i] == -1:
        test_raw['label'][i] = 2

# split inputs and labels
X_train = train_raw['content']
y_train = train_raw['label']
X_test = test_raw['content']
y_test = test_raw['label']

 

 


 

 

# 토크나이징

 

 BERT 토크나이저를 이용한다. BERT 모델에서 사용하는 토크나이저는 wordpiece 모델(참고1, 참고2) 기반이다. 이전의 Keras 토크나이저와 달리, [CLS], [SEP], [UNK] 등 special token이 필요하다.

 transformers 라이브러리로부터 사전훈련된 bert-base-multilingual-cased 기반의 BertTokenizer를 불러 와 사용하면 된다. do_lower_case 옵션을 False로 주어야 한다.

 

1) 토크나이저로 인코딩

 

 BertTokenizer의 encode 메서드를 사용하면 special token까지 포함하여 정수 인코딩을 한 번에 진행할 수 있다. 길이가 긴 문장 역시 max_length 옵션을 통해 최대 길이를 지정하여 맞출 수 있다. 한국어는 128 정도로 지정하면 충분하다는 글이 많았기 때문에, 최대 길이 옵션은 128로 지정했다.

 

# encode sentences using BertTokenizer
def BertTokenize(data, tokenizer):
    result = []
    for sent in data:
        encoded_sent = tokenizer.encode(sent, 
                                        add_special_tokens = True, # special token 추가
                                        max_lentgh = 128)
        result.append(encoded_sent)
    
    return result

tokenizer = BertTokenizer.from_pretrained('bert-base-multilingual-cased', do_lower_case=False)

train_input = BertTokenize(X_train, tokenizer)

 

2) 단계별 인코딩

 

 위와 같은 방식으로 토크나이징을 했는데, 문제 없이 잘 되다가 예측을 위해 포털 댓글 데이터셋을 토크나이징할 경우 Colab 세션이 터져 버리는 문제가 발생했다. 이 경우를 대비해 토크나이징을 수작업으로 진행하는 것으로 코드를 바꿨다.

 special token을 문장 앞 뒤로 추가하고, BertTokenizer의 convert_tokens_to_ids 메서드를 사용해 정수로 인코딩했다. 문장 길이를 제한하고 패딩하는 과정이 추가적으로 필요하다.

 

def BertTokenize(data, tokenizer):
    
    # add special tokens
    sentences = ["[CLS] " + str(sentence) + " [SEP]" for sent in data]
    tokenized_texts = [tokenizer.tokenize(sent) for sent in sentences]

    result = []
    
    # encode into integers
    for text in tokenized_texts:   
        encoded_sent = tokenizer.convert_tokens_to_ids(text)
        result.append(encoded_sent)     

    return result

def pad_sentences(data, max_len=128):
    data = pad_sequences(data, maxlen=max_len, dtype='long', truncating='post', padding='post')   

tokenizer = BertTokenizer.from_pretrained('bert-base-multilingual-cased', do_lower_case=False)

# 데이터 토크나이징
train_input = BertTokenize(X_train, tokenizer)
test_input = BertTokenize(X_test, tokenizer)

# 문장 길이 제한 및 패딩
train_input_ids = pad_sentenses(train_input)
test_input_ids = pad_sentences(test_input)

 

 토크나이징한 결과를 단계별로 살펴 보면 다음과 같다. Bert 모델은 '다' '##다' 를 다른 토큰으로 취급한다. 정수로 인코딩하면 [CLS]가 101, [SEP]가 102가 된다. 토크나이저에 없는 토큰의 경우, [UNK]로 토크나이징되나, 아래의 예에서는 등장하지 않았다.

 패딩의 과정은 순환신경망 기반 모델 설정에서와 동일하므로 생략한다.

>>> ['[CLS]', '근', '##데', '재', '##계', '##1', '##위', '금', '##융', '##회', '##사', '다', '##니', '##는', '직', '##원', '아', '##는데', '##그', '##친', '##구', '##는', '토', '##일', '계속', '출', '##근', '평', '##일', '매', '##일', '야', '##근', '##한', '##달', '##에', '일', '##요일', '두', '##번', '##정', '##도', '쉬', '##다', '##라', '##구', '##요', '##눈', '##치', '##보', '##여', '##서', '특', '##근', '##비', '처', '##리', '##도', '맘', '##대로', '못', '##한', '##다', '##고', '했', '##었는데', '##작', '##년에', '들', '##었던', '이야기', '##로', '[SEP]']
>>> array([ 101, 8926, 28911, 9659, 21611, 10759, 19855, 8928, 119184, 14863, 12945, 9056, 25503, 11018, 9707, 14279, 9519, 41850, 78136, 55358, 17196, 11018, 9873, 18392, 77039, 9768, 50248, 9926, 18392, 9258, 18392, 9538, 50248, 11102, 89851, 10530, 9641, 82888, 9102, 35465, 16605, 12092, 9469, 11903, 17342, 17196, 48549, 118753, 18622, 30005, 29935, 12424, 9891, 50248, 29455, 9744, 12692, 12092, 9253, 37601, 9290, 119153, 71104, 11664, 9965, 74311, 38709, 27056, 9117, 61439, 110148, 11261, 102, 0, 0, 0, 0, 0, 0, 0, 0, 0, .....])

 

 


# Attention Mask 생성

 

 BERT 모델에서 Attention Mask의 역할은 매우 크다. (자세한 설명은 여기, 그리고 여기를 참고하자.)

 일단 지금은 인코딩된 토큰 시퀀스에서 패딩(0)에 해당하는 부분은 0, 패딩이 아닌 부분은 1로 간주하는 마스크를 생성한다. 이를 통해 패딩 부분은 모델 내에서 Attention을 수행하지 않아 학습 속도가 향상되는 효과를 얻을 수 있다.

 

def create_masks(data):
    masks = []
    for sent in data:
        mask = [float(s>0) for s in sent]
        masks.append(mask)
    return masks

# 어텐션 마스크 생성
train_input_masks = create_masks(train_input_ids)
test_input_masks = create_masks(test_input_ids)

 


# PyTorch 텐서 변환 및 데이터 로더 설정

 

 이제 훈련 데이터와 테스트 데이터를 PyTorch의 텐서 형태로 변환해 주어야 한다. 

 

 먼저, 훈련 데이터를 학습 세트와 검증 세트로 여러 차례 분할한다. 이 과정은 훈련 데이터에만 적용된다. 이후 데이터를 파이토치에서 사용하는 텐서 형태로 변환한다. 그리고 데이터셋을 배치 사이즈만큼 쉽게 가져올 수 있도록 PyTorch의 DataLoader를 활용하여 입력 데이터와 마스크, 라벨을 묶어 준다.

 

1) 훈련 세트 분할

 사이킷런의 train_test_split 함수를 이용한다. 이전과 달라진 점은, 마스크 역시 분할해 주어야 한다는 점이다.

 

# 훈련 세트의 입력, 라벨 분리
train_inputs, validation_inputs, train_labels, validation_labels = train_test_split(train_input_ids, y_train, random_state=42, test_size=0.25)

# 마스크 분리
train_masks, validation_masks, _, _ = train_test_split(train_input_masks, train_input_ids, random_state=42, test_size=0.25)

 

2) PyTorch 텐서 변환

 파이토치의 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)

# 테스트 데이터 텐서 변환
test_inputs = torch.tensor(test_input_ids)
test_labels = torch.tensor(y_test)
test_masks = torch.tensor(test_input_masks)

 

3) PyTorch 데이터 로더 설정

 변환된 텐서들을 파이토치의 Dataset 함수를 이용해 데이터셋으로 만들어 주고, 배치 사이즈를 32로 하여 훈련 데이터와 테스트 데이터에 대한 데이터 로더를 설정한다.

 

BATCH = 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)

# 검증 데이터 로더
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)

# 테스트 데이터 로더
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)

 

반응형