Topic Modeling 기법 중 하나
💡 Topic Modeling 문장들의 코퍼스(Corpus)에 내재되어 있는 주제(토픽)를 끌어내는데 쓰이며, 전체 문서를 하나의 주제로 보고 주제를 구성하는 토픽을 찾아내어 문장을 분류하는 방법론이다. 비지도(Non-Training) 기계학습의 하나이며, 이 모델링은 주제뿐만 아니라 단어 차원 축소, 동음이의어를 찾아내는 부가적인 기능도 수행 가능하다.
BERT 기반 Embedding + Class-based TF-IDF를 사용한 것이 아이디어의 핵심이다
bertopic을 사용해서 특정 문서의 키워드를 뽑아내는 작업을 해보자.
$ pip install bertopic
$ pip install kiwipiepy
import json
import pickle
import glob, os
import pandas as pd
import kiwipiepy import Kiwi
import bertopic import BERTopic
AI Hub에 있는 민원(콜센터) 질의-응답 데이터 중, 질병관리본부의 데이터를 가져와서 상담사 및 질문자의 대화 내용만 가져온다. 데이터는 json 형식이며, 로그인만 하면 다운로드 받을 수 있다.
dataset = dict()
# 해당 경로에 있는 모든 json 파일에 대해서
for path in glob.iglob('./data/' + '**/*.json', recursive=True):
try:
with open(path, "r") as f:
data = json.load(f)
# 인코딩 관련 에러 처리
except:
try:
with open(path, "r", encoding="cp949") as f:
data = json.load(f, encoding="cp949")
except:
continue
for row in data:
category = row["카테고리"]
speaker = row["화자"]
QA = row["QA"]
# 의도 추출
if speaker == "고객":
intention = ''.join(row["고객의도"].split())
else:
intention = ''.join(row["상담사의도"].split())
# 대화 내용 추출
if QA == "Q":
if speaker == "고객":
content = row["고객질문(요청)"]
else:
content = row["상담사질문(요청)"]
else:
if speaker == "고객":
content = row["고객답변"]
else:
content = row["상담사답변"]
conversation_id = row["대화셋일련번호"]
sentence_id = row["문장번호"]
content = content.strip()
if content no in dataset:
dataset[content] = {"Q": {"고객": set(), "상담사": set()},
"A": {"고객": set(), "상담사": set()}}
dataset[content][QA][speaker].add(intention)
# 정제한 데이터 중, 대화 내용만 리스트에 저장
corpus = list(dataset.key())
corpus에는 다음과 같은 총 63283개의 문장이 들어있다.
['열이 나는데 코로나일까요?', '네, 안녕하세요', '열 말고 다른 증상은 없으신가요?', ...]
preprocessed_documents = list()
for sentence in corpus:
if sentence and not sentence.replace(' ','').isdecimal():
preprocessed_documents.append(sentence)
품사 태깅은 해당 링크에서 찾을 수 있다
# 불용어를 정의한다
user_stop_word = ["안녕", "안녕하세요", "때문", "지금", "감사", "네", "감사합니다"]
# 토크나이저에 명사만 추가한다
extract_pos_list = ["NNG", "NNP", "NNB", "NR", "NP"]
class CustomTokenizer:
def __ini__(self, kiwi):
self.kiwi = kiwi
def __call__(self, text):
result = list()
for word in self.kiwi.tokenize(text):
# 명사이고, 길이가 2이상인 단어이고, 불용어 리스트에 없으면 추가하기
if word[1] in extract_pos_list and len(word[0]) > 1 and word[0] not in user_stop_word:
result.append(word[0])
return result
custom_tokenizer = CustomTokenizer(Kiwi())
vectorizer = CountVetorizer(tokenizer=custom_tokenizer, max_features=300)
model = BERTopic(embedding_model="sentence-transformers/xlm-r-100langs-bert-base-nli-stsb-mean-tokens", \
vectorizer_model=vectorizer,
nr_topics=10, # 문서를 대표하는 토픽의 갯수
top_n_words=10,
calculate_probabilites=True)
약 6만문장에 대해 처리하는데 약 50분 정도가 소요되므로, 빠른 실습을 위해 10000개만 뽑아서 돌린다. 10000개를 처리하는데는 약 80초가 걸린다.
# 왜인지는 파악못했으나, 아래처럼 환경변수 셋팅을 안해주면 토크나이저 관련 에러가 남
os.environ["TOKENIZERS_PARALLELISM"] = "false"
topics, probs = model.fit_transform(preprocessed_documents[:10000])
model.get_topic_info()
BERTopic은 다양한 시각화 결과를 확인할 수 있는 함수도 제공한다.
더 많은 정보는 해당 링크에서 확인할 수 있다.
문서의 크기가 클수록, 모델이 토픽을 생성하는데 오랜 시간이 걸리기때문에 model 정보를 pickle로 저장하여 추후에 사용할때 용이하게 하려고 했으나, 'Kiwi' object는 pickle을 지원하지 않는다는 에러가 발생했다. Mecab을 사용했을때는 pickle로 저장이 되어서 Mecab 라이브러리를 이용하거나, 추출된 토픽의 결과를 csv 파일로 저장해서 추후에 사용하면 될 것 같다.
또, 토픽을 뽑으려는 문서가 너무 클때는, 랜덤 샘플링을 여러번 반복해서 샘플링된 문서들의 토픽들이 전체 문서를 대표하는 토픽을 나타내도록 코드를 작성하면 큰 문서도 빠르게 토픽을 추출할 수 있을 것이다.
https://arxiv.org/pdf/2203.05794.pdf
https://wikidocs.net/162079
https://github.com/MaartenGr/BERTopic
https://bongholee.com/bertopic/