【PyTorch深度学习】ST-ResNet模型预测出租车流入流出客流量(附源码和数据集)

需要源码请点赞关注收藏后评论区留言私信~~~

一、数据预处理

首先根据经纬度将一个城市区域划分为 I×J 网格地图,将时刻t的流入流量(inflow)和流出流量(outflow)叠在一起看成一个 2×I×J的张量,类似于两通道的图像,例如网格(i,j)的像素值(P1,P2)分别代表了该地区在在时刻t的流入和流出流量值

此处通过纽约出租车上下客需求的样例数据进行数据处理实战(已经对数据做好预处理),样例数据条目如下表

 

首先导入必要的库函数以及定义经纬度范围和划分网格的规格

对数据进行清洗,去除在所选定的经纬度范围之外的点

读取表格数据,并将数据按照半小时为基本时间片划分开

定义时间戳差分函数

再定义划分网格并计算网格矩阵的函数,该函数的基本功能为计算每一个点所对应的网格索引,并在对应的网格中累加统计值:

我们还可以对得到的网格图进行可视化,选择第一个时间片的网格矩阵,导入seaborn库,可视化上下客需求的网格热力图

 

最后是将所有时间片的网格矩阵生成出来并放置于列表中:

二、问题陈述及模型框架

 ST-ResNet可以用来同时预测城市中每个区域的流入和流出客流量。其主要贡献在于使用残差神经网络框架来对城市区域交通流量的时间临近性、周期和趋势特性建模(closeness, period, trend)

论文针对每种时间粒度的属性,都设计了一个残差卷积单元的分支,每个残差卷积单元对交通流的空间特性进行建模,ST-ResNet网络的末端整合三个残差神经网络分支的输出,为不同的分支和区域分配不同的权重。将整合结果进一步结合外部因素(external),如天气和一周中的哪一天,来预测每个地区最终流量

 

网络结构主要由4部分组成,分别提取时间邻近性、周期性、趋势性以及外部因素的影响。首先将一个城市在每个时间间隔内的流入流和流出流分别转化为一个双通道的类图像矩阵,然后将时间轴划分为三个片段,表示最近的时间、稍远的时间和遥远的时间(考虑了邻近时间段,前一天相同时间段,上一周相同时间段)。每一个片段分别被输入到三个残差卷积网络分支中用来提取提取时间邻近性、周期性、趋势性。前三个部分与卷积神经网络共享相同的网络结构,然后是残差单元序列。这种结构捕捉了空间上附近和远处区域之间的空间依赖关系。除了交通流量本身的特征外,还有很多外部因素可以对交通流量/需求产生显著影响,因此ST-ResNet模型还从外部数据集中提取了一些特征,例如天气条件和节假日事件,并将它们输入一个两层全连接的神经网络。在主干网络的末端,前三部分的输出与外部因素的输出进行融合,然后利用tanh激活函数映射到[-1, 1]区间内作为输出

我们以ST-ResNet论文中采用的北京出租车数据为例,描述数据处理以及深度模型搭建的流程。下面将从数据处理到模型构建的过程对各部分主要代码依次进行详细介绍

三、数据准备

项目结构如下

首先定义最大最小归一化函数,将数据归到[-1, 1]这样一个大小固定的区间内

可以有效防止了异常大值和异常小值对神经网络模型训练的影响。

将原始数据中字符串形式的日期转化成计算机可识别的时间戳形式

完成对时间戳数据的处理后,就要开始构建与时间戳对应的数据集,即由邻近性特征,周期性特征,趋势性特征和预测时间步所对应的真实值组成

循环判断时间间隔下界小于给定时间戳序列总数

返回代表邻近性、周期性以及趋势性特征数据

选取当前时刻的前三个时间片数据

读入北京市出租车数据。北京市出租车原始数据是一系列的H5格式的文件。H5是一种层次化的数据集,一个文件下面可以包含多个文件夹(group)和多个文件(dataset)。

进行数据预处理,移除一部分脏数据(按照半小时为基本单元,一天24小时被划分成48个时间片,所以需要删除一天不足48个时间片的部分)

遍历返回流量网格图数据以及时间戳数据

读取额外特征数据

获取三种时间粒度及标签数据

划分训练集、测试集、验证集

四、模型构建

定义一个残差卷积网络层基本单元

导入一些编写神经网络必要的库

由于ST-ResNet骨干网络拥有三种时间粒度的分支网络结构,且每个分支网络的结构相同,所以我们可以先统一定义分支网络的结构

定义ST-ResNet的整体网络架构。主干网络部分为融合三种时间粒度的卷积残差神经网络(临近性,周期性,趋势性),并且对还要合并额外的特征

五、模型的训练及测试

导入必要的库函数和先前定义好的数据读取模块及网络模块

读取训练、验证和测试数据(包括邻近性、周期性、趋势性以及额外特征数据)

读取归一化数据转换器,并将训练集、测试集、验证集转化为Dataset格式

批量封装训练集、测试集和验证集

结果展示如下 北京出租车流量网格热点图可视化如下,可以发现模型已经可以较为准确的预测出租车的整体流量态势

 六、代码

最后 部分代码如下

最大最小化代码

# Acknowledgement: This piece of code is taken from https://github.com/TolicWang/DeepST
import numpy as np


np.random.seed(1337)  # for reproducibility


class MinMaxNormalization(object):
    """
        MinMax Normalization --> [-1, 1]
        x = (x - min) / (max - min).
        x = x * 2 - 1
    """

    def __init__(self):
        pass

    def fit(self, X):
        self._min = X.min()
        self._max = X.max()
        print("min:", self._min, "max:", self._max)

    def transform(self, X):
        X = 1. * (X - self._min) / (self._max - self._min)
        X = X * 2. - 1.
        return X

    def fit_transform(self, X):
        self.fit(X)
        return self.transform(X)

    def inverse_transform(self, X):
        X = (X + 1.) / 2.
        X = 1. * X * (self._max - self._min) + self._min
        return X

ST-ResNet代码如下

import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np
import logging
import os
import torch.optim as optim
import time

class ResUnit(nn.Module):
    def __init__(self, in_channels, out_channels, lng, lat):
        # 模型的输入是一个四维张量 (B, C, lng, lat)
        super(ResUnit, self).__init__()
        self.bn1 = nn.BatchNorm2d(in_channels)
        self.conv1 = nn.Conv2d(in_channels, out_channels, 3, 1, 1)
        self.bn2 = nn.BatchNorm2d(out_channels)
        self.conv2 = nn.Conv2d(out_channels, out_channels, 3, 1, 1)
        # 分别定义两个批归一化层和卷积层
    def forward(self, x):
        z = F.relu(self.bn1(x))
        z = self.conv1(z)
        z = F.relu(self.bn2(z))
        z = self.conv2(z)
        out = z + x  # 经过两层卷积神经网络之后与输入网络的原始特征直接相加
        return out      


class Branch_net(nn.Module):
    def __init__(self, num_res_unit, input_lenght, flow_channel, grid_heigh, grid_width):
        super(Branch_net, self).__init__()
        self.num_res_unit = num_res_unit
        self.input_lenght = input_lenght
        self.flow_channel = flow_channel
        self.grid_heigh = grid_heigh
        self.grid_width = grid_width
        # 每个分支网络的首部都是用一个卷积网络将输入的特征维度从低维映射到高维
        self.branch_net = nn.ModuleList([nn.Conv2d(self.input_lenght*
           self.flow_channel, 64, kernel_size=3, stride=1, padding=1)])
        # 接下来依次添加多个残差卷积单元
        for i in range(self.num_res_unit): 
            self.branch_net.append(ResUnit(64, 64, self.grid_heigh, self.grid_width))
        # 每个分支网络的尾部都是用一个卷积网络将输入的特征维度从高维映射到原本的维度
        self.branch_net.append(nn.Conv2d(64, self.flow_channel, kernel_size=3,stride = 1, padding = 1))
    # 分支网络的前向传播
    def forward(self, x):
        for layer in self.branch_net:
             x = layer(x)
        return x 

class STResNet(nn.Module):
    def __init__(self,
                 lr=0.0001, #模型学习率
                 epoch=50, #训练轮次数
                 batch_size=32, #批训练batch大小
                 c_length=3, #邻近性时间流量特征序列默认长度
                 p_length=1, #周期性时间流量特征序列默认长度
                 t_length=1,  #趋势性时间流量特征序列默认长度
                 external_dim=28, #外部特征的维度
                 grid_heigh=32, #网格图的高度
                 grid_width=32, #网格图的宽度
                 flow_channel=2, #流量种类
                 num_res_unit=2, #设定残差卷积单元的数量
                 data_min = -10000,  # 输入数据的最小值默认值
                 data_max = 10000):  # 输入数据的最大值默认值
        super(STResNet, self).__init__()
        self.epoch = epoch  
        self.lr = lr
        self.batch_size = batch_size
        self.c_length = c_length
        self.p_length = p_length
        self.t_length = t_length
        self.external_dim = external_dim
        self.grid_heigh = grid_heigh
        self.grid_width = grid_width
        self.flow_channel  = flow_channel 
        self.num_res_unit = num_res_unit
        self.logger = logging.getLogger(__name__)
        self.data_min = data_min
        self.data_max = data_max
        self.gpu_available = torch.cuda.is_available()
        if self.gpu_available:  #如果GPU存在,则调用
            self.gpu = torch.device("cuda:0")
        self.backbone_net()
        self.save_path="L%d_C%d_P%d_T%d/"% (self.num_res_unit,self.c_length, self.p_length, self.t_length)
        self.best_mse = 10000

    def backbone_net(self): #创建ST-ResNet的主干网络
        #创建邻近性分支网络 
        self.c_net = Branch_net(self.num_res_unit,self.c_length,
                     self.flow_channel, self.grid_heigh, self.grid_width) 
        #创建周期性分支网络
        self.p_net = Branch_net(self.num_res_unit,self.p_length,
                    self.flow_channel, self.grid_heigh, self.grid_width)
        #创建趋势性分支网络
        self.t_net = Branch_net(self.num_res_unit,self.t_length,
                    self.flow_channel, self.grid_heigh, self.grid_width)
        # 外部特征映射网络, 将外部特征映射成固定维数的特征向量
        self.ext_net = nn.Sequential(
          nn.Linear(self.external_dim, 10), 
          nn.ReLU(inplace = True),
          nn.Linear(10,self.flow_channel*self.grid_heigh*self.grid_width))
        # 定义用于三个分支网络输出融合的三个融合矩阵参数
        self.w_c = nn.Parameter(torch.randn((self.flow_channel,
                self.grid_heigh, self.grid_width)), requires_grad=True)
        self.w_p = nn.Parameter(torch.randn((self.flow_channel,
                self.grid_heigh, self.grid_width)), requires_grad=True)
        self.w_t = nn.Parameter(torch.randn((self.flow_channel,
                self.grid_heigh, self.grid_width)), requires_grad=True)

    # 整个网络中的前向传播
    def forward(self, xc, xp, xt, ext):
        # 得到邻近性网络的输出
        xc = self.c_net(xc)
        # 得到周期性网络的输出
        xp = self.p_net(xp)
        # 得到趋势性网络的输出
        xt = self.t_net(xt)
        # 得到额外特征映射网络的输出
        ext_out = self.ext_net(ext).view([-1, self.flow_channel,
                                          self.grid_heigh, self.grid_width])
        # 三种时间特征及外部特征的融合
        res = self.w_c.unsqueeze(0) * xc + self.w_p.unsqueeze(0) * xp + self.w_t.unsqueeze(0) * xt
        out = torch.tanh(res + ext_out)
        return out
    
    # 模型的训练启动部分
    def train_model(self, train_loader, val_loader):
        optimizer = optim.Adam(self.parameters(),lr = self.lr)
        loss_func = nn.MSELoss() # 定义模型的优化器和损失函数
        early_stop_threshold = 10 # 设定提前停止阈值
        epoch_count = 0
        start_time = time.time()
        for ep in range(self.epoch):
            loss_list = []
            self.train() # 启动模型训练
            for i, (xc, xp, xt, xe, y) in enumerate(train_loader):
                if self.gpu_available:
                    xc = xc.to(self.gpu)
                    xp = xp.to(self.gpu)
                    xt = xt.to(self.gpu)
                    xe = xe.to(self.gpu)
                    y = y.to(self.gpu)
                ypred = self.forward(xc, xp, xt, xe)
                loss = loss_func(ypred, y)
                optimizer.zero_grad()
                loss.backward()
                optimizer.step()
            # 模型验证部分,根据验证集上的损失保存最优模型
            for i, (xc, xp, xt, xe, y) in enumerate(val_loader):
                if self.gpu_available:
                    xc = xc.to(self.gpu)
                    xp = xp.to(self.gpu)
                    xt = xt.to(self.gpu)
                    xe = xe.to(self.gpu)
                    y = y.to(self.gpu)
                ypred = self.forward(xc, xp, xt, xe)
                val_loss = loss_func(ypred, y)
                loss_list.append(val_loss.item())
                end_time = time.time() 
            val_mse = np.mean(loss_list)
            print("[%.2fs] ep %d val mse %.4f" %(end_time - start_time, ep, val_mse))
            if val_mse < self.best_mse: #保存当前更优模型参数
                self.save_model("best_model")
                self.best_mse = val_mse
                epoch_count = 0
            else:
                epoch_count = epoch_count +1
            if epoch_count >= early_stop_threshold:
                break  # 当超过提前停止阈值时整个循环结束

    # 模型的评估测试集启动部分
    def test_model(self, test_loader):
        self.eval() # 启动模型评估(固定BN层参数)
        for i, (xc, xp, xt, xe, y) in enumerate(test_loader):
            if self.gpu_available:
                xc = xc.to(self.gpu)
                xp = xp.to(self.gpu)
                xt = xt.to(self.gpu)
                xe = xe.to(self.gpu)
                y = y.to(self.gpu)
            with torch.no_grad():
                ypred = self.forward(xc, xp, xt, xe)
            # 采用RMSE和MAE作为模型评估的指标
            rmse = ((ypred - y) **2).mean().pow(1/2)
            mae = ((ypred - y).abs()).mean()
            # 将RMSE和MAE恢复到规范化之前的尺度
            rmse = rmse * (self.data_max - self.data_min)
            mae = mae * (self.data_max - self.data_min)
        return rmse, mae 
    
    # 模型保存代码
    def save_model(self, name):
        if not os.path.exists(self.save_path):
            os.makedirs(self.save_path)
        torch.save(self.state_dict(), self.save_path + name + ".pkl")
    
    # 读取已保存的模型
    def load_model(self, name):
        if not name.endswith(".pkl"):
            name += ".pkl"
        self.load_state_dict(torch.load(self.save_path + name))

创作不易 觉得有帮助请点赞关注收藏~~~

猜你喜欢

转载自blog.csdn.net/jiebaoshayebuhui/article/details/130462095