확률론적 언어 모형(Probabilistic Language Model)은 $m$개의 단어 $w_1, w_2, \ldots, w_m$ 열(word sequence)이 주어졌을 때 문장으로써 성립될 확률 $P(w_1, w_2, \ldots, w_m)$ 을 출력함으로써 이 단어 열이 실제로 현실에서 사용될 수 있는 문장(sentence)인지를 판별하는 모형이다.
이 확률은 각 단어의 확률과 단어들의 조건부 확률을 이용하여 다음과 같이 계산할 수 있다.
$$ \begin{eqnarray} P(w_1, w_2, \ldots, w_m) &=& P(w_1, w_2, \ldots, w_{m-1}) \cdot P(w_m\;|\; w_1, w_2, \ldots, w_{m-1}) \\ &=& P(w_1, w_2, \ldots, w_{m-2}) \cdot P(w_{m-1}\;|\; w_1, w_2, \ldots, w_{m-2}) \cdot P(w_m\;|\; w_1, w_2, \ldots, w_{m-1}) \\ &=& P(w_1) \cdot P(w_2 \;|\; w_1) \cdot P(w_3 \;|\; w_1, w_2) P(w_4 \;|\; w_1, w_2, w_3) \cdots P(w_m\;|\; w_1, w_2, \ldots, w_{m-1}) \end{eqnarray} $$여기에서 $P(w_m\;|\; w_1, w_2, \ldots, w_{m-1})$ 은 지금까지 $w_1, w_2, \ldots, w_{m-1}$라는 단어 열이 나왔을 때, 그 다음 단어로 $w_m$이 나올 조건부 확률을 말한다. 여기에서 지금까지 나온 단어를 문맥(context) 정보라고 한다.
이 때 조건부 확률을 어떻게 모형화하는냐에 따라
등으로 나뉘어 진다.
만약 모든 단어의 활용이 완전히 서로 독립이라면 단어 열의 확률은 다음과 같이 각 단어의 확률의 곱이 된다. 이러한 모형을 유니그램 모형이라고 한다.
$$ P(w_1, w_2, \ldots, w_m) = \prod_{i=1}^m P(w_i) $$만약 단어의 활용이 바로 전 단어에만 의존한다면 단어 열의 확률은 다음과 같다. 이러한 모형을 바이그램 모형 또는 마코프 모형(Markov Model)이라고 한다.
$$ P(w_1, w_2, \ldots, w_m) = P(w_1) \prod_{i=2}^{m} P(w_{i}\;|\; w_{i-1}) $$만약 단어의 활용이 바로 전 $n-1$개의 단어에만 의존한다면 단어 열의 확률은 다음과 같다. 이러한 모형을 N그램 모형이라고 한다.
$$ P(w_1, w_2, \ldots, w_m) = P(w_1) \prod_{i=n}^{m} P(w_{i}\;|\; w_{i-1}, \ldots, w_{i-n}) $$NLTK 패키지에는 바이그램과 N-그램을 생성하는 bigrams
, ngrams
명령이 있다.
from nltk import bigrams, word_tokenize
from nltk.util import ngrams
sentence = "I am a boy."
tokens = word_tokenize(sentence)
bigram = bigrams(tokens)
trigram = ngrams(tokens, 3)
print("\nbigram:")
for t in bigram:
print(t)
print("\ntrigram:")
for t in trigram:
print(t)
bigram: ('I', 'am') ('am', 'a') ('a', 'boy') ('boy', '.') trigram: ('I', 'am', 'a') ('am', 'a', 'boy') ('a', 'boy', '.')
조건부 확률을 추정할 때는 문장의 시작과 끝이라는 조건을 표시하기 위해 모든 문장에 문장의 시작과 끝을 나타내는 특별 토큰을 추가한다. 예를 들어 문장의 시작은 SS
, 문장의 끝은 SE
이라는 토큰을 사용할 수 있다.
예를 들어 ["I", "am", "a", "boy", "."]라는 토큰열(문장)은 ["SS", "I", "am", "a", "boy", ".", "SE"]라는 토큰열이 된다. ngrams 명령은 padding 기능을 사용하여 이런 특별 토큰을 추가할 수 있다.
bigram = ngrams(tokens, 2, pad_left=True, pad_right=True, left_pad_symbol="SS", right_pad_symbol="SE")
for t in bigram:
print(t)
('SS', 'I') ('I', 'am') ('am', 'a') ('a', 'boy') ('boy', '.') ('.', 'SE')
NLTK패키지를 사용하면 바이그램 형태의 조건부 확률을 쉽게 추정할 수 있다. 우선 ConditionalFreqDist
클래스로 각 문맥별 단어 빈도를 측정한 후에 ConditionalProbDist
클래스를 사용하면 조건부 확률을 추정한다.
from nltk import ConditionalFreqDist
sentence = "I am a boy."
tokens = word_tokenize(sentence)
bigram = ngrams(tokens, 2, pad_left=True, pad_right=True, left_pad_symbol="SS", right_pad_symbol="SE")
cfd = ConditionalFreqDist([(t[0], t[1]) for t in bigram])
ConditionalFreqDist
클래스는 문맥을 조건으로 가지는 사전 자료형과 비슷하다.
cfd.conditions()
['SS', 'I', 'am', 'a', 'boy', '.']
cfd["SS"]
FreqDist({'I': 1})
다음은 nltk 패키지의 샘플 코퍼스인 movie_reviews의 텍스트로부터 바이그램 확률을 추정하는 예제이다.
import nltk
nltk.download('movie_reviews')
nltk.download('punkt')
from nltk.corpus import movie_reviews
sentences = []
for tokens in movie_reviews.sents():
bigram = ngrams(tokens, 2, pad_left=True, pad_right=True, left_pad_symbol="SS", right_pad_symbol="SE")
sentences += [t for t in bigram]
sentences[:20]
[nltk_data] Downloading package movie_reviews to [nltk_data] /home/dockeruser/nltk_data... [nltk_data] Package movie_reviews is already up-to-date! [nltk_data] Downloading package punkt to /home/dockeruser/nltk_data... [nltk_data] Package punkt is already up-to-date!
[('SS', 'plot'), ('plot', ':'), (':', 'two'), ('two', 'teen'), ('teen', 'couples'), ('couples', 'go'), ('go', 'to'), ('to', 'a'), ('a', 'church'), ('church', 'party'), ('party', ','), (',', 'drink'), ('drink', 'and'), ('and', 'then'), ('then', 'drive'), ('drive', '.'), ('.', 'SE'), ('SS', 'they'), ('they', 'get'), ('get', 'into')]
문장의 처음(SS 문맥), i라는 단어 다음, 마침표 다음에 나오는 단어의 빈도는 다음과 같다.
cfd = ConditionalFreqDist(sentences)
cfd["SS"].most_common(5)
[('the', 8071), ('.', 3173), ('it', 3136), ('i', 2471), ('but', 1814)]
cfd["i"].most_common(5)
[("'", 1357), ('was', 506), ('can', 351), ('have', 330), ('don', 276)]
cfd["."].most_common(5)
[('SE', 63404), ('"', 1854), (')', 535), ("'", 70), (']', 10)]
그림으로 그리면 다음과 같다.
plt.subplot(311)
cfd["SS"].plot(5, title="문장의 첫단어 분포")
plt.subplot(312)
cfd["i"].plot(5, title="i 다음 단어의 분포")
plt.subplot(313)
cfd["."].plot(5, title=". 다음 단어의 분포")
빈도를 추정하면 각각의 조건부 확률은 기본적으로 다음과 같이 추정할 수 있다.
$$ P(w\;|\; w_c) = \dfrac{C((w_c, w))}{C((w_c))} $$위 식에서 $C(w_c, w)$은 전체 말뭉치에서 $(w_c, w)$라는 바이그램이 나타나는 횟수이고 $C(w_c)$은 전체 말뭉치에서 $(w_c)$라는 유니그램(단어)이 나타나는 횟수이다.
NLTK의 ConditionalProbDist
클래스에 MLEProbDist
클래스 팩토리를 인수로 넣어 위와 같이 빈도를 추정할 수 있다.
from nltk.probability import ConditionalProbDist, MLEProbDist
cpd = ConditionalProbDist(cfd, MLEProbDist)
트레이닝이 끝나면 조건부 확률의 값을 보거나 샘플 문장을 입력해서 문장의 로그 확률을 구할 수 있다.
cpd["i"].prob("am")
0.018562267971650354
cpd["i"].prob("is")
0.0002249971875351558
cpd["we"].prob("are")
0.08504504504504505
cpd["we"].prob("is")
0.0
조건부 확률을 알게 되면 각 문장의 확률을 구할 수 있다.
다음으로 이 토큰열을 N-그램형태로 분해한다. 바이그램 모형에서는 전체 문장의 확률은 다음과 같이 조건부 확률의 곱으로 나타난다.
$$ P(\text{SS I am a boy SE}) = P(\text{I}\;|\; \text{SS}) \cdot P(\text{am}\;|\; \text{I}) \cdot P(\text{a}\;|\; \text{am}) \cdot P(\text{boy}\;|\; \text{a}) \cdot P(.\;|\; \text{boy}) \cdot P(\text{SE}\;|\; .) $$우선 다음과 같이 문장(단어 리스트)의 리스트를 만든다.
def sentence_score(s):
p = 0.0
for i in range(len(s) - 1):
c = s[i]
w = s[i + 1]
p += np.log(cpd[c].prob(w) + np.finfo(float).eps)
return np.exp(p)
test_sentence = ["i", "like", "the", "movie", "."]
sentence_score(test_sentence)
2.740764134071561e-06
test_sentence = ["like", "i", "the", ".", "movie"]
sentence_score(test_sentence)
1.5015040140827832e-38
이 모형을 기반으로 임의의 랜덤한 문장을 생성할 수 있다.
def generate_sentence(seed=None):
if seed is not None:
import random
random.seed(seed)
c = "SS"
sentence = []
while True:
if c not in cpd:
break
w = cpd[c].generate()
if w == "SE":
break
elif w in ["i", "ii", "iii"]:
w2 = w.upper()
elif w in ["mr", "luc", "i", "robin", "williams", "cindy", "crawford"]:
w2 = w.title()
else:
w2 = w
if c == "SS":
sentence.append(w2.title())
elif c in ["`", "\"", "'", "("]:
sentence.append(w2)
elif w in ["'", ".", ",", ")", ":", ";", "?"]:
sentence.append(w2)
else:
sentence.append(" " + w2)
c = w
return "".join(sentence)
generate_sentence(6)
"Writers in one of the prison, yet, works as agent ray liotta, one of watching, we supposed to doubt that the holocaust - or the fact, the amc and don's wonderful experience, do what could either."
이번에는 한글 자료를 이용해보자 코퍼스로는 아래의 웹사이트에 공개된 Naver sentiment movie corpus 자료를 사용한다.
%%time
!wget -nc -q https://raw.githubusercontent.com/e9t/nsmc/master/ratings_train.txt
CPU times: user 0 ns, sys: 20 ms, total: 20 ms Wall time: 321 ms
import codecs
with codecs.open("ratings_train.txt", encoding='utf-8') as f:
data = [line.split('\t') for line in f.read().splitlines()]
data = data[1:] # header 제외
docs = [row[1] for row in data]
len(docs)
150000
import warnings
warnings.simplefilter("ignore")
from konlpy.tag import Okt
tagger = Okt()
def tokenize(doc):
tokens = ['/'.join(t) for t in tagger.pos(doc)]
return tokens
tokenize("그 영화는 아주 재미있었어요.")
['그/Noun', '영화/Noun', '는/Josa', '아주/Noun', '재미있었어요/Adjective', './Punctuation']
from tqdm import tqdm
sentences = []
for d in tqdm(docs):
tokens = tokenize(d)
bigram = ngrams(tokens, 2, pad_left=True, pad_right=True, left_pad_symbol="SS", right_pad_symbol="SE")
sentences += [t for t in bigram]
100%|██████████| 150000/150000 [03:59<00:00, 625.85it/s]
sentences[:30]
[('SS', '아/Exclamation'), ('아/Exclamation', '더빙/Noun'), ('더빙/Noun', '../Punctuation'), ('../Punctuation', '진짜/Noun'), ('진짜/Noun', '짜증나네요/Adjective'), ('짜증나네요/Adjective', '목소리/Noun'), ('목소리/Noun', 'SE'), ('SS', '흠/Noun'), ('흠/Noun', '.../Punctuation'), ('.../Punctuation', '포스터/Noun'), ('포스터/Noun', '보고/Noun'), ('보고/Noun', '초딩/Noun'), ('초딩/Noun', '영화/Noun'), ('영화/Noun', '줄/Noun'), ('줄/Noun', '..../Punctuation'), ('..../Punctuation', '오버/Noun'), ('오버/Noun', '연기/Noun'), ('연기/Noun', '조차/Josa'), ('조차/Josa', '가볍지/Adjective'), ('가볍지/Adjective', '않구나/Verb'), ('않구나/Verb', 'SE'), ('SS', '너/Modifier'), ('너/Modifier', '무재/Noun'), ('무재/Noun', '밓었/Noun'), ('밓었/Noun', '다그/Noun'), ('다그/Noun', '래서/Noun'), ('래서/Noun', '보는것을/Verb'), ('보는것을/Verb', '추천/Noun'), ('추천/Noun', '한/Josa'), ('한/Josa', '다/Adverb')]
cfd = ConditionalFreqDist(sentences)
cpd = ConditionalProbDist(cfd, MLEProbDist)
def korean_most_common(c, n, pos=None):
if pos is None:
return cfd[tokenize(c)[0]].most_common(n)
else:
return cfd["/".join([c, pos])].most_common(n)
korean_most_common("나", 10)
[('는/Josa', 831), ('의/Josa', 339), ('만/Josa', 213), ('에게/Josa', 148), ('에겐/Josa', 84), ('랑/Josa', 81), ('한테/Josa', 50), ('참/Verb', 45), ('이/Determiner', 44), ('와도/Josa', 43)]
korean_most_common("의", 10)
[('영화/Noun', 19), ('연기/Noun', 14), ('구심/Noun', 12), ('모습/Noun', 9), ('감독/Noun', 8), ('매력/Noun', 7), ('감동/Noun', 7), ('흐름/Noun', 6), ('그/Noun', 6), ('이야기/Noun', 6)]
korean_most_common(".", 10, "Punctuation")
[('SE', 26503), ('영화/Noun', 667), ('이/Noun', 565), ('정말/Noun', 480), ('그리고/Conjunction', 455), ('./Punctuation', 445), ('하지만/Conjunction', 369), ('이/Determiner', 352), ('그/Noun', 325), ('스토리/Noun', 317)]
def korean_bigram_prob(c, w):
context = tokenize(c)[0]
word = tokenize(w)[0]
return cpd[context].prob(word)
korean_bigram_prob("이", "영화")
0.4010748656417948
korean_bigram_prob("영화", "이")
0.00015767585785521414
def korean_generate_sentence(seed=None, debug=False):
if seed is not None:
import random
random.seed(seed)
c = "SS"
sentence = []
while True:
if c not in cpd:
break
w = cpd[c].generate()
if w == "SE":
break
w2 = w.split("/")[0]
pos = w.split("/")[1]
if c == "SS":
sentence.append(w2.title())
elif c in ["`", "\"", "'", "("]:
sentence.append(w2)
elif w2 in ["'", ".", ",", ")", ":", ";", "?"]:
sentence.append(w2)
elif pos in ["Josa", "Punctuation", "Suffix"]:
sentence.append(w2)
elif w in ["임/Noun", "것/Noun", "는걸/Noun", "릴때/Noun",
"되다/Verb", "이다/Verb", "하다/Verb", "이다/Adjective"]:
sentence.append(w2)
else:
sentence.append(" " + w2)
c = w
if debug:
print(w)
return "".join(sentence)
korean_generate_sentence(0)
'미키짱과 말도 전혀 빗나가지 않던 전개로 꽥꽥대는거 보니까 요^^'
korean_generate_sentence(1)
'내용 일테인데 이 영화 최고의 암살 할려고 한 데 선배랑 김선아 연기도 크다. 배슬기 여배우도 있는 척 하는거지?'
korean_generate_sentence(2)
'도리까지 본 영화 너무... 뭔가.. 최고네요. 하지만.. 눈물 낫다는건 또 영화에 들지 않는다. 근데 뭐야 어떻게 그렇게 착했던 윤재랑은 에바 그린 드레스 소리 듣는거임""" 에리 욧의 미모로 합성 한 가수 노래와 흥행 놓친 영화다. 사투리 연기 하나 없는 ‘ 스피드 감 넘치는 스릴 넘치는 연기를 이해 되지 못 하시는 분보다 훨 재밌구만 평점을 망처 놓은 듯하다. 영화 보는이로 하여금 불편함을 느꼇을듯'
korean_generate_sentence(3)
'내 인생을 반헬싱이 너무 무섭고 재밌고, 칼 세이건으로 연탄가스 맡아서 죽을 같이 작업 하는구나 ㅋㅋㅋㅋㅋ 진짜'
korean_generate_sentence(5)
'좋았어요... ㅎㄷㄷㄷ 시나리오나 그래픽이 대단한 심리전이 미라 파스틱 함.. 너무 무섭고 나쁜세 끼는 듯 진짜 꼭 필요가 있는지도 모르겠지만 나름 그의 복수 후!!!!!!!!!!!'
확률론적 언어 모형은 다음과 같은 분야에 광범위하게 활용할 수 있다.