在工作中,我们常常会遇到这样的场景:产品同事发来一段长长的活动文案,希望我们能从中自动提取几个关键词,用于标签、搜索或推荐。当文本只有一段,而非海量语料时,如何快速、准确地揪出那些最具代表性的词汇呢?
用 Python 来解决这个问题,有多种成熟的思路,从简单高效的统计方法到利用预训练模型的语义方法各有千秋。本文将介绍四种实用的方法,并提供可直接运行的代码示例。
首先,我们约定一个示例文本,后续所有方法都将基于它进行演示:
text = """
双十一大促活动正式开始!今年我们主打「环保生活」主题,
所有绿色商品满200减50,新用户还可以叠加一张20元优惠券。
活动期间下单的用户,将有机会获得限量帆布袋和不锈钢保温杯。
"""
方法一:词频统计与停用词过滤
这是最基础也最可控的方法。思路非常直接:将文本分词,统计每个词出现的频率,过滤掉常见的无意义词(停用词),最后按词频排序。
以下是可直接运行的代码实现:
import jieba
from collections import Counter
# 简单的停用词列表,实际应用中建议从文件加载更全的版本
STOPWORDS = set([
"的", "了", "和", "是", "我们", "还有", "可以",
"将", "一个", "以及", "进行", "用户", "新用户",
])
def extract_keywords_by_freq(text: str, top_k: int = 10):
# 分词
words = jieba.lcut(text)
# 过滤:长度>=2,且不在停用词里
filtered = [
w for w in words
if len(w) >= 2 and w not in STOPWORDS
]
# 统计词频
counter = Counter(filtered)
# 取前 top_k 个
return counter.most_common(top_k)
if __name__ == "__main__":
print(extract_keywords_by_freq(text, top_k=8))
运行结果可能如下:
[('活动', 2), ('大促', 1), ('正式开始', 1), ('环保生活', 1),
('绿色商品', 1), ('优惠券', 1), ('限量帆布袋', 1), ('保温杯', 1)]
可以看到,结果中混入了一些如“正式开始”这样可能并不理想的词。但此方法的优势非常明显:
- 完全透明可控:任何结果都可追溯,易于调试。
- 灵活调整权重:可以方便地手动增加或减少特定词的权重。
- 速度极快:非常适合嵌入小程序、脚本或对实时性要求高的场景。
如果你的项目刚起步,或仅需处理日志分析、运维脚本等任务,这个简单方案往往就足够了。
方法二:TF-IDF算法
单纯统计词频有一个问题:它只关注词在当前文本中的出现次数。例如,在一批技术文档中,“错误”、“接口”等词可能频繁出现,但真正有区分度的可能是某个具体的“错误码”。
TF-IDF(词频-逆文档频率)引入了“全局视野”。其核心思想是:
- TF(词频):词在当前文档中出现的频率越高,其TF值越高。
- IDF(逆文档频率):包含该词的文档越少,其IDF值越高(越稀有)。
- TF-IDF:将两者相乘,得分高的词被认为是当前文档更具代表性的关键词。
严格来说,TF-IDF需要在多篇文档构成的语料库上计算。但在单文档场景下,我们可以构建一个小型的背景语料库来模拟这个过程。
import jieba
from sklearn.feature_extraction.text import TfidfVectorizer
# 假设这是你系统中已有的其他文案,作为背景语料
corpus = [
“双十一活动预热中,全场商品五折起。”,
“夏季清凉节,主打轻薄服饰和防晒用品。”,
“环保主题周,推广可降解包装和循环利用。”,
]
def jieba_tokenizer(text: str):
return [w for w in jieba.lcut(text) if len(w) >= 2]
def extract_keywords_by_tfidf(text: str, top_k: int = 10):
# 将当前文本加入语料库
all_docs = corpus + [text]
vectorizer = TfidfVectorizer(
tokenizer=jieba_tokenizer,
stop_words=list(STOPWORDS),
)
tfidf_matrix = vectorizer.fit_transform(all_docs)
# 矩阵的最后一行对应我们的目标文本
tfidf_vec = tfidf_matrix[-1]
feature_names = vectorizer.get_feature_names_out()
# 提取非零权重项
coo = tfidf_vec.tocoo()
pairs = list(zip(coo.col, coo.data))
# 按权重降序排序
pairs = sorted(pairs, key=lambda x: x[1], reverse=True)[:top_k]
keywords = [(feature_names[idx], float(score)) for idx, score in pairs]
return keywords
if __name__ == "__main__":
print(extract_keywords_by_tfidf(text, top_k=8))
与纯词频法相比,TF-IDF能有效抑制“活动”、“商品”等常见词的权重,同时提升像“环保生活”、“帆布袋”这类更具主题特色词汇的排名。
需要注意的几点:
- 需要维护一个具有代表性的背景语料库,否则IDF计算可能不准。
- 对于只出现一次的长尾词,其权重可能被低估。
- 本质上仍是基于词面统计,无法理解语义。
尽管如此,在运营文案分析、日志分类等场景中,TF-IDF通常能提供显著优于词频统计的效果。
TextRank算法借鉴了Google的PageRank思想,将文本中的词汇视为图中的节点。如果两个词在一定的窗口范围内共现,则认为它们之间存在一条边。通过迭代计算每个节点的权重(重要性),最终得分高的词即为关键词。
对于中文文本,我们可以直接使用 jieba 库中集成的TextRank接口,非常方便。
首先确保已安装 jieba:
pip install jieba
使用示例:
import jieba.analyse as analyse
def extract_keywords_by_textrank(text: str, top_k: int = 10):
# allowPOS 参数可以限定词性,例如只提取名词、动词等
keywords = analyse.textrank(
text,
topK=top_k,
withWeight=True,
allowPOS=('ns', 'n', 'vn', 'v') # 地名、名词、动名词、动词
)
return keywords
if __name__ == "__main__":
print(extract_keywords_by_textrank(text, top_k=8))
TextRank与TF-IDF的侧重点不同,形成了有趣的互补:
- TF-IDF 看重的是“词本身的统计特性”(局部频率和全局稀有度)。
- TextRank 看重的是“词在文本结构中的位置关系”(通过共现网络计算出的中心性)。
例如,“优惠券”可能只出现一两次,但如果它周围频繁出现“满减”、“活动”、“下单”等重要词汇,TextRank算法就会给它一个较高的权重,认为它是语义网络中的关键节点。
方法四:基于预训练模型与语义相似度(KeyBERT思路)
前述方法均基于词面信息。随着预训练语言模型的普及,我们可以利用语义信息进行关键词提取。其核心思路是:将整个文档和候选短语分别编码成语义向量,然后计算它们之间的余弦相似度,相似度越高的候选短语,其语义越能代表整个文档。
KeyBERT 是一个实现此思路的流行库。它使用 sentence-transformers 来生成向量。
首先安装必要库:
pip install keybert sentence-transformers
代码实现如下:
from keybert import KeyBERT
import jieba
# 初始化模型,这里选用一个多语言小模型作为示例,可根据需要更换
kw_model = KeyBERT(model="paraphrase-multilingual-MiniLM-L12-v2")
def extract_keywords_by_bert(text: str, top_k: int = 10):
# 预处理:分词并过滤,用空格连接起来作为输入
tokens = [w for w in jieba.lcut(text) if len(w) >= 2 and w not in STOPWORDS]
joined = " ".join(tokens)
# use_maxsum 或 use_mmr 参数可以用于减少结果中语义重复的短语
keywords = kw_model.extract_keywords(
joined,
keyphrase_ngram_range=(1, 3), # 考虑1到3个词的短语
stop_words=None,
top_n=top_k,
use_maxsum=True,
nr_candidates=20,
)
return keywords
if __name__ == "__main__":
print(extract_keywords_by_bert(text, top_k=8))
这种方法的特点:
- 环境要求较高:需要下载预训练模型,适合在服务端或离线任务中运行。
- 能捕捉语义相似性:对于“绿色商品”、“环保好物”等不同表述但含义相近的短语,具有更好的识别能力。
- 计算开销较大:速度比统计方法慢,但在单文本场景下通常可接受。
如果你在构建“长文自动打标签”、“知识库智能索引”等系统,基于语义的方法往往能带来质的提升。生产环境中建议为模型推理添加缓存机制以提升性能。
工程实践:封装统一调用接口
在实际业务开发中,我们不应将不同方法的调用代码散落在各处。一个良好的实践是定义一个统一的入口函数,通过参数来切换不同的提取策略。
from enum import Enum
class KeywordMethod(str, Enum):
FREQ = "freq"
TFIDF = "tfidf"
TEXTRANK = "textrank"
BERT = "bert"
def extract_keywords(text: str,
top_k: int = 10,
method: KeywordMethod = KeywordMethod.FREQ):
if method == KeywordMethod.FREQ:
return extract_keywords_by_freq(text, top_k)
elif method == KeywordMethod.TFIDF:
return extract_keywords_by_tfidf(text, top_k)
elif method == KeywordMethod.TEXTRANK:
return extract_keywords_by_textrank(text, top_k)
elif method == KeywordMethod.BERT:
return extract_keywords_by_bert(text, top_k)
else:
raise ValueError(f"不支持的关键词提取方法: {method}")
在业务代码中可以这样清晰、灵活地调用:
if __name__ == "__main__":
for m in KeywordMethod:
print("=== 方法:", m, "===")
print(extract_keywords(text, top_k=5, method=m))
print()
这种设计允许你在项目初期使用简单的 freq 或 tfidf 方法快速上线,后续再针对特定场景平滑地迁移到更先进的 textrank 或 bert 方法,并具备快速回滚的能力。
总结与选择建议
四种方法各有优劣,适用场景也不同:
- 词频统计:简单、快速、可控,适用于对实时性要求高、逻辑需完全透明的场景,如日志实时监控、小型脚本工具。
- TF-IDF:在统计基础上引入了文档集的对比,能提升主题关键词的区分度,适用于文档分类、内容标签化等需要一定区分能力的场景。
- TextRank:基于图模型,关注词汇在文本结构中的重要性,能提取出在语义网络中处于中心位置的词,适用于捕捉文章核心概念。
- 基于语义模型:能理解词语的深层含义,对同义词、近义词有更好的概括能力,适用于对准确性要求高的知识管理、智能推荐等场景,但需考虑计算成本和模型管理。
对于 Python 开发者而言,掌握从基础的 jieba 分词到利用 scikit-learn 和现代预训练模型进行文本处理,是构建智能应用的重要一环。你可以根据实际业务的数据规模、性能要求和效果预期,灵活选择或组合这些方法。如果想了解更多 Python 在数据处理和自动化方面的实战技巧,可以持续关注云栈社区的相关技术分享。