この記事の狙い・目的
機械学習を取り入れたAIシステムの構築は、
①データセット作成(前処理)→ ②モデルの構築 → ③モデルの適用
というプロセスで行っていきます。
その際「データセット作成(前処理)」の段階では、正しくモデル構築できるよう、事前にデータを整備しておくことが求めらます。
このブログでは、その際「特徴量エンジニアリング」として用いられることの多い「エンコーディング」手法について解説していきます。
プログラムの実行環境
Python3
MacBook pro(端末)
PyCharm(IDE)
Jupyter Notebook(Chrome)
Google スライド(Chrome)
使用するデータセット
import pandas as pd
train_df = pd.read_csv("./train.csv")
train_df.shape
train_df.head()
確認の結果、「名義尺度」は「状態1、販売条件」、「順序尺度」は「全体的な品質、地下室の状態」であることがわかりました。
データセット:House Prices - Advanced Regression Techniques - kaggle
エンコーディング
エンコーディングとは、カテゴリ(質的)変数を数値に変換(符号化 (encode))する手法のことです。
カテゴリ変数とは
カテゴリ(質的)変数とは、以下の「名義尺度」と「順序尺度」の尺度で表される変数のことを言います。
名義尺度とは、他と区別・分類するための尺度。例えば、性別、血液型などです。
順序尺度とは、順序に意味はあるが、それらの間隔には意味がない尺度。例えば、順序、ランクなどです。
なぜカテゴリ変数の変換が必要なのか
多くのアルゴリズムではカテゴリカルな変数(例:性別・ランク)は、そのままの形(≠数値)では学習させることができません。その為、何らかの量的変数に置き換えて表現する必要があるから
エンコーディング手法
それではカテゴリ変数を変換する様々な手法について、解説していきます。
1.One-Hotエンコーディング
OneーHotエンコーディングとは、対象のカテゴリー変数の構成要素分の変数を作成し、作り出した各変数は行単位で「該当なし:0、該当あり:1」を変換する手法です。
メリット
カテゴリカル変数のそれぞれの要素に応じて重みづけができ、正確な予測ができる場合がある。
また、One-Hotエンコーディングであれば、後に紹介する「Labelエンコーディング」と異なり、数値の大小関係を各変数に持たせないため、線形回帰やニューラルネットのアルゴリズムでも有効な場合がある。
デメリット
カテゴリ数が非常に多いデータセットに対してOne-Hotエンコーディングをすると、ランダムフォレストなどランダムに特徴量を選択するアルゴリズムでは、数値変数などの非カテゴリカル変数の選択率が減少して精度が落ちる場合がある。
※その場合は、PCAやEmbeddingと組み合わせて次元を削減するにも一つの手と言えるでしょう。
※生成された各変数間には、線形の依存関係が出てしまいます。これも精度の低下を引き起こす原因となる場合があるため注意が必要です。
train_df['状態1'].value_counts()
train_oh = pd.DataFrame()
train_oh = pd.get_dummies(train_df['状態1'], prefix='状態1', prefix_sep='_')
train_oh.shape
train_oh.head(10)
2.ダミーコーディング
ダミーコーディングも、(One-Hotエンコーディング同様)カテゴリ変数を数値変換する手法です。One-Hotエンコーディングと異なる点は、生成する特徴量が(One-Hotエンコーディングより)1つ少ない点です。
もう少し詳しく解説していきましょう。
One-Hotエンコーディングの問題点として、生成された各変数間には、線形の依存関係があるという点がありました。
線形の依存関係がある特徴量の場合、線形モデルの回帰係数(重み)が一意に定められないという問題があることに注意が必要です。
というのも、2つの特徴量が強く相関している(線形性がある)場合、特徴量の効果が相加的であり、相関する特徴量のどちらが影響を与えているのか不定となるため、回帰係数(重み)の推定が不安定となります。
k個すべての特徴量を生成してしまった場合、ダミー変数に拘束関係が生まれてしまい、先述の通り回帰分析(のによる重み付け)がうまくできなくなってしまうことが想定されます。
この問題に対処するため、ダミーコーディングでは、生成する特徴量数を「K-1」個、つまり最初の列を削除する(drop_first)方法を取ります。
拘束関係とは、1つの変数が他の変数を使用して簡単に予測できることを意味します。
メリット
(One-Hotエンコーディングと比べ)ダミーコーディングの方が多重共線性を回避することができる。
デメリット
カテゴリカル変数の要素の数だけカラムが増え、スパースなデータとなり、結果、メモリを消費が大きくなる。
それでは実装と結果を見ていきましょう。
train_df['状態1'].value_counts()
train_dm = pd.DataFrame()
train_dm = pd.get_dummies(train_df['状態1'], prefix='状態1', prefix_sep='_', drop_first = True)
train_dm.shape
train_dm.head(10)
# prefix – カラムラベルに付け加える文字列
# prefix_sep – prefixとの繋ぎ文字
# drop_first – k-1個のダミー変数を作成するかどうか
3.Effectコーディング
参照カテゴリとは、ダミーコーディングのように参照カテゴリを[0]で表現するのではなく、[-1]で表現する手法です。
メリット
多重共線性を回避するもう一つの方法である。
欠損データにも対応できる。
デメリット
「0〜1」ではなく[-1〜1]で表現するため、使用するメモリ領域が増え、結果的に計算コストが増えてしまう。
train_ef = train_dm.copy()
train_ef['状態1_Norm'] = train_ef['状態1_Norm'].where(train_ef['状態1_Feedr'] == 0, -1) # 条件の結果がFalseの値を置換
train_ef[['状態1_Feedr', '状態1_Norm']].head(10)
4.ラベル・エンコーディング
ラベル・エンコーディングとは、ラベルの種類に応じた数値を割り当てて特徴量を生成する手法です。
LabelEncoderで1つの変数を離散値として扱う場合は、決定木ベース、ランダムフォレストなどの分析と相性が良いです。
反対に、線形回帰やニューラルネットなど、重み(係数)と特徴量の線形関係のデータが入力値となるアルゴリズムの場合には有効とは言えません。
メリット
カラム数(次元数)はそのままで、数値に置き換わるので、メモリを節約できる。
デメリット
欠損値に対応していない、どうなる?
ラベル間の数値の大小関係を学んでしまう。その為、発生頻度は低いが重要な変数などを学習しづらい。
エンコーディングの際、無意味な順序付けが発生する。
注意点
順序性がない値、すなわち名義尺度に適用するのは避ける必要があります。思わぬ順序生をモデルに学習させてしまう可能性があります。
その為、各変数の順序性は事前に吟味する必要があるでしょう。より意味のあるデータにする為には、明示的に順位を割り当てる必要もあるでしょう。
また、順位間の差が一定でないものは、Labelエンコーディングにより一定に変換されてしまう為、別の手法を検討する必要があるでしょう。
# LabelEncoder : 3値以上のラベル分類
from sklearn.preprocessing import LabelEncoder
train_df['外部の品質'].value_counts()
train_lbl = train_df.copy()
lbl = LabelEncoder()
train_lbl['外部の品質_enc'] = lbl.fit_transform(train_df['外部の品質'])
train_lbl[['外部の品質', '外部の品質_enc']].head()
5.Ordinalエンコーディング
Ordinalエンコーディングとは、カテゴリ変数の項目数に対応して連番、または指定番号(ラベル)を振った列を作成する手法です。
挙動はLabeleエンコーディングと似ていますが、変換後の値を指定することもできます。実装と結果を見ていきましょう。
メリット
順序性を考慮してエンコーディングができ、Labelエンコーディングの問題を解決できる。
デメメリット
Labelエンコーディング同様、順位間の差が一定でないものに対しては、その差を一律にしてしまう為、有効な手法ではない。
map_dict = {'TA': 0, 'Gd': 1, 'Ex': 2, 'Fa': 3}
def map_values(x):
return map_dict[x]
train_oe = train_df.copy()
train_oe['外部の品質_oe'] = train_oe['外部の品質'].apply(lambda x: map_values(x))
train_oe[['外部の品質', '外部の品質_oe']].tail()
from sklearn.preprocessing import OrdinalEncoder
# OrdinalEncoder : 複数項目に対する、一括ラベルエンコーディング
train_oe = train_df.copy()
oe = OrdinalEncoder()
train_oe[['外部の品質_oe', '外部の条件_oe']] = oe.fit_transform(train_df[['外部の品質', '外部の条件']]) # 欠損値不可
train_oe[['外部の品質', '外部の品質_oe', '外部の条件', '外部の条件_oe']].tail()
train_df['外部の品質'].value_counts()
train_oe['外部の品質_oe'].value_counts()
print('='*30)
train_df['外部の条件'].value_counts()
train_oe['外部の条件_oe'].value_counts()
6.Feature hashing
Feature hashingとは、カテゴリ変数に対しハッシュ関数を用いて指定した大きさのハッシュへ変換し、さらにOne-Hotエンコーディングを行う手法です。
メリット
変換前のカテゴリ変数の水準数より、変換後の特徴量の数を少なくすることができる。
※One-Hotエンコーディングと異なり、列の増加を抑制することができる。
その為、カテゴリの水準数が多く、One-Hotエンコーディングを行うと特徴量が膨大になってしまう場合に利用することが多い手法です。
デメリット
One-Hotエンコーディングより次元数を少なくできる反面、衝突が起きる可能性もあります。
衝突とは、水準数を少なくしているため、ハッシュ関数の計算によっては異なる水準でも同じ場所にフラグが立ってしまうことを言います。
また、変換過程にハッシュ値を用いていることから直感的な解釈が難しくなり、データの探索や可視化には不向きと言えます。
from sklearn.feature_extraction import FeatureHasher
from sys import getsizeof
tgt = train_df['全体的な品質'].astype('str')
tgt.value_counts()
m = len(tgt.unique())
print(f'要素数(次元数): {m}')
# Feature hasing
h = FeatureHasher(n_features=m, input_type='string')
f = h.fit_transform(tgt)
# 疎行列(スパース行列)を、NumPy配列 ndarray(密行列(非スパース行列)) に変換
f.toarray()
# 見づらいためDataFrameに変換
df = pd.DataFrame(data = f.todense().astype(str), columns = ["ハッシュ{}".format(x+1) for x in range(m)])
df.head()
# 変換前後のデータ容量の確認
print(f'メモリサイズ(前): {getsizeof(tgt)}')
print(f'メモリサイズ(後): {getsizeof(f)}')
7.カウント(Frequency)エンコーディング
カウント(Frequency)エンコーディングとは、特徴量の各値の出現頻度で置き換える手法です。
また、エンコーディング後の値の大小(出現頻度)に意味があり、そのことが目的変数と相関がある場合は、線形・非線形どちらでも有効な手法と言えます。
メリット
次元を増さずにエンコーディングできる。
デメリット
外れ値も全て拾ってしまう為、外れ値に敏感。ただし、対数変換すれば感度を弱められることがある。
異なるカテゴリカル変数に同じ値が割り当てられ、情報が失われる場合がある。
(例: A,Bの出現回数がそれぞれ100であれば、A→1, B→1と割り当てられてしまう)
count_df = train_df.copy()
count_map = count_df['全体的な品質'].value_counts().to_dict()
count_map
count_df['全体的な品質_cnt'] = count_df['全体的な品質'].map(count_map)
count_df[['全体的な品質', '全体的な品質_cnt']].head()
8.ラベル・カウント(Ranked frequency)エンコーディング
ラベル・カウントエンコーディングとは、特徴量の各値の出現頻度で順位付けし、その順位に置き換える手法です。
メリット
データの間隔が全て1になるため、外れ値の影響も小さくなる。
デメリット
出現回数が同じカテゴリカル変数に対して、無意味な順序が導入されてしまう。
rank_df = train_df.copy()
rank_map = rank_df['全体的な品質'].value_counts().rank(ascending=False).to_dict()
rank_map
rank_df['全体的な品質_cnt'] = count_df['全体的な品質'].map(rank_map)
rank_df[['全体的な品質', '全体的な品質_cnt']].head()
9.ビンカウンティング
ビンカウンティングとは、カテゴリ変数を、カテゴリに関する統計量に置き換える手法です。
目的変数に対して、有用な集計量を設定する必要がある。
カウントエンコーディングも集計値を利用していることから、これも一種のビンカウンティングの手法と言えるでしょう。
またビンカウンティングは、交互作用項を生成する際などにも利用できます。交互作用項とは、関係のある2つの変数をかけ合わせた新しい変数のことを言います。
メリット
大量の特徴量がある場合、統計量の取り方次第で次元数を減らすことができる。
デメメリット
目的変数の値を使用した統計量を学習させたい場合、データ分割(Fold)等を施さなければ、本来予測時に与えられない情報を用いていることになり、不適切な学習となってしまうため注意が必要。
bin_count = train_df.copy()
bin_df = pd.DataFrame()
taeget = bin_count.groupby('MSサブクラス').agg({'暖炉': 'mean'})
bin_df = pd.merge(bin_count, taeget, on='MSサブクラス', right_index=True)[['MSサブクラス', '暖炉_y']].astype(str)
bin_df.head()
bin_df.tail()
9.Nonエンコーディング
Nonエンコーディングとは、NaN値がある場合、(NaN自体にも情報はあることがあるため)その情報を保持したダミー変数を作る
メリット
欠損値に意味のある場合は効果的。
デメメリット
意味のない欠損値に対してエンコーディングした際には、ノイズを多く学習し、その結果精度の低下や、オーバーティングを引き起こす原因ともなる。
# 欠損値の再確認
num = missing_values1['地下室の状態'].T.values[0]
print(f'欠損値の数: {num}')
# 要素の確認
train_df['地下室の状態'].value_counts()
train_non = pd.DataFrame()
train_non = pd.get_dummies(train_df['地下室の状態'], prefix='地下室の状態', prefix_sep='_', drop_first = True, dummy_na=True)
train_non.shape
train_non.head(10)
まとめ
以上、様々なエンコーディング手法について解説してきました。
どれもメリット、デメリットがあり、有効な場面は限定される為、各手法の採用根拠を明確にして利用要否を判断していきたいと思います。
最後に
他の記事はこちらでまとめています。是非ご参照ください。
参考文献
統計Web 変数の尺度
南山大学 理工学研究センター - 最小二乗法による線形回帰分析
sklearn.feature_extraction.FeatureHasher
sklearn.preprocessing.OrdinalEncoder
解析結果
実装結果:GitHub/house_prices.ipynb
データセット:House Prices - Advanced Regression Techniques - kaggle
参考資料