フィジカルAIとは?Pythonで学ぶ仕組みとエッジ・クラウド連携の実装入門

catch-img

「生成AIの次はフィジカルAI」と言われるようになり、ロボットや自動運転、工場の自動化に関わる方にとって避けて通れないキーワードになりました。一方で、「言葉は聞くけれど、生成AIと何が違うのか」「実際にどうやって開発するのか」がつかみにくいと感じている方も多いのではないでしょうか。

この記事では、

・フィジカルAIの定義とエンボディドAIとの関係
・センサーからアクチュエータまでの仕組み
・VLM・VLA・世界モデル・強化学習

などの主要技術をわかりやすく解説します。

SEO監修者|田村 響
SEO監修者|田村 響
SEOマーケター/コンサルタント。 BtoB領域を中心に、フリーランスとして独立後は、幅広いコンテンツ制作に携わる。また、ライターとして活動した後、メディア責任者やコンサルタントとして、40以上のメディアを担当。CV11倍の向上などの実績を持ち、読者の検索意図を踏まえた情報設計と、問い合わせ・資料請求につながる導線づくりを重視している。

NVIDIA H200を搭載したGPUが、業界最安値で使える!

最先端のNVIDIA H200を、業界最安値クラスでご提供。学習・推論のスピードはそのままに、GPUコストを大幅圧縮。生成AI・LLM学習・シミュレーションなど大規模計算に最適。オンプレミスや他社クラウドと比べても圧倒的に低コストで、最新GPUを手軽に利用できます。

フィジカルAIとは

フィジカルAI(Physical AI)とは、カメラやセンサーで現実世界を認識し、AIが状況を理解・判断して、ロボットや機械の「動き」として実行するAIのことです。

NVIDIAはフィジカルAIを「カメラ、ロボット、自動運転車などの自律システムが、物理世界において認識、理解、推論し、複雑な行動の実行や調整ができるようにするもの」と説明しています(出典:NVIDIA「フィジカルAIとは?」)。

ChatGPTに代表される生成AIは、テキストや画像といった「デジタルな出力」を生み出します。これに対してフィジカルAIの出力は、アームの関節角度や車輪の回転速度といった「物理的な動き」です。

フィジカルAIとエンボディドAIの関係

エンボディドAIとは、「知能は身体と環境との相互作用の中で形づくられる」という考え方に基づき、身体を持つエージェントが環境と関わりながら学習・行動するAIを指す研究領域です。

観点

エンボディドAI

フィジカルAI

主な文脈

学術研究・ロボティクス・認知科学

産業・ビジネス

重視する点

身体を通じた学習と、知能の成り立ち

物理世界での認識・判断・行動の実装と社会実装

対象範囲

身体を持つエージェント

ロボット、自動運転車、産業機械、スマート空間のカメラ、開発基盤

実務上は、両者を厳密に区別する必要はほとんどありません。

フィジカルAIとエンボディドAIは重なる領域が大きいものの、明確な包含関係が定まっているわけではありません。

AI駆動開発の実践方法に関しては、こちらの記事で解説しています。

フィジカルAIの仕組み

フィジカルAIは、

①センサーで周囲を計測
②AIが環境と目的を理解
③行動計画を作る
④アクチュエータを制御する

という4つのステップを繰り返して動作します。ポイントは、この流れが一方通行ではなく「閉ループ」になっていることです。動いた結果は再びセンサーで計測され、狙いとのずれがあれば次の計画や制御で修正されます。

たとえば、倉庫で「棚から赤い箱を取り出して台車に載せる」ロボットであれば、4つのステップは次のように対応します。

ステップ

役割

倉庫ロボットの例

主な技術

①計測

周囲と自分の状態をデータにする

カメラで棚を撮影し、関節の角度センサーで腕の姿勢を取得

カメラ、LiDAR、IMU、センサーフュージョン

②理解

何がどこにあり、何をすべきかを把握する

赤い箱の位置・向きを推定し、指示の意味を解釈

物体認識、状態推定、VLM

③計画

目的を達成するための動きを決める

箱までのアームの軌道と、つかむ位置を決定

タスク計画、動作計画、VLA

④制御

計画どおりに体を動かす

モーターのトルクを調整し、箱をつかんで運ぶ

PID制御、モデル予測制御、強化学習

このループは、用途によって1秒間に数回から数千回という速さで回ります。

AIの作り方をエンジニア向け解説した記事はこちらです。

フィジカルAIを構成する主要技術

前章の「計測→理解→計画→制御」のループは、いくつもの技術の組み合わせで支えられています。ここでは、フィジカルAIを理解・開発するうえで特に重要な5つの技術を取り上げます。

  • センサーフュージョン

  • VLM・VLA・世界モデル

  • 強化学習

  • 模倣学習

  • 物理シミュレーション

POINT

01

センサーフュージョン

センサーフュージョンとは、複数のセンサーから得られた情報を統合し、単一のセンサーよりも正確で信頼できる状態推定を行う技術です。

「センサーで周囲を計測する」で見たとおり、どのセンサーにも得意・不得意があります。カメラは暗所に弱く、LiDARは雨や霧に弱く、IMUは時間とともに誤差が蓄積します。これらを組み合わせて弱点を補い合うことで、「いつでも・どこでも」安定して周囲を把握できるようになります。

代表例が自動運転です。Waymoの第6世代自動運転システムは、13台のカメラ、4台のLiDAR、6台のレーダーを搭載し、車両の全周を重なり合う視野でカバーしています。

POINT

02

VLM・VLA・世界モデル

VLM・VLA・世界モデルは、現在のフィジカルAIの「頭脳」を担う中核技術です。名前が似ているため、まずは3つの違いを整理しておきましょう。

モデル

入力

出力

役割のイメージ

VLM

画像+テキスト

テキスト

見て、言葉で説明・推論する

VLA

画像+テキスト+ロボットの状態

ロボットの行動(関節角度・手先位置など)

見て、指示を理解し、体を動かす

世界モデル

現在の状態+行動

未来の状態の予測

「こう動いたら、世界はどう変わるか」を想像する

VLMは、画像とテキストを同時に理解し、言葉で答えるモデルです。フィジカルAIでは、「机の上に何があるか」「指示を達成するにはどの順番で作業すべきか」といった状況の理解やタスク計画を担います。

VLAは、VLMにロボットの行動を出力する仕組みを加えたモデルです。VLAの先駆けとされるGoogle DeepMindの「RT-2」は、Webデータとロボットデータの両方から学習し、ロボットの動きをテキストと同じトークンとして出力します。初めて見る物体や背景を含む状況での成功率を、前世代のRT-1の32%から62%に高めました。

世界モデルは、「ある状態で、ある行動をとったら、次に世界がどう変化するか」を予測するモデルです。Metaは世界モデルに求められる能力として、

  • 観測を理解すること

  • 世界の変化を予測すること

  • 目標を達成する行動の順序を計画すること

の3つを挙げています。

(出典:Meta AI)

世界モデルは主に次の3つの用途で使われます。

  1. 合成データの生成:現実では集めにくい状況の映像を大量に作り出す
  2. 行動の計画:頭の中でシミュレーションするように、複数の行動案の結果を予測して最善の案を選ぶ
  3. 方策の評価:学習したAIを実機で試す前に、仮想的に動かして性能を確かめる

NVIDIAはCES 2025で世界基盤モデルのプラットフォーム「Cosmos」を発表し、2026年6月には言語・画像・動画・音声・行動を1つのモデルで扱う「Cosmos 3」の技術レポートを公開しています。

POINT

03

強化学習

強化学習とは、AI(エージェント)が環境の中で行動し、その結果として得られる「報酬」を最大化するように、試行錯誤を通じて行動のルール(方策)を学ぶ手法です。

「うまくいったらほめる」を大量に繰り返すことで、人がルールを書き下すのが難しい動作を獲得できます。

要素

意味

倒立振子(後半のサンプル)での例

エージェント

学習して行動を決めるAI

台車を押す制御AI

環境

エージェントが働きかける対象

台車と棒の物理シミュレーション

状態

環境から得られる情報

台車の位置・速度、棒の角度・角速度

行動

エージェントが選ぶ動き

台車を左に押す/右に押す

報酬

行動の良し悪しを表す数値

棒が倒れずに立っている間、1ステップごとに+1

方策

状態から行動を決めるルール

ニューラルネットワーク

二足歩行やバランス制御のように、物理法則が複雑に絡み合う動作は、人が制御ルールを書き切るのが困難です。

強化学習なら、目標(報酬)を与えるだけで、AI自身が試行錯誤から動かし方を見つけ出します。

POINT

04

模倣学習

模倣学習とは、人が操作したお手本(デモンストレーション)のデータから、ロボットに同じ動作を学ばせる手法です。

強化学習が「自力で試行錯誤して覚える」方法だとすれば、模倣学習は「見て真似して覚える」方法だといえます。報酬を設計する必要がなく、比較的少ないデータで実用的な動作を獲得しやすいため、ロボットアームによる物体操作の分野で急速に普及しています。

デモデータの主な集め方は次のとおりです。

  • テレオペレーション:操作者が「リーダーアーム」を動かし、同じ形の「フォロワーアーム」に動きを再現させながら記録する
  • VR機器による遠隔操作:VRゴーグルとコントローラーで、ロボットの視点から操作する
  • 人の作業動画:人が作業する様子を撮影し、手の動きなどを学習に活用する

模倣学習の基本形である「行動クローニング」には、学習時に見たことのない状態に入ると誤差が雪だるま式に拡大する「分布シフト」という弱点があります。

POINT

05

物理シミュレーション

物理シミュレーションとは、重力・摩擦・衝突といった物理法則をコンピューター上で再現し、その中でロボットを動かす技術です。

シミュレーションの主なメリットは次のとおりです。

  • 安全:転倒や衝突を何度繰り返しても、機材の破損やけがが起きない
  • 高速・並列:GPUを使って数千台のロボットを同時に動かし、現実の何倍もの速さで経験を積める
  • データ生成:正解ラベル付きの画像や動作データを自動で大量に作れる
  • 再現性:同じ条件を何度でも再現でき、改良の効果を比べやすい

AI開発に適したプログラミング言語はこちらの記事を参照してみてください。

PythonでフィジカルAIの基本ループを実装する方法


※本セクションのサンプルコードは学習・検証用であり、実機ロボットの安全制御や本番システムへそのまま適用するものではありません。

ここからは、フィジカルAIの「計測→理解→計画→制御」のループを、Pythonで実際に動かしてみます。実機のロボットは使わず、手元のPC(GPU不要)だけで再現できる構成にしました。

センサー入力と行動を扱うコードを書く

最初に、「センサーで計測し、判断して、行動する」ループの骨格を作ります。

Gymnasiumの倒立振子環境は、本来ノイズのない理想的な値を返しますが、実機のセンサーには必ず誤差があります。そこで、観測値に正規分布のノイズを加える「NoisySensor」ラッパーを作り、実機に近い条件で試せるようにしました。

また比較対象として、「棒が右に傾いていたら右に押す」という、人が書いた単純なルールベースの制御を用意します。

python

"""sense_act.py : センサー入力 → 判断 → 行動 の基本ループ"""
import gymnasium as gym
import numpy as np

SENSOR_NAMES = ["台車の位置", "台車の速度", "棒の角度", "棒の角速度"]
class NoisySensor(gym.ObservationWrapper):

"""実機センサーの計測ノイズを模擬するラッパー"""
def init(self, env, noise_std=0.01, seed=None):
super().__init__(env)
self.noise_std = noise_std
self.rng = np.random.default_rng(seed)

def observation(self, obs):
noise = self.rng.normal(0.0, self.noise_std, size=obs.shape)
return (obs + noise).astype(np.float32)

def make_env(noise_std=0.01, seed=None, render_mode=None):
"""CartPole(倒立振子)にセンサーノイズを加えた環境を作る"""
env = gym.make("CartPole-v1", render_mode=render_mode)
return NoisySensor(env, noise_std=noise_std, seed=seed)

def rule_based_policy(obs):
"""人が書いた単純なルール:棒が傾いた側へ台車を押す(0=左, 1=右)"""
angle = obs[2]
return 1 if angle > 0 else 0

def run_episode(env, policy, seed=None):
"""1エピソード分、計測→判断→行動のループを回す"""
obs, info = env.reset(seed=seed) # ① センサーで初期状態を計測
total_reward, steps = 0.0, 0
while True:
action = policy(obs) # ②③ 状態を理解し、行動を決める
obs, reward, terminated, truncated, info = env.step(action) # ④ 行動して再計測
total_reward += reward
steps += 1
if terminated or truncated: # 転倒 or 500ステップ到達で終了
return total_reward, steps, truncated and not terminated

if name == "__main__":
env = make_env(noise_std=0.01, seed=0)
obs, _ = env.reset(seed=0)
print("計測値:", {k: round(float(v), 4) for k, v in zip(SENSOR_NAMES, obs)})

for ep in range(5):
reward, steps, success = run_episode(env, rule_based_policy, seed=ep)
print(f"episode={ep} steps={steps} reward={reward:.0f} success={success}")

env.close()

実行すると、次のように表示されます。

計測値: {'台車の位置': 0.015, '台車の速度': -0.0243, '棒の角度': -0.0395, '棒の角速度': -0.0473}

episode=0 steps=28 reward=28 success=False

episode=1 steps=41 reward=41 success=False

episode=2 steps=35 reward=35 success=False

episode=3 steps=36 reward=36 success=False

episode=4 steps=47 reward=47 success=False

単純なルールでは、30〜50ステップで棒が倒れてしまいました。

棒の角度しか見ておらず、倒れる勢いや台車の位置を考慮していないためです。複数の情報を組み合わせた制御ルールを人が考えるのは意外と難しく、ここに学習の出番があります。

強化学習で簡単なタスクを学習させる

次に、先ほどのルールベースの代わりに、強化学習で「どう押せば倒れないか」をAIに学ばせます。

ライブラリには、PyTorchベースで定番アルゴリズムを手軽に使えるStable-Baselines3を使い、アルゴリズムは前章で紹介したPPOを選びました。

このコードは、強化学習で台車の上の棒を倒さずにバランスさせる制御方策を学習し、結果を保存するスクリプトです。

python

"""train_rl.py : PPO(強化学習)で倒立振子のバランス制御を学習する"""
import os
from stable_baselines3 import PPO
from stable_baselines3.common.env_util import make_vec_env
from sense_act import make_env

LOG_DIR = "logs"
os.makedirs(LOG_DIR, exist_ok=True)
# 4つの環境を並列に動かし、各エピソードの結果を logs/ に記録する

vec_env = make_vec_env(
lambda: make_env(noise_std=0.01),
n_envs=4,
seed=42,
monitor_dir=LOG_DIR,
)

model = PPO(
"MlpPolicy", # 4つの計測値 → 2つの行動 を出す小さなニューラルネット
vec_env,
learning_rate=3e-4,
n_steps=256,
batch_size=64,
gamma=0.99,
seed=42,
verbose=0,
)

model.learn(total_timesteps=100_000) # 試行錯誤を10万ステップ繰り返す
model.save("ppo_cartpole")
print("学習完了: ppo_cartpole.zip を保存しました")

実行すると、本記事の環境では約90秒で学習が完了し、学習済みモデル「ppo_cartpole.zip」と、エピソードごとの記録が保存されました。

このサンプルは、フィジカルAI開発の縮図になっています。環境を並列に動かして経験を集めるのは、Isaac LabなどでGPU上に数千台のロボットを並べて学習するのと同じ発想です。

学習結果を可視化する

学習がうまく進んだかどうかは、数字だけでなくグラフで確認するのが基本です。ここでは、次の2つを1枚の画像にまとめます。

  • 学習曲線:試行ステップ数に応じて、1エピソードあたりの報酬(倒れずに立っていたステップ数)がどう伸びたか

  • 棒の角度の推移:ルールベースと学習済みAIで、1エピソード中に棒の角度がどう動いたか

以下のコードは学習の成果を2枚のグラフで確認するスクリプトです。左が PPO の学習曲線、右が「ルールベース制御」と「学習済み PPO」の棒の角度の推移の比較で、results.png に保存します。

python
"""plot_results.py : 学習曲線と、学習前後の「棒の角度」の推移を可視化する"""
import matplotlib
matplotlib.use("Agg") # 画面のない環境でも画像を保存できるようにする
import matplotlib.pyplot as plt
import numpy as np
from stable_baselines3 import PPO
from stable_baselines3.common.monitor import load_results
from sense_act import make_env, rule_based_policy

# --- 1. 学習曲線(logs/ に記録されたエピソードごとの報酬) ---
df = load_results("logs").sort_values("t")
df["timesteps"] = df["l"].cumsum()
df["reward_ma"] = df["r"].rolling(20, min_periods=1).mean()

# --- 2. 1エピソード分の「棒の角度」を記録する ---
def record_angles(policy, seed=0):
env = make_env(noise_std=0.01, seed=seed)
obs, _ = env.reset(seed=seed)
angles = [np.degrees(obs[2])]
while True:
obs, _, terminated, truncated, _ = env.step(policy(obs))
angles.append(np.degrees(obs[2]))
if terminated or truncated:
return angles

model = PPO.load("ppo_cartpole")
ppo_policy = lambda obs: int(model.predict(obs, deterministic=True)[0])

fig, axes = plt.subplots(1, 2, figsize=(12, 4))
axes[0].plot(df["timesteps"], df["r"], alpha=0.3, label="episode reward")
axes[0].plot(df["timesteps"], df["reward_ma"], label="moving average (20)")
axes[0].axhline(500, color="gray", linestyle="--", label="max (500)")
axes[0].set(title="Learning curve (PPO)", xlabel="timesteps", ylabel="reward")
axes[0].legend()

axes[1].plot(record_angles(rule_based_policy), label="rule-based")
axes[1].plot(record_angles(ppo_policy), label="PPO (trained)")
axes[1].axhline(12, color="red", linestyle=":", label="fall limit (±12 deg)")
axes[1].axhline(-12, color="red", linestyle=":")
axes[1].set(title="Pole angle", xlabel="step (20 ms)", ylabel="angle [deg]")
axes[1].legend()

plt.tight_layout()
plt.savefig("results.png", dpi=150)
print("results.png を保存しました")

※Matplotlibは標準では日本語フォントを含まないため、文字化けを避けるためにグラフのラベルは英語にしています。日本語で表示したい場合は、日本語フォントを指定してください。

グラフからは、次のことが読み取れます。

  • 左(学習曲線):学習初期は十数〜数十ステップで倒れていましたが、試行を重ねるにつれて報酬が伸び、移動平均は約4万4,000ステップで上限の500に到達しました。その後は安定して500を維持しています

  • 右(棒の角度):ルールベース(青)は角度の振れが大きくなり、30ステップ弱で転倒ライン(−12度)まで傾いて倒れました。学習済みAI(オレンジ)は、ノイズがある中でも角度をおおむね±6度以内に保ち、500ステップを完走しています

学習曲線が途中で頭打ちになったり、上下に激しく振れ続けたりする場合は、報酬設計や学習率、学習ステップ数を見直すサインです。

コードで確認する評価指標

グラフで傾向をつかんだら、次は評価指標を数値で出します。

フィジカルAIでは、「平均的にうまくいくか」だけでなく、「どれくらいの確率で失敗するか」「制御周期に間に合うか」「条件が悪くなっても耐えられるか」まで確認する必要があります。

本サンプルでは、次の4つの指標を使います。

指標

定義

わかること

実務での例

成功率

500ステップ倒れずに完走したエピソードの割合

タスクをやり遂げられるか

ピッキングの成功率

平均報酬±標準偏差

エピソードごとの報酬の平均とばらつき

性能の高さと安定性

作業時間、品質スコア

推論時間(p50/p95)

1回の判断にかかる時間の中央値と95パーセンタイル

制御周期に間に合うか

1回あたりの推論レイテンシ

ノイズ耐性

センサーノイズを増やしたときの成功率の変化

実環境での頑健性

照明の変化やセンサー劣化への強さ

python
"""evaluate.py : 成功率・平均報酬・推論時間・ノイズ耐性をまとめて評価する"""
import time
import numpy as np
from stable_baselines3 import PPO
from sense_act import make_env, rule_based_policy

N_EPISODES = 50
model = PPO.load("ppo_cartpole")

def ppo_policy(obs):
return int(model.predict(obs, deterministic=True)[0])

def evaluate(policy, noise_std):
env = make_env(noise_std=noise_std, seed=123)
rewards, successes, infer_ms = [], [], []
for ep in range(N_EPISODES):
obs, _ = env.reset(seed=1000 + ep) # 学習時と違うシードで評価する
total = 0.0
while True:
t0 = time.perf_counter()
action = policy(obs)
infer_ms.append((time.perf_counter() - t0) * 1000)
obs, reward, terminated, truncated, _ = env.step(action)
total += reward
if terminated or truncated:
break
rewards.append(total)
successes.append(truncated and not terminated) # 500ステップ倒れずに完走
return {
"success_rate": np.mean(successes) * 100,
"reward_mean": np.mean(rewards),
"reward_std": np.std(rewards),
"infer_p50_ms": np.percentile(infer_ms, 50),
"infer_p95_ms": np.percentile(infer_ms, 95),
}

print("policy noise success% reward(mean±std) infer p50/p95[ms]")
for name, policy in [("rule-based", rule_based_policy), ("PPO", ppo_policy)]:
for noise in [0.01, 0.05, 0.10]:
m = evaluate(policy, noise)
print(f"{name:<10} {noise:<5} {m['success_rate']:>7.0f} "
f"{m['reward_mean']:>7.1f} ± {m['reward_std']:<6.1f} "
f"{m['infer_p50_ms']:.3f} / {m['infer_p95_ms']:.3f}")

実行結果を表にまとめると、次のとおりです(各条件50エピソード)。

方策

センサーノイズ

成功率

平均報酬±標準偏差

推論時間 p50/p95

ルールベース

0.01(学習時と同じ)

0%

43.1 ± 9.5

0.001/0.001ms

ルールベース

0.05

0%

51.7 ± 22.4

0.001/0.001ms

ルールベース

0.10

0%

44.9 ± 28.0

0.001/0.001ms

PPO

0.01(学習時と同じ)

100%

500.0 ± 0.0

0.390/0.527ms

PPO

0.05

100%

500.0 ± 0.0

0.379/0.487ms

PPO

0.10

84%

474.0 ± 71.5

0.396/0.600ms

この結果から、次の3つのことがわかります。

学習の効果:ルールベースの成功率0%に対し、学習済みAIは学習時と同じ条件で成功率100%を達成しました

ノイズ耐性の限界:ノイズを学習時の10倍(0.10)にすると、成功率は84%に下がりました。実機に移す前に、実際のセンサーのノイズの大きさを測り、その条件でも評価しておく必要があります

推論時間の余裕:推論時間はp95でも1ミリ秒未満で、制御周期の20ミリ秒に十分間に合っています。ただし、数十億パラメータのVLAでは推論に数十~数百ミリ秒かかることもあり、計算をどこで行うかが問題になります

評価の際は、学習時とは異なる乱数シードを使うこと、十分な回数を試してばらつきも確認することが大切です。

そして最後の「推論時間」の問題を掘り下げるのが、次の章で扱うMECです。

Pythonを用いたAI開発のポイントと注意点に関しては、こちらの記事でまとめています。

MECを使う場合の考え方

MECとは、通信ネットワークのエッジ、つまりデバイスに近い場所にサーバーを置き、そこで計算処理を行う技術です。

フィジカルAIの推論を「どこで行うか」は、大きく3つの選択肢があります。

配置

推論する場所

通信遅延

計算資源

向いている処理

エッジ

ロボット本体の計算機

なし

限られる

高頻度の反射的な制御、安全停止

MEC

基地局や通信事業者の拠点、工場構内など端末の近くのサーバー

小さい

中〜大

画像認識やVLAの推論、複数台の協調

クラウド

遠く離れたデータセンター

大きくなりやすい

非常に大きい

学習、大規模モデルでのタスク計画、ログ分析

※実際の遅延は、回線の種類や混雑状況、サーバーまでの距離によって大きく変わります。必ず自分の環境で実測して判断しましょう。

どこで推論するかを決める基準は、シンプルに「処理が許容できる遅延」と「実際の遅延(特に悪いケース)」の比較です。

「行動計画を作る」で紹介した二層構造に当てはめると、次のように役割を分担するのが基本になります。

  • エッジ(エッジAI):数十~数百Hzで回す反射的な制御と、通信が途切れたときの安全停止

  • MEC:端末では重すぎるが、数Hz~数十Hzで結果がほしい画像認識やVLAの推論

  • クラウド:時間に余裕のあるタスク計画、学習・再学習、複数拠点のデータ集約

MECには、低遅延に加えて、カメラ映像のような大容量データを遠隔のデータセンターまで送らずに処理できるといった利点もあります。

FastAPIで推論APIを作る

まず、MEC(またはクラウド)側に置く推論APIを作ります。Pythonの高速なWebフレームワークFastAPIを使い、エッジ端末から観測値(4つのセンサー値)を受け取って、学習済みモデルが決めた行動を返すシンプルなAPIにします。

このコードは、train_rl.py で学習した PPO モデルを、HTTP のFastAPIとして公開するサーバーです。

python
"""server.py : 学習済みモデルを推論APIとして公開する(MEC・クラウド側)"""
import time
from contextlib import asynccontextmanager

import numpy as np
from fastapi import FastAPI
from pydantic import BaseModel, Field
from stable_baselines3 import PPO

models = {}

@asynccontextmanager
async def lifespan(app: FastAPI):
models["ppo"] = PPO.load("ppo_cartpole", device="cpu") # 起動時に1回だけ読み込む
yield
models.clear()

app = FastAPI(title="Physical AI Inference API", lifespan=lifespan)

class ObservationIn(BaseModel):
observation: list[float] = Field(..., min_length=4, max_length=4) # 4つの計測値

class ActionOut(BaseModel):
action: int # 0=左に押す, 1=右に押す
inference_ms: float # サーバー内での推論時間

@app.get("/healthz")
def healthz():
return {"status": "ok", "model_loaded": "ppo" in models}

@app.post("/predict", response_model=ActionOut)
def predict(req: ObservationIn):
t0 = time.perf_counter()
obs = np.asarray(req.observation, dtype=np.float32)
action, _ = models["ppo"].predict(obs, deterministic=True)
return ActionOut(action=int(action),
inference_ms=(time.perf_counter() - t0) * 1000)

次のコマンドでサーバーを起動します。ほかの端末から接続する場合は--host 0.0.0.0を指定します。

bash
uvicorn server:app --host 0.0.0.0 --port 8000

別のターミナルから、curlで動作を確認してみましょう。棒が右に傾きながら右へ倒れつつある状態を送ると、「右に押す(1)」が返ってきます。

bash
curl -X POST http://127.0.0.1:8000/predict \
-H "Content-Type: application/json" \
-d '{"observation": [0.0, 0.0, 0.05, 0.5]}'
# => {"action":1,"inference_ms":1.03}(数値は一例)

エッジ側から推論APIを呼び出す

次に、ロボット側のプログラムを作ります。センサーで計測した値を推論APIに送り、返ってきた行動でアクチュエータを動かすループです。

ここで重要なのは、通信を待っている間も物理世界は止まらないということです。シミュレーターはstep()を呼ぶまで時間が止まっていますが、実機では応答を待つ間も棒は倒れ続けます。

そこで、応答に20ミリ秒(1制御周期)以上かかった場合は、遅れた周期の数だけ「直前の行動のまま」環境を進めるようにして、遅延の影響を再現しました。

このコードは、server.py の推論APIを、ロボット側から呼び出して倒立振子を制御するクライアントです。

python
"""edge_client.py : エッジ端末(ロボット側)から推論APIを呼び出して制御する"""
import time
import httpx
import numpy as np
from sense_act import make_env

CONTROL_PERIOD_MS = 20 # CartPoleの1ステップ = 20ms(50Hz制御)

class RemotePolicy:
"""推論APIを呼び出す方策。失敗時は直前の行動を維持する"""

def __init__(self, url, timeout_ms=200, simulated_rtt_ms=0.0, jitter_ms=0.0, seed=0):
self.client = httpx.Client(base_url=url, timeout=timeout_ms / 1000) # 接続を再利用
self.simulated_rtt_ms = simulated_rtt_ms # 検証用の擬似ネットワーク遅延
self.jitter_ms = jitter_ms
self.rng = np.random.default_rng(seed)
self.last_action = 0
self.log = [] # (遅延ms, API成功したか)

def __call__(self, obs):
t0 = time.perf_counter()
ok = False
try:
if self.simulated_rtt_ms > 0: # 実環境では0にし、MEC上のURLを指定する
delay = self.rng.normal(self.simulated_rtt_ms, self.jitter_ms)
time.sleep(max(delay, 0) / 1000)
res = self.client.post("/predict", json={"observation": obs.tolist()})
res.raise_for_status()
self.last_action = res.json()["action"]
ok = True
except httpx.HTTPError:
pass # タイムアウト・通信エラー時は直前の行動を維持(フォールバック)
latency_ms = (time.perf_counter() - t0) * 1000
self.log.append((latency_ms, ok))
return self.last_action, latency_ms

def run_realtime_episode(env, policy, seed=None):
"""推論の遅れを「物理世界が待ってくれない時間」として反映して1エピソード回す"""
obs, _ = env.reset(seed=seed)
applied = 0
while True:
action, latency_ms = policy(obs)
# 応答を待つ間も時間は進む:遅れた周期ぶん、直前の行動のまま環境を進める
for _ in range(int(latency_ms // CONTROL_PERIOD_MS)):
obs, _, terminated, truncated, _ = env.step(applied)
if terminated or truncated:
return truncated and not terminated
applied = action
obs, _, terminated, truncated, _ = env.step(applied)
if terminated or truncated:
return truncated and not terminated

if __name__ == "__main__":
url = "http://127.0.0.1:8000"
print(httpx.get(f"{url}/healthz").json())
policy = RemotePolicy(url)
success = run_realtime_episode(make_env(noise_std=0.01, seed=0), policy, seed=0)
lat = [l for l, _ in policy.log]
print(f"完走={success} リクエスト数={len(lat)} 平均遅延={np.mean(lat):.2f}ms")

サーバーを起動した状態でpython edge_client.pyを実行すると、次のように表示されました。

text

{'status': 'ok', 'model_loaded': True}

完走=True  リクエスト数=500  平均遅延=2.45ms

p50・p95遅延と成功率を測定する

最後に、推論をどこに置くと制御がうまくいくのかを、数値で比較します。遅延の評価では、平均値ではなくパーセンタイルを使うのが定石です。

平均値は、ときどき発生する大きな遅延(スパイク)に引っ張られたり、逆に隠れてしまったりするためです。

指標

意味

見るべきポイント

p50(中央値)

全リクエストの半分がこの時間以内に返ってくる

普段の速さ

p95

95%のリクエストがこの時間以内に返ってくる

20回に1回程度起きる「遅いとき」の速さ

p99

99%のリクエストがこの時間以内に返ってくる

まれに起きる最悪に近いケース

API成功率

エラーやタイムアウトなく応答が返ってきた割合

通信の安定性

期限内率

制御周期以内に応答が返ってきた割合

リアルタイム制御に間に合っているか

タスク成功率

500ステップ倒れずに完走したエピソードの割合

最終的にタスクを達成できたか

次のコードでは、「エッジ内推論(通信なし)」「MEC想定(追加遅延8±2ミリ秒)」「クラウド想定(往復遅延40±10ミリ秒)」の3パターンを、それぞれ20エピソードずつ実行して比較します。

MEC想定の「追加遅延」は、エッジ側でリクエストの前に意図的に待ち時間を入れ、通信距離を擬似的に上乗せしたものです。

測定値には、この追加遅延に加えて、同じPC上でのHTTP通信、FastAPIによるAPI処理、モデルの推論時間も含まれます。そのため、MEC想定のp50は、設定した追加遅延よりも大きくなっています。

以下のコードは、推論を置く場所ごとに、遅延の分布と、その遅延が制御の成否にどう効くかを測り、1つの表にまとめて比較するスクリプトです。

python
"""measure_latency.py : p50・p95遅延と各種成功率を配置パターン別に測定する"""
import time
import numpy as np
from stable_baselines3 import PPO
from sense_act import make_env
from edge_client import RemotePolicy, run_realtime_episode, CONTROL_PERIOD_MS

URL = "http://127.0.0.1:8000"
N_EPISODES = 20

class LocalPolicy:
"""比較用:エッジ端末の中で推論する(通信なし)"""
def __init__(self):
self.model = PPO.load("ppo_cartpole", device="cpu")
self.log = []
def __call__(self, obs):
t0 = time.perf_counter()
action = int(self.model.predict(obs, deterministic=True)[0])
latency_ms = (time.perf_counter() - t0) * 1000
self.log.append((latency_ms, True))
return action, latency_ms

scenarios = {
"エッジ内推論": LocalPolicy(),
"MEC想定(追加遅延8±2ms)": RemotePolicy(URL, simulated_rtt_ms=8, jitter_ms=2),
"クラウド想定(RTT 40±10ms)": RemotePolicy(URL, simulated_rtt_ms=40, jitter_ms=10),
}

print(f"{'配置':<24}{'p50':>8}{'p95':>8}{'p99':>8} API成功率 期限内率 タスク成功率")
for name, policy in scenarios.items():
task_ok = [run_realtime_episode(make_env(noise_std=0.01, seed=ep), policy, seed=ep)
for ep in range(N_EPISODES)]
lat = np.array([l for l, _ in policy.log])
api_ok = np.array([ok for _, ok in policy.log])
p50, p95, p99 = np.percentile(lat, [50, 95, 99])
print(f"{name:<24}{p50:>7.2f}ms{p95:>7.2f}ms{p99:>7.2f}ms"
f" {api_ok.mean() * 100:>7.1f}%"
f" {(lat <= CONTROL_PERIOD_MS).mean() * 100:>6.1f}%"
f" {np.mean(task_ok) * 100:>8.0f}%")

実行結果は、次のとおりです。

配置

p50

p95

p99

API成功率

期限内率(20ms以内)

タスク成功率

エッジ内推論

0.37ms

0.49ms

0.61ms

100.0%

100.0%

100%

MEC想定(追加遅延8±2ms)

11.48ms

15.27ms

17.04ms

100.0%

99.9%

95%

クラウド想定(RTT 40±10ms)

44.60ms

60.72ms

67.12ms

100.0%

0.9%

0%

結果から読み取れるポイントは次の3つです。

  • API成功率は全配置で100%であり、クラウド配置の失敗は通信障害ではなく「応答が制御周期(20ms)に間に合わない」ことが原因だと推測されます。
  • MEC想定はp99が17.04msで期限内に収まりタスク成功率95%を達成するが、実機ではさらに実網の遅延が乗るため余裕は薄い結果に。
  • 推論配置の判断基準は平均やp50ではなく「p99が制御周期を下回るか」であり、これを満たすエッジ・MECは成功し、満たさないクラウド(p99 67ms)は0%となりました。

今回の数値は擬似的な遅延によるものですが、simulated_rtt_ms=0にして実際のMECやクラウド上のサーバーを指定すれば、同じコードで実環境の測定ができます。

初心者向けのAI開発の基本ポイントに関しましては、こちらの記事でまとめています。

フィジカルAI開発を始める5段階

フィジカルAIの開発は、いきなり実機のロボットを買って始めると、費用も時間もかさみ、失敗したときに原因の切り分けも難しくなります。

おすすめは、

  1. 仮想空間で仕組みを理解する

  2. 実データに慣れる

  3. 目を与える

  4. 端末に載せる

  5. 限定した実機タスクで確かめる

と、段階的に現実へ近づけていく進め方です。

それぞれの段階で「何ができれば次に進んでよいか」を決めておくと、手戻りを減らせます。

このセクションのPythonサンプルは、このうち①と④の一部を体験する内容になっています。

段階

ゴール

主なツール・データ

次へ進む目安

①シミュレーション

計測→判断→行動のループと、学習・評価の流れを理解する

Gymnasium、MuJoCo、Isaac Lab

評価指標で学習前後の差を説明できる

②公開データセット

実ロボットのデータ形式と、学習済みモデルの扱いに慣れる

LeRobot、Open X-Embodiment、DROID

既存データで方策を学習し、評価できる

③カメラ入力

画像からの認識・推論をループに組み込む

USBカメラ、OpenCV、VLM/VLA

決めた遅延予算の中で画像推論が回る

④エッジ移行

推論を省電力な端末で動かす

Jetson、ONNX、TensorRT

端末上でp99遅延が制御周期に収まる

⑤限定実機タスク

実機で安全に性能を検証する

低価格ロボットアーム、LeRobot

合意した成功率と安全要件を満たす

必要な費用も段階的に増えていきます。①はPC1台で始められ、②③で大きなモデルを扱う段階になったら、GPUを搭載したPCや、必要なときだけ時間単位で借りられるGPUクラウドを使うと、初期投資を抑えながら検証を進められます。

POINT

01

実機なしでシミュレーションする

最初の段階では、実機を使わずにシミュレーションの中で「計測→判断→行動」のループと、学習・評価のサイクルを一通り体験します。本記事のCartPoleのサンプルが、まさにこの段階の入り口です。

CartPoleで流れをつかんだら、物理エンジンMuJoCoを使ったGymnasiumの環境に進みましょう。pip install "gymnasium[mujoco]"でインストールでき、本記事のコードの環境名を変えるだけで試せます。

環境名

内容

観測の次元/行動の次元

InvertedPendulum-v5

連続的な力で押す倒立振子

4/1

Reacher-v5

2関節のアームで目標点に手先を届かせる

10/2

Ant-v5

4本脚ロボットの歩行

105/8

Humanoid-v5

人型ロボットの歩行

348/17

MuJoCoの環境では行動が「押す/押さない」の2択ではなく、連続的なトルクになります。アルゴリズムをPPOのまま試すほか、連続制御と相性の良いSACに切り替えて比べてみるのもよいでしょう。

観測と行動の次元が増えるほど学習に必要なステップ数は増えるため、本格的に取り組む段階では、GPUで数千の環境を並列に動かせるIsaac LabやMuJoCo Playgroundへ移行します。

  • この段階で身につけておきたいのは、次のような「実験の型」です。

  • 報酬を何にするかを言葉で説明できる

  • 学習曲線と評価指標(成功率・ばらつき・推論時間)で結果を示せる
  • ノイズやパラメータのランダム化を加え、条件が変わっても性能が保てるかを確認できる

POINT

02

公開データセットで学習する

シミュレーションの次は、実際のロボットで集められた公開データセットに触れます。

自分でデータを集める前に、実ロボットのデータがどのような形式で、どのくらいの量が必要なのかを知っておくと、後の段階での設計がスムーズになります。

データセット

内容

規模

Open X-Embodiment

世界の研究機関が持ち寄ったロボット操作データ

22種類のロボット、100万件以上の軌跡、527のスキル(21機関・34研究室が参加)

DROID

家庭やオフィスなど多様な実環境で収集したアーム操作データ

7.6万件のデモ、350時間分、564のシーン、84のタスク

LeRobot(Hugging Face Hub)

コミュニティが共有するロボットデータ

映像・状態・行動が同期した共通形式「LeRobotDataset」で多数公開

(出典:Open X-Embodiment、DROID、LeRobot)

データに慣れたら、公開されている学習済みモデルを自分のタスク向けに追加学習(ファインチューニング)してみましょう。LeRobotでは、ACTやDiffusion Policyに加え、SmolVLAやπ0系、NVIDIAのGR00TといったVLAも扱えます。最初は約4.5億パラメータと小型のSmolVLAから始めると、比較的手頃な計算資源で試せます。

この段階で注意したいポイントは次の3つです。

  1. ライセンスを確認する:データセットやモデルごとに、商用利用の可否や条件が異なります

  2. ロボットの違いを意識する:関節の数、カメラの位置、行動の表し方(関節角度か手先位置か)が、自分の環境と合っているかを確認します

  3. 計算資源を見積もる:VLAのファインチューニングにはGPUが必要です。手元にGPUがない場合は、GPUクラウドを必要な時間だけ利用する方法が現実的です

POINT

03

カメラ入力と推論を追加する

3つ目の段階では、ロボットに「目」を与えます。CartPoleでは4つの数値が直接得られましたが、現実のタスクでは、カメラ画像から物体の位置や状態を読み取る必要があります。

まずはPCにUSBカメラをつなぎ、「撮影→前処理→推論」の1周にかかる時間を測るところから始めましょう。次のコードのinfer関数を、物体検出モデルやVLM・VLAの推論に置き換えていきます(OpenCVはpip install opencv-pythonでインストールできます)。

このコードは、「カメラ画像を取り込んで推論するまでの1周」にどれくらい時間がかかるかを、p50・p95・p99で計測するものです。

python
"""camera_loop.py : カメラ入力 → 前処理 → 推論 の1周にかかる時間を測る"""
import sys
import time
import cv2
import numpy as np

source = sys.argv[1] if len(sys.argv) > 1 else 0 # 0 = PCにつないだUSBカメラ
cap = cv2.VideoCapture(source)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)

def infer(image):
"""ここを物体検出・VLM・VLAなどの推論に置き換える(今は平均輝度を返すだけ)"""
return float(image.mean())

lat_ms = []
for _ in range(300):
t0 = time.perf_counter()
ok, frame = cap.read() # ① 計測(撮影・転送)
if not ok:
break
x = cv2.resize(frame, (224, 224)) # 前処理:モデルの入力サイズに合わせる
x = cv2.cvtColor(x, cv2.COLOR_BGR2RGB).astype(np.float32) / 255.0
result = infer(x) # ② 理解・③ 計画(推論)
lat_ms.append((time.perf_counter() - t0) * 1000)
cap.release()

p50, p95, p99 = np.percentile(lat_ms, [50, 95, 99])
print(f"frames={len(lat_ms)} p50={p50:.1f}ms p95={p95:.1f}ms p99={p99:.1f}ms")

実際のカメラでは、cap.read()が次のフレームを待つため、30fpsのカメラなら1周に約33ミリ秒以上かかります。

つまり、カメラを使った時点で「制御周期20ミリ秒」は成り立たなくなる、ということです。このように、カメラを加える段階では、処理全体を遅延予算として分解して考えることが大切です。

処理

考え方の例

撮影・転送

30fpsなら約33ミリ秒ごとに1枚。高速な制御が必要ならフレームレートの高いカメラを選ぶ

前処理

リサイズや色変換。解像度を下げるほど速くなるが、認識精度とのバランスを取る

推論

モデルの大きさと実行場所(エッジ/MEC)で大きく変わる。p95・p99で評価する

通信(MEC利用時)

前章の測定方法で、往復の遅延を実測する

制御指令

カメラ推論は低い頻度で目標を出し、関節の細かな制御は高い頻度で別に回す

あわせて、カメラの内部パラメータや取り付け位置のキャリブレーション、照明条件の変化への対策も、この段階で確認しておきましょう。

ロボットアームでは、手首に付けたカメラと作業全体を見渡すカメラを併用する構成も広く使われています。

POINT

04

エッジ端末へ移行する

4つ目の段階では、PCで動かしていた推論を、ロボットに搭載できる小型・省電力なエッジ端末へ移します。

このように、クラウドに頼らず端末側でAIの推論を実行する仕組みをエッジAIと呼びます。通信が途切れても動き続けられること、そして前章で見たように遅延を最小にできることが、フィジカルAIでエッジAIを活用する利点です。

エッジ端末の例

AI処理性能(公称値)

向いている用途

Raspberry Pi 5+AI HAT+

13 TOPSまたは26 TOPS

軽量な物体検出、試作

NVIDIA Jetson Orin Nano Super 開発者キット

最大67 TOPS

小〜中規模モデル、カメラ推論の入門

NVIDIA Jetson AGX Thor 開発者キット

最大2,070 TFLOPS(FP4)、メモリ128GB、消費電力40〜130W

ヒューマノイド、VLAなど大規模モデル

(出典:Raspberry Pi、CNX Software(Jetson Orin Nano Super)、CNX Software(Jetson AGX Thor))

エッジ端末へ移す際は、学習に使ったフレームワークのまま動かすのではなく、推論に特化した形式へ変換するのが一般的です。

代表的な方法が、モデルをONNX形式に書き出し、ONNX RuntimeやTensorRTで実行する方法です。

python
"""export_onnx.py : 学習済み方策をONNX形式に変換し、ONNX Runtimeで推論時間を測る"""
import time
import numpy as np
import onnxruntime as ort
import torch
from stable_baselines3 import PPO

class OnnxablePolicy(torch.nn.Module):
"""観測 → 行動ロジット だけを取り出した軽量モデル"""

def __init__(self, policy):
super().__init__()
self.policy = policy

def forward(self, obs):
features = self.policy.extract_features(obs, self.policy.pi_features_extractor)
latent_pi = self.policy.mlp_extractor.forward_actor(features)
return self.policy.action_net(latent_pi)

model = PPO.load("ppo_cartpole", device="cpu")
dummy = torch.zeros(1, 4, dtype=torch.float32)
torch.onnx.export(OnnxablePolicy(model.policy), dummy, "policy.onnx",
input_names=["obs"], output_names=["logits"])

session = ort.InferenceSession("policy.onnx", providers=["CPUExecutionProvider"])
obs = np.array([[0.0, 0.0, 0.05, 0.5]], dtype=np.float32)
lat = []
for _ in range(1000):
t0 = time.perf_counter()
logits = session.run(None, {"obs": obs})[0]
lat.append((time.perf_counter() - t0) * 1000)

print("ONNX action:", int(np.argmax(logits)),
"/ SB3 action:", int(model.predict(obs[0], deterministic=True)[0]))
print(f"ONNX Runtime p50={np.percentile(lat, 50):.3f}ms p95={np.percentile(lat, 95):.3f}ms")
本記事の環境では、変換前と同じ行動を出力しつつ、推論時間はp50で約0.011ミリ秒になりました。Stable-Baselines3のpredict(p50で約0.39ミリ秒)と比べて大幅に短縮できています。
text
ONNX action: 1 / SB3 action: 1
ONNX Runtime p50=0.011ms p95=0.017ms

エッジへの移行では、次の点もあわせて確認しましょう。

  • 量子化:FP16やINT8など、数値の精度を落としてモデルを軽くする。精度がどれだけ落ちるかを評価指標で必ず確認する
  • 再測定:変換後に、evaluate.pyやmeasure_latency.pyと同じ方法で、成功率とp50・p95・p99遅延を測り直す
  • 電力と発熱:長時間動かすと発熱で性能が落ちる(サーマルスロットリング)ことがあるため、連続稼働での遅延も確認する
  • 役割分担:端末で動かしきれない大きなモデルはMECに置き、端末には高頻度の制御と安全停止を残す

POINT

05

限定された実機タスクで検証する

最後の段階で、いよいよ実機を動かします。ここで大切なのは、最初から汎用的なロボットを目指さず、「決まった場所にある1種類の物体をつかんで箱に入れる」のように、条件を絞り込んだタスクから始めることです。

条件を限定すれば、失敗したときに原因を特定しやすく、安全も確保しやすくなります。

入門には、LeRobotが対応している低価格ロボットアーム「SO-101」などが適しています。リーダーアームで操作してデモを記録し、ACTなどで学習させて実機にデプロイする流れを、比較的少ない予算で一通り試せます。

実機の検証では、あらかじめ次のような計画を立てておきましょう。

項目

設定例

タスク

机の決まった範囲に置いた1種類の物体をつかみ、箱に入れる

試行回数

同じ条件で20〜50回(成功率のばらつきを見るため)

評価指標

成功率、1回あたりの所要時間、人が介入した回数、ヒヤリハットの件数

条件の広げ方

物体の位置→物体の種類→照明・背景の順に、少しずつ変化を加える

比較

シミュレーションでの成功率との差(Sim-to-Realギャップ)を記録し、原因を分析する

安全面では、次の対策を必ず講じてください。

  • 速度と力の上限を低めに設定する

  • 手の届く位置に非常停止スイッチを置く

  • ロボットの動作範囲を明示し、検証中は人が立ち入らないようにする

  • AIの判断とは独立して動く停止の仕組みを用意する

  • 工場などに導入する段階では、リスクアセスメントを実施し、労働安全衛生規則などの法令や、産業用ロボットの安全規格への適合を確認する

実機での結果をもとに、シミュレーションの物理パラメータやデータ収集の方法を見直し、①〜⑤を繰り返し改善していくことが、実用的なフィジカルAIへの近道です。

AI開発に必要なクラウドインフラに関しましては、こちらの記事を参考にしてみてください。

まとめ

フィジカルAIは、センサーで現実世界を認識し、AIが判断して、ロボットや機械の動きとして実行するAIです。生成AIが「デジタルな出力」を生み出すのに対し、フィジカルAIは「物理的な動き」を生み出すため、精度だけでなくリアルタイム性と安全性が求められます。

国内外で投資と技術開発が加速する今、フィジカルAIはロボット専門家だけのものではなくなりつつあります。まずは本記事のサンプルコードを手元で動かし、「計測→判断→行動」のループと、遅延が物理世界に与える影響を体感するところから始めてみてください。

本格的に取り組むなら、GPUSOROBANで開発を加速

サンプルのような小さなモデルは手元のPCでも動きますが、物体検出・VLM・VLAといった実用的なモデルの学習や検証には、大規模なGPUリソースが欠かせません。

GPUSOROBANなら、必要なときに必要な分だけGPUを活用でき、フィジカルAIの試作から検証までをスピーディーに進められます。サービスの詳細や導入のご相談は、ぜひ資料請求からお気軽にお問い合わせください。