13_4:行動を組み込む

つづいて、モデルを作成し、行動を組み込んでいきます。ただし通常と異なり、モデルにはオプティマイザーは指定せず、コンパイルもしません。

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;
}

モデルの最初の層に指定している活性化関数のeluは、次のグラフで表されます。

ELU関数には、xが正のときはそのままxを出力し、xが0のときは0を、負のときは、x=0の近くで滑らかに減少し、後は-1ほどの一定の値を出力する、という特徴があります。

モデルには、カートの位置と速度、ポールの角度と角速度のデータが時々刻々、入力として入るので、inputShapeは[4]になります。

またcurrentActions_という名前の変数を、上記buildModel()関数などの外に宣言しておきます。これは、tfjs-examples/cart-poleサンプルでは、index.jsに含まれるPolicyNetworkクラスの、外部からアクセスしないプライベートなプロパティとして扱われている変数で、現在のアクションを保持する変数として使用します。

 // 現在のアクションを保持する変数。
 // tfjs-examples/cart-poleサンプルでは、index.jsに含まれるPolicyNetworkクラスの
 // 外部からアクセスしないプライベートなプロパティとして扱われている。
 let currentActions_;

メインのJavaScriptではbuildModel()関数でモデルを作成し、forループに入ります。まず必要なのは、各ステップでのカートポールの状態なので、内側のforループで、次のコードを追加または変更ます。

for (let j = 0; j < maxStepsPerGame; ++j) {
    ...
    // 次の段階で使用する
    const gradients = tf.tidy(() => {
        const inputTensor = cartPoleSystem.getStateTensor();
        // カートポールの現在のinputTensorから、勾配を得る
        const {
            value, grads
        } = getGradientsAndSaveActions(inputTensor);
        return grads;
    });
    const action = currentActions_[0];
    ...
    
}

ここで追加しているgetGradientsAndSaveActions()関数は、カートポールの現在のinputTensorから勾配を計算して返す新しい関数です。この関数の中では、変数currentActions_に現在の行動の値(Int32Array [0]かInt32Array [1])が入れられるので、前は暫定的に割り当てていた変数actionに、正式な値が代入できます。

getGradientsAndSaveActions()関数はvalueとgradsを返すので、ここではgradsを変数gradientsに割り当てています。 gradientsは以降の段階で使用します。

grads、つまり勾配の中身は、たとえば次のコードで調べることができます。

Object.keys(grads).forEach((varName) => {
    console.log(varName); 
    console.log(grads[varName].dataSync());
});

2つのconsole.log()からは次のような結果が得られます。

dense_Dense1/kernel
Float32Array(16) [-0.03…といった数値を16個含む配列]
dense_Dense1/bias
Float32Array(4) [0.26…といった数値を4個含む配列]
dense_Dense2/kernel
Float32Array(4) [-0.08…といった数値を4個含む配列]
dense_Dense2/bias
Float32Array [-0.53…といった数値を1個含む配列]

getGradientsAndSaveActions()は次の関数です。

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);
}

ここではまず、fという名前の関数を定義しています。f()関数は、getGradientsAndSaveActions()が受け取ったカートポールの状態のデータ(inputTensor)を、また別のgetLogitsAndActions()関数に渡して、そこから確率の数値(logits)と行動の数値(actions)を得ます。そしてactionsの数値を変数currentActions_に保持し、 1からcurrentActions_を引いたものを変数labelsに代入します。currentActions_はInt32Array [0]かInt32Array [1]なので、currentActions_が0のときlabelsは1(1-1)、1のときは0(1-1)になります。これはつまり、正解は今の行動の逆ということです。f()関数は、この正解(labels)と現状の数値(logits)を最小化した結果を返します。

f()関数の定義後、getGradientsAndSaveActions()関数は、tf.variableGrads()関数にf()関数を渡した結果を返します。tf.variableGrads()は、訓練可能な変数について渡された関数fの勾配を計算し、{value: tf.Scalar, grads: {[name: string]: tf.Tensor}}というオブジェクトを返します。

tf.train.Optimizerのminimize()メソッドは、tf.variableGrads()関数とtf.train.OptimizerのapplyGradients()メソッドで、書き換えることができます。applyGradients()は後の段階で呼び出します。

// モデルを訓練する
for (let i = 0; i < 100; i++) {
    // optimizer.minimize()の書き換え
    // f()を実行し、訓練可能な変数について、f()のスカラー出力の勾配を計算する
    const {
        value, grads
    } = tf.variableGrads(
        () => loss(f(xs), ys)
    );
    // 変数を、計算した勾配を使って更新する。
    optimizer.applyGradients(grads);
}

getGradientsAndSaveActions()関数で呼び出しているgetLogitsAndActions()は次の関数です。


const getLogitsAndActions = (inputs) => {
    return tf.tidy(() => {
        // inputs(カートの位置、カートの速度、ポールの角度、ポールの角速度)からモデルの推測を実行
        const logits = model.predict(inputs);
        // モデルの推測結果に非線形のsigmoid関数を適用し、その結果を左方行動の確率とする。
        const leftProb = tf.sigmoid(logits);
        // 1から左方行動の確率を引いて、右方行動の確率とする
        const rightProb = tf.sub(1, leftProb);
        // 左方と右方行動の確率をaxis=1で連結。これにより[[左方の確率], [右方の確率]]という形になる。
        const leftRightProbs = tf.concat([leftProb, rightProb], 1);
        // 多項分布から抽出された値を使ってTensorを作成する。
        const actions = tf.multinomial(leftRightProbs, 1, null, true);
        // actionsはInt32Array [1]かInt32Array [0]になる。
        return [logits, actions];
    });
}

この関数は、カートポールからのカートの位置、カートの速度、ポールの角度、ポールの角速度のデータを受け取り、モデルの予測とカートポールが取る行動を決めて返します。1行めでは受け取った入力(inputs)をモデルのpredict()に渡して予測させていますが、今の段階ではモデルは何も学習していないので、適当な数値が返されるだけです。次の行では、tf.sigmoid()関数を使って、モデルの予測した数値を0と1の間に収めています。

tf.multinomial()は、渡された確率を使って実際にサンプリングを行う関数です。たとえば、さいころを1回投げてどの目が出るかは、次のコードで算出できます。

const res1 = tf.multinomial([1/6, 1/6, 1/6, 1/6, 1/6, 1/6], 1, null, true);

ここでは、(現時点では無理やり計算している)モデルの推測を元に、左方向と右方向の確率を求め、その確率にもとづいた行動を決めている、ということになります。

コメントを残す

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

CAPTCHA