2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Rで学ぶ行列分解の魔法 — SVTからLoRAまでを繋ぐ直感ガイド

2
Last updated at Posted at 2026-04-22

1. プロローグ:行列は「情報のレイヤー」である

現代のデータサイエンスと人工知能の根底には、線形代数という強力な言語が流れている。しかし、多くの初学者にとって、行列は単なる「数字が並んだ四角い塊」に見え、その背後に潜むダイナミックな情報の構造を見逃してしまいがちである。本報告書では、行列を「数値の集合」としてではなく、複数の「情報のレイヤー(層)」が重なり合ったものとして定義し直すことから始める 。

データの「重なり」としての行列

一つの行列を観察する際、それは完成された一枚の絵画のようなものである。しかし、その絵画は、透明なセル画が何枚も重ね合わされて構成されている。各セル画は、その絵の最も重要な輪郭、背景のグラデーション、あるいは微細な質感といった、異なる「パターン」を保持している。この「セル画」に相当するのが、数学的には「ランク1の行列」と呼ばれる最小単位である 。

任意の行列 $A$ は、これらのランク1行列の重み付き合計として分解できる。この概念を理解することが、後のSVDやSVT、さらにはLoRAといった高度な技術を直感的に理解するための鍵となる。行列の「ランク(階数)」とは、いわばそのデータを構成するのに必要な「独立した情報のレイヤー数」を指す 。

低ランク近似という魔法の杖

現実世界に存在する膨大なデータ――高解像度の画像、数百万人の購買履歴、数千億のパラメータを持つAIモデルの重み――は、見かけ上は非常に複雑で高次元(高ランク)に見える。しかし、驚くべきことに、これらの情報の大部分は、実はごく少数の「主要なレイヤー」だけで十分に表現可能であることが多い 。

これを「低ランク性」と呼ぶ。データの「本質(骨組み)」だけを残し、残りの「枝葉(あるいはゴミ)」を切り捨てる作業こそが、行列の低ランク近似である。このプロセスにより、データの圧縮、ノイズの除去、さらには欠損した情報の推論といった、魔法のような処理が可能になる 。本報告書では、Rを用いた実践を通じて、この魔法がどのように機能し、どのように最新のAI技術へと繋がっているのかを詳述する。

概念 従来の捉え方 本報告書の視点
行列 数値の矩形配列 複数の情報の重なり(レイヤー構造)
ランク 行列の独立な列数 データを構成するのに必要なパターンの数
特異値 分解で得られる係数 各レイヤーが持つ情報の「声の大きさ」
近似 数値的な誤差の許容 骨組みだけを残す「情報の彫刻」

2. 【実践1】SVDで画像を「骨組み」だけにする

行列を情報のレイヤーに分解するための最も基本的かつ強力なツールが、特異値分解(SVD: Singular Value Decomposition)である。SVDは、あらゆる長方形の行列を3つの特別な行列に分解する 。

$$ A = U \Sigma V^T $$

ここで、 $U$ は左特異ベクトル行列(行側の特徴)、 $V$ は右特異ベクトル行列(列側の特徴)、そして $\Sigma$ は対角線上に「特異値」が並ぶ行列である 。

特異値:情報の重要度のインジケーター

SVDの最大の魅力は、分解されたレイヤーが「情報の重要度(特異値の大きさ)」の順に並んでいることにある 。最初のレイヤーはデータの最も支配的なパターンを保持し、後ろに行くほど微細で重要度の低い情報になっていく。

Rの標準データセット volcano(ニュージーランドのマウンガファイ火山の標高データ)を用いて、この「情報の骨組み」を抽出する過程を可視化する。

R

# ---------------------------------------------------------
# 【実践1】SVDによる情報の段階的復元
# ---------------------------------------------------------

# ライブラリの準備
if (!require("grid")) install.packages("grid")
library(grid)

# 標高データの読み込み (87x61の行列)
data(volcano)
A <- volcano

# 1. SVDの実行
# svd()関数は、d(特異値ベクトル), u(左特異ベクトル), v(右特異ベクトル)を返す
s <- svd(A)

# 特異値の分布を確認(スクリープロット)
# 最初の数個で大部分のエネルギーを占めていることがわかる
plot(s$d, type="b", pch=16, col="blue", 
     main="特異値の減衰(情報のボリューム)",
     xlab="ランクのインデックス", ylab="特異値の大きさ")

# 2. 低ランク近似による復元関数の作成
reconstruct <- function(s_obj, k) {
  # 上位k個の成分のみを使用する
  # 数式 A_k = U[,1:k] %*% Σ[1:k,1:k] %*% t(V[,1:k]) の実装
  if(k == 1) {
    # ランク1の場合はベクトルの外積
    res <- s_obj$u %*% t(s_obj$v) * s_obj$d
  } else {
    res <- s_obj$u[, 1:k] %*% diag(s_obj$d[1:k]) %*% t(s_obj$v[, 1:k])
  }
  return(res)
}

# 3. 異なるランクで復元して比較表示
par(mfrow = c(2, 2), mar = c(1, 1, 2, 1))

# オリジナルの表示
image(A, col = terrain.colors(100), axes = FALSE, main = "Original (Full Rank)")

# ランク1: 最も太い骨組みだけ
image(reconstruct(s, 1), col = terrain.colors(100), axes = FALSE, main = "Rank 1")

# ランク5: 地形の概要が見えてくる
image(reconstruct(s, 5), col = terrain.colors(100), axes = FALSE, main = "Rank 5")

# ランク20: ほぼオリジナルと見分けがつかない
image(reconstruct(s, 20), col = terrain.colors(100), axes = FALSE, main = "Rank 20")

可視化の解説:「ぼんやりした影」が「実体」に変わる瞬間

このコードを実行すると、ランク1では火山の大まかな「傾き」や「位置」しかわからないが、ランクを上げるにつれて噴火口の形や急峻な斜面が再現されていく様子がわかる 。

重要なのは、ランク20(元の次元の約3分の1)の時点で、人間の目にはオリジナルとほぼ同じに見えることである。これは、残りの40以上のレイヤーには「微細すぎて見えない情報」や「冗長なデータ」しか含まれていないことを意味する。この「情報の濃縮」こそが、特異値を情報の重要度として扱うSVDの真髄である 。

ランク (k) 累積寄与率 (情報保持率) 視覚的特徴
1 約 70% - 80% データの重心と主方向のみがわかる「影」
5 約 90% 主要な地形構造が判明する「下書き」
20 99% 以上 ほぼ完全な復元に見える「完成品」
61 100% 完全なデータ(ノイズも含む)

3. 【実践2】SVT(特異値閾値処理)によるノイズ掃除

SVDが情報を並べ替える技術だとすれば、SVT(Singular Value Thresholding: 特異値閾値処理)は、その並べ替えられた情報を「選別」して加工する技術である。

「ゴミ」と「情報」の境界線

現実のデータには必ずノイズが含まれる。ノイズは数学的に「ランダムな変動」として定義され、行列に分解すると、特定の方向性を持たないため「多数の小さな特異値」として現れる傾向がある 。一方、重要な信号は「少数の大きな特異値」に集中する。

SVTの基本的なアイデアは非常にシンプルである。「小さな特異値はノイズ(ゴミ)であると見なして、強制的にゼロにする、あるいは縮小させる」というものだ 。これを実現するのが「ソフトしきい値関数」である。

$$ D_\tau(X) = U \text{diag}((\sigma_i - \tau)_+) V^T $$

ここで、 $(\sigma_i - \tau)_+$ は、特異値 $\sigma_i$ が閾値 $\tau$ より大きい場合はその差をとり、小さい場合はゼロにする操作を指す 。

Rコード実装:ザラザラした画像からノイズを消し去る

R

# ---------------------------------------------------------
# 【実践2】SVTによるデノイジング(ノイズ除去)
# ---------------------------------------------------------

# 1. 人工的にノイズを生成して追加
set.seed(42)
noise <- matrix(rnorm(nrow(A) * ncol(A), mean = 0, sd = 15), nrow = nrow(A))
A_noisy <- A + noise

# 2. SVTアルゴリズムの実装
svt <- function(M, tau) {
  # SVDを実行
  s_obj <- svd(M)
  
  # 特異値に対してソフトしきい値処理を適用
  # 全ての特異値からtauを引き、0未満になったら0にする
  d_new <- pmax(s_obj$d - tau, 0)
  
  # 非ゼロの特異値の数(有効ランク)をカウント
  effective_rank <- sum(d_new > 0)
  message(paste("有効ランク:", effective_rank))
  
  # 行列を再構成
  if (effective_rank == 0) return(matrix(0, nrow(M), ncol(M)))
  
  # 対角行列を生成して掛け合わせる
  res <- s_obj$u[, 1:effective_rank] %*% 
         diag(d_new[1:effective_rank], nrow = effective_rank) %*% 
         t(s_obj$v[, 1:effective_rank])
  
  return(res)
}

# 3. 結果の可視化
par(mfrow = c(1, 3), mar = c(1, 1, 2, 1))
image(A, col = terrain.colors(100), axes = FALSE, main = "Original")
image(A_noisy, col = terrain.colors(100), axes = FALSE, main = "Noisy (SD=15)")
image(svt(A_noisy, tau = 150), col = terrain.colors(100), axes = FALSE, main = "Denoised (SVT)")

なぜノイズだけが消えるのか?

この「ソフトしきい値」という魔法は、データの「意味のある構造」を優先的に保護する仕組みになっている。

  1. 大きな特異値(信号): 閾値 $\tau$ を引かれても、まだ大きな値として残る。これは「主要な形」は少しだけ平滑化されるが、消失はしないことを意味する 。
  2. 小さな特異値(ノイズ): 閾値 $\tau$ を引かれるとゼロになる。これにより、データ全体の「ザラつき」の原因となっていたランダムな成分が一掃される 。

この処理により、行列の「有効ランク」が劇的に減少する。つまり、複雑で汚れたデータが、少数のクリーンなレイヤーの組み合わせへと再構築されるのである。これは画像処理だけでなく、音声信号の浄化や金融データの異常検知など、幅広い分野で「情報のクリーニング」として利用されている 。

4. 【実践3】スカスカの表を埋める(行列補完)

行列分解の魔法が最も輝く場面の一つが「行列補完(Matrix Completion)」である。我々の生活に身近な例では、AmazonやNetflixのレコメンドシステムが挙げられる 。

欠損値だらけのパズルを解く

何万もの商品と何万人のユーザーがいる場合、一人のユーザーが評価した商品はごくわずかである。行列のほとんどのセルは「空(NA)」であり、この「スカスカの表」から、「もしこのユーザーがこの商品を買ったら、どんな評価をするか?」を予測しなければならない。

これを解く鍵もまた「低ランク性」にある。世の中には何万もの映画があっても、人々の「好み」は、「アクションが好き」「恋愛ものが苦手」といった、わずか数十種類の「潜在的な属性」の組み合わせで説明できるはずだ 。もしこの仮定(低ランク性の仮定)が正しければ、わずかな既知のデータから、行列全体を一貫性のある形で埋めることができる 。

反復SVTアルゴリズム

スカスカの行列を埋めるためには、先ほどのSVTを「繰り返し」適用する。

  1. 既知の場所はそのままで、未知の場所(NA)を便宜上 0 で埋める。
  2. その行列に対してSVTを行い、ランクを下げる(=「もっともらしい共通のパターン」を抽出する)。
  3. SVTの結果得られた行列の中で、元々データがあった場所だけを「元の正しい値」に戻す。
  4. これを繰り返すと、未知の部分が周囲のデータと矛盾しない「もっともらしい値」で徐々に埋まっていく 。

具体的なイメージ

$U_{true}$ (ユーザーの特性行列:50人 × 3タイプ)

  • ユーザーが「3つのジャンル」をどれくらい好きかを表します。
  • ユーザー1:[アクション度=0.9, 恋愛度=0.1, SF度=0.0]
  • ユーザー2:[アクション度=0.0, 恋愛度=0.8, SF度=0.2]

$V_{true}$ (アイテムの属性行列:40個 × 3タイプ)

  • 各アイテム(映画など)に「3つのジャンル」がどれくらい含まれるかを表します。
  • 映画A:[アクション成分=1.0, 恋愛成分=0.0, SF成分=0.0]
  • 映画B:[アクション成分=0.1, 恋愛成分=0.9, SF成分=0.0]

かけ算の瞬間!
この2つをかけ合わせる(内積をとる)と、

  • ユーザー1 × 映画A: $(0.9 \times 1.0) + (0.1 \times 0.0) + (0.0 \times 0.0) = \mathbf{0.9}$ (相性バッチリ!)
  • ユーザー1 × 映画B: $(0.9 \times 0.1) + (0.1 \times 0.9) + (0.0 \times 0.0) = \mathbf{0.18}$ (いまいち…)
    このように、「少数の要素(3つのタイプ)」のかけ算によって、全ユーザー×全アイテムの評価(2,000通り)が自動的に決まります。

Rコード実装:レコメンドエンジンのミニチュア版

R

# ---------------------------------------------------------
# 【実践3】反復SVTによる行列補完
# ---------------------------------------------------------

# 1. ランクが低い「真の評価行列」をシミュレーション
# 3つの「好みのタイプ」しかない世界を想定
set.seed(123)
n_users <- 50
n_items <- 40
true_rank <- 3

# U: ユーザーの特性, V: アイテムの属性
U_true <- matrix(rnorm(n_users * true_rank), n_users)
V_true <- matrix(rnorm(n_items * true_rank), n_items)
A_true <- U_true %*% t(V_true) # ランク3の完璧な行列

# 2. データをランダムに欠損させる(80%を未知にする)
missing_mask <- matrix(runif(n_users * n_items) < 0.8, n_users)
A_incomplete <- A_true
A_incomplete[missing_mask] <- NA

# 3. 反復SVTによる補完関数の作成
impute_matrix <- function(X, tau, iterations = 50) {
  # 初期化: NAを0で埋める
  X_filled <- X
  X_filled[is.na(X_filled)] <- 0
  
  for (i in 1:iterations) {
    # 低ランク化 (SVT)
    X_low_rank <- svt(X_filled, tau)
    
    # 観測データのみを元の値に戻す (データの一致性を維持)
    X_new <- X_low_rank
    X_new[!is.na(X)] <- X[!is.na(X)]
    
    # 収束判定: ロベニウスノルム用いた「行列の相対的な変化量」で判定
    # この値が小さくなるということは:「低ランク化して情報を整理した結果」と「観測データを固定して現実に戻した結果」がほぼ一致したということ
    change <- sqrt(sum((X_new - X_filled)^2)) / sqrt(sum(X_filled^2))
    X_filled <- X_new
    if (change < 1e-4) {
      message(paste("Iteration", i, "で収束しました"))
      break
    }
  }
  return(X_filled)
}

# 4. 実行と評価
A_recovered <- impute_matrix(A_incomplete, tau = 10)

# 誤差の比較
rmse <- function(m1, m2) sqrt(mean((m1 - m2)^2))
cat("未観測部分の復元誤差 (RMSE):", rmse(A_recovered[missing_mask], A_true[missing_mask]), "\n")

このアルゴリズムは、まるで霧の中に隠れた風景を、断片的な視覚情報から脳が補完して描き出すようなプロセスである 。行列が低ランクであるという「制約」を与えることで、バラバラだった数値が「意味のあるデータ」として繋がり始めるのである。

5. なぜ今、LoRAなのか?:巨大AIを賢く動かす低ランクの知恵

行列分解の理論は、今や画像処理や統計学の枠を超え、現代のAI(特にChatGPTのような大規模言語モデル: LLM)の進化を支える屋台骨となっている。その最前線にあるのが、LoRA (Low-Rank Adaptation) という技術である 。

巨大モデルの「微調整」という地獄

LLMは数千億個もの「重み(パラメータ)」を持っている。これらを新しいタスク(例えば、医学の専門知識を教え込むなど)に適応させるには、通常、全ての重みを更新し直さなければならない。しかし、これは天文学的な計算量とメモリ、そして莫大なコストを必要とする 。

ここで、行列の低ランク近似の知恵が登場する。「モデルを学習させる際、重みの変化分( $\Delta W$ )もまた、実は低ランクで済むのではないか?」という仮説である 。

LoRAの仕組み:壁を塗り替えるのではなく、壁紙を貼る

LoRAは、元の巨大な重み行列 $W$ (例えば $10,000 \times 10,000$ )は一切凍結して動かさない。代わりに、その変化量 $\Delta W$ を、2つの非常に小さな行列 $A$ と $B$ の積として表現する 。

$$ \Delta W = B \times A $$

もし元の行列が $d \times k$ なら、LoRAでは $r$ (ランク)という非常に小さな値(例えば 8 や 16)を使い、 $A \in \mathbb{R}^{r \times k}$ と $B \in \mathbb{R}^{d \times r}$ だけを学習させる 。

これは「巨大な城の壁全体を塗り直す」という重労働をやめ、「必要な部分だけに薄い壁紙(低ランク行列)を一枚貼る」ことで、見た目と機能を劇的に変えてしまうアプローチである。

Rによるパラメータ削減のシミュレーション

どの程度の「節約」になるのか、Rで計算してみよう。

R

# ---------------------------------------------------------
# LoRAのパラメータ削減シミュレーション
# ---------------------------------------------------------

simulate_lora_efficiency <- function(dim_in, dim_out, rank_r) {
  # フルファインチューニングのパラメータ数
  full_params <- dim_in * dim_out
  
  # LoRAのパラメータ数 (A行列 + B行列)
  lora_params <- (dim_in * rank_r) + (dim_out * rank_r)
  
  # 削減率の計算
  reduction_ratio <- (1 - lora_params / full_params) * 100
  
  # 結果のテーブル表示
  res <- data.frame(
    "項目" = c("元の行列次元", "LoRAランク", "フル更新パラメータ数", "LoRA更新パラメータ数", "削減率"),
    "値" = c(paste(dim_in, "x", dim_out), 
            as.character(rank_r), 
            format(full_params, big.mark=","), 
            format(lora_params, big.mark=","), 
            paste0(round(reduction_ratio, 4), "%"))
  )
  return(res)
}

# 1024次元の標準的なLLMの層で、ランク8のLoRAを適用した場合
print(simulate_lora_efficiency(1024, 1024, 8))

項目
元の行列次元 1024 x 1024
LoRAランク 8
フル更新パラメータ数 1,048,576
LoRA更新パラメータ数 16,384
削減率 98.4375%

この数値を見れば、LoRAがなぜ革命的なのかが一目瞭然である。更新すべきパラメータが $2%$ 未満になれば、計算時間は短縮され、消費メモリは劇的に減り、今まで手の届かなかった巨大なモデルを自分のPCで飼い慣らすことが可能になるのである 。

LoRAの副次的メリット

  1. プラグアンドプレイ: 訓練済みの「壁紙( $A, B$ 行列)」は非常に軽量なため、タスクごとに異なる壁紙を瞬時に貼り替えることができる 。
  2. 知識の保持: 元の重み $W$ を変えないため、モデルが元々持っていた汎用的な知識(常識など)が破壊されにくい 。
  3. 推論時のオーバーヘッド・ゼロ: 運用時には $W' = W + BA$ と計算して事前に統合してしまえば、計算速度は元のモデルと全く変わらない 。

6. さらに先へ:DoRAやAdaLoRAとSVTの関係

行列分解の魔法は進化を続けている。LoRAが「一律に低ランクにする」手法だとすれば、最新の手法は「どこを、どれくらい低ランクにするか」を賢く判断しようとしている。

AdaLoRA:SVTの精神をAI学習に持ち込む

AdaLoRA (Adaptive LoRA) は、本報告書で学んだ SVT の考え方を直接的に LLM の学習に応用したものである 。標準的な LoRA が全ての層で同じランク $r$ を使うのに対し、AdaLoRA は「重要な層には高いランクを、重要でない層には低いランク(あるいはゼロ)」を動的に割り当てる 。

具体的には、学習中に各層の特異値に相当する値を監視し、値が小さい(=学習への寄与が低い)成分を SVT のように「剪定(プルーニング)」していく 。これにより、パラメータの予算を最も効果的な場所に集中させることができる。

DoRA:幾何学的な純粋さへの回帰

DoRA (Weight-Decomposed Low-Rank Adaptation) は、重みを行列の「大きさ(Magnitude)」と「方向(Direction)」に分離する手法である 。これは、SVDが特異値(大きさ)と特異ベクトル(方向)を分離する数学的な美しさを、より実践的な形で AI に取り入れたものである。

DoRAは、LoRAが抱えていた「大きさと方向が混ざって更新されてしまう」という弱点を克服し、フルファインチューニング(全更新)に近い柔軟な学習能力を低ランクで実現している 。

結論:魔法の正体は「秩序の抽出」である

本報告書を通じて、SVD、SVT、そしてLoRAという一連の技術を横断的に見てきた。これらに共通するのは、複雑なデータの海から「本質的な秩序(低ランク構造)」を見つけ出し、それを利用して計算を効率化したり、失われた情報を補ったりするという思想である。

初学者にとって、行列計算は苦痛に満ちた作業に見えるかもしれない。しかし、行列を「複数のレイヤーが重なり合った情報の彫刻」として捉える視点を持てば、数式の背後にある「魔法」の鼓動を感じることができるはずだ。SVTによってノイズを消し去ることも、LoRAによって巨大な知能を自在に操ることも、全ては「行列の低ランク近似」という一つの美しい原理から生まれているのである。

技術 主な目的 キーワード
SVD 情報の整理・要約 特異値、情報の重なり、骨組み
SVT ノイズ除去・欠損補完 ソフトしきい値、クリーニング、パズルの補完
LoRA 巨大AIの効率的学習 パラメータ削減、壁紙の比喩、低コスト微調整
AdaLoRA 適応的なランク割り当て 動的剪定、SVTの応用、最適予算配分
DoRA 性能と効率の両立 重み分解、大きさと方向の分離、高度な微調整

線形代数の世界は、単なる過去の数学遺産ではない。それは、未来のAIを形作り、私たちが情報の複雑さに立ち向かうための、最強の「魔法の杖」なのである。Rを手に、自分だけの行列分解の旅を続けてほしい。その先には、まだ誰も見たことのない「データの真理」が待っているはずだ。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?