一文读懂循环神经网络(RNN)—语言模型+读取长序列数据(2)
目录
读取长序列数据
为什么需要 “读取长序列数据”?
读取长序列数据的核心方法
1. 滑动窗口(Sliding Window)
2. 分段截取(Segmentation)
3. 滚动生成(Rolling Generation)
4. 关键信息采样
读取长序列数据的核心挑战
随机抽样(Random Sampling)
1.核心原理
2.具体操作
3.优缺点
4.适用场景
顺序分区(Sequential Partitioning)
1.核心原理
2.具体操作
3.优缺点
4.适用场景
4种核心方法的区别
完整代码
实验结果
第一次运行结果
第二次运行结果
读取长序列数据
读取长序列数据指的是对超出模型单次处理能力(或固定长度限制)的超长序列进行读取、拆分和预处理的过程。其核心目标是将长序列转换为模型可接受的格式,同时尽可能保留序列中的时序关系和上下文信息。
为什么需要 “读取长序列数据”?
模型输入长度限制: 大多数序列模型(如 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种核心方法的区别
方法 核心逻辑 关键特点 典型场景 随机抽样 随机截取子序列,打破顺序 随机性高,丢失长期依赖 文本分类、短期预测 顺序分区 连续截取子序列,保留顺序 时序完整,样本相关性高 语言生成、长期预测 滑动窗口 重叠截取,保留局部上下文 平衡信息与效率 语音识别、段落理解 滚动生成 迭代处理,延续隐藏状态 支持无限序列,误差累积 实时数据流、超长文本处理
完整代码
"""
文件名: 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_sizeXs, Ys = Xs.reshape(batch_size, -1), Ys.reshape(batch_size, -1)# 步骤4:按顺序生成批次(保留序列顺序)num_batches = Xs.shape[1] // num_steps # 总批次数 = 每行长度 // num_stepsfor i in range(0, num_steps * num_batches, num_steps):# 从每行中截取第i到i+num_steps列,作为当前批次的输入XX = 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,每个子序列长度=5batch_size = 2num_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