프로젝트

KoBERT 이용해서 7가지 감정 분류하기(KoBERT 모델 오류 이 글보면 해결 가능) - 1

꿈이많은띵동이 2024. 1. 13. 17:21

2023.08.24에 작성한 글(비공개로 올린거) 티스토리에 올리기 위해 긁어왔다
KoBERT 모델을 돌리기 위해 오류를 해결하고 해결하면서 한 달 넘은 시간을 허비했는데 구글링해서 해결 방법을 따라해도 해결되지 않고
해결된다한들 정확도가 0.5도 넘지 않으며
딥러닝은 누가 먼저 오류를 해결하냐가 관건인 분야인 것 같다는 생각마저 들 때
어지쩌지 KoBERT 전문가 깃허브에서 부족한 지식 공부하고 공부해서 해결된 프로젝트!!!!!
네이버, 구글에 나온 방법 따라해도 해결되지 않는 딥러닝 비기너분들 이 방법 쓰세요 공유합니다

아무 문장이나 입력하면 7가지의 감정 중 문장의 감정을 분류하는 모델을 구현했다
감정은 분노 0 슬픔 1 공포 2 역겨움 3 중립 4 놀라움 5 행복 6 으로 7가지다
이 프로젝트의 방향은 본인의 감정을 글로 작성하면 그 감정과 관련한 노래를 추천해주는 시스템을 만드는 것
(예시: 오늘 하루 힘들었어 -> 소녀시대-힘내!) 조롱하는 거 같기도 ㅎㅎ

!pip install transformers
import transformers
transformers.__version__
import pandas as pd
import numpy as np
import urllib.request #URL(HTTP)을 여는데 사용
import os
from tqdm import tqdm #진행률 프로세스바
import tensorflow as tf
from transformers import BertTokenizer, TFBertModel

urllib 메소드는 여기에 쓸 필요 없음
사이트에서 데이터를 갖고 올 때 필요한 메소드인데 우리는 파일을 갖고 있기 때문에 생략 가능

from google.colab import files
uploaded = files.upload()

구글 코랩에서 c드라이브에 있는 파일 불러오려면 이 코드 쓰면 됨

data = pd.read_csv("realdata.csv")
data.drop('Unnamed: 0' ,axis=1, inplace=True)
data.columns=['document','label']
data.head()

분노 0 슬픔 1 공포 2 역겨움 3 중립 4 놀라움 5 행복 6 으로 되어있음

from sklearn.model_selection import train_test_split
train_data, test_data = train_test_split(data, test_size=0.2, random_state=0)

print(len(train_data)) #35192 출력 test_size가 0.2일 때
print(len(test_data)) #8799 출력
train_data = train_data.dropna(how = 'any') # Null 값이 존재하는 행 제거
train_data = train_data.reset_index(drop=True)
print(train_data.isnull().values.any()) # Null 값이 존재하는지 확인 -> False

test_data = test_data.dropna(how = 'any') # Null 값이 존재하는 행 제거
test_data = test_data.reset_index(drop=True)
print(test_data.isnull().values.any()) # Null 값이 존재하는지 확인 -> False

결측값 없는 것으로 확인

tokenizer = BertTokenizer.from_pretrained('klue/bert-base') #미리 학습된 토크나이저 불러오기

데이터를 BERT 모델에 맞는 형태로 변경해야 함
수행해야 하는 작업은 다음과 같다

•  각 문장에 [CLS], [SEP] 토큰 추가하기 ([CLS]: 문장 시작 토큰, [SEP]: 문장 끝 토큰)
• 각 문장을 BERT 방식으로 토큰화
• 각 문장의 길이를 통일하기 → 길이가 짧은 경우 [PAD] 토큰 추가
• Attenion Mask 부여 : 원래 문장의 토큰은 1로, 길이 맞추기 위해서 추가된 토큰은 0 값 부여([PAD])

이 과정을 BERT에서 제공하는 tokenizer를 사용해서 실시함

max_seq_len = 128

def convert_examples_to_features(examples, labels, max_seq_len, tokenizer):

    input_ids, attention_masks, token_type_ids, data_labels = [], [], [], []
#input_ids: 각 문서를 구성하는 토큰의 ID 정보를 저장하는 리스트
#attention_masks: 각 문서의 어텐션 마스크 정보를 저장하는 리스트
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) #문서 길이 128로 통일
        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 #세그먼트 인코딩(첫 번째 문장 0, 두 번째 문장 1)

        assert len(input_id) == max_seq_len, "Error with input length {} vs {}".format(len(input_id), max_seq_len)
        assert len(attention_mask) == max_seq_len, "Error with attention mask length {} vs {}".format(len(attention_mask), max_seq_len)
        assert len(token_type_id) == max_seq_len, "Error with token type length {} vs {}".format(len(token_type_id), 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.asarray(data_labels, dtype=np.int32) #asarray: copy=False 원본이 변경되면 복사본도 변됨

    return (input_ids, attention_masks, token_type_ids), data_labels #3개의 행렬은 X, data_labels는 y

input_ids: 단어에 대한 정수 인코딩
attention_masks: 어텐션 마스크
token_type_ids: 세그먼트 인코딩

model = TFBertModel.from_pretrained("klue/bert-base", from_pt=True)

사전학습 BERT 모델을 불러옴
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)

outputs = model([input_ids_layer, attention_masks_layer, token_type_ids_layer])
print(outputs)

TFBaseModelOutputWithPoolingAndCrossAttentions(last_hidden_state=<KerasTensor: shape=(None, 128, 768) dtype=float32 (created by layer 'tf_bert_model')>, pooler_output=<KerasTensor: shape=(None, 768) dtype=float32 (created by layer 'tf_bert_model')>, past_key_values=None, hidden_states=None, attentions=None, cross_attentions=None)
이 나옴

# TPU 작동을 위한 코드 TPU 작동을 위한 코드
resolver = tf.distribute.cluster_resolver.TPUClusterResolver(tpu='grpc://' + os.environ['COLAB_TPU_ADDR'])
tf.config.experimental_connect_to_cluster(resolver)
tf.tpu.experimental.initialize_tpu_system(resolver)

런타임을 TPU로 바꿔야함(구글 코랩->런타임->런타임 유형 변경)
이유는 데이터가 크기 때문에 속도를 높이기 위해서라고 함
TPU: 머신러닝 과정에서 생기는 작업 부하를 빠르게 처리하는 역할
일반 프로세서로 딥러닝을 진행하면 대량으로 한 번에 유입된 데이터를 빠르게 처리하지 못해 병목현상이 발생하는데 TPU를 이용하면 메모리가 데이터를 읽는 속도를 대폭 줄이면서 빠르게 처리함

strategy = tf.distribute.experimental.TPUStrategy(resolver)

rom transformers import TFBertForSequenceClassification

with strategy.scope():
  model = TFBertForSequenceClassification.from_pretrained("klue/bert-base", num_labels=7, from_pt=True)
  optimizer = tf.keras.optimizers.Adam(learning_rate=5e-5)
  model.compile(optimizer=optimizer, loss=model.hf_compute_loss, metrics = ['accuracy'])

감정분류 구글링 자료를 보면 보통 긍정, 부정 두 가지로 나누기 때문에 Sigmoid(라벨 2개)를 이용하는데 우리는 7가지 감정으로 분류해야 하기 때문에 Softmax(라벨 3개 이상)를 써야함
또한 구글링 자료를 이용하면 왜인지 모르는 곳에서 계속 오류가 발생함
찾아보니 문자 분류 수행이 목적이면 TFBertForSequenceClassification 클래스를 활용한다고 한다(얻어 걸렸다)
무튼 함수 안 만들고 클래스가 따로 있으면 나야 개이득

model.fit(train_X, train_y, epochs=2, batch_size=64)

batch: 딥러닝에서 모델의 가중치를 한 번 업데이트 시킬 때 사용되는 샘플들의 묶음
만약 총 1000개의 훈련 샘플이 있는데 배치 사이즈가 20이라면 20개의 샘플 단위마다 모델의 가중치를 한 번씩 업데이트 시킴
즉, 총 50번 가중치 업데이트가 됨 -> 하나의 데이터셋을 총 50개의 배치로 나눠서 훈련을 진행
epoch: 학습의 횟수
-> 배치 사이즈가 20이고 에포크가 10이면, 가중치를 50번 업데이트하는 것을 총 10번 반복

이 경우 시간이 많이 걸린다
epochs가 클수록 시간이 더 걸림
Epoch 1/2 550/550 [==============================] - 175s 169ms/step - loss: 0.7513 - accuracy: 0.7365
Epoch 2/2 550/550 [==============================] - 53s 96ms/step - loss: 0.5501 - accuracy: 0.8027 <keras.callbacks.History at 0x784f8647f970>
에포크가 클수록 정확도가 올라가나? 에포크를 5로 늘려보자 (당연한 거였네)

model.fit(train_X, train_y, epochs=5, batch_size=64)

Epoch 1/5 550/550 [==============================] - 53s 96ms/step - loss: 0.4474 - accuracy: 0.8373
Epoch 2/5 550/550 [==============================] - 53s 96ms/step - loss: 0.3584 - accuracy: 0.8722
Epoch 3/5 550/550 [==============================] - 53s 97ms/step - loss: 0.2889 - accuracy: 0.8956
Epoch 4/5 550/550 [==============================] - 53s 96ms/step - loss: 0.2392 - accuracy: 0.9139
Epoch 5/5 550/550 [==============================] - 53s 96ms/step - loss: 0.2083 - accuracy: 0.9254
역시 비례관계

results = model.evaluate(test_X, test_y, batch_size=1024)
print("test loss, test acc: ", results)

9/9 [==============================] - 20s 895ms/step - loss: 0.6336 - accuracy: 0.7708 test loss, test acc: [0.6335794925689697, 0.7707694172859192]
정확도는 0.7708
이정도면 나쁘지 않다 왜냐면 데이콘인가 캐글의 1위 정확도가 0.5대였던 걸로 알고 있음
하 우리가 나가서 씹어먹었어야 됐는데 ㅎㅎ

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 = list(model.predict(encoded_input)[0][0])

  result = score.index(max(score))

  return print(score, "\n 결과: ", result)

위 함수는 내가 마음대로 문장을 작성하면 감정의 라벨을 만들어주는 함수다
원래는 return이 이게 아니었는데 내가 원하는 결과는 감정의 라벨만 알면 되기 때문에 일부 수정함
score이 7개의 라벨의 각 확률을 알려준다 그래서 그 중 가장 높은 확률의 index를 result에 대입함

결과 잘 나오자마자 이 상황을 급히 팀원들에게 공유했다
팀원들과 이 프로젝트(그저 저 모델 하나를) 한 달동안 잡고 있어서 자신감이 바닥 났던 상태라 성공했어도 내가 날 못 믿는 수준에 이르렀네 ㅎㅎ
무튼 모델을 구성했으니 이제 웹을 구축해서 어떻게 노래를 추천할 지 논의해봐야겠다 행복하다

지금까지 나온 아이디어는 데이터베이스에 각 감정과 관련한 노래를 몇 개 넣어놓고 숫자가 겹치면 random으로 추천하는 시스템이 나왔는데 나쁘지 않고 지금 우리의 지식에서 딱 가능한 수준이다
노래 추천 알고리즘? 모델같은 것도 있다는데 이걸 또 공부해서 한 번 해볼지 데이터베이스로 해결할 지 얘기를 해봐야겠다
동기들과 대충 대충하자고 전에 패배주의 비슷한 태도로 임했는데 이렇게 모델 하나 잘 구축하니까 욕심이 생겼다
할 수 있는 데까지 해보자