得られた勾配は、ゲームの全ステップ(内側の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ループ両方で同じ関数を使って処理することを目的として、配列に要素を追加するという本来の機能を使わず、配列にプロパティを付加してそれに値を割り当てるという、かなりテクニカルな手法が取られています。