KoBERT를 이용한 네이버 영화 리뷰 분류
를해보려고 합니다.
import pandas as pd
import numpy as np
import urllib.request
import os
from tqdm import tqdm
import tensorflow as tf
from transformers import BertTokenizer, TFBertModel
기본임포트를 해주고 링크에서 받아온것을 파일로 다운받도록 하겠습니다.
# 네이버 영화 리뷰 데이터 학습을 위해 훈련 데이터와 테스트 데이터를 다운로드합니다.
urllib.request.urlretrieve("https://raw.githubusercontent.com/e9t/nsmc/master/ratings_train.txt",
filename="ratings_train.txt")
urllib.request.urlretrieve("https://raw.githubusercontent.com/e9t/nsmc/master/ratings_test.txt",
filename="ratings_test.txt")
이후에 판다스로 가져오도록 하겠습니다.
train_data = pd.read_table('ratings_train.txt')
test_data = pd.read_table('ratings_test.txt')
train_data.shape, test_data.shape
((150000, 3), (50000, 3))
HEAD를 찍어보면
로 볼 수있습니다.
중복데이터 그리고 결측값을 제거하려고 합니다.
# 중복데이터와 결측값 제거
train_data.drop_duplicates(subset=['document'], inplace=True) # document 열 중복 제거.
train_data = train_data.dropna(how='any') # 결측치 제거
이후 SHPAE를 찍어보면 (146182, 3)으로 줄어든것을 확인할 수 있습니다.
테스트 데이터에서는 결측치도 제거하도록 하겠습니다.
# 테스트 데이터에선 결측치 제거
test_data = test_data.dropna(how = 'any')
test_data.shape
TEST데이터의 쉐이프는 (49997,3)으로 확인됩니다.
BERT의 입력
tokenizer = BertTokenizer.from_pretrained('klue/bert-base')
print(tokenizer.tokenize("보는 내내 그대로 들어맞는 예측 카리스마 없는 악역"))
['보', '##는', '내내', '그대로', '들어맞', '##는', '예측', '카리스마', '없', '##는', '악역']
이어지는 토큰에는 ##이 붙는것을 볼 수 있습니다.
아래는 정수인코딩된 결과를 확인할 수 있습니다.
tokenizer.decode(tokenizer.encode("보는 내내 그대로 들어맞는 예측 카리스마 없는 악역"))
[2, 1160, 2259, 6404, 4311, 20657, 2259, 5501, 13132, 1415, 2259, 23713, 3]
위는 인코드고 아래는 디코드를 확인할 수 있습니다.
tokenizer.decode(tokenizer.encode("보는 내내 그대로 들어맞는 예측 카리스마 없는 악역"))
[CLS] 보는 내내 그대로 들어맞는 예측 카리스마 없는 악역 [SEP]
tokenizer.cls_token, tokenizer.cls_token_id
('[CLS]', 2)
tokenizer.sep_token, tokenizer.sep_token_id
('[SEP]', 3)
tokenizer.pad_token, tokenizer.pad_token_id
('[PAD]', 0)
입력: 정수 인코딩
max_seq_len = 128
encoded_result = tokenizer.encode('전율을 일으키는 영화. 다시 보고 싶은 영화',
max_length=max_seq_len, pad_to_max_length=True)
print(encoded_result)
print('길이:', len(encoded_result))
Truncation was not explicitly activated but `max_length` is provided a specific value, please use `truncation=True` to explicitly truncate examples to max length. Defaulting to 'longest_first' truncation strategy. If you encode pairs of sequences (GLUE-style) with the tokenizer you can select this strategy more precisely by providing a specific strategy to `truncation`.
[2, 1537, 2534, 2069, 6572, 2259, 3771, 18, 3690, 4530, 1335, 2073, 3771, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
길이: 128
입력: 세그먼트 인코딩
print([0] * max_seq_len)
[0,0,0,0,0,0,0,0,0,...]으로 128개가 나열된 모습을 확인할 수 있습니다.
입력: 어텐션 마스크 인코딩
valid_num = len(tokenizer.encode('전율을 일으키는 영화. 다시 보고 싶은 영화'))
print(valid_num * [1] + (max_seq_len - valid_num) * [0])
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 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]
의 결과를 확인할 수 있습니다.
1과 0으로 구성되어있고 1은 14개고 나머지는 0으로 채워지면 됩니다.
앞에는 1이되어있고 뒤에는 0으로 채워져있는것을 볼 수있습니다.
전체 데이터에 대해서 진행
def convert_examples_to_features(examples, labels, max_seq_len, tokenizer):
input_ids, attention_masks, token_type_ids, data_labels = [], [], [], []
for example, label in tqdm(zip(examples, labels), total=len(examples)):
# 정수인코딩
input_id = tokenizer.encode(example, max_length=max_seq_len, pad_to_max_length=True)
# 어텐션 마스크
padding_count = input_id.count(tokenizer.pad_token_id)
attention_mask = [1] * (max_seq_len - padding_count) + [0] * padding_count
# 세그먼트 인코딩
token_type_id = [0] * max_seq_len
input_ids.append(input_id)
attention_masks.append(attention_mask)
token_type_ids.append(token_type_id)
data_labels.append(label)
# 결과
input_ids = np.array(input_ids, dtype=int)
attention_masks = np.array(attention_masks, dtype=int)
token_type_ids = np.array(token_type_ids, dtype=int)
data_labels = np.array(data_labels, dtype=int)
return (input_ids, attention_masks, token_type_ids), data_labels
함수를 만들었습니다.
convert_examples_to_features에 매개변수로 (examples, labels, max_seq_len, tokenizer):을 넣었고
input_ids, attention_masks, token_type_ids, data_labels를 빈 리스트로 준비를 했습니다.
또한 훈련데이터, 테스트 데이터에 대해서 각각 진행해보려고합니다.
# 훈련데이터에 대해서 진행
train_X, train_y = convert_examples_to_features(train_data['document'], train_data['label'],
max_seq_len = max_seq_len, tokenizer=tokenizer)
# 테스트 데이터에 대해서 진행
test_X, test_y = convert_examples_to_features(test_data['document'], test_data['label'],
max_seq_len=max_seq_len, tokenizer=tokenizer)
# 훈련데이터의 첫번째 샘플 출력
input_id = train_X[0][0]
attention_mask = train_X[1][0]
token_type_id = train_X[2][0]
label = train_y[0]
print('단어에 대한 정수 인코딩:', input_id)
print('어텐션 마스크:', attention_mask)
print('세그먼트 인코딩:', token_type_id)
print('각 인코딩 의 길이:', len(input_id))
print('정수 인코딩 복원:', tokenizer.decode(input_id))
print('레이블 :',label)
에 대한 값은 아래처럼 나오게 됩니다.
단어에 대한 정수 인코딩: [ 2 1376 831 2604 18 18 4229 9801 2075 2203 2182 4243 3 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0]
어텐션 마스크: [1 1 1 1 1 1 1 1 1 1 1 1 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 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
각 인코딩 의 길이: 128
정수 인코딩 복원: [CLS] 아 더빙.. 진짜 짜증나네요 목소리 [SEP] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD]
레이블 : 0
아 더빙~ 목소리까지는 1로 채워져있는것을 확인할 수 있습니다.
BERT의 출력 이해하기.
model = TFBertModel.from_pretrained('klue/bert-base', from_pt=True)
사전학습된 모델을 가져오고
max_seq_len = 128 # 입력 문장의 길이
input_ids_layer = tf.keras.layers.Input(shape=(max_seq_len,), dtype=tf.int32)
attention_masks_layer = tf.keras.layers.Input(shape=(max_seq_len,), dtype=tf.int32)
token_type_ids_layer = tf.keras.layers.Input(shape=(max_seq_len,), dtype=tf.int32)
# BERT 모델의 출력결과
outputs = model([input_ids_layer, attention_masks_layer, token_type_ids_layer])
outputs에는 두개의 출력이 존재합니다.
print(outputs[0])
KerasTensor(type_spec=TensorSpec(shape=(None, 128, 768), dtype=tf.float32, name=None), name='tf_bert_model/bert/encoder/layer_._11/output/LayerNorm/batchnorm/add_1:0', description="created by layer 'tf_bert_model'")
print(outputs[1])
KerasTensor(type_spec=TensorSpec(shape=(None, 768), dtype=tf.float32, name=None), name='tf_bert_model/bert/pooler/dense/Tanh:0', description="created by layer 'tf_bert_model'"
로 결과를 확인할 수 있습니다.
outputs[0]은 batchsize가 128, 768이고 문장의 개수만큼의 출력입니다.
문장의 길이 개수만큼의 출력을 얻어낸것입니다.
만약 Many-to-Many 태스크의 경우 outputs[0] 을 사용하면되겠죠
outputs[1] 은 (batch size, 768)
[CLS] 토큰 위치의 출력. Many-to_One 태스크의 경우 outputs[1] 을 사용.
지금과 같은 영화리뷰 분류 문제는 이에 해당합니다.
각 출력을 확인해보면 텐서객체로 확인되는것을 볼 수 있습니다.
[1]도 마찬가지고 케라스 개체가 나오는것을 확인할 수 있습니다.
BERT를 이용한 Many - To - One 모델 만들기
모델상속을 받아서 만들겁니다.
class TFBertForSequenceClassification(tf.keras.Model):
def __init__(self, model_name):
super(TFBertForSequenceClassification, self).__init__()
self.bert = TFBertModel.from_pretrained(model_name, from_pt=True)
self.classifier = tf.keras.layers.Dense(1,
kernel_initializer=tf.keras.initializers.TruncatedNormal(0.02),
activation='sigmoid', name='classifier')
def call(self, inputs):
input_ids, attention_mask, token_type_ids = inputs
outputs = self.bert(input_ids=input_ids, attention_mask=attention_mask, token_type_ids=token_type_ids)
cls_token = outputs[1]
prediction = self.classifier(cls_token)
return prediction
사전학습된 모델을 가지고오고 dense를 거쳐서 가는것이고 1아니면0이기때문에 뉴런을 1개만 주면됩니다.
model = TFBertForSequenceClassification('klue/bert-base')
optimizer = tf.keras.optimizers.Adam(learning_rate=5e-5)
loss = tf.keras.losses.BinaryCrossentropy()
model.compile(optimizer=optimizer, loss=loss, metrics = ['accuracy'])
model.fit(train_X, train_y, epochs=2, batch_size=64, validation_split=0.2)
학습후 아래코드처럼 평가까지 해보겠습니다.
results = model.evaluate(test_X, test_y, batch_size=1024)
print('test loss, test acc:', results)
49/49 [==============================] - 405s 8s/step - loss: 0.2523 - accuracy: 0.8978
test loss, test acc: [0.2523243725299835, 0.8978338837623596]
acc이 89%정도로나오고 loss값이 25%정도 나오는것으로 확인이 됩니다.
예측하기
def sentiment_predict(new_sentence):
input_id = tokenizer.encode(new_sentence, max_length=max_seq_len, pad_to_max_length=True)
padding_count = input_id.count(tokenizer.pad_token_id)
attention_mask = [1] * (max_seq_len - padding_count) + [0] * padding_count
token_type_id = [0] * max_seq_len
input_ids = np.array([input_id])
attention_masks = np.array([attention_mask])
token_type_ids = np.array([token_type_id])
encoded_input = [input_ids, attention_masks, token_type_ids]
score = model.predict(encoded_input)[0][0]
if(score > 0.5):
print(score * 100, '% 확률로 긍정 리뷰')
else:
print((1 - score) * 100, '% 확률로 부정 리뷰')
/usr/local/lib/python3.10/dist-packages/transformers/tokenization_utils_base.py:2673: FutureWarning: The `pad_to_max_length` argument is deprecated and will be removed in a future version, use `padding=True` or `padding='longest'` to pad to the longest sequence in the batch, or use `padding='max_length'` to pad to a max length. In this case, you can give a specific length with `max_length` (e.g. `max_length=45`) or leave max_length to None to pad to the maximal input size of the model (e.g. 512 for Bert).
warnings.warn(
1/1 [==============================] - 3s 3s/step
99.6331817470491 % 확률로 부정 리뷰
1/1 [==============================] - 0s 51ms/step
99.62003827095032 % 확률로 긍정 리뷰
1/1 [==============================] - 0s 49ms/step
89.78744745254517 % 확률로 부정 리뷰
1/1 [==============================] - 0s 50ms/step
96.74618244171143 % 확률로 긍정 리뷰
1/1 [==============================] - 0s 50ms/step
97.44027853012085 % 확률로 긍정 리뷰
1/1 [==============================] - 0s 50ms/step
95.54433152079582 % 확률로 부정 리뷰
1/1 [==============================] - 0s 51ms/step
97.32261467725039 % 확률로 부정 리뷰
1/1 [==============================] - 0s 49ms/step
97.57013488560915 % 확률로 부정 리뷰
1/1 [==============================] - 0s 50ms/step
91.27309322357178 % 확률로 긍정 리뷰
까지 확률에 대해서 확인할 수 있습니다.
'AI > 자연어처리' 카테고리의 다른 글
GPT 를 이용한 영화 리뷰 분류 (38) (0) | 2025.01.10 |
---|---|
GPT (37) (0) | 2025.01.09 |
BERT 의 MLM, NSP (35) (0) | 2025.01.07 |
BERT(Bidirectional Encoder Representations from Transformers)_(34) (0) | 2025.01.06 |
Transformer (33-4 한국어 챗봇 구현하기) (1) | 2025.01.03 |