로봇의 세계를 숫자로 번역하기
이전 장에서 우리는 로봇 시연 데이터를 어떻게 수집할지 살펴보았다. 카메라 이미지, 관절 상태, 엔드 이펙터 자세, 그리퍼 상태, 조이스틱 명령을 같은 시간에 맞춰 저장했다. 이제 한 단계 더 들어가야 한다.
데이터를 저장했다고 해서 곧바로 모델이 배울 수 있는 것은 아니다. 사람은 영상을 보면 "아, 컵이 왼쪽에 있고 그리퍼가 조금 위에 있구나"라고 이해한다. 하지만 모델이 보는 것은 숫자 배열이다. 이미지도 숫자이고, 관절각도 숫자이고, 그리퍼가 열렸는지 닫혔는지도 숫자다. 모방학습에서 가장 중요한 설계 중 하나는 바로 이 질문이다.
"로봇이 보는 세계와 로봇이 해야 할 움직임을 어떤 숫자 구조로 표현할 것인가?"
상태는 모델의 입력이다. 행동은 모델의 출력이다. 모방학습은 결국 현재 상태를 보고 다음 행동을 맞히는 문제로 바뀐다. 사람이 시연한 데이터를 이용해 "이런 장면에서는 이렇게 움직였다"는 관계를 학습하는 것이다.
이번 장에서는 이미지 상태, 깊이 정보, 객체 좌표, 관절각, 엔드 이펙터 위치, 그리퍼 상태를 어떻게 표현하는지 살펴본다. 이어서 절대 위치 명령, 상대 이동량, 관절 속도, 그리퍼 열림값 같은 행동 벡터를 설계하는 방법을 다룬다. 마지막에는 시연 데이터에서 state_t → action_t 학습 쌍을 만드는 절차를 정리한다.

상태는 로봇이 결정을 내리는 데 필요한 단서다
상태, 즉 state는 현재 순간의 상황을 나타내는 정보다. 로봇팔이 컵을 집는 장면을 생각해 보자. 모델이 다음 행동을 결정하려면 무엇을 알아야 할까?
컵이 어디에 있는지 알아야 한다. 그리퍼가 컵보다 위에 있는지, 옆에 있는지, 이미 컵을 잡았는지도 알아야 한다. 로봇팔의 관절이 현재 어떤 자세인지도 중요하다. 같은 컵 위치라도 로봇팔이 왼쪽에서 접근 중인지 오른쪽에서 접근 중인지에 따라 다음 행동이 달라질 수 있기 때문이다.
따라서 로봇 조작에서 상태는 보통 두 종류의 정보를 함께 담는다. 하나는 외부 세계에 대한 정보이고, 다른 하나는 로봇 자신의 몸에 대한 정보다. 외부 세계 정보는 카메라 이미지, 깊이 이미지, 객체 위치 같은 시각 입력이다. 로봇 자신의 몸 정보는 관절각, 관절 속도, 엔드 이펙터 자세, 그리퍼 열림 정도 같은 내부 상태다.
사람으로 비유하면 간단하다. 우리는 눈으로 컵을 보고, 동시에 자기 손이 어디에 있는지도 느낀다. 눈은 컵의 위치를 알려 주고, 몸의 감각은 손과 팔의 자세를 알려 준다. 로봇도 마찬가지다. 카메라만 보고는 로봇팔이 현재 어디까지 움직였는지 알기 어렵고, 관절값만 보고는 컵이 어디 있는지 알 수 없다. 좋은 상태 표현은 이 둘을 자연스럽게 연결한다.
이미지 상태: 픽셀을 모델의 입력으로 만들기
가장 직관적인 시각 상태는 RGB 이미지다. 카메라에서 들어온 컬러 이미지는 보통 높이, 너비, 채널을 가진 숫자 배열이다. 예를 들어 640×480 컬러 이미지는 480 × 640 × 3 크기의 배열로 볼 수 있다. 여기서 3은 빨강, 초록, 파랑 채널을 의미한다.
하지만 학습 모델에 원본 이미지를 그대로 넣는 경우는 드물다. 이미지가 너무 크면 계산량이 많아지고, 카메라 해상도나 저장 방식에 따라 입력 크기가 달라질 수 있다. 그래서 보통 이미지를 일정한 크기로 줄이고, 픽셀값을 정규화한다.
예를 들어 다음과 같은 이미지 상태를 만들 수 있다.
rgb_t: [3, 224, 224]
value range: 0.0 ~ 1.0여기서 [3, 224, 224]는 채널, 높이, 너비 순서의 텐서다. 텐서는 다차원 숫자 배열이라고 생각하면 된다. 원래 이미지가 640×480이더라도 학습용으로는 224×224로 줄일 수 있다. 픽셀값도 0부터 255까지의 정수 대신 0.0부터 1.0까지의 실수로 바꾸면 모델이 다루기 쉬워진다.
이미지를 사용할 때 중요한 것은 일관성이다. 학습할 때는 카메라 이미지의 위쪽이 항상 같은 방향이어야 하고, 컬러 채널 순서도 일정해야 한다. OpenCV는 기본적으로 BGR 순서를 쓰는 경우가 많고, 딥러닝 모델은 RGB를 기대하는 경우가 많다. 이 차이를 놓치면 사람 눈에는 그럴듯한 이미지라도 모델에는 색이 뒤바뀐 이상한 입력이 들어간다.
또 하나의 선택은 전체 이미지를 쓸지, 관심 영역만 잘라 쓸지다. 책상 위 컵을 집는 작업에서 카메라가 넓은 방 전체를 보고 있다면, 이미지 대부분은 작업과 관계없는 배경일 수 있다. 이때 객체 검출 결과를 이용해 컵 주변을 crop해서 모델에 넣으면 모델이 더 집중하기 쉬워진다. 반대로 장애물, 목표 영역, 다른 물체와의 관계가 중요하다면 전체 장면을 보는 편이 낫다.
전체 이미지 입력:
rgb_t = full camera image
관심 영역 입력:
rgb_crop_t = crop around target object초기 프로젝트에서는 전체 이미지를 일정 크기로 줄여 쓰는 방식이 가장 단순하다. 이후 모델이 배경에 흔들리거나 물체가 작아서 잘 못 찾는다면, 객체 중심 crop이나 세그멘테이션 마스크를 추가하는 방식으로 발전시킬 수 있다.
깊이 정보: 픽셀에 거리 감각을 더하기
RGB 이미지는 색과 모양을 알려 주지만 거리 정보를 직접 담고 있지는 않다. 사람은 두 눈과 경험으로 거리를 추정하지만, 로봇은 물체를 실제로 집어야 하므로 거리 정보가 매우 중요하다. 컵이 그리퍼보다 5cm 앞에 있는지 20cm 앞에 있는지 모르면 정확한 접근이 어렵다.
깊이 카메라를 사용하면 각 픽셀마다 카메라로부터의 거리를 얻을 수 있다. 이를 깊이 이미지, 또는 depth image라고 한다. RGB 이미지가 [3, H, W]라면 깊이 이미지는 보통 [1, H, W] 형태로 표현할 수 있다.
depth_t: [1, 224, 224]
unit: meter깊이값의 단위는 반드시 명확해야 한다. 어떤 센서는 밀리미터 단위의 정수로 깊이를 저장하고, 어떤 파이프라인은 미터 단위의 실수로 변환한다. 500이라는 값이 500mm인지 500m인지 헷갈리면 로봇은 책상 위 컵을 벽 너머의 물체처럼 해석할 수도 있다.
깊이 이미지는 강력하지만 완벽하지 않다. 반짝이는 물체, 투명한 컵, 검은 표면, 얇은 모서리에서는 깊이값이 비거나 흔들릴 수 있다. 그래서 깊이 상태를 만들 때는 너무 가까운 값과 너무 먼 값을 잘라내고, 비어 있는 픽셀을 어떻게 처리할지 정해야 한다.
예를 들어 책상 위 조작 작업에서는 0.2m보다 가까운 값과 1.5m보다 먼 값을 제외할 수 있다. 비어 있는 깊이값은 0으로 두거나, 주변 값으로 보간하거나, 별도의 마스크로 표시할 수 있다. 중요한 것은 학습과 실행에서 같은 규칙을 사용하는 것이다.
RGB와 깊이를 함께 쓰는 방법도 여러 가지다. 가장 단순한 방식은 RGB 3채널과 depth 1채널을 합쳐 4채널 입력으로 만드는 것이다.
rgbd_t: [4, 224, 224]
channels: R, G, B, Depth다른 방식은 RGB 이미지는 CNN으로 처리하고, depth 이미지는 별도 네트워크로 처리한 뒤 중간 특징을 합치는 것이다. 처음부터 복잡하게 시작할 필요는 없다. 중요한 것은 깊이값이 어떤 단위와 범위를 갖는지, RGB와 depth가 같은 시점과 같은 시야에 정렬되어 있는지 확인하는 것이다.
객체 좌표: 이미지를 더 간결한 숫자로 바꾸기
모든 상황에서 원본 이미지를 그대로 모델에 넣어야 하는 것은 아니다. 앞선 장들에서 우리는 객체 검출, 세그멘테이션, 깊이 추정, 좌표 변환을 다루었다. 이 파이프라인을 이용하면 "컵이 이미지 안에 있다"는 정보를 더 직접적인 숫자로 바꿀 수 있다.
예를 들어 객체 검출 모델이 빨간 컵을 찾았다고 하자. 그러면 바운딩 박스 중심, 박스 크기, confidence 점수, 클래스 ID를 상태로 넣을 수 있다.
object_2d_t = [class_id, u, v, w, h, confidence]여기서 u, v는 이미지에서 객체 중심의 픽셀 좌표이고, w, h는 바운딩 박스의 너비와 높이다. 하지만 픽셀 좌표를 그대로 쓰면 해상도에 따라 값의 범위가 달라진다. 그래서 보통 0부터 1 사이로 정규화한다.
u_norm = u / image_width
v_norm = v / image_height
w_norm = w / image_width
h_norm = h / image_height깊이 정보와 카메라-로봇 좌표 변환을 결합하면 객체의 3D 위치도 상태로 넣을 수 있다.
object_3d_t = [x_obj, y_obj, z_obj]
frame: robot_base
unit: meter이 표현은 매우 강력하다. 모델이 이미지에서 컵의 위치를 직접 찾아내지 않아도, 이미 계산된 3D 좌표를 보고 그리퍼를 어디로 움직일지 배울 수 있기 때문이다. 초기 모방학습 실험에서는 이미지 전체를 넣는 것보다 이런 구조화된 상태가 더 안정적일 때도 많다.
다만 객체 좌표 기반 표현에는 약점도 있다. 객체 검출이 실패하면 상태 자체가 흔들린다. 컵을 잘못 찾으면 모델은 잘못된 좌표를 믿고 움직인다. 또한 객체의 자세, 잡을 수 있는 영역, 주변 장애물 같은 정보가 숫자 몇 개에 모두 담기지는 않는다.
그래서 실전에서는 이미지와 객체 좌표를 함께 쓰는 경우가 많다. 이미지는 풍부한 장면 정보를 제공하고, 객체 좌표는 목표 위치를 명확하게 알려 준다.
visual_state_t = {
rgb: [3, 224, 224],
depth: [1, 224, 224],
object_3d: [x_obj, y_obj, z_obj],
object_confidence: [score]
}객체가 여러 개라면 표현이 조금 더 복잡해진다. 컵, 상자, 장애물, 목표 영역을 모두 상태에 넣고 싶을 수 있다. 이때는 최대 객체 수를 정하고, 부족한 부분은 0으로 채우며, 실제 객체인지 아닌지를 나타내는 mask를 함께 넣는 방식을 사용할 수 있다.
objects_t: [N, D]
object_mask_t: [N]
N = 최대 객체 수
D = 객체 하나를 표현하는 벡터 길이예를 들어 최대 5개의 객체를 다룬다면 objects_t는 [5, 8] 같은 배열이 될 수 있다. 실제로 2개만 검출되었다면 나머지 3개는 0으로 채우고, object_mask에서 사용하지 않는 항목을 0으로 표시한다. 이렇게 하면 모델 입력 크기를 일정하게 유지할 수 있다.
로봇 내부 상태: 로봇 자신의 몸을 숫자로 느끼기
시각 상태가 로봇 바깥 세계를 보여 준다면, 로봇 내부 상태는 로봇 자신의 몸을 보여 준다. 이를 proprioception이라고 부르기도 한다. 우리말로는 고유수용감각에 가깝다. 사람이 눈을 감아도 자기 팔이 대략 어디에 있는지 느끼는 것처럼, 로봇도 관절 센서와 제어 상태를 통해 자기 자세를 안다.
가장 기본적인 내부 상태는 관절각이다. 6자유도 로봇팔이라면 관절각은 보통 6개의 숫자로 표현된다.
q_t = [q1, q2, q3, q4, q5, q6]
unit: radian여기서 q는 joint position, 즉 관절 위치를 뜻한다. 회전 관절에서는 각도이고, 직선으로 움직이는 관절에서는 거리일 수 있다. 로봇팔에서는 보통 라디안 단위를 사용한다. 도(degree)와 라디안(radian)을 섞으면 큰 문제가 생긴다. 90도는 약 1.57라디안이다. 모델과 제어기가 서로 다른 단위를 기대하면 로봇은 전혀 다른 자세로 움직이게 된다.
관절 속도도 자주 사용한다.
dq_t = [dq1, dq2, dq3, dq4, dq5, dq6]
unit: radian/second관절 속도는 현재 로봇이 어느 방향으로 얼마나 빠르게 움직이고 있는지 알려 준다. 상태에 속도를 넣으면 모델은 정지 상태와 이동 중인 상태를 구분할 수 있다. 예를 들어 그리퍼가 컵을 향해 이미 내려가는 중이라면, 다음 행동은 더 내려가기보다 속도를 줄이는 것일 수 있다.
필요하다면 관절 토크나 전류값도 넣을 수 있다. 접촉이 중요한 작업에서는 힘 정보가 도움이 된다. 하지만 처음부터 모든 값을 넣을 필요는 없다. Pick and Place의 기본 모방학습에서는 관절각, 관절 속도, 엔드 이펙터 자세, 그리퍼 상태만으로도 시작할 수 있다.
엔드 이펙터 자세는 로봇 조작에서 특히 중요하다. 엔드 이펙터는 로봇팔 끝, 즉 그리퍼가 달린 부분이다. 이 위치와 방향을 알면 "손이 지금 어디에 있는가"를 직접 표현할 수 있다.
ee_position_t = [x, y, z]
ee_orientation_t = [qx, qy, qz, qw]
frame: robot_base위치 [x, y, z]는 로봇 기준 좌표계에서의 3D 위치다. 방향은 오일러 각도 [roll, pitch, yaw]로 표현할 수도 있고, 쿼터니언 [qx, qy, qz, qw]로 표현할 수도 있다. 쿼터니언은 회전을 표현하는 네 개의 숫자다. 처음에는 낯설지만, 특정 각도에서 표현이 불안정해지는 문제를 줄일 수 있어 로봇과 3D 그래픽에서 자주 쓰인다.
초기 실험에서는 작업을 단순화해 방향을 고정하고 위치만 학습할 수도 있다. 예를 들어 위에서 아래로 컵을 집는 작업이라면 그리퍼 방향을 항상 아래로 유지하고, 모델은 [x, y, z] 이동과 그리퍼 열림만 예측하게 만들 수 있다. 이렇게 하면 학습 문제가 훨씬 쉬워진다.
그리퍼 상태도 빼놓으면 안 된다. 같은 이미지와 같은 로봇 자세라도 그리퍼가 열려 있을 때와 닫혀 있을 때 다음 행동이 달라질 수 있다. 그리퍼 상태는 보통 0부터 1 사이의 값으로 표현한다.
gripper_t = 0.0 # 완전히 열림
gripper_t = 1.0 # 완전히 닫힘물론 실제 하드웨어에 따라 반대일 수도 있다. 어떤 시스템은 0이 닫힘이고 1이 열림일 수 있다. 중요한 것은 데이터셋 전체에서 의미가 일관되어야 한다는 점이다. 이 규칙은 코드와 문서에 반드시 남겨야 한다.
하나의 상태 벡터로 합치기
이제 시각 상태와 로봇 내부 상태를 어떻게 합칠지 생각해 보자. 모든 정보를 무조건 하나의 긴 벡터로 펼칠 수도 있지만, 이미지처럼 큰 배열과 관절값처럼 작은 벡터는 성격이 다르다. 그래서 실제 모델에서는 보통 여러 입력을 딕셔너리 형태로 보관하고, 모델 내부에서 적절히 처리한다.
예를 들어 한 시점의 상태는 다음처럼 표현할 수 있다.
{
"rgb": "images/rgb_000123.jpg",
"depth": "depth/depth_000123.png",
"robot_state": {
"joint_position": [0.12, -0.84, 1.31, -1.02, 0.44, 0.08],
"joint_velocity": [0.01, -0.02, 0.00, 0.04, -0.01, 0.00],
"ee_position": [0.42, -0.15, 0.28],
"ee_orientation": [0.0, 0.707, 0.0, 0.707],
"gripper": 0.0
},
"object_state": {
"target_class": "red_cup",
"target_position": [0.50, -0.10, 0.06],
"confidence": 0.94
}
}학습 시점에는 문자열 경로로 되어 있는 이미지를 실제 배열로 읽고, 관절값과 객체 좌표를 숫자 벡터로 바꾼다. 모델 입장에서 최종 입력은 다음과 같은 구조가 된다.
image_feature_t = CNN(rgb_t)
robot_vector_t = [q_t, dq_t, ee_pose_t, gripper_t]
object_vector_t = [x_obj, y_obj, z_obj, confidence]
state_feature_t = concat(image_feature_t, robot_vector_t, object_vector_t)여기서 concat은 여러 벡터를 이어 붙인다는 뜻이다. 이미지는 CNN 같은 시각 모델을 통과해 특징 벡터가 되고, 관절값과 객체 좌표는 작은 숫자 벡터로 들어간다. 그런 다음 이들을 합쳐 다음 행동을 예측한다.
처음부터 너무 복잡한 입력을 설계하면 디버깅이 어려워진다. 따라서 작은 단계로 시작하는 것이 좋다. 예를 들어 첫 번째 실험은 객체 3D 위치와 엔드 이펙터 위치만 사용해 볼 수 있다.
state_t = [
x_obj, y_obj, z_obj,
x_ee, y_ee, z_ee,
gripper
]이 표현은 이미지 기반 정책보다 단순하지만, 로봇이 물체 쪽으로 접근하는 기본 동작을 배우기에는 충분할 수 있다. 이후 이미지, 깊이, 관절 속도, 목표 위치를 하나씩 추가하면서 성능 변화를 확인하면 된다.
절대 위치 행동: 어디로 가야 하는가
상태가 입력이라면 행동은 모델이 출력해야 하는 값이다. 행동 표현을 어떻게 설계하느냐에 따라 학습 난이도와 실행 안정성이 크게 달라진다.
가장 먼저 생각할 수 있는 행동은 절대 위치 명령이다. 모델이 다음 목표 엔드 이펙터 자세를 직접 출력하는 방식이다.
action_t = [x_target, y_target, z_target, qx, qy, qz, qw, gripper]이 표현은 의미가 명확하다. 모델이 "그리퍼를 로봇 기준 좌표계의 이 위치로 보내라"는 목표를 출력한다. MoveIt2 같은 경로 계획기와 연결하기도 쉽다. 목표 자세를 주면 계획기가 현재 자세에서 목표 자세까지의 경로를 만들 수 있기 때문이다.
하지만 모방학습에서는 절대 위치 행동이 부담스러울 때가 많다. 같은 장면이라도 로봇의 현재 자세가 조금 다르면 목표 위치가 달라져야 할 수 있다. 또한 모델이 작업 공간 바깥의 위치를 출력하면 안전 필터가 반드시 막아야 한다. 절대 좌표는 작은 예측 오차가 실제 위치 오차로 바로 이어진다.
절대 위치 명령은 고수준 스킬을 학습하거나, 목표점이 명확한 작업에 잘 맞는다. 예를 들어 "검출된 컵 위 10cm 지점으로 이동하기"처럼 목표가 정해져 있다면 절대 pose를 출력하는 방식이 자연스럽다. 하지만 저수준 연속 제어를 모방할 때는 상대 이동량이 더 다루기 쉬운 경우가 많다.
상대 이동량 행동: 지금 위치에서 조금 움직이기
모방학습에서 자주 쓰는 행동 표현은 상대 이동량이다. 현재 엔드 이펙터 위치를 기준으로 다음 순간에 얼마나 움직일지를 출력한다.
action_t = [dx, dy, dz, droll, dpitch, dyaw, gripper]여기서 dx, dy, dz는 현재 위치에서 x, y, z 방향으로 얼마나 움직일지를 나타낸다. 예를 들어 dx = 0.01이면 x 방향으로 1cm 이동하라는 뜻이다. 회전까지 학습한다면 droll, dpitch, dyaw를 포함할 수 있고, 방향을 고정한다면 위치 이동량과 그리퍼만 사용할 수 있다.
action_t = [dx, dy, dz, gripper]상대 이동량의 장점은 행동 범위가 작고 안정적이라는 점이다. 모델이 매번 1m 떨어진 목표를 맞히는 것이 아니라, 현재 위치에서 몇 mm 또는 몇 cm씩 움직이는 명령을 예측한다. 로봇은 이 작은 명령을 반복 실행하면서 목표에 가까워진다. 이는 사람이 컵을 집을 때 손을 한 번에 정확한 위치로 순간이동시키지 않고, 보면서 조금씩 조정하는 방식과 비슷하다.
다만 상대 이동량을 사용할 때는 기준 좌표계를 반드시 정해야 한다. dx가 로봇 base 좌표계의 x 방향인지, 엔드 이펙터 자신이 바라보는 앞 방향인지 명확해야 한다.
base frame action:
dx, dy, dz는 로봇 기준 좌표계 방향
ee frame action:
dx, dy, dz는 그리퍼 자신의 좌표계 방향로봇 기준 좌표계는 해석이 쉽고 시각화하기 좋다. 엔드 이펙터 기준 좌표계는 "그리퍼가 바라보는 방향으로 전진" 같은 명령을 표현하기 좋다. 어느 쪽이든 괜찮지만, 학습 데이터와 실행 코드가 같은 기준을 써야 한다. 데이터는 base frame인데 실행은 ee frame으로 해석하면 로봇은 엉뚱한 방향으로 움직인다.
상대 이동량에는 보통 제한을 둔다. 예를 들어 한 스텝에서 최대 2cm 이상 움직이지 못하게 한다.
dx, dy, dz ∈ [-0.02, 0.02] meter이 제한은 안전에도 좋고 학습에도 좋다. 모델이 가끔 큰 값을 출력하더라도 실행 전에 잘라낼 수 있다. 이를 clipping이라고 한다. 로봇 제어에서는 모델을 믿기보다, 모델 출력이 안전한 범위 안에 있는지 항상 확인해야 한다.
관절 기준 행동: 로봇팔의 각 관절을 직접 움직이기
또 다른 행동 표현은 관절 기준 명령이다. 모델이 각 관절의 목표 각도, 각도 변화량, 또는 관절 속도를 출력하는 방식이다.
절대 관절 목표:
action_t = [q1_target, q2_target, q3_target, q4_target, q5_target, q6_target, gripper]
관절 변화량:
action_t = [dq1, dq2, dq3, dq4, dq5, dq6, gripper]
관절 속도:
action_t = [v1, v2, v3, v4, v5, v6, gripper]관절 기준 행동은 로봇 제어기와 직접 연결하기 쉽다. 많은 로봇은 내부적으로 관절 명령을 받아 움직인다. 또한 역기구학 계산이 필요 없을 수 있다. 역기구학은 원하는 엔드 이펙터 위치를 만들기 위해 각 관절을 어떻게 움직여야 하는지 계산하는 과정이다.
하지만 관절 기준 행동은 사람에게 직관적이지 않다. 첫 번째 관절을 조금 돌리면 그리퍼가 왼쪽으로 가는지, 앞으로 가는지, 아래로 가는지는 현재 자세에 따라 달라진다. 모델도 객체와 그리퍼의 공간적 관계를 배우기 어려울 수 있다. 컵이 그리퍼 오른쪽에 있으니 y 방향으로 움직이라는 식의 관계가 관절 공간에서는 복잡하게 휘어진다.
그래서 Pick and Place 같은 작업에서는 엔드 이펙터 기준 행동으로 시작하는 경우가 많다. 다만 로봇 하드웨어나 시뮬레이터가 관절 속도 제어를 기본으로 제공한다면 관절 기준 행동도 좋은 선택이다. 중요한 것은 행동 표현이 로봇 제어 방식과 잘 맞아야 한다는 점이다.
그리퍼 행동: 열기와 닫기를 어떻게 표현할까
그리퍼 행동은 단순해 보이지만 실제로는 꽤 중요하다. 컵 집기에서 그리퍼를 언제 닫느냐가 성공과 실패를 가른다. 너무 일찍 닫으면 컵을 밀고, 너무 늦게 닫으면 빈 공간을 집는다.
가장 단순한 표현은 이진 값이다.
gripper_action = 0 # 열기
gripper_action = 1 # 닫기이 방식은 이해하기 쉽고, 분류 문제처럼 학습할 수 있다. 하지만 그리퍼가 중간 정도 열려야 하는 작업에는 부족할 수 있다. 물체 크기에 따라 그리퍼를 조금만 닫아야 하거나, 부드럽게 조여야 하는 경우도 있다.
연속값으로 표현할 수도 있다.
gripper_action = 0.0 # 완전히 열기
gripper_action = 1.0 # 완전히 닫기또는 실제 그리퍼 폭을 미터 단위로 표현할 수 있다.
gripper_width_target = 0.04 # 4cm초기 프로젝트에서는 이진 열기·닫기 명령으로 충분한 경우가 많다. 특히 단순한 평행 그리퍼로 컵이나 블록을 집는 작업이라면 open과 close만으로 시작할 수 있다. 나중에 다양한 크기의 물체를 다루거나 힘 조절이 필요해지면 연속 그리퍼 명령으로 확장하면 된다.
그리퍼 상태와 그리퍼 행동을 혼동하지 않는 것도 중요하다. 상태는 현재 그리퍼가 열려 있는 정도이고, 행동은 다음에 그리퍼를 어떻게 움직일지다.
gripper_state_t = 현재 열림 정도
gripper_action_t = 지금 내릴 열기/닫기 명령예를 들어 그리퍼가 현재 열려 있고, 컵 양옆에 도달했다면 상태의 gripper_state_t는 0.0에 가깝고, 행동의 gripper_action_t는 1.0이 될 수 있다. 두 값을 같은 이름으로 저장하면 나중에 데이터셋을 읽을 때 혼란이 생긴다.
행동 표현을 고르는 기준
행동 표현에는 정답이 하나만 있는 것이 아니다. 로봇 종류, 제어 주기, 작업 난이도, 시연 방식, 안전 요구사항에 따라 선택이 달라진다. 다만 다음 기준을 생각하면 설계가 쉬워진다.
| 행동 표현 | 장점 | 한계 | 잘 맞는 경우 |
|---|---|---|---|
| 절대 엔드 이펙터 자세 | 목표가 명확하고 경로 계획기와 연결하기 쉽다 | 큰 오차가 위험할 수 있고 작업 공간 제한이 필요하다 | 고수준 목표 pose 예측, 스킬 목표 생성 |
| 상대 엔드 이펙터 이동량 | 작은 보정 행동을 배우기 쉽고 안정적이다 | 많은 스텝을 반복해야 하며 기준 좌표계를 명확히 해야 한다 | Pick and Place 저수준 제어, 시각 피드백 제어 |
| 관절 목표 또는 관절 속도 | 로봇 제어기와 직접 연결하기 쉽다 | 공간적 의미가 직관적이지 않고 로봇 구조에 의존한다 | 특정 로봇팔 전용 정책, 시뮬레이션 제어 |
| 고수준 스킬 명령 | 계획 구조와 잘 맞고 안전 제어를 넣기 쉽다 | 세밀한 접촉 동작을 직접 배우기 어렵다 | LLM 에이전트의 스킬 호출, 작업 순서 학습 |
처음 모방학습을 구현한다면 상대 엔드 이펙터 이동량과 그리퍼 명령으로 시작하는 것을 추천한다. 예를 들어 다음과 같은 행동 벡터는 단순하면서도 강력하다.
action_t = [dx, dy, dz, dyaw, gripper_close]그리퍼 방향을 고정할 수 있다면 dyaw도 빼고 더 단순하게 만들 수 있다.
action_t = [dx, dy, dz, gripper_close]이 정도만으로도 "컵에 접근하기", "아래로 내려가기", "그리퍼 닫기", "위로 들어 올리기" 같은 기본 동작을 표현할 수 있다.
현재 상태에서 다음 행동을 예측한다
이제 모방학습의 핵심 구조를 만들 수 있다. 시연 데이터는 시간 순서로 저장되어 있다.
sample_0, sample_1, sample_2, ..., sample_T각 sample에는 이미지, 로봇 상태, 객체 상태, 행동 명령이 들어 있다. Behavior Cloning에서는 이 데이터를 입력과 정답으로 나눈다.
input = state_t
label = action_t
model(state_t) ≈ action_t즉, 모델은 현재 상태를 보고 사람이 그 순간에 내린 행동을 따라 하도록 학습한다. 이것이 가장 기본적인 형태다.
예를 들어 samples.jsonl의 한 줄이 다음과 같다고 하자.
{
"t": 12.340,
"rgb": "images/rgb_000123.jpg",
"depth": "depth/depth_000123.png",
"joint_position": [0.12, -0.84, 1.31, -1.02, 0.44, 0.08],
"joint_velocity": [0.01, -0.02, 0.00, 0.04, -0.01, 0.00],
"ee_position": [0.42, -0.15, 0.28],
"ee_orientation": [0.0, 0.707, 0.0, 0.707],
"gripper_state": 0.0,
"target_position": [0.50, -0.10, 0.06],
"action": [0.01, 0.00, -0.005, 0.0, 1.0]
}이 샘플에서 상태는 이미지, 깊이, 관절값, 엔드 이펙터 위치, 목표 물체 위치, 그리퍼 상태를 포함한다. 정답 행동은 [0.01, 0.00, -0.005, 0.0, 1.0]이다. 이는 x 방향으로 1cm 이동하고, z 방향으로 5mm 내려가며, yaw 회전은 하지 않고, 그리퍼를 닫는 쪽으로 가라는 의미일 수 있다.
이렇게 만들어진 학습 데이터셋은 다음과 같은 형태를 갖는다.
X = [state_0, state_1, state_2, ..., state_T]
Y = [action_0, action_1, action_2, ..., action_T]모델은 X를 입력받아 Y를 예측하도록 학습된다. 다음 장에서 다룰 Behavior Cloning은 바로 이 구조 위에 세워진다.
행동을 직접 저장하지 않았다면 계산해서 만들 수 있다
데이터 수집 시점에 조이스틱 명령이나 로봇 명령을 action으로 저장했다면 가장 좋다. 하지만 경우에 따라 행동 명령이 없고, 로봇의 상태 기록만 남아 있을 수도 있다. 이때는 연속된 상태의 차이를 이용해 행동을 만들 수 있다.
예를 들어 상대 엔드 이펙터 이동량을 행동으로 쓰고 싶다면 다음처럼 계산할 수 있다.
dx = x_ee(t+1) - x_ee(t)
dy = y_ee(t+1) - y_ee(t)
dz = z_ee(t+1) - z_ee(t)그러면 state_t의 정답 행동은 t+1 시점으로 이동한 차이가 된다.
state_t → action_t = ee_position_{t+1} - ee_position_t이 방식은 간단하지만 주의할 점이 있다. 샘플 간 시간 간격이 일정해야 행동의 의미가 안정된다. 0.1초 뒤의 이동량과 0.5초 뒤의 이동량을 같은 행동처럼 섞으면 모델이 혼란스러워진다. 그래서 dt, 즉 시간 간격을 일정하게 맞추거나 행동을 속도로 바꾸는 것이 좋다.
vx = (x_ee(t+1) - x_ee(t)) / dt
vy = (y_ee(t+1) - y_ee(t)) / dt
vz = (z_ee(t+1) - z_ee(t)) / dt또 하나의 선택은 몇 프레임 뒤를 예측할지다. 바로 다음 프레임의 이동량은 너무 작아서 노이즈에 민감할 수 있다. 예를 들어 30Hz 데이터에서 한 프레임은 약 0.033초다. 이때 이동량이 매우 작으면 센서 노이즈와 실제 움직임을 구분하기 어렵다. 그래서 3프레임 또는 5프레임 뒤의 차이를 행동으로 삼을 수도 있다.
action_t = ee_position_{t+k} - ee_position_t여기서 k를 action horizon이라고 생각할 수 있다. 너무 작으면 행동이 미세하고, 너무 크면 한 번의 행동이 거칠어진다. 로봇팔 저속 제어에서는 0.1초에서 0.2초 정도의 짧은 horizon부터 실험해 볼 수 있다.
상태는 한 순간이 아니라 짧은 기억일 수도 있다
지금까지는 state_t를 한 시점의 정보로 설명했다. 하지만 로봇이 결정을 내릴 때 한 장의 이미지만으로는 부족할 때가 있다. 물체가 움직이고 있는지, 로봇팔이 이미 내려가는 중인지, 그리퍼가 방금 닫혔는지 같은 정보는 시간 변화 속에 있다.
이럴 때는 최근 몇 프레임을 함께 상태로 넣을 수 있다.
state_t = [obs_{t-2}, obs_{t-1}, obs_t]이미지라면 최근 3장의 이미지를 쌓을 수 있고, 로봇 상태라면 최근 관절값과 그리퍼 상태를 함께 넣을 수 있다. 이렇게 하면 모델은 짧은 움직임의 흐름을 볼 수 있다.
예를 들어 컵이 그리퍼에 잡혀 함께 올라가고 있는지, 아니면 그리퍼만 올라가고 컵은 그대로 있는지는 한 장의 이미지로도 보일 수 있지만, 연속된 이미지를 보면 훨씬 확실하다. 또한 관절 속도를 직접 저장하지 않았다면 최근 위치 차이를 통해 움직임을 추정할 수 있다.
다만 이력을 길게 넣으면 입력이 커지고 학습이 어려워진다. 처음에는 현재 상태만 사용하고, 모델이 움직임의 흐름을 잘 못 잡는다고 판단될 때 최근 2~4프레임을 추가하는 식으로 확장하면 좋다.
정규화: 숫자의 크기를 모델이 다루기 쉽게 만들기
상태와 행동을 숫자로 만들었다면, 그 숫자의 범위를 조정해야 한다. 이를 정규화라고 한다. 모델은 입력 숫자의 크기가 크게 다르면 학습하기 어려워질 수 있다.
예를 들어 이미지 픽셀값은 0부터 255이고, 관절각은 -3.14부터 3.14 근처이며, 엔드 이펙터 위치는 0.2부터 0.8m 사이일 수 있다. 그리퍼 값은 0 또는 1이다. 이 값들을 아무 처리 없이 섞으면 어떤 값은 크게 보이고 어떤 값은 작게 보인다.
가장 흔한 방법은 평균을 빼고 표준편차로 나누는 것이다.
x_norm = (x - mean) / std또는 값의 범위가 명확한 경우 -1부터 1 사이로 스케일링할 수 있다.
x_scaled = 2 * (x - min) / (max - min) - 1행동도 정규화하는 것이 좋다. 예를 들어 dx, dy, dz의 실제 범위가 -2cm부터 2cm라면, 학습할 때는 이를 -1부터 1 사이로 바꿔 모델이 출력하게 만들 수 있다. 실행할 때는 다시 실제 단위로 되돌린다.
model output: [-1.0, 1.0]
real dx: output_dx * 0.02 meter정규화에서 가장 중요한 것은 통계값을 저장하는 것이다. 학습 데이터의 평균과 표준편차, min과 max를 별도 파일로 남겨야 한다. 그래야 나중에 실행할 때도 같은 방식으로 입력을 정규화하고, 모델 출력을 실제 로봇 명령으로 복원할 수 있다.
{
"joint_position_mean": [0.10, -0.70, 1.20, -1.00, 0.30, 0.05],
"joint_position_std": [0.20, 0.30, 0.25, 0.15, 0.20, 0.10],
"ee_position_mean": [0.45, -0.05, 0.25],
"ee_position_std": [0.10, 0.08, 0.06],
"action_scale": [0.02, 0.02, 0.02, 0.20, 1.0]
}이 파일을 잃어버리면 학습된 모델을 제대로 실행하기 어렵다. 모델은 정규화된 세계에서 배웠기 때문에, 실행할 때도 같은 숫자 세계를 만들어 주어야 한다.
좋은 샘플 구조 만들기
이제 하나의 학습 샘플을 명확하게 정의해 보자. 학습 샘플은 크게 세 부분으로 나눌 수 있다. 관측값, 행동, 메타정보다. 관측값은 모델 입력이고, 행동은 모델이 맞혀야 할 정답이며, 메타정보는 디버깅과 분석에 필요한 설명이다.
{
"episode_id": "cup_pick_place_000137",
"sample_index": 123,
"t": 12.340,
"observation": {
"rgb": "images/rgb_000123.jpg",
"depth": "depth/depth_000123.png",
"joint_position": [0.12, -0.84, 1.31, -1.02, 0.44, 0.08],
"joint_velocity": [0.01, -0.02, 0.00, 0.04, -0.01, 0.00],
"ee_pose": [0.42, -0.15, 0.28, 0.0, 0.707, 0.0, 0.707],
"gripper_state": 0.0,
"target_object": [0.50, -0.10, 0.06, 0.94]
},
"action": {
"type": "ee_delta",
"frame": "robot_base",
"dt": 0.1,
"vector": [0.01, 0.00, -0.005, 0.0, 1.0]
},
"label": {
"stage": "approach",
"success_episode": true
}
}여기서 action.type은 행동이 어떤 의미인지 알려 준다. ee_delta라면 엔드 이펙터 상대 이동량이다. frame은 행동 벡터가 어떤 좌표계 기준인지 알려 준다. dt는 이 행동이 어느 시간 간격의 명령인지 나타낸다.
이런 정보를 귀찮다고 생략하면 나중에 문제가 생긴다. 두 달 뒤 데이터셋을 다시 열었을 때 [0.01, 0.00, -0.005, 0.0, 1.0]이 무엇을 뜻하는지 기억나지 않을 수 있다. x, y, z 이동량인지, 관절 속도인지, 단위가 m인지 cm인지, 그리퍼 값 1.0이 열림인지 닫힘인지 알 수 없다. 데이터 구조는 미래의 자신에게 남기는 설명서이기도 하다.
컵 집기 작업의 상태와 행동 설계 예시
구체적인 예로 "빨간 컵을 집어 들어 올리기" 스킬을 생각해 보자. 이 스킬의 범위를 좁혀 보자. 컵은 카메라에 보이고, 로봇팔은 이미 책상 위 작업 공간 안에 있으며, 목표는 컵을 안정적으로 집어 10cm 들어 올리는 것이다.
첫 번째 버전의 상태는 단순하게 설계할 수 있다.
state_t = [
x_obj, y_obj, z_obj,
x_ee, y_ee, z_ee,
gripper_state
]이 상태는 이미지 대신 객체 검출과 좌표 변환 결과를 사용한다. 모델은 컵 위치와 그리퍼 위치의 관계를 보고 다음 이동량을 예측한다. 행동은 다음처럼 둔다.
action_t = [dx, dy, dz, gripper_close]이 설계에서는 회전을 학습하지 않는다. 그리퍼 방향은 항상 아래를 향한다고 가정한다. 그러면 모델이 배워야 할 일은 컵 위로 이동하고, 아래로 내려가고, 적절한 순간에 그리퍼를 닫고, 위로 들어 올리는 것이다.
이 단순한 설계는 첫 실험에 좋다. 모델이 잘 배우지 못한다면 원인을 찾기 쉽다. 객체 좌표가 틀렸는지, 그리퍼 상태가 반대로 저장되었는지, 행동 스케일이 너무 큰지 확인할 수 있다.
두 번째 버전에서는 이미지를 추가할 수 있다.
state_t = {
rgb_crop_t,
object_3d_t,
ee_position_t,
gripper_state_t
}이미지 crop은 컵 주변의 세부 모양과 그리퍼 위치를 보여 준다. 객체 3D 좌표는 목표 위치를 명확하게 알려 준다. 이 조합은 구조화된 좌표와 풍부한 시각 정보를 함께 사용하는 방식이다.
세 번째 버전에서는 관절 상태와 속도를 추가할 수 있다.
robot_state_t = [q_t, dq_t, ee_pose_t, gripper_state_t]이렇게 하면 모델은 로봇팔이 현재 어떤 자세로 컵에 접근하는지 더 잘 알 수 있다. 다만 입력이 늘어나는 만큼 데이터도 더 많이 필요할 수 있다. 항상 기억해야 할 원칙은 단순한 표현으로 시작해 필요한 정보만 추가하는 것이다.
학습 데이터 쌍을 만드는 절차
이제 에피소드 데이터에서 학습 데이터 쌍을 만드는 절차를 정리해 보자. 이전 장에서 수집한 samples.jsonl은 시간 순서의 기록이다. 여기에 이번 장의 상태·행동 설계를 적용하면 Behavior Cloning용 데이터셋이 된다.
절차는 다음과 같다.
에피소드 파일을 읽고 시간 순서대로 샘플을 정렬한다.
각 샘플에서 이미지, 깊이, 관절값, 엔드 이펙터 자세, 그리퍼 상태를 읽는다.
필요한 경우 객체 검출 결과나 3D 객체 좌표를 추가한다.
설계한 상태 형식에 맞게
state_t를 만든다.저장된 명령을 사용하거나, 다음 상태와의 차이로
action_t를 만든다.상태와 행동을 정규화한다.
(state_t, action_t)쌍을 학습 데이터로 저장한다.
의사코드로 쓰면 다음과 같다.
for episode in dataset:
samples = load_samples(episode)
samples = sort_by_time(samples)
for t in range(0, len(samples) - horizon):
obs = build_observation(samples[t])
if has_recorded_action(samples[t]):
action = samples[t].action
else:
action = compute_delta_action(
samples[t],
samples[t + horizon]
)
obs_norm = normalize_observation(obs)
action_norm = normalize_action(action)
save_training_pair(obs_norm, action_norm)여기서 horizon은 몇 프레임 뒤의 변화를 행동으로 볼지 결정하는 값이다. 저장된 action을 그대로 쓴다면 horizon이 필요 없을 수도 있다. 하지만 상태 차이로 행동을 계산한다면 horizon은 행동의 크기와 부드러움에 큰 영향을 준다.
실패 에피소드를 포함할 때는 라벨을 확인해야 한다. 실패 행동을 그대로 정답으로 넣으면 모델이 나쁜 행동까지 배울 수 있다. 복구 구간만 사용하거나, 실패 직전의 잘못된 행동은 제외하는 식으로 데이터 필터링이 필요하다.
if sample.label.stage == "unsafe" or sample.label.bad_action == true:
skip반대로 복구 행동은 매우 귀중할 수 있다. 컵을 살짝 밀었을 때 뒤로 물러나 다시 접근하는 행동은 실제 실행에서 로봇을 튼튼하게 만든다. 따라서 실패 데이터 전체를 버리기보다, 어떤 구간이 잘못된 행동이고 어떤 구간이 복구 행동인지 구분하는 것이 좋다.
상태와 행동 설계에서 자주 생기는 실수
상태와 행동 표현은 겉으로는 단순해 보여도 작은 실수가 모델 전체를 망칠 수 있다. 특히 로봇에서는 숫자 하나의 의미가 실제 움직임으로 이어지기 때문에 더 조심해야 한다.
가장 흔한 실수는 단위가 섞이는 것이다. 어떤 에피소드는 cm, 어떤 에피소드는 m를 쓰면 모델은 같은 움직임을 전혀 다른 크기로 배운다. 관절각도 마찬가지다. degree와 radian을 섞으면 학습은 물론 실행도 위험해진다.
두 번째 실수는 좌표계가 섞이는 것이다. 객체 위치는 카메라 좌표계인데 엔드 이펙터 위치는 로봇 base 좌표계일 수 있다. 두 값을 같은 벡터에 넣으면 모델은 서로 맞지 않는 지도를 보고 길을 찾는 셈이다. 상태 벡터에 들어가는 위치값은 가능한 한 같은 좌표계로 변환해야 한다.
세 번째 실수는 상태에 미래 정보를 넣는 것이다. 예를 들어 현재 상태를 만들면서 t+1의 객체 위치나 성공 여부를 입력에 넣으면 모델은 실제 실행 때 볼 수 없는 정보를 학습하게 된다. 이를 데이터 누수라고 볼 수 있다. 학습 성능은 좋아 보이지만 실제 로봇에서는 무너진다.
네 번째 실수는 행동 스케일이 너무 큰 것이다. 모델이 출력한 작은 오차가 로봇의 큰 움직임으로 증폭되면 위험하다. 저수준 정책에서는 한 스텝의 이동량을 작게 제한하고, 안전 필터를 반드시 적용해야 한다.
다섯 번째 실수는 그리퍼 의미를 불명확하게 저장하는 것이다. 1이 열림인지 닫힘인지, 상태값인지 명령값인지 구분하지 않으면 학습 결과가 이상해진다. 그리퍼 관련 필드는 이름부터 명확히 하는 것이 좋다.
gripper_state # 현재 열림 정도
gripper_command # 이번에 내릴 명령
gripper_closed # 닫힘 여부 라벨이런 기본 규칙을 지키면 모델 구조가 조금 단순하더라도 안정적인 학습을 시작할 수 있다.
실행까지 생각한 표현이어야 한다
상태와 행동 표현은 학습 코드 안에서만 존재하는 것이 아니다. 나중에 로봇이 실제로 움직일 때도 같은 표현을 사용해야 한다. 학습 때 state_t를 만들던 전처리 코드는 실행 때도 필요하다. 모델이 출력한 action_t를 실제 로봇 명령으로 바꾸는 후처리 코드도 필요하다.
실행 흐름은 다음과 같다.
카메라와 로봇 상태 읽기
→ 학습 때와 같은 방식으로 state 만들기
→ state 정규화
→ 모델이 action 예측
→ action을 실제 단위로 복원
→ 안전 제한 적용
→ 로봇 제어기로 전달여기서 학습과 실행이 조금이라도 달라지면 문제가 생긴다. 학습 때 이미지를 224×224로 줄였는데 실행 때 256×256을 넣으면 모델 입력 크기가 맞지 않는다. 학습 때 RGB를 썼는데 실행 때 BGR을 넣으면 색이 뒤집힌다. 학습 때 action scale이 2cm였는데 실행 때 10cm로 해석하면 로봇이 갑자기 크게 움직인다.
따라서 상태·행동 설계는 문서화되어야 한다. 데이터셋 폴더 안에 dataset_config.yaml 같은 파일을 두고 입력 크기, 단위, 좌표계, 정규화 방식, 행동 의미를 기록하는 것이 좋다.
observation:
rgb_size: [224, 224]
rgb_order: RGB
depth_unit: meter
pose_frame: robot_base
joint_unit: radian
action:
type: ee_delta
frame: robot_base
dt: 0.1
translation_limit: 0.02
rotation_limit: 0.2
gripper: 0_open_1_close이런 설정 파일은 사소해 보이지만, 프로젝트가 커질수록 매우 중요해진다. 모델을 다시 학습하거나, 다른 로봇으로 옮기거나, 팀원이 코드를 실행할 때 같은 약속을 공유할 수 있기 때문이다.
이번 장의 핵심 정리
모방학습에서 모델은 사람의 시연을 직접 이해하지 않는다. 모델이 배우는 것은 숫자로 표현된 상태와 행동의 관계다. 따라서 로봇의 세계와 움직임을 일관된 숫자 표현으로 바꾸는 일이 학습의 출발점이다.
시각 상태는 RGB 이미지, 깊이 이미지, 객체 2D/3D 좌표로 구성할 수 있다. 이미지는 풍부한 장면 정보를 담고, 깊이는 거리 감각을 더하며, 객체 좌표는 목표 위치를 간결하게 표현한다. 로봇 내부 상태는 관절각, 관절 속도, 엔드 이펙터 위치와 방향, 그리퍼 상태로 표현한다. 이 값들은 단위와 좌표계를 반드시 통일해야 한다.
행동 벡터는 로봇이 다음에 무엇을 해야 하는지 나타낸다. 절대 엔드 이펙터 목표, 상대 이동량, 관절 속도, 그리퍼 열림값 등 여러 방식이 있다. 초기 Pick and Place 모방학습에서는 상대 엔드 이펙터 이동량과 그리퍼 명령으로 시작하면 이해하기 쉽고 안정적이다.
결국 학습 데이터는 (state_t, action_t) 쌍으로 정리된다. 현재 상태를 입력으로 넣고, 사람이 그 순간 내린 행동을 정답으로 삼는다. 이때 시간 정렬, 정규화, 단위, 좌표계, 행동 스케일, 그리퍼 의미를 일관되게 유지해야 한다.
다음 장에서는 이렇게 만든 상태·행동 쌍을 이용해 실제 Behavior Cloning 모델을 구현한다. 로봇의 시연 데이터가 지도학습 데이터셋으로 바뀌고, 모델은 현재 장면을 보고 다음 움직임을 예측하는 법을 배우게 된다.
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.