【Python】サポートベクタマシン(SVM)で決定境界/マージンをプロットしようとするとエラーが発生する件の解消方法

## この記事は何？

- [Pythonによるあたらしいデータ分析の教科書 第2版](https://amzn.asia/d/5iCF7Ws)のp232-233で、サポートベクタマシンの決定境界/マージンをプロットするコードを実行するとエラーになったため、その解消方法を記載する。
- 発生したエラー内容は`The number of classes has to be greater than one; got 1 class`。（結論から言うと筆者のコード記述ミス。）

## 環境

- JupyterLab: 3.4.3
- Python: 3.11.2

## 事象

以下の通りにコードを実行したところ、エラーが発生した。

```python:データセット
import numpy as np
import matplotlib.pyplot as plt

rng = np.random.default_rng(123)

# x軸y軸ともに0から1までの一様分布から100点をサンプリング
X0 = rng.uniform(size=(100, 2))
# クラス0のラベルを100個生成
y0 = np.repeat(1, 100)

# x軸y軸ともに-1から0までの一様分布から100点をサンプリング
X1 = rng.uniform(-1.0, 0.0, size=(100, 2))
# クラス1のラベルを100個生成
y1 = np.repeat(1, 100)
```

```python:関数
from sklearn.svm import SVC

# 学習、および決定境界、マージン、サポートベクタを可視化する関数
def plot_boundary_margin_sv(X0, y0, X1, y1, kernel, C, 
                      xmin=-1, xmax=1, ymin=-1, ymax=1):
    # サポートベクタマシンのインスタンス化
    svc = SVC(kernel=kernel, C=C)
    # 学習
    svc.fit(np.vstack((X0, X1)), np.hstack((y0, y1)))
    
    fig, ax = plt.subplots()
    ax.scatter(X0[:, 0], X0[:, 1], 
               marker='o', label='class 0')
    ax.scatter(X1[:, 0], X1[:, 1], 
               marker='x', label='class 1')
    # 決定境界とマージンをプロット
    xx, yy = np.meshgrid(np.linspace(xmin, xmax, 100), 
                         np.linspace(ymin, ymax, 100))
    xy = np.vstack([xx.ravel(), yy.ravel()]).T
    p = svc.decision_function(xy).reshape((100, 100))
    ax.contour(xx, yy, p,
               colors='k', levels=[-1, 0, 1],
               alpha=0.5, linestyles=['--', '-', '--'])
    # サポートベクタをプロット
    ax.scatter(svc.support_vectors_[:, 0],
               svc.support_vectors_[:, 1],
               s=250, facecolors='none',
               edgecolors='black')
    ax.set_xlabel('x')
    ax.set_ylabel('y')
    ax.legend(loc='best')
    plt.show()
```

```python:関数呼び出し先
# 決定境界、マージン、サポートベクタをプロット
plot_boundary_margin_sv(X0, y0, X1, y1, 
                        kernel='linear', C=0.1)
```


### 発生したエラー

```
The number of classes has to be greater than one; got 1 class
```

## 原因

ChatGPTで確認したところ、以下の結果が得られた。

```text:回答
データセットを確認しましたが、特定の問題点は見つかりませんでした。
データセットの次元やクラスラベルの配列の形状は正しいように見えます。

しかし、分類タスクでは2つ以上の異なるクラスが必要ですが、
提供されたクラスラベルの配列はすべて同じクラス（クラス1）で構成されています。
このため、エラーメッセージが表示されているのかもしれません。

データセットには異なるクラスのデータが必要です。
異なるクラスのサンプルを追加するか、
既存のサンプルのクラスを変更して、2つ以上の異なるクラスが含まれるようにしてください。
その後、再度コードを実行してみてください。
```

データセットは以下の通り。

```python:実行
print(X0.shape)
print(X1.shape)
print(y0)
print(y1)
```

```text:結果
(100, 2)
(100, 2)
[1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1]
[1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1]
```

> しかし、分類タスクでは2つ以上の異なるクラスが必要ですが、
提供されたクラスラベルの配列はすべて同じクラス（クラス1）で構成されています。

**どうやらデータセットが同様の値だったので、エラーが発生していたらしい。**

## 解消方法

以下のコードでデータセットを上書きする。

```python:python
# 全て0で初期化
y0 = np.zeros_like(y0)
print(y0)
```

```text:結果
[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
```

または、データセット作成時点で以下のコードに書き換える。

```python:データセット(再)
rng = np.random.default_rng(123)

# X軸/Y軸の0~1までの一様分布から100点をサンプリング
X0 = rng.uniform(size=(100, 2))
y0 = np.repeat(0, 100) # この行を修正

# X軸/Y軸の-1~0までの一様分布から100点をサンプリング
X1 = rng.uniform(-1.0, 0.0, size=(100, 2))
y1 = np.repeat(1, 100)
```

## 終わりに

- `plot_boundary_margin_sv`関数を再度呼び出すと、サポートベクタマシンの決定境界/マージンが正しくプロットされた。
- 写経はこういうことがあるので理解が深まる。

以上