본문 바로가기

책/밑바닥부터 시작하는 딥러닝 2

2장 자연어와 단어의 분산 표현

시소러스

시소러스는 유의어 사전으로, 동의어나 유의어가 한 그룹으로 분류되어 있다. 또한 단어들의 의미에 상하위 관계가 존재한다.

시소러스는 다음과 같은 문제점들이 있다.

  • 시대 변화에 대응하기 어렵다.
  • 사람을 쓰는 비용이 크다
  • 단어의 미묘한 차이를 표현할 수 없다.

통계 기반 기법

말뭉치(corpus)라고 부르는 텍스트 데이터를 이용한다. 통계 기반 기법은 말뭉치로부터 효율적으로 그 핵심을 추출하는 것이다.

말뭉치 전처리

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)

print(corpus)
# [0 1 2 3 4 1 5 6]
print(word_to_id)
# {'you': 0, 'say': 1, 'goodbye': 2, 'and': 3, 'i': 4, 'hello': 5, '.': 6}
print(id_to_word)
# {0: 'you', 1: 'say', 2: 'goodbye', 3: 'and', 4: 'i', 5: 'hello', 6: '.'}

단어의 분산 표현

단어의 의미를 정확하게 파악할 수 있는 벡터 표현이고, 고정 길이의 밀집벡터로 표현한다.

 

분포 가설

단어의 의미는 주변 단어에 의해 형성된다는 아이디어인데, 단어 자체에는 의미가 없고, 그 단어가 사용된 맥락(context)이 의미를 형성한다는 것.

 

동시 발생 행렬

윈도우 크기 1로 맥락에 해당하는 단어의 빈도를 세어보자

이를 표로 정리하면 다음과 같다.

이를 모든 단어에 적용해보면 다음과 같다.

이를 동시발생 행렬이라 한다. 행렬의 크기가 (vocab_size, vocab_size)인 것이 특징

 

함수로 구현해보자

def create_co_matrix(corpus, vocab_size, window_size=1):
    '''동시발생 행렬 생성

    :param corpus: 말뭉치(단어 ID 목록)
    :param vocab_size: 어휘 수
    :param window_size: 윈도우 크기(윈도우 크기가 1이면 타깃 단어 좌우 한 단어씩이 맥락에 포함)
    :return: 동시발생 행렬
    '''
    corpus_size = len(corpus)
    co_matrix = np.zeros((vocab_size, vocab_size), dtype=np.int32)

    for idx, word_id in enumerate(corpus):
        for i in range(1, window_size + 1):
            left_idx = idx - i
            right_idx = idx + i

            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

벡터 간 유사도

벡터 사이의 유사도를 측정하는 방법은 다양하지만 여기서는 코사인 유사도를 사용한다.

- 분모는 x와 y의 L2-norm의 곱

- 분자는 벡터의 내적

def cos_similarity(x, y, eps=1e-8):
    '''코사인 유사도 산출

    :param x: 벡터
    :param y: 벡터
    :param eps: '0으로 나누기'를 방지하기 위한 작은 값
    :return:
    '''
    nx = x / (np.sqrt(np.sum(x ** 2)) + eps)
    ny = y / (np.sqrt(np.sum(y ** 2)) + eps)
    return np.dot(nx, ny)

이를 바탕으로 you와 다른 단어들의 유사도를 구해보면 다음과 같다.

[query] you
 goodbye: 0.7071067691154799
 i: 0.7071067691154799
 hello: 0.7071067691154799
 say: 0.0
 and: 0.0

말뭉치 크기가 너무 작아서 유사도 결과를 신뢰하기 어렵다.

 

통계 기반 기법 개선하기

상호정보량

사실 발생 횟수라는 것은 그리 좋은 특징이 아니다. 그 이유를 알아보자.

예를 들어 말뭉치에서 "the"와 "car"의 동시발생을 생각해보자. 분명 "...the car..."라는 문구가 자주 보여 두 단어의 동시 발생 횟수는 아주 많을 것이다. 그렇지만 car는 the보다는 drive와 더 관련이 깊을 것이다. 그리고 동시 발생 행렬에서는 car가 drive보다 고빈도인 the와 더 관련이 깊다고 나올 것이다.

 

이러한 문제를 해결하기 위해 점별 상호정보량(Pointwise Mutual Information)이라는 척도를 사용한다.

  • $P(x)$ : x가 일어날 확률
  • $P(x, y)$ : x와 y가 동시에 일어날 확률

이 수식은 다음과 같이 정리될 수 있다.

  • $C(x)$ : x가 발생한 횟수
  • $N$ : vocab size

말뭉치에서 the, car, drive가 각 1000, 20, 10번 발생했다고 가정하고, the와 car의 동시발생 횟수는 10, car와 drive의 동시발생 횟수는 5라고 가정해보자.

PMI에서는 car가 the보다 drive와 관련이 더 깊다고 나온다. 왜냐하면 the가 고빈도 단어라서 PMI 점수가 낮아진 것이다.

PMI의 경우 log를 이용하기 때문에 값이 음의 무한대에 가까워질 수 있어 다음과 같이 양의 상호정보량(PPMI)를 사용한다.

def ppmi(C, verbose=False, eps = 1e-8):
    '''PPMI(점별 상호정보량) 생성

    :param C: 동시발생 행렬
    :param verbose: 진행 상황을 출력할지 여부
    :return:
    '''
    M = np.zeros_like(C, dtype=np.float32)
    N = np.sum(C)
    S = np.sum(C, axis=0)
    total = C.shape[0] * C.shape[1]
    cnt = 0

    for i in range(C.shape[0]):
        for j in range(C.shape[1]):
            pmi = np.log2(C[i, j] * N / (S[j]*S[i]) + eps)
            M[i, j] = max(0, pmi)

            if verbose:
                cnt += 1
                if cnt % (total//100 + 1) == 0:
                    print('%.1f%% 완료' % (100*cnt/total))
    return M

차원 축소

PPMI의 경우 행렬의 크기가 (vocab size, vocab size)라서 매우 클 수 있다. 또한 희소 행렬이다 대부분의 값이 0이다. 차원 축소에는 여러 방법이 있지만 그 중 특잇값 분해를 사용해 PPMI 행렬을 근사해보자.

 

특이값분해(SVD)는 임의의 행렬을 세 행렬의 곱으로 분해하며, 수식으로는 다음과 같다.

U, V는 직교 행렬이고, S는 대각 행렬이다.

직교 행렬은 역행렬과 전치행렬이 같은 경우 직교행렬이라 한다. 대각행렬은 대각성분 외에는 모두 0인 행렬이다.

S의 대각 성분에는 해당 축의 중요도로 간주할 수 있는 특이값이 큰 순서대로 나열되어 있다. 따라서 U는 왼쪽부터 중요한 열벡터가 있을 것이므로 여분의 열벡터를 깎아내어 행렬을 근사할 수 있다.

즉 단어 벡터는 차원이 축소된 $U'$으로 표현될 수 있다.

from sklearn.utils,extmath import randomized_svd

U, S, V = randomized_svd(W, n_components=wordvec_size, n_iter=5)

sklearn의 Truncated SVD기반의 randomized_svd 메서드를 사용할 수 있고, W는 PPMI 행렬이다.

' > 밑바닥부터 시작하는 딥러닝 2' 카테고리의 다른 글

6장 게이트가 추가된 RNN (1)  (0) 2023.10.27
5장 RNN (2)  (0) 2023.09.27
5장 RNN (1)  (0) 2023.09.26
4장 word2vec 속도 개선  (0) 2023.09.26
3장 word2vec  (0) 2023.09.25