2.1 자연어 처리란
Natural Language Processing(NLP): 우리의 말을 컴퓨터에게 이해시키기 위한 기술
2.1.1 단어의 의미
우리의 말은 '문자'로 구성, 말의 의미는 '단어'로 구성
단어 : 의미의 최소 단위
2.2 시소러스
: (기본적으로는) 유의어 사전
자연어 처리에 이용되는 시소러스에서는 단어 사이의 '상위와 하위' 혹은 '전체와 부분' 등 더 세세한 관계까지 정의
2.2.1 WordNet
: 자연어 처리 분야에서 가장 유명한 시소러스
- 유의어를 얻거나 단어 네트워크 이용 가능
- 단어 네크워크를 사용해 단어 사이의 유사도 구할 수 있음
2.2.2 시소러스의 문제점
- 사람이 수작업으로 레이블링하는 방식
- 결점
1) 시대 변화에 대응하기 어렵다.
2) 사람을 쓰는 비용은 크다.
3) 단어의 미묘한 차이를 표현할 수 없다.
-> 통계 기반 기법과 추론 기반 기법을 사용하여 텍스트 데이터로부터 단어의 의미 자동으로 추출
2.3 통계 기반 기법
말뭉치 corpus : 대량의 텍스트 데이터, 자연어 처리 연구나 애플리케이션을 염두에 두고 수집된 텍스트 데이터
2.3.1 파이썬으로 말뭉치 전처리하기
전처리 preprocessing : 텍스트 데이터를 단어로 분할하고 그 분할된 단어들을 단어 ID목록으로 변환하는 일
##코퍼스 전처리 함수
def preprocess(text):
text = text.lower()
text = text.replace('.', ' .')
words = text.split(' ') #공백기준으로 분할
word_to_id = {} #단어에서 단어ID로의 변환
id_to_word = {} #단어ID에서 단어로의 변환
for word in words:
if word not in word_to_id: #새로운 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
text = 'You say goodbye and I say hello'
corpus, word_to_id, id_to_word = preprocess(text)
print(corpus) #단어 ID 목록
print(word_to_id)
print(id_to_word)
-> 결과
[0 1 2 3 4 1 5]
{'you': 0, 'say': 1, 'goodbye': 2, 'and': 3, 'i': 4, 'hello': 5}
{0: 'you', 1: 'say', 2: 'goodbye', 3: 'and', 4: 'i', 5: 'hello'}
2.3.2 단어의 분산 표현
: 단어의 의미를 정확하게 파악할 수 있는 벡터 표현
- 단어를 고정 길이의 밀집벡터로 표현
- 밀집벡터 : 원소가 0이 아닌 실수인 벡터
- 예) 3차원 분산 표현 : [0.21, -0.45, 0.83]
2.3.3 분포 가설
: 단어의 의미는 주변 단어에 의해 형성된다
- 단어 자체에는 의미가 없고, 그 단어가 사용된 맥락이 의미를 형성
- 맥락 : 주목하는 단어 주변에 놓인 단어
- 맥락의 크기 : 윈도우 크기 (window size)

2.3.4 동시발생 행렬
통계 기반 기법 : 어떤 단어에 주목했을 때, 그 주변에 어떤 단어가 몇 번이나 등장하는지를 세어 집계하는 방법
-> 동시발생 행렬
-> 각 단어의 벡터 표현
##말뭉치로부터 동시발생 행렬 만들어주는 함수
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)
for idx, word_id in enumerate(corpus): #인덱스와 word_id(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
if right_idx < corpus_size:
right_word_id = corpus[right_idx]
co_matrix[word_id, right_word_id]+=1
return co_matrix
2.3.5 벡터 간 유사도
- 유사도를 구하는 방법은 벡터의 내적이나 유클리드 거리 등 다양하지만 코사인 유사도를 자주 이용
- 코사인 유사도 : 두 벡터가 가리키는 방향이 얼마나 비슷한가 (완전히 같으면 1, 반대이면 -1)
- 분자에는 벡터의 내적
- 분모에는 각 벡터의 노름 norm
- 노름 : 벡터의 크기 (L2노름 : 벡터의 각 원소를 제곱해 더한 후 다시 제곱근을 구해 계산)
-> 벡터를 정규화하고 내적을 구하는 것이 핵심
##코사인 유사도 함수
def cos_similarity(x,y, eps=1e-8): #0으로 나누는 오류 막기위해
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의 유사도 구하는 코드
from common.util import preprocess, create_co_matrix, cos_similarity
text = 'You say goodbye and I say hello.'
corpus, word_to_id, id_to_word = preprocess(text)
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.7071067691154799
2.3.6 유사 단어의 랭킹 표시
: 어떤 단어가 검색어로 주어지면 그 검색어와 비슷한 단어를 유사도 순으로 출력하는 함수
##유사 단어의 랭킹 표시
def most_similar(query, word_to_id, id_to_word, word_matrix, top=5): #query :검색어(단어)
#1. 검색어를 꺼낸다
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]
#2. 코사인 유사도 계산
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)
#3. 코사인 유사도를 기준으로 내림차순으로 출력
count = 0
for i in (-1*similarity).argsort():
#argsort() : 넘파이 배열의 원소를 오름차순으로 정렬 후 배열의 인덱스 반환
if id_to_word[i] == query:
continue
print(' %s: %s' % (id_to_word[i], similarity[i]))
count +=1
if count >= top:
return
##you를 검색어로 유사 단어 출력
from common.util import preprocess, create_co_matrix, most_similar
text = 'You say goodbye and I say hello.'
corpus, word_to_id, id_to_word = preprocess(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)
-> [query] you
goodbye: 0.7071067691154799
i: 0.7071067691154799
hello: 0.7071067691154799
say: 0.0
and: 0.0
2.4 통계 기반 기법 개선하기
2.4.1 상호정보량
동시발생 행렬의 원소 - 두 단어가 동시에 발생한 횟수 나타냄
-> 좋은 특징이 아님
예) the와 car, 단순히 등장 횟수만을 본다면 car는 drive보다 the와 관련성이 강하다고 나옴
=> 점별 상호정보량 pointwise mutual information (PMI) 척도 사용
- P(x) : x가 일어날 확률
- P(y) : y가 일어날 확률
- P(x, y) : x와 y가 동시에 일어날 확률
-> PMI 값이 높을수록 관련성이 높음
- 동시발생 행렬로부터 PMI 구하는 식
- N : 말뭉치에 포함된 단어 수
- 단어가 단독으로 출현하는 횟수가 분모에 고려되기 때문에 the의 PMI점수는 낮아짐
- 두 단어의 동시발생 횟수가 0이면 log20 = -무한이 되기 때문에 양의 상호정보량 (PPMI) 사용
- PMI가 음수일 때는 0으로 취급
- 단어 사이의 관련성을 0 이상의 실수로 나타낼 수 있음
##양의 상호정보량 PPMI
#동시발생 행렬을 PPMI 행렬로 변환하는 함수
def ppmi(C, verbose=False, eps = 1e-8):
M = np.zeros_like(C, dtypes=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)
#음의 무한대로 빠지지 않기 위해 eps 더해줌
M[i,j] = max(0, pmi) #음수일 때는 0으로 취급
if verbose: #진행상황 출력
cnt +=1
if cnt % (total//100+1) ==0:
print('%.1f%% 완료' % (100*cnt/total))
return M
##변환해보기
from common.util import preprocess, create_co_matrix, cos_similarity, ppmi
text = 'You say goodbye and I say hello.'
corpus, word_to_id, id_to_word = preprocess(text)
vocab_size = len(word_to_id)
C = create_co_matrix(corpus, vocab_size)
W = ppmi(C) #각 원소는 0이상의 실수
np.set_printoptions(precision=3) #유효 자릿수를 세 자리로 표시
print('동시발생 행렬')
print(C)
print('-'*50)
print('PPMI')
print(W)
->
동시발생 행렬
[[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]]
--------------------------------------------------
PPMI
[[0. 1.807 0. 0. 0. 0. 0. ]
[1.807 0. 0.807 0. 0.807 0.807 0. ]
[0. 0.807 0. 1.807 0. 0. 0. ]
[0. 0. 1.807 0. 1.807 0. 0. ]
[0. 0.807 0. 1.807 0. 0. 0. ]
[0. 0.807 0. 0. 0. 0. 2.807]
[0. 0. 0. 0. 0. 2.807 0. ]]
- 말뭉치의 어휘 수가 증가함에 따라 각 단어 벡터의 차원 수도 증가한다는 문제
- 원소 대부분이 0 -> 각 원소의 중요도가 낮음
- 노이즈에 약하고 견고하지 못하다는 약점
-> 벡터의 차원 감소
2.4.2 차원 감소
dimentionality reduntion : 문자 그대로 벡터의 차원을 줄이는 방법
- 중요한 정보는 유지하면서 줄이는 게 핵심
- 데이터의 분포를 고려해 중요한 축을 찾는 일을 수행
- 원소 대부분이 0인 행렬 : 희소행렬
- 차원 감소의 핵심은 희소벡터에서 중요한 축을 찾아내어 더 적은 차원으로 다시 표현하는 것
- 차원 감소의 결과로 원래의 희소벡터는 대부분이 0이 아닌 값으로 구성된 밀집벡터로 변환
차원을 감소시키는 방법은 여러가지, 우리는 특잇값분해 singular value decomosition (SVD) 이용
- 임의의 행렬 x를 U, S, V라는 세 행렬의 곱으로 분해
- U, V는 직교행렬 (orthogonal matrix) : 열벡터는 서로 직교
- S는 대각행렬 (diagonal matrix) : 대각성분 외에는 모두 0인 행렬
- S의 대각성분에는 특잇값이 큰 순서로 나열
- 특잇값 : 해당 축의 중요도 -> 중요도가 낮은 원소를 깎아냄
- 행렬S에서 특잇값이 작다면 행렬 U에서 여분의 열벡터를 깎아내어 원래의 행렬을 근사
-> 행렬 X의 각 행에는 해당 단어 ID의 단어 벡터가 저장 -> 단어 벡터가 행렬 U라는 차원 감소된 벡터로 표현
2.4.3 SVD에 의한 차원 감소
##SVD에 의한 차원 감소
import matplotlib.pyplot as plt
text = 'You say goodbye and I say hello.'
corpus, word_to_id, id_to_word = preprocess(text)
vocab_size = len(word_to_id)
C = create_co_matrix(corpus, vocab_size)
W = ppmi(C)
#SVD - 넘파이의 linalg모듈이 제공하는 svd메소드 사용
U, S, V = np.linalg.svd(W)
print(C[0]) #동시발생 행렬
print(W[0]) #PPMI 행렬
print(U[0]) #SVD
print(U[0, :2]) #각 단어를 2차원 벡터로 줄임
-> 희소벡터인 W가 밀집벡터U로 변함
[0 1 0 0 0 0 0]
[0. 1.807 0. 0. 0. 0. 0. ]
[ 3.409e-01 -1.110e-16 -1.205e-01 -4.163e-16 -9.323e-01 -1.110e-16
-2.426e-17]
[ 3.409e-01 -1.110e-16]
2.4.4 PTB 데이터셋
: 펜 트리뱅크 penn Treddbank (PTB) 말뭉치
- 주어진 기법의 품질을 측정하는 벤치마크로 자주 이용
- 이 책에서는 각 문장을 연결한 하나의 큰 시계열 데이터로 취급할 것
- 각 문장 끝에 <eos>라는 특수 문자 삽입
##PTB데이터셋
import sys
sys.path.append('..')
from dataset import ptb
corpus, word_to_id, id_to_word = ptb.load_data('train')
#데이터 읽기, train/test/valid 중 하나 지정 가능
print('말뭉치 크기 :', len(corpus))
print('corpus[:30]', corpus[:30])
print()
print('id_to_word[0]:', id_to_word[0])
print('id_to_word[1]:', id_to_word[1])
print('id_to_word[2]:', id_to_word[2])
print()
print("word_to_id['car']:", word_to_id['car'])
print("word_to_id['happy']:", word_to_id['happy'])
print("word_to_id['lexus']:", word_to_id['lexus'])
->
말뭉치 크기 : 929589
corpus[:30] [ 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
24 25 26 27 28 29]
id_to_word[0]: aer
id_to_word[1]: banknote
id_to_word[2]: berlitz
word_to_id['car']: 3856
word_to_id['happy']: 4428
word_to_id['lexus']: 7426
2.4.4 PTB 데이터셋 평가
- 큰 행렬에 SVD 적용해야 하므로 고속 SVD 이용
- sklearn 모듈
##PTB 데이터셋 평가
window_size = 2
wordvec_size = 100
corpus, word_to_id, id_to_word = ptb.load_data('train')
#데이터 읽기, train/test/valid 중 하나 지정 가능
vocab_size = len(word_to_id)
print('동시발생 수 계산...')
C = create_co_matrix(corpus, vocab_size, window_size)
print('PPMI 계산...')
W = ppmi(C, verbose = True)
print('SVD 계산...')
try:
# truncated SVD
from sklearn.utils.extmath import randomized_svd
#sklearn의 고속SVD, 무작위 수를 사용, 특잇값이 큰 것들만 계산하여 기본적인 SVD보다 훨씬 빠름
U, S, V = randomized_svd(W, n_components=wordvec_size, n_iter=5,
random_state=None)
except:
# SVD
U, S, V = np.linalg.svd(W)
word_vecs = U[:, :wordvec_size]
querys = ['you', 'year', 'car', 'toyota']
for query in querys:
most_similar(query, word_to_id, id_to_word, word_vecs, top=5)
->
[query] you
i: 0.6880272626876831
do: 0.6079217195510864
we: 0.5786764025688171
anybody: 0.5776432752609253
else: 0.5051592588424683
[query] year
earlier: 0.6934074759483337
month: 0.6774067282676697
last: 0.636110246181488
next: 0.6313115954399109
quarter: 0.608620822429657
[query] car
auto: 0.717846691608429
luxury: 0.6236147284507751
truck: 0.5415768027305603
corsica: 0.5339930057525635
vehicle: 0.5269615054130554
[query] toyota
motor: 0.7085002660751343
honda: 0.6650682687759399
nissan: 0.6501489877700806
motors: 0.6489919424057007
lexus: 0.5389389991760254
결과 매번 다름 (Truncated SVD는 무작위 수를 사용하기 때문)
2.5 정리
- WordNet 등의 시소러스를 이용하면 유의어를 얻거나 단어 사이의 유사도를 측정하는 등 유용한 작업을 할 수 있다.
- 시소러스 기반 기법은 시소러스를 작성하는 데 엄청난 인적 자원이 든다거나 새로운 단어에 대응하기 어렵다는 문제가 있다.
- 현재는 말뭉치를 이용해 단어를 벡터화하는 방식이 주로 쓰인다.
- 최근의 단어 벡터화 기법들은 대부분 '단어의 의미는 주변 단어에 의해 형성된다'는 분포 가설에 기초한다.
- 통계 기반 기법은 말뭉치 안의 각 단어에 대해서 그 단어의 주변 단어의 빈도를 집계한다 (동시발생 행렬).
- 동시발생 행렬을 PPMI행렬로 변환하고 다시 차원을 감소시킴으로써, 거대한 '희소벡터'를 작은 '밀집벡터'로 변환할 수 있다.
- 단어의 벡터 공간에서는 의미가 가까운 단어는 그 거리도 가까울 것으로 기대된다.
책 참고 : 밑바닥부터 시작하는 딥러닝 (한빛미디어)
'딥러닝 > 밑바닥부터 시작하는 딥러닝 2' 카테고리의 다른 글
6장 : 게이트가 추가된 RNN (0) | 2023.09.26 |
---|---|
5장 : 순환 신경망(RNN) (0) | 2023.09.26 |
4장 : word2vec 속도 개선 (0) | 2023.09.23 |
3장 : word2vec (0) | 2023.09.21 |
1장 : 신경망 복습 (0) | 2023.09.20 |