본문 바로가기

Data Science/밑바닥부터시작하는딥러닝(2)

[밑시딥] 오직! Numpy로 단어의 분산 표현하기

반응형

🔊 해당 포스팅은 밑바닥부터 시작하는 딥러닝 2권의 교재 내용을 기반으로 자연어처리 딥러닝 신경망을 Tensorflow, Pytorch와 같은 딥러닝 프레임워크를 사용하지 않고 순수한 Numpy로 구현하면서 자연어 처리의 기초를 탄탄히 하고자 하는 목적 하에 게시되는 포스팅입니다. 내용은 주로 필자가 중요하다고 생각되는 내용 위주로 작성되었음을 알려드립니다.

 

밑바닥부터 시작하는 딥러닝(2)


이번 포스팅에서는 자연어 처리의 가장 기본인 단어의 분산 표현에 대해 알아보고 이를 넘파이로 구현하는 방법에 대해 알아보자. 

1. 단어의 의미를 표현하는 방법

최근에는 대부분 딥러닝으로 단어의 의미를 표현하지만 딥러닝이 등장하기 이전에도 단어의 의미를 표현하는 방법들이 존재했다. 단어의 의미를 표현하는 방법에는 크게 3가지 방법이 존재한다. 시소러스(Sysorus)를 활용하는 방법, 통계 기반의 기법, 그리고 요즘 가장 핫한 신경망을 활용한 추론 기반 기법(word2vec)이다. 이번 포스팅에서는 시소러스, 통계 기반의 기법에 대해 알아본다.

2. 시소러스란 무엇인가?

단어의 의미를 나타내기 위한 방법 중 하나는 '사람이 직접' 단어의 의미를 미리 정의해놓는 방법이 있다. 우리가 모르는 한국어 단어가 나오면 국어사전을 찾아보는 것처럼 사람이 직접 '국어사전'을 통해 단어의 정의를 미리 해놓는 것처럼 시소러스도 어떻게 보면 하나의 '사전'으로 표현할 수 있겠다. 시소러스에는 특정한 단어에 대하여 의미가 서로 다른 동의어 그룹이 정의되어 있다.

 

예를 들어, '차'라는 단어의 동의어로는 SUV, 세단, 트럭, 등이 있을 수 있다. 그리고 의미가 약간 다른 동의어로 '기차' 가 있을 수 있다. 이렇게 서로 의미가 다른 단어들끼리는 분리해놓되, 그 분리해 놓은 단어들 간의 비슷한 동의어들끼리는 그룹지어 놓은 것이 시소러스이다. 물론 사람이 직접 수기로 정의해놓은 것이다.

 

파이썬으로 시소러스를 활용해보는 방법으로는 NLTK 모듈이 있다. NLTK 모듈에서 Wordnet 라이브러리를 임포트해 여러가지 단어(물론 영어만 가능하다)들에 대한 시소러스를 탐색해볼 수 있다. 해당 포스팅에서는 구체적으로 소개하지 않지만 책 부록 B에서 다루고 있다. 필자도 개인적으로 호기심에 탐색해보았다.

 

하지만 시소러스는 치명적인 문제점이 몇 가지 존재한다. 우선 시대가 변화함에 따라 새롭게 생겨나는 단어나 기존의 단어에 또 다른 의미가 부여되게 된다면 그런 상황이 발생할 때마다 시소러스라는 일종의 '사전'을 계속 갱신해주어야 한다. 사람이 직접! 그렇게 되면 자연스럽게 "아니 이걸 사람이 어떻게 다해?"라고 생각하면서 인력 리소스가 매우 소비되는 문제점이 나타난다. 또한 단어 간의 미묘한 차이를 표현할 수가 없다. 게다가 이것은 사람이라는 특성이 갖고 있는 주관이라는 것 때문에 문제가 발생한다. A라는 단어가 누구에게는 중립적인 느낌일 수 있지만 누군가에게는 살짝 부정적인 느낌일 수 있기 때문이다. 그렇기에 시소러스를 작성하는 어떤 누군가도 하나의 주관을 갖고 있기 때문에 단어간의 미묘한 차이를 객관화하기가 힘들다.

3. 통계 기반 기법

위 시소러스를 좀 더 정량적으로 극복하는 방법은 통계 기반 기법이다. 통계 기반 방법을 사용하면서 말뭉치(corpus)를 사용하게 된다. 말뭉치는 대량의 텍스트 데이터를 의미한다. 예를 들어, 구글링에 검색어를 치면 나오는 모든 텍스트들, 네이버 뉴스의 모든 기사 텍스트 등등.. 이런 것들이 일종의 말뭉치라고 할 수 있다. 물론 말뭉치가 단순한 텍스트라고 볼 수 있지만, 어쨌거나 사람이 직접 작성한 것이기 떄문에 사람의 지식이 들어간 텍스트 데이터이다. 따라서 통계 기반 기법의 목표는 이러한 사람의 지식이 담겨져 있는 대량의 텍스트 데이터인 말뭉치로부터 자동으로, 그리고 효율적으로 핵심을 추출하는 것이다.

 

그렇다면 간단한 예시로, 'You say goodbye and I say Hello.' 라는 말뭉치가 있다고 가정하고, 이를 간단하게 단어 단위로 분할시켜보자.

 

def preprocess(text):
    text = text.lower()
    text = text.replace('.', ' .')
    words = text.split(' ')
    
    word_to_id = {}
    id_to_word = {}
    for word in words:
        if word not in word_to_id:
            new_id = len(word_to_id)
            word_to_id[word] = new_id
            id_to_word[new_id] = word
    
    corpus = np.array([word_to_id[w] for w in words])
    return corpus, word_to_id, id_to_word
    
text = 'You say goodbye and I say Hello.'
corpus, word_to_id, id_to_word = preprocess(text)

 

위 코드에서 word_to_id는 text라는 말뭉치에서 unique한 단어들에 고유한 ID를 붙인 딕셔너리이다. id_to_word는 word_to_id의 key, value 값을 서로 바꾼 딕셔너리이다. 

3-1. 단어의 분산 표현

위에서 말뭉치를 전처리하는 방법을 알아보았다. 그러면 이제 이 전처리한 말뭉치를 활용해 말뭉치를 구성하는 단어들의 분산을 표현해보자. 그런데 단어에 '분산'이라는 것이 어떻게 존재한다는 걸까? 이를 이해하기 위해서는 책에서 '색깔'을 비유로 든다. 색깔은 크게 2가지로 표현한다고 할 수 있는데, 하나는 '파란색', '빨간색', '코발트 블루'와 같은 텍스트 자체이다. 두 번째는 RGB 형태로 정량화된 수치로 표현할 수 있다. 만약 어떤 어려운(?) 색깔인 '비색' 이라는 것이 주어졌다고 가정하자. 근데 텍스트 자체만 보면 이게 빨간색 계열인지, 파란색 계열인지 모른다. 하지만 이 '비색'을 RGB 값으로 표현한다면, 우리는 빨간색, 파란색의 RGB 값과 비교하면서 "아, 비색의 RGB 값이 이러이러한데, 이것은 빨간색 RGB와 비슷한데?" 라고 하면서 '비색' 색깔이 어떤 색깔인지 추측할 수 있을 것이다.

 

단어도 똑같다. 단어도 일종의 숫자로 표현하면 의미를 표현하기 수월할 수 있다. 그 때 '숫자'란, 바로 벡터로 표현하는 것이다. 즉, 단어의 의미를 보다 정확하게 컴퓨터에 전달하기 위해서 단어를 벡터로 표현할 수 있다. 이렇게 단어를 벡터로 표현한다는 것을 '단어의 분산 표현' 이라고 한다.

 

추후에 배우겠지만 단어의 분산 표현은 구체적으로 밀집 벡터(Dense Vector)로 표현한다. 밀집 벡터라 함은 원소값이 0이 아닌 실수인 벡터를 의미한다. 즉, 무수히 많은 0값으로 채워진 Sparse한 벡터가 아닌 밀집된 벡터이다. 이를 Embedding이라고 하면 이해가 잘 되는 분들도 있을 것이다.

3-2. 분포 가설

분포 가설은 "단어의 의미는 주변 단어에 의해 형성된다"라는 것이다. 즉, 단어 자체에는 의미가 없고 그 단어가 위치한 주변의 단어들에 의해 의미가 형성된다는 것이다. 바로 주변 맥락을 이용하는 것! 

3-3. 동시발생 행렬(Co-Occurence Matrix)

3-2의 분포가설 아이디에서 출발한 것이 동시발생 행렬이다. 특정 단어의 의미를 파악하기 위해 특정 단어의 주변 단어가 몇 번 발생했는지를 카운트한 후 집계해 행렬로 만든 것이 동시발생 행렬이다.

 

동시발생 행렬의 예시

 

위 그림처럼 생긴 것이 동시발생 행렬이다. 위 그림에서 첫 번째 행인 'I'는 주변에 like가 2번 등장하고 enjoy라는 단어가 1번 등장한다는 것이다. 물론 여기서 '주변'이란 것이 어느정도의 거리를 말하는 것인지에 대해 의문을 가질 수 있다. 이는 문제에 따라 개별로 사람이 직접 설정하며 일종의 하이퍼파라미터가 된다. 용어로는 widnow_size(윈도우 사이즈)라고 한다. 

 

그렇다면 동시발생 행렬을 넘파이로 구현한 소스코드를 살펴보자. 아래 소스코드의 preprocess() 함수는 위 목차에서 살펴본 말뭉치 전처리 함수이다.

 

# 동시발생 행렬 만들기
import numpy as np


def create_co_matrix(corpus, vocab_size, window_size=1):
    corpus_size = len(corpus)
    # 동시발생 행렬 초기화 -> 사이즈는 말뭉치의 unique한 단어들 개수로!
    co_matrix = np.zeros((vocab_size, vocab_size), dtype=np.int32)
    
    for idx, word_id in enumerate(corpus):
        for size in range(1, window_size+1):
            left_idx = idx - size
            right_idx = idx + size
            
            if left_idx >= 0:
                left_word_id = corpus[left_idx]
                co_matrix[word_id, left_word_id] += 1
            
            if right_idx < corpus_size:
                right_word_id = corpus[right_idx]
                co_matrix[word_id, right_word_id] += 1
    
    return co_matrix

text = 'You say goodbye and I say Hello.'
corpus, word_to_id, _ = preprocess(text)
co_matrix = create_co_matrix(corpus=corpus, vocab_size=len(word_to_id), window_size=1)
co_matrix

3-4. 벡터 간 유사도

우리는 위에서 동시발생 행렬을 구해보았다. 이 때 동시발생 행렬의 행 벡터가 바로 그 행 번호가 가리키는 단어의 벡터를 의미한다. 그렇게 되면 우리는 단어의 벡터를 구했다는 것이다! 알다시피 벡터는 크기와 방향을 가지는 특성이 있다. 여기서 중요한 것은 바로 '방향' 이다. 그러면 2개 이상의 벡터들의 '방향'을 비교하면서 단어 간의 유사성을 비교할 수 있지 않을까? 바로 이런 아이디어에서 나온 것이 코사인 유사도이다.(물론 벡터간의 거리를 측정하는 여러가지 기법들은 존재한다. 이에 대해알고 싶다면 예전에 관련 포스팅을 게시한 적이 있으니 참고하자)

 

코사인 유사도 예시

 

코사인 유사도의 수식은 아래와 같다.

$$similarity(x,y) = \frac{x \cdot y}{||x|| \ ||y||} = \frac{x_1y_1 + \cdots + x_ny_n}{\sqrt{x_1^2 + \cdots + x_n^2} \sqrt{y_1^2 + \cdots + y_n^2}}$$

 

코사인 유사도의 핵심은 분모의 L2 노름으로 $x$, $y$ 벡터의 크기를 정규화해준 후 두 벡터의 내적($x \cdot y$)하는 것이다. 내적은 두 벡터의 각도를 계산하게 된다. 따라서 벡터의 크기를 정규화해줌으로써 두 벡터간의 크기 차이를 줄여주고 방향만을 비교해 두 벡터의 유사도를 계산한다. 만약 두 벡터의 크기가 같은 방향을 가리킨다면 코사인 유사도 값이 1이 되며, 정반대 방향을 가리키면 -1로 나오게 된다.

 

그렇다면 코사인 유사도를 넘파이로 구현하는 코드는 아래와 같다.

 

def cos_similarity(x: np.array, y: np.array, eps=1e-8):
    nx = x / np.sqrt(np.sum(x ** 2) + eps)
    ny = y / np.sqrt(np.sum(y ** 2) + eps)
    cos = np.matmul(nx, ny)
    
    return cos
    
c0 = co_matrix[word_to_id['you']]
c1 = co_matrix[word_to_id['i']]

cos = cos_similarity(c0, c1)
cos

 

주의할 점은 0으로 나누어지는 걸 방지하기 위해서 eps이라는 매우 작은 수를 더해주는 것이다. 이렇게 작은 수를 더해주는 테크닉은 기존 1권을 공부하면서도 많이 보았을 것이다.

 

이렇게 코사인 유사도를 구하는 방법까지 알아보았으니, 말뭉치에 존재하는  특정 단어를 입력시켰을 때, 다른 단어 벡터들과의 코사인 유사도를 계산하고 유사도 값이 높은 상위의 단어들을 출력해보자! 아래 소스코드에 사용된 preprocess(), create_co_matrix() 함수는 위와 동일한 함수이다.

 

def most_similar(query, word_to_id: dict, id_to_word: dict, co_matrix, top=5):
    """ 쿼리한 단어와 가장 유사도가 큰 상위 단어들 출력하는 함수
    
    Args:
        query: 비교 대상 단어
        word_to_id: 단어를 key, 단어의 ID를 value로 하는 딕셔너리
        id_to_word: 단어의 ID를 key, 단어를 value로 하는 딕셔너리
        co_matrix: 단어의 동시발생 행렬
        top: 상위 몇 개 단어 출력할 것인지
    
    """
    if query not in word_to_id:
        print(f'{query} 라는 단어는 Vocabulary에 존재하지 않습니다')
        return
    
    print('\n[query]' + query)
    query_id = word_to_id[query]
    query_vec = co_matrix[query_id]
    
    # 동시 발생 행렬의 벡터를 활용해 단어들 간 코사인 유사도 계산
    vocab_size = len(word_to_id)
    similarity = np.zeros(vocab_size)
    for i in range(vocab_size):
        similarity[i] = cos_similarity(query_vec, co_matrix[i])
    
    # 코사인 유사도 기준으로 내림차순 정렬
    count = 0
    for j in (-1 * similarity).argsort():
        if id_to_word[j] == query:    # 쿼리 자기 자신 단어와 비교는 pass
            continue
        print('%s 와의 유사도 : %s' % (id_to_word[j], similarity[j]))
        
        count += 1
        if count >= top:
            return
        
text = 'You say goodbye and I say Hello.'
corpus, word_to_id, id_to_word = preprocess(text)
co_matrix = create_co_matrix(corpus, len(word_to_id), 1)
query = 'you'

most_similar(query, word_to_id, id_to_word, co_matrix, 5)

3-5. 상호정보량(PMI, Pointwise Mutual Information)

방금까지 알아본 동시발생 행렬에는 한계점이 존재한다. 단순히 단어의 발생 횟수에만 기반하기 때문에 특별한 의미가 없지만 매우 자주 발생하는 단어들이 특정 단어와 연관이 깊다고 하는 문제가 발생한다. 예를 들어, 'car' 라는 명사앞에 자주 붙는 'the' 같은 관사들은 빈도수에만 기반하는 동시발생 행렬에서 살펴보면 두 단어가 매우 관련이 깊다고 할 것이다. 하지만 실질적으로 the, car 간의 관련성은 아무것도 없다. 이렇게 의미가 없지만 매우 자주 사용되는 단어(the, is, she 등)와 같은 것들을 불용어(Stopword)라고도 한다. 이 불용어들을 처리하기 위해서 점별 상호정보량(PMI)이라는 척도를 사용한다.

 

PMI는 아래와 같은 수식을 지닌다. 아래 수식에서 $x, y$는 특정 단어를 의미한다.

$$PMI(x, y) = \log_{2} \frac{P(x, y)}{P(x)P(y)}$$

 

위 수식에서 $P(x)$는 $x$라는 단어가 발생할 확률, $P(y)$는 $y$라는 단어가 발생할 확률을 의미한다. 그리고 $P(x, y)$는 $x, y$ 단어가 동시에 발생할 확률을 의미한다. 이를 통계 용어로 Joint(결합) 확률이라고도 한다. 어쨌거나 위 수식의 PMI 값이 높을수록 두 단어, 여기서는 $x, y$ 단어가 서로 관련성이 깊다는 것을 의미한다.

 

위 PMI 수식은 현재 확률로 되어 있다. 그렇다면 단어 발생 횟수를 가지고 있는 동시발생 행렬을 활용해서 수식을 재정의하면 아래와 같아진다.

$$PMI(x, y) = \log_{2} \frac{P(x, y)}{P(x)P(y)} = \log_{2} \frac{\frac{C(x,y)}{N}}{\frac{C(x)}{N} \frac{C(y)}{N}} = \log_{2} \frac{C(x, y) \cdot N}{C(x)C(y)}$$

 

위 수식에서 $N$은 말뭉치의 전체 단어 개수를 의미한다. 우선, $C(x)$는 $x$라는 단어가 말뭉치에서 발생한 횟수이다. 그렇다면 $C(x, y)$는 말뭉치에서 $x, y$라는 단어가 동시에 발생한 횟수를 의미한다. 

 

그런데 위 PMI는 약간의 문제점이 존재한다. 만약 $x, y$ 두 단어가 동시에 발생하는 횟수가 0이라면 $\log_{2}0 = -\infty$가 발생하게 된다는 것이다. 따라서 위 수식을 약간 개선한 양의 상호정보량(PPMI, Positive PMI) 수식을 사용한다. PPMI 수식은 아래와 같다. 단순히 음수일 때는 무조건 0으로 반환해주면 된다.

$$PPMI(x, y) = \max(0, PMI(x, y))$$

 

이제 PPMI를 넘파이로 구현한 소스코드를 하단에서 살펴보자.

 

def ppmi(C: np.array, verbose=False, eps=1e-8):
    """ 동시발생 행렬 기반 양의 상호정보량(PPMI) 계산
    ;p[]
    Args:
        C: 동시발생 행렬
    
    """
    M = np.zeros_like(C, dtype=np.float32)
    N = np.sum(C)            # 말뭉치 내의 전체 단어 개수
    S = np.sum(C, axis=0)    # 말뭉치의 unique한 단어들 발생 횟수(이 때, 단어란, 동시발생 행렬 만들 때 window_size에 따라 어절일 수도 있음)
    total = C.shape[0] * C.shape[1]
    cnt = 0
    
    # 말뭉치 단어 하나씩 PPMI 계산
    for i in range(C.shape[0]):
        for j in range(C.shape[1]):
            pmi = np.log2(C[i, j] * N / (S[i] * S[j]) + eps)
            M[i, j] = max(0, pmi)
            
            if verbose:
                cnt += 1
                if cnt % (total // 100) == 0:
                    print(f'{100*cnt/total :.1f}% 완료')
    return M

 

위 소스코드에서 eps이라는 아주 작은 수를 추가해 0으로 나누어지는 문제를 예방한 점도 참고하자. 그렇다면 위 PPMI 행렬을 구하는 함수를 사용해서 'You say goodbye and I say Hello.' 라는 말뭉치에 대한 PPMI 행렬을 구한 후 동시행렬과 비교해보자.

 

text = 'You say goodbye and I say Hello.'
corpus, word_to_id, id_to_word = preprocess(text)
vocab_size = len(word_to_id)
C = create_co_matrix(corpus, vocab_size, window_size=1)
W = ppmi(C)

np.set_printoptions(precision=3)
print('동시발생행렬')
print(C)
print('PPMI 행렬')
print(W)

 

동시발생 행렬 VS PPMI 행렬

 

위 출력 결과를 비교해보면 행렬 값들이 약간 달라졌다는 것을 관찰할 수 있다. 결국, 단어가 단독으로 발생하는 횟수를 고려했다는 것이다. 어떤 단어가 단독으로 매우 많이 발생했다면 해당 단어의 중요도는 낮추는 것이다.

 

하지만 이 PPMI 행렬도 문제점이 존재한다. 물론 현재 테스트하고 있는 말뭉치 단어가 하나의 문장 밖에 되지 않기 때문에 행렬의 차원의 수가 적지만 말뭉치 개수가 많아짐에 따라 PPMI 행렬 차원 수도 기하급수적으로 증가하게 된다. 또한 차원의 수도 증가하는 것뿐만 아니라 큰 차원의 행렬 안에 0으로 되어있는 값들이 매우 많다는 것이다. 0인 값들은 곧 중요하지 않음을 의미한다. 이렇게 0으로 많이 이루어진 Sparse(희소한) 행렬은 노이즈에 약하고 견고하지 못하다는 약점(Robust 하지 못하다는 약점)을 갖는다. 따라서 이러한 문제점은 PPMI 행렬의 차원을 감소시키는 방법으로 해결할 수 있다.

3-6. 차원 감소

차원 감소를 수행하는 목적은 '데이터의 중요한 정보'들은 최대한 남기면서 기존 데이터의 차원을 축소시키는 것이다. 차원 축소는 예전 포스팅에서 게시한 적이 있어 해당 포스팅을 참고하는 것도 좋을 듯 하다. 여기서는 간단하게만 설명하고 넘어가려 한다. 

 

차원을 감소시킨다는 것은 기존 데이터를 가장 잘 설명할 수 있는 새로운 축을 찾는다는 것을 의미한다. 여기서 '데이터를 가장 잘 설명하는' 것의 기준은 만약 어떠한 새로운 축을 기준으로 했을 때, 그 축을 기준으로 데이터가 넓게(분산이 크게) 분포한다면 그것이 데이터를 가장 잘 설명하는 축이다. 차원 감소를 수행하는 방법이 여러가지가 있지만 책에서는 SVD(특잇값 분해) 기법을 소개한다. 

 

SVD는 임의의 직각행렬을 세 행렬의 곱으로 분해한다. SVD 분해의 수식은 아래와 같다.

$$ X = USV^T$$

 

즉, 기존 행렬 $X$는 $U,S,V$라는 세 행렬의 곱으로 분해된다는 것이다. 이 때, $U$와 $V$는 직교행렬이며, $S$는 대각행렬이다. 이 대각행렬의 대각선의 값에는 특잇값이 큰 순서로 나열되어 있는데, 여기서 '특잇값'이라는 것은 '축의 중요도'를 의미한다. 가장 쉽게 SVD를 이해하기 위해 '직교' 개념같은 것은 잠시 접어두고 도식화를 통해 이해해보자.

 

SVD를 이해해보자

 

위와 같이 $S$라는 대각행렬에 특이값이 큰 순으로 되어 있다. SVD는 여기서 $S$ 행렬의 특이값이 낮은 부분을 깎아내어 차원을 축소한다고 할 수 있다. 아래처럼 말이다.

 

특이값이 낮은 부분을 깎아내자

 

따라서 SVD를 수행하게 되면 원본 행렬 $X$에서 차원이 축소된 $U'$ 행렬이 되게 된다. 

 

그러면 SVD를 파이썬으로 구현해보자. 예전에 SVD를 파이썬으로 구현해보는 것을 게시한 포스팅을 참고해도 된다. 책에서도 간단히 소개하므로 소개하겠다. 먼저 Numpy의 선형대수 라이브러리에서 기능을 제공한다. 그런데 이 라이브러리는 기본적으로 Full SVD를 반환하게 된다. 따라서 SVD를 수행해서 반환된 Full SVD 즉, 원본 행렬 $X$와 동일한 형상인 $U'$을 반환받은 후, 사용자가 원하는 차원의 수만큼 행렬을 슬라이싱해서 사용하면 된다. 소스코드로 이해하면 아래와 같다.

 

text = 'You say goodbye and I say Hello.'
corpus, word_to_id, id_to_word = preprocess(text)
vocab_size = len(word_to_id)
C = create_co_matrix(corpus, vocab_size, window_size=1)
W = ppmi(C)

# Full SVD 수행
U, S, V = np.linalg.svd(W)
print('W shape:', W.shape)
print(W)
print()
print('U shape:', U.shape)
print(U)

# 치원 축소한 SVD 얻으려면 원하는 차원 수만큼 인덱싱
dim_reduce_W = U[:, :2] # 2차원까지만 가져오기
print(dim_reduce_W.shape)

 

참고로 SVD는 행렬의 크기가 $N$이라고 하면 시간복잡도 $O(N^3)$이 소요된다. 상당히 많은 시간이 소요되는데, 그래서 보통은 중요한 정보만을 살리고 나머지는 다 버려버리는 Truncated SVD를 사용한다. 물론 Truncated SVD는 중요하지 않은 부분들을 다 날려버리기 때문에 Full SVD와는 달리 원본 행렬로 복구는 할 수 없지만 차원 축소 속도가 Full SVD보다 훨씬 빠르므로 자주 사용된다. Truncated SVD는 scipy, sklearn에서 제공한다. 사용방법은 여기를 참고하자. 기타 SVD 종류에 대해서는 SVD 이론 포스팅을 참고하자.

 

책에서는 마지막 실습으로 그동안 구현했던 함수들을 가지고 PTB 벤치마크 데이터셋으로 특정 단어를 입력시켰을 때, 그 단어와 관련성이 깊은 상위의 단어를 출력하는 실습으로 마무리한다. 해당 실습 소스코드는 여기를 참고하자. 참고로 PTB 말뭉치 개수가 많은 편이기 때문에 동시발생 행렬을 계산하는 데 다소 오래걸린다는 것은 알아두자.


이렇게 챕터 두번째가 끝이났다. 지금까지 단어의 의미를 표현해 컴퓨터에게 이해시키는 두 가지 방법에 대해 알아보았다. 단어의 의미를 컴퓨터에게 전달하기 위해서는 단어의 분산을 표현해야 했다. 단어의 분산을 표현하는 방법에는 세가지가 존재한다고 했다. 첫 번째는 시소러스 방법이었고 두 번째는 통계기반 기법이었다. 통계기법을 통해 단어의 분산을 표현하는 과정을 정리하면 아래와 같다.

 

  1. 말뭉치 내에서 단어의 동시발생 행렬을 계산
  2. 동시발생 행렬을 기반으로 PPMI 행렬을 계산. 이 때 Sparse한 행렬이 나옴
  3. Sparse한 행렬을 Dense한 행렬로 바꾸어주기 위해 SVD와 같은 차원 축소 수행

이제 다음 챕터부터는 단어의 분산을 표현하는 또 다른 방법이며 요새 가장 핫한 신경망을 활용한 추론 기법(word2vec)에 대해 살펴보기로 하자.

반응형