项目概述
构建一个快速过滤器来屏蔽留言板上的侮辱性言论。如果某条留言使用了负面或者侮辱性的语言,那么就将该留言标识为内容不当。对此问题建立两个类别: 侮辱类和非侮辱类,使用 1 和 0 分别表示。
准备数据
准备数据的方法比较简单,只是自己构造的训练数据集而已,嵌套列表里的每个列表表示已经分好词的句子样本。可以看到,为简单起见,每个句子里的词都不重复。
def loadDataSet():
"""
创建数据集
:return: 文档列表postingList,所属类别classVec
"""
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] # 1表示侮辱性 0表示正常
return postingList, classVec
创建词表
将我们刚刚自创的数据样本集里出现的所有单词提取出来,创建一个不重复的单词词表,创建词表的目的是为了接下来对每个样本进行词向量化作准备,其词向量里每个特征出现的位置要与词表里各词的位置相对应。
def createVocabList(dataSet):
"""
获取文档列表中出现的所有单词集合
:param dataSet: 数据集文档列表[[], [], [],...]
:return: 所有单词的列表(去重)
"""
vocabSet = set([]) # 创建一个空set集合
for document in dataSet:
vocabSet = vocabSet | set(document)
return list(vocabSet)
构建词向量
前面的样本是以字符列表的形式,现在要将它们向量化,对每个文章样本构建词向量。在构建中,对于每个文章里重复出现的词语(但本实例中无此现象),我们选用伯努利模型,即每个样本的词向量长度都与词表长度一致,并且一个单词重复出现我们只认为它出现了一次,如果某个单词出现了,则基于词表里该词的索引位置,在词向量的相同位置下赋特征值为1,否则为0
def setOfWords2Vec(vocabList, inputSet):
"""
为文档列表中每个文档构建文档词向量
遍历查看每个单词是否出现,出现则将其向量位,置1
:param vocabList: 所有单词集合列表
:param inputSet: 一篇训练文档数据集['','','']
:return: 文档词向量列表
"""
# 创建一个和词汇表等长的向量,并将其元素都置为0,即默认每个单词都没有在词汇表中出现
returnVec = [0] * len(vocabList) # [0,0,0,,...]
# 遍历这篇文档中的所有单词,如果单词在词汇表中,则将输出的文档词向量中的对应值置为1
for word in inputSet:
if word in vocabList:
returnVec[vocabList.index(word)] = 1
else:
print("thw word: ", word, " is not in my vocabList!")
return returnVec
训练朴素贝叶斯模型
利用已向量化的文章词向量矩阵和对应类别标签,进行贝叶斯模型的训练。其目的是计算出每个类别下每个单词的条件概率:p{w1|c0},…,p{wi|c0},p{w1|c1},…,p{wi|c1}以及每个类别文档出现的概率:p{c0} p{c1}
首先侮辱性类别的概率p(c1)即先验概率比较好求,就是:
P
(
“
侮
辱
性
类
”
)
=
侮
辱
性
文
章
的
个
数
总
文
章
个
数
P(“侮辱性类”)=\frac{侮辱性文章的个数}{总文章个数}
P(“侮辱性类”)=总文章个数侮辱性文章的个数
接下来计算各单词的条件概率:
定义两个矩阵p0Num和p1Num,分别表示侮辱性文章/正常文章中各单词出现的次数,其长度与词表长度一致,假设词表长度为n,xi表示特征词,ci表示该特征词出现次数
词
表
v
o
c
a
b
L
i
s
t
=
[
x
1
x
2
x
3
x
4
.
.
.
x
n
]
词表vocabList = \begin{bmatrix} x_1 &x _2& x_3 & x_4 & ... &x_n\\ \end{bmatrix}
词表vocabList=[x1x2x3x4...xn]
p
0
N
u
m
=
[
c
1
c
2
c
3
c
4
.
.
.
c
n
]
p0Num = \begin{bmatrix} c_1 &c _2& c_3 & c_4 & ... &c_n\\ \end{bmatrix}
p0Num=[c1c2c3c4...cn]
p
1
N
u
m
=
[
c
1
c
2
c
3
c
4
.
.
.
c
n
]
p1Num = \begin{bmatrix} c_1 &c _2& c_3 & c_4 & ... &c_n\\ \end{bmatrix}
p1Num=[c1c2c3c4...cn]
其中p0Num和p1Num的值都初始化为1,这是为了防止这样的情形:在训练时,某个词若没有在某个类的文章里出现,其计算条件概率的分子就为0,那么其条件概率p(xi|c)=0,由于要取对数log{p(xi|c)}作为最终的条件概率值,那对0取对数就会造成异常。因此,对所有单词的出现次数都取一个初始值1,也是运用了平滑技术,相应地对每个类别文章里出现的单词总数要初始化为2。
运用平滑技术的另一个用处是在预测一个新文章里时,出现了在训练样本的词表里从未出现过的词,此时就利用平滑处理计算一个默认概率值,我们在后文里再细说。
接下来就是遍历样本词向量矩阵,如果是侮辱性样本或正常样本,就将其词向量trainMatrix[i]累加到p1Num或p0Num矩阵里,同时对该词向量里的特征值进行累加。因为我们选的模型是伯努利模型,词向量里各维度非0即1,运用平滑处理在计算单词条件概率时的公式如下:
P
(
“
s
t
u
p
i
d
”
∣
侮
辱
性
类
)
=
出
现
“
s
t
u
p
i
d
”
的
侮
辱
性
文
章
个
数
+
1
每
个
侮
辱
性
文
章
中
所
有
词
出
现
次
数
(
出
现
了
只
计
算
一
次
)
的
总
和
+
2
P(“stupid”|侮辱性类)=\frac{出现“stupid”的侮辱性文章个数+1}{每个侮辱性文章中所有词出现次数(出现了只计算一次)的总和+2}
P(“stupid”∣侮辱性类)=每个侮辱性文章中所有词出现次数(出现了只计算一次)的总和+2出现“stupid”的侮辱性文章个数+1
p1Num或p0Num保存的即是各单词对应上述公式的分子,在各类别下,向量的累加求和,其各维度下特征值对应的即是某个类别文章里各单词出现的次数,也就是出现某个单词的该类别文章个数
这样条件概率公式里的分子就算出了。
而对每个词向量的特征值求和,即表示该类别文章里出现的单词个数(不重复),从而求得侮辱类/正常文章里的总词数p1Demo和p0Demo,就算出了公式里的分母。
最后分子矩阵和分母矩阵相除,再求对数,即得出训练样本里每个词的对应各类别的条件概率p{wi|c}
def trainNB(trainMatrix, trainCategory):
"""
一篇文档用w词向量表示,w=(w1, w2, w3, ...wn)
利用训练文档数据列表学习出每个类别下每个单词的条件概率:p{w1|c0},...,p{wi|c0},p{w1|c1},...,p{wi|c1}
以及每个类别文档出现的概率:p{c0} p{c1}
:param trainMatrix: 文档词向量矩阵 [[1 0 0 1..], [1 1 0 0..], [0 1 0 1..], ...]
:param trainCategory: 文档对应的类别列表[1 0 1 0...],其长度等于文档词向量矩阵的长度,1表示侮辱性,0表示正常
:return: 正常类别下出现各词的条件概率矩阵p0Vect,侮辱性类别下出现各词的条件概率矩阵p1Vect,侮辱性类别的概率pAbusive
"""
# 文档数
numTrainDocs = len(trainMatrix)
# 词向量里包含的单词数
numWords = len(trainMatrix[0])
# 侮辱性文档出现的概率,即p{c1},等于所有侮辱性文档的个数 / 总文档数
pAbusive = sum(trainCategory) / float(numTrainDocs)
# 构造在每个类别文档中,各单词出现次数数组,用来计算p{wi|c0}=Count(wi,c0) / Count(c0) = c0类出现wi词的文档数 / c0类文档数
# or = c0类中wi词的出现次数 / c0类单词数
p0Num = np.ones(numWords) # 初始化为1是避免p1Num / p1Demo有分量为0,从而导致log(p1Num / p1Demo)异常
p1Num = np.ones(numWords)
# 各类别文档下出现的单词总数
p0Demo = 2.0
p1Demo = 2.0
for i in range(numTrainDocs):
# 如果是侮辱性文档
if trainCategory[i] == 1:
# 将其对应的词向量累加到p1Num
p1Num += trainMatrix[i]
# 累加侮辱性文档中出现的单词总数
p1Demo += sum(trainMatrix[i])
else:
p0Num += trainMatrix[i]
p0Demo += sum(trainMatrix[i])
# 计算各类别下出现wi词的条件概率p{wi|c0},p{wi|c1}数组
p1Vect = np.log(p1Num / p1Demo) # array[ln(p{w1|c1}), ln(p{w2|c1}), ln(p{w3|c1}), ...]
p0Vect = np.log(p0Num / p0Demo) # array[ln(p{w1|c0}), ln(p{w2|c0}), ln(p{w3|c0}), ...]
return p0Vect, p1Vect, pAbusive
预测分类
前面我们已从训练样本出计算出了朴素贝叶斯分类模型,即各类别下出现各单词的条件概率矩阵,和出现各类别的先验概率值
下面就是对传入的一个待测文章词向量,利用已算好的条件概率和先验概率,判断其所属类别,其最终取对数后的预测条件概率的公式如下,以侮辱性类别为例,X表示词向量,xi表示特征单词:
l
o
g
P
(
侮
辱
性
∣
X
)
=
l
o
g
P
(
x
1
∣
侮
辱
性
)
+
l
o
g
P
(
x
2
∣
侮
辱
性
)
+
l
o
g
P
(
x
3
∣
侮
辱
性
)
+
.
.
.
+
l
o
g
P
(
“
x
n
”
∣
侮
辱
性
)
+
l
o
g
P
(
侮
辱
性
)
log{P(侮辱性|X)} = log{P(x_1|侮辱性)}+log{P(x_2|侮辱性)}+log{P(x_3|侮辱性)}+...+log{P(“x_n”|侮辱性)}+log{P(侮辱性)}
logP(侮辱性∣X)=logP(x1∣侮辱性)+logP(x2∣侮辱性)+logP(x3∣侮辱性)+...+logP(“xn”∣侮辱性)+logP(侮辱性)
其中每个 l o g P ( x i ∣ 侮 辱 性 ) log{P(x_i|侮辱性)} logP(xi∣侮辱性)已计算在矩阵p1Vec里
预测时,若新文章内的词出现在训练样本的词表里,则直接取出该词对应的条件概率值。
若没有,则取默认概率值(这里就是平滑处理算出一个从未出现过的单词的概率值),还是基于训练样本数据集来算默认值,对于伯努利模型,其平滑处理新词的默认概率计算公式为,其中xi表示一个新单词:
P
(
x
i
∣
侮
辱
类
)
=
出
现
x
i
的
侮
辱
性
文
章
数
+
1
每
篇
侮
辱
性
文
章
中
所
有
词
出
现
次
数
(
出
现
了
只
计
算
一
次
)
的
总
和
+
2
=
1
每
篇
侮
辱
性
文
章
中
所
有
词
出
现
次
数
(
出
现
了
只
计
算
一
次
)
的
总
和
+
2
P(x_i|侮辱类)=\frac{出现x_i的侮辱性文章数+1}{每篇侮辱性文章中所有词出现次数(出现了只计算一次)的总和+2}\\=\frac{1}{每篇侮辱性文章中所有词出现次数(出现了只计算一次)的总和+2}
P(xi∣侮辱类)=每篇侮辱性文章中所有词出现次数(出现了只计算一次)的总和+2出现xi的侮辱性文章数+1=每篇侮辱性文章中所有词出现次数(出现了只计算一次)的总和+21
但在本例中,我们直接将待测词向量与类别条件概率矩阵相乘了(默认待测文章里的词都是在训练集词表里出现过的),这样的话如果某个词对应的特征值为0即没出现该词,相乘后也不会参与到累加运算中,最后加上各类别先验概率,得到最终预测值
def classifyNB(vec2Classify, p0Vec, p1Vec, pClass1):
"""
给出待测文档词向量,利用训练好的各类别下出现各单词的条件概率及类别概率,计算该待测文档属于各类别的概率值,将其分类
:param vec2Classify: 待测文档词向量,即[0,1,1,0,1...]
:param p0Vec: 已训练好的类别0下出现各单词的条件概率,即array[ln(p{w1|c0}), ln(p{w2|c0}), ln(p{w3|c0}), ...]
:param p1Vec: 已训练好的类别1下出现各单词的条件概率,即array[ln(p{w1|c1}), ln(p{w2|c1}), ln(p{w3|c1}), ...]
:param pClass1: 已训练好的类别1文档出现的概率
:return: 类别1 or 0
"""
# 这里的向量相乘表示将词向量里每个词与其对应的概率相关联,比如词向量某个词的值为0,那么相乘后该词的概率值为0,
# 若某个词的值为1,那么相乘后该词的概率值为所训练出的条件概率值p{w|c},最后累加和(取对数的作用)
p0 = sum(vec2Classify * p0Vec) + np.log(1.0 - pClass1)
p1 = sum(vec2Classify * p1Vec) + np.log(pClass1)
if p0 > p1:
return 0
else:
return 1
测试朴素贝叶斯
写好上述各模块后,让我们测试一下简单实现的朴素贝叶斯
def testingNB():
"""
测试朴素贝叶斯算法
:return:
"""
# 1 加载文档数据集,获取所有文档对象和标签列表
postingList, classVec = loadDataSet()
# 2 创建文档中出现的所有单词列表
vocabList = createVocabList(postingList)
# 3 创建文档词向量矩阵(数组)
trainMat = []
for document in postingList:
trainMat.append(setOfWords2Vec(vocabList, document))
# 4 训练数据,获取类别概率及类别下单词条件概率
p0V, p1V, pAb = trainNB(np.array(trainMat), classVec)
# 5 测试数据
testEntry = ['love', 'my', 'dalmation']
thisDoc = np.array(setOfWords2Vec(vocabList, testEntry))
print(testEntry, 'classified as: ', classifyNB(thisDoc, p0V, p1V, pAb))
testEntry = ['stupid', 'garbage']
thisDoc = np.array(setOfWords2Vec(vocabList, testEntry))
print(testEntry, 'classified as: ', classifyNB(thisDoc, p0V, p1V, pAb))
if __name__ == "__main__":
testingNB()
结果: