参考: Federated Learning コードの解釈、超詳細
参考文献:[1602.05629] Communication-Efficient Learning of Deep Networks from Decentralized Data (arxiv.org)
参考コード:GitHub - AshwinRJ/Federated-Learning-PyTorch: 分散データからのディープネットワークの通信効率的な学習の実装
今すぐ先駆的な作品のコードを読んでみてください。
目次
2. データ IID および非 IID サンプリング -sampling.py
3. ローカルモデルパラメータの更新 - update.py
1. パラメーターをロードします—options.py
import argparse
def args_parser():
parser = argparse.ArgumentParser()
# federated arguments (Notation for the arguments followed from paper)
parser.add_argument('--epochs', type=int, default=10,
help="number of rounds of training")
parser.add_argument('--num_users', type=int, default=100,
help="number of users: K")
parser.add_argument('--frac', type=float, default=0.1,
help='the fraction of clients: C')
parser.add_argument('--local_ep', type=int, default=10,
help="the number of local epochs: E")
parser.add_argument('--local_bs', type=int, default=10,
help="local batch size: B")
parser.add_argument('--lr', type=float, default=0.01,
help='learning rate')
parser.add_argument('--momentum', type=float, default=0.5,
help='SGD momentum (default: 0.5)')
# model arguments
parser.add_argument('--model', type=str, default='mlp', help='model name')
parser.add_argument('--kernel_num', type=int, default=9,
help='number of each kind of kernel')
parser.add_argument('--kernel_sizes', type=str, default='3,4,5',
help='comma-separated kernel size to \
use for convolution')
parser.add_argument('--num_channels', type=int, default=1, help="number \
of channels of imgs")
parser.add_argument('--norm', type=str, default='batch_norm',
help="batch_norm, layer_norm, or None")
parser.add_argument('--num_filters', type=int, default=32,
help="number of filters for conv nets -- 32 for \
mini-imagenet, 64 for omiglot.")
parser.add_argument('--max_pool', type=str, default='True',
help="Whether use max pooling rather than \
strided convolutions")
# other arguments
parser.add_argument('--dataset', type=str, default='mnist', help="name \
of dataset")
parser.add_argument('--num_classes', type=int, default=10, help="number \
of classes")
parser.add_argument('--gpu', default=None, help="To use cuda, set \
to a specific GPU ID. Default set to use CPU.")
parser.add_argument('--optimizer', type=str, default='sgd', help="type \
of optimizer")
parser.add_argument('--iid', type=int, default=1,
help='Default set to IID. Set to 0 for non-IID.')
parser.add_argument('--unequal', type=int, default=0,
help='whether to use unequal data splits for \
non-i.i.d setting (use 0 for equal splits)')
parser.add_argument('--stopping_rounds', type=int, default=10,
help='rounds of early stopping')
parser.add_argument('--verbose', type=int, default=1, help='verbose')
parser.add_argument('--seed', type=int, default=1, help='random seed')
args = parser.parse_args()
return args
ここでは、argparse を使用して 3 種類のパラメーター、つまりフェデレーション パラメーター、モデル パラメーター、およびその他のパラメーターを入力します。その中で、フェデレーションパラメータは次のとおりです。
- エポック: トレーニング ラウンドの数、10
- num_users : ユーザー数 K、デフォルトは 100
- frac : ユーザーは比率 C を選択します。デフォルトは 0.1 です。
- local_ep : ローカルトレーニング量 E、デフォルトは 10
- local_bs : ローカル トレーニング バッチ B、デフォルトは 10
- lr : 学習率、デフォルトは 0.01
- 勢い: SGD の勢い (なぜ SGD に勢いがあるのか?)、デフォルトは 0.5
モデルパラメータ:
- model : モデル名、デフォルトは mlp で、完全に接続されたニューラル ネットワークです。
- kernel_num : 畳み込みカーネルの数、デフォルトでは 9
- kernel_sizes : コンボリューション カーネル サイズ、デフォルトは 3、4、5
- num_channels : 画像チャンネルの数、デフォルトは 1
- Norm : 正規化方法。BN および LN を使用できます。
- num_filters : フィルターの数、デフォルトは 32
- max_pool : 最大プーリング、デフォルトは True
その他の設定:
- dataset : 選択するデータセット、デフォルトの mnist
- num_class : カテゴリの数、デフォルトは 10
- gpu : デフォルトで使用され、特定の cuda 番号を入力できます
- optimizer : オプティマイザー、デフォルトは SGD アルゴリズムです
- iid : 独立かつ同一に分散、デフォルトは 1、つまり独立かつ同一に分散
- unequal : データセットを均等に分散するかどうか。デフォルトは 0 です。
- stop_rounds : 停止ラウンド数、デフォルトは 10
- verbose : ログ表示、0 出力しない、1 プログレスバー付きログ出力、2 プログレスバーなしログ出力
- シード: 乱数シード、デフォルトは 1
最後に、args_parser() 関数は、コンソールによって入力されたパラメータを含む args を返します。
2. データ IID および非 IID サンプリング -sampling.py
このファイルは、mnist および cifar-10 から IID データと非 IID データを収集します。
1.mnist_iid()
def mnist_iid(dataset, num_users):
"""
Sample I.I.D. client data from MNIST dataset
:param dataset:
:param num_users:
:return: dict of image index
"""
num_items = int(len(dataset)/num_users)
dict_users, all_idxs = {}, [i for i in range(len(dataset))]
for i in range(num_users):
dict_users[i] = set(np.random.choice(all_idxs, num_items,
replace=False))
all_idxs = list(set(all_idxs) - dict_users[i])
return dict_users
100 人のユーザーから 600 個のランダム サンプルをランダムに選択します。
2.mnist_nonid()
def mnist_noniid(dataset, num_users):
"""
Sample non-I.I.D client data from MNIST dataset
:param dataset:
:param num_users:
:return:
"""
# 60,000 training imgs --> 200 imgs/shard X 300 shards
num_shards, num_imgs = 200, 300
idx_shard = [i for i in range(num_shards)]
dict_users = {i: np.array([]) for i in range(num_users)}
idxs = np.arange(num_shards*num_imgs)
labels = dataset.train_labels.numpy()
# sort labels
idxs_labels = np.vstack((idxs, labels))
idxs_labels = idxs_labels[:, idxs_labels[1, :].argsort()]
idxs = idxs_labels[0, :]
# divide and assign 2 shards/client
for i in range(num_users):
rand_set = set(np.random.choice(idx_shard, 2, replace=False))
idx_shard = list(set(idx_shard) - rand_set)
for rand in rand_set:
dict_users[i] = np.concatenate(
(dict_users[i], idxs[rand*num_imgs:(rand+1)*num_imgs]), axis=0)
return dict_users
- num_shards: 60,000 個のトレーニング セットの画像を 200 の部分に分割します
- [i for i in range()]: 増分リストを生成できます
- {i: np.array([]) for i in range(num_users)}: 中かっこで囲んだ 100 ユーザーの辞書を生成します
- np.vstack ((idxs,labels)): 数値とラベルを積み重ねて (2,60000 )の配列を形成します
- idxs_labels = idxs_labels[:, idxs_labels[1, :]. argsort ()]: argsort 関数の機能は、配列内の要素のインデックス配列値を小さいものから大きいものまでソートして出力することです
スクリーニング後、小さいものから大きいものまでのラベル インデックス idxs が取得されます。次に、ユーザーシャーディングを実行します。
- np.random.choice (): スライスのシリアル番号から 2 つのシリアル番号を選択し、replace パラメーターはサンプリングが置き換えられないことを示します
- idxs[rand*num_imgs:(rand+1)*num_imgs]: 300 個の連続したソートされたインデックス番号を取得します。
- np. concatenate (): どの次元からどの次元を追加するかを表記します ここでは、200個のインデックス番号からランダムに2つの乱数を選択し、その2つの乱数に対応するデータを連結します。
最後に、この関数は各ユーザーの辞書と対応する 600 個のデータを返します。
3.mnist_nonid()
def mnist_noniid_unequal(dataset, num_users):
"""
Sample non-I.I.D client data from MNIST dataset s.t clients
have unequal amount of data
:param dataset:
:param num_users:
:returns a dict of clients with each clients assigned certain
number of training imgs
"""
ちょっと長くなるので分けて書きます。60,000 個のデータを 1,200 個に分割します。
# 60,000 training imgs --> 50 imgs/shard X 1200 shards
num_shards, num_imgs = 1200, 50
idx_shard = [i for i in range(num_shards)]
dict_users = {i: np.array([]) for i in range(num_users)}
idxs = np.arange(num_shards*num_imgs)
labels = dataset.train_labels.numpy()
ソートされたインデックス番号を取得します。
# sort labels
idxs_labels = np.vstack((idxs, labels))
idxs_labels = idxs_labels[:, idxs_labels[1, :].argsort()]
idxs = idxs_labels[0, :]
各ユーザーが保持するデータ コピーの範囲を設定します。
# Minimum and maximum shards assigned per client:
min_shard = 1
max_shard = 30
つまり、各ユーザーは少なくとも 1×50=50 枚の写真、最大で 30*50=1500 枚の写真を持っています。
次に、1,200 シェアをこれらのユーザーに割り当てる必要があり、各ユーザーには少なくとも 1 つのデータを割り当て、各データを割り当てる必要があります。
# Divide the shards into random chunks for every client
# s.t the sum of these chunks = num_shards
random_shard_size = np.random.randint(min_shard, max_shard+1,
size=num_users)
random_shard_size = np.around(random_shard_size /
sum(random_shard_size) * num_shards)
random_shard_size = random_shard_size.astype(int)
- np.random.randint: 開く前に閉じられ、その後開く間隔のリストを返します。長さはユーザーの数です。
- np.around: 丸め、偶数に戻す
この手順の後、合計が 1200 に近づくように、すべてのコピーが比例的に調整されます。(小数点以下は四捨五入されているため、厳密には 1200 にはなりません) そこで、次のステップでは、この厳密ではない部分を調整して割り当てます。
# Assign the shards randomly to each client
if sum(random_shard_size) > num_shards:
for i in range(num_users):
# First assign each client 1 shard to ensure every client has
# atleast one shard of data
rand_set = set(np.random.choice(idx_shard, 1, replace=False))
idx_shard = list(set(idx_shard) - rand_set)
for rand in rand_set:
dict_users[i] = np.concatenate(
(dict_users[i], idxs[rand*num_imgs:(rand+1)*num_imgs]),
axis=0)
random_shard_size = random_shard_size-1
# Next, randomly assign the remaining shards
for i in range(num_users):
if len(idx_shard) == 0:
continue
shard_size = random_shard_size[i]
if shard_size > len(idx_shard):
shard_size = len(idx_shard)
rand_set = set(np.random.choice(idx_shard, shard_size,
replace=False))
idx_shard = list(set(idx_shard) - rand_set)
for rand in rand_set:
dict_users[i] = np.concatenate(
(dict_users[i], idxs[rand*num_imgs:(rand+1)*num_imgs]),
axis=0)
else:
for i in range(num_users):
shard_size = random_shard_size[i]
rand_set = set(np.random.choice(idx_shard, shard_size,
replace=False))
idx_shard = list(set(idx_shard) - rand_set)
for rand in rand_set:
dict_users[i] = np.concatenate(
(dict_users[i], idxs[rand*num_imgs:(rand+1)*num_imgs]),
axis=0)
if len(idx_shard) > 0:
# Add the leftover shards to the client with minimum images:
shard_size = len(idx_shard)
# Add the remaining shard to the client with lowest data
k = min(dict_users, key=lambda x: len(dict_users.get(x)))
rand_set = set(np.random.choice(idx_shard, shard_size,
replace=False))
idx_shard = list(set(idx_shard) - rand_set)
for rand in rand_set:
dict_users[k] = np.concatenate(
(dict_users[k], idxs[rand*num_imgs:(rand+1)*num_imgs]),
axis=0)
return dict_users
最後に、ランダムに割り当てられたユーザーが保持する非 IID データのインデックス辞書が取得されます。
4.cifar_iid()、cifar_noniid()
違いはないので書かないでください
3. ローカルモデルパラメータの更新 - update.py
1.データセット分割(データセット)
まず、Dataset クラスの公式の説明を見てみましょう。 Dataset は何でもかまいませんが、常に __len__ 関数 (Python の標準関数 len によって呼び出されます) と、コンテンツのインデックス付けに使用される __getitem__ 関数が含まれています。
class DatasetSplit(Dataset):
"""An abstract Dataset class wrapped around Pytorch Dataset class.
"""
def __init__(self, dataset, idxs):
self.dataset = dataset
self.idxs = [int(i) for i in idxs]
def __len__(self):
return len(self.idxs)
def __getitem__(self, item):
image, label = self.dataset[self.idxs[item]]
return torch.tensor(image), torch.tensor(label)
コードのこの部分は Dataset クラスをオーバーライドします。
- __len__(self) メソッドは、データ リストの長さ、つまりデータ セット内のサンプル数を返すように書き直されました。
- __getitem__(self,item) メソッドをオーバーライドして、画像とラベルのテンソルを取得します。
2.ローカルアップデート(オブジェクト)
これはモデルをローカルで更新するためのコードです。少し量が多いので、個別に説明します。
class LocalUpdate(object):...
1 つ目はコンストラクターで、最初にパラメーターとログを定義し、次に train_val_test() 関数からデータ ローダーを取得し、次にコンピューティング デバイスを指定します。
さらに重要なことは、ここでの損失関数はクロスエントロピーに似た NLL 損失関数であることです。唯一の違いは、結果が NLL の対数で 1 回ソフトマックス化されることです。
def __init__(self, args, dataset, idxs, logger):
self.args = args
self.logger = logger
self.trainloader, self.validloader, self.testloader = self.train_val_test(
dataset, list(idxs))
self.device = 'cuda' if args.gpu else 'cpu'
# Default criterion set to NLL loss function
self.criterion = nn.NLLLoss().to(self.device)
次は train_val_test() 関数で、データセットを分割するために使用されます。入力データセットとインデックスは 8:1:1 に従って分割されます。バッチサイズを指定する場合、訓練セットが args パラメーターから指定されることを除き、val と test の両方が合計の 10 分の 1 を占めることに注意してください。
def train_val_test(self, dataset, idxs):
"""
Returns train, validation and test dataloaders for a given dataset
and user indexes.
"""
# split indexes for train, validation, and test (80, 10, 10)
idxs_train = idxs[:int(0.8*len(idxs))]
idxs_val = idxs[int(0.8*len(idxs)):int(0.9*len(idxs))]
idxs_test = idxs[int(0.9*len(idxs)):]
trainloader = DataLoader(DatasetSplit(dataset, idxs_train),
batch_size=self.args.local_bs, shuffle=True)
validloader = DataLoader(DatasetSplit(dataset, idxs_val),
batch_size=int(len(idxs_val)/10), shuffle=False)
testloader = DataLoader(DatasetSplit(dataset, idxs_test),
batch_size=int(len(idxs_test)/10), shuffle=False)
return trainloader, validloader, testloader
次はローカル重み更新関数です。これはモデルとグローバル更新のラウンド数を入力し、更新された重みと損失の平均を出力します。最初にオプティマイザーが選択され、次にトレーニング ループが開始されます。
def update_weights(self, model, global_round):
# Set mode to train model
model.train()
epoch_loss = []
# Set optimizer for the local updates
if self.args.optimizer == 'sgd':
optimizer = torch.optim.SGD(model.parameters(), lr=self.args.lr,
momentum=0.5)
elif self.args.optimizer == 'adam':
optimizer = torch.optim.Adam(model.parameters(), lr=self.args.lr,
weight_decay=1e-4)
for iter in range(self.args.local_ep):
batch_loss = []
for batch_idx, (images, labels) in enumerate(self.trainloader):
images, labels = images.to(self.device), labels.to(self.device)
model.zero_grad()
log_probs = model(images)
loss = self.criterion(log_probs, labels)
loss.backward()
optimizer.step()
if self.args.verbose and (batch_idx % 10 == 0):
print('| Global Round : {} | Local Epoch : {} | [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
global_round, iter, batch_idx * len(images),
len(self.trainloader.dataset),
100. * batch_idx / len(self.trainloader), loss.item()))
self.logger.add_scalar('loss', loss.item())
batch_loss.append(loss.item())
epoch_loss.append(sum(batch_loss)/len(batch_loss))
return model.state_dict(), sum(epoch_loss) / len(epoch_loss)
- self.logger.add_scalar ('loss', loss.item()): この関数は、プログラム内のデータを保存し、視覚化のために tensorboard ツールを使用するために使用されます。
- ローカルラウンドが通過するたびに、現在の損失がカウントされ、最終的な平均損失統計に使用されます。
- model.state_dict (): Pytorch でネットワーク パラメーターを表示するためのメソッドであり、torch.save( )を使用して pth ファイルとして保存できます。
次は評価関数: inference(self, model) です。入力はモデルであり、正確な値と損失値が計算されます。ここでのコードは非常に有益です。
def inference(self, model):
""" Returns the inference accuracy and loss.
"""
model.eval()
loss, total, correct = 0.0, 0.0, 0.0
for batch_idx, (images, labels) in enumerate(self.testloader):
images, labels = images.to(self.device), labels.to(self.device)
# Inference
outputs = model(images)
batch_loss = self.criterion(outputs, labels)
loss += batch_loss.item()
# Prediction
_, pred_labels = torch.max(outputs, 1)
pred_labels = pred_labels.view(-1)
correct += torch.sum(torch.eq(pred_labels, labels)).item()
total += len(labels)
accuracy = correct/total
return accuracy, loss
- model.eval ( ): モデルの評価モードを開きます
- torch.max (): 2 番目のパラメーターは次元を参照します。つまり、最初の次元 (つまり、行) を返します。ここで、大きな値のインデックスが返されます。
- pred_labels.view (-1): 本来の目的は、別の数値に従って次元を自動的に調整することですが、ここでは次元が 1 つしかないため、X 内のすべての次元データが 1 次元に変換され、順番に配置されます。
- torch.eq (): 2 つのテンソルを要素ごとに比較します。同じ位置にある 2 つの要素が同じ場合は True を返し、異なる場合は False を返します。
ここでの関数は、テスト セットの画像とラベルを取得し、モデルが結果を生成した後に損失を計算し、それらを累積します。
3.test_inference(self,model)
これは、LocalUpdate の推論関数とまったく同じですが、引数とモデルに加えて、入力パラメーターで test_dataset も指定する必要がある点が異なります。
def test_inference(args, model, test_dataset):
""" Returns the test accuracy and loss.
"""
model.eval()
loss, total, correct = 0.0, 0.0, 0.0
device = 'cuda' if args.gpu else 'cpu'
criterion = nn.NLLLoss().to(device)
testloader = DataLoader(test_dataset, batch_size=128,
shuffle=False)
for batch_idx, (images, labels) in enumerate(testloader):
images, labels = images.to(device), labels.to(device)
# Inference
outputs = model(images)
batch_loss = criterion(outputs, labels)
loss += batch_loss.item()
# Prediction
_, pred_labels = torch.max(outputs, 1)
pred_labels = pred_labels.view(-1)
correct += torch.sum(torch.eq(pred_labels, labels)).item()
total += len(labels)
accuracy = correct/total
return accuracy, loss
4. アプリケーションセット - utils.py
いくつかのツール関数はここにカプセル化されています: get_dataset()、average_weights()、exp_details()
1.get_dataset(args)
get_dataset(args) は、コマンド コンソール パラメーターに従って、対応するデータセットとユーザー データ ディクショナリを取得します。あくまでifの話なので、ちょっと簡単な話なら言いません。
2.平均重み(w)
重みの平均を返します。つまり、統合平均アルゴリズムを実行します。
def average_weights(w):
"""
Returns the average of the weights.
"""
w_avg = copy.deepcopy(w[0])
for key in w_avg.keys():
for i in range(1, len(w)):
w_avg[key] += w[i][key]
w_avg[key] = torch.div(w_avg[key], len(w))
return w_avg
- w : この w は、複数ラウンドのローカル トレーニング後に計算された重みリストです。パラメータのデフォルトの場合、長さ 10 のリストであり、各要素は辞書であり、各辞書にはモデルの名前が含まれていますパラメータ (layer_input.weight やlayer_hidden.bias など)、およびその重みの特定の値。
- copy.deepcopy (): ディープコピー。コピーされたオブジェクトが変更されても、コピーされたオブジェクトは変更されません。ここでは、最初のユーザーの体重辞書がコピーされます。
次に、各タイプのパラメーターをループし、各ユーザー モデルの対応するパラメーターの値を累積し、最後に平均をとって平均モデルを取得します。
3.exp_details(引数)
ビジュアルコマンドコンソールパラメータ引数:
def exp_details(args):
print('\nExperimental details:')
print(f' Model : {args.model}')
print(f' Optimizer : {args.optimizer}')
print(f' Learning : {args.lr}')
print(f' Global Rounds : {args.epochs}\n')
print(' Federated parameters:')
if args.iid:
print(' IID')
else:
print(' Non-IID')
print(f' Fraction of users : {args.frac}')
print(f' Local Batch size : {args.local_bs}')
print(f' Local Epochs : {args.local_ep}\n')
return
5. モデル設定 - models.py
このファイルは、より一般的なネットワーク モデルのいくつかをセットアップします。
1.MLP多層パーセプトロンモデル
class MLP(nn.Module):
def __init__(self, dim_in, dim_hidden, dim_out):
super(MLP, self).__init__()
self.layer_input = nn.Linear(dim_in, dim_hidden)
self.relu = nn.ReLU()
self.dropout = nn.Dropout()
self.layer_hidden = nn.Linear(dim_hidden, dim_out)
self.softmax = nn.Softmax(dim=1)
def forward(self, x):
x = x.view(-1, x.shape[1]*x.shape[-2]*x.shape[-1])
x = self.layer_input(x)
x = self.dropout(x)
x = self.relu(x)
x = self.layer_hidden(x)
return self.softmax(x)
- nn. Dropout (): わかりません、分からなかったら検索してください
2. CNN畳み込みニューラルネットワーク
多すぎて表示できません。
3. 独自のモデルを作成する
ここの元のコードは modelC で、そのコンストラクターの下で、super の最初のパラメーターは AllConvNet であり、コンパイラーでエラーが報告されます。ただし、これはタイプミスではなく、ユーザーがカスタマイズできるようにするためです。
6、メイン関数 - federated_main.py
(ここに投稿したコードはコメントを変更したものです)
まずはライブラリのリファレンスです。
import os
import copy
import time
import pickle
import numpy as np
from tqdm import tqdm
import torch
from tensorboardX import SummaryWriter
from options import args_parser
from update import LocalUpdate, test_inference
from models import MLP, CNNMnist, CNNFashion_Mnist, CNNCifar
from utils import get_dataset, average_weights, exp_details
次に、main 関数を直接開始します。
if __name__ == '__main__':
start_time = time.time()
# 定义路径
path_project = os.path.abspath('..') # 上级目录的绝对路径
logger = SummaryWriter('../logs') # python可视化工具
args = args_parser() # 输入命令行参数
exp_details(args) # 显示命令行参数情况
デバッグ状態で実行しているためパラメータは変更されておらず、パラメータは次のとおりです。
次に、データセットとユーザー データ ディクショナリを読み込みます。
# 判断GPU是否可用:
if args.gpu:
torch.cuda.set_device(args.gpu)
device = 'cuda' if args.gpu else 'cpu'
# 加载数据集,用户本地数据字典
train_dataset, test_dataset, user_groups = get_dataset(args)
ここでは、60,000 のトレーニング セット、10,000 のテスト セット、および長さ 100 のユーザー辞書が返されます。ユーザー辞書は、100 人のユーザーから 600 の IID トレーニング データへのマッピングです。
次に、モデルの構築を開始します。モデルは多層パーセプトロンを選択します。
# 建立模型
if args.model == 'cnn':
# 卷积神经网络
if args.dataset == 'mnist':
global_model = CNNMnist(args=args)
elif args.dataset == 'fmnist':
global_model = CNNFashion_Mnist(args=args)
elif args.dataset == 'cifar':
global_model = CNNCifar(args=args)
elif args.model == 'mlp':
# 多层感知机
img_size = train_dataset[0][0].shape
len_in = 1
for x in img_size:
len_in *= x
global_model = MLP(dim_in=len_in, dim_hidden=64,
dim_out=args.num_classes)
else:
exit('Error: unrecognized model')
次のステップでは、トレーニングの最初のラウンド用にモデルをセットアップし、重みをコピーします。
# 设置模型进行训练,并传输给计算设备
global_model.to(device)
global_model.train()
print(global_model)
# 复制权重
global_weights = global_model.state_dict()
モデルは次のようになります。
これは、784 の入力層、64 の隠れ層、10 の出力層を持つ多層パーセプトロンであり、ドロップアウトが 0.5 に設定されています。
次に、正式なトレーニングを開始します。
# 训练
train_loss, train_accuracy = [], []
val_acc_list, net_list = [], []
cv_loss, cv_acc = [], []
print_every = 2
val_loss_pre, counter = 0, 0
for epoch in tqdm(range(args.epochs)):
local_weights, local_losses = [], []
print(f'\n | Global Training Round : {epoch + 1} |\n')
global_model.train()
m = max(int(args.frac * args.num_users), 1) # 随机选比例为frac的用户
idxs_users = np.random.choice(range(args.num_users), m, replace=False)
for idx in idxs_users:
local_model = LocalUpdate(args=args, dataset=train_dataset,
idxs=user_groups[idx], logger=logger)
w, loss = local_model.update_weights(
model=copy.deepcopy(global_model), global_round=epoch)
local_weights.append(copy.deepcopy(w))
local_losses.append(copy.deepcopy(loss))
# 联邦平均,更新全局权重
global_weights = average_weights(local_weights)
# 将更新后的全局权重载入模型
global_model.load_state_dict(global_weights)
loss_avg = sum(local_losses) / len(local_losses)
train_loss.append(loss_avg)
# 每轮训练,都要计算所有用户的平均训练精度
list_acc, list_loss = [], []
global_model.eval()
for c in range(args.num_users):
local_model = LocalUpdate(args=args, dataset=train_dataset,
idxs=user_groups[idx], logger=logger)
acc, loss = local_model.inference(model=global_model)
list_acc.append(acc)
list_loss.append(loss)
train_accuracy.append(sum(list_acc) / len(list_acc))
# 每i轮打印全局Loss
if (epoch + 1) % print_every == 0:
print(f' \nAvg Training Stats after {epoch + 1} global rounds:')
print(f'Training Loss : {np.mean(np.array(train_loss))}')
print('Train Accuracy: {:.2f}% \n'.format(100 * train_accuracy[-1]))
- 正直、train_loss、train_accuracy、print_every以外は分かりません。
- tqdm は、for ループでの実行時間と進行状況の表示をサポートする強力な進行状況バーです。
- global_model.train() : モデルをトレーニング モードに設定します。
- idxs_users : ユーザーのインデックス リストをランダムに選択します。ここで、ユーザー選択率は 0.1、ユーザーの総数は 100 です。すると、100 × 0.1 = 10 人のユーザーがトレーニングに参加するためにランダムに選択されます。
- ローカル更新の実行: 選択したユーザーに対してローカル更新を実行します。データセット インデックスは user_groups[idx] から取得され、更新されたローカル パラメーターと損失値を記録します。
- 統合平均化: モデル パラメーター ディクショナリを更新関数に渡し、平均化されたモデル パラメーター ディクショナリを返し、それをグローバル モデルにロードします。
各ラウンドの終了時に、100 人のユーザー全員のトレーニング精度がカウントされ、ラウンドごと。
(注: スクロールし続けるモデルを実行しているグローバル ラウンドとローカル エポックは何ですか。このモデルは、update.py の LocalUpdate クラスの update_weights メソッドを呼び出すことで形成されます。頻繁にスクロールさせたくない場合は、コメントしてください。この関数で出力できます)
グローバル トレーニング後のテスト セットでのモデルのパフォーマンスは次のようになります。
# 训练后,测试模型在测试集的表现
test_acc, test_loss = test_inference(args, global_model, test_dataset)
print(f' \n Results after {args.epochs} global rounds of training:')
print("|---- Avg Train Accuracy: {:.2f}%".format(100 * train_accuracy[-1]))
print("|---- Test Accuracy: {:.2f}%".format(100 * test_acc))
結果:
最後は目標のトレーニングロスとトレーニング精度を保存し、最後に時間を出力します。
# 保存目标训练损失和训练精度
file_name = '../save/objects/{}_{}_{}_C[{}]_iid[{}]_E[{}]_B[{}].pkl'. \
format(args.dataset, args.model, args.epochs, args.frac, args.iid,
args.local_ep, args.local_bs)
with open(file_name, 'wb') as f:
pickle.dump([train_loss, train_accuracy], f)
print('\n Total Run Time: {0:0.4f}'.format(time.time() - start_time))
- pkl ファイル: pickle.dump (データ、f) は書き込み用、pickle.load (ファイル名) は読み取り用で、損失と精度が保存されます。
7. 描画
コードの最後に、作成者はコメント付きの描画コードを記述しました。
# 画图
import matplotlib
import matplotlib.pyplot as plt
matplotlib.use('Agg')
# 绘制损失曲线
plt.figure()
plt.title('训练损失 vs 通信回合数')
plt.plot(range(len(train_loss)), train_loss, color='r')
plt.ylabel('训练损失')
plt.xlabel('通信回合数')
plt.savefig('../save/fed_{}_{}_{}_C[{}]_iid[{}]_E[{}]_B[{}]_loss.png'.
format(args.dataset, args.model, args.epochs, args.frac,
args.iid, args.local_ep, args.local_bs))
# 平均准度曲线
plt.figure()
plt.title('平均准度 vs 通信回合数')
plt.plot(range(len(train_accuracy)), train_accuracy, color='k')
plt.ylabel('平均准度')
plt.xlabel('通信回合数')
plt.savefig('../save/fed_{}_{}_{}_C[{}]_iid[{}]_E[{}]_B[{}]_acc.png'.
format(args.dataset, args.model, args.epochs, args.frac,
args.iid, args.local_ep, args.local_bs))
次のように画像を作成します。
8. 個人的な概要
今回コードを読んで、コードの構成、いくつかのライブラリの適用、フェデレーテッド ラーニングの最も重要な仕組みなど、非常に勉強になりました。シンプルでわかりやすいコードでこのような有意義な記事を書いていただきました. 感心します。コードの強度が向上しただけでなく、フロリダ大学の門に正式に足を踏み入れることができました。