최근 졸업프로젝트를 진행중인데, 기술적으로 새롭게 도전해야 하는 영역이 많아 매일매일이 배움의 연속이다. 그 정보량이 때로는 버겁게 느껴지지만, 이전에는 잘 몰랐던 일을 할 수 있게 된다는 것은 언제나 기쁜 일이다.
졸업프로젝트의 주제를 간략하게 말하면 '은퇴한 시니어 전문가 인재풀을 이용한 AI 일자리 매칭 플랫폼'인데, OCR을 통한 이력서 자동완성과 키워드 추출 및 추천 AI를 이용한 일자리 매칭이 주 기능이다. 우리 프로젝트의 매칭 AI는 기업 사용자가 입력한 '업무 한 줄 소개'와 시니어 전문가가 업로드한 '이력서'에서 추출한 키워드의 유사도 계산을 통해 구성된다. 이를 위해서는 업무 한 줄 소개에서의 키워드 추출 및 이력서 핵심 문장 추출이 필요하다. 그래서 오늘은 KR-WordRank를 이용한 키워드 및 핵심 문장 추출에 대해 알아보려고 한다.
Summerization
라이브러리를 써보기에 앞서 간단히 요약 알고리즘에 대해 알아보자. 문서 집합을 요약하는 분야를 summerization이라고 하는데, 크게 extractive approach와 abstractive apporach로 나눌 수 있다.
1. Extractive approach
주어진 데이터(문서) 안에서 이를 대표할 수 있는 단어들이나 문장을 선택하는 방법을 말한다. 주어진 데이터 안에서만 선택하기 때문에 Unsupervised Learning(비지도학습)이 가능한 것이 특징이다. 또한 같은 이유로 맥락에서 어긋나는 엉뚱한 단어로 요약될 위험이 없다.
2. Abstractive Approach
'사람이 문서를 요약하는 것처럼' 요약하는 방법이다. 이 말은 즉, 문서 내의 단어나 문장 외에 그 문서를 잘 표현할 수 있는 새로운 문장이나 단어를 생성해서 요약한다는 뜻이다. 문서 외의 정보가 필요하기 때문에 Supervised Learning(지도학습) 만 가능하다는 단점이 있다. 맥락에 맞지 않는 단어가 선택될 위험도 어느 정도 있지만, 최근까지도 활발하게 연구되는 분야라고 한다.
우리가 개발할 초기 플랫폼은 아무런 데이터도 갖고 있지 않기 때문에 지도학습은 불가하다. 따라서 비지도학습이 가능한 Extractive approach 중에서도 WordRank 알고리즘을 선택했다.
Graph Ranking: PageRank - TextRank - WordRank
1. PageRank
TextRank는 PageRank에 기반을 둔 기술이므로 TextRank의 작동을 이해하려면 PageRank를 먼저 살펴볼 필요가 있다. PageRank는 구글이 검색 결과로 어떤 웹 사이트를 먼저 보여줄지 계산하기 위해 고안해낸 알고리즘이다. 다른 웹 사이트가 많이 참조한 사이트일수록 중요한 사이트일 것이다라는 단순한 아이디어에서 출발했다. PageRank는 Graph Ranking에 기반을 두고 있는데, Graph Ranking은 노드와 엣지로 구성된 그래프에서 중요한 노드를 찾는 알고리즘이다. 많은 알고리즘이 점과 점 사이의 거리를 통해 데이터의 성질을 표현하는 Metric(Vector) Space에 기반을 두고 있는데, 거리를 구할 수 없거나 벡터 거리로 정의하기 까다로운 경우 그래프를 통해 쉽게 표현할 수 있다. PageRank는 페이지 링크가 이동되는 단계마다 변하는 시스템을 그래프로 표현하는데, 웹 사이트의 방문자 수/페이지로 이동하는 링크의 개수/퀄리티 등이 핵심적인 중요도 평가 지표가 된다.
2. TextRank
1999년에 PageRank 논문이 나오고 5년 뒤인 2004년에 TextRank가 제안되었다. TextRank는 PageRank의 이러한 지표를 '연관된 단어의 수'로 바꾼 것으로, 연관된 단어의 수가 많은 단어일수록 중요하다고 판단한다. 이때 '연관된 단어'란 한 문장 또는 문단에서 같이 출현한 단어를 말하며 이러한 단어들은 의미적 근접성을 갖는다. 이를 바탕으로 TextRank는 동시 출현 단어 그래프를 생성하여 중요한 노드를 찾는다.
3. WordRank
WordRank는 TextRank와 유사한데, 중국어나 일본어처럼 띄어쓰기가 없는 언어에서 graph ranking 알고리즘을 이용해 단어를 추출하기 위해 제안된 방법이다. substring의 단어 가능 점수인 Rank를 이용해 unsupervised word segmentation을 수행하는데, substring graph를 만든 뒤 graph ranking 알고리즘을 학습한다. 단어는 다른 많은 단어들과 연결되므로 질이 좋은 links와 많이 연결되고, 단어가 아닌 substring은 backlinks가 적으므로 Ranking update를 할수록 단어들의 rank가 높아지는 방식이다.
KR-WordRank
WordRank는 한국어에 바로 적용하기에는 무리가 있는데, 이는 한국어의 문법적 특징 때문이다. 우선 한국어는 띄어쓰기가 있는데 이 띄어쓰기 정보가 WordRank에서는 무시된다. 또한 한국어는 어절의 왼쪽과 오른쪽에 각각 의미를 지니는 단어와 문법 기능을 하는 단어 또는 형태소가 결합되는데, WordRank는 좌우와 무관하게 모든 단어를 추출한다는 문제가 있다. 이러한 문제점을 개선하기 위해 KR-WordRank 라이브러리가 고안되었고, 오늘은 이 라이브러리를 이용해 키워드를 추출해보는 실습을 하고자 한다.
소스코드 참고)
1. KR-WordRank 라이브러리 설치
pip install krwordrank
https://github.com/lovit/KR-WordRank
https://pypi.org/project/krwordrank/
2. 문장 전처리 함수 생성
텍스트 출처) [네이버 지식백과] 모바일앱개발자 (커리어넷 직업백과)
2-1. 문장 단위로만 나누기
- .?!을 기준으로 문장을 구분하고 split하여 리스트로 저장
text = "모바일 앱 개발자는 스마트폰이나 태블릿PC 등 모바일 기기에서 사용되는 소프트웨어인 모바일 애플리케이션을 개발하고 수정하는 일을 합니다. 국내외의 모바일 앱 개발 흐름 및 우리나라 사람들의 모바일 기기 이용 특성에 적합한 애플리케이션을 기획하고 개발합니다. 기업이나 기관이 이용하기 원하는 정보들을 체계적으로 모바일 앱을 이용하여 관리할 수 있는 다양한 기능의 애플리케이션을 만듭니다. 애플리케이션이 작동하는데 필요한 구글 안드로이드, 애플, 윈도우 등 모바일 운영체제별로 기술 변경 사항 등에 대해 확인하고 고객의 요구 사항대로 앱을 개발하기 위한 기술 검토와 업무 분석을 합니다. 새로 개발하거나 이미 개발된 애플리케이션과 관리 시스템 이용에 필요한 사용자 교육을 하거나 실제 모바일앱 운영에 필요한 시스템 구성과 관련된 내용을 고객과 회의를 통해 결정합니다."
import re
def split_sentences(text):
sentences = text.replace(". ",".")
sentences = re.sub(r'([^\n\s\.\?!]+[^\n\.\?!]*[\.\?!])', r'\1\n', text).strip().split("\n")
for s in sentences:
s = s.replace(u"\xa0", u" ")
return sentences
split_sentences(text)
2-2. 명사로만 문장을 구성하여 나누기
- 문장을 명사로만 구성하기 위해 KoNLPy(파이썬 한국어 처리 패키지) 설치
// java 1.7이상인지 확인
pip install JPype1>=0.7.0
pip install konlpy
- Otk로 명사를 구분하여 명사로만 문장을 구성
from konlpy.tag import Okt
import re
def split_noun_sentences(text):
okt = Okt()
sentences = text.replace(". ",".")
sentences = re.sub(r'([^\\n\\s\\.\\?!]+[^\\n\\.\\?!]*[\\.\\?!])', r'\\1\\n', sentences).strip().split("\\n")
result = []
for sentence in sentences:
if len(sentence) == 0:
continue
sentence_pos = okt.pos(sentence, stem=True)
nouns = [word for word, pos in sentence_pos if pos == 'Noun']
if len(nouns) == 1:
continue
result.append(' '.join(nouns) + '.')
return result
split_noun_sentences(text)
3. 키워드(Key-Word) 추출
KRWordRank(min_count, max_length)
: 지정된 파라미터가 적용된 wordrank extractor를 반환한다.
함수 파라미터
- min_count: 단어(substring) 최소 출현 빈도수
- max_length: 단어(substring)의 최대 길이
- beta: PageRank의 decaying factor
현재 노드와 연결된 다른 노드들의 PageRank 값을 고려하는 정도를 조절하는 0과 1 사이의 값
→ beta가 작을수록 다른 노드의 영향력이 강해지므로 PageRank값이 분산됨
→ KRWordRank에서도 beta 값이 작을수록 다른 단어들의 영향력이 강해지므로 키워드 추출 결과가 분산됨) - max_iter: 최대 iteration 횟수
키워드 추출 소스코드
from krwordrank.word import KRWordRank
min_count = 1 # 단어의 최소 출현 빈도수 (그래프 생성 시)
max_length = 10 # 단어의 최대 길이
wordrank_extractor = KRWordRank(min_count=min_count, max_length=max_length)
beta = 0.85 # PageRank의 decaying factor beta
max_iter = 20
texts = split_noun_sentences(text)
keywords, rank, graph = wordrank_extractor.extract(texts, beta, max_iter)
for word, r in sorted(keywords.items(), key=lambda x:x[1], reverse=True):
print('%8s:\\t%.4f' % (word, r))
모바일 앱 개발자에 대한 설명에 대해 키워드를 추출한 결과 '모바일', '개발', '관리'와 같은 핵심 키워드가 잘 추출되었다.
2-2처럼 명사로만 구성된 문장을 이용해 키워드를 추출하여 성능을 향상시켰다.
2-1처럼 문장을 그대로 사용하면 다음과 같이 조사나 어미가 그대로 붙은 채로 추출된다.
4. 핵심 문장(Key-Sentence) 추출
KR-WordRank 버전 1.0.0부터 제공되는 기능이다. 이 라이브러리에서는 한국어 토크나이저 기능이 내제되어 있기 때문에 토크나이징이 된 문장 간 유사도를 이용하는 TextRank 방식을 이용하기 어렵다. 그 대신 keywords 를 많이 포함한 문장을 핵심 문장으로 선택한다. 추출된 키워드의 랭크값을 이용하여 키워드 벡터를 만든 뒤, 코싸인 유사도 기준으로 입력된 문장 벡터가 키워드 벡터와 유사한 문장을 선택하는 방식으로 핵심 문장을 추출한다.
summerize_with_sentences(texts, num_keywords, num_keysents)
: 지정된 개수의 키워드와 랭크값이 저장된 dictionary와 지정된 개수의 핵심 문장이 저장된 리스트를 반환한다.
함수 파라미터
- num_keywords: 반환할 키워드 개수
- num_keysents: 반환할 핵심 문장 개수
- penalty: 길이가 지나치게 길거나 짧은 문장을 제거하거나 특정 단어가 포함된 문장을 핵심 문장에서 제거하는 함수
- stopwords: 키워드에서 제거할 단어들 (키워드 벡터를 만들 때에도 이용되지 않음)
- diversity: 코싸인 유사도 기준 핵심 문장간의 최소 거리
→ 이전에 선택된 문장과 중복되는 문장들이 선택되는 것을 막기 위한 값
→ diversity 값이 클수록 다양한 문장이 선택됨
핵심 문장 추출 소스코드
from krwordrank.sentence import summarize_with_sentences
penalty=lambda x:0 if (25 <= len(x) <= 80 and not '흐름' in x) else 1,
stopwords=[]
keywords, sents = summarize_with_sentences(
texts,
penalty=penalty,
stopwords = stopwords,
diversity=0.5,
num_keywords=100,
num_keysents=10,
verbose=False
)
print(sents)
핵심 문장을 추출할 때는 추출한 문장을 활용할 용도에 따라 문장을 명사로만 구성하거나 그대로 이용하거나 하면 된다. 위 예시는 문장을 원본 그대로 이용했을 때의 결과다.