实验4.1 文本对抗攻击
动手体验文本扰动技术,通过字符替换、同义词替换等方法影响 Qwen 模型的判断
🧪 实验 4.1:文本对抗样本
🎯 学习目标
完成本实验后,你将能够:
- ✅ 理解对抗样本的基本概念和攻击原理
- ✅ 实现字符级攻击(零宽字符、同形异码、形近字)
- ✅ 实现词级攻击(同义词替换)
- ✅ 对比不同攻击方法的效果和适用场景
📚 前置知识
- 完成模块一 ~ 三的实验
- 了解文本分类模型的基本原理
- 相关理论:模块四:对抗样本
🖥️ 实验环境
- 平台:腾讯 Cloud Studio(https://cloudstudio.net/)
- GPU:NVIDIA Tesla T4(16GB 显存)
- 模型:Qwen2-1.5B-Instruct
- Python:≥ 3.10
- 关键依赖:transformers ≥ 4.37, torch ≥ 2.0, accelerate ≥ 0.26
📝 填空说明
本实验共 5 个填空,难度:⭐⭐⭐☆☆
⏱️ 预计用时
约 30 分钟
📑 目录
1. 第一部分:环境准备(约 5 分钟)
2. 第二部分:字符级对抗攻击(约 10 分钟)
3. 第三部分:词级对抗攻击(约 5 分钟)
4. 第四部分:综合对比实验(约 10 分钟)
📤 提交说明
完成所有填空后,请将本 Notebook 文件(.ipynb)导出并提交至课程平台。评分标准:
- 5 个填空正确完成(每个 15 分,共 75 分)
- 思考题回答质量(15 分)
- 代码运行结果(10 分)
⚠️ 安全提醒:本实验仅用于教育目的,请勿将所学技术用于未授权系统。
第一部分:环境准备
加载模型并搭建一个简单的情感分类器。
# ====== 环境依赖安装 ======
%pip install "torch>=2.0,<3.0" "transformers>=4.37,<5.0" "accelerate>=0.26,<1.0" ipywidgets ipython-autotime -q
%load_ext autotime
# ====== 导入依赖 ======
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
# ====== 验证环境 ======
print("=" * 50)
print("🔧 环境检查")
print("=" * 50)
print(f" PyTorch 版本: {torch.__version__}")
print(f" CUDA 可用: {torch.cuda.is_available()}")
if torch.cuda.is_available():
print(f" GPU 型号: {torch.cuda.get_device_name(0)}")
print(f" GPU 显存: {torch.cuda.get_device_properties(0).total_memory / 1e9:.1f} GB")
print("✓ 环境检查完成!")加载模型并定义情感分类器:
# ====== 加载 Qwen2 模型 ======
model_name = "Qwen/Qwen2-1.5B-Instruct"
print("=" * 50)
print("📥 模型加载")
print("=" * 50)
print(f" 模型名称: {model_name}")
print(" (首次运行需要下载,请耐心等待...)")
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForCausalLM.from_pretrained(
model_name,
dtype=torch.float16,
device_map="auto"
)
print("=" * 50)
print("✓ 模型加载完成!")
print(f" 模型参数量: {model.num_parameters()/1e9:.2f}B")
print(f" 运行设备: {next(model.parameters()).device}")
print("=" * 50)定义情感分析函数和对抗样本生成工具:
# ====== 定义情感分类器 ======
CLASSIFIER_PROMPT = """你是一个情感分析助手。你的唯一任务是判断用户输入的文本是"正面"还是"负面"。
规则:
- 只回复"正面"或"负面"两个字,不要回复任何其他内容
- 根据文本的整体情感倾向做判断"""
def classify_sentiment(text, max_tokens=10):
"""
使用 Qwen 模型对文本进行情感分类
参数:
text (str): 待分类的文本
max_tokens (int): 最大生成长度
返回:
str: 模型的分类结果
"""
messages = [
{"role": "system", "content": CLASSIFIER_PROMPT},
{"role": "user", "content": text}
]
chat_text = tokenizer.apply_chat_template(
messages, tokenize=False, add_generation_prompt=True
)
inputs = tokenizer([chat_text], return_tensors="pt").to(model.device)
outputs = model.generate(
**inputs,
max_new_tokens=max_tokens,
temperature=0.1, # 低温度使输出更稳定
do_sample=True,
pad_token_id=tokenizer.eos_token_id
)
response = tokenizer.decode(
outputs[0][inputs['input_ids'].shape[1]:],
skip_special_tokens=True
).strip()
return response
# 快速测试分类器
test_cases = [
"这部电影真的太好看了,强烈推荐!",
"服务态度很差,再也不来了。",
"今天天气不错,心情很好。",
"这个产品质量太差了,白花钱了。",
]
print("=" * 60)
print("🧪 情感分类器基线测试")
print("=" * 60)
for text in test_cases:
result = classify_sentiment(text)
print(f" 📝 文本:{text}")
print(f" 🏷️ 判断:{result}")
print()
print("✓ 分类器准备就绪!")# ✅ 检查点:验证模型和工具函数
assert model is not None, "❌ 模型加载失败"
assert tokenizer is not None, "❌ 分词器加载失败"
assert callable(classify_sentiment), "❌ classify_sentiment 函数未定义"
print("✅ 检查点通过:模型加载成功,情感分类函数就绪!")第二部分:字符级对抗攻击
本部分实践三种字符级扰动方法,观察微小的字符修改如何影响模型的分类结果。
# ====== 引导演示:形近字替换 ======
# 把部分简体字替换为繁体字或形近字
original = "这部电影真的太好看了,强烈推荐!"
adversarial = "這部電影真的太好看了,強烈推薦!" # 简体→繁体
print("=" * 60)
print("🔬 演示:形近字替换攻击")
print("=" * 60)
print(f" 原始文本:{original}")
print(f" 对抗文本:{adversarial}")
print()
# 展示字符差异
print(" 逐字对比(不同之处用 * 标记):")
for i, (c1, c2) in enumerate(zip(original, adversarial)):
if c1 != c2:
print(f" 位置 {i}: '{c1}' → '{c2}' *")
print()
orig_result = classify_sentiment(original)
adv_result = classify_sentiment(adversarial)
print(f" 原始文本分类:{orig_result}")
print(f" 对抗文本分类:{adv_result}")
print(f" 攻击结果:{'✓ 分类改变' if orig_result != adv_result else '✗ 分类未变'}")运行字符级别对抗样本测试:
# ========== 填空 1:零宽字符插入攻击 ==========
#
# 🎯 任务:在文本中插入零宽字符(\u200b),观察是否影响分类
#
# 💡 提示:
# - 零宽字符是不可见的 Unicode 字符
# - 可以用 Python 字符串拼接,在每个字之间插入 "\u200b"
# - 使用 "\u200b".join(list(text)) 可以在每个字符之间插入零宽字符
#
# 请将 ___________ 替换为正确的代码
original = "服务态度很差,再也不来了。"
# 在每个字符之间插入零宽字符
adversarial_zwj = ___________
# 期望:生成带有零宽字符的版本,如 "服\u200b务\u200b态\u200b度..."
print("=" * 60)
print("🔬 填空1:零宽字符插入攻击")
print("=" * 60)
print(f" 原始文本:{original}")
print(f" 对抗文本(显示):{adversarial_zwj}")
print(f" 原始文本长度:{len(original)} 字符")
print(f" 对抗文本长度:{len(adversarial_zwj)} 字符")
print()
orig_result = classify_sentiment(original)
adv_result = classify_sentiment(adversarial_zwj)
print(f" 原始文本分类:{orig_result}")
print(f" 对抗文本分类:{adv_result}")
print(f" 攻击结果:{'✓ 分类改变' if orig_result != adv_result else '✗ 分类未变'}")测试语义级别对抗样本——通过同义词替换改变情感判断:
# ========== 填空 2:同形异码字符替换 ==========
#
# 🎯 任务:将英文字母替换为外观相同的西里尔字母
#
# 💡 提示:
# - 英文 "a" 和 西里尔字母 "а"(\u0430)外观一样但编码不同
# - 英文 "o" 和 西里尔字母 "о"(\u043e)也是如此
# - 英文 "e" 和 西里尔字母 "е"(\u0435)同样如此
# - 使用 str.replace() 方法替换
#
# 请将 ___________ 替换为正确的代码
original_en = "This product is really bad, waste of money!"
# 将英文字母替换为西里尔字母
adversarial_homoglyph = ___________
# 期望:将 original_en 中的 "a" 替换为 "\u0430","o" 替换为 "\u043e"
print("=" * 60)
print("🔬 填空2:同形异码字符替换")
print("=" * 60)
print(f" 原始文本:{original_en}")
print(f" 对抗文本:{adversarial_homoglyph}")
print(f" 看起来一样吗?{'是' if original_en == adversarial_homoglyph else '不是(编码不同)'}")
print()
# 展示编码差异
print(" 编码对比(前20个字符):")
for i in range(min(20, len(original_en))):
o = original_en[i]
a = adversarial_homoglyph[i] if i < len(adversarial_homoglyph) else ""
marker = " *" if o != a else ""
print(f" '{o}' (U+{ord(o):04X}) → '{a}' (U+{ord(a):04X}){marker}")
print()
orig_result = classify_sentiment(original_en)
adv_result = classify_sentiment(adversarial_homoglyph)
print(f" 原始文本分类:{orig_result}")
print(f" 对抗文本分类:{adv_result}")
print(f" 攻击结果:{'✓ 分类改变' if orig_result != adv_result else '✗ 分类未变'}")🤔 思考一下
1. 零宽字符攻击成功了吗? 如果成功或失败,原因可能是什么?
2. 同形异码攻击对中文和英文的效果可能不同吗? 为什么?
3. 这些修改对人类来说几乎不可见,但对模型影响如何? 这说明了什么?
第三部分:词级对抗攻击
本部分实践同义词替换攻击——保持含义不变,但替换关键词语。
# ========== 填空 3:同义词替换攻击 ==========
#
# 🎯 任务:通过替换同义词来构造对抗样本
#
# 💡 提示:
# - 选择情感色彩强烈的词语进行替换
# - "太好看了" → "蛮好看的"、"不错的"
# - "强烈推荐" → "值得一看"、"可以看看"
# - 保持句子通顺,含义基本不变,但降低情感强度
#
# 请将 ___________ 替换为你构造的对抗文本
original = "这部电影真的太好看了,强烈推荐!"
# 通过同义词替换构造对抗样本
adversarial_synonym = ___________
# 期望:一个含义相近但用词不同的字符串,如 "这部电影还蛮不错的,值得一看。"
print("=" * 60)
print("🔬 填空3:同义词替换攻击")
print("=" * 60)
print(f" 原始文本:{original}")
print(f" 对抗文本:{adversarial_synonym}")
print()
orig_result = classify_sentiment(original)
adv_result = classify_sentiment(adversarial_synonym)
print(f" 原始文本分类:{orig_result}")
print(f" 对抗文本分类:{adv_result}")
print(f" 攻击结果:{'✓ 分类改变' if orig_result != adv_result else '✗ 分类未变'}")运行填空 3 的对抗样本检测器,测试检测效果:
# ====== 批量同义词替换测试 ======
# 对一组负面评价进行同义词替换,观察攻击成功率
negative_pairs = [
("服务态度很差,再也不来了。", "服务态度不太好,不太想再来了。"),
("这个产品质量太差了,白花钱了。", "这个产品质量一般般,性价比不高。"),
("难吃到极点,完全无法接受!", "味道不太好,不太符合期望。"),
("客服回复慢得要死,问题根本没解决。", "客服回复稍慢,问题还没有完全解决。"),
]
print("=" * 60)
print("📊 批量同义词替换测试")
print("=" * 60)
attack_success = 0
for orig, adv in negative_pairs:
orig_result = classify_sentiment(orig)
adv_result = classify_sentiment(adv)
success = orig_result != adv_result
if success:
attack_success += 1
print(f"\n 原始:{orig}")
print(f" 对抗:{adv}")
print(f" 分类:{orig_result} → {adv_result} {'✓ 攻击成功' if success else '✗ 攻击失败'}")
print(f"\n{'=' * 60}")
print(f"📈 攻击成功率:{attack_success}/{len(negative_pairs)} = {attack_success/len(negative_pairs):.0%}")第四部分:综合对比实验
将所有攻击方法汇总,对同一组文本进行测试,对比不同方法的效果。
# ========== 填空 4:构造多种扰动并对比 ==========
#
# 🎯 任务:对同一段文本,分别应用三种扰动方法,对比效果
#
# 💡 提示:
# - 原始文本:"这家餐厅的菜品非常美味,环境也很好,下次还来!"
# - 方法1 形近字:将部分简体字换成繁体字(如 "餐厅"→"餐廳","美味"→"美味"(不变),"環境")
# - 方法2 零宽字符:在每个字之间插入 \u200b
# - 方法3 同义词:替换关键情感词(如 "非常美味"→"还可以")
#
# 请将 ___________ 替换为形近字替换版本
original = "这家餐厅的菜品非常美味,环境也很好,下次还来!"
# 形近字替换
adversarial_char = ___________
# 期望:简繁混合版本,如 "這家餐廳的菜品非常美味,環境也很好,下次還來!"
# 零宽字符(复用之前的方法)
adversarial_zwj = "\u200b".join(list(original))
# 同义词替换
adversarial_syn = "这家餐厅的菜品味道还行,环境一般,有机会可以再来。"
# 对比测试
methods = {
"原始文本": original,
"形近字替换": adversarial_char,
"零宽字符": adversarial_zwj,
"同义词替换": adversarial_syn,
}
print("=" * 60)
print("📊 多方法对抗攻击对比")
print("=" * 60)
results = {}
for method, text in methods.items():
result = classify_sentiment(text)
results[method] = result
tag = "📝 原始" if method == "原始文本" else "💉 对抗"
print(f"\n {tag} [{method}]")
print(f" 文本:{text[:40]}...")
print(f" 分类:{result}")
# 统计
orig_label = results["原始文本"]
changed = sum(1 for k, v in results.items() if k != "原始文本" and v != orig_label)
print(f"\n{'=' * 60}")
print(f"📈 攻击统计:{changed}/3 种方法改变了分类结果")生成对抗攻击实验的综合分析报告:
# ========== 填空 5:设计自己的对抗样本 ==========
#
# 🎯 任务:综合运用本实验学到的技术,设计一个最有可能改变分类结果的对抗样本
#
# 💡 提示:
# - 可以组合使用多种扰动方法
# - 目标:让一条正面评价被分类为负面,或反之
# - 提示:结合字符替换和词语调整可能效果更好
# - 原始文本是正面的,尝试让模型判断为负面
#
# 请将 ___________ 替换为你设计的对抗样本
original = "老板人很好,菜量大又实惠,五星好评!"
# 设计你的对抗样本(目标:让模型把这条正面评价判为负面)
my_adversarial = ___________
# 期望:一个结合了多种扰动技术的对抗文本
print("=" * 60)
print("🎨 填空5:你设计的对抗样本")
print("=" * 60)
print(f" 原始文本:{original}")
print(f" 对抗文本:{my_adversarial}")
print()
orig_result = classify_sentiment(original)
adv_result = classify_sentiment(my_adversarial)
print(f" 原始分类:{orig_result}")
print(f" 对抗分类:{adv_result}")
print(f" 攻击结果:{'✓ 成功!分类被改变了' if orig_result != adv_result else '✗ 未成功,可以尝试其他策略'}")🤔 思考一下
1. 哪种攻击方法最有效? 字符级还是词级?为什么?
2. LLM 和传统分类模型的对抗鲁棒性有什么不同? LLM 因为理解语义,对某些字符级攻击可能更鲁棒,但对含义微妙变化可能更敏感。
3. 如何防御这些攻击? 回忆模块三第2章学到的输入层防护方法。
📋 实验小结
核心收获
1. 概念:对抗样本通过微小修改欺骗模型,根源在于模型关注"模式"而非"含义"
2. 技能:掌握了字符级(形近字、零宽字符、同形异码)和词级(同义词替换)两类对抗攻击方法
3. 思考:LLM 的语义理解能力使其对字符级攻击可能有一定鲁棒性,但仍非无懈可击
关键代码回顾
``
python
零宽字符插入
adversarial = "\u200b".join(list(original_text))同形异码替换
adversarial = text.replace("a", "\u0430").replace("o", "\u043e")同义词替换
adversarial = "还可以" # 替代 "非常美味"
``与防御的关联
- 模块三第2章的特殊字符清洗可以防御零宽字符攻击
- Unicode 规范化可以防御同形异码攻击
- 语义级分析可以减少同义词替换的影响
📎 参考答案
点击展开参考答案
填空 1:零宽字符插入
``python`
adversarial_zwj = "\u200b".join(list(original))
填空 2:同形异码字符替换
`python`
adversarial_homoglyph = original_en.replace("a", "\u0430").replace("o", "\u043e")
填空 3:同义词替换
`python`
adversarial_synonym = "这部电影还蛮不错的,值得一看。"
填空 4:形近字替换
`python`
adversarial_char = "這家餐廳的菜品非常美味,環境也很好,下次還來!"
填空 5:综合对抗样本(答案不唯一,以下是一个示例)
`python``
my_adversarial = "老板人還行吧,菜量大不过味道一般,三星。"
说明:结合了形近字替换(好→還行吧)和同义词替换(五星好评→三星),改变情感倾向。
⚠️ 实验结束提醒
实验完成后,请停止 Cloud Studio 运行中的实例以节省 GPU 资源额度。
操作步骤:Cloud Studio 控制台 → 运行中的实例 → 停止