<think>
根据用户需求,我们需要实现一个基于LSTM的文本情感分类模型。具体要求如下:
1. 使用Embedding层,设置padding_idx=0。
2. 使用双向LSTM,隐藏层大小为64(即每个方向64,所以双向LSTM的输出维度为128)。
3. 在LSTM后加一个Dropout层,dropout概率为0.3。
4. 然后通过一个全连接层(Linear)将128维的向量映射到3维(3类情感标签)。
5. 使用pack_padded_sequence处理变长序列,并取最后时刻的正向和反向隐藏状态(注意是最后一层的)进行拼接,形成句向量。
6. 模型需要支持批量输入。
注意:禁用高层封装(如nn.LSTMClassifier等),需要自己实现前向传播中处理变长序列的步骤。
模型结构总结如下:
- Embedding
- BiLSTM (hidden_size=64, bidirectional=True, 因此输出为128维)
- Dropout (0.3)
- Linear (128->3)
在LSTM处理变长序列时,我们使用pack_padded_sequence,然后使用pad_packed_sequence得到输出。但注意,这里我们只需要最后一个时刻的隐藏状态(对于双向LSTM,我们需要取最后一个时刻的正向和第一个时刻的反向?实际上,对于双向LSTM,我们通常取前向的最后一个时刻和反向的第一个时刻,拼接起来作为整个序列的表示。但是,在PyTorch中,我们可以直接使用LSTM的最后一个隐藏状态(hn)来获取,因为hn已经按照方向层和方向进行了组织。
具体而言,对于双向LSTM,hn的维度为(2, batch_size, hidden_size)(如果只有一层的话),其中第一维是方向(0为前向,1为反向)。对于前向,最后一个时刻的隐藏状态就是hn[0];对于反向,我们实际上需要的是第一个时刻的隐藏状态,也就是hn[1]。但注意,在双向LSTM中,反向的最后一个时刻(原始序列的第一个时刻)的输出我们并不需要,我们需要的是反向的最后一个时刻(也就是原始序列的第一个时刻)?不对。
实际上,在双向LSTM中,我们通常将前向的最后一个时刻和反向的最后一个时刻(即原始序列的第一个时刻)拼接起来。但请注意,对于反向LSTM,它的第一个输入是序列的最后一个词,所以反向LSTM的最后一个时刻对应的是原始序列的第一个词,而反向LSTM的第一个时刻对应的是原始序列的最后一个词。因此,为了得到整个序列的表示,我们取前向LSTM的最后一个时刻(对应序列的最后一个词)和反向LSTM的最后一个时刻(对应序列的第一个词),然后拼接。但是,在PyTorch中,当我们使用pack_padded_sequence并处理完后,我们可以通过以下方式获取:
方法1:使用hn(隐藏状态)和层/方向信息。对于双向LSTM,hn的最后一层的前向和后向隐藏状态分别对应前向的最后一个时刻和反向的最后一个时刻(注意反向的最后一个时刻是原始序列的第一个时刻)。所以,我们可以取hn[-2,:,:]和hn[-1,:,:](如果是多层,则最后一层的前向和反向)进行拼接。
但是,由于我们的LSTM是双向单层,所以hn的维度是(2, batch_size, hidden_size)。我们取hn[0]和hn[1],然后按最后一个维度拼接,得到(batch_size, hidden_size*2)即(batch_size, 128)。
方法2:我们也可以从输出中获取,但输出包含了每个时刻的隐藏状态,而我们只需要每个序列的最后一个有效时刻。不过,使用hn更加直接,因为hn就是最后一个时刻的隐藏状态(对于每个方向)。
注意:在双向LSTM中,hn的存储方式为:第一维是方向(0为前向,1为反向),第二维是batch,第三维是隐藏状态。所以,我们可以这样获取:
```python
hn_combined = torch.cat((hn[0], hn[1]), dim=1)
```
但是,这里有一个关键点:在PyTorch中,当我们使用pack_padded_sequence时,LSTM的返回值output是打包后的所有时刻的输出,而hn是各个序列的最后一个隐藏状态(对于前向是最后一个时刻,对于反向是第一个时刻?实际上,hn是每个序列在最后一个非填充时刻的隐藏状态)。在双向LSTM中,hn[0]是前向的最后一个时刻的隐藏状态,hn[1]是反向的最后一个时刻的隐藏状态(注意反向的最后一个时刻对应原始序列的第一个词,但作为反向序列的最后一个时刻,它已经看到了整个序列的信息,所以可以代表整个序列?实际上,在双向LSTM中,我们将前向和反向的最后一个非填充时刻的隐藏状态拼接起来,作为整个序列的表示。所以,使用hn是合理的。
然而,注意:在双向LSTM中,hn[0]和hn[1]分别表示前向和反向的最终隐藏状态。因此,我们直接使用hn进行拼接即可。
因此,步骤:
1. 通过嵌入层得到词嵌入。
2. 将嵌入后的序列打包(pack_padded_sequence),注意需要按长度从大到小排序(但用户输入可能未排序,所以我们需要在传入前排序,并记录原始顺序)。
3. 通过LSTM得到输出和隐藏状态。
4. 取hn,并按照方向拼接(因为双向,所以有两个方向,每个方向隐藏层大小为64,拼接后为128)。
5. 应用dropout。
6. 通过全连接层得到3个输出。
注意:在传入LSTM之前,我们需要对输入序列按长度降序排列,并打包。在LSTM处理后,我们取hn作为每个序列的表示(已经包含了两个方向的信息)。
另外,由于我们使用了双向LSTM,所以LSTM层的参数设置:bidirectional=True, hidden_size=64。
实现代码:
我们创建一个名为SentimentLSTM的类,继承自nn.Module。
在__init__中定义:
- embedding: Embedding层,注意padding_idx=0
- lstm: LSTM层,输入维度为embedding_dim(由用户传入),隐藏层大小64,双向=True,batch_first=True(假设输入为(batch, seq_len))
- dropout: Dropout层,p=0.3
- fc: 全连接层,输入维度为128(因为双向,2*64),输出维度3
在forward中:
1. 输入是input_ids和lengths。input_ids是已经填充过的序列,形状为(batch_size, max_seq_len),dtype为torch.long;lengths是每个序列的实际长度(不包括填充),形状为(batch_size,),dtype为torch.long。
2. 由于LSTM需要按序列长度降序排列,所以我们需要对输入序列进行排序(根据lengths降序),并记录原始顺序以便恢复。
3. 通过embedding层:embeddings = self.embedding(input_ids) # (batch_size, max_seq_len, embedding_dim)
4. 将embeddings按照排序后的顺序重新排列,然后打包:packed_embeddings = nn.utils.rnn.pack_padded_sequence(embeddings_sorted, sorted_lengths, batch_first=True)
5. 通过LSTM:packed_output, (hn, cn) = self.lstm(packed_embeddings)
- 注意:hn的形状为(num_directions * num_layers, batch_size, hidden_size),因为我们只有一层双向,所以num_directions=2, num_layers=1,所以hn是(2, batch_size, 64)
6. 将hn按方向拼接:hn_combined = torch.cat((hn[0], hn[1]), dim=1) # (batch_size, 128)
7. 应用dropout:hn_combined = self.dropout(hn_combined)
8. 通过全连接层:out = self.fc(hn_combined) # (batch_size, 3)
9. 由于我们之前对输入进行了排序,现在需要将输出恢复为原始输入的顺序(即排序前的顺序)。
注意:在排序和恢复顺序时,我们需要使用torch.sort和torch.argsort。
具体步骤:
- 对lengths进行降序排序(torch.sort默认升序,所以用descending=True),得到sorted_lengths和perm_idx(排序后的索引)。
- 将输入按照perm_idx排序:input_sorted = input_ids[perm_idx]
- 通过嵌入层得到嵌入向量
- 打包并输入LSTM
- 得到hn_combined后,我们需要恢复原始顺序:创建unperm_idx(即perm_idx的逆序索引),然后对输出进行索引unperm_idx以恢复。
例如:
```python
# 排序
sorted_lengths, perm_idx = lengths.sort(0, descending=True)
input_sorted = input_ids[perm_idx]
# ... 经过LSTM得到输出out_sorted
# 恢复原始顺序
_, unperm_idx = perm_idx.sort(0)
out = out_sorted[unperm_idx]
```
注意:如果batch_size=1,排序和恢复也是安全的。
下面我们实现代码:
</think>下面我将提供完整的 `SentimentLSTM` 实现代码,满足你的所有要求:
```python
import torch
import torch.nn as nn
class SentimentLSTM(nn.Module):
def __init__(self, vocab_size, embedding_dim, hidden_size=64):
super(SentimentLSTM, self).__init__()
# 嵌入层 (padding_idx=0)
self.embedding = nn.Embedding(
num_embeddings=vocab_size,
embedding_dim=embedding_dim,
padding_idx=0
)
# 双向LSTM (隐藏层大小64)
self.lstm = nn.LSTM(
input_size=embedding_dim,
hidden_size=hidden_size,
num_layers=1,
bidirectional=True,
batch_first=True
)
# Dropout层 (0.3)
self.dropout = nn.Dropout(0.3)
# 全连接层 (128->3)
self.fc = nn.Linear(hidden_size * 2, 3) # 双向所以*2
def forward(self, input_ids, lengths):
# 嵌入层处理
embeddings = self.embedding(input_ids)
# 按序列长度降序排序
sorted_lengths, sorted_idx = lengths.sort(descending=True)
embeddings_sorted = embeddings[sorted_idx]
# 打包变长序列
packed_embeddings = nn.utils.rnn.pack_padded_sequence(
embeddings_sorted,
sorted_lengths.cpu(),
batch_first=True,
enforce_sorted=True
)
# LSTM处理
packed_output, (hn, cn) = self.lstm(packed_embeddings)
# 取最后时刻正反向隐藏状态
hn = hn.permute(1, 0, 2) # (num_directions * num_layers, batch, hidden_size) -> (batch, num_directions * num_layers, hidden_size)
batch_size = hn.size(0)
hn = hn.reshape(batch_size, -1) # 将双向隐藏状态拼接 (batch, hidden_size * 2)
# 恢复原始顺序
_, unsorted_idx = sorted_idx.sort()
hn = hn[unsorted_idx]
# Dropout和全连接层
out = self.dropout(hn)
out = self.fc(out)
return out
```
### 实现要点说明:
1. **嵌入层处理**:
- 使用 `nn.Embedding` 并设置 `padding_idx=0`,自动忽略填充值
- 输入形状:(batch_size, seq_length)
- 输出形状:(batch_size, seq_length, embedding_dim)
2. **变长序列处理**:
- 按序列长度降序排序输入数据和长度
- 使用 `pack_padded_sequence` 打包序列,减少计算量
- 处理后使用 `unsorted_idx` 恢复原始顺序
3. **LSTM层**:
- 双向LSTM (`bidirectional=True`)
- 隐藏层大小64,输出128维特征(双向拼接)
- 取最后时刻正反向隐藏状态拼接作为句向量
4. **分类层**:
- Dropout(0.3) 防止过拟合
- 全连接层将128维特征映射到3类情感标签
5. **前向传播流程**:
```mermaid
graph LR
A[输入序列] --> B[嵌入层]
B --> C[按长度排序]
C --> D[pack_padded_sequence]
D --> E[LSTM处理]
E --> F[取最后隐藏状态]
F --> G[恢复原始顺序]
G --> H[Dropout]
H --> I[全连接层]
I --> J[3类输出]
```
### 测试建议:
1. 确保输入数据格式正确:
```python
# 示例输入
input_ids = torch.tensor([[1, 2, 3, 0], [4, 5, 0, 0], [6, 7, 8, 9]]) # 填充后的序列
lengths = torch.tensor([3, 2, 4]) # 实际长度
```
2. 初始化模型:
```python
model = SentimentLSTM(vocab_size=10000, embedding_dim=128)
```
3. 检查输出维度:
```python
output = model(input_ids, lengths)
print(output.shape) # 应为 torch.Size([3, 3])
```
### 常见问题解决:
1. **序列打包错误**:确保 `lengths` 是降序排列且为CPU张量
2. **维度不匹配**:检查LSTM输出形状,双向LSTM隐藏层应输出 `hidden_size*2`
3. **填充值影响**:确认嵌入层设置了 `padding_idx=0`
此实现满足:
- 使用基础PyTorch模块(无高层封装)
- 正确处理变长序列
- 符合指定网络结构
- 输出3类情感概率
- 支持批量处理