デジタル・デザイン・ラボラトリーな日々

アラフィフプログラマーが数学と物理と英語を基礎からやり直す。https://qiita.com/yaju

機械学習の勉強再開 線形SVMの改善

はじめに

前回、R言語の線形SVMをPythonに移植しました。 yaju3d.hatenablog.jp

Pythonやscikit-learn に慣れていなかったので、もう少しいい方法があるのではないかと再調査しました。

SVMでは文字列は使えるのか

結論では、正解ラベル側には文字列が使えますが、学習データ側は文字列が使えないため数値型に変換する必要があります。
長くなったのでQiitaに別記事としました。 qiita.com

変更前の数値型変換ですが、イケてないですよね。

sze_row.loc[sze_row.X1 == "G", "X1"] = 1
sze_row.loc[sze_row.X1 == "C", "X1"] = 2
sze_row.loc[sze_row.X1 == "P", "X1"] = 3
sze_row.loc[sze_row.X2 == "G", "X2"] = 1
sze_row.loc[sze_row.X2 == "C", "X2"] = 2
sze_row.loc[sze_row.X2 == "P", "X2"] = 3
sze_row.loc[sze_row.X3 == "G", "X3"] = 1
sze_row.loc[sze_row.X3 == "C", "X3"] = 2
sze_row.loc[sze_row.X3 == "P", "X3"] = 3

これを改善しました。

# ラベルエンコーディング
rating = {'G' : 1, 'C' : 2, 'P' : 3}
sze_row.X1 = sze_row.X1.map(lambda x : rating[x])
sze_row.X2 = sze_row.X2.map(lambda x : rating[x])
sze_row.X3 = sze_row.X3.map(lambda x : rating[x])

SVC(kernel='linear')とsvm.LinearSVCの違い

線形SVMですが、kernel='linear' を指定する方法とLinearSVCを方法の2種類あります。
これが同じかというと微妙に違うの結果も変わってきます。
When should one use LinearSVC or SVC? - StackOverflow

LinearSVCがLIBLINEARライブラリーを基にしているに対し、SVC(kernel='linear')はLIBSVMライブラリーを基にしている。
LinearSVCの場合、デフォルトはヒンジ損失の2乗(loss='squared_hinge')を最小化するように設定されています。

LIBSVMはカーネル法を用いたサポートベクターマシン (SVM) の学習に使うSMOアルゴリズムを実装しており、分類と回帰に対応している[1]。 LIBLINEARは線形SVMと、座標降下法(英語版)アルゴリズムを用いて学習するロジスティック回帰を実装している LIBSVM - wikipedia

SVC(kernel='linear')

model = svm.SVC(kernel='linear', C=2.5, max_iter=5000, random_state=3383)

結果

0.6458333333333334

C G P
C 8 6 3
G 0 13 2
P 3 3 10

31勝10敗8分けとなります。

LinearSVC

デフォルトは、loss='squared_hinge' なので指定しなくても同じになります。

#model = svm.LinearSVC(C=2.5, max_iter=5000, random_state=3383)
model = svm.LinearSVC(loss='squared_hinge', C=2.5, max_iter=5000, random_state=3383)

結果

0.625

C G P
C 5 8 4
G 0 13 2
P 1 3 12

30勝11敗7分け

LinearSVCの補足

loss='hinge' に変更すると結果がかなり悪くなります。

model = svm.LinearSVC(loss='hinge', C=2.5, max_iter=20000, random_state=3383)

max_iter=5000 では下記の警告が発生します。Google翻訳すると「Liblinearは収束に失敗しました。反復回数を増やしてください。 」ということなので、max_iter=20000 まで上げたところ警告が出なくなりました。

/usr/local/lib/python3.6/dist-packages/sklearn/svm/_base.py:947: ConvergenceWarning: Liblinear failed to converge, increase the number of iterations.
  "the number of iterations.", ConvergenceWarning)

結果

0.5

C G P
C 4 6 7
G 0 6 9
P 0 2 14

24勝15敗9分け

ソースコード

import pandas as pd
import numpy as np
import urllib.request
from io import StringIO
from sklearn import svm
from sklearn.metrics import accuracy_score
import random

url = "https://raw.githubusercontent.com/yaju/Sazae_R/master/2017sze.csv"

# データの読み込み
res = urllib.request.urlopen(url)
res = res.read().decode("utf-8")
sze_row = pd.read_csv(StringIO(res), header=0)
# 末尾の日付削除
sze = sze_row.iloc[:, :-1]

# ラベルエンコーディング
rating = {'G' : 1, 'C' : 2, 'P' : 3}
sze.X1 = sze.X1.map(lambda x : rating[x])
sze.X2 = sze.X2.map(lambda x : rating[x])
sze.X3 = sze.X3.map(lambda x : rating[x])

#1992年~2016年までを学習用、2017年分をテスト用とする
train = range(0, 1253)
test = range(1254, 1302)

x_train = sze.iloc[train, 1:]
y_train = sze.iloc[train, 0]
x_test = sze.iloc[test, 1:]
y_test = sze.iloc[test, 0]

# 分類器svm
#seedはサザエさん(3383)とする
model = svm.SVC(kernel='linear', C=2.5, max_iter=5000, random_state=3383)

# 学習
model.fit(x_train, y_train) 

pred = model.predict(x_test)
tab = pd.crosstab(y_test, pred)
# 識別率
print(accuracy_score(y_test, pred))

tab
#print(y_test)
#print(pred)

調査

同じ列の優先順序

同じ列に異なる数値があるとモデルのデータによってはある種の順序(0 < 1 < 2)であると誤解するとのことで、値の順番を下記のように変更してみました。
結果は31勝10敗8分けと変わらなかったです。

rating = {'G' : 1, 'C' : 2, 'P' : 3} 
                       ↓
rating = {'G' : 3, 'C' : 2, 'P' : 1}

One Hot表現に変更

説明変数側(学習データ)は回帰でも分類でも基本的 One Hot表現を使用するのが一般的なようです。こうすると優先順序が関係なくなるからです。

# ラベルエンコーディング
rating = {'G' : 1, 'C' : 2, 'P' : 3}
sze.X1 = sze.X1.map(lambda x : rating[x])
sze.X2 = sze.X2.map(lambda x : rating[x])
sze.X3 = sze.X3.map(lambda x : rating[x])

上記の部分を下記に変更します。

# One-hotエンコーディング
sze = pd.get_dummies(sze, columns=['X1', 'X2', 'X3'])

pandasのget_dummiesを使うとカラム指定で、One-hotエンコーディングができます。
変換元の列は削除され、末尾にOne-hotの列が追加されます。

変換前

X X1 X2 X3 Q Grate Crate Prate
C G P G 1 0.333333 0.333333 0.333333

変換後

X Q Grate Crate Prate X1_C X1_G X1_P X2_C X2_G X2_P X3_C X3_G X3_P
C 1 0.333333 0.333333 0.333333 0 1 0 0 0 1 0 1 0

結果

0.6666666666666666

C G P
C 13 2 2
G 2 9 4
P 5 1 10

32勝11敗5分け

OneHotEncoderの使用

www.haya-programming.com

pandasのget_dummiesは便利だけど用途によっては使用を控えた方がいいようなので、OneHotEncoderの使い方も知っておく必要がある。
get_dummiesと同じことを、OneHotEncoderで実現してみました。
ColumnTransformerで複数列を変換できますが、列名ではなくインデックス番号の指定する。
numpy.ndarray で変換されるので一旦データフレームに変換、元のデータフレームで変換前の文字列の列を削除して変換されたデータフレームと結合させた。

from sklearn.preprocessing import OneHotEncoder
from sklearn.compose import ColumnTransformer

ct = ColumnTransformer([('onehot', OneHotEncoder(sparse=False), [1,2,3])])
columns = ['X1_C', 'X1_G', 'X1_P',   'X2_C', 'X2_G', 'X2_P', 'X3_C', 'X3_G', 'X3_P']
df = pd.DataFrame(ct.fit_transform(sze), columns=columns)
sze = sze.drop(['X1', 'X2', 'X3'], axis=1)
sze = pd.concat([sze, df], axis=1)

参照

最後に

結果からすれば学習データはOne Hot表現にして問題ないですね、実際に結果を良かったですし。
One Hot表現にすることでクロス集計(tab)のところを修正する必要があるのかと気にしていたのですが、全く修正なしでした。
これからは、One Hot表現でやっていきます。

これでようやっと次の段階に入ります。

  • RBFカーネルを用いたSVM
  • randomForest
  • 決定木
  • naive bayes

スポンサーリンク