2022 社会计算创新大赛 https://momodel.cn/competition 城市是社会的重要组成部分,时间序列模型是深度学习中广泛影响的模型。本次比赛的主题是加深对时间序列模型的理解和应用,以及空间信息的利用。利用城市前两小时的历史交通流量(每个交通节点每五分钟记录一次交通流量)预测下一个五分钟的交通流量,数据给出节点之间的无向图数据。
A、提供数据读取、基本模型、模型训练等基本代码 B、参赛者需要完成核心模型构建代码,并尽可能将模型调整到最佳状态
评分指标是误差和评分的映射值。
1. 赛题介绍
1.1 大赛背景
时序模型是深度学习中影响广泛的模型,其主题是加深对时序模型的理解和应用,以及空间信息的利用。
1.2 大赛要求
利用城市前两小时的历史交通流量预测下一个五分钟的交通流量一次交通流量)预测下一个五分钟的交通流量,数据给出结点之间的无向图数据。
1.3 大赛环境
基使用基础 Python 的 Pandas、Numpy、Scikit-learn 处理等库的相关特征,使用 Keras、Tensorflow、Pytorch 框架建立深度学习模型时,请注意 Python 包(库)版本。
1.4 评估指标
评分指标是误差和评分的映射值。 误差和(error_score)=均方根误差(RMSE) 平均绝对误差(MAE)
1.5 大赛事项
使用平台的注意事项 该平台的 Notebook 在 CPU 所以尽量不要试试 Notebook 上做希望让 GPU 做的工作。
注意训练模型 如果想要线下训练模型,请保证线下的环境与该平台一致,否则可能无法在该平台运行,可以在该平台的 terminal 输入pip list查看相应的包装版本。
作业注意事项 该操作的目的是加深对空间和时间模型的理解和应用,理论上工作的预测指标不应低于基本模型。
1.6 参考资料
相关框架文档 scikit-learn: https://scikit-learn.org/stable/
tensorflow: https://tensorflow.google.cn/tutorials?hl=zh_cn
pytorch: https://pytorch.org/tutorials/
该领域的论文 [NIPS2015]ConvLSTM https://papers.nips.cc/paper/5955-convolutional-lstm-network-a-machine-learning-approach-for-precipitation-nowcasting.pdf
[ICML2016]vertex domain(spatial domain) http://proceedings.mlr.press/v48/niepert16.pdf
[ICLR2018]DCRNN https://arxiv.org/pdf/1707.01926.pdf
[AAAI2019]STDN https://www.aaai.org/ojs/index.php/AAAI/article/view/4511/4389
框架学习教程 动手学深度学习(Pytorch版): https://tangshusen.me/Dive-into-DL-PyTorch/
深度学习框架PyTorch:入门与实战: https://github.com/chenyuntc/pytorch-book
2. 赛题内容
2.1 数据集
数据集是 2018 1月和2月的一条高速公路被选中 307 通过大量传感器每五分钟统计一次个结点的交通流量统计。由 train_data.csv 给出。其中,银行代表一个时间点,从第一行表示 2018/01/01 00:00:00-2018/01/01 00:04:59 的车流量,逐五分钟递推,给出了前 50 天的信息。列代表结点。某一位置的值代表该时间段该结点的交通流量。
数据集已预处理,缺失值用线性插值填充。
测试和评估集不在上述给出的时间范围内,是在该区域相距不远的其他时间段。
另一文件 graph.csv 给出了高速公路相互直接点之间的无向边缘和距离。节选后的传感器确保至少有距离 3.5 英里。
# 首先先 import 一些主包 import numpy as np import pandas as pd import time import matplotlib.pyplot as plt # 画图使用 %matplotlib inline
# 数据文件夹 base_path = 'datasets/60843339ef0b1353a25d0e2a-momodel/' # 读取数据文件 data = pd.read_csv(base_path 'train_data.csv') # 输出数据的形状 print(data.shape)
# 前三行输出数据 print(data.head(3))
使用统计信息,可以看到每个结点的数量、平均值等信息。
# 统计信息输出数据 data.describe()
您还可以绘制具体的结点图像,以便更清楚地感受到数据。结点可以在下图中感觉到 5 和结点 154 即使在下面的无向图中,流量也有明显的相似性,两个结点实际上是相距的 347.2 里。
# 新建一个图像
plt.figure(figsize=(16,8))
# 绘画某些结点第一天的情况(即前 288 个结点)
plt.plot(data.iloc[:288,73].values.reshape(-1),c='blue')
plt.plot(data.iloc[:288,5].values.reshape(-1),c='red')
plt.plot(data.iloc[:288,154].values.reshape(-1),c='green')
# 展示图像
plt.show()
注意到数值较大处和较小处相比差距还是巨大的,为了深度模型更好的工作,我们使用 MinMaxScaler 进行归一化。当然,用户也可以自行选择其他的预处理方式。
这里我们选用 sklearn 的 Scaler ,如果有兴趣,也可以使用 torchvision 或者自己实现相关内容。
from sklearn.preprocessing import MinMaxScaler
scaler = MinMaxScaler(feature_range=(-1, 1))
data = np.array(data)
data_scaled = scaler.fit_transform(data.reshape(-1, 1)).reshape(data.shape)
本数据集提供了空间信息,并要求利用空间和时间信息预测未来的流量。
空间信息利用表格提供一张双向图,其中 cost 表示点与点之间的距离,单位是英里。
对于,也有一些简单的方法,比如精心挑选某些结点,抽取相邻结点构成子图,获取子图的邻接矩阵,然后利用 CNN 的方法提取特征等。
# 读入图文件
edges = pd.read_csv(base_path + 'graph.csv')
# 查看图的大小
print(edges.shape)
# 输出前十条边
print(edges.head(10))
2.2 数据处理
首先需要生成题目所需的训练集合。
# 生成题目所需的训练集合
def generate_data(data):
# 先将目标数据转换成 numpy 类型
data = np.array(data)
# 目标是生成可直接用于训练和测试的 x 和 y
x = []
y = []
# 每 24 行组成一个 x , 第 25 行为需要预测的值 y
for i in range(data.shape[0]-25):
curr_x = data[i:i+24]
curr_y = data[i+24:i+25]
x.append(curr_x)
y.append(curr_y)
return x,y
然后对数据集合进行分割,其中训练集用于训练,校验集用于检验模型训练情况,测试集合用于测试模型效果。
# 生成 train valid test 集合,以供训练所需
def generate_training_data(x,y):
# 样本总数
num_samples = x.shape[0]
# 测试集大小
num_test = round(num_samples * 0.2)
# 训练集大小
num_train = round(num_samples * 0.7)
# 校验集大小
num_val = num_samples - num_test - num_train
# 训练集拥有从 0 起长度为 num_train 的样本
x_train, y_train = x[:num_train], y[:num_train]
# 校验集拥有从 num_train 起长度为 num_val 的样本
x_val, y_val = (
x[num_train: num_train + num_val],
y[num_train: num_train + num_val],
)
# 测试集拥有尾部 num_test 个样本
x_test, y_test = x[-num_test:], y[-num_test:]
# 返回这些集合
return x_train, y_train, x_val, y_val, x_test, y_test
下面尝试整理空间信息,为了简化问题,我们忽略边权,用户可以自由选用,先根据表格构造出邻接矩阵。
邻接矩阵 G[a][b] 表示从 a 到 b 是否存在直接的边
# 结点个数
n_nodes = data.shape[1]
# 建立一个空的邻接矩阵
G = np.zeros(shape = (n_nodes,n_nodes))
# 输出 G 的大概外观
print(G)
# 为了方便,将 edges 转为 numpy
edges = np.array(edges,dtype=np.int32)
# 将表格中的边加入邻接矩阵
for i in range(edges.shape[0]):
# 取一条边的两个结点
u = edges[i][0]
v = edges[i][1]
# 将正向边和反向边赋值为 1
G[u][v] = G[v][u] = 1
print(G)
下面研究分析一下该图的情况。
# 结点连接计数
nodes_connected_count = {
}
for i in range(n_nodes):
# 计数
key = np.sum(G[i,:])
# 如果记录过该数字,则加 1
if key in nodes_connected_count:
nodes_connected_count[key] += 1
# 否则,则加入该元素,并赋值为 1
else:
nodes_connected_count[key] = 1
# 输出统计情况
print(nodes_connected_count)
{2.0: 194, 6.0: 3, 3.0: 29, 1.0: 50, 4.0: 20, 5.0: 10, 7.0: 1} 可以看到,该图大部分结点都仅拥有较小的度数。
2.3 建立一个简单的模型
选用一种框架,告诉其创建模型的常用方式以及常用的接口 建立一个简单模型并进行训练保存 分析模型训练过程以及模型概况 加载模型并对模型进行评估 加载模型并预测输入数据的结果
2.3.1 处理数据
该赛题示范使用 Pytorch 完成。也可以选用其他框架进行训练并预测结果。
# 加载 pytorch
import torch
# 处理数据,并将其转化为 Pytorch 的形式。
# 获取数据中的 x, y
x,y = generate_data(data_scaled)
# 将 x,y 转换乘 tensor , Pytorch 模型默认的类型是 float32
x = torch.tensor(x,dtype=torch.float32)
y = torch.tensor(y,dtype=torch.float32)
print(x.shape,y.shape)
# 将 y 的中间维度转化掉
y = y.view(y.shape[0],-1)
print(x.shape,y.shape)
torch.Size([14375, 24, 307]) torch.Size([14375, 1, 307]) torch.Size([14375, 24, 307]) torch.Size([14375, 307])
# 处理出训练集,校验集和测试集
x_train, y_train, x_val, y_val, x_test, y_test = generate_training_data(x,y)
为了方便使用 DataLoader ,我们需要自定义一个 Dataset ,自定义的 Dataset 只需要继承后实现下面三个函数。
# 建立一个自定 Dataset
class MyDataset(torch.utils.data.Dataset):
def __init__(self, x, y):
self.x = x
self.y = y
def __getitem__(self, item):
return self.x[item], self.y[item]
def __len__(self):
return len(self.x)
# 建立训练数据集、校验数据集和测试数据集
train_data = MyDataset(x_train,y_train)
valid_data = MyDataset(x_val,y_val)
test_data = MyDataset(x_test,y_test)
# 规定批次的大小
batch_size = 64
# 创建对应的 DataLoader
train_iter = torch.utils.data.DataLoader(train_data, batch_size=batch_size, shuffle=True)
# 校验集和测试集的 shuffle 是没有必要的,因为每次都会全部跑一遍
valid_iter = torch.utils.data.DataLoader(valid_data, batch_size=batch_size, shuffle=False)
test_iter = torch.utils.data.DataLoader(test_data, batch_size=batch_size, shuffle=False)
2.3.2 建立模型
下面展示如何建立模型, Pytorch 的建立模型较为简单,只需要完成 forward ,即前向传播函数即可进行训练。这里展示建立一个简单的线性模型。参数 Pytorch 会自动初始化,具体请查看官方文档。
# 输入的数量是 120分钟 除以 5分钟 = 24个时间段,每个时间段有307个结点的车流量数据。
num_inputs = 120 // 5 * 307
# 输出是后 5 分钟的车流量数据
num_outputs = 307
# 建立一个简单的线性模型
class LinearNet(torch.nn.Module):
def __init__(self, num_inputs, num_outputs):
super(LinearNet, self).__init__()
# 一个线性层
self.linear = torch.nn.Linear(num_inputs, num_outputs)
# 前向传播函数
def forward(self, x): # x shape: (batch, 24, 307)
# 这里暗含了将 x 的 shape 改变的操作
y = self.linear(x.view(x.shape[0], -1))
return y
下面建立一个复杂但可能不是很有效的 LSTM 模型,仅供理解 Pytorch 的运行方式而使用。
# 隐藏层的个数
num_hiddens = 128
# 建立一个稍微复杂的 LSTM 模型
class LSTMNet(torch.nn.Module):
def __init__(self, num_hiddens, num_outputs):
super(LSTMNet, self).__init__()
self.hidden_size = num_hiddens
# RNN 层,这里的 batch_first 指定传入的是 (批大小,序列长度,序列每个位置的大小)
# 如果不指定其为 True,传入顺序应当是 (序列长度,批大小,序列每个位置的大小)
self.rnn = torch.nn.LSTM(input_size=num_inputs//24, hidden_size=num_hiddens,batch_first=True)
# 线性层
self.dense = torch.nn.Linear(self.hidden_size*24, 256)
self.dense2 = torch.nn.Linear(256,num_outputs)
# dropout 层,这里的参数指 dropout 的概率
self.dropout = torch.nn.Dropout(0.3)
self.dropout2 = torch.nn.Dropout(0.5)
# ReLU 层
self.relu = torch.nn.ReLU()
# 前向传播函数,这是一个拼接的过程,使用大量变量是为了避免混淆,不做过多讲解
def forward(self, x): # x shape: (batch_size, 24, 307)
# LSTM 层会传出其参数,这里用 _ 将其舍弃
h, _ = self.rnn(x)
# LSTM 层会传出 (batch_size, 24, num_hiddens) 个参数,故需要 reshape 后丢入全连接层
h_r = h.reshape(-1,self.hidden_size*24)
h_d = self.dropout(h_r)
y = self.dense(h_d)
drop_y = self.dropout2(y)
a = self.relu(drop_y)
y2 = self.dense2(a)
return y2
可以看到,Pytorch建立一个模型较为清楚简单,具体使用可以参考文档。
Pytorch 在使用 GPU 和 CPU 上的写法有所不同。在需要将保存在内存中的数据在 GPU 上运行时,需要主动将数据和模型拷贝到显存。
为了简化差异,我们使用一个布尔值:use_gpu 来判断是否可用 GPU ,从而淡化差异。这样就不需要写两份代码。
# 判断 gpu 是否可用
use_gpu = torch.cuda.is_available()
# 另一种写法是固定 device,每次调用数据都 to(device)即可
# device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
2.3.3 评估函数建立
这里给出了评估使用的函数,可以自测以获得信息。
评估指标为误差和的评分表映射值。其中 误差和(error_score)=均方根误差(RMSE)+平均绝对误差(MAE)
我们可以根据建立误差和的评估函数
def compute_mae(y_hat, y):
''' :param y: 标准值 :param y_hat: 用户的预测值 :return: MAE 平均绝对误差 mean(|y*-y|) '''
return torch.mean(torch.abs(y_hat - y))
def compute_rmse(y_hat, y):
''' :param y: 标准值 :param y_hat: 用户的预测值 :return: RMSE 均方根误差 sqrt(mean((y*-y)^2)) '''
return torch.sqrt(torch.mean(torch.pow(y_hat - y, 2)))
下面描绘评估函数,输入 DataLoader 和用户的模型,返回对应的 MAE 和 RMSE 。
def evaluate_accuracy(data_iter, model):
''' :param data_iter: 输入的 DataLoader :param model: 用户的模型 :return: 对应的 MAE 和 RMSE '''
# 初始化参数
mae_sum, rmse_sum, n = 0.0, 0.0, 0
# 对每一个 data_iter 的每一个 x,y 进行计算
for x, y in data_iter:
# 如果运行在 GPU 上,需要将内存中的 x 拷贝到显存中
if (use_gpu):
x=x.cuda()
# 计算模型得出的 y_hat
y_hat = model(x)
# 将 y_hat 逆归一化,这里逆归一化需要将数据转移到 CPU 才可以进行
y_hat_real = torch.from_numpy(scaler.inverse_transform(np.array(y_hat.detach().cpu()).reshape(y_hat.shape)))
y_real = torch.from_numpy(scaler.inverse_transform(np.array(y).reshape(y.shape)))
# 计算对应的 MAE 和 RMSE 对应的和,并乘以 batch 大小
mae_sum += compute_mae(y_hat_real,y_real) * y.shape[0]
rmse_sum += compute_rmse(y_hat_real,y_real) * y.shape[0]
# n 用于统计 DataLoader 中一共有多少数量
n += y.shape[0]
# 返回时需要除以 batch 大小,得到平均值
return mae_sum / n, rmse_sum / n
2.3.4 模型训练
首先我们需要选取优化器和损失函数。
Pytorch 使用的优化器和损失函数可以选用其提供的,也可以自己写。一般来说, Pytorch 自带的具有更好的数值稳定性,这里给出参考。
# 使用均方根误差
loss = torch.nn.MSELoss()
# 自定义的损失函数,可以直接调用
def my_loss_func(y_hat, y):
return compute_mae(y_hat, y)
Pytorch 的优化器需要提供 model 的 parameters ,故需要先定义网络。
# 使用上面描述的线性网络
model = LinearNet(num_inputs,num_outputs)
# 使用 Adam 优化器, learning rate 调至 0.0001
optimizer = torch.optim.Adam(model.parameters(), lr=1e-4)
# 也可选用 SGD 或其他优化器
# optimizer = torch.optim.SGD(model.parameters(), lr=1e-4, momentum=0.9, weight_decay=0.1)
下面是训练函数。用于模型的直接训练。
def train_model(model, train_iter, test_iter, loss, num_epochs,
params=None, optimizer=None):
# 用于绘图用的信息
train_losses, valid_losses, train_maes, train_rmses, valid_maes, valid_rmses = [], [], [], [], [], []
# 循环 num_epochs 次
for epoch in range(num_epochs):
# 初始化参数
train_l_sum, n = 0.0, 0
# 初始化时间
start = time.time()
# 模型改为训练状态,如果使用了 dropout, batchnorm 之类的层时,训练状态和评估状态的表现会有巨大差别
model.train()
# 对训练数据集的每个 batch 执行
for x, y in train_iter:
# 如果使用了 GPU 则拷贝进显存
if (use_gpu):
x,y = x.cuda(),y.cuda()
# 计算 y_hat
y_hat = model(x)
# 计算损失
l = loss(y_hat, y).mean()
# 梯度清零
optimizer.zero_grad()
# L1 正则化
# for param in params:
# l += torch.sum(torch.abs(param))
# L2 正则化可以在 optimizer 上加入 weight_decay 的方式加入
# 求好对应的梯度
l.backward()
# 执行一次反向传播
optimizer.step()
# 对 loss 求和(在下面打印出来)
train_l_sum += l.item() * y.shape[0]
# 计数一共有多少个元素
n += y.shape[0]
# 模型开启预测状态
model.eval()
# 同样的,我们可以计算验证集上的 loss
valid_l_sum, valid_n = 0, 0
for x, y in valid_iter:
# 如果使用了 GPU 则拷贝进显存
if (use_gpu):
x,y = x.cuda(),y.cuda()
# 计算 y_hat
y_hat = model(x)
# 计算损失
l = loss(y_hat, y).mean()
# 对 loss 求和(在下面打印出来)
valid_l_sum += l.item() * y.shape[0]
# 计数一共有多少个元素
valid_n += y.shape[0]
# 对验证集合求指标
# 这里训练集其实可以在循环内高效地直接算出,这里为了代码的可读性牺牲了效率
train_mae, train_rmse = evaluate_accuracy(train_iter, model)
valid_mae, valid_rmse = evaluate_accuracy(valid_iter, model)
print('epoch %d, train loss %.4f, valid loss %.4f, train mae,rmse %.3f,%.3f, valid mae,rmse %.3f,%.3f, time %.2f sec'
% (epoch + 1, train_l_sum / n, valid_l_sum / valid_n, train_mae, train_rmse, valid_mae, valid_rmse, time.time(<