本文共 4873 字,大约阅读时间需要 16 分钟。
常见的python朴素贝叶斯算法的方式,都是使用for循环
来统计各个p(特征|类型)
的值。其实机器学习除了常规算法思路外,很关键且很优雅的地方在于矩阵化(向量化),即vectorization。通过各种矩阵运算来去除for循环
,是目前机器学习、深度学习中非常关键的技巧。文本不使用各种高阶机器学习库,单纯使用numpy来手撕朴素贝叶斯算法,带你领略机器学习中的矩阵运算之道。
数据准备
为了demo方便,我们先伪造一些数据。这里伪造的是NLP情感分类的数据。分为两部门train
和valid
,标签分为1和0,1代表侮辱性文字,0代表正常言论。
train = ['my dog had flea problems help help please', 'mybe not take him to dog park stupid dog', 'my dalmation is so cute I love him', 'stop posting stupid worthless garbage', 'mr licks ate my steak how to stop him',]valid = ['my stupid stupid worthless dog']label = [0,1,0,1,0]valid_y = [1]
数据预处理
NLP的数据预处理主要就是词典的创建和文本的向量化。这里由于是英语文档,不存在分词的问题。
这里由于是手撕教程,不使用spacy
等高阶NLP库,词典创建的也即使用Python的set
vocab = set([])for doc in train: vocab = vocab | set(doc.split())vocabList = list(vocab)# 根据词反查indexvocabList.index('please')
文档向量化
向量话的原理,首先将文档中句子的每一个单词按照字典转换为index
train_vec = []valid_vec = []for doc in train: train_vec.append([vocabList.index(x) for x in doc.split()])for doc in valid: valid_vec.append([vocabList.index(x) for x in doc.split()])
可以看看向量化后的结果
train_vec, valid_vec'''[[28, 11, 16, 7, 26, 19, 19, 8], [20, 24, 21, 14, 6, 11, 12, 22, 11], [28, 5, 9, 25, 0, 3, 2, 14], [1, 17, 22, 15, 13], [18, 4, 27, 28, 10, 23, 6, 1, 14]], [[28, 22, 22, 15, 11]]'''
接着是将词典的每一个单词作为一个feature维度处理,将所有文档处理成相同维度的矩阵,维度大小即为词典的长度,而每个维度的值有多种处理方式,比如按照句子中每个单词的Count
计数,或者是每个单词的TFIDF
值。这里我们采用了Count
,为了体现手撕,使用了Python的Counter
,为了体现numpy矩阵运算的思路,使用了scipy
的稀疏矩阵sparse.csr_matrix
。
from collections import Counterfrom scipy.sparse import csr_matrixdef get_term_doc_matrix(label_list, vocab_len): j_indices = [] indptr = [] values = [] indptr.append(0) for i, doc in enumerate(label_list): feature_counter = Counter(doc) j_indices.extend(feature_counter.keys()) values.extend(feature_counter.values()) indptr.append(len(j_indices)) return csr_matrix((values, j_indices, indptr), shape=(len(indptr) - 1, vocab_len), dtype=int)train_matrix = get_term_doc_matrix(train_vec, len(vocabList)valid_matrix = get_term_doc_matrix(valid_vec, len(vocabList)
可以看看矩阵化后的train和valid,分别转换成了samples*feature,29即为字典的维度。
train_matrix, valid_matrix'''<5x29 sparse matrix of type '' with 37 stored elements in Compressed Sparse Row format>, <1x29 sparse matrix of type '' with 4 stored elements in Compressed Sparse Row format>'''
我们将生成的train_matrix作图如下(为方便观看省去了部分维度)
朴素贝叶斯算法原理
谈到朴素贝叶斯算法,首先想到的必定是著名的贝叶斯公式:
不过这个定理如何体现到我们机器学习的场景呢?我们把其中的A和B换成实际的物理意义如下:
在我们的分类监督学习中,其实就是求在一定特征的样本情况下,某个具体类别的概率。根据贝叶斯公式,可以转换成分别求某个类别下这些特征的概率、某类别的概率、特征的概率。
实际场景中,特征一般是多维的,而多维特征的联合概率是比较复杂的。这时候,就体现出朴素的概念了。我们对样本作出假设:样本中的各个特征是相互独立的。这样,可以将联合概率转换成以下的独立概率乘积:
当然,这种假设肯定是粗暴的。但是实践过程中,计算量减少带来的益处是远大于粗暴假设带来的失真。这也是最考验算法工程师的地方,既要考虑数学的严谨,同时也要考虑工程实现。“part-science, part-art”,这或许也是机器学习之道。
当然,对于模型最终的应用来说,求出具体的概率绝对值是意义不大的。我们只需要知道不同特征之间概率的比值就行。也就是说,对于二分类问题,我们只需要知道:
就可以判断出该样本是属于类别1。于是,我们可以接着进行公式变换,将P(类别2|特征)
移到等式左边,并取log
:
同样,根据朴素的假设,可以得到:
即,我们需要求出每个特征对应的
以及每个类别对应的
然后全部相加,看结果是否大于0即可判断出样本的类别。而这两块,我们都是可以从训练样本中求出(先验概率),这也就是朴素贝叶斯算法训练的部分。
朴素贝叶斯矩阵运算
前言中说到,一般的教程,直接使用for循环
数数一般就可以求出上述的两个log值,完成“训练”。我们这样,要充分应用numpy的矩阵运算特性,不使用for循环
import numpy as npp1 = np.squeeze(np.asarray(train_matrix[np.asarray(label==1].sum(0)))p0 = np.squeeze(np.asarray(train_matrix[np.asarray(label==0].sum(0)))pr1 = (p1+1) / ((np.asarray(label)==1).sum() + 1)pr0 = (p0+1) / ((np.asarray(label)==0).sum() + 1)r = np.log(pr1/pr0)
还是以这个图为例,pr1
即为类别为1的样本中特征x的概率。在NLP中,每个特征都是一个词,其物理意义就是某个词在某个类别中出现的概率,而最后求出的r
即为所有特征的概率在2个类别的比值,注意,这里的r
是个向量,是通过样本之间的矩阵运算一次性求出。
r'''array([-0.40546511, 0.28768207, -0.40546511, -0.40546511,-0.40546511, -0.40546511, 0.28768207, -0.40546511, -0.40546511, -0.40546511, -0.40546511, 0.69314718, 0.98082925, 0.98082925, -0.11778304, 0.98082925, -0.40546511, 0.98082925, -0.40546511, -0.81093022, 0.98082925, 0.98082925, 1.38629436, -0.40546511, 0.98082925, -0.40546511, -0.40546511, -0.40546511, -1.09861229])'''
接下来求第二个log,这个直接就是不同类别的比例。
b = np.log((np.asarray(label)==1).mean() / (np.asarra(label)==0).mean())'''-0.4054651081081643'''
这里的r
和b
即为我们训练出的模型的参数。可以将其序列化到磁盘,供后续预测的时候使用(也就是深度学习中的inference)。
朴素贝叶斯推理
由于r
代表了训练样本中每一个特征词的在不同类别的概率比值,比如’stupid’等词就很高,而’love’等词就较低。在实际应用中,我们同样取出预测样本中的所有词,然后考虑这些词的概率比值,最后全部相加,即可判断最终样本的类别。同样,我们使用numpy的矩阵运算。
train_matrix@r + b > 0# array([False, True, False, True, False])valid_matrix@r + b > 0# array([ True])
这里@
是numpy的矩阵乘操作,举例如图
而b
作为一个标量,会直接触发numpy中的broadcast
操作,直接把+
应用到所有样本,同样>
也会broadcast
,最终得到一个array,表示每一个测试样本是否预测为类别1。
扩展
由于上面推理过程中,train_matrix
和valid_matrix
都是由词频组成,相当于在求具体概率比值的时候应用词频做了加权。在实际应用中,也可考虑不使用词频,直接使用是否含有某个特征词,如下:
train_matrix.sign()@r + b > 0valid_matrix.sign()@r + b > 0
具体哪种方式好,还是要根据实际的数据样本情况来具体测试。
总结
本文以NLP中的句子情感分析为例,使用numpy的矩阵运算来手撕朴素贝叶斯算法,可以加深对与朴素贝叶斯算法的理解,并体会机器学习中矩阵运算之道。
朴素贝叶斯简单快捷,特别适合较大规模的稀疏矩阵,在情感分析、垃圾邮件分类等场景中有很普遍的应用。在深度学习大行其道的今天,我们仍然可以通过这个简单的算法来快速搭建整个流程的pipeline。在我眼里,在机器学习项目的初期,流程pipeline的重要性大于任何算法。
转载地址:https://blog.csdn.net/weixin_39609822/article/details/110561191 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!