안녕하세요.
“기억하고자 하는 모든 것”을 담아내는 “리멤버미” 입니다.
최적화 문제를 풀다 보면, 미분이 어렵거나 목적함수가 울퉁불퉁해서 전통적인 방법으로는 잘 안 풀리는 경우가 있습니다. 이럴 때 자주 언급되는 전역 최적화 기법 중 하나가 바로 DE(Differential Evolution) 입니다. SciPy 문서에서도 differential evolution은 다변수 함수의 global minimum을 찾기 위한 확률적(stochastic) 전역 최적화 방법으로 소개하고 있으며, gradient를 직접 사용하지 않고 넓은 후보 공간을 탐색할 수 있다고 설명합니다.
이번 글에서는 DE 알고리즘의 이론을 길게 파기보다, Python에서 어떤 패키지로 시작하면 좋은지, 그리고 주요 하이퍼파라미터를 어떻게 이해하면 좋은지에 초점을 맞춰 가볍게 정리해보겠습니다.
[인공지능/최적화/AI] 휴리스틱 알고리즘 DE(Differential Evolution)란? 벡터 차이로 해를 진화시키는 전
안녕하세요.“기억하고자 하는 모든 것”을 담아내는 “리멤버미” 입니다. 최적화 문제를 풀다 보면, 해 공간이 너무 넓어서 어디부터 찾아야 할지 막막한 경우가 많습니다.특히 목적함수가
diary.remembermeeternally.com
어떤 Python 패키지를 쓰면 좋을까?
DE를 Python에서 써보려면 가장 먼저 많이 접하는 선택지는 SciPy의 scipy.optimize.differential_evolution 입니다. SciPy는 과학기술 계산용 대표 패키지이고, PyPI 기준 최신 공개 버전은 2026년 2월 릴리스된 1.17.1입니다. 이 함수는 bounds 기반의 연속 변수 전역 최적화에 바로 적용할 수 있어서, “일단 돌려보고 싶은” 경우에 가장 접근성이 좋습니다.
조금 더 알고리즘 구조를 만져보고 싶다면 pymoo의 DE 클래스도 좋은 선택입니다. pymoo 문서는 DE를 단일 목적(single-objective) 알고리즘으로 제공하고 있고, variant, CR, dither, sampling 같은 설정을 비교적 명시적으로 다룰 수 있게 구성되어 있습니다. PyPI 기준 pymoo 최신 공개 버전은 0.6.1.6입니다.
간단히 정리하면 아래처럼 볼 수 있습니다.
- 빠르게 시작하고 싶다 → SciPy
- DE 변형과 탐색 방식을 더 세밀하게 만지고 싶다 → pymoo
1. SciPy로 DE 가장 쉽게 시작하기
설치
pip install scipy
SciPy의 differential_evolution 함수는 아래처럼 사용할 수 있습니다. 공식 시그니처에는 strategy, maxiter, popsize, tol, mutation, recombination, rng, polish, init, workers, constraints, integrality, vectorized 같은 옵션이 포함되어 있습니다.
간단한 예제
아래 예제는 2차원 함수의 최솟값을 찾는 아주 단순한 예제입니다.
import numpy as np
from scipy.optimize import differential_evolution
def objective(x):
x1, x2 = x
return (x1 - 3.0)**2 + (x2 + 1.0)**2
bounds = [(-10, 10), (-10, 10)]
result = differential_evolution(
objective,
bounds=bounds,
strategy="best1bin",
maxiter=200,
popsize=15,
mutation=(0.5, 1.0),
recombination=0.7,
rng=42,
polish=True
)
print("best x:", result.x)
print("best f:", result.fun)
SciPy에서 DE를 돌리는 흐름은 꽤 단순합니다.
- 목적함수 objective()를 만든다
- 변수 범위 bounds를 정한다
- differential_evolution()에 하이퍼파라미터를 넣고 실행한다
- result.x, result.fun으로 최적 해와 목적함수 값을 확인한다
공식 문서 기준으로 bounds는 (min, max) 쌍의 리스트로 줄 수도 있고, Bounds 객체로 줄 수도 있습니다. 반환값은 OptimizeResult 형태이며, x, fun, success, message, population, population_energies 같은 정보를 담고 있습니다.
2. DE에서 가장 중요한 하이퍼파라미터는 무엇일까?
DE는 구조가 비교적 단순한 편이지만, 실제 성능은 하이퍼파라미터의 영향을 꽤 많이 받습니다. 특히 아래 항목들은 이름만 알아두는 수준이 아니라, 탐색 반경, 다양성, 수렴 속도와 연결해서 이해하는 것이 중요합니다.
strategy
strategy는 trial vector를 어떤 방식으로 만들지 정하는 핵심 옵션입니다. SciPy 기본값은 best1bin이며, 공식 문서에는 best1bin, rand1bin, rand2bin, best2bin, currenttobest1bin 등 여러 전략이 제공됩니다. SciPy 문서도 best1bin을 많은 시스템에서 좋은 시작점이라고 설명합니다.
감각적으로 보면 이렇습니다.
- best... 계열: 현재 좋은 해를 중심으로 더 빨리 수렴하기 쉬움
- rand... 계열: 탐색 다양성을 유지하기 좋음
- ...bin, ...exp: crossover 방식 차이
처음에는 best1bin 또는 rand1bin 정도로 시작해도 충분합니다.
popsize
popsize는 개체군 크기를 정하는 값입니다. SciPy에서는 실제 population 크기 = popsize * (N - N_equal) 로 계산됩니다. 여기서 N은 변수 수이고, 동일한 bound를 가진 고정 변수는 제외됩니다. popsize가 크면 탐색 다양성은 좋아지지만 함수 평가 횟수가 늘어나 계산량도 커집니다. SciPy 문서에는 최대 함수 평가 횟수도 대략 (maxiter + 1) * popsize * (N - N_equal) 형태로 설명되어 있습니다.
실무적으로는 보통 이렇게 생각하면 편합니다.
- 너무 작다 → 다양성이 부족해서 조기 수렴 가능성 증가
- 너무 크다 → 계산 시간이 급격히 증가
mutation = F
mutation은 문헌에서 보통 differential weight FF 로 부르는 값입니다. SciPy 문서에서도 mutation constant가 곧 FF 라고 설명합니다. 값이 커질수록 탐색 반경은 넓어지지만, 수렴은 느려질 수 있습니다. 또한 (min, max) 튜플로 주면 generation마다 무작위로 F를 바꾸는 dithering을 사용할 수 있습니다. SciPy는 이 dithering이 수렴 속도 향상에 도움이 될 수 있다고 설명합니다.
쉽게 말하면:
- F가 너무 작다 → 탐색 폭이 좁아짐
- F가 너무 크다 → 해가 자꾸 크게 흔들릴 수 있음
처음에는 0.5 ~ 1.0 구간부터 시작하는 경우가 많습니다.
recombination = CR
recombination은 문헌에서 crossover probability, 즉 CR 로 많이 표기됩니다. SciPy 문서에 따르면 범위는 [0, 1]이고, 값이 커질수록 mutant 벡터의 정보가 더 많이 다음 세대로 넘어갑니다. 대신 population 안정성이 떨어질 수 있습니다.
직관적으로 보면:
- CR이 낮다 → 기존 해를 많이 유지
- CR이 높다 → 새로운 조합이 더 적극적으로 반영
maxiter
maxiter는 세대 수라고 보면 됩니다. 너무 작으면 충분히 탐색하지 못하고, 너무 크면 이미 좋아질 만큼 좋아진 뒤에도 오래 돌 수 있습니다. SciPy는 이 값이 population 전체를 몇 세대까지 진화시킬지 정한다고 설명합니다.
tol / atol
SciPy의 tol, atol은 수렴 판단 기준입니다. 공식 문서에서는 population energy의 표준편차가 일정 기준 이하로 내려오면 종료하도록 설명합니다. 즉, 개체들의 목적함수 값이 서로 거의 비슷해졌다면 “이제 충분히 수렴했다”고 판단하는 셈입니다.
실무적으로는 이렇게 받아들이면 됩니다.
- tol이 너무 크다 → 일찍 멈출 수 있음
- tol이 너무 작다 → 끝까지 오래 돌 수 있음
init
초기 population을 어떻게 뿌릴지 정하는 옵션입니다. SciPy는 latinhypercube, sobol, halton, random, 그리고 직접 넣는 배열 초기화를 지원합니다. 문서에서는 기본값인 latinhypercube가 공간 커버리지를 높이려는 방법이라고 설명하고, sobol과 halton은 더 좋은 공간 분포 대안이라고 안내합니다. 반면 random은 군집이 생겨 탐색 공간을 고르게 덮지 못할 수 있습니다.
처음에는 보통 아래처럼 시작하면 무난합니다.
- 기본값 유지 → latinhypercube
- 좀 더 균일한 초기 분포를 원한다 → sobol
polish
polish=True이면 DE가 끝난 뒤, SciPy가 추가로 local minimizer를 사용해 최종 해를 조금 더 다듬습니다. 기본적으로는 minimize의 L-BFGS-B가 사용되며, 제약이 있으면 trust-constr가 사용됩니다. 전역 탐색은 DE로 하고, 마지막 미세 조정은 local optimizer로 하는 느낌입니다.
이 옵션은 꽤 실용적이지만, 제약이 많고 문제 규모가 큰 경우에는 polishing이 오래 걸릴 수 있습니다. 그래서 최종 미세 개선이 필요 없는 실험 단계에서는 꺼두는 것도 방법입니다.
workers / vectorized / updating
SciPy의 장점 중 하나는 계산 가속 옵션이 꽤 잘 들어가 있다는 점입니다. workers를 주면 병렬 평가가 가능하고, vectorized=True를 쓰면 목적함수를 population 단위로 한 번에 평가할 수 있습니다. 다만 공식 문서 기준으로 병렬화나 vectorization을 쓰면 updating='deferred'와 연결되며, workers != 1이면 updating과 vectorized 동작에도 영향이 있습니다. 또한 병렬화는 계산 비용이 큰 objective에 특히 유리하다고 설명합니다.
즉, 목적함수 하나 평가하는 데 시간이 오래 걸린다면:
- workers=-1 → CPU 코어를 활용한 병렬화
- vectorized=True → 한 번에 묶어서 계산
같은 방식이 꽤 유용할 수 있습니다.
constraints / integrality
최신 SciPy의 differential_evolution은 bounds 외에도 constraints를 지원하고, integrality를 통해 정수 변수도 지정할 수 있습니다. 즉, “단순 연속 변수 장난감 예제” 수준을 넘어서 제약식이 있는 문제나 정수/연속 혼합 변수 문제에도 어느 정도 대응할 수 있습니다.
이 부분은 실무 최적화에서 꽤 중요합니다.
예를 들어 설계 변수 중 일부는 정수 step만 허용되고, 어떤 값들은 선형/비선형 제약을 만족해야 하는 경우가 많기 때문입니다.
3. SciPy에서 처음 시작할 때 추천하는 설정
처음부터 파라미터를 너무 복잡하게 잡기보다는, 아래 정도로 시작하는 편이 좋습니다.
result = differential_evolution(
objective,
bounds=bounds,
strategy="best1bin",
popsize=15,
maxiter=200,
mutation=(0.5, 1.0),
recombination=0.7,
init="latinhypercube",
rng=42,
polish=True
)
이 조합은 “무난한 시작점”에 가깝습니다. SciPy 문서도 best1bin을 좋은 starting point로 설명하고 있고, mutation dithering과 population 크기 조절이 전역 최소 탐색 가능성에 영향을 준다고 안내합니다. 또한 최신 코드에서는 재현성을 위해 seed보다 rng 사용이 권장됩니다. seed는 과거 호환성 때문에 남아 있지만, SciPy는 새 코드에서는 rng 사용을 권합니다.
4. pymoo로 DE를 조금 더 명시적으로 다뤄보기
SciPy가 “바로 돌려보는 DE”에 가깝다면, pymoo는 “알고리즘 구성을 좀 더 의식적으로 다루는 DE”에 가깝습니다. pymoo 문서 예제에서도 DE 클래스를 만들 때 pop_size, sampling=LHS(), variant="DE/rand/1/bin", CR, dither, jitter 등을 직접 넣는 형태를 보여줍니다.
설치
pip install pymoo
간단한 예제
from pymoo.algorithms.soo.nonconvex.de import DE
from pymoo.core.problem import ElementwiseProblem
from pymoo.optimize import minimize
from pymoo.operators.sampling.lhs import LHS
import numpy as np
class MyProblem(ElementwiseProblem):
def __init__(self):
super().__init__(n_var=2, n_obj=1, xl=np.array([-10, -10]), xu=np.array([10, 10]))
def _evaluate(self, x, out, *args, **kwargs):
out["F"] = (x[0] - 3.0)**2 + (x[1] + 1.0)**2
problem = MyProblem()
algorithm = DE(
pop_size=40,
sampling=LHS(),
variant="DE/rand/1/bin",
CR=0.9,
dither="vector",
jitter=False
)
res = minimize(
problem,
algorithm,
seed=42,
verbose=False
)
print("best x:", res.X)
print("best f:", res.F)
pymoo 문서에는 고전적인 시작 설정으로 DE/rand/1/bin, population size를 변수 수의 10배 정도, F=0.8, CR=0.9 같은 가이드를 소개하고 있습니다. 또한 generation별 또는 difference vector별로 F를 랜덤하게 바꾸는 dither가 noisy objective에서 convergence 개선에 도움이 될 수 있다고 설명합니다.
5. pymoo에서 자주 보는 하이퍼파라미터
variant
pymoo의 variant는 사실상 DE의 변형 전략을 문자열로 표현한 것입니다. 예를 들어 DE/rand/1/bin, DE/best/1/bin 같은 식입니다. SciPy의 strategy와 비슷한 역할이라고 생각하면 이해가 쉽습니다.
pop_size
SciPy의 popsize가 multiplier 느낌이라면, pymoo의 pop_size는 보다 직접적으로 population 크기를 지정하는 쪽에 가깝습니다. 변수 수가 늘수록 population을 어느 정도 키워야 탐색 다양성이 유지됩니다. pymoo 문서의 가이드 문구에서도 classical setting으로 변수 수의 약 10배 정도를 먼저 시도해볼 수 있다고 안내합니다.
CR
CR은 crossover constant입니다. pymoo 문서 가이드에서는 separable function에는 낮은 CR이 도움이 될 수 있지만, 실제 문제처럼 변수 의존성이 큰 경우에는 오히려 CR=0.9 쪽이 적절할 수 있다고 설명합니다.
dither
dither는 F를 고정하지 않고 랜덤하게 흔들어 주는 개념입니다. pymoo 문서도 이 기법이 noisy objective에서 convergence behavior를 개선할 수 있다고 소개합니다.
sampling
초기 해를 어떻게 생성할지 정하는 옵션입니다. 예제에서는 LHS()를 사용하고 있는데, 이는 Latin Hypercube Sampling 기반 초기화입니다. 초기 다양성을 확보하고 싶을 때 꽤 자주 쓰입니다.
6. 자주 겪는 문제
너무 빨리 수렴할 때
대부분은 조기 수렴 문제입니다. 이럴 때는 보통 아래를 먼저 봅니다.
- population이 너무 작은지
- F가 너무 작은지
- best 계열 전략이 너무 공격적으로 수렴하는지
- 초기 범위가 너무 좁은지
SciPy 문서도 더 좋은 전역 최소를 찾고 싶다면 더 큰 popsize, 더 큰 mutation, 낮은 recombination이 탐색 반경을 넓히는 데 도움이 될 수 있다고 설명합니다. 대신 수렴은 느려집니다.
결과가 너무 불안정할 때
반대로 탐색이 너무 과한 경우일 수 있습니다.
- F가 너무 큰지
- CR이 너무 높은지
- 세대 수만 늘리고 bounds는 지나치게 넓은지
- 목적함수 자체에 noise가 큰지
이때는 탐색 강도를 조금 낮추고, 변수 범위를 더 현실적으로 주는 편이 좋습니다.
계산 시간이 너무 오래 걸릴 때
DE는 population 기반이라 목적함수 평가 횟수가 생각보다 많아질 수 있습니다. 그래서 아래 순서로 줄여보면 좋습니다.
- 먼저 bounds를 현실적으로 줄인다
- 필요 이상으로 큰 population을 쓰지 않는다
- 목적함수 계산이 무거우면 workers 병렬화를 검토한다
- Python 호출 오버헤드가 크면 vectorized를 검토한다
7. 그래서 처음에는 무엇을 쓰는 게 좋을까?
제 기준에서는 아래처럼 정리하면 가장 편합니다.
1) 연속 변수 전역 최적화를 빠르게 써보고 싶다
→ scipy.optimize.differential_evolution
2) DE 변형을 더 직접 제어하고 싶다
→ pymoo.DE
3) 나중에 멀티오브젝티브나 다른 진화 알고리즘까지 넓혀갈 계획이 있다
→ pymoo 쪽이 확장성이 좋음
마무리
정리하면 DE 패키지를 Python에서 시작할 때 가장 많이 볼 선택지는 SciPy와 pymoo 입니다.
- SciPy는 빠르게 적용하기 좋고
- pymoo는 DE의 구조를 더 명시적으로 다루기 좋습니다
그리고 DE의 핵심 하이퍼파라미터는 아래처럼 이해하면 편합니다.
- strategy / variant : 어떤 방식으로 새 후보를 만들지
- popsize / pop_size : 탐색 다양성
- mutation(F) : 탐색 반경
- recombination(CR) : 새 정보 반영 강도
- maxiter : 탐색 시간
- init / sampling : 시작점 다양성
- tol, atol : 언제 멈출지
- workers, vectorized : 계산 가속
- constraints, integrality : 실무 제약 반영
DE는 구조는 단순하지만, 의외로 실무 문제에 꽤 잘 버티는 전역 최적화 기법입니다.
특히 미분이 어렵고, 변수 상호작용이 복잡하고, local minimum이 많은 문제에서 한 번쯤 시도해볼 만한 방법입니다. 처음에는 SciPy로 감을 잡고, 필요하면 pymoo로 넘어가는 흐름이 가장 무난합니다.
댓글