- 人工智能
- 机器学习
- 流处理
- 数据分析
【免费下载链接】river
🌊 Online machine learning in Python
River 是面向 Python 的在线(流式)机器学习框架,其 0.18.0 版本发布于 2023-06-26,围绕"在线环境下的效率与稳健性"对多个核心模块做了升级。本文以官方发布说明(docs/releases/0.18.0.md)为骨架,逐项拆解 bandit、cluster、compat、compose、neighbors、preprocessing、proba、tree、utils 九个模块的变更,并结合仓库源码给出参数语义、底层机制与可运行的示例,帮助你在实际项目中准确选用这些新能力。
1. bandit:新增 Bayes-UCB 策略与离线评估工具
1.1bandit.BayesUCB:用后验分位数驱动探索与利用
0.18.0 新增了bandit.BayesUCB策略,其核心思想是在多臂老虎机问题中使用每个臂回报的后验分布计算置信上界(UCB),再拉动 UCB 最高的臂。与经典 UCB(基于频率统计的 Hoeffding 不等式)不同,Bayes-UCB 属于贝叶斯方法:先用 Beta 分布对每个臂的成功率建模,再取后验分布的特定分位数作为上界。
从源码 river/bandit/bayes_ucb.py 可以看到其实现细节:
compute_index(arm_id)计算第p分位数,其中p = 1 - 1 / (self._n + 1)(_n是已执行的更新次数),随后通过scipy.special.betaincinv求 Beta 分布逆 CDF;update在基类更新完统计量后,用self._posteriors[arm_id].update(reward)同步更新该臂的 Beta 后验;_pull在多个臂分数并列时用内置随机数生成器随机挑选,因此支持seed复现。
构造参数(reward_obj、burn_in、seed)继承自bandit.base.Policy:burn_in表示每个臂在启用后验分布前的初始观测数,seed用于保证结果可复现。官方示例在river_bandits/CandyCaneContest-v0环境中累计回报为Sum: 841,验证了策略的可用性。
import gymnasium as gym from river import bandit from river import stats env = gym.make('river_bandits/CandyCaneContest-v0') _ = env.reset(seed=42) _ = env.action_space.seed(123) policy = bandit.BayesUCB(seed=123) metric = stats.Sum() while True: action = policy.pull(range(env.action_space.n)) observation, reward, terminated, truncated, info = env.step(action) policy.update(action, reward) metric.update(reward) if terminated or truncated: break print(metric) # Sum: 841.1.2bandit.evaluate_offline:基于历史日志的重放式离线评估
同版本还新增了bandit.evaluate_offline,用于在历史(logged)数据上评估 bandit 策略,属于不依赖环境的数据驱动评估方法。其核心逻辑(见 river/bandit/evaluate.py 中evaluate_offline):
- 每一步从策略中拉取一个臂;
- 若拉取的臂与历史数据中记录的臂相同,就用该步的奖励更新策略与统计量;
- 若不同,则跳过该步——这正是"off-policy"评估的精髓:策略与历史行为不一致的轨迹不会被用来更新。
函数的history参数既可以是生成(arms, context, arm, reward)四元组的迭代器,也可以是继承自river.bandit.BanditDataset的数据集(如bandit.datasets.NewsArticles),返回值为(reward_stat, n_samples_used)二元组,其中第二项即策略与历史数据拉动相同臂的次数。文档示例中EpsilonGreedy(0.1)在 1000 条随机日志上只使用了 321 条样本,而随机策略在 NewsArticles 数据集上约 10% 的成功率也与"10 个臂、期望 1000 次成功"的统计预期吻合。
import random from river import bandit rng = random.Random(42) arms = ['A', 'B', 'C'] clicks = [ (arms, None, rng.choice(arms), rng.random() > 0.5) for _ in range(1000) ] total_reward, n_samples_used = bandit.evaluate_offline( policy=bandit.EpsilonGreedy(0.1, seed=42), history=clicks, ) print(total_reward) # Sum: 172. print(n_samples_used) # 3212. cluster:DBStream 按需重聚类
0.18.0 优化了cluster.DBStream的计算开销:不再每次调用learn_one时都重聚类,而是仅在需要时(如访问predict_one、n_clusters、clusters、centers等聚类结果接口)才触发重聚类。
从 river/cluster/dbstream.py 的源码可验证这一机制:
learn_one只负责_update与按cleanup_interval清理,并把self.clustering_is_up_to_date置为False(标记"需要重聚类");predict_one与n_clusters、clusters、centers属性均会先调用_recluster();_recluster内部首先检查clustering_is_up_to_date,若为True直接返回,避免重复计算。
这一惰性求值策略在在线聚类场景下收益明显:当只训练不查询时(例如长期吸收数据流),可以完全跳过基于共享密度图的宏聚类合并(Algorithm 3),显著降低 CPU 开销。
3. compat:scikit-learn 模型桥接层的两项稳健性修复
0.18.0 修复了compat模块(scikit-learn ↔ River 兼容层)的两个问题:
3.1 未拟合模型的predict_many
此前,用compat.convert_sklearn_to_river包装且尚未在数据上拟合过的 scikit-learn 模型,调用predict_many会直接抛异常。现在改为与 River 其余部分保持一致:产生默认预测。从 river/compat/sklearn_to_river.py 可见SKL2RiverRegressor.predict_many会捕获sklearn_exceptions.NotFittedError并返回全零数组(与predict_one返回0的行为一致),分类器侧同理。
3.2 特征顺序的自动对齐
此前compat.SKL2RiverRegressor与compat.SKL2RiverClassifier不检查每次调用时特征是否按相同顺序排列。修复后,桥接层在第一次调用时记录特征名列表(self._feature_names),后续每次调用都会把输入对齐到该顺序。
源码中的_align_dict(用于dict输入)和_align_frame(用于 DataFrame 小批量输入)实现了这一逻辑:_align_frame在发现批次列顺序与首批次不一致时会按_feature_names重新select,若缺少首批次中出现过的特征则抛出明确错误,避免静默错位导致模型语义被破坏。
4. compose:小批量流水线能力补全
0.18.0 为组合(compose)模块补齐了三个与小批量(mini-batch)处理相关的能力:
compose.FuncTransformer新增transform_many方法(见 river/compose/func.py):函数式转换器现在可以直接应用于 DataFrame 批量输入,从而进入小批量流水线。其用法与transform_one完全对称——传入一个"输入 dict 输出 dict"的函数即可,例如解析日期并提取小时、周末标记:
import datetime as dt from river import compose def parse_date(x): date = dt.datetime.strptime(x['date'], '%Y-%m-%d') return {'is_weekend': date.day in (5, 6), 'hour': date.hour, **x} t = compose.FuncTransformer(parse_date) # 也可直接 parse_date | modelcompose.pure_inference_mode支持小批量:推理模式上下文管理器现在同样作用于批处理路径,便于在纯推理(不更新统计量)模式下处理 DataFrame。compose.TransformerProduct保持稀疏列密度:特征叉乘组合(|运算符将流水线组合为 TransformerProduct 的场景)现在会保留稀疏列的高效表示,避免在组合过程中退化为稠密内存布局。
5. neighbors:SWINN 图索引与可插拔搜索引擎
0.18.0 对邻居模块做了一次较大重构,核心变化有三:新增neighbors.SWINN、将neighbors.NearestNeighbors重命名为neighbors.LazySearch、为KNNClassifier/KNNRegressor引入可选择的最近邻搜索引擎。
5.1neighbors.SWINN:滑动窗口上的近似最近邻图
SWINN(Sliding WIndow-based Nearest Neighbor)扩展了 NNDescent 算法,使其支持在FIFO 滑动窗口中动态增删顶点,通过维护一张有向最近邻图来加速搜索。其核心假设是"我邻居的邻居可能也是我的邻居",因此边会不断被重新审视以改善图结构。算法大致分三步(见 river/neighbors/ann/swinn.py):
- 从随机近邻图开始;
- 对每个节点,在其邻居的邻居中寻找更好的邻居选项来细化邻域;
- 若某轮总边变更数小于停止判据(
delta * graph_k * n_nodes),则停止。
关键参数及默认值如下(均可通过构造器传入):
| 参数 | 默认值 | 含义 |
|---|---|---|
graph_k | 20 | 每个节点最多拥有的直接近邻边数,越大越准但越慢 |
maxlen | 1000 | 数据缓冲(滑动窗口)最大容量 |
warm_up | 500 | 建立搜索图前需观测的样本数(此阶段退化为线性扫描) |
max_candidates | min(50, max(50, graph_k)) | 局部邻域连接时最多考察的候选顶点数 |
delta | 0.0001 | 邻域细化提前停止参数 |
prune_prob | 0.0 | 冗余边剪枝概率(0~1,0 表示不剪枝) |
n_iters | 10 | NNDescent 最大迭代次数 |
seed | None | 随机种子,保证可复现 |
实现层面,SWINN 用collections.deque(maxlen=self.maxlen)保存数据顶点(river/neighbors/ann/nn_vertex.py 中的Vertex类维护edges(出边)与r_edges(反边)两张边表);窗口满时通过_safe_node_removal安全移除最旧节点,并自动为受影响节点重建连接路径,避免产生孤立节点;connectivity()方法(基于 Kruskal 算法)可返回各连通分量大小,用于诊断图是否连成单块。文档特别提示:SWINN 在maxlen > 500时收益明显,小窗口下使用穷举式LazySearch更合适。
5.2neighbors.LazySearch:精确搜索的惰性实现
neighbors.NearestNeighbors在 0.18.0 被重命名为neighbors.LazySearch(见 river/neighbors/lazy.py)。它按 FIFO 策略维护一个deque(maxlen=window_size)的滑动窗口(默认window_size=50),并支持两个实用参数:
min_distance_keep(默认 0.0):控制新点是否被加入窗口的最小距离阈值——值为 0 时全量加入,增大后与已有点过于相似的新点会被拒绝,从而提升窗口多样性;dist_func:自定义距离函数,默认为p=2的 Minkowski(欧氏)距离。
搜索时若使用默认欧氏距离,会走 Cython 加速的快速路径(lazy_search_euclidean),否则用堆(heapq.nsmallest)在 O(n log k) 内取前 k 个最近点。
5.3 可插拔引擎:KNNClassifier / KNNRegressor
两个 KNN 估计器新增engine参数(见 river/neighbors/knn_classifier.py),默认使用SWINN(近似搜索),也可显式传入LazySearch(精确搜索)或其他实现BaseNN接口的引擎——接口要求实现append、search、refresh_targets三个方法(river/neighbors/base.py)。构造时KNNClassifier会克隆引擎,因此可安全复用于多个模型。
KNNClassifier其余参数包括:n_neighbors=5、weighted=True(按距离倒数加权投票)、softmax=False(是否用 softmax 归一化投票)、cleanup_every=0(多久清理一次窗口外的旧类别,0 表示不清理)。与 SWINN 配合自定义 L1 距离的完整示例:
import functools from river import datasets, evaluate, metrics, neighbors, preprocessing, utils dataset = datasets.Phishing() l1_dist = functools.partial(utils.math.minkowski_distance, p=1) model = ( preprocessing.StandardScaler() | neighbors.KNNClassifier( engine=neighbors.SWINN(dist_func=l1_dist, seed=42) ) ) print(evaluate.progressive_val_score(dataset, model, metrics.Accuracy())) # Accuracy: 89.51%6. preprocessing:OneHotEncoder 稀疏化改造
0.18.0 对preprocessing.OneHotEncoder做了两处改动:
- 参数重命名:
sparse→drop_zeros。新语义更直白——drop_zeros=True时输出只保留"命中"的类别列,不输出历史上见过的零值列(见 river/preprocessing/one_hot.py); transform_many返回稀疏 DataFrame:处理 pandas DataFrame 时(get_dummies(..., sparse=True, dtype='uint8')),输出列的类型为Sparse[uint8, 0],相比稠密矩阵大幅降低内存占用。从源码看,pandas 路径返回稀疏列,而 polars/pyarrow 等 narwhals 后端因无稀疏数组等价物则返回稠密整数列(to_dummies),文档与注释中对此差异有明确说明。
import pandas as pd from river import preprocessing X = pd.DataFrame({'c1': ['u', 'a', 'i'], 'c2': ['d', 'x', 'h']}) oh = preprocessing.OneHotEncoder(drop_zeros=True) df = oh.transform_many(X) print(df.dtypes.sort_index()) # c1_a Sparse[uint8, 0] # c1_i Sparse[uint8, 0] # ...OneHotEncoder还支持categories(显式指定每个特征的候选类别集合,固定编码空间)、drop_first(输出 k-1 列哑变量,避免完美共线性)等参数,并可用compose.Select组合实现只编码部分特征。
7. proba:Beta 分布新增 CDF 方法
proba.Beta在 0.18.0 新增了cdf(x)方法(river/proba/beta.py),通过scipy.special.betainc(self.alpha, self.beta, x)计算累积分布函数。Beta 分布本质上是"关于概率的概率分布",非常适合在线学习中对成功率这类伯努利参数做贝叶斯建模——这正是BayesUCB用它维护后验的原因。除 CDF 外,该分布还提供__call__(p)(PDF)、sample()、mode、update/revert(观测成功/失败)与n_samples(真实观测数,不含先验伪计数)等接口:
from river import proba beta = proba.Beta(81, 219) # 先验 alpha=81, beta=219 for _ in range(100): beta.update(True) for _ in range(200): beta.update(False) print(beta.cdf(.35)) # 0.994168... print(beta.n_samples) # 3008. tree:Hoeffding 树防"一边倒"分裂的两个新参数
0.18.0 为分类树(Hoeffding 树族)暴露了两个控制分裂质量的新参数,均用于避免数据过于集中导致的无意义分裂:
min_branch_fraction(默认 0.01):一个分裂候选要生效,其产生的分支中至少要有两个分支的样本占比大于该阈值。从源码(river/tree/hoeffding_tree_classifier.py)可见,该参数被直接传入GiniSplitCriterion、InfoGainSplitCriterion、HellingerDistanceCriterion等分裂准则对象,用于在候选评估阶段就过滤掉"绝大多数样本涌入单一分支"的分裂;max_share_to_split(默认 0.99):仅当叶节点中多数类占比小于该值时,才允许执行分裂。也就是多数类占比达到 99% 时不再继续分裂,避免在近乎纯净的节点上做无谓的切分。
这两个参数已同步暴露到HoeffdingTreeClassifier、HoeffdingAdaptiveTreeClassifier、ExtremelyFastDecisionTreeClassifier与LastClassifier(见 river/tree/last_classifier.py)等分类树中,默认值在四者的构造器签名中保持一致(0.01/0.99)。使用示例:
from river import tree model = tree.HoeffdingTreeClassifier( grace_period=100, delta=1e-5, min_branch_fraction=0.02, # 分支样本占比下限 max_share_to_split=0.95, # 多数类占比上限 )9. utils:修复 Minkowski 距离
utils.math.minkowski_distance在本版本被修复。该函数是邻居搜索(如functools.partial(utils.math.minkowski_distance, p=1)构造 L1 距离)与各类流式算法的公共距离原语,修复保证了自定义距离在 KNN 场景中的正确性。River 的距离计算路径中,river/utils/vectordict.py 同时提供基于字典(dict)与元组(tuple)两种数据形态的欧氏距离实现,供不同搜索引擎按存储结构选用。
10. 升级与兼容性速览
对正在使用 River 0.18.0 之前版本的用户,升级时需注意以下破坏性变更:
neighbors.NearestNeighbors已更名为neighbors.LazySearch,需同步更新导入与实例化代码;preprocessing.OneHotEncoder的sparse参数更名为drop_zeros,语义从"是否显式输出 0"变为"是否丢弃 0 值列",请注意参数取值方向;DBStream的重聚类时机变为惰性——依赖learn_one后立即读取n_clusters/clusters等属性的代码无需改动(属性内部会自动触发重聚类),但如果曾依赖其"每次学习后同步更新"的副作用,行为会有差异;KNNClassifier/KNNRegressor的默认搜索引擎从穷举改为 SWINN,小窗口场景下可通过显式传入LazySearch恢复精确搜索行为。
完整变更清单见 docs/releases/0.18.0.md,各模块的单元测试与回归测试分布在 tests 目录,可进一步查看具体行为边界。
- 人工智能
- 机器学习
- 流处理
- 数据分析
【免费下载链接】river
🌊 Online machine learning in Python
相关推荐
MKS TinyBee:为3D打印带来无线智能控制的新一代主板
MKS TinyBee:为3D打印带来无线智能控制的新一代主板 传统3D打印控制板面临着固件更新复杂、网络连接不便、扩展性有限等痛点。MKS TinyBee基于
固件嵌入式硬件开发智能硬件misakaX:解锁iOS系统深度定制的技术实践
misakaX:解锁iOS系统深度定制的技术实践 当iOS用户渴望超越系统限制,想要在官方框架内实现更多个性化功能时,往往会面临一个困境:如何在保持系统稳定性的
Helm Charts完全指南:Kubernetes"官方应用商店"如何一键部署50+热门应用
Helm Charts完全指南:Kubernetes"官方应用商店"如何一键部署50+热门应用 Helm Charts 是 Kubernetes 生态中最知名的
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考