개발 이야기/Machine learning

Spark 환경에서 Sentiment analysis를 해보자 (1)

가마뫼 2019. 2. 24. 23:54
spark-sentiment-analysis

Spark 환경에서 Sentiment analysis를 해보자 (1)

Sentiment Analysis는 자연 언어처리 필드에서 오랫동안 연구되어 온 주제입니다. 고전적인 방법 (Naive Bayes)부터 비교적 최근에 많이 사용하는 Neural Network 계열 방법까지 다양한 방법이 존재하는데요. 대용량 데이터가 쌓이고 있는 현재 Spark 환경에서 Sentiment Analysis를 End-to-End로 예제를 들어 진행하려고 합니다. 분량 조절을 위해 이번 포스트에서는 데이터의 전처리까지, 다음 포스트에서는 Classifier 생성 및 평가로 나눠서 포스트를 작성하겠습니다.

환경

  • Spark 2.3.2
  • Scala 2.11.x
  • Zeppelin 0.8

Interactive 한 결과를 위해서 Zeppelin을 사용하였고, Spark user의 기호에 맞게 (Scala, Java, Python, R) 코드를 수정하시면 됩니다.

(Optional) Zeppelin에서 KOMORAN 로드

%spark.dep
z.addRepo("jitpack").url("https://jitpack.io")
z.load("com.github.shin285:KOMORAN:3.3.4")

한국어 형태소 분석을 위해서 외부 라이브러리인 KOMORAN을 가져오기 위해서 Zeppelin 첫 Paragraph에 위와 같이 선언해줍니다. Notebook이 아닌 Gradle이나 SBT에서도 유사하게 외부 라이브러리를 가져올 수 있습니다.

영화 리뷰 Text 파일 로드 및 DataSet 변환

val filePath = "s3://foo/bar/baz/ratings.txt"

val rawData = spark.read.option("sep", "\t").option("header", "true").csv(filePath)

https://github.com/e9t/nsmc 에서 네이버 영화 리뷰 데이터를 다운받아서 S3 또는 HDFS 경로에 저장합니다. Tab-separated 및 header를 true로 주고 DataSet 형태로 가져옵니다.

데이터 형태 및 분포

// 부정 리뷰 예제
rawData.filter($"label" === 0).show
//+--------+--------------------+-----+
//|      id|            document|label|
//+--------+--------------------+-----+
//| 6826470|  이런영화로 관객들한테 돈벌고싶소?|    0|
//| 6239594|작품 선구안이 없다는게 배우 김...|    0|
//| 8946612|     사극?? 로멘스?? 퓨젼??|    0|
//| 4800899|ㅋㅋㅋㅋ엿국니네가그렇지므ㅝ 이건...|    0|
//+-------+--------------------+-----+

// 긍정 리뷰 예제
rawData.filter($"label" === 1).show
//+-------+--------------------+-----+
//|     id|            document|label|
//+-------+--------------------+-----+
//|8112052| 어릴때보고 지금다시봐도 재밌어요ㅋㅋ|    1|
//|8132799|디자인을 배우는 학생으로, 외국...|    1|
//|4655635|폴리스스토리 시리즈는 1부터 뉴...|    1|
//|9251303|와.. 연기가 진짜 개쩔구나.....|    1|
//+-------+--------------------+-----+

// 긍정 부정 레코드 개수
rawData.groupBy("label").count.show
//+-----+------+
//|label| count|
//+-----+------+
//|    0|100000|
//|    1|100000|
//+-----+------+

긍정 리뷰 (label: 1), 부정 리뷰 (label: 0)인 것을 눈으로 봐도 확인할 수 있습니다. 긍정 리뷰와 부정 리뷰가 각각 10만 건씩 있으므로, 데이터 불균형도 없음을 확인할 수 있네요.

자연어 데이터 Tokenize

분류기를 만들기 전 문장 형태로 되어있는 자연어를 전처리합니다. 단순히 띄어쓰기 단위로 토큰을 분리할 수도 있겠지만 이번 포스트에서는 한국어 형태소 분석기 중 하나인 KOMORAN으로 문장을 Tokenize 하겠습니다. 같은 발음의 토큰이지만 다른 품사인 경우 긍정과 부정을 분류하는 데 영향도가 다를 수 있기 때문에 대개 형태소 분석기를 이용해서 Tokenize를 많이 합니다.

나는 밥을 먹는다 vs 하늘을 나는 자동차 (Ref)

나/N + 는/J vs 나(-ㄹ다)/V + 는/E 는 다릅니다!

import kr.co.shineware.nlp.komoran.constant.DEFAULT_MODEL
import kr.co.shineware.nlp.komoran.core.Komoran
import org.apache.spark.sql.expressions.UserDefinedFunction
import org.apache.spark.sql.functions.udf

val getPlainTextUdf: UserDefinedFunction = udf[Seq[String], String] { sentence =>
    val komoran = new Komoran(DEFAULT_MODEL.LIGHT)
    komoran.analyze(sentence).getPlainText.split("\\s")
}

val tokenizedData = rawData.withColumn("tokens", getPlainTextUdf($"document")).select("tokens", "label")

tokenizedData.show
//+-----------------------------------------------------------------+-----+
//|tokens                                                           |label|
//+-----------------------------------------------------------------+-----+
//|[굿/NNG]                                                          |1    |
//|[재밌/VA, 다/EC]                                                    |1    |
//|[고질/NNG, 이/VCP, 라니/EC, 무/NNG, 귀엽다능ㅋㅋ/NA]                         |1    |
//|[내/NP, 인생/NNG, 의/JKG, 영화/NNG]                                    |1    |
//+-------+--------------------+-----+

Token to Vector

컴퓨터는 우리가 사용하는 자연어를 인식하지 못하기 때문에, 컴퓨터가 인식할 수 있는 형태로 데이터를 변환해주어야 합니다. 단순하게는 Bag-of-Word와 같이 단어 (또는 토큰)을 Index로 mapping하는 방법이 있습니다. Bag-of-word와 같은 방법은 유니크한 단어 수에 비례해서 너무 공간을 많이 차지하게 되고, 단어간의 관계를 추론하기 어려운 단점이 있습니다. 따라서 이번 포스트에서는 워드 임베딩을 통해 단어를 Vector로 변환하는 방법을 사용하겠습니다. GloVe, FastText 등 많은 워드 임베딩 방법이 존재하지만 Spark에서 제공하는 워드 임베딩은 Skip-gram 기반의 Word2Vec 뿐입니다. 아쉬운대로 Spark Word2Vec으로 앞서 만든 토큰을 벡터로 변환해줍니다.

import org.apache.spark.ml.feature.Word2Vec

val word2Vec = (new Word2Vec()
    .setInputCol("tokens")
    .setOutputCol("vector")
    .setVectorSize(300)
    .setMaxIter(8)
    .setNumPartitions(8))
    
val model = word2Vec.fit(tokenizedData)
val vectorizedData = model.transform(tokenizedData)

중간 점검

이번 포스트를 통해, 원본 텍스트 파일을 Spark의 메인 자료구조인 DataSet으로 읽은 후 형태소 분석기를 활용한 Tokenization과 Word Embedding까지 Spark 환경에서 진행해보았습니다. 분류기의 입력 데이터 준비는 이제 끝이 났네요! 다음 포스트에서는 준비한 입력 데이터를 분류기에 학습 하는 방법과 강건한 모델을 만들기 위해서 Train/Validation/Test 분리 등의 내용으로 다시 돌아오겠습니다.