Tensorflow

케라스 Conv-LSTM을 활용한 영상 예측 예제

카카오그래놀라 2020. 11. 3. 10:58

Keras 예제 번역: Next-frame prediction with Conv-LSTM

Next-frame prediction with Conv-LSTM

 

conv-LSTM

 

Author: jeammimi
Date created: 2016/11/02
Last modified: 2020/05/01
Description: Conv-LSTM을 활용해, sequence의 다음 프레임을 예측해봅시다.

Source:

keras.io/examples/vision/conv_lstm/

colab.research.google.com/github/keras-team/keras-io/blob/master/examples/vision/ipynb/conv_lstm.ipynb

github.com/keras-team/keras-io/blob/master/examples/vision/conv_lstm.py

 

소개

 이 글은 Conv-LSTM 모델의 사용에 관한 글입니다. 이 모델은 가상으로 만든, 움직이는 사각형을 담고 있는 영상을 활용하여 그 다음 프레임을 예측하는 모델입니다.

This script demonstrates the use of a convolutional LSTM model. The model is used to predict the next frame of an artificially generated movie which contains moving squares.

 

라이브러리 로드

from tensorflow import keras
from tensorflow.keras import layers
import numpy as np
import pylab as plt

 

모델 만들기

 우선, 영상의 (n_frames, width, height, channels)를 Input Shape로 가져와, 동일한 shape를 반환하는 모델을 만듭시다.

We create a model which take as input movies of shape (n_frames, width, height, channels) and returns a movie of identical shape.
seq = keras.Sequential(
    [
        keras.Input(
            shape=(None, 40, 40, 1)  
            # (time, rows, cols, channels)
            # 이 중, time을 지정하지 가변으로 두고, (None, 40,40,1) shape의 프레임을 입력받습니다.
        ),  # 40x40에 1채널을 가지는 프레임입니다.
        layers.ConvLSTM2D(
            filters=40, kernel_size=(3, 3), padding="same", return_sequences=True
        ),
        layers.BatchNormalization(),
        layers.ConvLSTM2D(
            filters=40, kernel_size=(3, 3), padding="same", return_sequences=True
        ),
        layers.BatchNormalization(),
        layers.ConvLSTM2D(
            filters=40, kernel_size=(3, 3), padding="same", return_sequences=True
        ),
        layers.BatchNormalization(),
        layers.ConvLSTM2D(
            filters=40, kernel_size=(3, 3), padding="same", return_sequences=True
        ),
        layers.BatchNormalization(),
        layers.Conv3D(
            filters=1, kernel_size=(3, 3, 3), activation="sigmoid", padding="same"
        ),
    ]
)
seq.compile(loss="binary_crossentropy", optimizer="adadelta")

 

인위적으로 영상 데이터 생성하기

 3개에서 7개의 움직이는 사각형을 담은 영상을 만듭시다. 사각형들의 shape는 1x1 또는 2x2 사이즈입니다. 그리고 선형적으로 움직입니다. 편의를 위해서 80x80 사이즈로 동영상을 만든 후, 40x40으로 crop하여 사용하겠습니다.

Generate movies with 3 to 7 moving squares inside. The squares are of shape 1x1 or 2x2 pixels, and move linearly over time. For convenience, we first create movies with bigger width and height (80x80) and at the end we select a 40x40 window.
def generate_movies(n_samples=1200, n_frames=15):
    row = 80
    col = 80
    
    # 인위적으로 만들 영화가 담길 배열을 선언
    # noisy라고 한 이유는 noisy를 추가한 영화를 만들 것이기 때문입니다. 특별한 이유는 없습니다.
    noisy_movies = np.zeros((n_samples, n_frames, row, col, 1), dtype=np.float)
    
    # 한 프레임 이동한 값(정답값)을 담고 있는 배열입니다.
    shifted_movies = np.zeros((n_samples, n_frames, row, col, 1), dtype=np.float)

    for i in range(n_samples): # 영화 샘플수만큼 반복
        # Add 3 to 7 moving squares
        # 랜덤하게 프레임 속 사각형의 개수를 정합니다. 여기서는 3~7개 중 랜덤하게 개수를 정합니다. 
        n = np.random.randint(3, 8)

        for j in range(n): # 사각형 개수만큼 반복

            # 초기 위치를 랜덤하게 지정합니다. 그 범위는 x,y축 모두 20이상 60미만 사이입니다.
            xstart = np.random.randint(20, 60)
            ystart = np.random.randint(20, 60)
            
            # 이동 방향을 랜덤하게 지정합니다.
            # 방향은 (-1, 0, 1) 중 하나의 값을 가집니다. 역방향, 정지, 정방향
            # x,y 축 모두를 고려하면 3x3의 총 9가지입니다.
            # (0,0)은 정지. 위,아래,좌,우, 대각선 4방향. 총 9가지
            directionx = np.random.randint(0, 3) - 1
            directiony = np.random.randint(0, 3) - 1

            # Size of the square
            # 사각형의 크기를 랜덤하게 정합니다. 2 또는 3의 크기
            w = np.random.randint(2, 4)

            for t in range(n_frames): # 프레임 수 만큼 반복
            	
                # 이동후 좌표.
                x_shift = xstart + directionx * t
                y_shift = ystart + directiony * t
                
                # i번째 샘플의, t번째 프레임에서, 이동 후 좌표를 기준으로 사각형을 채웁니다.
                # 맨 마지막 0은, 이 영상의 채널은 1채널이기 때문에 0이라고 한 것입니다.
                noisy_movies[
                    i, t, x_shift - w : x_shift + w, y_shift - w : y_shift + w, 0
                ] += 1

                # Make it more robust by adding noise.
                # robust한 모델을 만들기 위해 noise를 추가해줍니다.
                # robust를 간략하게 말하면, outlier에 민감하게 반응하지 않는
                # general(일반적인) 모델을 만든다는 의미입니다.
                # 추론 동안에 정확하게 1이 아니더라도 1가 가까운 값이라면
                # 여전히 사각형에 속한다고 간주하도록 하기 위함입니다.
                
                # The idea is that if during inference,
                # the value of the pixel is not exactly one,
                # we need to train the model to be robust and still
                # consider it as a pixel belonging to a square.
                
                # 0과 1중 랜덤하게 고를 때, 만약 1이라면 노이즈를 주겠다는 말입니다.
                if np.random.randint(0, 2):
                	
                    # noise_f가 가질 수 있는 값: 1 또는 -1
                    noise_f = (-1) ** np.random.randint(0, 2)
                    
                    # 위에서 추가한 프레임에서,
                    # 사각형보다 상,하,좌,우 각각 1씩 더 큰 사각형에 (가로,세로 모두 2씩 증가)
                    noisy_movies[
                        i,
                        t,
                        x_shift - w - 1 : x_shift + w + 1,
                        y_shift - w - 1 : y_shift + w + 1,
                        0,
                    ] += (noise_f * 0.1) # 노이즈 값을 추가합니다
                    # noise_f가 1 또는 -1의 값을 가지기 때문에
                    # 결과적으로 0.1 또는 -0.1씩 노이즈를 추가하는 셈입니다.
                
                
                # 정답 값을 담아봅시다.
                # 우리가 만든 이미지에서 한프레임씩 앞으로 간 영상입니다.
                # 여기는 노이즈가 추가되지 않습니다.
                x_shift = xstart + directionx * (t + 1)
                y_shift = ystart + directiony * (t + 1)
                shifted_movies[
                    i, t, x_shift - w : x_shift + w, y_shift - w : y_shift + w, 0
                ] += 1

    # 80x80 영상 중 가운데 40x40범위만 사용합니다.
    # 굳이 이렇게 한 이유는 영상을 처음부터 작게 만들면 인덱스 에러가 발생할 가능성이 있기 때문에
    # 편의상 크게 만들고, 잘라서 사용하는 것입니다. 특별한 이유는 없습니다.
     
    noisy_movies = noisy_movies[::, ::, 20:60, 20:60, ::]
    shifted_movies = shifted_movies[::, ::, 20:60, 20:60, ::]
    
    # 아까 노이즈를 0.1를 주면서 실제 값이 1.1로 변한 경우가 있기 때문에
    # 1.1을 다시 1로 돌려놓는 것입니다.
    
    noisy_movies[noisy_movies >= 1] = 1
    shifted_movies[shifted_movies >= 1] = 1
    return noisy_movies, shifted_movies

 

모델 훈련하기

epochs = 1  # 예제이기 때문에 에폭을 1로 지정했습니다.
            # 실제로 모델의 정확도를 높이려면 수백번 이상으로 설정하세요!

noisy_movies, shifted_movies = generate_movies(n_samples=1200)
seq.fit(
    noisy_movies[:1000],
    shifted_movies[:1000],
    batch_size=10,
    epochs=epochs,
    verbose=2,
    validation_split=0.1,
)

 

모델 예측 결과보기

 앞서 만든, noisy_movies 데이터 셋에서 1004번째 영상을 선택하고, 그 중 앞 7프레임을 Test set으로 선정합니다. 그리고 앞으로의 16개의 프레임을 예측합니다.

movie_index = 1004
test_movie = noisy_movies[movie_index]

# 1004번째 영상의 프레임 중 앞의 7개의 프레임을 test set으로 선정합니다.
track = test_movie[:7, ::, ::, ::]

# Predict 16 frames
# 앞으로의 16개의 프레임을 예측해 봅시다.
for j in range(16):

    # 모델의 input shape에 맞추기 위해 np.newaxis를 붙여줬습니다.
    # 영상 축을 추가한 것입니다.
    # (프레임, 세로, 가로, 채널)에서 (영상, 프레임, 세로, 가로, 채널)로 바꿔주었습니다.
    new_pos = seq.predict(track[np.newaxis, ::, ::, ::, ::])
    
    # test set에서 마지막 프레임에 추가하기 위해서 -1을 적어줬습니다.
    new = new_pos[::, -1, ::, ::, ::]
    track = np.concatenate((track, new), axis=0)
    
    # 처음에는 7프레임으로 예측했고,
    # 그 다음은 예측한 프레임을 더한 8프레임으로 예측하고,
    # 16번의 프레임이 더해질 때까지 반복합니다.