boundary_shrink
在boundary_shrink
方法中,训练和攻击过程是通过一系列精心设计的步骤来微调模型的。这一过程主要依赖于对抗性训练,其核心思想是利用对抗性样本(即被故意施加扰动的样本)来训练模型,从而使模型在面对扰动输入时更加鲁棒。下面详细解释这一过程:
对抗性样本的生成
- 生成对抗性样本 (
x_adv
): 使用FGSM(快速梯度符号攻击)方法对每个批次的输入数据x
添加扰动,生成对抗性样本。这一步的目的是创建稍微偏离原始数据分布的新样本,这些样本在人类观察者看来与原始样本没有明显区别,但足以欺骗神经网络模型。x_adv = adv.perturb(x, y, target_y=None, model=test_model, device=device)
模型的预测和微调
-
模型预测: 使用添加了扰动的数据
x_adv
对test_model
进行预测,得到预测标签pred_label
。这一步骤检测模型对于对抗性样本的反应,即看模型如何分类经过细微修改的数据。adv_logits = test_model(x_adv)
pred_label = torch.argmax(adv_logits, dim=1)
-
计算损失并更新
unlearn_model
: 利用unlearn_model
对原始数据x
的预测输出和步骤2中得到的pred_label
计算损失(即ori_loss
)。然后,根据这个损失更新unlearn_model
的参数,以使得unlearn_model
在面对对抗性样本时,尽可能地模拟test_model
的行为。这里的关键在于,我们希望通过训练使unlearn_model
在对抗性样本上的预测与test_model
尽可能接近。ori_logits = unlearn_model(x)
ori_loss = criterion(ori_logits, pred_label)
loss.backward()
optimizer.step()
额外实验设置的考虑
- 额外实验设置 (
extra_exp
): 根据extra_exp
参数的值,可能会进一步调整损失函数来考虑额外的实验条件,如模型的曲率变化(curv
)或权重分配(weight_assign
)。这些高级特性允许实验者根据特定的实验需求调整训练过程,例如通过考虑模型在局部的曲率变化来增加对模型参数微调的精细控制。- 如果设置为
curv
,则计算原始模型和unlearn_model
的曲率,根据它们的差异调整损失。 - 如果设置为
weight_assign
,则根据预测的置信度分配权重,以在损失函数中给予不同样本不同的重视程度。
- 如果设置为
通过以上步骤,boundary_shrink
方法利用对抗性训练来微调模型,特别是通过对训练数据施加细微的扰动,并根据这些扰动过的数据更新模型,以达到修改模型决策边界的目的。这种方法尤其适用于实现对特定信息的“遗忘”,因为它通过有目的地调整模型对某些类别的响应,来减少模型对这些类别信息的敏感性。
其中的
x_adv = adv.perturb(x, y, target_y=None, model=test_model, device=device)# 生成对抗样本
adv_logits = test_model(x_adv) # 对对抗样本进行预测
pred_label = torch.argmax(adv_logits, dim=1) # 获取预测标签
if itr >= (poison_epoch - 1) * batches_per_epoch:
nearest_label.append(pred_label.tolist()) # 在最后一个epoch收集预测标签
这几行代码的目的是在对抗性训练过程中生成对抗样本,对这些样本进行模型预测,并根据预测结果进行后续处理。具体来说:
-
生成对抗样本:
x_adv = adv.perturb(x, y, target_y=None, model=test_model, device=device)
: 这行代码使用已定义的对抗攻击方法(例如FGSM)来生成对抗样本x_adv
。对抗样本是原始输入x
加上了经过精心设计的扰动,这种扰动旨在欺骗模型,导致模型做出错误的预测。target_y=None
意味着这次攻击不是针对特定目标类别的,而是一种非目标性攻击。
-
对对抗样本进行预测:
adv_logits = test_model(x_adv)
: 使用测试模型test_model
对生成的对抗样本x_adv
进行预测,输出为adv_logits
。这一步骤检验了对抗样本是否能够成功地引导模型做出错误的预测。
-
获取预测标签:
pred_label = torch.argmax(adv_logits, dim=1)
: 通过取adv_logits
(模型输出的逻辑值或得分)的argmax
来获取预测的类别标签。dim=1
表示我们沿着每个样本的得分向量取最大值的索引,这个索引即为模型预测的类别。
-
在最后一个epoch收集预测标签:
if itr >= (poison_epoch - 1) * batches_per_epoch: nearest_label.append(pred_label.tolist())
: 这段代码的意图是在训练的最后一个epoch时,收集模型对于对抗样本的预测标签。这里使用了一个条件判断,itr >= (poison_epoch - 1) * batches_per_epoch
,用于确定当前迭代次数是否已经达到了最后一个epoch的开始。如果条件为真,表明当前迭代处于最后一个epoch,因此会执行nearest_label.append(pred_label.tolist())
,将这一批预测标签转换为列表形式并添加到nearest_label
列表中。这样做的目的可能是为了后续分析模型在最后阶段对于对抗样本的响应,比如评估攻击的效果或者是用于进一步的数据分析。
总之,这几步操作核心在于利用对抗攻击方法探究模型的脆弱性,通过观察模型对扰动后数据的响应来了解模型的决策边界,并在攻击过程的最后阶段收集关键数据以供后续分析使用。
值得注意的是,在这段代码中,for itr in tqdm.tqdm(range(poison_epoch * batches_per_epoch)):
这个循环执行了对抗性训练的过程,具体地,是对指定的遗忘数据进行反复的对抗攻击和模型训练,以实现对这部分数据的有效遗忘。poison_epoch
参数在这里设置为10,意味着整个遗忘训练过程将遍历遗忘数据集10次。每次遍历称为一个“epoch”,这是机器学习中常见的术语,代表整个数据集的一次前向和反向传播过程。
以下是该循环的主要步骤解释:
-
对抗样本生成:对每个批次的数据,使用预定义的对抗攻击方法(例如FGSM)生成对抗样本。这些对抗样本是通过在原始输入数据上添加微小扰动生成的,目的是使模型产生错误的预测。
-
模型预测与统计:通过攻击模型对生成的对抗样本进行预测,并统计模型预测错误的数量。这些信息用于评估攻击的成功率,并在最后一个epoch收集模型对对抗样本的预测标签。
-
对抗性训练:在对抗性训练阶段,模型使用生成的对抗样本进行训练,目的是使模型在面对这些经过精心设计的扰动时更加鲁棒。通过这种方式,模型学会忽略被遗忘类别的特征,从而实现对指定类别数据的遗忘。
-
损失函数调整:根据额外的实验设置,可能会对损失函数进行调整,例如考虑曲率变化或权重分配等因素,以更有效地实现遗忘目标。
设置 poison_epoch=10
的原因是为了确保模型有足够的训练来适应对抗样本的影响,从而更好地遗忘指定的类别。通过重复多次训练,模型在学习如何忽略被遗忘类别的同时,还能保持对其他类别的识别能力。这个数字(10)是一个经验值,可以根据具体任务和模型的性能进行调整。在实践中,可能需要通过实验来找到最佳的 poison_epoch
值,以平衡遗忘效果和训练成本。
如果 batches_per_epoch=79
,则 poison_epoch * batches_per_epoch=790
确实表明整个训练过程将遍历整个遗忘数据集10次(因为 poison_epoch=10
),每次遍历包含79个批次的数据。这意味着总共将处理790个批次的数据,但这并不是通过扩展数据样本数量实现的,而是通过重复遍历同一数据集多次来完成的。
当执行 forget_data_gen.__next__()
时,它实际上是从遗忘数据生成器中获取下一个批次的数据。这里的生成器是通过 inf_generator(train_forget_loader)
创建的,它能够无限次遍历训练加载器 train_forget_loader
。因此,当所有79个批次的数据被遍历一次后,生成器会自动重新开始,再次从第一个批次开始,如此循环,直到完成所有的790个批次。
简而言之,forget_data_gen.__next__()
在每次调用时选择的是遗忘数据集中的下一个批次,而通过重复遍历数据集10次(总共790个批次),实现了数据的“重复扩展”。这个过程是为了确保遗忘类别数据上的对抗性训练足够充分,以达到良好的遗忘效果。
boundary_expanding
boundary_expanding
函数设计用于扩展神经网络模型的决策边界,特别是通过增加一个额外的类别并对模型进行微调来实现。这种方法可以用于模型遗忘某些特定类别的信息,即通过将目标类别的数据标记为新增的类别,并通过训练使模型将这些数据与原有的类别区分开来。以下是对这个函数中每一行代码的详细解释,以及对整个函数内容的额外详细描述:
-
初始化和模型准备:
- 记录开始时间。
- 复制原始模型(
ori_model
)到narrow_model
,并将其转移到指定的设备上(device
)。 - 分离出
narrow_model
中的特征提取器(feature_extrator
)和分类器(classifier
)。
-
修改和扩展分类器:
- 创建一个新的线性层(
widen_classifier
),输出维度为原类别数加一,以容纳新增的类别。 - 初始化这个新的线性层。
- 将特征提取器和新的线性层组合成一个新的模型(
widen_model
)。 - 将原有分类器的参数复制到新的分类器中,除了为新增类别保留的参数。
- 创建一个新的线性层(
-
准备数据和训练:
- 通过
inf_generator
创建一个无限数据生成器,从遗忘数据加载器(train_forget_loader
)中获取数据。 - 设置微调的epoch数(
finetune_epochs
)。 - 选择损失函数和优化器。
- 通过
-
微调过程:
- 在指定的epoch数内,对每批数据执行以下步骤:
- 通过新模型(
widen_model
)计算输出(widen_logits
)。 - 为每个样本设置目标标签为新增的类别索引。
- 计算损失,执行反向传播和参数更新。
- 通过新模型(
- 在指定的epoch数内,对每批数据执行以下步骤:
-
剪枝过程:
- 创建一个新的分类器(
pruned_classifier
),只针对原有的类别。 - 将
widen_model
中的参数复制回pruned_classifier
,忽略新增类别的参数。 - 组合特征提取器和剪枝后的分类器成为最终的模型(
pruned_model
)。
- 创建一个新的分类器(
-
性能评估:
- 对剪枝后的模型(
pruned_model
)在各个数据集上进行评估,包括完整测试集、遗忘类别测试集、保留类别测试集、遗忘类别训练集和保留类别训练集。 - 打印和返回评估结果。
- 对剪枝后的模型(
-
保存模型:
- 保存扩展后的模型(
widen_model
)和剪枝后的模型(pruned_model
)到指定路径。
- 保存扩展后的模型(
这个boundary_expanding
函数通过对原始模型进行修改和扩展,增加一个新的类别,并通过针对性的训练使得模型能够将特定类别的信息“遗忘”(即将这些信息归类到新增的类别)。通过这种方法,可以实现对模型的细粒度控制,调整模型的决策边界,以适应特定的数据隐私或模型更新需求。最后,通过评估剪枝后的模型性能,可以验证“遗忘”过程的效果和模型整体的表现。
inf_generator
这个inf_generator
函数定义了一个无限迭代器,它可以不断地从一个可迭代对象(如数据加载器)中生成数据,即使到达了可迭代对象的末尾也会重新开始。这对于某些需要持续训练或反复利用数据集的场景特别有用,比如在线学习、对抗性训练或者需要固定步数而不是固定epoch数的训练场景。下面是对这个函数的逐行解释:
-
定义函数并接收一个可迭代对象:
def inf_generator(iterable)
: 这个函数接受一个可迭代对象iterable
作为输入。这个可迭代对象可以是任何Python迭代器,例如列表、元组或者更常见的,PyTorch的数据加载器。
-
创建迭代器:
iterator = iterable.__iter__()
: 从可迭代对象iterable
中创建一个迭代器iterator
。这一步是通过调用可迭代对象的__iter__()
方法实现的,它返回一个迭代器对象,可以让我们一次获取可迭代对象的一个元素。
-
无限循环生成数据:
while True
: 开始一个无限循环,确保数据可以不断被生成。
-
尝试从迭代器获取下一个元素:
- 使用
try
语句尝试从迭代器中获取下一个元素,yield iterator.__next__()
。这一步通过迭代器的__next__()
方法实现,每次调用都会返回迭代器中的下一个元素。如果迭代器中还有元素,yield
语句会将这个元素返回给函数的调用者,并在下次__next__ ()
被调用时从当前位置继续执行。
- 使用
-
处理迭代器耗尽的情况:
except StopIteration
: 当迭代器中的元素全部被遍历完,尝试再次从迭代器获取元素时会抛出StopIteration
异常。在这种情况下,except
块会被执行。iterator = iterable.__iter__()
: 当捕获到StopIteration
异常时,意味着原有的迭代器已经耗尽。这时,通过再次调用iterable.__iter__()
来重新创建一个新的迭代器,使得数据生成可以从头开始,实现无限循环地提供数据。
总之,inf_generator
函数通过不断重置迭代器来实现对任意可迭代对象的无限次遍历,这使得在需要无限或不确定数量迭代的场景下非常有用,如长时间的模型训练过程。通过这种方式,即使原始数据集的大小有限,也可以通过无限循环地重复利用数据,支持模型的持续训练。
get_unlearn_loader
这段代码定义了一个名为 get_unlearn_loader
的函数,旨在从训练集和测试集中分离出特定类别(即要“遗忘”的类别),并为这些数据以及剩余的数据创建相应的数据加载器。以下是对该函数解释:
-
函数定义:定义了一个函数
get_unlearn_loader
,它接收训练集trainset
、测试集testset
、要遗忘的类别forget_class
、批量大小batch_size
、遗忘数量num_forget
和修复数比例repair_num_ratio
作为参数。 -
调用
split_class_data
函数分割训练集:根据指定的要遗忘的类别和遗忘数量,将训练集分割为遗忘的数据索引train_forget_index
、剩余的数据索引train_remain_index
和剩余类别的索引class_remain_index
。 -
调用
split_class_data
函数分割测试集:根据指定的要遗忘的类别,将测试集分割为遗忘的数据索引test_forget_index
和剩余的数据索引test_remain_index
。这里,测试集的遗忘数量设置为测试集的总长度,即遗忘测试集中所有属于指定类别的数据。 -
选择修复数据的索引:从剩余类别的索引中随机选择一部分作为修复数据的索引,其数量是剩余类别索引数量的
repair_num_ratio
比例。 -
创建采样器:使用
SubsetRandomSampler
根据上面得到的索引创建各自的采样器,分别用于遗忘的训练数据、剩余的训练数据、修复数据、遗忘的测试数据和剩余的测试数据。 -
创建数据加载器:使用
torch.utils.data.DataLoader
和上一步创建的采样器来创建数据加载器,这些加载器用于批量加载对应的数据集。 -
返回值:函数返回所有创建的数据加载器和数据索引,包括遗忘的训练数据加载器
train_forget_loader
、剩余的训练数据加载器train_remain_loader
、遗忘的测试数据加载器test_forget_loader
、剩余的测试数据加载器test_remain_loader
和修复数据加载器repair_class_loader
,以及相应的数据索引。
这个函数的目的是为了支持在机器学习模型的训练过程中实现对特定类别数据的遗忘,同时通过选定的修复数据来尝试修复可能因遗忘而造成的学习效果损失。通过这种方式,可以研究和评估模型对于遗忘和修复操作的反应和适应能力。