텍스트를 위한 딥러닝
작성자 : 김혜영
자연어 처리
컴퓨터 과학에서 자연어란 사람들이 사용하는 영어, 한국어 같은 일반적인 언어를 뜻한다. 자연어의 반대말로는 기계가 사용하는 기계어가 있다. 자연어는 기계가 이해하기 어려운 아주 고급 언어다. 사람들이 기계에게 명령을 내리기 위해선 자연어를 번역하는 과정이 있다.(컴퓨터 구조라는 과목에서 배울 수 있다.) 이런, 자연어를 처리하는 분야를 NLP(Natural Langage Processing) 이라고 지칭한다.
AI가 없는 초기의 NLP는 응용 언어학의 입장에서 시도되었다. 간단한 챗봇이나 아주 기초적인 기계번역을 수행할 수 있었다.
하지만, 쓸만한 자연어 처리 프로그램을 만드는 것은 굉장히 어려웠다. 이후, 현대적으로 AI가 도입된 NLP는 다음과 같이 활용되고 있다.
- 텍스트 분류
- 콘텐츠 필터링
- 감성 분석
- 언어 모델링
- 번역
- 요약
물론, 위의 예시는 사람처럼 언어를 이해하는 것이 아닌 통계적인 규칙성을 찾는 프로그램들이다.
과거 nlp는 느리게 발전했었다. 그러다, 2015년 초 순환 신경망(RNN)에 대한 관심이 커지면서 케라스가 처음으로 LSTM 구현을 제공했다. 오늘날엔 트랜스포머(Transformer)가 RNN을 대체했다.
ChatGPT는 초거대 AI로 마찬가지로 nlp분야의 AI이다.
텍스트 데이터 준비
미분 가능한 함수인 딥러닝 모델은 수치 텐서만 처리 가능하다. 이 말은 곧 원시 텍스트 데이터 자체를 입력으로 사용할 수 없다는 뜻이다. 텍스트 데이터를 사용하기 위해선 특수한 처리가 필요하다. 텍스트 벡터화 는 텍스트를 수치 텐서로 바꾸는 과정이다.
- 텍스트를 표준화한다.(소문자로 바꾸거나 구두점을 제거하는 등)
- 텍스트를 토큰(Token) 단위로 분활한다.
- 각 토큰을 수치 벡터로 변경. 트큰을 인덱싱한다.
1. 텍스트 표준화
텍스트 표준화는 모델이 인코딩 차이를 고려하지 않도록 제거한 기초적인 형태이다. 영어 모델에서 사용되는 대문자를 모두 소문자로 변경하고 구문점을 제거하는 등의 정규화 작업을 의미한다.
2. 토큰화
모든 정규화 작업이 끝나면 텍스트 데이터를 벡터화할 단위로 쪼개야한다.
- 단어 수준 토큰화 : 공백을 기준으로 토큰화(영어)
- N-그램 토큰화 : N개의 연속된 단어 그룹으로 토큰화
- 문자 수준 토큰화 : 잘 쓰이지는 않지만 각 문자가 하나의 토큰이 됨.
3. 어휘 사전 인덱싱
텍스트를 토큰으로 나눈 후 각 토큰을 수치 표현으로 인코딩해야한다. 실전에선 모든 토큰의 인덱스를 만들어 어휘 사전의 각 항목에 고유한 정수를 할당하는 방법을 사용한다.
vocabulary = {}
for text in dataset:
text = standardize(text)
tokens = tokenize(text)
for token in tokens:
if token not in vocabulary:
vocabulary[token] = len(vocabulary)
위의 코드를 사용하면 사전에 정의된 고유한 정수를 할당할 수 있다.
할당한 고유한 정수를 신경망이 처리할 수 있도록 원-핫 벡터 같은 벡터 인코딩으로 변경할 수 있다.
def one_hot_encode_token(token):
vec = np.zeros((len(vocabulary),))
token_index = vocabulary[token]
vec[token_index] = 1
return vec
이 때, 주의할 것이 있다. 어휘 사전 인덱스에서 새로운 토큰을 찾을 때 이 토큰이 항상 존재하지 않을 수 있다. (예를 들어 신조어나 잘 안 쓰여서 등록되지 않은 단어) 이 땐, 예외 어휘 인덱스를 사용해야한다.(약어로 OOV) 사전에 없는 모든 토큰에 대응된다. 보통 1이 매칭된다.
만약 OOV 토큰으로 인덱싱 된다면 그 단어는 인식할 수 없는 단어라는 의미가 된다.
4. TextVectorization 층 사용하기
지금까지 설명한 방법론을 그대로 적용하면 성능이 높지 않다. 실전에선 빠르고 효율적으로 케라스 Text Vectorization 층을 사용한다.
from tensorflow.keras.layers import TextVectorization
text_vectorization = TextVectorization(
output_mode = "int",
)
기본적으로 TextVectorization 층은 텍스트 표준화를 위해 소문자로 바꾸고 구두점을 제거한 후 토큰화를 위해 공백으로 나눈다. 또, 기본적으로 어휘사전을 제공하고, 원하면 사용자 정의 사전을 추가할 수 있다.
만약, 사용자 정의 함수를 사용할려면 tf.string 텐서를 처리해야한다.
import re
import string
import tensorflow as tf
def custom_standardization_fn(string_tensor):
# 대문자를 소문자로 변경
lowercase_string = tf.strings.lower(string_tensor)
# 구두점 삭제
return tf.strings.regex_replace(
lowercase_string. f"[{re.excape(string.punctuation)}]", ""
)
# 공백을 기준으로 문자열 나누기
def custom_split_fn(string_tensor):
return tf.strings.split(string_tensor)
text_vectorization = TextVectorization(
output_mode = "int",
standardize = custom_standardization_fn,
split=custom_split_fn,
)
케라스에서 사용하는 어휘 사전은 빈도 순으로 정렬되어 있다.
단어 그룹을 표현하는 두 가지 방법: 집합과 시퀀스
어떻게 단어의 순서를 표현하는지는 여러 NLP 아키텍처를 발생시키는 핵심 질문이다. 가장 쉬운 방법은 순서를 무시하고 텍스트를 단어의 집합(즉, 순서가 없는)으로 처리하는 것이다. 이를 BoW 모델이라고 한다. 만약, 단어를 시계열의 타임 스텝처럼 하나씩 등장하는대로 처리해야한다고 하면 순환신경망을 활용할 수 있다. 마지막으로, 트랜스포머 아키텍처는 기술적으로 순서에 구애받지 않지만 처리하는 표현에 단어 위치 정보를 주입할 수 있다. 이 때문에 트랜스포머는 순서에 구애받지 않으면서 단어 순서를 고려할 수 있다.
IMDB의 영화 리뷰 데이터를 이용해서 감정분석 NLP를 실습해보자.
집합
1. 데이터셋
아래 이미지와 같이 명령어를 입력해 데이터셋을 다운받고 압축을 풀어준다.
그 다음 트레이닝 셋, 테스트 셋 등등 후딱 만들어준다.
2. 단어를 집합으로 처리:BoW
단어의 순서를 무시하고 토큰의 집합을 다루는 방식으로 처리해보자.
text_vectorization = TextVectorization(
max_tokens=20000,
output_mode="multi_hot",
)
text_only_trains_ds = train_ds.map(lambda x, y: x)
text_vectorization.adapt(text_only_train_ds)
binary_1gram_train_ds = train_ds.map(
lambda x, y: (text_vectorization(x), y),
num_parallel_calls=4
)
binary_1gram_val_ds = val_ds.map(
lambda x, y: (text_vectorization(x), y),
num_parallel_calls=4
)
binary_1gram_test_ds = test_ds.map(
lambda x, y: (text_vectorization(x), y),
num_parallel_calls=4
)
이 다음 모델을 빌드해준다.
from tensorflow import keras
from tensorflow import layers
def get_model(mas_tokens=20000, hidden_dim=16):
inputs = keras.Input(shape=(max_tokens,))
x = layers.Dense(hidden_dim, activation = "relu")(inputs)
x = layers.Dropout(0.5)(x)
outputs = layers.Dense(1, activation="sigmoid")
model = keras.Model(inputs, outputs)
model.compile(optimizer = "rmsprop",
loss="binary_crossentropy",
metrics = ["accuracy"])
return model
이 모델을 훈련하고 테스트하면 89.2% 정확도를 얻을 수 있다고 나온다.
이제 인코딩을 개선해보자. 하나의 개념이 여러 단어로 표현될 수 있기에 단어 순서를 무시하는 것은 성능에 굉장히 안좋다.. 단일 단어가 아닌 N-그램을 사용해 순서를 BoW 표현에 추가하면 된다.
N-그램을 반환할 방법은 간단하게 매개변수를 전달하면 된다.
text_vectorization = TextVectorization(
ngrams = 2,
max_tokens = 20000,
output_mode = "multi_hot",
)
위 방법으로 인코딩하면 동일한 모델을 훈련시켰을 때 90.4%의 정확도가 나온다.
만약 매개변수의 output_mode에 count를 사용한다면 개별 단어나 N-그램의 등장 횟수를 카운트한 정보를 추가할 수 있다.
시퀀스
위에서 살펴봤듯, 단어 사이 순서는 정확도에 영향을 끼친다. 이번엔 자동으로 단어 시퀀스를 모델에 전달하는 시퀀스 모델에 대해서 알아보자.
시퀀스 모델을 구현하려면 먼저 입력 샘플을 정수 인덱스의 시퀀스로 표현해야한다. (하나의 정수가 단어하나를 의미함) 마지막으로 순환신경망 즉, 벡터 사이의 특징을 비교할 수 있는 층에 전달한다.
정수 시퀀스를 반환하는 데이터 셋
from tensorflow.keras import layers
max_length = 600
max_tokens = 20000
text_vectorization = layers.TextVectorization(
max_tokens=max_tokens,
output_mode="int",
output_sequence_length = max_length,
)
text_vectorization.adapt(text_only_train_ds)
int_train_ds = train_ds.map(
lambda x, y: (text_vectorization(x), y),
num_parallel_calls=4
)
int_val_ds = val_ds.map(
lambda x, y: (text_vectorization(x), y),
num_parallel_calls=4
)
int_test_ds = test_ds.map(
lambda x, y: (text_vectorization(x), y),
num_parallel_calls=4
)
모델은 다음과 같이 설계한다. 정수 시퀀스를 벡터 시퀀스로 바꾸는 가장 간단한 방법은 정수를 원-핫 인코딩하는 것이다. 간단한 양방향 LSTM 층을 추가해준다.
import tensorflow as tf
inputs = keras.Input(shape=(None, ), dtype = "int64")
embedded = tf.one_hot(inputs, depth = max_tokens)
x = layers.Bidirectional(layers.LSTM(32))(embedded)
x = layers.Dropout(0.5)(x)
outputs = layers.Dense(1, activation = "sigmoid")(x)
model = keras.Model(inputs, outputs)
model.compile(optimizer = "rmsprop",
loss = "binary_crossentropy",
metrics = ["accuracy"])
model.summary()
이 모델을 이용해 훈련하면 느리다.. 그리고 정확도도 87%저도로 낮다. 가장 간단하지만 원-핫 인코딩으로 단어를 벡터로 바꾸는 것은 좋지 않다.
단어 임베딩
단어 임베딩(word embedding) 은 사람의 언어를 좀 더 정확하게 표현하기 위한 벡터 표현이다.
원-핫 인코딩은 단어 집합의 크기를 벡터의 차원으로 하고, 표현하고 싶은 단어의 인덱스에 대해 1의 값을 부여하고, 다른 인덱스에는 0을 부여하는 단어의 벡터 표현 방법이다. 하지만 이 방법은 유사한 두 단어를 아예 다르다고 판단하는 오류가 있을 수 있어 단어임베딩이 필요하다.
아래 이미지는 원핫 인코딩과 단어 임베딩의 비교 이미지이다.
위 그림에서 확인할 수 있듯이 단어 임베딩은 많은 정보를 더 적은 차원으로 압축한다. 단어 임베딩을 사용하면 비슷한 단어는 가까운 위치에 임베딩된다. 즉, 임베딩 공간의 특정 방향이 의미를 가질 수 있다.
위 그림을 확인하면 cat에서 tiger로 변화하는 벡터와 dog에서 wolf로 변화하는 벡터가 유사한 것을 살필 수 있다. 아래는 벡터를 직접 그리며 한 비교 이미지이다.(마우스여서 그림이 좀 구리넹..ㅎ)
실제 단어 임베딩 공간에서 의미 있는 기하학적 변환은 두 벡터의 덧셈이다.
간단한 예로, king 벡터에 female을 더하면 queen이 된다.
-
현재 작업과 함께 단어 임베딩을 학습한다. 이런 설정에서는 랜덤한 단어 벡터로 시작해 신경망의 가중치를 학습하는 것과 같은 방식으로 단어 벡터를 학습한다.
-
현재 풀어야하 할 문제와 다른 머신 러닝 작업에서 미리 계산된 단어 임베딩을 모델에 로드한다.
1. Embedding층으로 단어 임베딩 학습하기
아래 코드를 이용하면 Embedding 층을 만들 수 있다.
embedding_layer = layers.Embedding(input_dim=max_tokens, output_dim=256)
Embedding 층은 정수 인덱스를 밀집 벡터로 매핑하는 딕셔너리로 이해하면 편하다.
임베딩 층은 크기가 (batch_size, sequence_length)인 랭크-2 정수 텐서를 입력으로 받는다. 가중치는 다른 층과 동일하게 랜덤으로 초기화된다.
# 임베딩 층을 포함한 모델 설계
inputs = keras.Input(shape=(None,), dtype="int64")
embedded = layers.Embedding(input_dim=max_tokens, output_dim=256)(inputs)
x = layers.Bidirectional(layers.LSTM(32))(embedded)
x = layers.Dropout(0.5)(x)
outputs = layers.Dense(1, activation = "sigmoid")(x)
model = keras.Model(inputs, outputs)
model.compile(optimizer="rmsprop",
loss="binary_crossentropy",
metrics=["accuracy"])
model.summary()
callbacks=[
keras.callbacks.ModelCheckpoint("embeddings_bidir_lstm.keras",
save_best_only=True)
]
model.fit(int_train_ds, validation_data=int_val_ds, epochs=10, callbacks=callbacks)
model = keras.models.load_model("embeddings_bidir_lstm.keras")
위 코드로 모델을 훈련하면 원-핫 코딩보단 빠르게 학습이 진행된다. 다만, 아직도 정확도는 87%로 낮다.
패딩과 마스킹
입력 시퀀스가 0으로 가득 차 있으면 모델의 성능에 나쁜 영향을 미친다. 위 코드에선 600개의 토큰 보다 긴 문장은 600개의 토큰 길이로 잘리는데, 그 보다 짧은 문장은 뒤에 0으로 채우기 때문에 모델의 성능이 좋지 않다.
마스킹이란 특정 값들을 건너뛰어서 모델 연산에 방해가 되지 않도록하는 기법이다.
embedded = layers.Embedding(input_dim = max_tokens, output_dim=256, mask_zero=True)(inputs)
위의 코드를 사용하면 쉽게 마스킹 API를 사용할 수 있다. 실제로 적용하면 정확도가 88%로 소폭 상승한다.
2. 사전 훈련된 단어 임베딩
훈련 데이터가 부족하면 미리 계산된 임베딩 벡터를 로드할 수 있다. 연구나 산업에 많이 적용되는 임베딩으론 Word2Vec 알고리즘이 있다.
트랜스포머 아키텍처
트랜스포머는 순환 층이나 합성곱 층 없이 ‘뉴럴 어텐션’이라고 지칭하는 간단한 메커니즘을 사용해 강력한 시퀀스 모델을 만들었다.
셀프 어텐션
모델에 전해지는 모든 입력 정보가 현재 작업에 동일하게 중요하지는 않다. 따라서, 모델은 특성에 더 주목하고 덜 주목해야한다.
아래 이미지는 어텐션에 대한 시각화이다.
이처럼 중요한 데이터에 더 집중하는 것을 어텐션이라고 한다.
nlp에선 어텐션을 사용해 문맥 인식 특성을 만들 수 있다. 예를 들어, 시장하네요의 시장과 시장에 가다의 시장은 전혀 다른 의미이다. 하지만 기존의 모델은 이 차이를 잘 알지 못했다. 하지만 어텐션을 사용하면 둘의 벡터 표현이 달라질 수 있다.
인풋 시퀀스의 station이 방송국 station인지 국제 우주 정거자 station인지 구별을 시각화한 것이다.
첫 번째로 station과 나머지 인풋 단어를 비교해 관련성 점수를 계산한다. 이것을 어텐션 점수라고 한다. 두 단어 벡터 사이의 점곱을 사용해 관계의 강도를 측정한다. 그 다음 문장에 있는 모든 단어 벡터의 합을 계산한다. 가중치에 따라 계산한다. 이미지에선 train이라는 단어 때문에 station의 의미가 역이라는 것을 확인할 수 있다.
일반화된 셀프 어텐션
기계 번역에선 2개의 입력 시퀀스를 다뤄야한다. 현재 번역하려는 소스와 변환하려는 타깃 이렇게 두개를 다뤄야한다.
시퀀스-투-시퀀스는 트랜스포머에서 한 시퀀스를 다른 시퀀스로 변환하기 위해 고안된 모델이다.
셀프 어텐션 메커니즘은 다음과 같이 수행된다.
inputs(A) 있는 모든 토큰이 inputs(B)에 있는 토큰과 얼마나 관련되어 있는지 계산하고, 이 점수를 이용해 inputs(C)에 있는 모든 토큰의 가중치 합을 계산한다를 의미한다.
이 때, A는 Query 즉, 어떤 문장이고 B는 Key라고 지칭한다. 어떤 일련의 키워드라 생각하면 편하다. 마지막으로 C는 최정적인 결과값인 Value다.
개념적으로 트랜스포머 스타일의 어텐션이 하는 일을 찾아봤다.
멀티 헤드 어텐션
여러 개의 어텐션 헤드로 구성되어, 각 헤드가 서로 다른 부분을 집중하는 모델이다.
트랜스포머 인코더
트랜스포머 인코더는 입력 시퀀스를 표현하는 역할을 한다. 보통, 여러 셀프 어텐션 레이어로 구성되며, 입력 시퀀스의 각 위치에 대한 정보를 모델이 고려할 수 있게 한다.
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
class TransformerEncoder(layers.Layer):
def __init__(self, embed_dim, dense_dim, num_heads, **kwargs):
super().__init__(**kwargs)
self.embed_dim = embed_dim
self.dense_dim = dense_dim
self.num_heads = num_heads
self.attention = layers.MultiHeadAttention(
num_heads_num_heads, key_dim=embed_dim
)
self.dense_proj = keras.Sequential(
[layers.Dense(dense_dim, activation="relu"), layers.Dense(embed_dim),]
)
self.layernorm_1 = layers.LayerNormalization()
self.layernorm_2 = layers.LayerNormalization()
def call(self, inputs, mask=None):
if mask is not None:
mask = mask[:.tf.newaxis, :]
attention_output = self.attention(
inputs, inputs, attention_mask=mask
)
proj_input = self.layernorm_1(inputs + attention_output)
proj_output = self.dense_proj(proj_input)
return self.layernorm_2(proj_input + proj_output)
def get_config(self):
config = super().get_config()
config.update({
"embed_dim":self.embed_dim,
"num_heads":self.num_heads,
"dense_dim":self.dense_dim,
})
return config
위 코드는 layer층을 상속해 구현한 트랜스포머 인코더이다.
트랜스포머는 기술적으로 순서에 구애받지 않지만 모델이 처리하는 표현에 순서 정보를 수동으로 주입하는 하이브리드 방식이다.
텍스트 분류를 넘어: 시퀀스-투-시퀀스 학습
시퀀스-투-시퀀스 모델에 대해서 더 자세하게 배워보자.
시퀀스-투-시퀀스 모델은 입력으로 시퀀스를 받아 이를 다른 시퀀스로 변경한다.
- 기계번역
- 텍스트 요약
- 질문 답변
- 챗봇
- 텍스트 생성
시퀀스-투-시퀀스 모델의 일반적인 구조는 인코더 디코더 구조이다.
인코더는 모델이 소스 시퀀스를 중간 표현으로 바꾼다. 디코더는 이전 토큰과 인코딩된 소스 시퀀스를 보고 타깃 시퀀스에 있는 다음 토큰을 예측하도록 훈현된다.
아래 그림은 어텐션 메커니즘을 소개한 논문인 “Attention is All New Need”의 모델이다.