시뮬레이터의 우등생, 현실의 신입생
시뮬레이션에서 로봇팔 목표 도달 정책을 학습시키면 처음에는 꽤 감동적인 장면을 보게 된다. 화면 속 로봇팔은 수천 번, 수만 번의 시행착오 끝에 점점 목표점에 가까워진다. 처음에는 허공을 휘젓고, 관절 한계에 부딪히고, 이상한 방향으로 꺾이던 팔이 어느 순간부터 매끄럽게 목표로 다가간다. 성공률이 올라가고, 최종 거리 오차가 줄어들고, 충돌 횟수도 감소한다.
그런데 이 정책을 실제 로봇에 옮기는 순간 이야기가 달라진다. 시뮬레이터에서는 안정적이던 동작이 실제 로봇에서는 조금씩 흔들린다. 목표점 근처에서 떨리거나, 생각보다 느리게 반응하거나, 물체를 밀어 버리거나, 그리퍼가 정확한 위치에 도달하지 못한다. 더 심한 경우에는 로봇팔이 예상하지 못한 방향으로 움직이려 해서 즉시 정지해야 할 수도 있다.
이 간극을 Sim2Real, 즉 시뮬레이션에서 현실로 옮기는 문제라고 부른다. 강화학습에서 Sim2Real은 단순한 배포 문제가 아니다. 학습이 끝난 모델 파일을 실제 로봇 컴퓨터에 복사한다고 끝나는 일이 아니다. 시뮬레이터와 현실 사이에 존재하는 수많은 차이를 정책이 견딜 수 있도록 만들어야 한다.
핵심은 간단하다. 시뮬레이션은 깨끗하고 빠르고 반복하기 좋지만, 현실은 거칠고 느리고 예외가 많다. 따라서 현실에서 잘 동작하는 정책을 만들려면 시뮬레이터 안에 현실의 불편함을 일부러 넣어야 한다.

현실과 시뮬레이션은 왜 다를까
시뮬레이터는 현실을 수학적으로 흉내 낸 세계다. 로봇팔의 링크 길이, 관절 위치, 질량, 마찰, 중력, 충돌, 카메라 이미지, 센서값 등을 계산해서 다음 상태를 만들어 낸다. 하지만 현실 세계를 완벽하게 계산하는 것은 불가능하다. 우리가 쓰는 시뮬레이터는 언제나 단순화된 현실이다.
예를 들어 시뮬레이터 속 테이블은 완벽하게 평평할 수 있다. 하지만 실제 테이블은 약간 기울어져 있을 수 있고, 표면 재질도 다르며, 물체와 접촉할 때 생기는 마찰도 일정하지 않다. 시뮬레이터 속 로봇 관절은 명령한 각도에 정확히 도달한다고 가정할 수 있지만, 실제 모터는 백래시, 탄성, 제어 주기, 온도, 부하에 따라 조금씩 다르게 움직인다. 여기서 백래시는 기어 사이의 작은 유격을 뜻한다. 명령을 바꿨을 때 즉시 반응하지 않고 아주 조금 헛도는 구간이 생길 수 있다.
이 차이는 작은 숫자로 보일 수 있다. 하지만 로봇에서는 작은 오차가 큰 실패로 이어진다. 그리퍼가 컵 중심에서 1cm만 벗어나도 집기에 실패할 수 있다. 깊이 카메라의 값이 몇 프레임 늦게 들어오면 이미 물체는 다른 위치에 있을 수 있다. 관절 속도가 조금 늦게 반응하면 강화학습 정책이 예상한 다음 상태와 실제 다음 상태가 어긋난다.
Sim2Real에서 자주 문제가 되는 차이는 크게 네 종류로 볼 수 있다.
물리 파라미터 차이: 질량, 마찰, 관성, 관절 감쇠, 탄성, 충돌 특성 등이 시뮬레이션과 현실에서 다르다.
센서 차이: 카메라 노이즈, 깊이 오차, 조명 변화, 인코더 오차, 프레임 누락, 좌표계 보정 오차가 생긴다.
제어 차이: 모터 지연, 명령 주기 차이, 속도 제한, 토크 제한, 저수준 제어기의 응답 특성이 다르다.
환경 차이: 물체 위치, 테이블 높이, 배경, 조명, 장애물, 사람의 개입처럼 실험 환경이 매번 완전히 같지 않다.
시뮬레이션에서 학습한 정책은 자신이 경험한 세계에 맞춰 최적화된다. 그 세계가 지나치게 깨끗하면 정책도 깨끗한 세계에만 적응한다. 이때 현실은 정책 입장에서 낯선 환경이 된다. 강화학습 용어로 말하면 학습 환경의 상태 분포와 실제 배포 환경의 상태 분포가 달라진 것이다.
정책은 무엇을 믿고 움직이는가
Sim2Real을 이해하려면 정책이 무엇을 입력으로 받고 무엇을 출력하는지 다시 보아야 한다. 앞 장에서 다룬 로봇팔 목표 도달 예제를 떠올려 보자. 정책은 현재 관절 각도, 관절 속도, 엔드 이펙터 위치, 목표 위치 같은 상태를 입력으로 받고, 다음에 움직일 방향이나 관절 속도 명령을 출력했다.
시뮬레이션에서는 이 입력값이 매우 정확하다. joint_1 = 0.523이라고 나오면 실제로 그 값이라고 믿을 수 있다. 목표점도 정확히 주어진다. 로봇팔의 링크 길이도 모델에 적힌 그대로다. 정책이 어떤 행동을 출력하면 시뮬레이터는 그 행동이 거의 즉시 반영된 다음 상태를 계산한다.
하지만 실제 로봇에서는 정책이 보는 값이 완벽하지 않다. 관절 센서값은 조금 흔들릴 수 있고, 카메라 기반 목표 위치는 캘리브레이션 오차를 포함할 수 있다. 명령을 보낸 뒤 실제 모터가 움직이기까지 짧은 지연도 있다. 이 지연은 사람 눈에는 거의 보이지 않을 수 있지만, 초당 수십 번 제어하는 정책에게는 중요한 차이다.
정책은 자신이 보는 상태를 현실이라고 믿고 행동한다. 그런데 그 상태가 조금 늦거나 조금 틀리면, 정책은 잘못된 세계를 기준으로 행동하게 된다. 그래서 Sim2Real에서는 정책이 너무 예민하지 않도록 만들어야 한다. 입력이 조금 흔들려도, 모터가 조금 늦게 반응해도, 물체 위치가 조금 달라도 안전하게 목표에 가까워지는 정책이 필요하다.
좋은 Sim2Real 정책은 시험 문제의 정답만 외운 학생이 아니다. 문제의 숫자가 조금 바뀌고, 종이 색이 달라지고, 감독관이 말을 걸어도 침착하게 풀이할 수 있는 학생에 가깝다.
Domain Randomization: 일부러 세상을 흔들어 학습시키기
Sim2Real에서 가장 널리 쓰이는 아이디어 중 하나가 Domain Randomization이다. 한국어로는 도메인 랜덤화라고 부른다. 말 그대로 학습 중에 시뮬레이션 환경의 여러 요소를 무작위로 바꾸는 방법이다.
정책이 한 가지 시뮬레이션 세계에만 익숙해지면 현실의 작은 변화에도 취약해진다. 반대로 학습할 때부터 다양한 세계를 경험하면, 정책은 특정 값에 과하게 의존하지 않고 더 본질적인 패턴을 배우게 된다. 예를 들어 컵을 집는 정책이 항상 같은 밝기, 같은 위치, 같은 마찰의 컵만 보며 학습했다면 실제 환경에서 쉽게 실패한다. 하지만 학습 중에 컵 위치, 조명, 카메라 각도, 컵 질량, 테이블 마찰을 계속 바꿔 주면 정책은 "항상 이 픽셀에 컵이 있다"가 아니라 "관측된 컵 위치를 기준으로 접근해야 한다"는 더 일반적인 행동을 배우게 된다.
도메인 랜덤화는 크게 시각 랜덤화, 물리 랜덤화, 작업 랜덤화로 나눠 생각할 수 있다.
시각 랜덤화는 카메라 이미지가 입력일 때 특히 중요하다. 조명 밝기, 그림자, 배경 색, 물체 색상, 카메라 위치, 렌즈 왜곡, 이미지 노이즈 등을 바꾼다. 실제 로봇이 놓이는 장소는 실험실 하나라고 해도 시간에 따라 조명이 바뀌고, 사람 그림자가 지나가고, 테이블 위에 예상하지 못한 물건이 있을 수 있다. 시각 랜덤화는 이런 변화에 모델이 덜 놀라도록 만드는 예방주사다.
물리 랜덤화는 로봇과 물체의 물리적 성질을 바꾼다. 물체 질량, 마찰 계수, 관절 감쇠, 링크 질량, 그리퍼 힘, 충돌 반발, 테이블 높이 등을 조금씩 다르게 설정한다. 현실의 물리 파라미터를 정확히 알 수 없다면, 하나의 정답값을 맞추려 하기보다 가능한 범위를 정책이 견디도록 만드는 편이 더 실용적이다.
작업 랜덤화는 목표와 초기 상태를 바꾼다. 목표 위치, 물체 시작 위치, 로봇팔 초기 자세, 장애물 위치, 놓아야 할 영역을 매 에피소드마다 달리한다. 이는 정책이 특정 경로를 외우지 않게 만든다. 목표 도달 정책이라면 항상 같은 시작점에서 같은 목표점으로 움직이는 것이 아니라, 다양한 시작점과 목표점 사이를 이동해 보아야 한다.
간단한 랜덤화 설정은 다음처럼 생각할 수 있다.
randomization_config = {
"object_position": {
"x": [-0.05, 0.05],
"y": [-0.05, 0.05],
"z": [0.0, 0.0]
},
"object_mass": [0.08, 0.18],
"table_friction": [0.4, 1.2],
"joint_damping": [0.8, 1.2],
"camera_noise_std": [0.0, 0.01],
"action_delay_steps": [0, 2]
}이 코드는 특정 시뮬레이터에 바로 붙여 쓸 완성 코드라기보다 사고방식을 보여 주는 예시다. 중요한 점은 "현실과 똑같은 하나의 시뮬레이션"을 만드는 것이 아니라, "현실이 들어올 법한 여러 시뮬레이션"을 정책에게 보여 준다는 것이다.
랜덤화는 많이 할수록 좋을까
도메인 랜덤화는 강력하지만, 무조건 크게 흔드는 것이 답은 아니다. 처음부터 모든 값을 과하게 랜덤화하면 정책은 아무것도 배우지 못할 수 있다. 조명이 매번 극단적으로 바뀌고, 물체 질량이 말도 안 되게 달라지고, 관절 지연이 너무 크면 환경 자체가 혼란스러워진다. 학생에게 덧셈을 가르치기 전에 미적분, 물리, 철학 문제를 한꺼번에 내는 것과 비슷하다.
따라서 랜덤화에는 범위 설계가 필요하다. 현실에서 가능한 변화 범위를 대략 측정하고, 그 주변에서 시작하는 것이 좋다. 예를 들어 실제 컵의 질량이 100g에서 150g 사이라면 시뮬레이션에서는 처음에 90g에서 170g 정도로 설정할 수 있다. 이후 정책이 잘 학습되면 범위를 조금 넓혀 70g에서 200g까지 확장할 수 있다.
이처럼 학습 난이도를 점진적으로 높이는 방식을 커리큘럼이라고 한다. 로봇에게도 커리큘럼이 필요하다. 처음에는 깨끗한 환경에서 목표 도달을 배우고, 다음에는 목표 위치를 다양하게 바꾸고, 그다음에는 센서 노이즈를 추가하고, 마지막에는 물리 파라미터와 제어 지연까지 함께 흔드는 식이다.
좋은 랜덤화는 정책을 괴롭히는 것이 아니라 단련시키는 것이다. 현실에서 만날 가능성이 높은 불확실성을 중심으로, 학습 가능한 속도로 난이도를 높여야 한다.
센서 노이즈를 학습 중에 넣기
실제 센서는 조용하지 않다. 카메라 이미지는 조명과 노출에 따라 달라지고, 깊이 카메라는 반짝이는 표면이나 검은 물체에서 흔들릴 수 있다. 관절 인코더는 비교적 정확하지만 완전히 잡음이 없는 것은 아니다. 힘/토크 센서는 접촉 순간에 튀는 값을 낼 수 있다.
시뮬레이션에서 센서값을 너무 깨끗하게 제공하면 정책은 작은 오차에도 민감해진다. 그래서 학습 중에 일부러 센서 노이즈를 추가한다. 관절 각도에는 작은 가우시안 노이즈를 넣을 수 있다. 가우시안 노이즈는 평균 주변으로 흔들리는 임의의 값을 뜻한다. 예를 들어 실제 관절 각도가 0.5라면 정책에는 0.498이나 0.503처럼 약간 흔들린 값이 들어가도록 만드는 것이다.
카메라 이미지에는 밝기 변화, 블러, 픽셀 노이즈, 일부 영역 가림을 넣을 수 있다. 깊이값에는 거리별 오차를 추가할 수 있다. 물체가 멀수록 깊이 오차가 커지는 카메라라면, 시뮬레이션에서도 비슷하게 거리 기반 노이즈를 넣는 편이 좋다.
상태 벡터 기반 정책에서도 노이즈는 중요하다. 예를 들어 목표 도달 정책이 다음과 같은 상태를 입력으로 받는다고 하자.
state = [
joint_positions,
joint_velocities,
end_effector_position,
target_position
]학습 중에는 이 상태를 그대로 넣지 않고, 작은 노이즈가 섞인 상태를 넣을 수 있다.
noisy_joint_positions = joint_positions + normal_noise(std=0.002)
noisy_joint_velocities = joint_velocities + normal_noise(std=0.01)
noisy_target_position = target_position + normal_noise(std=0.005)여기서 std는 표준편차를 뜻한다. 값이 클수록 더 크게 흔들린다. 표준편차를 너무 크게 설정하면 정책이 정확한 위치를 알 수 없어 학습이 어려워진다. 반대로 너무 작으면 현실의 흔들림을 견디지 못한다. 결국 노이즈 크기도 실험 로그를 보며 조정해야 한다.
센서 노이즈를 넣을 때 기억할 점은 하나다. 정책이 깨끗한 데이터에만 의존하지 않도록 해야 한다. 현실의 센서는 언제나 약간 의심하면서 써야 한다.
제어 지연은 생각보다 무섭다
로봇에서 지연은 조용한 악당이다. 눈에 잘 보이지 않지만 정책의 판단을 계속 어긋나게 만든다. 정책은 현재 상태를 보고 행동을 결정한다. 그런데 실제 시스템에서는 상태 측정, 통신, 추론, 명령 전송, 모터 반응까지 시간이 걸린다. 정책이 행동을 출력한 순간의 세계와 실제 로봇이 움직이기 시작하는 순간의 세계가 다를 수 있다.
예를 들어 정책이 "지금 엔드 이펙터가 목표보다 왼쪽에 있으니 오른쪽으로 움직여야겠다"고 판단했다고 하자. 그런데 지연 때문에 명령이 늦게 적용되는 동안 로봇팔은 이미 관성으로 조금 더 움직였을 수 있다. 그러면 오른쪽으로 가라는 명령이 뒤늦게 적용되어 목표를 지나칠 수 있다. 이후 정책은 다시 반대 방향 명령을 내고, 또 지연이 생긴다. 이런 과정이 반복되면 목표 근처에서 떨림이 생긴다.
제어 지연에 대응하는 가장 단순한 방법은 학습 중에도 지연을 넣는 것이다. 정책이 출력한 행동을 즉시 적용하지 않고, 1~2스텝 뒤에 적용한다. 또는 이전 행동을 잠시 유지하게 한다.
action_buffer.append(action)
if len(action_buffer) > delay_steps:
applied_action = action_buffer.pop(0)
else:
applied_action = zero_action()
env.step(applied_action)이렇게 하면 정책은 "내 행동이 바로 반영되지 않을 수 있다"는 조건에서 학습한다. 처음에는 성능이 떨어질 수 있지만, 현실로 옮겼을 때 훨씬 안정적일 가능성이 커진다.
또 다른 방법은 정책 입력에 이전 행동이나 과거 상태를 함께 넣는 것이다. 현재 상태 하나만 보면 지연을 추정하기 어렵다. 하지만 최근 몇 스텝의 상태와 행동을 함께 보면 로봇이 어떤 방향으로 움직이고 있었는지 알 수 있다.
state_t = [obs_t, obs_t_minus_1, action_t_minus_1]이 구조는 정책에게 짧은 기억을 주는 것과 비슷하다. 로봇이 이미 오른쪽으로 움직이고 있었다면, 목표 근처에서 다시 강하게 오른쪽으로 미는 행동을 줄일 수 있다.
모터 오차와 저수준 제어기를 인정하기
강화학습 정책이 출력하는 행동은 실제 모터에 바로 전달되지 않는 경우가 많다. 대부분의 로봇은 내부에 저수준 제어기를 갖고 있다. 우리가 목표 관절 각도나 속도를 보내면, 로봇 컨트롤러가 PID 제어 같은 방식으로 모터 전류를 조정한다. PID 제어는 목표값과 현재값의 차이를 줄이기 위해 비례, 적분, 미분 성분을 조합하는 전통적인 제어 방법이다.
이 말은 강화학습 정책이 실제로 제어하는 대상이 "순수한 물리 로봇"이 아니라 "내부 제어기를 포함한 로봇 시스템"이라는 뜻이다. 시뮬레이션이 이 저수준 제어기의 특성을 잘 반영하지 못하면 Sim2Real 차이가 커진다.
현실의 모터에는 속도 제한과 토크 제한이 있다. 명령한 만큼 빠르게 움직이지 못할 수 있고, 무거운 물체를 들 때는 더 느려질 수 있다. 관절 마찰 때문에 작은 명령은 거의 움직임을 만들지 못할 수도 있다. 반대로 특정 구간에서는 예상보다 쉽게 움직일 수도 있다.
따라서 시뮬레이션에서도 행동을 현실처럼 제한해야 한다. 정책이 출력하는 값을 그대로 적용하지 말고, 실제 로봇에서 허용할 범위로 잘라야 한다.
raw_action = policy(obs)
safe_action = clip(raw_action, min_action, max_action)
safe_action = smooth(safe_action, previous_action, alpha=0.2)
robot.send_command(safe_action)여기서 clip은 행동값을 최소·최대 범위 안으로 제한하는 과정이다. smooth는 갑작스러운 변화가 생기지 않도록 이전 행동과 섞어 부드럽게 만드는 과정이다. 이런 제한은 학습 때부터 적용하는 것이 좋다. 학습할 때는 큰 행동을 마음껏 쓰게 해 놓고, 현실에서만 제한하면 정책은 갑자기 다른 몸을 갖게 된 것처럼 느낀다.
정책을 실제 로봇에 올릴 때는 "정책이 원하는 행동"과 "로봇에 보내도 되는 행동"을 구분해야 한다. 강화학습 모델이 아무리 똑똑해도 안전 필터 없이 모터 명령권을 직접 주는 것은 위험하다.
현실을 완벽히 맞출 것인가, 다양하게 만들 것인가
Sim2Real에는 두 가지 큰 접근이 있다. 하나는 시뮬레이션을 현실과 최대한 똑같이 맞추는 것이다. 이를 시스템 식별이라고 부를 수 있다. 로봇의 링크 질량, 마찰, 모터 응답, 센서 지연을 측정해 시뮬레이터 파라미터를 조정한다. 잘 맞춘 시뮬레이터는 학습 효율이 높고, 정책이 현실에서 더 예측 가능하게 움직인다.
다른 하나는 앞서 설명한 도메인 랜덤화처럼 정확한 하나의 세계 대신 여러 가능한 세계를 학습시키는 것이다. 현실의 정확한 마찰값을 모른다면 마찰값을 범위로 두고 랜덤하게 바꾼다. 센서 지연이 매번 조금 다르다면 지연도 범위로 둔다.
실전에서는 둘 중 하나만 쓰기보다 섞어서 쓴다. 먼저 현실을 대략 측정해 시뮬레이션의 중심값을 맞춘다. 그런 다음 그 주변을 랜덤화한다. 예를 들어 실제 테이블 마찰을 대략 측정해 0.8 정도라고 추정했다면, 학습에서는 0.6에서 1.0 사이로 랜덤화할 수 있다. 실제 카메라 지연이 평균 80ms라면, 학습에서는 50ms에서 120ms 사이로 랜덤화할 수 있다.
정확한 시뮬레이터를 만들려는 노력은 중요하다. 하지만 현실은 계속 변한다. 물체가 바뀌고, 로봇이 마모되고, 조명이 달라진다. 그래서 Sim2Real의 목표는 "현실 하나를 완벽히 복사하는 것"이 아니라 "현실의 변화 범위 안에서 무너지지 않는 정책을 만드는 것"에 가깝다.
로봇팔 목표 도달 예제에 적용해 보기
앞 장에서 만든 로봇팔 목표 도달 환경을 Sim2Real 관점으로 다시 설계해 보자. 목표는 간단하다. 엔드 이펙터가 주어진 3D 목표점에 가까워지도록 정책을 학습한다. 시뮬레이션에서는 이미 성공률이 높다. 이제 이 정책을 실제 로봇팔에 옮기고 싶다.
먼저 관측값을 점검한다. 실제 로봇에서 얻을 수 있는 관절 각도, 관절 속도, 엔드 이펙터 위치, 목표 위치가 시뮬레이션의 관측값과 같은 형식인지 확인해야 한다. 단위도 맞아야 한다. 각도는 라디안인지 도인지, 위치는 미터인지 밀리미터인지, 좌표계는 로봇 베이스 기준인지 카메라 기준인지 확인한다. 단위 하나가 틀리면 정책은 전혀 다른 세계를 보게 된다.
다음으로 행동값을 맞춘다. 정책이 관절 속도 명령을 출력한다면 실제 로봇도 같은 방식의 명령을 받을 수 있어야 한다. 만약 실제 로봇이 목표 관절 각도 명령만 받는다면, 정책 출력을 작은 각도 변화량으로 해석해 목표 관절 각도로 변환해야 한다.
current_q = robot.get_joint_positions()
delta_q = policy(obs)
target_q = current_q + clip(delta_q, -max_delta, max_delta)
target_q = enforce_joint_limits(target_q)
robot.send_joint_target(target_q)여기서 중요한 것은 max_delta를 작게 시작하는 것이다. 시뮬레이션에서는 한 스텝에 크게 움직여도 문제가 없었을 수 있다. 하지만 실제 로봇에서는 작은 변화부터 테스트해야 한다. 처음에는 로봇이 거의 기어가듯 움직이게 해야 한다.
학습 환경에는 다음과 같은 랜덤화를 추가할 수 있다.
목표 위치를 작업 공간 안에서 무작위로 바꾼다.
로봇 초기 자세를 안전한 범위 안에서 조금씩 다르게 시작한다.
관절 각도와 속도 관측값에 작은 노이즈를 넣는다.
행동 적용을 0~2스텝 정도 늦춘다.
관절 속도 제한과 행동 smoothing을 학습 중에도 적용한다.
링크 질량, 관절 감쇠, 마찰 계수를 작은 범위에서 랜덤화한다.
이렇게 학습한 정책은 한 가지 완벽한 시뮬레이션에서 최고 점수를 받는 정책보다 조금 덜 날카로울 수 있다. 목표까지 가는 속도도 약간 느려질 수 있다. 하지만 현실에서는 오히려 더 믿을 만하다. 로봇에서 중요한 것은 시뮬레이션 리더보드 점수가 아니라 실제 작업 성공률과 안전성이다.
실제 로봇에 올리기 전 체크리스트
시뮬레이션에서 충분히 학습했다고 해서 바로 실제 로봇을 움직이면 안 된다. Sim2Real의 마지막 단계는 기술보다 절차에 가깝다. 안전한 절차가 없으면 좋은 정책도 위험한 실험이 된다.
실제 로봇 적용 전에는 다음 항목을 차례로 확인해야 한다.
좌표계 확인: 목표 위치, 엔드 이펙터 위치, 로봇 베이스 좌표계가 일관되는지 확인한다.
단위 확인: 각도, 거리, 속도, 시간 단위가 시뮬레이션과 현실에서 같은지 확인한다.
행동 범위 확인: 정책 출력이 실제 로봇의 관절 한계, 속도 한계, 토크 한계를 넘지 않도록 제한한다.
제어 주기 확인: 정책 추론 주기와 로봇 명령 주기가 맞는지 확인한다.
정지 조건 확인: 비상 정지, 소프트 리밋, 충돌 감지, 작업 공간 이탈 감지가 동작하는지 확인한다.
이 체크리스트는 지루해 보이지만 매우 중요하다. 실제 로봇에서 가장 무서운 버그는 복잡한 알고리즘 오류가 아닐 때가 많다. 미터와 밀리미터를 혼동하거나, 좌표축 방향을 반대로 해석하거나, 관절 순서를 잘못 매핑하는 단순한 실수일 수 있다.
정책을 처음 실행할 때는 가능하면 shadow mode를 사용한다. shadow mode는 정책이 행동을 계산하되 실제 로봇에는 보내지 않는 방식이다. 대신 현재 상태에서 정책이 어떤 명령을 내려고 하는지 로그로 확인한다. 예를 들어 로봇팔이 목표에 가까워져야 하는 상황에서 정책이 관절 한계 방향으로 큰 명령을 계속 낸다면, 실제 실행 전에 문제를 발견할 수 있다.
그다음에는 모터 전원을 넣더라도 낮은 속도와 낮은 가속도로 시작한다. 작업 공간을 제한하고, 로봇 주변에 사람이 가까이 가지 않도록 하며, 손이 닿는 위치에 비상 정지 버튼을 둔다. 가능하다면 처음에는 엔드 이펙터에 날카롭거나 무거운 도구를 달지 않는다.
낮은 속도, 제한된 공간, 비상 정지
실제 로봇 테스트의 첫 원칙은 느리게 움직이는 것이다. 빠른 로봇은 멋져 보이지만, 디버깅 중인 정책에는 위험하다. 속도를 낮추면 사람이 이상 동작을 보고 멈출 시간이 생긴다. 또한 충돌이 발생하더라도 충격이 줄어든다.
두 번째 원칙은 작업 공간을 제한하는 것이다. 로봇팔이 움직여도 되는 영역을 작게 정하고, 그 밖으로 나가려 하면 즉시 멈추게 해야 한다. 예를 들어 목표 도달 실험이라면 테이블 위의 작은 박스 영역 안에서만 목표점을 생성한다. 엔드 이펙터가 이 영역을 벗어나면 정책이 어떤 명령을 내더라도 안전 모니터가 정지 명령을 보내야 한다.
if not workspace.contains(end_effector_position):
robot.stop()
return {"success": False, "reason": "workspace_violation"}세 번째 원칙은 비상 정지다. 비상 정지는 소프트웨어 버튼만으로 충분하지 않다. 가능하면 물리적인 비상 정지 장치가 있어야 한다. 네트워크가 끊기거나 프로그램이 멈춰도 로봇을 멈출 수 있어야 하기 때문이다. 소프트웨어 정지 조건도 여러 겹으로 두는 것이 좋다.
if collision_risk_detected:
robot.stop()
if joint_limit_near:
robot.stop()
if command_too_large:
robot.stop()
if sensor_timeout:
robot.stop()이런 정지 조건은 정책의 자유도를 제한한다. 하지만 실제 로봇에서는 자유로운 정책보다 안전한 정책이 우선이다. 강화학습 정책은 목표를 달성하려고 하지만, 안전 모니터는 하지 말아야 할 행동을 막는다. 두 역할을 분리해야 시스템이 견고해진다.
현실 테스트는 한 번에 하나씩 바꾼다
Sim2Real 디버깅에서 흔한 실수는 너무 많은 것을 한꺼번에 바꾸는 것이다. 새 정책을 학습했고, 노이즈도 추가했고, 카메라 위치도 바꿨고, 제어 주기도 수정했고, 실제 로봇의 속도 제한도 바꿨다면 실패했을 때 원인을 알기 어렵다.
현실 테스트는 한 번에 하나씩 바꾸는 것이 좋다. 먼저 시뮬레이션에서 정책이 정상인지 확인한다. 다음으로 shadow mode에서 출력 명령이 합리적인지 본다. 그다음 실제 로봇을 아주 낮은 속도로 움직인다. 목표 위치를 하나로 고정해 성공하는지 본다. 그 후 목표 위치를 조금씩 다양하게 바꾼다. 마지막으로 실제 작업 조건에 가까운 속도와 범위로 확장한다.
로그는 반드시 남겨야 한다. 적어도 시간, 관절 상태, 목표 위치, 정책 출력, 실제 명령, 엔드 이펙터 위치, 정지 이유는 기록해야 한다. 실패 영상도 매우 유용하다. 숫자 로그만 보면 "최종 거리 4cm 실패"일 뿐이지만, 영상을 보면 그리퍼가 목표 근처에서 흔들렸는지, 애초에 좌표계가 틀렸는지, 속도가 너무 빨랐는지 알 수 있다.
실패를 보면 다음 질문을 던진다.
정책이 본 관측값은 현실과 맞았는가?
정책 출력은 시뮬레이션에서 예상한 범위 안에 있었는가?
안전 필터가 행동을 지나치게 많이 잘라 내지는 않았는가?
실제 로봇의 응답이 시뮬레이션보다 느리거나 빠르지는 않았는가?
실패가 특정 위치, 특정 자세, 특정 조명에서 반복되는가?
이 질문에 답하다 보면 어떤 랜덤화를 추가해야 하는지 보인다. 특정 목표 위치에서만 실패한다면 목표 위치 샘플링을 보강해야 한다. 목표 근처에서 떨린다면 제어 지연과 행동 smoothing을 다시 봐야 한다. 카메라 기반 위치가 흔들린다면 센서 노이즈와 필터링을 조정해야 한다.
Sim2Real 파이프라인 정리
이제 전체 흐름을 하나의 파이프라인으로 정리해 보자. Sim2Real은 학습이 끝난 뒤 마지막에 붙이는 작업이 아니라, 환경 설계부터 함께 들어가야 한다.
첫째, 실제 로봇에서 사용할 관측과 행동을 먼저 정한다. 시뮬레이션에서만 얻을 수 있는 완벽한 정보에 의존하면 현실로 옮길 수 없다. 예를 들어 실제 로봇에서 물체의 정확한 질량을 알 수 없다면 정책 입력에도 질량을 넣지 않는 편이 낫다. 넣어야 한다면 추정 가능한 방식이어야 한다.
둘째, 시뮬레이션을 현실과 대략 맞춘다. 로봇 모델, 관절 한계, 속도 제한, 카메라 위치, 작업 공간 크기, 물체 크기 같은 기본 정보를 맞춘다. 이 단계가 엉망이면 랜덤화를 아무리 해도 어렵다.
셋째, 현실에서 달라질 수 있는 요소를 랜덤화한다. 물체 위치, 조명, 센서 노이즈, 물리 파라미터, 제어 지연을 현실적인 범위에서 흔든다. 처음부터 과하게 흔들지 말고 정책이 학습할 수 있는 수준에서 시작한다.
넷째, 안전 제약을 학습 중에도 적용한다. 행동 제한, 속도 제한, 작업 공간 제한, 충돌 회피 조건을 시뮬레이션에서도 사용한다. 현실에서만 갑자기 제한하면 정책이 예상하지 못한 동작을 보일 수 있다.
다섯째, 다양한 시뮬레이션 조건에서 평가한다. 학습에 사용하지 않은 랜덤 시드와 더 어려운 조건에서 성공률을 본다. 평균 성공률뿐 아니라 최악의 경우도 살펴야 한다. 로봇에서는 100번 중 99번 성공하는 정책도 남은 1번이 위험하면 그대로 쓰기 어렵다.
여섯째, 실제 로봇에서는 shadow mode, 저속 실행, 제한된 작업 공간, 비상 정지 순서로 점진적으로 적용한다. 바로 전체 작업을 맡기지 않는다.
마지막으로, 현실 로그를 다시 시뮬레이션 개선에 반영한다. 실제 실패가 반복되는 조건을 찾아 시뮬레이션에 추가하고 다시 학습한다. Sim2Real은 한 번에 끝나는 다리가 아니라, 시뮬레이션과 현실을 계속 왕복하는 반복 과정이다.
현실 적응력을 높이는 작은 습관들
Sim2Real 성능은 거창한 알고리즘만으로 결정되지 않는다. 작은 습관들이 모여 큰 차이를 만든다.
먼저 관측값을 정규화해야 한다. 정책 입력의 스케일이 학습 때와 현실에서 같아야 한다. 시뮬레이션에서 관절 각도를 [-1, 1] 범위로 정규화했다면 현실에서도 같은 방식으로 변환해야 한다. 목표 위치도 작업 공간 기준으로 정규화하면 학습이 안정적이고 배포 실수도 줄어든다.
둘째, 정책 출력에 급격한 변화가 생기지 않도록 한다. 실제 로봇은 갑작스러운 명령 변화에 취약하다. 행동 smoothing, 속도 제한, 가속도 제한을 두면 움직임이 훨씬 부드러워진다.
셋째, 성공 조건을 현실적으로 잡는다. 시뮬레이션에서는 목표와의 거리가 1mm 이하일 때만 성공으로 볼 수도 있다. 하지만 실제 로봇에서는 센서 오차와 제어 오차를 고려해 허용 범위를 정해야 한다. 목표 도달 작업이라면 1cm 이내를 성공으로 볼 수도 있고, 조립 작업처럼 정밀한 작업이라면 더 엄격한 조건이 필요할 수 있다.
넷째, 정책을 단독 영웅으로 만들지 않는다. 강화학습 정책은 좋은 행동을 제안하는 역할을 하고, 안전 제어기는 위험한 행동을 막으며, 상태 추정기는 센서값을 안정화하고, 상위 계획기는 실패 시 다시 목표를 정한다. 이런 계층 구조가 있어야 실제 시스템이 오래 버틴다.
흔한 실패와 대응 방법
Sim2Real 과정에서 자주 만나는 실패는 어느 정도 패턴이 있다. 실패를 무작정 "정책이 나쁘다"로 해석하면 개선 방향을 놓친다. 실패가 어디서 시작됐는지 나누어 보아야 한다.
| 증상 | 가능한 원인 | 대응 방법 |
|---|---|---|
| 목표 근처에서 떨림 | 제어 지연, 행동 변화가 너무 큼 | action delay 학습, smoothing, 속도 제한 |
| 항상 한쪽으로 빗나감 | 좌표계 오차, 캘리브레이션 오차 | 좌표 변환 재검증, 기준점 측정 |
| 특정 자세에서 실패 | 관절 한계, 특이점 근처, 학습 데이터 부족 | 초기 자세 랜덤화, 작업 공간 축소 |
| 실제 로봇이 너무 느림 | 모터 제한이 시뮬레이션에 반영되지 않음 | 속도·가속도 제한을 학습 환경에 추가 |
| 카메라 입력 정책이 조명에 취약 | 시각 랜덤화 부족 | 밝기, 색상, 그림자, 배경 랜덤화 |
| 물체 접촉 후 실패 | 마찰·질량·충돌 모델 차이 | 물리 랜덤화, 접촉 상황 데이터 보강 |
이 표는 정답지가 아니라 디버깅 지도에 가깝다. 실제 문제는 여러 원인이 겹쳐 생긴다. 예를 들어 목표 근처 떨림은 제어 지연 때문일 수도 있지만, 목표 위치 추정값이 흔들리기 때문일 수도 있다. 그러므로 로그와 영상을 함께 보며 원인을 좁혀야 한다.
현실에서는 보수적인 정책이 강하다
시뮬레이션에서는 빠르고 과감한 정책이 높은 보상을 받을 수 있다. 목표에 빨리 도달할수록 보상을 크게 주었다면, 정책은 최대한 빠르게 움직이려 할 것이다. 하지만 현실에서는 너무 빠른 정책이 위험하다. 특히 로봇팔 주변에 사람이나 깨지기 쉬운 물체가 있다면 속도 자체가 위험 요소다.
현실에 올릴 정책은 조금 보수적인 편이 좋다. 최단 시간보다 안정적인 접근을 우선하고, 목표 근처에서는 속도를 줄이며, 불확실성이 커지면 멈출 줄 알아야 한다. 로봇이 모르는 상황에서 계속 움직이는 것은 용감함이 아니라 위험함이다.
이 관점은 보상 설계와도 연결된다. 시뮬레이션에서 학습할 때부터 부드러운 움직임, 낮은 속도, 작은 행동 변화, 안전 거리 유지에 보상을 주거나 벌점을 줄 수 있다. 그러면 정책은 현실에 올렸을 때도 덜 공격적으로 움직인다.
reward = 0.0
reward += -distance_to_target
reward += -0.01 * action_magnitude
reward += -0.02 * action_change
reward += -1.0 * collision
reward += 10.0 * success여기서 action_magnitude는 행동의 크기, action_change는 이전 행동과 현재 행동의 차이, collision은 충돌 여부다. 이런 항목을 넣으면 정책은 목표만 보지 않고 움직임의 품질도 함께 고려하게 된다.
물론 보상을 너무 보수적으로 만들면 로봇이 움직이기를 꺼릴 수 있다. 그래서 실험이 필요하다. 빠르지만 위험한 정책과 느리지만 안전한 정책 사이에서 작업 목적에 맞는 균형을 찾아야 한다.
Sim2Real과 모방학습의 만남
강화학습만으로 Sim2Real을 해결하려고 하면 학습 비용이 커질 수 있다. 이때 앞서 배운 모방학습이 도움이 된다. 사람이 실제 로봇이나 시뮬레이터에서 안전한 동작을 시연하고, 정책이 그 행동을 먼저 따라 배우게 할 수 있다. 그런 다음 강화학습으로 미세 조정하면 무작위 시행착오를 줄일 수 있다.
예를 들어 로봇팔 목표 도달 작업에서 사람이 몇 가지 안전한 경로를 시연한다. 정책은 먼저 Behavior Cloning으로 "대체로 이런 방향으로 움직이면 된다"를 배운다. 이후 시뮬레이션에서 도메인 랜덤화와 함께 강화학습을 진행한다. 이렇게 하면 정책이 처음부터 이상한 행동을 덜 하고, 안전한 동작 패턴을 유지한 채로 다양한 조건에 적응할 수 있다.
실제 로봇에서 수집한 실패 데이터도 중요하다. 현실에서 특정 조건에 실패했다면 그 장면을 기록하고, 비슷한 조건을 시뮬레이션에 재현하거나 데이터셋에 추가한다. 정책은 성공 경험만으로 강해지지 않는다. 실패 직전의 상태와 회복 행동을 배울 때 더 튼튼해진다.
이 흐름은 이후 LLM 로봇 에이전트와도 연결된다. LLM은 "빨간 컵을 집어 오른쪽에 놓아줘"라는 명령을 작업 순서로 나눌 수 있다. 하지만 실제로 컵을 향해 움직이고, 잡고, 놓는 저수준 스킬은 Sim2Real을 견딜 수 있어야 한다. 언어 계획이 아무리 그럴듯해도 로봇 스킬이 현실에서 흔들리면 전체 에이전트는 실패한다.
실제 배포를 위한 계층 구조
Sim2Real을 고려한 로봇 시스템은 보통 한 개의 정책으로만 구성되지 않는다. 안전하고 실용적인 구조는 여러 계층으로 나뉜다.
가장 아래에는 실제 로봇 하드웨어와 저수준 제어기가 있다. 이 계층은 모터 명령, 관절 제어, 속도 제한, 토크 제한을 다룬다. 그 위에는 안전 필터가 있다. 정책이 낸 명령이 관절 한계나 작업 공간을 넘지 않는지 검사한다. 그 위에 강화학습 정책이나 모방학습 정책이 있다. 이 정책은 현재 상태를 보고 다음 행동을 제안한다. 더 위에는 작업 관리자가 있다. 목표를 설정하고, 성공 여부를 확인하고, 실패하면 재시도하거나 멈춘다.
이 구조를 단순화하면 다음과 같다.
while task_is_running:
obs = state_estimator.get_observation()
raw_action = policy(obs)
safe_action = safety_filter.apply(raw_action, obs)
if safety_filter.should_stop(obs, safe_action):
robot.stop()
task_manager.report_failure("safety_stop")
break
robot.send_command(safe_action)
task_manager.update(obs)여기서 중요한 점은 정책이 최종 권한을 갖지 않는다는 것이다. 정책은 제안하고, 안전 필터는 검문하고, 로봇 컨트롤러는 제한된 방식으로 실행한다. 이 원칙은 LLM 에이전트에서도 그대로 이어진다. LLM도 직접 모터를 움직이는 것이 아니라, 검증된 스킬을 호출하고 그 결과를 받아 다시 계획해야 한다.
Sim2Real은 현실을 존중하는 기술이다
Sim2Real을 처음 배우면 복잡하게 느껴질 수 있다. 시뮬레이션에서 학습한 정책 하나를 실제 로봇에 올리는 데 왜 이렇게 많은 절차가 필요한지 답답할 수도 있다. 하지만 로봇은 코드 안에만 사는 프로그램이 아니다. 로봇은 무게가 있고, 속도가 있고, 힘이 있으며, 사람과 같은 공간에서 움직인다.
시뮬레이션은 로봇에게 수많은 연습 기회를 준다. 현실은 그 연습이 진짜 의미를 갖는 무대다. 두 세계는 같지 않다. 그래서 우리는 두 세계 사이에 다리를 놓아야 한다. 그 다리가 Sim2Real이다.
좋은 Sim2Real 설계는 세 가지 태도를 갖는다. 첫째, 현실은 시뮬레이션보다 지저분하다는 사실을 인정한다. 둘째, 그 지저분함을 학습 중에 미리 경험시킨다. 셋째, 실제 로봇에서는 언제나 안전을 먼저 둔다.
이제 강화학습 파트의 큰 그림이 완성되었다. 에이전트는 보상을 통해 행동을 개선할 수 있고, 시뮬레이션에서 빠르게 연습할 수 있으며, Sim2Real을 통해 현실로 조심스럽게 넘어올 수 있다. 다음 부터는 이 로봇 스킬들을 더 높은 수준에서 다루는 방법을 살펴본다. 사람이 자연어로 명령하면, LLM은 그 명령을 이해하고 작업을 나누며, 로봇은 검증된 스킬을 호출해 실제 세계에서 행동한다. 결국 우리가 만들고 싶은 것은 생각만 하는 AI가 아니라, 안전하게 보고 판단하고 움직이는 AI 로봇 에이전트다.
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.