목차
👀 코드 보기 , 🤷♀️
이 두개의 아이콘을 누르시면 코드, 개념 부가 설명을 보실 수 있습니다:)
LSA란?
토픽 모델링 (Latent Semantic Analysis, LSA)이다.
이는 BoW에 기반한 통계기반 분석은 단어의 빈도 수를 이용한 통계적 분석이기 때문에 단어의 의미를 고려하지 못한다.
즉 단어의 토픽을 고려하지 못한다는 것이다.
또한 통계기반 분석은 말뭉치의 어휘수가 증가함에 따라 각 단어 벡터의 차원 수도 증가한다.
예를 들어 말물치 어휘 수가 10만 개면 한 단어를 나타낸 벡터의 차원 수도 10만 개다.
또한 각 단어들이 모든단어와 밀접한 연관이 있는 것이 아님으로(연관이 있으면 1에 가까움) 원소 대부분이 0이다.
즉 각 원소의 중요도가 낮은 것이다.
➡️ 이러한 벡터는 누가 들어도 비효율적이고 비현실적이며 노이즈에 약하고 견고하지 못하다.
그러므로 각 단어를 표현하는 벡터의 차원을 감소하여 이 문제를 해결해 볼 것 이다.
이를 위해 의미있는 벡터들만 골라 차원 수를 감소시키는 잠재된(Latent) 의미를 이끌어내는 방법으로 잠재 의미 분석(Latent Semantic Analysis, LSA)이 사용된다.
그럼 본격적으로 들어가기 전, LSA의 대략적인 목표를 알아보자.
[LSA를 통한 벡터의 차원을 줄이는 방법]
- 핵심: ‘중요한 정보’는 최대한 유지하면서 줄임
- [EX] 데이터의 분포를 고려해 중요한 ‘축’을 찾는 일을 수행.
2차원 데이터를 1차원으로 표현하기 위해 중요한 축(데이터를 넓게 분포시키는 축)을 찾는다.
- 왼쪽: 데이터점들을 2차원 좌표에 표시 모습.
- 오른쪽: 새로운 축을 도입해 똑같은 데이터를 좌표축 하나 만으로 표시 (새로운 축을 찾을 때는 데이터가 넓게 분포되도록 고려필요).
각 데이터점의 값은 새로운 축으로 사영된 값으로 변함.
- 중요 가장 적합한 축을 찾아내는 일로, 1차원 값만으로도 데이터의 본질적인 차이를 구별할 수 있어야 함
➕ 희소행렬(sparse matrix), 희소벡터(sparse vector)
원소 대부분이 0인 행렬 또는 벡터
차원 감소의 핵심: 희소벡터에서 중요한 축을 찾아내 더 적은 차원으로 다시 표현
차원 감소의 목표: 원래의 희소벡터는 원소 대부분이 0이 아닌 값으로 구성된 ‘밀집벡터’로 변환.
이 LSA를 이해하기 위해선 먼저 선형대수학의 개념인 특이값분해 Singular Value Decomposition (SVD) 를 이해해야 한다.
SVD
특이값분해 Singular Value Decomposition (SVD)
※ 특이값분해(Singular Value Decomposition, SVD)는 보통 복소수 공간에 대하여 정의하는 것이 일반적이지만, 현 포스트에서는 실수 벡터 공간에 한정했다.
이는 행렬 A를 \(UΣV^{T}\)의 형태로 분해하는 기법이다.
짧게 말하자면, SVD에서 행렬을 분해할때, 일부 값이 작은 데이터를 잘라내어버릴 수도 있다.
그러면 원래의 크기보다 적은 크기의 행렬로 전체 행렬을 표현하는게 가능해지는데, 이 과정에서 중요한 의미만 보존하면서 행렬의 크기를 줄이는 것이다.
그럼 더 자세히 들어가보자.
조건
[특이값 분해]
N×M 크기의 행렬 A를 다음과 같은 3개의 행렬의 곱으로 나타내는 것
\(A = UΣV^T\)
여기에서 U,Σ,V는 다음 조건을 만족해야 한다.
- \(U\): NxN 정방행렬로 직교행렬을 만족해야한다. \(U∈R^{N×N}\)
- \(V\): MxM 정방행렬로 직교행렬을 만족해야한다. \(V∈R^{MxM}\)
- \(Σ\): NxM의 대각성분이 양수인 대각행렬이어야 한다. 큰 수부터 작은 수 순서로 배열한다. \(Σ∈R^{N×M}\)
📜 정방행렬이란?
square matrix (정방행렬)은 행렬인데, 같은 수의 행과 열을 가지는 행렬을 의미한다.
아래는 N정방행렬의 예시이다.
📜 직교행렬이란?
정방행렬인 Q의 전치 행렬과 자기 자신 Q를 곱했을 때, I(단위 행렬)이 된다는 것은,
Q의 전치 행렬이 Q의 역행렬과 같아지고, 이렇게 되면, 직교 행렬이라고 부를 수 있다
📝 전치행렬이란?
전치 행렬(transposed matrix)은 원래의 행렬에서 행과 열을 바꾼 행렬입니다. 즉, 주대각선을 축으로 반사 대칭을 하여 얻는 행렬입니다. 기호는 기존 행렬 표현의 우측 T위에 를 붙입니다. 예를 들어서 기존의 행렬을 M이라고 한다면, 전치 행렬은 와 같이 표현합니다.
📝 단위행렬이란?
단위 행렬(identity matrix)은 주대각선의 원소가 모두 1이며 나머지 원소는 모두 0인 정사각 행렬을 말합니다. 보통 줄여서 대문자 로 표현하기도 하는데, 2 × 2 단위 행렬과 3 × 3 단위 행렬을 표현해보면 다음과 같습니다.
📝 역행렬이란?
만약 A행렬 와 어떤 행렬을 곱했을 때, 결과로서 단위 행렬이 나온다면 이때의 어떤 행렬을 A의 역행렬이라고 하며, \(A^{-1}\)라고 표현합니다.
📜 대각행렬이란?
대각행렬(diagonal matrix)은 주대각선을 제외한 곳의 원소가 모두 0인 행렬을 말합니다. 아래의 그림에서는 주대각선의 원소를 라고 표현하고 있습니다. 만약 대각 행렬 Σ가 3 × 3 행렬이라면, 다음과 같은 모양을 가집니다.
여기까진 정사각 행렬이기 때문에 직관적으로 이해가 쉽습니다. 그런데 정사각 행렬이 아니라 직사각 행렬이 될 경우를 잘 보아야 헷갈리지 않습니다. 만약 행의 크기가 열의 크기보다 크다면 다음과 같은 모양을 가집니다. 즉, m × n 행렬일 때, m > n인 경우입니다.
반면 n > m인 경우에는 다음과 같은 모양을 가집니다.
의미
행렬 U의 열벡터들
- 왼쪽 특이벡터(left singular vector)
- U 직교행렬: 어떠한 공간의 축(기저)을 형성. (지금 우리의 맥락에서는 이 U 행렬을 ‘단어 공간’)
행렬 V의 행벡터들
- 오른쪽 특이벡터(right singular vector)
행렬 Σ의 대각성분들
- 특잇값(singular value)
- 그 성분에 ‘특잇값 singular value ’이 큰 순서로 나열됨.
- 특잇값: 쉽게 말해 ‘해당 축’의 중요도.
➡ 중요도가 낮은 원소(특잇값이 작은 원소)를 깎아내는 방법.
➡ 행렬 Σ에서 특잇값이 작다면 중요도가 낮다는 뜻
➡ 행렬 U에서 여분의 열벡터를 깎아내 원래의 행렬 근사가능
즉, SVD는 A라는 우리가 갖고있는 행렬을 Σ,U,V로 나눠 특이점을 볼 수 있게 하는 것이구나. 정도로만 이해하면 된다.
구현
📜 단어의 PPMI 행렬에 적용
- 행렬 X의 각 행: 해당 단어 ID의 단어 벡터가 저장
- 행렬 U: 그 단어 벡터가 차원 감소된 벡터로 표현되는 것
[구현]
SVD는 넘파이의 linalg 모듈의 svd메서드로 실행 가능
- “linalg”는 선형대수 linear algebra 의 약어
👀 코드 보기
import sys
sys.path.append('..')
import numpy as np
import matplotlib.pyplot as plt
from common.util import preprocess, create_co_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.svd(W)
SVD에 의해 변환된 밀집벡터 표현은 변수 U에 저장
[실제 구현]
단어 ID가 0인 단어 벡터를 보겠습니다.
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.110e-16 -1.205e-01 -4.441e-16 0.000e+00 -9.323e-01 2.226e-16]
➡ 희소벡터인 W[0]가 SVD에 의해서 밀집벡터 U[0]로 변함
➡ 그리고 이 밀집벡터의 차원을 감소시키는 방법
- N차원 벡터로 줄이려면 인덱그 0부터 N개의 원소를 꺼내면 됩니다.
print(U[0, :2])
# [ 3.409e-01 -1.110e-16]
#각 단어를 2차원 벡터로 표현한 후 그래프로 그리기.
for word, word_id in word_to_id.items():
# plt.annotate (word, x,y) 메서드는 2차원 그래프상에서
# 좌표 (x, y ) 지점에 word에 담긴 텍스트를 그림
plt.annotate(word, (U[word_id, 0], U[word_id, 1]))
plt.scatter(U[:,0], U[:,1], alpha=0.5)
plt.show()
-> 동시발생 행렬에 SVD를 적용한 후, 각 단어를 2차원 벡터로 변환해 그린 그래프(“i”와 “goodbye”가 겹쳐 있음)
⛔ WARNING
행렬의 크기가 N이면 SVD 계산은 \(O(N^3)\)
계산량이 N의 3 제곱에 비례해 늘어남.
-> 이는 현실적으로 감당하기 어려운 수준
⭕ solution
Truncated SVD: 더 빠른 기법
- 특잇값이 작은 것은 버리는 truncated 방식으로 성능 향상
- 사이킷런 scikit-learn 라이브러리의 Truncated SVD를 이용
절단된 SVD(Truncated SVD)
그럼 위에서 이해한 SVD의 문제점을 보완하기위해 Truncated SVD를 사용해보자.
위에서 설명한 SVD를 풀 SVD(full SVD)라고 한다.
하지만 LSA의 경우 풀 SVD에서 나온 3개의 행렬에서 일부 벡터들을 삭제시킨 절단된 SVD(truncated SVD)를 사용하게 된다.
즉 특잇값이 작은 것은 버리는 truncated 방식으로 성능을 향상시킬 것이다.
➡️ 이는 특이값벡터인 행렬 Σ의 대각성분들이 중요한 순서대로 정렬되어있기 때문에 가능하다.
즉 Z를 자름으로서 더 중요한 특이값에 집중하는 것이다.
그림으로 이해해보자
1) 대각 행렬 Σ의 대각 원소의 값 중에서 상위값 t개(중요한 것)만 남기고 절단한다.
절단된 SVD를 수행하면 값의 손실이 일어나므로 기존의 행렬 A 복구 불가.
2) U행렬과 V행렬의 Σ의 절단된 열까지만 남김
여기서 Σ의 절단된 열은 우리가 찾고자하는 토픽의 수를 반영한 하이퍼파라미터값임.
Σ의 절단된 열을 선택하는 것은 쉽지 않다.
- Σ의 절단된 열을 크게 잡으면, 기존의 행렬 A로부터 다양한 의미를 가져갈 수 있음.
- Σ의 절단된 열을 작게 잡으면, 노이즈를 제거할 수 있음.
효과
이렇게 일부 벡터들을 삭제하는 것을 데이터의 차원을 줄인다고도 말하는데, 데이터의 차원을 줄이게되면 당연히 풀 SVD를 하였을 때보다,
- 계산 비용이 낮아지는 효과를 얻을 수 있음.
- 상대적으로 중요하지 않은 정보를 삭제가능
- 영상 처리 분야: 노이즈를 제거한다는 의미
- 자연어 처리 분야: 설명력이 낮은 정보를 삭제하고 설명력이 높은 정보를 남긴다는 의미
- 즉, 다시 말하면 기존의 행렬에서는 드러나지 않았던 심층적인 의미를 확인할 수 있게 해줍니다.
LSA 사용
그럼 위에서 배운 내용을 통해서 LSA가 어떻게 사용되는지 적용해보자.
다시 복기해보자면 LSA는 중요 의미를 함축하는 단어를 찾아 토픽을 찾는것이다.
즉, 여러개의 문서를 통해서(이 문서들을 분석하고 SVD를 적용함을 통해서) 이 문서들에서 중요시 여기지 않은 단어들은 절단(Truncated)해 버리고 중요한 단어만 살려 토픽을 찾아내는 것이다.
그렇다면 예를 보자
아래와 같이 문헌 4(A,B,C,D)개가 있다고 가정하자.
전처리 후 위 문헌에는 총 9가지의 용어(cute, cat, like, sleep, dog, love, play, with, me)가 있다고 하자.
단어의 순서를 무시해버리면(단어 주머니 모형, Bag-of-words model) 각 문헌은 문헌 내의 용어들의 빈도로 나타낼 수 있다.
지금 위의 문헌-용어 행렬이 가지는 큰 한계는 0이 너무 많다는 것이다.
지금은 전체 용어가 9종류, 문헌 4개였기에 이 정도이지, 실제로는 용어 종류가 굉장히 많고, 대부분의 문헌에서는 극히 일부의 용어만 출현하기에 문헌-용어 행렬은 대부분이 0인 희소행렬이 된다.
자, 그럼 이 문제를 해결하기 위해서 LSA를 사용해보자.
Full SVD
import numpy as np
1️⃣ 위와 같은 문헌-용어 행렬 생성
A = np.array([[0,0,0,1,0,1,1,0,0],
[0,0,0,1,1,0,1,0,0],
[0,1,1,0,2,0,0,0,0],
[1,0,0,0,0,0,0,1,1]])
print('문헌-용어 행렬의 크기(shape) :', np.shape(A))
문헌-용어 행렬(shape) : (4, 9)
# 4 × 9의 크기를 가지는 문헌-용어 행렬이 생성
2️⃣ SVD(full SVD)를 수행
- 대각 행렬의 변수명을 Σ가 아니라 S를 사용
- V의 전치 행렬을 VT로 지칭
- 소수점의 길이가 너무 길게 출력하면 보기 힘들어서 두번째 자리까지만 출력하기위해서 .round(2)를 사용
U, s, VT = np.linalg.svd(A, full_matrices = True)
[결과 확인]
print('행렬 U :') # 4 × 4의 크기를 가지는 직교 행렬 U가 생성
print(U.round(2))
print('행렬 U의 크기(shape) :',np.shape(U))
행렬 U :
[[-0.24 0.75 0. -0.62]
[-0.51 0.44 -0. 0.74]
[-0.83 -0.49 -0. -0.27]
[-0. -0. 1. 0. ]]
행렬 U의 크기(shape) : (4, 4)
print('특이값 벡터 :') # 4 × 9의 크기를 가지는 대각 행렬 S(Σ)
print(s.round(2))
print('특이값 벡터의 크기(shape) :',np.shape(s))
특이값 벡터 :
[2.69 2.05 1.73 0.77]
특이값 벡터의 크기(shape) : (4,)
# Numpy의 linalg.svd()는 특이값 분해의 결과로 대각 행렬이 아니라 특이값의 리스트를 반환함.
# 그러므로 앞서 본 수식의 형식으로 보려면 이를 다시 대각 행렬로 바꾸어 주어야 함.
# 우선 특이값을 s에 저장하고 대각 행렬 크기의 행렬을 생성한 후에 그 행렬에 특이값을 삽입해보겠음.
대각 행렬 S :
[[2.69 0. 0. 0. 0. 0. 0. 0. 0. ]
[0. 2.05 0. 0. 0. 0. 0. 0. 0. ]
[0. 0. 1.73 0. 0. 0. 0. 0. 0. ]
[0. 0. 0. 0.77 0. 0. 0. 0. 0. ]]
# 2.69 > 2.05 > 1.73 > 0.77 순으로 값이 내림차순을 보임
대각 행렬의 크기(shape) :
(4, 9)
print('직교행렬 VT :') # 9 × 9의 크기를 가지는 직교 행렬 VT(V의 전치 행렬)
print(VT.round(2))
print('직교 행렬 VT의 크기(shape) :')
print(np.shape(VT))
직교행렬 VT :
[[-0. -0.31 -0.31 -0.28 -0.8 -0.09 -0.28 -0. -0. ]
[ 0. -0.24 -0.24 0.58 -0.26 0.37 0.58 -0. -0. ]
[ 0.58 -0. 0. 0. -0. 0. -0. 0.58 0.58]
[ 0. -0.35 -0.35 0.16 0.25 -0.8 0.16 -0. -0. ]
[-0. -0.78 -0.01 -0.2 0.4 0.4 -0.2 0. 0. ]
[-0.29 0.31 -0.78 -0.24 0.23 0.23 0.01 0.14 0.14]
[-0.29 -0.1 0.26 -0.59 -0.08 -0.08 0.66 0.14 0.14]
[-0.5 -0.06 0.15 0.24 -0.05 -0.05 -0.19 0.75 -0.25]
[-0.5 -0.06 0.15 0.24 -0.05 -0.05 -0.19 -0.25 0.75]]
직교 행렬 VT의 크기(shape) :
(9, 9)
[최종 확인]
- 즉, U × S × VT를 하면 기존의 행렬 A가 나와야 함.
- Numpy의 allclose()는 2개의 행렬이 동일하면 True를 리턴.
- 이를 사용하여 정말로 기존의 행렬 A와 동일한지 확인해보겠음.
np.allclose(A, np.dot(np.dot(U,S), VT).round(2))
# True
# 기존의 행렬 A와 동일함
절단된 SVD(Truncated SVD)
이제 Σ의 절단된 열(t) 을 정하고, 절단된 SVD(Truncated SVD)를 수행해보자.
- 여기서는 t=2로 해보겠다.
- 즉, 대각 행렬 S 내의 특이값 중에서 상위 2개만 남기고 제거해보자
1️⃣ S 내의 특이값 중에서 상위 2개만 남기고 제거
# 특이값 상위 2개만 보존
S = S[:2,:2]
print('대각 행렬 S :')
print(S.round(2))
# 상위 2개의 값만 남기고 나머지는 모두 제거됨
[[2.69 0. ]
[0. 2.05]]
2️⃣ 직교 행렬 U에 대해서도 2개의 열만 남기고 제거
U = U[:,:2]
print('행렬 U :')
print(U.round(2))
# 2개의 열만 남기고 모두 제거가 됨
행렬 U :
[[-0.24 0.75]
[-0.51 0.44]
[-0.83 -0.49]
[-0. -0. ]]
3️⃣ VT에 대해서도 2개의 행만 남기고 제거
VT = VT[:2,:]
print('직교행렬 VT :')
print(VT.round(2))
# 2개의 열만 남기고 모두 제거가 됨
직교행렬 VT :
[[-0. -0.31 -0.31 -0.28 -0.8 -0.09 -0.28 -0. -0. ]
[ 0. -0.24 -0.24 0.58 -0.26 0.37 0.58 -0. -0. ]]
4️⃣ 축소된 행렬 U, S, VT에 대해서 다시 U × S × VT연산
- 기존의 A와는 다른 결과가 나옴.
- 값이 손실되었기 때문에 이 세 개의 행렬로는 이제 기존의 A행렬을 복구 불가.
- U × S × VT연산을 해서 나오는 값을 A_prime이라 하고 기존의 행렬 A와 값을 비교해보도록 하겠다.
A_prime = np.dot(np.dot(U,S), VT)
print(A)
print(A_prime.round(2))
# 기존 A행렬
[[0 0 0 1 0 1 1 0 0]
[0 0 0 1 1 0 1 0 0]
[0 1 1 0 2 0 0 0 0]
[1 0 0 0 0 0 0 1 1]]
# Truncated SVD 후 A 행렬(A_prime)
[[ 0. -0.17 -0.17 1.08 0.12 0.62 1.08 -0. -0. ]
[ 0. 0.2 0.2 0.91 0.86 0.45 0.91 0. 0. ]
[ 0. 0.93 0.93 0.03 2.05 -0.17 0.03 0. 0. ]
[ 0. 0. 0. 0. 0. 0. 0. 0. 0. ]]
[결과 해석]
- 대체적으로 기존에 0인 값들은 0에 가가운 값이 나오고, 1인 값들은 1에 가까운 값이 나옴.
- 값이 제대로 복구되지 않은 구간도 존재
결과 해석
이제 이렇게 차원이 축소된 U, S, VT의 크기가 어떤 의미를 가지고 있는지 알아보자.
U
축소된 U는 4 × 2의 크기를 가진다.
- 4: 문서의 개수
- 2: 토픽의 수
U = U[:,:2]
print('행렬 U :')
print(U.round(2))
# 2개의 열만 남기고 모두 제거가 됨
행렬 U :
[[-0.24 0.75]
[-0.51 0.44]
[-0.83 -0.49]
[-0. -0. ]]
여기서 단어의 개수인 9는 유지되지 않는데 문서의 개수인 4의 크기가 유지되었다.
➡️ 4개의 문서 각각을 2개의 값으로 표현한다.
➡️ 즉, U의 각 행은 잠재 의미를 표현하기 위한 수치화 된 각각의 문서 벡터라고 볼 수 있습니다. ➡️ 각 문헌이 어떤 잠재 의미군(토픽)에 속하는 용어들을 많이 포함하고 있는지, 그 비중을 보여준다고 할 수 있다.
VT
축소된 VT는 2 × 9의 크기를 가진다.
- 2: 토픽의 수
- 9: 단어의 개수
- VT의 각 열은 잠재 의미를 표현하기 위해 수치화된 각각의 단어 벡터라고 볼 수 있음
VT = VT[:2,:]
print('직교행렬 VT :')
print(VT.round(2))
# 2개의 열만 남기고 모두 제거가 됨
직교행렬 VT :
[[-0. -0.31 -0.31 -0.28 -0.8 -0.09 -0.28 -0. -0. ]
[ 0. -0.24 -0.24 0.58 -0.26 0.37 0.58 -0. -0. ]]
➡️ 각 잠재 의미군(토픽)에 어떤 용어들이 속하는지 비중을 보여준다
이 문서 벡터들과 단어 벡터들을 통해 다른 문서의 유사도, 다른 단어의 유사도, 단어(쿼리)로부터 문서의 유사도를 구하는 것들이 가능해진다
분석의 목표
크지 않는 N개 (이 예시에서는 2)의 잠재 의미군으로 문헌-용어 행렬을 압축함으로써 정보 검색이나 기타 텍스트 처리의 성능을 높이는 것임.
cute cat라는 질의어로 검색을 수행하면 cute나 cat이 포함된 결과만 얻을 수 있다.
하지만 잠재의미분석을 수행한다면 cute cat이라는 질의어가 더 낮은 차원의 행렬로 변환되고, 그 행렬값과 ###, ### 값이 유사한 문헌을 찾으면 dog나 love가 포함된 문헌도 얻어낼 수 있기 때문이다.
LSA의 장점과 단점
[장점]
- LSA는 쉽고 빠르게 구현이 가능.
[단점]
- 문서에 포함된 단어가 가우시안 분포를 따라야만 LSA를 적용 가능.
- 일반적으로 가우시안 분포를 따르겠지만 모든 문서의 단어가 가우시안 분포를 따르는 것은 아니기 때문에 적용하기가 힘들 때도 있음
- 또한 문서가 업데이트가 된다면 처음부터 다시 SVD를 적용해줘야 하므로 자원이 많이 소모됨.
실제 task에 적용
📜 PTB 데이터셋 펜 트리뱅크 Penn Treebank (PTB)
지금까지 사용한 것 보다 큰 말뭉치
말뭉치(텍스트 파일)의 예
- PTB 말뭉치에서는 한 문장이 하나의 줄로 저장
- 각 문장을 연결한 ‘하나의 큰 시계열 데이터’로 취급
- 이때 각 문장 끝에
라는 특수 문자를 삽입 (“eos”는 “end of sentence”의 약어). - 지금까지의 구현은 문장의 구분을 고려x
- 여러 문장을 연결한 ‘하나의 큰 시계열 데이터’로 간주
[구현]
👀 코드 보기
import sys
sys.path.append('..')
from dataset import ptb
# ptb.load _data ( )는 데이터를 읽어들임
# 인수로 ‘train’, ‘test’,‘valid’ 중 하나 지정 가능
# 차례대로 ‘훈련용’, ‘테스트용’, ‘검증용’ 데이터
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'])
결과
corpus size: 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
[PTB 데이터셋 평가]
PTB 데이터셋에 통계 기반 기법을 적용.
- 큰 행렬에 SVD를 적용해야 하므로 고속 SVD를 이용 sklearn 모듈을 설치 필요
- 물론 간단한 SVD (np.linalg.svd ( ))도 사용 가능
- But,시간, 메모리 비효율적
[구현]
👀 코드 보기
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 계산 ...')
#SVD 수행하는 데 sklearn의 randomized _svd () 메서드 이용
try:
# truncated SVD ( 빠르다 !)
from sklearn.utils.extmath import randomized_svd # 🤷♀️sklearn의 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_similar(query, word_to_id, id_to_word, word_vecs, top=5)
🤷♀️sklearn의 randomized_svd ( ) 메서드
무작위 수를 사용한 Truncated SVD
- 특잇값이 큰 것들만 계산하여 기본적인 SVD보다 훨씬 빠름.
- Truncated SVD는 무작위 수를 사용하므로 결과가 매번 다름.
[결과 분석]
“you”라는 검색어에서는 인칭대명사인 “i”와 “we”가 상위를 차지
-
영어 문장에서 관용적으로 자주 같이 나오는 단어들이기 때문
- “year”의 연관어로는 “month”와 “quarter”
- “car”의 연관어로는 “auto”와 “vehicle” 등
- “toyota”와 관련된 단어 “nissan”, “honda”, “lexus” 등 자동차 제조업체나 브랜드가 뽑힌 것도 확인
➡ 이처럼 단어의 의미 혹은 문법적인 관점에서 비슷한 단어들이 가까운 벡터로 나타남.
➡ 우리의 직관과 비슷한 결과.