全民AI计划:通过抬杠,了解文本摘要的实现原理

1,972 阅读11分钟

“今天一整天,天气都不错。温度不冷不热,风也刚刚好,很适宜出门。”

“少废话,说重点!”——“今天、天气、适宜”

拣要紧的说,这就是——文本摘要。

我就废话很多,东一句,西一句,有的没的,经常受到读者的批评。

有图为证!

我想,我如何才能像上面那些大牛一样呢?他们没有废话,看问题直达本质。我叽叽歪歪几千字,他们一句话就点透了。

于是,我驱车五个小时,来到一座深山,去请教一位大师。

我一坐下,大师便说:“爱过!”

我说,误会了大师,我是搞程序的,我单身。

大师缓缓睁开眼睛,似乎这突如其来的光线,有点刺眼,他问:“你们不是一块来的?”

我摇了摇头:“大师,我此番前来,想请教如何才能抓住一段话中的重点!”

词频 Term Frequency

大师说:“送你一句话。三番五次,必有大事。

如果一些词语反复出现,那么它们就是重要的关键词。至于其他的词,可以忽略,你少关注即可得重点,去吧!下一位!

我点了点头:“嗯,确实如此!怪不得每次领导开会,一些字眼总是反复强调、多次声明,原来是因为重要啊!”

听完,我立即写了一段代码,去统计一段话中词语出现的次数。一个词在文章中出现的频率,就叫“词频”(Term Frequency,TF)

我选择了一段小组长开早会时的发言。

“今天我们召开这次会议,主题是安全生产。作为一家公司,安全是我们发展的基石,关乎我们每一个人的生命安全和财产安全,因此,我们必须高度重视和加强安全工作。”

python代码如下:

text = "今天 我们 召开 这次 会议 , 主题 是 安全 生产……"
words = text.split(" ")
print(words)

因为要统计关键词,所以先将文本拆分成词语。这一步叫“分词”。英文是不用做分词处理的。因为“Today we are holding this meeting with……”它天然带有空格。

text.split(" ")可以将那段话,以空格为界限拆分成数组。最后我们打印数组,结果如下:

['今天', '我们', '召开', '这次', '会议', ',', '主题', '是', '安全', …… '加强', '安全', '工作', '。']

有了词语的数组,下面我们来统计每个词出现的次数。

python代码如下:

from collections import Counter
count_dict = Counter(words)
sorted_dict = dict(sorted(count_dict.items(), key=lambda x: x[1], reverse=True))
for key, value in sorted_dict.items():
    print(f'[{key}] 出现了 [{value}] 次')

其中,Counter这个类会对数组中重复的元素进行统计。统计结果以字典的形式({"安全":5})保存在count_dict中。随后,我们通过sorted函数进行排序。排序规则为依照每个词出现的次数,由大到小,降序排列。

最终输出结果:

[安全] 出现了 [5][,  ] 出现了 [5][我们] 出现了 [4][是  ] 出现了 [2][。  ] 出现了 [2][的  ] 出现了 [2][和  ] 出现了 [2][今天] 出现了 [1][召开] 出现了 [1] 次
……

我们看到“安全”的词频最大。那么数据显示,这段话的重点就是“安全”。哇哦,小组长开的确实是安全生产会议。

感谢大师!我悟到了!

停用词 Stop Words

后来,我又去找大师。因为,我发现了一个问题。

我用上述算法,对下面这段文本做了关键信息提取。

这是我们老板说的原话,“你们这是争论什么?什么是你的我的他的,争论这没意义,这些客户都是公司的!”

依照规则,排名前三的词语是:

[是  ] 出现了 [3][的  ] 出现了 [3][什么] 出现了 [2][争论] 出现了 [2]

我了个去,这它喵的关键词是什么?

大师说:“我再送你一句话吧。少则得,多则惑。

过于泛滥的东西,必定是不重要的。它只会扰乱你、迷惑你。物以稀为亏,真理往往掌握在少数人的手中。

我仔细一想,还真是这样。比如“的”、“是”、“在”这些词汇。它们本身对于结果是没有作用的,反而还经常占据榜首,属于“占着茅坑不拉屎”的行为。这类词,我们要停止它们的作用,因此就叫“停用词”(Stop Words)

对于停用词,很多机构都整理过具体的文件,我们可以直接用。

比如[ ? ]、[ “ ]、[ ” ]、[ 。 ]……这些符号。或者是“从而”、“其次”、“那个”、“所以”这类无意义的辅助词。

我查了下,大约有成百上千个,有多的有少的,我们可以自己来选择。有时候这可能也和行业有关。比如在农业上,“就在此时”意义不大。但是在小说中,“就在此时”可能很关键,标志着剧情反转,基本上大侠就要出现了。

去掉停用词之后,上述文中的词频最高的是“争论”。

[争论] 出现了 [2][意义] 出现了 [1][客户] 出现了 [1] 次
……

那么我认为,他那句话的重点在于发生了“争论”。

逆文档频率 Inverse Document Frequency

大师开门要去倒尿盆:“咦,你怎么又来了?!”

我说,大师啊,我不想总是找你抬杠。但我统计一段话的关键词时,发现词频都一样,我拿不准啊!

就是这句:“武汉人吃米饭和热干面”。这里,“米饭”和“热干面”出现的频次接近,那到底该选哪一个呢?

“这还不简单”大师提起盆要走,“成年人两个都要!”

我连忙拦住了大师。

大师说:“再送你一句话。不识庐山真面目,只缘身在此山中。

我苦笑一下:“大师,这正话反话,全都让您说了。你先前说重复才重要,后来又说泛滥要忽略。这次,你给我讲明白!”

身处庐山之中,只能看到一部分景色,认识上具有局限性,不要轻易妄下结论。看完大千世界,才能获得更准确的结果。究竟米饭和热干面,哪个能代表武汉?这得放眼全国来看。

“在北京、上海、广州、深圳,米饭出现的多吗?”

我说,日常生活中经常出现。

“那热干面呢?”

我回答,好像只在武汉非常流行。

这就对了!如果一个词并不是随处可见,但在某个场子里出现多次。那么这个词,多数会成为这个场子的代名词。

在那段话里,如果米饭的权重是1,那热干面得是5。出现4次米饭,也抵不上出现1次热干面。这种在大众视野里出镜率高的词,权重值反而会低,而出镜率低的词,权重又会高。这类逆着频次的权重,叫做“逆文档频率”(Inverse Document Frequency, IDF)

上面说的“大众视野”其实类似于一个语料库(corpus)。它是通过采集大量的语言材料,整理成的一个仓库,所以叫语料库

语料库是相对的。这就像是一个人的经历和见识。有的人语料库很丰富,读过很多书,去过很多地方,精通多种语言。也有的人语料库比较匮乏,甚至没有听过“米饭”这个词。

一个词在语料库中的地位,也就是IDF的权重值大不大,这要看它在每篇文章中被提到的频率。如果三句话不离吃喝,那么“吃喝”的权重就很低,它就无法作为一个特征去区别于其他群组。

那么,该如何确定IDF的数值呢?

假如有这么一个小语料库,里面有两句话(这也叫库?为了便于理解,我们甚至把每句话称为一个文档):

  • Java是编程语言。
  • Python是编程语言。

我们可以看到“编程语言”一词,每个文档中都有。而“Java”和“Python”,只有在50%的文档中提到了它们。“IT”这个词,在这个语料库中找不到。

ITF的思路是越常见,值越低;越稀缺,值越高。

我想了想,感觉用“总文档数÷包含该词的数量”能解决。

  • “编程语言”的ITF=总共2个文档÷出现2次=1。

  • “Python”的ITF=总共2个文档÷出现1次=2。

这似乎是实现了越泛滥值越小。

但是,轮到“IT”这个词时,“IT”的ITF=总共2个文档÷出现0次。这出现了分母为0的情况,这是数学错误。

不瞎猜了,还是看人家怎么弄的吧。

逆文档频率(IDF)=log(语料库文档总数÷(包含该词文档数+1))。

这个设计就很巧妙了。

x轴中那个+1,避免了字母为0的情况。当一个词在每个文档中都有的时候,x轴的值向1靠拢,它的权重向0靠近。 红色部分是变量,处于分母的位置。所以,当一个词在语料库出镜率高时,x变小,y变小。反之都会变大。

大师问我听明白了没有?

我说听得很明白。语料库就相当于我的公司,权重就像是找财务催报销,我去10趟,不如总经理去1趟

TF-IDF 代码实践

TF是词频,IDF是逆文档频率。TF-IDF这种统计方法就是“TF词频×IDF词的权重”。

我们是可以用TF-IDF来提取关键信息的。这就类似于通过多大的人物出现了几次来了解情况。如果连总经理都去催了20次,那这事小不了。

下面,我们就用python代码来操作一个例子。

我提供了三段话,出自我之前的博客:

  • 因为训练数据的输入序列格式我们可以控制,我们也用它训练了一个模型。但是当预测时,输入格式是千奇百怪的。
  • 解决问题思路还得开放些,几何不行就用代数。我是编程表演艺术家TF男孩,擅长编程表演。
  • 我们老说时间不够,时间不够,所以代码写的不规范。但是有一天,时间给得很充足,你是否会突然就写好代码了。
import jieba
from sklearn.feature_extraction.text import TfidfVectorizer

# 示例文本
corpus = [
    "因为训练数据的……",
    "解决问题思路还……",
    "我们老说时间不……"
]

# 分词和去除停用词
stop_words = ["的", "是", "在", "了", "有", "和", "中"]
corpus_list = []
for text in corpus:
    words = [word for word in jieba.cut(text) if word not in stop_words]
    corpus_list.append(" ".join(words))

# 创建TfidfVectorizer对象
vectorizer = TfidfVectorizer()
# 将文本转换为tf-idf向量 
tfidf = vectorizer.fit_transform(corpus_list)

# 提取关键词
feature_names = vectorizer.get_feature_names()
top_keywords = {}
for i in range(len(corpus)):
    scores = tfidf[i].toarray()[0]
    keyword_scores = [(feature_names[j], scores[j]) for j in range(len(feature_names))]
    sorted_keyword_scores = sorted(keyword_scores, key=lambda x: x[1], reverse=True)
    top_keywords[i] = sorted_keyword_scores[:3]

# 输出关键词排名
for i in range(len(corpus)):
    print("文章{}的关键词:{}".format(i + 1, top_keywords[i]))

最终结果如下:

文章1的关键词:[('格式', 0.4), ('训练', 0.4), ('输入', 0.4)]
文章2的关键词:[('编程', 0.5), ('tf', 0.2), ('不行', 0.2)]
文章3的关键词:[('时间', 0.5), ('不够', 0.3), ('代码', 0.4)]

上面的代码用了两个类库,jieba(中文分词)和sklearn(scikit-learn,机器学习库)。因此,需要先pip install jiebapip install sklearn

使用jieba库,对示例文本jieba.cut(text)进行分词操作,并去除了一些常见的停用词。

然后,我们创建了一个TfidfVectorizer对象,将分词后的文本集作为参数,传递给fit_transform方法。它是专门为处理TF-IDF封装的方法,它会计算那些权重啥的,还会生成了一个稀疏矩阵。

接下来,我们使用get_feature_names方法获取特征名称,即所有出现过的单词列表。

最后,对于每个文档,我们将每个单词的得分与特征名称,组成一个元组。再将所有元组按得分从高到低排序,取前3个作为最终关键词输出。

这就通过TF-IDF实现了文本的关键信息提取。

更成熟的方案

上面的例子,仅适用于教学演示以及原理讲解。

其实TF-IDF很初级,也存在很多问题,距离真正的文本摘要,还差很远。

如果大家愿意进一步实践,可以去 huggingface.co/ 上找找更高端的NLP模型。

筛选项可以选择TasksNatural Language ProcessingSummarizationLanguages选择Chinese。里面会有很多优秀的模型供您研究与学习。

我是掘金@TF男孩,致力于向大众普及人工智能。