シェルスクリプトマガジン

機械学習ことはじめ(Vol.75掲載)

著者:川嶋 宏彰

本連載では、機械学習の基礎となるさまざまな手法の仕組みや、それらの手法のPythonでの利用方法を解説していきます。今回は、入力データがどのようなクラス(カテゴリ)であるかを予測する分類の問題を扱います。

シェルスクリプトマガジン Vol.75は以下のリンク先でご購入できます。

図2 データセットの読み込みから、散布図のプロットまでを実施するPythonコード

import seaborn as sns
import matplotlib.pyplot as plt

plt.rcParams['font.size'] = 14
penguins = sns.load_dataset('penguins')
# 取り出す特徴量
features = ['bill_depth_mm', 'body_mass_g']  # (★)
# features = ['bill_depth_mm', 'bill_length_mm']  # 取り出す特徴量を変える
# 対象とするペンギン種
target_species = ['Adelie', 'Gentoo']  # (★)
# target_species = ['Adelie', 'Chinstrap', 'Gentoo']  # 3種のペンギンにする
# 今回用いる特徴量をクラスラベルと共に取り出す
df = penguins[['species'] + features].copy()
df.dropna(inplace=True)  # NaN が含まれる行は削除
# 今回用いるペンギン種のみとする
df2 = df[df['species'].isin(target_species)].copy()
print(df2.shape)  # (274, 3) と表示される
# 現在のパレットから用いる種 (target_species) に合わせたパレットを作成
palette = {c: sns.color_palette()[k] for k, c
           in enumerate(df['species'].unique()) if c in target_species}
fig = plt.figure(figsize=(5, 5))
sns.scatterplot(data=df2, x=features[0], y=features[1],
                hue='species', palette=palette)
plt.show()

図3 ロジスティック回帰の準備をするPythonコード

from sklearn.linear_model import LogisticRegression
import numpy as np

X = df2[features].values   # 各個体の2次元特徴量(行数 = 個体数)
y = df2['species'].values  # 各個体のクラスラベル
clf_lr = LogisticRegression(penalty='none')  # 分類モデルの準備

図4 特徴量一つ(体重)のみでロジスティック回帰をするPythonコード

f_idx = 1  # 用いる特徴量のインデックス
# 指定した列(特徴量)だけ取り出して学習
clf_lr.fit(X[:, [f_idx]], y)
# 学習されたパラメータを表示
w0 = clf_lr.intercept_[0]
w1 = clf_lr.coef_[0][0]
print(f'w_0 = {w0:.2f}, w_1 = {w1:.4f}, b = -w0/w1 = {-w0/w1:.2f}')
# 予測値および分類結果の可視化用グリッド
xmin = np.min(X[:, f_idx])
xmax = np.max(X[:, f_idx])
xs = np.linspace(xmin - (xmax - xmin) / 10,
                 xmax + (xmax - xmin) / 10, 100)  # 等間隔に100点用意
fig = plt.figure(figsize=(10, 5))
# 元データのプロット
sns.scatterplot(x=X[:, f_idx], y=(y=='Gentoo'), hue=y, palette=palette)
# 予測値(事後確率)の曲線をプロット
y_pred = clf_lr.predict_proba(xs.reshape(-1, 1))[:, 1]
plt.plot(xs, y_pred, c=palette['Gentoo'])
plt.ylim(-0.1, 1.1)
plt.xlabel(f'x ({features[f_idx]})')
plt.ylabel('y')
plt.grid()
plt.show()

図6 特徴量二つ(くちばしの高さと体重)でロジスティック回帰をするPythonコード

import matplotlib as mpl
from sklearn.preprocessing import StandardScaler

def plot_decision_boundary(X, y, clf, xylabels=None, palette=None, fig=None, ngrid=50):
  """ 分類器 clf の決定境界を描画 """
  if fig is None: fig = plt.figure(figsize=(5, 5))
  else: plt.figure(fig.number)
  # 2次元空間にグリッド点を準備
  xmin = X.min(axis=0)  # 各列の最小値
  xmax = X.max(axis=0)  # 各列の最大値
  xstep = [(xmax[j]-xmin[j]) / ngrid for j in range(2)]  # グリッドのステップ幅
  xmin = [xmin[j] - 8*xstep[j] for j in range(2)]  # 少し広めに
  xmax = [xmax[j] + 8*xstep[j] for j in range(2)]
  aranges = [np.arange(xmin[j], xmax[j] + xstep[j], xstep[j]) for j in range(2)]
  x0grid, x1grid = np.meshgrid(*aranges)
  # 各グリッド点でクラスを判定
  y_pred = clf.predict(np.c_[x0grid.ravel(), x1grid.ravel()])
  y_pred = y_pred.reshape(x0grid.shape)  # 2次元に
  y_pred = np.searchsorted(np.unique(y_pred), y_pred)  # 値をインデックスに
  clist = palette.values() if type(palette) is dict else palette
  cmap = mpl.colors.ListedColormap(clist) if palette is not None else None
  plt.contourf(x0grid, x1grid, y_pred, alpha=0.3, cmap=cmap)
  sns.scatterplot(x=X[:, 0], y=X[:, 1], hue=y, palette=palette)
  plt.legend()
  plt.xlim([xmin[0], xmax[0]])
  plt.ylim([xmin[1], xmax[1]])
  if xylabels is not None:
    plt.xlabel(xylabels[0])
    plt.ylabel(xylabels[1])
  return fig

Xs = StandardScaler().fit_transform(X)  # 標準化によるスケーリング
clf_lr.fit(Xs, y)  # 二つの特徴量を両方用いて学習
print('w0:', clf_lr.intercept_)
print('w1, w2:', clf_lr.coef_)
xylabels = [s + ' (standardized)' for s in features]  # 軸ラベル変更
plot_decision_boundary(Xs, y, clf_lr, xylabels, palette, ngrid=100)  # 決定境界
plt.show()

図9 ロジスティック回帰による各点での予測値をプロットするPythonコード

from matplotlib import cm
from mpl_toolkits.mplot3d import axes3d

# メッシュで
def gen_2dgrid(X):
  """ 2次元メッシュグリッドの生成 """
  d = X.shape[1]
  xmin = X.min(axis=0)  # 各列の最小値
  xmax = X.max(axis=0)  # 各列の最大値
  xstep = [(xmax[j]-xmin[j]) / 100 for j in range(d)]  # グリッドのステップ幅
  xmin = [xmin[j] - 10*xstep[j] for j in range(d)]  # 少し広めに
  xmax = [xmax[j] + 10*xstep[j] for j in range(d)]
  aranges = [np.arange(xmin[j], xmax[j] + xstep[j], xstep[j]) for j in range(2)]
  return np.meshgrid(*aranges)  # d=2のときは (x0grid, x1grid) が返る
# 2次元の場合の予測値 (proba)
def proba_2dgrid(clf, x0grid, x1grid):
  """ 2次元メッシュの各点での予測値を計算 """    
  return clf.predict_proba(np.c_[x0grid.ravel(), x1grid.ravel()])[:, 1]
                          .reshape(x0grid.shape)
xgrid_2d = gen_2dgrid(Xs)  # xgrid_2d: (x0grid, x1grid) のような二つ組
proba_2d = proba_2dgrid(clf_lr, *xgrid_2d)  # 予測値
# 3次元プロット
fig = plt.figure(figsize=(14, 8))
ax = fig.add_subplot(111, projection='3d')
cmap = cm.winter  # カラーマップを設定
ax.plot_surface(*xgrid_2d, proba_2d, cmap=cmap)
# ax.set_box_aspect((3, 3, 1))  # matplotlib のバージョンが3.3以上で利用可能
ax.set_zlim(-0.1, 1.1)
ax.set_xlabel(xylabels[0])
ax.set_ylabel(xylabels[1])
ax.set_zlabel('y')
ax.set_zticks([0, 0.5, 1.0])
ax.view_init(elev=60, azim=-120)  # 3次元プロットの表示角度の設定
plt.show()

図12 線形SVM による学習と決定境界の表示をするPythonコード

from sklearn.svm import SVC

C = 10  # 大きいほどハードマージンに近い
clf_lin = SVC(kernel='linear', C=C)
clf_lin.fit(Xs, y)
# 決定境界とサポートベクターを可視化
plot_decision_boundary(Xs, y, clf_lin, xylabels, palette, ngrid=100)
plt.scatter(clf_lin.support_vectors_[:, 0], clf_lin.support_vectors_[:, 1],
            s=50, facecolors='none', edgecolors='r')
plt.title(f'SVM(linear) C = {C}')
plt.show()

図17 非線形SVMとパラメータCの関係を示すPythonコード

from sklearn.datasets import make_moons

X_moon, y_moon = make_moons(n_samples=100, noise=0.2, random_state=6)
palette_o = {k: sns.color_palette()[k] for k in range(2)}
# moonデータの散布図をプロット
plt.figure(figsize=(5, 5))
sns.scatterplot(x=X_moon[:, 0], y=X_moon[:, 1],
                hue=y_moon, palette=palette_o)
plt.show()
# 異なるCでSVM(RBFカーネル)の学習と決定境界の表示
for C in [0.1, 10, 10000]:
  clf_rbf = SVC(C=C, kernel='rbf')  # kernel='rbf'は省略可能
  clf_rbf.fit(X_moon, y_moon)
  # 決定境界をプロット
  plot_decision_boundary(X_moon, y_moon,
                         clf_rbf, None, palette_o, ngrid=100)
  plt.title(f'SVM(rbf) C = {C}')
  plt.show()

図20 グリッドサーチをするPythonコード

from sklearn.model_selection import cross_val_score

best_score = -1
for gamma in [0.1, 0.5, 1, 2, 10]:  # 5通り
  for C in [0.1, 1, 10, 100, 1000, 10000]:  # 6通り
    print(f'gamma: {gamma},\tC: {C}', end='')
    svm = SVC(gamma=gamma, C=C)
    cv_scores = cross_val_score(svm, X_moon, y_moon, cv=5)
    score = np.mean(cv_scores)
    print(f'\t | average: {score:.2} <- {cv_scores}')
    if score > best_score:
      best_score = score
      best_params = {'C': C, 'gamma': gamma}
print('best_score:', best_score)
print('best_params:', best_params)
clf_rbf = SVC(**best_params)
clf_rbf.fit(X_moon, y_moon)
plot_decision_boundary(X_moon, y_moon,
                       clf_rbf, None, palette_o, ngrid=100)
title = f"C = {best_params['C']}, gamma = {best_params['gamma']}"
plt.title(title)
plt.show()