지난번 포스팅에서 전처리 및 EDA를 수행한 내용을 바탕으로 모델링을 진행했습니다.
3가지 모델을 후보로 실험 및 검증을 진행했습니다. 전처리 및 EDA 과정은 이전 글을 참고 바랍니다.
세 가지로 후보를 둔 이유는 다음과 같습니다.
+) XGBoost는 하이퍼 파라미터 튜닝이 중요하기 때문에, 추가적으로 GridSearchCV를 이용하여 최적의 파라미터를 찾고 학습을 진행하였습니다.
+) 클래스 불균형 문제를 해결하기 위해서 불균형한 클래스의 샘플 비율에 따라서 자동으로 가중치 조정을 하는 'compute_class_weight'를 이용하였습니다.
-LinearSVC는 선형 결정 경계를 통해 클래스를 분리하기 때문에, 비교적 간단하면서도 높은 성능을 발휘할 수 있다.
+) LinearSVC도 XGBoost와 마찬가지로 최적의 C값을 찾기 위해서 GridSearchCV를 사용했습니다.
모델링: XGBoost (eXtreme Gradient Boosting)
XGBoost를 사용하기 위해서 TF-IDF를 통한 벡터화와 label을 숫자로 매핑하기 위해 Label Encoder로 전처리를 한번 더 수행했습니다.
posts = data['posts'] # 설명변수
MBTItype = data['type'] # 예측변수
# numpy배열로 변환
posts_list = posts.to_numpy()
type_list = MBTItype.to_numpy()
type_list
array(['INTJ', 'INTJ', 'INTJ', ..., 'INTP', 'INFP', 'INFP'], dtype=object)
posts_list
array(['know tool use interaction people excuse antisocial truly enlighten mastermind know would count pet peeze something time matter people either whether group people mall never see best friend sit outside conversation jsut listen want interject sit formulate say wait inject argument thought find fascinate sit watch people talk people fascinate sit class watch different people find intrigue dad stand look like line safeway watch people home talk people like think military job people ...(생략)
import sklearn
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.feature_extraction.text import TfidfTransformer
# posts_list를 모델에 사용할 토큰 개수 행렬로 벡터화
cntizer = CountVectorizer(analyzer="word",
max_features=1000,
max_df=0.7,
min_df=0.1)
# analyzer="word": 텍스트를 단어 단위로 분석
# max_features=1000: 최대 1000개의 단어 피처를 선택
# max_df=0.7: 문서 빈도가 70% 이상인 단어는 무시
# min_df=0.1: 문서 빈도가 최소 10% 이상
print("Using CountVectorizer :")
# posts_list를 토큰 개수 행렬로 변환
X_cnt = cntizer.fit_transform(posts_list)
feature_names = list(enumerate(cntizer.get_feature_names_out()))
print("10 feature names can be seen below")
print(feature_names[0:10])
tfizer = TfidfTransformer()
print("\nUsing Tf-idf :")
print("Now the dataset size is as below")
# 카운트 행렬 X_cnt를 tf-idf 표현으로 변환
X_tfidf = tfizer.fit_transform(X_cnt).toarray()
print(X_tfidf.shape)
Using CountVectorizer :
10 feature names can be seen below
[(0, 'ability'), (1, 'able'), (2, 'absolutely'), (3, 'accept'), (4, 'accurate'), (5, 'across'), (6, 'act'), (7, 'action'), (8, 'actual'), (9, 'actually')]
Using Tf-idf :
Now the dataset size is as below
(114742, 672)
#counting top 10 words
reverse_dic = {}
for key in cntizer.vocabulary_:
reverse_dic[cntizer.vocabulary_[key]] = key
top_10 = np.asarray(np.argsort(np.sum(X_cnt, axis=0))[0,-10:][0, ::-1]).flatten()
[reverse_dic[v] for v in top_10]
XGBoost를 사용하기 위해 LabelEncoder를 사용했습니다. XGBoost는 기본적으로 숫자형 데이터를 다루는데 특화되어 있기 때문에 범주형 변수를 사용하려면 이를 숫자로 변환해야 합니다.
제가 사용한 데이터셋은 16개의 범주형 변수가 사용되었기 때문에 이를 0~15의 숫자로 매핑하여 모델이 이해할 수 있는 형태로 변환해주었습니다.
#XGBoost를 사용하기 위해 LabelEncoder 사용
from sklearn.preprocessing import LabelEncoder
le = LabelEncoder()
le.fit(type_list)
type_list = le.transform(type_list)
from sklearn.model_selection import train_test_split
X_data = X_tfidf
y_data = type_list
X, X_test, y, y_test = train_test_split(X_data, y_data, test_size=0.2, random_state=1)
X_train, X_valid, y_train, y_valid = train_test_split(X, y, test_size=0.2, random_state=1)
from xgboost import XGBClassifier
from sklearn.model_selection import GridSearchCV
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
import numpy as np
xgb_model = XGBClassifier(n_estimators=100)
params = {'learning_rate': [0.1, 0.06, 0.02], 'max_depth':[3, 5, 7], 'colsample_bytree':[0.3, 0.5,0.75]} # 'n_estimators': [100, 300, 500],'min_child_weight':[1,3], // 'scale_pos_weight': [1, 5, 10] 를 작성해도 사용되지 않아 경고가 뜸.
# GridSearchCV 객체 생성
gridcv = GridSearchCV(xgb_model, param_grid=params, cv=3, scoring='f1_weighted')
# 파라미터 튜닝 시작
gridcv.fit(X_train, y_train, early_stopping_rounds=30, eval_metric='mlogloss', eval_set=[(X_valid, y_valid)])
# 튜닝된 파라미터 출력
print(gridcv.best_params_)
print(gridcv.best_score_)
클래스 불균형 문제 해소를 위해서 하이퍼 파라미터 튜닝을 할때, scoring='f1_weighted'와 같이 설정하여 가중치를 적용한 평균 F1-score를 통해 최적의 하이퍼 파라미터를 구했습니다.
다음과 같이 하이퍼 파라미터 튜닝 결과가 나왔으며, 이를 바탕으로 학습을 진행하였습니다.
하이퍼 파라미터 튜닝 결과:
from xgboost import XGBClassifier
from sklearn.model_selection import GridSearchCV
# 1차적으로 튜닝된 파라미터를 가지고 객체 생성
xgb_model = XGBClassifier(n_estimators=500, learning_rate=0.1, max_depth=7, min_child_weight=3, colsample_bytree=0.75, reg_alpha=0.03)
# 학습
xgb_model.fit(X, y, early_stopping_rounds=200, eval_metric='mlogloss', eval_set=[(X_test, y_test)])
from sklearn.metrics import accuracy_score, precision_score, recall_score, confusion_matrix, f1_score
xgb_pred = xgb_model.predict(X_test)
accuracy = accuracy_score(y_test, xgb_pred)
precision = precision_score(y_test, xgb_pred, average='weighted')
recall = recall_score(y_test, xgb_pred, average='weighted')
f1 = f1_score(y_test, xgb_pred, average = 'weighted')
print(accuracy)
print(precision)
print(recall)
print(f1)
print('정확도 : {:.4f}\n정밀도 : {:.4f}\n재현율 : {:.4f}\nf1-score : {:.4f}'.format(accuracy, precision, recall, f1))
프로젝트 시작의 목적은 University of Southern California에서 주최한 2023년도 한미 해커톤을 위한 개발이었기 때문에 시간 부족으로 인해 전처리 및 EDA 작업이 부족했습니다. 클래스 불균형 문제의 심각성을 알고 있었지만, 이를 고려하여 모델링을 진행하기 어려웠습니다.
프로젝트 초기에 LinearSVC를 이용하여 개발했던 알고리즘은 accuracy만을 고려하여 모델 성능 평가를 진행했는데 84%라는 정확도만 보고 성능이 좋다고 판단했습니다. 이는 클래스 불균형을 전혀 고려하지 않은 성능 평가입니다.
클래스 불균형 문제가 있을 때, accuracy만을 사용하여 모델 평가를 하면 안되는 이유:
F1-score를 사용해야 하는 이유:
따라서, 성능 평가에 F1-score 또한 포함하기로 결정하였습니다.
결과적으로, 첫 모델은 accuracy는 높지만 f1-score가 아주 낮았기 때문에 현재의 모델도 성능이 좋다고 판단할 수는 없으나 개선이 되었다는 것을 알 수 있었습니다. 모델의 성능이 좋지 않은 이유는 XGBoost와 같은 gradient boosting 알고리즘은 주로 수치형 데이터에 대한 모델링에 뛰어난 성능을 보이지만, 텍스트와 같은 비정형 데이터에는 다소 한계가 있기 때문이라고 생각합니다. 불균형 클래스에 덜 민감하지만, 텍스트 데이터를 사용한 모델링이기 때문에 적합하지 않은 모델이라는 생각이 들었습니다.
그동안 모델링을 할때, 불균형 클래스 문제가 있는 데이터셋을 다뤄본 적이 많이 없었기 때문에 accuracy만을 고려하여 모델 성능 평가를 했습니다. 불균형이 심각한 데이터를 사용하여 모델링을 진행해보면서, 다양한 지표를 활용하여 모델 성능 평가를 해야 한다는 점을 배울 수 있었던 것 같습니다.
부족한 글이지만, 읽어주셔서 감사합니다.☺️
다음 포스팅은 'RNN 모델링과 성능평가'입니다.