大二文本分词过滤分类实验总结
2016-08-01 15:29
302 查看
这次作业的内容是给定一个体育分类测试文档和体育分类训练文档,以体育分类训练文档为训练集,体育分类测试文档为测试集,选择一种特征选择算法对训练集样本进行特征选择;选择一种文本分类算法对训练集样本进行文本分类;针对测试集进行分类,并对分类结果进行评价。先解释一下里面出现的几个名词。在机器学习和模式识别等领域中,一般需要将样本分成独立的三部分训练集(train set),验证集(validation set)和测试集(test set)。其中训练集用来估计模型,验证集用来确定网络结构或者控制模型复杂程度的参数,而测试集则检验最终选择最优的模型的性能如何。这里为了方便起见只考虑训练集和测试集。完成这个任务我们主要需要解决以下几个问题。
1.分词的算法;
2.特征选择的算法;
3.文本分类的算法。
1.分词算法
现有的分词算法可分为三大类:基于字符串匹配的分词方法、基于理解的分词方法和基于统计的分词方法。
基于字符串匹配的分词方法
这种方法又叫做机械分词方法,它是按照一定的策略将待分析的汉字串与一个充分大的机器词典中的词条进行匹配,若在词典中找到某个字符串,则匹配成功。按照扫描方向的不同,串匹配分词方法可以分为正向匹配和逆向匹配;按照不同长度优先匹配的情况,可以分为最大匹配和最小匹配。常用的几种机械分词方法有:正向最大匹配法(由左到右的方向);逆向最大匹配法(由右到左的方向);最少切分(使每一句中切出的词数最小);双向最大匹配法(进行由左到右、由右到左两次扫描)。可以将上述各种方法相互组合,例如,可以将正向最大匹配方法和逆向最大匹配方法结合起来构成双向匹配法。由于汉语单字成词的特点,正向最小匹配和逆向最小匹配一般很少使用。一般说来,逆向匹配的切分精度略高于正向匹配,遇到的歧义现象也较少。统计结果表明,单纯使用正向最大匹配的错误率为1/169,单纯使用逆向最大匹配的错误率为1/245。但这种精度还远远不能满足实际的需要。实际使用的分词系统,都是把机械分词作为一种初分手段,还需通过利用各种其它的语言信息来进一步提高切分的准确率。
基于理解的分词方法
这种分词方法是通过让计算机模拟人对句子的理解,达到识别词的效果。其基本思想就是在分词的同时进行句法、语义分析,利用句法信息和语义信息来处理歧义现象。它通常包括三个部分:分词子系统、句法语义子系统、总控部分。在总控部分的协调下,分词子系统可以获得有关词、句子等的句法和语义信息来对分词歧义进行判断,即它模拟了人对句子的理解过程。这种分词方法需要使用大量的语言知识和信息。由于汉语语言知识的笼统、复杂性,难以将各种语言信息组织成机器可直接读取的形式,因此目前基于理解的分词系统还处在试验阶段。
基于统计的分词方法
从形式上看,词是稳定的字的组合,因此在上下文中,相邻的字同时出现的次数越多,就越有可能构成一个词。因此字与字相邻共现的频率或概率能够较好的反映成词的可信度。可以对语料中相邻共现的各个字的组合的频度进行统计,计算它们的互现信息。互现信息体现了汉字之间结合关系的紧密程度。当紧密程度高于某一个阈值时,便可认为此字组可能构成了一个词。这种方法只需对语料中的字组频度进行统计,不需要切分词典,因而又叫做无词典分词法或统计取词方法。但这种方法也有一定的局限性,会经常抽出一些共现频度高、但并不是词的常用字组,例如“这一”、“之一”、“有的”、“我的”、“许多的”等,并且对常用词的识别精度差,时空开销大。实际应用的统计分词系统都要使用一部基本的分词词典进行串匹配分词,同时使用统计方法识别一些新的词,即将串频统计和串匹配结合起来,既发挥匹配分词切分速度快、效率高的特点,又利用了无词典分词结合上下文识别生词、自动消除歧义的优点。
到底哪种分词算法的准确度更高,目前并无定论。对于任何一个成熟的分词系统来说,不可能单独依靠某一种算法来实现,都需要综合不同的算法。在这次作业中使用的是逆向最大匹配法。先举个例子来说明逆向最大匹配法和正向最大匹配法。
以“我是一个坏人”为例:
正向最大匹配法:
我是一
我是
我==>得到一个词
是一个
是一
是==>得到一个词
一个坏
一个==>得到一个词
坏人==>得到一个词
结果:我、是、一个、坏人
逆向最大匹配法:
个坏人
坏人==>坏人
是一个
一个==> 一个
我是
是==>是
我==>我
结果:我、是、一个、坏人
在这个例子中正向最大匹配法和逆向最大匹配法的结果是一样的,但很多时候分词的结果往往不同。我们先了解下汉字编码的知识。
看完编码表就会发现,每个汉字用两个字节编码,第一个字节的范围是0xb0到0xf7,第二个字节的范围是0xa1到0xfe。回到逆向最大匹配法,可以借鉴Robin-Karp算法hash处理的思想和STL中的map实现。对于词典中的每个词我们都生成一个hash值并用map来存储它们,匹配时边生成hash值边比较。下面是一个简单的示例程序,先输入词典的大小和词典中的词,再输入待分词的文本进行匹配。
Document Frequency,缩写为IDF),它的大小与一个词的常见程度成反比。将TF和IDF这两个值相乘,就得到了一个词的TF-IDF值。某个词对文章的重要性越高,它的TF-IDF值就越大。
还是以《中国的蜜蜂养殖》为例,假定该文长度为1000个词,"中国"、"蜜蜂"、"养殖"各出现20次,则这三个词的TF都为0.02。然后,搜索Google发现,包含"的"字的网页共有250亿张,假定这就是中文网页总数。包含"中国"的网页共有62.3亿张,包含"蜜蜂"的网页为0.484亿张,包含"养殖"的网页为0.973亿张。则它们的IDF和TF-IDF如下表。
上表可见,"蜜蜂"的TF-IDF值最高,"养殖"其次,"中国"最低。所以,如果只选择一个词,"蜜蜂"就是这篇文章的关键词。
2.特征选择的算法
机器学习算法的空间、时间复杂度依赖于输入数据的规模,维度规约(Dimensionality reduction)则是一种被用于降低输入数据维数的方法。维度规约可以分为两类:
特征提取(feature extraction),将原始的d维空间映射到k维空间中(新的k维空间不输入原始空间的子集)。
特征选择(feature selection),从原始的d维空间中,选择为我们提供信息最多的k个维(这k个维属于原始空间的子集)。
在文本挖掘与文本分类的有关问题中,常采用特征选择方法。原因是文本的特征一般都是单词(term),具有语义信息,使用特征选择找出的k维子集,仍然是单词作为特征,保留了语义信息,而特征提取则找k维新空间,将会丧失了语义信息。目前,文本的特征选择方法主要有DF,MI,IG,CHI这几种。为了方便描述,我们首先明确一些定义。
p(t):文档包含特征词t的概率。
:文档不属于Ci的概率。
p(Ci|t):已知文档包括特征词t的条件下,该文档属于Ci的概率。
:已知文档属于Ci的条件下,该文档不包括特征词t的概率。
类似的其他的一些概率如p(C i ),
,
等,有着类似的定义。
为了估计这些概率需要通过统计训练样本的相关频率信息,如下表。
Aij:包含特征词ti,并且类别属于Cj的文档数量
Bij:包含特征词ti,并且类别不属于Cj的文档数量
Cij:不包含特征词ti,并且类别属于Cj的文档数量
Dij:不包含特征词ti,并且类别不属于Cj的文档数量
Aij+Bij:包含特征词ti的文档数量
Cij+Dij:不包含特征词ti的文档数量
Aij+Cij:Cj类的文档数量
Bij+Dij:非Cj类的文档数量
Aij+Bij+Cij+Dij=N:语料中所有文档数量
有了这些统计量,有关概率的估算就变得容易,如:
p(ti) =(Aij+Bij)/N
p(Cj)=(Aij+Cij)/N
p(Cj|tj)=Aij/(Aij+Bij)
下面讲解几种常用的文本特征选择算法。
DF(Document Frequency)
DF是统计特征词出现的文档数量,用来衡量某个特征词的重要性。
如果某些特征词在文档中经常出现,那么这个词就可能很重要。而对于在文档中出现很少(如仅在语料中出现1次)特征词,携带了很少的信息量,甚至是噪声,这些特征词,对分类器学习影响也是很小。DF特征选择方法属于无监督的学习算法,仅考虑了频率因素而没有考虑类别因素,因此,DF算法的将会引入一些没有意义的词。
MI(Mutual Information)
互信息法用于衡量特征词与文档类别直接的信息量。
从上面的公式上看出,如果某个特征词的频率很低,那么互信息得分就会很大,因此互信息法倾向"低频"的特征词。相对的词频很高的词,得分就会变低,如果这词携带了很高的信息量,互信息法就会变得低效。
IG(Information Gain)
信息增益法,通过某个特征词的缺失与存在的两种情况下,通过语料中前后信息的增加,衡量某个特征词的重要性。
依据IG的定义,每个特征词ti的IG得分前面一部分
计算值是一样,可以省略。因此,IG的计算公式如下。
IG与MI存在关系:
,因此,IG方式实际上就是互信息
与互信息
加权。
CHI(Chi-square)
CHI特征选择算法利用了统计学中的假设检验的基本思想:首先假设特征词与类别直接是不相关的,如果利用CHI分布计算出的检验值偏离阈值越大,那么更有信心否定原假设,接受原假设的备则假设:特征词与类别有着很高的关联度。
对于一个给定的语料而言,文档的总数N、Cj类文档的数量和非Cj类文档的数量都是一个定值,因此CHI的计算公式可以简化为
。
3.文本分类的算法
常用的文本分类算法有朴素贝叶斯算法、向量空间距离测度分类算法、K最邻近分类算法(KNN)、支持向量机(SVM)、神经网络算法、决策树分类算法等。我们主要讲解朴素贝叶斯算法和K最邻近分类算法(KNN)算法。
朴素贝叶斯算法
要理解贝叶斯算法,必须先理解贝叶斯定理。我们先讲解条件概率公式和全概率公式。
假定样本空间S,是两个事件A与A'的和。
在这种情况下,事件B可以划分成两个部分。
这就是全概率公式。这就是全概率公式。它的含义是,如果A和A'构成样本空间的一个划分,那么事件B的概率,就等于A和A'的概率分别乘以B对这两个事件的条件概率之和。
将这个公式代入上一节的条件概率公式,就得到了条件概率的另一种写法。
接下来进入正题。对条件概率公式进行变形,可以得到如下形式。
我们把P(A)称为先验概率(Prior probability),即在B事件发生之前,我们对A事件概率的一个判断。P(A|B)称为后验概率(Posterior probability),即在B事件发生之后,我们对A事件概率的重新评估。P(B|A)/P(B)称为可能性函数(Likelyhood),这是一个调整因子,使得预估概率更接近真实概率。所以,条件概率可以理解成:后验概率=先验概率X调整因子。我们先预估一个先验概率,然后加入实验结果,看这个实验到底是增强还是削弱了先验概率,由此得到更接近事实的后验概率。在这里,如果可能性函数P(B|A)/P(B)>1,意味着先验概率被增强,事件A的发生的可能性变大;如果可能性函数=1,意味着B事件无助于判断事件A的可能性;如果可能性函数<1,意味着先验概率被削弱,事件A的可能性变小。为了加深对贝叶斯推断的理解,我们看个例子。
第一个例子。两个一模一样的碗,一号碗有30颗水果糖和10颗巧克力糖,二号碗有水果糖和巧克力糖各20颗。现在随机选择一个碗,从中摸出一颗糖,发现是水果糖。请问这颗水果糖来自一号碗的概率有多大?我们假定,H1表示一号碗,H2表示二号碗。由于这两个碗是一样的,所以P(H1)=P(H2),也就是说,在取出水果糖之前,这两个碗被选中的概率相同。因此,P(H1)=0.5,我们把这个概率就叫做"先验概率",即没有做实验之前,来自一号碗的概率是0.5。再假定,E表示水果糖,所以问题就变成了在已知E的情况下,来自一号碗的概率有多大,即求P(H1|E)。我们把这个概率叫做后验概率,即在E事件发生之后,对P(H1)的修正。
这表明,来自一号碗的概率是0.6。也就是说,取出水果糖之后,H1事件的可能性得到了增强。贝叶斯算法在计算机领域有着许多的应用,举一个垃圾邮件过滤的例子。现在,我们收到了一封新邮件。在未经统计分析之前,我们假定它是垃圾邮件的概率为50%。我们用S表示垃圾邮件(spam),H表示正常邮件(healthy)。因此,P(S)和P(H)的先验概率都是50%。然后,对这封邮件进行解析,发现其中包含了sex这个词,请问这封邮件属于垃圾邮件的概率有多高?我们用W表示"sex"这个词,那么问题就变成了如何计算P(S|W)的值,即在某个词语(W)已经存在的条件下,垃圾邮件(S)的概率有多大。
公式中,P(W|S)和P(W|H)的含义是,这个词语在垃圾邮件和正常邮件中,分别出现的概率。这两个值可以从历史资料库中得到,对sex这个词来说,上文假定它们分别等于5%和0.05%。另外,P(S)和P(H)的值,前面说过都等于50%。
因此,这封新邮件是垃圾邮件的概率等于99%。这说明,sex这个词的推断能力很强,将50%的先验概率一下子提高到了99%的后验概率。做完上面一步,请问我们能否得出结论,这封新邮件就是垃圾邮件?回答是不能。因为一封邮件包含很多词语,一些词语说这是垃圾邮件,另一些说这不是。你怎么知道以哪个词为准?Paul Graham的做法是,选出这封信中P(S|W)最高的15个词,计算它们的联合概率。如果有的词是第一次出现,无法计算P(S|W),就假定这个值等于0.4。因为垃圾邮件用的往往都是某些固定的词语,所以如果你从来没见过某个词,它多半是一个正常的词。所谓联合概率,就是指在多个事件发生的情况下,另一个事件发生概率有多大。比如,已知W1和W2是两个不同的词语,它们都出现在某封电子邮件之中,那么这封邮件是垃圾邮件的概率,就是联合概率。在已知W1和W2的情况下,无非就是两种结果:垃圾邮件(事件E1)或正常邮件(事件E2)。
其中,W1、W2和垃圾邮件的概率分别如下。
如果假定所有事件都是独立事件,那么就可以计算P(E1)和P(E2)。
将P(S)等于0.5代入,P(S|W1)记为P1,P(S|W2)记为P2。
将上面的公式扩展到15个词的情况,就得到了最终的概率计算公式。
一封邮件是不是垃圾邮件,就用这个式子进行计算。这时我们还需要一个用于比较的门槛值。Paul Graham的门槛值是0.9,概率大于0.9,表示15个词联合认定,这封邮件有90%以上的可能属于垃圾邮件;概率小于0.9,就表示是正常邮件。有了这个公式以后,一封正常的信件即使出现sex这个词,也不会被认定为垃圾邮件了。
K最邻近分类算法(KNN)
K最近邻(kNN,k-NearestNeighbor)分类算法是数据挖掘分类技术中最简单的方法之一。所谓K最近邻,就是k个最近的邻居的意思,说的是每个样本都可以用它最接近的k个邻居来代表。kNN算法的核心思想是如果一个样本在特征空间中的k个最相邻的样本中的大多数属于某一个类别,则该样本也属于这个类别,并具有这个类别上样本的特性。下图中,绿色圆要被决定赋予哪个类,是红色三角形还是蓝色四方形?如果K=3,由于红色三角形比例为2/3,绿色圆被赋予红色三角形类,如果K=5,由于蓝色四方形比例为3/5,绿色圆被赋予蓝色四方形类。
对于我们文本分类的例子,可以用余弦相似度的办法计算文本之间的距离。余弦相似度用向量空间中两个向量夹角的余弦值作为衡量两个个体间差异的大小。相比距离度量,余弦相似度更加注重两个向量在方向上的差异,而非距离或长度上。为了简单起见,我们先从句子着手。
句子A:我喜欢看电视,不喜欢看电影。
句子B:我不喜欢看电视,也不喜欢看电影。
请问怎样才能计算上面两句话的相似程度?基本思路是:如果这两句话的用词越相似,它们的内容就应该越相似。因此,可以从词频入手,计算它们的相似程度。
第一步,分词。
句子A:我/喜欢/看/电视,不/喜欢/看/电影。
句子B:我/不/喜欢/看/电视,也/不/喜欢/看/电影。
第二步,列出所有的词。
我,喜欢,看,电视,电影,不,也。
第三步,计算词频。
句子A:我1,喜欢2,看2,电视1,电影1,不1,也0。
句子B:我1,喜欢2,看2,电视1,电影1,不2,也1。
第四步,写出词频向量。
句子A:[1, 2, 2, 1, 1, 1, 0]
句子B:[1, 2, 2, 1, 1, 2, 1]
到这里,问题就变成了如何计算这两个向量的相似程度。我们可以把它们想象成空间中的两条线段,都是从原点出发,指向不同的方向。两条线段之间形成一个夹角,如果夹角为0度,意味着方向相同、线段重合;如果夹角为90度,意味着形成直角,方向完全不相似;如果夹角为180度,意味着方向正好相反。因此,我们可以通过夹角的大小,来判断向量的相似程度。夹角越小,就代表越相似。
总结一下KNN算法的流程:
step.1---初始化距离为最大值;
step.2---利用余弦相似度计算未知样本和每个训练样本的距离dist;
step.3---得到目前K个最临近样本中的最大距离maxdist;
step.4---如果dist小于maxdist,则将该训练样本作为K-最近邻样本;
step.5---重复步骤2、3、4,直到未知样本和所有训练样本的距离都算完;
step.6---统计K最近邻样本中每个类标号出现的次数;
step.7---选择出现频率最大的类标号作为未知样本的类标号。
4.总结
基本的原理到此为止就差不多讲清楚了,下面是代码,是一个很粗糙的实现。完整的代码和文档在https://github.com/houjingyi233/text-categorization-experiment/
1.分词的算法;
2.特征选择的算法;
3.文本分类的算法。
1.分词算法
现有的分词算法可分为三大类:基于字符串匹配的分词方法、基于理解的分词方法和基于统计的分词方法。
基于字符串匹配的分词方法
这种方法又叫做机械分词方法,它是按照一定的策略将待分析的汉字串与一个充分大的机器词典中的词条进行匹配,若在词典中找到某个字符串,则匹配成功。按照扫描方向的不同,串匹配分词方法可以分为正向匹配和逆向匹配;按照不同长度优先匹配的情况,可以分为最大匹配和最小匹配。常用的几种机械分词方法有:正向最大匹配法(由左到右的方向);逆向最大匹配法(由右到左的方向);最少切分(使每一句中切出的词数最小);双向最大匹配法(进行由左到右、由右到左两次扫描)。可以将上述各种方法相互组合,例如,可以将正向最大匹配方法和逆向最大匹配方法结合起来构成双向匹配法。由于汉语单字成词的特点,正向最小匹配和逆向最小匹配一般很少使用。一般说来,逆向匹配的切分精度略高于正向匹配,遇到的歧义现象也较少。统计结果表明,单纯使用正向最大匹配的错误率为1/169,单纯使用逆向最大匹配的错误率为1/245。但这种精度还远远不能满足实际的需要。实际使用的分词系统,都是把机械分词作为一种初分手段,还需通过利用各种其它的语言信息来进一步提高切分的准确率。
基于理解的分词方法
这种分词方法是通过让计算机模拟人对句子的理解,达到识别词的效果。其基本思想就是在分词的同时进行句法、语义分析,利用句法信息和语义信息来处理歧义现象。它通常包括三个部分:分词子系统、句法语义子系统、总控部分。在总控部分的协调下,分词子系统可以获得有关词、句子等的句法和语义信息来对分词歧义进行判断,即它模拟了人对句子的理解过程。这种分词方法需要使用大量的语言知识和信息。由于汉语语言知识的笼统、复杂性,难以将各种语言信息组织成机器可直接读取的形式,因此目前基于理解的分词系统还处在试验阶段。
基于统计的分词方法
从形式上看,词是稳定的字的组合,因此在上下文中,相邻的字同时出现的次数越多,就越有可能构成一个词。因此字与字相邻共现的频率或概率能够较好的反映成词的可信度。可以对语料中相邻共现的各个字的组合的频度进行统计,计算它们的互现信息。互现信息体现了汉字之间结合关系的紧密程度。当紧密程度高于某一个阈值时,便可认为此字组可能构成了一个词。这种方法只需对语料中的字组频度进行统计,不需要切分词典,因而又叫做无词典分词法或统计取词方法。但这种方法也有一定的局限性,会经常抽出一些共现频度高、但并不是词的常用字组,例如“这一”、“之一”、“有的”、“我的”、“许多的”等,并且对常用词的识别精度差,时空开销大。实际应用的统计分词系统都要使用一部基本的分词词典进行串匹配分词,同时使用统计方法识别一些新的词,即将串频统计和串匹配结合起来,既发挥匹配分词切分速度快、效率高的特点,又利用了无词典分词结合上下文识别生词、自动消除歧义的优点。
到底哪种分词算法的准确度更高,目前并无定论。对于任何一个成熟的分词系统来说,不可能单独依靠某一种算法来实现,都需要综合不同的算法。在这次作业中使用的是逆向最大匹配法。先举个例子来说明逆向最大匹配法和正向最大匹配法。
以“我是一个坏人”为例:
正向最大匹配法:
我是一
我是
我==>得到一个词
是一个
是一
是==>得到一个词
一个坏
一个==>得到一个词
坏人==>得到一个词
结果:我、是、一个、坏人
逆向最大匹配法:
个坏人
坏人==>坏人
是一个
一个==> 一个
我是
是==>是
我==>我
结果:我、是、一个、坏人
在这个例子中正向最大匹配法和逆向最大匹配法的结果是一样的,但很多时候分词的结果往往不同。我们先了解下汉字编码的知识。
#include<string> #include<iostream> using namespace std; int main() { string str="啊"; printf("%x %x\n",str[0],str[1]); }这个程序输出的结果是ffffffb0和ffffffa1。把啊换成阿,输出的结果是ffffffb0和ffffffa2。这是因为现在汉字编码大都使用的是GBK编码。可以在网上找到GBK编码的编码表,下图即为其中的一部分。
看完编码表就会发现,每个汉字用两个字节编码,第一个字节的范围是0xb0到0xf7,第二个字节的范围是0xa1到0xfe。回到逆向最大匹配法,可以借鉴Robin-Karp算法hash处理的思想和STL中的map实现。对于词典中的每个词我们都生成一个hash值并用map来存储它们,匹配时边生成hash值边比较。下面是一个简单的示例程序,先输入词典的大小和词典中的词,再输入待分词的文本进行匹配。
#include<map> #include<string> #include<iomanip> #include<iostream> using namespace std; int main() { string str; map<int,string> dict; int maxlength=0,temp=0,i,j,size; cin>>size; //词典的大小 for(i=0;i<size;i++) { temp=0; cin>>str; if(str.length()>maxlength) maxlength=str.length(); for(j=str.length()-1;j>=0;j--) { temp+=str[j]; temp=temp*100; } dict[temp]=str; } int pos; //记录匹配的位置 cin>>str; i=j=str.length()-1; map<int,string>::iterator map_it; while(i>=0) { j=i; temp=0; for(;j>=0;j--) { temp+=str[j]; temp=temp*100; map_it=dict.find(temp); if(map_it!=dict.end()&&j!=0) { if(j%2==0) pos=j; //如果j能被2整除说明j是一个汉字中的第一个字节,如果匹配成功就记录此时的位置 } if(map_it!=dict.end()&&j==0) { cout<<str.substr(0,i-j+1)<<endl; //如果到第一个词匹配成功就直接输出并结束 i=-1; break; } if(map_it==dict.end()&&(i-j>=maxlength-1||j==0)) { //如果超出了最大匹配长度或者到第一个词仍然没有匹配 if(pos==-1) { i-=2; //如果没有记录上一次匹配的位置就进行单字切分 break; } else { cout<<str.substr(pos,i-pos+1)<<endl; //如果记录了上一次匹配的位置就回到匹配的位置 i=pos-1; pos=-1; break; } } } } }对一篇文档进行分词后,常用的提取关键词的方法是TF-IDF法。假定现在有一篇长文《中国的蜜蜂养殖》,准备用计算机提取它的关键词。一个容易想到的思路就是找到出现次数最多的词。如果某个词很重要,它应该在这篇文章中多次出现。于是,我们进行词频(Term Frequency)统计。结果出现次数最多的词是"的"、"是"、"在"等这一类最常用的词。它们叫做"停用词"(stop words),表示对找到结果毫无帮助、必须过滤掉的词。假设把它们都过滤掉了,只考虑剩下的有实际意义的词。这样又会遇到了另一个问题,我们可能发现"中国"、"蜜蜂"、"养殖"这三个词出现次数一样多。这是不是意味着,作为关键词,它们的重要性是一样的?显然不是这样。因为"中国"是很常见的词,相对而言,"蜜蜂"和"养殖"不那么常见。如果这三个词在一篇文章的出现次数一样多,有理由认为,"蜜蜂"和"养殖"的重要程度要大于"中国",也就是说,在关键词排序上面,"蜜蜂"和"养殖"应该排在"中国"的前面。所以,我们需要一个重要性调整系数,衡量一个词是不是常见词。如果某个词比较少见,但是它在这篇文章中多次出现,那么它很可能就反映了这篇文章的特性,正是我们所需要的关键词。用统计学语言表达,就是在词频的基础上,要对每个词分配一个"重要性"权重。最常见的词("的"、"是"、"在")给予最小的权重,较常见的词("中国")给予较小的权重,较少见的词("蜜蜂"、"养殖")给予较大的权重。这个权重叫做"逆文档频率"(Inverse
Document Frequency,缩写为IDF),它的大小与一个词的常见程度成反比。将TF和IDF这两个值相乘,就得到了一个词的TF-IDF值。某个词对文章的重要性越高,它的TF-IDF值就越大。
还是以《中国的蜜蜂养殖》为例,假定该文长度为1000个词,"中国"、"蜜蜂"、"养殖"各出现20次,则这三个词的TF都为0.02。然后,搜索Google发现,包含"的"字的网页共有250亿张,假定这就是中文网页总数。包含"中国"的网页共有62.3亿张,包含"蜜蜂"的网页为0.484亿张,包含"养殖"的网页为0.973亿张。则它们的IDF和TF-IDF如下表。
上表可见,"蜜蜂"的TF-IDF值最高,"养殖"其次,"中国"最低。所以,如果只选择一个词,"蜜蜂"就是这篇文章的关键词。
2.特征选择的算法
机器学习算法的空间、时间复杂度依赖于输入数据的规模,维度规约(Dimensionality reduction)则是一种被用于降低输入数据维数的方法。维度规约可以分为两类:
特征提取(feature extraction),将原始的d维空间映射到k维空间中(新的k维空间不输入原始空间的子集)。
特征选择(feature selection),从原始的d维空间中,选择为我们提供信息最多的k个维(这k个维属于原始空间的子集)。
在文本挖掘与文本分类的有关问题中,常采用特征选择方法。原因是文本的特征一般都是单词(term),具有语义信息,使用特征选择找出的k维子集,仍然是单词作为特征,保留了语义信息,而特征提取则找k维新空间,将会丧失了语义信息。目前,文本的特征选择方法主要有DF,MI,IG,CHI这几种。为了方便描述,我们首先明确一些定义。
p(t):文档包含特征词t的概率。
:文档不属于Ci的概率。
p(Ci|t):已知文档包括特征词t的条件下,该文档属于Ci的概率。
:已知文档属于Ci的条件下,该文档不包括特征词t的概率。
类似的其他的一些概率如p(C i ),
,
等,有着类似的定义。
为了估计这些概率需要通过统计训练样本的相关频率信息,如下表。
Aij:包含特征词ti,并且类别属于Cj的文档数量
Bij:包含特征词ti,并且类别不属于Cj的文档数量
Cij:不包含特征词ti,并且类别属于Cj的文档数量
Dij:不包含特征词ti,并且类别不属于Cj的文档数量
Aij+Bij:包含特征词ti的文档数量
Cij+Dij:不包含特征词ti的文档数量
Aij+Cij:Cj类的文档数量
Bij+Dij:非Cj类的文档数量
Aij+Bij+Cij+Dij=N:语料中所有文档数量
有了这些统计量,有关概率的估算就变得容易,如:
p(ti) =(Aij+Bij)/N
p(Cj)=(Aij+Cij)/N
p(Cj|tj)=Aij/(Aij+Bij)
下面讲解几种常用的文本特征选择算法。
DF(Document Frequency)
DF是统计特征词出现的文档数量,用来衡量某个特征词的重要性。
如果某些特征词在文档中经常出现,那么这个词就可能很重要。而对于在文档中出现很少(如仅在语料中出现1次)特征词,携带了很少的信息量,甚至是噪声,这些特征词,对分类器学习影响也是很小。DF特征选择方法属于无监督的学习算法,仅考虑了频率因素而没有考虑类别因素,因此,DF算法的将会引入一些没有意义的词。
MI(Mutual Information)
互信息法用于衡量特征词与文档类别直接的信息量。
从上面的公式上看出,如果某个特征词的频率很低,那么互信息得分就会很大,因此互信息法倾向"低频"的特征词。相对的词频很高的词,得分就会变低,如果这词携带了很高的信息量,互信息法就会变得低效。
IG(Information Gain)
信息增益法,通过某个特征词的缺失与存在的两种情况下,通过语料中前后信息的增加,衡量某个特征词的重要性。
依据IG的定义,每个特征词ti的IG得分前面一部分
计算值是一样,可以省略。因此,IG的计算公式如下。
IG与MI存在关系:
,因此,IG方式实际上就是互信息
与互信息
加权。
CHI(Chi-square)
CHI特征选择算法利用了统计学中的假设检验的基本思想:首先假设特征词与类别直接是不相关的,如果利用CHI分布计算出的检验值偏离阈值越大,那么更有信心否定原假设,接受原假设的备则假设:特征词与类别有着很高的关联度。
对于一个给定的语料而言,文档的总数N、Cj类文档的数量和非Cj类文档的数量都是一个定值,因此CHI的计算公式可以简化为
。
3.文本分类的算法
常用的文本分类算法有朴素贝叶斯算法、向量空间距离测度分类算法、K最邻近分类算法(KNN)、支持向量机(SVM)、神经网络算法、决策树分类算法等。我们主要讲解朴素贝叶斯算法和K最邻近分类算法(KNN)算法。
朴素贝叶斯算法
要理解贝叶斯算法,必须先理解贝叶斯定理。我们先讲解条件概率公式和全概率公式。
假定样本空间S,是两个事件A与A'的和。
在这种情况下,事件B可以划分成两个部分。
这就是全概率公式。这就是全概率公式。它的含义是,如果A和A'构成样本空间的一个划分,那么事件B的概率,就等于A和A'的概率分别乘以B对这两个事件的条件概率之和。
将这个公式代入上一节的条件概率公式,就得到了条件概率的另一种写法。
接下来进入正题。对条件概率公式进行变形,可以得到如下形式。
我们把P(A)称为先验概率(Prior probability),即在B事件发生之前,我们对A事件概率的一个判断。P(A|B)称为后验概率(Posterior probability),即在B事件发生之后,我们对A事件概率的重新评估。P(B|A)/P(B)称为可能性函数(Likelyhood),这是一个调整因子,使得预估概率更接近真实概率。所以,条件概率可以理解成:后验概率=先验概率X调整因子。我们先预估一个先验概率,然后加入实验结果,看这个实验到底是增强还是削弱了先验概率,由此得到更接近事实的后验概率。在这里,如果可能性函数P(B|A)/P(B)>1,意味着先验概率被增强,事件A的发生的可能性变大;如果可能性函数=1,意味着B事件无助于判断事件A的可能性;如果可能性函数<1,意味着先验概率被削弱,事件A的可能性变小。为了加深对贝叶斯推断的理解,我们看个例子。
第一个例子。两个一模一样的碗,一号碗有30颗水果糖和10颗巧克力糖,二号碗有水果糖和巧克力糖各20颗。现在随机选择一个碗,从中摸出一颗糖,发现是水果糖。请问这颗水果糖来自一号碗的概率有多大?我们假定,H1表示一号碗,H2表示二号碗。由于这两个碗是一样的,所以P(H1)=P(H2),也就是说,在取出水果糖之前,这两个碗被选中的概率相同。因此,P(H1)=0.5,我们把这个概率就叫做"先验概率",即没有做实验之前,来自一号碗的概率是0.5。再假定,E表示水果糖,所以问题就变成了在已知E的情况下,来自一号碗的概率有多大,即求P(H1|E)。我们把这个概率叫做后验概率,即在E事件发生之后,对P(H1)的修正。
这表明,来自一号碗的概率是0.6。也就是说,取出水果糖之后,H1事件的可能性得到了增强。贝叶斯算法在计算机领域有着许多的应用,举一个垃圾邮件过滤的例子。现在,我们收到了一封新邮件。在未经统计分析之前,我们假定它是垃圾邮件的概率为50%。我们用S表示垃圾邮件(spam),H表示正常邮件(healthy)。因此,P(S)和P(H)的先验概率都是50%。然后,对这封邮件进行解析,发现其中包含了sex这个词,请问这封邮件属于垃圾邮件的概率有多高?我们用W表示"sex"这个词,那么问题就变成了如何计算P(S|W)的值,即在某个词语(W)已经存在的条件下,垃圾邮件(S)的概率有多大。
公式中,P(W|S)和P(W|H)的含义是,这个词语在垃圾邮件和正常邮件中,分别出现的概率。这两个值可以从历史资料库中得到,对sex这个词来说,上文假定它们分别等于5%和0.05%。另外,P(S)和P(H)的值,前面说过都等于50%。
因此,这封新邮件是垃圾邮件的概率等于99%。这说明,sex这个词的推断能力很强,将50%的先验概率一下子提高到了99%的后验概率。做完上面一步,请问我们能否得出结论,这封新邮件就是垃圾邮件?回答是不能。因为一封邮件包含很多词语,一些词语说这是垃圾邮件,另一些说这不是。你怎么知道以哪个词为准?Paul Graham的做法是,选出这封信中P(S|W)最高的15个词,计算它们的联合概率。如果有的词是第一次出现,无法计算P(S|W),就假定这个值等于0.4。因为垃圾邮件用的往往都是某些固定的词语,所以如果你从来没见过某个词,它多半是一个正常的词。所谓联合概率,就是指在多个事件发生的情况下,另一个事件发生概率有多大。比如,已知W1和W2是两个不同的词语,它们都出现在某封电子邮件之中,那么这封邮件是垃圾邮件的概率,就是联合概率。在已知W1和W2的情况下,无非就是两种结果:垃圾邮件(事件E1)或正常邮件(事件E2)。
其中,W1、W2和垃圾邮件的概率分别如下。
如果假定所有事件都是独立事件,那么就可以计算P(E1)和P(E2)。
将P(S)等于0.5代入,P(S|W1)记为P1,P(S|W2)记为P2。
将上面的公式扩展到15个词的情况,就得到了最终的概率计算公式。
一封邮件是不是垃圾邮件,就用这个式子进行计算。这时我们还需要一个用于比较的门槛值。Paul Graham的门槛值是0.9,概率大于0.9,表示15个词联合认定,这封邮件有90%以上的可能属于垃圾邮件;概率小于0.9,就表示是正常邮件。有了这个公式以后,一封正常的信件即使出现sex这个词,也不会被认定为垃圾邮件了。
K最邻近分类算法(KNN)
K最近邻(kNN,k-NearestNeighbor)分类算法是数据挖掘分类技术中最简单的方法之一。所谓K最近邻,就是k个最近的邻居的意思,说的是每个样本都可以用它最接近的k个邻居来代表。kNN算法的核心思想是如果一个样本在特征空间中的k个最相邻的样本中的大多数属于某一个类别,则该样本也属于这个类别,并具有这个类别上样本的特性。下图中,绿色圆要被决定赋予哪个类,是红色三角形还是蓝色四方形?如果K=3,由于红色三角形比例为2/3,绿色圆被赋予红色三角形类,如果K=5,由于蓝色四方形比例为3/5,绿色圆被赋予蓝色四方形类。
对于我们文本分类的例子,可以用余弦相似度的办法计算文本之间的距离。余弦相似度用向量空间中两个向量夹角的余弦值作为衡量两个个体间差异的大小。相比距离度量,余弦相似度更加注重两个向量在方向上的差异,而非距离或长度上。为了简单起见,我们先从句子着手。
句子A:我喜欢看电视,不喜欢看电影。
句子B:我不喜欢看电视,也不喜欢看电影。
请问怎样才能计算上面两句话的相似程度?基本思路是:如果这两句话的用词越相似,它们的内容就应该越相似。因此,可以从词频入手,计算它们的相似程度。
第一步,分词。
句子A:我/喜欢/看/电视,不/喜欢/看/电影。
句子B:我/不/喜欢/看/电视,也/不/喜欢/看/电影。
第二步,列出所有的词。
我,喜欢,看,电视,电影,不,也。
第三步,计算词频。
句子A:我1,喜欢2,看2,电视1,电影1,不1,也0。
句子B:我1,喜欢2,看2,电视1,电影1,不2,也1。
第四步,写出词频向量。
句子A:[1, 2, 2, 1, 1, 1, 0]
句子B:[1, 2, 2, 1, 1, 2, 1]
到这里,问题就变成了如何计算这两个向量的相似程度。我们可以把它们想象成空间中的两条线段,都是从原点出发,指向不同的方向。两条线段之间形成一个夹角,如果夹角为0度,意味着方向相同、线段重合;如果夹角为90度,意味着形成直角,方向完全不相似;如果夹角为180度,意味着方向正好相反。因此,我们可以通过夹角的大小,来判断向量的相似程度。夹角越小,就代表越相似。
总结一下KNN算法的流程:
step.1---初始化距离为最大值;
step.2---利用余弦相似度计算未知样本和每个训练样本的距离dist;
step.3---得到目前K个最临近样本中的最大距离maxdist;
step.4---如果dist小于maxdist,则将该训练样本作为K-最近邻样本;
step.5---重复步骤2、3、4,直到未知样本和所有训练样本的距离都算完;
step.6---统计K最近邻样本中每个类标号出现的次数;
step.7---选择出现频率最大的类标号作为未知样本的类标号。
4.总结
基本的原理到此为止就差不多讲清楚了,下面是代码,是一个很粗糙的实现。完整的代码和文档在https://github.com/houjingyi233/text-categorization-experiment/
#include<set> #include<map> #include<io.h> #include<queue> #include<vector> #include<string> #include<iomanip> #include<fstream> #include<sstream> #include<iostream> using namespace std; string str; int maxlength; //max length of words in dict int feature_nums = 0, tot = 0; //nums of feature words //nums of known articles const int K = 15; //set k in KNN vector<string> files; //store file names map<int, string> dict; //store dict map<string, int> words; //store words in to be classified struct Distance { int dis; int attribute; bool operator<(const Distance&a) const { return a.dis<dis; } }distances[10000]; //store distances between known articles and unknown articles string featureword[100000]; //store feature words map<string, int> articles[100000]; //store feature words and occurrences in each known articles priority_queue< Distance, vector<Distance>, less<Distance> > knn_queue; vector< pair<string, int> > words_result; struct Articles_result { vector< pair<string, int> > article_words; int attribute; }articles_result[100000]; ofstream fout("D:\\result.txt"); //dict files for each classification char *dictnames[39] = { "none","D:\\足球.txt","D:\\篮球.txt","D:\\排球.txt","D:\\网球.txt","D:\\手球.txt","D:\\垒球.txt","D:\\曲棍球.txt", "D:\\橄榄球.txt","D:\\水球.txt","D:\\棒球.txt","D:\\高尔夫.txt","D:\\乒乓.txt","D:\\羽毛球.txt","D:\\台球.txt", "D:\\壁球.txt","D:\\棋牌.txt","D:\\游泳.txt","D:\\跳水.txt","D:\\赛车.txt","D:\\自行车.txt","D:\\体操.txt", "D:\\田径.txt","D:\\武术.txt","D:\\拳击.txt","D:\\摔跤.txt","D:\\柔道跆拳道.txt","D:\\举重.txt","D:\\击剑.txt", "D:\\马术.txt","D:\\射击射箭.txt","D:\\赛艇与皮划艇.txt","D:\\帆船帆板.txt","D:\\铁人三项和现代五项.txt", "D:\\冰雪项目.txt","D:\\登山.txt","D:\\奥运.txt","D:\\亚运.txt","D:\\全运.txt", }; //training files' catalog char *training_filepaths[39] = { "none", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类训练文档\\01足球", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类训练文档\\02篮球", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类训练文档\\03排球", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类训练文档\\04网球", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类训练文档\\05手球", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类训练文档\\06垒球", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类训练文档\\07曲棍球", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类训练文档\\08橄榄球", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类训练文档\\09水球", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类训练文档\\10棒球", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类训练文档\\11高尔夫", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类训练文档\\12乒乓", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类训练文档\\13羽毛球", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类训练文档\\14台球", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类训练文档\\15壁球", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类训练文档\\16棋牌", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类训练文档\\17游泳", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类训练文档\\18跳水", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类训练文档\\19赛车", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类训练文档\\20自行车", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类训练文档\\21体操", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类训练文档\\22田径", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类训练文档\\23武术", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类训练文档\\24拳击", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类训练文档\\25摔跤", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类训练文档\\26柔道跆拳道", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类训练文档\\27举重", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类训练文档\\28击剑", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类训练文档\\29马术", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类训练文档\\30射击射箭", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类训练文档\\31赛艇与皮划艇", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类训练文档\\32帆船帆板", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类训练文档\\33铁人三项和现代五项", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类训练文档\\34冰雪项目", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类训练文档\\35登山", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类训练文档\\36奥运", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类训练文档\\37亚运", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类训练文档\\38全运", }; //test files' catalog char *test_filepaths[39] = { "none", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类测试文档\\01足球", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类测试文档\\02篮球", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类测试文档\\03排球", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类测试文档\\04网球", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类测试文档\\05手球", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类测试文档\\06垒球", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类测试文档\\07曲棍球", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类测试文档\\08橄榄球", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类测试文档\\09水球", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类测试文档\\10棒球", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类测试文档\\11高尔夫", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类测试文档\\12乒乓", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类测试文档\\13羽毛球", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类测试文档\\14台球", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类测试文档\\15壁球", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类测试文档\\16棋牌", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类测试文档\\17游泳", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类测试文档\\18跳水", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类测试文档\\19赛车", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类测试文档\\20自行车", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类测试文档\\21体操", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类测试文档\\22田径", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类测试文档\\23武术", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类测试文档\\24拳击", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类测试文档\\25摔跤", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类测试文档\\26柔道跆拳道", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类测试文档\\27举重", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类测试文档\\28击剑", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类测试文档\\29马术", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类测试文档\\30射击射箭", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类测试文档\\31赛艇与皮划艇", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类测试文档\\32帆船帆板", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类测试文档\\33铁人三项和现代五项", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类测试文档\\34冰雪项目", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类测试文档\\35登山", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类测试文档\\36奥运", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类测试文档\\37亚运", "C:\\Users\\Administrator\\Desktop\\实验4\\体育领域\\体育分类测试文档\\38全运", }; int cmp(const pair<string, int> &x, const pair<string, int> &y) { return x.second > y.second; } void sort_by_value(map<string, int> &t_map, vector< pair<string, int> > &t_vec) { for (map<string, int>::iterator iter = t_map.begin(); iter != t_map.end(); iter++) { t_vec.push_back(make_pair(iter->first, iter->second)); } sort(t_vec.begin(), t_vec.end(), cmp); } //store all files in path with exd extension in vector void getfiles(string path, string exd, vector<string>& files) { long hFile = 0; struct _finddata_t fileinfo; string pathName, exdName; if (0 != strcmp(exd.c_str(), "")) { exdName = "\\*." + exd; } else { exdName = "\\*"; } if ((hFile = _findfirst(pathName.assign(path).append(exdName).c_str(), &fileinfo)) != -1) { do { if ((fileinfo.attrib& _A_SUBDIR)) { if (strcmp(fileinfo.name, ".") != 0 && strcmp(fileinfo.name, "..") != 0) getfiles(pathName.assign(path).append("\\").append(fileinfo.name), exd, files); } else { if (strcmp(fileinfo.name, ".") != 0 && strcmp(fileinfo.name, "..") != 0) files.push_back(pathName.assign(path).append("\\").append(fileinfo.name)); } } while (_findnext(hFile, &fileinfo) == 0); _findclose(hFile); } } //read text in file and store chinese character in str void read_text(char *textname) { ifstream infile(textname); ostringstream buf; buf.clear(); str.clear(); char ch; while (buf&&infile.get(ch)) buf.put(ch); str = buf.str(); unsigned char c1, c2; int i = 0; while (i<str.length()) { if (i + 1<str.length()) { c1 = (unsigned char)str[i]; c2 = (unsigned char)str[i + 1]; if (c1 >= 0xb0 && c1 <= 0xf7 && c2 >= 0xa1 && c2 <= 0xfe) i += 2; //in GB2312 a chinese character is represented as two bytes //high bytes from 0xB0 to 0xF7,low bytes from 0xA1 to 0xFE else str.erase(i, 1); } else str.erase(i, 1); } infile.close(); } //read feature word in file,store in map and string void read_feature(char *featurename) { ifstream infile(featurename, ios::in); feature_nums = 0; while (getline(infile, str)) { int temp = 0; featureword[feature_nums]= str; int len = str.length(); if (len>maxlength) maxlength = str.length(); for (int i = str.length() - 1; i >= 0; i--) { temp += str[i]; temp = temp * 100; } dict[temp] = str; str.clear(); feature_nums++; } infile.close(); } //record keywords in each training distances void pre_knn(char *dictname, char *filepath,int attribute) { dict.clear(); files.clear(); feature_nums = 0; read_feature(dictname); getfiles(filepath, "txt", files); int size = files.size(); for (int k = 0; k<size; k++) { //deal with escape character '\' string tempstr = files[k]; int len = tempstr.size(); string backslash = "\\"; for (int m = 0; m<len; m++) { if (tempstr[m] == '\\') { tempstr.insert(m, backslash); m += 2; len = tempstr.size(); } } char *fname = (char *)tempstr.c_str(); cout << fname << endl; read_text(fname); int pos = -1; //pos is for record matching position int i = str.length() - 1; map<int, string>::iterator map_it; while (i >= 0) { int j = i; int temp = 0; for (; j >= 0; j--) { temp += str[j]; temp = temp * 100; map_it = dict.find(temp); if (map_it != dict.end() && j != 0) { if (j % 2 == 0) pos = j; //record matching position } if (map_it != dict.end() && j == 0) { for (int k = 0; k<feature_nums; k++) { if (str.substr(0, i - j + 1) == featureword[k]) { if (articles[tot].find(str.substr(0, i + 1)) == articles[tot].end()) { articles[tot][str.substr(0, i + 1)] = 1; } else { articles[tot][str.substr(0, i + 1)]++; } break; } } i = -1; break; } if (map_it == dict.end() && (i - j >= maxlength - 1 || j == 0)) { if (pos == -1) { i -= 2; break; } //match failed,skip this character else { for (int k = 0; k<feature_nums; k++) { if (str.substr(pos, i - pos + 1) == featureword[k]) { if (articles[tot].find(str.substr(pos, i - pos + 1)) == articles[tot].end()) { articles[tot][str.substr(pos, i - pos + 1)] = 1; } else { articles[tot][str.substr(pos, i - pos + 1)]++; } } } i = pos - 1; pos = -1; break; } //use pos recorded last time } } } sort_by_value(articles[tot], articles_result[tot].article_words); articles_result[tot].attribute = attribute; tot++; } } //read terms in file ,compute hash value and store them in map void read_dict(char *dictname) { ifstream infile(dictname, ios::in); dict.clear(); string dict_str; int temp; while (getline(infile, dict_str)) { temp = 0; int len = dict_str.length(); if (len>maxlength) maxlength = dict_str.length(); for (int i = dict_str.length() - 1; i >= 0; i--) { temp += dict_str[i]; temp = temp * 100; } dict[temp] = dict_str; dict_str.clear(); } infile.close(); } //count distances between two articles based on cosine similarity void count_dis(int i) { int temp1, temp2, temp3; temp1 = temp2 = temp3 = 0; for (int j = 0; j < articles_result[i].article_words.size(); j++) { temp1 += articles_result[i].article_words[j].second*articles_result[i].article_words[j].second; } for(int k = 0; k < words_result.size(); k++) { temp2 += words_result[k].second*words_result[k].second; } for (int j = 0; j <articles_result[i].article_words.size(); j++) { for (int k = 0; k < words_result.size(); k++) { if (articles_result[i].article_words[j].first == words_result[k].first) { temp3 += articles_result[i].article_words[j].second*words_result[k].second; break; } } } if (temp1 != 0 && temp2 != 0) { distances[i].dis = 100000*temp3*temp3 / temp1 / temp2; } else { distances[i].dis = 0; } distances[i].attribute = articles_result[i].attribute; if (knn_queue.size()<K) knn_queue.push(distances[i]); else if (distances[i].dis>knn_queue.top().dis) { knn_queue.pop(); knn_queue.push(distances[i]); } } //count the number of each class label in k nearest neighbor //the class label with the largest frequency //is selected as the class label of the unknown sample void select_category() { int categorys[39]; for (int i = 1; i<39; i++) categorys[i] = 0; while (!knn_queue.empty()) { int t = knn_queue.top().attribute; cout << t << endl; knn_queue.pop(); categorys[t] += 1; } int maxelement = 0, pos; for (int i = 1; i<39; i++) { if (categorys[i]>maxelement) { maxelement = categorys[i]; pos = i; } } cout << "this acticle is classified in:" << pos << endl; fout << "this acticle is classified in:" << pos << endl; } //segmentation acticles in directory void txt_segmentation(char *filepath) { files.clear(); getfiles(filepath, "txt", files); int size = files.size(); for (int k = 0; k<size; k++) { //deal with escape character '\' string tempstr = files[k]; int len = tempstr.size(); string backslash = "\\"; for (int m = 0; m<len; m++) { if (tempstr[m] == '\\') { tempstr.insert(m, backslash); m += 2; len = tempstr.size(); } } char *fname = (char *)tempstr.c_str(); fout << fname << endl; cout << "judging the category of this article:" << fname << endl; read_text(fname); int pos = -1; int i = str.length() - 1; map<int, string>::iterator map_it; words.clear(); //store all terms while (!knn_queue.empty()) knn_queue.pop(); while (i >= 0) { int j = i; int temp = 0; for (; j >= 0; j--) { temp += str[j]; temp = temp * 100; map_it = dict.find(temp); if (map_it != dict.end() && j != 0) { if (j % 2 == 0) pos = j; } if (map_it != dict.end() && j == 0) { if (words.find(str.substr(0, i + 1)) == words.end()) { words[str.substr(0, i + 1)] = 1; } else { words[str.substr(0, i + 1)]++; } i = -1; break; } if (map_it == dict.end() && (i - j >= maxlength - 1 || j == 0)) { if (pos == -1) { i -= 2; break; } else { if (words.find(str.substr(pos, i - pos + 1)) == words.end()) { words[str.substr(pos, i - pos + 1)] = 1; } else { words[str.substr(pos, i - pos + 1)]++; } i = pos - 1; pos = -1; break; } } } } sort_by_value(words,words_result); for (int k = 0; k<tot; k++) count_dis(k); //count dis between all known article select_category(); } } int main() { char *filepath, *dictname; for (int i = 1; i<39; i++) { dictname = dictnames[i]; filepath = training_filepaths[i]; pre_knn(dictname, filepath,i); } read_dict("D:\\词典.txt"); for (int i = 1; i<39; i++) { filepath = test_filepaths[i]; txt_segmentation(filepath); } fout.close(); system("pause"); return 0; }
相关文章推荐
- 文本分类专题(ultimate 版)绝对是目前最全的C++版开源文本分类代码和最令人耳目一新的实验解释
- 文本分类入门(七)相关概念总结
- 【机器学习实验】使用朴素贝叶斯进行文本的分类
- AdaBoost算法在文本分类中的总结
- 文本分类的算法总结
- 汇编实验,字符分类统计,提示的错误总结
- 最近关注学习文本分类——天书般的ICTCLAS分词系统代码(一)
- 文本分类入门(七)相关概念总结
- LibSVM实现文本分类总结
- Python使用jieba分词并用weka进行文本分类
- 文本分类专题(ultimate 版)绝对是目前最全的C++版开源文本分类代码和最令人耳目一新的实验解释
- 【文本分类】最强中文分词系统ICTCLAS
- 重新实现关于Mikolov的集成文本分类实验(详细过程)-
- 机器学习C3分类:垃圾过滤(tm包文本挖掘,朴素贝叶斯算法的垃圾邮件分类处理等)
- 借助weka实现的分类器进行针对文本分类问题的特征词选择实验(实验代码备份)
- 关于一次小工作的总结,python解压缩,过滤文本
- JQuery的基本选择器使用总结以及过滤,文本,可见度的选择代码
- 文本情感分类:分词 OR 不分词(3)
- 深度学习与文本分类总结第一篇--常用模型总结