-----------------------2018年3月14日,虽然巨星陨落,但我们依然要保持前行
朴素贝叶斯(navie bayes),首先要理解贝叶斯:
目的是求w条件下,Ci发生的概率。其中Ci发生的概率、Ci发生的情况下,w发生的概率、w发生的概率都是可以根据已有的数据计算出来的。
另外要理解的就是朴素一词,朴素贝叶斯假设Ci之间互相独立,而w条件中各维度也是独立的,这在现实中一般不成立,因此称为朴素。虽然在现实中不太现实,但是朴素贝叶斯用来做分类依然能得到较好的效果。
最近在看《机器学习实战》朴素贝叶斯一章的时候觉得他给出的代码或多或少存在一些问题,虽然他的代码并不影响最终的分类结果。出于严谨,认真研究了一波,写了自己的朴素贝叶斯代码,在分类的同时还能给出属于每一个类的概率。
(一)女孩嫁不嫁问题
首先解决网络上流传的女孩子嫁与不嫁的问题(参考: http://blog.csdn.net/fisherming/article/details/79509025)
手工计算过程参考博客中已经给出,下面给出代码分析
1、加载数据
def load_data_marry():
# 帅,性格好,身高,上进
_ds = [[1, 0, 0, 0], # 不嫁
[0, 1, 0, 1], # 不嫁
[1, 1, 0, 1], # 嫁
[0, 1, 1, 1], # 嫁
[1, 0, 0, 1], # 不嫁
[0, 0, 0, 0], # 不嫁
[1, 1, 1, 0], # 嫁
[0, 1, 1, 1], # 嫁
[1, 0, 1, 1], # 嫁
[0, 0, 1, 1], # 不嫁
[1, 1, 0, 0], # 不嫁
[1, 1, 0, 0]] # 不嫁
_labels = [0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0]
return _ds, _labels
这里我将数据进行了一定的修改,与上面的图片个别地方不太一样,主要是为了避免出现只要嫁身高就一定高或者只要不嫁身高就一定不高的情况出现,更加合理,更加有学习意义
2、计算贝叶斯公式的几个概率
# 原始的朴素贝叶斯
def train_bayes_org(_train_matrix, _labels):
num_traindocs = len(_train_matrix) # 训练文档的数量
words_list_len = len(_train_matrix[0]) # 特征向量的长度,或者说是词汇表的长度
_p_c1 = sum(_labels)/float(num_traindocs)
_p_wi_c0 = np.zeros(words_list_len) # 理论上这里应该初始化为0,但是为了让最后计算
_p_wi_c1 = np.zeros(words_list_len)
_c0_num = 0.0 # 该类别中单词的总数,为了计算每个单词出现的概率
_c1_num = 0.0
for i in range(num_traindocs):
if _labels[i] == 1:
_p_wi_c1 += _train_matrix[i] # 统计每个单词出现的次数
_c1_num += 1
else:
_p_wi_c0 += _train_matrix[i]
_c0_num += 1
# 取对数,将后面测试时候P(w|C)的计算换为加法,如果是乘法,则会出现数十甚至数百个小数相乘的现象
_p_w_c0 = _p_wi_c0 / _c0_num
# print(sum(_p_wi_c0 / _c0_num))
# 这里返回一个与词汇表一样长度的列表,每个元素表示对应单词在该类别中出现的概率,也就是P(wi|c1)
_p_w_c1 = _p_wi_c1 / _c1_num
# print(sum(_p_wi_c1 / _c1_num))
return _p_w_c0, _p_w_c1, 1-_p_c1, _p_c1
代码中的注释是垃圾邮件分类时候写的,可以参考。那个for循环主要是求Ci成立的情况下,wi发生的概率,最后面将统计值除以num得到概率。这里之所以求Ci发生的情况下,wi的概率,主要是为了求公式中的P(w|Ci)=P(w0|Ci)*P(w1|Ci)*P(w2|Ci)...
函数最后还返回C0和C1
3、分类计算
def test_bayes_org(test_feature, _p_w_c0, _p_w_c1, _p_c0, _p_c1):
pw0 = 1
pw1 = 1
for i in range(len(_p_w_c0)):
if test_feature[i] == 0: # 不帅、不好、矮、不上进
pw0 *= 1 - _p_w_c0[i] # 不嫁且不帅
pw1 *= 1 - _p_w_c1[i] # 嫁且不帅
else:
pw0 *= _p_w_c0[i] # 不嫁且帅
pw1 *= _p_w_c1[i] # 嫁且帅
pw = _p_c0 * pw0 + _p_c1 * pw1
_p0 = pw0 * _p_c0/pw
_p1 = pw1 * _p_c1/pw
print(_p0, _p1)
if _p1 > _p0:
return 1
else:
return 0
这里的公式应该都很容易理解。根据输入特征向量求该向量w发生的情况下的各个概率,最后是为了求P0和P1,其实只需要计算一个即可,另外不需要算。pw是分母项,如果不需要知道概率,也可以不算。
4、效果测试
dataset, labels = load_data_marry()
p_w_c0, p_w_c1, p_c0, p_c1 = train_bayes_org(dataset, labels)
test_bayes_org(np.array([1, 0, 1, 0]), p_w_c0, p_w_c1, p_c0, p_c1)
所以,[帅、性格不好、高、不上进]的条件下,女孩子不嫁
换为[0, 1, 1,1]嫁的概率为92.8%,说明除了天生的帅,后天因素也是很重要滴。不过身高也很重要,不能矮。实际应该离散更多情况,这里太简单粗暴了点。
(二)垃圾邮件分类问题
问题是这样的,数据中有两个集合,都是txt文本,每个文件表示一封邮件,现在要根据训练数据计算贝叶斯先验概率,输入新的邮件时,识别该邮件是否垃圾邮件。
根据之前的知识,需要将邮件转换为可以计算的特征向量,在女孩嫁不嫁问题中,将给出的帅不帅、性格、身高、是否上进直接量化为01特征,因此可以直接计算。
思路:①每封邮件都由很多单词构成,根据某些单词的是否出现来判断邮件是否垃圾邮件;
②对训练数据的所有邮件单词进行统计,构建词汇表,columns是单词,值是该单词在是垃圾邮件和不是垃圾邮件情况下是否出现或者出现的次数
③利用上面构造的特征向量套入到朴素贝叶斯中进行计算
问题:特征向量的长度可能达到数百甚至上千维度,多的话还会上万,计算P(w|Ci)的时候需要将每个特征向量的条件概率进行相乘,无法计算。下面引入对数log来解决
1、主函数处理
这里没有给出单独的数据导入函数,下面的函数通过调用若干之前写好的函数来实现了垃圾邮件分类的测试
主要函数有
构建词汇表:create_words_list
将文档根据词汇表转换为向量:document2vector_bag
计算先验概率:train_bayes_myself
测试,计算w情况下的类概率,进行分类:test_bayes_self
# 垃圾邮件识别,也就是求P(C|w),知道特征的情况下求是垃圾邮箱的概率
def email_test():
doclist = [] # 邮件列表,一行一封邮件
label_list = [] # 邮件类型类别,垃圾邮件或者正常邮件
# 所有单词列表,邮件不会分行,主要是为了制作词汇表,应该必要性不大,因为求词汇表的函数是遍历每个文档求的并集
fulltext = []
for i in range(1, 26): # 每种邮件有1-25份
_email = text_parser(open("email/spam/%d.txt" % i).read())
doclist.append(_email)
fulltext.extend(_email)
label_list.append(1)
_email = text_parser(open("email/ham/%d.txt" % i, encoding="gb18030", errors="ignore").read())
# print(i)
# print(_email)
doclist.append(_email)
fulltext.extend(_email)
label_list.append(0)
words_list = create_words_list(doclist)
trainingset_index = list(range(50))
test_set_index = []
for _ in range(10):
index = int(np.random.uniform(0, len(trainingset_index)))
test_set_index.append(trainingset_index[index])
del(trainingset_index[index])
train_matrix = []
train_labels = []
for index in trainingset_index:
train_matrix.append(document2vector(words_list, doclist[index]))
train_labels.append(label_list[index])
p_w_c0, p_w_c1, p_c0, p_c1 = train_bayes_myself(np.array(train_matrix), np.array(train_labels))
error_count = 0
for index in test_set_index:
v = document2vector(words_list, doclist[index])
if test_bayes_self(v, p_w_c0, p_w_c1, p_c0, p_c1) != label_list[index]:
error_count += 1
print("the error rate is", float(error_count) / len(test_set_index))
数据集可以在网上下载到,是《机器学习实战》的数据集。spam目录是垃圾邮件,ham目录不是垃圾邮件,读入过程中,还需要对文本进行划分,划分为一个个的单词。得到doclist和label_list后创建词汇表,然后随机将从数据中取10个作为测试数据,其余作为训练数据。然后构建真正的可以输入贝叶斯计算函数的特征向量集,主要使用document2vector_bag或者document2vector函数,将文档转换为特征向量,如果词汇表中的某个单词出现在文档中,则向量中+1,如果出现两次则+2。。。。这里涉及词集模型和词袋模型,如果是词集模型,则不管出现次数是多少,向量中都是1,也就是只标记是否出现
2、词汇表构建
# 创建训练数据集单词集合,去除重复项,也就是词汇表
def create_words_list(dataset):
li = set([])
for _docs in dataset:
li = li | set(_docs)
return list(li)
比较简单
3、将文档转换为特征向量
# 将文档的字转换成向量,返回长度和词汇表长度一样
# 如果文档中的字在词汇表中,则用1表示,否则用0表示
# 这是词集模型,文档中出现某个单词,不管数量多少,特征都是设为1
def document2vector(wordlist, document):
_word_vector = [0]*len(wordlist)
for w in document:
if w in wordlist:
_word_vector[wordlist.index(w)] = 1
else:
print(w + " not in the word list")
return _word_vector
# 这是词袋模型,返回的特征向量会统计文档中每个单词出现的次数
def document2vector_bag(wordlist, document):
_word_vector = [0]*len(wordlist)
for w in document:
if w in wordlist:
_word_vector[wordlist.index(w)] += 1
else:
print(w + " not in the word list")
return _word_vector
注意wordlist是邮件已经转换为单词列表储存了
4、计算先验概率
# 原始的朴素贝叶斯,求对数log
def train_bayes_myself(_train_matrix, _labels):
num_traindocs = len(_train_matrix) # 训练文档的数量
words_list_len = len(_train_matrix[0]) # 特征向量的长度,或者说是词汇表的长度
_p_c1 = sum(_labels) / float(num_traindocs)
_p_wi_c0 = np.zeros(words_list_len) + 0.00001 # 理论上这里应该初始化为0,但是为了让最后计算
_p_wi_c1 = np.zeros(words_list_len) + 0.00001
_c0_num = 0.0 # 该类别中单词的总数,为了计算每个单词出现的概率
_c1_num = 0.0
for i in range(num_traindocs):
if _labels[i] == 1:
_p_wi_c1 += _train_matrix[i] # 统计每个单词出现的次数
_c1_num += 1 # 统计该类别所有单词出现的总数
else:
_p_wi_c0 += _train_matrix[i]
_c0_num += 1
# 取对数,将后面测试时候P(w|C)的计算换为加法,如果是乘法,则会出现数十甚至数百个小数相乘的现象
pos_p_w_c0 = np.log(_p_wi_c0 / _c0_num)
neg_p_w_c0 = np.log(1.0 - _p_wi_c0 / _c0_num)
# print(sum(_p_wi_c0 / _c0_num))
# 这里返回一个与词汇表一样长度的列表,每个元素表示对应单词在该类别中出现的概率,也就是P(wi|c1)
pos_p_w_c1 = np.log(_p_wi_c1 / _c1_num)
neg_p_w_c1 = np.log(1.0 - _p_wi_c1 / _c1_num)
# print(sum(_p_wi_c1 / _c1_num))
return np.array([pos_p_w_c0, neg_p_w_c0]), np.array([pos_p_w_c1, neg_p_w_c1]), 1 - _p_c1, _p_c1
与女孩嫁不嫁问题的函数比较可以发现,这里做了几个修改:
①_p_w_c0和_p_w_c1初始化加上去了一些小数,主要是为了避免后面出现log(0)的情况
②对每个特征的条件概率求对数
③还计算了1.0 - _p_wi_c0 / _c0_num 和1.0 - _p_wi_c1 / _c1_num。因为我们也不确定后面输入的特征是什么样子的,所以这个必须计算,否则后面要计算就很麻烦,因为pos_p_w_c1是log后的。
最后return的时候讲pos和neg合并在一个array中,保证接口不变
5、预测结果
def test_bayes_self(test_feature, _p_w_c0, _p_w_c1, _p_c0, _p_c1):
pw0 = 0
pw1 = 0
for i in range(len(test_feature)):
if test_feature[i] == 0: # 不帅、不好、矮、不上进
pw0 += _p_w_c0[1][i] # 不嫁且不帅
pw1 += _p_w_c1[1][i] # 嫁且不帅
else:
pw0 += _p_w_c0[0][i] # 不嫁且帅
pw1 += _p_w_c1[0][i] # 嫁且帅
# pw项是分母,由于p0和p1的分母相同,因此实际如果只是判断是否而不需要概率,可以不计算pw
pw0_p = np.exp(pw0)
pw1_p = np.exp(pw1)
pw = _p_c0 * pw0_p + _p_c1 * pw1_p
log_pw = np.log(pw)
# 计算分子项
_p0 = pw0 + np.log(_p_c0)
_p1 = pw1 + np.log(_p_c1)
# 计算真实概率
p0 = np.exp(_p0 - log_pw)
p1 = np.exp(_p1 - log_pw)
print(p0, p1, pw)
if _p1 > _p0:
return 1
else:
return 0
这里的注释是女孩嫁与不嫁问题的注释,并不影响理解。通过对数据进行对数变换,将乘法变换为加法,避免下溢出问题。不过如果要计算概率,pw还是会出现0值,这种情况还不知道如何处理。如果不需要计算概率,则不需要计算pw,只需要pw0和pw1,不存在乘法运算。
6、结果
第一个是P0,第二个是P1,第三个是pw。预计总体错误率在3%到8%这样子,效果还不错,主要优点是计算速度快。
第二张图片中出现了一个错误,这个错误出现的次数还是挺多的,问题是pw太小了,小于double型的最小值【double类型 64位 -1.7*10(-308)~1.7*10(308)】,python中已经无法表示这么小的数,于是直接被认为是0了。因此出现了log(0)的警告,这里只是一个警告,numpy中log(0)返回-Inf。现在还不知道怎么解决这个问题,略蛋疼。。。。
如果不需要计算概率,则不存在下溢出问题。