본문 바로가기

딥러닝

단어의 의미를 파악하는 방법 (1)

단어의 의미를 파악하는 기법으로 (1) 사람이 수작업으로 레이블링하는 시소러스를 활용한 기법과 텍스트 데이터로부터 단어의 의미를 자동으로 추출하는 (2) 통계 기반 기법, (3) 추론 기반 기법이 있다.

 

1. 시소러스(Theasurus)

■ 시소러스는 단어를 의미에 따라 분류·배열한 일종의 유의어 사전으로, 뜻이 같은 단어인 '동의어'나 뜻이 비슷한 단어인 '유의어'가 한 그룹으로 분류되어 있다.

■ 예를 들어 시소러스에서 car의 동의어는 다음과 같이 나타낼 수 있다.

■ 그리고 자연어 처리에 이용되는 시소러스에는 단어 사이의 '상·하위 관계', '전체와 부분 관계' 등 더 세부적인 관계를 그래프 구조로 정의하는 경우가 있다.

■ 예를 들어 단어 car같은 경우, 상위 개념으로 motor vehicle, 하위 개념으로 suv나 compact car 등이 있다. 이를 다음과 같은 그래프 구조로 정의하면, 각 단어들 간의 관계를 쉽게 파악할 수 있다.

■ 이렇게 모든 단어에 유의어 집합을 구성한 다음, 단어 간의 관계를 그래프로 표현해 정의해서, 이 그래프를 토대로 컴퓨터에게 단어 간의 관계를 학습시킬 수 있다.  

1.1 WordNet

■ WordNet은 자연어 처리 분야에서 대표적인 시소러스로 유의어 사이의 관계를 그래프 구조로 정의한 DB이다.

■ 파이썬에서 WordNet을 이용하려면 NLTK(Natural Language Toolkit) 라이브러리를 사용해야 한다.

import nltk
nltk.download('wordnet')

(* NLTK는 품사 태깅, 구문 분석, 정보 추출, 의미 분석 등 자연어 처리에서 사용하는 기능들을 제공하는 라이브러리)

NLTK Book

 

NLTK Book

 

www.nltk.org

WordNet에서는 각 단어가 synset이라는 동의어 그룹으로 분류되어 있어서 예시 단어 car의 동의어를 찾으려면, synsets( ) 메서드를 사용하면 된다.

from nltk.corpus import wordnet

wordnet.synsets('car')
```#결과#```
[Synset('car.n.01'),
 Synset('car.n.02'),
 Synset('car.n.03'),
 Synset('car.n.04'),
 Synset('cable_car.n.01')]
````````````

■ 결과로 원소가 5개인 리스트가 나왔는데, 이는 단어 car가 '자동차'라는 의미 외에 다른 의미도 가지고 있기 때문이다. 즉, 위의 결과는 car라는 단어에는 다섯 가지 의미가 존재한다는 것이다. 

각 원소는 점(.)으로 이뤄진 표제어이다. 표제어를 읽는 방법은 맨 앞이 단어 이름, 두 번째는 단어의 속성(명사, 동사 등), 세 번째는 그룹의 인덱스이다.

- 예를 들어 car.n.01이라는 표제어는 car라는 명사의 첫 번째, car.n.02는 car라는 명사의 두번째의 의미라는 것을 나타낸 것이다.

이렇게 단어 하나는 여러 의미를 갖기 때문에, 여떤 단어의 동의어를 얻으려면 단어가 어떤 의미를 갖는지 표제어를 지정해야 한다.

■ 명시된 표제어가 가리키는 동의어의 의미를 확인해 보려면, synsent( ) 메서드를 사용해 표제어의 동의어 그룹을 가져와서 definition( ) 메서드를 호출하면 된다.

car_n_01 = wordnet.synset('car.n.01')
car_n_02 = wordnet.synset('car.n.02')
cable_car_n_01 = wordnet.synset('cable_car.n.01')

car_n_01.definition()
```#결과#```
'a motor vehicle with four wheels; usually propelled by an internal combustion engine'
````````````

car_n_02.definition()
```#결과#```
'a wheeled vehicle adapted to the rails of railroad'
````````````

cable_car_n_01.definition()
```#결과#```
'a conveyance for passengers or freight on a cable railway'
````````````

■ 표제어의 동의어 그룹에 어떤 단어들이 존재하는지 확인하려면 lemma_names( ) 메서드를 호출하면 된다.

car_n_01.lemma_names()
```#결과#```
['car', 'auto', 'automobile', 'machine', 'motorcar']
````````````

car_n_02.lemma_names()
```#결과#```
['car', 'railcar', 'railway_car', 'railroad_car']
````````````

cable_car_n_01.lemma_names()
```#결과#```
['cable_car', 'car']
````````````

- car.n.01 표제어에는 4개의 단어 'auto', 'automobile', 'machine', 'motocar',

- car.n.02 표제어에는 3개의 단어 'railcar', 'railway_car', 'railroad_car',

- cable_car.n.01 표제어에는 1개의 단어 'cable_car'가 동의어로 등록되어 있는 것을 확인할 수 있다.

■ 더 세부적인 단어 사이의 관계를 확인하려면 WordNet 단어의 단어 네트워크를 이용하면 된다. 예를 들어 ·하 관계를 확인하려면 hypernym_paths( ) 메서드를 사용하면 된다.

cable_car_n_01.hypernym_paths()
```#결과#```
[[Synset('entity.n.01'),
  Synset('physical_entity.n.01'),
  Synset('object.n.01'),
  Synset('whole.n.02'),
  Synset('artifact.n.01'),
  Synset('structure.n.01'),
  Synset('area.n.05'),
  Synset('room.n.01'),
  Synset('compartment.n.02'),
  Synset('cable_car.n.01')]]
````````````

- cable_car.n.01의 상하 관계는 (상) entity \( \rightarrow \) physical_entity \( \rightarrow  \ldots \) compartment \( \rightarrow \) (하) cable_car로 관계에 대한 구조를 갖는 것을 확인할 수 있다.

- 이렇게 WordNet의 단어 네트워크는 위로 갈수록 단어의 의미가 추상적이고, 아래로 갈수록 구체적이 되도록 단어들이 배치되어 있다.

- cable_car.n.01 표제어의 경우 한 가지의 관계를 갖지만, 표제어에 따라서 2개 이상의 경로가 존재할 수 있다.

car_n_01.hypernym_paths()
```#결과#```
[[Synset('entity.n.01'),
  Synset('physical_entity.n.01'),
  Synset('object.n.01'),
  Synset('whole.n.02'),
  Synset('artifact.n.01'),
  Synset('instrumentality.n.03'),
  Synset('container.n.01'),
  Synset('wheeled_vehicle.n.01'),
  Synset('self-propelled_vehicle.n.01'),
  Synset('motor_vehicle.n.01'),
  Synset('car.n.01')],
 [Synset('entity.n.01'),
  Synset('physical_entity.n.01'),
  Synset('object.n.01'),
  Synset('whole.n.02'),
  Synset('artifact.n.01'),
  Synset('instrumentality.n.03'),
  Synset('conveyance.n.03'),
  Synset('vehicle.n.01'),
  Synset('wheeled_vehicle.n.01'),
  Synset('self-propelled_vehicle.n.01'),
  Synset('motor_vehicle.n.01'),
  Synset('car.n.01')]]
````````````

car_n_01.hypernym_paths()[0]
```#결과#```
[Synset('entity.n.01'),
 Synset('physical_entity.n.01'),
 Synset('object.n.01'),
 Synset('whole.n.02'),
 Synset('artifact.n.01'),
 Synset('instrumentality.n.03'),
 Synset('container.n.01'),
 Synset('wheeled_vehicle.n.01'),
 Synset('self-propelled_vehicle.n.01'),
 Synset('motor_vehicle.n.01'),
 Synset('car.n.01')]
````````````

car_n_01.hypernym_paths()[1]
```#결과#```
[Synset('entity.n.01'),
 Synset('physical_entity.n.01'),
 Synset('object.n.01'),
 Synset('whole.n.02'),
 Synset('artifact.n.01'),
 Synset('instrumentality.n.03'),
 Synset('conveyance.n.03'),
 Synset('vehicle.n.01'),
 Synset('wheeled_vehicle.n.01'),
 Synset('self-propelled_vehicle.n.01'),
 Synset('motor_vehicle.n.01'),
 Synset('car.n.01')]
````````````

- car.n.01 표제어 같은 경우 'container'나 'conveyance'같은 개념으로 연결될 수 있어 2가지 경로가 반환되는 것을 확인할 수 있다.

■ WordNet에는 동의어, 유의어 카테고리가 있으며, 네트워크 구조로 단어가 연결되어 있다. 

■ 이렇게 단어가 연결되어 있으면, 이 관계를 통해 다양한 계산이 가능하며 대표적으로 단어 사이의 유사도(similarity) 계산이 있다. 유사도 계산은 path_similarity( ) 메서드를 표제어에 적용하여 구할 수 있다.

car_n_01.path_similarity(car_n_02)
```#결과#```
0.2
````````````

car_n_02.path_similarity(cable_car_n_01)
```#결과#```
0.1
````````````

car_n_01.path_similarity(cable_car_n_01)
```#결과#```
0.08333333333333333
````````````

path_similarity( ) 메서드를 적용하면 결과로 0과 1사이의 실숫값이 나오는데, 1에 가까울수록(값이 높을수록) 두 단어가 서로 비슷하다는 것을 나타낸다.

- 위의 결과를 해석하면, car.n.01은 car.n.02와 유사하고, car.n.01은 car.n.02보다 cable_car.n.01과 덜 유사한 것으로 볼 수 있다.

이와 같은 결과가 나오는 이유는 다음 그림과 같이 path_similarity( ) 메서드가 단어 네트워크의 공통 경로를 바탕으로 단어 사이의 유사성(단어 사이의 의미적 가까움)을 계산하기 때문이다.

- 위의 그림을 보면 car.n.01과 car.n.02는 경로에 있는 많은 표제어를 공유하고 있음을 볼 수 있다. 한편, cable_car.n.01은 artifact.n.01에서 갈라져 나와 car.n.01과 거리가 멀어지는 것을 볼 수 있다. 

- 이러한 계산 방법으로 cable_car.n.01의 유사도는 car.n.01보다 car.n.02와 더 높게 나타난 것이다.


1.2 시소러스 문제점

WordNet과 같은 시소러스는 사람이 수작업으로 단어에 대한 동의어와 구조 등의 관계를 정의하기 때문에 다음과 같은 문제점들이 있다.

- 사람이 수작업으로 단어와 단어 사이의 관계를 정의해야 하기 때문에 인적 비용이 발생한다.

- 신조어가 생기거나 기존 단어에 새로운 의미가 추가되면, 변화에 맞춰 수작업으로 단어 사이의 관계를 갱신해야 한다.

- 시소러스는 뜻이 비슷한 단어들을 묶는다. 예를 들어 vintage 'adj. 낡고 오래된 것'과 antique 'n. 골동품(역사적 가치가 있는 오래된 물건)'은 의미가 비슷하지만 용법이 다르다. antique는 보통 가구, 장식품, 예술품 등 역사적 가치를 가진 오래된 물건을 묘사하는 데 사용하기 때문이다. 시소러스는 이런 미묘한 차이를 표현할 수 없다.

■ 이런 시소러스 문제를 해결하기 위해 개발된 기법이 단어의 의미를 자동으로 추출할 수 있는 '통계 기반 기법'과 신경망을 사용한 '추론 기반 기법'이다.

 

2. 통계 기반 기법

통계 기반 기법에서 '말뭉치(corpus)'라는 텍스트 데이터를 표본으로 이용해서 단어의 의미를 추출한다.

■ 말뭉치에는 특정 언어, 방언, 문장이나 단어를 선택하는 방법, 단어의 의미 등 사람이 알고 있는 자연어의 다양한 특성과 패턴이 포함되어 있다.

2.1 말뭉치 전처리

■ 이 말뭉치라는 텍스트 데이터는 전처리를 통해 텍스트를 단어 단위로 분할해서 다양한 작업에 활용할 수 있다. 여기서 전처리란 텍스트 데이터를 단어로 분할하고 분할된 단어들을 단어 ID 목록으로 변환하는 일이다.

■ 예를 들어 text = 'You say goodbye and I say hello.'라는 말뭉치가 있다면, 먼저 다음과 같이 split( )를 사용해 텍스트를 단어 단위로 분할 할 수 있다.

text = 'You say goodbye and I say hello.'
text = text.lower() # 모든 문자 소문자로 변환
text = text.replace('.', ' .') # 문장 끝의 마침표(.) 고려
text
```#결과#```
'you say goodbye and i say hello .'
````````````

words = text.split(' ') # 공백 기준으로 분할
words
```#결과#```
['you', 'say', 'goodbye', 'and', 'i', 'say', 'hello', '.']
````````````

cf) 정규표현식을 사용해 동일한 text를 다음과 같이 분할할 수도 있다.

import re

text2 = 'You say goodbye and I say hello.'
text2 = text2.lower()
words2 = re.findall(r'\b\w+\b|[.,!?]', text2)
print(words2)
```#결과#```
['you', 'say', 'goodbye', 'and', 'i', 'say', 'hello', '.']
````````````

words == words2
```#결과#```
True
````````````

■ 이렇게 단어 단위로 분할한 다음, 단어에 ID를 부여해서 단어 ID와 단어를 짝지어주는 대응표를 만든다.

■ 대응표는 word_to_id, id_to_word이며, word_to_id는 단어에서 단어 ID로의 변환, id_to_word는 단어 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
        
word_to_id
```#결과#```
{'you': 0, 'say': 1, 'goodbye': 2, 'and': 3, 'i': 4, 'hello': 5, '.': 6}
````````````

id_to_word
```#결과#```
{0: 'you', 1: 'say', 2: 'goodbye', 3: 'and', 4: 'i', 5: 'hello', 6: '.'}
````````````

- 딕셔너리를 사용하면 key-value를 이용해서 단어로 단어 ID를 검색하거나, 반대로 단어 ID로 단어를 검색할 수 있다.

id_to_word[5]
```#결과#```
'hello'
````````````

word_to_id['say']
```#결과#```
1
````````````

마지막으로 단어 목록을 단어 ID 목록으로 변경한다. 

corpus = [word_to_id[w] for w in words]
corpus = np.array(corpus)
corpus
```#결과#```
array([0, 1, 2, 3, 4, 1, 5, 6])
````````````

 

- 이러한 전처리 과정을 하나로 모아 다음과 같이 말뭉치 전처리를 수행하는 함수로 정의할 수 있다.

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
corpus, word_to_id, id_to_word = preprocess(text) # 말뭉치 전처리 수행

corpus
```#결과#```
array([0, 1, 2, 3, 4, 1, 5, 6, 7])
````````````

word_to_id
```#결과#```
{'you': 0, 'say': 1, 'goodbye': 2, 'and': 3, 'i': 4, 'hello': 5, '': 6, '.': 7}
````````````

id_to_word
```#결과#```
{0: 'you', 1: 'say', 2: 'goodbye', 3: 'and', 4: 'i', 5: 'hello', 6: '', 7: '.'}
````````````

 

2.2 단어의 분산 표현과 분포 가설

■ RGB같은 벡터 표현으로 더 정확하게 '색'을 구분하듯이 '단어'가 있으면 그 단어의 '의미'를 벡터로 파악할 수 있는 방법으로 단어의 '분산 표현(distributional representation)'이 있다. 

■ 단어의 분산 표현은 단어를 고정 길이의 밀집 벡터(dense vector)로 표현한다. 밀집 벡터는 대부분의 원소가 0이 아닌 실수로 구성된 벡터를 말한다.

예를 들어 3차원의 분산 표현은 [-0.12, 0.34, 0.65] 같은 형태이다. 

■ 단어를 벡터로 표현하는 기법의 대부분은 '단어의 의미는 주변 단어에 의해 형성된다.'는 가설에 기반을 두고 있다. 이를 '분포 가설(distributional hypothesis)'이라 한다. 분산 표현도 분포 가설에 기반한다.

■ 즉, 분포 가설은 단어 자체에 단어가 의미가 있는 것이 아니라 단어가 사용된 맥락, 문맥(context)이 단어의 의미를 형성한다고 간주한다.

예를 들어 분산 표현은 puppy라는 단어가 cute, lovely라는 단어와 자주 등장하면, 'puppy라는 단어는 cute, lovely한 느낌이다.'라고 정의한다. 이렇게 분산 표현은 주변 단어, 즉 맥락을 고려해서 단어의 뉘앙스를 표현할 수 있다.

다른 예로 'I drink beer.', 'We drink soju.'처럼 drik 단어 주변에는 일반적으로 음료의 범주에 속하는 단어들이 많이 사용된다.

 그리고 'I guzzle beer.', 'We guzzle soju.', 'I sip beer.', 'We sip soju.'라는 문장이 있으면 guzzle과 sip은 drink와 같은 맥락에서 사용되는 단어이며 drink, guzzle, sip이 서로 가까운 의미의 단어임을 알 수 있다.

■ 이렇게 '맥락'이란 특정 단어를 중심으로 그 주변에 위치한 단어를 말하며, 맥락의 크기(주변 단어를 몇 개나 포함할지)를 윈도우 크기(window size)라고 한다.  

- 윈도우 크기가 1이면 특정 단어의 좌우 한 단어씩, 윈도우 크기가 2이면 좌우 두 단어씩이 맥락에 포함된다.

- 단, 상황에 따라 왼쪽 단어만 또는 오른쪽 단어만을 사용하거나 문장의 시작과 끝을 사용할 수도 있다.

 

2.3 동시발생 행렬

분포 가설에 기초해 단어를 벡터로 나타내는 방법으로 주변 단어를 세어 보는 방법이 있다. 이를 통계 기반 기법이라고 한다.

 예를 들어 위의 text = 'You say goodbye and I say hello.'에 말뭉치 전처리를 하면 이 텍스트를 구성하는 단어 수가 총 7개임을 알 수 있다. 

text = 'You say goodbye and I say hello.'
corpus, word_to_id, id_to_word = preprocess(text)

corpus
```#결과#```
array([0, 1, 2, 3, 4, 1, 5, 6])
````````````

id_to_word
```#결과#```
{0: 'you', 1: 'say', 2: 'goodbye', 3: 'and', 4: 'i', 5: 'hello', 6: '.'}
````````````

■ 그리고 윈도우 크기가 1일 때, 단어의 맥락에 해당하는 주변 단어의 빈도를 단어 순서대로 세어보면 'you'의 맥락은 'say' 하나뿐이다.

- 이 빈도수를 벡터로 표현하면, 'you'라는 단어는 벡터 [0, 1, 0, 0, 0, 0, 0]로 표현할 수 있다.

- 다음 단어인 'say'에 대해서도 동일한 작업을 수행하면 'you say goodbye and i say hello.'에서 'say'의 주변 단어(윈도우 크기 1 기준)는 'you', 'goodbye', 'i', 'hello'이므로 단어 'say'의 주변 단어의 빈도수는 다음과 같다.

즉, 단어 'say'는 벡터 [1, 0, 1, 0, 1, 1, 0]으로 표현할 수 있다.

■ 나머지 단어들에 대해서도 동일한 작업을 수행해서 각 단어의 맥락에 해당하는 주변 단어의 빈도수를 표로 나타내면 다음과 같다.

이 표의 각 행은 해당 단어를 표현한 벡터이다. 따라서 위의 표는 1차원 벡터들이 행 방향으로 쌓여 만든 2차원 행렬로 볼 수 있다. 이 행렬을 동시발생 행렬(Co-occurrence matrix)이라 한다.

■ 이 동시발생 행렬을 이용하면 단어 ID별로 해당 단어의 벡터 표현을 확인할 수 있다.

co_occurrence_matrix = np.array([
    [0, 1, 0, 0, 0, 0, 0], # 'you'
    [1, 0, 1, 0, 1, 1, 0], # 'say'
    [0, 1, 0, 1, 0, 0, 0], # 'goodbye'
    [0, 0, 1, 0, 1, 0, 0], # 'and'
    [0, 1, 0, 1, 0, 0, 0], # 'i'
    [0, 1, 0, 0, 0, 0, 1], # 'hello'
    [0, 0, 0, 0, 0, 1, 0], # '.'
])

co_occurrence_matrix[0] # ID가 0인 단어 'you'의 벡터 표현 
```#결과#```
array([0, 1, 0, 0, 0, 0, 0])
````````````

word_to_id
```#결과#```
{'you': 0, 'say': 1, 'goodbye': 2, 'and': 3, 'i': 4, 'hello': 5, '.': 6}
````````````

co_occurrence_matrix[word_to_id['hello']] # ID가 5인 단어 'hello'의 벡터 표현 
```#결과#```
array([0, 1, 0, 0, 0, 0, 1])
````````````

■ 파이썬에서 말뭉치로부터 동시발생 행렬을 생성해 주는 함수를 다음과 같이 구현할 수 있다.

def create_co_occurrence_matrix(corpus, num_words, window_size = 1): # 단어 ID 리스트, 단어 수, 윈도우 크기
    corpus_size = len(corpus) # 텍스트에 있는 단어의 총 개수
    co_occurrence_matrix = np.zeros((num_words, num_words), dtype = np.int32) # 단어 수 x 단어 수만큼의 행렬 
    
    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_occurrence_matrix[word_id, left_word_id] += 1 # 주변 단어의 빈도수를 행렬에 추가
            
            if right_idx < corpus_size:
                right_word_id = corpus[right_idx]
                co_occurrence_matrix[word_id, right_word_id] += 1 # 주변 단어의 빈도수를 행렬에 추가
    return co_occurrence_matrix

예를 들어 'you'의 경우 idx = 0, word_id = 0일 때이며, left_idx = -1, right_idx = 1. 즉, 'you'의 주변 단어는 'you'의 오른쪽 단어인 'say'밖에 없다. 따라서 right_idx = 1이며, 'say'의 인덱스가 텍스트에 있는 단어의 총 개수를 벗어나지 않는다.

- 윈도우 크기가 1이고 단어가 'you'일 때, 주변 단어는 'say'뿐인 이 관계를 동시발생 행렬로  표현하면 1행 2열에 빈도수인 1이 표시되어야 한다.

- 따라서 모든 원소를 0으로 초기화한 '단어 수 x 단어 수' 크기의 행렬의 행은 첫 번째 단어일 때의 word_id = 0, 열은 주변 단어의 ID = 1. co_occurrence_matrix[0, 1]에 1을 추가한다. 

- 파이썬의 인덱스는 0부터 시작하므로 1행 2열에 단어 'you'의 맥락에 포함되는 단어의 빈도수 1이 저장된 것이다. (윈도우 크기가 1일 때)

- 다른 예로, 단어 'hello'는 idx = 6, word_id = 5일 때이며, left_idx = 5, right_idx = 7이 된다.

- 먼저 left_idx >= 0이므로 co_occurrence_matrix[5, corpus[5]] = co_occurrence_matrix[5, 1]. 즉, 동시발생 행렬의 6행 2열에 'hello'의 왼쪽 단어 'say'의 빈도수 1이 추가된다.

- 그다음, right_idx < corpus_size이므로 co_occurrence_matrix[5, corpus[7]] = co_occurrence_matrix[5, 6]이다. 따라서 동시발생 행렬의 6행 7열에 'hello'의 오른쪽 단어 '.'의 빈도수 1이 추가된다.

- 이런 작업을 말뭉치에 있는 모든 단어에 대해 수행하므로 다음과 같은 동시발생 행렬을 얻을 수 있다.

create_co_occurrence_matrix(corpus, 7)
```#결과#```
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, 1, 0, 0, 0],
       [0, 1, 0, 0, 0, 0, 1],
       [0, 0, 0, 0, 0, 1, 0]])
````````````

 

2.4 벡터 간 유사도

단어를 벡터로 표현한 뒤, 단어 벡터 사이의 유사도를 계산할 수 있다. 벡터 사이의 유사도를 계산하는 방법으로 벡터의 내적, 유클리드 거리 등이 있으며, 보통 단어 벡터의 유사도를 계산할 때는 코사인 유사도(cosine similarity)를 사용한다.

■ 코사인 유사도는 두 벡터 간의 각도를 구할 수 있을 때, 두 벡터 간의 각도가 0도. 즉, 두 벡터의 방향이 완전히 동일하면 \( \cos 0 =1 \), 두 벡터가 90도를 이루면 \( \cos 90 = 0 \),  두 벡터의 각도가 180도. 즉 반대 방향이면 \( \cos 180 = -1 \)의 값을 갖는다.

■ 따라서 코사인 유사도는 -1 이상 1 이하의 값을 가지며 값이 1에 가까울수록 두 벡터가 같은 방향을 바라보고 있는 것이므로 유사도가 높다고 판단할 수 있다. 반대로 값이 -1에 가까울수록 두 벡터가 서로 정반대인 방향을 바라보고 있는 것이므로 유사도가 낮다고 판단할 수 있다.

■ 두 벡터가 \( \mathbf{x} = (x_1, x_2, x_3, \ldots , x_n), \quad \mathbf{y} = (y_1, y_2, y_3, \ldots , y_n) \)이라면, 코사인 유사도의 식은 분자에는 벡터의 내적, 분모에는 각 벡터의 노름(norm)이 들어간다. \[ \text{similarity}(\mathbf{x}, \mathbf{y}) = \cos(\theta) = \frac{\mathbf{x} \cdot \mathbf{y}}{\|\mathbf{x}\| \|\mathbf{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}} \] - 코사인 유사도 식의 핵심은 벡터를 정규화하고 내적을 구하는 것이다.
■ 이 식을 파이썬 함수로 구현하면 다음과 같다. 

def cos_similarity(x, y, eps = 1e-8):
    nx = x / (np.sqrt(np.sum(x**2)) + eps)
    ny = y / (np.sqrt(np.sum(y**2)) + eps)
    similarity = np.dot(nx, ny)
    return similarity

- 분모에 엡실론 값을 더한 이유는 원소가 모두 0인 벡터가 들어오면 분모가 0이 되기 때문에 이를 방지하고자 아주 작은 값인 엡실론을 더한 것이다.

- 이 함수를 이용해서 다음과 같이 두 벡터 간의 코사인 유사도를 계산할 수 있다.

co_occurrence_matrix = create_co_occurrence_matrix(corpus, 7)
co_occurrence_matrix[0], co_occurrence_matrix[2]
```#결과#```
(array([0, 1, 0, 0, 0, 0, 0]), array([0, 1, 0, 1, 0, 0, 0]))
````````````

cos_similarity(co_occurrence_matrix[0], co_occurrence_matrix[2])
```#결과#```
0.7071067691154799
````````````

- 결과를 보면, 단어 'you'와 'goodbye'의 코사인  유사도는 약 0.7이므로 두 단어의 유사성이 높은 편이라고 말할 수 있다. 

 

2.5 유사 단어의 랭킹 표시

■ 코사인 유사도 함수를 활용하면 어떤 단어가 주어졌을 때, 그 단어와 비슷한 단어를 유사도 순으로 계산할 수 있다.

def most_similar(query, word_to_id, id_to_word, word_matrix, top = 5):
    if query not in word_to_id: # 찾고자 하는 단어가 없으면 종료
        print(f'{query} 단어를 찾을 수 없다.')
    
    ## 쿼리 단어의 ID와 쿼리 단어 벡터 추출
    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)
    
    ## 코사인 유사도 계산 결과 내림차순 출력
    count = 0 
    for i in (-1 * similarity).argsort():
        if id_to_word[i] == query:
            continue
        print(f'{id_to_word[i]}, {similarity[i]}')
        count += 1
        if count >= top:
            return

- most_similar 함수의 인수는 검색어, 단어에서 단어 ID로의 딕셔너리, 단어 ID에서 단어로의 딕셔너리, 동시발생 행렬, 출력할 상위 개수이다.

 예를 들어 단어 'you'를 검색한다면

- query가 word_to_id에 있으므로 query_id는 딕셔너리 word_to_id의 킷값에 접근해 단어 'you'의 ID를 가져올 수 있다.

- 가져온 'you'의 ID가 동시발생 행렬에 존재한다면 검색 단어의 벡터를 가져올 수 있다.

- 동시발생 행렬의 각 행은 단어를 표현한 벡터이므로, 동시발생 행렬의 행과 검색 단어 벡터 간의 코사인 유사도를 계산하고

- 계산된 값을 내림차순하여 검색 단어와 코사인 유사도가 높은 순으로 출력한다.

- 넘파이의 argsort( )는 넘파이 배열의 원소를 오름차순 했을 때의 인덱스 결과를 반환한다. 예를 들어 다음과 같은 넘파이 배열이 있을 때

a = np.array([10, -10, 0, 100])

- argsort( )를 적용하면 오름차순 했을 때의 배열의 인덱스가 반환된다.

a.argsort()
```#결과#```
array([1, 2, 0, 3], dtype=int64)
````````````

- 반환값에서 1은 a의 인덱스 1은  -10이고 인덱스 2는 0, 인덱스 0은 10, 인덱스 3은 100이다.

- 즉, argsort( )는  배열 a를 오름차순 정렬한 결과 -10(기존 a에서의 인덱스 1), 0(기존 a에서의 인덱스 2), 10(기존 a에서의 인덱스 0), 100(기존 a에서의 인덱스 3)의 원소 인덱스 [1, 2, 0, 3]을 반환한다.

- argsort( ) 메서드를 이용해서 배열의 원소들을 내림차순 정렬하려면, 배열에 마이너스 값을 곱해서 argsort( ) 메서드를 호출하면 된다.

- 따라서 내림차순 출력을 위해 유사도 값이 저장된 similarity 배열[0.99999998, 0., 0.70710677, 0., 0.70710677, 0.70710677, 0.]에 마이너스 1을 곱한 것이다.

text = 'You say goodbye and I say hello.'
corpus, word_to_id, id_to_word = preprocess(text)
vocab_size = len(word_to_id)
co_occurrence_matrix = create_co_occurrence_matrix(corpus, vocab_size)

most_similar('you', word_to_id, id_to_word, co_occurrence_matrix, top = 6)
```#결과#```
goodbye, 0.7071067691154799
i, 0.7071067691154799
hello, 0.7071067691154799
say, 0.0
and, 0.0
., 0.0
````````````

- 결과를 보면, 단어 'you'와 가까운 단어는 'goodbye', 'i', 'hello'이다. 'you'와 'i'는 인칭 대명사이므로 높은 유사도를 납득할 수 있지만, 'you'와 'goodbye' 그리고 'you'와 'hello'의 유사도가 높은 것은 직관적으로 이해하기 어렵다. 이와 같은 결과는 말뭉치의 크기가 너무 작은 것이 원인이다.

 

단어의 의미를 파악하는 방법 (2)

'딥러닝' 카테고리의 다른 글

단어의 의미를 파악하는 방법 (4)  (0) 2024.11.22
단어의 의미를 파악하는 방법 (2)  (0) 2024.11.21
매개변수 갱신 방법(2)  (0) 2024.11.03
매개변수 갱신 방법(1)  (0) 2024.11.01
합성곱 신경망(CNN) (1)  (0) 2024.11.01