機械学習の分類アルゴリズムとして先駆的な手法であり*1、例として用いたirisデータセットを世に広めたフィッシャー御大の線形判別分析(LDA)の説明用のコード*2を探したのですが、教科書の説明と対応のよい固有値の計算をしていたのが見つからなかったので書きました。
概説
フィッシャーのLDAは、複数の群によるデータから、データから群を予測する分類器を構成する手法です*3。多変量を一変数に変換する写像()をつくり、一次元の値からどの群に含まれるかを非確率的に予測します。予測するデータの写像の値を、群ごとの平均値の写像の値と比較し、もっとも近い平均値の写像の値の群を予測値とします。
写像をどう作るのかが問題になるわけですが、データ全体の共分散を、群ごとの平均値の共分散(
; Between-class co-variance)と、群ごとの写像の共分散(
; Within-class co-variance)に、標本分散の計算と似たような方法で分離します。
が全体のサンプルサイズ、
が群の集合、
が群
のサンプルサイズ、
が観測値のベクトル、
が群ごとの平均ベクトル、
がサンプル全体の平均ベクトルです。
次に、データの写像
の分散
を、平均値の写像分散
と、群ごとの写像の分散
に分けて考え、
,
を満たすを、構成する分類器の写像として使います。ある群の平均値と別の群の平均値が、群の中の散らばりと比較して、なるべく離れるようにするわけです。この最適値を求める計算は、
の最大固有値に対応する固有ベクトルを求める固有値問題として考えることができます*4。
は要素の比が重要で、大きさは任意です。
と置くことができます。さらに
と置いて、最大化問題を書き直します。
です。
は実対称行列なので、直交行列
と固有値が並んだ対角行列
を使って表すことができます。
と置くと
となります。は固有値になります。
と置き、(順番は任意にとれるので)
の対角成分とします。
直交行列の逆行列が転置行列になることに注意すると、
となります。この制約の下での最大化を考えると、
の係数
が一番大きいので、
で
です。よって、
となります。は
の最初の行があらわすベクトルになるので、
は最大の固有値
に対応する固有ベクトルになります。
で
と置くと、
は任意の固有ベクトルとなります。このとき
を整理すると、
となります。が
の固有ベクトルであることに注意して、
となり、が
の固有値
に対応する固有ベクトルになることが分かります。
と
の行列のランクは等しいので、この二つの行列の固有値はすべて一致します。
が
の最大の固有値に対応する固有ベクトルとなるとき、
も
の最大の固有値に対応する固有ベクトルになっており、求める最大化条件が満たされていることが分かります。
ソースコード
Irisデータセットを使って実際に分類してみましょう。訓練データと検証データは分けた方がよいので、行番号が奇数のデータを訓練に、偶数のデータを検証に使います。
# 3種すべてを分類(1つは減らせる) G <- c("setosa", "virginica", "versicolor") # がくと花弁の長さで分類(2つ以外にするとプロットがエラーになる) column <- c("Sepal.Length", "Petal.Length") # 分類器(写像と平均値)の学習 training <- subset(iris[seq(1, nrow(iris), 2), ], Species %in% G) X <- training[, column] n <- nrow(X) mu <- apply(X, 2, mean) Mu <- list() B <- W <- matrix(0, ncol(X), ncol(X)) for(g in G){ X_s <- subset(training, Species == g)[, column] mu_s <- apply(X_s, 2, mean) Mu[[g]] <- mu_s B <- B + (mu_s - mu) %*% t(mu_s - mu) * nrow(X_s) / n X_c <- apply(X_s, 1, \(x) x - mu_s) W <- W + X_c %*% t(X_c) / n } e <- eigen(solve(W) %*% B) a <- e$vectors[, 1] # 検証データの分類 test_data <- subset(iris[seq(2, nrow(iris), 2), ], Species %in% G) test_data$Species <- factor(test_data$Species, labels = G) Species_id <- as.numeric(test_data[, "Species"]) X <- test_data[, column] z <- c(as.matrix(X) %*% a) z_d <- matrix(NA, length(z), length(G)) z_mu <- sapply(1:length(G), \(i) a %*% Mu[[G[i]]]) p_g <- G[t(apply(replicate(length(G), z), 1, \(z){ z_d <- sqrt((z - z_mu)^2) which(min(z_d)==z_d) }))] # 精度の計算 accuracy <- sum(p_g == test_data$Species)/length(p_g) # プロット(2変数のときのみ動く) pch <- c(21, 23, 24)[1:length(G)] bg <- c("white", "red", "blue")[1:length(G)] plot(as.formula(sprintf("%s ~ %s", column[2], column[1])), data = test_data, pch = pch[Species_id], bg = bg[Species_id], main = "Fisher's linear discriminant") G_o <- G[order(z_mu)] for(i in 1:(length(G)-1)){ c <- c(a %*% Mu[[G_o[i]]] + a %*% Mu[[G_o[i + 1]]])/2 curve({ -(a[1]/a[2])*x + c/a[2] }, 1, 9, add = T) } legend("bottomright", G, pch = pch, pt.bg = bg, bty = "n")

*1:深層学習が一般化していますし、それよりは簡便な手法であるランダムフォレストやSVM(Rで機械学習(SVM) - 餡子付゛録゛)やQDAも、LDAよりは柔軟な予測器を構成できます。
*2:計算や図示はMASS::ldaやklaR::partimatのような既存パッケージの関数で間に合います。
*3:係数の標準誤差などが必要な計量分析には使えない一方、ロジットモデルなどと比較してデータが完全分離する場合でも一意な値が得られる利点があります。
*4:詳しくは8.3 Fisher’s linear discriminant rule | Multivariate Statisticsを参照してください。導出までの計算は教科書的な線形代数の操作ですが、実対称行列の性質などの復習機会になりました。