지난 포스팅에서 생성한 시뮬레이션 담금질 프로그램을 활용하여, 대표적인 조합 최적화combinatorial optimization 문제인 외판원 문제travelling salesman problem을 풀어 보도록 하겠습니다. 시뮬레이션 담금질에 대해서 설명한 지난 포스팅을 아직 읽지 않으신 분이라면, 이전 포스팅을 먼저 읽고 이 포스팅을 읽으시길 바랍니다.
https://studyingrabbit.tistory.com/100
[수학적 최적화] 시뮬레이션 담금질 Simulated Annealing의 이해와 파이썬 코드
수학적 최적화Mathematical optimization 수학적 최적화는 최적 조건을 만족하는 정의역의 원소를 찾는 방법 (혹은 그에 대한 분야) 입니다. 이렇게만 설명하면 약간 불 명확한데 아래와 같이 정의하면,
studyingrabbit.tistory.com
외판원 문제
외판원 문제에 대해서는 한 번 쯤은 들어았을 것이라 생각합니다. 외판원 문제는
지도 상에 여러 도시들이 있고, 한 도시에서 다른 도시 사이의 거리가 주어져 있을 때, 모든 도시들을 단 한 번만 방문하고 여행을 시작 했던 도시로 돌아올 때, 이동 거리를 최소로 하는 도시 방문 순서를 찾는 문제
입니다. 물론 문제의 성격에 따라서 약간의 차이가 있을 수 있습니다.
외판원 문제는 일상생활에서도 겪는 문제입니다. 특히 택배 기사분들은 매일 겪는 문제인데요, 배달의 목적지가 여러곳인 경우, 어떤 순서로 배송을 해야 가장 빠르게 물건을 배송할 수 있을지 고민하실 것 입니다. (약간은 다른 문제이긴 하지만) 여러 마을을 지나는 버스나 셔틀의 경로를 결정할 때, 외판원 문제를 풀게 됩니다.
문제의 정확한 정의
지도상에
로 주어집니다. 위에서 사용한 예시를 사용하면,
이 됩니다.
외판원 문제는 매우 풀기 어려운 문제다
외판원 문제는 계산 복잡성 이론에 따르면 NP-hard에 속하는 문제 입니다. 이 문제를 풀기 위해 다양한 방법이 시도되고 있습니다. 우리는 이번 포스팅에서 이 문제를 이전 포스팅에서 공부한 바 있는 시뮬레이션 담금질을 통해서 풀어 보고자 합니다. 시뮬레이션 담금질을 통해서 얻어진 해가 실제 최적의 해라는 것을 보장할 수는 없지만 현실적으로 볼 때 "꽤 괜찮은 해" 정도는 됩니다.
시뮬레이션 담금질 방법을 통한 외판원 문제 풀기
1. 목적 함수의 정의역
지난 포스팅에서 개발한 시뮬레이션 담금질 코드를 약간만 수정하면, 외판원 문제에 적용할 수 있습니다. 지난 포스팅에서 다룬 문제와 이번 포스팅에서 다루는 외판원 문제의 차이는 목적 함수의 정의역이 다르다는 것 입니다. 그 밖에 차이는 없습니다. 물론 주어진 목적 함수의 정의역의 한 점 (상태) 에 대응하는 목적 함수를 계산하는 방식이 조금 다르긴 하지만, 이는 큰 문제는 되지 않습니다. 그냥 목적 함수를 계산하는 부분의 코딩을 각각에 맞게 하면 됩니다.
목적 함수의 정의역은 2차원 유클리드 공간의 부분집합이었습니다. 즉
와 같이 한 상태
조금 더 명확하게 수학적으로 표시하자면,
가 됩니다.
2. 목적 함수
외판원 문제의 목적 함수는 주어진 경로에 대응하는 거리를 구하는 것 입니다. 임이의 두 도시 쌍
3. 문제 세팅
이번 포스팅에서 실제로 풀어볼 문제에 대해 설명을 하도록 하겠습니다.

4. 풀어 보기
위 문제에 대한 파이썬 코드 입니다.
import numpy as np
from matplotlib import pyplot as plt
from matplotlib import animation
import random
plt.style.use('dark_background')
def objective_tsp(x, P):
dist = 0.0
for index in range(len(x)):
dx = P[x[index]] - P[x[(index+1) % len(P)]]
d = np.linalg.norm(dx)
dist += d
return dist
def transition_probability(e, e_new, T):
de = e_new - e
p = np.exp(-de/T)
return p
def transition_or_not(p):
r = np.random.random()
if p > r:
return True
else:
return False
def update_tsp(num, list_line, list_E, list_T, line, time_text):
line.set_data(list_line[num][:,0], list_line[num][:,1])
time_text.set_text("Iteration = {}\nObjective = {:.4f}\nTemperature = {:.4f}".format(num, list_E[num], list_T[num]))
def tsp():
Pt = 30
list_theta = np.linspace(0, 2*np.pi, Pt)[:Pt-1]
P = [[0.5+0.4*np.cos(theta), 0.5+0.4*np.sin(theta)] for theta in list_theta]
P = np.array(P)
E_opt = objective_tsp(range(Pt-1), P)
P = P[:]
T = 10
Niteration = 10000
T_reduce = 10 ** (-3 / Niteration)
N_skip = Niteration//400
X = list(range(len(P)))
np.random.shuffle(X)
list_X, list_E, list_T = [], [], []
for iteration in range(Niteration):
X_new = list(X)
a, b = random.sample(range(len(X)), 2)
X_new[a], X_new[b] = X_new[b], X_new[a]
e = objective_tsp(X, P)
e_new = objective_tsp(X_new, P)
p = transition_probability(e, e_new, T)
bool_transition = transition_or_not(p)
if bool_transition:
X = X_new
T *= T_reduce
list_X.append(X)
list_E.append(e)
list_T.append(T)
print(iteration, e, T, E_opt)
list_X = np.array(list_X)
list_line = []
for X in list_X:
line = []
for index in range(len(X)):
p = [P[X[index]][0], P[X[index]][1]]
line.append(p)
line.append([P[X[0]][0], P[X[0]][1]])
list_line.append(line)
list_line = np.array(list_line)
fig = plt.figure(figsize=(8, 8))
ax = plt.axes()
point = ax.scatter(P[:,0], P[:,1], s=50, color='lime', zorder=1)
line, = ax.plot([], [], color='lime', lw=2)
time_text = ax.text(0.65, 0.05, '', transform=ax.transAxes, fontsize='large')
plt.xlabel('X')
plt.ylabel('Y')
ax.set_xlim(0, 1)
ax.set_ylim(0, 1)
list_line = list_line[::N_skip]
list_E = list_E[::N_skip]
list_T = list_T[::N_skip]
ani = animation.FuncAnimation(fig, update_tsp, len(list_E), fargs=(list_line, list_E, list_T, line, time_text),
interval=10, blit=False)
plt.show()
plt.close('all')
plt.plot(range(len(list_E)), list_E, color = 'yellow', label = 'SA')
plt.axhline(E_opt, color = 'lime', lw = 1, ls = '--', label = 'exact')
plt.xlabel("Iteration")
plt.ylabel("Objective")
plt.legend()
plt.grid(lw = 1, color = 'gray')
plt.show()
if __name__ == '__main__':
tsp()
코드에 대한 설명과, 지난 포스팅 대비 이번 포스팅에서 달라진 부분에 대해서는 앞에서 설명을 했기 때문에 코드 부분 부분에 대한 설명은 생략 하도록 하겠습니다.
위 코드를 실행하면, 아래와 같은 결과를 얻게 됩니다. 물론 프로그램에는 임의성(randomness)를 포함하고 있기 때문에, 정확히 똑같은 결과가 얻어지는 것은 아닙니다.
반복의 초기에는 녹색 실선들이 어지럽게 펼쳐져 있다가 끝 부분이 되면 가장 가까이에 있는 점을 잇는 선분 형태로 최적의 경로를 찾아 갑니다. 영상의 마지막에는 반복 시행에 따른 목적 함수 (해당 경로의 거리)가 나옵니다. 이 시뮬레이션은 총 10,000번의 반복을 시행한 것이며, 동영상에서는 시각화의 편의를 위해서 총 400개의 스냅샷만 표현 됩니다. 즉, 25 번의 반복 중에서 단 한 번의 이미지만 보여주는 것 입니다.
반복의 시행에 따라 찾아진 경로가 어떻게 최적의 경로로 옮아 가는지 확인해 보도록 하겠습니다.

시뮬레이션의 초반 입니다. 시작 경로는

반복 시행을 하면 할 수록 최적의 경로를 찾아 갑니다. 위 스냅샷에서는 대략 한 절반 조금 못 되는 정도로 최적의 경로(바로 옆에 있는 도시와 연결 됨)를 찾을 것 같고, 절반 정도는 최적의 경로를 찾지 못 했습니다. 길게 표시 되는 선분이 5개 정도 있는 것을 봐서는, 아직까지는 정답에 비슷한 경로를 찾지 못하는 경우도 좀 있습니다.

반복 시행을 조금씩 더 해가면 최적의 경로와 매우 유사한 경로를 찾게 됩니다. 위 경우에는 아직 5곳에서는 최적의 경로를 찾지 못 했습니다. 목적 함수의 변화량이 대략 1 정도의 수준이고, 온도 값이 대략 0.01 오더 이니, 이 정도 시점부터는 국소 최소점을 벗어나려는 시도 보다는 주어진 최소점 근처에서 보다 작은 목적 함수 값을 찾으려는 시도를 합니다.

동영상 상에서 약 300번째 반복, 전체 반복 횟수에서는 약 7,500번째 반복에서 최적 해를 찾게 됩니다. 이후에도 새로운 경로를 찾으려는 시도를 당연히 하지만, 목적 함수의 값이 증가하는 시도이기에 새로운 경로로 이동하지는 않습니다.

반복 시행에 따른 목적 함수의 변화를 보여주는 그래프 입니다. 반복 시행 초기에는 온도가 높기 때문에 이런 저런 상태를 막 오고 갑니다. 따라서 목적 함수의 값 역시 중구난방으로 튀게 됩니다. 그러다가 반복 시행이 늘어날 수록 목적 함수의 값은 극적으로 낮아집니다. 이 과정에서도 목적 함수가 증가하는 시행이 있긴 하지만, 그 횟수는 반복의 초기 보다는 작습니다.
제가 시행했을 때는 위와 같이 성공적으로 최적의 경로를 찾았습니다. 그러나 앞서 설명한대로 코드에는 무작위성이 있기 때문에 항상 최적 경로를 찾을 수 있는 것은 아닙니다. 이 경우에는 반복 시행의 횟수를 늘리거나, 온도를 좀 더 키우는 방식으로 최적 경로를 찾을 수 있게 할 수 있습니다.
시뮬레이션 담금질이 외판원 문제를 제대로 풀었다!
30개의 도시에 대한 외판원 문제의 경로는 총 대략
'수학' 카테고리의 다른 글
[수학적 최적화] 경사 하강법Gradient Descent(혹은 Steepest Descent) 알아 보기 : (1) 기본 개념 (1) | 2023.05.29 |
---|---|
타원 상의 한 점과 타원 밖의 한 점 사이의 거리 구하기 (1) | 2023.05.12 |
[수학적 최적화] 시뮬레이션 담금질 Simulated Annealing의 이해와 파이썬 코드 (2) | 2023.05.06 |
포물선의 반사의 성질 : 축에 나란한 방향으로 입사한 빛은 반드시 포물선의 초점을 지난다 (3) | 2023.04.06 |
중심 극한 정리 (CLT : Central Limit Theorem) : 여러개를 뽑아서 평균을 내면 정규 분포와 유사해 진다 (0) | 2022.05.16 |