はじめに
前記事の「【R】"Need numeric dependent variable for regression."の原因と対応」でR言語でサポートベクターマシンを使用した分類問題が動作させることが出来た。
今度はこれを R言語からPythonに移植しようとした際に、R言語では文字列データ(正確には因子型)で出来たが、Pythonでは数値型に変換しないと下記のエラーになってしまう。
model.fit(sze.iloc[train, 1:], sze.iloc[train, 0])
ValueError: could not convert string to float: 'G'
それだと不便だし、自分が知らないだけで本当はPythonでも出来るのではないかと疑い調べました。
環境
- Google Colaboratory
- Python 3.6.9
- sklearn 0.22.2
調査
「scikit-learn SVM 文字列」で検索してもズバリの回答が得られない、日本語の検索は無理と判断して英語に変更し「scikit-learn SVM String」や「scikit-learn SVM Non-Integer」で検索して下記サイトを見つけた。
Non-Integer Class Labels Scikit-Learn - stackoverflow
これを実行してもエラーにはならない。
from sklearn.svm import SVC
clf = SVC()
x = [[1,2,3], [4,5,6]]
y = ['dog', 'cat']
clf.fit(x,y)
yhat = clf.predict([[1,2,5]])
print yhat[0]
それではと使用したいのはジャンケンデータなのでGとCとPの文字列を使用して、学習データと正解ラベルに文字列を用いてみた。しかし、こうすると同じエラーが出てしまう。
from sklearn.svm import SVC
clf = SVC()
x = [['G',2,3],['C',5,6]]
y = ['G', 'C']
clf.fit(x,y)
yhat = clf.predict([['P',2,5]])
print(yhat[0])
ValueError: could not convert string to float: 'G'
サンプル同様に正解ラベルのみ文字列にした場合、エラーは出なかった。
from sklearn.svm import SVC
clf = SVC()
x = [['1',2,3],['4',5,6]]
y = ['G', 'C']
clf.fit(x,y)
yhat = clf.predict([['1',2,5]])
print(yhat[0])
SVM(サポートベクターマシン)は線形二値分類のアルゴリズムなので、学習データにURLのような文字データが入ってきても分類できないと思われます。
機械学習ValueErrorでURL文字列を読み込めない。
学習データに関しては、文字列ではなく数値型を使用する必要がある。
文字列型から数値型にする
文字列型から数値型にするには、カテゴリーエンコーディングを使用する。
カテゴリーエンコーディングには、ラベルエンコーディングやOne-Hotエンコーディングなど多くの種類がある。
機械学習のための前処理(scikit-learn.preprocessing)として提供されている
LabelEncoder
from sklearn.preprocessing import LabelEncoder
le = LabelEncoder()
le.fit(['C', 'P', 'G', 'C', 'C'])
print(le.classes_)
print(le.transform(['G', 'C', 'P', 'C']))
print(le.inverse_transform([1, 0, 2, 0]))
# 結果
['C' 'G' 'P']
[1 0 2 0]
['G' 'C' 'P' 'C']
le.fit()
の部分で変換したいデータを選択します。
le.classes_
では割り振りの値を確認できます。アルファベットの昇順に数値が割り振られています。
le.transform()
で数値へ変換します。
le.inverse_transform()
で文字列データに戻せます。
OneHotEncoder
One-hotベクトル(ワンホット表現)の意味とメリット・デメリット
LabelEncoder
のように1次元配列で渡すと、次のエラー「'Expected 2D array, got 1D array instead'」になるので2次元配列で渡します。1次元配列で変換したい場合、後述のラベルバイナライザーを使用します。
pandasのDataFrameを使用する上では2次元配列の方が都合がいい。
from sklearn.preprocessing import OneHotEncoder
from sklearn.compose import ColumnTransformer
ohe = OneHotEncoder()
ohe.fit([['C'], ['P'], ['G'], ['C'], ['C']])
ct = ColumnTransformer([("category", ohe, [0])], remainder="passthrough")
print(ohe.categories_)
print(ct.fit_transform([['G'], ['C'], ['P'], ['C']]))
print(ohe.inverse_transform([[0,1,0],[1,0,0],[0,0,1],[1,0,0]]))
# 結果
[array(['C', 'G', 'P'], dtype=object)]
[[0. 1. 0.]
[1. 0. 0.]
[0. 0. 1.]
[1. 0. 0.]]
[['G']
['C']
['P']
['C']]
ohe.fit()
の部分で変換したいデータを選択します。
ohe.classes_
では割り振りの値を確認できます。アルファベットの昇順に数値が割り振られています。
ct.fit_transform()
で数値へ変換します。
ohe.inverse_transform()
で文字列データに戻せます。
ColumnTransformerを使うと複数の列の変換も可能、引数の意味合いなどは下記サイトを参考
OneHotEncoderのサンプルでみる'categorical_features'キーワードですが、バージョン0.20で非推奨、0.22で削除となり、代わりに'ColumnTransformer'を使用するように提示されます。
scikit-learnのColumnTransformerを使ってみる - 静かなる名辞
pandasには'get_dummies'を使用してOne-Hotエンコーディングができます。用途に合わせて使いましょう。
pandasのget_dummiesを使う - 静かなる名辞
本来の使い方としては、下記のように複数の組み合わせでOne-Hot表現したい場合となります。
OneHotEncoder - Taustation
from sklearn.preprocessing import OneHotEncoder
X = [
['Tokyo', 'Male'],
['Tokyo', 'Female'],
['Osaka', 'Male'],
['Kyoto', 'Female'],
['Osaka', 'Female'],
['Osaka', 'Male']
]
ohe = OneHotEncoder(sparse=False)
ohe.fit(X)
print(ohe.categories_)
print(ohe.transform(X))
# 結果
[array(['Kyoto', 'Osaka', 'Tokyo'], dtype=object), array(['Female', 'Male'], dtype=object)]
[[0. 0. 1. 0. 1.]
[0. 0. 1. 1. 0.]
[0. 1. 0. 0. 1.]
[1. 0. 0. 1. 0.]
[0. 1. 0. 1. 0.]
[0. 1. 0. 0. 1.]]
LabelBinarizer
One-Hotエンコーディングでは2次元配列で渡していましたが、ラベルバイナライザーは1次元配列でOne-Hot表現に変換できます。
import numpy as np
from sklearn.preprocessing import LabelBinarizer
lb = LabelBinarizer()
lb.fit(['C', 'P', 'G', 'C', 'C'])
print(lb.classes_)
print(lb.transform(['G', 'C', 'P', 'C']))
print(lb.inverse_transform(np.array([[0,1,0],[1,0,0],[0,0,1],[1,0,0]])))
# 結果
['C' 'G' 'P']
[[0 1 0]
[1 0 0]
[0 0 1]
[1 0 0]]
['G' 'C' 'P' 'C']
lb.fit()
の部分で変換したいデータを選択します。
lb.classes_
では割り振りの値を確認できます。アルファベットの昇順に数値が割り振られています。
lb.transform()
で数値へ変換します。
lb.inverse_transform()
で文字列データに戻せます。※np.arrayが必要
Scikit-learn's LabelBinarizer vs. OneHotEncoder - stackoverflow
OrdinalEncoder
LabelEncoder
と同じようにアルファベットの昇順に数値が割り振られます。違いとしてはOneHotEncoderと同様に2次元配列で渡します。
pandasのDataFrameを使用する上では2次元配列の方が都合がいい。
from sklearn.preprocessing import OrdinalEncoder
oe = OrdinalEncoder()
oe.fit([['C'], ['P'], ['G'], ['C'], ['C']])
print(oe.categories_)
print(oe.fit_transform([['G'], ['C'], ['P'], ['C']]))
print(oe.inverse_transform([[1],[0],[2],[0]]))
# 結果
[array(['C', 'G', 'P'], dtype=object)]
[[1.]
[0.]
[2.]
[0.]]
[['G']
['C']
['P']
['C']]
oe.fit()
の部分で変換したいデータを選択します。
oe.classes_
では割り振りの値を確認できます。アルファベットの昇順に数値が割り振られています。
oe.fit_transform()
で数値へ変換します。
oe.inverse_transform()
で文字列データに戻せます。
OrdinalEncoder
は複数の組み合わせで使用できるので、LabelEncoder
が正解ラベル側として使用するのに対し、学習データ側はOrdinalEncoder
を使用するイメージです。
from sklearn.preprocessing import OrdinalEncoder
X = [
['Tokyo', 'Male'],
['Tokyo', 'Female'],
['Osaka', 'Male'],
['Kyoto', 'Female'],
['Osaka', 'Female'],
['Osaka', 'Male']
]
oe = OrdinalEncoder()
oe.fit(X)
print(oe.categories_)
print(oe.transform(X))
# 結果
[array(['Kyoto', 'Osaka', 'Tokyo'], dtype=object), array(['Female', 'Male'], dtype=object)]
[[2. 1.]
[2. 0.]
[1. 1.]
[0. 0.]
[1. 0.]
[1. 1.]]
CustomerRating
サザエさんのジャンケンでは、統計情報からG>C>Pの強さになっているので、LabelEncoder
やOrdinalEncoder
のようにアルファベットの昇順に数値が割り振られると、結果に違いが出てしまう。CをGとPの間にある別のアルファベットに変更すれば、LabelEncoder
やOrdinalEncoder
でもそのまま使用できるが、記号を維持したまま数値を割り当てたい。
mapを使用して割り当てたい数値に変換している。
import pandas as pd
rating = {'G' : 1, 'C' : 2, 'P' : 3}
df = pd.DataFrame(['C', 'P', 'G', 'C', 'C'])
print(df.applymap(lambda x : rating[x]))
# 結果
0 2
1 3
2 1
3 2
4 2
最後に
同じ列に異なる数値があるとモデルのデータによってはある種の順序(0 < 1 < 2)であると誤解する。この問題を克服するために、One Hot表現を使用する。逆にある種の順序(G > C > P)の優先度を考慮するならば、同じ列に異なる数値を用いてもいい。
One Hot表現を使用する意味がやっと分かった気がする。
入力(説明変数、独立変数)と出力(目的変数、従属変数)があって、説明変数側は回帰でも分類でも基本的 One Hot表現を使用すればいいわけだ、目的変数側は文字列であっても問題ないよってことだね。
【scikit-learn】Label EncoderとOne Hot Encoderの使い分けについて - teratail