Surrogate model-based Derivative-Free Optimization
Surrogate model
고비용의 목적 함수를 근사하는 저비용의 모델.
- : upper level objective (실제 목적 함수, 고비용)
- : surrogate model (저비용 근사 모델)
- : noise (잔차)
Optimization steps
어떤 하이퍼파라미터 를 고르면, 그걸로 모델을 학습해서 테스트 목적 함수 값 을 얻을 수 있음. 하지만 이 과정은 시간도 오래 걸리고, 학습 랜덤성 때문에 노이즈도 있음. 그렇기에 직접 를 최적화 하는 대신, 몇 번만 계산해본 결과들을 토대로 근사함수(surrogate model)를 만들어 놓고 그걸 이용해서 더 효율적으로 최적화를 하려는 것이 목적.
- Approximate a function
- Select next hyperparameter
Radial Basis Functions (RBF) + Stochastic Sampling
radial = distance from a center
- : 지금까지 실험해본 하이퍼파라미터 값들
- : 그때 얻은 테스트 목적 함수 값의 평균값(기대값)
- : 중심 로부터의 거리만을 보는 함수 (Gaussian kernel, 등)
- : 각 중심 주변의 영향력을 결정하는 계수
- : 전체적인 선형 추세(기울기) 를 표현
이 근사 모델이 제대로 작동하려면 다음 두 가지 조건을 만족해야 함:
-
보간 조건: 모든 실험점에서 근사 모델이 실제 실험값과 일치해야 함.
-
추세 분리 조건: RBF 부분이 선형 추세와 섞이지 않도록,
이 두 조건을 합치면 다음과 같은 연립방정식을 얻게 됨.
- : 각 점들 사이의 거리에 대한 RBF 값
- : 각 점의 좌표와 상수항 1을 붙여 만든 행렬
- : 각 실험점에서 얻은 평균 목적 함수 값
이 시스템을 풀면 가 정해짐.
Note (확률적 샘플링)
벡터 안의 (확률적 샘플링)
실제로는 기대값 을 직접 알 수 없으니, 같은 하이퍼파라미터 를 여러 번 돌려보고 평균을 냄.
- 이렇게 하면 랜덤성 때문에 생기는 잡음을 줄일 수 있음.
- 중요한 후보점은 반복 횟수를 더 늘려서 더 정확하게 평균을 추정하기도 함.
- 노이즈가 심하면 “보간” 대신 “회귀” 방식으로 약간 틀어져도 되게 만듦 (ridge 같은 방식).
최종 최적화 루프는 다음과 같음:
- 몇 개 하이퍼파라미터 를 골라서 실제 목적 함수 값 를 계산
- 위 시스템을 풀어 근사 모델을 구축
- 이 모델을 기반으로 새로운 를 선택
- 그 를 실제로 평가하고 다시 2번으로 돌아감
Selecting the next evaluation point
근사 모델이 완성되면, 그걸 단순히 최소화해서 다음 를 고를 수도 있지만, 이렇게 하면 이미 조사한 영역만 파고드는 문제가 생길 수 있음. 그래서 탐색(exploration) 과 활용(exploitation) 을 모두 고려하는 절차를 씀:
-
Local Search: 지금까지 찾은 가장 좋은 점을 기준으로, 각 좌표를 ±(작은 랜덤 값) 만큼 흔들어 개의 후보를 생성
-
Global Search: 동시에, 전체 탐색 공간에서 무작위로 개의 후보를 추가 생성
-
총 후보 각각에 대해 두 가지 점수를 계산:
-
RBF Score: surrogate 모델 이 예측한 값
-
Distance Score: 기존 평가점들과 얼마나 멀리 떨어져 있는지
-
-
두 점수를 각각 0~1 범위로 스케일링:
-
가중합으로 최종 점수 계산:
- 값으로 탐색과 활용의 균형을 조절 (가 크면 탐색 위주, 작으면 활용 위주)
-
다음 평가점은 가 가장 작은 후보로 선택
이렇게 하면 단순히 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)는 함수 를 평균과 공분산 구조로 묘사하는 확률적 모델.
- : 평균 (stochastic process의 mean)
- : GP 랜덤항,
- 서로 다른 위치의 들은 거리에 따라 상관관계를 가짐
Note (GP's meaning)
-
평균 (Mean) : 현재까지 관찰된 데이터들을 바탕으로 예측한 의 가장 가능성 높은 값 (기대값).
-
분산 (Variance) : 위 예측이 얼마나 불확실한지를 나타내는 값.
- 관측된 데이터 포인트 근처에서는 분산이 매우 작음 (거의 0). 즉, 예측이 매우 확실.
- 관측된 데이터 포인트들로부터 멀리 떨어진 미지의 영역에서는 분산이 큼. 즉, 예측이 매우 불확실.
상관관계는 보통 아래와 같이 커널 함수로 정의됨 (“가까운 입력은 비슷한 출력을 가질 것이다”라는 가정).:
- : 하이퍼파라미터 차원 수
- : 각 차원의 스케일링 파라미터 (maximium likelihood estimation으로 추정)
- : smoothness 파라미터 (커널의 형태를 결정, 보통 1 또는 2)
주어진 데이터 에 대해, 평균과 분산은 다음과 같이 추정됨:
- : correlation matrix ()
- : upper level objective 벡터 (실제 평가값들)
새로운 점 에서의 평균(예측값):
기존 관측값들의 가중 평균으로 예측을 수행
- : 가중치에 해당하는 부분.
- : 와 기존 점들 간의 상관관계 벡터
- : 기존 점들끼리의 상관관계 행렬.
즉, 와 더 가까운(상관관계가 높은) 기존 점의 함수값 에 더 높은 가중치를 부여하여 예측값을 계산
새로운 점 에서의 분산(불확실성) 추정:
- 핵심은 항으로, 가 기존 데이터 포인트들과 얼마나 강하게 연관되어 있는지를 나타냄.
- 만약 가 기존 점들과 매우 가깝다면(탐색이 이미 된 영역), 값은 1에 가까워져서 전체 분산 는 0에 가까워짐 (불확실성 감소).
- 반대로 가 기존 점들과 매우 멀다면(미탐색 영역), 이 값은 0에 가까워져서 분산은 커짐 (불확실성 증가).
최종 최적화 루프는 다음과 같음:
- 몇 개 초기점에서 를 계산
- GP 모델을 학습해 , 추정
- 후보 들에 대해 Expected Improvement 계산
- 선택
- 실제 평가 후 데이터셋에 추가 → 2단계로 돌아감
Expected Improvement (EI)
다음 평가점을 고르기 위해, 현재까지의 최적값보다 더 좋아질 기대값을 고려.
-
현재 최적 목적함수 값:
-
개선(improvement):
- : 불확실성을 반영하기 위한 확률변수 (즉, 평균이 이고 분산이 인 정규분포를 따르는 값)
-
기대 개선값:
- : 표준정규 누적분포 (CDF)
- : 표준정규 밀도함수 (PDF)
genetic algorithm 등을 이용해 를 최대화하는 를 찾음.
Example (GP + EI python example)
import numpy as npimport arrayfrom deap import base, creator, tools, algorithmsfrom sklearn.gaussian_process import GaussianProcessRegressorfrom sklearn.gaussian_process.kernels import RBFfrom 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)