自动打Tag杂记

2021-12-14 08:37:09 浏览数 (1)

给一段文字标记 Tag 是一个很常见的需求,比如我每篇博客下面都有对应的 Tag,不过一般说来,Tag 是数据录入者人为手动添加的,但是对大量用户产生的数据而言,我们不能指望他们能够主动添加合适的 Tag,于是乎就产生了这样的需求:自动打 Tag。

实际上这已经属于 NLP 高大上的范畴了,不是我这种非科班出身的人所能掌控的。好消息是百度和腾讯都有 NLP 平台可供选择,坏消息是免费版的 API 配额极其有限。如果不差钱的话,直接选择 NLP 平台无疑是最方便的,不过对我来说还是基于开源软件自己搭建一套吧,常见选择有 THUTag 和 Jieba,因为 THUTag 是 Java 写的,Jieba 是 Python 写的,而我搞不定 Java,所以就选择了 Jieba。

如果要实现自动打 Tag,那么首先要实现分词,然后选择权重最大的词即可。

Jieba 是如何实现分词的呢?简单点说就是通过 trie 树扫描词典(dict.txt),然后基于词频找到最大切分组合,作者在 issues 里举例说明了实现过程,有兴趣可以看看。更神奇的是,在这个过程中,如果存在词典中没有的词,那么 Jieba 还可以根据 HMM 模型推断出可能的新词,不过这不是我们的重点,就不多说了。

在这里,一个重要的概念是词频(P),其在 Jieba 里的计算方法如下:

代码语言:javascript复制
python> jieba.get_FREQ(...) / jieba.dt.total

下面通过「吴国忠臣伍子胥」这个例子来理解一下分词过程:

代码语言:javascript复制
python> print("/".join(jieba.cut("吴国忠臣伍子胥")))
吴国忠/臣/伍子胥

显而易见,本次 Jieba 的分词是有问题的,为什么没有分词为「吴国/忠臣」呢?理解清楚这个问题,基本就能搞清楚 Jieba 是怎么分词的了:

代码语言:javascript复制
python> from __future__ import unicode_literals

python> import jieba
python> jieba.initialize()

python> jieba.dt.total
60101967
python> jieba.get_FREQ("吴国忠")
14
python> jieba.get_FREQ("臣")
5666
python> jieba.get_FREQ("吴国")
174
python> jieba.get_FREQ("忠臣")
316

因为「P(吴国忠) * P(臣) > P(吴国) * P(忠臣)」,所以出现了错误的结果。此时我们可以通过调整相关词语的词频来解决问题,比如提升忠臣(忠臣啊!)的词频:

代码语言:javascript复制
python> jieba.add_word("忠臣", 456)
python> print("/".join(jieba.cut("吴国忠臣伍子胥")))
吴国/忠臣/伍子胥

说明:456 是怎么来的?「14*5666/174 = 455.9」(本例可以省略 total)。

不过要实现自动打 Tag,光有分词还不够,我们还需要选择权重最大的词。Jieba 中有两种算法可供选择,它们分别是:TF-IDF 和 TextRank:

TF-IDF 指的是如果某个词在一篇文章中出现的频率高,并且在其他文章中出现频率不高,那么认为此词具有很好的类别区分能力,适合用来做关键词(当然,这个过程中要去掉通常无意义的停止词)。举例说明:现在各大门户网站的头版头条永远都是某大大的丰功伟绩,所以可以推定某大大从 TF-IDF 的角度看没有太大的价值。TextRank 则和 PageRank 基本是一个路子:临近的词语互相打分。

  • TF-IDF自动提取文本关键词
  • TextRank自动提取文本关键词

两种算法相比较,TF-IDF 要训练出一个 idf.txt 数据,而 TextRank 则不需要,看上去似乎 TextRank 更方便,但是从我的实际测试结果来看,对于短文本来说,如果有一个高质量的 idf 数据,那么 TF-IDF 的效果要比 TextRank 好得多。

如果你面对的是更专业化的语境,那么你可能想尝试构建自己的 idf.txt,操作前记得先倒入自定义的词典(userdict.txt),另外需要说明的是语料数据(data.txt)以行为单位:

代码语言:javascript复制
#!/usr/bin/env python
#-*- coding: utf-8 -*-

from __future__ import print_function

import math
import sys
import jieba

jieba.load_userdict("userdict.txt")

data = {}
total = 0

with open('data.txt') as f:
    for line in f:
        line = line.decode("utf-8")
        words = [w for w in jieba.cut(line) if w in jieba.dt.FREQ]

        for word in words:
            data[word] = data.get(word, 0.0)   1.0

        total  = 1

        if total % 10000 == 0:
            print(total, file=sys.stderr)

data = [(k, math.log(total / v)) for k, v in data.iteritems()]

for k, v in data:
    print(k.encode("utf-8"), v)

如果需要抓取语料数据的话,推荐使用 requests lxml,以百度的停止词为例:

代码语言:javascript复制
#!/usr/bin/env python
#-*- coding: utf-8 -*-

from __future__ import print_function

import requests
from lxml import html

page = requests.get("http://www.baiduguide.com/baidu-stopwords/")
tree = html.fromstring(page.text)
elements = tree.xpath("//div/p[preceding-sibling::*[1][name()='h4']]/text()")

for element in elements:
    words = [e.strip() for e in element.split(",") if e.strip()]
    print(*words, sep="n")

按照 xpath 来抓取数据,关键是抓取规则,推荐 chrome xpath helper:

XPath Helper

最后看看我的成果吧,我大概收集了几百万条汽车维修方面的数据,然后通过 Jieba 自动给每条数据打 Tag,接着把得到的 Tag 以 Tag Cloud 的形式展示出来:

Tag Cloud

怎么样,通过 Jieba 自动的打 Tag,我们很清晰的可以看出在汽车维修领域,用户最容易遇到的问题是什么。嗯,估计有人会问这个 Tag Cloud 怎么画的,点这里。不谢!

0 人点赞