by museonghwang

pytorch tensor의 기본 연산

|

파이토치 텐서들을 활용한 기본 연산에 대해서 살펴보겠습니다.

import torch


요소별 산술연산

다음과 같이 두 개의 텐서(행렬) a와 b가 있다고 가정하겠습니다.

[a=\left[ \begin{array}{cc} 1 & 2
3 & 4
\end{array} \right], b=\left[ \begin{array}{cc} 2 & 2
3 & 3
\end{array} \right]]


a = torch.FloatTensor([[1, 2],
                       [3, 4]])
b = torch.FloatTensor([[2, 2],
                       [3, 3]])


이제 두 행렬 사이의 덧셈을 수행할 수 있습니다.

[a+b=\left[ \begin{array}{cc} 1 & 2
3 & 4
\end{array} \right] + \left[ \begin{array}{cc} 2 & 2
3 & 3
\end{array} \right] = \left[ \begin{array}{cc} 1 + 2 & 2 + 2
3 + 3 & 4 + 3
\end{array} \right] = \left[ \begin{array}{cc} 3 & 4
6 & 7
\end{array} \right]]


파이토치에서 구현하면 다음과 같습니다.

a + b
[output]
tensor([[3., 4.],
        [6., 7.]])


마찬가지로 뺄셈, 곱셈, 나눗셈 연산을 파이토치 코드로 구현하면 다음과 같습니다.

print(a - b) # 뺄셈
print(a * b) # 곱셈
print(a / b) # 나눗셈
[output]
tensor([[-1.,  0.],
        [ 0.,  1.]])
tensor([[ 2.,  4.],
        [ 9., 12.]])
tensor([[0.5000, 1.0000],
        [1.0000, 1.3333]])


제곱 연산도 비슷하게 취해볼 수 있습니다. 이 연산을 파이토치 코드로 구현하면 다음과 같습니다.

a ** b
[output]
tensor([[ 1.,  4.],
        [27., 64.]])


논리 연산자도 마찬가지로 쉽게 구현할 수 있습니다. 아래 코드는 행렬의 각 위치의 요소가 같은 값일 경우 True, 다른 값일 경우 False를 갖도록 하는 연산입니다.

a == b
[output]
tensor([[False,  True],
        [ True, False]])


마찬가지로 != 연산자를 사용하게 되면 다른 값일 경우 True, 같은 값일 경우 False를 갖게 됩니다.

a != b
[output]
tensor([[ True, False],
        [False,  True]])


인플레이스 연산

앞에서 수행한 연산들의 결과 텐서는 빈 메모리에 결과 텐서가 새롭게 할당됩니다. 하지만 인플레이스(in-place) 연산은 같은 산술 연산을 수행하지만 기존 텐서에 결과가 저장된다는 차이점이 있습니다. 다음 코드를 확인하겠습니다.

print(a)
print(a.mul(b))
print(a)
[output]
tensor([[1., 2.],
        [3., 4.]])
tensor([[ 2.,  4.],
        [ 9., 12.]])
tensor([[1., 2.],
        [3., 4.]])


곱셈 연산 함수인 a.mul(b) 의 연산 결과 텐서는 새로운 메모리에 할당됩니다. 따라서 다시 텐서 a를 출력하면 a의 값은 그대로인 것을 볼 수 있습니다.

인플레이스 연산들은 밑줄(underscore)이 함수명 뒤에 붙어있는 것이 특징입니다. 따라서 곱셈 함수의 인플레이스 연산 함수는 mul_() 으로 대응됩니다.

print(a.mul_(b))
print(a)
[output]
tensor([[ 2.,  4.],
        [ 9., 12.]])
tensor([[ 2.,  4.],
        [ 9., 12.]])


즉, 메모리의 새로운 공간에 계산 결과가 저장되는 것이 아니라 기존 a의 공간에 계산결과가 저장되는 것입니다. 얼핏 생각하면 새로운 메모리의 공간을 할당하는 작업이 생략되기 때문에 속도나 공간 사용 측면에서 훨씬 효율적일 것 같지만 파이토치 측은 가비지 컬렉터가 효율적으로 작동하기 때문에 굳이 인플레이스 연산을 사용할 필요는 없다고 합니다.


차원 축소 연산: 합과 평균

다음과 같은 텐서 x가 있다고 가정해보겠습니다.

[x=\left[ \begin{array}{cc} 1 & 2
3 & 4
\end{array} \right]]


x = torch.FloatTensor([[1, 2],
                       [3, 4]])


다음과 같이 sum() 함수 또는 mean() 함수를 통해 행렬 전체 요소의 합이나 평균을 구할 수 있습니다. 행렬 요소 전체의 합이나 평균은 텐서나 행렬이 아닌 스칼라(scalar) 값으로 저장되므로 차원이 축소된다고 볼 수 있습니다.

print(x.sum())
print(x.mean())
[output]
tensor(10.)
tensor(2.5000)


여기에서 함수의 dim 인자에 원하는 연산의 차원을 넣어줄 수 있습니다. dim 인자의 값은 없어지는 차원이라고 생각하면 쉽습니다.

print(x.sum(dim=0))
[output]
tensor([4., 6.])


dim=0 이면 첫 번째 차원을 이야기하는 것이므로 행렬의 세로축에 대해서 합(sum) 연산을 수행합니다. 수식으로 표현하면 다음과 같이 표현될 수 있습니다. 2차원인 행렬의 차원이 축소되어 벡터가 되었으므로 세로로 표현되는 것이 맞지만, 이해를 돕기 위해 전치 연산을 통해 가로 벡터로 표현했습니다.

[sum(x, \ dim=0)=\left[ \begin{array}{cc} 1 & 2
{+} & {+}
3 & 4
\end{array} \right] = [4 \ 6]^T]


행렬의 세로 축인 첫 번째 차원에 대해서 축소 연산이 수행되는 것을 확인할 수 있습니다. dim 인자의 값으로 -1도 줄 수 있는데 -1을 차원의 값으로 넣어주게 되면 뒤에서 첫 번째 차원을 의미합니다. 여기에서는 2개의 차원만 존재하므로 dim=1을 넣어준 것과 동일할 것입니다.

print(x.sum(dim=-1))
[output]
tensor([3., 7.])


[sum(x, \ dim=-1)=\left[ \begin{array}{cc} 1 + 2
3 + 4
\end{array} \right] = \left[ \begin{array}{cc} 3
7
\end{array} \right]]


브로드캐스트 연산

텐서 + 스칼라

가장 먼저 쉽게 생각해볼 수 있는 것은 행렬(또는 텐서)에 스칼라를 더하는 것입니다.

x = torch.FloatTensor([[1, 2],
                       [3, 4]])
y = 1


텐서 x와 스칼라 y를 선언하였습니다. 다음의 코드는 x와 y를 더하여 z에 저장한 후, z의 값과 z의 크기를 출력하도록 하는 코드입니다.

z = x + y
print(z)
print(z.size())
[output]
tensor([[2., 3.],
        [4., 5.]])
torch.Size([2, 2])


행렬 x의 각 요소에 모두 1이 더해진 것을 볼 수 있습니다.


텐서 + 벡터

x = torch.FloatTensor([[1, 2],
                       [4, 8]])
y = torch.FloatTensor([3,
                       5])

print(x.size())
print(y.size())
[output]
torch.Size([2, 2])
torch.Size([2])


위의 코드를 실행하면 2×2 행렬 x와 2개의 요소를 갖는 벡터 y를 선언하고, 크기가 다른 두 텐서를 더해보려 합니다. 크기가 다른 두 텐서 사이의 연산을 위해 브로드캐스팅이 적용될 경우 다음과 같이 됩니다. 차원에 맞춰 줄을 세우고 빈칸의 값이 1이라고 가정할 때 다른 한쪽에 똑같이 맞춥니다.

[2, 2]     [2, 2]     [2, 2]
[   2] --> [1, 2] --> [2, 2]


이렇게 같은 모양을 맞춘 이후에 덧셈 연산을 수행합니다. 수식으로 나타내면 다음과 같습니다.

[a+b=\left[ \begin{array}{cc} 1 & 2
4 & 8
\end{array} \right] + \left[ \begin{array}{cc} 3 \ 5 \end{array} \right] = \left[ \begin{array}{cc} 1 & 2
4 & 8
\end{array} \right] + \left[ \begin{array}{cc} 3 & 5
3 & 5
\end{array} \right] = \left[ \begin{array}{cc} 4 & 7
7 & 13
\end{array} \right]]


다음 코드를 실행하면 예측한 정답이 나오는 것을 볼 수 있습니다.

z = x + y
print(z)
print(z.size())
[output]
tensor([[ 4.,  7.],
        [ 7., 13.]])
torch.Size([2, 2])


그러면 텐서들의 덧셈을 살펴보기 위해 텐서를 선언하고 크기를 출력합니다.

x = torch.FloatTensor([[[1, 2]]])
y = torch.FloatTensor([3,
                       5])

print(x.size())
print(y.size())
[output]
torch.Size([1, 1, 2])
torch.Size([2])


실행 결과를 보면 텐서들의 크기를 확인할 수 있습니다. 그러고 나면 좀 전의 규칙을 똑같이 적용해볼 수 있습니다.

[1, 1, 2]     [1, 1, 2]
[      2] --> [1, 1, 2]


다음 코드를 수행하면 결과를 얻을 수 있습니다.

z = x + y
print(z)
print(z.size())
[output]
tensor([[[4., 7.]]])
torch.Size([1, 1, 2])


텐서 + 텐서

이 브로드캐스팅 규칙은 차원의 크기가 1인 차원에 대해서도 비슷하게 적용됩니다. 다음과 같이 두 텐서를 선언하고 크기를 출력합니다.

x = torch.FloatTensor([[1, 2]])
y = torch.FloatTensor([[3],
                       [5]])

print(x.size())
print(y.size())
[output]
torch.Size([1, 2])
torch.Size([2, 1])


마찬가지로 출력 결과를 통해 텐서들의 크기를 확인할 수 있습니다. 여기에서도 브로드캐스팅 규칙을 적용하면 다음과 같이 크기가 변화하며 덧셈 연산을 수행할 수 있습니다.

[1, 2] --> [2, 2]
[2, 1] --> [2, 2]


덧셈 연산을 수행하면 다음과 같은 결과를 얻을 수 있을 것입니다.

z = x + y
print(z)
print(z.size())
[output]
tensor([[4., 5.],
        [6., 7.]])
torch.Size([2, 2])


이처럼 브로드캐스팅을 지원하는 연산의 경우, 크기가 다른 텐서끼리 연산을 수행할 수있습니다. 다만 앞에서의 예제에서 볼 수 있듯이 브로드캐스팅 규칙 자체가 복잡하기 때문에 잘 적용한다면 편리하겠지만 실수가 발생하면 잘못된 결과를 가져올 수도 있습니다.

Read more

Word2Vec 학습방식 - CBOW와 Skip-gram의 이해

|

  1. Word2Vec의 학습방식
  2. CBoW(Continuous Bag of Words)
  3. Skip-gram
  4. CBOW Vs Skip-gram
    • 4.1 CBOW
    • 4.2 Skip-gram
  5. NNLM Vs Word2Vec(CBOW)
  6. 한계점

Word2Vec의 학습방식인 CBOW와 Skip-gram에 대해 살펴보겠습니다.



1. Word2Vec의 학습방식

Word2Vec 은 기본적으로 NNLM을 개선한 모델로, 이전 단어들로부터 다음 단어를 예측하는 목표는 버리고, 임베딩 그 자체에만 집중 했습니다. Word2Vec는 학습방식에 따라 크게 2가지 로 나눌 수 있습니다. CBOW(Continuous Bag of Words)Skip-gram 두 가지 방식이 있습니다.

image


CBOW주변 단어(Context Word)로 중간에 있는 단어인 중심 단어(Center Word)를 예측하는 방법 입니다. 반대로, Skip-gram중심 단어를 바탕으로 주변 단어들을 예측하는 방법 입니다. 메커니즘 자체는 거의 동일합니다. 먼저 CBOW에 대해서 알아보겠습니다.

2. CBoW(Continuous Bag of Words)

먼저 CBOW의 메커니즘에 대해서 알아보겠습니다. 다음과 같은 예문이 있다고 가정하겠습니다.

[“The\ \ fat\ \ cat\ \ sat\ \ on\ \ the\ \ mat”]


예를 들어서 갖고 있는 코퍼스에 위와 같은 예문이 있다고 했을때, [‘The’, ‘fat’, ‘cat’, ‘on’, ‘the’, ‘mat’]으로 부터 sat을 예측하는 것은 CBOW가 하는 일 입니다. 이때 예측해야하는 단어 sat중심 단어(center word) 라고 하고, 예측에 사용되는 단어들주변 단어(context word) 라고 합니다.

중심 단어를 예측하기 위해서 앞, 뒤로 몇 개의 단어를 볼지를 결정 해야 하는데 이 범위윈도우(window) 라고 합니다. 예를 들어 윈도우 크기가 2이고, 예측하고자 하는 중심 단어가 sat이라고 한다면 앞의 두 단어인 fat와 cat, 그리고 뒤의 두 단어인 on, the를 입력으로 사용합니다. 윈도우 크기가 $n$ 이라고 한다면, 실제 중심 단어를 예측하기 위해 참고하려고 하는 주변 단어의 개수는 $2n$ 입니다. 윈도우 크기가 정해지면 윈도우를 옆으로 움직여서 주변 단어와 중심 단어의 선택을 변경해가며 학습을 위한 데이터 셋을 만드는데 이 방법슬라이딩 윈도우(sliding window) 라고 합니다.

image


즉, CBOW는 주변 단어들로부터 중심 단어를 예측하는 과정에서 임베딩 벡터를 학습하며, 윈도우 크기를 정해준다면, 주어진 텍스트로부터 훈련 데이터를 자체 구축 합니다.

위 그림에서 좌측의 중심 단어와 주변 단어의 변화는 윈도우 크기가 2일때, 슬라이딩 윈도우가 어떤 식으로 이루어지면서 데이터 셋을 만드는지 보여줍니다. Word2Vec에서 입력은 모두 원-핫 벡터가 되어야 하는데, 우측 그림은 중심 단어와 주변 단어를 어떻게 선택했을 때에 따라서 각각 어떤 원-핫 벡터가 되는지를 보여줍니다. 위 그림은 결국 CBOW를 위한 전체 데이터 셋을 보여주는 것 입니다.

image


CBOW의 인공 신경망 을 간단히 도식화하면 위와 같습니다. 입력층(Input layer)의 입력으로서 앞, 뒤로 사용자가 정한 윈도우 크기 범위 안에 있는 주변 단어들의 원-핫 벡터가 들어가게 되고, 출력층(Output layer)에서 예측하고자 하는 중간 단어의 원-핫 벡터가 레이블로서 필요합니다.

위 그림에서 알 수 있는 사실은 Word2Vec은 NNLM과 달리 총 3개의 Layer로 구성되며, 은닉층(Hidden layer)이 1개인 얕은 신경망(shallow neural network)이라는 점입니다. 또한 Word2Vec의 은닉층은 일반적인 은닉층과는 달리 활성화 함수가 존재하지 않으며 Lookup Table 이라는 연산을 담당하는 층으로 투사층(Projection layer)이라고 부르기도 합니다.


CBOW의 인공 신경망을 좀 더 확대하여, 동작 메커니즘에 대해서 상세하게 알아보겠습니다.

image


이 그림에서 주목해야할 것은 두 가지 입니다.

  1. 투사층(Projection layer)의 크기는 $M$
    • CBOW에서 투사층의 크기 $M$ 은 임베딩하고 난 벡터의 차원이 됨.
    • 위 그림에서 투사층의 크기는 $M=5$ 이므로 CBOW를 수행하고나서 얻는 각 단어의 임베딩 벡터의 차원은 5가 될 것.
  2. 입력층과 투사층 사이의 가중치 $W$ 는 $V × M$ 행렬이며, 투사층에서 출력층사이의 가중치 $W’$ 는 $M × V$ 행렬임
    • 여기서 $V$ 는 단어 집합의 크기를 의미
    • 즉, 위의 그림처럼 원-핫 벡터의 차원인 $V$ 가 7이고, $M$ 은 5라면 가중치 $W$ 는 $7 × 5$ 행렬이고, $W’$ 는 $5 × 7$ 행렬이 될 것.
    • 주의할 점은 이 두 행렬은 동일한 행렬을 전치(transpose)한 것이 아니라, 서로 다른 행렬이라는 점으로 인공 신경망의 훈련 전에 이 가중치 행렬 $W$ 와 $W’$ 는 랜덤 값을 가짐


즉, Word2Vec은 총 2개의 가중치 행렬을 가지며 $W$ 는 단어 집합의 크기인 $V$ 행, 임베딩 행렬의 차원인 $M$ 열($M$ 은 하이퍼파라미터), $W’$는 그 반대의 크기를 가지며, $W$ 와 $W’$ 는 동일한 행렬을 전치(transpose)한 것이 아닙니다. 이때 CBOW는 주변 단어로 중심 단어를 더 정확히 맞추기 위해 계속해서 이 $W$ 와 $W’$ 를 학습해가는 구조 입니다.

입력으로 들어오는 주변 단어의 원-핫 벡터와 가중치 $W$ 행렬의 곱이 어떻게 이루어지는지 보겠습니다.

image


위 그림에서는 각 주변 단어의 원-핫 벡터를 $x$ 로 표기하였습니다. 입력 벡터는 원-핫 벡터입니다. $i$ 번째 인덱스에 1이라는 값을 가지고 그 외의 0의 값을 가지는 입력 벡터와 가중치 $W$ 행렬의 곱은 사실 $W$ 행렬의 $i$ 번째 행을 그대로 읽어오는 것과(lookup) 동일합니다. 이 작업을 룩업 테이블(lookup table)이라고 합니다. 즉, Projection layer에서는 입력된 원-핫 벡터와 가중치 행렬 $W$ 의 곱 입니다. 앞서 CBOW의 목적은 $W$ 와 $W'$ 를 잘 훈련시키는 것 이라고 언급한 적이 있는데, 그 이유가 여기서 lookup해온 $W$ 의 각 행벡터가 Word2Vec 학습 후에는 각 단어의 $M$ 차원의 임베딩 벡터로 간주되기 때문 입니다.

image


이렇게 주변 단어의 원-핫 벡터에 대해서 가중치 $W$ 가 곱해서 생겨진 결과 벡터들은 투사층에서 만나 이 벡터들의 평균인 벡터를 구하게 됩니다. 만약 윈도우 크기 $n=2$ 라면, 입력 벡터의 총 개수는 $2n$ 이므로 중간 단어를 예측하기 위해서는 총 4개가 입력 벡터로 사용됩니다. 그렇기 때문에 평균을 구할 때는 4개의 결과 벡터에 대해서 평균을 구하게 됩니다. 즉, Projection layer에서의 최종 연산시 Projection layer에서 모든 embedding vector들은 평균값을 구하여 $M$ 차원의 벡터를 얻습니다.

투사층에서 벡터의 평균을 구하는 부분은 CBOW가 Skip-Gram과 다른 차이점 이기도 합니다. Skip-Gram은 입력이 중심 단어 하나이기 때문에 투사층에서 벡터의 평균을 구하지 않습니다.

image


이렇게 구해진 평균 벡터는 두번째 가중치 행렬 $W’$ 와 곱해져, 곱셈의 결과로 차원이 $V$ 벡터인 원-핫 벡터가 나옵니다. 이 벡터에 CBOW는 소프트맥스(softmax) 함수를 지나면서 벡터의 각 원소들의 값은 0과 1사이의 실수로, 총 합은 1이 됩니다. 다중 클래스 분류 문제를 위한 일종의 스코어 벡터(score vector) 입니다. 스코어 벡터의 $j$ 번째 인덱스가 가진 0과 1사이의 값은 $j$ 번째 단어가 중심 단어일 확률을 나타냅니다.

그리고 이 스코어 벡터의 값은 레이블에 해당하는 벡터인 중심 단어 원-핫 벡터의 값에 가까워져야 합니다. 스코어 벡터를 $\hat{y}$ 라고 하고, 중심 단어의 원-핫 벡터를 $y$ 로 했을 때, 이 두 벡터값의 오차를 줄이기위해 CBOW는 손실 함수(loss function)로 크로스 엔트로피(cross-entropy) 함수를 사용합니다. 크로스 엔트로피 함수에 중심 단어인 원-핫 벡터와 스코어 벡터를 입력값으로 넣고, 이를 식으로 표현하면 다음과 같습니다. 아래의 식에서 $V$ 는 단어 집합의 크기입니다.

[cost(\hat{y}, y) = -\sum^V_{j=1} y_j log(\hat{y_j})]


정리하면, Projection layer의 M차원의 벡터는 가중치 행렬 $W’$ 와 곱하여 소프트맥스 함수를 통과한 뒤, 이 결과값은 CBoW의 예측값으로 실제 중심 단어의 원-핫 벡터와 loss를 구하고 역전파합니다. 역전파(Back Propagation)를 수행하면 $W$ 와 $W’$ 가 학습이 되는데, 학습이 다 되었다면 $M$ 차원의 크기를 갖는 $W$ 의 행렬의 행을 각 단어의 임베딩 벡터로 사용 하거나 $W$ 와 $W’$ 행렬 두 가지 모두를 가지고 임베딩 벡터를 사용 하기도 합니다.


CBOW를 다시한번 그림으로 정리하겠습니다.

image


image


image


3. Skip-gram

CBOW 에서는 주변 단어를 통해 중심 단어를 예측했다면, Skip-gram중심 단어로부터 주변 단어를 예측 합니다. 앞서 언급한 예문에 대해서 동일하게 윈도우 크기가 2일 때, 데이터셋은 다음과 같이 구성됩니다.

image


인공 신경망을 도식화해보면 아래와 같습니다.

image


CBOW와 마찬가지로 Skip-gram은 입력층, 투사층, 출력층 3개의 층으로 구성된 신경망이며, 소프트맥스 함수를 지난 예측값(Prediction)과 실제값으로부터 오차(error)를 구합니다. 소프트맥스 함수를 지난 예측값(Prediction)과 실제값으로부터 오차(error)를 구하고, 이로부터 embedding table을 update 합니다.

image


중심 단어에 대해서 주변 단어를 예측하므로 투사층에서 벡터들의 평균을 구하는 과정은 없습니다. 여러 논문에서 성능 비교를 진행했을 때 CBOW보다 전반적으로 Skip-gram이 더 성능이 좋다고 알려 져 있습니다.


4. CBOW Vs Skip-gram

Word2Vec 방법 중 하나인 CBOW와 비교했을 때, Skip-gram이 성능이 우수하여 더욱 많이 사용 되고 있습니다. 왜냐하면, 모델 학습 시 Skip-gram이 CBOW에 비해 여러 문맥을 고려하기 때문 입니다. 앞선 예문을 다시 활용하여 살펴보겠습니다.

[“The\ \ fat\ \ cat\ \ sat\ \ on\ \ the\ \ mat”]


CBOW와 Skip-gram 각각에 대해서 예측하는 단어마다 몇 번의 서로 다른 문맥을 고려했는지 확인해 보겠습니다.


4.1 CBOW

CBOW주변 단어로부터 오직 1개의 타겟 단어를 예측 및 학습 합니다. Input과 Output 간의 관계를 나타내면 아래의 표와 같습니다.

Input Output
fat, cat The
The, cat, sat fat
The, fat, sat, on cat
fat, cat, on, the sat
cat, sat, the, table on
sat, on, table the
on, the table


즉, 단어를 예측 및 학습할 때 고려하는 문맥은 오직 아래의 표와 같이 1개뿐입니다. 예를 들어, ‘sat’이라는 단어를 예측할 때는 ‘fat’, ‘cat’, ‘on’, ‘the’라는 주변 단어를 활용한 게 전부입니다.

Word Count
The 1
fat 1
cat 1
sat 1
on 1
the 1
table 1


4.2 Skip-gram

Skip-gram타겟 단어를 바탕으로 여러 문맥 단어를 예측하고 학습 합니다. Input과 Output 간의 관계를 나타내면 아래의 표와 같습니다.

Input Output
The fat, cat
fat The, cat, sat
cat The, fat, sat, on
sat fat, cat, on, the
on cat, sat, the, table
the sat, on, table
table on, the


위에서 예측 및 학습되는 출력값마다 고려하는 단어의 개수가 몇 개인지 계산해 보면 아래의 표와 같습니다. 이처럼, Skip-gram은 여러 문맥에 걸쳐 단어를 학습하기 때문 에, 대부분의 상황에서 Skip-gram이 CBOW보다 좋은 성능을 보입니다.

Word Count
The 1
fat 1
cat 1
sat 1
on 1
the 1
table 1


5. NNLM Vs Word2Vec(CBOW)

image


피드 포워드 신경망 언어 모델(NNLM)은 단어 벡터 간 유사도를 구할 수 있도록 워드 임베딩의 개념을 도입하였고, 워드 임베딩 자체에 집중하여 NNLM의 느린 학습 속도와 정확도를 개선하여 탄생한 것이 Word2Vec입니다.

NNLM과 Word2Vec의 차이를 비교해보겠습니다.

  1. 예측하는 대상이 달라짐.
    • NNLM
      • 다음 단어를 예측하는 언어 모델이 목적이므로 다음 단어를 예측함
    • Word2Vec(CBOW)
      • 워드 임베딩 자체가 목적이므로 다음 단어가 아닌 중심 단어를 예측하게 하여 학습함
      • 중심 단어를 예측하므로 NNLM이 예측 단어의 이전 단어들만을 참고하였던 것과는 달리, Word2Vec은 예측 단어의 전, 후 단어들을 모두 참고합니다.
  2. 구조가 달라짐.
    • NNLM
      • 입력층, 투사층, 출력층 전부 존재
      • 연산량 : $(n×m)+(n×m×h)+(h×V)$
    • Word2Vec(CBOW)
      • NNLM에 존재하던 활성화 함수가 있는 은닉층이 제거되었으므로, 이에 따라 투사층 다음에 바로 출력층으로 연결되는 구조임.
      • Word2Vec이 NNLM보다 학습 속도에서 강점을 가짐.
      • 연산량 : $(n×m)+(m×log(V))$
      • 추가적인 기법을 사용하면 출력층에서의 연산에서 $V$ 를 $log(V)$ 로 변경가능


Word2Vec이 NNLM보다 학습 속도에서 강점을 가지는 이유는 은닉층을 제거한 것뿐만 아니라 추가적으로 사용되는 기법들 덕분이기도 합니다. 대표적인 기법으로 계층적 소프트맥스(hierarchical softmax)네거티브 샘플링(negative sampling) 이 있습니다.


6. 한계점

Skip-gram뿐만 아니라 CBOW 역시 출력층에서 소프트맥스 함수를 거쳐 단어 집합 크기의 벡터와 실제 참값인 원-핫 벡터와의 오차를 계산합니다. 이를 통해 가중치를 수정하고 모든 단어에 대한 임베딩 벡터 값을 업데이트 합니다.

그런데, 만약 단어 집합의 크기가 수만, 수십만 이상에 달하면 위와 같은 일련의 작업은 시간 소모가 큰 무거운 작업입니다. 즉, Word2Vec의 학습 모델 자체가 무거워집니다. 이를 해결하는 방법으로 Hierarchical Softmax와 Negative Sampling 방법이 있습니다.

Read more

pytorch tensor의 이해

|

텐서(tensor)딥러닝에서 가장 기본이 되는 단위 중 하나 입니다. 스칼라(scalar), 벡터(vector), 행렬(matrix), 그리고 텐서 를 통해 딥러닝 연산을 수행 할 수 있습니다. 다음 그림은 스칼라, 벡터, 행렬, 텐서의 관계를 나타냅니다.

image


각 값을 메모리에 저장할 때 스칼라(scalar)하나의 변수 로 나타낼 수 있고, 벡터(vector)1차원의 배열 로 나타낼수 있으며, 행렬(matrix)2차원의 배열 로 나타내며 텐서(tensor) 부터는 3차원 이상의 배열 로 나타냅니다. 일반적으로 3차원부터는 모두 텐서라고 묶어서 부릅니다.


행렬의 표현

우리가 다룰 대부분의 값은 보통 float 타입이나 double 타입으로 표현되는 실수(real number) 입니다. 실수들로 채워진 행렬 은 다음 그림과 같이 표현할 수 있습니다.

image


행렬 $x$ 는 $k$ 개의 행(row)$n$ 개의 열(column) 로 이루어져 있으며 값들은 모두 실수로 이루어져 있습니다. 이것을 수식으로 표현하면 다음과 같습니다.

[x∈R^{k×n} -> x =(k,n)]


이는 텐서에 대한 size() 함수를 호출한 것과 같습니다. 앞의 그림에서 첫 번째 차원(dimension) $k$ 가 세로축의 크기를 나타내고 있고, 두 번째 차원 $n$ 이 가로축의 크기를 나타내고 있습니다.


텐서의 표현

이번에는 행렬에 이어 텐서의 표현 에 대해서 살펴보도록 하겠습니다. 다음 그림은 실수로 이루어진 $k×n×m$ 차원의 텐서 를 나타냅니다.

image


중요한 점은 첫 번째 차원 $k$ 가 세로축의 크기를 나타내고, $n$ 이 가로축의 차원을 나타내고, $m$ 이 마지막 남은 축의 차원을 나타냅니다.


다양한 행렬/텐서의 모양들

딥러닝을 통해 다양한 분야의 많은 문제를 풀어나갈때 데이터의 도메인에 따라 문제들을 나눠 볼 수 있습니다. 각 도메인에 따라서 자주 다루게 될 텐서의 형태 또한 상이합니다. 이번에는 각 도메인 별로 자주 만날 행렬/텐서의 형태에 대해서 알아보겠습니다.


데이터 사이언스: 테이블 형태의 데이터셋

데이터 사이언스 또는 데이터 분석을 수행할때에는 주로 테이블 형태(tabular)의 데이터셋 을 다루게 되는데 쉽게 말해, 여러 개의 열(column) 이 존재하고 각 샘플들은 각 열에 대해서 값을 가지며 하나의 행(row) 을 이루게 됩니다. 테이블 형태의 데이터를 텐서로 나타내면 다음과 같습니다.

image


  • $N$
    • 행의 개수(샘플의 수) 를 나타내고, 세로축의 크기 를 나타냅니다.
  • $n$
    • 열의 개수 를 나타내고 가로축의 크기 를 나타냅니다.
    • 열은 피처(feature)라고 부르며 각 샘플의 고유한 속성을 설명하는 값을 담고 있습니다.
    • 만약 피처의 값이 비슷한 샘플끼리는 비슷한 속성을 가진다고 볼 수 있습니다.


위 그림에서 빨간색 점선으로 둘러싸인 부분은 하나의 샘플을 나타냅니다. 전체 데이터가 $N×n$ 행렬이므로, 하나의 샘플은 $n$ 개의 요소를 갖는 $n$ 차원의 벡터가 됩니다. $n$ 차원의 벡터가 $N$ 개 모이므로, $N×n$ 차원의 행렬이 되겠죠.

딥러닝은 병렬(parallel) 연산을 수행합니다. 만약에 $N$ 개의 샘플을 신경망에 통과시킨다면 $N$ 번의 연산을 각각 수행하는 것이 아니라 메모리의 크기가 허용되는 범위 안에서 덩어리로 통과시킵니다. 예를 들어, $k$ 개의 샘플 벡터를 통과시킨다면 이를 위해 $k$ 의 샘플들은 $k×n$ 의 행렬이 되어 신경망을 통과하게 될 것입니다. 다음 그림은 이런 병렬 연산을 위한 행렬을 빨간 점선으로 나타낸 것입니다.

image


빨간색 점선 부분의 $k×n$ 행렬은 전체 데이터셋 $N×n$ 행렬에서 슬라이싱(slicing)을 통해 얻을 수 있습니다.


자연어 처리: 문장 데이터셋

자연어 처리가 주로 다루는 데이터의 대상은 문장 입니다.

  • 문장 : 단어 또는 토큰(token)들이 모여서 이루어진 시퀀셜 데이터
  • 시퀀셜 데이터 : 내부 토큰들의 출현과 순서 관계에 의해서 속성이 정의


단어(토큰)는 각각이 의미를 지니기 때문에 의미를 나타내기 위한 벡터로 표현 되는데, 이를 단어 임베딩 벡터(word embedding vector) 라고 부릅니다. 그리고 단어들이 모여서 문장이 되기 때문에 단어 임베딩 벡터가 모여 문장을 표현하는 행렬 이 됩니다. 또한 문장 행렬은 병렬 처리를 위해 덩어리로 묶어 야 하니 3차원의 텐서 가 됩니다. 이것을 그림으로 나타내면 다음과 같습니다.

image


$N$ 개의 문장을 갖는 텐서 $x$ 는 다음과 같이 나타낼 수 있습니다.

[ x =(N,ℓ,d)]


각 문장은 최대 $ℓ$ 개의 단어를 갖고 있을 것이고 이것은 문장의 길이 를 나타냅니다. 그리고 각 단어는 $d$ 차원의 벡터로 표현 될 것입니다. 이와 같이 자연어 처리를 위한 데이터는 3차원의 텐서 로 나타낼 수 있습니다.

이 데이터의 가장 큰 특징은 문장의 길이에 따라 텐서의 크기가 변할 수 있다는 것 입니다. 데이터셋 또는 코퍼스(corpus) 내부의 문장의 길이가 전부 제각각일 것이므로 어떻게 문장을 선택하여 덩어리로 구성하느냐에 따라서 $ℓ$ 의 크기가 바뀌게 됩니다. 즉, 프로그램이 실행되는 와중에 덩어리 텐서의 크기가 가변적이게 되므로 일반적인 신경망 계층(e.g.선형 계층)을 활용하여 처리하기 어렵고, 주로 순환신경망(recurrent neural networks)을 사용하거나 트랜스포머(Transformer)를 사용합니다.

컴퓨터비전: 이미지 데이터셋

자연어 처리가 주로 다루는 데이터의 대상은 컴퓨터비전(computer vision) 분야는 주로 이미지 데이터 를 다룹니다. 다음 그림은 흑백(gray scale) 이미지의 텐서를 그림으로 나타낸 것입니다.

image


흑백 이미지의 각 픽셀0부터 255까지의 8비트(bit)(1바이트(1byte)) 값으로 표현 됩니다. 한 장의 이미지세로축×가로축 만큼의 픽셀들로 이루어져 있으며 이것은 행렬로 표현 가능합니다. 그리고 여러장의 이미지 행렬이 합쳐지면 3차원의 텐서가 됩니다.

다음 그림은 컬러 이미지의 텐서를 그림으로 나타낸 것입니다. 컬러 이미지의 각 픽셀RGB 값으로 표현 됩니다. RGB 값은 빨강(0~255), 초록(0~255), 파랑(0~255) 값이 모여 8×3 비트로 표현됩니다. 여기에서 각 색깔을 나타내는 값은 채널이라고 부릅니다. 즉, RGB에는 3개의 채널이 존재 합니다.

image


따라서 한 장의 흑백 이미지를 표현하기 위해서는 행렬이 필요 했던 반면, 컬러 이미지 한 장을 표현하기 위해서는 3차원의 텐서가 필요 합니다. 정확하게는 빨강 값을 나타내는 행렬, 초록값을 나타내는 행렬, 파랑 값을 나타내는 행렬이 합쳐져서 텐서가 됩니다. 결과적으로 이미지 덩어리를 표현하기 위해서 4차원의 텐서가 필요 합니다.

테이블 형태의 데이터는 각 열(피처)의 값이 굉장히 중요하지만, 이미지의 경우에는 한 픽셀씩 그림이 평행 이동하더라도 그림의 속성이 바뀌지는 않습니다. 따라서 이러한 이미지의 속성을 반영하기 위해 일반적인 계층(e.g. 선형 계층)을 사용하기보다 합성곱신경망(convolution neural network)을 주로 사용합니다.


파이토치 실습

텐서 생성

먼저 파이토치를 불러옵니다.

import torch


파이토치 텐서는 다양한 타입의 텐서를 지원합니다. 다음 코드는 실수형 Float 텐서 를 선언하는 모습입니다.

ft = torch.FloatTensor([[1, 2],
                        [3, 4]])
ft
[output]
tensor([[1., 2.],
        [3., 4.]])


출력 결과를 보면 실수형 값들로 요소가 채워진 것을 확인할 수 있습니다. 해당 텐서를 실제 행렬로 나타내면 다음과 같습니다.

[ft=\left[ \begin{array}{cc} 1.0 & 2.0
3.0 & 4.0
\end{array} \right]]


이처럼 다차원 배열 값(또는 배열 값이 담겨있는 변수)을 넣어 원하는 요소 값을 갖는 텐서를 직접 생성할 수 있습니다. 같은 방법으로 Long 타입Byte 타입 을 선언할 수 있습니다.

lt = torch.LongTensor([[1, 2],
                       [3, 4]])
lt
[output]
tensor([[1, 2],
        [3, 4]])


bt = torch.ByteTensor([[1, 0],
                       [0, 1]])
bt
[output]
tensor([[1, 0],
        [0, 1]], dtype=torch.uint8)


만약 임의의 값으로 채워진 원하는 크기의 텐서를 만들고자 한다면 다음과 같이 간단하게 만들 수 있습니다.

x = torch.FloatTensor(3, 2)
x
[output]
tensor([[-1.2503e+16,  4.5586e-41],
        [-1.2503e+16,  4.5586e-41],
        [ 3.1360e+27,  7.0800e+31]])


넘파이 호환

파이토치는 넘파이와 높은 호환성 을 자랑하며, 실제로 대부분의 함수들은 넘파이와 비슷한 사용법을 가지고 있습니다. 다음과 같이 넘파이를 불러온 후 넘파이의 배열을 선언하고 출력하면 numpy.ndarray 가 할당되어 있는 것을 확인할 수 있습니다.

import numpy as np

# Define numpy array.
x = np.array([[1, 2],
              [3, 4]])
print(x, type(x))
[output]
[[1 2]
 [3 4]] <class 'numpy.ndarray'>


이렇게 선언한 ndarray 를 파이토치 텐서로 변환할 수 있습니다.

x = torch.from_numpy(x)
print(x, type(x))
[output]
tensor([[1, 2],
        [3, 4]]) <class 'torch.Tensor'>


출력 결과를 보면 파이토치 텐서로 변환된 것을 볼 수 있습니다. 반대로 파이토치 텐서를 넘파이 ndarray로 변환할 수도 있습니다.

x = x.numpy()
print(x, type(x))
[output]
[[1 2]
 [3 4]] <class 'numpy.ndarray'>


텐서 타입 변환

파이토치 텐서의 타입 변환 도 굉장히 간단합니다. 단순히 원하는 타입을 함수로 호출하면 됩니다. 다음 코드는 Float 타입 텐서Long 타입 텐서 로 변환하는 코드입니다.

ft.long()
[output]
tensor([[1, 2],
        [3, 4]])


lt.float()
[output]
tensor([[1., 2.],
        [3., 4.]])


텐서 크기 구하기

딥러닝 계산을 수행하다 보면 텐서의 크기를 구해야 할 때가 많습니다. 텐서 크기를 구하는 방법을 알아보겠습니다. 다음과 같이 3×2×2 텐서 x를 선언합니다.

x = torch.FloatTensor([[[1, 2],
                        [3, 4]],
                       [[5, 6],
                        [7, 8]],
                       [[9, 10],
                        [11, 12]]])


텐서 크기 를 구하려면 size() 함수를 호출하거나 shape 속성 에 접근합니다. size() 함수의 결과값이 shape 속성 에 담겨있다고 보면 됩니다.

print(x.size())
print(x.shape)
[output]
torch.Size([3, 2, 2])
torch.Size([3, 2, 2])


이 크기 정보는 배열(list)에 담겨있다고 생각하면 됩니다. 따라서 특정 차원의 크기를 알기 위해 서는 shape 속성의 해당 차원 인덱스에 접근하거나 size() 함수의 인자에 원하는 차원의 인덱스를 넣어주면 됩니다.

print(x.size(1))
print(x.shape[1])
[output]
2
2


텐서 차원의 개수 를 알기 위해서는 dim() 함수를 활용합니다. 이것은 shape 속성의 배열 크기와 같습니다.

print(x.dim())
print(len(x.size()))
[output]
3
3

Read more

Word2Vec의 이해

|

1. 희소 표현(Sparse Representation)의 한계

데이터를 벡터 또는 행렬을 기반으로 수치화하여 표현할 때 극히 일부의 인덱스만 특정 값으로 표현하고, 대부분의 나머지 인덱스는 의미 없는 값으로 표현하는 방법, 즉 값의 대부분이 0으로 표현되는 방법희소 표현(sparse representation) 이라고 합니다. 대표적인 희소 표현인 원-핫 벡터는 전체 단어 집합의 크기를 갖는 벡터에서 표현할 단어의 인덱스만 1로 나타내고, 나머지는 모두 0으로 표현하는 기법으로 희소 벡터(sparse vector) 입니다.

희소 벡터의 문제점 은 단어의 개수가 늘어나면 벡터의 차원이 한없이 커져 고차원의 벡터가 되어 불필요한 벡터 공간의 낭비를 유발한다는 점 입니다. 또한 원-핫 인코딩, 카운트 기반 단어 표현기법은 단어의 의미를 반영하지 못합니다. 왜냐하면 단어의 의미는 고려하지 않고 단순히 어떤 단어가 문서에서 몇 번 등장했는지만 고려하기 때문입니다.


즉, 희소 표현(Sparse Representation)고차원에 각 차원이 분리된 표현 방법 입니다.

image


이 문제를 해결하기 위해 벡터의 크기가 작으면서도 벡터의 단어의 의미를 표현할 수 있는 방법 들이 제안되었습니다. 이러한 방법들은 분포 가설(Distributed hypothesis)을 기반 으로 합니다.


2. 분산 표현(Distributed Representation)의 등장

분산 표현(Distributed Representation) 이란 분포 가설(Distibutional Hypothesis) 가정 하저차원에 단어 의미를 분산하여 표현하는 기법 입니다. 분포가설“비슷한 문맥에 등장한 단어는 비슷한 의미를 갖는다”라는 가정 입니다. 예를 들어 강아지란 단어는 귀엽다, 예쁘다, 애교 등의 단어가 주로 함께 등장하는데 분포 가설에 따라서 해당 내용을 가진 텍스트의 단어들을 벡터화한다면 해당 단어 벡터들은 유사한 벡터값을 가집니다. 이렇게 표현된 벡터들은 원-핫 벡터처럼 벡터의 차원이 단어 집합(vocabulary)의 크기일 필요가 없으므로, 벡터의 차원이 상대적으로 저차원으로 줄어듭니다.

정리하면, 분산 표현은 분포 가설을 이용하여 텍스트를 학습하고, 단어의 의미를 벡터의 여러 차원에 분산하여 표현 합니다.


여기서 단어를 벡터화하는 작업워드 임베딩(Word Embedding) 이라고 부르며 임베딩 된 벡터임베딩 벡터(Embedding Vector) 라고 부릅니다. 워드 임베딩은 단어를 밀집 표현으로 변환합니다. 밀집 표현(dense representation) 은 희소 표현과 반대되는 단어표현 방법으로써 텍스트를 실숫값으로 구성하고, 벡터의 차원을 단어 집합의 크기로 상정하지 않고 사용자가 설정한 차원의 벡터로 표현합니다.

image


요약하면 희소 표현(Sparse Representation)고차원에 각 차원이 분리된 표현 방법 이었다면, 분산 표현(Distributed Representation)저차원에 단어의 의미를 여러 차원에다가 분산 하여 표현 합니다. 이런 표현 방법을 사용하면 단어 벡터 간 유의미한 유사도를 계산할 수 있습니다. 이를 위한 대표적인 학습 방법이 Word2Vec 입니다.


3. Word2Vec 개념

Word2Vec 는 Word to Vector라는 이름에서 알 수 있듯이 단어(Word)를 컴퓨터가 이해할 수 있도록 수치화된 벡터(Vector)로 표현하는 기법 중 하나 입니다. 구체적으로는 분산 표현(Distributed Representation) 기반의 워드 임베딩(Word Embedding) 기법 중 하나입니다.

즉, Word2Vec단어의 의미를 반영한 임베딩 벡터를 만드는 대표적인 방법 으로, 벡터가 된 단어들은 이제 수치화된 벡터(Vector)이므로 서로 연산 또한 가능 하므로 단어 벡터 간 유의미한 유사도를 계산할 수 있습니다.


Word2Vec가 어떤 일을 할 수 있는지 확인해보겠습니다. 아래 사이트는 한국어 단어에 대해서 벡터 연산을 해볼 수 있는 사이트로, 단어들(실제로는 Word2Vec 벡터)로 더하기, 빼기 연산을 할 수 있습니다.


image


이런 연산이 가능한 이유는 각 단어 벡터가 단어 벡터 간 유사도를 반영한 값을 가지고 있기 때문 입니다.


Word2Vec은 기본적으로 NNLM을 개선한 모델 입니다. 이전 단어들로부터 다음 단어를 예측하는 목표는 버리고, 임베딩 그 자체에만 집중 했습니다. Word2Vec는 학습방식에 따라 크게 2가지로 나눌 수 있습니다. CBOW(Continuous Bag of Words)와 Skip-gram 두 가지 방식이 있습니다.

CBOW 는 주변 단어(Context Word)로 중간에 있는 단어를 예측하는 방법입니다. 여기서 중간에 있는 단어를 중심 단어(Center Word) 또는 타겟 단어(Target Word)라고 부릅니다. 반대로, Skip-gram 은 중심 단어를 바탕으로 주변 단어들을 예측하는 방법입니다.

image

Read more

TF-IDF 개념 및 실습

|

  1. Vectorization
  2. TF-IDF(단어 빈도-역 문서 빈도, Term Frequency-Inverse Document Frequency)
  3. TF-IDF 계산 방법
    • 3.1 TF(Term Frequency, 단어 빈도) : $tf(d,t)$
    • 3.2 DF(Document Frequency, 문서 빈도) : $df(t)$
    • 3.3 IDF(Inverse Document Frequency, 역 문서 빈도) : $idf(d, t)$
  4. TF-IDF 계산
  5. TF-IDF 구현
  6. 사이킷런을 이용한 DTM과 TF-IDF
  7. Summary
  8. Bag of Words 기반의 DTM, TF-IDF와의 딥 러닝의 관계

DTM 내에 있는 각 단어에 대한 중요도를 계산할 수 있는 TF-IDF 가중치에 대해서 알아보겠습니다. TF-IDF를 사용하면, 기존의 DTM을 사용하는 것보다 보다 많은 정보를 고려하여 문서들을 비교할 수 있습니다. TF-IDF가 DTM보다 항상 좋은 성능을 보장하는 것은 아니지만, 많은 경우에서 DTM보다 더 좋은 성능을 얻을 수 있습니다.



1. Vectorization

텍스트를 컴퓨터가 이해하고, 효율적으로 처리하게 하기 위해서는 컴퓨터가 이해할 수 있도록 텍스트를 적절히 숫자로 변환해야 하며, 이를 Vectorization 이라 지칭할 수 있습니다. 상황별 Vectorization을 정리하겠습니다.

  1. 벡터화에 신경망을 사용하지 않을 경우
    • 단어에 대한 벡터 표현 방법 : 원-핫 인코딩
    • 문서에 대한 벡터 표현 방법 : Document Term Matrix(DTM), TF-IDF
  2. 벡터화에 신경망을 사용하는 경우 (2008 ~ 2018)
    • 단어에 대한 벡터 표현 방법 : 워드 임베딩(Word2Vec, GloVe, FastText, Embedding layer)
    • 문서에 대한 벡터 표현 방법 : Doc2Vec, Sent2Vec
  3. 문맥을 고려한 벡터 표현 방법 (2018 - present)
    • ELMo, BERT
    • Pretrained Language Model의 시대


원-핫 인코딩과 DTM, TF-IDF는 Vectorization에 신경망을 사용하지 않을 경우 주로 사용합니다. 이때 원-핫 인코딩은 단어에 대한 벡터 표현 방법이며, Document Term Matrix(DTM)과 TF-IDF는 문서에 대한 벡터 표현 방법 입니다.


2. TF-IDF(단어 빈도-역 문서 빈도, Term Frequency-Inverse Document Frequency)

TF-IDF(Term Frequency-Inverse Document Frequency)단어의 빈도와 역 문서 빈도(문서의 빈도에 특정 식을 취함)를 사용 하여 Document Term Matrix(DTM) 내의 각 단어들마다 중요한 정도를 가중치로 주는 방법 입니다. 우선 DTM을 만든 후, TF-IDF 가중치를 부여합니다.

TF-IDF는 전체 문서에서 빈출되는 단어의 중요도는 낮다고 판단하고, 특정 문서에서만 빈출되는 단어는 중요도가 높다고 판단합니다. 즉, DTM 에서 추가적으로 중요한 단어에 가중치를 주는 방식을 TF-IDF 라고 하며, TF-IDF 값은 단어의 중요도와 비례 하는데 TF-IDF 기준으로 중요한 단어는 값이 Up, 중요하지 않은 값이 Down 하게 됩니다.

image


TF-IDF는 단어의 중요도를 고려하기 때문 에, 일반적으로 단순히 문서 내 단어의 출현빈도만 고려하는 DTM보다 문서에서 유의미한 정보를 얻어내는 데 더욱 효과적 입니다.

TF-IDF는 주로 문서의 유사도를 구하는 작업, 검색 시스템에서 검색 결과의 중요도를 정하는 작업, 문서 내에서 특정 단어의 중요도를 구하는 작업 등에 쓰일 수 있습니다. 또한 벡터이므로 인공 신경망의 입력으로도 사용할 수 있습니다.


3. TF-IDF 계산 방법

TF-IDF는 TF(Term Frequency, 단어 빈도)IDF(Inverse Document Frequency, 역 문서 빈도) 라는 두 값을 곱한 결과를 의미 합니다.

[TF-IDF = TF × IDF]


TF, DF, IDF 각각을 정의하겠습니다.

  • $d$ : 문서
  • $t$ : 단어
  • $n$ : 문서의 총 개수


또한 설명을 위해 4개의 문서가 있다고 가정하겠습니다. 문서 내 띄어쓰기를 기준으로 토큰화(tokenization)한다고 가정하겠습니다.

  1. 문서 1 : 먹고 싶은 사과
  2. 문서 2 : 먹고 싶은 바나나
  3. 문서 3 : 길고 노란 바나나 바나나
  4. 문서 4 : 저는 과일이 좋아요


3.1 TF(Term Frequency, 단어 빈도) : $tf(d,t)$

TF(Term Frequency, 단어 빈도)특정 문서 $d$ 에서의 특정 단어 $t$ 의 등장 횟수를 의미 하며, 사실 TF는 Document Term Matrix(DTM)의 각 단어들이 가진 값들 입니다. DTM이 각 문서에서의 각 단어의 등장 빈도를 나타내는 값이었기 때문입니다.

- 과일이 길고 노란 먹고 바나나 사과 싶은 저는 좋아요
문서1 0 0 0 1 0 1 1 0 0
문서2 0 0 0 1 1 0 1 0 0
문서3 0 1 1 0 2 0 0 0 0
문서4 1 0 0 0 0 0 0 1 1


image


Document Term Matrix(DTM)은 Bag of Words 가설 기반으로, Bag of Words를 직역하면 단어들의 가방을 의미하며, 가방에 문장의 단어들을 넣고 흔든다면, 단어의 순서는 무의미해집니다. 즉 단어의 순서는 무시하고, 오직 단어의 빈도수에만 집중하는 방법을 의미합니다.

image


3.2 DF(Document Frequency, 문서 빈도) : $df(t)$

DF(Document Frequency, 문서 빈도)특정 단어 $t$ 가 등장한 문서의 수를 의미 합니다. 여기서 특정 단어가 각 문서, 또는 문서들에서 몇 번 등장했는지는 관심가지지 않으며, 오직 특정 단어 $t$ 가 몇 개의 문서에 출현했는지 개수에만 관심 을 가집니다.

예를 들어 위 DTM에서 바나나의 df값은 2입니다. 왜냐하면 위 DTM에서의 문서2와 문서3에서 등장했습니다. 문서3에서 바나나가 두 번 등장했지만, 그것은 중요한 게 아닙니다. 심지어 바나나란 단어가 문서2에서 100번 등장했고, 문서3에서 200번 등장했다고 하더라도 바나나의 df는 2가 됩니다.

단어 DF(문서 빈도)
과일이 1
길고 1
노란 1
먹고 2
바나나 2
사과 1
싶은 2
저는 1
좋아요 1


3.3 IDF(Inverse Document Frequency, 역 문서 빈도) : $idf(d, t)$

IDF(Inverse Document Frequency, 역 문서 빈도)DF 값의 역수 입니다.

[idf(d,t) = log\left(\cfrac{n}{1 + df(t)}\right)]


IDF는 DF의 역수를 취하는것이 맞습니다. 하지만 단순히 역수를 취하지 않고 $log$ 와 분모에 1을 더해줍니다. 만약 $log$ 를 사용하지 않았을 때, IDF를 그냥 DF의 역수로 사용한다면 총 문서의 수 $n$ 이 커질 수록, IDF의 값은 기하급수적으로 커지게 됩니다. 그렇기 때문에 $log$ 를 사용합니다. 로그 내 분모에 1을 더해주는 이유는 특정 단어가 전체 문서에서 출현하지 않는 경우, 분모가 0이 되는 상황을 방지하기 위함입니다.

왜 log가 필요한지 $n=1,000,000$ 일 때의 예를 들어봅시다. $log$ 의 밑은 10을 사용한다고 가정하였을 때 결과는 아래와 같습니다.

image


즉, 로그를 사용하지 않으며 idf의 값은 기하급수적으로 커질 수 있습니다. IDF에 로그를 씌우는 이유 를 정리하겠습니다.

  • TF-IDF는 모든 문서에서 자주 등장하는 단어는 중요도가 낮다고 판단 하며, 특정 문서에서만 자주 등장하는 단어는 중요도가 높다고 판단 합니다.
  • 즉, TF-IDF 값이 낮으면 중요도가 낮은 것이며, TF-IDF 값이 크면 중요도가 큰 것입니다.
  • 불용어 등과 같이 자주 쓰이는 단어들은 비교적 자주 쓰이지 않는 단어들 보다 최소 수십 배 자주 등장합니다.
  • 비교적 자주 쓰이지 않는 단어들조차 희귀 단어들과 비교하면 또 최소 수백 배는 더 자주 등장하는 편입니다.
  • log를 씌워주지 않으면, 희귀 단어들에 엄청난 가중치가 부여될 수 있으므로, 로그를 씌우면 이런 격차를 줄이는 효과가 있습니다.


4. TF-IDF 계산

이제, 앞서 DTM을 설명하기위해 들었던 위의 예제를 가지고 TF-IDF(단어 빈도-역 문서 빈도, Term Frequency-Inverse Document Frequency) 에 대해 이해해보겠습니다. 설명을 위해 4개의 문서를 다시 가져오겠습니다. 이때 문서 내 띄어쓰기를 기준으로 토큰화(tokenization)한다고 가정하겠습니다.

  1. 문서 1 : 먹고 싶은 사과
  2. 문서 2 : 먹고 싶은 바나나
  3. 문서 3 : 길고 노란 바나나 바나나
  4. 문서 4 : 저는 과일이 좋아요


위 문서에 대한 TF(Term Frequency, 단어 빈도) 는 특정 문서 $d$ 에서의 특정 단어 $t$ 의 등장 횟수를 의미하며, TF를 구하면 다음과 같습니다. TF는 DTM의 각 단어들이 가진 값들 입니다.

- 과일이 길고 노란 먹고 바나나 사과 싶은 저는 좋아요
문서1 0 0 0 1 0 1 1 0 0
문서2 0 0 0 1 1 0 1 0 0
문서3 0 1 1 0 2 0 0 0 0
문서4 1 0 0 0 0 0 0 1 1


우선 TF는 앞서 사용한 DTM을 그대로 사용하면, 그것이 각 문서에서의 각 단어의 TF가 됩니다. 이제 IDF를 위한 DF를 구해보겠습니다. DF(Document Frequency, 문서 빈도) 는 특정 단어 $t$ 가 등장한 문서의 수를 의미하며, 오직 특정 단어 $t$ 가 몇 개의 문서에 출현했는지 개수에만 관심을 가집니다.

단어 DF(문서 빈도)
과일이 1
길고 1
노란 1
먹고 2
바나나 2
사과 1
싶은 2
저는 1
좋아요 1


이제 구해야할 것은 TF와 곱해야할 값인 IDF(Inverse Document Frequency, 역 문서 빈도) 로, DF 값의 역수 입니다. 로그는 자연 로그를 사용하도록 하겠습니다.

단어 IDF(역 문서 빈도)
과일이 ln(4/(1+1)) = 0.693
길고 ln(4/(1+1)) = 0.693
노란 ln(4/(1+1)) = 0.693
먹고 ln(4/(1+2)) = 0.287
바나나 ln(4/(1+2)) = 0.287
사과 ln(4/(1+1)) = 0.693
싶은 ln(4/(1+2)) = 0.287
저는 ln(4/(1+1)) = 0.693
좋아요 ln(4/(1+1)) = 0.693


문서의 총 수는 4이기 때문에 ln 안에서 분자는 4로 동일합니다. 분모의 경우에는 각 단어가 등장한 문서의 수(DF)를 의미하는데, 예를 들어서 ‘먹고’의 경우에는 총 2개의 문서(문서1, 문서2)에 등장했기 때문에 2라는 값을 가집니다. 각 단어에 대해서 IDF의 값을 비교해보면 문서 1개에만 등장한 단어와 문서 2개에만 등장한 단어는 값의 차이를 보입니다. IDF 값을 보면 2회 등장한 단어들이 값이 더 낮습니다. IDF는 여러 문서에서 등장한 단어의 가중치를 낮추는 역할을 하기 때문 입니다.

TF-IDF를 계산해보겠습니다. 각 단어의 TF는 DTM에서의 각 단어의 값과 같으므로, 앞서 사용한 DTM에서 단어 별로 위의 IDF값을 곱해주면 TF-IDF 값을 얻습니다.

image


- 과일이 길고 노란 먹고 바나나 사과 싶은 저는 좋아요
문서1 0 0 0 0.287 0 0.693 0.287 0 0
문서2 0 0 0 0.287 0.287 0 0.287 0 0
문서3 0 0.693 0.693 0 0.575 0 0 0 0
문서4 0.693 0 0 0 0 0 0 0.693 0.693


같은 단어라도 TF-IDF값은 다른데, 이는 해당 문서의 TF값에 영향을 받기 때문입니다. 문서2에서의 바나나의 TF-IDF 가중치와 문서3에서의 바나나의 TF-IDF 가중치가 다른 것을 볼 수 있습니다. 수식적으로 말하면, TF가 각각 1과 2로 달랐기 때문인데 TF-IDF에서의 관점에서 보자면 TF-IDF는 특정 문서에서 자주 등장하는 단어는 그 문서 내에서 중요한 단어로 판단하기 때문입니다. 문서2에서는 바나나를 한 번 언급했지만, 문서3에서는 바나나를 두 번 언급했기 때문에 문서3에서의 바나나를 더욱 중요한 단어라고 판단하는 것입니다.

즉, TF-IDF는 모든 문서에서 자주 등장하는 단어는 중요도가 낮다고 판단하며, 특정 문서에서만 자주 등장하는 단어는 중요도가 높다고 판단합니다.


5. TF-IDF 구현

위의 계산 과정을 파이썬으로 직접 구현해보겠습니다. 앞의 설명에서 사용한 4개의 문서를 docs에 저장합니다.

from math import log
import pandas as pd
 
docs = [
    '먹고 싶은 사과',
    '먹고 싶은 바나나',
    '길고 노란 바나나 바나나',
    '저는 과일이 좋아요'
] 
 
vocab = list(set(w for doc in docs for w in doc.split()))
vocab.sort()
print('단어장의 크기 :', len(vocab))
print(vocab)
[output]
단어장의 크기 : 9
['과일이', '길고', '노란', '먹고', '바나나', '사과', '싶은', '저는', '좋아요']


TF, IDF, 그리고 TF-IDF 값을 구하는 함수를 구현합니다.

# 총 문서의 수
N = len(docs) 
 
def tf(t, d):
    return d.count(t)
 
def idf(t):
    df = 0
    for doc in docs:
        df += t in doc
    return log(N/(df+1))
 
def tfidf(t, d):
    return tf(t,d)* idf(t)


TF를 구해보겠습니다. 다시 말해 DTM을 데이터프레임에 저장하여 출력해보겠습니다.

result = []

# 각 문서에 대해서 아래 연산을 반복
for i in range(N):
    result.append([])
    d = docs[i]
    for j in range(len(vocab)):
        t = vocab[j]
        result[-1].append(tf(t, d))

tf_ = pd.DataFrame(result, columns = vocab)

image


정상적으로 DTM이 출력되었습니다. 각 단어에 대한 IDF 값을 구해보겠습니다.

result = []
for j in range(len(vocab)):
    t = vocab[j]
    result.append(idf(t))

idf_ = pd.DataFrame(result, index=vocab, columns=["IDF"])

image


위에서 수기로 구한 IDF 값들과 정확히 일치합니다. TF-IDF 행렬을 출력해보겠습니다.

result = []
for i in range(N):
    result.append([])
    d = docs[i]
    for j in range(len(vocab)):
        t = vocab[j]
        result[-1].append(tfidf(t,d))

tfidf_ = pd.DataFrame(result, columns = vocab)

image


TF-IDF의 가장 기본적인 식에 대해서 학습하고 구현하였습니다. 사실 실제 TF-IDF 구현시 위에서 배운 식과는 다른 조정된 식을 사용하는데, 그 이유는 위의 기본적인 식을 바탕으로 한 구현에는 몇 가지 문제점이 존재하기 때문입니다. 만약 전체 문서의 수 $n$ 이 4인데, $df(f)$ 의 값이 3인 경우에는 $df(f)$ 에 1이 더해지면서 $log$ 항의 분자와 분모의 값이 같아지게 됩니다. 이는 $log$ 의 진수값이 1이 되면서 $idf(d,t)$ 의 값이 0이 됨을 의미합니다. 식으로 표현하면 $idf(d,t)=log(n/(df(t)+1))=0$ 입니다.

IDF의 값이 0이라면 더 이상 가중치의 역할을 수행하지 못합니다. 아래 사이킷런의 TF-IDF 구현체 또한 위의 식에서 조정된 식을 사용하고 있습니다.


6. 사이킷런을 이용한 DTM과 TF-IDF

사이킷런을 통해 DTM과 TF-IDF를 만들어보겠습니다. CountVectorizer 를 사용하면 DTM을 만들 수 있습니다.

from sklearn.feature_extraction.text import CountVectorizer

corpus = [
    'you know I want your love',
    'I like you',
    'what should I do ',    
]

vector = CountVectorizer()

# 코퍼스로부터 각 단어의 빈도수를 기록
print(vector.fit_transform(corpus).toarray())

# 각 단어와 맵핑된 인덱스 출력
print(vector.vocabulary_)
[output]
[[0 1 0 1 0 1 0 1 1]
 [0 0 1 0 0 0 0 1 0]
 [1 0 0 0 1 0 1 0 0]]
{'you': 7, 'know': 1, 'want': 5, 'your': 8, 'love': 3, 'like': 2, 'what': 6, 'should': 4, 'do': 0}


사이킷런은 TF-IDF를 자동 계산해주는 TfidfVectorizer 를 제공합니다. 사이킷런의 TF-IDF는 위에서 배웠던 보편적인 TF-IDF 기본 식에서 조정된 식을 사용합니다. 요약하자면, IDF의 로그항의 분자에 1을 더해주며, 로그항에 1을 더해주고, TF-IDF에 L2 정규화라는 방법으로 값을 조정하는 등의 차이로 TF-IDF가 가진 의도는 여전히 그대로 갖고 있습니다.

from sklearn.feature_extraction.text import TfidfVectorizer

corpus = [
    'you know I want your love',
    'I like you',
    'what should I do ',    
]

tfidfv = TfidfVectorizer().fit(corpus)
print(tfidfv.transform(corpus).toarray())
print(tfidfv.vocabulary_)
[output]
[[0.         0.46735098 0.         0.46735098 0.         0.46735098 0.         0.35543247 0.46735098]
 [0.         0.         0.79596054 0.         0.         0.         0.         0.60534851 0.        ]
 [0.57735027 0.         0.         0.         0.57735027 0.         0.57735027 0.         0.        ]]
{'you': 7, 'know': 1, 'want': 5, 'your': 8, 'love': 3, 'like': 2, 'what': 6, 'should': 4, 'do': 0}


7. Summary

TF-IDF(Term Frequency-Inverse Document Frequency) 는 Document Term Matrix(DTM) 내의 각 단어들마다 중요한 정도를 가중치로 주는 방법으로, DTM 에서 추가적으로 중요한 단어에 가중치를 주는 방식을 TF-IDF 라고 하며, TF-IDF는 전체 문서에서 빈출되는 단어의 중요도는 낮다고 판단하고, 특정 문서에서만 빈출되는 단어는 중요도가 높다고 판단합니다. 그러므로 TF-IDF 값은 단어의 중요도와 비례 하는데 TF-IDF 기준으로 중요한 단어는 값이 Up, 중요하지 않은 값이 Down 하게 됩니다.

TF-IDF는 여전히 현업에서도 굉장히 많이 쓰이는 벡터화 방법입니다. 문서를 벡터화한다면 각 문서 간의 유사도 를 구할 수 있는데, 문서 간 유사도를 구할 수 있다면 다음과 같은 Task들을 수행할 수 있습니다.

  1. 문서 클러스터링
  2. 유사한 문서 찾기
  3. 문서 분류 문제


또한 Vectorization 방법 중 벡터화에 신경망을 사용하지 않을 경우를 정리할 수 있습니다.

  • 벡터화에 신경망을 사용하지 않을 경우
    • 단어에 대한 벡터 표현 방법 : 원-핫 인코딩 Vs 워드 임베딩
    • 문서에 대한 벡터 표현 방법 : Document Term Matrix(DTM)
    • 문서에 대한 벡터 표현 방법에 가중치를 넣는 방법 : TF-IDF


문서가 있을 때, 이를 DTM으로 표현한다면 문서 하나가 벡터가 됩니다. 만약 문서가 있을 때, 문서 내의 모든 단어를 워드 임베딩 또는 원-핫 인코딩 으로 표현한다면 단어 하나는 벡터가 되고, 문서 하나는 행렬이 됩니다.

또한 인공 신경망으로 단어 임베딩과 유사하게 문서 임베딩 벡터를 얻는 방법도 존재합니다.

  • Ex) Doc2Vec, Sent2Vec, Universal Sentence Encoder, ELMo, BERT


8. Bag of Words 기반의 DTM, TF-IDF와의 딥 러닝의 관계

  • DTM과 TF-IDF를 이용한 NLP
    • DTM과 TF-IDF는 사실 일반적으로 (딥 러닝이 아닌) 머신 러닝 자연어 처리와 사용합니다.
    • 인공 신경망(딥 러닝)의 입력으로 사용하는 경우는 흔한 경우는 아닙니다.
    • 딥 러닝에서는 입력으로 워드 임베딩이라는 보다 강력한 방법이 이미 존재하기 때문입니다.
    • 그럼에도 TF-IDF는 검색 시스템, 추천 알고리즘 등으로 여전히 수요가 많습니다.

Read more