以在线社区的留言板为例。为了不影响社区的发展,需要屏蔽侮辱性的言论,所以要构件一个快速过滤器,如果某条留言使用了负面或者侮辱性的语言,那么久将该留言标识为内容不当。过滤这类内容是一个很常见的需求。对此问题建立两个类别:侮辱类和非侮辱类,使用1和0分别表示。
把文本看成单词向量或者词条向量,也就是说将句子转换为向量。考虑出现在所有文档中的所有单词,需要构件词汇表,然后必须要将每一篇文档转换为词汇表上的向量。
这里的留言文本数据,已经被切分好,存放在列表中,并且人为标注好类别,这些标注信息用于训练程序以便自动检测侮辱性留言。有两个类别:侮辱类(1)和非侮辱类(0)。主要包括以下函数:
import numpy as np # 导入numpy包
def loadDataSet():
"""
构建数据集
:return:
dataSet:切分好的样本词条
classVec:类标签向量
"""
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
def createVocabList(dataSet):
"""
构建词汇表,即创建一个包含在所有文档中出现的不重复词的列表
:param dataSet: 切分好的样本词条
:return:
vocabList:不重复的词汇表
"""
vocabSet = set([]) # 创建一个空的集合
for document in dataSet:
vocabSet = vocabSet | set(document) # 取并集
vocabList = list(vocabSet) # 将集合转换为列表
return vocabList
def setOfWords2Vec(vocabList,inputSet):
"""
基于词集模型创建文档向量,向量的每一元素为1或0,分别表示词汇表中的单词在输入文档中是否出现
:param vocabList: 词汇表
:param inputSet: 某一条切分好的词条列表
:return:
returnVec:文档向量
"""
returnVec = [0]*len(vocabList) # 创建一个和词汇表等长的向量,并将其元素设置为0
for word in inputSet: # 遍历每个词条
if word in vocabList: # 如果词条出现在词汇表中
returnVec[vocabList.index(word)] = 1 # 文档向量中对应值设置为1
else:
print("the word: %s is not in my vocabulary!" % word)
return returnVec
def trainNB(trainMatrix,classVec):
"""
朴素贝叶斯分类器训练函数
:param trainMatrix: 经过转换的文档矩阵,每行都是一个文档向量
:param classVec: 由每篇文档类别标签所构成的向量
:return:
p0V: 非侮辱类的条件概率数组
p1V:侮辱类的条件概率数组
pAb:文档属于侮辱类的概率
"""
num_docs = len(trainMatrix) # 文档数量
num_words = len(trainMatrix[0]) # 词表数
pAb = sum(classVec)/float(num_docs) # 文档属于侮辱类的概率
# 初始化概率
p0Num = np.zeros(num_words) # 词条出现数初始化为0
p0Denom = 0.0 # 分母初始化为0
p1Num = np.zeros(num_words)
p1Denom = 0.0
for i in range(num_docs):
if classVec[i] == 1:
p1Num += trainMatrix[i]
p1Denom += sum(trainMatrix[i])
else:
p0Num += trainMatrix[i]
p0Denom += sum(trainMatrix[i])
p0V = p0Num/p0Denom # 对每个元素做除法后,取对数
p1V = p1Num/p1Denom
return p0V,p1V,pAb
def classifyNB(vec2Classify,p0V,p1V,pAb):
"""
朴素贝叶斯分类函数
:param vec2Classify: 待分类的词条
:param p0V: 非侮辱类的条件概率数组
:param p1V: 侮辱类的条件概率数组
:param pAb: 文档属于侮辱类的概率
:return:
0: 属于非侮辱类
1: 属于侮辱类
"""
from functools import reduce
# reduce从左到右对一个序列的项累计地应用有两个参数的函数,以此合并序列到一个单一值。
# reduce(lambda x, y: x * y, [1, 2, 3, 4, 5]),其实就是((((1*2)*3)*4)*5)=120
p1 = reduce(lambda x,y:x*y,vec2Classify * p1V)*pAb # 元素相乘
p0 = reduce(lambda x,y:x*y,vec2Classify * p0V)*(1-pAb)
if p1 > p0:
return 1
else:
return 0
def testingNB(testVec):
"""
朴素贝叶斯测试函数
:param testVec: 测试样本
:return: 测试样本的预测类别
"""
# 构建数据集
dataSet,classVec = loadDataSet()
# 生成词汇表
vocabList = createVocabList(dataSet)
# 将训练数据转换为词集文档向量
trainMat = []
for inputSet in dataSet:
trainMat.append(setOfWords2Vec(vocabList,inputSet))
# 获取三个概率
p0V,p1V,pAb = trainNB(np.array(trainMat),np.array(classVec))
thisDoc = np.array(setOfWords2Vec(vocabList,testVec))
if classifyNB(thisDoc,p0V,p1V,pAb):
print(testEntry,'属于侮辱类') # 打印分类结果
else:
print(testEntry, '属于非侮辱类')
输入测试文本,具体代码如下
# 测试文本1
testEntry = ['love', 'my', 'dalmation']
testingNB0(testEntry)
# 测试文本2
testEntry = ['stupid', 'garbage']
testingNB0(testEntry)
运行结果如下:
[‘love’, ‘my’, ‘dalmation’] 属于非侮辱类
[‘stupid’, ‘garbage’] 属于非侮辱类
这是什么原因呢?因为有一个概率为0,多个概率乘积后也为0,这种严重缺陷是要通过下面的方法来处理的。
利用贝叶斯分类器对文档进行分类时,要计算多个概率的乘积以获得文档属于某个类别的概率。即计算 p ( x 0 ∣ 1 ) p ( x 1 ∣ 1 ) p ( x 2 ∣ 1 ) p(x_0|1)p(x_1|1)p(x_2|1) p(x0∣1)p(x1∣1)p(x2∣1)。如果其中一个概率为0,那么最后的乘积也为0。为了降低这种影响,可以将所有词的出现数初始化为1,并将分母初始化为2(因为所有属性的取值都只有0和1两个值)。这种做法叫做拉普拉斯平滑(Laplace Smoothing),又被称为加1平滑,是比较常用的平滑方法,它就是为了解决0概率问题。
原来的条件概率公式为:
P ( x i ∣ c ) = ∣ D c , x i ∣ ∣ D c ∣ P(x_i|c)= \frac{|D_{c,x_i}|}{|D_c|} P(xi∣c)=∣Dc∣∣Dc,xi∣
被拉普拉斯修正为:
P ( x i ∣ c ) = ∣ D c , x i ∣ + 1 ∣ D c ∣ + N i P(x_i|c)= \frac{|D_{c,x_i}|+1}{|D_c|+N_i} P(xi∣c)=∣Dc∣+Ni∣Dc,xi∣+1
其中, ∣ D c , x i ∣ |D_{c,x_i}| ∣Dc,xi∣是c类别下第i个属性上取值为 x i x_i xi的样本数, ∣ D c ∣ |D_c| ∣Dc∣是c类的样本数。 N i N_i Ni表示第i个属性可能的取值数,本例中每个属性的取值都是2,所以 N i N_i Ni为2。
另一个问题就是下溢出,这是由于太多很小的数相乘造成的。在计算乘积时,由于大部分因子都非常小,所以程序会下溢出或得到不正确答案。一种解决方法是对乘积取自然对数,即ln(a*b)=ln(a)+ln(b)。通过求对数避免下溢出或者浮点数舍入导致的错误。同时,采用自然对数进行处理不会有任何损失。下图给出函数f(x)和ln(f(x))曲线。
检查这两条曲线,会发现它们在相同区域内同时增加或减少,并且在相同点上取到极值。它们的取值虽然不同,但不影响最终结果。具体修改代码如下:
def trainNB(trainMatrix,classVec):
"""
朴素贝叶斯分类器训练函数
:param trainMatrix: 经过转换的文档矩阵,每行都是一个文档向量
:param classVec: 由每篇文档类别标签所构成的向量
:return:
p0V: 非侮辱类的条件概率数组
p1V:侮辱类的条件概率数组
pAb:文档属于侮辱类的概率
"""
num_docs = len(trainMatrix) # 文档数量
num_words = len(trainMatrix[0]) # 词表数
pAb = sum(classVec)/float(num_docs) # 文档属于侮辱类的概率
# 初始化概率
p0Num = np.ones(num_words) # 词条出现数初始化为1
p0Denom = 2.0 # 分母初始化为2
p1Num = np.ones(num_words)
p1Denom = 2.0
for i in range(num_docs):
if classVec[i] == 1:
p1Num += trainMatrix[i]
p1Denom += sum(trainMatrix[i])
else:
p0Num += trainMatrix[i]
p0Denom += sum(trainMatrix[i])
p0V = np.log(p0Num/p0Denom) # 对每个元素做除法后,取对数
p1V = np.log(p1Num/p1Denom)
return p0V,p1V,pAb
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
输入上面的测试样本,运行结果如下
[‘love’, ‘my’, ‘dalmation’] 属于非侮辱类
[‘stupid’, ‘garbage’] 属于侮辱类
这个结果,看起来就没问题了!
使用朴素贝叶斯解决现实生活中的问题时,需要先从文本内容得到字符串列表,然后生成词向量。主要有一下函数:
def textParse(bigString):
"""
文本解析:将给的大字符串解析为字符串列表,并去掉少于两个字符的字符串,并将字符串转换为小写
:param bigString: 大字符串
:return: 词条向量
"""
import re
# 用正则表示来切分句子,其中分隔符是除单词,数字外的任意字符串
listOfTokens = re.split(r'\W*',bigString)
return [tok.lower() for tok in listOfTokens if len(tok) > 2]
def spamTest():
"""
基于朴素贝叶斯的垃圾邮件分类测试函数
"""
docList =[]
classList = []
# 导入50封电子邮件,并解析文件
for i in range(1,26):
wordList = textParse(open('email/spam/%d.txt' % i).read())
docList.append(wordList)
classList.append(1)
wordList = textParse(open('email/ham/%d.txt' % i).read())
docList.append(wordList)
classList.append(0)
# 解析为词典列表
vocabList = createVocabList(docList)
# 随机构建训练集和测试集
traingSet = list(np.arange(50))
testSet = []
for i in range(10):
randIndex = int(np.random.uniform(0,len(traingSet)))
testSet.append(traingSet[randIndex])
del traingSet[randIndex]
trainMat = []
trainClass = []
for docIndex in traingSet:
trainMat.append(setOfWords2Vec(vocabList,docList[docIndex]))
trainClass.append(classList[docIndex])
p0V,p1V,pSpam = trainNB(np.array(trainMat),np.array(trainClass))
# 对测试集进行分类
errorCount = 0
for docIndex in testSet:
wordVect = setOfWords2Vec(vocabList,docList[docIndex])
if classifyNB(wordVect,p0V,p1V,pSpam) != classList[docIndex]:
errorCount += 1
print("classificaiton error ",docList[docIndex]) # 输出分类错误的词条向量
print('the error rate is : ',float(errorCount)/len(testSet)) # 分类错误率
return float(errorCount)/len(testSet)
输入下列内容
spamTest()
运行结果如下:
classificaiton error [‘oem’, ‘adobe’, ‘microsoft’, ‘softwares’, ‘fast’, ‘order’, ‘and’, ‘download’, ‘microsoft’, ‘office’, ‘professional’, ‘plus’, ‘2007’, ‘2010’, ‘129’, ‘microsoft’, ‘windows’, ‘ultimate’, ‘119’, ‘adobe’, ‘photoshop’, ‘cs5’, ‘extended’, ‘adobe’, ‘acrobat’, ‘pro’, ‘extended’, ‘windows’, ‘professional’, ‘thousand’, ‘more’, ‘titles’]
the error rate is : 0.1
再次运行,结果如下
the error rate is : 0.0
spamTest函数会输出在10封随机选择的电子邮件上的分类错误率。由于这些电子邮件是随机选择的,所以每次的输出结果可能会有差别。如果发现错误的话,函数会食醋胡错分文档的词条列表,这样就可以廖家到底是哪篇文档发生了错误。
如果想更好地估计错误率,可以将上述过程迭代多次。文本迭代10次,输入下列内容:
errorrate = 0.0
for i in range(10):
errorrate += spamTest()
print("迭代10次的平均错误率为:",errorrate/10)
运行结果如下:
这里一直出现的错误是将垃圾邮件误判为正常邮件。相比之下,将垃圾邮件误判为正常邮件要比将正常邮件归为垃圾邮件好。
如果没有出现报错,直接跳过
报错:UnicodeDecodeError: ‘gbk’ codec can’t decode byte 0xae in position 199: illegal multib
原因:表示读取ham文件夹中的文件有异常。
解决方法: 打开23.txt 文件发现,包含一个?字符。“SciFinance?is”,删除该“?”即可。
应用GaussianNB原理来对鸾尾花数据集进行分类,主要有一下函数:
import pandas as pd
import numpy as np
import random
def loadDataSet():
"""加载数据集"""
from sklearn import datasets
iris_data = datasets.load_iris()
# print(iris_data.DESCR) # 查看数据集简介
dataSets = pd.DataFrame(iris_data.data)
# sepal_length:花萼长度,sepal_width:花萼宽度 单位是厘米
# petal_length:花瓣长度,petal_width:花瓣宽度 单位是厘米
dataSets.columns = ['sepal_length', 'sepal_width', 'petal_length', 'petal_width']
dataSets['类别'] = iris_data.target
return dataSets
def randSplit(dataSet,rate):
"""
随机拆分数据集,分为训练集和测试集
:param dataSet: 数据集
:param rate:训练集数据量占比
:return: 拆分后的训练集和测试集
"""
l = list(dataSet.index) # 提取出数据集的索引
random.shuffle(l) # 随机打乱索引
dataSet.index = l # 将打乱后的索引重新赋值给原数据集
m = dataSet.shape[0]
m_train = int(rate * m) # 训练集数量
train_data = dataSet.loc[range(m_train),:] # 提取前m_train个样本作为训练集
test_data = dataSet.loc[range(m_train,m),:] # 剩下的作为测试集
# 重置索引
train_data.index = range(len(train_data))
test_data.index = range(len(test_data))
return train_data,test_data
def gnb_classify(train_data,test_data):
"""
基于高斯分布的朴素贝叶斯分类函数
:param train_data:训练数据集
:param test_data:测试数据集
:return:含预测分类结果的测试数据集
"""
labels = list(set(train_data.iloc[:,-1])) # 获取类别
mean_list = [] # 存放每个类别的均值 3行4列
var_list = [] # 存放每个类别的方差 3行4列
for label in labels:
curr_label_data = train_data.loc[train_data.iloc[:,-1]==label,:] # 提取某种类别的数据
m = curr_label_data.iloc[:,:-1].mean() # 计算当前类别的平均值
v = np.sum((curr_label_data.iloc[:,:-1]-m)**2)/curr_label_data.shape[0] # 当前类别的方差
mean_list.append(m)
var_list.append(v)
mean_df = pd.DataFrame(mean_list,index=labels)
var_df = pd.DataFrame(var_list,index=labels)
result = []
for j in range(test_data.shape[0]):
curr_test = test_data.iloc[j,:-1].tolist()
predict_prob = np.exp(-1*(curr_test-mean_df)**2/(2*var_df))/(np.sqrt(2*np.pi*var_df)) # 正态分布公式
prob = 1 # 初始化当前实例总概率
for k in range(test_data.shape[1]-1):
prob *= predict_prob.iloc[:,k]
cla = prob.index[np.argmax(prob.values)] # 返回最大概率的类别
result.append(cla)
test_data['predict'] = result
acc = (test_data.iloc[:,-1]==test_data.iloc[:,-2]).mean() # 计算预测准确率
print("预测准确率为:",acc)
return test_data
输入下列内容
dataSet = loadDataSet()
# 迭代10次,看下准确率情况
for i in range(10):
train_data, test_data = randSplit(dataSet, 0.8)
test_result = gnb_classify(train_data, test_data)
运行结果如下
若直接调用sklearn包,则用下列函数
def gbn_classify_by_sklearn(train_data,test_data):
"""
调用sklearn实现基于高斯分布的朴素贝叶斯分类函数
:param train_data: 训练数据集
:param test_data: 测试数据集
:return: 含预测分类结果的测试数据集
"""
from sklearn.naive_bayes import GaussianNB # 高斯分布
from sklearn.metrics import accuracy_score
# 建模
gnb_clf = GaussianNB()
gnb_clf.fit(train_data.iloc[:,:-1], train_data.iloc[:,-1])
# 对测试集进行预测
# predict():直接给出预测的类别
predict_class = gnb_clf.predict(test_data.iloc[:,:-1])
test_data['predict'] = list(predict_class)
acc = accuracy_score(test_data.iloc[:, -1], test_data.iloc[:,-2]) # 计算预测准确率
print("预测准确率为:", acc)
return predict_class
运行下面内容
dataSet = loadDataSet()
# 迭代10次,看下准确率情况
for i in range(10):
train_data, test_data = randSplit(dataSet, 0.8)
test_result = gbn_classify_by_sklearn(train_data, test_data)
运行结果如下