特征工程(上):流量统计特征与时间窗口特征

各位好,欢迎来到第四章。前面我们聊了数据采集和清洗,那都是准备工作。今天要讲的,才是真正让模型「看懂」网络流量的关键一步——特征工程。

说白了,原始的网络流量就是一堆IP、端口、时间戳和字节数。你把这些扔给模型,它什么都学不到。你得帮它提炼出有意义的指标。我个人习惯把特征工程比作「给数据化妆」——底子再好,也得画对了妆才能上台。

核心观点:特征工程的质量,直接决定了异常检测模型的天花板。数据决定了上限,算法只是逼近这个上限的手段。

4.1 流量统计特征——最基础也最有效

我们先从最直观的统计特征入手。这些特征计算简单,但效果往往出奇的好。我在多个安全项目中验证过,单靠统计特征就能检出80%以上的常见异常。

4.1.1 包长特征

网络数据包的长度,其实藏着很多信息。正常业务流的包长分布通常比较稳定,而攻击流量往往会打破这种稳定。

常用的包长特征包括:

  • 平均包长:流中所有数据包长度的平均值
  • 最大/最小包长:反映极端情况
  • 包长标准差:衡量包长的波动程度
  • 包长分布直方图:将包长分桶,统计每个桶的频次

举个例子,正常的HTTP请求,包长通常在几百字节到几KB之间。但如果是DDoS攻击中的小包攻击,平均包长可能只有几十字节。我遇到过一起案例,某天晚上业务监控显示平均包长从450字节骤降到28字节,一查果然是UDP反射攻击。

# 计算包长特征的示例代码
def extract_packet_length_features(packet_lengths):
    features = {}
    features['mean_len'] = np.mean(packet_lengths)
    features['max_len'] = np.max(packet_lengths)
    features['min_len'] = np.min(packet_lengths)
    features['std_len'] = np.std(packet_lengths)
    # 包长分桶:0-64, 64-128, 128-256, 256-512, 512-1024, 1024+
    bins = [0, 64, 128, 256, 512, 1024, 65535]
    hist, _ = np.histogram(packet_lengths, bins=bins)
    for i, count in enumerate(hist):
        features[f'len_bin_{i}'] = count
    return features

经验之谈:包长分桶的粒度要根据实际场景调整。内网流量包长分布比较集中,可以分细一些;互联网流量包长分布更广,分桶可以粗一些。我一般先用百分位数看一下分布,再决定分桶边界。

4.1.2 速率特征

速率特征反映的是流量在时间维度上的变化节奏。常见的速率特征有:

  • 包速率:每秒传输的数据包数量
  • 字节速率:每秒传输的字节数
  • 流速率:每秒新建的连接数

这三种速率各有侧重。包速率对DDoS小包攻击敏感,字节速率对带宽耗尽型攻击敏感,流速率则对扫描类攻击敏感。我建议三个都算上,别偷懒。

速率类型 计算公式 典型异常场景
包速率 总包数 / 时间窗口 SYN Flood、DNS放大攻击
字节速率 总字节数 / 时间窗口 带宽耗尽攻击、大文件下载
流速率 新建流数 / 时间窗口 端口扫描、暴力破解

4.1.3 协议分布特征

不同协议在正常流量中占比相对稳定。比如一个Web服务器,TCP流量通常占95%以上,UDP占一小部分。如果某天UDP流量突然飙升到30%,那肯定有问题。

协议分布特征通常包括:

  • 各协议流量占比:TCP、UDP、ICMP等协议的包数/字节数占比
  • 协议熵值:衡量协议分布的均匀程度
  • 特定协议统计:如HTTP状态码分布、DNS查询类型分布

嗯,这里要注意一点。协议分布特征在基线建立时,需要排除已知的维护窗口。我曾经踩过这个坑——把每周三凌晨的数据库备份流量也算进基线,结果模型天天报警,搞得运维同事差点要打我。

4.2 时间窗口特征——让模型学会「看时间」

统计特征只能反映一个时间点的状态,但网络行为是有时间规律的。用户早上9点上班开始刷网页,中午休息时流量下降,晚上又有一波高峰。这些规律,得靠时间窗口特征来捕捉。

4.2.1 滑动窗口统计

滑动窗口是最常用的时间特征提取方式。你想想看,我们用一个固定大小的窗口在时间轴上滑动,每个窗口内计算一次统计量。

常用的窗口大小:

  • 短窗口(1-5分钟):捕捉突发变化,适合检测DDoS
  • 中窗口(15-30分钟):反映短期趋势,适合检测扫描行为
  • 长窗口(1-24小时):捕捉周期性规律,适合建立基线

我个人习惯同时使用多个窗口。短窗口负责快速响应,长窗口负责消除噪声。多窗口组合的效果,比单窗口好得多。

# 多窗口滑动统计示例
def sliding_window_stats(series, windows=[5, 15, 60]):
    features = {}
    for w in windows:
        # 计算滑动窗口内的均值、标准差、最大值
        features[f'mean_{w}min'] = series.rolling(w).mean().iloc[-1]
        features[f'std_{w}min'] = series.rolling(w).std().iloc[-1]
        features[f'max_{w}min'] = series.rolling(w).max().iloc[-1]
    return features

4.2.2 周期性特征

网络流量有明显的周期性。日周期、周周期、月周期,各有各的规律。提取周期性特征,可以帮助模型区分「正常波动」和「异常变化」。

常用的周期性特征:

  • 同比特征:当前时刻与昨天同一时刻的差值
  • 环比特征:当前时刻与上一时刻的差值
  • 周同比特征:当前时刻与上周同一时刻的差值

举个例子,如果今天是周二上午10点,那么同比就是和周一上午10点比,环比就是和周二上午9点55分比,周同比就是和上周二上午10点比。三种对比角度,覆盖了不同的异常模式。

避坑指南:我曾经在做一个金融客户的项目时,直接用周同比特征训练模型,结果模型在节假日全部误报。后来才意识到,节假日的工作日特征完全不适用。解决方案是加入节假日标签,或者使用「最近相似日」替代固定周期。

4.2.3 时间衰减特征

这个技巧比较高级,但效果很好。时间衰减的核心思想是:越近的数据越重要,越远的数据权重越低。

实现方式很简单,给历史数据加一个指数衰减权重:

# 指数衰减加权平均
def exponential_decay_weighted_mean(series, decay_factor=0.9):
    weights = [decay_factor ** i for i in range(len(series))]
    weights = weights / sum(weights)  # 归一化
    return np.dot(series, weights)

衰减因子一般取0.8到0.99之间。值越大,历史数据的影响持续越久;值越小,模型越关注近期数据。我一般先用0.95试跑,看效果再调。

4.3 特征工程的整体流程

说了这么多,我们来梳理一下特征工程的完整流程。下面这张图是我自己总结的,每次做项目都按这个步骤来。

特征工程整体流程 原始流量数据 基础统计特征提取 包长特征 速率特征 协议分布特征 时间窗口特征融合 最终特征向量 多窗口组合效果优于单窗口 建议同时使用短/中/长窗口

4.4 特征选择的几个原则

特征不是越多越好。我见过有人提取了200多个特征,结果模型训练时间长了10倍,检测效果反而下降了。这里分享几个我常用的特征选择原则:

  1. 相关性原则:特征之间相关性太高,保留一个就行。比如平均包长和中位数包长,选一个。
  2. 稳定性原则:特征在正常情况下的分布要稳定,波动太大的特征容易导致误报。
  3. 可解释性原则:选那些你能说清楚「为什么这个特征能反映异常」的指标。
  4. 计算成本原则:实时检测场景下,特征计算时间不能超过检测间隔。

实用技巧:我一般先用随机森林的特征重要性排序,选出Top 20的特征。然后结合业务经验,人工筛选出10-15个核心特征。这样既保证了效果,又控制了计算成本。