自给自足,完全手写一个朴素贝叶斯分类器,完成文本分类

Part 1: 本文解决的问题:

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

Part 2:本文的结构

  • 数据来源以及含义
  • 贝叶斯公式的简单介绍
  • 朴素贝叶斯分类器代码编写
  • 划分测试数据和训练数据,计算分类精度
  • 使用sklearn自带的朴素贝叶斯分类器,计算分类精度
  • 比较手写的分类器和sklearn自带的分类器的优点和缺点
  • 参考资料和引用

Part 3 :数据来源以及含义

本文所用的测试数据和训练数据都是来源于康奈尔大学网站的2M影评数据集。下载地址。里面共计有1400条影评,700条好评,700条差评,作者已经为我们分好了类。

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 参考资料

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

QQ :1527927373
Email: 1527927373@qq.com

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,042评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 89,996评论 2 384
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,674评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,340评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,404评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,749评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,902评论 3 405
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,662评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,110评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,451评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,577评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,258评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,848评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,726评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,952评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,271评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,452评论 2 348

推荐阅读更多精彩内容