Algorithm. Text Mining for Dummy
텍스트 마이닝이란 표현도 이젠 좀 올드해 보인다. NLP는 이미 수십 년 전부터 있던 건데 여전히 유효하고 딥러닝 이후 더 중요해졌다. 랭귀지 모델이란 용어는 언제부터 사용된 걸까? 어쨌든 요즘 자연어 연구의 방법은 다소 획일화된 듯하다. 일단 단어를 Word2Vec이나 GloVe 등으로 워드 임베딩을 하고, 이를 문장 (등의) 단위로 연결해서 RNN 또는 이후 등장한 여러 딥러닝 기반 모델에 넣으면 그냥 끝난다.
자연어처리와는 다소 무관한 경력을 쌓긴 했지만 그래도 키워드와 텍스트는 늘 조금씩 다뤄왔기에 전통적인 텍스트 마이닝 기법들을 정리, 소개하려 한다. 최근에 입문한 분들은 요즘 방식이 더 친숙하겠지만, 과거의 방식에서 여전히 유효한 것들이 많으니 참고 삼아 읽으면 보면 좋을 거다. 텍스트 마이닝이라고 했지만, 오랫동안 검색 서비스 관련 업무에 종사해서 텍스트보다는 키워드 (쿼리)에 관한 설명이라 보는 게 더 맞을 수도 있다.
라떼의 단어와 문장을 다루는 기법들
Tokenization: Java로 텍스트를 처리할 때 으레 StringTokenizer라는 클래스를 사용한다. 이는 정해진 delimiter에 따라서 텍스트를 단어들로 쪼개는 작업이다. 보통 띄어쓰기나 구두점 (punctuation)으로 문장을 분리한다.
Stemming & Lemmatization: 형태소 분석기의 가장 기본 기능은 단어의 어간이나 기본형을 찾아주는 거다. 예를 들어, Compute에서 파생된 computer, computes, computed, computation 등을 개별 단어로 분석해도 되지만, 의미가 비슷할 것으로 유추되기 때문에 어간 (comput)이나 기본형 (compute)로 치환한다. 이렇게 함으로써 dictionary의 dimension도 다소 줄일 수 있고, 단어들의 출현/발생 빈도를 다소나마 덜 희소하게 만든다. 이렇게 어간으로 자르는 것을 stemming이고, 기본형으로 치환하는 것이 lemmatization이다. 파이썬 NLTK 라이브러리에 기본적인 함수가 정의돼있다.
Stop word: 다음으로 문장에서 구두점이나 마크업을 포함해서 불필요한 요소나 단어들을 제거한다. 특히 Stop Word라는 것은 문장에서 별 의미가 없는 단어를 뜻한다. 워낙 흔해서 많은 문장에서 반복 등장해서 텍스트의 의미 구분에 별로 영향을 주지 않는 단어들이다. 영어에서 정관사 the, 부정관사 a/an, 전치사 (on, of, under 등)나 접속사 (and, or), 그리고 be 동사를 포함한 흔한 동사 (have, go, get 등) 등을 포함한다. 너무 흔해서 문장의 특성을 특정 짓는데 별로 도움이 되지 않고, 앞서 말했듯이 불필요하게 dictionary의 차원만 차지할 뿐이다.
Lowercase & No blank: 때론 (특히 검색 쿼리를 분석할 때) 모든 단어를 소문자로 바꾸거나 띄어쓰기를 없애기도 한다. 가능한 같은 단어가 여러 형태로 사용되는 걸 막기 위해서다. 예를 들어, 검색어의 검색 빈도를 집계한다면 '데이터과학'과 '데이터 과학'을 별도의 단어로 볼 것이 아니라 '데이터과학' 한 단어로 취급한다. 강력한 형태소 분석기를 갖고 있다면 추가적으로 'data science' 도 동의어 처리해서 함께 집계하는 것도 가능하다.
구두점이나 stopword 뿐만 아니라, 의미가 모호한 단어도 제거한다. 대표적으로 1음절 단어가 있다. 문장 속에서 의미를 파악할 수도 있지만, '일'이라는 단어가 있을 때 이게 숫자 1인지 아니면 일(요일)인지 아니면 work인지 단어만으로 의미를 파악하기 힘들다. 뿐만 아니라, 오탈자도 치환 대상이 확실치 않으면 제거하는 편이다. ... 그런데 구두점을 그냥 놔뒀을 때 더 정확도가 높았던 경우도 있었다.
N-Gram: 개별 단어 단위로 문장을 분석할 수도 있지만 때론 연속한 2개 또는 3개 단어를 하나로 묶어서 분석하기도 한다. 2개 단어를 묶은 것이 bi-gram이고 3개를 묶으면 tri-gram이다. 물론 이렇게 하면 사전의 차원이 급격히 증가하는 부작용이 있지만, 하나로 묶었을 때 의미가 더 정확한 합성어도 있기 때문이다. 예를 들어, '서울'(에 있는)과 '대학교'가 떨어져 있을 때와 '서울대학교'는 의미가 완전히 달라진다. (String Kernel도 참고)
최근에는 거의 본 적은 없지만, 예전에는 한글 encoding이 문제가 될 때가 종종 있었다. euc-kr이나 utf-8 등 인코딩을 맞추는 게 분석의 시작이었다.
라떼의 문장/문단 임베딩 방식
Bag of words (BOW): 데이터를 분석한다는 건 결국 수치 데이터를 분석한다는 걸 의미한다. 그래서 모든 데이터는 수치화 또는 수치 벡터화돼야 한다. 요즘은 이를 임베딩 embedding이라 부른다. 텍스트 문장을 수치 벡터로 만드는 가장 흔한 방식이 bag of words다. 즉, 단어를 1부터 마지막 단어까지 색인하고, 각 단어가 출현한 색인의 값을 1 또는 실수 값으로, 나머지를 0으로 설정하면 단어를 포함한 문장의 수치 벡터가 만들어진다. 아래와 같이 다양한 BOW 구현 방식이 있다.
Boolean Vector: 가장 쉬운 방식은 단어의 존재 유무에 따라서 1과 0, 즉 boolean 값으로 설정하는 거다. 그런데 단어의 빈도와 무관하게 모두 1로 표시하기 때문에 단어별 중요도를 알 수 없다. 사람들은 보통 중요한 단어 (개념)을 반복해서 말하는데, 1번 언급해도 1이고, 10번 언급해도 1이 되기 때문에 단어의 빈도가 무시되는 단점이 있다. 그렇지만 때론 그런 빈도가 필요 없는 경우도 있기 때문에 여전히 boolean vector가 필요하고, python의 sk-learn에서 CountVectorizer에 binary=True 옵션이 있다.
Term Frequency (TF): 앞서 말했듯이 단어의 반복은 중요한 의미를 갖는다. 그래서 단어의 발생 빈도를 정수 값으로 임베딩 한다.
Inverse Document Frequency (IDF): Stop word에서 설명했듯이 어떤 단어가 모든 문서에 편재한다면 그 단어는 문서들을 특정 짓는데 아무런 영향력이 없다. 역으로 소수의 문서에만 등장하는 단어라면 그 단어를 포함한 문서들은 서로 비슷한 주제를 다루는 것이라 말할 수 있을 만큼 중요하다. 그래서 단어가 얼마나 적은 문서에 등장하느냐의 측도로 '단어를 포함한 문서수 Document Frequency'의 역수, 즉 IDF도 사용된다. 그런데 보통 IDF는 단독으로 사용되지 않고 TF와 결함 해서 TF-IDF로 보통 사용한다. sk-learn의 TfidfVectorizer가 이에 해당한다.
LSI/LSA/pLSI/LDA: 그런데 보통 dictionary의 단어수는 적게는 수만, 많게는 수십/백만을 넘는데 하나의 문장 또는 문단은 기껏해야 수백에 불과하다. 즉, TF-IDF 등으로 표현한 문장/문단은 매우 큰 차원임에도 데이터는 매우 sparse 하다. 그래서 차원을 축소하면서 dense 한 데이터를 만들기 위해서 여러 matrix factorization 방식을 사용한다. Latent Semantic Indexing 또는 Latent Semantic Analysis가 초기부터 사용됐고 후에 확률을 더한 pLSI가 나왔고, 다음으론 Latent Dirichlet Allocation (LDA)을 포함한 topic modeling 기법이 많이 등장했다. 물론 최근에는 모든 게 딥러닝으로 통합되고 있지만...
라떼의 방식들을 쭉 나열했지만 오늘날에도 여전히 유효하다. 요즘은 그냥 텍스트 뭉치를 가져와서 미지의 전처리기를 거쳐서 유망한 딥러닝 모델 (LSTM, Attention, GRU, BERT 등)을 통과시키면 결과가 나온다. 어쩌면 최근 입문하는 많은 ML 개발자들이 그렇게 생각하고 있을지도 모르겠다. SOTA 모델이 정답을 보장하지 않는다. 딥러닝이 모든 것을 해결할 수 있더라도 모든 문제의 최적 솔루션이 딥러닝인 건 아니다. 전처리에 자동화가 많이 진행됐지만 여전히 세심한 인간의 손길이 필요한 상황이 많다.