Python 联邦学习(二)
原文:
annas-archive.org/md5/59885cc35c2ca63f9f092895c4c3c9b9译者:飞龙
第六章:运行联邦学习系统并分析结果
在本章中,你将运行在前几章中讨论过的联邦学习(FL)系统,并分析系统行为和聚合模型的输出结果。我们将首先解释 FL 系统组件的配置,以便正确运行系统。基本上,在安装我们 GitHub 示例提供的简单 FL 系统之后,你首先需要选择运行数据库和聚合模块的服务器机器或实例。然后,你可以运行代理程序连接到已经运行的聚合器。每个代理端配置中都需要正确设置聚合器的 IP 地址。此外,还有一个模拟模式,这样你可以在同一台机器或笔记本电脑上运行所有组件,仅测试 FL 系统的功能。在成功运行 FL 系统的所有模块后,你将能够看到在数据库服务器和代理端设置的路径下创建的数据文件夹和数据库。你将能够检查本地和全局模型,包括训练和聚合的模型,以便你可以从数据文件夹中下载最新的或表现最佳的模型。
此外,你还可以看到在最小引擎和图像分类上运行 FL 系统的示例。通过审查生成的模型和性能数据,你可以理解聚合算法以及聚合器和代理之间模型的实际交互。
在本章中,我们将涵盖以下主要主题:
-
配置和运行 FL 系统
-
理解最小示例运行时会发生什么
-
运行图像分类和分析结果
技术要求
本章中介绍的所有代码文件都可以在 GitHub 上找到(github.com/tie-set/sim…
重要注意事项
你可以使用代码文件用于个人或教育目的。请注意,我们不会支持商业部署,也不会对使用代码造成的任何错误、问题或损害负责。
配置和运行 FL 系统
配置 FL 系统及其环境相对简单,请遵循下一小节中的说明。
安装 FL 环境
首先,要运行前一章中讨论的 FL 系统,请使用以下命令将以下存储库克隆到你想要运行 FL 的机器上:
git clone https://github.com/tie-set/simple-fl
一旦完成克隆过程,请在命令行中将目录更改为simple-fl文件夹。模拟运行可以使用一台机器或使用多个系统进行。为了在一台或多台机器上运行 FL 过程,这些机器包括 FL 服务器(聚合器)、FL 客户端(代理)和数据库服务器,你应该创建一个conda虚拟环境并激活它。
要在 macOS 中创建conda环境,你需要输入以下命令:
conda env create -n federatedenv -f ./setups/federatedenv.yaml
如果你使用的是 Linux 机器,你可以使用以下命令创建conda环境:
conda env create -n federatedenv -f ./setups/federatedenv_linux.yaml
然后,在运行代码时激活conda环境federatedenv。为了你的信息,federatedenv.yaml和federatedenv_linux.yaml文件可以在simple-fl GitHub 仓库的setups文件夹中找到,并包含本书中代码示例所使用的库。
如同 GitHub 仓库的README文件所述,运行此程序主要需要三个组件:数据库服务器、聚合器和代理(们)。如果你想在单台机器上运行模拟,你只需在该机器上安装一个conda环境(federatedenv)即可。
如果你想要创建一个分布式环境,你需要在你想要使用的所有机器上安装conda环境,例如在云实例上的数据库服务器、聚合器服务器以及本地客户端机器。
现在 FL 过程的安装过程已经准备就绪,让我们继续使用配置文件来配置 FL 系统。
使用 JSON 文件为每个组件配置 FL 系统
首先,编辑提供的 GitHub 仓库setups文件夹中的配置 JSON 文件。这些 JSON 文件被数据库服务器、聚合器和代理读取以配置它们的初始设置。再次提醒,配置细节如下所述。
config_db.json
config_db.json文件用于配置数据库服务器。使用以下信息来正确操作服务器:
-
db_ip:数据库服务器的 IP 地址(例如,localhost)。如果你想在云实例上运行数据库服务器,例如在亚马逊网络服务(AWS)EC2 实例上,你可以指定实例的私有 IP 地址。 -
db_socket:数据库和聚合器之间使用的套接字编号(例如,9017)。 -
db_name:SQLite 数据库的名称(例如,sample_data)。 -
db_data_path:SQLite 数据库的路径(例如,./db)。 -
db_model_path:保存所有./db/models的目录路径。
config_aggregator.json
config_aggregator.json文件用于在 FL 服务器中配置聚合器。使用以下信息来正确操作聚合器:
-
aggr_ip:聚合器的 IP 地址(例如,localhost)。如果你想在云实例上运行聚合器服务器,例如 AWS EC2 实例,你可以指定实例的私有 IP 地址。 -
db_ip:数据库服务器的 IP 地址(例如,localhost)。如果你想要连接到托管在不同云实例上的数据库服务器,你可以指定数据库实例的公网 IP 地址。如果你将数据库服务器托管在与聚合器实例相同的云实例上,你可以指定实例的相同私有 IP 地址。 -
reg_socket:代理首次连接到聚合器时使用的套接字编号(例如,8765)。 -
recv_socket: 用于从代理上传本地模型或轮询聚合器的套接字编号。代理将通过与聚合器通信来学习此套接字信息(例如,7890)。 -
exch_socket: 当使用推送方法时,用于从聚合器将全局模型发送回代理的套接字编号。代理将通过与聚合器通信来学习此套接字信息(例如,4321)。 -
db_socket: 数据库和聚合器之间使用的套接字编号(例如,9017)。 -
round_interval: 代理检查是否有足够模型以启动聚合步骤的时间间隔(单位:秒;例如,5)。 -
aggregation_threshold: 需要收集的本地模型百分比,以启动聚合步骤(例如,0.85)。 -
polling: 指定是否使用轮询方法的标志。如果标志为1,则使用轮询方法;如果标志为0,则使用推送方法。此值需要在聚合器和代理之间相同。
config_agent.json
config_agent.json文件用于在 FL 客户端中配置代理。使用以下信息正确操作代理:
-
aggr_ip: 聚合器服务器的 IP 地址(例如,localhost)。如果你想连接到托管在云实例上的聚合器服务器,例如 AWS EC2 实例,你可以指定聚合器实例的公网 IP 地址。 -
reg_socket: 代理首次加入聚合器时使用的套接字编号(例如,8765)。 -
model_path: 代理机器中本地目录的路径,用于保存本地和全局模型以及一些状态信息(例如,./data/agents)。 -
local_model_file_name: 在代理机器中保存本地模型的文件名(例如,lms.binaryfile)。 -
global_model_file_name: 在代理机器中保存全局模型的文件名(例如,gms.binaryfile)。 -
state_file_name: 存储代理状态的文件名(例如,state)。 -
init_weights_flag: 如果权重以特定值初始化,则为1,否则为0,其中权重以零初始化。 -
polling: 指定是否使用轮询方法的标志。如果标志为1,则使用轮询方法;如果标志为0,则使用推送方法。此值需要在聚合器和代理之间相同。
现在,可以使用本节中解释的配置文件配置 FL 系统。接下来,你将在 FL 服务器端运行数据库和聚合器。
在 FL 服务器上运行数据库和聚合器
在本节中,你将在 FL 服务器端配置数据库和聚合器。然后,你将编辑simple-fl GitHub 仓库中setups文件夹中的配置文件。之后,你将首先运行pseudo_db,然后运行server_th,如下所示:
python -m fl_main.pseudodb.pseudo_db
python -m fl_main.aggregator.server_th
重要注意事项
如果数据库服务器和聚合服务器运行在不同的机器上,您需要指定数据库服务器或聚合服务器实例的 IP 地址。数据库服务器的 IP 地址可以在setups文件夹中的config_aggregator.json文件中修改。此外,如果数据库和聚合实例都在公共云环境中运行,这些服务器的配置文件 IP 地址需要是私有 IP 地址。代理需要使用公共 IP 地址和连接套接字(端口号)连接到聚合器,并且连接套接字(端口号)需要打开以接受入站消息。
在您启动数据库和聚合服务器后,您将在控制台中看到如下消息:
# Database-side Console Example
INFO:root:--- Pseudo DB Started ---
在控制台的聚合端,您将看到如下内容:
# Aggregator-side Console Example
INFO:root:--- Aggregator Started ---
在这个聚合服务器背后,模型合成模块每 5 秒运行一次,它开始检查收集到的本地模型数量是否超过了聚合阈值定义的数量。
我们现在已经运行了数据库和聚合模块,并准备好使用 FL 客户端运行最小示例。
使用 FL 客户端运行最小示例
在上一章中,我们讨论了将本地 ML 引擎集成到 FL 系统中的方法。在这里,我们使用一个没有实际训练数据的最小样本来尝试运行已经讨论过的 FL 系统。这个最小示例可以作为实现任何本地分布式 ML 引擎时的模板。
在运行最小示例之前,您应该检查数据库和聚合服务器是否已经启动。然后,运行以下命令:
python -m examples.minimal.minimal_MLEngine
在这种情况下,仅连接了一个具有最小 ML 引擎的代理。因此,聚合会在默认代理上传本地模型时发生。
注意,如果聚合服务器运行在不同的机器上,您需要指定聚合服务器或实例的公共 IP 地址。聚合服务器的 IP 地址可以在setups文件夹中的config_agent.json文件中修改。我们还建议在云实例中运行聚合器和数据库时将polling标志设置为1。
图 6.1 展示了运行数据库服务器时的控制台屏幕示例。
![图 6.1 – 数据库端控制台示例
图 6.1 – 数据库端控制台示例
图 6.2 展示了运行聚合器时的控制台屏幕示例:
![图 6.2 – 聚合端控制台示例
图 6.2 – 聚合端控制台示例
图 6.3 展示了运行代理时的控制台屏幕示例。
![图 6.3 – 代理端控制台示例
图 6.3 – 代理端控制台示例
现在我们知道了如何运行所有 FL 组件:数据库、聚合器和代理。在下一节中,我们将检查运行 FL 系统时如何生成输出。
数据和数据库文件夹
运行 FL 系统后,您将注意到数据库文件夹和数据文件夹是在您在数据库和代理的配置文件中指定的位置创建的。
例如,db文件夹是在db_data_path下创建的,在config_db.json文件中写入。在数据库文件夹中,您将找到 SQLite 数据库,例如model_data12345.db,其中存储了本地和集群全局模型的元数据,以及一个包含所有由代理上传的实际本地模型和由聚合器创建的全局模型的models文件夹。
图 6.4 展示了在db文件夹中存储的二进制文件格式下的 SQLite 数据库和 ML 模型文件,这些文件是通过运行最小示例代码创建的:
图 6.4 – 存储在 db 文件夹中的 SQLite 数据库和 ML 模型文件的二进制文件格式
data文件夹位于代理设备上的model_path位置,这是一个在config_agent.json中定义的字符串值。在最小示例的运行示例中,以下文件在data/agents/default-agent文件夹下创建:
-
lms.binaryfile: 包含由代理创建的本地模型的二进制文件 -
gms.binaryfile: 包含由聚合器创建的全局模型的二进制文件,发送回代理 -
state: 一个包含整数值的文件,表示客户端自身的状态
图 6.5 展示了代理端数据的结构,包括以二进制文件格式表示的全局和本地 ML 模型,以及反映 FL 客户端状态的文件:
图 6.5 – 包含全局和本地 ML 模型以及以二进制文件格式表示的客户端状态的代理数据
现在我们已经了解了关键数据,如全局和本地模型,存储的位置。接下来,我们将更详细地查看数据库,使用 SQLite。
SQLite 数据库
在db文件夹中创建的数据库可以使用任何工具查看,以显示可以打开***.db格式文件的 SQLite 数据库。数据库表在以下章节中定义。
数据库中的本地模型
图 6.6 展示了与上传的本地模型相关的示例数据库条目,其中每个条目列出了本地模型 ID、模型生成的时间、上传本地模型的代理 ID、轮次信息、性能指标和数据样本数量:
图 6.6 – 与上传的本地模型相关的示例数据库条目
数据库中的集群模型
图 6.7显示了与上传的集群模型相关的样本数据库条目,其中每个条目列出了集群模型 ID、模型创建时间、创建此集群模型的聚合器 ID、轮次信息和数据样本数量:
![图 6.7 – 与上传的集群模型相关的样本数据库条目
图 6.7 – 与上传的集群模型相关的样本数据库条目
现在我们已经学会了如何使用最小示例配置和运行联邦学习系统,以及如何检查结果。在下一节中,你将了解联邦学习系统的行为以及当运行最小示例时会发生什么。
理解最小示例运行时发生的情况
逐步理解整个联邦学习系统的行为将有助于你设计启用联邦学习的应用程序,并进一步增强联邦学习系统本身。让我们首先看看当我们只运行一个代理时会发生什么,通过打印代理和聚合器模块的一些过程。
只运行一个最小代理
在运行数据库和聚合器服务器之后,让我们运行最小代理并看看会发生什么。当代理以最小机器学习引擎启动时,你将在代理控制台中看到以下消息:
# Agent-side Console Example
INFO:root:--- This is a minimal example ---
INFO:root:--- Agent initialized —
INFO:root:--- Your IP is xxx.xxx.1.101 ---
当代理初始化用于联邦学习的模型时,它会显示这条消息,如果你查看state文件,它已经进入了发送状态,当联邦学习客户端启动时,将触发向聚合器发送模型:
# Agent-side Console Example
INFO:root:--- Model template generated ---
INFO:root:--- Local (Initial/Trained) Models saved ---
INFO:root:--- Client State is now sending ---
然后,在用start_fl_client函数启动客户端之后,参与消息被发送到聚合器。以下是发送到聚合器的参与消息:
[
<AgentMsgType.participate: 0>, # Agent Message Type
'A89fd1c2d9*****', # Agent ID
'047b18ddac*****', # Model ID
{
'model1': array([[1, 2, 3], [4, 5, 6]]),
'model2': array([[1, 2], [3, 4]])
}, # ML Models
True, # Init weights flag
False, # Simulation flag
0, # Exch Port
1645141807.846751, # Generated Time of the models
{'accuracy': 0.0, 'num_samples': 1}, # Meta information
'xxx.xxx.1.101' # Agent's IP Address
]
发送到聚合器的参与消息包括消息类型、代理 ID、模型 ID、带有 NumPy 的机器学习模型、初始化权重标志、模拟标志、交换端口号、模型生成时间以及性能指标和代理的 IP 地址等元信息。
代理收到聚合器发送的确认连接的欢迎消息,其中还包含以下信息:
# Agent-side Console Example
INFO:root:--- Init Response: [
<AggMsgType.welcome: 0>, # Message Type
'4e2da*****', # Aggregator ID
'23487*****', # Model ID
{'model1': array([[1, 2, 3], [4, 5, 6]]),
'model2': array([[1, 2], [3, 4]])}, # Global Models
1, # FL Round
'A89fd1c2d9*****', # Agent ID
'7890', # exch_socket number
'4321' # recv_socket number
] ---
在聚合器端,在此代理向聚合器发送参与消息后,聚合器确认参与并将此初始模型推送到数据库:
# Aggregator-side Console Example
INFO:root:--- Participate Message Received ---
INFO:root:--- Model Formats initialized, model names: ['model1', 'model2'] ---
INFO:root:--- Models pushed to DB: Response ['confirmation'] ---
INFO:root:--- Global Models Sent to A89fd1c2d9***** ---
INFO:root:--- Aggregation Threshold (Number of agents needed for aggregation): 1 ---
INFO:root:--- Number of collected local models: 0 ---
INFO:root:--- Waiting for more local models to be collected ---
在数据库服务器端控制台中,你还可以检查本地模型是否从聚合器发送过来,并且模型已保存在数据库中:
# DB-side Console Example
INFO:root:Request Arrived
INFO:root:--- Model pushed: ModelType.local ---
INFO:root:--- Local Models are saved ---
在聚合器将全局模型发送回代理后,代理接收并保存它,并将客户端状态从等待 _gm更改为gm_ready,表示全局模型已准备好在本地重新训练:
# Agent-side Console Example
INFO:root:--- Global Model Received ---
INFO:root:--- Global Models Saved ---
INFO:root:--- Client State is now gm_ready ---
这里是聚合器发送给代理的消息,包括全局模型。消息内容包含消息类型、聚合器 ID、集群模型 ID、联邦学习轮次和带有 NumPy 的机器学习模型:
[
<AggMsgType.sending_gm_models: 1>, # Message Type
'8c6c946472*****', # Aggregator ID
'ab633380f6*****', # Global Model ID
1, # FL Round Info
{
'model1': array([[1., 2., 3.],[4., 5., 6.]]),
'model2': array([[1., 2.],[3., 4.]])
} # ML models
]
然后,代理读取全局模型,以便使用它们进行本地训练,并将客户端状态更改为训练:
# Agent-side Console Example
INFO:root:--- Global Models read by Agent ---
INFO:root:--- Client State is now training ---
INFO:root:--- Training ---
INFO:root:--- Training is happening ---
INFO:root:--- Training is happening ---
INFO:root:--- Training Done ---
INFO:root:--- Local (Initial/Trained) Models saved ---
INFO:root:--- Client State is now sending ---
INFO:root:--- Local Models Sent ---
INFO:root:--- Client State is now waiting_gm ---
INFO:root:--- Polling to see if there is any update (shown only when polling) ---
INFO:root:--- Global Model Received ---
INFO:root:--- The global models saved ---
在前面的本地训练过程之后,代理继续将训练好的本地模型发送到聚合器,并将客户端状态更改为waiting_gm,这意味着它正在等待具有轮询机制的全球模型。
这里是发送给聚合器的消息,作为训练好的本地模型消息。消息内容包含消息类型、代理 ID、模型 ID、机器学习模型、模型的生成时间以及如性能数据之类的元数据:
[
<AgentMsgType.update: 1>, # Agent's Message Type
'a1031a737f*****', # Agent ID
'e89ccc5dc9*****', # Model ID
{
'model1': array([[1, 2, 3],[4, 5, 6]]),
'model2': array([[1, 2],[3, 4]])
}, # ML Models
1645142806.761495, # Generated Time of the models
{'accuracy': 0.5, 'num_samples': 1} # Meta information
]
然后,在聚合器中,在本地模型被推送到数据库后,它显示了缓冲区中的变化,收集到的本地模型数量从 0 增加到 1,从而表明已收集足够的本地模型以开始聚合:
# Aggregator-side Console Example
INFO:root:--- Models pushed to DB: Response ['confirmation'] ---
INFO:root:--- Local Model Received ---
INFO:root:--- Aggregation Threshold (Number of agents needed for aggregation): 1 ---
INFO:root:--- Number of collected local models: 1 ---
INFO:root:--- Enough local models are collected. Aggregation will start. ---
然后,第一轮的聚合发生,集群全局模型形成,在收到代理的轮询消息后推送到数据库,并发送给代理。聚合器也可以通过推送方法将消息推回代理:
# Aggregator-side Console Example
INFO:root:Round 1
INFO:root:Current agents: [{'agent_name': 'default_agent', 'agent_id': 'A89fd1c2d9*****', 'agent_ip': 'xxx.xxx.1.101', 'socket': 7890}]
INFO:root:--- Cluster models are formed ---
INFO:root:--- Models pushed to DB: Response ['confirmation'] ---
INFO:root:--- Global Models Sent to A89fd1c2d9***** ---
在数据库服务器端,集群全局模型被接收并推送到数据库:
# DB-side Console Example
INFO:root:Request Arrived
INFO:root:--- Model pushed: ModelType.cluster ---
INFO:root:--- Cluster Models are saved ---
在生成并保存了即将到来的联邦学习轮次的集群模型后,本节中的此过程会重复进行,并且联邦学习轮次将继续使用此交互机制进行。
如果您查看本地和集群全局模型,它们如下所示:
{
'model1': array([[1, 2, 3],[4, 5, 6]]),
'model2': array([[1, 2],[3, 4]])
}
这意味着即使发生聚合,也始终只使用一个固定的模型,因此全局模型与初始模型完全相同,因为这里使用了虚拟训练过程。
我们现在将查看在下一节中运行两个最小代理的结果。
运行两个最小代理
在数据库和聚合器服务器运行的情况下,您可以使用simple-fl/examples/minimal文件夹中的minimal_MLEngine.py文件运行许多代理。
您应该通过指定聚合器的 IP 地址来从不同的本地机器运行两个单独的代理,以将那些代理与最小机器学习示例连接起来。
您也可以通过为单个代理指定不同的端口号来从同一台机器运行多个代理以进行模拟。
在 GitHub 上simple-fl存储库中提供的代码中,您可以通过使用以下命令运行多个代理:
python -m examples.minimal.minimal_MLEngine [simulation_flag] [gm_recv_port] [agent_name]
要进行模拟,应将simulation_flag设置为1。gm_recv_port是从聚合器接收全局模型的端口号。聚合器将通过参与消息的响应通知代理端口号。此外,agent_name是本地代理的名称和存储状态和模型文件的目录名称。这对于每个代理都需要是唯一的。
例如,您可以使用以下命令运行第一个和第二个代理:
# First agent
python -m examples.minimal.minimal_MLEngine 1 50001 a1
# Second agent
python -m examples.minimal.minimal_MLEngine 1 50002 a2
如果需要,您可以编辑setups文件夹中的配置 JSON 文件。在这种情况下,agg_threshold被设置为1。
当您在运行多个代理的最小示例的数据库服务器上运行模拟时,控制台屏幕将类似于图 6.1中的屏幕。
图 6.8 展示了在聚合器服务器上运行使用虚拟 ML 模型的最小示例的模拟控制台屏幕:
图 6.8 – 示例:运行最小示例的聚合器端控制台连接两个代理
图 6.9 展示了在其中一个代理中运行使用虚拟 ML 模型的最小示例的模拟控制台屏幕:
图 6.9 – 示例:代理 1 的控制台运行使用虚拟 ML 模型的最小示例
图 6.10 展示了在另一个代理中运行使用虚拟 ML 模型的最小示例的模拟控制台屏幕:
图 6.10 – 示例:代理 2 的控制台运行使用虚拟 ML 模型的最小示例
现在我们知道了如何使用两个代理运行最小示例。为了进一步了解使用此示例的 FL 流程,我们将回答以下问题:
-
对于简单情况,聚合是否已经正确完成?
-
FedAvg算法是否被正确应用? -
聚合器阈值是否与连接的代理一起工作?
在运行和连接两个代理后,聚合器将等待接收来自两个连接代理的两个模型,如下所示:
# Aggregator-side Console Example
INFO:root:--- Aggregation Threshold (Number of agents needed for aggregation): 2 ---
INFO:root:--- Number of collected local models: 0 ---
INFO:root:--- Waiting for more local models to be collected ---
在这种情况下,聚合器阈值在 setups 文件夹中的 config_aggregator.json 文件中设置为 1.0,因此聚合器需要收集所有连接代理的模型,这意味着它需要从连接到聚合器的所有代理那里接收本地 ML 模型。
然后,它从其中一个代理那里接收一个模型,收集到的本地模型数量增加到 1。然而,由于聚合器仍然缺少一个本地模型,它还没有开始聚合:
# Aggregator-side Console Example
INFO:root:--- Local Model Received ---
INFO:root:--- Aggregation Threshold (Number of agents needed for aggregation): 2 ---
INFO:root:--- Number of collected local models: 1 ---
INFO:root:--- Waiting for more local models to be collected ---
在代理端,在将本地模型发送到聚合器后,它将等待聚合器创建集群全局模型并将其发送回代理。这样,您可以在代理端同步 FL 流程,并在全局模型发送回代理并准备重新训练时自动化本地训练过程。
在聚合器收到另一个本地模型后,收集到的模型数量足够开始聚合过程:
# Aggregator-side Console Example
INFO:root:--- Local Model Received ---
INFO:root:--- Aggregation Threshold (Number of agents needed for aggregation): 2 ---
INFO:root:--- Number of collected local models: 2 ---
INFO:root:--- Enough local models are collected. Aggregation will start. ---
它最终将开始第一轮的聚合,如下所示:
# Aggregator-side Console Example
INFO:root:Round 1
INFO:root:Current agents: [{'agent_name': 'a1', 'agent_id': '1f503*****', 'agent_ip': 'xxx.xxx.1.101', 'socket': 50001}, {'agent_name': 'a2', 'agent_id': '70de8*****', 'agent_ip': 'xxx.xxx.1.101', 'socket': 50002}]
INFO:root:--- Cluster models are formed ---
INFO:root:--- Models pushed to DB: Response ['confirmation'] ---
INFO:root:--- Global Models Sent to 1f503***** ---
INFO:root:--- Global Models Sent to 70de8***** ---
在这里,让我们看看在本地训练的代理端 ML 模型:
# Agent 1's Console Example
INFO:root:--- Training ---
INFO:root:--- Training is happening ---
INFO:root:--- Training Done ---
Trained models: {'model1': array([[1, 2, 3],
[4, 5, 6]]), 'model2': array([[1, 2],
[3, 4]])}
INFO:root:--- Local (Initial/Trained) Models saved ---
此外,让我们看看另一个代理在本地训练的 ML 模型:
# Agent 2's Console Example
INFO:root:--- Training ---
INFO:root:--- Training is happening ---
INFO:root:--- Training Done ---
Trained models: {'model1': array([[3, 4, 5],
[6, 7, 8]]), 'model2': array([[3, 4],
[5, 6]])}
INFO:root:--- Local (Initial/Trained) Models saved ---
与从代理 1 和 2 发送到聚合器的模型一样,如果 FedAvg 被正确应用,全局模型应该是这两个模型的平均值。在这种情况下,代理 1 和 2 的数据样本数量相同,因此全局模型应该是这两个模型的平均值。
因此,让我们看看在聚合器中生成的全局模型:
# Agent 1 and 2's Console Example
Global Models: {'model1': array([[2., 3., 4.],
[5., 6., 7.]]), 'model2': array([[2., 3.],
[4., 5.]])}
收到的模型是两个本地模型的平均值,因此平均已经正确执行。
数据库和数据文件夹是在代理配置文件中指定的model_path下创建的。您可以使用 SQLite 查看器应用程序查看数据库值,并基于模型 ID 查找一些模型。
现在我们已经通过最小示例运行了解了正在发生的事情,在下一节中,我们将使用一个卷积神经网络(CNN)的图像分类模型运行一个真实的机器学习应用。
运行图像分类和分析结果
本例演示了使用此 FL 框架进行图像分类任务的使用。我们将使用著名的图像数据集 CIFAR-10(URL:www.cs.toronto.edu/~kriz/cifar.html),以展示 ML 模型如何通过 FL 过程随时间增长。然而,这个例子只是为了使用我们之前讨论的 FL 系统,并不专注于最大化图像分类任务的性能。
准备 CIFAR-10 数据集
以下是与数据集大小、训练和测试数据、类别数量和图像大小相关的信息:
-
数据集大小:60,000 张图像
-
训练数据:50,000 张图像
-
测试数据:10,000 张图像
-
类别数量:10(飞机、汽车、鸟、猫、鹿、狗、青蛙、马、船和卡车)
-
每个类别有 6,000 张图像
-
图像大小:32x32 像素,彩色
图 6.11展示了数据集中 10 个不同类别的样本图片集合,每个类别有 10 个随机图像:
![图 6.11 – 数据集中的类别以及每个类别的 10 个随机图像(图像来自 www.cs.toronto.edu/~kriz/cifar…]
图 6.11 – 数据集中的类别以及每个类别的 10 个随机图像(图像来自 www.cs.toronto.edu/~kriz/cifar…
现在数据集已经准备好了,我们将探讨用于 FL 过程的 CNN 模型。
用于图像分类的 FL 机器学习模型
这是本图像分类示例中使用的 CNN 模型机器学习模型架构的描述。要了解更多关于 CNN 的信息,您可以找到许多有用的学习资源,例如cs231n.github.io/convolutional-networks/:
-
Conv2D
-
MaxPool2D(最大池化)
-
Conv2D
-
3 个全连接层
定义 CNN 模型的脚本已经设计好,可以在 GitHub 上的simple-fl仓库中的examples/image_classification目录下的cnn.py中找到。接下来,我们将使用 FL 系统运行图像分类应用。
如何使用 CNN 运行图像分类示例
如本章开头所述的安装步骤中提到,我们首先使用 federatedenv 安装必要的库,然后安装 torch 和 torchvision:
pip install torch
pip install torchvision
您可以通过 GitHub 上 simple-fl 仓库的 setups 文件夹中的 JSON 配置文件来配置许多设置。有关更多详细信息,您可以在我们的 setups 文档中阅读配置文件的一般描述 (github.com/tie-set/simple-fl/tree/master/setups)。
首先,您可以运行两个代理。您可以通过指定适当的端口号来增加在同一设备上运行的代理数量。
如您所知,您首先可以运行数据库和聚合器:
# FL server side
python -m fl_main.pseudodb.pseudo_db
python -m fl_main.aggregator.server_th
然后,启动第一个和第二个代理以运行图像分类示例:
# First agent
python -m examples.image_classification.classification
_engine 1 50001 a1
# Second agent
python -m examples.image_classification.classification
_engine 1 50002 a2
为了模拟实际的 FL 场景,每个代理可访问的训练数据量可以限制为特定数量。这应该在 classification_engine.py 中的 num_training_data 变量中指定。默认情况下,每轮使用 8,000 张图像(2,000 个批次)。
现在我们可以使用 CNN 模型运行两个代理来测试 FL 过程,让我们通过运行图像分类示例来进一步查看结果。
使用 CNN 进行图像分类的运行评估
性能数据(每个本地模型集群模型的准确率)存储在我们的数据库中。您可以通过访问相应的 .db 文件来查看性能历史。
DataManager 实例(在 ic_training.py 中定义)有一个函数可以返回一批图像及其标签(get_random_images)。您可以使用此函数通过训练的 CNN 在特定图像上显示实际标签和预测标签。
图 6.12 展示了我们在自己一侧进行的实验运行的学习性能图;当您使用自己的设置运行时,结果可能会有所不同:
图 6.12 – 使用 CNN 进行图像分类的 FL 实验运行的学习性能图
再次,因为我们这里只使用两个代理,所以结果看起来略有不同。然而,通过适当的超参数设置、数据量和代理数量,您将能够执行一个产生有意义的 FL 评估,我们希望您自己探索,因为这里的重点是只是如何将实际的 ML 模型连接到这个 FL 环境中。
运行五个代理
您可以通过在终端中指定不同的端口号和代理名称来轻松运行五个代理进行图像分类应用。结果看起来与我们在上一节中讨论的相似,只是连接了实际的 ML 模型(在这种情况下,聚合的 ML 模型是 CNN)。运行五个代理后,数据和数据库文件夹看起来像 图 6.13:
![Figure 6.13 – Results to be stored in each folder with the agent’s unique name
图 6.13 – 每个文件夹中存储的具有代理唯一名称的结果
图 6.14显示了数据库中上传的本地模型,包括本地模型 ID、模型生成时间、上传本地模型的代理 ID、性能指标和轮次信息:
![Figure 6.14 – Information about the local models in the database
图 6.14 – 数据库中本地模型的信息
如果你查看图6.14中的数据库,可以看到五个代理收集的具有本地性能数据的五个模型。
对于每一轮,这五个本地模型被聚合以生成一个集群全局模型,正如数据库中cluster_models表所示,如图6.15所示。存储集群模型的数据库包含有关集群模型 ID、模型生成时间、创建集群模型的聚合器 ID 以及轮次信息:
![Figure 6.15 – Information about the cluster models in the database
图 6.15 – 数据库中集群模型的信息
通过这种方式,你可以连接尽可能多的代理。优化本地机器学习算法的设置以获得 FL 系统中性能最佳的联邦模型取决于你。
摘要
在本章中,我们详细讨论了联邦学习系统的执行情况以及系统将如何根据聚合器和代理之间的交互行为。基于控制台示例结果的逐步解释指导你理解FedAvg算法的聚合过程。此外,图像分类示例展示了 CNN 模型如何连接到联邦学习系统,以及联邦学习过程如何通过聚合提高准确性,尽管这并没有优化以最大化训练结果,而是简化以验证使用 CNN 的集成。
通过本章所学的内容,你将能够设计自己的联邦学习应用,整合本书中介绍的原则和框架,并且能够评估你自己的联邦学习行为,以查看整个联邦学习过程和模型聚合是否正确且一致地进行。
在下一章中,我们将介绍各种模型聚合方法,并展示联邦学习如何与这些聚合算法良好地协同工作。
第七章:模型聚合
在第三章的“模型聚合基础”部分,我们介绍了联邦学习(FL)过程中聚合的概念。回想一下,聚合是联邦学习方法使用每个代理在本地训练的模型来产生具有强大全局性能的模型的方式。很明显,所采用的聚合方法的强度和鲁棒性与最终全局模型的性能直接相关。
因此,根据本地数据集、代理和联邦学习系统层次结构选择合适的聚合方法是实现联邦学习良好性能的关键。实际上,该领域许多出版物的研究焦点是提供这些方法在各种理论场景下的数学保证收敛性。
本章的目标是介绍一些关于聚合方法及其在理想和非理想情况下的收敛性的研究,将这些方法与它们在联邦学习实际应用中出现的不同场景中的优势联系起来。阅读完本章后,你应该能够理解不同的联邦学习场景特征如何要求不同的聚合方法,并且应该对如何实现这些算法有一个大致的了解。
本章将涵盖以下主题:
-
重新审视聚合
-
理解 FedAvg
-
修改非理想情况下的聚合
技术要求
书中展示的 Python 算法实现都可以在ch7文件夹中找到,该文件夹位于github.com/PacktPublishing/Federated-Learning-with-Python/tree/main/ch7。
重要注意事项
你可以使用代码文件用于个人或教育目的。请注意,我们不会支持商业部署,并且不会对使用代码造成的任何错误、问题或损害负责。
对于纯聚合算法,包括辅助代码以显示从预设的本地参数中获取的示例输出。修改本地训练过程的聚合方法需要一个联邦学习系统来运行——对于这些,包括使用 STADLE 的完整实现。此外,纯聚合算法可以通过配置聚合方法直接使用 STADLE 进行测试。有关运行示例的信息可以在相关的README文件中找到。
通过pip安装stadle-client包是运行完整的联邦学习过程示例所必需的。以下命令可以用来执行此安装:
pip install stadle-client
建议使用虚拟环境来隔离stadle-client安装的特定包版本与其他系统上的安装。
重新审视聚合
为了在联邦学习中将聚合的上下文语境化,首先,我们描述了应用联邦学习所必需的系统组件:
-
一组执行联邦学习本地训练部分的计算代理。
-
每个代理都拥有一个本地数据集(静态或动态),在严格的联邦学习场景下,其任何部分都不能被发送给另一个代理。
-
每个代理都拥有一个参数化的模型,可以在本地数据集上进行训练,这个过程产生了模型的本地最优参数集。
-
参数服务器或聚合器,在每个迭代中从代理那里接收本地训练的模型,并返回由所选聚合方法产生的结果模型。
每一轮联邦学习通信都可以分解为以下两个阶段:
-
本地训练阶段,在这个阶段,代理在其本地数据集上对本地模型进行多次迭代训练
-
聚合阶段,在这个阶段,代理将上一阶段训练好的本地模型发送给聚合器,并接收聚合模型作为下一轮本地训练阶段的起始模型。
那么,在聚合阶段,一个代理发送一个本地训练的模型究竟意味着什么?一般的方法是使用定义本地模型的参数集,允许在所有可以以这种方式参数化的模型之间实现一定程度的泛化。然而,第二种方法侧重于在基于梯度的优化方法中,将本地训练期间累积的本地梯度发送给聚合器,代理在每一轮结束时使用接收到的聚合梯度更新他们的模型。虽然这种方法限制了只能使用基于梯度的本地训练方法的模型,但这种方法在训练深度学习模型时的普遍性导致了基于梯度聚合的聚合方法的一个子集。在本章中,我们选择通过 FedAvg 算法的视角来构建模型聚合的框架。
理解 FedAvg
在第三章“联邦学习系统的工作原理”中,介绍了名为 FedAvg 的聚合算法,以帮助阐明一般结构和用具体示例表示之前讨论的更抽象的概念。FedAvg 被用于两个原因:底层算法的简单性和比基于梯度的方法更广泛模型类型的通用性。它还受益于研究者的广泛引用,在提出新的聚合方法时,使用 FedAvg 作为基准在不同理论场景中进行性能分析。这种研究界的关注很可能归因于原始 FedAvg 论文是由第一个将 FL 的概念和好处公之于众的谷歌团队发表的。关于进一步阅读,这篇论文可以在arxiv.org/abs/1602.05629?context=cs找到。
FedAvg 之前有一个称为联邦随机梯度下降(FedSGD)的聚合方法。FedSGD 可以看作是 FedAvg 执行的模型参数平均的梯度聚合类似物。此外,在 FL 的背景下,在 FedAvg 之前还研究了平均模型参数的概念,用于并行化 SGD 方法。本质上,这些并行化 SGD 方法的分析反映了 FedAvg 的独立同分布(IID)情况——这一概念将在后面的章节中讨论。无论如何,FedAvg 的简单性、通用性和流行性使其成为深入研究的好基础,为后面章节中讨论的众多聚合方法提供了背景,这些方法基于或改进了 FedAvg。
以前,FedAvg 仅被呈现为一个算法,它接受具有相应本地数据集大小的模型 ,其中总和等于 N,并返回:
如第四章中“聚合本地模型”部分所示,使用 Python 实现的联邦学习服务器实现(simple-fl)使用以下函数根据每个模型本地训练所使用的数据量来计算缓冲模型(当前轮次中客户端发送的模型)的加权平均值:
def _average_aggregate(self,
buffer: List[np.array],
num_samples: List[int]) -> np.array:
"""
Given a list of models, compute the average model (FedAvg).
This function provides a primitive mathematical operation.
:param buffer: List[np.array] - A list of models to be aggregated
:return: np.array - The aggregated models
"""
denominator = sum(num_samples)
# weighted average
model = float(num_samples[0]) / denominator * buffer[0]
for i in range(1, len(buffer)):
model += float(num_samples[i]) / denominator * buffer[i]
return model
原始算法与这种描述差异不大。算法的高级步骤如下:
-
服务器随机抽取 K * C 个客户端,其中 K 是客户端的总数,C 是介于 0 和 1 之间的一个参数。
-
选定的 K * C 客户端接收最新的聚合模型,并开始在他们的本地数据上训练模型。
-
每个客户端在完成一定量的训练后,将其本地训练的模型发送回服务器。
-
服务器计算接收到的模型的参数算术平均值,以计算最新的聚合模型。
可以立即将这种形式表示与我们对联邦学习过程的介绍进行比较,其中 ClientUpdate 为代理执行本地训练,服务器使用相同的加权平均算法进行聚合。一个重要点是,在每一轮中采样一部分客户端进行本地训练和模型传输,允许通过 C 参数进行客户端子采样。这个参数包括实验性地确定各种客户端集大小的收敛速度——在理想情况下,这个值将被设置为 1。
如前所述,FedAvg 是一种理想的联邦学习场景,本质上反映了并行化随机梯度下降的方法。在 并行化随机梯度下降(pSGD)中,目标是利用硬件并行化(例如,在多个核心上并行运行)来加速特定机器学习任务上的 SGD 收敛。为此任务的一种方法是每个核心并行地在数据的一些子集上训练基础模型若干次迭代,然后聚合部分训练好的模型,并使用聚合模型作为下一次训练的基础。在这种情况下,如果将核心视为联邦学习场景中的代理,那么并行化 SGD 方法与理想情况下的 FedAvg 是相同的。这意味着为 pSGD 所做的所有收敛保证和相应的分析都可以直接应用于 FedAvg,假设是理想联邦学习场景。因此,从这项先前工作中可以看出,FedAvg 显示出强大的收敛速度。
在对 FedAvg 进行了所有这些赞誉之后,自然会质疑为什么更复杂的聚合方法甚至有必要。回想一下,在讨论 FedAvg 收敛时,多次使用了“理想联邦学习场景”这个短语。不幸的现实是,大多数实际的联邦学习应用将无法满足该短语所规定的条件之一或多个。
理想联邦学习场景可以分解为三个主要条件:
-
用于训练的本地数据集是 IID(数据集是从相同的数据分布中独立抽取的)。
-
计算代理在计算能力上相对同质。
-
可以假设所有代理都不是对抗性的。
从高层次来看,为什么这些特性在联邦学习场景中是可取的是显而易见的。为了更详细地了解为什么这三个条件是必要的,将在接下来的小节中检查在没有每个条件的情况下 FedAvg 的性能。
数据集分布
为了检验非 IID 情况下的 FedAvg,首先,定义数据集的分布究竟指的是什么非常重要。在分类问题中,数据分布通常指的是与每个数据点相关的真实类别的分布。例如,考虑 MNIST 数据集,其中每个图像是从 0 到 9 的手写数字。如果从数据集中抽取 1,000 个图像的均匀随机样本,每个类别的预期图像数量将是相同的——这可以被认为是一种均匀数据分布。或者,一个包含 910 个数字 0 的图像和 10 个其他数字的图像的样本将是一个严重偏斜的数据分布。
为了将定义推广到分类任务之外,可以将它扩展到指代数据集中存在的特征的分布。这些特征可能是手动制作并提供给模型的(例如线性回归),或者它们可以作为模型管道的一部分从原始数据中提取(例如深度 CNN 模型)。对于分类问题,类分布通常包含在特征分布中,这是由于隐含的信念,即特征足以正确预测类别。查看特征分布的好处是,它关注于数据(相对于关注于任务的类别),允许在机器学习任务中进行泛化。
然而,在实验分析的情况下,从数据集中轻松构建非 IID 样本的能力使得分类任务非常适合测试 FedAvg 在 FL 环境中的鲁棒性以及不同的聚合方法。为了在本节中检验 FedAvg,考虑一个玩具 FL 场景,其中每个代理在前面描述的 MNIST 数据集的数据样本上训练 CNN。有两种主要情况,下面将详细说明。
IID 情况
模型的收敛可以通过使用模型参数空间来表示。具有n个参数的模型参数空间可以被视为一个n-维欧几里得空间,其中每个参数对应于空间中的一个维度。考虑一个初始化的模型;这个模型的初始参数可以表示为参数空间中的一个点。随着局部训练和聚合的发生,这个代表点将在参数空间中移动,最终目标是收敛到空间中的一个点,该点对应于损失或误差函数的最小化局部最优。
这些函数的一个关键点是它们依赖于在本地训练过程中使用的数据 – 当代理之间的数据集是 IID 时,相应的损失/误差函数的最优解在参数空间中相对接近。考虑一个简单的情况,即数据集是 IID,并且所有模型都使用相同的参数初始化。如第三章中联邦学习系统的工作原理的模型聚合基础部分所示,参数空间的一个简化版本可以表示如下:
![图 7.1 – 具有相同初始化和 IID 数据集的模型
![img/B18369_07_01.jpg]
图 7.1 – 具有相同初始化和 IID 数据集的模型
观察两个模型如何从相同的起点(紫色 x)开始,并朝向相同的最优解(紫色点)移动,从而产生接近两个模型共享的最优解的聚合模型。
由于代理之间误差/损失函数的相似性,模型在训练过程中倾向于收敛到相同或相似的最优解。这意味着在每个聚合步骤之后,模型的变化相对较小,导致收敛速度与单本地模型情况相匹配。如果底层数据分布代表真实数据分布(例如,MNIST 中的 10 个不同数字是均匀的),则生成的聚合模型将表现出强大的性能。
接下来,考虑每个模型分别初始化的广义 IID 情况:
![图 7.2 – 具有不同初始化和 IID 数据集的模型
![img/B18369_07_02.jpg]
图 7.2 – 具有不同初始化和 IID 数据集的模型
在这种情况下,观察两个模型如何从不同的起点(粗体/虚线 x)开始,并最初朝向不同的最优解移动,从而产生一个较差的第一个模型。然而,在第一次聚合之后,两个模型从相同的起点开始,并朝向相同的最优解移动,导致与第一个案例相似的收敛。
应该很明显,在第一次聚合步骤之后,这会简化为之前的情况,因为每个模型都从聚合参数开始第二轮。因此,之前所述的收敛属性可以扩展到具有 IID 本地数据集的 FedAvg 的一般情况。
非 IID 情况
在 IID 本地数据集情况下,允许收敛速度与单模型情况相匹配的关键属性是由于它们从相似的数据分布中构建,损失/误差函数的局部最优相似性。在非 IID 情况下,最优相似性通常不再观察到。
以 MNIST 为例,让我们考虑一个有两个代理的 FL 场景,其中第一个代理只有 0 到 4 的数字图像,第二个代理只有 5 到 9 的数字图像;也就是说,数据集不是 IID。这些数据集在局部训练级别本质上会导致两个完全不同的五类分类任务,而不是原始的 10 类分类问题——这将导致第一个代理和第二个代理之间的参数空间最优解完全不同。以下是对这个参数空间的简化表示,其中两个模型具有相同的初始化:
![图 7.3 – 不同初始化和非 IID 数据集的模型
图 7.3 – 不同初始化和非 IID 数据集的模型
现在由于最优解不再共享(分别用三角形/正方形表示粗体/点划线模型的最优解),即使重复聚合也无法创建一个接近任一模型最优解的聚合模型。由于每一轮中每个模型的目标最优解不同,模型在每次局部训练阶段都会发生分歧或漂移。
只有最优解的一小部分将在两个代理的损失/误差函数之间共享。因此,每个模型向局部训练期间未共享的最优解移动的概率很高,导致模型在参数空间中相互漂移。然后,每个聚合步骤都会将模型拉向错误的最优解,逆转局部训练期间取得的进展,并阻碍收敛。请注意,仅仅取不同代理的最优解的平均值,在参数空间中几乎不可能接近任何代理的最优解,因此在这种情况下,持续聚合的结果通常是一个在整个数据集上表现不佳的模型。由于在局部训练期间观察到的随机性,最终可能会收敛到一个共享的最优解,这会导致聚合模型在参数空间中的移动,但这没有理论保证,并且当它发生时,收敛速度将远慢于 IID 情况下的收敛速度。
重要提示
这个 MNIST 示例是非 IID 数据集的理论极端。在实践中,非 IID 数据集可能指的是代理之间数据分布的不同偏差(例如,0 到 4 的数字图像是 5 到 9 的两倍,反之亦然)。差异的严重程度与 FedAvg 的性能相关,因此在较轻的情况下仍然可以达到足够的性能。然而,在这些情况下,FedAvg 的性能通常始终劣于在所有本地数据集上一次性训练单个模型的类似集中式训练任务——FL 能够实现的理想模型。
虽然本节重点介绍了非 IID 数据集引起的问题的统计基础,但下一节将探讨一个更为直接的问题,尤其是在更大规模部署时可能会出现的问题。
计算能力分布
参与联邦学习的代理的一个未声明的假设是,如果给予无限的时间,每个代理都能够执行本地训练。计算能力有限(内存和速度)的代理可能比其他代理花费更多的时间来完成本地训练,或者他们可能需要量化等技术来支持模型和训练过程。然而,在某些轮次中无法完成本地训练的代理将简单地通过阻碍联邦学习过程来阻止收敛。
通常,收敛界限和实验结果关注的是达到一定性能水平所需的通信轮数。在这个指标和上述假设下,收敛与分配给每个代理的计算能力完全无关,因为计算能力只影响完成一轮所需的实际时间。然而,在实际应用中,收敛速度是通过实际花费的时间来衡量的,而不是完成的通信轮数——这意味着完成每一轮的时间与轮数一样重要。这个总时间的指标是,当在参与联邦学习的代理中观察到异构计算能力时,简单的 FedAvg 表现不佳的地方。
具体来说,每轮完成的时间瓶颈在于参与该轮的最慢代理的本地训练时间;这是因为与大多数情况下的训练相比,聚合是极其快速的,并且必须等待所有代理完成本地训练。当所有代理都参与该轮时,这个瓶颈成为最慢的整体代理。在计算能力同质化的情况下,最快代理和最慢代理之间的本地训练时间差异将相对微不足道。在异质化的情况下,单个落后代理将大大减少 FedAvg 的收敛时间,并导致更快代理在等待接收聚合模型时产生显著的空闲时间。
两个针对完全参与代理的 FedAvg 的修改可能最初看起来可以解决这个问题;然而,两者都有缺点,导致性能次优:
-
一种方法是依靠每轮的代理子采样,导致每轮发生落后效应的概率取决于代理的数量和每轮采样的样本大小。在只有少数落后代理的情况下,这可能足够,但随着这个数量的增加,问题会成比例地恶化,并且并不能完全消除问题发生的可能性。此外,小样本大小会失去从更多代理的聚合中获得的鲁棒性优势。
-
第二种方法是允许所有代理在每个回合开始时开始本地训练,并在接收到一定数量的模型后提前开始聚合。这种方法的好处是能够在不大大限制每个回合参与聚合的代理数量的情况下,完全消除拖沓效应。然而,它会导致最慢的代理在整个回合中都不会参与聚合,这实际上减少了活跃代理的数量,并可能限制训练期间使用的数据的多样性。此外,那些太慢而无法参与聚合的代理将进行无益的计算工作。
很明显,无论在每个回合结束时应用于接收到的模型的最终聚合方法是什么,都需要基于可用的计算能力进行一些本地调整,以便有效地执行聚合。
非独立同分布案例和异构计算能力案例都关注 FL 系统的一般容易观察到的属性,并在一定程度上受到行政控制。我们接下来提出的案例与此不同,因为它挑战了在考虑实际 FL 系统时的一项关键假设。
防御对抗性代理
到目前为止,一直假设每个参与 FL 场景的代理总是以期望的方式行事;也就是说,积极且正确地在本地训练接收到的模型,并参与模型向聚合器或从聚合器传输。这在研究环境中很容易实现,其中联邦设置被模拟,代理被单独控制;然而,代理行为正确的这种假设在实践中并不总是成立。
一个不涉及针对性恶意意图的例子是代理向聚合器传输的模型权重中的错误。这可能发生在代理使用的数据集有缺陷或训练算法实现不正确(参数数据在传输过程中的损坏也是可能的)。在最坏的情况下,这可能导致一个或多个模型的参数在统计上等同于随机噪声。当随机噪声的 L2 范数(n 维张量向量大小的扩展)不显著大于有效模型的范数时,FedAvg 将遭受与故障代理与所有代理的比例成比例的性能损失——当这个比例较小时,这是相对可以接受的。然而,即使只有一个故障代理,如果代理噪声的范数显著较高,它也会导致几乎随机的聚合模型。这是由于 FedAvg 聚合过程中内部执行算术平均的性质。
当代理可以被恶意对手控制时,问题变得更加严重。一个拥有足够信息的恶意代理可以通过对其提交的模型参数进行大量修改,在聚合后产生任何期望的模型。即使没有直接了解其他代理的模型参数和相关权重,恶意代理也可以利用在后续回合中本地模型和聚合模型之间的相对较小变化,将之前的聚合模型作为对预期本地模型参数的估计。
因此,FedAvg 在 FL 环境中对随机和受控的对抗性代理提供的鲁棒性很少或没有。虽然一种可能的缓解方法是对代理进行单独监控并防止对抗性代理传输模型,但在识别这些代理所需的时间内,最终模型的收敛可能已经受到了重大损害。
现在应该很清楚,FedAvg 在这些非理想情况下牺牲了鲁棒性以换取计算的简单性。不幸的是,由于与研究环境相比缺乏控制,这种鲁棒性是 FL 实际应用中的一个关键考虑因素。下一节将重点介绍实现针对本节中提出的三个非理想情况鲁棒性的方法。
修改聚合以适应非理想情况
在实际的联邦学习(FL)应用中,上述构成理想 FL 场景的假设通常并不成立;因此,可能需要使用替代的聚合方法来最佳地执行 FL。本节的目标是介绍针对异构计算能力、对抗性代理和非独立同分布(non-IID)数据集的聚合方法示例,按照难度顺序排列。
处理异构计算能力
如前所述,在这种情况下,理想的聚合方法始终避免出现落后效应(straggler effect),同时最大化参与 FL 的代理数量,并允许所有代理在一定程度上做出贡献,无论计算能力差异如何。当代理的本地训练时间显著长于大多数代理时,它们在某一回合中成为落后者。因此,有效地解决这个问题实际上需要在代理级别上在本地训练过程中具有一定的适应性,基于每个代理可用的计算能力。
手动调整
实现这一点的简单方法是根据每次迭代所需的时间来改变本地训练迭代的次数。换句话说,本地训练时间是固定的,每个代理尽可能在这个时间内完成尽可能多的迭代,而不是执行固定数量的迭代。这可以简单地消除落后者问题,但如果必须分配大量的本地训练时间给慢速代理以有意义地贡献,因为快速代理可能执行了过多的本地训练迭代而导致模型漂移,这可能会导致性能不佳。这可以通过设置最大本地训练迭代次数来缓解。然而,必须仔细平衡分配的本地训练时间,以便慢速代理有足够的时间产生足够的模型,同时防止快速代理在达到最大迭代次数后闲置。此外,如何预先确定这样的阈值以实现最佳性能,而不是依赖于实验结果来寻找最佳配置,目前还不清楚。
自动调整 – FedProx
一种称为 FedProx 的聚合方法遵循了基于计算能力动态调整每个代理的本地训练过程的相同方法,同时修改了本地训练的终止条件,以帮助进行收敛的理论分析。具体来说,固定的本地训练迭代次数被替换为训练循环的终止条件,该条件可以适应具有不同计算能力的代理。
这种终止条件的潜在概念是 γ-不精确解,当 γ-不精确最优点的梯度幅度小于本地训练开始时梯度幅度的 γ 倍时,该条件得到满足。直观地说,γ 是介于 0 和 1 之间的一个值,值越接近 0,由于更严格的终止条件,会导致更多的本地训练迭代。因此,γ 允许参数化代理的计算能力。
使用终止条件方法的一个潜在问题是,由于严格的条件,在多次本地训练迭代后,局部训练模型可能会从聚合模型中发散。为了解决这个问题,FedProx 在被最小化的目标函数中添加了一个近端项,等于以下内容:
在这里, 表示接收到的聚合模型权重。
近端项惩罚当前权重与聚合模型权重之间的差异,通过 μ 参数参数化的强度限制上述本地模型发散。从这两个概念出发,FedProx 允许每个代理执行与每个代理的计算能力成比例的变量次数,而无需为每个代理手动调整迭代次数或分配一定量的训练时间。由于添加了近端项,FedProx 需要使用基于梯度的优化方法才能工作——有关底层理论和与 FedAvg 的比较的更多信息,可以在原始论文中找到(该论文位于 arxiv.org/abs/1812.06127)。
实现 FedProx
由于 FedProx 对 FedAvg 的修改都是在客户端进行的,因此 FedProx 的实际实现完全由对本地训练框架的修改组成。具体来说,FedProx 涉及本地训练的新终止条件,以及将约束项添加到本地损失函数中。因此,使用本地训练代码的示例可以帮助精确地说明如何集成 FedProx。
让我们考虑以下使用 PyTorch 的通用训练代码:
agg_model = ... # Get aggregate model – abstracted out of example
model.load_state_dict(agg_model.state_dict())
for epoch in range(num_epochs):
for batch_idx, (inputs, targets) in enumerate(trainloader):
inputs, targets = inputs.to(device), targets.to(device)
optimizer.zero_grad()
outputs = model(inputs)
loss = criterion(outputs, targets)
loss.backward()
optimizer.step()
让这段代码在每个回合使用接收到的聚合模型在本地数据集上执行 num_epochs 个训练周期。FedProx 的第一个必要修改是将固定的训练周期数替换为动态终止条件,检查是否已找到以聚合模型为初始模型的 γ-不精确解。为此,必须存储整个训练数据集上聚合模型和当前本地模型的总体梯度——这可以按以下方式执行:
agg_model = ... # Get aggregated model from aggregator
model.load_state_dict(agg_model.state_dict())
agg_grad = None
curr_grad = None
gamma = 0.9
mu = 0.001
两个 FedProx 参数 gamma 和 mu 的值已设置,并定义了存储聚合模型和最新本地模型梯度的变量。
我们然后使用这些梯度变量定义本地训练的 γ-不精确新终止条件:
def gamma_inexact_solution_found(curr_grad, agg_grad, gamma):
if (curr_grad is None):
return False
return curr_grad.norm(p=2) < gamma * agg_grad.norm(p=2)
在每次训练循环迭代之前检查此条件,以确定何时停止本地训练。创建 total_grad 变量以存储在反向传播期间从每个小批量中创建的累积梯度:
model.train()
while (not gamma_inexact_solution_found(curr_grad, agg_grad, gamma)):
total_grad = torch.cat([torch.zeros_like(param.data.flatten()) for param in model.parameters()])
for batch_idx, (inputs, targets) in enumerate(trainloader):
inputs, targets = inputs.to(device), targets.to(device)
optimizer.zero_grad()
outputs = model(inputs)
loss = criterion(outputs, targets)
为了计算近端项,计算聚合模型和最新本地模型的权重。从这些权重中,计算近端项并将其添加到损失项中:
curr_weights = torch.cat([param.data.flatten() for param in model.parameters()])
agg_weights = torch.cat([param.data.flatten() for param in agg_model.parameters()])
prox_term = mu * torch.norm(curr_weights - agg_weights, p=2)**2
loss += prox_term
计算梯度并将其添加到存储在 total_grad 中的累积和中:
loss.backward()
grad = torch.cat([param.grad.flatten() for param in model.parameters()])
total_grad += grad
optimizer.step()
最后,在完成当前本地训练迭代后,我们更新 agg_grad(如果梯度是用聚合权重计算的)和 curr_grad:
if (agg_grad == None):
agg_grad = total_grad
curr_grad = total_grad
这些修改使得 FedProx 可以在 FedAvg 之上实现。使用 FedProx 的完整 FL 示例可以在github.com/PacktPublishing/Federated-Learning-with-Python/tree/main/ch7/agg_fl_examples/cifar_fedprox_example找到。
处理异构计算能力场景的辅助方法,当观察到轻微异质性时有助于提高计算效率的想法是聚合中的补偿。考虑聚合发生时接收到的模型数量超过某个阈值的情况(通常,这小于参与代理的数量)。使用这个阈值可以减轻拖沓效应;然而,较慢代理所做的每轮工作最终都会被丢弃,导致训练效率低下。
补偿的核心思想是允许在某一轮次中由较慢的代理进行的本地训练被包含到下一轮次的模型聚合中。在下一轮次中,通过在聚合期间乘以用于加权平均的权重和惩罚项来补偿模型的年龄。通过这样做,较慢的代理可以获得的训练时间可以比快速代理多两到三倍,同时避免拖沓效应。为了防止较慢的代理需要过多的额外训练时间,需要轻微的异质性。这是由于在经过多轮之后给予模型的关联惩罚;它将足够严重,足以有效地导致没有贡献并减少补偿聚合 - 这是必要的,以防止过旧的模型阻碍聚合模型的收敛。
最后,我们检查帮助解决第三个非理想属性的方法,即某些代理子集被对手控制或以其他方式表现出不可取的行为。
对抗性代理
在上一节中,已经表明,在存在对抗性代理的情况下,FedAvg 的核心问题是对聚合过程中使用的底层算术平均值的异常值缺乏鲁棒性。这自然引发了一个问题:这种均值是否可以以提供这种鲁棒性的方式估计。答案是鲁棒均值估计器类别。有许多这样的估计器,它们在鲁棒性、与真实算术平均值的距离和计算效率之间提供了不同的权衡。
作为以下聚合方法实现的基,考虑以下通用聚合函数:
def aggregate(parameter_vectors):
# Perform some form of aggregation
return aggregated_parameter_vector
此函数接受一个参数向量的列表,并返回结果聚合参数向量。
现在我们将检查鲁棒均值估计器的三个示例实现。
使用几何中值进行聚合
样本的空间中位数是指使自身与样本之间的 L1 距离之和最小的点。从概念上讲,这与算术平均数相似,算术平均数是使自身与样本之间的 L2 距离之和最小的点。使用 L1 距离可以提供对异常值更大的鲁棒性;事实上,只有当至少一半的点来自对抗性代理时,才能在空间中位数中诱导出任意点。然而,空间中位数不能直接计算,而是依赖于数值近似或迭代算法来计算。
要迭代地计算几何平均数,可以使用 Weiszfeld 算法如下:
def geometric_median_aggregate(parameter_vectors, epsilon):
vector_shape = parameter_vectors[0].shape
vector_buffer = list(v.flatten() for v in parameter_vectors)
prev_median = np.zeros(vector_buffer[0].shape)
delta = np.inf
vector_matrix = np.vstack(vector_buffer)
while (delta > epsilon):
dists = np.sqrt(np.sum((vector_matrix - prev_median[np.newaxis, :])**2, axis=1))
curr_median = np.sum(vector_matrix / dists[:, np.newaxis], axis=0) / np.sum(1 / dists)
delta = np.linalg.norm(curr_median - prev_median)
prev_median = curr_median
return prev_median.reshape(vector_shape)
该算法利用了这样一个事实:一组点的空间中位数是使该集合上欧几里得距离之和最小的点,在每个迭代中执行一种加权最小二乘法,权重与点与当前中位数估计的欧几里得距离成反比。
使用坐标中位数进行聚合
坐标中位数是通过取样本中每个坐标的中值来构建的,正如其名称所暗示的。这个中值可以直接计算,与空间中位数不同,并且由于单变量统计中中位数的性质,直观上提供了对异常值的类似鲁棒性。然而,不清楚所得到的模型在数据集性能和收敛性方面是否显示出与算术平均数有任何理论上的相似性。
NumPy 使得实现这个函数变得非常简单,如下所示:
def coordinate_median_aggregate(parameter_vectors):
return np.median(parameter_vectors, axis=0)
很明显,坐标中位数比空间中位数更易于计算,这是以牺牲理论保证为代价来换取速度的。
使用 Krum 算法进行聚合
另一种方法是,在聚合之前从对抗性代理中隔离异常值点。这种方法的最著名例子是Krum 算法,在该算法中,在聚合之前执行基于距离的评分,作为定位异常值点的一种手段。
具体来说,Krum 算法首先计算每个点的成对 L2 距离——这些距离随后被用来计算每个点的分数,等于n-f-2个最小的 L2 距离之和(f是一个设置好的参数)。然后,Krum 输出具有最低分数的点,实际上返回了与f个被忽略的异常值点具有最小总 L2 距离的点。或者,Krum 使用的评分方法可以在计算算术平均值之前修剪异常值点。在两种情况下,对于足够大的n和2f+2 < n,收敛率与非对抗情况下的 FedAvg 相似。有关 Krum 算法的更多信息,可以在原始论文中找到,该论文位于papers.nips.cc/paper/2017/hash/f4b9ec30ad9f68f89b29639786cb62ef-Abstract.html。
Krum 算法可以按以下方式执行聚合:
def krum_aggregate(parameter_vectors, f, use_mean=False):
num_vectors = len(parameter_vectors)
filtered_size = max(1, num_vectors-f-2)
scores = np.zeros(num_vectors)
for i in range(num_vectors):
distances = np.zeros(num_vectors)
for j in range(num_vectors):
distances[j] = np.linalg.norm(parameter_vectors[i] - parameter_vectors[j])
scores[i] = np.sum(np.sort(distances)[:filtered_size])
if (use_mean):
idx = np.argsort(scores)[:filtered_size]
return np.mean(np.stack(parameter_vectors)[idx], axis=0)
else:
idx = np.argmin(scores)
return parameter_vectors[idx]
注意,已经包含了一个标志来决定应该使用两种 Krum 聚合方法中的哪一种(单选与修剪平均值)。向量化距离计算是可能的,但由于预期参数向量较大且代理数量较小,迭代方法被优先考虑。
非 IID 数据集
通过与独立同分布(IID)数据集合作,FL(联邦学习)所获得的理论基础在实现高性能聚合模型方面发挥着重要作用。从高层次来看,这可以通过不同数据集中模型学习到的差异来解释。当应用数据集无关的聚合方法时,无法对这些模型的收敛性做出理论保证——除非对数据集的非 IID 性质施加约束。关键阻碍因素是局部模型在参数空间中向非共享最优解移动的高概率,导致在每个局部训练阶段之后,局部模型与聚合模型之间的一致漂移。
有一些方法试图根据局部机器学习任务对聚合模型所做的修改进行限制,依赖于深度学习模型的过度参数化来找到相对分离的参数子集以优化每个任务的聚合模型。这种聚合方法之一是FedCurv,它使用先前聚合模型的 Fisher 信息矩阵作为局部训练期间辅助参数修改的调节器。然而,在实际应用中,对于极端非 IID 情况,这种方法鲁棒性的测试可能需要进一步进行,以确保可接受的性能。
实现 FedCurv
FedCurv 的实现涉及对标准 FedAvg 方法的两个关键修改。首先,本地损失函数必须修改以包含包含上一轮汇总 Fisher 信息的正则化项。其次,必须正确计算和汇总参数的 Fisher 信息矩阵,以便在下一轮中使用。
如实现 FedProx部分所示,本地训练示例代码将被再次使用,以展示 FedCurv 的实现。在第四章 使用 Python 实现联邦学习服务器中,我们了解到一个模型转换层允许聚合器对框架无关的模型表示进行操作。以前,这些表示只包含原始模型的相关参数;然而,这种无关表示实际上允许聚合任何所需的参数,甚至那些与真实模型参数只有松散联系的那些参数。这意味着次要参数可以捆绑并随本地模型发送,汇总后,在下一轮中与汇总模型分离。
在 FedCurv 中,有两组参数必须在本地计算并汇总以用于下一轮;因此,可以假设这些参数在训练后与本地模型一起发送,并在训练前与汇总模型分离,以简化示例代码(此功能的实现很简单)。因此,如前所述,FedCurv 的两个关键修改可以简化为在本地训练模型后计算 Fisher 信息参数,并使用接收到的汇总 Fisher 信息参数计算正则化项。
Fisher 信息矩阵指的是模型对数似然函数的梯度相对于其参数的协方差,通常在现有数据上经验性地评估。FedCurv 仅利用此矩阵的对角线元素,即梯度参数之间的方差及其零的期望值。
在高层次上,这个方差项可以被视为一个估计,即参数在改变模型在数据上的性能方面的影响程度。这个信息对于防止在本地训练其他代理时修改对某个数据集上良好性能至关重要的参数至关重要——这是 FedCurv 背后的基本思想。
将模型性能的度量从对数似然梯度放宽到任何目标函数的梯度,允许在计算使用基于梯度的优化方法(如深度学习模型)的模型的方差项时直接使用反向传播期间计算的梯度项。具体来说,参数的方差项等于其相应梯度项的平方,允许直接从本地训练期间计算的净梯度中计算这些项。
首先,我们创建两个变量来存储代理的最新 Fisher 信息参数和接收到的聚合 Fisher 信息参数,这些参数用于确定来自其他代理的 Fisher 信息。FedCurv 的 lambda 参数值是固定的,total_grad被初始化为一个容器,用于存储每个训练循环的累积梯度:
agg_model = ... # Get aggregated model from aggregator
model.load_state_dict(agg_model.state_dict())
fisher_info_params = ... # Initialize at start, then maintain to store past round parameters
agg_fisher_info_params = ... # Separate aggregate Fisher information parameters from aggregate model parameters
# Only consider other agents, and convert to PyTorch tensor
agg_fisher_info_params = {k:torch.tensor(agg_fisher_info_params[k] - fisher_info_params[k]) for k in fisher_info_params.keys()}
# Scaling parameter for FedCurv regularization term
fedcurv_lambda = 1.0
total_grad = {i:torch.zeros_like(param.data) for i,param in enumerate(model.parameters())}
然后,我们从模型权重和聚合 Fisher 信息参数中计算 FedCurv 正则化项。这个项由 lambda 加权,并在计算梯度之前添加到损失项中:
model.train()
for epoch in range(num_epochs):
for batch_idx, (inputs, targets) in enumerate(trainloader):
inputs, targets = inputs.to(device), targets.to(device)
optimizer.zero_grad()
outputs = model(inputs)
loss = criterion(outputs, targets)
for i,param in enumerate(model.parameters()):
# Factor out regularization term to use saved fisher info parameters
reg_term = (param.data ** 2) * agg_fisher_info_params[f'fedcurv_u_{i}']
reg_term += 2 * param.data * agg_fisher_info_params[f'fedcurv_v_{i}']
reg_term += (agg_fisher_info_params[f'fedcurv_v_{i}'] ** 2) / agg_fisher_info_params[f'fedcurv_u_{i}']
loss += fedcurv_lambda * reg_term.sum()
然后,在更新模型权重之前,我们计算并存储梯度到total_grad中:
loss.backward()
for i,param in enumerate(model.parameters()):
total_grad[i] += param.grad
optimizer.step()
最后,我们计算并存储代理的最新 Fisher 信息参数,以便在下一轮中使用:
for i,param in enumerate(model.parameters()):
fisher_info_params[f'fedcurv_u_{i}'] = (total_grad[i] ** 2).numpy()
fisher_info_params[f'fedcurv_v_{i}'] = ((total_grad[i] ** 2) * param.data).numpy()
因此,可以在 FedAvg 之上使用框架无关的聚合来实现 FedCurv。使用 FedCurv 的完整 FL 示例可以在github.com/PacktPublishing/Federated-Learning-with-Python/tree/main/ch7/agg_fl_examples/cifar_fedcurv_example找到。
数据共享方法
为了取得进一步进展,需要对 FL 场景的外部方面进行更改。例如,假设数据隐私限制被放宽,使得每个代理的本地数据集的小子集可以与其他代理共享。这种数据共享方法允许在本地数据分布中实现与共享数据量成比例的同质性,但以牺牲 FL 的关键平稳数据属性为代价,这使得它在许多以隐私为导向的应用中变得有吸引力。因此,数据共享方法通常不适合大多数应用。
通过微调实现个性化
当数据集是独立同分布(IID)时,产生一个在本地数据集上表现出强大性能的单个模型并不容易。然而,如果从 FL 过程中移除单个模型限制会发生什么?如果目标是产生在训练所进行的相同边缘设备上表现良好的本地模型,移除单个模型限制允许使用在精确数据分布上训练的不同本地模型,这些数据分布是推理应用的地方。
这个概念被称为个性化,其中代理使用针对本地数据分布调整过的聚合模型版本来实现强大的性能。这种方法的关键点是平衡本地训练模型的本地性能、全局性能以及每一轮接收到的聚合模型的鲁棒性。实现这一目标的一种方法是在每一轮中,每个代理都保持其本地模型,通过每一轮将前一个本地模型和接收到的聚合模型的加权平均值更新本地模型。
或者,考虑一种放松,允许在每一轮中产生多个聚合模型。在本地数据分布可以被聚类成几个分离组的情况下,分布感知聚合允许对属于同一分布聚类的模型组选择性地应用聚合方法。
这种方法的例子之一是基于性能的邻近选择(PENS)算法,其中代理在第一阶段从其他代理那里接收本地训练的模型,并在自己的本地数据集上对其进行测试。利用在相似数据集上训练的模型将比在差异数据集上训练的模型表现更好的假设,代理随后确定具有相似数据分布的其他代理的集合,允许在第二阶段只与相似代理进行聚合。
第二种方法是在本地模型和全局聚合模型之间添加一个中间聚合步骤,称为集群模型。通过利用关于代理数据分布的知识或通过动态分配方法,具有相似数据分布的代理可以被分配到一个集群聚合器,由于其代理拥有独立同分布的数据集,因此该聚合器产生的模型强度较大。
平衡集群模型的表现和全局聚合的鲁棒性导致了半全局模型的概念,在这种模型中,可以从集群模型中选择子样本(可能基于数据分布)来创建一个较小的部分全局聚合模型集,这些模型集可以保持性能和鲁棒性。因此,集群和半全局模型方法对聚合和实现完全分布式联邦学习系统都有益。
摘要
本章的目标是提供一个关于当前聚合知识的概念概述,这是联邦学习中的关键理论步骤,允许每个代理执行的非连接训练以最小的传输需求汇总在一起。FedAvg 是一个简单但出奇强大的聚合算法,在理想的联邦学习场景中表现良好。当使用具有相似计算能力的机器在独立同分布数据集上进行训练,并且没有对抗性或其他表现不佳的代理时,这种场景得以实现。
不幸的是,在现实世界中部署联邦学习系统时,这些条件往往无法满足。为了解决这些问题,我们引入并实施了修改后的聚合方法:FedProx、FedCurv 和三种不同的鲁棒均值估计器。阅读完这一章后,你应该对实际联邦学习应用中必须考虑的因素有了一个坚实的理解,并且应该能够将这些算法集成到这些应用中。
在下一章中,我们将通过几个玩具示例深入探讨一些现有的联邦学习框架,以展示每个框架提供的功能。
第三部分 联邦学习应用的生产化
在这部分,你将了解现有的联邦学习(FL)框架,例如TensorFlow Federated(TFF)、PySyft、Flower 和 STADLE,并学习它们的库以及如何实际运行这些框架。此外,通过了解全球范围内正在实施的当前和潜在用例,特别是全球企业公司中的用例,你将了解现实世界中的联邦学习情况。本书通过探讨联邦学习的未来趋势和发展,来理解人工智能技术本身的发展方向,以展望智慧驱动的未来。
本部分包括以下章节:
-
第八章, 介绍现有的联邦学习框架
-
第九章, 联邦学习应用的关键用例案例研究
-
第十章, 未来趋势与发展
第八章:介绍现有的联邦学习框架
本章的目标是介绍现有的联邦学习(FL)框架和平台,将每个平台应用于涉及玩具机器学习(ML)问题的联邦学习场景。本章关注的平台是 Flower、TensorFlow Federated、OpenFL、IBM FL 和 STADLE——选择这些平台背后的想法是通过涵盖现有的 FL 平台范围来帮助你。
到本章结束时,你应该对如何使用每个平台进行联邦学习有一个基本的了解,并且你应该能够根据其相关的优势和劣势选择一个平台用于联邦学习应用。
在本章中,我们将涵盖以下主题:
-
现有 FL 框架的介绍
-
使用现有框架在电影评论数据集上实现示例 NLP FL 任务
-
使用现有框架实现示例计算机视觉 FL 任务,使用非-IID 数据集
技术要求
你可以在本书的 GitHub 仓库中找到本章的补充代码文件:
重要提示
你可以使用代码文件进行个人或教育目的。请注意,我们不会支持商业部署,并且不会对使用代码造成的任何错误、问题或损害负责。
本章中的每个实现示例都是在运行 Ubuntu 20.04 的 x64 机器上运行的。
NLP 示例的训练代码实现需要以下库来运行:
-
Python 3 (版本 ≥ 3.8)
-
NumPy
-
TensorFlow(版本 ≥ 2.9.1)
-
TensorFlow Hub (
pipinstall tensorflow-hub) -
TensorFlow Datasets (
pipinstall tensorflow-datasets) -
TensorFlow Text (
pipinstall tensorflow-text)
由于模型的大小,建议使用带有适当 TensorFlow 安装的 GPU 来节省 NLP 示例的训练时间。
训练非-IID(非独立同分布)计算机视觉示例的代码实现需要以下库来运行:
-
Python 3(版本 ≥ 3.8)
-
NumPy
-
PyTorch(版本 ≥ 1.9)
-
Torchvision(版本 ≥ 0.10.0,与 PyTorch 版本相关联)
每个 FL 框架的安装说明列在以下子节中。
TensorFlow Federated
你可以安装以下库来使用 TFF:
-
tensorflow_federated(使用pip install tensorflow_federated命令) -
nest_asyncio(使用pip install nest_asyncio命令)
OpenFL
你可以使用pip install openfl命令安装 OpenFL。
或者,你可以使用以下命令从源代码构建:
git clone https://github.com/intel/openfl.git
cd openfl
pip install .
IBM FL
安装 IBM FL 的本地版本需要位于代码仓库中的 wheel 安装文件。要执行此安装,请运行以下命令:
git clone https://github.com/IBM/federated-learning-lib.git
cd federated-learning-lib
pip install federated_learning_lib-*-py3-none-any.whl
Flower
你可以使用pip install flwr命令安装 Flower。
STADLE
您可以使用 pip install stadle-client 命令安装 STADLE 客户端库。
联邦学习框架简介
首先,我们介绍后续实现重点章节中将要使用的联邦学习框架和平台。
Flower
Flower (flower.dev/) 是一个开源且与机器学习框架无关的联邦学习框架,旨在让用户易于使用。Flower 采用标准的客户端-服务器架构,其中客户端被设置为从服务器接收模型参数,在本地数据上训练,并将新的本地模型参数发送回服务器。
联邦学习过程的高级编排由 Flower 所称的策略决定,服务器使用这些策略来处理客户端选择和参数聚合等方面。
Flower 使用 远程过程调用 (RPCs) 来通过客户端执行从服务器发送的消息以执行所述编排。框架的可扩展性允许研究人员尝试新的方法,例如新的聚合算法和通信方法(如模型压缩)。
TensorFlow Federated (TFF)
TFF (www.tensorflow.org/federated) 是一个基于 TensorFlow 的开源 FL/计算框架,旨在允许研究人员轻松地使用现有的 TensorFlow/Keras 模型和训练管道模拟联邦学习。它包括联邦核心层,允许实现通用联邦计算,以及联邦学习层,它建立在核心之上,并为 FL 特定过程提供接口。
TFF 专注于 FL 的单机本地模拟,使用包装器从标准的 TensorFlow 等价物创建 TFF 特定的数据集、模型和联邦计算(FL 过程中执行的核心客户端和服务器计算)。从通用联邦计算构建一切的关注使研究人员能够按需实现每个步骤,从而支持实验。
OpenFL
OpenFL (github.com/intel/openfl) 是英特尔开发的开源联邦学习框架,专注于允许跨隔离区隐私保护机器学习。OpenFL 允许根据联盟(指整个 FL 系统)的预期生命周期选择两种不同的工作流程。
在基于聚合器的流程中,单个实验及其相关的联邦学习计划从聚合器发送到参与 协作者(代理)以作为 FL 流程的本地训练步骤运行——实验完成后,联盟停止。在基于导演的流程中,使用持久组件而不是短生命周期的组件,以便按需运行实验。以下图表描述了基于导演的流程的架构和用户:
![图 8.1 – 基于总监的工作流程架构(改编自 openfl.readthedocs.io/en/latest/s…
图 8.1 – 基于总监的工作流程架构(改编自 openfl.readthedocs.io/en/latest/s…
总监经理负责实验的运行,与位于协作节点上的长期信使组件合作,管理每个实验的短期组件(协作者+聚合者)。在针对跨数据孤岛的场景时,OpenFL 对管理数据分片给予了独特的关注,包括数据表示在不同孤岛中不同的情况。
IBM FL
IBM FL 是一个也专注于企业联邦学习的框架。它遵循简单的聚合者-参与者设计,其中一些拥有本地数据的参与者通过向聚合者发送增量模型训练结果并与生成的聚合模型(遵循标准的客户端-服务器联邦学习架构)合作,与其他参与者协作。IBM FL 对多种融合(聚合)算法和旨在对抗偏见的某些公平技术提供官方支持——这些算法的详细信息可以在位于 github.com/IBM/federat… 的存储库中找到。IBM FL 的一个具体目标是高度可扩展,使用户能够轻松地进行必要的修改,以满足特定的功能需求。它还支持基于 Jupyter-Notebook 的仪表板,以帮助协调联邦学习实验。
STADLE
与之前的框架不同,STADLE (stadle.ai/) 是一个与机器学习框架无关的联邦学习和分布式学习 SaaS 平台,旨在允许无缝地将联邦学习集成到生产就绪的应用程序和机器学习管道中。STADLE 的目标是最大限度地减少集成所需的特定于联邦学习的代码量,使联邦学习对新手来说易于访问,同时仍然为那些想要进行实验的人提供灵活性。
使用 STADLE SaaS 平台,不同技术能力的用户可以在所有规模上协作进行联邦学习项目。性能跟踪和模型管理功能使用户能够生成具有强大性能的验证联邦模型,而直观的配置面板允许对联邦学习过程进行详细控制。STADLE 使用两级组件层次结构,允许多个聚合器并行操作,以匹配需求。以下图展示了高级架构:
![图 8.2 – STADLE 多聚合器架构
图 8.2 – STADLE 多聚合器架构
STADLE 客户端的开发通过pip安装和易于理解的配置文件简化,同时公开提供了一些示例,供用户参考 STADLE 如何集成到现有的机器学习代码中的不同方式。
PySyft
尽管由于代码库的持续变化,PySyft (github.com/OpenMined/PySyft) 的实现不包括在本章中,但它仍然是隐私保护深度学习空间中的主要参与者。PySyft 背后的核心原则是允许在不对数据进行直接访问的情况下对存储在机器上的数据进行计算。这是通过在用户和数据位置之间添加一个中间层来实现的,该层向参与工作的机器发送计算请求,将计算结果返回给用户,同时保持每个工人存储和使用的用于执行计算的数据的隐私。
这种通用能力直接扩展到 FL,重新设计正常深度学习训练流程的每一步,使其成为对每个参与 FL 的工人(代理)存储的模型参数和数据的计算。为了实现这一点,PySyft 利用钩子封装标准的 PyTorch/TensorFlow 库,修改必要的内部函数,以便支持模型训练和测试作为 PySyft 隐私保护计算。
现在已经解释了 FL 框架背后的高级思想,我们将转向其实际应用中的实现级细节,以两个示例场景为例。首先,我们来看如何修改现有的用于 NLP 模型的集中式训练代码,使其能够使用 FL。
示例 - NLP 模型的联邦训练
通过上述每个 FL 框架将第一个 ML 问题转换为 FL 场景的将是 NLP 领域的分类问题。从高层次来看,NLP 是指计算语言学和 ML 的交集,其总体目标是使计算机能够从人类语言中达到某种程度的理解 - 这种理解的细节根据要解决的具体问题而大相径庭。
在这个例子中,我们将对电影评论进行情感分析,将它们分类为正面或负面。我们将使用的数据集是 SST-2 数据集 (nlp.stanford.edu/sentiment/)… 0/1,分别代表负面和正面情感。
我们将使用进行二元分类的模型是一个带有自定义分类头的预训练 BERT 模型。BERT 模型允许我们将句子编码成一个高维数值向量,然后将其传递到分类头以输出二元标签预测;有关 BERT 模型的更多信息,请参阅 huggingface.co/blog/bert-1… SST-2 数据集上的性能,从而节省时间并保持性能。
现在,我们将通过本地(集中式)训练代码,该代码将作为展示如何使用每个 FL 框架的基础,从 Keras 模型定义和数据集加载器开始。
定义情感分析模型
在sst_model.py文件中定义的SSTModel对象是我们将在这个示例中使用的 Keras 模型。
首先,我们导入必要的库:
import tensorflow as tf
from tensorflow import keras
from keras import layers
import tensorflow_text
import tensorflow_hub as hub
import tensorflow_datasets as tfds
TensorFlow Hub 用于轻松下载预训练的 BERT 权重到 Keras 层。当从 TensorFlow Hub 加载 BERT 权重时使用 TensorFlow Text。TensorFlow Datasets 将允许我们下载和缓存 SST-2 数据集。
接下来,我们定义模型并初始化模型层对象:
class SSTModel(keras.Model):
def __init__(self):
super(SSTModel, self).__init__()
self.preprocessor = hub.KerasLayer("https://tfhub.dev/tensorflow/bert_en_uncased_preprocess/3")
self.small_bert = hub.KerasLayer("https://tfhub.dev/tensorflow/bert_en_uncased_L-12_H-768_A-12/4")
self.small_bert.trainable = False
self.fc1 = layers.Dense(512, activation='relu')
self.fc2 = layers.Dense(64, activation='relu')
self.fc3 = layers.Dense(1, activation='sigmoid')
preprocessor对象将原始句子输入批次转换为 BERT 模型使用的格式。我们从 TensorFlow Hub 加载预处理器和 BERT 层,然后初始化构成分类头的密集层。我们使用 sigmoid 激活函数在最后将输出压缩到区间(0,1),以便与真实标签进行比较。
然后,我们可以定义模型的正向传递:
def call(self, inputs):
input_dict = self.preprocessor(inputs)
bert_output = self.small_bert(input_dict)['pooled_output']
output = self.fc1(keras.activations.relu(bert_output, alpha=0.2))
scores = self.fc3(self.fc2(output))
return scores
我们将 leaky ReLU 应用于 BERT 输出,在传递到分类头层之前添加非线性。
创建数据加载器
我们还实现了一个函数,使用 TensorFlow Datasets 库加载 SST-2 数据集。首先,加载训练数据并将其转换为 NumPy 数组,以便在训练期间使用:
def load_sst_data(client_idx=None, num_clients=1):
x_train = []
y_train = []
for d in tfds.load(name="glue/sst2", split="train"):
x_train.append(d['sentence'].numpy())
y_train.append(d['label'].numpy())
x_train = np.array(x_train)
y_train = np.array(y_train)
我们以类似的方式加载测试数据:
x_test = []
y_test = []
for d in tfds.load(name="glue/sst2", split="validation"):
x_test.append(d['sentence'].numpy())
y_test.append(d['label'].numpy())
x_test = np.array(x_test)
y_test = np.array(y_test)
如果指定了client_idx和num_clients,我们返回训练数据集的相应分区——这将用于执行联邦学习:
if (client_idx is not None):
shard_size = int(x_train.size / num_clients)
x_train = x_train[client_idx*shard_size:(client_idx+1)*shard_size]
y_train = x_train[client_idx*shard_size:(client_idx+1)*shard_size]
return (x_train, y_train), (x_test, y_test)
接下来,我们检查位于local_training.py中的执行本地训练的代码。
训练模型
我们首先导入必要的库:
import tensorflow as tf
from tensorflow import keras
from sst_model import SSTModel, load_sst_data
然后,我们可以使用之前定义的数据集加载器(不进行拆分)来加载训练和测试分割:
(x_train,y_train), (x_test,y_test) = load_sst_data()
现在,我们可以编译模型并开始训练:
model.compile(
optimizer = keras.optimizers.Adam(learning_rate=0.0005, amsgrad=False),
loss = keras.losses.BinaryCrossentropy(),
metrics = [keras.metrics.BinaryAccuracy()]
)
model.fit(x_train, y_train, batch_size=64, epochs=3)
最后,我们在测试分割上评估模型:
_, acc = model.evaluate(x_test, y_test, batch_size=64)
print(f"Accuracy of model on test set: {(100*acc):.2f}%")
经过三个训练周期后,模型应该达到大约 82%的测试准确率。
现在我们已经通过了本地训练代码,我们可以检查如何修改代码以使用上述每个 FL 框架进行联邦学习。
采用 FL 训练方法
为了展示如何将 FL 应用于 SST 模型训练场景,我们首先需要将原始 SST-2 数据集拆分成不相交的子集,这些子集代表 FL 应用中的本地数据集。为了简化问题,我们将研究三个代理各自在数据集的不同三分之一上训练的情况。
目前,这些子集是从数据集中随机采样而不重复的 – 在下一节“在非-IID 数据上对图像分类模型进行联邦训练”中,我们将研究本地数据集是从原始数据集的有偏采样中创建的情况。我们不会在本地训练三个 epoch,而是将进行三轮 FL,每轮本地训练阶段在本地数据上训练一个 epoch。FedAvg 将在每一轮结束时用于聚合本地训练的模型。在这三轮之后,将使用最终的聚合模型计算上述验证指标,从而允许比较本地训练案例和 FL 案例。
集成 TensorFlow Federated 用于 SST-2
如前所述,TensorFlow Federated(TFF)框架是在 TensorFlow 和 Keras 深度学习库之上构建的。模型实现是使用 Keras 完成的;因此,将 TFF 集成到本地训练代码中相对简单。
第一步是在加载数据集之前添加 TFF 特定的导入和 FL 特定的参数:
import nest_asyncio
nest_asyncio.apply()
import tensorflow_federated as tff
NUM_CLIENTS = 3
NUM_ROUNDS = 3
TFF 允许我们通过向 FL 过程传递适当数量的数据集(本地数据集)来模拟一定数量的代理。为了在预处理后将 SST-2 数据集分成三份,我们可以使用以下代码:
client_datasets = [load_sst_data(idx, NUM_CLIENTS)[0] for idx in range(NUM_CLIENTS)]
接下来,我们必须使用 TFF API 函数包装 Keras 模型,以便轻松创建相应的tff.learning.Model对象。我们创建一个函数,初始化 SST 模型,并将其与输入规范(关于每个数据元素大小的信息)一起传递给这个 API 函数,返回结果 – TFF 将在 FL 过程中内部使用此函数来创建模型:
def sst_model_fn():
sst_model = SSTModel()
sst_model.build(input_shape=(None,64))
return tff.learning.from_keras_model(
sst_model,
input_spec=tf.TensorSpec(shape=(None), dtype=tf.string),
loss=keras.metrics.BinaryCrossentropy()
)
使用sst_model_fn函数以及用于更新本地模型和聚合模型的优化器,可以创建 TFF FedAvg 过程。对于服务器优化器函数使用 1.0 的学习率,允许在每一轮结束时用新的聚合模型替换旧的模型(而不是计算旧模型和新模型的加权平均值):
fed_avg_process = tff.learning.algorithms.build_unweighted_fed_avg(
model_fn = sst_model_fn,
client_optimizer_fn = lambda: keras.optimizers.Adam(learning_rate=0.001),
server_optimizer_fn = lambda: keras.optimizers.SGD(learning_rate=1.0)
)
最后,我们初始化并运行联邦学习过程 10 轮。每次fed_avg_process.next()调用通过在客户端数据集上使用三个模型进行本地训练,然后使用 FedAvg 进行聚合来模拟一轮。第一轮后的状态被传递到下一次调用,作为该轮的起始 FL 状态:
state = fed_avg_process.initialize()
for round in range(NUM_ROUNDS):
state = fed_avg_process.next(state, client_datasets).state
FL 过程完成后,我们将最终的聚合 tff.learning.Model 对象转换回原始的 Keras 模型格式,以便计算验证指标:
fed_weights = fed_avg_process.get_model_weights(state)
fed_sst_model = SSTModel()
fed_sst_model.build(input_shape=(None, 64))
fed_sst_model.compile(
optimizer = keras.optimizers.Adam(learning_rate=0.005, amsgrad=False),
loss = keras.losses.BinaryCrossentropy(),
metrics = [keras.metrics.BinaryAccuracy()]
)
fed_weights.assign_weights_to(fed_sst_model)
_, (x_test, y_test) = load_sst_data()
_, acc = fed_sst_model.evaluate(x_test, y_test, batch_size=64)
print(f"Accuracy of federated model on test set: {(100*acc):.2f}%")
聚合模型的最终准确率应约为 82%。
从这一点来看,应该很清楚 TFF FedAvg 的结果几乎与本地训练场景的结果相同。
集成 OpenFL 用于 SST-2
请记住,OpenFL 支持两种不同的工作流程:基于聚合器的工作流程和基于导演的工作流程。本例将使用基于导演的工作流程,涉及长期存在的组件,可以处理传入的 FL 任务请求。这选择是因为希望有一个持久的 FL 设置来部署多个项目;然而,两种工作流程都执行相同的核心 FL 过程,因此表现出类似的表现。
为了帮助在此情况下进行模型序列化,我们只聚合分类头权重,在训练和验证时运行时重建完整模型(TensorFlow Hub 缓存下载的层,因此下载过程只发生一次)。我们在 sst_model.py 中包含以下函数以帮助进行此修改:
def get_sst_full(preprocessor, bert, classification_head):
sst_input = keras.Input(shape=(), batch_size=64, dtype=tf.string)
scores = classification_head(bert(preprocessor(sst_input))['pooled_output'])
return keras.Model(inputs=sst_input, outputs=scores, name='sst_model')
def get_classification_head():
classification_head = keras.Sequential([
layers.Dense(512, activation='relu', input_shape=(768,)),
layers.Dense(64, activation='relu', input_shape=(512,)),
layers.Dense(1, activation='sigmoid', input_shape=(64,))
])
return classification_head
由于 OpenFL 专注于解决数据孤岛问题,从 SST-2 数据创建本地数据集比 TFF 情况稍微复杂一些。创建数据集所需的对象将在名为 sst_fl_dataset.py 的单独文件中实现。
首先,我们包括必要的导入。我们导入的两个 OpenFL 特定对象是处理数据集加载和分片的 ShardDescriptor 对象,以及处理数据集访问的 DataInterface 对象:
from openfl.interface.interactive_api.shard_descriptor import ShardDescriptor
from openfl.interface.interactive_api.experiment import DataInterface
import tensorflow as tf
from sst_model import load_sst_data
实现 ShardDescriptor
我们首先实现了 SSTShardDescriptor 类。当创建此分片描述符时,我们保存 rank(客户端编号)和 worldsize(客户端总数)值,然后加载训练和验证数据集:
class SSTShardDescriptor(ShardDescriptor):
def __init__(
self,
rank_worldsize: str = '1, 1',
**kwargs
):
self.rank, self.worldsize = tuple(int(num) for num in rank_worldsize.split(','))
(x_train,y_train), (x_test,y_test) = load_sst_data(self.rank-1, self.worldsize)
self.data_by_type = {
'train': tf.data.Dataset.from_tensor_slices((x_train, y_train)).batch(64),
'val': tf.data.Dataset.from_tensor_slices((x_test, y_test)).batch(64)
}
我们实现了 ShardDescriptor 类函数以获取可用的数据集类型(在这种情况下为训练和验证)以及基于客户端排名的相应数据集/分片:
def get_shard_dataset_types(self):
return list(self.data_by_type)
def get_dataset(self, dataset_type='train'):
if dataset_type not in self.data_by_type:
raise Exception(f'Wrong dataset type: {dataset_type}')
return self.data_by_type[dataset_type]
我们还指定了正在使用的数据集的具体属性。请注意,样本形状设置为 1。SSTModel 的预处理层允许我们传入字符串作为输入,这些字符串被视为类型为 tf.string 且长度为 1 的输入向量:
@property
def sample_shape(self):
return ["1"]
@property
def target_shape(self):
return ["1"]
@property
def dataset_description(self) -> str:
return (f'SST dataset, shard number {self.rank}'
f' out of {self.worldsize}')
这样,SSTShardDescriptor 的实现就完成了。
实现数据接口
接下来,我们将 SSTFedDataset 类实现为 DataInterface 的子类。这是通过实现分片描述符获取器和设置器方法来完成的,设置器方法准备要提供给训练/验证 FL 任务的数据:
class SSTFedDataset(DataInterface):
def __init__(self, **kwargs):
super().__init__(**kwargs)
@property
def shard_descriptor(self):
return self._shard_descriptor
@shard_descriptor.setter
def shard_descriptor(self, shard_descriptor):
self._shard_descriptor = shard_descriptor
self.train_set = shard_descriptor.get_dataset('train')
self.valid_set = shard_descriptor.get_dataset('val')
我们还实现了 API 函数以授予数据集访问和数据集大小信息(用于聚合):
def get_train_loader(self):
return self.train_set
def get_valid_loader(self):
return self.valid_set
def get_train_data_size(self):
return len(self.train_set) * 64
def get_valid_data_size(self):
return len(self.valid_set) * 64
这样,就可以构建并使用本地 SST-2 数据集了。
创建 FLExperiment
现在,我们专注于在新的文件fl_sim.py中实现 FL 过程的实际实现。首先,我们导入必要的库——从 OpenFL 中,我们导入以下内容:
-
TaskInterface:允许我们为模型定义 FL 训练和验证任务;注册的任务是 director 指示每个 envoy 执行的任务 -
ModelInterface:允许我们将我们的 Keras 模型转换为 OpenFL 在注册任务中使用的格式 -
Federation:管理与 director 连接相关的信息 -
FLExperiment:使用TaskInterface、ModelInterface和Federation对象来执行 FL 过程
必要的导入如下所示:
import tensorflow as tf
from tensorflow import keras
import tensorflow_hub as hub
from openfl.interface.interactive_api.experiment import TaskInterface
from openfl.interface.interactive_api.experiment import ModelInterface
from openfl.interface.interactive_api.experiment import FLExperiment
from openfl.interface.interactive_api.federation import Federation
from sst_model import get_classification_head, get_sst_full
from sst_fl_dataset import SSTFedDataset
接下来,我们使用默认的director连接信息创建Federation对象:
client_id = 'api'
director_node_fqdn = 'localhost'
director_port = 50051
federation = Federation(
client_id=client_id,
director_node_fqdn=director_node_fqdn,
director_port=director_port,
tls=False
)
然后,我们使用相关的优化器和损失函数初始化模型——这些对象被 OpenFL 的KerasAdapter用于创建ModelInterface对象。我们在一个虚拟的 Keras 输入上调用模型,以便在将模型传递给ModelInterface之前初始化所有权重:
classification_head = get_classification_head()
optimizer = keras.optimizers.Adam(learning_rate=0.005, amsgrad=False)
loss = keras.losses.BinaryCrossentropy()
framework_adapter = 'openfl.plugins.frameworks_adapters.keras_adapter.FrameworkAdapterPlugin'
MI = ModelInterface(model=classification_head, optimizer=optimizer, framework_plugin=framework_adapter)
接下来,我们创建一个TaskInterface对象,并使用它来注册训练任务。请注意,将优化器包含在任务的装饰器函数中会导致训练数据集被传递给任务;否则,验证数据集将被传递给任务:
TI = TaskInterface()
@TI.register_fl_task(model='model', data_loader='train_data', device='device', optimizer='optimizer')
def train(model, train_data, optimizer, device):
preprocessor = hub.KerasLayer("https://tfhub.dev/tensorflow/bert_en_uncased_preprocess/3")
small_bert = hub.KerasLayer("https://tfhub.dev/tensorflow/bert_en_uncased_L-12_H-768_A-12/4")
small_bert.trainable = False
full_model = get_sst_full(preprocessor, small_bert, model)
full_model.compile(loss=loss, optimizer=optimizer)
history = full_model.fit(train_data, epochs=1)
return {'train_loss':history.history['loss'][0]}
类似地,我们使用TaskInterface对象注册验证任务。请注意,我们可以收集由evaluate函数生成的指标,并将值作为跟踪性能的手段:
@TI.register_fl_task(model='model', data_loader='val_data', device='device')
def validate(model, val_data, device):
preprocessor = hub.KerasLayer("https://tfhub.dev/tensorflow/bert_en_uncased_preprocess/3")
small_bert = hub.KerasLayer("https://tfhub.dev/tensorflow/bert_en_uncased_L-12_H-768_A-12/4")
small_bert.trainable = False
full_model = get_sst_full(preprocessor, small_bert, model)
full_model.compile(loss=loss, optimizer=optimizer)
loss, acc = full_model.evaluate(val_data, batch_size=64)
return {'val_acc':acc, 'val_loss':loss,}
现在,我们可以使用之前实现的SSTFedDataset类加载数据集,并使用创建的ModelInterface、TaskInterface和SSTFedDatasets对象创建并启动一个新的FLExperiment:
fed_dataset = SSTFedDataset()
fl_experiment = FLExperiment(federation=federation, experiment_name='sst_experiment')
fl_experiment.start(
model_provider=MI,
task_keeper=TI,
data_loader=fed_dataset,
rounds_to_train=3,
opt_treatment='CONTINUE_LOCAL'
)
定义配置文件
最后一步是创建由director和envoys使用的配置文件,以便实际加载数据并启动 FL 过程。首先,我们创建包含以下信息的director_config:
settings:
listen_host: localhost
listen_port: 50051
sample_shape: ["1"]
target_shape: ["1"]
这被保存在director/director_config.yaml中。
我们随后创建了三个envoy配置文件。第一个文件(envoy_config_1.yaml)包含以下内容:
params:
cuda_devices: []
optional_plugin_components: {}
shard_descriptor:
template: sst_fl_dataset.SSTShardDescriptor
params:
rank_worldsize: 1, 3
第二个和第三个envoy配置文件与第一个相同,只是rank_worldsize的值分别为2, 3和3, 3。这些配置文件以及所有代码文件都存储在实验目录中。目录结构应如下所示:
-
directordirector_config.yaml
-
experiment-
envoy_config_1.yaml -
envoy_config_2.yaml -
envoy_config_3.yaml -
sst_fl_dataset.py -
sst_model.py -
fl_sim.py(包含FLExperiment创建的文件)
-
一切准备就绪后,我们现在可以使用 OpenFL 执行 FL。
运行 OpenFL 示例
首先,从director文件夹中运行以下命令以启动 director(确保 OpenFL 已安装在工作环境中):
fx director start --disable-tls -c director_config.yaml
接下来,在实验目录中分别在不同的终端运行以下命令:
fx envoy start -n envoy_1 -–disable-tls --envoy-config-path envoy_config_1.yaml -dh localhost -dp 50051
fx envoy start -n envoy_2 -–disable-tls --envoy-config-path envoy_config_2.yaml -dh localhost -dp 50051
fx envoy start -n envoy_3 -–disable-tls --envoy-config-path envoy_config_3.yaml -dh localhost -dp 50051
最后,通过运行 fl_sim.py 脚本来启动 FLExperiment。完成三轮后,聚合模型应该达到大约 82% 的验证准确率。再次强调,性能几乎与本地训练场景相同。
集成 IBM FL 用于 SST-2
IBM FL 在执行联邦学习时使用保存的模型版本。以下代码(create_saved_model.py)初始化一个模型(在虚拟输入上调用模型以初始化参数)然后以 Keras SavedModel 格式保存模型供 IBM FL 使用:
import tensorflow as tf
from tensorflow import keras
from sst_model import SSTModel
sst_model = SSTModel()
optimizer = keras.optimizers.Adam(learning_rate=0.005, amsgrad=False)
loss = keras.losses.BinaryCrossentropy(),
sst_model.compile(loss=loss, optimizer=optimizer)
sst_input = keras.Input(shape=(), dtype=tf.string)
sst_model(sst_input)
sst_model.save('sst_model_save_dir')
运行此命令一次以将模型保存到名为 sst_model_save_dir 的文件夹中 – 我们将指示 IBM FL 从此目录加载保存的模型。
创建 DataHandler
接下来,我们创建一个 IBM FL DataHandler 类的子类,该类负责向模型提供训练和验证数据 – 这个子类将加载、预处理并存储 SST 数据集作为类属性。我们首先导入必要的库:
from ibmfl.data.data_handler import DataHandler
import tensorflow as tf
from sst_model import load_sst_data
这个类的 init 函数加载数据信息参数,然后使用这些参数来加载正确的数据集分片 SST-2:
class SSTDataHandler(DataHandler):
def __init__(self, data_config=None):
super().__init__()
if (data_config is not None):
if ('client_id' in data_config):
self.client_id = int(data_config['client_id'])
if ('num_clients' in data_config):
self.num_clients = int(data_config['num_clients'])
train_data, val_data = load_sst_data(self.client_id-1, self.num_clients)
self.train_dataset = tf.data.Dataset.from_tensor_slices(train_data).batch(64)
self.val_dataset = tf.data.Dataset.from_tensor_slices(val_data).batch(64)
我们还实现了返回用于训练/验证期间使用的加载数据集的 API 函数:
def get_data(self):
return self.train_dataset, self.val_dataset
定义配置文件
下一步是创建在启动聚合器和初始化聚会时使用的配置 JSON 文件。聚合配置首先指定它将用于与聚会通信的连接信息:
{
"connection": {
"info": {
"ip": "127.0.0.1",
"port": 5000,
"tls_config": {
"enable": "false"
}
},
"name": "FlaskConnection",
"path": "ibmfl.connection.flask_connection",
"sync": "False"
},
接下来,我们指定用于聚合的融合处理器:
"fusion": {
"name": "IterAvgFusionHandler",
"path": "ibmfl.aggregator.fusion.iter_avg_fusion_handler"
},
我们还指定了与本地训练和聚合相关的超参数。perc_quorum 指的是在聚合开始之前必须参与聚会的比例:
"hyperparams": {
"global": {
"max_timeout": 10800,
"num_parties": 1,
"perc_quorum": 1,
"rounds": 3
},
"local": {
"optimizer": {
"lr": 0.0005
},
"training": {
"epochs": 1
}
}
},
最后,我们指定要使用的 IBM FL 协议处理器:
"protocol_handler": {
"name": "ProtoHandler",
"path": "ibmfl.aggregator.protohandler.proto_handler"
}
}
此配置保存在 agg_config.json 文件中。
我们还创建了用于使用本地数据进行联邦学习的基聚会配置文件。我们首先指定聚合器和聚会的连接信息:
{
"aggregator":
{
"ip": "127.0.0.1",
"port": 5000
},
"connection": {
"info": {
"ip": "127.0.0.1",
"port": 8085,
"id": "party",
"tls_config": {
"enable": "false"
}
},
"name": "FlaskConnection",
"path": "ibmfl.connection.flask_connection",
"sync": "false"
},
然后,我们指定要使用的数据处理器和本地训练处理器 – 此组件使用模型信息和本地数据训练 SST 模型:
"data": {
"info": {
"client_id": 0,
"num_clients": 3
},
"name": "SSTDataHandler",
"path": "sst_data_handler"
},
"local_training": {
"name": "LocalTrainingHandler",
"path": "ibmfl.party.training.local_training_handler"
},
指定模型格式和信息 – 这是我们指向之前创建的保存模型的地方:
"model": {
"name": "TensorFlowFLModel",
"path": "ibmfl.model.tensorflow_fl_model",
"spec": {
"model-name": "sst_model",
"model_definition": "sst_model_save_dir"
}
},
最后,我们指定协议处理器:
"protocol_handler": {
"name": "PartyProtocolHandler",
"path": "ibmfl.party.party_protocol_handler"
}
}
创建 IBM FL 聚会
使用这种方式,剩下的只是启动每个聚会的代码,保存在 fl_sim.py 文件中。我们首先导入必要的库:
import argparse
import json
from ibmfl.party.party import Party
我们包含一个 argparse 参数,允许指定聚会编号 – 这用于修改基本聚会配置文件,以便从同一文件启动不同的聚会:
parser = argparse.ArgumentParser()
parser.add_argument("party_id", type=int)
args = parser.parse_args()
party_id = args.party_id
with open('party_config.json') as cfg_file:
party_config = json.load(cfg_file)
party_config['connection']['info']['port'] += party_id
party_config['connection']['info']['id'] += f'_{party_id}'
party_config['data']['info']['client_id'] = party_id
最后,我们使用修改后的配置信息创建并启动一个新的 Party 对象:
party = Party(config_dict=party_config)
party.start()
party.register_party()
使用这种方式,我们现在可以开始使用 IBM FL 进行联邦学习。
运行 IBM FL 示例
首先,通过运行以下命令来启动 aggregator:
python -m ibmfl.aggregator.aggregator agg_config.json
在聚合器完成设置后,输入 START 并按 Enter 键以打开聚合器以接收传入的连接。然后,你可以在单独的终端中使用以下命令启动三个参与者:
python fl_sim.py 1
python fl_sim.py 2
python fl_sim.py 3
最后,在聚合器窗口中输入 TRAIN 并按 Enter 键开始 FL 流程。当完成三轮后,你可以在同一窗口中输入 SAVE 以保存最新的聚合模型。
将 Flower 集成到 SST-2 中
必须在现有本地训练代码之上集成的两个主要 Flower 组件是客户端和策略子类实现。客户端子类实现允许我们与 Flower 接口,API 函数允许在客户端和服务器之间传递模型参数。策略子类实现允许我们指定服务器执行的聚合方法的细节。
我们首先编写代码来实现并启动客户端(存储在 fl_sim.py 中)。首先,导入必要的库:
import argparse
import tensorflow as tf
from tensorflow import keras
from sst_model import SSTModel, load_sst_data
import flwr as fl
我们添加一个命令行参数来指定客户端 ID,以便允许相同的客户端脚本被所有三个代理重用:
parser = argparse.ArgumentParser()
parser.add_argument("client_id", type=int)
args = parser.parse_args()
client_id = args.client_id
NUM_CLIENTS = 3
然后我们加载 SST-2 数据集:
(x_train,y_train), (x_test,y_test) = load_sst_data(client_id-1, NUM_CLIENTS)
注意,我们使用客户端 ID 从训练数据集中获取相应的分片。
接下来,我们创建模型和相关优化器以及损失对象,确保在哑输入上调用模型以初始化权重:
sst_model = SSTModel()
sst_model.compile(
optimizer = keras.optimizers.Adam(learning_rate=0.005, amsgrad=False),
loss = keras.losses.BinaryCrossentropy(),
metrics = [keras.metrics.BinaryAccuracy()]
)
sst_input = keras.Input(shape=(), dtype=tf.string)
sst_model(sst_input)
实现 Flower 客户端
现在我们可以实现 Flower 客户端对象,该对象将在服务器之间传递模型参数。要实现客户端子类,我们必须定义三个函数:
-
get_parameters(self, config): 返回模型参数值 -
fit(self, parameters, config): 将本地模型的权重设置为接收到的参数,执行本地训练,并返回新的模型参数以及数据集大小和训练指标 -
evaluate(self, parameters, config): 将本地模型的权重设置为接收到的参数,然后在验证/测试数据上评估模型,并返回性能指标
使用 fl.client.NumPyClient 作为超类允许我们利用 Keras 模型的 get_weights 和 set_weights 函数,这些函数将模型参数转换为 NumPy 数组的列表:
class SSTClient(fl.client.NumPyClient):
def get_parameters(self, config):
return sst_model.get_weights()
def fit(self, parameters, config):
sst_model.set_weights(parameters)
history = sst_model.fit(x_train, y_train, epochs=1)
return sst_model.get_weights(), len(x_train), {'train_loss':history.history['loss'][0]}
evaluate 函数也被定义:
def evaluate(self, parameters, config):
sst_model.set_weights(parameters)
loss, acc = sst_model.evaluate(x_test, y_test, batch_size=64)
return loss, len(x_train), {'val_acc':acc, 'val_loss':loss}
使用此客户端实现,我们最终可以使用以下行使用默认连接信息启动客户端:
fl.client.start_numpy_client(server_address="[::]:8080", client=SSTClient())
创建 Flower 服务器
在运行 Flower 之前,我们需要创建一个脚本(server.py),该脚本将启动 Flower 服务器。我们开始导入必要的库和 MAX_ROUNDS 参数:
import flwr as fl
import tensorflow as tf
from tensorflow import keras
from sst_model import SSTModel
MAX_ROUNDS = 3
因为我们希望在执行联邦学习后保存模型,所以我们创建了一个 flower FedAvg 策略的子类,并在聚合阶段的最后一步添加了一个保存模型的步骤:
class SaveKerasModelStrategy(fl.server.strategy.FedAvg):
def aggregate_fit(self, server_round, results, failures):
agg_weights = super().aggregate_fit(server_round, results, failures)
if (server_round == MAX_ROUNDS):
sst_model = SSTModel()
sst_input = keras.Input(shape=(), dtype=tf.string)
sst_model(sst_input)
sst_model.set_weights(fl.common.parameters_to_ndarrays(agg_weights[0]))
sst_model.save('final_agg_sst_model')
return agg_weights
使用这种策略,我们可以运行以下行来启动服务器(通过 config 参数传递 MAX_ROUNDS 参数):
fl.server.start_server(strategy=SaveKerasModelStrategy(), config=fl.server.ServerConfig(num_rounds=MAX_ROUNDS))
现在我们可以启动服务器和客户端,允许使用 Flower 进行 FL。
运行 Flower 示例
要启动服务器,首先运行 server.py 脚本。
每个客户端都可以通过在单独的终端窗口中运行以下命令来启动:
python fl_sim.py 1
python fl_sim.py 2
python fl_sim.py 3
FL 最终的聚合模型将被保存在 final_agg_sst_model 目录中,作为一个 SavedModel 对象。
集成 STADLE 用于 SST-2
STADLE 与之前考察的 FL 框架不同,它提供了一个基于云的平台(STADLE Ops),用于处理聚合器的部署和 FL 流程的管理。因为服务器端的部署可以通过该平台完成,所以使用 STADLE 进行 FL 所需实现的只是客户端的实现。这种集成是通过创建一个客户端对象来完成的,该对象偶尔发送本地模型,并从上一轮返回聚合模型。为此,我们需要创建代理配置文件,并修改本地训练代码以与 STADLE 接口。
首先,我们创建代理的配置文件,如下所示:
{
"model_path": "./data/agent",
"aggr_ip": "localhost",
"reg_port": "8765",
"token": "stadle12345",
"base_model": {
"model_fn": "SSTModel",
"model_fn_src": "sst_model",
"model_format": "Keras",
"model_name": "Keras-SST-Model"
}
}
这些参数的详细信息可以在 stadle-documentation.readthedocs.io/en/latest/d… 找到。请注意,这里列出的聚合器 IP 和注册端口号是占位符,在连接到 STADLE Ops 平台时将被修改。
接下来,我们修改本地训练代码以与 STADLE 一起工作。我们首先导入所需的库:
import argparse
import tensorflow as tf
from tensorflow import keras
from sst_model import SSTModel, load_sst_data
from stadle import BasicClient
再次,我们添加一个命令行参数来指定代理应接收的训练数据分区:
parser = argparse.ArgumentParser()
parser.add_argument("client_id", type=int)
args = parser.parse_args()
client_id = args.client_id
NUM_CLIENTS = 3
(x_train,y_train), (x_test,y_test) = load_sst_data(client_id-1, NUM_CLIENTS)
接下来,我们实例化一个 BasicClient 对象——这是 STADLE 客户端组件,用于处理本地训练过程与服务器端聚合器之间的通信。我们使用之前定义的配置文件来创建此客户端:
stadle_client = BasicClient(config_file="config_agent.json", agent_name=f"sst_agent_{client_id}")
最后,我们实现 FL 训练循环。在每一轮中,客户端从上一轮(从基础模型开始)获取聚合模型,并在本地数据上进一步训练,然后再通过客户端将其发送回聚合器:
for round in range(3):
sst_model = stadle_client.wait_for_sg_model()
history = sst_model.fit(x_train, y_train, epochs=1)
loss = history.history['loss'][0]
stadle_client.send_trained_model(sst_model, {'loss_training': loss})
stadle_client.disconnect()
wait_for_sg_model 函数从服务器返回最新的聚合模型,而 send_trained_model 函数将具有所需性能指标的本地训练模型发送到服务器。有关这些集成步骤的更多信息,请参阅 stadle-documentation.readthedocs.io/en/latest/u…
现在客户端实现完成后,我们可以使用 STADLE Ops 平台启动一个聚合器并启动一个 FL 流程。
创建 STADLE Ops 项目
首先,访问 stadle.ai 并创建一个新账户。一旦登录,你应该会被引导到 STADLE Ops 的项目信息页面:
![图 8.3 – STADLE Ops 中的项目信息页面
![图片 B18369_08_03.jpg]
图 8.3 – STADLE Ops 中的项目信息页面
点击创建新项目,然后填写项目信息并点击创建项目。项目信息页面应已更改以显示以下内容:
![图 8.4 – 新项目添加到项目信息页面
![图片 B18369_08_04.jpg]
图 8.4 – 新项目添加到项目信息页面
点击启动聚合器下方的加号图标以启动项目的新聚合器,然后在确认提示中点击确定。现在您可以导航到左侧的仪表板页面,页面看起来如下所示:
![图 8.5 – STADLE Ops 仪表板页面
![图片 B18369_08_05.jpg]
图 8.5 – STADLE Ops 仪表板页面
将config_agent.json文件中的aggr_ip和reg_port占位符参数值分别替换为连接 IP 地址和连接端口下的值。
这样,我们现在就可以开始 FL 训练过程了。
运行 STADLE 示例
第一步是将基础模型对象发送到服务器,使其能够反过来将模型分发给训练代理。这可以通过以下命令完成:
stadle upload_model --config_path config_agent.json
一旦命令成功运行,STADLE Ops 仪表板上的基础模型信息部分应更新以显示模型信息。现在我们可以通过运行以下命令来启动三个代理:
python fl_sim.py 1
python fl_sim.py 2
python fl_sim.py 3
经过三轮后,代理将终止,最终的聚合模型将在项目仪表板上显示,并以 Keras SavedModel 格式可供下载。建议查阅位于stadle.ai/user_guide/guide的用户指南,以获取有关 STADLE Ops 平台各种功能的更多信息。
评估每个联邦学习框架产生的结果聚合模型,得出的结论相同——聚合模型的性能基本上与集中式训练模型的性能相匹配。正如在第七章的“数据集分布”部分所解释的,模型聚合,这通常是预期的结果。自然要问的是,当本地数据集不是独立同分布(IID)时,性能会受到怎样的影响——这是下一节的重点。
示例 – 在非 IID 数据上对图像分类模型进行联邦训练
在前面的例子中,我们考察了如何通过在联邦学习过程中在原始训练数据集(本地数据集)的不相交子集上训练多个客户端来将集中式深度学习问题转换为联邦学习的类似问题。这个本地数据集创建的一个关键点是,子集是通过随机采样创建的,导致所有本地数据集在原始数据集相同的分布下都是独立同分布的。因此,FedAvg 与本地训练场景相似的性能是可以预期的——每个客户端的模型在训练过程中本质上都有相同的局部最小值集合要移动,这使得所有本地训练都对全局目标有益。
回想一下,在第七章“模型聚合”中,我们探讨了 FedAvg 如何容易受到严重非独立同分布的本地数据集引起的训练目标发散的影响。为了探索 FedAvg 在变化非独立同分布严重程度上的性能,本例在从 CIFAR-10 数据集(位于www.cs.toronto.edu/~kriz/cifar.html)中采样的构建的非独立同分布的本地数据集上训练了 VGG-16 模型(一个基于简单深度学习的图像分类模型)。CIFAR-10 是一个著名的简单图像分类数据集,包含 60,000 张图像,分为 10 个不同的类别;在 CIFAR-10 上训练的模型的目标是正确预测与输入图像相关联的类别。相对较低复杂性和作为基准数据集的普遍性使 CIFAR-10 成为探索 FedAvg 对非独立同分布数据的响应的理想选择。
重要提示
为了避免包含冗余的代码示例,本节重点介绍允许在 PyTorch 模型上使用非独立同分布的本地数据集执行联邦学习的关键代码行。建议在阅读本节之前,先阅读本章中“示例 – NLP 模型的联邦训练”部分中的示例,以便了解每个联邦学习框架所需的核心组件。本例的实现可以在本书的 GitHub 仓库中找到,完整内容位于github.com/PacktPublishing/Federated-Learning-with-Python树/main/ch8/cv_code),供参考使用。
本例的关键点是确定如何构建非独立同分布(non-IID)数据集。我们将通过改变训练数据集中每个类别的图像数量来改变每个本地数据集的类别标签分布。例如,一个偏向于汽车和鸟类的数据集可能包含 5,000 张汽车的图像,5,000 张鸟类的图像,以及每个其他类别 500 张图像。通过创建 10 个类别的三个不相交子集,并构建偏向这些类别的本地数据集,我们产生了三个本地数据集,其非独立同分布的严重程度与从未选择的类别中包含的图像数量成比例。
偏斜 CIFAR-10 数据集
我们首先将三个类别子集映射到客户端 ID,并设置从原始数据集中选取的类别(sel_count)和其他类别(del_count)的图像比例:
classes = ('airplane', 'automobile', 'bird', 'cat', 'deer',
'dog', 'frog', 'horse', 'ship', 'truck')
class_id_map = {
1: classes[:3],
2: classes[3:6],
3: classes[6:]
}
sel_count = 1.0, def_count = 0.2
然后我们从原始数据集中采样适当数量的图像,使用数据集中图像的索引来构建有偏的 CIFAR-10 子集:
class_counts = int(def_count * 5000) * np.ones(len(classes))
for c in classes:
if c in class_rank_map[self.rank]:
class_counts[trainset.class_to_idx[c]] = int(sel_count * 5000)
class_counts_ref = np.copy(class_counts)
imbalanced_idx = []
for i,img in enumerate(trainset):
c = img[1]
if (class_counts[c] > 0):
imbalanced_idx.append(i)
class_counts[c] -= 1
trainset = torch.utils.data.Subset(trainset, imbalanced_idx)
然后使用有偏的训练集创建用于本地训练的有偏 trainloader。当我们提到对未来的训练数据进行偏差时,这就是运行的代码。
我们现在将演示如何使用不同的 FL 框架来运行这个非-IID FL 流程。请参阅上一节 示例 - NLP 模型的联邦训练 中的安装说明和框架特定实现,以了解本节中省略的基本概念。
集成 OpenFL 用于 CIFAR-10
与 Keras NLP 示例类似,我们首先在 cifar_fl_dataset.py 中为非-IID 的 CIFAR-10 数据集创建 ShardDescriptor 和 DataInterface 子类。为了适应新的数据集,只需要进行少数几个更改。
首先,我们修改 self.data_by_type 字典,以便存储修改后的 CIFAR 数据集:
train_dataset, val_dataset = self.load_cifar_data()
self.data_by_type = {
'train': train_dataset,
'val': val_dataset
}
load_cifar_data 函数使用 torchvision 加载训练和测试数据,然后根据传递给对象的排名对训练数据进行偏差。
由于数据元素的维度现在已知(CIFAR-10 图像的大小),我们还使用固定值修改了形状属性:
@property
def sample_shape(self):
return ["32", "32"]
@property
def target_shape(self):
return ["10"]
然后我们实现 CifarFedDataset 类,它是 DataInterface 类的子类。对于这个实现不需要进行重大修改;因此,我们现在可以使用带有 OpenFL 的有偏 CIFAR-10 数据集。
现在我们转向实际的 FL 流程实现 (fl_sim.py)。一个关键的区别是必须使用框架适配器来从 PyTorch 模型创建 ModelInterface 对象:
model = vgg16()
optimizer = optim.SGD(model.parameters(), lr=0.001, momentum=0.9, weight_decay=5e-4)
criterion = nn.CrossEntropyLoss()
framework_adapter = 'openfl.plugins.frameworks_adapters.pytorch_adapter.FrameworkAdapterPlugin'
MI = ModelInterface(model=model, optimizer=optimizer, framework_plugin=framework_adapter)
唯一的另一个主要更改是修改传递给 TaskInterface 对象的培训和验证函数,以反映本地训练代码中这些函数的 PyTorch 实现。
最后一步是创建导演和使节使用的配置文件。导演配置中唯一必要的更改是更新 CIFAR-10 数据的 sample_shape 和 target_shape:
settings:
listen_host: localhost
listen_port: 50051
sample_shape: ["32","32"]
target_shape: ["10"]
这个文件保存在 director/director_config.yaml 中。
使节配置文件除了更新对象和文件名之外不需要任何更改——目录结构应该如下所示:
-
directordirector_config.yaml
-
experiment-
envoy_config_1.yaml -
envoy_config_2.yaml -
envoy_config_3.yaml -
cifar_fl_dataset.py -
fl_sim.py
-
您可以参考 *在 集成 OpenFL 用于 SST-2 部分的 运行 OpenFL 示例 来运行此示例。
集成 IBM FL 用于 CIFAR-10
记住,IBM FL 需要保存训练过程中使用的模型版本。我们首先在 create_saved_model.py 中运行以下代码以创建保存的 VGG-16 PyTorch 模型:
import torch
from torchvision.models import vgg16
model = vgg16()
torch.save(model, 'saved_vgg_model.pt')
接下来,我们为倾斜的 CIFAR-10 数据集创建DataHandler子类。唯一的核心更改是修改load_and_preprocess_data函数,以加载 CIFAR-10 数据并对训练集进行偏差。
下一步是创建启动聚合器和初始化各方时使用的配置 JSON 文件。聚合器配置(agg_config.json)无需进行重大更改,而各方配置的核心更改仅是修改模型信息以与 PyTorch 兼容:
"model": {
"name": "PytorchFLModel",
"path": "ibmfl.model.pytorch_fl_model",
"spec": {
"model-name": "vgg_model",
"model_definition": "saved_vgg_model.pt",
"optimizer": "optim.SGD",
"criterion": "nn.CrossEntropyLoss"
}
},
由于广泛使用配置文件,fl_sim.py中负责启动各方代码基本上无需修改。
您可以参考在 SST-2 中集成 IBM FL部分的运行 IBM FL 示例来运行此示例。
集成 Flower 用于 CIFAR-10
在加载 CIFAR-10 数据并对训练数据进行偏差后,Flower 实现所需的核心更改是NumPyClient子类。与 Keras 示例不同,get_parameters和set_parameters方法依赖于 PyTorch 模型状态字典,并且更为复杂:
class CifarClient(fl.client.NumPyClient):
def get_parameters(self, config):
return [val.numpy() for _, val in model.state_dict().items()]
def set_parameters(self, parameters):
params_dict = zip(model.state_dict().keys(), parameters)
state_dict = OrderedDict({k: torch.tensor(v) for k, v in params_dict})
model.load_state_dict(state_dict)
我们修改fit函数,使其与本地训练示例中的训练代码相匹配,并修改evaluate函数,使其与本地训练评估代码相类似。请注意,我们调用self.set_parameters(parameters)来更新本地模型实例的最新权重。
我们还在启动 Flower 客户端和服务器时将grpc_max_message_length参数设置为 1 GB,以适应更大的 VGG16 模型大小。客户端初始化函数现在是以下内容:
fl.client.start_numpy_client(
server_address="[::]:8080",
client=CifarClient(),
grpc_max_message_length=1024**3
)
最后,我们修改了server.py中的聚合器代码——我们之前用于在最后一轮结束时保存聚合模型的自定义策略需要修改以与 PyTorch 模型兼容:
if (server_round == MAX_ROUNDS):
vgg_model = vgg16()
np_weights = fl.common.parameters_to_ndarrays(agg_weights[0])
params_dict = zip(vgg_model.state_dict().keys(), np_weights)
state_dict = OrderedDict({k: torch.tensor(v) for k, v in params_dict})
torch.save(state_dict, "final_agg_vgg_model.pt")
使用此策略,我们可以运行以下行来启动服务器(在此处也添加了grpc_max_message_length参数):
fl.server.start_server(
strategy=SavePyTorchModelStrategy(),
config=fl.server.ServerConfig(num_rounds=MAX_ROUNDS),
grpc_max_message_length=1024**3
)
请参考在 SST-2 中集成 Flower部分的运行 Flower 示例来运行此示例。
集成 STADLE 用于 CIFAR-10
我们首先修改config_agent.json配置文件,以使用torchvision库中的 VGG16 模型:
{
"model_path": "./data/agent",
"aggr_ip": "localhost",
"reg_port": "8765",
"token": "stadle12345",
"base_model": {
"model_fn": "vgg16",
"model_fn_src": "torchvision.models",
"model_format": "PyTorch",
"model_name": "PyTorch-VGG-Model"
}
}
要将 STADLE 集成到本地训练代码中,我们初始化BasicClient对象,并修改训练循环,每两个本地训练轮次发送一次本地模型,并等待新的聚合模型:
stadle_client = BasicClient(config_file="config_agent.json")
for epoch in range(num_epochs):
state_dict = stadle_client.wait_for_sg_model().state_dict()
model.load_state_dict(state_dict)
# Normal training code...
if (epoch % 2 == 0):
stadle_client.send_trained_model(model)
注意
位于github.com/PacktPublishing/Federated-Learning-with-Python的代码包含此集成示例的完整实现,供参考。要启动聚合器并使用 CIFAR-10 STADLE 示例进行联邦学习,请参考创建 STADLE Ops 项目和在 SST-2 中运行 STADLE 示例部分。
测试构建的局部数据集中不同水平的偏差,应得出与第七章中“数据集分布”部分所陈述的相同结论——模型聚合对于非独立同分布情况——随着非独立同分布严重程度的增加,收敛速度和模型性能降低。本节的目标是在 SST-2 示例中理解每个联邦学习框架的基础上,突出与修改后的数据集上使用 PyTorch 模型所需的关键变化。结合本节和github.com/PacktPublishing/Federated-Learning-with-Python中的代码示例,应有助于理解此示例集成。
摘要
在本章中,我们通过两个不同示例的背景,介绍了几个联邦学习(FL)框架。从第一个示例中,你学习了如何通过将数据分割成互不重叠的子集,将传统的集中式机器学习(ML)问题转化为类似的联邦学习场景。现在很清楚,随机采样会导致局部数据集是独立同分布(IID),这使得 FedAvg 能够达到与集中式等效的任何联邦学习框架相同的性能水平。
在第二个示例中,你了解到了一组数据集可以是非独立同分布(不同类别标签分布)的许多方法之一,并观察到了不同严重程度的非独立同分布数据集如何影响 FedAvg 的性能。我们鼓励你探索如何通过替代聚合方法在这些情况下改进 FedAvg。
这两个示例也应该让你对不同联邦学习框架工作时的一般趋势有了坚实的理解;虽然具体的实现级细节可能会改变(由于该领域的快速变化),但核心概念和实现细节将仍然是基础。
在下一章中,我们将继续转向联邦学习的商业应用方面,通过研究涉及联邦学习在特定领域应用的几个案例研究。