在本教程中,我们将通过获取一个在公共文本数据上预训练的模型,并针对不同的任务进行微调,来训练一个具有差分隐私(Differential Privacy)的文本分类器。
在使用差分隐私训练模型时,我们几乎总是在模型大小和任务准确度之间面临权衡。具体细节取决于具体问题,但一个经验法则是:模型的参数越少,就越容易通过 DP 获得良好的性能。
大多数先进的 NLP 模型都非常深且庞大(例如 BERT-base 拥有超过 1 亿个参数),这使得在隐私数据集上训练文本模型的任务相当具有挑战性。
解决此问题的一种方法是将训练过程分为两个阶段。首先,我们将在公共数据集上对模型进行预训练,让模型接触通用的文本数据。假设通用文本数据是公开的,我们在此步骤中将不使用差分隐私。然后,我们冻结大部分层,仅保留少数高层,使用 DP-SGD 在隐私数据集上进行训练。通过这种方式,我们可以兼得两者的优点——我们既拥有深度且强大的文本理解模型,同时又只使用差分隐私算法训练少量参数。
在本教程中,我们将使用预训练的 BERT-base 模型,并对其进行微调,以识别 SNLI 数据集上的文本蕴涵(textual entailment)。
我们还将进一步展示以下技术的微调结果:
首先,我们需要下载数据集(我们将使用斯坦福 NLP 镜像)。
STANFORD_SNLI_URL = "https://nlp.stanford.edu/projects/snli/snli_1.0.zip"
DATA_DIR = "data"
import zipfile
import urllib.request
import os
import warnings
warnings.simplefilter("ignore")
def download_and_extract(dataset_url, data_dir):
print("Downloading and extracting ...")
filename = "snli_1.0.zip"
urllib.request.urlretrieve(dataset_url, filename)
with zipfile.ZipFile(filename) as zip_ref:
zip_ref.extractall(data_dir)
os.remove(filename)
print("Completed!")
download_and_extract(STANFORD_SNLI_URL, DATA_DIR)
Downloading and extracting ... Completed!
该数据集有两种格式(tsv 和 json),并且已经划分为训练集/开发集/测试集(train/dev/test)。让我们验证一下情况是否如此。
snli_folder = os.path.join(DATA_DIR, "snli_1.0")
os.listdir(snli_folder)
['snli_1.0_test.jsonl', 'snli_1.0_train.txt', '.DS_Store', 'snli_1.0_dev.jsonl', 'Icon\r', 'README.txt', 'snli_1.0_dev.txt', 'snli_1.0_test.txt', 'snli_1.0_train.jsonl']
现在让我们查看一下内部内容。SNLI 数据集 提供了丰富的句法元数据,但我们只使用原始输入文本。因此,我们唯一感兴趣的字段是 sentence1(前提)、sentence2(假设)和 gold_label(由大多数标注者选择的标签)。
标签定义了前提与假设之间的关系:矛盾(contradiction)、中立(neutral) 或 蕴涵(entailment)。
import pandas as pd
train_path = os.path.join(snli_folder, "snli_1.0_train.txt")
dev_path = os.path.join(snli_folder, "snli_1.0_dev.txt")
df_train = pd.read_csv(train_path, sep='\t')
df_test = pd.read_csv(dev_path, sep='\t')
df_train[['sentence1', 'sentence2', 'gold_label']][:5]
| sentence1 | sentence2 | gold_label | |
|---|---|---|---|
| 0 | A person on a horse jumps over a broken down a... | A person is training his horse for a competition. | neutral |
| 1 | A person on a horse jumps over a broken down a... | A person is at a diner, ordering an omelette. | contradiction |
| 2 | A person on a horse jumps over a broken down a... | A person is outdoors, on a horse. | entailment |
| 3 | Children smiling and waving at camera | They are smiling at their parents | neutral |
| 4 | Children smiling and waving at camera | There are children present | entailment |
BERT(来自 Transformers 的双向编码器表示)是处理各种 NLP 任务的一种先进方法。它使用 Transformer 架构,并高度依赖预训练的概念。
我们将使用 huggingface transformers 库提供的预训练 BERT-base 模型。它为我们提供了经典 BERT 架构的 PyTorch 实现,以及在公共英语语料库(维基百科)上预训练的分词器(tokenizer)和权重。
在继续操作之前,请遵循这些 安装说明。
from transformers import BertConfig, BertTokenizer, BertForSequenceClassification
model_name = "bert-base-cased"
config = BertConfig.from_pretrained(
model_name,
num_labels=3,
)
tokenizer = BertTokenizer.from_pretrained(
"bert-base-cased",
do_lower_case=False,
)
model = BertForSequenceClassification.from_pretrained(
"bert-base-cased",
config=config,
)
config.json: 0%| | 0.00/570 [00:00<?, ?B/s]
tokenizer_config.json: 0%| | 0.00/49.0 [00:00<?, ?B/s]
vocab.txt: 0%| | 0.00/213k [00:00<?, ?B/s]
tokenizer.json: 0%| | 0.00/436k [00:00<?, ?B/s]
model.safetensors: 0%| | 0.00/436M [00:00<?, ?B/s]
Some weights of BertForSequenceClassification were not initialized from the model checkpoint at bert-base-cased and are newly initialized: ['classifier.bias', 'classifier.weight'] You should probably TRAIN this model on a down-stream task to be able to use it for predictions and inference.
该模型具有以下结构:它结合使用词嵌入、位置嵌入和标记 嵌入(embeddings) 来创建序列表示,然后通过 12 个 Transformer 编码器(transformer encoders) 传递数据,最后使用 线性分类器(linear classifier) 生成最终标签。由于模型已经过预训练,且我们只打算微调少数高层,因此我们要冻结除最后一个编码器及以上层(BertPooler 和 Classifier)之外的所有层。
from IPython.display import Image
Image(filename='img/BERT.png')
trainable_layers = [model.bert.encoder.layer[-1], model.bert.pooler, model.classifier]
total_params = 0
trainable_params = 0
for p in model.parameters():
p.requires_grad = False
total_params += p.numel()
for layer in trainable_layers:
for p in layer.parameters():
p.requires_grad = True
trainable_params += p.numel()
print(f"Total parameters count: {total_params:,}") # ~108M
print(f"Trainable parameters count: {trainable_params:,}") # ~7M
Total parameters count: 108,312,579 Trainable parameters count: 7,680,771
因此,通过使用预训练模型,我们将可训练参数的数量从超过 1 亿减少到略高于 750 万。这将有助于在添加噪声的情况下提高性能和收敛性。
在开始训练之前,我们需要预处理数据并将其转换为模型预期的格式。
(注意:在笔记本电脑上运行大约需要 5-10 分钟)
import torch
import torch.nn as nn
import transformers
from torch.utils.data import TensorDataset
from transformers.data.processors.utils import InputExample
from transformers.data.processors.glue import glue_convert_examples_to_features
LABEL_LIST = ['contradiction', 'entailment', 'neutral']
MAX_SEQ_LENGHT = 128
def _create_examples(df, set_type):
""" Convert raw dataframe to a list of InputExample. Filter malformed examples
"""
examples = []
for index, row in df.iterrows():
if row['gold_label'] not in LABEL_LIST:
continue
if not isinstance(row['sentence1'], str) or not isinstance(row['sentence2'], str):
continue
guid = f"{index}-{set_type}"
examples.append(
InputExample(guid=guid, text_a=row['sentence1'], text_b=row['sentence2'], label=row['gold_label']))
return examples
def _df_to_features(df, set_type):
""" Pre-process text. This method will:
1) tokenize inputs
2) cut or pad each sequence to MAX_SEQ_LENGHT
3) convert tokens into ids
The output will contain:
`input_ids` - padded token ids sequence
`attention mask` - mask indicating padded tokens
`token_type_ids` - mask indicating the split between premise and hypothesis
`label` - label
"""
examples = _create_examples(df, set_type)
#backward compatibility with older transformers versions
legacy_kwards = {}
from packaging import version
if version.parse(transformers.__version__) < version.parse("2.9.0"):
legacy_kwards = {
"pad_on_left": False,
"pad_token": tokenizer.convert_tokens_to_ids([tokenizer.pad_token])[0],
"pad_token_segment_id": 0,
}
return glue_convert_examples_to_features(
examples=examples,
tokenizer=tokenizer,
label_list=LABEL_LIST,
max_length=MAX_SEQ_LENGHT,
output_mode="classification",
**legacy_kwards,
)
def _features_to_dataset(features):
""" Convert features from `_df_to_features` into a single dataset
"""
all_input_ids = torch.tensor([f.input_ids for f in features], dtype=torch.long)
all_attention_mask = torch.tensor(
[f.attention_mask for f in features], dtype=torch.long
)
all_token_type_ids = torch.tensor(
[f.token_type_ids for f in features], dtype=torch.long
)
all_labels = torch.tensor([f.label for f in features], dtype=torch.long)
dataset = TensorDataset(
all_input_ids, all_attention_mask, all_token_type_ids, all_labels
)
return dataset
train_features = _df_to_features(df_train, "train")
test_features = _df_to_features(df_test, "test")
train_dataset = _features_to_dataset(train_features)
test_dataset = _features_to_dataset(test_features)
让我们聊聊批量大小。
除了在选择批量大小时通常考虑的所有因素外,使用 DP 训练模型还增加了一个因素——隐私成本。
由于我们假设的威胁模型以及我们向梯度添加噪声的方式,较大的批量大小(在一定范围内)通常有助于收敛。无论批量大小如何,我们都会向每次梯度更新添加相同量的噪声(按批次中一个样本的范数缩放)。这意味着随着批量大小的增加,添加的噪声相对量会减少,同时保持相同的 epsilon 保证。
然而,你应该记住,增加批量大小在 epsilon 方面是有代价的,随着训练的进行,epsilon 以 O(sqrt(batch_size)) 的速度增长(因此较大的批次会使其增长得更快)。这里的良好策略是尝试 batch_size 和 noise_multiplier(噪声倍数)的多种组合,以找到在可接受的隐私保证下提供最佳质量的组合。
这还有另一方面——内存。Opacus 计算并存储 每个样本(per sample) 的梯度,因此对于每个普通梯度,Opacus 在每一步都会存储 n=batch_size 个样本梯度,从而使内存占用至少增加 O(batch_size)。然而在现实中,与非隐私模型相比,峰值内存需求为 O(batch_size^2)。这是因为样本梯度计算中的某些中间步骤涉及对两个矩阵的操作,而每个矩阵都以 batch_size 作为其中一个维度。
好消息是,我们可以选择最合适的批量大小,而不受内存限制。Opacus 内置了对 虚拟(virtual) 批次的支持。使用它,我们可以分离物理步骤(梯度计算)和逻辑步骤(噪声添加和参数更新):在训练中使用较大的批次,同时保持较低的内存占用。下面我们将指定两个常量:
MAX_PHYSICAL_BATCH_SIZE:定义了从内存角度我们可以承受的最大批量大小,仅影响计算速度。BATCH_SIZE:另一方面,将仅影响收敛和隐私保证。BATCH_SIZE = 32
MAX_PHYSICAL_BATCH_SIZE = 8
from torch.utils.data import DataLoader, RandomSampler, SequentialSampler
from opacus.utils.uniform_sampler import UniformWithReplacementSampler
train_dataloader = DataLoader(train_dataset, batch_size=BATCH_SIZE)
test_dataloader = DataLoader(test_dataset, sampler=SequentialSampler(test_dataset), batch_size=BATCH_SIZE)
# Move the model to appropriate device
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = model.to(device)
# Set the model to train mode (HuggingFace models load in eval mode)
model = model.train()
# Define optimizer
optimizer = torch.optim.AdamW(model.parameters(), lr=5e-4, eps=1e-8)
首先,我们指定一些训练参数,准备运行三个轮次(epochs)的训练循环。
EPOCHS = 3
LOGGING_INTERVAL = 5000 # once every how many steps we run evaluation cycle and report metrics
EPSILON = 7.5
DELTA = 1 / len(train_dataloader) # Parameter for privacy accounting. Probability of not achieving privacy guarantees
现在让我们定义评估循环。
import numpy as np
from tqdm.notebook import tqdm
def accuracy(preds, labels):
return (preds == labels).mean()
# define evaluation cycle
def evaluate(model):
model.eval()
loss_arr = []
accuracy_arr = []
for batch in test_dataloader:
batch = tuple(t.to(device) for t in batch)
with torch.no_grad():
inputs = {'input_ids': batch[0],
'attention_mask': batch[1],
'token_type_ids': batch[2],
'labels': batch[3]}
outputs = model(**inputs)
loss, logits = outputs[:2]
preds = np.argmax(logits.detach().cpu().numpy(), axis=1)
labels = inputs['labels'].detach().cpu().numpy()
loss_arr.append(loss.item())
accuracy_arr.append(accuracy(preds, labels))
model.train()
return np.mean(loss_arr), np.mean(accuracy_arr)
接下来,我们将定义并附加 PrivacyEngine。这里有两个你需要考虑的参数:
noise_multiplier。它定义了隐私与准确度之间的权衡。添加更多噪声将提供更强的隐私保证,但也会损害模型质量。在本次运行中,PrivacyEngine 将根据 EPSILON、DELTA 和 EPOCHS 的目标值确定此值。对于默认设置,这将使 noise_multiplier 设置为约 0.4。max_grad_norm。定义了我们将样本梯度裁剪到的 L2 范数的最大量级。这个阈值存在一种博弈:一方面,低阈值意味着我们将裁剪许多梯度,从而损害收敛,因此我们可能倾向于提高它。但是,请记住我们添加的噪声标准差 std=noise_multiplier * max_grad_norm,因此我们将为增加的阈值付出更多噪声的代价。在大多数情况下,由于模型对裁剪具有相当的韧性(在最初的几次迭代后,模型往往会自行调整,使其梯度保持在裁剪阈值以下),因此你通常可以保留默认值(=1.0),而专注于调整 batch_size 和 noise_multiplier。尽管如此,有时裁剪确实会伤害模型,因此值得尝试不同的裁剪阈值,就像我们在本教程中所做的那样。这两个参数定义了我们向梯度添加的噪声尺度:噪声将从均值为 0、标准差 std=noise_multiplier * max_grad_norm 的高斯分布中采样。
from opacus import PrivacyEngine
MAX_GRAD_NORM = 0.1
privacy_engine = PrivacyEngine()
model, optimizer, train_dataloader = privacy_engine.make_private_with_epsilon(
module=model,
optimizer=optimizer,
data_loader=train_dataloader,
target_delta=DELTA,
target_epsilon=EPSILON,
epochs=EPOCHS,
max_grad_norm=MAX_GRAD_NORM,
)
现在我们可以训练模型了。
from opacus.utils.batch_memory_manager import BatchMemoryManager
for epoch in range(1, EPOCHS+1):
losses = []
with BatchMemoryManager(
data_loader=train_dataloader,
max_physical_batch_size=MAX_PHYSICAL_BATCH_SIZE,
optimizer=optimizer
) as memory_safe_data_loader:
for step, batch in enumerate(tqdm(memory_safe_data_loader)):
optimizer.zero_grad()
batch = tuple(t.to(device) for t in batch)
inputs = {'input_ids': batch[0],
'attention_mask': batch[1],
'token_type_ids': batch[2],
'labels': batch[3]}
outputs = model(**inputs) # output = loss, logits, hidden_states, attentions
loss = outputs[0]
loss.backward()
losses.append(loss.item())
optimizer.step()
if step > 0 and step % LOGGING_INTERVAL == 0:
train_loss = np.mean(losses)
eps = privacy_engine.get_epsilon(DELTA)
eval_loss, eval_accuracy = evaluate(model)
print(
f"Epoch: {epoch} | "
f"Step: {step} | "
f"Train loss: {train_loss:.3f} | "
f"Eval loss: {eval_loss:.3f} | "
f"Eval accuracy: {eval_accuracy:.3f} | "
f"ɛ: {eps:.2f}"
)
关于测试准确度,在训练三个轮次后,你应该期望得到接近以下结果。
你可以看到,在 epsilon=7.5 的情况下,我们可以实现相当强的隐私保证,与在类似设置(仅限高层)下训练的非隐私模型相比,准确度损失适中,为 11 个百分点;与我们使用相同架构能达到的最佳结果相比,损失 16 个百分点。
注:未指明时,DP-SGD 仅针对高层进行训练。
| 模型 | 噪声倍数 | 批量大小 | 准确度 | Epsilon |
|---|---|---|---|---|
| 无 DP,训练全模型 | 不适用 | 32 | 90.1% | 不适用 |
| 无 DP,仅训练高层 | 不适用 | 32 | 85.4% | 不适用 |
| DP-SGD | 1.0 | 32 | 70.5% | 0.7 |
| DP-SGD(本教程) | 0.4 | 32 | 74.3% | 7.5 |
| DP-SGD | 0.3 | 32 | 75.8% | 20.7 |
| DP-SGD | 0.1 | 32 | 78.3% | 2865 |
| DP-SGD | 0.4 | 8 | 67.3% | 5.9 |
在本节中,我们将展示如何使用快速梯度裁剪(Fast Gradient Clipping)和 Ghost Clipping DP-SGD。训练循环与 Opacus 中现有的循环几乎完全相同,后者基于(非隐私的)PyTorch 训练循环。要使用快速梯度裁剪,我们需要在 make_private 函数中传递 grad_sample_mode = 'ghost'。
另一个变化是,隐私引擎的 make_private 函数也将损失准则(loss criterion)作为输入并进行处理。这允许我们重新利用 loss.backward 来执行两次反向传播,并在中间进行损失缩放。第一次反向传播计算每个样本的梯度范数,而对缩放后的损失进行的第二次反向传播则计算聚合后的裁剪梯度。
# Let's import the model again and freeze layers as before
model = BertForSequenceClassification.from_pretrained(
"bert-base-cased",
config=config,
)
trainable_layers = [model.bert.encoder.layer[-1], model.bert.pooler, model.classifier]
for p in model.parameters():
p.requires_grad = False
total_params += p.numel()
for layer in trainable_layers:
for p in layer.parameters():
p.requires_grad = True
trainable_params += p.numel()
Some weights of BertForSequenceClassification were not initialized from the model checkpoint at bert-base-cased and are newly initialized: ['classifier.bias', 'classifier.weight'] You should probably TRAIN this model on a down-stream task to be able to use it for predictions and inference.
device = torch.device("cuda:0")
os.environ["CUDA_LAUNCH_BLOCKING"] = "1"
optimizer = torch.optim.AdamW(model.parameters(), lr=5e-4, eps=1e-8)
model = model.train()
privacy_engine = PrivacyEngine()
criterion = nn.CrossEntropyLoss(reduction="mean")
model_gc, optimizer_gc, criterion_gc, train_dataloader = (
privacy_engine.make_private_with_epsilon(
module=model,
optimizer=optimizer,
data_loader=train_dataloader,
criterion=criterion,
target_delta=DELTA,
target_epsilon=EPSILON,
epochs=EPOCHS,
max_grad_norm=MAX_GRAD_NORM,
grad_sample_mode="ghost",
)
)
model_gc = model_gc.to(device)
model_gc = model_gc.train()
for epoch in range(1, EPOCHS + 1):
losses = []
for step, batch in enumerate(tqdm(train_dataloader)):
optimizer_gc.zero_grad()
batch = tuple(t.to(device) for t in batch)
inputs = {
"input_ids": batch[0],
"attention_mask": batch[1],
"token_type_ids": batch[2],
"labels": batch[3],
}
outputs = model_gc(**inputs) # output = loss, logits, hidden_states, attentions
loss = criterion_gc(outputs[1], batch[3])
loss.backward()
optimizer_gc.step()
losses.append(loss.item())
if step > 0 and step % LOGGING_INTERVAL == 0:
train_loss = np.mean(losses)
eval_loss, eval_accuracy = evaluate(model_gc)
eps = privacy_engine.get_epsilon(DELTA)
print(
f"Epoch: {epoch} | "
f"Step: {step} | "
f"Train loss: {train_loss:.3f} | "
f"Eval loss: {eval_loss:.3f} | "
f"Eval accuracy: {eval_accuracy:.3f} | "
f"ɛ: {eps:.2f}"
)
Epoch: 1 | Step: 5000 | Train loss: 1.559 | Eval loss: 1.508 | Eval accuracy: 0.683 | ɛ: 4.83
Epoch: 1 | Step: 10000 | Train loss: 1.625 | Eval loss: 1.635 | Eval accuracy: 0.723 | ɛ: 5.46
Epoch: 1 | Step: 15000 | Train loss: 1.655 | Eval loss: 1.649 | Eval accuracy: 0.735 | ɛ: 5.86
Epoch: 2 | Step: 5000 | Train loss: 1.742 | Eval loss: 1.676 | Eval accuracy: 0.739 | ɛ: 6.29
Epoch: 2 | Step: 10000 | Train loss: 1.746 | Eval loss: 1.681 | Eval accuracy: 0.743 | ɛ: 6.54
Epoch: 2 | Step: 15000 | Train loss: 1.759 | Eval loss: 1.683 | Eval accuracy: 0.745 | ɛ: 6.76
Epoch: 3 | Step: 5000 | Train loss: 1.784 | Eval loss: 1.769 | Eval accuracy: 0.745 | ɛ: 7.05
Epoch: 3 | Step: 10000 | Train loss: 1.789 | Eval loss: 1.695 | Eval accuracy: 0.748 | ɛ: 7.24
Epoch: 3 | Step: 15000 | Train loss: 1.792 | Eval loss: 1.714 | Eval accuracy: 0.749 | ɛ: 7.42
# reset the model
model = BertForSequenceClassification.from_pretrained(
"bert-base-cased",
config=config,
)
Some weights of BertForSequenceClassification were not initialized from the model checkpoint at bert-base-cased and are newly initialized: ['classifier.bias', 'classifier.weight'] You should probably TRAIN this model on a down-stream task to be able to use it for predictions and inference.
回想一下,模型参数总数约为 1.08 亿。
total_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
print(f"Total parameters count: {total_params:,}") # ~108M
Total parameters count: 108,312,579
启用 LoRA 后,可训练参数总数减少了 100 倍,降至约 100 万。
请注意使用 LoRA 时的一些关键超参数,例如分解矩阵的秩 $r$。这些参数可能需要调优。
from peft import get_peft_model, LoraConfig, TaskType
lora_config = LoraConfig(
task_type=TaskType.SEQ_CLS, # our particular task is sequence classification
inference_mode=False, # Enable training mode
r=32, # Low-rank dimension
lora_alpha=32, # Alpha scaling factor
lora_dropout=0.05, # Dropout for LoRA layers
)
model_with_lora = get_peft_model(model, lora_config)
trainable_params = sum(p.numel() for p in model_with_lora.parameters() if p.requires_grad)
print(f"Total trainable parameters with LoRA: {trainable_params:,}") # ~1M
Total trainable parameters with LoRA: 1,181,955
与之前类似,我们将冻结除模型最后一个注意力层之外的所有层。这进一步将可训练参数的数量减少到约 10 万,而没有 LoRA 时约为 700 万。
attention_layers_to_freeze = model_with_lora.base_model.bert.encoder.layer[:-1]
# Freeze the parameters in the first 11 attention layers
for param in attention_layers_to_freeze.parameters():
param.requires_grad = False
trainable_params = sum(p.numel() for p in model_with_lora.parameters() if p.requires_grad)
print(f"Total trainable parameters with LoRA after freezing: {trainable_params:,}") # ~1M
Total trainable parameters with LoRA after freezing: 100,611
现在我们已经完成了 LoRA 设置的模型准备,接下来使用 DP-SGD 进行训练就如往常一样了。我们使用带有 Ghost Clipping 的 DP-SGD。
EPOCHS = 3
LOGGING_INTERVAL = 5000 # once every how many steps we run evaluation cycle and report metrics
DELTA = 1 / len(train_dataloader) # Parameter for privacy accounting. Probability of not achieving privacy guarantees
MAX_GRAD_NORM = 0.1
device = torch.device("cuda:0")
os.environ["CUDA_LAUNCH_BLOCKING"] = "1"
optimizer = torch.optim.AdamW(model_with_lora.parameters(), lr=5e-4, eps=1e-8)
model_with_lora = model_with_lora.train()
privacy_engine = PrivacyEngine()
criterion = nn.CrossEntropyLoss(reduction="mean")
model_lora, optimizer_lora, criterion_lora, train_dataloader = (
privacy_engine.make_private_with_epsilon(
module=model_with_lora,
optimizer=optimizer,
data_loader=train_dataloader,
criterion=criterion,
target_delta=DELTA,
target_epsilon=EPSILON,
epochs=EPOCHS,
max_grad_norm=MAX_GRAD_NORM,
grad_sample_mode="ghost",
)
)
model_lora = model_lora.to(device)
model_lora = model_lora.train()
for epoch in range(1, EPOCHS + 1):
losses = []
for step, batch in enumerate(tqdm(train_dataloader)):
optimizer_lora.zero_grad()
batch = tuple(t.to(device) for t in batch)
inputs = {
"input_ids": batch[0],
"attention_mask": batch[1],
"token_type_ids": batch[2],
"labels": batch[3],
}
outputs = model_lora(**inputs) # output = loss, logits, hidden_states, attentions
loss = criterion_lora(outputs[1], batch[3])
loss.backward()
optimizer_lora.step()
losses.append(loss.item())
if step > 0 and step % LOGGING_INTERVAL == 0:
train_loss = np.mean(losses)
eval_loss, eval_accuracy = evaluate(model_lora)
eps = privacy_engine.get_epsilon(DELTA)
print(
f"Epoch: {epoch} | "
f"Step: {step} | "
f"Train loss: {train_loss:.3f} | "
f"Eval loss: {eval_loss:.3f} | "
f"Eval accuracy: {eval_accuracy:.3f} | "
f"ɛ: {eps:.2f}"
)
Epoch: 1 | Step: 5000 | Train loss: 1.370 | Eval loss: 1.208 | Eval accuracy: 0.512 | ɛ: 4.83
Epoch: 1 | Step: 10000 | Train loss: 1.551 | Eval loss: 1.584 | Eval accuracy: 0.621 | ɛ: 5.46
Epoch: 1 | Step: 15000 | Train loss: 1.601 | Eval loss: 1.523 | Eval accuracy: 0.661 | ɛ: 5.86
Epoch: 2 | Step: 5000 | Train loss: 1.713 | Eval loss: 1.556 | Eval accuracy: 0.708 | ɛ: 6.29
Epoch: 2 | Step: 10000 | Train loss: 1.720 | Eval loss: 1.573 | Eval accuracy: 0.712 | ɛ: 6.54
Epoch: 2 | Step: 15000 | Train loss: 1.723 | Eval loss: 1.510 | Eval accuracy: 0.725 | ɛ: 6.76
Epoch: 3 | Step: 5000 | Train loss: 1.704 | Eval loss: 1.492 | Eval accuracy: 0.735 | ɛ: 7.05
Epoch: 3 | Step: 10000 | Train loss: 1.705 | Eval loss: 1.535 | Eval accuracy: 0.734 | ɛ: 7.24
Epoch: 3 | Step: 15000 | Train loss: 1.704 | Eval loss: 1.492 | Eval accuracy: 0.740 | ɛ: 7.42
我们在训练参数减少约 100 倍的情况下,实现了与 Ghost Clipping(以及普通 DP-SGD)相当的准确度。
请注意,DP 训练和非 DP 训练之间存在显著的模型准确度差距,约为 15 个百分点。
这一差距可以通过以下方式进一步缩小:
我们邀请您亲自尝试并进一步提高 DP-SGD 训练的准确度。