# [GLIP](https://github.com/microsoft/GLIP/tree/main) 程式碼 ## Detection Model ![image](https://hackmd.io/_uploads/HJ7tI7LRgx.png) 1. language_dict_features的 mask -> one-hot 首先建立一個 和原本 mask 張量形狀一樣 的張量(tensor),但裡面的值全部都是 0。同時確保它被放在同一個 GPU/CPU 裝置上(用 .device)。 ```python new_masks = torch.zeros_like(language_dict_features['masks'], device=language_dict_features['masks'].device) ``` 將前面幾個維度(對應到類別數)全部設為 1。 ```python new_masks[:, :self.cfg.MODEL.DYHEAD.NUM_CLASSES] = 1 ``` 最後,將這個新建的 new_masks 回存回原本的 language_dict_features 裡面。 ```python language_dict_features['masks'] = new_masks ``` ## Region Proposal Network (RPN) ## VLDyHead 1. `torch.nn.functional.normalize` : 會把輸入張量的每個向量除以它的範數(通常是 L2 norm),讓每個向量的長度變成 1。寫成數學就是 $$y=\cfrac{x}{\vert \vert x\vert \vert _p}$$ 2. Contrastive Align Projection 有 對比學習(contrastive learning) 和 跨模態對齊(alignment) 的概念 ```python self.contrastive_align_projection_text = nn.Linear(channels, contrastive_hdim, bias=True) proj_tokens = F.normalize( self.contrastive_align_projection_text(embedding), p=2, dim=-1 ) ``` - 文字 embedding (embedding) 通過一個線性層 (nn.Linear) 映射到新的向量空間,這層叫做 projection。 - F.normalize 把向量歸一化到單位長度,使得它只保留方向資訊(方便做餘弦相似度)。 - 結果 (proj_tokens) 繼續與圖像特徵的 projection 進行 contrastive loss 訓練 為什麼要「projection + alignment + contrastive」?這是多模態模型(例如 CLIP, MDETR, ALBEF)的共同做法。 核心目標是:把圖片和文字嵌入映射到同一語義空間,讓語意相近的對應樣本在這個空間裡更接近。 - projection_text:把文字 embedding 投影到對比空間 - projection_image:把圖片 embedding 投影到同一空間 然後算相似度矩陣 ```python logits = proj_image @ proj_text.T # 內積或 cosine similarity ``` 計算損失 ```python loss = contrastive_loss(logits) ``` 這樣模型會學會 - 圖像 A ↔ 文字 A 相似度高 - 圖像 A ↔ 文字 B 相似度低 3. 矩陣乘法 (matrix multiplication) ```python torch.matmul(embedding, self.bias_lang) ``` 如果 A 是形狀 (batch, d1),B 是形狀 (d1, d2),輸出 (batch, d2)。 $$ Y = A \times B$$ | 函數| 全名| 維度要求| 適用場景| broadcasting | |-|-|-|-|-| |`torch.matmul` / `@`| **matrix multiply**| 任意維度 ≥ 1 | 通用矩陣乘法(自動擴展 batch)|✅ | | `torch.bmm`| **batch matrix multiply** | 僅支援 **3D 張量** (B, N, M) × (B, M, P) | 一批矩陣乘法(批次版 matmul)| ❌| 4. permute and flatten ```python def permute_and_flatten(layer, N, A, C, H, W): layer = layer.view(N, -1, C, H, W) layer = layer.permute(0, 3, 4, 1, 2) layer = layer.reshape(N, -1, C) return layer ``` 整體過程 ```python 原始輸入 layer: [N, A*C, H, W] ↓ reshape [N, A, C, H, W] ↓ permute [N, H, W, A, C] ↓ flatten [N, H*W*A, C] ``` - 第一步 : 將 [N, A*C, H, W] → [N, A, C, H, W] 原本每個位置的通道數是 A*C,這裡把它拆成: - A: 每個位置有 A 個 anchor - C: 每個 anchor 有 C 個輸出(例如類別或 box offsets) - 第二步 : 調整維度順序 `layer = layer.permute(0, 3, 4, 1, 2)` 會重新排列維度 ```bash ( N, A, C, H, W ) → ( N, H, W, A, C ) ``` - 第三步 : 攤平成 anchor 列表 `layer = layer.reshape(N, -1, C)` 把 (H, W, A) 攤平成一個維度,也就是「所有空間位置的所有 anchor」 5. `bias = dot_product_proj_tokens_bias.unsqueeze(1).repeat(1, A, 1)` 是在 改變 bias 的張量形狀 (shape),讓它能在計算中與其他張量(例如 anchor 或 feature map)對齊進行 廣播 (broadcast)。 - `repeat(1, A, 1)` : 會沿著每個維度「重複」張量 ```python bias = bias.repeat(1, A, 1) ``` 第 0 維(batch)重複 1 次 → 不變 第 1 維(anchor 數 A)重複 A 次 第 2 維(通道 C)不變 ```python dot_product_proj_tokens_bias.shape = [2, 256] ``` 經過 `repeat()` ```python bias.shape after repeat = [2, 9, 256] ``` ## [VLFuse](https://github.com/microsoft/GLIP/blob/main/maskrcnn_benchmark/modeling/rpn/vldyhead.py#L350) ![image](https://hackmd.io/_uploads/BylhapSCeg.png) ## [BiMultiHeadAttention](https://github.com/microsoft/GLIP/blob/main/maskrcnn_benchmark/utils/fuse_helper.py#L171) ![image](https://hackmd.io/_uploads/rJrCp6HCxe.png) 1. 維度轉換 ```pyhton q = feat.flatten(2).transpose(1, 2) # 從 第二維 把所有維度展開 # 把維度 1 和 2 交換 # (bs, C, H, W) -> (bs, C, H*W) -> (bs, H*W, C) ``` 2. Transformer -> Multi-Head ```python def _shape(self, tensor: torch.Tensor, seq_len: int, bsz: int): return tensor.view(bsz, seq_len, self.num_heads, self.head_dim).transpose(1, 2).contiguous() ``` | 步驟 | 操作| 輸入形狀 | 輸出形狀| 目的| |-|-|-|-| -| |1|`view(bsz, seq_len, num_heads, head_dim)`| `(bsz, seq_len, embed_dim)`|`(bsz, seq_len, num_heads, head_dim)`|拆成多頭| |2|`transpose(1, 2)`|`(bsz, seq_len, num_heads, head_dim)`|`(bsz, num_heads, seq_len, head_dim)`| 交換維度方便注意力計算 | |3|`contiguous()`|非連續張量|連續張量|確保可安全運算| `contiguous()` : 確保 tensor 在記憶體中的儲存是連續的(contiguous memory layout)。這樣才不會有 `RuntimeError: view size is not compatible with input tensor's size and stride` 之類的錯誤 3. Query 和 Key 的內積 ```python attn_weights = torch.bmm(query_states, key_states.transpose(1, 2)) ``` 也就是在計算 attention 中的 $QK^T$ $$Attention(Q,K,V)=softmax(\frac{QK^T}{\sqrt{d_k}})V$$ `torch.bmm(A, B)` : 對一批 (batch) 的矩陣做乘法運算。舉例: ```bash A 的 shape 是 (batch, n, m) B 的 shape 是 (batch, m, p) 則輸出 shape 是 (batch, n, p) ``` 4. Stabilization Trick: 把所有值減掉最大值 ```python attn_weights = attn_weights - attn_weights.max() ``` 如果直接去計算 $softmax(\frac{QK^T}{\sqrt{d_k}})$ $$Softmax(x)_i = \cfrac{e^{x_i}}{\sum_j{e^{x_j}}}$$ 中的 $e^{x_j}$ 可能會很大到接近 $\infty$ 然後爆掉,可能又會因為 $e^{x_j}$ 太小而都是 0 所以要用 數值穩定化 (Stabilization Trick) 的技巧 因為 softmax 只取決於相對大小,而不是絕對值,那麼對所有元素減去同一個常數(例如最大值): $$Softmax(x)_i=Softmax(x-c)_i$$ 結果不會變,但這樣可以避免 $e^{x_i}$ 的數值爆炸 以下舉例說明 ```python attn_weights = [1000, 1001, 999] ``` 全部減去最大值 ```python attn_weights = [-1, 0, -2] ``` 再去算 softmax 就會很安全 5. 防止 underflow ```python torch.clamp(x, min=a) ``` 把 x 中所有小於 a 的值都設成 a。 6. 對每個 query token 的注意力分數列 (row) 分別做穩定化 ```python attn_weights_l = (attn_weights_T - torch.max(attn_weights_T, dim=-1, keepdim=True)[0]) ``` - `torch.max(..., dim=-1, keepdim=True)` 在最後一個維度(dim=-1)上找出每一行的最大值, `keepdim` 是看要不要保留被壓縮維度 以下舉例: 維度是 (2, 3) ```python attn_weights_T = [[100, 102, 101], [ 2, 3, 1]] ``` 今天 `torch.max(attn_weights_T, dim=-1, keepdim=True)[0]` 就是在最後一維度(第 2 個維度) 中找出每組元素中最大值 ```python [[102], [ 3]] ``` 在減掉 ```pyhotn attn_weights_l = attn_weights_T - [[102], [3]] ``` 最後變 ```python [[-2, 0, -1], [-1, 0, -2]] ``` 一樣可以防止 overflow 7. Stabilization Trick: 在 Softmax 前,先將 array 中的值做 softmax ```python attn_weights_l = attn_weights_l.softmax(dim=-1) ``` 8. 把結果拼回來成原本的維度 ```python attn_output_v = attn_output_v.view(bsz, self.num_heads, tgt_len, self.head_dim) attn_output_v = attn_output_v.transpose(1, 2) attn_output_v = attn_output_v.reshape(bsz, tgt_len, self.embed_dim) ``` 為什麼不用 `view()` - `view()` 要求 tensor 在記憶體中是連續的 (contiguous()),否則會報錯; - `reshape()` 會自動幫你處理非連續的情況(會自動 copy 一份新的 tensor),所以比較安全。