인공지능

[빅데이터 직무연구회] 7회차 모임 정리

Chipmunks 2018. 6. 26.
728x90

[빅데이터 직무연구회] 7회차 모임 정리

모임 요일 : 5월 31일 목요일 저녁 6시

Chapter 7. 텍스트 데이터 다루기

텍스트 데이터는 주로 글자가 연결된 문자열로 표현된다. 텍스트 데이터의 길이는 서로 같은 경우는 거의 없다. 이런 특성은 이제까지 본 수치형 특성과 매우 다르므로 머신러닝 알고리즘에 적용하기 전에 전처리를 해야 한다.


7.1  문자열 데이터 타입

문자열 데이터는 네 종류가 있다

  • 범주형 데이터
    • 빨강, 녹색, 파랑, 노랑, 검정, 흰색, 자주, 분홍 중 하나를 선택
  • 범주에 의미를 연결시킬 수 있는 임의의 문자열
    • 철차를 틀리거나, 회색이나 쥐색처럼 다르게 쓸 수 있다. 이런 데이터를 범주형 변수로 인코딩하려면 가장 보편적인 값을 선택하든지, 애플리케이션에 맞게 이런 응답을 포용할 수 있는 범주를 저으이하는게 최선이다. "녹색과 빨강 줄무늬" 같은 응답은 "여러가지 색" 범주에 할당하거나, 다른 것으로 인코딩할 수 없는 값은 "그 외" 라고 하면 된다.
    • 범주형 변수로 받을 수 있는 것은 직접 입력받지 않는 것을 권장한다.
  • 구조화된 문자열 데이터
    • 주소, 장소, 사람 이름, 날짜, 전화번호, 식별번호처럼 일정한 구조를 가지는 것
  • 텍스트 데이터
    • 자유로운 형태의 절과 문장으로 구성된 것.
    • 트윗, 채팅, 호텔 리뷰, 셰익스피어 작품, 위키백과 문서 등
말뭉치(Corpus) : 텍스트 분석에서의 데이터셋
문서(Document) : 하나의 텍스트를 의미하는 각 데이터 포인트

이런 용어는 텍스트 데이터를 주로 다루는 정보 검색(IR, Information Retrieval)과 자연어 처리(NLP, Natural Language Processing) 공동체에서 유래했다.

7.2 예제 애플리케이션: 영화 리뷰 감성 분석

데이터셋은 다음 주소에서 내려받을 수 있다.
http://ai.stanford.edu/~amaas/data/sentiment/

위 데이터셋은 스탠퍼드 대학교 연구원인 앤드류 마스(Andrew Mass)가 IMDb(Internet Movie Database) 웹사이트에서 수집한 영화 리뷰 데이터셋이다.

리뷰 텍스트와 '양성' 혹은 '음성'을 나타내는 레이블을 포함하고 있다. IMDb 웹사이트에서 1에서 10까지의 점수가 있다. 이 데이터셋은 7점 이상은 '양성', 4점 이하는 '음성'인 이진 분류 데이터셋으로 구분되어 있다.

pos 폴더에는 긍정적인 리뷰가 각각 하나의 파일로 나뉘어 있고, neg 폴더도 마찬가지다.
unsup 폴더는 레이블이 없는 데이터를 담고 있다. 이 폴더는 사용하지 않는다.

scikit-learn 의 load_files 함수를 사용해 파일을 읽을 수 있다. 훈련 데이터를 load_files 함수로 읽어들인다.

text_train 리스트의 길이는 25,000 이다. 각 항목은 리뷰 한 개에 대한 문자열이다. 인덱스가 1인 리뷰를 출력했는데, HTML 줄바꿈 태그(<br />)를 포함하고 있다. 이 태그를 replace 함수로 깔끔하게 삭제해 데이터를 정리하자.


이 데이터셋은 양성 클래스와 음성 클래스를 같은 비율로 수집했다. 따라서 양성과 음성 레이블의 수가 같다.


우리가 풀려는 문제는, 리뷰가 하나 주어졌을 때 이 리뷰의 텍스트 내용을 보고 '양성'인지 '음성'인지 구분하는 것이다. 전형적인 이진 분류 문제이다. 텍스트 데이터는 머신러닝 모델이 다룰 수 있는 형태가 아니다. 텍스트의 문자열 표현을 머신러닝 알고리즘에 적용할 수 있도록 수치 표현으로 바꿔야 한다.


7.3 텍스트 데이터를 BOW로 표현하기

BOW(bag of words)는 가장 간단하지만 효과적이면서 널리 쓰이는 방법이다. 장, 문단, 문장, 서식 같은 입력 텍스트의 구조 대부분을 잃는다. 각 단어가 이 말뭉치에 있는 텍스트에 얼마나 많이 나타나는지만 헤아린다. 구조와 상관없이 단어의 출현 횟수만 센다. 이를 텍스트를 담는 가방(bag)으로 생각할 수 있다.


BOW 표현은 다음 세 단계를 거친다.

  1. 토큰화(tokenization) : 각 문서를 문서에 포함된 단어(토큰)로 나눈다. 예를 들어 공백이나 구두점 등을 기준으로 분리한다.
  2. 어휘 사전 구축 : 모든 문서에 나타난 모든 단어의 어휘를 모으고 번호를 매긴다. (알파벳 순서)
  3. 인코딩 : 어휘 사전의 단어가 문서마다 몇 번이나 나타나는지를 헤아린다.

"This is how you get ants." 문자열을 처리하는 과정은 다음과 같다.


(토큰화) => ['this', 'is', 'how', 'you', 'get', 'ants']

(어휘 사전 구축) => ['aardvark', 'amsterdam', 'ants', ..., 'you', 'your', 'zyxst']

(희소 행렬 인코딩) => [0(aardvark), ..., 0, 1(ants), 0, ..., 0, 1(get), 0, ..., 0, 1(you), 0, ..., 0(zyxst)]


이 수치 표현은 전체 데이터셋에서 고유한 각 단어를 특성으로 가진다. 원본 문자열에 있는 단어의 순서는 BOW 특성 표현에서 완전히 무시된다.


7.3.1 샘플 데이터에 BOW 적용하기

BOW 표현은 CountVectorizer 에 변환기 인터페이스로 구현되어 있다.
BOW 표현은 0이 아닌 값만 저장하는 SciPy 희소 행렬로 저장되어 있다. 희소 행렬의 실제 내용을 보려면 toarray 메서드를 사용하여 밀집된 NumPy 배열로 바꿔야 한다.


"The fool doth think he is wise," 은 첫 번째 행으로 나타난다. 어휘 사전의 첫 번째 단어 "be"가 0번 나온다. 두 번째 단어 "but"도 0번, 세 번째 단어 "doth"는 1번 나오는 식이다.


7.3.2 영화 리뷰에 대한 BOW

CountVectorize 객체의 get_feature_name 메서드는 각 특성에 해당하는 단어를 리스트로 반환한다. 숫자 특성들도 있고, 단수와 복수형이 서로 다른 단어로 어휘 사전에 포함되어 있다. 이런 단어들은 의미가 매우 비슷하므로 다른 특성으로 간주하여 개별적을 기록하는 것이 바람직하지 않다.

특성 추출 방법을 개선하기 전, 분류기를 만들어 성능 수치를 확인해본다. 희소 행렬의 고차원 데이터셋에서는 LogisticRegression 같은 선형 모델의 성능이 가장 뛰어나다. 교차 검증을 사용해 모델의 성능을 평가해보자.

교차 검증 평균 점수로 88%를 얻었다. 규제 매개변수 C가 있으므로 그리드 서치를 사용해 조정해보자.

 C = 0.1에서 교차 검증 점수 89%를 얻었다. 이 매개벼수를 사용해 테스트 세트의 일반화 성능을 재보자.

이제 단어 추출 방법을 개선해 보자. CountVectorizer는 정규표현식을 사용해 토큰을 추출한다. 기본적으로 사용하는 정규표현식은 \b\w\w+\b 이다. 경계(\b)가 구분되고 적어도 둘 이상의 문자나 숫자(\w)가 연속된 단어를 찾는다는 뜻이다. 한 글자로 된 단어는 찾지 않는다. "doesn't" 같은 축약형이나 "bit.ly" 같은 단어는 분리되고, "h8ter"는 한 단어로 매칭된다. CountVectorizer는 모든 단어를 소문자로 바꾼다. "soon", "Soon", "sOon"이 모두 같은 토큰(즉 특성)이 된다.

이를 줄이는 방법은 적어도 두 개의 문서(또는 다섯 개의 문서 등)에 나타난 토큰만을 사용하는 것이다. 하나의 문서에서만 나타난 토큰은 테스트 세트에 나타날 가능성이 적으므로 그리 큰 도움이 되지 않는다. min_df 매개변수로 토큰이 나타날 최소 문서 개수를 지정할 수 있다.

5로 설정하자, 특성의 수가 원래 개수의 1/3 정도인 27,271 개로 줄었다. 숫자 길이가 줄고 희귀한 단어와 철자가 틀린 단어들이 사라졌다. 그리드 서치를 사용해 모델의 성능을 확인해보자.

여전히 89%로 이전과 달라지지 않았다. 성능은 높아지지 않았지만, 특성의 개수가 줄어 처리 속도가 빨라지고 모델을 이해하기가 쉬워졌다.

7.4 불용어

의미 없는 단어를 제거하는 또 다른 방법이다. 너무 빈번하여 유용하지 않은 단어를 제외하는 것이다. 두 가지 방식이 있다. 언어별 불용어(stopword) 목록을 사용하는 것과 너무 자주 나타나는 단어를 제외하는 것이다. scikit-learn은 feature_extraction.text 모듈에 영어의 불용어를 가지고 있다.


stop_wrods="english" 라고 지정하면 내장된 불용어를 사용한다. 데이터셋에서 특성이 27,271 - 26,966 = 305개가 줄었다. 그리드 서치를 다시 적용해보자. 특성 305개를 제외했다고 성능이나 모델 해석이 나아진 것 같지는 않다. 고로 이 목록을 사용하는 게 도움이 되지 않는다.


고정된 불용어 목록은 모델이 데이터셋만 보고 불용어를 골라내기 어려운 작은 데이터셋에서 도움이 된다. 다른 방식으로는 CountVectorizer의 max_df 옵션을 지정하여 자주 나타나는 단어를 제거하고, 특성의 개수와 성능에 어떻게 영향을 주는지 확인하다.


7.5 tf-idf로 데이터 스케일 변경하기

중요치 않아 보이는 특성을 제외하는 대신, 얼마나 의미 있는 특성인지를 계산해 스케일을 조정하는 방식이 있다. 가장 널리 알려진 방식은 tf-idf(term frequency-inverse document frequency, 단어빈도-역문서빈도)이다. tf-idf는 말뭉치의 다른 문서보다 특정 문서에 자주 나타나는 단어에 높은 가중치를 주는 방법이다.

한 단어가 특정 문서에 자주 나타나고 다른 여러 문서에서는 그렇지 않다면, 그 문서의 내용을 아주 잘 설명하는 단어라고 볼 수 있다. scikit-learn은 두 개의 파이썬 클래스에 tf-idf를 구현했다. TfidfTransformer 는 CountVectorizer가 만든 희소 행렬을 입력받아 변환한다. TfidfVectorizer 는 텍스트 데이터를 입력받아 BOW 특성 추출과 tf-idf 변환을 수행한다.

문서 d에 있는 단어 w에 대한 tf-idf 점수는 TfidTransformer와 TfidfVectorizer에 다음과 같이 정의되어 있다.


N은 훈련 세트에 있는 문서의 개수

Nw는 단어 w가 나타난 훈련 세트 문서의 개수

tf(단어 빈도수)는 단어 w가 대상 문서 d(변환 또는 인코딩하려는 문서)에 나타난 횟수


두 파이썬 클래스 모두 tf-idf 계산 뒤 L2 정규화(L2 normalization)를 적용한다. 유클리디안 노름(euclidean norm)이 1이 되도록 각 문서 벡터의 스케일을 바꾼다. 스케일이 바뀐 벡터는 문서의 길이(단어의 수)에 영햐을 받지 않는다.


tf-idf는 어떤 단어가 가장 중요한지도 알려준다. 문서를 구별하는 단어를 찾는 방법임에도 완전히 비지도 학습이다. 따라서 '긍정적인 리뷰'와 '부정적인 리뷰' 레이블과 꼭 관계있지 않다는 게 중요하다.


tf-idf 가 낮은 특성은 전체 문서에 걸쳐 매우 많이 나타나거나, 조금씩만 사용되거나, 매우 긴 문서에서만 사용된다. tf-idf 가 높은 특성은 어떤 쇼나 영화를 나타내는 경우가 많다.


idf 값이 낮은 단어, 즉 자주 나타나서 덜 중요하다고 생각되는 단어들은 idf_ 속성에 저장되어 있다. 대부분 영어의 불용어다. "good", "great", "bad"도 매우 자주 나타나는 단어라 감정 분석에는 매우 중요하지만, tf-idf로 봤을 때는 덜 중요한 단어라고 분류된다.


7.6 모델 계수 조사

로지스틱 회귀 모델이 실제로 이 데이터에서 무엇을 학습했는지 자세히 살펴보자. 가장 큰 값의 계수와 해당 단어를 확인해보자.


왼쪽의 음수 계수는 모델에서 부정적인 리뷰를 의미하는 단어에 속한다. 오른쪽 양수 계수는 긍정적인 리뷰의 단어에 해당한다. "worst", "disappointment", "laughable"는 부정적인 리뷰이다. 반면, "excellent", "wonderful", "enjoyable", "refreshing"는 긍정적인 리뷰임을 말해준다.


"bit", "job", "today" 같은 단어들은 조금 덜 명확하지만 아마도 "good job", "best today" 같은 구절의 일부로 보인다.


7.7 여러 단어로 만든 BOW(n-그램)

BOW 표현 방식은 단어의 순서가 완전히 무시된다는 큰 단점이 있다. 의미가 완전히 반대인 두 문자열 "it's bad, not good at all" 과 "it's good, not bad at all"이 완전히 동일하게 변환된다. BOW 표현 방식을 사용할 때 문맥을 고려하는 방법이 있다.


토큰 하나의 횟수만 고려하지 않고 옆에 있는 두세 개의 토큰을 함께 고려하는 방법이다.

토큰 하나를 유니그램(unigram), 토큰 두 개를 바이그램(bigram), 세 개를 트라이그램(trigram)이라고 한다. 일반적으로 연속된 토큰을 n-그램(n-gram)이라고 한다. CounterVectorizer와 TfidfVectorizer는 ngram_range 매개변수에 특성으로 고려할 토큰의 범위를 지정할 수 있다. 매개변수의 입력값은 튜플이다. 연속된 토큰의 최소 길이와 최대 길이다,


n-그램이 클수록, 특성의 개수가 많아지며 구체적인 특성이 많아져 과대적합될 가능성이 있다. 그리드 서치로 최적의 n-그램 범위를 찾을 수 있다.


유니그램 모델에서는 없던 단어인 "worth"가 들어간 흥미로운 특성이 있다. "not worth"는 부정적인 리뷰를 의미한다. 그러나 "ㅇefinitely worth"와 "well worth"는 긍정적인 리뷰를 암시한다. 문맥을 파악할 수 있게 됐다.


7.8 고급 토큰화, 어간 추출, 표제어 추출

단수와 복수, 동사 형태이거나 'to 동사' 같은 명사 형태다. 이 들을 다른 토큰으로 다루면 모델을 일반화하는 데 도움이 되지 않는다. 각 단어를 그 단어의 어간(stem)으로 표현해 같은 어간을 가진 모든 단어를 구분해야 ( 또는 합쳐야 ) 한다. 일일이 어미를 찾아 제외하는 규칙 기반 방식을 어간 추출(stemming)이라고 한다.

알려진 단어의 형태 사전(명시적이고 사람이 구축한 시스템)을 사용하고 문장에서 단어의 역할을 고려하는 처리 방식을 표제어 추출(lemmatization)이라고 한다. 단어의 표준 형태를 표제어라고 한다.

두 처리 방식, 표제어 추출과 어간 추출은 단어의 일반 형태를 추출하는 정규화(normalization)의 한 형태로 볼 수 있다. 포터(Poter) 어간 추출기 ( nltk 패키지에서 임포트 )와 spacy 패키지에 구현된 표제어 추출 방식을 비교한다.

어간 추출은 항상 단어에서 어간만 남겨놓기 때문에, "was"는 "wa"가 된다. 표제어 추출은 올바른 동사인 "be" 를 추출한다. 표제어 추출은 "worse"를 "bad"로 정규화시키는 반면, 어간 추출은 "wors" 가 됐다. 어간 추출이 두 번의 "meeting"을 "meet"로 바꾼 것이 또 다른 큰 차이다. 표제어 추출은 첫 번째 "meeting"은 명사로 인식해 그대로 두고 두 번째 나타났을 땐 동사로 인식해 "meet"로 바꿨다.

일반적으로 표제어 추출은 어간 추출보다 훨씬 복잡한 처리를 거친다. 머신러닝을 위해 토큰 정규화를 할 때는 어간 추출보다 좋은 결과를 낸다.

scikit-learn 에 두 방법이 구현되어 있지 않다. 그러나 CounterVectorizer 에서 tokenizer 매개변수로 문서를 토큰화하는 방법을 따로 지정할 수 있다. spacy 표제어 추출을 사용해 문자열을 표제어 리스트로 변환하는 익명함수를 만든다.

27,271 개에서 21,637 개로 줄어든다. 일부 특성들을 합치기 때문에 일종의 규제로 볼 수 있다. 데이터셋이 작을 때도 표제어 추출이 성능을 높여줄 수 있다.

교차 검증을 하면, 표제어 추출의 성능이 조금 더 높다. 마지막 성능까지 끌어 올려야 할 때 시도해보면 좋다.

7.8.1 (한국어판 부록) KoNLPy를 사용한 영화 리뷰 분석

데이터셋은 한글로 된 영화 리뷰 20만개를 모은 <Naver sentiment movie corpus v1.0> (https://github.com/e9t/nsmc/) 을 사용한다. KoNLPy와 CountVectorizer 을 함께 사용해 감성 분서글 할 수 있다.


macOS 에서는 KoNLPy의 자바 기반의 태그 클래스가 호환되지 않는다. C++ 기반의 Mecab 태그 클래스를 사용한다. Mecab을 사용하려면 konlpy를 설치한 후 추가적인 설치가 필요하다. Mecab은 윈도우를 지원하지 않는다. KoNLPy의 설치 페이지(http://konlpy.org/ko/latest/install) 를 참고한다.


7.9 토픽 모델링과 문서 군집화

토픽 모델링(topic modeling)은 텍스트 데이터에 자주 적용하는 특별한 기법이다. 비지도 학습으로 문서를 하나 또는 그 이상의 토픽으로 할당하는 작업을  통칭한다. '정치', '스포츠', '금융' 등의 토픽으로 묶을 수 있는 뉴스 데이터가 좋은 예다. 문서가 둘 이상의 토픽을 가질 수 있다면 이는 3장에서 본 분해 방법과 관련이 있다.


학습된 각 성분은 하나의 토픽에 해당하며 문서를 표현한 성분의 계수는 문서가 어떤 토픽에 얼마만큼 연관되어 있는지를 말해준다. 잠재 디리클레 할당(Latent Dirichlet Allocation, LDA)이라고 하는 특정한 성분 분해 방법을 말한다.


7.9.1 LDA

LDA 모델은 함께 자주 나타나는 단어의 그룹(토픽)을 찾는 것이다. LDA는 각 문서에 토픽의 일부가 혼합되어 있다고 간주한다. 머신러닝에서 토픽은 일상에서의 "주제"가 아니라, 의미가 있든 없든 PCA나 NMF로 추출한 성분에 가까운 것이다. LDA의 토픽에 의미가 있다하더라도,일상 주제라고 부르는 것은 아니다.


영화 리뷰 데이터셋에 적용해보자. 텍스트 문서에 대하 비지도 학습 모델에서 분석의 결과가 왜곡되지 않으려면 자주 나타나는 단어를 제거하는 것이 좋다. NMF의 성분과 비슷하게 토픽은 어떤 순서를 가지고 있지 않다. 토픽의 수를 바꾸면 모든 토픽이 바뀌게 된다. 기본 학습 방법(online) 대신 조금 느리지만 성능이 더 나은 batch 방법을 사용하고 모델 성능을 위해 max_iter 값을 증가시킨다.


더 구체적일 수록, 해석하기가 더 어렵다. 중요도가 높은 토픽 중 거의 불용어에 가깝고 약간 부정적 경향의 단어인 "didn thought" 이다. 토픽 16 ("worst awful") 은 확실히 부정적이다.


LDA가 장르와 점수라는 두 종류의 큰 토픽과 어디에도 속하지 않는 토픽 몇 개를 더 찾았다. 대부분의 리뷰가 특정 영화에 대한 의견이거나 평가 점수를 합리화하거나 강조하기 위한 댓글이라는 사실은 재미있는 발견이다.


LDA와 같은 토픽 모델은 레이블이 없거나, 여기서처럼 레이블이 있더라도 큰 규모의 텍스트 말뭉치를 해석하는 데 좋은 방법이다. LDA는 확률적 알고리즘이기 때문에 random_state 매개변수를 바꾸면 결과가 많이 달라진다.


비지도 학습에서 내린 결론은 각 토픽에 해당하는 문서를 직접 보고 직관을 검증하는게 좋다. LDA.transform 메서드에서 만든 토픽이 지도 학습을 위한 압축된 표현으로 사용될 수도 있다. 특히 훈련 샘플이 적을 때 유용하다.


7.10 요약 및 정리

더 깊은 내용은 'Natural Language Processing with Python' (오라일리, 2009), 'Introduction to Information Retrieval' (캐임브리지 대학교, 2008, http://nlp.stanford.edu/IR-book/' 참고

고 수준의 텍스트 처리를 위해
spacy(비교적 최근, 효율적 잘 설계)
nltk (매우 잘 구축, 기능 풍부, 조금 오래된 라이브러리)
gensim (토픽 모델링이 강점인 자연어 처리 패키지)

word2vec 라이브러리에 구현된 단어 벡터(word vector) 또는 분산 단어 표현(distribute word representations) 이라는 연속적인 벡터 표현이다. 토마스 미콜로프의 논문 'Distributed Representations of Words an Phrases and Their Compositionality' (https://goo.gl/V3mTpj) 가 이 주제를 잘 소개하고 있다.

텍스트 처리에 순환 신경망 (recurrent neural networks, RNN)을 적용하는 것이다. RNN은 신경망의 한 종류로 클래스 레이블을 할당하는 분류 모델과 달리 텍스트를 출력할 수 있다. 텍스트 출력을 만들 수 있기 때문에 자동 번역이나 자동 요약에 RNN이 잘 들어맞는다.

이 주제에 관해서 일리야 수스케버(Ilya Suskever), 오리올 비니얼스(Oriol Vinyals), 쿠트 르(Quoc Le)가 쓴 'Sequence to Sequence Learning with Neural Networks' (https://goo.gl/1YNWlg) 논문을 참고.

텐서플로 프레임워크를 사용한 예제는 https://www.tensorflow.org/tutorials/seq2seq 에서 볼 수 있음


댓글