모두야

CH2) 자연어와 단어의 분산 표현 - (1) 본문

밑.시.딥/2권

CH2) 자연어와 단어의 분산 표현 - (1)

미미밍2 2021. 9. 15. 20:24
728x90
반응형

자연어 처리 -> 컴퓨터가 우리가 하는 말을 알아듣게(이해하게) 만드는 것이다.

  • 고전적인 기법 (딥러닝 이전)
  • 딥러닝 기법 (신경망)
  • 파이썬으로 텍스트를 다루는 연습 - 텍스트를 단어로 분할하는 처리, 단어를 단어 ID로 변환하는 처리

앞으로 텍스트 처리를 위한 사전 준비!!!


자연어 (Natural Language)

: 한국어, 영어 등 평소에 우리가 쓰는 말

 

자연어 처리(Natural Language Processing)

: 자연어를 처리하는 분야

: 우리의 말을 컴퓨터에게 이해 시키기 위한 기술

: 사람의 말을 컴퓨터가 이해하도록 만들어서, 컴퓨터가 우리에게 도움이 되는 일을 수행하도록 하는 것

 

컴퓨터 이해 (프로그래밍 언어 : 딱딱한 언어)

평소 사용하는 말(자연어 : 부드러운 언어- 똑같은 의미의 문장을 여러 형태로 표현하고, 문장의 뜻이 애매할 땐 의미나 형태가 유연하게 바뀌는 것)


자연어 처리 기술 예시

번역, 질의응답 시스템, IME(입력기 전환), 문장 자동 요약, 감정 분석 등

 

말(문자로 구성)

말의 의미(단어로 구성) -> 단어는 의미의 최소 단위

=> 컴퓨터에게 "단어의 의미"를 이해시키는 것이 중요하다.

1. 시소러스 기법 : 유의어 사전
2. 통계 기반 기법
3. 추론 기반 기법(word2vec)

사람이 직접 단어의 의미를 정의하는 방식 -> [표준국어대사전] 처럼 단어를 정의하자.

1. 시소러스 기법

: 유의어 사전 

뜻이 같은 단어(동의어) / 뜻이 비슷한 단어(유의어)


> WordNet

자연어 처리 분야에서 가장 유명한 시소러스이다.

WordNet을 사용하면 유의어를 얻거나 '단어 네트워크'를 이용할 수 있다. 단어 사이의 유사도를 구할 수 있다.

부록B 참고

문제점

- 시대 변화에 대응하기 어렵다.

  : 새로운 단어가 생기고, 사라짐을 사람이 수작업으로 끊임없이 갱신하기 어렵다.

 

- 사람 쓰는 비용이 크다.

  : 시소러스는 만드는 인적 비용이 엄청나다. 단어 사이의 관계를 정의해주어야 한다.

 

- 단어의 미묘한 차이를 알 수 없다.

  : 비슷한 단어라고 미묘한 차이가 있다. (빈티지 & 레트로)


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])# 단어 ID, 넘파이 배열로 변환
    
    return corpus, word_to_id, id_to_word

단어의 분산 표현 (distributional representation)

색을 RGB 값의 벡터로 표현하듯, 단어도 벡터로 표현하면 단어의 의미를 더 정확하게 파악할 수 있다.

 

분포 가설 (distrivutional hypothesis)

단어의 의미는 주변 단어에 의해 형성된다.

= 단어가 사용된 맥락(context)가 의미를 형성한다.

I drink beer.
We drink wine.
I guzzle beer.

drink 뒤에는 음료.

guzzle(폭음하다)= drink

 

맥락 : 단어 주변에 놓인 단어

윈도우 크기 (window size) : 주변 단어를 몇 개나 포함 할 것인지


통계 기반 기법 (statistical based)

어떤 단어를 주목했을 때, 그 주변에 어떤 단어가 몇 번이나 등장하는지 세어 집계하는 방법

  you  say goodbye and  i hello .
you 0 1 0 0 0 0 0
say 1 0 1 0 1 1 0
goodbye 0 1 0 1 0 0 0
and 0 0 1 0 1 0 0
i 0 1 0 1 0 0 0
hello 0 1 0 0 0 0 1
. 0 0 0 0 0 1 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)  # 0으로 채워진 2차원 배열로 초기화
    
    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 # 행렬에 1추가
                
            if right_idx < corpus_size: #오른쪽 윈도우
                right_word_id = corpus[right_idx]
                co_matrix[word_id,right_word_id] += 1 # 행렬에 1추가
                
    return co_matrix #행렬

벡터 사이의 유사도

  • 벡터의 내적
  • 유클리드 거리
  • 코사인 유사도 (cosine similarity) : 두 벡터가 가리키는 방향이 얼마나 비슷한가
                                              : 방향이 같으면 1, 완전히 반대면 -1
    = 벡터의 내적 / 벡터의 노름 
    = 노름 : 벡터의 크기를 나타낸 것
    => L2 노름 : 벡터의 각 원소를 제곱해 더한 후 다시 제곱근을 구하여 계산한다.
                    : 벡터를 정규화하고, 내적을 구하는 것이다. -> 각도가 중요! 

코사인 유사도

# 코사인 유사도
def cos_similarity(x,y,eps=1e-8):
    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의 유사도 구하기

text2 = "You say goodbye and I say hello."
corpus, word_to_id, id_to_word = preprocess(text2)
vocab_size = len(word_to_id)
C = create_co_matrix(corpus,vocab_size)

c0 = C[word_to_id['you']] # 'you'의 단어 벡터
c1 = C[word_to_id['i']] # 'i'의 단어 벡터

print(cos_similarity(c0,c1)) 
>> 0.7071067758832467  # 유사성이 높다

유사 단어 랭킹 표시

: 검색어 입력 후 그 검색어와 비슷한 단어를 유사도 순으로 출력한다.

  1. 검색어의 단어 벡터를 꺼낸다.
  2. 검색어의 단어 벡터와 다른 모든 단어 벡터와의 코사인 유사도를 각각 구한다.
  3. 계산한 코사인 유사도 결과를 기준으로 값이 높은 순서대로 출력한다.
def most_similar(query,word_to_id,id_to_word,word_matrix,top=5):
    # 1. 검색어를 꺼낸다.
    if query not in word_to_id:
        print('%s(을)를 찾을 수 없습니다.' %query)
        return
    
    print('[검색어]:'+query +'\n')
    query_id = word_to_id[query] # 검색 단어의 id 지정
    query_vec = word_matrix[query_id] # 유사도 행렬의 id의 행
    
    # 2. 코사인 유사도 계산
    vocab_size = len(id_to_word)
    similarity = np.zeros(vocab_size) # 0벡터 초기화
    for i in range(vocab_size):
        similarity[i] = cos_similarity(word_matrix[i],query_vec) # 각 단어와의 코사인 유사도 계산
        
    # 3. 코사인 유사도를 기준으로 내림차순으로 출력
    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

# 검색어 you와 유사도가 높은 순서

text2 = "You say goodbye and I say hello."
corpus, word_to_id, id_to_word = preprocess(text2)
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)
#결과
[검색어:]you

goodbye: 0.7071067758832467
i: 0.7071067758832467
hello: 0.7071067758832467
say: 0.0
and: 0.0

통계 기반 기법 개선하기

 

문제점

동시발생 행렬 : 두 단어가 동시엥 발생한 횟수 = 고빈도 단어에서 문제가 생긴다!

ex) the car >> car drive  : car 이 drive보다 the와 더 많이 붙어 있는 경우가 많아 관련성이 강하다고 나타난다.

 

점별 상호 정보량 (PMI : Pointwise Mutual Information)

P(x) : x가 일어날 확률

P(y) : y가 일어날 확률

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

=> PMI 값이 높을수록 관련성이 높다.


동시발생 행렬 (행렬의 원소를 동시발생한 단어의 횟수) 를 이용하여 PMI를 완성 시켜보자.

C : 동시발생 행렬

C(x,y) : 단어 x와 y가 동시발생 하는 횟수

C(x) : 단어 x의 등장 횟수

C(y) : 단어 y의 등장 횟수

N : 말뭉치에 포함된 단어 수

Ex)

'the'는 1000번, 'car'은 20번, 'drive'는 10번 등장하였을 때,
1) 'the'와 'car'의 동시발생 수가 10회라면 PMI 결과는 다음과 같다.

2) 'car'와 'drive'의 동시발생 수가 5라면 PMI 결과는 다음과 같다.

동시발생 횟수로만 보면 the와 car의 관계가 더 높지만, PMI 기준으로는 car과 drive의 관련성이 더 높다.

=> 단어가 단독으로 출현하는 횟수가 고려 되었기 때문이다.

-> the는 단독으로도 나타나는 경우가 많음!!(흔하다..)


동시발생 횟수가 0일 경우 log2(0)이 되므로 양의 상호정보량 (PPMI : Positive PMI) 를 이용한다.

PMI가 음수일 때는 0으로 취급 된다.

def ppmi(C,verbose=False, eps=1e-8): #동시발생 행렬
                                #verbose=진행사항 출력 여부 플래그 - 큰 말뭉치에서 True하면 중간중간 진행 상황 알려줌
    M = np.zeros_like(C,dtype=np.float32) #행렬
    N = np.sum(C)  # 총 단어수
    S = np.sum(C,axis=0) # C 동시발생행렬
    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) #pmi
            M[i,j] = max(0,pmi) #ppmi
            
            if verbose:
                cnt +=1
                if cnt%(total//100)==0:
                    print('%.1f%% 완료' % (100*cnt/total))
    return M
text2 = "You say goodbye and I say hello."
corpus, word_to_id, id_to_word = preprocess(text2)
vocab_size = len(word_to_id)
C = create_co_matrix(corpus,vocab_size)
W = ppmi(C) # return M

np.set_printoptions(precision=3) # 유효 자릿수 3자리 표시
print("동시발생 행렬")
print(C)
print('-'*50)
print("PPMI")
print(W)
동시발생 행렬
[[0 1 0 0 0 0 0]
 [0 0 1 0 1 1 0]
 [0 1 0 1 0 0 0]
 [0 0 1 0 1 0 0]
 [0 1 0 1 0 0 0]
 [0 1 0 0 0 0 1]
 [0 0 0 0 0 1 0]]
--------------------------------------------------
PPMI
[[0.  inf 0.  0.  0.  0.  0. ]
 [0.  0.  0.7 0.  0.7 0.7 0. ]
 [0.  0.7 0.  1.7 0.  0.  0. ]
 [0.  0.  1.7 0.  1.7 0.  0. ]
 [0.  0.7 0.  1.7 0.  0.  0. ]
 [0.  0.7 0.  0.  0.  0.  2.7]
 [0.  0.  0.  0.  0.  2.7 0. ]]

PPMI 문제점

말뭉치 어휘 수가 증가하면, 각 단어 벡터의 차원 수가 증가한다.

말뭉치 어휘 수가 10만 개이면, 10만 차원의 벡터를 다뤄야한다ㅠㅠ

행렬을 살펴보면 대부분이 0이고, 각 원소의 중요도가 낮다. 이러한 경우 노이즈가 약하고 견고하지 못하다.

=> 벡터의 차원 감소가 필요하다.

 


차원 감소 (demensionality reduction)

중요한 정보는 유지하며 벡터의 차원을 줄인다.

=> 데이터 분포를 고려하여 중요한 '축'을 찾는다.

희소행렬(희소벡터) : 원소 대부분이 0인 행렬(벡터)

-> 밀집벡터 : 중요한 축을 찾아내어 더 작은 차원으로 다시 표현하여 원소 대부분이 0이 아니다.


차원 감소 방법

특잇값 분해 (SVD : Singular Value Decomposition)

행렬을 3개의 행렬 곱으로 분해한다. 

U와 V는 직교행렬, U(T) = V(T) 이다. S는 대각행렬(대각 성분 외에는 모두 0인 행렬)이다.

행렬 S에서 특잇값이 작다면 중요도가 낮다. 단어공간 직교행렬인 U에서 차원이 감소 된 단어 벡터를 만들 수 있다.

?????

코드 오류..

 

 


PTB 데이터셋

텍스트 파일로 된 PTB 말뭉치는 한 문장이 하나의 줄로 저장되어 있다.

<unk>,<eos>

 

728x90
반응형