使用libsvm实现文本分类

文本分类,首先它是分类问题,应该对应着分类过程的两个重要的步骤,一个是使用训练数据集训练分类器,另一个就是使用测试数据集来评价分类器的分类精度。然而,作为文本分类,它还具有文本这样的约束,所以对于文本来说,需要额外的处理过程,我们结合使用libsvm从宏观上总结一下,基于libsvm实现文本分类实现的基本过程,如下所示:

  1. 选择文本训练数据集和测试数据集:训练集和测试集都是类标签已知的;
  2. 训练集文本预处理:这里主要包括分词、去停用词、建立词袋模型(倒排表);
  3. 选择文本分类使用的特征向量(词向量):最终的目标是使得最终选出的特征向量在多个类别之间具有一定的类别区分度,可以使用相关有效的技术去实现特征向量的选择,由于分词后得到大量的词,通过选择降维技术能很好地减少计算量,还能维持分类的精度;
  4. 输出libsvm支持的量化的训练样本集文件:类别名称、特征向量中每个词元素分别到数字编号的映射转换,以及基于类别和特征向量来量化文本训练集,能够满足使用libsvm训练所需要的数据格式;
  5. 测试数据集预处理:同样包括分词(需要和训练过程中使用的分词器一致)、去停用词、建立词袋模型(倒排表),但是这时需要加载训练过程中生成的特征向量,用特征向量去排除多余的不在特征向量中的词(也称为降维);
  6. 输出libsvm支持的量化的测试样本集文件:格式和训练数据集的预处理阶段的输出相同。
  7. 使用libsvm训练文本分类器:使用训练集预处理阶段输出的量化的数据集文件,这个阶段也需要做很多工作(后面会详细说明),最终输出分类模型文件
  8. 使用libsvm验证分类模型的精度:使用测试集预处理阶段输出的量化的数据集文件,和分类模型文件来验证分类的精度。
  9. 分类模型参数寻优:如果经过libsvm训练出来的分类模型精度很差,可以通过libsvm自带的交叉验证(Cross Validation)功能来实现参数的寻优,通过搜索参数取值空间来获取最佳的参数值,使分类模型的精度满足实际分类需要。

基于上面的分析,分别对上面每个步骤进行实现,最终完成一个分类任务。

数据集选择

我们选择了搜狗的语料库,可以参考后面的链接下载语料库文件。这里,需要注意的是,分别准备一个训练数据集和一个测试数据集,不要让两个数据集有交叉。例如,假设有C个类别,选择每个分类的下的N篇文档作为训练集,总共的训练集文档数量为C*N,剩下的每一类下M篇作为测试数据集使用,测试数据集总共文档数等于C*M。

数据集文本预处理

我们选择使用ICTCLAS分词器,使用该分词器可以不需要预先建立自己的词典,而且分词后已经标注了词性,可以根据词性对词进行一定程度过滤(如保留名词,删除量词、叹词等等对分类没有意义的词汇)。
下载ICTCLAS软件包,如果是在Win7 64位系统上使用Java实现分词,选择如下两个软件包:

  • 20131115123549_nlpir_ictclas2013_u20131115_release.zip
  • 20130416090323_Win-64bit-JNI-lib.zip

将第二个软件包中的NLPIR_JNI.dll文件拷贝到C:\Windows\System32目录下面,将第一个软件包中的Data目录和NLPIR.dll、NLPIR.lib、NLPIR.h、NLPIR.lib文件拷贝到Java工程根目录下面。
对于其他操作系统,可以到ICTCLAS网站(http://ictclas.nlpir.org/downloads)下载对应版本的软件包。
下面,我们使用Java实现分词,定义分词器接口,以便切换其他分词器实现时,容易扩展,如下所示:

package org.shirdrn.document.processor.common;

import java.io.File;
import java.util.Map;

public interface DocumentAnalyzer {
     Map<String, Term> analyze(File file);
}

增加一个外部的停用词表,这个我们直接封装到抽象类AbstractDocumentAnalyzer中去了,该抽象类就是从一个指定的文件或目录读取停用词文件,将停用词加载到内存中,在分词的过程中对词进行进一步的过滤。然后基于上面的实现,给出包裹ICTCLAS分词器的实现,代码如下所示:

package org.shirdrn.document.processor.analyzer;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;

import kevin.zhang.NLPIR;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.shirdrn.document.processor.common.DocumentAnalyzer;
import org.shirdrn.document.processor.common.Term;
import org.shirdrn.document.processor.config.Configuration;

public class IctclasAnalyzer extends AbstractDocumentAnalyzer implements DocumentAnalyzer {

     private static final Log LOG = LogFactory.getLog(IctclasAnalyzer.class);
     private final NLPIR analyzer;
    
     public IctclasAnalyzer(Configuration configuration) {
          super(configuration);
          analyzer = new NLPIR();
          try {
               boolean initialized = NLPIR.NLPIR_Init(".".getBytes(charSet), 1);
               if(!initialized) {
                    throw new RuntimeException("Fail to initialize!");
               }
          } catch (Exception e) {
               throw new RuntimeException("", e);
          }
     }

     @Override
     public Map<String, Term> analyze(File file) {
          String doc = file.getAbsolutePath();
          LOG.info("Process document: file=" + doc);
          Map<String, Term> terms = new HashMap<String, Term>(0);
          BufferedReader br = null;
          try {
               br = new BufferedReader(new InputStreamReader(new FileInputStream(file), charSet));
               String line = null;
               while((line = br.readLine()) != null) {
                    line = line.trim();
                    if(!line.isEmpty()) {
                         byte nativeBytes[] = analyzer.NLPIR_ParagraphProcess(line.getBytes(charSet), 1);
                         String content = new String(nativeBytes, 0, nativeBytes.length, charSet);
                         String[] rawWords = content.split("\\s+");
                         for(String rawWord : rawWords) {
                              String[] words = rawWord.split("/");
                              if(words.length == 2) {
                                   String word = words[0];
                                   String lexicalCategory = words[1];
                                   Term term = terms.get(word);
                                   if(term == null) {
                                        term = new Term(word);
                                        // TODO set lexical category
                                        term.setLexicalCategory(lexicalCategory);
                                        terms.put(word, term);
                                   }
                                   term.incrFreq();
                                   LOG.debug("Got word: word=" + rawWord);
                              }
                         }
                    }
               }
          } catch (IOException e) {
               e.printStackTrace();
          } finally {
               try {
                    if(br != null) {
                         br.close();
                    }
               } catch (IOException e) {
                    LOG.warn(e);
               }
          }
          return terms;
     }

}

它是对一个文件进行读取,然后进行分词,去停用词,最后返回的Map包含了的集合,此属性包括词性(Lexical Category)、词频、TF等信息。
这样,遍历数据集目录和文件,就能去将全部的文档分词,最终构建词袋模型。我们使用Java中集合来存储文档、词、类别之间的关系,如下所示:

     private int totalDocCount;
     private final List<String> labels = new ArrayList<String>();
     // Map<类别, 文档数量>
     private final Map<String, Integer> labelledTotalDocCountMap = new HashMap<String, Integer>();
     //  Map<类别, Map<文档 ,Map<词, 词信息>>>
     private final Map<String, Map<String, Map<String, Term>>> termTable =
               new HashMap<String, Map<String, Map<String, Term>>>();
     //  Map<词 ,Map<类别, Set<文档>>>
     private final Map<String, Map<String, Set<String>>> invertedTable =
               new HashMap<String, Map<String, Set<String>>>();

基于训练数据集选择特征向量

上面已经构建好词袋模型,包括相关的文档和词等的关系信息。现在我们来选择用来建立分类模型的特征词向量,首先要选择一种度量,来有效地选择出特征词向量。基于论文《A comparative study on feature selection in text categorization》,我们选择基于卡方统计量(chi-square statistic, CHI)技术来实现选择,这里根据计算公式:
chi-formula
其中,公式中各个参数的含义,说明如下:

  • N:训练数据集文档总数
  • A:在一个类别中,包含某个词的文档的数量
  • B:在一个类别中,排除该类别,其他类别包含某个词的文档的数量
  • C:在一个类别中,不包含某个词的文档的数量
  • D:在一个类别中,不包含某个词也不在该类别中的文档的数量

要想进一步了解,可以参考这篇论文。
使用卡方统计量,为每个类别下的每个词都进行计算得到一个CHI值,然后对这个类别下的所有的词基于CHI值进行排序,选择出最大的topN个词(很显然使用堆排序算法更合适);最后将多个类别下选择的多组topN个词进行合并,得到最终的特征向量。
其实,这里可以进行一下优化,每个类别下对应着topN个词,在合并的时候可以根据一定的标准,将各个类别都出现的词给出一个比例,超过指定比例的可以删除掉,这样可以使特征向量在多个类别分类过程中更具有区分度。这里,我们只是做了个简单的合并。
我们看一下,用到的存储结构,使用Java的集合来存储:

     // Map<label, Map<word, term>>
     private final Map<String, Map<String, Term>> chiLabelToWordsVectorsMap = new HashMap<String, Map<String, Term>>(0);
     // Map<word, term>, finally merged vector
     private final Map<String, Term> chiMergedTermVectorMap = new HashMap<String, Term>(0);

下面,实现特征向量选择计算的实现,代码如下所示:

package org.shirdrn.document.processor.component.train;

import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.shirdrn.document.processor.common.AbstractComponent;
import org.shirdrn.document.processor.common.Context;
import org.shirdrn.document.processor.common.Term;
import org.shirdrn.document.processor.utils.SortUtils;

public class FeatureTermVectorSelector extends AbstractComponent {

     private static final Log LOG = LogFactory.getLog(FeatureTermVectorSelector.class);
     private final int keptTermCountEachLabel;
     
     public FeatureTermVectorSelector(Context context) {
          super(context);
          keptTermCountEachLabel = context.getConfiguration().getInt("processor.each.label.kept.term.count", 3000);
     }

     @Override
     public void fire() {
          // compute CHI value for selecting feature terms 
          // after sorting by CHI value
          for(String label : context.getVectorMetadata().getLabels()) {
               // for each label, compute CHI vector
               LOG.info("Compute CHI for: label=" + label);
               processOneLabel(label);
          }
          
          // sort and select CHI vectors
          Iterator<Entry<String, Map<String, Term>>> chiIter = 
                    context.getVectorMetadata().chiLabelToWordsVectorsIterator();
          while(chiIter.hasNext()) {
               Entry<String, Map<String, Term>> entry = chiIter.next();
               String label = entry.getKey();
               LOG.info("Sort CHI terms for: label=" + label + ", termCount=" + entry.getValue().size());
               Entry<String, Term>[] a = sort(entry.getValue());
               for (int i = 0; i < Math.min(a.length, keptTermCountEachLabel); i++) {
                    Entry<String, Term> termEntry = a[i];
                    // merge CHI terms for all labels
                    context.getVectorMetadata().addChiMergedTerm(termEntry.getKey(), termEntry.getValue());
               }
          }
     }
     
     @SuppressWarnings("unchecked")
     private Entry<String, Term>[] sort(Map<String, Term> terms) {
          Entry<String, Term>[] a = new Entry[terms.size()];
          a = terms.entrySet().toArray(a);
          SortUtils.heapSort(a, true, keptTermCountEachLabel);
          return a;
     }

     private void processOneLabel(String label) {
          Iterator<Entry<String, Map<String, Set<String>>>> iter = 
                    context.getVectorMetadata().invertedTableIterator();
          while(iter.hasNext()) {
               Entry<String, Map<String, Set<String>>> entry = iter.next();
               String word = entry.getKey();
               Map<String, Set<String>> labelledDocs = entry.getValue();
               
               // A: doc count containing the word in this label
               int docCountContainingWordInLabel = 0;
               if(labelledDocs.get(label) != null) {
                    docCountContainingWordInLabel = labelledDocs.get(label).size();
               }
               
               // B: doc count containing the word not in this label
               int docCountContainingWordNotInLabel = 0;
               Iterator<Entry<String, Set<String>>> labelledIter = 
                         labelledDocs.entrySet().iterator();
               while(labelledIter.hasNext()) {
                    Entry<String, Set<String>> labelledEntry = labelledIter.next();
                    String tmpLabel = labelledEntry.getKey();
                    if(!label.equals(tmpLabel)) {
                         docCountContainingWordNotInLabel += entry.getValue().size();
                    }
               }
               
               // C: doc count not containing the word in this label
               int docCountNotContainingWordInLabel = 
                         getDocCountNotContainingWordInLabel(word, label);
               
               // D: doc count not containing the word not in this label
               int docCountNotContainingWordNotInLabel = 
                         getDocCountNotContainingWordNotInLabel(word, label);
               
               // compute CHI value
               int N = context.getVectorMetadata().getTotalDocCount();
               int A = docCountContainingWordInLabel;
               int B = docCountContainingWordNotInLabel;
               int C = docCountNotContainingWordInLabel;
               int D = docCountNotContainingWordNotInLabel;
               int temp = (A*D-B*C);
               double chi = (double) N*temp*temp / (A+C)*(A+B)*(B+D)*(C+D);
               Term term = new Term(word);
               term.setChi(chi);
               context.getVectorMetadata().addChiTerm(label, word, term);
          }
     }

     private int getDocCountNotContainingWordInLabel(String word, String label) {
          int count = 0;
          Iterator<Entry<String,Map<String,Map<String,Term>>>> iter = 
                    context.getVectorMetadata().termTableIterator();
          while(iter.hasNext()) {
               Entry<String,Map<String,Map<String,Term>>> entry = iter.next();
               String tmpLabel = entry.getKey();
               // in this label
               if(tmpLabel.equals(label)) {
                    Map<String, Map<String, Term>> labelledDocs = entry.getValue();
                    for(Entry<String, Map<String, Term>> docEntry : labelledDocs.entrySet()) {
                         // not containing this word
                         if(!docEntry.getValue().containsKey(word)) {
                              ++count;
                         }
                    }
                    break;
               }
          }
          return count;
     }
     
     private int getDocCountNotContainingWordNotInLabel(String word, String label) {
          int count = 0;
          Iterator<Entry<String,Map<String,Map<String,Term>>>> iter = 
                    context.getVectorMetadata().termTableIterator();
          while(iter.hasNext()) {
               Entry<String,Map<String,Map<String,Term>>> entry = iter.next();
               String tmpLabel = entry.getKey();
               // not in this label
               if(!tmpLabel.equals(label)) {
                    Map<String, Map<String, Term>> labelledDocs = entry.getValue();
                    for(Entry<String, Map<String, Term>> docEntry : labelledDocs.entrySet()) {
                         // not containing this word
                         if(!docEntry.getValue().containsKey(word)) {
                              ++count;
                         }
                    }
               }
          }
          return count;
     }

}

输出量化数据文件

特征向量已经从训练数据集中计算出来,接下来需要对每个词给出一个唯一的编号,从1开始,这个比较容易,输出特征向量文件,为测试验证的数据集所使用,文件格式如下所示:

认识     1
代理权     2
病理     3
死者     4
影子     5
生产国     6
容量     7
螺丝扣     8
大钱     9
壮志     10
生态圈     11
好事     12
全人类     13

由于libsvm使用的训练数据格式都是数字类型的,所以需要对训练集中的文档进行量化处理,我们使用TF-IDF度量,表示词与文档的相关性指标。
然后,需要遍历已经构建好的词袋模型,并使用已经编号的类别和特征向量,对每个文档计算TF-IDF值,每个文档对应一条记录,取出其中一条记录,输出格式如下所示:

8 9219:0.24673737883635047 453:0.09884635754820137 10322:0.21501394457319623 11947:0.27282495932970074 6459:0.41385272697452935 46:0.24041607991272138 8987:0.14897255497578704 4719:0.22296154731520754 10094:0.13116443653818177 5162:0.17050804524212404 2419:0.11831944042647048 11484:0.3501901869096251 12040:0.13267440708284894 8745:0.5320327758892881 9048:0.11445287153209653 1989:0.04677087098649205 7102:0.11308242956243426 3862:0.12007217405755069 10417:0.09796211412332205 5729:0.148037967054332 11796:0.08409157900442304 9094:0.17368658217203461 3452:0.1513474608736807 3955:0.0656773581702849 6228:0.4356889927309336 5299:0.15060439516792662 3505:0.14379243687841153 10732:0.9593462052245719 9659:0.1960034406311122 8545:0.22597403804274924 6767:0.13871522631066047 8566:0.20352452713417019 3546:0.1136541497082903 6309:0.10475466997804883 10256:0.26416957780238604 10288:0.22549409383630933

第一列的8表示类别编号,其余的每一列是词及其权重,使用冒号分隔,例如“9219:0.24673737883635047”表示编号为9219的词,对应的TF-IDF值为0.24673737883635047。如果特征向量有个N个,那么每条记录就对应着一个N维向量。
对于测试数据集中的文档,也使用类似的方法,不过首先需要加载已经输出的特征向量文件,从而构建一个支持libsvm格式的输出测试集文件。

使用libsvm训练文本分类器

前面的准备工作已经完成,现在可以使用libsvm工具包训练文本分类器。在使用libsvm的开始,需要做一个尺度变换操作(有时也称为归一化),有利于libsvm训练出更好的模型。我们已经知道前面输出的数据中,每一维向量都使用了TF-IDF的值,但是TF-IDF的值可能在一个不规范的范围之内(因为它依赖于TF和IDF的值),例如0.19872~8.3233,所以可以使用libsvm将所有的值都变换到同一个范围之内,如0~1.0,或者-1.0~1.0,可以根据实际需要选择。我们看一下命令:

F:\libsvm-3.0\windows>svm-scale.exe -l 0 -u 1 C:\\Users\\thinkpad\\Desktop\\vector\\train.txt > C:\\Users\\thinkpad\\Desktop\\vector\\train-scale.txt

尺度变换后输出到文件train-scale.txt中,它可以直接作为libsvm训练的数据文件,我使用Java版本的libsvm代码,输入参数如下所示:

train -h 0 -t 0 C:\\Users\\thinkpad\\Desktop\\vector\\train-scale.txt C:\\Users\\thinkpad\\Desktop\\vector\\model.txt

这里面,-t 0表示使用线性核函数,我发现在进行文本分类时,线性核函数比libsvm默认的-t 2非线性核函数的效果要要好一些。最后输出的是模型文件model.txt,内容示例如下所示:

svm_type c_svc
kernel_type linear
nr_class 10
total_sv 54855
rho -0.26562545584492675 -0.19596934447720876 0.24937032535471693 0.3391566771481882 -0.19541394291523667 -0.20017990510840347 -0.27349052681332664 -0.08694672836814998 -0.33057155365157015 0.06861675551386985 0.5815821822995312 0.7781870137763507 0.054722797451472065 0.07912846180263113 -0.01843419889020123 0.15110176721612528 -0.08484865489154271 0.46608205351462983 0.6643550487438468 -0.003914533674948038 -0.014576392246426623 -0.11384567944039309 0.09257404411884447 -0.16845445862600575 0.18053514069700813 -0.5510915276095857 -0.4885382860289285 -0.6057167948571457 -0.34910272249526764 -0.7066730463805829 -0.6980796972363181 -0.639435517196082 -0.8148772080348755 -0.5201121512955246 -0.9186975203736724 -0.008882360255733836 -0.0739010940085453 0.10314117392946448 -0.15342997221636115 -0.10028736061509444 0.09500443080371801 -0.16197536915675026 0.19553010464320583 -0.030005330377757263 -0.24521471309904422
label 8 4 7 5 10 9 3 2 6 1
nr_sv 6542 5926 5583 4058 5347 6509 5932 4050 6058 4850
SV
0.16456599916886336 0.22928285261208994 0.921277302054534 0.39377902901901013 0.4041207410447258 0.2561997963212561 0.0 0.0819993502684317 0.12652009525418703 9219:0.459459 453:0.031941 10322:0.27027 11947:0.0600601 6459:0.168521 46:0.0608108 8987:0.183784 4719:0.103604 10094:0.0945946 5162:0.0743243 2419:0.059744 11484:0.441441 12040:0.135135 8745:0.108108 9048:0.0440154 1989:0.036036 7102:0.0793919 3862:0.0577064 10417:0.0569106 5729:0.0972222 11796:0.0178571 9094:0.0310078 3452:0.0656566 3955:0.0248843 6228:0.333333 5299:0.031893 3505:0.0797101 10732:0.0921659 9659:0.0987654 8545:0.333333 6767:0.0555556 8566:0.375 3546:0.0853659 6309:0.0277778 10256:0.0448718 10288:0.388889
... ... 

上面,我们只是选择了非默认的核函数,还有其他参数可以选择,比如代价系数c,默认是1,表示在计算线性分类面时,可以容许一个点被分错。这时候,可以使用交叉验证来逐步优化计算,选择最合适的参数。
使用libsvm,指定交叉验证选项的时候,只输出经过交叉验证得到的分类器的精度,而不会输出模型文件,例如使用交叉验证模型运行时的参数示例如下:

-h 0 -t 0 -c 32 -v 5 C:\\Users\\thinkpad\\Desktop\\vector\\train-scale.txt C:\\Users\\thinkpad\\Desktop\\vector\\model.txt

用-v启用交叉验证模式,参数-v 5表示将每一类下面的数据分成5份,按顺序1对2,2对3,3对4,4对5,5对1分别进行验证,从而得出交叉验证的精度。例如,下面是我们的10个类别的交叉验证运行结果:

Cross Validation Accuracy = 71.10428571428571%

在选好各个参数以后,就可以使用最优的参数来计算输出模型文件。

使用libsvm验证文本分类器精度

前面已经训练出来分类模型,就是最后输出的模型文件。现在可以使用测试数据集了,通过使用测试数据集来做对基于文本分类模型文件预测分类精度进行验证。同样,需要做尺度变换,例如:

F:\libsvm-3.0\windows>svm-scale.exe -l 0 -u 1 C:\\Users\\thinkpad\\Desktop\\vector\\test.txt > C:\\Users\\thinkpad\\Desktop\\vector\\test-scale.txt

注意,这里必须和训练集使用相同的尺度变换参数值。
我还是使用Java版本的libsvm进行预测,验证分类器精度,svm_predict类的输入参数:

C:\\Users\\thinkpad\\Desktop\\vector\\test-scale.txt C:\\Users\\thinkpad\\Desktop\\vector\\model.txt C:\\Users\\thinkpad\\Desktop\\vector\\predict.txt

这样,预测结果就在predict.txt文件中,同时输出分类精度结果,例如:

Accuracy = 66.81% (6681/10000) (classification)

如果觉得分类器精度不够,可以使用交叉验证去获取更优的参数,来训练并输出模型文件,例如,下面是几组结果:

train -h 0 -t 0 C:\\Users\\thinkpad\\Desktop\\vector\\train-scale.txt C:\\Users\\thinkpad\\Desktop\\vector\\model.txt
Accuracy = 67.10000000000001% (6710/10000) (classification)

train -h 0 -t 0 -c 32 -v 5 C:\\Users\\thinkpad\\Desktop\\vector\\train-scale.txt C:\\Users\\thinkpad\\Desktop\\vector\\model.txt
Cross Validation Accuracy = 71.10428571428571%
Accuracy = 66.81% (6681/10000) (classification)

train -h 0 -t 0 -c 8 -m 1024 C:\\Users\\thinkpad\\Desktop\\vector\\train-scale.txt 
C:\\Users\\thinkpad\\Desktop\\vector\\model.txt
Cross Validation Accuracy = 74.3240057320121%
Accuracy = 67.88% (6788/10000) (classification)

第一组是默认情况下c=1时的精度为 67.10000000000001%;
第二组使用了交叉验证模型,交叉验证精度为71.10428571428571%,获得参数c=32,使用该参数训练输出模型文件,基于该模型文件进行预测,最终的精度为66.81%,可见没有使用默认c参数值的精度高;
第三组使用交叉验证,精度比第二组高一些,输出模型后并进行预测,最终精度为67.88%,略有提高。
可以基于这种方式不断地去寻找最优的参数,从而使分类器具有更好的精度。

总结

文本分类有其特殊性,在使用libsvm分类,或者其他的工具,都不要忘记,有如下几点需要考虑到:

  1. 其实文本在预处理过程进行的很多过程对最后使用工具进行分类都会有影响。
  2. 最重要的就是文本特征向量的选择方法,如果文本特征向量选择的很差,即使再好的分类工具,可能训练得到的分类器都未必能达到更好。
  3. 文本特征向量选择的不好,在训练调优分类器的过程中,虽然找到了更好的参数,但是这本身可能会是一个不好的分类器,在实际预测中可以出现误分类的情况。
  4. 选择训练集和测试集,对整个文本预处理过程,以及使用分类工具进行训练,都有影响。

相关资源

最后,附上文章中有关文本分类预处理的实现代码,仅供参考,链接为:

这个是最早的那个版本,默认分词使用了中科院ICTCLAS分词器,不过我当时使用的现在好像因为License的问题可能用不了了。

这个是我最近抽时间将原来document-processor代码进行重构,使用Maven进行构建,分为多个模块,并做了部分优化:

  1. 默认使用Lucene的SmartChineseAnalyzer进行分词,默认没有使用词典,分词效果可能没有那么理想。
  2. 增加了api层,对于某些可以自定义实现的,预留出了接口,并可以通过配置来注入自定义实现功能,主要是接口:FeatureTermSelector。
  3. 分词过程中,各个类别采用并行处理,处理时间大大减少:DocumentWordsCollector。
  4. 将train阶段和test阶段通用的配置提出来,放在配置文件config.properties中。
  5. 默认给予CHI卡方统计量方法选择特征向量,实现了各个类别之间并行处理的逻辑,可以查看实现类ChiFeatureTermSelector。

另外,使用的语料库文件,我放在百度网盘上了,可以下载:http://pan.baidu.com/s/1pJv6AIR

参考链接

Creative Commons License

本文基于署名-非商业性使用-相同方式共享 4.0许可协议发布,欢迎转载、使用、重新发布,但务必保留文章署名时延军(包含链接:http://shiyanjun.cn),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请与我联系

评论(256): “使用libsvm实现文本分类

  1. 赞~介绍的很详细!我刚开始学数据挖掘,对于特征选择那块看得还不是很明白,照着你的代码试了试发现有不少问题,不知道您可不可以给我邮箱发该程序的源码让我学学?谢谢啦~

  2. processor.dataset.chi.term.vector.file=C:\\Users\\Shirdrn\\Desktop\\vector\\terms.txt
    processor.dataset.file.extension=.txt
    processor.dataset.label.vector.file=C:\\Users\\Shirdrn\\Desktop\\vector\\labels.txt
    请问一下config_test.properties里面这三项是做什么的?

    • 第一个和第三个都是经过训练阶段以后,产出的结果。processor.dataset.label.vector.file是包含了类标签的文件(类标签名称+编号),你在测试阶段肯定要对应上;processor.dataset.chi.term.vector.file是包含经过训练节点选择的特征向量文件(词+词编号)。

  3. 膜拜下楼主,楼主的功力十分深厚啊,佩服!!
    想请问下楼主配置文件中的processor.document.filter.kept.lexical.categories=n,这个意思是特征向量只保留了名词?请问楼主这样做有什么依据呢?是否这样选择能提高分类精度?非常感谢。

    呵呵,还有个问题呢,楼主您最后将所有的特征项都合并放到一个map中,而Term中并没有label的属性,请问如何标记他们的类别的呢?

    谢谢楼主作出的卓有成效的工作,为我们指明了方向,再次感谢。

    • 1、n表示名词,根据数据的特点来选择,当然也可以不使用词性来过滤。
      2、这个其实就是选择能够标识文档类别的特征词,决定这个“特征”的也未必就是根据词性。
      3、选择名词作为特征向量,肯定比你选择介词、助词等更能表达语义,精度自然会好,因为名词相对来说更有区分度。
      4、举个简单的例子:假如有3个类别,C={体育,农业 , 军事},合并得到一个8维特征词向量为T(体育, 篮球, 跑步, 玉米, 花生, 大豆, 作物, 军舰),那么T(1, 3, 5, 0, 0, 1, 0, 0)会被认为属于体育这个类别,而T(0, 0, 0, 0, 1, 0, 0, 5)会被认为属于军事这个类别,T(0, 0, 0, 0, 4, 1, 7, 0)属于农业这个类别,讲这个例子你能明白了吧。

      • 非常感谢楼主的回复,您讲解的非常清楚,我了解楼主的意思了。

        只是对于第一个问题,呵呵,我只是有点好奇呢,名词确实包含的意义更多,只是奇怪楼主如何想到只保留名词,而将动词、形容词去掉,这样做是否提高了精度,或者说是只在某个领域的分类应用提高了精度呢?

        望楼主不吝赐教。

  4. 非常感谢。讲解得很详细,我是刚接触svm,从楼主的文章获益颇多。
    代码也跑通了,只是得到的输出文件还不是直接符合libsvm要求的,libsvm要求每条数据特征向量index按升序排列。
    再次感谢楼主的无私!

    • 您好!请教下,最后这个index排序的数据预处理怎么做的?能把你的脚本发我一份吗?邮箱:659324338@qq.com

        • 楼主您好,请问您这代码跑通了没,请问停用词去除了没有,停用词文件夹下是不是有多个停用词表文件,不然干嘛要用dir.fileList()方法呢?

  5. 还在运行这个程序,发现了一个问题,如果我测试集合里面类别下面只有一个文件,算出来的tf-idf值为-0.125,我看了一下代码,测试的时候idf值是重新计算的,标准情况下,idf的值是不是应该使用训练集得出的值?

  6. 由衷的感谢楼主的帮助,这是我第一个算是数据挖掘案例的实现吧,之前一直在找文本分类的资料,都很零碎,而这次通过楼主的源代码,一句句分析下来,觉得很清晰,除此之外,楼主的代码真的写的很漂亮。再次感谢楼主。
    此外,我发现楼主的代码里有两个问题,可能正是这两个导致了测试正确率低吧,一个是对CHI堆排序的算法,楼主没有写全。另一个是计算TF时,应该是除以该篇文档中所有词的总数,就是说是词*词频,而楼主可能疏忽少乘了词频。这样我测出来的正确率竟然有95%,发现提高好多哦。

      • 还有一个问题,希望楼主解答下,可能是我新手不懂,看到很多小白也在提,就是如果我现在想测试一篇未知分类的文本,用tf-idf进行向量化就会出现问题了,对于这个我们该怎么做呢?

        • 如果对单个文档进行分类,实际上就没有IDF的概念了,可以直接使用TF计算看看,直观上看应该可以,模型只不过是间接地给了一个加权而已。单个文档这种情况我还真没试过,你可以试试。
          或者,我们可以选择其他一种度量去计算呢。

    • 堆排序选出特征向量的逻辑,已经修正了,详细可以查看代码文件:https://github.com/shirdrn/document-processor/blob/master/src/main/java/org/shirdrn/document/processor/utils/SortUtils.java

    • 您好!请教一下,你是怎么做到95%的准确率的,能把您的源码发我一份吗?刚接触文本分类,还望大神指教!

    • 我按照博文中的方法获得的特测征向量只有2000+维(训练样本8000*10),预测准确度也很低,只有35%,不知道是哪里有问题导致这么大的差异?

      • 可以看看文章后面,我总结的那几点,根据你实际的情况进行相关参数的调整可能也会改善分类精度。

        • 我基本上是按照博文中描述进行的,包括语料库等,所以预想会有接近的结果。我后来将词性进行了一点扩展(加入了v,vt等)后,特征向量维数增加到了近3000,不过实际预测的准确度还是很低。

          想问博主,你当时的测试向量的维数是多少?猜想维数太小可能是一个原因。

    • 请问,你说的第二个问题,关于计算TF的问题是什么意思?“另一个是计算TF时,应该是除以该篇文档中所有词的总数,就是说是词*词频,而楼主可能疏忽少乘了词频。这样我测出来的正确率竟然有95%,发现提高好多哦”

    • 您好,问一下95%的正确率怎么得到的?
      还有,为什么正确率一直在60%几呢?楼主的测试也都是60%几,原因是什么啊

  7. 您好,我是小白,刚接触文本分类,对JAVA也不是很熟悉,能不能提供源码给我下载,或者发到我邮箱?不胜感谢!

  8. 楼主,你的计算CHI值有问题,“N*temp*temp / (A+C)*(A+B)*(B+D)*(C+D)” 其中整个分母(A+C)*(A+B)*(B+D)*(C+D)外应该加个括号,要不然计算顺序会出错,就成了乘乘除乘乘乘,你看是不是?

      • 我发现如果数据量太大的时候,按照楼主的代码计算出的CHI=N*temp*temp / (A+C)*(A+B)*(B+D)*(C+D)值有负值,应该是超出了double的显示范围,用BigDecimal也没得到很好的解决,请问楼主有没有比较好的办法?

        • BigDecimal肯定可以解决的吧,你试试下面的计算逻辑:
          BigDecimal bd = new BigDecimal(“4318747498174987189479874983127978.82777193″);
          System.out.println(bd.multiply(bd));
          System.out.println(bd.divide(new BigDecimal(“88.431424317″), 10, RoundingMode.HALF_UP));

  9. 运行TrainDocumentProcessorDriver出错:
    Exception in thread “main” java.lang.RuntimeException: java.lang.reflect.InvocationTargetException
    at org.shirdrn.document.processor.utils.ReflectionUtils.getInstance(ReflectionUtils.java:43)
    at org.shirdrn.document.processor.utils.ReflectionUtils.getInstance(ReflectionUtils.java:30)
    at org.shirdrn.document.processor.component.DocumentWordsCollector.(DocumentWordsCollector.java:30)
    at org.shirdrn.document.processor.TrainDocumentProcessorDriver.process(TrainDocumentProcessorDriver.java:42)
    at org.shirdrn.document.processor.AbstractDocumentProcessorDriver.start(AbstractDocumentProcessorDriver.java:24)
    at org.shirdrn.document.processor.TrainDocumentProcessorDriver.main(TrainDocumentProcessorDriver.java:51)
    Caused by: java.lang.reflect.InvocationTargetException
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(Unknown Source)
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(Unknown Source)
    at java.lang.reflect.Constructor.newInstance(Unknown Source)
    at org.shirdrn.document.processor.utils.ReflectionUtils.construct(ReflectionUtils.java:76)
    at org.shirdrn.document.processor.utils.ReflectionUtils.getInstance(ReflectionUtils.java:41)
    … 5 more
    Caused by: java.lang.RuntimeException:
    at org.shirdrn.document.processor.analyzer.IctclasAnalyzer.(IctclasAnalyzer.java:33)
    … 11 more
    Caused by: java.lang.RuntimeException: Fail to initialize!
    at org.shirdrn.document.processor.analyzer.IctclasAnalyzer.(IctclasAnalyzer.java:30)
    … 11 more
    不明白这个该怎么解决,麻烦楼主赐教!!

    • 问题已经解决,是因为NLPIR初始化失败造成的,用ictcals2014下面的Data文件夹把ictcals2013的Data替换掉,就好了

    • Exception in thread “main” java.lang.RuntimeException: java.lang.reflect.InvocationTargetException
      at org.shirdrn.document.processor.utils.ReflectionUtils.getInstance(ReflectionUtils.java:43)
      at org.shirdrn.document.processor.utils.ReflectionUtils.getInstance(ReflectionUtils.java:30)
      at org.shirdrn.document.processor.component.DocumentWordsCollector.(DocumentWordsCollector.java:30)
      at org.shirdrn.document.processor.TrainDocumentProcessorDriver.process(TrainDocumentProcessorDriver.java:42)
      at org.shirdrn.document.processor.AbstractDocumentProcessorDriver.start(AbstractDocumentProcessorDriver.java:24)
      at org.shirdrn.document.processor.TrainDocumentProcessorDriver.main(TrainDocumentProcessorDriver.java:51)
      Caused by: java.lang.reflect.InvocationTargetException
      at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
      at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:39)
      at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:27)
      at java.lang.reflect.Constructor.newInstance(Constructor.java:513)
      at org.shirdrn.document.processor.utils.ReflectionUtils.construct(ReflectionUtils.java:76)
      at org.shirdrn.document.processor.utils.ReflectionUtils.getInstance(ReflectionUtils.java:41)
      … 5 more
      Caused by: java.lang.UnsatisfiedLinkError: kevin.zhang.NLPIR.NLPIR_Init([BI)Z
      at kevin.zhang.NLPIR.NLPIR_Init(Native Method)
      at org.shirdrn.document.processor.analyzer.IctclasAnalyzer.(IctclasAnalyzer.java:28)
      … 11 more
      我的也是这个问题,按着你说的方法试了,可还是不行。能帮帮忙吗?谢了

  10. 选择文本训练数据集和测试数据集:训练集和测试集都是类标签已知的;对于这里我有一个疑问,请问博主,你的测试集类别都是已知的,你通过训练集训练出来的模型,对于类别已知的数据分类还有什么意义?

    • 这你就应该理解,为什么分类要使用测试集了。测试集是用来验证分类器精度的,倘若你不知道测试集的类标签,那你怎么评估你的分类器精度呢?当你认为精度达到要求了,才可以用它处理未知的数据,这些未知的数据就相当于未知类标签的测试集数据。

  11. 楼主你好,有一个地方有疑问,测试数据的特征向量文件是如何得到的,需要加载训练过程中所生成的特征向量,然后得到测试数据的所属类别,特征项及其权重,生成test.txt文件以供测试使用,我想问的是:在得到测试文件中某一个文档的向量类标签时楼主是如何得到的呢? 我看楼主的意思是从AbstractOutputtingQuantizedData类的Integer labelId = getLabelId(label);这一行来得到类别,查看其具体方法实现可知最终是由VectorMetadata类的getlabelId()方法来实现它返回globalLabelToIdMap.get(label)这个值可以得到类别labelId,可是在这个VectorMetadata类中只定义了globalLabelToIdMap这个集合,并没有在他里边加入元素,所以获得的labelId是空值,我想问楼主您是不是后来又加了往globalLabelToIdMap这个集合中添加元素的方法实现呢,具体怎么实现的呢?或者楼主是从哪里得到的这个值呢?因为怎么加载训练向量来得到测试文件的相应类别标签的逻辑我不太明白,所以不知道此处应该怎么实现这个方法,请楼主给予指点

    • 测试阶段文档的类标签,包含在测试文件路径中,和训练阶段的类似,测试文件所在的上级目录名称就是类标签,例如F:\SogouC-UTF8\UTF8\test\ClassFile\C000007。

  12. 您好博主我就是做文本分类的 我想问下你是否有爬虫代码。有的话能否发我一份供我研究

  13. 楼主,我看了您的源码,但在我的MacBook上运行不了ictclas这个分词系统,我改用了ansj,但是那个chi值计算我看不懂楼主的逻辑,楼主能否留个QQ交流下,项目还有一个月,时间非常赶,期待楼主回复。这方面我也是刚刚接触不久。

    • 你就根据那个公式N*(A*D-B*C)*(A*D-B*C)/((A+C)*(A+B)*(B+D)*(C+D)),把对应的量计算出来,套公式计算即可,代码实现在这个类里面:org/shirdrn/document/processor/component/train/FeatureTermVectorSelector.java

  14. 请问楼主,import org.shirdrn.document.processor.utils.SortUtils.Result;出现错误,是不是少了一个类哇??

  15. 请问楼主,Configuration类中的private static final String DEFAULT_CONFIG = “config.properties”;
    config.properties 文件在哪?作用是什么?你的源码中没有呢?

  16. 请问楼主,VectorMetadata类中的方法
    public void addChiTerm(String label, String word, Term term) {
    Map words = chiLabelToWordsVectorsMap.get(label);
    if(words == null) {
    words = new HashMap(1);
    chiLabelToWordsVectorsMap.put(label, words);
    }
    words.put(word, term);
    }
    其中,words.put(word, term);这一语句的作用是什么呢?局部变量方法调用完,不就失效了吗?
    自己JAVA语言不太熟悉,还望楼主抽空恢复下,万分感谢!!

  17. 楼主,您好。刚接触这些东西有很多不懂的,调程序的时候出现2015-04-09 22:45:47.086 [main] INFO processor.config.Configuration – Load properties file: prop=config.properties是什么原因?还有你的语料库是哪个?方便加QQ吗?

  18. 请问楼主,如果用TFIDF计算特征权重,需要被测试或分类的文档有多个,如果是单独来对一个文档分类,可以考虑什么度量方法计算特征权重?

  19. Map<类别, Map<类别, Map<文档 ,Map>>>
    这个结构从表面来看是一个类别只能对应一个key,也就是Map<文档 ,Map>>,但是事实是每个类别都会有很多文档,这究竟是如何实现统计的?

  20. 楼主好,请问:train 出来的term.txt label.txt 有什么用处? 他和tain.txt是什么关系?
    又怎么和测试阶段对应起来呢?
    非常感谢

    • term.txt是特征向量文件,包括对应的数字化编号,label.txt是类标签,包括第一营的类标签数字化编号,这个主要是根据libsvm训练阶段输入的数据格式对应的。如果还不明白,你可以了解一下libsvm在训练的时候,需要什么样的输入数据格式,就知道是怎么样的对应关系了。

  21. 博主我搭好工程了,有个实例化的错误,也定位到具体出错位置是在ReflectionUtils.java文件,但是不知道怎么解决,楼主能帮帮忙吗?这边很着急啊~谢谢了!附上具体出错的函数位置和异常名称。
    @SuppressWarnings(“unchecked”)
    private static T construct(Class clazz, Class baseClass, Object[] parameters)throws InstantiationException, IllegalAccessException,InvocationTargetException
    {
    T instance = null;
    Constructor[] constructors = (Constructor[])clazz.getConstructors();
    for (Constructor c : constructors) {
    if (c.getParameterTypes().length == parameters.length) {
    instance = c.newInstance(parameters); //java.lang.RuntimeException: java.lang.reflect.InvocationTargetException
    break;
    }
    }
    return instance;
    }

    • 估计是中科院那个ICTCLAS初始化的问题(License失效),要不你就实现一个自己的,然后不用Java反射,直接在代码中创建一个实例看看。

  22. 楼主,我想问一下,你的语料库是哪个文件,其他的文件在配置文件中都有哪些作用,需要我添加那些文件

      • 请问楼主,这些语料资源是需要我们自己添加的吗?还是不需要添加的?然后,我运行了TrainDocumentProcessorDriver.java 文件显示如下错误,不知道是什么原因,已经将Data的文件替换为2014年的了,谢谢楼主了。
        Exception in thread “main” java.lang.NullPointerException
        at org.shirdrn.document.processor.component.BasicInformationCollector.fire(BasicInformationCollector.java:23)
        at org.shirdrn.document.processor.AbstractDocumentProcessorDriver.run(AbstractDocumentProcessorDriver.java:18)
        at org.shirdrn.document.processor.TrainDocumentProcessorDriver.process(TrainDocumentProcessorDriver.java:47)
        at org.shirdrn.document.processor.AbstractDocumentProcessorDriver.start(AbstractDocumentProcessorDriver.java:24)
        at org.shirdrn.document.processor.TrainDocumentProcessorDriver.main(TrainDocumentProcessorDriver.java:51)

  23. 请问楼主,为什么测试的时候,得到的两类文档集的词汇个数总是一样多?
    Sort CHI terms for: label=com, termCount=28956
    Sort CHI terms for: label=adv, termCount=28956
    并且
    for (int i = result.getStartIndex(); i <= result.getEndIndex(); i++) {
    Entry termEntry = result.get(i);
    // merge CHI terms for all labels
    context.getVectorMetadata().addChiMergedTerm(termEntry.getKey(),
    termEntry.getValue());
    }
    此处的开始索引和结束索引也都一样,一般不会这样吧??

    • 是每一类文档集的词汇集合根据卡方排序后得到的开始索引和结束索引都一样

  24. Pingback: 文本挖掘初步 – 对自由文本进行信息抽取 | JetMufffin - 梦想追逐者

  25. 请教一下楼主,我的理解是否正确?

    卡方统计量(CHI) 是为了从每一个类别,所有分词中挑选出来topN。然后合并所有类别的的topN分词,作为最终的词袋模型。

    然后对词袋模型的每一个分词进行svm训练(TF-IDF值)。

    svm阶段(训练-分类)是不需要CHI值的。

  26. 为什么我归一化的特征值全变成1和-1,而且是1很多很多,我是不是搞错了,求解答

  27. 数据格式变成了这样
    1:1 2:1 3:1 4:1 5:1 6:1 7:1 8:1 9:1 10:1 11:1 12:1 13:1 14:1 15:1 16:1 17:1 18:1 19:1 20:1 21:1 22:1 23:1 24:1 25:1 26:1 27:1 28:1 29:1 30:1 31:1 32:1 33:1 34:1 35:1 36:1 37:1 38:1 39:1 40:1 41:1 42:1 43:1 44:1 45:1 46:1 47:1 48:1 49:1 50:1 51:1 52:1 53:1 54:1 55:1 56:1 57:1 58:1 59:1 60:1 61:1 62:1 63:1 64:1 65:1 66:1 67:1 68:1 69:1 70:1 71:1 72:1 73:1 74:1 75:1 76:1 77:1 78:1 79:1 80:1 81:1 82:1 83:1 84:1 85:1 86:1 87:1 88:1 89:1 90:1 91:1 92:1 93:1 94:1 95:1 96:1 97:1 98:1 99:1 100:1 101:1 102:1 103:1 104:1 105:1 106:1 107:1 108:1 109:1 110:1 111:1 112:1 113:1 114:1 115:1 116:1 117:1 118:1 119:1 120:1 121:1 122:1 123:1 124:1 125:1 126:1 127:1 128:1 129:1 130:1 131:1 132:1 133:1 134:1 135:1 136:1 137:1 138:1 139:1 140:1 141:1 142:1 143:1 144:1 145:1 146:1 147:1 148:1 149:1 150:1 151:1 152:1 153:1 154:1 155:1 156:1 157:1 158:1 159:1 160:1 161:1 162:1 163:1 164:1 165:1 166:1 167:1 168:1 169:1 170:1 171:1 172:1 173:1 174:1 175:1 176:1 177:1 178:1 179:1 180:1 181:1 182:1 183:1 184:1 185:1 186:1 187:1 188:1 189:1 190:1 191:1 192:1 193:1 194:1 195:1 196:1 197:1 198:1 199:1 200:1 201:1 202:1 203:1 204:1 205:1 206:1 207:1 208:1 209:1 210:1 211:1 212:1 213:1 214:1 215:1 216:1 217:1 218:1 219:1 220:1 221:1 222:1 223:1 224:1 225:1 226:1 227:1 228:1 229:1 230:1 231:1 232:1 233:1 234:1 235:1 236:1 237:1 238:1 239:1 240:1 241:1 242:1 243:1 244:1 245:1 246:1 247:1 248:1 249:1 250:1 251:1 252:1 253:1 254:1 255:1 256:1 257:1 258:1 259:1 260:1 261:1 262:1 263:1 264:1 265:1 266:1 267:1 268:1 269:1 270:1 271:1 272:1 273:1 274:1 275:1 276:1 277:1 278:1 279:1 280:1 281:1 282:1 283:1 284:1 285:1 286:1 287:1 288:1 289:1 290:1 291:1 292:1 293:1 294:1 295:1 296:1 297:1 298:1 299:1 300:1 301:1 302:1 303:1 304:1 305:1 306:1 307:1 308:1 309:1 310:1 311:1 312:1 313:1 314:1 315:1 316:1 317:1 318:1 319:1 320:1 321:1 322:1 323:1 324:1 325:1 326:1 327:1 328:1 329:1 330:1 331:1 332:1 333:1 334:1 335:1 336:1 337:1 338:1 339:1 340:1 341:1 342:1 343:1 344:1 345:1 346:1 347:1 348:1 349:1 350:1 351:1 352:1 353:1 354:1 355:1 356:1 357:1 358:1 359:1 360:1 361:1 362:1 363:1 364:1 365:1 366:1 367:1 368:1 369:1 370:1 371:1 372:1 373:1 374:1 375:1 376:1 377:1 378:1 379:1 380:1 381:1 382:1 383:1 384:1 385:1 386:1 387:1 388:1 389:1 390:1 391:1 392:1 393:1 394:1 395:1 396:1 397:1 398:1 399:1 400:1 401:1 402:1 403:1 404:1 405:1 406:1 407:1 408:1 409:1 410:1 411:1 412:1 413:1 414:1 415:1 416:1 417:1 418:1 419:1 420:1 421:1 422:1 423:1 424:1 425:1 426:1 427:1 428:1 429:1 430:1 431:1 432:1 433:1 434:1 435:1 436:1 437:1 438:1 439:1 440:1 441:1 442:1 443:1 444:1 445:1 446:1 447:1 448:1 449:1 450:1 451:1 452:1 453:1 454:1 455:1 456:1 457:1 458:1 459:1 460:1 461:1 462:1 463:1 464:1 465:1 466:1 467:1 468:1 469:1 470:1 471:1 472:1 473:1 474:1 475:1 476:1 477:1 478:1 479:1 480:1 481:1 482:1 483:1 484:1 485:1 486:1 487:1 488:1 489:1 490:1 491:1 492:1 493:1 494:1 495:1 496:1 497:1 498:1 499:1 500:1 501:1 502:1 503:1 504:1 505:1 506:1 507:1 508:1 509:1 510:1 511:1 512:1 513:1 514:1 515:1 516:1 517:1 518:1 519:1 520:1 521:1 522:1 523:1 524:1 525:1 526:1 527:1 528:1 529:1 530:1 531:1 532:1 533:1 534:1 535:1 536:1 537:1 538:1 539:1 540:1 541:1 542:1 543:1 544:1 545:1 546:1 547:1 548:1 549:1 550:1 551:1 552:1 553:1 554:1 555:1 556:1 557:1 558:1 559:1 560:1 561:1 562:1 563:1 564:1 565:1 566:1 567:1 568:1 569:1 570:1 571:1 572:1 573:1 574:1 575:1 576:1 577:1 578:1 579:1 580:1 581:1 582:1 583:1 584:1 585:1 586:1 587:1 588:1 589:1 590:1 591:1 592:1 593:1 594:1 595:1 596:1 597:1 598:1 599:1 600:1 601:1 602:1 603:1 604:1 605:1 606:1 607:1 608:1 609:1 610:1 611:1 612:1 613:1 614:1 615:1 616:1 617:1 618:1 619:1 620:1 621:1 622:1 623:1 624:1 625:1 626:1 627:1 628:1 629:1 630:1 631:1 632:1 633:1 634:1 635:1 636:1 637:1 638:1 639:1 640:1 641:1 642:1 643:1 644:1 645:1 646:1 647:1 648:1 649:1 650:1 651:1 652:1 653:1 654:1 655:1 656:1 657:1 658:1 659:1 660:1 661:1 662:1 663:1 664:1 665:1 666:1 667:1 668:1 669:1 670:1 671:1 672:1 673:1 674:1 675:1 676:1 677:1 678:1 679:1 680:1 681:1 682:1 683:1 684:1 685:1 686:1 687:1 688:1 689:1 690:1 691:1 692:1 693:1 694:1 695:1 696:1 697:1 698:1 699:1 700:1 701:1 702:1 703:1 704:1 705:1 706:1 707:1 708:1 709:1 710:1 711:1 712:1 713:1 714:1 715:1 716:1 717:1 718:1 719:1 720:1 721:1 722:1 723:1 724:1 725:1 726:1 727:1 728:1 729:1 730:1 731:1 732:1 733:1 734:1 735:1 736:1 737:1 738:1 739:1 740:1 741:1 742:1 743:1 744:1 745:1 746:1 747:1 748:1 749:1 750:1 751:1 752:1 753:1 754:1 755:1 756:1 757:1 758:1 759:1 760:1 761:1 762:1 763:1 764:1 765:1 766:1 767:1 768:1 769:1 770:1 771:1 772:1 773:1 774:1

  28. 博主您好!我在运行您的代码时也遇到这样的问题:“得到的输出文件还不是直接符合libsvm要求的,libsvm要求每条数据特征向量index按升序排列“,比如:
    7 399:1.3564509446378685 88:1.7009310552655554
    7
    7 108:1.2702909150035386 39:0.7771082550830171 332:1.1277802615663919 56:0.6442579729778579 191:0.9722587458336817
    7 6:2.1920460811942317 56:0.8053224662223224 88:0.8504655276327777 411:2.1920460811942317
    而且有的会好像上面第2行那样只有标签号。

    希望博主能抽空解答,谢谢!

      • 谢谢博主回答!我使用linsvm-3.20,可能版本真的是版本问题。但是我理解的是,为什么得到的train文件是这样的(二分类):
        1
        1 31:8.380821783940931
        1
        1
        1
        1 20:9.965784284662087
        1
        1
        1
        1
        1
        1
        1
        1
        1
        1 46:8.643856189774725
        1
        1 17:7.265344566520995
        中间很多语句都没有提取出特征向量,而且我不明白为什么得到的trem文件里面的词并不是我训练文本中的:
        液态 1
        毒品 2
        石头 3
        委员 4
        趾头 5
        拷纱 6
        雌黄 7
        水源 8
        妇女 9
        志士 10
        水漂 11
        水平 12
        水牛 13
        全胜 14
        同志 15
        实证 16
        食品 17
        传说 18
        模式 19
        猕猴 20
        请问博主该如何解决这两个问题?

  29. 请教博主一个问题,我在二元分类的时候训练出来的模型对负样本识别准确率90%+,但对正样本识别准确率只有20%几,请问这是什么原因?因为是选择的特征空间不均匀吗?

      • 楼主,能帮我答疑一下吗,同一个数据集,用贝叶斯分类器能有80%的正确率,用这个SVM就只有60几,是什么原因呢?是特征选择的问题吗

        • 影响分类准确率的因素很多,我在5折交叉验证时准确率78%。但另外找测试集效果差一些,目前找到得原因是因为我计算tf的方法和博主的不一样。

      • 博主,您在计算tf的时候貌似疏忽了,计算词频应该除以文档词的总数,但是您把词放入一个map中就会把重复的词的个数算少了,这样计算出来的词频和会大于1.可以在getTermCount那里修改一下。

  30. 楼主,这个地方是不是错了
    FeatureTermVectorSelector类下:
    计算B时:docCountContainingWordNotInLabel += entry.getValue().size();
    应该是:docCountContainingWordNotInLabel += labelledEntry.getValue().size();

    请确认,多谢!

    • 我看了下,代码中计算是不对,多谢指正。
      抽时间,我把代码更正一下,文章也更新一下。
      大家有问题可以在评论这留言,我收集大家的意见,修正代码、改正文章,或者考虑对libsvm不同版本,代码也提供相应的版本。

  31. 博主,还有一个问题请教,对计算出来的CHI值堆排序的时候,result.setStartIndex(array.length – i); 这个StartIndex为什么只有一个,如果分析完文档后总得词少于3000个,这时候这个StartIndex是不是应该等于0

    麻烦,博主

  32. 这种归一化在tf*idf的情况下不对吧,按照0-1归一化不就是变成去掉idf了吗,其实只有tf了,比如一个常见词idf很低,一个不常见词idf很高,但是如果一个文档中出现了这两个词,并且出现的频次都是单文档出现的最高频次,那么这两个词对应的特征值都是1,没有idf的区分度体现了吧。
    我理解应该是对单个特征向量进行l2归一化。

    • 有没有区分度是在选特征的时候进行的工作,这里归一化只是让量纲统一吧