안녕하세요.
“기억하고자 하는 모든 것”을 담아내는 “리멤버미” 입니다.
최적화 문제를 풀다 보면, 목적함수의 식을 깔끔하게 알 수 없거나, 한 번 평가하는 데 시간이 오래 걸리거나, gradient를 쓰기 어려운 경우가 있습니다. 이런 상황에서 자주 등장하는 방법이 바로 BO(Bayesian Optimization) 입니다. 공식 문서들도 BO를 비싼(expensive) black-box 함수를 적은 평가 횟수로 최적화하기 위한 방법으로 설명하고 있고, 핵심 아이디어는 Gaussian Process 같은 surrogate model로 목적함수를 근사한 뒤 acquisition function으로 다음 탐색 지점을 고르는 것입니다.
핵심만 먼저 말하면, Python에서 BO를 시작할 때는 보통 아래 흐름으로 생각하면 편합니다.
- 가장 쉽게 시작하고 싶다 → bayesian-optimization
- 최소화 문제를 간단히 돌리고 시각화까지 보고 싶다 → scikit-optimize
- 머신러닝 하이퍼파라미터 튜닝 중심이다 → Optuna
- 연구용, PyTorch 기반, GPU/배치/고급 acquisition function까지 쓰고 싶다 → BoTorch
이번 글에서는 BO 이론 자체를 길게 파기보다, Python에서 어떤 패키지로 시작하면 좋은지, 그리고 주요 하이퍼파라미터를 어떻게 이해하면 좋은지를 중심으로 실무적으로 정리해보겠습니다.
[인공지능/AI/최적화]휴리스틱 알고리즘 BO(Bayesian Optimization)란? 적은 실험으로 최적점을 똑똑하게
안녕하세요.“기억하고자 하는 모든 것”을 담아내는 “리멤버미” 입니다. 최적화 문제를 풀다 보면 이런 상황을 자주 만나게 됩니다.한 번 평가하는 데 시간이 오래 걸리거나, 실제 실험 비용
diary.remembermeeternally.com
어떤 Python 패키지를 쓰면 좋을까?
현재 기준으로 많이 참고할 만한 패키지는 크게 4가지 축입니다.
bayesian-optimization은 PyPI 기준 3.2.1이며 2026년 3월 공개 릴리스가 올라와 있습니다. 이 패키지는 공식 설명에서도 Gaussian Process 기반의 bayesian global optimization 패키지라고 소개하고 있고, expensive function과 exploration/exploitation 균형이 중요한 문제에 잘 맞는다고 설명합니다. 개인적으로는 “BO를 처음 손으로 돌려보는 용도”에 가장 직관적입니다.
scikit-optimize는 PyPI 기준 0.10.2이고, (very) expensive and noisy black-box functions를 위한 sequential model-based optimization 도구라고 소개합니다. gp_minimize가 깔끔해서 최소화 문제를 빠르게 실험하기 좋고, acquisition function이나 convergence plot도 다루기 편합니다.
Optuna는 PyPI 기준 4.8.0이며, 특히 머신러닝을 위한 자동 하이퍼파라미터 최적화 프레임워크로 설명됩니다. 다만 여기서 한 가지 중요한 점은, create_study() 기본 샘플러가 single-objective에서는 TPESampler라는 점입니다. 즉, Optuna를 쓴다고 해서 자동으로 “클래식 GP 기반 BO”가 되는 것은 아니고, GP 기반 BO를 원한다면 GPSampler를 명시적으로 넣는 편이 맞습니다.
BoTorch는 PyPI와 공식 사이트 기준 0.17.2이며, 이름 그대로 Bayesian Optimization in PyTorch 입니다. 공식 문서에서도 modular 구조, PyTorch 기반, GPU 활용, analytic/Monte-Carlo acquisition function 지원을 강점으로 내세우고 있어서, 고급 실험이나 배치 BO, 맞춤 acquisition function이 필요한 경우에 특히 강합니다.
1. 가장 쉽게 시작하기: bayesian-optimization
설치
pip install bayesian-optimization
공식 문서 기준으로 이 패키지의 중심 클래스는 bayes_opt.BayesianOptimization 입니다. f에는 최대화할 함수, pbounds에는 각 파라미터의 최소/최대 범위를 넣습니다. 즉, 이 패키지는 기본적으로 maximize 기준 API라는 점을 먼저 기억해두면 좋습니다. 최소화 문제라면 실무에서는 보통 목적함수에 음수를 붙여 최대화 형태로 바꿔 쓰는 경우가 많습니다.
간단한 예제
from bayes_opt import BayesianOptimization
# 최소화하고 싶은 함수:
# (x-2.5)^2 + (y+1)^2
# 하지만 bayesian-optimization은 maximize 기준이므로 음수를 붙인다.
def objective(x, y):
return -((x - 2.5)**2 + (y + 1.0)**2)
optimizer = BayesianOptimization(
f=objective,
pbounds={
"x": (-5, 5),
"y": (-5, 5),
},
random_state=42,
verbose=2,
allow_duplicate_points=False,
)
optimizer.maximize(
init_points=5,
n_iter=25,
)
print(optimizer.max)
이 코드는 흐름이 단순합니다.
- 목적함수를 만든다
- 탐색 범위 pbounds를 정한다
- 랜덤 초기 탐색 init_points를 수행한다
- 이후 n_iter 동안 surrogate model과 acquisition function을 이용해 더 유망한 점을 고른다
- 최종적으로 optimizer.max에서 가장 좋은 결과를 확인한다
2. bayesian-optimization에서 꼭 알아야 할 하이퍼파라미터
pbounds
pbounds는 가장 기본이 되는 설정입니다. 공식 API에서도 파라미터 이름을 key로 하고 (min, max) 튜플을 value로 갖는 dictionary라고 설명합니다. BO는 search space를 무한대로 두고 쓰는 방식이 아니라, 먼저 어디를 탐색할지 범위를 정해놓고 그 안에서 효율적으로 샘플링하는 구조라고 보면 됩니다. 범위를 너무 넓게 주면 탐색 낭비가 커지고, 너무 좁게 주면 좋은 해가 밖에 있어도 못 찾을 수 있습니다.
init_points
maximize(init_points=..., n_iter=...)에서 init_points는 BO를 시작하기 전에 랜덤으로 찍어보는 초기 점 개수입니다. 공식 문서에서도 “optimization을 시작하기 전에 probe할 random points 수”로 설명합니다. 이 값이 너무 작으면 초반 surrogate model이 편향될 수 있고, 너무 크면 그냥 random search에 가까워집니다. 보통 변수 수가 적다면 5~10 정도부터 시작해도 무난합니다.
n_iter
n_iter는 랜덤 초기화 이후 실제 BO 반복 횟수입니다. 공식 문서 표현 그대로 보면, “최대값을 찾기 위해 method가 시도하는 iteration 수”입니다. 결국 BO도 예산이 필요하므로, 이 값은 얼마나 많은 함수 평가를 허용할 것인가에 해당합니다. 목적함수 한 번 평가가 아주 비싸다면 init_points보다 n_iter를 더 신중하게 잡아야 합니다.
random_state
재현성용 seed입니다. 문서에서는 int, RandomState, 또는 None을 받을 수 있다고 설명합니다. 실험 결과를 비교하거나 블로그 예제를 재현하고 싶다면 꼭 고정해두는 편이 좋습니다.
verbose
로그 출력 레벨입니다. 공식 API에도 verbosity level이라고 되어 있습니다. 초반에는 어떤 점을 찍는지 흐름을 보는 것이 도움이 되므로 켜두는 편이 좋고, 실제 자동화 파이프라인에서는 줄이거나 꺼두는 경우가 많습니다.
allow_duplicate_points
기본값은 False이고, 같은 점을 중복 등록할지 여부를 결정합니다. 공식 문서에서는 노이즈가 큰 문제에서는 같은 점을 반복 측정하는 것이 유의미할 수 있으므로 True가 도움이 될 수 있다고 설명합니다. 반대로 deterministic objective라면 중복 점은 대체로 낭비가 됩니다.
constraint
이 패키지는 constrained global optimization도 지원합니다. 문서에서는 constraint로 NonlinearConstraint를 받을 수 있고, constraint 함수의 인자 이름과 f의 인자 이름이 같아야 한다고 설명합니다. 즉, 단순한 unconstrained toy problem을 넘어서 제약이 있는 BO도 어느 정도 다룰 수 있습니다.
bounds_transformer
공식 문서의 표현대로, 제공하면 bounds에 transformation을 적용하는 옵션입니다. 탐색 범위를 점진적으로 줄여가고 싶을 때 쓰는 고급 기능 쪽에 가깝습니다. 처음에는 없어도 충분하지만, 탐색이 너무 넓고 비효율적일 때 고려해볼 만합니다.
3. BO에서 정말 중요한 하이퍼파라미터: Acquisition Function
BO에서 가장 중요한 철학은 “다음 점을 어떻게 고를 것인가”입니다. 이 역할을 하는 것이 acquisition function입니다. bayesian-optimization 문서에서는 기본 acquisition 함수 계열로 Upper Confidence Bound(UCB), Probability of Improvement(PI), Expected Improvement(EI) 를 제공한다고 설명합니다. BoTorch 문서도 analytic acquisition function의 대표 예로 EI, UCB, PI를 명시하고 있습니다.
직관적으로 이해하면 아래처럼 받아들이면 됩니다.
- EI(Expected Improvement)
지금까지의 최고점보다 “얼마나 더 좋아질지”를 기대값으로 보는 방식입니다. 처음 BO를 시작할 때 가장 무난한 선택지로 자주 언급됩니다. - UCB(Upper Confidence Bound)
예측 평균뿐 아니라 불확실성까지 같이 반영합니다. 즉, 지금 좋아 보이는 곳뿐 아니라 아직 잘 모르지만 잠재력이 있는 곳도 보게 해줍니다. exploration 성향을 더 주기 좋은 편입니다. - PI(Probability of Improvement)
개선될 “확률” 자체를 보는 방식입니다. 직관적이지만, 개선 폭보다는 개선 여부에 집중한다는 느낌으로 이해하면 됩니다.
결국 acquisition function 선택은 exploration과 exploitation의 균형을 어떻게 줄 것인가의 문제입니다. 이미 좋아 보이는 주변을 더 파고들지, 아니면 아직 uncertainty가 큰 영역을 더 탐색할지 결정하는 축이라고 보면 됩니다.
4. scikit-optimize는 언제 좋을까?
scikit-optimize의 gp_minimize는 “최소화” 문제를 바로 다루기 편합니다. 공식 예제에서도 BO 루프를 설명하면서, acq_func="EI", n_calls, n_random_starts, noise, random_state 같은 인자를 중심으로 예제를 보여줍니다. 그리고 acquisition function으로 EI(기본), LCB, PI를 설명합니다.
예를 들어 이런 식입니다.
from skopt import gp_minimize
def objective(x):
return (x[0] - 2.5)**2
res = gp_minimize(
func=objective,
dimensions=[(-5.0, 5.0)],
acq_func="EI",
n_calls=20,
n_random_starts=5,
noise=1e-8,
random_state=42,
)
print(res.x, res.fun)
여기서 자주 보는 하이퍼파라미터는 아래 정도입니다.
- acq_func : EI / LCB / PI 중 무엇을 쓸지
- n_calls : 총 함수 평가 횟수
- n_random_starts 또는 n_initial_points : 초기 랜덤 탐색 수
- noise : 관측 노이즈 수준
- random_state : 재현성 확보
즉, 최소화 문제를 바로 쓰고 싶고, 결과 시각화까지 포함해 빠르게 실험해보고 싶다면 skopt는 여전히 꽤 편한 선택지입니다.
5. 머신러닝 하이퍼파라미터 튜닝이라면 Optuna는 어떻게 봐야 할까?
Optuna는 자동 하이퍼파라미터 최적화 프레임워크로 매우 유명하지만, 기본 create_study()는 single-objective에서 TPESampler를 사용합니다. 즉, “Optuna = 항상 GP 기반 BO”는 아닙니다. GP 기반 BO를 원하면 GPSampler를 명시적으로 지정하는 것이 맞습니다.
Optuna의 GPSampler는 공식 문서 기준으로 Gaussian process-based Bayesian optimization sampler이며, 현재 구현은 Matern kernel (ν=2.5) 과 ARD length scale을 사용합니다. single-objective에서는 acquisition function으로 log expected improvement(logEI) 를 사용한다고 설명하고 있습니다. 또 이 샘플러는 scipy와 torch가 필요합니다.
Optuna GPSampler 예제
import optuna
import numpy as np
def objective(trial):
lr = trial.suggest_float("lr", 1e-5, 1e-1, log=True)
depth = trial.suggest_int("depth", 2, 10)
# 예시용 toy objective
return (np.log10(lr) + 2.0)**2 + (depth - 6)**2
sampler = optuna.samplers.GPSampler(
seed=42,
n_startup_trials=10,
deterministic_objective=True,
)
study = optuna.create_study(
direction="minimize",
sampler=sampler,
)
study.optimize(objective, n_trials=40)
print(study.best_params)
print(study.best_value)
GPSampler에서 특히 눈여겨볼 하이퍼파라미터는 아래입니다.
- n_startup_trials : GP를 쓰기 전 초기 trial 수
- deterministic_objective : 목적함수가 deterministic한지 여부
- constraints_func : 제약조건 함수
- seed : 재현성
- warn_independent_sampling : GP 대신 independent sampler가 동작할 때 경고할지 여부
특히 deterministic_objective=True로 두면 문서 설명대로 surrogate model의 noise variance를 거의 0에 가깝게 고정합니다. 따라서 같은 파라미터를 넣으면 거의 같은 값이 나오는 안정적인 objective라면 이 옵션이 잘 맞고, 반대로 학습/검증 split이나 stochastic training 때문에 값이 흔들리는 문제라면 False 쪽이 더 자연스럽습니다.
6. BoTorch는 언제 쓰는가?
BoTorch는 입문용보다는 고급 실험용에 가깝습니다. 공식 문서가 강조하는 포인트는 modular 구조, PyTorch 기반, GPU 지원, analytic/Monte-Carlo acquisition function 지원입니다. 특히 qEI 같은 batch acquisition function이나, 직접 acquisition function을 바꾸고 autograd로 최적화하는 흐름이 강점입니다.
즉, 아래 상황이면 BoTorch 쪽이 더 어울립니다.
- 단순한 1개 점 추천이 아니라 batch BO를 하고 싶다
- GP surrogate나 acquisition function을 더 적극적으로 커스터마이징하고 싶다
- PyTorch 생태계 안에서 end-to-end로 실험하고 싶다
- GPU를 활용한 고급 최적화를 하고 싶다
7. 처음 시작할 때 추천하는 조합
처음 BO를 배우는 입장이라면 저는 아래처럼 시작하는 것을 추천합니다.
1) BO 개념을 가장 직관적으로 익히고 싶다
→ bayesian-optimization
optimizer.maximize(init_points=5, n_iter=25)
이 조합은 “랜덤으로 조금 찍고 → GP로 더 똑똑하게 찍는다”는 BO의 핵심 감각을 익히기에 좋습니다.
2) 최소화 문제를 간단히 돌리고 싶다
→ scikit-optimize
gp_minimize(..., acq_func="EI", n_calls=20, n_random_starts=5)
최소화 기준 API가 더 자연스럽고, 시각화 자료를 만들기도 편합니다.
3) ML 하이퍼파라미터 튜닝을 실무적으로 하고 싶다
→ Optuna + GPSampler
Optuna는 define-by-run 방식과 study 관리가 편하고, GP 기반 BO가 필요할 때는 GPSampler를 명시적으로 선택하면 됩니다.
4) 연구용 / 배치 / 고급 acquisition function 실험이 필요하다
→ BoTorch
EI, UCB, PI뿐 아니라 qEI 같은 MC 기반 acquisition function과 batch 설정까지 다루기 좋습니다.
마무리
정리하면, BO는 “적은 평가 횟수로 비싼 black-box 함수를 잘 찾고 싶을 때” 강한 방법입니다. 그리고 Python에서는 상황에 따라 선택지가 나뉩니다.
- 입문과 직관성 → bayesian-optimization
- 간단한 최소화 실험 → scikit-optimize
- ML 튜닝 워크플로우 → Optuna
- 고급 연구/배치/커스터마이징 → BoTorch
그리고 실제로 가장 중요한 하이퍼파라미터는 결국 아래 몇 가지입니다.
- 초기 탐색 예산: init_points, n_startup_trials, n_random_starts
- 전체 평가 예산: n_iter, n_calls, n_trials
- acquisition function 선택: EI / UCB / PI / logEI
- 노이즈 가정: allow_duplicate_points, noise, deterministic_objective
- 재현성: random_state, seed
- 제약조건 반영: constraint, constraints_func
처음부터 너무 복잡하게 접근하기보다,
작은 toy problem에 먼저 돌려보고 → acquisition function과 초기 탐색 수를 바꿔보면서 감을 잡는 것이 가장 좋습니다. BO는 결국 “어디를 더 찍어볼까?”를 똑똑하게 결정하는 알고리즘이기 때문에, 직접 몇 번 돌려보면 exploration과 exploitation 감각이 훨씬 빨리 잡힙니다.
댓글