完整手写一个朴素贝叶斯分类器,完成文本分类

Posted 大数据挖掘DT数据分析

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了完整手写一个朴素贝叶斯分类器,完成文本分类相关的知识,希望对你有一定的参考价值。






Part 1: 本文解决的问题:
我在有这样的一个数据集,里面存放了人们对近期播放电影的评价,当然评价也就分成两部分,好评和差评。我们想利用这些数据训练一个模型,然后可以自动的对影评做出判断,到底是好评还是差评,差评的话,那么我们赶紧删掉它,哈哈。
好吧,这就是自然语言处理领域的基本问题:文本分类。文本分类在我们的日常生活中有非常多的应用,最有名的当属垃圾邮件过滤啦。我们肯定希望不要受到垃圾邮件,但是我们更不希望正常的邮件被当做垃圾邮件过滤掉了。这对我们分类的精度提出了很高的要求。



Part 4: 代码编写
Part4.1:文档和单词

新建一个文件,命名为docclass.py,里面加入一个getwords的函数,完成从文本中提取特征。

def getwords(doc):
    splitter = re.compile('\\W*')
    words = [s.lower() for s in splitter.split(doc) if len(s) > 2 and len(s) < 20]
    # 过滤掉单词中包含数字的单词
    words = [word for word in words if word.isalpha()]
    with open(r'E:\研究生阶段课程作业\python\好玩的数据分析\stopwords.txt') as f:
        stopwords = f.read()
    stopwords = stopwords.split('\n')
    stopwords = set(stopwords)
    # 过滤掉一些经常出现的单词,例如 a,an,we,the
    words = [word for word in words if word not in stopwords]
    return set(words)

该函数的输入一个文档,一般来说是一个大的字符串,我们首先使用正则表达式划分单个单词,对于一些特别常见的单词,例如a,an,the,these,这些毫无意义的单词,我们都保存在stopwords 中,并进行过滤,最后返回一组文档中不重复的单词(所有的单词都是小写的形式)。



Part4.2: 编写分类器

新建一个classifier的类:

class classifier:
    def __init__(self, getfeatures):
        # Counts of feature/category combinations
        self.fc = {}
        # Counts of documents in each category
        self.cc = {}
        self.getfeatures = getfeatures

该类中有三个实例变量:fc,cc, getfeatures.
变量fc将记录位于各分类中不同特征的数量。例如:
{'python': {'bad': 0, 'good': 6}, 'money': {'bad': 5, 'good': 1}}
上述示例表明,单词'money'被划归'bad'类文档中已经出现了5次,而被划为'good'类只有1次,单词'python'被划归'bad'类文档中已经出现了0次,而被划为'good'类有6次。

变量cc是一个记录各分类被使用次数的词典。这一信息是我们稍后讨论的概率计算所需的。最后一个实例变量是 getfeatures,对应一个函数,作用是从即将被归类的文档中提取出特征来-本例中,就是我们刚才定义的getwords函数。
向我们刚才定义的类中加入下面的几个函数,实现分类器的训练

#增加对特征/分类组合的计数值
def incf(self, f, cat):
    self.fc.setdefault(f, {})
    self.fc[f].setdefault(cat, {})
    self.fc[f][cat] += 1

#增加某一个分类的计数值:
def incc(self, cat):
    self.cc.setdefault(cat, {})
    self.cc[cat] += 1

#计算某一个特征在某一个分类中出现的次数
def fcount(self, f, cat):
    if f in self.fc and cat in self.fc[f]:
        return self.fc[f][cat]
    else:
        return 0.0
#属于某一个分类的文档总数
def catcount(self, cat):
    if cat in self.cc:
        return self.cc[cat]
    return 0
#所有的文档总数
def totalcount(self):
    return sum(self.cc.values())
#所有文档的种类
def categories(self):
    return self.cc.keys()

train函数接受一个文档和其所属分类(‘good’或者‘bad’),利用我们定义的getwords函数,对文档进行划分,划分成一个个独立的单词,然后调用incf函数,针对该分类为每个特征增加计数值,最后增加该分类的总计数值:

def train(self, item, cat):
        features = self.getfeatures(item)
        # 针对该分类,为每个特征增加计数值
        for f in features:
            self.incf(f, cat)

    # 增加该分类的计数值
        self.incc(cat)

下面我们开始测试我们编写的类是否可用

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.fcout('quick','good')
out: 1.0
cl.fcout('quick','bad')
out: 1.0

上面的几行代码很好理解,我们首先实例化了 classifier 类,然后使用两个文档对我们的分类器进行了简单的训练。cl.fcout('quick','good') 用来计算在分类为‘good’的所有文档中,单词‘qucik’出现的次数。

当然喽,我们现实生活中的分类器训练肯定需要使用大量数据,我们新建一个函数(需要注意的是,这个函数不属于任何一个类),来训练大规模数据

def sampletrain(cl):
     cl.train('nobody owns the water','good')
     cl.train('the quick rabbit jumps fences','good')
     cl.train('buy phamaceuticals now','bad')
     cl.train('make quick money at the online casino','bad')
     cl.train('the quick borwn fox jumps','good')

在上述的函数中,我们已经计算了对于每一个特征(单词),我们计算了它在某一个分类中出现的次数,是时候将其转化成概率了。在本例中,我们对于一个特定单词,计算它在某个分类中所占的比例。(也就是某个分类中出现该单词的文档数目 / 该分类的文档总数)

def fprob(self, f, cat):
        if self.catcount(cat) == 0:
            return 0

        # 特征在该分类中出现的次数 /
        # 该特征下文档的总数目
        return self.fcount(f, cat)/self.catcount(cat)

通俗的来说,这个函数就是我们要求的条件概率。 P(word | classification),意思就是对于一个给定的分类,某个单词出现的概率,下面我们测试一下这个函数:

cl = classifier(getwords)
sampletrain(cl)
cl.fprob('quick','good')
out:0.6666666

从执行的结果上看,在所有的三篇被归类于‘good’文档中,有2篇出现了单词‘qucik’,所以我们要求的条件概率 p('quick' | 'good') = 2/3


Part 4.2.1 一个小小的问题

在训练的样本中,由于单词‘money’只出现了一次,并且是一个赌博类的广告,因此被分类‘bad’类,那我们计算p('money' | 'good') = 0,这是非常危险和不公平的,由于我们训练样本的缺失,导致所有含有‘money’这个单词的文档都被判断为‘bad’类文档。显然这种结果是我们不愿意接受的,因此我们对概率进行一些加权,使一些即使在训练样本中没有出现的单词,在求条件概率的时候,不至于为0。具体做法如下:

def weightedprob(self, f, cat, prf, weight=1, ap=0.5):
        # 使用fprob函数计算原始的条件概率
        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)
sampletrain(cl)
cl.fprob('money','good')
out:0
cl.weightedprob('money','good')
out:0.25

Part 4.3 朴素分类器

之所以称为朴素贝叶斯分类器的前提是被组合的各个概率之间是独立的,在我们的例子中,可以这样理解:一个单词在属于某个分类文档中概率,与其他单词出现在该分类的概率是不相关的。事实上,这个假设并不成立,因为很多词都是结伴出现的,但是我们可以忽略,实践显示,在假设各单词互相独立的基础上,使用朴素贝叶斯对文本分类可以达到比较好的效果
Part 4.3.1 计算整篇文档属于某个分类的概率

假设我们已经注意到,有20%的‘bad’文档出现了‘python’单词- P('python'| 'bad') = 0.2,同时有80%的文档出现了单词‘casino’-P('casino'| 'bad')=0.8,那么当‘python’和‘casino’同时出现在一篇‘bad’文档的概率是P('casino' & 'python' | 'bad') = 0.8 * 0.2 = 0.16。
我们新建一个子类,继承自classifier,取名naivebayes,并添加一个docprob函数

class naivebayes(classifier):

    def __init__(self, getfeatures):
        classifier.__init__(self, getfeatures)

    def docprob(self, item, cat):
        features = self.getfeatures(item)
        # Multiply the probabilities of all the features together
        p = 1
        for f in features:
            p *= self.weightedprob(f, cat, self.fprob)
        return p

现在我们已经知道了如何计算P(Document|category),但是我们需要知道的是,最终我们需要的结果是P(category|Document),换而言之,对于一篇给定的文档,我们需要找出它属于各个分类的概率,我们感到欣慰的是,这就是贝叶斯需要解决的事情
在本例中:
P(category|Document) = P(Document|category) * P(category) / P(Document)
P(Document|category) 已经被我们用 docprob 函数计算出来了,P(category)也很好理解和计算:代表我们随你选择一篇文档,它属于某个分类的概率。P(Document)对于所有的文档来说,都是一样的,我们直接选择忽略掉他
我们在naivebayes中新添加一个prob函数,计算一篇文档属于某个分类的概率(P(Document|category) * P(category) )

def prob(self, item, cat):
        catprob = self.catcount(cat)/self.totalcount()
        docprob = self.docprob(item, cat)
        return docprob * catprob

到现在为止,我们的朴素贝叶斯分类器编写基本完成。我们看看针对不同的文档(字符串),概率值是如何变化的:

cl = naivebayes(getwords)
sampletrain(cl)
cl.prob('quick rabbit', 'good')
out: 0.156
cl.prob('quick rabbit', 'bad')
out: 0.05

根据训练的数据,我们认为相对于‘bad’分类而言,我们认为‘quick rabbit’更适合于'good'分类.
最后我们完善一下我们的分类器,我们只需要给出文档,分类器会自动给我们找出概率最大的哪一个分类。
我们为naivebayes新添加一个方法 :classify

def classify(self, item):
        max = 0.0
        for cat in self.categories():
            probs[cat] = self.prob(item, cat)
            if probs[cat] > max:
                max = probs[cat]
                best = cat
        return best

继续测试:

cl = naivebayes(getwords)
sampletrain(cl)
cl.classify('quick rabbit')
out:good

但是到目前为止,我们所使用的训练数据,或者测试数据,都是简单的字符串,同时也是我们人为制造的,但是在真实的生产环境中,这几乎是不可能的,数据要更为复杂,更为庞大。回到开头,我这里使用在康奈尔大学下载的2M影评作为训练数据和测试数据,里面共同、共有1400条,好评和差评各自700条,我选择总数的70%作为训练数据,30%作为测试数据,来检测我们手写的朴素贝叶斯分类器的效果
首先我们稍微修改一下:我们的训练函数:sampletrain,以便能够训练大规模数据

def sampletrain(cl, traindata, traintarget):
    for left, right in zip(traindata, traintarget):.
        cl.train(left, right)

我们可以把需要训练的数据放在一个list里面或者迭代器里面,其对应的分类也是如此,在函数中,我们使用traindata, traintarget分别替代我们的训练数据和其对应的分类。
我们定义一个函数 get_dataset获得打乱后的数据

def get_dataset():
    data = []
    for root, dirs, files in os.walk(r'E:\研究生阶段课程作业\python\好玩的数据分析\朴素贝叶斯文本分类\tokens\neg'):
        for file in files:
            realpath = os.path.join(root, file)
            with open(realpath, errors='ignore') as f:
                data.append((f.read(), 'bad'))
    for root, dirs, files in os.walk(r'E:\研究生阶段课程作业\python\好玩的数据分析\朴素贝叶斯文本分类\tokens\pos'):
        for file in files:
            realpath = os.path.join(root, file)
            with open(realpath, errors='ignore') as f:
                data.append((f.read(), 'good'))
    random.shuffle(data)
    return data

在定义一个函数,对我们的数据集进行划分,训练集和测试集分别占07和0.3

def train_and_test_data(data_):
    filesize = int(0.7 * len(data_))
    # 训练集和测试集的比例为7:3
    train_data_ = [each[0] for each in data_[:filesize]]
    train_target_ = [each[1] for each in data_[:filesize]]

    test_data_ = [each[0] for each in data_[filesize:]]
    test_target_ = [each[1] for each in data_[filesize:]]

    return train_data_, train_target_, test_data_, test_target_

计算我们的分类器在真实数据上的表现:

if __name__ == '__main__':
    cl = naivebayes(getwords)
    data = dataset()
    train_data, train_target, test_data, test_target = train_and_test_data(data)
    sampletrain(cl, train_data, train_target)  #对训练我们的分类器进行训练
    predict = []
    for each in test_data:
        predict.append(cl.classify(each))
    count = 0
    for left,right in zip(predict,test_target ):
          if left == right:
                count += 1
    print(count/len(test_target))
    out :0.694

对于我们的测试集,大约有420个影评,我们使用简单的、完全手写的贝叶斯分类器达到了将近70%的预测准确率,效果还算可以,从头到尾,你是不是被贝叶斯的神奇应用折服了呢。如果你是初学者,可以按照本片博客,一步一步完成朴素贝叶斯分类器的编写,如果你嫌麻烦,可以直接向我要源码。(其实把本文所有的代码加起来就是完整的源码啦)


Part 5 总结
作为学计算机的人,重复造轮子,恐怕是最消耗精力也是最得不偿失的一件事情了,在下一篇文档,我将会使用sklearn库里自带的贝叶斯分类器,对相同的数据进行分类,比较我们手写的和自带的有哪些优点和缺点。


Part 6 参考资料
需要说明的是,本篇文章关于分类器编写的部分,我参考了《集体智慧编程》一书的第六章: 文档过滤,我真心推荐《集体智慧编程》这本书,如果你是机器学习的初学者,那么这本书将使你受益颇多。



数据挖掘入门与实战


教你机器学习,教你数据挖掘






以上是关于完整手写一个朴素贝叶斯分类器,完成文本分类的主要内容,如果未能解决你的问题,请参考以下文章

朴素贝叶斯-商品评论情感分析

算法 | 使用sklearn自带的贝叶斯分类器进行文本分类和参数调优

学习使用sklearn自带的贝叶斯分类器进行文本分类和参数调优

贝叶斯分类器(3)朴素贝叶斯分类器

朴素贝叶斯文本分类器 - 确定何时应将文档标记为“未分类”

朴素贝叶斯(naive bayes)原理小结