从 “被动救火” 到 “主动预判”:用 NeuralProphet 搭建运维数据 AI 预测体系

发布于 2025-10-10 09:29
浏览
0收藏

从 “被动救火” 到 “主动预判”:用 NeuralProphet 搭建运维数据 AI 预测体系-AI.x社区

作者 | 崔皓

审校 | 重楼

开篇

对多数运维工程师而言,日常工作更像一场 “被动的消防演练”—— 紧盯着监控面板上跳动的 CPU 使用率、内存占用、磁盘容量与网络流量,等数值触达告警阈值时,再匆忙排查问题、扩容资源、处理故障。这种 “盯数据 - 等告警 - 忙救火” 的模式,看似能保障系统稳定,却藏着难以规避的痛点:当业务高峰期突然来临,CPU 使用率骤升导致服务卡顿;当磁盘空间在深夜悄然耗尽,核心业务中断才触发告警;当网络流量突发峰值冲垮带宽,用户投诉已堆积成山……

我们总在事后补救,却很少能提前回答:“1 小时后 CPU 会不会过载?”“3 天后磁盘空间是否够用?”“下周这个时段网络流量会不会突破阈值?” 并非运维工程师不愿主动预判,而是传统运维中,既缺乏能捕捉系统指标时间规律的工具,也没有低成本落地的预测方法 —— 直到 NeuralProphet 的出现,让 “提前预测运维数据” 从复杂的 AI 课题,变成了像搭乐高积木一样可落地的实践。

想象一下用乐高积木搭建一个模型:每一块积木都有特定的形状和功能,有的负责搭建底座(对应数据的基础趋势),有的负责拼接循环结构(对应指标的周期性波动),有的负责填补细节(对应突发异常的修正),将这些积木按逻辑组合,就能从零散部件变成完整的、可复用的模型。NeuralProphet 处理运维时间序列数据(如 CPU 使用率、内存波动)的方式,与此几乎完全一致。

它本质上是 Facebook 经典预测模型 Prophet 的 “升级版”——Prophet 曾因将复杂时间序列拆解为 “趋势、周期、节假日” 等可解释模块而风靡运维圈,但在面对运维数据的 “短期高频波动”(如每 10 分钟一次的 CPU 骤升)时,常因缺乏 “局部上下文” 建模能力导致预测偏差;同时,其基于 Stan 的后端架构,也让普通运维工程师难以根据实际场景调整参数。

而 NeuralProphet 的诞生,正是为了解决这些痛点。它完整保留了 Prophet “模块化拆解数据” 的核心优势 —— 比如将 CPU 使用率数据拆成 “长期增长趋势(业务扩容导致的使用率缓步上升)”“日周期波动(早 9 晚 6 的办公高峰)”“周周期波动(工作日与周末的负载差异)” 等独立模块,让非 AI 背景的运维人员也能看懂预测逻辑;同时,它通过引入 “自回归组件”(能捕捉近 1 小时内的短期波动)和 “PyTorch 后端”(支持灵活调参与轻量化部署),完美弥补了 Prophet 的短板,既能精准预测下 10 分钟的 CPU 峰值,也能适配从边缘服务器到云集群的不同运维场景。

上面说了这么多,其实就是一句话:NeuralProphet 非常好用,我们可以用NeuralProphet 模型来预测系统指标(CPU、内存、磁盘、网络)。

那么如何用 NeuralProphet 这个模型呢, 我的思路也比较简单粗暴, 如下图所示。

为了大家理解方便,我们举个简单的例子,我们使用“ 8 月份的数据”(CPU 等)来预测“下一个小时的数据”,然后把“8 月份的数据”+“下一个小时的数据”预测“再下一个小时的数据”。依次类推,有点俄罗斯套娃的感觉,实际情况也是如此。CPU 的使用率数据会在系统中不断产生,有了历史数据可以帮助我们预测下个时间段(一小时)的数据,同时下个时间段的数据也会成为历史数据,为后面数据的预测发光发热。

从 “被动救火” 到 “主动预判”:用 NeuralProphet 搭建运维数据 AI 预测体系-AI.x社区

好了,有了目标接下来就好办了,整个实战案例的思路如下图所示。

首先,生成历史数据,也就是真实的系统指标数据,模拟真实数据分为 CPU、内存、磁盘、网络,每 10 分钟生成一条数据。

接着,利用已经生成的历史数据训练模型,模型就用NeuralProphet,生成的模型保存备用。

然后,利用训练好的模型预测下一个小时的系统指标数据,例如:现在时间是 9 月 1 日的 00:00:00, 我们要预测 从 00:00:00 到 01:00:00 的 CPU 使用率,由于预测数据也是 10 分钟一条,所以会生成 6 条 CPU 使用率的数据。

从 “被动救火” 到 “主动预判”:用 NeuralProphet 搭建运维数据 AI 预测体系-AI.x社区

最终的效果如下,蓝色的线条为实际数据,橙色线条为预测值,红色的文字为误差比,也就是实际值与预测值之间存在的差距。

从 “被动救火” 到 “主动预判”:用 NeuralProphet 搭建运维数据 AI 预测体系-AI.x社区

NeuralProphet 的核心理念

好了,说明了目的(系统数据预测)以及方法(利用 NeuralProphet 预测)之后,需要介绍主角NeuralProphet。

 NeuralProphet 的核心理念非常直观:最终的预测值为“独立组件”预测值的总和。简单来说,多个“独立组件”等同于从多维度思考。把不同维度预测数据的模块得到的结果加起来就是全面的预测。你可以想象有一个复杂问题,让多个不同的专家一起思考提出方案,然后整合他们的方案得到最终方案。

但是这些预测模型或者说思考方案总得通过一个公式表示一下, 要不我们也不好描述,于是就有了下面的公式。:

ŷt = T(t) + S(t) + E(t) + F(t) + A(t) + L(t)

看到公式这么复杂,我是有点懵的,用一个列表表示,通过“说人话”对其进行解释。

组件符号

简单描述

T(t)

趋势 (Trend):数据随时间变化的长期基础走向(例如,增长或下降)。

S(t)

季节性 (Seasonality):在固定周期内重复出现的模式(例如,每周、每年的周期)。

E(t)

事件 (Events):特殊日期(如节假日)对数据产生的短期影响。

F(t)

未来回归量 (Future Regressors):其未来值已知的外部变量(例如,已计划的营销活动)。

A(t)

自回归 (Auto-Regression):近期历史观测值对未来值的直接影响。

L(t)

滞后回归量 (Lagged Regressors):其未来值未知的外部变量(例如,昨天的天气)。

下面再花一点点篇幅对各个组件进行介绍,特别是自回归。如果对这部分不感兴趣或者已经有所了解的同学,可以自行跳到实战环节,从“安装依赖”开始看。

趋势 (T(t)):整体走向

趋势组件捕捉的是时间序列总体、长期的发展方向。为了使趋势线能够适应现实世界中的变化,NeuralProphet 引入了变化点 (changepoints) 的概念。假设你在开车,正在直线行驶,路上的一个转弯,你的方向(或速度)就要根据这个转弯发生变化,这个转弯就是变化点 (changepoints) 。

NeuralProphet 将趋势建模为一个“分段线性”序列。数据变化的趋势基本就是一条直线,直线可以在变化点 (changepoints)改变方向。让模型识别在数据中反复出现的变化模式,从而知道在变化点转弯--改变方向。

从 “被动救火” 到 “主动预判”:用 NeuralProphet 搭建运维数据 AI 预测体系-AI.x社区

线性

从 “被动救火” 到 “主动预判”:用 NeuralProphet 搭建运维数据 AI 预测体系-AI.x社区

分段线性

季节性 (S(t)):周期性节律

季节性是指在固定时期内发生的可预测、重复的模式,重点是可预测和重复。比如:劳动节、儿童节、春节人们会更多出游购物,零售店的销售额通常在周末达到高峰。冰淇淋的销量在夏季会显著增加,等等。

NeuralProphet 利用傅里叶项(本质上是正弦和余弦函数的组合)使模型能够同时捕捉多种季节性,例如,一个模型可以同时识别出数据中的每日、每周和年度模式。

此外,季节性还具备如下特性:

加法性 (Additive) 季节性:一家冰淇淋店每到夏天,销量总是在平日基础上固定增加50份,这个增量不受公司规模变化的影响。

乘法性 (Multiplicative) 季节性:一家电商公司每逢节假日的销售额,总是能达到当年平均水平的两倍,因此公司规模越大,节假日带来的销量增长就越多。

自回归 (A(t)):近期历史的影响

自回归 (Auto-Regression, AR) 在短期预测方面表现突出。在介绍自回归之前先说说什么是回归,回归是通过外部变量(如促销活动)与目标指标(CPU 使用率)的关系,量化外部因素对目标的影响,比如 “电商平台的促销能让服务器的 CPU 负载提升 15%”;而自回归是回归的特殊形式,不依赖外部变量,仅通过目标指标自身的历史数据(如过去 1 小时的 CPU 使用率)预测未来值,核心是捕捉 “历史惯性”—— 比如 “上 10 分钟 CPU 使用率超 80%,下 10 分钟大概率维持高负载”,也就是 “同一变量过去影响未来” 。

所以,自回归的核心思想:“最近发生的事情是对接下来会发生的事情产生影响。” 模型会回顾过去特定数量的数据点,这些数据点被称为“滞后项 (lags)”。例如,如果我们使用 5 个滞后项,模型就会查看最近的 5 个观测值来帮助预测下一个值。

好了到这里,可能有人感觉趋势和自回归有点像,都是通过历史预测未来,所以我们停下来,给他们做一个小区分。趋势是运维指标长期的宏观走向(比如业务增长导致 CPU 使用率每月稳步上升 2%),自回归则是指标短期的实时动态(比如上 10 分钟 CPU 突升 15%,下 10 分钟大概率维持高负载)。

NeuralProphet 的 AR 模块基于一个名为 AR-Net 的架构,它可以配置为两种模式,兼具简单性和强大的功能:

线性 AR:这是一种简单直接的方法。它假设每个过去的值都对预测有一个直接的、加权的线性影响。这种方式非常容易解释,你可以清楚地看到每个滞后项对预测的贡献大小。

深度 AR:这是一种更高级的方法,它使用一个小型神经网络(即 AR-Net)来发现过去值与未来预测之间复杂的、非线性的关系。这通常可以提高预测的准确性,但其内部工作机制不如线性 AR 那样易于直接解读。

回归量与事件:外部影响因素

回归量是帮助预测目标的外部变量,可以理解为“外援”。例如:“要预测冰淇淋销量(目标),了解每日温度(“外援”=回归量)会非常有帮助。”

NeuralProphet 可以处理多种类型的外部影响因素,下表对它们进行了比较:

类型

关键特征

简单示例

滞后回归量

历史值已知,未来值未知。功能上与自回归模块相同,但使用外部变量作为输入。

利用昨天的天气数据来预测今天的能源消耗。

未来回归量

历史和未来值都已知。

在销售预测中包含已提前计划好的市场营销活动的日期。

事件与节假日

特殊的一次性或重复性日期。它们被建模为二元变量,并且可以自动包含特定国家的节假日。

对每年“黑色星期五”购物节期间出现的销售高峰进行建模。

上面已经了解了所有的构建模块,NeuralProphet模型就是将每个独立组件生成的预测值(趋势 + 季节性 + 自回归 + 回归量等)全部加在一起,得到最终的综合预测值,可以理解为“汇总专家意见”。这种方法的最大好处是可解释性 (explainability)。由于每个组件可以独立建模,用户可以单独绘制每个组件的图表,从而确切地了解是哪个因素在影响最终预测结果。

安装依赖

好了理论的部分已经讲完,如果没有听懂也不要紧,直接实操帮助理解,如果还是没有听懂,那就回到开头再看一遍。接下来,我们需要安装必要的环境,先确认已安装conda,conda 是老演员了,安装的链接放到这里,​​https://anaconda.org/anaconda/conda​​。

为了本次实践创建虚拟环境,如下:

conda create -n forecast pythnotallow=3.12

创建完毕之后,启用虚拟环境,如下:

conda activate forecast

然后再虚拟环境中,安装相关的依赖包,如下:

# 基于 PyTorch 的时间序列预测工具,用来训练模型并进行预测
pip install neuralprophet
# 用于处理Excel的库,演示用数据保存在Excel
pip install openpyxl
# 用于构建网页应用界面
pip install streamlit

生成“历史数据”

安装完了环境,我们开始造一些数据,方便后面测试,在造数据之前先回顾一下我们要做的事情,如下图所示。

从 “被动救火” 到 “主动预判”:用 NeuralProphet 搭建运维数据 AI 预测体系-AI.x社区

一般而言,我们在预测未来的系统指标的时候,需要利用历史数据,未来演示的需要我们用代码生成所谓的“历史数据”。在真实的场景中,大家可以按照我后面说的数据格式,将系统指标(CPU、内存、磁盘、网络)的信息填入到 XSL 中,为后面训练预测模型做好准备。在forecast目录中新建python脚本。

cd /forecast
# 直接编辑,在保存时会生成文件
vim history_data.py

import numpy as np
import pandas as pd

# ============================
# 参数配置
# ============================
start_time = pd.Timestamp("2025-08-01 00:00:00")
end_time = pd.Timestamp("2025-08-31 23:59:59")
rng = pd.date_range(start=start_time, end=end_time, freq="10min")

periods = len(rng)
np.random.seed(42)  # 保证可复现

# ============================
# 辅助函数:日内波动模式
# ============================
def daily_pattern(minutes, amplitude=1.0, phase_shift=0):
    radians = (minutes / (24 * 60)) * 2 * np.pi
    return 0.5 + 0.5 * np.sin(radians - 0.3 + phase_shift)

minutes_from_start = (np.arange(periods) * 10) % (24 * 60)
weekday = rng.weekday  # 0=周一
hours = rng.hour

# ============================
# CPU
# ============================
cpu_base = 10 + 5 * (np.random.rand(periods) - 0.5)
cpu_daily = 25 * daily_pattern(minutes_from_start)
cpu = cpu_base + cpu_daily * np.where(weekday < 5, 1.0, 0.7)
spike_mask = (np.random.rand(periods) < 0.005)  # 偶发高峰
cpu[spike_mask] += np.random.uniform(20, 60, size=spike_mask.sum())

# ============================
# 内存
# ============================
mem_base = 50 + 10 * np.sin((np.arange(periods) / periods) * 2 * np.pi)
mem_noise = np.random.normal(0, 2.0, size=periods)
mem_weekday_effect = np.where(weekday < 5, 1.02, 0.99)
memory_used = mem_base * mem_weekday_effect + mem_noise
mem_spike_mask = (np.random.rand(periods) < 0.002)
memory_used[mem_spike_mask] += np.random.uniform(5, 20, size=mem_spike_mask.sum())
memory_used = np.clip(memory_used, 10, 99.5)

# ============================
# 磁盘使用率
# ============================
disk_start, disk_end = 60.0, 62.0
disk_trend = np.linspace(disk_start, disk_end, periods)
disk_noise = np.random.normal(0, 0.05, size=periods)
disk_used = np.clip(disk_trend + disk_noise, 20, 99.9)

# ============================
# 网络流量
# ============================
net_in_base = 5 * daily_pattern(minutes_from_start) + 1.0
net_out_base = 3 * daily_pattern(minutes_from_start) + 0.5
lunch_mask = ((hours >= 11) & (hours <= 13))
net_in = net_in_base * (1.2 * np.where(weekday < 5, 1, 0.9))
net_out = net_out_base * (1.15 * np.where(weekday < 5, 1, 0.9))
net_in[lunch_mask] *= 1.3
net_out[lunch_mask] *= 1.2
burst_mask = (np.random.rand(periods) < 0.003)
net_in[burst_mask] += np.random.uniform(20, 200, size=burst_mask.sum())
net_out[burst_mask] += np.random.uniform(5, 80, size=burst_mask.sum())

# ============================
# 组合 DataFrame
# ============================
df_aug = pd.DataFrame({
    "ds": rng,
    "cpu": np.round(np.clip(cpu, 0, 100), 2),
    "memory": np.round(memory_used, 2),
    "disk": np.round(disk_used, 2),
    "net_in": np.round(net_in, 3),
    "net_out": np.round(net_out, 3)
})

# ============================
# 每小时统计(平均、最大、最小)
# ============================
df_hourly = df_aug.set_index("ds").resample("1h").agg(
    {
        "cpu": ["mean", "max", "min"],
        "memory": ["mean", "max", "min"],
        "disk": ["mean", "max", "min"],
        "net_in": ["mean", "max", "min"],
        "net_out": ["mean", "max", "min"],
    }
)
df_hourly.columns = ["_".join(col).strip() for col in df_hourly.columns.values]
df_hourly.reset_index(inplace=True)


# ============================
# 为每个指标创建单独的sheet(ds:日期,y:值)
# ============================
# CPU sheet
df_cpu = pd.DataFrame({
    "ds": rng,
    "y": cpu
})

# Memory sheet
df_memory = pd.DataFrame({
    "ds": rng,
    "y": memory_used
})

# Disk sheet
df_disk = pd.DataFrame({
    "ds": rng,
    "y": disk_used
})

# Network In sheet
df_net_in = pd.DataFrame({
    "ds": rng,
    "y": net_in
})

# Network Out sheet
df_net_out = pd.DataFrame({
    "ds": rng,
    "y": net_out
})

# ============================
# 导出到 Excel
# ============================
output_file = "server_metrics_2025_08.xlsx"
with pd.ExcelWriter(output_file, engine="openpyxl") as writer:
    df_aug.to_excel(writer, index=False, sheet_name="10min_data")
    df_hourly.to_excel(writer, index=False, sheet_name="hourly_stats")
    # 添加各个指标的单独sheet
    df_cpu.to_excel(writer, index=False, sheet_name="cpu")
    df_memory.to_excel(writer, index=False, sheet_name="memory")
    df_disk.to_excel(writer, index=False, sheet_name="disk")
    df_net_in.to_excel(writer, index=False, sheet_name="net_in")
    df_net_out.to_excel(writer, index=False, sheet_name="net_out")

print(f"✅ 数据已生成并导出到 {output_file}")

代码详细内容如下:

  • 参数配置:设定 2025 年 8 月 1 日至 31 日的时间范围,按 10 分钟间隔生成时间序列,固定随机种子确保数据可复现。
  • 函数定义:编写日内波动模式函数,通过三角函数计算,模拟指标一天内随分钟变化的周期性波动规律。
  • 时间计算:算出每个时间点对应的起始分钟数、星期几和小时数,为后续指标模拟提供时间维度数据。
  • CPU 生成:先确定 CPU 基础值范围,叠加日内波动,按工作日和周末调整幅度,再用随机掩码添加偶发高峰,限制值在 0-100%。
  • 内存生成:以正弦曲线为内存基础趋势,叠加随机噪声,结合工作日差异调整,加偶发峰值后限制在 10%-99.5%。
  • 磁盘生成:设定磁盘使用率从 60.0 到 62.0 的线性增长趋势,叠加小噪声,将值限制在 20%-99.9%。
  • 网络生成:为网络流入、流出设定日内波动基础值,按工作日和午餐时段调整,用随机掩码加突发流量,确保值非负。
  • 组合数据:将所有指标数据与时间序列组合,生成含完整指标的 DataFrame,保留指定小数位数。
  • 小时统计:对原始 10 分钟数据按小时重采样,计算各指标每小时的均值、最大值、最小值,整理成统计 DataFrame。
  • 单表创建:为 CPU、内存等 5 个指标各建单独 DataFrame,仅含 “时间(ds)” 和 “指标值(y)” 两列。
  • 导出 Excel:用 ExcelWriter 将所有数据导出到 “server_metrics_2025_08.xlsx”,包含多类数据表。
  • 完成提示:打印数据已生成并导出到指定文件的提示信息。

执行如下命令,命令完成之后在python脚本所在目录生成历史数据的excel文件。

python history_data.py

如下图所示,会在 history_data.py 相同的目录下生成“server_metrics_2025_08.xlsx”的文件。

从 “被动救火” 到 “主动预判”:用 NeuralProphet 搭建运维数据 AI 预测体系-AI.x社区

如下图所示,在生成的excel中的 “ds”会显示采集时间,我们按照 10 分钟一次采集数据,同时在“y”列显示的是 CPU 的使用率。

从 “被动救火” 到 “主动预判”:用 NeuralProphet 搭建运维数据 AI 预测体系-AI.x社区

在上图的下部的“sheet”中,列出了不同维度的数据,按从左到右的顺序分别是:

  • 10min_data:包含所有指标的数据
  • hourly_stats:所有指标按小时统计数据
  • cpu:cpu使用率
  • memory:内存使用率
  • disk:磁盘使用率
  • net_in:网络入流量
  • net_out:网络出流量

训练模型

我们通过代码的方式生成了系统指标的历史数据,接着会根据这些数据训练模型。

在forecast目录中新建python脚本,如下:

cd /forecast
# 直接编辑,在保存时会生成文件
vim train_model.py

下面代码用来训练历史数据,这里只选择 cpu 进行训练, 其他的 sheet 中存放了内存、磁盘和网络信息,在代码中已经 remark 上了,有兴趣的同学可以自行训练。

from neuralprophet import NeuralProphet
import pandas as pd
import warnings
from neuralprophet import save

# 忽略警告
warnings.filterwarnings('ignore')

# 1. 读取Excel数据
excel_path = 'server_metrics_2025_08.xlsx'
sheet_name = 'cpu'
# sheet_name = 'memory'
# sheet_name = 'disk'
# sheet_name = 'net_in'
# sheet_name = 'net_out'
df = pd.read_excel(excel_path, sheet_name=sheet_name)

# 只保留 ds 和 y 列
df = df[['ds', 'y']]

# 2. 初始化并训练模型
m = NeuralProphet(
    changepoints_range=0.8,         # 只在前80%历史数据中寻找变点,避免对最新数据过拟合
    trend_reg=1,                    # 适度正则化,防止趋势过度拟合 
    seasonality_reg=0.5,            # 适度正则化,防止季节性过拟合
    n_lags=144,                     # n_lags越大,可用训练样本越少
    ar_reg=0.7,                     # 轻微正则化自回归系数,防止过拟合
    n_forecasts=6,                  # 预测的步数,10分数间隔的数据生成6条
    collect_metrics={ "MAE": "MeanAbsoluteError", "MAPE": "MeanAbsolutePercentageError" },
)

# 3. 分割训练集和验证集
df_train, df_val = m.split_df(df, valid_p=0.2)

# 4. 训练模型
m.fit(df_train, validation_df=df_val, freq="10min")

# 5. 保存模型到本地目录
save(m, sheet_name + ".np")

这里稍微对代码部分做解释;

  • 导入工具:引入 NeuralProphet 模型、pandas 数据处理库、警告忽略工具及模型保存函数。
  • 忽略警告:关闭程序运行中的警告提示,使输出更简洁。
  • 读取数据:从 “server_metrics_2025_08.xlsx”Excel 文件中读取指定工作表(默认 cpu,可切换为 memory、disk 等)的数据。
  • 数据筛选:仅保留数据中的 “ds”(时间)和 “y”(指标值)两列,符合 NeuralProphet 模型要求的输入格式。
  • 模型初始化:创建 NeuralProphet 模型实例,配置核心参数 —— 在前 80% 数据中寻找趋势变点,对趋势和季节性特征进行适度正则化,设置 144 个历史滞后项捕捉自回归效应,轻微正则化自回归系数,指定预测 6 个时间步(10 分钟间隔),并收集 MAE 和 MAPE 评估指标。
  • 数据分割:将数据集按 8:2 比例拆分为训练集(df_train)和验证集(df_val),用于模型训练和性能验证。
  • 模型训练:使用训练集训练模型,同时用验证集评估效果,指定数据时间间隔为 10 分钟。
  • 保存模型:将训练好的模型以 “指标名称.np”(如 cpu.np)的格式保存到本地,便于后续加载使用。

预测系统指标

上面的代码会生成一个 np 为后缀的文件,.np 文件是一个模型检查点文件,它是一个包含了模型完整状态的快照,其核心内容是模型在训练后学到的所有参数(权重),以及恢复训练所必需的模型配置和优化器状态。前面通过命令pip install neuralprophet 安装neuralprophet 的基础模型,此时只需要通过历史数据训练成.np 的模型检查点文件,再将这个文件与之前安装的neuralprophet 基础模型进行合并,就是完整的模型了,再用这个完整的模型预测外来数据。只不过,开发者看到的是.np 的文件,而不用关心模型是如何合并以及预测的,这一系列操作都是neuralprophet 框架帮我完成了。接下来,我们来生成一个 UI 界面,对外来数据进行预测,并且展示对应内容。

在forecast目录中新建python 代码,代码中的 UI 界面 streamlit 生成。

cd /forecast
# 直接编辑,在保存时会生成文件
vim app.py

由于篇幅关系,我们只展示部分核心代码,如下:

import streamlit as st
import pandas as pd
import plotly.graph_objects as go
from neuralprophet import load
import warnings
import numpy as np
from datetime import datetime, timedelta
import sys


def generate_forecast(df, sheet_name):
    """
    生成预测的独立函数
    按步数遍历,步数1取yhat1,步数2取yhat2...
    所有预测值都放到yhat1列中
    """
    try:
        # 在预测函数中仅保留'ds'和'y'列用于模型训练
        model_df = df[['ds', 'y']].copy()
        
        # 加载预训练模型
        model_path = f"{sheet_name}.np"
        
        model = load(model_path)
        
        # 生成预测
        future_df = model.make_future_dataframe(model_df, n_historic_predictions=False)
        forecast_result = model.predict(future_df, decompose=False)

        print(forecast_result)

        # 获取最后一个数据点
        last_data_point = model_df['ds'].max()

        # 处理预测结果
        forecast_long = []
        last_actual_idx = forecast_result['y'].last_valid_index() if 'y' in forecast_result.columns else -1
        start_idx = last_actual_idx + 1 if last_actual_idx + 1 < len(forecast_result) else 0

        # 生成预测步骤,按步数取对应yhat,统一放到yhat1列
        for step in range(1, FORECAST_STEPS + 1):
            yhat_col = f'yhat{step}'  # 步数1取yhat1,步数2取yhat2...
            
            current_idx = start_idx + (step - 1)
            if current_idx >= len(forecast_result):
                current_idx = len(forecast_result) - 1
                st.warning(f"预测数据不足,使用最后一条数据预测第{step}步")
            
            if current_idx < len(forecast_result):
                date = forecast_result.loc[current_idx, 'ds']
                
                if pd.notna(date) and yhat_col in forecast_result.columns and pd.notna(forecast_result.loc[current_idx, yhat_col]):
                    forecast_time = last_data_point + step * FORCE_TIME_INTERVAL
                    # 所有预测值都放到yhat1列
                    forecast_entry = {
                        'ds': forecast_time,
                        'yhat1': forecast_result.loc[current_idx, yhat_col],
                        'step': step
                    }
                    forecast_long.append(forecast_entry)
                else:
                    st.warning(f"第{step}步预测值缺失,使用历史平均值")
                    forecast_time = last_data_point + step * FORCE_TIME_INTERVAL
                    avg_value = model_df['y'].mean()
                    forecast_entry = {
                        'ds': forecast_time,
                        'yhat1': avg_value,
                        'step': step
                    }
                    forecast_long.append(forecast_entry)
            else:
                forecast_time = last_data_point + step * FORCE_TIME_INTERVAL
                avg_value = model_df['y'].mean()
                forecast_entry = {
                    'ds': forecast_time,
                    'yhat1': avg_value,
                    'step': step
                }
                forecast_long.append(forecast_entry)

        # 转换为DataFrame
        forecast_long = pd.DataFrame(forecast_long)
        forecast_merged = forecast_long.merge(df[['ds', 'y']], on='ds', how='left')
        
        return forecast_merged
        
    except Exception as e:
        st.error(f"预测出错: {str(e)}")
        st.info("可能的原因:模型文件不存在、数据格式不正确或模型训练不完整")
        return None

代码的主要内容如下:

  • 数据筛选:从输入的历史数据中,仅保留 “ds(时间列)” 和 “y(指标值列)” 并复制,形成模型可识别的输入数据(model_df)。
  • 加载模型:cpu.np,通过 NeuralProphet 的 load 函数加载本地预训练好的模型。
  • 生成预测数据:调用模型的 make_future_dataframe 方法,基于历史数据创建未来预测所需的时间框架(关闭 “包含历史预测” 功能,仅生成未来数据);再用 predict 方法生成预测结果,关闭 “成分分解” 功能(不拆分趋势、季节性等组件),并打印预测结果供调试查看。
  • 确定时间基准:提取历史数据中最新的时间点(last_data_point),作为后续计算 “未来预测时间” 的基准。
  • 初始化存储列表:创建空列表 forecast_long,用于暂存每一步的预测结果;同时找到历史数据在预测结果中的最后有效索引(last_actual_idx),确定未来预测的起始位置(start_idx)。
  • 循环生成预测:按设定的预测步数(FORECAST_STEPS)循环,每一步对应一个预测列(如第 1 步取 yhat1、第 2 步取 yhat2):

     a. 计算当前步在预测结果中的索引(current_idx),若索引超出预测结果范围,自动调整为最后一条数据的索引,并通过 Streamlit 给出 “预测数据不足” 的警告;

     b. 若索引有效,先获取对应时间,若 “时间非空、预测列存在、预测值非空”,则以 “历史最新时间 + 当前步数 × 时间间隔(FORCE_TIME_INTERVAL)” 为预测时间,将 “时间、该步预测值(存入 yhat1)、步数” 组成字典存入列表;

     c. 若预测值缺失或时间无效,通过 Streamlit 给出 “预测值缺失” 的警告,用历史指标的平均值作为预测值,同样按上述格式存入列表;

     d. 若索引无效(极端情况),直接用历史平均值作为预测值,生成对应时间和数据存入列表。

然后,通过如下命令执行应用查看结果。

streamlit run app.py

如下图所示,在弹出的页面中选择“cpu”,以及展示的开始和结束时间,然后点击“生成预测”。在右边的界面中会展示预测的曲线(橙色),这就是我们利用 8 月份 CPU 使用率的模型预测出来,9 月 1 日 00:00 到 02:50 的数据,每 10 分钟产生一个数据点。

从 “被动救火” 到 “主动预判”:用 NeuralProphet 搭建运维数据 AI 预测体系-AI.x社区

如果把鼠标放到任何一个数据点上,可以看到如下图所示的红色字体“误差比”,描述了预测值与实际值之间的误差情况。

从 “被动救火” 到 “主动预判”:用 NeuralProphet 搭建运维数据 AI 预测体系-AI.x社区

接入LLM进行对话

好,到这里,数据预测的功能完成了,接着来到最后一步,我们可以与历史数据进行“对话”,让大模型基于数据给出运维存在的风险,并提出建议。

接下来,还是开始实战操作,先安装一些依赖。

# python-dotenv:可以使用load_dotenv从同目录的.env文件中读取配置
pip install python-dotenv openai

由于要使用 DeepSeek 模型,所以要创建.env文件,用来保存 DeepSeek API Key。

DEEPSEEK_API_KEY=sk-*********

新建config.py文件,用于管理配置参数,包括:项目根目录(使用 Path 获取当前文件所在目录)、Excel数据文件的路径(指向服务器指标数据文件)、需要监控的服务器资源类型列表(CPU、内存、磁盘、网络入流量和出流量),以及这些资源类型的中文显示映射(便于用户界面展示),同时还定义了日期时间的标准格式化模式。

import os
from pathlib import Path

# 项目根目录
ROOT_DIR = Path(__file__).parent

# Excel文件路径
EXCEL_PATH = os.path.join(ROOT_DIR, 'server_metrics_2025_08.xlsx')

# 资源类型列表
RESOURCE_TYPES = ['cpu', 'memory', 'disk', 'net_in', 'net_out']

# 资源类型中文映射
RESOURCE_TYPE_MAP = {
    'cpu': 'CPU使用率',
    'memory': '内存使用率',
    'disk': '磁盘使用率',
    'net_in': '网络入流量',
    'net_out': '网络出流量'
}

# 时间格式
DATETIME_FORMAT = '%Y-%m-%d %H:%M:%S'
DATE_FORMAT = '%Y-%m-%d'

新建logger.py文件,这是一个日志配置模块,它使用 Python 的 logging 库来设置应用程序的日志记录功能。该模块首先在项目目录下创建 logs 文件夹,然后配置日志记录的格式(包含时间戳、日志级别和消息内容)和日期格式。它会根据当前日期创建日志文件(格式如 chat_app_20250919.log),并设置日志记录同时输出到文件和控制台。日志级别被设置为 INFO,这意味着它会记录信息性消息、警告和错误。最后创建了一个名为 logger 的日志记录器实例,供其他模块使用来记录应用程序的运行状态和调试信息。

import logging
import os
from datetime import datetime
from pathlib import Path

# 创建logs目录
log_dir = Path(__file__).parent / 'logs'
log_dir.mkdir(exist_ok=True)

# 配置日志格式
log_format = '%(asctime)s - %(levelname)s - %(message)s'
date_format = '%Y-%m-%d %H:%M:%S'

# 创建日志文件名(包含日期)
log_file = log_dir / f'chat_app_{datetime.now().strftime("%Y%m%d")}.log'

# 配置日志
logging.basicConfig(
    level=logging.INFO,
    format=log_format,
    datefmt=date_format,
    handlers=[
        logging.FileHandler(log_file, encoding='utf-8'),
        logging.StreamHandler()
    ]
)

# 创建logger实例
logger = logging.getLogger(__name__)

新建chat_app.py文件如下,代码中会引用config.py和logger.py。 由于篇幅原因我们展示核心函数如下:

def initialize_openai_client():
    """初始化OpenAI客户端
    
    功能:
    - 从环境变量获取API密钥
    - 初始化DeepSeek API客户端
    - 配置API基础URL
    
    返回:
    - OpenAI客户端实例 或 None(如果初始化失败)
    """
    # 从环境变量获取API密钥
    api_key = os.getenv('DEEPSEEK_API_KEY')
    if not api_key:
        st.error("未找到DEEPSEEK_API_KEY环境变量")
        return None
    
    # 创建并返回配置好的客户端实例
    return OpenAI(
        api_key=api_key,
        base_url="https://api.deepseek.com/v1"  # DeepSeek API的基础URL
    )

def generate_prompt(start_dt, end_dt, resource_type=None):
    """生成提示词:支持时间点或时间范围。
    - 当 start_dt == end_dt 时,生成“在 X 时 ...”的时间点提示
    - 当不相等时,生成“在 X 至 Y 这段时间 ...”的时间范围提示
    """
    if not start_dt or not end_dt:
        return ""

    start_str = start_dt.strftime("%Y-%m-%d %H:%M:%S")
    end_str = end_dt.strftime("%Y-%m-%d %H:%M:%S")

    resource_name = RESOURCE_TYPE_MAP.get(resource_type, resource_type) if resource_type else None

    if start_dt == end_dt:
        if resource_name:
            return f"在{start_str} 时服务器的{resource_name}情况如何?请进行分析,指出异常与风险。"
        return f"在{start_str} 时服务器的整体运行情况如何?请进行分析,指出异常与风险。"
    else:
        if resource_name:
            return f"在{start_str} 至 {end_str} 这段时间内,服务器的{resource_name}情况如何?请进行分析,指出异常与风险。"
        return f"在{start_str} 至 {end_str} 这段时间内,服务器的整体运行情况如何?请进行分析,指出异常与风险。"

def filter_data(df, date, time=None, resource_type=None, end_date=None, end_time=None):
    """根据选择的日期时间范围和资源类型筛选数据"""
    if df is None:
        logger.warning("输入的DataFrame为空")
        return None
    
    logger.info(f"开始筛选数据,起始日期:{date}, 起始时间:{time or '00:00:00'}")
    logger.info(f"结束日期:{end_date or date}, 结束时间:{end_time or '23:59:59'}")
    logger.info(f"资源类型:{resource_type or '所有资源'}")
    
    # 构建开始日期时间
    start_datetime = pd.Timestamp.combine(date, time) if time else pd.Timestamp.combine(date, pd.Timestamp.min.time())
    
    # 构建结束日期时间
    if end_date and end_time:
        end_datetime = pd.Timestamp.combine(end_date, end_time)
    elif end_date:
        end_datetime = pd.Timestamp.combine(end_date, pd.Timestamp.max.time())
    else:
        end_datetime = pd.Timestamp.combine(date, pd.Timestamp.max.time())
    
    logger.info(f"筛选时间范围:{start_datetime} 至 {end_datetime}")
    
    # 筛选时间范围内的数据
    filtered_df = df[(df['ds'] >= start_datetime) & (df['ds'] <= end_datetime)]
    
    # 记录筛选结果
    logger.info(f"时间范围内的数据条数:{len(filtered_df)}")
    
    # 如果选择了资源类型,只返回相关列
    if resource_type and resource_type in df.columns:
        filtered_df = filtered_df[['ds', resource_type]]
        logger.info(f"已筛选资源类型:{resource_type}")
    
    # 记录最终结果
    logger.info(f"最终筛选结果数据条数:{len(filtered_df)}")
    if not filtered_df.empty:
        logger.info(f"数据时间范围:{filtered_df['ds'].min()} 至 {filtered_df['ds'].max()}")
        if resource_type:
            stats = filtered_df[resource_type].describe()
            logger.info(f"资源统计信息:\n{stats}")
    else:
        logger.warning("筛选结果为空")
    
    return filtered_df

其中initialize_openai_client函数连接系统与 DeepSeek 大模型的 “桥梁”,创建大模型调用客户端。从环境变量中读取DEEPSEEK_API_KEY,配置 DeepSeek API 的 URL(​​https://api.deepseek.com/v1​​),生成并返回可直接用于调用的客户端实例。

执行如下命令,运行对话应用。

streamlit run chat_app.py

如下图所示,可以选择一段时间以及对应的系统指标(CPU 使用率),此时会自动填写提示词,让 DeepSeek 协助分析异常和风险,然后点击“开始分析”按钮。在右侧的对话框中就可以看到“CPU 使用率分析报告”的详细信息了。

从 “被动救火” 到 “主动预判”:用 NeuralProphet 搭建运维数据 AI 预测体系-AI.x社区

作者介绍

崔皓,51CTO社区编辑,资深架构师,拥有18年的软件开发和架构经验,10年分布式架构经验。

参考论文

​https://arxiv.org/abs/2111.15397?fbclid=IwAR2vCkHYiy5yuPPjWXpJgAJs-uD5NkH4liORt1ch4a6X_kmpMqagGtXyez4​

收藏
回复
举报
回复
相关推荐