機械学習 実践(教師なし学習)
前章までは、入力値と目標値をセットで持つ問題設定に対して用いる教師あり学習を扱いました。しかし、すべてのデータが目標値(答え)を持つわけではありません。
そこで本章では、目標値を持たない問題設定に対して用いる教師なし学習を扱います。教師なし学習には 2 つの代表的な手法があり、どちらもデータの背後に存在する本質的な構造を抽出する際に用います。
本章の構成
- 主成分分析 (Principal Component Analysis)
- k-平均法 (k-means)
主成分分析 (Principal Component Analysis)
主成分分析 (Principal Component Analysis) は次元削減 (Dimensionality reduction) の手法です。次元削減とは、例えば 4 次元のデータ(列数が 4 つのデータ)があった場合、2 次元などの低次元に落とし込むことを指します。また、一般的に次元削減は単にデータを削除するのではなく、可能な限り元のデータの情報を保持したまま、低次元のデータに変形を行います。
中身のイメージは上記の図です。この次元削減の 1 つの手法が主成分分析といいます。主成分分析とは何ですか、と問われたら以下の 2 つを頭に浮かべましょう。
- 可視化したいときに用いる手法
- 高次元(多次元)のデータを低次元化する手法
それでは一連の流れを実装します。頭の中でイメージすることを目標に進めていきましょう。
データセットの準備
データセットは scikit-learn にデフォルトで用意されているアヤメ (Iris) のデータセットを使用します。
import numpy as np
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
# データの読み込み
from sklearn.datasets import load_iris
dataset = load_iris()
x = dataset.data
t = dataset.target
feature_names = dataset.feature_names
# データの確認
pd.DataFrame(x, columns=feature_names).head()
sepal length (cm) | sepal width (cm) | petal length (cm) | petal width (cm) | |
---|---|---|---|---|
0 | 5.1 | 3.5 | 1.4 | 0.2 |
1 | 4.9 | 3.0 | 1.4 | 0.2 |
2 | 4.7 | 3.2 | 1.3 | 0.2 |
3 | 4.6 | 3.1 | 1.5 | 0.2 |
4 | 5.0 | 3.6 | 1.4 | 0.2 |
元のデータは 4 次元であることが確認できます。このデータを主成分分析を用いて、2 次元に落とし込みましょう。主成分分析の実装もこれまでの実装方法と同じになります。
モデルの定義・学習
# モデルの定義
from sklearn.decomposition import PCA
pca = PCA(n_components=2, random_state=0)
n_components
で指定しているのは、次元削減後の次元数です。今回は 4 次元から 2 次元に次元削減します。理由としては、2 次元であればデータを可視化することが可能というメリットがある点にあります。
モデルの学習では主成分分析を適用するために必要な分散を算出します。教師なし学習は正解となる指標がありません。今回であれば、データを低次元でも十分に表すことのできる方向を見つける必要があります。主成分分析では実測値の分散を用いて求めます。
# モデルの学習
pca.fit(x)
# 分散の確認
pca.get_covariance()
scikit-learn を用いると、.fit()
メソッドを実行すると分散を算出します。今回は n_components=2
としているので、第二主成分まで出力します。
しかしまだデータは変換されていません。求めた主成分へと写像するには transform
メソッド を使用します。ここで写像という新しい概念が登場しましたが、下記の図のように元のデータを主成分軸上へと写すことを写像といいます。第二主成分は第一主成分と直交するような関係になります。
イメージを掴む事ができたら、実装しましょう。
# 主成分分析の適用
x_transformed = pca.transform(x)
# 主成分分析適用後のデータの確認
pd.DataFrame(x_transformed, columns=['第一主成分', '第二主成分']).head(10)
第一主成分 | 第二主成分 | |
---|---|---|
0 | -2.684126 | 0.319397 |
1 | -2.714142 | -0.177001 |
2 | -2.888991 | -0.144949 |
3 | -2.745343 | -0.318299 |
4 | -2.728717 | 0.326755 |
5 | -2.280860 | 0.741330 |
6 | -2.820538 | -0.089461 |
7 | -2.626145 | 0.163385 |
8 | -2.886383 | -0.578312 |
9 | -2.672756 | -0.113774 |
データが 2 次元に落とし込まれていることが確認できました。
1 列目のデータを第一主成分と呼び、2 列目を第二主成分と呼びます。それぞれの列は次元削減前の情報を保持しており、それぞれの列が保持する元のデータの情報の割合を寄与率 (Proportion of the variance) と呼びます。寄与率は fit()
メソッド後の explained_variance_ratio_
属性からそれぞれの寄与率を確認することができます。
print('第一主成分の寄与率:{}'.format(pca.explained_variance_ratio_[0]))
print('第二主成分の寄与率:{}'.format(pca.explained_variance_ratio_[1]))
第一主成分は 92% 、第二主成分は 5% ほどの寄与率であることが確認できます。すなわち 97% 程度の割合で元のデータの情報を保持したまま次元削減できていることが確認できます。
このように、主成分分析は 100% 情報を保持したまま次元削減を行うのではなく、いくらかの情報は保持できていないことがわかります。主成分分析を適用した後にはこの寄与率を確認し、元のデータをどの程度再現できているのかを確認することが重要です。
次元削減後のデータを可視化しましょう。2 次元に次元削減したのにも高次元のデータをイメージが湧くように可視化したいという想いがあります。今回はアヤメの花の種類が 3 種類のデータセットを使用しています。可視化を行う際にそれぞれのクラスに色をつけて表示し、次元削減後のデータを確認してみましょう。
# 0, 1, 2 の 3 つのクラスがあることを確認
np.unique(t)
# 次元削減後のデータを可視化
sns.scatterplot(x_transformed[:, 0], x_transformed[:, 1],
hue=t, palette=sns.color_palette(n_colors=3));
元のデータセットでは 4 次元のため可視化を行うことができませんでしたが、次元削減を行うことにより、データセットを可視化することができました。このように可視化することにより、手元のデータを直感的に理解できるということは重要です。
例えば今回の可視化結果から、このデータセットで分類を行った場合、クラス 1(オレンジ)とクラス 2(緑)は分類することが少し困難であることが想定することができます。また、クラス 0(青)に関しては機械学習を用いることなく、分類を行うことも可能であることがわかります。
次元削減を行うことにより、可視化困難であったデータを可視化することができるということは、大きなメリットがありそうです。
標準化の適用
ここで一点、主成分分析を使う際の注意点があります。主成分分析を行う際には、標準化を必ず行いましょう。標準化とは、平均を 0、分散(標準偏差)を 1 とする操作でした。こちらは主成分分析が分散の最大化させるように主成分を決めていることが背景にあります。詳しくは説明しませんが、
- 主成分分析では必ず変数の標準化を行う
と最初は覚えましょう。理論背景の詳細が知りたい方はこちらを参考にしてください。
それでは可視化までの流れを実装します。
# 標準化
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
x_scaled = scaler.fit_transform(x)
# モデルの定義
pca = PCA(n_components=2, random_state=0)
# 主成分分析の適用
x_std_transformed = pca.fit_transform(x_scaled)
# 主成分分析適用後のデータの確認
pd.DataFrame(x_std_transformed, columns=['第一主成分', '第二主成分']).head(10)
第一主成分 | 第二主成分 | |
---|---|---|
0 | -2.264703 | 0.480027 |
1 | -2.080961 | -0.674134 |
2 | -2.364229 | -0.341908 |
3 | -2.299384 | -0.597395 |
4 | -2.389842 | 0.646835 |
5 | -2.075631 | 1.489178 |
6 | -2.444029 | 0.047644 |
7 | -2.232847 | 0.223148 |
8 | -2.334640 | -1.115328 |
9 | -2.184328 | -0.469014 |
# 寄与率
print('第一主成分の寄与率:{}'.format(pca.explained_variance_ratio_[0]))
print('第二主成分の寄与率:{}'.format(pca.explained_variance_ratio_[1]))
先ほどと結果が異なります。第二主成分の寄与率が高くなっていますが、全体の情報の損失も大きくなっています。
# 次元削減後のデータを可視化
sns.scatterplot(x_std_transformed[:, 0], x_std_transformed[:, 1],
hue=t, palette=sns.color_palette(n_colors=3));
2 つの結果を並べて可視化して違いを確認します。
fig = plt.figure(figsize=(7, 10))
# 標準化適用前
ax1 = fig.add_subplot(2, 1, 1)
sns.scatterplot(x_transformed[:, 0], x_transformed[:, 1],
hue=t, palette=sns.color_palette(n_colors=3));
ax1.set_title('Before')
# 標準化適用後
ax2 = fig.add_subplot(2, 1, 2)
sns.scatterplot(x_std_transformed[:, 0], x_std_transformed[:, 1],
hue=t, palette=sns.color_palette(n_colors=3));
ax2.set_title('After');
主成分分析では平均を 0 にすることが必要な前処理であるため、施した上で主成分分析を適用しましょう。
その他の活用方法
本項では具体的に使用されている例をあげますが、少し専門的な内容になりますので気になる方だけ見てください。
主成分分析は分野によって、呼び名が異なります。特に製造業ではホテリング理論と呼ばれセンサーデータの外れ値の異常検知に使われています。ホテリングとは主成分分析を導入した方の名前です。
また例えばセンサーデータとは、工場で稼働している製造機器に取り付けられ、機器の振動や圧力、温度などさまざまな変数を指します。これらは多くの場合に数十から数百の変数からデータを取得し、機械の状態を監視しています。
外れ値検知にはいくつも手法がありますが、ホテリング理論は現在でも使われる主要な手法のひとつです。例えばセンサーデータを正規分布と仮定したとき、外れ値検知の代表例として 法が思いつきます。こちらは正規分布だとすると、 内にはデータの が入るため 外のデータは外れ値と判断して良いだろうという手法です。
このとき 100 個の多変量なセンサーデータを考えてみます。それぞれの変数で 法を適用すると
となってしまい、全体の約 を異常と判定してしまいます。多変量になればなるほど異常と判定してしまう幅が大きくなり、実現場で適用することが難しいことがわかります。このような場合にホテリング理論が適用されることになります。
もちろんその他にも活用例はいくつもありますが、さまざまな場面で活躍できる手法なのでぜひ活用してみましょう。
k-平均法 (k-means)
k-平均法 (k-means) はクラスタリングと呼ばれる手法に当たり、データを複数のクラスター(グループ)に分けて大まかな特徴を捉える際に使用します。
k-平均法がクラスタリングを行うステップは次の通りです。
- 人間側がクラスター(グループ)の数を決める
- ランダムに振られた点(重心)から近いものをクラスターとする
- 紐づいたクラスターとの距離を元に重心を移動させる
上記の 3 つを重心が動かなくなるまで繰り返します。
今回はコンビニエンスストアの購買データを元にしてクラスタリングの活用方法を理解していきましょう。使用するデータは convinience_store.csv をダウンロードしてください。
データセットの準備
from google.colab import files
uploaded = files.upload()
df = pd.read_csv('convinience_store.csv')
df.head(3)
No | 弁当・麺類 | 飲料 | おにぎり・サンドイッチ | スイーツ | カップスープ | サラダ | |
---|---|---|---|---|---|---|---|
0 | 1 | 25350 | 3650 | 8945 | 0 | 4867 | 8945 |
1 | 2 | 24500 | 0 | 0 | 1827 | 0 | 0 |
2 | 3 | 23050 | 5750 | 11570 | 0 | 7667 | 11570 |
今回取り扱うデータセットはコンビニエンスストアのそれぞれの顧客の 3 ヶ月の購買履歴になります。No
は顧客 ID を表し、それぞれの顧客がどのカテゴリの商品を何円分購入したかを表しています。この過去の購買データを元に k-平均法を適用し、顧客をいくつかのクラスター(グループ)に分割します。k-平均法にデータを適用する際に顧客 ID は使用しないため、予め取り除いておきます。
また、scikit-learn を用いて実装を行うため、同時に .values
属性を適用します。
x = df.drop('No', axis=1).values
x[:3]
k-平均法の実装
実装方法はこれまでの手法と同様の手順で実装することができます。
from sklearn.cluster import KMeans
前述の説明にもある通り、k-平均法では予め分割するクラスター数を決めておく必要あります。クラスター数を決定するハイパーパラメータは n_clusters
になります。クラスター数を決定するのは簡単ではないのですが、ドメイン知識、または色々な数値を試して最も良い値を選択しましょう。
# モデルの定義
kmeans = KMeans(n_clusters=3, random_state=0)
# モデルの学習
kmeans.fit(x)
モデルの学習が完了しました。モデルの学習ではそれぞれのクラスターの中心座標を算出し、保持します。今回取り扱ったデータセットの次元数が 6 次元、そしてクラスターの数を 3 と指定していたため、3 行 6 列の値を確認することができます。
# クラスターの中心座標の確認
kmeans.cluster_centers_
kmeans.cluster_centers_.shape
predict()
メソッドで入力値に対してクラスタリングを適用することができます。
# クラスタリングの適用
cluster = kmeans.predict(x)
cluster
予測値が 0 ~ 2 となっていて、ハイパーパラメータで設定したクラスター数と同じことが確認できます。しかし、この結果からではどのようにクラスタリングされているか判断が難しいため、この情報を元のデータフレームに追加します。
# データフレームの作成
df_cluster = df.copy() # データフレームをコピー
df_cluster['cluster'] = cluster
df_cluster.head()
No | 弁当・麺類 | 飲料 | おにぎり・サンドイッチ | スイーツ | カップスープ | サラダ | cluster | |
---|---|---|---|---|---|---|---|---|
0 | 1 | 25350 | 3650 | 8945 | 0 | 4867 | 8945 | 2 |
1 | 2 | 24500 | 0 | 0 | 1827 | 0 | 0 | 1 |
2 | 3 | 23050 | 5750 | 11570 | 0 | 7667 | 11570 | 2 |
3 | 4 | 22850 | 4100 | 10145 | 0 | 5467 | 10145 | 2 |
4 | 5 | 22500 | 0 | 0 | 0 | 0 | 0 | 1 |
クラスター結果の考察
クラスタリングした結果、顧客を 3 つのクラスター(グループ)に分けることができました。しかし、クラスターに分けるだけではどのように活用できるかわかりません。
教師あり学習と異なり明確な答えが存在しないため、その予測結果がどのようなもので、どのように活用するかに関しては人間側が考慮する必要がある部分であることを覚えておきましょう。
今回分割した 3 つの顧客クラスターに対してどのような活用方法があるのかを考察していきます。今回は一例として、各クラスターの平均購買金額から考察を行います。
# 空のデータフレームを作成
df_results = pd.DataFrame()
df_results
クラスターごとに平均値を算出し、その値を作成した空のデータフレームに追加していきます。
# クラスター 0 の平均値
df_cluster[df_cluster['cluster'] == 0].mean().tolist()
# 各クラスターの平均値を追加
df_results['cluster 0'] = df_cluster[df_cluster['cluster'] == 0].mean().tolist()
df_results['cluster 1'] = df_cluster[df_cluster['cluster'] == 1].mean().tolist()
df_results['cluster 2'] = df_cluster[df_cluster['cluster'] == 2].mean().tolist()
df_results
cluster 0 | cluster 1 | cluster 2 | |
---|---|---|---|
0 | 37.806452 | 46.250000 | 13.533333 |
1 | 5043.548387 | 28681.250000 | 17266.666667 |
2 | 3486.419355 | 5637.666667 | 2730.933333 |
3 | 7017.741935 | 1298.750000 | 10721.666667 |
4 | 2206.000000 | 1271.000000 | 590.866667 |
5 | 2512.903226 | 1770.875000 | 3447.333333 |
6 | 3509.129032 | 716.958333 | 9851.200000 |
7 | 0.000000 | 1.000000 | 2.000000 |
行が df_cluster
の列名に当てはまるため、行に列名を当てはめます。また、転置を行い、列がそれぞれの項目、行がクラスターになるように変更します。
# Index に列名を追加
df_results = df_results.set_index(df_cluster.columns)
# 転置
df_results = df_results.drop(['No', 'cluster']).T
# 結果の表示
df_results
弁当・麺類 | 飲料 | おにぎり・サンドイッチ | スイーツ | カップスープ | サラダ | |
---|---|---|---|---|---|---|
cluster 0 | 5043.548387 | 3486.419355 | 7017.741935 | 2206.000000 | 2512.903226 | 3509.129032 |
cluster 1 | 28681.250000 | 5637.666667 | 1298.750000 | 1271.000000 | 1770.875000 | 716.958333 |
cluster 2 | 17266.666667 | 2730.933333 | 10721.666667 | 590.866667 | 3447.333333 | 9851.200000 |
それぞれのクラスターの特徴を考察してみましょう。下記のような考察が得られるのではないでしょうか。
- クラスター 0 :全体的に購入金額が少ないが、スイーツの購入金額は最も大きい
- クラスター 1 :弁当・麺類と飲料の購入金額が最も大きい
- クラスター 2 :飲料やスイーツの購入は少ないが、全般的に他の食品類の購入金額が大きい
目的をもってクラスタリング後のデータを加工・分析することでクラスターの特徴を把握することができます。この考察を用いて、例えばランチの時間帯に配信するクーポンをグループごとに分ける事で、個々人の趣味嗜好に応じた効果的なマーケティング施策を行うことが可能です。
このように教師なし学習では明確な答えが無く、人間側が予測結果からデータの加工を行い、そこから活用方法の考察を行う必要がある点を覚えておきましょう。