> > > [simanneal / scipy_dual_annealing] Python으로 Simulated Annealing 시작하기: SA 패키지 사용법과 하이퍼파라미터 완전 정리
본문 바로가기
기억하고 싶은 지식/python

[simanneal / scipy_dual_annealing] Python으로 Simulated Annealing 시작하기: SA 패키지 사용법과 하이퍼파라미터 완전 정리

by Remember-me 2026. 4. 7.
반응형

안녕하세요.
“기억하고자 하는 모든 것”을 담아내는 리멤버미입니다.

 

지난 글에서 SA(Simulated Annealing) 알고리즘 자체의 개념을 정리했다면, 이번에는 조금 더 실무적인 관점에서 Python에서 SA를 어떻게 써야 하는지, 그리고 하이퍼파라미터는 어떤 의미를 가지는지 정리해보겠습니다.

Python에서 SA를 다룰 때 많이 언급되는 축은 크게 두 가지입니다.
하나는 조합 최적화나 이산 상태 탐색에 직관적인 simanneal이고, 다른 하나는 SciPy가 제공하는 연속 변수용 전역 최적화 함수 scipy.optimize.dual_annealing입니다. simanneal은 PyPI 기준으로 Simulated Annealing 전용 패키지이며, 최신 공개 릴리스는 0.5.0입니다. 반면 dual_annealing은 현재 SciPy 공식 문서에 포함된 전역 최적화 함수입니다

 

어떤 패키지를 써야 할까?

먼저 결론부터 말하면 이렇습니다.

순서, 조합, 배치, 라우팅처럼 “상태를 직접 바꿔가며” 푸는 문제라면 simanneal 쪽이 이해하기 쉽습니다. 이 패키지는 Annealer를 상속한 뒤 move()와 energy()를 구현하는 구조라서, SA의 원리를 눈으로 따라가기에 좋습니다.

반대로 연속 변수 범위 안에서 함수값을 최소화하는 문제라면 scipy.optimize.dual_annealing이 더 실전적입니다. SciPy 문서에 따르면 이 함수는 Generalized SA 계열 탐색과 local search 전략을 결합한 방식입니다.

즉, 아주 단순하게 나누면

  • 조합 최적화 입문 / SA 원리 이해 → simanneal
  • 연속 변수 전역 최적화 실전 적용 → dual_annealing
    이렇게 생각하면 편합니다. 이 구분은 패키지의 API 구조와 문서 설명을 보면 꽤 자연스럽습니다.

 

1. simanneal 패키지 사용법

설치

simanneal은 PyPI에서 바로 설치할 수 있습니다. README 예시에도 pip install simanneal이 안내되어 있습니다.

 

pip install simanneal

기본 구조

simanneal은 Annealer 클래스를 상속받아 문제를 정의합니다.
핵심은 딱 두 가지입니다.

  • move() : 현재 상태를 조금 바꾸는 함수
  • energy() : 현재 상태의 비용을 계산하는 함수

공식 README의 Quickstart도 바로 이 구조로 설명합니다

최소 예제

아래 예시는 “도시 방문 순서”를 바꿔가며 총 이동 거리를 줄이는 아주 전형적인 TSP 형태입니다.

 

import math
import random
from simanneal import Annealer

cities = {
    "A": (0, 0),
    "B": (2, 3),
    "C": (5, 4),
    "D": (6, 1),
    "E": (1, 5),
}

class TSPAnnealer(Annealer):
    def distance(self, c1, c2):
        x1, y1 = cities[c1]
        x2, y2 = cities[c2]
        return math.hypot(x2 - x1, y2 - y1)

    def move(self):
        a = random.randint(0, len(self.state) - 1)
        b = random.randint(0, len(self.state) - 1)
        self.state[a], self.state[b] = self.state[b], self.state[a]

    def energy(self):
        e = 0
        for i in range(len(self.state)):
            e += self.distance(self.state[i - 1], self.state[i])
        return e

initial_state = list(cities.keys())
random.shuffle(initial_state)

annealer = TSPAnnealer(initial_state)

annealer.Tmax = 25000.0
annealer.Tmin = 2.5
annealer.steps = 50000
annealer.updates = 100

best_state, best_energy = annealer.anneal()

print("best route:", best_state)
print("best distance:", best_energy)

 

 

이 예제가 중요한 이유는 simanneal의 사고방식을 그대로 보여주기 때문입니다.
상태는 self.state에 있고, move()는 이 상태를 흔들고, energy()는 그 결과가 얼마나 좋은지 평가합니다. README에서도 동일한 구조의 TSP 예제를 제공합니다.


2. simanneal 하이퍼파라미터 설명

simanneal README에는 기본 파라미터로 Tmax=25000.0, Tmin=2.5, steps=50000, updates=100가 제시되어 있습니다. 또한 Tmax는 대략 98% 정도의 move를 수용할 수준으로 시작하고, Tmin은 해가 거의 더 좋아지지 않을 정도로 낮게 잡는 것을 권장합니다.

1) Tmax

시작 온도입니다.
온도가 높을수록 초반에 더 많은 나쁜 해를 받아들이게 되어 탐색 범위가 넓어집니다. 너무 낮게 시작하면 SA의 장점인 local minimum 탈출 능력이 약해집니다. README는 시작 온도를 “대부분의 move를 받아들일 정도”로 잡는 것을 권장합니다.

실무 감각으로 말하면:

  • local minimum에 너무 빨리 갇힌다 → Tmax를 올려보기
  • 초반 탐색이 너무 산만하다 → Tmax를 조금 낮춰보기

2) Tmin

종료 온도입니다.
이 값이 충분히 낮아야 후반부에 탐색이 안정화됩니다. README 표현대로는, 더 이상 해가 거의 개선되지 않을 정도까지 내려가는 것이 좋습니다.

실무적으로는:

  • 결과가 아직 흔들리는데 끝난다 → Tmin을 더 낮추기
  • 이미 충분히 수렴하는데 너무 오래 돈다 → Tmin을 조금 올려도 됩니다

3) steps

총 반복 횟수입니다.
탐색 예산이라고 보면 됩니다. simanneal 문서는 steps가 부족하면 탐색 공간을 충분히 보지 못해 local minimum에 갇힐 수 있다고 설명합니다.

보통은:

  • 문제 크기가 크다
  • neighbor 변화가 작다
  • 결과 편차가 크다
    이럴수록 steps를 늘리는 편이 안전합니다.

4) updates

중간 진행 상황을 몇 번 출력할지 정하는 값입니다.
중요한 점은 이 값이 결과 자체를 바꾸지는 않는다는 것입니다. 기본 update는 현재 온도, 에너지, 수용률, 개선률 등을 출력하는 용도입니다.

즉:

  • 디버깅/관찰 목적 → updates 유지
  • 출력이 너무 많다 → 줄이기
  • 성능 개선용 파라미터는 아님

3. simanneal에서 알아두면 좋은 기능

auto()

simanneal은 .auto(minutes=1) 같은 방식으로 자동 스케줄을 추정할 수 있습니다. README에 따르면 이 기능은 탐색 공간을 살펴보며 적당한 tmin, tmax, steps를 추정해줍니다.

auto_schedule = annealer.auto(minutes=1)
annealer.set_schedule(auto_schedule)
best_state, best_energy = annealer.anneal()

 

처음 SA를 붙일 때는 이 방법이 꽤 편합니다.
처음부터 수작업으로 모든 파라미터를 잡기보다, 자동 스케줄을 기준점으로 삼고 그다음 미세 조정하는 흐름이 훨씬 낫습니다. 이 부분은 README 기능 설명을 바탕으로 한 실전적인 해석입니다.

move()에서 delta 반환

문서에는 move()가 None이 아닌 값을 반환하면, 이를 에너지 변화량(delta)로 간주해 이전 에너지에 더하는 방식으로 처리할 수 있다고 나와 있습니다. 에너지 전체 재계산이 비싼 문제에서는 꽤 중요합니다.

즉, 배치나 순서 변경처럼 변화량만 빠르게 계산 가능한 문제에서는 이 기능이 성능 체감이 큽니다.
에너지 함수를 매번 전체 계산하는 대신, 바뀐 부분만 반영하면 되기 때문입니다. 이건 문서 내용을 실무 관점으로 해석한 것입니다.

copy_strategy

simanneal은 상태를 자주 복사해야 하므로 복사 방식도 지정할 수 있습니다. 문서에는 deepcopy, slice, method 세 가지가 소개되어 있습니다.

  • 리스트 기반 상태 → slice
  • dict/객체 → 상황에 따라 method 또는 사용자 정의
  • 무거운 구조를 무조건 deepcopy → 느려질 가능성 큼

즉, 상태 표현 방식 자체가 성능에 영향을 줍니다.
SA가 느린 것 같다면 하이퍼파라미터만 볼 게 아니라 상태 복사 비용도 같이 봐야 합니다. 이 역시 문서의 copy 전략 설명에서 바로 이어지는 실무 포인트입니다.

 

4. scipy.optimize.dual_annealing 사용법

이제 연속 변수 최적화 쪽으로 넘어가겠습니다.

SciPy 공식 문서에서 dual_annealing의 시그니처는 다음과 같습니다.

dual_annealing(func, bounds, args=(), maxiter=1000, minimizer_kwargs=None, initial_temp=5230.0, restart_temp_ratio=2e-05, visit=2.62, accept=-5.0, maxfun=10000000.0, rng=None, no_local_search=False, callback=None, x0=None, ...)

 

설치

SciPy가 설치되어 있으면 바로 사용할 수 있습니다.

pip install scipy

최소 예제

아래는 전역 최적화 예제로 자주 쓰이는 Rastrigin 함수입니다.

import numpy as np
from scipy.optimize import dual_annealing

def rastrigin(x):
    x = np.asarray(x)
    A = 10
    return A * len(x) + np.sum(x**2 - A * np.cos(2 * np.pi * x))

bounds = [(-5.12, 5.12), (-5.12, 5.12)]

result = dual_annealing(
    rastrigin,
    bounds=bounds,
    maxiter=300,
    initial_temp=5230.0,
    restart_temp_ratio=2e-5,
    visit=2.62,
    accept=-5.0,
    no_local_search=False,
)

print("best x:", result.x)
print("best f(x):", result.fun)
print("message:", result.message)

 

SciPy 문서 기준으로 bounds는 필수이며, 각 변수의 (min, max) 범위를 넣어주면 됩니다. 반환값은 OptimizeResult이고, 대표적으로 x, fun, message를 확인하면 됩니다.


5. dual_annealing 하이퍼파라미터 설명

1) bounds

가장 먼저 봐야 하는 값입니다.
문서상 bounds는 Bounds 객체 또는 (min, max) 쌍의 시퀀스로 지정합니다.

이 함수는 기본적으로 bounded continuous optimization에 맞는 느낌이 강합니다.
즉, 파라미터마다 허용 범위가 명확할수록 쓰기 좋습니다. 이것은 API 설계 자체에서 드러납니다.

2) maxiter

전역 탐색 반복 횟수입니다. 기본값은 1000입니다.

maxiter가 작으면 빨리 끝나지만 전역 탐색이 부족할 수 있습니다.
연속 변수 공간이 넓거나 다봉형 함수면 이 값을 늘려야 할 때가 많습니다. 이 부분은 파라미터 의미에 대한 자연스러운 해석입니다.

3) initial_temp

초기 온도이며 기본값은 5230.0, 범위는 (0.01, 5.e4]입니다. 문서에는 값이 높을수록 더 넓게 탐색하며 local minima에서 빠져나오기 쉬워진다고 설명합니다.

실무 팁은 단순합니다.

  • 초반 탐색이 너무 좁다 → initial_temp 증가
  • 탐색이 너무 퍼지고 수렴이 늦다 → initial_temp 감소

4) restart_temp_ratio

온도가 initial_temp * restart_temp_ratio에 도달하면 reannealing이 트리거됩니다. 기본값은 2e-5입니다.

이 값은 “식혀 가다가 어느 지점에서 다시 탐색 감각을 리셋할 것인가”에 가깝습니다.
문서 그대로 보면 reannealing의 임계 비율을 정하는 값이고, 실무적으로는 너무 일찍/자주 재가열되는지 여부를 조절하는 손잡이로 이해하면 됩니다.

5) visit

기본값은 2.62, 범위는 (1, 3]입니다. SciPy 설명에 따르면 값이 클수록 visiting distribution의 tail이 무거워져 더 먼 영역으로 점프하기 쉬워집니다.

쉽게 말하면:

  • 값을 키우면 멀리 뛰는 탐색
  • 값을 낮추면 근처를 더 촘촘히 보는 탐색

다봉형 함수에서 지역해가 많으면 visit을 조금 높여 전역 점프 성향을 키우는 전략을 생각해볼 수 있습니다. 이는 공식 설명을 실무적으로 해석한 것입니다.

6) accept

기본값은 -5.0, 범위는 (-1e4, -5]입니다. 문서에는 accept 값이 더 낮을수록 수용 확률이 더 작아진다고 설명되어 있습니다.

즉:

  • 더 보수적으로 가고 싶다 → accept를 더 낮게
  • 나쁜 해도 조금 더 열어두고 싶다 → 기본값 근처 유지

이 파라미터는 직관적으로는 “나쁜 해를 얼마나 허용할지”와 연결됩니다. 공식 문서의 acceptance distribution 설명을 실전 감각으로 옮기면 그렇습니다.

7) maxfun

목적 함수 호출 수의 soft limit이며 기본값은 1e7입니다. 로컬 탐색 중이면 이 숫자를 조금 넘어서고 나서 종료될 수 있다고 문서에 적혀 있습니다.

목적 함수가 아주 비싼 경우에는 maxiter만 볼 게 아니라 maxfun도 같이 관리하는 편이 좋습니다.
특히 시뮬레이터를 물고 도는 black-box 최적화에서는 함수 평가 횟수가 곧 시간과 비용이기 때문입니다. 이건 문서 설명에서 바로 이어지는 실무 해석입니다.

8) no_local_search

문서에 따르면 True로 두면 local search 없이 보다 전통적인 generalized SA에 가깝게 동작합니다. 기본은 False입니다.

따라서:

  • SA 자체의 전역 탐색 성향을 더 보고 싶다 → True
  • 보통은 전역 탐색 후 local refinement까지 받고 싶다 → 기본값 False

9) minimizer_kwargs

로컬 탐색에 넘길 인자를 지정합니다. 문서에 따르면 지정하지 않으면 local minimizer는 기본적으로 L-BFGS-B를 사용하고 이미 제공된 bounds를 활용합니다. 다만 minimizer_kwargs를 직접 넣는 경우에는 필요한 제어 파라미터를 스스로 챙겨야 하며, bounds는 자동 전달되지 않는다고 명시되어 있습니다.

이 부분은 실무에서 자주 실수합니다.
즉, 커스텀 local minimizer를 넣을 때는 “global 단계의 bounds가 local 단계에도 자동으로 잘 들어가겠지”라고 생각하면 안 됩니다. 문서상 그 보장은 없습니다.


6. 실전에서 어떻게 튜닝하면 좋을까?

simanneal 튜닝 순서

제 경험상, 그리고 README 구조를 따라가도 가장 무난한 순서는 이렇습니다.

  1. move()가 문제 구조를 잘 반영하는지 먼저 본다
  2. energy() 계산이 너무 비싸면 delta 반환을 고민한다
  3. .auto()로 초기 스케줄을 잡아본다
  4. steps를 늘려 결과 안정성을 먼저 확보한다
  5. 그다음 Tmax, Tmin을 미세 조정한다

즉, simanneal에서는 하이퍼파라미터보다도 상태 설계와 neighbor 설계가 먼저입니다.
이건 패키지 구조가 move()와 energy() 중심이라는 점에서도 드러납니다.

dual_annealing 튜닝 순서

연속 함수 최적화에서는 보통 이렇게 접근하는 편이 깔끔합니다.

  1. bounds를 현실적으로 좁힌다
  2. 기본값으로 먼저 실행한다
  3. local minimum에 자주 갇히면 initial_temp, visit, maxiter를 조정한다
  4. 함수 평가가 비싸면 maxfun을 반드시 관리한다
  5. local refinement가 과하거나 불필요하면 no_local_search=True도 시험해본다

핵심은 간단합니다.
**탐색 범위는 bounds, 탐색 강도는 initial_temp와 visit, 계산 예산은 maxiter와 maxfun, 후처리 정교화는 no_local_search와 minimizer_kwargs**가 맡는다고 보면 됩니다. 이건 공식 파라미터 의미를 한 줄로 묶은 해석입니다.


7. 정리

이번 글을 한 줄로 정리하면 이렇습니다.

Python에서 SA를 쓸 때는, 문제 타입에 따라 패키지 선택부터 달라져야 합니다.

simanneal은 SA의 원리를 손으로 만지듯 이해하기 좋은 구조입니다. Annealer를 상속해 move()와 energy()를 직접 정의하기 때문에 조합 최적화, 순서 최적화, 배치 문제에 특히 잘 어울립니다. 그리고 핵심 하이퍼파라미터는 Tmax, Tmin, steps입니다.

반면 scipy.optimize.dual_annealing은 연속 변수의 bounded global optimization에 더 어울립니다. 여기서는 bounds, initial_temp, visit, accept, maxiter를 중심으로 보는 게 좋고, local search를 함께 쓰는 구조라는 점도 기억해둘 만합니다.

결국 SA는 “패키지 하나 외워서 쓰는 알고리즘”이 아니라,
문제의 상태 표현, 이웃 생성 방식, 온도 스케줄, 계산 예산을 어떻게 설계하느냐가 성능을 좌우하는 최적화 프레임워크에 가깝습니다.

그래서 처음 시작할 때는

  • 원리 이해는 simanneal
  • 연속 함수 실전 최적화는 dual_annealing
    이렇게 접근하면 훨씬 덜 헷갈립니다.

 

2026.04.07 - [기억하고 싶은 지식/인공지능] - [인공지능/최적화/AI] 휴리스틱 알고리즘 SA(Simulated Annealing)란 무엇인가? 국소해를 넘어서 더 좋은 해를 찾는 방법

 

[인공지능/최적화/AI] 휴리스틱 알고리즘 SA(Simulated Annealing)란 무엇인가? 국소해를 넘어서 더 좋은

안녕하세요.“기억하고자 하는 모든 것”을 담아내는 “리멤버미” 입니다. 최적화 문제를 풀다 보면 자주 부딪히는 벽이 있습니다.바로 지금 당장은 좋아 보이지만 전체적으로는 최선이 아닌

diary.remembermeeternally.com

 

반응형

댓글