본문 바로가기

Data Science/Machine Learning 구현

[ML] Tensorflow Window Dataset 객체로 시계열 예측 구현하기

반응형

이번 포스팅에서는 Tensorflow 2.x 버전에서 제공하는 Window Dataset 객체로 시계열 순환신경망을 구현하는 방법에 대해 알아본다. 최근에 수요 예측 알고리즘 대회에 참가하면서 Tensorflow를 활용해 딥러닝으로 시계열 예측을 시도했는데, 구현하는 과정이 순조롭지 않았다. 레퍼런스 문서를 Tensorflow 공식 문서를 활용해 구현하려 했지만 개인적으로 이해하기가 매우 난해했다. 너무 작성자 위주로 작성되어 있다고 해야 할까?

 

그러던 중 유용한 유투브 강의를 참고하면서 tf.data.Dataset 객체를 활용해 시계열 Window 데이터셋을 만드는 데 성공했다. 그리고 시계열 데이터를 예측하는 데 있어서 문제에 따라 예측하는 유형이 매우 다양하다는 것을 느꼈다. 그래서 이번 기회에 알게된 내용을 기반으로 해결하려는 시계열 예측 유형에 맞게 Tensorflow 윈도우 데이터셋 객체를 만드는 방법에 대해 정리해보려고 한다. 물론 모든 예측 유형의 경우의 수를 모두 나열할 순 없겠지만 이를 기반으로 자신이 해결하려는 문제에 맞게 응용은 할 수 있지 않을까 한다.

 

Tensorflow.Dataset 객체를 활용해 시계열 예측을 해보자.


우선 알아보기에 앞서서 예시 데이터를 사용해야 하는데, 시계열 예측 Tensorflow 공식 문서에서 활용하는 오픈 데이터를 활용하려고 한다. 그런데 해당 문서에 가서 데이터를 다운로드 받는 것도 좋긴 하지만 앞으로 설명하려는 내용에 맞게 필자가 전처리를 일부 처리해 놓은 데이터를 여기에서 다운로드 받자.

 

이제 데이터를 한 번 살펴보자. 참고로 현재 사용하는 Tensorflow 버전은 2.7.0 이다.(Tensorflow 1.x 버전으로 아래의 실습을 하게 되면 원하는 결과가 나오지 않을 수 있습니다! 꼭 2.x 버전으로 업데이트 해주는 게 좋을 듯 합니다!)

 

import pandas as pd
import numpy as np
import tensorflow as tf

df = pd.read_csv('./tf_tutorial_final.csv', encoding='utf-8')
print(df.shape)
df.head()

 

데이터 미리보기

 

위 데이터는 일일 데이터이며, 날짜 변수와 13개의 독립변수, 한 개의 종속변수를 갖고 있음을 알 수 있다. 해당 포스팅에서는 데이터 분석 목적이 아니기 때문에 각 변수가 무엇을 의미하는지는 설명하지 않겠다. 단지 y 라는 변수를 예측하기 위해 x1 ~ x13 변수들을 사용한다는 것만 알아두자.

 

또 하나 추가적으로 해주어야 할 것이 있다. tf.Dataset 객체로 만들려면 Numpy array 형태로 넣어주어야 하는데, 이 때 array에는 모두 정수 또는 실수형의 데이터들만 있어야 한다. 위 데이터의 datetime 칼럼처럼 object나 datetime 형태로 들어가선 안 된다. 그런데 우리는 여기서 datetime 변수를 버리는 것이 아니라 특수 문자(-, : 같은 것들)을 제거해주고 datetime 값을 정수형태로 바꾸어 줄 것이다. 왜 이렇게 하는지 의문이 들텐데, 이유는 tf.Dataset 객체로 만든 후 어떻게 Window Dataset이 구성되었는지 '날짜'로 확인하면 매우 식별하기 쉽기 때문이다. 이에 대해서는 포스팅을 읽다보면 왜 이렇게 했는지 이해가 갈 것이다. 어쨌거나 datetime을 전처리 해주는 소스코드는 아래와 같다.

 

df['datetime'] = df['datetime'].str.replace('-','')
df['datetime'] = df['datetime'].str.split(' ', expand=True)[0]
df['datetime'] = df['datetime'].astype(int)

 

 

이제 데이터를 X, y로 나누어 주기 위해 아래와 같이 Numpy array로 각각 분할한다. 이 때 독립변수, 종속변수들의 값들 모두 다 정수형으로 바꾸어준다. 왜냐하면 하나라도 실수형으로 들어가 있다면 tf.Dataset 객체로 변환 후 출력했을 때, 모두 실수형으로 자동 변환되기 때문에 우리가 위에서 datetime을 식별하기 위해 바꾸어준 정수형 변환이 헛수고(?)가 된다. 따라서 아래와 같이 전처리를 수행해준다.

 

X_train, y_train = df.iloc[:, :-1].values, df.iloc[:, [-1]].values
X_train = X_train.astype(int)
y_train = y_train.astype(int)
print(X_train.shape, type(X_train))
print(y_train.shape, type(y_train))

 

0. Tensorflow Window 함수들 이해하기

Tensorflow 윈도우 데이터셋 객체로 만드는 방법을 배우기 전에 몇 가지 배워야 할 함수들이 있다. 해당 함수들에 대해서는 참고한 유투브 강의를 보는 것도 추천한다. 여기서는 간단하게만 설명하고 넘어가려 한다. 먼저 소스코드를 살펴보자.

 

import tensorflow as tf

ds_x = tf.data.Dataset.from_tensor_slices(X_train)
ds_x = ds_x.window(size=X_window_size, stride=stride, shift=shift, drop_remainder=True)
ds_x = ds_x.flat_map(lambda x: x.batch(X_window_size))

 

먼저 X_train 변수는 위에서 만든 X_train 변수와 동일하다. 이제 한 줄씩 살펴보자. 우선 tf.data.Dataset.from_tensor_slices 함수는 Numpy Array를 입력받아 Tensor로 바꾸어 주는 역할을 한다. 구체적인 클래스 이름은 TensorSliceDataset 이지만 이것이 구체적으로 어떤 클래스인지에 대해서는 너무 깊게 들어가지 말자. 다음은 tf.data.Dataset.from_tensor_slices 함수로 반환받은 TensorSliceDataset 객체를 window라는 함수를 사용해서 윈도우 데이터셋으로 분할할 수 있다. 자주 사용되는 인자로는 size, stride, shift, drop_remainder 가 있다.

 

이 중 drop_remainder에 대해서만 설명하겠다. 나머지는 아래 예시를 살펴보면서 이해하자. drop_remainder는 특정 윈도우 사이즈로 짤라낼 때, 데이터 개수에 따라 마지막 윈도우는 특정 사이즈만큼의 길이가 아닐 수도 있다. 예를 들어, 총 9개의 데이터가 있는데 윈도우 사이즈를 2라고 한다면 2개, 2개, 2개, 2개, 1개가 될 것이다. 이 때 사이즈가 1개인 마지막 윈도우를 drop 할지 말지를 결정하는 것이 drop_remainder 가 하는 역할이다. 보통은 True로 설정해서 마지막 남은 윈도우를 날려버리는데, 왜냐하면 마지막 윈도우 사이즈가 다름에도 drop_remainder = False로 설정한다면 딥러닝 모델에 윈도우 데이터를 입력시킬 때 에러를 발생시키기 때문이다. 모델링을 많이 해보신 분은 아시겠지만 입력으로 넣을 때, 데이터의 shape 아다리(?)를 맞춰주지 못해서 많이 고생한 경험이 있을 것이다.

 

다음은 flat_map 함수이다. 파이썬의 map 함수와 기능이 비슷하다. 차이점은 'flat(평평한)'의 의미처럼, 인자로 넣은 Tensor의 한 차원을 줄여주어 반환하게 된다. 그리고 batch 함수 인자에는 윈도우 사이즈와 동일한 값을 넣어준다. 이 때, batch 함수에 넣는 인자값은 방금 위에서 size로 정의한 윈도우 사이즈를 얼마만큼의 배치 사이즈로 가져올지를 의미한다.

 

예를 들어, 7일을 윈도우 사이즈 즉, size의 값으로 설정했다. 그러면 7일씩 하나의 윈도우로 설정해서 데이터를 가져올 것이다. 그런데 이 때, batch 함수에 만약 2를 집어넣는다면 7일씩 데이터를 또 2일 데이터씩 쪼개서 가져오게 된다. 따라서 보통은 size에 넣는 값과 batch 함수 인자에 넣는 값을 동일하게 해준다. 즉, 7일을 윈도우 사이즈로 설정했으면 batch 사이즈도 7로 설정해서 한 번에 다 가져오도록 설정한다.

1. Window 함수의 size, shift, stride 이해하기

이번엔 window 함수의 인자에 대해 이해해보자. 먼저 아래와 같은 소스코드가 있다고 가정하자. 참고로 take 함수는 데이터를 몇 개 가져올지를 수행한다.

 

# 로그 레벨 3으로 변경해서 경고 표시 안뜨게 하기
import os
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'

X_window_size = 1  # 총 1개의 window(총 1개의 row)
stride = 1         # 이전 window와 다음 window 시작점 간의 간격
shift = 1          

ds_x = tf.data.Dataset.from_tensor_slices(X_train)
ds_x = ds_x.window(size=X_window_size, stride=stride, shift=shift, drop_remainder=True)
ds_x = ds_x.flat_map(lambda x: x.batch(X_window_size))

for x in ds_x.take(4):
    print(x.shape)
    print(x)
    print('-'*100)

 

위 소스코드에서 size = 1, shift = 1, stride = 1일 때의 각각 의미를 도식화해보면 아래와 같다.

 

size = 1, shift = 1 , stride = 1인 경우

 

소스코드에서 size = 1로 설정했다. 그 말은 즉, 윈도우 사이즈를 1로 한다는 의미이다. 위 그림에서 A, B라고 되어 있는 부분이 바로 사이즈가 1인 윈도우 2개를 의미한다. 그리고 A, B 윈도우 간의 간격은 현재 하루 차이로 1일이다. 이 때, 윈도우 간의 간격이 몇 인지를 설정하는 것이 shift가 하는 역할이다. stride는 윈도우 내의 데이터 간의 시간 간격을 의미한다. 하지만 위 예시에서는 stride를 어떤 값으로 설정해도 바뀌지 않는다. 왜냐하면 윈도우 내의 데이터가 1개 밖에 없기 때문이다. 따라서 stride는 size가 2 이상일 때만 적용된다.

 

이번엔 stride를 가시적으로 이해하기 위해서 sizeshift를 2로 늘린 후, stride가 1일 때와, 2일 때 차이를 비교해보자. 먼저 stride = 1일 때이다.

 

size = 2, shift = 2, stride = 1일 경우

 

size = 2이기 때문에 A 윈도우는 2009-01-01, 2009-01-02를 담고 있다. 이 때, shift = 2로 설정했기 때문에 B 윈도우는 2009-01-03, 2009-01-04를 담고 있다. 위에서 언급한 것처럼 shift은 윈도우 간의 시간 차이를 의미한다고 했다. A의 윈도우 내 첫 번째 데이터인 2009-01-01과 B 윈도우 내 첫 번째 데이터인 2009-01-03은 2일 차이임을 알 수 있다. 이것이 바로 shift의 의미다.

 

이제 stride = 1을 살펴보자. stride는 윈도우 내의 날짜의 간격을 의미한다. A 윈도우 내에는 2009-01-01, 2009-01-02를 담고 있다. 즉, stride는 2009-01-01 과 2009-01-02의 시간 차이를 의미한다. B 윈도우도 마찬가지다. 그렇다면 위 조건에서 stride만 2로 바꾸었을 때는 아래와 같이 변경된다.

 

size = 2, shift = 2, stride = 2일 경우

 

stride를 2로 변경했기 때문에 2009-01-03 데이터를 A, B 윈도우가 동시에 공유하고 있다. A 윈도우는 stride가 2인 것을 지키기 위해 2009-01-01 과 2일 차이가 나는 2009-01-03 데이터를 포함한다. 이 떄, shift = 2이기 때문에 B 윈도우는 A 윈도우와 간격이 2일 이어야 한다. 따라서 A 윈도우의 시작날짜인 2009-01-01 에 2일을 더한 2009-01-03일이 B 윈도우의 시작 날짜이다. 그리고 B 윈도우도 stride = 2를 지키기 위해 2009-01-03일 보다 2일 뒤인 2009-01-05 데이터를 포함한다.

 

이제 어느 정도 감이 잡혔을까? 그렇다면 만약 size = 3, shift = 3, stride = 1로 설정했다면, 다음과 같은 데이터로 분할 될 것이다.

 

size = 3, shift = 3, strid = 1로 설정했을 경우

 

결과를 눈으로 직접 확인해보고 싶다면 아래 소스코드를 실행시켜보자.

 

import os
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'

# size = 3, shift = 3, stride = 1
X_window_size = 3
shift = 3 
stride = 1                  

ds_x = tf.data.Dataset.from_tensor_slices(X_train)
ds_x = ds_x.window(size=X_window_size, stride=stride, shift=shift, drop_remainder=True)
ds_x = ds_x.flat_map(lambda x: x.batch(X_window_size))

for x in ds_x.take(4):
    print(x.shape)
    print(x)
    print('-'*100)

 

아래는 결과 화면인데, 출력 array의 날짜를 의미하는 맨 왼쪽 첫 번째 원소값들을 살펴보면 위 그림과 같이 잘 분할된 것을 알 수 있다.(이러한 식별을 위해서 초반에 날짜를 정수로 전처리해준 이유였다)

 

size = 3, shift = 3, stride = 1일때의 윈도우 분할 결과

 

이러한 방식으로 y_train 변수에 대해서도 원하는 예측 유형에 맞게 윈도우 데이터셋을 만들어주면 된다. 따라서 X, y에 대해 윈도우 데이터셋을 만드는 것을 함수화하면 아래와 같이 할 수 있다. 주의할 점은 문제 유형에 따라 X, y 윈도우 사이즈가 다를 수 있기 때문에 X, y의 각 window 함수로 넣어주는 size, shift, stride 값은 개별 인자로 넣어주도록 하자.

 

import tensorflow as tf  # 2.7.0 version

def window_dataset(X, y, X_size, y_size, X_shift, y_shift, X_stride, y_stride, batch_size):

    ds_x = tf.data.Dataset.from_tensor_slices(X)
    ds_x = ds_x.window(size=X_size, stride=X_stride, shift=X_shift, drop_remainder=True)
    ds_x = ds_x.flat_map(lambda x: x.batch(X_size))
    
    ds_y = tf.data.Dataset.from_tensor_slices(y)
    ds_y = ds_y.window(size=y_size, stride=y_stride, shift=y_shift, drop_remainder=True)
    ds_y = ds_y.flat_map(lambda y: y.batch(y_size))
    
    ds = tf.data.Dataset.zip((ds_x, ds_y))
    return ds.batch(batch_size).prefetch(1)

tf_dataset = window_dataset(X_train, y_train, X_size, y_size,
                            X_shift, y_shift, X_stride, y_stride, batch_size)

2. 단일스텝 예측

이제 위에서 배운 window 함수를 사용해서 단일스텝 예측을 위한 윈도우 데이터셋을 만들어보자. 먼저 단일스텝 예측이 어떤 방식으로의 예측인지를 도식화해서 알아보자. 참고로 단일스텝 개념과 추후에 소개할 다중스텝 개념은 Tensorflow 시계열 예측 공식 문서에서 설명하는 것과 동일하다.

 

단일스텝의 대표적인 유형

 

위 그림의 2가지 데이터프레임 유형은 단일스텝 예측의 전형적인 유형이라고 할 수 있다. 첫 번째 그림부터 살펴보자. 0번째 데이터의 Feature들로 1번째 레이블을 예측하고, 1번째 데이터로 2번째 레이블을 예측하고, ... 반복적으로 현재 타임스텝의 1개 데이터를 가지고 다음의 단일스텝 1개를 예측한다. 이러한 유형을 윈도우 데이터셋으로 만들어보는 소스코드는 아래와 같다.(소스코드에 사용된 window_dataset 함수는 위에서 소개한 함수와 동일하다)

 

def window_dataset(X, y, X_size, y_size, X_shift, y_shift, X_stride, y_stride, batch_size):

    ds_x = tf.data.Dataset.from_tensor_slices(X)
    ds_x = ds_x.window(size=X_size, stride=X_stride, shift=X_shift, drop_remainder=True)
    ds_x = ds_x.flat_map(lambda x: x.batch(X_size))
    
    ds_y = tf.data.Dataset.from_tensor_slices(y)
    ds_y = ds_y.window(size=y_size, stride=y_stride, shift=y_shift, drop_remainder=True)
    ds_y = ds_y.flat_map(lambda y: y.batch(y_size))
    
    ds = tf.data.Dataset.zip((ds_x, ds_y))
    return ds.batch(batch_size).prefetch(1)

# X에 대한 윈도우 함수 설정값
X_size = 1
X_shift = 1
X_stride = 1
# y에 대한 윈도우 함수 설정값
y_size = 1
y_shift = 1
y_stride = 1

batch_size = 1

tf_dataset = window_dataset(X_train, y_train[1:], X_size, y_size,
                            X_shift, y_shift, X_stride, y_stride, batch_size)

# 데이터 shape, 미리보기로 체크
for x, y in tf_dataset.take(3):
    print('X:', x.shape)
    print(x)
    print()
    print('Y:', y.shape)
    print(y)
    print('-'*100)

 

주의할 점은  y_train을 설정해줄 때, 한 단계 lag된 값을 넣어주어야 하기 때문에 슬라이싱으로 [1:] 하는 것을 잊지 말자. 아래는 위 소스코드에서 take(3) 으로 윈도우 데이터셋을 미리보기하고 실제 데이터프레임과 일치하는지 비교한 결과다. 

 

분할 결과와 실제 데이터프레임 비교

 

첫 번째 데이터인 20090101 이라는 데이터의 레이블로는 -4가 들어가 있는 것을 볼 수 있다. 이를 하단 셀의 test 라는 데이터프레임 출력화면을 보면 -4가 20090102 날짜의 y 값임을 알 수 있다. 예상대로 잘 분할되었다.

 

다음은 밑에 있는 두 번째 그림에 있는 단일스텝 예측 유형이다. 주의할 점은 X, y의 윈도우 사이즈(size 값)가 커졌다고 해서 다중스텝을 의미하지 않는다. 그림을 보면 알겠지만 단지 첫 번째 유형보다 단순히 size만 커진 것일 뿐, 하나의 데이터(데이터프레임의 하나의 row)가 들어가서 다음 스텝의 레이블 하나를 예측하는 것은 동일하다. Tensorflow 공식문서에서는 이에 대해 아래와 같은 그림으로 설명해준다.(아래 그림을 밑에서 소개할 다중스텝과 비교해보자)

 

단일스텝 예측(출처: Tensorflow 공식문서)

 

따라서 두 번째 그림을 윈도우 데이터셋으로 분할하는 방법은 아래와 같다.

 

def window_dataset(X, y, X_size, y_size, X_shift, y_shift, X_stride, y_stride, batch_size):

    ds_x = tf.data.Dataset.from_tensor_slices(X)
    ds_x = ds_x.window(size=X_size, stride=X_stride, shift=X_shift, drop_remainder=True)
    ds_x = ds_x.flat_map(lambda x: x.batch(X_size))
    
    ds_y = tf.data.Dataset.from_tensor_slices(y)
    ds_y = ds_y.window(size=y_size, stride=y_stride, shift=y_shift, drop_remainder=True)
    ds_y = ds_y.flat_map(lambda y: y.batch(y_size))
    
    ds = tf.data.Dataset.zip((ds_x, ds_y))
    return ds.batch(batch_size).prefetch(1)

# X에 대한 윈도우 함수 설정값
X_size = 3
X_shift = 1
X_stride = 1
# y에 대한 윈도우 함수 설정값
y_size = 3
y_shift = 1
y_stride = 1

batch_size = 1

tf_dataset = window_dataset(X_train, y_train[3:], X_size, y_size,
                            X_shift, y_shift, X_stride, y_stride, batch_size)

# 데이터 shape, 미리보기로 체크
for x, y in tf_dataset.take(3):
    print('X:', x.shape)
    print(x)
    print()
    print('Y:', y.shape)
    print(y)
    print('-'*100)

 

 

실제로 잘 분할되었는지 결과 비교화면은 생략하겠다. 필자가 확인하긴 했지만 혹시 의구심이 갖는다면 소스코드를 직접 돌려 확인해보는 것도 좋을 듯 하다.

3. 다중스텝 예측

이전에 알아본 단일스텝 예측은 하나씩 데이터를 넣어서 시간적인 특성을 파악하지 못하는 단점이 있다. 그래서 우리는 아래와 같은 유형처럼 시간적인 특성을 넣어줄 수 있는 다중스텝 예측을 한다고 가정해보자.

 

다중스텝의 대표적인 유형

 

위 두 그림의 큰 차이점은 1개의 y값을 예측하는지, 2개 이상의 연속적인 y값을 예측하는 지의 차이이다. 그리고 단일스텝 유형과의 큰 차이점은 윈도우를 구성할 때, 윈도우 간의 간격인 shift를 늘려줌으로써 모델이 데이터의 시간적인 특성을 파악할 수 있도록 했다. 아래 그림을 보면서 위에서 보았던 단일스텝일 때의 그림과 비교하면 차이점을 알 수 있을 것이다.

 

다중스텝 예측(출처: Tensorflow 공식문서)

 

위 그림의 첫 번째 유형을 윈도우 데이터셋으로 분할하는 소스코드는 아래와 같다.

 

def window_dataset(X, y, X_size, y_size, X_shift, y_shift, X_stride, y_stride, batch_size):

    ds_x = tf.data.Dataset.from_tensor_slices(X)
    ds_x = ds_x.window(size=X_size, stride=X_stride, shift=X_shift, drop_remainder=True)
    ds_x = ds_x.flat_map(lambda x: x.batch(X_size))
    
    ds_y = tf.data.Dataset.from_tensor_slices(y)
    ds_y = ds_y.window(size=y_size, stride=y_stride, shift=y_shift, drop_remainder=True)
    ds_y = ds_y.flat_map(lambda y: y.batch(y_size))
    
    ds = tf.data.Dataset.zip((ds_x, ds_y))
    return ds.batch(batch_size).prefetch(1)

# X에 대한 윈도우 함수 설정값
X_size = 3
X_shift = 3
X_stride = 1
# y에 대한 윈도우 함수 설정값
y_size = 1
y_shift = 3
y_stride = 1

batch_size = 1

tf_dataset = window_dataset(X_train, y_train[3:], X_size, y_size,
                            X_shift, y_shift, X_stride, y_stride, batch_size)

# 데이터 shape, 미리보기로 체크
for x, y in tf_dataset.take(3):
    print('X:', x.shape)
    print(x)
    print()
    print('Y:', y.shape)
    print(y)
    print('-'*100)

 

다음은 두 번째 그림에 해당하는 y값을 연속적으로 2개 예측하는 유형에 대한 소스코드이다.

 

def window_dataset(X, y, X_size, y_size, X_shift, y_shift, X_stride, y_stride, batch_size):

    ds_x = tf.data.Dataset.from_tensor_slices(X)
    ds_x = ds_x.window(size=X_size, stride=X_stride, shift=X_shift, drop_remainder=True)
    ds_x = ds_x.flat_map(lambda x: x.batch(X_size))
    
    ds_y = tf.data.Dataset.from_tensor_slices(y)
    ds_y = ds_y.window(size=y_size, stride=y_stride, shift=y_shift, drop_remainder=True)
    ds_y = ds_y.flat_map(lambda y: y.batch(y_size))
    
    ds = tf.data.Dataset.zip((ds_x, ds_y))
    return ds.batch(batch_size).prefetch(1)

# X에 대한 윈도우 함수 설정값
X_size = 3
X_shift = 3
X_stride = 1
# y에 대한 윈도우 함수 설정값
y_size = 2
y_shift = 3
y_stride = 1

batch_size = 1

tf_dataset = window_dataset(X_train, y_train[3:], X_size, y_size,
                            X_shift, y_shift, X_stride, y_stride, batch_size)

# 데이터 shape, 미리보기로 체크
for x, y in tf_dataset.take(3):
    print('X:', x.shape)
    print(x)
    print()
    print('Y:', y.shape)
    print(y)
    print('-'*100)

지금까지 시계열 예측을 위해 Tensorflow의 tf.Dataset 객체를 활용한 윈도우 데이터셋을 만들어 보는 방법에 대해 알아보았다. 위 코드를 기반으로 윈도우 데이터셋을 구축하고 사용자가 원하는 것에 따라 SimpleRNN, LSTM과 같은 순환신경망 모델들을 추가해 모델링을 진행할 수 있다. 다음 포스팅에서는 위 방법을 기반으로 간단하게 순환신경망을 모델링하고 학습시키는 과정까지 진행해보도록 하자.

반응형