단어의 의미를 파악하는 기법으로 (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
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'의 유사도가 높은 것은 직관적으로 이해하기 어렵다. 이와 같은 결과는 말뭉치의 크기가 너무 작은 것이 원인이다.
'딥러닝' 카테고리의 다른 글
단어의 의미를 파악하는 방법 (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 |