原代码

点击查看代码
class Dataset_Custom(Dataset):
    def __init__(self, root_path, flag='train', size=None,
                 features='S', data_path='ETTh1.csv',
                 target='OT', scale=True, timeenc=0, freq='h', train_only=False):
        # 初始化函数,定义数据集的基本参数
        # size [seq_len, label_len, pred_len] 表示序列长度、标签长度和预测长度
        # info
        if size == None:
            # 如果未指定size,则使用默认值
            self.seq_len = 24 * 4 * 4  # 序列长度
            self.label_len = 24 * 4   # 标签长度
            self.pred_len = 24 * 4     # 预测长度
        else:
            # 如果指定了size,则使用指定的值
            self.seq_len = size[0]
            self.label_len = size[1]
            self.pred_len = size[2]
        # init
        assert flag in ['train', 'test', 'val']  # 确保flag是'train', 'test', 或 'val'中的一个
        type_map = {'train': 0, 'val': 1, 'test': 2}  # 将flag映射为数字
        self.set_type = type_map[flag]  # 设置当前数据集的类型(训练、验证或测试)

        self.features = features  # 特征类型,'S'表示单变量,'M'表示多变量
        self.target = target  # 目标变量
        self.scale = scale  # 是否对数据进行标准化
        self.timeenc = timeenc  # 时间编码方式
        self.freq = freq  # 时间频率
        self.train_only = train_only  # 是否仅使用训练数据

        self.root_path = root_path  # 数据根路径
        self.data_path = data_path  # 数据文件路径
        self.__read_data__()  # 读取数据

    def __read_data__(self):
        self.scaler = StandardScaler()  # 初始化标准化器
        df_raw = pd.read_csv(os.path.join(self.root_path,
                                          self.data_path))  # 读取原始数据

        '''
        df_raw.columns: ['date', ...(other features), target feature]
        '''
        cols = list(df_raw.columns)  # 获取所有列名
        if self.features == 'S':
            cols.remove(self.target)  # 如果是单变量特征,移除目标列
        cols.remove('date')  # 移除日期列
        num_train = int(len(df_raw) * (0.7 if not self.train_only else 1))  # 计算训练集大小
        num_test = int(len(df_raw) * 0.2)  # 计算测试集大小
        num_vali = len(df_raw) - num_train - num_test  # 计算验证集大小
        border1s = [0, num_train - self.seq_len, len(df_raw) - num_test - self.seq_len]  # 计算数据分割的起始点
        border2s = [num_train, num_train + num_vali, len(df_raw)]  # 计算数据分割的结束点
        border1 = border1s[self.set_type]  # 根据数据集类型选择起始点
        border2 = border2s[self.set_type]  # 根据数据集类型选择结束点

        if self.features == 'M' or self.features == 'MS':
            df_raw = df_raw[['date'] + cols]  # 如果是多变量特征,保留日期列和其他特征列
            cols_data = df_raw.columns[1:]  # 获取特征列
            df_data = df_raw[cols_data]  # 提取特征数据
        elif self.features == 'S':
            df_raw = df_raw[['date'] + cols + [self.target]]  # 如果是单变量特征,保留日期列和目标列
            df_data = df_raw[[self.target]]  # 提取目标数据

        if self.scale:
            train_data = df_data[border1s[0]:border2s[0]]  # 获取训练数据
            self.scaler.fit(train_data.values)  # 对训练数据进行标准化
            data = self.scaler.transform(df_data.values)  # 对整个数据集进行标准化
        else:
            data = df_data.values  # 如果不进行标准化,直接使用原始数据

        df_stamp = df_raw[['date']][border1:border2]  # 提取日期数据
        df_stamp['date'] = pd.to_datetime(df_stamp.date)  # 将日期转换为datetime格式
        if self.timeenc == 0:
            # 如果时间编码方式为0,提取月份、日、星期和小时
            df_stamp['month'] = df_stamp.date.apply(lambda row: row.month, 1)
            df_stamp['day'] = df_stamp.date.apply(lambda row: row.day, 1)
            df_stamp['weekday'] = df_stamp.date.apply(lambda row: row.weekday(), 1)
            df_stamp['hour'] = df_stamp.date.apply(lambda row: row.hour, 1)
            data_stamp = df_stamp.drop(['date'], 1).values  # 移除日期列并转换为数组
        elif self.timeenc == 1:
            # 如果时间编码方式为1,使用time_features函数进行时间编码
            data_stamp = time_features(pd.to_datetime(df_stamp['date'].values), freq=self.freq)
            data_stamp = data_stamp.transpose(1, 0)

        self.data_x = data[border1:border2]  # 设置输入数据
        self.data_y = data[border1:border2]  # 设置输出数据
        self.data_stamp = data_stamp  # 设置时间戳数据

    def __getitem__(self, index):
        s_begin = index  # 序列起始点
        s_end = s_begin + self.seq_len  # 序列结束点
        r_begin = s_end - self.label_len  # 标签起始点
        r_end = r_begin + self.label_len + self.pred_len  # 标签结束点

        seq_x = self.data_x[s_begin:s_end]  # 获取输入序列
        seq_y = self.data_y[r_begin:r_end]  # 获取输出序列
        seq_x_mark = self.data_stamp[s_begin:s_end]  # 获取输入序列的时间戳
        seq_y_mark = self.data_stamp[r_begin:r_end]  # 获取输出序列的时间戳

        return seq_x, seq_y, seq_x_mark, seq_y_mark  # 返回输入序列、输出序列及其时间戳

    def __len__(self):
        return len(self.data_x) - self.seq_len - self.pred_len + 1  # 返回数据集的长度

    def inverse_transform(self, data):
        return self.scaler.inverse_transform(data)  # 对数据进行逆标准化

1、什么时候使用 Dataset_Custom 类:

顾名思义,它适用于自定义的数据集,但自定义的数据任然需要满足一些基本前提,如第一列是时间,最后一列要是‘OT’(即target)

像electricity、exchange_rate、traffic、weather,用这些数据训练时,都是使用Dataset_Custom类来加载数据的

他们的列数(channel或者说输入变量)都是不一样的,所以当你想使用自己的数据集时就需要根据具体情况更改enc_in


2、为什么__init__的参数和Dataset_ETT写着都是“一样的”?

感觉是习惯问题,因为前人(Informer)也是这么写的,但其实那些是“形参”,在真正运行时,模型的实际参数会用“data_provider”的方法进行传入


3、单变量和多变量预测的区别?

单变量可以理解成 y=f(x1):x1 就是 target 变量的历史值,y 是 target 在未来的预测值

多变量相当于 y=f(x1,x2,x3...xn):x1, x2, ..., xn 是所有的非日期特征(包括 target 本身和其他特征变量),然后模型基于这些变量预测 target 的未来值

4、能不能用Dataset_Custom 替代 Dataset_ETT_hour 和 Dataset_ETT_minute

答:理论上可以,但...

尽管 Dataset_Custom 可以加载各种数据格式,并且适应不同时间粒度,但它不能直接替代 Dataset_ETT_hour 和 Dataset_ETT_minute,原因如下:

  • 时间间隔不同
    Dataset_ETT_hour 使用 freq='h',而 Dataset_ETT_minute 使用 freq='t'。
    Dataset_Custom 需要用户自己指定 freq,如果使用不当,可能会导致时间特征错误。

  • 数据格式不同
    Dataset_ETT_hour 和 Dataset_ETT_minute 采用 ETT 数据集格式,直接适配 ETTh1.csv / ETTm1.csv 等文件。
    Dataset_Custom 的列顺序由用户定义,可能和 ETT 的格式不完全匹配,可能会导致训练过程中的数据不一致。

  • 训练集、测试集划分不同
    Dataset_ETT_hour 和 Dataset_ETT_minute 基于固定的时间区间划分数据集,适用于具有周期性的时序数据(如电力数据)。
    Dataset_Custom 基于数据量的比例划分,适用于数据长度不固定的情况。

  • 数据增强
    Dataset_ETT_hour 和 Dataset_ETT_minute 可能包含特定的时间序列增强方法,而 Dataset_Custom 需要用户自行添加增强逻辑。

所以

如果要用 Dataset_Custom 代替 Dataset_ETT_hour 和 Dataset_ETT_minute,需要确保:

  • 正确设置 freq(即 'h' 或 't')。
  • 确保数据格式匹配,尤其是 date 列和 target 列。
  • 手动调整训练、验证、测试集划分方式,以匹配 ETT 预设的时间区间划分。
  • 处理时间特征编码,如果使用 timeenc=1,确保 time_features() 生成的时间特征是合理的。

5、为什么要用训练集数据拟合标准化器self.scaler.fit(train_data.values)

标准化器 (scaler) 只使用训练集的数据计算均值和标准差,然后用这个scaler来变换整个数据集(包括验证集和测试集)

这样做的目的是避免数据泄露(Data Leakage)

如果用 df_data 计算均值和标准差,那训练集、验证集、测试集的信息都会被用来计算,导致测试数据的均值/方差信息泄露到训练阶段,可能会让模型在测试时表现不真实

但这里 StandardScaler() 只用训练数据来计算均值和标准差,然后应用到整个数据集,这样测试数据就不会影响训练的归一化参数,避免数据泄露问题


6、为什么要提取时间数据df_stamp

在时间序列预测任务中,时间信息通常对预测目标有很大影响。

例如,销售数据可能具有季节性模式(每年夏天销量高),而交通流量数据可能具有周期性模式(每天早晚高峰)。

因此,模型需要显式地编码时间特征,帮助它理解数据中的时间依赖关系。

时间编码方式 作用
手动编码 (timeenc=0) 直接提取 month, day, weekday, hour,适合简单模型
自动编码 (timeenc=1) 可能用正弦/余弦编码来表示时间,适合深度学习模型

7、df_stamp长什么样?

假设

date
-------------------
2025-03-01 00:00 ...
2025-03-01 01:00 ...
2025-03-01 02:00 ...
2025-03-01 03:00 ...
...

手动提取时间特征(timeenc = 0):

month day weekday hour
3 1 5 0
3 1 5 1
3 1 5 2
3 1 5 3

自动时间编码(timeenc = 1),通常使用正弦和余弦进行周期性时间编码:

  • 月份 (month) → sin(2π * month / 12), cos(2π * month / 12)

  • 星期 (weekday) → sin(2π * weekday / 7), cos(2π * weekday / 7)

  • 小时 (hour) → sin(2π * hour / 24), cos(2π * hour / 24)

sin_month cos_month sin_weekday cos_weekday sin_hour cos_hour
0.5 0.866 -0.707 0.707 0.0 1.0
0.5 0.866 -0.707 0.707 0.2588 0.9659
0.5 0.866 -0.707 0.707 0.5 0.866
0.5 0.866 -0.707 0.707 0.707 0.707
0.5 0.866 -0.707 0.707 0.866 0.5

所以

时间编码方式 df_stamp可能的样子
手动编码 (timeenc=0) [[3, 1, 5, 0], [3, 1, 5, 1], ...]
自动编码 (timeenc=1) [[0.5, 0.866, -0.707, 0.707, 0.0, 1.0], ...]

8、df_stamp怎么被模型使用的?

时间编码 (data_stamp) 是直接作为额外特征加到数据 (data_x) 里面,用于训练模型。

它的作用是提供时间信息,帮助模型学习时间相关的模式,比如季节性、周期性和趋势。

在 Dataset_Custom 里,时间编码 data_stamp 通过 __getitem__() 被返回,和 data_x 一起输入模型,seq_x_mark(输入时间编码)和 seq_y_mark(输出时间编码)就是 data_stamp 的子集
假设

seq_x = [ # 温度;单变量s
    [15.2],  # 00:00
    [14.8],  # 01:00
    [14.5],  # 02:00
    [14.2],  # 03:00
    [14.0],  # 04:00
]
seq_x_mark = [
    [3, 1, 5, 0],  # 00:00
    [3, 1, 5, 1],  # 01:00
    [3, 1, 5, 2],  # 02:00
    [3, 1, 5, 3],  # 03:00
    [3, 1, 5, 4],  # 04:00
]
x = torch.cat([seq_x, seq_x_mark], dim=-1)  # 在最后一个维度拼接

得到:

x = [
    [15.2, 3, 1, 5, 0],  # 00:00
    [14.8, 3, 1, 5, 1],  # 01:00
    [14.5, 3, 1, 5, 2],  # 02:00
    [14.2, 3, 1, 5, 3],  # 03:00
    [14.0, 3, 1, 5, 4],  # 04:00
]

这样,模型不仅可以看到温度数据,还能知道当前的时间点


9、label_len 在时间序列中的含义

首先,label 并不是传统意义上的“标签”(如分类任务的 y),而是"过去一部分时间点的数据",在预测时仍然会输入给模型

在时间序列预测任务中,我们的目标是:输入 seq_len 长度的历史数据,预测 pred_len 长度的未来数据

label_len 在这里表示:“桥接” 过去 seq_len 和 未来 pred_len 的过渡部分。

虽然 label_len 的这部分数据属于 seq_y(输出),但它仍然是已知的,可以作为模型输入。

举个例子:

seq_len = 10
label_len = 5
pred_len = 3

时间点:  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20
数据值: 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29

seq_x (输入)  = [  6,  7,  8,  9, 10, 11, 12, 13, 14, 15 ]   # 10 个点
seq_y (目标)  = [ 11, 12, 13, 14, 15, 16, 17, 18 ]           # 8 个点
               [<----label_len---->][<-pred_len->]
  • seq_x 是输入
  • seq_y 前 label_len=5 作为已知部分
  • seq_y 的 pred_len=3 是模型的预测目标

label_len 的作用

  • 缓冲作用:
    预测序列的第一部分可能对 pred_len 预测有帮助,比如有些模型会继续输入 label_len 作为辅助信息,而不是直接预测 pred_len

  • 适配 Transformer-based 模型:
    Transformer-based 时间序列预测模型(如 Informer)中,解码器的输入通常包含一部分已知 label_len,然后让模型自回归预测 pred_len

  • 控制预测难度:
    如果 label_len=0,则 seq_y 只有 pred_len,这样模型只能靠 seq_x 预测未来
    适当的 label_len 可以让模型更平稳地过渡到未来预测部分

所以

  • 不使用 Transformer(比如 LSTM、GRU、CNN)时,label_len 可以设为 0,甚至去掉相关逻辑

  • 使用 Transformer-based 结构(如 Informer, Autoformer, Transformer)时,label_len 有助于提升预测效果

  • label_len 并不是必需的,而是用于提升 Transformer 预测稳定性的技巧


10、一个批次的数据一般长什么样

在 DataLoader 中,每个批次的数据来自 Dataset_Custom 的 getitem 方法

当 DataLoader 以 batch_size = B 取数据时,它会批量堆叠多个样本,每个样本是 (seq_x, seq_y, seq_x_mark, seq_y_mark),所以:

如果 batch_size = B,seq_len = 96,label_len = 48,pred_len = 96,数据的形状可能是:

数据 形状
seq_x (B, seq_len, 特征数) → (B, 96, C)
seq_y (B, label_len + pred_len, 特征数) → (B, 144, C)
seq_x_mark (B, seq_len, 时间特征数) → (B, 96, T)
seq_y_mark (B, label_len + pred_len, 时间特征数) → (B, 144, T)

C 是变量(或通道)个数(多变量模式下,可能是 7、10 等)
T 是时间特征的维度(可能是 4,比如 year, month, day, hour)