PID 제어기는 얼마나 안정적일까? MuJoCo로 관성·센서 노이즈·지연 실험하기
같은 PID 게인, 같은 진자 — 이번엔 불확실한 관성, 노이즈 섞인 센서, 늦게 도착하는 관측
Day 2는 제어기가 동작하는 걸로 끝났다. 재사용 가능한 스칼라 PID가 MuJoCo 진자를 +30°에 붙잡았다. 스텝 후 3.630초 만에 안정됐고, 마지막 2초간 평균 절대 오차(MAE)는 0.313°였다. P와 PI는 진동하며 안정되지 않았고, PD는 중력 때문에 목표 각도와 차이가 남았다. PID만 안정됐고, 숫자가 그렇게 말했다.
그런데 그건 깨끗한 시뮬레이션이었다. 제어기는 진짜 각도를 10ms마다, 지체 없이 봤다. Day 2에서는 그게 깨지면 어떻게 되는지 테스트하지 않았다.
Day 3의 질문은 당연한 다음 질문이다. 깨끗한 시뮬레이션에서 동작한 PID 제어기는, 모델이 불확실하거나 센서 측정값에 노이즈가 섞이거나 관측이 늦게 도착하면 어떻게 될까? 게인은 그대로, 진자도 그대로, 스텝도 그대로. 불확실성 세 가지를 하나씩, 그리고 세 개를 한 번에.
짧게 답하면, 고정된 게인은 관성과 지연 변화는 실패나 미안정 없이 견뎠다. 진짜 얘기는 센서 노이즈였다. 진자가 목표를 못 따라가서가 아니라, 제어기가 명령을 마구 휘두르기 시작했기 때문이다. 그리고 복합 실험에서는 단일 요인 실험에서 한 번도 안 나온 결과가 나왔다. 오버슈트다.
여전히 걷는 로봇도, 학습된 정책도, 하드웨어도 없다. 있는 것은 재현 가능한 시뮬레이션, 측정된 숫자, 그리고 재사용할 수 있는 관측 파이프라인이다.
1. Day 2가 실제로 증명한 것
강건성을 테스트하기 전에 기준선이 정확히 뭔지부터 말해야 한다. Day 3의 모든 숫자는 이 기준선과 비교한 것이기 때문이다.
Day 2 실험은 스텝 응답이었다. 진자는 0°에서 시작해 0.5초에 목표가 +30°로 바뀌고, 10초 동안 돌린다. PID 게인은 Kp=6, Ki=3, Kd=1, 토크 제한은 ±3 N·m이다. 제어기는 10ms마다 결정하고, 물리는 2ms 타임스텝에 RK4로 돌아간다.
기준선 결과는 Day 3의 노이즈 0, 지연 0 실행에서 정확히 재현됐다.
| 지표 | 기준선 |
|---|---|
| 마지막 2초간 평균 절대 오차(MAE) | 0.313° |
| 스텝 이후 RMSE (0.5–10초) | 4.300° |
| +30° 위 오버슈트 | 0.000° |
| 목표 각도에 도달한 뒤 안정되는 시간 (±2° 안) | 3.630초 |
| 인접 명령 간 변화 RMS | 0.099 N·m |
| 포화 (명령이 ±3 N·m을 넘은 비율) | 0.1% |
"안정"은 여기서 엄격한 뜻이다. 목표에서 ±2° 안에 들어간 뒤, 그 뒤의 모든 물리 샘플이 계속 그 안에 머물러야 한다. 안정 시간이 null이면 끝까지 그 안에 머물지 못했다는 뜻이고, 0으로 바꾸지 않는다. 마지막 2초간 평균 절대 오차는 8–10초 구간만 보는 유한 구간 통계지, 정상 상태 오차를 보장하는 숫자가 아니다.
+30°에서 중력을 버티는 데 필요한 토크는 1.22625 N·m이다. 가만히 서 있는 것만으로도 모터가 계속 이 토크를 내야 한다.
Day 2가 증명한 건 하나다. 정보가 완벽하면 이 게인 세트는 동작한다. Day 3은 정보가 완벽하지 않을 때에도 계속 동작하는지 테스트한다.
2. 세 가지 불확실성
진짜 로봇 관절은 모델과 정확히 일치하지도, 완벽하게 측정하지도, 현재를 즉시 듣지도 못한다. 휴머노이드라면 팔다리를 갈아끼울 때마다 관성도, 센싱도, 타이밍도 달라진다. 그게 로봇을 모듈형으로 만드는 이유다. 그래서 Day 3은 관절 하나에 가장 중요한 세 가지 어긋남을 테스트한다.
관성. 모델의 힌지 관성 모멘트는 0.086392578125 kg·m², 1kg짜리 0.5m 캡슐에서 컴파일된 값이다. 진짜 팔다리는 더 무겁거나 질량이 다르게 분포할 수 있다. 한 관성 모멘트에 맞춰 튜닝한 제어기가 1.5배나 2배에서는 어떻게 될까?
센서 노이즈. 진짜 각도 센서는 진실에 노이즈를 더해서 보고한다. Day 2의 제어기는 측정값 자체를 미분했다. 미분 항은 측정된 각도가 얼마나 빨리 변하는지에 반응한다. 연속된 두 측정값 사이의 노이즈는 그대로 미분 항에 들어간다.
관측 지연. 진짜 측정은 센서에서 제어기까지 오는 데 시간이 걸린다. 제어기가 움직일 때쯤이면, 제어기가 보는 각도는 이미 지난 각도다. 10, 30, 50ms의 고정 지연은 절대값으로는 작다. 제어기 업데이트 다섯 번이 채 안 된다. 하지만 낡은 데이터로 도는 피드백은 안정성을 잃는 고전적인 방법이다.
이 세 가지는 서로 다른 종류의 틀림이다. 모델이 틀렸고, 측정이 틀렸고, 타이밍이 틀렸다. 따로 테스트해야 하는 이유가 있다. 세 개를 한 번에 바꾸고 뭔가 깨지면, 무엇 때문에 깨졌는지 말할 수 없기 때문이다.
3. 통제된 실험 설계
설계는 한 번에 하나의 요인, 그리고 복합 케이스 하나다. 아홉 가지 시나리오, 나머지는 전부 동일하다.
SCENARIOS = ( Scenario("baseline"), Scenario("inertia_1_5x", inertia_factor=1.5), Scenario("inertia_2x", inertia_factor=2.0), Scenario("noise_0_2deg", noise_sd_deg=0.2), Scenario("noise_0_4deg", noise_sd_deg=0.4), Scenario("delay_10ms", delay_ms=10), Scenario("delay_30ms", delay_ms=30), Scenario("delay_50ms", delay_ms=50), Scenario("combined", inertia_factor=2.0, noise_sd_deg=0.4, delay_ms=50), )
모든 케이스는 Day 2 설정을 그대로 유지한다. 1kg 몸체, 중력 −9.81 m/s², RK4, 0.002초 물리 스텝, 0.01초 제어 스텝, ±3 N·m 토크, 초기 상태 0°, 0.5초에 +30° 스텝, 10초, Kp=6, Ki=3, Kd=1. 게인은 다시 튜닝하지 않았다. 그게 핵심이다. 여기서 강건성이란 같은 제어기가 다른 세상에 적응하는 것이지, 세상마다 제어기를 다시 맞추는 게 아니다.
확률적인 케이스(노이즈 케이스와 복합 케이스)는 시드 다섯 개 — 11, 23, 37, 53, 71 — 로 돌리고 평균과 관측된 범위를 보고한다. 결정적인 케이스는 시드 11 하나만 쓴다. 노이즈는 제어기 결정마다 한 번씩 뽑는 독립적인 평균 0 가우시안 각도 노이즈고, 제어기의 관측에만 들어간다. MuJoCo의 진짜 상태는 바꾸지 않는다. 지연은 고정값이고 10ms 업데이트 간격의 배수여야 한다.
전체 결과표는 다음과 같다. 모든 오차는 제어기가 무엇을 봤든 MuJoCo의 실제 각도 기준으로 잰다.
| 시나리오 | 마지막 2초간 평균 절대 오차(MAE) ° 평균 [범위] | RMSE ° 평균 | 오버슈트 ° 평균 | 목표 각도에 도달한 뒤 안정되는 시간 초 평균 [범위] | 제어 명령 변화 RMS N·m | 포화 평균 |
|---|---|---|---|---|---|---|
| 기준선 | 0.313 [0.313, 0.313] | 4.300 | 0.000 | 3.630 | 0.099 | 0.1% |
| 관성 1.5배 | 0.319 [0.319, 0.319] | 4.468 | 0.000 | 3.674 | 0.098 | 0.2% |
| 관성 2배 | 0.320 [0.320, 0.320] | 4.608 | 0.000 | 3.680 | 0.097 | 0.2% |
| 노이즈 0.2° | 0.308 [0.282, 0.321] | 4.314 | 0.000 | 3.650 [3.586, 3.726] | 0.888 | 0.14% |
| 노이즈 0.4° | 0.390 [0.307, 0.485] | 4.396 | 0.027 | 4.152 [3.792, 4.364] | 1.715 | 3.9% |
| 지연 10ms | 0.315 [0.315, 0.315] | 4.256 | 0.000 | 3.648 | 0.100 | 0.2% |
| 지연 30ms | 0.318 [0.318, 0.318] | 4.184 | 0.000 | 3.684 | 0.102 | 0.4% |
| 지연 50ms | 0.321 [0.321, 0.321] | 4.177 | 0.000 | 3.718 | 0.109 | 0.6% |
| 복합 | 0.395 [0.298, 0.496] | 4.649 | 1.477 | 4.190 [3.838, 4.494] | 1.680 | 4.1% |
이 글에서 "강건성"이 무슨 뜻인지, 그리고 무슨 뜻이 아닌지 짚고 넘어가자. 뜻하는 건 이 아홉 번의 측정된 실행, 이 게인, 이 진자에서의 결과다. 다른 게인, 다른 목표, 다른 시드, 하드웨어에 대한 보장이 아니다. 아래의 모든 숫자는 summary.json의 측정값이지, 일반적인 로봇에 대한 주장이 아니다.
4. 실험 A — 관성 모멘트 바꾸기
관성 변화는 일부러 좁게 잡았다. PendulumModelConfig(inertia_factor=...)는 막대와 수직인 두 중심 관성 성분을 같은 양만큼 늘린다. 컴파일된 유효 힌지 관성 모멘트는 1.5배에서 0.1295888671875 kg·m², 2배에서 0.17278515625 kg·m²가 된다. 몸체 질량, 질량중심, 중력, 댐핑, 액추에이터 기어, 토크 범위는 전부 그대로다. +30° 중력 유지 토크도 세 경우 모두 정확히 1.22625 N·m로 같다.
이 분리가 핵심이자 한계다. 진짜로 무거워진 팔다리는 질량도, 질량중심도, 유지 토크도 함께 바뀐다. 여기서는 회전 관성만 바뀌니까, 실험이 답하는 질문은 하나다. 이 제어기가 관성 하나에만 얼마나 민감한가? 보이는 캡슐 모양은 그대로인데 관성 텐서가 다른 질량 분포를 나타내니까, 이건 이상화된 민감도 테스트지, 물리적으로 다시 설계한 막대가 아니다.
그림: 기준선, 관성 1.5배, 2배의 시드 11 각도 궤적. 세 곡선이 거의 겹친다. 차이는 모양이 아니라 숫자에 있다.
결과는 거의 지루할 정도다. 그게 오히려 정보다. 관성을 두 배로 늘렸더니 마지막 2초간 평균 절대 오차는 0.313°에서 0.320°로, 안정되는 시간은 3.630초에서 3.680초로 바뀌었다. 3.6초짜리 안정에 50ms가 더 걸린 셈이다. 스텝 이후 RMSE는 4.300°에서 4.608°로 더 눈에 띄게 올랐는데, 무거운 진자가 목표에 가는 길이 조금 달라지기 때문이다. 끝나는 지점은 같은데 가는 길이 다르다. 오버슈트는 세 경우 모두 0이었고, 제어 명령 변화 RMS는 오히려 조금 줄었다(0.099 → 0.097 N·m). 무거운 진자는 각 명령에 더 느리게 반응하기 때문이다.
이걸 "진짜 로봇 팔 질량을 두 배로 늘려도 상관없다"고 읽으면 안 된다. 여기서 유지 토크는 안 변했지만, 진짜 팔에서는 바뀐다. 실험이 말하는 건 더 좁다. 게인도 유지 토크도 같은 조건에서, 이 제어기의 최종 정확도는 관성 오차 2배에 거의 둔감하다. 과도 응답은 조금 더 민감하다. 팔다리를 갈아끼우는 로봇에는 유용한 성질이다. 하지만 관성 부분에 대해서만 그렇다.
5. 실험 B — 센서 노이즈
이 실험이 제어기를 보는 내 생각을 바꿨다. 메커니즘은 PID 코드 한 줄에 있다. 미분 항은 측정값에 대한 미분이다.
derivative = ( 0.0 if self.previous_measurement is None else -(measurement - self.previous_measurement) / dt )
미분 항은 오차를 미분하지 않는다. 제어기가 보는 각도 자체가 얼마나 빨리 변하는지를 미분한다. 측정이 깨끗하면 괜찮다. 노이즈가 있으면 연속된 두 측정값의 차이는 대부분 노이즈고, dt = 0.01초로 나누면 100배가 된다.
실행하기 전에 이 효과의 크기를 예측할 수 있다. 각 측정값에 표준편차 σ인 독립 가우시안 노이즈가 있으면, 연속된 두 측정값의 차이는 표준편차가 이고, 업데이트 간격으로 나누면 미분 노이즈 크기는 가 된다. σ = 0.4°면:
측정된 미분 입력 오차 RMS는 0.974 rad/s였다. 예측과 1.5% 차이다. 0.2° 노이즈면 예측은 0.494 rad/s, 측정값은 0.487 rad/s였다. 이론이 미묘한 게 아니다. 산수고, 시뮬레이션이 그대로 따라온다.
그림: 0.4° 노이즈, 시드 11에서 실제 각도(파랑)와 제어기가 본 관측값(주황). 관측값은 매 업데이트마다 진짜 각도 주위로 ±0.4°씩 흔들린다. 센서 오차 RMS는 0.400°로, 넣은 노이즈 수준과 정확히 같다.
그림: 기준선, 0.2° 노이즈, 0.4° 노이즈의 명령 토크, 시드 11. 기준선 명령은 매끈한데, 0.4° 케이스는 실행 내내 ±3 N·m 범위를 거의 다 쓰며 떨린다.
결과는 최종 각도가 아니라 명령에서 드러난다. 0.4° 노이즈에서 마지막 2초간 평균 절대 오차(MAE)는 다섯 시드 평균 0.390°였다. 기준선보다 나쁘지만, 진자는 모든 시드에서 안정됐다(평균 4.152초, 범위 3.792–4.364초). 폭발한 건 명령이다. 인접 명령 간 변화 RMS가 기준선 0.099 N·m에서 1.715 N·m로, 17배가 됐다. 포화도 0.1%에서 3.9%로 올랐다. 전체 토크 RMS도 1.124에서 1.504 N·m로 올랐다.
1.715 N·m가 정확히 무엇인지 짚자. 연속된 명령 사이의 변화량의 RMS다. 전체 모터 토크도, 전기 에너지도, 측정된 물리적 마모도 아니다. 명령 신호 자체가 얼마나 격렬하게 흔들리는지를 재는 숫자다. 진짜 모터라면 그 흔들림은 열과 진동과 마모로 이어진다. 하지만 이 시뮬레이션에는 모터 모델이 없으니까, 이 숫자는 마모 측정값이 아니라 경고 신호다.
반대 방향으로 주의할 결과도 하나 있다. 0.2° 노이즈에서 마지막 2초간 평균 절대 오차(MAE)는 다섯 시드 평균 0.308°로, 노이즈 없는 기준선 0.313°보다 조금 낮았다. "약간의 노이즈가 도움이 된다"고 말하고 싶어진다. 그렇다는 근거는 없다. 10초짜리 다섯 시드로는 노이즈가 제어를 좋게 한다고 말할 수 없고, 같은 케이스의 명령 변화 RMS(0.888 N·m)는 이미 기준선의 아홉 배였다. 정직하게 읽으면 이렇다. 0.2°에서는 추적 오차가 대충 그대로인데, 명령은 이미 훨씬 거칠다.
6. 실험 C — 고정된 관측 지연
지연은 노이즈와 다르게 동작한다. 노이즈는 값을 망가뜨리고, 지연은 타이밍을 망가뜨린다. 관측 파이프라인은 타임스탬프가 찍힌 샘플들을 시간 순서대로 쌓아두고, 제어 결정 시각에서 요청된 지연을 뺀 시점 이전의 가장 최신 샘플을 돌려준다. 히스토리가 아직 다 안 쌓인 시작 구간에는 t=0 샘플을 유지한다. 그래서 시작할 때는 측정이 요청된 지연보다 젊다. 50ms 케이스에서 결정마다 측정의 평균 나이는 49.85ms였다.
def observe(self, decision_time_s: float, true_angle_rad: float) -> Observation: if not isfinite(true_angle_rad): raise ValueError("true angle must be finite") raw = true_angle_rad + (self.rng.gauss(0.0, self.noise_sd_rad) if self.noise_sd_rad else 0.0) self.history.append(decision_time_s, raw) measurement_time, angle = self.history.at(decision_time_s) return Observation(angle, raw, measurement_time, decision_time_s - measurement_time)
제어기는 지연된 값을 본다. 성능 지표는 항상 진짜 상태를 기준으로 잰다. 낡은 목표를 따라가는 제어기를 같은 낡은 목표로 평가하면 좋아 보일 테니까.
그림: 기준선과 10, 30, 50ms 고정 지연의 시드 11 각도 궤적. 지연된 곡선은 기준선보다 조금 늦게 따라가고 조금 늦게 안정된다. 모양은 그 외에는 같다.
지연 결과는 이 연구에서 가장 직관에 안 맞는 부분이다. 안정되는 시간은 예상대로 지연과 함께 느려졌다. 10ms에서 3.648초, 30ms에서 3.684초, 50ms에서 3.718초, 기준선은 3.630초였다. 그런데 스텝 이후 RMSE는 지연과 함께 낮아졌다. 기준선 4.300°, 10ms 4.256°, 30ms 4.184°, 50ms 4.177°다. 마지막 2초간 평균 절대 오차(MAE)는 거의 안 움직였다(0.315°, 0.318°, 0.321°).
이건 이번 과도 응답 하나에 대한 측정된 트레이드오프지, 지연이 제어를 좋게 한다는 증거가 아니다. 지연된 관측은 초기 응답을 살짝 부드럽게 만든다. 제어기가 진자가 있었던 곳에 반응하는데, 이번 스텝에서는 그게 전체 제곱 오차를 줄이는 쪽으로 작용한 반면, 마지막 접근은 더 오래 걸렸다. 오버슈트는 0으로 유지됐고 포화도 낮았다(0.2–0.6%). 여기서 가져갈 교훈은 더 좁고 더 쓸모 있다. 고정된 50ms 지연은 이 제어기를 불안정하게 만들지 않았다. 하지만 좋아진 지표(RMSE)와 나빠진 지표(안정되는 시간)는 다른 것을 재고, 둘 중 하나만 인용하면 오해하게 된다.
7. 복합 외란
복합 케이스는 선택된 설정 하나지, 전수 조사가 아니다. 관성 2배, 노이즈 0.4°, 지연 50ms를 함께, 시드 다섯 개. 세 가지 불확실성이 상호작용하는 유일한 케이스고, 질적으로 새로운 것이 나타난 유일한 케이스다.
그림: 기준선과 복합 케이스(관성 2배, 노이즈 0.4°, 지연 50ms)의 시드 11 각도 궤적. 복합 궤적은 +30°를 넘어갔다가 내려오고 흔들리다 안정된다. 단일 요인 케이스에서는 없던 동작이다.
숫자는 이렇다. 마지막 2초간 평균 절대 오차(MAE) 다섯 시드 평균 0.395°(범위 0.298–0.496°), 평균 안정 시간 4.190초(범위 3.838–4.494초), 평균 오버슈트 1.477°(범위 1.248–1.910°), 명령 변화 RMS 1.680 N·m, 포화 4.1%. 모든 시드가 10초 안에 안정됐고, 값이 발산하지 않았다.
오버슈트가 신호다. 단일 요인 케이스는 전부 오버슈트가 0이었다. 0.4° 노이즈 단독의 평균 오버슈트도 0.027°에 불과했다. 세 요인을 합치자 진자가 평균 1.5° 목표를 넘어갔다. 이게 일반적인 상호작용 법칙의 증거는 아니다. 복합 지점 하나로는 관성, 노이즈, 지연이 일반적으로 어떻게 상호작용하는지 알 수 없다. 하지만 단일 요인 결과를 더하면 된다고 가정하지 말고 상호작용을 연구해야 할 구체적인 이유가 된다. 복합 지점 하나가 아니라 작은 요인 실험이 당연한 다음 실험이다.
8. 예상과 달랐던 것
실행 전에 예상과 달랐던 결과가 세 가지 있다. 이 연구는 특정한 단조 응답을 가정하지도, 어떤 케이스가 실패할 거라고 가정하지도 않고 설계됐으니까, 의외의 결과가 곧 발견이지, 민망한 일이 아니다.
첫째, 관성은 거의 문제되지 않았다. 관성을 두 배로 늘리면 응답이 눈에 띄게 느려질 거라고 예상했다. 3.6초짜리 안정에 50ms가 더 걸렸고, 마지막 2초간 평균 절대 오차는 0.007° 늘었다. 이유가 설정에 보인다. 유지 토크는 안 변했고, 게인은 최종 정확도를 묶는 제약이 아니었다. 관성은 과도 응답 쪽에서 드러났다. RMSE가 4.300°에서 4.608°로 올랐지만, 마지막 2초간의 평균 오차는 거의 그대로였다.
둘째, 약간의 노이즈는 대표 지표만 보면 해롭지 않아 보였는데, 해롭지 않았다는 게 아니었다. 0.2°에서 마지막 2초간 평균 절대 오차(MAE)는 다섯 시드 평균으로 기준선보다 조금 낮았다. 맥락 없이 인용되기 딱 좋은 숫자다. 명령이 진짜 얘기를 했다. 0.2°에서 기준선의 아홉 배, 0.4°에서 열일곱 배의 흔들림이다. 제어기는 잘 따라가면서도 마구 휘두를 수 있고, 추적 오차만으로는 그걸 알 수 없다.
셋째, 지연은 하나의 지표는 좋게 하고 다른 지표는 나쁘게 했다. RMSE는 지연과 함께 단조롭게 떨어졌고, 안정되는 시간은 단조롭게 올랐다. RMSE만 보고했으면 지연이 좋아 보였을 거고, 안정 시간만 보고했으면 나빠 보였을 거다. 둘 다 사실이고, 그 쌍이 곧 결과다. 이건 지연에 대한 얘기만이 아니라 평가 일반에 대한 교훈이다. 숫자 하나로 제어기를 설명할 수 없다.
9. 모듈형 관측·평가 아키텍처
Day 3은 실험만 바꾼 게 아니라 구조를 바꿨다. Day 2의 재사용 가능한 PID 제어기는 그대로고, 여전히 MuJoCo 의존성이 없다. 새로 생긴 건 시뮬레이터와 제어기 사이의 계층, 그리고 결과를 채점하는 계층이다.
관측 파이프라인(src/mochi_sprout/simulation/observation.py)은 각도 노이즈를 도(degree)에서 라디안으로 바꾸고, 전용 시드 생성기에서 뽑고, 타임스탬프가 찍힌 샘플을 인과적 버퍼에 저장한다. 시뮬레이터와 무관하다. 진짜 각도와 결정 시각을 받아서, 각도와 원본 각도, 측정 시각, 나이를 담은 관측을 돌려준다. 기존의 reset, update, saturated 인터페이스를 구현한 다른 제어기도 같은 파이프라인을 쓸 수 있다.
MuJoCo 어댑터(src/mochi_sprout/simulation/controlled_pendulum.py)는 시뮬레이션을 전진시키고 제어기에 진짜 각도(Day 2)나 파이프라인의 관측값(Day 3)을 넘긴다. 진짜 qpos와 원본 센서 각도, 제어기 관측값, 샘플 타임스탬프, 관측 나이를 따로 기록한다. 따로 기록하는 게 진단을 정직하게 만든다. 노이즈 0, 지연 0 경로는 이 머신에서 Day 2의 PID 궤적을 정확히 재현한다.
평가기(src/mochi_sprout/evaluation/robustness_metrics.py)는 Day 2의 스텝 응답 지표를 재사용하고, Day 3을 해석할 수 있게 해주는 진단을 더한다. 나중에 어댑터가 관측 기반 오차를 넘겨줘도, 추적 오차를 실제 상태에서 다시 계산한다.
true_rows = [{**row, "error_rad": row["target_rad"] - row["qpos_rad"]} for row in rows] metrics = evaluate_step_response( true_rows, step_time_s=step_time_s, target_rad=target_rad, steady_window_s=steady_window_s, settling_band_rad=settling_band_rad, )
그다음 관측, 미분 입력, 토크 변화, 나이 진단을 더한다. 전부 기록된 궤적에서 계산하고, 제어기 내부 상태에는 손대지 않는다.
raw_error = [row["raw_sensor_rad"] - row["qpos_rad"] for row in updates] observed_error = [row["observed_rad"] - row["qpos_rad"] for row in updates] torque_changes = [b["torque_nm"] - a["torque_nm"] for a, b in zip(updates, updates[1:])] derivative_error = [ ((b["observed_rad"] - a["observed_rad"]) - (b["qpos_rad"] - a["qpos_rad"])) / (b["time_s"] - a["time_s"]) for a, b in zip(updates, updates[1:]) ]
마지막 값, 미분 입력 오차가 노이즈 결과를 예측한 숫자다. 0.4° 노이즈에서 예측 0.987 rad/s에 측정 0.974 rad/s. 평가기가 있는 이유가 이거다. 다음 실험도 측정 코드를 다시 쓰지 않고 같은 질문을 던질 수 있다. 평가기는 궤적만 읽을 뿐, 제어기 내부 상태에는 접근하지 않는다.
10. 결과 재현하기
이 연구는 모듈 실행 한 번으로 재현된다. 저장소 루트에서 Python 3.11과 uv가 설치된 상태로 실행한다.
uv sync --locked --python 3.11 uv lock --check --python 3.11 uv run --locked --python 3.11 pytest -q uv run --locked --python 3.11 python -m mochi_sprout.experiments.day03 --output-dir experiments/day03
실행하면 새 recorded_at 타임스탬프가 찍힌다. 커밋된 실행은 2026-11-09T20:55:10−08:00에 기록됐고, Python 3.11.8, MuJoCo 3.17.0, Day 2 main dc24cd5에서 나왔다. 숫자는 개발 머신에서 재현됐고, 플랫폼마다 작은 부동소수점 차이가 날 수 있다. 소수점 셋째 자리까지 맞는 게 기대치고, 비트 단위 일치가 아니다.
전체 테스트가 통과한다. 진자, PID, 제어 진자, 관측, 지표, Day 3 강건성 테스트 파일에 걸쳐 27개다. 개발자는 커스텀 시나리오 하나를 직접 돌릴 수도 있다. 예를 들어 run_scenario(Scenario("custom", inertia_factor=1.5, noise_sd_deg=0.3, delay_ms=20), seed=101) — 지연은 10ms 업데이트 간격의 배수여야 한다.
출처 (머지된 커밋 3a7db47):
- Day 3 커밋
- 기술 분석: docs/phase0-day3.md
- 근거 인계: docs/publication/day03-evidence.md
- 실험 기록: experiments/day03/README.md
- 반올림 없는 결과: experiments/day03/summary.json
- 대표 샘플: experiments/day03/representative_updates.csv
- PID 제어기: src/mochi_sprout/control/pid.py
- 관측 파이프라인: src/mochi_sprout/simulation/observation.py
- MuJoCo 어댑터: src/mochi_sprout/simulation/controlled_pendulum.py
- 실험 러너: src/mochi_sprout/experiments/day03.py
- 강건성 지표: src/mochi_sprout/evaluation/robustness_metrics.py
- 테스트: tests/test_day03_robustness.py
이 글의 다섯 그림은 커밋된 시드 11 제어기 업데이트 샘플(representative_updates.csv)에서 SVG로 다시 그린 것이고, 원래 PNG와 같은 계열, 제목, 정의를 쓴다. 원래 PNG는 근거로 mochi-sprout 저장소에 남아 있다. 글 표지는 커밋된 combined_angle.png 자체(1260×630)를 머지된 커밋에 고정해서 쓰고 있어, 소셜 미리보기가 래스터 이미지를 받는다. SVG og:image는 대부분의 플랫폼에서 제대로 안 나온다.
11. 한계와 다음 실험
이 글의 모든 것은 이상화된 진자 시뮬레이션이다. 결과를 과대해석하기 쉬우니까, 있는 그대로 말하겠다.
- 진짜 휴머노이드 하드웨어도, 진짜 센서 측정도, 모터 동역학이나 백래시 모델도, 물리적 접촉도, 보행도 없다.
- 목표 스텝 하나(+30°), 초기 상태 하나(0°), 10초 관측 창.
- 확률적인 케이스는 무작위 시드 다섯 개, 결정적인 케이스는 시드 하나.
- 복합 외란 시나리오는 하나뿐이다. 일반적인 상호작용 법칙이 아니다.
- 게인 세트에 걸친 강건성은 입증되지 않았다. 게인은 다시 튜닝하지도, 바꿔보지도 않았다.
- 측정값에 대한 검증된 저역 통과 필터도, 검증된 가변 지연이나 지터 완화도 없다. 고정 지연은 준비가 끝나면 나이가 예측 가능하지만, 지터는 나이가 들쭉날쭉해지고 어떤 샘플 상태가 쓰이는지 바뀔 수 있다. 지터 실험은 여기 없다.
- 센서 바이어스도, 관절 마찰 불확실성도, 타임스텝 수렴 검사도 없다.
- Sim-to-Real 검증은 어떤 형태로도 없다. 여기서 어떤 것도 실제 로봇의 안전을 입증하지 않는다.
가장 쓸모 있는 다음 실험을, 실제로 돌릴 순서대로 적으면 이렇다.
- 측정값을 필터링한다. 노이즈 결과가 미분 항을 직접 가리킨다. 관측값에 저역 통과 필터를 거는 것, 혹은 미분 필터가 당연한 완화책이다. 따로 측정해야 한다. 명령을 가라앉히면서 응답을 느리게 만들지 않는지.
- 작은 요인 실험을 한다. 복합 지점 하나에서 단일 요인에는 없던 오버슈트가 나왔다. 관성, 노이즈, 지연에 대한 요인 설계라면 그 상호작용이 계속되는지, 아니면 한 지점의 우연인지 알 수 있다.
- 고정 지연 대신 지터를 쓴다. 고정 50ms를 시드가 있는 10/30/50ms 지터로 바꾸고, 타임스탬프와 마지막으로 쓴 샘플보다 오래된 샘플을 버리는 규칙을 두고, 고정 30ms 케이스와 비교한다.
- 게인을 바꾼다. 한 게인 세트에 대한 강건성이 일반적인 강건성이 아니다. Kp, Ki, Kd를 달리한 같은 행렬이 결론이 튜닝을 따라가는지 보여준다.
12. 모듈형 휴머노이드 로봇을 위한 교훈
Day 3은 진자에 대한 얘기가 아니었다. 휴머노이드가 필요로 할 인터페이스를, 시스템이 아직 작고 깨끗하게 잴 수 있을 때 테스트하는 일이었다.
첫 번째 교훈은 경계를 어디에 둘지다. PID 제어기는 자기 측정이 진짜인지, 노이즈 섞인 건지, 낡은 건지 모른다. 몰라도 된다. 노이즈와 지연은 관측 파이프라인이 맡고, 제어는 제어기가 맡고, 측정은 평가기가 맡는다. 로봇에 진짜 센서가 달리면 파이프라인만 바뀐다. 두 번째 관절이 생기면 같은 파이프라인과 평가기가 새 어댑터에 적용된다. 이 분리가 이 연구를 재사용할 수 있게 하는 이유고, 노이즈 0, 지연 0 경로가 Day 2를 정확히 재현하는 이유이기도 하다. 제어기 안은 아무것도 안 바뀌었다.
두 번째 교훈은 어떤 지표를 볼지다. 추적 오차만 보면 노이즈 케이스는 괜찮았다. 마지막 2초간 평균 절대 오차 0.4° 미만, 전부 안정. 명령은 달랐다. 열일곱 배의 흔들림, 3.9% 포화. 진짜 관절에서 명령은 모터를 달구고 기어박스를 마모시키는 것이다. 추적 오차만 보고하는 강건성 연구는 잘못된 것을 재고 있다. 미분 입력 오차 RMS가 있는 이유가 정확히 이거다. 토크보다 먼저 명령 문제를 예측한다.
세 번째 교훈은 숫자 하나에 대한 겸손이다. 관성은 마지막 2초간 평균 절대 오차를 거의 안 움직였는데 RMSE는 움직였다. 지연은 RMSE를 좋게 하면서 안정되는 시간을 나쁘게 했다. 약간의 노이즈는 대표 오차를 좋게 하면서 명령을 망가뜨렸다. 그 쌍 중 하나만 인용하면 오해한다. 3번 섹션의 표가 곧 결과다. 어느 한 행이 아니다.
Day 4는 시작하지 않았다. 시작된 건 패턴이다. 불확실성 하나를 골라서, 분리하고, 측정하고, 숫자가 무엇을 말하고 무엇을 말하지 않는지 적는 것. 로봇은 아직 없다. 측정하는 습관은 있다.
Day 1: MuJoCo로 시작하는 첫 물리 시뮬레이션. Day 2: 모찌 새싹 개발기 2편: P·PI·PD·PID 제어기 비교 실험.
업데이트 2026년 11월 10일