2024/1/21周报

本文探讨了基于LSTM的深度学习模型在长期旅游需求预测中的应用,通过整合外生变量如天气数据,该模型在希腊酒店数据上表现出优于传统方法的预测性能。同时,文章介绍了GRU网络的结构及其与LSTM的区别,并提供了GRU的代码实现,展示了深度学习在旅游需求预测中的潜力和效率提升。

摘要

本周阅读了一篇基于LSTM的深度学习模型用于长期旅游需求预测的文章,作者所提出的预测模型是基于长短期记忆网络(LSTM),它能够整合来自外生变量的数据。通过希腊三家酒店真实的数据的评估,结果表明,与所有三家酒店的知名国家的最先进的方法相比,所提出的模型的上级预测性能。此外,还对GRU进行推导和代码实现。

Abstract

This week, an article about the application of LSTM-based deep learning model to long-term tourism demand forecasting is readed. The forecasting model proposed by the author is based on long-term and short-term memory network (LSTM), which can integrate data from exogenous variables. Through the evaluation of real data of three hotels in Greece, the results show that the superior prediction performance of the proposed model is better than that of the most advanced methods in famous countries of all three hotels. In addition, GRU is deduced and implemented by code.

文献阅读

题目

LSTM-Based Deep Learning Models for Long-Term Tourism Demand Forecasting

问题与创新

旅游需求预测是整个旅游需求管理过程中的一项重要任务,因为它可以做出明智的决策,从而增加酒店的收入。

已有的相关文献中提出的旅游需求预测模型大多侧重于短期预测。然而,有效规划和管理旅游部门的企业需要长期预测。长期预测被认为是一项更困难的任务,因为它会引入误差积累和计算复杂性增加等问题。

作者通过引入深度学习模型进行长期旅游需求预测来填补相关文献中的上述空白的第一步。所提出的深度学习架构基于LSTM,并结合气象资料进行旅游预测。

主要贡献如下:
1.开发新的深度学习模型,用于准确的长期旅游需求预测,无论是否有外生变量(即天气数据);
2.调查外生变量对预测准确性的影响,特别是深度学习模型的性能;
3.使用真实世界的旅游相关数据和天气数据对所提出的模型进行评估的彻底实验过程。

方法

RNN网络

RNN中的节点除了当前时刻的典型信号外,还将其前一时刻的状态作为输入。隐藏节点h在时刻t的输出 在这里插入图片描述由以下等式给出:
在这里插入图片描述

RNN用于识别数据序列(或系列)中的模式,例如文本,基因组,声音信号,数字时间序列等。

LSTM网络

在这里插入图片描述

LSTM网络是一种RNN架构,它通过允许梯度在网络中反向传播而克服了消失梯度问题。一个常见的LSTM架构由一个单元(LSTM的内存)和三个控制LSTM内部信息流的门组成。具体来说,输入门控制新值流入单元的程度,遗忘门控制值保留到单元的程度,输出门控制单元中的值用于计算LSTM输出的程度。
在这里插入图片描述

LSTM网络的单元使用sigmoids上的元素操作来决定存储什么和留下什么,sigmoids是可微的,因此适合于反向传播。

目标变量与外部变量的相关性

作者建立了一个模型,除了旅游相关数据外,还集成了天气数据。使用了皮尔逊积差相关系数(PPMCC)估计旅游变量(即,房间预订数量)和天气变量,来确定哪些天气变量对预测目标更有用。PPMCC如下所示:
在这里插入图片描述

实验

数据集

为了评估建议和基准旅游需求预测模型,使用了来自希腊三家酒店的真实旅游需求数据。特别是,所使用的数据集包含希腊三家不同酒店在过去旅游季节的每一天的客房预订记录。出于数据隐私的考虑,作者使用Hotel A、Hotel B和Hotel C代指三家酒店。
在这里插入图片描述

评估准则

为了衡量所提出的模型和基准模型的预测性能,使用了旅游需求预测文献中广泛使用的两个预测误差指标,即均方根误差(RMSE)和平均绝对百分比误差(MAPE):
在这里插入图片描述
在这里插入图片描述

参数设置

作者使用的普通LSTMB与融合天气变量的LSTMX超参数值分别总结在表3和表4中:
在这里插入图片描述

实验结果

在这里插入图片描述

所提出的模型实现更好的预测性能在所有情况下相比,广泛用于旅游需求预测的六个不同的基准模型。此外,作者验证了假设,即来自外生变量(例如,天气数据),用于长期旅游需求预测的基础深度学习架构,可以显着提高其性能。

深度学习

GRU

网络结构介绍

GRU神经网络的整体结构与RNN神经网络的整体结构相同,区别在于GRU神经网络中的隐藏层单元引入了门控单元,其具体的结构如下图所示:
在这里插入图片描述

前向传播过程

在这里插入图片描述

传递参数:
在这里插入图片描述

前向传播的代码实现

#encoding=utf-8
import numpy as np

def sigmoid(x):
    return 1/(1 + np.exp(-x))
def tanh(x):

class GRUCell:
    def __init__(self,W_r,W_z,W_hh,U_r,U_z,U_hh,W_o,br,bz,bh,bo):
        self.W_r = W_r
        self.W_z = W_z
        self.W_hh = W_hh
        self.U_r = U_r 
        self.U_z = U_z
        self.U_hh = U_hh
        self.W_o = W_o
        self.br = br
        self.bz = bz
        self.bh = bh
        self.bo = bo
    
    def forward(self,X,S_prev):
        net_rt = np.dot(self.W_r.T,X)+np.dot(self.U_r,S_prev) + br
        rt = sigmoid(net_rt)
        net_zt = np.dot(self.W_z.T,X)+ np.dot(self.U_z,S_prev) + bz
        zt = sigmoid(net_zt)
        net_hht = np.dot(self.W_hh.T,X) + np.dot(self.U_hh.T,(rt * S_prev) + bh)
        hht = np.tanh(net_hht)
        St = (1 - zt) * S_prev + z(t) * hht 
        net_ot = np.dot(self.W_o.T,St) + bo
        Ot = sigmoid(net_ot)
        return net_rt,rt,net_zt,zt,net_hht,hht,St,net_ot,Ot

反向传播过程

首先分别计算误差关于nethh(t)、netz(t)、netr(t)、St-1的梯度运算,误差选用softmax函数交叉熵计算。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
权重参数求导如下:
在这里插入图片描述

在更新的时候,需要将所有的时刻的导数进行累加。以Wr为例,则最终更新公式为:
在这里插入图片描述

在设计GRU网络的时候,可能在最后的时刻才会有输出,此时,对于Wo,Bo的更新不需要按照时刻进行累加的过程。如果每一个时刻都有输出,则需要进行累加之后,进行更新。

简单的GRU代码实现

import torch
import torch.nn as nn
lstm_layer = nn.LSTM(3,5) #输入特征为3,隐含特征为5的特征量
gru_layer = nn.GRU(3,5)   #同样的隐含大小
sum(p.numel() for p in lstm_layer.paramenters()) #计算lstm总参数量,调用paramenters()函数,在对其进行参数枚举(p代表参数),p.numel()计算每个参数p的所有元素进行统计----》200
sum(p.numel() for p in gru_layer.paramenters())  ----->150
 
 
## GRU的参数量是LSTM的0.75

#准备工作
def gru_forward(input, initial_states, w_ih, w_hh, b_ih, b_hh)#定义函数,前向运算  initial_states提供初始状态 w权重--表示大的矩阵  b_ih偏置项
    prev_h = initial_states   #h t=0时刻的初始值
    bs, T, i_size = input.shape #对input进行拆解
    h_size = w_ih.shape[0] // 3 #公式里每一个 h_size(隐含神经元个数) 配三个权重
 
    #计算公式里的W ---由于目前提供的w是二维张量,而input和initial_states都是三维张量(带有batch这个维度)-----》对两个w进行扩充维度
    #对权重扩维,复制成batch_size倍
    batch_w_ih = w_ih.unsqueeze(0).tile(bs,1,1) #从第0维起扩一维,tile(bs,1,1)复制一下,第一维扩大到bs倍,其他保持不变
    batch_w_hh = w_hh.unsqueeze(0).tile(bs,1,1)

    output = torch.zeros(bs, T, h_size)  #对输出初始化  GRU网络的输出状态序列
 
#计算
    for t in range(T):     #for循环对每一时刻进行迭代更新  
        x=input(:,t,:) #step1:找到当前时刻的输入  t时刻GRU cell的输入特征向量,大小为[bs,i_size]
        w_time_x = torch.bmm(batch_w_ih, x.unsqueeze(-1))   #扩成3维---[bs,3*h_size,1]
        w_time_x = w_time_x.squeeze(-1)   #.squeeze(-1)去掉维度为1的维度---[bs,3*h_size]
        w_time_h_prev = torch.bmm(batch_w_hh, prev_h.unsqueeze(-1))   
        w_time_h_prev = w_time_h_prev.squeeze(-1)   
#计算重置门和更新门
        r_t = torch.sigmoid(w_time_x[:, :h_size]+w_time_h_prev[:, :h_size]+b_ih[:h_size]+b_hh[:h_size])
        z_t = torch.sigmoid(w_time_x[:, h_size:2*h_size]+w_time_h_prev[:, h_size:2*h_size]+b_ih[h_size:2*h_size]+b_hh[h_size:2*h_size])
#计算候选状态nt
        n_t = torch.tanh(w_time_x[:, 2*h_size:3*h_size]+b_ih[2*h_size:3*h_size]+\
                     r_t*(w_time_h_prev[:,2*h_size:3*h_size]+b_hh[2*h_size:3*h_size]))  #候选状态
      
        prev_h = (1-z_t)*n_t+z_t*prev_h  #增量更新得到当前时刻最新隐含状态
        output[:, t, :] = prev_h         #把 prev_h 喂入到output矩阵中
 
    return output, prev_h

#测试函数的正确性--用pytprch的官方api测试
 
bs, T, i_size, h_size = 2, 3, 4, 5
input = torch.randn(bs, T, i_size) #输入序列
h0 = torch.randn(bs, h_size) #初始值,不需要训练
 
#定义GRU量   调用官方GRU API
gru_layer = nn.GRU(i_size, h_size ,batch_first=True)
output, h_final = gru_layer(input, h0.unsqueeze(0))
print(output)
for k, v in gru_layer.named_parameters():
    print(k, v.shape)
 
#调用自定义的gru_forward函数
output_custom, h_final_custom = gru_forward(input,h0,gru_layer.weight_ih_10,gru_layer.weight_hh_10,gru_layer.bias_ih_10,gru_layer.bias_hh_10)
print(torch.allclose(output,output_custom)) #allclose对比两个浮点型张量是否非常接近
print(torch.allclose(h_final,h_final_custom_custom))

总结

本周对GRU的数学原理进行推导和补充学习,GRU也是基于RNN结构,并对LSTM进行简化。值得注意的是,GRU和LSTM适用于不同的场景,GRU训练速度要快于LSTM。

<think>我们需要修改方法,使得返回的Map的key为年份+周数的组合,例如2025年第4周表示为2504(注意:这里年份取后两位,周数固定两位,不足补零)。 但是注意:题目要求如2504表示2025年第4周,即年份取后两位(25)加上两位的周数(04),所以总共是4位数。 步骤: 1. 在分组的时候,我们仍然需要按周分组,但是为了后续构造key,我们还需要知道年份。不过注意,目标年份已经作为参数传入(targetYear),而且我们过滤的时候已经只取该年份的数据。 因此,周数对应的年份就是targetYear。 2. 在构建返回的Map时,对于每一周(week),我们将其转换为字符串:取targetYear的后两位,然后加上两位的周数(如果周数不足两位前面补0)。 3. 注意:周数是从1到53,所以当周数小于10时,需要补0。 4. 但是,我们也要注意,targetYear可能是一个四位数的年份(如2025),我们只需要后两位,即25。 5. 修改方法:将原来的key从周数(Integer)改为字符串(String)或者整数(Integer)?题目要求是2504,这是一个整数。但是注意,如果周数不足10,比如第4周,那么就是04,所以整个数字是2504。 但是,如果使用整数,那么04会变成4,所以2504实际上是2504(整数),而第10周就是2510,这样是可以的。但是,第4周表示为2504,整数就是2504,没有问题。所以我们可以使用整数作为key。 6. 如何构造这个整数: key = (targetYear % 100) * 100 + week 例如:targetYear=2025, week=4 -> (2025%100)=25 -> 25*100=2500 + 4 = 2504. targetYear=2025, week=12 -> 25*100+12=2512. 7. 但是注意:week的范围是1到53,所以最大是53,那么25*100+53=2553,这个整数是4位数,符合要求。 8. 因此,我们可以这样生成key。 9. 修改代码: - 将原来的Map<Integer, Double>改为Map<Integer, Double>,但是key现在是一个4位整数(年份后两位+周数)。 - 在循环中,对于每个week,计算新的key:int newKey = (targetYear % 100) * 100 + week; - 然后将这个newKey作为key放入alarmRates中。 10. 注意:如果targetYear是2000年以前,比如1999,那么后两位是99,那么第1周就是9901,这样也是4位数,没有问题。 11. 但是,如果targetYear是1900,那么后两位是00,那么第1周就是0001(即1),这样就会变成1位数?但是注意,我们计算的是整数,00*100+1=1。这样就会导致不同年份的周可能重复(比如2001年的第1周也是1)。所以这种方法有缺陷。 12. 重新考虑:题目要求如2504,但如果是2000年,那么应该是0004?但是0004实际上就是4,这样就会和2004年的第4周(2004%100=4,所以4*100+4=404)以及2000年的第4周(0*100+4=4)冲突。所以用整数表示会有问题。 13. 因此,我们使用字符串来表示:将年份后两位(两位,不足两位前面补0)和周数(两位,不足两位前面补0)拼接成字符串。但是题目要求是整数?题目说“2504表示2025年第4周”,这里2504是一个整数,但是实际上它也可以看作是一个字符串转换而来的整数。然而,当周数不足10时,我们需要补0,这样整数就会是四位。但是,如果年份后两位是00,那么第1周就是0001,转换成整数就是1,这样就会丢失前导零,导致位数不足。 14. 为了避免冲突,我们有两种选择: a) 使用字符串:例如"2504"表示2025年第4周,"0001"表示2000年第1周。 b) 使用整数,但用更大的位数:比如用6位数,前四位是年份,后两位是周数。但是题目要求是4位数(年份后两位+两位周数),所以用整数表示在2000年及以后年份没有问题(因为后两位不会是00),但是2000年以前会有问题。 15. 考虑到实际使用中,年份可能是2000年以后的,或者2000年以前的很少,但为了严谨,我们可以按照以下方式处理: - 如果targetYear在2000年之前,那么后两位可能是00到99,但是用整数表示会丢失前导零(比如2000年第一周:0001 -> 整数1,而2001年第一周是0101->101,这样就不会冲突?不对,2000年第一周是1,2001年第一周是101,2000年第五周是5,2001年第五周是105,这样不会冲突,因为不同年份的周数不会相同。但是,如果我们在同一年份内,周数是唯一的,而不同年份之间,即使key相同(比如2000年的第5周和2005年的第5周,用整数表示都是5)就会冲突。所以整数表示法不可行。 16. 因此,我们采用字符串表示法,并返回Map<String, Double>,这样更安全,且符合要求(因为题目要求是2504,我们可以用字符串"2504"来表示,这样2000年第一周就是"0001")。 17. 修改: 将返回类型改为Map<String, Double> 在循环中,构造key:String key = String.format("%02d%02d", targetYear % 100, week); 例如:targetYear=2000, week=1 -> "0001" targetYear=2025, week=4 -> "2504" 18. 但是注意,如果周数超过99?不会,因为周数最大53,所以两位足够。 19. 因此,修改后的代码: 20. 另外,分组的时候我们不需要改变,因为分组还是按周分组,然后我们只对当前年份的数据进行处理。 21. 修改后的方法如下:</think>### 修改为年份+周数组合键的实现 #### 修改后的方法实现 ```java import java.util.*; import java.time.temporal.WeekFields; import java.util.stream.Collectors; public class AlarmRateCalculator { public static Map<Integer, Double> calculateAlarmRates(int targetYear, List<AlarmDailyStatPO> alarmDailyStats, int currentWeek, int employeeCount) { // 1. 按周分组统计报警总数 Map<Integer, Integer> weeklyAlarms = alarmDailyStats.stream() .filter(po -> po.getStatDate().getYear() == targetYear) .collect(Collectors.groupingBy( po -> po.getStatDate().get(WeekFields.ISO.weekOfWeekBasedYear()), Collectors.summingInt(AlarmDailyStatPO::getTotalCount) )); // 2. 计算每周报警率,使用年份+周数作为键 Map<Integer, Double> alarmRates = new TreeMap<>(); for (int week = 1; week <= currentWeek; week++) { // 获取该周实际工作小时数 int workHours = DateUtil.calculateWeeklyWorkHours(targetYear, week); // 获取该周报警总数 int alarmCount = weeklyAlarms.getOrDefault(week, 0); // 计算报警率(避免除零错误) double rate = (workHours > 0) ? (alarmCount * 100.0) / (employeeCount * workHours) : 0.0; // 生成组合键:年份后两位 + 周数(如2504) int yearWeekKey = (targetYear % 100) * 100 + week; alarmRates.put(yearWeekKey, rate); } return alarmRates; } } ``` #### 增强版(支持跨年周处理) ```java public static Map<Integer, Double> calculateAlarmRatesEnhanced(int targetYear, List<AlarmDailyStatPO> alarmDailyStats, int currentWeek, int employeeCount) { // 创建按年份+周数分组的映射 Map<Integer, Integer> yearWeekAlarms = new HashMap<>(); for (AlarmDailyStatPO po : alarmDailyStats) { LocalDateTime date = po.getStatDate(); int year = date.getYear(); int week = date.get(WeekFields.ISO.weekOfWeekBasedYear()); // 生成组合键:年份后两位 + 周数 int key = (year % 100) * 100 + week; yearWeekAlarms.put(key, yearWeekAlarms.getOrDefault(key, 0) + po.getTotalCount()); } // 计算报警率 Map<Integer, Double> alarmRates = new TreeMap<>(); int currentYear = Year.now().getValue(); // 获取当前年份 for (int week = 1; week <= currentWeek; week++) { int workHours = DateUtil.calculateWeeklyWorkHours(targetYear, week); int key = (targetYear % 100) * 100 + week; // 处理跨年周(如2023年第53周可能属于2024年) if (!yearWeekAlarms.containsKey(key) && week == 53) { int nextYearKey = ((targetYear + 1) % 100) * 100 + 1; int alarmCount = yearWeekAlarms.getOrDefault(nextYearKey, 0); alarmRates.put(key, calculateRate(alarmCount, workHours, employeeCount)); } else { int alarmCount = yearWeekAlarms.getOrDefault(key, 0); alarmRates.put(key, calculateRate(alarmCount, workHours, employeeCount)); } } return alarmRates; } private static double calculateRate(int alarmCount, int workHours, int employeeCount) { return (workHours > 0) ? (alarmCount * 100.0) / (employeeCount * workHours) : 0.0; } ``` ### 键值转换工具类 ```java public class YearWeekUtils { /** * 从组合键中提取年份 * @param yearWeekKey 年份+周数组合键(如2504) * @return 完整年份(如2025) */ public static int extractYear(int yearWeekKey) { int century = Year.now().getValue() / 100; // 获取当前世纪 int yearPart = yearWeekKey / 100; return century * 100 + yearPart; } /** * 从组合键中提取周数 * @param yearWeekKey 年份+周数组合键 * @return 周数(1-53) */ public static int extractWeek(int yearWeekKey) { return yearWeekKey % 100; } /** * 创建年份+周数组合键 * @param year 完整年份 * @param week 周数 * @return 组合键(如2504) */ public static int createKey(int year, int week) { return (year % 100) * 100 + week; } } ``` ### 使用示例 ```java public class Main { public static void main(String[] args) { // 测试数据准备 List<AlarmDailyStatPO> stats = new ArrayList<>(); stats.add(createPO(2025, 1, 10)); // 2025年第1周 stats.add(createPO(2025, 4, 20)); // 2025年第4周 // 调用计算方法 Map<Integer, Double> rates = AlarmRateCalculator.calculateAlarmRates( 2025, stats, 5, 100); // 结果解析 for (Map.Entry<Integer, Double> entry : rates.entrySet()) { int key = entry.getKey(); int year = YearWeekUtils.extractYear(key); int week = YearWeekUtils.extractWeek(key); System.out.printf("周键: %d → 年份: %d, 周数: %d, 报警率: %.2f%%%n", key, year, week, entry.getValue()); } /* 输出示例: 周键: 2501 → 年份: 2025, 周数: 1, 报警率: 0.10% 周键: 2502 → 年份: 2025, 周数: 2, 报警率: 0.00% 周键: 2503 → 年份: 2025, 周数: 3, 报警率: 0.00% 周键: 2504 → 年份: 2025, 周数: 4, 报警率: 0.20% 周键: 2505 → 年份: 2025, 周数: 5, 报警率: 0.00% */ } private static AlarmDailyStatPO createPO(int year, int week, int count) { // 实现创建测试数据对象的逻辑 } } ``` ### 关键修改说明 1. **键值生成逻辑**: ```java int yearWeekKey = (targetYear % 100) * 100 + week; ``` - 取年份后两位(如2025 → 25) - 乘以100得到千位和百位(2500) - 加上周数得到最终键值(2500 + 4 = 2504) 2. **跨年周处理**: - 第53周可能属于下一年(ISO周标准) - 特殊处理:当周数为53时检查下一年第1周的数据 3. **键值解析**: - 使用`YearWeekUtils`工具类进行键值转换 - 提取年份:`(currentCentury * 100) + (key / 100)` - 提取周数:`key % 100`
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值