# 課程連結
[CS50 Artificial Intelligence](https://cs50.harvard.edu/x/2024/weeks/ai/#artificial-intelligence)
<iframe width="560" height="315" src="https://www.youtube.com/embed/6X58aP7yXC4?si=sf3FynyjcjYcOdPo" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
# 1. Image Generation
> 參考資料:
>- [基於 Diffusion Models 的生成圖像演算法](https://d246810g2000.medium.com/%E5%9F%BA%E6%96%BC-diffusion-models-%E7%9A%84%E7%94%9F%E6%88%90%E5%9C%96%E5%83%8F%E6%BC%94%E7%AE%97%E6%B3%95-984212710610)
>- [AIAIART #7](https://colab.research.google.com/drive/1NFxjNI-UIR7Ku0KERmv7Yb_586vHQW43?usp=sharing#scrollTo=g7btoXL7Im7M)
>- [原始論文:Denoising Diffusion Probabilistic Models](https://arxiv.org/abs/2006.11239)
## 常見圖片生成
![](https://lilianweng.github.io/posts/2021-07-11-diffusion-models/generative-overview.png)
- Autoencoder [2006]
- Variational Autoencoder (VAE) [2014]
- Flow-based models
- GAN [2014]
- PixelRNN [2016]
- Diffusion [2020]
## Defusion Model
Defusion Model 是模仿Markov chain,利用常態分佈(高斯分佈)逐漸對一張圖片增加雜訊,直到看不到原始的圖片(下圖的q),再利用模型一步一步把圖片還原回來(下圖的p)
![image](https://hackmd.io/_uploads/BJz50T7LR.png)
可以看下左這張圖逐漸從雜訊中生出一張圖。
<div style="display: flex; justify-content: space-around;">
<div style="text-align: center;">
<img src="https://miro.medium.com/v2/resize:fit:640/format:webp/1*4GmL_x9gzQu3uiH8JKdfaQ.gif" alt="Image 1" style="max-width:97%;">
<p>生成過程</p>
</div>
<div style="text-align: center;">
<img src="https://truth.bahamut.com.tw/s01/202304/4adfdcb747b978c794f5709339986662.JPG" alt="Image 2" style=";">
<p>我的train壞掉的版本</p>
</div>
</div>
### Diffusion process (擴散過程)
下面是的意思是,我們會逐步對一張圖片加上常態分配的雜訊,這個步驟總共會做T次,其中的一次叫做t。`Beta_t`是一個會隨著t越大而增加的值,而雜訊的常態分配的平均數(mean)是`Beta_t`*上一步驟t-1的圖片, 變異數(var)是`Beta_t`,可以算出雜訊eps, 接著新的圖片會是`main + √var*eps`。
我們模型要訓練的其實是給定一張被雜訊污染的`X_t`,要求模型幫我們預測出是什麼雜訊污染他,也就是預測`eps`
> 公式
$$
q(\mathbf{x}_{1:T} | \mathbf{x}_0) := \prod_{t=1}^T q(\mathbf{x}_t | \mathbf{x}_{t-1}), \quad
q(\mathbf{x}_t | \mathbf{x}_{t-1}) := \mathcal{N}(\mathbf{x}_t; \sqrt{1 - \beta_t}\mathbf{x}_{t-1}, \beta_t \mathbf{I})
$$
```python=
n_steps = 100
beta = torch.linspace(0.0001, 0.04, n_steps)
def q_xt_xtminus1(xtm1, t):
# gat
mean = gather(1. - beta, t) ** 0.5 * xtm1 # √(1−βt)*xtm1
var = gather(beta, t) # βt I
eps = torch.randn_like(xtm1) # Noise shaped like xtm1
return mean + (var ** 0.5) * eps
def gather(consts: torch.Tensor, t: torch.Tensor):
"""
Gather consts for $t$ and reshape to feature map shape
用來提取一個tensor中第t個值,並錢reshape
"""
c = consts.gather(-1, t)
return c.reshape(-1, 1, 1, 1)
```
### Reverse process (逆擴散過程)
上面說到模型需要給定圖片`x_t`,預測雜訊`eps`,實際上是要讓模型學會產生`eps`的常態分配的平均值和標準差。
有了上面的`beta_t`,可以算出`alpha_t = 1 - beta_t`, `alpha_bar_t = 從 t=0 連乘到t=t`,接著按下面步驟算出:
1. 雜訊的係數 eps_coef = `(1-alpha_t )/ √(1-alpha_bar_t)`
2. 平均mean = `(1/(√alpha_t))*(x_t - eps_coef * noise)`
3. var: 就是`beta_t`
4. 新的雜訊eps: 用torch生成隨機數
5. 新的圖片 `main + √var*eps`
> 公式:
$$
p_\theta(\mathbf{x}_{0:T}) := p(\mathbf{x}_T) \prod_{t=1}^T p_\theta(\mathbf{x}_{t-1}|\mathbf{x}_t), \quad
p_\theta(\mathbf{x}_{t-1}|\mathbf{x}_t) := \mathcal{N}(\mathbf{x}_{t-1}; \mu_\theta(\mathbf{x}_t, t), \Sigma_\theta(\mathbf{x}_t, t))
$$
```python=
# Set up some parameters
n_steps = 100
beta = torch.linspace(0.0001, 0.04, n_steps).cuda()
alpha = 1. - beta
alpha_bar = torch.cumprod(alpha, dim=0)
def p_xt(xt, noise, t):
"""
x_t:被污染的圖片
noise:模型看著x_t預測出來的雜訊
t:現在是第幾步驟
"""
alpha_t = gather(alpha, t)
alpha_bar_t = gather(alpha_bar, t)
eps_coef = (1 - alpha_t) / (1 - alpha_bar_t) ** .5
mean = 1 / (alpha_t ** 0.5) * (xt - eps_coef * noise) # Note minus sign
var = gather(beta, t)
eps = torch.randn(xt.shape, device=xt.device)
return mean + (var ** 0.5) * eps
```
### Algorithm 演算法
![image](https://hackmd.io/_uploads/HJw4C6Q8C.png)
#### Loss Function
學習的時候會使用下面這個loss function來計算,其實就是算出污染`x_t`圖片的雜訊`eps_t`與模型預測的雜訊`pred_noise_t`平方差的平方 (mse)
$$
L_{\text{simple}}(\theta) := \mathbb{E}_{t, \mathbf{x}_0, \epsilon} \left[ \left\| \epsilon - \epsilon_\theta \left( \sqrt{\bar{\alpha}_t} \mathbf{x}_0 + \sqrt{1 - \bar{\alpha}_t} \epsilon, t \right) \right\|^2 \right]
$$
#### Training
重複以下4個步驟直到loss降低
1.訓練時先從我們想訓練的圖片集(`q(x_theata`)中挑出一張 `x_theata`
2.設定總訓練步驟數大T(也就是上面設定的`n_step`),中**隨機**挑朱一個步驟做
3.先用常態分佈隨機出一個雜訊`eps`
4.用上面的loss function算出`eps`與模型預測的雜訊`pred_noise`的平方差的平方,然後做gradient descent
<div style="display: flex-ㄏㄠ; justify-content: space-around;">
<div style="text-align: left; width: 100%;;">
<b>Algorithm 1</b> Training
<pre>
1: repeat
2: <b>x</b><sub>0</sub> ~ <i>q</i>(<b>x</b><sub>0</sub>)
3: <i>t</i> ~ Uniform({1, ..., T})
4: <b>ε</b> ~ <i>𝒩</i>(0, <b>I</b>)
5: Take gradient descent step on
∇<sub>θ</sub> || <b>ε</b> - <b>ε</b><sub>θ</sub>(√<span style="text-decoration: overline;">α</span><sub>t</sub><b>x</b><sub>0</sub> + √1 - <span style="text-decoration: overline;">α</span><sub>t</sub><b>ε</b>, <i>t</i>) ||<sup>2</sup>
6: until converged
</pre>
</div>
</div>
#### Sampling
訓練好之後就可以來產圖了,產圖流程如下:
1. 先獲得一個雜訊,就做`x_T`
2. 執行大T(n_step)次去噪動作,以下為loop
1. 使用訓練好的model產生預測的雜訊`pred_eps`
1. 使用[Reverse process (逆擴散過程)](#Reverse-process-逆擴散過程)的公式,幫圖片降噪,並還要加上一個小雜訊
3. loop完之後圖片產出
<div style="display: flex-ㄏㄠ; justify-content: space-around;">
<div style="text-align: left; width: 100%;">
<b>Algorithm 2</b> Sampling
<pre>
1: <b>x</b><sub>T</sub> ~ <i>𝒩</i>(0, <b>I</b>)
2: for <i>t</i> = T, ..., 1 do
3: <b>z</b> ~ <i>𝒩</i>(0, <b>I</b>) if <i>t</i> > 1, else <b>z</b> = 0
4: <b>x</b><sub>t-1</sub> = <sup>1</sup>/<sub>√α<sub>t</sub></sub> (<b>x</b><sub>t</sub> - <sup>1 - α<sub>t</sub></sup>/<sub>√1 - <span style="text-decoration: overline;">α</span><sub>t</sub></sub> <b>ε</b><sub>θ</sub>(<b>x</b><sub>t</sub>, <i>t</i>)) + σ<sub>t</sub><b>z</b>
5: end for
6: return <b>x</b><sub>0</sub>
</pre>
</div>
</div>
### 模型 UNet
> 參考資料
>- [UNet 原始論文:U-Net: Convolutional Networks for Biomedical Image Segmentation](https://arxiv.org/abs/1505.04597)
>- [ConvTranspose2d原理,深度网络如何进行上采样?](https://blog.csdn.net/qq_27261889/article/details/86304061)
U Net 是用來預測雜訊 `pred_eps`的模型本體,他可以輸入兩個值:
1. 用來預測的被污染的圖片`x_t`
2. 現在是第幾個步驟t
步驟是
1. down: 2次的 3\*3 convolution 搭配一次2\*2 max pool,共執行4次
2. middle(bottom),兩次的convolution,在這裡要加數步驟t的embedding資訊,模型才會知道現在在第幾步驟
3. up:2次的 3\*3 convolution 搭配一次2\*2 up-conv(逆convolution),共執行4次
![image](https://hackmd.io/_uploads/HJDoCe4LA.png)
# 2. Deep Learning
> 參考資料
> - [李宏毅. 2016d. ML Lecture 6: Brief Introduction of Deep Learning. YouTube](https://www.youtube.com/watch?v=Dr-WRlEFefw.)
> - [李宏毅. 2016g. ML Lecture 11: Why Deep? YouTube.](https://www.youtube.com/watch?v=XsC9byQkUH8)
> - [李宏毅. 2016d. ML Lecture 6: Brief Introduction of Deep Learning. YouTube.](https://www.youtube.com/watch?v=Dr-WRlEFefw)
> - [李宏毅. 2017a. ML Lecture 5: Logistic Regression. YouTube.](https://www.youtube.com/watch?v=hSXFuypLukA)
> - [李宏毅. 2016a. ML Lecture 1: Regression - Case Study. YouTube.](https://tdr.lib.ntu.edu.tw/jspui/bitstream/123456789/8399/1/U0001-1204202115134100.pdf)
> - [Glorot, X., and Y. Bengio. 2010. Understanding the difficulty of training deep feedforward neural
networks. ](https://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf)
## 名稱的由來
1958年,Frank Rosenblatt 提出 Perceptron 演算法,用於線性分類。1962年,Marvin Minsky 批評其局限性,導致研究熱潮迅速平息。1980年代中期,Multilayer Perceptron(多層感知器)被提出,實際上是多層 Logistic Model 的連接,被稱為 Neural Network(神經網路)。1986年,Rumelhart 等提出 Back-propagation 演算法,可調整神經網路中各單元的權重。然而,隱藏層超過三層時效果不佳,1989年,多數學者認為一層隱藏層足夠。
1999年後,隨著GPU的發展,訓練多層隱藏層變得可能,Multilayer Perceptron 被重新命名為 Deep Learning(深度學習)。2006年,Hinton等人提出使用 Restricted Boltzmann machines (RBM) 來初始化深度學習的權重,吸引了許多研究者的關注。一些人認為使用RBM才算是深度學習,但隨著研究深入,發現不需要RBM也能訓練深度學習模型。因此,Deep Learning 與 Multilayer Perceptron(Neural Network)成為同一種演算法的不同名稱,「深度學習」與「神經網絡」常被交互使用。
## 深度學習5個組件
### 1. Activation Function
Activation function 是深度學習中最基本的單位,可以想像它是神經網路中的
一個節點(node),執行最簡單的非線性轉換。 常見的有Sigmoid 和 ReLU等:
> **Sigmoid 函數:**
$$
\sigma(x) = \frac{1}{1 + e^{-x}}
$$
> **ReLU 函數:**
$$
\text{ReLU}(x) = \max(0, x)
$$
Activation function 可以產生相當於邏輯運算子的效果,舉例來說,如果有四的點,分別為(0,0)、(1,0)、(0,1)、(1,1)。如果要將(0,0)與(1,1)分為一類,(1,0)與(0,1)分為另一類,會發現若單純使用一次方程會不能輕易區分。
但如果我們用以以下兩個Function做轉換
$$
\text{w} = \max(0, x - 0.5 y)
$$
$$
\text{z} = \max(0, -0.9x + y)
$$
轉換後的四個點分別為(0,0)、(0.5,0.1)、(1,0)、(0,1),就可以被一次方程分成兩邊。
<div style="display: flex; justify-content: space-around;">
<div style="text-align: center;">
<img src="https://hackmd.io/_uploads/SyXMZjjU0.png" alt="Image 1" style="max-width:95%;">
<p>轉換前</p>
</div>
<div style="text-align: center;">
<img src="https://hackmd.io/_uploads/SJY7Wss8R.png" alt="Image 2" style="max-width:100%;">
<p>轉換後</p>
</div>
</div>
### 2. 深度學習架構
深度學習架構可以靈活變化以滿足不同需求。這邊使用最基礎的 Fully Connected Feedforward Network 架構,由 Input Layer、Output Layer 和任意數量的 Hidden Layer 組成。
1. **Input Layer**:這一層並不是真正的一層,而是表示輸入向量。每個自變數都會輸入到下一層的所有節點中。
2. **Hidden Layer**:這層是由任意數量的 Activation Function 組成的,可以有多層。每層的節點數量和使用的 Activation Function 可以不同。例如,第一層可以使用5000個 Sigmoid 函數,而第二層可以使用3000個 ReLU 函數。從 Input Layer 得到的自變數在每個節點會產生一個值,這些值作為新的自變數輸入到下一層。每個 Hidden Layer 的輸出都會輸入到下一層的所有節點中,最終輸出到 Output Layer。
3. **Output Layer**:這一層作為分類器,將經過 Hidden Layer 非線性轉換的資料分類。例如,經過多層非線性轉換後,資料點會轉換成容易被分類的型態,找出資料的特徵(Feature),再利用 Output Layer 把資料分類成多個類別。常用的分類函數是 Softmax function,它將 hidden layer 中的資料壓縮到 0 到 1 之間,計算出每個類別的後驗機率,最大值對應的類別即為預測類別。
$$
\text{Softmax}(x_i) = \frac{e^{x_i}}{\sum_{j=1}^{N} e^{x_j}}
$$
4. **Fully Connected Feedforward Network**:這是最常見的連接各層的方法。每個節點的輸出都會傳遞到下一層的所有節點,而每個節點也會使用上一層所有節點的輸出。這種結構被稱為 Fully Connected,資料從 Input Layer 傳遞到 Hidden Layer,再傳遞到 Output Layer,呈現順向結構。
這種架構允許任意改變以適應不同的需求,並提供了強大的靈活性和可擴展性,使其成為深度學習領域的基礎。
<div style="display: flex; justify-content: space-around;">
<div style="text-align: center;">
<img src="https://hackmd.io/_uploads/Hk94VsjU0.png" alt="Image 1" style="max-width:95%;">
<p>Fully Connected Feedforward Network</p>
</div>
</div>
### 3. Loss Function
在深度學習中,評估模型好壞需要使用 Loss Function。Loss Function 通過比較模型預測結果與真實資料來計算一個分數(Loss),分數越低表示預測結果越接近真實資料,模型性能越好。常用的 Loss Function 包括 Cross Entropy,特別是在分類任務中。
Cross Entropy 的優勢在於當模型預測與真實資料相差較大時,微分後的斜率較大,有助於訓練,而平方差公式在相同情況下則較平緩,不利於訓練。Loss Function 必須可微,以便使用 Gradient Descent 找出最符合真實情況的模型。根據不同情況,也可以選擇其他 Loss Function 或自定義 Loss Function。
> Cross Entropy
$$
L = -\sum_{i=1}^{N} y_i \log(\hat{y}_i)
$$
<div style="display: flex; justify-content: space-around;">
<div style="text-align: center;">
<img src="https://hackmd.io/_uploads/B1KWUiiIR.png" alt="Image 1" style="max-width:95%;">
<p>紅色為平方差,黑色為Cross Entropy</p>
</div>
</div>
### 4. Gradient Descent
想像模型中的每個權重 (weight) 代表空間中的一軸,把所有權重帶入 Loss Function 中形成高低起伏的空間,每個點表示一組權重計算出的 Loss。最佳模型擁有最低的 Loss,即整個空間中最底點的位置。計算全部權重組合的 Loss 會耗費大量資源,因此使用 Gradient Descent 來找出答案。
Gradient Descent 從 Loss 組成的波浪中選擇一個點,朝著該點斜率的反方向走一步,重複此步驟直到斜率為0的最底點。要得到此斜率需要把 Loss Function 對每一個權重做偏微分,得到的
值組成一個向量就叫做梯度(Gradient),可用符號∇表示
而為了要求得最低值,需要將原本的權重減去 Gradient,才會朝最低值前進。原先的點差距太遠而非像最低點前進,於是在減 Gradient 時會乘上一個數來限制Gradient 的步伐長度,這個數稱之為 Learning Rate,可用符號 η 表示。以上步驟可表示如下:
$$
w^{(T+1)} = w^{(T)} - \eta \nabla \text{Loss}(w^{(T)})
$$
除了基本的 Gradient Descent,還有許多變形方法,如每次只使用一筆資料的 Stochastic Gradient Descent、處理不均衡自變數的 Adagrad,結合動量概念的 Adam。
### 5. Backpropagation
> 參考影片:
<iframe width="540" height="400" src="https://www.youtube.com/embed/ibJpTrp5mcE?si=ITgK5Ima4WK61AVT" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
Backpropagation是在Deep Leaning Model中計算Gradient的方法,在計算之前我們要先了解微積分的Chain Rule。
> Chain Rule:
如果有兩個function如下
$$
\begin{aligned}
y &= g(x) \\
z &= h(y)
\end{aligned}
$$
則$x$對$z$的微分如下:
$$
\frac{\mathrm{d}z}{\mathrm{d}x} = \frac{\mathrm{d}z}{\mathrm{d}y} \cdot \frac{\mathrm{d}y}{\mathrm{d}x}
$$
又如果有一個值 $s$ 同時影響$x$和$y$, 而$x$和$y$又影響$z$:
$$
\begin{aligned}
x &= g(s) \\
y &= h(s) \\
z &= k(x,y)
\end{aligned}
$$
則$s$對$z$的微分如下
$$
\frac{\mathrm{d}z}{\mathrm{d}s} = \frac{\partial{z}}{\partial{x}} \cdot \frac{\mathrm{d}x}{\mathrm{d}s} + \frac{\partial{z}}{\partial{y}} \cdot \frac{\mathrm{d}y}{\mathrm{d}s}
$$
接著我們先定義深度學習模型會要使用的幾個function:
>activation function 用sigmoid function
$$
a = \sigma_{z}
$$
> 填入activation function中的$z$如下
$$
z = x_1w_1 +x_2w_2 +b
$$
> loss function 用 Cross Entropy
$$
L(\theta) = \sum_{n=1}^{N} C^n(\theta)
$$
> 也可以改寫如下
$$
\frac{\partial L(\theta)}{\partial w} = \sum_{n=1}^{N} \frac{\partial c^n(\theta)}{\partial w}
$$
從下圖可以看到完整的function邏輯,input layer傳入參數$x_1$, $x_2$,並與$w_1$, $w_2$, $b$組成 $z$, 將$z$帶入 activation function $\sigma{(z)}$產出$a$, $a$再作為下一層layer的參數。
![圖片](https://hackmd.io/_uploads/BkihfRo80.png)
為了要更新參數 $w$ ,我們需要用$w$對Loss function做偏微分,依照chain rule如下:
$$
\frac{\partial{C}}{\partial{w}} = \frac{\partial{z}}{\partial{w}} \cdot \frac{\partial{C}}{\partial{z}}
$$
而我們可以輕易知道$\frac{\partial{z}}{\partial{w_1}}$就是$x_1$,而這個$x_1$從上一層傳過來的,因此也叫做前向傳播。
> 前向傳播 (Forward Propagation)
$$
\begin{aligned}
\frac{\partial{z}}{\partial{w_1}} = x_1 \\
\frac{\partial{z}}{\partial{w_2}} = x_2
\end{aligned}
$$
而$\frac{\partial{C}}{\partial{w}}$後面的部分是:
$$
\frac{\partial{C}}{\partial{z}} = \frac{\partial{a}}{\partial{z}} \cdot \frac{\partial{C}}{\partial{a}}
$$
其中activation function $\sigma(z)$的微分已知:
$$
\frac{\partial{a}}{\partial{z}} = \sigma'(z)
$$
而$\frac{\partial{C}}{\partial{a}}$又可以繼續如下(因為$a$出來的值又會向下影響到下一層的參數function $z'$ 和 $z''$)
> Chain Rule
$$
\frac{\partial{C}}{\partial{a}} = \frac{\partial{z'}}{\partial{a}} \cdot \frac{\partial{C}}{\partial{z'}} + \frac{\partial{z''}}{\partial{a}} \cdot \frac{\partial{C}}{\partial{z''}}
$$
從上面的圖片我們可以知道
$$
\begin{aligned}
\frac{\partial{z'}}{\partial{a}} = w_1 \\
\frac{\partial{z''}}{\partial{a}} = w_2
\end{aligned}
$$
雖然$\frac{\partial{C}}{\partial{z'}}$和$\frac{\partial{C}}{\partial{z''}}$仍然未知,但我們先假設我們知道怎麼算,可以從上面的算是中得出
$$
\frac{\partial{C}}{\partial{z}} = \sigma'(x)\left[w_3 \cdot \frac{\partial{C}}{\partial{z'}} + w_4\cdot \frac{\partial{C}}{\partial{z''}}\right]
$$
最後假設$z'$和$z''$下一層就是進入Output layer並產出預測的結果$y_1$和$y_2$, 並有相對應的答案(Ground Truth) $\hat{y}_1$和$\hat{y}_2$,我們便可以算出$\frac{\partial{C}}{\partial{z'}}$和$\frac{\partial{C}}{\partial{z''}}$:
$$
\begin{aligned}
\frac{\partial{C}}{\partial{z'}} = \frac{\partial{y_1}}{\partial{z'}} \cdot \frac{\partial{C}}{\partial{y_1}} \\
\frac{\partial{C}}{\partial{z''}} = \frac{\partial{y_2}}{\partial{z''}} \cdot \frac{\partial{C}}{\partial{y_2}}
\end{aligned}
$$
而我們知道了$\frac{\partial{C}}{\partial{z'}}$和$\frac{\partial{C}}{\partial{z''}}$就可以算出$\frac{\partial{C}}{\partial{z}}$
$$
\frac{\partial{C}}{\partial{z}} = \sigma'(x)\left[w_3 \cdot \frac{\partial{y_1}}{\partial{z'}} \cdot \frac{\partial{C}}{\partial{y_1}} + w_4\cdot \frac{\partial{y_2}}{\partial{z''}} \cdot \frac{\partial{C}}{\partial{y_2}}\right]
$$
再搭配前向傳播的$x_1$可以組出來:
$$
\frac{\partial{C}}{\partial{w_1}} = x_1 \cdot \frac{\partial{C}}{\partial{z}} \cdot \sigma'(x)\left[w_3 \cdot \frac{\partial{y_1}}{\partial{z'}} \cdot \frac{\partial{C}}{\partial{y_1}} + w_4\cdot \frac{\partial{y_2}}{\partial{z''}} \cdot \frac{\partial{C}}{\partial{y_2}}\right]
$$
我們就可以用$\frac{\partial{C}}{\partial{w_1}}$去調整$w_1$了
---
# 3. Embedding 與 向量資料庫
> 參考資料:
> - [極速ChatGPT開發者兵器指南:跨界整合Prompt Flow、LangChain與Semantic Kernel框架](https://www.books.com.tw/products/0010987469)
> - [TinyMurky/embed_and_vector_database_practice](https://github.com/TinyMurky/embed_and_vector_database_practice)
## Embedding
Embedding 是大型語言模型開發中的一個重要關鍵技術,可以將文字轉成依照向量 (vector)的方式存在,方便輸入到深度學習的模型中。經由特殊Embedding模型embed後的文字,還可以讓類似詞意的文字在向量空間中在一起。Embedding也不一定要限制在詞,也可以針對整個句子的句義抽取出訊息變成 vector (像是Openai 可以提供將句子變成 1,536為度的向量)
### One-hard encoding
最簡單的Embedding是 one-hard encoding,就是一個跟字典一樣長的vector,在該詞的位置上標上1,其他都是0。
例如 `你好嗎?` 就可以變成 `你`, `好`, `嗎`的字典,並表示如下:
1. 你:`[1, 0, 0]`
2. 好:`[0, 1, 0]`
3. 嗎:`[0, 0, 1]`
這樣的好處是很好Embedding,壞處是vector會變得太長。
### Hugging Face
我們可以用Hugging face上的模型`all-MiniLM-L6-v2`來做embed,他會把一整個句子直接抽取成 384為度的向量,如下面所表示
```python=
from sentence_transformers import SentenceTransformer
class EmbeddingModel:
"""
this class is responsible for the embedding model
"""
def __init__(self, model_name="sentence-transformers/all-MiniLM-L6-v2"):
self.model: SentenceTransformer = SentenceTransformer(model_name)
def encode(self, text: list[str]):
"""
change the texts into embeddings
"""
return self.model.encode(text)
```
下面我們將三個句子做Embed
```python=
def main():
embedding_model = EmbeddingModel()
sentences = ["Hello, World!", "I am Kyaru!", "I am a virtual YouTuber!"]
embeddings = embedding_model.encode(sentences)
print("Dimension: ", len(embeddings[0])) # 384 個向量
print("embeddings: ", embeddings)
```
得到下面結果,三個長度為384的 float list
```
Dimension: 384
embeddings: [[-0.03817715 0.03291114 -0.0054594 ... -0.04089032 0.03187141
0.0181632 ]
[-0.03684668 -0.03121175 0.11125965 ... 0.00893893 -0.08519208
0.01065892]
[-0.03827702 -0.10684672 -0.0039953 ... 0.05886666 -0.01260292
-0.01910227]]
```
### 向量資料庫
向量資料庫可以把Embedding後的vector當作key,並在裡面存放資料。並且可以藉由向量的相似程度來搜尋出相似的資料,主要用在推薦系統,大型語言模型的RAG時使用等。
#### Qdrant
向量資料庫有很多種,下面介紹[Qdrant](https://qdrant.tech/), Qdrant是開源的Rust語言寫成的向量資料庫,並且可以在本地端用docker架設
##### docker compose
docker-compose.yml 如下設定
```yml=
services:
qdrant:
image: qdrant/qdrant:v1.6.1
restart: always
container_name: qdrant
ports:
- "6333:6333"
volumes:
- ./qdrant/storage:/qurant/storage
- ./qdrant/config.yaml:/qurant/config/production.yaml
```
上面比較重要的是 volumn的設定。
- `/qurant/storage`:是設定向量資料庫真實的檔案是要存在你的電腦裡面的哪裡,我就是存在專案資料夾下面的 `./qdrant/storage` 資料夾
- `/qurant/config/production.yaml`:是設定Qdrand config檔在你的電腦上的真實位置,像是我的就是在專案資料夾下面的 `./qdrant/config.yaml`。
- config.yaml下載:[點我](https://github.com/qdrant/qdrant/blob/master/config/config.yaml)
另外在config.yaml找到 下面這個部份可以設定密碼
```yaml
service:
api_key: your_secret_api_key_here
```
接著在comand line中輸入下面指令就可以啟用了
```
docker-compose up -d
```
##### Qdrand Python SDK
Qdrand有提供Python 的SDK,可使用pip下載
```
poetry add qdrant-client openai
```
以下的是完整的程式碼,解說在後面
```python=
from app.embedding.model import EmbeddingModel
from qdrant_client import QdrantClient
from qdrant_client.http import models
from qdrant_client.http.models import PointStruct
class QdrantSingleton:
_instance = None
_initialized = False
def __new__(cls, *args, **kwargs):
if not cls._instance:
cls._instance = super(QdrantSingleton, cls).__new__(cls)
return cls._instance
def __init__(self):
if not QdrantSingleton._initialized:
self.qdrant_client = QdrantClient(
url="http://localhost",
port=6333,
# api_key="api_key",
)
self.embedding_model = EmbeddingModel()
QdrantSingleton._initialized = True
def recreate_collection(self, collection_name: str):
"""
這個方法會重新建立collection,並設定collection的參數
https://ithelp.ithome.com.tw/articles/10335513
"""
# https://python-client.qdrant.tech/qdrant_client.http.models.models#qdrant_client.http.models.models.VectorParams
# 一個用cosine算距離的向量,長度是384
vectors_config = models.VectorParams(
distance=models.Distance.COSINE,
size=384,
)
# m代表每個節點近鄰數量。m值越大,查詢速度越快,但內存和構建時間也會增加。
# ef_construct這是用於構建圖時的效率參數。較大的ef_construct值會導致更好的查詢品質,但會增加構建時間。代表在構建索引時搜索的節點數量
hnsw_config = models.HnswConfigDiff(on_disk=True, m=16, ef_construct=100)
# memmap_threshold是這表示當數據大小超過20000時,將使用內存映射來管理數據,這可以有效地處理大量數據並減少內存使用。
optimizers_config = models.OptimizersConfigDiff(memmap_threshold=20000)
self.qdrant_client.recreate_collection(
collection_name=collection_name,
vectors_config=vectors_config,
hnsw_config=hnsw_config,
optimizers_config=optimizers_config,
)
return self.qdrant_client
def create_collection(self, collection_name: str):
"""
create collection
"""
vectors_config = models.VectorParams(
distance=models.Distance.COSINE,
size=384,
)
hnsw_config = models.HnswConfigDiff(on_disk=True, m=16, ef_construct=100)
optimizers_config = models.OptimizersConfigDiff(memmap_threshold=20000)
if not self.qdrant_client.collection_exists(collection_name):
self.qdrant_client.create_collection(
collection_name=collection_name,
vectors_config=vectors_config,
hnsw_config=hnsw_config,
optimizers_config=optimizers_config,
)
return self.qdrant_client
def get_embedding(self, text: str) -> list[float]:
"""
get embedding
"""
embedding_list = self.embedding_model.encode([text])
embedding = embedding_list[0]
embedding_to_float_list = embedding.tolist()
return embedding_to_float_list
def upsert_vectors(
self, vectors: list[list[float]], collection_name: str, data: list
):
"""
upsert vectors
payload is metadata, can be any data in dict
"""
for i, vector in enumerate(vectors):
self.qdrant_client.upsert(
collection_name=collection_name,
points=[
PointStruct(
id=i,
vector=vector,
payload=data[i],
)
],
)
print("upsert_vectors done")
def search_for_qdrant(self, text: str, collection_name: str, limit_k: int):
"""
search for qdrant
"""
embedding_vector = self.get_embedding(text)
search_result = self.qdrant_client.search(
collection_name=collection_name,
query_vector=embedding_vector,
limit=limit_k,
append_payload=True,
)
return search_result
```
這邊看起來叫複雜,但其實這個是python的Singleton的寫法,在呼叫這個class的時候都只會回傳同一個init。最重要的是`QdrantClient`,這裡要放和Qdrant的連線資訊,另外EmbeddingModel則是上面的Class引入。
```python=
class QdrantSingleton:
_instance = None
_initialized = False
def __new__(cls, *args, **kwargs):
if not cls._instance:
cls._instance = super(QdrantSingleton, cls).__new__(cls)
return cls._instance
def __init__(self):
if not QdrantSingleton._initialized:
self.qdrant_client = QdrantClient(
url="http://localhost",
port=6333,
# api_key="api_key",
)
self.embedding_model = EmbeddingModel()
QdrantSingleton._initialized = True
```
接著我們要在向量資料庫中建立一個collection,collection就像是一般資料庫的table,用來存放我們的資料,由於是練習,我選擇使用`recreate_collection`,這樣如果呼叫撞名的collection就可以先刪除再重新創一個,如果不想一直刪除也可以選擇上面的`create_collection`
在這裡我們可以用`VectorParams`設定向量之間的距離怎麼算,我選擇COSINE,也可以用`models.Distance.EUCLID`選擇兩點直線距離。
並且`VectorParams`還可以設定當作key的vector的長度有多長,因為我使用Hugging face的`all-MiniLM-L6-v2`固定會產生 384 維度的vector,所以寫384
```python=
def recreate_collection(self, collection_name: str):
"""
這個方法會重新建立collection,並設定collection的參數
https://ithelp.ithome.com.tw/articles/10335513
"""
# https://python-client.qdrant.tech/qdrant_client.http.models.models#qdrant_client.http.models.models.VectorParams
# 一個用cosine算距離的向量,長度是384
vectors_config = models.VectorParams(
distance=models.Distance.COSINE,
size=384,
)
# m代表每個節點近鄰數量。m值越大,查詢速度越快,但內存和構建時間也會增加。
# ef_construct這是用於構建圖時的效率參數。較大的ef_construct值會導致更好的查詢品質,但會增加構建時間。代表在構建索引時搜索的節點數量
hnsw_config = models.HnswConfigDiff(on_disk=True, m=16, ef_construct=100)
# memmap_threshold是這表示當數據大小超過20000時,將使用內存映射來管理數據,這可以有效地處理大量數據並減少內存使用。
optimizers_config = models.OptimizersConfigDiff(memmap_threshold=20000)
self.qdrant_client.recreate_collection(
collection_name=collection_name,
vectors_config=vectors_config,
hnsw_config=hnsw_config,
optimizers_config=optimizers_config,
)
return self.qdrant_client
```
呼叫之後進入[localhost:6333/dashboard](http://localhost:6333/dashboard)應該可以看到下面的面
![image](https://hackmd.io/_uploads/ByymCXEIA.png)
以下則是利用Hugging face的`all-MiniLM-L6-v2`將文字產出vector,我設計成一次用一個句子embedding,但要注意回傳的embedding會是 tensor, shape是(1, 384),所以要拿出第0個之後呼叫to_list,轉成float array後才能放入Qdrant
```python=
def get_embedding(self, text: str) -> list[float]:
"""
get embedding
"""
embedding_list = self.embedding_model.encode([text])
embedding = embedding_list[0]
embedding_to_float_list = embedding.tolist()
return embedding_to_float_list
```
接著可以用upsert(其實insert也可以)的方法,把vector當作key, data當作value(在Qdrant裡面叫做payload),data可以是任何型態的資料組成的array,像是python 的dictionary, 會存成json的形式
```python=
def upsert_vectors(
self, vectors: list[list[float]], collection_name: str, data: list
):
"""
upsert vectors
payload is metadata, can be any data in dict
"""
for i, vector in enumerate(vectors):
self.qdrant_client.upsert(
collection_name=collection_name,
points=[
PointStruct(
id=i,
vector=vector,
payload=data[i],
)
],
)
print("upsert_vectors done")
```
最後是查詢,只要提供一段文字,會先進行embed之後,用這個vector去資料庫查詢,並查出最接近的`limit_k`的資料,然後把裡面的payload拿出來。
```python=
def search_for_qdrant(self, text: str, collection_name: str, limit_k: int):
"""
search for qdrant
"""
embedding_vector = self.get_embedding(text)
search_result = self.qdrant_client.search(
collection_name=collection_name,
query_vector=embedding_vector,
limit=limit_k,
append_payload=True,
)
return search_result
```
##### Qdrant實戰演練
以下是我有`American Idiots`前四句的歌詞,我們把他一個一個embed好,再用embed的vector當作key, 把歌詞的資料當作payload存在向量資料庫
```python=
def main():
# qdrant
american_idiots = [
{"id": "1", "lyric": "Don't wanna be an American idiot"},
{"id": "2", "lyric": "Don't want a nation under the new media"},
{"id": "3", "lyric": "And can you hear the sound of hysteria?"},
{"id": "4", "lyric": "The subliminal * America"},
]
qdrant = QdrantSingleton()
collection_name = "Lyrics"
qdrant.recreate_collection(collection_name)
embedding_array = [qdrant.get_embedding(text["lyric"]) for text in american_idiots]
qdrant.upsert_vectors(embedding_array, collection_name, american_idiots)
```
在 [localhost:6333/dashboard](http://localhost:6333/dashboard)中可以看到已經存進去了。
![image](https://hackmd.io/_uploads/B1oYkr4IC.png)
接著我們用一句話進去搜查,並且設定我們只想要找到最相近的一個值
```python=
query_text = "stupid american"
search_result = qdrant.search_for_qdrant(query_text, collection_name, limit_k=1)
print(f"尋找: {query_text}", search_result)
```
output可以看到最接近的歌詞是`"Don't wanna be an American idiot"`
```
尋找: stupid american [ScoredPoint(id=0, version=0, score=0.5378643, payload={'id': '1', 'lyric': "Don't wanna be an American idiot"}, vector=None, shard_key=None)]
```