LeRobot 언어 레시피: 지속 지시와 순간 이벤트를 카메라별로 결합하기

LeRobot의 언어 레시피는 저장된 문장을 현재 프레임에 맞는 학습 메시지로 바꾸는 설정입니다. 먼저 그 시점에 유지되는 지시와 바로 그 프레임에서 발생한 이벤트를 나누고, 카메라에 관한 질문·답변이라면 어느 시점의 어느 화면인지까지 지정합니다. 이 선택을 마친 뒤 문장, 이미지, 학습 대상 메시지를 묶습니다.

공식 Language columns and recipes 문서는 이를 데이터 열, YAML 레시피, 학습 메시지 형식의 세 층으로 설명합니다. 데이터에 언어를 저장하는 규칙과 그중 무엇을 학습 입력으로 꺼내는 규칙이 분리되어 있어, 같은 주석을 여러 학습 목적에 사용할 수 있습니다.

유지되는 지시는 active_at, 순간 이벤트는 emitted_at

두 언어 열은 프레임 데이터와 함께 Parquet에 저장되지만 시간의 의미가 다릅니다. language_persistent는 하위 작업, 계획, 메모리처럼 발생한 뒤 교체될 때까지 유효한 항목을 담습니다. 이 행에는 자체 timestamp가 있으며, 에피소드의 모든 프레임에 행 목록이 전달됩니다. 현재 사용할 항목은 목록을 그대로 이어 붙이는 대신 active_at 같은 선택 규칙으로 찾습니다.

language_events는 끼어든 지시나 시각 질문·답변처럼 정확히 해당 프레임에서 발생한 항목을 담습니다. 이벤트 행 자체에는 별도 timestamp가 없고, 그 행이 놓인 프레임의 시간이 기준이 됩니다. emitted_at은 그 시간과 정확히 일치하는 이벤트를 찾습니다.

다음은 컵을 옮기는 가상 에피소드입니다. 0초에 “컵 집기”가 시작되고 2초에 “선반에 놓기”로 바뀌며, 2초 프레임에만 카메라 질문이 기록되었다고 가정하겠습니다.

조회 시점 active_at으로 고르는 하위 작업 emitted_at으로 찾는 카메라 질문
1.5초 컵 집기 없음
2.0초 선반에 놓기 2.0초 프레임의 질문
2.5초 선반에 놓기 없음

이벤트를 2.01초로 조회하면 2.0초의 질문을 가까운 값으로 대신 골라 주지 않습니다. 공식 규칙에는 허용 오차 창이 없으므로, 이벤트 시간을 만들 때도 Parquet의 실제 프레임 시간을 기준으로 삼아야 합니다. 지속 지시의 “현재도 유효함”과 이벤트의 “바로 이때 발생함”을 같은 시간 검색으로 처리하지 않는 이유입니다.

같은 시간의 질문도 카메라가 다르면 다른 행이다

위 가상 에피소드에 상단과 손목 카메라가 있다고 해 보겠습니다. 두 영상의 feature key를 각각 observation.images.top, observation.images.wrist로 두고, 같은 프레임에 카메라마다 사용자 질문과 assistant 답변을 한 쌍씩 기록합니다.

이때 시간, style=vqa, role=user만으로 검색하면 사용자 질문이 두 개 잡힙니다. 문서가 설명하는 resolver는 하나를 임의로 고르지 않고 모호성 오류를 냅니다. 상단 영상의 질문을 선택하려면 카메라 조건도 같은 선택식에 넣습니다.

emitted_at(t, style=vqa, role=user, camera=observation.images.top)

답변 binding도 같은 카메라와 role=assistant를 지정하고, 메시지의 이미지 블록에는 observation.images.top을 연결합니다. 이렇게 해야 상단 영상의 질문·답변에 손목 영상이 붙는 조합을 피할 수 있습니다. 다른 카메라는 자신의 binding과 이미지 블록으로 구성합니다.

카메라 필드는 모든 문장에 붙이는 부가 정보는 아닙니다. 공식 스키마에서 vqa와 trace는 해당 observation.images.* 키를 반드시 지정합니다. 나머지 style은 camera=null을 사용하며, 관절·Cartesian 공간의 로봇 동작을 나타내는 motion도 이쪽에 포함됩니다. 어느 화면에 근거한 문장인지가 필요한 행에만 화면의 정체성을 넣는 구조입니다.

레시피는 선택한 행을 대화 차례와 학습 대상으로 바꾼다

레시피의 bindings는 어떤 행을 가져올지 정하고, messages는 가져온 내용을 사용자·assistant 차례에 배치합니다. 예를 들어 현재 하위 작업을 답하는 메시지에는 그 하위 작업 binding을 넣고 target: true로 학습 대상임을 표시할 수 있습니다. 카메라 VQA라면 이미지와 질문을 사용자 메시지에, 대응하는 답을 assistant 메시지에 놓습니다.

에피소드의 기본 작업 문장은 별도 경로를 유지합니다. ${task}는 meta/tasks.parquet의 작업 문자열을 읽으므로, 추가 언어 주석이 있어야만 기본 작업을 사용할 수 있는 구조가 아닙니다.

여러 목적을 섞는 blend를 쓰면 한 샘플에서 하위 레시피 하나가 선택됩니다. 문서는 가중 분기 선택이 샘플 인덱스로 결정된다고 설명합니다. 따라서 메모리 업데이트, 하위 작업 수행, VQA 분기를 등록했다고 해서 한 프레임에 세 대화를 모두 합치는 것은 아닙니다. 어떤 분기를 골랐는지에 따라 그 샘플이 요구하는 행과 학습 대상이 달라집니다.

메시지 생성과 토큰화 사이에 남는 경계

RenderMessagesStep의 결과는 messages, message_streams, target_message_indices입니다. 메시지 본문뿐 아니라 어느 스트림인지, 어느 메시지가 대상인지도 정책 처리기로 넘깁니다. 실제 모델의 tokenizer chat template을 적용하는 일은 그 다음 정책 처리기가 맡습니다.

언어 데이터가 없는 경우와 선택한 이벤트가 없는 경우도 동작이 다릅니다. 두 언어 열이 모두 누락되었거나 None 또는 빈 값이면 렌더 단계가 아무 일도 하지 않고 기존 흐름을 유지합니다. 반면 이벤트용 분기가 선택됐는데 필요한 이벤트 행이 없으면 렌더링은 None을 반환해 로더가 다른 샘플을 시도할 수 있게 합니다. 앞의 가상 예에서 2.5초에 VQA 분기를 고르는 상황이 후자에 해당합니다.

이 구분을 알고 있으면 저장 주석의 문제와 레시피의 선택 문제를 나눠 볼 수 있습니다. 주석을 만들고 검수하는 단계는 장기 로봇 데이터의 언어 주석과 품질 관리로 이어지고, 여기서 설명한 레시피는 그 주석을 특정 프레임의 학습 메시지로 구성하는 단계에 해당합니다.

이 글은 2026년 10월 15일(UTC) 확인한 LeRobot main 문서를 분석했습니다. 주석 파이프라인이나 학습 코드를 직접 실행한 기록은 아닙니다.