목차
👀 코드 보기 , 🤷♀️
이 두개의 아이콘을 누르시면 코드, 개념 부가 설명을 보실 수 있습니다:)
자연어처리의 의미
Natural Language Processing (NLP)
자연어: 한국어와 영어 등 우리가 평소에 쓰는 말
자연어 처리: 컴퓨터가 우리 말을 알아듣게(이해하게)만드는 것 으로 ‘단어의 의미’를 이해시키는 게 중요
단어의 의미를 잘 파악하는 법
- 1) 시소러스를 활용한 기법
- 2) 통계 기반 기법
- 3) 추론 기반 기법(word2vec)
여기에서는 1) 과 2)만 보고 다음 포스트에서 3)을 보겠다!
1) 시소러스
국어 사전과 같이 유의어, 정의 등을 통해 ‘단어의 의미’ 를 나타내는 방법
‘상위와 하위’ 혹은 ‘전체와 부분’ 등, 더 세세한 관계까지 정의해 둘 수도 있음
[방법]
1) 모든 단어에 대한 유의어 집합을 만듦
2) 단어들의 관계를 그래프로 표현 ➡ 단어 사이의 연결을 정의 가능
3) 이 ‘단어 네트워크’를 이용하여 컴퓨터에게 단어 사이의 관계를 가르칠 수 있음.
[WordNet]
자연어 처리 분야에서 가장 유명
⛔ 문제점
- 시대 변화에 대응하기 어렵다.
- 사람을 쓰는 비용은 크다.
- 단어의 미묘한 차이를 표현할 수 없다.
밑 두 기법은 대량 텍스트 데이터에서 ‘단어의 의미’ 자동 추출
2) 통계 기반 기법
말뭉치: corpus(대량의 텍스트 데이터)
맹목적으로 수집된 텍스트데이터가 아닌 자연어 처리 연구나 애플리케이션을 염두에 두고 수집된 텍스트 데이터- 그 안에 담긴 문장들은 사람이 쓴 글
- 자연어에 대한 사람의 ‘지식’이 충분히 담겨 있음
목표: 이처럼 사람의 지식으로 가득한 말뭉치에서 자동으로, 그리고 효율적으로 그 핵심을 추출하는 것
말뭉치 전처리 구현
전처리
1) 텍스트 데이터를 단어로 분할
2) 그 분할된 단어들을 단어 ID 목록으로 변환하는 일
1️⃣ 텍스트를 단어 단위로 분할
ID로 변환시키기 위해 변환시킬 단어들을 쪼개준다
쪼개기 전 대소문자 구분을 없애고 마침표가 다른 단어에 포함되지 않게 띄어준다
👀 코드 보기
text = 'You say goodbye and I say hello.'
# 모든 문자를 소문자로 변환
text = text.lower()
# 마침표도 문자에서 띄어줌
text = text.replace('.', ' .')
# 'you say goodbye and i say hello .'
# 공백을 기준으로 분할
words = text.split(' ')
## words ['you', 'say', 'goodbye', 'and', 'i', 'say', 'hello', '.']
이제 단어들을 추출했으니 ID를 추출해보자
2️⃣ 단어에 ID를 부여
ID의 리스트로 이용할 수 있도록 한 번 더 손질
컴퓨터는 단어 자체로 이해를 못하기 때문에 단어마다 ID를 구분해 주는 것이다.
- 파이썬의 딕셔너리를 이용하여 단어 ID와 단어를 짝지어주는 대응표를 작성
👀 코드, 코드 설명 보기
word _to _id
: 단어 ➡ ID 로의 변환- key: 단어
- value: ID
id _to _word
: ID ➡ 단어 로의 변환- key: ID
- value: 단어
## 처리 문장 words ['you', 'say', 'goodbye', 'and', 'i', 'say', 'hello', '.']
#단어-> ID
word _to _id = {}
#ID-> 단어
id _to _word = {}
# 반복된 단어(say)처리
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
[코드 설명]
단어 단위로 분할된 words의 각 원소를 처음부터 하나씩 봄
단어가 word_to_id
에 들어 있지X
➡ word_to_ id
와 id_to_word
각각에 새로운 ID와 단어를 추가
- 추가 시점의 딕셔너리 길이가 새로운 단어의 ID로 설정되기 때문에 단어 ID는 0, 1, 2, … 식으로 증가
# ID와 단어의 대응표가 만들어 짐
id_to_word
## {0:'you', 1:'say', 2:'goodbye', 3:'and', 4:'i', 5:'hello', 6:'.'}
word_to_id
## {'you':0, 'say':1, 'goodbye':2, 'and':3, 'i':4, 'hello':5, '.':6}
3️⃣ ‘단어 목록’을 ‘단어 ID 목록’으로 변경
파이썬의 내포 comprehension 표기를 사용
- 1) 단어 목록에서 단어 ID 목록으로 변환
- 2) 다시 넘파이 배열로 변환
내포
리스트나 딕셔너리 등의 반복문 처리를 간단하게 쓰기 위한 기법.
(ex)
xs = [1, 2, 3, 4]라는 리스트의 각 원소를 2배하여 새로운 리스트를 만들고 싶다면 [x* 2 for x in xs] 처럼 쓰면 됨
👀 코드, 코드 설명 보기
import numpy as np
corpus = [word_to_id[w] for w in words]
corpus = np.array(corpus)
corpus array([0, 1, 2, 3, 4, 1, 5, 6])
이것으로 말뭉치를 이용하기 위한 사전 준비 완료
4️⃣ preprocess ( )라는 함수로 구현
위의 1,2,3의 과정을 나타내는 함수를 구현
👀 코드 보기
def preprocess(text):
# 1️⃣
text = text.lower()
text = text.replace('.', ' .')
words = text.split(' ')
# 2️⃣
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
# 3️⃣
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)
단어의 분산 표현
색 표현을 고유 이름 VS RGB (Red/Green/Blue)
➡ RGB 같은 벡터 표현이 색을 더 정확하게 명시
그럼 단어도 색을 RGB처럼 표현하는 것처럼 벡터로 표현이 가능할까?
➡ ‘단어의 의미’를 정확하게 파악할 수 있는 벡터 표현이 가능하다
백터로 표현하는 법
1) ‘단어의 의미’ 추출
= 단어의 분산 표현 distributional representation
단어를 고정 길이의 밀집벡터 dense vector 로 표현.
- 밀집벡터: 대부분의 원소가 0이 아닌 실수인 벡터.
2) 분포 가설(distributional hypothesis) 사용
‘단어의 의미는 주변 단어에 의해 형성된다’를 나타내기 위한 기법
- 단어 자체에는 의미가 없고, 그 단어가 사용된 ‘맥락 context ’이 의미를 형성한다는 것
- 맥락: 특정 단어를 중심에 둔 그 주변 단어
- 맥락의 크기(윈도우 크기 window size): 주변 단어를 몇 개나 포함할지
- 윈도우 크기 = 2
3) 동시발생 행렬 사용
분포 가설에 기초해 단어를 벡터로 나타내는 방법
통계 기반 statistical based: 주변 단어를 ‘세어 보는’ 방법
- 어떤 단어에 주목했을 때, 그 주변에 어떤 단어가 몇 번이나 등장하는지를 세어 집계하는 방법
1️⃣ 말뭉치 전처리
👀 코드 보기
# 말뭉치와 preprocess ( ) 함수를 사용
import sys
sys.path.append('..')
import numpy as np
from common.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: '.'}
➡ 단어 수: 총 7개
2️⃣ 각 단어의 맥락에 해당하는 단어의 빈도를 세기
윈도우 크기는 1, 단어 ID가 0인 “you”부터 시작
단어 “you”의 맥락을 세어본다.
단어 “you”의 맥락은 “say”라는 단어 하나뿐(윈도우가 1이니 바로옆의 1칸인 애들만 맥락임).
이를 표로 정리 “you”를 [0, 1, 0, 0, 0, 0, 0 ]이라는 벡터로 표현가능
3️⃣ 동시발생 행렬 (co-occurrence matrix)
계속해서 ID가 1인 “say”…에 대해서도 같은 작업 수행
이 표는 행렬의 형태를 띤다는 뜻
👀 코드 보기
# 인수들은 차례로 단어 ID의 리스트, 어휘 수, 윈도우 크기
def create_co_matrix(corpus, vocab _size, window _size=1):
corpus_size = len(corpus)
# 먼저 co _matrix를 0으로 채워진 2차원 배열로 초기화
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
👀 예상 출력 보기
C = np.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], ], dtype=np.int32)
각 단어의 벡터를 얻는 방법
- 동시발생 행렬을 사용.
- 단어를 벡터로 나타낼 수 있음.
👀 코드 보기
print(C[0]) # ID 가 0 인 단어의 벡터 표현
# [0 1 0 0 0 0 0]
print(C[4]) # ID 가 4 인 단어의 벡터 표현
# [0 1 0 1 0 0 0]
print(C[word_to_id['goodbye']])
# "goodbye" 의 벡터 표현
# [0 1 0 1 0 0 0]
4️⃣ 벡터 간 유사도
벡터 사이의 유사도를 측정하는 방법
벡터 사이의 유사도를 측정하는 방법은 다양.
코사인 유사도(cosine similarity)
단어 벡터의 유사도를 나타낼 때 자주 이용
(ex)
두 벡터 \(x = (x_1 , x_2 , x_3 , …, x_n )\)과 \(y = (y_1 , y_2 , y_3 , …, y_n )\)이 있다면,
코사인 유사도는 다음 식으로 정의
- 분자: 벡터의 내적
- 분모: 각 벡터의 노름(norm)
- 노름: 벡터의 크기를 나타낸 것
- 여기에서는 ‘L2 노름’을 계산
벡터를 정규화하고 내적을 구하는 것
- 코사인 유사도를 직관적으로 풀면 ‘두 벡터가 가리키는 방향이 얼마나 비슷한가’
- 두벡터의 방향이 완전히 같다면 코사인 유사도가 1, 완전히 반대라면 -1이 됨
👀 코드 보기
def cos_similarity(x, y):
# x 의 정규화
nx = x / np.sqrt(np.sum(x**2))
# y 의 정규화
ny = y / np.sqrt(np.sum(y**2))
return np.dot(nx, ny)
⛔ PROBLEM
‘0으로 나누기 divide by zero ’ 오류
⭕ solution
(+eps) eps(엡실론 epsilon)의 인수를 받아 더함
인수의 값을 지정하지 않으면 기본값으로 1e-8 (=0.00000001 )
- 작은 값으로 1e-8을 사용하는 이유: 이 정도 작은 값이면 일반적으로 부동소수점 계산 시 ‘반올림’되어 다른 값에 ‘흡수’.
앞의 구현에서는 이 값이 벡터의 노름에 ‘흡수’되기 때문에 대부분의 경우 eps를 더한다고 해서 최종 계산 결과에는 영향X
👀 코드 보기
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)
return np.dot(nx, ny)
단어 벡터의 유사도 구현
“you”와 “i (=I )”의 유사도를 구하는 코드
👀 코드 보기
import sys
sys.path.append('..')
from common.util import preprocess, create_co_matrix, cos_similarity
text = 'You say goodbye and I say hello.'
# 1) 단어의 벡터화
corpus, word_to_id, id_to_word = preprocess(text)
vocab_size = len(word_to_id)
# 3️⃣ 동시발생 행렬
C = create_co_matrix(corpus, vocab_size)
c0 = C[word_to_id['you']] # "you" 의 단어 벡터
c1 = C[word_to_id['i']] # "i" 의 단어 벡터
# 4️⃣ 벡터간의 유사도
print(cos_similarity(c0, c1)) # 0.7071067691154799
(결과)
유사도가 꽤 높다
유사 단어의 랭킹 표시 구현
❶ 검색어의 단어 벡터를 꺼낸다
❷ 코사인 유사도 계산
❸ 코사인 유사도를 기준으로 내림차순으로 출력
- 1) similarity 배열에 담긴 원소의 인덱스를 내림차순으로 정렬
- 2) 상위 원소들을 출력
👀 코드 보기
def most_similar(query, word_to_id, id_to_word, word_matrix, top=5):
# ❶ 검색어의 단어 벡터를 꺼낸다
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]
# ❷ 코사인 유사도 계산
# 검색어의 단어 벡터와 다른 모든 단어 벡터와의 코사인 유사도를 각각 구한다.
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(): ## 🤷♀️argsort?!
if id _to _word[i] == query:
continue
print(' %s: %s' % (id_to_word[i], similarity[i]))
count += 1
if count >= top:
return
🤷♀️argsort 알아보기
argsort( ): 배열 인덱스의 정렬을 바꾸는데 사용
- 메서드는 넘파이 배열의 원소를 오름차순으로 정렬
- 반환값은 배열의 인덱스
>>> x = np.array([100, -20, 2])
# 작은 순서
>>> x.argsort() array([1, 2, 0])
# 큰 순서
>>> (-x).argsort() array([0, 2, 1])
(함수 사용)
“you”를 검색어로 지정해 유사한 단어들을 출력
👀 코드 보기
import sys
sys.path.append('..') 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
통계기반 기법 개선
상호정보량 PMI
동시발생 행렬의 원소: 두 단어가 동시에 발생한 횟수
[PROBLEM]
‘발생’ 횟수라는 것은 좋은 특징X
[SOLVE]
점별 상호정보량 (Pointwise Mutual Information)(PMI).
- 확률 변수 x와 y에 대해 다음 식으로 정의
- P (x): x가 일어날 확률
- P (y): y가 일어날 확률
- P (x, y): x와 y가 동시에 일어날 확률
이 PMI 값이 높을수록 관련성이 높다는 의미
- (적용) P (x)는 단어 x가 말뭉치에 등장할 확률
- C: 동시발생 행렬
- C(x, y): 단어 x와 y가 동시발생하는 횟수
- C(x), C(y): 각각 단어 x와 y의 등장 횟수
- N: 말뭉치에 포함된 단어 수
[EX]
말뭉치의 단어 수(N)를 10,000이라 하고,
“the”와 “car”와 “drive”가 각 1,000번, 20번, 10번 등장
“the”와 “car”의 동시발생 수는 10회,
“car” 와 “drive”의 동시발생 수는 5회
➡ 동시발생 횟수 관점: “car”는 “drive”보다 “the”와 관련이 깊다
➡ PMI 관점:PMI를 이용하면 “car”는 “the”보다 “drive”와의 관련성이 강해짐 (단어가 단독으로 출현하는 횟수가 고려 되었기 때문.)
⛔ PROBLEM
바로 두 단어의 동시발생 횟수가 0이면 \(log_2 0 = -∞\)
⭕ SOLVE
양의 상호정보량 Positive PMI (PPMI)사용
- PMI가 음수일 때는 0으로 취급.
[함수 구현]
동시발생 행렬을 PPMI 행렬로 변환하는 함수.
👀 코드 보기
def ppmi(C, verbose=False, eps=1e-8):
M = np.zeros _like(C, dtype=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)
M[i, j] = max(0, pmi)
if verbose:
cnt += 1
if cnt % (total//100) == 0:
print('%.1f%% 완료 ' % (100*cnt/total))
return M
- C: 동시발생 행렬
- verbose: 진행상황 출력 여부를 결정하는 플래그
- 큰 말뭉치를 다룰 때 verbose=True로 설정하면 중간중간 진행 상황을 알려줌
- 전제 코드: 동시발생 행렬에 대해서만 PPMI 행렬을 구할 수 있도록 하고자 단순화해 구현.
- 단어 x와 y가 동시에 발생하는 횟수를 C (x, y )라 했을 때,(근삿값 구함)구현
[함수 사용 구현]
동시발생 행렬을 PPMI 행렬로 변환
👀 코드 보기
import sys
sys.path.append('..')
import numpy as np 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)
# 유효 자릿수를 세 자리로 표시
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. ]]
⛔ PROBLEM
- 말뭉치의 어휘 수가 증가함에 따라 각 단어 벡터의 차원 수도 증가
- EX) 말뭉치의 어휘 수가 10만 = 그 벡터의 차원 수 10만.
- 비현실적
- 원소 대부분이 0
- 벡터의 원소 대부 분이 중요하지 않음
- 각 원소의 ‘중요도’가 낮다
- 이런 벡터는 노이즈에 약하고 견고하지 못함
⭕ SOLVE
벡터의 차원 감소 LDA