- Authors

- Name
- Youngju Kim
- @fjvbn20031
概要
ウェブブラウザを操作して情報を探し、フォームを記入し、ボタンをクリックする作業は人間には自然ですが、機械にとっては非常に困難です。ウェブページは動的で多様なレイアウトを持ち、同じ作業でもサイトごとに異なる方法で実行する必要があります。
ウェブナビゲーションに強化学習を適用すると、エージェントがウェブページの視覚情報やDOM構造を観察して適切な行動(クリック、タイピングなど)を学習できます。
ウェブナビゲーションの課題
なぜ難しいのか
ウェブ環境は従来のRL環境と大きく異なります:
- 巨大な状態空間:ウェブページのレンダリング結果(ピクセル)は数百万次元
- 巨大な行動空間:マウス位置(x, y)+ クリック/ドラッグ + キーボード入力の組み合わせ
- 遅延報酬:複数回のクリックと入力の後にようやくタスク完了を確認できる
- 部分観測性:スクロールしないと見えない要素、ポップアップ、動的ロードなど
- 環境の非決定性:同じページでもロード時間によって異なる状態を見せる
状態表現方式
| 方式 | 説明 | 長所 | 短所 |
|---|---|---|---|
| ピクセルベース | スクリーンショットを直接入力に使用 | 汎用的、視覚情報を含む | 高次元、学習が遅い |
| DOMベース | HTML DOMツリーをパースして使用 | 構造情報が豊富 | サイトごとに構造が異なる |
| ハイブリッド | ピクセル + DOM情報を結合 | 最も豊富な情報 | 複雑な前処理が必要 |
ブラウザ自動化と強化学習
環境インターフェース
ウェブブラウザをGym互換環境としてラップします:
import numpy as np
class WebEnvironment:
"""Gym互換ウェブブラウザ環境"""
def __init__(self, task_config, screen_width=160, screen_height=210):
self.screen_width = screen_width
self.screen_height = screen_height
self.task = task_config
# 行動空間:グリッドクリック + キーボード入力
self.grid_size = 16 # 16x16グリッド
self.num_click_actions = self.grid_size * self.grid_size
self.num_type_actions = 128 # ASCII文字
self.total_actions = self.num_click_actions + self.num_type_actions
def reset(self):
"""新しいエピソード開始:ウェブページのロード"""
self._load_page(self.task['url'])
screenshot = self._get_screenshot()
return self._preprocess(screenshot)
def step(self, action):
"""行動実行と結果の返却"""
if action < self.num_click_actions:
# クリック行動:グリッド座標に変換
row = action // self.grid_size
col = action % self.grid_size
x = col * (self.screen_width // self.grid_size)
y = row * (self.screen_height // self.grid_size)
self._click(x, y)
else:
# タイピング行動
char_idx = action - self.num_click_actions
self._type_char(chr(char_idx))
screenshot = self._get_screenshot()
obs = self._preprocess(screenshot)
reward = self._compute_reward()
done = self._check_done()
return obs, reward, done, {}
def _preprocess(self, screenshot):
"""スクリーンショットをモデル入力に変換"""
# リサイズと正規化
resized = np.array(screenshot.resize((self.screen_width,
self.screen_height)))
return resized.astype(np.float32) / 255.0
Mini World of Bitsベンチマーク
OpenAIと研究者が開発したMini World of Bits(MiniWoB)は、ウェブナビゲーションRLの標準ベンチマークです。シンプルなHTMLウィジェットで特定のタスクを実行することが目標です。
タスク例
- click-button:特定テキストが書かれたボタンをクリック
- click-checkboxes:指定されたチェックボックスを選択
- enter-text:テキストフィールドに指定された文字列を入力
- navigate-tree:ツリー構造メニューを探索して特定項目を選択
- email-inbox:メールリストから特定条件のメールを見つけてタスクを実行
各タスクは160x210ピクセルの小型ウェブページで、エージェントは10秒以内に完了する必要があります。
class MiniWoBTask:
"""MiniWoBタスク定義"""
def __init__(self, task_name):
self.task_name = task_name
self.time_limit = 10.0 # 10秒
self.reward_range = (-1.0, 1.0)
def get_reward(self, page_state, time_elapsed):
"""タスク完了に応じた報酬"""
if self._is_task_complete(page_state):
# 速いほど高い報酬
time_bonus = 1.0 - (time_elapsed / self.time_limit)
return max(time_bonus, 0.1)
elif time_elapsed >= self.time_limit:
return -1.0 # タイムアウトペナルティ
else:
return 0.0 # 進行中
OpenAI Universe
OpenAI Universeは、さまざまな環境(ゲーム、ウェブブラウザなど)を標準インターフェースで提供するプラットフォームでした。VNC(Virtual Network Computing)を通じてブラウザ画面を観察し、マウス/キーボードイベントを送信します。
VNCベース環境の特徴
- 実際のブラウザ(Chrome、Firefox)をDockerコンテナで実行
- エージェントはVNCプロトコルで画面ピクセルを受信
- 行動はマウス座標とキーボードイベントで送信
- ネットワーク遅延とレンダリング遅延が存在
class VNCActionSpace:
"""VNCベースの行動空間"""
def __init__(self, screen_width, screen_height):
self.screen_width = screen_width
self.screen_height = screen_height
def click(self, x, y):
"""マウスクリックイベント生成"""
return {
'type': 'pointer',
'x': int(x),
'y': int(y),
'button': 1, # 左クリック
}
def type_text(self, text):
"""キーボード入力イベント生成"""
events = []
for char in text:
events.append({
'type': 'key',
'key': char,
'action': 'press',
})
return events
def scroll(self, x, y, direction='down'):
"""スクロールイベント生成"""
delta = -3 if direction == 'down' else 3
return {
'type': 'scroll',
'x': int(x),
'y': int(y),
'delta': delta,
}
シンプルクリックアプローチ
最も基本的なウェブナビゲーションエージェントは、画面をグリッドに分割し、どのセルをクリックするかを決定する分類問題に変換します。
グリッド行動空間
class GridActionSpace:
"""画面をNxNグリッドに分割してクリック位置を決定"""
def __init__(self, screen_w, screen_h, grid_n=16):
self.screen_w = screen_w
self.screen_h = screen_h
self.grid_n = grid_n
self.cell_w = screen_w / grid_n
self.cell_h = screen_h / grid_n
self.n_actions = grid_n * grid_n
def action_to_coordinate(self, action_idx):
"""行動インデックスを画面座標に変換"""
row = action_idx // self.grid_n
col = action_idx % self.grid_n
# セル中央座標
x = (col + 0.5) * self.cell_w
y = (row + 0.5) * self.cell_h
return int(x), int(y)
def coordinate_to_action(self, x, y):
"""画面座標を行動インデックスに変換"""
col = min(int(x / self.cell_w), self.grid_n - 1)
row = min(int(y / self.cell_h), self.grid_n - 1)
return row * self.grid_n + col
CNNベースモデル
スクリーンショットを入力として受け取り、グリッドセルのクリック確率を出力するモデル:
import torch
import torch.nn as nn
class WebNavigationModel(nn.Module):
"""CNNベースのウェブナビゲーションエージェント"""
def __init__(self, grid_size=16):
super().__init__()
self.grid_size = grid_size
n_actions = grid_size * grid_size
# CNNで画面特徴を抽出
self.conv = nn.Sequential(
nn.Conv2d(3, 32, 8, stride=4),
nn.ReLU(),
nn.Conv2d(32, 64, 4, stride=2),
nn.ReLU(),
nn.Conv2d(64, 64, 3, stride=1),
nn.ReLU(),
)
# 特徴ベクトルサイズの計算
self._feature_size = self._get_conv_output_size((3, 210, 160))
# Actor: クリック位置の方策
self.policy = nn.Sequential(
nn.Linear(self._feature_size, 512),
nn.ReLU(),
nn.Linear(512, n_actions),
)
# Critic: 状態価値
self.value = nn.Sequential(
nn.Linear(self._feature_size, 512),
nn.ReLU(),
nn.Linear(512, 1),
)
def _get_conv_output_size(self, shape):
with torch.no_grad():
dummy = torch.zeros(1, *shape)
return self.conv(dummy).view(1, -1).shape[1]
def forward(self, screen):
features = self.conv(screen).view(screen.size(0), -1)
policy_logits = self.policy(features)
value = self.value(features)
return policy_logits, value
学習ループ
def train_web_agent(model, env, num_episodes=10000, gamma=0.99):
"""A2Cでウェブナビゲーションエージェントを学習"""
optimizer = torch.optim.Adam(model.parameters(), lr=1e-4)
for episode in range(num_episodes):
state = env.reset()
done = False
episode_reward = 0
log_probs = []
values = []
rewards = []
while not done:
state_t = torch.FloatTensor(state).permute(2, 0, 1).unsqueeze(0)
logits, value = model(state_t)
probs = torch.softmax(logits, dim=-1)
dist = torch.distributions.Categorical(probs)
action = dist.sample()
next_state, reward, done, info = env.step(action.item())
log_probs.append(dist.log_prob(action))
values.append(value.squeeze())
rewards.append(reward)
state = next_state
episode_reward += reward
# A2C更新
returns = compute_returns(rewards, gamma)
returns_t = torch.FloatTensor(returns)
values_t = torch.stack(values)
log_probs_t = torch.stack(log_probs)
advantages = returns_t - values_t.detach()
policy_loss = -(log_probs_t * advantages).mean()
value_loss = (returns_t - values_t).pow(2).mean()
loss = policy_loss + 0.5 * value_loss
optimizer.zero_grad()
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), 40.0)
optimizer.step()
if episode % 100 == 0:
print(f"Episode {episode}: Reward={episode_reward:.2f}")
def compute_returns(rewards, gamma):
returns = []
R = 0
for r in reversed(rewards):
R = r + gamma * R
returns.insert(0, R)
return returns
人間のデモンストレーションを活用した学習
純粋なRLだけではウェブナビゲーションの学習は非常に遅くなる可能性があります。**人間のデモンストレーション(human demonstrations)**を活用すると学習を大幅に加速できます。
行動クローニング(Behavioral Cloning)
まず人間の行動を模倣する教師あり学習を行い、その後RLでファインチューニングします:
def pretrain_with_demonstrations(model, demos, num_epochs=50):
"""人間のデモンストレーションデータで事前学習"""
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
criterion = nn.CrossEntropyLoss()
for epoch in range(num_epochs):
total_loss = 0
correct = 0
total = 0
for screen, action in demos:
screen_t = torch.FloatTensor(screen).permute(2, 0, 1).unsqueeze(0)
action_t = torch.tensor([action], dtype=torch.long)
logits, _ = model(screen_t)
loss = criterion(logits, action_t)
optimizer.zero_grad()
loss.backward()
optimizer.step()
total_loss += loss.item()
predicted = logits.argmax(dim=-1)
correct += (predicted == action_t).sum().item()
total += 1
accuracy = correct / total
print(f"Epoch {epoch}: Loss={total_loss/len(demos):.4f}, "
f"Accuracy={accuracy:.2%}")
return model
デモンストレーション重み付け学習
人間のデモンストレーション経験をreplay bufferに入れて高い優先度を付与し、RL学習中も継続的に参照します:
class DemonstrationReplayBuffer:
"""デモンストレーションデータを含む優先度リプレイバッファ"""
def __init__(self, capacity, demo_ratio=0.25):
self.capacity = capacity
self.demo_ratio = demo_ratio
self.demo_buffer = []
self.agent_buffer = []
def add_demonstration(self, transition):
self.demo_buffer.append(transition)
def add_agent_experience(self, transition):
if len(self.agent_buffer) >= self.capacity:
self.agent_buffer.pop(0)
self.agent_buffer.append(transition)
def sample(self, batch_size):
"""デモンストレーションとエージェント経験を混合してサンプリング"""
n_demo = int(batch_size * self.demo_ratio)
n_agent = batch_size - n_demo
demo_samples = random.sample(
self.demo_buffer,
min(n_demo, len(self.demo_buffer))
)
agent_samples = random.sample(
self.agent_buffer,
min(n_agent, len(self.agent_buffer))
)
return demo_samples + agent_samples
実践的な考慮事項と限界
現在の技術的限界
- MiniWoBベンチマークでも複雑なタスク(email-inbox、social-mediaなど)は人間レベルに達していない
- 実際のウェブサイトの多様性と複雑性への対応が困難
- DOM構造の動的変化に脆弱
最新の研究方向
- 大規模言語モデル(LLM)ベースのウェブエージェント:HTMLをテキストとして入力し行動を生成
- マルチモーダルモデル:画面画像とテキストを同時に理解するエージェント
- 階層的RL:高レベル計画(どの要素と対話するか)と低レベル実行(正確な座標クリック)を分離
要点まとめ
- ウェブナビゲーションは巨大な状態/行動空間と遅延報酬が特徴の難しいRL問題である
- グリッドベースの行動空間で単純化し、CNN + A2Cで基本エージェントを実装できる
- 人間のデモンストレーション(行動クローニング)で事前学習するとRL学習が大幅に加速される
- 最新の研究はLLMとマルチモーダルモデルを活用した汎用ウェブエージェントに向かっている
次の記事では、連続行動空間を扱うDDPGと分布方策勾配を見ていきます。