2023年11月8日水曜日

Keras で2値判定のRNNをやってみる

Keras で2値判定のRNNをやってみる

概要

前回 RNN を使って正弦波の予測を行ってみました
10 個の入力に対して 10 個の予測値を出力するようなモデルを組みました
今回は複数の入力に対して 0 or 1 を判定するようなモデルを組んでみたいと思います

コードも前回のを引き継いでいます
内容は参考サイトと同じですがコードのリファクタと執筆時点での最新の tenforflow を使って動作確認しています

環境

  • macOS 14.0
  • Python 3.11.6
    • keras 2.14.0
    • tensorflow 2.14.0
    • matplotlib 3.8.1

サンプルコード

from dataclasses import dataclass
from random import randint
from typing import Any

import numpy as np
from tensorflow.keras.layers import Dense, SimpleRNN
from tensorflow.keras.models import Sequential

# RNN で使う一時系列のデータ数
NUM_RNN = 10
# 学習させるデータの数
NUM_DATA = 200


# 説明変数を管理するクラス
class IndependentVariable:
    # 3次元の行列データ (200, 10, 1)
    data: np.ndarray

    def __init__(self, num_rnn: int) -> None:
        # 今回は説明変数と目的変数を別で作るのが面倒なので説明変数側で作成してしまう
        # 空の器を作成
        x_train = np.empty((0, num_rnn))
        y_train = np.empty((0, 1))
        for i in range(NUM_DATA):
            num_random = randint(-20, 20)
            if i % 2 == 1:  # 奇数の場合
                # linespaceは等差数列
                x_train = np.append(
                    x_train,
                    np.linspace(
                        num_random, num_random + num_rnn - 1, num=num_rnn
                    ).reshape(1, num_rnn),
                    axis=0,
                )
                # 減少する数列の場合は1
                y_train = np.append(y_train, np.zeros(1).reshape(1, 1), axis=0)
            else:  # 偶数の場合
                x_train = np.append(
                    x_train,
                    np.linspace(
                        num_random, num_random - num_rnn + 1, num=num_rnn
                    ).reshape(1, num_rnn),
                    axis=0,
                )
                # 増加する数列の場合は0
                y_train = np.append(y_train, np.ones(1).reshape(1, 1), axis=0)
        # RNN 学習用に整形
        self.data = x_train.reshape(NUM_DATA, num_rnn, 1)
        self.labels = y_train.reshape(NUM_DATA, 1)


# 目的変数を管理するクラス
class DependentVariable:
    # 3次元の行列データ (10, 1)
    # RNN では目的変数は説明変数の一つずれた配列データを作成するのがポイント
    labels: Any

    def __init__(self, labels: np.ndarray) -> None:
        self.labels = labels


# テストデータを管理するクラス
class TestData:
    def __init__(self) -> None:
        self.independent = IndependentVariable(NUM_RNN)
        self.dependent = DependentVariable(self.independent.labels)


# RNN モデルを管理するクラス
class RNNModel:
    NUM_DIM = 16  # 中間層の次元数
    model: Sequential

    def __init__(self) -> None:
        self.model = Sequential()

    def compile(self) -> None:
        # RNN層
        self.model.add(
            SimpleRNN(
                self.NUM_DIM,
                batch_input_shape=(None, NUM_RNN, 1),
                return_sequences=False,
            )
        )
        # 最終出力が1つの結合層を作成
        self.model.add(Dense(1, activation="sigmoid"))
        self.model.compile(loss="binary_crossentropy", optimizer="adam")

    def show(self):
        self.model.summary()

    def train(
        self,
        independent: IndependentVariable,
        dependent: DependentVariable,
        epochs=20,
        batch_size=8,
    ):
        self.model.fit(
            independent.data, dependent.labels, epochs=epochs, batch_size=batch_size
        )

    def save(self, file_name="my_model"):
        self.model.save(file_name)


# 実際にモデルを評価するためのクラス
@dataclass
class Result:
    y_pred: int
    test_data: np.ndarray

    def show(self):
        if self.y_pred > 0.9:
            print(f"減算する数列です ({self.y_pred}) -> ", self.test_data)
        elif self.y_pred < 0.1:
            print(f"増加する数列です ({self.y_pred}) -> ", self.test_data)
        else:
            print(f"未知の数列です ({self.y_pred}) -> ", self.test_data)


class Test:
    def __init__(self, model: RNNModel, test_data: TestData) -> None:
        self.model = model
        # 学習データの一部をそのまま学習データに利用する
        self.test_data = test_data

    def run(self) -> list[Result]:
        # 10回テスト
        result: list[Result] = []
        for i in range(10):
            y_pred = self.model.model.predict(
                self.test_data.independent.data[i].reshape(1, NUM_RNN, 1)
            )
            r = Result(
                y_pred=y_pred,
                test_data=self.test_data.independent.data[i].reshape(NUM_RNN),
            )
            result.append(r)
        return result


if __name__ == "__main__":
    # テストデータ作成
    test_data = TestData()
    # モデル生成
    model = RNNModel()
    model.compile()
    # モデルの訓練
    model.train(
        test_data.independent,
        test_data.dependent,
    )
    # モデルの評価
    test = Test(model, test_data)
    result = test.run()
    # 結果を表示
    for r in result:
        r.show()

ちょっと解説

今回テストデータの生成はすべて説明変数内で行っています
インタフェースの関係で目的変数用のクラスは残しています

学習させる際は最後に出力 1 の Dense レイヤーを作成するのがポイントです
結果の表示は 0.9 以上 0.1 以下でやっていますがたまーに 0.7 や 0.3 などが出てくる可能性があるのでここは適当に変更してください
RNN の 2 値判定の場合は必ず 1 or 0 が出力されるわけではなく与えられた値から 1 っぽい 0 っぽい値が出てくるのでこの判定は自分で行う必要があります

最後に

2値判定の RNN をやってみました
ほぼ前回のコードと流れも変わっていない感じです
少しリファクタをさぼった感じがするのでもう少しいい感じに書ける気がします

参考サイト

0 件のコメント:

コメントを投稿