見出し画像

Python機械学習プログラミング:第4章

今回は「Python機械学習プログラミング」の第4章を内容を簡単に記載します。

表形式のデータで欠損値を特定する

CSVファイルから単純なサンプルデータを作成します。

import pandas as pd
import numpy as np
from io import StringIO
# サンプルデータを作成
csv_data = '''A,B,C,D
              1.0,2.0,3.0,4.0
              5.0,6.0,,8.0
              10.0,11.0,12.0,
           '''
# サンプルデータを読み込み
df = pd.read_csv(StringIO(csv_data))
df

read_csv関数を使ってCSVフォーマットのデータをpandasのDataFrameオブジェクトに読み込むと、欠損している2つのセルがNaNに置き換えられることがわかります。isnullメソッドを使って欠損値をカウントしてみます。

# 各特徴量の欠損値をカウント
df.isnull().sum()

欠損値を持つ訓練データ/特徴量を取り除く

# 欠損行を含む行を削除
df.dropna()

dropnaメソッドには、便利な引数が他にもいくつか用意されてます。

# 全ての列がNaNである行だけを削除(全ての値がNaNである行はないため、配列全体が返される)
df.dropna(how='all')
# 非NaNが4つ未満の場合の行を削除
df.dropna(thresh=4)
# 特定の列(この場合は'C')にNaNが含まれてる行だけを削除
df.dropna(subset=['C'])

欠損値を補完する

欠損値を平均値で補完します。

df.fillna(df.mean())

pandasを使ったカテゴリデータのエンコーディング

問題を具体的に占める新しいDataFrameを作成します。

# サンプルデータを作成(Tシャツの色 - サイズ - 価格 - クラスラベル)
df = pd.DataFrame([
    ['green', 'M', 10.1, 'class2'],
    ['red', 'L', 13.5, 'class1'],
    ['blue', 'XL', 15.3, 'class2']])

# 列名を設定
df.columns = ['color', 'size', 'price', 'classlabel']
df

順序特徴量のマッピング

学習アルゴリズムに順序特徴量を正しく解釈させるには、カテゴリ文字列の値を整数に変換する必要があります。sizeにおいて「XL = L +1 = M + 2」の関係を数値に変換します。

# Tシャツのサイズと整数を対応させるディクショナリを生成
size_mapping = {'XL':3, 'L':2, 'M':1}
# Tシャツのサイズを整数に変換
df['size'] = df['size'].map(size_mapping)
df

整数値を元の文字列表現に戻してみます。

inv_size_mapping = {v: k for k, v in size_mapping.items()}
df['size'].map(inv_size_mapping)

クラスラベルのエンコーディング

多くの機械学習ライブラリは、クラスラベルが整数値としてエンコードされていることを要求します。classlabelもエンコーディングしてみます。

class_mapping = {label: idx for idx, label in enumerate(np.unique(df['classlabel']))}
class_mapping

マッピングディクショナリを作成したら次にようにクラスラベルを整数に変換します。

# クラスラベルを整数に変換
df['classlabel'] = df['classlabel'].map(class_mapping)
df

変換されたクラスラベルを元の文字位列表現に戻すには、マッピングディクショナリをキーとペアを逆の順にします。

inv_class_mapping = {v: k for k, v in class_mapping.items()}
df['classlabel'] = df['classlabel'].map(inv_class_mapping)
df

あるいは、sklearnで直接実装されているLabelEncoderという便利なクラスを使う手もあります。

from sklearn.preprocessing import LabelEncoder
# ラベルエンコーダのインスタンスを生成
class_le = LabelEncoder()
y = class_le.fit_transform(df['classlabel'].values)
y

fit_transformメソッドは、fitとtransromsを別々に呼び出すことに相当するショートカットです。整数のクラスラベルを元の文字列表現に変換するには、inverse_transformメソッドを使います。

class_le.inverse_transform(y)

名義特徴量でのone-hotエンコーディング

color列についても同じように変換してみる。

color_mapping = {label: idx for idx, label in enumerate(np.unique(df['color']))}
color_mapping

このコードを実行するとcolor列に格納されてる値が数値として認識できます。「blue : 0 / green : 1 / red : 2」となります。上手く変換できたと思いますが、この形式だと正しい前処理ではありません。色の値には順序がないが、この変換方法だと順序があるようになってしまいます。学習アルゴリズムではgreenがblueより大きく、redがgreenよりも大きいと認識してしまいます。この問題を回避する一般的な方法が、one-hotエンコーディングです。この手法は名義特徴量の列の一意な値ごとにダミー特徴量を新たに作成するという発想です。実装してみてみましょう。

from sklearn.preprocessing import OneHotEncoder
X = df[['color', 'size', 'price']].values
# one-hotエンコーダの生成
color_ohe = OneHotEncoder()
# one-hotエンコーディングを実行
color_ohe.fit_transform(X[:, 0].reshape(-1, 1)).toarray()

OneHotEncoderを1つの列だけに適用することで(X[:, 0].reshape(-1, 1))、配列の他の2列まで変更されてしまうのを防いでます。複数の特徴量からなる配列の列を選択的に変換したい場合は、ColumnTransformerを使うことができます。このクラスには「name,transformer,columns」タプルのリストを次のように指定します。

from sklearn.compose import ColumnTransformer
X = df[['color', 'size', 'price']].values
c_transf = ColumnTransformer([('onehot', OneHotEncoder(), [0]),
                                                         ('nothing', 'passthrough', [1, 2])])
c_transf.fit_transform(X).astype(float)

上記のコードでは'passthrough'引数を使って、最初の列だけ変更し、残りの2つの列は変更しないことを指定してます。one-hotエンコーディングを使ってダミー特徴量を作成する場合はpandasで実装されているget_dummies関数を使うとさらに便利です。get_dummies関数をDateFrameオブジェクトに適用すると、文字列値の持つ列だけが変換され、それ以外の列はそのままとなります。

# one-hotエンコーディングを実行します。
pd.get_dummies(df[['price', 'color', 'size']])

one-hotエンコーディングのデータセットを使うときは、逆行列を要求する手法などで「多重共線性」が発生する可能性に注意しないといけません。特徴量の間の相関を減らすには、one-hotエンコーディングの配列から特徴量の列を1つ削除すれば良いです。ただし、列を削除しても重要な情報は失われないことに注意しましょう。例えば。color_blue列を削除しても、その特徴量の情報は依然として残っています。color_green=0とcolor_red=0が観測されれば、その観測値がblueである事がわかります。get_dummies関数を使う場合は、drop_firstパラメータに引数としてTrueを渡すことで最初の列を削除できます。

# one-hotエンコーディングを実行します。
pd.get_dummies(df[['price', 'color', 'size']], drop_first=True)

3レコード目のcolorがblueと認識できます。

OneHotEncoderを使って冗長な列を削除するには、drop='first'とcaetgories='auto'を指定する必要があります。

# one-hotエンコーダの生成
color_ohe = OneHotEncoder(categories='auto', drop='first')
c_transf = ColumnTransformer([('onehot', color_ohe, [0]),
                                                         ('nothing','passthrough', [1,2])])
c_transf.fit_transform(X).astype(float)

順序特徴量のエンコーディング

size特徴量を2つの新しい特徴量"x > M"と"x > L"に分割してみます

df = pd.DataFrame([['green', 'M', 10.1, 'class2'],
                                    ['red', 'L', 13.5, 'class1'],
                                    ['blue', 'XL', 15.3, 'class2']])
df.columns = ['color', 'size', 'price', 'classlabel']
df
df['x > M'] = df['size'].apply(lambda x : 1 if x in ('L', 'XL') else 0)
df['x > L'] = df['size'].apply(lambda x : 1 if x == 'XL' else 0)
del df['size']
df

データセットを訓練データセットと、テストデータセットに分割する

# wineデータセットを読み込む
df_wine = pd.read_csv(
    'https://archive.ics.uci.edu/ml/machine-learning-databases/wine/wine.data', header=None)
# 列名を設定
df_wine.columns = ['Class label', 'Alcohol', 'Malic acid', 'Ash', 'Alcalinity of ash', 'Magnesium', 'Total phenols', 'Flavanoids', 'Nonflavanoid phenols',
                                   'Proanthocyanins', 'Color intensity', 'Hue', 'OD280/OD315 of diluted wines', 'Proline']
# クラスラベルを表示
print('Class labels', np.unique(df_wine['Class label']))
# wineデータセットの先頭5行を表示
df_wine.head()

これらのサンプルは、クラス1、2、3のいずれかに所属してます。これらの3つのクラスはイタリアの同じ地域で栽培されてる異なる品種の葡萄を表してます。このデータセットを訓練データとテストデータにランダム分割してみましょう。

from sklearn.model_selection import train_test_split
# 特徴量とクラスラベルを別々に抽出
X, y = df_wine.iloc[:, 1:].values, df_wine.iloc[:, 0].values
# 訓練データとテストデータに分割(全体の30%をテストデータにする)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=0, stratify=y)

特徴量の列1~13のNumpy配列を変数Xに代入し、最初の列のクラスラベルを変数yに代入してます。次にtrain_test_split関数を使ってXとyを訓練データセットとテストデータセットにランダムに分割してます。test_size=0.3に設定することで全体の30%をX_testとy_testに割り当て、残りの70%をX_trainとy_trainに割り当ててます。stratifyパラメータに引数としてクラスラベル配列yを渡す事で(層化サンプリングのためのクラスラベルを指定)、訓練データセットとテストデータセットのクラスの比率が元のデータセットと同じになるようにしてます。

特徴量の尺度を揃える

min-maxスケーリングを実装します。

from sklearn.preprocessing import MinMaxScaler
mms = MinMaxScaler()
# 訓練データセットをスケーリング
X_train_norm = mms.fit_transform(X_train)
# テストデータセットをスケールング
X_test_norm = mms.transform(X_test)

次に標準化を実行します。

from sklearn.preprocessing import StandardScaler
stdsc = StandardScaler()
X_train_std = stdsc.fit_transform(X_train)
X_test_std = stdsc.transform(X_test)

L1正則化を実行します。

from sklearn.linear_model import LogisticRegression
LogisticRegression(penalty='l1', solver='liblinesr', multi_class='ovr')

標準化されたWineデータにL1正則化付きロジスティック回帰を適用すると下記のようになります。

# L1ロジスティック回帰のインスタンスを生成:逆正則化パラメータC=1.0はデフォルト値
# 値を大きくしたり小さくすると正則化の効果得お強めたり弱めたりできる
lr = LogisticRegression(penalty='l1', C=1.0, solver='liblinear', multi_class='ovr')
# 訓練データに適合
lr.fit(X_train_std, y_train)
# 訓練データに対する正解率の表示
print('Training accuracy : ', lr.score(X_train_std, y_train))
# テストデータに対する正解率の表示
print('Test accuracy : ', lr.score(X_test_std, y_test))

訓練データセットとテストデータセットの正解率はどちらも100%であり、モデルが完璧に動作していることを示してます。lr.intercept_属性にアクセスすると3つの切片が返されます。

lr.intercept_

LogisticRegressionオブジェクトを多クラスのデータセットに適合させるために一対他(Ovr)アプローチを使っている。このため、1つ目の切片はクラス1対クラス2/3に適合するモデルの切片です。2つ目の値はクラス2対クラス1/3に適合するモデルの切片です。3つ目の値はクラス3対クラス1/2に適合するモデルの切片です。

# 重み係数の表示
lr.coef_

lr.coef_属性を使ってアクセスした重み配列は3行の重み係数を含んでおり、クラスごとに重みベクトルを1つ含んでることが分かります。各行は13この重みで構成されており、総入力を計算するには各重みに対して13次元のWineデータセット内の対応する特徴量を掛けます。

正則化の最後の例として、正則化の強さを変化させながら正則化パスをプロットしてみます。正則化パスは正則化の強さに対する特徴量の重み係数を表します。

import matplotlib.pyplot as plt
# 描画の準備
fig = plt.figure()
ax = plt.subplot(111)
# 各係数の色のリスト
colors = ['blue', 'green', 'red', 'cyan', 'magenta', 'yellow', 'black', 'pink', 'lightgreen', 'lightblue', 'gray', 'indigo', 'orange']
# からのリストを生成(重み係数、逆正則化パラメータ)
weights, params = [], []
# 逆正則化パラメータの値ごとに処理
for c in np.arange(-4, 6.):
    lr = LogisticRegression(penalty='l1', C=10.**c, solver='liblinear', multi_class='ovr', random_state=0)
    lr.fit(X_train_std, y_train)
    weights.append(lr.coef_[1])
    params.append(10**c)

# 重み係数をNumPy配列に変換
weights = np.array(weights)
# 各重み係数をプロット
for column, color in zip(range(weights.shape[1]), colors):
    # 横軸を逆正則化パラメータ、縦軸を重み係数とした折れ線グラフ
    plt.plot(params, weights[:, column], label=df_wine.columns[column+1], color = color)

# y=0に黒い波線を引く
plt.axhline(0, color='black', linestyle='--', linewidth=3)
# 横軸の範囲の設定
plt.xlim([10**(-5), 10**5])
# 軸のラベルの設定
plt.ylabel('weight coefficient')
plt.xlabel('C')
# 横軸の大数スケールを設定
plt.xscale('log'),
plt.legend(loc='upper left')
ax.legend(loc='upper center', bbox_to_anchor=(1.38, 1.03), ncol=1, fancybox=True)
plt.show()

正則化パラメータの強さを強めて(C < 0.01)、モデルにペナルティを科した場合、特徴量の重みは全て0になります。 Cは逆正則化パラメータの逆数です。

逐次特徴量選択アルゴリズム

次元削減法は主に「特徴量選択」と「特徴量抽出」の2つに分かれます。特徴量選択では元の特徴量の一部を選択し、特徴量抽出では新しい特徴量部分空間を生成するために特徴量の集合から情報を抽出します。逐次特徴量選択アルゴリズムは元々の「d次元」の特徴量空間を「k次元」の特徴量部分空間に削減するために使われます。逐次特徴量選択の目的は2つあり、1つは計算効率の改善、もう1つは無関係の特徴量やノイズを取り除くことでモデルの汎化誤差を削減する事です。後者は正則化をサポートしてないアルゴリズムに役立ちます。

逐次後退選択(SBS)は典型的な逐次特徴量選択アルゴリズムです。SBSの目的は元々の特徴量空間の次元を減らすことで、分類器の性能の低下を最小限に抑えた上で計算効率を改善します。モデルが過学習に陥っている場合にSBSを適用することで、モデルの予測性能を改善できる事があります。Pythonで1から実装してみます。

from sklearn.base import clone
from itertools import combinations
import numpy as np
from sklearn.metrics import accuracy_score
from sklearn.model_selection import train_test_split

class SBS():
    """
    逐次後退選択を実行するクラス
    """
    def __init__(self, estimator, k_features, scoring=accuracy_score, test_size=0.25, random_state=1):
        self.scoring = scoring                          # 特徴量を評価する指標
        self.estimator = clone(estimator)     # 推定器
        self.k_features = k_features             # 選択する特徴量の個数
        self.test_size = test_size                  # テストデータの割合
        self.random_state = random_state # 乱数シードの固定
        
    def fit(self, X, y):
        # 訓練データとテストデータに分割
        X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=self.test_size, random_state=self.random_state)
        # 全ての特徴量の個数、列インデックス
        dim = X_tarain.shape[1]
        self.indices_ = tuple(range(dim))
        self.subsets_ = [self.indices_]
        # 全ての特徴量を用いてスコアを算出
        score = self._calc_score(X_train, y_train, X_test, y_test, self.indices_)
        self.scores_ = [score] # スコアを格納
        # 特徴量の部分集合を表す列員で食う酢の組み合わせごとに処理を反復
        while dim > self.k_features:
            scores = []              # 空のスコアリストを作成
            subsets = []            # 空の列インデックスリストを作成
            # 特徴量の部分集合を表すインデックスの組み合わせごとに処理を反復
            for p in combinations(self.indices_, r=dim - 1):
                # スコアを算出して格納
                score = self._calc_score(X_train, y_train, X_test, y_test, p)
                scores.append(score)
                # 特徴量の部分集合を表す列インデックスのリストを格納
                subsets.append(p)
                
            # 最良のスコアのインデックスを抽出
            best = np.argmax(scores)
            # 最良のスコアとなる列インデックスを抽出して格納
            self.indices_ = subsets[best]
            self.subsets_.append(self.indices_)
            # 特徴量の個数を1つだけ減らして次のステップへ
            dim -= 1
            # スコアを格納
            self.scores_.append(scores[best])
        # 最後に格納したスコア
        self.k_score_ = self.scores_[-1]
        return self
    def transform(self, X):
        # 抽出した特徴量を返す
        return X[:, self.indices_]
    
    def _calc_score(self, X_train, y_train, X_test, y_test, indices):
        # 指定された列番号indicesの特徴量を抽出してモデルを適合
        self.estimator.fit(X_train[:, indices], y_train)
        # テストデータを用いてクラスラベルを予測
        y_pred = self.estimator.predict(X_test[:, indices])
        # 真のクラスラベルごと予測を用いてスコアを算出
        score = self.scoring(y_test, y_pred)
        return score

この実装では選択する特徴量の個数を指定するためにk_featuresパラメータを定義してます。デフォルトではモデルの性能の評価にsklearnのaccuracy_scoreを使い、特徴量の部分集合に対する分類問題に推定器を使用してます。fitメソッドのwhileループではitertools.combinations関数によって作成された特徴量の部分集合を評価し、特徴量が目的の次元数になるまで削減してます。イテレーションの度に内部で作成されたテストデータセットX_testに対して、特徴量の組み合わせを変えた時に最も良い正解率をリスト slef.scores_に集めます。これらの正解率は後で結果を評価するために使います。最終的に選択された特徴量の部分集合の列インデックスはself.indicces_に代入します。このインデックスは選択された特徴量の列を持つ新しいデータ配列を取得するためにtransformメソッドで使われます。fitメソッドの中で個々の特徴量の評価を明示的に行うのではなく、最も良い正解率を示す特徴量を示す特徴量の集合に含まれていない特徴量を削除するだけです。sklearnのKNN分類器にSBSを実装します。

from sklearn.neighbors import KNeighborsClassifier
import matplotlib.pyplot as plt
# k最近傍法分類器のインスタンスを生成(近傍点数=5)
knn = KNeighborsClassifier(n_neighbors=5)
# 逐次後退選択のインスタンスを生成(特徴量の個数が1になるまで特徴量を選択)
sbs = SBS(knn, k_features=1)
sbs.fit(X_train_std, y_train)

KNN分類器の正解率の可視化をします。

# 特徴量の個数のリスト
k_feat = [len(k) for k in sbs.subsets_]
# 横軸を特徴量の個数、縦軸をスコアとした折れ線グラフのプロット
plt.plot(k_feat, sbs.scores_, marker='o')
plt.ylim([0.7, 1.02])
plt.ylabel('Accuracy')
plt.xlabel('Number of features')
plt.grid()
plt.tight_layout()
plt.show()

特徴量の個数を減らしたためデータセットに対するKNN分類器の正解率が改善されてます。最小限の特徴量部分集合(k = 3)が何であるか確認してみます。

k3 = list(sbs.subsets_[10])
print(df_wine.columns[1:][k3])

sbs.subsets_属性の11番目の位置から、3つの特徴量からなる部分集合の列インデックスを取得してます。この列インデックスをpandasのWineデータセットのDattaFrameオブジェクトに指定することで対応する特徴量の名前を取得してます。次に、元のテストデータセットでKNN分類器の性能を評価します。

# 13個全ての特徴量を用いてモデルを適合
knn.fit(X_train_std, y_train)
# 訓練の正解率を出力
print('Training accuracy', knn.score(X_train_std, y_train))

# テストの正解率を出力
print('Test accuraby', knn.score(X_test_std, y_test))

特徴量全体を使った場合、訓練データセットでは約97%、テストデータセットでは約96%の正解率が得られました。この結果から、新しいデータに対するモデルの汎化性能が上々だという事がわかります。次に、3つの特徴量からなる部分集合を使ってKNNの性能を調べます。

# 3個の特徴量を用いてモデルを適合
knn.fit(X_train_std[:, k3], y_train)
# 訓練の正解率を出力
print('Training accuracy', knn.score(X_train_std[:, k3], y_train))

# テストの正解率を出力
print('Test accuraby', knn.score(X_test_std[:, k3], y_test))

特徴量の個数を減らしてもKNNモデルの性能は改善されなかったが、データセットのサイズを小さくするとデータ収集ステップでコストがかかりがちな現実のアプリケーションで役立つ可能性があります。また、特徴量の個数を大胆に減らすと、解釈しやすい単純なモデルが得られます。

ランダムフォレストで特徴量の重要度を評価する

WIneデータセットで500本の決定木を訓練し、13個の特徴量をそれぞれの重要性を表す指標に基づいてランク付します。ランダムフォレストなどのツリーベースのモデルでは特徴量の標準化や正則化は必要ないことを思い出しましょう。

from sklearn.ensemble import RandomForestClassifier
# Wineデータセットの特徴量の名称
feat_labels = df_wine.columns[1:]
# ランダムフォレストオブジェクトの生成(決定木の個数=500)
forest = RandomForestClassifier(n_estimators=500, random_state=1)
# モデルを適合
forest.fit(X_train, y_train)
# 特徴量の重要度を摘出
importances = forest.feature_importances_
# 重要度の降順で特徴量のインデックスを抽出
indices = np.argsort(importances)[::-1]
# 重要度の降順で特徴量の名称、重要度を表示
for f in range(X_train.shape[1]):
    print("%2d) %-*s %f" %
          (f + 1, 30, feat_labels[indices[f]], importances[indices[f]]))
plt.title('Feature Importances')
plt.bar(range(X_train.shape[1]), importances[indices], align='center')
plt.xticks(range(X_train.shape[1]), feat_labels[indices], rotation=90)
plt.xlim([-1, X_train.shape[1]])
plt.tight_layout()
plt.show()

このコードを実行するとWineデータセットの様々な特徴量をそれらの相対的な重要度でランクづけしたグラフが作成されます。特徴量の重要度が合計して1.0になるように正則化されてる事に注意しましょう。

上記のグラフを参照すると、「proline. flabonoids, colorintensity, 希釈ワインのOD280/OD315, alcohol」の5つであると結論付けられます。

最後にsklearnにSelectFromModelクラスについて触れます。このクラスはモデルを適合させた後にユーザーが指定の閾値以上の重要度を持つ特徴量を選択します。これが役立つのはsklearnのpipelineオブジェクトの特徴量選択器と中間ステップとしてRandomForestClassifierを使いたい場合です。それにより、様々な前処理ステップを推定器として結びつける事ができます。例えば、閾値を0.1に設定することでデータセットを最も重要な5つの特徴量に絞り込む事ができます。

from sklearn.feature_selection import SelectFromModel
# 特徴量オブジェクトを生成(重要度の閾値を0.1に設定)
sfm = SelectFromModel(forest, threshold=0.1, prefit=True)
# 特徴量を抽出
X_selected = sfm.transform(X_train)
print('NUmber of features that meet this threshold criterion : ', X_selected.shape[1])

for f in range(X_selected.shape[1]):
    print("%2d) %-*s %f" % (f + 1, 30, feat_labels[indices[f]], importances[indices[f]]))

まとめ

本章では欠測データを正しく処理するための有益な手法を調べました。データを機械学習アルゴリズムに入力するにはカテゴリ変数を正しく符号化しておく必要があります。そこで順序特徴量と名義特徴量の値を整数表現にマッピングする方法を確認しました。さらにL1正則化についても簡単に説明しました。L1正則化はモデルの複雑さを低減することにより、過学習を回避するのに役立ちます。無関係な特徴量を削除するもう1つの手法として逐次特徴量選択アルゴリズムを使ってデータセットから有益な特徴量を選択しました。


サポートして頂いたお金は開業資金に充てさせて頂きます。 目標は自転車好きが集まる場所を作る事です。 お気持ち程度でいいのでサポートお願い致します!