홍쌤의 인포클래스AI + X · 기술 블로그

← 목록

시뮬에선 피했는데 교실에선 후진했다: 젯슨 오린 나노 2륜 로봇과 Isaac Lab 강화학습 일지

작성 2026.10.11 · 수정 2026.10.12 · 홍창민

시뮬레이션에서 장애물을 잘 피하던 로봇이 교실 바닥에서는 계속 뒤로 갔다.
이틀 동안 그 이유를 찾은 기록.


10월 6일 (월)

수업 중에 한 학생이 물었다. "로봇이 스스로 피하는 법은 누가 가르쳐요?"

지금까지는 내가 규칙을 짜서 넣었다. 장애물이 왼쪽에 있으면 오른쪽으로 꺾는 식이다. 그런데 그건 "로봇이 배웠다"고 말하기 어렵다. 그래서 같은 로봇, 같은 카메라로 두 가지를 비교해 보기로 했다. 하나는 내가 짠 규칙, 다른 하나는 강화학습으로 로봇이 스스로 찾은 방법이다.

학습하는 컴퓨터와 달리는 컴퓨터

컴퓨터는 두 대로 나눴다.

오린 나노는 CPU와 GPU가 8GB 메모리를 나눠 쓴다. 여기서 학습까지 돌리는 건 무리라서, 학습은 DGX에서 하고 오린 나노는 결과물인 정책 파일(약 118KB)만 받아 쓰기로 했다.

처음 막힌 곳은 모터 전원이었다. 6V 배터리로는 바퀴가 돌다 말다 했다. 전압 강하였다. 12V를 넣고 나서야 제대로 돌았다.

비교 기준부터 만들었다. 학습 없이 바로 돌아가는 규칙 기반 회피다. 화면 아래쪽을 왼쪽·가운데·오른쪽 셋으로 나누고, 가장 덜 막힌 쪽으로 핸들을 꺾는다.

def decide(scores, block_th=0.12, base_speed=0.4):
    left, center, right = scores          # 구역별 막힘 점수(0~1)
    if center < block_th:                 # 앞이 비었으면 전진
        return base_speed, (right - left) * 0.8, "전진"
    if min(left, right) > block_th * 1.5:
        return 0.0, 0.0, "정지(양쪽막힘)"
    return (base_speed * 0.3, 0.9, "좌회전") if left < right else (base_speed * 0.3, -0.9, "우회전")

규칙 기반: 의자를 보고 오른쪽으로

단순하지만 바로, 예측한 대로 움직였다.

내친김에 "YOLO가 보고 작은 LLM이 판단"하는 방식도 재 봤다. 15W 모드에서 10번씩 돌린 최솟값이다.

구성 지연 초당 판단
YOLO만 약 38ms 약 26회
YOLO + llama3.2 1B 131ms 7.6회
YOLO + llama3.2 3B 714ms 1.4회

3B는 판단 한 번에 0.7초가 걸린다. 그 사이 로봇은 이미 벽에 닿는다. 핸들은 YOLO에 맡기고, LLM은 "멈출까 말까" 정도의 느린 판단에만 쓰는 게 맞겠다.

밤에는 Isaac Lab 학습을 시작했다. 처음에는 "목표까지 직선거리가 줄어들면 보상"으로 줬다. 그랬더니 로봇이 ㄷ자 통로에서 벽에 딱 붙었다. 벽 너머가 목표 쪽이니 직선거리로는 가까워진 셈이다. 내가 준 보상이 거짓말을 하고 있었다.

10월 7일 (화)

새벽에 보상을 고쳤다. 통로 중심선 위에 로봇 위치를 투영하고, 그 선을 따라 얼마나 왔는지로 계산했다. 이번에는 로봇이 코너를 돌았다.

오전에는 물리 엔진을 Newton(MuJoCo-Warp)으로 바꿔 봤다. 첫 평균 보상이 -10^32 근처로 튀었다. NaN이었다. 바닥 마찰값을 명시하고 nan_to_num으로 막았지만 불안해서, 최종 회피 학습은 PhysX로 되돌렸다.

오후에는 회피 전용으로 다시 짰다.

보상 설계

알고리즘은 PPO, 네트워크는 128-128-64였다. 가중치를 바꿔 가며 일곱 번 돌렸다.

실행 전진 보상 벽 감점 학습률 반복 마지막 평균 보상
r1 8 6 1e-3 300 456
r2 10 8 1e-3 300 532
r3 8 12 1e-3 400 440
r4 12 6 5e-4 400 626
r5 10 10 1e-3 500 681
r6 6 15 1e-3 400 569
r7 14 8 5e-4 500 1,118

점수가 가장 높은 r7을 골랐다. 시뮬레이션 영상 속 Jetbot은 분홍 벽 사이를 빠져나가 목표까지 갔다. 이때는 다 됐다고 생각했다.

Isaac Lab 시뮬레이션

웹캠에는 레이캐스트가 없다. 그래서 YOLO로 흉내를 냈다. 화면을 세 구역으로 나누고, 구역마다 가장 큰 장애물 박스가 화면의 12%를 넘으면 "완전히 막힘"으로 쳤다.

def yolo_to_distsensor(dets, w, h):
    zone_block = [0.0, 0.0, 0.0]                  # 좌, 중, 우
    for (cx, area) in dets:
        zi = 0 if cx / w < 1/3 else (1 if cx / w < 2/3 else 2)
        zone_block[zi] = max(zone_block[zi], min(area / 0.12, 1.0))
    return 1 - zone_block[1], 1 - zone_block[0], 1 - zone_block[2]   # 앞, 왼쪽, 오른쪽

오후 4시 50분쯤 교실에서 돌렸다. 로봇이 뒤로 갔다.

실제 웹캠: 의자를 잡았는데 FORWARD, v=-0.40

녹화를 다시 보니 이상했다. 의자 두 개를 잡았는데 화면에는 계속 FORWARD가 떴다. 그런데 아래 숫자는 v=-0.40이었다. 전진이라면서 속도는 음수다.

10월 11일 (일)

블로그에 정리하려고 정책 파일을 열었다. 관측값을 손으로 넣고 출력을 하나씩 찍어 봤다.

넣은 상황 정책 원래 출력 [좌, 우] 시뮬에서 실제로 쓰인 값
사방이 열림, 멈춤 [-33.5, -27.4] [-1, -1]
사방이 열림, 주행 중 [-16.0, -12.3] [-1, -1]
앞이 막힘 [+360.7, +430.6] [+1, +1]
왼쪽이 막힘 [-16.0, -50.0] [-1, -1]
오른쪽이 막힘 [-20.8, -25.0] [-1, -1]

보고 나서야 이해가 됐다. 원인은 세 가지였다.

첫째, 앞뒤가 반대였다. 더 단순한 "목표 방향으로 가기" 정책도 목표가 앞에 있으면 [-1, -1]을 낸다. 보상은 전진해야만 오르는데 학습이 잘 됐다. 그러니 Isaac의 Jetbot은 바퀴에 음수를 줄 때 앞으로 가는 것으로 보인다. 아직 시뮬에서 직접 확인하지는 않았고, 출력 패턴으로 추정한 것이다. 실로봇 코드는 이 값을 그대로 썼다. 그래서 시뮬의 전진이 교실에서는 후진이 됐다. 화면 글자는 회전값만 보고 정하고 있었기 때문에 FORWARD라고 떴다.

둘째, 로봇이 배운 동작은 사실상 두 가지뿐이었다. 출력이 수십에서 수백인데 -1~1로 자르니 "끝까지 전진" 아니면 "끝까지 후진"만 남는다. 옆이 막혀도 돌지 않는다. ㄷ자 미로 하나에서만 연습했으니, "앞이 막히면 물러서고 아니면 직진"만 해도 점수가 충분히 나왔을 것이다. 동작 감점은 자른 뒤 값에 걸려 있었다. 그래서 출력이 커지는 것을 막지 못했다.

셋째, 눈이 달랐다. YOLO는 의자나 사람 같은 물체만 안다. 벽, 케이블, 검은 상자는 못 본다. 그래서 바로 앞이 막혀 있어도 "앞이 완전히 열림(1.00)"이 들어갔다. 선속도 자리에도 실제로 잰 값이 아니라 직전 명령값을 넣고 있었다.

강화학습이 실패한 게 아니었다. 시뮬과 교실 사이의 약속을 내가 안 맞췄다. 부호, 값의 범위, 관측의 뜻이다. 규칙 기반은 그런 약속이 처음부터 없어서 바로 됐다.

r7을 고른 이유도 다시 생각해 봤다. 가중치가 다르면 점수 크기 자체가 달라진다. 1,118점이 456점보다 두 배 잘했다는 뜻이 아니다. 평균 보상으로 실행끼리 순위를 매긴 것부터 잘못이었다.

다음에 할 일

이 과정은 학생용 자료(10개 모듈, 110차시) 중 7~8모듈에 그대로 넣을 생각이다. 성공한 예제보다 이 실패가 더 좋은 교재다. 학생들에게는 이렇게 물어볼 것이다.

안전 수칙은 처음부터 끝까지 같다. 모터는 GPIO에 바로 꽂지 않고 드라이버를 거친다. GND는 공통으로 묶는다. 처음에는 바퀴를 공중에 띄우거나 --sim으로, 최고 속도 0.3에서 시작한다.