关于深度实战社区
我们是一个深度学习领域的独立工作室。团队成员有:中科大硕士、纽约大学硕士、浙江大学硕士、华东理工博士等,曾在腾讯、百度、德勤等担任算法工程师/产品经理。全网20多万+粉丝,拥有2篇国家级人工智能发明专利。
社区特色:深度实战算法创新
获取全部完整项目数据集、代码、视频教程,请进入官网:zzgcz.com。竞赛/论文/毕设项目辅导答疑,v:zzgcz_com
1. 项目简介
本项目旨在利用深度学习中的长短期记忆网络(LSTM)来预测未来30天的销售额。
递归预测(Recursive Forecasting)和多步预测(Direct Multi-Step Forecasting)
方法 | 优点 | 缺点 |
---|---|---|
递归预测 | 简单易实现;资源消耗较低;适应性强 | 误差累积;依赖性高;难以捕捉长期依赖关系 |
直接多步预测 | 避免误差累积;更好地捕捉多步间依赖关系;灵活性高;提高预测效率 | 模型复杂度增加;训练难度较大;难以扩展到长预测步数;需要更多的标注数据 |
递归预测是一种逐步预测方法,利用模型对一个时间步的预测结果作为下一个时间步的输入,递归地生成多个未来时间步的预测值。具体步骤如下:
- 使用历史数据预测第一个未来时间步。
- 将预测的值添加到输入序列中,移除最早的一个时间步。
- 使用更新后的输入序列预测下一个时间步。
- 重复上述过程,直到预测所需的所有未来时间步。
优点
-
简单易实现:
- 实现过程直接,不需要对模型结构进行复杂修改。
- 适用于大多数现有的单步预测模型,如LSTM、ARIMA等。
-
资源消耗较低:
- 只需训练一个模型来预测单个时间步,因此训练和存储需求较低。
-
适应性强:
- 可以应用于不同类型的时间序列数据,无需对数据进行特殊处理。
缺点
-
误差累积(Error Accumulation):
- 每一步的预测误差都会传递到下一个预测步骤,导致后续预测的误差可能显著增加。
- 尤其在长时间步预测时,累积误差可能使预测结果不可靠。
-
依赖性高:
- 后续预测依赖于之前所有的预测结果,任何一个步骤的错误都会影响整体预测性能。
-
难以捕捉长期依赖关系:
- 对于需要考虑长时间跨度依赖关系的序列,递归预测可能无法充分利用这些信息。
直接多步预测是一种同时预测多个未来时间步的方法。具体来说,模型直接输出多个未来时间步的预测值,而不是逐步预测。常见的实现方式包括:
- 独立模型方法(Independent Models): 为每个未来时间步训练一个独立的模型。
- 联合模型方法(Joint Models): 使用一个模型同时输出多个未来时间步的预测值。
优点
-
避免误差累积:
- 由于模型直接输出多个时间步的预测值,单步预测的误差不会传递到后续步骤,减少了误差累积的问题。
-
更好地捕捉多步间的依赖关系:
- 模型可以学习多个未来时间步之间的相互关系,提高预测的整体一致性和准确性。
-
灵活性高:
- 可以根据需求调整预测的时间步长度,而不受递归预测的限制。
-
提高预测效率:
- 通过一次性预测多个时间步,可以减少计算资源的消耗,尤其是在需要大规模预测时。
缺点
-
模型复杂度增加:
- 需要设计和训练能够输出多个时间步的复杂模型,可能需要更多的训练数据和计算资源。
-
训练难度较大:
- 由于需要同时预测多个时间步,模型在训练过程中可能面临更复杂的优化问题,导致训练不稳定或收敛速度较慢。
-
难以扩展到非常长的预测步数:
- 随着预测步数的增加,模型的输出维度也会增加,可能导致模型性能下降或过拟合。
-
需要更多的标注数据:
- 直接预测多个时间步需要更多的训练样本,以确保模型能够学习到有效的多步依赖关系。
-
选择合适的方法的建议
选择递归预测还是直接多步预测,取决于具体的应用场景和数据特性。以下是一些建议:
-
预测步数较短:
- 如果只需要预测未来几个时间步,递归预测通常足够且简单。
-
预测步数较长:
- 对于需要预测较长时间步的情况,直接多步预测可能更合适,能够减少误差累积的问题。
-
数据特性:
- 如果时间序列具有强烈的多步依赖关系,直接多步预测能够更好地捕捉这些依赖。
- 如果数据波动较大,递归预测可能更容易受到误差的影响。
-
计算资源:
- 递归预测通常计算资源消耗较低,适合资源有限的情况。
- 直接多步预测需要更高的计算资源和更多的训练数据,但在资源充足时能够提供更好的预测性能。
-
模型复杂度与训练时间:
- 如果模型训练时间和复杂度是限制因素,递归预测更具优势。
- 如果可以接受更长的训练时间和更高的模型复杂度,直接多步预测可能带来更好的结果。
2.模型核心改进点
数据预处理中的数据泄漏问题
问题描述
在代码中,使用 MinMaxScaler
对整个数据集(包括训练集和测试集)进行了拟合(fit
)和转换(transform
):
特征缩放
scaler = MinMaxScaler()
scaled_features = scaler.fit_transform(data)
scaled_data = pd.DataFrame(scaled_features, columns=features + [target])
这种做法会导致数据泄漏(Data Leakage) ,即测试集的信息被泄露到训练集中,从而在模型评估时高估模型的性能。
数据泄漏(Data Leakage)是什么?
数据泄漏是指在模型训练过程中,未经意间将测试集(或未来的信息)引入训练过程,使得模型在评估时表现出不真实的高性能。这种情况会导致模型在实际应用中表现不佳,因为模型在训练时已经"见过"测试集的信息。
为什么会发生数据泄漏?
在时间序列预测中,数据具有时间依赖性,训练集和测试集应保持时间上的先后顺序。通过在整个数据集上进行拟合 MinMaxScaler
,您实际上是在训练阶段使用了测试集的信息,具体表现为:
- 统计信息共享:
MinMaxScaler
在拟合时计算整个数据集的最小值和最大值。这意味着测试集的最小值和最大值也被用于训练集的缩放。 - 特征分布影响: 测试集的特征分布信息被传递到了训练集中,使得模型在训练时已经部分"了解"测试集的分布。
数据泄漏带来的影响
- 性能高估: 模型在训练时已经获得了测试集的一部分信息,导致在测试集上的表现被高估。
- 泛化能力差: 由于模型在训练时依赖于测试集的信息,实际应用中面对新的、未见过的数据时,模型的表现可能远不如预期。
- 不可靠的评估指标: 评估指标(如MSE、MAE等)可能反映出不真实的模型性能,误导模型选择和优化过程。
如何避免数据泄漏
要避免数据泄漏,尤其是在数据预处理阶段,关键是在数据划分(训练集和测试集)之后,再进行任何数据转换操作。具体步骤如下:
- 数据划分: 首先,将数据集划分为训练集和测试集,确保训练集仅包含用于模型训练的数据,测试集仅用于模型评估。
- 拟合Scaler: 仅在训练集上拟合(
fit
)缩放器。 - 转换数据: 使用在训练集上拟合的缩放器,分别转换训练集和测试集。
2、处理缺失值的方法可能引入未来信息
在代码中,添加了 last_week_sale
特征后,使用了 后向填充(Backfill) 方法来处理缺失值:
添加上周销售额特征
df['last_week_sale'] = df['sale'].shift(7)
处理缺失值
df.fillna(method='bfill', inplace=True) 使用后向填充方法填补缺失值
问题在于 ,使用后向填充方法可能会导致 数据泄漏(Data Leakage) ,即利用未来的信息来填补过去的缺失值。这会导致模型在训练时"看到了"未来的数据,从而高估模型的性能,并在实际应用中表现不佳。
为什么使用后向填充会引入未来信息?
在时间序列数据中,处理缺失值时,填补缺失值的方法需要谨慎选择,以避免利用未来的信息。具体到您的代码:
last_week_sale
特征的创建:
df['last_week_sale'] = df['sale'].shift(7)
这意味着,每一天的 last_week_sale
是七天前的销售额。周期性模式捕捉 : 在许多业务场景中,销售额具有明显的周期性,特别是基于星期的周期。例如,周末的销售额可能高于工作日。通过创建7天的滞后特征,模型可以学习到这种每周的周期性模式。因此,前七天的数据在创建 last_week_sale
时会产生缺失值(因为没有七天前的数据)。
使用后向填充(Backfill):
df.fillna(method='bfill', inplace=True)
后向填充会用后面的非缺失值来填补缺失值。例如,假设第1天到第7天的 last_week_sale
是缺失的,后向填充会用第8天到第14天的 last_week_sale
来填补第1天到第7天的缺失值。
问题 在于,填补第1天到第7天的 last_week_sale
使用了第8天到第14天的销售额,这相当于用未来的信息来填补过去的缺失值,导致数据泄漏。
为了避免数据泄漏,尤其是在处理时间序列数据时,需遵循以下原则:
- 选择合适的填补缺失值方法: 在时间序列中,通常使用 前向填充(Forward Fill) 或 插值(Interpolation) 来填补缺失值,以避免引入未来信息。
3、LSTM模型输出维度与损失函数的匹配问题
在代码中,LSTM模型的输出维度设置为 FORECAST_STEPS
(30),即一次性预测未来30天的销售额。然而,PyTorch中的 nn.MSELoss
默认计算每个元素的均方误差(Mean Squared Error, MSE),这可能未能充分利用时间序列的结构信息,影响模型的预测性能和效果。
定义模型参数
input_size = len(features)
hidden_size = 64
num_layers = 2
output_size = FORECAST_STEPS 输出维度设为30
dropout = 0.2初始化模型
model = LSTMModel(input_size, hidden_size, num_layers, output_size, dropout).to(device)
定义损失函数和优化器
criterion = nn.MSELoss()
learning_rate = 0.001
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
- LSTM模型的输出维度设置
output_size = FORECAST_STEPS
(30) :这意味着模型的最终输出层是一个线性层,将LSTM的隐藏状态映射到30个输出值,代表未来30天的销售额预测。
self.fc = nn.Linear(hidden_size, output_size)
-
forward
方法中的输出:- LSTM层的输出形状为
(batch_size, seq_length, hidden_size)
。 - 通过
out[:, -1, :]
取最后一个时间步的隐藏状态,得到形状为(batch_size, hidden_size)
。 - 通过全连接层,输出形状变为
(batch_size, output_size)
,即(batch_size, 30)
。
- LSTM层的输出形状为
2. nn.MSELoss
的工作机制
- 默认行为 :
nn.MSELoss
计算预测值和目标值之间每个元素的均方误差,并对所有元素取平均。 - 形状匹配 :在您的设置中,预测值和目标值的形状都是
(batch_size, 30)
,因此nn.MSELoss
会逐元素计算误差并求平均。
- 潜在的问题
-
未充分利用时间序列结构:
nn.MSELoss
只是简单地计算每个预测值与实际值的误差,并对所有预测步数进行平均。这种方式忽略了不同时间步之间的依赖关系和序列结构。- 时间序列数据具有明显的时序依赖性,未来的销售额往往依赖于过去的销售模式,而不仅仅是单独的预测值。
-
预测步数之间的相互依赖性未被利用:
- 直接预测30个时间步意味着模型在一次性输出所有预测值,但
nn.MSELoss
无法捕捉这些预测值之间的相互关系。 - 例如,某一天的销售额可能会影响接下来几天的销售额,这种依赖关系在损失函数中没有得到体现。
- 直接预测30个时间步意味着模型在一次性输出所有预测值,但
-
权重分配不均:
- 所有预测步数在损失计算中被平等对待,未考虑某些时间步对整体预测的重要性。例如,近期的预测可能比远期的预测更重要,但
nn.MSELoss
无法反映这一点。
- 所有预测步数在损失计算中被平等对待,未考虑某些时间步对整体预测的重要性。例如,近期的预测可能比远期的预测更重要,但
为了解决上述问题,可以考虑以下几种方法,以确保损失函数和模型输出更好地匹配时间序列的特性。
使用自定义损失函数
设计一个能够捕捉时间步之间依赖关系的损失函数。例如,可以为不同的预测步数赋予不同的权重,或引入时间步之间的平滑性约束。加权MSELoss
序列到序列(Seq2Seq)模型
使用编码器-解码器结构的模型,可以更好地捕捉序列间的依赖关系。这种结构允许模型在生成每个时间步的预测时,参考整个输入序列的上下文。
3. 数据集与预处理
本项目所使用的数据集来自某商品的历史销售记录,包含多个特征字段,例如销售日期、销售额、温度等外部因素。数据集中最为核心的目标变量是商品的每日销售额,辅助特征包括温度和日期相关的周数信息。通过这些特征,项目能够分析和捕捉销售额的周期性趋势以及受到外部因素影响的波动性。
- 在数据预处理环节,首先对数据集进行了日期格式的标准化,将日期列转换为
datetime
格式,并按照时间顺序对数据进行了排序,确保后续模型能够捕捉到时间序列的顺序关系。此外,添加了周数作为新的特征,帮助模型捕捉到销售额的季节性和周期性模式。项目还引入了"上一周销售额"这一特征,使得模型在预测时能够参考历史数据,增加了时间序列的上下文信息。 - 为了处理缺失值,项目采用了向前填充的方式(
bfill
),确保特征值的完整性,避免数据不完整导致的模型训练问题。接下来,对选定的特征(如温度、周数、上一周销售额)进行了归一化处理。归一化使用了MinMaxScaler
方法,将所有特征值缩放至0到1之间,确保特征的数值范围相近,从而避免某些特征因数值较大而对模型产生过度影响。 - 项目还对时间序列数据进行了序列化处理,通过自定义函数将原始数据转换为适合LSTM模型的序列输入形式。每个输入序列包含过去30天的特征值,而对应的目标是预测未来30天的销售额。这种序列化处理不仅保留了原始时间序列的顺序信息,还为模型提供了充足的上下文。
4. 模型架构
本项目采用的是长短期记忆网络(LSTM)来进行未来30天销售额的预测。LSTM是一种能够捕捉时间序列中长期依赖关系的循环神经网络(RNN)变种,能够有效解决序列数据中的梯度消失问题。项目中的LSTM模型由以下几部分组成:
LSTM层
LSTM的核心是其记忆单元和门机制,通过这些机制,它能够决定在每个时间步保留、更新或丢弃哪些信息。
输入门(Input Gate):决定当前时间步的输入对记忆状态的影响。其数学公式为:
i t = σ ( W i ⋅ [ h t − 1 , x t ] + b i ) i_t = \sigma(W_i \cdot [h_{t-1}, x_t] + b_i) it=σ(Wi⋅[ht−1,xt]+bi)
其中,it表示输入门的输出,σ是sigmoid激活函数,Wi 和 bi 是输入门的权重和偏置,ht−1 是前一个时间步的隐藏状态,xt 是当前时间步的输入。
遗忘门(Forget Gate):决定从记忆单元中遗忘多少信息,公式为:
f t = σ ( W f ⋅ [ h t − 1 , x t ] + b f ) f_t = \sigma(W_f \cdot [h_{t-1}, x_t] + b_f) ft=σ(Wf⋅[ht−1,xt]+bf)
其中,ft表示遗忘门的输出,Wf 和 bf 是遗忘门的权重和偏置。
候选记忆单元(Cell Candidate):生成候选记忆状态,用于更新当前的记忆单元,公式为:
C ~ t = tanh ( W C ⋅ [ h t − 1 , x t ] + b C ) \tilde{C}t = \tanh(W_C \cdot [h{t-1}, x_t] + b_C) C~t=tanh(WC⋅[ht−1,xt]+bC)
其中,C~t 是候选的记忆单元状态,tanh 是双曲正切激活函数。
输出门(Output Gate):控制记忆状态如何影响输出,公式为:
o t = σ ( W o ⋅ [ h t − 1 , x t ] + b o ) o_t = \sigma(W_o \cdot [h_{t-1}, x_t] + b_o) ot=σ(Wo⋅[ht−1,xt]+bo)
记忆单元更新(Cell State Update):通过遗忘门和输入门来更新记忆单元,公式为:
C t = f t ∗ C t − 1 + i t ∗ C ~ t C_t = f_t * C_{t-1} + i_t * \tilde{C}_t Ct=ft∗Ct−1+it∗C~t
隐藏状态更新(Hidden State Update):通过输出门和当前的记忆单元状态来更新隐藏状态,公式为:
h t = o t ∗ tanh ( C t ) h_t = o_t * \tanh(C_t) ht=ot∗tanh(Ct)
LSTM前向传播公式:
h t , C t = LSTM ( x t , h t − 1 , C t − 1 ) h_t, C_t = \text{LSTM}(x_t, h_{t-1}, C_{t-1}) ht,Ct=LSTM(xt,ht−1,Ct−1)
全连接层
LSTM的输出通过全连接层(fully connected layer)进行映射,得到最终的预测值。全连接层的公式为:
y = W f c ⋅ h t + b f c y = W_{fc} \cdot h_t + b_{fc} y=Wfc⋅ht+bfc
其中,Wfc 和 bfc 分别是全连接层的权重和偏置,ht 是LSTM输出的隐藏状态。
Dropout层
为了防止过拟合,模型中还使用了Dropout层,在训练过程中随机屏蔽掉部分神经元,Dropout的公式为:
h ′ = Dropout ( h , p ) h' = \text{Dropout}(h, p) h′=Dropout(h,p)
其中,p 是保留的神经元概率。
模型的整体训练流程
数据加载与处理:首先将时间序列数据按30天作为一个输入序列,构建训练和测试集。每个输入序列对应未来30天的销售额预测。
模型训练:在训练阶段,模型使用均方误差(MSE)作为损失函数。MSE的公式为:
MSE = 1 n ∑ i = 1 n ( y i − y ^ i ) 2 \text{MSE} = \frac{1}{n} \sum_{i=1}^{n}(y_i - \hat{y}_i)^2 MSE=n1i=1∑n(yi−y^i)2
其中,yi是实际值,y^i是模型预测值,n 是样本数量。优化器选择Adam算法,基于反向传播更新模型参数。
评估指标:在评估阶段,除了MSE外,还使用了平均绝对误差(MAE)作为评估指标。MAE的公式为:
MAE = 1 n ∑ i = 1 n ∣ y i − y ^ i ∣ \text{MAE} = \frac{1}{n} \sum_{i=1}^{n} |y_i - \hat{y}_i| MAE=n1i=1∑n∣yi−y^i∣
评估过程中,测试集的预测结果通过这些指标进行比较,最后输出模型的误差情况。
可视化与结果分析:训练完成后,模型的损失曲线和预测结果通过图表进行可视化,便于直观了解模型的收敛情况以及预测性能。
5. 核心代码详细讲解
1. 数据预处理与特征工程
df = pd.read_csv('smoothed_ES_sku023.csv')
解释 : 读取销售数据的CSV文件。这里使用的是pandas
库的read_csv
函数,将原始数据加载为DataFrame格式。
df['date'] = pd.to_datetime(df['date'], format='%Y/%m/%d')
解释 : 将date
列转换为datetime
格式,方便后续的时间序列操作和按日期排序。
df = df.sort_values('date')
df.reset_index(drop=True, inplace=True)
解释: 对数据集按日期升序排序,确保时间序列的顺序一致。同时重置索引,删除旧索引并更新为新顺序。
df['week_number'] = df['date'].dt.isocalendar().week
解释: 提取每条数据所属的周数,并将其作为新的特征添加到数据集中,捕捉销售额的周期性特征。
df['last_week_sale'] = df['sale'].shift(7)
解释 : 创建"上一周销售额"这一特征,用于捕捉前一周销售额对当前销售额的影响。shift(7)
表示将销售额向前平移7天。
df.fillna(method='bfill', inplace=True)
解释: 处理缺失值。这里使用向后填充的方法(backward fill),用之后的有效值填补缺失数据,确保数据的完整性。
features = ['temperature', 'week_number', 'last_week_sale']
target = 'sale'
解释: 定义模型的特征列和目标变量。其中,特征列包含温度、周数和上一周的销售额,目标变量为每日的销售额。
scaler = MinMaxScaler()
scaled_features = scaler.fit_transform(data)
解释 : 使用MinMaxScaler
将特征和目标变量缩放到0到1之间的范围。这样做可以避免不同特征数值范围差异太大,防止某些特征对模型的影响过大。
2. 创建多步序列数据
def create_sequences(data, seq_length, forecast_steps, feature_cols, target_col):
xs = []
ys = []for i in range(len(data) - seq_length - forecast_steps + 1):
x = data.iloc[i:i + seq_length][feature_cols].values
y = data.iloc[i + seq_length:i + seq_length + forecast_steps][target_col].values
xs.append(x)
ys.append(y)return np.array(xs), np.array(ys)
-
解释: 这是创建多步序列数据的核心函数。它通过滑动窗口的方式将输入数据分割成固定长度的序列,用于训练LSTM模型。
-
data
: 输入的已预处理数据。 -
seq_length
: 输入序列的长度,这里为过去30天。 -
forecast_steps
: 预测步长,这里为未来30天。 -
feature_cols
: 使用的特征列。 -
target_col
: 目标列。 -
函数返回两个数组:
xs
为输入特征序列,ys
为对应的目标序列。X, y = create_sequences(scaled_data, SEQ_LENGTH, FORECAST_STEPS, features, target)
-
-
解释 : 使用上面的
create_sequences
函数生成训练和测试数据,X
是特征序列,y
是目标值序列。
3. 模型架构构建
class LSTMModel(nn.Module):def init(self, input_size, hidden_size, num_layers, output_size, dropout=0.2):super(LSTMModel, self).
__init__
()
self.hidden_size = hidden_size
self.num_layers = num_layers
self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True, dropout=dropout)
self.fc = nn.Linear(hidden_size, output_size)
-
解释: 这是模型的构建部分,定义了LSTM模型的结构。
-
input_size
: 输入特征的数量。 -
hidden_size
: LSTM层的隐藏单元数量,决定了每层输出的维度。 -
num_layers
: LSTM层的数量,这里使用了2层。 -
dropout
: Dropout的比例,用于防止过拟合。 -
lstm
: 定义了一个LSTM层,带有batch_first=True
,即输入的batch维度为第一维。 -
fc
: 全连接层,将LSTM的输出映射到最终的预测值(未来30天的销售额)。def forward(self, x):
h0 = torch.zeros(self.num_layers, x.size(0), self.hidden_size).to(x.device)
c0 = torch.zeros(self.num_layers, x.size(0), self.hidden_size).to(x.device)
out, _ = self.lstm(x, (h0, c0))
out = out[:, -1, :]
out = self.fc(out)return out
-
-
解释: 前向传播函数。初始化LSTM的隐藏状态和细胞状态为全零向量,然后通过LSTM层获取输出,并仅保留最后一个时间步的输出。最终通过全连接层生成预测结果。
4. 模型训练与评估
criterion = nn.MSELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
-
解释: 定义损失函数和优化器。损失函数为均方误差(MSE),优化器为Adam算法,它通过梯度下降优化模型的参数。
for epoch in range(EPOCHS):
model.train()
epoch_train_loss = 0for X_batch, y_batch in train_loader:
X_batch = X_batch.to(device)
y_batch = y_batch.to(device)
outputs = model(X_batch)
loss = criterion(outputs, y_batch)
optimizer.zero_grad()
loss.backward()
optimizer.step()
epoch_train_loss += loss.item() * X_batch.size(0) -
解释: 训练循环。每个epoch中,模型首先进入训练模式,并循环遍历训练数据进行前向传播、计算损失、反向传播和优化更新权重。每个batch训练完成后,累积损失用于后续的模型评估。
model.eval()
epoch_test_loss = 0with torch.no_grad():for X_batch, y_batch in test_loader:
X_batch = X_batch.to(device)
y_batch = y_batch.to(device)
outputs = model(X_batch)
loss = criterion(outputs, y_batch)
epoch_test_loss += loss.item() * X_batch.size(0) -
解释: 测试循环。在评估阶段,模型切换为评估模式,不进行梯度更新。通过前向传播计算测试集的损失,以评估模型的泛化能力。
mse = mean_squared_error(y_test_unscaled, y_pred_unscaled)
mae = mean_absolute_error(y_test_unscaled, y_pred_unscaled) -
解释: 计算均方误差(MSE)和平均绝对误差(MAE),作为模型的评估指标,评估预测值与真实值之间的偏差。
↓↓↓更多热门推荐:
CNN模型实现CIFAR-10彩色图片识别
全部项目数据集、代码、教程进入官网zzgcz.com