该文档为datawhale情感分析组队学习的笔记
Github地址:team-learning-nlp/Emotional_Analysis at master · datawhalechina/team-learning-nlp (github.com)
- 本文实现论文 Bag of Tricks for Efficient Text Classification中的模型(fasttext)
- 该模型与改进后的RNN有相当的性能,但训练速度要快得多
- FastText分类模型与其他文本分类模型最大的不同之处在于其计算了输入句子的n-gram,并将n-gram作为一种附加特征来获取局部词序特征信息添加至标记化列表的末尾。
- 它是一种基于语言模型的算法,基本思想是将文本内容按照字节顺序进行大小为N的滑动窗口操作,最终形成长度为N的字节片段序列。
def generate_bigrams(x):
n_grams = set(zip(*[x[i:] for i in range(2)]))
for n_gram in n_grams:
x.append(' '.join(n_gram))
return x
generate_bigrams(['This', 'film', 'is', 'terrible'])
# ['This', 'film', 'is', 'terrible', 'film is', 'This film', 'is terrible']
- 带来的优点:
- 对于低频词生成的词向量效果会更好。因为它们的n-gram可以和其他词共享
- 对于训练词库之外的单词,仍然可以构建他们的词向量。我们可以叠加它们的字符级n-gram向量。
- TorchText ‘Field’ 中有一个preprocessing参数。此处传递的函数将在对句子进行 tokenized (从字符串转换为标token列表)之后,但在对其进行数字化(从tokens列表转换为indexes列表)之前应用于句子。我们将在这里传递generate_bigrams函数。(由于我们没有使用RNN,所以不需要使用压缩填充序列,因此我们不需要设置“include_length=True”。)
import torch
from torchtext.legacy import data
from torchtext.legacy import datasets
SEED = 1234
torch.manual_seed(SEED)
torch.backends.cudnn.deterministic = True
TEXT = data.Field(tokenize = 'spacy',
tokenizer_language = 'en_core_web_sm',
preprocessing = generate_bigrams)
LABEL = data.LabelField(dtype = torch.float)
1.3数据预处理
- 与前面一样
import random
train_data, test_data = datasets.IMDB.splits(TEXT, LABEL)
train_data, valid_data = train_data.split(random_state = random.seed(SEED))
MAX_VOCAB_SIZE = 25_000
TEXT.build_vocab(train_data,
max_size = MAX_VOCAB_SIZE,
vectors = "glove.6B.100d",
unk_init = torch.Tensor.normal_)
LABEL.build_vocab(train_data)
BATCH_SIZE = 64
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
train_iterator, valid_iterator, test_iterator = data.BucketIterator.splits(
(train_data, valid_data, test_data),
batch_size = BATCH_SIZE,
device = device)
2. 构建模型
2.1 模型架构
fasttext模型只有三层:输入层,隐含层,输出层
输入层:多个经向量表示的单词及其n-gram特征,这些特征用来表示单个文档
隐含层:多个词向量的叠加平均
输出层:一个特定的target
- fastText在输入时,将单词的字符级别的n-gram向量作为额外的特征;
- fasttext在输出时,fastText采用了分层Softmax,大大降低了模型训练时间。
- fastText的核心思想就是:将整篇文档的词及n-gram向量叠加平均得到文档向量,然后使用文档向量做softmax多分类。这中间涉及到两个技巧:字符级n-gram特征的引入以及分层Softmax分类。
具体地,它首先使用’Embedding’层(蓝色)计算每个词嵌入,然后计算所有词嵌入的平均值(粉红色),并通过’Linear’层(银色)将其输入。
我们使用二维池化函数“avg_pool2d”实现单词在Embedding空间中的平均化。我们可以将词嵌入看作为一个二维网格,其中词沿着一个轴,词嵌入的维度沿着另一个轴。下图是一个转换为5维词嵌入的示例句子,词沿纵轴,嵌入沿横轴。[4x5] tensor中的每个元素都由一个绿色块表示。
“avg_pool2d”使用大小为“embedded.shape[1]”(即句子长度)乘以1的过滤器。下图中以粉红色显示。
我们计算filter 覆盖的所有元素的平均值,然后filter 向右滑动,计算句子中每个单词下一列嵌入值的平均值。
每个filter位置提供一个值,即所有覆盖元素的平均值。filter 覆盖所有嵌入维度后,会得到一个[1x5] 的张量,然后通过线性层进行预测。
2.3模型代码- torch.avg_pool2d(第二个参数为kernel_size)
torch.nn.functional.max_pool2d(input, kernel_size, stride=None, padding=0, dilation=1, ceil_mode=False, return_indices=False)
import torch.nn as nn
import torch.nn.functional as F
class FastText(nn.Module):
def __init__(self, vocab_size, embedding_dim, output_dim, pad_idx):
super().__init__()
self.embedding = nn.Embedding(vocab_size, embedding_dim, padding_idx=pad_idx)
self.fc = nn.Linear(embedding_dim, output_dim)
def forward(self, text):
#text = [sent len, batch size]
embedded = self.embedding(text)
#embedded = [sent len, batch size, emb dim]
embedded = embedded.permute(1, 0, 2)
#embedded = [batch size, sent len, emb dim]
pooled = F.avg_pool2d(embedded, (embedded.shape[1], 1)).squeeze(1)
#pooled = [batch size, embedding_dim]
return self.fc(pooled)
- 创建fasttext类的实例
INPUT_DIM = len(TEXT.vocab) EMBEDDING_DIM = 100 OUTPUT_DIM = 1 PAD_IDX = TEXT.vocab.stoi[TEXT.pad_token] model = FastText(INPUT_DIM, EMBEDDING_DIM, OUTPUT_DIM, PAD_IDX)
- 查看参数:跟标准RNN大致相同
def count_parameters(model):
return sum(p.numel() for p in model.parameters() if p.requires_grad)
print(f'The model has {count_parameters(model):,} trainable parameters')
# The model has 2,500,301 trainable parameters
- 预训练好的向量复制到嵌入层
pretrained_embeddings = TEXT.vocab.vectors model.embedding.weight.data.copy_(pretrained_embeddings)
- unk和pad的初始权重归零(- 因为我们的
和 token不在预训练词表里,它们已经在构建我们自己的词表时,使用 unk_init (an N ( 0 , 1 ) mathcal{N}(0,1) N(0,1) distribution)初始化了。所以,最好显式地告诉模型,将它们初始化变为0,它们与情感无关。)
UNK_IDX = TEXT.vocab.stoi[TEXT.unk_token] model.embedding.weight.data[UNK_IDX] = torch.zeros(EMBEDDING_DIM) model.embedding.weight.data[PAD_IDX] = torch.zeros(EMBEDDING_DIM)3. 训练模型 3.1 优化器
- Adam:Adam会调整每个参数的学习率,给出更新频率更高的参数,以及更新频率更低的参数和更新频率不高的参数
import torch.optim as optim optimizer = optim.Adam(model.parameters())3.2 设置损失函数和GPU
criterion = nn.BCEWithLogitsLoss() # 损失函数. criterion 在本task中时损失函数的意思 model = model.to(device) criterion = criterion.to(device)3.3 计算精确度
def binary_accuracy(preds, y):
"""
Returns accuracy per batch, i.e. if you get 8/10 right, this returns 0.8, NOT 8
"""
#round predictions to the closest integer
rounded_preds = torch.round(torch.sigmoid(preds))
correct = (rounded_preds == y).float() #convert into float for division
acc = correct.sum() / len(correct)
return acc
3.4 train函数
- batch.text为一个元组
- 第一个元素是数字张量
- 第二个元素是每个序列的实际长度
- 在将它们传递给模型之前,我们将它们分成各自的变量“text”和“text_length”。
def train(model, iterator, optimizer, criterion):
epoch_loss = 0
epoch_acc = 0
model.train()
for batch in iterator:
optimizer.zero_grad()
predictions = model(batch.text).squeeze(1)
loss = criterion(predictions, batch.label)
acc = binary_accuracy(predictions, batch.label)
loss.backward()
optimizer.step()
epoch_loss += loss.item()
epoch_acc += acc.item()
return epoch_loss / len(iterator), epoch_acc / len(iterator)
3.5 evaluate函数
def evaluate(model, iterator, criterion):
epoch_loss = 0
epoch_acc = 0
model.eval()
with torch.no_grad():
for batch in iterator:
predictions = model(batch.text).squeeze(1)
loss = criterion(predictions, batch.label)
acc = binary_accuracy(predictions, batch.label)
epoch_loss += loss.item()
epoch_acc += acc.item()
return epoch_loss / len(iterator), epoch_acc / len(iterator)
3.6 epoch_time
import time
def epoch_time(start_time, end_time):
elapsed_time = end_time - start_time
elapsed_mins = int(elapsed_time / 60)
elapsed_secs = int(elapsed_time - (elapsed_mins * 60))
return elapsed_mins, elapsed_secs
4. 正式训练
N_EPOCHS = 5
best_valid_loss = float('inf')
for epoch in range(N_EPOCHS):
start_time = time.time()
train_loss, train_acc = train(model, train_iterator, optimizer, criterion)
valid_loss, valid_acc = evaluate(model, valid_iterator, criterion)
end_time = time.time()
epoch_mins, epoch_secs = epoch_time(start_time, end_time)
# 保留最好的训练结果的那个模型参数,之后加载这个进行预测
if valid_loss < best_valid_loss:
best_valid_loss = valid_loss
torch.save(model.state_dict(), 'tut2-model.pt')
print(f'Epoch: {epoch+1:02} | Epoch Time: {epoch_mins}m {epoch_secs}s')
print(f'tTrain Loss: {train_loss:.3f} | Train Acc: {train_acc*100:.2f}%')
print(f't Val. Loss: {valid_loss:.3f} | Val. Acc: {valid_acc*100:.2f}%')
Epoch: 01 | Epoch Time: 0m 14s
Train Loss: 0.686 | Train Acc: 61.64%
Val. Loss: 0.630 | Val. Acc: 71.84%
Epoch: 02 | Epoch Time: 0m 14s
Train Loss: 0.646 | Train Acc: 74.07%
Val. Loss: 0.511 | Val. Acc: 75.98%
Epoch: 03 | Epoch Time: 0m 15s
Train Loss: 0.572 | Train Acc: 79.88%
Val. Loss: 0.428 | Val. Acc: 80.59%
Epoch: 04 | Epoch Time: 0m 14s
Train Loss: 0.496 | Train Acc: 84.16%
Val. Loss: 0.388 | Val. Acc: 83.76%
Epoch: 05 | Epoch Time: 0m 14s
Train Loss: 0.431 | Train Acc: 87.00%
Val. Loss: 0.372 | Val. Acc: 85.97%
5. 测试结果
model.load_state_dict(torch.load('tut3-model.pt'))
test_loss, test_acc = evaluate(model, test_iterator, criterion)
print(f'Test Loss: {test_loss:.3f} | Test Acc: {test_acc*100:.2f}%')
Test Loss: 0.384 | Test Acc: 85.38%
6. 模型验证
我们现在可以使用这个模型来预测我们给出的任何句子的情感了,注意需要提供的句子是电影评论方面的。
负面评论返回接近0的值,正面评论返回接近1的值。
import spacy
nlp = spacy.load('en_core_web_sm')
def predict_sentiment(model, sentence):
model.eval()
tokenized = generate_bigrams([tok.text for tok in nlp.tokenizer(sentence)])
indexed = [TEXT.vocab.stoi[t] for t in tokenized]
tensor = torch.LongTensor(indexed).to(device)
tensor = tensor.unsqueeze(1)
prediction = torch.sigmoid(model(tensor))
return prediction.item()
predict_sentiment(model, "This film is terrible") # 1.1890469719944008e-09



