PyLearnMLCR 02: サポートベクターマシン

サポートベクターマシンも、決定木と同様に、分類問題・回帰問題を解くことが可能なモデルです。過学習を抑制するためのパラメータが導入されているので、大変人気が高い人工知能の構築手法となります。一般に、決定木よりも性能が高いモデルを構築しやすく、ニューラルネットワークよりも安定した性能を出しやすい傾向があります。ただし、決定木のように、なぜそのクラスと判定されるのか、ということはわかりにくいです。

分類問題

ダミーデータの生成

決定木での解説と同じデータなので、説明は省略します。

In [1]:
import numpy as np
np.random.seed(1) # 擬似乱数シード: 毎回同じ乱数を出す

# Class1: 成績上位群
mu = [7, 7]
sigma = [[0.5, 0.05], [0.05, 0.5]]
Dat01 = np.random.multivariate_normal(mu, sigma, 100)
Label01=[]
for i in range(0, len(Dat01)):
    Label01.append("Good")

# Class 2: 成績中位群
mu = [5.5, 4]
sigma = [[0.8, -0.5], [-0.5, 0.8]]
Dat02 = np.random.multivariate_normal(mu, sigma, 100)
Label02=[]
for i in range(0, len(Dat02)):
    Label02.append("Middle")

# Class 3: 成績下位群
mu = [2, 5]
sigma = [[0.3, 0.0], [0.0, 3]]
Dat03 = np.random.multivariate_normal(mu, sigma, 100)
Label03=[]
for i in range(0, len(Dat01)):
    Label03.append("Bad")

散布図によるデータ確認

続いて、散布図でデータを可視化してみます。これも決定木と同じなので、説明は省略します。

In [3]:
import matplotlib.pyplot as plt

x = [Dat01[:,0], Dat02[:,0], Dat03[:,0]]
y = [Dat01[:,1], Dat02[:,1], Dat03[:,1]]

plt.figure(figsize=(5, 4)) # figureの縦横の大きさ

# Goodの散布図
plt.scatter(Dat01[:,0], Dat01[:,1], s=50, c='blue', marker='s', 
            alpha=0.8, linewidths=0.5, edgecolors='black')

# Middleの散布図
plt.scatter(Dat02[:,0], Dat02[:,1], s=50, c='orange', marker='s', 
            alpha=0.8, linewidths=0.5, edgecolors='black')

# Badの散布図
plt.scatter(Dat03[:,0], Dat03[:,1], s=50, c='red', marker='s', 
            alpha=0.8, linewidths=0.5, edgecolors='black')

plt.title("Relationship: X1, X2 and Score")  # タイトル
plt.xlabel("X1: Studying Time") # 軸名
plt.ylabel("X2: Understanding") #軸名
plt.grid(True)      #グリッド線(True:引く、False:引かない)
plt.xlim(0, 10)  # 横軸最小最大
plt.ylim(0, 10)  # 縦軸最小最大

plt.legend(["Good", "Middle", "Bad"], loc="upper right") # 凡例

plt.show() # グラフの表示

教師データセットの作成

これも決定木のときと同じです。Xの1列目には勉強時間、2列目には理解傾向が格納されています。そして、YにはGood, Middle, Badのラベルが導入されています。それぞれ100人分なので、合計300人分の勉強の仕方と学力が入っているデータセットになります。

In [4]:
X=np.concatenate([Dat01, Dat02, Dat03]) # 問題
Y=np.concatenate([Label01, Label02, Label03]) # 正解ラベル

SVMの学習

それでは、XからYを推定するモデルを構築してみます。SVMが有する主要なパラメータは以下の4点です。

  • kernel: 'linear' or 'rbf'(他にもありますがとりあえずこの2つで)

    • 大雑把に言うと、識別境界を直線にするか(linear: 線形カーネル)、曲線にするか(rbf: 非線形カーネル)を表します。一般に、線形カーネルは複雑な識別境界を生成することができないというデメリットがありますが、外れ値を学習しようとしなくなる、すなわち過学習が生じにくく安定というメリットがあります。対照的に、非線形カーネルは複雑な識別境界を獲得できるというメリットがありますが、外れ値も学習することができてしまうので、過学習が生じやすくなります。非線形カーネルを使用する場合は、後述するCとgammaのチューニングが大変重要になります。
  • C: 正の実数

    • 教師データの誤差をどの程度許容するかを意味するパラメータです。小さな値を入れると、教師データの誤答をあまり気にしなくなるので、性能の良い識別境界が得られないことがあります。一方、大きな値にしすぎると、過学習の原因になるので、性能が下がります。トライアンドエラーでいい値を探しましょう。
  • gamma: 正の実数

    • カーネルにrbfを指定した場合に使用するパラメータです(linearのときは使用しません)。大雑把に言うと、教師データの一つ一つのデータが識別境界に与える影響となります。大きな値に設定すると、個々のデータが識別境界の形状に大きな影響を与えてしまうので、過学習が起きやすくなります。とはいえ、小さすぎるのもあまりよくありません。トライアンドエラーでいい値を探しましょう。
  • class_weight: None or 'balanced'

    • 教師データのクラスラベルに偏りがあるとき、偏りが大きいクラスと回答した方が正答率が上がってしまいます。このように、クラスラベルに偏りが生じていると、学習が不適切なものになる場合があります。これを解消するために、クラスラベルに重みをつけることができます。class_weightはこのためのパラメータです。もし、クラスラベル間に大きな偏りがあると感じる場合は、'balanced'を指定してください。重み付けをしない場合は、何も記載しないか、Noneを記載します。

線形SVM

線形カーネルを使用したSVMを線形SVMと言います。まずはこれを構築してみます。一つは誤差を許容するモデル、もう一つは誤差を許容しないモデルです。

In [5]:
from sklearn import svm

# モデル構造の決定
SVMModel_Lin01 = svm.SVC(kernel='linear', C=1)
SVMModel_Lin02 = svm.SVC(kernel='linear', C=10000)

# 学習
SVMModel_Lin01 = SVMModel_Lin01.fit(X,Y)
SVMModel_Lin02 = SVMModel_Lin02.fit(X,Y)

続いて、学習したモデルで新しいデータを推定してみます。勉強時間を5、理解傾向を6としてみます。

In [6]:
InputDat = [[5, 6]]

# Cが低いモデル
OutputDat = SVMModel_Lin01.predict(InputDat)
OutputScore = SVMModel_Lin01.decision_function(InputDat)
print("Cが低いモデル:", OutputDat, OutputScore)

# Cが高いモデル
OutputDat = SVMModel_Lin02.predict(InputDat)
OutputScore = SVMModel_Lin02.decision_function(InputDat)
print("Cが高いモデル:", OutputDat, OutputScore)
Cが低いモデル: ['Middle'] [[-0.5         1.03016848  2.46983152]]
Cが高いモデル: ['Good'] [[-0.5         2.03505459  1.46494541]]

predictは判定クラスをそのまま返してきます。decision_functionは実数値の出力です。クラスはソートされているので、Bad, Good, Middleに並び替えられており、それらの確信度が出力されるわけです。Cが低いモデルは、Middleクラスの確信度が高いこと、Cが高いモデルはBadクラスの確信度が高いことがわかります(実際に判定クラスはそうなっていますね)。異なる出力になりました。モデルが違うので、推定結果が違う、ということですね。どういう識別境界が得られているのか、可視化してみます。コードの説明は、決定木と同じなので省略します。

In [7]:
# メッシュデータ生成
Xmin, Ymin, Xmax, Ymax = 0, 0, 10, 10 # 空間の最小最大値
resolution = 0.1 # 細かさ
x_mesh, y_mesh = np.meshgrid(np.arange(Xmin, Xmax, resolution),
                             np.arange(Ymin, Ymax, resolution))
MeshDat = np.array([x_mesh.ravel(), y_mesh.ravel()]).T

# メッシュデータの推定
z_lin01 = SVMModel_Lin01.predict(MeshDat) # モデル1
z_lin02 = SVMModel_Lin02.predict(MeshDat) # モデル2

# データ整形
z_lin01 = np.reshape(z_lin01, (len(x_mesh), len(y_mesh))) 
z_lin02 = np.reshape(z_lin02, (len(x_mesh), len(y_mesh))) 

# 可視化
import matplotlib.pyplot as plt
fig = plt.figure(figsize=(12, 4))

# *** lin01 ***
plt.subplot(1,2,1)

# SVM出力
plt.scatter(x_mesh[z_lin01=='Bad'], y_mesh[z_lin01=='Bad'], s=5, alpha=0.3, c='red')
plt.scatter(x_mesh[z_lin01=='Middle'], y_mesh[z_lin01=='Middle'], s=5, alpha=0.3, c='yellow')
plt.scatter(x_mesh[z_lin01=='Good'], y_mesh[z_lin01=='Good'], s=5, alpha=0.3, c='blue')
# 教師データ(Bad, Middle, Good)
plt.scatter(Dat03[:,0], Dat03[:,1], s=50, c='red', marker='s', 
            alpha=0.8, linewidths=0.5, edgecolors='black')
plt.scatter(Dat02[:,0], Dat02[:,1], s=50, c='orange', marker='s', 
            alpha=0.8, linewidths=0.5, edgecolors='black')
plt.scatter(Dat01[:,0], Dat01[:,1], s=50, c='blue', marker='s', 
            alpha=0.8, linewidths=0.5, edgecolors='black')
plt.title("Feature space by SVM Lin 01")
plt.xlabel("X1: Studying Time")
plt.ylabel("X2: Understanding")
plt.grid(True)
plt.xlim(0, 10)
plt.ylim(0, 10)

# *** lin02 ***
plt.subplot(1,2,2)
# SVM出力
plt.scatter(x_mesh[z_lin02=='Bad'], y_mesh[z_lin02=='Bad'], s=5, alpha=0.3, c='red')
plt.scatter(x_mesh[z_lin02=='Middle'], y_mesh[z_lin02=='Middle'], s=5, alpha=0.3, c='yellow')
plt.scatter(x_mesh[z_lin02=='Good'], y_mesh[z_lin02=='Good'], s=5, alpha=0.3, c='blue')
# 教師データ(Bad, Middle, Good)
plt.scatter(Dat03[:,0], Dat03[:,1], s=50, c='red', marker='s', 
            alpha=0.8, linewidths=0.5, edgecolors='black')
plt.scatter(Dat02[:,0], Dat02[:,1], s=50, c='orange', marker='s', 
            alpha=0.8, linewidths=0.5, edgecolors='black')
plt.scatter(Dat01[:,0], Dat01[:,1], s=50, c='blue', marker='s', 
            alpha=0.8, linewidths=0.5, edgecolors='black')
plt.title("Feature space by SVM Lin 02")
plt.xlabel("X1: Studying Time")
plt.ylabel("X2: Understanding")
plt.grid(True)
plt.xlim(0, 10)
plt.ylim(0, 10)
plt.legend(["Bad", "Middle", "Good",
            "Bad (TrainData)", "Middle (TrainData)", "Good (TrainData)"], 
           loc="upper right", bbox_to_anchor=(1.5, 1))
Out[7]:
<matplotlib.legend.Legend at 0x112b7b9b0>

左はCが低いモデルなので、教師データの誤差がある程度許容されています。右はCが高いモデルなので、教師データの誤差が許されないモデルです。また、いずれも線形カーネルを使用しているので、識別境界が直線です。ほとんど似ていますが、X1=4, X2=2付近のBadを分類しようとしている、そうではない、という部分に違いがありますね。一般的に、Cを高めると過学習を起こしますが、線形カーネルは過学習を起こしにくいという特性があります。そのため、Cが高い場合でも、過学習が起きていない空間に見えます。

非線形SVM

続いてrbfカーネルを使用した、識別境界を曲げることのできるモデルを構築してみます。このようなSVMを非線形SVMと言います。rbfカーネルを用いた場合、Cとgammaを設定する必要があります。この影響を調べるため、

  • モデル01: (C, gamma) = (低, 低)
  • モデル02: (C, gamma) = (低, 高)
  • モデル03: (C, gamma) = (高, 低)
  • モデル04: (C, gamma) = (高, 高)

の4つの推定モデルを構築してみます。

In [8]:
# モデル構造の決定
SVMModel_RBF01 = svm.SVC(kernel='rbf', C=0.1, gamma=0.1)
SVMModel_RBF02 = svm.SVC(kernel='rbf', C=0.1, gamma=10)
SVMModel_RBF03 = svm.SVC(kernel='rbf', C=10, gamma=0.1)
SVMModel_RBF04 = svm.SVC(kernel='rbf', C=10, gamma=10)

# 学習
SVMModel_RBF01 = SVMModel_RBF01.fit(X,Y)
SVMModel_RBF02 = SVMModel_RBF02.fit(X,Y)
SVMModel_RBF03 = SVMModel_RBF03.fit(X,Y)
SVMModel_RBF04 = SVMModel_RBF04.fit(X,Y)

続いて空間を可視化してみます。

In [9]:
# メッシュデータ生成
Xmin, Ymin, Xmax, Ymax = 0, 0, 10, 10 # 空間の最小最大値
resolution = 0.1 # 細かさ
x_mesh, y_mesh = np.meshgrid(np.arange(Xmin, Xmax, resolution),
                             np.arange(Ymin, Ymax, resolution))
MeshDat = np.array([x_mesh.ravel(), y_mesh.ravel()]).T

# メッシュデータの推定
z_RBF01 = SVMModel_RBF01.predict(MeshDat) # モデル1
z_RBF02 = SVMModel_RBF02.predict(MeshDat) # モデル2
z_RBF03 = SVMModel_RBF03.predict(MeshDat) # モデル3
z_RBF04 = SVMModel_RBF04.predict(MeshDat) # モデル4

# データ整形
z_RBF01 = np.reshape(z_RBF01, (len(x_mesh), len(y_mesh))) 
z_RBF02 = np.reshape(z_RBF02, (len(x_mesh), len(y_mesh))) 
z_RBF03 = np.reshape(z_RBF03, (len(x_mesh), len(y_mesh))) 
z_RBF04 = np.reshape(z_RBF04, (len(x_mesh), len(y_mesh))) 

# 可視化
import matplotlib.pyplot as plt
fig2 = plt.figure(figsize=(12, 12))

# *** RBF01 ***
plt.subplot(2,2,1)

# SVM出力
plt.scatter(x_mesh[z_RBF01=='Bad'], y_mesh[z_RBF01=='Bad'], s=5, alpha=0.3, c='red')
plt.scatter(x_mesh[z_RBF01=='Middle'], y_mesh[z_RBF01=='Middle'], s=5, alpha=0.3, c='yellow')
plt.scatter(x_mesh[z_RBF01=='Good'], y_mesh[z_RBF01=='Good'], s=5, alpha=0.3, c='blue')
# 教師データ(Bad, Middle, Good)
plt.scatter(Dat03[:,0], Dat03[:,1], s=50, c='red', marker='s', 
            alpha=0.8, linewidths=0.5, edgecolors='black')
plt.scatter(Dat02[:,0], Dat02[:,1], s=50, c='orange', marker='s', 
            alpha=0.8, linewidths=0.5, edgecolors='black')
plt.scatter(Dat01[:,0], Dat01[:,1], s=50, c='blue', marker='s', 
            alpha=0.8, linewidths=0.5, edgecolors='black')
plt.title("C=Low, gamma=Low")
plt.xlabel("X1: Studying Time")
plt.ylabel("X2: Understanding")
plt.grid(True)
plt.xlim(0, 10)
plt.ylim(0, 10)

# *** RBF02 ***
plt.subplot(2,2,2)

# SVM出力
plt.scatter(x_mesh[z_RBF02=='Bad'], y_mesh[z_RBF02=='Bad'], s=5, alpha=0.3, c='red')
plt.scatter(x_mesh[z_RBF02=='Middle'], y_mesh[z_RBF02=='Middle'], s=5, alpha=0.3, c='yellow')
plt.scatter(x_mesh[z_RBF02=='Good'], y_mesh[z_RBF02=='Good'], s=5, alpha=0.3, c='blue')
# 教師データ(Bad, Middle, Good)
plt.scatter(Dat03[:,0], Dat03[:,1], s=50, c='red', marker='s', 
            alpha=0.8, linewidths=0.5, edgecolors='black')
plt.scatter(Dat02[:,0], Dat02[:,1], s=50, c='orange', marker='s', 
            alpha=0.8, linewidths=0.5, edgecolors='black')
plt.scatter(Dat01[:,0], Dat01[:,1], s=50, c='blue', marker='s', 
            alpha=0.8, linewidths=0.5, edgecolors='black')
plt.title("C=Low, gamma=High")
plt.xlabel("X1: Studying Time")
plt.ylabel("X2: Understanding")
plt.grid(True)
plt.xlim(0, 10)
plt.ylim(0, 10)

# *** RBF03 ***
plt.subplot(2,2,3)

# SVM出力
plt.scatter(x_mesh[z_RBF03=='Bad'], y_mesh[z_RBF03=='Bad'], s=5, alpha=0.3, c='red')
plt.scatter(x_mesh[z_RBF03=='Middle'], y_mesh[z_RBF03=='Middle'], s=5, alpha=0.3, c='yellow')
plt.scatter(x_mesh[z_RBF03=='Good'], y_mesh[z_RBF03=='Good'], s=5, alpha=0.3, c='blue')
# 教師データ(Bad, Middle, Good)
plt.scatter(Dat03[:,0], Dat03[:,1], s=50, c='red', marker='s', 
            alpha=0.8, linewidths=0.5, edgecolors='black')
plt.scatter(Dat02[:,0], Dat02[:,1], s=50, c='orange', marker='s', 
            alpha=0.8, linewidths=0.5, edgecolors='black')
plt.scatter(Dat01[:,0], Dat01[:,1], s=50, c='blue', marker='s', 
            alpha=0.8, linewidths=0.5, edgecolors='black')
plt.title("C=High, gamma=Low")
plt.xlabel("X1: Studying Time")
plt.ylabel("X2: Understanding")
plt.grid(True)
plt.xlim(0, 10)
plt.ylim(0, 10)

# *** RBF04 ***
plt.subplot(2,2,4)

# SVM出力
plt.scatter(x_mesh[z_RBF04=='Bad'], y_mesh[z_RBF04=='Bad'], s=5, alpha=0.3, c='red')
plt.scatter(x_mesh[z_RBF04=='Middle'], y_mesh[z_RBF04=='Middle'], s=5, alpha=0.3, c='yellow')
plt.scatter(x_mesh[z_RBF04=='Good'], y_mesh[z_RBF04=='Good'], s=5, alpha=0.3, c='blue')
# 教師データ(Bad, Middle, Good)
plt.scatter(Dat03[:,0], Dat03[:,1], s=50, c='red', marker='s', 
            alpha=0.8, linewidths=0.5, edgecolors='black')
plt.scatter(Dat02[:,0], Dat02[:,1], s=50, c='orange', marker='s', 
            alpha=0.8, linewidths=0.5, edgecolors='black')
plt.scatter(Dat01[:,0], Dat01[:,1], s=50, c='blue', marker='s', 
            alpha=0.8, linewidths=0.5, edgecolors='black')
plt.title("C=High, gamma=High")
plt.xlabel("X1: Studying Time")
plt.ylabel("X2: Understanding")
plt.grid(True)
plt.xlim(0, 10)
plt.ylim(0, 10)
Out[9]:
(0, 10)

ちょっとわかりにくいので、教師データを載せないでみます。

In [11]:
fig2 = plt.figure(figsize=(12, 12))

# *** RBF01 ***
plt.subplot(2,2,1)

# SVM出力
plt.scatter(x_mesh[z_RBF01=='Bad'], y_mesh[z_RBF01=='Bad'], s=5, alpha=0.3, c='red')
plt.scatter(x_mesh[z_RBF01=='Middle'], y_mesh[z_RBF01=='Middle'], s=5, alpha=0.3, c='yellow')
plt.scatter(x_mesh[z_RBF01=='Good'], y_mesh[z_RBF01=='Good'], s=5, alpha=0.3, c='blue')
plt.title("C=Low, gamma=Low")
plt.xlabel("X1: Studying Time")
plt.ylabel("X2: Understanding")
plt.grid(True)
plt.xlim(0, 10)
plt.ylim(0, 10)

# *** RBF02 ***
plt.subplot(2,2,2)

# SVM出力
plt.scatter(x_mesh[z_RBF02=='Bad'], y_mesh[z_RBF02=='Bad'], s=5, alpha=0.3, c='red')
plt.scatter(x_mesh[z_RBF02=='Middle'], y_mesh[z_RBF02=='Middle'], s=5, alpha=0.3, c='yellow')
plt.scatter(x_mesh[z_RBF02=='Good'], y_mesh[z_RBF02=='Good'], s=5, alpha=0.3, c='blue')
plt.title("C=Low, gamma=High")
plt.xlabel("X1: Studying Time")
plt.ylabel("X2: Understanding")
plt.grid(True)
plt.xlim(0, 10)
plt.ylim(0, 10)

# *** RBF03 ***
plt.subplot(2,2,3)

# SVM出力
plt.scatter(x_mesh[z_RBF03=='Bad'], y_mesh[z_RBF03=='Bad'], s=5, alpha=0.3, c='red')
plt.scatter(x_mesh[z_RBF03=='Middle'], y_mesh[z_RBF03=='Middle'], s=5, alpha=0.3, c='yellow')
plt.scatter(x_mesh[z_RBF03=='Good'], y_mesh[z_RBF03=='Good'], s=5, alpha=0.3, c='blue')
plt.title("C=High, gamma=Low")
plt.xlabel("X1: Studying Time")
plt.ylabel("X2: Understanding")
plt.grid(True)
plt.xlim(0, 10)
plt.ylim(0, 10)

# *** RBF04 ***
plt.subplot(2,2,4)

# SVM出力
plt.scatter(x_mesh[z_RBF04=='Bad'], y_mesh[z_RBF04=='Bad'], s=5, alpha=0.3, c='red')
plt.scatter(x_mesh[z_RBF04=='Middle'], y_mesh[z_RBF04=='Middle'], s=5, alpha=0.3, c='yellow')
plt.scatter(x_mesh[z_RBF04=='Good'], y_mesh[z_RBF04=='Good'], s=5, alpha=0.3, c='blue')
plt.title("C=High, gamma=High")
plt.xlabel("X1: Studying Time")
plt.ylabel("X2: Understanding")
plt.grid(True)
plt.xlim(0, 10)
plt.ylim(0, 10)

plt.show()

青がGood, 黄がMiddle, 赤がBadとSVMが推定した領域であることを意味します。右側、gammaが高いモデルはかなりダメダメであることがわかります。一方、左側、gammaが低いモデルはまあまあ良さそうなモデルです。好みによりますが、どちらかといえば、左上の方が解釈しやすいでしょうか。このように、非線形SVMは複雑な識別境界を生成することができますが、そのせいでかなりアレな知能が形成されてしまう場合もあります。非線形SVMが線形SVMよりも安定しないと言われる所以ですね。なお、今回は偶然gammaが高い方が悪い結果になっただけで、gammaを高めた方が性能が良くなる場合もあります。トライアンドエラーでいい値を探しましょう。

回帰問題

SVMは決定木と同様に、入力変数から実数値を判断する回帰問題も解くことができます。回帰問題を解くSVMを、SVRと区別する人もいます。Rはregression(回帰)から取ってきている模様です。

In [12]:
import numpy as np
from sklearn import svm
import matplotlib.pyplot as plt

# xからyを推定するモデル
x=np.zeros([100,1])
for i in range(0,100):
    x[i,0]=i*0.2
y = np.sin(x).ravel() # 配列を1次元に変換

# モデル構築 (回帰の場合は、SVRを使用)
SVR_rbf = svm.SVR(kernel='rbf', C=10, gamma=10)
SVR_lin = svm.SVR(kernel='linear', C=1)
SVR_rbf = SVR_rbf.fit(x, y)
SVR_lin = SVR_lin.fit(x, y)

# # 推定
y_rbf = SVR_rbf.predict(x)
y_lin = SVR_lin.predict(x)

plt.scatter(x, y, c='black', label='Train data')
plt.plot(x, y_rbf, c='red', label='RBF')
plt.plot(x, y_lin, c='blue', label='Linear')

plt.xlabel('x')
plt.ylabel('y')
plt.title('Support Vector Regression')
plt.legend()
plt.show()

xからyを回帰するモデルです。教師データが黒プロット、赤がRBFカーネルによる推定、青が線形カーネルによる推定です。今回はsin波形を回帰させており、直線ではないので、線形カーネルではうまく学習ができていないことがわかります。今回は1入力1出力の回帰モデルを扱いましたが、多入力1出力のモデルも構築可能です。その場合は、xの列成分に変数を増やしていってください。