7 분 소요

04 문서 유사도 및 언어 모델

1. 문서 유사도 측정

  • 문서는 다양한 요소와 이들의 상호작용으로 구성된다.

    image

  • 가장 기본 단위인 단어와 상위 개념인 문장은 다양한 정보를 포함한다.

    image

  • 문서의 가장 기본 단위인 단어를 활용하여 문서를 수치형 데이터로 표현하고 이를 기반으로 유사도를 측정한다.
  • 문서 유사도를 측정하기 위해 단어 기준으로 생성한 문서 벡터 간의 코사인 유사도를 사용한다.

2. Bag of words

  • Bag of words는 문서 내 단어의 빈도수를 기준으로 문서를 벡터로 바꿔준다.
  • 가정: 자주 발생하는 단어가 문서의 특징을 나타낸다.

image

  • 존재하지 않는 단어의 빈도수는 0으로 설정한다.
  • Bag of words 문서 벡터의 차원은 데이터 내 발생하는 모든 단어의 종류 개수와 동일하다.

장점

Bag of words의 장점: 벡터의 구성 요소가 직관적이다. 왜 유사도가 크고 낮은지 쉽게 이해할 수 있다.

문제1

Bag of words 문서 벡터는 합성어를 독립적인 단어로 개별 처리한다.

  • log off ⇒ log, off ⇒ 통나무의 log로 오해할 수 있다.

해결1 Bag of N-grams

Bag of N-grams: n-gram의 발생 빈도를 기준으로 문서 벡터를 표현한다. ⇒ log off를 하나의 단어로 취급 가능하다.

  • N-gram은 연속된 N개의 단어를 기준으로 텍스트 분석을 수행한다. N은 원하는 만큼 늘릴 수 있다.

    image

    • unigram: 띄어쓰기를 기준으로 각각 문서 벡터의 컬럼이 된다.
    • N = 2 (bi-gram): 2개 묶음으로 1개씩 겹쳐서 문서 벡터의 컬럼이 된다.
    • N = 3 (trigram): 3개 묶음으로 2개씩 겹쳐서 문서 벡터의 컬럼이 된다.
  • Bag of N-grams는 여러 n-gram을 합쳐서 발생 빈도를 기준으로 문서 벡터를 표현할 수 있다.

    image

문제2

Bag of words: 자주 발생하는 단어가 문서의 특징을 항상 효과적으로 표현하지는 않는다.

문서의 내용이 많아지면 자주 발생하는 단어가 문서의 주요 내용이나 특징을 대변하지는 않는다.

image

그리고, 그러나, 잘 등은 문서의 핵심적인 내용과 무관하다.

image

해결2: TF-IDF

TF-IDF(term frequency-inverse document frequency)

  • TF-IDE 기반의 bag of words 문서 벡터는 단어의 상대적 중요성을 반영한다.
  • 문서 내 상대적으로 자주 발생하는 단어가 더 중요하다. 즉 똑같이 많이 등장하는 단어는 의미가 없다.
  • 문서 내 상대적으로 자주 발생하는 단어를 점수화 한다.

    image

    • 그리고, 그러나, 잘 등 단어들의 점수가 줄어들었고, 봄, 컴퓨터의 점수는 상승했다.

코드 실습 Bag of words 기반 문서 벡터 생성

import re
from sklearn.feature_extraction.text import CountVectorizer

regex = re.compile('[^a-z ]')

with open("text.txt", 'r') as f:
    documents = []
    for line in f: # 각 line들은 영화의 리뷰
        # doucments 리스트에 리뷰 데이터를 저장하세요.
        filtered_doc = regex.sub("", line.rstrip())
        documents.append(filtered_doc) # 전처리 완료
        
        
# CountVectorizer() 객체를 이용해 Bag of words 문서 벡터를 생성하여 변수 X에 저장하세요.  
cv = CountVectorizer()
X = cv.fit_transform(documents)

# 변수 X의 차원을 변수 dim에 저장하세요.
dim = X.shape
# X 변수의 차원을 확인해봅니다.
print("dim", dim)

# 위에서 생성한 CountVectorizer() 객체에서 첫 10개의 칼럼이 의미하는 단어를 words_feature 변수에 저장하세요.
words_feature = cv.get_feature_names()[:10]
# CountVectorizer() 객체의 첫 10개 칼럼이 의미하는 단어를 확인해봅니다.
print("words_feature", words_feature)

# 단어 "comedy"를 의미하는 칼럼의 인덱스 값을 idx 변수에 저장하세요.
idx = cv.vocabulary_['comedy']
# 단어 "comedy"의 인덱스를 확인합니다.
print("idx", idx)

# 첫 번째 문서의 Bag of words 벡터를 vec1 변수에 저장하세요.
vec1 = X[0]
# 첫 번째 문서의 Bag of words 벡터를 확인합니다.
print(vec1)

>> 실행 결과
# 454개의 영화 데이터, 12640개의 bag of words
dim (454, 12640)
# 10개의 bag of words 출력
words_feature ['aal', 'aba', 'abandon', 'abandoned', 'abbot', 'abducted', 'abets', 'abilities', 'ability', 'abilitytalent']
# comedy 열의 인덱스
idx 2129
# 첫 번째 문서의 Bag of words 빈도수
  (0, 9686)     4
  (0, 5525)     4
  (0, 6010)     4
  (0, 1761)     1
  (0, 2129)     1
  (0, 9081)     1
  (0, 948)      2
  (0, 11184)    8
  (0, 9829)     1
  (0, 11321)    1
  (0, 870)      2
  (0, 10446)    1
  (0, 8064)     1
  (0, 8847)     1
  (0, 16)       1
  (0, 9910)     2
  (0, 6444)     1
  (0, 10842)    1
  (0, 3245)     2
  (0, 12582)    1
  (0, 5689)     2
  (0, 11070)    1
  (0, 8837)     1
  (0, 6343)     1
  (0, 6826)     2
  :     :
  (0, 9400)     1
  (0, 11527)    1
  (0, 1635)     1
  (0, 3144)     1
  (0, 5633)     1
  (0, 9213)     1
  (0, 2012)     1
  (0, 6479)     1
  (0, 5126)     1
  (0, 9794)     1
  (0, 7803)     1
  (0, 12607)    1
  (0, 3420)     1
  (0, 3978)     1
  (0, 6750)     1
  (0, 196)      1
  (0, 7185)     1
  (0, 257)      1
  (0, 11237)    1
  (0, 4134)     1
  (0, 4212)     1
  (0, 4996)     1
  (0, 8519)     1
  (0, 6046)     1
  (0, 6028)     1

코드 실습 TF-IDF Bag fo words 기반 문서 벡터 생성

import re
from sklearn.feature_extraction.text import TfidfVectorizer

regex = re.compile('[^a-z ]')

# 리뷰 데이터를 가져옵니다. 이전 실습과 동일하게 리스트 `documents`에는 전처리되어 있는 리뷰 데이터가 들어있습니다.
with open("text.txt", 'r') as f:
    documents = []
    for line in f:
        lowered_sent = line.rstrip().lower()
        filtered_sent = regex.sub('', lowered_sent)
        documents.append(filtered_sent)

# TfidfVectorizer() 객체를 이용해 TF-IDF Bag of words 문서 벡터를 생성하여 변수 X에 저장하세요.
tv = TfidfVectorizer()
X = tv.fit_transform(documents)

# 변수 X의 차원을 변수 dim1에 저장하세요.
dim1 = X.shape
# X 변수의 차원을 확인해봅니다.
print("dim1", dim1)

# 첫 번째 문서의 TF-IDF Bag of words를 vec1 변수에 저장하세요.
vec1 = X[0]
# 첫 번째 문서의 TF-IDF Bag of words를 확인합니다.
print("vec1", vec1)

# 위에서 생성한 TfidfVectorizer() 객체를 이용해 TF-IDF 기반 Bag of N-grams 문서 벡터를 생성하세요.
unibi_v = TfidfVectorizer(ngram_range=(1, 2)) # unigram과 bigram을 사용하여 문서 벡터를 생성한다는 의미
unibigram_X = unibi_v.fit_transform(documents)

# 생성한 TF-IDF 기반 Bag of N-grams 문서 벡터의 차원을 변수 dim2에 저장하세요.
dim2 = unibigram_X.shape
# 문서 벡터의 차원을 확인합니다.
print("dim2", dim2)

>> 실행 결과
dim1 (454, 12136)
# 정수 형태가 아닌 보정된 값이 들어갔다.
vec1   (0, 5679)        0.058640619958889736
  (0, 8003)     0.10821800789540346
  (0, 11827)    0.03351360629176965
  (0, 3976)     0.10821800789540346
  (0, 3885)     0.056559253324120214
  (0, 10825)    0.040897765011371726
  (0, 228)      0.07670128660443204
  (0, 173)      0.08289290212342751
  (0, 6546)     0.043212451333837304
  (0, 3731)     0.06944792122696908
  (0, 11793)    0.08712444266241767
  (0, 12103)    0.05368632319734369
  (0, 7470)     0.025687260438575044
  (0, 9189)     0.10821800789540346
  (0, 4994)     0.04450120519256354
  (0, 5326)     0.05241495461089306
  (0, 5538)     0.09654704887371249
  (0, 6240)     0.05908965016142883
  (0, 1896)     0.06002531501567428
  (0, 8681)     0.10139093452026096
  (0, 5344)     0.08289290212342751
  (0, 3162)     0.05211156559734475
  (0, 1438)     0.09278983927035103
  (0, 11136)    0.07804901647687901
  (0, 8858)     0.09654704887371249
  :     :
  (0, 8333)     0.09654704887371249
  (0, 10655)    0.10821800789540346
  (0, 5394)     0.036855273761029705
  (0, 12074)    0.045486142952180335
  (0, 7080)     0.06950749415600704
  (0, 10653)    0.43287203158161386
  (0, 10410)    0.04631780012339499
  (0, 6199)     0.045486142952180335
  (0, 9332)     0.14858361374703516
  (0, 22)       0.030847058442278544
  (0, 8343)     0.09654704887371249
  (0, 7576)     0.03767413181742104
  (0, 9948)     0.03226580332806431
  (0, 632)      0.04595176416002607
  (0, 10915)    0.03552172020508319
  (0, 9232)     0.05368632319734369
  (0, 10769)    0.1528768111964441
  (0, 683)      0.05694325708073058
  (0, 8584)     0.08971997549857
  (0, 5685)     0.038437344953303164
  (0, 2038)     0.05617216421425451
  (0, 1605)     0.08712444266241767
  (0, 5672)     0.07371054752205941
  (0, 5036)     0.2575794789063761
  (0, 1356)     0.43287203158161386
# 12136에서 74358으로 feature가 크게 늘어났다.
dim2 (454, 74358)

코드 실습 Bag of words 기반 문서 유사도 측정 서비스

# 경고문을 제거합니다.
import warnings
warnings.filterwarnings(action='ignore')

import pickle
from sklearn.metrics.pairwise import cosine_similarity

sent1 = ["I first saw this movie when I was a little kid and fell in love with it at once."]
sent2 = ["Despite having 6 different directors, this fantasy hangs together remarkably well."]

with open('bow_models.pkl', 'rb') as f:
    # 저장된 모델을 불러와 객체와 벡터를 각각vectorizer와 X에 저장하세요.
    vectorizer, X = pickle.load(f) # vectorizer: TF-IDF 객체, X: 문서 행렬

# sent1, sent2 문장을 vectorizer 객체의 transform() 함수를 이용해 변수 vec1, vec2에 저장합니다.
vec1 = vectorizer.transform(sent1)
vec2 = vectorizer.transform(sent2)

#  vec1과 vec2의 코사인 유사도를 변수 sim1에 저장합니다.
sim1 = cosine_similarity(vec1, vec2)
# 두 벡터의 코사인 유사도를 확인해봅니다.
print("sim1", sim1)

# vec1과 행렬 X의 첫 번째 문서 벡터 간 코사인 유사도를 변수 sim2에 저장합니다.
sim2 = cosine_similarity(vec1, X[0])
# X의 첫 번째 문서와 vec1의 코사인 유사도를 확인해봅니다.
print("sim2", sim2)

>> 실습 결과
sim1 [[0.00936629]]
sim2 [[0.04116601]]

문제3

텍스트 데이터의 양이 증가하면, 문서 벡터의 차원 증가

  • 지프의 법칙: long tail
  • 대부분 단어의 빈도수가 0인 희소 벡터가 생성

⇒ 메모리 제약 및 비효율성 발생, 차원의 저주 발생

해결3 doc2vec

3. doc2vec

  • doc2vec은 문서 내 단어 간 문맥적 유사도를 기반으로 문서 벡터를 임베딩한다.
  • 문서 내 단어의 임베딩 벡터를 학습하면서 문서의 임베딩도 지속적으로 학습한다.

image

  • word2vec과 차이점
    • 문서 벡터가 입력값으로 추가된다.
  • 유사한 문맥의 문서 임베딩 벡터는 인접한 공간에 위치

    image

  • 장점: doc2vec은 상대적으로 저차원의 공간에서 문서 벡터를 생성
    • 히든 노드의 개수에 따라 차원이 달라진다. 히든 노드의 개수를 지정할 수 있다.

코드 실습

# -*- coding: utf-8 -*-
import random
import re
from gensim.models.doc2vec import Doc2Vec, TaggedDocument
from numpy import sqrt, dot

random.seed(10)

doc1 = ["homelessness has been an issue for years but never a plan to help those on the street that were once considered human who did everything from going to school work or vote for the matter"]

doc2 = ["it may have ends that do not tie together particularly well but it is still a compelling enough story to stick with"]

# 데이터를 불러오는 함수입니다.
def load_data(filepath):
    regex = re.compile('[^a-z ]')

    gensim_input = []
    with open(filepath, 'r') as f:
        for idx, line in enumerate(f):
            lowered_sent = line.rstrip().lower()
            filtered_sent = regex.sub('', lowered_sent)
            tagged_doc = TaggedDocument(filtered_sent, [idx])
            gensim_input.append(tagged_doc)
            
    return gensim_input
    
def cal_cosine_sim(v1, v2):
    # 벡터 간 코사인 유사도를 계산해 주는 함수를 완성합니다.
    top = dot(v1, v2)
    size1 = sqrt(dot(v1, v1))
    size2 = sqrt(dot(v2, v2))
    return top / (size1*size2)
    
# doc2vec 모델을 documents 리스트를 이용해 학습하세요.
documents = load_data("text.txt")
d2v_model = Doc2Vec(window=2, vector_size=50) # word2vec 클래스 설명 참고 (01 텍스트 전처리 및 단어 임베딩)
d2v_model.build_vocab(documents) # 수치형 인덱스 할당
# train(학습할 데이터, 문서개수, epochs)
d2v_model.train(documents, total_examples = d2v_model.corpus_count, epochs=5)

# 학습된 모델을 이용해 doc1과 doc2에 들어있는 문서의 임베딩 벡터를 생성하여 각각 변수 vector1과 vector2에 저장하세요.
vector1 = d2v_model.infer_vector(doc1)
vector2 = d2v_model.infer_vector(doc2)

# vector1과 vector2의 코사인 유사도를 변수 sim에 저장하세요.
sim = cal_cosine_sim(vector1, vector2)
# 계산한 코사인 유사도를 확인합니다.
print("sim", sim)

>> 실행 결과
sim 0.041860808

4. N-gram 기반 언어 모델

언어 모델이란

  • 언어 모델이란 주어진 문장텍스트 데이터에서 발생할 확률을 계산하는 모델이다.
  • 자동 문장 생성: 가장 발생 가능성이 높은 문장을 제시해준다.

    image

  • 챗봇: 언어 모델은 챗봇 내 핵심 요소 중 하나이다. 가장 발생 가능한 단어나 문장 추출하는 방식으로 생성을 한다.
  • 언어 모델의 문장 발생 확률 계산 ⇒ 단어가 발생할 조건부 확률의 곱

    image

N-gram 기반 언어 모델

  • N-gram을 사용하여 단어의 조건부 확률을 근사하여 계산한다. 즉, 모든 단어가 아닌 직전의 단어들의 영향만 고려한다.

image

댓글남기기