지난 포스팅에서 Localization에 대한 2가지 방법으로 칼만필터와 파티클 필터에 대해 알아봤습니다.
https://dev-cock.tistory.com/9
[Localization] 칼만 필터 Kalman Filter
본 글은 과거 블로그에서 퍼왔습니다. 이번 포스팅에서는 지난 학기 수강했던(Udacity) cs7838 (Artificial Intelligence for Robotics) 과목과 Jonathan Hui의 글을 번역하여 구성하였습니다. 모든 자료 위 강의..
dev-cock.tistory.com
https://dev-cock.tistory.com/10
[Localization] Particle Filter 파티클 필터
지난 칼만필터 포스팅에 이어, 이번 포스팅에서는 particle filter에 대해 다뤄보겠습니다. cs7638 과제를 하면서 가장 재밌고 직관적으로 다가왔던 필터이기도 합니다. 이번 포스팅도 cs7638 AI4R의 유
dev-cock.tistory.com
이번에는 PID 제어를 통해 목표값에 부드럽게 다가가는 알고리즘에 대해 알아보겠습니다. 몇년 전에도 PID 제어에 대해 코드 베이스로 다뤄본 적이 있는데요. 해당 포스팅은 아래 글에서 보실 수 있습니다. 몇년 전까지만 해도 PID 제어를 검색하면 구글 검색 상위권에 노출되던 글이었는데, 요새는 시간이 많이 흘러서 대체되었는지 모르겠습니다.
https://hyongdoc.tistory.com/49
드론 DIY | 드론 PID 제어 이론 1
이번 편은 드론 자세 제어에 가장 많이 등장하는 PID 제어의 개념에 대한 포스팅 1편!! 처음 드론을 만드는 사람 입장에서는 제어라는 개념이 생소할 수도 있다. 거기에 PID.. 뭔지 모를 영어까지
hyongdoc.tistory.com
https://hyongdoc.tistory.com/51
드론 DIY | 드론 PID 제어 이론 2
지난 포스팅을 요약하자면, 1. 드론 자세제어에는 PID가 필요하다. 2. PID란, 비례 적분 미분의 약어이다. 3. PID의 원리를 진자운동에 비유했는데, 이를테면 드론을 10도 기울여라~~ 란 명령을 주면,
hyongdoc.tistory.com
위 두 글은 드론 자세제어를 예시로 들며 PID 제어를 쉽게 설명하는데 초점을 맞췄습니다. 이번 포스팅에서는 실사례보다는, 개념적인 부분 위주로 깔끔 명료하게 글을 써보고자 합니다.
PID 제어가 언제 필요할까요?
PID 제어를 비례, 적분, 미분으로 설명하기 전에, 왜 이 제어가 필요한지에 대해 생각해볼 필요가 있습니다. 몇년 전 드론 글을 작성할 때만해도 드론을 위한 PID 제어 글이 많았는데 요새는 확실히 화두가 자율주행인 것 같습니다. (물론 그 때도 없었던 건 아니지만요)
자율주행차가 차선을 인지하고, 스티어링 휠을 조절하는 로직을 갖고 있을 것입니다. 어떤 인지적 기능에 의해서 스티어링 휠을 5도 틀라는 명령을 내릴텐데, 컴퓨터가 가고자 하는 루트대로 100% 정확하게 갈 수는 없을 노릇입니다. 노면으로부터 노이즈도 있을 것이고, 중간중간 끼어드는 차들로 인해 속도를 줄이거나, 늘리는 과정도 있을테니 정확히는 5도 돌리라기보다는, 가상의 선을 따라 가라는 명령을 내릴 겁니다.
가상의 선보다 바깥으로 조향이 되고 있을 경우 스티어링을 더 돌려야할 것이고, 더 내측으로 조향이 되고 있을 경우 스티어링을 덜 돌려야할 것입니다. (물론 차속을 조절할 수도 있겠지만 이 포스팅에서는 단순하게 생각해보겠습니다.)
가상의 선에 맞춰서 스티어링을 더, 혹은 덜 돌릴 수 있도록 하는 제어가 바로 PID 제어이며, 해당 제어가 필요한 이유가 되겠습니다.
Proportional Control (비례 제어)
P제어를 알아보자.
PID제어의 시작으로 P제어에 대해 알아보겠습니다. 사진자료들은 OMSCS cs7638 자료에서 발췌해왔습니다.
방금 위에서 기준이 되는 가상의 선이라고 말씀을 드렸었는데요. 그 가상의 선과, 현재 선의 차이를 Cross Track Error (CTE) 라고 합니다.
설명을 간결하기 위해, 기준이 되는 선은 y=0 이라고 하고, 현재 나의 위치를 y 라고 가정해보겠습니다. 이 때, 스티어링을 조작해서 기준의 되는 선에 가려면 어떻게 해야할까요?
CTE 의 크기를 이용해서 스티어링에 반영해줘 봅시다. 에러가 클수록, 스티어링의 각도가 커질 것이고 당연히 기준 선으로 차가 향하기 시작할 것입니다. 그러다가 CTE가 줄어들수록, 각도가 점점 작아져서 원하는 기준 선에 도달할 수 있게 됩니다.
이 때 튜닝 파라미터는 Pgain을 곱셈 인자로 넣어줌으로써 보다 정밀하게 제어할 수 있습니다. 이것이 바로 P 제어입니다.
위 식에서 알파는 스티어링 각도, 타우P는 P게인, CTE_t는 시간에 대한 에러를 의미합니다. 직관적으로도 이해가 쉽지만, 단순히 에러에 파라미터를 곱해서 조향해주는 방식입니다.
직관적으로, 위 사진처럼 자동차는 조향을 틀어서 CTE를 줄이게 되고, 기준선 이상으로 차가 이동했을 경우, CTE부호가 바뀌게 되고, 다시말해 스티어링에 인가되는 a값의 부호가 바뀌면서 반대로 조향이 될 것입니다. 위 그림처럼 일종의 fluctuation을 가지면서 원하는 목표값에 도달하게 됩니다. P제어의 Pseudo Code는 다음과 같습니다.
for everyLoop of SW:
# vehicle: vehicle
# y: vehicle's y position
crossTrackError = vehicle.y
# Pgain: P gain
steering = -Pgain * crossTrackError
# move with calculated steer
vehicle.move(steering)
크게 어려울 것은 없습니다. 프로그램이 돌아가는 매 loop마다 갱신되는 CTE를 계산하고, P게인 파라미터를 곱한 뒤 스티어링에 전달해주면 됩니다.
P 제어의 문제점 2가지
안타깝게도, P제어만 사용했을 때는 크게 2가지 문제가 있습니다.
첫번째는 진동이 발생할 수 있다는 점입니다. 위 그림처럼 아름답게 기준선에 수렴하면 좋겠지만, 차량의 스티어링이 생각보다 정교하지 않을 수 있습니다. 스티어링을 할수록 차가 팍팍 튀어나가는 느낌이랄까요. 아래 그림처럼 기준선인 Setpoint 대비 위 아래로 진동하는 모습을 종종 보입니다. 또 P게인이 너무 클 경우, 진동하다가 오히려 발산해버리는 경우가 발생하기도 합니다.
또 다른 문제는 Setpoint에 도달하지 않을 수도 있다는 점입니다. 아래 사진과 같이 처음엔 CTE가 작아지는 방향으로 차량이 나아가다가, 특정 거리 이상으로는 좁혀지지 않는 것입니다.
에러가 작아진다는 것은, 스티어링에 전달되는 각도의 값도 작아지는 것을 의미합니다. 처음에는 스티어링 각도를 10도 꺾다가, 나중에는 0.5도 꺾으라는 명령이 들어가게 되는 셈입니다. 그런데 0.5도 각도는 정~말 작아서, 노면의 마찰에 의해 조향이 안되는 경우가 발생할 수도 있는 것입니다. 즉, 마찰을 이겨낼 만큼 충분한 조향각이 주어져야하는데, 너무 작다보니 더이상 조향이 되지 않는 식입니다.
안타깝게도 P게인 하나만으로는 위 상황에서 벗어날 방법이 없습니다. 다른 제어 방식이 추가로 필요한 이유입니다.
Differential Control (미분 제어)
위에서 언급한 P제어의 첫번째 문제를 해결하기 위한 것이 바로 미분 제어입니다. 계속 진동하거나, 초기에 진동하는 것처럼 튀는 것을 Overshoot이라고 합니다. 말그대로 진동하면서 슛하듯이 값이 튄다는 의미입니다. 정밀성을 요구하는 제어에서 이런 오버슛은 문제를 일으킬 수 있으므로 최소화하면 당연히 좋겠죠?
비례 제어가 단순히 파라미터를 곱셈하여 '비례' 성분을 나타냈듯이, 미분 제어는 미분계수 값을 곱셈하여 '미분' 성분을 반영합니다. 아래 수식으로 나타낼 수 있습니다.
아까와 차이가 있다면, CTE의 미분값이 들어간다는 점입니다. 여기서 미분값은, 이전 루프의 에러와 현재 루프의 에러를 루프 간 시간 차이인 델타 t로 나눠준 값입니다. 따라서 미분 제어를 위한 pseudo code는 다음과 같이 작성해볼 수 있습니다.
for everyLoop of SW:
# vehicle: vehicle
# y: vehicle's y position
diff_crossTrackError = vehicle.y - crossTrackError
crossTrackError = vehicle.y
# Pgain: P gain
steering = -Pgain * crossTrackError - Dgain * diff_crossTrackError
# move with calculated steer
vehicle.move(steering)
이렇게 P와 D제어를 합친 것을 PD제어라고 할 수 있고, 그 결과물은 다음과 같습니다. 에러의 시간에 대한 미분 개념이 스티어링에 전달되는 a값에 반영되어 있기 때문에, 지나친 오버슛이 발생하는 것을 억제합니다. 부호 차이를 이용해서 스티어링의 각도에 전달되는 a값의 변동폭이 줄어든다고도 이해해볼 수 있겠습니다.
위 사진의 예시처럼, 오버슛이 발생하는 것을 억제합니다.
그런데 D게인 만으로도 역시 P제어의 두번째 문제, 영원히 기준선에 도달하지 못하는 문제는 해결하기가 쉽지 않아보입니다. 이 때 필요한 것이 바로 I제어입니다.
Integral Control (적분 제어)
적분 제어도 말그대로 적분하는 방식을 의미합니다. 고등학교 때 구분구적법에 대해 배워보신 분들은 아시겠지만, 적분을 이산적인 단위로 뜯어보면 결국 시간 단위 별로 y값을 곱해서 그래프의 면적을 계산하는 방식인데요.
P제어의 두번째 문제점을 다시 가져와보고 생각해보겠습니다.
적분은 시간에 대해 CTE를 계속 적분합니다. 즉, 위 상황처럼 작은 CTE가 계속 남아있다고 할때, 지속적으로 작은 면적들이 스티어링으로 전달할 인자인 a 값에 더해지는 방식입니다. 다시말해, 스티어링 각도가 조금씩조금씩 더 커지면서 어느 임계점을 넘는 순간, CTE가 작아지기 시작하는 것입니다. Pseudo code는 다음과 같습니다.
for everyLoop of SW:
# vehicle: vehicle
# y: vehicle's y position
integral_crossTrackError += crossTrackError
diff_crossTrackError = vehicle.y - crossTrackError
crossTrackError = vehicle.y
# Pgain: P gain
steering = -Pgain * crossTrackError - Dgain * diff_crossTrackError - Igain * int_crossTrackError
# move with calculated steer
vehicle.move(steering)
이렇게 PID제어에 대해 가볍게 알아봤는데요. 막상 실제 적용하려다보면 또 문제점에 봉착하게 됩니다. 바로 P, I, D 제어에 적용되는 각각의 파라미터들을 튜닝해야한다는 점인데요.
몇년전에 드론 자세제어 할때는 거의 초기 변수를 그저 1, 1, 1로 잡고 시작했던 것 같습니다. Trial and Error 방식으로 엄청난 노력을 했던 것 같은데...
PID 제어 파라미터 튜닝
Thrun 교수가 'Twiddle' 이라고 부른 방법에 대해 소개해보고자 합니다. 엄청난 내용은 아니지만, 튜닝할 때 도움이 되는 것 같습니다. 일단 코드부터 보고 시작하겠습니다.
def twiddle(tol = 0.2): #Make this tolerance bigger if you’re timing out!
n_params = 3
dparams = [1.0 for row in range(n_params)]
params = [0.0 for row in range(n_params)]
best_error = run(params)
n = 0
while sum(dparams) > tol:
for i in range(len(params)):
params[i] += dparams[i]
err = run(params)
if err < best_error:
best_error = err
dparams[i] *= 1.1
else:
params[i] = 2.0 * dparams[i]
err = run(params)
if err < best_error:
best_error = err
dparams[i] *= 1.1
else:
params[i] += dparams[i]
dparams[i] *= 0.9
n += 1
print ‘Twiddle #’, n, params, ‘ > ‘, best_error
return run(params)
코드를 보면 알겠지만, 3가지 파라미터에 대해 for문을 돌리고, 그 for문은 while loop 안에서 계속 돌게 되어 있습니다. 각각의 파라미터 변동량을 dparam이라는 변수로 저장하고 있는데, 변동량 자체가 아주 작아질 경우 어느정도 PID 파라미터가 안정화되었다고 판단하여 루프를 빠져나오는 구조입니다. 대체로 정리하면 다음과 같습니다.
- 파라미터를 dparam 변동량 만큼 변화시킨 후 CTE를 측정합니다.
- CTE가 현재까지 베스트 에러보다 작다면, 더 좋은 파라미터이므로 dparam의 값을 1.1배 곱해서 약간만 더 키워봅니다.
- 베스트 에러보다 크다면, 별로 좋지 못한 파라미터입니다. 따라서 2배를 키운 뒤 에러를 재측정합니다.
- 이번에 만약 베스트에러보다 작다면, 2배 키운게 잘한것이지요! 따라서 이번에는 1.1배만 곱해서 약간만 더 키워봅니다.
- 먄약 베스트에러보다 작다면, 2배 키우지 말았어야 했습니다. 따라서 0.9배로 축소한 값을 곱해서 약간 더 줄여봅니다.
위 과정을 P, I, D 게인 파라미터 각각에 모두 적용하게 됩니다.
마무리
이렇게 PID 제어에 대해 알아봤습니다. 코드를 보시면 아시겠지만 제어 자체는 크게 어렵지 않습니다. 실제 어플리케이션에 맞게 파라미터를 튜닝하는 과정이 더 어려워보입니다. 이만 글 마치겠습니다. 감사합니다.
'Artificial Intelligence' 카테고리의 다른 글
탐색 알고리즘 - BFS, Uniform Cost Search, A Star Search (0) | 2022.02.16 |
---|---|
탐색 알고리즘 - 깊이우선탐색, 너비우선탐색 기본개념 (0) | 2022.02.15 |
[Localization] 칼만 필터 Kalman Filter (0) | 2022.02.14 |
[Localization] Particle Filter 파티클 필터 (0) | 2022.02.14 |