由于序列数据本质上是连续的,因此我们在处理数据时需要解决这个问题。当序列过长而不能被模型一次性全部处理时,我们希望能拆分这样的序列以便模型方便读取。
Q:怎样随机生成一个具有n个时间步的mini batch的特征和标签?
A:从随机偏移量开始拆分序列,以同时获得覆盖性和随机性。(内容参考了李沐老师的动手学深度学习,简化这个问题,仅进行序列的切分,不区分特征和标签,二者逻辑基本一样)
时间序列数据包含4个特征:温度、湿度、降水、气压,有60000条左右记录。
目标:根据24h内温度,预测下一个24h内的温度。
如果直接把这个dataframe丢到dataloader
里会怎样呢。比如按照batch_size=24
进行划分,使一组数据包含24个记录。
df = pd.read_csv('../data/2013-2022-farm/farm.csv')
dataloader = torch.utils.data.DataLoader(df.iloc[:, 1:].values, batch_size=24, shuffle=True)
结果如下:
for data in dataloader:
print(data)
print()
可以看到,这样处理后,得到的序列实际上只有2529个,完全没有好好地利用整个序列。
而且data代表的其实不是一个小batch,而是一条序列数据。
每个样本都是在原始的长序列上任意捕获的子序列。先给出整体代码,然后进行解释。
def seq_data_iter_random(data, batch_size, num_steps):
# 随机初始化位置对data进行切割得到新的data列表
data = data[random.randint(0, num_steps - 1):]
# 能够得到的子序列数目
num_subseqs = (len(data) - 1) // num_steps
# 创建一个新的列表, 用于记录子序列的开始位置
initial_indices = list(range(0, num_subseqs * num_steps, num_steps))
# 随机打乱各个子序列的顺序
random.shuffle(initial_indices)
# 总批量个数等于:子序列个数 / 小批量大小
num_batches = num_subseqs // batch_size
# 每次取batch_size个数据
for i in range(0, batch_size * num_batches, batch_size):
# 取batch_size个数值,取出该批量的子序列的开始位置
initial_indices_per_batch = initial_indices[i:i + batch_size]
X = [data[j: j + num_steps] for j in initial_indices_per_batch]
yield torch.tensor(X)
使用示例,本质上是一个迭代器:
df = pd.read_csv('../data/2013-2022-farm/farm.csv')
data = df.iloc[:, 1:].values
for X in seq_data_iter_random(data, 32, 24):
print(X)
data = data[random.randint(0, num_steps - 1):]
因为不同的随机偏移量可以得到不同的子序列,这样能够提高覆盖性。
以num_steps=5
为例,可能产生的切割有:
在程序中,步长设为24,执行前后,data从60695变为60684:
num_subseqs = (len(data) - 1) // num_steps
结果: ( 60684 − 1 ) ÷ 24 = 2528 (60684 - 1) \div 24 = 2528 (60684−1)÷24=2528
# 创建一个新的列表, 用于记录子序列的开始位置
initial_indices = list(range(0, num_subseqs * num_steps, num_steps))
# {list: 2528}[0, 24, 48, 72, 96, 120, 144, 168, 192, 216, ...]
# 随机打乱各个子序列的顺序
random.shuffle(initial_indices)
batch_size设置为32
# 总批量个数等于:子序列个数 / 小批量大小
num_batches = num_subseqs // batch_size
变量 | 大小 | 含义 |
---|---|---|
data | (60684, 4) | |
num_steps | 24 | 步长 |
num_subseqs | 2528 | 划分产生的子序列数目, d a t a n u m _ s t e p s \frac{data}{num\_steps} num_stepsdata |
batch_size | 32 | |
num_batches | 79 | 能产生多少个批量,不足的一个批量的部分直接舍去, n u m _ s u b s e q s n u m _ b a t c h e s \frac{num\_subseqs}{num\_batches} num_batchesnum_subseqs |
for i in range(0, batch_size * num_batches, batch_size):
# 取batch_size个数值,取出该批量的子序列的开始位置
initial_indices_per_batch = initial_indices[i:i + batch_size]
X = [data[j: j + num_steps] for j in initial_indices_per_batch]
yield torch.tensor(X)
循环进行num_batches
:
# 每次取batch_size个数据,range (0, 2528, 32)
for i in range(0, batch_size * num_batches, batch_size):
循环体内,每次取batch_size个数值,取出该批量的子序列的开始位置:
initial_indices_per_batch = initial_indices[i:i + batch_size]
# {list: 32}[18456, 17232, 13320, 2904, 51240, 56472, 25056, 17040, 8040, 33936, 30792, 12312, 17328, 8304, 28128, 29976, 46560, 4680, 53928, 39096, 14616, 12240, 57120, 29784, 2784, 4752, 22272, 5040, 42600, 41856, 38232, 20448]
根据子序列的开始位置生成这个batch的子序列数据:
X = [data[j: j + num_steps] for j in initial_indices_per_batch]
yield torch.tensor(X)
由于在训练过程中会不断地调用seq_data_iter_random
迭代器产生数据,而初始时的偏移量是随机的,最终有机会获得所有可能的序列:
随机偏移量切割情况:
可能获得的序列如下,每次完整运行seq_data_iter_random
时,可以产生下述示意图中的一行数据(但每一行中的子序列顺序随机),多次运行可以覆盖所有的情况。
保证两个相邻的小批量在原始序列中也是相邻的。保留了拆分的子序列的顺序,因此称为顺序分区。
有一说一我感觉把上面那行shuffle=True改一下不就好了嘛。
注意:不是指在一个小批量里的数据是相邻的,而是两个不同的小批量的相邻访问性质。
def seq_data_iter_sequential(data, batch_size, num_steps, num_features=1):
# 从偏移量开始拆分序列
offset = random.randint(0, num_steps)
# 计算偏移offset后的序列长度
num_tokens = ((len(data) - offset - 1) // batch_size) * batch_size
# 截取序列
Xs = torch.tensor(data[offset: offset + num_tokens])
# 变形为第一维度为batch_size大小;
Xs = Xs.reshape(batch_size, -1, num_features)
# 求得批量总数
num_batches = Xs.shape[1] // num_steps
# 访问各个batch
for i in range(0, num_steps * num_batches, num_steps):
X = Xs[:, i:i + num_steps]
yield X
和上述流程比较类似,不再解释
# 从偏移量开始拆分序列
offset = random.randint(0, num_steps)
# 计算偏移offset后的序列长度
num_tokens = ((len(data) - offset - 1) // batch_size) * batch_size
# 截取序列
Xs = torch.tensor(data[offset: offset + num_tokens])
Xs = data.reshape(batch_size, -1, num_features)
执行前后:
可以理解为将一整个序列,先拆分为32个小序列。然后每一个batch从32个小序列中取一个元素。
# 求得批量总数
num_batches = Xs.shape[1] // num_steps
# 1896 // 24 = 79
# 访问各个batch
for i in range(0, num_steps * num_batches, num_steps):
X = Xs[:, i:i + num_steps]
yield X
以batch_size = 3, num_steps=2
为例,首先将一整个序列折叠为3份。
然后每一个batch顺序地分别从3份中选择两个元素
class SeqDataLoader:
"""加载序列数据的迭代器"""
def __init__(self, data, batch_size, num_steps, use_random_iter, num_features=1):
if use_random_iter:
self.data_iter_fn = self.seq_data_iter_random
else:
self.data_iter_fn = self.seq_data_iter_sequential
self.data = data
self.batch_size, self.num_steps = batch_size, num_steps
self.num_features = num_features
def seq_data_iter_random(self, data, batch_size, num_steps, num_features=1):
# 随机初始化位置对data进行切割得到新的data列表
data = data[random.randint(0, num_steps - 1):]
num_subseqs = (len(data) - 1) // num_steps
# 创建一个新的列表, 用于记录子序列的开始位置
initial_indices = list(range(0, num_subseqs * num_steps, num_steps))
# 随机打乱各个子序列的顺序
random.shuffle(initial_indices)
# 总批量个数等于:子序列个数 / 小批量大小
num_batches = num_subseqs // batch_size
# 每次取batch_size个数据
for i in range(0, batch_size * num_batches, batch_size):
# 取batch_size个数值,取出该批量的子序列的开始位置
initial_indices_per_batch = initial_indices[i:i + batch_size]
X = [data[j: j + num_steps] for j in initial_indices_per_batch]
yield torch.tensor(X)
def seq_data_iter_sequential(self, data, batch_size, num_steps, num_features=1):
# 从偏移量开始拆分序列
offset = random.randint(0, num_steps)
# 计算偏移offset后的序列长度
num_tokens = ((len(data) - offset - 1) // batch_size) * batch_size
# 截取序列
Xs = torch.tensor(data[offset: offset + num_tokens])
# 变形为第一维度为batch_size大小;
Xs = Xs.reshape(batch_size, -1, num_features)
# 求得批量总数
num_batches = Xs.shape[1] // num_steps
# 访问各个batch
for i in range(0, num_steps * num_batches, num_steps):
X = Xs[:, i:i + num_steps]
yield X
def __iter__(self):
return self.data_iter_fn(self.data, self.batch_size, self.num_steps, self.num_features)
使用方法:
dataloader = SeqDataLoader(data, batch_size=32, num_steps=24, use_random_iter=False, num_features=4)
for X in dataloader:
print(X)
根据num_steps的数据,预测predict_steps的数据。
获取标签Y的逻辑与上类似,下次填坑。
可使用.normalization函数进行归一化,.reverse_normalization函数复原。
import random
import pandas as pd
import torch.utils.data
from sklearn.preprocessing import MinMaxScaler
# 将时间序列文件分为小批量
class SeqDataLoader:
"""加载序列数据的迭代器,根据num_steps的数据,预测predict_steps的数据"""
def __init__(self, data, batch_size, num_steps, use_random_iter, num_features=1, predict_steps=1):
if use_random_iter:
self.data_iter_fn = self.seq_data_iter_random
else:
self.data_iter_fn = self.seq_data_iter_sequential
self.data = data
self.batch_size, self.num_steps, self.predict_steps = batch_size, num_steps, predict_steps
self.num_features = num_features
self.scaler = MinMaxScaler()
self.normalization()
def seq_data_iter_random(self, data, batch_size, num_steps, num_features=1, predict_steps=1):
# 随机初始化位置对data进行切割得到新的data列表
data = data[random.randint(0, num_steps - 1):]
# 减去predict_steps以保证子序列可获得对应的标签而不会越界
num_subseqs = (len(data) - self.predict_steps) // num_steps
# 创建一个新的列表, 用于记录子序列的开始位置
initial_indices = list(range(0, num_subseqs * num_steps, num_steps))
# 随机打乱各个子序列的顺序
random.shuffle(initial_indices)
# 总批量个数等于:子序列个数 / 小批量大小
num_batches = num_subseqs // batch_size
# 每次取batch_size个数据
for i in range(0, batch_size * num_batches, batch_size):
# 取batch_size个数值,取出该批量的子序列的开始位置
initial_indices_per_batch = initial_indices[i:i + batch_size]
X = [data[j: j + num_steps] for j in initial_indices_per_batch]
Y = [data[j + num_steps: j + num_steps + predict_steps] for j in initial_indices_per_batch]
yield torch.tensor(X), torch.tensor(Y)
def seq_data_iter_sequential(self, data, batch_size, num_steps, num_features=1, predict_steps=1):
# 从偏移量开始拆分序列
offset = random.randint(0, num_steps)
# 计算偏移offset后的序列长度,减去predict_steps以保证子序列可获得对应的标签而不会越界
num_tokens = ((len(data) - offset - predict_steps) // batch_size) * batch_size
# 截取序列
Xs = torch.tensor(data[offset: offset + num_tokens])
# Ys从后一个时间序列开始截取,表示label
Ys = torch.tensor(data[offset + num_steps: offset + num_tokens + num_steps])
# 变形为第一维度为batch_size大小;
Xs = Xs.reshape(batch_size, -1, num_features)
Ys = Ys.reshape(batch_size, -1, num_features)
# 求得批量总数
num_batches = Xs.shape[1] // num_steps
# 访问各个batch
for i in range(0, num_steps * num_batches, num_steps):
X = Xs[:, i:i + num_steps]
Y = Ys[:, i:i + predict_steps]
yield X, Y
def __iter__(self):
return self.data_iter_fn(self.data, self.batch_size, self.num_steps, self.num_features, self.predict_steps)
def normalization(self):
self.data = self.scaler.fit_transform(self.data) # 对选定的列进行归一化
self.data = self.data.astype('float32')
def reverse_normalization(self):
self.data = self.scaler.inverse_transform(self.data) # 恢复各列数据