Statistically Improbable Phrases은 Amazon에서 개발한 시스템으로써, 해당 phrase가 얼마나 통계적으로 unique한 것인지를 파악하기 위한 장치입니다. 즉, 한 document에서 uniquely 자주 등장하는 phrase가 있다면,(corpus 전체적으로 봤을 때는 상대적으로 많이 등장하지 않으면서요) 그 phrase를 해당 document의 특징적인 phrase라고 할 수 있겠죠?

한 가지 예로, 만약 의학책이 있다면 해당 책의 SIP(Statistically Improbable Phrases)는 [‘심장수술’, ‘뼈의 구조’, …] 등이 나올 것이고, 법과 관련된 책이 있다면 SIP는 [‘미란다 원칙’, ‘형사소송’, …]등이 나올 것입니다.

img

아주 똑똑하죠..? 역시 Amazon답네요.. :)

SIP를 파악하기 위해서는 먼저 우리가 일반적으로 사용하는 구문들의 표준 데이터인 CORPUS가 필요합니다. 어떻게 보면 사전과 같다고 볼 수 있는데요, 저는 이 작업을 위해서 bi-gram data를 허가를 통해 가져왔습니다. 공짜이나 허가를 얻어야 하니 간단하게 sign up 하시고 받으시길 바랍니다 :)

Corpus data는 다음과 같이 (빈도수, 첫번째 단어, 두번째 단어) 형태로 구성되어 있습니다.

275	a	a
31	a	aaa
29	a	all
45	a	an
...

이 corpus를 다음과 같이 저장하고자 합니다.

  • 첫번째 단어로 key를 삼아 dictionary를 만듭니다.
  • value로 (두번째 단어, 빈도수)의 list를 관리합니다.
from collections import defaultdict

def readCorpus(filename):
    f = open(filename, 'r')
    corpus = defaultdict(list) #기본적으로 value로 []를 가지는 dict를 만듭니다.

    #corpus파일의 각 문장을 split하여 우리가 원하는 형태로 data를 넣어줍니다.
    for line in f: 
        freq, first, second = line.split()
        corpus[first].append((second, int(freq)))
    return corpus

>>> corpus = readCorpus('w2_.txt')
>>> len(corpus) #distinct한 첫번째 단어들의 개수는 47763개입니다.
47763

그럼 이제 우리가 알고 싶은 document를 같은 구조로 parsing하는 방법에 대해 알아봅시다. “she loves you yeah yeah yeah she loves you yeah yeah yeah”라는 document가 있다고 했을 때, 이것을 위와 같은 구조로 parsing하는 함수를 구현해 보겠습니다.

def tokenizeDoc(document):
    words = document.split()
    doc = defaultdict(lambda:defaultdict(int)) #Counting을 위해 nested defaultdict를 사용했습니다.
    for i in range(len(words)-1):
        doc[words[i]][words[i+1]] += 1

    #최종적인 결과는 dictionary[[list]]의 형태로 가져가야 하므로 counting결과를 변환시켜줍니다.
    tokens = {}
    for key in doc:
        tokens[key] = doc[key].items()
    return tokens

>>> document = "she loves you yeah yeah yeah she loves you yeah yeah yeah"
>>> doc = tokenizeDoc(document)
>>> doc
{'loves': [('you', 2)],
 'she': [('loves', 2)],
 'yeah': [('yeah', 4), ('she', 1)],
 'you': [('yeah', 2)]}

잘 parsing됨을 확인할 수 있네요 :) 다음으로 우리가 필요한 것은 document내의 빈도수와 corpus내에서의 빈도수 두 term이 주어졌을 때, improbability를 계산하는 함수입니다.

def calc_improbability(docFreq, corFreq):
	#분모가 0인 case는 항상 예외처리를 해줘야 합니다!
    return float(docFreq) / corFreq if corFreq > 0 else docFreq

우리의 데이터 구조가 dictionary[[dictionary]]구조였다면 쉽게 (첫번째 단어, 두번째 단어)로 접근이 가능했겠지만, 우리가 가진 구조는 dictionary[[list]]형태이므로 별도의 검색 함수가 필요합니다!(우리는 정확한 빈도수까지는 모르고 두번째 단어만 알기 때문이죠.)

def search(L, w, fun):
	#L은 검색할 list, w는 찾고자 하는 어떤 것, fun은 list element에서 어떻게 찾고자 하는 것을 끄집어 낼지에 대한 함수변수 입니다.
    for item in L:
        if fun(item) == w:
            return item
    return None #못 찾으면 None을 리턴합니다.

>>> print search(doc['yeah'], 'she', lambda x:x[0])
('she', 1)
>>> print search(doc['yeah'], 'haha', lambda x:x[0])
None

검색함수도 잘 구현되었네요 :) 이제 본격적으로 document의 bi-gram을 loop돌면서 improbability를 계산하고, 상위 k개의 SIP를 뽑는 함수를 만들어보겠습니다.

def top_k_improbables(k, doc, corpus):
    result = [("", 0)]*k
    for first in doc:
        for second, freq in doc[first]:
            search_result = search(corpus[first], second, lambda x:x[0])
            cor_freq = search_result[1] if search_result else 0
            improb = calc_improbability(freq, cor_freq)
            for i in range(k):
                if result[i][1] < improb:
                    result[i+1:] = result[i:-1]
                    result[i] = (first+' '+second, improb)
                    break
    return result

>>> top_k_improbables(5, doc, corpus)
[('you yeah', 2),
 ('yeah she', 1),
 ('yeah yeah', 0.05128205128205128),
 ('loves you', 0.0023121387283236996),
 ('she loves', 0.0011527377521613833)]

역시나 ‘she loves’와 같은 구문이 압도적으로 general하다는 것을 알 수 있네요 :) 이런식으로 document를 parsing한 후에 corpus와 빈도수를 비교해서 통계적으로 희소한 구문들을 뽑아내는 SIP 기능을 구현해 볼 수 있었습니다 :)