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=[bb2bdb0],=[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를 여러 번 돌려보고 평균을 냄.

i1Mim=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,Θ)=minj=1,,nxiθ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)sminsmaxsminV_\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 에 대해, 평균과 분산은 다음과 같이 추정됨:

μ^=1TR11TR11,σ^2=(1μ^)TR1(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)=μ^+rTR1(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}
  • rTR1r^T R^{-1}: 가중치에 해당하는 부분.
  • rr: θnew\theta^{new}와 기존 점들 간의 상관관계 벡터
  • RR: 기존 점들끼리의 상관관계 행렬.

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

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

s2(θnew)=σ^2(1rTR1r+(11TR1r)21TR11)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)
  • 핵심은 rTR1rr^T R^{-1} r 항으로, θnew\theta^{new}가 기존 데이터 포인트들과 얼마나 강하게 연관되어 있는지를 나타냄.
    • 만약 θnew\theta^{new}가 기존 점들과 매우 가깝다면(탐색이 이미 된 영역), rTR1rr^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=argmaxθ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=bestL,LN(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=bestmGP(θ)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)