언어모델 (Language Model)

Jane의 study note.·2022년 11월 30일
0

NLP 자연어처리

목록 보기
6/24

[9-1]N_gram언어모델로문장_생성하기

목적 및 설명

  • 파이썬의 NLTK 패키지를 이용하여 N-gram 언어 모델을 구축한다
  • 네이버에서 오픈 소스로 제공하는 nsmc 영화 리뷰 데이터셋을 이용해 문장을 생성한다.
import nltk
from nltk.util import ngrams
from nltk import word_tokenize
from nltk import ConditionalFreqDist
from nltk.probability import ConditionalProbDist, MLEProbDist
import numpy as np
import codecs
from tqdm import tqdm
import random

# 한국어 처리에 필요한 konlpy 패키지를 설치하기 전에 선행 파일을 설치한다. 
!apt-get update

!apt-get install g++ openjdk-8-jdk python-dev python3-dev

!pip3 install JPype1-py3

!pip3 install konlpy

!JAVA_HOME="/usr/lib/jvm/java-8-openjdk-amd64"

from konlpy.tag import Okt

# NLTK 패키지를 이용하여 입력 텍스트를 N-gram 형태로 변환한다.
sentence = "나는 매일 아침 지하철을 탄다"

# NLTK 사용을 위하여 선행 패키지를 설치한다.
nltk.download('punkt')

# 입력 텍스트를 띄어쓰기 기준으로 토큰화한다.
tokens = word_tokenize(sentence) 

print(tokens)
['나는', '매일', '아침', '지하철을', '탄다']

# 한국어의 단어는 띄어쓰기를 기준으로 하지 않기 때문에 konlpy를 이용해 형태소를 기준으로 토큰화한다.
tagger = Okt()

def tokenize(text):
  tokens = ['/'.join(t) for t in tagger.pos(text)]
  return tokens

tokens = tokenize(sentence)
print(tokens)
['나/Noun', '는/Josa', '매일/Noun', '아침/Noun', '지하철/Noun', '을/Josa', '탄다/Verb']

# 토큰을 N-gram의 형태로 바꾸어준다. 
# ngrams 함수의 두 번째 인자로 N값을 지정할 수 있다.
bigram = ngrams(tokens, 2)
trigram = ngrams(tokens, 3)

# N-gram을 출력해본다.
print("bigram: ")
for b in bigram:
  print(b)

print("\ntrigram: ")
for t in trigram:
  print(t) 
  
bigram: 
('나/Noun', '는/Josa')
('는/Josa', '매일/Noun')
('매일/Noun', '아침/Noun')
('아침/Noun', '지하철/Noun')
('지하철/Noun', '을/Josa')
('을/Josa', '탄다/Verb')

trigram: 
('나/Noun', '는/Josa', '매일/Noun')
('는/Josa', '매일/Noun', '아침/Noun')
('매일/Noun', '아침/Noun', '지하철/Noun')
('아침/Noun', '지하철/Noun', '을/Josa')
('지하철/Noun', '을/Josa', '탄다/Verb')

# padding을 통해 입력 데이터에 문장의 시작과 끝을 알리는 토큰을 추가한다. 
bigram = ngrams(tokens, 2, pad_left=True, pad_right=True, left_pad_symbol="<s>", right_pad_symbol="</s>")
print("bigrams with padding: ")
for b in bigram:
  print(b)

bigram with padding: 
('<s>', '나/Noun')
('나/Noun', '는/Josa')
('는/Josa', '매일/Noun')
('매일/Noun', '아침/Noun')
('아침/Noun', '지하철/Noun')
('지하철/Noun', '을/Josa')
('을/Josa', '탄다/Verb')
('탄다/Verb', '</s>')

# 문장 생성을 위하여 네이버 영화 리뷰 데이터셋을 다운로드한다.
%%time
!wget -nc -q https://raw.githubusercontent.com/e9t/nsmc/master/ratings_train.txt

# 다운로드 받은 데이터셋을 읽고 인덱스와 라벨을 제외한 텍스트 부분만 가져온다.
# codecs 패키지는 대용량 파일을 조금씩 읽을 수 있게 해준다. 

with codecs.open("ratings_train.txt", encoding='utf-8') as f:
  data = [line.split('\t') for line in f.read().splitlines()] # \n 제외
  data = data[1:] # header 제외
print("데이터셋: ", data[:10])
docs = [row[1] for row in data] # 텍스트 부분만 가져옴
print("\n텍스트 데이터:", docs[:5])
print("\n문장 개수: ",len(docs)) # 총 15만개의 문장으로 이루어진 데이터셋임을 알 수 있다.


데이터셋:  [['9976970', '아 더빙.. 진짜 짜증나네요 목소리', '0'], ['3819312', '흠...포스터보고 초딩영화줄....오버연기조차 가볍지 않구나', '1'], ['10265843', '너무재밓었다그래서보는것을추천한다', '0'], ['9045019', '교도소 이야기구먼 ..솔직히 재미는 없다..평점 조정', '0'], ['6483659', '사이몬페그의 익살스런 연기가 돋보였던 영화!스파이더맨에서 늙어보이기만 했던 커스틴 던스트가 너무나도 이뻐보였다', '1'], ['5403919', '막 걸음마 뗀 3세부터 초등학교 1학년생인 8살용영화.ㅋㅋㅋ...별반개도 아까움.', '0'], ['7797314', '원작의 긴장감을 제대로 살려내지못했다.', '0'], ['9443947', '별 반개도 아깝다 욕나온다 이응경 길용우 연기생활이몇년인지..정말 발로해도 그것보단 낫겟다 납치.감금만반복반복..이드라마는 가족도없다 연기못하는사람만모엿네', '0'], ['7156791', '액션이 없는데도 재미 있는 몇안되는 영화', '1'], ['5912145', '왜케 평점이 낮은건데? 꽤 볼만한데.. 헐리우드식 화려함에만 너무 길들여져 있나?', '1']]

텍스트 데이터: ['아 더빙.. 진짜 짜증나네요 목소리', '흠...포스터보고 초딩영화줄....오버연기조차 가볍지 않구나', '너무재밓었다그래서보는것을추천한다', '교도소 이야기구먼 ..솔직히 재미는 없다..평점 조정', '사이몬페그의 익살스런 연기가 돋보였던 영화!스파이더맨에서 늙어보이기만 했던 커스틴 던스트가 너무나도 이뻐보였다']

문장 개수:  150000

# 토큰화한 텍스트 데이터의 bigram을 모두 리스트에 추가한다.
sentences = []
for d in tqdm(docs):
  tokens = tokenize(d)
  bigram = ngrams(tokens, 2, pad_left=True, pad_right=True, left_pad_symbol="<s>", right_pad_symbol="</s>")
  sentences += [t for t in bigram]
  
100%|██████████| 150000/150000 [05:49<00:00, 429.55it/s]

print(sentences[:10])
[('<s>', '아/Exclamation'), ('아/Exclamation', '더빙/Noun'), ('더빙/Noun', '../Punctuation'), ('../Punctuation', '진짜/Noun'), ('진짜/Noun', '짜증나네요/Adjective'), ('짜증나네요/Adjective', '목소리/Noun'), ('목소리/Noun', '</s>'), ('<s>', '흠/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', '</s>'), ('<s>', '너/Modifier'), ('너/Modifier', '무재/Noun'), ('무재/Noun', '밓었/Noun'), ('밓었/Noun', '다그/Noun'), ('다그/Noun', '래서/Noun'), ('래서/Noun', '보는것을/Verb'), ('보는것을/Verb', '추천/Noun'), ('추천/Noun', '한/Josa'), ('한/Josa', '다/Adverb')]

cfd = ConditionalFreqDist(sentences)
print(cfd["<s>"].most_common(5))

[('정말/Noun', 2718), ('이/Noun', 2371), ('진짜/Noun', 2232), ('이/Determiner', 2115), ('영화/Noun', 2069)]

# 주어진 토큰(c) 다음에 가장 많이 등장하는 n개의 단어를 반환하는 함수를 만든다.
def 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)

print(most_common("나", 10))
[('는/Josa', 831), ('의/Josa', 339), ('만/Josa', 213), ('에게/Josa', 148), ('에겐/Josa', 84), ('랑/Josa', 81), ('한테/Josa', 50), ('참/Verb', 45), ('이/Determiner', 44), ('와도/Josa', 43)]

# 단어별 등장 빈도를 기반으로 조건부 확률을 추정한다.
cpd = ConditionalProbDist(cfd, MLEProbDist)

# “.” 다음에 “</s>”가 올 확률을 출력한다.
print(cpd[tokenize(".")[0]].prob("</s>"))
0.39102658679807606

# 토큰 c 다음에 토큰 w가 bigram으로 함께 등장할 확률을 구한다.
def bigram_prob(c, w):
  context = tokenize(c)[0]
  word = tokenize(w)[0]
  return cpd[context].prob(word)
  
print(bigram_prob("이", "영화"))
0.4010748656417948

print(bigram_prob("영화", "이"))
0.00015767585785521414

# 조건부 확률을 알게 되면 가장 확률이 높은 토큰열을 토대로 문장을 생성할 수 있다.
def generate_sentence(seed=None, debug=False):
  if seed is not None:
    import random
    random.seed(seed)
  c = "<s>"
  sentence = []
  while True:
    if c not in cpd:
      break
    w = cpd[c].generate()

    if w == "</s>":
      break
    
    word = w.split("/")[0]
    pos = w.split("/")[1]

    # 조사, 어미 등을 제외하고 각 토큰은 띄어쓰기로 구분하여 생성한다.
    if c == "<s>":
      sentence.append(word.title())
    elif c in ["`", "\"","'","("]:
      sentence.append(word)
    elif word in ["'", ".", ",", ")", ":", ";", "?"]:
      sentence.append(word)
    elif pos in ["Josa", "Punctuation", "Suffix"]:
        sentence.append(word)
    elif w in ["임/Noun", "것/Noun", "는걸/Noun", "릴때/Noun",
                "되다/Verb", "이다/Verb", "하다/Verb", "이다/Adjective"]:
        sentence.append(word)
    else:
        sentence.append(" " + word)
    c = w

    if debug:
      print(w)

  return "".join(sentence)
  
  print(generate_sentence(2))
  도리까지 본 영화 너무... 뭔가.. 최고네요. 하지만.. 눈물 낫다는건 또 영화에 들지 않는다. 근데 뭐야 어떻게 그렇게 착했던 윤재랑은 에바 그린 드레스 소리 듣는거임""" 에리 욧의 미모로 합성 한 가수 노래와 흥행 놓친 영화다. 사투리 연기 하나 없는 ‘ 스피드 감 넘치는 스릴 넘치는 연기를 이해 되지 못 하시는 분보다 훨 재밌구만 평점을 망처 놓은 듯하다. 영화 보는이로 하여금 불편함을 느꼇을듯
  
  generate_sentence(2, debug=True)
  도리/Noun
까지/Josa
본/Verb
영화/Noun
너무/Adverb
.../Punctuation
...중략
영화/Noun
보는이로/Verb
하여금/Adverb
불편함을/Adjective
느꼇을듯/Noun
'도리까지 본 영화 너무... 뭔가.. 최고네요. 하지만.. 눈물 낫다는건 또 영화에 들지 않는다. 근데 뭐야 어떻게 그렇게 착했던 윤재랑은 에바 그린 드레스 소리 듣는거임""" 에리 욧의 미모로 합성 한 가수 노래와 흥행 놓친 영화다. 사투리 연기 하나 없는 ‘ 스피드 감 넘치는 스릴 넘치는 연기를 이해 되지 못 하시는 분보다 훨 재밌구만 평점을 망처 놓은 듯하다. 영화 보는이로 하여금 불편함을 느꼇을듯'
  

: 위 예제는 코퍼스 내의 등장 빈도에 기반하여 문장을 생성한다. bigram 언어 모델로 생성한 것이기 때문에 인접한 두 단어는 그나마 자연스럽지만 멀리 떨어진 단어와는 전혀 무관한 모습을 보인다. 또한 생성된 문장의 전체적인 문맥이 부자연스러우며 통사적으로 부적절한 모습도 보인다. 이는 코퍼스 내의 정보만으로 제한된 단어 조합만을 고려하는 N-gram 언어 모델의 한계로 보인다. 위 예제는 단순화를 위해 전처리와 규칙 처리를 최소화하였는데, 데이터셋을 늘리고 한국어 특징에 맞게 전처리를 진행한다면 보다 향상된 결과를 얻을 수 있을 것이다.

0개의 댓글