使用 Flower 与 Hugging Face Transformers 联邦微调大语言模型:IMDB 情感分类快速入门指南
【免费下载链接】flowerFlower: A Friendly Federated AI Framework项目地址: https://gitcode.com/GitHub_Trending/flo/flower
本指南基于 Flower 官方 Quickstart 教程,讲解如何用 Flower 联合 Hugging Face Transformers,在 IMDB 电影评论数据集上联邦微调一个预训练 Transformer 模型(bert-tiny),实现"正面/负面"二分类情感分析。读完本文,你将掌握用flwr new一键生成 Flower 应用、通过flwr run在本地模拟引擎与 GPU 环境下运行两节点联邦、以及ClientApp/ServerApp中Message、ArrayRecord与 PyTorchstate_dict之间转换的完整实战方法,并了解其背后的源码级实现原理。
教程概览:联邦学习 + 预训练语言模型
本教程在 Flower 与 Hugging Face Transformers 的配合下,将"大语言模型(LLM)"的微调过程联邦化:
- 数据集:
stanfordnlp/imdb电影评论数据集,使用 Flower Datasets 的IidPartitioner划分为多个 IID(独立同分布)分区,每个客户端只持有属于自己的那份数据; - 模型:从 Hugging Face Hub 加载预训练
bert-tiny序列分类模型,输出 2 个类别(正面/负面); - 联邦策略:默认使用 FedAvg(联邦平均),在两台节点上运行 3 轮;
- 运行时:
flwr run启动本地受管 SuperLink,由 Flower Simulation Runtime(模拟运行时)执行整轮联邦流程。
核心思路是:每个客户端用自己本地分区的 IMDB 数据对接收到的全局模型做若干步微调,把更新后的权重发回服务端,服务端用 FedAvg 聚合出新的全局模型,如此迭代多轮。文中所有代码均来自仓库中的 examples/quickstart-huggingface 示例,你可直接对照阅读。
环境准备与项目创建
创建虚拟环境并安装 Flower
官方建议先创建一个 Python 虚拟环境(venv / virtualenv),在干净的环境中运行整个流程,具体步骤可参考 contributor-how-to-set-up-a-virtual-env.rst。随后安装 Flower:
# 在全新的 Python 环境中 $ pip install flwr用flwr new一键生成项目
Flower 提供flwr new命令从模板生成一个完整的 Flower + Hugging Face 项目:
$ flwr new @flwrlabs/quickstart-huggingface运行后当前目录下会新增一个名为quickstart-huggingface的目录,结构如下:
quickstart-huggingface ├── huggingface_example │ ├── __init__.py │ ├── client_app.py # 定义 ClientApp │ ├── server_app.py # 定义 ServerApp │ └── task.py # 定义模型、训练与数据加载 ├── pyproject.toml # 项目元数据、依赖与配置 └── README.md各文件的职责非常清晰:task.py封装与 Flower 无关的纯模型/数据逻辑;client_app.py定义参与训练的ClientApp;server_app.py定义编排联邦流程的ServerApp;pyproject.toml集中声明依赖、入口点与运行配置。
安装项目依赖
进入项目目录后,以可编辑模式安装pyproject.toml中声明的依赖以及huggingface_example包本身:
$ cd quickstart-huggingface $ pip install -e .该示例的依赖声明位于 pyproject.toml,主要包括:
flwr[simulation]>=1.36.0:Flower 框架及模拟引擎;flwr-datasets>=0.6.1:提供FederatedDataset与IidPartitioner;torch==2.10.0:PyTorch 后端;transformers>=4.30.0,<5.0:Hugging Face Transformers;evaluate>=0.4.0,<1.0:提供load_metric("accuracy")等评估指标;scikit-learn>=1.3.1, <2.0:evaluate库计算准确率的依赖。
运行联邦训练
使用模拟引擎运行(默认配置)
在项目根目录下执行:
$ flwr run . --stream带--stream参数会实时流式打印日志;不带--stream的flwr run .则只提交运行、打印运行 ID 后立即返回。默认情况下,flwr run会使用本地模拟 Profile:启动一个受管的本地 SuperLink,并把运行任务提交给它,由 Flower Simulation Runtime 在两个虚拟节点上执行。
默认参数下你会看到类似如下的流式输出:
Starting local SuperLink on 127.0.0.1:39091... Successfully started run 1859953118041441032 INFO : Starting FedAvg strategy: INFO : ├── Number of rounds: 3 INFO : [ROUND 1/3] INFO : configure_train: Sampled 2 nodes (out of 2) INFO : aggregate_train: Received 2 results and 0 failures INFO : └──> Aggregated MetricRecord: {'train_loss': 0.6974} INFO : configure_evaluate: Sampled 2 nodes (out of 2) INFO : aggregate_evaluate: Received 2 results and 0 failures INFO : └──> Aggregated MetricRecord: {'val_loss': 0.0223, 'val_accuracy': 0.5024} INFO : [ROUND 2/3] INFO : ... INFO : [ROUND 3/3] INFO : ... INFO : Strategy execution finished in 151.02s INFO : Final results: INFO : ServerApp-side Evaluate Metrics: INFO : {}这段日志展示了 FedAvg 每轮的标准流程:服务端从 2 个节点中采样参与训练的节点(configure_train: Sampled 2 nodes),等待各节点返回训练结果后聚合(aggregate_train得到train_loss);随后进入评估阶段(configure_evaluate/aggregate_evaluate得到验证集上的val_loss与val_accuracy)。想要了解本地工作流的完整细节,可查阅 how-to-run-flower-locally.rst。
使用 GPU 运行
如果本机有 GPU,可以用localhost-gpu这个内置 Profile 运行:
# 使用默认参数运行 $ flwr run . localhost-gpu --stream该 Profile 的默认资源配置为:每个ClientApp使用 4 个 CPU,并且同一张 GPU 上最多并发运行 4 个ClientApp(即每个客户端占用约 0.25 张 GPU)。另一种等价做法是先用flwr federation simulation-config命令显式配置模拟资源,再运行应用:
flwr federation simulation-config \ --client-resources-num-cpus=4 \ # 每个 ClientApp 假设使用 4 个 CPU --client-resources-num-gpus=0.25 # 每张 GPU 上最多运行 4 个 ClientApp更多模拟配置方式可参考 how-to-run-simulations.rst。
覆盖pyproject.toml中的默认参数
pyproject.toml的[tool.flwr.app.config]节定义了应用的运行参数(详见下一节)。你可以通过--run-config在不改动文件的前提下覆盖它们:
# 覆盖部分参数 $ flwr run . --run-config "num-server-rounds=5 fraction-train=0.2"上面的命令把联邦轮数改为 5、训练采样比例改为 0.2,其余参数保持默认。
配置详解:pyproject.toml
pyproject.toml 是 Flower App 的配置中心,除了常规的构建系统与依赖声明外,还包含以下 Flower 专属配置节:
[tool.flwr.app] publisher = "flwrlabs" fab-format-version = 1 flwr-version-target = "1.37.0" [tool.flwr.app.components] serverapp = "huggingface_example.server_app:app" clientapp = "huggingface_example.client_app:app" [tool.flwr.app.config] num-server-rounds = 3 model-name = "prajjwal1/bert-tiny" fraction-train = 1.0 fraction-evaluate = 1.0 save-model = false各配置项含义如下:
| 配置键 | 默认值 | 说明 |
|---|---|---|
num-server-rounds | 3 | FedAvg 联邦训练的轮数,对应strategy.start(num_rounds=...) |
model-name | "prajjwal1/bert-tiny" | 从 Hugging Face Hub 加载的预训练模型标识 |
fraction-train | 1.0 | 每轮参与训练的节点采样比例(FedAvg 的fraction_train) |
fraction-evaluate | 1.0 | 每轮参与评估的节点采样比例(FedAvg 的fraction_evaluate) |
save-model | false | 若为true,服务端在训练结束后把全局模型state_dict保存为final_model.pt |
[tool.flwr.app.components]指定了ServerApp与ClientApp的 Python 入口点;[tool.flwr.app]记录了发布者、FAB 格式版本与目标 Flower 版本,供flwr打包与版本校验使用。
数据准备:Flower Datasets 与 IMDB 分区
数据加载逻辑封装在 task.py 的load_data()中。它借助 Flower Datasets 下载 IMDB 数据集,用IidPartitioner生成num_partitions个分区,并完成分词与 DataLoader 构建:
partitioner = IidPartitioner(num_partitions=num_partitions) fds = FederatedDataset( dataset="stanfordnlp/imdb", partitioners={"train": partitioner}, ) partition = fds.load_partition(partition_id) # 划分数据:80% 训练,20% 测试 partition_train_test = partition.train_test_split(test_size=0.2, seed=42) tokenizer = AutoTokenizer.from_pretrained(model_name, model_max_length=512) def tokenize_function(examples): return tokenizer( examples["text"], truncation=True, add_special_tokens=True ) partition_train_test = partition_train_test.map(tokenize_function, batched=True) partition_train_test = partition_train_test.remove_columns("text") partition_train_test = partition_train_test.rename_column("label", "labels") data_collator = DataCollatorWithPadding(tokenizer=tokenizer) trainloader = DataLoader( partition_train_test["train"], shuffle=True, batch_size=32, collate_fn=data_collator, ) testloader = DataLoader( partition_train_test["test"], batch_size=32, collate_fn=data_collator )要点拆解:
- IID 分区:
IidPartitioner(num_partitions=...)把训练集均匀切分为指定数量的分区。从 iid_partitioner.py 的源码可以看到,其load_partition(partition_id)底层是对数据集执行dataset.shard(num_shards=num_partitions, index=partition_id, contiguous=True),即按连续分片方式返回第partition_id个分区。除IidPartitioner外,Flower Datasets 还提供多种其他分区器(如 Dirichlet 非 IID 分区等),可按需选用; - 本地切分:每个客户端拿到自己的分区后,再用
train_test_split(test_size=0.2, seed=42)在该分区内部切出 80% 训练集与 20% 验证集; - 分词:使用与训练模型配套的
AutoTokenizer,设置model_max_length=512、truncation=True与add_special_tokens=True,把text列分词后移除原始文本、并把label列改名为labels,与 Transformers 序列分类模型的输入约定对齐; - 动态填充:
DataCollatorWithPadding在批次内动态补齐到等长,两个DataLoader的batch_size=32; - 缓存优化:示例中把
FederatedDataset缓存在模块级全局变量fds(task.py顶部fds = None),保证每个客户端进程只下载/初始化数据集一次。
每个ClientApp都会调用该函数,用自己partition_id对应的数据构建训练与评估 DataLoader。
模型与训练/评估函数
模型同样在 task.py 中定义。get_model()从 Hugging Face Hub 加载model_name对应的预训练 Transformer,并为其套上 2 分类头:
net = AutoModelForSequenceClassification.from_pretrained( model_name, num_labels=2 )这里model_name是一个字符串,由ClientApp/ServerApp从各自的Context运行配置中读取(默认prajjwal1/bert-tiny)。目标任务是判断一条电影评论是正面还是负面,即 2 分类。如果你的 GPU 显存更大,也可以替换成更大的模型。
除加载预训练权重与结构外,task.py还提供两个与 Flower 无关的标准 PyTorch 工具函数,供客户端在本地数据上调用:
def train_fn(net, trainloader, epochs, device) -> None: optimizer = AdamW(net.parameters(), lr=5e-5) net.train() for _ in range(epochs): for batch in trainloader: batch = {k: v.to(device) for k, v in batch.items()} outputs = net(**batch) loss = outputs.loss loss.backward() optimizer.step() optimizer.zero_grad() def test_fn(net, testloader, device) -> tuple[Any | float, Any]: metric = load_metric("accuracy") loss = 0 net.eval() for batch in testloader: batch = {k: v.to(device) for k, v in batch.items()} with torch.no_grad(): outputs = net(**batch) logits = outputs.logits loss += outputs.loss.item() predictions = torch.argmax(logits, dim=-1) metric.add_batch(predictions=predictions, references=batch["labels"]) loss /= len(testloader.dataset) accuracy = metric.compute()["accuracy"] return loss, accuracytrain_fn使用AdamW(学习率5e-5)逐批次前向、反向、更新参数;test_fn在no_grad下推理,用evaluate库的accuracy指标统计准确率并返回平均损失。这两个函数不包含任何 Flower 特有逻辑,可完全复用你熟悉的中心化 PyTorch 训练/评估代码——这正是 Flower 的设计初衷:客户端的本地训练逻辑与联邦框架解耦。
ClientApp:Message 与 ArrayRecord 的双向转换
把 Hugging Face 模型接入 Flower,核心改动在于:把Message中携带的ArrayRecord转换为 PyTorchstate_dict加载进模型,训练完成后再把state_dict转回ArrayRecord放进回复消息。得益于ArrayRecord的内置方法,这一转换非常简洁:
# 加载模型 model = get_model(model_name) # 从 Message 中取出 ArrayRecord 并转换为 PyTorch state_dict arrays = msg.content["arrays"] # 把 state_dict 加载进模型 model.load_state_dict(arrays.to_torch_state_dict(), strict=True) # ... 进行本地训练 ... # 把 state_dict 转回 ArrayRecord model_record = ArrayRecord(model.state_dict())ClientApp提供三个核心方法:train(用本地数据训练收到的模型)、evaluate(在本地验证集上评估收到的模型)、query(查询运行ClientApp的节点信息)。本教程只用到前两个。
实现@app.train()
train方法接收来自ServerApp的Message,默认携带:
- 一个
ArrayRecord,存放待联邦训练的模型权重,通过msg.content["arrays"]获取; - 一个
ConfigRecord,存放ServerApp下发的配置,通过msg.content["config"]获取。
同时它还能拿到Context:run_config对应pyproject.toml中定义的应用运行超参;node_config是节点级配置,模拟运行时由系统注入(如partition-id、num-partitions),部署运行时才可显式设置。完整实现见 client_app.py:
# Flower ClientApp app = ClientApp() @app.train() def train(msg: Message, context: Context) -> Message: """在本地数据上训练模型。""" # 获取该客户端的本地数据分区 partition_id = context.node_config["partition-id"] num_partitions = context.node_config["num-partitions"] model_name = context.run_config["model-name"] trainloader, _ = load_data(partition_id, num_partitions, model_name) # 加载模型 model = get_model(model_name) # 用接收到的权重初始化模型 arrays = msg.content["arrays"] model.load_state_dict(arrays.to_torch_state_dict(), strict=True) device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") model.to(device) # 在本地数据上训练模型 train_fn(model, trainloader, epochs=1, device=device) # 构造并返回回复 Message model_record = ArrayRecord(model.state_dict()) metrics = MetricRecord({"num-examples": len(trainloader)}) # 构造 RecordDict,放入 ArrayRecord 与 MetricRecord content = RecordDict({"arrays": model_record, "metrics": metrics}) return Message(content=content, reply_to=msg)可见训练流程非常直观:按partition-id加载本地数据 → 用服务端下发的全局权重初始化模型 → 本地训练 1 个 epoch → 把更新后的state_dict封装进ArrayRecord,连同num-examples指标一起放入RecordDict,最后以Message(reply_to=msg)的形式返回给服务端。num-examples是 FedAvg 聚合时的权重依据(详见下文FedAvg的weighted_by_key参数)。
实现@app.evaluate()
@app.evaluate()与train几乎相同,仅有两点差异:
- 模型不做本地训练,而是直接在本地留出的验证集上评估性能;
- 回复消息中不再需要携带模型(因为模型未被本地修改),只需返回评估指标。
仓库中的实现会在MetricRecord中额外带上loss与accuracy(见 client_app.py 的evaluate方法),这正是日志里val_loss、val_accuracy指标的来源。
ServerApp:FedAvg 策略与全局模型聚合
ServerApp的核心是@app.main()方法,它接收两个参数:
Grid:与服务端交互的接口,用于调度运行ClientApp的节点参与每轮 train/evaluate/query;Context:提供运行配置。
本示例使用FedAvg策略,其fraction_train从运行配置读取(默认值定义在pyproject.toml)。随后调用策略的start方法启动联邦流程,传入:
Grid对象;- 一个携带随机初始化模型的
ArrayRecord,作为待联邦化的全局模型; - 发送给客户端的训练超参
ConfigRecord(策略会在下发前自动注入当前轮号); num_rounds:指定执行多少轮 FedAvg。
完整实现见 server_app.py:
# 创建 ServerApp app = ServerApp() @app.main() def main(grid: Grid, context: Context) -> None: # 定义要联邦化的模型并提取参数 model_name = context.run_config["model-name"] model = get_model(model_name) arrays = ArrayRecord(model.state_dict()) # 实例化策略 fraction_train = context.run_config["fraction-train"] fraction_evaluate = context.run_config["fraction-evaluate"] strategy = FedAvg( fraction_train=fraction_train, fraction_evaluate=fraction_evaluate, ) num_rounds = context.run_config["num-server-rounds"] # 启动策略 result = strategy.start( grid=grid, initial_arrays=arrays, num_rounds=num_rounds, ) if context.run_config["save-model"]: # 把最终模型保存到磁盘 print("\nSaving final model to disk...") state_dict = result.arrays.to_torch_state_dict() torch.save(state_dict, "final_model.pt")start方法返回一个Result对象,其中包含联邦过程的全部关键信息:以ArrayRecord形式给出的最终全局模型权重,以及各轮训练/评估的MetricRecord指标。你可以用 Python 标准库pprint打印这些指标;当save-model为true时,result.arrays转回 PyTorchstate_dict后即可用torch.save落盘。
底层原理:从源码看关键实现
ArrayRecord 的 PyTorch 桥接
ArrayRecord是 Flower 在客户端与服务端之间传输模型权重的载体,其源码位于 arrayrecord.py。与本教程直接相关的两个方法是:
from_torch_state_dict(state_dict, keep_input=True):逐个遍历state_dict的键,调用Array.from_numpy_ndarray(v.detach().cpu().numpy())把每个 PyTorch 张量转成 Flower 的Array(即"Tensor → ArrayRecord");to_torch_state_dict():反向遍历,用torch.from_numpy(arr.numpy())把每个Array还原成 PyTorch 张量并组装成OrderedDict(即"ArrayRecord → Tensor")。
两个方法都会先检查torch是否已导入,未安装 PyTorch 时抛出明确的RuntimeError提示。这就是client_app.py中arrays.to_torch_state_dict()与ArrayRecord(model.state_dict())两行代码背后的完整机制。
FedAvg 策略与start()的调度循环
FedAvg类定义在 fedavg.py,其关键参数包括:fraction_train(训练采样比例)、fraction_evaluate(评估采样比例)、min_train_nodes/min_evaluate_nodes/min_available_nodes(各类最小节点数,默认 2)、以及weighted_by_key(默认"num-examples",聚合时以各客户端上报的样本数作为加权平均的权重键)——这解释了为什么ClientApp必须在MetricRecord中上报num-examples。
策略基类Strategy.start()的调度循环在 strategy.py 中,每一轮按以下顺序执行:
configure_train(round, arrays, train_config, grid)构造训练消息,经grid.send_and_receive(messages=..., timeout=...)发给采样到的节点并等待回复;aggregate_train(...)聚合训练结果,得到新一轮全局参数agg_arrays与聚合训练指标(对应日志中的train_loss);configure_evaluate(...)+grid.send_and_receive(...)发起客户端评估;aggregate_evaluate(...)聚合评估指标(对应日志中的val_loss/val_accuracy);- 循环结束后,日志打印
Strategy execution finished in ...并返回包含最终arrays与各轮指标的Result。
整个流程与前面flwr run的流式日志逐行对应,理解这段源码即可精确掌握"采样 → 下发 → 本地训练/评估 → 聚合"的完整时序。
小结与进一步阅读
至此,你已经完成了第一个联邦学习系统的搭建与运行:数据经IidPartitioner在客户端间划分,bert-tiny模型在本地 IMDB 分区上微调,权重经ArrayRecord往返传输,FedAvg在服务端聚合出全局模型,最终输出联邦训练/评估指标,并可选择保存模型权重。
如果你希望继续深入,仓库中有以下资源可供参考:
- 关于模拟引擎的配置与调优,阅读 how-to-run-simulations.rst;
- 关于本地运行(SuperLink 启动、日志流式输出等)的完整说明,阅读 how-to-run-flower-locally.rst;
- 本教程的完整可运行源码位于 examples/quickstart-huggingface,内含 task.py、client_app.py、server_app.py 与 README.md;
- 若要进行更大规模的 LLM 联邦微调,可参考仓库中的 FlowerTune LLM 系列示例(如 flowertune-llm),它们展示了更完整的训练、评估与实验组织方式;
- 若想把这套应用部署到真实分布式环境中,可参考 how-to-run-flower-with-deployment-engine.rst,并进一步了解 how-to-enable-tls-connections.rst 等安全通信配置。
【免费下载链接】flowerFlower: A Friendly Federated AI Framework项目地址: https://gitcode.com/GitHub_Trending/flo/flower
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考