NIRVANA
[밑바닥부터 시작하는 딥러닝2] 2장 자연어와 단어의 분산 표현 본문
시소러스
- 유의어 사전, 뜻이 같은 단어 혹은 뜻이 비슷한 단어(유의어)가 한 그룹으로 분류되어 있는 것을 의미함
- 자연어 처리에 이용되는 시소러스에는 단어 사의 '상위와 하위' 혹은 '전체와 부분' 등, 더 세세한 관계까지 정의해둔 경우도 존재
- 모든 단어에 대한 유의어 집합을 만든 다음, 단어들의 관계를 그래프로 표현하여 단어 사이의 연결을 정의
- 단어에 대한 동의어와 계층 구조의 관계 정의 가능 → '단어 네트워크' 생성
- '단어 네트워크'를 사용하여 컴퓨터에게 단어 사이의 관계를 가르침
WordNet
- 자연어 처리 분야에서 가장 유명한 시소러스, 워드넷을 사용하면 유의어를 얻거나 '단어 네트워크' 사용이 가능함
- 단어 네트워크를 사용해 단어 사이의 유사도를 구할 수도 있음
문제점
- 시대 변화에 대응하기 어려움
- '크라우드펀딩'와 같은 새로운 단어가 생성되고, 사용되지 않은 단어가 발생하지만 시소러스 방안은 이런 시대 변화에 대응하기 어려움
- 사람을 쓰는 비용은 크다
- 단어의 미묘한 차이를 표현할 수 없다
- 빈티지와 레트로는 의미는 같지만 용법은 다름, 그러나 시소러스에서는 이런 미묘한 차이를 표현하기 어려움
통계 기반 기법
- 말뭉치에서 자동으로, 그리고 효율적으로 핵심을 추출하는 방법
- 말뭉치: 자연어 처리 연구나 애플리케이션을 염두에 두고 수집된 텍스트 데이터, 사람의 지식이 담긴 데이터
파이썬으로 말뭉치 전처리하기
text = 'You say goodbye and I say hello.'
text = text.lower() #문장 내 모든 대문자를 소문자로 변경
text = text.replace('.', ' .')
words = text.split(' ') #공백을 기준으로 나눔
#단어를 ID의 리스트로 사용할 수 있도록 변경
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
print(id_to_word)
print(word_to_id)
#단어 목록을 단어 ID 목록으로 변경
import numpy as np
corpus = [word_to_id[w] for w in words]
corpus = np.array(corpus)
print(corpus)
분산 표현
- 단어의 의미를 정확하게 파악할 수 있는 벡터 표현
- 단어의 분산 표현은 단어를 고정 길이의 밀집벡터로 표현함
- 밀집벡터: 대부분의 원소가 0이 아닌 실수인 벡터를 의미
분포 가설
- 단어의 의미는 주변 단어에 의해 형성됨
- 단어 자체에는 의미가 없고, 그 단어가 사용된 '맥락(context)'이 의미를 형성함
- ex) "I drink beer", "we drink wine" → drink 주변에 음료가 등장함을 알 수 있음
- ex) "I guzzle beer", "we guzzle wine" → "guzzle"과 "drink"가 같은 맥락에서 사용됨을 알 수 있음 + 가까운 의미의 단어임을 알 수 있음
맥락
- 주변에 놓인 단어, 특정 단어를 중심에 둔 '주변 단어' 를 의미
- 윈도우 크기(window size): 맥락의 크기(주변 단어를 몇 개나 포함할지)를 결정
- 윈도우 크기가 2이면 좌우 두 단어씩 맥락에 포함, 윈도우 크기가 1이면 좌우 한 단어씩 맥락에 포함
동시발생 행렬
통계기반 기법
- 어떤 단어에 주목했을 때, 그 주변에 어떤 단어가 몇 번이나 등장하는 지를 세어 집계하는 방법
import sys
sys.path.append('..')
import numpy as np
#from common.util import proccess
text = 'You say goodbye and I say hello.'
corpus, word_to_id, id_to_word = proccess(text)
print(corpus)
#[0 1 2 3 4 1 5 6]
print(id_to_word)
#{0: 'you', 1: 'say', 2: 'goodbye', 3: 'and', 4: 'i', 5: 'hello', 6: '.'}
단어의 개수가 7개 임을 알 수 있음
단어의 맥락에 해당하는 단어의 빈도를 count → 윈도우 크기는 1, 단어 ID가 0인 "you"부터 시작
" You say goodbye and I say hello ."
단어 you의 맥락은 say 하나 뿐이게 됨 → 이를 [0 1 0 0 0 0 0 0] 인 벡터로 표현 가능
다른 단어들 역시 같은 방식으로 벡터 표현 가능
동시발생 행렬 기반 단어 벡터화
#동시발생 행렬 생성
C = np.array([
[0, 1, 0, 0, 0, 0, 0],
[1, 0, 1, 0, 1, 1, 0],
[0, 1, 0, 1, 0, 0, 0],
[0, 0, 1, 0, 1, 0, 0],
[0, 1, 0, 0, 0, 0, 1],
[0, 0, 0, 0, 0, 1, 0]
], dtype=np.int32)
#단어의 벡터 얻기
print(C[0])
#[0 1 0 0 0 0 0]
print(C[4])
#[0 1 0 0 0 0 1]
print(C[word_to_id['goodbye']])
#[0 1 0 1 0 0 0]
동시 발생 행렬 자동으로 얻기
def create_co_matrix(corpus, vocab_size, window_size=1):
corpus_size = len(corpus)
co_matrix = np.zeros((vocab_size, vocab_size), dtype=np.int32) #co_matrix를 0으로 채워진 2차원 배열로 초기화
#반복문을 통해 말뭉치의 모든 단어 각각에 대해 윈도우에 포함된 주변 단어를 세어 나감
for idx, word_id in enumerate(corpus):
for i in range(1, window_size+1):
left_idx = idx - i
right_idx = idx +1
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
벡터 간 유사도
- 단어 벡터의 유사도를 나타낼 때는 코사인 유사도를 자주 사용함
#코사인 유사도 파이썬 함수 표현
def cos_similarity(x, y , eps=1e-8): #0d이 나누어지는 상황을 방지하기 위해 엡실론 값을 더해줌
nx = x / np.sqrt(np.sum(x**2) + eps) #x의 정규화
ny = y / np.sqrt(np.sum(y**2)+ eps) #y의 정규화
return np.dot(nx, ny)
You와 I의 유사도 구하기
import sys
sys.path.append('..')
import numpy as np
#from common.util import proccess, create_co_matrix, cos_similarity
text = 'You say goodbye and I say hello.'
corpus, word_to_id, id_to_word = proccess(text)
vocab_size = len(word_to_id)
C = create_co_matrix(corpus, vocab_size)
c0 = C[word_to_id['you']]
c1 = C[word_to_id['i']]
print(cos_similarity(c0, c1))
유사 단어의 랭킹 표시
- 어떤 단어가 검색어로 주어질 때, 검색어와 비슷한 단어를 유사도 순으로 출력하는 함수 만들기
#어떤 단어가 검색어로 주어질 때, 검색어와 비슷한 단어를 유사도 순으로 출력하는 함수 만들기
def most_similar(query, word_to_id, id_to_word, word_matrix, top=5):
#검색어 꺼내기
if query not in word_to_id:
print('%s를 찾을 수 없습니다.' % query)
return
print('\n[query]' + query)
query_id = word_to_id[query]
query_vec = word_matrix[query_id]
#코사인 유사도 계산
vocab_size = len(id_to_word)
similarity = np.zeros(vocab_size)
for i in range(vocab_size):
similarity[i] = cos_similarity(word_matrix[i], query_vec)
#코사인 유사도를 기준, 내림차순 출력
#argsort: 배열의 원소를 오름차순으로 정렬 후 배열의 인덱스 반환
count = 0
for i in (-1 * similarity).argsort():
if id_to_word[i] == query:
continue
print(' %s: %s' % (id_to_word[i], similarity[i]))
count +=1
if count >= top:
return
import sys
sys.path.append('..')
import numpy as np
#from common.util import proccess, create_co_matrix, cos_similarity
text = 'You say goodbye and I say hello.'
corpus, word_to_id, id_to_word = proccess(text)
vocab_size = len(word_to_id)
C = create_co_matrix(corpus, vocab_size)
most_similar('you', word_to_id, id_to_word, C, top=5)
통계 기반 기법 개선하기
상호정보량
- 발생 횟수는 좋은 특징이 될 수 없음
- 문장에서 'the'와 'car'의 동시발생을 생각할 때, "...the car"라는 문구가 자주 나오게 되므로 두 단어의 동시발생 횟수는 많음 그러나 car는 dirve와 더 연관성이 높음 → 등장 횟수만 보면 the와 car의 관련성이 높게 나옴
- 점별 상호정보량(PMI)을 사용, 문제를 해결
- PMI 값이 높을 수록 관련성이 높다는 뜻
- PMI는 두 단어의 동시 발생 횟수가 0일 경우 log_2 0 = -∞ 이 됨 → 양의 상호정보량(PPMI)를 사용
PPMI(x, y) = max(0, PMI(x, y))
- PMI가 음수일 경우 0 취급
def ppmi(C, verbose=False, eps=1e-8): #vervose: 진행상황 출력 여부 결정 플래그
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 단점
- 말뭉치의 어휘 수 증가에 따라 단어 벡터의 차원 수도 증가함
- (지금까지 기준으로) 벡터의 원소 대부분이 0으로 이루어짐 → 각 원소의 중요도가 낮음
- 대부분이 0으로 이루어진 벡터(희소행렬-희소벡터) 의 경우 노이즈에 약하고 견고하지 못함
- 차원 감소를 통해 문제 해결
차원 감소
- 중요한 정보를 최대한 유지하면서 벡터의 차원을 줄이는 방법
- 희소벡터에서 중요한 축을 찾아내어 더 작은 차원으로 다시 표현하는 것을 의미
- 데이터의 분포를 고려, 중요한 '축'을 찾는 일을 수행
- 1차원 값만으로도 데이터의 본질적인 차이를 구별할 수 있는 적합한 축을 찾아야 함
- 차원 감소의 결과로 원래의 희소벡터는 원소 대부분이 0이 아닌 값으로 구성된 '밀집 벡터'로 변환됨
특이값 분해(Singular Value Decomposition, SVD)
X = USV^t
- 임의의 행렬을 세 행렬의 곱을 분해
- U, V: 직교 행렬, 열벡터가 서로 직교
- 직교 행렬은 어떠한 공간의을 형성할 수 있으므로, 행렬 U를 단어 공간으로 취급할 수 있음
- S: 대각 행렬
- S는 대각행렬 → 대각 성분에는 특잇값이 큰 순서로 나열되어 있음
- 특잇값: "해당 축"의 중요한 요소
- 행렬 S에서 특잇값이 작다면 중요도가 낮다는 뜻이므로, 행렬 U에서 여분의 열벡터를 깎아내어 원래의 행렬을 근사할 수 있음
- 우리의 문제에 적용해본다면, 행렬 X의 각 행에는 해당 단어 ID의 단어 벡터가 저장되어 있으며, 그 단어 벡터가 행렬 U`라는 차원 감소된 벡터로 표현되는 것
SVD에 의한 차원감소
import sys
sys.path.append('..')
import numpy as np
import matplotlib.pyplot as plt
#from common.util import proccess, create_co_matrix, cos_similarity
text = 'You say goodbye and I say hello.'
corpus, word_to_id, id_to_word = proccess(text)
vocab_size = len(word_to_id)
C = create_co_matrix(corpus, vocab_size)
W = ppmi(C)
U, S, V = np.linalg.svd(W)
print(C[0])
print(W[0])
print(U[0])
print(U[0, :2])
'AI' 카테고리의 다른 글
[밑바닥부터 시작하는 딥러닝2] 6장. 게이트가 추가된 RNN (0) | 2024.03.20 |
---|---|
[밑바닥부터 시작하는 딥러닝2] 5장. 순환 신경망(RNN) (0) | 2024.03.18 |
[밑바닥부터 시작하는 딥러닝2] 4장. word2vec 속도 개선 (5) | 2024.03.18 |
[밑바닥부터 시작하는 딥러닝2] 3장 word2vec (2) | 2024.03.17 |
[pandas] 판다스 melt() (0) | 2023.08.09 |