NLP系列(4)_朴素贝叶斯实战与进阶

最后更新于:2022-04-01 09:52:21

作者: [寒小阳](http://blog.csdn.net/han_xiaoyang?viewmode=contents) && [龙心尘](http://blog.csdn.net/longxinchen_ml?viewmode=contents)  时间:2016年2月。  出处:[http://blog.csdn.net/han_xiaoyang/article/details/50629608](http://blog.csdn.net/han_xiaoyang/article/details/50629608)  [http://blog.csdn.net/longxinchen_ml/article/details/50629613](http://blog.csdn.net/longxinchen_ml/article/details/50629613)  声明:版权所有,转载请联系作者并注明出处 ### 1.引言 前两篇博文介绍了朴素贝叶斯这个名字读着”萌蠢”但实际上简单直接高效的方法,我们也介绍了一下贝叶斯方法的一些细节。按照老规矩,『锄头』给你了,得负责教教怎么用和注意事项,也顺便带大家去除除草对吧。恩,此节作为更贴近实际应用的部分,将介绍贝叶斯方法的优缺点、常见适用场景和可优化点,然后找点实际场景撸点例子练练手,看看工具怎么用。 **PS:本文所有的python代码和ipython notebook已整理至[github相应页面](https://github.com/HanXiaoyang/naive_bayes),欢迎下载和尝试。** ### 2.贝叶斯方法优缺点 既然讲的是朴素贝叶斯,那博主保持和它一致的风格,简单直接高效地丢干货了: * 优点 > 1. 对待预测样本进行预测,**过程简单速度快**(想想邮件分类的问题,预测就是分词后进行概率乘积,在log域直接做加法更快)。 > 2. **对于多分类问题也同样很有效**,复杂度也不会有大程度上升。 > 3. **在分布独立这个假设成立的情况下**,贝叶斯分类器**效果奇好**,会略胜于逻辑回归,同时我们**需要的样本量也更少一点**。 > 4. 对于类别类的输入特征变量,效果非常好。对于数值型变量特征,我们是默认它符合正态分布的。 * 缺点 > 1. 对于测试集中的一个类别变量特征,如果在训练集里没见过,直接算的话概率就是0了,预测功能就失效了。当然,我们前面的文章提过我们有一种技术叫做**『平滑』操作**,可以缓解这个问题,最常见的平滑技术是拉普拉斯估测。 > 2. 那个…咳咳,朴素贝叶斯算出的概率结果,比较大小还凑合,实际物理含义…恩,别太当真。 > 3. 朴素贝叶斯有分布独立的假设前提,而**现实生活中这些predictor很难是完全独立的**。 ### 3.最常见应用场景 * 文本分类/垃圾文本过滤/情感判别:这大概会朴素贝叶斯应用做多的地方了,即使在现在这种分类器层出不穷的年代,在文本分类场景中,朴素贝叶斯依旧坚挺地占据着一席之地。原因嘛,大家知道的,因为多分类很简单,同时在文本数据中,分布独立这个假设基本是成立的。而垃圾文本过滤(比如垃圾邮件识别)和情感分析(微博上的褒贬情绪)用朴素贝叶斯也通常能取得很好的效果。 * 多分类实时预测:这个是不是不能叫做场景?对于文本相关的多分类实时预测,它因为上面提到的优点,被广泛应用,简单又高效。 * 推荐系统:是的,你没听错,是用在推荐系统里!!朴素贝叶斯和协同过滤([Collaborative Filtering](https://en.wikipedia.org/wiki/Collaborative_filtering))是一对好搭档,协同过滤是强相关性,但是泛化能力略弱,朴素贝叶斯和协同过滤一起,能增强推荐的覆盖度和效果。 ### 4.朴素贝叶斯注意点 这个部分的内容,本来应该在最后说的,不过为了把干货集中放在代码示例之前,先搁这儿了,大家也可以看完朴素贝叶斯的各种例子之后,回来再看看这些tips。 * 大家也知道,很多特征是连续数值型的,但是它们不一定服从正态分布,一定要想办法把它们变换调整成满足正态分布!! * 对测试数据中的0频次项,一定要记得平滑,简单一点可以用『拉普拉斯平滑』。 * 先处理处理特征,把相关特征去掉,因为高相关度的2个特征在模型中相当于发挥了2次作用。 * 朴素贝叶斯分类器一般可调参数比较少,比如[scikit-learn中的朴素贝叶斯](http://scikit-learn.org/stable/modules/generated/sklearn.naive_bayes.MultinomialNB.html#sklearn.naive_bayes.MultinomialNB)只有拉普拉斯平滑因子alpha,类别先验概率class_prior和预算数据类别先验fit_prior。模型端可做的事情不如其他模型多,因此我们还是集中精力进行数据的预处理,以及特征的选择吧。 * 那个,一般其他的模型(像logistic regression,SVM等)做完之后,我们都可以尝试一下bagging和boosting等融合增强方法。咳咳,很可惜,对朴素贝叶斯里这些方法都没啥用。原因?原因是这些融合方法本质上是减少过拟合,减少variance的。朴素贝叶斯是没有variance可以减小。 ### 5\. 朴素贝叶斯训练/建模 理论干货和注意点都说完了,来提提怎么快速用朴素贝叶斯训练模型吧。博主一直提倡要站在巨人的肩膀上编程(其实就是懒…同时一直很担忧写出来的代码的健壮性…),咳咳,我们又很自然地把scikit-learn拿过来了。scikit-learn里面有3种不同类型的朴素贝叶斯: * **[高斯分布型](http://scikit-learn.org/stable/modules/naive_bayes.html#gaussian-naive-bayes)**:用于classification问题,假定属性/特征是服从正态分布的。 * **[多项式型](http://scikit-learn.org/stable/modules/naive_bayes.html#multinomial-naive-bayes)**:用于离散值模型里。比如文本分类问题里面我们提到过,我们不光看词语是否在文本中出现,也得看出现的次数。如果总词数为n,出现词数为m的话,说起来有点像掷骰子n次出现m次这个词的场景。 * **[伯努利型](http://scikit-learn.org/stable/modules/naive_bayes.html#bernoulli-naive-bayes)**:这种情况下,就如之前博文里提到的bag of words处理方式一样,最后得到的特征只有0(没出现)和1(出现过)。 根据你的数据集,可以选择scikit-learn中以上任意一种朴素贝叶斯,我们直接举个简单的例子,用高斯分布型朴素贝叶斯建模: ~~~ # 我们直接取iris数据集,这个数据集有名到都不想介绍了... # 其实就是根据花的各种数据特征,判定是什么花 from sklearn import datasets iris = datasets.load_iris() iris.data[:5] #array([[ 5.1, 3.5, 1.4, 0.2], # [ 4.9, 3\. , 1.4, 0.2], # [ 4.7, 3.2, 1.3, 0.2], # [ 4.6, 3.1, 1.5, 0.2], # [ 5\. , 3.6, 1.4, 0.2]]) #我们假定sepal length, sepal width, petal length, petal width 4个量独立且服从高斯分布,用贝叶斯分类器建模 from sklearn.naive_bayes import GaussianNB gnb = GaussianNB() y_pred = gnb.fit(iris.data, iris.target).predict(iris.data) right_num = (iris.target == y_pred).sum() print("Total testing num :%d , naive bayes accuracy :%f" %(iris.data.shape[0], float(right_num)/iris.data.shape[0])) # Total testing num :150 , naive bayes accuracy :0.960000 ~~~ 你看,朴素贝叶斯分类器,简单直接高效,在150个测试样本上,准确率为96%。 ### 6.朴素贝叶斯之文本主题分类器 这是朴素贝叶斯最擅长的应用场景之一,对于不同主题的文本,我们可以用朴素贝叶斯训练一个分类器,然后将其应用在新数据上,预测主题类型。 **6.1 新闻数据分类** 我们使用[搜狐新闻数据](http://www.sogou.com/labs/dl/cs.html)来实验朴素贝叶斯分类器,这部分新闻数据包括it、汽车、财经、健康等9个类别,简洁版数据解压缩后总共16289条新闻,一篇新闻一个txt,我们把数据合并到一个大文件中,一行一篇文章,同时将新闻id(指明新闻的类别)放在文章之前,然后用ICTCLAS(python的话你也可以用[结巴分词](https://github.com/fxsjy/jieba))进行分词,得到以下的文本内容:  ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2431113eed.jpg)  我们随机选取3/5的数据作为训练集,2/5的数据作为测试集,采用互信息对文本特征进行提取,提取出1000个左右的特征词。然后用朴素贝叶斯分类器进行训练,实际训练过程就是对于特征词,统计在训练集和各个类别出现的次数,测试阶段做预测也是扫描一遍测试集,计算相应的概率。因此整个过程非常高效,完整的运行代码如下: ~~~ # 这部分代码基本纯手撸的...没有调用开源库...大家看看就好... #!encoding=utf-8 import sys, math, random, collections def shuffle(inFile): ''' 简单的乱序操作,用于生成训练集和测试集 ''' textLines = [line.strip() for line in open(inFile)] print "正在准备训练和测试数据,请稍后..." random.shuffle(textLines) num = len(textLines) trainText = textLines[:3*num/5] testText = textLines[3*num/5:] print "准备训练和测试数据准备完毕,下一步..." return trainText, testText #总共有9种新闻类别,我们给每个类别一个编号 lables = ['A','B','C','D','E','F','G','H','I'] def lable2id(lable): for i in xrange(len(lables)): if lable == lables[i]: return i raise Exception('Error lable %s' % (lable)) def doc_dict(): ''' 构造和类别数等长的0向量 ''' return [0]*len(lables) def mutual_info(N,Nij,Ni_,N_j): ''' 计算互信息,这里log的底取为2 ''' return Nij * 1.0 / N * math.log(N * (Nij+1)*1.0/(Ni_*N_j))/ math.log(2) def count_for_cates(trainText, featureFile): ''' 遍历文件,统计每个词在每个类别出现的次数,和每类的文档数 并写入结果特征文件 ''' docCount = [0] * len(lables) wordCount = collections.defaultdict(doc_dict()) #扫描文件和计数 for line in trainText: lable,text = line.strip().split(' ',1) index = lable2id(lable[0]) words = text.split(' ') for word in words: wordCount[word][index] += 1 docCount[index] += 1 #计算互信息值 print "计算互信息,提取关键/特征词中,请稍后..." miDict = collections.defaultdict(doc_dict()) N = sum(docCount) for k,vs in wordCount.items(): for i in xrange(len(vs)): N11 = vs[i] N10 = sum(vs) - N11 N01 = docCount[i] - N11 N00 = N - N11 - N10 - N01 mi = mutual_info(N,N11,N10+N11,N01+N11) + mutual_info(N,N10,N10+N11,N00+N10)+ mutual_info(N,N01,N01+N11,N01+N00)+ mutual_info(N,N00,N00+N10,N00+N01) miDict[k][i] = mi fWords = set() for i in xrange(len(docCount)): keyf = lambda x:x[1][i] sortedDict = sorted(miDict.items(),key=keyf,reverse=True) for j in xrange(100): fWords.add(sortedDict[j][0]) out = open(featureFile, 'w') #输出各个类的文档数目 out.write(str(docCount)+"\n") #输出互信息最高的词作为特征词 for fword in fWords: out.write(fword+"\n") print "特征词写入完毕..." out.close() def load_feature_words(featureFile): ''' 从特征文件导入特征词 ''' f = open(featureFile) #各个类的文档数目 docCounts = eval(f.readline()) features = set() #读取特征词 for line in f: features.add(line.strip()) f.close() return docCounts,features def train_bayes(featureFile, textFile, modelFile): ''' 训练贝叶斯模型,实际上计算每个类中特征词的出现次数 ''' print "使用朴素贝叶斯训练中..." docCounts,features = load_feature_words(featureFile) wordCount = collections.defaultdict(doc_dict()) #每类文档特征词出现的次数 tCount = [0]*len(docCounts) for line in open(textFile): lable,text = line.strip().split(' ',1) index = lable2id(lable[0]) words = text.split(' ') for word in words: if word in features: tCount[index] += 1 wordCount[word][index] += 1 outModel = open(modelFile, 'w') #拉普拉斯平滑 print "训练完毕,写入模型..." for k,v in wordCount.items(): scores = [(v[i]+1) * 1.0 / (tCount[i]+len(wordCount)) for i in xrange(len(v))] outModel.write(k+"\t"+scores+"\n") outModel.close() def load_model(modelFile): ''' 从模型文件中导入计算好的贝叶斯模型 ''' print "加载模型中..." f = open(modelFile) scores = {} for line in f: word,counts = line.strip().rsplit('\t',1) scores[word] = eval(counts) f.close() return scores def predict(featureFile, modelFile, testText): ''' 预测文档的类标,标准输入每一行为一个文档 ''' docCounts,features = load_feature_words() docScores = [math.log(count * 1.0 /sum(docCounts)) for count in docCounts] scores = load_model(modelFile) rCount = 0 docCount = 0 print "正在使用测试数据验证模型效果..." for line in testText: lable,text = line.strip().split(' ',1) index = lable2id(lable[0]) words = text.split(' ') preValues = list(docScores) for word in words: if word in features: for i in xrange(len(preValues)): preValues[i]+=math.log(scores[word][i]) m = max(preValues) pIndex = preValues.index(m) if pIndex == index: rCount += 1 #print lable,lables[pIndex],text docCount += 1 print("总共测试文本量: %d , 预测正确的类别量: %d, 朴素贝叶斯分类器准确度:%f" %(rCount,docCount,rCount * 1.0 / docCount)) if __name__=="__main__": if len(sys.argv) != 4: print "Usage: python naive_bayes_text_classifier.py sougou_news.txt feature_file.out model_file.out" sys.exit() inFile = sys.argv[1] featureFile = sys.argv[2] modelFile = sys.argv[3] trainText, testText = shuffle(inFile) count_for_cates(trainText, featureFile) train_bayes(featureFile, trainText, modelFile) predict(featureFile, modelFile, testText) ~~~ **6.2 分类结果** 运行结果如下,在6515条数据上,9个类别的新闻上,有84.1%的准确度:  ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2431127da6.jpg)  ### 7\. Kaggle比赛之『旧金山犯罪分类预测』 **7.1 旧金山犯罪分类预测问题** 没过瘾对吧,确实每次学完一个机器学习算法,不在实际数据上倒腾倒腾,总感觉不那么踏实(想起来高中各种理科科目都要找点题来做的感觉)。好,我们继续去Kaggle扒点场景和数据来练练手。正巧之前[Kaggle](https://www.kaggle.com/)上有一个分类问题,场景和数据也都比较简单,我们拿来用朴素贝叶斯试试水。问题请戳[这里](https://www.kaggle.com/c/sf-crime)。 **7.2 背景介绍** 我们大致介绍一下,说的是『水深火热』的大米国,在旧金山这个地方,一度犯罪率还挺高的,然后很多人都经历过大到暴力案件,小到东西被偷,车被划的事情。当地警方也是努力地去总结和想办法降低犯罪率,一个挑战是在给出犯罪的地点和时间的之后,要第一时间确定这可能是一个什么样的犯罪类型,以确定警力等等。后来干脆一不做二不休,直接把12年内旧金山城内的犯罪报告都丢带Kaggle上,说『大家折腾折腾吧,看看谁能帮忙第一时间预测一下犯罪类型』。犯罪报告里面包括`日期`,`描述`,`星期几`,`所属警区`,`处理结果`,`地址`,`GPS定位`等信息。当然,分类问题有很多分类器可以选择,我们既然刚讲过朴素贝叶斯,刚好就拿来练练手好了。 **7.3 数据一瞥** 数据可以在[Kaggle比赛数据页面](https://www.kaggle.com/c/sf-crime/data)下载到,大家也可以在博主提供的[百度网盘地址](http://pan.baidu.com/s/1o6Wgch8)中下载到。我们依旧用pandas载入数据,先看看数据内容。 ~~~ import pandas as pd import numpy as np #用pandas载入csv训练数据,并解析第一列为日期格式 train=pd.read_csv('/Users/Hanxiaoyang/sf_crime_data/train.csv', parse_dates = ['Dates']) test=pd.read_csv('/Users/Hanxiaoyang/sf_crime_data/test.csv', parse_dates = ['Dates']) train ~~~ 得到如下的结果:  ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e243114913e.png) 我们依次解释一下每一列的含义: * Date: 日期 * Category: 犯罪类型,比如 Larceny/盗窃罪 等. * Descript: 对于犯罪更详细的描述 * DayOfWeek: 星期几 * PdDistrict: 所属警区 * Resolution: 处理结果,比如说『逮捕』『逃了』 * Address: 发生街区位置 * X and Y: GPS坐标 train.csv中的数据时间跨度为12年,包含了90w+的记录。另外,这部分数据,大家从上图上也可以看出来,大部分都是『类别』型,比如犯罪类型,比如星期几。 ### 7.4 特征预处理 上述数据中类别和文本型非常多,我们要进行特征预处理,对于特征预处理的部分,我们在前面的博文[机器学习系列(3)*逻辑回归应用之Kaggle泰坦尼克之灾*](http://blog.csdn.net/han_xiaoyang/article/details/49797143)和[机器学习系列(6)从白富美相亲看特征预处理与选择(下)](http://blog.csdn.net/han_xiaoyang/article/details/50503115)都有较细的介绍。对于类别特征,我们用最常见的因子化操作将其转成数值型,比如我们把犯罪类型用因子化进行encode,也就是说生成如下的向量: ~~~ 星期一/Monday = 1,0,0,0,... 星期二/Tuesday = 0,1,0,0,... 星期三/Wednesday = 0,0,1,0,... ... ~~~ 我们之前也提到过,用pandas的[get_dummies()](http://pandas.pydata.org/pandas-docs/version/0.13.1/generated/pandas.get_dummies.html)可以直接拿到这样的一个二值化的01向量。Pandas里面还有一个很有用的方法[LabelEncoder](http://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.LabelEncoder.html)可以用于对类别编号。对于已有的数据特征,我们打算做下面的粗略变换: * 用LabelEncoder对犯罪类型做编号; * 处理时间,在我看来,也许犯罪发生的时间点(小时)是非常重要的,因此我们会用Pandas把这部分数据抽出来; * 对`街区`,`星期几`,`时间点`用get_dummies()因子化; * 做一些组合特征,比如把上述三个feature拼在一起,再因子化一下; 具体的数据和特征处理如下: ~~~ import pandas as pd import numpy as np from sklearn.cross_validation import train_test_split from sklearn import preprocessing #用LabelEncoder对不同的犯罪类型编号 leCrime = preprocessing.LabelEncoder() crime = leCrime.fit_transform(train.Category) #因子化星期几,街区,小时等特征 days = pd.get_dummies(train.DayOfWeek) district = pd.get_dummies(train.PdDistrict) hour = train.Dates.dt.hour hour = pd.get_dummies(hour) #组合特征 trainData = pd.concat([hour, days, district], axis=1) trainData['crime']=crime #对于测试数据做同样的处理 days = pd.get_dummies(test.DayOfWeek) district = pd.get_dummies(test.PdDistrict) hour = test.Dates.dt.hour hour = pd.get_dummies(hour) testData = pd.concat([hour, days, district], axis=1) trainData ~~~ 然后可以看到特征处理后的数据如下所示:  ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e243117fa62.png) ### 7.5 朴素贝叶斯 VS 逻辑回归 拿到初步的特征了,下一步就可以开始建模了。 因为之前的博客[机器学习系列(1)*逻辑回归初步*](http://blog.csdn.net/han_xiaoyang/article/details/49123419),[机器学习系列(2)从初等数学视角解读逻辑回归](http://blog.csdn.net/han_xiaoyang/article/details/49332321),[机器学习系列(3)_逻辑回归应用之Kaggle泰坦尼克之灾](http://blog.csdn.net/han_xiaoyang/article/details/49797143)中提到过逻辑回归这种分类算法,我们这里打算一并拿来建模,做个比较。  还需要提到的一点是,大家参加Kaggle的比赛,一定要注意最后排名和评定好坏用的标准,比如说在现在这个多分类问题中,Kaggle的评定标准并不是precision,而是[multi-class log_loss](http://scikit-learn.org/stable/modules/generated/sklearn.metrics.log_loss.html),这个值越小,表示最后的效果越好。 我们可以快速地筛出一部分重要的特征,搭建一个baseline系统,再考虑步步优化。比如我们这里简单一点,就只取`星期几`和`街区`作为分类器输入特征,我们用scikit-learn中的`train_test_split`函数拿到训练集和交叉验证集,用朴素贝叶斯和逻辑回归都建立模型,对比一下它们的表现: ~~~ ffrom sklearn.cross_validation import train_test_split from sklearn import preprocessing from sklearn.metrics import log_loss from sklearn.naive_bayes import BernoulliNB from sklearn.linear_model import LogisticRegression import time # 只取星期几和街区作为分类器输入特征 features = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday', 'BAYVIEW', 'CENTRAL', 'INGLESIDE', 'MISSION', 'NORTHERN', 'PARK', 'RICHMOND', 'SOUTHERN', 'TARAVAL', 'TENDERLOIN'] # 分割训练集(3/5)和测试集(2/5) training, validation = train_test_split(trainData, train_size=.60) # 朴素贝叶斯建模,计算log_loss model = BernoulliNB() nbStart = time.time() model.fit(training[features], training['crime']) nbCostTime = time.time() - nbStart predicted = np.array(model.predict_proba(validation[features])) print "朴素贝叶斯建模耗时 %f 秒" %(nbCostTime) print "朴素贝叶斯log损失为 %f" %(log_loss(validation['crime'], predicted)) #逻辑回归建模,计算log_loss model = LogisticRegression(C=.01) lrStart= time.time() model.fit(training[features], training['crime']) lrCostTime = time.time() - lrStart predicted = np.array(model.predict_proba(validation[features])) log_loss(validation['crime'], predicted) print "逻辑回归建模耗时 %f 秒" %(lrCostTime) print "逻辑回归log损失为 %f" %(log_loss(validation['crime'], predicted)) ~~~ 实验的结果如下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e24311ac0df.jpg)  我们可以看到目前的特征和参数设定下,朴素贝叶斯的log损失还低一些,另外我们可以明显看到,朴素贝叶斯建模消耗的时间0.640398秒远小于逻辑回归建模42.856376秒。 考虑到犯罪类型可能和犯罪事件发生的小时时间点相关,我们加入小时时间点特征再次建模,代码和结果如下: ~~~ from sklearn.cross_validation import train_test_split from sklearn import preprocessing from sklearn.metrics import log_loss from sklearn.naive_bayes import BernoulliNB from sklearn.linear_model import LogisticRegression import time # 添加犯罪的小时时间点作为特征 features = ['Friday', 'Monday', 'Saturday', 'Sunday', 'Thursday', 'Tuesday', 'Wednesday', 'BAYVIEW', 'CENTRAL', 'INGLESIDE', 'MISSION', 'NORTHERN', 'PARK', 'RICHMOND', 'SOUTHERN', 'TARAVAL', 'TENDERLOIN'] hourFea = [x for x in range(0,24)] features = features + hourFea # 分割训练集(3/5)和测试集(2/5) training, validation = train_test_split(trainData, train_size=.60) # 朴素贝叶斯建模,计算log_loss model = BernoulliNB() nbStart = time.time() model.fit(training[features], training['crime']) nbCostTime = time.time() - nbStart predicted = np.array(model.predict_proba(validation[features])) print "朴素贝叶斯建模耗时 %f 秒" %(nbCostTime) print "朴素贝叶斯log损失为 %f" %(log_loss(validation['crime'], predicted)) #逻辑回归建模,计算log_loss model = LogisticRegression(C=.01) lrStart= time.time() model.fit(training[features], training['crime']) lrCostTime = time.time() - lrStart predicted = np.array(model.predict_proba(validation[features])) log_loss(validation['crime'], predicted) print "逻辑回归建模耗时 %f 秒" %(lrCostTime) print "逻辑回归log损失为 %f" %(log_loss(validation['crime'], predicted)) ~~~ ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e24311c9bfd.jpg)  可以看到在这三个类别特征下,朴素贝叶斯相对于逻辑回归,依旧有一定的优势(log损失更小),同时训练时间很短,这意味着模型虽然简单,但是效果依旧强大。顺便提一下,朴素贝叶斯1.13s训练出来的模型,预测的效果在Kaggle排行榜上已经能进入Top 35%了,如果进行一些优化,比如特征处理、特征组合等,结果会进一步提高。 ### 8\. Kaggle比赛之影评与观影者情感判定 博主想了想,既然朴素贝叶斯最常见的应用场景就那么几个,干脆我们都一并覆盖得了。咳咳,对,还有一个非常重要的应用场景是情感分析(尤其是褒贬判定),于是我又上Kaggle溜达了一圈,扒下来一个类似场景的比赛。比赛的名字叫做[**当词袋/Bag of words 遇上 爆米花/Bags of Popcorn**](https://www.kaggle.com/c/word2vec-nlp-tutorial/),地址为[https://www.kaggle.com/c/word2vec-nlp-tutorial/](https://www.kaggle.com/c/word2vec-nlp-tutorial/),有兴趣的同学可以上去瞄一眼。 **8.1 背景介绍** 这个比赛的背景大概是:国外有一个类似[豆瓣电影](http://movie.douban.com/)一样的[IMDB](http://www.imdb.com/),也是你看完电影,可以上去打个分,吐个槽的地方。然后大家就在想,有这么多数据,总得折腾点什么吧,于是乎,第一个想到的就是,赞的喷的内容都有了,咱们就来分分类,看看能不能根据内容分布褒贬。PS:很多同学表示,分个褒贬有毛线难的,咳咳,计算机比较笨,另外,语言这种东西,真心是博大精深的,我们随手从豆瓣上抓了几条《功夫熊猫3》影评下来,表示有些虽然我是能看懂,但是不处理直接给计算机看,它应该是一副『什么鬼』的表情。。。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e24311dfa0c.jpg)  多说一句,Kaggle原文引导里是用word2vec的方式将词转为词向量,后再用deep learning的方式做的。深度学习好归好,但是毕竟耗时耗力耗资源,我们用最最naive的朴素贝叶斯撸一把,说不定效果也能不错,不试试谁知道呢。另外,朴素贝叶斯建模真心速度快,很多场景下,快速建模快速迭代优化正是我们需要的嘛。 **8.2 数据一瞥** 言归正传,回到Kaggle中这个问题上来,先瞄一眼数据。Kaggle数据页面地址为[https://www.kaggle.com/c/word2vec-nlp-tutorial/data](https://www.kaggle.com/c/word2vec-nlp-tutorial/data),大家也可以到博主的[百度网盘](http://pan.baidu.com/s/1c1jX8nI)中下载。数据包如下图所示: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2431209b18.jpg)  其中包含有情绪标签的训练数据labeledTrainData,没有情绪标签的训练数据unlabeledTrainData,以及测试数据testData。labeledTrainData包括id,sentiment和review3个部分,分别指代用户id,情感标签,评论内容。 解压缩labeledTrainData后用vim打开,内容如下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e243122425c.png) 下面我们读取数据并做一些基本的预处理(比如说把评论部分的html标签去掉等等): ~~~ import re #正则表达式 from bs4 import BeautifulSoup #html标签处理 import pandas as pd def review_to_wordlist(review): ''' 把IMDB的评论转成词序列 ''' # 去掉HTML标签,拿到内容 review_text = BeautifulSoup(review).get_text() # 用正则表达式取出符合规范的部分 review_text = re.sub("[^a-zA-Z]"," ", review_text) # 小写化所有的词,并转成词list words = review_text.lower().split() # 返回words return words # 使用pandas读入训练和测试csv文件 train = pd.read_csv('/Users/Hanxiaoyang/IMDB_sentiment_analysis_data/labeledTrainData.tsv', header=0, delimiter="\t", quoting=3) test = pd.read_csv('/Users/Hanxiaoyang/IMDB_sentiment_analysis_data/testData.tsv', header=0, delimiter="\t", quoting=3 ) # 取出情感标签,positive/褒 或者 negative/贬 y_train = train['sentiment'] # 将训练和测试数据都转成词list train_data = [] for i in xrange(0,len(train['review'])): train_data.append(" ".join(review_to_wordlist(train['review'][i]))) test_data = [] for i in xrange(0,len(test['review'])): test_data.append(" ".join(review_to_wordlist(test['review'][i]))) ~~~ 我们在ipython notebook里面看一眼,发现数据已经格式化了,如下:  ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e243127e684.png) **8.3 特征处理** 紧接着又到了头疼的部分了,数据有了,我们得想办法从数据里面拿到有区分度的特征。比如说Kaggle该问题的引导页提供的word2vec就是一种文本到数值域的特征抽取方式,比如说我们在第6小节提到的用互信息提取关键字也是提取特征的一种。比如说在这里,我们打算用在文本检索系统中非常有效的一种特征:TF-IDF(term frequency-interdocument frequency)向量。每一个电影评论最后转化成一个TF-IDF向量。对了,对于TF-IDF不熟悉的同学们,我们稍加解释一下,TF-IDF是一种统计方法,用以评估一字词(或者n-gram)对于一个文件集或一个语料库中的其中一份文件的重要程度。字词的重要性随着它在文件中出现的次数成正比增加,但同时会随着它在语料库中出现的频率成反比下降。这是一个能很有效地判定对评论褒贬影响大的词或短语的方法。 那个…博主打算继续偷懒,把scikit-learn中TFIDF向量化方法直接拿来用,想详细了解的同学可以戳[sklearn TFIDF向量类](http://scikit-learn.org/stable/modules/generated/sklearn.feature_extraction.text.TfidfVectorizer.html)。对了,再多说几句我的处理细节,停用词被我掐掉了,同时我在单词的级别上又拓展到2元语言模型(对这个不了解的同学别着急,后续的博客介绍马上就来),恩,你可以再加3元4元语言模型…博主主要是单机内存不够了,先就2元上,凑活用吧… ~~~ from sklearn.feature_extraction.text import TfidfVectorizer as TFIV # 初始化TFIV对象,去停用词,加2元语言模型 tfv = TFIV(min_df=3, max_features=None, strip_accents='unicode', analyzer='word',token_pattern=r'\w{1,}', ngram_range=(1, 2), use_idf=1,smooth_idf=1,sublinear_tf=1, stop_words = 'english') # 合并训练和测试集以便进行TFIDF向量化操作 X_all = train_data + test_data len_train = len(train_data) # 这一步有点慢,去喝杯茶刷会儿微博知乎歇会儿... tfv.fit(X_all) X_all = tfv.transform(X_all) # 恢复成训练集和测试集部分 X = X_all[:len_train] X_test = X_all[len_train:] ~~~ **8.4 朴素贝叶斯 vs 逻辑回归** 特征现在我们拿到手了,该建模了,好吧,博主折腾劲又上来了,那个…咳咳…我们还是朴素贝叶斯和逻辑回归都建个分类器吧,然后也可以比较比较,恩。  『talk is cheap, I’ll show you the code』,直接放码过来了哈。 ~~~ # 多项式朴素贝叶斯 from sklearn.naive_bayes import MultinomialNB as MNB model_NB = MNB() model_NB.fit(X, y_train) #特征数据直接灌进来 MNB(alpha=1.0, class_prior=None, fit_prior=True) from sklearn.cross_validation import cross_val_score import numpy as np print "多项式贝叶斯分类器20折交叉验证得分: ", np.mean(cross_val_score(model_NB, X, y_train, cv=20, scoring='roc_auc')) # 多项式贝叶斯分类器20折交叉验证得分: 0.950837239 ~~~ ~~~ # 折腾一下逻辑回归,恩 from sklearn.linear_model import LogisticRegression as LR from sklearn.grid_search import GridSearchCV # 设定grid search的参数 grid_values = {'C':[30]} # 设定打分为roc_auc model_LR = GridSearchCV(LR(penalty = 'L2', dual = True, random_state = 0), grid_values, scoring = 'roc_auc', cv = 20) # 数据灌进来 model_LR.fit(X,y_train) # 20折交叉验证,开始漫长的等待... GridSearchCV(cv=20, estimator=LogisticRegression(C=1.0, class_weight=None, dual=True, fit_intercept=True, intercept_scaling=1, penalty='L2', random_state=0, tol=0.0001), fit_params={}, iid=True, loss_func=None, n_jobs=1, param_grid={'C': [30]}, pre_dispatch='2*n_jobs', refit=True, score_func=None, scoring='roc_auc', verbose=0) #输出结果 print model_LR.grid_scores_ ~~~ 最后逻辑回归的结果是`[mean: 0.96459, std: 0.00489, params: {'C': 30}]` 咳咳…看似逻辑回归在这个问题中,TF-IDF特征下表现要稍强一些…不过同学们自己跑一下就知道,这2个模型的训练时长真心不在一个数量级,逻辑回归在数据量大的情况下,要等到睡着…另外,要提到的一点是,因为我这里只用了2元语言模型(2-gram),加到3-gram和4-gram,最后两者的结果还会提高,而且朴素贝叶斯说不定会提升更快一点,内存够的同学们自己动手试试吧^_^ ### 9\. 总结 本文为朴素贝叶斯的实践和进阶篇,先丢了点干货,总结了贝叶斯方法的优缺点,应用场景,注意点和一般建模方法。紧接着对它最常见的应用场景,抓了几个例子,又来了一遍手把手系列,不管是对于文本主题分类、多分类问题(犯罪类型分类) 还是 情感分析/分类,朴素贝叶斯都是一个简单直接高效的方法。尤其是在和逻辑回归的对比中可以看出,在这些问题中,朴素贝叶斯能取得和逻辑回归相近的成绩,但是训练速度远快于逻辑回归,真正的直接和高效。
';

NLP系列(3)_用朴素贝叶斯进行文本分类(下)

最后更新于:2022-04-01 09:52:19

作者: [龙心尘](http://blog.csdn.net/longxinchen_ml?viewmode=contents) && [寒小阳](http://blog.csdn.net/han_xiaoyang?viewmode=contents)  时间:2016年2月。  出处:[http://blog.csdn.net/longxinchen_ml/article/details/50629110](http://blog.csdn.net/longxinchen_ml/article/details/50629110)  [http://blog.csdn.net/han_xiaoyang/article/details/50629587](http://blog.csdn.net/han_xiaoyang/article/details/50629587)  声明:版权所有,转载请联系作者并注明出处 ### 1\. 引言 上一篇文章我们主要从理论上梳理了朴素贝叶斯方法进行文本分类的基本思路。这篇文章我们主要从实践上探讨一些应用过程中的tricks,并进一步分析贝叶斯方法,最后以情绪褒贬分析和拼写纠错为例展示这种简单强大的方法在自然语言处理问题上的具体应用。 ### 2\. 为什么不直接匹配关键词来识别垃圾邮件? 看了上一篇文章的一些同学可能会问:“何必费这么大劲算那么多词的概率?直接看邮件中有没有‘代开发票’、‘转售发票’之类的关键词不就得了?如果关键词比较多就认为是垃圾邮件呗。” 咳咳,其实关键词匹配的方法如果有效的话真不必用朴素贝叶斯。毕竟这种方法简单嘛,就是一个字符串匹配。从历史来看,之前没有贝叶斯方法的时候主要也是用关键词匹配。但是这种方法准确率太低。我们在工作项目中也尝试过用关键词匹配的方法去进行文本分类,发现大量误报。感觉就像扔到垃圾箱的邮件99%都是正常的!这样的效果不忍直视。而加一个朴素贝叶斯方法就可能把误报率拉低近一个数量级,体验好得不要不要的。 另一个原因是词语会随着时间不断变化。发垃圾邮件的人也不傻,当他们发现自己的邮件被大量屏蔽之后,也会考虑采用新的方式,如变换文字、词语、句式、颜色等方式来绕过反垃圾邮件系统。比如对于垃圾邮件“我司可办理正规发票,17%增值税发票点数优惠”,他们采用火星文:“涐司岢办理㊣規髮票,17%增値稅髮票嚸數優蕙”,那么字符串匹配的方法又要重新找出这些火星文,一个一个找出关键词,重新写一些匹配规则。更可怕的是,这些规则可能相互之间的耦合关系异常复杂,要把它们梳理清楚又是大一个数量级的工作量。等这些规则失效了又要手动更新新的规则……无穷无尽猫鼠游戏最终会把猫给累死。 而朴素贝叶斯方法却显示出无比的优势。因为它是基于统计方法的,只要训练样本中有更新的垃圾邮件的新词语,哪怕它们是火星文,都能自动地把哪些更敏感的词语(如“髮”、“㊣”等)给凸显出来,并根据统计意义上的敏感性给他们分配适当的权重,这样就不需要什么人工了,非常省事。你只需要时不时地拿一些最新的样本扔到训练集中,重新训练一次即可。 小补充一下,对于火星文、同音字等替代语言,一般的分词技术可能会分得不准,最终可能只把一个一个字给分出来,成为“分字”。当然也可以用过n-gram之类的语言模型(后续博客马上提到,尽请关注),拿到最常见短语。对于英文等天生自带空格来间隔单词的语言,分词则不是什么问题,使用朴素贝叶斯方法将会更加顺畅。 ### 3.工程上的一些tricks 应用朴素贝叶斯方法的过程中,一些tricks能显著帮助工程解决问题。我们毕竟经验有限,无法将它们全都罗列出来,只能就所知的一点点经验与大家分享,欢迎批评指正。 **3.1 trick1:取对数** 我们上一篇文章用来识别垃圾邮件的方法是比较以下两个概率的大小(字母S表示“垃圾邮件”,字母H表示“正常邮件”): > C=P(“我”|S)P(“司”|S)P(“可”|S)P(“办理”|S)P(“正规发票”|S)  > ×P(“保真”|S)P(“增值税”|S)P(“发票”|S)P(“点数”|S)P(“优惠”|S)P(“垃圾邮件”)  > C¯¯¯=P(“我”|H)P(“司”|H)P(“可”|H)P(“办理”|H)P(“正规发票”|H)  > ×P(“保真”|H)P(“增值税”|H)P(“发票”|H)P(“点数”|H)P(“优惠”|H)P(“正常邮件”) 但这里进行了很多乘法运算,计算的时间开销比较大。尤其是对于篇幅比较长的邮件,几万个数相乘起来还是非常花时间的。如果能把这些乘法变成加法则方便得多。刚好数学中的对数函数log就可以实现这样的功能。两边同时取对数(本文统一取底数为2),则上面的公式变为: > logC=logP(“我”|S)+logP(“司”|S)+logP(“可”|S)+logP(“办理”|S)+logP(“正规发票”|S)  > +logP(“保真”|S)+logP(“增值税”|S)+logP(“发票”|S)+logP(“点数”|S)+logP(“优惠”|S)+logP(“垃圾邮件”)  > logC¯¯¯=logP(“我”|H)+logP(“司”|H)+logP(“可”|H)+logP(“办理”|H)+logP(“正规发票”|H)  > +logP(“保真”|H)+logP(“增值税”|H)+logP(“发票”|H)+logP(“点数”|H)+logP(“优惠”|H)+logP(“正常邮件”) 有同学可能要叫了:“做对数运算岂不会也很花时间?”的确如此,但是可以在训练阶段直接计算 logP ,然后把他们存在一张大的hash表里。在判断的时候直接提取hash表中已经计算好的对数概率,然后相加即可。这样使得判断所需要的计算时间被转移到了训练阶段,实时运行的时候速度就比之前快得多,这可不止几个数量级的提升。 **3.2 trick2:转换为权重** 对于二分类,我们还可以继续提高判断的速度。既然要比较logC 和logC¯¯¯ 的大小,那就可以直接将上下两式相减,并继续化简: > logCC¯=logP(“我”|S)P(“我”|H)+logP(“司”|S)P(“司”|H)+logP(“可”|S)P(“可”|H)+logP(“办理”|S)P(“办理”|H)+logP(“正规发票”|S)P(“正规发票”|H)  > +logP(“保真”|S)P(“保真”|H)+logP(“增值税”|S)P(“增值税”|H)+logP(“发票”|S)P(“发票”|H)+logP(“点数”|S)P(“点数”|H)+logP(“优惠”|S)P(“优惠”|H)+logP(“正常邮件”|S)P(“正常邮件”) logCC¯ 如果大于0则属于垃圾邮件。我们可以把其中每一项作为其对应词语的权重,比如logP(“发票”|S)P(“发票”|H) 就可以作为词语“发票”的权重,权重越大就越说明“发票”更可能是与“垃圾邮件”相关的特征。这样可以根据权重的大小来评估和筛选显著的特征,比如关键词。而这些权重值可以直接提前计算好而存在hash表中 。判断的时候直接将权重求和即可。 关键词hash表的样子如下,左列是权重,右列是其对应的词语,权重越高的说明越“关键”:  ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e24310bd45a.jpg)  **3.3 trick3:选取topk的关键词** 前文说过可以通过提前选取关键词来提高判断的速度。有一种方法可以省略提前选取关键词的步骤,就是直接选取一段文本中权重最高的K个词语,将其权重进行加和。比如Paul Graham 在《黑客与画家》中是选取邮件中权重最高的15个词语计算的。 通过权重hash表可知,如果是所有词语的权重,则权重有正有负。如果只选择权重最高的K个词语,则它们的权重基本都是正的。所以就不能像之前那样判断logCC¯ 是否大于0来区分邮件了。而这需要依靠经验选定一个正数的阈值(门槛值),依据logCC¯ 与该门槛值的大小来识别垃圾邮件。 如下图所示,蓝色点代表垃圾邮件,绿色点代表正常邮件,横坐标为计算出来的logCC¯ 值,中间的红线代表阈值。  ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e24310d24e9.jpg)  **3.4 trick4:分割样本** 选取topk个词语的方法对于篇幅变动不大的邮件样本比较有效。但是对篇幅过大或者过小的邮件则会有判断误差。 比如这个垃圾邮件的例子:(“我”,“司”,“可”,“办理”,“正规发票”,“保真”,“增值税”,“发票”,“点数”,“优惠”)。分词出了10个词语,其中有“正规发票”、“发票”2个关键词。关键词的密度还是蛮大的,应该算是敏感邮件。但因为采用最高15个词语的权重求和,并且相应的阈值是基于15个词的情况有效,可能算出来的结果还小于之前的阈值,这就造成漏判了。 类似的,如果一封税务主题的邮件有1000个词语,其中只有“正规发票”、“发票”、“避税方法”3个权重比较大的词语,它们只是在正文表述中顺带提到的内容。关键词的密度被较长的篇幅稀释了,应该算是正常邮件。但是却被阈值判断成敏感邮件,造成误判了。 这两种情况都说明topk关键词的方法需要考虑篇幅的影响。这里有许多种处理方式,它们的基本思想都是选取词语的个数及对应的阈值要与篇幅的大小成正比,本文只介绍其中一种方方法:  - 对于长篇幅邮件,按一定的大小,比如每500字,将其分割成小的文本段落,再对小文本段落采用topk关键词的方法。只要其中有一个小文本段落超过阈值就判断整封邮件是垃圾邮件。  - 对于超短篇幅邮件,比如50字,可以按篇幅与标准比较篇幅的比例来选取topk,以确定应该匹配关键词语的个数。比如选取 50500×15≈2 个词语进行匹配,相应的阈值可以是之前阈值的 215 。以此来判断则更合理。 **3.5 trick5:位置权重** 到目前为止,我们对词语权重求和的过程都没有考虑邮件篇章结构的因素。比如“正规发票”如果出现在标题中应该比它出现在正文中对判断整个邮件的影响更大;而出现在段首句中又比其出现在段落正文中对判断整个邮件的影响更大。所以可以根据词语出现的位置,对其权重再乘以一个放大系数,以扩大其对整封邮件的影响,提高识别准确度。 比如一封邮件其标题是“正规发票”(假设标题的放大系数为2),段首句是“发票”,“点数”,“优惠”(假设段首的放大系数为1.5),剩下的句子是(“我”,“司”,“可”,“办理”,“保真”)。则计算logCC¯ 时的公式就可以调整为: > logCC¯=2×logP(“正规发票”|S)P(“正规发票”|H)+1.5×logP(“发票”|S)P(“发票”|H)+1.5×logP(“点数”|S)P(“点数”|H)+1.5×logP(“优惠”|S)P(“优惠”|H)  > +logP(“我”|S)P(“我”|H)+logP(“司”|S)P(“司”|H)+logP(“可”|S)P(“可”|H)+logP(“办理”|S)P(“办理”|H)+logP(“保真”|S)P(“保真”|H)+logP(“正常邮件”|S)P(“正常邮件”) **3.6 trick6:蜜罐** 我们通过辛辛苦苦的统计与计算,好不容易得到了不同词语的权重。然而这并不是一劳永逸的。我们我们之前交代过,词语及其权重会随着时间不断变化,需要时不时地用最新的样本来训练以更新词语及其权重。 而搜集最新垃圾邮件有一个技巧,就是随便注册一些邮箱,然后将它们公布在各大论坛上。接下来就坐等一个月,到时候收到的邮件就绝大部分都是垃圾邮件了(好奸诈)。再找一些正常的邮件,基本就能够训练了。这些用于自动搜集垃圾邮件的邮箱叫做“蜜罐”。“蜜罐”是网络安全领域常用的手段,因其原理类似诱捕昆虫的装有蜜的罐子而得名。比如杀毒软件公司会利用蜜罐来监视或获得计算机网络中的病毒样本、攻击行为等。 ### 4\. 贝叶斯方法的思维方式 讲了这么多tricks,但这些手段都是建立在贝叶斯方法基础之上的。因此有必要探讨一下贝叶斯方法的思维方式,以便更好地应用这种方法解决实际问题。 **4.1 逆概问题** 我们重新看一眼贝叶斯公式: > P(Y|X)=P(X|Y)P(Y)P(X) 先不考虑先验概率P(Y)与P(X),观察两个后验概率P(Y|X)与P(X|Y),可见贝叶斯公式能够揭示两个相反方向的条件概率之间的转换关系。 从贝叶斯公式的发现历史来看,其就是为了处理所谓“逆概”问题而诞生的。比如P(Y|X) 不能通过直接观测来得到结果,而P(X|Y) 却容易通过直接观测得到结果,就可以通过贝叶斯公式从间接地观测对象去推断不可直接观测的对象的情况。 好吧,我们说人话。基于邮件的文本内容判断其属于垃圾邮件的概率不好求(不可通过直接观测、统计得到),但是基于已经搜集好的垃圾邮件样本,去统计(直接观测)其文本内部各个词语的概率却非常方便。这就可以用贝叶斯方法。 引申一步,基于样本特征去判断其所属标签的概率不好求,但是基于已经搜集好的打上标签的样本(有监督),却可以直接统计属于同一标签的样本内部各个特征的概率分布。因此贝叶斯方法的理论视角适用于一切分类问题的求解。 **4.2 处理多分类问题** 前面我们一直在探讨二分类(判断题)问题,现在可以引申到多分类(单选题)问题了。 还是用邮件分类的例子,这是现在不只要判断垃圾邮件,还要将正常邮件细分为私人邮件、工作邮件。现在有这3类邮件各1万封作为样本。需要训练出一个贝叶斯分类器。这里依次用Y1,Y2,Y3表示这三类邮件,用X表示被判断的邮件。套用贝叶斯公式有: > P(Y1|X)=P(X|Y1)P(Y1)P(X)  > P(Y2|X)=P(X|Y2)P(Y2)P(X)  > P(Y3|X)=P(X|Y3)P(Y3)P(X) 通过比较3个概率值的大小即可得到X所属的分类。发现三个式子的分母P(X) 一样,比较大小时可以忽略不计,于是就可以用下面这一个式子表达上面3式: > P(Yi|X)∝P(X|Yi)P(Yi);i=1,2,3 其中 ∝ 表示“正比于”。而P(X|Yi) 则有个特别高逼格的名字叫做“似然函数”。我们上大学的时候也被这个名字搞得晕晕乎乎的,其实它也是个概率,直接理解成“P(Yi|X) 的逆反条件概率”就方便了。 这里只是以垃圾邮件3分类问题举了个例子,对于任意多分类的问题都可以用这样的思路去理解。比如新闻分类、情感喜怒哀乐分类等等。 **4.3 先验概率的问题** 在垃圾邮件的例子中,先验概率都相等,P(Y1)=P(Y2)=P(Y3)=10000/30000=1/3,所以上面是式子又可以进一步化简: > P(Yi|X)∝P(X|Yi);i=1,2,3 只需比较右边式子(也就是“似然函数”)的大小就可以了。这种方法就是传说中的最大似然法:不考虑先验概率而直接比较似然函数。 关于选出最佳分类Yi是否要考虑先验概率P(Yi)的问题,曾经在频率学派和贝叶斯学派之间产生了激烈的教派冲突。统计学家(频率学派)说:我们让数据自己说话。言下之意就是要摒弃先验概率。而贝叶斯学派支持者则说:数据会有各种各样的偏差,而一个靠谱的先验概率则可以对这些随机噪音做到健壮。对此有兴趣的同学可以找更多资料进行了解,本文在此不做更多的引申,只基于垃圾邮件识别的例子进行探讨。 比如我们在采集垃圾邮件样本的时候,不小心delete掉了一半的数据,就剩下5000封邮件。则计算出来的先验概率为: > P(Y1)=5000/25000=1/5,  > P(Y2)=P(Y3)=10000/25000=2/5 如果还用贝叶斯方法,就要在似然函数后面乘上先验概率。比如之前用最大似然法算出Y1 垃圾邮件的概率大,但是因为P(Y1)特别小,用贝叶斯方法得出的结果是Y2 私人邮件的概率大。那相信哪个呢?其实,我们删掉了部分带标签的样本,从计算结果看P(Y1),P(Y2),P(Y3)的概率分布变化了,但实际上这三个类别的真实分布应该是一个客观的状态,不应该因为我们的计算方法而发生变化。所以是我们计算出来的先验概率失真,应该放弃这样计算出来的先验概率,而用最大似然法。但即便我们不删掉一半垃圾邮件,这三类邮件的分布就真的是1:1:1那样平均吗?那也未必。我们只是按1:1:1这样的方式进行了抽样而已,真正在邮箱里收到的这三类邮件的分布可能并不是这样。也就是说,在我们对于先验概率一无所知时,只能假设每种猜测的先验概率是均等的(其实这也是人类经验的结果),这个时候就只有用最大似然了。在现实运用过程中如果发现最大似然法有偏差,可以考虑对不同的似然函数设定一些系数或者阈值,使其接近真实情况。 但是,如果我们有足够的自信,训练集中这三类的样本分布的确很接近真实的情况,这时就应该用贝叶斯方法。难怪前面的贝叶斯学派强调的是“靠谱的先验概率”。所以说贝叶斯学派的适用范围更广,关键要先验概率靠谱,而频率学派有效的前提也是他们的先验概率同样是经验统计的结果。 ### 5\. (朴素)贝叶斯方法的常见应用 说了这么多理论的问题,咱们就可以探讨一下(朴素)贝叶斯方法在自然语言处理中的一些常见应用了。以下只是从原理上进行探讨,对于具体的技术细节顾及不多。 **5.1 褒贬分析** 一个比较常见的应用场景是情感褒贬分析。比如你要统计微博上人们对一个新上映电影的褒贬程度评价:好片还是烂片。但是一条一条地看微博是根本看不过来,只能用自动化的方法。我们可以有一个很粗略的思路: * 首先是用爬虫将微博上提到这个电影名字的微博全都抓取下来,比如有10万条。 * 然后用训练好的朴素贝叶斯分类器分别判断这些微博对电影是好评还是差评。 * 最后统计出这些好评的影评占所有样本中的比例,就能形成微博网友对这个电影综合评价的大致估计。 接下来的核心问题就是训练出一个靠谱的分类器。首先需要有打好标签的文本。这个好找,豆瓣影评上就有大量网友对之前电影的评价,并且对电影进行1星到5星的评价。我们可以认为3星以上的评论都是好评,3星以下的评论都是差评。这样就分别得到了好评差评两类的语料样本。剩下就可以用朴素贝叶斯方法进行训练了。基本思路如下: * 训练与测试样本:豆瓣影评的网友评论,用爬虫抓取下100万条。 * 标签:3星以上的是好评,3星以下的是差评。 * 特征:豆瓣评论分词后的词语。一个简单的方法是只选择其中的形容词,网上有大量的情绪词库可以为我们所用。 * 然后再用常规的朴素贝叶斯方法进行训练。 但是由于自然语言的特点,在提取特征的过程当中,有一些tricks需要注意: * 对否定句进行特别的处理。比如这句话“我不是很喜欢部电影,因为它让我开心不起来。”其中两个形容词“喜欢”、“开心”都是褒义词,但是因为句子的否定句,所以整体是贬义的。有一种比较简单粗暴的处理方式,就是“对否定词(“不”、“非”、“没”等)与句尾标点之间的所有形容词都采用其否定形式”。则这句话中提取出来的形容词就应该是“不喜欢”和“不开心”。 * 一般说来,最相关的情感词在一些文本片段中仅仅出现一次,词频模型起得作用有限,甚至是负作用,则使用伯努利模型代替多项式模型。这种情况在微博这样的小篇幅文本中似乎不太明显,但是在博客、空间、论坛之类允许长篇幅文本出现的平台中需要注意。 * 其实,副词对情感的评价有一定影响。“不很喜欢”与“很不喜欢”的程度就有很大差异。但如果是朴素贝叶斯方法的话比较难处理这样的情况。我们可以考虑用语言模型或者加入词性标注的信息进行综合判断。这些内容我们将在之后的文章进行探讨。 当然经过以上的处理,情感分析还是会有一部分误判。这里涉及到许多问题,都是情感分析的难点: * 情绪表达的含蓄微妙:“导演你出来,我保证不打死你。”你让机器怎么判断是褒还是贬? * 转折性表达:“我非常喜欢这些大牌演员,非常崇拜这个导演,非常赞赏这个剧本,非常欣赏他们的预告片,我甚至为了这部影片整整期待了一年,最后进了电影院发现这是个噩梦。” 五个褒义的形容词、副词对一个不那么贬义的词。机器自然判断成褒义,但这句话是妥妥的贬义。 **5.2 拼写纠错** 拼写纠错本质上也是一个分类问题。但按照错误类型不同,又分为两种情况: * 非词错误(Non-word Errors):指那些拼写错误后的词本身就不合法,如将“wifi”写成“wify”; * 真词错误(Real-word Errors):指那些拼写错误后的词仍然是合法的情况,如将“wifi”写成“wife”。 真词错误复杂一些,我们将在接下来的文章中进行探讨。而对于非词错误,就可以直接采用贝叶斯方法,其基本思路如下: * 标签:通过计算错误词语的最小编辑距离(之前咱们提到过的),获取最相似的候选词,每个候选词作为一个分类。 * 特征:拼写错误的词本身。因为它就一个特征,所以没有什么条件独立性假设、朴素贝叶斯啥的。它就是纯而又纯的贝叶斯方法。 * 判别公式: > P(候选词i|错误词)∝P(错误词|候选词i)P(候选词i);i=1,2,3,... * 训练样本1:该场景下的正常用词语料库,用于计算P(候选词i)。 > P(候选词i)=候选词出现的次数所有词出现的次数 * 训练样本2:该场景下错误词与正确词对应关系的语料库,用于计算P(错误词|候选词i) > P(错误词|候选词i)=候选词被拼写成该“错误词”的次数候选词出现的次数 由于自然语言的特点,有一些tricks需要注意: * 据统计,80%的拼写错误编辑距离为1,几乎所有的拼写错误编辑距离小于等于2。我们只选择编辑距离为1或2的词作为候选词,这样就可以减少大量不必要的计算。 * 由于我们只选择编辑距离为1或2的词,其差别只是一两个字母级别差别。因此计算似然函数的时候,可以只统计字母层面的编辑错误,这样搜集的样本更多,更满足大数定律,也更简单。对于编辑距离为1的似然函数计算公式可以进化为: > > > P(错误词|候选词i)=⎧⎩⎨⎪⎪⎪⎪⎪⎪⎪⎪⎪⎪⎪⎪⎪⎪⎪⎪字母“xy”被拼写成“y”的次数字母“xy”出现的次数,字母“x”被拼写成“xy”的次数字母“x”出现的次数,字母“x”被拼写成“y”的次数字母“x”出现的次数,字母“xy”被拼写成“yx的次数字母“xy”出现的次数, * 键盘上临近的按键更容易拼写错误,据此可以对上面这个条件概率进行加权。  ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e24310effe8.jpg)  ### 6\. 小结 从这两篇文章大家基本可以看出,工程应用不同于学术理论,有许多tricks需要考虑,而理论本质就是翻来倒去折腾贝叶斯公式,都快玩出花来了。但是如果只用朴素贝叶斯,很多情况还是无法应付,需要我们在贝叶斯公式上再折腾出一些花样。详细内容,请听下回分解。
';

NLP系列(2)_用朴素贝叶斯进行文本分类(上)

最后更新于:2022-04-01 09:52:16

作者:[寒小阳](http://blog.csdn.net/han_xiaoyang?viewmode=contents) && [龙心尘](http://blog.csdn.net/longxinchen_ml?viewmode=contents)  时间:2016年1月。  出处:[http://blog.csdn.net/longxinchen_ml/article/details/50597149](http://blog.csdn.net/longxinchen_ml/article/details/50597149)  [http://blog.csdn.net/han_xiaoyang/article/details/50616559](http://blog.csdn.net/han_xiaoyang/article/details/50616559)  声明:版权所有,转载请联系作者并注明出处 ### 1\. 引言 贝叶斯方法是一个历史悠久,有着坚实的理论基础的方法,同时处理很多问题时直接而又高效,很多高级自然语言处理模型也可以从它演化而来。因此,学习贝叶斯方法,是研究自然语言处理问题的一个非常好的切入口。 ### 2\. 贝叶斯公式 贝叶斯公式就一行: > P(Y|X)=P(X|Y)P(Y)P(X) 而它其实是由以下的联合概率公式推导出来: > P(Y,X)=P(Y|X)P(X)=P(X|Y)P(Y) 其中P(Y)叫做先验概率,P(Y|X)叫做后验概率,P(Y,X)叫做联合概率。 额,恩,没了,贝叶斯最核心的公式就这么些。 ### 3\. 用机器学习的视角理解贝叶斯公式 在机器学习的视角下,我们把X理解成“具有某特征”,把Y理解成“类别标签”(一般机器学习问题中都是`X=>特征`, `Y=>结果`对吧)。在最简单的二分类问题(`是`与`否`判定)下,我们将Y理解成“属于某类”的标签。于是贝叶斯公式就变形成了下面的样子: P(“属于某类”|“具有某特征”)=P(“具有某特征”|“属于某类”)P(“属于某类”)P(“具有某特征”) 我们尝试更口(shuo)语(ren)化(hua)的方式解释一下上述公式: P(“属于某类”|“具有某特征”)=在已知某样本“具有某特征”的条件下,该样本“属于某类”的概率。所以叫做『后验概率』。  P(“具有某特征”|“属于某类”)=在已知某样本“属于某类”的条件下,该样本“具有某特征”的概率。  P(“属于某类”)=(在未知某样本具有该“具有某特征”的条件下,)该样本“属于某类”的概率。所以叫做『先验概率』。  P(“具有某特征”)=(在未知某样本“属于某类”的条件下,)该样本“具有某特征”的概率。 而我们二分类问题的最终目的就是要判断P(“属于某类”|“具有某特征”)是否大于1/2就够了。贝叶斯方法把计算“具有某特征的条件下属于某类”的概率转换成需要计算“属于某类的条件下具有某特征”的概率,而后者获取方法就简单多了,我们只需要找到一些包含已知特征标签的样本,即可进行训练。而样本的类别标签都是明确的,所以贝叶斯方法在机器学习里属于有监督学习方法。 这里再补充一下,一般『先验概率』、『后验概率』是相对出现的,比如P(Y)与P(Y|X)是关于Y的先验概率与后验概率,P(X)与P(X|Y)是关于X的先验概率与后验概率。 ### 4\. 垃圾邮件识别 举个例子好啦,我们现在要对邮件进行分类,识别垃圾邮件和普通邮件,如果我们选择使用朴素贝叶斯分类器,那目标就是判断P(“垃圾邮件”|“具有某特征”)是否大于1/2。现在假设我们有垃圾邮件和正常邮件各1万封作为训练集。需要判断以下这个邮件是否属于垃圾邮件: > “我司可办理正规发票(保真)17%增值税发票点数优惠!” 也就是判断概率P(“垃圾邮件”|“我司可办理正规发票(保真)17%增值税发票点数优惠!”)是否大于1/2。 咳咳,有木有发现,转换成的这个概率,计算的方法:就是写个计数器,然后+1 +1 +1统计出所有垃圾邮件和正常邮件中出现这句话的次数啊!!!好,具体点说: P(“垃圾邮件”|“我司可办理正规发票(保真)17%增值税发票点数优惠!”)  =垃圾邮件中出现这句话的次数垃圾邮件中出现这句话的次数+正常邮件中出现这句话的次数 ### 5\. 分词 然后同学们开始朝我扔烂白菜和臭鸡蛋,“骗纸!!误人子弟!!你以为发垃圾邮件的人智商都停留在20世纪吗!!你以为它们发邮件像抄作业一样不改内容吗!!哪来那么多相同的句子!!”。 咳咳,表闹,确实,在我们这样的样本容量下,『完全击中』的句子很少甚至没有(无法满足大数定律,),算出来的概率会很失真。一方面找到庞大的训练集是一件非常困难的事情,另一方面其实对于任何的训练集,我们都可以构造出一个从未在训练集中出现的句子作为垃圾邮件(真心的,之前看过朴素贝叶斯分类分错的邮件,我觉得大中华同胞创(zao)新(jia)的能力简直令人惊(fa)呀(zhi))。 一个很悲哀但是很现实的结论:  训练集是有限的,而句子的可能性则是无限的。所以覆盖所有句子可能性的训练集是不存在的。 所以解决方法是?  对啦!句子的可能性无限,但是词语就那么些!!汉语常用字2500个,常用词语也就56000个(你终于明白小学语文老师的用心良苦了)。按人们的经验理解,两句话意思相近并不强求非得每个字、词语都一样。比如“我司可办理正规发票,17%增值税发票点数优惠!”,这句话就比之前那句话少了“(保真)”这个词,但是意思基本一样。如果把这些情况也考虑进来,那样本数量就会增加,这就方便我们计算了。 于是,我们可以不拿句子作为特征,而是拿句子里面的词语(组合)作为特征去考虑。比如“正规发票”可以作为一个单独的词语,“增值税”也可以作为一个单独的词语等等。 句子“我司可办理正规发票,17%增值税发票点数优惠!”就可以变成(“我”,“司”,“可”,“办理”,“正规发票”,“保真”,“增值税”,“发票”,“点数”,“优惠”))。 于是你接触到了中文NLP中,最最最重要的技术之一:分词!!!也就是把一整句话拆分成更细粒度的词语来进行表示。咳咳,另外,分词之后去除标点符号、数字甚至无关成分(停用词)是特征预处理中的一项技术。 中文分词是一个专门的技术领域(我不会告诉你某搜索引擎厂码砖工有专门做分词的!!!),我们将在下一篇文章探讨,这里先将其作为一个已知情况进行处理。具体细节请见下回分晓 我们观察(“我”,“司”,“可”,“办理”,“正规发票”,“保真”,“增值税”,“发票”,“点数”,“优惠”),这可以理解成一个向量:向量的每一维度都表示着该特征词在文本中的特定位置存在。这种将特征拆分成更小的单元,依据这些更灵活、更细粒度的特征进行判断的思维方式,在自然语言处理与机器学习中都是非常常见又有效的。 因此贝叶斯公式就变成了: P(“垃圾邮件”|(“我”,“司”,“可”,“办理”,“正规发票”,“保真”,“增值税”,“发票”,“点数”,“优惠”))  =P((“我”,“司”,“可”,“办理”,“正规发票”,“保真”,“增值税”,“发票”,“点数”,“优惠”)|"垃圾邮件")P(“垃圾邮件”)P((“我”,“司”,“可”,“办理”,“正规发票”,“保真”,“增值税”,“发票”,“点数”,“优惠”)) P(“正常邮件”|(“我”,“司”,“可”,“办理”,“正规发票”,“保真”,“增值税”,“发票”,“点数”,“优惠”))  =P((“我”,“司”,“可”,“办理”,“正规发票”,“保真”,“增值税”,“发票”,“点数”,“优惠”)|"正常邮件")P(“正常邮件”)P((“我”,“司”,“可”,“办理”,“正规发票”,“保真”,“增值税”,“发票”,“点数”,“优惠”)) ### 6\. 条件独立假设 有些同学说…好像…似乎…经过上面折腾,概率看起来更复杂了-_-||  那…那我们简化一下… 概率P((“我”,“司”,“可”,“办理”,“正规发票”,“保真”,“增值税”,“发票”,“点数”,“优惠”)|"垃圾邮件")依旧不够好求,我们引进一个很朴素的近似。为了让公式显得更加紧凑,我们令字母S表示“垃圾邮件”,令字母H表示“正常邮件”。近似公式如下: P((“我”,“司”,“可”,“办理”,“正规发票”,“保真”,“增值税”,“发票”,“点数”,“优惠”)|S)  =P(“我”|S)×P(“司”|S)×P(“可”|S)×P(“办理”|S)×P(“正规发票”|S)  ×P(“保真”|S)×P(“增值税”|S)×P(“发票”|S)×P(“点数”|S)×P(“优惠”|S) 这就是传说中的条件独立假设。基于“正常邮件”的条件独立假设的式子与上式类似,此处省去。接着,将条件独立假设代入上面两个相反事件的贝叶斯公式。 于是我们就只需要比较以下两个式子的大小: C=P(“我”|S)P(“司”|S)P(“可”|S)P(“办理”|S)P(“正规发票”|S)  ×P(“保真”|S)P(“增值税”|S)P(“发票”|S)P(“点数”|S)P(“优惠”|S)P(“垃圾邮件”)  C¯¯¯=P(“我”|H)P(“司”|H)P(“可”|H)P(“办理”|H)P(“正规发票”|H)  ×P(“保真”|H)P(“增值税”|H)P(“发票”|H)P(“点数”|H)P(“优惠”|H)P(“正常邮件”) 厉(wo)害(cao)!酱紫处理后**式子中的每一项都特别好求**!只需要**分别统计各类邮件中该关键词出现的概率**就可以了!!!比如: P(“发票”|S)=垃圾邮件中所有“发票”的次数垃圾邮件中所有词语的次数 统计次数非常方便,而且样本数量足够大,算出来的概率比较接近真实。于是垃圾邮件识别的问题就可解了。 ### 7\. 朴素贝叶斯(Naive Bayes),“Naive”在何处? 加上条件独立假设的贝叶斯方法就是朴素贝叶斯方法(Naive Bayes)。Naive的发音是“乃一污”,意思是“朴素的”、“幼稚的”、“蠢蠢的”。咳咳,也就是说,大神们取名说该方法是一种比较萌蠢的方法,为啥? 将句子(“我”,“司”,“可”,“办理”,“正规发票”) 中的 (“我”,“司”)与(“正规发票”)调换一下顺序,就变成了一个新的句子(“正规发票”,“可”,“办理”, “我”, “司”)。新句子与旧句子的意思完全不同。但由于乘法交换律,朴素贝叶斯方法中算出来二者的条件概率完全一样!计算过程如下: P((“我”,“司”,“可”,“办理”,“正规发票”)|S)  =P(“我”|S)P(“司”|S)P(“可”|S)P(“办理”|S)P(“正规发票”|S)  =P(“正规发票”|S)P(“可”|S)P(“办理”|S)P(“我”|S)P(“司”|S)  =P((“正规发票”,“可”,“办理”,“我”,“司”)|S) 也就是说,在朴素贝叶斯眼里,“我司可办理正规发票”与“正规发票可办理我司”完全相同。朴素贝叶斯失去了词语之间的顺序信息。这就相当于把所有的词汇扔进到一个袋子里随便搅和,贝叶斯都认为它们一样。因此这种情况也称作词袋子模型(bag of words)。  ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e243108ec25.jpg)  词袋子模型与人们的日常经验完全不同。比如,在条件独立假设的情况下,“武松打死了老虎”与“老虎打死了武松”被它认作一个意思了。恩,朴素贝叶斯就是这么单纯和直接,对比于其他分类器,好像是显得有那么点萌蠢。 ### 8\. 简单高效,吊丝逆袭 虽然说朴素贝叶斯方法萌蠢萌蠢的,但实践证明在垃圾邮件识别的应用还令人诧异地好。Paul Graham先生自己简单做了一个朴素贝叶斯分类器,“1000封垃圾邮件能够被过滤掉995封,并且没有一个误判”。(Paul Graham《黑客与画家》) 那个…效果为啥好呢? “有人对此提出了一个理论解释,并且建立了什么时候朴素贝叶斯的效果能够等价于非朴素贝叶斯的充要条件,这个解释的核心就是:有些独立假设在各个分类之间的分布都是均匀的所以对于似然的相对大小不产生影响;即便不是如此,也有很大的可能性各个独立假设所产生的消极影响或积极影响互相抵消,最终导致结果受到的影响不大。具体的数学公式请参考[这篇 paper](http://www.cs.unb.ca/profs/hzhang/publications/FLAIRS04ZhangH.pdf)。”(刘未鹏《:平凡而又神奇的贝叶斯方法》) 恩,这个分类器中最简单直接看似萌蠢的小盆友『朴素贝叶斯』,实际上却是简单、实用、且强大的。 ### 9\. 处理重复词语的三种方式 我们之前的垃圾邮件向量(“我”,“司”,“可”,“办理”,“正规发票”,“保真”,“增值税”,“发票”,“点数”,“优惠”),其中每个词都不重复。而这在现实中其实很少见。因为如果文本长度增加,或者分词方法改变,必然会有许多词重复出现,因此需要对这种情况进行进一步探讨。比如以下这段邮件: “代开发票。增值税发票,正规发票。”  分词后为向量:  (“代开”,“发票”,“增值税”,“发票”,“正规”,“发票”) 其中“发票”重复了三次。 ### 9.1 多项式模型: 如果我们考虑重复词语的情况,也就是说,**重复的词语我们视为其出现多次**,直接按条件独立假设的方式推导,则有 P((“代开”,“发票”,“增值税”,“发票”,“正规”,“发票”)|S)  =P(“代开””|S)P(“发票”|S)P(“增值税”|S)P(“发票”|S)P(“正规”|S)P(“发票”|S)  =P(“代开””|S)P3(“发票”|S)P(“增值税”|S)P(“正规”|S)  注意这一项:P3(“发票”|S)。 在统计计算P(“发票”|S)时,每个被统计的垃圾邮件样本中重复的词语也统计多次。 > P(“发票”|S)=每封垃圾邮件中出现“发票”的次数的总和每封垃圾邮件中所有词出现次数(计算重复次数)的总和 你看这个多次出现的结果,出现在概率的指数/次方上,因此这样的模型叫作多项式模型。 ### 9.2 伯努利模型 另一种更加简化的方法是**将重复的词语都视为其只出现1次**, P((“代开”,“发票”,“增值税”,“发票”,“正规”,“发票”)|S)  =P(“发票”|S)P(“代开””|S)P(“增值税”|S)P(“正规”|S) 统计计算P(“词语”|S)时也是如此。 P(“发票”|S)=出现“发票”的垃圾邮件的封数每封垃圾邮件中所有词出现次数(出现了只计算一次)的总和 这样的模型叫作伯努利模型(又称为二项独立模型)。这种方式更加简化与方便。当然它丢失了词频的信息,因此效果可能会差一些。 ### 9.3 混合模型 第三种方式是在计算句子概率时,不考虑重复词语出现的次数,但是在统计计算词语的概率P(“词语”|S)时,却考虑重复词语的出现次数,这样的模型可以叫作混合模型。 我们通过下图展示三种模型的关系。  ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e243109ebc3.jpg)  实践中采用哪种模型,关键看具体的业务场景。笔者的简单经验是,对于垃圾邮件识别,混合模型更好些。 ### 10\. 去除停用词与选择关键词 我们继续观察(“我”,“司”,“可”,“办理”,“正规发票”,“保真”,“增值税”,“发票”,“点数”,“优惠”) 这句话。其实,像“我”、“可”之类词其实非常中性,无论其是否出现在垃圾邮件中都无法帮助判断的有用信息。所以可以直接不考虑这些典型的词。这些无助于我们分类的词语叫作“停用词”(Stop Words)。这样可以减少我们训练模型、判断分类的时间。 于是之前的句子就变成了(“司”,“办理”,“正规发票”,“保真”,“增值税”,“发票”,“点数”,“优惠”) 。 我们进一步分析。以人类的经验,其实“正规发票”、“发票”这类的词如果出现的话,邮件作为垃圾邮件的概率非常大,可以作为我们区分垃圾邮件的“关键词”。而像“司”、“办理”、“优惠”这类的词则有点鸡肋,可能有助于分类,但又不那么强烈。如果想省事做个简单的分类器的话,则可以直接采用“关键词”进行统计与判断,剩下的词就可以先不管了。于是之前的垃圾邮件句子就变成了(“正规发票”,“发票”) 。这样就更加减少了我们训练模型、判断分类的时间,速度非常快。 “停用词”和“关键词”一般都可以提前靠人工经验指定。不同的“停用词”和“关键词”训练出来的分类器的效果也会有些差异。那么有没有量化的指标来评估不同词语的区分能力?在我们之前的文章[《机器学习系列(6)_从白富美相亲看特征选择与预处理(下)》](http://blog.csdn.net/longxinchen_ml/article/details/50493845)其实就提供了一种评价方法,大家可以参考。此处就不赘述了。 ### 11\. 浅谈平滑技术 我们来说个问题(中文NLP里问题超级多,哭瞎T_T),比如在计算以下独立条件假设的概率: > P((“我”,“司”,“可”,“办理”,“正规发票”)|S)  > =P(“我”|S)P(“司”|S)P(“可”|S)P(“办理”|S)P(“正规发票”|S) 我们扫描一下训练集,发现“正规发票”这个词从出现过!!!,于是P(“正规发票”|S)=0…问题严重了,整个概率都变成0了!!!朴素贝叶斯方法面对一堆0,很凄惨地失效了…更残酷的是这种情况其实很常见,因为哪怕训练集再大,也可能有覆盖不到的词语。本质上还是样本数量太少,不满足大数定律,计算出来的概率失真。为了解决这样的问题,一种分析思路就是直接不考虑这样的词语,但这种方法就相当于默认给P(“正规发票”|S)赋值为1。其实效果不太好,大量的统计信息给浪费掉了。我们进一步分析,既然可以默认赋值为1,为什么不能默认赋值为一个很小的数?这就是平滑技术的基本思路,依旧保持着一贯的作风,`朴实/土`但是`直接而有效`。 对于伯努利模型,P(“正规发票”|S)的一种平滑算法是: > P(“正规发票”|S)=出现“正规发票”的垃圾邮件的封数+1每封垃圾邮件中所有词出现次数(出现了只计算一次)的总和+2 对于多项式模型,P(“正规发票”| S)的一种平滑算法是: > P(“发票”|S)=每封垃圾邮件中出现“发票”的次数的总和+1每封垃圾邮件中所有词出现次数(计算重复次数)的总和+被统计的词表的词语数量 说起来,平滑技术的种类其实非常多,有兴趣的话回头我们专门拉个专题讲讲好了。这里只提一点,就是所有的平滑技术都是给未出现在训练集中的词语一个估计的概率,而相应地调低其他已经出现的词语的概率。 平滑技术是因为数据集太小而产生的现实需求。如果数据集足够大,平滑技术对结果的影响将会变小。 ### 12\. 小结 我们找了个最简单常见的例子:垃圾邮件识别,说明了一下朴素贝叶斯进行文本分类的思路过程。基本思路是先区分好训练集与测试集,对文本集合进行分词、去除标点符号等特征预处理的操作,然后使用条件独立假设,将原概率转换成词概率乘积,再进行后续的处理。 > 贝叶斯公式 + 条件独立假设 = 朴素贝叶斯方法 基于对重复词语在训练阶段与判断(测试)阶段的三种不同处理方式,我们相应的有伯努利模型、多项式模型和混合模型。在训练阶段,如果样本集合太小导致某些词语并未出现,我们可以采用平滑技术对其概率给一个估计值。而且并不是所有的词语都需要统计,我们可以按相应的“停用词”和“关键词”对模型进行进一步简化,提高训练和判断速度。 因为公式比较多,为了防止看到公式就狗带的情况,我们尽量用口(shuo)语(ren)化(hua)的方式表达公式,不严谨之处还望见谅,有纰漏之处欢迎大家指出。
';

机器学习系列(7)_机器学习路线图(附资料)

最后更新于:2022-04-01 09:52:14

作者:[寒小阳](http://blog.csdn.net/han_xiaoyang?viewmode=contents)&&[龙心尘](http://blog.csdn.net/longxinchen_ml?viewmode=contents)  时间:2016年2月。  出处:[http://blog.csdn.net/han_xiaoyang/article/details/50759472](http://blog.csdn.net/han_xiaoyang/article/details/50759472)  [http://blog.csdn.net/longxinchen_ml/article/details/50749614](http://blog.csdn.net/longxinchen_ml/article/details/50749614)  声明:版权所有,转载请联系作者并注明出处 ### 1\. 引言 也许你和这个叫『机器学习』的家伙一点也不熟,但是你举起iphone手机拍照的时候,早已习惯它帮你框出人脸;也自然而然点开今日头条推给你的新闻;也习惯逛淘宝点了找相似之后货比三家;亦或喜闻乐见微软的年龄识别网站结果刷爆朋友圈。恩,这些功能的核心算法就是机器学习领域的内容。 套用一下大神们对机器学习的定义,机器学习研究的是计算机怎样模拟人类的学习行为,以获取新的知识或技能,并重新组织已有的知识结构使之不断改善自身。简单一点说,就是计算机从数据中学习出规律和模式,以应用在新数据上做预测的任务。近年来互联网数据大爆炸,数据的丰富度和覆盖面远远超出人工可以观察和总结的范畴,而机器学习的算法能指引计算机在海量数据中,挖掘出有用的价值,也使得无数学习者为之着迷。 但是越说越觉得机器学习有距离感,云里雾里高深莫测,我们不是专家,但说起算有一些从业经验,做过一些项目在实际数据上应用机器学习。这一篇就我们的经验和各位同仁的分享,总结一些对于初学者入门有帮助的方法和对进阶有用的资料。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e24312b8ae6.png) ### 2\. 机器学习关注问题 并非所有的问题都适合用机器学习解决(很多逻辑清晰的问题用规则能很高效和准确地处理),也没有一个机器学习算法可以通用于所有问题。咱们先来了解了解,机器学习,到底关心和解决什么样的问题。 从功能的角度分类,机器学习在一定量级的数据上,可以解决下列问题: **1.分类问题** * 根据数据样本上抽取出的特征,判定其属于有限个类别中的哪一个。比如:  * 垃圾邮件识别(结果类别:1、垃圾邮件 2、正常邮件) * 文本情感褒贬分析(结果类别:1、褒 2、贬) * 图像内容识别识别(结果类别:1、喵星人 2、汪星人 3、人类 4、草泥马 5、都不是)。 **2.回归问题** * 根据数据样本上抽取出的特征,预测一个连续值的结果。比如:  * 星爷《美人鱼》票房 * 大帝都2个月后的房价 * 隔壁熊孩子一天来你家几次,宠幸你多少玩具 **3.聚类问题** * 根据数据样本上抽取出的特征,让样本抱抱团(相近/相关的样本在一团内)。比如:  * google的新闻分类 * 用户群体划分 我们再把上述常见问题划到机器学习最典型的2个分类上。 * 分类与回归问题需要用已知结果的数据做训练,属于“监督学习” * 聚类的问题不需要已知标签,属于“非监督学习”。 如果在IT行业(尤其是互联网)里溜达一圈,你会发现机器学习在以下热点问题中有广泛应用: **1.计算机视觉** * 典型的应用包括:人脸识别、车牌识别、扫描文字识别、图片内容识别、图片搜索等等。 **2.自然语言处理** * 典型的应用包括:搜索引擎智能匹配、文本内容理解、文本情绪判断,语音识别、输入法、机器翻译等等。 **3.社会网络分析** * 典型的应用包括:用户画像、网络关联分析、欺诈作弊发现、热点发现等等。 **4.推荐** * 典型的应用包括:虾米音乐的“歌曲推荐”,某宝的“猜你喜欢”等等。 ### 3\. 入门方法与学习路径 OK,不废话,直接切重点丢干货了。看似学习难度大,曲线陡的机器学习,对大多数入门者也有一个比较通用的学习路径,也有一些优秀的入门资料可以降低大家的学习门槛,同时激发我们的学习乐趣。 简单说来,大概的一个学习路径如下:  ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e24313024dc.jpg)  简单说一点,之所以最左边写了『数学基础』『典型机器学习算法』『编程基础』三个并行的部分,是因为机器学习是一个将数学/算法理论和工程实践紧密结合的领域,需要扎实的理论基础帮助引导数据分析与模型调优,同时也需要精湛的工程开发能力去高效化地训练和部署模型和服务。 需要多说一句的是,在互联网领域从事机器学习的人,有2类背景的人比较多,其中一部分(很大一部分)是程序员出身,这类同学工程经验相对会多一些,另一部分是学数学统计领域的同学,这部分同学理论基础相对扎实一些。因此对比上图,2类同学入门机器学习,所欠缺和需要加强的部分是不一样的。 下面就上述图中的部分,展开来分别扯几句: **3.1 数学基础** 有无数激情满满大步向前,誓要在机器学习领域有一番作为的同学,在看到公式的一刻突然就觉得自己狗带了。是啊,机器学习之所以相对于其他开发工作,更有门槛的根本原因就是数学。每一个算法,要在训练集上最大程度拟合同时又保证泛化能力,需要不断分析结果和数据,调优参数,这需要我们对数据分布和模型底层的数学原理有一定的理解。所幸的是如果只是想合理应用机器学习,而不是做相关方向高精尖的research,需要的数学知识啃一啃还是基本能理解下来的。至于更高深的部分,恩,博主非常愿意承认自己是『数学渣』。 基本所有常见机器学习算法需要的数学基础,都集中在微积分、线性代数和概率与统计当中。下面我们先过一过知识重点,文章的后部分会介绍一些帮助学习和巩固这些知识的资料。 #### 3.1.1 微积分 * 微分的计算及其几何、物理含义,是机器学习中大多数算法的求解过程的核心。比如算法中运用到梯度下降法、牛顿法等。如果对其几何意义有充分的理解,就能理解“梯度下降是用平面来逼近局部,牛顿法是用曲面逼近局部”,能够更好地理解运用这样的方法。 * 凸优化和条件最优化的相关知识在算法中的应用随处可见,如果能有系统的学习将使得你对算法的认识达到一个新高度。 #### 3.1.2 线性代数 * 大多数机器学习的算法要应用起来,依赖于高效的计算,这种场景下,程序员GG们习惯的多层for循环通常就行不通了,而大多数的循环操作可转化成矩阵之间的乘法运算,这就和线性代数有莫大的关系了 * 向量的内积运算更是随处可见。 * 矩阵乘法与分解在机器学习的主成分分析(PCA)和奇异值分解(SVD)等部分呈现刷屏状地出现。 #### 3.1.3 概率与统计 从广义来说,机器学习在做的很多事情,和统计层面数据分析和发掘隐藏的模式,是非常类似的。 * 极大似然思想、贝叶斯模型是理论基础,朴素贝叶斯(Naïve Bayes )、语言模型(N-gram)、隐马尔科夫(HMM)、隐变量混合概率模型是他们的高级形态。 * 常见分布如高斯分布是混合高斯模型(GMM)等的基础。 **3.2 典型算法** 绝大多数问题用典型机器学习的算法都能解决,粗略地列举一下这些方法如下: 1. 处理分类问题的常用算法包括:逻辑回归(工业界最常用),支持向量机,随机森林,朴素贝叶斯(NLP中常用),深度神经网络(视频、图片、语音等多媒体数据中使用)。 2. 处理回归问题的常用算法包括:线性回归,普通最小二乘回归(Ordinary Least Squares Regression),逐步回归(Stepwise Regression),多元自适应回归样条(Multivariate Adaptive Regression Splines) 3. 处理聚类问题的常用算法包括:K均值(K-means),基于密度聚类,LDA等等。 4. 降维的常用算法包括:主成分分析(PCA),奇异值分解(SVD)等。 5. 推荐系统的常用算法:协同过滤算法 6. 模型融合(model ensemble)和提升(boosting)的算法包括:bagging,adaboost,GBDT,GBRT 7. 其他很重要的算法包括:EM算法等等。 我们多插一句,机器学习里所说的“算法”与程序员所说的“数据结构与算法分析”里的“算法”略有区别。前者更关注结果数据的召回率、精确度、准确性等方面,后者更关注执行过程的时间复杂度、空间复杂度等方面。当然,实际机器学习问题中,对效率和资源占用的考量是不可或缺的。 **3.3 编程语言、工具和环境** 看了无数的理论与知识,总归要落到实际动手实现和解决问题上。而没有工具所有的材料和框架、逻辑、思路都给你,也寸步难行。因此我们还是得需要合适的编程语言、工具和环境帮助自己在数据集上应用机器学习算法,或者实现自己的想法。对初学者而言,Python和R语言是很好的入门语言,很容易上手,同时又活跃的社区支持,丰富的工具包帮助我们完成想法。相对而言,似乎计算机相关的同学用Python多一些,而数学统计出身的同学更喜欢R一些。我们对编程语言、工具和环境稍加介绍: #### 3.3.1 python python有着全品类的数据科学工具,从数据获取、数据清洗到整合各种算法都做得非常全面。 * 网页爬虫: [scrapy](http://scrapy.org/) * 数据挖掘:  * [pandas](http://pandas.pydata.org/):模拟R,进行数据浏览与预处理。 * [numpy](http://www.numpy.org/):数组运算。 * [scipy](http://www.scipy.org/):高效的科学计算。 * [matplotlib](http://matplotlib.org/):非常方便的数据可视化工具。 * 机器学习:  * [scikit-learn](http://scikit-learn.org/stable/):远近闻名的机器学习package。未必是最高效的,但是接口真心封装得好,几乎所有的机器学习算法输入输出部分格式都一致。而它的支持文档甚至可以直接当做教程来学习,非常用心。对于不是非常高纬度、高量级的数据,scikit-learn胜任得非常好(有兴趣可以看看sklearn的源码,也很有意思)。 * [libsvm](https://www.csie.ntu.edu.tw/~cjlin/libsvm/):高效率的svm模型实现(了解一下很有好处,libsvm的系数数据输入格式,在各处都非常常见) * keras/[TensorFlow](http://www.tensorflow.org/):对深度学习感兴趣的同学,也能很方便地搭建自己的神经网络了。 * 自然语言处理:  * [nltk](http://www.nltk.org/):自然语言处理的相关功能做得非常全面,有典型语料库,而且上手也非常容易。 * 交互式环境:  * [ipython notebook](http://ipython.org/notebook.html):能直接打通数据到结果的通道,方便至极。强力推荐。 #### 3.3.2 R R最大的优势是开源社区,聚集了非常多功能强大可直接使用的包,绝大多数的机器学习算法在R中都有完善的包可直接使用,同时文档也非常齐全。常见的package包括:RGtk2, pmml, colorspace, ada, amap, arules, biclust, cba, descr, doBy, e1071, ellipse等等。另外,值得一提的是R的可视化效果做得非常不错,而这对于机器学习是非常有帮助的。 #### 3.3.3 其他语言 相应资深程序员GG的要求,再补充一下java和C++相关机器学习package。 * Java系列  * [WEKA Machine Learning Workbench](http://machinelearningmastery.com/what-is-the-weka-machine-learning-workbench/) 相当于java中的scikit-learn * 其他的工具如[Massive Online Analysis(MOA)](http://moa.cms.waikato.ac.nz/)、[MEKA ](http://meka.sourceforge.net/)、 [Mallet](http://mallet.cs.umass.edu/) 等也非常有名。 * 更多详细的应用请参考这篇文章[《25个Java机器学习工具&库》](http://www.csdn.net/article/2015-12-25/2826560) * C++系列  * [mlpack](http://www.mlpack.org/),高效同时可扩充性非常好的机器学习库。 * [Shark](http://image.diku.dk/shark/sphinx_pages/build/html/rest_sources/downloads/downloads.html):文档齐全的老牌C++机器学习库。 #### 3.3.4 大数据相关 * [Hadoop](http://hadoop.apache.org/):基本上是工业界的标配了。一般用来做特征清洗、特征处理的相关工作。 * [spark](http://blog.csdn.net/han_xiaoyang/article/details/spark.apache.org):提供了[MLlib](http://blog.csdn.net/han_xiaoyang/article/details/spark.apache.org/mllib/)这样的大数据机器学习平台,实现了很多常用算法。但可靠性、稳定性上有待提高。 #### 3.3.5 操作系统 * mac和linux会方便一些,而windows在开发中略显力不从心。所谓方便,主要是指的mac和linux在下载安装软件、配置环境更快捷。 * 对于只习惯windows的同学,推荐anaconda,一步到位安装完python的全品类数据科学工具包。 **3.4 基本工作流程** 以上我们基本具备了机器学习的必要条件,剩下的就是怎么运用它们去做一个完整的机器学习项目。其工作流程如下: #### 3.4.1抽象成数学问题 * 明确问题是进行机器学习的第一步。机器学习的训练过程通常都是一件非常耗时的事情,胡乱尝试时间成本是非常高的。 * 这里的抽象成数学问题,指的我们明确我们可以获得什么样的数据,目标是一个分类还是回归或者是聚类的问题,如果都不是的话,如果划归为其中的某类问题。 #### 3.4.2获取数据 * 数据决定了机器学习结果的上限,而算法只是尽可能逼近这个上限。 * 数据要有代表性,否则必然会过拟合。 * 而且对于分类问题,数据偏斜不能过于严重,不同类别的数据数量不要有数个数量级的差距。 * 而且还要对数据的量级有一个评估,多少个样本,多少个特征,可以估算出其对内存的消耗程度,判断训练过程中内存是否能够放得下。如果放不下就得考虑改进算法或者使用一些降维的技巧了。如果数据量实在太大,那就要考虑分布式了。 #### 3.4.3特征预处理与特征选择 * 良好的数据要能够提取出良好的特征才能真正发挥效力。 * 特征预处理、数据清洗是很关键的步骤,往往能够使得算法的效果和性能得到显著提高。归一化、离散化、因子化、缺失值处理、去除共线性等,数据挖掘过程中很多时间就花在它们上面。这些工作简单可复制,收益稳定可预期,是机器学习的基础必备步骤。 * 筛选出显著特征、摒弃非显著特征,需要机器学习工程师反复理解业务。这对很多结果有决定性的影响。特征选择好了,非常简单的算法也能得出良好、稳定的结果。这需要运用特征有效性分析的相关技术,如相关系数、卡方检验、平均互信息、条件熵、后验概率、逻辑回归权重等方法。 #### 3.4.4训练模型与调优 * 直到这一步才用到我们上面说的算法进行训练。现在很多算法都能够封装成黑盒供人使用。但是真正考验水平的是调整这些算法的(超)参数,使得结果变得更加优良。这需要我们对算法的原理有深入的理解。理解越深入,就越能发现问题的症结,提出良好的调优方案。 #### 3.4.5模型诊断 如何确定模型调优的方向与思路呢?这就需要对模型进行诊断的技术。 * 过拟合、欠拟合判断是模型诊断中至关重要的一步。常见的方法如交叉验证,绘制学习曲线等。过拟合的基本调优思路是增加数据量,降低模型复杂度。欠拟合的基本调优思路是提高特征数量和质量,增加模型复杂度。 * 误差分析** 也是机器学习至关重要的步骤。通过观察误差样本,全面分析误差产生误差的原因:是参数的问题还是算法选择的问题,是特征的问题还是数据本身的问题…… * 诊断后的模型需要进行调优,调优后的新模型需要重新进行诊断,这是一个反复迭代不断逼近的过程,需要不断地尝试,进而达到最优状态。 #### 3.4.6模型融合 * 一般来说,模型融合后都能使得效果有一定提升。而且效果很好。 * 工程上,主要提升算法准确度的方法是分别在模型的前端(特征清洗和预处理,不同的采样模式)与后端(模型融合)上下功夫。因为他们比较标准可复制,效果比较稳定。而直接调参的工作不会很多,毕竟大量数据训练起来太慢了,而且效果难以保证。 #### 3.4.7上线运行 * 这一部分内容主要跟工程实现的相关性比较大。工程上是结果导向,模型在线上运行的效果直接决定模型的成败。不单纯包括其准确程度、误差等情况,还包括其运行的速度(时间复杂度)、资源消耗程度(空间复杂度)、稳定性是否可接受。 这些工作流程主要是工程实践上总结出的一些经验。并不是每个项目都包含完整的一个流程。这里的部分只是一个指导性的说明,只有大家自己多实践,多积累项目经验,才会有自己更深刻的认识。 **3.5 关于积累项目经验** 初学机器学习可能有一个误区,就是一上来就陷入到对各种高大上算法的追逐当中。动不动就我能不能用深度学习去解决这个问题啊?我是不是要用boosting算法做一些模型融合啊?我一直持有一个观点,『脱离业务和数据的算法讨论是毫无意义的』。 实际上按我们的学习经验,从一个数据源开始,即使是用最传统,已经应用多年的机器学习算法,先完整地走完机器学习的整个工作流程,不断尝试各种算法深挖这些数据的价值,在运用过程中把数据、特征和算法搞透,真正积累出项目经验才是最快、最靠谱的学习路径。 那如何获取数据和项目呢?一个捷径就是积极参加国内外各种数据挖掘竞赛,数据直接下载下来,按照竞赛的要求去不断优化,积累经验。国外的[Kaggle](https://www.kaggle.com/)和国内的[DataCastle](http://www.pkbigdata.com/) 以及[阿里天池比赛](https://tianchi.aliyun.com/)都是很好的平台,你可以在上面获取真实的数据和数据科学家们一起学习和进行竞赛,尝试使用已经学过的所有知识来完成这个比赛本身也是一件很有乐趣的事情。和其他数据科学家的讨论能开阔视野,对机器学习算法有更深层次的认识。 有意思的是,有些平台,比如[阿里天池比赛](https://tianchi.aliyun.com/),甚至给出了从数据处理到模型训练到模型评估、可视化到模型融合增强的全部组件,你要做的事情只是参与比赛,获取数据,然后使用这些组件去实现自己的idea即可。具体内容可以参见[阿里云机器学习文档](https://help.aliyun.com/document_detail/shujia/machine-learning/pai-quickstart.html)。 **3.6 自主学习能力** 多几句嘴,这部分内容和机器学习本身没有关系,但是我们觉得这方面的能力对于任何一种新知识和技能的学习来说都是至关重要的。自主学习能力提升后,意味着你能够跟据自己的情况,找到最合适的学习资料和最快学习成长路径。 #### 3.6.1 信息检索过滤与整合能力 对于初学者,绝大部分需要的知识通过网络就可以找到了。 google搜索引擎技巧——组合替换搜索关键词、站内搜索、学术文献搜索、PDF搜索等——都是必备的。 一个比较好的习惯是找到信息的原始出处,如个人站、公众号、博客、专业网站、书籍等等。这样就能够找到系统化、不失真的高质量信息。 百度搜到的技术类信息不够好,建议只作为补充搜索来用。各种搜索引擎都可以交叉着使用效果更好。 学会去常见的高质量信息源中搜索东西:stackoverflow(程序相关)、quora(高质量回答)、wikipedia(系统化知识,比某某百科不知道好太多)、知乎(中文、有料)、网盘搜索(免费资源一大把)等。 将搜集到的网页放到分类齐全的云端收藏夹里,并经常整理。这样无论在公司还是在家里,在电脑前还是在手机上,都能够找到自己喜欢的东西。 搜集到的文件、代码、电子书等等也放到云端网盘里,并经常整理。 #### 3.6.2 提炼与总结能力 经常作笔记,并总结自己学到的知识是成长的不二法门。其实主要的困难是懒,但是坚持之后总能发现知识的共性,就能少记一些东西,掌握得更多。 笔记建议放到云端笔记里,印象笔记、为知笔记都还不错。这样在坐地铁、排队等零碎的时间都能看到笔记并继续思考。 #### 3.6.3 提问与求助能力 机器学习的相关QQ群、论坛、社区一大堆。总有人知道你问题的答案。 但是大多数同学都很忙,没法像家庭教师那样手把手告诉你怎么做。 为了让回答者最快明白你的问题,最好该学会正确的问问题的方式:陈述清楚你的业务场景和业务需求是什么,有什么已知条件,在哪个具体的节点上遇到困难了,并做过哪些努力。 有一篇经典的文章告诉你怎样通过提问获得帮助:[《提问的智慧》](https://github.com/FredWe/How-To-Ask-Questions-The-Smart-Way/blob/master/README-zh_CN.md),强力推荐。话锋犀利了些,但里面的干货还是很好的。 别人帮助你的可能性与你提问题的具体程度和重要性呈指数相关。 #### 3.6.4 分享的习惯 我们深信:“证明自己真的透彻理解一个知识,最好的方法,是给一个想了解这个内容的人,讲清楚这个内容。”分享能够最充分地提升自己的学习水平。这也是我们坚持长期分享最重要的原因。 分享还有一个副产品,就是自己在求助的时候能够获得更多的帮助机会,这也非常重要。 ### 4\. 相关资源推荐 文章的最后部分,我们继续放送干货。其实机器学习的优质资源非常多。博主也是翻遍浏览器收藏夹,也问同事取了取经,整合了一部分资源罗列如下: **4.1 入门资源** 首先[coursera](https://www.coursera.org/) 是一个非常好的学习网站,集中了全球的精品课程。上述知识学习的过程都可以在上面找到合适的课程。也有很多其他的课程网站,这里我们就需要学习的数学和机器学习算法推荐一些课程(有一些课程有中文字幕,有一些只有英文字幕,有一些甚至没有字幕,大家根据自己的情况调整,如果不习惯英文,基础部分有很多国内的课程也非常优质): * 微积分相关 > [Calculus: Single Variable](https://www.coursera.org/learn/single-variable-calculus)  > [Multivariable Calculus](http://ocw.mit.edu/courses/mathematics/18-02sc-multivariable-calculus-fall-2010/) * 线性代数 > [Linear Algebra](http://ocw.mit.edu/courses/mathematics/18-06-linear-algebra-spring-2010/) * 概率统计 > [Introduction to Statistics: Descriptive Statistics](https://www.edx.org/course/introduction-statistics-descriptive-uc-berkeleyx-stat2-1x)  > [Probabilistic Systems Analysis and Applied Probability](http://ocw.mit.edu/courses/electrical-engineering-and-computer-science/6-041-probabilistic-systems-analysis-and-applied-probability-fall-2010/) * 编程语言 > [Programming for Everybody](https://www.coursera.org/learn/python):Python  > [DataCamp: Learn R with R tutorials and coding challenges](https://www.datacamp.com/):R * 机器学习方法 > [Statistical Learning(R)](https://lagunita.stanford.edu/courses/HumanitiesandScience/StatLearning/Winter2015/about)  > [machine learning](https://www.coursera.org/learn/machine-learning):强烈推荐,Andrew Ng老师的课程  > [机器学习基石](https://www.coursera.org/course/ntumlone)  > [机器学习技术](https://www.coursera.org/course/ntumltwo):林轩田老师的课相对更有深度一些,把作业做完会对提升对机器学习的认识。  > [自然语言处理](https://class.coursera.org/nlp/lecture):斯坦福大学课程 * 日常阅读的资源 > [@爱可可-爱生活的微博](http://weibo.com/fly51fly?from=myfollow_all)  > [机器学习日报的邮件订阅](http://ml.memect.com/) 等。 **4.2 进阶资源** * 有源代码的教程 > [scikit-learn](http://scikit-learn.org/stable/auto_examples/index.html)中各个算法的例子  > 《机器学习实战》 有中文版,并附有python源代码。  > [《The Elements of Statistical Learning (豆瓣)》](http://book.douban.com/subject/3294335/) 这本书有对应的中文版:[《统计学习基础 (豆瓣)》](http://book.douban.com/subject/1152126/)。书中配有R包。可以参照着代码学习算法。网盘中有中文版。  > [《Natural Language Processing with Python (豆瓣)》](http://book.douban.com/subject/3696989/) NLP 经典,其实主要是讲 python的NLTK 这个包。网盘中有中文版。  > [《Neural Networks and Deep Learning》](http://neuralnetworksanddeeplearning.com/) Michael Nielsen的神经网络教材,浅显易懂。国内有部分翻译,不全,建议直接看原版。 * 图书与教材 > 《数学之美》:入门读起来很不错。  > [《统计学习方法 (豆瓣) 》](http://book.douban.com/subject/10590856/):李航经典教材。  > [《Pattern Recognition And Machine Learning (豆瓣) 》](http://book.douban.com/subject/2061116/):经典中教材。  > 《统计自然语言处理》自然语言处理经典教材  > 《Applied predictive modeling》:英文版,注重工程实践的机器学习教材  > [《UFLDL教程》](http://ufldl.stanford.edu/wiki/index.php/UFLDL%E6%95%99%E7%A8%8B):神经网络经典教材  > [《deeplearningbook》](http://www.deeplearningbook.org/):深度学习经典教材。 * 工具书 > [《SciPy and NumPy (豆瓣) 》](http://book.douban.com/subject/10561724/)  > [《Python for Data Analysis (豆瓣) 》](http://book.douban.com/subject/10760444/)作者是Pandas这个包的作者 * 其他网络资料 > [机器学习(Machine Learning)与深度学习(Deep Learning)资料汇总](http://blog.csdn.net/zhongwen7710/article/details/45331915): 作者太给力,量大干货多,有兴趣的同学可以看看,博主至今只看了一小部分。
';

机器学习系列(6)_从白富美相亲看特征预处理与选择(下)

最后更新于:2022-04-01 09:52:11

作者:[龙心尘](http://blog.csdn.net/longxinchen_ml?viewmode=contents) &&[寒小阳](http://blog.csdn.net/han_xiaoyang?viewmode=contents) 时间:2016年1月。 出处: [http://blog.csdn.net/longxinchen_ml/article/details/50493845](http://blog.csdn.net/longxinchen_ml/article/details/50493845), [http://blog.csdn.net/han_xiaoyang/article/details/50503115](http://blog.csdn.net/han_xiaoyang/article/details/50503115) 声明:版权所有,转载请联系作者并注明出处 ### 1. 剧情一:挑螃蟹的秘密 李雷与韩梅梅的关系发展得不错,趁国庆休假一起来天津玩。今天,李雷十分神秘地请韩梅梅去一家餐馆吃螃蟹。韩梅梅大失所望,这个餐馆很不起眼,感觉就像路边的老食堂。菜单都用粉笔写在黑板上,一点都不高档。一看价格,满黄螃蟹120块钱一只!这也太贵了。 李雷看到了韩梅梅的神情,笑着解释道:“这家店老板有一个绝活——会看螃蟹。他能保证120块的螃蟹就是满黄。如果拆开来不是,这个螃蟹就不要钱,再换一个。靠着老板的绝活,这家店已经是几十年的老店了,在当地非常有名气。郭德纲、赵丽蓉这些天津社会名流都来这家店吃过螃蟹。” 韩梅梅将信将疑。拆开螃蟹,饱满的蟹黄喷薄欲出。韩梅梅边吃边惊叹:“从没有吃个这么好吃的螃蟹!” 李雷接着说:“老板的绝活密不外传,几十年来都自己上货。虽说是一个大老板,一年到头满身海鲜味。而且他也不开分店。” 韩梅梅说:“那是,这么高明的绝活只有他自己知道才能挣钱啊。”这时,韩梅梅拂面而笑,突然想考一考自己的相亲对象,说:“李大码农,你不是做机器学习的吗?如果要你去用机器学习挑满黄的螃蟹,你怎么做?” ### 2. 初步划定特征的范围,获取特征 李雷早就想过这个问题了。长期的职业素养让他对任何事情都想用机器学习的方法去鼓捣。李雷的基本思路是这样的,我们尽可能观察螃蟹更多的特征,从中找出与“螃蟹满黄”最相关的特征来,帮助我们去判断。当然特征有非常多,我们可以先头脑风暴一下: 1. 一些直观的特征:包括蟹壳的颜色和光泽度、钳子的大小、肚脐的形状、螃蟹腿的粗细和长度、眼睛的大小和颜色光泽、螃蟹的品种、重量、体积、腰围等等…… 1. 一些需要在互动过程中观察到的特征:螃蟹钳子的力量,对外界刺激的反应,用筷子触碰螃蟹眼睛后的反应,螃蟹行动的速度…… 1. 还有一些外部环境的特征: 收获螃蟹的季节,培养螃蟹的水域…… >韩梅梅插话到:“这么多特征我头都大了,你还有完没完?” 其实,如果真要穷举出所有的特征可能永远也举不完。但是我们目的很明确——判断螃蟹是否是满黄。所以我们只关心跟这个问题(“标签”)相关的特征,它们只占所有特征中很小一部分。怕就怕一些糊涂的需求方连目的都不明确就要求一通乱搞,即便出来了一堆结果,也不知道有什么用。 头脑风暴完之后,很重要的一点就是找到对这个问题有长期经验的人,虚心向他们学习。人脑其实是一个很好的特征筛选器,这些经验可以给我们非常多的指导和启发,极大地减少我们试错的工作量。比如我们可以直接去找海鲜市场问螃蟹贩子,去田间地头找螃蟹养殖户,去海鲜饭店去问有经验的采购员和厨师……他们的最一线的经验是特征工程中的宝贵财富。 但这里需要考虑将经验转换成可量化的指标,才能便于机器学习。比如人们可能会说螃蟹很“活跃”、很“精神”,或者很“慵懒”。这些特征需要转换成一些可量化指标去衡量,具体怎么转换也有很大学问。 接下来要考虑的问题是对这些特征的可用性进行简单的评估。比如: 1. 特征获取、描述难度 1. 数据的规模 1. 特征的准确率 1. 特征的覆盖率 1. 其他 我们**通过明确目标,头脑风暴,咨询专家,特征量化,可用性评估等流程,就基本划定了特征范围**。 ### 3. 剧情二:“特征预处理”的门道 李雷说完,便拿出自己的平板,给韩梅梅看自己某个项目中搜集的初始特征。这些特征被放在一张巨大的表里。 韩梅梅看着这些密密麻麻的数字,心想:看李雷说得头头是道,但还是没告诉我怎么挑,不能让他轻易绕过去。 于是她说:“我看你这些特征数据有大有小,有些就是几万上下浮动,有些仅仅是小数点后好几位的微小变化,有些就是在0或1这两种可能中变化,有些连值都没有。你这些数据能用吗?” 李雷说:“不能,要转换成标准件。” 韩梅梅:“标准件?” ### 4. “特征标准件” 如果把机器学习过程当做一个加工厂的话,那输入的数据(特征、标签)就是原材料,输出的模型和判断结果就是产品。并不是胡乱扔进去任何原材料都能加工出合格产品的。原材料需要一个“预处理”过程才能方便地被算法处理。这些预处理后的数据,李雷起了个不够规范的名字,叫做“特征标准件”。 以二分类问题为例,不同的算法对“特征标准件”的要求是不同的。比如逻辑回归和神经网络,比较喜欢归一化之后在[-1,1]区间内浮动的特征。而贝叶斯方法,喜欢因子化之后的{0,1}分布的二元特征,每个特征只有“是”和“不是”两种可能的状态。 ### 5. 连续特征与非连续特征 特征可以分为两类:“连续特征”和“非连续特征”。 “身高”、“体重”、“成绩”、“腰围”、“长度”、“宽度”、“体积”、“速度”等等,都是连续特征。连续特征能够比较方便地进行归一化。归一化的统一公式如下: x∗=x−μS μ为所有样本数据的均值,x−μ的步骤叫做去均值化> 1. 当S=xmax−xmin时,经过处理的数据在区间[-1,1]之间。 1. 当S=σ(所有样本的标准差)时,经过处理的数据符合标准正态分布,即均值为0,标准差为1 另一方面:“是否高富帅”、“是否白富美”、“螃蟹的品种”、“螃蟹所在的水域”、“收获螃蟹的季节”等等,都是非连续特征。非连续特征能够比较方便地进行因子化,或者它本身就是二元特征。方法如下: 特征“收获螃蟹的季节”:{春,夏,秋,冬} 因子化后的结果为: - 特征“是否春”:{是,否} - 特征“是否夏”:{是,否} - 特征“是否秋”:{是,否} - 特征“是否冬”:{是,否} ### 6. 两类特征的相互转化 连续特征可以当非连续特征来用,非连续特征可以当连续特征来用。 连续特征可以离散化非连续特征。比如“年龄”作为一个连续特征,假设它的取值范围是[0,100]。我们可以中间切一刀,比如选择60(岁)。大于等于60岁的就叫做“老年”,小于60岁的就是“非老年”,这样就转化成了一个二元特征了。怎么选择离散的分离边界也很有学问。 如果我们中间切两刀甚至更多刀,比如18(岁)和60(岁)。大于等于60岁的就叫做“老年”,18岁到60岁之间的就叫做“中青年”,小于18岁就叫做“未成年”。然后再把这3类因子化成3个二分类就够了:“是否老年”、“是否中青年”和“是否未成年”。 ![等宽直方图](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430fed55f.jpg "") 非连续特征因子化成二元特征{0,1}后可以直接当做[0,1]之间的连续特征来用。我们之前文章[《机器学习系列(3)_逻辑回归应用之Kaggle泰坦尼克之灾》](http://blog.csdn.net/han_xiaoyang/article/details/49797143)就是这么使用的。 ### 7. 去除特征之间的共线性 我们在对离散特征**因子化过程中细分到二元特征为止即可**。那对于二元特征本身能否因子化成两个特征?比如以下例子: 特征“螃蟹的性别”:{公,母} ,可否转换为: - 特征“是否公螃蟹”:{是,否} - 特征“是否母螃蟹”:{是,否} 这是不行的,因为这两个特征的信息完全一样,也叫做**共线性**。计算这两个特征之间的条件熵: H(“是否公螃蟹”|“是否母螃蟹”)=0 也可以用计算条件熵的方法去衡量两类离散特征的差异性,方便去除共线性关系的特征。 连续特征也有着共线性的情况,比如同一个品种的螃蟹腿的“长度”和“粗细”是共线性关系。也就是说,如果我们知道螃蟹腿的长度是x厘米,那么螃蟹腿的直径就是kx厘米,k是一个稳定的常数。因此我们只需要螃蟹腿的“长度”这一个特征就够了。那么连续特征的共线性如何去除? 可以计算两个变量(x,y)的相关系数: rxy=cov(x,y)σxσy=cov(x,y)cov(x,x)×cov(y,y)√ rxy的取值范围是[-1,1],如果是0则统计独立,如果接近1则强相关。> 可以计算这些数据的协方差矩阵,进而求出相关系数矩阵。就可以比较任意两个特征了。 ![协方差矩阵](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e243100f1b3.jpg "") ![相关系数矩阵](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2431026ab7.jpg "") 既然协方差矩阵都求了,那就干脆用主成分分析(PCA)吧,这样更省事。得到主成分,线性相关的那些量就直接被舍弃了。我们在前文[《深度学习与计算机视觉系列(7)_神经网络数据预处理,正则化与损失函数》](http://blog.csdn.net/han_xiaoyang/article/details/50451460) 对PCA有相关论述。 感兴趣的同学可以试试把上述离散二元特征当做连续变量使用,构造几个数据,计算其相关系数并进行主成分分析。发现其相关系数就是-1,主成分分析后自动就变成一个主成分了。可见PCA对于连续特征与非连续特征都是去除共线性的通用方法。 ### 8. 缺失值的处理 这个问题现在才讲,但实际过程中应该在前期去处理。掌握以下三点就够了: 1. 如果某个特征的缺失值比较多:可能就直接舍弃。 1. 如果缺失值不是很多,而且是连续特征:可以考虑用回归方法去拟合,或者直接用众数、中位数、平均数等具体的值去替代即可。 1. 如果缺失值不是很多,而且是非连续特征:可以尝试把缺失值当做一个新的类目去处理,可能也揭示了一定的客观现实。 ### 9. 离群点的分析 对于连续特征,最好看看其在样本中的分布。如果某些值偏离了主要聚集区域,可能需要单独抽出来分析,里面可能包含了更多的信息,可以这样画图方便观察: ![离群点](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e243103e800.jpg "") ### 10. 特征预处理小结 特征的预处理步骤比较多,相互之间的关系比较复杂。我们画了一张图以揭示它们之间的关系: ![特征预处理](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e243105b8d0.jpg "") ### 11. 剧情三:李雷另辟蹊径挑螃蟹 韩梅梅长叹一口气:“终于听你叨逼叨逼说完了。” 李雷说:“没办法啊,这块工作其实挺多的。我还要好多没说完……” “你打住”,韩梅梅赶紧说,“我算服了你了。但是李大码农,你还没有跟我说你怎么靠这些特征挑螃蟹呢。” 李雷说:“不急,用逻辑回归……”韩梅梅说:“不要用逻辑回归,我已经从赵媒婆那知道了。你换个方法,用非连续特征来做。”韩梅梅存心想刁难她的相亲对象。 李雷说:“那可以用贝叶斯。” ### 12. 用贝叶斯方法挑螃蟹 我们的标签用Y=“是满黄”来表示,相应的Y⎯⎯⎯=“不是满黄”。Xi表示所有离散化的二元特征,如X1=“是河蟹”,X2=“是秋季收货”,X3=”钳子的力量大”……。于是在已知这些特征的情况下,该螃蟹“是满黄”的概率如下: P(Y|X1,X2,X3...)=P(X1,X2,X3...|Y)×P(Y)P(X1,X2,X3...)> 其实,可以直接判断P(Y|X1,X2,X3...)是否大于1/2即可。因为P(X1,X2,X3...)这一项算起来比较麻烦,我们用以下方法直接把它约掉。 先求出螃蟹“不是满黄”的概率: P(Y⎯⎯⎯|X1,X2,X3...)=P(X1,X2,X3...|Y⎯⎯)×P(Y⎯⎯)P(X1,X2,X3...)> 再两式相处,得到: P(Y|X1,X2,X3...)P(Y⎯⎯|X1,X2,X3...)=P(X1,X2,X3...|Y)×P(Y)P(X1,X2,X3...|Y⎯⎯)×P(Y⎯⎯)> 这样就约去了P(X1,X2,X3…)。只需要判断P(Y|X1,X2,X3...)P(Y⎯⎯|X1,X2,X3...)是否大于1即可。但是,工程上用除法不太方便,两边同时取对数log,得到: logP(Y|X1,X2,X3...)P(Y⎯⎯|X1,X2,X3...)=logP(X1,X2,X3...|Y)P(X1,X2,X3...|Y⎯⎯)+logP(Y)P(Y⎯⎯)> 左边是螃蟹“是满黄”的逻辑发生比,只需要判断其是否大于0即可。 到目前为止,以上都是等价变换。 接下来我们引入贝叶斯方法中常用的条件独立假设: P(X1,X2,X3...|Y)=P(X1|Y)×P(X2|Y)×P(X3|Y)... P(X1,X2,X3...|Y⎯⎯⎯)=P(X1|Y⎯⎯⎯)×P(X2|Y⎯⎯⎯)×P(X3|Y⎯⎯⎯)...> 将它们带入上式,就变成了: logP(Y|X1,X2,X3...)P(Y⎯⎯|X1,X2,X3...)=logP(X1|Y)P(X1|Y⎯⎯)+logP(X2|Y)P(X2|Y⎯⎯)+logP(X3|Y)P(X3|Y⎯⎯)+...+logP(Y)P(Y⎯⎯)> 于是我们得到了一个简单的求和式,只需要判断等式右边求和的结果是否大于0即可。而最关键的就是右边每一项都非常好求!假如训练集中所有的满黄螃蟹收集在一起,统计每一个特征出现的次数,除以满黄螃蟹的总数,就是其相应的条件(后验)概率了。再统计该特征在非满黄螃蟹集合中的条件(后验)概率,二者相除再取对数即可。 ### 13. 用贝叶斯方法进行特征有效性分析 等式右边作为一个求和式,其中每个求和项logP(Xi|Y)P(Xi|Y⎯⎯)的绝对值越大,其对结果的影响越强烈,相对应的特征就是显著特征。而绝对值比较小的特征就是非显著特征,剔除掉也不会很明显地影响结果。这就完成了一个特征筛选的过程。 我们再分析一下各个求和项的结构,里面的概率部分是后验概率,是特征相对于标签的后验概率。这个后验概率与我们上一篇文章[《从白富美相亲看特征预处理与选择(上)》](http://blog.csdn.net/han_xiaoyang/article/details/50481967)中的后验概率方向相反,不要搞混淆了。 ### 14. 贝叶斯与逻辑回归之间的关系 我们继续看看这个求和项,是不是很像逻辑回归中的求和项?我们如果拿二元特征当做连续变量采用逻辑回归方法。其判别式如下: z=w1x1+w2x2+w3x3+...+b;其中xi∈{0,1}> 二者的表达式惊人地相似!莫非logP(Xi|Y)P(Xi|Y⎯⎯)=wi,二者一模一样? 感兴趣的同学可以自己举个例子试一下,发现还是有区别的,二者求出来的权重不一样。产生这样差别的原因是什么呢? 想必大家都猜到了。就是贝叶斯方法引入的两个条件独立假设。正因为这两个条件独立假设,贝叶斯方法直接跳过了逻辑回归中反复迭代用梯度下降法才能求出的各个权重。 因此贝叶斯方法与逻辑回归的区别就是贝叶斯方法引入了一个更强的附加假设,而且可以直接通过统计结果求权重,而不必用梯度下降法。 所以有些情况下贝叶斯方法求出来的结果不好,就可以考虑考虑是不是条件独立假设的原因。 因此,可以说“在某种假定下,可以证明:与朴素贝叶斯分类方法一样,许多神经网络和曲线拟合算法输出最大的后验假定。”——韩家炜:《数据挖掘:概念与技术(第三版)》 ### 15. 剧情四:李雷露馅儿了 韩梅梅听完,十分感慨地说:“难怪机器学习能挑出正确的结果,难怪赵媒婆用机器学习方法从这么多人中能把你挑出里来。你还是有两下子嘛。” “废话,她是我干妈”,李雷志得意满,不小心说漏嘴。 韩梅梅:“什么?!” 李雷后悔不已,尴尬地陪着笑脸说道:“梅梅,我错了,我不该瞒你这么久。”到了这个地步,李雷只能和盘托出了。 ### 16. 数据VS算法 其实李雷早就知道韩妈妈要挑选相亲名单,如果按她的标准,李雷根本没法进入名单中。而李雷也猜想她会去找赵媒婆。他就早早地联系赵媒婆,跟她推销他的机器学习方法。赵媒婆终于被李雷忽悠动心了。李雷就帮她开发那个相亲算法。但其实赵媒婆的样本数量不够,特征数量却非常多,肯定会过拟合。李雷就跟她说他会多找一些相亲的数据。李雷能从哪里找啊,只能发动周围的同学,让他们找他们观察到的情侣案例。而在这些群体中,恰好中学、大学是同学的情侣比率非常高,而且很多男方是码农。而李雷刚好符合这个条件,李雷的评分就非常高了。 因为样本选择本来就代表性不足,没能覆盖更多的青年群体,所以还是过拟合,只是偏向了李雷这边的概率而已。 可见,做机器学习虽然看起来比较炫酷的是算法,但真正关键的是数据。数据决定了你结果的上限,而算法只是尽可能逼近这个上限。而这点李雷并没有告诉赵媒婆。 对样本情况的分析要在特征优化过程中尤其注意。整个流程图如下: ![特征处理](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e24310707ba.jpg "") ### 17. 特征选择的局限性 而且,李雷并不觉得感情这样复杂的东西能够用赵媒婆那些量化的指标衡量好的。房子、车子、学历、文凭这些并不能衡量两个人之间的感情。一些非常重要的特征是难以量化的,比如两个人的“三观”、两个人对待感情的态度、两个人相互相处的独一无二的经历、两个人刻骨铭心的情感体验、那种两个人相信能够一辈子都在一起的笃定的感觉……这些至关重要的特征极其复杂,却非常难以量化。所以对于这类问题,机器学习的能力还是很有限的。 ### 18. 剧情五:尾声 韩梅梅听完李雷,既生气,又好笑,还有一点点小感动:这小子为了感情还是蛮拼的。 一段沉默之后,韩梅梅笑着对李雷说:“好了好了,我不怪你了。”李雷长舒一口气。 韩梅梅继续说:“问个挑螃蟹的问题。你刚才选了这么多特征。为什么不考虑用B超直接照一下,看看里面什么东西不就成了吗?” 李雷一听,犹如当头一棒,整个脑子都被草泥马占满了:“我去,这么简单的方法我怎么想不到?!” 韩梅梅这时已经笑得肚子痛了,根本说不上话。 李雷吐槽到:“梅梅,你太厉害了。我觉得机器永远也学不到的两样东西就是人类的情感和脑洞啊!” ### 19. 后记 其实博主也没有丧心病狂到抓只螃蟹去照B超,只是自己被这个想法逗乐了,大家开心就好哈。O(∩_∩)O~~ 如果真要死磕,据说B超的穿透力比较弱,对骨骼、空气等很难达到深部,因此难以成像。但是通过声波的回声来判断,也是一个思路。就像有些人可以通过拍打西瓜听声音来判断它甜不甜的道理一样。 如果不用机械波而用电磁波,比如X射线,估计哪怕能看到螃蟹满黄顾客也不会吃了。顾客也会担心放射残留的。CT应该好些,但是贵呀。一套设备下来,螃蟹估计也不止120块钱了吧。没玩过CT,不知道成本多少……总之还是要考虑获取特征的成本的。
';

机器学习系列(5)_从白富美相亲看特征预处理与选择(上)

最后更新于:2022-04-01 09:52:09

作者:[龙心尘](http://blog.csdn.net/longxinchen_ml?viewmode=contents) &&[寒小阳](http://blog.csdn.net/han_xiaoyang?viewmode=contents) 时间:2016年1月。 出处: [http://blog.csdn.net/longxinchen_ml/article/details/50471682](http://blog.csdn.net/longxinchen_ml/article/details/50471682), [http://blog.csdn.net/han_xiaoyang/article/details/50481967](http://blog.csdn.net/han_xiaoyang/article/details/50481967) 声明:版权所有,转载请联系作者并注明出处 ### 1. 引言 再过一个月就是春节,相信有很多码农就要准备欢天喜地地回家过(xiang)年(qin)了。我们今天也打算讲一个相亲的故事。 讲机器学习为什么要讲相亲?被讨论群里的小伙伴催着相亲,哦不,催着讲特征工程紧啊。只是我们不太敢讲这么复杂高深的东西,毕竟工程实践的经验太复杂了,没有统一的好解释的理论,一般的教材讲这方面的内容不多。我们就打算以一个相亲的故事为例,串一些特征工程的内容。 ### 2. 故事背景 **事先声明:本故事纯属虚构,如有雷同,纯属巧合!** > 海归白富美韩梅梅刚回国,还没适应工作,母亲就催着相亲。以父母的关系,他们了解到的适龄单身男青年有100个。要从100个男生中找到1个理想的女婿,可谓百里挑一。韩梅梅母亲也担心女儿相亲多了会反感,打算草拟一个相亲名单,人数不多。怎么从中挑出优秀男青年就是一个首要的问题。 ### 3. 用机器学习的框架去分析 我们用机器学习的框架分析,在父母眼中,这100个男生最终将会分成两类:“女婿”(1人)和“非女婿”(99人)。“女婿”和“非女婿”就叫做“标签”。 而选择相亲名单的标准——如“是否高富帅”、“是否海归”等等——就叫作“特征”。最好能有一个特征能够精确定位理想女婿。但这太过理想了。比较现实的方法是从这些“特征”中选择、拆分、组合出最合适的特征,逐渐逼近我们的标签,以形成一个精简的相亲名单。而这个过程,就可以理解成特征处理、特征工程的过程。 但是,现实中的特征有千千万,拆分重组之后特征又是几何级数地增加,可能永远也穷举不完。因此需要有统一客观的指标来衡量这些特征对标签的识别能力,以便进一步地深入分析。而评估这些“特征”对我们的“标签”的有效程度的过程就叫作“特征有效性分析”。 ### 4. 剧情一:韩妈妈的“如意算盘” 为人父母嘛,总是希望女儿嫁得好。韩妈妈的第一反应的就是要找“高富帅”。先她先从这100个男生中挑了挑,符合高富帅这个标准的有5个人。 韩妈妈的如意算盘是这样的:女婿就从这5个人中挑,概率就是20%,比之前的1%整整提高了20倍,嘿嘿嘿。。。 ### 5. 特征有效性分析 其实,这就韩妈妈不知不觉就走了一个特征有效性分析的过程。我们用图表演示一下: ![图表](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430f07a54.jpg "") 考虑到各方面的概率,用下图表示更加直观: ![条件概率](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430f180a9.jpg "") 为了表述方便,我们以随机挑女婿而不考虑任何特征的概率叫做“先验概率”(1%)。而中间的箭头中的概率则表示在已经知道样本所属特征前提下,属于女婿还是不属于女婿的概率,也可以叫作“标签相对于某个特征的后验概率”(20%)。而母亲的如意算盘就是考虑了上图中红圈部分的先验概率与后验概率(也可以叫条件概率)。这其实是一种很朴素的特征有效性分析的方法。而且她还做了个更加精确的数量化描述: 后验概率先验概率=20%1%=20(倍)。 只是在工程上做除法可能运算会麻烦些,而两边同时取对数转换成减法则更方便: log(后验概率先验概率)=log(后验概率)−log(先验概率)> 概率表示着选女婿的可能性或者确定性。在本例中,后验概率的确定性比先验概率的确定性更高。可见,“确定性的增加”可以作为特征有效性分析的一个指标。 我们进一步分析,无论先验概率还是后验概率,其本身是0-1之间的一个数,取完对数之后是一个负数,这在现实中不太方便找到其对应的现象解释。但是概率的倒数一定大于1,取完对数之后就是一个正数,就好找现实解释了。我们可以把这个“概率倒数的对数”理解成不确定性的指标。于是上式就变成: log(后验概率先验概率)=log(1先验概率)−log(1后验概率)> 这里面的log(后验概率先验概率)我们叫做互信息。 因此,“不确定性的减少”可以作为特征有效性分析的一个指标。这个结论我们接下来将会反复用到。 ### 6. 剧情二:白富美巧劝慈母 韩妈妈半开玩笑地问韩梅梅:“我们家闺女只挑高富帅的怎么样?”女儿想了想,说:“如果人家看不上我们怎么办?”母亲笑着说:“我们的家境哪里差了?何况我们的女儿这么优秀,我们还看不上他们呢。” 女儿说:“这就是说明我们双方不合适了。我们家条件虽然还不错,但是比下有余、比上不足,跟真正条件好的家庭比较起来我们根本不算事儿。如果一味挑高富帅,他们可能觉得我们只是看中他们的钱,反倒把我们家看低了。相反,要是真要有个真正对我好的男生,比什么都幸福,而他不一定必须是高富帅。毕竟跟我一起相处一辈子的是一个活生生的人,而不是他背后的东西嘛。” 母亲很有感慨地说:“嗯,你能这样想我就放心了。梅梅真是长大了。那么,你打算怎么办?”女儿说道:“高富帅也得分人,踏实人品好的也可以接触一下,但是玩心太重不会照顾人的我就不喜欢。估计高富帅里面这两种人一半一半吧。很多男生并不是高富帅,其中没准也有合适的人呢。” ### 7. 特征有效性分析 现在特征的分布发生了新变化。按韩梅梅的分析,高富帅中可能有一半她就不会喜欢,而不是高富帅的男生中没准有合适的人。我们可以简单假设高富帅中与非高富帅中各有0.5个合适的人。则分析图表如下: ![图表](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430f2d6f8.jpg "") ![条件概率](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430f4abcc.jpg "") 现在的情况是,“是不是女婿”的可能性同时分布在“是高富帅”和“不是高富帅”中,单独衡量“高富帅”本身的后验概率已经不够描述特征的整体效果了。我们可以有一个考虑特征整体情况的指标。 还是回到之前的那句话: > “不确定性的减少”可以作为特征有效性分析的一个指标。 我们之前考虑了“是女婿”的不确定性是log(11%),相应的“不是女婿”的不确定性是log(199%),那么标签“是否女婿”作为整体的平均不确定性则可以理解为这两个状态的加权平均: > H(Y)=1%×log(11%)+99%×log(199%)=0.08079。(全文假定对数log的底数取为2) > 这就是传说中的信息熵。我们用Y表示标签,用H(Y)表示“是否女婿”的信息熵,也就是其整体的平均不确定性。 那么考虑特征(“是否高帅富”)后的标签(“是否女婿”)的平均不确定性怎么衡量?我们用X:{“是高富帅”,“不是高富帅”}来表示特征。其实,与上面的思路类似,我们在已知特征为“是高富帅”的前提下,“是否女婿”这个标签的整体平均不确定性可以用相对“是高富帅”的后验概率来求出: > H(Y|X=“是高富帅”)=(0.5/5)×log(1(0.5/5))+(4.5/5)×log(1(4.5/5))=0.46900 在已知特征为“不是高富帅”的前提下,“是否女婿”这个标签的整体平均不确定性可以用相对“不是高富帅”的后验概率来求出: > H(Y|X=“不是高富帅”)=(0.5/95)×log(1(0.5/95))+(94.5/95)×log(1(94.5/95))=0.04741。 因此,已知特征(无论具体是“是高富帅”还是“不是高富帅”)情况下的标签平均不确定性为前面两种情况的加权平均: > H(Y|X) =P(X=“是高富帅”)×H(Y|X=“是高富帅”)+P(X=“不是高富帅”)×H(Y|X=“不是高富帅”) =5/100×0.46900+95/100×0.04741=0.06849 这就是传说中的条件熵。 所以,考虑特征后,标签的“不确定性的减少”为: > I(Y,X)=H(Y)−H(Y|X)=0.01230 这个I(Y,X)就叫做**平均互信息**。 我们用同样的方法去评价之前母亲设想的女婿只在高富帅中的理想情况(也就是女婿只在高富帅中产生的情况)的互信息I(Y,X′)=0.04470 平均互信息从理想情况的0.04470下降到0.01230,也就是说原以为特征“是否高富帅”与标签“是否女婿”的相关性很高,后来发现相关性其实是比较低的。可见理想很丰满,现实很骨感。 ### 8. 剧情三:白富美重定名单 其实,韩梅梅没有说出来的话是她有一个青梅竹马的码农叫李雷。她出国之前的对他的印象还不错。如果按母亲的标准李雷肯定排除在相亲名单外了,而她想给他一个机会。 这时母亲说话了:“我们家女儿考虑得挺好,那相亲名单你来定吧。”女儿说:“不是高富帅的男生也该好好区分一下,那些品行端正、气度不凡、踏实肯干的潜力股的男生我也比较欣赏,其他的就暂且不考虑了。”母亲说:“就是说可以从高富帅中挑出部分品德好的,还有从不是高帅富的男生中挑出部分潜力股,共同组成一个新的名单,我们的女婿就在这里面了?”女儿不好意思地说:“妈妈您真着急,八字还没一撇呢。” 接着,韩梅梅母女俩从高富帅中挑了2个口碑不错的,又从不是高富帅的男生中条了10个很不错的。最终组成了12人的相亲名单。李雷的名字在其中。 ### 9. 拆分重组成为新特征 其实以上韩梅梅母女俩完成了一次特征的拆分与重组过程。具体图示如下: ![特征拆分重组](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430f5eee2.jpg "") 这里用“潜帅德”表示韩梅梅对“品行端正、气度不凡、踏实肯干的潜力股”的特征的描述。 特征进行拆分与重组的过程在特征工程中经常出现。因为当你对特征与标签的相关性有定量的评估方法后,会筛选出那些不那么显著的特征(如本例中的“是否高富帅”),然后去分析考核指标这么低的原因,启发你引入新的特征(如本例中的“是否品德良好”、“是否有潜力”)将原有特征拆分重组,可能会有更好的效果。而这些生成的新特征,又要经过特征有效性分析来最终评估。如此反复迭代。 ### 10. 特征有效性分析 ![新特征条件概率](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430f744dc.jpg "") 我们用X2来表示新特征,与上面的思路类似,我们计算X2的平均互信息: I(Y,X2)=H(Y)−H(Y|X2)=0.03114> 与之前的平均互信息I(Y,X)=0.01230比起来,有了显著提高。可见新特征X2比之前的特征X更有效。 ### 11. 剧情四:韩妈妈给名单分级 在跟韩梅梅聊完之后,韩妈妈转念一想:“为什么非要有一份相亲名单?可以把这12个人再分成两类,第一类是高富帅的,先相亲。这些觉得不合适后再考虑剩下的10个人啊。” ### 12. 特征有效性分析 ![X3](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430f88c4d.jpg "") 我们继续分析,用X3来表示新特征,与上面的思路类似,我们计算X3的平均互信息: I(Y,X3)=H(Y)−H(Y|X3)=0.03593> 与之前的平均互信息I(Y,X2)=0.03114比起来,又有了一定的提高。可见新特征X3比之前的特征X2更有效。 韩妈妈真是为女儿的相亲操碎了心。 ### 13. 剧情五:韩妈妈问计赵媒婆 韩妈妈思索完之后抑制不住内心的兴奋,想找人倾诉。这时她正好在路上碰见了赵媒婆。赵媒婆在韩妈妈的老闺蜜圈中享有盛誉,相亲非常有经验。 赵媒婆听了韩妈妈的诉说后,微微一笑,说:“你这个名单不够专业。”韩妈妈大为诧异。赵媒婆继续说:“高、富、帅三个特征本来就是相互独立的三个特征,你硬生生地绑在一起,多少大好青年被你给甩掉了。后面的潜力股啊、人品端正啊什么的都类似。”韩妈妈恍然大悟:“真是这样啊。” 赵媒婆说:“其实你这里最大的问题是这些特征的评估都是拍脑袋决定,没有充分的现实数据做支撑,很可能会犯错误的。”韩妈妈暗暗点头,心生佩服。 赵媒婆接着说:“还有一个问题,你准备了两份名单,也就是把人群分成了三份,你算平均互信息只能评价整体的,具体到每一份人群你怎么对他们评价?”韩妈妈想了想,说:“我们可以直接用相对于某个具体条件的信息熵啊。”赵媒婆说:“何苦这么麻烦呢?” 韩妈妈听她话里有话,打算继续问下去。 ### 14. 评价特征选项的两个方法 在赵媒婆最后一个问题中,韩妈妈所说的其实是可以计算以下三个值来评估具体的特征选项: - H(Y|X3=“高富帅德”) - H(Y|X3=“潜帅德”) - H(Y|X3=“不是高富帅德且不是潜帅德”) 而这三个值在之前计算条件熵H(Y|X3)的过程中就已经计算出来了。所以比较起来应该很方便。 但其实更简单的方法用他们相对于所需要标签的后验概率评价。如下图红色的部分,比较大小就可以找出评价较好的特征。 ![X3](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430fa617b.jpg "") 显然“高富帅德”的评分最高(0.25),“潜帅德”的评分次之(0.05),“不是高富帅德且不是潜帅德”评分最差(0)。符合韩妈妈的预期。 然而,赵媒婆的想说的并不是这种方法,而是逻辑回归…… ### 15. 剧情六:赵媒婆的数据库 赵媒婆不等韩妈妈说话,就直接拿出了自己的神器:一个平板电脑。然后打开她的相亲数据库,点了点鼠标,一张巨大的表展现出来。韩妈妈目瞪口呆:“现在媒婆都用高科技了?”赵媒婆傲娇地说:“那是。” 这张大表是她这么多年来全国各地相亲介绍的所有男生信息,分别标注了每个男生的升高、年龄、年薪、长相特点、教育经历、工作经历、是否海归、工作年限、工作公司、工作地点、出身地、是否有户口、是否公务员、具体职业、行业、性格倾向等等信息。 她还有一张女生信息表,另外一张男生女生相亲情况表(相亲成功、相亲不成、继续发展、未接触)。媒婆一一给韩妈妈解释这些信息。韩妈妈连连惊呼。 赵媒婆接着说道:“我们可以从里面找出跟你女儿情况相近的一些女生信息,再把跟她们相过亲的男生找出来,把其中相亲成功的归为一类,剩下的归为另一类。然后假设男生的每个特征对相亲成功都有贡献,贡献的权重为wi。我们用逻辑回归的方法可以求出这些权重,把这些权重大的特征挑出来,你再用它们来找女婿就方便了。” 韩妈妈说:“逻~辑~什么?”赵媒婆说:“高科技了,你不懂的。不过给我干儿子写了个[博客](http://blog.csdn.net/han_xiaoyang/article/details/49332321)来介绍,你可以看看。” ### 16. 特征筛选与特征工程工作流 呃,我们什么时候成赵媒婆的干儿子了?先不管这些。逻辑回归并不是什么高科技,在[前面的文章](http://blog.csdn.net/han_xiaoyang/article/details/49123419)里已有简单的解释。我们在这里就补充说明一下为什么可以用权重来衡量特征的贡献。以下是一个典型的逻辑回归过程: ![逻辑回归](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430fbc382.jpg "") 我们期望P(z)的概率越大越好,sigmoid函数是个单调递增函数,所以z越大越好,在所有特征都归一化的前提下,显然是权重wi越大越好。因此与wi对应的特征就是我们要寻找的显著特征。而那些权重小的特征就可以先不考虑了。这就完成了一个最简单的特征筛选的过程。 当然,这里所说的权重大可以指的是权重的绝对值很大,比如特征“富”的权重是-100,是一个很小的数,但这也就意味着“不富”的权重会很大,以至于显著影响我们的z的结果。所以这也是一个显著特征。 需要补充一下的是,在工程实践中,权重的幅度和正则化也有关系。L1正则化会把特征拉稀疏,会产出一部分0特征。而不是0的那些特征,是有作用的特征。所以L1正则化其实具备一定的特征选择(feature selection)的作用。尤其是很高维空间的feature,用L1正则化,其实能帮助做一下feature selection的。而L2正则化,则会把各个维度的权重拉平均一些,抑制住各个维度权重幅度的方差。但是抑制归抑制,最后的权重还是会有大小差异,就像上文说的,绝对值大的权重,对应的特征区分度好一些。 对于那些不够显著的特征,我们需要分析一下这个特征的具体情况是怎样,是否需要对其进行重新拆分与重组,拆分重组后新的特征又可以进行特征有效性分析。如此不断迭代反复,就可以挑选出比较理想的特征了。 我们用以下整个工作流大致展现这个过程。由于很多内容没有展开,我们先把名字写进去,在后续的文章中继续扩展。 ![工作流](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430fd02ab.jpg "") ### 17. 剧情七:韩妈妈新名单尘埃落定 在韩妈妈与赵媒婆的尽心鼓捣下,最终生成了一个只有4个人的相亲名单。其中只剩下一名高富帅,另外三人中有一人正是李雷。韩妈妈拿着新名单给女儿看,韩梅梅沉默半晌,心想李雷在四人名单中怎么也能存在,莫非这也是缘分? ### 18. 小结 本文中主要讲了一些特征有效性分析的方法,包括用互信息,平均互信息,条件熵,后验概率,逻辑回归权重等方法对特征与标签的相关性进行了评估。有了这些评估做基础,可以筛选出显著的特征,并对对不显著的特征进行分析、拆分和重组,最终形成新的特征并反复迭代。本文略过了一些特征预处理的方法,并对特征有效性评估的阐述不够充分,我们将在接下来的文章中予以讨论。
';

机器学习系列(4)_机器学习算法一览,应用建议与解决思路

最后更新于:2022-04-01 09:52:06

作者:[寒小阳](http://blog.csdn.net/han_xiaoyang?viewmode=contents) 时间:2016年1月。 出处:[http://blog.csdn.net/han_xiaoyang/article/details/50469334](http://blog.csdn.net/han_xiaoyang/article/details/50469334) 声明:版权所有,转载请联系作者并注明出处 ### 1.引言 提起笔来写这篇博客,突然有点愧疚和尴尬。愧疚的是,工作杂事多,加之懒癌严重,导致这个系列一直没有更新,向关注该系列的同学们道个歉。尴尬的是,按理说,机器学习介绍与算法一览应该放在最前面写,详细的应用建议应该在讲完机器学习常用算法之后写,突然莫名奇妙在中间插播这么一篇,好像有点打乱主线。 老话说『亡羊补牢,为时未晚』,前面开头忘讲的东西,咱在这块儿补上。我们先带着大家过一遍传统机器学习算法,基本思想和用途。把问题解决思路和方法应用建议提前到这里的想法也很简单,希望能提前给大家一些小建议,对于某些容易出错的地方也先给大家打个预防针,这样在理解后续相应机器学习算法之后,使用起来也有一定的章法。 ### 2.机器学习算法简述 按照不同的分类标准,可以把机器学习的算法做不同的分类。 **2.1 从机器学习问题角度分类** 我们先从机器学习问题本身分类的角度来看,我们可以分成下列类型的算法: - 监督学习算法 机器学习中有一大部分的问题属于`『监督学习』`的范畴,简单口语化地说明,这类问题中,给定的训练样本中,每个样本的输入x都对应一个确定的结果y,我们需要训练出一个模型(数学上看是一个x→y的映射关系f),在未知的样本x′给定后,我们能对结果y′做出预测。 这里的预测结果如果是离散值(很多时候是类别类型,比如邮件分类问题中的垃圾邮件/普通邮件,比如用户会/不会购买某商品),那么我们把它叫做分类问题(classification problem);如果预测结果是连续值(比如房价,股票价格等等),那么我们把它叫做回归问题(regression problem)。 有一系列的机器学习算法是用以解决监督学习问题的,比如最经典的用于分类问题的朴素贝叶斯、逻辑回归、支持向量机等等;比如说用于回归问题的线性回归等等。 - 无监督学习 有另外一类问题,给我们的样本并没有给出『标签/标准答案』,就是一系列的样本。而我们需要做的事情是,在一些样本中抽取出通用的规则。这叫做`『无监督学习』`。包括关联规则和聚类算法在内的一系列机器学习算法都属于这个范畴。 - 半监督学习 这类问题给出的训练数据,有一部分有标签,有一部分没有标签。我们想学习出数据组织结构的同时,也能做相应的预测。此类问题相对应的机器学习算法有自训练(Self-Training)、直推学习(Transductive Learning)、生成式模型(Generative Model)等。 总体说来,最常见是前两类问题,而对应前两类问题的一些机器学习算法如下: ![机器学习算法](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430bba31d.png "") **2.2 从算法的功能角度分类** 我们也可以从算法的共性(比如功能,运作方式)角度对机器学习算法分类。下面我们根据算法的共性去对它们归个类。不过需要注意的是,我们下面的归类方法可能对分类和回归有比较强的倾向性,而这两类问题也是最常遇到的。 #### 2.2.1 回归算法(Regression Algorithms) ![回归算法](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430bddb58.png "") 回归算法是一种通过最小化预测值与实际结果值之间的差距,而得到输入特征之间的最佳组合方式的一类算法。对于连续值预测有线性回归等,而对于离散值/类别预测,我们也可以把逻辑回归等也视作回归算法的一种,常见的回归算法如下: - Ordinary Least Squares Regression (OLSR) - Linear Regression - Logistic Regression - Stepwise Regression - Locally Estimated Scatterplot Smoothing (LOESS) - Multivariate Adaptive Regression Splines (MARS) #### 2.2.2 基于实例的算法(Instance-based Algorithms) ![基于实例的算法](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430beea5b.png "") 这里所谓的基于实例的算法,我指的是我们最后建成的模型,对原始数据样本实例依旧有很强的依赖性。这类算法在做预测决策时,一般都是使用某类相似度准则,去比对待预测的样本和原始样本的相近度,再给出相应的预测结果。常见的基于实例的算法有: - k-Nearest Neighbour (kNN) - Learning Vector Quantization (LVQ) - Self-Organizing Map (SOM) - Locally Weighted Learning (LWL) #### 2.2.3 决策树类算法(Decision Tree Algorithms) ![决策树类算法](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430c0b82e.png "") 决策树类算法,会基于原始数据特征,构建一颗包含很多决策路径的树。预测阶段选择路径进行决策。常见的决策树算法包括: - Classification and Regression Tree (CART) - Iterative Dichotomiser 3 (ID3) - C4.5 and C5.0 (different versions of a powerful approach) - Chi-squared Automatic Interaction Detection (CHAID) - M5 - Conditional Decision Trees #### 2.2.4 贝叶斯类算法(Bayesian Algorithms) ![贝叶斯类算法](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430c210c7.png "") 这里说的贝叶斯类算法,指的是在分类和回归问题中,隐含使用了贝叶斯原理的算法。包括: - Naive Bayes - Gaussian Naive Bayes - Multinomial Naive Bayes - Averaged One-Dependence Estimators (AODE) - Bayesian Belief Network (BBN) - Bayesian Network (BN) #### 2.2.5 聚类算法(Clustering Algorithms) ![聚类算法](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430c350ce.png "") 聚类算法做的事情是,把输入样本聚成围绕一些中心的『数据团』,以发现数据分布结构的一些规律。常用的聚类算法包括: - k-Means - Hierarchical Clustering - Expectation Maximisation (EM) #### 2.2.6 关联规则算法(Association Rule Learning Algorithms) ![关联规则算法](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430c48c34.png "") 关联规则算法是这样一类算法:它试图抽取出,最能解释观察到的训练样本之间关联关系的规则,也就是获取一个事件和其他事件之间依赖或关联的知识,常见的关联规则算法有: - Apriori algorithm - Eclat algorithm #### 2.2.7 人工神经网络类算法(Artificial Neural Network Algorithms) ![人工神经网络类算法](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430c6599d.png "") 这是受人脑神经元工作方式启发而构造的一类算法。需要提到的一点是,我把『深度学习』单拎出来了,这里说的人工神经网络偏向于更传统的感知算法,主要包括: - Perceptron - Back-Propagation - Radial Basis Function Network (RBFN) #### 2.2.8 深度学习(Deep Learning Algorithms) ![深度学习](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430c78c42.png "") 深度学习是近年来非常火的机器学习领域,相对于上面列的人工神经网络算法,它通常情况下,有着更深的层次和更复杂的结构。有兴趣的同学可以看看我们另一个系列[机器学习与计算机视觉](http://blog.csdn.net/column/details/dl-computer-vision.html),最常见的深度学习算法包括: - Deep Boltzmann Machine (DBM) - Deep Belief Networks (DBN) - Convolutional Neural Network (CNN) - Stacked Auto-Encoders #### 2.2.9 降维算法(Dimensionality Reduction Algorithms) ![降维算法](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430c887d5.png "") 从某种程度上说,降维算法和聚类其实有点类似,因为它也在试图发现原始训练数据的固有结构,但是降维算法在试图,用更少的信息(更低维的信息)总结和描述出原始信息的大部分内容。 有意思的是,降维算法一般在数据的可视化,或者是降低数据计算空间有很大的作用。它作为一种机器学习的算法,很多时候用它先处理数据,再灌入别的机器学习算法学习。主要的降维算法包括: - Principal Component Analysis (PCA) - Principal Component Regression (PCR) - Partial Least Squares Regression (PLSR) - Sammon Mapping - Multidimensional Scaling (MDS) - Linear Discriminant Analysis (LDA) - Mixture Discriminant Analysis (MDA) - Quadratic Discriminant Analysis (QDA) - Flexible Discriminant Analysis (FDA) #### 2.2.10 模型融合算法(Ensemble Algorithms) ![模型融合算法](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430ca154a.png "") 严格意义上来说,这不算是一种机器学习算法,而更像是一种优化手段/策略,它通常是结合多个简单的弱机器学习算法,去做更可靠的决策。拿分类问题举个例,直观的理解,就是单个分类器的分类是可能出错,不可靠的,但是如果多个分类器投票,那可靠度就会高很多。常用的模型融合增强方法包括: - Random Forest - Boosting - Bootstrapped Aggregation (Bagging) - AdaBoost - Stacked Generalization (blending) - Gradient Boosting Machines (GBM) - Gradient Boosted Regression Trees (GBRT) **2.3 机器学习算法使用图谱** scikit-learn作为一个丰富的python机器学习库,实现了绝大多数机器学习的算法,有相当多的人在使用,于是我这里很无耻地把machine learning cheat sheet for sklearn搬过来了,原文可以看[这里](http://peekaboo-vision.blogspot.de/2013/01/machine-learning-cheat-sheet-for-scikit.html)。哈哈,既然讲机器学习,我们就用机器学习的语言来解释一下,这是针对实际应用场景的各种条件限制,对scikit-learn里完成的算法构建的一颗决策树,每一组条件都是对应一条路径,能找到相对较为合适的一些解决方法,具体如下: ![sklearn机器学习算法使用图谱](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430cb983c.png "") 首先样本量如果非常少的话,其实所有的机器学习算法都没有办法从里面『学到』通用的规则和模式,so多弄点数据是王道。然后根据问题是有/无监督学习和连续值/离散值预测,分成了`分类`、`聚类`、`回归`和`维度约减`四个方法类,每个类里根据具体情况的不同,又有不同的处理方法。 ### 3. 机器学习问题解决思路 上面带着代价走马观花过了一遍机器学习的若干算法,下面我们试着总结总结在拿到一个实际问题的时候,如果着手使用机器学习算法去解决问题,其中的一些注意点以及核心思路。主要包括以下内容: - **拿到数据后怎么了解数据(可视化)** - **选择最贴切的机器学习算法** - **定位模型状态(过/欠拟合)以及解决方法** - **大量极的数据的特征分析与可视化** - **各种损失函数(loss function)的优缺点及如何选择** 多说一句,这里写的这个小教程,主要是作为一个通用的建议和指导方案,你不一定要严格按照这个流程解决机器学习问题。 **3.1 数据与可视化** 我们先使用scikit-learn的make_classification函数来生产一份分类数据,然后模拟一下拿到实际数据后我们需要做的事情。 ~~~ #numpy科学计算工具箱 import numpy as np #使用make_classification构造1000个样本,每个样本有20个feature from sklearn.datasets import make_classification X, y = make_classification(1000, n_features=20, n_informative=2, n_redundant=2, n_classes=2, random_state=0) #存为dataframe格式 from pandas import DataFrame df = DataFrame(np.hstack((X, y[:, None])),columns = range(20) + ["class"]) ~~~ 我们生成了一份包含1000个分类数据样本的数据集,每个样本有20个数值特征。同时我们把数据存储至pandas中的`DataFrame`数据结构中。我们取前几行的数据看一眼: ~~~ df[:6] ~~~ ![前6行](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430d0d5c5.png "") 不幸的是,肉眼看数据,尤其是维度稍微高点的时候,很有可能看花了也看不出看不出任何线索。幸运的是,我们对于图像的理解力,比数字好太多,而又有相当多的工具可以帮助我们『可视化』数据分布。 > 我们在处理任何数据相关的问题时,了解数据都是很有必要的,而可视化可以帮助我们更好地直观理解数据的分布和特性 数据的可视化有很多工具包可以用,比如下面我们用来做数据可视化的工具包[Seaborn](http://stanford.edu/~mwaskom/software/seaborn/)。最简单的可视化就是数据散列分布图和柱状图,这个可以用Seanborn的`pairplot`来完成。以下图中2种颜色表示2种不同的类,因为20维的可视化没有办法在平面表示,我们取出了一部分维度,两两组成pair看数据在这2个维度平面上的分布状况,代码和结果如下: ~~~ import matplotlib.pyplot as plt import seaborn as sns #使用pairplot去看不同特征维度pair下数据的空间分布状况 _ = sns.pairplot(df[:50], vars=[8, 11, 12, 14, 19], hue="class", size=1.5) plt.show() ~~~ ![pair_plot下数据分布状况](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430d35317.png "") 我们从散列图和柱状图上可以看出,确实有些维度的特征相对其他维度,有更好的区分度,比如第11维和14维看起来很有区分度。这两个维度上看,数据点是近似线性可分的。而12维和19维似乎呈现出了很高的负相关性。接下来我们用Seanborn中的`corrplot`来计算计算各维度特征之间(以及最后的类别)的相关性。代码和结果图如下: ~~~ import matplotlib.pyplot as plt plt.figure(figsize=(12, 10)) _ = sns.corrplot(df, annot=False) plt.show() ~~~ ![各位特征相关性](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430d53bcd.png "") 相关性图很好地印证了我们之前的想法,可以看到第11维特征和第14维特征和类别有极强的相关性,同时它们俩之间也有极高的相关性。而第12维特征和第19维特征却呈现出极强的负相关性。强相关的特征其实包含了一些冗余的特征,而除掉上图中颜色较深的特征,其余特征包含的信息量就没有这么大了,它们和最后的类别相关度不高,甚至各自之间也没什么先惯性。 插一句,这里的维度只有20,所以这个相关度计算并不费太大力气,然而实际情形中,你完全有可能有远高于这个数字的特征维度,同时样本量也可能多很多,那种情形下我们可能要先做一些处理,再来实现可视化了。别着急,一会儿我们会讲到。 **3.2 机器学习算法选择** 数据的情况我们大致看了一眼,确定一些特征维度之后,我们可以考虑先选用机器学习算法做一个baseline的系统出来了。这里我们继续参照上面提到过的[机器学习算法使用图谱](http://1.bp.blogspot.com/-ME24ePzpzIM/UQLWTwurfXI/AAAAAAAAANw/W3EETIroA80/s1600/drop_shadows_background.png)。 我们只有1000个数据样本,是分类问题,同时是一个有监督学习,因此我们根据图谱里教的方法,使用`LinearSVC`(support vector classification with linear kernel)试试。注意,`LinearSVC`需要选择正则化方法以缓解过拟合问题;我们这里选择使用最多的L2正则化,并把惩罚系数C设为10。我们改写一下sklearn中的学习曲线绘制函数,画出训练集和交叉验证集上的得分: ~~~ from sklearn.svm import LinearSVC from sklearn.learning_curve import learning_curve #绘制学习曲线,以确定模型的状况 def plot_learning_curve(estimator, title, X, y, ylim=None, cv=None, train_sizes=np.linspace(.1, 1.0, 5)): """ 画出data在某模型上的learning curve. 参数解释 ---------- estimator : 你用的分类器。 title : 表格的标题。 X : 输入的feature,numpy类型 y : 输入的target vector ylim : tuple格式的(ymin, ymax), 设定图像中纵坐标的最低点和最高点 cv : 做cross-validation的时候,数据分成的份数,其中一份作为cv集,其余n-1份作为training(默认为3份) """ plt.figure() train_sizes, train_scores, test_scores = learning_curve( estimator, X, y, cv=5, n_jobs=1, train_sizes=train_sizes) train_scores_mean = np.mean(train_scores, axis=1) train_scores_std = np.std(train_scores, axis=1) test_scores_mean = np.mean(test_scores, axis=1) test_scores_std = np.std(test_scores, axis=1) plt.fill_between(train_sizes, train_scores_mean - train_scores_std, train_scores_mean + train_scores_std, alpha=0.1, color="r") plt.fill_between(train_sizes, test_scores_mean - test_scores_std, test_scores_mean + test_scores_std, alpha=0.1, color="g") plt.plot(train_sizes, train_scores_mean, 'o-', color="r", label="Training score") plt.plot(train_sizes, test_scores_mean, 'o-', color="g", label="Cross-validation score") plt.xlabel("Training examples") plt.ylabel("Score") plt.legend(loc="best") plt.grid("on") if ylim: plt.ylim(ylim) plt.title(title) plt.show() #少样本的情况情况下绘出学习曲线 plot_learning_curve(LinearSVC(C=10.0), "LinearSVC(C=10.0)", X, y, ylim=(0.8, 1.01), train_sizes=np.linspace(.05, 0.2, 5)) ~~~ ![学习曲线1](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430d6db86.png "") 这幅图上,我们发现随着样本量的增加,训练集上的得分有一定程度的下降,交叉验证集上的得分有一定程度的上升,但总体说来,两者之间有很大的差距,训练集上的准确度远高于交叉验证集。这其实意味着我们的模型处于过拟合的状态,也即模型太努力地刻画训练集,一不小心把很多噪声的分布也拟合上了,导致在新数据上的泛化能力变差了。 #### 3.2.1 过拟合的定位与解决 问题来了,过拟合咋办? 针对过拟合,有几种办法可以处理: - **增大样本量** 这个比较好理解吧,过拟合的主要原因是模型太努力地去记住训练样本的分布状况,而加大样本量,可以使得训练集的分布更加具备普适性,噪声对整体的影响下降。恩,我们提高点样本量试试: ~~~ #增大一些样本量 plot_learning_curve(LinearSVC(C=10.0), "LinearSVC(C=10.0)", X, y, ylim=(0.8, 1.1), train_sizes=np.linspace(.1, 1.0, 5)) ~~~ ![学习曲线2](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430d7f67b.png "") 是不是发现问题好了很多?随着我们增大训练样本量,我们发现训练集和交叉验证集上的得分差距在减少,最后它们已经非常接近了。增大样本量,最直接的方法当然是想办法去采集相同场景下的新数据,如果实在做不到,也可以试试在已有数据的基础上做一些人工的处理生成新数据(比如图像识别中,我们可能可以对图片做镜像变换、旋转等等),当然,这样做一定要谨慎,强烈建议想办法采集真实数据。 - **减少特征的量(只用我们觉得有效的特征)** 比如在这个例子中,我们之前的数据可视化和分析的结果表明,第11和14维特征包含的信息对识别类别非常有用,我们可以只用它们。 ~~~ plot_learning_curve(LinearSVC(C=10.0), "LinearSVC(C=10.0) Features: 11&14", X[:, [11, 14]], y, ylim=(0.8, 1.0), train_sizes=np.linspace(.05, 0.2, 5)) ~~~ ![特征选择后](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430d8e1f0.png "") 从上图上可以看出,过拟合问题也得到一定程度的缓解。不过我们这是自己观察后,手动选出11和14维特征。那能不能自动进行特征组合和选择呢,其实我们当然可以遍历特征的组合样式,然后再进行特征选择(前提依旧是这里特征的维度不高,如果高的话,遍历所有的组合是一个非常非常非常耗时的过程!!): ~~~ from sklearn.pipeline import Pipeline from sklearn.feature_selection import SelectKBest, f_classif # SelectKBest(f_classif, k=2) 会根据Anova F-value选出 最好的k=2个特征 plot_learning_curve(Pipeline([("fs", SelectKBest(f_classif, k=2)), # select two features ("svc", LinearSVC(C=10.0))]), "SelectKBest(f_classif, k=2) + LinearSVC(C=10.0)", X, y, ylim=(0.8, 1.0), train_sizes=np.linspace(.05, 0.2, 5)) ~~~ ![自动特征选择](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430da3270.png "") 如果你自己跑一下程序,会发现在我们自己手造的这份数据集上,这个特征筛选的过程超级顺利,但依旧像我们之前提过的一样,这是因为特征的维度不太高。 从另外一个角度看,我们之所以做特征选择,是想降低模型的复杂度,而更不容易刻画到噪声数据的分布。从这个角度出发,我们还可以有(1)多项式你和模型中降低多项式次数 (2)神经网络中减少神经网络的层数和每层的结点数 (c)SVM中增加RBF-kernel的bandwidth等方式来降低模型的复杂度。 话说回来,即使以上提到的办法降低模型复杂度后,好像能在一定程度上缓解过拟合,但是我们一般还是不建议一遇到过拟合,就用这些方法处理,优先用下面的方法: - **增强正则化作用(比如说这里是减小LinearSVC中的C参数)** 正则化是我认为在不损失信息的情况下,最有效的缓解过拟合现象的方法。 ~~~ plot_learning_curve(LinearSVC(C=0.1), "LinearSVC(C=0.1)", X, y, ylim=(0.8, 1.0), train_sizes=np.linspace(.05, 0.2, 5)) ~~~ ![调整正则化参数](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430db9f8e.png "") 调整正则化系数后,发现确实过拟合现象有一定程度的缓解,但依旧是那个问题,我们现在的系数是自己敲定的,有没有办法可以自动选择最佳的这个参数呢?可以。我们可以在交叉验证集上做grid-search查找最好的正则化系数(对于大数据样本,我们依旧需要考虑时间问题,这个过程可能会比较慢): ~~~ from sklearn.grid_search import GridSearchCV estm = GridSearchCV(LinearSVC(), param_grid={"C": [0.001, 0.01, 0.1, 1.0, 10.0]}) plot_learning_curve(estm, "LinearSVC(C=AUTO)", X, y, ylim=(0.8, 1.0), train_sizes=np.linspace(.05, 0.2, 5)) print "Chosen parameter on 100 datapoints: %s" % estm.fit(X[:500], y[:500]).best_params_ ~~~ 在500个点得到的结果是:{‘C’: 0.01} 使用新的C参数,我们再看看学习曲线: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430dceb32.png) 对于特征选择的部分,我打算多说几句,我们刚才看过了用sklearn.feature_selection中的SelectKBest来选择特征的过程,也提到了在高维特征的情况下,这个过程可能会非常非常慢。那我们有别的办法可以进行特征选择吗?比如说,我们的分类器自己能否甄别那些特征是对最后的结果有益的?这里有个实际工作中用到的小技巧。 我们知道: - **l2正则化,它对于最后的特征权重的影响是,尽量打散权重到每个特征维度上,不让权重集中在某些维度上,出现权重特别高的特征。** - **而l1正则化,它对于最后的特征权重的影响是,让特征获得的权重稀疏化,也就是对结果影响不那么大的特征,干脆就拿不着权重。** 那基于这个理论,我们可以把SVC中的正则化替换成l1正则化,让其自动甄别哪些特征应该留下权重。 ~~~ plot_learning_curve(LinearSVC(C=0.1, penalty='l1', dual=False), "LinearSVC(C=0.1, penalty='l1')", X, y, ylim=(0.8, 1.0), train_sizes=np.linspace(.05, 0.2, 5)) ~~~ ![使用l1正则化](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430de1f20.png "") 好了,我们一起来看看最后特征获得的权重: ~~~ estm = LinearSVC(C=0.1, penalty='l1', dual=False) estm.fit(X[:450], y[:450]) # 用450个点来训练 print "Coefficients learned: %s" % est.coef_ print "Non-zero coefficients: %s" % np.nonzero(estm.coef_)[1] ~~~ 得到结果: ~~~ Coefficients learned: [[ 0. 0. 0. 0. 0. 0.01857999 0. 0. 0. 0.004135 0. 1.05241369 0.01971419 0. 0. 0. 0. -0.05665314 0.14106505 0. ]] Non-zero coefficients: [5 9 11 12 17 18] ~~~ 你看,5 9 11 12 17 18这些维度的特征获得了权重,而第11维权重最大,也说明了它影响程度最大。 #### 3.2.2 欠拟合定位与解决 我们再随机生成一份数据[1000*20]的数据(但是分布和之前有变化),重新使用LinearSVC来做分类。 ~~~ #构造一份环形数据 from sklearn.datasets import make_circles X, y = make_circles(n_samples=1000, random_state=2) #绘出学习曲线 plot_learning_curve(LinearSVC(C=0.25),"LinearSVC(C=0.25)",X, y, ylim=(0.5, 1.0),train_sizes=np.linspace(.1, 1.0, 5)) ~~~ ![环形数据的学习曲线](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430e04239.png "") 简直烂出翔了有木有,二分类问题,我们做随机猜测,准确率都有0.5,这比随机猜测都高不了多少!!!怎么办? 不要盲目动手收集更多资料,或者调整正则化参数。我们从学习曲线上其实可以看出来,训练集上的准确度和交叉验证集上的准确度都很低,这其实就对应了我们说的『欠拟合』状态。别急,我们回到我们的数据,还是可视化看看: ~~~ f = DataFrame(np.hstack((X, y[:, None])), columns = range(2) + ["class"]) _ = sns.pairplot(df, vars=[0, 1], hue="class", size=3.5) ~~~ ![环形数据可视化](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430e15a06.png "") 你发现什么了,数据根本就没办法线性分割!!!,所以你再找更多的数据,或者调整正则化参数,都是无济于事的!!! 那我们又怎么解决欠拟合问题呢?通常有下面一些方法: - **调整你的特征(找更有效的特征!!)** 比如说我们观察完现在的数据分布,然后我们先对数据做个映射: ~~~ # 加入原始特征的平方项作为新特征 X_extra = np.hstack((X, X[:, [0]]**2 + X[:, [1]]**2)) plot_learning_curve(LinearSVC(C=0.25), "LinearSVC(C=0.25) + distance feature", X_extra, y, ylim=(0.5, 1.0), train_sizes=np.linspace(.1, 1.0, 5)) ~~~ ![平方映射后的准确度](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430e2c9b9.png "") 卧槽,少年,这准确率,被吓尿了有木有啊!!!所以你看,选用的特征影响太大了,当然,我们这里是人工模拟出来的数据,分布太明显了,实际数据上,会比这个麻烦一些,但是在特征上面下的功夫还是很有回报的。 - **使用更复杂一点的模型(比如说用非线性的核函数)** 我们对模型稍微调整了一下,用了一个复杂一些的非线性rbf kernel: ~~~ from sklearn.svm import SVC # note: we use the original X without the extra feature plot_learning_curve(SVC(C=2.5, kernel="rbf", gamma=1.0), "SVC(C=2.5, kernel='rbf', gamma=1.0)",X, y, ylim=(0.5, 1.0), train_sizes=np.linspace(.1, 1.0, 5)) ~~~ ![rbf核SVM学习曲线](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430e3d804.png "") 你看,效果依旧很赞。 **3.3 关于大数据样本集和高维特征空间** 我们在小样本的toy dataset上,怎么捣鼓都有好的方法。但是当数据量和特征样本空间膨胀非常厉害时,很多东西就没有那么好使了,至少是一个很耗时的过程。举个例子说,我们现在重新生成一份数据集,但是这次,我们生成更多的数据,更高的特征维度,而分类的类别也提高到5。 #### 3.3.1 大数据情形下的模型选择与学习曲线 在上面提到的那样一份数据上,我们用LinearSVC可能就会有点慢了,我们注意到[机器学习算法使用图谱](http://1.bp.blogspot.com/-ME24ePzpzIM/UQLWTwurfXI/AAAAAAAAANw/W3EETIroA80/s1600/drop_shadows_background.png)推荐我们使用`SGDClassifier`。其实本质上说,这个模型也是一个线性核函数的模型,不同的地方是,它使用了随机梯度下降做训练,所以每次并没有使用全部的样本,收敛速度会快很多。再多提一点,`SGDClassifier`对于特征的幅度非常敏感,也就是说,我们在把数据灌给它之前,应该先对特征做幅度调整,当然,用sklearn的`StandardScaler`可以很方便地完成这一点。 `StandardScaler`每次只使用一部分(mini-batch)做训练,在这种情况下,我们使用交叉验证(cross-validation)并不是很合适,我们会使用相对应的progressive validation:简单解释一下,estimator每次只会拿下一个待训练batch在本次做评估,然后训练完之后,再在这个batch上做一次评估,看看是否有优化。 ~~~ #生成大样本,高纬度特征数据 X, y = make_classification(200000, n_features=200, n_informative=25, n_redundant=0, n_classes=10, class_sep=2, random_state=0) #用SGDClassifier做训练,并画出batch在训练前后的得分差 from sklearn.linear_model import SGDClassifier est = SGDClassifier(penalty="l2", alpha=0.001) progressive_validation_score = [] train_score = [] for datapoint in range(0, 199000, 1000): X_batch = X[datapoint:datapoint+1000] y_batch = y[datapoint:datapoint+1000] if datapoint > 0: progressive_validation_score.append(est.score(X_batch, y_batch)) est.partial_fit(X_batch, y_batch, classes=range(10)) if datapoint > 0: train_score.append(est.score(X_batch, y_batch)) plt.plot(train_score, label="train score") plt.plot(progressive_validation_score, label="progressive validation score") plt.xlabel("Mini-batch") plt.ylabel("Score") plt.legend(loc='best') plt.show() ~~~ 得到如下的结果: ![SGDClassifier学习曲线](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430e53787.png "") 从这个图上的得分,我们可以看出在50个mini-batch迭代之后,数据上的得分就已经变化不大了。但是好像得分都不太高,所以我们猜测一下,这个时候我们的数据,处于欠拟合状态。我们刚才在小样本集合上提到了,如果欠拟合,我们可以使用更复杂的模型,比如把核函数设置为非线性的,但遗憾的是像rbf核函数是没有办法和`SGDClassifier`兼容的。因此我们只能想别的办法了,比如这里,我们可以把`SGDClassifier`整个替换掉了,用`多层感知神经网`来完成这个任务,我们之所以会想到`多层感知神经网`,是因为它也是一个用随机梯度下降训练的算法,同时也是一个非线性的模型。当然根据[机器学习算法使用图谱](http://1.bp.blogspot.com/-ME24ePzpzIM/UQLWTwurfXI/AAAAAAAAANw/W3EETIroA80/s1600/drop_shadows_background.png),也可以使用核估计(kernel-approximation)来完成这个事情。 #### 3.3.2 大数据量下的可视化 大样本数据的可视化是一个相对比较麻烦的事情,一般情况下我们都要用到降维的方法先处理特征。我们找一个例子来看看,可以怎么做,比如我们数据集取经典的『手写数字集』,首先找个方法看一眼这个图片数据集。 ~~~ #直接从sklearn中load数据集 from sklearn.datasets import load_digits digits = load_digits(n_class=6) X = digits.data y = digits.target n_samples, n_features = X.shape print "Dataset consist of %d samples with %d features each" % (n_samples, n_features) # 绘制数字示意图 n_img_per_row = 20 img = np.zeros((10 * n_img_per_row, 10 * n_img_per_row)) for i in range(n_img_per_row): ix = 10 * i + 1 for j in range(n_img_per_row): iy = 10 * j + 1 img[ix:ix + 8, iy:iy + 8] = X[i * n_img_per_row + j].reshape((8, 8)) plt.imshow(img, cmap=plt.cm.binary) plt.xticks([]) plt.yticks([]) _ = plt.title('A selection from the 8*8=64-dimensional digits dataset') plt.show() ~~~ ![数字示意图](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430e6cebe.png "") 我们总共有1083个训练样本,包含手写数字(0,1,2,3,4,5),每个样本图片中的像素点平铺开都是64位,这个维度显然是没办法直接可视化的。下面我们基于[scikit-learn的示例教程](http://scikit-learn.org/stable/auto_examples/manifold/plot_lle_digits.html#example-manifold-plot-lle-digits-py)对特征用各种方法做降维处理,再可视化。 **随机投射** 我们先看看,把数据随机投射到两个维度上的结果: ~~~ #import所需的package from sklearn import (manifold, decomposition, random_projection) rp = random_projection.SparseRandomProjection(n_components=2, random_state=42) #定义绘图函数 from matplotlib import offsetbox def plot_embedding(X, title=None): x_min, x_max = np.min(X, 0), np.max(X, 0) X = (X - x_min) / (x_max - x_min) plt.figure(figsize=(10, 10)) ax = plt.subplot(111) for i in range(X.shape[0]): plt.text(X[i, 0], X[i, 1], str(digits.target[i]), color=plt.cm.Set1(y[i] / 10.), fontdict={'weight': 'bold', 'size': 12}) if hasattr(offsetbox, 'AnnotationBbox'): # only print thumbnails with matplotlib > 1.0 shown_images = np.array([[1., 1.]]) # just something big for i in range(digits.data.shape[0]): dist = np.sum((X[i] - shown_images) ** 2, 1) if np.min(dist) < 4e-3: # don't show points that are too close continue shown_images = np.r_[shown_images, [X[i]]] imagebox = offsetbox.AnnotationBbox( offsetbox.OffsetImage(digits.images[i], cmap=plt.cm.gray_r), X[i]) ax.add_artist(imagebox) plt.xticks([]), plt.yticks([]) if title is not None: plt.title(title) #记录开始时间 start_time = time.time() X_projected = rp.fit_transform(X) plot_embedding(X_projected, "Random Projection of the digits (time: %.3fs)" % (time.time() - start_time)) ~~~ 结果如下: ![2方向随机投射图](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430e8ab19.png "") **PCA降维** 在维度约减/降维领域有一个非常强大的算法叫做PCA(Principal Component Analysis,主成分分析),它能将原始的绝大多数信息用维度远低于原始维度的几个主成分表示出来。PCA在我们现在的数据集上效果还不错,我们来看看用PCA对原始特征降维至2维后,原始样本在空间的分布状况: ~~~ from sklearn import (manifold, decomposition, random_projection) #TruncatedSVD 是 PCA的一种实现 X_pca = decomposition.TruncatedSVD(n_components=2).fit_transform(X) #记录时间 start_time = time.time() plot_embedding(X_pca,"Principal Components projection of the digits (time: %.3fs)" % (time.time() - start_time)) ~~~ 得到的结果如下: ![PCA后的可视化](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430ea6c91.png "") 我们可以看出,效果还不错,不同的手写数字在2维平面上,显示出了区域集中性。即使它们之间有一定的重叠区域。 如果我们用一些非线性的变换来做降维操作,从原始的64维降到2维空间,效果更好,比如这里我们用到一个技术叫做t-SNE,sklearn的manifold对其进行了实现: ~~~ from sklearn import (manifold, decomposition, random_projection) #降维 tsne = manifold.TSNE(n_components=2, init='pca', random_state=0) start_time = time.time() X_tsne = tsne.fit_transform(X) #绘图 plot_embedding(X_tsne, "t-SNE embedding of the digits (time: %.3fs)" % (time.time() - start_time)) ~~~ ![非线性降维手写数字分布图](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430ec4492.png "") 我们发现结果非常的惊人,似乎这个非线性变换降维过后,仅仅2维的特征,就可以将原始数据的不同类别,在平面上很好地划分开。不过t-SNE也有它的缺点,一般说来,相对于线性变换的降维,它需要更多的计算时间。也不太适合在大数据集上全集使用。 **3.4 损失函数的选择** 损失函数的选择对于问题的解决和优化,非常重要。我们先来看一眼各种不同的损失函数: ~~~ import numpy as np import matplotlib.plot as plt # 改自http://scikit-learn.org/stable/auto_examples/linear_model/plot_sgd_loss_functions.html xmin, xmax = -4, 4 xx = np.linspace(xmin, xmax, 100) plt.plot([xmin, 0, 0, xmax], [1, 1, 0, 0], 'k-', label="Zero-one loss") plt.plot(xx, np.where(xx < 1, 1 - xx, 0), 'g-', label="Hinge loss") plt.plot(xx, np.log2(1 + np.exp(-xx)), 'r-', label="Log loss") plt.plot(xx, np.exp(-xx), 'c-', label="Exponential loss") plt.plot(xx, -np.minimum(xx, 0), 'm-', label="Perceptron loss") plt.ylim((0, 8)) plt.legend(loc="upper right") plt.xlabel(r"Decision function $f(x)$") plt.ylabel("$L(y, f(x))$") plt.show() ~~~ 得到结果图像如下: ![损失函数对比](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430ee4b7d.png "") 不同的损失函数有不同的优缺点: - **0-1损失函数(zero-one loss)**非常好理解,直接对应分类问题中判断错的个数。但是比较尴尬的是它是一个非凸函数,这意味着其实不是那么实用。 - **hinge loss**(SVM中使用到的)的健壮性相对较高(对于异常点/噪声不敏感)。但是它没有那么好的概率解释。 - **log损失函数(log-loss)**的结果能非常好地表征概率分布。因此在很多场景,尤其是多分类场景下,如果我们需要知道结果属于每个类别的置信度,那这个损失函数很适合。缺点是它的健壮性没有那么强,相对hinge loss会对噪声敏感一些。 - **多项式损失函数(exponential loss)**(AdaBoost中用到的)对离群点/噪声非常非常敏感。但是它的形式对于boosting算法简单而有效。 - **感知损失(perceptron loss)**可以看做是hinge loss的一个变种。hinge loss对于判定边界附近的点(正确端)惩罚力度很高。而perceptron loss,只要样本的判定类别结果是正确的,它就是满意的,而不管其离判定边界的距离。优点是比hinge loss简单,缺点是因为不是max-margin boundary,所以得到模型的泛化能力没有hinge loss强。 ### 4. 总结 全文到此就结束了。先走马观花看了一遍机器学习的算法,然后给出了对应scikit-learn的『秘密武器』[机器学习算法使用图谱](http://1.bp.blogspot.com/-ME24ePzpzIM/UQLWTwurfXI/AAAAAAAAANw/W3EETIroA80/s1600/drop_shadows_background.png),紧接着从了解数据(可视化)、选择机器学习算法、定位过/欠拟合及解决方法、大量极的数据可视化和损失函数优缺点与选择等方面介绍了实际机器学习问题中的一些思路和方法。本文和文章[机器学习系列(3)_逻辑回归应用之Kaggle泰坦尼克之灾](http://blog.csdn.net/han_xiaoyang/article/details/49797143)都提及了一些处理实际机器学习问题的思路和方法,有相似和互补之处,欢迎大家参照着看。
';

手把手入门神经网络系列(2)_74行代码实现手写数字识别

最后更新于:2022-04-01 09:52:03

作者: [龙心尘](http://blog.csdn.net/longxinchen_ml?viewmode=contents)&&[寒小阳](http://blog.csdn.net/han_xiaoyang?viewmode=contents) 时间:2015年12月。 出处: [http://blog.csdn.net/longxinchen_ml/article/details/50281247](http://blog.csdn.net/longxinchen_ml/article/details/50281247), [http://blog.csdn.net/han_xiaoyang/article/details/50282141](http://blog.csdn.net/han_xiaoyang/article/details/50282141) 声明:版权所有,转载请联系作者并注明出处,谢谢。 ### 1、引言:不要站在岸上学游泳 “机器学习”是一个很实践的过程。就像刚开始学游泳,你在只在岸上比划一堆规定动作还不如先跳到水里熟悉水性学习来得快。以我们学习“机器学习”的经验来看,很多高大上的概念刚开始不懂也没关系,先写个东西来跑跑,有个感觉了之后再学习那些概念和理论就快多了。如果别人已经做好了轮子,直接拿过来用则更快。因此,本文直接用[Michael Nielsen](http://michaelnielsen.org/)先生的代码([github地址](https://github.com/mnielsen/neural-networks-and-deep-learning.git),[压缩包地址](https://github.com/mnielsen/neural-networks-and-deep-learning/archive/master.zip))作为例子,给大家展现神经网络分析的普遍过程:导入数据,训练模型,优化模型,启发式理解等。 本文假设大家已经了解python的基本语法,并在自己机器上运行过简单python脚本。 ### 2、我们要解决的问题:手写数字识别 手写数字识别是机器学习领域中一个经典的问题,是一个看似对人类很简单却对程序十分复杂的问题。很多早期的验证码就是利用这个特点来区分人类和程序行为的,当然此处就不提12306近乎反人类的奇葩验证码了。 ![验证码图](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430aa9e7a.jpg "") 回到手写数字识别,比如我们要识别出一个手写的“9”,人类可能通过识别“上半部分一个圆圈,右下方引出一条竖线”就能进行判断。但用程序表达就似乎很困难了,你需要考虑非常多的描述方式,考虑非常多的特殊情况,最终发现程序写得非常复杂而且效果不好。 而用(机器学习)神经网络的方法,则提供了另一个思路:获取大量的手写数字的图像,并且已知它们表示的是哪个数字,以此为训练样本集合,自动生成一套模型(如神经网络的对应程序),依靠它来识别新的手写数字。 ![手写数字](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430ac3f8e.jpg "") 本文中采用的数据集就是**著名的“MNIST数据集”**。它的收集者之一是人工智能领域著名的科学家——Yann LeCu。这个数据集有60000个训练样本数据集和10000个测试用例。运用本文展示的单隐层神经网络,就可以达到96%的正确率。 ### 3、图解:解决问题的思路 我们可以用下图展示上面的粗略思路。 ![粗略思路](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430adaffd.jpg "") 但是如何由“训练集”来“生成模型”呢? 在这里我们使用反复推荐的逆推法——假设这个模型已经生成了,它应该满足什么样的特性,再以此特性为条件反过来求出模型。 可以推想而知,被生成的模型应该对于训练集的区分效果非常好,也就是相应的训练误差非常低。比如有一个未知其相应权重和偏移的神经网络,而训练神经网络的过程就是逐步确定这些未知参数的过程,最终使得这些参数确定的模型在训练集上的误差达到最小值。我们将会设计一个数量指标衡量这个误差,如果训练误差没有达到最小,我们将继续调整参数,直到这个指标达到最小。但这样训练出来的模型我们仍无法保证它面对新的数据仍会有这样好的识别效果,就需要用测试集对模型进行考核,得出的测试结果作为对模型的评价。因此,上图就可以细化成下图: ![细化成下图](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430aeaf64.jpg "") 但是,如果我们已经生成了多个模型,怎么从中选出最好的模型?一个自然的思路就是通过比较不同模型在测试集上的误差,挑选出误差最小的模型。这个想法看似没什么问题,但是随着你测试的模型增多,你会觉得用测试集筛选出来的模型也不那么可信。比如我们增加一个神经网络的隐藏层节点,就会产生新的对应权重,产生一个新的模型。但是我也不知道增加多少个节点是合适的,所以比较全面的想法就是尝试测试不同的节点数x∈(1,2,3,4,…,100), 来观察这些不同模型的测试误差,并挑出误差最小的模型。这时我们发现我们的模型其实多出来了一个参数x, 我们挑选模型的过程就是确定最优化的参数x 的过程。这个分析过程与上面训练参数的思路如出一辙!只是这个过程是基于同一个测试集,而不训练集。那么,不同的神经网络的层数是不是也是一个新的参数y∈(1,2,3,4,…,100), 也要经过这么个过程来“训练”? 我们会发现我们之前生成模型过程中很多不变的部分其实都是可以变换调节的,这些也是新的参数,比如训练次数、梯度下降过程的步长、规范化参数、学习回合数、minibatch 值等等,我们把他们叫做超参数。超参数是影响所求参数最终取值的参数,是机器学习模型里面的框架参数,可以理解成参数的参数,它们通常是手工设定,不断试错调整的,或者对一系列穷举出来的参数组合一通进行枚举(网格搜索)来确定。但无论如何,这也是基于同样一个数据集反复验证优化的结果。在这个数据集上最后的结果并不一定在新的数据继续有效。所以为了评估这个模型的识别效果,就需要用新的测试集对模型进行考核,得出的测试结果作为对模型的评价。这个新的测试集我们就直接叫“测试集”,之前那个用于筛选超参数的测试集,我们就叫做“交叉验证集”。筛选模型的过程其实就是交叉验证的过程。 所以,规范的方法的是将数据集拆分成三个集合:训练集、交叉验证集、测试集,然后依次训练参数、超参数,最终得到最优的模型。 因此,上图可以进一步细化成下图: ![进一步细化成下图](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430b0adf5.jpg "") 或者下图: ![进一步细化成下图2](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430b228b7.jpg "") 可见机器学习过程是一个反复迭代不断优化的过程。其中很大一部分工作是在调整参数和超参数。 ### 4、先跑跑再说:初步运行代码 Michael Nielsen的代码封装得很好,只需以下5行命令就可以生成神经网络并测试结果,并达到94.76%的正确率!。 ~~~ import mnist_loader import network # 将数据集拆分成三个集合:训练集、交叉验证集、测试集 training_data, validation_data, test_data = mnist_loader.load_data_wrapper() # 生成神经网络对象,神经网络结构为三层,每层节点数依次为(784, 30, 10) net = network.Network([784, 30, 10]) # 用(mini-batch)梯度下降法训练神经网络(权重与偏移),并生成测试结果。 # 训练回合数=30, 用于随机梯度下降法的最小样本数=10,学习率=3.0 net.SGD(training_data, 30, 10, 3.0, test_data=test_data) ~~~ - 第一个命令的功能是:将数据集拆分成三个集合:训练集、交叉验证集、测试集。 - 第二个命令的功能是:生成神经网络对象,神经网络结构为三层,每层节点数依次为(784, 30, 10)。 - 第三个命令的功能是:用(mini-batch)梯度下降法训练神经网络(权重与偏移),并生成测试结果。 - 该命令设定了三个超参数:训练回合数=30, 用于随机梯度下降法的最小样本数(mini-batch-size)=10,步长=3.0。 本文并不打算详细解释随机梯度下降法的细节,感兴趣的同学请阅读前文[《深度学习与计算机视觉系列(4)_最优化与随机梯度下降》](http://blog.csdn.net/longxinchen_ml/article/details/50178845) 总共的输出结果如下: ~~~ Epoch 0: 9045 / 10000 Epoch 1: 9207 / 10000 Epoch 2: 9273 / 10000 Epoch 3: 9302 / 10000 Epoch 4: 9320 / 10000 Epoch 5: 9320 / 10000 Epoch 6: 9366 / 10000 Epoch 7: 9387 / 10000 Epoch 8: 9427 / 10000 Epoch 9: 9402 / 10000 Epoch 10: 9400 / 10000 Epoch 11: 9442 / 10000 Epoch 12: 9448 / 10000 Epoch 13: 9441 / 10000 Epoch 14: 9443 / 10000 Epoch 15: 9479 / 10000 Epoch 16: 9459 / 10000 Epoch 17: 9446 / 10000 Epoch 18: 9467 / 10000 Epoch 19: 9470 / 10000 Epoch 20: 9459 / 10000 Epoch 21: 9484 / 10000 Epoch 22: 9479 / 10000 Epoch 23: 9475 / 10000 Epoch 24: 9482 / 10000 Epoch 25: 9489 / 10000 Epoch 26: 9489 / 10000 Epoch 27: 9478 / 10000 Epoch 28: 9480 / 10000 Epoch 29: 9476 / 10000 ~~~ ### 5、神经网络如何识别手写数字:启发式理解 首先,我们解释一下神经网络每层的功能。 ![神经网络每层](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430b39ea4.jpg "") 第一层是输入层。因为mnist数据集中每一个手写数字样本是一个28X28像素的图像,因此对于每一个样本,其输入的信息就是每一个像素对应的灰度,总共有28*28=784个像素,故这一层有784个节点。 第三层是输出层。因为阿拉伯数字总共有10个,我们就要将样本分成10个类别,因此输出层我们采用10个节点。当样本属于某一类(某个数字)的时候,则该类(该数字)对应的节点为1,而剩下9个节点为0,如[0,0,0,1,0,0,0,0,0,0]。 因此,我们每一个样本(手写数字的图像)可以用一个超长的784维的向量表示其特征,而用一个10维向量表示该样本所属的类别(代表的真实数字),或者叫做标签。 mnist的数据就是这样表示的。所以,如果你想看训练集中第n个样本的784维特征向量,直接看training_data[n][0]就可以找到,而要看其所属的标签,看training_data[n][1]就够了。 那么,第二层神经网络所代表的意义怎么理解?这其实是很难的。但是我们可以有一个启发式地理解,比如用中间层的某一个节点表示图像中的某一个小区域的特定图像。这样,我们可以假设中间层的头4个节点依次用来识别图像左上、右上、左下、右下4个区域是否存在这样的特征的。 ![左上](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430b58d27.jpg "") ![右上、左下、右下](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430b67e12.jpg "") 如果这四个节点的值都很高,说明这四个区域同时满足这些特征。将以上的四个部分拼接起来,我们会发现,输入样本很可能就是一个手写“0”! ![0](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430b769f3.jpg "") 因此,同一层的几个神经元同时被激活了意味着输入样本很可能是某个数字。 当然,这只是对神经网络作用机制的一个启发式理解。真实的过程却并不一定是这样。但通过启发式理解,我们可以对神经网络作用机制有一个更加直观的认识。 由此可见,神经网络能够识别手写数字的关键是它有能够对特定的图像激发特定的节点。而神经网络之所以能够针对性地激发这些节点,关键是它具有能够适应相关问题场景的权重和偏移。那这些权重和偏移如何训练呢? ### 6、神经网络如何训练:进一步阅读代码 上文已经图解的方式介绍了机器学习解决问题的一般思路,但是具体到神经网络将是如何训练呢? 其实最快的方式是直接阅读代码。我们将代码的结构用下图展示出来,运用其内置函数名表示基本过程,发现与我们上文分析的思路一模一样: ![分析思路](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430b85966.jpg "") 简单解释一下,在神经网络模型中: - 所需要求的关键参数就是:神经网络的权重(self.weights)和偏移(self.biases)。 - 超参数是:隐藏层的节点数=30,训练回合数(epochs)=30, 用于随机梯度下降法的最小样本数(mini_batch_size)=10,步长(eta)=3.0。 - 用随机梯度下降法调整参数: ![梯度下降](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430ba6a2d.jpg "") - 用反向传播法求出随机梯度下降法所需要的梯度(偏导数): backprop() - 用输出向量减去标签向量衡量训练误差:cost_derivative() = output_activations-y 全部代码如下(去掉注释之后,只有74行): ~~~ """ network.py ~~~~~~~~~~ A module to implement the stochastic gradient descent learning algorithm for a feedforward neural network. Gradients are calculated using backpropagation. Note that I have focused on making the code simple, easily readable, and easily modifiable. It is not optimized, and omits many desirable features. """ #### Libraries # Standard library import random # Third-party libraries import numpy as np class Network(object): def __init__(self, sizes): """The list ``sizes`` contains the number of neurons in the respective layers of the network. For example, if the list was [2, 3, 1] then it would be a three-layer network, with the first layer containing 2 neurons, the second layer 3 neurons, and the third layer 1 neuron. The biases and weights for the network are initialized randomly, using a Gaussian distribution with mean 0, and variance 1. Note that the first layer is assumed to be an input layer, and by convention we won't set any biases for those neurons, since biases are only ever used in computing the outputs from later layers.""" self.num_layers = len(sizes) self.sizes = sizes self.biases = [np.random.randn(y, 1) for y in sizes[1:]] self.weights = [np.random.randn(y, x) for x, y in zip(sizes[:-1], sizes[1:])] def feedforward(self, a): """Return the output of the network if ``a`` is input.""" for b, w in zip(self.biases, self.weights): a = sigmoid(np.dot(w, a)+b) return a def SGD(self, training_data, epochs, mini_batch_size, eta, test_data=None): """Train the neural network using mini-batch stochastic gradient descent. The ``training_data`` is a list of tuples ``(x, y)`` representing the training inputs and the desired outputs. The other non-optional parameters are self-explanatory. If ``test_data`` is provided then the network will be evaluated against the test data after each epoch, and partial progress printed out. This is useful for tracking progress, but slows things down substantially.""" if test_data: n_test = len(test_data) n = len(training_data) for j in xrange(epochs): random.shuffle(training_data) mini_batches = [ training_data[k:k+mini_batch_size] for k in xrange(0, n, mini_batch_size)] for mini_batch in mini_batches: self.update_mini_batch(mini_batch, eta) if test_data: print "Epoch {0}: {1} / {2}".format( j, self.evaluate(test_data), n_test) else: print "Epoch {0} complete".format(j) def update_mini_batch(self, mini_batch, eta): """Update the network's weights and biases by applying gradient descent using backpropagation to a single mini batch. The ``mini_batch`` is a list of tuples ``(x, y)``, and ``eta`` is the learning rate.""" nabla_b = [np.zeros(b.shape) for b in self.biases] nabla_w = [np.zeros(w.shape) for w in self.weights] for x, y in mini_batch: delta_nabla_b, delta_nabla_w = self.backprop(x, y) nabla_b = [nb+dnb for nb, dnb in zip(nabla_b, delta_nabla_b)] nabla_w = [nw+dnw for nw, dnw in zip(nabla_w, delta_nabla_w)] self.weights = [w-(eta/len(mini_batch))*nw for w, nw in zip(self.weights, nabla_w)] self.biases = [b-(eta/len(mini_batch))*nb for b, nb in zip(self.biases, nabla_b)] def backprop(self, x, y): """Return a tuple ``(nabla_b, nabla_w)`` representing the gradient for the cost function C_x. ``nabla_b`` and ``nabla_w`` are layer-by-layer lists of numpy arrays, similar to ``self.biases`` and ``self.weights``.""" nabla_b = [np.zeros(b.shape) for b in self.biases] nabla_w = [np.zeros(w.shape) for w in self.weights] # feedforward activation = x activations = [x] # list to store all the activations, layer by layer zs = [] # list to store all the z vectors, layer by layer for b, w in zip(self.biases, self.weights): z = np.dot(w, activation)+b zs.append(z) activation = sigmoid(z) activations.append(activation) # backward pass delta = self.cost_derivative(activations[-1], y) * \ sigmoid_prime(zs[-1]) nabla_b[-1] = delta nabla_w[-1] = np.dot(delta, activations[-2].transpose()) # Note that the variable l in the loop below is used a little # differently to the notation in Chapter 2 of the book. Here, # l = 1 means the last layer of neurons, l = 2 is the # second-last layer, and so on. It's a renumbering of the # scheme in the book, used here to take advantage of the fact # that Python can use negative indices in lists. for l in xrange(2, self.num_layers): z = zs[-l] sp = sigmoid_prime(z) delta = np.dot(self.weights[-l+1].transpose(), delta) * sp nabla_b[-l] = delta nabla_w[-l] = np.dot(delta, activations[-l-1].transpose()) return (nabla_b, nabla_w) def evaluate(self, test_data): """Return the number of test inputs for which the neural network outputs the correct result. Note that the neural network's output is assumed to be the index of whichever neuron in the final layer has the highest activation.""" test_results = [(np.argmax(self.feedforward(x)), y) for (x, y) in test_data] return sum(int(x == y) for (x, y) in test_results) def cost_derivative(self, output_activations, y): """Return the vector of partial derivatives \partial C_x / \partial a for the output activations.""" return (output_activations-y) #### Miscellaneous functions def sigmoid(z): """The sigmoid function.""" return 1.0/(1.0+np.exp(-z)) def sigmoid_prime(z): """Derivative of the sigmoid function.""" return sigmoid(z)*(1-sigmoid(z)) ~~~ ### 7、神经网络如何优化:训练超参数与多种模型对比 由以上分析可知,神经网络只需要74行代码就可以完成编程,**可见机器学习真正困难的地方并不在编程,而在你对数学过程本身,和对它与现实问题的对应关系有深入的理解**。理解深入后,你才能写出这样的程序,并**对其进行精微的调优**。 我们初步的结果已经是94.76%的正确率了。但如果要将准确率提得更高怎么办? 这其实是一个开放的问题,有许多方法都可以尝试。我们这里仅仅是抛砖引玉。 首先,隐藏层只有30个节点。由我们之前对隐藏层的启发式理解可以猜测,神经网络的识别能力其实与隐藏层对一些细节的识别能力正相关。如果**隐藏层的节点**更多的话,其识别能力应该会更强的。那么我们设定100个隐藏层节点试试? ~~~ net = network.Network([784, 100, 10]) net.SGD(training_data, 30, 10, 3.0, test_data=test_data) ~~~ 发现结果如下: ~~~ Epoch 0: 6669 / 10000 Epoch 1: 6755 / 10000 Epoch 2: 6844 / 10000 Epoch 3: 6833 / 10000 Epoch 4: 6887 / 10000 Epoch 5: 7744 / 10000 Epoch 6: 7778 / 10000 Epoch 7: 7876 / 10000 Epoch 8: 8601 / 10000 Epoch 9: 8643 / 10000 Epoch 10: 8659 / 10000 Epoch 11: 8665 / 10000 Epoch 12: 8683 / 10000 Epoch 13: 8700 / 10000 Epoch 14: 8694 / 10000 Epoch 15: 8699 / 10000 Epoch 16: 8715 / 10000 Epoch 17: 8770 / 10000 Epoch 18: 9611 / 10000 Epoch 19: 9632 / 10000 Epoch 20: 9625 / 10000 Epoch 21: 9632 / 10000 Epoch 22: 9651 / 10000 Epoch 23: 9655 / 10000 Epoch 24: 9653 / 10000 Epoch 25: 9658 / 10000 Epoch 26: 9653 / 10000 Epoch 27: 9664 / 10000 Epoch 28: 9655 / 10000 Epoch 29: 9672 / 10000 ~~~ **发现,我们只是改了一个超参数,准确率就从94.76%提升到96.72%!** 这里强调一下,**更加规范的模型调优方法是将多个模型用交叉验证集的结果来横向比较,选出最优模型后再用一个新的测试集来最终评估该模型。**本文为了与之前的结果比较,才采用了测试集而不是交叉验证集。**读者千万不要学博主这样做哈,因为这很有可能会过拟合。这是工程实践中数据挖掘人员经常犯的错误,我们之后会专门写篇博文探讨**。 我们现在回来继续调优我们的模型。那么还有其他的隐藏节点数更合适吗?这个我们也不知道。常见的方法是用几何级数增长的数列(如:10,100,1000,……)去尝试,然后不断确定合适的区间,最终确定一个相对最优的值。 但是即便如此,我们也只尝试了一个超参数,还有其他的超参数没有调优呢。我们于是尝试另一个超参数:**步长**。之前的步长是3.0,但是我们可能觉得学习速率太慢了。那么尝试一个更大的步长试试?比如100? ~~~ net = network.Network([784, 30, 10]) net.SGD(training_data, 30, 10, 100.0, test_data=test_data) ~~~ 发现结果如下: ~~~ Epoch 0: 1002 / 10000 Epoch 1: 1002 / 10000 Epoch 2: 1002 / 10000 Epoch 3: 1002 / 10000 Epoch 4: 1002 / 10000 Epoch 5: 1002 / 10000 Epoch 6: 1002 / 10000 Epoch 7: 1002 / 10000 Epoch 8: 1002 / 10000 Epoch 9: 1002 / 10000 Epoch 10: 1002 / 10000 Epoch 11: 1002 / 10000 Epoch 12: 1001 / 10000 Epoch 13: 1001 / 10000 Epoch 14: 1001 / 10000 Epoch 15: 1001 / 10000 Epoch 16: 1001 / 10000 Epoch 17: 1001 / 10000 Epoch 18: 1001 / 10000 Epoch 19: 1001 / 10000 Epoch 20: 1000 / 10000 Epoch 21: 1000 / 10000 Epoch 22: 999 / 10000 Epoch 23: 999 / 10000 Epoch 24: 999 / 10000 Epoch 25: 999 / 10000 Epoch 26: 999 / 10000 Epoch 27: 999 / 10000 Epoch 28: 999 / 10000 Epoch 29: 999 / 10000 ~~~ 发现准确率低得不忍直视,看来步长设得太长了。根本跑不到最低点。那么我们设定一个小的步长试试?比如0.01。 ~~~ net = network.Network([784, 100, 10]) net.SGD(training_data, 30, 10, 0.001, test_data=test_data) ~~~ 结果如下: ~~~ Epoch 0: 790 / 10000 Epoch 1: 846 / 10000 Epoch 2: 854 / 10000 Epoch 3: 904 / 10000 Epoch 4: 944 / 10000 Epoch 5: 975 / 10000 Epoch 6: 975 / 10000 Epoch 7: 975 / 10000 Epoch 8: 975 / 10000 Epoch 9: 974 / 10000 Epoch 10: 974 / 10000 Epoch 11: 974 / 10000 Epoch 12: 974 / 10000 Epoch 13: 974 / 10000 Epoch 14: 974 / 10000 Epoch 15: 974 / 10000 Epoch 16: 974 / 10000 Epoch 17: 974 / 10000 Epoch 18: 974 / 10000 Epoch 19: 976 / 10000 Epoch 20: 979 / 10000 Epoch 21: 981 / 10000 Epoch 22: 1004 / 10000 Epoch 23: 1157 / 10000 Epoch 24: 1275 / 10000 Epoch 25: 1323 / 10000 Epoch 26: 1369 / 10000 Epoch 27: 1403 / 10000 Epoch 28: 1429 / 10000 Epoch 29: 1451 / 10000 ~~~ 呃,发现准确率同样低得不忍直视。但是有一个优点,准确率是稳步提升的。**说明模型在大方向上应该还是对的。如果在调试模型的时候忽视了这个细节,你可能真的找不到合适的参数。** 可见,我们第一次尝试的神经网络结构的超参数设定还是比较不错的。但是真实的应用场景中,基本没有这样好的运气,很可能刚开始测试出来的结果全是奇葩生物,长得违反常理,就像来自另一个次元似的。这是数据挖掘工程师常见的情况。此时最应该做的,就是遏制住心中数万草泥马的咆哮奔腾,静静地观察测试结果的分布规律,尝试找到些原因,再继续将模型试着调优下去,与此同时,做好从一个坑跳入下一个坑的心理准备。当然,**在机器学习工程师前赴后继的填坑过程中,还是总结出了一些调优规律。我们会在接下来专门写博文分析。** 当然,以上的调优都没有逃出神经网络模型本身的范围。但是可不可能其他的模型效果更好?比如传说中的**支持向量机**?关于支持向量机的解读已经超越了本文的篇幅,我们也考虑专门撰写博文分析。但是在这里我们只是引用一下在[scikit-learn](http://www.csie.ntu.edu.tw/~cjlin/libsvm/)中提供好的接口,底层是用性能更好的C语言封装的著名的[LIBSVM](http://www.csie.ntu.edu.tw/~cjlin/libsvm/)。 相关代码也在Michael Nielsen的文件中。直接引入,并运行一个方法即可。 ~~~ import mnist_svm mnist_svm.svm_baseline() ~~~ 我们看看结果: ~~~ Baseline classifier using an SVM. 9435 of 10000 values correct. ~~~ 94.35%,好像比我们的神经网络低一点啊。看来我们的神经网络模型还是更优秀一些? 然而,实际情况并非如此。因为我们用的只是scikit-learn给支持向量机的设好的默认参数。支持向量机同样有一大堆可调的超参数,以提升模型的效果。 跟据 [Andreas Mueller](http://peekaboo-vision.blogspot.ca/)的[这篇博文](http://peekaboo-vision.blogspot.de/2010/09/mnist-for-ever.html),调整好超参数的支持向量机能够达到98.5%的准确度!比我们刚才最好的神经网络提高了1.8个百分点! 然而,故事并没有结束。2013年,通过深度神经网络,研究者可以达到99.79%的准确度!而且,他们并没有运用很多高深的技术。很多技术在我们接下来的博文中都可以继续介绍。 所以,从目前的准确度来看: > 简单的支持向量机<浅层神经网络<调优的支持向量机<深度神经网络 但还是要提醒一下,炫酷的算法固然重要,但是良好的数据集有时候比算法更重要。Michael Nielsen专门写了一个公式来来表达他们的关系: > 精致的算法 ≤ 简单的算法 + 良好的训练数据 sophisticated algorithm ≤ simple learning algorithm + good training data. 所以为了调优模型,往往要溯源到数据本身,好的数据真的会有好的结果。 ### 8、小结与下期预告 以上我们只是粗略地展示了用神经网络分析问题的基本过程,很多深入的内容并没有展开。我们将会在接下来的博文中进行深入探讨。 在该系列下一篇博文中,我们试图直接探讨深度神经网络的表现能力,并提供一个启发式理解。敬请关注。
';

手把手入门神经网络系列(1)_从初等数学的角度初探神经网络

最后更新于:2022-04-01 09:52:01

作者: [龙心尘](http://blog.csdn.net/longxinchen_ml?viewmode=contents)&&[寒小阳](http://blog.csdn.net/han_xiaoyang?viewmode=contents) 时间:2015年11月。 出处:[http://blog.csdn.net/longxinchen_ml/article/details/50082873](http://blog.csdn.net/longxinchen_ml/article/details/50082873), [http://blog.csdn.net/han_xiaoyang/article/details/50100367](http://blog.csdn.net/han_xiaoyang/article/details/50100367) 声明:版权所有,转载请联系作者并注明出处,谢谢。 ### 1.开场先扔个段子 在互联网广告营销中,经常会有这样的对话: 问:你们的人群标签是什么样的? 答:我们是专门为您订制的look-alike标签! 问:好吧,你们的定向算法能不能说明一下? 答:我们用的是deep learning技术! 一般来说,话题到此,广告主会因为自己理论知识的匮乏而羞愧得无地自容。唯一的办法就是先透过投放广告的方式先学习起来。 ——刘鹏《广告技术公司十大装逼姿势》 像deep learning、DNN、BP神经网络等这些高逼格的词,真的很能吓唬人,但真正的内容什么,很多人却解释得不多,要么说得太晦涩,要么说得太神叨。其实,就我们的粗浅了解,神经网络、深度学习啥的,入门还是很容易的。如果去除外面浮夸的包装,它们的理论基础也相当直白。当然这并不是说神经网络不难,它们真正难的地方是在实践方面,但这也是有方法可处理的。为了浅显易懂地给大家解释这个过程,我们开设了“手把手入门神经网络系列”。 在本篇文章中,我们依然少用公式多画图,只用初等数学的知识解释一下神经网络。 ### 2.神经网络有什么牛逼之处? 机器学习算法这么多,为什么神经网络这么牛逼? 为了解释这个问题,我们呈现了神经网络在分类问题上优于逻辑回归的地方——它几乎可以实现任意复杂的分类边界,无误差地实现训练集上的分类。 然而,这是有代价的:由于其强大的拟合能力,极容易产生过拟合。为了降低过拟合,我们介绍了一种降低过拟合的思路。 在这个过程中,我们尽量解释神经网络每一步操作对应的现实意义和最终目的。可是,神经网络的可解释性往往是个非常大的难题。为此,我们采用了最易于理解的“交集”(逻辑与)、“并集”(逻辑或)神经网络,希望能帮助大家进行启发式理解。 但是,神经网络的问题并没有解决完。之后,我们引出了用神经网络的一些典型问题,我们将在接下来的系列文章对其进行深入探讨。 ### 3.从逻辑回归到神经网络 神经网络主要应用场景之一是分类,我们之前博文中提到的[逻辑回归](http://blog.csdn.net/han_xiaoyang/article/details/49332321)也是解决分类问题的一类机器学习算法,有趣的是,实际上两者有着很紧密的关系,而逻辑回归作为帮助我们理解神经网络的切入口也是极好的,所以我们先从逻辑回归开始。看过本我们前文[《机器学习系列(2)_用初等数学解读逻辑回归》](http://blog.csdn.net/han_xiaoyang/article/details/49332321)的读者盆友应该会有个印象,逻辑回归可以理解成将空间中的点经过一系列几何变换求解损失函数而进行分类的过程,具体过程如图: ![逻辑回归](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430899c64.jpg "") 具体分析一下我们所求的“参数” 的几何意义,(θ1,θ2)就是图中法向量p的方向,θ0 对应着法向量p的起点距离坐标系原点的偏移。在神经网络中,向量(θ1,θ2)也叫做权重,常用w表示;θ0就叫做偏移,常用b表示。 而上面的逻辑回归过程,用神经元的表达方式如下,这就是传说中的“感知器”: ![感知器](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e24308b5df4.jpg "") 其中,z=θ0+θ1X1+θ2X2,a=g(z)=11+e−z ,g(z)叫做激励函数 逻辑回归的激励函数自然就是sigmoid函数,输出值a对应着输入样本点x(X1,X2)在法向量p所指向的那部分空间的概率。本文为了兼容逻辑回归,只考虑sigmoid函数作为激励函数的情况,而且这也是工业界最常见的情况。其他激励函数的选择将会在后面系列文章继续介绍。 补充说明一下,以上只是为了表示清晰而这样画神经网络。在一般的神经网络画法中,激励函数的操作节点会与输出节点写成同一个节点,如下图: ![感知器二](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e24308c40b2.jpg "") 好了,到此为止,神经网络相对于逻辑回归没有提供任何新的知识,就是换了个花样来表示。 但神经网络这样表示有个重大的作用:它方便我们以此为基础做逻辑回归的多层组合嵌套——比如对同样的输入x(X1,X2)可以同时做n个逻辑回归,产生n个输出结果,这就是传说中的“单层感知器”或者叫“无隐层神经网络”。示意图如下: ![无隐层感知器](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e24308d4c49.jpg "") 你还可以对n个逻辑回归的n个输出再做m个逻辑回归,这就是传说中的“单隐层神经网络”。示意图如下: ![单隐层感知器](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e24308e6f9c.jpg "") 如果你愿意,可以继续做下去,子子孙孙无穷匮也: ![多隐层](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430907558.jpg "") 最左边的层叫做输入层,最右边的层叫做输出层,二者之间的所有层叫做隐藏层。 如果层数比较少,就是传说中的SNN(shallow nerual network) “浅层神经网络”;如果层数比较多,就是传说中的DNN(Deep Neural Networks) “深度神经网络”。对“浅层神经网络”的建模叫做“浅层学习”,那么对于“深度神经网络”……“深度学习”! 恭喜你已经会抢答了! 到目前为止,我们没有运用任何更多的数学知识,就已经收获了一大批装逼术语。 如果你愿意,把上面的所有情况叫做“逻辑回归的逻辑回归的逻辑回归的逻辑……”,我也觉得蛮好哈。(喘不过气来了……) ### 4.双隐层神经网络彻底实现复杂分类 我们之前已经探讨过,逻辑回归的比较擅长解决线性可分的问题。对于非线性可分的问题,逻辑回归有一种运用复杂映射函数的思路,但这种思路只做了一次(非线性的)几何变换,就得到了线性可分的情形。可参考下图: ![几何变换非线性](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430922ee4.jpg "") 神经网络的视角则给我们提供了另一种思路,可以连续做几次的几何变换,每次变换都是一些极简单的逻辑回归,最终达到线性可分情形。看起来似乎很不错哦。比如下面这个貌似很简单的问题,我们怎么找出他的分离边界? ![异或](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e243093c9e9.jpg "") 发现无论怎么用一条直线切都不行。那么两条直线行不行? ![异或二](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e243094adeb.jpg "") 这么看确实是可以的,我们只需要把两个”半平面”取交集就可以找到一个楔形区域的非线性分离边界了,而且并不是唯一的解。因此我们发现如果允许对同一个输入做多种不同的逻辑回归,再对这些结果取交集,就可以解决很大一部分非线性可分问题。 那么,对于取交集怎么用逻辑回归表达?我们考虑两个输入的情况,每一个输入是样本点x(X1,X2)属于某一个”半平面”的概率,取交集意味着该样本点属于两个”半平面”的概率均接近于1,可以用一条直线将其区分开来。 可见,对于交集运算,通过选取合适的权重和偏移,就可以得到一个线性的分离边界。这也就是所谓用神经元实现“逻辑与”运算,这是神经网络的一个常见应用: ![逻辑与](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e243095d483.jpg "") 以上过程如果用神经网络来展现的话,可以设计成一个单隐层神经网络,每层的节点数依次为:3、3、1,如下如图: ![单隐层神经网络](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e243097453f.jpg "") 解释下每一层的作用: 1. 第一层是输入层,对应着每一个输入的二维样本点x(X1,X2)的2个输坐标和1个辅助偏移的常量“1”,总共3个节点。 2. 第二层是第一层的输出层,每个节点对应着输入样本点属于某个”半平面”的概率,该”半平面”的正向法向量就对应着输入层(输入样本点坐标)到该节点的权重w和偏移b。为了定位中间这2个X点,我们围绕它们上下切2刀,用2个”半平面”相交产生的楔形开放凸域定位这2个X点,所以这一层对应着总共产生了的2个”半平面”,有2个节点。第二层同时也是第三层的输入层,要考虑再加上求第三层节点时要用的1个辅助偏移的常量节点“1”,第二层总共有2+1=3个节点。 3. 第三层是第二层也是整体的输出层,输出节点对应着输入样本点属于楔形开放凸域的概率,也就是与这2个X点属于同一类的概率。 但是这种方法有局限,这些直线分割的”半平面”取的交集都有个特点,用专业的说法“都是凸域的”——其边界任意两点的连线都在域内。如下图各个色块,每一个色块都是一个凸域。 ![凸域](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e24309a11ec.jpg "") 单独一个凸域的表现能力可能不是很强,无法构造一些非凸域的奇怪的形状。于是我们考虑是不是把n个凸域并在一起,这样就可以组成任意非凸域的奇葩的形状了:如上图所有彩色色块拼起来的区域。那对应于神经元是怎么一个操作?……再加一层,取他们的并集! 恭喜你又会抢答了。 而取并集就意味着该样本点属于两个”半平面”的概率至少有一个接近于1,也可以用一条直线将其区分开来。 可见,对于并集运算,通过选取合适的权重和偏移,也可以得到一个线性的分离边界。这就是所谓用神经元实现“逻辑或”运算,这也是神经网络的一个常见应用: ![逻辑或](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e24309b45b4.jpg "") 我们用一下这张图总结上面所说的内容,一图胜千言唉: ![神经网络表达全图](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e24309ccf5a.jpg "") 数学家们经过严格的数学证明,双隐层神经网络能够解决任意复杂的分类问题。 ### 5.以三分类问题为例演示神经网络的统一解法 对于一切分类问题我们都可以有一个统一的方法,只需要两层隐藏层。以下面这个3分类问题为例: ![三分类](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430a005e6.jpg "") 我们通过取”半平面”、取交集生成精确包含每一个样本点的凸域(所以凸域的个数与训练集的样本的个数相等),再对同类的样本点的区域取并集,这样无论多复杂的分离界面我们可以考虑进去。于是,我们设计双隐层神经网络结构如下,每一层的节点数依次为:3、25、7、3: ![网络结构](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430a1618a.jpg "") 解释下每一层的作用: 1. 第一层是输入层,对应着每一个输入的二维样本点x(X1,X2)的2个输坐标和1个辅助偏移的常量“1”,总共3个节点。 2. 第二层是第一层的输出层,每个节点对应着输入样本点属于某个”半平面”的概率,该”半平面”的正向法向量就对应着输入层(输入样本点坐标)到该节点的权重w和偏移b。为了精确定位总共这6个点,我们围绕每个点附近平行于坐标轴切4刀,用四个”半平面”相交产生的方形封闭凸域精确包裹定位该点,所以这一层对应着总共产生了的4*6=24个”半平面”,有24个节点。第二层同时也是第三层的输入层,要考虑再加上求第三层节点时要用的1个辅助偏移的常量节点“1”,第二层总共有24+1=25个节点。 3. 第三层是第二层的输出层,每个节点对应着输入样本点属于第二层区分出来的某4个”半平面”取交集形成的某个方形封闭凸域的概率,故总共需要24/4=6个节点。因为采用我们的划分方式,只需要四个”半平面”就可以精确包裹某一个样本点了,我们认为其他的”半平面”贡献的权重应该为0,就只画了生成该凸域的四个”半平面”给予的权重对应的连线,其他连线不画。第三层同时也是第四层的输入层,要考虑再加上求第四层节点时要用的1个辅助偏移的常量节点“1”,第三层总共有6+1=7个节点。 4. 第四层是第三层的输出层,每个节点对应着输入样本点属于某一类点所在的所有区域的概率。为找到该类的区分区域,对第三层属于同一类点的2个凸域的2个节点取并集,故总共需要6/2=3个节点。(同样,我们不画其他类的凸域贡献的权重的连线) 小结一下就是先取”半平面”再取交集最后取并集。 如果看文字太累,请看下图。只可惜图太小,省略了很多节点,请见谅。如果觉得还是有些问题,欢迎在评论区中参与讨论哈。 ![神经网络全图](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430a2c9b0.jpg "") ### 6.一种降低过拟合的方法 我们的确有了一个统一的解法。但估计很多同学看到还没看完就开始吐槽了:“总共也就6个样本,尼玛用了32个隐藏节点,训练111个(24*3+6*5+3*3)参数,这过拟合也太过分了吧!” 的确如此,这个过拟合弄得我们也不忍直视。如果我们多增加几个样本,就会发现上面训练出来的模型不够用了。 ![过拟合](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430a523dc.jpg "") 通过这个反面教材我们发现,神经网络是特别容易过拟合的。而如何降低过拟合就是神经网络领域中一个非常重要的主题,后面会有专文讨论。本例只是简单应用其中一个方法——就是降低神经网络的层数和节点数。 我们仔细观察样本点的分布,发现每两个类别都可以有一条直线直接将它们分开,这是所谓的one-vs-one的情况,用三条直线就可以完全将这些类别区分开来。 ![三线分离](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430a62d64.jpg "") 相应的,我们只需要求出每两个类别的分离直线,找出每个类别所属的两个半平面,对它们求一个交集就够了。神经网络结构如下: ![三线分类](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430a74818.jpg "") 解释下每一层的作用: 1. 第一层是输入层,对应着每一个输入的二维样本点x(X1,X2)的2个输坐标和1个辅助偏移的常量“1”,总共3个节点。 2. 第二层是第一层的输出层,每个节点对应着输入样本点属于某个”半平面”的概率,该”半平面”的正向法向量就对应着输入层(输入样本点坐标)到该节点的权重w和偏移b。因为3个”半平面”足以定位每一个类型的区域,所以这一层有3个节点。第二层同时也是第三层的输入层,要考虑再加上求第三层节点时要用的1个辅助偏移的常量节点“1”,第二层总共有3+1=4个节点。 3. 第三层是第二层的输出层,每个节点对应着输入样本点属于第二层区分出来的某2个”半平面”取交集形成的某个开放凸域的概率。我们同样认为其他的”半平面”贡献的权重应该为0,就只画了生成该凸域的2个”半平面”给予的权重对应的连线,其他连线不画。只是这里与上面例子的情况有一点小小的不同,因为一个分离直线可以区分出两个“半平面”,两个“半平面”都被我们的利用起来做交集求凸域,故每个第二层的节点都引出了两条权重的连线到第三层。而这取交集的结果足以定位每一个类型的区域,因此第三层也就是总体的输出层。故第三层总共需要3个节点。 在这里,我们只用了1个隐藏层4个节点就完成了分类任务,能够识别的样本还比之前多。效果是很明显的。 从以上推导我们可以得出一个启发式的经验:通过适当减少神经元的层数和节点个数,可以极大地提高计算效率,降低过拟合的程度。我们会在另一篇文章中有更加全面系统的论述。 ### 7.然而,问题真的解决了吗? 其实并没有。 - 我们凭什么知道上面改进的模型还有没有**过拟合**? > 其实我们并不知道。因此我们需要更加科学客观的方法进行评估模型。 - 降低神经网络过拟合的方式有哪些? - 我们凭什么知道我们的节点数和隐藏层个数的选择是合适的? > 我们刚才只是直接是画出了分界线和相应的神经元组合。问题是,很多时候你并不知道样本真实的分布,并不知道需要多少层、多少个节点、之间的链接方式怎样。 - 我们怎么求出这些(超)参数呢? - 假设我们已经知道了节点数和隐藏层数,我们怎么求这些神经元的权重和偏移呢? - 最常用的最优化的方法是梯度下降法。求梯度就要求**偏导数**,但是你怎么求每一个权重和偏移的偏导数呢? - 假设我们已经知道了传说中的**BP算法,也就是反向传播算法来求偏导数**。但是对于稍微深一点的神经网络,这种方法可能会求出一些很**奇葩的偏导数,跟真实的偏导数完全不一样,导致整个模型崩溃。**那么我们怎么处理、怎么预防这样的事情发生呢? - 为什么神经网络这么难以训练? - 除掉逻辑回归类型对应的sigmoid激励函数神经元,还有很多其他的激励函数,我们对不同的问题,怎么选择合适的激励函数? - 神经网络还有其他的表示形式和可能性吗? - 怎样选择不同的神经网络类型去解决不同的问题? ……. - 其实,本文举例中将后几层的神经网络直接解释成做交集、并集的“与”、“或”逻辑运算,**这些技术很早就出现了,神经网络依然只是对它们的包装,并不能显示神经网络有多强大。神经网络的独特特点是它能够自动调整到合适的权重,不拘泥于交集、并集等逻辑运算对权重的限制。而神经网络真正的威力,是它几乎可以拟合一切函数。**它为什么这么牛?怎么解释神经网络的拟合特性呢?能不能形象地解释一下?也将是我们下一步需要探讨的问题。 …… 可见,神经网络深度学习有很多问题还没有解决。我们会在接下来的文章中一一与大家探讨。 但是这么多问题从哪入手?其实,我们觉得,直接给大家一个栗子,解决一个真实的问题,可能更容易理解。所以下一篇文章我们将给大家演示怎么用神经网络计算解决问题,并做进一步的启发式探讨。而且,我们将会发码——让大家之间在自己的电脑上运行python代码——来进行手写数字的识别。你们猜猜代码有多短?只有74行! ### 8.附:本系列文章的逻辑体系图 ![逻辑体系](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430a8813c.jpg "")
';

机器学习系列(3)_逻辑回归应用之Kaggle泰坦尼克之灾

最后更新于:2022-04-01 09:51:59

作者: [寒小阳](http://blog.csdn.net/han_xiaoyang?viewmode=contents) 时间:2015年11月。 出处:[http://blog.csdn.net/han_xiaoyang/article/details/49797143](http://blog.csdn.net/han_xiaoyang/article/details/49797143) 声明:版权所有,转载请注明出处,谢谢。 ### 1.引言 先说一句,年末双十一什么的一来,真是非(mang)常(cheng)欢(gou)乐(le)!然后push自己抽出时间来写这篇blog的原因也非常简单: - 写完前两篇逻辑回归的介绍和各个角度理解之后,我们讨论群([戳我入群](http://blog.csdn.net/han_xiaoyang/article/details/49624963))的小伙伴们纷纷表示『好像很高级的样纸,but 然并卵 啊!你们倒是拿点实际数据来给我们看看,这玩意儿 有!什!么!用!啊!』 - talk is cheap, show me the code! - no example say a jb! OK,OK,这就来了咯,同学们别着急,我们先找个简单的实际例子,来看看,所谓的数据挖掘或者机器学习实际应用到底是怎么样一个过程。 『喂,那几个说要看大数据上机器学习应用的,对,就是说你们!别着急好么,我们之后拉点大一点实际数据用[liblinear](http://www.csie.ntu.edu.tw/~cjlin/liblinear/)或者[spark,MLlib](http://spark.apache.org/mllib/)跑给你们看,行不行?咱们先拿个实例入入门嘛』 好了,我是一个严肃的技术研究和分享者,咳咳,不能废话了,各位同学继续往下看吧! ### 2.背景 **2.1 关于Kaggle** - [我是Kaggle地址,翻我牌子](https://www.kaggle.com/) - 亲,逼格这么高的地方,你一定听过对不对?是!这就是那个无数『数据挖掘先驱』们,在回答”枪我有了,哪能找到靶子练练手啊?”时候的答案! - 这是一个要数据有数据,要实际应用场景有场景,要一起在数据挖掘领域high得不要不要的小伙伴就有小伙伴的地方啊!!! 艾玛,逗逼模式开太猛了。恩,不闹,不闹,说正事,Kaggle是一个数据分析建模的应用竞赛平台,有点类似[KDD-CUP](http://www.kdd.org/)(国际知识发现和数据挖掘竞赛),企业或者研究者可以将问题背景、数据、期望指标等发布到Kaggle上,以竞赛的形式向广大的数据科学家征集解决方案。而热爱数(dong)据(shou)挖(zhe)掘(teng)的小伙伴们可以下载/分析数据,使用统计/机器学习/数据挖掘等知识,建立算法模型,得出结果并提交,排名top的可能会有奖金哦! **2.2 关于泰坦尼克号之灾** - 带大家去[该问题页面](https://www.kaggle.com/c/titanic)溜达一圈吧 - 下面是问题背景页 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e24303be733.png) - 下面是可下载Data的页面 ![Data页面](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e24303e6877.png "") - 下面是小伙伴们最爱的forum页面,你会看到各种神级人物厉(qi)害(pa)的数据处理/建模想法,你会直视『世界真奇妙』。 ![论坛页面](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e24304211aa.png "") - 泰坦尼克号问题之背景 - 就是那个大家都熟悉的『Jack and Rose』的故事,豪华游艇倒了,大家都惊恐逃生,可是救生艇的数量有限,无法人人都有,副船长发话了『lady and kid first!』,所以是否获救其实并非随机,而是基于一些背景有rank先后的。 - 训练和测试数据是一些乘客的个人信息以及存活状况,要尝试根据它生成合适的模型并预测其他人的存活状况。 - 对,这是一个二分类问题,是我们之前讨论的logistic regression所能处理的范畴。 ### 3.说明 接触过Kaggle的同学们可能知道这个问题,也可能知道RandomForest和SVM等等算法,甚至还对多个模型做过融合,取得过非常好的结果,那maybe这篇文章并不是针对你的,你可以自行略过。 我们因为之前只介绍了Logistic Regression这一种分类算法。所以本次的问题解决过程和优化思路,都集中在这种算法上。其余的方法可能我们之后的文章里会提到。 说点个人的观点。不一定正确。 **『解决一个问题的方法和思路不止一种』** **『没有所谓的机器学习算法优劣,也没有绝对高性能的机器学习算法,只有在特定的场景、数据和特征下更合适的机器学习算法。』** ### 4.怎么做? 手把手教程马上就来,先来两条我看到的,觉得很重要的经验。 1. 印象中Andrew Ng老师似乎在coursera上说过,应用机器学习,千万不要一上来就试图做到完美,先撸一个baseline的model出来,再进行后续的分析步骤,一步步提高,所谓后续步骤可能包括『分析model现在的状态(欠/过拟合),分析我们使用的feature的作用大小,进行feature selection,以及我们模型下的bad case和产生的原因』等等。 1. Kaggle上的大神们,也分享过一些experience,说几条我记得的哈: - **『对数据的认识太重要了!』** - **『数据中的特殊点/离群点的分析和处理太重要了!』** - **『特征工程(feature engineering)太重要了!在很多Kaggle的场景下,甚至比model本身还要重要』** - **『要做模型融合(model ensemble)啊啊啊!』** 更多的经验分享请加讨论群,具体方式请联系作者,或者参见[《“ML学分计划”说明书》](http://blog.csdn.net/han_xiaoyang/article/details/49624963) ### 5.初探数据 先看看我们的数据,长什么样吧。在Data下我们train.csv和test.csv两个文件,分别存着官方给的训练和测试数据。 ~~~ import pandas as pd #数据分析 import numpy as np #科学计算 from pandas import Series,DataFrame data_train = pd.read_csv("/Users/Hanxiaoyang/Titanic_data/Train.csv") data_train ~~~ pandas是常用的python数据处理包,把csv文件读入成dataframe各式,我们在ipython notebook中,看到data_train如下所示: ![训练数据](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430449f5c.png "") 这就是典型的dataframe格式,如果你没接触过这种格式,完全没有关系,你就把它想象成Excel里面的列好了。 我们看到,总共有12列,其中Survived字段表示的是该乘客是否获救,其余都是乘客的个人信息,包括: - PassengerId => 乘客ID - Pclass => 乘客等级(1/2/3等舱位) - Name => 乘客姓名 - Sex => 性别 - Age => 年龄 - SibSp => 堂兄弟/妹个数 - Parch => 父母与小孩个数 - Ticket => 船票信息 - Fare => 票价 - Cabin => 客舱 - Embarked => 登船港口 逐条往下看,要看完这么多条,眼睛都有一种要瞎的赶脚。好吧,我们让dataframe自己告诉我们一些信息,如下所示: ~~~ data_train.info() ~~~ 看到了如下的信息: ![数据信息](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e24304756cb.png "") 上面的数据说啥了?它告诉我们,训练数据中总共有891名乘客,但是很不幸,我们有些属性的数据不全,比如说: - Age(年龄)属性只有714名乘客有记录 - Cabin(客舱)更是只有204名乘客是已知的 似乎信息略少啊,想再瞄一眼具体数据数值情况呢?恩,我们用下列的方法,得到数值型数据的一些分布(因为有些属性,比如姓名,是文本型;而另外一些属性,比如登船港口,是类目型。这些我们用下面的函数是看不到的): ![数值型数据基本信息](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e243049eea5.png "") 我们从上面看到更进一步的什么信息呢? mean字段告诉我们,大概0.383838的人最后获救了,2/3等舱的人数比1等舱要多,平均乘客年龄大概是29.7岁(计算这个时候会略掉无记录的)等等… ### 6.数据初步分析 每个乘客都这么多属性,那我们咋知道哪些属性更有用,而又应该怎么用它们啊?说实话这会儿我也不知道,但我们记得前面提到过 - **『对数据的认识太重要了!』** - **『对数据的认识太重要了!』** - **『对数据的认识太重要了!』** 重要的事情说三遍,恩,说完了。仅仅最上面的对数据了解,依旧无法给我们提供想法和思路。我们再深入一点来看看我们的数据,看看每个/多个 属性和最后的Survived之间有着什么样的关系呢。 **6.1 乘客各属性分布** 脑容量太有限了…数值看花眼了。我们还是统计统计,画些图来看看属性和结果之间的关系好了,代码如下: ~~~ import matplotlib.pyplot as plt fig = plt.figure() fig.set(alpha=0.2) # 设定图表颜色alpha参数 plt.subplot2grid((2,3),(0,0)) # 在一张大图里分列几个小图 data_train.Survived.value_counts().plot(kind='bar')# 柱状图 plt.title(u"获救情况 (1为获救)") # 标题 plt.ylabel(u"人数") plt.subplot2grid((2,3),(0,1)) data_train.Pclass.value_counts().plot(kind="bar") plt.ylabel(u"人数") plt.title(u"乘客等级分布") plt.subplot2grid((2,3),(0,2)) plt.scatter(data_train.Survived, data_train.Age) plt.ylabel(u"年龄") # 设定纵坐标名称 plt.grid(b=True, which='major', axis='y') plt.title(u"按年龄看获救分布 (1为获救)") plt.subplot2grid((2,3),(1,0), colspan=2) data_train.Age[data_train.Pclass == 1].plot(kind='kde') data_train.Age[data_train.Pclass == 2].plot(kind='kde') data_train.Age[data_train.Pclass == 3].plot(kind='kde') plt.xlabel(u"年龄")# plots an axis lable plt.ylabel(u"密度") plt.title(u"各等级的乘客年龄分布") plt.legend((u'头等舱', u'2等舱',u'3等舱'),loc='best') # sets our legend for our graph. plt.subplot2grid((2,3),(1,2)) data_train.Embarked.value_counts().plot(kind='bar') plt.title(u"各登船口岸上船人数") plt.ylabel(u"人数") plt.show() ~~~ ![数据基本信息图示](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e24304d4b37.png "") bingo,图还是比数字好看多了。所以我们在图上可以看出来,被救的人300多点,不到半数;3等舱乘客灰常多;遇难和获救的人年龄似乎跨度都很广;3个不同的舱年龄总体趋势似乎也一致,2/3等舱乘客20岁多点的人最多,1等舱40岁左右的最多(→_→似乎符合财富和年龄的分配哈,咳咳,别理我,我瞎扯的);登船港口人数按照S、C、Q递减,而且S远多于另外俩港口。 这个时候我们可能会有一些想法了: - 不同舱位/乘客等级可能和财富/地位有关系,最后获救概率可能会不一样 - 年龄对获救概率也一定是有影响的,毕竟前面说了,副船长还说『小孩和女士先走』呢 - 和登船港口是不是有关系呢?也许登船港口不同,人的出身地位不同? 口说无凭,空想无益。老老实实再来统计统计,看看这些属性值的统计分布吧。 **6.2 属性与获救结果的关联统计** ~~~ #看看各乘客等级的获救情况 fig = plt.figure() fig.set(alpha=0.2) # 设定图表颜色alpha参数 Survived_0 = data_train.Pclass[data_train.Survived == 0].value_counts() Survived_1 = data_train.Pclass[data_train.Survived == 1].value_counts() df=pd.DataFrame({u'获救':Survived_1, u'未获救':Survived_0}) df.plot(kind='bar', stacked=True) plt.title(u"各乘客等级的获救情况") plt.xlabel(u"乘客等级") plt.ylabel(u"人数") plt.show() ~~~ ![各乘客等级的获救情况](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e243053ef66.png "") 啧啧,果然,钱和地位对舱位有影响,进而对获救的可能性也有影响啊←_← 咳咳,跑题了,我想说的是,明显等级为1的乘客,获救的概率高很多。恩,这个一定是影响最后获救结果的一个特征。 ~~~ #看看各性别的获救情况 fig = plt.figure() fig.set(alpha=0.2) # 设定图表颜色alpha参数 Survived_m = data_train.Survived[data_train.Sex == 'male'].value_counts() Survived_f = data_train.Survived[data_train.Sex == 'female'].value_counts() df=pd.DataFrame({u'男性':Survived_m, u'女性':Survived_f}) df.plot(kind='bar', stacked=True) plt.title(u"按性别看获救情况") plt.xlabel(u"性别") plt.ylabel(u"人数") plt.show() ~~~ ![各乘客等级的获救情况](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e243055bead.png "") 歪果盆友果然很尊重lady,lady first践行得不错。性别无疑也要作为重要特征加入最后的模型之中。 再来个详细版的好了。 ~~~ #然后我们再来看看各种舱级别情况下各性别的获救情况 fig=plt.figure() fig.set(alpha=0.65) # 设置图像透明度,无所谓 plt.title(u"根据舱等级和性别的获救情况") ax1=fig.add_subplot(141) data_train.Survived[data_train.Sex == 'female'][data_train.Pclass != 3].value_counts().plot(kind='bar', label="female highclass", color='#FA2479') ax1.set_xticklabels([u"获救", u"未获救"], rotation=0) ax1.legend([u"女性/高级舱"], loc='best') ax2=fig.add_subplot(142, sharey=ax1) data_train.Survived[data_train.Sex == 'female'][data_train.Pclass == 3].value_counts().plot(kind='bar', label='female, low class', color='pink') ax2.set_xticklabels([u"未获救", u"获救"], rotation=0) plt.legend([u"女性/低级舱"], loc='best') ax3=fig.add_subplot(143, sharey=ax1) data_train.Survived[data_train.Sex == 'male'][data_train.Pclass != 3].value_counts().plot(kind='bar', label='male, high class',color='lightblue') ax3.set_xticklabels([u"未获救", u"获救"], rotation=0) plt.legend([u"男性/高级舱"], loc='best') ax4=fig.add_subplot(144, sharey=ax1) data_train.Survived[data_train.Sex == 'male'][data_train.Pclass == 3].value_counts().plot(kind='bar', label='male low class', color='steelblue') ax4.set_xticklabels([u"未获救", u"获救"], rotation=0) plt.legend([u"男性/低级舱"], loc='best') plt.show() ~~~ ![各性别和舱位的获救情况](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430582bce.png "") 恩,坚定了之前的判断。 我们看看各登船港口的获救情况。 ~~~ fig = plt.figure() fig.set(alpha=0.2) # 设定图表颜色alpha参数 Survived_0 = data_train.Embarked[data_train.Survived == 0].value_counts() Survived_1 = data_train.Embarked[data_train.Survived == 1].value_counts() df=pd.DataFrame({u'获救':Survived_1, u'未获救':Survived_0}) df.plot(kind='bar', stacked=True) plt.title(u"各登录港口乘客的获救情况") plt.xlabel(u"登录港口") plt.ylabel(u"人数") plt.show() ~~~ ![各登船港口的获救情况](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e24305a8d0d.png "") 下面我们来看看 堂兄弟/妹,孩子/父母有几人,对是否获救的影响。 ~~~ g = data_train.groupby(['SibSp','Survived']) df = pd.DataFrame(g.count()['PassengerId']) print df g = data_train.groupby(['SibSp','Survived']) df = pd.DataFrame(g.count()['PassengerId']) print df ~~~ ![堂兄弟/妹影响](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e24305c7345.png "") ![父母/孩子影响](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e24305e6bec.png "") 好吧,没看出特别特别明显的规律(为自己的智商感到捉急…),先作为备选特征,放一放。 ~~~ #ticket是船票编号,应该是unique的,和最后的结果没有太大的关系,先不纳入考虑的特征范畴把 #cabin只有204个乘客有值,我们先看看它的一个分布 data_train.Cabin.value_counts() ~~~ 部分结果如下: ![Cabin分布](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e243060a114.png "") 这三三两两的…如此不集中…我们猜一下,也许,前面的ABCDE是指的甲板位置、然后编号是房间号?…好吧,我瞎说的,别当真… 关键是Cabin这鬼属性,应该算作类目型的,本来缺失值就多,还如此不集中,注定是个棘手货…第一感觉,这玩意儿如果直接按照类目特征处理的话,太散了,估计每个因子化后的特征都拿不到什么权重。加上有那么多缺失值,要不我们先把Cabin缺失与否作为条件(虽然这部分信息缺失可能并非未登记,maybe只是丢失了而已,所以这样做未必妥当),先在有无Cabin信息这个粗粒度上看看Survived的情况好了。 ~~~ fig = plt.figure() fig.set(alpha=0.2) # 设定图表颜色alpha参数 Survived_cabin = data_train.Survived[pd.notnull(data_train.Cabin)].value_counts() Survived_nocabin = data_train.Survived[pd.isnull(data_train.Cabin)].value_counts() df=pd.DataFrame({u'有':Survived_cabin, u'无':Survived_nocabin}).transpose() df.plot(kind='bar', stacked=True) plt.title(u"按Cabin有无看获救情况") plt.xlabel(u"Cabin有无") plt.ylabel(u"人数") plt.show() ~~~ ![有无Cabin记录影响](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430630efb.png "") 咳咳,有Cabin记录的似乎获救概率稍高一些,先这么着放一放吧。 ### 7.简单数据预处理 大体数据的情况看了一遍,对感兴趣的属性也有个大概的了解了。 下一步干啥?咱们该处理处理这些数据,为机器学习建模做点准备了。 对了,我这里说的数据预处理,其实就包括了很多Kaggler津津乐道的feature engineering过程,灰常灰常有必要! **『特征工程(feature engineering)太重要了!』** **『特征工程(feature engineering)太重要了!』** **『特征工程(feature engineering)太重要了!』** 恩,重要的事情说三遍。 先从最突出的数据属性开始吧,对,Cabin和Age,有丢失数据实在是对下一步工作影响太大。 先说Cabin,暂时我们就按照刚才说的,按Cabin有无数据,将这个属性处理成Yes和No两种类型吧。 再说Age: 通常遇到缺值的情况,我们会有几种常见的处理方式 - 如果缺值的样本占总数比例极高,我们可能就直接舍弃了,作为特征加入的话,可能反倒带入noise,影响最后的结果了 - 如果缺值的样本适中,而该属性非连续值特征属性(比如说类目属性),那就把NaN作为一个新类别,加到类别特征中 - 如果缺值的样本适中,而该属性为连续值特征属性,有时候我们会考虑给定一个step(比如这里的age,我们可以考虑每隔2/3岁为一个步长),然后把它离散化,之后把NaN作为一个type加到属性类目中。 - 有些情况下,缺失的值个数并不是特别多,那我们也可以试着根据已有的值,拟合一下数据,补充上。 本例中,后两种处理方式应该都是可行的,我们先试试拟合补全吧(虽然说没有特别多的背景可供我们拟合,这不一定是一个多么好的选择) 我们这里用scikit-learn中的RandomForest来拟合一下缺失的年龄数据(注:RandomForest是一个用在原始数据中做不同采样,建立多颗DecisionTree,再进行average等等来降低过拟合现象,提高结果的机器学习算法,我们之后会介绍到) ~~~ from sklearn.ensemble import RandomForestRegressor ### 使用 RandomForestClassifier 填补缺失的年龄属性 def set_missing_ages(df): # 把已有的数值型特征取出来丢进Random Forest Regressor中 age_df = df[['Age','Fare', 'Parch', 'SibSp', 'Pclass']] # 乘客分成已知年龄和未知年龄两部分 known_age = age_df[age_df.Age.notnull()].as_matrix() unknown_age = age_df[age_df.Age.isnull()].as_matrix() # y即目标年龄 y = known_age[:, 0] # X即特征属性值 X = known_age[:, 1:] # fit到RandomForestRegressor之中 rfr = RandomForestRegressor(random_state=0, n_estimators=2000, n_jobs=-1) rfr.fit(X, y) # 用得到的模型进行未知年龄结果预测 predictedAges = rfr.predict(unknown_age[:, 1::]) # 用得到的预测结果填补原缺失数据 df.loc[ (df.Age.isnull()), 'Age' ] = predictedAges return df, rfr def set_Cabin_type(df): df.loc[ (df.Cabin.notnull()), 'Cabin' ] = "Yes" df.loc[ (df.Cabin.isnull()), 'Cabin' ] = "No" return df data_train, rfr = set_missing_ages(data_train) data_train = set_Cabin_type(data_train) ~~~ ![处理Cabin和Age之后](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430653356.png "") 恩。目的达到,OK了。 因为逻辑回归建模时,需要输入的特征都是数值型特征,我们通常会先对类目型的特征因子化。 什么叫做因子化呢?举个例子: 以Cabin为例,原本一个属性维度,因为其取值可以是[‘yes’,’no’],而将其平展开为’Cabin_yes’,’Cabin_no’两个属性 - 原本Cabin取值为yes的,在此处的”Cabin_yes”下取值为1,在”Cabin_no”下取值为0 - 原本Cabin取值为no的,在此处的”Cabin_yes”下取值为0,在”Cabin_no”下取值为1 我们使用pandas的”get_dummies”来完成这个工作,并拼接在原来的”data_train”之上,如下所示。 ~~~ dummies_Cabin = pd.get_dummies(data_train['Cabin'], prefix= 'Cabin') dummies_Embarked = pd.get_dummies(data_train['Embarked'], prefix= 'Embarked') dummies_Sex = pd.get_dummies(data_train['Sex'], prefix= 'Sex') dummies_Pclass = pd.get_dummies(data_train['Pclass'], prefix= 'Pclass') df = pd.concat([data_train, dummies_Cabin, dummies_Embarked, dummies_Sex, dummies_Pclass], axis=1) df.drop(['Pclass', 'Name', 'Sex', 'Ticket', 'Cabin', 'Embarked'], axis=1, inplace=True) df ~~~ ![离散/因子化之后](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430687a13.png "") bingo,我们很成功地把这些类目属性全都转成0,1的数值属性了。 这样,看起来,是不是我们需要的属性值都有了,且它们都是数值型属性呢。 有一种临近结果的宠宠欲动感吧,莫急莫急,我们还得做一些处理,仔细看看Age和Fare两个属性,乘客的数值幅度变化,也忒大了吧!!如果大家了解逻辑回归与梯度下降的话,会知道,各属性值之间scale差距太大,将对收敛速度造成几万点伤害值!甚至不收敛! (╬▔皿▔)…所以我们先用scikit-learn里面的preprocessing模块对这俩货做一个scaling,所谓scaling,其实就是将一些变化幅度较大的特征化到[-1,1]之内。 ~~~ import sklearn.preprocessing as preprocessing scaler = preprocessing.StandardScaler() age_scale_param = scaler.fit(df['Age']) df['Age_scaled'] = scaler.fit_transform(df['Age'], age_scale_param) fare_scale_param = scaler.fit(df['Fare']) df['Fare_scaled'] = scaler.fit_transform(df['Fare'], fare_scale_param) df ~~~ ![scaling](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e24306ad1da.png "") 恩,好看多了,万事俱备,只欠建模。马上就要看到成效了,哈哈。我们把需要的属性值抽出来,转成scikit-learn里面LogisticRegression可以处理的格式。 ### 8.逻辑回归建模 我们把需要的feature字段取出来,转成numpy格式,使用scikit-learn中的LogisticRegression建模。 ~~~ from sklearn import linear_model # 用正则取出我们要的属性值 train_df = df.filter(regex='Survived|Age_.*|SibSp|Parch|Fare_.*|Cabin_.*|Embarked_.*|Sex_.*|Pclass_.*') train_np = train_df.as_matrix() # y即Survival结果 y = train_np[:, 0] # X即特征属性值 X = train_np[:, 1:] # fit到RandomForestRegressor之中 clf = linear_model.LogisticRegression(C=1.0, penalty='l1', tol=1e-6) clf.fit(X, y) clf ~~~ good,很顺利,我们得到了一个model,如下: ![modeling](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e24306d4cda.png "") 先淡定!淡定!你以为把test.csv直接丢进model里就能拿到结果啊…骚年,图样图森破啊!我们的”test_data”也要做和”train_data”一样的预处理啊!! ~~~ data_test = pd.read_csv("/Users/Hanxiaoyang/Titanic_data/test.csv") data_test.loc[ (data_test.Fare.isnull()), 'Fare' ] = 0 # 接着我们对test_data做和train_data中一致的特征变换 # 首先用同样的RandomForestRegressor模型填上丢失的年龄 tmp_df = data_test[['Age','Fare', 'Parch', 'SibSp', 'Pclass']] null_age = tmp_df[data_test.Age.isnull()].as_matrix() # 根据特征属性X预测年龄并补上 X = null_age[:, 1:] predictedAges = rfr.predict(X) data_test.loc[ (data_test.Age.isnull()), 'Age' ] = predictedAges data_test = set_Cabin_type(data_test) dummies_Cabin = pd.get_dummies(data_test['Cabin'], prefix= 'Cabin') dummies_Embarked = pd.get_dummies(data_test['Embarked'], prefix= 'Embarked') dummies_Sex = pd.get_dummies(data_test['Sex'], prefix= 'Sex') dummies_Pclass = pd.get_dummies(data_test['Pclass'], prefix= 'Pclass') df_test = pd.concat([data_test, dummies_Cabin, dummies_Embarked, dummies_Sex, dummies_Pclass], axis=1) df_test.drop(['Pclass', 'Name', 'Sex', 'Ticket', 'Cabin', 'Embarked'], axis=1, inplace=True) df_test['Age_scaled'] = scaler.fit_transform(df_test['Age'], age_scale_param) df_test['Fare_scaled'] = scaler.fit_transform(df_test['Fare'], fare_scale_param) df_test ~~~ ![modeling](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e24306f2d25.png "") 不错不错,数据很OK,差最后一步了。 下面就做预测取结果吧!! ~~~ test = df_test.filter(regex='Age_.*|SibSp|Parch|Fare_.*|Cabin_.*|Embarked_.*|Sex_.*|Pclass_.*') predictions = clf.predict(test) result = pd.DataFrame({'PassengerId':data_test['PassengerId'].as_matrix(), 'Survived':predictions.astype(np.int32)}) result.to_csv("/Users/Hanxiaoyang/Titanic_data/logistic_regression_predictions.csv", index=False) ~~~ ![预测结果](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e243072522f.png "") 啧啧,挺好,格式正确,去make a submission啦啦啦! 在Kaggle的Make a submission页面,提交上结果。如下: ![Kaggle排名](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e243074098b.png "") 0.76555,恩,结果还不错。毕竟,这只是我们简单分析处理过后出的一个baseline模型嘛。 ### 9.逻辑回归系统优化 **9.1 模型系数关联分析** 亲,你以为结果提交上了,就完事了? 我不会告诉你,这只是万里长征第一步啊(泪牛满面)!!!这才刚撸完baseline model啊!!!还得优化啊!!! 看过Andrew Ng老师的machine Learning课程的同学们,知道,我们应该分析分析模型现在的状态了,是过/欠拟合?,以确定我们需要更多的特征还是更多数据,或者其他操作。我们有一条很著名的learning curves对吧。 不过在现在的场景下,先不着急做这个事情,我们这个baseline系统还有些粗糙,先再挖掘挖掘。 - 首先,Name和Ticket两个属性被我们完整舍弃了(好吧,其实是因为这俩属性,几乎每一条记录都是一个完全不同的值,我们并没有找到很直接的处理方式)。 - 然后,我们想想,年龄的拟合本身也未必是一件非常靠谱的事情,我们依据其余属性,其实并不能很好地拟合预测出未知的年龄。再一个,以我们的日常经验,小盆友和老人可能得到的照顾会多一些,这样看的话,年龄作为一个连续值,给一个固定的系数,应该和年龄是一个正相关或者负相关,似乎体现不出两头受照顾的实际情况,所以,说不定我们把年龄离散化,按区段分作类别属性会更合适一些。 上面只是我瞎想的,who knows是不是这么回事呢,老老实实先把得到的model系数和feature关联起来看看。 ~~~ pd.DataFrame({"columns":list(train_df.columns)[1:], "coef":list(clf.coef_.T)}) ~~~ ![LR模型系数](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430768553.png "") 首先,大家回去[前两篇文章](http://blog.csdn.net/han_xiaoyang/article/details/49123419)里瞄一眼公式就知道,这些系数为正的特征,和最后结果是一个正相关,反之为负相关。 我们先看看那些权重绝对值非常大的feature,在我们的模型上: - Sex属性,如果是female会极大提高最后获救的概率,而male会很大程度拉低这个概率。 - Pclass属性,1等舱乘客最后获救的概率会上升,而乘客等级为3会极大地拉低这个概率。 - 有Cabin值会很大程度拉升最后获救概率(这里似乎能看到了一点端倪,事实上从最上面的有无Cabin记录的Survived分布图上看出,即使有Cabin记录的乘客也有一部分遇难了,估计这个属性上我们挖掘还不够) - Age是一个负相关,意味着在我们的模型里,年龄越小,越有获救的优先权(还得回原数据看看这个是否合理) - 有一个登船港口S会很大程度拉低获救的概率,另外俩港口压根就没啥作用(这个实际上非常奇怪,因为我们从之前的统计图上并没有看到S港口的获救率非常低,所以也许可以考虑把登船港口这个feature去掉试试)。 - 船票Fare有小幅度的正相关(并不意味着这个feature作用不大,有可能是我们细化的程度还不够,举个例子,说不定我们得对它离散化,再分至各个乘客等级上?) 噢啦,观察完了,我们现在有一些想法了,但是怎么样才知道,哪些优化的方法是promising的呢? 因为test.csv里面并没有Survived这个字段(好吧,这是废话,这明明就是我们要预测的结果),我们无法在这份数据上评定我们算法在该场景下的效果… 而『每做一次调整就make a submission,然后根据结果来判定这次调整的好坏』其实是行不通的… **9.2 交叉验证** 重点又来了: **『要做交叉验证(cross validation)!』** **『要做交叉验证(cross validation)!』** **『要做交叉验证(cross validation)!』** 恩,重要的事情说三遍。我们通常情况下,这么做cross validation:把train.csv分成两部分,一部分用于训练我们需要的模型,另外一部分数据上看我们预测算法的效果。 我们用scikit-learn的cross_validation来帮我们完成小数据集上的这个工作。 先简单看看cross validation情况下的打分 ~~~ from sklearn import cross_validation #简单看看打分情况 clf = linear_model.LogisticRegression(C=1.0, penalty='l1', tol=1e-6) all_data = df.filter(regex='Survived|Age_.*|SibSp|Parch|Fare_.*|Cabin_.*|Embarked_.*|Sex_.*|Pclass_.*') X = all_data.as_matrix()[:,1:] y = all_data.as_matrix()[:,0] print cross_validation.cross_val_score(clf, X, y, cv=5) ~~~ 结果是下面酱紫的: [0.81564246 0.81005587 0.78651685 0.78651685 0.81355932] 似乎比Kaggle上的结果略高哈,毕竟用的是不是同一份数据集评估的。 等等,既然我们要做交叉验证,那我们干脆先把交叉验证里面的bad case拿出来看看,看看人眼审核,是否能发现什么蛛丝马迹,是我们忽略了哪些信息,使得这些乘客被判定错了。再把bad case上得到的想法和前头系数分析的合在一起,然后逐个试试。 下面我们做数据分割,并且在原始数据集上瞄一眼bad case: ~~~ # 分割数据,按照 训练数据:cv数据 = 7:3的比例 split_train, split_cv = cross_validation.train_test_split(df, test_size=0.3, random_state=0) train_df = split_train.filter(regex='Survived|Age_.*|SibSp|Parch|Fare_.*|Cabin_.*|Embarked_.*|Sex_.*|Pclass_.*') # 生成模型 clf = linear_model.LogisticRegression(C=1.0, penalty='l1', tol=1e-6) clf.fit(train_df.as_matrix()[:,1:], train_df.as_matrix()[:,0]) # 对cross validation数据进行预测 cv_df = split_cv.filter(regex='Survived|Age_.*|SibSp|Parch|Fare_.*|Cabin_.*|Embarked_.*|Sex_.*|Pclass_.*') predictions = clf.predict(cv_df.as_matrix()[:,1:]) origin_data_train = pd.read_csv("/Users/HanXiaoyang/Titanic_data/Train.csv") bad_cases = origin_data_train.loc[origin_data_train['PassengerId'].isin(split_cv[predictions != cv_df.as_matrix()[:,0]]['PassengerId'].values)] bad_cases ~~~ 我们判定错误的 bad case 中部分数据如下: ![预测错误的原始数据](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e24307924e6.png "") 大家可以自己跑一遍试试,拿到bad cases之后,仔细看看。也会有一些猜测和想法。其中会有一部分可能会印证在系数分析部分的猜测,那这些优化的想法优先级可以放高一些。 现在有了”train_df” 和 “vc_df” 两个数据部分,前者用于训练model,后者用于评定和选择模型。可以开始可劲折腾了。 我们随便列一些可能可以做的优化操作: - Age属性不使用现在的拟合方式,而是根据名称中的『Mr』『Mrs』『Miss』等的平均值进行填充。 - Age不做成一个连续值属性,而是使用一个步长进行离散化,变成离散的类目feature。 - Cabin再细化一些,对于有记录的Cabin属性,我们将其分为前面的字母部分(我猜是位置和船层之类的信息) 和 后面的数字部分(应该是房间号,有意思的事情是,如果你仔细看看原始数据,你会发现,这个值大的情况下,似乎获救的可能性高一些)。 - Pclass和Sex俩太重要了,我们试着用它们去组出一个组合属性来试试,这也是另外一种程度的细化。 - 单加一个Child字段,Age<=12的,设为1,其余为0(你去看看数据,确实小盆友优先程度很高啊) - 如果名字里面有『Mrs』,而Parch>1的,我们猜测她可能是一个母亲,应该获救的概率也会提高,因此可以多加一个Mother字段,此种情况下设为1,其余情况下设为0 - 登船港口可以考虑先去掉试试(Q和C本来就没权重,S有点诡异) - 把堂兄弟/兄妹 和 Parch 还有自己 个数加在一起组一个Family_size字段(考虑到大家族可能对最后的结果有影响) - Name是一个我们一直没有触碰的属性,我们可以做一些简单的处理,比如说男性中带某些字眼的(‘Capt’, ‘Don’, ‘Major’, ‘Sir’)可以统一到一个Title,女性也一样。 大家接着往下挖掘,可能还可以想到更多可以细挖的部分。我这里先列这些了,然后我们可以使用手头上的”train_df”和”cv_df”开始试验这些feature engineering的tricks是否有效了。 试验的过程比较漫长,也需要有耐心,而且我们经常会面临很尴尬的状况,就是我们灵光一闪,想到一个feature,然后坚信它一定有效,结果试验下来,效果还不如试验之前的结果。恩,需要坚持和耐心,以及不断的挖掘。 我最好的结果是在『Survived~C(Pclass)+C(Title)+C(Sex)+C(Age_bucket)+C(Cabin_num_bucket)Mother+Fare+Family_Size』下取得的,结果如下(抱歉,博主君commit的时候手抖把页面关了,于是没截着图,下面这张图是在我得到最高分之后,用这次的结果重新make commission的,截了个图,得分是0.79426,不是目前我的最高分哈,因此排名木有变…): ![做完feature engineering调整之后的结果](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e24307bef28.png "") **9.3 learning curves** 有一个很可能发生的问题是,我们不断地做feature engineering,产生的特征越来越多,用这些特征去训练模型,会对我们的训练集拟合得越来越好,同时也可能在逐步丧失泛化能力,从而在待预测的数据上,表现不佳,也就是发生过拟合问题。 从另一个角度上说,如果模型在待预测的数据上表现不佳,除掉上面说的过拟合问题,也有可能是欠拟合问题,也就是说在训练集上,其实拟合的也不是那么好。 额,这个欠拟合和过拟合怎么解释呢。这么说吧: - 过拟合就像是你班那个学数学比较刻板的同学,老师讲过的题目,一字不漏全记下来了,于是老师再出一样的题目,分分钟精确出结果。but数学考试,因为总是碰到新题目,所以成绩不咋地。 - 欠拟合就像是,咳咳,和博主level差不多的差生。连老师讲的练习题也记不住,于是连老师出一样题目复习的周测都做不好,考试更是可想而知了。 而在机器学习的问题上,对于过拟合和欠拟合两种情形。我们优化的方式是不同的。 对过拟合而言,通常以下策略对结果优化是有用的: - 做一下feature selection,挑出较好的feature的subset来做training - 提供更多的数据,从而弥补原始数据的bias问题,学习到的model也会更准确 而对于欠拟合而言,我们通常需要更多的feature,更复杂的模型来提高准确度。 著名的learning curve可以帮我们判定我们的模型现在所处的状态。我们以样本数为横坐标,训练和交叉验证集上的错误率作为纵坐标,两种状态分别如下两张图所示:过拟合(overfitting/high variace),欠拟合(underfitting/high bias) ![过拟合](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e24307e45bd.png "") ![欠拟合](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e243080a8cc.png "") 我们也可以把错误率替换成准确率(得分),得到另一种形式的learning curve(sklearn 里面是这么做的)。 回到我们的问题,我们用scikit-learn里面的learning_curve来帮我们分辨我们模型的状态。举个例子,这里我们一起画一下我们最先得到的baseline model的learning curve。 ~~~ import numpy as np import matplotlib.pyplot as plt from sklearn.learning_curve import learning_curve # 用sklearn的learning_curve得到training_score和cv_score,使用matplotlib画出learning curve def plot_learning_curve(estimator, title, X, y, ylim=None, cv=None, n_jobs=1, train_sizes=np.linspace(.05, 1., 20), verbose=0, plot=True): """ 画出data在某模型上的learning curve. 参数解释 ---------- estimator : 你用的分类器。 title : 表格的标题。 X : 输入的feature,numpy类型 y : 输入的target vector ylim : tuple格式的(ymin, ymax), 设定图像中纵坐标的最低点和最高点 cv : 做cross-validation的时候,数据分成的份数,其中一份作为cv集,其余n-1份作为training(默认为3份) n_jobs : 并行的的任务数(默认1) """ train_sizes, train_scores, test_scores = learning_curve( estimator, X, y, cv=cv, n_jobs=n_jobs, train_sizes=train_sizes, verbose=verbose) train_scores_mean = np.mean(train_scores, axis=1) train_scores_std = np.std(train_scores, axis=1) test_scores_mean = np.mean(test_scores, axis=1) test_scores_std = np.std(test_scores, axis=1) if plot: plt.figure() plt.title(title) if ylim is not None: plt.ylim(*ylim) plt.xlabel(u"训练样本数") plt.ylabel(u"得分") plt.gca().invert_yaxis() plt.grid() plt.fill_between(train_sizes, train_scores_mean - train_scores_std, train_scores_mean + train_scores_std, alpha=0.1, color="b") plt.fill_between(train_sizes, test_scores_mean - test_scores_std, test_scores_mean + test_scores_std, alpha=0.1, color="r") plt.plot(train_sizes, train_scores_mean, 'o-', color="b", label=u"训练集上得分") plt.plot(train_sizes, test_scores_mean, 'o-', color="r", label=u"交叉验证集上得分") plt.legend(loc="best") plt.draw() plt.show() plt.gca().invert_yaxis() midpoint = ((train_scores_mean[-1] + train_scores_std[-1]) + (test_scores_mean[-1] - test_scores_std[-1])) / 2 diff = (train_scores_mean[-1] + train_scores_std[-1]) - (test_scores_mean[-1] - test_scores_std[-1]) return midpoint, diff plot_learning_curve(clf, u"学习曲线", X, y) ~~~ ![学习曲线](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430826f5a.png "") 在实际数据上看,我们得到的learning curve没有理论推导的那么光滑哈,但是可以大致看出来,训练集和交叉验证集上的得分曲线走势还是符合预期的。 目前的曲线看来,我们的model并不处于overfitting的状态(overfitting的表现一般是训练集上得分高,而交叉验证集上要低很多,中间的gap比较大)。因此我们可以再做些feature engineering的工作,添加一些新产出的特征或者组合特征到模型中。 ### 10.模型融合(model ensemble) 好了,终于到这一步了,我们要祭出机器学习/数据挖掘上通常最后会用到的大杀器了。恩,模型融合。 『强迫症患者』打算继续喊喊口号… **『模型融合(model ensemble)很重要!』** **『模型融合(model ensemble)很重要!』** **『模型融合(model ensemble)很重要!』** 重要的事情说三遍,恩,噢啦。 先解释解释,一会儿再回到我们的问题上哈。 啥叫模型融合呢,我们还是举几个例子直观理解一下好了。 大家都看过知识问答的综艺节目中,求助现场观众时候,让观众投票,最高的答案作为自己的答案的形式吧,每个人都有一个判定结果,最后我们相信答案在大多数人手里。 再通俗一点举个例子。你和你班某数学大神关系好,每次作业都『模仿』他的,于是绝大多数情况下,他做对了,你也对了。突然某一天大神脑子犯糊涂,手一抖,写错了一个数,于是…恩,你也只能跟着错了。 我们再来看看另外一个场景,你和你班5个数学大神关系都很好,每次都把他们作业拿过来,对比一下,再『自己做』,那你想想,如果哪天某大神犯糊涂了,写错了,but另外四个写对了啊,那你肯定相信另外4人的是正确答案吧? 最简单的模型融合大概就是这么个意思,比如分类问题,当我们手头上有一堆在同一份数据集上训练得到的分类器(比如logistic regression,SVM,KNN,random forest,神经网络),那我们让他们都分别去做判定,然后对结果做投票统计,取票数最多的结果为最后结果。 bingo,问题就这么完美的解决了。 模型融合可以比较好地缓解,训练过程中产生的过拟合问题,从而对于结果的准确度提升有一定的帮助。 话说回来,回到我们现在的问题。你看,我们现在只讲了logistic regression,如果我们还想用这个融合思想去提高我们的结果,我们该怎么做呢? 既然这个时候模型没得选,那咱们就在数据上动动手脚咯。大家想想,如果模型出现过拟合现在,一定是在我们的训练上出现拟合过度造成的对吧。 那我们干脆就不要用全部的训练集,每次取训练集的一个subset,做训练,这样,我们虽然用的是同一个机器学习算法,但是得到的模型却是不一样的;同时,因为我们没有任何一份子数据集是全的,因此即使出现过拟合,也是在子训练集上出现过拟合,而不是全体数据上,这样做一个融合,可能对最后的结果有一定的帮助。对,这就是常用的Bagging。 我们用scikit-learn里面的Bagging来完成上面的思路,过程非常简单。代码如下: ~~~ from sklearn.ensemble import BaggingRegressor train_df = df.filter(regex='Survived|Age_.*|SibSp|Parch|Fare_.*|Cabin_.*|Embarked_.*|Sex_.*|Pclass.*|Mother|Child|Family|Title') train_np = train_df.as_matrix() # y即Survival结果 y = train_np[:, 0] # X即特征属性值 X = train_np[:, 1:] # fit到BaggingRegressor之中 clf = linear_model.LogisticRegression(C=1.0, penalty='l1', tol=1e-6) bagging_clf = BaggingRegressor(clf, n_estimators=20, max_samples=0.8, max_features=1.0, bootstrap=True, bootstrap_features=False, n_jobs=-1) bagging_clf.fit(X, y) test = df_test.filter(regex='Age_.*|SibSp|Parch|Fare_.*|Cabin_.*|Embarked_.*|Sex_.*|Pclass.*|Mother|Child|Family|Title') predictions = bagging_clf.predict(test) result = pd.DataFrame({'PassengerId':data_test['PassengerId'].as_matrix(), 'Survived':predictions.astype(np.int32)}) result.to_csv("/Users/HanXiaoyang/Titanic_data/logistic_regression_bagging_predictions.csv", index=False) ~~~ 然后你再Make a submission,恩,发现对结果还是有帮助的。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e243084ed2e.png) ### 11.总结 文章稍微有点长,非常感谢各位耐心看到这里。 总结的部分,我就简短写几段,出现的话,很多在文中有对应的场景,大家有兴趣再回头看看。 对于任何的机器学习问题,不要一上来就追求尽善尽美,先用自己会的算法撸一个baseline的model出来,再进行后续的分析步骤,一步步提高。 在问题的结果过程中: - **『对数据的认识太重要了!』** - **『数据中的特殊点/离群点的分析和处理太重要了!』** - **『特征工程(feature engineering)太重要了!』** - **『模型融合(model ensemble)太重要了!』** 本文中用机器学习解决问题的过程大概如下图所示: ![机器学习解决问题的过程](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430870e6c.png "") ### 12.关于数据和代码 本文中的数据和代码已经上传至[github](https://github.com/HanXiaoyang/Kaggle_Titanic)中,欢迎大家下载和自己尝试。
';

机器学习系列(2)_从初等数学视角解读逻辑回归

最后更新于:2022-04-01 09:51:56

作者:[龙心尘](http://blog.csdn.net/longxinchen_ml?viewmode=contents) && [寒小阳](http://blog.csdn.net/han_xiaoyang?viewmode=contents) 时间:2015年10月。 出处:[http://blog.csdn.net/longxinchen_ml/article/details/49284391](http://blog.csdn.net/longxinchen_ml/article/details/49284391), [http://blog.csdn.net/han_xiaoyang/article/details/49332321](http://blog.csdn.net/han_xiaoyang/article/details/49332321)。 声明:版权所有,转载请注明出处,谢谢。 ### 一、 引言 前一篇文章[《机器学习系列(1)_逻辑回归初步》](http://blog.csdn.net/han_xiaoyang/article/details/49123419)中主要介绍了逻辑回归的由来,作用和简单的应用,这里追加这篇[《机器学习系列(2)用初等数学视角解读逻辑回归》](http://blog.csdn.net/han_xiaoyang/article/details/49332321)来看看从直观的数学视角,可以怎么去理解逻辑回归的思想思路。 > 为了降低理解难度,本文试图用最基础的初等数学来解读逻辑回归,少用公式,多用图形来直观解释推导公式的现实意义,希望使读者能够对逻辑回归有更直观的理解。 ### 二、 逻辑回归问题的通俗几何描述 逻辑回归处理的是分类问题。我们可以用通俗的几何语言重新表述它: 空间中有两群点,一群是圆点“〇”,一群是叉点“X”。我们希望从空间中选出一个分离边界,将这两群点分开。 ![逻辑回归几何](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e242ff02eac.png "") > 注:分离边界的维数与空间的维数相关。如果是二维平面,分离边界就是一条线(一维)。如果是三维空间,分离边界就是一个空间中的面(二维)。如果是一维直线,分离边界就是直线上的某一点。不同维数的空间的理解下文将有专门的论述。 为了简化处理和方便表述,我们做以下4个约定: 1.我们先考虑在二维平面下的情况。 2.而且,我们假设这两类是线性可分的:即可以找到一条最佳的直线,将两类点分开。 3.用离散变量y表示点的类别,y只有两个可能的取值。y=1表示是叉点“X”,y=0表示是是圆点“〇”。 4.点的横纵坐标用![x(X1,X2)](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e242ff167b7.jpg "") 表示。 于是,现在的问题就变成了:怎么依靠现有这些点的坐标![(X1,X2)](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e242ff167b7.jpg "") 和标签(y),找出分界线的方程。 ### 三、 如何用解析几何的知识找到逻辑回归问题的分界线? 1.我们用逆推法的思路: 假设我们已经找到了这一条线,再寻找这条线的性质是什么。根据这些性质,再来反推这条线的方程。 2.这条线有什么性质呢? 首先,它能把两类点分开来。——好吧,这是废话。( ̄▽ ̄)” 然后,两类点在这条线的法向量p上的投影的值的正负号不一样,一类点的投影全是正数,另一类点的投影值全是负数! - 首先,这个性质是非常好,可以用来区分点的不同的类别。 - 而且,我们对法向量进行规范:只考虑延长线通过原点的那个法向量p。这样的话,只要求出法向量p,就可以唯一确认这条分界线,这个分类问题就解决了。 ![法向量投影](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e242ff2e35e.png "") ![投影结果](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e242ff569c8.png "") 3.还有什么方法能将法向量p的性质处理地更好呢? 因为计算各个点到法向量p投影,需要先知道p的起点的位置,而起点的位置确定起来很麻烦,我们就干脆将法向量平移使其起点落在坐标系的原点,成为新向量p’。因此,所有点到p’的投影也就变化了一个常量。 ![原点法向量投影](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e242ff65c54.png "") ![原点法向量投影结果](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e242ffb4e2a.png "") 假设这个常量为![θ0](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e242ffca088.jpg "") ,p’向量的横纵坐标为![(θ1,θ2)](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e242ffd94b3.jpg "") 。空间中任何一个点![x (X1,X2)](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e242ff167b7.jpg "") 到p’的投影就是![θ1X1+θ2X2](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e242ffec0ce.jpg "") ,再加上前面的常量值就是:![θ0+θ1X1+θ2X2](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430007f57.jpg "") 看到上面的式子有没有感到很熟悉?这不就是逻辑回归函数![hθ(x)=g(θ0+θ1X1+θ2X2)](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e243001862b.jpg "") 中括号里面的部分吗? 令![z=θ0+θ1X1+θ2X2](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430028c27.jpg "") 就可以根据z的正负号来判断点x的类别了。 ### 四、 从概率角度理解z的含义。 由以上步骤,我们由点x的坐标得到了一个新的特征z,那么: > z的现实意义是什么呢? 首先,我们知道,z可正可负可为零。而且,z的变化范围可以一直到正负无穷大。 z如果大于0,则点x属于y=1的类别。而且z的值越大,说明它距离分界线的距离越大,更可能属于y=1类。 那可否把z理解成点x属于y=1类的概率P(y=1|x) (下文简写成P)呢?显然不够理想,因为概率的范围是0到1的。 但是我们可以将概率P稍稍改造一下:令Q=P/(1-P),期望用Q作为z的现实意义。我们发现,当P的在区间[0,1]变化时,Q在[0,+∞)区间单调递增。函数图像如下(以下图像可以直接在度娘中搜“x/(1-x)”,超快): ![发生比函数图像](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430037d0e.png "") 但是Q的变化率在[0,+∞)还不够,我们是希望能在(-∞,+∞)区间变化的。而且在P=1/2的时候刚好是0。这样才有足够的解释力。 >注:因为P=1/2说明该点属于两个类别的可能性相当,也就是说这个点恰好在分界面上,那它在法向量的投影自然就是0了。 而在P=1/2时,Q=1,距离Q=0还有一段距离。那怎么通过一个函数变换然它等于0呢?有一个天然的函数log,刚好满足这个要求。 于是我们做变换R=log(Q)=log(P/(1-P)),期望用R作为z的现实意义。画出它的函数图像如图: ![logit函数](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e243004b863.png "") 这个函数在区间[0,1]中可正可负可为零,单调地在(-∞,+∞)变化,而且1/2刚好就是唯一的0值!基本完美满足我们的要求。 回到我们本章最初的问题, > “我们由点x的坐标得到了一个新的特征z,那么z的具体意义是什么呢?” 由此,我们就可以将z理解成x属于y=1类的概率P经过某种变换后对应的值。也就是说,z= log(P/(1-P))。反过来就是P=![g(z)=1/(1+e-z)](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e243005ad4d.jpg "") 。图像如下: ![sigmoid函数](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e243006ceec.png "") 这两个函数log(P/(1-P)) 、![1/(1+e-z)](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430083463.jpg "") 看起来熟不熟悉? > 这就是传说中的logit函数和sigmoid函数! 小小补充一下: - 在概率理论中,Q=P/(1-P)的意义叫做赔率(odds)。世界杯赌过球的同学都懂哈。赔率也叫发生比,是事件发生和不发生的概率比。 - 而z= log(P/(1-P))的意义就是对数赔率或者对数发生比(log-odds)。 于是,我们不光得到了z的现实意义,还得到了z映射到概率P的拟合方程: > ![P=hθ(x)=g(θ0+θ1X1+θ2X2)= g(z)=1/(1+e-z)](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e24300934b7.jpg "") 有了概率P,我们顺便就可以拿拟合方程P=![g(z)=1/(1+e-z)](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e243005ad4d.jpg "") 来判断点x所属的分类: > 当P>=1/2的时候,就判断点x属于y=1的类别;当P<1/2,就判断点x属于y=0的类别。 ![logit变换](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e24300ab545.png "") ### 五、 构造代价函数求出参数的值 到目前为止我们就有两个判断某点所属分类的办法,一个是判断z是否大于0,一个是判断g(z)是否大于1/2。 然而这并没有什么X用, > 以上的分析都是基于“假设我们已经找到了这条线”的前提得到的,但是最关键的![(θ0,θ1,θ2)](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e24300e078e.jpg "") 三个参数仍未找到有效的办法求出来。 还有没有其他的性质可供我们利用来求出参数![(θ0,θ1,θ2)](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e24300e078e.jpg "") 的值? - 我们漏了一个关键的性质:这些样本点已经被标注了y=0或者y=1的类别! - 我们一方面可以基于z是否大于0或者g(z) 是否大于1/2来判断一个点的类别,另一方又可以依据这些点已经被标注的类别与我们预测的类别的插值来评估我们预测的好坏。 - 这种衡量我们在某组参数下预估的结果和实际结果差距的函数,就是传说中的代价函数Cost Function。 - 当代价函数最小的时候,相应的参数![(θ0,θ1,θ2)](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e24300e078e.jpg "") 就是我们希望的最优解。 由此可见,设计一个好的代价函数,将是我们处理好分类问题的关键。而且不同的代价函数,可能会有不同的结果。因此更需要我们将代价函数设计得解释性强,有现实针对性。 为了衡量“预估结果和实际结果的差距”,我们首先要确定“预估结果”和“实际结果”是什么。 - “实际结果”好确定,就是y=0还是y=1。 - “预估结果”有两个备选方案,经过上面的分析,我们可以采用z或者g(z)。但是显然g(z)更好,因为g(z)的意义是概率P,刚好在[0,1]范围之间,与实际结果{0,1}很相近,而z的意思是逻辑发生比,范围是整个实数域(-∞,+∞),不太好与y={0,1}进行比较。 接下来是衡量两个结果的“差距”。 - 我们首先想到的是y-hθ(x)。 - 但这是当y=1的时候比较好。如果y=0,则y- hθ(x)= - hθ(x)是负数,不太好比较,则采用其绝对值hθ(x)即可。综合表示如下: ![cost1](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e243010733b.png "") - 但这个函数有个问题:求导不太方便,进而用梯度下降法就不太方便。 - 因为梯度下降法超出的初等数学的范围,这里就暂且略去不解释了。 - 于是对上面的代价函数进行了简单的处理,使之便于求导。结果如下: ![cost](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e24301188d8.jpg "") 代价函数确定了,接下来的问题就是机械计算的工作了。常见的方法是用梯度下降法。于是,我们的平面线形可分的问题就可以说是解决了。 ### 六、 从几何变换的角度重新梳理我们刚才的推理过程。 回顾我们的推理过程,我们其实是在不断地将点![x(X1,X2)](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e242ff167b7.jpg "") 进行几何坐标变换的过程。 - 第一步是将分布在整个二维平面的点![x(X1,X2)](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e242ff167b7.jpg "") 通过线性投影映射到一维直线中,成为点x(z) - 第二步是将分布在整个一维直线的点x(z)通过sigmoid函数映射到一维线段[0,1]中成为点x(g(z))。 - 第三步是将所有这些点的坐标通过代价函数统一计算成一个值,如果这是最小值,相应的参数就是我们所需要的理想值。 ![几何变换](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e243013ab00.png "") ### 七、 对于简单的非线性可分的问题。 1.由以上分析可知。比较关键的是第一步,我们之所以能够这样映射是因为假设我们点集是线性可分的。但是如果分离边界是一个圆呢?考虑以下情况。 ![非线性可分](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e243015796d.png "") 2.我们仍用逆推法的思路: - 通过观察可知,分离边界如果是一个圆比较合理。 - 假设我们已经找到了这个圆,再寻找这个圆的性质是什么。根据这些性质,再来反推这个圆的方程。 3.我们可以依据这个性质: - 圆内的点到圆心的距离小于半径,圆外的点到圆心的距离大于半径 - 假设圆的半径为r,空间中任何一个点![x (X1,X2)](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e242ff167b7.jpg "") 到原点的距离为![X12+X22](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430174324.jpg "") 。 - 令![z= X12+X22-r2](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430182c76.jpg "") ,就可以根据z的正负号来判断点x的类别了 - 然后令![P=hθ(x)=g( X12+X22-r2)= g(z)=1/(1+e-z)](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e24301935f7.jpg "") ,就可以继续依靠我们之前的逻辑回归的方法来处理和解释问题了。 4.从几何变换的角度重新梳理我们刚才的推理过程。 - 第一步是将分布在整个二维平面的点![x(X1,X2)](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e242ff167b7.jpg "") 通过某种方式映射到一维直线中,成为点x(z) - 第二步是将分布在整个一维射线的点x(z)通过sigmoid函数映射到一维线段[0,1]中成为点x(g(z))。 - 第三步是将所有这些点的坐标通过代价函数统一计算成一个值v,如果这是最小值,相应的参数就是我们所需要的理想值。 ![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e24301ab6df.png "") ### 八、 从特征处理的角度重新梳理我们刚才的分析过程 其实,做数据挖掘的过程,也可以理解成做特征处理的过程。我们典型的数据挖掘算法,也就是将一些成熟的特征处理过程给固定化的结果。 对于逻辑回归所处理的分类问题,我们已有的特征是这些点的坐标![(X1,X2)](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e242ff167b7.jpg "") ,我们的目标就是判断这些点所属的分类y=0还是y=1。那么最理想的想法就是希望对坐标![(X1,X2)](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e242ff167b7.jpg "") 进行某种函数运算,得到一个(或者一些)新的特征z,基于这个特征z是否大于0来判断该样本所属的分类。 对我们上一节非线性可分问题的推理过程进行进一步抽象,我们的思路其实是: - 第一步,将点![x(X1,X2](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e242ff167b7.jpg "") 的坐标通过某种函数运算,得到一个新的类似逻辑发生比的特征,![z=f(X1,X2)= X12+X22-r2](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e24301d662b.jpg "") - 第二步是将特征z通过sigmoid函数得到新的特征![q=g(z)= 1/(1+e-z)= 1/(1+e-f(X1,X2))](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e24301e82cf.jpg "") 。 - 第三步是将所有这些点的特征q通过代价函数统一计算成一个值![v=J(q1,q2,…)](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430205235.jpg "") ,如果这是最小值,相应的参数(r)就是我们所需要的理想值。 ![特征处理](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430214320.png "") ### 九、 对于复杂的非线性可分的问题 由以上分析可知。比较关键的是第一步,如何设计转换函数![z=f(X1,X2)](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e243022be58.jpg "") 。我们现在开始考虑分离边界是一个极端不规则的曲线的情况。 ![复杂非线性可分](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e243023d7d7.jpg "") 我们仍用逆推法的思路: - 通过观察等先验的知识(或者完全不观察乱猜),我们可以假设分离边界是某种6次曲线(这个曲线方程可以提前假设得非常复杂,对应着各种不同的情况)。 - 第一步:将点![x(X1,X2)](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e242ff167b7.jpg "") 的坐标通过某种函数运算,得到一个新的特征![z=f(X1,X2)=θ0+θ1X1+θ2X2+θ3X12+θ4X1X2+θ5X22+…+θ26X1X25+θ27X26](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430256c02.jpg "") 。并假设z是某种程度的逻辑发生比,通过其是否大于0来判断样本所属分类。 - 第二步:将特征z通过sigmoid函数映射到新的特征![q=g(z)= 1/(1+e-z)](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430264bcc.jpg "") - 第三步:将所有这些样本的特征q通过逻辑回归的代价函数统一计算成一个值![v=J(q1,q2,…)](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430205235.jpg "") ,如果这是最小值,相应的参数![(θ0,θ1,θ2,…, θ27)](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e243027a783.jpg "") 就是我们所需要的理想值。 ### 十、 多维逻辑回归的问题 以上考虑的问题都是基于在二维平面内进行分类的情况。其实,对于高维度情况的分类也类似。 高维空间的样本,其区别也只是特征坐标更多,比如四维空间的点x的坐标为![(X1,X2,X3,X4)](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430293aeb.jpg "") 。但直接运用上文特征处理的视角来分析,不过是对坐标![x(X1,X2,X3,X4)](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430293aeb.jpg "") 进行参数更多的函数运算得到新的特征![z=f(X1,X2,X3,X4)](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e24302b1c5a.jpg "") 。并假设z是某种程度的逻辑发生比,通过其是否大于0来判断样本所属分类。 而且,如果是高维线性可分的情况,则可以有更近直观的理解。 - 如果是三维空间,分离边界就是一个空间中的一个二维平面。两类点在这个二维平面的法向量p上的投影的值的正负号不一样,一类点的投影全是正数,另一类点的投影值全是负数。 ![三维逻辑回归](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e24302c1575.png "") - 如果是高维空间,分离边界就是这个空间中的一个超平面。两类点在这个超平面的法向量p上的投影的值的正负号不一样,一类点的投影全是正数,另一类点的投影值全是负数。 - 特殊的,如果是一维直线空间,分离边界就是直线上的某一点p。一类点在点p的正方向上,另一类点在点p的负方向上。这些点在直线上的坐标可以天然理解成类似逻辑发生比的情况。可见一维直线空间的分类问题是其他所有高维空间投影到法向量后的结果,是所有逻辑回归问题的基础。 ![一维逻辑回归](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e24302dec30.png "") ### 十一、 多分类逻辑回归的问题 以上考虑的问题都是二分类的问题,基本就是做判断题。但是对于多分类的问题,也就是做选择题,怎么用逻辑回归处理呢? ![多分类](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e24302eded6.png "") 其基本思路也是二分类,做判断题。 比如你要做一个三选一的问题,有ABC三个选项。首先找到A与BUC(”U”是并集符号)的分离边界。然后再找B与AUC的分离边界,C与AUB的分离边界。 ![多分类-二分类](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e2430310760.png "") 这样就能分别得到属于A、B、C三类的概率,综合比较,就能得出概率最大的那一类了。 ![最大化](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e24303213f0.png "") ### 十二、 总结列表 为了把本文的关系梳理清楚,我们画了以下这张图表。 ![逻辑回归](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e243033280e.jpg "")
';

机器学习系列(1)_逻辑回归初步

最后更新于:2022-04-01 09:51:54

作者:寒小阳 && 龙心尘 时间:2015年10月。 出处:[http://blog.csdn.net/han_xiaoyang/article/details/49123419](http://blog.csdn.net/han_xiaoyang/article/details/49123419)。 声明:版权所有,转载请注明出处,谢谢。 ### 1、总述 逻辑回归是应用非常广泛的一个分类机器学习算法,它将数据拟合到一个logit函数(或者叫做logistic函数)中,从而能够完成对事件发生的概率进行预测。 ### 2、由来 要说逻辑回归,我们得追溯到线性回归,想必大家对线性回归都有一定的了解,即对于多维空间中存在的样本点,我们用特征的线性组合去拟合空间中点的分布和轨迹。如下图所示: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e242f6117b5.jpg) 线性回归能对连续值结果进行预测,而现实生活中常见的另外一类问题是,分类问题。最简单的情况是是与否的二分类问题。比如说医生需要判断病人是否生病,银行要判断一个人的信用程度是否达到可以给他发信用卡的程度,邮件收件箱要自动对邮件分类为正常邮件和垃圾邮件等等。 当然,我们最直接的想法是,既然能够用线性回归预测出连续值结果,那根据结果设定一个阈值是不是就可以解决这个问题了呢?事实是,对于很标准的情况,确实可以的,这里我们套用Andrew Ng老师的课件中的例子,下图中X为数据点肿瘤的大小,Y为观测结果是否是恶性肿瘤。通过构建线性回归模型,如hθ(x)所示,构建线性回归模型后,我们设定一个阈值0.5,预测hθ(x)≥0.5的这些点为恶性肿瘤,而hθ(x)<0.5为良性肿瘤。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e242fbb1c6d.jpg) 但很多实际的情况下,我们需要学习的分类数据并没有这么精准,比如说上述例子中突然有一个不按套路出牌的数据点出现,如下图所示: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e242fbd03df.jpg) 你看,现在你再设定0.5,这个判定阈值就失效了,而现实生活的分类问题的数据,会比例子中这个更为复杂,而这个时候我们借助于线性回归+阈值的方式,已经很难完成一个鲁棒性很好的分类器了。 在这样的场景下,逻辑回归就诞生了。它的核心思想是,如果线性回归的结果输出是一个连续值,而值的范围是无法限定的,那我们有没有办法把这个结果值映射为可以帮助我们判断的结果呢。而如果输出结果是 (0,1) 的一个概率值,这个问题就很清楚了。我们在数学上找了一圈,还真就找着这样一个简单的函数了,就是很神奇的sigmoid函数(如下): ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e242fbf0eb2.jpg) 如果把sigmoid函数图像画出来,是如下的样子: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e242fc0bd83.jpg) Sigmoid Logistic Function 从函数图上可以看出,函数y=g(z)在z=0的时候取值为1/2,而随着z逐渐变小,函数值趋于0,z逐渐变大的同时函数值逐渐趋于1,而这正是一个概率的范围。 所以我们定义线性回归的预测函数为Y=WTX,那么逻辑回归的输出Y= g(WTX),其中y=g(z)函数正是上述sigmoid函数(或者简单叫做S形函数)。 ### 3、判定边界 我们现在再来看看,为什么逻辑回归能够解决分类问题。这里引入一个概念,叫做判定边界,可以理解为是用以对不同类别的数据分割的边界,边界的两旁应该是不同类别的数据。 从二维直角坐标系中,举几个例子,大概是如下这个样子: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e242fc24c17.jpg) 有时候是这个样子: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e242fc4e205.jpg) 甚至可能是这个样子: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e242fc8317a.jpg) 上述三幅图中的红绿样本点为不同类别的样本,而我们划出的线,不管是直线、圆或者是曲线,都能比较好地将图中的两类样本分割开来。这就是我们的判定边界,下面我们来看看,逻辑回归是如何根据样本点获得这些判定边界的。 我们依旧借用Andrew Ng教授的课程中部分例子来讲述这个问题。 回到sigmoid函数,我们发现:   当g(z)≥0.5时, z≥0; 对于hθ(x)=g(θTX)≥0.5, 则θTX≥0, 此时意味着预估y=1; 反之,当预测y = 0时,θTX<0; 所以我们认为θTX =0是一个决策边界,当它大于0或小于0时,逻辑回归模型分别预测不同的分类结果。 先看第一个例子hθ(x)=g(θ0+θ1X1+θ2X2),其中θ0 ,θ1 ,θ2分别取-3, 1, 1。则当−3+X1+X2≥0时, y = 1; 则X1+X2=3是一个决策边界,图形表示如下,刚好把图上的两类点区分开来: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e242fcbf50d.jpg) 例1只是一个线性的决策边界,当hθ(x)更复杂的时候,我们可以得到非线性的决策边界,例如: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e242fcd5fe6.jpg) 这时当x12+x22≥1时,我们判定y=1,这时的决策边界是一个圆形,如下图所示: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e242fce881b.jpg) 所以我们发现,理论上说,只要我们的hθ(x)设计足够合理,准确的说是g(θTx)中θTx足够复杂,我们能在不同的情形下,拟合出不同的判定边界,从而把不同的样本点分隔开来。 ### 4、代价函数与梯度下降 我们通过对判定边界的说明,知道会有合适的参数θ使得θTx=0成为很好的分类判定边界,那么问题就来了,我们如何判定我们的参数θ是否合适,有多合适呢?更进一步,我们有没有办法去求得这样的合适参数θ呢? 这就是我们要提到的代价函数与梯度下降了。 所谓的代价函数Cost Function,其实是一种衡量我们在这组参数下预估的结果和实际结果差距的函数,比如说线性回归的代价函数定义为: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e242fd0b004.jpg) 当然我们可以和线性回归类比得到一个代价函数,实际就是上述公式中hθ(x)取为逻辑回归中的g(θTx),但是这会引发代价函数为“非凸”函数的问题,简单一点说就是这个函数有很多个局部最低点,如下图所示: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e242fd20c09.png) 而我们希望我们的代价函数是一个如下图所示,碗状结构的凸函数,这样我们算法求解到局部最低点,就一定是全局最小值点。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e242fd32027.png) 因此,上述的Cost Function对于逻辑回归是不可行的,我们需要其他形式的Cost Function来保证逻辑回归的成本函数是凸函数。 我们跳过大量的数学推导,直接出结论了,我们找到了一个适合逻辑回归的代价函数: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e242fd48035.jpg) Andrew Ng老师解释了一下这个代价函数的合理性,我们首先看当y=1的情况: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e242fd5e534.png) 如果我们的类别y = 1, 而判定的hθ(x)=1,则Cost = 0,此时预测的值和真实的值完全相等,代价本该为0;而如果判断hθ(x)→0,代价->∞,这很好地惩罚了最后的结果。 而对于y=0的情况,如下图所示,也同样合理: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e242fd7270d.png) 下面我们说说梯度下降,梯度下降算法是调整参数θ使得代价函数J(θ)取得最小值的最基本方法之一。从直观上理解,就是我们在碗状结构的凸函数上取一个初始值,然后挪动这个值一步步靠近最低点的过程,如下图所示: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e242fd86178.jpg) 我们先简化一下逻辑回归的代价函数: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e242fdab5b4.png) 从数学上理解,我们为了找到最小值点,就应该朝着下降速度最快的方向(导函数/偏导方向)迈进,每次迈进一小步,再看看此时的下降最快方向是哪,再朝着这个方向迈进,直至最低点。 用迭代公式表示出来的最小化J(θ)的梯度下降算法如下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e242fdc1ba8.png) ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e242fdd89aa.png) ### 5、代码与实现 我们来一起看两个具体数据上做逻辑回归分类的例子,其中一份数据为线性判定边界,另一份为非线性。 示例1。 第一份数据为data1.txt,部分内容如下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e242fdf200b.jpg) 我们先来看看数据在空间的分布,代码如下。 ~~~ from numpy import loadtxt, where from pylab import scatter, show, legend, xlabel, ylabel #load the dataset data = loadtxt('/home/HanXiaoyang/data/data1.txt', delimiter=',') X = data[:, 0:2] y = data[:, 2] pos = where(y == 1) neg = where(y == 0) scatter(X[pos, 0], X[pos, 1], marker='o', c='b') scatter(X[neg, 0], X[neg, 1], marker='x', c='r') xlabel('Feature1/Exam 1 score') ylabel('Feature2/Exam 2 score') legend(['Fail', 'Pass']) show() ~~~ 得到的结果如下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e242fe1a5bb.jpg) 下面我们写好计算sigmoid函数、代价函数、和梯度下降的程序: ~~~ def sigmoid(X):    '''Compute sigmoid function '''    den =1.0+ e **(-1.0* X)    gz =1.0/ den    return gz def compute_cost(theta,X,y):    '''computes cost given predicted and actual values'''    m = X.shape[0]#number of training examples    theta = reshape(theta,(len(theta),1))        J =(1./m)*(-transpose(y).dot(log(sigmoid(X.dot(theta))))- transpose(1-y).dot(log(1-sigmoid(X.dot(theta)))))        grad = transpose((1./m)*transpose(sigmoid(X.dot(theta))- y).dot(X))    #optimize.fmin expects a single value, so cannot return grad    return J[0][0]#,grad def compute_grad(theta, X, y):    '''compute gradient'''    theta.shape =(1,3)    grad = zeros(3)    h = sigmoid(X.dot(theta.T))    delta = h - y    l = grad.size    for i in range(l):        sumdelta = delta.T.dot(X[:, i])        grad[i]=(1.0/ m)* sumdelta *-1    theta.shape =(3,)    return  grad ~~~ 我们用梯度下降算法得到的结果判定边界是如下的样子: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e242fe3c8c7.jpg) 最后我们使用我们的判定边界对training data做一个预测,然后比对一下准确率: ~~~ def predict(theta, X):    '''Predict label using learned logistic regression parameters'''    m, n = X.shape    p = zeros(shape=(m,1))    h = sigmoid(X.dot(theta.T))    for it in range(0, h.shape[0]):        if h[it]>0.5:            p[it,0]=1        else:            p[it,0]=0    return p #Compute accuracy on our training set p = predict(array(theta), it) print'Train Accuracy: %f'%((y[where(p == y)].size / float(y.size))*100.0) ~~~ 计算出来的结果是89.2% 示例2. 第二份数据为data2.txt,部分内容如下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e242fe64081.jpg) 我们同样把数据的分布画出来,如下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e242fe77a16.jpg) 我们发现在这个例子中,我们没有办法再用一条直线把两类样本点近似分开了,所以我们打算试试多项式的判定边界,那么我们先要对给定的两个feature做一个多项式特征的映射。比如说,我们做了如下的一个映射: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e242fe9f229.jpg) 代码如下: ~~~ def map_feature(x1, x2):    '''    Maps the two input features to polonomial features.    Returns a new feature array with more features of    X1, X2, X1 ** 2, X2 ** 2, X1*X2, X1*X2 ** 2, etc...    '''    x1.shape =(x1.size,1)    x2.shape =(x2.size,1)    degree =6    mapped_fea = ones(shape=(x1[:,0].size,1))    m, n = mapped_fea.shape    for i in range(1, degree +1):        for j in range(i +1):            r =(x1 **(i - j))*(x2 ** j)            mapped_fea = append(out, r, axis=1)    return mapped_fea mapped_fea = map_feature(X[:,0], X[:,1]) ~~~ 接着做梯度下降: ~~~ def cost_function_reg(theta, X, y, l):    '''Compute the cost and partial derivatives as grads    '''    h = sigmoid(X.dot(theta))    thetaR = theta[1:,0]    J =(1.0/ m)*((-y.T.dot(log(h)))-((1- y.T).dot(log(1.0- h)))) \            +(l /(2.0* m))*(thetaR.T.dot(thetaR))    delta = h - y    sum_delta = delta.T.dot(X[:,1])    grad1 =(1.0/ m)* sumdelta    XR = X[:,1:X.shape[1]]    sum_delta = delta.T.dot(XR)    grad =(1.0/ m)*(sum_delta + l * thetaR)    out = zeros(shape=(grad.shape[0], grad.shape[1]+1))    out[:,0]= grad1    out[:,1:]= grad    return J.flatten(), out.T.flatten() m, n = X.shape y.shape =(m,1) it = map_feature(X[:,0], X[:,1]) #Initialize theta parameters initial_theta = zeros(shape=(it.shape[1],1)) #Use regularization and set parameter lambda to 1 l =1 # Compute and display initial cost and gradient for regularized logistic # regression cost, grad = cost_function_reg(initial_theta, it, y, l) def decorated_cost(theta):    return cost_function_reg(theta, it, y, l) print fmin_bfgs(decorated_cost, initial_theta, maxfun=500) ~~~ 接着在数据点上画出判定边界: ~~~ #Plot Boundary u = linspace(-1,1.5,50) v = linspace(-1,1.5,50) z = zeros(shape=(len(u), len(v))) for i in range(len(u)):    for j in range(len(v)):        z[i, j]=(map_feature(array(u[i]), array(v[j])).dot(array(theta))) z = z.T contour(u, v, z) title('lambda = %f'% l) xlabel('Microchip Test 1') ylabel('Microchip Test 2') legend(['y = 1','y = 0','Decision boundary']) show() def predict(theta, X):    '''Predict whether the label    is 0 or 1 using learned logistic    regression parameters '''    m, n = X.shape    p = zeros(shape=(m,1))    h = sigmoid(X.dot(theta.T))    for it in range(0, h.shape[0]):        if h[it]>0.5:            p[it,0]=1        else:            p[it,0]=0    return p #% Compute accuracy on our training set p = predict(array(theta), it) print'Train Accuracy: %f'%((y[where(p == y)].size / float(y.size))*100.0) ~~~ 得到的结果如下图所示: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-11_56e242feba06a.jpg) 我们发现我们得到的这条曲线确实将两类点区分开来了。 ### 6、总结 最后我们总结一下逻辑回归。它始于输出结果为有实际意义的连续值的线性回归,但是线性回归对于分类的问题没有办法准确而又具备鲁棒性地分割,因此我们设计出了逻辑回归这样一个算法,它的输出结果表征了某个样本属于某类别的概率。 逻辑回归的成功之处在于,将原本输出结果范围可以非常大的θTX 通过sigmoid函数映射到(0,1),从而完成概率的估测。 而直观地在二维空间理解逻辑回归,是sigmoid函数的特性,使得判定的阈值能够映射为平面的一条判定边界,当然随着特征的复杂化,判定边界可能是多种多样的样貌,但是它能够较好地把两类样本点分隔开,解决分类问题。 求解逻辑回归参数的传统方法是梯度下降,构造为凸函数的代价函数后,每次沿着偏导方向(下降速度最快方向)迈进一小部分,直至N次迭代后到达最低点。 ### 7、补充 本文的2份数据可在[http://pan.baidu.com/s/1pKxJl1p](http://pan.baidu.com/s/1pKxJl1p)上下载到,分别为data1.txt和data2.txt,欢迎大家自己动手尝试。
';

前言

最后更新于:2022-04-01 09:51:52

> 原文出处:[机器学习与数据挖掘](http://blog.csdn.net/column/details/machine-learning-dm.html) 作者:[han_xiaoyang](http://blog.csdn.net/han_xiaoyang) **本系列文章经作者授权在看云整理发布,未经作者允许,请勿转载!** # 机器学习与数据挖掘 > 本系列将涵盖机器学习与数据挖掘领域最常用的算法,包括有监督学习分类/回归算法与无监督学习聚类等算法,以及神经网络。并试图给出机器学习/数据挖掘算法解决实际问题的思路,方法与技巧。欢迎大家关注。
';