對於連續的最佳化問題,典型的方法是梯度下降法;對於離散的最佳化問題,典型的方法是爬山法/退火法。
讓我們從一道簡單的題目入手吧。
任務:生成能夠欺騙文字分類模型的對抗性文字。
你需要對給定的positive、negative、neutral文字資料進行微小擾動,以確保模型對這些擾動後的文字進行錯誤預測。你必須保持原文字與擾動文字之間的語義相似度至少為75%。如果你成功生成了至少90%符合相似度要求且能欺騙模型的對抗性樣本,你將完成任務並獲得比賽的flag。
翻譯成人話大概是:模型是一個判別器,我們要做的是對輸入的文字微小擾動,使得模型對文字的判別錯誤。乍一看是對抗學習的內容,但實際上不需要。
問題本質上是構思一個演算法,去選擇特定的某些詞去替換文字,使得儘可能地偏離原本的類別(同義詞替換)。
怎樣替換呢?
Take 1 純隨機
十分樸素地,我們先嚐試隨機替換一句話的一個詞。
在這裡我們用到Wordnet的近義詞表,其中可以查到某個詞的多個近義詞。
純隨機演算法的步驟如下:
- 從句子
j
中隨機選擇m
個詞。 - 對於選擇的每個詞(
k
從 1 到m
),執行以下步驟:- a. 將詞
k
替換為它的一個近義詞。 - b. 使用替換後的句子輸入模型,獲取模型的分類結果。
- c. 判斷模型的分類結果:
- 如果模型的分類結果與原來的分類結果相同,繼續處理下一個詞(
continue
)。 - 如果模型的分類結果發生變化,則標記攻擊成功(
break
),將該句子新增到list
,從原句表中移除該句子。
- 如果模型的分類結果與原來的分類結果相同,繼續處理下一個詞(
- a. 將詞
基於這個流程,我們可以稍微包裝一下,變成:
- 建立一個空的列表
list
用於儲存成功的攻擊樣本。 - 進行
t
次迭代(i
從 1 到t
):- 2.1 對每個輸入的句子(
j
從 1 到n
)執行以下操作:- 2.1.1 從句子
j
中隨機選擇m
個詞。 - 2.1.2 對於選擇的每個詞(
k
從 1 到m
),執行以下步驟:- a. 找到詞
k
的不超過p
個近義詞。 - b. 對於每個近義詞(
l
從 1 到p
),執行以下操作:- b.1 獲取將詞
k
替換為近義詞l
後,模型的分類結果。 - b.2 判斷模型的分類結果:
- 如果模型的分類結果與原來的分類結果相同,繼續處理下一個近義詞(
continue
)。 - 如果模型的分類結果發生變化,則標記攻擊成功(
break
),將該句子新增到list
,從原句表中移除該句子。
- 如果模型的分類結果與原來的分類結果相同,繼續處理下一個近義詞(
- b.1 獲取將詞
- a. 找到詞
- 2.1.3 對於沒有攻擊成功的句子
j
,等待下一次迭代重新隨機選擇詞。
- 2.1.1 從句子
- 2.1 對每個輸入的句子(
- 重複步驟 2,直到達到
t
次迭代或表為空為止。
其實到了這裡,已經拿到flag了。
random.py (90% 63s)
import pandas as pd
import torch
from transformers import AutoTokenizer, AutoModelForSequenceClassification
from sklearn.metrics.pairwise import cosine_similarity
import nltk
from nltk.corpus import wordnet
import random
import time
nltk.download('wordnet')
NUM_ITERATIONS = 5
SIMILARITY_THRESHOLD = 0.75
MAX_WORDS_TO_SELECT = 10
MAX_SYNONYMS_TO_SELECT = 10
csv_path = 'original_text.csv'
data = pd.read_csv(csv_path)
model_path = 'Sentiment_classification_model'
tokenizer = AutoTokenizer.from_pretrained(model_path)
model = AutoModelForSequenceClassification.from_pretrained(model_path)
def synonym_replacement(text, model, tokenizer):
words = text.split()
original_encoding = tokenizer(text, return_tensors='pt', padding=True, truncation=True, max_length=512)
original_prediction = model(**original_encoding).logits.argmax(dim=1).item()
for _ in range(MAX_WORDS_TO_SELECT):
i = random.randint(0, len(words) - 1)
synonyms = wordnet.synsets(words[i])
if not synonyms:
continue
lemmas = set(lemma.name() for syn in synonyms for lemma in syn.lemmas() if lemma.name() != words[i])
selected_synonyms = random.sample(list(lemmas), min(MAX_SYNONYMS_TO_SELECT, len(lemmas)))
for synonym in selected_synonyms:
new_words = words.copy()
new_words[i] = synonym
new_text = ' '.join(new_words)
new_encoding = tokenizer(new_text, return_tensors='pt', padding=True, truncation=True, max_length=512)
new_prediction = model(**new_encoding).logits.argmax(dim=1).item()
if new_prediction != original_prediction:
return new_text
return text
def verify_similarity(original, modified, model, tokenizer):
model.eval()
original_encoding = tokenizer(original, return_tensors='pt', padding=True, truncation=True, max_length=512)
modified_encoding = tokenizer(modified, return_tensors='pt', padding=True, truncation=True, max_length=512)
with torch.no_grad():
original_outputs = model.distilbert(**original_encoding)
original_hidden_state = original_outputs.last_hidden_state.mean(dim=1)
modified_outputs = model.distilbert(**modified_encoding)
modified_hidden_state = modified_outputs.last_hidden_state.mean(dim=1)
similarity = cosine_similarity(original_hidden_state.cpu().numpy(),
modified_hidden_state.cpu().numpy())[0][0]
return similarity
def check_attack_success(text, attacked_text, model, tokenizer):
original_encoding = tokenizer(text, return_tensors='pt', padding=True, truncation=True, max_length=512)
attacked_encoding = tokenizer(attacked_text, return_tensors='pt', padding=True, truncation=True, max_length=512)
with torch.no_grad():
original_prediction = model(**original_encoding).logits.argmax(dim=1).item()
attacked_prediction = model(**attacked_encoding).logits.argmax(dim=1).item()
return original_prediction != attacked_prediction
start_time = time.time()
attacked_texts = []
remaining_data = data.copy()
for iteration in range(NUM_ITERATIONS):
print(f"Starting iteration {iteration + 1}...")
successful_attacks = []
to_remove = []
for index, row in remaining_data.iterrows():
original_text = row['text']
attack_success = False
attacked_text = synonym_replacement(original_text, model, tokenizer)
similarity = verify_similarity(original_text, attacked_text, model, tokenizer)
if similarity >= SIMILARITY_THRESHOLD:
attack_success = check_attack_success(original_text, attacked_text, model, tokenizer)
if attack_success:
successful_attacks.append((row['id'], attacked_text))
to_remove.append(index)
attacked_texts.extend(successful_attacks)
remaining_data = remaining_data.drop(to_remove)
print(f"Iteration {iteration + 1}: {len(successful_attacks)} successful attacks.")
if remaining_data.empty:
print("All texts successfully attacked, ending early.")
break
success_rate = len(attacked_texts) / len(data)
print(f'Final attack success rate: {success_rate * 100:.2f}%')
for index, row in remaining_data.iterrows():
attacked_texts.append((row['id'], row['text']))
attacked_texts.sort(key=lambda x: x[0])
output_df = pd.DataFrame(attacked_texts, columns=['id', 'attacked_text'])
output_df.to_csv('attacked_text.csv', index=False)
end_time = time.time()
total_time = end_time - start_time
print(f"Total running time: {total_time:.2f} seconds")
Take 2 隨機+爬山
與其每次迭代都從原始狀態選擇詞的組合,不妨每次只選擇一個詞,繼承上一次迭代的路徑:
- 建立一個空列表
list
用於儲存成功的攻擊樣本。 - 進行
t
次迭代(i
從 1 到t
):- 2.1 對每個輸入的句子(
j
從 1 到n
)執行以下操作:- 2.1.1 從句子
j
中隨機選擇m
個詞。 - 2.1.2 對於每個選擇的詞(
k
從 1 到m
),執行以下步驟:- a. 找到詞
k
的不超過p
個近義詞。 - b. 對於每個近義詞(
l
從 1 到p
),執行以下操作:- b.1 計算將詞
k
替換為近義詞l
後,模型輸出的logits
絕對值的變化量。 - b.2 判斷模型的分類結果:
- 如果模型的分類結果發生變化且相似度檢驗有效,則標記攻擊成功(
break
),將該句子新增到list
,從原句表中移除該句子。 - 如果沒有分類變化但發現更大的變化量,則更新當前最大的變化量。
- 如果模型的分類結果發生變化且相似度檢驗有效,則標記攻擊成功(
- b.1 計算將詞
- c. 比較每個詞
k
的最大logits
變化量,選擇變化量最大的一個詞進行替換。
- a. 找到詞
- 2.1.3 對於沒有攻擊成功的句子
j
,將其替換詞更新到表中,等待下一次迭代。
- 2.1.1 從句子
- 2.1 對每個輸入的句子(
- 重複步驟 2,直到達到
t
次迭代或句表為空為止。
其中 logits
指的是模型最後一層的輸出,即每個標籤的機率分佈。
這正是“爬山”的具體體現:每次迭代向鄰域區域性最優(logits
變化量最大)的方向前進,形成一條“路徑”,直到上限。
當然,這和標準的爬山演算法有一些細微的差異,因為這個情境下的鄰域是隨機選出來的,而不是全部鄰域,這可以降低陷入區域性最優的機率。
climb.py (98% 45s)
import pandas as pd
import torch
from transformers import AutoTokenizer, AutoModelForSequenceClassification
from sklearn.metrics.pairwise import cosine_similarity
import nltk
from nltk.corpus import wordnet
import random
import time
nltk.download('wordnet')
NUM_ITERATIONS = 5
SIMILARITY_THRESHOLD = 0.75
MAX_WORDS_TO_SELECT = 10
MAX_SYNONYMS_TO_SELECT = 10
csv_path = 'original_text.csv'
data = pd.read_csv(csv_path)
model_path = 'Sentiment_classification_model'
tokenizer = AutoTokenizer.from_pretrained(model_path)
model = AutoModelForSequenceClassification.from_pretrained(model_path)
def calculate_logits_change(original_logits, new_logits):
return torch.abs(original_logits - new_logits).sum().item()
def synonym_replacement(text, model, tokenizer):
words = text.split()
original_encoding = tokenizer(text, return_tensors='pt', padding=True, truncation=True, max_length=512)
with torch.no_grad():
original_logits = model(**original_encoding).logits
original_prediction = original_logits.argmax(dim=1).item()
max_change = 0
best_text = text
selected_indices = random.sample(range(len(words)), min(MAX_WORDS_TO_SELECT, len(words)))
for i in selected_indices:
synonyms = wordnet.synsets(words[i])
if not synonyms:
continue
lemmas = set(lemma.name() for syn in synonyms for lemma in syn.lemmas() if lemma.name() != words[i])
selected_synonyms = random.sample(list(lemmas), min(MAX_SYNONYMS_TO_SELECT, len(lemmas)))
best_word_change = 0
best_word_text = text
for synonym in selected_synonyms:
new_words = words.copy()
new_words[i] = synonym
new_text = ' '.join(new_words)
new_encoding = tokenizer(new_text, return_tensors='pt', padding=True, truncation=True, max_length=512)
with torch.no_grad():
new_logits = model(**new_encoding).logits
new_prediction = new_logits.argmax(dim=1).item()
logits_change = calculate_logits_change(original_logits, new_logits)
if new_prediction != original_prediction:
similarity = verify_similarity(text, new_text, model, tokenizer)
if similarity >= SIMILARITY_THRESHOLD:
return new_text
if logits_change > best_word_change:
best_word_change = logits_change
best_word_text = new_text
if best_word_change > max_change:
max_change = best_word_change
best_text = best_word_text
return best_text
def verify_similarity(original, modified, model, tokenizer):
model.eval()
original_encoding = tokenizer(original, return_tensors='pt', padding=True, truncation=True, max_length=512)
modified_encoding = tokenizer(modified, return_tensors='pt', padding=True, truncation=True, max_length=512)
with torch.no_grad():
original_outputs = model.base_model(**original_encoding)
original_hidden_state = original_outputs.last_hidden_state.mean(dim=1)
modified_outputs = model.base_model(**modified_encoding)
modified_hidden_state = modified_outputs.last_hidden_state.mean(dim=1)
similarity = cosine_similarity(original_hidden_state.cpu().numpy(),
modified_hidden_state.cpu().numpy())[0][0]
return similarity
def check_attack_success(text, attacked_text, model, tokenizer):
original_encoding = tokenizer(text, return_tensors='pt', padding=True, truncation=True, max_length=512)
attacked_encoding = tokenizer(attacked_text, return_tensors='pt', padding=True, truncation=True, max_length=512)
with torch.no_grad():
original_prediction = model(**original_encoding).logits.argmax(dim=1).item()
attacked_prediction = model(**attacked_encoding).logits.argmax(dim=1).item()
return original_prediction != attacked_prediction
start_time = time.time()
attacked_texts = []
remaining_data = data.copy()
for iteration in range(NUM_ITERATIONS):
print(f"Starting iteration {iteration + 1}...")
successful_attacks = []
to_remove = []
updated_remaining_data = []
for index, row in remaining_data.iterrows():
original_text = row['text']
attacked_text = synonym_replacement(original_text, model, tokenizer)
if attacked_text != original_text:
similarity = verify_similarity(original_text, attacked_text, model, tokenizer)
if similarity >= SIMILARITY_THRESHOLD:
attack_success = check_attack_success(original_text, attacked_text, model, tokenizer)
if attack_success:
successful_attacks.append((row['id'], attacked_text))
to_remove.append(index)
else:
updated_remaining_data.append({'id': row['id'], 'text': attacked_text})
else:
updated_remaining_data.append({'id': row['id'], 'text': original_text})
else:
updated_remaining_data.append({'id': row['id'], 'text': original_text})
attacked_texts.extend(successful_attacks)
remaining_data = pd.DataFrame(updated_remaining_data)
print(f"Iteration {iteration + 1}: {len(successful_attacks)} successful attacks.")
if remaining_data.empty:
print("All texts successfully attacked, ending early.")
break
success_rate = len(attacked_texts) / len(data)
print(f'Final attack success rate: {success_rate * 100:.2f}%')
for index, row in remaining_data.iterrows():
attacked_texts.append((row['id'], row['text']))
attacked_texts.sort(key=lambda x: x[0])
output_df = pd.DataFrame(attacked_texts, columns=['id', 'attacked_text'])
output_df.to_csv('attacked_text.csv', index=False)
end_time = time.time()
total_time = end_time - start_time
print(f"Total running time: {total_time:.2f} seconds")
Take 3 隨機+退火
退火是對爬山的一種變形,它會以一定機率向一個鄰域中更差的方向前進,更不容易陷入區域性最優。
假設一次迴圈中,當前最佳的子狀態 \(E^*=E_1\),\(E_2\) 是下一個子狀態。若 \(\Delta E=E_2-E_1>0\) 則一定令 \(E^*=E_2\);若 \(\Delta E\leq 0\) 則根據 \(\displaystyle{P=\text e^{\frac{\Delta E}{T}}}\) 得到令 \(E^*=E_2\) 的機率。
這裡的 \(T\) 指的是溫度,通常在一次迭代後令 \(T = \alpha T\),\(\alpha\) 是降溫係數。
結合退火的演算法流程:
- 建立一個空列表
list
用於儲存成功的攻擊樣本,初始化溫度T
和降溫係數alpha
。 - 進行
t
次迭代(i
從 1 到t
):- 2.1 對每個輸入的句子(
j
從 1 到n
)執行以下操作:- 2.1.1 從句子
j
中隨機選擇m
個詞。 - 2.1.2 對於每個選擇的詞(
k
從 1 到m
),執行以下步驟:- a. 找到詞
k
的不超過p
個近義詞。 - b. 對於每個近義詞(
l
從 1 到p
),執行以下操作:- b.1 計算將詞
k
替換為近義詞l
後,模型輸出的logits
絕對值的變化量ΔE
。 - b.2 判斷模型的分類結果:
- 如果模型的分類結果發生變化且相似度檢驗有效,則標記攻擊成功(
break
),將該句子新增到list
,從原句表中移除該句子。 - 如果沒有分類變化,則根據退火公式檢查替換條件:
- 如果
ΔE > 0
,直接接受這個替換,將替換後的句子更新為當前句子。 - 如果
ΔE <= 0
,計算接受機率P = exp(ΔE / T)
,以機率P
接受替換。
- 如果
- 如果模型的分類結果發生變化且相似度檢驗有效,則標記攻擊成功(
- b.1 計算將詞
- c. 比較每個詞
k
的ΔE
值,根據退火公式選擇一個詞替換。
- a. 找到詞
- 2.1.3 對於沒有攻擊成功的句子
j
,將其替換詞更新到表中,等待下一次迭代。
- 2.1.1 從句子
- 2.2 更新溫度
T = alpha * T
。
- 2.1 對每個輸入的句子(
- 重複步驟 2,直到達到
t
次迭代或句表為空為止。
anneal.py (94% 47s)
import pandas as pd
import torch
from transformers import AutoTokenizer, AutoModelForSequenceClassification
from sklearn.metrics.pairwise import cosine_similarity
import nltk
from nltk.corpus import wordnet
import random
import math
import time
nltk.download('wordnet')
NUM_ITERATIONS = 5
SIMILARITY_THRESHOLD = 0.75
MAX_WORDS_TO_SELECT = 10
MAX_SYNONYMS_TO_SELECT = 10
INITIAL_TEMPERATURE = 1.0
ALPHA = 0.9
csv_path = 'original_text.csv'
data = pd.read_csv(csv_path)
model_path = 'Sentiment_classification_model'
tokenizer = AutoTokenizer.from_pretrained(model_path)
model = AutoModelForSequenceClassification.from_pretrained(model_path)
def calculate_logits_change(original_logits, new_logits):
return torch.abs(original_logits - new_logits).sum().item()
def synonym_replacement(text, model, tokenizer, temperature):
words = text.split()
original_encoding = tokenizer(text, return_tensors='pt', padding=True, truncation=True, max_length=512)
with torch.no_grad():
original_logits = model(**original_encoding).logits
original_prediction = original_logits.argmax(dim=1).item()
selected_indices = random.sample(range(len(words)), min(MAX_WORDS_TO_SELECT, len(words)))
for i in selected_indices:
synonyms = wordnet.synsets(words[i])
if not synonyms:
continue
lemmas = set(lemma.name() for syn in synonyms for lemma in syn.lemmas() if lemma.name() != words[i])
selected_synonyms = random.sample(list(lemmas), min(MAX_SYNONYMS_TO_SELECT, len(lemmas)))
best_word_change = 0
best_word_text = text
for synonym in selected_synonyms:
new_words = words.copy()
new_words[i] = synonym
new_text = ' '.join(new_words)
new_encoding = tokenizer(new_text, return_tensors='pt', padding=True, truncation=True, max_length=512)
with torch.no_grad():
new_logits = model(**new_encoding).logits
new_prediction = new_logits.argmax(dim=1).item()
logits_change = calculate_logits_change(original_logits, new_logits)
if new_prediction != original_prediction:
similarity = verify_similarity(text, new_text, model, tokenizer)
if similarity >= SIMILARITY_THRESHOLD:
return new_text
if logits_change > best_word_change:
best_word_change = logits_change
best_word_text = new_text
else:
acceptance_probability = math.exp(logits_change / temperature)
if random.random() < acceptance_probability:
best_word_change = logits_change
best_word_text = new_text
text = best_word_text
return text
def verify_similarity(original, modified, model, tokenizer):
model.eval()
original_encoding = tokenizer(original, return_tensors='pt', padding=True, truncation=True, max_length=512)
modified_encoding = tokenizer(modified, return_tensors='pt', padding=True, truncation=True, max_length=512)
with torch.no_grad():
original_outputs = model.base_model(**original_encoding)
original_hidden_state = original_outputs.last_hidden_state.mean(dim=1)
modified_outputs = model.base_model(**modified_encoding)
modified_hidden_state = modified_outputs.last_hidden_state.mean(dim=1)
similarity = cosine_similarity(original_hidden_state.cpu().numpy(),
modified_hidden_state.cpu().numpy())[0][0]
return similarity
def check_attack_success(text, attacked_text, model, tokenizer):
original_encoding = tokenizer(text, return_tensors='pt', padding=True, truncation=True, max_length=512)
attacked_encoding = tokenizer(attacked_text, return_tensors='pt', padding=True, truncation=True, max_length=512)
with torch.no_grad():
original_prediction = model(**original_encoding).logits.argmax(dim=1).item()
attacked_prediction = model(**attacked_encoding).logits.argmax(dim=1).item()
return original_prediction != attacked_prediction
start_time = time.time()
attacked_texts = []
remaining_data = data.copy()
temperature = INITIAL_TEMPERATURE
for iteration in range(NUM_ITERATIONS):
print(f"Starting iteration {iteration + 1}...")
successful_attacks = []
to_remove = []
updated_remaining_data = []
for index, row in remaining_data.iterrows():
original_text = row['text']
attacked_text = synonym_replacement(original_text, model, tokenizer, temperature)
if attacked_text != original_text:
similarity = verify_similarity(original_text, attacked_text, model, tokenizer)
if similarity >= SIMILARITY_THRESHOLD:
attack_success = check_attack_success(original_text, attacked_text, model, tokenizer)
if attack_success:
successful_attacks.append((row['id'], attacked_text))
to_remove.append(index)
else:
updated_remaining_data.append({'id': row['id'], 'text': attacked_text})
else:
updated_remaining_data.append({'id': row['id'], 'text': original_text})
else:
updated_remaining_data.append({'id': row['id'], 'text': original_text})
attacked_texts.extend(successful_attacks)
remaining_data = pd.DataFrame(updated_remaining_data)
print(f"Iteration {iteration + 1}: {len(successful_attacks)} successful attacks.")
if remaining_data.empty:
print("All texts successfully attacked, ending early.")
break
temperature *= ALPHA
success_rate = len(attacked_texts) / len(data)
print(f'Final attack success rate: {success_rate * 100:.2f}%')
for index, row in remaining_data.iterrows():
attacked_texts.append((row['id'], row['text']))
attacked_texts.sort(key=lambda x: x[0])
output_df = pd.DataFrame(attacked_texts, columns=['id', 'attacked_text'])
output_df.to_csv('attacked_text.csv', index=False)
end_time = time.time()
total_time = end_time - start_time
print(f"Total running time: {total_time:.2f} seconds")