目次(まとめ)

◾️ データの特徴を学習する「訓練データ」として、手元のデータを使用する

◾️学習成果を評価する「テストデータ」として、未来のデータを使用する

◾️k最近傍法は、Rのclassパッケージのknn関数で実行できる

◾️「訓練データ」と「テストデータ」の分割は、ランダムに行うのが一般的



こんにちは、みっちゃんです。

以前の記事で、多数決でデータを振り分ける「k最近傍法」を紹介しました。

今回の記事では

いくつかのグループに属することがわかっているデータが手元にあって、未来のデータをどのグループに分けるべきかわからない

もしくは

手元のデータを使って、未来のデータを適切にグループ分けする方法が知りたい

という方向けに、Rを使ってk最近傍法を実行する方法を紹介します。

データの特徴を学習する「訓練データ」として、手元のデータを使用する

以前の記事で解説したように、k最近傍法は「未来のデータが、所属グループがわかっている手元のデータにどれぐらい近いのか」という情報をもとに、未来のデータを適切なグループに振り分けます。

これを可能にするためには、まず、手元のデータを分析して、どのグループに所属するグループがどういう特徴をもつデータなのかを調べる必要があります。

機械学習の専門用語を使うと「訓練データを使った学習」です。

ここでは、以前の記事でも使用してきたように、説明のために、Rにあらかじめ準備されているアヤメのデータ(iris)を使用します。

> head(iris)
  Sepal.Length Sepal.Width Petal.Length Petal.Width Species
1          5.1         3.5          1.4         0.2  setosa
2          4.9         3.0          1.4         0.2  setosa
3          4.7         3.2          1.3         0.2  setosa
4          4.6         3.1          1.5         0.2  setosa
5          5.0         3.6          1.4         0.2  setosa
6          5.4         3.9          1.7         0.4  setosa

このデータは、150行5列の行列になっており、5列目には、アヤメの種類の名前("setosa", "versicolor", "virginica")が示されていて、それぞれの種類に対して50行のデータがあります。

つまり、3つのグループに所属することがわかっているデータがあるということです。

ここでは、"dplyr" ライブラリの "filter" という関数を使って、それぞれに所属する行だけを抜き出してみたいと思います。

> library(dplyr)

> group_set <- filter(iris, iris$Species == "setosa")
> group_ver <- filter(iris, iris$Species == "versicolor")
> group_vir <- filter(iris, iris$Species == "virginica")

"filter" 関数の使い方は、「filter(全データ、取り出すデータの指定)」です。1行目では、"iris" のデータのうち、5列目の "iris$Species" が "setosa" に一致する行だけを抜き出しています。

それぞれのグループのデータは、50行ずつありますが、ここでは、先頭40行を「訓練データ」にしたいと思います。

そのために以下のように実行します。

> train_size <- 40

> train_feature <- rbind(group_set[1:train_size,-5], group_ver[1:train_size,-5], group_vir[1:train_size,-5])
> train_label <- c(group_set[1:train_size,5], group_ver[1:train_size,5], group_vir[1:train_size,5])

「訓練データ」は、「データの特徴(train_feature)」と「データが所属するグループ(train_label)」のペアの情報からなっています。

データの中身を確認すると、以下のようになっています。

> head(train_feature)
  Sepal.Length Sepal.Width Petal.Length Petal.Width
1          5.1         3.5          1.4         0.2
2          4.9         3.0          1.4         0.2
3          4.7         3.2          1.3         0.2
4          4.6         3.1          1.5         0.2
5          5.0         3.6          1.4         0.2
6          5.4         3.9          1.7         0.4

> head(train_label)
[1] 1 1 1 1 1 1

"train_label" に関しては、本来、"setosa", "versicolor", "virginica" なのですが、自動的に、"setosa"→"1", "versicolor"→"2", "virginica"→"3" と置き換わっています。

学習成果を評価する「テストデータ」として、未来のデータを使用する

先ほど「訓練データ」として、50行のうち先頭40行を用いました。

ここでは、未来のデータを想定して、残り10行のデータを「テストデータ」として用いたいと思います。

ここで「テスト」の意味は、「訓練データ」を用いて、どういう特徴をもつデータがどのグループに所属するのかを学習したので、その学習成果を評価するという意味です。

先ほどと同様に、以下のように実行します。

> test_feature <- rbind(group_set[(train_size+1):nrow(group_set),-5], group_ver[(train_size+1):nrow(group_ver),-5], group_vir[(train_size+1):nrow(group_vir),-5])
> test_label <- c(group_set[(train_size+1):nrow(group_set),5], group_ver[(train_size+1):nrow(group_ver),5], group_vir[(train_size+1):nrow(group_vir),5])

k最近傍法は、Rのclassパッケージのknn関数で実行できる

上で準備した「訓練データ」である "train_feature" と "train_label"、「テストデータ」である "test_feature" と "test_label" を使用して、k最近傍法を実行・評価します。

k最近傍法は、以下のように、Rの "class" パッケージの "knn" 関数を使って実行できます。

> library(class)

> predicted_label <- knn(train_feature, test_feature, train_label, k = 6, prob = TRUE)

このように実行すると、k = 6 としたときのk最近傍法が実行され「テストデータ」である "test_feature" に対して予測されたグループが "predicted_label" に出力されます。

"knn" 関数を用いてk最近傍法を実行する場合、多数決でグループを決められない場合には、ランダムにグループが割り当てられます。

実行結果を見るために、以下のように実行します。

> predicted_label
 [1] 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3
attr(,"prob")
 [1] 1.0000000 1.0000000 1.0000000 1.0000000 1.0000000 1.0000000 1.0000000
 [8] 1.0000000 1.0000000 1.0000000 1.0000000 1.0000000 1.0000000 1.0000000
[15] 1.0000000 1.0000000 1.0000000 1.0000000 1.0000000 1.0000000 1.0000000
[22] 0.8333333 0.8333333 1.0000000 1.0000000 1.0000000 0.6666667 0.8333333
[29] 1.0000000 0.6666667
Levels: 1 2 3

1行目に並んでいる数字が「テストデータ」の "test_feature" に対して予測されたグループ(番号)です。

また、実行の際に「prob = TRUE」としたため、それぞれのテストデータに対して、そのグループの得票率が表示されています。

例えば、30個(10個×3)のテストデータのうち、最初のデータに対しては、その近傍にある6つの点(訓練データ)がすべてグループ1("setosa")に所属していたため、グループ1の得票率が100%だったことを意味しています。

逆に、最後のデータに対しては、その近傍にある6つの点(訓練データ)のうち4つがグループ3("virginica")に所属していたため、グループ3の得票率が66.6%であり、多数決で他のグループに勝ったことを意味しています。

ちなみに、以下のように実行することで、30個のデータ全てに対する正答率を計算できます。

> sum(predicted_label == test_label)/length(test_label)
1

この場合には、"1" となるので、すべてのテストデータについて、正しいグループを予測できたということを意味しています。

「訓練データ」と「テストデータ」の分割は、ランダムに行うのが一般的

上の例では「訓練データ」と「テストデータ」を分ける際に、データの上から40個を訓練データに、、といった操作をしていました。

しかし、このような操作では、「訓練データ」と「テストデータ」との間で、データの偏りが生じたりして、正しい評価ができなくなる危険性があります。

そこで一般的には、ランダムにデータを分けることが行われます。

例えば、以下のように実行すると、50個のデータのうち、ランダムに約8割のデータを「訓練データ」に、残り約2割のデータを「テストデータ」にすることができます。

(例)
> ind <- sample(c(TRUE, FALSE), 50, replace=TRUE, prob=c(0.8, 0.2))

> train_feature < group_set[ind,]
> test_feature < group_set[!ind, ]

「replace=TRUE」とすると、「c(TRUE, FALSE)」から復元抽出するということになります。