CTF中的优化随机算法(爬山&退火)

对于连续的优化问题,典型的方法是梯度下降法;对于离散的优化问题,典型的方法是爬山法/退火法。

让我们从一道简单的题目入手吧。

任务:生成能够欺骗文本分类模型的对抗性文本。
你需要对给定的positive、negative、neutral文本数据进行微小扰动,以确保模型对这些扰动后的文本进行错误预测。你必须保持原文本与扰动文本之间的语义相似度至少为75%。如果你成功生成了至少90%符合相似度要求且能欺骗模型的对抗性样本,你将完成任务并获得比赛的flag。

翻译成人话大概是:模型是一个判别器,我们要做的是对输入的文本微小扰动,使得模型对文本的判别错误。乍一看是对抗学习的内容,但实际上不需要。

问题本质上是构思一个算法,去选择特定的某些词去替换文本,使得尽可能地偏离原本的类别(同义词替换)。

怎样替换呢?

Take 1 纯随机

十分朴素地,我们先尝试随机替换一句话的一个词。

在这里我们用到Wordnet的近义词表,其中可以查到某个词的多个近义词。

纯随机算法的步骤如下:

  1. 从句子 j 中随机选择 m 个词。
  2. 对于选择的每个词(k 从 1 到 m),执行以下步骤:
    • a. 将词 k 替换为它的一个近义词。
    • b. 使用替换后的句子输入模型,获取模型的分类结果。
    • c. 判断模型的分类结果:
      • 如果模型的分类结果与原来的分类结果相同,继续处理下一个词(continue)。
      • 如果模型的分类结果发生变化,则标记攻击成功(break),将该句子添加到 list,从原句表中移除该句子。

基于这个流程,我们可以稍微包装一下,变成:

  1. 创建一个空的列表 list 用于存储成功的攻击样本。
  2. 进行 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,从原句表中移除该句子。
      • 2.1.3 对于没有攻击成功的句子 j,等待下一次迭代重新随机选择词。
  3. 重复步骤 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 随机+爬山

与其每次迭代都从原始状态选择词的组合,不妨每次只选择一个词,继承上一次迭代的路径:

  1. 创建一个空列表 list 用于存储成功的攻击样本。
  2. 进行 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,从原句表中移除该句子。
            • 如果没有分类变化但发现更大的变化量,则更新当前最大的变化量。
        • c. 比较每个词 k 的最大 logits 变化量,选择变化量最大的一个词进行替换。
      • 2.1.3 对于没有攻击成功的句子 j,将其替换词更新到表中,等待下一次迭代。
  3. 重复步骤 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\) 是降温系数。

结合退火的算法流程:

  1. 创建一个空列表 list 用于存储成功的攻击样本,初始化温度 T 和降温系数 alpha
  2. 进行 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 接受替换。
        • c. 比较每个词 kΔE 值,根据退火公式选择一个词替换。
      • 2.1.3 对于没有攻击成功的句子 j,将其替换词更新到表中,等待下一次迭代。
    • 2.2 更新温度 T = alpha * T
  3. 重复步骤 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")
posted @ 2024-08-29 01:18  rainrzk  阅读(96)  评论(0)    收藏  举报