13_5:勾配と報酬の記録

得られた勾配は、ゲームの全ステップ(内側のforループ)で一度記録し、1ゲームの終了後、外側のforループで、ゲームごとに記録します。また報酬は、ゲームの全ステップでゲームがつづいている間は配列に1を追加しゲームが終わると0を追加する、という方法で組み込みます。そして1ゲームの終了後、外側のforループで、総合計を計算して記録します。

下記リンクをクリックすると、サンプルの実行例を見ることができます。
勾配と報酬の記録を実行する例

このサンプルでは、次のコードを使用しています。

document.addEventListener('DOMContentLoaded', async() => {

    const gameInfo = document.getElementById('game-info');
    const stepInfo = document.getElementById('step-info');

    let currentActions_;

    const buildModel = () => {
        const model = tf.sequential();
        model.add(tf.layers.dense({
            units: 4,
            activation: 'elu',
            inputShape: [4]
        }));
        model.add(tf.layers.dense({
            units: 1
        }));
        model.summary();
        return model;
    }

    const getLogitsAndActions = (inputs) => {
        return tf.tidy(() => {
            const logits = model.predict(inputs);
            const leftProb = tf.sigmoid(logits);
            const rightProb = tf.sub(1, leftProb)
            const leftRightProbs = tf.concat([leftProb, rightProb], 1);
            const actions = tf.multinomial(leftRightProbs, 1, null, true);
            return [logits, actions];
        });
    }

    const getGradientsAndSaveActions = (inputTensor) => {
        const f = () => tf.tidy(() => {
            const [logits, actions] = getLogitsAndActions(inputTensor);
            currentActions_ = actions.dataSync();
            const labels = tf.sub(1, tf.tensor2d(currentActions_, actions.shape, 'float32'));
            return tf.losses.sigmoidCrossEntropy(labels, logits).asScalar();
        });
        return tf.variableGrads(f);
    }

    // 配列recordに、オブジェクトgradientsのデータを、プロパティ値として保持する
    // gradientsには、Objectのほか、この方法でデータを保持する配列も指定できる。
    const pushGradients = (record, gradients) => {
        // for-inループ
        // gradientsオブジェクトのプロパティに対して、 変数keyに、異なるプロパティ名を代入する処理を繰り返す。
        for (const key in gradients) {
            // 変数keyに代入されたプロパティが配列recordに存在する場合 -> ゲームの繰り返しの2回め以降
            if (key in record) {
                record[key].push(gradients[key]);
                // record[key]は、dense_Dense1/kernel、dense_Dense1/bias、dense_Dense2/kernel、dense_Dense2/bias
                // gradients[key]は、各重みパラメータが対応する数値

                // 変数keyに代入されたプロパティが、配列recordに存在しない場合
                // => recordにまだ設定されていない、"素の"配列の場合 => ゲームの繰り返しの初回
            }
            else {
                // 配列recordのプロパティとして変数keyの値を使用し、その値として、gradients[key]を持つ配列を割り当てる
                record[key] = [gradients[key]];
            }
        }
    }

    const maxStepsPerGame = 100;
    const numGames = 2;
    const cartPoleSystem = new CartPole(true);
    const model = buildModel();

    // すべての勾配を保持する配列
    const allGradients = [];
    // すべての報酬を保持する配列
    const allRewards = [];
    // 報酬の総和を保持する配列
    const gameSteps = [];

    for (let i = 0; i < numGames; ++i) {
        gameInfo.textContent = i + 1 + '回めのゲーム';
        cartPoleSystem.setRandomState();

        // ゲーム中の報酬を記録する配列
        const gameRewards = [];
        // ゲーム中の勾配を記録する配列。ただし勾配データは要素としてでなく、配列のプロパティの値として記録する。
        const gameGradients = [];

        for (let j = 0; j < maxStepsPerGame; ++j) {
            stepInfo.textContent = j + 1 + '回めのステップ';

            const gradients = tf.tidy(() => {
                const inputTensor = cartPoleSystem.getStateTensor();
                const {
                    value, grads
                } = getGradientsAndSaveActions(inputTensor);
                return grads;
            });

            // 配列gameGradientsに、gradientsが持つ重みパラメータを記録する。
            pushGradients(gameGradients, gradients);

            const action = currentActions_[0];
            const isDone = cartPoleSystem.update(action);
            await maybeRenderDuringTraining(cartPoleSystem);

            // 報酬を組み込む
            // ゲームはいずれ終わるので、そのとき、配列の末尾に0が追加される
            if (isDone) {
                // ステップの最大数に達する前にゲームが終わったら、0の報酬が与えられる。
                // [1,1,1,...0]
                gameRewards.push(0);
                break;
            }
            else {
                // ゲームが終わらない限り、各ステップは1の報酬につながる。
                // これらの報酬値は、ゲームがより長くつづく、より高い報酬値につながるように、後で"割引かれ"る。
                // [1,1,1,1,...1]
                gameRewards.push(1);
            }
        } // 内側のforループの終わり

        // 報酬の総和
        gameSteps.push(gameRewards.length);
        // [1,1,1,..0]なので、その長さがそのまま報酬の合計になる。

        // pushGradients()関数を再度使用
        pushGradients(allGradients, gameGradients);
        // 1ゲーム分の全報酬を記録
        allRewards.push(gameRewards);
        await tf.nextFrame();

    } // 外側のforループの終わり
}, false);

ここで使用されているpushGradients(record,gradients)関数は、おそらく内側と外側のforループ両方で同じ関数を使って処理することを目的として、配列に要素を追加するという本来の機能を使わず、配列にプロパティを付加してそれに値を割り当てるという、かなりテクニカルな手法が取られています。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

CAPTCHA