如何用pytorch尝试第一个预测模型
前言
记录一下自学pytorch构建预测模型的过程,希望尽量详细并且具有逻辑性。欢迎建议和指导。
一、lstm预测
lstm是在时间序列预测中广泛使用到的模型,通过对输入的序列数据进行特征传递,包含了预测点之前的特征信息,效果更好。
因此,我先尝试构建lstm+fc层的时序预测模型。
几个关键步骤:
1:将特征值和标签值提取出来
2:拆分训练集和测试集,将训练集的特征值和标签值进行归一化拟合,在利用训练的归一化函数转换测试集。而后将dataframe格式的数据处理为tensor
3:构建模型
4:训练模型参数
5:模型测试
6:绘图
二、使用步骤
1. 采用的数据
在实际应用中,多维特征是更常见的,因此,不同于单维预测,我尝试选择特征维度为9,标签维度为2的数据集进行预测。
2.引入库
代码如下(示例):
import numpy as np
import pandas as pd
from sklearn.preprocessing import MinMaxScaler
import math
import torch
import torch.nn as nn
import torch.nn.functional
import torch.utils.data as data
import matplotlib.pyplot as plt
3.读入数据
首先将数据中缺省值采用interpolate()进行填充,并计算目标值。这里的目标值是对下一个时刻(5分钟)进行预测
col_list1=['Lane 1 Flow (Veh/h)', 'Lane 2 Flow (Veh/h)','Lane 3 Flow (Veh/h)', 'Lane 4 Flow (Veh/h)']
col_list2=['Lane 1 Speed (km/h)','Lane 2 Speed (km/h)', 'Lane 3 Speed (km/h)', 'Lane 4 Speed (km/h)']
df=pd.read_csv(r"D:\学习\实验\CTS2023竞赛\CTS2023竞赛数据\train-5min\1.csv",usecols=col_list1+col_list2+['Week'])
df=df.replace(0,np.nan) #这句是因为有些数据是异常值0,我把它替换为空值,后面和空值一起做填充
df = df.interpolate() #将空值进行填充
df['target_veh']=df[col_list1].sum(axis=1).shift(-1)
df['target_speed']=(df['Lane 1 Flow (Veh/h)']*df['Lane 1 Speed (km/h)']+df['Lane 2 Flow (Veh/h)']*df['Lane 2 Speed (km/h)']+df['Lane 3 Flow (Veh/h)']*df['Lane 3 Speed (km/h)']+df['Lane 4 Flow (Veh/h)']*df['Lane 4 Speed (km/h)']).shift(-1)/df['target_veh']
由于最后一个没有目标值,因此我们将最后一行删除,我采用的方式是直接使用dropna函数,对数据集中的空白行进行删除。
df=df.dropna()
可以看到,数据行数和我们设想的一样,少了最后一行。
4.输入数据格式整理
首先切分一下训练集和测试集,接下来训练过程中都是使用训练集,包括归一化,保证测试集完全新鲜。由于归一化标签以后,还需要反归一化,为了避免格式问题,我设定了两个归一化器分别对X和y进行归一化。所以,数据划分的时候,也要将X和y划分出来。
这里使用的是iloc,如果使用的是loc是无法通过数字索引切分的。
train_len=math.ceil(len(df1)*0.8)
df_train_X,df_train_y=df1.iloc[:train_len,0:-2],df1.iloc[:train_len,-2:]
df_test_X,df_test_y=df1.iloc[train_len:,0:-2],df1.iloc[train_len:,-2:]
下一步就是将数据进行归一化处理,防止特征数据之间值的大小差异太大,导致对小数值特征不灵敏。
我采用的是sklearn.preprocessing 包里面的 MinMaxScaler。scalar_X就是对X数据进行归一化,scalar_y就是y数据进行归一化,后面方便对预测出来的y值进行反归一化。
注意这里对train的数据进行的是拟合后转换,而test数据则不进行拟合,直接转换。
scalar_X=MinMaxScaler()
data_train_X=scalar_X.fit_transform(df_train_X)
data_test_X=scalar_X.transform(df_test_X)
scalar_y=MinMaxScaler()
data_train_y=scalar_y.fit_transform(df_train_y)
data_test_y=scalar_y.transform(df_test_y)
然后将数据处理成tensor的格式,也就是 [(feature), (label)] 的格式。这里我原本想将数据分成三个连续数据为一组的形式,也就是seq_len=3,使得lstm能够保留一定的前后文关系。
但是后面发现seq_len=3没有使得模型损失更小,但是给反归一化带来了麻烦,我这里就又换成了1。
注意的是,再写循环的时候,要在len(x)后面+1,因为切分取值最后一个不取,因此会损失一个,加上1就解决了这个问题。
seq_len=1
def create_seq(X,y):
seq=[]
for i in range(seq_len,len(X)+1):
fea=torch.FloatTensor(X[i-seq_len:i])
label=torch.FloatTensor(y[i-seq_len:i])
seq.append([fea,label])
return seq
train_=create_seq(data_train_X,data_train_y)
test_=create_seq(data_test_X,data_test_y)
5.构建lstm+fc模型
在模型构建的第一步思路中,我先是按照pytorch的普遍格式,首先在init模块定义网络层,然后再forward模块定义网络层的流动,也就是lstm的隐藏层数据再输入到全连接层,由全连接生成预测值。
#====构建模型
class lstm(nn.module):
def __init__(self,input_size,hidden_layer_size,output_size):
super().__init__() #这主要是继承父类的意思,暂时还不太懂作用
self.lstm=nn.LSTM(input_size,hidden_layer_size)
self.linear=nn.linear(hidden_layer_size,output_size)
def forward(self,input_seq):
lstm_out,self.hidden_cell=self.lstm(input_seq,self.hidden_cell)
predictions=self.linear(lstm_out)
return predictions
input_size就是特征维度,hidden_layer_size是隐藏层神经元个数,output_size是目标输出维度。(https://blog.csdn.net/foneone/article/details/104002372)
在使用模型中,需要了解lstm的输入格式,input=(seq_len, batch, input_size)。我个人开始难理解的是这个batch和seq_len的意义差别,batch很好理解,就是批次数据,seq_len就是输入的sequence_length(序列长度),既然LSTM是处理序列数据的,那么序列就需要一个长度。input_size就是特征维度了。(https://www.cnblogs.com/danielkung/p/14354415.html)
还有lstm的输出格式,output, (hn, cn) = lstm(x, (h0, c0))得到输出,输出output的size是(seq_len, batch, num_directions × hidden_size) 。(https://blog.csdn.net/qq_39540454/article/details/117304312)
因此,在输入数据的过程中,要注意与输入数据格式进行对齐。
6.训练模型
批次训练过程中首先要对数据集进行批次数据采集
我用到的是torch.utils.data包中的data.dataloader(),这个可以从数据集中直接进行无放回的批次采样,很便捷。
#====构建模型
loader=data.DataLoader(dataset=train_,batch_size=batch_size)
再在每个epoch里面循环这个loader就会得到(batch_size,feature_dimension)的数据
epochs=150
for i in range(epochs):
for step,(batch_x,batch_y) in enumerate(loader):
这里我打印了batch_x的size, 可以看到刚好是我们想要的batch_size,seq_len,feature_dimension的格式。
输入到lstm中,还需要一步维度转换,将batch_size与seq_len的维度位置进行调换。pytorch中维度交换的函数好像只有两个,transpose和permute。我用的是permute,将第0维和第1维进行交换。交换以后,就可以开始训练了。
首先optimizer.zero_grad()将模型的梯度设为0,然后开始模型训练,得到每个批次的损失值,反向传播再优化。
为了后面打印损失值,我在将每个批次的损失值添加到列表中,注意这里因为single_loss是tensor格式,取值就用item().
batch_size=50
epochs=50
input_size=9
loader=data.DataLoader(dataset=train_,batch_size=batch_size)
loss=[]
for i in range(epochs):
loss1=[]
for step,(batch_x,batch_y) in enumerate(loader):
# print(batch_x.size(),batch_y.size())
batch_x = batch_x. permute(1, 0, 2)
batch_y = batch_y. permute(1, 0, 2)
# print(batch_x.size(),batch_y.size())
optimizer.zero_grad()
y_pred=model(batch_x)
single_loss=loss_function(y_pred,batch_y)
single_loss.backward()
optimizer.step()
loss1.append(single_loss.item())
print(f'epoch:{i:3} loss:{np.mean(loss1):10.8f}')
loss.append(np.mean(loss1))
得到损失值以后,就可以对损失值进行绘制,查看训练效果。
plt.plot(loss,'b',label='loss')
plt.xlabel('epochs')
plt.ylabel('loss value')
plt.legend()
损失图可以看出来收敛的效果挺好的。下一步就是对测试集进行测试。
测试集,首先要更改模型为测试或者验证模式:model.eval()
这里测试的时候,是直接把test_x整个数据集放进去的,为了保证数据类型和格式一致,我仍然用的Dataloader取值的,就是把batch_size直接设定为test集的长度。
同样,要输入到模型中,输入格式要注意调整为(seq_lem,batch_size,feature_dimension),我用的view对格式进行了转换。-1就是它自动计算。
然后,再设定with torch.no_grad():此时数据不需要计算梯度,也不会进行反向传播。
将数据输入模型,得到预测值和损失值
loader1=data.DataLoader(dataset=test_,batch_size=len(test_))
model.eval()
for step,(test_x,test_y) in enumerate(loader1):
test_x = test_x. view(seq_len,-1,9)
test_y = test_y. view(seq_len,-1,2)
with torch.no_grad():
test_pred=model(test_x)
print(test_pred.size())
single_loss=loss_function(test_pred,test_y)
print(f' loss:{single_loss.item():10.8f}')
最后一步就是将预测值进行反归一化,得到预测值。因为输出的test_pred格式是3维的,通过view将其转为我们想要的方便处理的2维数据,再用是scaler_y直接进行反归一化。
test_pred=np.array(test_pred.view(len(test_),2).tolist())
test_y_real=scalar_y.inverse_transform(test_pred)
然后绘图检验真实值和预测值之间的差异。这里就是要把df_test中的索引要去掉,不然显示的位置会不一致
plt.plot(df_test_y['target_veh'].reset_index(drop=True),'r',label='test')
plt.plot(test_y_real[:,0],'b',label='pred')
plt.legend()
plt.show()
plt.plot(df_test_y['target_speed'].reset_index(drop=True),'r',label='test')
plt.plot(test_y_real[:,1],'b',label='pred')
plt.legend()
plt.show()
三、总结
1、模型中的输入和输出维度特别关键,是模型能运行的保证。
2、其次,tensor和list和np.array数据类型的转换有点绕,感觉最后一步再生成tensor后,不要再改动数据类型稳妥一点。
3、不熟练真的要有耐心,希望熟能生巧吧
完整代码如下
import numpy as np
import pandas as pd
from sklearn.preprocessing import MinMaxScaler
import math
import torch
import torch.nn as nn
import torch.nn.functional
import torch.utils.data as data
import matplotlib.pyplot as plt
#====导入数据并将数据读取成(9,2)
col_list1=['Lane 1 Flow (Veh/h)', 'Lane 2 Flow (Veh/h)','Lane 3 Flow (Veh/h)', 'Lane 4 Flow (Veh/h)']
col_list2=['Lane 1 Speed (km/h)','Lane 2 Speed (km/h)', 'Lane 3 Speed (km/h)', 'Lane 4 Speed (km/h)']
df=pd.read_csv(r"D:\学习\博士材料\实验\CTS2023竞赛\CTS2023竞赛数据\train-5min\1.csv",usecols=col_list1+col_list2+['Week'])
df=df.replace(0,np.nan)
df = df.interpolate()
df['target_veh']=df[col_list1].sum(axis=1).shift(-1)
df['target_speed']=(df['Lane 1 Flow (Veh/h)']*df['Lane 1 Speed (km/h)']+df['Lane 2 Flow (Veh/h)']*df['Lane 2 Speed (km/h)']+df['Lane 3 Flow (Veh/h)']*df['Lane 3 Speed (km/h)']+df['Lane 4 Flow (Veh/h)']*df['Lane 4 Speed (km/h)']).shift(-1)/df['target_veh']
df1=df.dropna()
train_len=math.ceil(len(df1)*0.8)
df_train_X,df_train_y=df1.iloc[:train_len,0:-2],df1.iloc[:train_len,-2:]
df_test_X,df_test_y=df1.iloc[train_len:,0:-2],df1.iloc[train_len:,-2:]
scalar_X=MinMaxScaler()
data_train_X=scalar_X.fit_transform(df_train_X)
data_test_X=scalar_X.transform(df_test_X)
scalar_y=MinMaxScaler()
data_train_y=scalar_y.fit_transform(df_train_y)
data_test_y=scalar_y.transform(df_test_y)
seq_len=1
def create_seq(X,y):
seq=[]
for i in range(seq_len,len(X)+1):
fea=torch.FloatTensor(X[i-seq_len:i])
label=torch.FloatTensor(y[i-seq_len:i])
seq.append([fea,label])
return seq
train_=create_seq(data_train_X,data_train_y)
test_=create_seq(data_test_X,data_test_y)
#====构建模型
class lstm(nn.Module):
def __init__(self,input_size=9,hidden_layer_size=100,output_size=2):
super().__init__() #这主要是继承父类的意思,暂时还不太懂作用
self.lstm=nn.LSTM(input_size,hidden_layer_size)
self.linear=nn.Linear(hidden_layer_size,output_size)
def forward(self,input_seq):
lstm_out,_=self.lstm(input_seq)
# print(lstm_out.size())
predictions=self.linear(lstm_out)
# print(predictions.size())
return predictions
model=lstm()
loss_function=nn.MSELoss()
optimizer=torch.optim.Adam(model.parameters(),lr=0.001)
print(model)
#====训练模型
batch_size=50
epochs=50
input_size=9
loader=data.DataLoader(dataset=train_,batch_size=batch_size)
loss=[]
for i in range(epochs):
loss1=[]
for step,(batch_x,batch_y) in enumerate(loader):
print(batch_x.size(),batch_y.size())
batch_x = batch_x. permute(1, 0, 2)
batch_y = batch_y. permute(1, 0, 2)
# print(batch_x.size(),batch_y.size())
optimizer.zero_grad()
y_pred=model(batch_x)
single_loss=loss_function(y_pred,batch_y)
single_loss.backward()
optimizer.step()
loss1.append(single_loss.item())
print(f'epoch:{i:3} loss:{np.mean(loss1):10.8f}')
loss.append(np.mean(loss1))
plt.plot(loss,'b',label='loss')
plt.xlabel('epochs')
plt.ylabel('loss value')
plt.legend()
loader1=data.DataLoader(dataset=test_,batch_size=len(test_))
model.eval()
for step,(test_x,test_y) in enumerate(loader1):
test_x = test_x. view(seq_len,-1,9)
test_y = test_y. view(seq_len,-1,2)
with torch.no_grad():
test_pred=model(test_x)
print(test_pred.size())
single_loss=loss_function(test_pred,test_y)
print(f' loss:{single_loss.item():10.8f}')
test_pred=np.array(test_pred.view(len(test_),2).tolist())
test_y_real=scalar_y.inverse_transform(test_pred)
plt.plot(df_test_y['target_veh'].reset_index(drop=True),'r',label='test')
plt.plot(test_y_real[:,0],'b',label='pred')
plt.legend()
plt.show()
plt.plot(df_test_y['target_speed'].reset_index(drop=True),'r',label='test')
plt.plot(test_y_real[:,1],'b',label='pred')
plt.legend()
plt.show()