☰
用PyTorch搭建LSTM:从零实现IMDB情感分类实战
2026/9/30 15:28:12 网站建设 项目流程

简介:面向NLP初学者与深度学习实践者的PyTorch实战文档,以IMDB影评情感分类为主线,完整演示基于LSTM的训练与评估过程。文档共34页,支持目录章节跳转和阅读器大纲定位,内容涵盖情感分析任务界定、PyTorch与LSTM核心原理、IMDB数据集获取与探索性分析(标签分布、评论长度与情感关系可视化)、数据预处理(填充、划分),以及基于嵌入层、LSTM层、全连接层的完整模型搭建与训练流程。工程实践方面,针对损失函数与准确率监控、混淆矩阵、超参数调优、正则化与早停策略、模型保存与加载展开讲解;评估部分引入准确率、精确率、召回率、F1值及ROC曲线与AUC值,并结合电商评论、社交媒体舆情等场景讨论部署与拓展。全文从问题提出到总结展望形成完整闭环,配套代码思路清晰,适合课程设计、毕业设计或科研入门。包体为单个1.95MB的PDF文件,目前已有74人学习使用。

1. 撞到 IMDB 情感分类这面墙:为什么偏偏是 PyTorch + LSTM

拿到一批 IMDB 英文电影评论,要做情感分析,判定每段评论是正向还是负向——这是 NLP 从业者绕不开的入门场景,也是面试里出现频率最高的项目题。很多人一上来就搬 BERT,结果显卡跑不动、数据量不够、还解释不清。反直觉的结论是:用 PyTorch 搭一个 LSTM 分类器,2 万词的词表加 128 维 Embedding,单卡几分钟就能训到 88% 以上准确率,成本远低于预训练模型,却把 NLP 的全流程都覆盖了:文本清洗、分词、词表构建、序列建模、训练调参、模型落盘和推理。下面按一条可复现的路径走,从环境安装讲到推理封装。适合刚学完 PyTorch 基础、想拿完整项目练手的读者,也适合需要快速上线二分类文本任务的工程师照着改,中间会把常见翻车点单独拉出来,每条都能对号入座。

2. 环境与数据先行:PyTorch 安装、CUDA 验证和 IMDB 数据准备

2.1 用 Anaconda 建独立环境:Python 版本与 PyTorch 安装命令

LSTM 模型对 PyTorch 版本不算挑剔,但环境没立住是新手第一个大坑。常见做法是用 Anaconda 建独立环境,避免 torch 和系统 Python 里的包互相打架。Python 版本选 3.10 是稳妥的:太老的 3.7 对 PyTorch 2.x 支持不完整,太新的 3.12 有些配套库还没跟上。

# 创建项目环境,Python 版本选 3.10 比较稳 conda create -n imdb python=3.10 -y conda activate imdb # CPU 版:先跑通流程用这个就够 pip install torch # GPU 版:先确认显卡驱动支持的 CUDA 版本, # 然后去 PyTorch 官网选对应的安装命令,装完用下一节的代码验证

参数说明:conda create -n imdb里的imdb是这个环境的名称,后面conda activate imdb进出都要用它。-y表示跳过确认交互。CPU 版 torch 包体积小、装得快,如果你只是想先复现模型逻辑,完全够用;等确认代码没问题再换 GPU 版也不迟。GPU 版安装前先跑一次nvidia-smi看右上角驱动支持的 CUDA 版本,再去官网匹配对应的 whl 命令,这是最容易踩的坑——很多人直接pip install torch装出来是 CPU 版,代码跑得慢还以为模型有问题。

2.2 用两行代码验证 CUDA:别让训练悄悄跑在 CPU 上

环境装完先别急着写模型,第一步是确认 PyTorch 能看到显卡。这一步花十秒钟,能省掉后面几个小时的困惑。

import torch print("PyTorch 版本:", torch.__version__) print("CUDA 是否可用:", torch.cuda.is_available()) if torch.cuda.is_available(): print("GPU 名称:", torch.cuda.get_device_name(0))

逻辑说明:torch.cuda.is_available()返回True时才代表当前 PyTorch 真的调用到了 GPU。这里有个隐蔽情况:如果装的是 CPU 版 torch,即使显卡驱动正常,这个值也永远是False。所以看到False不要先去怀疑显卡,先检查torch.__version__里有没有+cu后缀。

参数说明:get_device_name(0)里的0是 GPU 序号,单卡就写 0。后续训练代码里的device = "cuda" if torch.cuda.is_available() else "cpu"就是靠这个判断来切换设备,如果这里没验证,后面所有的.to(device)都在 CPU 上走,训练速度差几十倍。

2.3 解析 aclImdb 原始数据:目录结构、标签映射和验证集切分

IMDB 数据集最常见的形式是官网那份aclImdb_v1.tar.gz,解压后是纯文本文件,不要依赖 torchtext 的旧版接口——torchtext 0.13 之后 IMDB 数据集的接口被改掉了,直接解析原始目录最稳。目录结构是固定的:train/pos和train/neg各 12500 条,test/pos和test/neg各 12500 条,文件名形如12419_8.txt,前面是编号,后面是人工评分。

import os from pathlib import Path base = Path("./aclImdb") def load_split(split: str): texts, labels = [], [] for label_name in ["pos", "neg"]: folder = base / split / label_name label = 1 if label_name == "pos" else 0 for fp in folder.iterdir(): if fp.suffix != ".txt": continue texts.append(fp.read_text(encoding="utf-8", errors="ignore")) labels.append(label) return texts, labels train_texts, train_labels = load_split("train") test_texts, test_labels = load_split("test") print(len(train_texts), len(test_texts)) # 25000 25000

逻辑说明:load_split读一个 split 目录下的 pos 和 neg 两个子目录,文件名后缀必须是.txt,否则会把 README 之类的无关文件读进来。标签映射用的是 pos=1、neg=0,后面对应模型输出的两个类别索引。

参数说明:errors="ignore"用来跳过个别文件里的非法编码字符,IMDB 原始文件基本是干净的,但保留这个参数能避免程序突然崩掉。这里要特别提醒:官方给出的test集是最终评测用的,不是调参用的。正确做法是从train_texts里随机切出约 2500 条当验证集,后面第 5 章会专门讲为什么。

3. 文本流水线:分词、定长序列、词表构造与 Embedding 维度选择

3.1 英文分词:从最简单正则到<pad>与<unk>两个特殊标记

LSTM 吃不了字符串,只能吃整数索引序列。第一步是把英文评论拆成 token。英文分词比中文简单得多,一个正则就够用,不需要引入 NLTK 或 spaCy——IMDB 评论里大写、标点和数字混杂,split()太粗糙,会让don't变成两个 token,还会把标点留在词上。

import re from collections import Counter def tokenize(text: str): # 只保留小写字母、数字和撇号,其余全部切掉 return re.findall(r"[a-z0-9']+", text.lower()) # 统计全量训练数据的词频 counter = Counter() for t in train_texts: counter.update(tokenize(t)) # 构建词表:前 20000 高频词 + 两个特殊标记 vocab = {"<pad>": 0, "<unk>": 1} for word, freq in counter.most_common(20000): vocab[word] = len(vocab)

逻辑说明:re.findall(r"[a-z0-9']+", text.lower())做两件事:先把文本全部小写,再按连续的小写字母、数字、撇号切分。这样"I loved it!"变成['i', 'loved', 'it']。词表里必须留两个位置:<pad>放在索引 0,因为后面模型里padding_idx=0要指定它;<unk>放在索引 1,表示没进词表的生词,推理时遇到没见过的词就落到它头上,否则程序直接 KeyError。

参数说明:most_common(20000)控制了词表上限。IMDB 全量训练数据的不重复词大约 10 万级别,但低频词大多只出现一两次,训练不出来有效信息,还会让 Embedding 层巨大。20000 是个常用起点,效果和 30000 差别不到 1 个点,但内存省不少。

3.2 截断策略:max_len=256 是怎么来的,截前还是截后

IMDB 评论长度很不均匀:短的只有十几个词,长的能到两三千词。LSTM 是串行处理每个时间步的,序列越长计算量越大,显存占用也线性增长。常见的做法是设一个max_len,把超长评论截断、短评论补齐到同样长度,这样才能组成 batch 矩阵。

MAX_LEN = 256 def encode(text: str, max_len: int = MAX_LEN): tokens = tokenize(text)[:max_len] # 超长部分直接截掉尾部 ids = [vocab.get(w, vocab["<unk>"]) for w in tokens] ids += [vocab["<pad>"]] * (max_len - len(ids)) # 不足部分补 pad return ids

逻辑说明:tokenize(text)[:max_len]是从开头截断,而不是从末尾截。IMDB 影评的特点是一般开头几句话就交代了核心态度,结尾是总结性评价,中间往往是叙事过程。如果只保留前 256 个 token,等于把"总结性结尾"丢掉了;但反过来只保留尾部,开头背景信息又没了。更复杂的做法是头和尾各留一半再拼起来,但对于入门项目,直接截前 256 个 token 简单且足够。

参数说明:max_len=256不是随便拍的。我用这个值时,约 95% 的训练样本能被完整覆盖,剩下 5% 的超级长评论截掉尾部对整体准确率的影响在 1 个点以内。如果你显存紧张就降到 128,效果会掉 2 个点左右;想追高准确率就上 512,但训练时间翻倍。

3.3 词表大小与 Embedding 维度:一组可以直接抄的默认参数

分词和编码做完,还得把每条评论包装成 PyTorch 的 Dataset,DataLoader 才能喂给模型。这一步同时也是决定 Embedding 维度的时机——它要作为模型的vocab_size和embedding_dim参数传进去。

import torch from torch.utils.data import Dataset class IMDBDataset(Dataset): def __init__(self, texts, labels, max_len=MAX_LEN): self.data = [encode(t, max_len) for t in texts] self.labels = labels def __len__(self): return len(self.data) def __getitem__(self, idx): return torch.tensor(self.data[idx], dtype=torch.long), \ torch.tensor(self.labels[idx], dtype=torch.long) train_dataset = IMDBDataset(train_texts, train_labels) test_dataset = IMDBDataset(test_texts, test_labels)

逻辑说明:__getitem__返回的是两个torch.long类型的张量——Embedding 层要求输入是整数索引,不能用浮点类型。DataLoader 在 collate 时会把 batch 内所有样本堆叠成(batch, max_len)的矩阵,因为我们在encode阶段已经统一了长度,所以不需要额外的 padding 逻辑。

参数说明:embedding_dim的取值在 100 到 300 之间,我常用 128。word2vec 当年用 300,但那是为了学通用词向量;在 IMDB 这种单任务、2 万词表规模下,128 和 300 的效果差距不到 1 个点,训练速度却差了一倍。如果你换到中文 NLP 入门任务,词表更大,可以试着提到 200。这里的关键认知是:Embedding 维度不是越大越好,数据量才是短板,25000 条训练样本撑不起太高的维度。

词汇表结构:

参数取值说明
vocab_size2000220000 高频词 +<pad>+<unk>
embedding_dim128词向量维度,128 是效率甜点
max_len256截断长度,覆盖约 95% 评论

4. 模型定义:一个可直接抄的 PyTorch LSTM 分类器与 3 个关键参数

4.1 最小可跑模型:Embedding 到 LSTM 再到全连接层

整个模型只有三个组件:Embedding 层把词的整数索引变成稠密向量,LSTM 层按时间步读完整条序列,全连接层把最后的隐状态映射成两个类别的打分。下面是完整定义,直接抄就能跑。

import torch.nn as nn class LSTMClassifier(nn.Module): def __init__(self, vocab_size, embedding_dim=128, hidden_size=128, num_layers=2, num_classes=2, dropout=0.5): super().__init__() self.embedding = nn.Embedding(vocab_size, embedding_dim, padding_idx=0) self.lstm = nn.LSTM( input_size=embedding_dim, hidden_size=hidden_size, num_layers=num_layers, batch_first=True, dropout=dropout if num_layers > 1 else 0.0, ) self.dropout = nn.Dropout(dropout) self.fc = nn.Linear(hidden_size, num_classes) def forward(self, x): # x 形状: (batch, seq_len) emb = self.embedding(x) # (batch, seq_len, embedding_dim) _, (h_n, c_n) = self.lstm(emb) # h_n: (num_layers, batch, hidden_size) last = h_n[-1] # 取最后一层的最终隐状态 out = self.dropout(last) return self.fc(out) # (batch, num_classes)

逻辑说明:nn.Embedding的padding_idx=0让索引 0 对应的词向量在训练中不参与梯度更新,这样<pad>的向量始终是初始化时的值,不会给 LSTM 注入无意义的更新信号。nn.LSTM输入形状因batch_first=True而变成(batch, seq_len, embedding_dim),这和 DataLoader 默认输出的维度顺序一致,省去转置的麻烦。

参数说明:h_n[-1]是最后一层 LSTM 的最后时间步隐状态,形状是(hidden_size,)。c_n是细胞状态,分类任务里一般不用。dropout在num_layers>1时作用于相邻 LSTM 层之间,self.dropout再对最后的隐状态做一次随机置零,两处一起防过拟合。

4.2 hidden_size、num_layers、dropout 怎么设:参数逻辑与边界

三个参数里最容易纠结。hidden_size=128是这个任务的经验值:IMDB 评论虽然有长有短,但情感极性的判别不需要模型记住特别长的依赖。hidden_size 加到 256 通常能再涨零点几个点,可一旦调到 512 配合num_layers=2,过拟合速度明显加快,验证集反而不升。

num_layers=2是我觉得性价比最高的配置。一层 LSTM 拟合能力偏弱,对复杂句式捕捉不够;三层以上训练时间变长,收益趋近于零。num_layers=2时 LSTM 内部会在两层之间自动插入 dropout,还记得代码里dropout=dropout if num_layers > 1 else 0.0这个条件吗——只有两层以上 dropout 参数才生效,这是 PyTorch 的硬性要求。

dropout=0.5是针对 IMDB 这种 2.5 万条数据量的保守选择。数据量少,模型很容易在训练集上刷到 99%,验证集却停滞在 88%。dropout 设 0.3 可以加快收敛,但泛化会略差;0.5 是最稳妥的中间值。如果你后续用了预训练词向量,可以降到 0.3,因为词向量本身带先验知识,不需要那么强的正则。

4.3 forward 里为什么取 hidden[-1]:它和 output 的区别

LSTM 的返回值有两个部分:output和(h_n, c_n)。output的形状是(batch, seq_len, hidden_size),保存了每个时间步的隐状态;h_n的形状是(num_layers, batch, hidden_size),只保留每个 LSTM 层最后一个时间步的隐状态。做分类时,整条评论的信息经过 256 个时间步的传递,最终都汇聚到最后一步的隐状态里,所以取h_n[-1]就是拿到了"读完整个句子后的语义摘要"。

更完整的写法里,有人用output[:, -1, :]来取,这两者在当前架构下是等价的——h_n[-1]就是最后一层最后一个时间步的输出。区别在于:output保留了所有中间时间步,这是做注意力机制时的输入;h_n则按层组织,取[-1]表示只要最后一层。一个常见误用是取h_n[0],那只拿到第一层的结果,信息量远不如最后一层,模型效果会明显变差。

5. 训练避坑与排查:loss 波动、显存 OOM、验证分虚高的真实原因

5.1 标准训练循环写法:梯度裁剪、早停和交叉熵选择

训练部分我用一个标准写法,包含三个容易被新手忽略的细节:梯度裁剪、验证集独立评估、早停。二分类用nn.CrossEntropyLoss()而不是手动算 sigmoid 加BCELoss,因为 CrossEntropyLoss 内部做了 log_softmax,数值稳定性更好,模型最后一层全连接输出的 logits 直接丢进去就行。

import torch.nn as nn from torch.utils.data import DataLoader device = "cuda" if torch.cuda.is_available() else "cpu" def evaluate(model, loader, criterion, device): model.eval() total_loss, correct, total = 0., 0, 0 with torch.no_grad(): for xs, ys in loader: xs, ys = xs.to(device), ys.to(device) logits = model(xs) loss = criterion(logits, ys) total_loss += loss.item() * len(xs) correct += (logits.argmax(dim=1) == ys).sum().item() total += len(ys) return total_loss / total, correct / total def train_epoch(model, loader, optimizer, criterion, device, clip=1.0): model.train() total_loss, correct, total = 0., 0, 0 for xs, ys in loader: xs, ys = xs.to(device), ys.to(device) optimizer.zero_grad() logits = model(xs) loss = criterion(logits, ys) loss.backward() nn.utils.clip_grad_norm_(model.parameters(), max_norm=clip) optimizer.step() total_loss += loss.item() * len(xs) correct += (logits.argmax(dim=1) == ys).sum().item() total += len(ys) return total_loss / total, correct / total model = LSTMClassifier(vocab_size=len(vocab)).to(device) optimizer = torch.optim.Adam(model.parameters(), lr=2e-3) criterion = nn.CrossEntropyLoss() scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=3, gamma=0.5)

逻辑说明:clip_grad_norm_括号里的model.parameters()是作用对象,max_norm=1.0是梯度的最大范数。LSTM 反向传播时梯度很容易爆炸,特别在序列较长的情况下,梯度范数可能突然涨到上百,直接把权重冲飞,loss 变成nan。梯度裁剪就是在每次loss.backward()之后、optimizer.step()之前,把梯度的范数限制在 1.0 以内。

参数说明:lr=2e-3配 Adam 是这类文本分类任务的常见起点,太大会让 loss 前期震荡,太小收敛太慢。StepLR每 3 个 epoch 把学习率减半,让模型后期走得更稳。早停逻辑不在这里展开,核心是:如果验证集连续多个 epoch 不涨,就停止训练并回滚到验证集最优的权重——神经网络训练本质是个"过拟合逐渐加深"的过程,早停就是它的后悔药。

5.2 坑 1:max_len 设得过大,显存 OOM 还伴随 loss 波动

现象:max_len=512、batch_size=64时,训练到第二个 epoch 突然报CUDA out of memory;把batch_size降到 16 后才跑起来,但 loss 曲线震荡得很厉害,准确率上不去。

原因:两个问题叠在一起。第一,LSTM 的显存占用随序列长度线性增长,每个时间步都要保存隐状态和细胞状态用于反向传播,max_len=512意味着每条样本的计算量是max_len=256的两倍。第二,batch 内 padding 比例过高,很多样本实际只有一百来个词,却被 pad 到 512,这些 pad 位置上的梯度全是无效信息,反而把 loss 曲线搅乱。

解决:把max_len降到 256,batch_size从 32 起步。如果你想把 batch 拉大来稳定梯度,可以按每条样本的真实长度排序再切 batch,让长度相近的样本凑一起,减少 padding 浪费——这就是所谓的 bucket sampler,效果明显但代码复杂度也上来了,入门阶段先降max_len最直接。

5.3 坑 2:拿官方 test 集当验证集反复调参,最终分数失真

现象:训练时每个 epoch 都在官方 test 集上跑一遍,test 准确率从 84% 一路涨到 92%,你觉得模型很棒拿去上线,实测却只有 86%——线下在线差异大得没法解释。

原因:这是典型的数据泄漏。官方 test 集被反复用来评估,等于把测试样本的特征间接参与了调参决策:早停时机、学习率衰减、dropout 选择,全都在"看过答案"的集合上调出来的。模型权重虽然没直接在 test 上训练,但超参数已经在 test 分布上做了隐式拟合,最终数字虚高。

解决:从train_texts里随机切出 2500 条当验证集,test 集只允许在全部流程结束后碰一次。

from torch.utils.data import random_split n = len(train_dataset) train_ds, val_ds = random_split(train_dataset, [n - 2500, 2500]) train_loader = DataLoader(train_ds, batch_size=64, shuffle=True) val_loader = DataLoader(val_ds, batch_size=64, shuffle=False) test_loader = DataLoader(test_dataset, batch_size=64, shuffle=False)

逻辑说明:random_split按给定比例切分,返回的还是 Dataset 对象,可以直接套 DataLoader。注意train_loader的shuffle=True,每个 epoch 样本顺序都打乱,防止模型学到批次内部的偶发顺序;val_loader和test_loader必须shuffle=False,评估结果才跟样本顺序无关、可复现。

5.4 坑 3:加载模型报 vocab size 不匹配,词表和权重没一起保存

现象:训练完用torch.save(model.state_dict(), "model.pt")存了权重,换个脚本加载时报错,输出类似size mismatch for embedding.weight: copying a param with shape torch.Size([20002, 128]),怎么改都对不上。

原因:state_dict里只存了张量数值,没存词表本身。如果新脚本构建词表时只取了 5000 个高频词,模型初始化时vocab_size=5002,Embedding 层的权重尺寸就和保存时不一样了。很多人还会遇到另一个变体:训练脚本里明明用的 20000,加载脚本里却因为预处理不同,词表里<pad>和<unk>的索引位置互换,权重对上了但语义全乱了。

解决:把词表和权重一起保存,加载时先恢复词表再重建模型。具体代码在第 6 章,这里先记结论:vocab和state_dict必须成对出现,缺一个都等于模型废了。

5.5 坑 4:两次训练结果差 2%,随机种子和复现性没固定

现象:同一份代码、同样的参数,连着跑两次,验证集准确率一次 88.4%、一次 86.7%。你什么都没改,结果却对不上,开始怀疑自己改坏了代码。

原因:PyTorch 的默认行为里,每个 epoch 的数据打乱顺序、Embedding 层的初始化都依赖随机数,而随机数是不固定的。模型结构相同、超参相同,但初始化值不同,训练轨迹就完全不同。数据量越小,这种差异越明显。IMDB 2.5 万条的规模正好处于"差异肉眼可见"的区间。

解决:在训练脚本最开头固定随机种子,代码顺序不能变。

def set_seed(seed=42): torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) import numpy as np np.random.seed(seed) set_seed(42)

逻辑说明:torch.manual_seed固定 CPU 上的随机数生成器,torch.cuda.manual_seed_all固定所有 GPU 上的生成器。import numpy as np放在函数里是为了避免忘掉 numpy 的种子——如果数据预处理里用了 numpy 的随机函数,不固定它一样没法复现。

6. 让模型能落地:state_dict 与 vocab 分开存、推理封装和两个提升方向

6.1 模型的保存与加载约定:这两个文件一个都不能丢

训练完成后,保存的格式直接决定后续能不能顺利加载。我的习惯是保存两个文件:一个是权重,另一个是词表 JSON。权重文件里额外存一份模型配置,加载时不用手动跟传参谱对照。

import json torch.save({ "state_dict": model.state_dict(), "config": { "vocab_size": len(vocab), "embedding_dim": 128, "hidden_size": 128, "num_layers": 2, "dropout": 0.5, } }, "imdb_lstm.pt") with open("imdb_vocab.json", "w", encoding="utf-8") as f: json.dump(vocab, f)

加载时先读词表,再按 config 重建模型,顺序反了会报错——LSTMClassifier构造时需要vocab_size才能确定 Embedding 层的尺寸,它从 config 里来。

6.2 推理函数:未知词、超长输入和 batch 维度的处理

推理封装要处理三个边界:词表外的新词、超过max_len的超长输入、以及 PyTorch 模型要求的 batch 维度。encode函数里vocab.get(w, vocab["<unk>"])已经保证了新词落到<unk>;超长输入在encode阶段就被截断了;剩下的就是别把(1, seq_len)写成(seq_len,)。

def predict(text, model, vocab, max_len=256, device="cpu"): model.eval() ids = encode(text, max_len=max_len) # 未知词自动落到 <unk> x = torch.tensor([ids], dtype=torch.long, device=device) # 补 batch 维 with torch.no_grad(): logits = model(x) prob_pos = torch.softmax(logits, dim=1)[0, 1].item() return prob_pos model = LSTMClassifier(**ckpt["config"]).to(device) model.load_state_dict(ckpt["state_dict"]) print(predict("This movie is absolutely fantastic!", model, vocab))

逻辑说明:[ids]外面再套一层中括号,是为了让形状变成(1, seq_len)——LSTM 的首个维度是 batch,就算只预测一条样本也不能丢。softmax(logits, dim=1)得到两个类别的概率分布,[0, 1]取第一条样本的索引 1 概率,也就是正向概率。

6.3 想从 88% 再往上走:双向 LSTM、注意力与预训练模型的取舍

IMDB 场景里 LSTM 的准确率上限大约在 88% 到 89%,想突破 90% 需要换结构。三条路的成本差别很大:

方案训练成本大致上限修改量
当前 LSTM单卡几分钟88% 上下基准方案
BiLSTM + 注意力单卡十几分钟90% 上下forward 改两处,fc 输入维度翻倍
BERT 微调GPU 半天量级94% 上下数据流水线重写,显存要求高

双向 LSTM 是把序列正读一遍、反读一遍,最后把两个方向的隐状态拼接,fc的输入维度从hidden_size变成2 * hidden_size。注意力机制让模型对output的所有时间步做加权求和,而不是只取最后一步,对长评论尤其有效。BERT 微调的门槛不在代码量,而在数据和算力——25000 条样本对 BERT 来说偏少,很容易微调过度,而且推理速度慢十倍以上。

我自己跑这类任务时有个习惯:先用最朴素的配置完整跑通一个 epoch,确认训练循环没有隐藏 bug,再开始折腾hidden_size和 dropout。这个顺序能筛掉大半调参返工,也希望这份笔记能帮你少走几步弯路。如果照着跑通后卡在准确率上不去,回头检查第二件事:验证集是不是泄漏了。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询