예제 밖으로 나가는 순간
LeRobot을 처음 배울 때는 제공되는 로봇, 데이터셋, 정책을 그대로 따라가는 것이 가장 빠릅니다. 그러나 실제 프로젝트는 대부분 예제와 다릅니다. 카메라 위치가 다르고, 관절 수가 다르며, 작업 목표도 다릅니다. 어떤 로봇은 팔 하나만 있고, 어떤 로봇은 양팔과 이동 베이스를 함께 씁니다. 어떤 태스크는 블록 집기처럼 단순하지만, 어떤 태스크는 문을 열고 물체를 꺼내는 긴 순서를 포함합니다.
이때 필요한 능력은 LeRobot을 "그대로 쓰는 능력"이 아니라 "내 문제에 맞게 확장하는 능력"입니다. LeRobot의 장점은 로봇 하드웨어 제어, 데이터 수집, 학습, 평가 흐름을 하나의 인터페이스로 묶는 데 있습니다. 공식 저장소에서도 사용자가 Robot 인터페이스를 구현하면 자신만의 로봇에 대해 데이터 수집, 학습, 시각화 도구를 활용할 수 있다고 설명합니다.1
핵심은 단순합니다. 로봇마다 다른 부분은 로봇 클래스 안에 숨기고, LeRobot이 기대하는 공통 형식으로 관측값과 행동값을 내보내면 됩니다. 그러면 이후 단계인 데이터셋 생성, 정책 학습, 평가, 배포는 기존 파이프라인을 최대한 재사용할 수 있습니다.
AI 생성 이미지
새 하드웨어는 설정과 제어를 분리한다
커스텀 로봇을 붙일 때 가장 먼저 할 일은 설정 클래스를 만드는 것입니다. LeRobot에서는 로봇별 설정을 RobotConfig를 상속한 클래스로 정의하고, @RobotConfig.register_subclass("...")로 고유 이름을 등록합니다.2 이 이름은 나중에 명령줄이나 설정 파일에서 "어떤 로봇을 쓸 것인가"를 구분하는 열쇠가 됩니다.
설정 클래스에는 실행 중 계속 바뀌는 값이 아니라, 로봇을 연결하고 초기화하는 데 필요한 정보를 넣습니다. 예를 들면 다음과 같습니다.
@RobotConfig.register_subclass("tabletop_arm")
@dataclass
class TabletopArmConfig(RobotConfig):
port: str = "/dev/ttyUSB0"
baudrate: int = 1_000_000
cameras: dict[str, CameraConfig] = field(default_factory=dict)여기서 port는 모터 컨트롤러나 마이크로컨트롤러가 연결된 장치 경로입니다. baudrate는 시리얼 통신 속도입니다. cameras는 카메라 이름과 설정을 담는 딕셔너리입니다. 중요한 점은 카메라를 리스트가 아니라 이름이 있는 딕셔너리로 관리하는 것입니다. front, wrist, side처럼 의미 있는 이름을 붙이면 데이터셋, 학습 설정, 평가 스크립트에서 같은 이름을 계속 사용할 수 있습니다.
설정 클래스는 로봇의 "명세서"에 가깝습니다. 반대로 실제 제어 코드는 Robot을 상속한 로봇 클래스에 넣습니다. 예를 들어 모터 버스를 여는 방법, 카메라 프레임을 읽는 방법, 액추에이터에 목표 위치를 보내는 방법은 모두 로봇 클래스 안에 캡슐화합니다.
class TabletopArm(Robot):
config_class = TabletopArmConfig
name = "tabletop_arm"
def __init__(self, config: TabletopArmConfig):
self.config = config
self.cameras = make_cameras_from_configs(config.cameras)이 구조를 지키면 LeRobot의 공통 제어 흐름은 하드웨어 세부사항을 몰라도 됩니다. 학습 스크립트는 "관측값을 가져오라", "행동을 보내라"만 호출합니다. 실제로 그것이 Dynamixel 모터인지, Feetech 모터인지, 별도 마이크로컨트롤러인지, TCP 기반 제어기인지는 로봇 클래스가 책임집니다.
Robot 인터페이스에서 반드시 맞춰야 할 것
커스텀 로봇 클래스에서 가장 중요한 것은 feature 정의입니다. observation_features와 action_features는 하드웨어가 아직 연결되지 않은 상태에서도 확인 가능해야 합니다. 그래야 데이터셋 스키마와 정책 입력 차원을 미리 결정할 수 있습니다.
예를 들어 6축 로봇팔과 손목 카메라 하나가 있다면 관측 feature는 다음처럼 생각할 수 있습니다.
@property
def observation_features(self):
return {
"joint_1.pos": float,
"joint_2.pos": float,
"joint_3.pos": float,
"joint_4.pos": float,
"joint_5.pos": float,
"joint_6.pos": float,
"observation.images.wrist": (480, 640, 3),
}행동 feature는 정책이 로봇에 보낼 명령의 형식입니다. 위치 제어를 한다면 각 관절의 목표 위치가 될 수 있고, 속도 제어를 한다면 각 관절의 목표 속도가 될 수 있습니다. 중요한 것은 send_action()이 받는 키와 action_features의 키가 일치해야 한다는 점입니다.
연결 흐름도 명확히 나누어야 합니다. is_connected는 읽기와 쓰기가 실제로 가능한 상태에서만 True가 되어야 합니다. 단순히 포트가 열렸다는 이유만으로 연결 완료로 처리하면, 카메라가 실패했거나 일부 모터가 응답하지 않는 상황을 놓칠 수 있습니다. connect()에서는 모터 통신, 카메라 연결, 필요 시 캘리브레이션을 수행합니다. disconnect()에서는 포트와 장치를 닫고 자원을 해제합니다.
캘리브레이션은 실제 로봇에서 특히 중요합니다. 같은 명령값이라도 영점이 틀어져 있으면 로봇은 전혀 다른 자세로 움직입니다. 따라서 is_calibrated, calibrate(), configure()를 분리하는 편이 좋습니다. calibrate()는 영점, 관절 범위, 센서 보정값을 저장하거나 불러오는 역할을 합니다. configure()는 제어 모드, 게인, 토크 상태처럼 하드웨어 설정을 적용합니다. 가능하면 configure()는 여러 번 호출해도 같은 결과가 나오도록 만들어야 합니다.
마지막으로 런타임 입출력은 두 함수로 모입니다. get_observation()은 모터 위치, 센서값, 카메라 이미지를 읽어 관측 딕셔너리를 반환합니다. send_action(action)은 정책이 낸 행동 딕셔너리를 실제 하드웨어 명령으로 바꿉니다. 여기서 안전을 위해 행동값을 관절 범위 안으로 자르거나, 급격한 변화를 완만하게 만드는 smoothing을 적용할 수 있습니다.
새 태스크는 성공 조건만으로 정의되지 않는다
새 태스크를 만든다는 것은 "성공하면 1, 실패하면 0"을 정하는 것보다 넓은 작업입니다. 학습과 재사용이 가능한 태스크가 되려면 최소한 네 가지가 함께 정의되어야 합니다.
첫째, 자연어 태스크 설명입니다. 예를 들어 pick up the red cube and place it in the bowl 같은 문장은 사람에게도 의미가 있고, 언어 조건 정책을 쓸 때도 입력으로 활용될 수 있습니다. 한국어 프로젝트라면 내부 문서는 한국어로 작성하더라도, 모델과 데이터셋 공유를 고려하면 영어 설명을 함께 관리하는 것이 좋습니다.
둘째, 관측 feature 이름입니다. 카메라 이미지는 보통 observation.images.<camera_name> 형태로 이름을 붙입니다. 저차원 상태는 observation.state처럼 묶을 수도 있고, 관절별 이름으로 나눌 수도 있습니다. 어느 방식을 택하든 수집, 학습, 배포에서 동일해야 합니다.
셋째, 행동 feature의 shape와 dtype입니다. 예를 들어 6축 관절 위치 명령이면 action shape는 (6,)입니다. 그리퍼까지 포함하면 (7,)이 될 수 있습니다. dtype은 보통 실수형입니다. 이 정의가 어긋나면 학습은 되더라도 배포 시 정책 출력과 로봇 명령 차원이 맞지 않아 실패합니다.
넷째, FPS입니다. FPS는 초당 몇 번 관측하고 행동할지를 뜻합니다. 데이터 수집은 30FPS였는데 배포는 10FPS로 실행하면 같은 정책도 다른 동작을 보일 수 있습니다. 특히 action chunk를 사용하는 정책에서는 시간 간격이 정책 의미에 직접 영향을 줍니다.
커스텀 데이터셋은 메타데이터가 중심이다
LeRobotDataset v3.0에서는 데이터가 단순히 "에피소드별 파일 묶음"으로만 관리되지 않습니다. 여러 에피소드가 Parquet 데이터 shard와 MP4 비디오 shard에 함께 저장될 수 있고, 실제 에피소드 경계는 메타데이터의 offset과 length로 복원됩니다.3 따라서 커스텀 데이터셋을 만들 때는 파일 이름보다 메타데이터가 더 중요합니다.
반드시 준비해야 할 핵심 파일은 다음과 같습니다.
meta/info.json: feature 이름, shape, dtype, FPS, 코드베이스 버전, 데이터와 비디오 shard 경로 템플릿을 담습니다.meta/stats.json: 평균, 표준편차, 최솟값, 최댓값 같은 정규화 통계를 담습니다.meta/tasks.jsonl: 자연어 태스크 설명과 정수 task ID의 대응 관계를 담습니다.meta/episodes/: 각 에피소드의 길이, task ID, 데이터와 비디오 offset 정보를 담습니다.
이 구조를 맞추면 데이터셋을 로컬에서만 쓰는 것이 아니라 Hugging Face Hub에 올려 공유하거나 스트리밍 방식으로 불러오는 것도 쉬워집니다. 반대로 메타데이터가 부정확하면 모델은 잘못된 구간을 하나의 에피소드로 읽거나, 이미지와 상태를 어긋난 시간으로 학습할 수 있습니다.
가장 흔한 실수는 정규화 통계를 오래된 데이터 기준으로 남겨두는 것입니다. 에피소드를 삭제하거나 새로 추가했다면 meta/stats.json도 다시 계산해야 합니다. 정책은 이 통계를 기준으로 입력과 출력을 스케일링합니다. 통계가 틀리면 같은 관절값도 모델 내부에서는 전혀 다른 값처럼 보입니다.
여러 카메라와 여러 로봇을 다룰 때
복합 설정에서는 이름이 곧 안전장치입니다. 카메라가 두 대 이상이면 cam0, cam1 같은 이름보다 front, wrist, left_side처럼 위치가 드러나는 이름이 좋습니다. 데이터셋에는 observation.images.front, observation.images.wrist처럼 저장하고, 학습 설정과 평가 스크립트에서도 같은 이름을 사용합니다.
여러 로봇을 동시에 쓰는 경우에도 같은 원칙을 적용합니다. 양팔 로봇이라면 left_arm.joint_1.pos, right_arm.joint_1.pos처럼 네임스페이스를 나누는 방식이 도움이 됩니다. 이렇게 해야 나중에 feature를 보았을 때 어느 관절이 어느 로봇에 속하는지 바로 알 수 있습니다.
동기화도 중요합니다. 카메라 FPS, 로봇 제어 FPS, timestamp 기준이 서로 다르면 데이터는 겉으로는 정상처럼 보여도 학습에는 나쁜 신호가 됩니다. 예를 들어 이미지에는 이미 물체를 잡은 장면이 있는데 행동값은 아직 접근 명령을 가리키고 있다면, 모델은 원인과 결과를 반대로 배울 수 있습니다. 수집 스크립트와 평가 스크립트에서 동일한 FPS, 동일한 카메라 이름, 동일한 timestamp 기준을 유지해야 합니다.
실전에서는 짧은 검증 에피소드를 먼저 기록하는 것이 좋습니다. 5초 정도 로봇을 천천히 움직인 뒤, 상태값 변화와 영상 프레임을 함께 재생해 봅니다. 관절이 증가할 때 화면 속 로봇도 같은 방향으로 움직이는지, 그리퍼 명령과 실제 그리퍼 움직임이 맞는지 확인합니다. 이 작은 검증이 긴 데이터 수집 실패를 막아 줍니다.
새 정책까지 확장할 때
대부분의 프로젝트는 기존 정책으로 시작하는 것이 좋습니다. ACT, Diffusion Policy 같은 기본 정책으로 기준선을 만든 뒤, 정말 필요한 경우에만 새 정책을 추가합니다. 새 정책을 추가하려면 단순히 PyTorch 모델 하나를 만드는 것으로 끝나지 않습니다. LeRobot의 학습, 평가, 배포 파이프라인에 들어오려면 설정, 정책 클래스, 전처리·후처리 factory가 같은 이름 규칙으로 연결되어야 합니다.
정책 설정은 PreTrainedConfig를 상속하고 @PreTrainedConfig.register_subclass("my_policy")로 등록합니다. 이 등록 문자열은 정책 클래스의 name, processor factory 이름, 명령줄의 --policy.type과 일치해야 합니다.4 이름이 하나라도 어긋나면 CLI에서 정책을 찾지 못하거나, 잘못된 전처리기를 연결할 수 있습니다.
정책 클래스는 PreTrainedPolicy를 상속합니다. 학습에는 forward(batch)가 쓰이고, 평가는 매 step마다 select_action(batch)을 호출합니다. action chunk를 쓰는 정책이라면 predict_action_chunk(batch)도 중요합니다. 에피소드가 시작될 때 내부 상태를 초기화해야 한다면 reset()에 그 로직을 넣습니다.
배치 입력도 마음대로 새 접두사를 만들기보다 LeRobot이 쓰는 상수를 따르는 편이 안전합니다. 예를 들어 상태, 이미지, 언어, 행동은 각각 정해진 키 체계로 전달됩니다. 새 센서를 쓰더라도 기존 구조 안에서 표현할 수 있는지 먼저 확인해야 합니다. 그래야 데이터셋 로더, processor, 정책이 같은 언어로 대화할 수 있습니다.
확장 작업을 위한 체크리스트
커스텀 로봇과 새 태스크를 만들 때는 다음 순서로 점검하면 좋습니다.
로봇 설정을
RobotConfig로 분리했는가?고유 등록 이름이 다른 로봇과 충돌하지 않는가?
Robot클래스가 연결, 캘리브레이션, 관측, 행동 전송을 명확히 구현하는가?observation_features와action_features가 실제 입출력 딕셔너리와 일치하는가?카메라 이름, 해상도, FPS가 수집·학습·평가에서 동일한가?
태스크 설명, feature shape, dtype, FPS가 데이터셋 스키마에 기록되어 있는가?
meta/info.json,meta/stats.json,meta/tasks.jsonl, episode 메타데이터가 최신인가?짧은 에피소드로 timestamp와 영상·상태·행동 정렬을 확인했는가?
새 정책을 추가한다면 config 등록 문자열, 정책 이름, processor factory,
--policy.type이 모두 일치하는가?
LeRobot 확장의 본질은 복잡한 코드를 많이 쓰는 데 있지 않습니다. 경계를 잘 나누는 데 있습니다. 로봇별 차이는 RobotConfig와 Robot 안에 넣고, 태스크별 의미는 데이터셋 메타데이터에 기록하며, 정책별 차이는 PreTrainedConfig와 PreTrainedPolicy로 연결합니다. 이 경계를 지키면 새로운 하드웨어와 새로운 태스크도 기존 LeRobot 생태계 안에서 학습하고 평가하고 공유할 수 있습니다.
참고
1Hugging Face LeRobot GitHub 저장소
2Bring Your Own Hardware · Hugging Face LeRobot
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.