개발자의 스터디 노트
임베딩 본문
1. 임베딩을 배우는 이유
그동안 우리는 벡터 표현으로 원-핫 표현을 사용했습니다. 이 벡터의 길이는 어휘 사전의 크기와 같고, 값은 특정 단어를 나타내는 한 위치만 1이고 나머지는 모두 0입니다. 또한 카운트 기반 표현을 보았습니다. 이 벡터의 길이는 모델에 있는 고유한 단어의 개수와 같지만, 문장에 등장하는 카운트 값은 단어의 빈도에 상응합니다. 카운트 기반 표현은 중요한 내용이나 의미가 벡터의 여러 차원에 표현되어서 분산적 표현이라고도 부릅니다. 분산 표현은 역사가 깊고 많은 머신러닝 및 신경망 모델에서 잘 동작합니다.
분산 표현은 단어가 훨씬 낮은 밀집 벡터로 표현된다는 사실에서 이름을 따왔습니다. 단어의 의미와 다른 속성이 이 밀집 벡터의 여러 차원에 걸쳐 분산됩니다.
저차원으로 학습된 밀집 표현은 원-핫 벡터나 카운트 기반의 벡터와 다른 장점이 몇 가지 있습니다.
첫째, 차원을 줄이며 계산을 효율적으로 수행합니다.
둘째, 카운트 기반 표현은 여러 차원에 비슷한 정보를 중복해 인코딩한 고차원 벡터를 만듭니다. 이런 벡터는 통계적 장점을 공유하지 못합니다.
셋째, 매우 고차원 입력은 머신러닝과 최적화에서 실제로 문제가 될 수 있습니다. 이런 현상을 차원의 저주(curse of dimensionality)라고 부릅니다. 전통적으로 이런 차원 문제를 해결하는데 특잇값 분해나 주성분 분석과 같은 차원 축소 방법을 사용했습니다. 하지만 아이러니하게도 이런 방법은(NLP에서는 일반적인 상황인) 차원이 수백만 개일 때는 잘 적용되지 않습니다.
넷째, 작업에 특화된 데이터에서 학습된 표현은 현재 작업에 최적입니다. TF-IDF 같이 경험적이거나 SVD같이 저차원 방법은 임베딩의 최적화 목적이 해당 작업과 관련이 있는지 명확하지 않습니다.
2. 임베딩의 효율성
원-핫 인코딩된 벡터와 Linear 층의 가중치 행렬을 사용한 행렬 곱셈의 예. 원-핫 벡터는 하나만 1이고 모두 0이므로 1의 위치가 행렬 곱셈에서 선택의 역할을 합니다. 이 과정이 가중치 행렬과 결과 벡터에서 음영 패턴을 사용하여 시각적으로 나타납니다.ㅣ 동작은 하지만, 이런 선택 방법은 원-핫 벡터를 가중치 행렬의 모든 값에 곱한 후 각 행의 합을 계산하므로 계산 비용이 많이 들고 비효율적입니다.
임베딩은 원-핫 벡터 또는 카운트 기반 표현보다 낮은 차원 공간에서 단어를 표현하는 데 자주 사용됩니다. 연구 논문에서 사용하는 일반적인 임베딩 크기는 25차원에서 500차원까지 다양하며 준비할 수 있는 GPU의 메모리 양에 따라 적절히 선택하면 됩니다.
3. 단어 임베딩 학습 방법
단어 임베딩 방법은 모두 단어만으로(즉, 레이블이 없는 데이터로) 학습되지만 지도 학습 방식을 사용합니다. 이를 위해 데이터가 암묵적으로 레이블되어 있는 보조 작업을 구성합니다. 이 보조 작업을 해결하기 위한 최적화된 표현은 텍스트 말뭉치의 많은 통계적, 언어적 속성을 감지하여 유용성이 높을 것이라고 직관적으로 알 수 있습니다.
보조작업의 예.
- 단어 시퀀스가 주어지면 다음 단어를 예측합니다. 언어 모델링 작업(language modeling task)이라고도 합니다.
- 앞과 뒤의 단어 시퀀스가 주어지면 누락된 단어를 예측합니다.
- 단어가 주어지면 위치에 관계없이 윈도(window)안에 등장할 단어를 예측합니다.
물론 이 목록은 완전하지 않으며 보조 작업의 선택은 알고리즘 설계자의 직관과 계산 비용에 따라 달라집니다. 예를 들면 GloVe, CBOW(continuous Bag-of-Words), 스킵그램(skipgram)등이 있습니다.
차후 임베딩 학습 모델을 만드는 코드에 대해서 따로 포스팅 하도록 하겠습니다.
4. 사전 훈련된 단어 임베딩
- 임베딩 로드
우선 Word2Vec 임베딩을 다운로드합니다.
http://nlp.stanford.edu/data/glove.6B.zip
임베딩을 효율적으로 처리하는 PreTrainedEmbeddings 유틸리티 클래스입니다.
빠른 조회를 위해 메모리 내에 모든 단어 벡터의 인덱스를 구축하고 근사 최근접 이웃 알고리즘을 구현한 annoy 패키지를 사용해 최근접 이웃 쿼리를 수행합니다.
윈도우에서 annoy 패키지를 설치하다 보면 에러가 발생합니다. 그럴 땐 직접 설치 파일을 다운로드하여야 합니다.
https://www.lfd.uci.edu/~gohlke/pythonlibs/
파일 목록 중 내 가상 환경의 파이썬 버전에 맞는 걸 다운로드하여야 합니다. 저는 파이썬 3.6이기 때문에
annoy-1.17.0-cp36-cp36m-win_amd64.whl
cp36이 라고 되어 있는 파일을 다운로드하였습니다.
pip install annoy-1.17.0-cp36-cp36m-win_amd64.whl
로 설치하면 됩니다.
import numpy as np
from annoy import AnnoyIndex
class PreTrainedEmbeddings(object):
""" 사전 훈련된 단어 벡터 사용을 위한 래퍼 클래스 """
def __init__(self, word_to_index, word_vectors):
"""
매개변수:
word_to_index (dict): 단어에서 정수로 매핑
word_vectors (numpy 배열의 리스트)
"""
self.word_to_index = word_to_index
self.word_vectors = word_vectors
self.index_to_word = {v: k for k, v in self.word_to_index.items()}
self.index = AnnoyIndex(len(word_vectors[0]), metric='euclidean')
print("인덱스 만드는 중!")
for _, i in self.word_to_index.items():
self.index.add_item(i, self.word_vectors[i])
self.index.build(50)
print("완료!")
@classmethod
def from_embeddings_file(cls, embedding_file):
"""사전 훈련된 벡터 파일에서 객체를 만듭니다.
벡터 파일은 다음과 같은 포맷입니다:
word0 x0_0 x0_1 x0_2 x0_3 ... x0_N
word1 x1_0 x1_1 x1_2 x1_3 ... x1_N
매개변수:
embedding_file (str): 파일 위치
반환값:
PretrainedEmbeddings의 인스턴스
"""
word_to_index = {}
word_vectors = []
with open(embedding_file, encoding='UTF8') as fp:
for line in fp.readlines():
line = line.split(" ")
word = line[0]
vec = np.array([float(x) for x in line[1:]])
word_to_index[word] = len(word_to_index)
word_vectors.append(vec)
return cls(word_to_index, word_vectors)
def get_embedding(self, word):
"""
매개변수:
word (str)
반환값
임베딩 (numpy.ndarray)
"""
return self.word_vectors[self.word_to_index[word]]
def get_closest_to_vector(self, vector, n=1):
"""벡터가 주어지면 n 개의 최근접 이웃을 반환합니다
매개변수:
vector (np.ndarray): Annoy 인덱스에 있는 벡터의 크기와 같아야 합니다
n (int): 반환될 이웃의 개수
반환값:
[str, str, ...]: 주어진 벡터와 가장 가까운 단어
단어는 거리순으로 정렬되어 있지 않습니다.
"""
nn_indices = self.index.get_nns_by_vector(vector, n)
return [self.index_to_word[neighbor] for neighbor in nn_indices]
def compute_and_print_analogy(self, word1, word2, word3):
"""단어 임베딩을 사용한 유추 결과를 출력합니다
word1이 word2일 때 word3은 __입니다.
이 메서드는 word1 : word2 :: word3 : word4를 출력합니다
매개변수:
word1 (str)
word2 (str)
word3 (str)
"""
vec1 = self.get_embedding(word1)
vec2 = self.get_embedding(word2)
vec3 = self.get_embedding(word3)
# 네 번째 단어 임베딩을 계산합니다
spatial_relationship = vec2 - vec1
vec4 = vec3 + spatial_relationship
closest_words = self.get_closest_to_vector(vec4, n=4)
existing_words = set([word1, word2, word3])
closest_words = [word for word in closest_words
if word not in existing_words]
if len(closest_words) == 0:
print("계산된 벡터와 가장 가까운 이웃을 찾을 수 없습니다!")
return
for word4 in closest_words:
print("{} : {} :: {} : {}".format(word1, word2, word3, word4))
단어 임베딩 사이의 관계
단어 임베딩에 인코딩 된 의미 관계를 탐색하는 방법은 다양합니다.
먼저 세 단어가 제공되며 처음 두 단어 간의 관계와 일치하는 네 번째 단어를 결정해야 합니다. 단어 임베딩을 사용하여 이를 공간적으로 인코딩할 수 있습니다. 먼저 Word1에서 Word2를 뺍니다. 뺄셈의 결과로 만들어진 차이 벡터는 Word1와 Word2 사이의 관계를 인코딩합니다. 그런 다음 이 차이 벡터를 Word3에 더해 비어있는 네 번째 단어에 가까운 벡터를 생성할 수 있습니다.
아래 예제는 유추 작업입니다.
embeddings = PreTrainedEmbeddings.from_embeddings_file('data/glove.6B.100d.txt')
인덱스 만드는 중!
완료!
# 성별 명사와 대명사의 관계
embeddings.compute_and_print_analogy('man', 'he', 'woman')
man : he :: woman : she
man : he :: woman : her
# 동사-명사 관계
embeddings.compute_and_print_analogy('fly', 'plane', 'sail')
fly : plane :: sail : ship
fly : plane :: sail : vessel
#명사-명사 관계
embeddings.compute_and_print_analogy('cat', 'kitten', 'dog')
cat : kitten :: dog : puppy
cat : kitten :: dog : puppies
cat : kitten :: dog : pooch
# 상위어
embeddings.compute_and_print_analogy('blue', 'color', 'dog')
cat : kitten :: dog : puppy
cat : kitten :: dog : puppies
cat : kitten :: dog : pooch
# 상위어
embeddings.compute_and_print_analogy('blue', 'color', 'dog')
blue : color :: dog : cat
blue : color :: dog : dogs
blue : color :: dog : pet
blue : color :: dog : viewer
# 부분에서 전체
embeddings.compute_and_print_analogy('toe', 'foot', 'finger')
toe : foot :: finger : ground
toe : foot :: finger : small
toe : foot :: finger : apart
# 방식 차이
embeddings.compute_and_print_analogy('talk', 'communicate', 'read')
talk : communicate :: read : instructions
talk : communicate :: read : scanned
talk : communicate :: read : transmit
# 전체 의미 표현(관습/인물)
embeddings.compute_and_print_analogy('blue', 'democrat', 'red')
blue : democrat :: red : republican
blue : democrat :: red : congressman
blue : democrat :: red : senator
# 비교급
embeddings.compute_and_print_analogy('fast', 'fastest', 'young')
fast : fastest :: young : youngest
fast : fastest :: young : fellow
fast : fastest :: young : sixth
fast : fastest :: young : fifth
# 동시에 등장하는 정보로 의미를 인코딩하는 위험을 보여주는 예제
embeddings.compute_and_print_analogy('fast', 'fastest', 'small')
fast : fastest :: small : smallest
fast : fastest :: small : third
fast : fastest :: small : 400
# 단어 임베딩에 인코딩된 성별과 같은 보호 속성에 주의.
embeddings.compute_and_print_analogy('man', 'king', 'woman')
man : king :: woman : queen
man : king :: woman : monarch
man : king :: woman : throne
# 벡터에 인코딩된 문화적 성별 편견
embeddings.compute_and_print_analogy('man', 'doctor', 'woman')
man : doctor :: woman : nurse
man : doctor :: woman : physician
man : doctor :: woman : pregnant
위 예제에서 보듯이 오랜 문화 편견은 언어의 규칙성으로 관찰되고 단어 벡터로 나타납니다. NLP애플리케이션에서 임베딩이 인기를 끌며 더 많이 사용되는 만큼 임베딩의 편향성을 염두에 둬야 합니다.
'파이썬 > 파이토치 자연어처리' 카테고리의 다른 글
인공지능 참고 싸이트 - 학습 및 데이터 싸이트 모음 (0) | 2022.03.09 |
---|---|
CBOW 임베딩 학습하기 (0) | 2022.03.05 |
CNN의 추가 개념 (0) | 2022.02.23 |
CNN을 사용한 성씨 분류(3) - 평가, 추론, 분석 하기 (0) | 2022.02.23 |
CNN을 사용한 성씨 분류(2) - 학습 하기 (0) | 2022.02.23 |