DP-SGDにおけるノイズの大きさの影響

はじめに

DP-SGDで教師データのプライバシーを保護しつつ、高いテスト精度を実現することを目指します。
学習は以前の記事をベースにしています。
Jupyter Notebookは下記にあります。

概要

  • ResNet18によるCIFAR-10の学習に、Opacusを用いてDP-SGDを適用する。

  • DP-SGDの学習における$${\sigma}$$と$${C}$$の影響を確認する。

参考資料

実装

1. ライブラリのインポート

必要なライブラリをインポートします。

import torch
import torchvision
from torch.utils.data import DataLoader
import sys
from tqdm import tqdm
import matplotlib.pyplot as plt
import numpy as np
from sklearn import metrics

!pip install 'opacus>=1.0'
import opacus
from opacus.validators import ModuleValidator
from opacus.utils.batch_memory_manager import BatchMemoryManager

import warnings
warnings.simplefilter("ignore")

2. 実行環境の確認

使用するライブラリのバージョンや、GPU環境を確認します。
Opacusは1.0.0で書き方が大きく変わっているので注意してください。
Google Colaboratoryで実行した際の例になります。

print('Python:', sys.version)
print('PyTorch:', torch.__version__)
print('Torchvision:', torchvision.__version__)
print('Opacus:', opacus.__version__)
!nvidia-smi
Python: 3.9.16 (main, Dec  7 2022, 01:11:51) 
[GCC 9.4.0]
PyTorch: 2.0.0+cu118
Torchvision: 0.15.1+cu118
Opacus: 1.4.0
Tue Apr  4 09:00:28 2023       
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 525.85.12    Driver Version: 525.85.12    CUDA Version: 12.0     |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|                               |                      |               MIG M. |
|===============================+======================+======================|
|   0  Tesla T4            Off  | 00000000:00:04.0 Off |                    0 |
| N/A   66C    P8    11W /  70W |      0MiB / 15360MiB |      0%      Default |
|                               |                      |                  N/A |
+-------------------------------+----------------------+----------------------+
                                                                               
+-----------------------------------------------------------------------------+
| Processes:                                                                  |
|  GPU   GI   CI        PID   Type   Process name                  GPU Memory |
|        ID   ID                                                   Usage      |
|=============================================================================|
|  No running processes found                                                 |
+-----------------------------------------------------------------------------+

3. データセットの用意

データセットの取得

カラー画像データセットのCIFAR-10を、torchvisionを用いて取得します。
以前の結果に基づいて、教師データにdata augmentationは適用せずに、前処理は下記のみを適用します。

  • ToTensor:データをTensor型に変換

  • Normalize:各チャネルの平均が0、標準偏差が1となるように標準化

CIFAR10_MEAN = (0.4914, 0.4822, 0.4465)
CIFAR10_STD = (0.2470, 0.2435, 0.2616)

training_data = torchvision.datasets.CIFAR10(
    root="data",
    train=True,
    download=True,
    transform=torchvision.transforms.Compose([
        torchvision.transforms.ToTensor(), 
        torchvision.transforms.Normalize(CIFAR10_MEAN, CIFAR10_STD)]),
)


test_data = torchvision.datasets.CIFAR10(
    root="data",
    train=False,
    download=True,
    transform=torchvision.transforms.Compose([
        torchvision.transforms.ToTensor(), 
        torchvision.transforms.Normalize(CIFAR10_MEAN, CIFAR10_STD)]),
)

4. 学習の関数化

今回は、異なる条件で複数回学習を実行するため、学習部分を関数として定義します。
各エポックのテスト精度と消費した$${\epsilon}$$を出力します。

def training(optim='Adam', lr=1e-3, batch_size = 512, sigma=1.0, c=1.0):    
    # make dataloader
    train_dataloader = DataLoader(training_data, batch_size=batch_size, shuffle=True)
    test_dataloader = DataLoader(test_data, batch_size=1024, shuffle=False)

    # make model
    model = torchvision.models.resnet18()
    model = ModuleValidator.fix(model)

    device = "cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu"
    model = model.to(device)

    # make optimizer
    criterion = torch.nn.CrossEntropyLoss()
    if optim == 'Adam':
        optimizer = torch.optim.Adam(model.parameters(), lr=lr, weight_decay=1e-4)
    elif optim == 'RMSprop':
        optimizer = torch.optim.RMSprop(model.parameters(), lr=lr, weight_decay=1e-4)
    elif optim == 'MSGD':
        optimizer = torch.optim.SGD(model.parameters(), lr=lr,  momentum=0.9, weight_decay=1e-4)
    elif optim == 'SGD':
        optimizer = torch.optim.SGD(model.parameters(), lr=lr, weight_decay=1e-4)

    # DP-SGD settig
    privacy_engine = opacus.PrivacyEngine()
    model, optimizer, train_dataloader = privacy_engine.make_private(
        module=model,
        optimizer=optimizer,
        data_loader=train_dataloader,
        noise_multiplier=sigma,
        max_grad_norm=c,
    )

    # training 
    test_acc = []
    epsilon = []

    pbar = tqdm(range(15), desc=f"[sigma: {sigma}, c: {c}]")
    for epoch in pbar:
            
        with BatchMemoryManager(
            data_loader=train_dataloader, 
            max_physical_batch_size=64, 
            optimizer=optimizer
        ) as memory_safe_data_loader:
            model.train()
            for (X, y) in memory_safe_data_loader:
                X, y = X.to(device), y.to(device)

                # optimization step
                optimizer.zero_grad()
                pred = model(X)
                loss = criterion(pred, y)
                loss.backward()
                optimizer.step()

        # calculate epsilon
        epsilon.append(privacy_engine.get_epsilon(1e-5))
    
        # test
        model.eval()
        pred_list = []
        y_list = []
        with torch.no_grad():
            for X, y in test_dataloader:
                X, y = X.to(device), y.to(device)

                # predict
                pred = model(X)
                loss = criterion(pred, y)

                y_list.extend(y.to('cpu').numpy().tolist())
                pred_list.extend(pred.argmax(1).to('cpu').numpy().tolist())

        test_acc.append(metrics.accuracy_score(y_list, pred_list))
        pbar.set_postfix(epsilon=epsilon[-1],test_acc=test_acc[-1])
    return epsilon, test_acc

5. 学習

DP-SGDのパラメータ$${\sigma}$$と$${C}$$を変えて学習を行います。
DP-SGDでは、ニューラルネットワークのパラメータ$${\theta}$$を下記のように更新します。

$$
\theta_{i+1} \leftarrow \theta_i - \eta \left( \frac{1}{|B|} \sum_{(x,y)\in B} {\rm clip}C (\nabla{\theta}\ell(f(x), y)) + Z\right)
$$

ここで、$${\ell}$$は損失関数、$${{\rm clip}_C}$$はL2ノルムを$${C}$$に制限する関数、$${Z}$$は多変量正規分布$${\mathcal{N}(0,\sigma^2C^2\mathbb{I})}$$に従うノイズです。
この中で、$${\sigma}$$と$${C}$$がDP-SGD特有のパラメータになります。

result = {}

for sigma in [0.5, 1.0]:
    for c in [0.1, 1.0, 10]:
        epsilon, test_acc = training(sigma=sigma, c=c)
        result[(sigma, c)] = (epsilon, test_acc)
[sigma: 0.5, c: 0.1]: 100%|██████████| 15/15 [21:56<00:00, 87.73s/it, epsilon=16, test_acc=0.572]
[sigma: 0.5, c: 1.0]: 100%|██████████| 15/15 [21:25<00:00, 85.73s/it, epsilon=16, test_acc=0.574]
[sigma: 0.5, c: 10]: 100%|██████████| 15/15 [21:22<00:00, 85.49s/it, epsilon=16, test_acc=0.565]
[sigma: 1.0, c: 0.1]: 100%|██████████| 15/15 [21:32<00:00, 86.14s/it, epsilon=2.27, test_acc=0.51]
[sigma: 1.0, c: 1.0]: 100%|██████████| 15/15 [21:33<00:00, 86.22s/it, epsilon=2.27, test_acc=0.508]
[sigma: 1.0, c: 10]: 100%|██████████| 15/15 [21:30<00:00, 86.05s/it, epsilon=2.27, test_acc=0.502]

6. 学習結果の表示

学習結果として、消費した$${\epsilon}$$とテストデータの精度を描画します。

for val, res in result.items():
    epsilon, test_acc = res
    plt.plot(epsilon, test_acc, label=f'sigma: {val[0]}, c: {val[1]}')
plt.xlabel('Epsilon')
plt.ylabel('Accuracy')
plt.legend()
plt.show()


おわりに

今回の結果

$${C}$$の値を変えても結果はほぼ変わりませんでした、一方で$${\sigma}$$を小さくすると、学習時のノイズの大きさが小さくなるので、消費する$${\epsilon}$$が増大し、テスト精度も少し改善することが分かりました。

これまで目安としていた$${\epsilon=2}$$で学習を行うために、今後も$${\sigma=1.0}$$と$${C=1.0}$$を採用しようと思います。

次にやること

ニューラルネットワークの種類を変えることでDP-SGDのテスト精度がどう変わるか検証したいと思います。

この記事が気に入ったらサポートをしてみませんか?