RAG with FAISS

RAG (Retrieval-Augmented Generation) 大家應該都知道了。至於 FAISS 是 Facebook AI Research(FAIR)開發的技術,主要用途是對海量高維度的資料作出相似度的比較。由於我是第一次看到 RAG 和 FAISS (Facebook AI Similarity Search) 一起用,所以做個筆記。

基本上 RAG 的知識庫可能是一本書、一本使用手冊、一堆 Facebook 的用戶資料等等。它們先分為不同的小段落,然後 tokenize,每個段落再 encode 為一個向量。同理,對使用者提問也做同樣的事,但此處理解為只產生一個向量。

由於知識庫的向量筆數很多, FAISS 為他們製作 Index。提問的向量用 FAISS 的函數中找出最接近的幾筆最相關的向量,再根據 index 反查出 text 原文。然後把 text 和使用者提問合在一起去問 LLM。

對 LLM 來說,它可以有自己的 tokenizer。總之,RAG 已經功成身退了。

假如不用 FAISS,純靠 Pytorch 的話,那麼要自己用 dot product 去比較相似性。因為每個 word 就可能對應到一個高維的 token,所以每個 pragraph 的向量就是所有組成 pragraph 的 word 的向量的平均值。

最後補充 DPR (Dense Passage Retriever)。顯然,它對於 question 和 context (passage) 用了兩套函數 – 所謂 dual encoder [1]。我們可以想像,對於 question 的編碼應該要力求精準,但是對於參考文件這部分,如果是一本百科全書、或是整個資料庫,那編碼時主要是求快!所以兩邊的編碼方式不太一樣、甚至 tokenizer 不太一樣 (但是相容) 應該也是合理的。

[REF]

  1. https://blog.csdn.net/qq_45668004/article/details/138256448

Adjusted R-Squared 小整理

Adjusted R2 是 R2 的進階版, 它考慮了模型中變數 (預測因子) 的數量, 懲罰那些濫竽充數的變數, 在多變量回歸 (multiple regression ) 中達到去蕪存菁的效果.

聽起來相當美好.  但我們怎麼知道何時需要它? 要是我們的 model 已經是最好了, 再調整它是否浪費時間? 我們可以先看 R2 的破綻在哪裡?

R2 = 1 – SSR/SST

若 SSR/SST < 1, 表示這是個真分數. 當我新增一個因子, 表現得和平均 (猜的期望值) 一樣好, 真分數 (分子分母加同樣數字) 愈加愈大, R 會愈來愈小. 所以加到沒用的因子, R 會變差 (Adjusted R 變差). 反過來, 如果我們是要減少變數, 也可以預測減掉一個變數是否有感?

Adjusted R² = 1 – [(1 – R²) * (n – 1) / (n – k – 1)]

  • 如果加入的變數有貢獻 (提高解釋力), Adjusted R² 會上升.
  • 如果加入的變數無貢獻 (p-value 高, 不顯著), Adjusted R² 會下降或持平.

基本上, k 如果增加一個變數, 但它對 R2 又無額外貢獻, 分母的 (n – k – 1) 變小, … 以小學程度的數學可知 Adjusted R² 變小.

順便偷渡一下 n. n -> ∞, (n – 1) / (n – k – 1) -> 1, Adjusted R² 越接近 R² (增加變數的懲罰很小). n 越小, 分母越小, 懲罰項越大, 讓 Adjusted R² 更容易小於 R², 這能更敏感地偵測冗餘變數 (overfitting). 但因為它效力強到難以分辨 feature 好壞, 所以一般不調整 n, 只調整 k.

舉例而言, 現在要估計房價模型. 變數有坪數、屋齡、噪音 (亂數) 三個, k = 3. 統計樣本 n = 100 個. 為了說明方便, 我們先在 Python 固定亂數種子. 做出一堆可以收斂的假資料.

import statsmodels.api as sm
import pandas as pd
import numpy as np

# 步驟 1: 固定亂數種子與資料(同之前)
np.random.seed(0)
n = 100
X = pd.DataFrame({
    '坪數': np.random.uniform(20, 200, n),
    '屋齡': np.random.uniform(1, 50, n),
    '噪音': np.random.normal(0, 1, n)
})
y = 5000 * X['坪數'] - 2000 * X['屋齡'] + np.random.normal(0, 10000, n)  # real y

安裝 library ( pip install statsmodels) 後, 很多東西都不用算. 包括 OLS (Ordinary Least Squares) model 都會建好. 因為太方便了, 所以定義一個會印出過程的 function.

# 函數:計算 R²/adj_r2、係數和 p-value
def compute_and_compare(model_vars, model_name):
    X_sub = sm.add_constant(X[model_vars])  # 加常數項
    model = sm.OLS(y, X_sub).fit()          # 擬合 OLS 模型
    
    # 計算 R² 和 adj_r2
    r2 = model.rsquared
    adj_r2 = model.rsquared_adj
    print(f"\n{model_name} (變數: {model_vars}):")
    print(f"  R² = {r2:.4f}")
    print(f"  Adjusted R² = {adj_r2:.4f}")
    
    # 得到估計係數
    params = model.params
    print("  估計係數(params):")
    for var in params.index:
        print(f"    {var}: {params[var]:.2f}")
    
    # 得到每個變數的 p-value
    pvalues = model.pvalues
    print("  每個變數的 p-value(顯著性檢定):")
    for var in pvalues.index:
        print(f"    {var}: {pvalues[var]:.4f} (顯著: {pvalues[var] < 0.05})")
    
    print()  # 空行分隔

讓 3 個變數的 model A 跑一下, 然後讓除掉噪音 (random noise, 不是房子旁邊有噪音), 用只有 2 個變數的 model B 跑一下.

# 步驟 2: 計算模型 A (有噪音)
compute_and_compare(['坪數', '屋齡', '噪音'], "模型 A (有噪音)")

# 步驟 3: 計算模型 B (無噪音)
compute_and_compare(['坪數', '屋齡'], "模型 B (無噪音)")

執行上面程式, 會印出:

模型 A (有噪音) (變數: ['坪數', '屋齡', '噪音']):
  R² = 0.9745
  Adjusted R² = 0.9736
  估計係數(params):
    const: -949.47
    坪數: 4995.64
    屋齡: -2003.71
    噪音: 42.60
  每個變數的 p-value(顯著性檢定):
    const: 0.9644 (顯著: False)
    坪數: 0.0000 (顯著: True)
    屋齡: 0.0000 (顯著: True)
    噪音: 0.9655 (顯著: False)


模型 B (無噪音) (變數: ['坪數', '屋齡']):
  R² = 0.9744
  Adjusted R² = 0.9737
  估計係數(params):
    const: -947.35
    坪數: 4995.64
    屋齡: -2003.72
  每個變數的 p-value(顯著性檢定):
    const: 0.9623 (顯著: False)
    坪數: 0.0000 (顯著: True)
    屋齡: 0.0000 (顯著: True)

我們先關心 Model A 的 R² = 0.9745 > Adjusted R² = 0.9736, 或曰 Adjusted R² < , 這個訊號是提示我們的 model 可能有冗餘的變數存在! 調一調會更好! 至於誰是老鼠屎? 這時還看不出來.

但我們也印出了 p-value, 噪音的 p-value = 0.9655 很不顯著, 所以就抓它了! 在 Model B, 我們只用兩個變數.

此時 R² = 0.9744 > Adjusted R² = 0.9737 仍然成立, 或曰 Adjusted R² < 就是成立, 那這個時候戰犯要抓誰呢?

const 這個 p-value 值雖然大了點, 但是通常不會是標的. 而且坪數和屋齡的 p-value 都為 0 了, 表示極為顯著! 基本上就沒得挑了. 這也告訴我們指標並非絕對的, 要綜合起來使用.

Model B 的 R² 雖然輸給 Model A (0.9744 < 0.9745), 但我們本來就是抓 overfitting (模型在訓練資料上表現好, 但在未見過的資料上表現差), 所以不會太在意這裡掉了一點. 反而是看 Adjusted R² 從 0.9736 升到 0.9737, 還拔掉一個 p-value 很大的變數, 故改用 model B 是值得肯定的方向.

R-Squared 小註解

原本寫完兩篇 R, 就要來寫 R2, 我覺得這個哏不錯! 不過呢, 因為寫著寫著歪樓了, 所以把那邊的主題讓給 P-value, R2 放到後面來.

前面說到 P-value 用來衡量變數的顯著性. 由於我舉的是單一變數的 t-test, 好像 p-value 就足以解釋整個 model. 但真正能解釋整個 model 的是 R2 , 說明如下. 假設:

全部的誤差 SST = SSR + SSE .

SSR = residual variation = model 無法解釋的 (實際觀測值 – 模型預測值)

SSE = regression model variation = model 解釋掉的部分 (模型預測值 – y 的平均值)

R2 代表模型解釋變異的能力, 所以是 model 可以解釋的變異的比重 = SSE / SST = 1 – SSR / SST. 所以怎麼看 R 都是介於 0~1 之間. R 越接近 1, 表示 model 解釋得更好.

換個生活化的例子來說明.

  • 假設抽樣調查了三對父子, 記下父親與兒子的身高 (單位:公分):
父親身高 (x)兒子身高 (y)
170172
175177
180179
  • 新手根據網路謠言, 建構了下面這個回歸模型:
  • 我們來檢驗看看. 首先將每個父親的身高 x 帶入計算, 預測兒子身高 y^.
父親身高 (x)兒子實際身高 (y)預測兒子身高 y^
170172177.4
175177178
180179178.6
  • 計算 R2
  1. 計算 yˉ​= (172+177+179) /3 ​=528/3​ ≈ 176 — 兒子的平均身高
  2. 計算 SST (Sum of squared Total) = (172−176)2 + (177−176)2 + (179−176)2 = (−4)2+(1)2+(3)2=16+1+9=26 — 兒子的身高和平均身高的差的平方和
  3. 計算 SSR (Sum of Squared Residual) = (172-177.4)2 + (177-178)2 + (179-178.6)2 = 29.16+1+0.16=30.32 — 兒子的身高和預測身高的差的平方和

R2 = 1 – SST / SSR = 1 – 30.32 /26 = 1 – 1.166 ≈ -0.166

因為 R2 < 0, 或 SST > SSR, 等於是說: 全部猜平均身高的話, 還比用 model 估來得準. 所以此 model 很不好!

如果我們一項一項分開算. SSE = (177.4−176)2 + (178−176)2 + (178.6−176)2 = 1+4+6.76 = 12.72

SST ≠ SSR + SSE 或 26 ≠30.32 + 12.72

為何會有這樣的結果呢? 主要是這個 model 不正確, 所以什麼都解釋不了. 發生這種問題可能的原因包括:

  • 模型套錯:使用的模型和資料的型態根本不吻合(例如線性模型去解釋非線性資料)
  • 資料點太少:樣本數極少,容易受到極端值影響
  • 預測變數選錯:所用的 x、公式、或回歸參數明顯偏離資料特性
  • 資料雜訊過高:資料本身的變異無法用簡單模型捕捉

讓我們只用這三筆資料來生成合理的 linear regression model, 並重新計算 R2. 這個部分就請 AI 來執行了.

1. 原始資料
父親身高 (xx)兒子身高 (yy)
170172
175177
180179
2. 計算步驟
步驟一:計算平均值
步驟二:計算斜率 (b)


b=0.7

步驟三:計算截距 (a)

a=53.5

3. 生成線性回歸方程式

​解釋為: 父親每增加 1 公分身高, 兒子的預測身高增加 0.7 公分.

4 . 重新計算 R2

這個新模型不就超準了嗎? 但我們這裡只是建立 model, 並且評估其效果. 還不可以把它視為真正能實用. 下圖可以看到網路謠言版和依照數學原理建立的 model 有何差異? 網路謠言 model 不能解釋我們的資料庫, 但實測會不會更好? 這件事無法反映得出來.

T-Test 和 P-Value

雖然今天有望創下投資市值新高 (舊紀錄是 2025/2/20) – 台股平平、日股創高、美股小漲還沒收市, 結果還不好說 . 但這篇的重點是整理上班會用到的一門統計技術.

假設我有大把的 features, 要怎麼知道每個因子對 model 有沒有影響? 以及因子之間是否過於相關 (例如華氏度和攝氏度) 而顯得多餘呢? 讓我們從基本問題開始理解.

首先我們會遇到每個因子的 scale 差異很大. 例如預測房價時, 我們可能有屋齡、坪數、坪單價、附近實價登錄總價等因子. 前兩個算它們從 0~1,000 應該就夠用. 甚至普通人應該不會買千坪豪宅、千年老宅 (古剎?). 至於後兩個就有趣了. 假設我們用萬元為單位, 那麼 後兩者的 order 可能差個 10 倍到上百倍 (10~100 坪), 更不用說和前兩個因子擺在一起比較, 總會有些因子顯得參差不齊~~~

甚至那些 one hot encoding 來的, 根本就是單位向量, 這時要怎麼辦呢? 我們可以對 skewed 的變數取對數 log, 或用標準化 (z-score) 將所有因子拉到類似範圍。這類轉換能處理加法和乘法效應的混合 [1],讓模型更公平. 這部分先帶過, 減少數學、訴求直覺就好.

接下來要篩選出有用的因子, 把雜質打掉, 才能真正抓到關聯性. 我們要先檢查因子之間的相關性 (避免如華氏/攝氏度的多餘), 並確保數據的有效性. 如果數據本身沒意義, 其他動作都是多餘的.

以下就介紹 t-test 和 p-value:它們是用來測試每個因子是否真的對目標變數 (如房價) 有顯著影響. 簡單說, t-test 測量因子的 “效果強度", p-value 則告訴你這種效果在隨機情況下發生的機率有多高— p 小表示影響顯著!

顯然, 我們要做一些測試才會產生測量因子. 假設我們用了單樣本測試 (One-sample t-test), t 公式表示如下 (t 公式還有很多型態).

例如測試新藥是否使血壓平均降至 120 mmHg, 多筆測量出的血壓為 x, 假設值 μ0 為吃了藥但無效果的血壓 (虛無假設), 此時可以看出 x 的平均值 xˉ 並不是 μ0. 假設此藥對某人藥效不佳, 吃了高血壓藥, 收縮壓從 140 變成 150, 差異是 +10, 或是對某人藥效太強, 收縮壓從 135 變成 90, 差異就是 -45.

  • 假設資料(血壓變化值,單位 mmHg):
    • 受試者 1: -5(血壓下降 5)
    • 受試者 2: -3
    • 受試者 3: -7
    • 受試者 4: -4
    • 受試者 5: -6
    • 樣本平均 xˉ=−5xˉ=−5(整體平均下降 5 mmHg)
    • 樣本標準差 s ≈ 1.58
    • 樣本大小 n = 5
    • 虛無假設 μ₀ = 0(藥物無效果)

此時 t = (-5-0)/(1.58 / √5)=-5/​0.707≈−7.07,

t 可以表示出藥效過高或試過低. 由於 t 就是一個正規化的過程. 所以 x 不需要預先做正規化處理.

另外還有個 p-value, 它的計算不是公式, 而是機率的累積函數 CDF (cumulative distribution function). 再以血壓藥為例, p 是紅線以下的面積 + 右邊對稱位置的面積 (所以 x2).

p=2×(1−CDFt​(∣−7.07∣,df=4)) = 0.0021

也就是在自由度 [2] df = n – 1 = 5 – 1 = 4 的情況下, 落在分布長尾部分的機率 (tail probability).

用實際的數字翻譯上述公式就是: t ≈ -7.07, df=4, p ≈ 0.0021 解釋為: 如果藥物無效(H0:μ=0),則發生五個受試者 t = -7.07 或更極端 (更負或更正) 的機率只有 0.2%. 隱含這種事不太可能發生, 藥物相當可能是有效的.

換個角度, 如果大家試完藥, t -> 0, 測起來像是這個藥吃了沒效. 但 p -> 1 只是表示這個測試的證據力很低, 表示無法證明這個藥有效, 而不是證明這個藥非常無效.

[REF]

  1. http://ch.whu.edu.cn/cn/article/doi/10.13203/j.whugis20210659?viewType=SUP
  2. 自由度 – 在計算樣本標準差(s)時,我們先用樣本平均值 (xˉ) 來「約束」資料: 一旦知道平均值, 前 n-1 個值可以自由變化, 但最後一個值必須調整以符合平均值. 這 “犧牲" 了 1 個自由度. 當自由度愈低, 表示愈不確定, 長尾就比較肥. 對同一個 t, p-value 相對大.

利率公式小整理

自從川普 2.0 之後, 讓我想要惡補一下財經這個領域. 回想起當時亂花的錢, 流失的歲月和破碎的記憶. 先來複習十六年前讀過的東西好了.

[常用縮寫]

  • T = numbers of annuity payments
  • r = the interest rate
  • g = growing rate
  • Po = Present value of annuity
  • PMT = Payment
  • A = annuity payment
  • PT = Expected future value of investment
  • n = number of compounding periods
  • FV = Future value of an annuity
  • PV = Present value of cash flows

[基本公式]

PT = Po(1 + r)T

Po = PT(1 + r)-T

  • if Perpetuities, T ->∞,

Po = (A/r) (1-(1 + r)-T) = A/r

[縮寫公式]

Annual Compound Factor = ACF(r, n) = [(1+r)n −1]/r

Annual Discount Factor = ADF(r, n) = (1−(1+r)-n)/r

[分期付款]

貸款金額的複利本利和 = 每月還款金額的 ACF,

假設貸款 100 萬, 每個月還 1 萬, 一共還 10 年, 共 120 期.

理解為現在的 100 萬, 放著會生利息, 最後得到 Fv. 由貸款人每個月 PMT 的 ACF 償還.

100 * (1+r)120 = ACF(r, 120) = [(1+r)120 −1]/r

100 = [1-(1+r)-120 ]/r

PV = PMT * [1-(1+r)-n ]/r

[不斷配息的股票]

有個股票, 每年無止盡地配息 100 元, 堪比永動機, 股價為何不是無限大?

Perpetuity is a series of equal payments of a fixed amount for an
infinite number of periods.

  • if Perpetuities, T ->∞,

Po = (A/r) (1-(1 + r)-T) = A/r

假設機會成本為 5%, 該股票只值 2,000 元.

[利息成長的股票] (Growing Anuity)

假設這個股票不只是每年配息 A, 利率 r, 還會以 g 的比例成長, 那價值應該更高了吧!

第一年, 配息 A

第二年, 配息成長為 A(1+g)

第 n 年, 配息成長為 A(1+g)n-1, 折現為 A(1+g)n-1 / (1+r)n

將無限多年的折現現金流加起來:

PV = ∑t=1A(1+g)t−1​ / (1+r)

把 1+r 和 1+g 的指數弄成一樣 PV = A / (1+r) * ∑t=1 ​((1+g) / (1+r))t-1

因為 ∑t=0x=1 / (1−x) 當 ​∣x∣<1. 也就是末項趨近於 0, 首項為 1 的等比級數公式特例.

其中, 乘號左邊為 A / (1+r), 右邊為 1 / (1-x), 其中 x = (1+g)/ (1+r)

右邊 = 1 /( (1+r-1-g)/ (1+r)) = 1 /( (r-g)/ (1+r)) = (1+r) / ( r-g)

PV = (A / (1+r)) * (1+r)/ (r-g) = A / (r-g)

當然 r > g 的時候本公式都符合直覺. 忘掉一切, 只記得 A / (r-g) 就好.

[利息成長的縮寫公式]

Annual Discount Factor = ADF(r, n, g) = 上面沒投機化簡的 PV = A / (1+r) * ∑t=1n ​((1+g)/ (1+r))t-1 ,

老老實實地按等比級數求和展開, 乘號右邊 ∑t=1n ​((1+g)/ (1+r))t-1 = 1-(1+g)/ (1+r))n / ( 1- ​((1+g)/ (1+r)))

PV = A / (r-g) *(1- ((1+r)/(1+g)) n-1)

同樣的思路可以推導出, ACF (r.n.g) = A/(r-g) * ((1+r)n – (1+g)n)

以前除了手算, 考試可以用財務計算機. 我手上的 TI BA II Plus 已經沒電了, 但沒有換電池的必要, 現在有 Excel 處理這些東西, 更可以無腦問 AI. 不過呢, 直覺是慢慢培養起來的, 一直問 AI, 自己沒辦法產生敏銳度~~~