前言:
上一篇比较详细的介绍了卡方检验和卡方分布。这篇我们就实际操刀,找到一些训练集,正所谓纸上得来终觉浅,绝知此事要躬行。然而我在躬行的时候,发现了卡方检验对于文本分类来说应该把公式再变形一般,那样就完美了。
目录:
文本分类学习(一)开篇
文本分类学习(二)文本表示
文本分类学习(三)特征权重(TF/IDF)和特征提取
文本分类学习(四)特征选择之卡方检验
文本分类学习(五)机器学习SVM的前奏-特征提取(卡方检验续集)
一,回顾卡方检验
1.公式一:
先回顾一下卡方检验:
卡方检验:事先做一个假设,计算由有假设得来的理论值于实际观察值之间的偏差来推断这个假设是否成立,公式:
2.四表格的卡方检验公式:
卡方检验对于文本分类:每个词对于每个类别,使用四表格的形式,计算该词对于该类是否有较大的影响,公式:
二,训练集的准备
我选择了复旦语料库中的历史篇:469篇
每篇的格式大多如下:
历史类文档
自己爬了博客园的博客:420篇
选择一篇贴出来:
计算机类博客
这里就要抛出一个问题来:机器学习:你到底需要多少训练数据,尤其是SVM?
我这里训练集加起来才889篇,可以明确的是这些训练集是肯定不够的,理论上来说训练集应该越多越好,但是其分类想过应该是一个越来越平缓的曲线,这个貌似应该研究起来也是一个不少篇幅的内容。
三,开始特征提取吧!
接下来就开始机器学习第一步也是最重要的一步,也是最麻烦的一步吧,事实上自己要做的工作就是这一步。毕竟后面的训练只要用前辈们已经不断完善的分类算法和工具了,我选择的是SVM算法和libsvm工具包。
再声明一下,我是利用卡方检验对需要进行二分类的文本进行特征选择,已达到降维的目的,最终要得到的是能够代表每个类别的特征集合,和一个总的特征词典。当然在这个工程中,我们也会看到每个词对于一个文本重要性的规律。
1.分词工具
第一步:选择分词工具对训练集进行分词
我选择的分词工具是JIEba分词,而我使用的语言是C# ,关于.net core版本的JIEba分词可以在这篇博文里面找到:
http://www.cnblogs.com/dacc123/p/8431369.html
利用JIEba分词工具,我们才能进行后面的计算词频,词的文档频率,词的四表格值,词的卡方值χ2 。这里还是把自己的代码贴出来吧,如果有需要的话我会整理在GitHub上。
2.计算词频
第二步:计算词频
相信大家都会写,我把自己代码贴出来以供参考,代码中多用了Dictionary 数据结构,对了分词之前,咱们应该有一份比较全的停用词表。插一句:对于文本分类来说停用词越多越好,对于搜索引擎来说就不是这样了。
1800多个停用词
停用词表
计算词频代码:
public void ReadText()
{
rd = File.OpenText("./stopwords.txt");
string s = "";
while ((s = rd.ReadLine()) != null)
{
if (s == null) continue;
if (!stopwords.ContainsKey(s))
stopwords.Add(s, 1);
}
Console.WriteLine("*******读取停用词完毕");
rd.Close();
}
//计算词频,url1地址是放入训练集的文件夹,url2地址是存放计算词频结果的文件
public void WriteText(string url1, string url2)
{
DirectoryInfo folder = new DirectoryInfo(url1);
foreach (FileInfo file in folder.GetFiles("*.txt"))
{
rd = File.OpenText(file.FullName);
string s = "";
System.Console.WriteLine("**************开始读取数据...");
while ((s = rd.ReadLine()) != null)
{
var segment = segmenter.Cut(s, false, false);
foreach (var x in segment)
{
if (stopwords.ContainsKey(x)) continue;
if (!keys.ContainsKey(x))
keys.Add(x, 1);
else
keys[x]++;
}
}
}
System.Console.WriteLine("**************读取完毕,计算词频并插入...");
wt = new StreamWriter(url2, true);
//wt = File.AppendText(url2);
var dicSort = from objDic in keys orderby objDic.Value descending select objDic;
foreach (KeyValuePair kvp in dicSort)
{
wt.WriteLine(kvp.Key + " " + kvp.Value.ToString());
}
System.Console.WriteLine("**************插入完毕...");
wt.Flush();
rd.Close();
wt.Close();
}
经过一顿操作:
历史类: 计算机博客类:
选择词频排名前30的词,来看看,排名靠前的词乍一看好像貌似是那么回事,这也是有时候你只用词频这一个属性来分类文本,发现效果也不是那么差。仔细看一下:
历史类:“标”,“题”,“年”…等等,计算机博客类:“中”,“时”,等等这些词总是那么的刺眼,我们需要把这些冒充上来的词给去掉。
忘了说下,计算机博客类的词的个数是:21503个;历史类的词的个数是:68912个,由于自己找的训练集不是那么好所以两种类的词差别有点大。。
词的个数这么多,如果用词频排序的词表来当做特征集,是不是效果不能到达最好,而且维度太大了。
3.文档频率DF
前面提到过一个名词:文档频率DF ,也就是一个词在多少个文档中出现过,对于那些文档频率十分低的词,我们叫做生僻词,这些词有可能词频很高,比如一个人写博客:“我是大牛,我是大牛,我是大牛…”循环了几千次,那么“大牛”这个词就很靠前了,然而他只出现过在一篇博客里,所以我们可以把这些生僻词去掉。我也统计了两个类别中的生僻词,发现一大半都是DF为1,2的词。这里也就不贴代码和统计结果了,因为我们不需要取出文档频率低的词,为什么呢?因为有卡方检验啊,这个十分强大的机器,是肯定会帮我们过滤掉DF极低的词,所以咱们直奔卡方检验,看看是不是可以验证自己的猜想。(而统计DF的值恰恰帮助了我们计算卡方检验)
4.卡方检验 一
根据上一篇博客中的公式,对于每个词,我们需要计算四个值,A,B,C,D。
再解释一下,以“大牛”和计算机博客类为例子:A 包含“大牛”属于计算机博客的文档个数,B 包含“大牛”不属于计算机博客的文档个数,C 不包含“大牛”属于计算机博客类的文档个数,D 不包含“大牛”不属于计算机的文档个数。
看起来很繁琐,其实只要有了上一步统计的DF表,那就很容易了。以计算机博客类为例子:
BlogDF 表示计算机博客类的词的文档频率表,HistoryDF表示历史类的词的文档频率表
那么A的值自然就是BlogDF的值
B的值:
forearch BlogDF
if(HistoryDF[x]!=0)
B[x] = HistoryDF[x];
else
B[x] = 0;
C 和D 的值自然就是:
foreach C,D
C[x] = 计算机总文档数-A[x]
D[x] = 计算机总文档数-B[x]
代码如下:比较简陋没有收拾
计算卡方检验
于是兴高采烈的看看我们的强大的卡方检验得到的值,以计算机类:为了做对比,左边是词频排名,右边的是卡方检验排名
顿时傻眼了,为什么卡方检验 之后,词的排名变成这样了?“历史”,“中国”,“发展”,貌似是代表的历史了,难道自己代码写错了?仔细排查发现代码并没有写错,这些词也确实在计算机博客类别的文档里出现过。可是为什么这些词的排名如此之高?
查找这些的A,B,C,D值,以“标”为例
属于计算机类博客 不属于计算机类博客(属于历史类)
包含“标” A:3 B:443
不包含“标” C:417 D:26
根据公式,计算出来的值确实是780多,而标的词频只有:3!!。翻看历史类别的卡方值也是780多,这个“标”这么全能吗?(实际上两个类别的公共词的卡方值都是一样的,观察公式和ABCD的值就可以发现了)
我们再回顾一开始的卡方检验:我们假设某个词对于文档是不是某个类别是没有影响的,而不是某个词是不是能代表某个类别,那么“标”这个词虽然对计算机博客类几乎没有一点代表性,但是你看看之前的词频表,“标”在历史类中的词频排名非常靠前。到这里就应该清楚了“标”这个词,卡方检验认为“标”这个词对历史类别的影响很大,当一个文档出现“标”那么可以很大一部分确定他是历史类别,不是计算机博客类别,所以“标”对于文档不是计算机类别还是有很大影响力的,自然排名靠前。这里就有一个疑问了,为什么“标”这种词可以很好的代表历史?这个后面再提,这也是前面说过的卡方检验的低词频性缺陷。
5.卡方检验二
所以眼前这个酷似历史的卡方检验排名表,是否可以作为计算机博客类的特征集合呢?答案是肯定的,这些排名靠前的词对于判断一个文档是否属于计算机博客类别相当有说服力。 但是这样的排名表,我看着真的不是很喜欢。
于是我就做了点小动作。我们回顾一下卡方检验公式推导过程,
为了防止正负相互抵消,所以我们采用了平方和。然后在二分类问题中,这个正负其实是很有意义的,不应该就这么被和谐掉。我们看看“标”的四格表
属于计算机类博客 不属于计算机类博客(属于历史类)
包含“标” A:3 B:443
不包含“标” C:417 D:26
A和D的值很小,B和C的值很大,这就告诉了我们一个信息含有“标”很大可能是历史类,很小可能是计算机类,在计算过程中:
以计算A的观察值和理论值的偏差为例(约等于):
实际上这个偏差应该是负的,3-210应该是负数,我们使用平方和才变成为正的,所以我们不使用平方和而是使用(E-A)*|(E-A)|
偏差为负表示啥呢,表示这个词能够否定文本属于该类文档(语气重了一点),为正表示这个词能够肯定文本属于该类文档。
属于计算机类博客 不属于计算机类博客(属于历史类)
包含“标” A:3 EA:210 B:443 EB:235
不包含“标” C:417 EC : 209 D:26 ED:233
推广到四个值,发现实际上B的偏差值是正的 443-235是正数嘛,实际上对于B我们应该取负数,同理C应该去负数。对于B,C在计算结果之后加上一个负数
这样算出来的标就是-780多。为什么呢?因为B是包含“标”属于历史类,对于计算机类说是反例,同理C也是,所以要取反,正变负,负变正。
这样我们可以想象,如果用符号表示卡方值的大小,那么卡方检验得到的值应该是类似于正态分布:
越靠近0的词越没有用,离0越远的词我们就越关注。那么这个具体的阀值是什么?还记得在卡方分布中说过的那个拒绝域吗?
3.84,对就是他了。你别看上面的图+3.84 和-3.84之间距离很短,但是这么短的距离中包含的词可多着呢,我的数据集中,大概三分之二的词都集中在-3.84到+3.84之间
于是在这个有符号的卡方检验指导下,我们变更公式!(对于四格表而言哦,也就是对于二分类而言哦)
根据此公式,我们修改代码
//计算观察值A的偏差 符号为+
double EA = (double)(keysA[x.Key]+keysB[x.Key])*(double)(keysA[x.Key]+keysC[x.Key])/(double)(category1+category2);
double a = (double)(keysA[x.Key]-EA)*System.Math.Abs((double)(keysA[x.Key]-EA))/EA;
//计算观察值B的偏差 符号为-
double EB = (double)(keysA[x.Key]+keysB[x.Key])*(double)(keysB[x.Key]+keysD[x.Key])/(double)(category1+category2);
double b = -1*(double)(keysB[x.Key]-EB)*System.Math.Abs((double)(keysB[x.Key]-EB))/EB;
//计算观察值C的偏差 符号为-
double EC = (double)(keysC[x.Key]+keysD[x.Key])*(double)(keysA[x.Key]+keysC[x.Key])/(double)(category1+category2);
double c = -1*(double)(keysC[x.Key]-EC)*System.Math.Abs((double)(keysC[x.Key]-EC))/EC;
//计算观察值D的偏差 符号为+
double ED = (double)(keysC[x.Key]+keysD[x.Key])*(double)(keysB[x.Key]+keysD[x.Key])/(double)(category1+category2);
double d = (double)(keysD[x.Key]-ED)*System.Math.Abs((double)(keysD[x.Key]-ED))/ED;
result.Add(x.Key,a+b+c+d);
得到一个新的卡方检验表,以计算机类别,同样和词频作对比
乍一看,卡方检验的效果确实不错,仔细一看,嗯还是效果很好。哈哈哈。“中”这个词终于消失了。果然名不虚传 ,卡方检验确实是一个好东西
6.卡方检验的低词频性
再看看历史类的:
哎呀,这个“标”,“期” …等等,真是差强人意。看看之前的文本范例,我们就明白了
【 文献号 】1-1
【原文出处】历史研究
【原刊地名】京
【原刊期号】199602
【原刊页号】5-25
【分 类 号】K1
【分 类 名】历史学
【 作 者 】林甘泉
【复印期号】199607
【 标 题 】二十世纪中国历史学回顾 二十世纪的中国历史学
【 正 文 】
每一篇都有一个“标题”,“文献号”等等,因为卡方检验本来就是忽视了词频的,这次个每篇文章只出现一次的词,反而重要性排第一去了。所以我们就需要结合词频信息,对卡方检验再次来改造。具体应该怎么权衡卡方检验和词频的值呢?一时间我也没有想到好的方法。可以将卡方检验排名靠前的词,词频小于等于文档数,或者小于等于文档数2倍的词都去掉。
7.卡方检验的神奇
再来看看卡方检验排名表的后半部分,左边计算机博客类,右边历史类!
可以看到,这些历史类排名最后的是不是很像是计算机博客类的词语?这些距离0很远的词,是论证文章不属于历史类的词语,也验证了上面的正态分布的猜想。两个类别正好倒过来了,十分对称,十分完美。绝知此事要躬行,躬行之后的感觉果然不同呀。
其实具体实验的时候才发现,词汇这个组成文章的基本成分,在众多文本之间有太多规律,太多巧妙的地方值得去挖掘了,这也是自然语言处理的魅力了吧。
再次回到之前的文档频率DF,我们说文档频率DF低的不用处理,卡方检验会帮我处理,看看结果,这里截两张图:
第一个参数是卡方检验的值,第二个值是文档频率DF
果然这些DF十分低的词都被分配到了0周围,坚决的和0站在一起,坚决的要被淘汰掉。
四,结语
那么经过前面的步骤,的确得到了可以代表两个类的特征集合,将两个特征集合距离0的距离大于3.84的特征(就是词啦)取一个并集,那么就是一个特征词典了。我们可以想象,历史类和计算机博客类的文本向量如果映射在这个词典上,他们分布是不同的,而SVM正是解决中在高维空间(也就是向量维度很高),把两类向量进行分类,如果线性不可分,SVM会使用核函数,映射到更高的维度使其变成线性可分。具体的原理这里也不细究。可见在SVM之前,将文本变成向量的过程是一个非常重要的步骤。