방대한 텍스트 데이터가 주어졌을 때, 그 속에서 의미 있는 정보를 추출하려면 어떻게 해야 할까요?
가장 좋은 출발점은 텍스트를 n-gram 단위로 쪼개는 것입니다.
n-gram이란 무엇일까요?
계산언어학과 확률론 분야에서 n-gram이란 주어진 텍스트 시퀀스에서 n개의 항목으로 이루어진 연속적인 시퀀스를 의미합니다. – 위키백과
예시를 살펴보겠습니다:
"Hello there, how are you?"라는 문구를 예로 들면, 유니그램(unigram, 1개 요소로 구성된 n-gram)은 "Hello", "there", "how", "are", "you"이며, 바이그램(bigram, 2개 요소로 구성된 n-gram)은 ["Hello", "there"], ["there", "how"], ["how", "are"], ["are", "you"]입니다.
그림으로 보는 것이 더 이해에 도움이 된다면 아래 이미지를 참고하세요:

이제 루비로 이를 구현하는 방법을 살펴보겠습니다!
샘플 데이터 다운로드하기
본격적으로 작업에 들어가기 전에 샘플 데이터가 필요합니다.
분석할 데이터가 없다면 위키백과 문서나 블로그 글 몇 개를 내려받으면 됩니다. 이번 예제에서는 freenode의 #ruby 채널 IRC 로그를 다운로드해 사용했습니다.
로그는 다음 사이트에서 확인할 수 있습니다:
irclog.whitequark.org/ruby
데이터 형식에 관한 참고 사항:
분석하려는 자료의 일반 텍스트 버전이 제공되지 않는 경우, Nokogiri를 사용해 페이지를 파싱하고 데이터를 추출할 수 있습니다.
IRC 로그는 URL 끝에 .txt를 붙이면 일반 텍스트 형식으로 제공되므로, 이번 예제에서는 이 기능을 활용했습니다.
다음 클래스는 데이터를 다운로드하고 저장하는 역할을 담당합니다:
require 'restclient'
class LogParser
LOG_DIR = 'irc_logs'
def initialize(date)
@date = date
@log_name = "#{LOG_DIR}/irc-log-#{@date}.txt"
end
def download_page(url)
return log_contents if File.exist? @log_name
RestClient.get(url).body
end
def save_page(page)
File.open(@log_name, "w+") { |f| f.puts page }
end
def log_contents
File.readlines(@log_name).join
end
def get_messages
page = download_page("https://irclog.whitequark.org/ruby/#{@date}.txt")
save_page(page)
page
end
end
log = LogParser.new("2015-04-15")
msg = log.get_messages
구조가 매우 단순한 클래스입니다.
HTTP 클라이언트로는 RestClient를 사용하고, 결과물을 파일로 저장해 둡니다. 이렇게 하면 프로그램을 수정하며 테스트하는 동안 같은 페이지를 반복해서 요청하지 않아도 됩니다.
데이터 분석하기
데이터를 확보했으니 이제 분석을 시작해 보겠습니다.
다음은 간단한 Ngram 클래스입니다.
이 클래스에서는 n-gram을 생성해 주는 Array#each_cons 메서드를 사용합니다.
이 메서드는 Enumerator를 반환하기 때문에 Array 객체로 변환하려면 to_a를 호출해야 합니다.
class Ngram
def initialize(input)
@input = input
end
def ngrams(n)
@input.split.each_cons(n).to_a
end
end
그다음에는 루프와 Hash#merge!, Enumerable#sort_by를 활용해 전체 과정을 하나로 연결합니다.
코드는 다음과 같습니다:
# 이 횟수보다 적게 등장하는 단어는 필터링
MIN_REPETITIONS = 20
total = {}
# 해당 월의 처음 15일치 로그를 가져와 트라이그램 집계
(1..15).each do |n|
day = '%02d' % [n]
total.merge!(get_trigrams_for_date "2015-04-#{day}") { |k, old, new| old + new }
end
# 내림차순 정렬
total = total.sort_by { |k, v| -v }.reject { |k, v| v < MIN_REPETITIONS }
total.each { |k, v| puts "#{v} => #{k}" }
참고: 설명의 간결함을 위해
get_trigrams_for_date메서드는 본문에서 생략했습니다. 전체 코드는 GitHub에서 확인할 수 있습니다.
실제 실행 결과는 다음과 같습니다:
112 => i want to 83 => link for more 82 => is there a 71 => you want to 66 => i don't know 66 => i have a 65 => i need to
결과를 보면 알 수 있듯이, #ruby 채널에서는 "무언가를 하고 싶다(i want to)"는 표현이 가장 많이 등장합니다 🙂
마무리
이제 여러분 차례입니다!
에디터를 열고 직접 n-gram 분석을 실험해 보세요. n-gram이 실무에서 어떻게 활용되는지 궁금하다면 Google Ngram Viewer를 살펴보는 것도 좋은 방법입니다.
자연어 처리(NLP)는 무척 흥미로운 분야입니다. 위키백과에서 이 주제에 대한 훌륭한 개요를 확인할 수 있습니다.
이 글의 전체 코드는 다음 GitHub 저장소에서 확인할 수 있습니다: https://github.com/matugm/ngram-analysis/blob/master/irc_histogram.rb