Skip to main content
Summary

미분은 로봇에게 "지금 얼마나 변하고 있니?"라고 묻는 일이다

미분이라는 말을 들으면 많은 사람이 먼저 복잡한 기호를 떠올린다. (frac{dx}{dt}), 극한, 접선, 함수 같은 단어들이 한꺼번에 밀려오면 로봇을 만들기도 전에 머리가 먼저 멈춰 버릴 수 있다.

하지만 로봇의 입장에서 미분은 훨씬 단순한 질문이다.

"지금 위치가 얼마나 빠르게 바뀌고 있는가?"

"지금 방향이 얼마나 빠르게 돌아가고 있는가?"

"지금 센서값이 갑자기 튀고 있는가, 천천히 변하고 있는가?"

이 질문에 답하는 도구가 미분이다. 즉 미분은 어려운 기호가 아니라 변화의 속도를 읽는 방법이다. 로봇이 움직인다면 위치가 변한다. 로봇 팔이 회전한다면 관절 각도가 변한다. 카메라가 물체에 가까워진다면 물체까지의 거리가 변한다. 이 모든 변화 앞에서 우리는 같은 질문을 던질 수 있다.

"얼마나 빨리?"

이 장에서는 미분을 로봇 위치 그래프와 센서 데이터로 이해해 볼 것이다. 핵심은 하나다. 연속적인 세계에서는 미분을 순간 변화율이라고 부르고, 컴퓨터 안의 이산 데이터에서는 아주 작은 시간 간격의 변화량으로 근사한다.

미분을 로봇으로 이해하기

평균 변화율: 긴 구간을 한 번에 본 속도

먼저 로봇이 x축 위를 움직인다고 하자. 시간 (t)에 따른 위치 (x(t))가 있다. 1초일 때 로봇 위치가 0.5m이고, 4초일 때 8m라고 해 보자.

이때 1초부터 4초까지의 평균 속도는 다음처럼 계산한다.

[ text{평균 변화율} = frac{x(4) - x(1)}{4 - 1} ]

위치 변화량을 시간 변화량으로 나누는 것이다. 이전 장에서 본 속도 계산과 같다. 평균 변화율은 한 구간 전체를 놓고 "대체로 얼마나 빨랐는가?"를 알려 준다.

자동차 여행을 떠올리면 쉽다. 서울에서 출발해 3시간 동안 240km를 이동했다면 평균 속력은 80km/h다. 하지만 그 3시간 내내 정확히 80km/h로 달렸다는 뜻은 아니다. 신호등 앞에서는 멈췄을 수도 있고, 고속도로에서는 더 빨랐을 수도 있다. 평균 속도는 긴 이야기를 하나의 숫자로 압축한 값이다.

로봇도 마찬가지다. 0초에서 5초까지 평균 속도가 1m/s라고 해서 로봇이 매 순간 1m/s로 움직였다고 말할 수는 없다. 출발할 때는 느렸고, 중간에는 빨랐고, 마지막에는 다시 느려졌을 수 있다.

그래프로 확인해 보자.

import numpy as np
import matplotlib.pyplot as plt

# 로봇 위치 함수: 시간이 지날수록 점점 빨라지는 움직임
def x_func(t):
    return 0.3 * t**2

# 연속적인 곡선처럼 보이도록 촘촘한 시간 배열 생성
t = np.linspace(0, 5, 200)
x = x_func(t)

# 평균 변화율을 계산할 구간
t1 = 1.0
t2 = 4.0
x1 = x_func(t1)
x2 = x_func(t2)
avg_rate = (x2 - x1) / (t2 - t1)

# 두 점을 잇는 직선
line_t = np.array([t1, t2])
line_x = np.array([x1, x2])

plt.figure(figsize=(8, 4))
plt.plot(t, x, label='position x(t)')
plt.plot(line_t, line_x, 'o--', label=f'average rate = {avg_rate:.2f} m/s')
plt.xlabel('time [s]')
plt.ylabel('position x [m]')
plt.title('Average rate of change')
plt.grid(True)
plt.legend()
plt.show()

곡선 위의 두 점을 잇는 점선은 1초부터 4초까지의 평균 움직임을 나타낸다. 이 점선의 기울기가 평균 변화율이다. 기울기란 그래프가 얼마나 가파른지를 뜻한다. 시간-위치 그래프에서 기울기가 크면 위치가 빠르게 변하고, 기울기가 작으면 위치가 천천히 변한다.

중요한 점은 평균 변화율이 구간 전체의 대표값이라는 것이다. 긴 구간을 한 번에 보면 세부적인 움직임은 사라진다. 로봇이 중간에 잠깐 멈췄는지, 갑자기 빨라졌는지, 부드럽게 가속했는지는 평균값 하나만으로 알기 어렵다.

순간 변화율: 바로 지금의 속도

이제 질문을 더 날카롭게 바꿔 보자.

"1초부터 4초까지 평균적으로 얼마나 빨랐는가?"가 아니라 "2초 바로 그 순간에는 얼마나 빨랐는가?"를 알고 싶다면 어떻게 해야 할까?

이 질문이 미분의 출발점이다. 순간 변화율은 아주 짧은 시간 동안의 변화율을 뜻한다. 구간을 점점 짧게 줄여서, 거의 한 순간에 가까운 변화율을 보는 것이다.

예를 들어 2초 근처에서 속도를 알고 싶다고 하자. 먼저 2.0초와 3.0초 사이의 평균 변화율을 계산할 수 있다. 그다음 2.0초와 2.5초 사이를 보고, 더 줄여서 2.0초와 2.1초 사이를 본다. 시간 간격이 작아질수록 우리는 "2초 순간의 속도"에 가까워진다.

수학에서는 이것을 다음처럼 쓴다.

[ frac{dx}{dt} ]

이 기호는 "시간 (t)에 대한 위치 (x)의 변화율"이라는 뜻이다. 어렵게 보이지만 로봇 언어로 번역하면 이렇다.

"위치 x가 지금 초당 얼마나 변하고 있는가?"

이번에는 2초 근처에서 시간 간격을 줄여 가며 변화율이 어떻게 달라지는지 계산해 보자.

t0 = 2.0

for dt in [1.0, 0.5, 0.1, 0.01]:
    rate = (x_func(t0 + dt) - x_func(t0)) / dt
    print(f'dt = {dt:>4}: estimated velocity = {rate:.4f} m/s')

위치 함수가 (x(t) = 0.3t^2)일 때, 실제 순간 속도는 (0.6t)이다. 따라서 2초에서의 속도는 약 1.2m/s다. 출력 결과를 보면 dt가 작아질수록 계산된 변화율이 1.2에 가까워지는 것을 볼 수 있다.

여기서 중요한 감각은 이것이다. 미분은 마법처럼 새로운 값을 꺼내는 공식이 아니다. 우리가 이미 알고 있는 "위치 변화량 나누기 시간 변화량"을 아주 짧은 시간 간격으로 밀어붙인 것이다.

접선의 기울기로 보는 순간 속도

평균 변화율은 곡선 위의 두 점을 잇는 직선의 기울기다. 이 직선을 할선이라고 부른다. 반면 순간 변화율은 한 점에서 곡선에 살짝 닿는 직선의 기울기다. 이 직선을 접선이라고 부른다.

로봇 위치 그래프에서 접선은 그 순간 로봇이 향하고 있는 변화의 방향을 보여 준다. 시간-위치 그래프의 접선이 가파르면 로봇은 빠르게 움직이고, 완만하면 천천히 움직인다.

아래 코드로 같은 위치 곡선 위에 2초 지점의 접선을 그려 보자.

t0 = 2.0
x0 = x_func(t0)
true_velocity = 0.6 * t0  # x(t) = 0.3t^2의 미분값

# 접선: x0 + 기울기 * (t - t0)
tangent_t = np.linspace(0.5, 3.5, 50)
tangent_x = x0 + true_velocity * (tangent_t - t0)

plt.figure(figsize=(8, 4))
plt.plot(t, x, label='position x(t)')
plt.plot(tangent_t, tangent_x, '--', label='tangent at t=2s')
plt.scatter([t0], [x0], color='red', zorder=3, label='t = 2s')
plt.xlabel('time [s]')
plt.ylabel('position x [m]')
plt.title('Instantaneous rate as tangent slope')
plt.grid(True)
plt.legend()
plt.show()

이 그림에서 접선은 2초 순간의 움직임을 직선으로 근사한 것이다. 로봇이 2초 근처에서 아주 짧은 시간 동안 움직인다면, 곡선 전체를 보지 않고 이 접선만 봐도 꽤 잘 설명할 수 있다.

이것이 미분의 강력함이다. 복잡한 곡선 운동도 한순간만 확대해서 보면 직선 운동처럼 다룰 수 있다. 로봇 제어는 이런 생각을 자주 사용한다. 지금 상태를 기준으로 아주 짧은 미래를 예측하고, 다시 센서값을 읽고, 또 아주 짧은 미래를 예측한다. 이런 과정이 빠르게 반복되면 로봇은 마치 부드럽게 판단하며 움직이는 것처럼 보인다.

연속 미분과 이산 미분

수학 교과서에서는 시간이 연속적으로 흐른다고 가정하는 경우가 많다. 1초와 2초 사이에 1.1초, 1.01초, 1.001초가 있고, 그 사이도 끝없이 나눌 수 있다고 생각한다. 이런 세계에서는 (x(t)) 같은 함수가 있고, 그 함수의 미분을 구한다.

하지만 로봇 프로그램이 실제로 보는 세계는 조금 다르다. 센서는 일정한 주기로 값을 보내 준다. 엔코더는 0.01초마다 바퀴 회전량을 알려 줄 수 있고, 카메라는 1초에 30번 이미지를 줄 수 있으며, LiDAR는 0.1초마다 주변 거리 데이터를 줄 수 있다.

컴퓨터가 받는 것은 연속 함수가 아니라 시간표에 찍힌 데이터다.

time:      0.0   0.1   0.2   0.3   0.4   ...
position:  0.0   0.02  0.08  0.18  0.32  ...

그래서 실제 로봇에서는 미분을 그대로 계산하기보다, 인접한 데이터의 차이로 근사한다.

[ v_i approx frac{x_{i+1} - x_i}{t_{i+1} - t_i} ]

시간 간격이 일정해서 (t_{i+1} - t_i = dt)라면 더 간단해진다.

[ v_i approx frac{x_{i+1} - x_i}{dt} ]

이것을 유한 차분이라고 부른다. 이름은 조금 딱딱하지만 의미는 간단하다. 유한한 시간 차이를 사용해서 변화율을 계산한다는 뜻이다.

Python에서는 np.diff 또는 np.gradient로 이런 계산을 쉽게 할 수 있다. 이전 장에서 이미 맛보았던 도구들이다. 이번에는 시간 간격이 결과에 어떤 영향을 주는지 더 자세히 들여다보자.

시간 간격이 작아지면 무엇이 좋아질까

먼저 노이즈가 없는 깨끗한 위치 데이터를 생각해 보자. 로봇이 다음 위치 함수에 따라 움직인다고 하겠다.

[ x(t) = sin(t) + 0.2t ]

이 로봇은 전체적으로 앞으로 가면서, 약간 물결치듯 움직인다. 이 위치의 실제 속도는 (cos(t) + 0.2)이다. 우리는 실제 속도를 알고 있으므로, 샘플링 간격 dt에 따라 근사 속도가 얼마나 정확한지 비교할 수 있다.

def position(t):
    return np.sin(t) + 0.2 * t

def true_velocity(t):
    return np.cos(t) + 0.2

# 비교할 시간 간격들
dts = [0.5, 0.1, 0.02]

plt.figure(figsize=(9, 5))

# 실제 속도 곡선
t_fine = np.linspace(0, 10, 1000)
plt.plot(t_fine, true_velocity(t_fine), 'k', linewidth=3, label='true velocity')

for dt in dts:
    t_sample = np.arange(0, 10 + dt, dt)
    x_sample = position(t_sample)
    v_est = np.gradient(x_sample, dt)

    plt.plot(t_sample, v_est, marker='o', markersize=3, label=f'estimated dt={dt}')

plt.xlabel('time [s]')
plt.ylabel('velocity [m/s]')
plt.title('Velocity estimation with different dt')
plt.grid(True)
plt.legend()
plt.show()

그래프를 보면 dt = 0.5일 때는 속도 곡선이 조금 거칠게 따라간다. 샘플 사이가 넓어서 곡선의 세부 변화가 잘 보이지 않기 때문이다. dt = 0.1이면 훨씬 부드럽고, dt = 0.02에서는 실제 속도와 거의 겹쳐 보인다.

깨끗한 데이터에서는 시간 간격이 작을수록 순간 변화율에 더 가까워진다. 카메라로 비유하면 프레임 수가 많아지는 것과 비슷하다. 1초에 한 장만 찍으면 로봇이 어디서 어떻게 움직였는지 듬성듬성 보인다. 1초에 100장을 찍으면 훨씬 자세히 볼 수 있다.

하지만 실제 센서 데이터에서는 이야기가 조금 복잡해진다. 시간 간격이 작아지면 좋은 점도 있지만, 노이즈가 있을 때는 오히려 속도 추정이 더 흔들릴 수 있다.

센서 위치 데이터에서 속도 추정하기

이번에는 우리가 위치 함수를 모른다고 생각해 보자. 실제 로봇에서는 대개 이쪽이 더 현실적이다. 로봇은 완벽한 수식으로 움직이지 않는다. 대신 센서가 측정한 위치 데이터가 있다.

예를 들어 비전 시스템이나 외부 모션 캡처 장비가 로봇의 x 위치를 측정해 준다고 하자. 우리는 이 위치 기록에서 속도를 추정하고 싶다.

# 가상의 센서 기록 생성
np.random.seed(0)

dt = 0.1
t = np.arange(0, 10 + dt, dt)

# 실제 위치와 실제 속도
x_true = position(t)
v_true = true_velocity(t)

# 센서가 측정한 위치: 지금은 노이즈 없이 시작
x_measured = x_true.copy()

# 위치 데이터에서 속도 추정
v_from_diff = np.diff(x_measured) / dt
t_diff = t[:-1]

v_from_gradient = np.gradient(x_measured, dt)

plt.figure(figsize=(9, 5))
plt.plot(t, v_true, label='true velocity', linewidth=3)
plt.plot(t_diff, v_from_diff, '--', label='diff estimate')
plt.plot(t, v_from_gradient, ':', label='gradient estimate', linewidth=3)
plt.xlabel('time [s]')
plt.ylabel('velocity [m/s]')
plt.title('Velocity from position data')
plt.grid(True)
plt.legend()
plt.show()

np.diff는 바로 다음 위치에서 현재 위치를 빼므로 결과 길이가 하나 줄어든다. 그래서 시간 배열도 t[:-1]처럼 맞춰 주었다. np.gradient는 앞뒤 값을 적절히 사용해서 원래 배열과 같은 길이의 변화율을 만들어 준다.

둘 중 어느 것이 무조건 더 좋다고 외우기보다는, 배열 길이와 시간 위치가 다르다는 점을 이해하는 것이 중요하다. np.diff로 계산한 속도는 사실 t[i]와 t[i+1] 사이 구간의 평균 속도다. 반면 np.gradient는 각 샘플 시각 근처의 변화율처럼 다루기 편하다.

로봇 로그를 분석할 때 이 차이는 생각보다 중요하다. 속도 그래프가 위치 그래프보다 반 박자 밀려 보이거나, 배열 길이가 맞지 않아 그림이 안 그려지는 일이 자주 생긴다. 그런 문제가 생기면 먼저 "이 속도값은 어느 시간 구간을 대표하는가?"를 확인해야 한다.

dt가 너무 크면 급한 변화를 놓친다

이번에는 로봇이 중간에 잠깐 빠르게 움직이는 상황을 만들어 보자. 평소에는 천천히 움직이다가 5초 근처에서 갑자기 속도가 커지는 로봇이다. 이런 움직임은 장애물을 피하려고 순간적으로 방향을 바꾸거나, 목표점을 지나치지 않기 위해 급하게 감속하는 장면에서 나타날 수 있다.

def burst_position(t):
    # 기본 전진 + 5초 근처에서 급격한 위치 변화
    return 0.2 * t + 1.5 / (1 + np.exp(-8 * (t - 5)))

def estimate_velocity(dt):
    t_sample = np.arange(0, 10 + dt, dt)
    x_sample = burst_position(t_sample)
    v_est = np.gradient(x_sample, dt)
    return t_sample, x_sample, v_est

# 매우 촘촘하게 계산한 값을 기준처럼 사용
t_ref, x_ref, v_ref = estimate_velocity(0.005)

plt.figure(figsize=(9, 5))
plt.plot(t_ref, v_ref, 'k', linewidth=3, label='fine reference')

for dt in [0.8, 0.2, 0.05]:
    t_sample, x_sample, v_est = estimate_velocity(dt)
    plt.plot(t_sample, v_est, marker='o', markersize=4, label=f'dt={dt}')

plt.xlabel('time [s]')
plt.ylabel('estimated velocity [m/s]')
plt.title('Large dt can miss fast changes')
plt.grid(True)
plt.legend()
plt.show()

dt = 0.8처럼 시간 간격이 크면 5초 근처의 급한 변화가 둔하게 보인다. 실제로는 짧은 시간 동안 속도가 크게 올라갔는데, 샘플이 듬성듬성 찍히면 그 변화가 평균 속도 안에 묻혀 버린다.

이것은 실제 로봇에서 매우 중요한 문제다. 제어 주기가 너무 느리면 로봇은 빠른 변화를 제때 보지 못한다. 센서가 느리면 장애물이 갑자기 나타났을 때 반응이 늦을 수 있다. 로그 저장 주기가 너무 낮으면 나중에 분석할 때 중요한 순간을 놓칠 수 있다.

따라서 dt는 단순한 코드 속 숫자가 아니다. 로봇이 세상을 얼마나 자주 관찰하고, 얼마나 자주 판단하고, 얼마나 자주 행동을 수정하는지를 결정하는 값이다.

노이즈가 들어오면 미분은 예민해진다

이제 진짜 로봇다운 문제를 만나 보자. 센서는 항상 조금씩 틀린다. 바퀴 엔코더에는 미끄러짐이 있고, 카메라 위치 추정에는 흔들림이 있으며, 거리 센서는 표면 상태에 따라 값이 튈 수 있다.

위치 데이터에 작은 노이즈가 섞이면 어떤 일이 일어날까? 눈으로 보면 위치 그래프는 그럭저럭 괜찮아 보일 수 있다. 하지만 그 위치를 미분해서 속도를 구하면 흔들림이 크게 드러난다.

이유는 간단하다. 미분은 차이를 보는 연산이기 때문이다. 두 위치값이 아주 조금만 흔들려도, 그 차이를 작은 dt로 나누면 속도값은 크게 튈 수 있다.

np.random.seed(1)

dt = 0.05
t = np.arange(0, 10 + dt, dt)

x_true = position(t)
v_true = true_velocity(t)

noise_std = 0.03
noise = np.random.normal(0, noise_std, size=len(t))
x_noisy = x_true + noise

v_noisy = np.gradient(x_noisy, dt)

fig, axes = plt.subplots(2, 1, figsize=(9, 7), sharex=True)

axes[0].plot(t, x_true, label='true position', linewidth=3)
axes[0].plot(t, x_noisy, '.', markersize=3, label='noisy measured position')
axes[0].set_ylabel('position [m]')
axes[0].set_title('Position data with noise')
axes[0].grid(True)
axes[0].legend()

axes[1].plot(t, v_true, label='true velocity', linewidth=3)
axes[1].plot(t, v_noisy, label='velocity from noisy position')
axes[1].set_xlabel('time [s]')
axes[1].set_ylabel('velocity [m/s]')
axes[1].set_title('Differentiation amplifies noise')
axes[1].grid(True)
axes[1].legend()

plt.tight_layout()
plt.show()

위쪽 위치 그래프에서는 노이즈가 작아 보일 수 있다. 점들이 실제 곡선 주변에 살짝 흩어져 있을 뿐이다. 그런데 아래쪽 속도 그래프를 보면 훨씬 거칠다. 위치에서는 귀여운 잔물결처럼 보이던 노이즈가, 속도에서는 갑자기 소란스러운 진동으로 변한다.

이것이 센서 데이터에서 단순 미분을 조심해야 하는 이유다. 위치값을 그냥 빼고 dt로 나누면 속도 추정이 너무 흔들릴 수 있다. 로봇 제어기가 이 흔들리는 속도를 그대로 믿으면, 모터 명령도 같이 흔들릴 수 있다. 그러면 로봇은 부드럽게 움직이지 못하고 덜덜 떨거나 불안정해질 수 있다.

작은 dt는 항상 좋은가

앞에서는 dt가 작을수록 순간 변화율에 가까워진다고 했다. 그런데 방금은 작은 dt로 나누면 노이즈가 커질 수 있다고 했다. 둘 중 어느 말이 맞을까?

둘 다 맞다. 단, 조건이 다르다.

깨끗한 위치 함수나 노이즈가 거의 없는 데이터에서는 dt가 작을수록 미분 근사가 좋아진다. 더 촘촘하게 관찰하므로 순간 변화율에 가까워지기 때문이다. 하지만 센서 노이즈가 있는 데이터에서는 dt가 작아질수록 위치 오차를 더 작은 시간으로 나누게 된다. 그러면 속도 추정의 흔들림이 커질 수 있다.

코드로 직접 보자. 같은 크기의 위치 노이즈가 있을 때, dt를 바꾸며 속도 추정이 얼마나 흔들리는지 비교해 보겠다.

np.random.seed(2)

noise_std = 0.02
dts = [0.2, 0.05, 0.01]

fig, axes = plt.subplots(len(dts), 1, figsize=(9, 8), sharex=True)

for ax, dt in zip(axes, dts):
    t_sample = np.arange(0, 10 + dt, dt)
    x_true = position(t_sample)
    v_true = true_velocity(t_sample)

    noise = np.random.normal(0, noise_std, size=len(t_sample))
    x_noisy = x_true + noise
    v_est = np.gradient(x_noisy, dt)

    ax.plot(t_sample, v_true, linewidth=3, label='true velocity')
    ax.plot(t_sample, v_est, label='estimated from noisy position')
    ax.set_title(f'dt = {dt}')
    ax.set_ylabel('velocity')
    ax.grid(True)
    ax.legend(loc='upper right')

axes[-1].set_xlabel('time [s]')
plt.tight_layout()
plt.show()

이 실험은 중요한 균형을 보여 준다. 너무 큰 dt는 빠른 변화를 놓친다. 너무 작은 dt는 노이즈를 크게 키울 수 있다. 실제 로봇에서는 센서 주기, 노이즈 크기, 제어기의 반응 속도, 로봇의 물리적 움직임을 함께 고려해야 한다.

그래서 로봇공학에서는 속도를 추정할 때 단순 차분만 쓰지 않는 경우가 많다. 데이터를 부드럽게 만든 뒤 미분하거나, 여러 센서를 함께 사용하거나, 나중에 배울 칼만 필터 같은 추정 기법을 사용한다. 지금은 일단 한 가지만 기억하자.

미분은 변화에 민감하다. 그래서 진짜 변화도 잘 잡지만, 노이즈에도 민감하다.

간단한 이동 평균으로 흔들림 줄이기

노이즈가 있는 위치 데이터에서 속도를 계산하기 전에, 위치 데이터를 조금 부드럽게 만들면 속도 추정의 흔들림이 줄어들 수 있다. 가장 단순한 방법 중 하나는 이동 평균이다.

이동 평균은 주변 몇 개 값을 평균 내어 현재 값을 조금 부드럽게 만드는 방법이다. 예를 들어 현재 위치 하나만 보지 않고, 앞뒤의 가까운 위치들을 함께 보면서 튀는 값을 누그러뜨린다.

def moving_average(data, window_size):
    kernel = np.ones(window_size) / window_size
    return np.convolve(data, kernel, mode='same')

np.random.seed(3)

dt = 0.05
t = np.arange(0, 10 + dt, dt)
x_true = position(t)
v_true = true_velocity(t)

x_noisy = x_true + np.random.normal(0, 0.03, size=len(t))
x_smooth = moving_average(x_noisy, window_size=7)

v_noisy = np.gradient(x_noisy, dt)
v_smooth = np.gradient(x_smooth, dt)

fig, axes = plt.subplots(2, 1, figsize=(9, 7), sharex=True)

axes[0].plot(t, x_true, linewidth=3, label='true position')
axes[0].plot(t, x_noisy, '.', markersize=3, label='noisy position')
axes[0].plot(t, x_smooth, label='smoothed position')
axes[0].set_ylabel('position')
axes[0].grid(True)
axes[0].legend()

axes[1].plot(t, v_true, linewidth=3, label='true velocity')
axes[1].plot(t, v_noisy, alpha=0.6, label='velocity from noisy position')
axes[1].plot(t, v_smooth, label='velocity after smoothing')
axes[1].set_xlabel('time [s]')
axes[1].set_ylabel('velocity')
axes[1].grid(True)
axes[1].legend()

plt.tight_layout()
plt.show()

부드럽게 만든 위치에서 속도를 계산하면 그래프의 진동이 줄어드는 것을 볼 수 있다. 하지만 이동 평균에도 대가가 있다. 너무 많이 평균 내면 실제로 빠르게 변한 부분까지 둔하게 만들어 버린다. 즉 노이즈는 줄지만 반응이 느려질 수 있다.

로봇에서는 이런 타협이 자주 등장한다. 센서값을 너무 믿으면 명령이 흔들리고, 너무 부드럽게 만들면 반응이 늦어진다. 좋은 로봇 시스템은 이 둘 사이에서 적절한 균형을 찾는다.

2D 로봇 위치 데이터에서 속도 벡터 추정하기

지금까지는 1차원 위치 (x(t))로 설명했다. 하지만 평면 위의 이동 로봇은 보통 (x, y) 두 좌표를 가진다. 이때 미분은 각 좌표에 따로 적용하면 된다.

[ mathbf{p}(t) = begin{bmatrix} x(t) \ y(t) end{bmatrix} ]

[ mathbf{v}(t) = frac{dmathbf{p}}{dt} = begin{bmatrix} frac{dx}{dt} \ frac{dy}{dt} end{bmatrix} ]

즉 x 위치의 변화율이 (v_x), y 위치의 변화율이 (v_y)다. 두 값을 합치면 속도 벡터가 된다.

dt = 0.05
t = np.arange(0, 12 + dt, dt)

# 2D 궤적: 앞으로 나아가며 물결치는 로봇
x = 0.4 * t
y = np.sin(0.8 * t)
positions = np.column_stack([x, y])

# 위치 데이터에서 속도 벡터 추정
velocities = np.gradient(positions, dt, axis=0)
speed = np.linalg.norm(velocities, axis=1)

step = 12

fig, axes = plt.subplots(1, 2, figsize=(12, 5))

axes[0].plot(positions[:, 0], positions[:, 1], label='trajectory')
axes[0].quiver(
    positions[::step, 0], positions[::step, 1],
    velocities[::step, 0], velocities[::step, 1],
    angles='xy', scale_units='xy', scale=2.5,
    color='orange', label='estimated velocity'
)
axes[0].axis('equal')
axes[0].set_xlabel('x [m]')
axes[0].set_ylabel('y [m]')
axes[0].set_title('Velocity vectors on 2D path')
axes[0].grid(True)
axes[0].legend()

axes[1].plot(t, velocities[:, 0], label='vx')
axes[1].plot(t, velocities[:, 1], label='vy')
axes[1].plot(t, speed, linewidth=3, label='speed')
axes[1].set_xlabel('time [s]')
axes[1].set_ylabel('velocity')
axes[1].set_title('Velocity components')
axes[1].grid(True)
axes[1].legend()

plt.tight_layout()
plt.show()

왼쪽 그림에서는 궤적 위에 속도 벡터가 화살표로 표시된다. 화살표는 로봇이 그 순간 어느 방향으로 움직이고 있는지 보여 준다. 오른쪽 그래프에서는 x 방향 속도, y 방향 속도, 그리고 속력의 변화를 볼 수 있다.

여기서 np.gradient(positions, dt, axis=0)가 중요하다. axis=0은 시간 방향으로 변화율을 계산하라는 뜻이다. positions 배열에서 각 행은 시간 순서대로 쌓인 위치다. 따라서 위아래 행끼리 비교해야 위치의 시간 변화율이 나온다.

만약 실수로 axis=1을 사용하면 x와 y 사이의 차이를 계산하게 된다. 그것은 시간에 따른 움직임이 아니라 좌표 성분끼리의 차이다. 로봇 데이터는 배열 모양이 조금만 달라도 의미가 크게 달라지므로, 항상 "어느 축이 시간인가?"를 먼저 확인해야 한다.

각도도 미분하면 각속도가 된다

미분은 위치에만 적용되는 것이 아니다. 로봇의 방향 (theta)도 시간에 따라 변한다. 방향의 변화율은 각속도다.

[ omega = frac{dtheta}{dt} ]

이전 장에서 본 것처럼 2D 이동 로봇은 보통 ([x, y, theta])라는 pose를 가진다. 여기서 (x), (y)를 미분하면 선속도 성분이 나오고, (theta)를 미분하면 각속도가 나온다.

dt = 0.05
t = np.arange(0, 8 + dt, dt)

# 로봇 방향이 시간에 따라 부드럽게 변한다고 가정
theta = 0.5 * np.sin(t) + 0.2 * t
omega = np.gradient(theta, dt)

fig, axes = plt.subplots(2, 1, figsize=(8, 6), sharex=True)

axes[0].plot(t, theta)
axes[0].set_ylabel('theta [rad]')
axes[0].set_title('Robot heading angle')
axes[0].grid(True)

axes[1].plot(t, omega, color='orange')
axes[1].set_xlabel('time [s]')
axes[1].set_ylabel('omega [rad/s]')
axes[1].set_title('Angular velocity from heading')
axes[1].grid(True)

plt.tight_layout()
plt.show()

각속도 그래프가 양수이면 방향 각도가 증가하고 있다는 뜻이다. 보통 반시계 방향으로 회전한다고 해석한다. 각속도가 음수이면 시계 방향으로 회전하고 있는 것이다. 각속도의 크기가 클수록 더 빠르게 돈다.

다만 각도 데이터에는 특별한 주의점이 있다. 각도는 (pi)와 (-pi) 사이에서 갑자기 점프한 것처럼 표현될 수 있다. 실제 로봇은 부드럽게 회전했는데, 기록된 각도는 3.13에서 -3.13으로 튀는 식이다. 이런 상태에서 그대로 차이를 구하면 엄청나게 큰 각속도가 나온다.

이 문제를 줄이려면 np.unwrap을 사용할 수 있다. unwrap은 각도가 경계에서 갑자기 점프한 것처럼 보이는 부분을 이어 붙여, 연속적으로 변하는 각도처럼 만들어 준다.

# -pi ~ pi 범위로 감긴 각도 예시
theta_wrapped = np.arctan2(np.sin(theta), np.cos(theta))

omega_bad = np.gradient(theta_wrapped, dt)
omega_good = np.gradient(np.unwrap(theta_wrapped), dt)

plt.figure(figsize=(9, 4))
plt.plot(t, omega_bad, label='without unwrap')
plt.plot(t, omega_good, linewidth=3, label='with unwrap')
plt.xlabel('time [s]')
plt.ylabel('angular velocity [rad/s]')
plt.title('Angle wrapping affects differentiation')
plt.grid(True)
plt.legend()
plt.show()

각도는 원 위의 값이기 때문에 일반 숫자처럼 빼면 문제가 생길 때가 있다. 로봇의 방향, 관절 각도, IMU yaw 값을 미분할 때는 각도 래핑을 항상 의심해야 한다.

미분을 로봇 제어의 언어로 번역하기

이제 미분 기호를 로봇의 말로 번역할 수 있다.

(frac{dx}{dt})는 x 위치가 지금 얼마나 빠르게 바뀌는지다. (frac{dy}{dt})는 y 위치가 지금 얼마나 빠르게 바뀌는지다. (frac{dtheta}{dt})는 로봇 방향이 지금 얼마나 빠르게 돌아가는지다.

로봇 제어기는 이런 변화율을 계속 확인한다. 목표보다 너무 느리면 속도를 올리고, 너무 빠르면 줄인다. 방향 오차가 크면 각속도를 만들고, 목표 방향에 가까워지면 회전을 줄인다. 센서값이 갑자기 변하면 장애물이나 미끄러짐 같은 사건이 생겼는지 의심한다.

미분은 그래서 로봇에게 현재를 읽는 눈이다. 위치만 보면 "어디에 있는가?"를 알 수 있다. 미분하면 "어떻게 움직이고 있는가?"를 알 수 있다. 한 번 더 미분하면 "움직임이 얼마나 거칠게 바뀌고 있는가?"까지 볼 수 있다.

물론 미분은 예민하다. 깨끗한 데이터에서는 순간의 움직임을 날카롭게 잡아내지만, 노이즈가 있는 데이터에서는 흔들림도 함께 키운다. 그래서 실제 로봇에서는 미분값을 계산할 때 시간 간격, 센서 주기, 노이즈, 필터링을 함께 생각해야 한다.

이번 장에서 잡아야 할 감각

미분은 공식 암기보다 감각이 먼저다. 로봇이 움직이는 장면을 떠올리며 다음 흐름을 연결해 보자.

위치 그래프에서 두 점을 멀리 잡으면 평균 변화율이 나온다. 두 점을 점점 가깝게 잡으면 순간 변화율에 가까워진다. 이 순간 변화율이 미분이고, 로봇 위치를 미분하면 속도가 된다.

컴퓨터 안에서는 연속적인 미분을 직접 계산하지 않는다. 대신 일정한 시간 간격으로 찍힌 위치 데이터를 사용해 변화율을 근사한다. 이때 dt가 너무 크면 빠른 변화를 놓칠 수 있고, 노이즈가 있는 상황에서 dt가 너무 작으면 속도 추정이 크게 흔들릴 수 있다.

센서 데이터에서 속도를 구하는 일은 단순히 np.diff(x) / dt를 쓰는 것으로 끝나지 않는다. 그 값이 어느 시간 구간을 대표하는지, 센서 노이즈가 얼마나 있는지, 필요하다면 데이터를 얼마나 부드럽게 만들지 함께 생각해야 한다.

이제 우리는 위치에서 속도를 읽는 법을 배웠다. 다음 장에서는 반대 방향으로 걸어가 볼 것이다. 속도를 시간에 따라 조금씩 누적하면 위치가 된다. 작은 이동을 계속 더해 전체 이동을 만드는 과정, 그것이 적분이다.

책 목록

로봇을 위한 수학과 Python: 좌표에서 시뮬레이터까지

전체 목차 보기

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기