Exploration 04

2022. 1. 13. 14:13

1. 시퀀스? 스퀀스!I

파이썬에서의 시퀀스 자료형

값이 연속적으로 이어진 자료형들을 총칭하여 ‘시퀀스 자료형(sequence type)’이라고 한다.

인공지능이 예측을 하기위해서는 어느 정도는 연관성이 있어야한다.

2. 다음 am을 쓰면 반 이상은 맞더라

순환신경망(RNN)

우리는 ‘나는 밥을’이라는 input을 가지고 ‘먹었다’라는 output을 예측할 수 있다. 그러면 ‘나는’ 이라는 output을 내려면 어떻게 할까?

순환신경망에서는 output을 다시 input으로 사용해 이러한 문제를 해결한다. 이러한 순환적 구조에서 시작과 끝은 , 라는 것을 사용해 알아낼 수 있다.

sentence = " 나는 밥을 먹었다 "

source_sentence = "<start>" + sentence
target_sentence = sentence + "<end>"

print("Source 문장:", source_sentence)
print("Target 문장:", target_sentence)
'''
>>>
Source 문장: <start> 나는 밥을 먹었다 
Target 문장:  나는 밥을 먹었다 <end>
'''

언어모델 (Language Model)

실습

1. 데이터 전처리

데이터 다운로드

import os, re 
import numpy as np
import tensorflow as tf

# 파일을 읽기모드로 열고
# 라인 단위로 끊어서 list 형태로 읽어옵니다.
file_path = os.getenv('HOME') + '/aiffel/lyricist/data/shakespeare.txt'
with open(file_path, "r") as f:
    raw_corpus = f.read().splitlines()

# 앞에서부터 10라인만 화면에 출력해 볼까요?
print(raw_corpus[:9])

데이터는 완벽한 연극 대본이다. 하지만 필요없는 공백정보가 포함되었다.

for idx, sentence in enumerate(raw_corpus):
    if len(sentence) == 0: continue   # 길이가 0인 문장은 건너뜁니다.
    if sentence[-1] == ":": continue  # 문장의 끝이 : 인 문장은 건너뜁니다.

    if idx > 9: break   # 일단 문장 10개만 확인해 볼 겁니다.

    print(sentence)

토큰화

이제 문장을 일정한 기준으로 쪼개야 하는 토큰화 작업을 해야한다.

띄어쓰기를 기준으로 할 경우 몇 가지의 문제점이 있다.

  1. Hi, my name is John. *("Hi," "my", ..., "john." 으로 분리됨) - 문장부호
  2. First, open the first chapter. *(First와 first를 다른 단어로 인식) - 대소문자
  3. He is a ten-year-old boy. *(ten-year-old를 한 단어로 인식) - 특수문자

이러한 전처리를 위해 정규표현식(Regex)을 이용한 필터링이 유용하게 사용된다.

# 입력된 문장을
#     1. 소문자로 바꾸고, 양쪽 공백을 지웁니다
#     2. 특수문자 양쪽에 공백을 넣고
#     3. 여러개의 공백은 하나의 공백으로 바꿉니다
#     4. a-zA-Z?.!,¿가 아닌 모든 문자를 하나의 공백으로 바꿉니다
#     5. 다시 양쪽 공백을 지웁니다
#     6. 문장 시작에는 <start>, 끝에는 <end>를 추가합니다
# 이 순서로 처리해주면 문제가 되는 상황을 방지할 수 있겠네요!
def preprocess_sentence(sentence):
    sentence = sentence.lower().strip() # 1
    sentence = re.sub(r"([?.!,¿])", r" \1 ", sentence) # 2
    sentence = re.sub(r'[" "]+', " ", sentence) # 3
    sentence = re.sub(r"[^a-zA-Z?.!,¿]+", " ", sentence) # 4
    sentence = sentence.strip() # 5
    sentence = '<start> ' + sentence + ' <end>' # 6
    return sentence

# 이 문장이 어떻게 필터링되는지 확인해 보세요.
print(preprocess_sentence("This @_is ;;;sample        sentence."))
'''
>>>
<start> this is sample sentence . <end>
'''

이상하고 더러운 문장을 넣어도 깔끔하게 표현된다.

자연어처리 분야에서 모델의 입력이 되는 문장을 소스문장(Source Sentence), 정담 역할을 하하는 출력문장을 타겟 문장(Target Sentence)이라고 한다. 위에서 만든 정제화 함수를 통해 토큰화를 진행한 후 끝 단어 를 없애면 소스문장, 첫 단어 를 없애면 타겟 문장이 된다.

# 여기에 정제된 문장을 모을겁니다
corpus = []

for sentence in raw_corpus:
    # 우리가 원하지 않는 문장은 건너뜁니다
    if len(sentence) == 0: continue
    if sentence[-1] == ":": continue

    # 정제를 하고 담아주세요
    preprocessed_sentence = preprocess_sentence(sentence)
    corpus.append(preprocessed_sentence)

# 정제된 결과를 10개만 확인해보죠
corpus[:10]
'''
>>>
['<start> before we proceed any further , hear me speak . <end>',
 '<start> speak , speak . <end>',
 '<start> you are all resolved rather to die than to famish ? <end>',
 '<start> resolved . resolved . <end>',
 '<start> first , you know caius marcius is chief enemy to the people . <end>',
 '<start> we know t , we know t . <end>',
 '<start> let us kill him , and we ll have corn at our own price . <end>',
 '<start> is t a verdict ? <end>',
 '<start> no more talking on t let it be done away , away ! <end>',
 '<start> one word , good citizens . <end>']
'''

우리가 영어를 한국어로 번역하듯 컴퓨터도 언어가 들어오면 숫자로 번역하는 것이 필요하다.

tf.keras.preprocessing.text.Tokenizer패키지는 정재된 데이터를 토큰화하고, 단어사전을 만들어 주며, 딩터를 숫자로 변환까지 한 번에 해준다. 이과정을 벡터화(vectorize)라 하며, 숫자로 변환된 데이터를 텐서(tensor)라고 한다.

# 토큰화 할 때 텐서플로우의 Tokenizer와 pad_sequences를 사용합니다
# 더 잘 알기 위해 아래 문서들을 참고하면 좋습니다
# https://www.tensorflow.org/api_docs/python/tf/keras/preprocessing/text/Tokenizer
# https://www.tensorflow.org/api_docs/python/tf/keras/preprocessing/sequence/pad_sequences
def tokenize(corpus):
    # 7000단어를 기억할 수 있는 tokenizer를 만들겁니다
    # 우리는 이미 문장을 정제했으니 filters가 필요없어요
    # 7000단어에 포함되지 못한 단어는 '<unk>'로 바꿀거에요
    tokenizer = tf.keras.preprocessing.text.Tokenizer( 
        num_words=7000, 
        filters=' ',
        oov_token="<unk>"
    )
    # corpus를 이용해 tokenizer 내부의 단어장을 완성합니다
    tokenizer.fit_on_texts(corpus)
    # 준비한 tokenizer를 이용해 corpus를 Tensor로 변환합니다
    tensor = tokenizer.texts_to_sequences(corpus)   
    # 입력 데이터의 시퀀스 길이를 일정하게 맞춰줍니다
    # 만약 시퀀스가 짧다면 문장 뒤에 패딩을 붙여 길이를 맞춰줍니다.
    # 문장 앞에 패딩을 붙여 길이를 맞추고 싶다면 padding='pre'를 사용합니다
    tensor = tf.keras.preprocessing.sequence.pad_sequences(tensor, padding='post')  

    print(tensor,tokenizer)
    return tensor, tokenizer

tensor, tokenizer = tokenize(corpus)
# 생성된 텐서 데이터를 3번째 행, 10번째 열까지만 출력
print(tensor[:3, :10])
'''
>>>
[[   2  143   40  933  140  591    4  124   24  110]
 [   2  110    4  110    5    3    0    0    0    0]
 [   2   11   50   43 1201  316    9  201   74    9]]
'''
# tokenizer에 구축된 단어 사전 확인
for idx in tokenizer.index_word:
    print(idx, ":", tokenizer.index_word[idx])

    if idx >= 10: break
'''
>>>
1 : <unk>
2 : <start>
3 : <end>
4 : ,
5 : .
6 : the
7 : and
8 : i
9 : to
10 : of
'''

위에 텐서 데이터의 시작이 2인 이류는 tokenizer에 구축된 단어사전 속 의 인덱스가 2이기 때문이다.

소스와 타겟분리

# tensor에서 마지막 토큰을 잘라내서 소스 문장을 생성합니다
# 마지막 토큰은 <end>가 아니라 <pad>일 가능성이 높습니다.
src_input = tensor[:, :-1]  
# tensor에서 <start>를 잘라내서 타겟 문장을 생성합니다.
tgt_input = tensor[:, 1:]    

print(src_input[0])
print(tgt_input[0])

tf.data.Dataset객체 생성

우리는 지금까지 모델을 만들 때 numpy array형태의 데이터 셋을 사용하였지만 RNN에서는 tf.data.Dataset객체를 많이 사용한다.

BUFFER_SIZE = len(src_input)
BATCH_SIZE = 256
steps_per_epoch = len(src_input) // BATCH_SIZE

 # tokenizer가 구축한 단어사전 내 7000개와, 여기 포함되지 않은 0:<pad>를 포함하여 7001개
VOCAB_SIZE = tokenizer.num_words + 1   

# 준비한 데이터 소스로부터 데이터셋을 만듭니다
# 데이터셋에 대해서는 아래 문서를 참고하세요
# 자세히 알아둘수록 도움이 많이 되는 중요한 문서입니다
# https://www.tensorflow.org/api_docs/python/tf/data/Dataset
dataset = tf.data.Dataset.from_tensor_slices((src_input, tgt_input))
dataset = dataset.shuffle(BUFFER_SIZE)
dataset = dataset.batch(BATCH_SIZE, drop_remainder=True)
dataset
'''
>>>
<BatchDataset shapes: ((256, 20), (256, 20)), types: (tf.int32, tf.int32)>
'''

데이터셋 생성과정 정리

  • 정규표현식을 이용한 corpus 생성
  • tf.keras.preprocessing.text.Tokenizer를 이용해 corpus를 텐서로 변환
  • tf.data.Dataset.from_tensor_slices()를 이용해 corpus 텐서를tf.data.Dataset객체로 변환

2. 학습 시키기

만들 모델의 구조

우리가 만들 모델은 tf.keras.Model을 Subclassing하는 방식이다. 위 그림에서 설명한 것처럼 우리가 만들 모델에는 1개의 Embedding 레이어, 2개의 LSTM 레이어, 1개의 Dense 레이어로 구성되어 있다.

모델 생성

class TextGenerator(tf.keras.Model):
    def __init__(self, vocab_size, embedding_size, hidden_size):
        super().__init__()

        self.embedding = tf.keras.layers.Embedding(vocab_size, embedding_size)
        self.rnn_1 = tf.keras.layers.LSTM(hidden_size, return_sequences=True)
        self.rnn_2 = tf.keras.layers.LSTM(hidden_size, return_sequences=True)
        self.linear = tf.keras.layers.Dense(vocab_size)

    def call(self, x):
        out = self.embedding(x)
        out = self.rnn_1(out)
        out = self.rnn_2(out)
        out = self.linear(out)

        return out

embedding_size = 256
hidden_size = 1024
model = TextGenerator(tokenizer.num_words + 1, embedding_size , hidden_size)

Embedding 레이어는 입력 텐서에 들어있는 단어 사전의 인덱스를 실제 워드 벡터로 바꿔주는 역할을 한다.

embedding_size는 워드 벡터의 차원수, 즉 단어가 추상적으로 표현되는 크기이다.
ex).
차갑다: [0.0, 1.0]
뜨겁다: [1.0, 0.0]
미지근하다: [0.5, 0.5]

모델 확인

# 데이터셋에서 데이터 한 배치만 불러오는 방법입니다.
# 지금은 동작 원리에 너무 빠져들지 마세요~
for src_sample, tgt_sample in dataset.take(1): break

# 한 배치만 불러온 데이터를 모델에 넣어봅니다
model(src_sample)
'''
>>>
tf.Tensor: shape=(256, 20, 7001)
'''

shape=(batch size, 자신에게 입력된 시퀀스의 길이만큼 동일한 길이의 시퀀스를 출력 ,Dense layer의 출력 차원수)

model.summary()
'''
>>>
Model: "text_generator_1"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
embedding_1 (Embedding)      multiple                  1792256   
_________________________________________________________________
lstm_2 (LSTM)                multiple                  5246976   
_________________________________________________________________
lstm_3 (LSTM)                multiple                  8392704   
_________________________________________________________________
dense_1 (Dense)              multiple                  7176025   
=================================================================
Total params: 22,607,961
Trainable params: 22,607,961
Non-trainable params: 0
_________________________________________________________________
'''

output shape를 알려주지 않는다. 그 이유는 입력 시퀀스의 길이를 모르기 때문이다.

모델 학습

# optimizer와 loss등은 차차 배웁니다
# 혹시 미리 알고 싶다면 아래 문서를 참고하세요
# https://www.tensorflow.org/api_docs/python/tf/keras/optimizers
# https://www.tensorflow.org/api_docs/python/tf/keras/losses
# 양이 상당히 많은 편이니 지금 보는 것은 추천하지 않습니다
optimizer = tf.keras.optimizers.Adam()
loss = tf.keras.losses.SparseCategoricalCrossentropy(
    from_logits=True,
    reduction='none'
)

model.compile(loss=loss, optimizer=optimizer)
model.fit(dataset, epochs=30)
'''
>>>
Epoch 30/30
93/93 [==============================] - 18s 193ms/step - loss: 1.3930
'''

3. 모델 평가

모델이 작문을 잘하는 지 컴퓨터 알고리즘이 평가하는 것은 무리가 있다. 작문 모델을 평가하는 가장 확실한 방법은 작문을 시켜보고 직접 평가 하는 것이다.

def generate_text(model, tokenizer, init_sentence="<start>", max_len=20):
    # 테스트를 위해서 입력받은 init_sentence도 텐서로 변환합니다
    test_input = tokenizer.texts_to_sequences([init_sentence])
    test_tensor = tf.convert_to_tensor(test_input, dtype=tf.int64)
    end_token = tokenizer.word_index["<end>"]

    # 단어 하나씩 예측해 문장을 만듭니다
    #    1. 입력받은 문장의 텐서를 입력합니다
    #    2. 예측된 값 중 가장 높은 확률인 word index를 뽑아냅니다
    #    3. 2에서 예측된 word index를 문장 뒤에 붙입니다
    #    4. 모델이 <end>를 예측했거나, max_len에 도달했다면 문장 생성을 마칩니다
    while True:
        # 1
        predict = model(test_tensor) 
        # 2
        predict_word = tf.argmax(tf.nn.softmax(predict, axis=-1), axis=-1)[:, -1] 
        # 3 
        test_tensor = tf.concat([test_tensor, tf.expand_dims(predict_word, axis=0)], axis=-1)
        # 4
        if predict_word.numpy()[0] == end_token: break
        if test_tensor.shape[1] >= max_len: break

    generated = ""
    # tokenizer를 이용해 word index를 단어로 하나씩 변환합니다 
    for word_index in test_tensor[0].numpy():
        generated += tokenizer.index_word[word_index] + " "

    return generated
generate_text(model, tokenizer, init_sentence="<start> he")
'''
>>>
'<start> he s not fourteen . <end> '
'''

'AIFFEL > exploration' 카테고리의 다른 글

Exploration 06  (0) 2022.01.20
Exploration 05  (0) 2022.01.18
Exploration 03  (0) 2022.01.11
Exploration 02  (0) 2022.01.06
Exploration 01  (0) 2022.01.04

BELATED ARTICLES

more