AI tech

밑바닥부터 시작하는 딥러닝2 정리 Chapter 2 - 자연어와 단어의 분산 표현

도리컴 2024. 4. 16. 01:48
반응형

 

 

 

개요

  • NLP의 본질적 문제 : 컴퓨터가 우리의 말을 알아듣게(이해하게) 만들기
  • 딥러닝 등장 이전의 고전적인 기법 살펴볼 예정
  • 파이썬으로 텍스트 처리를 위한 사전 준비

2.1 자연어 처리란

  • 자연어 처리(NLP)가 추구하는 목표 : 사람의 말을 컴퓨터가 이해하도록 만들기
  • → 우리에게 도움이 되는 일을 수행할 수 있도록
  • 일반적인 프로그래밍 언어는 기계적이고, 고정적임 → 딱딱한 언어
  • 자연어 : 영어, 한국어 등 → 부드러운 언어 / 뜻이 애매하거나, 의미나 형태가 유연하게 바뀜
  • 자연어 처리 예 : IME(입력기 전환), 문장 자동요약, 감정분석, 질의응답, 검색 엔진, 기계 번역 등

단어의 의미

  • 의미의 최소 단위가 단어
  • 그래서 ‘단어의 의미’를 컴퓨터에게 이해시키는 것이 중요
  • 세 가지 기법
    • 시소러스 기법 - 유의어 사전
    • 통계 기반 기법 - 통계 정보로부터 단어 표현
    • 추론 기반 기법(word2vec) (다음 장에 다룸) - 신경망을 활용한 추론

2.2 시소러스(thesaurus)

  • 유의어 사전으로, ‘동의어’나 ‘유의어’가 한 그룹으로 분류되어 있음
  • 단어 사이의 ‘상위, 하위’ 또는 ‘전체와 부분’ 등 더 세세한 관계 정의한 경우도 있음→ 모든 단어에 대한 유의어 집합 생성 후, 관계를 그래프로 표현하여 연결을 정의할 수 있음

  • 이렇게 만든 ‘단어 네크워크’를 통해 컴퓨터에게 단어 사이의 관계를 가르칠 수 있음

WordNet

  • NLP분야에서 가장 유명한 시소러스(1985부터 구축하기 시작한 전통 있는 시소러스)
  • 많은 연구와 다양한 NLP 앱에서 활용
  • 유의어를 얻거나 ‘단어 네트워크’를 이용할 수 있음
  • → 단어 사이의 유사도 구할 수 있음
  • 부록 B에 파이썬 구현 맛보기 나옴(정확하게는 NLTK 모듈 활용)

시소러스의 문제점

  • 사람이 이처럼 수작업으로 레이블링 하는 방식엔 결점이 따를 수 밖에 없음

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

  • 없어지고, 새로 생기는 말 대응 어렵
  • 의미가 변하는 언어도 있음

사람을 쓰는 비용은 크다.

  • 예 : 현존하는 영어 단어 수는 1000만 개(WordNet에 등록된 단어는 20만 개)

단어의 미묘한 차이를 표현할 수 없다.

  • 예 : ‘빈티지’와 ‘레트로’ → 의미가 같지만, 용법은 다름

그래서! 아래 두 기법이 나온 것 → 대량의 텍스트 데이터로부터 단어의 의미를 자동 추출!

→ 사람 개입을 최소화하고 텍스트 데이터만으로 결과를 얻어내는 방향으로 패러다임이 바뀌는 중

2.3 통계 기반 기법

  • corpus(말뭉치) 등장 → NLP연구나 앱을 염두에 두고 수집된 대량의 텍스트 데이터
  • 걍 텍스트 데이터지만, 여기엔 사람의 ‘지식’이 충분히 담겨있다 볼 수 있음
  • 목표 : 사람의 지식이 동반된 말뭉치에서 자동으로, 효율적으로 핵심을 추출하는 것

파이썬으로 말뭉치 전처리하기

  • NLP에 사용되는 유명한 말뭉치 : Wikipedia, Google News
  • 여기서는 일단 문장 하나로 이뤄진 단순한 텍스트 사용
  • 전처리 진행 : 텍스트 → 단어로 분할 / 단어를 → 단어 ID 목록으로 변환

단계 별 진행

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', '.']
  • 정규표현식(regular expression) 쓰면 더 나을 수 있음
    • re.split(’(\W+)?’, text) → 단어 단위로 분할 가능
  • 이제 단어 목록 형태로 이용 가능
    • 이후 단어에 ID 부여, ID 리스트로 이용할 수 있도록 한 번 더 손질
  • 이를 위해, 파이썬 딕셔너리 이용 → 단어, 단어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로
    		id_to_word[new_id] = word  # ID에서 단어로
    ​

  • 단어 목록을 ID 목록으로 바꿔보기
  • import numpy as np
    corpus = [word_to_id[w] for w in words]  # 내포(comprehension) 표기 이용
    corpus = np.array(corpus)
    corpus  # array([0, 1, 2, 3, 4, 1, 5, 6])
    

 

사전 준비 끝! → preprocess()라는 함수로 만들기

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로
            id_to_word[new_id] = word  # ID에서 단어로

    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)
  • 다음 목표 : 말뭉치를 사용해서 ‘단어의 의미’ 추출하기 → 통계 기반 기법(단어를 벡터로 표현)

단어의 분산 표현

  • 색 표현을 예시로 들어봄
    • 코발트 블루로 표현할 수도 있지만, RGB라는 3가지 성분으로 표현도 가능
    • 핵심 : RGB와 같은 벡터 표현이 색을 더 정확하게 명시할 수 있다!
      • 어느 색 계열인지도 한 눈에 보임 → 관련성 표현, 정량화 모두 벡터 표현이 이득
  • 이처럼, ‘단어’도 벡터로 표현할 수 있다면?
    • 원하는 것 : 단어의 의미를 정확하게 파악할 수 있는 벡터 표현→ 보통 고정 길이의 dense vector로 표현 함(원소가 0이 아닌 실수인 벡터)
    • → 이를 단어의 ‘분산 표현(distributional representation)’ 이라 함

분포 가설

  • 단어를 벡터로 표현하는 수많은 연구에서 단 하나의 핵심 아이디어!
    • ‘단어의 의미는 주변 단어에 의해 형성된다~’ ⇒ 이게 분포 가설(distributional hypothetis)
    • 단어 자체에는 의미가 없고, 그 단어가 사용된 맥락(context)이 의미를 형성!
  • 맥락이라는 말을 자주 쓸 예정
    • 맥락 : 특정 단어를 중심으로, 주변에 놓인 단어를 가리킬 때 쓸 거
    • 예 : 윈도우 크기가 2인 context / 맥락의 크기 = 윈도우 크기

동시발생 행렬

  • 분포 가설을 기반으로 단어를 벡터로 나타내보자
  • 어떤 단어에서, 그 주변에 어떤 단어가 몇 번이나 등장하는지 집계
    • 이를 ‘통계 기반(statistical based) 기법’ 이라 함
      import sys
      sys.path.append('..')
      import numpy as nnp
      from commom.util import preprocess
      
      text = 'You say goodbye and I say hello.'
      corpus, word_to_id, id_to_word = preprocess(text)
      
      print(corpus)  # [0 1 2 3 4 1 5 6]
      print(id_to_word)  # {0: 'you, 1: 'say', 2: 'goodbye', 3: 'and', 4: 'i', 5: 'hello', 6: '.'}
      
      ​
    • you의 맥락 살펴보기(윈도우 크기 1로 정함) → 동시발생 행렬 만드는 과정
    • say라는 단어 하나 뿐임→ 이렇게 되면, “you”는 [0, 1, 0, 0, 0, 0, 0]이라는 벡터로 표현
    • → 이렇게 되면, “you”는 [0, 1, 0, 0, 0, 0, 0]이라는 벡터로 표현
    • → “say”에 대해서도 같은 작업 수행

  •  
    • → 이제 모든 단어 수행 하면?
    • → 모든 단어에 대해 동시발생하는 단어를 표에 정리한 것 / 각 행 : 해당 단어를 표현한 벡터

  •  
    • → 이 표를 동시발생 행렬(co-occurrence maxrix)이라고 함
  • 코드로 동시발생 행렬 만들어주는 함수 구현
    • 파라미터 : 단어 ID 리스트, 어휘 수, 윈도우 크기
    • 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):
      		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
      
    • co_matrix를 2차원 배열로 초기화
    • 모든 단어 각각에 대해, 윈도우에 포함된 주변 단어를 세어 나감
    • 말뭉치가 아무리 커져도 자동으로 동시발생 행렬을 만들어 준다~

 

벡터 간 유사도

  • 내적이나 유클리드 거리 등 다양
  • 자주 이용 → 코사인 유사도
  • 코사인 유사도 : 분자에는 내적, 분모에는 각 벡터의 노름(L2 norm)
  • 핵심 : 벡터 정규화 + 내적 구하기

유사 단어의 랭킹 표시

  • 말뭉치 크기가 너무 작으면 직관과는 거리가 먼 결과가 나옴
  • 이걸로, 통계 기반 기법의 ‘기본’을 끝마침

2.4 통계 기반 기법 개선하기

  • 개선 작업 + 실용적인 말뭉치를 통해 진짜 단어의 분산표현 얻어볼 예정

상호정보량

  • 발생 횟수라는 건 사실 그리 좋은 특징이 아님
  • 고빈도 단어가 the, car 등 → 동시발생은 많지만, 이렇게 따지면 car은 drive보다 the와의 관련성이 강하다고 나타남
  • 이 문제를 해결하기 위해 점별 상호정보량(PMI, Pointwise Mutual Information) 사용
  • 확률 변수 x, y에 대해 다음 식으로 정의

  • → x, y를 각각 단어에 대응시키면, P(x, y) 는 동시발생 확률을 나타낼 수 있음

PMI의 문제

  • 두 단어의 동시발생 횟수가 0이면, 오류(log2 0은 -무한 이므로)
  • 이를 해결하기위해 양의 상호정보량(PPMI, Positive PMI) 사용

PPMI에도 문제가 있음

  • 말뭉치 어휘 수에 따라 각 단어 벡터 차원 수도 증가한다는 점
  • 각 원소의 ‘중요도’도 낮음 → 노이즈에 약하고 견고하지 못하다~
  • 이를 해결하기 위해 차원 감소 수행

차원 감소

  • 핵심 : 중요한 정보를 최대한 유지하면서 줄이는 것
  • 데이터 분포를 고려해 중요한 ‘축’을 찾음

  • 각 데이터점 값은 새로운 축으로 사영된 값으로 변함
  • 다차원 데이터에 대해서도 수행 가능
  • 우리는 특잇값분해(SVD, Singular Value Decomposition) 이용 예정
  • 임의의 행렬을 세 행렬의 곱으로 분해

  • → U, V : 직교행렬(열벡터 서로 직교), S : 대각행렬(대각성분 외 모두 0) 

SVD에 의한 차원 감소

  • SVD : Singular Value Decomposition, 특잇값분해
  • 냅다 코드
  • import sys
    sys.path.append('..')
    import numpy as np
    import matpotlib.pyplot as plt
    from common.util import preprocess, create_to_matrix, ppmi
    
    text = 'you say goodbye and I say hello.'
    corpus, word_to_id, id_to_word = preprocess(text)
    vocab_size = len(id_to_word)
    C = create_co_matrix(corpus, vocab_size, window_size=1)
    W = ppmi(C)
    
    # SVD
    U, S, V = np.linalg.svg(W)
    
    print(C[0])  # 동시발생 행렬 -> [0 1 0 0 0 0 0]
    print(W[0])  # PPMI 행렬 -> [0.  1.807  0.  0.  0.  0.  0.  ]
    print(U[0])  # SVD -> [3.409e-01 -1.11e0-16 -1.205e-01 -4.441e-16 0.000e+00 -9.323e-01 2.226e-16
    ​
    • → SVD에 의해 변환된 Dence vector 표현은 변수 U에 저장됨
    • 밀집벡터 U의 차원을 감소시키려면? → 단순히 처음의 두 원소를 꺼내면 됨
    • print(U[0, :2])
      
  • 각 단어를 2차원 벡터로 표현 후 그래프로 그려보기
    for word, word_id in word_to_id.items():
    	plt.annotate(word, (U[word_id, 0], U[word_id, 1]))
    
    plt.scatter(U[:, 0], U[:, 1], alpha=0.5)
    plt.show()
    

  • → goodbay, hello 가까이, you, i 가까이 있음(우리의 직관과 비교적 비슷)
    • PTB 데이터셋이라는 더 큰 말뭉치 사용해보자!
  • 행렬크기 N에 대해, SVD 계산은 O(N^3) 걸림
  • → Truncated SVD 등의 더 빠른 기법을 실제 이용함(특이값이 작은 것은 버리는 방식)

PTB 데이터셋

  • 본격적인 말뭉치로 실험 시작
  • 펜 트리뱅크(Penn Treebank, PTB)
  • 주어진 기법의 품질 측정용 벤치마크로 자주 사용됨
  • 한 문장이 하나의 줄로 저장되어 있음
  • 코드 예시
  • import sys
    sys.path.append('..')
    from dataset import ptb
    
    corpus, word_to_id, id_to_word = ptb.load_data('train') # 데이터 읽어들이기
    
    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'])
    
  • 실행 결과

PTB 데이터셋 평가

  • 통계 기반 기법 적용
  • 고속 SVD 이용을 위해 sklearn 모듈 설치
import sys
sys.path.append('..')
import numpy as np
from common.util import most_similar, create_co_matrix, ppmi
from dataset import ptb

window_size = 2
wordvec_size = 100

corpus, word_to_id, id_to_word = ptb.load_data('train')
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
    U, S, V = randomized_svd(W, n_components=wordvec_size, n_iter=5, random_state=None)
except ImportError:
    # SVD (느림~)
    U, S, V = np.linalg.svd(W)

word_vecs = U[:, :wordvec_size]

querys = ['you', 'year', 'car', 'toyota']
for query in querys:
    most_similar9query, word_to_id, id_to_word, word_vecs, top=5)
  • 실행 결과

  • → you를 보면, ‘i’, ‘we’가 상위(인칭대명사)
    • 의미 혹은 문법적인 관점에서 비슷한 단어들이 가까운 벡터로 나타남
    • → 우리의 직관과 비슷!
  • 이제 ‘단어의 의미’를 벡터로 인코딩하는데 성공!
    • 맥락에 속한 단어의 등장 횟수 셈 → PPMI 행렬로 변환
    • SVD로 차원 감소 → 단어의 분산 표현(고정 길이의 밀집 벡터)
    • 대규모 말뭉치를 쓰면 단어 분산 표현 품질도 더 좋아질 것임~

2.5 정리

  • 컴퓨터에게 ‘단어의 의미’ 이해시키기 위주로 진행했음
  • 시소러스 기법 → 통계 기반 기법 순으로 살펴봄
  • 시소러스는 사람이 갈리기에 비효율적이고 표현력에 한계가 있다~
  • 통계 기반 기법에 따른 분산 표현은 의미가 비슷한 단어들이 벡터공간에서도 서로 가까이 모여 있더라!
  • 전처리 함수 cos_similarity(), most_similar() 함수 구현했었음 → 다음 장 이후에도 사용

반응형