Opacus
  • 简介
  • 常见问题
  • 教程
  • API 参考
  • GitHub

›

教程

  • 概览

使用 Opacus

  • 使用快速梯度裁剪 DP-SGD 构建文本分类器
  • 使用差分隐私构建图像分类器
  • 训练用于姓名分类的差分隐私 LSTM 模型
  • 深入了解 Opacus 的高级功能
  • 模块验证器 (Module Validator) 和修复器 (Fixer) 指南
  • 梯度采样器 (Grad Samplers) 指南
  • 使用 DistributedDataParallel 进行多 GPU 训练

训练用于姓名分类的差分隐私 LSTM 模型¶

在本教程中,我们将构建一个差分隐私 LSTM 模型,将姓名分类为其源语言,这与 NLP From Scratch 教程 (https://pytorch.ac.cn/tutorials/intermediate/char_rnn_classification_tutorial.html) 中的任务相同。由于本教程的目标是演示在具有隐私保证的情况下如何有效使用 LSTM,因此我们将使用它来代替原始教程中定义的裸机 RNN 模型。具体来说,我们使用 opacus.layers.dp_lstm 中的 DPLSTM 模块来辅助计算样本级梯度(per-example gradients),这些梯度在应用差分隐私期间添加噪声时会被使用。DPLSTM 具有与 nn.LSTM 相同的 API 和功能,但有一些限制(例如:我们目前支持单层,完整列表如下所示)。

数据集¶

首先,让我们按照 https://pytorch.ac.cn/tutorials/intermediate/char_rnn_classification_tutorial.html 中提供的内容下载姓名及其关联语言标签的数据集。我们在该教程中使用的相同数据集上训练我们的差分隐私 LSTM。

输入 [1]
import warnings
warnings.simplefilter("ignore")

import os
import requests


NAMES_DATASET_URL = "https://download.pytorch.org/tutorial/data.zip"
DATA_DIR = "names"

import zipfile
import urllib

def download_and_extract(dataset_url, data_dir):
    print("Downloading and extracting ...")
    filename = "data.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(NAMES_DATASET_URL, DATA_DIR)
Downloading and extracting ...
Completed!
输入 [2]
names_folder = os.path.join(DATA_DIR, 'data', 'names')
all_filenames = []

for language_file in os.listdir(names_folder):
    all_filenames.append(os.path.join(names_folder, language_file))
    
print(os.listdir(names_folder))
['Italian.txt', 'Arabic.txt', 'English.txt', 'German.txt', 'French.txt', 'Spanish.txt', 'Greek.txt', 'Dutch.txt', 'Korean.txt', 'Portuguese.txt', 'Japanese.txt', 'Polish.txt', 'Irish.txt', 'Chinese.txt', 'Russian.txt', 'Czech.txt', 'Vietnamese.txt', 'Scottish.txt']
输入 [3]
import torch
import torch.nn as nn

class CharByteEncoder(nn.Module):
    """
    This encoder takes a UTF-8 string and encodes its bytes into a Tensor. It can also
    perform the opposite operation to check a result.
    Examples:
    >>> encoder = CharByteEncoder()
    >>> t = encoder('Ślusàrski')  # returns tensor([256, 197, 154, 108, 117, 115, 195, 160, 114, 115, 107, 105, 257])
    >>> encoder.decode(t)  # returns "<s>Ślusàrski</s>"
    """

    def __init__(self):
        super().__init__()
        self.start_token = "<s>"
        self.end_token = "</s>"
        self.pad_token = "<pad>"

        self.start_idx = 256
        self.end_idx = 257
        self.pad_idx = 258

    def forward(self, s: str, pad_to=0) -> torch.LongTensor:
        """
        Encodes a string. It will append a start token <s> (id=self.start_idx) and an end token </s>
        (id=self.end_idx).
        Args:
            s: The string to encode.
            pad_to: If not zero, pad by appending self.pad_idx until string is of length `pad_to`.
                Defaults to 0.
        Returns:
            The encoded LongTensor of indices.
        """
        encoded = s.encode()
        n_pad = pad_to - len(encoded) if pad_to > len(encoded) else 0
        return torch.LongTensor(
            [self.start_idx]
            + [c for c in encoded]  # noqa
            + [self.end_idx]
            + [self.pad_idx for _ in range(n_pad)]
        )

    def decode(self, char_ids_tensor: torch.LongTensor) -> str:
        """
        The inverse of `forward`. Keeps the start, end, and pad indices.
        """
        char_ids = char_ids_tensor.cpu().detach().tolist()

        out = []
        buf = []
        for c in char_ids:
            if c < 256:
                buf.append(c)
            else:
                if buf:
                    out.append(bytes(buf).decode())
                    buf = []
                if c == self.start_idx:
                    out.append(self.start_token)
                elif c == self.end_idx:
                    out.append(self.end_token)
                elif c == self.pad_idx:
                    out.append(self.pad_token)

        if buf:  # in case some are left
            out.append(bytes(buf).decode())
        return "".join(out)

    def __len__(self):
        """
        The length of our encoder space. This is fixed to 256 (one byte) + 3 special chars
        (start, end, pad).
        Returns:
            259
        """
        return 259

训练集 / 验证集准备¶

输入 [4]
from torch.nn.utils.rnn import pad_sequence

def padded_collate(batch, padding_idx=0):
    x = pad_sequence(
        [elem[0] for elem in batch], batch_first=True, padding_value=padding_idx
    )
    y = torch.stack([elem[1] for elem in batch]).long()

    return x, y
输入 [5]
from torch.utils.data import Dataset
from pathlib import Path


class NamesDataset(Dataset):
    def __init__(self, root):
        self.root = Path(root)

        self.labels = list({langfile.stem for langfile in self.root.iterdir()})
        self.labels_dict = {label: i for i, label in enumerate(self.labels)}
        self.encoder = CharByteEncoder()
        self.samples = self.construct_samples()

    def __getitem__(self, i):
        return self.samples[i]

    def __len__(self):
        return len(self.samples)

    def construct_samples(self):
        samples = []
        for langfile in self.root.iterdir():
            label_name = langfile.stem
            label_id = self.labels_dict[label_name]
            with open(langfile, "r") as fin:
                for row in fin:
                    samples.append(
                        (self.encoder(row.strip()), torch.tensor(label_id).long())
                    )
        return samples

    def label_count(self):
        cnt = Counter()
        for _x, y in self.samples:
            label = self.labels[int(y)]
            cnt[label] += 1
        return cnt


VOCAB_SIZE = 256 + 3  # 256 alternatives in one byte, plus 3 special characters.

我们将数据集按 80-20 的比例拆分为训练集和验证集。

输入 [6]
secure_mode = False
train_split = 0.8
test_every = 5
batch_size = 800

ds = NamesDataset(names_folder)
train_len = int(train_split * len(ds))
test_len = len(ds) - train_len

print(f"{train_len} samples for training, {test_len} for testing")

train_ds, test_ds = torch.utils.data.random_split(ds, [train_len, test_len])
16059 samples for training, 4015 for testing
输入 [7]
from torch.utils.data import DataLoader

train_loader = DataLoader(
    train_ds,
    batch_size=batch_size,
    pin_memory=True,
    collate_fn=padded_collate,
)

test_loader = DataLoader(
    test_ds,
    batch_size=2 * batch_size,
    shuffle=False,
    pin_memory=True,
    collate_fn=padded_collate,
)

将数据集拆分为训练集和验证集后,我们现在必须将数据转换为适合训练 LSTM 模型的数值形式。对于每个姓名,我们设置最大序列长度为 15,如果姓名超过该阈值,我们会对其进行截断(在此数据集中很少发生!)。如果姓名小于阈值,我们会添加一个虚拟的 # 字符来填充到所需长度。我们还对数据集中的姓名进行分批,并在本教程的所有实验中设置批大小(batch size)为 256。函数 line_to_tensor() 返回一个形状为 [15, 256] 的张量,其中每个元素是对应字符(在 all_letters 中)的索引。

训练/评估周期¶

训练和评估函数 train() 和 test() 定义如下。在训练循环期间,将计算样本级梯度,并在梯度裁剪(以限制其敏感度)和添加噪声后更新参数。

输入 [8]
from statistics import mean

def train(model, criterion, optimizer, train_loader, epoch, privacy_engine, device="cuda:0"):
    accs = []
    losses = []
    for x, y in train_loader:
        x = x.to(device)
        y = y.to(device)

        logits = model(x)
        loss = criterion(logits, y)
        loss.backward()

        optimizer.step()
        optimizer.zero_grad()

        preds = logits.argmax(-1)
        n_correct = float(preds.eq(y).sum())
        batch_accuracy = n_correct / len(y)

        accs.append(batch_accuracy)
        losses.append(float(loss))

    printstr = (
        f"\t Epoch {epoch}. Accuracy: {mean(accs):.6f} | Loss: {mean(losses):.6f}"
    )
    if privacy_engine:
        epsilon = privacy_engine.get_epsilon(delta)
        printstr += f" | (ε = {epsilon:.2f}, δ = {delta})"

    print(printstr)
    return


def test(model, test_loader, privacy_engine, device="cuda:0"):
    accs = []
    with torch.no_grad():
        for x, y in test_loader:
            x = x.to(device)
            y = y.to(device)

            preds = model(x).argmax(-1)
            n_correct = float(preds.eq(y).sum())
            batch_accuracy = n_correct / len(y)

            accs.append(batch_accuracy)
    printstr = "\n----------------------------\n" f"Test Accuracy: {mean(accs):.6f}"
    if privacy_engine:
        epsilon = privacy_engine.get_epsilon(delta)
        printstr += f" (ε = {epsilon:.2f}, δ = {delta})"
    print(printstr + "\n----------------------------\n")
    return

超参数¶

此模型关联有两组超参数。第一组是我们在任何机器学习训练中都会看到的超参数,如学习率和批大小。第二组与隐私引擎(privacy engine)相关,例如,我们定义了添加到梯度的噪声量 (noise_multiplier),以及样本级梯度被裁剪到的最大 L2 范数 (max_grad_norm)。

输入 [9]
# Training hyper-parameters
epochs = 50
learning_rate = 2.0

# Privacy engine hyper-parameters
max_per_sample_grad_norm = 1.5
delta = 8e-5
epsilon = 12.0

模型¶

我们在下面的单元格中定义姓名分类模型。请注意,它是一个简单的字符级 LSTM 分类器,其中输入字符通过 nn.Embedding 层,随后输入到 DPLSTM。

输入 [10]
import torch
from torch import nn
from opacus.layers import DPLSTM

class CharNNClassifier(nn.Module):
    def __init__(
        self,
        embedding_size,
        hidden_size,
        output_size,
        num_lstm_layers=1,
        bidirectional=False,
        vocab_size=VOCAB_SIZE,
    ):
        super().__init__()

        self.embedding_size = embedding_size
        self.hidden_size = hidden_size
        self.output_size = output_size
        self.vocab_size = vocab_size

        self.embedding = nn.Embedding(vocab_size, embedding_size)
        self.lstm = DPLSTM(
            embedding_size,
            hidden_size,
            num_layers=num_lstm_layers,
            bidirectional=bidirectional,
            batch_first=True,
        )
        self.out_layer = nn.Linear(hidden_size, output_size)

    def forward(self, x, hidden=None):
        x = self.embedding(x)  # -> [B, T, D]
        x, _ = self.lstm(x, hidden)  # -> [B, T, H]
        x = x[:, -1, :]  # -> [B, H]
        x = self.out_layer(x)  # -> [B, C]
        return x

我们现在开始为我们的差分隐私 LSTM 训练实例化对象(隐私引擎、模型和优化器)。但是,nn.LSTM 被替换为 DPLSTM 模块,这使我们能够计算样本级梯度。

输入 [11]
# Set the device to run on a GPU
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

# Define classifier parameters
embedding_size = 64
hidden_size = 128  # Number of neurons in hidden layer after LSTM
n_lstm_layers = 1
bidirectional_lstm = False

model = CharNNClassifier(
    embedding_size,
    hidden_size,
    len(ds.labels),
    n_lstm_layers,
    bidirectional_lstm,
).to(device)

为问题定义隐私引擎、优化器和损失标准¶

输入 [12]
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate)
输入 [13]
from opacus import PrivacyEngine
privacy_engine = PrivacyEngine(secure_mode=secure_mode)

model, optimizer, train_loader = privacy_engine.make_private_with_epsilon(
    module=model,
    optimizer=optimizer,
    data_loader=train_loader,
    max_grad_norm=max_per_sample_grad_norm,
    target_delta=delta,
    target_epsilon=epsilon,
    epochs=epochs,
)

在隐私保护下训练姓名分类器¶

最后,我们可以开始训练了!我们将进行 50 个 epoch 的迭代(每个 epoch 对应对整个数据集的一次遍历)。我们将每隔 test_every 个 epoch 报告一次隐私 epsilon。我们还将把这个差分隐私模型与没有隐私的模型进行基准测试,并获得几乎相同的性能。此外,使用 Opacus 训练的隐私模型在训练时间上的开销极小,差分隐私分类器仅比非隐私模型稍慢(慢几分钟)。

输入 [14]
print("Train stats: \n")
for epoch in range(epochs):
    train(model, criterion, optimizer, train_loader, epoch, privacy_engine, device=device)
    if test_every:
        if epoch % test_every == 0:
            test(model, test_loader, privacy_engine, device=device)

test(model, test_loader, privacy_engine, device=device)
Train stats: 

	 Epoch 0. Accuracy: 0.428835 | Loss: 2.220773

----------------------------
Test Accuracy: 0.469154 (ε = 2.30, δ = 8e-05)
----------------------------

	 Epoch 1. Accuracy: 0.472534 | Loss: 1.895850
	 Epoch 2. Accuracy: 0.471778 | Loss: 1.893783
	 Epoch 3. Accuracy: 0.459604 | Loss: 1.958717
	 Epoch 4. Accuracy: 0.491896 | Loss: 1.782331
	 Epoch 5. Accuracy: 0.540205 | Loss: 1.577036

----------------------------
Test Accuracy: 0.559490 (ε = 4.16, δ = 8e-05)
----------------------------

	 Epoch 6. Accuracy: 0.593796 | Loss: 1.456133
	 Epoch 7. Accuracy: 0.616827 | Loss: 1.388250
	 Epoch 8. Accuracy: 0.632560 | Loss: 1.345773
	 Epoch 9. Accuracy: 0.639074 | Loss: 1.327238
	 Epoch 10. Accuracy: 0.650502 | Loss: 1.316831

----------------------------
Test Accuracy: 0.650821 (ε = 5.43, δ = 8e-05)
----------------------------

	 Epoch 11. Accuracy: 0.649294 | Loss: 1.315323
	 Epoch 12. Accuracy: 0.656350 | Loss: 1.288794
	 Epoch 13. Accuracy: 0.656104 | Loss: 1.285352
	 Epoch 14. Accuracy: 0.656424 | Loss: 1.283710
	 Epoch 15. Accuracy: 0.666633 | Loss: 1.273102

----------------------------
Test Accuracy: 0.667164 (ε = 6.51, δ = 8e-05)
----------------------------

	 Epoch 16. Accuracy: 0.672707 | Loss: 1.247125
	 Epoch 17. Accuracy: 0.680121 | Loss: 1.223817
	 Epoch 18. Accuracy: 0.686456 | Loss: 1.214923
	 Epoch 19. Accuracy: 0.694982 | Loss: 1.193048
	 Epoch 20. Accuracy: 0.694282 | Loss: 1.184953

----------------------------
Test Accuracy: 0.682519 (ε = 7.46, δ = 8e-05)
----------------------------

	 Epoch 21. Accuracy: 0.701802 | Loss: 1.161172
	 Epoch 22. Accuracy: 0.706358 | Loss: 1.166274
	 Epoch 23. Accuracy: 0.722667 | Loss: 1.097268
	 Epoch 24. Accuracy: 0.703950 | Loss: 1.185700
	 Epoch 25. Accuracy: 0.720196 | Loss: 1.112226

----------------------------
Test Accuracy: 0.707127 (ε = 8.33, δ = 8e-05)
----------------------------

	 Epoch 26. Accuracy: 0.720644 | Loss: 1.115221
	 Epoch 27. Accuracy: 0.708652 | Loss: 1.158104
	 Epoch 28. Accuracy: 0.724744 | Loss: 1.119688
	 Epoch 29. Accuracy: 0.733490 | Loss: 1.088846
	 Epoch 30. Accuracy: 0.729441 | Loss: 1.089938

----------------------------
Test Accuracy: 0.701941 (ε = 9.15, δ = 8e-05)
----------------------------

	 Epoch 31. Accuracy: 0.731014 | Loss: 1.096586
	 Epoch 32. Accuracy: 0.736907 | Loss: 1.065786
	 Epoch 33. Accuracy: 0.733743 | Loss: 1.098627
	 Epoch 34. Accuracy: 0.741741 | Loss: 1.064197
	 Epoch 35. Accuracy: 0.742394 | Loss: 1.053995

----------------------------
Test Accuracy: 0.720777 (ε = 9.93, δ = 8e-05)
----------------------------

	 Epoch 36. Accuracy: 0.749420 | Loss: 1.034596
	 Epoch 37. Accuracy: 0.748662 | Loss: 1.037211
	 Epoch 38. Accuracy: 0.745869 | Loss: 1.061525
	 Epoch 39. Accuracy: 0.751734 | Loss: 1.022538
	 Epoch 40. Accuracy: 0.751194 | Loss: 1.028292

----------------------------
Test Accuracy: 0.744636 (ε = 10.67, δ = 8e-05)
----------------------------

	 Epoch 41. Accuracy: 0.754300 | Loss: 1.032082
	 Epoch 42. Accuracy: 0.753252 | Loss: 1.017024
	 Epoch 43. Accuracy: 0.755629 | Loss: 1.035767
	 Epoch 44. Accuracy: 0.758195 | Loss: 1.029165
	 Epoch 45. Accuracy: 0.751091 | Loss: 1.028669

----------------------------
Test Accuracy: 0.739427 (ε = 11.38, δ = 8e-05)
----------------------------

	 Epoch 46. Accuracy: 0.760692 | Loss: 0.995788
	 Epoch 47. Accuracy: 0.763821 | Loss: 0.990309
	 Epoch 48. Accuracy: 0.763423 | Loss: 0.997126
	 Epoch 49. Accuracy: 0.767976 | Loss: 0.982944

----------------------------
Test Accuracy: 0.752090 (ε = 11.93, δ = 8e-05)
----------------------------

差分隐私姓名分类模型在 epsilon 略低于 12 的情况下获得了 0.75 的测试准确率。这表明我们可以在此任务上实现良好的准确率,同时将隐私损失降至最低。

在没有隐私的情况下训练姓名分类器¶

我们还运行了与非隐私模型的对比,看看在隐私保护下获得的性能是否与其相当。为此,我们保持学习率和批大小等参数相同,仅定义一个不同的模型实例以及一个单独的优化器。

输入 [15]
model_nodp = CharNNClassifier(
    embedding_size,
    hidden_size,
    len(ds.labels),
    n_lstm_layers,
    bidirectional_lstm,
).to(device)


optimizer_nodp = torch.optim.SGD(model_nodp.parameters(), lr=0.5)
输入 [16]
for epoch in range(epochs):
    train(model_nodp, criterion, optimizer_nodp, train_loader, epoch, device=device)
    if test_every:
        if epoch % test_every == 0:
            test(model_nodp, test_loader, None, device=device)

test(model_nodp, test_loader, None, device=device)
	 Epoch 0. Accuracy: 0.423231 | Loss: 1.957621

----------------------------
Test Accuracy: 0.469154
----------------------------

	 Epoch 1. Accuracy: 0.470835 | Loss: 1.850998
	 Epoch 2. Accuracy: 0.461741 | Loss: 1.845881
	 Epoch 3. Accuracy: 0.466039 | Loss: 1.848411
	 Epoch 4. Accuracy: 0.470612 | Loss: 1.857506
	 Epoch 5. Accuracy: 0.460152 | Loss: 1.845789

----------------------------
Test Accuracy: 0.469154
----------------------------

	 Epoch 6. Accuracy: 0.477714 | Loss: 1.775618
	 Epoch 7. Accuracy: 0.518488 | Loss: 1.622382
	 Epoch 8. Accuracy: 0.535421 | Loss: 1.565642
	 Epoch 9. Accuracy: 0.545521 | Loss: 1.511846
	 Epoch 10. Accuracy: 0.543908 | Loss: 1.514014

----------------------------
Test Accuracy: 0.575170
----------------------------

	 Epoch 11. Accuracy: 0.561950 | Loss: 1.454853
	 Epoch 12. Accuracy: 0.605502 | Loss: 1.388555
	 Epoch 13. Accuracy: 0.607155 | Loss: 1.367188
	 Epoch 14. Accuracy: 0.615066 | Loss: 1.346803
	 Epoch 15. Accuracy: 0.621913 | Loss: 1.332553

----------------------------
Test Accuracy: 0.635465
----------------------------

	 Epoch 16. Accuracy: 0.619772 | Loss: 1.314691
	 Epoch 17. Accuracy: 0.629337 | Loss: 1.302999
	 Epoch 18. Accuracy: 0.634173 | Loss: 1.277790
	 Epoch 19. Accuracy: 0.647275 | Loss: 1.226866
	 Epoch 20. Accuracy: 0.652142 | Loss: 1.226686

----------------------------
Test Accuracy: 0.651832
----------------------------

	 Epoch 21. Accuracy: 0.646773 | Loss: 1.219855
	 Epoch 22. Accuracy: 0.663006 | Loss: 1.195204
	 Epoch 23. Accuracy: 0.670526 | Loss: 1.165726
	 Epoch 24. Accuracy: 0.676121 | Loss: 1.148621
	 Epoch 25. Accuracy: 0.687536 | Loss: 1.109896

----------------------------
Test Accuracy: 0.690590
----------------------------

	 Epoch 26. Accuracy: 0.690961 | Loss: 1.110705
	 Epoch 27. Accuracy: 0.674958 | Loss: 1.158181
	 Epoch 28. Accuracy: 0.696233 | Loss: 1.091395
	 Epoch 29. Accuracy: 0.699146 | Loss: 1.077446
	 Epoch 30. Accuracy: 0.710076 | Loss: 1.061827

----------------------------
Test Accuracy: 0.716664
----------------------------

	 Epoch 31. Accuracy: 0.714624 | Loss: 1.040824
	 Epoch 32. Accuracy: 0.709445 | Loss: 1.044048
	 Epoch 33. Accuracy: 0.719751 | Loss: 1.021937
	 Epoch 34. Accuracy: 0.722247 | Loss: 1.002287
	 Epoch 35. Accuracy: 0.725602 | Loss: 0.985023

----------------------------
Test Accuracy: 0.717073
----------------------------

	 Epoch 36. Accuracy: 0.721840 | Loss: 0.990956
	 Epoch 37. Accuracy: 0.726419 | Loss: 0.978770
	 Epoch 38. Accuracy: 0.730414 | Loss: 0.945205
	 Epoch 39. Accuracy: 0.733045 | Loss: 0.931660
	 Epoch 40. Accuracy: 0.743858 | Loss: 0.914782

----------------------------
Test Accuracy: 0.724982
----------------------------

	 Epoch 41. Accuracy: 0.751916 | Loss: 0.876523
	 Epoch 42. Accuracy: 0.737594 | Loss: 0.914662
	 Epoch 43. Accuracy: 0.735986 | Loss: 0.923208
	 Epoch 44. Accuracy: 0.752869 | Loss: 0.868417
	 Epoch 45. Accuracy: 0.753095 | Loss: 0.867506

----------------------------
Test Accuracy: 0.740716
----------------------------

	 Epoch 46. Accuracy: 0.755373 | Loss: 0.851085
	 Epoch 47. Accuracy: 0.755981 | Loss: 0.842593
	 Epoch 48. Accuracy: 0.768917 | Loss: 0.813079
	 Epoch 49. Accuracy: 0.761222 | Loss: 0.829013

----------------------------
Test Accuracy: 0.754173
----------------------------

我们再次运行训练循环,这次不带隐私保护,并运行相同的迭代次数。

在相同的参数和 epoch 数下,非隐私分类器获得了约 0.75 的测试准确率。我们实际上是在姓名分类任务的性能与较低的隐私损失之间进行权衡。

下载教程 Jupyter Notebook
Opacus
文档
简介常见问题教程API 参考
Github
opacus
法律
隐私政策条款
Meta Open Source
Copyright © 2025 Meta Platforms, Inc.