联邦学习基础

联邦学习

非独立同分布解决方案

完成于2024/03/23

联邦学习基础概念

联邦学习的定义

img

联邦学习训练流程

img

联邦学习的分类

img

img

联邦学习架构介绍

img

img

联邦学习面临问题

img

Non-IID问题引出

Non-IID的引出

img

Non-IID的类别

img

Non-IID的衡量指标

img

Non-IID的解决方法

img

涉及重点模型解释:
img

img

img

img

Non-IID的解法推广

img

img

对策1:多任务学习 ( Multi-task Learning )

img

img

对策2:元学习 ( Meta-Learning )

img

img

对策3:全局模型 + Personalization 层

img

img

img

对策4:基于 HyperNetwork 的个性化

img

img

img

img

梯度下降基础概念

什么是梯度下降法

img

梯度下降法的运行过程

img

img

「算法初始化」

import matplotlib.pyplot as plt
import numpy as np
# 初始算法开始之前的坐标
# cur_x 和 cur_y 
cur_x = 6
cur_y = (cur_x-1)**2 + 1
# 设置学习率 eta 为 0.05
eta = 0.05
# 变量 iter 用于存储迭代次数
# 这次我们迭代 1000 次
# 所以给它赋值 1000
iter = 1000
# 变量 cur_df 用于存储
# 当前位置的导数
# 一开始我给它赋值为 None
# 每一轮循环的时候为它更新值
cur_df = None
# all_x 用于存储
# 算法进行时所有点的横坐标
all_x = []
# all_y 用于存储
# 算法进行时所有点的纵坐标
all_y = []
# 把最一开始的坐标存储到
# all_x 和 all_y 中
all_x.append(cur_x)
all_y.append(cur_y)

「迭代 1000 次」

# 循环结束也就意味着算法的结束
for i in range(iter):
    # 每一次迭代之前先计算当前位置的梯度 cur_df
    # cur 是英文单词 current 
    cur_df = 2*cur_x - 2
    # 更新 cur_x 到下一个位置
    cur_x = cur_x - eta*cur_df
    # 更新下一个 cur_x 对应的 cur_y
    cur_y = (cur_x-1)**2 + 1
    
    # 其实 cur_y 并没有起到实际的计算作用
    # 在这里计算 cur_y 只是为了将每一次的点的坐标存储到 all_x 和 all_y 中
    # all_x 存储了二维平面上所有点的横坐标
    # all_y 存储了二维平面上所欲点的纵坐标
    # 使用 list 的 append 方法添加元素
    
    all_x.append(cur_x)
    all_y.append(cur_y) 

「绘图」

# 这里的 x, y 值为了绘制二次函数
# 的那根曲线用的,和算法没有关系
# linspace 将会从区间 [-5, 7] 中
# 等距离分割出 100 个点并返回一个
# np.array 类型的对象给 x
x = np.linspace(-5, 7, 100)
# 计算出 x 中每一个横坐标对应的纵坐标
y = (x-1)**2 + 1
# plot 函数会把传入的 x, y
# 组成的每一个点依次连接成一个平滑的曲线
# 这样就是我们看到的二次函数的曲线了
plt.plot(x, y)
# axis 函数用来指定坐标系的横轴纵轴的范围
# 这样就表示了 
# 横轴为 [-7, 9]
# 纵轴为 [0, 50]
plt.axis([-7, 9, 0, 50])
# scatter 函数是用来绘制散点图的
# scatter 和 plot 函数不同
# scatter 并不会将每个点依次连接
# 而是直接将它们以点的形式绘制出来
plt.scatter(np.array(all_x), np.array(all_y), color='red')
plt.show() 

img

img

通过最终的效果图可以发现,「梯度下降」在一步一步地收敛到二次函数的极小值点,同时也是二次函数的最小值点

二元函数的梯度下降

img

多元函数的梯度下降

img

梯度下降常见算法

批量梯度下降

img

这部分待补充……

随机梯度下降算法

待补充……

小批量梯度下降

待补充……

配置环境时收获的知识

代码环境配置

目前GPU环境已经配置好:
img

img

待补充……

云服务器

img

老师论文分析

我方论文分析

img

论文框架分析

img

img

论文代码分析

img

下文均待补充……

推荐论文研究

待补充……

重点论文研究

FedAvG代码分析

Connect the Notebook with GoogleDrive

# from google.colab import drive
# drive.mount('/content/drive')
import sys
sys.path.insert(0,'./Federated-Learning-with-ResNet-50-on-CIFAR-10-main/FedAVG/')

这行代码是Python中用来操作系统路径的命令。sys.path是一个列表,其中包含了Python解释器自动查找所需模块(.py文件)的路径的字符串集合。当尝试导入一个模块时,Python解释器会遍历这个列表,从中查找对应的模块。

  • sys:这是一个内建模块,可以使用它来访问与Python解释器紧密相关的变量和函数。
  • sys.path:这是一个环境变量,它是一个字符串列表,表示解释器在导入模块时应该搜索的路径集合。
  • insert(0, path):这个函数用于将一个新的路径添加到sys.path列表的开头。列表中的第一个路径是解释器首先查找模块的地方。
  • './Federated-Learning-with-ResNet-50-on-CIFAR-10-main/FedAVG/':这是要添加到sys.path列表中的具体路径。点(.)表示当前目录,即运行Python脚本的目录。之后的路径是相对于当前目录的路径,指向一个包含要使用模块的文件夹。
    将这个路径添加到sys.path的最前面的原因是为了确保当你尝试导入模块时,解释器首先查找你指定的这个目录。

Import Libraries

from options import args_parser
#`options`:通常这个自定义模块包含了命令行参数解析的逻辑,`args_parser`函数可能用来读取和设置实验的配置参数,如学习率、批大小等。
from update import LocalUpdate, test_inference, client_update, server_aggregate, test
#`update`:这个自定义模块可能包含联邦学习框架中客户端和服务端更新模型的函数,如`LocalUpdate`用于执行客户端的模型更新,`test_inference`进行测试推断,`client_update`可能是客户端的权重更新,`server_aggregate`用于服务器端聚合模型权重,`test`用于测试模型性能。
from utils import get_dataset, average_weights, weighted_average_weights, exp_details, plot_client_distribution, get_n_params
#`utils`:这个自定义模块可能包含各种工具函数,如`get_dataset`获取数据集,`average_weights`和`weighted_average_weights`用于权重平均,`exp_details`打印实验细节,`plot_client_distribution`绘制客户端数据分布,`get_n_params`获取模型参数数量。
from modelRN50 import ResNet50
# `modelRN50`:这个自定义模块可能包含ResNet50模型的定义。
import torch
# `torch`:是PyTorch库,一个流行的深度学习框架,用于构建和训练模型。
torch.cuda.empty_cache()
import torch.optim as optim
# `torch.optim`:提供了多种优化算法,如SGD、Adam等,用于模型训练。
import numpy as np
#`numpy`:是一个科学计算库,提供了高性能的数组对象和工具。
import pandas as pd
#`pandas`:是一个数据分析库,提供了数据结构和数据分析工具。
from tqdm import tqdm # progress bar
#`tqdm`:是一个快速、扩展性强的进度条工具,可以在Python长循环中添加一个进度提示信息,用户只需要封装任意的迭代器`tqdm(iterator)`。
import copy
#`copy`:提供了通用的(深层和浅层)复制操作的接口。
from torch.utils.tensorboard import SummaryWriter
# `torch.utils.tensorboard`:允许使用TensorBoard,一个用于可视化训练过程的工具,`SummaryWriter`是它的一个组件。
logger = SummaryWriter('../logs')
#`logger = SummaryWriter('../logs')` 行创建了一个`SummaryWriter`的实例,它是PyTorch的`tensorboard`模块中的一个工具,用于记录和跟踪实验过程中的数据,以便于后续在TensorBoard中进行可视化。这个`SummaryWriter`对象通常用于记录训练过程中的参数、模型的性能指标如损失和准确率、系统资源利用率等。这里指定的`'../logs'`目录是用来存储这些记录的文件夹位置。

Set the parameters for the training

sys.argv=['',
#`sys.argv`通常用于获取命令行参数。这里手动设置`sys.argv`的内容,这样可以模拟命令行参数传递给`args_parser`函数。
          '--iid=1',  #0 -> NONiid, 1 -> iid
#设置数据集是独立同分布(i.i.d.)的,即所有用户的数据分布是相同的。若设为0,则表示非独立同分布(NON-iid),即不同用户的数据分布可能不同。
          '--num_users=100',
#定义总用户数为100。
          '--lr=0.0001',
#设置学习率为0.0001。
          '--local_ep=1',
#设置每个用户本地训练的轮数为1。
          '--epochs=300',
#设置全局训练轮数为300。
        args=args_parser()
'--optimizer=adam',
#选择Adam优化器。
          '--norm=batch_norm',
#指定使用批量归一化。
          '--local_bs=10',
#设置本地批大小为10。
          '--dataset=cifar',
#指明使用的数据集是CIFAR。
          '--loss=CrossEntropyLoss',
#设置损失函数为交叉熵损失。
          '--gpu=/device:GPU:0']
#指定使用的GPU设备。
#解析上面定义的参数并将它们保存到`args`对象中。
num_selected = int( args.num_users * args.frac )
#计算实际参与训练的用户数。这里没有给出`args.frac`的值,但通常这个参数表示从总用户数中选取的用户的比例。
baseline_num = 100 # number of baseline images to be saved on the global server
#在全局服务器上保存的基线图像数量,这些图像用于在聚合前重训练客户端的模型。
                   # for retraining of the client's model before aggregation
unbalanced = False # if True the clients will contain different number of classes
#这表示所有用户将拥有相同数量的类别。如果为True,不同的客户端将包含不同数量的类别,从而创建一个不平衡的数据环境。
verbose = False  
#这是一个日志级别标志。如果设置为True,则程序会打印出更多的输出信息,帮助调试或更详细地理解程序运行的情况。
 

Set the device on cuda

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
#这行代码首先调用`torch.cuda.is_available()`来检查当前环境是否有可用的CUDA支持的GPU。如果有,`torch.device('cuda')`指定PyTorch应该使用GPU进行计算。如果没有,`torch.device('cpu')`则指定使用CPU。这个检查是很有用的,因为如果尝试在没有可用CUDA支持的系统上使用GPU,代码会失败。创建的`device`对象可以在以后将模型或数据传送到指定的设备上。
if verbose:
#这个条件语句检查前面设置的`verbose`变量是否为`True`。如果是,它将执行内部的打印语句。
 print(f"Device available: {device}")  
#如果`verbose`为`True`,这行代码将打印出可用的设备。`f`前缀表示这是一个格式化字符串,允许你直接在字符串中插入变量的值。这里它会输出`Device available: cuda`(如果GPU可用)或`Device available: cpu`(如果GPU不可用)。

Build the Model ResNet-50

global_model = ResNet50(args.norm)
#实例化一个ResNet50模型。`args.norm`参数决定了模型中使用的正则化类型,如批量正则化(batch normalization)。
global_model.to(device)
#将模型的所有参数和缓冲区移动到定义的设备上,这个设备可以是CPU或GPU。
global_model.train()
#将模型设置为训练模式。这对于某些模型组件,如Dropout和BatchNorm层,其行为在训练和评估模式下是不同的,是非常重要的。
if verbose:
#检查是否应该打印详细信息。
  print(global_model)
#如果`verbose`为真,则打印模型的架构。
  print(f"Number of Parameters: {get_n_params(global_model)}")
#如果`verbose`为真,打印出模型的参数数量。`get_n_params`函数可能是计算模型所有可训练参数的函数。
global_weights = global_model.state_dict()
#获取全局模型的状态字典,包含了模型的所有权重和偏置参数。
client_models = [ global_model for _ in range(num_selected)]
#创建客户端模型列表,每个模型都是全局模型的一个副本。
# Synchronizing the clients with the global model 
for model in client_models:           遍历客户端模型。
    # load the parameters and bufferes from global model
    model.load_state_dict(global_model.state_dict()) 
#加载全局模型的状态字典到每个客户端模型中,这确保了所有客户端模型在开始训练时是同步的。
# Optimizer selection
if args.optimizer == 'sgd':            #   检查是否应该使用SGD优化器。
    opt = [optim.SGD(model.parameters(), lr=args.lr, momentum=args.momentum, weight_decay=args.wd) for model in client_models]
#如果选择了SGD优化器,为每个客户端模型创建一个SGD优化器实例。设置学习率、动量和权重衰减。
elif args.optimizer == 'adam':                 # 检查是否使用了Adam优化器
    opt = [optim.Adam(model.parameters(), lr=args.lr,  weight_decay=args.wd) for model in client_models]  
#如果选择了Adam优化器,为每个客户端模型创建一个Adam优化器实例。设置学习率和权重衰减。

Dataset split:

train_dataset, test_dataset, user_groups = get_dataset(args=args, unbalanced=unbalanced,)
#这行代码调用`get_dataset`函数,这个函数接收两个参数:`args`(包含各种训练设置的参数)和`unbalanced`(一个布尔值,指示是否创建不平衡的数据集)。`get_dataset`函数的返回值是三个对象:`train_dataset`(用于训练的数据集),`test_dataset`(用于测试的数据集),以及`user_groups`(一个字典或类似的结构,它包含每个用户/客户端的数据索引)。
if verbose:            #检查是否应该输出额外的调试信息
  plot_client_distribution(user_groups, train_dataset)
#如果`verbose`为真,这行代码将执行`plot_client_distribution`函数,该函数生成一个图表,展示了每个客户端拥有的训练数据的分布情况。这对于可视化数据在不同用户间的分布和确认数据是否如期望那样分配(例如,平衡或不平衡)非常有用。

这部分代码关键的作用是从整体数据集中创建训练和测试数据集,并且根据实验的需要,分配给不同的用户或客户端。
如果设置为详细模式,还会输出每个客户端数据的分布情况,这有助于理解数据是如何分布的,对于评估联邦学习算法在不同数据分布下的性能非常重要。
Training

# training
train_loss, train_accuracy = [], []
#初始化两个空列表来存储训练损失和训练准确率
test_acc_list, test_loss_list = [], []
#初始化两个空列表来存储测试准确率和测试损失。
for epoch in range(1, args.epochs+1):
#开始一个循环,从1到指定的轮数args.epochs
    local_weights = []
    local_losses = []
#为每个epoch初始化两个空列表来存储每个客户端的权重和损失。
    print(f'Epoch: {epoch} \n')
#打印当前epoch
    model.train()
#确保模型处于训练模式。
    m = max(int(args.frac * args.num_users), 1) # num of users, at least 1
#计算这个epoch中将被训练的用户数。args.frac是用户参与训练的比例。
    idxs_users = np.random.choice(range(args.num_users), m, replace=False) 
#随机选择m个用户进行本地更新。
    for idx in idxs_users:        #循环中每个被选中的用户会进行本地更新
        local_model = LocalUpdate(args=args, dataset=train_dataset, idxs=user_groups[idx],logger=logger)
#为每个选中的用户创建一个LocalUpdate对象。
        w, loss = local_model.update_weights(model=copy.deepcopy(model), # pass the global model to the clients
                                             global_round=epoch)
#调用update_weights方法,传入当前的全局模型和epoch,返回更新后的权重和损失。
        print('| Client : {} | Average Loss: {:.4f} '.format(idx, loss))
        local_weights.append(copy.deepcopy(w))
#将得到的权重深拷贝并添加到local_weights列表中。
        local_losses.append(copy.deepcopy(loss))
#将损失深拷贝并添加到local_losses列表中。
    # compute global weights (average of local weights)计算全局权重:
    if unbalanced:
        global_weights = weighted_average_weights(local_weights, user_groups, idxs_users)
    else:
        global_weights = average_weights(local_weights)
#如果数据是不平衡的,使用weighted_average_weights函数计算全局权重;如果数据是平衡的,使用average_weights函数计算全局权重。

# update weights of the global model
    model.load_state_dict(global_weights)
#加载计算出的全局权重到全局模型中。
    # compute average loss
    loss_avg = sum(local_losses) / len(local_losses)
#计算并存储这个epoch的平均训练损失。
    train_loss.append(loss_avg)

    model.eval()
#将模型切换到评估模式,这通常在模型评估或测试时使用,确保某些特定层(如Dropout和BatchNorm)以评估模式运行。
    # calculate avg training accuracy over all users at every epoch          代码计算所有用户的平均训练准确率:
    list_acc, list_loss = [], []
    for client in range(args.num_users): 
        local_model = LocalUpdate(args=args, dataset=train_dataset, idxs=user_groups[client],logger=logger)
#循环遍历每个用户,创建一个新的LocalUpdate实例。
        acc, loss = local_model.inference(model=model)          #评估当前全局模型在每个客户端的数据上的性能。
#acc和loss是每个客户端上模型的准确率和损失。
        list_acc.append(acc)
        list_loss.append(loss)
#将每个客户端的准确率添加到list_acc中,损失添加到list_loss中。
    train_accuracy.append(sum(list_acc)/len(list_acc))# 计算并存储所有客户端的平均训练准确率。
    print(f'\nAverage training statistics (global epoch) : {epoch}') 
    print(f'|---- Trainig Loss : {np.mean(np.array(train_loss))}')
    print('|---- Training Accuracy: {:.2f}% \n'.format(100*train_accuracy[-1]))
#打印平均训练损失和平均训练准确率
    test_loss, test_acc = test(args, model,global_model, test_dataset)
#在全局模型上调用test函数来评估模型在整个测试集上的性能。
    test_acc_list.append(test_acc)
    test_loss_list.append(test_loss)
#将测试准确率和损失添加到相应的列表中以便跟踪。
    print('%d-th round' %epoch)
    print('average train loss %0.3g | test loss %0.3g | test acc: %0.3f' % (loss / num_selected, test_loss, acc)) 
#打印出当前轮数、平均训练损失、测试损失和测试准确率

Storing the Result

dict_ = {'train_acc' : train_accuracy, 'train_loss' : train_loss}
#创建一个字典dict_,其中包含两个键:'train_acc'和'train_loss'。这些键对应的值分别是train_accuracy和train_loss列表,它们存储了训练过程中记录的准确率和损失值。
df = pd.DataFrame(dict_) 
#使用pandas库将上面创建的字典dict_转换成一个DataFrame对象df。
iid = ['iid' if args.iid else 'nonIID']
#根据args.iid的值确定数据是否是独立同分布(iid)。如果args.iid为真,则列表iid包含字符串'iid',否则包含'nonIID'。
unb = ['unbalanced' if unbalanced and not args.iid else 'balanced' ]
#这行代码确定数据集是否不平衡。如果unbalanced为真且args.iid为假,则列表unb包含字符串'unbalanced',表示数据集是不平衡的;否则,包含字符串'balanced'。
bs = args.local_bs 
#从args对象中获取本地批大小并赋值给变量bs
filename = f"fedAVG_{iid}_{unb}_{args.norm}{bs}_{args.epochs}_lr_{args.lr}_optimizer_{args.optimizer}"
#构建用于保存结果文件的文件名。这个文件名包含了实验的一些关键参数,如是否iid、是否平衡、正则化类型、批大小、轮数、学习率和优化器类型。这里使用了Python的f-string来插入变量值。
df = pd.DataFrame(dict_) 
#当pd.DataFrame(dict_)被执行时,pandas会将dict_中的数据转换成DataFrame格式,其中字典的键变成了数据表的列标题,字典的值变成了表中的数据行。这样,每一列就代表了一种类型的数据(例如,训练准确率或训练损失),每一行则对应于一个训练周期(或epoch)的数据。
df.to_csv('/content/drive/MyDrive/FL2022/FedAVG/Results/'+filename+'.csv', encoding='utf-8')
#将DataFrame对象df保存为CSV文件。文件路径和名称由前面定义的filename变量确定。

#Showing the Results
print(f' \n Results after {args.epochs} global rounds of training:')
#打印一个消息,说明接下来将展示训练完成后的结果。
print("|---- Avg Train Accuracy: {:.2f}%".format(100*train_accuracy[-1])) 
#使用str.format()方法打印训练过程中最后一个记录体现的平均准确率值,并将其转换成百分比格式显示,保留两位小数。

其他算法待补充……

其他论文分析

待补充

最新领域进展

待补充

论文改进方向

待补充

posted @ 2024-05-25 21:04  岁月月宝贝  阅读(170)  评论(0编辑  收藏  举报