原代码
点击查看代码
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)
浙公网安备 33010602011771号