이 포스트에서는 OpenAI API의 Embedding을 활용하여 간단하게 데이터 분석을 해보고자 한다. 한국어 텍스트 데이터를 분석해보면 더 친숙할 것 같아서 한국어 혐오 발언 분류 데이터셋을 이용하기로 했다.
(데이터 및 패키지 링크: https://github.com/kocohub/korean-hate-speech)
아래는 이번 포스트에서 사용할 패키지들이다. 여기서 koco 패키지는 데이터셋을 위해 필요하다.
from openai import OpenAI
import os
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.metrics import confusion_matrix, accuracy_score
from sklearn.manifold import TSNE
from sklearn.utils.class_weight import compute_sample_weight
from xgboost import XGBClassifier
import koco
import matplotlib.pyplot as plt
import seaborn as sns
import graphviz
from matplotlib.colors import ListedColormap
graphviz.set_jupyter_format('png')
%matplotlib inline
이전 포스트에서 했던 것처럼 API key를 이용하여 OpenAI 객체를 불러온다.
2024.06.09 - [Python] - OpenAI API 이용하기
file = "KeyOpenAI.txt"
with open(os.path.normpath(file), "r") as f:
api_key = f.readline().strip()
os.environ["OPENAI_API_KEY"] = api_key
client = OpenAI()
그리고 labeling이 되어있는 한국어 혐오 발언 분류 데이터셋을 불러온다. 이 데이터셋은 dictionary로 train set 과 test set을 나눠놨다.
train_dev = koco.load_dataset('korean-hate-speech', mode='train_dev')
train_dev.keys()
dict_keys(['train', 'dev'])
이제 예시로 train data의 observation 하나를 살펴보자. 이 포스트는 OpenAI의 Embedding을 사용해보는 것이 목적이므로, 데이터 분석은 최대한 단순하게 해보고자 한다. 따라서, Input viariable은 comment로 하고, target은 hate로 설정하였다. 아래의 Output을 보면 알 수 있듯이 comment는 기사에 달린 댓글이고, hate은 댓글의 혐오 수준을 labeling 해놓은 변수로 none, offensive, hate 3개의 카테고리로 이루어져있다.
train_dev['train'][1]
{'comments': '....한국적인 미인의 대표적인 분...너무나 곱고아름다운모습...그모습뒤의 슬픔을 미처 알지못했네요ㅠ',
'contain_gender_bias': False,
'bias': 'none',
'hate': 'none',
'news_title': '"\'연중\' 故 전미선, 생전 마지막 미공개 인터뷰…환하게 웃는 모습 \'먹먹\'[종합]"'}
이제 아래의 함수들을 이용하면 Ebedding을 얻을 수 있다. 위의 truncate 함수는 내가 사용할 text-embedding-3-small 의 input size(8191)를 맞춰주기 위해 썼다. (사실 텍스트가 댓글이라 크기가 8000을 넘어갈 가능성은 거의 없지만...)
def truncate(s, n=6000):
return ' '.join(s.split()[:n])
def get_embedding(text, model="text-embedding-3-small"):
out = client.embeddings.create(input = [truncate(text)], model=model).data[0].embedding
return np.array(out)
텍스트 하나를 Embedding 해보면 output size가 1536임을 알 수 있다.
embedding = get_embedding(train_dev['train'][17]['comments'])
embedding.shape
(1536,)
본격적으로 train data 전체를 Embedding 하기 위해 category code를 설정해준다.
df_train = pd.DataFrame(train_dev['train'])
df_test = pd.DataFrame(train_dev['dev'])
lst = sorted(set(df_train['hate'].dropna().unique()))
lvl = pd.CategoricalDtype(lst)
df_train['hate'] = df_train['hate'].astype(lvl).cat.codes
df_test['hate'] = df_test['hate'].astype(lvl).cat.codes
그리고 train set과 test set 전체에 Embedding을 적용해보았다. Embedding은 데이터의 크기에 비례해서 비용이 발생하므로, 반드시 결과를 저장해놓도록 하자.
embeddings = df_train['comments'].apply(lambda x: get_embedding(x)).to_numpy()
np.save('embeddings.npy', embeddings)
embeddings_test = df_test['comments'].apply(lambda x: get_embedding(x)).to_numpy()
np.save('embeddings_test.npy', embeddings_test)
앞의 1000개의 text를 TSNE를 사용하여 시각화 해보았다. (패턴은 잘 보이진 않는다.)
tsne = TSNE(n_components=2, perplexity=15, random_state=42,
init='random', learning_rate=200)
vis_dims = tsne.fit_transform(np.vstack(embeddings[0:1000]))
# Get colormap
colors = plt.get_cmap('RdYlGn')(np.linspace(0.2, 0.7, 3))
# Set colours
x = [x for x,y in vis_dims]
y = [y for x,y in vis_dims]
color_indices = df_train['hate'].iloc[0:1000]
colormap = ListedColormap(colors)
ratings = ['hate', 'offensive', 'none']
# Create the plot
plt.scatter(x, y, c=color_indices, cmap=colormap, alpha=0.3, label=ratings)
plt.show()
#Define the classifier.
XGB_hate = XGBClassifier(max_depth=2,
learning_rate=0.1,
n_estimators=1000,
verbosity=1,
objective='multi:softmax',
booster='gbtree',
n_jobs=4,
gamma=0.001,
subsample=0.632,
colsample_bytree=1,
colsample_bylevel=1,
colsample_bynode=1,
reg_alpha=1,
reg_lambda=0,
random_state=20230331,
tree_method='hist',
eval_metric=accuracy_score,
)
Category들이 imbalance 하므로, sample weights도 구해서 train에 추가해주었다.
sample_weights = compute_sample_weight('balanced', y=df_train['hate'])
그리고 train set에서 validation set을 분리해서 fitting을 해보았다.
x_train_xgb, x_val_xgb, w_train, w_val, y_train_xgb, y_val_xgb = train_test_split(np.vstack(embeddings), sample_weights, df_train['hate'], test_size=0.33, random_state=20230331)
validation set의 accuracy score를 보니 예측력이 거의 동전 던지기에 가깝다는 것을 확인할 수 있다.
XGB_hate.fit(x_train_xgb, y_train_xgb,
sample_weight=w_train,
eval_set=[(x_train_xgb, y_train_xgb), (x_val_xgb, y_val_xgb)])
[0] validation_0-mlogloss:1.09211 validation_0-accuracy_score:0.44537 validation_1-mlogloss:1.09384 validation_1-accuracy_score:0.41366
[1] validation_0-mlogloss:1.08623 validation_0-accuracy_score:0.47524 validation_1-mlogloss:1.08965 validation_1-accuracy_score:0.44321
[2] validation_0-mlogloss:1.08111 validation_0-accuracy_score:0.47751 validation_1-mlogloss:1.08608 validation_1-accuracy_score:0.44628
...
[998] validation_0-mlogloss:0.30589 validation_0-accuracy_score:0.98847 validation_1-mlogloss:0.95510 validation_1-accuracy_score:0.53377
[999] validation_0-mlogloss:0.30554 validation_0-accuracy_score:0.98847 validation_1-mlogloss:0.95515 validation_1-accuracy_score:0.52993
마지막으로 test set에 대해 model을 적용하고, confusion matrix를 만들었다. 아래 이미지를 보면 offensive에 대해서는 나쁘지 않게 예측하는데 나머지 카테고리에 대한 예측력은 약하다는 것을 확인할 수 있다.
# Predict over test set
XGB_pred_test = XGB_hate.predict(np.vstack(embeddings_test))
# Calculate confusion matrix
confusion_matrix_cs = confusion_matrix(y_true = df_test['hate'],
y_pred =XGB_pred_test)
# Turn matrix to percentages
confusion_matrix_cs = confusion_matrix_cs.astype('float') / confusion_matrix_cs.sum(axis=1)[:, np.newaxis]
# Turn to dataframe
df_cm = pd.DataFrame(
confusion_matrix_cs, index=ratings,
columns=ratings,
)
# Parameters of the image
figsize = (10,7)
fontsize=14
# Create image
fig = plt.figure(figsize=figsize)
heatmap = sns.heatmap(df_cm, annot=True, fmt='.2f')
# Make it nicer
heatmap.yaxis.set_ticklabels(heatmap.yaxis.get_ticklabels(), rotation=0,
ha='right', fontsize=fontsize)
heatmap.xaxis.set_ticklabels(heatmap.xaxis.get_ticklabels(), rotation=45,
ha='right', fontsize=fontsize)
# Add labels
plt.ylabel('True label')
plt.xlabel('Predicted label')
# Plot!
plt.show()