Copyright 2021 TF-AgentsAuthors。
はじめに
![]() | ![]() | ![]() | ![]() |
このチュートリアルは、アクション(腕)が機能(ジャンル、リリース年、リリース年)で表される映画のリストなど、独自の機能を備えているコンテキストバンディットの問題にTF-Agentsライブラリを使用する方法のステップバイステップガイドです。 ...)。
前提条件
読者は、TF-AgentのBanditライブラリにある程度精通していることを前提としています。特に、このチュートリアルを読む前に、TF-AgentのBanditsのチュートリアルに取り組んでいます。
アーム機能を備えた多腕バンディット
「古典的な」コンテキスト多腕バンディット設定では、エージェントはすべてのタイムステップでコンテキストベクトル(別名観測)を受け取り、累積報酬を最大化するために、番号付きアクション(アーム)の有限セットから選択する必要があります。
次に、エージェントがユーザーに次に見る映画を勧めるシナリオを考えてみましょう。決定を下す必要があるたびに、エージェントはコンテキストとしてユーザーに関する情報(視聴履歴、ジャンルの好みなど)と、選択する映画のリストを受け取ります。
ユーザー情報をコンテキストとして使用し、アームをmovie_1, movie_2, ..., movie_K
にすることで、この問題を定式化することができますが、このアプローチには複数の欠点があります。
- アクションの数はシステム内のすべての映画である必要があり、新しい映画を追加するのは面倒です。
- エージェントは、すべての映画のモデルを学習する必要があります。
- 映画間の類似性は考慮されていません。
映画に番号を付ける代わりに、より直感的なことを行うことができます。ジャンル、長さ、キャスト、評価、年などの一連の機能で映画を表すことができます。このアプローチの利点は多岐にわたります。
- 映画全体の一般化。
- エージェントは、ユーザーと映画の機能で報酬をモデル化する1つの報酬関数のみを学習します。
- システムから簡単に削除したり、新しい映画をシステムに導入したりできます。
この新しい設定では、アクションの数はすべてのタイムステップで同じである必要はありません。
TFエージェントの腕ごとの盗賊
TF-Agents Banditスイートは、アームごとのケースにも使用できるように開発されています。アームごとの環境があり、ほとんどのポリシーとエージェントはアームごとのモードで動作できます。
例のコーディングに飛び込む前に、必要なインポートが必要です。
インストール
pip install -q tf-agents
輸入
import functools
import matplotlib.pyplot as plt
import numpy as np
import tensorflow as tf
from tf_agents.bandits.agents import lin_ucb_agent
from tf_agents.bandits.environments import stationary_stochastic_per_arm_py_environment as p_a_env
from tf_agents.bandits.metrics import tf_metrics as tf_bandit_metrics
from tf_agents.drivers import dynamic_step_driver
from tf_agents.environments import tf_py_environment
from tf_agents.replay_buffers import tf_uniform_replay_buffer
from tf_agents.specs import tensor_spec
from tf_agents.trajectories import time_step as ts
# Clear any leftover state from previous colabs run.
# (This is not necessary for normal programs.)
tf.compat.v1.reset_default_graph()
tf.compat.v1.enable_resource_variables()
tf.compat.v1.enable_v2_behavior()
nest = tf.compat.v2.nest
パラメータ-気軽に遊んでください
# The dimension of the global features.
GLOBAL_DIM = 40
# The elements of the global feature will be integers in [-GLOBAL_BOUND, GLOBAL_BOUND).
GLOBAL_BOUND = 10
# The dimension of the per-arm features.
PER_ARM_DIM = 50
# The elements of the PER-ARM feature will be integers in [-PER_ARM_BOUND, PER_ARM_BOUND).
PER_ARM_BOUND = 6
# The variance of the Gaussian distribution that generates the rewards.
VARIANCE = 100.0
# The elements of the linear reward parameter will be integers in [-PARAM_BOUND, PARAM_BOUND).
PARAM_BOUND = 10
NUM_ACTIONS = 70
BATCH_SIZE = 20
# Parameter for linear reward function acting on the
# concatenation of global and per-arm features.
reward_param = list(np.random.randint(
-PARAM_BOUND, PARAM_BOUND, [GLOBAL_DIM + PER_ARM_DIM]))
シンプルなアームごとの環境
他のチュートリアルで説明されている定常確率的環境には、アームごとの対応物があります。
アームごとの環境を初期化するには、生成する関数を定義する必要があります
- グローバルおよびアームごとの特徴:これらの関数には入力パラメーターがなく、呼び出されると単一の(グローバルまたはアームごとの)特徴ベクトルが生成されます。
- 報酬:この関数は、パラメーターとしてグローバルおよびアームごとの特徴ベクトルの連結を取り、報酬を生成します。基本的に、これはエージェントが「推測」しなければならない機能です。ここで注目に値するのは、アームごとの場合、報酬関数はすべてのアームで同じです。これは、エージェントが各アームの報酬関数を個別に推定する必要がある従来の盗賊の場合との根本的な違いです。
def global_context_sampling_fn():
"""This function generates a single global observation vector."""
return np.random.randint(
-GLOBAL_BOUND, GLOBAL_BOUND, [GLOBAL_DIM]).astype(np.float32)
def per_arm_context_sampling_fn():
""""This function generates a single per-arm observation vector."""
return np.random.randint(
-PER_ARM_BOUND, PER_ARM_BOUND, [PER_ARM_DIM]).astype(np.float32)
def linear_normal_reward_fn(x):
"""This function generates a reward from the concatenated global and per-arm observations."""
mu = np.dot(x, reward_param)
return np.random.normal(mu, VARIANCE)
これで、環境を初期化する準備が整いました。
per_arm_py_env = p_a_env.StationaryStochasticPerArmPyEnvironment(
global_context_sampling_fn,
per_arm_context_sampling_fn,
NUM_ACTIONS,
linear_normal_reward_fn,
batch_size=BATCH_SIZE
)
per_arm_tf_env = tf_py_environment.TFPyEnvironment(per_arm_py_env)
以下では、この環境が何を生成するかを確認できます。
print('observation spec: ', per_arm_tf_env.observation_spec())
print('\nAn observation: ', per_arm_tf_env.reset().observation)
action = tf.zeros(BATCH_SIZE, dtype=tf.int32)
time_step = per_arm_tf_env.step(action)
print('\nRewards after taking an action: ', time_step.reward)
observation spec: {'global': TensorSpec(shape=(40,), dtype=tf.float32, name=None), 'per_arm': TensorSpec(shape=(70, 50), dtype=tf.float32, name=None)} An observation: {'global': <tf.Tensor: shape=(20, 40), dtype=float32, numpy= array([[-10., 8., 4., -5., 4., 8., -4., -10., 5., 8., 2., -6., 5., -6., -8., -7., 4., -4., 9., 4., 7., -7., -3., -2., -1., 9., -7., 7., 5., -5., 6., 4., 6., 4., 5., 9., -8., -1., -4., -2.], [ -3., -4., 1., 9., 7., 5., 8., 5., 6., -8., 4., 3., -3., 0., -7., -3., -4., -2., -9., -2., -9., 7., -1., -5., 7., 9., 0., 0., -3., 0., -8., -5., -10., 7., 8., 8., 6., 1., 0., -6.], [ 0., -1., -8., 6., 3., -4., 4., 2., 4., -2., 9., 9., 3., 0., -8., -8., 5., -6., 9., 8., -1., 7., 0., 3., -5., 3., 9., -2., 2., 3., -9., -2., 1., 5., 5., -7., 4., -6., -6., 2.], [ -6., -3., -5., 6., 5., -5., 5., -9., 9., 5., -7., -10., 3., -3., -9., 3., 4., 7., -5., 1., -9., 2., -4., 0., -6., 4., -7., 6., 8., 6., -8., -4., 8., -6., 5., 2., 6., 7., 5., 4.], [ 3., -6., 9., 1., -8., 3., -10., 0., -10., -6., 5., -5., -1., -5., -7., 9., 6., 9., -2., 8., 5., 0., 8., 7., -4., 5., -10., 8., -6., 9., 4., -9., -9., -1., 1., 6., 6., -5., -1., -3.], [ -4., 6., 3., -7., 5., -3., 2., 6., 4., -1., 5., 8., -3., 4., 5., 0., -2., 2., -4., 1., -2., 8., -2., -9., -10., -10., -8., 5., 7., -8., 6., 3., -9., 6., -1., -2., 6., 4., 7., 5.], [ -6., -9., 0., -3., 3., -8., 3., -1., -1., -9., 0., -7., -10., 2., -2., -10., 9., -2., 0., -6., -7., 4., -5., 2., 2., 3., 4., 4., -3., 7., 9., -6., 7., -8., 8., 9., -8., 9., -8., -10.], [ 9., -1., 1., -10., 8., 8., -6., -6., 3., 4., -6., -7., -6., -1., 8., -10., 5., 1., -2., 1., 5., -3., 2., 0., 6., -1., -4., 5., 9., 9., -3., -5., 8., -4., -1., -5., -4., 4., 5., 7.], [ -7., -9., -9., 2., -3., 6., -3., -10., -7., 5., -9., -7., 7., 9., -7., -5., 9., 9., -2., 1., 6., -3., -4., -1., 5., 9., -5., 8., 4., 8., -5., -8., -7., 9., 4., -10., 9., 7., -7., 5.], [ -6., -5., 3., 8., -1., 9., 5., 9., -4., -7., -5., -6., -3., 2., -6., 6., -9., -7., 5., 2., 5., 8., -2., 6., 5., -1., 3., 9., 2., -10., 5., 9., 2., -4., -1., 7., 3., 7., -3., 1.], [ 0., 1., -4., 4., -8., 4., -4., 0., -1., -7., 4., -8., -9., 1., 5., -5., -9., 8., 8., 0., -2., -8., -4., 3., 5., 4., -10., 4., 4., -9., 8., 4., 1., -7., 3., 7., -6., 0., -1., -7.], [ 7., -7., 1., 3., -8., 8., -10., -5., -4., -1., -9., -2., -10., -10., 2., -9., -10., 2., 6., 4., 8., -8., -7., 6., -7., 2., 4., 7., -6., -1., 5., 5., -8., 4., -8., -4., -3., 9., 7., -1.], [ 4., -8., -10., 3., 2., -2., -2., -5., -10., -10., -5., 8., 3., 3., -8., -7., 5., -10., 6., -10., 3., 5., 6., -7., -10., 2., -2., -3., 7., 8., -4., -7., 6., -7., 6., -5., -6., -9., -1., 0.], [ -5., -3., 1., -2., 3., -7., 4., -3., 0., 9., -10., 3., -6., 5., -4., -10., 8., -2., 5., 7., 3., 3., -3., -3., -5., 1., 5., -10., -4., 5., -6., 4., 9., -4., -7., -4., 9., 0., 7., -1.], [ 7., -2., -8., -4., -9., 9., -8., -10., 3., -3., 9., -4., -4., 3., -2., -10., 5., -1., -6., 6., 6., 6., -2., -8., 4., 7., -5., -4., 7., 2., 5., -5., -1., -9., 1., -2., 7., -8., 5., -1.], [ 6., -3., 5., -9., -6., 1., -9., 5., -10., -8., -7., 2., -1., 1., -6., -5., -8., 5., -4., 4., -6., 3., -8., 5., -7., -2., 5., 1., 8., -1., -6., 4., -1., -3., 8., -7., 3., -5., -8., 3.], [ 9., 0., -4., -4., 5., 2., 8., -8., -7., -6., 7., 5., -5., 9., 1., 2., 8., -4., -8., 6., -1., 1., -8., 7., 0., 3., 3., 6., 6., 6., 9., -1., -6., -10., 4., 3., 7., -9., -6., -2.], [ 5., -4., 1., 7., 2., -7., 6., -2., 8., 8., 0., 7., 0., 1., -5., 1., 1., 6., 4., 0., -2., -3., 8., 0., -7., -3., 9., -10., -6., -2., -1., 2., 6., -6., 8., -1., -5., -6., 3., -8.], [ -2., -10., 0., 5., 8., 6., 9., -10., 4., 9., -9., 5., -10., -7., -1., -9., -1., 2., -8., 3., 4., -6., -3., 0., 3., 3., -8., -6., 4., -5., -3., 3., -4., 0., -3., -5., -4., 6., 7., 6.], [ -6., -2., 6., -9., 4., 9., 2., -8., -2., 5., 2., 4., -1., 7., -5., -5., 6., -5., 6., -7., 8., 4., 2., 3., 2., 5., -4., 2., 4., 0., 6., 8., -4., -6., -4., -9., -10., -1., 3., -1.]], dtype=float32)>, 'per_arm': <tf.Tensor: shape=(20, 70, 50), dtype=float32, numpy= array([[[ 1., -2., 5., ..., 4., -6., 1.], [-1., -2., 5., ..., 4., 2., -5.], [-3., -3., -6., ..., 5., -1., -4.], ..., [-2., 5., 0., ..., 0., 5., 0.], [ 2., -5., 5., ..., 5., 1., -6.], [ 2., -5., 0., ..., 4., -5., -5.]], [[-2., -5., -5., ..., 2., -4., 4.], [ 0., -2., 2., ..., -5., -2., -1.], [-2., -6., -5., ..., 2., 3., 4.], ..., [-1., -5., -4., ..., 1., -1., 4.], [-3., -1., 0., ..., 2., -4., 3.], [-4., -3., 3., ..., 4., 4., 5.]], [[ 2., 5., 1., ..., -2., 4., 5.], [ 4., -3., 3., ..., -5., 0., 3.], [ 1., -4., 1., ..., -1., 2., -6.], ..., [-5., -4., 4., ..., 3., -5., -4.], [-1., 4., 1., ..., 2., -4., 1.], [-2., -6., -5., ..., 5., 2., -5.]], ..., [[ 5., 5., -2., ..., 0., -4., 0.], [ 5., 2., -3., ..., 5., 4., 0.], [ 5., 1., -1., ..., -5., 1., -4.], ..., [ 0., -6., 5., ..., 1., 3., 2.], [-1., 0., -3., ..., 1., 2., -6.], [ 1., 5., 4., ..., 0., -6., -3.]], [[ 4., -2., 4., ..., -1., -4., 4.], [-2., 0., 2., ..., 4., 0., -2.], [ 4., 1., 0., ..., -4., 2., 2.], ..., [-1., 5., -6., ..., 1., -1., -4.], [-3., -4., -6., ..., 1., -2., -3.], [ 0., 5., 4., ..., 3., 0., -4.]], [[ 3., 4., -5., ..., 1., -2., -6.], [-4., -3., 5., ..., 3., -6., 4.], [-2., 2., -1., ..., -2., -4., -6.], ..., [-1., -4., 0., ..., -6., -1., 1.], [-4., 5., 2., ..., 2., 4., -5.], [ 0., -1., 4., ..., 2., -6., -2.]]], dtype=float32)>} Rewards after taking an action: tf.Tensor( [-252.3932 -441.83798 -256.43826 300.39215 -223.00279 -256.83987 70.24384 45.34822 -430.71158 386.66544 143.3806 -199.32378 293.33572 29.244879 48.652817 -33.93667 -224.3618 63.739243 -311.28802 -361.41095 ], shape=(20,), dtype=float32)
観測仕様は、次の2つの要素を持つ辞書であることがわかります。
- キーが
'global'
1つ:これはグローバルコンテキスト部分であり、形状はパラメーターGLOBAL_DIM
一致します。 - キー
'per_arm'
:これはアームごとのコンテキストであり、その形状は[NUM_ACTIONS, PER_ARM_DIM]
です。この部分は、タイムステップ内のすべてのアームのアーム機能のプレースホルダーです。
LinUCBエージェント
LinUCBエージェントは、同じ名前のBanditアルゴリズムを実装します。このアルゴリズムは、線形報酬関数のパラメーターを推定すると同時に、推定値の周囲の信頼楕円体を維持します。エージェントは、パラメーターが信頼楕円体内にあると仮定して、予想される報酬が最も高いアームを選択します。
エージェントを作成するには、観察とアクションの仕様に関する知識が必要です。エージェントを定義するときに、ブールパラメータaccepts_per_arm_features
をTrue
設定しTrue
。
observation_spec = per_arm_tf_env.observation_spec()
time_step_spec = ts.time_step_spec(observation_spec)
action_spec = tensor_spec.BoundedTensorSpec(
dtype=tf.int32, shape=(), minimum=0, maximum=NUM_ACTIONS - 1)
agent = lin_ucb_agent.LinearUCBAgent(time_step_spec=time_step_spec,
action_spec=action_spec,
accepts_per_arm_features=True)
トレーニングデータの流れ
このセクションでは、アームごとの機能がポリシーからトレーニングにどのように移行するかについての仕組みを簡単に説明します。次のセクション(後悔指標の定義)にジャンプして、興味があれば後でここに戻ってください。
まず、エージェントのデータ仕様を見てみましょう。エージェントのtraining_data_spec
属性は、トレーニングデータに必要な要素と構造を指定します。
print('training data spec: ', agent.training_data_spec)
training data spec: Trajectory(step_type=TensorSpec(shape=(), dtype=tf.int32, name='step_type'), observation=DictWrapper({'global': TensorSpec(shape=(40,), dtype=tf.float32, name=None)}), action=BoundedTensorSpec(shape=(), dtype=tf.int32, name=None, minimum=array(0, dtype=int32), maximum=array(69, dtype=int32)), policy_info=PerArmPolicyInfo(log_probability=(), predicted_rewards_mean=(), predicted_rewards_optimistic=(), predicted_rewards_sampled=(), bandit_policy_type=(), chosen_arm_features=TensorSpec(shape=(50,), dtype=tf.float32, name=None)), next_step_type=TensorSpec(shape=(), dtype=tf.int32, name='step_type'), reward=TensorSpec(shape=(), dtype=tf.float32, name='reward'), discount=BoundedTensorSpec(shape=(), dtype=tf.float32, name='discount', minimum=array(0., dtype=float32), maximum=array(1., dtype=float32)))
スペックのobservation
部分を詳しく見ると、アームごとの機能が含まれていないことがわかります。
print('observation spec in training: ', agent.training_data_spec.observation)
observation spec in training: {'global': TensorSpec(shape=(40,), dtype=tf.float32, name=None)}
アームごとの機能はどうなりましたか?この質問に答えるために、最初に、LinUCBエージェントがトレーニングするとき、すべてのアームのアームごとの機能は必要なく、選択したアームの機能のみが必要であることに注意してください。したがって、特にアクションの数が多い場合は非常に無駄になるため、形状[BATCH_SIZE, NUM_ACTIONS, PER_ARM_DIM]
のテンソルを削除することは理にかなっています。
しかし、それでも、選択したアームのアームごとの機能はどこかにあるはずです!この目的のために、LinUCBポリシーが、選択したアームの機能をトレーニングデータのpolicy_info
フィールド内に格納していることを確認します。
print('chosen arm features: ', agent.training_data_spec.policy_info.chosen_arm_features)
chosen arm features: TensorSpec(shape=(50,), dtype=tf.float32, name=None)
形状から、 chosen_arm_features
フィールドには1つの腕の特徴ベクトルしかなく、それが選択された腕になることがchosen_arm_features
ます。 policy_info
とchosen_arm_features
は、トレーニングデータの仕様を調べたところからchosen_arm_features
に、トレーニングデータの一部であるため、トレーニング時に利用できることに注意してください。
後悔指標の定義
トレーニングループを開始する前に、エージェントの後悔を計算するのに役立ついくつかの効用関数を定義します。これらの関数は、一連のアクション(アームの機能によって与えられる)とエージェントから隠されている線形パラメーターを指定して、最適な期待報酬を決定するのに役立ちます。
def _all_rewards(observation, hidden_param):
"""Outputs rewards for all actions, given an observation."""
hidden_param = tf.cast(hidden_param, dtype=tf.float32)
global_obs = observation['global']
per_arm_obs = observation['per_arm']
num_actions = tf.shape(per_arm_obs)[1]
tiled_global = tf.tile(
tf.expand_dims(global_obs, axis=1), [1, num_actions, 1])
concatenated = tf.concat([tiled_global, per_arm_obs], axis=-1)
rewards = tf.linalg.matvec(concatenated, hidden_param)
return rewards
def optimal_reward(observation):
"""Outputs the maximum expected reward for every element in the batch."""
return tf.reduce_max(_all_rewards(observation, reward_param), axis=1)
regret_metric = tf_bandit_metrics.RegretMetric(optimal_reward)
これで、盗賊のトレーニングループを開始する準備が整いました。以下のドライバーは、ポリシーを使用したアクションの選択、選択したアクションの報酬のリプレイバッファーへの保存、事前定義された後悔メトリックの計算、およびエージェントのトレーニングステップの実行を処理します。
num_iterations = 20 # @param
steps_per_loop = 1 # @param
replay_buffer = tf_uniform_replay_buffer.TFUniformReplayBuffer(
data_spec=agent.policy.trajectory_spec,
batch_size=BATCH_SIZE,
max_length=steps_per_loop)
observers = [replay_buffer.add_batch, regret_metric]
driver = dynamic_step_driver.DynamicStepDriver(
env=per_arm_tf_env,
policy=agent.collect_policy,
num_steps=steps_per_loop * BATCH_SIZE,
observers=observers)
regret_values = []
for _ in range(num_iterations):
driver.run()
loss_info = agent.train(replay_buffer.gather_all())
replay_buffer.clear()
regret_values.append(regret_metric.result())
WARNING:tensorflow:From /tmpfs/src/tf_docs_env/lib/python3.6/site-packages/tf_agents/drivers/dynamic_step_driver.py:203: calling while_loop_v2 (from tensorflow.python.ops.control_flow_ops) with back_prop=False is deprecated and will be removed in a future version. Instructions for updating: back_prop=False is deprecated. Consider using tf.stop_gradient instead. Instead of: results = tf.while_loop(c, b, vars, back_prop=False) Use: results = tf.nest.map_structure(tf.stop_gradient, tf.while_loop(c, b, vars)) WARNING:tensorflow:From <ipython-input-1-3fff329da73f>:21: ReplayBuffer.gather_all (from tf_agents.replay_buffers.replay_buffer) is deprecated and will be removed in a future version. Instructions for updating: Use `as_dataset(..., single_deterministic_pass=True)` instead.
結果を見てみましょう。すべてが正しく行われた場合、エージェントは線形報酬関数を適切に推定できるため、ポリシーは、期待される報酬が最適な報酬に近いアクションを選択できます。これは、上で定義した後悔メトリックによって示されます。このメトリックは低下し、ゼロに近づきます。
plt.plot(regret_values)
plt.title('Regret of LinUCB on the Linear per-arm environment')
plt.xlabel('Number of Iterations')
_ = plt.ylabel('Average Regret')
次は何ですか?
上記の例は、 Neural epsilon-Greedyエージェントなど、他のエージェントからも選択できるコードベースに実装されています。