极大似然估计(MLE)
在日常生活中有一个常识:概率大的事件越有可能发生,极大似然估计便是利用这一思想去对参数进行估计:
假设X为离散型随机变量,其分布律为P{X=x} = p{x;θ},存在样本X1,X2,X3…,观察值为x1,x2,x3,…构造似然函数:
L
(
Θ
)
=
P
{
X
1
=
x
1
,
X
2
=
x
2
,
.
.
.
,
X
n
=
x
n
}
=
∏
p
{
x
;
Θ
}
L(\Theta) = P\{ X1=x1,X2=x2,...,Xn=xn\} = \prod p\{x;\Theta\}
L(Θ)=P{X1=x1,X2=x2,...,Xn=xn}=∏p{x;Θ}
该函数求最大值即是满足让当前已发生事件发生概率最大的参数值,为了方便求值,通常将其转换为对数似然函数
KaTeX parse error: Undefined control sequence: \ at position 46: …eta\ \ \ \ \ \ \̲ ̲
最大后验概率(MAP)
当前统计学界分为两大学派,频率学派与贝叶斯学派,二者对于概率的定义不同,从而在计算概率的方法上也有不同:
频率学派:频率学派认为θ是固定的,未知的,通过大量重复的实验,即可得到θ的值
贝叶斯学派:贝叶斯学派认为,概率依赖于已有的经验与知识,也就是说θ满足某个分布H(),在求解θ的值时,应该考虑θ的先验概率,从而计算出相应的概率。
最大后验概率便是贝叶斯思想下常用的参数估计方法:
a
r
g
max
log
p
(
θ
∣
x
)
=
a
r
g
max
log
p
(
x
∣
θ
)
p
(
θ
)
p
(
x
)
arg\max\log\ p(\theta|x) = arg\max\log\ \frac{p(x|\theta)p(\theta)}{p(x)}
argmaxlog p(θ∣x)=argmaxlog p(x)p(x∣θ)p(θ)
分母与θ无关,求解分子式:
a
r
g
max
log
p
(
x
∣
θ
)
p
(
θ
)
arg\max\log\ p(x|\theta)p(\theta)
argmaxlog p(x∣θ)p(θ)
可以观察到,MAP可看作由MLE与θ自身分布共同组合而成的,也可以说MLE中,认为p(Θ)为1,即服从U(0,1)。
贝叶斯估计(BE)
贝叶斯估计与MAP前半部分是一样的,相比于MAP,BE将θ完全看作一个概率变量,而不是一个确定的值:
p
(
θ
∣
x
)
=
α
p
(
x
∣
θ
)
p
(
θ
)
=
α
∏
p
(
x
∣
θ
)
p
(
θ
)
(
1
)
p(\theta|x) = \alpha p(x|\theta)p(\theta) = \alpha \prod p(x|\theta)p(\theta) (1)
p(θ∣x)=αp(x∣θ)p(θ)=α∏p(x∣θ)p(θ)(1)
α是与θ无关的部分。
在MAP中,只需要计算出(1)式的最大值即可,但BE会要求出θ分布的期望值作为最后的估计值。
朴素贝叶斯分类器
了解完概率基础知识之后,我们将使用贝努利模型实现一个朴素贝叶斯分类器,对垃圾邮件进行分类。
在对文本数据进行分类任务时,不可能直接对文本进行操作,要将其转换成数字向量,方便我们进一步操作。
文本向量化
读取样本邮件内容,首先对邮件内容进行清洗(包括去掉非字母数字及小写化):
def textParse(text):
"""
文本分词
:param text:
:return: 分词列表
"""
import re
text = re.sub(r'[^a-zA-Z0-9_]',' ',text)# 0个或多个非字母数字或下划线字符
listOfTokens = re.split(' ',text)
return [token.lower() for token in listOfTokens if len(token) > 2]
使用清洗后的分词列表,构建一个包含所有出现过的词并且每个词不重复的向量:
def createVocabList(dataset):
"""
创建词典列表,包含出现过的每一个词,且不重复
:param dataset:
:return:
"""
vocabSet = set([])
for document in dataset:
#print(vocabSet)
vocabSet = vocabSet | set(document) # 求两个集合的并集
return list(vocabSet)
创建训练集及测试集
这里采用留存交叉验证方法创建训练集及测试集
trainingSet = list(range(50)); testSet = []
# 随机选择10封邮件创建测试集
for i in range(10):
randIndex = int(random.uniform(0,len(trainingSet)))
testSet.append(trainingSet[randIndex])
del(trainingSet[randIndex])
构建词集模型
构建词集模型,描述邮件内容中出现的单词,其实是一个one-hot向量
def setOfWords2Vec(vecabList, inputSet):
"""
词集模型SOW,每个单词只出现一次
:param vecabList:
:param inputSet:
:return:
"""
returnVec = [0] * len(vecabList)
for word in inputSet:
if word in vecabList:
returnVec[vecabList.index(word)] = 1
else:
print("the word is not blonging into this List ", word)
return returnVec
训练参数
根据贝叶斯模型,我们要训练的参数有
a
r
g
max
log
p
(
x
∣
θ
)
p
(
θ
)
求
:
p
(
θ
)
、
p
(
x
∣
θ
)
arg\max\log\ p(x|\theta)p(\theta) \\求:p(\theta)、p(x|\theta)
argmaxlog p(x∣θ)p(θ)求:p(θ)、p(x∣θ)
这里我们计算通过每一个词Wi是垃圾邮件的概率p(Wi|θ),将一封邮件中所有的p(Wi|θ)p(θ)求联合概率
因为程序会下溢出,当连乘的p(w|θ)值很小时,乘积最后会四舍五入为0,所以转化为对数概率求和。
∏
p
(
w
∣
θ
)
p
(
θ
)
=
∑
l
o
g
(
p
(
w
∣
θ
)
p
(
θ
)
)
\prod p(w|\theta)p(\theta) = \sum log(p(w|\theta)p(\theta))
∏p(w∣θ)p(θ)=∑log(p(w∣θ)p(θ))
当某个词Wi未出现,即某属性值未出现时,对应SOW位置上的值为0,则p(Wi|θ) = 0,但在之后的连乘中,该值会将其他p(Wi|θ)抹去,所有这里使用到一个拉普拉斯平滑的思想:
P
(
c
)
=
∣
D
c
∣
+
1
∣
D
∣
+
N
P
(
X
i
∣
c
)
=
∣
D
(
c
,
x
i
)
∣
+
1
∣
D
c
∣
+
N
i
P(c) = \frac{|Dc| + 1}{|D|+N} \\ P(Xi|c) = \frac{|D(c,xi)| + 1}{|Dc| + Ni}
P(c)=∣D∣+N∣Dc∣+1P(Xi∣c)=∣Dc∣+Ni∣D(c,xi)∣+1
N为类别数,Ni为该属性上可取的类别数
def trainNB0(trainMatrix, trainCategory):
"""
训练特征向量
:param trainMatrix:
:param trainCategory:
:return: p1Vect(垃圾邮件的概率向量),p0Vect(非垃圾邮件的概率向量),pAbusive(先验概率)
"""
numTrainDocs = len(trainMatrix)
numWords = len(trainMatrix[0])
pAbusive = sum(trainCategory)/float(numTrainDocs) # 为1的概率,也就是为垃圾邮件的先验概率
p0Num = ones(numWords) # 统计对应单词出现次数,拉普拉斯平滑
p1Num = ones(numWords)
p0Denom = 2.0 # 总词数,初始化为2,拉普拉斯平滑
p1Denom = 2.0
for i in range(numTrainDocs):
if trainCategory[i] == 1:
p1Num += trainMatrix[i]
p1Denom += sum(trainMatrix[i])
else:
p0Num += trainMatrix[i]
p0Denom += sum(trainMatrix[i])
# 取对数,防止下溢出
p1Vect = log(p1Num/p1Denom)
p0Vect = log(p0Num/p0Denom)
# 如不取对数,准确率会大大降低
# p1Vect = p1Num/p1Denom
# p0Vect = (p0Num/p0Denom)
return p0Vect,p1Vect,pAbusive
测试结果
将测试集转换为SOW结构,分别计算二分类的概率,取概率大者。
def classifyNB(p0V,p1V,vec2Classify,pClass1):
"""
分类
:param p0V:
:param p1V:
:param vec2Classify:
:param pClass1:
:return: 1 - 垃圾邮件, 0 - 非垃圾邮件
"""
p1 = sum(vec2Classify * p1V) + log(pClass1)
p0 = sum(vec2Classify * p0V) + log(1 - pClass1)
if p1 > p0:
return 1
return 0
完整代码
def spamText():
"""
垃圾邮件分类
:return:
"""
docList = []; classList = []; fullText = []
for i in range(1,26):
wordList = textParse(open("./email/spam/"+str(i)+".txt",'r').read())
docList.append(wordList)
classList.append(1) # 垃圾邮件
fullText.extend(wordList) #
wordList = textParse(open("./email/ham/"+str(i)+".txt",'r').read())
docList.append(wordList)
classList.append(0)
fullText.extend(wordList)
vocabList = createVocabList(docList) # 创建词典
trainingSet = list(range(50)); testSet = []
# 随机选择10封邮件创建测试集
for i in range(10):
randIndex = int(random.uniform(0,len(trainingSet)))
testSet.append(trainingSet[randIndex])
del(trainingSet[randIndex])
#print(trainingSet)
trainMat = []; trainClasses = [];
for docIndex in trainingSet:
trainMat.append(setOfWords2Vec(vocabList,docList[docIndex]))
trainClasses.append(classList[docIndex])
p0V,p1V,pSpam = trainNB0(trainMat,trainClasses)
# print(p0V,p1V,pSpam)
# 测试
errorCount = 0
for docIndex in testSet:
wordVector = setOfWords2Vec(vocabList,docList[docIndex])
if(classifyNB(p0V,p1V,wordVector,pSpam)) != classList[docIndex]:
errorCount += 1
# print(errorCount,len(testSet))
print("error rate is %.2f%%"%(float(errorCount)/len(testSet)*100))
以上参考自《机器学习实战》第四章