模块五 数据投毒实验
实验 5.1:标签翻转攻击
实践标签翻转投毒攻击,观察不同投毒比例对模型准确率的影响
实验目标
本实验将帮助你理解标签翻转攻击的原理,并通过实际操作体验数据投毒对模型性能的影响。
学习目标
完成本实验后,你将能够:
- 理解标签翻转攻击的基本原理和实现步骤
- 构造包含错误标签的投毒数据集
- 观察不同投毒比例对模型准确率的影响
- 对比目标攻击与无目标攻击的效果差异
- 分析投毒攻击的隐蔽性与攻击效果之间的权衡
实验前提
环境要求
- Python 3.8+
- PyTorch 1.10+
- torchvision
- matplotlib
- numpy
- scikit-learn
确保已安装所需依赖后再开始实验。
实验内容
实验 5.1:标签翻转攻击
实验目标
- 理解标签翻转攻击的原理
- 观察不同投毒比例对模型性能的影响
- 体验数据投毒攻击的持久性危害
实验背景
标签翻转攻击是最简单的数据投毒方式:故意给数据标注错误的标签。
虽然简单,但即使只污染1-5%的数据,也能显著影响模型性能。
预计时间:20分钟
第一步:环境准备
In [ ]:
# 导入必要的库
import torch
import torch.nn as nn
import torch.optim as optim
import numpy as np
import matplotlib.pyplot as plt
# 设置中文显示
plt.rcParams['font.sans-serif'] = ['SimHei', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
# 设置随机种子,确保结果可复现
torch.manual_seed(42)
np.random.seed(42)
print("环境准备完成!")第二步:创建模拟数据集
我们创建一个简单的二分类数据集来演示标签翻转攻击。
In [ ]:
def create_dataset(n_samples=1000):
"""
创建一个简单的二分类数据集
类别0:中心在(-2, -2)的点
类别1:中心在(2, 2)的点
"""
# 类别0的数据点
X0 = np.random.randn(n_samples // 2, 2) + np.array([-2, -2])
y0 = np.zeros(n_samples // 2)
# 类别1的数据点
X1 = np.random.randn(n_samples // 2, 2) + np.array([2, 2])
y1 = np.ones(n_samples // 2)
# 合并数据
X = np.vstack([X0, X1])
y = np.hstack([y0, y1])
# 打乱顺序
indices = np.random.permutation(n_samples)
return X[indices], y[indices]
# 创建训练集和测试集
X_train, y_train = create_dataset(800)
X_test, y_test = create_dataset(200)
print(f"训练集大小: {len(X_train)}")
print(f"测试集大小: {len(X_test)}")
# 可视化原始数据
plt.figure(figsize=(8, 6))
plt.scatter(X_train[y_train==0, 0], X_train[y_train==0, 1], c='blue', label='类别0', alpha=0.5)
plt.scatter(X_train[y_train==1, 0], X_train[y_train==1, 1], c='red', label='类别1', alpha=0.5)
plt.xlabel('特征1')
plt.ylabel('特征2')
plt.title('原始训练数据(干净)')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()第三步:实现标签翻转攻击
标签翻转攻击的核心:随机选择一部分样本,将其标签翻转(0→1 或 1→0)。
In [ ]:
def poison_labels(y, poison_ratio):
"""
对标签进行投毒(翻转)
参数:
y: 原始标签数组
poison_ratio: 投毒比例(0.0-1.0)
返回:
投毒后的标签数组
"""
y_poisoned = y.copy()
n_samples = len(y)
# 【填空1】计算需要翻转的样本数量
# 提示:用总样本数乘以投毒比例,然后取整
# 参考答案:n_poison = int(n_samples * poison_ratio)
n_poison = ___________________
# 随机选择要翻转的样本索引
poison_indices = np.random.choice(n_samples, n_poison, replace=False)
# 【填空2】翻转选中样本的标签(0变1,1变0)
# 提示:用 1 减去原标签即可实现翻转
# 参考答案:y_poisoned[poison_indices] = 1 - y_poisoned[poison_indices]
y_poisoned[poison_indices] = ___________________
return y_poisoned, poison_indices
# 测试:对10%的数据进行投毒
y_train_poisoned, poison_idx = poison_labels(y_train, 0.1)
print(f"投毒样本数量: {len(poison_idx)}")
print(f"投毒比例: {len(poison_idx)/len(y_train)*100:.1f}%")第四步:定义简单分类模型
In [ ]:
class SimpleClassifier(nn.Module):
"""简单的二分类神经网络"""
def __init__(self):
super().__init__()
# 两层全连接网络
self.fc1 = nn.Linear(2, 16) # 输入2维,隐藏层16个神经元
self.fc2 = nn.Linear(16, 1) # 输出1维(二分类)
self.sigmoid = nn.Sigmoid()
def forward(self, x):
x = torch.relu(self.fc1(x))
x = self.sigmoid(self.fc2(x))
return x
def train_model(X_train, y_train, epochs=100):
"""训练分类模型"""
# 转换为PyTorch张量
X = torch.FloatTensor(X_train)
y = torch.FloatTensor(y_train).reshape(-1, 1)
# 创建模型
model = SimpleClassifier()
criterion = nn.BCELoss() # 二分类交叉熵损失
optimizer = optim.Adam(model.parameters(), lr=0.01)
# 训练
for epoch in range(epochs):
optimizer.zero_grad()
outputs = model(X)
loss = criterion(outputs, y)
loss.backward()
optimizer.step()
return model
def evaluate_model(model, X_test, y_test):
"""评估模型准确率"""
X = torch.FloatTensor(X_test)
with torch.no_grad():
predictions = model(X).numpy().flatten()
# 【填空3】将概率转换为类别预测(>0.5为类别1)
# 提示:使用比较运算符,结果转为整数
# 参考答案:predicted_labels = (predictions > 0.5).astype(int)
predicted_labels = ___________________
# 计算准确率
accuracy = np.mean(predicted_labels == y_test)
return accuracy
print("模型定义完成!")第五步:对比实验 - 不同投毒比例的影响
In [ ]:
# 测试不同的投毒比例
poison_ratios = [0.0, 0.05, 0.10, 0.15, 0.20, 0.30]
accuracies = []
print("开始对比实验...")
print("="*50)
for ratio in poison_ratios:
# 对训练数据进行投毒
y_poisoned, _ = poison_labels(y_train, ratio)
# 训练模型
model = train_model(X_train, y_poisoned, epochs=100)
# 在干净的测试集上评估
acc = evaluate_model(model, X_test, y_test)
accuracies.append(acc)
print(f"投毒比例: {ratio*100:5.1f}% | 测试准确率: {acc*100:.2f}%")
print("="*50)
print("实验完成!")第六步:可视化投毒效果
In [ ]:
# 绘制投毒比例与准确率的关系
plt.figure(figsize=(10, 5))
# 左图:准确率变化
plt.subplot(1, 2, 1)
plt.plot([r*100 for r in poison_ratios], [a*100 for a in accuracies],
'bo-', linewidth=2, markersize=8)
plt.xlabel('投毒比例 (%)')
plt.ylabel('测试准确率 (%)')
plt.title('标签翻转攻击效果')
plt.grid(True, alpha=0.3)
plt.ylim([40, 100])
# 右图:准确率下降幅度
plt.subplot(1, 2, 2)
baseline = accuracies[0]
drops = [(baseline - a) * 100 for a in accuracies]
colors = ['green' if d < 5 else 'orange' if d < 15 else 'red' for d in drops]
plt.bar([f"{r*100:.0f}%" for r in poison_ratios], drops, color=colors)
plt.xlabel('投毒比例')
plt.ylabel('准确率下降 (%)')
plt.title('准确率下降幅度')
plt.axhline(y=5, color='orange', linestyle='--', label='轻微影响线')
plt.axhline(y=15, color='red', linestyle='--', label='严重影响线')
plt.legend()
plt.tight_layout()
plt.show()第七步:可视化投毒数据分布
In [ ]:
# 对20%的数据投毒,可视化投毒样本的位置
y_poisoned_20, poison_idx_20 = poison_labels(y_train, 0.20)
plt.figure(figsize=(12, 5))
# 左图:原始数据
plt.subplot(1, 2, 1)
plt.scatter(X_train[y_train==0, 0], X_train[y_train==0, 1],
c='blue', label='类别0', alpha=0.5)
plt.scatter(X_train[y_train==1, 0], X_train[y_train==1, 1],
c='red', label='类别1', alpha=0.5)
plt.xlabel('特征1')
plt.ylabel('特征2')
plt.title('原始数据(干净标签)')
plt.legend()
plt.grid(True, alpha=0.3)
# 右图:投毒后数据(标记被翻转的样本)
plt.subplot(1, 2, 2)
# 正常样本
normal_idx = np.setdiff1d(np.arange(len(y_train)), poison_idx_20)
plt.scatter(X_train[normal_idx, 0], X_train[normal_idx, 1],
c=['blue' if y==0 else 'red' for y in y_poisoned_20[normal_idx]],
alpha=0.3, label='正常样本')
# 投毒样本(用特殊标记)
plt.scatter(X_train[poison_idx_20, 0], X_train[poison_idx_20, 1],
c='yellow', edgecolors='black', s=100, marker='X',
label='投毒样本(标签被翻转)')
plt.xlabel('特征1')
plt.ylabel('特征2')
plt.title('投毒后数据(20%标签被翻转)')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
print("\n观察:黄色X标记的点是被投毒的样本")
print("这些点的标签被翻转,会导致模型学习到错误的决策边界")实验总结
关键发现
1. 投毒效果显著:即使只翻转5-10%的标签,模型准确率就会明显下降
2. 攻击简单有效:标签翻转是最简单的投毒方式,不需要了解模型结构
3. 危害持久:一旦模型被投毒数据训练,错误会持续存在
思考问题
1. 为什么即使只有少量投毒样本,也能显著影响模型性能?
2. 如果你是数据收集方,如何防止众包标注中的恶意标签?
3. 标签翻转攻击有什么明显的缺点?(提示:考虑检测难度)
In [ ]:
# 实验完成检查
print("="*50)
print("实验 5.1 完成!")
print("="*50)
print("\n请回答以下问题:")
print("1. 投毒比例从0%增加到20%,准确率下降了多少?")
print("2. 观察可视化图,投毒样本主要分布在什么位置?")
print("3. 为什么标签翻转攻击容易被检测?")实验总结
完成检查
完成本实验后,你应该已经:
- 成功实现了标签翻转攻击
- 对比了不同投毒比例(1%、5%、10%、20%)下模型准确率的变化
- 理解了无目标攻击如何降低模型整体性能
- 理解了目标攻击如何针对特定类别制造误分类
- 认识到数据质量对模型安全的重要性
延伸思考
-
在实际场景中,攻击者如何在不被发现的情况下向训练数据中注入投毒样本?
-
如果你是防御方,你会采取什么措施来检测训练数据中可能存在的标签错误?
-
投毒比例与攻击隐蔽性之间存在什么样的权衡关系?攻击者会如何平衡这两个因素?