Sim-to-Real: Model Uncertainty와 Action Delay

학습 목표

시뮬레이터에서 좋은 정책도 물리 계수와 지연이 달라지면 실패할 수 있습니다. 실제 장치로 옮기기 전에 이 차이를 수치로 확인합니다.

실행 조건

필요한 패키지가 없으면 Colab·Jupyter 코드 셀에서 %pip install control==0.10.2 gymnasium==1.3.0 stable-baselines3==2.9.2를 실행하세요.

적용 사례

구동계의 추종 성능과 입력 비용

각 실험에서 일정한 Reference Angular Velocity를 추종하는 합성 1차 Motor 모델입니다. 학습은 목표 1, 평가는 0.5·1·1.5에서 구분합니다.

Observation·State: Angular Velocity ω/5\omega/5, 목표 r/2r/2.

행동: 연속 a∈[−1,1]a\in[-1,1], 실제 입력 u=2au=2a.

Reward·Constraint: −((r−ω)2+0.05u2)-((r-\omega)^2+0.05u^2). |ω|>5|\omega|>5에서 종료, 100 step에서 시간 제한.

비교 기준: 동일 입력 제한의 P 제어.

확인할 지표: 추종 RMSE, 평균 u², 안전 종료 수. 목표별 분리 평가.

공조·열 공정 제어

온도 편차와 에너지 사용을 함께 줄이는 제어 문제의 설계 사례입니다.

Observation·State: 온도·목표·외기 조건·최근 입력. 지연이 크면 이력 포함.

행동: 제한된 연속 가열·냉각 출력.

Reward·Constraint: 온도 오차와 에너지 비용. 안전 한계는 보상과 별도 지표로도 기록.

비교 기준: 온도 조절기, PI 제어.

확인할 지표: 오차, 에너지, 한계 위반 시간. 외기 조건별 분리 평가.

명목 모델에서의 기준 제어

시작 조건

Motor 기준 모델과 P 제어입니다. Reference Angular Velocity 0.5·1·1.5를 비교합니다.

진행 방법

  1. 모델의 입력 제한·시간 간격·관측 단위를 적고 기준 제어를 실행하세요.
  2. 목표별 RMSE·입력 비용·Return을 비교하세요.

남길 결과

합성 모델에서 a∈[−1,1]a\in[-1,1]인 Action은 u=2a∈[−2,2]u=2a\in[-2,2]인 모델 입력에 대응합니다. uu의 물리 단위는 지정하지 않았습니다.

"""Synthetic motor; not a hardware interface. dt=0.1 s, scalar speed and bounded input."""
import numpy as np
class Motor:
    def reset(self, seed=0, reference=1., noise=.01):
        self.rng = np.random.default_rng(seed)
        self.omega, self.t = 0., 0
        self.reference, self.noise = reference, noise
        return np.array([self.omega / 5, reference / 2], dtype=np.float32)
    def step(self, action):
        u = 2 * float(np.clip(np.asarray(action).item(), -1, 1))
        self.omega += .1 * (-.4 * self.omega + .8 * u + self.rng.uniform(-self.noise, self.noise))
        self.t += 1; error = self.reference - self.omega
        reward = -(error**2 + .05 * u**2)
        return np.array([self.omega / 5, self.reference / 2], dtype=np.float32), reward, abs(self.omega) > 5, self.t >= 100, {'error': error, 'u': u}

def motor_evaluate(policy, references=(.5, 1., 1.5)):
    rows=[]
    for reference in references:
        env=Motor(); obs=env.reset(100, reference)
        errors=[]; inputs=[]; total=0.
        while True:
            obs,r,terminated,truncated,info=env.step(policy(obs))
            errors.append(info['error']);inputs.append(info['u']);total+=r
            if terminated or truncated:break
        rows.append([reference, np.sqrt(np.mean(np.square(errors))),np.mean(np.square(inputs)),int(terminated),total])
    return np.array(rows)

def proportional(obs):return np.clip((2 * obs[1] - 5 * obs[0]), -1, 1)
print('reference / RMSE / mean u² / safety termination / Return')
print(motor_evaluate(proportional))

결과 기록·자가 점검

목표:

RMSE:

평균 u²:

Return:

확인할 점

모델이 틀릴 때 무너지는 지점

시작 조건

Gain .8→.6 또는 입력 지연 0→0.2 s를 하나씩 적용합니다. P 제어와 짧게 학습한 SAC를 같은 조건에서 비교합니다.

진행 방법

  1. 제공된 StressMotor가 명목 모델에서 바꾼 항만 찾아 표시하세요.
  2. 명목·Gain 저하·입력 지연의 평가를 실행하고 지표를 비교하세요.
  3. 각 제어기가 취약한 조건 하나를 찾아 실제 장치에서 측정할 항목과 연결하세요.

남길 결과

# Run the Queue or Motor definition first. Install gymnasium and stable-baselines3 if absent.
import gymnasium as gym
from gymnasium import spaces
from stable_baselines3 import A2C, PPO, SAC
class QueueGym(gym.Env):
    def __init__(self):
        self.model=Queue();self.action_space=spaces.Discrete(3)
        self.observation_space=spaces.Box(0.,1.,(1,),dtype=np.float32)
    def reset(self,seed=None,options=None):
        super().reset(seed=seed)
        return np.array([self.model.reset(seed)/20],dtype=np.float32),{}
    def step(self,a):
        q,r,te,tr,info=self.model.step(a)
        return np.array([q/20],dtype=np.float32),r/20,te,tr,info
class MotorGym(gym.Env):
    def __init__(self):
        self.model=Motor();self.action_space=spaces.Box(-1.,1.,(1,),dtype=np.float32)
        self.observation_space=spaces.Box(-np.inf,np.inf,(2,),dtype=np.float32)
    def reset(self,seed=None,options=None):
        super().reset(seed=seed)
        return self.model.reset(seed),{}
    def step(self,a):return self.model.step(a)

def fit_sb3(algorithm='PPO',seed=0,steps=2048):
    cls={'A2C':A2C,'PPO':PPO,'SAC':SAC}[algorithm]
    env=MotorGym() if algorithm=='SAC' else QueueGym()
    options={'policy_kwargs':dict(net_arch=[32,32]),'seed':seed,'verbose':0,'device':'cpu'}
    if algorithm=='PPO':options.update(n_steps=128,batch_size=64,n_epochs=3)
    if algorithm=='A2C':options.update(n_steps=32)
    if algorithm=='SAC':options.update(learning_starts=100,buffer_size=5000,batch_size=64)
    model=cls('MlpPolicy',env,**options);model.learn(total_timesteps=steps)
    return model

motor_model=fit_sb3('SAC')
# Run the Motor baseline first. These are sensitivity tests, not real-robot results.
from collections import deque
class StressMotor(Motor):
    def __init__(self,damping=.4,gain=.8,delay=0):
        self.damping,self.gain,self.delay=damping,gain,delay
    def reset(self,seed=0,reference=1.,noise=.01):
        obs=super().reset(seed,reference,noise)
        self.pending=deque([0.]*self.delay)
        return obs
    def step(self,action):
        command=2*float(np.clip(np.asarray(action).item(),-1,1))
        self.pending.append(command);u=self.pending.popleft()
        self.omega+=.1*(-self.damping*self.omega+self.gain*u+self.rng.uniform(-self.noise,self.noise))
        self.t+=1;error=self.reference-self.omega
        return np.array([self.omega/5,self.reference/2],dtype=np.float32),-(error**2+.05*u**2),abs(self.omega)>5,self.t>=100,{'error':error,'u':u}
def stress_test(policy):
    rows=[]
    for name,params in [('nominal',{}),('gain loss',dict(gain=.6)),('delay 0.2s',dict(delay=2))]:
        for seed in range(100,103):
            env=StressMotor(**params);obs=env.reset(seed);errors=[];energy=[];total=0.
            while True:
                obs,r,te,tr,info=env.step(policy(obs));errors.append(info['error']);energy.append(info['u']**2);total+=r
                if te or tr:break
            rows.append([name,seed,total,float(np.sqrt(np.mean(np.square(errors)))),float(np.mean(energy)),te])
    return rows
for row in stress_test(proportional):print(row)

for row in stress_test(lambda o:motor_model.predict(o,deterministic=True)[0]):print('SAC',row)

결과 기록·자가 점검

정책:

조건:

seed:

Return:

RMSE:

입력 비용:

확인할 점

내 연구의 Reality gap 정하기

시작 조건

앞의 세 조건 중 본인의 연구와 가장 가까운 하나를 선택합니다.

진행 방법

  1. 식별할 물리 계수·센서 잡음·지연 중 우선순위를 하나 정하세요.
  2. 명목값·측정할 범위·학습에 사용할 범위·별도 평가 조건을 표로 정하세요.
  3. 실측 자료가 있다면 추정 방법을, 없다면 필요한 최소 관측을 적으세요.

남길 결과

결과 기록·자가 점검

불확실성:

측정 방법:

학습 범위:

평가 범위:

확인할 점

공식 예제·데이터