상세 컨텐츠

본문 제목

[PHP, MySQL] 코퍼스를 통해 관련어 추출

프로그래밍

by ∫2tdt=t²+c 2016. 8. 3. 03:22

본문

최근 살아있는 라틴어 사전에서는 관련어 정보를 함께 제공하기 시작했습니다. 한 단어와 같이 등장할 가능성이 높은 단어를 함께 보여줌으로써 단어 검색의 효율성과 효과를 높이고자 한 것인데요, 이번엔 이 관련어 추출에 사용한 방법에 대해서 이야기해보고자 합니다.


기본적인 가정은 다음과 같습니다.

관련 있는 단어일수록 한 문장에 같이 등장하는 빈도가 높을 것이다.

마찬가지로 한 문장에 같이 등장하는 빈도가 높을 수록 관련 있는 단어일 것이다.


즉 관련도를 함께 등장하는 빈도로 평가하겠다는 것이지요. 자 가장 간단한 방법으로 어떤 단어 A랑 함께 등장하는 모든 단어를 찾아서 그 함께 등장하는 빈도수로 정렬하는 것을 생각해 볼 수 있겠지요. 오늘 예시로 함께 할 라틴어 단어는 bibo (마시다)입니다.


코퍼스를 이용해 bibo와 함께 등장하는 단어를 빈도순으로 정렬해보았습니다.


단어빈도
et0.798
in0.281
edo0.265
aqua0.228
is0.208
non0.203
ut0.170
ego0.161
vinum0.161
qui0.153
sum0.138
cum0.136
tu0.127
ad0.121
qui20.119


어, 뭔가 우리가 원했던것이 아닙니다. et(그리고), in(안에), is(그), non(아닌) 등 라틴어 문장에서 자주쓰이는 단어들이 나타났네요. 너무나도 자주 쓰여서 실제 분석에 필요없는 이런 단어들을 불용어(stoplist)라 하는데, 불용어 제거가 필요한 시점입니다. 불용어를 어느정도 자주 등장하는 녀석으로 기준을 잡아야 할지 애매하지만, 일단 걸러내 보았습니다.


원래 등장빈도가 0.5% 이상인 어휘를 불용어라고 판단하고 필터링 해보았습니다.

aqua0.228
vinum0.161
comedo0.079
de0.076
tuus0.062
dominus0.060
suus0.058
meus0.056
do0.056
aut0.055
autem0.048
dies0.047
per0.046
ipse0.046
cibus0.041


아까 보다 나아졌습니다. 아직도 전치사나 기본적인 동사들이 많이 등장하긴 하지만, '마시다'는 의미와 관련있는 aqua(물), vinum(술), comedo(먹다) 등이 등장하고 있네요. 불용어 범위를 더 키워서 많은 단어를 필터링하면 해결될 것 같지만, 이 방법은 원래 중간쯤 등장하는 단어(불용어에는 속하지는 않지만 라틴어 문장에서 대체로 자주 등장하는 그런 어휘)를 걸러내지 못한다는 한계가 있겠지요. 그래서 단순 빈도로 관련어를 추출한다는 것은 영 바람직한 일은 아닌듯합니다.


다른 전략을 찾아보았습니다. 같이 등장하는 절대빈도를 가지고 판단할 것이 아니라, bibo라는 단어와 특별히 자주 같이 등장하는 단어를 찾는게 우리의 목표니깐, 평소의 등장 비율과 bibo와 함께 등장하는 비율 차이가 큰 녀석을 찾는게 합당할듯하네요. 수식으로 쓰면 다음과 같겠죠.


: 단어 w의 등장확률( = w가 등장하는 문장의 수 / 전체 문장의 수)

: bibo가 등장했을때 단어 w가 등장할 확률 (= bibo와 w가 동시에 등장하는 문장의 수 / bibo가 등장하는 문장의 수)

라고 했을때

bibo와 w의 관련도는 두 확률의 비라고 할 수 있지 않을까 하는 아이디어지요.

만약 평소에는 적게 등장하던 단어(P(w)가 작게 됨)가 bibo와는 자주 등장한다면(P(w|bibo)가 크게 됨), Cor(bibo, w)값은 커지게 되겠죠. 반면 bibo와 관련이 별로 없는 단어라면 두 비율의 차이가 거의 없을테니 Cor(bibo, w)값은 1에 가까워질 것이구요.


그러기 위해서는 사전에 모든 단어별로 P(w)값을 계산해두어야합니다. 코퍼스가 잘 색인되어있다면 별로 어렵지 않은 작업이지요.


한번 이 아이디어를 바탕으로 실험을 해보았습니다.


단어log Cor(bibo, w)log P(w|bibo)log P(w)
calix4.162-6.119-10.620
manduco3.910-6.215-10.405
comedo3.769-5.167-9.014
sitio3.705-6.377-10.348
inebrio3.505-7.264-11.388
hydria3.370-7.824-12.404
cisterna3.333-7.601-11.711
vinum3.323-4.465-7.812
asininus3.223-8.112-12.964
camelus3.082-7.419-10.926
aqua2.989-4.117-7.117
sanguisuga2.969-8.517-14.063
pluvialis2.948-8.112-11.982
ructo2.936-8.112-11.952
poculum2.928-6.571-9.634

값이 너무 커지길래 로그를 취했습니다. 오호 이제 제대로된 결과가 나오는듯합니다. calix(컵), manduco(씹다), comedo(먹다), sitio(목마르다), inebrio(취하다), hydria(물병), cisterna(물구덩이), vinum(술) 등이 줄줄이 등장하고 있지요. 모두 '마시다'는 의미와 연관을 찾을 수 있는 단어들입니다.


<추가내용>


이정도로 만족해도 좋겠지만, 여기서 좀더 나아가보지요. 과연 저 식이 수학적으로 엄밀한 것일까 하는 고민이 들었어요. 만약 통계적으로 좀더 유의미한 결과를 도출하려면 어떻게 식을 수정할 수 있을지 생각해보도록 해요.


전체 코퍼스 분석을 통해 얻어진 P(w)는 임의의 문장에서 단어 w가 등장할 확률을 가리킵니다. 만약 bibo와 단어 w가 등장하는 것이 독립적인 사건이라면, bibo라는 단어가 포함된 문장이 N개 있다고 할때, 그 문장에서 단어 w가 등장하는 횟수는 어떤 통계적 모델을 따릅니다. 기억을 돌이켜서 생각해보면 이항분포(Binomial distribution)이 떠오를겁니다. 발생확률이 p인 사건을 n회 시행한다고 하면, 이 분포는 B(n, p)인 이항분포를 따르게 됩니다. 저 많은 단어를 이항분포로 계산하기엔 계산량이 너무 많아요. 하지만 이항분포는 np > 5일때 정규분포 N(np, np(1-p))와 근사해진다는 성질이 있습니다. 그렇다면 임의의 문장 N개에서 단어 w가 등장하는 횟수 X는 N(np, np(1-p))의 정규분포를 따르게 되고, X가 a보다 클 확률 P(X > a)을 손쉽게 계산할 방법이 생깁니다.


이쯤에서 중간 정리를 하도록 하지요.

  1. 임의의 문장에서 단어 w가 등장하는 확률을 구했음 : P(w)
  2. 일정 확률의 사건을 여러 번 반복하는 것은 이항분포를 따르게 됨
  3. 단어 w가 출현할 확률 P(w)와 bibo가 출현하는 문장갯수 n을 바탕으로 이항분포 B(n, P(w))를 세울 수 있음
  4. 이항분포의 반복횟수가 증가할수록 이항분포는 정규분포를 따르게 됨
  5. 정규분포 공식을 이용해서 bibo가 출현하는 문장에 등장하는 단어 w의 갯수가 유의미한 수준인지 확인할 수 있음

자 이제 본격적으로 식을 세워봅시다. 


편의상 다음과 같이 두겠습니다.

이때 단어 w가 bibo와 독립이라고 하면 bibo와 함께 등장하는 횟수 X는 다음과 같은 분포를 따르게됩니다.

따라서 단어 w가 bibo와 함께 등장하는 횟수가 a이상일 확률은 다음과 같이 구해집니다. 평균과 표준편차를 이용해 표준화시키는 것이죠.

이때 Z가 1.96이상일 확률은 2.5%, 2.58이상일 확률은 0.5%라는 것은 잘 알려진 사실입니다. 보통 이 정도 확률에 도달하면 정상적으로 일어날수 없는 것이라고 가정하고, 둘 사이가 독립이라는 가정을 기각할 수 있게 됩니다. 즉 단어 w의 등장횟수 a가 위 계산을 통해 Z점수로 변환했을때 2.58이상이면 이건 독립이라고 말할 수 없다는거죠. 이 방법을 통해 다시 실험을 해보았습니다.


단어Z점수실제 등장횟수

예상 등장횟수(np)

표준편차()

vinum73.5112027.0372.652
aqua72.72328714.0943.753
comedo67.2981002.1161.454
calix57.672380.4250.652
manduco48.894360.5260.726
sitio39.453300.5570.746
panis27.324442.3261.525
inebrio26.595120.1970.444
hydria25.94670.0710.267
sanguisuga25.63330.0140.117
asininus24.57050.0410.202
praefoco24.21020.0070.082
genimen22.70940.0310.175
poculum22.372251.1381.067
cisterna20.80580.1430.378


결과는 앞의 것과 대동소이합니다. (vinum Z값이 73을 넘네요. 이정도 값이 나올 확률은 0.00000...1%가 간신히 될까말까일테니, 100%로 관련어라고 볼수 있겠죠.) 대부분 같은 단어가 순서 변동만 있는 정도네요. 약간의 약점이 있다면 평균치인 np가 5미만인 경우가 생각보다 많다는 것... 이항분포가 정규분포로 근사할수 있는 조건 중 가장 기초적인 것이 np > 5인데 이를 어기고선 그냥 정규분포로 근사했으니 통계학자한테 까여도 할말이 없어요.


앞서 확률 비율을 활용한 방법이나, 정규분포 근사를 활용한 방법이 큰 차이가 없어서, 실제 살아있는 라틴어 사전에 적용할때는 그나마 연산속도가 빠른 전자를 적용했습니다. 정규분포 근사하려면 제곱근도 구하고 빼고 나누고 좀더 절차가 복잡하니깐요.


실험에 사용했던 PHP, MySQL 코드를 공개합니다.


테이블 dict_usage에는 코퍼스 태깅이 완료된 단어목록이 들어가 있습니다. sid(문장 고유번호), wpos(문장 내 위치), pid(단어 고유번호)가 주요 필드이구요,

dict_word_dist에는 전체 단어 분포가 들어가 있습니다. pid(단어 고유번호), p(등장 확률)이 주요 필드입니다.


1. 단순 빈도



2. 확률 비



3. 정규분포 근사



관련글 더보기

댓글 영역