리뷰의 내용이 긍정적인 내용인지 부정적인 내용인지 분류를 해보려고 한다.
본격적인 구현에 앞서 이번 포스팅에서는 분류에 필요한 기본적인 용어들과 개념들에 대해서 알아보려고 한다.
토큰화 (Tokenization)
토큰화는 주어진 텍스트를 토큰 단위로 나누는 작업을 뜻한다.
토큰화를 진행할 때 문자, 단어, 형태소와 같은 기준으로 나눌 수가 있고 spaCy, NLTK, mecab 등등을 사용할 수 있다.
NLTK를 설치해서 토큰화를 한 후 결과를 출력해보자.
import nltk
from nltk.tokenize import word_tokenize
nltk.download('punkt')
text = 'A man is crossing the road'
print(word_tokenize(text))
출력
['A', 'man', 'is', 'crossing', 'the', 'road']
단어 사전 (Vocabulary)
단어 사전은 텍스트의 모든 단어의 집합이다.
여러 텍스트 데이터를 분류하려고 할 때 먼저 해당 텍스트 데이터의 모든 단어들을 정수로 매핑을 해야 한다.
이 과정에서 만들어지는 것이 단어 사전이다.
A man is crossing the road를 기준으로 다음과 같은 단어 사전을 만들 수가 있다. (대문자는 모두 소문자로 바꾸었다고 가정)
{ '<unk>' : 0, '<pad>' : 1, 'a' : 2, 'man' : 3, 'is' : 4, 'crossing' : 5, 'the' : 6, 'road' : 7}
<unk> : 단어 사전에 없는 단어가 존재할 때 사용
<pad> : 문장의 크기를 맞출 때 사용
여기서 위 문장을 정수로 바꾼다면 [2, 3, 4, 5, 6, 7]이 될 것이다.
또한 A woman and a man are crossing the crosswalk라는 문장이 있다면 [2, 0, 0, 2, 3, 0, 5, 6, 0]이 될 것이다.
그런데 보다싶이 첫 문장과 두 번째 문장의 길이가 다르다.
이럴 때 첫 문장의 나머지 부분을 '<pad>'로 채워서 길이를 맞출 수가 있다.
즉 [2, 3, 4, 5, 6, 7, 1, 1, 1]이 되는 것이다.
워드 임베딩 (word embedding)
단어를 표현할 때 원 핫 인코딩을 사용할 수도 있지만 원 핫 인코딩의 경우 단어 사이의 유사도를 판단할 수가 없다.
또한 원 핫 인코딩으로 얻은 벡터는 희소 벡터이고 이 크기는 단어 사전의 크기와 같기 때문에 단어 사전의 크기가 커지면 공간적인 낭비가 발생한다는 문제점도 있다.
이럴 때 워드 임베딩과 밀집 벡터를 사용하면 이러한 문제점을 해결할 수 있다.
우선 벡터를 단어 사전의 크기로 정하는 것이 아닌 사용자가 임의로 정한 길이를 사용한다.
또한 임베딩을 통해 단어 사이의 유사도를 판단할 수 있다.
[0 0 0 1 0 . . . ] 처럼 희소 벡터로 표현하는 것이 아니라 [1.6 -0.5 1.1 4.4 . . . ] 처럼 표현하면 벡터끼리 연산을 통해 유사도를 판단할 수 있다.
단어를 벡터화 할 때는 주로 Word2Vec라는 방식을 사용한다.
Word2Vec에는 CBOW와 Skip-Gram이라는 방식이 존재하는데 CBOW만 알아보자.
CBOW는 주변 단어를 가지고 중심 단어를 예측하는 방식이다.
A woman and a man ___ crossing the crosswalk
위에서 빈 칸의 단어를 예측하고 싶다면 원하는 범위를 정해서 주변 단어를 통해 중심 단어를 예측한다.
범위를 2로 정한다면 a, man, crossing, the이 4 단어를 통해 중심 단어를 예측한다.
아래 그림은 CBOW 방식을 나타낸 그림이다.
우선 입력층에서 원 핫 벡터를 받는다. 이 벡터의 길이는 단어 사전의 크기가 된다.
앞 뒤로 두 개씩 범위를 잡았다면 입력층에서의 원 핫 벡터의 개수는 4개가 될 것이다.
그리고 이 벡터들은 각각 매트릭스 연산을 통해 임베딩 벡터로 변환된다.
위에서 임베딩 벡터의 길이는 원 핫 벡터와다르게 임의로 정할 수 있다고 했다.
여기서 임베딩 벡터의 길이를 M으로 정했다고 가정하자.
그렇다면 당연히 입력층과 투사층 사이의 행렬은 V x M 크기가 된다.
이렇게 주변 단어의 임베딩 벡터들을 구했다면 이 벡터들의 값들의 평균을 내서 여러 개의 임베딩 벡터들을 하나의 임베딩 벡터로 만든다.
이제 이 길이가 M인 임베딩 벡터를 다시 길이가 V인 임베딩 벡터로 만들어야 한다.
임베딩 벡터를 원 핫 벡터의 크기와 동일하게 만들어서 가장 확률이 높은 인덱스를 구해야하기 때문이다.
최종적으로 다음과 같은 길이가 V인 임베딩 벡터가 만들어졌다고 하자.
[0.4 1.5 -2.0 3.5 1.2 ... ]
위 값들 중 가장 큰 값이 3.5라고 한다면 이 값을 1로 만들고 나머지는 0으로 만든다.
[0 0 0 1 0 ...]
즉 단어 사전에서 4번째 값에 해당하는 단어로 예측을 했다는 뜻이다.
이를 위해 길이가 M인 임베딩 벡터를 길이가 V인 임베딩 벡터로 만든 후 원 핫 벡터로 만들어야 한다.
즉 투사층과 출력층 사이의 행렬은 M x V인 행렬이 되어야 한다.
입력층과 투사층 사이의 V x M 행렬과 투사층과 출력층 사이의 M x V 행렬의 값들은 모두 갱신되는 파라미터들이다.
이렇게 모든 텍스트를 돌면서 중심 단어를 최대한 예측하기 위한 방향으로 학습이 진행된 파라미터 행렬 두 개가 생성될 것이다.
이 두 행렬 중 적절하게 조합하거나 선택을 해서 임베딩 벡터로 사용하면 된다.
RNN (Recurrent Neural Network)
은닉층의 노드에서 활성화 함수를 통해 나온 값을 은닉 상태(hidden state)라고 한다.
RNN은 이 값을 출력층과 다음 은닉층의 노드, 총 두 곳으로 보낸다.
즉 특정 시점의 은닉 상태는 여기까지 거쳐온 모든 노드의 값에 영향을 받은 셈이고 이는 이전 상태를 기억하고 있다는 뜻이 된다.
원하는 목적에 따라서 일대다, 다대일, 다대다 등의 RNN 구조가 가능하다.
은닉 상태를 계산하기 위해서는 이전 시점의 은닉 상태, 현재 시점에서의 입력 벡터와 각각의 가중치 또한 필요하다.
이전 시점의 은닉 상태 h(t-1)과 가중치 행렬 W(h)를 행렬 연산한 값과 현재 시점의 입력 벡터 x(t)와 가중치 행렬 W(x)를 행렬 연산한 값과 편향(있다면)을 더해서 활성화 함수를 지난다.
이 값은 다시 가중치 W(t)와 행렬 연산을 통해 출력층으로 보낸다.
위 예시는 은닉층이 하나인 RNN 구조인데 은닉층을 더 추가해서 깊은 RNN으로 만들 수도 있고 이전 시점의 데이터 뿐만 아니라 이후 시점의 데이터 또한 반영하기 위해서 양방향 RNN으로 만들 수도 있다.
또한 텍스트의 길이가 길어짐에 따라 초기 시점의 값들은 잊힐 수 있기 때문에 LSTM (장단기 메모리)로 만들기도 하고 LSTM을 단순화 한 GRU를 만들기도 한다.
파이토치에서 제공해주는 nn.RNN에 대해서 알아보자.
가장 기초적인 RNN부터 만들어보자.
import torch
import torch.nn as nn
input_size = 4 # 보통 입력 벡터의 차원
hidden_size = 5 # 셀의 크기
cell = nn.RNN(input_size, hidden_size)
batch_size = 10 # 미니 배치 크기
time_steps = 6 # 텍스트의 토큰 개수
# 입력 벡터
inputs = torch.Tensor(batch_size, time_steps, input_size)
# outputs : 모든 시점의 은닉 상태
# _status : 마지막 시점 은닉 상태
outputs, _status = cell(inputs)
print(outputs.shape, _status.shape)
위 코드를 해석해보자.
우선 batch_size는 미니 배치의 크기를 의미한다.
1000개의 텍스트 데이터가 있고 batch_size가 10이라면 10개씩 끊어서 총 100번 학습을 진행하게 된다.
time_steps는 보통 텍스트의 토큰 개수이다.
토크나이저를 split을 사용한다면 텍스트의 단어 개수가 될 것이다.
input_size는 사용자가 임의로 정할 수 있는 입력 벡터의 차원이다.
예를 들어 A man is crossing the road가 있고 토크나이저로 split을 사용했다고 해보자.
토큰의 개수는 6개가 될 것이다.
그리고 임베딩 벡터의 차원을 4로 정했다고 해보자.
각각의 토큰이 차원이 4인 임베딩 벡터가 될 것이다.
[ [ 0.5 1.2 2.3 -0.4 ] ( A )
[ 1.1 0.3 -0.5 1.6 ] ( man )
[1.1 2.2 3.3 4.4 ] ( is )
[-1.1 -2.2 -3.3 1.0 ] ( crossing )
[ 1.1 0.3 -0.5 1.6 ] ( the )
[0.5 1.2 2.3 -0.4 ] ] ( road )
그리고 미니 배치 크기가 10이라면 위와 같은 형태가 임베딩 벡터가 10개가 묶여서 입력 값으로 사용될 것이다.
입력 값의 형태는 다음과 같이 (배치 크기, 토큰 개수, 임베딩 벡터 차원)이다.
torch.Size([10, 6, 4])
RNN의 경우 셀의 크기가 5이기 때문에 다음과 같이 5개의 셀이 존재할 것이다.
그리고 RNN에 우리가 정의한 inputs을 넣으면 두 가지를 반환한다.
첫 번째는 모든 시점의 은닉 상태이고 두 번째는 마지막 시점의 은닉 상태이다.
모든 시점의 은닉 상태의 형태는 (배치 크기, 토큰 개수, 셀의 크기)이다.
이번 포스팅에서는 구현에 앞서 기본적인 개념들과 용어들에 대해서 알아보았다.
다음 포스팅에서는 파이 토치를 통해 직접 구현해보자.
참고
'공부 > AI' 카테고리의 다른 글
[LangChain] LangChain 개념 및 사용법 (0) | 2023.10.18 |
---|---|
[PyTorch] 긍정 리뷰, 부정 리뷰 분류하기 (3) - 모델 변경 (GRU) (0) | 2023.09.19 |
[PyTorch] 긍정 리뷰, 부정 리뷰 분류하기 (2) - 구현 (임베딩, RNN) (0) | 2023.09.18 |
[PyTorch] MNIST로 학습한 CNN 모델로 웹 캠 손 글씨 숫자 인식하기 (2) (0) | 2023.09.10 |
[PyTorch] MNIST로 학습한 CNN 모델로 웹 캠 손 글씨 숫자 인식하기 (1) (0) | 2023.09.09 |
댓글