许多机器学习模型,特别是处理序列或时间序列数据的模型,从特定时间间隔内聚合的特征中获得显著的预测能力。例如,欺诈检测(过去一小时内用户交易频率)、推荐系统(上一会话中查看的商品)或物联网分析(过去5分钟内的平均传感器读数)等应用。高效、准确地实现这些时间窗口聚合,尤其是在处理大量数据和低延迟需求时,会带来巨大的工程挑战。本节主要讨论在特征存储背景下计算这些特征的可扩展技术。理解时间窗口时间窗口聚合根据时间对数据进行分组,并对每个窗口内的元素应用聚合函数(例如,计数、求和、平均值、最大值、最小值)。主要的窗口类型包括:滚动窗口: 这些是固定大小、不重叠、连续的窗口。例如,一个1小时的滚动窗口将时间线划分为 [0:00-1:00)、[1:00-2:00)、[2:00-3:00) 等。每个事件都恰好属于一个窗口。滑动窗口: 这些窗口大小固定,但以指定的间隔(跳跃大小)在时间线上滑动,允许窗口重叠。例如,一个每15分钟跳跃一次的1小时窗口会生成 [0:00-1:00)、[0:15-1:15)、[0:30-1:30) 等窗口。事件可以属于多个窗口。会话窗口: 这些窗口根据活动将事件分组,由不活动间隔分隔。窗口持续时间不固定,而是由数据本身决定。例如,一个会话窗口可能会捕获所有用户点击,直到出现30分钟的暂停。digraph TimeWindows { rankdir=LR; node [shape=plaintext, fontsize=10]; edge [arrowhead=none, color="#495057"]; subgraph cluster_tumbling { label = "滚动窗口 (大小=1小时)"; style=dashed; color="#adb5bd"; T0 [label="0:00"]; T1 [label="1:00"]; T2 [label="2:00"]; T3 [label="3:00"]; T0 -> T1 [label="窗口 1", headlabel="]", taillabel="[", color="#1c7ed6", fontcolor="#1c7ed6"]; T1 -> T2 [label="窗口 2", headlabel="]", taillabel="[", color="#1c7ed6", fontcolor="#1c7ed6"]; T2 -> T3 [label="窗口 3", headlabel="]", taillabel="[", color="#1c7ed6", fontcolor="#1c7ed6"]; } subgraph cluster_hopping { label = "滑动窗口 (大小=1小时, 跳跃=30分钟)"; style=dashed; color="#adb5bd"; H0 [label="0:00"]; H05 [label="0:30"]; H1 [label="1:00"]; H15 [label="1:30"]; H2 [label="2:00"]; H25 [label="2:30"]; H3 [label="3:00"]; H0 -> H1 [label="窗口 A", headlabel="]", taillabel="[", color="#7048e8", fontcolor="#7048e8"]; H05 -> H15 [label="窗口 B", headlabel="]", taillabel="[", color="#7048e8", fontcolor="#7048e8", style=dashed]; H1 -> H2 [label="窗口 C", headlabel="]", taillabel="[", color="#7048e8", fontcolor="#7048e8"]; H15 -> H25 [label="窗口 D", headlabel="]", taillabel="[", color="#7048e8", fontcolor="#7048e8", style=dashed]; H2 -> H3 [label="窗口 E", headlabel="]", taillabel="[", color="#7048e8", fontcolor="#7048e8"]; } // Invisible edges for spacing edge [style=invis]; T3 -> H0; }用于聚合的不同类型时间窗口。滚动窗口是离散的,而滑动窗口则重叠。事件时间与处理时间时间窗口聚合中一个基本要点,特别是在处理流数据时,是区分:事件时间: 事件在源头实际发生的时间。处理时间: 事件被计算系统处理的时间。"依赖处理时间更简单,但如果数据到达或处理存在延迟,可能导致结果不准确。对于大多数旨在捕获行为的机器学习特征,事件时间处理是必要的。然而,这带来了处理迟到数据的复杂性:即在相应时间窗口理论上关闭后才到达的事件。"水印是流处理中管理迟到数据的一种常用技术。水印是一种启发式阈值,表示系统预期不再有早于特定时间 $T$ 的事件。在 $T$ 之前结束的窗口可以被认为是完整的,并最终用于聚合。水印通常根据观察到的事件时间推进,允许对迟到数据有一定的容忍度。可扩展的实现策略大规模计算时间窗口聚合通常涉及对历史数据进行批处理或进行实时流处理。离线特征的批处理计算模型训练或周期性批处理预测所需的聚合通常可以使用Apache Spark或Apache Flink等批处理框架在批处理模式下计算。这通常涉及从离线特征存储(例如,S3、GCS、ADLS等数据湖,或BigQuery、Snowflake、Redshift等数据仓库)读取大量历史数据。处理流程:从离线存储加载相关的历史事件数据。对数据进行分区,通常按实体ID(例如,user_id、device_id)进行。按实体ID和相应的事件时间戳对数据进行分组。应用窗口函数和聚合。将计算出的特征写回离线存储,通常与标记窗口结束的时间戳关联。示例 (Spark SQL):SELECT user_id, window.end AS feature_timestamp, AVG(transaction_amount) AS avg_txn_amount_7d FROM transactions GROUP BY user_id, -- 根据事件时间定义一个7天滚动窗口 window(event_timestamp, "7天")优点:处理复杂的历史查询(时间点正确性)更简单。使用成熟、可扩展的批处理引擎。适用于特征回填。缺点:高延迟:特征仅在批处理作业运行时更新。对于大型数据集和复杂窗口,计算成本可能较高。在线特征的流处理计算对于需要低延迟(例如,用于实时预测)的特征,流处理不可或缺。Apache Flink、Spark Streaming或Kafka Streams等引擎可以在事件到达时计算聚合。处理流程:摄取事件流(例如,来自Kafka、Kinesis、Pub/Sub)。按实体ID对流进行键控。根据事件时间定义窗口,并加入水印以处理迟到数据。为每个活动窗口和实体维护聚合的状态。在窗口关闭时(或对于滑动窗口定期)发出聚合结果。将结果写入在线存储以便快速检索,并可能写入离线存储以保持一致性和用于训练。示例 (Flink DataStream API):// 简化的Flink示例 DataStream<Transaction> transactions = env.addSource(...); DataStream<AggregatedFeature> aggregatedFeatures = transactions .keyBy(transaction -> transaction.getUserId()) .window(TumblingEventTimeWindows.of(Time.days(7))) .aggregate(new AverageTransactionAmount()); // 自定义聚合函数 // 将特征写入在线/离线存储 aggregatedFeatures.addSink(...);状态管理: 聚合的流处理本质上是有状态的。系统必须为每个键和活动窗口维护中间结果(例如,用于计算平均值的当前总和和计数)。大规模可靠、高效地管理此状态非常重要。状态后端: 流处理器提供不同的状态后端(例如,内存、文件系统、RocksDB)。选择合适的后端需要在性能、可扩展性和容错性之间进行权衡。RocksDB常用于大型状态,因为它能够将数据溢写到磁盘。容错性: 检查点等机制定期保存状态,允许在故障后恢复而不会丢失聚合结果。优点:低延迟特征,适用于实时应用。与重复批处理作业相比,连续计算的资源使用效率更高。缺点:操作复杂性更高(管理流、状态、水印)。准确处理迟到数据可能具有挑战性。如果逻辑存在细微差异,批处理计算的特征与流计算的特征之间可能存在偏差。混合方法Lambda或Kappa等架构有时会被采用。一种常见模式是使用流处理为在线存储提供低延迟特征,并使用单独的、可能更复杂或经过修正的批处理流程为离线存储和模型训练生成特征。确保这些路径之间的一致性需要仔细设计和验证(将在第3章进一步讨论)。时间窗口计算的扩展无论采用何种方法(批处理或流处理),有几个因素会影响可扩展性:数据分区: 根据实体ID有效分区输入数据和聚合状态是基础。这允许多个工作节点/节点之间的分布式处理。倾斜分区(某些键的数据量远多于其他键)可能造成瓶颈。资源分配: 批处理和流处理作业都需要仔细调整资源(CPU、内存、网络带宽)。内存不足可能导致过多的磁盘溢写,降低性能,特别是对于有状态的流处理。增量聚合: 在可能的情况下,设计聚合逻辑为增量式的。而不是重新计算整个窗口内容,而是根据新事件进入或旧事件离开窗口来更新聚合(这对于滑动窗口尤其重要)。近似聚合: 对于某些不需要极高准确度的用例,概率数据结构(如用于去重计数的HyperLogLog或用于频率的Count-Min Sketch)可以显著减少状态大小和计算成本。窗口策略: 窗口的类型和大小对性能有显著影响。非常长的窗口或非常小的跳跃大小会增加计算负载和状态大小。准确性、一致性与特征存储集成大规模实现准确的时间窗口聚合需要细致的工作:事件时间处理: 优先采用事件时间与水印策略。根据数据源特性和业务需求配置可接受的迟到阈值。考虑为迟到数据提供侧输出,使其不影响主要计算。一致性: 如果采用混合方法,在批处理和流处理路径中实现相同的聚合逻辑。定期验证两个系统生成的特征是否一致,以防止训练-服务偏差。特征存储更新: 计算出的聚合应摄取到特征存储中。在线存储: 频繁(流式)或定期(批处理)更新,以实现低延迟服务。键通常是entity_id。离线存储: 附加历史聚合,以entity_id和feature_timestamp(通常是窗口结束时间)作为键,确保训练集生成的时间点正确性。元数据: 特征存储注册表应捕获有关聚合的元数据:源事件流、窗口类型(滚动、滑动、会话)、窗口持续时间、跳跃大小(如果适用)、聚合函数以及所使用的事件时间列。正确且高效地实现时间窗口聚合是成熟特征工程系统的特点。通过仔细选择计算策略(批处理、流处理或混合)、有效管理状态以及侧重于事件时间准确性,您可以直接从特征存储基础设施为您的机器学习模型提供功能强大且及时的特征。