【第一期AI夏令营丨自然语言处理】使用BERT模型解决问题

发表于 2023-10-24更新于 2023-10-24字数统计 5.1k阅读时长 37m阅读次数

# 一、使用预训练的BERT模型解决文本二分类问题

深度学习模型训练的一般步骤:

  1. 导入前置依赖
  2. 设置全局配置
  3. 进行数据读取与数据预处理
  4. 构建训练所需的dataloader与dataset
  5. 定义预测模型
  6. 定义出损失函数和优化器
  7. 定义一个验证方法,获取到验证集的精准率和loss。
  8. 模型训练,保存最好的模型
  9. 加载最好的模型,然后进行测试集的预测
  10. 将测试数据送入模型,得到结果

1. 导入前置依赖

1
2
3
4
5
6
7
8
9
10
import os
import pandas as pd
import torch
from torch import nn
from torch.utils.data import Dataset, DataLoader
# 用于加载bert模型的分词器
from transformers import AutoTokenizer
# 用于加载bert模型
from transformers import BertModel
from pathlib import Path

当我们需要导入项目中的摸个函数时,应该这样操作:

from 文件夹名.某个py文件 import 某个函数

例如在当前目录下有一个FaceModel文件夹,文件夹下有一个faceModel.py, py文件下有一个predict函数,那应该如何操作呢?

1
from  FaceModel.faceModel import predict

2.设置全局配置

主要设置一些超参数。超参数是在开 始学习过程之前设置值的参数,而不是通过训练得到的参数数据。通常情况下,需要对超参数进行优化,给学习机选择一组最优超参数,以提高学习的性能和效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
batch_size = 16
# 文本的最大长度
text_max_length = 128
# 总训练的epochs数
epochs = 100
# 学习率
lr = 3e-5
# 取多少训练集的数据作为验证集
validation_ratio = 0.1
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

# 每多少步,打印一次loss
log_per_step = 50

# 数据集所在位置
dataset_dir = Path("./data")
os.makedirs(dataset_dir) if not os.path.exists(dataset_dir) else '' # 当不存在这一文件夹时,就创建它

# 模型存储路径
model_dir = Path("./model/bert_checkpoints")
# 如果模型目录不存在,则创建一个
os.makedirs(model_dir) if not os.path.exists(model_dir) else ''

print("Device:", device)

3. 进行数据读取与数据预处理

数据预处理的常见步骤:

  1. 数据清洗:检查数据中的缺失值、异常值、重复值等情况,并进行相应处理。可以使用插补方法填充缺失值,剔除异常值或者利用统计方法进行处理。
  2. 特征选择:根据实际问题和领域知识,选择最相关和有用的特征。可以使用相关性分析、特征重要性评估等方法进行特征选择。
  3. 特征缩放:将不同尺度或数量级的特征进行缩放,以保证模型的准确性和稳定性。常见的特征缩放方法包括标准化和归一化。
  4. 特征编码:将非数值型的特征转换为数值型,以便模型可以进行处理。可以使用独热编码、标签编码等方法进行特征编码。
  5. 数据集划分:将数据集划分为训练集、验证集和测试集。训练集用于模型训练,验证集用于模型调优和选择,测试集用于评估模型性能。
  6. 处理类别不平衡:如果数据集中存在类别不平衡问题,可以采取一些方法来处理,例如欠采样、过采样等。

具体的预处理方法和步骤会根据具体的数据和问题而有所不同。在实际应用中,根据具体情况选择适当的数据预处理方法非常重要,以提高模型的性能和准确性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 读取数据集,进行数据处理

pd_train_data = pd.read_csv('./data/train.csv')
pd_train_data['title'] = pd_train_data['title'].fillna('') # 缺失值将被替换为空字符串
pd_train_data['abstract'] = pd_train_data['abstract'].fillna('')

test_data = pd.read_csv('./data/test.csv')
test_data['title'] = test_data['title'].fillna('')
test_data['abstract'] = test_data['abstract'].fillna('')
# 将几个字段连接在一起 形成一行文本数据
pd_train_data['text'] = pd_train_data['title'].fillna('') + ' ' + pd_train_data['author'].fillna('') + ' ' + pd_train_data['abstract'].fillna('')+ ' ' + pd_train_data['Keywords'].fillna('')
test_data['text'] = test_data['title'].fillna('') + ' ' + test_data['author'].fillna('') + ' ' + test_data['abstract'].fillna('')+ ' ' + pd_train_data['Keywords'].fillna('')

# 从训练集中随机采样测试集
validation_data = pd_train_data.sample(frac=validation_ratio)
train_data = pd_train_data[~pd_train_data.index.isin(validation_data.index)]# 获取不在验证集索引中的数据行

构建数据集,将数据集划分为训练集、验证集和测试集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# 构建Dataset
class MyDataset(Dataset):

def __init__(self, mode='train'):
super(MyDataset, self).__init__()
self.mode = mode
# 拿到对应的数据
if mode == 'train':
self.dataset = train_data
elif mode == 'validation':
self.dataset = validation_data
elif mode == 'test':
# 如果是测试模式,则返回内容和uuid。拿uuid做target主要是方便后面写入结果。
self.dataset = test_data
else:
raise Exception("Unknown mode {}".format(mode))

def __getitem__(self, index):
# 取第index条
data = self.dataset.iloc[index]
# 取其内容
text = data['text']
# 根据状态返回内容
if self.mode == 'test':
# 如果是test,将uuid做为target
label = data['uuid']
else:
label = data['label']
# 返回内容和label
return text, label

def __len__(self):
return len(self.dataset)

train_dataset = MyDataset('train')
validation_dataset = MyDataset('validation')
# 获取Bert预训练模型
tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased") # 使用Hugging Face库中的AutoTokenizer类来加载预训练的BERT模型的tokenizer

4. 构建训练所需的dataloader与dataset

接构造Dataloader,需要定义一下collate_fn,在其中完成对句子进行编码、填充、组装batch等动作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def collate_fn(batch):
"""
将一个batch的文本句子转成tensor,并组成batch。
:param batch: 一个batch的句子,例如: [('推文', target), ('推文', target), ...]
:return: 处理后的结果,例如:
src: {'input_ids': tensor([[ 101, ..., 102, 0, 0, ...], ...]), 'attention_mask': tensor([[1, ..., 1, 0, ...], ...])}
target:[1, 1, 0, ...]
"""
text, label = zip(*batch) # 带有星号(*)作为前缀的参数表示可变长度的位置参数。
print('text:',text, 'label:',label)
text, label = list(text), list(label)

# src是要送给bert的,所以不需要特殊处理,直接用tokenizer的结果即可 编码后的文本数据。
# padding='max_length' 不够长度的进行填充
# truncation=True 长度过长的进行裁剪
# eturn_tensors=‘pt’: 指定返回PyTorch张量对象。
src = tokenizer(text, padding='max_length', max_length=text_max_length, return_tensors='pt', truncation=True)
print('src:',src)
return src, torch.LongTensor(label)

train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True, collate_fn=collate_fn)
validation_loader = DataLoader(validation_dataset, batch_size=batch_size, shuffle=False, collate_fn=collate_fn)

image.png

下面是对BERT模型的详细介绍:

  1. 架构:BERT模型的核心是Transformer架构,它由多个编码器层组成。每个编码器层都由多头自注意力机制(Multi-Head Self-Attention)和前馈神经网络(Feed-Forward Neural Network)组成。
  2. 预训练阶段:BERT在预训练阶段通过两个自监督任务来学习文本表示:Masked Language Model(MLM)和Next Sentence Prediction(NSP)。
  3. MLM:模型随机地遮盖输入文本的一部分单词,并训练来预测这些被遮盖的单词。这样可以使模型学会理解上下文和句子中的关系以及词汇的表征。
  4. NSP:模型输入两个句子,并判断这两个句子是否相邻。这个任务可以使模型学会理解句子级别的关系和上下文之间的相关性。
  5. 微调阶段:在预训练阶段得到的BERT模型可以在特定的下游任务上进行微调。这些下游任务可能包括文本分类、命名实体识别、问答等。在微调阶段,BERT模型通过在下游任务上进行有监督学习来进一步优化和适应。
  6. 输入表示:BERT模型的输入通常是经过分词(tokenization)后的文本。BERT使用WordPiece分词技术将输入序列拆分为多个子词(subword)。每个子词都有一个唯一的标记,并且可以通过词嵌入得到对应的向量表示。
  7. 输出表示:BERT模型在每一层的输出都包含了每个输入的表示。通常情况下,我们只使用最后一层的输出作为输入文本的表示,也可以使用多层的输出进行组合。
  8. 上下文无关性和上下文敏感性:BERT模型通过上下文无关的方式进行预训练。这意味着模型可以独立地对每个输入进行编码,而不考虑其上下文信息。在微调和应用阶段,BERT模型可以根据需要进行上下文敏感性编码。

BERT模型的优点是能够学习到更好的语言表示,能够根据上下文理解词汇的含义和句子的关系,并在各种下游任务上取得了良好的性能。但它也有一些限制,例如计算资源要求较高,模型较大,需要较长的训练时间。

5. 定义预测模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class MyModel(nn.Module):

def __init__(self):
super(MyModel, self).__init__()

# 加载bert模型
self.bert = BertModel.from_pretrained('bert-base-uncased', mirror='tuna')

# 最后的预测层
self.predictor = nn.Sequential(
nn.Linear(768, 256),
nn.ReLU(),
nn.Linear(256, 1),
nn.Sigmoid()
)

def forward(self, src):
"""
:param src: 分词后的推文数据
"""

# 将src直接序列解包传入bert,因为bert和tokenizer是一套的,所以可以这么做。
# 得到encoder的输出,用最前面[CLS]的输出作为最终线性层的输入

# ".last_hidden_state[:, 0, :]“的操作,从模型的最后一个隐藏状态中提取有用的信息
# 使用切片操作”[:, 0, :]",即保留所有样本的第0个位置的隐藏状态特征,
#对应于BERT模型的CLS(Classification)标记。这个CLS特征通常被用作文本分类或序列标注任务的整体表示。
outputs = self.bert(**src).last_hidden_state[:, 0, :]

# 使用线性层来做最终的预测
return self.predictor(outputs)

model = MyModel()
model = model.to(device)

BERT模型在文本前插入一个[CLS]符号,并将该符号对应的输出向量作为整篇文本的语义表示,用于文本分类,如下图所示。可以理解为:与文本中已有的其它字/词相比,这个无明显语义信息的符号会更“公平”地融合文本中各个字/词的语义信息。

6. 定义出损失函数和优化器

二元交叉熵(Binary Cross Entropy)是一种用于衡量两个概率分布之间差异的损失函数,通常用于二分类问题。
在这里插入图片描述
其中,L表示损失,y是真实标签(取值为0或1),p是模型输出的概率(预测为类别1的概率)。当y为1时,损失函数的第一项起作用,计算的是模型正确预测为类别1的概率的对数。当y为0时,损失函数的第二项起作用,计算的是模型正确预测为类别0的概率的对数。
通过最小化二元交叉熵损失函数,我们可以使得模型对两个类别的分类更加准确。

1
2
3
4
5
6
7
8
9
criteria = nn.BCELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=lr)

# 由于inputs是字典类型的,定义一个辅助函数帮助to(device)
def to_device(dict_tensors):
result_tensors = {}
for key, value in dict_tensors.items():
result_tensors[key] = value.to(device) # 将张量移动到指定的设备上
return result_tensors

7. 定义一个验证方法,获取到验证集的精准率和loss

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def validate():
model.eval() # 将模型切换到评估模式
total_loss = 0.
total_correct = 0
for inputs, targets in validation_loader:
inputs, targets = to_device(inputs), targets.to(device)
outputs = model(inputs)
# view(-1) 可以自动计算其他维度的大小
loss = criteria(outputs.view(-1), targets.float()) # outputs.view(-1) 意味着它会将原始张量中的所有元素展平,并将它们放置在一个单一的维度中
total_loss += float(loss)

# 计算模型的预测结果与目标数据之间的正确预测数量
correct_num = (((outputs >= 0.5).float() * 1).flatten() == targets).sum()
total_correct += correct_num # 累加每个批次的正确预测数量

return total_correct / len(validation_dataset), total_loss / len(validation_dataset)

8. 模型训练,保存最好的模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# 首先将模型调成训练模式
model.train()

# 清空一下cuda缓存
if torch.cuda.is_available():
torch.cuda.empty_cache()

# 定义几个变量,帮助打印loss
total_loss = 0.
# 记录步数
step = 0

# 记录在验证集上最好的准确率
best_accuracy = 0

# 开始训练
for epoch in range(epochs):
model.train()
for i, (inputs, targets) in enumerate(train_loader):
# 从batch中拿到训练数据
inputs, targets = to_device(inputs), targets.to(device)
# 传入模型进行前向传递
outputs = model(inputs)
# 计算损失
loss = criteria(outputs.view(-1), targets.float())
loss.backward()
optimizer.step()
optimizer.zero_grad()

total_loss += float(loss)
step += 1

if step % log_per_step == 0:
print("Epoch {}/{}, Step: {}/{}, total loss:{:.4f}".format(epoch+1, epochs, i, len(train_loader), total_loss))
total_loss = 0

del inputs, targets

# 一个epoch后,使用过验证集进行验证
accuracy, validation_loss = validate()
print("Epoch {}, accuracy: {:.4f}, validation loss: {:.4f}".format(epoch+1, accuracy, validation_loss))
torch.save(model, model_dir / f"model_{epoch}.pt")

# 保存最好的模型
if accuracy > best_accuracy:
torch.save(model, model_dir / f"model_best.pt")
best_accuracy = accuracy

9. 加载最好的模型,然后进行测试集的预测

1
2
3
4
5
model = torch.load(model_dir / f"model_best.pt")
model = model.eval()

test_dataset = MyDataset('test')
test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False, collate_fn=collate_fn)

10. 将测试数据送入模型,得到结果

1
2
3
4
5
6
7
8
9
10
11
results = []
for inputs, ids in test_loader:
outputs = model(inputs.to(device))
outputs = (outputs >= 0.5).int().flatten().tolist()
ids = ids.tolist()
results = results + [(id, result) for result, id in zip(outputs, ids)]


test_label = [pair[1] for pair in results]
test_data['label'] = test_label
test_data[['uuid', 'label']].to_csv('submit_task1_test.csv', index=None)

二、Bert_for_关键词提取

1. 导入前置依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 导入pandas用于读取表格数据
import pandas as pd

# 导入BOW(词袋模型),可以选择将CountVectorizer替换为TfidfVectorizer(TF-IDF(词频-逆文档频率)),注意上下文要同时修改,亲测后者效果更佳
from sklearn.feature_extraction.text import TfidfVectorizer
# 导入Bert模型
from sentence_transformers import SentenceTransformer

# 导入计算相似度前置库,为了计算候选者和文档之间的相似度,我们将使用向量之间的余弦相似度,因为它在高维度下表现得相当好。
from sklearn.metrics.pairwise import cosine_similarity

# 过滤警告消息
from warnings import simplefilter
from sklearn.exceptions import ConvergenceWarning
simplefilter("ignore", category=ConvergenceWarning)

from sklearn.feature_extraction.text import TfidfVectorizer

texts=["dog cat fish","dog cat cat","fish bird", 'bird'] # “dog cat fish” 为输入列表元素,即代表一个文章的字符串
cv = TfidfVectorizer()#创建词袋数据结构
cv_fit=cv.fit_transform(texts)
#上述代码等价于下面两行
#cv.fit(texts)
#cv_fit=cv.transform(texts)

print(cv.get_feature_names()) #['bird', 'cat', 'dog', 'fish'] 列表形式呈现文章生成的词典

print(cv.vocabulary_ ) # {‘dog’:2,'cat':1,'fish':3,'bird':0} 字典形式呈现,key:词,value:词频

print(cv_fit) # 第n个列表元素,**词典中索引为n的元素**, 词频

2. 读取数据集并处理

1
2
3
4
5
6
7
8
9
10
# 读取数据集
test = pd.read_csv('./data/testB.csv')
test['title'] = test['title'].fillna('')
test['abstract'] = test['abstract'].fillna('')


test['text'] = test['title'].fillna('') + ' ' +test['abstract'].fillna('')

# 定义停用词,去掉出现较多,但对文章不关键的词语
stops =[i.strip() for i in open(r'stop.txt',encoding='utf-8').readlines()]

使用n_gram_range来改变结果候选词的词长大小。例如,如果我们将它设置为(3,3),那么产生的候选词将是包含3个关键词的短语。然后,变量candidates就是一个简单的字符串列表,其中包含了我们的候选关键词或者关键短语。

3. Embeddings

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
model = SentenceTransformer(r'xlm-r-distilroberta-base-paraphrase-v1')

test_words = []
for row in test.iterrows():
# 读取第每一行数据的标题与摘要并提取关键词

n_gram_range = (2,2) #要考虑的 n-gram 的范围是 2-gram 到 2-gram,也就是只考虑连续两个词组成的序列。
# 这里我们使用TF-IDF算法来获取候选关键词
count = TfidfVectorizer(ngram_range=n_gram_range, stop_words=stops).fit([row[1].text]) # 从一个文本数据集中创建了一个 TF-IDF 特征计数器(feature counter)。
candidates = count.get_feature_names()
print(candidates)
# 将文本标题以及候选关键词/关键短语转换为数值型数据(numerical data)。我们使用BERT来实现这一目的
title_embedding = model.encode([row[1].title])

candidate_embeddings = model.encode(candidates)

4. Cosine Similarity

要找到与文档最相似的候选词汇或者短语。假设与文档最相似的候选词汇/短语,是能较好的表示文档的关键词/关键短语。为了计算候选者和文档之间的相似度,将使用向量之间的余弦相似度,因为它在高维度下表现得相当好。

1
2
3
4
5
6
7
8
9
# 通过修改这个参数来更改关键词数量
top_n = 35
# 利用文章标题进一步提取关键词
distances = cosine_similarity(title_embedding, candidate_embeddings)
keywords = [candidates[index] for index in distances.argsort()[0][-top_n:]]

if len( keywords) == 0:
keywords = ['A', 'B']
test_words.append('; '.join( keywords))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
print(keywords)

'''
输出结果:
['monomers roasting', 'ara monomers', 'enzyme linked', 'degranulation basophils',
'matrix amount', 'total proteins', 'stimulate degranulation', 'roasting ara',
'allergenicity increase', 'structure ara', 'allergenicity cross', 'allergenicity change',
'proteins iac', 'addition methylation', 'processing roasting', 'food allergy',
'derivatives roasting', 'ara roasted', 'ara matrix', 'processing structure',
'reflect allergenicity', 'oxidation modification', 'allergenicity ara',
'blotting enzyme', 'reduce allergenicity', 'potential allergenicity',
'terms allergenicity', 'roasted matrix', 'peanut allergy', 'matrix peanut',
'methylation oxidation', 'structure allergenicity', 'allergenicity processing',
'allergenicity peanut', 'peanut allergen']

'''

所有的关键词/短语都是如此的相似,所以可以考虑结果的多样化策略。

5. Diversification

结果的多样化需要在关键词/关键短语的准确性(accuracy)和它们之间的多样性(diversity)之间取得一个微妙的平衡(a delicate balance)。使用两种算法来实现结果的多样化。可参考:基于上下文语境的文档关键词提取

  • Max Sum Similarity(最大相似度)
  • Maximal Marginal Relevance(最大边际相关性)

5.1 Max Sum Similarity(最大相似度)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import numpy as np
import itertools

def max_sum_sim(doc_embedding, word_embeddings, words, top_n, nr_candidates):
'''
使用余弦相似度计算文档嵌入向量和候选词嵌入向量之间的相似度。
根据余弦相似度的值,选择具有最高相似度的候选词作为候选集合。
构建候选词之间相似度的矩阵。
使用贪心算法,选择使得相似度之和最大化的词组合作为最终的多样化结果。
'''
# Calculate distances and extract keywords
distances = cosine_similarity(doc_embedding, candidate_embeddings)
distances_candidates = cosine_similarity(candidate_embeddings,
candidate_embeddings)

# Get top_n words as candidates based on cosine similarity
words_idx = list(distances.argsort()[0][-nr_candidates:])
words_vals = [candidates[index] for index in words_idx]
distances_candidates = distances_candidates[np.ix_(words_idx, words_idx)]

# Calculate the combination of words that are the least similar to each other
min_sim = np.inf
candidate = None
for combination in itertools.combinations(range(len(words_idx)), top_n):
sim = sum([distances_candidates[i][j] for i in combination for j in combination if i != j])
if sim < min_sim:
candidate = combination
min_sim = sim

return [words_vals[idx] for idx in candidate]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
max_sum_sim(doc_embedding=title_embedding, 
word_embeddings=candidate_embeddings,
words=candidates,
top_n=10,
nr_candidates=10)

'''
输出结果:
['potential allergenicity',
'terms allergenicity',
'roasted matrix',
'peanut allergy',
'matrix peanut',
'methylation oxidation',
'structure allergenicity',
'allergenicity processing',
'allergenicity peanut',
'peanut allergen']
'''


max_sum_sim(doc_embedding=title_embedding,
word_embeddings=candidate_embeddings,
words=candidates,
top_n=10,
nr_candidates=20)


'''
输出结果:
['derivatives roasting',
'ara roasted',
'ara matrix',
'processing structure',
'oxidation modification',
'reduce allergenicity',
'potential allergenicity',
'peanut allergy',
'matrix peanut',
'allergenicity processing']

'''

较高的nr_candidates值会创造出更多样化的关键词/关键短语,但这并不能很好地代表文档。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
test_words = []
for row in test.iterrows():
# 读取第每一行数据的标题与摘要并提取关键词

n_gram_range = (2,2) #要考虑的 n-gram 的范围是 2-gram 到 2-gram,也就是只考虑连续两个词组成的序列。
# 这里我们使用TF-IDF算法来获取候选关键词
count = TfidfVectorizer(ngram_range=n_gram_range, stop_words=stops).fit([row[1].text]) # 从一个文本数据集中创建了一个 TF-IDF 特征计数器(feature counter)。
candidates = count.get_feature_names()
print(candidates)
# 将文本标题以及候选关键词/关键短语转换为数值型数据(numerical data)。我们使用BERT来实现这一目的
title_embedding = model.encode([row[1].title])

candidate_embeddings = model.encode(candidates)

# 通过修改这个参数来更改关键词数量
top_n = 35
# 利用文章标题进一步提取关键词.
###########################################################################
keywords = max_sum_sim(doc_embedding=title_embedding,
word_embeddings=candidate_embeddings,
words=candidates,
top_n=10,
nr_candidates=10)
###########################################################################
# distances = cosine_similarity(title_embedding, candidate_embeddings)
# keywords = [candidates[index] for index in distances.argsort()[0][-top_n:]]

if len( keywords) == 0:
keywords = ['A', 'B']
test_words.append('; '.join( keywords))

5.2 Maximal Marginal Relevance(最大边际相关性)

最大边际相关性试图在文本摘要任务中最小化冗余(minimize redundancy)和最大化结果的多样性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import numpy as np

def mmr(doc_embedding, word_embeddings, words, top_n, diversity):
'''
使用余弦相似度计算每个候选词与文档嵌入向量的相似度以及候选词之间的相似度。
初始化已选择的关键词列表,首先选择与文档嵌入向量相似度最高的候选词作为第一个关键词。
根据要选择的关键词数量 top_n 进行循环迭代,每次选择与已选择关键词之间边际相关性最大的候选词作为下一个关键词。
更新已选择的关键词列表和候选词列表。
'''
# Extract similarity within words, and between words and the document
word_doc_similarity = cosine_similarity(word_embeddings, doc_embedding)
word_similarity = cosine_similarity(word_embeddings)

# Initialize candidates and already choose best keyword/keyphras
keywords_idx = [np.argmax(word_doc_similarity)]
candidates_idx = [i for i in range(len(words)) if i != keywords_idx[0]]

for _ in range(top_n - 1):
# Extract similarities within candidates and
# between candidates and selected keywords/phrases
candidate_similarities = word_doc_similarity[candidates_idx, :]
target_similarities = np.max(word_similarity[candidates_idx][:, keywords_idx], axis=1)

# Calculate MMR
mmr = (1-diversity) * candidate_similarities - diversity * target_similarities.reshape(-1, 1)
mmr_idx = candidates_idx[np.argmax(mmr)]

# Update keywords & candidates
keywords_idx.append(mmr_idx)
candidates_idx.remove(mmr_idx)

return [words[idx] for idx in keywords_idx]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
mmr(doc_embedding=title_embedding, 
word_embeddings=candidate_embeddings,
words=candidates,
top_n=20,
diversity=0.2)

#---------------------------------------------------------------

mmr(doc_embedding=title_embedding,
word_embeddings=candidate_embeddings,
words=candidates,
top_n=20,
diversity=0.8)

''''
同样的,较高的多样性数值会生成非常多样化的关键词/关键短语
'''

参考

[1] AI夏令营 - NLP实践教程
[2] 基于上下文语境的文档关键词提取