k-NN(k-近傍法)のPython実装

目次

はじめに

この記事では、k-Nearest Neighbors (k-NN) アルゴリズムの実装方法を詳しく解説します。k-NNは、機械学習の教師あり学習の一手法で、分類と回帰の両方の問題に適用可能です。特に分類問題において広く使われています。

k-NNは、新しいデータポイントに対して、そのデータポイントに最も近いk個の訓練データを見つけ出し、それらのクラスラベルの多数決によって新しいデータポイントのクラスを予測します。kの値は、モデルのハイパーパラメータであり、適切な値を選ぶことが重要です。

この実装例では、scikit-learnのk-NNクラスを使用し、ワインデータセットを基に、k-NNモデルの構築に必要な手順と考え方を理解することができます。特に、データの前処理とkの選択は、モデルの性能に大きな影響を与えます。また、モデルの評価では、数値だけでなく視覚的な分析も重要であることを示します。

この記事が、k-NNの理解を深め、実際の問題へ適用する際の一助となれば幸いです。

実装手順

1. ライブラリのインポート

機械学習モデルを構築するには、様々なライブラリやモジュールが必要になります。ここでは以下のライブラリをインポートしています。

from matplotlib.colors import ListedColormap
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from sklearn import datasets
from sklearn.metrics import accuracy_score
from sklearn.model_selection import train_test_split
from sklearn.neighbors import KNeighborsClassifier
from sklearn.preprocessing import StandardScaler
Pythonコード詳細
  • matplotlib.colorsmatplotlib.pyplotはデータの可視化に使用します。
  • ListedColormapはカスタムの色マップを作成するためのクラスです。
  • numpyは数値計算に使用する基本的なライブラリです。データを配列の形で扱うことができます。
  • pandasはデータ処理に使用します。DataFrameという2次元の表形式のデータ構造を提供しています。
  • sklearnはPythonで機械学習モデルを構築するためのライブラリです。k-NNをはじめ、様々なアルゴリズムが実装されています。
    • datasetsモジュールからサンプルデータをロードできます
    • metricsモジュールから評価指標を計算するための関数が提供されています
    • model_selectionモジュールから訓練データとテストデータへの分割ができます
    • neighborsモジュールからk-NNモデルをインポートしています
    • preprocessingモジュールから特徴量のスケーリングなどの前処理手法が提供されています

2. データセットのインポート

ここではsklearn.datasetsからワインデータセットをロードしています。

datasets.load_wine()は、ワインの化学分析結果とそのワインの品質評価のデータをまとめたデータセットを返します。このデータセットは、13次元の説明変数(ワインの化学特性)と、3つのクラスラベル(ワインの種類)からなる多クラス分類問題です。

dataset = datasets.load_wine()
print('columns:', dataset.feature_names)
print('ラベルの種類:', np.unique(dataset.target))
Pythonコード詳細
  • .dataには13個の説明変数の値が格納されています。
  • .targetには目的変数の正解ラベル(0, 1, 2)が格納されています。
  • .feature_namesには説明変数の名前が格納されています。 .target_namesには目的変数の名前(ワインの種類)が格納されています。

出力

データの形式:(178, 14)
欠損値の数:0

最初に.feature_names.targetの中身を確認しています。これにより、扱うデータの概要を把握できます。

3. データセットの確認

データセットの中身を詳しく確認し、理解を深めることは、モデル構築の前提となる重要な作業です。ここではpandasのDataFrameを使ってデータを視覚的に確認できる形に変換しています。

pd.set_option('display.max_columns', None)
df = pd.DataFrame(dataset.data, columns=dataset.feature_names)
df['target'] = dataset.target
df.head()
Pythonコード詳細
  1. pd.set_option('display.max_columns', None)
    • pandasの表示オプションを変更し、出力時にすべての列を表示するようにしています。
  2. df = pd.DataFrame(dataset.data, columns=dataset.feature_names)
    • DataFrameを作成し、説明変数(dataset.data)を割り当てています。列ラベルにはdataset.feature_namesを使用しています。
  3. df['target'] = dataset.target
    • 目的変数(dataset.target)を新しい列として追加しています。
  4. df.head()
    • 作成したDataFrameの先頭5行を表示することで、データの中身を確認できます。

出力

DataFrameに変換することで、各説明変数の値の範囲、欠損値の有無、目的変数の分布などを簡単に確認できます。また、.describe()メソッドを使えば、各列の統計量(平均、標準偏差、最小値、最大値など)も一度に確認できます。

データの確認は、以下のような点で重要です。

  • データの前処理の必要性を判断できる(スケーリングが必要か、欠損値の処理が必要かなど)
  • 外れ値の存在を確認できる
  • 説明変数間の関係性や、目的変数との関係性について仮説を立てられる

4. 相関関係の確認

説明変数と目的変数の間の相関関係を確認することで、モデルの性能向上が見込めます。相関が高い説明変数を選択することにより、過学習を防ぎ、一般化性能を高めることができます。

corr_matrix = df.corr()
sorted_columns = corr_matrix.abs().sort_values('target', ascending=False).index
sorted_corr_matrix = corr_matrix.loc[sorted_columns, sorted_columns]
print(sorted_corr_matrix)
Pythonコード詳細
  1. corr_matrix = df.corr()
    • DataFrameの.corr()メソッドで相関行列を計算しています。
  2. sorted_columns = corr_matrix.abs().sort_values('target', ascending=False).index
    • 目的変数'target'との絶対値の相関が高い順に説明変数を並び替えています。
  3. sorted_corr_matrix = corr_matrix.loc[sorted_columns, sorted_columns]
    • 並び替えた列順で相関行列を作り直しています。
  4. print(sorted_corr_matrix)
    • 並び替えた相関行列を出力しています。

出力

これにより、目的変数との相関が高い説明変数がわかります。後の手順で、この情報を使って重要な説明変数を選択することができます。

相関の高い変数を選択することで、モデルの性能が向上する可能性があります。また、相関の低い変数を除外することで、モデルの単純化やデータの次元削減にもつながります。相関分析は重要な前処理ステップの一つと言えます。

ただし、相関関係だけでなく、ドメイン知識に基づいて変数を選択することも大切です。相関が高くても、因果関係がない変数を選んでしまうと、モデルの解釈性が損なわれる可能性があります。

5. 説明変数と目的変数に分割

前の手順で確認した相関関係に基づき、重要な説明変数を選択します。そして、説明変数(X)と目的変数(y)に分離させます。

select_features = ['flavanoids','od280/od315_of_diluted_wines'] 
X =  df.loc[:, select_features].values
y = df.loc[:, 'target'].values
Pythonコード詳細
  1. select_features = ['flavanoids','od280/od315_of_diluted_wines']
    • 4.の相関分析で相関が高かった2つの説明変数を選択しています。
  2. X = df.loc[:, select_features].values
    • DataFrameの特定の列(選択した説明変数)を抽出し、NumPyの配列に変換しています。
  3. y = df.loc[:, 'target'].values
    • 同様に目的変数の値を配列に変換しています。

説明変数Xと目的変数yに分離することで、後の学習ステップでそれぞれを入力値と正解値として扱えるようになります。

k-NNは距離ベースのアルゴリズムなので、説明変数の選択は特に重要です。関連性の低い変数を含めると、かえって性能が低下してしまう可能性があります。そのため、ドメイン知識と相関分析を組み合わせて、適切な変数選択を行うことが求められます。

6. 訓練データとテストデータに分割

次に、選択した説明変数Xと目的変数yのデータを訓練データとテストデータに分割します。

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.3, random_state=0, stratify=y)
Pythonコード詳細

sklearn.model_selection.train_test_split関数を使って分割しています。

  • test_size=0.3で、全体の30%をテストデータとして割り当てています。
  • random_state=0で乱数シードを固定しています。再現性を確保するために設定します。
  • stratify=yを指定することで、目的変数yの値の割合が訓練/テストデータで維持されます。

訓練データは実際にモデルを学習させるためのデータセット、テストデータはモデルの汎化性能を評価するためのデータセットとなります。

テストデータは学習には使わず、最後の評価のみに使うことが重要です。そうしないと、テストデータの情報も含んだモデルになってしまい、過学習を起こす可能性があります。

分割する際には、目的変数の値の割合が偏らないよう気をつける必要があります。上記のstratifyパラメータはその観点から重要です。特に不均衡データ(あるクラスのデータが極端に少ない場合)では、stratifyを使わないと適切な評価ができなくなります。

通常、テストデータの割合は20~30%程度が一般的です。訓練データが少なすぎるとモデルが適切に学習できなくなる可能性がありますが、多すぎるとテストデータが少なくなり、汎化性能の評価が不安定になります。

以上の手順で、モデル構築に適した形にデータを分割することができました。次は、このデータを使ってk-NNモデルを学習していきます。

7. 特徴量のスケーリング

説明変数の値が異なる単位やスケールになっている場合、モデルの性能が低下する可能性があります。特にk-NNのような距離ベースのアルゴリズムでは、スケーリングが重要な前処理となります。

ここでは、標準化(平均0、分散1に変換)を行います。

sc = StandardScaler()
sc.fit(X_train)
X_train_std = sc.transform(X_train)  
X_test_std = sc.transform(X_test)
Pythonコード詳細
  1. sc = StandardScaler()
    • scikit-learnのStandardScalerクラスのインスタンスを作成します。
  2. sc.fit(X_train)
    • 訓練データX_trainから、平均と標準偏差を計算します。
  3. X_train_std = sc.transform(X_train)
    • 計算した平均と標準偏差を使って、訓練データを標準化します。
  4. X_test_std = sc.transform(X_test)
    • 同じ平均と標準偏差を使って、テストデータも標準化します。

訓練データとテストデータを別々にスケーリングしてはいけません。そうすると、異なる基準でスケーリングされた値を比較することになり、モデルの性能が低下してしまいます。必ず訓練データから計算した統計量を使ってテストデータを変換します。

スケーリングによって、すべての特徴量が同じスケールになります。これにより、特定の特徴量が過度に重視されることを防げます。k-NNでは、距離の計算に直接影響するため、スケーリングは特に重要な前処理となります。

一方で、全ての特徴量が同じくらい重要とは限りません。そのような場合は、重要な特徴量により大きな重みを与えるようなスケーリング(例えば、特徴量の重要度に基づくスケーリング)を検討する必要があります。

8. 最適なkの選択

k-NNでは、kの値によってモデルの性能が大きく変わります。kが小さすぎると過学習し、大きすぎると未学習になります。そこで、適切なkの値を見つける必要があります。

ここでは、kを1から100まで変化させ、訓練データとテストデータの正解率の差が最も小さくなるkを選んでいます。

training_accuracy = []
test_accuracy = []

# k(n_neighbors)に対する正解率の差を最小化
min_diff = float('inf')
best_k = 0

for neighbors in range(1, 101):
    classifier = KNeighborsClassifier(n_neighbors=neighbors, metric='minkowski', p=2)
    classifier.fit(X_train_std, y_train)
    
    train_acc = classifier.score(X_train_std, y_train)
    test_acc = classifier.score(X_test_std, y_test)
    
    training_accuracy.append(train_acc)
    test_accuracy.append(test_acc)
    
    diff = abs(train_acc - test_acc)
    
    # テストデータの正解率が訓練データの正解率より高くなった場合、ループを終了
    if test_acc > train_acc:
        break
    
    # 最小差を更新
    if diff < min_diff:
        min_diff = diff
        best_k = neighbors
        
plt.figure(figsize=(10, 5))
plt.plot(range(1, len(training_accuracy) + 1), training_accuracy, label='Training accuracy')
plt.plot(range(1, len(test_accuracy) + 1), test_accuracy, label='Test accuracy')
plt.axvline(x=best_k, color='r', linestyle='--', linewidth=1)
plt.text(best_k + 1, 0.9, f'Min diff when k = {best_k}', rotation=90, fontweight='bold')
plt.xlabel('k (n_neighbors)')
plt.ylabel('Accuracy')
plt.xticks(np.arange(0, neighbors+5, step=10))
plt.xticks(np.arange(0, neighbors+5, step=1), minor=True)
plt.legend()
plt.title(f"Best k based on minimal difference: {best_k}")
plt.show()
Pythonコード詳細
  1. for ループでkを1から100まで変化させます。
  2. 各kの値でKNeighborsClassifierを初期化し、訓練データで学習させます。
  3. 訓練データとテストデータの正解率を計算し、training_accuracytest_accuracyにそれぞれ格納します。
  4. 訓練データとテストデータの正解率の差の絶対値を計算します。
  5. テストデータの正解率が訓練データの正解率を上回る場合、過学習と判断しループを終了します。
  6. 正解率の差が最小値を更新した場合、min_diffbest_kを更新します。

ループ終了後、best_kが最適なkの値となります。

出力

ここでは2つの基準を使っています。1つは訓練データとテストデータの正解率の差が最小となるk、もう1つは過学習を避けるためにテストデータの正解率が訓練データの正解率を上回らないようにすることです。

このようにしてkを選ぶことを、kの値による交差検証と呼びます。ただし、この方法はテストデータを使ってkを選んでいるため、厳密にはテストデータの情報が学習に使われていることになります。より厳密にはさらに検証用データを分けるか、訓練データだけを使った交差検証(例えばk-fold交差検証)を行う必要があります。

9. 訓練データによるモデルの学習

前の手順で最適なkの値(ここでは33)を決定したので、その値を使ってk-NNモデルを訓練データで学習させます。

classifier = KNeighborsClassifier(n_neighbors = 33, metric = 'minkowski', p = 2)
classifier.fit(X_train_std, y_train)
Pythonコード詳細
  1. classifier = KNeighborsClassifier(n_neighbors = 33, metric = 'minkowski', p = 2)
    • KNeighborsClassifierクラスのインスタンスを作成します。
    • n_neighborsは最適なkの値(33)を指定しています。
    • metricはデータポイント間の距離の計算方法を指定します。ここでは'minkowski'(ミンコフスキー距離)を使用しています。
    • pはミンコフスキー距離のパラメータで、p=2の場合はユークリッド距離になります。
  2. classifier.fit(X_train_std, y_train)
    • 訓練データX_train_stdと目的変数y_trainを使ってモデルを学習させます。

KNeighborsClassifierは、fit()の際に訓練データを保存するだけで、実際の計算は predict() の際に行われます。つまり、新しいデータポイントに対して、保存された訓練データとの距離を計算し、最も近いk個のデータポイントのラベルを使って分類を行います。

距離の計算方法はいくつか選択肢がありますが、一般的にはユークリッド距離かマンハッタン距離が使われます。ミンコフスキー距離はこれらを一般化したもので、pの値によって異なる距離になります。

以上の手順で、訓練データを使ってk-NNモデルを学習することができました。次は、この学習済みモデルを使って新しいデータポイントの分類を行います。

10. 新しいデータセットで予測

学習済みのモデルを使って、新しいデータポイントに対する予測を行うことができます。ここでは、新しいデータポイントとして[4, 2]を使用しています。

new_data = [[4, 2]]
pred_label = classifier.predict(sc.transform(new_data))[0]
print(f'予測したクラス:{pred_label}({dataset.target_names[pred_label]})')
Pythonコード詳細
  1. new_data = [[4, 2]]
    • 新しい説明変数の値を配列で用意しています。この例では2つの説明変数があるので、サイズ2の配列になっています。
  2. classifier.predict(sc.transform(new_data))
    • predictメソッドで予測を行います。引数には新しい説明変数の値を入力する必要があります。
    • sc.transform(new_data)で、訓練データと同じ基準でスケーリングを行っています。これは重要です。新しいデータポイントは、訓練データと同じ基準でスケーリングされている必要があります。
  3. [0]
    • predictメソッドは配列を返すので、インデックス[0]を指定して1つの値を取り出しています。
  4. dataset.target_names[pred_label]
    • 予測されたラベルに対応するクラス名を取得しています。

出力

予測したクラス:0(class_0) #ラベルが0のワイン

出力される予測値は、クラスラベル(この例では0, 1, 2のいずれか)になります。

新規データポイントに対する予測は、学習済みモデルの実用的な使い方の一つです。ただし、この予測の信頼性は、モデルの汎化性能に依存します。そのため、次の手順でモデルの性能を評価します。

11. テストデータで予測

学習済みのモデルをテストデータで評価します。テストデータはモデルの学習に使われていない未知のデータセットです。

y_pred = classifier.predict(X_test_std)
Pythonコード詳細
  1. classifier.predict(X_test_std)
    • predictメソッドにテストデータX_test_stdを入力して予測を行います。
  2. y_pred = ...
    • 予測結果をy_predという変数に格納しています。

テストデータに対する予測値y_predが得られたので、次の手順で実際の正解値y_testとの比較を行い、モデルの性能を評価します。

予測値と実際の正解の比較は、モデルの汎化性能を評価するために重要です。汎化性能とは、未知のデータに対してどの程度正確に予測できるかを表します。この性能が高いモデルは、実際の問題に適用できる可能性が高くなります。

12. モデルの性能評価

予測値と実際の正解値を比較することで、モデルの性能を評価します。ここでは、正解率(Accuracy)を使って評価しています。

print(f'正分類のデータ点: {(y_test == y_pred).sum()}個/{len(y_test)}個' )
print(f'Accuracy(Test): {accuracy_score(y_test, y_pred):.3f}')
Pythonコード詳細
  1. (y_test == y_pred).sum()
    • 予測値y_predと正解値y_testを比較し、一致した数をカウントしています。
    • ==は要素ごとの比較を行い、TrueまたはFalseの配列を返します。
    • sum()でTrueの個数、つまり正解の個数を計算しています。
  2. len(y_test)
    • テストデータ全体の数を取得しています。
  3. accuracy_score(y_test, y_pred)
    • scikit-learnのaccuracy_score関数で、正解率(Accuracy)を計算しています。正解率は、正解の数をデータ全体の数で割ったものです。

出力

正分類のデータ点: 43個/54個
Accuracy(Test): 0.796

出力される値から、テストデータ全体の何個が正しく分類できたか、また正解率がどの程度であるかがわかります。

正解率は分類問題でよく使われる評価指標ですが、クラスの割合が不均衡なデータセットでは適切でない場合があります。例えば、全体の90%がクラスAで、10%がクラスBの場合、全てをクラスAに分類するモデルの正解率は90%になりますが、このモデルはクラスBを全く予測できていません。このような場合は、適合率(Precision)、再現率(Recall)、F1スコアなど他の指標も合わせて評価する必要があります。

以上の手順により、k-NNモデルの性能を評価することができました。次の手順では、この結果を可視化し、より詳細にモデルの挙動を分析します。

13. 性能評価の可視化

モデルの性能を評価する際、数値だけでなく視覚的に確認することも重要です。ここでは、決定境界を可視化することで、モデルがどのように分類を行っているかを詳細に分析します。

def calculate_bounds(X1, X2):
    X1_min, X1_max = X1.min() - (X1.max()-X1.min())/20, X1.max() + (X1.max()-X1.min())/20
    X2_min, X2_max = X2.min() - (X2.max()-X2.min())/20, X2.max() + (X2.max()-X2.min())/20
    return X1_min, X1_max, X2_min, X2_max
def plot_data(ax, X_set, y_set, X1, X2, Z, colors, kind, classifier):
    cmap = ListedColormap(colors[:len(np.unique(y_set))])
    ax.contourf(X1, X2, Z, alpha=0.3, cmap=cmap)
    for idx, feature in enumerate(np.unique(y_set)):
        ax.scatter(x=X_set[y_set == feature, 0], 
                   y=X_set[y_set == feature, 1],
                   alpha=0.5, 
                   color=colors[idx],
                   marker='o', 
                   label=dataset.target_names[feature], 
                   edgecolor='black')
    ax.set_xlabel(select_features[0])
    ax.set_ylabel(select_features[1])
    ax.set_title(f'{type(classifier).__name__} ({kind})')
    ax.legend(loc='best')
def plot_decision_regions(X_train_std, X_test_std, y_train, y_test, classifier):
    # マーカーとカラーマップの準備
    colors = ('red', 'blue', "green")
    
    # スケーリング前の元のデータに変換
    X_train_set, y_train_set = sc.inverse_transform(X_train_std), y_train
    X_test_set, y_test_set = sc.inverse_transform(X_test_std), y_test
    
    # 訓練データとテストデータの範囲を統一
    X_combined = np.vstack((X_train_set, X_test_set))
    X1_min, X1_max, X2_min, X2_max = calculate_bounds(X_combined[:, 0], X_combined[:, 1])
    
    # グリッドポイントの生成
    X1, X2 = np.meshgrid(np.arange(X1_min, X1_max, step=(X1_max - X1_min) / 1000),
                         np.arange(X2_min, X2_max, step=(X2_max - X2_min) / 1000))
    # 各特徴を1次元配列に変換して予測を実行
    Z = classifier.predict(sc.transform(np.array([X1.ravel(), X2.ravel()]).T))
    # 予測結果を元のグリッドポイントのデータサイズに変換
    Z = Z.reshape(X1.shape)
    
    fig, ax = plt.subplots(1, 2, figsize=(8, 4))
    
    # 訓練データとテストデータのプロット
    plot_data(ax[0], X_train_set, y_train_set, X1, X2, Z, colors, "Training set", classifier)
    plot_data(ax[1], X_test_set, y_test_set, X1, X2, Z, colors, "Test set", classifier)
    
    plt.tight_layout()
    plt.show()
plot_decision_regions(X_train_std, X_test_std, y_train, y_test, classifier)
Pythonコード詳細

この可視化は以下の手順で行われています。

  1. calculate_bounds関数で、プロットの範囲を計算します。説明変数の最小値と最大値から少し余裕を持たせた範囲を返します。
  2. plot_data関数で、実際のプロットを行います。
    • contourfで決定境界を等高線で描画します。
    • scatterで訓練データとテストデータの点をプロットします。
    • 軸ラベル、タイトル、凡例を設定します。
  3. plot_decision_regions関数で、訓練データとテストデータそれぞれに対してplot_data関数を呼び出し、2つのプロットを並べて表示します。
    • meshgridで、プロットする範囲内に等間隔の点を生成します。
    • 生成した点に対してpredictを行い、予測結果に基づいて色分けします。
  4. 最後にplot_decision_regions関数を呼び出して、実際にプロットを表示します。

出力

このプロットから、以下のことが読み取れます。

  • モデルが訓練データに対してどの程度適合しているか(訓練データのプロット)
  • モデルがテストデータに対してどの程度汎化しているか(テストデータのプロット)
  • 決定境界の形状(等高線)
  • 誤分類の傾向(異なる色の点が混ざっている領域)

これらの情報は、モデルの性能を評価し、改善の方向性を決める上で非常に有益です。例えば、訓練データでは良好な結果が得られているのにテストデータではうまくいっていない場合は、過学習が疑われます。逆に、訓練データでもテストデータでも性能が低い場合は、未学習の可能性があります。決定境界の形状から、モデルが学習したパターンを読み取ることもできます。

以上のように、可視化は機械学習のモデル開発において非常に重要な役割を果たします。数値だけでは見えない情報を提供し、モデルの理解を深める助けとなります。

まとめ

この記事では、scikit-learnを使ったk-NNモデルの実装方法を詳細に解説しました。実装の各ステップを経ることで、データの特性を理解し、それに適したモデルを構築・評価することができます。

k-NNは、シンプルでありながら強力なアルゴリズムです。特に、少量のデータしかない場合や、データの分布が複雑な場合に有効です。一方で、データの次元が高い場合は計算量が増大するという問題もあります。

この記事で示した実装方法は、k-NNに限らず、他の機械学習アルゴリズムにも応用できる汎用的なものです。データの前処理、モデルの構築、評価の一連の流れは、機械学習のプロジェクトに共通するものだからです。

機械学習のモデル開発では、これらの工程を繰り返し、試行錯誤することが重要です。この記事が、そのための基礎知識と実践的なスキルを提供できていれば幸いです。

この記事が気に入ったら
フォローしてね!

この記事が参考になった方はシェアしてね!
  • URLをコピーしました!

本コンテンツへの意見や質問

コメントする

目次