저번 문장 생성의 경우 RNN의 하나인 GRU를 이용해 문장을 생성해보았다.
이번엔 언어 모델계 강력한 모델인 GPT...는 아니고, GPT구조를 간략하게 만든 mini-gpt를 이용해 문장을 생성해보려고 한다.
GPT를 이해하기 위해서는 이해해야 하는 선행 개념들이 있다.
여러 블로그와 글들을 참고해서 나름의 정리를 하는데 틀릴수 있으니 걸러서 보면된다.
attention이 뭔지?
-한문장으로 요약하자면 학습할때 중요한 부분만 보고, 나머지는 무시해서 학습 결과를 높힌다는 개념
transformer가 뭔지?
transformer가 뭔지 알기 위해서는 seq2seq 부터 짚고 넘어가야 한다.
seq2seq 란 특정 속성을 지닌 데이터 나열(sequence)를 다른 속성의 데이터 나열(sequence)로 변환하는 작업을 말한다.
보통 자연어 처리 분야에서 기계번역에 사용된다.(영어-> 한국어)
seq2seq는 encoder와 decoder로 구성되어있는데
encoder 는 소스 시퀀스를 받아 압축해 소스 정보를 만들어 내는 역할을 하고,
decoder 는 앞에서 넘겨준 소스 정보를 통해 타겟 시퀀스를 만들어내는 역할을 한다.
기존의 경우엔 encoder, decoder를 RNN을 이용해 만들었지만, attention 알고리즘의 등장이후에
RNN + attention을 통해 성능이 향상됨을 확인했다.
그이후 RNN도 걷어내, attention 만을 이용해 encoder와 decoder를 만들었는데 그게
transformer라고 할수 있겠다.
( 아닐수있다. 안잊어 먹으려고 기록하는 용도이지만 누군가 지적해주신다면 언제든 수정하겠다.)
간략히 정리했지만 더 자세히 알고싶은 사람( Like me) 을 위해 찾아본 링크를 남기겠다.
글 : ratsgo.github.io/nlpbook/docs/language_model/transformers/
동영상 강의 : www.boostcourse.org/ai331
그래서 GPT가 뭔데?
언어 모델 중 하나이고, 이전 단어들이 주어졌을때 다음 단어가 무엇인지 맞추는 과정을 학습하는 모델이다.
transformer의 decoder 부분 만을 이용한다. ( encoder가 없기 때문에, encoder의 결과값을 받는 multi-head attention부분의 step이 없다.)
기존 RNN처럼 문장생성시 이전 단어들의 정보를 순차적으로 중첩해 연산하는 것이 아닌
이전 단어중 중요한 부분만 attention을 통해 가려서 연산하기 때문에 결과가 좋게 나온...다고 한다.
구조는 어떻게?
위 그림을 보고 순서대로 진행하면 된다.
1. text vectorize
- 문장 -> sequence of number로 변환
(차후 문장이 아닌 다른 sequence라도 숫자로면 변환 시킬수 있다면 모델 생성이 가능해보임)
2. embedding
- token embedding & positional embedding
- token embedding -> GRU 때 했던것 처럼 단어 index 를 지정된 크기의 벡터로 맵핑
- positional embedding -> 기존에 없던 과정으로 각 단어의 "위치 값"에 대한 embedding
-------------------------------------------------------------------------------------------------------- decoder block 이전
3. multi-head masked attention
4. layer add & normalization
- 이전 단계의 원 벡터(attention을 거치지 않은) 와 attention을 거친 벡터간의 normalize & add 연산 수행
- 원 문장 정보와 attention을 통과한 문장 정보간의 합산이 결과적으로 좋기 때문에 한것..같다.
5. feed forward
6. layer add & normalization
-------------------------------------------------------------------------------------------------------- decoder block 종료
7. Linear func & softmax
- 결과로 나온 단어 벡터 -> 확률로 변환해 어떤 단어가 확률이 높은지 연산
중간 decoder block에 multi-head masked attention 부분에 대한 서술을 따로 설명을 해야 할 것 같아 비워두었다.
"multi-head" + "masked" + "attention" 로 쪼개서 살펴보면
attention은 현재 문장에서 각 단어별로 다른 특정 단어와 연관성을 살펴보는 알고리즘이다.
masked 라는 수식어가 붙은 이유는 문장 생성시 이전 단어들만을 본다는 의미이기 때문이다.
(이후 단어들은 안보이게 masked 처리)
예를 들어 "나는 밥을 먹는다" 라는 문장이 있을때 "나는 밥" 다음 "을" 의 경우엔 "먹는다"라는 정보는 "을" 단계에서는 고려되지 않는다.
multi-head 이라 함은 head가 여러개를 의미하는 말인데...
head는 입력으로 받은 단어 벡터에 대해 query, key, value로 나타내 다른 단어 벡터간의 연관성을 찾는다.
조금 더 구체적으로 얘기해보자면,
1. query_weight, key_weight, value_weight 벡터를 통해 단어 벡터를 query, key, value로 나타내고,
2. query 벡터와 자기자신 포함 입력된 단어 key 벡터간의 dot product 연산을 통해 값을 얻고
3. 해당 결과값에 value값을 연산해 단어간의 연관성을 나타낸다.
multi head는 저 과정을 여러개 돌린다는 것을 의미한다.
이제 대충 개념도 알았고, 구조도 알았고, 순서도 알았으니 코드를 통해 살펴보자.
코드는 keras.io/examples/generative/text_generation_with_miniature_gpt/
이 링크의 코드를 사용했다.
지난번 문장 생성때 처럼 colab 을 이용했다.
먼저 내 구글 드라이브와 연결해준다.
지난번 모았던 네이버 뉴스 기사 댓글 데이터를 가져온다.
위 gpt 코드에서 필요로 하는 모듈은
이렇다. 나는 기사 댓글로 불러와야 하므로 필요한 모듈 몇개를 더 선언해 사용한다.
%matplotlib inline
import pandas as pd
from tqdm.notebook import tqdm
import numpy as np
import matplotlib.pyplot as plt
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
from tensorflow.keras.layers.experimental.preprocessing import TextVectorization
import os
import re
import string
import random
데이터를 불러와보자
위 코드상에서 불러와서 전처리 하는 부분이있다. 나는 문장생성할때 가능한 전처리 없이 하고싶은데
띄어쓰기의 경우 간혹 2칸씩 입력되어있는 경우가 있다. 해당 부분에 대해서는 1칸으로 처리하는 코드이다.
df = pd.read_pickle('/content/drive/My Drive/Colab Notebooks/data/comment_20210328.pkl')
# spacing 이 2번이상 인경우를 제외하기 위함
comment_preprocessing_list = []
for i, row in tqdm(df.iterrows()):
comment = row['comment'] # 댓글 불러오기
# 띄어쓰기대로 나눠 어절만 가져오기, 가져온 어절을 " " 1칸 단위로 합쳐주기
comment = ' '.join(comment.split())
comment = comment.strip() # 앞뒤 띄어쓰기 존재시 제거 위함
comment_preprocessing_list.append(comment) # 새로운 dataframe 만들기 위한 리스트 추가
df = pd.DataFrame(comment_preprocessing_list, columns= ['comment']) # 새로운 df 생성
df.head()
문장 길이에 대해서 살펴보자.
문장 길이를 제한해 입력할 것이기 때문에 어느정도 선에서 잘라야 하는지 알아보기 위함이다.
내 경우엔 문장 길이 max를 40으로 설정했다.
이제 input으로 넣기 위한 문장 -> 벡터로 변환해야 한다.
이전 글에 비해 모르는 tensorflow 함수가 많이 늘어났다.
하지만 코드를 참고한 사이트에선 딱히 코드에 대한 설명이 없어 직접 찾아봤다. (부족하다는 말이야)
먼저
tf.data.Dataset.from_tensor_slices
ref) www.tensorflow.org/api_docs/python/tf/data/Dataset
해당 링크를 통해 가져온 바로 요약하자면 입력데이터 올릴때, 메모리 사용을 좀더 효율적으로 바꿔주는것 같았다.
내가 사용하려는 [문장1, 문장2, 문장3,,,] 형태의 문장 리스트를 사용하려면 from_tensor_slices 함수를 이용해야 한다.
text_ds.shuffle과 text_df.batch의 경우
shuffle은 입력 데이터 섞는 함수 이고,
batch는 한번에 입력되는 데이터 값을 조정하는 함수 이다.
tf.keras.layers.experimental.preprocessing.TextVectorization
tokenizing 후 vector로 만들기 위한 모듈이다.
이것 역시 링크를 통해 자세히 살펴보고, 정리해보자
www.tensorflow.org/api_docs/python/tf/keras/layers/experimental/preprocessing/TextVectorization
- max_tokens : 단어 개수 제한, None 일경우, 제한 없이 만들기
- OOV를 위한 token을 만들기때문에 so the effective number of tokens is (max_tokens - 1)
- standardize : 단어 전처리를 위한 파라미터
- None : 처리x
- (기본)lower_and_strip_punctuation : 영어 소문자 변환, 앞뒤 공백 제거, 기호제거
- 함수 인자 가능
- split
- token으로 나누는 기준 설정
- (기본) whitespace : 공백 기준
- ngrams
- split 기준으로 나눠진 token을 통해서 ngram 만들어주는 변수
- int값이나 int tuple([1,2,3,..])등을 인자로 받음
- ex) ngram =2, input = ["hi hello world"], output = ['hi hello' 'hello world']
- output_mode
- 해당 함수의 output 결정
- int : Outputs integer indices, one integer index per split string token
- 0 is reserved for masked locations
- binary : 0 또는 1의 배열? (이해안감)
- 해당 인덱스에 매핑 된 토큰이 배치 항목에 한 번 이상 존재하는 모든 요소에 1을 포함하는 vocab_size 또는 max_tokens 크기의 배치 당 단일 int 배열을 출력합니다.
- count : 등장 횟수 카운트?
- "binary"로하지만 int 배열에는 해당 인덱스의 토큰이 배치 항목에 나타난 횟수가 포함됩니다.
- tf-idf : 단어 tfidf score?
- "바이너리"이지만 각 토큰 슬롯에서 값을 찾기 위해 TF-IDF 알고리즘이 적용됩니다.
- output_sequence_length
- Only valid in INT mode
- time dimension padded or truncated to exactly output_sequence_length values
- a tensor of shape [batch_size, output_sequence_length]
- regardless of how many tokens resulted from the splitting step
- pad_to_max_tokens
- Only valid in "binary", "count", and "tf-idf" modes
- If True, the output will have its feature axis padded to max_tokens even if the number of unique tokens in the vocabulary is less than max_tokens,
- 구글 번역) True이면 어휘의 고유 토큰 수가 max_tokens 미만인 경우에도 출력의 기능 축이 max_tokens로 채워집니다.
- a tensor of shape [batch_size, max_tokens]
- regardless of vocabulary size
- vocabulary
- token set을 지정해줄수있음
- An optional list of vocabulary terms, or a path to a text file containing a vocabulary to load into this layer
# comment list build
comment_list = []
for i, row in tqdm(df.iterrows()):
comment = row['comment']
comment_list.append(comment)
# tensorflow 입력값에 맞추기 위해 데이터셋 객체로 변환
text_ds = tf.data.Dataset.from_tensor_slices(comment_list)
# batchsize 설정해 메모리 터지는거 방지
batch_size = 128
text_ds = text_ds.shuffle(buffer_size=256)
text_ds = text_ds.batch(batch_size)
vocab_size = 200000
maxlen = 40
vectorize_layer = TextVectorization(
# standardize=custom_standardization,
max_tokens=vocab_size - 1,
output_mode="int",
output_sequence_length=maxlen + 1,
)
vectorize_layer.adapt(text_ds)
vocab = vectorize_layer.get_vocabulary() # To get words back from token indices
colab의 경우 단어 vocab이 25만일때는 메모리를 감당을 못해서 대충 20만으로 설정했다.
prepare_lm_inputs_labels는 무엇을 하는 함수인고하니...
문장 벡터를 넣었을때 x, y 값을 뱉어내는 것을 볼수 있다.
예를 들어 [1,2,3,4,5] 라는 벡터가 있을때 [1, 2, 3, 4, 5] -> x : [1,2,3,4,5] / y : [2,3,4,5] 라고 표현 할수 있다.
prefetch 부분의 경우 데이터를 불러올때, 시간을 단축시키는 방법이라고 하나... 정확히 모르겠다.
def prepare_lm_inputs_labels(text):
"""
Shift word sequences by 1 position so that the target for position (i) is
word at position (i+1). The model will use all words up till position (i)
to predict the next word.
"""
text = tf.expand_dims(text, -1)
tokenized_sentences = vectorize_layer(text)
x = tokenized_sentences[:, :-1]
y = tokenized_sentences[:, 1:]
return x, y
text_ds = text_ds.map(prepare_lm_inputs_labels)
text_ds = text_ds.prefetch(tf.data.experimental.AUTOTUNE)
이제 맨 위에서 본 gpt- decoder 부분을 하나씩 만들어 보자
우선 token embedding & positional embedding 부분
class TokenAndPositionEmbedding(layers.Layer):
def __init__(self, maxlen, vocab_size, embed_dim):
super(TokenAndPositionEmbedding, self).__init__()
self.token_emb = layers.Embedding(input_dim=vocab_size, output_dim=embed_dim)
self.pos_emb = layers.Embedding(input_dim=maxlen, output_dim=embed_dim)
def call(self, x):
maxlen = tf.shape(x)[-1]
# token 위치에 따른 embedding을 하기위함
positions = tf.range(start=0, limit=maxlen, delta=1)
positions = self.pos_emb(positions)
x = self.token_emb(x)
return x + positions
decoder block 부분 구현
def causal_attention_mask(batch_size, n_dest, n_src, dtype):
"""
Mask the upper half of the dot product matrix in self attention.
This prevents flow of information from future tokens to current token.
1's in the lower triangle, counting from the lower right corner.
"""
i = tf.range(n_dest)[:, None]
j = tf.range(n_src)
m = i >= j - n_src + n_dest
mask = tf.cast(m, dtype)
mask = tf.reshape(mask, [1, n_dest, n_src])
mult = tf.concat(
[tf.expand_dims(batch_size, -1), tf.constant([1, 1], dtype=tf.int32)], 0
)
return tf.tile(mask, mult)
class TransformerBlock(layers.Layer):
def __init__(self, embed_dim, num_heads, ff_dim, rate=0.1):
super(TransformerBlock, self).__init__()
self.att = layers.MultiHeadAttention(num_heads, embed_dim)
self.ffn = keras.Sequential(
[layers.Dense(ff_dim, activation="relu"), layers.Dense(embed_dim),]
)
self.layernorm1 = layers.LayerNormalization(epsilon=1e-6)
self.layernorm2 = layers.LayerNormalization(epsilon=1e-6)
self.dropout1 = layers.Dropout(rate)
self.dropout2 = layers.Dropout(rate)
def call(self, inputs):
input_shape = tf.shape(inputs)
batch_size = input_shape[0]
seq_len = input_shape[1]
causal_mask = causal_attention_mask(batch_size, seq_len, seq_len, tf.bool)
attention_output = self.att(inputs, inputs, attention_mask=causal_mask)
attention_output = self.dropout1(attention_output)
out1 = self.layernorm1(inputs + attention_output)
ffn_output = self.ffn(out1)
ffn_output = self.dropout2(ffn_output)
return self.layernorm2(out1 + ffn_output)
여기서 저 casual_attention_mask 부분의 모르는 tensorflow 함수들이 많아 뭐지... 싶었다.
하나씩 써보자
tf.range
ref) www.tensorflow.org/api_docs/python/tf/range
tf.range => tf.range(start, limit, delta=1, dtype=None, name='range')
# start = 시작 숫자
# limit = 끝 숫자 (미만 설정으로 끝숫자는 포함 x)
# delta = 증가숫자
# dtype = 데이터 타입(인트, 플롯 등)
# name = ? A name for the operation. Defaults to "range"
tf.range()[:,None] 은 뭐지?
# The newaxis object can be used in all slicing operations to create an axis of length one. :const: newaxis is an alias for ‘None’, and ‘None’ can be used in place of this with the same result.
# newaxis 객체는 모든 슬라이싱 작업에서 길이가 1 인 축을 만드는 데 사용할 수 있습니다. : const : newaxis는‘None’의 별칭이며‘None’을 동일한 결과로 대신 사용할 수 있습니다.
ref) stackoverflow.com/questions/37867354/in-numpy-what-does-selection-by-none-do
tf.cast
ref) www.tensorflow.org/api_docs/python/tf/cast?hl=ko
tf.cast(x, dtype, name=None)
# x -> Tensor
# dtype => 데이터 타입
# x 요소들을 dtype으로 type Casting
tf.expand_dims
ref) www.tensorflow.org/api_docs/python/tf/expand_dims
# tf.expand_dims( input, axis, name=None)
# input => tensor
# axis => 어느차원에 차원을 추가할건지 (0 = x, 1 = y, -1 = z)
tf.constant
ref) www.tensorflow.org/api_docs/python/tf/constant
# Creates a constant tensor from a tensor-like object.
tf.concat
ref) www.tensorflow.org/api_docs/python/tf/concat
# 조금 더 쉬운 설명 ref) lsjsj92.tistory.com/456
# concat([x,y] , axix , name = 'concat')
# axis = 0 -> 가장 바깥에 있는 차원을 기준으로 붙인다
# axis = 1 -> 0번째 차원 이후에 처음의 안쪽 차원을 붙인다
tf.tile
ref)www.tensorflow.org/api_docs/python/tf/tile
# 더 이해하기 쉬운 링크 ref) sysyn.tistory.com/22
# tile(input, multiples, name=None)
# 주어진 텐서 input을 multiples회 복사하여 새로운 텐서를 만듭니다
# multiples: int32형 Tensor. 1-D. 길이는 input의 차원의 수와 같아야 합니다.
그래서 저 casual_attention_mask 함수가 어떻게 동작하는지 예시로 살펴봤다.
하나씩 살펴보니 입력으로 들어오는 batch sequece of number 배열에
해당하는 masked 배열을 생성하는 것을 알 수 있다.
transformer block의 경우,
__init__ 부분에서 block에 사용할 레이어들을 설정하고,
call 부분에서 해당 레이어의 순서를 명시했다.
순서는 위에 적은 것과 같다.
------------------------------------------------ (transformer 시작)
3. multi-head masked attention
- masked vector 구성
- multi-head attention 함수 불러오기
4. Dropout
- 랜덤하게 특정 layer를 0으로
5. layer normalization-1
- 위 ouput vector에 대해 정규화
- 인풋이 되는 output vector= (처음 Embedding vector+ Attention output vector)
6. feed forward network
- 위 ouput vector에 대해 feedfoward 연산 수행
7. layer normalization-2
- 위 ouput vector에 대해 정규화
- 인풋이 되는 output vector= (5번 결과 vector+ 6번 결과 vector)
------------------------------------------------ (transformer 끝)
이제 모델을 정의 해보자
# 변수 정의 및 모델 함수 정의
embed_dim = 256 # Embedding size for each token
num_heads = 2 # Number of attention heads
feed_forward_dim = 256 # Hidden layer size in feed forward network inside transformer
def create_model():
inputs = layers.Input(shape=(maxlen,), dtype=tf.int32)
# input 정의
embedding_layer = TokenAndPositionEmbedding(maxlen, vocab_size, embed_dim)
# token Embedding + positional Embedding layer class 정의
x = embedding_layer(inputs)
# 선언한 Embedding layer class 이용해 Embedding
transformer_block = TransformerBlock(embed_dim, num_heads, feed_forward_dim)
# transformer block layer class 정의
x = transformer_block(x)
# 선언한 transformer layer class 이용해 학습
outputs = layers.Dense(vocab_size)(x)
# 압축된 결과를 vocab에 맞춰 팽창 (후보단어 선별을 위한 각 단어에 대한 결과치 도출)
model = keras.Model(inputs=inputs, outputs=[outputs, x])
# model 정의
loss_fn = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True)
model.compile(
"adam", loss=[loss_fn, None],
) # No loss and optimization based on word embeddings from transformer block
model.summary()
return model
참고한 코드 사이트에는 model에 대한 save, load 부분에 대한 코드가 없기 때문인지
모델 epoch가 끝날때마다, 학습 결과 예시를 출력하게 하는 코드가 있었다.
callback 함수를 상속 받아 동작하게 한 것으로 보인다.
class TextGenerator(keras.callbacks.Callback):
"""A callback to generate text from a trained model.
1. Feed some starting prompt to the model
2. Predict probabilities for the next token
3. Sample the next token and add it to the next input
Arguments:
max_tokens: Integer, the number of tokens to be generated after prompt.
start_tokens: List of integers, the token indices for the starting prompt.
index_to_word: List of strings, obtained from the TextVectorization layer.
top_k: Integer, sample from the `top_k` token predictions.
print_every: Integer, print after this many epochs.
"""
def __init__(
self, max_tokens, start_tokens, index_to_word, top_k=10, print_every=1
):
self.max_tokens = max_tokens
self.start_tokens = start_tokens
self.index_to_word = index_to_word
self.print_every = print_every
self.k = top_k
def sample_from(self, logits):
logits, indices = tf.math.top_k(logits, k=self.k, sorted=True)
indices = np.asarray(indices).astype("int32")
preds = keras.activations.softmax(tf.expand_dims(logits, 0))[0]
preds = np.asarray(preds).astype("float32")
return np.random.choice(indices, p=preds)
def detokenize(self, number):
return self.index_to_word[number]
def on_epoch_end(self, epoch, logs=None):
start_tokens = [_ for _ in self.start_tokens]
if (epoch + 1) % self.print_every != 0:
return
num_tokens_generated = 0
tokens_generated = []
while num_tokens_generated <= self.max_tokens:
pad_len = maxlen - len(start_tokens)
sample_index = len(start_tokens) - 1
if pad_len < 0:
x = start_tokens[:maxlen]
sample_index = maxlen - 1
elif pad_len > 0:
x = start_tokens + [0] * pad_len
else:
x = start_tokens
x = np.array([x])
y, _ = self.model.predict(x)
sample_token = self.sample_from(y[0][sample_index])
tokens_generated.append(sample_token)
start_tokens.append(sample_token)
num_tokens_generated = len(tokens_generated)
txt = " ".join(
[self.detokenize(_) for _ in self.start_tokens + tokens_generated]
)
print(f"generated text:\n{txt}\n")
# Tokenize starting prompt
word_to_index = {}
for index, word in enumerate(vocab):
word_to_index[word] = index
start_prompt = "문재인"
start_tokens = [word_to_index.get(_, 1) for _ in start_prompt.split()]
num_tokens_generated = 40
text_gen_callback = TextGenerator(num_tokens_generated, start_tokens, vocab)
크게 바꾼 부분은 없고,
start_prompt(시작 단어) 변수를 "문재인"으로, num_tokens_generated(생성문장길이) 변수를 40으로 바꿨다.
그럼 모델 학습이 어떻게 되었는지 살펴보자
어...음... 생각보다 잘 만든 것을 확인 할 수 있었다.(악플생성기를 만들어버렸다!)
아쉬운 점
- 정확히 알고 사용했다는 느낌이 아니다. 그래서 그런지 중간중간 ??인 부분도 많고... 그래서 parameter 수정도 못했다.
- model save, load 부분이 없어 열심히 학습한 결과가 공중 분해 되었는데 다 돌아가기 전까진 몰랐다.
다음에 만들 경우엔 그부분을 먼저 조사한 후에 코드에 추가해야 겠다.
중요한건 취미로 하는 코딩이 재밌다는걸 다시금 깨닫게 된 계기가 되었다!
댓글과 지적은 언제나 환영~
'text > Python' 카테고리의 다른 글
python dictionary sort 정리 (sort by key & value) (0) | 2021.08.24 |
---|---|
뉴스 문서 군집화 하기 (document clustering with DBSCAN) (4) | 2021.04.28 |
문장 생성 해보기 (feat. 네이버 기사 댓글) (0) | 2021.04.02 |
DOM Based Content Extraction via Text Density 구현해보기 (3) | 2021.04.01 |
네이버 기사 댓글 가져오기 (5) | 2021.03.29 |
댓글