레이블이 tensorflow인 게시물을 표시합니다. 모든 게시물 표시
레이블이 tensorflow인 게시물을 표시합니다. 모든 게시물 표시

2022년 8월 20일 토요일

tensorflow keras / RNN, LSTM char

 

RNN에서 대표적인 예제라고 한다면 text 예제가 많이 있습니다. 

앞에서 숫자 예제를 만들어 봤는데 이번에는 해당 예제를 변형하여 text예제를 만들어 볼까합니다.

text는 애국가가 나오고 다음 한 글자를 예측하는 형태가 됩니다.

소스 코드가 이전과 거의 비슷하기 때문에 입력 shape과 one hot encoding을 중점적으로 보면 쉽게 이해가 될것입니다.


입력 데이터

앞서 언급했듯이 입력은 애국가가 됩니다. 훈련을 위한 x , y 만들어야 하는데 여기 예제에서는 *(이전 데이터)입력 (time_step_size) 4개를 가지고 다음번 1개를 예측해야하므로 다 대 일(Many-to-One) 구조로 데이터를 만들었습니다.

원본 text

동해 물과 백두산이 마르고 닳도록
하느님이 보우하사 우리나라 만세.
...

x_data.txt

동해 물
해 물과
 물과 
물과 백
과 백두
 백두산
백두산이
...

y_data.txt

과
 
백
두
산
이
 
...


이러한 문자 처리에서는 몇가지 알아둬야 할 사항이 있습니다. 

1. python3 utf-8에서는 한글이 한개의 문자가 됩니다. 즉 len의 길이가 byte단위로 측정되는것이 아니기 길이가 1이 됩니다.

len("동해 물") 은 4가 되게 됩니다. (공백도 1이 됩니다.)

2. text학습을 위해서는 결국 숫자(tensor)로 변환해야 하는데 그것을 encoding이라고 부른다. 일종의 약속(변환 테이블)을 해야 합니다, 즉 가=1, 나=2 이런식으로 하게됩니다. 그런데 모든 문자에 대해서 코드화를 해놓으면 불필요한 메모리가 낭비가 되다보니 사용하는 문자들만 하게 됩니다. 즉 동=1, 해=2, 물=3 이런식으로 됩니다.

문자 기반으로 할 경우의 예를 위와 같이 들었고 단어 기반의 경우 단어 단위로 encoding을 하게 됩니다.

3. 엔코딩을 했더라도 한가지 추가 고민이 필요합니다. 동=1, 해=2, 물=3 로 했을때 1,3이 어떤 관계냐는것 입니다. 연산을 하게될텐데 y결과가 3이었다면 동=1 에 뭔가를 더 더해서 물=3이 될 수 있느냐는건데(예를들어서 동+동+동 을 하게되면 물이 되느냐 그런 질문입니다.) 문자에서는 전혀 관계가 없다는것 입니다. 그래서 코드화된 숫자를 다시 하나의 feature로 가지도록 one hot encoding을 사용하는것입니다.

이부분은 다음 함수를 이용해서 전환을 하게 됩니다. from tensorflow.keras.utils import to_categorical   


여기에서는 데이터 생성을 하면서 2번 엔코딩 테이블까지 만들어서 pickle로 저장하게 됩니다.

char_vocab = sorted(list(set(timed_y_data)))
vocab_size = len(char_vocab)
print(char_vocab)
print(vocab_size)

set은 텍스트의 중복 데이터를 지워줍니다. sorted는 list를 정렬해주게 됩니다.

[' ', ',', '.', '가', '갑', '강', '거', '고', '공', '과', '괴', '구', '궁', '기', '길', '나', '남', '높', '느', '늘', '님', '다', '단', '달', '닳', '대', '데', '도', '동', '두', '듯', '라', '람', '랑', '려', '로', '록', '르', '른', '름', '리', '마', '만', '맘', '무', '물', '바', '밝', '백', '변', '보', '불', '사', '산', '삼', '상', '서', '성', '세', '소', '슴', '심', '없', '에', '여', '우', '위', '으', '은', '을', '이', '일', '저', '전', '즐', '천', '철', '충', '편', '하', '한', '함', '해', '화', '활']
85

문자가 들어오면 index로 변환하는 dict 입니다.

char_to_index = dict((char, index) for index, char in enumerate(char_vocab))
print(char_to_index)

결과

{' ': 0, ',': 1, '.': 2, '가': 3, '갑': 4, '강': 5, '거': 6, '고': 7, '공': 8, '과': 9, '괴': 10, '구': 11, '궁': 12, '기': 13, '길': 14, '나': 15, '남': 16, '높': 17, '느': 18, '늘': 19, '님': 20, '다': 21, '단': 22, '달': 23, '닳': 24, '대': 25, '데': 26, '도': 27, '동': 28, '두': 29, '듯': 30, '라': 31, '람': 32, '랑': 33, '려': 34, '로': 35, '록': 36, '르': 37, '른': 38, '름': 39, '리': 40, '마': 41, '만': 42, '맘': 43, '무': 44, '물': 45, '바': 46, '밝': 47, '백': 48, '변': 49, '보': 50, '불': 51, '사': 52, '산': 53, '삼': 54, '상': 55, '서': 56, '성': 57, '세': 58, '소': 59, '슴': 60, '심': 61, '없': 62, '에': 63, '여': 64, '우': 65, '위': 66, '으': 67, '은': 68, '을': 69, '이': 70, '일': 71, '저': 72, '전': 73, '즐': 74, '천': 75, '철': 76, '충': 77, '편': 78, '하': 79, '한': 80, '함': 81, '해': 82, '화': 83, '활': 84}

이번에는 반대로 숫자가 들어오면 문자로 변환 해주는 dict 입니다.

index_to_char = {}
for key, value in char_to_index.items():
    index_to_char[value] = key
print(index_to_char)

결과

{0: ' ', 1: ',', 2: '.', 3: '가', 4: '갑', 5: '강', 6: '거', 7: '고', 8: '공', 9: '과', 10: '괴', 11: '구', 12: '궁', 13: '기', 14: '길', 15: '나', 16: '남', 17: '높', 18: '느', 19: '늘', 20: '님', 21: '다', 22: '단', 23: '달', 24: '닳', 25: '대', 26: '데', 27: '도', 28: '동', 29: '두', 30: '듯', 31: '라', 32: '람', 33: '랑', 34: '려', 35: '로', 36: '록', 37: '르', 38: '른', 39: '름', 40: '리', 41: '마', 42: '만', 43: '맘', 44: '무', 45: '물', 46: '바', 47: '밝', 48: '백', 49: '변', 50: '보', 51: '불', 52: '사', 53: '산', 54: '삼', 55: '상', 56: '서', 57: '성', 58: '세', 59: '소', 60: '슴', 61: '심', 62: '없', 63: '에', 64: '여', 65: '우', 66: '위', 67: '으', 68: '은', 69: '을', 70: '이', 71: '일', 72: '저', 73: '전', 74: '즐', 75: '천', 76: '철', 77: '충', 78: '편', 79: '하', 80: '한', 81: '함', 82: '해', 83: '화', 84: '활'}


데이터 생성 전체 소스

import pickle

timed_y_data = """
동해 물과 백두산이 마르고 닳도록
하느님이 보우하사 우리나라 만세.
무궁화 삼천리 화려 강산
대한 사람, 대한으로 길이 보전하세.
남산 위에 저 소나무, 철갑을 두른 듯
바람 서리 불변함은 우리 기상일세.
무궁화 삼천리 화려 강산
대한 사람, 대한으로 길이 보전하세.
가을 하늘 공활한데 높고 구름 없이
밝은 달은 우리 가슴 일편단심일세.
무궁화 삼천리 화려 강산
대한 사람, 대한으로 길이 보전하세.
이 기상과 이 맘으로 충성을 다하여
괴로우나 즐거우나 나라 사랑하세.
무궁화 삼천리 화려 강산
대한 사람, 대한으로 길이 보전하세.
"""

timed_y_data = timed_y_data.strip()
timed_y_data = timed_y_data.replace("\n","")
print(timed_y_data)

time_step_size = 4
x_list = []
y_list = []

for xx in range(len(timed_y_data)-time_step_size):
    x_list.append(timed_y_data[xx:xx+time_step_size])
    y_list.append(timed_y_data[xx+time_step_size])

print(x_list)
print(y_list)

char_vocab = sorted(list(set(timed_y_data)))
vocab_size = len(char_vocab)
print(char_vocab)
print(vocab_size)
char_to_index = dict((char, index) for index, char in enumerate(char_vocab))
print(char_to_index)
index_to_char = {}
for key, value in char_to_index.items():
    index_to_char[value] = key
print(index_to_char)

file = open("x_data.txt", "w", encoding="utf-8")
for data in x_list:
    file.write(data)
    file.write("\n")
file.close()

file = open("y_data.txt", "w", encoding="utf-8")
for data in y_list:
    file.write(data)
    file.write("\n")
file.close()

with open('char_to_index.pickle', 'wb') as fw:
    pickle.dump(char_to_index, fw)
with open('index_to_char.pickle', 'wb') as fw:
    pickle.dump(index_to_char, fw)


소스 변경점

기존 RNN 예제 대비 변경점은 3가지 정도입니다.


1. pickle를 이용한 엔코딩 데이터 처리

이 부분은 data 저장하는 곳에서 저장했기 때문에 load가 필요합니다.

import pickle

with open('char_to_index.pickle', 'rb') as fr:
    char_to_index = pickle.load(fr)

with open('index_to_char.pickle', 'rb') as fr:
    index_to_char = pickle.load(fr)


2. CustomDataset 변경

기존에는 csv 파일을 읽어오기만 했었는데 여기에서는 x, y를 읽어서 다시 onehot encoding이 필요합니다.

그래서 값을 읽어오는 부분이 변경되었습니다. 앞에서 문자크기가 85개이기 때문에 onehot encoding시에도 85개의 feature가 필요합니다. 

    def __getitem__(self, index):
        """
        주어진 인덱스 index 에 해당하는 샘플을 데이터셋에서 불러오고 반환합니다.
        """
        if self.base_idx is not None:
            index = index + self.base_idx
        x_data = self.x_data_lines[index].replace("\n", "")
        #print(x_data)
        x_data = np.array([to_categorical(char_to_index[i], 85) for i in x_data])
        #print(x_data.shape)
        #print(x_data)
        y_data = self.y_data_lines[index].replace("\n", "")
        #print(y_data)
        y_data = to_categorical(char_to_index[y_data], 85)
        #print(y_data)
        #x_data = np.reshape(x_data, (-1, 85))
        return x_data, y_data

아래 코드에 의해서 (batch, 4, 85) 크기를 가지는 data 입력이 됩니다.

x_data = np.array([to_categorical(char_to_index[i], 85) for i in x_data])

물론 여기에서는 (4, 85) 형태를 리턴해주면 CustomDataloader 에 의해서 batch로 묶여지게 됩니다.


3. 모델

모델은 입력 shape 을 적당히 변경하고 작은 경우 훈련이 거의 되지 않아서 LSTM 크기를 좀 늘렸습니다. Dense는 85개의 입력을 받고 이것을 softmax 활성화 함수를 사용하였습니다.

inputs = keras.Input(shape=(4, 85))
x = layers.LSTM(60)(inputs)
x = layers.Dense(85, activation='softmax')(x)

그리고 Loss함수는 CategoricalCrossentropy 를 사용하였고 Adam Optimizer를 사용하였습니다.

INPUT_OPTIMIZER = tf.keras.optimizers.Adam(learning_rate=lr)
LOSS = tf.keras.losses.CategoricalCrossentropy()


전체 소스

from tensorflow import keras
from tensorflow.keras.utils import Sequence
from tensorflow.keras import layers
import tensorflow as tf
from tensorflow.keras.utils import to_categorical
import numpy as np
import math
import pickle

with open('char_to_index.pickle', 'rb') as fr:
    char_to_index = pickle.load(fr)

with open('index_to_char.pickle', 'rb') as fr:
    index_to_char = pickle.load(fr)

batch_size = 20
lr = 1e-3
n_epochs = 50
train_data_ratio = 0.8


class CustomDataset:
    def __init__(self, x_tensor_filename, y_tensor_filename, base_idx=None, cnt=None):
        self.x_fn = x_tensor_filename
        self.y_fn = y_tensor_filename
        self.base_idx = base_idx
        self.cnt = cnt
        if self.base_idx is None:
            with open(y_tensor_filename, 'r', encoding='utf-8') as fp:
                for count, line in enumerate(fp):
                    pass
            self.total_len = count + 1
        else:
            self.total_len = cnt

        with open(x_tensor_filename, "r", encoding='utf-8') as f:
            self.x_data_lines = [line for line in f]
        with open(y_tensor_filename, "r", encoding='utf-8') as f:
            self.y_data_lines = [line for line in f]

    def __getitem__(self, index):
        """
        주어진 인덱스 index 에 해당하는 샘플을 데이터셋에서 불러오고 반환합니다.
        """
        if self.base_idx is not None:
            index = index + self.base_idx
        x_data = self.x_data_lines[index].replace("\n", "")
        #print(x_data)
        x_data = np.array([to_categorical(char_to_index[i], 85) for i in x_data])
        #print(x_data.shape)
        #print(x_data)
        y_data = self.y_data_lines[index].replace("\n", "")
        #print(y_data)
        y_data = to_categorical(char_to_index[y_data], 85)
        #print(y_data)
        #x_data = np.reshape(x_data, (-1, 85))
        return x_data, y_data

    def __len__(self):
        """
        데이터셋의 샘플 개수를 반환합니다.
        """
        return self.total_len


class CustomDataloader(Sequence):
    def __init__(self, _dataset, batch_size=1, shuffle=False):
        self.dataset = _dataset
        self.batch_size = batch_size
        self.total_len = math.ceil(len(self.dataset) / self.batch_size)
        self.shuffle = shuffle
        self.indexer = np.arange(len(self.dataset))
        self.on_epoch_end()

    def __getitem__(self, index):
        indexer = self.indexer[index * self.batch_size:(index + 1) * self.batch_size]
        batch_x = [self.dataset[i][0] for i in indexer]
        batch_y = [self.dataset[i][1] for i in indexer]
        #[[5.         5.00999946 5.0199977  5.02999448 5.03998959]
        # [5.00999946 5.0199977  5.02999448 5.03998959 5.04998279]
        # ...
        return np.array(batch_x), np.array(batch_y)

    def __len__(self):
        return self.total_len

    def on_epoch_end(self):
        if self.shuffle:
            np.random.shuffle(self.indexer)
            print("shuffle")


temp_dataset = CustomDataset("x_data.txt", "y_data.txt")
train_dataset_cnt = int(len(temp_dataset) * train_data_ratio)
val_dataset_cnt = len(temp_dataset) - train_dataset_cnt
train_dataset = CustomDataset("x_data.txt", "y_data.txt", 0, train_dataset_cnt)
val_dataset = CustomDataset("x_data.txt", "y_data.txt", train_dataset_cnt, val_dataset_cnt)
print(len(temp_dataset), train_dataset_cnt, val_dataset_cnt)
train_loader = CustomDataloader(train_dataset, batch_size=batch_size, shuffle=False)
val_loader = CustomDataloader(val_dataset, batch_size=batch_size, shuffle=False)

# https://keras.io/api/layers/
inputs = keras.Input(shape=(4, 85))
x = layers.LSTM(60)(inputs)
x = layers.Dense(85, activation='softmax')(x)
outputs = x
model = keras.Model(inputs, outputs)
model.summary()

# https://keras.io/api/optimizers/
# https://keras.io/api/losses/
INPUT_OPTIMIZER = tf.keras.optimizers.Adam(learning_rate=lr)
LOSS = tf.keras.losses.CategoricalCrossentropy()
model.compile(optimizer=INPUT_OPTIMIZER, loss=LOSS)
model.fit(train_loader, batch_size=batch_size, epochs=n_epochs, validation_data=val_loader)
model.save("model_lstm.keras")

결과

279 223 56
Model: "model"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 input_1 (InputLayer)        [(None, 4, 85)]           0         
                                                                 
 lstm (LSTM)                 (None, 60)                35040     
                                                                 
 dense (Dense)               (None, 85)                5185      
                                                                 
=================================================================
Total params: 40,225
Trainable params: 40,225
Non-trainable params: 0
_________________________________________________________________
Epoch 1/50
12/12 [==============================] - 1s 23ms/step - loss: 4.4400 - val_loss: 4.4262
Epoch 2/50
12/12 [==============================] - 0s 4ms/step - loss: 4.4039 - val_loss: 4.3948
Epoch 3/50
12/12 [==============================] - 0s 4ms/step - loss: 4.3606 - val_loss: 4.3512
Epoch 4/50
12/12 [==============================] - 0s 4ms/step - loss: 4.2906 - val_loss: 4.2747
Epoch 5/50
12/12 [==============================] - 0s 4ms/step - loss: 4.1535 - val_loss: 4.1075
Epoch 6/50
12/12 [==============================] - 0s 4ms/step - loss: 3.8944 - val_loss: 3.8651
Epoch 7/50
12/12 [==============================] - 0s 4ms/step - loss: 3.7237 - val_loss: 3.8388
Epoch 8/50
12/12 [==============================] - 0s 4ms/step - loss: 3.6581 - val_loss: 3.7765
Epoch 9/50
12/12 [==============================] - 0s 4ms/step - loss: 3.5942 - val_loss: 3.7746
Epoch 10/50
12/12 [==============================] - 0s 4ms/step - loss: 3.5545 - val_loss: 3.7465
Epoch 11/50
12/12 [==============================] - 0s 4ms/step - loss: 3.5149 - val_loss: 3.7227
Epoch 12/50
12/12 [==============================] - 0s 4ms/step - loss: 3.4697 - val_loss: 3.7108
Epoch 13/50
12/12 [==============================] - 0s 4ms/step - loss: 3.4246 - val_loss: 3.6961
Epoch 14/50
12/12 [==============================] - 0s 4ms/step - loss: 3.3848 - val_loss: 3.6519
Epoch 15/50
12/12 [==============================] - 0s 3ms/step - loss: 3.3276 - val_loss: 3.6426
Epoch 16/50
12/12 [==============================] - 0s 3ms/step - loss: 3.2704 - val_loss: 3.5921
Epoch 17/50
12/12 [==============================] - 0s 4ms/step - loss: 3.2082 - val_loss: 3.5731
Epoch 18/50
12/12 [==============================] - 0s 4ms/step - loss: 3.1412 - val_loss: 3.5327
Epoch 19/50
12/12 [==============================] - 0s 4ms/step - loss: 3.0699 - val_loss: 3.4891
Epoch 20/50
12/12 [==============================] - 0s 4ms/step - loss: 2.9889 - val_loss: 3.4257
Epoch 21/50
12/12 [==============================] - 0s 4ms/step - loss: 2.9030 - val_loss: 3.3826
Epoch 22/50
12/12 [==============================] - 0s 4ms/step - loss: 2.8177 - val_loss: 3.3197
Epoch 23/50
12/12 [==============================] - 0s 4ms/step - loss: 2.7216 - val_loss: 3.2720
Epoch 24/50
12/12 [==============================] - 0s 4ms/step - loss: 2.6283 - val_loss: 3.2156
Epoch 25/50
12/12 [==============================] - 0s 4ms/step - loss: 2.5297 - val_loss: 3.1799
Epoch 26/50
12/12 [==============================] - 0s 4ms/step - loss: 2.4361 - val_loss: 3.1063
Epoch 27/50
12/12 [==============================] - 0s 4ms/step - loss: 2.3331 - val_loss: 3.0865
Epoch 28/50
12/12 [==============================] - 0s 4ms/step - loss: 2.2367 - val_loss: 3.0124
Epoch 29/50
12/12 [==============================] - 0s 4ms/step - loss: 2.1327 - val_loss: 2.9974
Epoch 30/50
12/12 [==============================] - 0s 4ms/step - loss: 2.0387 - val_loss: 2.9617
Epoch 31/50
12/12 [==============================] - 0s 4ms/step - loss: 1.9494 - val_loss: 2.9479
Epoch 32/50
12/12 [==============================] - 0s 3ms/step - loss: 1.8460 - val_loss: 2.9146
Epoch 33/50
12/12 [==============================] - 0s 4ms/step - loss: 1.7531 - val_loss: 2.9316
Epoch 34/50
12/12 [==============================] - 0s 4ms/step - loss: 1.6658 - val_loss: 2.8914
Epoch 35/50
12/12 [==============================] - 0s 4ms/step - loss: 1.5826 - val_loss: 2.9087
Epoch 36/50
12/12 [==============================] - 0s 4ms/step - loss: 1.5006 - val_loss: 2.9242
Epoch 37/50
12/12 [==============================] - 0s 4ms/step - loss: 1.4244 - val_loss: 2.9103
Epoch 38/50
12/12 [==============================] - 0s 4ms/step - loss: 1.3474 - val_loss: 2.9748
Epoch 39/50
12/12 [==============================] - 0s 4ms/step - loss: 1.2815 - val_loss: 2.9538
Epoch 40/50
12/12 [==============================] - 0s 3ms/step - loss: 1.2125 - val_loss: 3.0242
Epoch 41/50
12/12 [==============================] - 0s 3ms/step - loss: 1.1398 - val_loss: 2.9633
Epoch 42/50
12/12 [==============================] - 0s 4ms/step - loss: 1.0929 - val_loss: 3.0585
Epoch 43/50
12/12 [==============================] - 0s 4ms/step - loss: 1.0249 - val_loss: 3.0636
Epoch 44/50
12/12 [==============================] - 0s 4ms/step - loss: 0.9632 - val_loss: 3.0656
Epoch 45/50
12/12 [==============================] - 0s 4ms/step - loss: 0.9112 - val_loss: 3.0943
Epoch 46/50
12/12 [==============================] - 0s 4ms/step - loss: 0.8600 - val_loss: 3.0998
Epoch 47/50
12/12 [==============================] - 0s 3ms/step - loss: 0.8218 - val_loss: 3.1171
Epoch 48/50
12/12 [==============================] - 0s 4ms/step - loss: 0.7693 - val_loss: 3.1759
Epoch 49/50
12/12 [==============================] - 0s 4ms/step - loss: 0.7254 - val_loss: 3.1406
Epoch 50/50
12/12 [==============================] - 0s 4ms/step - loss: 0.6885 - val_loss: 3.1862

validation loss가 마지막에 증가하는것을 봤을때 제대로 훈련되는것 같지는 않습니다.

생각해보면 데이터가 너무 부족합니다. 즉 부족한 text에서 train, validation을 분해했으니 실제 train이 안되는 부분도 있을것 같네요


tester code

이번에는 훈련된 데이터를 이용해서 실제 동작이 어떻게 되는지 확인해보도록 하겠습니다.

처음 4글자가 주어지면 계속해서 예측해서 출력하도록 제작해보았습니다.

여기에서는 np.argmax() 라는 것을 사용했는데 최대값의 index를 넘겨주게 됩니다.

즉 85개의 onehot encoding으로 예측값이 들어가있을텐데 85개 feature중 어디의 값이 가장 큰것인지 index를 넘겨주게 됩니다.


import numpy as np
import tensorflow as tf
import matplotlib.pyplot as plt
import pickle
from tensorflow.keras.utils import to_categorical


if __name__ == '__main__':

    with open('char_to_index.pickle', 'rb') as fr:
        char_to_index = pickle.load(fr)

    with open('index_to_char.pickle', 'rb') as fr:
        index_to_char = pickle.load(fr)

    flag = False
    fig, ax = plt.subplots()
    for model_name in ["model_lstm.keras"]:
        model = tf.keras.models.load_model(model_name)
        model.summary()
        x_list = []
        x_list.extend(" 삼천리")
        predict_count = 30

        for idx in range(predict_count):
            print("xlist",x_list)
            x_data = np.array([[to_categorical(char_to_index[i], 85) for i in x_list]])
            print(x_data.shape)
            y = model.predict(x_data, batch_size=1)
            #print("y:", y)
            result = np.argmax(y, axis=1)
            print("result:", result)
            newchar = index_to_char[int(result)]
            print("newchar:", newchar)
            x_list.append(newchar)
            x_list.pop(0)

결과

Model: "model"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 input_1 (InputLayer)        [(None, 4, 85)]           0         
                                                                 
 lstm (LSTM)                 (None, 60)                35040     
                                                                 
 dense (Dense)               (None, 85)                5185      
                                                                 
=================================================================
Total params: 40,225
Trainable params: 40,225
Non-trainable params: 0
_________________________________________________________________
xlist [' ', '삼', '천', '리']
(1, 4, 85)
1/1 [==============================] - 0s 275ms/step
result: [0]
newchar:  
xlist ['삼', '천', '리', ' ']
(1, 4, 85)
1/1 [==============================] - 0s 8ms/step
result: [83]
newchar: 화
xlist ['천', '리', ' ', '화']
(1, 4, 85)
1/1 [==============================] - 0s 10ms/step
result: [34]
newchar: 려
xlist ['리', ' ', '화', '려']
(1, 4, 85)
1/1 [==============================] - 0s 9ms/step
result: [0]
newchar:  
xlist [' ', '화', '려', ' ']
(1, 4, 85)
1/1 [==============================] - 0s 9ms/step
result: [5]
newchar: 강
xlist ['화', '려', ' ', '강']
(1, 4, 85)
1/1 [==============================] - 0s 8ms/step
result: [53]
newchar: 산
xlist ['려', ' ', '강', '산']
(1, 4, 85)
1/1 [==============================] - 0s 9ms/step
result: [25]
newchar: 대
xlist [' ', '강', '산', '대']
(1, 4, 85)
1/1 [==============================] - 0s 8ms/step
result: [80]
newchar: 한
xlist ['강', '산', '대', '한']
(1, 4, 85)
1/1 [==============================] - 0s 11ms/step
result: [0]
newchar:  
xlist ['산', '대', '한', ' ']
(1, 4, 85)
1/1 [==============================] - 0s 10ms/step
result: [52]
newchar: 사
xlist ['대', '한', ' ', '사']
(1, 4, 85)
1/1 [==============================] - 0s 9ms/step
result: [32]
newchar: 람
xlist ['한', ' ', '사', '람']
(1, 4, 85)
1/1 [==============================] - 0s 9ms/step
result: [1]
newchar: ,
xlist [' ', '사', '람', ',']
(1, 4, 85)
1/1 [==============================] - 0s 8ms/step
result: [0]
newchar:  
xlist ['사', '람', ',', ' ']
(1, 4, 85)
1/1 [==============================] - 0s 8ms/step
result: [25]
newchar: 대
xlist ['람', ',', ' ', '대']
(1, 4, 85)
1/1 [==============================] - 0s 8ms/step
result: [80]
newchar: 한
xlist [',', ' ', '대', '한']
(1, 4, 85)
1/1 [==============================] - 0s 8ms/step
result: [67]
newchar: 으
xlist [' ', '대', '한', '으']
(1, 4, 85)
1/1 [==============================] - 0s 9ms/step
result: [35]
newchar: 로
xlist ['대', '한', '으', '로']
(1, 4, 85)
1/1 [==============================] - 0s 8ms/step
result: [0]
newchar:  
xlist ['한', '으', '로', ' ']
(1, 4, 85)
1/1 [==============================] - 0s 9ms/step
result: [14]
newchar: 길
xlist ['으', '로', ' ', '길']
(1, 4, 85)
1/1 [==============================] - 0s 9ms/step
result: [57]
newchar: 성
xlist ['로', ' ', '길', '성']
(1, 4, 85)
1/1 [==============================] - 0s 9ms/step
result: [0]
newchar:  
xlist [' ', '길', '성', ' ']
(1, 4, 85)
1/1 [==============================] - 0s 9ms/step
result: [50]
newchar: 보
xlist ['길', '성', ' ', '보']
(1, 4, 85)
1/1 [==============================] - 0s 13ms/step
result: [73]
newchar: 전
xlist ['성', ' ', '보', '전']
(1, 4, 85)
1/1 [==============================] - 0s 9ms/step
result: [0]
newchar:  
xlist [' ', '보', '전', ' ']
(1, 4, 85)
1/1 [==============================] - 0s 8ms/step
result: [58]
newchar: 세
xlist ['보', '전', ' ', '세']
(1, 4, 85)
1/1 [==============================] - 0s 12ms/step
result: [2]
newchar: .
xlist ['전', ' ', '세', '.']
(1, 4, 85)
1/1 [==============================] - 0s 8ms/step
result: [3]
newchar: 가
xlist [' ', '세', '.', '가']
(1, 4, 85)
1/1 [==============================] - 0s 8ms/step
result: [44]
newchar: 무
xlist ['세', '.', '가', '무']
(1, 4, 85)
1/1 [==============================] - 0s 8ms/step
result: [0]
newchar:  
xlist ['.', '가', '무', ' ']
(1, 4, 85)
1/1 [==============================] - 0s 8ms/step
result: [79]
newchar: 하

차례로 결과를 읽어보면 일부 내용이 약간 맞지 않는 부분이 있지만... 그래도 정상적으로 나오는것 같습니다.


sourcecode/keras/04_rnn_char at main · donarts/sourcecode · GitHub


2022년 8월 13일 토요일

tensorflow keras / RNN, LSTM

RNN/LSTM

RNN (LSTM, GRU, 이하 LSTM으로 사용함)은 시계열 분석이나 자연어 처리 (NLP) 등에 널리 사용됩니다. 시계열 데이터, 자연어의 단어 시퀀스, 문장 시퀀스 등은 모두 시간에 따라 변하면서, 과거와 현재가 서로 연관성을 가진 데이터입니다. 

여기에서는 keras LSTM 을 매우 다양한 유형으로 LSTM을 구성할 수 있습니다. 단방향 (unidirectional)/양방향 (bidirectional), many-to-one, many-to-many, multi-layered LSTM, stacked LSTM 등이 있고, 이들을 서로 조합하면서 다양한 방식으로 구성할 수 있다.

여기에서는 단순한 단층-단방향 many-to-one 유형에 대해 살펴 보도록 하겠습니다.

기존에 많은 예제들은 문자 예측이나 문자열 예측이 대부분 이었습니다. 오히려 이런 부분들이 RNN을 이해하는데 어려움이 있기 때문에 여기에서는 최대한 기존에 사용했던 숫자 형태를 이용해서 진행하겠습니다.


데이터 준비

예제로 이용할 데이터는 금융 데이터, 주식, 주가 예측 와 비슷한 형태로 가정할 것 입니다. 대략 이런 형태의 데이터를 준비하였습니다.


위 데이터는 삼각함수를 이용하여 적당히 생성 시켰습니다. 함수는 가능하면 양수를 가지도록 적당히 +4 정도로 했습니다.


데이터 생성

사용할 데이터를 준비하였는데 이번에는 이것을 데이터 파일로 만드는 것을 진행할 것입니다. 코드는 아래와 같은 형태가 되고 x 에 따른 y 값을 저장하는데 range는 0~14.4 정도까지 생성했습니다. (range는 정수 값만 사용이 가능하므로, x입력을 위해서 부동 소수점 수가 필요하기 때문에 14000/1000 나누는 코드가 있습니다.)

import math
import matplotlib.pyplot as plt
import numpy as np

time_step_size = 5
timed_y_list = []
for xx in range(0, 14400, 1):
    x = xx/1000.0
    y = 2.0 * math.sin(x) + math.sin(2.0*x)+math.sin(6.0*x)+math.cos(x) + 4.0
    timed_y_list.append(y)

timed_y_data = np.array(timed_y_list)
print(timed_y_data)
np.savetxt("timed_y_data.csv", timed_y_data, delimiter=',')

timed_y_data.csv

5.000000000000000000e+00
5.009999462333439624e+00
5.019997698669415698e+00
5.029994483019190277e+00
5.039989589410644122e+00
5.049982791896089118e+00
5.059973864560072698e+00
5.069962581527184931e+00
5.079948716969867384e+00
5.089932045116213999e+00
5.099912340257776400e+00
5.109889376757358548e+00
......

앞쪽 데이터만 출력해보면 위와 같이 저장되어 있는데 결국 이 데이터는 x 도 아닌 y 도 아닌 이상한 데이터가 됩니다.

RNN의 경우 어떤 부분이 x, y로 만들어서 학습을 해야할까요?

데이터를 구성하기 전에 many to one 구조는 아래 그림을 참고해 주세요


이번에 학습시킬 데이터는 many to one 입니다. 다시 설명하자면 입력은 여러개하고 출력은 하나만 하게 되는 형태입니다.
timed_y_data.csv 파일을 가지고 설명을 하자면 예를 들어 many부분(time step)을 5라고 설정한다면 5개의 입력(x)가 들어가야 하고 하나의 출력은 하나(y)가 됩니다.
이것을 앞에서 3개만 예를들어 그려봤습니다.
이것을 실제 데이터로 x 와 y를 다른 파일로 구성해서 저장해봤습니다. 아래와 같은 형태로 됩니다.
코드로는 아래와 같은 형태가 됩니다.
import math
import matplotlib.pyplot as plt
import numpy as np

time_step_size = 5
timed_y_list = []
x_list = []
y_list = []
for xx in range(0, 14400, 1):
    x = xx/1000.0
    y = 2.0 * math.sin(x) + math.sin(2.0*x)+math.sin(6.0*x)+math.cos(x) + 4.0
    timed_y_list.append(y)

timed_y_data = np.array(timed_y_list)
print(timed_y_data)
np.savetxt("timed_y_data.csv", timed_y_data, delimiter=',')

for xx in range(len(timed_y_list)-time_step_size):
    x_list.append(timed_y_list[xx:xx+time_step_size])
    y_list.append(timed_y_list[xx+time_step_size])

x_np = np.array(x_list)
y_np = np.array(y_list)
print(x_np)
print(y_np)

np.savetxt("x_data.csv", x_np, delimiter=',')
np.savetxt("y_data.csv", y_np, delimiter=',')


fig, ax = plt.subplots()
ax.plot(timed_y_data)
plt.savefig("timed_y_data.png")

저장한 것을 놓고 보면 x 에 5개의 입력을 가지는 한개의 출력 y를 가지는 형태가 됩니다. 이것을 학습 하면 되는데... 결국 RNN이 아니더라도 NN layer로도 충분이 학습 가능할 것 같습니다. 이건 RNN을 학습하고 마지막에 따로 진행해 보도록 하겠습니다.


RNN의 입력

RNN의 가이드는 여기 보시면 됩니다. LSTM이나 SimpleRNN이나 모두 첫번째 인자로는 units인데 이건 출력 갯수 입니다.
https://www.tensorflow.org/api_docs/python/tf/keras/layers/SimpleRNN
https://www.tensorflow.org/api_docs/python/tf/keras/layers/LSTM
https://www.tensorflow.org/guide/keras/rnn?hl=ko#setup
그러면 입력을 어떻게 해야 할까요?
기존의 Linear Layer 다르게 차원이 하나 더 추가가 됩니다. 
xdata로 우리는 앞에서 5개의 변수를 준비했습니다.
기존 Linear 데이터 입력 [1, 2, 3, 4, 5] 형태 => RNN에서는 [[1],[2],[3],[4],[5]] 이런식으로 준비해야 합니다. (마지막 차원이고 사실 변환 안해도 tensorflow가 처리를 해주긴 합니다.)
이 부분 설명이 좀 더 복잡한데... text embeding 을 할때 이방식이 유용해집니다. 여기에서는 1,2,3,4,5 로 예를 들었지만, text라고 한다면 각각이 엔코딩된 값이 될 겁니다. 엔코딩 값들은 한개의 차원을 소모하게 되며, RNN layer에 의해 제일 하위 차원이 사라지게 됩니다.


_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 input_1 (InputLayer)        [(None, None, 5)]         0         
                                                                 
 dense (Dense)               (None, None, 6)           36        
                                                                 
 dense_1 (Dense)             (None, None, 1)           7      

기존 Linear Layer 차원은 그대로 유지

_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 input_1 (InputLayer)        [(None, 5, 1)]            0         
                                                                 
 simple_rnn (SimpleRNN)      (None, 6)                 48        
                                                                 
 dense (Dense)               (None, 1)                 7        

RNN에서는 차원이 하나 사라짐


차원을 변환하는 부분을 CustomDataset 에서 x_data = np.reshape(x_data, (-1, 1)) 여기에서 만들어 두었습니다. 즉 x data를 읽어들일때 차원을 변환 하도록 하였습니다.

class CustomDataset:
    def __init__(self, x_tensor_filename, y_tensor_filename, base_idx=None, cnt=None):
        self.x_fn = x_tensor_filename
        self.y_fn = y_tensor_filename
        self.base_idx = base_idx
        self.cnt = cnt
        if self.base_idx is None:
            with open(y_tensor_filename, 'r') as fp:
                for count, line in enumerate(fp):
                    pass
            self.total_len = count + 1
        else:
            self.total_len = cnt
        x_data = np.loadtxt(self.x_fn, delimiter=',', skiprows=0, max_rows=1)
        self.feature_cnt = len(x_data)

    def __getitem__(self, index):
        """
        주어진 인덱스 index 에 해당하는 샘플을 데이터셋에서 불러오고 반환합니다.
        """
        if self.base_idx is not None:
            index = index + self.base_idx
        x_data = np.loadtxt(self.x_fn, delimiter=',', skiprows=index, max_rows=1)
        # [0.37454012 0.95071431 0.73199394]
        if x_data.shape == ():
            x_data = np.array([x_data])
        y_data = np.loadtxt(self.y_fn, delimiter=',', skiprows=index, max_rows=1)
        # 1.5258549401766552
        if y_data.shape == ():
            y_data = np.array([y_data])
        # [1.5258549401766552]
        # x_data [5.          5.00999946 5.0199977 5.02999448 5.03998959] y_data [2.40299196]
        x_data = np.reshape(x_data, (-1, 1))
        # x_data [[5.        ][5.00999946][5.0199977 ][5.02999448][5.03998959]] y_data [2.40299196]
        return x_data, y_data

    def __len__(self):
        """
        데이터셋의 샘플 개수를 반환합니다.
        """
        return self.total_len


Model

모델 구성하는 부분만 빼고 기본 소스는 Linear Layer 작업할 때와 거의 동일합니다.

기본 소스는 다음 링크를 참고하세요

https://swlock.blogspot.com/2022/08/tensorflow-keras-train-regression.html

input의 shape 에서 5는 time step의 수가 됩니다. data를 구성할때 앞의 몇개의 시계열 데이터를 이용해서 만들었는지 정보입니다. 그리고 뒤에 1은 해당 데이터가 몇개의 feature를 사용하는지 혹은 몇 개의 데이터를 이용해서 엔코딩되어 있는지 나타냅니다. 텍스트 예제의 경우 이 부분이 보통 onehot encoding으로 되어있어 상당히 큰 (feature)수치를 가지고 있게 됩니다.

inputs = keras.Input(shape=(5, 1))
x = layers.SimpleRNN(6)(inputs)
x = layers.Dense(1, activation="linear")(x)
outputs = x
model = keras.Model(inputs, outputs)
model.summary()

위 두가지 부분만 바꾸고 나머지는 거의 비슷하게 구성하였습니다.


훈련

GPU를 사용하지 않고 진행했더니, 훈련 시간은 생각보다 오래 걸렸습니다. validation 데이터로 0.0208 loss 가 발생 하였습니다.

14395 11516 2879
2022-08-13 18:23:59.001158: W tensorflow/stream_executor/platform/default/dso_loader.cc:64] Could not load dynamic library 'cusolver64_11.dll'; dlerror: cusolver64_11.dll not found
2022-08-13 18:23:59.003608: W tensorflow/stream_executor/platform/default/dso_loader.cc:64] Could not load dynamic library 'cudnn64_8.dll'; dlerror: cudnn64_8.dll not found
2022-08-13 18:23:59.003760: W tensorflow/core/common_runtime/gpu/gpu_device.cc:1850] Cannot dlopen some GPU libraries. Please make sure the missing libraries mentioned above are installed properly if you would like to use GPU. Follow the guide at https://www.tensorflow.org/install/gpu for how to download and setup the required libraries for your platform.
Skipping registering GPU devices...
2022-08-13 18:23:59.004247: I tensorflow/core/platform/cpu_feature_guard.cc:193] This TensorFlow binary is optimized with oneAPI Deep Neural Network Library (oneDNN) to use the following CPU instructions in performance-critical operations:  AVX AVX2
To enable them in other operations, rebuild TensorFlow with the appropriate compiler flags.
Model: "model"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 input_1 (InputLayer)        [(None, 5, 1)]            0         
                                                                 
 simple_rnn (SimpleRNN)      (None, 6)                 48        
                                                                 
 dense (Dense)               (None, 1)                 7         
                                                                 
=================================================================
Total params: 55
Trainable params: 55
Non-trainable params: 0
_________________________________________________________________
Epoch 1/50
116/116 [==============================] - 101s 874ms/step - loss: 3.7985 - val_loss: 4.1360
Epoch 2/50
116/116 [==============================] - 99s 853ms/step - loss: 1.7602 - val_loss: 2.4225
Epoch 3/50
116/116 [==============================] - 99s 857ms/step - loss: 1.0908 - val_loss: 1.5594
Epoch 4/50
116/116 [==============================] - 101s 872ms/step - loss: 0.6845 - val_loss: 0.9763
Epoch 5/50
116/116 [==============================] - 100s 864ms/step - loss: 0.4588 - val_loss: 0.6872
Epoch 6/50
116/116 [==============================] - 110s 954ms/step - loss: 0.3262 - val_loss: 0.4753
Epoch 7/50
116/116 [==============================] - 116s 1s/step - loss: 0.2494 - val_loss: 0.3548
Epoch 8/50
116/116 [==============================] - 107s 926ms/step - loss: 0.1984 - val_loss: 0.2714
Epoch 9/50
116/116 [==============================] - 109s 939ms/step - loss: 0.1632 - val_loss: 0.2266
Epoch 10/50
116/116 [==============================] - 106s 922ms/step - loss: 0.1373 - val_loss: 0.1841
Epoch 11/50
116/116 [==============================] - 112s 974ms/step - loss: 0.1157 - val_loss: 0.1762
Epoch 12/50
116/116 [==============================] - 111s 960ms/step - loss: 0.1002 - val_loss: 0.1350
Epoch 13/50
116/116 [==============================] - 114s 986ms/step - loss: 0.0866 - val_loss: 0.1165
Epoch 14/50
116/116 [==============================] - 106s 916ms/step - loss: 0.0768 - val_loss: 0.1015
Epoch 15/50
116/116 [==============================] - 104s 899ms/step - loss: 0.0673 - val_loss: 0.0911
Epoch 16/50
116/116 [==============================] - 106s 917ms/step - loss: 0.0598 - val_loss: 0.0920
Epoch 17/50
116/116 [==============================] - 105s 908ms/step - loss: 0.0517 - val_loss: 0.0738
Epoch 18/50
116/116 [==============================] - 105s 908ms/step - loss: 0.0471 - val_loss: 0.0676
Epoch 19/50
116/116 [==============================] - 107s 925ms/step - loss: 0.0435 - val_loss: 0.0622
Epoch 20/50
116/116 [==============================] - 123s 1s/step - loss: 0.0403 - val_loss: 0.0588
Epoch 21/50
116/116 [==============================] - 120s 1s/step - loss: 0.0368 - val_loss: 0.0545
Epoch 22/50
116/116 [==============================] - 111s 950ms/step - loss: 0.0339 - val_loss: 0.0522
Epoch 23/50
116/116 [==============================] - 118s 1s/step - loss: 0.0314 - val_loss: 0.0470
Epoch 24/50
116/116 [==============================] - 110s 952ms/step - loss: 0.0283 - val_loss: 0.0441
Epoch 25/50
116/116 [==============================] - 122s 1s/step - loss: 0.0273 - val_loss: 0.0447
Epoch 26/50
116/116 [==============================] - 123s 1s/step - loss: 0.0250 - val_loss: 0.0430
Epoch 27/50
116/116 [==============================] - 108s 930ms/step - loss: 0.0242 - val_loss: 0.0389
Epoch 28/50
116/116 [==============================] - 105s 907ms/step - loss: 0.0228 - val_loss: 0.0366
Epoch 29/50
116/116 [==============================] - 105s 906ms/step - loss: 0.0222 - val_loss: 0.0382
Epoch 30/50
116/116 [==============================] - 105s 910ms/step - loss: 0.0206 - val_loss: 0.0344
Epoch 31/50
116/116 [==============================] - 106s 921ms/step - loss: 0.0201 - val_loss: 0.0336
Epoch 32/50
116/116 [==============================] - 106s 916ms/step - loss: 0.0196 - val_loss: 0.0320
Epoch 33/50
116/116 [==============================] - 105s 912ms/step - loss: 0.0177 - val_loss: 0.0337
Epoch 34/50
116/116 [==============================] - 106s 916ms/step - loss: 0.0183 - val_loss: 0.0305
Epoch 35/50
116/116 [==============================] - 105s 909ms/step - loss: 0.0176 - val_loss: 0.0280
Epoch 36/50
116/116 [==============================] - 106s 917ms/step - loss: 0.0172 - val_loss: 0.0305
Epoch 37/50
116/116 [==============================] - 111s 955ms/step - loss: 0.0166 - val_loss: 0.0265
Epoch 38/50
116/116 [==============================] - 113s 977ms/step - loss: 0.0163 - val_loss: 0.0264
Epoch 39/50
116/116 [==============================] - 116s 1s/step - loss: 0.0153 - val_loss: 0.0275
Epoch 40/50
116/116 [==============================] - 115s 989ms/step - loss: 0.0156 - val_loss: 0.0249
Epoch 41/50
116/116 [==============================] - 118s 1s/step - loss: 0.0154 - val_loss: 0.0238
Epoch 42/50
116/116 [==============================] - 121s 1s/step - loss: 0.0147 - val_loss: 0.0232
Epoch 43/50
116/116 [==============================] - 110s 944ms/step - loss: 0.0141 - val_loss: 0.0266
Epoch 44/50
116/116 [==============================] - 123s 1s/step - loss: 0.0146 - val_loss: 0.0239
Epoch 45/50
116/116 [==============================] - 105s 906ms/step - loss: 0.0139 - val_loss: 0.0232
Epoch 46/50
116/116 [==============================] - 107s 925ms/step - loss: 0.0136 - val_loss: 0.0215
Epoch 47/50
116/116 [==============================] - 120s 1s/step - loss: 0.0134 - val_loss: 0.0211
Epoch 48/50
116/116 [==============================] - 148s 1s/step - loss: 0.0129 - val_loss: 0.0214
Epoch 49/50
116/116 [==============================] - 131s 1s/step - loss: 0.0127 - val_loss: 0.0200
Epoch 50/50
116/116 [==============================] - 104s 901ms/step - loss: 0.0126 - val_loss: 0.0208


RNN으로 구현한 소스

from tensorflow import keras
from tensorflow.keras.utils import Sequence
from tensorflow.keras import layers
import tensorflow as tf
import numpy as np
import math

batch_size = 100
lr = 1e-3
n_epochs = 50
train_data_ratio = 0.8


class CustomDataset:
    def __init__(self, x_tensor_filename, y_tensor_filename, base_idx=None, cnt=None):
        self.x_fn = x_tensor_filename
        self.y_fn = y_tensor_filename
        self.base_idx = base_idx
        self.cnt = cnt
        if self.base_idx is None:
            with open(y_tensor_filename, 'r') as fp:
                for count, line in enumerate(fp):
                    pass
            self.total_len = count + 1
        else:
            self.total_len = cnt
        x_data = np.loadtxt(self.x_fn, delimiter=',', skiprows=0, max_rows=1)
        self.feature_cnt = len(x_data)

    def __getitem__(self, index):
        """
        주어진 인덱스 index 에 해당하는 샘플을 데이터셋에서 불러오고 반환합니다.
        """
        if self.base_idx is not None:
            index = index + self.base_idx
        x_data = np.loadtxt(self.x_fn, delimiter=',', skiprows=index, max_rows=1)
        # [0.37454012 0.95071431 0.73199394]
        if x_data.shape == ():
            x_data = np.array([x_data])
        y_data = np.loadtxt(self.y_fn, delimiter=',', skiprows=index, max_rows=1)
        # 1.5258549401766552
        if y_data.shape == ():
            y_data = np.array([y_data])
        # [1.5258549401766552]
        # x_data [5.          5.00999946 5.0199977 5.02999448 5.03998959] y_data [2.40299196]
        x_data = np.reshape(x_data, (-1, 1))
        # x_data [[5.        ][5.00999946][5.0199977 ][5.02999448][5.03998959]] y_data [2.40299196]
        return x_data, y_data

    def __len__(self):
        """
        데이터셋의 샘플 개수를 반환합니다.
        """
        return self.total_len


class CustomDataloader(Sequence):
    def __init__(self, _dataset, batch_size=1, shuffle=False):
        self.dataset = _dataset
        self.batch_size = batch_size
        self.total_len = math.ceil(len(self.dataset) / self.batch_size)
        self.shuffle = shuffle
        self.indexer = np.arange(len(self.dataset))
        self.on_epoch_end()

    def __getitem__(self, index):
        indexer = self.indexer[index * self.batch_size:(index + 1) * self.batch_size]
        batch_x = [self.dataset[i][0] for i in indexer]
        batch_y = [self.dataset[i][1] for i in indexer]
        #[[5.         5.00999946 5.0199977  5.02999448 5.03998959]
        # [5.00999946 5.0199977  5.02999448 5.03998959 5.04998279]
        # ...
        return np.array(batch_x), np.array(batch_y)

    def __len__(self):
        return self.total_len

    def on_epoch_end(self):
        if self.shuffle:
            np.random.shuffle(self.indexer)
            print("shuffle")


temp_dataset = CustomDataset("x_data.csv", "y_data.csv")
train_dataset_cnt = int(len(temp_dataset) * train_data_ratio)
val_dataset_cnt = len(temp_dataset) - train_dataset_cnt
train_dataset = CustomDataset("x_data.csv", "y_data.csv", 0, train_dataset_cnt)
val_dataset = CustomDataset("x_data.csv", "y_data.csv", train_dataset_cnt, val_dataset_cnt)
print(len(temp_dataset), train_dataset_cnt, val_dataset_cnt)
train_loader = CustomDataloader(train_dataset, batch_size=batch_size, shuffle=False)
val_loader = CustomDataloader(val_dataset, batch_size=batch_size, shuffle=False)

# https://keras.io/api/layers/
inputs = keras.Input(shape=(5, 1))
x = layers.SimpleRNN(6)(inputs)
x = layers.Dense(1, activation="linear")(x)
outputs = x
model = keras.Model(inputs, outputs)
model.summary()

# https://keras.io/api/optimizers/
# https://keras.io/api/losses/
INPUT_OPTIMIZER = tf.keras.optimizers.SGD(learning_rate=lr)
LOSS = tf.keras.losses.MeanSquaredError(reduction="auto", name="mean_squared_error")
model.compile(optimizer=INPUT_OPTIMIZER, loss=LOSS)
model.fit(train_loader, batch_size=batch_size, epochs=n_epochs, validation_data=val_loader)
model.save("model_rnn.keras")

LSTM 으로 모델만 바꾼 소스

from tensorflow import keras
from tensorflow.keras.utils import Sequence
from tensorflow.keras import layers
import tensorflow as tf
import numpy as np
import math

batch_size = 100
lr = 1e-3
n_epochs = 50
train_data_ratio = 0.8


class CustomDataset:
    def __init__(self, x_tensor_filename, y_tensor_filename, base_idx=None, cnt=None):
        self.x_fn = x_tensor_filename
        self.y_fn = y_tensor_filename
        self.base_idx = base_idx
        self.cnt = cnt
        if self.base_idx is None:
            with open(y_tensor_filename, 'r') as fp:
                for count, line in enumerate(fp):
                    pass
            self.total_len = count + 1
        else:
            self.total_len = cnt
        x_data = np.loadtxt(self.x_fn, delimiter=',', skiprows=0, max_rows=1)
        self.feature_cnt = len(x_data)

    def __getitem__(self, index):
        """
        주어진 인덱스 index 에 해당하는 샘플을 데이터셋에서 불러오고 반환합니다.
        """
        if self.base_idx is not None:
            index = index + self.base_idx
        x_data = np.loadtxt(self.x_fn, delimiter=',', skiprows=index, max_rows=1)
        # [0.37454012 0.95071431 0.73199394]
        if x_data.shape == ():
            x_data = np.array([x_data])
        y_data = np.loadtxt(self.y_fn, delimiter=',', skiprows=index, max_rows=1)
        # 1.5258549401766552
        if y_data.shape == ():
            y_data = np.array([y_data])
        # [1.5258549401766552]
        # x_data [5.          5.00999946 5.0199977 5.02999448 5.03998959] y_data [2.40299196]
        x_data = np.reshape(x_data, (-1, 1))
        # x_data [[5.        ][5.00999946][5.0199977 ][5.02999448][5.03998959]] y_data [2.40299196]
        return x_data, y_data

    def __len__(self):
        """
        데이터셋의 샘플 개수를 반환합니다.
        """
        return self.total_len


class CustomDataloader(Sequence):
    def __init__(self, _dataset, batch_size=1, shuffle=False):
        self.dataset = _dataset
        self.batch_size = batch_size
        self.total_len = math.ceil(len(self.dataset) / self.batch_size)
        self.shuffle = shuffle
        self.indexer = np.arange(len(self.dataset))
        self.on_epoch_end()

    def __getitem__(self, index):
        indexer = self.indexer[index * self.batch_size:(index + 1) * self.batch_size]
        batch_x = [self.dataset[i][0] for i in indexer]
        batch_y = [self.dataset[i][1] for i in indexer]
        #[[5.         5.00999946 5.0199977  5.02999448 5.03998959]
        # [5.00999946 5.0199977  5.02999448 5.03998959 5.04998279]
        # ...
        return np.array(batch_x), np.array(batch_y)

    def __len__(self):
        return self.total_len

    def on_epoch_end(self):
        if self.shuffle:
            np.random.shuffle(self.indexer)
            print("shuffle")


temp_dataset = CustomDataset("x_data.csv", "y_data.csv")
train_dataset_cnt = int(len(temp_dataset) * train_data_ratio)
val_dataset_cnt = len(temp_dataset) - train_dataset_cnt
train_dataset = CustomDataset("x_data.csv", "y_data.csv", 0, train_dataset_cnt)
val_dataset = CustomDataset("x_data.csv", "y_data.csv", train_dataset_cnt, val_dataset_cnt)
print(len(temp_dataset), train_dataset_cnt, val_dataset_cnt)
train_loader = CustomDataloader(train_dataset, batch_size=batch_size, shuffle=False)
val_loader = CustomDataloader(val_dataset, batch_size=batch_size, shuffle=False)

# https://keras.io/api/layers/
inputs = keras.Input(shape=(5, 1))
x = layers.LSTM(6)(inputs)
x = layers.Dense(1, activation="linear")(x)
outputs = x
model = keras.Model(inputs, outputs)
model.summary()

# https://keras.io/api/optimizers/
# https://keras.io/api/losses/
INPUT_OPTIMIZER = tf.keras.optimizers.SGD(learning_rate=lr)
LOSS = tf.keras.losses.MeanSquaredError(reduction="auto", name="mean_squared_error")
model.compile(optimizer=INPUT_OPTIMIZER, loss=LOSS)
model.fit(train_loader, batch_size=batch_size, epochs=n_epochs, validation_data=val_loader)
model.save("model_lstm.keras")

NN으로 구현한 소스

from tensorflow import keras
from tensorflow.keras.utils import Sequence
from tensorflow.keras import layers
import tensorflow as tf
import numpy as np
import math

batch_size = 100
lr = 1e-3
n_epochs = 5
train_data_ratio = 0.8


class CustomDataset:
    def __init__(self, x_tensor_filename, y_tensor_filename, base_idx=None, cnt=None):
        self.x_fn = x_tensor_filename
        self.y_fn = y_tensor_filename
        self.base_idx = base_idx
        self.cnt = cnt
        if self.base_idx is None:
            with open(y_tensor_filename, 'r') as fp:
                for count, line in enumerate(fp):
                    pass
            self.total_len = count + 1
        else:
            self.total_len = cnt

    def __getitem__(self, index):
        """
        주어진 인덱스 index 에 해당하는 샘플을 데이터셋에서 불러오고 반환합니다.
        """
        if self.base_idx is not None:
            index = index + self.base_idx
        x_data = np.loadtxt(self.x_fn, delimiter=',', skiprows=index, max_rows=1)
        # [0.37454012 0.95071431 0.73199394]
        if x_data.shape == ():
            x_data = np.array([x_data])
        y_data = np.loadtxt(self.y_fn, delimiter=',', skiprows=index, max_rows=1)
        # 1.5258549401766552
        if y_data.shape == ():
            y_data = np.array([y_data])
        # [1.5258549401766552]
        return x_data, y_data

    def __len__(self):
        """
        데이터셋의 샘플 개수를 반환합니다.
        """
        return self.total_len


class CustomDataloader(Sequence):
    def __init__(self, _dataset, batch_size=1, shuffle=False):
        self.dataset = _dataset
        self.batch_size = batch_size
        self.total_len = math.ceil(len(self.dataset) / self.batch_size)
        self.shuffle = shuffle
        self.indexer = np.arange(len(self.dataset))
        self.on_epoch_end()

    def __getitem__(self, index):
        indexer = self.indexer[index * self.batch_size:(index + 1) * self.batch_size]
        batch_x = [self.dataset[i][0] for i in indexer]
        batch_y = [self.dataset[i][1] for i in indexer]
        return np.array(batch_x), np.array(batch_y)

    def __len__(self):
        return self.total_len

    def on_epoch_end(self):
        if self.shuffle:
            np.random.shuffle(self.indexer)
            print("shuffle")


temp_dataset = CustomDataset("x_data.csv", "y_data.csv")
train_dataset_cnt = int(len(temp_dataset) * train_data_ratio)
val_dataset_cnt = len(temp_dataset) - train_dataset_cnt
train_dataset = CustomDataset("x_data.csv", "y_data.csv", 0, train_dataset_cnt)
val_dataset = CustomDataset("x_data.csv", "y_data.csv", train_dataset_cnt, val_dataset_cnt)
print(len(temp_dataset), train_dataset_cnt, val_dataset_cnt)
train_loader = CustomDataloader(train_dataset, batch_size=batch_size, shuffle=False)
val_loader = CustomDataloader(val_dataset, batch_size=batch_size, shuffle=False)

# https://keras.io/api/layers/
inputs = keras.Input(shape=(None, 5))
x = layers.Dense(6, activation="linear")(inputs)
x = layers.Dense(1, activation="linear")(x)
outputs = x
model = keras.Model(inputs, outputs)
model.summary()

# https://keras.io/api/optimizers/
# https://keras.io/api/losses/
INPUT_OPTIMIZER = tf.keras.optimizers.SGD(learning_rate=lr)
LOSS = tf.keras.losses.MeanSquaredError(reduction="auto", name="mean_squared_error")
model.compile(optimizer=INPUT_OPTIMIZER, loss=LOSS)
model.fit(train_loader, batch_size=batch_size, epochs=n_epochs, validation_data=val_loader)
model.save("model_nn.keras")


비교

import numpy as np
import tensorflow as tf
import math
import matplotlib.pyplot as plt


if __name__ == '__main__':
    flag = False
    fig, ax = plt.subplots()
    for model_name in ["model_rnn.keras", "model_lstm.keras", "model_nn.keras"]:
        model = tf.keras.models.load_model(model_name)
        model.summary()
        timed_y_list = []
        graph_data = []
        x_list = []
        time_step_size = 5
        predict_count = 20
        for xx in range(14400, 14400+predict_count+time_step_size, 1):
            x = xx / 1000.0
            y = 2.0 * math.sin(x) + math.sin(2.0 * x) + math.sin(6.0 * x) + math.cos(x) + 4.0
            timed_y_list.append(y)

        timed_y_data = np.array(timed_y_list)

        x_list.extend(timed_y_list[0:time_step_size])
        graph_data.extend(timed_y_list[0:time_step_size])
        for idx in range(predict_count):
            print("xlist",x_list)
            x_data = np.array(x_list)
            if model_name == "model_nn.keras":
                rsdata = np.reshape(x_data, (1, 5))
                print(rsdata)
                y = model.predict(rsdata, batch_size=1)
            else:
                rsdata = np.reshape(x_data, (1, -1, 1))
                print(rsdata)
                y = model.predict(rsdata, batch_size=1)
            print("y", y)
            x_list.append(y[0][0])
            x_list.pop(0)
            graph_data.append(y[0][0])

        if not flag:
            flag = True
            ax.plot(timed_y_data, label="real")

        ax.plot(np.array(graph_data), label=model_name)

    plt.legend()
    plt.savefig("tester.png")

결과 데이터를 마지막에 계속 추가해서 새로운 입력을 만들어서 호출 하는 방식으로 20개 정도 예측하도록 하였고 이것을 그래프로도 그려보았습니다.
nn으로 했을때 입력 포맷이 약간 달라지기 때문에 아래와 같이 작업 하였습니다.
            if model_name == "model_nn.keras":
                rsdata = np.reshape(x_data, (1, 5))
                print(rsdata)
                y = model.predict(rsdata, batch_size=1)
            else:
                rsdata = np.reshape(x_data, (1, -1, 1))
                print(rsdata)
                y = model.predict(rsdata, batch_size=1)

최적화를 하지 않은 상태에서 결론만 놓고 본다면 rnn,LSTM을 사용하지 않고 기존 nn에 feature를 늘리는 방식으로도 예측이 잘된다고 볼 수 있습니다.