这两天仔细研究了一下SVM的底层数学原理,感觉大学基础数学没好好学,现在看起来真的很吃力啊~~
整理了几篇关于SVM原理的文章,大家有兴趣可以看看。
1. jasper的SVM入门教程,总共有9篇,作者文笔了得,深入浅出,将很复杂抽象的概念都能讲解的特别清楚,非常值得一看。
2.July、pluskid的支持向量机通俗导论(理解SVM的三层境界),讲得十分详细,很多细节都做了推导。
3.勿在浮砂筑高台【机器学习详解】SVM解二分类,多分类,及后验概率输出以及【机器学习详解】SMO算法剖析

由于编辑公式很繁琐,我在纸上进行了推导。主要参考了勿在浮砂筑高台的推导过程!

 

在文本分类中有这样一个场景,当我们已经分好词,并构造出词频向量后,这个向量会很大,经常会多达几万维,甚至十几万维。这种规模的模型如果要用SVM等较高级的机器学习进行训练的话,那简直是慢的要死,深度学习就再别谈了。为了较少向量维度,我们可以采用一些方法,比如在词向量中过滤掉词频小于N的词,这个N可以自己定,但一般取值比较小。采用这种方法能有效降低向量维度,但是还是不够。我们不能为了降低向量维度到一定的程度而不断地增大N值,因为这种过滤掉低频次的方法,对向量维度的减少是随着N的增大而逐步降低的,比如说:将N=1,你可以过滤掉10000个单词;接着N=2,你过滤掉了15000个单词;N=3,过滤掉16000个单词;N=4,过滤掉了16800个单词。所以这种方法适用于粗糙地降维。我们可以在原始数据集上利用这种方法先过滤一波词。然后再想别的方法。

那么怎样才能将向量的维度降到我们想要的大小,而不会对整个系统的信息产生重大影响呢?这个问题就是特征选择的问题。
特征选择有2个很明显的优点:
1. 减少特征数量、降维,使模型泛化能力更强,减少过拟合
2. 增强对特征和特征值之间的理解

一般对于文本分类有两种最常用的信息选择方法。一个是卡方检验,另一个是信息增益。但凡是特征选择,总是在将特征的重要程度量化之后再进行选择,而如何量化特征的重要性,就成了各种方法间最大的不同。开方检验中使用特征与类别间的关联性来进行这个量化,关联性越强,特征得分越高,该特征越应该被保留。

在信息增益中,重要性的衡量标准就是看特征能够为分类系统带来多少信息,带来的信息越多,该特征越重要。本文只针对信息增益,卡方检验请自行谷歌度娘。

关于信息增益,大牛Jasper有一篇十分精彩的博文来进行介绍,其计算方法Jasper已经说得十分清楚了。建议你看完这篇文章,再来继续阅读本文。

信息增益最后的推导公式为
我们现在要用python来实现信息增益的计算。
假设我们有4篇文档,每篇文档都已经分好词了:

文档1: 鲜花 体育
文档2: 太阳 大象 体育
文档3:  大象

假设我们词的索引是:

[鲜花,太阳,大象,体育]

那么我们的文档构造好的词频向量分别是:

文档1:[1,0,0,1]:
文档2:[0,1,1,1]
文档3:[0,0,1,0]

假设3篇文档的分类分别是:文档1和文档2属于“0”类,文档3属于“1”类。

那么我们程序的输入就是X,y:
X代表词频向量组成的矩阵

[[1,0,0,1],
 [0,1,1,1],
 [0,0,1,0]]

y代表标签向量

[0,0,1]

我们利用numpy包中的矩阵来包装X,因为它可以提供许多方便的高级函数。
下面就贴上代码,代码里有注释,有兴趣的同学可以好好研究一下,如果你想拿来直接用的话,也是没问题的。代码的效率应该还是可以的,其中有一个比较巧妙的矩阵的转秩。
可以好好理解一下:

#coding: utf-8
import numpy as np
'''
    计算信息增益
    powerd by ayonel
'''

class InformationGain:
    def __init__(self, X, y):
        self.X = X
        self.y = y
        self.totalSampleCount = X.shape[0]      # 样本总数
        self.totalSystemEntropy = 0             # 系统总熵
        self.totalClassCountDict = {}           # 存储每个类别的样本数量是多少
        self.nonzeroPosition = X.T.nonzero()    # 将X转置之后输出非零值的位置
        self.igResult = []                      # 保存结果的list
        self.wordExistSampleCount = 0
        self.wordExistClassCountDict = {}
        self.iter()


    # 将结果列表排序输出
    def get_result(self):
        return self.igResult

    # 计算系统总熵
    def cal_total_system_entropy(self):
        # 计算每个类别各有多少个
        for label in self.y:
            if label not in self.totalClassCountDict:
                self.totalClassCountDict[label] = 1
            else:
                self.totalClassCountDict[label] += 1
        for cls in self.totalClassCountDict:
            probs = self.totalClassCountDict[cls] / float(self.totalSampleCount)
            self.totalSystemEntropy -= probs * np.log(probs)


    # 遍历nonzeroPosition时,逐步计算出每个word的信息增益
    def iter(self):
        self.cal_total_system_entropy()

        pre = 0
        for i in range(len(self.nonzeroPosition[0])):
            if i != 0 and self.nonzeroPosition[0][i] != pre:
                for notappear in range(pre+1, self.nonzeroPosition[0][i]):  # 如果一个词在整个样本集中都未出现,则直接赋为0
                    self.igResult.append(0.0)
                ig = self.cal_information_gain()
                self.igResult.append(ig)
                self.wordExistSampleCount = 0
                self.wordExistClassCountDict = {}
                pre = self.nonzeroPosition[0][i]
            self.wordExistSampleCount += 1
            yclass = self.y[self.nonzeroPosition[1][i]]  # 求得当前样本的标签
            if yclass not in self.wordExistClassCountDict:
                self.wordExistClassCountDict[yclass] = 1
            else:
                self.wordExistClassCountDict[yclass] += 1
        # 计算最后一个单词的ig
        ig = self.cal_information_gain()
        self.igResult.append(ig)

    # 计算ig的主要函数
    def cal_information_gain(self):
        x_exist_entropy = 0
        x_nonexist_entropy = 0

        for cls in self.wordExistClassCountDict:
            probs = self.wordExistClassCountDict[cls] / float(self.wordExistSampleCount)
            x_exist_entropy -= probs * np.log(probs)

            probs = (self.totalClassCountDict[cls] - self.wordExistClassCountDict[cls]) / float(self.totalSampleCount - self.wordExistSampleCount)
            if probs == 0: #该单词在每条样本中都出现了,虽然该几率很小
                x_nonexist_entropy = 0
            else:
                x_nonexist_entropy -= probs*np.log(probs)

        for cls in self.totalClassCountDict:
            if cls not in self.wordExistClassCountDict:
                probs = self.totalClassCountDict[cls] / float(self.totalSampleCount - self.wordExistSampleCount)
                x_nonexist_entropy -= probs*np.log(probs)

        # 合并两项,计算出ig
        ig = self.totalSystemEntropy - ((self.wordExistSampleCount/float(self.totalSampleCount))*x_exist_entropy +
                                        ((self.totalSampleCount-self.wordExistSampleCount)/float(self.totalSampleCount)*x_nonexist_entropy))
        return ig
if __name__ == '__main__':
    X = np.array([[1,0,0,1],[0,1,1,1],[0,0,1,0]])
    y = [0,0,1]
    ig = InformationGain(X, y)
    print(ig.get_result())

以上程序运行结果:

[0.17441604792151594, 0.17441604792151594, 0.17441604792151594, 0.63651416829481278]

上面的结果代表着
鲜花的信息增益:0.17441604792151594
太阳的信息增益:0.17441604792151594
大象的信息增益:0.17441604792151594
体育的信息增益:0.63651416829481278

乍一看题目,使用”meka“实现分类器,是不是笔者打错了啊,应该是weka啊。其实不然,这世上还真有一个叫meka的包,并且他还跟weka算是亲戚关系。只不过,这个meka是一个专门用来解决多标签分类问题的包,并且是基于weka的,可以算作是weka的一个扩展。

那好,什么是多标签分类呢?我们先来看一个示例:如果我有一篇文章,我要推断它是不是“体育”类新闻,那么分类标签就只有“是”或者“不是”,这种问题就是经典的二分类(binary classifier)问题。好,再复杂一点,如果我们要判断这篇文章是属于“体育”、“财经”、“社会”中的哪一类?那么这个问题就变成了一个多类分类问题(multiclass classifier),注意,对于每篇文章它只能属于“体育”、“财经”、“社会”中的某一类,不能属于多个类。也就是我们的样本的标签可以是[0,0,1]或者[0,1,0]或者[1,0,0],但绝不可能是[1,1,0]或者[1,0,1]等。好了,与多分类对应的就是多标签分类(multilabel classifier),它是解决什么问题的呢?如果一篇文章,它既能属于“体育”类,又能属于“财经类”,甚至还能属于“社会”类,也就是我们待分类的文章有可能同时分到多个类中去,对于这样的分类我们就叫多标签分类。

明白了多标签分类的概念,我们怎么实现呢?其实说到底,多类分类,多标签分类,其实底层都是二分类组合。我们当然可以自己手动实现。但有没有已有的库来帮助我们实现呢?当然有!!!sklearn中已经有实现多标签分类的方法了,它的底层支持可以是Decision Trees, Random Forests以及Nearest Neighbors. 不过我们这次不使用我们优雅的sklearn,要跳到JAVA的坑去(好桑心)。JAVA中比较有名的多标签分类包是mulan以及meka,我们今天只讲meka,至于mulan的实现方法其实也跟meka很像,甚至在meka中都已经集成了mulan的jar包。

meka是由三个大牛Jesse Read、Peter Reutemann、Joerg Wicker实现,并且现在已经开源到github了,与weka其实是师出同门。meka的使用基本与weka一致,它甚至到包含一个客户端,长相也跟weka很像。
meka
对于图形界面的使用,可以去看看它们的官方教程,很详细。我们今天来说一说如何使用集成meka-1.9.0.jar来进行编程实现。这个jar包存在于lib文件夹下。

meka也是用arff文件作为输入。官方文档中有这么一句话:”A suitable dataset is the only requirement to begin running experiments with Meka.”,足见meka使用之方便~~
meka中已经自带了几个用于实现多标签分类的数据集在data目录下,这个数据集是描述某段音频的特征,以及它们属于哪些分类(是悲伤类的还是安静类的亦或是疯狂摇滚的…)我们来看看大概看看,每行%后面是我自己加的注释:

% 'Music' dataset; normalised version.
@relation 'Music: -C 6'                       %代表标签一共有6类

@attribute amazed-suprised {0,1}              %第一个分类标签,取值是0或者1,代表是或者不是
@attribute happy-pleased {0,1}                %第二个分类标签
@attribute relaxing-clam {0,1}                %第三个分类标签
@attribute quiet-still {0,1}                  %第四个分类标签
@attribute sad-lonely {0,1}                   %第五个分类标签
@attribute angry-aggresive {0,1}              %第六个分类标签
@attribute Mean_Acc1298_Mean_Mem40_Centroid numeric  %第一个特征,numeric类型
@attribute Mean_Acc1298_Mean_Mem40_Rolloff numeric   %第二个特征,numeric类型
@attribute Mean_Acc1298_Mean_Mem40_Flux numeric      %第三个特征,numeric类型
@attribute Mean_Acc1298_Mean_Mem40_MFCC_0 numeric    %第四个特征,numeric类型
@attribute Mean_Acc1298_Mean_Mem40_MFCC_1 numeric    %第五个特征,numeric类型
@attribute Mean_Acc1298_Mean_Mem40_MFCC_2 numeric    %第六个特征,numeric类型
@attribute Mean_Acc1298_Mean_Mem40_MFCC_3 numeric    %第七个特征,numeric类型
@attribute Mean_Acc1298_Mean_Mem40_MFCC_4 numeric    %第八个特征,numeric类型
@attribute Mean_Acc1298_Mean_Mem40_MFCC_5 numeric    %第九个特征,numeric类型
....
@data

0,1,1,0,0,0,0.132498,0.077848,0.229227,0.602629,0.512861,0.467404,0.529733,0.573498,0.592831,0.520031,0.598853,0.537699,0.780658,0.462982,0.407108,0.684364,0.135824,0.245631,0.157515,0.301285,0.350107,0.459476,0.583274,0.430053,0.416198,0.581916,0.342758,0.309345,0.388929,0.323521,0.455207,0.26139,0.027559,0.149077,0.195433,0.571354,0.326404,0.246745,0.524645,0.354798,0.240244,0.239788,0.128689,0.173252,0.204863,0.131632,0.245653,0.144607,0.258203,0.470051,0.259909,0.61364,0.458314,0.434716,0.448941,0.370609,0.285647,0.663082,0.29708,0.273671,0.286411,0.197026,0.196244,0.164323,0.030017,0.253968,0.008473,0.240602,0.136735,0.058442,0.107594  %第一条数据
1,0,0,0,0,1,0.384281,0.355249,0.16719,0.853089,0.260577,0.332757,0.15393,0.519381,0.268043,0.251955,0.459922,0.430814,0.654323,0.641021,0.356511,0.647367,0.367659,0.539078,0.100569,0.133502,0.337194,0.319752,0.349012,0.171182,0.191357,0.390569,0.289253,0.208641,0.341328,0.265669,0.273736,0.181791,0.028513,0.252827,0.25819,0.011351,0.236247,0.069285,0.192754,0.154258,0.128671,0.116726,0.059704,0.073697,0.080341,0.062701,0.075672,0.041256,0.207782,0.300735,0.888274,0.444,0.294673,0.210429,0.132036,0.167474,0.205996,0.155514,0.086631,0.071462,0.067492,0.093526,0.085649,0.025101,0.182955,0.285714,0.156764,0.270677,0.191377,0.153728,0.197951  %第二条数据

与weka有点区别的是,对于标签在每个样本集中的位置,weka一般是放在每行末尾,虽然可以放在每行的任意位置,可以通过Instances.setClassIndex(n)方法,来指定每行的第n位是属于分类标签。
先来上代码吧:

package com.ayonel.meka;
import meka.classifiers.multilabel.BR;
import weka.classifiers.bayes.NaiveBayesMultinomialUpdateable;
import weka.core.Instances;
import weka.core.converters.ArffLoader;

import java.io.File;

/**
 * @author ayonel
 * @created_at 16/9/27
 */


public class YYRec {
    public static void main(String[] args) throws Exception{
        ArffLoader loader = new ArffLoader();
        loader.setFile(new File("testdata/Music.arff")); //读取测试集
        loader.getStructure();
       
        Instances originDataSet = loader.getDataSet(); //获取测试集
        originDataSet.setClassIndex(6); //这一步是指定前6个元素作为分类标签
        BR br = new BR(); //建立分类器
        br.buildClassifier(originDataSet);//开始训练
        double [] result = br.distributionForInstance(originDataSet.instance(0));//对训练集中的第一个实例进行预测
        //输出每个标签的预测结果(为1的概率值)
        for(int i=0; i<result.length; i++) {
            System.out.println(result[i]);
        }
    }
}

输出结果:

0.0
1.0
0.9859154929577465
0.034482758620689655
0.0
0.013452914798206279

甚至还可以使用多标签分类进行增量学习,使用:
meka.classifiers.multilabel.incremental.BRUpdateable类,该类底层使用weka.classifiers.trees.HoeffdingTree来实现。

以前写过一篇博文是利用weka来实现多项式朴素贝叶斯增量学习。这次利用python中的sklearn学习工具来实现。其实个人感觉像机器学习,数据处理这种活本来就应该让我们高雅而优美的Python来实现,Java你瞎凑什么热闹啊~~

与weka的实现相比,sklearn果然还是继承了Python简洁优雅的传统,实现起来十分方便。如何判断sklearn中分类器是否支持增量学习(incremental learning & online learning)呢?很简单,只需要查阅sklearn文档,如果该分类器存在partial_fit()方法,即说明它支持增量学习。
比如我们的多项式朴素贝叶斯中就有该方法:
sklearn

下面我以一个十分简单的实例来进行演示:

比如我们有三份数据集,先用数据集1进行训练,然后预测数据集3;然后再利用数据集2再对模型进行更新,之后再预测数据集3。
数据集1:‘[ ]’中代表样本向量, ‘#’后面代表样本标签

 [1 1 0 1 2 0 3 1 2 2]    # 1
 [0 3 1 1 2 2 1 3 2 1]    # 0
 [0 3 0 3 2 2 2 1 4 0]    # 0
 [1 4 3 2 4 0 2 1 3 4]    # 1
 [4 4 3 4 0 3 4 2 4 4]    # 0
 [0 2 1 3 3 1 0 1 2 3]    # 1

数据集2:

 [0 1 2 2 2 1 0 0 2 3]    # 0
 [2 1 2 1 0 3 1 2 1 1]    # 1
 [4 0 3 3 3 4 2 2 4 2]    # 0
 [1 3 3 4 1 4 3 0 3 3]    # 1
 [1 4 4 0 3 4 0 2 2 2]    # 1
 [1 4 0 4 1 0 4 1 4 1]    # 1

数据集3(测试集,没有标签):

 [1 4 1 3 0 3 1 2 3 4]
 [0 2 0 2 3 2 0 0 2 2]
 [0 3 4 3 0 1 4 4 4 2]
 [0 4 3 0 2 2 2 1 0 3]
 [4 3 1 3 3 2 2 0 0 1]
 [3 0 3 2 4 2 1 2 1 0]

代码十分简单,先来看看吧:

import numpy as np
#构造数据集,每份数据集是6*10的随机生成的矩阵,代表有6个样本,每个样本中有10个向量。
X1 = np.random.randint(5, size=(6, 10))#数据集1
X2 = np.random.randint(5, size=(6, 10))#数据集2
X3 = np.random.randint(5, size=(6, 10))#数据集3

#打印出3份数据集
print(X1)
print(X2)
print(X3)

y1 = np.array([1, 0, 0, 1, 0, 1])#数据集1中每份数据对应的标签
y2 = np.array([0, 1, 0, 1, 1, 1])#数据集2中每份数据对应的标签

#所有数据集中出现的分类标签
labels = np.array([0, 1])

from sklearn.naive_bayes import MultinomialNB
clf = MultinomialNB() #定义分类器

#先利用X1与y1进行部分训练
print('start tranning...')
clf.partial_fit(X1, y1, classes=labels)#核心方法,部分训练,第一次partial_fit,需要在classes参数中给出所有标签
print('第一次预测结果:'+str(clf.predict(X3[:]))) #打印出对数据集3的预测结果

#再利用X2与y2进行模型更新
clf.partial_fit(X2, y2)#
print('更新后预测结果:'+str(clf.predict(X3[:]))) #再打印出对数据集3的预测结果

运行结果为:(由于数据集是随机生成的,所以每次的结果均会不同)

[[1 1 0 1 2 0 3 1 2 2]
 [0 3 1 1 2 2 1 3 2 1]
 [0 3 0 3 2 2 2 1 4 0]
 [1 4 3 2 4 0 2 1 3 4]
 [4 4 3 4 0 3 4 2 4 4]
 [0 2 1 3 3 1 0 1 2 3]]
[[0 1 2 2 2 1 0 0 2 3]
 [2 1 2 1 0 3 1 2 1 1]
 [4 0 3 3 3 4 2 2 4 2]
 [1 3 3 4 1 4 3 0 3 3]
 [1 4 4 0 3 4 0 2 2 2]
 [1 4 0 4 1 0 4 1 4 1]]
[[1 4 1 3 0 3 1 2 3 4]
 [0 2 0 2 3 2 0 0 2 2]
 [0 3 4 3 0 1 4 4 4 2]
 [0 4 3 0 2 2 2 1 0 3]
 [4 3 1 3 3 2 2 0 0 1]
 [3 0 3 2 4 2 1 2 1 0]]
start tranning...
第一次预测结果:[0 1 0 1 0 0]
更新后预测结果:[1 1 1 1 1 0]

最终的预测结果表示的是对数据集3中的6个样本,分别预测出的标签。
如果最终需要的结果不是预测出类别,而是需要每个类别精确的概率值。
可以采用方法clf.predict_proba()

GitHub API其实是一座宝藏,它拥有着海量项目以及开发者的各类信息,可以作为社交编程以及经验软件工程课题的数据载体。

本次教程较大家如何使用scrapy来爬取GitHub API,抓取我们所需要的特定信息。GitHub API 是GitHub基于OAuth2协议开放出来的数据获取接口,我们能够在GitHub API上获取各类信息,比如一个项目的commit,issue,pull request;一个用户的粉丝,关注,提交活动,评论等等。GitHub API 有着详细的官方文档教程,上面各类数据的获取接口地址,以及一些过滤参数等。本教程将以爬取rails的issue信息为例,教大家如何使用scrapy来爬取GitHub API。

再开始爬取之前,我们需要进行一些准备工作,由于GitHub API采用OAuth2认证,需要我们提供认证token。当然不提供token可以进行爬取,但是爬取速率会大大降低。

For requests using Basic Authentication or OAuth, you can make up to 5,000 requests per hour. For unauthenticated requests, the rate limit allows you to make up to 60 requests per hour. Unauthenticated requests are associated with your IP address, and not the user making requests. Note that the Search API has custom rate limit rules.

上面显示如果我们提供token可以每小时进行5000次请求,对于非认证(不提供token)一小时只能提供60次的请求,超出请求速率限制后,会返回状态码403 forbidden。有人会问,即使认证后5000次/小时的速率也有些慢啊,如果要更快地进行爬取,那就多注册几个GitHub账号,同时利用多个账号的token进行爬取。GitHub的爬取限制针对的是每个用户,而不是IP,也就是同一台机器,只要你保证每个账号每小时进行小于5000次的爬取,也是完全没有问题的。

接下来我们来展示如何获取自己的token,其实全名为personal access token,每个账号可以拥有多个token,所以不小心哪一天忘了的话,重新生成一个新的即可。

首先我们需要登录自己的GitHub账号。然后在设置里面有一个personal access token选项:
QQ截图20160830092436
点进去,再点击Generate new token .

选择你想让你的token拥有的权限,一般默认全选即可。点击绿色的Generate token按钮。
QQ截图20160830092627

这样就生成我们的token了。注意一定要在这时候把token保存下来,这将是你在GitHub上最后一次看见你的token。一旦刷新,token就会隐藏掉。这时候只有生成新的token了。

QQ截图20160830092306

好了,有了预先的准备工作,接下来就可以编写爬虫了。这次爬虫采用scrapy,关于scrapy相关的知识就不再赘述了,网上有很多教程。

简单说一下爬取思路。

GitHub API 返回的数据都是json,这极大地方便了数据的解析。我们本次的任务是爬取rails项目的所有issue,先来大概看一下返回的issue是什么样子。由于内容太长,我就直接放链接,大家可以点击去看。

https://api.github.com/repos/rails/rails/issues

我们可以看到返回的页面是一个json数组,每个元素其实就是一个issue.而每个issue里面又有诸多信息,比如number,title,body等等。我们本次任务就爬取rails所有issue的信息number,提交者(user.login),body以及title.

注意,https://api.github.com/repos/rails/rails/issues返回的是按时间倒叙排列的issue,并且每页默认返回30条,我们需要在url后面接上一些参数,来爬取指定的页。我们将url接上参数构造成如下的样子:

https://api.github.com/repos/rails/rails/issues?per_page=99&page=num

其中num代表页数,我们需要从1开始自增。per_page代表每页返回的元素个数,GitHub最大只能制定到99。

所以我们的爬虫,应该是从1开始不停地自增num,直到返回的json数组元素个数不足99,就说明爬取完了。另外,由于GitHub API 爬取速率的限制,我事先准备了10个不同账号的token,对于每次请求,重新自定义请求header,带上不同的token.

下文是源代码:

# -*- coding: utf-8 -*-
__author__ = 'ayonel'
import itertools
import json
import os
import scrapy
from scrapy import Request

class IssueSpider(scrapy.spiders.Spider):

    name = "issue" #爬虫名称
    allowed_domains = ["github.com"] #制定爬取域名
    num = 1 # 页数,默认从第一页开始
    handle_httpstatus_list = [404, 403, 401] #如果返回这个列表中的状态码,爬虫也不会终止
    output_file = open('issue.txt', "a") #输出文件
    #token列表,隐去部分
    token_list = [
        '293a06ac6ed5a746f7314be5a25f3d**********',
        '66de084042a7d3311544c656ad9273**********',
        'a513f61368e16c2da229e38e139a8e**********',
        '9055150c8fd031468af71cbb4e12c5**********',
        'ba119dc83af804327fa9dad8e07718**********',
        'b93e6996a4d76057d16e5e45788fbf**********',
        'c9c13e5c14d6876c76919520c9b05d**********',
        '3e41cbfc0c8878aec935fba68a0d3c**********',
        '402ff55399ca08ca7c886a2031f49f**********',
        '7cb6e20a24000968983b79b5de705c**********',
    ]
    token_iter = itertools.cycle(token_list) #生成循环迭代器,迭代到最后一个token后,会重新开始迭代


    def __init__(self): #初始化
        scrapy.spiders.Spider.__init__(self)

    def __del__(self): #爬虫结束时,关闭文件
        self.output_file.close()

    def start_requests(self):
        start_urls = [] #初始爬取链接列表
        url = "https://api.github.com/repos/rails/rails/issues?per_page=99&page="+str(self.num) #第一条爬取url
        #添加一个爬取请求
        start_urls.append(scrapy.FormRequest(url, headers={
            'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:36.0) Gecko/20100101 Firefox/36.0',
            'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
            'Accept-Language': 'en',
            'Authorization': 'token ' + self.token_iter.next(),#这个字段为添加token字段
            }, callback=self.parse))

        return start_urls

    def yield_request(self): #定义一个生成请求函数
        url = "https://api.github.com/repos/rails/rails/issues?per_page=99&page="+str(self.num) #生成url
        #返回请求
        return Request(url,headers={
                'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:36.0) Gecko/20100101 Firefox/36.0',
                'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
                'Accept-Language': 'en',
                'Authorization': 'token ' + self.token_iter.next(),
                },callback=self.parse)

    #解析函数
    def parse(self, response):
        if response.status in self.handle_httpstatus_list:#如果遇见handle_httpstatus_list中出现的状态码
            self.num += 1 #num自增,相当于直接跳过,可以输出当前url到log文件
            yield self.yield_request() #产生新的请求
            return

        json_data = json.loads(response.body_as_unicode()) #获取json
        length = len(json_data) #获取json长度

        if length == 99:
            self.num = self.num + 1
            for issue in json_data:
                data = {}
                data['number'] = issue['number']
                data['owner'] = issue['user']['login']
                data['title'] = issue['title']
                data['body'] = issue['body']
               
                self.output_file.write(json.dumps(data)+'\n') #输出每一行,格式也为json
            self.output_file.flush()
            yield self.yield_request() #产生新的请求

        elif length < 99: #意味着爬取到最后一页
            for issue in json_data:
               data = {}
                data['number'] = issue['number']
                data['owner'] = issue['user']['login']
                data['title'] = issue['title']
                data['body'] = issue['body']
                self.output_file.write(json.dumps(data)+'\n')
            self.output_file.flush()

多元混合效应逻辑回归(Mixed Effects Logistic Regression)是什么:
混合效应逻辑回归是一种二分类模型,其输出是一组预测变量(自变量)的线性组合,但是样本不是简单地独立的,而是集群式分布,也即某个群体之间存在内部关联。
(Mixed effects logistic regression is used to model binary outcome variables, in which the log odds of the outcomes are modeled as a linear combination of the predictor variables when data are clustered or there are both fixed and random effects.)

举个例子:
A large HMO wants to know what patient and physician factors are most related to whether a patient’s lung cancer goes into remission after treatment as part of a larger study of treatment outcomes and quality of life in patients with lunger cancer.
一个医疗机构想要知道对于骨瘤是否最终治愈,医生和病人之间哪种属性是影响最大的。比如医生的属性:从业经验,学历等等。病人的属性有骨瘤大小,患病时间,性别等等。
我们现在就会有一组样本:

is_cured    doctor  experience    education    patient    tumour_size    tumour_time    patient_sex
   1         Bob      10            1          jav          3              2                 0    
   0         Bob      10            1          hex          4              1                 1
   1         Bob      10            1          lancy        6              4                 0
   1         Jack     5             3          joogh        1              2                 0    
   0         Jack     5             3          silla        4              1                 1
   0         Jack     5             3          grecy        6              4                 0
   1         Jack     5             2          lori         2              2                 0    
   0         Bob      10            2          carl         5              1                 1
   1         Bob      10            2          sharun       6              4                 0

我们现在有这组数据,我们现在要预测一个骨瘤病人能否治愈。

普通的多元线性逻辑回归会把每一组样本看成独立的,也就是样本之间是完全独立的,但是实际中我们可以看到,这些样本之间其实是聚集分布的,它们并不是完全独立。同为Bob医生的样本之间肯定存在某种关联,同为Jack医生的样本之间也肯定存在关联。多元混合效应逻辑回归就是加入一个随机效应(上面这个例子中就是doctor),弥补了普通线性回归视样本之间并不存在关联的不足。

混合效应线性回归更加贴近于真实的世界环境。UCLA有十分棒的教程,大家可以去读一读。

那么如何在R里面使用混合效应回归呢?我以混合效应逻辑为例。
读入数据:

>data <- csv.read("data.csv")

对数据中的计数类型(比如年龄啊,从业经验啊等等)进行正规化。为什么需要正规化,请自行谷歌。

>pvar <- c("experience","education","tumour_size,tumour_time")
>datsc -< data
>datasc[pvar] <- lapply(datasc[pvar], scale) //正规化

引入我们的核心计算包lme4:

>require(lme4)

进行计算:

>result <- glmer(is_cured ~ experience+education+tumour_size+tumour_time+(1|doctor),data=datsc,family=binomial,control=glmerControl(optimizer="bobyqa")) //随机变量doctor放在(1|doctor),其余变量为固定变量,optimizer需要加上,防止不收敛

输出结果:

>summary(result)

有时候我们进行贝叶斯分类时,由于数据量太大导致内存溢出或者对模型的训练有着特殊要求(比如用第一个月的数据预测第二个月,再将第二个月的数据加入已经训练好的模型,去预测第三个月…),这时普通的贝叶斯分类不行了。我们需要使用贝叶斯来进行增量学习(incremental learning), weka的贝叶斯分类器即存在增量式的贝叶斯分类器。下文以多项式朴素贝叶斯增量学习为例,weka中的包为:

weka.classifiers.bayes.NaiveBayesMultinomialUpdateable

下文是核心代码:

        //获取第一个月的数据用于建立模型
        ArffLoader loader = new ArffLoader();
        loader.setFile(new File("month1.arff"));
        loader.getStructure();//这句话必须有,不然会报错

        //定义分类器 并用第一个月的数据做训练
        Instances originDataSet = loader.getDataSet();//获取第一个月的实例
        originDataSet.setClassIndex(originDataSet.numAttributes()-1);//定义最后一个属性作为分类目标
        NaiveBayesMultinomialUpdateable nb = new NaiveBayesMultinomialUpdateable();//定义模型
        nb.buildClassifier(originDataSet);//建立模型

        //对从第二个月起的每个月进行预测和训练,一直到第100个月,(2,100只是用于示例)
        for (int month = 2; month <= 100; month++) {
            //读取测试集
            ArffLoader testLoader = new ArffLoader();
            testLoader.setFile(new File("month"+i+".arff"));
            testLoader.getStructure();

            Instances testDataSet = testLoader.getDataSet();//获取测试集实例
            //进行分类,并且输出
            for (int i = 0; i< testDataSet.numInstances(); i++) {
                FileWriter fileWritter = new FileWriter("output.txt", true);
                BufferedWriter bufferWritter = new BufferedWriter(fileWritter);
                bufferWritter.write(nb.distributionForInstance(testDataSet.instance(i))[1]+'\n');//这一行输出的是分类中分到第二类(下标为1)的概率
                bufferWritter.flush();
            }

            // 再把这次的测试集当做训练集进行训练
            for (int i = 0; i< testDataSet.size(); i++) {
                testDataSet.setClassIndex(testDataSet.numAttributes()-1);
                nb.updateClassifier(testDataSet.get(i));
            }