PythonによるScikit-Learnを用いた次元削減に関する研究

機械学習において,モデルの性能が向上するのはある時点までで,それ以後は素性が増えるごとに性能が向上する.モデルに投入する素性が増えれば増えるほど、データの次元は増加する。次元が増加すると、オーバーフィットの可能性が高くなる。

オーバーフィッティングに対抗するための手法は複数あるが、次元削減は最も効果的な手法の一つである。次元削減は、特徴空間の最も重要な成分を選択し、それを保存して、他の成分を削除する。

なぜ次元削減が必要なのか?

機械学習において次元削減が行われる理由はいくつかある:計算コストと戦うため、オーバーフィットを制御するため、高次元のデータセットを可視化し解釈するためである。

機械学習では、データセットに含まれる素性が多いほど、分類器の学習能力が向上することがよくある。しかし、特徴が多いということは、計算コストが高くなるということでもある。次元が大きいと学習時間が長くなるだけでなく、特徴量が多いと、アルゴリズムがデータ中のすべての特徴を説明するモデルを作ろうとするため、オーバーフィットにつながることが多い。

次元削減は特徴の総数を減らすので、モデルの学習に伴う計算負荷を軽減するだけでなく、モデルに与える特徴をかなり単純に保つことでオーバーフィットに対抗することができる。

次元削減は教師あり学習と教師なし学習の両方の文脈で使用することができる。教師なし学習の場合、次元削減は特徴選択または特徴抽出を行うことでデータの前処理を行うために用いられることが多い。

教師なし学習の次元削減には、主成分分析(PCA)と特異値分解(SVD)という主なアルゴリズムが使われる。

教師あり学習の場合,機械学習分類器に入力する特徴を単純化するために次元削減を行うことができる.教師あり学習の問題で次元削減を行うために最もよく使われる方法は、線形判別分析(LDA)とPCAであり、新しい事例を予測するために活用することができる。

上記のユースケースは一般的なユースケースであり、これらの手法が使われる唯一の条件ではないことに注意されたい。結局のところ、次元削減技術は統計的手法であり、その使用は機械学習モデルによって制限されるものではない。

それでは、最も一般的な次元削減手法の背景にある考え方をそれぞれ説明することにしよう。

主成分分析

主成分分析(PCA)とは、データセットの特徴を分析することで、データの新しい特徴や特性を作り出す統計手法である。基本的には、データの特徴をまとめたり、組み合わせたりする。主成分分析は、データを高次元の空間からわずか数次元に「押し込める」ものと考えることもできる。

具体的には、ある飲み物は多くの特徴によって記述されるが、その特徴の多くは冗長であり、その飲み物を特定するには比較的役立たない。ワインをエアレーション、C02レベルなどの特徴で説明するよりも、色、味、年数で説明する方が簡単である。

主成分分析では、データセットの「主要な」または最も影響力のある特徴を選択し、それらに基づいて特徴を作成します。データセットに最も影響を与える特徴のみを選択することで、次元を削減することができる。

PCAでは、新しい特徴を作成する際に変数間の相関関係を保持します。この手法で作成される主成分は、元の変数の線形結合であり、固有ベクトルと呼ばれる概念で計算される。

新しい成分は直交している、つまり互いに無関係であると仮定される。

PCA実装例

Scikit-LearnでPCAがどのように実装されるか見てみましょう。ここでは、Mushroom分類データセットを使用します。

まず、PCA、train_test_splitラベリングとスケーリングツールを含む必要なモジュールをすべてインポートする必要があります。

import pandas as pd
import matplotlib.pyplot as plt
from sklearn.preprocessing import LabelEncoder, StandardScaler
from sklearn.decomposition import PCA
from sklearn.model_selection import train_test_split
import warnings
warnings.filterwarnings("ignore")


データをロードした後、NULL値をチェックします。また、LabelEncoderでデータをエンコードします。クラスの特徴はデータセットの最初のカラムなので、それに合わせて特徴とラベルを分割する。

m_data = pd.read_csv('mushrooms.csv')


# Machine learning systems work with integers, we need to encode these
# string characters into ints


encoder = LabelEncoder()


# Now apply the transformation to all the columns:
for col in m_data.columns:
    m_data[col] = encoder.fit_transform(m_data[col])


X_features = m_data.iloc[:,1:23]
y_label = m_data.iloc[:, 0]


次に、標準的なスケーラを使用して、特徴量をスケーリングします。実際に分類器を実行するわけではないので、これはオプションですが、PCAによるデータの解析方法に影響を与えるかもしれません。

# Scale the features
scaler = StandardScaler()
X_features = scaler.fit_transform(X_features)


ここでPCAを用いて特徴量のリストを取得し、どの特徴量が最も説明力があるか、または最も分散が大きいかをプロットします。これが主成分です。17個か18個の素性で、データの大部分(95%近く)を説明しているようです。

# Visualize
pca = PCA()
pca.fit_transform(X_features)
pca_variance = pca.explained_variance_


plt.figure(figsize=(8, 6))
plt.bar(range(22), pca_variance, alpha=0.5, align='center', label='individual variance')
plt.legend()
plt.ylabel('Variance ratio')
plt.xlabel('Principal components')
plt.show()


特徴量を上位17個の特徴量に変換してみましょう。そして、この17の特徴量に基づくデータポイントの分類を散布図にします。

pca2 = PCA(n_components=17)
pca2.fit(X_features)
x_3d = pca2.transform(X_features)


plt.figure(figsize=(8,6))
plt.scatter(x_3d[:,0], x_3d[:,5], c=m_data['class'])
plt.show()


また、上位2つの特徴量についても同様に行い、分類がどのように変化するかを見てみる。

pca3 = PCA(n_components=2)
pca3.fit(X_features)
x_3d = pca3.transform(X_features)


plt.figure(figsize=(8,6))
plt.scatter(x_3d[:,0], x_3d[:,1], c=m_data['class'])
plt.show()


特異値分解

特異値分解の目的は、行列を単純化し、その行列を使った計算を容易にすることです。PCAの目的と同様に、行列は構成要素に還元されます。SVDの内部と外部を理解することは、機械学習モデルへの実装に完全に必要というわけではないが、その仕組みの直感を持つことで、いつ使うべきかをよりよく理解できるようになる。

SVDは複素数または実数値の行列に対して実行できますが、この説明を理解しやすくするために、実数値の行列を分解する方法について説明します。

SVDを行う場合、データで埋め尽くされた行列があり、その行列が持つ列の数を減らしたいということがあります。これにより、データのばらつきをできるだけ残しつつ、行列の次元を小さくすることができます。

行列Aは行列Vの転置に等しいと言えます。

A=U*D*VtA=U*D*Vt
A=U*D*Vt A=U*D*Vt

行列 A は元の x×y 要素を持ち、行列 U は x×x 要素を含む直交行列、行列 V は y×y 要素を含む別の直交行列である。最後に、Dはx*y要素を含む対角行列である。

行列の値の分解は、元の行列の特異値を新しい行列の対角値に変換することを意味します。直交行列は他の数値を乗じてもその性質が変わらないので、この性質を利用して行列Aの近似値を求めることができます。直交行列を掛け合わせると、行列Vの転置と相まって、元の行列Aと等価な行列が得られます。

行列AをU、D、Vに分解すると、行列Aの情報を含む3種類の行列ができあがります。

行列の左端の列がデータの大部分を占めることがわかり、これらの数列を選択することで、行列Aの近似行列を得ることができるのです。この新しい行列は、次元数がはるかに少ないので、より単純で扱いやすくなっています。

SVDの実装例

SVD が最もよく使われる方法の 1 つは、画像の圧縮です。結局のところ、画像の赤、緑、青のチャンネルを構成するピクセル値を減らすだけで、結果は、より複雑でなく、同じ画像内容を含む画像になります。それでは、SVDを使って画像を圧縮し、レンダリングしてみましょう。

画像の圧縮を行うためにいくつかの関数を使います。NumpyにはSVDの計算を行うメソッドがあるので、これを実現するにはNumpyとPILライブラリの Image 関数だけが必要です。

import numpy
from PIL import Image


まず、画像を読み込んでNumpyの配列に変換する関数を書きます。そして、画像から赤、緑、青の3つのカラーチャンネルを選択します。

def load_image(image):
    image = Image.open(image)
    im_array = numpy.array(image)


red = im_array[:, :, 0]
    green = im_array[:, :, 1]
    blue = im_array[:, :, 2]


return red, green, blue


さて、色が揃ったところで、色チャンネルを圧縮する必要があります。まず、必要な色チャンネルに対してNumpyのSVD関数を呼び出します。次に、ゼロの配列を作成します。これは行列の乗算が完了した後に埋めるものです。そして、計算時に使用する特異値の上限を指定します。

def channel_compress(color_channel, singular_value_limit):
    u, s, v = numpy.linalg.svd(color_channel)
    compressed = numpy.zeros((color_channel.shape[0], color_channel.shape[1]))
    n = singular_value_limit


left_matrix = numpy.matmul(u[:, 0:n], numpy.diag(s)[0:n, 0:n])
    inner_compressed = numpy.matmul(left_matrix, v[0:n, :])
    compressed = inner_compressed.astype('uint8')
    return compressed


red, green, blue = load_image("dog3.jpg")
singular_val_lim = 350


この後、上記のように対角線とU行列の値域に対して行列の掛け算を行います。これで左の行列ができあがり、これにV行列を掛け合わせます。これで圧縮された値が得られるはずで、これを「uint8」型に変換する。

def compress_image(red, green, blue, singular_val_lim):
    compressed_red = channel_compress(red, singular_val_lim)
    compressed_green = channel_compress(green, singular_val_lim)
    compressed_blue = channel_compress(blue, singular_val_lim)


im_red = Image.fromarray(compressed_red)
    im_blue = Image.fromarray(compressed_blue)
    im_green = Image.fromarray(compressed_green)


new_image = Image.merge("RGB", (im_red, im_green, im_blue))
    new_image.show()
    new_image.save("dog3-edited.jpg")


compress_image(red, green, blue, singular_val_lim)


この犬の画像を使ってSVD圧縮のテストを行います。

また、使用する特異値の上限を設定する必要がありますが、ここでは600で始めます。

red, green, blue = load_image("dog.jpg")
singular_val_lim = 350


最後に、3つの色チャンネルの圧縮値を取得し、PILを使用してNumpy配列から画像コンポーネントに変換することができます。あとは3つのチャンネルを結合して、画像を表示するだけです。この画像は元の画像より少し小さく、シンプルになるはずです。

確かに、画像の大きさを調べてみると、非可逆圧縮も少ししていますが、圧縮した方が小さくなっていることがわかります。また、画像にはノイズも見られます。

特異値の制限を調整することで遊べます。しかし、ある時点で画像のアーチファクトが現れ、画像の品質が低下します。

def compress_image(red, green, blue, singular_val_lim):
    compressed_red = channel_compress(red, singular_val_lim)
    compressed_green = channel_compress(green, singular_val_lim)
    compressed_blue = channel_compress(blue, singular_val_lim)


im_red = Image.fromarray(compressed_red)
    im_blue = Image.fromarray(compressed_blue)
    im_green = Image.fromarray(compressed_green)


new_image = Image.merge("RGB", (im_red, im_green, im_blue))
    new_image.show()


compress_image(red, green, blue, singular_val_lim)


線 形 判 別 分 析

線形判別分析は、多次元グラフのデータを線形グラフに投影することで動作します。最もイメージしやすいのは、2つの異なるクラスのデータ点で埋め尽くされたグラフである。データを2つのクラスにきれいに分ける線がないと仮定すると、2次元のグラフは1次元のグラフに縮小される。この1次元グラフを用いることで、データポイントの分離を最適化することができる。

LDAの主な目的は、2つのクラスの分散を最小化することと、2つのデータクラスの平均間の距離を最大化することの2つである。

これを達成するために、2Dグラフに新しい軸がプロットされます。この新しい軸は、先に述べた基準に基づいて2つのデータポイントを分離する必要があります。新しい軸が作成されると、2Dグラフ内のデータポイントは、新しい軸に沿って再描画されます。

LDAは、元のグラフを新しい軸に移動させるために、3つの異なるステップを実行する。まず、クラス間の分離性が計算されなければならないが、これはクラス平均間の距離またはクラス間分散に基づくものである。次のステップでは,クラス内分散を計算する必要があり,これは異なるクラスの平均と標本との間の距離である.最後に,クラス間分散を最大化する低次元空間を構築しなければならない.

LDAは,クラスの平均が互いに離れているときに最もよく機能する.分布の平均が共有されている場合、LDAは新しい直線軸でクラスを分離することができません。

LDAの実装例

最後に、LDA を使ってどのように次元削減を行うかを見てみましょう。なお、LDAは次元削減だけでなく、分類アルゴリズムとしても利用できる。

以下の例では、タイタニックデータセットを使用します。

まず、必要なインポートを行うことから始めましょう。

import pandas as pd
import numpy as np
from sklearn.metrics import accuracy_score, f1_score
from sklearn.preprocessing import LabelEncoder, StandardScaler
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis as LDA
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression


次に、学習データを読み込みます。学習セットと検証セットに分けます。

しかし、最初に少しデータの前処理をする必要があります。NameCabinTicketのカラムは、あまり有用な情報を持っていないので削除しましょう。また、欠損データを埋める必要があります。Ageの特徴の場合は中央値で、Embarkedの特徴の場合はS` で置き換えることにします。

training_data = pd.read_csv("train.csv")


# Let's drop the cabin and ticket columns
training_data.drop(labels=['Cabin', 'Ticket'], axis=1, inplace=True)


training_data["Age"].fillna(training_data["Age"].median(), inplace=True)
training_data["Embarked"].fillna("S", inplace=True)


また、数値以外の特徴量もエンコードする必要があります。ここでは、 SexEmbarked の両方のカラムをエンコードすることにします。名前`カラムも分類に使えそうにないので、削除しましょう。

encoder_1 = LabelEncoder()


# Fit the encoder on the data
encoder_1.fit(training_data["Sex"])


# Transform and replace the training data
training_sex_encoded = encoder_1.transform(training_data["Sex"])
training_data["Sex"] = training_sex_encoded


encoder_2 = LabelEncoder()
encoder_2.fit(training_data["Embarked"])


training_embarked_encoded = encoder_2.transform(training_data["Embarked"])
training_data["Embarked"] = training_embarked_encoded


# Assume the name is going to be useless and drop it
training_data.drop("Name", axis=1, inplace=True)


値をスケーリングする必要がありますが、 Scaler ツールは配列を受け取るので、形を変えたい値はまず配列に変換する必要があります。その後、データをスケーリングすることができます。

# Remember that the scaler takes arrays
ages_train = np.array(training_data["Age"]).reshape(-1, 1)
fares_train = np.array(training_data["Fare"]).reshape(-1, 1)


scaler = StandardScaler()


training_data["Age"] = scaler.fit_transform(ages_train)
training_data["Fare"] = scaler.fit_transform(fares_train)


# Now to select our training and testing data
features = training_data.drop(labels=['PassengerId', 'Survived'], axis=1)
labels = training_data['Survived']


あとは学習用の特徴量とラベルを選択し、 train_test_split を用いて学習データと検証データを作成すればよい。LDAを使った分類は簡単で、Scikit-Learnの他の分類器と同じように扱えます。

学習データに対して関数をフィットさせ、検証データ/テストデータに対して予測をさせるだけです。そして、実際の値に対する予測値のメトリクスを出力することができます。

X_train, X_val, y_train, y_val = train_test_split(features, labels, test_size=0.2, random_state=27)


model = LDA()
model.fit(X_train, y_train)
preds = model.predict(X_val)
acc = accuracy_score(y_val, preds)
f1 = f1_score(y_val, preds)


print("Accuracy: {}".format(acc))
print("F1 Score: {}".format(f1))


プリントアウトはこんな感じです。

Accuracy: 0.8100558659217877
F1 Score: 0.734375


データを変換して次元を減らす場合、まずデータに対してロジスティック回帰分類器を実行し、次元削減前の性能を確認しましょう。

logreg_clf = LogisticRegression()
logreg_clf.fit(X_train, y_train)
preds = logreg_clf.predict(X_val)
acc = accuracy_score(y_val, preds)
f1 = f1_score(y_val, preds)


print("Accuracy: {}".format(acc))
print("F1 Score: {}".format(f1))


その結果がこちらです。

Accuracy: 0.8100558659217877
F1 Score: 0.734375


ここで、LDAに必要な成分の数を指定してデータの特徴を変換し、その特徴とラベルにモデルを当てはめます。あとは特徴量を変換して、新しい変数に保存するだけです。元の特徴量と削減した特徴量を出力してみましょう。

LDA_transform = LDA(n_components=1)
LDA_transform.fit(features, labels)
features_new = LDA_transform.transform(features)


# Print the number of features
print('Original feature #:', features.shape[1])
print('Reduced feature #:', features_new.shape[1])


# Print the ratio of explained variance
print(LDA_transform.explained_variance_ratio_)


上のコードのプリントアウトは以下の通り。

Original feature #: 7
Reduced feature #: 1
[1.]


あとは、新しい素性で再度train/test splitを行い、分類器を再度実行して性能がどのように変化したかを確認するのみです。

X_train, X_val, y_train, y_val = train_test_split(features_new, labels, test_size=0.2, random_state=27)


logreg_clf = LogisticRegression()
logreg_clf.fit(X_train, y_train)
preds = logreg_clf.predict(X_val)
acc = accuracy_score(y_val, preds)
f1 = f1_score(y_val, preds)


print("Accuracy: {}".format(acc))
print("F1 Score: {}".format(f1))


Accuracy: 0.8212290502793296
F1 Score: 0.7500000000000001


結論

次元削減手法の主な方法を説明しました。主成分分析、特異値分解、線形判別分析です。これらは、機械学習モデルの性能を向上させ、オーバーフィッティングに対処し、データ分析を支援するために使用できる統計的手法です。

これら3つの手法は最もよく使われる次元削減手法であるが、他にもある。他の次元削減手法としては、カーネル近似やアイソマップスペクトル埋め込みなどがある。

タイトルとURLをコピーしました