logo
0
0
WeChat Login

SimpleTSAD_

GitHub TencentCNB

轻量级C++时序数据异常检测工具


项目设计哲学:极简主义与性能优先

设计思路

采用分层解耦设计,体现了三个核心思想:

  • 数据层:只负责存储
class TimeSeriesData { std::vector<double> timestamps, values; // 连续内存,缓存友好 };
  • 算法层:只负责计算
class AnomalyDetector { static std::vector<bool> detect(...); // 无状态,线程安全 };
  • 应用层:负责流程控制
int main(){ /* 读取 -> 检测 -> 输出 */ }

设计优势

  • 职责单一:每个类只做一件事
  • 零依赖:仅使用C++17标准库,可跨平台编译
  • 内存连续:vector 保证数据在内存中连续,CPU缓存命中率高

核心算法:滑动窗口异常检测

算法原理(动态Z-score方法)

  • 核心思想:

    一个数据点如果偏离近期历史数据太远,就是异常

  • 数学公式:

    • 均值:μ=i=1nxin\mu = \frac{\sum_{i=1}^n x_i}{n}

    • 标准差:σ=i=1n(xiμ)2n\sigma = \sqrt{\frac{\sum_{i=1}^n (x_i - \mu)^2}{n}}

    • Z-score:z=xcurrentμσz = \frac{|x_{current} - \mu|}{\sigma}

    • 判定:z>threshold异常z > \text{threshold} \Rightarrow \text{异常}

滑动窗口的实现技巧

关键代码拆解

//(来自 AnomalyDetector.cpp) for (size_t i = window_size; i < n; ++i) { // 窗口范围: [i-window_size, i-1] // 例如 i=50, window_size=20 → 分析第30-49个点,判断第50个点 double sum = 0.0, sum_sq = 0.0; for (size_t j = i - window_size; j < i; ++j) { sum += values[j]; // Σx sum_sq += values[j] * values[j]; // Σx² } double mean = sum / window_size; double variance = (sum_sq - sum * sum / window_size) / window_size; double std_dev = sqrt(variance); // 判断当前点 i 是否为异常 if (abs(values[i] - mean) > threshold * std_dev) { anomalies[i] = true; } }

时间复杂度分析:

  • 朴素实现:O(n × window_size) = 10万 × 50 = 500万次运算

  • 优化潜力:使用增量更新可降至O(n),窗口滑动时只需加减一个值

边界处理策略

前window_size个点:信息不足,默认标记为 false (正常) 标准差为零:所有值相同,无法判定,跳过

性能优化深度分析

内存预分配

// 在 loadCSV 中 std::vector<double> timestamps, values; timestamps.reserve(100000); // 预先分配,避免动态扩容 values.reserve(100000);

性能差异:

  • 不reserve:每次push_back可能触发内存重分配,时间复杂度O(n²)

  • reserve:仅分配一次,push_back均摊O(1),总时间O(n)

缓存友好性

// 优秀:顺序访问 for (auto& v : values) { sum += v; } // 糟糕:随机访问(本项目未使用) for (int idx : random_indices) { sum += values[idx]; }
原理:CPU缓存以64字节为单位加载,顺序访问几乎 **100%** 命中缓存

编译器优化

在CMakeLists.txt中添加:

set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O3 -march=native")

-O3 :最高级别优化 -march=native :针对当前CPU指令集优化(如AVX2)


CSV解析的底层细节

核心代码解析

bool TimeSeriesData::loadCSV(const std::string& filename) { std::ifstream file(filename); std::string line; while (std::getline(file, line)) { // 假设格式: timestamp,value size_t comma_pos = line.find(','); double ts = std::stod(line.substr(0, comma_pos)); double val = std::stod(line.substr(comma_pos + 1)); timestamps.push_back(ts); values.push_back(val); } }

关键点:

  • std::getline() :逐行读取,内存占用稳定

  • std::stod() :字符串转double,比C的 atof() 更安全

  • 假设:数据已清洗,无空行或格式错误

时间复杂度:O(n),n为行数


可扩展算法路径

从滑动窗口到高级算法

基于当前架构,可无缝升级:

① STL分解(Seasonal-Trend Decomposition)

// 伪代码 auto [trend, seasonal, residual] = STLdecompose(values, period); // 检测residual的异常

② 孤立森林(Isolation Forest)

// 伪代码 IsolationForest forest(100); // 100棵树 forest.fit(values); auto scores = forest.anomaly_score(values);

③ LSTM自编码器

// 伪代码 LSTMAutoencoder model(input_size=1, hidden_size=32); model.train(values); auto reconstructions = model.predict(values); auto errors = |values - reconstructions|; // 重构误差

性能提升方向

增量计算(将复杂度降至O(n)):

// 维护窗口统计量,避免重复计算 class IncrementalWindow { double sum = 0, sum_sq = 0; std::deque<double> window; void push(double value) { window.push_back(value); sum += value; sum_sq += value * value; } void pop() { double old = window.front(); window.pop_front(); sum -= old; sum_sq -= old * old; } };

工程化思考

为什么用静态方法?

static std::vector<bool> detect(...);

优势:

  • 无状态:不依赖类成员变量,线程安全
  • 函数式:输入→输出,易于单元测试

性能:

  • 避免 this 指针开销

错误处理策略

if (window_size > n) throw std::invalid_argument(...); // 使用异常而非错误码,符合C++ RAII思想

代码总结

模块算法/技术时间复杂度空间复杂度优化点
CSV读取字符串分割O(n)O(n)reserve() 预分配
异常检测滑动窗口Z-scoreO(n×w)O(1)增量更新可优化至O(n)
结果输出文件I/OO(n)O(1)缓冲区写入

项目完成过程

Day1:CSV I/O与数据结构

Day 2:滑动窗口异常检测

About

读取CSV格式的时序数据(如温度、心率、服务器负载),自动检测并标记异常值,输出结果到新CSV文件。

Language
C++92.3%
CMake7.7%