Fine Tuning手法「LoRA」の理論的裏付けに関するゆるい考察

# ポストGPT3時代の企業におけるFine Tuningのあり方
2020年に登場したGPT3はデコーダ型Transformerのニューラルネットワーク（NN）ですが、公開情報によれば2048Token長の文脈インプットに対応し、かつNNの重みパラメータ行列成分を約1750億個持つとされています。

このGPT3の登場以降、基盤モデルを取得して各企業毎にFine Tuningを行う場合、Full-parameter Fine Tuningを行う事はとても現実的でなものでなくなりました。数千億オーダーのパラメータを保持し再計算を施すのに必要なコスト（主にGPU）を払ってまで実行できる企業は世界にも限られています。

故に、基盤モデルの重みパラメータ行列は固定しつつ、特定の業務タスク（下流タスク）に特化した学習を実施することで、「効率的にパラメータのFine Tuningを行う手法」であるParameter Efficient Fine Tuning（以下、PEFT）が確率されつつあります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3627586/033ea567-e7e3-be79-9baa-52e8bfa87066.png)

## PEFTの代表格＝LoRA
Low Rank Adaptation（LoRA）は、Adapter型と呼ばれるPEFT手法のひとつです。LoRAの概念は[同論文](https://arxiv.org/abs/2106.09685)に掲載されている以下の図解に集約されていると言っても過言ではないでしょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3627586/f41b50a6-aa06-8a53-2426-ea1da452b99b.png)

左の「Pretrained Weights」は基盤モデル（Foundation ModelとかPre-trained Modelとも言う）が持つNNの重みパラメータです。ここで言う重みパラメータ$W$は狭義のそれ、つまり行列です。

:::note
TransformerにおけるLoRAの適用は、注意機構の線形層である、アテンション層（Key, Query, Value, Output）に対して適用されることが多いです。Hagging Faceのライブラリである`peft`のデフォルトは、同層の重みパラメータ（$W_k$, $W_q$, $W_v$）を対象に実行されます。
>for simplicity and further parameter efficiency, in Transformer models LoRA is typically applied to attention blocks only. [^1]
:::

[^1]: [https://huggingface.co/docs/peft/main/en/conceptual_guides/lora](https://huggingface.co/docs/peft/main/en/conceptual_guides/lora)


さて、LoRAがなにをやっているのか？というと、それはだいたい以下に集約できると考えています。
- Pre-trainedモデルの重み行列（$W$）は固定し、変化量（$ΔW$）のみを再計算する手法
- $ΔW$はそれより低階数（低rank, Low Rank）の行列$A$,$B$を用いて$ΔW=AB$と定義できる
- 特定の下流タスクに特化した$ΔW$を「アダプタ」として推論を実行させる
- 推論実行時の重みパラメータ行列は$W’=W+ΔW$となり、Full-FTに引けを取らない性能を出せる
- なお、rankは元のWの1/10~1/100程度にされることが多い

## LoRAで何が嬉しくて何が難しいのか？
LoRAは特定の業務（下流タスク）に限って$ΔW$を計算します。この$ΔW$が「アダプタ」として、特定の業務実行時に云わば「外付けの脳みそ」として振る舞います。これにより、基盤モデルのパラメータをいじる（再計算する）ことなく、何らかの業務に特化・最適化されたLLMとして利用することができるのです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3627586/fbe9c9b1-3e5f-f5dc-87ba-93200e526760.png)

逆に言うと、特定の業務以外の下流タスクに対しては$ΔW$の優位性がありません。つまり、営業特化の外付け脳みそをビルトインされた大卒新人は、確かに営業においては「なんでもできるベテラン社会人」に引けを取らないですが、それ以外の下流タスク（例えば、事業企画・財務・人事 等）の業務については、その知識レベル・能力は大卒新人のときから何も変わっていない、といった雰囲気です。

## なぜ重みパラメータの変化量を低rank行列の積として近似してもよいのか？
> $ΔW$はそれより低階数（低rank, Low Rank）の行列$A$,$B$を用いて$ΔW=AB$と定義できる

この点は非常に気になります。なぜこんなことをしても良いでしょうか？こちらの[大規模言語モデル入門書籍](https://gihyo.jp/book/2023/978-4-297-13633-8)のp.126、**5.5.4 LoRATuning**から引用すると、
>LoRAでは事前学習済みモデルのパラメータの数は実質的には小さい次元で表せるという仮説に基づき、差分行列$ΔW$を低ランク行列で表現し、学習するパラメータを削減します

とあります。これだけ読むとよくわからないのですが、とても簡単に言うと「営業活動の為に脳みそを使っている時に、イヤイヤ期の3歳児の姪っ子を昼寝させる知識を司る脳みその領域は多分関係ないだろう」ということです。
つまり、基盤モデルであるところの「大卒新人ちゃん」の脳みそには、本当に多くの知識が含まれています。それこそ1750億を超えるパラメータで表現されるニューラルネットワークです。ところが、特定の業務（この場合は営業活動）を遂行する際には、その多くの知識のうち圧倒的多数が関係していないだろうというわけです。具体的には
- 大卒新人ちゃんには姉が居ますが、その姉の娘（姪っ子）は絶賛イヤイヤ期中です
- 姉にも義理の兄もその彼女を昼寝させるのに四苦八苦しているのですが、子守手伝いをしている大卒新人ちゃんは、持ち前の天性で姪っ子を昼寝させる知識を習得しました
- また、大卒新人ちゃんは重度の女児アニメオタクで、ニチアサアニメ（プリキュア）やテレ東の女児向けアニメ（アイカツ 等）に異様に詳しいです
- その他、刀剣男子に影響されて刀剣界隈にも触手を伸ばしています
- しかし、彼女の営業活動においてこれらの知識は全く寄与していないです

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3627586/24c78847-8824-4769-fbe0-34f966fe65d6.png)

という「仮説」なのです。ニューラルネットワークの各パラメータそれぞれが本当にどういった影響を推論結果に与えているのかを特定することは困難です。例えば「女児向けアニメにどハマリして発火したニューロンが、回り回って全然関係ない営業資料のクリエイティビティに寄与している可能性」を完全に排除することができないのです。完全に排除することができないものの、「恐らく十中八九関係ないでしょう」という風に切り捨てる（低rank近似する）事は一定の合理性を持つだろうという仮説が、LoRAの根幹にあると思われます。

# 線形代数学的に低rank化を解説
重みパラメータ行列$W$の低rank化には、特異値分解（Singular Value Decomposition, SVD）を用います。そのためには特異値と特異ベクトルを考えます。

## 特異値分解について

:::note
定義①
$n$行$m$列の$n×m$行列$A$について
$Av=σu$
$A^Tu=σv$
の両者を満たす特異値$σ$（ただし$σ$は$min(n,m)$個、つまり$n$か$m$の小さい方と同じ数だけある）及び特異ベクトル$u, v$が存在する。
ただし、$σ$は非負かつ、$u,v$は単位ベクトル（絶対値が1）をである。また$A^T$は$A$の転置行列（行と列を入れ替えた行列）である
:::
ちなみに行列の積の公理から、$u$は$n$次元ベクトル, $v$は$m$次元ベクトルです。
（でないと掛け算できません）

この定義式の意味するところは、要は$r$個の連立方程式です。ここで$r$は$σ$の個数（$min(m, n)$）とします。１つ目の式$Av=σu$について

$Av_1=σu_1$
$Av_2=σu_2$
$Av_3=σu_3$
・・・
$Av_r=σu_r$

と書き下すことができます。ここで重要なのは$r$は行列$A$の階数（rank）となっていることです。行列のrankとは、行と列それぞれについて線形独立なそれの個数を示したものでした。行変形・列変形によって冗長性を排除した行列$A$は階段行列に変形することができました。基底ベクトルに対して作用するユニークな行（あるいは列）の本数（rank）を導出することができます。すべての行列は必ずrankを持ちます。つまり、$r$個の連立方程式が定義できるという事は、行列$A$のrankは$r$です。また行列$A$を行変形しても列変形しても同じrank（線形独立な行ベクトル or 列ベクトルの本数）になります。
さて、これら$r$個のベクトル$u,v$のセットについては、以下の様に行列表現でまとめます。
$U=(u_1,u_2,u_3,・・・,u_r)$
$V=(v_1,v_2,v_3,・・・,v_r)$
ちなみに、$U$, $V$は正規直交基底とる列ベクトルを成分として持ちます。なぜなら、定義①から各列ベクトル$u_i$は絶対値が1であり、線形独立（直行、内積が0）だからです。
すると、

```math
AV=A(v_1,v_2,v_3,・・・,v_r)=U
\begin{pmatrix}
σ_{1} & 0 & \cdots & \cdots &0\\
0 & σ_{2} & \cdots & \cdots &0\\
0 & 0 & σ_{3} & \cdots & 0\\
\vdots & \vdots & \vdots & \ddots & 0\\
0 & \cdots & \cdots & \cdots & σ_r\\
\end{pmatrix}
=UΣ
```

と変形できます。正規直交基底を持つ列ベクトルを成分に持つ行列Vは

$VV^T=E$

となります。ここで$E$は対角成分に1を持つ単位行列です。よって行列式は最終的に以下の様になります。

$A=UΣV^T$

これが意味するところは、とてもエレガントです。つまり、任意の行列$A$は上記の様に変形できます。

:::note
定義②
$m×n$行列$A$について$A=UΣV^T$と変形できる。
ここで
$Σ$：$r$個の特異値を対角成分に持つ$r×r$の対角行列。$∀σ_i≠0$である。
$U$：$n×r$の直交行列。行列$A$の右特異ベクトル$u_i$が列ベクトルとして並んでいる。
$V$:$m×r$の直交行列。行列$A$の左特異ベクトル$v_i$が行ベクトルとして並んでいる。$V$の転置行列を$V^T$とする。
:::
定義②をビジュアライズしてみると非常に簡単です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3627586/13c74d31-de46-b051-d185-46c30b53e71b.png)

これを幾何学的に解釈してみたいと思います。なお、行列演算に限らず、演算子をつなげる場合、左から付加していきます。例えば、ベクトル空間$x$に対して何らかの演算子$P$を作用させることを式化すると$Px$となります。その$Px$に対して更に演算子$Q$を作用させることを考えると、その結果は$QPx$と表現できます。

すると、ベクトル空間$x$に対して行列$A$を作用させる（これを線形変換と言いました）事は、$A=UΣV^T$と変換できることに鑑みれば、以下のように解釈できます。
1. $m$次元ベクトル空間$x$に対して、$v_1〜v_r$の方向に変換する（入力空間）
1. 変換したベクトル空間（$r$次元に飛ばされてしまった結果$V^Tx$となる）をそれぞれ$σ_1〜σ_r$倍だけ伸長（あるいは短縮）させる
1. その結果（$ΣV^Tx$）を$u_1〜u_r$の方向に押し出して、最終的に$UΣV^T$という変換がされる（出力空間）

例えとして適切なのかは自信ないのですが、少なくとも私の理解を図解してみるとこんな感じです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3627586/6e4cf81b-dcf7-ca37-593d-df0434780cfe.png)

大卒新人ちゃんを線形変換して部署に配属するわけですが、社会人能力を要素に持つベクトル空間（方向）に着目し、それぞれの方向に$σ_i$倍だけ能力値を向上させます。そのうえで、配属部署の上長の好み、新卒に対して重視する方向を表すベクトル空間（方向）に押し出す（配属する）という雰囲気です。最終的な配属結果はどの社会人能力方向にどれだけ能力向上したかと、新卒配属部署の上長の重視する観点の掛け算で決まります。

行列$A$による線形変換とは、其の実なにやっているのか？を表すことができる、これが特異値分解の面白い点です。

## 低rank化による情報圧縮
さて、特異値分解によって$r$個の特異値を持つ対角行列$Σ$を得ることができました。この対角成分$σ_1~σ_r$について、小さい値から順にいくつかを0にしてしまいます。特異値の大小とは、入力空間の各方向（を表すベクトル）をそれぞれ何倍するかを表す値でした。ということは、小さい特異値の場合はあまりその方向には注目しない、大きい特異値の場合はその方向に大きく影響を受けるという理解ができます。
ここで、「あまりその方向には注目しない」方向（ベクトル）は無視しちゃいましょう、というのが低rank化（低rank近似）です。線形変換$A$においてあまり着目されない方向の変換については、「その方向には変換していない」と見てしまってもよいだろう、それでも線形変換$A$の特徴（情報量）の損失は限定されたものになり、大勢に影響はないだろうということです。
これがまさしくLoRAによる低rank近似の根拠になっています。

# Pythonで試してみよう
さっそくこれまでの議論をPythonで実験・実証してみたいと思います。具体的に特異値分解（SVD）を実行します。さて、

**「Pythonをさくっとブラウザで実行したいな〜」**

と思ったときにおすすめなのが、「OpenShift AI（Sandbox）」です。OpenShift AIはData Scienceのプロジェクトを作成・管理することのできる「Red Hat OpenShift」の機能ですが、なんと無料のRed HatアカウントがあればさくっとSandbox環境が手に入ります。

## OpenShift AIのSandbox環境でJupyter Labを開く
まずは[こちら](https://developers.redhat.com/learn/openshift-ai/how-create-natural-language-processing-nlp-application-using-red-hat-openshift?source=sso)の「Launch your Developer Sandbox」をクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3627586/f132aea5-e924-4c32-6749-63650ef4669c.png)

次に「OpenShift AI」の「Launch」をクリックします

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3627586/cff5f86f-c5ed-54cf-68e3-8c1477ec29d8.png)

既に自分のRed Hatアカウント名のData Science Projectが作られているので、クリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3627586/6e9e45a6-7471-f506-10bf-7afd64f31669.png)

「create workbench」をクリックして、自分のJupyter Lab（旧Jupyter Notebook）環境を作成します。
名称は何か適当なものを入れ、「Image selection」は「Standard Data Science」を選びます。
※今回はただのPython実行環境がほしいだけなので、こちらでOKです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3627586/4a0a42a6-2728-b7ce-6d57-0e9b980054f1.png)

Jupyter Labが開くので、「Notebook」欄のPython3.9をクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3627586/f7b07412-a23f-1b45-d72f-e4236184ac1e.png)

これでPythonのコードを入力・実行することができます。セルにコードを入力しCommand ＋ Enterで実行できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3627586/25cd8153-dd4c-4186-3502-e746a31773e6.png)

## 5行4列行列でSVDを実行する

```python
#行列計算の為のライブラリをインポート
import numpy as np
from numpy.linalg import svd, matrix_rank

# rank=3の4行5列行列を作成
A = np.array([[1, 6, 4, 7, 10], [9, 6, 9, 15, 15], [7, 3, 7, 10, 10], [1, 1, 8, 2, 9]])

# 行列とそのrank数を表示
print('matrix A\n', A, A.shape)
print('rank: ', matrix_rank(A))
```
行列Aとそのrankを表示すると以下の通り。
```sh
matrix A
 [[ 1  6  4  7 10]
 [ 9  6  9 15 15]
 [ 7  3  7 10 10]
 [ 1  1  8  2  9]] (4, 5)
rank:  3
```

```python
# Numpyの超便利ツールで特異値分解（SVD）を一発でやる
#full_matrices=Falseで、Σをr×rの対角行列として計算するように矯正してます
U, Σ, Vt = np.linalg.svd(A, full_matrices=False)

# 結果を表示（少数第三位までで四捨五入）
print("U:\n", U.round(2), U.shape)
print("Σ:\n", Σ.round(2), Σ.shape)
print("Vt:\n", Vt.round(2), Vt.shape)
```
SVDをNumpyの関数で一発でやります。右特異ベクトル$V^T$, 特異値行列$Σ$, 左特異ベクトル$U$はそれぞれ以下になります。
```sh
U:
 [[ 0.38  0.21 -0.87  0.24]
 [ 0.72 -0.34  0.07 -0.6 ]
 [ 0.49 -0.21  0.38  0.75]
 [ 0.3   0.89  0.32 -0.1 ]] (4, 4)
Σ:
 [35.14  6.93  5.04  0.  ] (4,)
Vt:
 [[ 0.3   0.24  0.39  0.54  0.63]
 [-0.49 -0.07  0.5  -0.56  0.43]
 [ 0.54 -0.66  0.46 -0.12 -0.2 ]
 [ 0.59  0.55 -0.04 -0.59  0.04]] (4, 5)
```

```python
# 行列Aを特異値分解の定義に従って再計算してみる
# 特異値Σを対角行列Sigmaに変換
Sigma = np.diag(Σ)

#Aを再構成
A_reconstructed = U @ Sigma @ Vt
print('matrix A_reconstructed\n', A_reconstructed)
```
さて、定義に従って$A=UΣV^T$となるでしょうか？
```sh
matrix A_reconstructed
 [[ 1.  6.  4.  7. 10.]
 [ 9.  6.  9. 15. 15.]
 [ 7.  3.  7. 10. 10.]
 [ 1.  1.  8.  2.  9.]]
```
$A$と同じものが最構成されました！定義は正しい！
```python
# 特異値対角行列Σの成分を選別する
# (1,1)成分35.137が突出して大きいので、それ以外を全部0にしてしまう（低rank近似）

Σ_lora = [35.137, 0, 0, 0]
Sigma_lora = np.diag(Σ_lora)

#低rank近似したΣ_loraを使って行列Aを再構成
A_lora = U @ Sigma_lora @ Vt
print('matrix A_lora\n', A_lora.round(2))
```
$σ_1$だけが他の$σ$よりも突出して大きいということは、このベクトル$A$による線形変換への「影響度合い」は$σ_1$がかなり多くを占めていると想像できます。よって、それ以外を全部0にしてしまい、行列${A}_{lora}$を定義に従って計算してみます。

```sh
matrix A_lora
 [[ 4.07  3.22  5.31  7.29  8.53]
 [ 7.67  6.06 10.01 13.73 16.07]
 [ 5.25  4.15  6.85  9.4  11.  ]
 [ 3.17  2.51  4.14  5.68  6.65]]
```

こんなベクトルが再構成されました。これが低ランク近似された後の行列です。

```python
# ランダムな5次元ベクトルを生成
vector = np.random.randn(5)

# ベクトルのノルム（絶対値）を計算
norm = np.linalg.norm(vector)

# ベクトルをノルムで割って正規化
normalized_vector = vector / norm

Linear_A = np.dot(A, normalized_vector)
Linear_A_lora = np.dot(A_lora, normalized_vector)
cosine_similarity = np.dot(Linear_A, Linear_A_lora) / (np.linalg.norm(Linear_A) * np.linalg.norm(Linear_A_lora))
print(cosine_similarity)
```
適当な5次元ベクトルに対して、$A$と${A}_{lora}$をそれぞれ作用させた結果のベクトル同士の類似度を計算します。類似度はコサイン類似度です。
```sh
0.9605566805721031
```
約0.96とまぁまぁ近しい値になりました。
※コサイン類似度は1が全く一致、-1が正反対を意味します。

ただ、今回の5行4列行列だと、低ランク化がかなり低ランク化されてしまいました。というのも、rank=3の行列からrank=1まで特異値を削減しています。これだとちょっと情報を削減しすぎな感も否めません。事実、LoRA論文では
>rankは元のWの1/10~1/100程度にされることが多い

でありました。
## 100行100列行列でSVDを実行する
ということで、全く同じ事をrank=80の100行100列行列でやってみましょう。なお、特異値は上位10個までを残します。

```python
#行列計算の為のライブラリをインポート
import numpy as np
from numpy.linalg import svd, matrix_rank

# ランダムな100x80の整数行列を生成
B = np.random.randint(0, 10, (100, 80))

# ランダムな80x100の整数行列を生成
C = np.random.randint(0, 10, (80, 100))

# BとCを掛け合わせてランク80の100x100の行列を生成
A = np.dot(B, C)
norm_A = np.linalg.norm(A)
A = A/norm_A*100

# 行列とそのrank数を表示
print('matrix A\n', A.round(2), A.shape)
print('rank: ', matrix_rank(A))

# Numpyの超便利ツールで特異値分解（SVD）を一発でやる
#full_matrices=Falseで、Σをr×rの対角行列として計算するように矯正してます
U, Σ, Vt = np.linalg.svd(A, full_matrices=False)

# 結果を表示（少数第二位までで四捨五入）
print("U:\n", U.round(2), U.shape)
print("Σ:\n", Σ.round(2), Σ.shape)
print("Vt:\n", Vt.round(2), Vt.shape)

# 11番目以降の要素をすべて0にする
Σ[10:] = 0
Σ_lora = Σ
# 結果を表示
print("Σ_lora\n", Σ_lora.round(2))

Sigma_lora = np.diag(Σ_lora)

#低rank近似したΣ_loraを使って行列Aを再構成
A_lora = U @ Sigma_lora @ Vt
print('matrix A_lora\n', A_lora.round(2))
print('matrix A\n', A.round(2), A.shape)

# ランダムな100次元ベクトルを生成
vector = np.random.randn(100)

# ベクトルのノルム（絶対値）を計算
norm = np.linalg.norm(vector)

# ベクトルをノルムで割って正規化
normalized_vector = vector / norm

Linear_A = np.dot(A, normalized_vector)
Linear_A_lora = np.dot(A_lora, normalized_vector)
cosine_similarity = np.dot(Linear_A, Linear_A_lora) / (np.linalg.norm(Linear_A) * np.linalg.norm(Linear_A_lora))
print("cosine_similarity:", cosine_similarity)
```

```sh
matrix A
 [[0.93 0.96 0.96 ... 0.78 0.7  0.93]
 [1.2  1.28 1.1  ... 0.91 0.85 1.13]
 [0.99 1.07 0.94 ... 0.75 0.7  0.95]
 ...
 [1.12 1.24 1.08 ... 0.9  0.79 1.09]
 [1.06 1.19 1.09 ... 0.92 0.94 1.06]
 [1.04 1.16 0.9  ... 0.83 0.8  1.07]] (100, 100)
rank:  80
U:
 [[-0.09 -0.03  0.06 ...  0.01  0.01 -0.35]
 [-0.1  -0.15  0.13 ...  0.25  0.1   0.02]
 [-0.09 -0.11 -0.16 ...  0.17 -0.06 -0.11]
 ...
 [-0.11  0.06 -0.05 ... -0.05  0.02  0.1 ]
 [-0.1   0.1   0.13 ...  0.02 -0.03 -0.08]
 [-0.09 -0.18  0.17 ...  0.03  0.06  0.13]] (100, 100)
Σ:
 [9.99e+01 1.18e+00 1.11e+00 1.07e+00 1.04e+00 9.70e-01 9.60e-01 9.00e-01
 8.80e-01 8.50e-01 8.30e-01 8.10e-01 7.70e-01 7.40e-01 7.30e-01 7.10e-01
 7.00e-01 6.80e-01 6.40e-01 6.30e-01 6.20e-01 6.10e-01 5.70e-01 5.60e-01
 5.30e-01 5.20e-01 5.10e-01 4.90e-01 4.70e-01 4.60e-01 4.40e-01 4.40e-01
 4.20e-01 4.20e-01 4.10e-01 4.00e-01 3.80e-01 3.70e-01 3.50e-01 3.40e-01
 3.30e-01 3.20e-01 3.10e-01 3.00e-01 3.00e-01 2.80e-01 2.70e-01 2.60e-01
 2.50e-01 2.40e-01 2.20e-01 2.20e-01 2.00e-01 1.90e-01 1.80e-01 1.80e-01
 1.70e-01 1.60e-01 1.60e-01 1.50e-01 1.30e-01 1.20e-01 1.20e-01 1.10e-01
 1.10e-01 1.00e-01 1.00e-01 9.00e-02 8.00e-02 8.00e-02 7.00e-02 6.00e-02
 6.00e-02 5.00e-02 5.00e-02 5.00e-02 4.00e-02 4.00e-02 3.00e-02 2.00e-02
 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00
 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00
 0.00e+00 0.00e+00 0.00e+00 0.00e+00] (100,)
Vt:
 [[-0.11 -0.11 -0.11 ... -0.09 -0.08 -0.11]
 [-0.08 -0.19  0.1  ...  0.07  0.06 -0.09]
 [ 0.06  0.03 -0.08 ...  0.13  0.14  0.05]
 ...
 [ 0.   -0.09  0.08 ...  0.09  0.1  -0.16]
 [ 0.    0.06  0.09 ...  0.13  0.03  0.04]
 [-0.52  0.02  0.04 ...  0.13  0.1  -0.08]] (100, 100)
Σ_lora
 [99.9   1.18  1.11  1.07  1.04  0.97  0.96  0.9   0.88  0.85  0.    0.
  0.    0.    0.    0.    0.    0.    0.    0.    0.    0.    0.    0.
  0.    0.    0.    0.    0.    0.    0.    0.    0.    0.    0.    0.
  0.    0.    0.    0.    0.    0.    0.    0.    0.    0.    0.    0.
  0.    0.    0.    0.    0.    0.    0.    0.    0.    0.    0.    0.
  0.    0.    0.    0.    0.    0.    0.    0.    0.    0.    0.    0.
  0.    0.    0.    0.    0.    0.    0.    0.    0.    0.    0.    0.
  0.    0.    0.    0.    0.    0.    0.    0.    0.    0.    0.    0.
  0.    0.    0.    0.  ]
matrix A_lora
 [[0.97 1.01 0.97 ... 0.82 0.73 0.97]
 [1.2  1.28 1.11 ... 0.92 0.88 1.11]
 [0.99 1.09 0.94 ... 0.78 0.7  0.98]
 ...
 [1.11 1.17 1.09 ... 0.91 0.83 1.12]
 [1.14 1.16 1.11 ... 0.92 0.85 1.06]
 [1.03 1.12 0.91 ... 0.85 0.78 1.05]]
matrix A
 [[0.93 0.96 0.96 ... 0.78 0.7  0.93]
 [1.2  1.28 1.1  ... 0.91 0.85 1.13]
 [0.99 1.07 0.94 ... 0.75 0.7  0.95]
 ...
 [1.12 1.24 1.08 ... 0.9  0.79 1.09]
 [1.06 1.19 1.09 ... 0.92 0.94 1.06]
 [1.04 1.16 0.9  ... 0.83 0.8  1.07]] (100, 100)
cosine_similarity: 0.9858175814976664
```

コサイン類似度が約0.99ということで、類似度がより近くなりました。巨大な行列であれば、低rank近似の影響が大きくないだろうということが予想できる結果になりました。

# おわりに
LLMのFine Tuning手法として注目される「LoRA」の理論的に背景にある特異値分解（SVD）を理解することができました。正直、LLMの中身を理解するためには大学数学（特に線形代数）が必須となりますので、私の様に大学時代に単位取得したけど結構忘れている人はこれを気に復習するのをおすすめします。
また、大学数学を履修していない！という人はヨビノリとか超おすすめです。

https://youtu.be/svm8hlhF8PA?si=7JMAAEDVu4Z25QEg

実際、機械学習を勉強するための初歩として線形代数を理解するために役立てている人のコメントもありました！
>仕事で機械学習を勉強するのに、そのベースとなる線形台数と微分を本を買って勉強しようとおもいましたが、非常にすばらしい講義で時間的にも効率的に勉強できるので、利用させていただきます。大学で勉強したことを忘れた社会人でもいけます。ありがとうございます。

ヨビノリを見よう！