CTF中的优化随机算法(爬山&退火)
对于连续的优化问题,典型的方法是梯度下降法;对于离散的优化问题,典型的方法是爬山法/退火法。
让我们从一道简单的题目入手吧。
任务:生成能够欺骗文本分类模型的对抗性文本。
你需要对给定的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")

浙公网安备 33010602011771号