原文首发在公众号 AI天天用,欢迎关注,共同进步。
应朋友所托,复现Informer代码。没想到后来还吃到瓜了。如果您也有需要复现的文章(ns子刊,顶刊顶会为主),麻烦您关注公众号留言。
Informer是2021年AAAI会议的最佳论文,用于处理长时间序列预测。 由于发表时间较早,当前一些比较简单高效的命令尚未出现。 此外,原代码中涉及巨多的参数设置,对于理解代码而言非常不容易。 基于此,可复现Informer的代码,以便更好地理解文章。 这篇博文先复现数据部分。
Informer论文
原始代码地址: https://github.com/zhouhaoyi/Informer2020/tree/main
部分参数说明(大概40多个):
部分参数说明
原始代码中,数据部分主要涉及3个文件,data/loader_loader.py
,exp/exp_informer.py
, 以及utils/timefeatures.py
。对于初学者而言,大量的参数以及数据处理的复杂性可能已吓退了不少同学。而将关于数据的代码放在不同的文件夹下,也增加了复现和理解数据处理和加载的难度。
我们以ETTh1.csv
文件为例,重写数据部分代码。 首先,设置所有数据存储的主文件夹,一般命名为datasets
或data
等。
dataroot = "informer/datasets" # 数据文件夹路径
datafile = "ETTh1.csv" # 文件名
根据数据文件夹和数据名,即可读取数据
df = pd.read_csv(os.path.join(dataroot, datafile), parse_dates=[0], index_col=0)
df.head()
数据首行
数据尾行
根据上图所示,有一个时间列(设置为索引index)和7个数据列。 数据从2016年7月1日开始,到2018年6月26日结束,大约2年的数据。 时间序列有其特殊性,因此,需要分开处理时间特征和其他数据特征两部分。
时间特征提取
数据部分较好处理,对数据做归一化即可。时间特征的处理,则有不同的方法。 原始代码通过输入时间,时间的编码方式 (args.embed
)以及频率参数(freq
)来控制时间特征获取方法。
复现过程中,可尝试将不同的方法合并到一个函数里,通过想要提取时间特征的不同,选择不同的方法。原始代码仅提供了两种方法,我们对这两种方法进行整合复现。
时间特征提取复现代码如下,仅接收2个参数的输入。通过显式地告诉函数需要的特征来判断特征提取的方法。然后通过特征的名字,从编码字典里获取相应的时间编码方法func
,最后返回时间特征numpy数组。在返回数组前,删除可能的全0列。
def get_time_features(dates, features=["month", "day", "weekday", "hour"]):
"""
"""
if "of" in features[0].lower():
# define the func dict
names = ["SecondOfMinte", "MinuteOfHour", "HourOfDay", "DayOfWeek", "DayOfMonth", "DayOfYear", "WeekOfyear", "MonthOfYear"]
funcs = [
lambda x: x.second / 59. - 0.5,
lambda x: x.minute / 59. - 0.5,
lambda x: x.hour / 23. - 0.5,
lambda x: x.dayofweek / 6. - 0.5,
lambda x: (x.day - 1) / 30. - 0.5,
lambda x: (x.dayofyear - 1) / 365. - 0.5,
lambda x: (x.isocalendar().week - 1) / 52. - 0.5,
lambda x: (x.month - 1) / 11. - 0.5
]
else:
# define the function dict
names = ["month", "day", "weekday", "hour", "minute", "second"]
funcs = [
lambda x:x.month,
lambda x:x.day,
lambda x:x.weekday(),
lambda x:x.hour,
lambda x:x.minute,
lambda x:x.second
]
func_dict = dict(zip(names, funcs))
# get the functions according to desired features
funcs = [func_dict[feature] for feature in features]
# apply the selected features
features = dates.apply(lambda row: [func(row) for func in funcs])
# drop the all-zero columns
features = np.array(features.tolist())[:, ~(features == 0).all(axis=0)]
return features.squeeze()
原始代码实际采用的是特征名字是
features = ["HourOfDay", "DayOfWeek", "DayOfMonth", "DayOfYear"]
通过对比,发现复现后的代码和原始代码输入的结果不说100%相似,至少是一模一样,达到了复现的效果。
需要注意的是,原始代码中提取week
的函数已发生变化,改为x.isocalendar().week
才是正确的调用命令。
数据特征提取
数据特征只需做归一化处理,调用sklearn.preprocessing
中的StandardScaler
即可。 但需要注意的是,只能用训练集数据的统计量(均值和方差等)来归一化验证集和测试集。 因而,需要划分训练集,验证集和测试集。
原始代码通过人为设置border1和border2来划分数据集,但对于不同的数据集,这种做法可能并不通用。 复现时,可采用常规的比例划分方法。即,训练集、验证集和测试集根据总体样本数据的多少根据比例来分割。这个比例没有固定的数值,通常可设置为(0.7, 0.2, 0.1)或 (0.8, 0.1, 0.1)或(0.7, 0.1, 0.2)等。
原始代码通过多个参数来控制输入和输出的多变量还是单变量,可能会增加理解难度。复现时,采用了in_cols
和out_cols
显式地给出作为输入和输出特征的列。若是多变量输入,则len(in_cols)
大于1。反之,若是单变量输入,则len(in_cols)
等于1。显式的好处是,一看便知是哪些列作为输入特征,哪些列是输出特征,输入输出是单变量还是多变量。
根据上边的分析,则可复现数据集类:
def parse_column_index(columns, cols, col_type="in"):
"""
columns -- the columns of the dataframe
cols -- columns to be selected
"""
len_columns = len(columns)
if (cols is None) and (col_type == "in"):
return list(range(0, len_columns-1))
if (cols is None) and (col_type == "out"):
return [-1]
if cols == "all":
return list(range(len_columns))
index = []
if type(cols) == list:
for col in cols:
if type(col) == int:
index.append(col)
elif type(col) == str:
index.append(columns.get_loc(col))
return index
class ETTHour(Dataset):
"""
Load ETT hour dataset (ETTh1.csv and ETTh2.csv)
Parameters:
df -- the dataframe with index as dates
flag -- train, validation, and test
in_cols -- the columns as input featuers (exclude index, or the date)
out_cols -- the columns as output features
seq_len -- the length of the sequence
label_len -- the length of the label
pred_len -- the length of the prediction
scale -- whether to scale the features
timeenc -- the type of time features encoding
"""
def __init__(
self,
df,
flag,
in_cols=None,
out_cols=None,
seq_len=96,
label_len=48,
pred_len=24,
scale=True,
ratios=[0.6, 0.2, 0.2],
features = ["HourOfDay", "DayOfWeek", "DayOfMonth", "DayOfYear"]
):
# get the [train | validation | test] set first
dataset_dict = dict(zip(["train", "val", "test"], range(3)))
index = dataset_dict[flag]
# split the whole dataframe according to ratios
nsamples = len(df)
assert nsamples * ratios[-1] > 1, f"test set must have at least one sample"
train_len, val_len = [int(nsamples* ratio) for ratio in ratios[:-1]]
test_len = nsamples - train_len - val_len
self.in_cols_index = parse_column_index(df.columns, in_cols, "in")
self.out_cols_index = parse_column_index(df.columns, out_cols, "out")
cols_index = list(set(self.in_cols_index + self.out_cols_index))
df = df.iloc[:, cols_index]
# train_len = 12*30*24
# val_len = 4*30*24
df_train = df.iloc[:train_len]
df_val = df.iloc[train_len - seq_len : train_len+val_len]
df_test = df.iloc[train_len+val_len-seq_len:]
self.df = [df_train, df_val, df_test][index]
vals = self.df.values
if scale:
self.scaler = StandardScaler()
self.scaler.fit(df_train.values)
# self.df = self.scaler.transform(self.df.values)
vals = self.scaler.transform(vals)
self.vals = vals
self.seq_len = seq_len
self.label_len = label_len
self.pred_len = pred_len
self.t_features = get_time_features(pd.Series(self.df.index), features)
def __getitem__(self, index):
in_start, in_end = index, index + self.seq_len
pred_start, pred_end = in_end - self.label_len, in_end + self.pred_len
X = self.vals[:, self.in_cols_index][in_start: in_end]
Y = self.vals[:, self.out_cols_index][pred_start: pred_end]
Xt = self.t_features[in_start: in_end]
Yt = self.t_features[pred_start: pred_end]
X, Y, Xt, Yt = map(lambda x: torch.from_numpy(x).float(), (X, Y, Xt, Yt))
return X, Y, Xt, Yt
def __len__(self):
return len(self.df) - self.seq_len - self.pred_len + 1
def inverse_transform(self, vals):
return self.scaler.inverse_transform(vals)
然后则按照正常数据集加载方法来加载数据集。
train_set = ETTHour(df, "train", seq_len=seq_len, label_len=label_len, pred_len=pred_len)
val_set = ETTHour(df, "val", seq_len=seq_len, label_len=label_len, pred_len=pred_len)
test_set = ETTHour(df, "test", seq_len=seq_len, label_len=label_len, pred_len=pred_len)
train_loader = DataLoader(train_set, shuffle=True, batch_size=batch_size, num_workers=num_workers, drop_last=True, pin_memory=True)
val_loader = DataLoader(val_set, shuffle=False, batch_size=batch_size, num_workers=num_workers, drop_last=True, pin_memory=True)
test_loader = DataLoader(test_set, shuffle=False, batch_size=batch_size, num_workers=num_workers, drop_last=False, pin_memory=True)
建立好数据集后,则可以打印尺寸和最大值最小值来判断,是初步验证是否正确加载了数据集。
for X, Y, X_t, Y_t in train_loader:
print(X.shape, Y.shape, X_t.shape, Y_t.shape)
print(X.min(), X.max(), Y.min(), Y.max(), X_t.min(), X_t.max(), Y_t.min(), Y_t.max())
为避免打印输出太多内容,可通过assert X.shape == (batch_size, seq_len, num_features)
来判断。
模型,训练和论文解读部分后续更新,请关注公众号,及时获取更新动态。