条件概率,就是指在事件B发生的情况下,事件A发生的概率,用 P(A|B) 来表示
根据文氏图可知:在事件B发生的情况下,事件A发生的概率如下:↓
同理可得:P(A∩B) = P(B|A)*P(A)
所以:(将上述两个式子联立)
如果事件A1,A2,A3, … ,An构成一个完备事件且都有正改率,那么对于任何一个B事件满足以下公式:
根据条件概率和全概率公式,可以得到贝叶斯概率:
P(A)称为“先验概率”(Prior prob),即在B发生之前,我们队A事件概率的一个判断。
P(A|B)称为“后验概率”(Posterior prob),即在B事件发生之后,我们对A事件概率的重新评估。
P(A|B)/P(B)称为“可能性函数”(Likely hood),这是一个调整因子,使得预估概率更加接近真实概率。
所以条件概率可以理解为:
后验概率 = 先验概率 * 调整因子
如果“可能性函数” > 1,意味着“先验概率”被增强,事件A的可能性变大;
如果“可能性函数” = 1,意味着B事件无助于判断A事件的可能性;
如果“可能性函数” < 1,意味着“先验概率”被削弱,事件A的可能性变小。
问:某男(帅,性格不好,不上进)向女生求婚,该女生嫁还是不嫁?
这个栗子,按照朴素贝叶斯的求解可转化为一下两个式子:
最后根据表格和公式算出数据就好啦~
在sklearn中,一共有3个朴素贝叶斯的分类算法。
分别是GaussianNB,MultinomialNB和BernoulliNB。
GaussianNB就是先验为高斯分布(正态分布)的朴素贝叶斯,假设每个标签的数据都服从简单的正态分布。
其中Ck为Y的第k类类别。mean(k) 和 Var(k) 为需要从训练集估计的值。
1,会使用到的包
import pandas as pd # 数据预览
from sklearn.model_selection import train_test_split #数据切分
from sklearn.naive_bayes import GaussianNB # GaussianNB方法
from sklearn.metrics import accuracy_score # 计算准确的
2,导入sklearn中的鸢尾花数据
from sklearn import datasets
iris = datasets.load_iris()
3,使用pandas预览
传送门:Pandas的数据前处理
pf = pd.DataFrame(iris.data, columns=iris.feature_names)
pf['类别']=(iris.target)
pf.head()
from sklearn.model_selection import train_test_split
x_train, x_test, y_train, y_test = train_test_split(iris.data, iris.target, random_state=12)
6,建立算法模型
clf = GaussianNB()
clf.fit(x_train, y_train)
>>>GaussianNB(priors=None, var_smoothing=1e-09)
7,在测试集上进行预测
直接返回得出标签(分类结果)
clf.predict(x_test)
>>>array([0, 2, 0, 1, 2, 2, 2, 0, 2, 0, 1, 0, 0, 0, 1, 2, 2, 1, 0, 1, 0, 1,
2, 1, 0, 2, 2, 1, 0, 0, 0, 1, 2, 0, 2, 0, 1, 1])
返回每一个测试数据所对应的每一个标签的可能性(概率),概率最高的标签就会返回到上边的那个方法中
clf.predict_proba(x_test)
>>>array([[1.00000000e+000, 2.32926069e-017, 1.81656357e-023],
[4.28952299e-154, 2.48576754e-002, 9.75142325e-001],
[1.00000000e+000, 7.45528845e-018, 3.79800436e-024],
[3.59748710e-076, 9.99751806e-001, 2.48194200e-004],
[2.20411871e-239, 4.45798016e-009, 9.99999996e-001],
[1.23795145e-173, 1.95814902e-003, 9.98041851e-001],
[2.45866589e-206, 2.34481513e-007, 9.99999766e-001],
[1.00000000e+000, 2.61810906e-017, 2.67446831e-023],
......]
结果是一一对应的
len(clf.predict(x_test)) == len(clf.predict_proba(x_test))
>>>True
8,模型准确率(跑分啦)
accuracy_score(y_test, clf.predict(x_test))
>>>0.9736842105263158
MultinomialNB就是先验为多项式分布的朴素贝叶斯算法。
他假设特征是由一个简单地多项式分布生成的。多项式分布可以描述各种类型样本出现次数的概率,因此多项式朴素贝叶斯非常适合用于描述出现次数或者出现次数比例的特征。该模型常用于文本分类,特征表示是次数,例如某个词语的出现次数。
多项式分布公式如下:
其中,P(Xj = xjl | Y = Ck) 是第k个类别的第j维特征的第l个取值的条件概率。mk是训练集中输出位第k类的样本个数。入为一个大于 0 的常数,常常取 1,即拉普拉斯平滑。也可以取其他值。
from sklearn.naive_bayes import MultinomialNB
mlf = MultinomialNB()
mlf.fit(x_train, y_train)
>>>MultinomialNB(alpha=1.0, class_prior=None, fit_prior=True)
(其他的和高斯分布贝叶斯都一样,嘎嘎)
BernoulliNB就是先验为伯努利分布的朴素贝叶斯。
假设特征的先验概率为二元伯努利分布,即如下式:
此时 l 只有两种取值。xjl 只能取值 0或1。
在伯努利模型中,每个特征的取值是布尔值,即True和False(或者1和0)。在文本分类中,就是一个特征有没有在一个文档中出现。
from sklearn.naive_bayes import BernoulliNB
mlf = BernoulliNB()
mlf.fit(x_train, y_train)
>>>BernoulliNB(alpha=1.0, binarize=0.0, class_prior=None, fit_prior=True)
(其他的和高斯分布贝叶斯都一样,嘎嘎)
应用GaussianNB对鸢尾花数据进行分类
import pandas as pd
dataSet = pd.read_csv('D:\Python\pycharm\机器学习\朴素贝叶斯算法\iris.txt',header=None)
dataSet.head()
函数
在这里明没有将整个数据进行乱序,而只是对索引进行了乱序,然后再由乱序的索引随机提取样本,好处在于切分后不会改变数据原本的排列方式
import random
def randSplit(dataSet, rate):
"""
数据集切分函数
:param dataSet:原始数据集
:param rate: 训练集所占原始数据集比例[0, 1]
:return: 训练集,测试集
"""
index = list(dataSet.index) # 取出索引
random.shuffle(index) # 打乱索引
dataSet.index = index # 将打乱后的索引重新赋给原数据集
n = dataSet.shape[0] # 最大行数(共有几组数据)
m = int(n * rate) # 训练集所占的行数(训练集样本个数)
# 依照比例提取训练集和测试集
train = dataSet.loc[range(m), :]
test = dataSet.loc[range(m, n), :]
# 恢复原始数据集索引
dataSet.index = range(dataSet.shape[0])
# 恢复测试集索引
test.index = range(test.shape[0])
return train, test
使用
train, test = randSplit(dataSet, 0.8)
以下为高斯朴素贝叶斯分类器
(注意:会对test进行格式上的更改,所以不能反复对text进行预测)
def gnd_classify(train, test):
"""
高斯朴素贝叶斯分类器
:param train: 训练集
:param test: 测试集(包括属性与分类)
:return: 测试集(追加一列为预测结果)
"""
labels = train.iloc[:, -1].value_counts().index # 提取训练集的标签样本种类(无重复)
mean = [] # 存放每个类别的均值
std = [] # 存放每个类别的方差
result = [] # 存放测试集预测结果
for i in labels:
item = train.loc[train.iloc[:, -1] == i, :] # 取出每一种类别
m = item.iloc[:, :-1].mean() # 当前类别的平均值
mean.append(m)
s = np.sum((item.iloc[:, :-1] - m) ** 2) / (item.shape[0]) # 当前类别的方差
std.append(s)
# 转换为DataFrame格式,索引为类标签
means = pd.DataFrame(mean, index=labels)
stds = pd.DataFrame(std, index=labels)
for j in range(test.shape[0]):
iset = test.iloc[j, :-1].tolist() # 取出当前测试样本,并将其转换为list格式
iprod = np.exp(-1 * (iset - means) ** 2 / (stds ** 2)) / (np.sqrt(2 * np.pi * stds)) # 正态分布公式
prob = 1
# 遍历每一个特征
for k in range(test.shape[1] - 1):
prob *= iprod[k] # 特征概率之积即为当前实例概率
cla = prob.index[np.argmax(prob.values)] # 返回最大概率类别
result.append(cla)
test['预测结果'] = result # 将预测结果加入测试集
acc = (test.iloc[:, -1] == test.iloc[:, -2]).mean() # 计算准确率
print(f'模型预测准确率为:{acc}')
return test
代码测试
(因为之前对数据集的切分是随机的所以这边的准确率可能会不大一样)
gnd_classify(train, test)
>>>模型预测准确率为:0.9333333333333333
留言文本已经被切分好,并且认为标注好类别,用于训练模型。
类别有两类:侮辱性(1);非侮辱性(2)。
创建数据集
def loadDataSet():
"""
创建实验数据集合
:return: 切分好的样本词条,类标签向量
"""
# 切分好的样本词条
dataSet = [['my', 'dog', 'has', 'flea', 'problems', 'help', 'please'],
['maybe', 'not', 'take', 'him', 'to', 'dog', 'park', 'stupid'],
['my', 'dalmation', 'is', 'so', 'cute', 'I', 'love', 'him'],
['stop', 'posting', 'stupid', 'worthless', 'garbage'],
['mr', 'licks', 'ate', 'my', 'steak', 'how', 'to', 'stop', 'him'],
['quit', 'buying', 'worthless', 'dog', 'food', 'stupid']]
# 类标签向量
classVec = [0, 1, 0, 1, 0, 1] # 1代表侮辱性词汇;0代表非侮辱性词汇
return dataSet, classVec
dataSet, classVec
dataSet, classVec = loadDataSet()
生成不重复词汇表(set类型中会去除掉相同元素)
def createVocabList(dataSet):
"""
将切分好的样本词条整理成词汇表(不重复)
:param dataSet: 切分好的样本词条
:return: 不重复词汇表
"""
vocabSet = set() # 创建一个空集合
for doc in dataSet: # 遍历每一条言论
vocabSet = vocabSet | set(doc) # 取并集
vocabList = list(vocabSet)
return vocabList
vocabList
vocabList = createVocabList(dataSet)
>>>['flea', 'has', 'cute', 'licks', 'posting', 'please', 'ate', 'maybe', 'dog',
'quit', 'I', 'him', 'buying', 'problems', 'steak', 'to', 'food', 'take',
'not', 'how', 'help', 'mr', 'stupid', 'so', 'stop', 'dalmation', 'love',
'park', 'worthless', 'garbage', 'my', 'is']
生成词向量(输入的是一个词条)-- 辅助函数
def setofWords2Vec(vocabList, inputSet):
"""
生成词向量
:param vocabList: 词汇表
:param inputSet: 切分好的词条列表中的一条
:return: 文档向量,词集模型
"""
returnVec = [0]*len(vocabList) # 创建一个与词汇表等长的0向量
for word in inputSet: # 遍历每一个词条
if word in vocabList: # 如果词条存在于词汇表中,则变为1
returnVec[vocabList.index(word)] = 1 # 更改在该词在词汇表中对应的位置!
else:
print(f'{word} is not in my Vocabulary!')
return returnVec
生成所有词条的向量
def get_trainMat(dataSet):
"""
所有词条向量列表
:param dataSet:切分好的样本词条
:return: 所有词条向量组成的列表
"""
trainMat = [] # 初始化向量列表
vocabList = createVocabList(dataSet) # 生成词汇列表
for inputList in dataSet: # 遍历样本词条中的每一条
returnVec = setofWords2Vec(vocabList, inputList) # 将当前词条向量化
trainMat.append(returnVec) # 追加到向量列表中
return trainMat
trainMat
trainMat = get_trainMat(dataSet)
>>>[[1, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0],
[0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0],
[0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 1, 1],
[0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0],
[0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0]]
词向量构建好之后,我们就可以来构建分类器的训练函数了。
(下边代码有问题,经过平滑后的正确代码在“标题六”)
def trainNB(trainMat, classVec):
"""
朴素贝斯分类器训练函数
:param trainMat: 训练文档矩阵
:param classVec: 训练类别标签向量
:return: p0V:非侮辱类条件概率数组
p1V:侮辱类条件概率数组
pAb:文档属于侮辱类的概率
"""
n = len(trainMat) # 计算训练函数的文档数目
m = len(trainMat[0]) # 计算每篇文档的词条数目
pAb = sum(classVec) / n # 文档属于侮辱类的概率
p0Num = np.zeros(m) # 非侮辱类词条出现数初始化为0
p1Num = np.zeros(m) # 侮辱类词条出现数初始化为0
p0Denom = 0 # 非侮辱类分母初始化为0
p1Denom = 0 # 侮辱类分母初始化为0
# 遍历每一个文档
for i in range(n):
# 统计侮辱类的条件概率所需要的数据
if classVec[i] == 1:
p1Num += trainMat[i]
p1Denom += sum(trainMat[i])
# 统计非侮辱类的条件概率所需要的数据
else:
p0Num += trainMat[i]
p0Denom += sum(trainMat[i])
p1V = p1Num / p1Denom
p0V = p0Num / p0Denom
return p0V, p1V, pAb
p0V, p1V, pAb = trainNB(trainMat, classVec)
下边来看一下,这些参数都表达了什么
我们以词汇表中的第一个词‘flea’(蚤)为例
vocabList = createVocabList(dataSet)
>>>['flea', 'has', 'cute', 'licks', 'posting', 'please', 'ate', 'maybe', 'dog',
'quit', 'I', 'him', 'buying', 'problems', 'steak', 'to', 'food', 'take',
'not', 'how', 'help', 'mr', 'stupid', 'so', 'stop', 'dalmation', 'love',
'park', 'worthless', 'garbage', 'my', 'is']
对应他是非侮辱性词汇的条件概率为 0.04166667
p0V
>>>array([0.04166667, 0.04166667, 0.04166667, 0.04166667, 0. ,
0.04166667, 0.04166667, 0. , 0.04166667, 0. ,
0.04166667, 0.08333333, 0. , 0.04166667, 0.04166667,
0.04166667, 0. , 0. , 0. , 0.04166667,
0.04166667, 0.04166667, 0. , 0.04166667, 0.04166667,
0.04166667, 0.04166667, 0. , 0. , 0. ,
0.125 , 0.04166667])
对应他是侮辱性词汇的条件概率为 0.
p1V
>>>array([0. , 0. , 0. , 0. , 0.05263158,
0. , 0. , 0.05263158, 0.10526316, 0.05263158,
0. , 0.05263158, 0.05263158, 0. , 0. ,
0.05263158, 0.05263158, 0.05263158, 0.05263158, 0. ,
0. , 0. , 0.15789474, 0. , 0.05263158,
0. , 0. , 0.05263158, 0.10526316, 0.05263158,
0. , 0. ])
可以说 vocabList – p0V – p1V 中的元素一一对应
最后看一下该文档属于侮辱类的概率
pAb
>>>0.5
传送门:有关lambda讲解
关于functools中的reduce函数,我把源码放在下边了 ↓
(这里不想看可以直接跳过)
def reduce(function, sequence, initial=None): # real signature unknown; restored from __doc__
"""
reduce(function, sequence[, initial]) -> value
将两个参数的函数累加应用于序列的项,
从左到右,以便将序列减少到单个值。
For example,
reduce(lambda x, y: x+y, [1, 2, 3, 4, 5])
表示((((1+2)+3)+4)+5).
如果有首字母,则将其放在项目之前计算中的序列,
并在序列为空。
"""
pass
朴素贝叶斯分类器分类函数
(下边代码有问题,经过平滑后的正确代码在“标题六”)
from functools import reduce
def classifyNB(vec2Classify, p0V, p1V, pAb):
"""
朴素贝叶斯分类器分类函数
:param vec2Classify:待分类词条数组
:param p0V: 非侮辱类条件概率数组
:param p1V: 侮辱类条件概率数组
:param pAb: 文档属于侮辱类的概率
:return: 0(非侮辱类),1(侮辱类)
"""
# 对应元素相乘
p1 = reduce(lambda x, y: x * y, vec2Classify * p1V) * pAb
p0 = reduce(lambda x, y: x * y, vec2Classify * p0V) * (1 - pAb)
print('p0:', p0)
print('p1:', p1)
if p1 > p0:
return 1
else:
return 0
朴素贝叶斯测试函数
def testingNB(testVec):
"""
朴素贝叶斯测试函数
:param testVec:测试样本 (词条)
:return: 测试样本的类别
"""
dataSet, classVec = loadDataSet() # 创建实验样本
vocabList = createVocabList(dataSet) # 创建词汇表
trainMat = get_trainMat(dataSet) # 将实验样本向量化
p0V, p1V, pAb = trainNB(trainMat, classVec) # 训练分类器
thisone = setofWords2Vec(vocabList, testVec) # 测试样本向量化
# 执行分类,并打印结果
if classifyNB(thisone, p0V, p1V, pAb):
print(testVec, '属于侮辱类')
else:
print(testVec, '属于非侮辱类')
测试样本1
testVec1 = ['love','my','bed']
testingNB(testVec1)
>>>p0: 0.0
p1: 0.0
['love', 'my', 'bed'] 属于非侮辱类
测试样本2
testVec2 = ['stupid','garbage']
testingNB(testVec2)
>>>p0: 0.0
p1: 0.0
['stupid', 'garbage'] 属于非侮辱类
这里会发现,这样写的算法无法进行分类,p0和p1的计算结果都为0,显示结果错误。这是为什么呢?
(还记p0V和p1V中的那些0吗,就是他们在搞事情)
利用贝叶斯分类器对文档进行分类时,要计算多个概率乘积以获得文档属于某个类别的概率,即计算P(w1|1)P(w2|1)P(w3|1)。如果其中一个值为0,那么最后的乘积也为0。显然这样是不合理的,为了降低这种影响可以将所有词的出现数初始化为1,并将分母初始化为2。这种做法就叫做拉普拉斯平滑(Laplace Smoothing)又被称为加1平滑,是比较常用的平滑方法,他就是为了解决0概率问题。
另外一个遇到的问题就是下溢出,这是由于太多很小的数相乘造成的。我们在计算乘积时,由于大部分因子都很小,所以程序会下溢或者得不到正确答案。为了解决这个问题,对乘积的结果取自然对数。通过求对数可以避免下溢出或者浮点数舍入导致的错误。同时,采用自然对数进行处理不会有任何损失。
下图给出函数 f(x) 与 ln(f(x)) 的曲线:
检查这两条曲线就会发现他们在相同区域内同时增加或者减少(x相同时,导数的正负相同),并且在相同点上取到极值。他们的取值虽然不同,但不影响最终结果。因此可以将代码修改如下:
def trainNB(trainMat, classVec):
"""
朴素贝斯分类器训练函数
:param trainMat: 训练文档矩阵
:param classVec: 训练类别标签向量
:return: p0V:非侮辱类条件概率数组
p1V:侮辱类条件概率数组
pAb:文档属于侮辱类的概率
"""
n = len(trainMat) # 计算训练函数的文档数目
m = len(trainMat[0]) # 计算每篇文档的词条数目
pAb = sum(classVec) / n # 训练文档属于侮辱类的概率
p0Num = np.ones(m) # 非侮辱类词条出现数初始化为1
p1Num = np.ones(m) # 侮辱类词条出现数初始化为1
p0Denom = 2 # 非侮辱类分母初始化为2
p1Denom = 2 # 侮辱类分母初始化为2
# 遍历每一个文档
for i in range(n):
# 统计侮辱类的条件概率所需要的数据
if classVec[i] == 1:
p1Num += trainMat[i]
p1Denom += sum(trainMat[i])
# 统计非侮辱类的条件概率所需要的数据
else:
p0Num += trainMat[i]
p0Denom += sum(trainMat[i])
p1V = np.log(p1Num / p1Denom)
p0V = np.log(p0Num / p0Denom)
return p0V, p1V, pAb
(修改:分子初始化改为1,分母初始化改为2,对概率取log)
def classifyNB(vec2Classify, p0V, p1V, pAb):
"""
朴素贝叶斯分类器分类函数
:param vec2Classify:待分类词条数组
:param p0V: 非侮辱类条件概率数组
:param p1V: 侮辱类条件概率数组
:param pAb: 文档属于侮辱类的概率
:return: 0(非侮辱类),1(侮辱类)
"""
# 对应元素相乘
p1 = sum(vec2Classify * p1V) + np.log(pAb)
p0 = sum(vec2Classify * p0V) + np.log(1 - pAb)
if p1 > p0:
return 1
else:
return 0
(因为loga*logb = log(a+b),所以改成sum就ok)
然后再重新测试一下代码
testVec1 = ['love','my','bed']
testingNB(testVec1)
>>>bed is not in my Vocabulary!
['love', 'my', 'bed'] 属于非侮辱类
testVec2 = ['stupid','dog']
testingNB(testVec2)
>>>['stupid', 'dog'] 属于侮辱类
(这会就可以啦~)
(2020年3月28日21:20:47)