iT邦幫忙

2024 iThome 鐵人賽

DAY 15
1
AI/ ML & Data

一個Kaggle金牌解法是如何誕生的?跟隨Kaggle NLP競賽高手的討論,探索解題脈絡系列 第 15

[Day 15]🧟成為特級LLM咒言師的第四天 - 為什麼"lucrarea"咒語會這麼強大?一些實驗設計與思考 - 淺談文本對抗攻擊(Adversarial Attack)實作篇

  • 分享至 

  • xImage
  •  

昨天提到第一名利用 T5-base 模型的弱點,在提交的 prompt 後面瘋狂重複"lucrarea"這個神秘咒語,就能有效提高自身和正確答案的相似度。

像是這樣:

"Convert this to a shanty. 'it 's ' something Think A Human Plucrarealucrarealucrarealucrarealucrarealucrarealucrarealucrarea"

我們也在末尾做了一個小實驗驗證了這點!

但這到底為什麼呀🤯?

image
(圖片來源1

今天會透過深入分析 T5-Base 的 embedding space,來嘗試回答這個問題,或至少提出一些合理的猜測~

但是在進入這部分前,我們先來深入研究昨天簡單介紹過的 HotFlip 攻擊算法具體是怎麼做的。

📢 作者有話說:大家在看這篇前,建議可以先參考昨天[Day14]🧟成為特級LLM咒言師的第二天 - All you need is just "lucrarea" :淺談文本對抗攻擊(Adversarial Attack)原理篇。我有新增更多關於原理的解釋,如果昨天已經看過的朋友,建議可以再回去看看更新版呦 (ノ>ω<)ノ

💡 HotFlip Attack 介紹

昨天我們提到如何找到一個合適的 mean prompt? 可以透過建立一個訓練資料集、定義一個基於 t5-base embedding similarity 的 loss function,再使用 HotFlip Attack 演算法,就可以找到當前這個 token 應該被哪一個 token 替代優化。

for iter in range(n):
    for token_idx in range(len(trigger_tokens)):
        loss =  1 - similarity(target_embeddings, model.encode(trigger_tokens))
        loss.backward() 
        replacement_token = hotflip_attack(loss, embedding_matrix, token_idx) 
        trigger_tokens[token_idx] = replacement_token

那麼, hotflip_attack() 究竟是怎麼實現的呢?

HotFlip 攻擊的核心思想是基於模型對每個 token 的梯度,找到能夠最大化損失變化的替換 token。這可以用於增加或減少模型的損失,取決於攻擊目標。

  • 對於「非目標攻擊」(untargeted attack),攻擊者希望增加損失,讓模型分類錯誤。
  • 對於「目標攻擊」(targeted attack),攻擊者希望減少損失,使模型更傾向於預期的錯誤輸出。

以我們上面定義的 loss: 1 - similarity(target_embeddings, model.encode(trigger_tokens))來說,我們是希望減少 loss 的,所以屬於 target attack。

下面是它的原始code(reference to 2):

def hotflip_attack(averaged_grad, embedding_matrix, trigger_token_ids,
                   increase_loss=False, num_candidates=1):
    """
    The "Hotflip" attack described in Equation (2) of the paper. This code is heavily inspired by
    the nice code of Paul Michel here https://github.com/pmichel31415/translate/blob/paul/
    pytorch_translate/research/adversarial/adversaries/brute_force_adversary.py

    This function takes in the model's average_grad over a batch of examples, the model's
    token embedding matrix, and the current trigger token IDs. It returns the top token
    candidates for each position.

    If increase_loss=True, then the attack reverses the sign of the gradient and tries to increase
    the loss (decrease the model's probability of the true class). For targeted attacks, you want
    to decrease the loss of the target class (increase_loss=False).
    """
    averaged_grad = averaged_grad.cpu()
    embedding_matrix = embedding_matrix.cpu()
    trigger_token_embeds = torch.nn.functional.embedding(torch.LongTensor(trigger_token_ids),
                                                         embedding_matrix).detach().unsqueeze(0)
    averaged_grad = averaged_grad.unsqueeze(0)
    gradient_dot_embedding_matrix = torch.einsum("bij,kj->bik",
                                                 (averaged_grad, embedding_matrix))        
    if not increase_loss:
        gradient_dot_embedding_matrix *= -1    # lower versus increase the class probability.
    if num_candidates > 1: # get top k options
        _, best_k_ids = torch.topk(gradient_dot_embedding_matrix, num_candidates, dim=2)
        return best_k_ids.detach().cpu().numpy()[0]
    _, best_at_each_step = gradient_dot_embedding_matrix.max(2)
    return best_at_each_step[0].detach().cpu().numpy()

先來解釋一下它的 input:

  • averaged_grad:這是對於當前 token 的梯度(通常是對於批量數據的平均梯度),表示每個 token 如何影響模型的損失。
  • embedding_matrix:模型的詞嵌入矩陣,其中每個 token 都被表示為一個嵌入向量,這些嵌入位於一個連續的空間中。
  • trigger_token_ids:當前 token 的 ID 列表(整數索引),這些 token 是我們希望替換的目標。
    * increase_loss:如果為 True,則攻擊的目標是增加損失;如果為 False,則目標是減少損失。
  • num_candidates:我們希望返回的最佳替換 token 的數量。如果為 1,則返回每個位置的最佳替換 token。

接下來開始分析 function 裡面的邏輯:

  1. 先獲取當前 token 的嵌入向量:
trigger_token_embeds = torch.nn.functional.embedding(torch.LongTensor(trigger_token_ids),
                                                     embedding_matrix).detach().unsqueeze(0)
  1. 計算梯度與嵌入矩陣的內積:
gradient_dot_embedding_matrix = torch.einsum("bij,kj->bik",
                                             (averaged_grad, embedding_matrix))
  • 這邊使用了 torch.einsum(愛因斯坦求和約定)來計算每個 token 的梯度和嵌入矩陣之間的內積。
  • 具體來說,它計算了梯度矩陣 averaged_grad 和嵌入矩陣 embedding_matrix 之間的點積(內積),從而獲得每個 token 的嵌入向量在梯度空間中的投影值。
  • 結果 gradient_dot_embedding_matrix 是形狀為 [batch_size, num_tokens, vocab_size] 的張量,其中每個元素表示替換當前 token 為詞彙表中某個詞時對損失的影響。

這邊是整個 function 中最精華的部分。

為什麼要計算梯度和詞彙表中所有 token 的 embedding matrix 的內積呢?

假設我們有一個梯度矩陣 𝐺 它表示損失函數 𝐿 對每個 token 的嵌入向量的偏導數:
image
這告訴我們,如果我們修改嵌入向量 𝑒_𝑖 對應於當前的 token),損失會如何變化。

假設我們還有一個嵌入矩陣 𝐸,它包含了詞彙表中所有詞的嵌入向量。這個矩陣的大小是 vocab_size×embedding_dim,其中每一行都是詞彙表中一個詞的嵌入向量。

我們的目標是找到詞彙表中哪個詞的嵌入向量可以替換當前的 token,從而最大化(或最小化)損失變化,內積的結果將告訴我們,替換詞 𝑒_𝑗 能如何影響當前的 token 𝑖 的損失。內積越大,替換這個詞對損失的影響越大;反之亦然。

那我們為什麼要使用內積來計算呢?

當我們計算內積時,我們實際上是在衡量兩個向量之間的「對齊程度」或者「相似性」。在 HotFlip 的情境中,這些向量是:

梯度向量 𝐺_𝑖:表示當前 token 對模型損失的影響。梯度告訴我們,如果我們改變這個 token,損失會如何變化。
詞嵌入向量 𝑒_𝑗:詞彙表中的某個候選詞的向量。我們希望找到一個嵌入向量,當替換掉當前 token 時,會最大化或最小化損失變化。

當我們在做 back propagation 的時候,因為希望 loss 下降,所以我們會往梯度的反方向(梯度乘上負號)來更新模型參數;這邊也是一樣,我們希望沿著該個token梯度的反方向更新token,那自然是盡可能在詞彙表中,找出和梯度向量相反方向的 token embedding 來更新囉!

再舉個例子:

假設我們的模型在處理句子 "I like this product",其中每個詞的詞嵌入向量分別是 e_I, e_like, e_this, e_product。現在,假設我們想對這個句子進行攻擊,具體來說,我們希望找到一個替換詞來替換 "like",使得模型錯誤地認為這是一個消極的句子。

  • 計算梯度:首先,我們計算出 "like" 這個詞對損失的梯度 G_like,這個梯度向量告訴我們如何修改這個詞來最大化損失變化。

  • 內積計算:接下來,我們計算 "like" 的梯度 G_like 與詞彙表中其他候選詞的詞嵌入向量的內積,例如:

G_likee_love
G_likee_hate
G_likee_dislike

這裡的內積告訴我們:如果我們將 "like" 替換為 "love"、"hate" 或 "dislike",哪一個詞的替換會讓模型的損失變化最大。

如果 G_like ⋅ e_hate 的內積值最大,這意味著將 "like" 替換為 "hate" 會對損失變化產生最大的影響。
相反,如果內積值很小,則說明替換後的影響不大。

回到 hotflip_attack() 的邏輯,接下來我們要進行第三步:
3. 決定是否增加或減少損失:

if not increase_loss:
    gradient_dot_embedding_matrix *= -1    # lower versus increase the class probability.

如果 increase_loss 為 False,則我們希望減少損失。這時,通過對內積值乘以 -1,我們將優化目標轉換為最小化損失(即使得模型的預測更接近真實標籤)。
4. 選擇最佳替換詞:

if num_candidates > 1: # get top k options
    _, best_k_ids = torch.topk(gradient_dot_embedding_matrix, num_candidates, dim=2)
    return best_k_ids.detach().cpu().numpy()[0]
_, best_at_each_step = gradient_dot_embedding_matrix.max(2)
return best_at_each_step[0].detach().cpu().numpy()

回傳gradient_dot_embedding_matrix中,內積結果最大的 token index。

如果我們實際跑一遍這部分的 code,起始句子如果是:"Rewrite this text to make it more helpful.",當我們在昨天提到的 training set 執行 hotflip attack 算法多次後,可以發現改造出來的句子和訓練資料集的其他 prompt 的 t5-base embedding similarity 逐漸提升📈(reference to 3
image

🤔 為什麼"lucrarea"咒語會這麼強大? 一些實驗與思考

其實討論區很多人發現,那個真的會讓不管什麼句子,比較兩者相似度時,只要無腦加上就會提高相似度的咒語,好像不是 "lucrarea",而是 </s> 這個特殊 token。

我們來看下面兩個實驗:

  • 加上 <\/s> token 比較不同 text 和 target text: "Convert this into a sea shanty." 的相似實驗結果:
text scs
0 rewrite next text 0.771167
1 rewrite next text </s> 0.805036
2 rewrite next text </s></s> 0.818626
3 rewrite next text </s></s></s> 0.823307
4 rewrite next text </s></s></s></s> 0.82409
5 rewrite next text </s></s></s></s></s> 0.823169
6 rewrite next text </s></s></s></s></s></s> 0.821531
  • 加上 lucrarea token 比較不同 text 和 target text: "Convert this into a sea shanty." 的相似實驗結果:
text scs
0 rewrite next text 0.771167
1 rewrite next text lucrarea 0.797233
2 rewrite next text lucrarealucrarea 0.810702
3 rewrite next text lucrarealucrarealucrarea 0.817683
4 rewrite next text lucrarealucrarealucrarealucrarea 0.820616
5 rewrite next text lucrarealucrarealucrarealucrarealucrarea 0.821444
6 rewrite next text lucrarealucrarealucrarealucrarealucrarealucrarea 0.821421

你會發現,加上 </s> 最高可以提升相似度到 0.82409,和一開始的 text 相比,漲幅約為6.8%,但是加上 "lucrarea" 最高只會提升到 0.821444,漲幅約為 6.5%。

剛剛都比較用 adversarial attack 找出來的特殊 token 的效果,現在我們再隨便選一個不相干的 token 來做實驗比對看看好了。

我們從詞彙表隨便抽一個出來,剛好抽到 "please"。

  • 加上 please token 比較不同 text 和 target text: "Convert this into a sea shanty." 的相似實驗結果:
text scs
0 rewrite next text 0.771167
1 rewrite next text please 0.770623
2 rewrite next text pleaseplease 0.765593
3 rewrite next text pleasepleaseplease 0.76702
4 rewrite next text pleasepleasepleaseplease 0.767312
5 rewrite next text pleasepleasepleasepleaseplease 0.766147
6 rewrite next text pleasepleasepleasepleasepleaseplease 0.766535

Similarity 先是下降後來又上升再下降,就沒有什麼明顯的趨勢了,而且最高與最低之間只差了 0.6% 左右。

那麼現在你可能會有兩個問題?

  1. 為什麼 </s> 會有這麼好的效果?
  2. 為什麼要用 "lucrarea" 當作咒語?用</s>效果不是更好?

🤔 為什麼 </s> 會有這麼好的效果?

關於這個問題,我個人的解釋是,T5 模型(Text-To-Text Transfer Transformer)是一個基於 文本生成任務 預訓練的模型。在 T5 的設計中,所有任務,包括分類、回歸、翻譯等,都被轉換為一個 文本生成任務。模型的輸入和輸出都是文本,而 是一個 結束標志符,通常用於表示句子或文本段落的結束。在 T5 的訓練過程中, 作為結束符出現頻率很高,尤其在輸出生成任務中, 標記的意義類似於句子結束符,因此會對模型的行為產生一些影響。

由於它頻繁出現,T5 可能學會了將 作為重要的上下文標志,因此當在相似度計算中加入 時,模型傾向於認為這個標志符的存在表明兩個文本具有相似的結構或任務。

但是如果我們做下面的實驗:

  • 加上 </s> token 比較 target text: "Convert this into a sea shanty." 和自己的變體的相似實驗結果:

也就是"Convert this into a sea shanty."和自己比較相似度。

可以發現隨著插入的 </s> token 越來越多, similarity 從原本接近 1.0 逐漸下降到 0.916274,下降了將近 8%。

這又是為什麼呢?又要怎麼解釋有時候加上 </s> 相似度變高,有時候又會變低的現象呢?

text scs
0 Convert this into a sea shanty 0.997268
1 Convert this into a sea shanty </s> 0.986207
2 Convert this into a sea shanty </s></s> 0.970834
3 Convert this into a sea shanty </s></s></s> 0.955499
4 Convert this into a sea shanty </s></s></s></s> 0.941059
5 Convert this into a sea shanty </s></s></s></s></s> 0.928068
6 Convert this into a sea shanty </s></s></s></s></s></s> 0.916274

為了做比較,我們一樣拿剛剛隨便亂找的 token 加在後面對比看看:

  • 加上 please token 比較 target text: "Convert this into a sea shanty." 和自己的變體的相似實驗結果:
text scs
0 Convert this into a sea shanty 0.997268
1 Convert this into a sea shanty please 0.978942
2 Convert this into a sea shanty pleaseplease 0.974357
3 Convert this into a sea shanty pleasepleaseplease 0.974118
4 Convert this into a sea shanty pleasepleasepleaseplease 0.972775
5 Convert this into a sea shanty pleasepleasepleasepleaseplease 0.970757
6 Convert this into a sea shanty pleasepleasepleasepleasepleaseplease 0.969344

你會發現加上隨便找的 token 對 similarity 的影響明顯比特殊 token 的小很多,只有大概 2.8% 左右。

對以上四個實驗的結果,這邊我提出一個假設:<\/s> 可能位在 sentence-t5-base 的 embedding space 的中心點。
用下圖來解釋的話:
image

如果 P1 和 P2 本來距離就很近(如圖左),那把P2往球心拉(移動到 P2'),P1 和 P2' 之間的距離反而會變大,d2 > d1,也就是上面這個實驗結果的解釋;如果本來距離比較遠,那把 P2 往球心拉反而可以縮短距離,d2<d1(如圖右的部分)。

當然什麼時候距離會變短、什麼時候會變長,這之間的差距我們也可以嘗試用數學來分析:

球坐標系中的點用三個變量表示:
r(徑向距離,即點到球心的距離)、
θ(方位角)和
ϕ(極角)。兩點之間的距離公式可以透過徑向距離和角度差來計算。

假設兩個點的坐標分別為 (r1, θ1, ϕ1) 和 (r2, θ2, ϕ2),
他們的距離公式為:

d = sqrt(r1^2 + r2^2 - 2 * r1 * r2 * [sin(θ1) * sin(θ2) * cos(ϕ1 - ϕ2) + cos(θ1) * cos(θ2)])

當其中一個點往球心移動或是遠離球心,假設改變的只有 r1 其他參數不變,兩點之間的距離 d 如何變化取決於兩點的徑向距離 r1 和 r2 之間的相對大小,以及角度差對距離的影響。

假設我們要考慮的兩個點坐標為 (r1, θ1, ϕ1) 和 (r2, θ2, ϕ2),且其他角度參數 θ1, θ2, ϕ1, ϕ2 都保持不變。

兩點之間的距離公式可以簡化為:

d = sqrt(r1^2 + r2^2 - 2 * r1 * r2 * A)

其中 A = sin(θ1) * sin(θ2) * cos(ϕ1 - ϕ2) + cos(θ1) * cos(θ2) 是一個與角度相關的常數。

現在,我們考慮 r1 改變時,距離 d 如何變化。這可以通過對 d 對 r1 的導數來分析:

∂d/∂r1 = (r1 - r2 * A) / sqrt(r1^2 + r2^2 - 2 * r1 * r2 * A)

這個導數的符號決定了 d 的增減情況:

當 r1 > r2 * A 時,導數為正,這意味著當 r1 增加時,距離 d 也會增加;當 r1 減少時,距離 d 會減少。

當 r1 = r2 * A 時,導數為零,這意味著 d 在這個點處達到一個極值,改變 r1 不會立即影響距離。

當 r1 < r2 * A 時,導數為負,這意味著當 r1 增加時,距離 d 會減少;當 r1 減少時,距離 d 會增加。

🤔 為什麼要用 "lucrarea" 當作咒語?用</s>效果不是更好?

因為如果我們用 huggingface 裡面實現的 t5-base 的話,他的 tokenizer 確實會有 </s> 這個 token,做出來的實驗結果也如上面所示。

但是這個比賽,應該是 google 辦的,他後台那邊使用的是 tensorflow 版本的 t5-base,而在這個版本中,是沒有 <\/s> 這個 token 的。

<\/s> 會被 decode 成 ['<', '/', 's', '>'] 多個 token。

如果我們用 tensorflow 版本的 sentence-t5-base 去看和 </s> token 最相近的 token,會發現 "lucrarea" 和 </s> 靠得非常非常近。

甚至如果我們用t5預訓練的資料來解釋的話,由於 t5 有用到羅馬尼雅等語種的語料,在羅馬尼亞語中,「lucrarea」是「作品」、「工作」或「作業」的意思,視上下文而定。它常用於指以下幾種情況:

學術或文學作品:例如一篇論文、書籍或藝術作品。

Exempl: „Aceasta este lucrarea mea de diplomă.”(這是我的畢業論文。)

”text“ 可能大量出現在訓練 t5 的 instruction 或是任務說明等語句中,所以也變成一個和 </s> 一樣有帶有功能意義的 token 了。

"lucrarea", "text", "work" 都非常非常靠近,幾乎重疊,,也距離 </s> 很近:
https://ithelp.ithome.com.tw/upload/images/20240929/201526687KftOMYuy1.png

小結

LLM Prompt Recovery 的專題到今天就結束啦~

雖然這幾個得獎的作法除了在這個比賽的設定下使用,也很難應用到生活或工作中和 LLM 相關的問題,但在過程中看這些參賽者為了提高分數做的各種思考和嘗試,也是很有趣的事情!

接下來我們就會進入新的 Kaggle 賽題了,我們明天見~


🚨勘誤說明🚨:


謝謝讀到最後的你,希望你會覺得有趣!
如果喜歡這系列,別忘了按下訂閱,才不會錯過最新更新,也可以按讚給我鼓勵唷!
如果有任何回饋和建議,歡迎在留言區和我說✨✨


Kaggle - LLM Prompt Recovery 解法分享系列)


上一篇
[Day14]🧟成為特級LLM咒言師的第三天 - All you need is just "lucrarea" :淺談文本對抗攻擊(Adversarial Attack)原理篇
下一篇
[Day 16]輕量級模型能否在複雜科學問題上追平ChatGPT呢?- OOM了怎麼辦?淺談 LLM 分層加載技術(layer-wise loading)、Perplexity 與 RAG 策略
系列文
一個Kaggle金牌解法是如何誕生的?跟隨Kaggle NLP競賽高手的討論,探索解題脈絡30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言