行业知识
机器学习之朴素贝叶斯分类器原理详解、公式推导(手推)、面试问题、简单实例(python实现,sklearn调包)
目录
- 1. 朴素贝叶斯原理
- 1.1. 特性
- 1.2. 思路
- 2. 公式推导
- 3. 简单实例
- 3.1. 数据集
- 脱单数据集2.0
- 脱单数据集1.0
- 西瓜数据集
- 3.2. python实现
- 3.3. sklearn实现
- 3.4. 实验结果
- 4. 几个注意点(面试问题)
- 5. 运行(可直接食用)
1. 朴素贝叶斯原理
1.1. 特性
朴素贝叶斯是一种有监督学习算法,这种算法基于贝叶斯的一个朴素的假设——每对特征和样本数据都是独立同分布的。最终可以推出朴素贝叶斯分类器的判定准则:
h n b ( x ) = a r g m a x c ∈ Υ P ( c ) ∏ i = 1 d P ( x i ∣ c ) h_{nb}(x)=\mathop{arg\ max}\limits_{c\in \varUpsilon}\ P(c) \prod_{i=1}^dP(x_i\ | \ c) hnb(x)=c∈Υarg max P(c)i=1∏dP(xi ∣ c)
我们之前说朴素贝叶斯是一系列算法,这是因为基于不同的条件独立性假设,朴素贝叶斯的分类器会有所不同。
虽然朴素贝叶斯看起来很简单(实际上推起来也很简单),但是它们在现实中的运用还是很不错的,主要是它们仅仅只需要少量训练数据就能估计几个必要的参数,这非常重要。
下面说一下朴素贝叶斯的优缺点:
优点:
- 朴素贝叶斯模型发源于古典数学理论,有稳定的分类效率。
- 对小规模的数据表现很好,能个处理多分类任务,适合增量式训练,尤其是数据量超出内存时,我们可以一批批的去增量训练。
- 对缺失数据不太敏感,算法也比较简单,常用于文本分类。
- 快。和SVM等复杂的方法相比,朴素贝叶斯分类器可以非常快,因为类条件特征分布的独立性意味着每个分布可以独立估计为一维分布,这有助于缓解维度数量过大带来的问题。
缺点:
- 理论上,朴素贝叶斯模型与其他分类方法相比具有最小的误差率。但是实际上并非总是如此,这是因为朴素贝叶斯模型假设属性之间相互独立,这个假设在实际应用中往往是不成立的,在属性个数比较多或者属性之间相关性较大时,分类效果不好。而在属性相关性较小时,朴素贝叶斯性能最为良好。对于这一点,有半朴素贝叶斯之类的算法通过考虑部分关联性适度改进。
- 需要知道先验概率,且先验概率很多时候取决于假设,假设的模型可以有很多种,因此在某些时候会由于假设的先验模型的原因导致预测效果不佳。所以在使用sklearn等包的时候,predict_proba仅仅做参考就好了,一般都不太靠谱。
- 由于我们是通过先验和数据来决定后验的概率从而决定分类,所以分类决策存在一定的错误率。
- 对输入数据的表达形式很敏感。
1.2. 思路
贝叶斯的主要思路就是数数,统计每个条件组合下各种结果的概率,并根据这个概率做出分类。
但是,单纯的贝叶斯在计算机计算的时候会出现计算规模过大的情况,这里就需要 引入朴素条件,化简式子。
最终结果就是在计算时我们只需要统计单个条件下的结果,并据此计算得结果就好了,区别于先自由组合条件在统计结果。
2. 公式推导
朴素贝叶斯分类器的推导离不开概率论,这边顺便复习下概率论最基础的知识:
已知有一种疾病,在城市中的患病率为1%,在检查时有银兴阳性两种结果,在检查结果为阴性时有10%概率患病,在检查结果为阳性时有90%概率患病。
城市中有10000个人参与检查。
我们将这个问题带入到 0-1 评分下的脱单数据集中:
不难发现,直接使用贝叶斯公式时,我们遇到了麻烦。我们判断交往(R)的特征有5个,在计算时条件的结果有 2 5 2^5 25 种组合方式。如果我们有50个特征,那需要计算 2 50 2^{50} 250 种特征组合;如果特征还不是 0-1 变量,而是另一个脱单数据集那样的 0-10 评分数据,则需要计算 1 1 50 11^{50} 1150 种特征组合!甚至在连续数据下,这种方法是不可用的。
这个时候就体现出朴素贝叶斯下那个朴素的假设效果不朴素了。在此假设下,问题转化为:
需要注意的是, 朴素贝叶斯在加入朴素条件后变换式子还能提高算法的可靠性。在样本量不够大的时候很可能出现 P ( A 0 B 0 C 0 D 0 E 0 ∣ R 1 ) P(\ A_0\ B_0\ C_0\ D_0\ E_0\ |\ R_1) P( A0 B0 C0 D0 E0 ∣ R1) 为 0 和 P ( A 0 B 0 C 0 D 0 E 0 ) P(\ A_0\ B_0\ C_0\ D_0\ E_0) P( A0 B0 C0 D0 E0) 为 0 的情况,毕竟总有一类人的条件在相亲市场中不受欢迎,但是因为这是在小样本数据中的结果,并不能真实代表这个概率,直接判定 0 显然不可靠。
在朴素贝叶斯公式中, P ( A 0 ∣ R 1 ) P(\ A_0\ |\ R_1) P( A0 ∣ R1) 等条件累乘的结果显然大于等于 P ( A 0 B 0 C 0 D 0 E 0 ∣ R 1 ) P(\ A_0\ B_0\ C_0\ D_0\ E_0\ |\ R_1) P( A0 B0 C0 D0 E0 ∣ R1) ,这意味着变换后更不容易为0。
换一种说法, P ( A 0 ∣ R 1 ) P(\ A_0\ |\ R_1) P( A0 ∣ R1) 每个条件相较于 P ( A 0 B 0 C 0 D 0 E 0 ∣ R 1 ) P(\ A_0\ B_0\ C_0\ D_0\ E_0\ |\ R_1) P( A0 B0 C0 D0 E0 ∣ R1) 的可能性是更高的,他们每个不等于0的概率都比 P ( A 0 B 0 C 0 D 0 E 0 ∣ R 1 ) P(\ A_0\ B_0\ C_0\ D_0\ E_0\ |\ R_1) P( A0 B0 C0 D0 E0 ∣ R1) 大。
因此,在变换后,朴素贝叶斯公式更加稳定,在小样本数据中发挥更好。
为了模拟计算过程,我们回到 0-1 脱单数据集中:
然后对照着计算。
我们之前提过一嘴,这样的方法只是最简单的数量统计,面对连续型数据时,简单的数量统计势必会使算法陷入维度灾难中,此外简单的统计数量也不能体现样本数据的连续性。因此,我们需要一种方法处理连续型数据,当然如果这种方法对连续型数据有效,那对离散型数据也同样适用。
3. 简单实例
3.1. 数据集
脱单数据集2.0
我们这里根据身边朋友的相亲意向对如下脱单样本打标签,完成如下交往意向数据集。我们将这个数据集称为脱单数据集2.0。
样本 颜值(A) 身材(B) 性格(C) 收入(D) 学历(E) 交往(R) 1 6 4 7 7 8 1 2 10 10 2 4 2 0 3 7 9 6 8 6 1 4 8 2 10 2 6 0 5 6 6 1 9 4 0 6 3 9 5 7 6 0 7 9 7 3 4 4 0 8 8 5 6 7 6 1 9 4 1 6 8 10 0 10 6 6 7 6 7 1 11 7 4 9 5 2 0 12 1 5 9 10 6 0 13 6 7 7 5 8 1 14 6 5 6 8 6 1 15 8 7 7 6 10 1 lovedata_2.csv
颜值(A),身材(B),性格(C),收入(D),学历(E),交往(R)6,4,7,7,8,110,10,2,4,2,07,9,6,8,6,18,2,10,2,6,06,6,1,9,4,03,9,5,7,6,09,7,3,4,4,08,5,6,7,6,14,1,6,8,10,06,6,7,6,7,17,4,9,5,2,01,5,9,10,6,06,7,7,5,8,16,5,6,8,6,18,7,7,6,10,1
脱单数据集1.0
楼上的数据集不是0-1数据,不太适合说明问题,我们这里退化下交往意向数据集,将1-5的评分标记为0,6-10的评分标记为1,完成0-1交往意向数据集。暂时忽略重复数据,毕竟现实中也可能出现样本特性一样的情况。我们将这个数据集称为脱单数据集1.0。
样本 颜值(A) 身材(B) 性格(C) 收入(D) 学历(E) 交往(R) 1 1 0 1 1 1 1 2 1 1 0 0 0 0 3 1 1 1 1 1 1 4 1 0 1 0 1 0 5 1 1 0 1 0 0 6 0 1 0 1 1 0 7 1 1 0 0 0 0 8 1 0 1 1 1 1 9 0 0 1 1 1 0 10 1 1 1 1 1 1 11 1 0 1 0 0 0 12 0 0 1 1 1 0 13 1 1 1 0 1 1 14 1 0 1 1 1 1 15 1 1 1 1 1 1 lovedata_1.csv
颜值(A),身材(B),性格(C),收入(D),学历(E),交往(R)1,0,1,1,1,11,1,0,0,0,01,1,1,1,1,11,0,1,0,1,01,1,0,1,0,00,1,0,1,1,01,1,0,0,0,01,0,1,1,1,10,0,1,1,1,01,1,1,1,1,11,0,1,0,0,00,0,1,1,1,01,1,1,0,1,11,0,1,1,1,11,1,1,1,1,1
西瓜数据集
参考西瓜书上的数据,转化为整型变量。
watermelon.csv
编号,色泽,根蒂,敲声,纹理,脐部,触感,密度,含糖率,好瓜1,0,0,0,0,0,0,0.697,0.46,12,1,0,1,0,0,0,0.774,0.376,13,1,0,0,0,0,0,0.634,0.264,14,0,0,1,0,0,0,0.608,0.318,15,2,0,0,0,0,0,0.556,0.215,16,0,1,0,0,1,1,0.403,0.237,17,1,1,0,1,1,1,0.481,0.149,18,1,1,0,0,1,0,0.437,0.211,19,1,1,1,1,1,0,0.666,0.091,010,0,2,2,0,2,1,0.243,0.267,011,2,2,2,2,2,0,0.245,0.057,012,2,0,0,2,2,1,0.343,0.099,013,0,1,0,1,0,0,0.639,0.161,014,2,1,1,1,0,0,0.657,0.198,015,1,1,0,0,1,1,0.36,0.37,016,2,0,0,2,2,0,0.593,0.042,017,0,0,1,1,1,0,0.719,0.103,0
3.2. python实现
def love1(filename, coiled=False): # 计算p(a|b) def calculate_frq_ab(data, label, coiled): # 存储各个label对应的概率 label_p = {} # unique就series能用 for label_value in label.unique(): # 存储这个label值下,这个特征值组合的概率 feature_p = {} # 筛选label后的数据 data_filter = data.loc[label[label == label_value].index.values] # 每个feature一个个数过去 for feature in data_filter.columns: feature_p[feature] = data_filter.value_counts(feature) / data_filter.shape[0] label_p[label_value] = feature_p return label_p # label = { # label_value1: {feature1: df, feature2: df}, # label_value2: {feature1: df, feature2: df}, # } # 计算p(a),p(b) def calculate_frq_a_b(data, label, coiled): p = {} p[label.name] = label.value_counts() / label.shape[0] for feature in data.columns: p[feature] = data.value_counts(feature) / data.shape[0] return p # 分类器,a为条件,b为label def clf(data, label, p_ab, p_a_b): # label = label.unique() ret = [] for sample in data.T.iteritems(): r = [] for label_i in label: p_b_i = p_a_b[label.name][label_i] p_a_i = 1 p_ab_i = 1 for index, feature_name in enumerate(data.columns): try: p_a_i *= p_a_b[feature_name][sample[1][index]] p_ab_i *= p_ab[label_i][feature_name][sample[1][index]] except KeyError as e: # 出现概率为0的情况 p_a_i = 0 p_ab_i = 1 break r.append(p_b_i * p_a_i) # / p_ab_i r = label[r.index(max(r))] ret.append(r) return ret data_train, data_test, label_train, label_test = get_data(filename) p_ab = calculate_frq_ab(data_train, label_train, coiled) p_a_b = calculate_frq_a_b(data_train, label_train, coiled) print("训练准确率为:") eveluate(clf(data_train, label_train, p_ab, p_a_b), label_train) print("测试准确率为:") eveluate(clf(data_test, label_train, p_ab, p_a_b), label_test)3.3. sklearn实现
# 高斯朴素贝叶斯def sk(files): while 1: for index, name in enumerate(files): print(index, ". ", name) print(len(files), ". 退出") try: choice = int(input()) file = files[choice] except: return 0 data_train, data_test, label_train, label_test = get_data(file) clf = GaussianNB() clf = clf.fit(data_train, label_train) predict = clf.predict(data_test) eveluate(predict, label_test)
3.4. 实验结果
一、脱单数据集1.0
二、脱单数据集2.0
三、西瓜数据集
四、sklearn
4. 几个注意点(面试问题)
朴素贝叶斯有几个注意点,可能会在面试中被提到,还是比较能体现被面试者对这个算法的理解的。当然我们也不一定就是为了面试,搞清楚这些问题对帮助我们理解这个算法还是很有好处的。其中部分答案是博主自己的理解,如果有问题麻烦路过的大佬评论区指正。
-
证明:条件独立性假设不成立时,朴素贝叶斯分类器仍有可能产生最优贝叶斯分类器。
**答:**这篇论文给出了详细的解答《On the Optimality of the Simple Bayesian Classifier under Zero-One Loss》,我们浅浅尝试复述下。
-
实践中使用 h n b ( x ) = a r g m a x c ∈ Υ P ( c ) ∏ i = 1 d P ( x i ∣ c ) h_{nb}(x)=\mathop{arg\ max}\limits_{c\in \varUpsilon}\ P(c) \prod_{i=1}^dP(x_i\ | \ c) hnb(x)=c∈Υarg max P(c)∏i=1dP(xi ∣ c) 决定分类类别时,如果数据维度很高,则会导致 ∏ i = 1 d P ( x i ∣ c ) \prod_{i=1}^dP(x_i\ | \ c) ∏i=1dP(xi ∣ c) 的结果非常接近0,从而导致下溢。试述防止下溢的方法。
答: 这个问题其实西瓜书第153页有提及,方法跟很多的数学式子的思路相似,对原式取对数,将连乘转化为连加。 h n b ( x ) = a r g m a x c ∈ Υ P ( c ) ∏ i = 1 d P ( x i ∣ c ) h_{nb}(x)=\mathop{arg\ max}\limits_{c\in \varUpsilon}\ P(c) \prod_{i=1}^dP(x_i\ | \ c) hnb(x)=c∈Υarg max P(c)∏i=1dP(xi ∣ c) 因此转化为 h n b ( x ) = a r g m a x θ l o g ( P ( c ) ) + ∑ i = 1 d l o g ( P ( x i ∣ c ) ) h_{nb}(x)=\mathop{arg\ max}\limits_{\theta}\ log(P(c)) + \sum_{i=1}^dlog(P(x_i\ | \ c)) hnb(x)=θarg max log(P(c))+∑i=1dlog(P(xi ∣ c)) 。
-
证明:二分类任务种两类数据满足高斯分布且方差相同时,线性判别分析产生贝叶斯最优分类器。
答: 转其他博主答案
5. 运行(可直接食用)
import randomfrom sklearn import preprocessingimport numpy as npfrom sklearn.naive_bayes import GaussianNBimport pandas as pddef get_data(filename, to_one=False, train_size=0.5): data = pd.read_csv(filename).sample(frac=1, random_state=1129) labels = data[data.columns[-1]] data_shuffled = data[data.columns[:-1]] if to_one: data_shuffled = pd.DataFrame(preprocessing.scale(data_shuffled), columns=data.columns[:-1]) # 划分训练集测试集 # 更喜欢pandas的计算,这里就任性点全变成dataframe的了 data_train = data_shuffled.head(int(data_shuffled.shape[0] * train_size)).reset_index(drop=True) label_train = labels.head(int(data_shuffled.shape[0] * train_size)).reset_index(drop=True) # .reset_index(drop=True)可重置index data_test = data_shuffled.tail(int(data_shuffled.shape[0] * (1 - train_size))).reset_index(drop=True) label_test = labels.tail(int(data_shuffled.shape[0] * (1 - train_size))).reset_index(drop=True) return data_train, data_test, label_train, label_testdef eveluate(predict, result): result = result.tolist() correct = 0 for index, i in enumerate(predict): correct += (i == result[index]) print("准确率为:", correct / len(predict) * 100, "%")# love1.0def love1(filename, coiled=False): # 计算p(a|b) def calculate_frq_ab(data, label, coiled): # 存储各个label对应的概率 label_p = {} # unique就series能用 for label_value in label.unique(): # 存储这个label值下,这个特征值组合的概率 feature_p = {} # 筛选label后的数据 data_filter = data.loc[label[label == label_value].index.values] # 每个feature一个个数过去 for feature in data_filter.columns: feature_p[feature] = data_filter.value_counts(feature) / data_filter.shape[0] label_p[label_value] = feature_p return label_p # label = { # label_value1: {feature1: df, feature2: df}, # label_value2: {feature1: df, feature2: df}, # } # 计算p(a),p(b) def calculate_frq_a_b(data, label, coiled): p = {} p[label.name] = label.value_counts() / label.shape[0] for feature in data.columns: p[feature] = data.value_counts(feature) / data.shape[0] return p # 分类器,a为条件,b为label def clf(data, label, p_ab, p_a_b): # label = label.unique() ret = [] for sample in data.T.iteritems(): r = [] for label_i in label: p_b_i = p_a_b[label.name][label_i] p_a_i = 1 p_ab_i = 1 for index, feature_name in enumerate(data.columns): try: p_a_i *= p_a_b[feature_name][sample[1][index]] p_ab_i *= p_ab[label_i][feature_name][sample[1][index]] except KeyError as e: # 出现概率为0的情况 p_a_i = 0 p_ab_i = 1 break r.append(p_b_i * p_a_i) # / p_ab_i r = label[r.index(max(r))] ret.append(r) return ret data_train, data_test, label_train, label_test = get_data(filename) p_ab = calculate_frq_ab(data_train, label_train, coiled) p_a_b = calculate_frq_a_b(data_train, label_train, coiled) print("训练准确率为:") eveluate(clf(data_train, label_train, p_ab, p_a_b), label_train) print("测试准确率为:") eveluate(clf(data_test, label_train, p_ab, p_a_b), label_test)# 高斯朴素贝叶斯def sk(files): while 1: for index, name in enumerate(files): print(index, ". ", name) print(len(files), ". 退出") try: choice = int(input()) file = files[choice] except: return 0 data_train, data_test, label_train, label_test = get_data(file) clf = GaussianNB() clf = clf.fit(data_train, label_train) predict = clf.predict(data_test) eveluate(predict, label_test)if __name__ == '__main__': random.seed(1129) choice = 0 while choice != 5: # # 因为有个label载columns里,所以有data.columns-1个w,偏置b还有一列 # w = np.random.rand(data_shuffled.shape[1]).reshape(-1, 1) print("1. 恋爱1.0数据集\n2. 恋爱2.0数据集\n3. 西瓜数据集(手写)\n4. sklearn高斯朴素贝叶斯\n5. 退出") try: choice = int(input()) except: break if choice == 1: print("恋爱1.0数据集手写求解中...") love1("lovedata_1.csv") elif choice == 2: print("恋爱2.0数据集手写求解中...") love1("lovedata_2.csv") elif choice == 3: print("西瓜数据集求解中...") love1("watermelon.csv") elif choice == 4: print('西瓜数据集sklearn yyds') sk(["lovedata_1.csv", "lovedata_2.csv", "watermelon.csv"]) else: print("退出成功") choice = 6 break参考:
吴恩达《机器学习》
sklearn官网