
이번 포스팅에서는 리뷰가 아닌, 오늘 끝난 해커톤에 대해 리뷰하는 시간을 가져볼려고한다. 최근에 계속 그동안 해왔던 것들을 정리하고, 부족했던 부분을 확인하고 채우는 시간을 가지면서 새로운 시도를 해보고 싶다는 생각이 들었다. 그래서 월간 데이콘 쇼츠에 도전하였다. 짧은 시간동안 진행한 대회인만큼, 최선을 다했지만 아쉬운 결과로 마무리하였다. 포스팅을 진행하면서 어떤 방향으로 시도를 했는지, 그리고 내가 만든 결과보다 더 좋은 결과가 나온 분들은 어떤 방법을 사용하였는지 살펴보자.
1. 데이터 로드 및 전처리

먼저 데이터를 다운받는다. 규칙 상 외부 데이터를 사용할 수 없기 때문에, 단순히 기사의 ID, 제목, 내용만이 출력됨을 확인할 수 있다. 하지만, 출력된 내용을 그대로 쓰면 불필요한 정보가 포함되어 모델의 성능을 저하시킬 수 있다. 따라서, URL/이모티콘, 날짜 등을 제거하고, NLTK의 불용어 사전을 통해 분석에 필요없다고 생각되는 단어들을 배제시켰다. 또한, 제목과 내용을 한번에 input 시키기 위해 두 가지를 합친 새로운 column을 만들었다. 결과는 위의 사진과 동일하다.
2. Baseline

모든 Dacon 대회는 Baseline을 제공해서, 참여자의 원활한 문제 해결을 돕는다. 이번 대회에서는 text embedding을 추출하고 이를 clustering하는 방식으로 구현되었는데, 모델을 그대로 쓰기보다는 다른 모델을 사용해서 결과를 출력해보았다. 코드는 다음과 같다.
from sentence_transformers import SentenceTransformer
from sklearn.cluster import KMeans
# Sentence BERT 모델 로드
model = SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2')
# 텍스트 feature 추출
sentence_embeddings = model.encode(data['text'].tolist())
# 추출한 feature를 데이터프레임에 저장
df_embeddings = pd.DataFrame(sentence_embeddings)
df_embeddings
# Sentence BERT 임베딩을 사용하여 군집화 수행
kmeans = KMeans(n_clusters=6, random_state=42)
data['kmeans_cluster'] = kmeans.fit_predict(sentence_embeddings)
data.head()
# 군집화 확인(6개 동일하게 진행)
data[data['kmeans_cluster'] == 0]['text'].head(5) # world
print(data['text'][1])
print(data['text'][10])
print(data['text'][29])
print(data['text'][34])
print(data['text'][37])
mapping_dict = {
0: 5,
1: 3,
2: 4,
3: 1,
4: 0,
5: 2
}
data['mapping'] = data['kmeans_cluster'].apply(lambda x : mapping_dict[x])
sample = pd.read_csv('data/sample_submission.csv')
sample['category'] = data['mapping'].values
sample.to_csv('submit_first.csv', index = False)
코드의 내용은 어렵지 않기 때문에, 사용한 라이브러리에 대해서만 정리해보았다.
- SentenceTransformer : 문장 임베딩을 생성하기 위한 python library로, 사전 학습된 모델을 기반으로 문장을 고차원 벡터로 변환한다.
- k-means: k-means는 비지도 학습 알고리즘 중 하나로 주로 클러스터링에 사용된다다. 이 알고리즘은 데이터 포인트를 'k'개의 클러스터로 그룹화한다. 알고리즘이 동작하는 방식은 다음과 같다:
- 먼저, 데이터 포인트에서 임의로 'k'개의 점(중심)을 선택한다.
- 각 데이터 포인트는 가장 가까운 중심에 할당되며, 이렇게 하여 'k'개의 클러스터가 형성된다.
- 중심은 해당 클러스터 내부의 모든 점들의 평균 위치로 업데이트 된다.
- 중심이 업데이트 되면, 데이터 포인트들은 다시 가장 가까운 새로운 중심에 할당되며, 이 과정이 반복된다.
- 이 반복 과정은 중심점이 더 이상 크게 움직이지 않거나 일정 횟수를 넘어갈 때까지 계속된다.
- 즉, SentenceTransformer와 k-means를 함께 사용해서 사용한 문장들을 의미적으로 그룹화 하였다.
- macro f1 score : 0.6289
3. change pre-trained model

baseline에서 사용한 사전학습 모델보다 성능이 더 좋은 모델을 사용한다면, 더 좋은 결과가 나올 것이라는 생각이 들었다. SOTA 모델을 알려주는 paperwithcode에서 MTEB라는 Text embedding benchmark를 찾았고, Text Clustering에서 가장 성능이 좋은 모델을 사용하려고 시도했다. 제일 위에 있는 ST5-XXL은 batch_size를 4까지 줄였음에도 컴퓨팅 자원의 한계로 모델을 사용할 수 없었고, 거의 성능이 차이나지 않는 MPNet을 사용했다. hugging face의 all-mpnet-base-v2 모델을 load하면서 baseline보다 코드의 고도화를 위해 노력했다. 결과는 아래와 같다.
import torch
from transformers import AutoTokenizer, AutoModel
from sentence_transformers import SentenceTransformer
from sklearn.cluster import KMeans
# 최종 결과를 def function으로 출력
def call_model(df, model_name):
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = SentenceTransformer(model_name).to(device)
bert_embeddings = model.encode(df['text'].tolist())
sentence_embeddings = pd.DataFrame(bert_embeddings)
kmeans = KMeans(n_clusters=6, random_state=42)
df['kmeans_cluster'] = kmeans.fit_predict(sentence_embeddings)
return df
all_data = call_model(data, "sentence-transformers/all-mpnet-base-v2")
# 결과를 확인하는 def function
def check_category(data, val, count):
return print(data[data['kmeans_cluster']==val]['text'].head(count))
check_category(all_data, 0, 5) # sports
check_category(all_data, 1, 5) # politics
check_category(all_data, 2, 5) # Business
check_category(all_data, 3, 5) # entertain
check_category(all_data, 4, 5) # world
check_category(all_data, 5, 5) # tech
mapping_dict = {
0: 3,
1: 2,
2: 0,
3: 1,
4: 5,
5: 4
}
all_data['mapping'] = all_data['kmeans_cluster'].apply(lambda x : mapping_dict[x])
sample = pd.read_csv('data/sample_submission.csv')
sample['category'] = all_data['mapping'].values
sample.to_csv('sumbit_third.csv', index = False)
사전학습 모델을 바꾸고, 다른 사전학습 모델도 편리하게 사용하기 위해 define function으로 코드의 편의성을 향상시켰다. macro f1 score는 0.7768점으로, 이 기록이 제일 높은 점수를 기록했다.
4. LDA + Develop model
점수를 높이기 위해 어떤 방법을 쓰면 좋을지 고민해보았다. 그러다, LDA 토픽 모델링을 사용하고, 토픽 모델링의 결과를 가장 성능이 좋았던 model의 sentence embedding 결과와 결합하면 사용할 수 있는 정보의 양이 늘어나기 때문에 보다 정확한 군집화를 할 수 있을 수 있다는 가설을 세워보았다. 먼저 LDA 토픽 모델링에 대해 간단하게 정리해보고, 가설을 확인해보자.
토픽 모델링은 문서의 집합에서 토픽(주제)를 찾아내는 과정으로, 각 문서가 여러 개의 토픽으로 구성될 수 있다는 가정하에 작동한다. 우리는 정해진 토픽들(Business, World, Politics, entertainment, Tech, Sports)이 있기 때문에, 이 주제들을 사용한다. LDA를 사용하면 비슷한 맥락을 가진 단어들이 그룹화되며, 그룹 별로 어떤 단어들이 많이 등장하는지를 파악할 수 있다. 이제 실제 코드를 확인해보자.
from gensim import corpora, models
from nltk.corpus import stopwords
from nltk.tokenize import word_tokenize
from sentence_transformers import SentenceTransformer
from sklearn.cluster import KMeans
import numpy as np
texts = data['text'].tolist()
tokenized_texts = [word_tokenize(text.lower()) for text in texts]
dictionary = corpora.Dictionary(tokenized_texts)
corpus = [dictionary.doc2bow(text) for text in tokenized_texts]
lda_model = models.LdaModel(corpus, num_topics=6, id2word=dictionary)
most_probable_topic_labels = []
for doc_bow in corpus:
topic_dist_for_doc = lda_model[doc_bow]
most_probable_topic_label_for_doc = max(topic_dist_for_doc, key=lambda x: x[1])[0]
most_probable_topic_labels.append(most_probable_topic_label_for_doc)
# min-max 정규화
min_val = min(most_probable_topic_labels)
max_val = max(most_probable_topic_labels)
normalized_data = [(x - min_val) / (max_val - min_val)/20 for x in most_probable_topic_labels]
most_probable_topic_labels_matrix = np.array(normalized_data).reshape(-1,1)
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# concat matrix
model = SentenceTransformer('all-MPNet-base-v2').to(device)
new_embeddings = model.encode(texts)
combined_embeddings = np.concatenate((new_embeddings , most_probable_topic_labels_matrix), axis=1)
combined_embeddings
# kmeans, category output
제일 처음에는 문장별 토픽의 확률을 sentence embedding matrix에 결합하였으나, 결과가 좋지 않았다. 원인을 생각해보면, 확률 값에서 0이 다수 존재하였는데 이로 인해 서로 관련 없는 문장들이 유사하게 출력되었다는 생각이 들었다. 따라서, 확률 값 대신 개별 문장에서 확률이 제일 높은 label을 정수로 바꿔서 보다 정확한 군집을 위한 변수를 만들었다. 단, label의 값이 0~6 인데 embedding value의 값은 0에 근접한 소수가 많기 때문에, min-max 정규화 이후 20을 나눠서 수치를 비슷하게 만들어 편향이 안되도록 진행하였다. 하지만, 아쉽게도 f1 score는 0.7746점이 나왔다.
5. Change Cluster
LDA를 사용하면 점수가 오를 수 있다는 가설이 틀렸기 때문에, 또 다른 방법으로 생각을 해보았다. 그러다, 사전 학습 모델이 아닌 군집화를 다른 방식으로 진행하면 어떨까라는 생각이 들었다. kmeans clustering을 다른 library로 호출해서, 반복을 통해 최상의 결과를 뽑아내는 방식으로 진행해보았다. 코드는 다음과 같다.
import torch
from transformers import AutoTokenizer, AutoModel
from sentence_transformers import SentenceTransformer
from nltk.cluster import KMeansClusterer, cosine_distance
def call_model(df, model_name):
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = SentenceTransformer(model_name).to(device)
embeddings = model.encode(df['text'].tolist())
kclusterer = KMeansClusterer(6, distance=cosine_distance, repeats=10)
clusters = kclusterer.cluster(embeddings, assign_clusters=True)
df['kmeans_cluster'] = clusters
return df
all_data = call_model(data, "sentence-transformers/all-mpnet-base-v2")
def check_category(data, val, count):
return print(data[data['kmeans_cluster']==val]['text'].head(count))
clusters = all_data['kmeans_cluster'].unique()
# 개별 군집 내용 출력
for cluster in clusters:
print(f"Cluster {cluster}:")
check_category(all_data, cluster, 5)
print("\n") # Add a newline for readability
# submit
def submit_data(data, submit_name):
mapping_dict = {
0: 3,
1: 5,
2: 0,
3: 1,
4: 4,
5: 2
}
data['mapping'] = data['kmeans_cluster'].apply(lambda x : mapping_dict[x])
sample = pd.read_csv('data/sample_submission.csv')
sample['category'] = data['mapping'].values
return sample.to_csv(submit_name, index = False)
submit_data(data, 'submit_final.csv')
결론부터 말하자면, f1 score는 0.7756이 나왔다. 최고점인 기록보다 0.001점이 낮은 기록이지만, 군집화된 결과가 지금까지 진행했던 내용 중에 가장 훌륭하다고 판단해서 해당 파일을 최종적으로 제출했다. Dacon은 대회 기간 동안은 30%의 데이터만으로 채점을 진행하고, 대회 종료 직후 나머지 70%의 데이터로 최종 순위를 도출하기 때문이다. 1등의 f1-score가 0.82836점으로, 0.05 정도 차이가 났지만 아쉬운 마음으로 마무리했다.
6. 상위권 코드 Review

대회가 종료된 이후, 순위를 확인해보니 30등이었다. 다른 분들의 code를 확인해보니 최종적으로 2등과 4등을 하신 분이 동일한 사전학습 모델을 사용했음을 확인할 수 있었다. 또한, 전처리 과정에서 나는 일괄적으로 이모티콘, URL 등을 제거했지만 상위권에 계신 분들은 일괄적으로 제거한 것이 아닌 패턴을 세세하게 지정해서 제거하였고, 이러한 부분이 성능을 보다 제고시킨 방법이라는 생각이 들었다. 실제로, 최종 제출을 진행했던 code에서 사전 학습 모델만 변경한 결과 점수가 0.79199로 바로 올라감을 확인할 수 있었다. 예전과 달리 hugging face를 활용하고, 여러가지 방법을 사용해보는 것은 좋은 시도였다. 하지만, 전처리에서 보다 세밀하게 생각을 하지 못했고, 주어진 Task에서 보다 높은 성능을 보이는 사전학습 모델을 찾지 못한 점이 다소 아쉬운점이라고 판단된다.
차후 해커톤에 참여한다면 제시된 데이터 혹은 그와 유사한 데이터의 전처리를 어떻게 했는지 보다 꼼꼼하게 찾아보고, 사전 학습모델 역시 단순히 많이 사용하거나, 많이 다운받은 것이 아닌 보다 성능을 제고할 수 있는 모델을 찾는 방향으로 진행하도록 하자.
출처
월간 데이콘 쇼츠 - 뉴스 기사 레이블 복구 해커톤 - DACON
분석시각화 대회 코드 공유 게시물은 내용 확인 후 좋아요(투표) 가능합니다.
dacon.io
'Dacon' 카테고리의 다른 글
| [금융] 제1회 KRX 금융 빅데이터 활용 아이디어 경진대회 (0) | 2023.09.17 |
|---|---|
| [금융] 월간 데이콘 신용카드 사용자 연체 예측 AI 경진대회 (0) | 2023.01.07 |