本章介绍如何依据内容对文档进行分类,文档分类是机器智能(machine intelligence)的一个应用。
例子:
基于规则的分类器(rule-based classifier),使用时会有人事先设计好一组规则,用以指明某条信息是否属于垃圾信息。
存在的问题是
解决:程序在开始阶段逐渐收到更多消息之后,根据人们提供给它的有关哪些是垃圾邮件,哪些不是垃圾邮件的信息,不断地进行学习。通过这样的方式,我们可以分别为不同的用户、群组或网站建立起各自的应用实例和数据集,它们对垃圾信息的界定将逐步形成自己的观点。
分类器需要用某些特征来对不同的内容项进行分类。所谓特征,是指任何可以用来判断内容中具备或缺失的东西。 在对文档进行分类时,内容即是文档,特征则是文档中的单词。单词作为特征时,其假设是:某些单词相对而言更有可能出现在垃圾信息中。
# docclass.py
import re
import math
def getwords(doc):
splitter = re.compile('\\W*')
# 根据非字母字符进行单词拆分
words = [s.lower() for s in splitter.split(doc) if len(s)>2 and len(s)<20]
# 只返回一组不重复的单词
return {w:1 for w in words}
选择特征集时需要做大量的权衡,而且还要不断地进行调整
class classifier:
def __init__(self, getfeatures, filename=None):
# 统计特征/分类组合的数量
self.fc = {}
# 统计每个分类中的文档数量
self.cc = {}
self.getfeatures = getfeatures
def setdb(self, dbfile):
self.con = sqlite.connect(dbfile)
self.con.execute('create table if not exists fc(feature,category,count)')
self.con.execute('create table if not exists cc(category,count)')
# 类方法不会直接引用这些字典,因为这会有碍于将训练数据存入文件或数据库的潜在选择。加入下列辅助函数
def incf(self, f, cat):
'''增加对特征/分类组合的计数值'''
#self.fc.setdefault(f, {})
#self.fc[f].setdefault(cat, 0)
#self.fc[f][cat] += 1
count = self.fcount(f, cat)
if count == 0:
self.con.execute("insert into fc values ('%s','%s'1)" % (f, cat))
else:
self.con.execute(
"update fc set count=%d where feature='%s' and category='%s'" % (count+1,f,cat))
def incc(self, cat):
'''增加对某一分类的计数值'''
#self.cc.setdefault(cat, 0)
#sele.cc[cat] += 1
count=self.catcount(cat)
if count==0:
self.con.execute("insert into cc values ('%s',1)" % (cat))
else:
self.con.execute("update cc set count=%d where category='%s'"
% (count+1,cat))
def fcount(self, f, cat):
'''某一特征出现于某一分类中的次数'''
#if f in self.fc and cat in self.fc[f]:
# return float(self.fc[f][cat])
#return 0.0
res=self.con.execute(
'select count from fc where feature="%s" and category="%s"'
%(f,cat)).fetchone()
if res==None: return 0
else: return float(res[0])
def catcount(self, cat):
'''属于某一分类的内容项数量'''
#if cat in self.cc:
# return float(self.cc[cat])
#return 0
res=self.con.execute('select count from cc where category="%s"'
%(cat)).fetchone()
if res==None: return 0
else: return float(res[0])
def totalcount(self):
'''所有内容项的数量'''
#return sum(self.cc.values())
res=self.con.execute('select sum(count) from cc').fetchone();
if res==None: return 0
return res[0]
def categories(self):
'''所有分类的列表'''
#return self.cc.keys()
cur=self.con.execute('select category from cc');
return [d[0] for d in cur]
def train(self, item, cat):
'''将内容拆分为彼此独立的各个特征。
针对该分类为每个特征增加计数值。增加对该分类的总计数值'''
features = self.getfeatures(item)
# 针对该分类为每个特征增加计数值
for f in features:
self.incf(f, cat)
# 增加针对该分类的计数值
self.incc(cat)
self.con.commit()
def fprob(self, f, cat):
# 给定分类的单词概率——分类cat中出现特征f的概率
if self.catcount(cat) == 0: return 0
# 特征在分类中出现的总次数,除以分类中包含内容项的总数
return self.fcount(f, cat)/self.catcount(cat)
def weightedprob(self, f, cat, prf, weight=1.0, ap=0.5):
'''在我们手头掌握的有关当前特征的信息极为有限时,我们还需要根据一个假设的概率来作出判断'''
# 计算当前的概率值
basicprob = prf(f, cat)
# 统计特征在所有分类中出现的次数
totals = sum([self.fcount(f, c) for c in self.categories()])
# 计算加权平均
bp = ((weight*ap)+(totals*basicprob))/(weight+totals)
return bp
# cl = classifier(getwords)
# cl.train('the quick brown fox jumps over the lazy dog', 'good')
# cl.train('make quick money in the online casino', 'bad')
# cl.fcount('quick', 'good')
# cl.fcount('quick', 'bad')
def sampletrain(cl):
cl.train('Nobody owns the water', 'good')
cl.train('the quick rabbit jumps fences', 'good')
cl.train('buy pharmaceuticals now', 'bad')
cl.train('make quick money at the online casino', 'bad')
cl.train('the quick brown fox jumps', 'good')
fprob:给定分类的单词概率——分类cat中出现特征f的概率 为条件概率,记为Pr(A|B),在给定B的条件下A的概率 本例中,Pr(word|classification) classifier.fprob(self, f, cat)
sampletrain(cl) cl.fprob('quick', 'good')
fprob方法针对目前为止见到过的特征与分类,给出了一个精确的结果。但它有一个小问题——只根据以往见过的信息,会令其在训练的初期阶段,对那些极少出现的单词变得异常敏感。
为解决上述问题,在我们手头掌握的有关当前特征的信息极为有限时,我们还需要根据一个假设的概率来作出判断。一个推荐的初始值是0.5.我们还要为假设的概率赋以多大的权重——权重为1代表假设概率的权重与一个单词相当。经过加权的概率值返回的是一个有getprobability与假设概率组成的加权平均。
classifier.weightedprob(self, f, cat, prf, weight=1.0, ap=0.5)
sampletrain(cl)
cl.weightedprob('money', 'good', cl.fprob)
sampletrain(cl)
cl.weightedprob('money', 'good', cl.fprob)
将各个单词的概率进行组合,从而得出整篇文档属于该分类的概率。 本章将考查两种不同的分类方法。
朴素贝叶斯分类器:朴素指的是它假设将要被组合的各个概率是彼此独立的。即,一个单词在属于某个指定分类的文档中出现的概率,与其他单词出现于该分类的概率是不相关的。
事实上文章中的各个单词并不是独立的,这意味着我们无法采用朴素贝叶斯分类器所求得的结果实际用作一篇文档属于某个分类的概率。不过,我们还是可以对各个分类的计算结果进行比较,然后在看哪个分类的概率最大。在现实中,若不考虑假设的潜在缺陷,朴素贝叶斯分类器将被证明是一种非常有效的文档分类方法。
假设概率的彼此独立性 Pr(Document|Category) = Pr(w1|Category) * Pr(w2|Category)...
class naivebayes(classifier):
def __init__(self, getfeatures):
classifier.__init__(self, getfeatures)
self.thresholds = {}
def setthreshold(self, cat, t):
self.thresholds[cat] = t
def getthreshold(self, cat):
if cat not in self.thresholds: return 1.0
return self.thresholds[cat]
def docprob(self, item, cat):
'''得到整篇文档的概率'''
features = self.getfeatures(item)
# 将所有特征的概率相乘
p = 1
for f in features: p *= self.weightedprob(f, cat, self.fprob)
return p
def prob(self, item, cat):
'''计算分类的概率 Pr(Category|Document)
= Pr(Document|Category) * Pr(Category) / Pr(Document)
'''
catprob = self.catcount(cat)/self.totalcount()
docprob = self.docprob(item, cat)
return docprob * catprob
def classify(self, item, default=None):
probs = {}
# 寻找概率最大的分类
max = 0.0
for cat in self.categories():
probs[cat] = self.prob(item, cat)
if probs[cat] > max:
max = probs[cat]
best = cat
# 确保概率超出域值*次大概率值
for cat in probs:
if cat == best: continue
if probs[cat] * self.getthreshold(best)>probs[best]: return default
return best
贝叶斯定理是一种对条件概率进行调换求解(flipping around)的方法,通常写作: Pr(A|B) = Pr(B|A) Pr(A) / Pr(B) 在本例中,即为: Pr(Category|Document) = Pr(Document|Category) Pr(Category)/Pr(Document) Pr(Document|Category) = Pr(w1|Category) * Pr(w2|Category)... Pr(Category)是随机选择一篇文档属于该分类的概率,因此就是属于该分类的文档数除以文档的总数。
naivebayes.prob(self, item, cat)
cl = naivebayes(getwords)
sampletrain(cl)
cl.prob('quick rabbit', 'good')
cl.prob('quick rabbit', 'bad')
构造朴素贝叶斯分类器的最后一个步骤是实际判定某个内容项所属的分类。 此处最简单的方法是计算被考查内容在每个不同分类中的概率,然后选择概率最大的分类。 在许多问题中无法将各个分类同等看待,如与将正常邮件归为垃圾邮件相比,偶尔收到几封垃圾邮件还是可以容忍的。在另一些应用中承认不知道答案,要好过判断答案就是概率值稍大一些的分类。
为解决这一问题,我们可以为每个分类定义一个最小阈值。 以垃圾邮件过滤为例,假如过滤到“bad”分类的阈值为3,则针对“bad”分类的概率就必须至少3倍于针对“good”分类的概率才行。假如针对“good”分类的阈值为1,则对于任何邮件,只要概率确实大于针对“bad“分类的概率,它就是属于”good“分类的。任何有可能属于”bad“分类,但概率并没有超过3倍以上的邮件,都将被划归到”未知“分类中。
# 阈值操作
classifier.__init__(self, getfeatures)
classifier.setthreshold(self, cat, t)
classifier.getthreshold(self, cat)
# 分类
classifier.classify(self, item, default=None)
cl = naivebayes(getwords)
sampletrain(cl)
cl.classify('quick rabbit', default='unknown')
cl.classify('quick money', default='unknown')
cl.setthreshold('bad', 3.0)
cl.classify('quick money', default='unknown')
for i in range(10): sampletrain(cl)
cl.classify('quick money', default='unknown')
以R.A.Fisher的名字命名的费舍尔方法,是朴素贝叶斯方法的一种替代方案,它可以给出非常精确的结果,尤其适合垃圾信息过滤。
与朴素贝叶斯过滤器利用特征概率来计算整篇文档的概率不同,费舍尔方法为文档中的每个特征都求得了分类的概率,然后又将这些概率组合起来,并判断其是否可能构成一个随机集合。尽管该方法更为复杂,但是因为它在分类选择临界值(cutoff)时允许更大的灵活性,所以还是值得一学的。
cprob:给定单词的分类概率——特征f出现在cat中的概率 Pr(category|feature) (具有指定特征的属于某分类的文档数)/(具有指定特征的文档总数)
为了进行归一化计算,函数将分别求得3个量
class fisherclassifier(classifier):
def __init__(self, getfeatures):
classifier.__init__(self, getfeatures)
self.minimums = {}
def setminimum(self, cat, min):
self.minimums[cat] = min
def getminimun(self, cat):
if cat not in self.minimums: return 0
return self.minimums[cat]
def cprob(self, f, cat):
'''特征f出现在cat中的概率'''
# 特征在该分类中出现的频率
clf = self.fprob(f, cat)
if clf == 0: return 0
# 特征在所有分类中出现的频率
freqsum = sum([self.fprob(f,c) for c in self.categories()])
# 概率等于特征在该分类中出现的频率除以总体频率
p = clf/(freqsum)
return p
def fisherprob(self, item, cat):
# 将所有概率值相乘
p = 1
features = self.getfeatures(item)
for f in features:
p *= (self.weightedprob(f, cat, self.cprob))
# 取自然对数,并乘以-2
fscore = -2 * math.log(p)
# 利用倒置对数卡方函数求得概率
return self.invchi2(fscore, len(features)*2)
def invchi2(self, chi, df):
m = chi / 2.0
sum = term = math.exp(-m)
for i in range(1, df//2):
term *= m / i
sum += term
return min(sum, 1.0)
def classify(self, item, default=None):
# 循环遍历并寻找最佳结果
best = default
max = 0.0
for c in self.categories():
p = self.fisherprob(item, c)
# 确保其超过下限值
if p > self.getminimun(c) and p > max:
best = c
max = p
return best
# cl = fisherclassifier(getwords)
# sampletrain(cl)
# cl.cprob('quick', 'good')
# cl.cprob('money', 'bad')
对于因为算法接触单词的次数太少,所以它有可能会对概率值估计过高。因此可以对概率进行加权处理
cl.weightedprob('money', 'bad', cl.cprob)
费舍尔方法返回的结果是对概率的一种更好的估计,这对于结果报告或临界值判断而言是非常有价值的。
费舍尔方法的计算过程是将所有概率相乘起来,然后取自然对数,在将结果乘以-2
fisherclassifier.fisherprob(self, item, cat)
cl = fisherclassifier(getwords)
sampletrain(cl)
cl.cprob('quick', 'good')
cl.fisherprob('quick rabbit', 'good')
cl.fisherprob('quick rabbit', 'bad')
我们可以利用fisherprob的返回值来决定如何进行分类。不像贝叶斯过滤器那样需要乘以阈值,此处我们可以为每个分类指定下限。 在垃圾信息过滤器中,我们可以将“bad”分类的下限值设得很高,如0.6,将“good”分类的下限值设置得很低,比如0.2.任何针对“good”分类的分值低于0.2,针对“bad”分类的分值低于0.6的邮件,将被划归到“未知”分类中。
fisherclassifier.__init__(self, getfeatures)
fisherclassifier.setminimum(self, cat, min)
fisherclassifier.getminimun(self, cat)
fisherclassifier.classify(self, item, default=None)
sampletrain(cl)
cl.classify('quick rabbit')
cl.classify('quick money')
cl.setminimum('bad', 0.8)
cl.classify('quick money')
cl.setminimum('good', 0.4)
cl.classify('quick money')
import sqlite3 as sqlite
# 需要修改的函数
classifier.setdb(self, dbfile)
classifier.incf(self, f, cat)
classifier.fcount(self, f, cat)
classifier.incc(self, cat)
classifier.catcount(self, cat)
classifier.categories(self)
classifier.totalcount(self)
cl = fisherclassifier(getwords)
cl.setdb('test1.db')
sampletrain(cl)
cl2 = naivebayes(getwords)
cl2.setdb('test1.db')
cl2.classify('quick money')
import feedparser
import re
# Takes a filename of URL of a blog feed and classifies the entries
def read(feed,classifier):
# Get feed entries and loop over them
f=feedparser.parse(feed)
for entry in f['entries']:
print
print '-----'
# Print the contents of the entry
print 'Title: '+entry['title'].encode('utf-8')
print 'Publisher: '+entry['publisher'].encode('utf-8')
print
print entry['summary'].encode('utf-8')
# Combine all the text to create one item for the classifier
fulltext='%s\n%s\n%s' % (entry['title'],entry['publisher'],entry['summary'])
# Print the best guess at the current category
print 'Guess: '+str(classifier.classify(entry))
# Ask the user to specify the correct category and train on that
cl=raw_input('Enter category: ')
classifier.train(entry,cl)
# cl = fisherclassifier(getwords)
# cl.setdb('python_feed.db')
# read('python_search.xml', cl)
# cl.cprob('python', 'prog')
# cl.cprob('python', 'snake')
# cl.cprob('python', 'monty')
# cl.cprob('eric', 'monty')
# cl.fprob('eric', 'monty')
几种不同的方法可以对其加以改进
def entryfeatures(entry):
splitter=re.compile('\\W*')
f={}
# Extract the title words and annotate
titlewords=[s.lower() for s in splitter.split(entry['title'])
if len(s)>2 and len(s)<20]
for w in titlewords: f['Title:'+w]=1
# Extract the summary words
summarywords=[s.lower() for s in splitter.split(entry['summary'])
if len(s)>2 and len(s)<20]
# Count uppercase words
uc=0
for i in range(len(summarywords)):
w=summarywords[i]
f[w]=1
if w.isupper(): uc+=1
# Get word pairs in summary as features
if i<len(summarywords)-1:
twowords=' '.join(summarywords[i:i+1])
f[twowords]=1
# Keep creator and publisher whole
f['Publisher:'+entry['publisher']]=1
# UPPERCASE is a virtual word flagging too much shouting
if float(uc)/len(summarywords)>0.3: f['UPPERCASE']=1
return f
cl = fisherclassifier(entryfeatures)
cl.setdb('python_feed.db')
read('python_search.xml', cl)
??
本章介绍的两个分类器都是监督型学习方法(supervised learning methods)的例子 第4章中的人工神经网络是另一个监督型学习的例子。通过将特征作为输入,并令输出代表每一种可能的分类,我们也可以将神经网络用于本章中的相同问题。 第9章中介绍的支持向量机(support vector machines),也可以用于解决本章的问题
贝叶斯分类器被常用于文档分类的原因是,与其他方法相比它所要求的计算资源更少。 相对与神经网络的复杂性导致了其在理解上的困难,我们可以清楚地看到单词的概率,以及它们对最终分值的实际贡献有多大,而对于网络中两个神经元之间的连接强度而言,则并不存在同样简单的解释
另一方面,神经网络和支持向量机有一个很大的优势:它们可以捕捉到输入特征之间更为复杂的关系。神经网络中,某个特征的概率可能会依据其他特征的存在或缺失而改变。也许你正在试图阻止赌博的垃圾信息,但是有对跑马很感兴趣,在这种情况下,只有电子邮件中的其他地方没有出现单词'horse'时,单词'casino'才被认为'bad'的。朴素贝叶斯分类器无法捕获这样的相互依赖性,而神经网络却是可以的。
输入层:文档特征(单词,作者等),隐含层:特征集合,输出层:分类类别