Overview

Surrogate model-based Derivative-Free Optimizations

September 16, 2025
9 min read
surrogate-model

Surrogate model-based Derivative-Free Optimization

Surrogate model

ℓ(θ)=m(θ)+e(θ)\ell(\theta) = m(\theta) + e(\theta)

고비용의 목적 함수를 근사하는 저비용의 모델.

  • ℓ(θ)\ell(\theta): upper level objective (실제 목적 함수, 고비용)
  • m(θ)m(\theta): surrogate model (저비용 근사 모델)
  • e(θ)e(\theta): noise (잔차)

Optimization steps

어떤 하이퍼파라미터 θ\theta를 고르면, 그걸로 모델을 학습해서 테스트 목적 함수 값 ℓ(θ,w∗(ζ),Dtest)\ell(\theta, w^*(\zeta), \mathcal{D}_{\text{test}})을 얻을 수 있음. 하지만 이 과정은 시간도 오래 걸리고, 학습 랜덤성 ζ\zeta 때문에 노이즈도 있음. 그렇기에 직접 ℓ(θ)\ell(\theta)를 최적화 하는 대신, 몇 번만 계산해본 결과들을 토대로 근사함수(surrogate model)를 만들어 놓고 그걸 이용해서 더 효율적으로 최적화를 하려는 것이 목적.

  1. Approximate a function
  2. Select next hyperparameter

Radial Basis Functions (RBF) + Stochastic Sampling

radial = distance from a center

mRBF(θ)=∑i=1Nλiϕ(∣∣θ−θi∣∣)+p(θ)m_{RBF}(\theta) = \sum_{i=1}^{N} \lambda_i \phi(||\theta - \theta_i||) + p(\theta)
  • θi\theta_i: 지금까지 실험해본 하이퍼파라미터 값들
  • ℓi\ell_i: 그때 얻은 테스트 목적 함수 값의 평균값(기대값)
  • ϕ\phi: 중심 θi\theta_i로부터의 거리만을 보는 함수 (Gaussian kernel, r3r^3 등)
  • λi\lambda_i: 각 중심 θi\theta_i 주변의 영향력을 결정하는 계수
  • p(θ)=b0+b⊤θp(\theta) = b_0 + b^\top \theta: 전체적인 선형 추세(기울기) 를 표현

이 근사 모델이 제대로 작동하려면 다음 두 가지 조건을 만족해야 함:

  1. 보간 조건: 모든 실험점에서 근사 모델이 실제 실험값과 일치해야 함.

    mRBF(θi)=ℓi,i=1,…,Nm_{\mathrm{RBF}}(\theta_i) = \ell_i, \quad i=1,\dots,N
  2. 추세 분리 조건: RBF 부분이 선형 추세와 섞이지 않도록,

    PTλ=0P^T \lambda = 0

이 두 조건을 합치면 다음과 같은 연립방정식을 얻게 됨.

[ΦPPT0][λb]=[ℓ0]\begin{bmatrix} \Phi & P \\ P^T & 0 \end{bmatrix} \begin{bmatrix} \lambda \\ b \end{bmatrix} = \begin{bmatrix} \ell \\ 0 \end{bmatrix} Φij=ϕ(∣∣θi−θj∣∣),P=[θ1T1θ2T1⋮⋮θNT1],b=[bb2⋮bdb0],ℓ=[Eζ ⁣[ℓ(θ1,w1∗(ζ),Dtest)]Eζ ⁣[ℓ(θ2,w2∗(ζ),Dtest)]⋮Eζ ⁣[ℓ(θn,wn∗(ζ),Dtest)]]\Phi_{ij} = \phi(||\theta_i - \theta_j||), \quad P = \begin{bmatrix} \theta_1^T & 1 \\ \theta_2^T & 1 \\ \vdots & \vdots \\ \theta_N^T & 1 \end{bmatrix}, \quad b = \begin{bmatrix} b \\ b_2 \\ \vdots \\ b_{d} \\ b_0 \end{bmatrix}, \quad \ell = \begin{bmatrix} \mathbb{E}_{\zeta}\!\left[ \ell(\theta_1, w_1^*(\zeta), \mathcal{D}_{\text{test}}) \right] \\[6pt] \mathbb{E}_{\zeta}\!\left[ \ell(\theta_2, w_2^*(\zeta), \mathcal{D}_{\text{test}}) \right] \\[6pt] \vdots \\[6pt] \mathbb{E}_{\zeta}\!\left[ \ell(\theta_n, w_n^*(\zeta), \mathcal{D}_{\text{test}}) \right] \end{bmatrix}
  • Φij=ϕ(∥θi−θj∥)\Phi_{ij} = \phi(\lVert \theta_i - \theta_j \rVert): 각 점들 사이의 거리에 대한 RBF 값
  • PP: 각 점의 좌표와 상수항 1을 붙여 만든 행렬
  • ℓ\ell: 각 실험점에서 얻은 평균 목적 함수 값

이 시스템을 풀면 λ,b\lambda, b가 정해짐.

Note (확률적 샘플링)

ℓ\ell 벡터 안의 Eζ[⋅]\mathbb{E}_\zeta[\cdot] (확률적 샘플링)

실제로는 기대값 Eζ[⋅]\mathbb{E}_\zeta[\cdot]을 직접 알 수 없으니, 같은 하이퍼파라미터 θi\theta_i를 여러 번 돌려보고 평균을 냄.

ℓi≈1Mi∑m=1Miℓ(θi,wi∗(ζi,m),Dtest)\ell_i \approx \frac{1}{M_i}\sum_{m=1}^{M_i}\ell(\theta_i, w_i^*(\zeta_{i,m}), \mathcal{D}_{\text{test}})
  • 이렇게 하면 랜덤성 때문에 생기는 잡음을 줄일 수 있음.
  • 중요한 후보점은 반복 횟수를 더 늘려서 더 정확하게 평균을 추정하기도 함.
  • 노이즈가 심하면 “보간” 대신 “회귀” 방식으로 약간 틀어져도 되게 만듦 (ridge 같은 방식).

최종 최적화 루프는 다음과 같음:

  1. 몇 개 하이퍼파라미터 θi\theta_i를 골라서 실제 목적 함수 값 ℓi\ell_i를 계산
  2. 위 시스템을 풀어 mRBFm_{\mathrm{RBF}} 근사 모델을 구축
  3. 이 모델을 기반으로 새로운 θ\theta를 선택
  4. 그 θ\theta를 실제로 평가하고 다시 2번으로 돌아감

Selecting the next evaluation point

근사 모델이 완성되면, 그걸 단순히 최소화해서 다음 θ\theta를 고를 수도 있지만, 이렇게 하면 이미 조사한 영역만 파고드는 문제가 생길 수 있음. 그래서 탐색(exploration) 과 활용(exploitation) 을 모두 고려하는 절차를 씀:

  1. Local Search: 지금까지 찾은 가장 좋은 점을 기준으로, 각 좌표를 ±(작은 랜덤 값) 만큼 흔들어 MM개의 후보를 생성

  2. Global Search: 동시에, 전체 탐색 공간에서 무작위로 MM개의 후보를 추가 생성

  3. 총 2M2M 후보 각각에 대해 두 가지 점수를 계산:

    • RBF Score: surrogate 모델 mRBFm_{\mathrm{RBF}}이 예측한 값

      sRBF(xi)=∑j=1nλ^jϕ(∥xi−θj∥)+p(xi)s_{\text{RBF}}(x_i) = \sum_{j=1}^n \hat{\lambda}_j \phi(\lVert x_i - \theta_j \rVert) + p(x_i)
    • Distance Score: 기존 평가점들과 얼마나 멀리 떨어져 있는지

      Δ(xi,Θ)=min⁡j=1,…,n∥xi−θj∥\Delta(x_i, \Theta) = \min_{j=1,\dots,n} \lVert x_i - \theta_j \rVert
  4. 두 점수를 각각 0~1 범위로 스케일링:

    VΔ(xi)=Δmax⁡−Δ(xi,Θ)Δmax⁡−Δmin⁡,Vs(xi)=sRBF(xi)−smin⁡smax⁡−smin⁡V_\Delta(x_i) = \frac{\Delta_{\max} - \Delta(x_i,\Theta)}{\Delta_{\max} - \Delta_{\min}}, \quad V_s(x_i) = \frac{s_{\text{RBF}}(x_i) - s_{\min}}{s_{\max} - s_{\min}}
  5. 가중합으로 최종 점수 계산:

    V(xi)=ωVΔ(xi)+(1−ω)Vs(xi)V(x_i) = \omega V_\Delta(x_i) + (1-\omega) V_s(x_i)
    • ω\omega 값으로 탐색과 활용의 균형을 조절 (ω\omega가 크면 탐색 위주, 작으면 활용 위주)
  6. 다음 평가점은 V(xi)V(x_i)가 가장 작은 후보로 선택

이렇게 하면 단순히 surrogate 최소값을 고르는 게 아니라,

  • RBF 점수 → 지금까지 학습된 surrogate 상에서 좋은 위치(활용)
  • Distance 점수 → 새로운 지역을 탐색해보려는 시도(탐색)
Example (RBF + Stochastic Sampling python example)
import numpy as np
# -------------------------
# 목적 함수 (예시)
# -------------------------
def objective(x):
"""노이즈가 있는 quadratic"""
return np.sum(np.array(x)**2) + np.random.randn()*0.1
# -------------------------
# RBF Surrogate
# -------------------------
class RBF_Surrogate:
def __init__(self, phi="cubic"):
if phi == "gaussian":
self.phi = lambda r: np.exp(-(r**2))
elif phi == "linear":
self.phi = lambda r: r
else: # cubic
self.phi = lambda r: r**3
def fit(self, X, y):
self.X = np.array(X)
self.y = np.array(y)
N, d = self.X.shape
Phi = np.zeros((N, N))
for i in range(N):
for j in range(N):
Phi[i, j] = self.phi(np.linalg.norm(self.X[i] - self.X[j]))
P = np.hstack([self.X, np.ones((N, 1))])
A = np.block([
[Phi, P],
[P.T, np.zeros((d+1, d+1))]
])
b = np.concatenate([self.y, np.zeros(d+1)])
sol = np.linalg.solve(A, b)
self.lmbda = sol[:N]
self.beta = sol[N:]
def predict(self, X_new):
X_new = np.atleast_2d(X_new)
y_pred = np.zeros(len(X_new))
for k, xk in enumerate(X_new):
rbf_sum = 0
for i in range(len(self.X)):
r = np.linalg.norm(xk - self.X[i])
rbf_sum += self.lmbda[i] * self.phi(r)
trend = np.dot(self.beta[:-1], xk) + self.beta[-1]
y_pred[k] = rbf_sum + trend
return y_pred
# -------------------------
# Data 관리 클래스
# -------------------------
class DataStore:
def __init__(self, dim, m_init=5, bounds=(-2, 2)):
self.dim = dim
self.bounds = bounds
self.m = m_init
self.S = np.random.uniform(bounds[0], bounds[1], size=(m_init, dim))
self.Y = np.array([objective(s) for s in self.S])
self.rbf = RBF_Surrogate()
self.update_rbf()
def update_rbf(self):
self.rbf.fit(self.S, self.Y)
def add_point(self, x):
y = objective(x)
self.S = np.vstack([self.S, x])
self.Y = np.append(self.Y, y)
self.m += 1
self.update_rbf()
# -------------------------
# 후보 점수 계산 (RBF 예측 + 거리)
# -------------------------
def RBF_score(candidates, data, omega=0.5):
s = data.rbf.predict(candidates)
dist = np.array([np.min(np.linalg.norm(data.S - c, axis=1)) for c in candidates])
# 0~1 scaling
s_scaled = (s - s.min()) / (s.max() - s.min() + 1e-8)
d_scaled = (dist.max() - dist) / (dist.max() - dist.min() + 1e-8)
score = omega*d_scaled + (1-omega)*s_scaled
return score
# -------------------------
# 최적화 루프
# -------------------------
def run_rbf(dim=2, n_iter=10):
np.random.seed(0)
data = DataStore(dim=dim)
for it in range(n_iter):
best_idx = np.argmin(data.Y)
best_x = data.S[best_idx]
# 후보 생성
local_candidates = best_x + np.random.uniform(-0.5, 0.5, size=(20, dim))
global_candidates = np.random.uniform(data.bounds[0], data.bounds[1], size=(20, dim))
candidates = np.vstack([local_candidates, global_candidates])
# 점수 계산
score = RBF_score(candidates, data, omega=0.5)
# 다음 점 선택
next_x = candidates[np.argmin(score)]
data.add_point(next_x)
print(f"Iter {it}: best_y={data.Y.min():.4f}, next_x={next_x}")
return data
# 실행
if __name__ == "__main__":
result = run_rbf(dim=2, n_iter=10)

Gaussian Process (GP) + Expected Improvement (EI)

가우시안 프로세스(GP)는 함수 ℓ(θ)\ell(\theta)를 평균과 공분산 구조로 묘사하는 확률적 모델.

mGP(θ)=μ+Z(θ)m_{GP}(\theta) = \mu + Z(\theta)
  • μ\mu: 평균 (stochastic process의 mean)
  • Z(θ)Z(\theta): GP 랜덤항, Z(θ)∼N(0,σ2)Z(\theta) \sim \mathcal{N}(0, \sigma^2)
  • 서로 다른 위치의 Z(θ)Z(\theta)들은 거리에 따라 상관관계를 가짐
Note (GP's meaning)
  • 평균 (Mean) mGP(θnew)m_{GP}(\theta^{new}): 현재까지 관찰된 데이터들을 바탕으로 예측한 ℓ(θnew)\ell(\theta^{new})의 가장 가능성 높은 값 (기대값).

  • 분산 (Variance) s2(θnew)s^2(\theta^{new}): 위 예측이 얼마나 불확실한지를 나타내는 값.

    • 관측된 데이터 포인트 근처에서는 분산이 매우 작음 (거의 0). 즉, 예측이 매우 확실.
    • 관측된 데이터 포인트들로부터 멀리 떨어진 미지의 영역에서는 분산이 큼. 즉, 예측이 매우 불확실.

상관관계는 보통 아래와 같이 커널 함수로 정의됨 (“가까운 입력은 비슷한 출력을 가질 것이다”라는 가정).:

Corr(Z(θk),Z(θl))=exp⁡(−∑i=1dγi∣θk(i)−θl(i)∣qi)\text{Corr}(Z(\theta_k), Z(\theta_l)) = \exp\left(-\sum_{i=1}^d \gamma_i \lvert \theta_k^{(i)} - \theta_l^{(i)} \rvert^{q_i}\right)
  • dd: 하이퍼파라미터 차원 수
  • γi\gamma_i: 각 차원의 스케일링 파라미터 (maximium likelihood estimation으로 추정)
  • qiq_i: smoothness 파라미터 (커널의 형태를 결정, 보통 1 또는 2)

주어진 데이터 {(θi,ℓi)}i=1n\{(\theta_i, \ell_i)\}_{i=1}^n 에 대해, 평균과 분산은 다음과 같이 추정됨:

μ^=1TR−1ℓ1TR−11,σ^2=(ℓ−1μ^)TR−1(ℓ−1μ^)n\hat{\mu} = \frac{1^T R^{-1} \ell}{1^T R^{-1} 1}, \quad \hat{\sigma}^2 = \frac{(\ell - 1 \hat{\mu})^T R^{-1} (\ell - 1 \hat{\mu})}{n}
  • RR: n×nn\times n correlation matrix (Rkl=Corr(Z(θk),Z(θl))R_{kl} = \text{Corr}(Z(\theta_k), Z(\theta_l)))
  • ℓ\ell: upper level objective 벡터 (실제 평가값들)

새로운 점 θnew\theta^{new}에서의 평균(예측값):

기존 관측값들의 가중 평균으로 예측을 수행

mGP(θnew)=μ^+rTR−1(ℓ−1μ^)m_{GP}(\theta^{new}) = \hat{\mu} + r^T R^{-1} (\ell - 1\hat{\mu}) r=[Corr(Z(θnew),Z(θ1))⋮Corr(Z(θnew),Z(θn))]r = \begin{bmatrix} \text{Corr}(Z(\theta^{new}), Z(\theta_1)) \\ \vdots \\ \text{Corr}(Z(\theta^{new}), Z(\theta_n)) \end{bmatrix}
  • rTR−1r^T R^{-1}: 가중치에 해당하는 부분.
  • rr: θnew\theta^{new}와 기존 점들 간의 상관관계 벡터
  • RR: 기존 점들끼리의 상관관계 행렬.

즉, θnew\theta^{new}와 더 가까운(상관관계가 높은) 기존 점의 함수값 ℓi\ell_i에 더 높은 가중치를 부여하여 예측값을 계산

새로운 점 θnew\theta^{new}에서의 분산(불확실성) 추정:

s2(θnew)=σ^2(1−rTR−1r+(1−1TR−1r)21TR−11)s^2(\theta^{new}) = \hat{\sigma}^2 \left( 1 - r^T R^{-1} r + \frac{(1 - 1^T R^{-1} r)^2}{1^T R^{-1} 1} \right)
  • 핵심은 rTR−1rr^T R^{-1} r 항으로, θnew\theta^{new}가 기존 데이터 포인트들과 얼마나 강하게 연관되어 있는지를 나타냄.
    • 만약 θnew\theta^{new}가 기존 점들과 매우 가깝다면(탐색이 이미 된 영역), rTR−1rr^T R^{-1} r 값은 1에 가까워져서 전체 분산 s2(θnew)s^2(\theta^{new})는 0에 가까워짐 (불확실성 감소).
    • 반대로 θnew\theta^{new}가 기존 점들과 매우 멀다면(미탐색 영역), 이 값은 0에 가까워져서 분산은 커짐 (불확실성 증가).

최종 최적화 루프는 다음과 같음:

  1. 몇 개 초기점에서 ℓ(θ)\ell(\theta)를 계산
  2. GP 모델을 학습해 mGP(θ)m_{GP}(\theta), s(θ)s(\theta) 추정
  3. 후보 θ\theta들에 대해 Expected Improvement E[I]E[I] 계산
  4. θnew=arg⁡max⁡θE[I]\theta^{new} = \arg\max_\theta E[I] 선택
  5. 실제 평가 후 데이터셋에 추가 → 2단계로 돌아감

Expected Improvement (EI)

다음 평가점을 고르기 위해, 현재까지의 최적값보다 더 좋아질 기대값을 고려.

  1. 현재 최적 목적함수 값:

    ℓbest=ℓ(θbest)\ell^{best} = \ell(\theta^{best})
  2. 개선(improvement):

    I=ℓbest−L,L∼N(mGP(θ),s2(θ))I = \ell^{best} - L, \quad L \sim \mathcal{N}(m_{GP}(\theta), s^2(\theta))
    • LL: 불확실성을 반영하기 위한 확률변수 (즉, 평균이 mGP(θ)m_{GP}(\theta)이고 분산이 s2(θ)s^2(\theta)인 정규분포를 따르는 값)
  3. 기대 개선값:

    E[I]=s(θ)(Z⋅Φ(Z)+ϕ(Z))whereZ=ℓbest−mGP(θ)s(θ)E[I] = s(\theta) \left( Z \cdot \Phi(Z) + \phi(Z) \right) \quad \text{where} \quad Z = \frac{\ell^{best} - m_{GP}(\theta)}{s(\theta)}
    • Φ\Phi: 표준정규 누적분포 (CDF)
    • ϕ\phi: 표준정규 밀도함수 (PDF)

genetic algorithm 등을 이용해 E[I]E[I]를 최대화하는 θ\theta를 찾음.

Example (GP + EI python example)
import numpy as np
import array
from deap import base, creator, tools, algorithms
from sklearn.gaussian_process import GaussianProcessRegressor
from sklearn.gaussian_process.kernels import RBF
from scipy.stats import norm
# -------------------------
# 목적 함수 (black-box 예시)
# -------------------------
def objective(x):
"""노이즈가 있는 2차 함수"""
return np.sum(np.array(x) ** 2) + np.random.randn() * 0.1
# -------------------------
# 데이터 저장용 클래스 흉내
# -------------------------
class DataStore:
def __init__(self, dim, m_init=5, bounds=(-2, 2)):
self.dim = dim
self.bounds = bounds
self.m = m_init # 초기 샘플 수
self.S = np.random.uniform(bounds[0], bounds[1], size=(m_init, dim))
self.Y = np.array([objective(s) for s in self.S])
# GP surrogate
self.gpr = GaussianProcessRegressor(kernel=RBF(), random_state=0)
self.update_gp()
def update_gp(self):
self.gpr.fit(self.S, self.Y.reshape(-1, 1))
def add_point(self, x):
y = objective(x)
self.S = np.vstack([self.S, x])
self.Y = np.append(self.Y, y)
self.m += 1
self.update_gp()
# -------------------------
# EI 함수 (스크린샷 기반)
# -------------------------
def Expected_improvement(x, data):
x_to_predict = np.array(x).reshape(1, -1)
mu, sigma = data.gpr.predict(x_to_predict, return_std=True)
greater_is_better = False
if greater_is_better:
loss_optimum = np.max(data.Y[:data.m])
else:
loss_optimum = np.min(data.Y[:data.m])
scaling_factor = (-1) ** (not greater_is_better)
with np.errstate(divide='ignore'):
Z = scaling_factor * (mu - loss_optimum) / sigma
expected_improvement = scaling_factor * (mu - loss_optimum) * norm.cdf(Z) + sigma * norm.pdf(Z)
expected_improvement[sigma == 0.0] = 0.0
return -expected_improvement[0] # deap은 최소화 문제
# -------------------------
# DEAP 설정
# -------------------------
def run_gp_ei(dim=2, n_iter=10):
data = DataStore(dim=dim)
# Fitness 정의
creator.create("FitnessMin", base.Fitness, weights=(-1.0,))
creator.create("Individual", array.array, typecode='d', fitness=creator.FitnessMin)
toolbox = base.Toolbox()
# 각 차원 범위
for i in range(dim):
INT_MIN, INT_MAX = data.bounds
toolbox.register(f"attr_float_{i}", np.random.uniform, INT_MIN, INT_MAX)
# Individual 생성
toolbox.register("individual", tools.initCycle, creator.Individual,
(toolbox.__getattribute__(f"attr_float_{i}") for i in range(dim)), n=1)
toolbox.register("population", tools.initRepeat, list, toolbox.individual)
# Evaluate = EI
toolbox.register("evaluate", Expected_improvement, data=data)
toolbox.register("mate", tools.cxTwoPoint)
toolbox.register("mutate", tools.mutUniformInt,
low=[data.bounds[0]]*dim, up=[data.bounds[1]]*dim, indpb=0.2)
toolbox.register("select", tools.selTournament, tournsize=3)
# -------------------------
# 최적화 루프
# -------------------------
for it in range(n_iter):
pop = toolbox.population(n=20)
hof = tools.HallOfFame(1)
algorithms.eaSimple(pop, toolbox, cxpb=0.5, mutpb=0.3,
ngen=15, halloffame=hof, verbose=False)
next_x = np.array(hof[0])
data.add_point(next_x)
print(f"Iter {it}: best_y={data.Y.min():.4f}, next_x={next_x}")
return data
# 실행
if __name__ == "__main__":
result = run_gp_ei(dim=2, n_iter=10)