基于MLP算法实现交通流量预测

在浩如烟海的城市数据中,交通流量信息无疑是揭示城市运行脉络、洞察出行规律的关键要素之一。实时且精准的交通流量预测不仅能为交通规划者提供科学决策依据,助力提升道路使用效率、缓解交通拥堵,还能为公众出行提供参考,实现个性化导航服务,进而提升整个城市的运行效能。然而,交通流量受到众多因素的交织影响,如天气变化、特殊事件、节假日效应、时段特性等,其动态变化规律呈现出显著的非线性、时变性和不确定性,这无疑给预测工作带来了巨大挑战。

在此背景下,机器学习技术,尤其是深度学习方法,凭借其强大的模型构建能力和对复杂非线性关系的出色捕捉能力,逐渐崭露头角,成为交通流量预测领域的研究热点。其中,多层感知器(MLP)作为一类基础而经典的前馈型人工神经网络,以其简洁的结构、灵活的适应性和良好的泛化性能,在处理高维、非线性问题上展现出独特优势。 MLP通过模拟人脑神经元的工作机制,通过多层非线性变换对输入数据进行深度抽象和特征学习,能够有效挖掘交通流量数据背后的复杂关联与潜在模式,从而实现对未来的流量状态进行精准预测。

模型简介

多层感知器(Multi-Layer Perceptron, MLP)是一种基础且广泛使用的前馈型人工神经网络模型,它是深度学习领域的重要组成部分,尤其在处理非线性关系和模式识别问题中表现出强大的能力。MLP的核心特征在于其多层结构,由多个神经元组成的隐藏层与输入层和输出层相互连接,形成一个分层信息处理系统。下面是对MLP的详细介绍:

基本结构与组件:

  1. 输入层(Input Layer):接收原始数据作为网络的输入。这些数据通常是经过预处理后的数值向量,代表了待解决问题中的各种特征或变量。

  2. 隐藏层(Hidden Layers):位于输入层与输出层之间的中间层。MLP可以有一个或多个隐藏层,每个隐藏层包含多个神经元。隐藏层的主要功能是通过非线性变换对输入数据进行复杂的特征提取和表示学习,从而捕获数据中的潜在关系和模式。神经元之间通常是全连接的,即每个隐藏层神经元接收到前一层所有神经元的加权输出。

  3. 输出层(Output Layer):产生网络的最终输出,根据任务需求可以是一维或多维的。在交通流量预测等回归任务中,输出层通常只有一个神经元,给出连续的预测值;而在分类任务中,输出层神经元数量对应类别数,每个神经元的激活值代表对应类别的概率。

工作原理:

  1. 加权求和与激活函数:每个神经元接收前一层所有神经元的输出(或输入层的原始数据),对这些输入进行加权求和,加上一个偏置项后,通过一个非线性激活函数进行转换。常见的激活函数包括Sigmoid、Tanh、ReLU及其变种等。激活函数引入非线性,使得网络能够表达复杂的非线性关系。

  2. 前向传播(Forward Propagation):信息从输入层依次经过隐藏层直至输出层的过程称为前向传播。在这个过程中,数据通过各层神经元的加权求和和激活函数计算,逐步形成更高级的特征表示,最终在输出层生成预测结果。

  3. 反向传播(Backpropagation):当网络进行预测后,会计算预测结果与实际标签之间的误差(损失函数)。反向传播算法根据这个误差,从输出层开始,逐层反向调整各层神经元的权重和偏置,以最小化总体误差。这是通过链式法则计算梯度并应用梯度下降或其变种算法实现的。反向传播确保了网络在训练过程中能够自我学习并逐步改善其预测性能。

训练与应用:

  1. 训练过程:给定标记好的训练数据集,MLP通过迭代执行前向传播和反向传播,更新模型参数,直到达到预设的停止条件(如达到一定迭代次数、损失函数收敛或验证集性能不再提升等)。训练过程中可能涉及正则化、批量归一化、dropout等技术防止过拟合。

  2. 应用领域:MLP因其灵活性和普适性,被广泛应用于各种领域,如图像识别、语音识别、自然语言处理、时间序列预测(如交通流量预测)、金融风险评估、生物医学信号分析等。在交通流量预测中,MLP可以接收历史流量数据、气象信息、节假日标志等多元输入,学习并建模这些因素与未来流量之间的复杂非线性关系,从而做出准确预测。

综上所述,多层感知器(MLP)作为一种基础的前馈神经网络模型,通过多层非线性变换对输入数据进行抽象和学习,适用于各种非线性预测和分类任务。其训练过程依赖于反向传播算法来优化模型参数,使其能够捕捉数据中的复杂模式,并在诸多实际应用场景中展现出强大的预测性能。在交通流量预测中,MLP能够整合多种影响因素,为交通管理者提供有价值的决策支持。

模型构建

class MLP(nn.Module):def __init__(self, input_size, hidden_sizes, output_size):super(MLP, self).__init__()self.input_size = input_sizeself.hidden_sizes = hidden_sizesself.output_size = output_sizelayers = []prev_size = input_sizefor hidden_size in hidden_sizes:layers.append(nn.Linear(prev_size, hidden_size))layers.append(nn.Sigmoid())prev_size = hidden_sizelayers.append(nn.Linear(prev_size, output_size))self.model = nn.Sequential(*layers)def forward(self, x):# [Batch, Input_len, Node] --> [Batch, Node, Input_len]x = x.permute(0, 2, 1)y = self.model(x)# [Batch, Node, Output_len] --> [Batch, Output_len, Node]y = y.permute(0, 2, 1)return y

其中,input_size代表输入层节点数目,hidden_sizes为隐含层的节点数目,如[24, 36, 24]则代表有3层隐含层,隐含层的节点数目分别为24、36、24,output_size则为输出层节点数目。

一个典型的MLP神经网络如下图所示:

在这里插入图片描述

在时间序列任务中,考虑到数据变化的趋势性,认为未来时间窗的特征和当前时刻的前置时间窗特征相关性较大。

在这里插入图片描述

在单变量的预测场景下,假设时间窗为5分钟,用最近12个时间窗的流量预测未来3个时间窗的流量,则可以将最近12个时间窗的流量作为输入特征,此时输入层的节点数目为12,而需要预测的未来的3个时间窗的流量作为输出特征,此时输出层的节点数目为3。

编写个主函数验证下网络shape变化是否符合预期:

if __name__ == '__main__':parser = argparse.ArgumentParser()parser.add_argument('--window_size', type=int, default=12)parser.add_argument('--horizon', type=int, default=3)parser.add_argument('--hidden_sizes', type=list, default=[24, 36, 24])args = parser.parse_args()model = DNN(input_size=args.window_size, hidden_sizes=args.hidden_sizes, output_size=args.horizon)print(model)x = torch.randn(8, 12, 96)y = model(x)print(y.shape)

输出为:

MLP((model): Sequential((0): Linear(in_features=12, out_features=24, bias=True)(1): Sigmoid()(2): Linear(in_features=24, out_features=36, bias=True)(3): Sigmoid()(4): Linear(in_features=36, out_features=24, bias=True)(5): Sigmoid()(6): Linear(in_features=24, out_features=3, bias=True))
)
torch.Size([8, 3, 96])

输入张量的shape为[batch_size, input_len, node],node代表不同的序列,如果是交通流量预测,代表的是不同路段或交叉口。

输出张量的shape为[batch_size, output_len, node]。

用最近12个时间窗的流量预测未来3个时间窗的流量,输入张量的shape为[8, 12, 96],输出张量的shape为[8, 3, 96],符合预期。

那如果是多变量的预测场景呢?

其实,也很简单,假设时间窗为5分钟,用最近12个时间窗的流量、速度、占有率来预测未来3个时间窗的流量,此时输入特征为最近12个时间窗的流量、速度、占有率,有12*3=36个特征,故输入层的节点数目为36,由于输出还是流量这个单变量,所以输出层的节点数目还是3。

数据输入

本文以PEMS数据集作为算法的训练、验证和测试数据。

PEMS数据集是针对加利福尼亚州不同区域高速公路网络收集的交通数据,数据集可能包含多个传感器站点的数据,每个站点每五分钟记录了特定路段或路口的交通状况,包括但不限于:

  • 流量

单位时间内通过某路段或交叉口的车辆数量,反映道路的使用程度。

  • 速度

车辆在道路上行驶的平均速度,用于评估道路的运行效率和拥堵状况。

  • 占有率

车道被车辆占用的比例,是衡量道路拥挤程度的另一个重要指标。

PEMS03数据,26208*358*3,358个检测器,26208个5分钟时间窗(2012/5/1开始,91天),3个变量分别为流量、速度和占有率。PEMS04数据,16992*307*3,307个检测器,16992个5分钟时间窗(2017/7/1开始,59天),3个变量分别为流量、速度和占有率。PEMS07数据,28224*883*3,883个检测器,28224个5分钟时间窗(2017/5/1开始,98天),3个变量分别为流量、速度和占有率。PEMS08数据,17856*170*3,170个检测器,17856个5分钟时间窗(2012/3/1开始,62天),3个变量分别为流量、速度和占有率。

读取数据后,可以将数据传入自定义的DataSet,以方便后续的训练:

class ForecastDataset(torch_data.Dataset):def __init__(self, df, window_size, horizon, normalize_method=None, norm_statistic=None, interval=1):self.window_size = window_size # 12self.interval = interval  #1self.horizon = horizonself.normalize_method = normalize_methodself.norm_statistic = norm_statisticdf = pd.DataFrame(df)df = df.fillna(method='ffill', limit=len(df)).fillna(method='bfill', limit=len(df)).valuesself.data = dfself.df_length = len(df)self.x_end_idx = self.get_x_end_idx()if normalize_method:self.data, _ = normalized(self.data, normalize_method, norm_statistic)def __getitem__(self, index):hi = self.x_end_idx[index] #12lo = hi - self.window_size #0train_data = self.data[lo: hi] #0:12target_data = self.data[hi:hi + self.horizon] #12:24x = torch.from_numpy(train_data).type(torch.float)y = torch.from_numpy(target_data).type(torch.float)return x, ydef __len__(self):return len(self.x_end_idx)def get_x_end_idx(self):# each element `hi` in `x_index_set` is an upper bound for get training data# training data range: [lo, hi), lo = hi - window_sizex_index_set = range(self.window_size, self.df_length - self.horizon + 1)x_end_idx = [x_index_set[j * self.interval] for j in range((len(x_index_set)) // self.interval)]return x_end_idxdef normalized(data, normalize_method, norm_statistic=None):if normalize_method == 'min_max':if not norm_statistic:norm_statistic = dict(max=np.max(data, axis=0), min=np.min(data, axis=0))scale = norm_statistic['max'] - norm_statistic['min'] + 1e-5data = (data - norm_statistic['min']) / scaledata = np.clip(data, 0.0, 1.0)elif normalize_method == 'z_score':if not norm_statistic:norm_statistic = dict(mean=np.mean(data, axis=0), std=np.std(data, axis=0))mean = norm_statistic['mean']std = norm_statistic['std']std = [1 if i == 0 else i for i in std]data = (data - mean) / stdnorm_statistic['std'] = stdreturn data, norm_statisticdef de_normalized(data, normalize_method, norm_statistic):if normalize_method == 'min_max':if not norm_statistic:norm_statistic = dict(max=np.max(data, axis=0), min=np.min(data, axis=0))scale = norm_statistic['max'] - norm_statistic['min'] + 1e-8data = data * scale + norm_statistic['min']elif normalize_method == 'z_score':if not norm_statistic:norm_statistic = dict(mean=np.mean(data, axis=0), std=np.std(data, axis=0))mean = norm_statistic['mean']std = norm_statistic['std']std = [1 if i == 0 else i for i in std]data = data * std + meanreturn data

上述代码定义了一个名为ForecastDataset的类,它是基于torch_data.Dataset的子类,用于处理时间序列预测任务的数据集。同时,还提供了normalizedde_normalized两个函数,分别用于数据的标准化(归一化)和反标准化。

ForecastDataset类

  1. 初始化方法__init__

    • 输入参数:
      • df:原始数据。
      • window_size:滑动窗口大小,用于截取历史数据作为模型训练的输入。
      • horizon:预测步长,即模型需要预测未来多少个时间点的数据。
      • normalize_method:数据标准化方法,可选值为'min_max'(最小最大值归一化)或'z_score'(Z-score标准化)。
      • norm_statistic:若已知数据的统计信息(如最大值、最小值、均值、标准差),可直接传入;否则,将根据数据计算这些统计量。
      • interval:采样间隔。
    • 方法内部:
      • 将输入的DataFrame填充缺失值,并转化为NumPy数组。
      • 初始化类的属性:滑动窗口大小、采样间隔、预测步长、标准化方法、统计信息等。
      • 计算数据集长度和训练数据结束索引(x_end_idx),用于后续按索引获取训练数据和目标数据。
      • 如果指定了标准化方法,则对数据进行标准化处理,同时更新统计信息。
  2. __getitem__方法:

    • 输入参数:index,表示数据集中第index个样本的索引。
    • 方法内部:
      • 根据x_end_idx列表计算当前样本的训练数据起始索引lo和结束索引hi
      • 提取训练数据(历史数据)和目标数据(未来数据)。
      • 将提取到的训练数据和目标数据转化为PyTorch张量并设置为浮点类型。
      • 返回包含训练数据和目标数据的元组。
  3. __len__方法:

    • 返回数据集的样本数,即x_end_idx列表的长度。
  4. get_x_end_idx方法:

    • 该方法用于生成一个列表,其中每个元素hi表示一个训练数据结束索引。
    • 计算x_index_set,包含所有可能的训练数据结束索引(满足window_sizedf_length - horizon + 1的范围)。
    • 根据采样间隔intervalx_index_set中选取训练数据结束索引,组成x_end_idx列表并返回。

辅助函数

  1. normalized函数:

    • 输入参数:待标准化数据data、标准化方法normalize_method及统计信息字典norm_statistic(可选)。
    • 函数内部:
      • 根据指定的标准化方法进行数据标准化:
        • 若为'min_max'
          • 若未提供统计信息,计算数据的最大值和最小值,然后进行最小最大值归一化。
          • 确保数据在[0, 1]范围内。
        • 若为'z_score'
          • 若未提供统计信息,计算数据的均值和标准差,然后进行Z-score标准化。
          • 避免除以零错误,当标准差为零时将其置为1。
      • 返回标准化后的数据及更新后的统计信息字典。
  2. de_normalized函数:

    • 输入参数:已标准化数据data、标准化方法normalize_method及统计信息字典norm_statistic
    • 函数内部:
      • 根据指定的标准化方法进行数据反标准化:
        • 若为'min_max'
          • 使用提供的统计信息(最大值、最小值)进行反归一化。
        • 若为'z_score'
          • 使用提供的统计信息(均值、标准差)进行反Z-score标准化。
          • 避免除以零错误,当标准差为零时将其置为1。
      • 返回反标准化后的数据。

综上所述,上述代码实现了一个用于时间序列预测任务的数据集类ForecastDataset,支持滑动窗口、预测步长、数据标准化等功能,并提供了数据标准化与反标准化的辅助函数。

代码中,window_size即为前置时间窗的个数,horizon则为预测时间窗的个数,interval为采样间隔,默认为1。

比如现在我们有100个时间窗的历史数据,window_size为12,horizon为12,interval为1。

则第1个样本为:

x: 第1到第12个时间窗
y: 第13到第24个时间窗

第2个样本为:

x: 第2到第13个时间窗
y: 第14到第25个时间窗

以此类推,总共可构建出(his_num-window_size-horizon+1) // interval=(100-12-12+1)//1=77个样本。

同样的,若interval为2,则第1个样本为:

x: 第1到第12个时间窗
y: 第13到第24个时间窗

第2个样本为:

x: 第3到第14个时间窗
y: 第15到第26个时间窗

其他样本以此类推,可以看出,所谓的interval其实就是相邻样本之间的时间窗个数。

模型训练

为了适配不同的模型和数据集,我们采用类继承的方式来编写模型训练代码。

首先,我们定义一个父类。

class Exp_Basic(object):def __init__(self, args):self.args = argsself.device = self._acquire_device()self.model = self._build_model()def _acquire_device(self):if self.args.use_gpu:os.environ["CUDA_VISIBLE_DEVICES"] = str(self.args.gpu) if not self.args.use_multi_gpu else self.args.devicesdevice = torch.device('cuda:{}'.format(self.args.gpu))print('Use GPU: cuda:{}'.format(self.args.gpu))else:device = torch.device('cpu')print('Use CPU')return devicedef _get_data(self):pass# 创建模型def _build_model(self):raise NotImplementedErrorreturn Nonedef train(self):passdef valid(self):passdef test(self):pass

接着,编写具体的实现类Exp_MLP_PEMS(Exp_Basic)。

数据读取

首先实现_get_data方法来获取数据:

def _get_data(self):data_file = os.path.join('../../data/PEMS', self.args.dataset, self.args.dataset+'.npz')print('data file:',data_file)data = np.load(data_file,allow_pickle=True)data = data['data'][:, :, 0]train_ratio = self.args.train_length / (self.args.train_length + self.args.valid_length + self.args.test_length)valid_ratio = self.args.valid_length / (self.args.train_length + self.args.valid_length + self.args.test_length)train_data = data[:int(train_ratio * len(data))]valid_data = data[int(train_ratio * len(data)):int((train_ratio + valid_ratio) * len(data))]test_data = data[int((train_ratio + valid_ratio) * len(data)):]if len(train_data) == 0:raise Exception('Cannot organize enough training data')if len(valid_data) == 0:raise Exception('Cannot organize enough validation data')if len(test_data) == 0:raise Exception('Cannot organize enough test data')if self.args.normtype == 0:train_mean = np.mean(train_data, axis=0)train_std = np.std(train_data, axis=0)train_normalize_statistic = {"mean": train_mean.tolist(), "std": train_std.tolist()}val_mean = np.mean(valid_data, axis=0)val_std = np.std(valid_data, axis=0)val_normalize_statistic = {"mean": val_mean.tolist(), "std": val_std.tolist()}test_mean = np.mean(test_data, axis=0)test_std = np.std(test_data, axis=0)test_normalize_statistic = {"mean": test_mean.tolist(), "std": test_std.tolist()}elif self.args.normtype == 1:data_mean = np.mean(data, axis=0)data_std = np.std(data, axis=0)train_normalize_statistic = {"mean": data_mean.tolist(), "std": data_std.tolist()}val_normalize_statistic = {"mean": data_mean.tolist(), "std": data_std.tolist()}test_normalize_statistic = {"mean": data_mean.tolist(), "std": data_std.tolist()}else:train_mean = np.mean(train_data, axis=0)train_std = np.std(train_data, axis=0)train_normalize_statistic = {"mean": train_mean.tolist(), "std": train_std.tolist()}val_normalize_statistic = {"mean": train_mean.tolist(), "std": train_std.tolist()}test_normalize_statistic = {"mean": train_mean.tolist(), "std": train_std.tolist()}train_set = ForecastDataset(train_data, window_size=self.args.window_size, horizon=self.args.horizon,normalize_method=self.args.norm_method, norm_statistic=train_normalize_statistic)valid_set = ForecastDataset(valid_data, window_size=self.args.window_size, horizon=self.args.horizon,normalize_method=self.args.norm_method, norm_statistic=val_normalize_statistic)test_set = ForecastDataset(test_data, window_size=self.args.window_size, horizon=self.args.horizon,normalize_method=self.args.norm_method, norm_statistic=test_normalize_statistic)train_loader = DataLoader(train_set, batch_size=self.args.batch_size, drop_last=False, shuffle=True,num_workers=1)valid_loader = DataLoader(valid_set, batch_size=self.args.batch_size, shuffle=False, num_workers=1)test_loader = DataLoader(test_set, batch_size=self.args.batch_size, shuffle=False, num_workers=1)node_cnt = train_data.shape[1]return test_loader, train_loader, valid_loader,node_cnt,test_normalize_statistic,val_normalize_statistic

上述Python代码定义了一个名为_get_data的方法,其目的是根据传入的参数配置来读取PEMS 03/04/07/08数据集中的流量数据,进行预处理(如划分训练集、验证集、测试集,标准化),并创建相应的DataLoader对象。下面是代码的详细解读:

  1. 读取数据

    • 使用os.path.join()函数根据传入的args.dataset参数拼接数据文件路径,数据文件位于../../data/PEMS/{dataset}/{dataset}.npz目录下。
    • 使用np.load()函数加载.npz文件,其中数据以字典形式存储,键为'data'。加载后的数据是一个三维数组,第三维代表不同特征(流量、速度、占有率),这里取第一个特征(流量)作为建模数据。
    • 数据现在是一个形状为(len, node)的二维数组,其中len表示时间维度上的观测点数,node表示不同节点(如道路、路口)的数量。
  2. 划分数据集

    • 根据传入的train_lengthvalid_lengthtest_length参数,按照时间维度将数据划分为训练集、验证集和测试集。
    • 计算训练集、验证集、测试集在时间轴上的起始和结束索引,并分别切分数据。
    • 如果划分后任一数据集的长度为0,抛出异常,表示无法组织足够的数据。
  3. 数据标准化

    • 根据normtype参数选择不同的标准化方法:
      • normtype=0:分别计算训练集、验证集、测试集的均值和标准差,进行独立标准化。
      • normtype=1:使用整个数据集(训练集、验证集、测试集组合)的均值和标准差,对所有数据进行统一标准化。
      • normtype=2:仅使用训练集的均值和标准差,对训练集、验证集、测试集进行标准化。
    • 计算所需统计量(均值和标准差),并将结果存储为字典格式,如{"mean": [mean1, mean2, ...], "std": [std1, std2, ...]}
  4. 创建并返回数据集和DataLoader对象

    • 使用ForecastDataset类(未在提供的代码中定义)创建训练集、验证集、测试集对象。传入原始数据、窗口大小(window_size)、预测时域(horizon)、标准化方法(norm_method)和对应的标准化统计量。
    • 为每个数据集对象创建一个DataLoader,设置批次大小(batch_size)、是否丢弃最后一小批(drop_last)、是否打乱数据(shuffle)、并行处理的worker数(num_workers)等参数。
    • 返回测试集、训练集、验证集的DataLoader对象,以及节点数量(node_cnt)和测试集、验证集的标准化统计量。

总结:该方法完成了PEMS数据集的读取、划分、标准化,并为训练、验证、测试准备了相应的DataLoader对象,为后续模型训练和评估提供了数据支持。

构建模型

def _build_model(self):model = MLP(input_size=self.args.window_size, hidden_sizes=self.args.hidden_sizes, output_size=self.args.horizon)print(model)return model

训练

def train(self):my_optim = self._select_optimizer()my_lr_scheduler = torch.optim.lr_scheduler.ExponentialLR(optimizer=my_optim, gamma=self.args.decay_rate)test_loader, train_loader, valid_loader, node_cnt, test_normalize_statistic, val_normalize_statistic = self._get_data()forecast_loss = nn.L1Loss()best_validate_mae = np.infbest_test_mae = np.infvalidate_score_non_decrease_count = 0if self.args.resume:self.model, lr, epoch_start = load_model(self.model, self.result_file, model_name=self.args.dataset,horizon=self.args.horizon)else:epoch_start = 0for epoch in range(epoch_start, self.args.epoch):lr = adjust_learning_rate(my_optim, epoch, self.args)epoch_start_time = time.time()self.model.train()loss_total = 0cnt = 0for i, (inputs, target) in enumerate(train_loader):inputs = inputstarget = targetself.model.zero_grad()forecast = self.model(inputs)loss = forecast_loss(forecast, target)cnt += 1loss.backward()my_optim.step()loss_total += float(loss)print('| end of epoch {:3d} | time: {:5.2f}s | train_total_loss {:5.4f} '.format(epoch, (time.time() - epoch_start_time), loss_total / cnt))if (epoch + 1) % self.args.exponential_decay_step == 0:my_lr_scheduler.step()if (epoch + 1) % self.args.validate_freq == 0:is_best_for_now = Falseprint('------ validate on data: VALIDATE ------')valid_metrics = self.validate(self.model, valid_loader, self.args.norm_method,val_normalize_statistic,self.args.window_size, self.args.horizon,test=False)test_metrics = self.validate(self.model, test_loader, self.args.norm_method,test_normalize_statistic,self.args.window_size, self.args.horizon,test=True)if best_validate_mae > valid_metrics['mape']:best_validate_mae = valid_metrics['mape']is_best_for_now = Truevalidate_score_non_decrease_count = 0print('got best validation result:', valid_metrics, test_metrics)else:validate_score_non_decrease_count += 1if best_test_mae > test_metrics['mape']:best_test_mae = test_metrics['mape']print('got best test result:', test_metrics)# save modelif is_best_for_now:save_model(epoch, lr, model=self.model, model_dir=self.result_file, model_name=self.args.dataset,horizon=self.args.horizon)print('saved model!')# early stopif self.args.early_stop and validate_score_non_decrease_count >= self.args.early_stop_step:break

上述代码定义了一个名为train的方法,用于训练一个给定的模型。

  1. 初始化变量与加载模型:

    • 调用_select_optimizer方法选择优化器(optimizer)。
    • 创建指数衰减学习率调度器(learning rate scheduler),使用ExponentialLR类,指定优化器和衰减率(decay rate)。
    • 调用_get_data方法获取训练、验证、测试数据加载器、节点数量、测试数据与验证数据的标准化统计信息。
    • 定义损失函数(loss function)为L1损失(nn.L1Loss)。
    • 设置最佳验证MAE(Mean Absolute Error)和最佳测试MAE初始值为正无穷大。
    • 初始化验证分数非下降计数器(validate score non-decrease count)为0。
  2. 检查是否继续之前训练:

    • 如果args.resume参数为真(即继续之前训练),则加载模型、学习率(lr)和开始的训练轮数(epoch_start)。
    • 否则,设置epoch_start为0,从头开始训练。
  3. 主训练循环:

    • 对于每个训练轮(epoch),从epoch_startargs.epoch
      • 调整学习率(adjust_learning_rate函数)。
      • 记录当前轮开始时间。
      • 将模型设置为训练模式。
      • 初始化累计训练损失(loss_total)和样本计数器(cnt)为0。
      • 遍历训练数据加载器中的样本(enumerate(train_loader)):
        • 输入(inputs)和目标(target)保持不变。
        • 清除模型的梯度。
        • 使用模型对输入进行预测(forecast)。
        • 计算预测与目标之间的损失(loss)。
        • 更新样本计数器和累计训练损失。
        • 反向传播损失并更新模型参数。
      • 打印本训练轮的训练耗时和平均损失。
  4. 学习率调整与验证:

    • 如果当前训练轮数(epoch+1)能被指数衰减步长整除,执行学习率调度器的step方法,降低学习率。
    • 如果当前训练轮数能被验证频率整除,进行验证过程:
      • 验证模型在验证集上的表现,调用validate方法,传入模型、验证数据加载器、标准化方法、验证数据的统计信息、窗口大小、预测步长等参数,并设置test=False
      • 同样地,验证模型在测试集上的表现,此时设置test=True
      • 检查当前验证MAPE(Mean Absolute Percentage Error)是否优于历史最佳验证MAPE:
        • 如果是,则更新最佳验证MAPE、重置验证分数非下降计数器,并记录当前验证和测试结果。
      • 检查当前测试MAPE是否优于历史最佳测试MAPE,如果是,则更新最佳测试MAPE。
      • 若当前验证结果为最佳,保存模型(save_model函数),并打印提示信息。
  5. 早停条件判断:

    • 如果启用了早停(args.early_stop为真)且连续args.early_stop_step个验证周期内验证分数未下降,则跳出训练循环。

对应的_select_optimizer方法为:

def _select_optimizer(self):if self.args.optimizer == 'RMSProp':my_optim = torch.optim.RMSprop(params=self.model.parameters(), lr=self.args.lr, eps=1e-08)else:my_optim = torch.optim.Adam(params=self.model.parameters(), lr=self.args.lr, betas=(0.9, 0.999),weight_decay=self.args.weight_decay)return my_optim

验证

def validate(self, model, dataloader, normalize_method, statistic,window_size, horizon, test=False):if test:print("===================Test Normal=========================")else:print("===================Validate Normal=========================")forecast_norm, target_norm, input_norm = self.inference(model, dataloader, window_size, horizon)if normalize_method and statistic:forecast = de_normalized(forecast_norm, normalize_method, statistic)target = de_normalized(target_norm, normalize_method, statistic)else:forecast, target, input = forecast_norm, target_norm, input_normscore = evaluate(target, forecast)score_final_detail = evaluate(target, forecast, by_step=True)print('by each step: MAPE & MAE & RMSE', score_final_detail)if test:print(f'TEST: RAW : MAE {score[1]:7.2f};MAPE {score[0]:7.2f}; RMSE {score[2]:7.2f}.')else:print(f'VAL: RAW : MAE {score[1]:7.2f};MAPE {score[0]:7.2f}; RMSE {score[2]:7.2f}.')return dict(mae=score[1], mape=score[0], rmse=score[2])

上述代码定义了一个名为validate的方法,用于评估模型在给定数据集上的预测性能。

  1. 判断验证或测试模式:

    • 根据test参数的值(True或False)输出不同的提示信息,表明正在进行的是测试(Test)还是验证(Validate)。
  2. 模型推理与数据标准化恢复:

    • 调用inference方法,传入模型、数据加载器、窗口大小、预测步长,得到模型对数据集的预测结果(forecast_norm)、真实目标值(target_norm)和输入数据(input_norm)。
    • 判断是否进行了数据标准化:
      • 如果指定了标准化方法(normalize_method)且提供了统计信息(statistic):
        • 使用de_normalized函数对预测结果和真实目标值进行反标准化,恢复到原始数值范围。
      • 否则,直接使用标准化后的预测结果、真实目标值和输入数据。
  3. 计算评估指标:

    • 调用evaluate函数,传入真实目标值和模型预测结果,计算各项评估指标(MAE、MAPE、RMSE)。
  4. 输出详细评估结果:

    • 调用evaluate函数,传入额外参数by_step=True,得到按预测步长分解的各项评估指标。
    • 输出按预测步长分解的评估指标(MAPE、MAE、RMSE)。
  5. 打印总体评估结果:

    • 根据test参数的值,打印相应的测试或验证结果标签。
    • 输出总体的MAE、MAPE、RMSE值,保留两位小数。
  6. 返回评估指标字典:

    • 将计算得到的MAE、MAPE、RMSE值封装到一个字典中,以maemapermse为键,对应值为值,返回该字典。

综上,validate方法通过模型推理、数据标准化恢复、计算评估指标、输出结果等步骤,对模型在给定数据集上的预测性能进行评估,并返回评估指标的字典。根据test参数的不同,该方法可用于模型的验证或测试阶段。

测试

def test(self):test_loader, train_loader, valid_loader, node_cnt, test_normalize_statistic, val_normalize_statistic = self._get_data()model, lr, epoch = load_model(self.model, self.result_file, model_name=self.args.dataset, horizon=self.args.horizon)return self.validate(model, test_loader, self.args.norm_method, test_normalize_statistic,self.args.window_size, self.args.horizon, test=True)

实时推理

def inference(self, model, dataloader, window_size, horizon):forecast_set = []target_set = []input_set = []self.model.eval()with torch.no_grad():for i, (inputs, target) in enumerate(dataloader):inputs = inputstarget = targetinput_set.append(inputs.detach().cpu().numpy())step = 0forecast_steps = np.zeros([inputs.size()[0], horizon, inputs.size()[2]], dtype=np.float64)# 适配迭代预测和非迭代预测while step < horizon:forecast_result = model(inputs)len_model_output = forecast_result.size()[1]if len_model_output == 0:raise Exception('Get blank inference result')inputs[:, :window_size - len_model_output, :] = inputs[:, len_model_output:window_size,:].clone()inputs[:, window_size - len_model_output:, :] = forecast_result.clone()forecast_steps[:, step:min(horizon - step, len_model_output) + step, :] = \forecast_result[:, :min(horizon - step, len_model_output), :].detach().cpu().numpy()step += min(horizon - step, len_model_output)forecast_set.append(forecast_steps)target_set.append(target.detach().cpu().numpy())return np.concatenate(forecast_set, axis=0), np.concatenate(target_set, axis=0), np.concatenate(input_set,axis=0)

上述代码定义了一个名为inference的方法,用于执行模型在给定数据集上的推理(预测)。

  1. 初始化变量:

    • 创建三个空列表forecast_settarget_setinput_set,分别用于存储模型预测结果、真实目标值和输入数据。
  2. 设置模型为评估模式并禁用梯度计算:

    • 将模型设为评估模式(self.model.eval()),避免在推理过程中进行不必要的前向传播计算。
    • 使用with torch.no_grad():语句块,确保在该块内的计算不记录梯度,节省内存并提高推理速度。
  3. 遍历数据加载器中的样本:

    • 使用enumerate(dataloader)遍历数据加载器中的每个样本,包括输入数据(inputs)和目标值(target)。
    • 将当前样本的输入数据和目标值分别添加到input_settarget_set列表中。
  4. 进行模型推理:

    • 初始化变量step为0,用于记录当前已预测的时间步数。
    • 初始化forecast_steps数组,用于存储当前样本的所有预测结果,形状为(batch_size, horizon, input_feature_dim),其中batch_size为样本批次大小,horizon为预测步长,input_feature_dim为输入特征维度。
    • 迭代预测逻辑:
      • 使用模型对当前输入数据进行预测,得到forecast_result
      • 检查模型输出长度(len_model_output),若为0则抛出异常。
      • 更新输入数据(inputs):将较旧的历史数据移至左侧,并将最新预测结果移至右侧,以准备下一轮预测。
      • 将模型当前输出的预测结果按需填充到forecast_steps数组中,确保每个样本的预测结果按时间步正确排列。
      • 更新step变量,累加已预测的时间步数。
  5. 处理完所有时间步后,将当前样本的预测结果、真实目标值和输入数据添加到相应列表中。

  6. 合并所有样本的预测结果、真实目标值和输入数据:

    • 使用np.concatenate函数,将forecast_settarget_setinput_set列表中的数据沿第一个维度(样本维度)拼接成一个完整的数组。
  7. 返回结果:

    • 返回合并后的预测结果数组、真实目标值数组和输入数据数组。

综上,inference方法通过遍历数据加载器、执行模型推理并收集预测结果、真实目标值和输入数据,最终返回这些数据的完整数组。该方法支持迭代预测(递归使用模型输出作为下一轮输入的一部分)和非迭代预测(单次模型预测即可得到全部结果),适用于不同类型的预测模型。

模型效果

最后,将我们构建好的MLP网络在PEMS数据集进行了准确性测试,结果如下:

数据集MAEMAPERMSE
PEMS0319.72260.18440331.5506
PEMS0427.24220.16668742.5976
PEMS0728.80020.12248444.1366
PEMS0820.85040.12366832.5307

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/1716.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

vue-manage-system 版本更新,让开发更加简单

vue-manage-system 近期进行了一次版本升级&#xff0c;主要是支持了更多功能、升级依赖版本和优化样式&#xff0c;并且上线了官方文档网站&#xff0c;大部分功能都有文档或者使用示例&#xff0c;更加适合新手上手开发&#xff0c;只需要根据实际业务简单修改&#xff0c;就…

QA测试开发工程师面试题满分问答15: 讲一讲InnoDB和MyISAM

InnoDB和MyISAM是MySQL中两种常见的存储引擎&#xff0c;它们在数据存储和处理方面有着显著的区别。让我们逐一来看一下它们的区别、原理以及适用场景。 区别&#xff1a; 事务支持&#xff1a;InnoDB是一个支持事务的存储引擎&#xff0c;而MyISAM不支持事务。事务是一种用于维…

mysql 重复单号 统计

任务&#xff1a; 增加重复件统计分析&#xff1a; 统计展示选择时间范围内重复1次、重复2次、重复3次、重复4次、重复5次及以上的数据量 17、统计出现的重复次数 增加重复件统计分析&#xff1a; 统计展示选择时间范围内重复1次、重复2次、重复3次、重复4次、重复5次及以上的数…

【机器学习】各大模型原理简介

目录 ⛳️推荐 前言 一、神经网络&#xff08;联结主义&#xff09;类的模型 二、符号主义类的模型 三、决策树类的模型 四、概率类的模型 五、近邻类的模型 六、集成学习类的模型 ⛳️推荐 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风…

uniapp H5的弹窗滚动穿透解决

目录 方案一 事件修饰符 overscroll-behavior 修饰符 overscroll-behavior 属性 看个案例 兼容 方案二 overflow&#xff1a;hiden 有一层遮罩蒙层覆盖在body上时&#xff0c;当我们滚动遮罩层&#xff0c;它下面的内容也会跟着一起滚动&#xff0c;看起来好像是上面的…

冯唐成事心法笔记

文章目录 卷首语 管理是一生的日常&#xff0c;成事是一生的修行PART 1 知己 用好自己的天赋如何管理自我用好你的天赋成大事无捷径如何平衡工作和生活做一个真猛人做自己熟悉的行业掌控情绪如何对待妒忌和贪婪如何战胜自己&#xff0c;战胜逆境真正的高手都有破局思维有时候…

本地环境测试

1. 在 Anaconda Navigator 中&#xff0c;打开 Jupyter Notebook &#xff0c;在网页中&#xff0c;点击进入本地环境搭建中创 建的工作目录&#xff0c;点击右上角的 New- 》 Folder &#xff0c;将新出现的 Untitled Folder 选中&#xff0c;并使用左上角 的 Rename 按钮重…

白蚁自动化监测系统解放方案

一、系统介绍 白蚁自动化监测系统是基于物联网的各项白蚁监测点数据的采集形成智能控制系统。提供白蚁实时预警及解决方案&#xff0c;真正实现区域内白蚁种群消灭。白蚁入侵&#xff0c;系统第一时间自动报警&#xff0c;显示入侵位置&#xff0c;实现抓获白蚁于现场的关键环…

【Linux】认识文件(三):缓冲区

【Linux】认识文件&#xff08;三&#xff09;&#xff1a;缓冲区 一.啥是缓冲区&#xff1f;二.缓冲区现象三.缓冲区的刷新方法四.缓冲区在哪&#xff1f;五.为什么要有缓冲区 一.啥是缓冲区&#xff1f; 缓冲区&#xff0c;官方说法就是&#xff1a;指的是一块用于临时存储数…

LeetCode 409—— 最长回文串

阅读目录 1. 题目2. 解题思路3. 代码实现 1. 题目 2. 解题思路 要想组成回文串&#xff0c;那么只有最中间的字符可以是奇数个&#xff0c;其余字符都必须是偶数个。 所以&#xff0c;我们先遍历一遍字符串&#xff0c;统计出每个字符出现的次数。 然后如果某个字符出现了偶…

vi, vim,data,wc,系统常用命令-读书笔记(十)

vi 文本编辑器 基本上 vi 共分为三种模式&#xff0c;分别是“一般指令模式”、“编辑模式”与“命令行命令模式”。这三种模式的作用分别是&#xff1a; 一般指令模式&#xff08;command mode&#xff09;以 vi 打开一个文件就直接进入一般指令模式了&#xff08;这是默认的…

Compose 简单组件

文章目录 Compose 简单组件TextText属性使用AnnotatedStringSpanStyleParagraphStyle SelectionContainer 和 DisableSelectionClickableText TextFieldTextField属性使用OutlinedTextFieldBasicTextFieldKeyboardOptions 键盘属性KeyboardActions IME动作 ButtonButton属性使用…

玩转压力管理,轻松高效编程

程序员缓解工作压力的小窍门 在当今快速发展的科技时代&#xff0c;程序员作为数字世界的建筑师&#xff0c;面临着高强度、高压力的工作环境。为保持工作效率和创新能力&#xff0c;同时也确保身心健康和个人热情的持久续航&#xff0c;采取科学合理的减压策略至关重要。 方…

一二三应用开发平台使用手册——系统管理-用户组-使用说明

概述 在RBAC模型中&#xff0c;资源、角色、用户三个关键元素&#xff0c;构成权限体系。在平台设计和实现的时候&#xff0c;以下几个核心问题思考如下&#xff1a; 角色&#xff0c;单层平铺还是树形结构&#xff1f; 在小型应用中&#xff0c;角色数量有限的情况下&#x…

高级数据结构—树状数组

引入问题&#xff1a; 给出一个长度为n的数组&#xff0c;完成以下两种操作&#xff1a; 1. 将第i个数加上k 2. 输出区间[i,j]内每个数的和 朴素算法&#xff1a; 单点修改&#xff1a;O( 1 ) 区间查询&#xff1a;O( n ) 使用树状数组&#xff1a; 单点修改&#xff1a…

17-软件脉冲宽度调制(SW_PWM)

ESP32-S3的软件脉冲宽度调制&#xff08;SW_PWM&#xff09; 引言 ESP32-S3 LED 控制器LEDC 主要用于控制 LED&#xff0c;也可产生PWM信号用于其他设备的控制。该控制器有 8 路通道&#xff0c;可以产生独立的波形&#xff0c;驱动 RGB LED 等设备。LED PWM 控制器可在无需C…

CLion远程调试

一 CLion远程调试 ## 1.1 建立远程连接过程 设置——部署——“”——SFTP——新建服务器名称——输入主机、用户名、密码信息——确定 工具链建立远程主机 设置——工具链——“”——远程主机——凭据新增服务器信息 上传本地代码到服务器 ps:要保证本地文件完整&#…

测试人员一定要避免的这些不专业行为!

软件测试并非一个简单的任务&#xff0c;需要高度的专业性和责任感&#xff0c;本文将探讨一些常见的不专业行为&#xff0c;及其对软件开发过程和产品质量可能产生的负面影响。 1. 忽略细节 在测试过程中忽视细节&#xff0c;导致测试不彻底&#xff0c;漏洞未被发现。 2. …

从 Android 恢复已删除文件的 3 种简单方法

如何从 Android 恢复已删除的文件&#xff1f;毫不犹豫&#xff0c;有些人可能会认为从 Google 备份恢复 Android 文件太容易了。但是&#xff0c;如果删除的文件未同步到您的帐户或未备份怎么办&#xff1f;您错误的恢复可能会永久删除您想要的数据。因此&#xff0c;我们发布…