新闻详情

新闻详情

首页 / 资讯中心 / 详情

文本编码中的滑动窗口数字采样:从分词到位置编码的工程实践

发布时间:2026/9/29 14:01:32来源:尧图网络
文本编码中的滑动窗口数字采样:从分词到位置编码的工程实践
如果你跟我一样在“从零手搓大模型”这条路上走到文本编码阶段大概率会卡在同一个地方明明 tokenizer 已经能把文本变成 token id 了为什么训练时还要再套一层滑动窗口S07 的文本编码我拆成了好几讲E03 这次专门聊滑动窗口的数字采样——它解决的是“序列长度超过模型容量时怎么把长文本切成一批边界干净、位置明确的数字样本”的问题。这篇东西不做理论复读直接讲我在自己实现过程中怎么设计窗口参数、怎么处理 position id、怎么避开那些看似不起眼但一跑就崩的坑。先说一个我自己的判断文本编码阶段最容易翻车的不是 MLP 或者注意力矩阵而是数据准备层。模型再聪明吃进去的也是整数序列序列怎么切、窗口怎么滑、每个 token 站在哪个位置都会直接决定训练目标的正确性。所以这一讲我尽量把滑动窗口的数字采样讲透。1. 为什么文本编码阶段要先动手做滑动窗口1.1 模型不是吞下全文而是嚼固定长度很多刚开始写代码的人会直觉地认为语言模型是把一整篇文章塞进网络里然后模型自己“理解全文”。真实情况完全不是这样。Transformer 的输入是一个形状固定或者有条件上限的矩阵典型形状是[batch_size, seq_len, hidden_dim]。seq_len就是你给模型的一次性能看到的 token 数量。无论是 512、1024 还是 2048这个数字是超参数不是文本长度。那长文章怎么办答案就是切。把一篇文章从开头到结尾切成一段一段每段长度不超过模型能接受的seq_len然后一段一段喂进去。这个切法如果做得太粗暴比如纯按字符硬切很容易把语义完整的一句话拦腰截断更麻烦的是可能把一个 token 从中间切开导致后面前后文错位。滑动窗口在这个场景里干的活就一句话用一个固定大小的窗口在 token 序列上按固定步长移动每次窗口内的内容生成一个训练样本。窗口像是探照灯照着一段数字序列记录下这一段里所有 token id 和它们各自的绝对位置然后交给模型学习。1.2 滑动窗口到底滑的是什么这里要分清楚滑动窗口滑的是 token 序列不是原始字符串。这是 E03 之所以叫“数字采样”的根本原因。字符串层面你看到的是“你好世界”这样有意义的字符但编码之后你面对的是一个整数数组比如[101, 925, 838, 102]。滑动窗口操作的数组就是这样的整数列表。每次窗口滑过取出来的是一段连续的整数子序列加上这段子序列在原文中的起始位置信息最终组成一条训练数据。所以滑动窗口在工程上等价于“在整数数组上做子序列采样”这个采样过程有三个关键数字窗口大小w每次采多长移动步长s窗口每次往前走多远采样起点offset从第几个 token 开始第一次采样。三个数字一旦确定整篇文章能被切成多少个样本就完全确定不会多也不会少。用公式表达当 token 序列长度为L时滑窗采样的总样本数大约是(L - w) / s 1。这个公式看起来简单但里面藏着一个很隐蔽的问题如果L不是s的整数倍最后的剩余片段怎么处理是丢掉还是补 padding还是把窗口往回退一步我在后面的边界内容里会细说。2. 从原始文本到数字序列编码链路里的采样三要素2.1 分词与词表 id 化先保证“窗口内是可数的数字”滑动窗口的数字采样前提是窗口里“全是数字”。这里的数字指 token id它来自任何一种确定性分词器BPE、SentencePiece、WordPiece 都可以。我自己的实现里用的是 BPE 风格的分词器因为它在中英文混合场景下表现比较稳。分词器干的事分三步先把输入文本按规则切成子串再用词表把子串映射成整数最后拼成 token id 数组。一个典型的处理流程大致是这样原始文本先做规范化Unicode 归一化、大小写处理按字节或字符级别做预分词在预分词结果上跑 BPE 合并找到词表里最合适的子词每个子词查词表得到 id同时记录 id 对应的原文偏移量。这一步很多教程会跳过但我建议你务必把“token id 到原文片段的映射表”一起保存下来。因为后面滑窗采样的样本如果出现问题比如发现某些样本开头少了一个字你必须有办法把 token id 还原成可以肉眼检查的文本否则排查会非常痛苦。2.2 数字 token 的特殊性处理连续数字时的采样策略E03 标题里有“数字采样”除了“把文本变成数字序列”这层意思还有另一层文本里大量出现的阿拉伯数字比如年份、金额、身份证号、版本号它们的 token 化方式非常容易翻车。我卡得最久的一个例子是“2024”。如果用 SentencePiece它可能被切成[▁20, 24]或者[▁202, 4]具体切法取决于词表训练时的统计结果。你无法直接控制它怎么切但你必须知道它怎么切。因为滑动窗口切割的是 token id不是原字符窗口边界很可能落在“2024”中间。如果切出来的前一段以20结尾后一段以24开头模型预测下一个 token 时看到的上下文就被人为切断了数字的完整性会丢失。我在项目里做了一个额外处理分词之后扫描一遍 token id 序列把属于“连续数字片段”的 token 标记出来在滑窗采样时尽量不让窗口边界落到这些标记中间。如果边界必然要落到数字片段里就执行一个小的回退把窗口起点往前挪几个 token直到边界落在句号、空格或非数字 token 上。这个策略不能 100% 避免数字被截断但可以显著减少。模型对年份、价格这类数字的连续性很敏感我用这个逻辑实测过训练 loss 下降曲线比无脑切窗更平稳生成的文本里数字乱码现象也少了很多。2.3 样本样式的“数字”含义采样不是随机抽而是有步长的复制另一个容易误解的点是滑动窗口的数字采样不是随机抽样而是确定性的、有重叠的切片操作。很多做 CV 的朋友会把滑动窗口理解为“把图片抠成 patch”中间有重叠很正常。NLP 的滑窗采样也是类似逻辑同一段 token 可能同时出现在两个样本里只是位置不同。比如窗口大小 1024步长 512那么相邻两个样本之间有 512 个 token 是重叠的。这不是浪费而是刻意的设计。为什么要让样本重叠因为语言模型的训练任务是“给定前文预测下一个 token”。如果窗口完全不重叠每个 token 只能作为“被预测的目标”出现在一个样本里而有了重叠同一个 token 会在不同上下文深度下反复作为预测目标模型对上下文利用的效率会更高尤其是长距离依赖的捕捉效果会更好。这一步在代码层面相当于一次带步长的列表切片samples [tokens[i:iw] for i in range(0, len(tokens)-w, s)]。对于大多数中长文本数据集这样做就够了。真正的复杂度在下面一节。3. 滑动窗口采样工程实现从公式到代码3.1 窗口大小、步长与样本量估算参数怎么定要结合你的模型设置来。假设你的 Transformer 设置了seq_len 1024那窗口大小w通常也取 1024。但要注意w必须小于等于模型能接受的最大序列长度否则前向传播会直接报维度错误。步长s的选择需要权衡。s越大样本越少训练效率高但相邻样本的连续性变弱s越小样本越多重叠越多训练成本上升但样本间关联性更强。我常用的经验值是s w / 2也就是一半重叠。如果你想快速验证模型能不能跑通可以先设s w也就是完全不重叠跑通之后再调参。样本量的估算有一个显而易见的坑。假设一篇文章 token 化后长度为 2048窗口 1024、步长 512理论上能产生(2048 - 1024) / 512 1 3个样本。但是如果你把步长改成 600(2048 - 1024) / 600 1 ≈ 2.70向下取整是 2最后会剩下一段 448 个 token 的尾巴。这段尾巴如果直接丢掉文章结尾的信息就全被浪费了尤其是很多文档的结论恰恰在最后面。我的处理方式是对于剩余尾巴如果长度大于等于w / 4就以len(tokens) - w为起点再采一个尾部窗口如果太短直接丢弃。这样做的理由是长度低于w / 4的尾巴本身信息量有限硬塞进去还要做大量 padding训练时这些 padding 位置会被 mask 掉实际上能学到的东西很少性价比不划算。3.2 位置编码与 attention mask 如何跟着窗口走滑动窗口采样之后光有 token id 不够还得给每个 token 安排位置编码。这地方非常容易出错。如果模型用的是绝对位置编码比如原始 Transformer 里的正弦位置编码或者可学习位置嵌入那么每个样本内部的 position id 通常从 0 开始到w-1结束。注意这里的“0”是窗口内的相对位置不是原文里的绝对位置。但如果你用的是 ALiBi 这类基于相对位置信息的位置编码方案位置信息本身不需要作为额外的 embedding 输入模型会从 token 之间的距离隐式获得位置线索。这种情况下滑动窗口切断的上下文仍然会对远端位置感知造成一定影响但这属于模型结构层面的问题不是采样层能完全解决的。我自己的实现选择是在采样时同时生成三份对齐数组——input_ids窗口内容、position_ids相对位置、attention_mask有效长度标记。三份数组长度完全一致一起送到模型里。计算position_ids的代码很直接position_ids torch.arange(seq_len, dtypetorch.long).unsqueeze(0)如果你的实现里加入了文档分隔符比如用[CLS]和[SEP]包裹每个窗口那么position_ids不需要变化但attention_mask要把 padding 部分置 0同时窗口内特殊 token 对应的位置也要保留。这里有个经验绝对位置编码与滑窗重叠并不矛盾。很多人在直觉上会觉得同一段 token 出现在两个样本里位置编码不同了模型会不会学乱实际上不会。模型学的是 token 和 token 之间的关系而不是 token 和“整个语料里的位置”之间的关系。每个样本内部的相对位置足够支撑注意力计算。真正要避免的是同一个 batch 内不同样本长度不一致导致的计算浪费。3.3 一个可以直接落地的 Python 实现要点这一节给出一个简化但不缺核心逻辑的实现。我假设你已经有一个tokenizer.encode(text)返回 token id 列表的方法。def slide_window_sample(tokens, window_size, stride, min_tail256): tokens: List[int] 整段文本的 token id 序列 window_size: int 窗口大小通常等于模型的 seq_len stride: int 滑动步长 min_tail: int 剩余尾巴小于该值则直接丢弃 samples [] n len(tokens) if n window_size: # 文本比窗口还短直接补 padding 到窗口长度 samples.append(align_to_window(tokens, window_size)) return samples start 0 while start window_size n: samples.append(tokens[start:start window_size]) start stride # 处理尾巴 if n - start min_tail: # 回退窗口起点让尾巴尽量靠后 tail_start n - window_size tail_sample tokens[tail_start:n] # 如果和上一个样本完全相同极端情况则跳过 if tail_sample ! samples[-1]: samples.append(tail_sample) return samples def align_to_window(token_ids, window_size): if len(token_ids) window_size: return token_ids[:window_size] padding [PAD_TOKEN_ID] * (window_size - len(token_ids)) return token_ids paddingalign_to_window的作用是把过短样本补到固定长度。这一步不要直接拿一段文本硬切因为PAD_TOKEN_ID在后续计算 loss 时必须是忽略项。如果你要处理的是超大语料一次性把所有样本读进内存不现实。更合理的做法是做一个生成器边读语料边产出样本产出后直接写进缓存文件或者送给 dataloader。我习惯把每个窗口样本的元数据一起存下来包括sample_id、source_doc_id、start_offset、end_offset这样后续做数据筛选或者定位异常样本时能直接回溯原文。4. 边界情况与数据质量踩坑实录4.1 切坏多字节字符和半个 token滑窗切片本身只在 token 数组上操作理论上不会切出半个 token——因为 token 已经分词结束了。但在某些实现里如果你为了节省时间直接用字符数组做滑窗再扔给 tokenizer 分 token就会遇到严重的边界问题。我犯过的错误是这样的一段中文文本我用 Python 的字符串切片text[i:i256]按字符数切然后对每段单独 token 化。中文的 UTF-8 编码里一个汉字可能占 3 个字节stride 和 window 都是按字符数算的切出来的片段偶尔会从 emoji 或生僻字中间劈开。Python 的 str 切片是按 Unicode 码点切的通常不会出现“半个字符”但如果你的预分词是在字节级别做的比如 byte-level BPE那么输入给 tokenizer 的片段就必须按字节边界对齐否则会出现无法解码的乱码。这个问题的最稳解法永远先对完整文本做 token 化得到 token id 数组之后再在 token id 层面做滑窗。需要还原原文时用 tokenizer.decode 把窗口里的 id 重新转回文本。这样能保证每个样本里的文本都是完整的、可解码的。4.2 窗口重叠区域的处理去重与关键片段保留当步长小于窗口长度时相邻样本之间必然存在重叠区域。重叠本身不是问题但如果你自己在样本里额外拼接了一些特殊 token重叠区域可能会被重复学习很多遍。举个例子你在每个窗口的开头加上[BOS]结尾加上[EOS]。那么同一个[BOS]只属于它所在的那一个窗口没问题。但如果窗口有 50% 重叠中间的那段 token 会先后以“前文的一部分”和“后文的一部分”两种身份出现。这其实是我期望看到的但如果你的语料里某些片段频率天然很高再叠加重叠窗口这些片段在训练集中占比就会畸高训练出来的模型容易局部过拟合。我后来做过一个简单过滤按窗口内容做哈希去重只去掉完全相同且来源相同的样本。对于内容相似但不完全相同的窗口优先保留包含文档标题、开头、结尾的样本中间部分可以适当降采样。这个逻辑没有写进框架里而是数据预处理阶段的一个脚本跑一次就能把样本量压下来不少。4.3 验证采样正确性的对拍方案滑动窗口实现完之后你怎么证明它是对的我很长一段时间靠肉眼检查零散样本直到有一次发现 30% 的样本里文本顺序错乱才知道没写对拍脚本。对拍方案非常简单随机抽一批原始文本先算整篇 token 化后的长度L然后按滑窗参数生成样本最后把所有样本按start_offset排序并拼接起来检查公共部分是否和原 token 数组完全一致。注意拼接的时候要去掉样本之间的重叠区只保留每个样本的“新增部分”也就是从start s到start w这段。如果滑窗逻辑正确这个校验一定会通过一旦校验不通过直接定位到第一个不一致的样本看它的start_offset和相邻样本的接缝处很快就能找到 bug。这个对拍脚本虽然笨但我建议一定保留在仓库里因为后续你改步长、改尾巴策略、改 padding 方式时跑一遍就能防止老代码被无意改坏。5. 从单段采样到整个数据管线的思考5.1 为什么滑动窗口常和长度衰减策略搭配滑窗采样解决了长文本拆分问题但它也会引入一个副作用每个窗口都是从文档中间某个位置硬生生开始没有文档标题、开头和全局结构信息。这对短文档还好对长文档影响明显。在语料预处理里我习惯配合长度衰减策略超长文档的中间部分滑窗采样时降低采样密度比如步长从w/2调整为w开头和结尾部分保持密集采样。这样能保证模型更频繁地见到文档的关键位置也避免中间大段的重复内容占据过多训练数据。实现方式不复杂。在采样时给每个样本打一个region标签head、middle、tail然后按标签做概率过滤。比如head和tail全部保留middle只保留一半。这个策略没有标准答案只能说是针对长文档数据的一种可用方案。5.2 面向推理阶段的滑窗采样以 KV cache 视角看训练阶段用完滑窗推理阶段也会遇到类似的采样问题。长文本输入超出模型窗口时常规做法是滑窗把前文切成几段分多次进入模型但每次都要重新计算前文的 KV cache成本很高。我自己的做法是把滑动窗口和 KV cache 结合起来先以窗口大小读入最前面的一段正常计算并缓存 KV然后把窗口步长往前移把下一段 token 输入模型此时只需要计算新 token 的 KV前一段的 KV 可以直接复用。这个方式在数据结构和训练时的滑窗完全不同但思路同源核心都是“窗口步长”的数字采样。有一个细节值得注意推理阶段复用的 KV cache 只处理左侧上下文输入后半段的 token 不会反过来影响前面 token 的表示这对自回归生成来说是严格的而训练阶段的前向计算是无损双向注意力的二者不要混用。如果你打算在训练代码里直接套用推理的 KV cache 优化训练 loss 会变因为注意力模式已经变了。另外如果你用的是 ALiBi 这类和绝对位置无关的位置编码方案滑窗的起点被推后时模型不会因为“位置 id 从 0 开始”而困惑但如果你用的是可学习位置嵌入推理阶段的滑窗起点如果仍然给 position id 0模型会以为新窗口是全新文档。拐弯抹角的话不多说直接记住绝对位置编码下每个窗口的 position id 要从 0 开始推理阶段复用 KV 时必须对相对位置做偏移处理。5.3 一个我经常用来排查问题的“最小复现法”如果你在滑动窗口的数字采样上遇到问题卡住半天也找不出来我建议你做一个极小的数据集只拿 10 篇文档每篇长度比窗口大 1.5 倍然后跑完整流程原始文本 - 分词 - 滑窗 - 模型前向 - 反解回文本。这样损失函数的数值、样本数量、每一步的张量形状都能手算出来排查范围能压缩得特别小。我在调试 BPE 数字切分问题的时候就干过这件事。最小复现脚本总共不到 100 行但每次改动后只需要盯住几个关键位置窗口边界的 token id、反向解码后的文本、position id 的最大值基本一两轮就能定位问题。顺序别搞反先修数据层再动模型结构因为数据层错误会让模型层的任何努力都失真。我自己在实际项目里最深的感受是滑动窗口这个名词看起来人畜无害真正做得滴水不漏是要花时间的。参数设置、边界处理、数字保持、位置对齐每一个都值得单独写测试。它不产生任何“看起来炫酷”的成果但所有后续模型训练的成功率都压在这些细节上。E03 只要把这些数字采样的逻辑理顺后面做长文本微调、做推理加速、做数据配比都会省下大量返工的力气。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

基于 Docker 的 Thanos 部署:TaoToken 统一 Key 打通 MinIO 与 Prometheus 长期存储 2026/9/29 14:53:56

基于 Docker 的 Thanos 部署:TaoToken 统一 Key 打通 MinIO 与 Prometheus 长期存储

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
GSM信令排查实战:七号信令协议栈与接口拓扑解析 2026/9/29 14:53:56

GSM信令排查实战:七号信令协议栈与接口拓扑解析

简介:GSM网络拓扑结构讲义是一份面向移动通信初学者的PPT课件,系统讲解GSM网络拓扑结构与信令协议体系,适合通信专业学生、网络优化工程师以及需要快速理解移动核心网原理的技术人员自学或教学使用。内容先梳理BSS、NSS、OSS三类子系统&#…

阅读更多 →
MCP SSE 传输详解:双通道设计与异步请求匹配的 Java 落地实践 2026/9/29 14:53:56

MCP SSE 传输详解:双通道设计与异步请求匹配的 Java 落地实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
Linux Swap释放原理与安全实操:从内核页回收到cgroup迁移 2026/9/29 14:53:56

Linux Swap释放原理与安全实操:从内核页回收到cgroup迁移

1. 项目概述:Swap不是“备用内存”,而是内核级的紧急缓冲区Linux里的swap分区,常被新手误读成“内存不够时自动把数据搬过去”的简单扩展盘。这种理解错得离谱——swap本质是内核内存管理子系统(MMU)在物理内存严重短缺…

阅读更多 →
2026 智能降AIGC软件深度测评:TaoToken 统一 Key 接入科研党救急指南 2026/9/29 14:53:56

2026 智能降AIGC软件深度测评:TaoToken 统一 Key 接入科研党救急指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
OpenClaw 对接 Ollama 本地大模型实操教程:TaoToken 统一 Key 配置与连通性验证 2026/9/29 14:53:50

OpenClaw 对接 Ollama 本地大模型实操教程:TaoToken 统一 Key 配置与连通性验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉