情感分析项目代码详解(三):textrnn.py 模型搭建
前两篇完成了数据准备:词汇表建好了,文本也变成了定长的数字序列,还写好了分批读取的迭代器。这一篇开始搭建真正的"大脑"——情感分类模型。textrnn.py 用经典的 RNN(循环神经网络)家族成员 LSTM 来做文本分类,代码只有二十几行,但每一行都值得细看。
一、导入库
import torch import torch.nn as nntorch 是 PyTorch 主库,nn 是它的神经网络模块,里面封装好了各种常用层(嵌入层、LSTM、全连接层等)。我们只需要像搭积木一样把它们拼起来。
二、定义模型类
class Model(nn.Module): def __init__(self, embedding_pretained, n_vocab, embed, num_classes): super(Model, self).__init__()在 PyTorch 里,所有自定义模型都要继承 nn.Module 这个基类,这样它才具备"可训练、可保存、可迁移"等能力。
__init__ 接收四个参数:embedding_pretained(预训练的字向量,没有就传 None)、n_vocab(词汇表大小)、embed(每个字的向量维度)、num_classes(分类数,本项目是 4 种情绪)。
super().__init__() 先调用父类的初始化方法,这是继承的标准写法,必须放在第一行。
三、嵌入层 Embedding
if embedding_pretained is not None: self.embedding = nn.Embedding.from_pretrained(embedding_pretained, padding_idx=n_vocab - 1, freeze=False) else: self.embedding = nn.Embedding(n_vocab, embed, padding_idx=n_vocab - 1)嵌入层的作用:把"字编号"变成"向量"。编号本身只是个整数,没有任何含义;Embedding 相当于一张大查找表,把每个编号映射成一个稠密向量,让意思相近的字,向量也相近。
if 分支:如果提供了预训练向量(比如别人在大语料上训练好的),就用 from_pretrained 直接加载;freeze=False 表示不冻结,训练过程中还可以继续微调这些向量。
else 分支:没有预训练向量,就新建一个 Embedding 层,随机初始化,边训练边学。
padding_idx=n_vocab-1:第二篇里 PAD 的编号是最后一个(n_vocab-1)。指定它之后,填充位置的向量永远是 0,梯度也不会更新它——模型不会把"填充"当成有效信息。
四、LSTM 层
self.lstm = nn.LSTM(embed, 128, 3, bidirectional=True, batch_first=True, dropout=0.3)参数逐个看:embed 是输入维度(字的向量长度);128 是隐藏层大小,可以理解为 LSTM 的"记忆容量";3 是层数,堆叠 3 层 LSTM,越深能学到越抽象的特征。
bidirectional=True:双向 LSTM,一条从句首往句尾读,一条从句尾往句首读。比如单独一个"不"字,光看前半句不知道它后面跟什么,双向就能兼顾前后文。
batch_first=True:约定输入格式为 [批次, 长度, 维度],和第二篇迭代器输出的形状对得上,省去转置的麻烦。
dropout=0.3:训练时随机"丢弃"30% 的神经元,防止模型死记硬背训练集(过拟合)。
五、全连接层
self.fc = nn.Linear(128 * 2, num_classes)LSTM 是双向的,正向 128 维 + 反向 128 维拼在一起是 256 维,所以输入维度写成 128*2。
输出 num_classes 个分数,对应 4 种情绪,哪个分数最高就预测哪一类。
六、forward 前向传播(上)
def forward(self, x): x, seq_len = x emb = self.embedding(x) # [B, L, E]forward 定义了数据在模型里怎么流动,调用 model(x) 时会自动执行它。
第二篇的迭代器返回 ((x, seq_len), y),所以第一行先把元组拆开:x 是字编号矩阵,seq_len 是每条样本的真实长度。
注释里的 B、L、E 分别代表:批次大小、序列长度(70)、字向量维度。嵌入后每个字都变成一条 E 维向量。
七、forward 前向传播(中)
out, _ = self.lstm(emb) # [B, L, 2H]把向量送进 LSTM,out 是每一个时刻(每个字位置)的输出,形状 [B, 70, 256]。
LSTM 还会返回最后一刻的隐藏状态和记忆单元,这里用下划线 _ 接收后直接丢掉——因为我们接下来要用 seq_len 精确取位置,用不上它。
八、取真实最后一步
idx = (seq_len - 1).clamp(min=0) # [B] last = out[torch.arange(out.size(0), device=out.device), idx] # [B, 2H]关键点!短句子后面补了 PAD,如果直接用最后一位,取到的是一堆填充向量,等于白看。所以要根据 seq_len 找到每个样本"真实"的最后一个字的位置。
seq_len - 1 是最后一个字的索引(索引从 0 开始);clamp(min=0) 是保险,防止空句出现 -1。
torch.arange(out.size(0)) 生成 [0,1,2,...] 批次序号,配合 idx 做"花式索引":对第 i 条样本,取它第 idx[i] 个位置的输出。一句话:从每条序列里抠出真实最后一个字的 LSTM 编码。
九、分类输出
return self.fc(last)把 256 维的特征送进全连接层,得到 4 个类别的分数并返回。下一篇的训练代码会拿这个分数和真实标签计算损失。
小结
textrnn.py 虽然短,但完整走通了"编号→向量→LSTM 编码→取真实最后状态→分类"的链路。模型建好了,下一篇就来看怎么训练它、评估它、测试它。