目录
[为什么需要 "读取长序列数据"?](#为什么需要 “读取长序列数据”?)
[1. 滑动窗口(Sliding Window)](#1. 滑动窗口(Sliding Window))
[2. 分段截取(Segmentation)](#2. 分段截取(Segmentation))
[3. 滚动生成(Rolling Generation)](#3. 滚动生成(Rolling Generation))
[4. 关键信息采样](#4. 关键信息采样)
[随机抽样(Random Sampling)](#随机抽样(Random Sampling))
[顺序分区(Sequential Partitioning)](#顺序分区(Sequential Partitioning))
读取长序列数据
读取长序列数据 指的是对超出模型单次处理能力(或固定长度限制)的超长序列进行读取、拆分和预处理的过程。其核心目标是将长序列转换为模型可接受的格式,同时尽可能保留序列中的时序关系和上下文信息。
为什么需要 "读取长序列数据"?
模型输入长度限制: 大多数序列模型(如 RNN、Transformer)对输入长度有固定限制(例如,早期 Transformer 的输入长度上限为 512 个词元)。若原始序列(如一篇万字文章、1 小时的语音)远超此限制,无法直接输入模型。
计算资源约束: 即使模型支持变长输入,超长序列会导致计算量和内存占用呈指数级增长(例如,自注意力机制的时间复杂度为 \(O(n^2)\),n 为序列长度),实际训练或推理时难以承受。
保留时序关系: 长序列的核心价值在于其内部的时序依赖(如文本中的上下文关联、时间序列中的长期趋势)。读取时需避免破坏关键依赖,否则会导致模型性能下降。
读取长序列数据的核心方法
处理长序列的核心思路是 "拆分",但需根据任务需求选择合适的拆分策略,常见方法包括:
1. 滑动窗口(Sliding Window)
- 原理:用固定长度的 "窗口" 在长序列上滑动,每次截取窗口内的子序列作为样本,窗口间可重叠(保留部分上下文)。
- 示例:对长度为 1000 的文本,用长度为 100 的窗口,步长为 50 滑动,可得到 19 个样本(窗口位置:[0-99], [50-149], ..., [900-999])。
- 适用场景:时间序列预测(如预测未来温度需保留近期趋势)、文本分类(需捕捉局部上下文)。
- 优点:保留局部时序关系,样本数量多;
- 缺点:窗口外的远距离依赖可能被割裂。
2. 分段截取(Segmentation)
- 原理:将长序列按固定长度直接分割为不重叠的子序列(类似 "分块")。
- 示例:将 1000 个词的文本按 200 个词一段,分为 5 段(无重叠)。
- 适用场景:对局部信息依赖较强的任务(如语音识别中的短句分割、长文档的段落级分类)。
- 优点:简单高效,无冗余;
- 缺点:可能切断段落中间的关键依赖(如句子被拆分为两段)。
3. 滚动生成(Rolling Generation)
- 原理:对超长序列,每次用前序子序列的输出作为 "记忆",辅助处理下一段子序列(类似人类 "分段阅读并记忆上下文")。
- 示例:用 RNN 处理 10000 词文本时,先处理前 1000 词并保存隐藏状态,再用该隐藏状态初始化模型,处理接下来的 1000 词,以此类推。
- 适用场景:长文本生成(如小说续写)、实时数据流处理(如股票实时行情)。
- 优点:可处理无限长序列,保留长期记忆;
- 缺点:误差可能累积(前序处理的偏差会影响后续结果)。
4. 关键信息采样
- 原理:对超长序列,只抽取关键部分(如摘要、峰值点),忽略冗余信息。
- 示例:在长文本中提取关键词或句子组成短序列;在高频时间序列中保留峰值和谷值点。
- 适用场景:对全局趋势而非细节敏感的任务(如长文档摘要、异常检测)。
- 优点:大幅降低序列长度,保留核心信息;
- 缺点:可能丢失重要细节,依赖有效的采样策略。
读取长序列数据的核心挑战
- 平衡长度与信息保留:拆分过短会丢失上下文,过长则增加计算负担。
- 处理时序断裂:拆分点可能位于关键依赖处(如句子中间、事件转折点),导致语义割裂。
- 动态适配模型:不同模型(如 RNN 对长距离依赖敏感,Transformer 对局部依赖更高效)需匹配不同的拆分策略。
随机抽样(Random Sampling)
1.核心原理
- 用固定长度的 "窗口" 在长序列上随机滑动,截取不重叠(或少量重叠)的子序列作为样本。
- 子序列的起始位置随机打乱,打破原始序列的连续性,降低样本间的相关性。
2.具体操作
- 设定窗口长度(
num_steps
)和批量大小(batch_size
)。- 从序列中随机选择起始点,生成多个子序列,组成批量数据。
- 标签为子序列向右偏移 1 位的结果(预测下一个元素)。
示例
对序列
[0,1,2,...,34]
,用窗口长度 5 随机抽样,可能得到子序列:
[3,4,5,6,7]
、[18,19,20,21,22]
、[10,11,12,13,14]
3.优缺点
- 优点 :样本随机性高,训练时梯度波动小,适合并行计算。
- 缺点:破坏长距离时序依赖(如子序列前后的关联被割裂)。
4.适用场景
对长期依赖要求不高的任务(如文本分类、短期时间序列预测)。
顺序分区(Sequential Partitioning)
1.核心原理
- 将长序列按固定长度分割为连续的子序列,保留原始时序顺序,子序列间可连续拼接。
- 按 "批次" 划分序列:先将序列均匀分为
batch_size
个连续片段,再从每个片段中按顺序截取子序列。2.具体操作
- 设定窗口长度(
num_steps
)和批量大小(batch_size
)。- 将序列分为
batch_size
个并行的连续子序列(如序列分为 2 段:[0,1,...,17]
和[18,19,...,34]
)。- 从每个子序列中按顺序截取窗口,组成批量(确保同批次样本在原始序列中位置对齐)。
示例
对序列
[0,1,2,...,34]
,分 2 个批次,窗口长度 5,可能得到:
- 第 1 批:
[0,1,2,3,4]
和[18,19,20,21,22]
- 第 2 批:
[5,6,7,8,9]
和[23,24,25,26,27]
3.优缺点
- 优点 :保留时序连续性,适合捕捉长期依赖(子序列可拼接为完整原始序列)。
- 缺点:样本相关性高,训练时梯度可能震荡(同批次样本来自相邻区域)。
4.适用场景
对时序依赖敏感的任务(如语言生成、长文本翻译、长期时间序列预测)。
4种核心方法的区别
方法 核心逻辑 关键特点 典型场景 随机抽样 随机截取子序列,打破顺序 随机性高,丢失长期依赖 文本分类、短期预测 顺序分区 连续截取子序列,保留顺序 时序完整,样本相关性高 语言生成、长期预测 滑动窗口 重叠截取,保留局部上下文 平衡信息与效率 语音识别、段落理解 滚动生成 迭代处理,延续隐藏状态 支持无限序列,误差累积 实时数据流、超长文本处理
完整代码
python
"""
文件名: 8.4 读取长序列数据
作者: 墨尘
日期: 2025/7/14
项目名: dl_env
备注: 实现长序列数据的两种读取方式(随机抽样和顺序分区),将超长序列拆分为小批量子序列,
适配序列模型的训练需求
"""
import random
import torch
import collections # 备用:用于统计(本代码未直接使用)
import re # 备用:用于文本处理(本代码未直接使用)
from d2l import torch as d2l # 提供辅助功能(本代码未直接使用)
# 手动显示图像相关库(本代码未涉及绘图,仅保留配置)
import matplotlib.pyplot as plt
import matplotlib.text as text
# -------------------------- 1. 随机抽样读取长序列 --------------------------
def seq_data_iter_random(corpus, batch_size, num_steps): #@save
"""
使用随机抽样生成小批量子序列(打破原始序列顺序,适合并行训练)
参数:
corpus: 长序列数据(1D列表或数组,如[0,1,2,...,34])
batch_size: 批量大小(每个批次包含的样本数)
num_steps: 每个子序列的长度(模型单次处理的序列长度)
生成器返回:
X: 输入子序列(批量),形状为(batch_size, num_steps)
Y: 标签子序列(批量),形状为(batch_size, num_steps),其中Y[i]是X[i]向右偏移1位的结果
"""
# 步骤1:随机偏移起始位置,避免总是从序列开头抽样(增加随机性)
# 偏移范围为[0, num_steps-1],确保初始偏移不超过子序列长度
corpus = corpus[random.randint(0, num_steps - 1):]
# 步骤2:计算可生成的子序列总数
# 减1是因为Y需要比X右移1位(最后一个元素没有标签)
num_subseqs = (len(corpus) - 1) // num_steps # 整数除法,确保子序列完整
# 步骤3:生成所有子序列的起始索引
# 从0开始,每隔num_steps取一个索引(如num_steps=5时,索引为0,5,10,...)
initial_indices = list(range(0, num_subseqs * num_steps, num_steps))
# 步骤4:随机打乱起始索引(核心:打破原始序列的顺序,避免样本相关性过高)
random.shuffle(initial_indices)
# 辅助函数:根据起始索引pos,返回长度为num_steps的子序列
def data(pos):
return corpus[pos: pos + num_steps]
# 步骤5:按批量生成样本
num_batches = num_subseqs // 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:每个元素是长度为num_steps的子序列
X = [data(j) for j in initial_indices_per_batch]
# 生成标签Y:每个元素是X中对应子序列右移1位的结果(预测下一个元素)
Y = [data(j + 1) for j in initial_indices_per_batch]
# 返回当前批次的X和Y(转换为张量)
yield torch.tensor(X), torch.tensor(Y)
# -------------------------- 2. 顺序分区读取长序列 --------------------------
def seq_data_iter_sequential(corpus, batch_size, num_steps): #@save
"""
使用顺序分区生成小批量子序列(保留原始序列顺序,适合捕捉长期依赖)
参数:
corpus: 长序列数据(1D列表或数组)
batch_size: 批量大小
num_steps: 每个子序列的长度
生成器返回:
X: 输入子序列(批量),形状为(batch_size, num_steps)
Y: 标签子序列(批量),形状为(batch_size, num_steps),Y是X右移1位的结果
"""
# 步骤1:随机偏移起始位置(与随机抽样类似,增加随机性)
offset = random.randint(0, num_steps)
# 步骤2:计算有效序列长度(确保能被batch_size整除,便于均匀分区)
# 总有效长度 = ((原始长度 - 偏移 - 1) // batch_size) * batch_size
# 减1是因为Y需要右移1位,确保X和Y长度相同
num_tokens = ((len(corpus) - offset - 1) // batch_size) * batch_size
# 步骤3:生成输入X和标签Y,并重塑为(batch_size, 总长度//batch_size)
# X:从偏移开始,取num_tokens个元素
Xs = torch.tensor(corpus[offset: offset + num_tokens])
# Y:比X右移1位,同样取num_tokens个元素
Ys = torch.tensor(corpus[offset + 1: offset + 1 + num_tokens])
# 重塑为二维:每行是一个样本,列数为总长度//batch_size
Xs, Ys = Xs.reshape(batch_size, -1), Ys.reshape(batch_size, -1)
# 步骤4:按顺序生成批次(保留序列顺序)
num_batches = Xs.shape[1] // num_steps # 总批次数 = 每行长度 // num_steps
for i in range(0, num_steps * num_batches, num_steps):
# 从每行中截取第i到i+num_steps列,作为当前批次的输入X
X = Xs[:, i: i + num_steps]
# 对应的标签Y(同样截取,与X对齐)
Y = Ys[:, i: i + num_steps]
yield X, Y
# -------------------------- 3. 测试两种读取方法 --------------------------
if __name__ == '__main__':
# 生成测试序列:0到34(长度35的序列)
my_seq = list(range(35))
print("测试序列:", my_seq)
print("序列长度:", len(my_seq))
# 超参数:批量大小=2,每个子序列长度=5
batch_size = 2
num_steps = 5
# 测试1:随机抽样读取
print("\n===== 随机抽样生成的批量 =====")
for X, Y in seq_data_iter_random(my_seq, batch_size, num_steps):
print("X(输入):")
print(X)
print("Y(标签,X右移1位):")
print(Y)
print("-" * 50)
# 只打印3个批次(避免输出过长)
break
# 测试2:顺序分区读取
print("\n===== 顺序分区生成的批量 =====")
for X, Y in seq_data_iter_sequential(my_seq, batch_size, num_steps):
print("X(输入):")
print(X)
print("Y(标签,X右移1位):")
print(Y)
print("-" * 50)
# 只打印3个批次
break
实验结果
第一次运行结果

第二次运行结果
