PyTorch Dataset与DataLoader实战:NLP文本分类数据管道
2026/9/7 19:30:20 网站建设 项目流程

我在 PyTorch 新手答疑里碰到的最多的一类问题,不是模型结构写不出来,也不是 loss 不下降,而是数据从原始文本到 tensor 的那一段完全失控:报错五花八门,要么维度对不上,要么一轮训练慢到怀疑人生。这中间最核心的两个角色就是 Dataset 和 DataLoader。说实话,这俩类本身并不复杂,但因为它俩夹在“数据准备”和“模型训练”之间,很多人要么只抄了一段能用就完事,要么压根没搞懂内部协作方式,一旦数据形态稍微变化,整条链路就崩了。这篇文章我会用 NLP 文本分类场景做主线,从数据组织开始,把这两个类掰开揉碎,最终给出一份可以直接复制运行的代码。看完你应该能理解,为什么 Dataset 只负责返回单条样本,而 padding、batch、打乱这些操作统统要交给 DataLoader 和 collate_fn 来做。

1. 数据管道是训练流程里真正的隐形瓶颈

1.1 Dataset 和 DataLoader 的分工边界

很多初学者会把 Dataset 当成“数据容器”,觉得只要把数据塞进去就行,这种理解其实会踩坑。从设计上看,Dataset 更像是一份“数据契约”:它定义了一共有多少条样本,以及通过下标能取出什么内容。DataLoader 则是训练循环隔壁的“调度中心”,它负责按照你设定的策略去 Dataset 里取样本、拼批次、打乱顺序、并行加载,然后把符合模型输入的 tensor 送到训练循环里。

举个例子,你有一批中文评论,每条评论对应一个情感标签。Dataset 要做的事情是:你给我第 i 条,我就把第 i 条文本和标签生成一个可用的样本返回给你。DataLoader 要做的事情则是:我要以多大的 batch 去取,取的时候要不要先打乱顺序,要不要用多个进程并行取,取出来之后怎么把一堆长度不一致的样本拼成一个 batch。你可以把 Dataset 想成“货架”,DataLoader 想成“分拣流水线”。货架负责告诉你第几个格子放的是什么,流水线负责按订单把货物组合打包,再送到 GPU 这个“客户”手上。

这个设计最大的好处是解耦。你换一种数据格式,只改 Dataset,不碰 DataLoader;你想改训练批次策略,只调 DataLoader 参数,不用重新写数据读取逻辑。真实项目里数据来源千奇百怪,JSON、CSV、数据库、Excel 都有,但训练流程基本都长一个样,这正是上层的训练代码可以一直稳定的原因。

1.2 为什么说 DataLoader 的调度策略直接影响训练结果

很多人觉得 DataLoader 只是“把数据一批批送进去”,没有技术含量。但实际训练时,数据加载的速度如果跟不上 GPU 计算的速度,GPU 就会空转,整个训练时长可能拉长数倍。反过来,如果数据打乱的策略不合理,模型学到的分布就会偏,比如按时间排序的语料不 shuffle 直接训练,模型会在短时间内集中看到某类话题,导致梯度方向不够稳定。

除了性能,DataLoader 还承担了“把单条样本变成批量张量”的关键转换。在 NLP 里,一条样本经过 tokenizer 之后通常是一个长度不定的 token id 列表,而一个 batch 必须是规整的二维张量,所以必须做 padding。这个操作放哪里做、怎么做,会影响内存占用、显存利用率,甚至模型最终效果。可以说,Dataset 决定数据“对不对”,DataLoader 决定数据“稳不稳、快不快”。

2. 从零实现一个 Dataset:三个方法背后的逻辑

2.1 为什么必须实现lengetitem

PyTorch 的 Dataset 是一个抽象类,你继承它之后,最低限度要实现两个方法:__len____getitem____len__返回数据集的样本总数,DataLoader 需要靠它知道采样范围。__getitem__接收一个整数下标,返回该下标对应的样本。这个返回内容没有强制规定,可以是一个 tuple、一个字典,也可以是任意 Python 对象,只要你的下游逻辑能处理就行。

这里有个容易忽略的点:__getitem__在训练过程中会被反复调用,而且当你设置num_workers大于 0 时,它会被多个子进程并行调用。所以不要在__getitem__里做太重的操作,比如每次都从磁盘重新读大文件、每个样本都做复杂正则清洗,这些都应该在 Dataset 初始化阶段提前处理好,或者用缓存机制把结果存下来。数据读取逻辑写得好不好,在单条样本上感觉不出来,一旦跑几万步训练,差距就非常明显。

2.2 一个最简单的文本分类 Dataset 骨架

先看一份基础代码,我不引入任何外部预训练模型,字典也可以自己构造,这样新手能完全跑通。

import torch from torch.utils.data import Dataset, DataLoader class TextClsDataset(Dataset): def __init__(self, texts, labels, vocab, max_len=64): self.texts = texts self.labels = labels self.vocab = vocab self.max_len = max_len def __len__(self): return len(self.texts) def __getitem__(self, idx): text = self.texts[idx] label = self.labels[idx] # 简单按字符切分,中文场景下可以先这样处理 tokens = list(text)[:self.max_len] token_ids = [self.vocab.get(t, self.vocab["[UNK]"]) for t in tokens] return { "input_ids": token_ids, "length": len(token_ids), "label": torch.tensor(label, dtype=torch.long) }

这里我刻意没有在__getitem__里做 padding,而是返回了原始的 token id 列表和真实长度。原因后面会说,NLP 批处理的标准做法是回到 DataLoader 里通过collate_fn做动态 padding。

vocab是一个字典,负责把字符映射成数字编号。实际项目中,这个 vocab 要么从训练语料构建,要么用预训练模型自带的词表。我这里用最原始的方式,是为了让你理解 tokenizer 并不是什么神秘操作,它本质上就是“查字典拿到 id”。

2.3 为什么返回字典比返回 tuple 更稳妥

新手写__getitem__的时候,最喜欢返回(tokens, label)这种元组,代码确实简洁。但在项目稍微复杂之后,我强烈建议返回字典。原因有三个:第一,字典自带字段名,你自己过一段时间回头看代码,不用去数batch[0]到底是 input_ids 还是 token_type_ids;第二,在collate_fn里按 key 处理更清晰,不容易出错;第三,后续要给样本增加字段,比如增加 token_type_ids、特殊标记,字典只需要加一个 key,而元组要改动所有取下标的地方,非常容易漏。

我自己踩过这个坑。之前写一个序列标注项目时,样本返回的是(input_ids, attention_mask, labels),后来要加一个 segment_ids 字段,结果在 DataLoader 的回调里漏改了一处,训练时张量拼接直接报错,排查了半天才发现是字段顺序对不上。从那以后,我所有 Dataset 的样本统一返回字典,再也没犯过这类低级问题。

3. DataLoader 参数精讲:每个参数背后都是一次工程取舍

3.1 batch_size 不只是数值大小问题

batch_size决定了一次梯度更新用多少样本。它直接影响显存占用和训练的梯度噪声。batch_size 太小,比如 1,梯度方向非常不稳定,loss 曲线会剧烈震荡;batch_size 太大,显存可能不够,而且大 batch 在训练初期容易收敛到尖锐极小值,泛化性不一定好。

选择的时候有个常见的工程原则:在显存允许的范围内,优先把 batch_size 设成 2 的幂次,比如 16、32、64。原因一方面是和底层矩阵库的优化对齐,另一方面是很多模型代码内部做了维度假设,比如多头注意力要求 batch 维和序列维能正确拆分。实际训练时,我是先用一个小 batch 测试通流程,然后慢慢增大,直到 OOM 再退回一档。不要一开始就设一个很大的值,不然报错之后还要回头排查到底是模型问题还是显存问题。

还有一点,drop_last和 batch_size 是配套的。当数据集总数不能被 batch_size 整除时,最后一个 batch 会偏小。训练时我一般设drop_last=True,避免最后一个小 batch 对梯度更新造成干扰;但验证集和测试集必须设drop_last=False,因为你不能为了凑整丢掉样本,那样评估指标就不完整了。

3.2 shuffle、sampler、drop_last 之间的协作逻辑

DataLoader 内部决定“该取哪些样本”的不是 batch_size 本身,而是一个采样器 sampler。默认情况下,如果你设shuffle=True,DataLoader 会创建一个RandomSampler;设shuffle=False,则创建SequentialSampler。这个 sampler 负责生成样本下标的序列,然后 DataLoader 再按 batch_size 把这些下标切分成一个个小批次。

这里有一条重要规则:如果你自己传入sampler,那么shuffle参数不能再设置,因为 sampler 已经决定了下标的顺序,再来一个 shuffle 就语义冲突了。如果你使用batch_sampler,那么batch_sizeshuffledrop_last也不能再设置,因为batch_sampler本身已经产出完整的 batch 下标序列。

我建议新手先用默认的 sampler,不要着急自定义。默认逻辑已经覆盖了绝大多数需求:训练集打乱,验证集不打乱。等你真正遇到需要特殊采样策略的时候,再去研究自定义 sampler。比如样本类别不平衡,你要做类别均衡采样,或者处理超长文本时想按长度分组,这种场景才需要自己写 sampler。

3.3 num_workers 和 pin_memory 到底提升了什么

这两个参数是性能优化的关键。num_workers表示用几个子进程去并行调用 Dataset 的__getitem__。设置大于 0 后,DataLoader 会在后台预取数据,让 GPU 在计算当前 batch 的同时,CPU 已经在准备下一个 batch,从而减少等待时间。

我刚入门时图省事,全用默认值 0,结果一个小模型在 GPU 上计算只要 50 毫秒,但数据加载要 200 毫秒,GPU 利用率一直上不去。后来把num_workers调成 4,训练速度提升非常明显。不过这个参数不是越大越好,worker 太多会占用大量内存,还可能把 CPU 打满,导致其他进程卡顿。我的建议是先设 4,然后观察 CPU 利用率和 GPU 利用率,如果 GPU 还是经常空闲,再往上加。

pin_memory=True在 GPU 训练时建议直接开启。它的作用是让 DataLoader 在 CPU 端分配张量时使用页锁定内存,这样 CPU 到 GPU 的拷贝速度会更快。这个参数对训练性能的提升不如 num_workers 那么直观,但是一个无脑开关,开了只有好处没有坏处。

有个 Windows 下的典型问题:在 Windows 上使用多进程数据加载时,代码必须放在if __name__ == "__main__":里面,否则会无限递归创建子进程。Linux 下因为进程模型不同,通常没这个限制。如果你在 Windows 上跑训练遇到“RuntimeError: An attempt has been made to start a new process before the current process has finished its bootstrapping phase”,大概率就是这个问题。

4. NLP 实战:从原始文本到可训练批次的完整链路

4.1 准备一条极简训练数据

为了让代码不依赖任何外部下载,我用一组非常小的中文评论数据。这组数据只是示例,实际项目里你完全可以替换成自己的语料。

texts = [ "这家酒店的早餐很好吃,值得推荐。", "房间隔音效果一般,晚上有点吵。", "前台服务态度很好,退房也很快。", "地理位置不错,交通方便,会再来住。", "性价比很高,就是被子有点薄。", "床垫太硬了,睡得不太舒服。", ] labels = [1, 0, 1, 1, 1, 0]

labels 里1表示正面评价,0表示负面评价。这个小数据集一共 6 条,正好可以测试完整流程。

4.2 手写一个微型 vocab

NLP 中把文本变成数字最直接的方法就是建一个字典。中文场景里,按字切分是常见的做法,因为中文词边界不明显,按字切分可以避免分词器带来的额外误差,也能简化逻辑。

def build_vocab(texts): vocab = {} vocab["[PAD]"] = 0 vocab["[UNK]"] = 1 for text in texts: for ch in text: if ch not in vocab: vocab[ch] = len(vocab) return vocab vocab = build_vocab(texts) print(vocab)

这里我把[PAD]固定为 0,[UNK]固定为 1,后面所有 padding 都用 id 0,未知字符用 id 1。这个约定非常重要,尤其是后面计算CrossEntropyLoss时,padding 部分不能参与 loss 计算,我们要靠 attention mask 把它遮住,而 mask 的生成往往就依赖[PAD]id 是 0 这个事实。

我见过有人把 vocab 的 id 顺序搞乱,[PAD]不是 0,结果 embedding 层里 padding 位置的向量参与了更新,模型行为变得很怪。虽然可以通过 mask 解决,但最省事的办法就是从一开始固定 PAD token 的 id。

4.3 实现核心 Dataset 类

沿用第 2 节的思路,我来实现一个专门针对这个任务的 Dataset。它在__getitem__里做字符到 id 的转换,并返回字典。

class TextDataset(Dataset): def __init__(self, texts, labels, vocab, max_len=32): self.texts = texts self.labels = labels self.vocab = vocab self.max_len = max_len def __len__(self): return len(self.texts) def __getitem__(self, idx): text = self.texts[idx] label = self.labels[idx] tokens = list(text)[:self.max_len] token_ids = [self.vocab.get(ch, self.vocab["[UNK]"]) for ch in tokens] return { "input_ids": token_ids, "length": len(token_ids), "label": torch.tensor(label, dtype=torch.long) }

注意input_ids这里还是一个普通列表,length记录这个样本的真实 token 数量。在collate_fn里,我会根据length做动态 padding。你可能会问,为什么不直接在__getitem__里 pad 到max_len?因为如果一条文本只有 10 个字,却 pad 到 32,那么 22 个位置都是无效计算,在小 batch 里不明显,在大规模语料里就是巨大的浪费。

4.4 collate_fn:NLP 批处理的关键回调

DataLoader 默认的 collate 函数只会处理长度相同的张量。当你一批样本里的input_ids长度不一致时,它会直接报错。所以我们必须自定义collate_fn,它的作用是把一个 batch 的样本列表整合成一个可供模型输入的大张量。

def collate_batch(batch): token_ids_list = [item["input_ids"] for item in batch] lengths = [item["length"] for item in batch] labels = torch.stack([item["label"] for item in batch]) batch_size = len(batch) max_len = max(lengths) input_ids = torch.zeros((batch_size, max_len), dtype=torch.long) attention_mask = torch.zeros((batch_size, max_len), dtype=torch.long) for i, ids in enumerate(token_ids_list): input_ids[i, :len(ids)] = torch.tensor(ids, dtype=torch.long) attention_mask[i, :len(ids)] = 1 return { "input_ids": input_ids, "attention_mask": attention_mask, "labels": labels, }

我把attention_mask在真实 token 位置置为 1,padding 位置保持为 0。这正好对应 Transformer 里常见的 mask 语义:1 表示模型需要关注的位置,0 表示忽略。如果你的模型用的是src_key_padding_mask,可能还需要把 0 转成 True、1 转成 False,具体看模型实现。

这个collate_fn是 NLP 批次处理的核心。不管你是用 BERT 还是用自建 Transformer,也不管你是做分类、序列标注还是文本匹配,批量数据的形态都离不开这套“动态 padding + attention mask”的组合。

4.5 创建 DataLoader 并验证输出形状

现在把 Dataset 和 DataLoader 组装起来,跑通一次迭代:

dataset = TextDataset(texts, labels, vocab, max_len=32) dataloader = DataLoader( dataset, batch_size=2, shuffle=True, collate_fn=collate_batch, num_workers=0, ) for batch in dataloader: print(batch["input_ids"].shape) # 期望 [2, L] print(batch["attention_mask"].shape) # 期望 [2, L] print(batch["labels"].shape) # 期望 [2] break

这里L是该 batch 内最长样本的 token 数量,不一定是 32。因为我们做的是动态 padding,每个 batch 的L都可能不同。这是正常现象,不要惊讶。

如果shuffle=True,你每次迭代时样本顺序都会变化;验证集则设shuffle=False,保证评估时顺序稳定。

4.6 把管道接进一个极简模型

最后,我把这个 DataLoader 接到一个非常小的 Transformer 分类模型上,验证整条链路能端到端跑通。

import torch.nn as nn class TinyTransformerCls(nn.Module): def __init__(self, vocab_size, hidden_dim=64, num_classes=2): super().__init__() self.embedding = nn.Embedding(vocab_size, hidden_dim, padding_idx=0) encoder_layer = nn.TransformerEncoderLayer( d_model=hidden_dim, nhead=4, batch_first=True, dim_feedforward=128, ) self.encoder = nn.TransformerEncoder(encoder_layer, num_layers=2) self.classifier = nn.Linear(hidden_dim, num_classes) def forward(self, input_ids, attention_mask): x = self.embedding(input_ids) # src_key_padding_mask 为 True 的位置表示忽略 padding_mask = attention_mask == 0 out = self.encoder(x, src_key_padding_mask=padding_mask) return self.classifier(out.mean(dim=1)) model = TinyTransformerCls(len(vocab)) optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) loss_fn = nn.CrossEntropyLoss() for epoch in range(2): for batch in dataloader: logits = model(batch["input_ids"], batch["attention_mask"]) loss = loss_fn(logits, batch["labels"]) optimizer.zero_grad() loss.backward() optimizer.step() print(loss.item())

这段代码能在 CPU 上直接跑通。虽然数据量很小,但你已经能看到 Dataset、DataLoader、collate_fn、模型训练是怎么串联起来的。以后换成真实语料,只需要把数据读入方式换掉,主体管线都不用大改。

5. 进阶技巧与避坑指南

5.1 动态 padding 与静态 padding 的取舍

collate_fn里做动态 padding,是在 batch 内取最大长度,而不是把所有样本都 pad 到预先设定的max_len。这样做的好处很明显:当语料长度差异大时,短文本多的 batch 计算量会小很多。比如一批 32 条样本里,实际长度大多在 10 到 20 之间,少数是 30,静态 padding 到 64 就会造成大量无效计算。

如果使用 HuggingFace 的 transformers 库,把padding=True写在 tokenizer 调用里,也是在当前 batch 内动态 padding,原理和我这里的collate_fn是一样的。新手最容易犯的错误是提前把每条文本 pad 到固定长度,然后训练时又做一遍 padding,双重浪费。

当然,动态 padding 也有代价:每个 batch 长度不一样,batch 内的计算量不断变化,对显存规划不太友好。某些需要固定 shape 的推理框架要求所有输入长度一致,这时候你可能要改成静态 padding。但在普通训练场景,动态 padding 通常是更优解。

5.2 用 Sampler 做按长度分桶,减少 padding 浪费

既然动态 padding 只取 batch 内最大长度,如果一批样本长度差异悬殊,比如最短的 5 个 token,最长的 100 个 token,那么短样本会 pad 到 100,浪费依然严重。更好的做法是让同一批内的样本长度尽量接近,这就是 length bucketing 的思路。

实现方式一般是自定义 Sampler。我举一个简化版思路:先按样本长度排序,然后在排序后的下标序列里,每隔 batch_size 个取一组。这样同一个 batch 的长度会比较接近。但完全排序会破坏随机性,所以更常见的做法是做一个“半随机”分桶:把样本按长度分成若干桶,然后在每个桶内随机取样本。

下面是一个基础实现,核心是把长度接近的样本尽可能放进同一个 batch:

from torch.utils.data import Sampler class BucketSampler(Sampler): def __init__(self, lengths, batch_size, shuffle=True): self.lengths = lengths self.batch_size = batch_size self.shuffle = shuffle def __iter__(self): indices = list(range(len(self.lengths))) if self.shuffle: # 先随机打乱一次,降低完全排序带来的偏差 random.shuffle(indices) indices.sort(key=lambda i: self.lengths[i]) batches = [] for i in range(0, len(indices), self.batch_size): batch = indices[i:i + self.batch_size] if len(batch) == self.batch_size: batches.append(batch) if self.shuffle: random.shuffle(batches) for batch in batches: yield batch

注意这里产生的是“batch 下标”,所以你要用DataLoader(..., batch_sampler=BucketSampler(...))而不是batch_size参数。这是 NLP 训练中一个很实用的优化,在文本长度差异大的语料里能显著提升训练速度。

5.3 多进程加载时的内存与稳定性问题

num_workers是一个能够显著提升性能,却也最容易引入问题的参数。进程多了,每个 worker 都会复制一份 Dataset 的引用,如果 Dataset 在初始化阶段加载了巨大的词向量矩阵或全量文本,那么 worker 数量翻倍,内存占用也可能翻倍。我见过一个项目,数据集在__init__里把 20GB 的词向量读进内存,然后开了 8 个 worker,服务器直接 OOM。

对策有三个:第一,控制num_workers的数量,不要盲目堆高;第二,把大对象放到只读共享内存,或者利用 PyTorch 的共享内存机制;第三,最稳妥的做法是把耗时的预处理离线完成,比如提前把所有文本转成 token id 并保存成 npy 或内存映射文件,训练时 Dataset 只负责读取内存数组,速度飞快。

还有一个细节,如果你在 Dataset 的__getitem__里调用了 HuggingFace 的 tokenizer,建议显式设置环境变量TOKENIZERS_PARALLELISM=false,避免多进程内部再并行,导致线程爆炸和 CPU 竞争。

5.4 验证集到底要不要 shuffle

很多人会在所有 DataLoader 上都设shuffle=True,这是不对的。验证集和测试集的评估结果应该稳定、可复现,如果你把验证集顺序打乱,两次评估虽然指标差异通常不大,但会让问题定位变得困难。尤其是当你想对比两个模型的输出差异时,顺序不稳定会干扰判断。

所以我的习惯是:训练 DataLoader 设shuffle=True,验证和测试 DataLoader 设shuffle=False。如果验证集还要做数据增强,那另说,但常规评估下不建议打乱。另外,验证集也不建议开num_workers=0,除非数据量极小,否则同样可以设置 worker 数量来加速评估。

6. 常见错误速查表与排查技巧

6.1 高频报错对照表

错误信息常见原因解决办法
stack expects each tensor to be equal sizebatch 内样本长度不一致,且没有自定义 collate_fn写 collate_fn 做 padding
IndexError: list index out of range__getitem__返回的 id 超出 vocab 范围,或数据列表长度不匹配检查 vocab 构建,检查 Dataset 初始化数据
TypeError: default_collate: batch must contain tensors默认 collate 无法处理你返回的字典或变长列表自定义 collate_fn
RuntimeError: CUDA out of memorybatch_size 过大或 max_len 过大降低 batch_size,或者用动态 padding 减少无效 token
DataLoader worker is killed by signal数据加载进程内存超限降低 num_workers,或把大对象改成共享内存
dataloader workers are exiting unexpectedlyWindows 下没有把训练代码放在if __name__ == "__main__"加上主模块保护
Too many open files每个 worker 都打开了大量文件句柄torch.multiprocessing.set_sharing_strategy('file_system'),或者关闭多余的文件流

这个表我建议直接收藏。实际项目里很多报错来来回回就是这几类,尤其是 NLP 场景下,绝大多数都是变长序列处理不当导致的。

6.2 快速定位是数据问题还是模型问题

模型训练发生异常时,第一反应不应该是改网络结构,而是先判断问题出在数据管道还是模型代码。我常用一个很简单的测试方法:先跑一个空转的训练循环,只迭代 DataLoader,不反向传播,看能否稳定跑完。

for batch in dataloader: input_ids = batch["input_ids"] attention_mask = batch["attention_mask"] labels = batch["labels"] print(input_ids.shape, attention_mask.shape, labels.shape)

如果这一步已经报错,说明问题在数据管道;如果能跑通,再接入模型 forward 和 loss。这样可以把问题一分为二,排查范围立刻缩小。

另外一个实用的调试技巧是检查 DataLoader 返回的张量是否存在异常值,比如input_ids出现负数或者超大值,那大概率是 vocab 构建或者 tokenizer 映射出了问题;labels维度与 loss 函数不匹配,则要检查num_classes的配置。数据管道是整个训练过程的底层,它不炸不代表没问题,但一旦炸,你的第一优先级永远是先把它修稳。

6.3 用计时器量化数据加载耗时

很多人说训练慢,但说不清慢在哪里。我建议在训练循环外简单包一层计时,对比一下模型计算时长和数据加载时长。如果是数据加载占了大部分时间,就要考虑调整 num_workers、优化 Dataset 的预处理逻辑,或者做一个简单的缓存。如果模型计算本身就很慢,那再回头优化模型结构。

最简单的做法:

import time start = time.perf_counter() for batch in dataloader: data_load_time = time.perf_counter() - start start = time.perf_counter() # forward 和 backward logits = model(...) forward_time = time.perf_counter() - start

在正式训练前,先用几个 batch 打印这些耗时,能做到心里有数。这个习惯能帮助你避开很多性能陷阱,尤其是在数据集很大的时候,提前优化数据管道比事后再改要省心得多。

从我自己的经验来看,Dataset 和 DataLoader 的学习曲线并不长,但非常重要。很多人花大量时间研究模型结构,却忽视了数据管道的规范性。结果是换一个数据集、换一种任务类型,代码就崩得千奇百怪。把这两个类的设计意图吃透,再结合 NLP 场景特有的变长序列、padding、mask 处理,后续你再接触任何 PyTorch 项目,都会觉得数据这一环特别稳。你在自己项目里做数据工程时,如果发现训练效果不稳定,先回头看一眼数据管道,八成能找到原因。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询