3、TFF核心概念:计算、类型系统、联合计算、放置与执行

好,咱们进入正题。TensorFlow Federated(TFF)这套框架,刚接触时确实有点绕。它不像你平时写Keras或者PyTorch那样,直接定义模型、丢数据、跑循环。TFF有自己的一套“世界观”。

我个人习惯,学一个新框架,先别急着写代码。先把它的核心概念摸清楚。不然你写出来的代码,看着像那么回事,跑起来全是坑。今天我们就来拆解TFF的四个核心概念:计算类型系统联合计算放置与执行

3.1 计算(Computation)—— TFF的“最小积木块”

在TFF里,一切皆计算。你写的任何逻辑,最终都会被表示成一个计算。你可以把它理解成一个函数:有输入,有输出,内部有逻辑。

但这里的“计算”不是Python函数。它是TFF自己定义的一种中间表示(IR)。说白了,TFF把你的Python代码“编译”成了一套它自己能理解的计算图。

核心要点: TFF的计算是序列化的、可组合的。你可以把一个小计算塞进一个大计算里,像搭积木一样。

举个例子,一个简单的加法,在TFF里是这样定义的:

@tff.tf_computation
def add_one(x):
    return x + 1

嗯,这里要注意。这个@tff.tf_computation装饰器,就是把一个普通的TensorFlow函数,变成了一个TFF计算。我在项目中遇到过,有人直接在里面写Python的print,结果发现根本不会打印。为什么?因为这段代码不是在本地执行的,它被序列化后发送到了别的地方去跑。

避坑指南: 我曾经在tff.tf_computation里用了if x is None这种Python逻辑,结果报错。记住,里面只能写TensorFlow ops,不能写纯Python控制流。

3.2 类型系统(Type System)—— 别让类型坑了你

TFF有自己的类型系统。它不像Python那么随意。每个计算,每个数据,都有明确的类型。

主要有这么几种:

  • TensorType:就是张量类型,比如tf.float32,形状是(3, 4)
  • StructType:结构体类型,可以包含多个命名字段。相当于Python里的字典或命名元组。
  • SequenceType:序列类型,表示一组数据。在联邦学习中,客户端的数据集通常就是这个类型。
  • FunctionType:函数类型,描述一个计算的输入和输出。
  • PlacementType:放置类型,后面会细说。

你想想看,为什么TFF要搞这么一套?因为联邦学习涉及多个客户端,数据格式必须统一。不然A客户端传上来一个int32,B客户端传上来一个float64,服务器怎么聚合?

看个例子:

# 定义一个结构体类型
model_weights_type = tff.StructType([
    ('weights', tff.TensorType(tf.float32, [10, 10])),
    ('bias', tff.TensorType(tf.float32, [10]))
])

我个人习惯,在写任何联合计算之前,先把类型定义清楚。这能省掉后面80%的调试时间。

注意: TFF的类型是静态的。一旦定义好,运行时不能变。如果你在tff.tf_computation里返回了一个形状不固定的张量,TFF会直接报错。我刚开始用的时候,被这个坑了好几次。

3.3 联合计算(Federated Computation)—— 联邦学习的“灵魂”

这是TFF最核心的概念。联合计算,说白了就是描述“数据在客户端,模型在服务器,它们之间怎么协作”的计算。

普通的计算(比如tff.tf_computation)是在单机上跑的。联合计算(tff.federated_computation)是在多个参与者之间跑的。

看一个最简单的联合计算:

@tff.federated_computation
def hello_federated():
    return tff.federated_value('Hello, Federated!', tff.SERVER)

这个计算做了什么?它把一个字符串放到了服务器上。注意这里的tff.SERVER,它是一个放置(Placement)。

联合计算里,你经常会看到这些操作:

  • tff.federated_broadcast:把服务器上的东西广播给所有客户端。
  • tff.federated_sum:把所有客户端的东西加起来,放到服务器上。
  • tff.federated_mean:求所有客户端东西的平均值。
  • tff.federated_map:在每个客户端上独立执行一个函数。

这些操作,就是联邦学习算法的“积木块”。你想想看,FedAvg算法不就是:广播模型 -> 客户端本地更新 -> 上传更新 -> 服务器聚合吗?

核心要点: 联合计算是声明式的。你告诉TFF“我要做什么”,而不是“怎么做”。TFF会帮你处理底层的通信、序列化、执行。

3.4 放置与执行(Placement & Execution)—— 数据在哪,代码在哪跑

放置,是TFF里一个非常独特的概念。它回答了三个问题:

  1. 数据放在哪? 是在服务器上,还是在客户端上?
  2. 计算在哪执行? 是在服务器上跑,还是在每个客户端上跑?
  3. 结果在哪? 最终的结果是放在服务器上,还是留在客户端?

TFF里有两个内置的放置:tff.SERVERtff.CLIENTS

看个例子:

@tff.federated_computation
def federated_sum(x):
    # x 是放在客户端上的
    return tff.federated_sum(x)  # 结果放在服务器上

这里,输入x的类型是{float32}@CLIENTS,输出类型是{float32}@SERVER。那个@CLIENTS@SERVER就是放置标注。

执行呢?TFF的执行是延迟的。你定义了一个联合计算,它不会立刻跑。你需要显式地调用它,并且提供一个执行上下文。

在本地调试时,我们通常用tff.backends.native.set_local_python_execution_context()。这个上下文会在本地模拟多个客户端。我在项目中遇到过,有人直接在生产环境用了本地上下文,结果性能惨不忍睹。记住,本地上下文只适合调试。

避坑指南: 我曾经在定义联合计算时,忘记指定放置。结果TFF报了一个很奇怪的类型错误。后来我才发现,tff.federated_sum要求输入必须是@CLIENTS放置的,我传了一个@SERVER的进去。嗯,类型系统在这里卡得很死。

3.5 它们怎么配合?—— 一个完整的例子

说了这么多,我们来看一个完整的、简单的联邦平均(FedAvg)的骨架。注意,这里只是骨架,让你感受一下这些概念怎么配合。

# 1. 定义类型
model_weights_type = tff.StructType(...)
client_data_type = tff.SequenceType(...)

# 2. 定义客户端本地计算(普通计算)
@tff.tf_computation
def client_update(model, data):
    # 在本地数据上训练模型
    return updated_model

# 3. 定义服务器聚合计算(普通计算)
@tff.tf_computation
def server_aggregate(models):
    # 平均多个模型
    return averaged_model

# 4. 定义联合计算(核心)
@tff.federated_computation
def fed_average(model_weights, client_data):
    # 广播模型到客户端
    model_at_clients = tff.federated_broadcast(model_weights)
    # 每个客户端本地更新
    updated_models = tff.federated_map(client_update, (model_at_clients, client_data))
    # 聚合回服务器
    return tff.federated_mean(updated_models)

你看,整个流程非常清晰。类型系统保证了数据格式一致,放置标注指明了数据流向,联合计算描述了协作逻辑,而底层的执行上下文负责真正跑起来。

我个人觉得,TFF这套设计,虽然学习曲线陡峭,但一旦理解了,写出来的代码非常优雅。它把联邦学习的复杂性,封装在了这几个核心概念里。

好,这一章就到这里。下一章我们会深入tff.tf_computationtff.federated_computation的细节,看看它们到底有什么区别,以及怎么用好它们。