给一段文字标记 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 怎么画的,点这里。不谢!