一、为什么迭代要分"有界"和"无界"?
1)有界迭代(Bounded Iteration):离线训练的主力
特点:
-
训练数据是有限集(bounded dataset)
-
算法会反复扫描数据多轮(epoch),不断更新参数
-
一般会跑到:
- 达到指定轮数(epochs)
- 或者损失收敛、满足终止条件
例子:离线 KMeans、LR/Logistic Regression、GBDT 的迭代训练过程(概念上)。
2)无界迭代(Unbounded Iteration):在线训练 / 持续学习
特点:
-
训练数据是无限流(unbounded dataset)
-
不可能"扫完整个数据集"再做下一轮
-
通常做法是:
- 累积一个 mini-batch
- 做一次参数更新
- 持续进行
例子:在线学习、实时风控模型增量更新、持续推荐模型更新(概念上)。
二、Flink ML 的迭代范式:Iteration Paradigm
Flink ML 抽象了一个统一的"迭代范式",用 Flink 的概念来描述一个迭代算法:
1)迭代算法的行为模式
一个迭代算法通常是这样运行的:
-
它有一个"迭代体"(iteration body),会反复执行;
-
每一轮迭代体都会基于:
- 用户提供的数据(user-provided data)
- 当前最新的模型参数(model variables)
来更新参数;
-
输入包含:
- 初始模型参数(initial model parameters)
- 用户数据(data)
-
输出可以是:
- 每轮 loss、指标
- 最终模型参数
- 任何你想让用户"观察到"的结果
2)把它映射到 Flink 的子图(Subgraph)
在 Flink ML 里,迭代体 iteration body 被看成一个 Flink 子图(subgraph),它的输入输出被统一定义为:
-
输入(Inputs)
- model-variables:模型变量流(一组 DataStreams)
- user-provided-data:用户数据流(另一组 DataStreams)
-
输出(Outputs)
- feedback-model-variables:反馈回路的模型变量流(用于下一轮)
- user-observed-outputs:用户可见输出流(例如 loss、最终模型等)
3)核心点:model-variables ≠ initVariableStreams
很多人第一次读这里会卡住:
"迭代体需要的 model-variables,不是用户提供的 initVariableStreams 吗?"
不是。
Flink ML 规定:
- 用户只提供 初始模型变量(initVariableStreams)
- 迭代体会产生 反馈模型变量(feedback-model-variables)
- 真正传给迭代体的 model-variables 是两者的 union:
model-variables = union(initVariableStreams, feedback-model-variables)
这意味着:
- 第 0 轮:只有 initVariableStreams(epoch=0)
- 第 1 轮开始:既有 init 也有上一轮反馈回来的变量(epoch=1/2/...)
Flink ML 会通过 Iterations 工具类把这套"union + feedback"的 wiring 组装起来,用户只需要提供迭代体逻辑。
三、核心 API:Iterations
Flink ML 的迭代入口在 Iterations 类,它提供两种主要方法(按输入数据类型区分):
java
public class Iterations {
public static DataStreamList iterateUnboundedStreams(
DataStreamList initVariableStreams,
DataStreamList dataStreams,
IterationBody body) { ... }
public static DataStreamList iterateBoundedStreamsUntilTermination(
DataStreamList initVariableStreams,
ReplayableDataStreamList dataStreams,
IterationConfig config,
IterationBody body) { ... }
}
1)你需要提供三样东西
构建迭代时,用户必须提供:
-
initVariableStreams- 初始模型变量(会被每轮更新)
- 例如初始权重向量、初始聚类中心等
-
dataStreams- 迭代过程中用到的"用户数据",但它本身不走 feedback 更新
- 有界迭代一般需要可 replay(多轮重复读取)
-
iterationBody(迭代体逻辑)-
定义如何用(变量流 + 数据流)计算:
- 新的反馈变量流(newModelUpdate)
- 以及输出流(loss / modelOutput / metrics 等)
-
四、IterationBody:你写的"迭代核心逻辑"
IterationBody 接口长这样:
java
public interface IterationBody extends Serializable {
IterationBodyResult process(DataStreamList variableStreams, DataStreamList dataStreams);
}
它的两个入参:
variableStreams:模型变量流(由 init + feedback union 得到)dataStreams:用户数据流(传入的那批数据)
它的返回值 IterationBodyResult 包含两类输出:
- feedback variable streams:下一轮的模型变量(走反馈边)
- user-observed outputs:用户可见输出(不走反馈边)
五、Epoch 机制:每条数据都带"迭代轮次"
为了让系统知道"迭代进度",Flink ML 在迭代运行时会给每条参与迭代的数据打上 epoch 标记,用于表示它属于第几轮迭代。
epoch 的规则总结如下:
-
初始变量流、初始数据流中的所有记录:
epoch = 0 -
从算子输出到**非反馈流(普通输出)**的记录:
- 输出记录的 epoch = 触发该输出的输入记录 epoch
- 如果是由
onEpochWatermarkIncremented()发出的记录,则 epoch = 当前 epochWatermark
-
输出到**反馈变量流(feedback stream)**的记录:
- 输出记录的 epoch = 输入记录 epoch + 1
- 这条规则非常关键:意味着反馈回来的变量会自动进入"下一轮"
迭代监听:IterationListener
框架在每个 epoch 结束时,会通知实现了 IterationListener 的算子 / UDF:
java
public interface IterationListener<T> {
void onEpochWatermarkIncremented(int epochWatermark, Context context, Collector<T> collector)
throws Exception;
void onIterationTerminated(Context context, Collector<T> collector) throws Exception;
}
用途非常实用:
- 每轮结束时输出一条 loss / metric
- 每轮结束时触发 checkpoint / 日志
- 迭代终止时输出最终模型等
六、示例代码解读:无界迭代的"在线参数更新"模式
你提供的示例属于"迭代 API 的典型用法"。我把它按意图解读一下:
java
DataStream<double[]> initParameters = ...
DataStream<Tuple2<double[], Double>> dataset = ...
DataStreamList resultStreams = Iterations.iterateUnboundedStreams(
DataStreamList.of(initParameters),
ReplayableDataStreamList.notReplay(dataset),
IterationConfig.newBuilder().setOperatorRoundMode(ALL_ROUND).build();
(variableStreams, dataStreams) -> {
DataStream<double[]> modelUpdate = variableStreams.get(0);
DataStream<Tuple2<double[], Double>> dataset = dataStreams.get(0);
DataStream<double[]> newModelUpdate = ...
DataStream<double[]> modelOutput = ...
return new IterationBodyResult(
DataStreamList.of(newModelUpdate),
DataStreamList.of(modelOutput)
});
1)每个变量代表什么?
-
initParameters:初始参数(比如模型权重 w0)这是 要走 feedback 的变量(会在迭代中更新)
-
dataset:训练数据流(无限流 / 在线数据)这是 不走 feedback 的输入数据(不需要每轮 replay)
-
modelUpdate:本轮使用的模型参数(由 init + feedback union 得到) -
newModelUpdate:更新后的模型参数(通过 feedback 返回给下一轮) -
modelOutput:用户可见输出例如每轮输出当前参数、loss、或者最终模型等(不走 feedback)
最后:
java
DataStream<double[]> finalModel = resultStreams.get("final_model");
意味着 resultStreams 里会包含你在 IterationBodyResult 中定义的输出流(名称具体取决于实现返回的 key 方式,这里是示意)。
七、工程落地建议:怎么选用界 vs 无界?怎么组织输出?
1)什么时候用 iterateBounded?
适用于典型离线训练:
- 数据集是有限的(批数据 / bounded 流)
- 需要反复多轮训练直到终止条件
- 更关注"收敛"与"最终模型质量"
一般配合:
ReplayableDataStreamList(确保每轮都能重复消费数据)IterationConfig(配置终止条件、轮次、算子 round mode 等)
2)什么时候用 iterateUnbounded?
适用于在线训练 / 增量学习:
- 数据无限流入(Kafka 等)
- 以 mini-batch / 增量更新参数
- 更关注"持续更新"和"实时适应"
3)输出设计建议(非常关键)
强烈建议你在 iteration body 里输出两类内容:
-
用户可见指标流 :例如每个 epoch 输出 loss、样本数、梯度范数等
方便你在 Flink UI 或日志里观察训练是否正常
-
模型参数流 :最终模型/中间模型
你可以:
- 写到 Kafka 作为在线模型下发
- 写到 HDFS/Hive 作为离线模型落盘
- 或写到 Redis/ES 供在线预测服务读取
八、总结
Flink ML 的迭代能力,核心是把"机器学习迭代训练"抽象为 Flink 的可组合子图:
-
两类迭代:
- Bounded Iteration:离线、多轮、可 replay、直到终止
- Unbounded Iteration:在线、无限流、mini-batch、持续更新
-
统一范式:
- iteration body 接收:变量流 + 数据流
- iteration body 输出:反馈变量流 + 用户可见输出流
- 变量流由:init + feedback union 形成
-
工程关键点:
- epoch 标记帮助组织多轮训练
- IterationListener 帮助你在每轮结束输出指标、做收尾
- 迭代输出分离:feedback 更新 vs 用户观测输出