IsaacGym ONNX 모델을 IsaacLab에서 추가학습하기
IsaacGym에서 학습된 정책을 ONNX로 내보낸 후, 원본 .pt 파일이 없을 때
ONNX → PyTorch 역변환 → IsaacLab fine-tuning 까지 이어가는 전체 과정을 정리합니다.
G1 휴머노이드 로봇 + RSL-RL 기준이지만, 네트워크 구조만 맞으면 다른 모델에도 적용 가능합니다.
목차
- 사전 확인: .pt 체크포인트가 있나요?
- 필요한 파일 준비
- ONNX → PyTorch 역변환
- 체크포인트 정리 (pickle 제거)
- train_new.py 수정 (pretrain 로직)
- IsaacLab에서 첫 학습 실행
- 이후 추가학습 (resume)
- 주의사항 & 트러블슈팅
0. 사전 확인: .pt 체크포인트가 있나요?
ONNX 역변환 작업에 들어가기 전에, 먼저 원본 .pt 파일이 남아있는지 확인하세요.
IsaacGym으로 학습하면 기본적으로 runs/ 또는 logs/ 폴더 안에 체크포인트가 자동 저장됩니다.
# 체크포인트 위치 확인
ls runs/<experiment_name>/
# 또는
ls logs/<experiment_name>/
# model_XXXXX.pt 파일이 있으면 그걸 바로 사용하세요
| 상황 | 난이도 | 방법 |
|---|---|---|
.pt 체크포인트가 있다 |
바로 5단계로 | |
| ONNX 파일만 있다 | 아래 절차 전체를 따라가세요 |
1. 필요한 파일 준비
역변환을 위해 아래 두 파일이 반드시 필요합니다.
policy.onnx— IsaacGym에서 내보낸 ONNX 정책 파일actor_critic_future.py—ActorFuture/ActorCriticFuture클래스가 정의된 파일
(save_onnx.py의 import 경로를 참고해서 찾으세요)
왜 네트워크 클래스가 필요한가?
ONNX 파일 자체에 weight는 있지만, PyTorchstate_dict로딩을 위해서는 원래 클래스 구조와 키 이름이 일치해야 합니다.save_onnx.py에서 사용한 클래스를 그대로 가져오세요.
critic 없음 주의
ONNX export는 보통 actor만 내보냅니다. critic 네트워크는 복원 불가하므로, fine-tuning 시 랜덤 초기화된 critic에서 시작하게 됩니다. 초반에 value loss가 높은 것은 정상입니다.
2. ONNX → PyTorch 역변환
ONNX 모델에서 weight를 직접 추출해 원래 PyTorch 클래스 구조에 맞게 매핑합니다.
onnx2torch 같은 범용 변환 라이브러리는 레이어 이름이 달라져 RSL-RL과 호환이 안 될 수 있으므로, 직접 매핑 방식을 사용합니다.
2-1. ONNX weight 구조 확인
import onnx
import numpy as np
model = onnx.load("policy.onnx")
# initializer에 weight 이름과 shape이 모두 있습니다
for init in model.graph.initializer:
arr = np.array(init.float_data or
np.frombuffer(init.raw_data, dtype=np.float32))
print(init.name, arr.shape)
출력된 키 이름이 원래 PyTorch 모델의 state_dict 키와 일치하는지 확인하세요.
ONNX 변환 시 이름이 그대로 보존된 경우, 별도 매핑 없이 바로 로드할 수 있습니다.
2-2. weight 추출 및 .pt 저장
import onnx
import numpy as np
import torch
# 1. ONNX에서 weight 추출
model = onnx.load("policy.onnx")
weights = {}
for init in model.graph.initializer:
arr = np.frombuffer(init.raw_data, dtype=np.float32).copy()
weights[init.name] = torch.tensor(arr.reshape(
list(init.dims) if init.dims else [-1]))
# 2. normalizer 파라미터 분리 (키 이름에 normalizer가 포함된 것)
normalizer_mean = weights["actor.normalizer.mean"] # 키 이름은 확인 후 수정
normalizer_divisor = weights["actor.normalizer.divisor"]
# 3. model_state_dict 구성
state_dict = {k: v for k, v in weights.items()}
# 4. 저장
torch.save({
"model_state_dict": state_dict,
"normalizer": {
"mean": normalizer_mean,
"divisor": normalizer_divisor,
},
"config": {
"num_observations": 1432, # 본인 환경에 맞게 수정
"num_actions": 23,
}
}, "policy_restored.pt")
2-3. 복원 결과 검증
ONNX 출력과 복원된 PyTorch 모델의 출력을 비교합니다. 최대 오차가 1e-5 이하면 성공입니다.
import onnxruntime as ort
session = ort.InferenceSession("policy.onnx")
test_input = torch.randn(1, 1432) # num_obs에 맞게
# ONNX 추론
onnx_out = session.run(None, {"obs": test_input.numpy()})[0]
# PyTorch 추론
model.eval()
with torch.no_grad():
pt_out = model(test_input).numpy()
max_err = np.abs(onnx_out - pt_out).max()
print(f"최대 오차: {max_err:.8f}") # 1e-5 이하면 성공
3. 체크포인트 정리 (pickle 제거)
변환 과정에서 생성된 .pt 파일에 클래스 정의가 pickle로 포함되어 있으면,
다른 환경에서 로드 시 ModuleNotFoundError가 발생합니다. 순수 텐서만 남기도록 다시 저장합니다.
import torch
# 기존 체크포인트 로드
ckpt = torch.load("policy_restored.pt", map_location="cpu", weights_only=False)
# state_dict를 순수 텐서 딕셔너리로 변환
clean_state_dict = {}
for k, v in ckpt["model_state_dict"].items():
clean_state_dict[k] = v.detach().clone()
# 재저장 (weights_only 호환 형식)
torch.save({
"model_state_dict": clean_state_dict,
"actor_state_dict": {k.replace("actor.", ""): v
for k, v in clean_state_dict.items()
if k.startswith("actor.")},
"normalizer_mean": ckpt["normalizer"]["mean"].detach().clone(),
"normalizer_divisor": ckpt["normalizer"]["divisor"].detach().clone(),
"config": ckpt["config"],
}, "policy_clean.pt")
print("저장 완료: policy_clean.pt")
저장된 파일에는 다음 키가 있어야 합니다: model_state_dict, actor_state_dict, normalizer_mean, normalizer_divisor, config
4. train_new.py 수정 (pretrain 로직)
IsaacLab의 train 스크립트에 --pretrain 옵션을 추가합니다.
RSL-RL 버전에 따라 policy 속성명이 다를 수 있으므로, 자동 탐색 로직을 사용합니다.
4-1. argument parser에 옵션 추가
# train_new.py의 argparse 부분에 추가
parser.add_argument("--pretrain", type=str, default=None,
help="복원된 .pt 체크포인트 경로")
4-2. runner 생성 후 pretrain 로직 삽입
runner = OnPolicyRunner(...) 다음에 아래 코드를 삽입합니다.
if args_cli.pretrain:
print(f"[INFO]: Loading pretrain weights from: {args_cli.pretrain}")
ckpt = torch.load(args_cli.pretrain, map_location="cpu", weights_only=False)
# RSL-RL 버전별 policy 속성명 자동 탐색
# v2.x → runner.alg.actor_critic
# v3.x → runner.alg.policy
policy = None
for attr in ["policy", "actor_critic", "actor"]:
if hasattr(runner.alg, attr):
policy = getattr(runner.alg, attr)
print(f"[INFO]: Found policy at runner.alg.{attr}")
break
if policy is None:
print(f"[WARN]: runner.alg attributes: {dir(runner.alg)}")
raise RuntimeError("policy 속성을 찾을 수 없습니다")
# Format A: model_state_dict가 있는 경우 (전체 로드)
if "model_state_dict" in ckpt:
missing, unexpected = policy.load_state_dict(
ckpt["model_state_dict"], strict=False)
print(f"[INFO]: Loaded model_state_dict")
print(f" missing: {missing}")
print(f" unexpected: {unexpected}")
# Format B: actor_state_dict만 있는 경우 (actor만 로드)
elif "actor_state_dict" in ckpt:
missing, unexpected = policy.actor.load_state_dict(
ckpt["actor_state_dict"], strict=False)
print(f"[INFO]: Loaded actor_state_dict only")
# Format C: 그 외 (strict=False로 시도)
else:
policy.load_state_dict(ckpt, strict=False)
print(f"[INFO]: Loaded raw checkpoint (strict=False)")
# Normalizer 복원
if "normalizer_mean" in ckpt:
for attr in ["actor_obs_normalizer", "obs_normalizer", "normalizer"]:
if hasattr(policy, attr):
norm = getattr(policy, attr)
norm.running_mean.data.copy_(ckpt["normalizer_mean"])
norm.running_var.data.copy_(ckpt["normalizer_divisor"] ** 2)
print(f"[INFO]: Restored normalizer at policy.{attr}")
break
디버그 출력 확인
missing keys에 critic 관련 키가 있는 것은 정상입니다.
actor 관련 키가 missing에 있다면 키 이름 불일치 문제이므로, state_dict 키를 직접 출력해서 비교하세요.
5. IsaacLab에서 첫 학습 실행
일반 학습 (헤드리스, 빠름)
python train_new.py \
--task Isaac-Velocity-Rough-G1-v0 \
--pretrain /path/to/policy_clean.pt \
--num_envs 4096 \
--max_iterations 5000 \
--exptid g1_finetune_v1
시각적 확인 (렌더링 ON)
# --debug 옵션: 환경 4개 + 렌더링 ON
python train_new.py \
--task Isaac-Velocity-Rough-G1-v0 \
--pretrain /path/to/policy_clean.pt \
--debug
권장 PPO 하이퍼파라미터 (fine-tuning용)
| 파라미터 | 권장값 | 이유 |
|---|---|---|
learning_rate |
1e-5 |
기본값보다 낮게 — actor 보존 |
clip_param |
0.1 |
기본 0.2에서 낮춰 급격한 변화 방지 |
desired_kl |
0.005 |
기본 0.01에서 낮춤 |
critic_learning_rate |
3e-4 |
critic은 높게 — 빠른 수렴 |
6. 이후 추가학습 (resume)
첫 학습이 끝난 후, 저장된 체크포인트에서 이어서 학습하려면
--pretrain 대신 --resume을 사용합니다.
pretrain (1회) → model_5000.pt 저장 → resume (반복) → optimizer + iter 복원
# 체크포인트 저장 위치 확인
ls logs/rsl_rl/g1_finetune_v1/
# resume으로 추가학습
python train_new.py \
--task Isaac-Velocity-Rough-G1-v0 \
--resume True \
--load_run g1_finetune_v1 \
--load_checkpoint model_5000.pt \
--max_iterations 10000
--pretrainvs--resume차이
--pretrain: model weight만 복원, optimizer state와 iteration은 처음부터 시작--resume: model weight + optimizer momentum + iteration 카운터 모두 복원. 완전히 이어서 학습됨
7. 주의사항 & 트러블슈팅
AttributeError: 'PPO' object has no attribute 'actor_critic'
RSL-RL 버전에 따라 속성명이 다릅니다. 위의 자동 탐색 코드를 사용하거나, 직접 확인하세요.
# RSL-RL 버전별 속성명
# v2.x: runner.alg.actor_critic
# v3.x: runner.alg.policy
print(dir(runner.alg)) # 실제 속성 목록 확인
Observation 차원 불일치
IsaacGym과 IsaacLab 환경의 obs 구성이 다를 수 있습니다. 반드시 맞추세요.
# ONNX 기준 (예시)
# motion(35) + priop(92) + history(127×10) + future(35) = 1432
# IsaacLab 환경의 obs 수 확인
print(env.observation_space)
print(agent_cfg.num_observations)
초반에 value loss가 매우 높음
critic이 랜덤 초기화이므로 정상입니다. 보통 500~1000 iteration 이내에 안정화됩니다.
Normalizer 복원 검증
print("mean:", policy.actor_obs_normalizer.running_mean[:5])
print("var:", policy.actor_obs_normalizer.running_var[:5])
# 모두 0이면 복원 실패 → normalizer 속성명을 다시 확인
체크포인트에 pickle된 클래스가 포함된 경우
저장된 .pt 파일에 pickle된 클래스가 포함되어 있으면 다른 환경에서 로드 시 오류가 납니다.
3단계의 정리 과정을 반드시 거치세요.
IsaacGym (Legged Gym / RSL-RL) + IsaacLab 환경 기준 · G1 humanoid robot
환경 구성이나 네트워크 구조가 다르면 obs 차원 및 키 이름을 본인 환경에 맞게 수정하세요.