一. 生成式(generative)学习算法
如果算法直接学习,或者尝试学习从输入空间到类别的映射关系的算法,称为判别式(discriminative)学习算法;比线性回归(lineaar regression)的模型:
再比如逻辑回归(logistic regression):
这里的是sigmoid函数
而另外一种算法是建立(和)的模型,这类算法称为生成式(generative)学习算法,我们今天要讨论的朴素贝叶斯算法即是其中一个。对(先验 prior)和,使用贝叶斯规则来推导给定的的后验(posterior)分布:
其中分母可由全概率公式计算:
实际上,我们在计算做预测时,可以不用计算分母,因为:
二. 朴素贝叶斯(Naive Bayes)
2.1 朴素贝叶斯算法
假设我们想利用机器学习来构建一个言论过滤器,希望对网站的留言评论自动区分是侮辱性言论,还是非侮辱性言论。现在已知一个训练数据集(一些已经标记为侮辱或者非侮辱的言论集合)如下:
"""
函数说明:创建实验样本
Parameters:
无
Returns:
postingList - 实验样本切分的词条
classVec - 类别标签向量
"""
def loadDataSet():
postingList = [['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] # 对应postingList中每个样本的类别标签向量,1代表侮辱性言论, 0代表非侮辱性言论
return postingList, classVec
令 表示该言论是否是侮辱性言论:
接下来,我们将样本数据,即postingList,转化成特征向量,用 表示,其中包含单词特征:
编码到向量中的单词的集合称为词汇表,例如“ my”,“stupid”等等,向量的长度等于词汇表的长度。
注意,这里的词汇表不是将英语词典中单词全部列出来,通常是将训练数据集中的所有单词,即使仅出现过一次,放到词汇表中。这样做可以减少模型的词汇数量,从而减少计算量,节省空间;还有个好处是能够将英语词典中不会出现的词,但会出现在留言评论中的词,比如“”放到词汇表中;同时还需要剔除一些高频词,比如 “” “” “”,这些词在很多的文本中都会出现,对区分是否是侮辱性言论没有任何帮助。
特征向量与词汇表如下图所示,言论中包含“”,“buy”,不包含“aardvard”,“aardwolf”,“zygmurgy”:
创建词汇表代码如下:
"""
函数说明:将切分的实验样本词条整理成不重复的词条列表,也就是词汇表
Parameters:
dataSet - 样本数据集 postingList
Returns:
vocabSet - 返回不重复的词条列表,也就是词汇表
"""
def createVocabList(dataSet):
vocabSet = set([])
for document in dataSet:
vocabSet = vocabSet | set(document)
return list(vocabSet)
将样本数据转换成特征向量代码如下:
"""
函数说明:根据vocabList词汇表,将inputSet向量化,向量的每个元素为1或0
Parameters:
vocabList - 词汇表
inputSet - 某条言论文档
Returns:
returnVec - 言论文档向量
"""
def setOfWords2Vec(vocabList, inputSet):
returnVec = [0] * len(vocabList) # 创建一个其中所含元素都为0的向量
for word in inputSet: # 遍历每个词条
if word in vocabList: # 如果词条存在于词汇表中,则置1
returnVec[vocabList.index(word)] = 1
else:
print("the word: %s is not in my vocabulary !" % word)
return returnVec
现在我们来构建。上面代码示例中词汇表中单词较少,但如果词汇表中包含50000个单词,那么(是50000维0和1组成的向量),如果我们直接用多项式来构造的个可能的结果,那么多项式的参数向量有维,显然参数太多了。
因此算法做了一个强假设,即假设给定的情况下条件独立,这个假设就称为朴素贝叶斯假设,算法称为朴素贝叶斯分类。例如,如果表示侮辱性言论,“”是第个单词,“”是第个单词,那么我们假设如果已知,那么的值(“”是否出现在言论中) 对的值(“”是否出现在言论中)没有任何影响。正式一点来描述,上述可以写成。需要注意的是,这里并不是说和相互独立,相互独立表示为,而是仅假设和在给定的情况下条件独立。根据上述假设,有如下等式成立:
第一个等式是概率的基本性质,第二个等式用了朴素贝叶斯假设。
我们的模型中的参数为,和,其中为词汇表中第个单词。给定一个训练数据集,我们可以写出似然函数:
最大化关于参数, , 的上述似然函数,得到第个单词相关参数的极大似然估计:
即是对的估计;
即是对的估计;
即是对的估计;同理
即是对的估计;参数表达式中的 表示“与”。上面的参数不难理解,就是侮辱性言论()中单词出现的百分比。训练阶段代码如下:
"""
函数说明:朴素贝叶斯分类器训练函数
Parameters:
trainMatrix - 训练文档矩阵,即setOfWords2Vec返回的returnVec构成的矩阵
trainCategory - 训练类别标签向量,即loadDataSet返回的classVec
Returns:
p0Vect - 非侮辱类的条件概率数组
p1Vect - 侮辱类的条件概率数组
pAbusive - 文档属于侮辱类的概率
"""
def trainNB0(trainMatrix, trainCategory):
numTrainDocs = len(trainMatrix) #计算训练的文档数目
numWords = len(trainMatrix[0]) #计算每篇文档的词条数
pAbusive = sum(trainCategory)/float(numTrainDocs) #文档属于侮辱类的概率
p0Num = np.zeros(numWords) #创建numpy.zeros数组,词条出现数初始化为0
p1Num = np.zeros(numWords)
p0Denom = 0.0 #分母初始化为0
p1Denom = 0.0
for i in range(numTrainDocs):
if trainCategory[i] == 1: #向量相加,统计属于侮辱类的条件概率所需的数据,即P(w0|1),P(w1|1),P(w2|1)···
p1Num += trainMatrix[i]
p1Denom += sum(trainMatrix[i])
else: #向量相加,统计属于非侮辱类的条件概率所需的数据,即P(w0|0),P(w1|0),P(w2|0)···
p0Num += trainMatrix[i]
p0Denom += sum(trainMatrix[i])
p1Vect = p1Num/p1Denom
p0Vect = p0Num/p0Denom
return p0Vect, p1Vect, pAbusive #返回属于侮辱类的条件概率数组,属于非侮辱类的条件概率数组,文档属于侮辱类的概率
已知参数估计之后,对一个特征向量的新样本计算是侮辱性言论的概率如下:
同样,计算非侮辱性言论的概率如下:
如果计算得出,则认为言论是侮辱性言论,反之是非侮辱性言论,预测代码如下:
"""
函数说明:朴素贝叶斯分类器分类函数
Parameters:
vec2Classify - 待分类的词条数组
p0Vec - 侮辱类的条件概率数组
p1Vec -非侮辱类的条件概率数组
pClass1 - 文档属于侮辱类的概率
Returns:
0 - 属于非侮辱类
1 - 属于侮辱类
"""
def classifyNB0(vec2Classify, p0Vec, p1Vec, pClass1):
p1 = reduce(lambda x,y : x * y, vec2Classify * p1Vec) * pClass1 # 对应元素相乘
p0 = reduce(lambda x,y : x * y, vec2Classify * p0Vec) * (1 - pClass1)
print('p0:', p0)
print('p1', p1)
if p1 > p0:
return p1
else:
return p0
2.2 下溢出与拉普拉斯平滑
2.1节中我们使用朴素贝叶斯算法构造了言论分类器,下面我们对算法进行测试:
"""
函数说明:测试朴素贝叶斯分类器
Parameters:
无
Returns:
无
"""
def testingNB0():
listOPosts, classVec = loadDataSet() # 创建实验样本
myVocabList = createVocabList(listOPosts) # 创建词汇表
# 打印中间结果
print('myVocabList:\n', myVocabList)
trainMat = []
for postinDoc in listOPosts:
trainMat.append(setOfWords2Vec(myVocabList, postinDoc)) # 将实验样本向量化
p0V, p1V, pAb = trainNB0(np.array(trainMat), np.array(classVec)) # 训练朴素贝叶斯分类器
# 打印中间结果
print('p0V:\n', p0V)
print('p1V:\n', p1V)
print('classVec:\n', classVec)
print('pAb:\n', pAb)
testEntry = ['love', 'my', 'dalmation'] # 测试样本1
thisDoc = np.array(setOfWords2Vec(myVocabList, testEntry)) # 测试样本向量化
if classifyNB0(thisDoc, p0V, p1V, pAb):
print(testEntry, '属于侮辱类') # 执行分类并打印分类结果
else:
print(testEntry, '属于非侮辱类') # 执行分类并打印分类结果
testEntry = ['stupid', 'garbage'] # 测试样本2
thisDoc = np.array(setOfWords2Vec(myVocabList, testEntry)) # 测试样本向量化
if classifyNB0(thisDoc, p0V, p1V, pAb):
print(testEntry, '属于侮辱类') # 执行分类并打印分类结果
else:
print(testEntry, '属于非侮辱类') # 执行分类并打印分类结果
if __name__ == "__main__":
testingNB0()
运行结果截图如下:
小伙伴们,发现问题了吗?显然这个结果是不对的。算法存在两个问题:
- 某些单词0概率,导致整体乘积为0,例如“”;
- 下溢出(underflow):
对于第一个问题,我们通过打印中间结果可以看出:
又或者对一个训练数据集中未出现的词,概率也是0,显然,这样是不合理的,因为训练集有限,不能因为训练集中没有出现,就认为这个词永远不会出现。记得吴恩达老师在机器学习课上讲了个段子,斯坦福大学的校篮球队接连输了5场比赛,问下一场比赛赢的概率?连输5场,下一场肯定输吗?合理的预测是有赢的概率,只是比较小,设置赢的概率为1/7吧。这种做法就叫做拉普拉斯平滑(Laplace Smoothing)。具体做法是修改极大似然估计的参数:
即是对的估计;
即是对的估计。
其中代表的可能取值数量,我们的例子中的取值只有和两种,因此。
那么为什么分子加,分母加呢?因为要保证相加仍然是。下面我们检验一下,假设随机变量的取值有个
其中,则应用拉普拉斯平滑之后
不难推出
除此之外,另外一个遇到的问题就是下溢出,这是由于太多很小的数相乘造成的。学过数学的人都知道,两个小数相乘,越乘越小,这样就造成了下溢出。在程序中,在相应小数位置进行四舍五入,计算结果可能就变成0了。为了解决这个问题,对乘积结果取自然对数。通过求对数可以避免下溢出或者浮点数舍入导致的错误。同时,采用自然对数进行处理不会有任何损失。下图给出函数f(x)和ln(f(x))的曲线:
检查这两条曲线,就会发现它们在相同区域内同时增加或者减少,并且在相同点上取到极值。它们的取值虽然不同,但不影响最终结果。因此我们可以对上篇文章的trainNB0(trainMatrix, trainCategory)函数进行更改,修改如下:
"""
函数说明:朴素贝叶斯分类器训练函数
Parameters:
trainMatrix - 训练文档矩阵,即setOfWords2Vec返回的returnVec构成的矩阵
trainCategory - 训练类别标签向量,即loadDataSet返回的classVec
Returns:
p0Vect - 非侮辱类的条件概率数组
p1Vect - 侮辱类的条件概率数组
pAbusive - 文档属于侮辱类的概率
"""
def trainNB(trainMatrix, trainCategory):
numTrainDocs = len(trainMatrix) # 计算训练的文档数目
numWords = len(trainMatrix[0]) # 计算每篇文档的词条数
pAbusive = sum(trainCategory) / float(numTrainDocs) # 文档属于侮辱类的概率
p0Num = np.ones(numWords)
p1Num = np.ones(numWords) # 创建numpy.ones数组,词条出现数初始化为1,拉普拉斯平滑
p0Denom = 2.0
p1Denom = 2.0 # 分母初始化为2,拉普拉斯平滑
for i in range(numTrainDocs):
if trainCategory[i] == 1: # 向量相加,统计属于侮辱类的条件概率所需的数据,,即P(w0|1),P(w1|1),P(w2|1)···
p1Num += trainMatrix[i]
p1Denom += sum(trainMatrix[i])
else: # 向量相加,统计属于非侮辱类的条件概率所需的数据,即P(w0|0),P(w1|0),P(w2|0)···
p0Num += trainMatrix[i]
p0Denom += sum(trainMatrix[i])
p1Vect = np.log(p1Num / p1Denom) # 取对数,防止下溢出
p0Vect = np.log(p0Num / p0Denom)
return p0Vect, p1Vect, pAbusive # 返回属于侮辱类的条件概率数组,属于非侮辱类的条件概率数组,文档属于侮辱类的概率
"""
函数说明:朴素贝叶斯分类器分类函数
Parameters:
vec2Classify - 待分类的词条数组
p0Vec - 非侮辱类的条件概率数组
p1Vec -侮辱类的条件概率数组
pClass1 - 文档属于侮辱类的概率
Returns:
0 - 属于非侮辱类
1 - 属于侮辱类
"""
def classifyNB(vec2Classify, p0Vec, p1Vec, pClass1):
p1 = sum(vec2Classify * p1Vec) + np.log(pClass1) # 对应元素相乘。logA * B = logA + logB,所以这里加上log(pClass1)
p0 = sum(vec2Classify * p0Vec) + np.log(1.0 - pClass1)
if p1 > p0:
return 1
else:
return 0
"""
函数说明:测试朴素贝叶斯分类器
Parameters:
无
Returns:
无
"""
def testingNB():
listOPosts, classVec = loadDataSet() # 创建实验样本
myVocabList = createVocabList(listOPosts) # 创建词汇表
# 打印中间结果
print('myVocabList:\n', myVocabList)
trainMat = []
for postinDoc in listOPosts:
trainMat.append(setOfWords2Vec(myVocabList, postinDoc)) # 将实验样本向量化
p0V, p1V, pAb = trainNB(np.array(trainMat), np.array(classVec)) # 训练朴素贝叶斯分类器
# 打印中间结果
print('p0V:\n', p0V)
print('p1V:\n', p1V)
print('classVec:\n', classVec)
print('pAb:\n', pAb)
testEntry = ['love', 'my', 'dalmation'] # 测试样本1
thisDoc = np.array(setOfWords2Vec(myVocabList, testEntry)) # 测试样本向量化
if classifyNB(thisDoc, p0V, p1V, pAb):
print(testEntry, '属于侮辱类') # 执行分类并打印分类结果
else:
print(testEntry, '属于非侮辱类') # 执行分类并打印分类结果
testEntry = ['stupid', 'garbage'] # 测试样本2
thisDoc = np.array(setOfWords2Vec(myVocabList, testEntry)) # 测试样本向量化
if classifyNB(thisDoc, p0V, p1V, pAb):
print(testEntry, '属于侮辱类') # 执行分类并打印分类结果
else:
print(testEntry, '属于非侮辱类') # 执行分类并打印分类结果
if __name__ == "__main__":
testingNB()
运行结果如下:
Reference
Naive_Bayes_classifier wiki
Gaussian Discriminant Analysis an example of Generative Learning Algorithms
cs229 lecture notes Part IV Generative Learning algorithms
Machine Learning in Action
Jack Cui 机器学习实战教程(四):朴素贝叶斯基础篇之言论过滤器