hello, robot

라즈베리파이 기반 RC카에 차선 추종 모델을 올려 S자 트랙을 돌렸어요. 첫 회전은 통과하는데 두 번째 회전에서 반복해서 이탈했어요. 차선 검출 마스크는 테이프 위에 정확히 얹혀 있었고, 주행 속도를 12%까지 낮춰도, 조향 명령 40도와 150도에 바퀴가 끝까지 꺾이는 것을 확인해도 증상이 같았어요.

검출도 되고 조향도 되는데 코스를 못 도는 상태였어요. 원인은 모델의 출력 분포에 있었어요.

과제Behavior Cloning · 조향각 회귀 구조PilotNet 계열 CNN 손실MSE 표본채점 1,162장 · 40~150도 기체라즈베리파이 RC카
전체 평균 오차
10.86
도 · 상수 모델은 14.08
급우 구간 오차
39.4
도 · 110장에서만
예측 표준편차
12.69
정답 라벨은 19.14
직진 쏠림
71%
75~105도 구간
가중 샘플링 후
16.17
표준편차 · S자 완주

평균 오차는 소수 구간의 실패를 감춰요

학습된 모델의 평균 절대 오차는 10.86도예요. 조향각을 항상 90도(직진)로만 출력하는 상수 베이스라인이 14.08도이니 학습이 무의미하지는 않다는 뜻이에요. 이 숫자만 보면 문제를 찾을 수 없어요.

각도 구간을 나눠 채점하니 다른 그림이 나왔어요.

구간장수정답 평균예측 평균부족분판정
40~60도 (급좌)1249.25도75.6도26.3도표본 부족
85~95도 (직진)28789.56도93.1도3.5도정확
120~151도 (급우)110138.18도98.8도39.4도못 꺾음

더 확실한 증거는 예측 분포 자체예요. 정답 라벨의 표준편차가 19.14도인데 모델 예측은 12.69도였고, 예측 범위는 60~134도에 갇혀 있었어요. 트랙이 요구하는 40도와 150도를 출력한 적이 아예 없어요.

예측이 중앙으로 수축한 모양
위는 출력 범위, 아래는 구간별 평균이에요. 급우 구간에서 예측이 정답보다 39도 안쪽에 머물러요.
406080100120140조향각 (도) — 90이 직진출력 범위정답 라벨40150적용 전60134적용 후32142구간별 평균급좌 40~60도12장26.3도 차직진 85~95도287장3.5도 차급우 120~151도110장39.4도 차정답 평균적용 전 예측적용 후 예측

전체 평균이 이 실패를 감춘 이유는 표본 수예요. 급커브 122장이 30도 넘게 틀려도 직진 근처 800여 장이 정확하면 전체 평균은 크게 움직이지 않아요. 급좌 구간은 12장뿐이라 이 구간만으로는 판단을 세울 수 없지만, 방향은 같아요.

회귀 손실이 소수 구간을 평균으로 뭉개요

원인은 데이터의 각도 분포예요. 채점에 쓴 1,162장 가운데 51%가 80~100도, 즉 직진에서 10도 안쪽에 들어 있고 75~105도로 넓히면 71%예요. 트랙을 도는 시간의 대부분이 직진이므로 자연스러운 분포예요.

이 분포에 평균제곱오차(MSE)를 쓰면 모델에 특정한 압력이 걸려요. 개별 표본의 가중치는 1/N로 모두 같지만, 한 구간이 손실 전체에서 차지하는 몫은 그 구간의 표본 개수에 비례해요. 소수인 급커브를 정확히 맞히려 애쓰는 것보다, 다수인 직진에 맞춰두고 급커브는 평균값 근처로 뭉개는 편이 전체 손실이 작아요.

조건부 평균을 추정하는 것이 제곱 손실의 최적해라는 성질이 여기서 불리하게 작동해요. 데이터가 직진에 몰려 있으면 모델이 학습하는 조건부 평균 자체가 90도 쪽으로 당겨져요. 결과적으로 중앙만 출력하는 모델로 수렴해요.

파라미터 튜닝과 결이 달라요
[[2026-07-29_회고_앞을_얼마나_멀리_볼까요|Pure Pursuit 전방 주시 거리의 U자 최적점과 이탈 오차 72% 감소]]에서 다룬 전방 주시 거리는 파라미터를 조정해 이탈 오차를 줄이는 문제였어요. 이번은 학습 목표 자체가 급커브를 포기하는 방향으로 정렬돼 있어서, 검출이나 제어 쪽을 아무리 만져도 바뀌지 않았어요.

예측 표준편차가 조기 신호예요

예측이 중앙으로 수축하는 이 현상은 구간별 표를 그리기 전에도 한 숫자로 잡아낼 수 있어요. 예측 분포의 표준편차를 정답 라벨의 표준편차와 비교하면 돼요.

정답 std예측 std상태
19.112.70.66조향 범위를 못 냄
19.116.20.85정상
19.11.920.10학습 붕괴

예측이 정답의 3분의 2 아래로 좁아지면 모델이 평균값으로 수축하는 중이에요. 검증 손실은 이때도 개선되는 것처럼 보이므로 손실 곡선만으로는 알아채기 어려워요. 마지막 줄은 데이터를 1,200장 아래로 줄여 시드만 바꿔 세 번 학습했을 때 나온 건데, 예측이 86~97도 안에서만 움직였어요. 정답이 40~150도인데 11도 폭으로 대답한 셈이라 어떤 커브도 돌 수 없어요.

베이스라인 비교도 함께 봐요. 이 모델은 직진 구간(85~95도)에서 상수 베이스라인보다 5.71도 나빴어요. 표본의 4분의 1이 이 구간에 있으니 전체 평균에서 그만큼을 잃고 시작하는 셈이에요. 그런데도 전체가 베이스라인보다 나은 것은 급커브에서 크게 벌기 때문이고, 뒤집어 말하면 급커브 개선이 사라지는 순간 학습 전체가 상수 모델과 다를 바 없어져요.

역빈도 가중 샘플링으로 압력을 상쇄해요

분포가 원인이므로 배치를 뽑는 단계에서 보정했어요. 각도를 10도 폭 구간으로 나눠 개수를 세고, 개수가 적은 구간이 자주 뽑히도록 표본별 가중치를 줬어요.

counts = np.bincount(구간_인덱스, minlength=구간_수).astype(float)
w = (1.0 / np.maximum(counts[구간_인덱스], 1.0)) ** 0.7
sampler = WeightedRandomSampler(w, num_samples=len(학습_표본), replacement=True)

지수 0.7은 보정 강도예요. 1.0이면 모든 구간이 같은 빈도로 뽑히는 완전 균등이고, 0이면 보정이 없어요. 같은 데이터와 같은 구조로 학습해 같은 표본으로 채점했어요.

지표적용 전적용 후 (0.7)과보정 (1.0)
급우 오차39.41도31.16도47.75도
급우 예측 평균98.8도107.0도
예측 표준편차12.6916.1712.10
예측 범위60~134도32~142도
전체 평균 오차10.86도11.09도13.55도

예측 분포가 정답의 19.14도에 가까워지고 범위가 실제 조향 범위를 덮게 됐어요. 전체 평균 오차는 오히려 0.23도 나빠졌는데, 직진 구간의 정밀도를 급커브와 맞바꾼 결과예요. 채점으로는 조금 나빠졌지만 코스를 벗어나게 만드는 것은 급커브 쪽이라 교환이 이득이었어요.

좌우로 두 번 꺾이는 S자 구간을 차선 사이로 통과하는 소형 자율주행차
가중 샘플링을 적용한 모델의 주행이에요. 두 번째 굽이가 그동안 이탈하던 구간인데, 예측 범위가 60~134도에 갇혀 있던 모델은 여기서 필요한 만큼 꺾지 못하고 바깥으로 밀려났어요. (출처: 직접 촬영)
강도를 1.0으로 올리면 과보정이에요
급우 오차가 47.75도로 되돌아가는데, 상수 베이스라인의 48.18도와 사실상 같은 값이에요. 소수 구간을 과대 대표시키면 배치가 급커브로 채워지면서 직진의 정밀도를 잃는데, 급커브 표본 자체가 122장뿐이라 같은 프레임을 반복해 뽑게 되고 그 구간도 일반화되지 않아요. 전체 오차 13.55도로 베이스라인 14.08도를 0.53도밖에 못 앞선 게 그 결과예요. 0.7 부근이 두 구간이 함께 개선되는 지점이었어요.

데이터를 늘리는 것으로는 해결되지 않았어요

균형 샘플링에 닿기 전에 먼저 해본 것은 수집이었어요. 900장을 추가로 모아 재학습했는데 결과가 같았어요. 새로 모은 데이터도 같은 트랙을 같은 방식으로 돈 것이라 각도 분포가 그대로였기 때문이에요. 분포가 원인인 문제에서는 같은 분포의 표본을 늘려도 압력이 그대로예요.

수집을 늘릴 거라면 부족한 구간만 늘려야 해요. 급커브 구간만 왕복하며 프레임을 모으거나, 트랙을 역방향으로 돌아 좌우 균형을 맞추는 식이에요. 급좌 구간이 12장에 그친 것도 트랙을 한 방향으로만 돈 결과예요. 다만 그 전에 손실 쪽 보정으로 얼마나 회수되는지 먼저 보는 편이 비용이 적어요.

조향 회귀뿐 아니라 라벨이 연속값이고 그 분포가 한쪽에 몰리는 회귀 문제 전반에 같은 구조가 있어요. 평균 지표 하나로 판단하지 않고 구간별로 쪼개 보는 것, 그리고 예측 분포의 폭을 정답과 나란히 확인하는 것이 이 실패를 조기에 잡는 방법이에요.