Informer复现--数据篇

原文首发在公众号 AI天天用,欢迎关注,共同进步。

应朋友所托,复现Informer代码。没想到后来还吃到瓜了。如果您也有需要复现的文章(ns子刊,顶刊顶会为主),麻烦您关注公众号留言。

Informer是2021年AAAI会议的最佳论文,用于处理长时间序列预测。 由于发表时间较早,当前一些比较简单高效的命令尚未出现。 此外,原代码中涉及巨多的参数设置,对于理解代码而言非常不容易。 基于此,可复现Informer的代码,以便更好地理解文章。 这篇博文先复现数据部分。

Informer论文

原始代码地址: https://github.com/zhouhaoyi/Informer2020/tree/main

部分参数说明(大概40多个):

部分参数说明

原始代码中,数据部分主要涉及3个文件,data/loader_loader.pyexp/exp_informer.py, 以及utils/timefeatures.py。对于初学者而言,大量的参数以及数据处理的复杂性可能已吓退了不少同学。而将关于数据的代码放在不同的文件夹下,也增加了复现和理解数据处理和加载的难度。

我们以ETTh1.csv文件为例,重写数据部分代码。 首先,设置所有数据存储的主文件夹,一般命名为datasetsdata等。

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_colsout_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)来判断。

模型,训练和论文解读部分后续更新,请关注公众号,及时获取更新动态。

  • 18
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值