對圖像做 adversarial attack 可能你已經很熟悉了,今天帶大家來看看怎麼對文本做對抗攻擊。
第一名的 solution 就使用到 adversarial attack 的技巧!
相信大家或多或少都有聽過深度學習的對抗攻擊(adversarial attack),這是一種在機器學習和深度學習模型中使用的技術,這個技術最早是在2014年由 Ian J. Goodfellow 等人提出的(請參考:Explaining and Harnessing Adversarial Examples),目的是人為地干擾或欺騙模型,使其產生錯誤的預測或分類。
用來解釋對抗攻擊最有名的一組圖,應該是下面這張(reference to 1):
兩個熊貓照片人眼看起來根本一模一樣,但是模型卻一個預測是 "panda" 另一個預測是 "gibbon",就是長臂猿。雖然長臂猿長得跟 panda 一點都不像,但是被攻擊過的圖片,卻成功誤導模型分類錯誤,而且信心值還高達 99%。
一開始做 Adversarial Attack 的相關研究都是在圖像上面做攻擊,這部分有很多科普文章在介紹,大家有興趣可以參考[2],或是李宏毅老師的課程。
❓❓那麼問題就來了:如何在離散的文本數據上進行基於梯度的攻擊❓❓
我們先來看一下,在文本上做攻擊是什麼意思呢?
舉例來說,我們訓練一個 BERTSequenceClassification
model,來區分輸入的句子是正向積極的,還是負向消極的,模型本來都分得好好的:
"I like you." -> Positive
"I hate you." -> Negative
今天如果你想搞破壞,讓這個模型的預測全部都變錯!
這時候我們如果加上一個神秘的 token 變成:
"zoning tapping fiennes I like you" -> Negative*
"zoning tapping fiennes I hate you" -> Positive*
咦?發現了嗎? 加上 zoning tapping fiennes
這幾個 token 之後,本來分類正確的,通通都分錯了!
這是怎麼做到的呢?
解決這個問題的關鍵在於嵌入空間 Embedding Space。
NLP 模型,尤其是基於深度學習的模型(如 BERT、GPT 等),會將文本的每個 token 轉換為連續空間中的向量,即嵌入空間。這樣,我們就能夠在這個連續空間中應用基於梯度的優化技術,進而找到最佳的詞替換。
我們舉一個具體的例子來說明:
所以回到一開始的問題:如何在離散的文本上做 gradient-based 的對抗攻擊呢?
其實這部分基於 NLP 模型中的embedding layer來完成的,具體來說,雖然 token 是離散的數據,但通過embedding layer,它們被映射到了連續的嵌入空間。我們可以在這個連續空間中進行梯度計算和優化,然後根據優化的結果回到離散的 token 空間。
雖然過去曾經困擾NLP界一陣子到底要怎麼對文本做對抗攻擊,但現在 adverarial attack on text 也算是滿成熟的技術了,大家可能比較常聽到的像是 TextAttack,或是 Universal Adversarial Triggers。
由於前者只能針對 "Text" 做攻擊,而後者可以針對更小的單位 "token" 做攻擊,今天我們就來介紹一下 Universal Adversarial Triggers的演算法。
Universal Adversarial Triggers 是一種對抗性攻擊技術,專門用來針對自然語言處理(NLP)模型。在文本數據中,這種方法的目的是生成一組「觸發詞」(triggers),這些詞可以在不考慮輸入內容的情況下,系統性地欺騙模型,從而誘使模型輸出特定的結果。
如果我們要數學化地解釋 Universal Adversarial Triggers(UAT)的原理,可以將其理解為一個基於梯度優化的對抗性攻擊問題,其目的是生成一組通用的觸發詞,使模型在不同的輸入下都會生成目標輸出。
假設一個 NLP 模型 𝑓(𝑥)是一個從輸入文本(𝑥)映射到輸出空間的函數。我們的目標是找到一組通用的詞(𝑡),即 adversarial triggers,使得無論輸入 𝑥 是什麼,當附加這些觸發詞時,模型的輸出都會接近某個特定目標,或是最大化/最小化某個損失函數。
數學上,我們的目標可以表述為一個優化問題:
x 是原始的輸入文本向量(例如一句話的 token 序列)。
𝑡 是我們想要學習的 adversarial trigger,即一組觸發詞。
𝑓(𝑥+𝑡) 是模型輸出。
𝑦 是目標輸出或我們希望模型偏離的正確標籤。
𝐿(⋅) 是損失函數,用來衡量輸入 𝑥+𝑡 對應的輸出 𝑓(𝑥+𝑡) 與目標 𝑦之間的差距。
常見的損失函數有 交叉熵損失 或是 句子相似度損失。
目標:我們想要找到一組觸發詞 𝑡 使得模型的預測 𝑓(𝑥+𝑡) 偏離或逼近目標𝑦。
這個優化過程依賴於模型對輸入文本的梯度。我們通過計算損失函數對每個 token 的梯度,來決定如何修改句子中的 token。具體過程如下:
初始化一個提示詞 𝑡(可以是隨機的詞語序列,或者是簡單的詞語,如 "Rewrite this text")。
給定輸入 𝑥 , 我們將其與當前的提示詞 𝑡 拼接,並將其輸入模型。這時可以計算輸出與目標之間的損失:
如果是相似度問題,我們可以使用句子嵌入的相似度作為損失,如餘弦相似度:
使用反向傳播計算損失函數相對於每個 token 的梯度:
這表示每個 token 如何影響損失值,告訴我們如何修改該 token 以最小化損失。
HotFlip 攻擊 是一種基於梯度的技術,用來選擇能最有效替換當前 token 的新 token。具體來說,對於每個 token 𝑡_𝑖,我們根據梯度選擇能最大化損失變化的替換 token。
用數學表示,HotFlip 攻擊的目的是找到新的 token 𝑡_𝑖′,使得損失變化最大:
其中 𝜖 是學習率或步長。通過這種方式,我們可以逐個 token 地進行替換,找到最佳的替換詞。
下一篇我們會詳細介紹並實作HotFlip攻擊!可以參考:[Day 15]🧟成為特級LLM咒言師的第四天 - 為什麼"lucrarea"咒語會這麼強大?一些實驗設計與思考 - 淺談文本對抗攻擊(Adversarial Attack)實作篇
將找到的最佳 token 替換當前的 token,然後重複上述步驟直到損失最小化或達到預設的迭代次數。
由於主辦方沒有公佈訓練資料集,第六名的 team 他們首先先做了一個大約有 8000 多筆 (origin_text, rewrite_prompt, rewrite_text) 的 dataset。
他們製造 dataset 的流程如下:
為了生成重寫提示詞(rewrite prompt)和重寫後的文本(rewritten text),他們使用了以下算法。為了使數據集更加多樣化,他們使用了Gemini、GPT-3.5和GPT-4模型來生成數據。
通過這種方法,他們總共生成了8000多行數據(每行包含原始文本、重寫提示詞和重寫後的文本)。其中大約2000行數據被用於訓練我們選擇的最佳模型。
我們直接從 code 去解釋:
train_dataset = [dataset of rewrite prompts]
model = sentence_t5
target_embeddings = model.encode(train_dataset)
initial_string = "Rewrite this text to make it more helpful."
trigger_tokens = tokenizer.encode(initial_string)
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
具體來解釋一下:
train_dataset = [dataset of rewrite prompts]
model = sentence_t5
target_embeddings = model.encode(train_dataset)
這段代碼首先加載一個他們自己做好的訓練數據集 train_dataset,該數據集包含了rewrite prompt。
接著,使用預訓練的 sentence_t5 模型對這些訓練數據進行嵌入,並將這些嵌入保存為 target_embeddings,作為相似度的目標。
initial_string = "Rewrite this text to make it more helpful."
trigger_tokens = tokenizer.encode(initial_string)
這裡選擇了一個初始提示詞,"Rewrite this text to make it more helpful.",並將其通過 tokenizer 編碼為一組 token,保存為 trigger_tokens。這些 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, token_idx)
trigger_tokens[token_idx] = replacement_token
這是主要的迭代過程,逐步進行對提示詞的 token 替換。具體步驟如下:
(for iter in range(n))
:這是外部的迭代循環,總共執行 n 次,每次都會遍歷整個句子的 token。(for token_idx in range(len(trigger_tokens)))
:對句子中的每個 token 進行遍歷,逐個 token 地進行修改。loss = 1 - similarity(target_embeddings, model.encode(trigger_tokens))
loss.backward()
但這邊的梯度有別於我們平常訓練模型,是計算模型權重的梯度,這邊計算的是輸入文字的 token 梯度,這樣我們就能知道如何修改這些 token 來最大化相似度。
5. 使用 HotFlip 攻擊替換 token
replacement_token = hotflip_attack(loss, token_idx)
HotFlip 攻擊,這是一種基於梯度的對抗性攻擊技術,它會尋找在當前 token 位置可以替換的最優 token,來最小化損失(即最大化相似度)。具體怎麼實現這個對抗攻擊演算法,可以參考下一篇:[Day 15]🧟成為特級LLM咒言師的第四天 - 為什麼"lucrarea"咒語會這麼強大?一些實驗設計與思考 - 淺談文本對抗攻擊(Adversarial Attack)實作篇
trigger_tokens[token_idx] = replacement_token
這邊其實他們就是實現了 Universal Adversarial Triggers 的流程,最後找到的 mean prompt 為:
, but alter THAT story info Create formally tone homind), send poetic presentation etc Res biggerlucrarealucrarea repaslucrareaDistinguished text functi Simplelucrareaably basically you would thrulucrareaphrase emulate sentiment.
直接每一題都輸入這樣用對抗攻擊找到的、類似亂碼的 mean prompt,這樣就完成了!
接下來,來到第一名的部分。
第一名更簡單了,他也是找到一個萬能咒語:
" 'it 's ' something Think A Human Plucrarealucrarealucrarealucrarealucrarealucrarealucrarealucrarea"
他們和剛剛的第六名一樣,發現只要在輸入文字後面不斷重複:lucrarea
就可以大幅提升 performance。
所以假設他們用模型預測這一題的 rewrite prompt 應該是:"Convert this to a shanty."
只要在後面加上咒語變成:"Convert this to a shanty. 'it 's ' something Think A Human Plucrarealucrarealucrarealucrarealucrarealucrarealucrarealucrarea""
就可以立刻提升 performance!
但是到底為什麼 "lucrarea" 會這麼有用呀?!
這邊先賣個關子,我們明天會透過一些實驗和分析提出一些可能的猜測。
最後....只能用這張圖來結尾了...
雖然有點無語,但很多參賽者最後都發現無法開發有效的 Modeling-based 的方法。
因為比賽使用一個非常不好的評估指標,也就是 t5-base 轉換出來的 embedding 相似度。
這個模型有很明顯的弱點,例如我們下面做的實驗(reference to []),比較text_list裡面所有詞和 "gt_text = "Convert this into a sea shanty."" 的 t5-base embedding similarity。
結果發現"lucrarea"這個詞只要加上去,不管你輸入的是什麼,相似度就會馬上提高,直到收斂。
gt_text = "Convert this into a sea shanty."
text_lis = [
"rewrite next text",
"rewrite next text lucrarea",
"rewrite next text lucrarealucrarea",
"rewrite next text lucrarealucrarealucrarea",
"rewrite next text lucrarealucrarealucrarealucrarea",
"rewrite next text lucrarealucrarealucrarealucrarealucrarea",
"rewrite next text lucrarealucrarealucrarealucrarealucrarealucrarea",
]
pred_emb = embed_text(text_lis)
ret = scs(gt_emb, pred_emb)
ret
謝謝讀到最後的你,希望你會覺得有趣!
如果喜歡這系列,別忘了按下訂閱,才不會錯過最新更新,也可以按讚給我鼓勵唷!
如果有任何回饋和建議,歡迎在留言區和我說✨✨
(Kaggle - LLM Prompt Recovery 解法分享系列)