0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ベイズ最適化のお気持ち

0
Last updated at Posted at 2026-07-06

TLDL;

ベイズ最適化をどう数理的とコードをつなげるかをNumPyで作成をしたコードを掲示をします。

用語説明

  • ベイズ最適化では、ベイズ推論をしているNoise等のハイパラを最適化をする事を指します
  • ベイズ推論とは、不確実性を含んだ予測モデルです

具体的な例としてはアンケートの最適化を考えてみましょう。
アンケート項目をどうするかを考える際に、愚直に集めた解答から次の項目を考えてみると、例えば適当にアンケートを”っぽい”と解答をした人がいたとすると、この結果が明らかに悪影響が出ると思います。これをどうにかするのが、ベイズ推論の具体例です。

問題設定

では、問題設定を考えてみましょう!
noiseは$\sigma \sim \mathcal{N}(0, 0.01)$として、設定をしています。

$$
y = \sin(2\pi{x}) + \sigma \cdot \sqrt{0.01}
$$
これでNoiseを含んだデータになっています。
今回はこのNoiseを推定をして、どれくらいの範囲にデータがあるか等を見ていくのが目的です!

まずは観測データを生成をしてみましょう。

np.random.seed(42) # 再現性確保の為に、Seed値固定
train_x = np.linspace(0,1,100) # 0から1の間で100点選択。
# 真の関数:sin(2*pi*x) に少しのノイズを加える
# np.random.randnは標準正規分布(平均0, 分散1)の値から選択をするので、$\sqrt{0.01}$を掛ける事で、分散を0.1の分布を作成できます
train_y = np.sin(train_x * (2 * np.pi)) + np.random.randn(len(train_x)) * np.sqrt(0.01) 
true_y = np.sin(train_x * (2 * np.pi)) 
# 次の点を探すための探索空間(候補点200個)
test_x = np.linspace(0, 1, 200)

では可視化をしてみましょう!

# ベイズ最適化のMotivationはこのノイズの含んだデータでも予測をしたい。という事です。
plt.title('ノイズがどう作用をしているかを理解する。')
plt.plot(train_x, train_y, label="実験データのPlot")
plt.plot(train_x, true_y, label="本来の理想のデータ")
plt.legend()
plt.show()

出力結果が以下です。
以下の出力結果を見るとわかる通り、やはり、Noiseが入っているので、本来の関数からはギザギザでずれています。
このギザギザを推定をしてみようがモチベーションです。
9258ecc5-fd9f-4959-b984-4cba70406b5f.png

ベイズ最適化

カーネル関数

ベイズと切っても切り離せないのが、カーネル関数です。
これは非常に便利ですが、カーネルトリック等を理解をすると良いとは思いますが、恐らくこの記事では書ききれないので、後日記載をした記事のリンクを挿入をしておきます。
今回は、基本的なRBFカーネルを使います。
$$
RBF(x_1, x_2) = \sigma^2 exp(\frac{|x_1 - x_2|^2}{2l^2})
$$
上記で表せます。
コードでは以下と記載をします。

def rbf_kernel(x1, x2, output_scale, lengthsale):
    sq_dist = (x1[:, None], x2[None, :]) 
    return output_scale * np.exp(sq_dist **2 / 2 * lengthscale **2)

獲得関数

次に獲得関数について説明をします。
獲得関数のイメージとしては、$x_1$という点を選択をした際に、情報量が増えるように$x_2$を選択をする。その選択をする記述です。
今回はUCBを使用をしてみます。
$\mu$は平均、$\sigma$は分散をあらわし、$\beta$は、こちらもハイパラです。
$$
UCB(\x) = \mu + \beta \sigma
$$

コードでは

def upper_confidence_bound(mu, std, beta=2.0)
    return mu + beta * std

周辺尤度最大化

まず周辺尤度の式を記載をします。導出はLLMに聞いてみる事をお勧めします。
線形代数、統計学と色々な知識が必要で楽しいです。$\theta$はハイパラメータ(Noise, Lengthscale, outputscale)の事を指し示します。
$$
log_p(y | X, \theta) = -\frac{1}{2}y^T\Sigma^{-1}y - \frac{1}{2}log|\Sigma| - \frac{N}{2}log(2 \pi)
$$

で表せます。実装は以下です。

def negative_marginal_log_likelihood(paramas, x, y):
    lengthscale, outputscale, noise = params
    N = len(y)
    K_noise = rbf_kernel(x, y, outputscale, lengthscale) + noise * np.eye(N) # eyeは単位行列を作成します。
    try: 
        L = np.linalg.cholesky(K_noise) # 正定値行列であるかの保証を確認をしていないので、念の為tryでエラーを回避します。
    except np.linalg.LinAlgError:
        return 1e10 # 失敗をした際に大き目の値で更新をやり直させます。
    alpha = np.linalg.solve(L.T, np.linalg.solve(L, y))
    logdet = 2 * np.sum(np.log(np.diag)) # 行列式計算
    return 0.5 * y @ alpha + 0.5 * logdet + 0.5 * N * np.log(2 * np.pi)

事後分散・平均の算出

ベイズ推論では、事後分散、事後平均を求める事で、予測の不確実性を明らかにします。
まず以下で事後分散・平均が算出をされます。
事後平均

$$
\mu_{* } = K_{X_{*}X}(K_{XX} + \sigma_{n}^2 I)^{-1}y
$$

mu_s = K_sX @ np.linalg.inv(K_XX + noise * np.eye(N)) @ train_y

事後分散

$$
\Sigma_{* } = K_{x_{* }X}(K_{XX} + \sigma_{n}^2I)^{-1}K_{XX}
$$

sigma_s = K_ss - K_sX @ np.linalg.inv(K_XX + noise * np.eye(N)) @ K_sX.T

予測全体のコードを見ます。

def predict(test_x, train_x, train_y, lengthscale, output_scale, noise):
    N_train = len(train_x)
    K_XX = rbf_kernel(train_x, train_x, outputscale, lengthscale)
    K_sX = rbf_kernel(test_x, train_x, outputscale, lengthscale)
    K_ss = rbf_kernel(test_x, test_x, outputscale, lengthscale)
    L = np.linalg.solve(L.T, np.linalg.solve(L, train_y))
    mu_s = K_sX @ alpha
    v = np.linalg.solve(L, K_sX.T)
    sigma_s = K_ss - v.T @ v
    pred_var = np.sqrt(np.diag(sigma_s), 1e-8)
    return mu_s, np.sqrt(pred_var)

では最適化と予測、訓練を実施をします。
コードはAppendexにて掲示をしますが、ここでは先に訓練後の図を掲示をします。
以下を見ればある程度分かりますが、上では訓練をした後のガウス過程モデルで予測をした図です。
このように、青色で不確実性を予測をできており、観測点に関しても、今回は運よく、すべて不確実性で予測をされている部分に入っています。
上記の通り簡単にですが、説明をしました。
理論的な部分は恐らく今後勉強の為に書いていく予定ですが、Appendexにてコードをすべて掲示をしているので、暇があれば動かしてみてください。

c4ac0682-c27e-40c4-b7b7-0e021309629d.png

Appendex

今回使用をしたすべてのコード

# %% [markdown]
# ## import

# %%
import numpy as np
import matplotlib.pyplot as plt
plt.rcParams['font.family'] = "IPAGothic"

# %% [markdown]
# ## Generate data

# %%
# ==========================================
# 1. 初期データの設定(あえて少ない6点からスタート)
# ==========================================
np.random.seed(42)
train_x = np.linspace(0,1,10)
# 真の関数:sin(2*pi*x) に少しのノイズを加える
train_y = np.sin(train_x * (2 * np.pi)) + np.random.randn(len(train_x)) * np.sqrt(0.01)
true_y = np.sin(train_x * (2 * np.pi)) 
# 次の点を探すための探索空間(候補点200個)
test_x = np.linspace(0, 1, 200)

# %%
# ベイズ最適化のMotivationはこのノイズの含んだデータでも予測をしたい。という事です。
plt.title('ノイズがどう作用をしているかを理解する。')
plt.plot(train_x, train_y, label="実験データのPlot")
plt.plot(train_x, true_y, label="本来の理想のデータ")
plt.legend()
plt.show()

# %% [markdown]
# ## Gaussian Regression

# %%
# ==========================================
# 2. ガウス過程の基本関数の定義
# ==========================================
def rbf_kernel(x1, x2, lengthscale, output_scale):
    """RBF(高斯)カーネルの計算"""
    sq_dist = (x1[:, None] - x2[None, :]) ** 2 # (N, 1) - (1, N)で計算をしている。これって正しいの?->for ループを使わないで処理をしようとするという動機で合致をする。
    return output_scale * np.exp(-sq_dist / (2 * lengthscale ** 2))


def negative_marginal_log_likelihood(params, x, y):
    """paramsは生の値 (lengthscale, output_scale, noise)"""
    lengthscale, output_scale, noise = params
    N = len(y)
    # 共分散行列の左上
    K_noise = rbf_kernel(x, x, lengthscale, output_scale) + noise * np.eye(N) # $K_{XX} + noise * I$
    try:
        L = np.linalg.cholesky(K_noise)          # 逆行列でなくCholeskyを使う
    except np.linalg.LinAlgError:
        return 1e10
    # L^{-1} * yを算出
    alpha = np.linalg.solve(L.T, np.linalg.solve(L, y))
    
    logdet = 2 * np.sum(np.log(np.diag(L)))
    return 0.5 * y @ alpha + 0.5 * logdet + 0.5 * N * np.log(2 * np.pi)


def predict(test_x, train_x, train_y, lengthscale, output_scale, noise):
    # 行数を解答、ここで行数はBatch数をあらわしている。ただ、行列にし、うまく計算をする事で、本来\Order{N}のものが\Order{N^{1/2}}等になりうれしい。
    N_train = len(train_x) 
    K_XX = rbf_kernel(train_x, train_x, lengthscale, output_scale) + noise * np.eye(N_train)
    K_sX = rbf_kernel(test_x, train_x, lengthscale, output_scale)
    K_ss = rbf_kernel(test_x, test_x, lengthscale, output_scale)
    L = np.linalg.cholesky(K_XX)
    alpha = np.linalg.solve(L.T, np.linalg.solve(L, train_y))
    mu_s = K_sX @ alpha
    v = np.linalg.solve(L, K_sX.T)
    sigma_s = K_ss - v.T @ v
    pred_var = np.maximum(np.diag(sigma_s), 1e-8)
    return mu_s, np.sqrt(pred_var)

# %% [markdown]
# ## Aquisition Function

# %%
def upper_confidence_bound(mu, std, beta=2.0):
    return mu + beta * std

# %% [markdown]
# ## Marginal Log Likelihood

# %%
def numpy_minimize(train_x, train_y, init_params, epochs=10, lr=0.05, h=1e-5):
    """log空間で最適化。正値制約は exp() により自動的に満たされる。"""
    log_theta = np.log(np.array(init_params, dtype=float)) # hyperparameters
    for epoch in range(epochs):
        raw = np.exp(log_theta)
        current_loss = negative_marginal_log_likelihood(raw, train_x, train_y)
        gradients = np.zeros_like(log_theta)
        for i in range(len(log_theta)):
            pert = log_theta.copy(); pert[i] += h
            perturbed_loss = negative_marginal_log_likelihood(np.exp(pert), train_x, train_y)
            gradients[i] = (perturbed_loss - current_loss) / h
        log_theta -= lr * gradients
        if (epoch + 1) % 10 == 0 or epoch == 0:
            print(f"Epoch {epoch+1:3d}/{epochs} - Loss: {current_loss:.3f} | "
                  f"lengthscale: {raw[0]:.3f} | output_scale: {raw[1]:.3f} | noise: {raw[2]:.4f}")
    return np.exp(log_theta)

# %%
def numpy_minimize_with_bounds(train_x, train_y, init_params, epochs=10, lr=0.05, h=1e-5):
    log_theta = np.log(np.array(init_params), dtype=float)
    gradients = np.zeros_like(log_theta)
    
    # --- 1. 実数空間での Bounds を定義 ---
    # (例: lengthscale, output_scale, noise の探索範囲をそれぞれ [1e-3, 10.0] に制限)
    lower_bounds = np.array([0.3, 0.9, 1e-4])
    upper_bounds = np.array([0.4, 1.0, 1.0])
    # raw
    # --- 2. 対数空間用の Bounds に変換 ---
    log_lower = np.log(lower_bounds)
    log_upper = np.log(upper_bounds)
    for epoch in range(epochs):
        current_loss = negative_marginal_log_likelihood(np.exp(log_theta), train_x, train_y)        
        for i in range(len(log_theta)):
            pert = log_theta.copy()
            pert[i] += h 
            perturbed_loss = negative_marginal_log_likelihood(np.exp(pert), train_x, train_y)
            gradients[i] = (perturbed_loss - current_loss) / h
            
        # パラメータの更新
        log_theta -= lr * gradients
        # --- 3. 【ここを追加】Bounds の範囲内に丸め込む(投影) ---
        log_theta = np.clip(log_theta, log_lower, log_upper)
        # if (epoch + 1) % 30 == 0 or epoch == 0:
        #     print(f"Epoch {epoch+1:3d}/{epochs} - Loss: {current_loss:.3f} | "
        #           f"lengthscale: {raw[0]:.3f} | output_scale: {raw[1]:.3f} | noise: {raw[2]:.4f}")
    return np.exp(log_theta)

# %% [markdown]
# ## training & prediction & aqisition func calc

# %%
# ==========================================
# 4. メイン処理:モデル訓練 ➔ 予測 ➔ 獲得関数で次の一手を選択
# ==========================================

# ── Step 1: モデルの訓練 (MLLの最小化) ──
initial_params = [0.2, 1.0, 0.05]
bounds = [(1e-3, None), (1e-3, None), (1e-6, None)]
opt_params = numpy_minimize_with_bounds(train_x, train_y, initial_params, epochs=10, lr=0.01)
opt_lengthscale, opt_output_scale, opt_noise = opt_params

# ── Step 2: 予測の計算 ──
pred_mean, pred_std = predict(test_x, train_x, train_y, opt_lengthscale, opt_output_scale, opt_noise)

# ── Step 3: 獲得関数の計算と「次の点」の選定 ──
# 探索空間全体(test_x)に対してUCBスコアを計算
ucb_scores = upper_confidence_bound(pred_mean, pred_std, beta=2.5)
# print('UCB Score')
# print(ucb_scores)
# print('-'*40)

# UCBスコアが「最も高いインデックス」を取得 = 次に実験すべき場所
next_index = np.argmax(ucb_scores)
next_x = test_x[next_index]
next_y_score = ucb_scores[next_index]

print(f"候補となる次の実験点 x: {next_x:.4f} (UCBスコア: {next_y_score:.3f})")

# UCBも分布になるはず。
# plt.plot(ucb_scores)
# plt.show()

# %%
# ==========================================
# 5. 可視化(予測分布と獲得関数を上下に並べる)
# ==========================================
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(7, 6), sharex=True)

# 上段:ガウス過程の予測結果
ax1.plot(train_x, train_y, 'k*', markersize=10, label='Observed Data')
ax1.plot(test_x, np.sin(test_x * (2 * np.pi)), 'r--', alpha=0.5, label='True Function')
ax1.plot(test_x, pred_mean, 'b-', label='GP Mean')
ax1.fill_between(test_x, pred_mean - 2 * pred_std, pred_mean + 2 * pred_std, alpha=0.2, color='blue', label='Confidence ($2\sigma$)')
ax1.set_title("1. Gaussian Process Regression")
ax1.set_ylabel("y")
ax1.legend(loc="lower left")
ax1.grid(True)

# 下段:獲得関数(UCB)の形状と、次の一手

ax2.plot(test_x, ucb_scores, 'g-', label='UCB Score')
ax2.axvline(x=next_x, color='red', linestyle=':', linewidth=2, label=f'Next Sampling Point (x={next_x:.2f})')
ax2.plot(next_x, next_y_score, 'ro')  # 最大値の点に赤丸
ax2.set_title("2. Acquisition Function (UCB)")
ax2.set_xlabel("x")
ax2.set_ylabel("Acquisition Value")
ax2.legend(loc="lower left")
ax2.grid(True)

plt.tight_layout()
plt.show()

# %%
!pip list | grep numpy

# %%
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?