단어의 의미를 파악하는 기법으로 (1) 사람이 수작업으로 레이블링하는 시소러스를 활용한 기법과 텍스트 데이터로부터 단어의 의미를 자동으로 추출하는 (2) 통계 기반 기법, (3) 추론 기반 기법이 있다.
1. 통계 기반 기법 개선
1.1 동시발생 행렬의 문제점
■ 동시발생 행렬의 각 행의 원소는 윈도우 크기를 기준으로 주변 단어 빈도수를 나타낸 것이다.
■ 따라서 윈도우 크기 조건을 만족하는 단어 중, 서로 관련이 없는 단어 간의 빈도가 높다면, 두 단어 벡터의 유사다고 높게 계산된다는 문제점이 있다.
■ 예를 들어 말뭉치의 단어 수가 10,000개인데, 'you'와 'goodbye'가 5,000번, 'you'와 'i'가 10번 등장한다면,
- 서로 관련이 적은 'you'와 'goodbye'의 동시발생 횟수는 많고, 오히려 관련이 깊은 'you'와 'i'의 동시발생 횟수가 적게 나타난다.
- 따라서 빈도수만 고려한다면, 'i'보다 'goodbye'가 고빈도 단어라서 'you'와 'i'보다는 'you'와 'goodbye'의 관련성이 더 강하다는 결과를 얻게 된다.
■ 이런 문제를 해결하기 위해 '점별 상호정보량('Pointwise Mutual Information, PMI)라는 척도를 사용한다.
1.2 점별 상호정보량
■ 점별 상호정보량(PMI)는 확률변수 \( P(x): \) \( x \)가 일어날 확률, \( P(y): \) \( y \)가 일어날 확률이 존재할 때, 두 확률변수 사이의 상호 관련성을 측정하는 지표이다. \[ \text{PMI}(x, y) = \log_2 \frac{P(x, y)}{P(x) P(y)} \] ■ 분자의 \( P(x, y) \)는 \( x \)와 \( y \)가 동시에 일어날 확률이다. 따라서 PMI 값이 높으면 상호 관련성이 높다는 것을 의미한다.
■ 이 개념을 자연어에 적용한다면 말뭉치가 표본공간일 때 \( P(x) \)는 단어 \( x \)가 말뭉치에 등장할 확률, \( P(y) \)는 단어 \( y \)가 말뭉치에 등장할 확률, \( P(x, y) \)는 단어 \( x \)와 \( y \)가 동시에 등장할 확률로 볼 수 있다.
■ 그리고 분모가 \( P(x) \cdot P(y) \)이므로 PMI는 '두 단어의 독립을 가정했을 때 대비 얼마나 자주 두 단어가 같이 등장하는지'를 나타낸 것으로 볼 수 있다.
■ 예를 들어 10,000개의 단어로 이뤄진 말뭉치가 표본공간일 때,
- 'goodbye'라는 단어가 1,000번 등장했다면 \( P(\text{'goodbye'}) = \dfrac{1000}{10000} = 0.1 \)이 된다.
- 만약, 단어 'you'와 'goodbye'가 동시에 5,000번 등장했다면 \( P(\text{'you', 'goodbye'}) = \dfrac{5000}{10000} = 0.2 \)가 된다.
■ \( P(\text{'goodbye'}) = \dfrac{1000}{10000} \), \( P(\text{'you', 'goodbye'}) = \dfrac{5000}{10000} \)으로 \( P(x), P(y) \) 그리고 \( P(x, y) \)의 확률이 계산된다는 개념을 가지고 '동시발생 행렬'을 \( C \)라고 한다면,
- \( C(x) \)와 \( C(y) \)는 각각 단어 \( x \)와 \( y \)가 등장한 횟수,
- \( C(x, y) \)는 단어 \( x \)와 \( y \)가 '동시발생'한 횟수이다.
■ 그리고 말뭉치에 포함된 단어의 수를 \( N \)이라고 할 때, \( P(\text{'goodbye'}) = \dfrac{C(x)}{N} \), \( P(\text{'you', 'goodbye'}) = \dfrac{C(x, y)}{N} \)으로 계산되는 것을 알 수 있다.
■ 따라서 동시발생 확률 \( 'C' \), 말뭉치 개수 \( 'N' \)을 이용해 PMI 식을 다음 식과 같이 다시 나타낼 수 있다. \[
\text{PMI}(x, y) = \log_2 \dfrac{P(x, y)}{P(x) P(y)}
= \log_2 \dfrac{\dfrac{C(x, y)}{N}}{\dfrac{C(x)}{N} \dfrac{C(y)}{N}}
= \log_2 \dfrac{N \cdot C(x, y)}{C(x) C(y)}
\] ■ 이 식을 통해 동시발생 행렬로부터 PMI를 계산할 수 있다.
■ 예를 들어 말뭉치의 단어 수(N) = 10000일 때, 단어 'you'와 'goodbye', 'i'의 등장 수가 각각 20번, 1000번, 10번, 'you'와 'goodbye'의 동시발생 횟수는 10, 'you'와 'i'의 동시발생 횟수가 5라고 하자.
- 동시발생 횟수의 관점에서는 'you'는 'i'보다 'goodbye'와 더 관련성이 강하지만,
- PMI 관점에서는 \[
\text{PMI}(\text{'you'}, \text{'goodbye'}) = \log_2 \dfrac{10000 \cdot 10}{20 \cdot 1000} = \log_2 5 = 2.31928... \approx 2.32
\] \[
\text{PMI}(\text{'you'}, \text{'i'}) = \log_2 \dfrac{10000 \cdot 5}{20 \cdot 10} = \log_2 250 = 7.96578... \approx 7.97
\] - 'you'와 'goodbye'보다 'you'와 'i'의 관련성이 더 강하게 나타난다.
■ 이러한 결과가 나오는 이유는 단어가 단독으로 출현하는 횟수가 고려되었기 때문이다.
- 'goodbye'가 더 자주 등장해서 'you'와 'goodbye'의 PMI 점수가 낮아진 것이다.
■ 단, PMI는 분자에 '동시발생 횟수'가 있기 때문에 동시발생 횟수가 0이면 \(
\text{PMI} = \log_2 0 = -\infty
\)가 된다.
■ 이 문제를 피하기 위해 NLP에서는 PMI 대신 '양의 점별 상호정보량(Positive PMI)'를 사용한다.\[
\text{PPMI}(x, y) = \max(0, \text{PMI}(x, y))
\] ■ PPMI 식을 사용하면 두 단어의 동시발생 횟수가 0일 때, 단어 사이의 관련성을 \( -\infty \) 대신 0으로 나타낼 수 있고 두 단어 사이의 관련성을 0 이상의 실수로 나타낼 수 있다.
■ 파이썬에서는 동시발생 행렬을 입력으로 받아 PMI를 계산한 후, max(0, PMI)로 PPMI 값을 계산하면 된다.
def ppmi(C, eps = 1e-8):
M = np.zeros_like(C, dtype = np.float32)
N = np.sum(C)
S = np.sum(C, axis = 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)
return M
- C는 입력으로 받을 동시발생 행렬, M은 PPMI 값을 저장할 행렬을 모든 원소가 0이며 동시발생 행렬과 크기가 동일한 행렬로 초가화, N은 전체 동시발생 횟수의 총합, S는 N의 원소. 즉, 행별로 단어의 동시발생 횟수의 합을 더한 배열이다.
- 이중 반복문을 통해 PPMI를 계산하는데, 여기서 C[i, j]는 단어 i와 단어 j의 동시발생 횟수이므로 C[i, j] * N은 PMI 식의 분자, S[i]와 S[j]는 단어 i와 j의 개별 발생 횟수의 곱이므로 PMI 식의 분모이다.
- eps 값을 더해주는 이유는 \( \log_2 0 \)을 방지하기 위함이다.
- PMI를 계산한 후, M[i, j] = max(0, pmi)로 PPMI 값을 PPMI 행렬에 저장한다.
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)
ppmi_matrix = ppmi(co_occurrence_matrix)
print(co_occurrence_matrix); print(' '); print(ppmi_matrix)
```#결과#```
[[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]]
[[0. 1.8073549 0. 0. 0. 0. 0. ]
[1.8073549 0. 0.8073549 0. 0.8073549 0.8073549 0. ]
[0. 0.8073549 0. 1.8073549 0. 0. 0. ]
[0. 0. 1.8073549 0. 1.8073549 0. 0. ]
[0. 0.8073549 0. 1.8073549 0. 0. 0. ]
[0. 0.8073549 0. 0. 0. 0. 2.807355 ]
[0. 0. 0. 0. 0. 2.807355 0. ]]
````````````
■ 단, PPMI는 말뭉치의 단어 수가 증가하면 벡터의 차원 수도 증가한다는 문제점이 있다.
■ 이 예에서 말뭉치의 단어 수는 7개이므로 각 벡터의 차원 수는 7이었지만, 말뭉치의 단어 수가 100개, 1000개, 10000개, ... 가 된다면, 차원 수도 똑같이 100, 1000, 10000, ... 이 된다.
■ PPMI 행렬의 원소를 살펴보면, 각 벡터의 원소가 대부분 0이다. 0은 각 원소의 '중요도'가 낮음을 의미한다.
■ 즉, 벡터 대부분의 원소인 0의 존재는, 두 단어의 관련성을 의미하는 벡터의 구성에 있어 중요하지 않다. 그러나 원소 0은 생략되지 않고 벡터의 차원을 차지하고 있다.
■ 또한, 이런 벡터 형태는 노이즈에 취약하다.
■ 예를 들어 다음과 같이 이미지 데이터 MNIST 셋의 훈련 데이터를 어떠한 값도 없는 데이터와 노이즈가 추가된 데이터로 모델을 각각 훈련했을 때
import numpy as np
from tensorflow.keras.datasets import mnist
from tensorflow import keras
from tensorflow.keras import layers
(train_images, train_labels), (test_images, test_labels) = mnist.load_data()
train_images = train_images.reshape((60000, 28*28)) # (60000, 784)
train_images = train_images.astype('float32')/255 # 정규화
# 노이즈 추가 o
train_images_with_noise_channels = np.concatenate(
[train_images, np.random.random((len(train_images), 784))], axis = 1)
```#결과#```
array([[0. , 0. , 0. , ..., 0.81036608, 0.20451723,
0.5576852 ],
[0. , 0. , 0. , ..., 0.06510639, 0.27404286,
0.36874629],
[0. , 0. , 0. , ..., 0.49467994, 0.44232418,
0.59822468],
...,
[0. , 0. , 0. , ..., 0.55955093, 0.47028996,
0.88688653],
[0. , 0. , 0. , ..., 0.77480555, 0.60740733,
0.85115957],
[0. , 0. , 0. , ..., 0.01020939, 0.93373763,
0.36030365]])
````````````
# 노이즈 추가 x
train_images_with_zeros_channels = np.concatenate(
[train_images, np.zeros((len(train_images), 784))], axis = 1)
train_images_with_zeros_channels
```#결과#```
array([[0., 0., 0., ..., 0., 0., 0.],
[0., 0., 0., ..., 0., 0., 0.],
[0., 0., 0., ..., 0., 0., 0.],
...,
[0., 0., 0., ..., 0., 0., 0.],
[0., 0., 0., ..., 0., 0., 0.],
[0., 0., 0., ..., 0., 0., 0.]])
````````````
model = keras.Sequential([
layers.Dense(512, activation="relu"),
layers.Dense(10, activation="softmax")
])
model.compile(optimizer="rmsprop",
loss="sparse_categorical_crossentropy",
metrics=["accuracy"])
# 노이즈가 있는 훈련 데이터로 모델 훈련
history_noise = model.fit(
train_images_with_noise_channels, train_labels,
epochs=10,
batch_size=128,
validation_split=0.2)
# 노이즈가 없는 훈련 데이터로 모델 훈련
history_zeros = model.fit(
train_images_with_zeros_channels, train_labels,
epochs=10,
batch_size=128,
validation_split=0.2)
검증 데이터의 정확도를 확인하면 다음과 같다.
- PPMI 행렬의 각 행과 같은 형태인 노이즈가 섞인 훈련 데이터가 그렇지 않은 훈련 데이터에 비해 검증 정확도가 낮은 것을 볼 수 있다. 이는 데이터에 노이즈가 섞여 발생한 가짜 상관관계의 영향 때문이다.
■ 이런 문제를 해결하고자 차원 축소(dimensionality reduction) 기법을 사용한다.
1.3 차원 축소
■ 차원 축소는 데이터의 핵심 속성은 가져가면서 더 적은 수의 특징(차원)을 사용하여 데이터를 표현하는 방법이다.
■ 즉, 고차원 공간에서 무관하거나 중복된 특징, 노이즈가 있는 데이터를 제거해 저차원 공간으로 변환하되, 중요한 정보는 최대한 본질적인 차원에 가깝게 유지하는 방법이다.
■ 단순하게 생각하면 차원 축소는 불필요한 특징(차원)을 줄여 가짜 상관관계를 제거하는 방법이며, 이상적인 결과는 다음 그림과 같이 저차원에서도 고차원의 중요한 정보를 유지시키는 것이다.
■ 예시의 PPMI 결과처럼 원소 대부분이 0인 행렬, 벡터를 희소 행렬(sparse matrix), 희소 벡터(sparse vector)라고 한다.
## PPMI 결과
[[0. 1.8073549 0. 0. 0. 0. 0. ]
[1.8073549 0. 0.8073549 0. 0.8073549 0.8073549 0. ]
[0. 0.8073549 0. 1.8073549 0. 0. 0. ]
[0. 0. 1.8073549 0. 1.8073549 0. 0. ]
[0. 0.8073549 0. 1.8073549 0. 0. 0. ]
[0. 0.8073549 0. 0. 0. 0. 2.807355 ]
[0. 0. 0. 0. 0. 2.807355 0. ]]
■ 이런 형태의 행렬, 벡터에 차원 축소를 적용하는 이유는 불필요한 축(차원)을 제거하고 중요한 축만 더 작은 차원에 나타내어 PPMI의 문제점(희소 벡터)을 해결하고자 하는 것이며, 그 결과 희소 벡터의 불필요한 차원이 제거되어 원소 대부분이 0이 아닌 값으로 구성된 밀집 벡터(dense vector)가 된다.
■ 이 밀집 벡터가 바로 단어의 분산 표현이다.
■ 정리하면, 희소 벡터는 각 단어를 차원이 분리된 고차원의 공간에 표현하고, 밀집 벡터는 저차원 공간에서 단어를 여러 차원에 분산시켜 표현한다.
1.3.1 특잇값 분해(Singular Value Decomposition, SVD)
■ 특잇값 분해는 하나의 행렬을 왼쪽 특이 벡터\( (U) \), 특잇값\( (S) \), 오른쪽 특이 벡터\( (V) \)라고 하는 세 행렬의 곱으로 분해하는 것이다.
■ 특잇값 행렬은 행렬 \( A \)와 같은 크기의 대각 행렬이며, 특잇값은 항상 가장 큰 값부터 가장 작은 값을 특잇값 행렬의 왼쪽 위부터 오른쪽 아래 순서대로 정렬한다. 이때, 가장 큰 특잇값들은 데이터의 대부분의 정보를 담고 있다.
■ 그러므로 \( U \)를 단어 벡터가 만든 공간으로 생각한다면, SVD로 \( U \)를 차원 축소하는 방법은 특잇값 행렬에서 특잇값이 가장 큰 \( k \)개를 선택해서 \( k \)개의 특잇값에 대응하는 특이 벡터들( \( U_k \)와 \(V_k^T\) )로 원래 행렬 \( A \)를 근사시키는 것이다.
■ 데이터의 핵심 속성이 \( k \)개의 특잇값에 저장되어 잇으므로, \( k \)개에 들지 못한 불필요한 차원(축)을 제거하고 \( k \)개의 차원(축)만으로도 원래 행렬 \( A \)에 근사할 수 있는 것이다.
■ 이런 방법을 Truncated SVD라고 하며, \( k \)개의 축만 고려하므로 연산속도가 SVD보다 빠를 수 밖에 없다.
- 행렬의 크기가 \( N \)이면 SVD 계산은 \( O(N^3) \)이 소요된다.
■ 파이썬에서 PPMI 행렬에 SVD를 수행하려면, 다음과 같이 넘파이의 선형대수 모듈인 linalg 모듈이 제공하는 svd( ) 메서드를 PPMI 행렬에 적용시키면 되고, Truncated SVD를 수행하려면 sklearn의 randomized_svd( ) 메서드를 PPMI 행렬에 적용시키면 된다.
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)
ppmi_matrix = ppmi(co_occurrence_matrix)
U, S, V = np.linalg.svd(ppmi_matrix) # SVD
## 특이값
S
```#결과#```
array([3.1680453e+00, 3.1680453e+00, 2.7029872e+00, 2.7029872e+00,
1.5144811e+00, 1.5144811e+00, 1.4839370e-16], dtype=float32)
````````````
## 단어 'you'의 희소 벡터
ppmi_matrix[0]
```#결과#```
array([0. , 1.8073549, 0. , 0. , 0. , 0. ,
0. ], dtype=float32)
````````````
## 단어 'you'의 밀집 벡터
U[0]
```#결과#```
array([-3.4094876e-01, -1.1102230e-16, -3.8857806e-16, -1.2051624e-01,
0.0000000e+00, 9.3232495e-01, 2.2259700e-16], dtype=float32)
````````````
- 희소벡터 W[i]가 SVD에 의해 밀집 벡터로 변환된 것을 확인할 수 있다.
■ 밀집 벡터의 차원을 감소시키려면, 파이썬에서는 단순히 벡터 크기를 다음과 같이 조절하면 된다.
U[0, :2] # 2차원 벡터
```#결과#```
array([-3.4094876e-01, -1.1102230e-16], dtype=float32)
````````````
■ Truncated SVD
## Truncated SVD
from sklearn.utils.extmath import randomized_svd
U, S, V = randomized_svd(ppmi_matrix, n_components = vocab_size, random_state = 2024) # n_components는 추출할 특이값, 특이벡터의 개수
U[0]
```#결과#```
array([-8.0466270e-07, 3.4094831e-01, -1.1846580e-01, -2.2137322e-02,
1.3291046e-01, 9.2280257e-01, 5.8813328e-09], dtype=float32)
````````````
'딥러닝' 카테고리의 다른 글
word2vec 속도 개선 (1) (0) | 2024.11.27 |
---|---|
단어의 의미를 파악하는 방법 (4) (0) | 2024.11.22 |
단어의 의미를 파악하는 방법 (1) (0) | 2024.11.20 |
매개변수 갱신 방법(2) (0) | 2024.11.03 |
매개변수 갱신 방법(1) (1) | 2024.11.01 |