botorch入門1 モデル作成1

2年ぶりの更新となります。
ベイズ最適化を調査しており、pytorchと親和性の高そうなbotorchを利用を検討しています。
備忘録代わりに使い方をまとめていこうと思います。

冗長的な部分がありますが、あとでコピペできるようにという意図もあります。致命的なミスなどあればご指摘いただければと思います。

botorchはモデル作成だけでも多数実装されています。
何回かに分けて記事にします。まずは単目的モデルです。

## 1. SingleTaskGP

**概要**:
`SingleTaskGP` は BoTorch における最も基本的なガウス過程モデルであり、1つの出力をもつ連続値回帰問題に使用されます。

**主な用途**:

* 標準的なベイズ最適化の基礎モデル
* 比較的ノイズの少ないスカラー出力のモデリング

**特徴**:

* デフォルトでは RBF カーネル（Radial Basis Function）を使用
* `outcome_transform` を利用して標準化などの前処理が可能
* カーネルの変更（Matern, ARDなど）や `likelihood` のカスタマイズも容易

次のような関数からデータ点をサンプリングしてモデルの作成を行います。


```python
import numpy as np
import torch
import matplotlib.pyplot as plt
import japanize_matplotlib
```


```python
def blackbox(x):
    return np.sin(x*2)+np.cos(x/2-2)-x*0.1+np.random.rand(len(x),1)

x = np.arange(0,10,0.1).reshape(-1,1)
y = blackbox(x)

plt.plot(x, y);
plt.xlabel('x', size=20)
plt.ylabel('y', size=20)
```

![output_3_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2358656/61a949ba-d963-4fa6-95dc-012cf6f060b2.png)


```python
from botorch.fit import fit_gpytorch_mll
from botorch.models import SingleTaskGP
from botorch.models.transforms.outcome import Standardize
from botorch.optim import optimize_acqf
from botorch.sampling import SobolQMCNormalSampler
from botorch.utils.transforms import normalize, unnormalize
from gpytorch.mlls import ExactMarginalLogLikelihood
import gpytorch
```


```python
train_x = torch.rand(10,1, dtype=torch.double)*10
train_y = blackbox(train_x)
```


```python
bounds = torch.stack([
    torch.min(train_x, dim=0)[0],
    torch.max(train_x, dim=0)[0]
])

# 入力データを正規化
train_x_scaled = normalize(train_x, bounds=bounds)

# モデルの作成
model = SingleTaskGP(
    train_x_scaled,
    train_y,
    outcome_transform=Standardize(
        m=train_y.size(-1)
    )
)

# モデルの最尤推定を行う
mll = ExactMarginalLogLikelihood(
    model.likelihood,
    model
)
fit_gpytorch_mll(mll)
```

```python
x = torch.arange(0,10,0.1).reshape(-1,1)
x_norm = normalize(x, bounds)

pred = model.posterior(x_norm)
pred_mean = pred.mean.detach().numpy().ravel()
pred_std = pred.variance.sqrt().detach().numpy().ravel()

plt.plot(x.ravel(), pred_mean, color='orange')
plt.fill_between(x.ravel(), pred_mean+pred_std, pred_mean-pred_std, color='orange', alpha=.4)
plt.scatter(train_x, train_y, color='red')
plt.xlabel('x', size=20)
plt.ylabel('y', size=20)
```

![output_7_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2358656/45dce4bb-9d3d-44f8-a362-ca0f4737fa5b.png)

    

#### カーネル関数の変更


```python
from gpytorch.kernels import RBFKernel, MaternKernel, ScaleKernel
```


```python
bounds = torch.stack([
    torch.min(train_x, dim=0)[0],
    torch.max(train_x, dim=0)[0]
])

# 入力データを正規化
train_x_scaled = normalize(train_x, bounds=bounds)

# モデルの作成
model = SingleTaskGP(
    train_x_scaled,
    train_y,
    covar_module=ScaleKernel(RBFKernel()),
    outcome_transform=Standardize(
        m=train_y.size(-1)
    )
)

# モデルの最尤推定を行う
mll = ExactMarginalLogLikelihood(
    model.likelihood,
    model
)
fit_gpytorch_mll(mll)
```

```python
x = torch.arange(0,10,0.1).reshape(-1,1)
x_norm = normalize(x, bounds)

pred = model.posterior(x_norm)
pred_mean = pred.mean.detach().numpy().ravel()
pred_std = pred.variance.sqrt().detach().numpy().ravel()

plt.plot(x.ravel(), pred_mean, color='orange')
plt.fill_between(x.ravel(), pred_mean+pred_std, pred_mean-pred_std, color='orange', alpha=.4)
plt.scatter(train_x, train_y, color='red')
plt.xlabel('x', size=20)
plt.ylabel('y', size=20)
```

![output_11_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2358656/9922c005-1989-4da6-8430-385338e120bb.png)


```python
bounds = torch.stack([
    torch.min(train_x, dim=0)[0],
    torch.max(train_x, dim=0)[0]
])

# 入力データを正規化
train_x_scaled = normalize(train_x, bounds=bounds)

# モデルの作成
model = SingleTaskGP(
    train_x_scaled,
    train_y,
    covar_module=ScaleKernel(
        MaternKernel(nu=2.5)
    ),
    outcome_transform=Standardize(
        m=train_y.size(-1)
    )
)

# モデルの最尤推定を行う
mll = ExactMarginalLogLikelihood(
    model.likelihood,
    model
)
fit_gpytorch_mll(mll)
```



```python
x = torch.arange(0,10,0.1).reshape(-1,1)
x_norm = normalize(x, bounds)

pred = model.posterior(x_norm)
pred_mean = pred.mean.detach().numpy().ravel()
pred_std = pred.variance.sqrt().detach().numpy().ravel()

plt.plot(x.ravel(), pred_mean, color='orange')
plt.fill_between(x.ravel(), pred_mean+pred_std, pred_mean-pred_std, color='orange', alpha=.4)
plt.scatter(train_x, train_y, color='red')
plt.xlabel('x', size=20)
plt.ylabel('y', size=20)
```


![output_13_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2358656/fdeb7c08-bb5d-450f-9160-e67dcd4e3395.png)

    


ard_num_dims を指定すると、各入力次元に個別の長さスケールを割り当てる ARD (Automatic Relevance Determination) が有効になる（次元ごとの関連性を学習）


```python
bounds = torch.stack([
    torch.min(train_x, dim=0)[0],
    torch.max(train_x, dim=0)[0]
])

# 入力データを正規化
train_x_scaled = normalize(train_x, bounds=bounds)

# モデルの作成
model = SingleTaskGP(
    train_x_scaled,
    train_y,
    covar_module=ScaleKernel(
        RBFKernel(
            ard_num_dims=train_x.shape[-1]
        )
    ),
    outcome_transform=Standardize(
        m=train_y.size(-1)
    )
)

# モデルの最尤推定を行う
mll = ExactMarginalLogLikelihood(
    model.likelihood,
    model
)
fit_gpytorch_mll(mll)
```




```python
x = torch.arange(0,10,0.1).reshape(-1,1)
x_norm = normalize(x, bounds)

pred = model.posterior(x_norm)
pred_mean = pred.mean.detach().numpy().ravel()
pred_std = pred.variance.sqrt().detach().numpy().ravel()

plt.plot(x.ravel(), pred_mean, color='orange')
plt.fill_between(x.ravel(), pred_mean+pred_std, pred_mean-pred_std, color='orange', alpha=.4)
plt.scatter(train_x, train_y, color='red')
plt.xlabel('x', size=20)
plt.ylabel('y', size=20)
```


![output_15_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2358656/9778fbec-9fb0-4607-8924-2d51f16fd986.png)

    


#### 尤度関数の変更


```python
from gpytorch.likelihoods.gaussian_likelihood import GaussianLikelihood
from gpytorch.constraints import GreaterThan
from gpytorch.priors.torch_priors import LogNormalPrior
```


```python
bounds = torch.stack([
    torch.min(train_x, dim=0)[0],
    torch.max(train_x, dim=0)[0]
])

# 入力データを正規化
train_x_scaled = normalize(train_x, bounds=bounds)

noise_prior = LogNormalPrior(loc=-4.0, scale=1.0)
likelihood = GaussianLikelihood(
    noise_prior=noise_prior,
    batch_shape=train_x.shape[:-2],
    noise_constraint=GreaterThan(
        1e-1,
        transform=None,
        initial_value=noise_prior.mode,
    ),
)

# モデルの作成
model = SingleTaskGP(
    train_x_scaled,
    train_y,
    covar_module=ScaleKernel(
        RBFKernel(
            ard_num_dims=train_x.shape[-1]
        )
    ),
    likelihood=likelihood,
    outcome_transform=Standardize(
        m=train_y.size(-1)
    )
)

# モデルの最尤推定を行う
mll = ExactMarginalLogLikelihood(
    model.likelihood,
    model
)
fit_gpytorch_mll(mll)
```



```python
x = torch.arange(0,10,0.1).reshape(-1,1)
x_norm = normalize(x, bounds)

pred = model.posterior(x_norm)
pred_mean = pred.mean.detach().numpy().ravel()
pred_std = pred.variance.sqrt().detach().numpy().ravel()

plt.plot(x.ravel(), pred_mean, color='orange')
plt.fill_between(x.ravel(), pred_mean+pred_std, pred_mean-pred_std, color='orange', alpha=.4)
plt.scatter(train_x, train_y, color='red')
plt.xlabel('x', size=20)
plt.ylabel('y', size=20)
```



![output_18_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2358656/07cda7f1-ac23-4920-a168-f1e60bdf0567.png)



## 2. MixedSingleTaskGP

**概要**:
`MixedSingleTaskGP` は、連続値とカテゴリカル（離散）変数の両方を入力に持つ回帰モデルです。

**主な用途**:

* 材料設計や条件設定など、カテゴリ変数を含む最適化問題

**特徴**:

* `cat_dims` を指定することで、指定した次元に対してカテゴリカルカーネルを適用
* 内部的に AdditiveKernel と ProductKernel を組み合わせて連続・離散空間の依存性をモデリング
* モデルは入力空間ごとに挙動を変化させるため、カテゴリごとの関数形が大きく異なる場合に有効

次のような関数からデータ点をサンプリングしてモデルの作成を行います。
カテゴリ値によって関数の振る舞いが変わる場合を想定します。カテゴリ値は0,1,...のようにラベルで与えられているとします。


```python
def blackbox(x, noise=True):
    if type(x)==np.ndarray:
        return np.sin(x[:,[0]]*2)+np.cos(x[:,[0]]/2-2)+np.sign((x[:,[1]]==1).astype(int)-0.5)*x[:,[0]]*0.2 - 2*(x[:,[1]]==1).astype(int) + int(noise) * np.random.rand(len(x),1)
    else:
        return np.sin(x[:,[0]]*2)+np.cos(x[:,[0]]/2-2)+np.sign((x[:,[1]]==1).int()-0.5)*x[:,[0]]*0.2 - 2*(x[:,[1]]==1).int() + int(noise) * np.random.rand(len(x),1)

x = np.arange(0,10,0.1).reshape(-1,1)
y1=blackbox(np.concatenate([x,np.zeros_like(x)], axis=1), noise=False)
y2=blackbox(np.concatenate([x,np.ones_like(x)], axis=1), noise=False)

plt.plot(x[:,0], y1);
plt.plot(x[:,0], y2);
plt.xlabel('x', size=20)
plt.ylabel('y', size=20)
```



![output_20_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2358656/0bb83a22-1069-453c-9ed4-a58c9ba866d6.png)

    



```python
from botorch.models import MixedSingleTaskGP
```


```python
train_x = torch.cat([torch.rand(15,1, dtype=torch.double)*10, torch.tensor(np.random.choice([0,1], (15,1)))], dim=1)
train_y = blackbox(train_x)
```


```python
bounds = torch.stack([
    torch.min(train_x, dim=0)[0],
    torch.max(train_x, dim=0)[0]
])

# 入力データを正規化
train_x_scaled = normalize(train_x, bounds=bounds)

# モデルの作成
model = MixedSingleTaskGP(
    train_x_scaled,
    train_y,
    cat_dims=[1],
    outcome_transform=Standardize(
        m=train_y.size(-1)
    )
)

# モデルの最尤推定を行う
mll = ExactMarginalLogLikelihood(
    model.likelihood,
    model
)
fit_gpytorch_mll(mll)
```




```python
x = torch.arange(0,10,0.1).reshape(-1,1)
x1 = torch.cat([x, torch.zeros_like(x)], axis=1)
x2 = torch.cat([x, torch.ones_like(x)], axis=1)
x_norm1 = normalize(x1, bounds)
x_norm2 = normalize(x2, bounds)

pred1 = model.posterior(x_norm1)
pred1_mean = pred1.mean.detach().numpy().ravel()
pred1_std = pred1.variance.sqrt().detach().numpy().ravel()

pred2 = model.posterior(x_norm2)
pred2_mean = pred2.mean.detach().numpy().ravel()
pred2_std = pred2.variance.sqrt().detach().numpy().ravel()

plt.plot(x, pred1_mean, color='orange')
plt.fill_between(x.ravel(), pred1_mean+pred1_std, pred1_mean-pred1_std, color='orange', alpha=.4)

plt.plot(x, pred2_mean, color='green')
plt.fill_between(x.ravel(), pred2_mean+pred2_std, pred2_mean-pred2_std, color='green', alpha=.4)

plt.scatter(train_x[:,0], train_y, color='red')
plt.xlabel('x', size=20)
plt.ylabel('y', size=20)
```



![output_24_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2358656/1065ee99-e8f4-4a8b-afa8-82c55f6452b9.png)



尤度関数の変更も確認します。


```python
bounds = torch.stack([
    torch.min(train_x, dim=0)[0],
    torch.max(train_x, dim=0)[0]
])

# 入力データを正規化
train_x_scaled = normalize(train_x, bounds=bounds)

noise_prior = LogNormalPrior(loc=-4.0, scale=1.0)
likelihood = GaussianLikelihood(
    noise_prior=noise_prior,
    batch_shape=train_x.shape[:-2],
    noise_constraint=GreaterThan(
        1e-1,
        transform=None,
        initial_value=noise_prior.mode,
    ),
)

# モデルの作成
model = MixedSingleTaskGP(
    train_x_scaled,
    train_y,
    cat_dims=[1],
    likelihood=likelihood,
    outcome_transform=Standardize(
        m=train_y.size(-1)
    )
)

# モデルの最尤推定を行う
mll = ExactMarginalLogLikelihood(
    model.likelihood,
    model
)
fit_gpytorch_mll(mll)
```





```python
x = torch.arange(0,10,0.1).reshape(-1,1)
x1 = torch.cat([x, torch.zeros_like(x)], axis=1)
x2 = torch.cat([x, torch.ones_like(x)], axis=1)
x_norm1 = normalize(x1, bounds)
x_norm2 = normalize(x2, bounds)

pred1 = model.posterior(x_norm1)
pred1_mean = pred1.mean.detach().numpy().ravel()
pred1_std = pred1.variance.sqrt().detach().numpy().ravel()

pred2 = model.posterior(x_norm2)
pred2_mean = pred2.mean.detach().numpy().ravel()
pred2_std = pred2.variance.sqrt().detach().numpy().ravel()

plt.plot(x, pred1_mean, color='orange')
plt.fill_between(x.ravel(), pred1_mean+pred1_std, pred1_mean-pred1_std, color='orange', alpha=.4)

plt.plot(x, pred2_mean, color='green')
plt.fill_between(x.ravel(), pred2_mean+pred2_std, pred2_mean-pred2_std, color='green', alpha=.4)

plt.scatter(train_x[:,0], train_y, color='red')
plt.xlabel('x', size=20)
plt.ylabel('y', size=20)
```



![output_26_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2358656/08016dcc-a129-4951-95e6-27edb49e862b.png)

    


## 3. RobustRelevancePursuitSingleTaskGP

**概要**:
`RobustRelevancePursuitSingleTaskGP` は、入力の次元に対する選択性（relevance）と外れ値の影響を同時に学習可能なロバストな GP モデルです。

**主な用途**:

* 外れ値の混入が予想される実験データ
* 重要な変数の選択を含むモデリング

**特徴**:

* 自動的に入力変数の中で重要なものを選択（Relevance Pursuit）
* 外れ値（outlier）候補の数を指定して同時にモデル化
* 通常の GP と比較して過学習の抑制や予測の安定性が向上
* `prior_mean_of_support` は、変数選択に対してスパース性の事前情報を与える役割を果たす（値が大きいほど強くスパース化を促す）


次のような関数からデータ点をサンプリングしてモデルの作成を行います。
サンプリングしたデータに明示的に外れ値となる値を追加します。


```python
from botorch.models.relevance_pursuit import forward_relevance_pursuit
from botorch.models.robust_relevance_pursuit_model import RobustRelevancePursuitSingleTaskGP
from gpytorch.means import ZeroMean
from botorch.utils.constraints import NonTransformedInterval
from botorch.models.transforms import Normalize
```


```python
def blackbox(x):
    return np.sin(x*2)+np.cos(x/2-2)-x*0.1+np.random.rand(len(x),1)

x = np.arange(0,10,0.1).reshape(-1,1)
y = blackbox(x)

plt.plot(x, y);
plt.xlabel('x', size=20)
plt.ylabel('y', size=20)
```



![output_29_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2358656/201e3efa-8bbd-4088-9095-f0cfaa887362.png)

    



```python
train_x = torch.rand(30,1, dtype=torch.double)*10
train_y = blackbox(train_x)

train_x = torch.cat([train_x, torch.tensor([[2],[5]])])
train_y = torch.cat([train_y, torch.tensor([[-2],[3]])])
```


```python
bounds = torch.stack([
    torch.min(train_x, dim=0)[0],
    torch.max(train_x, dim=0)[0]
])

# 入力データを正規化
train_x_scaled = normalize(train_x, bounds=bounds)

# モデルの作成
model = SingleTaskGP(
    train_x_scaled,
    train_y,
    outcome_transform=Standardize(
        m=train_y.size(-1)
    )
)

# モデルの最尤推定を行う
mll = ExactMarginalLogLikelihood(
    model.likelihood,
    model
)
fit_gpytorch_mll(mll)
```







```python
model_robust = RobustRelevancePursuitSingleTaskGP(
    train_x_scaled,
    train_y,
    prior_mean_of_support=16.0,
)

# モデルの最尤推定を行う
mll = ExactMarginalLogLikelihood(
    model_robust.likelihood,
    model_robust
)
fit_gpytorch_mll(
    mll=mll, 
    numbers_of_outliers=[0, 1, 2, 3, 4],
    reset_parameters=False,
    relevance_pursuit_optimizer=forward_relevance_pursuit,
);
```


```python
min_lengthscale = 0.05
lengthscale_constraint = NonTransformedInterval(
    min_lengthscale, torch.inf, initial_value=0.2
)

covar_module = ScaleKernel(
    RBFKernel(ard_num_dims=train_x_scaled.shape[-1], lengthscale_constraint=lengthscale_constraint),
    outputscale_constraint=NonTransformedInterval(0.01, 10.0, initial_value=0.1),
)

model_robust2 = RobustRelevancePursuitSingleTaskGP(
    train_x_scaled,
    train_y,
    input_transform=Normalize(d=train_x_scaled.shape[-1]),
    cache_model_trace=True,  # necessary to plot the model trace after optimization
    mean_module=ZeroMean(),
    covar_module=covar_module,
)

# モデルの最尤推定を行う
mll2 = ExactMarginalLogLikelihood(
    model_robust2.likelihood,
    model_robust2
)
fit_gpytorch_mll(
    mll=mll2, 
    numbers_of_outliers=[0, 1, 2, 3, 4],
    reset_parameters=False,
    relevance_pursuit_optimizer=forward_relevance_pursuit,
);
```


```python
x = torch.arange(0,10,0.1).reshape(-1,1)
x_norm = normalize(x, bounds)

pred1 = model.posterior(x_norm)
pred1_mean = pred1.mean.detach().numpy().ravel()
pred1_std = pred1.variance.sqrt().detach().numpy().ravel()

pred2 = model_robust.posterior(x_norm)
pred2_mean = pred2.mean.detach().numpy().ravel()
pred2_std = pred2.variance.sqrt().detach().numpy().ravel()

pred3 = model_robust2.posterior(x_norm)
pred3_mean = pred3.mean.detach().numpy().ravel()
pred3_std = pred3.variance.sqrt().detach().numpy().ravel()

plt.plot(x.ravel(), pred1_mean, color='orange')
plt.fill_between(x.ravel(), pred1_mean+pred1_std, pred1_mean-pred1_std, color='orange', alpha=.4)

plt.plot(x.ravel(), pred2_mean, color='green')
plt.fill_between(x.ravel(), pred2_mean+pred2_std, pred2_mean-pred2_std, color='green', alpha=.4)

plt.plot(x.ravel(), pred3_mean, color='blue')
plt.fill_between(x.ravel(), pred3_mean+pred3_std, pred3_mean-pred3_std, color='blue', alpha=.4)

plt.scatter(train_x, train_y, color='red')
plt.xlabel('x', size=20)
plt.ylabel('y', size=20)
```



![output_34_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2358656/5bd6b8a9-e27c-4153-9dda-18853af6c38b.png)

    


## 4. SaasFullyBayesianSingleTaskGP

**概要**:
`SaasFullyBayesianSingleTaskGP` は、MCMC（NUTSアルゴリズム）による事後分布推論を用いた完全ベイズモデルです。"SaaS"は "Sparse Axis-Aligned Subspace" を意味し、関連性の低い入力変数に対して自然に抑制をかける機構を持ちます。

**主な用途**:

* 高次元空間でのベイズ最適化
* モデル不確実性をより厳密に考慮したい場合

**特徴**:

* NUTS によるサンプルベースの推論（`fit_fully_bayesian_model_nuts`）
* 高次元問題での変数選択と精度の両立
* 実行時間は長くなるが不確実性の扱いに優れる

次のような関数からデータ点をサンプリングしてモデルの作成を行います。
20次元のデータを用意しますが、目的変数は4つの説明変数のみに依存しているものを想定します。


```python
from botorch.models import SaasFullyBayesianSingleTaskGP
from botorch.fit import fit_fully_bayesian_model_nuts
```


```python
f_hd = lambda y: 3*torch.sin(y[:,[4]]*6) + 2*torch.sin(y[:,[9]]*2+torch.pi/6) - torch.sin(y[:,[14]]*2+torch.pi/1.5) + 2.5*torch.sin(y[:,[19]]*4) + 0.2*torch.randn(len(y),1)

train_X = torch.rand(20,20, dtype=torch.double)
train_Y = f_hd(train_X)
```


```python
model = SaasFullyBayesianSingleTaskGP(
    train_X,
    train_Y,
    outcome_transform=Standardize(m=train_Y.size(-1))
)

fit_fully_bayesian_model_nuts(
    model,
    warmup_steps=128,
    num_samples=64,
    thinning=16,
    disable_progbar=False
)
```

    Sample: 100%|█████████████████████████████████████████| 192/192 [01:53,  1.69it/s, step size=4.20e-01, acc. prob=0.897]
    

重要度として各説明変数に対するパラメータを確認します。


```python
def plot_lengthscale_importance(model, feature_names=None):
    """
    SaasFullyBayesianSingleTaskGP の lengthscale を解析・可視化する。

    Args:
        model: 学習済みの SaasFullyBayesianSingleTaskGP モデル
        feature_names: 特徴量名のリスト（Noneならインデックス表示）

    Returns:
        None（プロット表示）
    """
    # MCMCサンプルからlengthscaleを取得
    lengthscale_samples = model.covar_module.base_kernel.lengthscale.detach().cpu().numpy()
    # shape: (num_samples, 1, input_dim) → squeezeして (num_samples, input_dim)
    lengthscale_samples = lengthscale_samples.squeeze(1)

    # 各特徴量ごとの統計量
    mean_lengthscales = np.mean(lengthscale_samples, axis=0)
    perc_5 = np.percentile(lengthscale_samples, 5, axis=0)
    perc_95 = np.percentile(lengthscale_samples, 95, axis=0)

    # 重要度順に並べ替え（lengthscaleが短い方が重要）
    sorted_idx = np.argsort(mean_lengthscales)
    sorted_means = mean_lengthscales[sorted_idx]
    sorted_err_low = sorted_means - perc_5[sorted_idx]
    sorted_err_high = perc_95[sorted_idx] - sorted_means

    # 特徴量名
    if feature_names is None:
        feature_names = [f"X{i}" for i in range(lengthscale_samples.shape[1])]
    sorted_feature_names = np.array(feature_names)[sorted_idx]

    # プロット
    plt.figure(figsize=(10, 6))
    plt.errorbar(
        range(len(sorted_feature_names)),
        sorted_means,
        yerr=[sorted_err_low, sorted_err_high],
        fmt='o', ecolor='gray', capsize=5, markersize=5
    )
    plt.xticks(range(len(sorted_feature_names)), sorted_feature_names, rotation=45, ha='right')
    plt.xlabel("Features")
    plt.ylabel("Mean Lengthscale (lower = more important)")
    plt.title("Feature Importance based on Lengthscale (SaasFullyBayesianSingleTaskGP)")
    plt.grid(True)
    plt.tight_layout()
    plt.show()

plot_lengthscale_importance(model)
```

![output_39_0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2358656/07087369-0d39-44a6-b3a8-5d195e7dfbe4.png)

今回は以上となります。
次回は多目的モデルについて説明したいと思います。

https://qiita.com/tanaka_benkyo/items/16ef82cc65c12e81b2a7

ご意見あればコメントお願いします