Rで線形代数:射影行列

Rで 線形代数:射影行列 を確認します。

1. 射影行列とは何か?

射影行列(Projection Matrix)を理解する最も簡単な方法は、「影を落とす操作」をイメージすることです。

あるベクトル(点)があったとき、それを特定の空間(直線や平面など)に最も近い点に移動させる変換を行列で表現したものが射影行列です。

直感的なイメージ:懐中電灯と影

  1. 空間に浮かぶ点(ベクトル b: あなたが射影したい元の点です。
  2. 地面(部分空間 S: 影を落としたい場所です。これは直線であったり、平面であったりします。
  3. 真上からの光: 点 b から地面 S に向かって垂直に光を当てます。
  4. 地面にできた影(射影されたベクトル p: この影こそが、点 b を空間 S に射影した点です。

数学的な定義

なぜ「真上からの光」なのでしょうか?それは、元の点 b と、影の点 p を結ぶ線(誤差ベクトル e = b - p)が、地面(部分空間 S)に対して直交(垂直)になるからです。この「直交」が、“最も近い点”であるための数学的な条件です。

部分空間 S が行列 A の列ベクトルによって作られる空間(列空間 C(A))であるとします。このとき、ベクトル bC(A) に射影する射影行列 P は、以下の式で与えられます。

P = A (AᵀA)⁻¹ Aᵀ

この行列 P をベクトル b に掛けると、射影されたベクトル p を得ることができます。

p = Pb

射影行列の性質

  1. 冪等性(Idempotence): P² = P

    • 意味:一度射影した点(影)をもう一度射影しても、位置は変わらない。地面にある影にさらに影を落としても、影は動かないのと同じです。
  2. 対称性(Symmetry): Pᵀ = P

    • これは射影が「直交射影」であるための性質です。

この射影の考え方は、最小二乗法の根幹をなす非常に重要な概念です。データ点に最もフィットする直線(回帰直線)を求めることは、データ点をモデルが表現する空間(直線)に射影することと等価なのです。


2. Rコードによるシミュレーション

それでは、実際にRでベクトルを定義し、射影行列を計算して、ベクトルを射影してみましょう。

ケース1:2次元ベクトルを1次元の直線に射影する

  • ベクトル a = (2, 1) が作る直線に、
  • ベクトル b = (1, 3) を射影します。
# 1. ベクトルの定義
# 射影先の部分空間(直線)を定義するベクトル a
a <- matrix(c(2, 1), ncol = 1)

# 射影したい元のベクトル b
b <- matrix(c(1, 3), ncol = 1)

# 2. 射影行列 P の計算
# P = (a %*% t(a)) / (t(a) %*% a)
# %*% は行列の積、t()は転置
# a %*% t(a) は 2x2 の行列(外積)
# t(a) %*% a は 1x1 の行列(スカラー、内積)
P <- (a %*% t(a)) / as.numeric(t(a) %*% a)

cat("--- 射影先のベクトル a ---\n")
print(a)

cat("\n--- 射影したいベクトル b ---\n")
print(b)

cat("\n--- 計算された射影行列 P ---\n")
print(P)

# 3. ベクトル b を射影して p を計算
# p = P * b
p <- P %*% b

cat("\n--- 射影されたベクトル p ---\n")
print(p)

# 4. 誤差ベクトル e の計算
# e = b - p
e <- b - p

cat("\n--- 誤差ベクトル e ---\n")
print(e)


# --- 検証 ---
# 1. P^2 = P の確認 (冪等性)
cat("\n--- 検証: P^2 は P と等しいか? ---\n")
print(all.equal(P %*% P, P))

# 2. 誤差ベクトル e が a と直交しているか確認 (内積が0か?)
cat("\n--- 検証: a と e は直交しているか? (内積が0か?) ---\n")
print(t(a) %*% e)
--- 射影先のベクトル a ---
     [,1]
[1,]    2
[2,]    1

--- 射影したいベクトル b ---
     [,1]
[1,]    1
[2,]    3

--- 計算された射影行列 P ---
     [,1] [,2]
[1,]  0.8  0.4
[2,]  0.4  0.2

--- 射影されたベクトル p ---
     [,1]
[1,]    2
[2,]    1

--- 誤差ベクトル e ---
     [,1]
[1,]   -1
[2,]    2

--- 検証: P^2 は P と等しいか? ---
[1] TRUE

--- 検証: a と e は直交しているか? (内積が0か?) ---
     [,1]
[1,]    0

この結果から、射影ベクトル p は (2, 1) となり、これはベクトル a と同じです(この例ではたまたまそうなりました)。また、誤差ベクトル ea の内積が0であり、直交していることが確認できます。

続いてシミュレーションの結果を ggplot2 を使って可視化します。

library(ggplot2)

# --- 計算部分 ---
a <- matrix(c(2, 1), ncol = 1)
b <- matrix(c(1, 3), ncol = 1)
P <- (a %*% t(a)) / as.numeric(t(a) %*% a)
p <- P %*% b
e <- b - p

# --- プロット用データの作成 ---

# ベクトル描画用のデータフレーム
df_vectors <- data.frame(
  x_start = c(0, 0, p[1]), y_start = c(0, 0, p[2]),
  x_end = c(b[1], p[1], b[1]), y_end = c(b[2], p[2], b[2]),
  label = c("b (元のベクトル)", "p (射影ベクトル)", "e (誤差ベクトル)")
)

# ラベル表示用のデータフレーム(中点)
df_labels <- data.frame(
  x_pos = c((0 + b[1]) / 2, (0 + p[1]) / 2, (p[1] + b[1]) / 2),
  y_pos = c((0 + b[2]) / 2, (0 + p[2]) / 2, (p[2] + b[2]) / 2),
  label = c("b (元のベクトル)", "p (射影ベクトル)", "e (誤差ベクトル)")
)

# --- プロットの作成 ---
ggplot() +
  # 射影先の部分空間(直線)
  geom_abline(
    intercept = 0, slope = a[2] / a[1],
    color = "skyblue", linetype = "dashed", linewidth = 1
  ) +

  # ベクトルを矢印として描画
  geom_segment(
    data = df_vectors,
    aes(x = x_start, y = y_start, xend = x_end, yend = y_end, color = label),
    arrow = arrow(length = unit(0.3, "cm")),
    linewidth = 1.2
  ) +

  # ラベルを中点に表示
  geom_text(
    data = df_labels,
    aes(x = x_pos, y = y_pos, label = label, color = label),
    hjust = 0.5,
    vjust = -0.7,
    size = 4,
    fontface = "bold",
    show.legend = FALSE # 凡例が重複しないようにテキストの凡例は非表示
  ) +

  # 原点を描画
  geom_point(aes(x = 0, y = 0), size = 3, color = "black") +

  # 色の手動設定
  scale_color_manual(
    name = "ベクトル", # 凡例のタイトル
    values = c("b (元のベクトル)" = "red", "p (射影ベクトル)" = "blue", "e (誤差ベクトル)" = "darkgreen")
  ) +

  # 軸のスケールを1:1に固定
  coord_fixed(ratio = 1) +

  # 軸の範囲を設定
  xlim(-1.5, 3.5) +
  ylim(-0.5, 3.5) +

  # タイトルとラベル
  labs(
    title = "ベクトルbのベクトルaへの射影",
    subtitle = "P*b = p,  e = b - p",
    x = "X軸",
    y = "Y軸"
  ) +

  # テーマ
  theme_minimal() +
  theme(legend.position = "top")
Figure 1
  • 水色の破線: ベクトル a が作る直線(部分空間)です。
  • b (元のベクトル): 射影したかった元のベクトル (1, 3)。
  • p (射影ベクトル): b の影にあたるベクトル (2, 1)。このベクトルは完全に水色の直線上に乗っています。
  • e (誤差ベクトル): p の先端から b の先端へ向かうベクトル。このベクトル e が、水色の直線と直角に交わっていることが視覚的に確認できます。

ケース2:3次元ベクトルを2次元の平面に射影する

3次元空間において、2つの線形独立なベクトル a1a2 が張る平面(部分空間)を考えます。この平面に、別の3次元ベクトル b を正射影します。

  • 射影先の平面を張るベクトル:
    • a1 = (1, 0, 1)
    • a2 = (0, 1, 1)
    • この2つのベクトルが張る平面は z = x + y という方程式で表せます。
  • 射影したい元のベクトル:
    • b = (2, 3, 1)

射影先の部分空間が複数のベクトル(a1, a2, …)で張られる場合、それらを列ベクトルとして並べた行列 A = [a1 a2 …] を使います。 このときの射影行列 P は、以下の一般式で与えられます。

P = A (AᵀA)⁻¹ Aᵀ

ここで、(AᵀA)⁻¹ は行列 AᵀA の逆行列です。

# 1. ベクトルの定義
# 射影先の平面を張るベクトル
a1 <- matrix(c(1, 0, 1), ncol = 1)
a2 <- matrix(c(0, 1, 1), ncol = 1)

# 射影したい元のベクトル
b <- matrix(c(2, 3, 1), ncol = 1)

# 行列 A を作成
A <- cbind(a1, a2)

cat("--- 行列 A ---\n")
print(A)

# 2. 射影行列 P の計算
# P = A * (A^T * A)^-1 * A^T
# solve() は逆行列を計算する関数
P <- A %*% solve(t(A) %*% A) %*% t(A)

cat("\n--- 3D->2D平面への射影行列 P ---\n")
print(P)

# 3. ベクトル b を射影して p を計算
p <- P %*% b

cat("\n--- 射影されたベクトル p ---\n")
print(p)

# 4. 誤差ベクトル e の計算
e <- b - p

cat("\n--- 誤差ベクトル e ---\n")
print(e)

# --- 検証 ---
# 誤差ベクトル e が a1, a2 の両方と直交しているか確認 (内積が0か?)
cat("\n--- 検証: e と a1 の内積 (0になるか) ---\n")
print(t(e) %*% a1)

cat("\n--- 検証: e と a2 の内積 (0になるか) ---\n")
print(t(e) %*% a2)
--- 行列 A ---
     [,1] [,2]
[1,]    1    0
[2,]    0    1
[3,]    1    1

--- 3D->2D平面への射影行列 P ---
           [,1]       [,2]      [,3]
[1,]  0.6666667 -0.3333333 0.3333333
[2,] -0.3333333  0.6666667 0.3333333
[3,]  0.3333333  0.3333333 0.6666667

--- 射影されたベクトル p ---
          [,1]
[1,] 0.6666667
[2,] 1.6666667
[3,] 2.3333333

--- 誤差ベクトル e ---
          [,1]
[1,]  1.333333
[2,]  1.333333
[3,] -1.333333

--- 検証: e と a1 の内積 (0になるか) ---
             [,1]
[1,] 4.440892e-16

--- 検証: e と a2 の内積 (0になるか) ---
             [,1]
[1,] 2.220446e-16

計算結果より、誤差ベクトル e は平面を張るベクトル a1, a2 の両方と直交していることが確認できます(2つの内積結果は事実上のゼロです)。これは、ベクトル b が正しく平面に正射影されたことを意味します。

続いて2次元の場合と同様に ggplot2 を使って可視化します。

# --- 1. 計算部分 ---

# 射影先の平面を張るベクトル
a1 <- matrix(c(1, 0, 1), ncol = 1)
a2 <- matrix(c(0, 1, 1), ncol = 1)
# 射影したい元のベクトル
b <- matrix(c(2, 3, 1), ncol = 1)

# 行列 A を作成
A <- cbind(a1, a2)
# 射影行列 P の計算
P <- A %*% solve(t(A) %*% A) %*% t(A)
# ベクトル b を射影して p を計算
p <- P %*% b
# 誤差ベクトル e の計算
e <- b - p


# --- 2. プロット用データ作成 ---

# 3D座標を擬似2D座標に変換する関数
project_3d_to_2d <- function(v, angle = 0.8, z_scale = 0.5) {
  x2d <- v[1] - z_scale * v[3] * cos(angle)
  y2d <- v[2] - z_scale * v[3] * sin(angle)
  return(c(x = x2d, y = y2d))
}

# 1. 射影先の平面グリッド (3D -> 2D)
s <- seq(-1.5, 2.5, by = 0.5)
t <- seq(-1, 3, by = 0.5)
grid_data <- expand.grid(s = s, t = t)
plane_points_3d <- t(apply(grid_data, 1, function(row) row["s"] * a1 + row["t"] * a2))
plane_points_2d_matrix <- t(apply(plane_points_3d, 1, project_3d_to_2d))
df_plane_2d <- as.data.frame(plane_points_2d_matrix)
colnames(df_plane_2d) <- c("x", "y")

# 2. ベクトル描画用のデータ (3D -> 2D)
vec_data_3d <- data.frame(
  x_start = c(0, 0, p[1]), y_start = c(0, 0, p[2]), z_start = c(0, 0, p[3]),
  x_end = c(b[1], p[1], b[1]), y_end = c(b[2], p[2], b[2]), z_end = c(b[3], p[3], b[3]),
  label = c("b (元のベクトル)", "p (射影ベクトル)", "e (誤差ベクトル)")
)
vec_data_2d_matrix <- cbind(
  t(apply(vec_data_3d[, 1:3], 1, project_3d_to_2d)),
  t(apply(vec_data_3d[, 4:6], 1, project_3d_to_2d))
)
vec_data_2d <- as.data.frame(vec_data_2d_matrix)
colnames(vec_data_2d) <- c("x_start", "y_start", "x_end", "y_end")
vec_data_2d$label <- vec_data_3d$label


# 3. ラベル表示用のデータを作成 (3Dの中点を2Dに投影)
# まず、各ベクトルの中点の3D座標を計算
label_pos_3d <- data.frame(
  x_pos = c((0 + b[1]) / 2, (0 + p[1]) / 2, (p[1] + b[1]) / 2),
  y_pos = c((0 + b[2]) / 2, (0 + p[2]) / 2, (p[2] + b[2]) / 2),
  z_pos = c((0 + b[3]) / 2, (0 + p[3]) / 2, (p[3] + b[3]) / 2),
  label = c("b (元のベクトル)", "p (射影ベクトル)", "e (誤差ベクトル)")
)

# 次に、その3D中点座標を2Dに投影
df_labels_2d_matrix <- t(apply(label_pos_3d[, 1:3], 1, project_3d_to_2d))
df_labels_2d <- as.data.frame(df_labels_2d_matrix)
colnames(df_labels_2d) <- c("x", "y")
df_labels_2d$label <- label_pos_3d$label


# --- 3. ggplotによる描画 ---
ggplot() +
  # 射影先の平面をグリッドで描画
  geom_point(data = df_plane_2d, aes(x = x, y = y), color = "black", size = 1) +

  # 座標軸を描画
  geom_segment(aes(x = 0, y = 0, xend = project_3d_to_2d(c(4, 0, 0))["x"], yend = project_3d_to_2d(c(4, 0, 0))["y"]), color = "gray", arrow = arrow(length = unit(0.2, "cm"))) +
  geom_segment(aes(x = 0, y = 0, xend = project_3d_to_2d(c(0, 4, 0))["x"], yend = project_3d_to_2d(c(0, 4, 0))["y"]), color = "gray", arrow = arrow(length = unit(0.2, "cm"))) +
  geom_segment(aes(x = 0, y = 0, xend = project_3d_to_2d(c(0, 0, 4))["x"], yend = project_3d_to_2d(c(0, 0, 4))["y"]), color = "gray", arrow = arrow(length = unit(0.2, "cm"))) +
  geom_text(aes(x = project_3d_to_2d(c(4.2, 0, 0))["x"], y = project_3d_to_2d(c(4.2, 0, 0))["y"]), label = "X", color = "gray") +
  geom_text(aes(x = project_3d_to_2d(c(0, 4.2, 0))["x"], y = project_3d_to_2d(c(0, 4.2, 0))["y"]), label = "Y", color = "gray") +
  geom_text(aes(x = project_3d_to_2d(c(0, 0, 4.2))["x"], y = project_3d_to_2d(c(0, 0, 4.2))["y"]), label = "Z", color = "gray") +

  # 各ベクトルを矢印で描画
  geom_segment(
    data = vec_data_2d,
    aes(x = x_start, y = y_start, xend = x_end, yend = y_end, color = label),
    arrow = arrow(length = unit(0.3, "cm")),
    linewidth = 1.2
  ) +

  # ラベルを各ベクトルの中点に表示
  geom_text(
    data = df_labels_2d,
    aes(x = x, y = y, label = label, color = label),
    hjust = 0.5, # 水平方向中央
    vjust = -0.7, # 少し上にずらす
    size = 4,
    fontface = "bold"
  ) +

  # スケール、テーマ、タイトルの設定
  scale_color_manual(values = c("b (元のベクトル)" = "red", "p (射影ベクトル)" = "blue", "e (誤差ベクトル)" = "darkgreen")) +
  coord_fixed(ratio = 1) +
  theme_void() +
  labs(
    title = "3次元ベクトルの2次元平面への射影",
    subtitle = "ベクトルbを、a1とa2が張る平面へ射影する",
    color = ""
  ) +
  theme(legend.position = "top")
Figure 2
  • 黒色の点群: ベクトル a1a2 が張る2次元平面を表しています。
  • 灰色の軸: 3次元空間のX, Y, Z軸です。
  • b (元のベクトル, 赤): 3次元空間に浮かんでいる、私たちが射影したいベクトル (2, 3, 1) です。
  • p (射影ベクトル, 青): ベクトル b を平面に射影した結果のベクトルです。このベクトルの先端は、完全に黒色の点群の平面上にあることがわかります。
  • e (誤差ベクトル, 緑): 元のベクトル b と射影ベクトル p の差を表すベクトルです。幾何学的には、b の先端から平面へ下ろした垂線に相当します。このベクトル e が平面と直交している様子が視覚的に確認できます。

3. まとめ

このように、射影行列を用いることで、あるベクトルを任意の次元の部分空間へ正確に射影することができます。これは、データからノイズを除去したり、次元削減を行ったりする際に非常に重要な役割を果たします。

以上です。