☰
流感时间序列预测:ARIMA-LSTM-Transformer组合与集成实战
2026/10/11 21:23:40 网站建设 项目流程

简介:基于ARIMA、LSTM与Transformer等模型的流感时间序列预测Python源码,是一份面向计算机相关专业课程设计与期末大作业的高分实操项目,特别适合需要完成多模型对比实验、时序预测实战的学生或学习者。资源包共25个文件,容量约4.93MB,包含7个CSV数据文件(如ILINet及流感历史数据)、7个Python脚本、2个Jupyter Notebook、4个Excel表格及若干辅助压缩包,覆盖数据加载、平稳性检验、ADF检验、ACF/PACF定阶、ARIMA建模与残差分析、LSTM/SARIMA训练以及多模型预测对比和未来走势预测等完整流程,目录划分清晰,便于按步骤复现代码。目前已有387人学习下载。通过该资源可系统掌握流感时序数据的预处理方法、SARIMA与Transformer等模型构建及评估思路,获得从数据清洗到预测出图的整套可运行代码,并能在课程设计、毕业设计或项目实战中直接参考、改造与扩展。

1. 流感时间序列预测:为什么单模型不够,ARIMA-LSTM-Transformer组合才敢拿高分

流感预测最常踩的坑,不是模型不会跑,而是数据一换模型就失效。单独用ARIMA,它能抓住平稳趋势,却抓不住暴发期的突然抬升;单独用LSTM,它能记住序列的周期,却容易在样本量只有几百周的时候过拟合;单独用Transformer,理论上表达能力强,但训练数据太少时反而比LSTM更飘。所谓高分项目,本质上不是把三个模型各跑一遍,而是把同一份流感数据清洗、建模、评估、集成的完整过程跑通,并给出可信的误差对比。这篇文章会从数据准备讲到ARIMA基线、LSTM神经网络、Transformer,最后用加权集成交付一套能直接交差的时间序列预测方案。适合正在做课程设计、毕业设计,以及想认真做时序预测的Python开发者。

2. 数据准备与评估口径:把流感周报变成能喂给ARIMA、LSTM、Transformer的样本

流感监测数据是典型周频序列,一年约52个点,做得好的项目通常会拿三年以上数据,也就是150到200个点。这个体量对ARIMA很友好,但对LSTM和Transformer来说偏小,所以本章所有处理都围绕“小样本下怎么保证稳定”。

2.1 数据源与字段选择:ILI%周序列怎么整理最省事

常见的数据源是各地疾控中心公开的流感监测周报,字段经常包括“监测周、ILI%、流感病毒阳性率、暴发疫情起数”。做时间序列预测,我们只需要日期和ILI%两列,其他字段可以作为附加特征,但第一版不建议一上来就堆特征,因为周报越杂,缺测和口径变更带来的坑越多。ILI%就是流感样病例占门急诊就诊数的百分比,数值相对稳定,适合建模。

我习惯把数据整理成只有两列的CSV:date和ili。下面这段代码演示读取、解析和排序:

import pandas as pd # 列名统一成 date 和 ili,周报通常给的是周起始日期 df = pd.read_csv('flu_data.csv', parse_dates=['date']) df = df.set_index('date').sort_index() print(df.head()) print(df.tail()) print(df.info())

逻辑说明:parse_dates把date列解析成时间类型,set_index把date设成索引,sort_index保证序列按时间正序。流感数据只要时间顺序一乱,后面所有滑窗和train/test切分都会错位,所以这步是底线。

如果暂时没有真实数据,可以先用合成数据跑通流程,但报告里要明确标注是模拟数据。我常用下面的方式生成260周左右的合成序列,方便本地调试:

import numpy as np np.random.seed(42) dates = pd.date_range('2019-01-01', periods=260, freq='W') trend = np.linspace(2.0, 3.0, len(dates)) season = 0.6 * np.sin(2 * np.pi * np.arange(len(dates)) / 52) ili = np.round(trend + season + np.random.normal(0, 0.2, len(dates)), 2) df = pd.DataFrame({'date': dates, 'ili': ili}) df.to_csv('flu_data.csv', index=False)

参数说明:freq='W'按周生成日期,/52表示以年为周期叠加季节性。真实项目里不建议用这种手工合成数据做训练,它只适合验证代码路径能跑通。

数据量方面,想要拿高分,训练样本尽量不要少于150周,否则深度学习基本学不到周期。如果不到100周,我一般只做ARIMA和LSTM,放弃Transformer,因为后者在极小样本上几乎没有优势。

2.2 清洗与归一化:缺失值插补、MinMaxScaler和节假日噪声

流感周报的缺失值很典型:跨年时周次合并、部分地区春节休假不报,导致某几周连续为NaN。处理原则是先看缺失长度,缺1到2周用线性插值,缺太多周要考虑是不是报告口径变了,机械填充等于编数据。有时候原始数据不是标准自然周,可能是“2023-01-02至2023-01-08”,这种先重采样成自然周再插值:

# 先重采样成自然周,缺失的周会自动变成NaN df = df.set_index('date').resample('W').first() # 缺失值用线性插值补上,流感占比通常是渐变 df['ili'] = df['ili'].interpolate(method='linear', limit_direction='both') print(df.isnull().sum())

逻辑说明:resample('W').first()会把同一周的多行合并,取第一条记录,并补出没有数据的周。interpolate(method='linear')对流感这种缓慢变化的占比指标是安全的,因为暴发期也是连续爬坡,很少直接跳变。

节假日噪声需要单独处理。春节前后门急诊量明显下降,ILI%会出现非流感因素的尖刺。常见做法是把春节前后两周生成哑变量特征:

# 假设春节近似在每年第4-6周,这里按周序号标记 week_of_year = df.index.isocalendar().week df['spring_festival'] = week_of_year.isin([4, 5, 6]).astype(float)

参数说明:isocalendar().week返回ISO周序号,春节一般落在第4到6周,用这种简单规则可以避免查每年农历日期。后面LSTM/Transformer把spring_festival列当作额外输入特征,ARIMA则可以直接忽略,因为它本质上是线性模型,学了也白学。

归一化这步只给深度学习模型用,ARIMA继续用原始数值:

from sklearn.preprocessing import MinMaxScaler scaler = MinMaxScaler(feature_range=(0, 1)) df['ili_scaled'] = scaler.fit_transform(df[['ili']]) train_size = int(len(df) * 0.8) train = df.iloc[:train_size] test = df.iloc[train_size:]

参数说明:feature_range=(0, 1)是默认范围,LSTM和Transformer对量纲敏感,把ILI%从3.0这种小数值缩放到0到1,能明显加速收敛。注意fit_transform只需要在训练集上fit,测试集应该用同一个scaler去transform,避免测试集信息泄漏进归一化参数。这里简单写法是整体fit,实际项目中要在切分之后对训练集fit。

2.3 评估指标与验证方式:RMSE、MAPE和walk-forward窗口怎么切

评估流感预测不能只看一个指标。RMSE对大误差敏感,适合衡量谁在暴发期更离谱;MAPE是百分比误差,报告里容易解释,但真实值接近0时会爆炸;R²在时间序列上天然虚高,因为序列自相关太强,不建议单独使用。下面这两个指标足够用了:

from sklearn.metrics import mean_squared_error, mean_absolute_percentage_error def evaluate(y_true, y_pred): rmse = float(np.sqrt(mean_squared_error(y_true, y_pred))) mape = float(mean_absolute_percentage_error(y_true, y_pred)) return {'RMSE': rmse, 'MAPE': mape}

逻辑说明:evaluate返回字典,方便汇总多个模型。这里y_true和y_pred都要用原始尺度,不能用归一化后的值,否则RMSE看起来很小,换数据后完全不可比。

验证方式上,时间序列最忌讳随机划分。train_test_split(random_state=42)会把未来数据塞进训练集,评估结果虚高,评审一质疑就翻车。正确的做法是按时间顺序切分:

from sklearn.model_selection import TimeSeriesSplit tscv = TimeSeriesSplit(n_splits=5) for train_idx, val_idx in tscv.split(df): train_data = df.iloc[train_idx] val_data = df.iloc[val_idx]

参数说明:TimeSeriesSplit(n_splits=5)按时间递增切5折,每一折的训练集都严格在验证集之前。小样本数据我通常取n_splits=3或4,保证每折训练集不少于100周。切完之后先跑ARIMA基线,如果基线RMSE都超过你心中的容忍值,后续深度学习也不用继续了。

最后补一个针对暴发的指标:峰值时刻偏差。流感预测的核心是暴发预警,如果模型把峰值预测偏了两周,RMSE再低也白搭。常见做法是取真实峰值前后4周,看预测峰是否落在窗口内。下面这个函数统计峰值位置的平均偏差:

def peak_time_error(y_true, y_pred, top_k=4): idx_true = np.argsort(np.array(y_true))[-top_k:] idx_pred = np.argsort(np.array(y_pred))[-top_k:] return float(np.mean([abs(float(np.min(abs(p - idx_true))) for p in idx_pred]))

逻辑说明:np.argsort取最大的top_k个位置,然后对每个预测峰值,找它离某个真实峰值最近的距离,再取平均。这个指标不会写进主损失函数,但会写进项目报告的对比表里,专门给评委看“你的模型有没有预警价值”。

到这里,数据已经能被三个模型共用。下一步先把ARIMA基线跑通,用它给LSTM和Transformer立一个必须超过的分数基准。

3. ARIMA基线:用pmdarima跑通统计模型,先给深度学习立一个分数

深度学习模型不是上来就训练。先跑ARIMA有两个好处:一是代码量小,能快速暴露数据预处理的问题;二是得到一个最低线,后面LSTM和Transformer必须在这个RMSE基础上再降,否则没有存在意义。

3.1 为什么ARIMA适合做基线:平稳性、ACF/PACF与差分

ARIMA全称差分自回归移动平均模型,三个参数分别叫p、d、q。p是自回归阶数,表示用最近p个时刻的值来预测当前值;d是差分阶数,用来消除趋势;q是移动平均阶数,用来拟合噪声。

流感ILI%序列通常不平稳,因为每年有季节性抬升,所以差分是必须的。可以用ADF检验看平稳性:

from statsmodels.tsa.stattools import adfuller result = adfuller(df['ili'].dropna()) print('ADF统计量:', result[0]) print('p值:', result[1])

逻辑说明:p值小于0.05说明序列平稳。如果p值很大,就做一次差分再检验。对流感数据,d=1通常就够了,差分一次后季节性会保留但整体趋势被抹平。这里不展开SARIMA是因为项目题目点名了ARIMA、LSTM、Transformer,先用非季节性ARIMA当基线是常规做法,拿了SARIMA反而让对比变量不干净。

ACF和PACF图能帮助定p、q,但实际项目里大部分人不会看图,直接交给auto_arima自动搜索。如果评审老师问起来,能说出“ACF拖尾、PACF截尾,所以p取1到2、q取0到1”这种话就行,不必真的纠结。

3.2 pmdarima自动定阶与滚动预测:代码、参数与输出

pmdarima是Python里最省心的ARIMA工具。我一般在训练集上跑自动定阶,然后对测试集做滚动预测。所谓滚动预测,就是每预测完一周,把真实值补充进历史,再预测下一周。这样不会让误差像雪球一样越滚越大。

from pmdarima import auto_arima y_train = df['ili'].iloc[:train_size] model = auto_arima( y_train, start_p=0, max_p=7, start_q=0, max_q=7, d=None, seasonal=False, stepwise=True, trace=True, error_action='ignore', suppress_warnings=True ) print(model.order)

参数说明:d=None表示让pmdarima自己用KPSS或ADF判断差分阶数;seasonal=False是因为另一路人马做深度学习,ARIMA这里刻意不用季节性差分。stepwise=True会启用启发式搜索,避免遍历所有组合,260周数据上基本几秒钟就出结果。error_action='ignore'的作用是某一组p、q不收敛时不直接报错中断。

定完阶之后,滚动预测的代码是项目里的核心:

history = list(y_train.values) preds = [] for t in range(len(test)): # 用当前模型预测下周 pred = model.predict(n_periods=1)[0] preds.append(pred) # 把测试集真实值喂给模型,而不是喂预测值 history.append(test['ili'].iloc[t]) model.update(history[-1])

逻辑说明:model.update(history[-1])是增量更新,不是重新fit。你拿到新一周的真实ILI%后更新模型状态,预测下个月时模型始终处于“见过最新真实数据”的状态。这个策略叫expanding window,样本量小的时候比纯滚动预测稳定。

model.predict(n_periods=1)返回一个数组,这里[0]取第一个值。如果要做未来4周预测,改成n_periods=4,但流感项目里最常见的是1步预测加滚动更新,因为周报每周都出,没有必要牺牲精度预测整月。

3.3 ARIMA的典型失败样子:它在暴发期为什么容易滞后

ARIMA在流感项目里一定会被问到一个问题:为什么预测曲线比真实数据滞后一两周?原因是ARIMA本质是线性自回归,它对“尖峰”能做的只是用前几周的趋势外推,一旦暴发是突然抬升,自回归项只能把预测值往上周的真实值方向拉,于是看起来像把真实曲线平移了一周。

这种滞后在RMSE里会被重重惩罚,因为暴发期误差很大。我见过不少项目为了压RMSE,给ARIMA堆了不少假特征,比如把时间戳的sin/cos加进去,结果只是让滞后变成抖动,并没有解决根本问题。

所以ARIMA的定位就是基线:它必须能抓住趋势,但允许在暴发期系统性滞后。后面LSTM和Transformer能不能在暴发期做到“拐头快”,才是拿分的关键。

4. LSTM神经网络:把流感序列改造成监督学习再训练

LSTM需要对数据做滑窗,把原始序列改造成“过去8周预测下周”的监督学习格式。这一步不做好,模型结构再复杂也没用。

4.1 滑窗构建数据集:seq_len、stride怎么设

滑窗长度seq_len是三模型共用的超参数。流感周期是年52周,但暴发持续时间通常4到6周,所以窗口太长会引入无关信息,太短又学不到暴发前兆。我一般从8周起步,因为流感暴发前一个月往往有低位徘徊,8周足够看到完整的前兆。

def create_sequences(data, feature_cols, seq_len=8): X, y = [], [] for i in range(len(data) - seq_len): X.append(data.iloc[i:i+seq_len][feature_cols].values) y.append(data.iloc[i+seq_len]['ili']) return np.array(X), np.array(y) feature_cols = ['ili_scaled', 'spring_festival'] X, y = create_sequences(train, feature_cols, seq_len=8) print('X shape:', X.shape) print('y shape:', y.shape)

逻辑说明:create_sequences返回的X形状是(样本数, 8, 特征数)。i从0取到len(data)-seq_len-1,每次取连续8周作为输入,第9周作为标签。spring_festival列在这里当作外部特征拼进每一步,这样LSTM可以“看到”春节前后几周的输入。

注意stride=1,也就是窗口每次滑动一周。流感周报本身样本就少,stride大于1等于人为丢弃数据,除非是数据量过万的长序列,否则不建议。

测试集同样要转成序列,但有个差别:测试集滑窗不能跨训练集。应该在完整训练+测试序列上重建,并在构建时记录原始索引:

full_df = df.iloc[train_size - seq_len:] X_test, y_test = create_sequences(full_df, feature_cols, seq_len=8)

逻辑说明:为了让测试集第一个窗口能看到训练集最后8周的信息,从train_size - seq_len处开始截取。这样既没有数据泄漏,又能让模型基于最新历史做预测。很多踩坑项目直接把测试集单独滑窗,结果第一个窗口全是NaN或随机值,预测惨不忍睹。

4.2 LSTM模型定义与训练:hidden_size、num_layers、dropout怎么选

PyTorch里写一个面向小样本的LSTM模型,核心是别堆太大。流感数据只有一两百个训练样本,hidden_size取32已经足够,取128大概率过拟合。num_layers取1到2层,再多就没有足够数据约束。dropout取0.2到0.3,给序列加一点随机失活。

import torch import torch.nn as nn class LSTMForecast(nn.Module): def __init__(self, input_size, hidden_size=32, num_layers=2, output_size=1, dropout=0.2): super().__init__() self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True, dropout=dropout) self.fc = nn.Linear(hidden_size, output_size) def forward(self, x): out, (h, c) = self.lstm(x) # out形状: (batch, seq_len, hidden_size) return self.fc(out[:, -1, :])

逻辑说明:batch_first=True让输入形状变成(批次, 时间步, 特征数),和create_sequences返回的X直接对齐。取out[:, -1, :]表示只要最后一个时间步的hidden state,再接一个线性层输出下周ILI%。注意输入的input_size是特征数,这里是2(ili_scaled和spring_festival)。

训练循环里,学习率、batch_size、梯度裁剪和早停比模型结构更容易影响结果。下面这段是标准的训练代码:

from torch.utils.data import DataLoader, TensorDataset train_dataset = TensorDataset(torch.FloatTensor(X_train), torch.FloatTensor(y_train)) train_loader = DataLoader(train_dataset, batch_size=16, shuffle=True) model = LSTMForecast(input_size=X_train.shape[2], hidden_size=32) optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) loss_fn = nn.MSELoss() for epoch in range(60): model.train() for batch_X, batch_y in train_loader: optimizer.zero_grad() loss = loss_fn(model(batch_X), batch_y.unsqueeze(1)) loss.backward() # 梯度裁剪,防止小样本下梯度爆炸 torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) optimizer.step()

参数说明:shuffle=True在训练时可以打乱样本顺序,因为滑窗样本本身已经包含时间信息,打乱不影响LSTM的内部时序,反而有助于收敛。clip_grad_norm_(..., 1.0)把梯度范数限制在1.0,Transformer和LSTM都建议加,流感数据一旦出现一两周的异常尖峰,梯度很容易翻车。

关键的是不要对测试集shuffle,测试时用完整序列按时间顺序逐周预测。

4.3 LSTM训练监控:学习率、batch_size和早停

学习率是LSTM训练里最需要手动盯的参数。流感数据量小,lr=1e-3是安全起点,如果损失曲线震荡得厉害,直接降到3e-4。batch_size可以选8或16,26个样本的小序列用32会导致一个batch几乎覆盖全部样本,梯度更新太“猛烈”。

早停要小心:验证集只有三四十个点,不能像CV数据集那样等验证loss连续3轮不降就停。我一般只看训练loss走势,加上固定epoch数双保险:

best_loss = float('inf') best_state = None for epoch in range(60): model.train() for batch_X, batch_y in train_loader: optimizer.zero_grad() loss = loss_fn(model(batch_X), batch_y.unsqueeze(1)) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) optimizer.step() # 每个epoch结束在验证集上测一次,保存最佳权重 model.eval() with torch.no_grad(): val_pred = model(torch.FloatTensor(X_val)) val_loss = loss_fn(val_pred, torch.FloatTensor(y_val).unsqueeze(1)) if val_loss < best_loss: best_loss = val_loss best_state = {k: v.clone() for k, v in model.state_dict().items()} # 恢复最佳权重 model.load_state_dict(best_state)

逻辑说明:验证集也要从训练集末尾往前回退8周来构建,不能跳过。保存最佳权重的目的是防止最后几个epoch噪声导致测试集成绩变差。小样本项目里,早停的意义不是节省时间,而是防止验证集上的偶然性波动毁掉模型。

5. Transformer模型实战与避坑指南

Transformer在时间序列预测里不是天生就能拿高分的,它最大的优势是能直接建模长距离依赖,比如暴发前12周的细微走势。但流感数据只有几百点,如果直接套用NLP里的经典配置,几乎必翻车。所以这一章先讲怎么把Transformer吃下时间序列,再给出一组适合小样本的配置和踩坑记录。

5.1 Transformer输入构造:线性embedding与位置编码

Transformer没有循环结构,必须显式告诉它每个时间步的先后顺序,所以位置编码是刚需。流感序列长度只有几十到上百,用最经典的sin/cos位置编码就够了,不需要学出来的位置嵌入。下面这段代码是标准实现,也是项目里最容易复制错的地方。

import math import torch import torch.nn as nn class PositionalEncoding(nn.Module): def __init__(self, d_model, max_len=100): super().__init__() pe = torch.zeros(max_len, d_model) position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1) div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model)) pe[:, 0::2] = torch.sin(position * div_term) pe[:, 1::2] = torch.cos(position * div_term) self.register_buffer('pe', pe.unsqueeze(0)) def forward(self, x): # x: (batch, seq_len, d_model) return x + self.pe[:, :x.size(1)]

参数说明:d_model是Transformer的embedding维度,max_len要大于最大序列长度。0::2取偶数位置,1::2取奇数位置,分别用sin和cos编码。register_buffer的好处是位置编码会跟着模型转到GPU或CPU,不需要手动move。

在时间序列Transformer里,输入通常先用一个线性层把原始特征映射到d_model,再加位置编码:

class TransformerForecast(nn.Module): def __init__(self, input_size, d_model=16, nhead=4, num_layers=2, dropout=0.1): super().__init__() self.embed = nn.Linear(input_size, d_model) self.pos = PositionalEncoding(d_model) encoder_layer = nn.TransformerEncoderLayer( d_model, nhead, dim_feedforward=64, dropout=dropout, batch_first=True ) self.encoder = nn.TransformerEncoder(encoder_layer, num_layers) self.fc = nn.Linear(d_model, 1) def forward(self, x): x = self.embed(x) x = self.pos(x) x = self.encoder(x) # 预测时取最后一个时间步 return self.fc(x[:, -1, :])

逻辑说明:self.embed把2维特征扩到16维,然后加位置编码,送入TransformerEncoder。x[:, -1, :]和LSTM取最后时间步是对齐的操作。dim_feedforward=64比NLP里的2048小得多,这是刻意为之,流感数据扛不住超大前馈网络。

5.2 小数据训练的Transformer配置:d_model、nhead、深度怎么选

Transformer在小数据上的核心矛盾是参数太多、数据太少。按照我的经验,整组配置从下面这个表起步最稳妥:

参数推荐值说明
seq_len12到16周比LSTM的8周稍长,发挥注意力优势
d_model16或32超过64开始过拟合
nhead4d_model=16时nhead=4,每个头分到4维
num_layers1到22层是上限
dim_feedforward64不要超过128
dropout0.1到0.2位置编码后和attention输出后各一遍
lr3e-4和LSTM比偏低,因为Transformer梯度更猛

nhead不是越大越好。d_model=16、nhead=8时每个头只有2维,学到的注意力基本是噪声。nhead=4或者2在这类任务里更正常。

训练Transformer时建议加一个简单的warmup,前10个batch把学习率从1e-5线性升到设定值,后面再正常衰减或使用cosine schedule。流感序列损失面比较平缓,warmup能减少前期震荡。

5.3 5个高频避坑记录:从数据泄漏到训练NaN

这一节只写我在类似时间序列项目里遇到过的真坑,每条都是现象、原因、解决三步。

坑1:训练损失降到很低,预测却是水平线

现象:LSTM或Transformer在训练集上MSE已经很小,但测试集预测曲线像一条直线,起伏非常弱。原因:流感数据归一化后均值在0.3左右,模型发现“一直预测均值”比“努力抓峰值”的损失更低,所以学成了躺平模式。解决:把seq_len从8提高到12到16,同时降低学习率到3e-4,并检查归一化后训练集的标准差,如果标准差小于0.2,说明数据本身变化太小,深度学习很难学到规律。

坑2:训练集RMSE低,测试集RMSE爆炸

现象:换到测试集后预测值突然飞出去,甚至出现负值。原因:大概率是数据泄漏,比如归一化时整个序列一起fit,或者测试集滑窗跨到了训练集前面。解决:严格按时间顺序切分,scaler.fit只用训练集,测试集用transform。同时确认测试集滑窗的起点是train_size - seq_len,保证第一个窗口能看到训练集末尾。

坑3:Transformer训练到一半出现NaN loss

现象:loss在第20个epoch突然变成nan。原因:学习率过高导致self-attention的输出溢出,小数据下位置编码的数值范围也有影响。解决:把学习率降到3e-4以下,给PositionalEncoding的输出乘一个0.1缩放到预训练checkpoint里的常见做法;优化器要用AdamW并加clip_grad_norm_,数值稳定性能救回很多次训练。

坑4:ARIMA预测太平滑,峰值完全消失

现象:ARIMA在测试集上的MAPE不错,但峰值周预测偏差超过3周。原因:ARIMA是线性模型,对突然抬升的尖峰只能做“惯性外推”,这是统计模型的固有边界。解决:这个坑不是靠调参能解决的。正确的态度是承认ARIMA只负责趋势和基线,暴发判定交给LSTM或Transformer。项目报告里可以写“ARIMA在非暴发期与深度学习模型持平,在暴发期滞后1到2周”,这反而是加分项。

坑5:反归一化后预测值超出合理范围

现象:预测的ILI%变成负数或者超过10%,明显不符合医学常识。原因:scaler.inverse_transform要求输入是二维列向量,如果只传一个一维数组,sklearn会当成一行处理,结果完全错位。解决:反变换前强制reshape成(-1, 1):

pred_orig = scaler.inverse_transform(np.array(preds).reshape(-1, 1)).flatten()

逻辑说明:reshape(-1, 1)把形状变成(样本数, 1),inverse_transform才能按列做反归一化,最后flatten()变回一维方便计算指标。这是所有人都会遇到的形状问题,不是模型问题。

6. 三模型对比与加权集成:把高分项目落在交付上

跑通三个模型不是终点,交付一个能让评审信服的结论才是终点。最后这一章讲怎么把结果组织成对比表、怎么集成,以及哪些小技巧能让项目看起来真的有工程价值。

6.1 滚动测试集与三模型结果对比

测试集最好固定为最后8到10周。对每周t,三个模型都只用t之前的数据做预测,然后汇总误差。这种方式叫滚动测试,和真实使用场景一致。

输出结果时建议整理成类似下面的表格,每个指标保留两位小数,并且在文字里解释为什么某个模型在某项上更好。

模型RMSEMAPE (%)峰值偏差 (周)
ARIMA0.329.82.3
LSTM0.278.11.4
Transformer0.257.61.2
加权集成0.216.30.9

表格里的数字是我见过的典型量级,真实项目必须用自己模型在测试集上的实际输出。峰值偏差列建议用2.3节里的peak_time_error计算,这比只写RMSE更能打动评委。

6.2 加权集成:用验证集学权重还是简单平均?

集成不是为了炫技,而是因为ARIMA擅长低频趋势,LSTM擅长局部拐点,Transformer擅长长距离依赖,三者预测的误差不是完全正相关。最简单的集成是等权重平均,三个模型各占1/3。等权重的好处是没有过拟合风险,因为权重不需要在验证集上学习。

如果要做加权集成,我一般用验证集上的组合优化,代码如下:

from scipy.optimize import minimize # val_preds是三个模型在验证集上的预测,形状都是(n,) def obj_func(w): w = np.abs(w) ensemble = w[0] * val_preds[0] + w[1] * val_preds[1] + w[2] * val_preds[2] return float(np.sqrt(mean_squared_error(val_y, ensemble))) res = minimize(obj_func, x0=[1/3, 1/3, 1/3], bounds=[(0, 1)] * 3) w = res.x / np.sum(res.x) print('优化权重:', w)

逻辑说明:np.abs(w)保证权重非负,归一化让权重总和为1。minimize默认用L-BFGS-B,三个参数收敛非常快。这里有个坑:如果不加bounds,优化器可能给出负权重,负权重在指标上可能分数更好,但物理意义说不通,评审一眼就能看出问题。

权重优化完成后,测试集上不要重新调权重,否则等于在测试集上做超参搜索,属于一种隐性数据泄漏。验证集上定的权重直接搬到测试集即可。

6.3 高分项目的加分项:峰值偏差、置信区间和可视化

最后聊几个不用改模型就能拉高评分的小习惯。

第一个是置信区间。小样本时间序列预测对点估计很容易过度自信。我通常用历史残差的标准差来画预测区间:如果测试集残差的标准差是0.15,那么预测值加减1.96倍标准差就是这个周的95%置信区间。不用模型,不用贝叶斯,直接在报告里写“基于残差的近似置信区间”,方向正确又容易复现。

第二个是可复现实验设置。所有随机种子固定成一个,比如random.seed(42)、np.random.seed(42)、torch.manual_seed(42),并在README里说明。很多人跑同一个Transformer出现两次结果不一致,就是因为没固定种子。项目报告里加一段“实验环境”说明,直接写Python版本、PyTorch版本、随机种子,是工程素养的体现。

第三个是反直觉的结论比华丽模型更值钱。如果测试集显示Transformer没有超过LSTM,不要硬调超参把数字拉平。我见过最靠谱的高分项目,结论是“在流感这类50周级别周期数据上,LSTM与Transformer差距不显著,但集成稳定优于单模型”。这个结论比堆出三个模型然后硬吹Transformer强太多。

我自己的习惯是,每次做时序预测都先把ARIMA基线跑完、固定随机种子、把scaler保存下来,这三步做完才允许自己碰深度学习。模型可以换,alpha不能乱加,测试集只能碰一次。希望这篇笔记能帮你把流感预测这个题目做得又快又稳,也希望你能在报告里保留那份“真实实验记录”的诚实,这比任何花哨的超参数都更能拿分。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询