使用libsvm實現文本分類


@Hcy(黃燦奕)

文本分類,首先它是分類問題,應該對應着分類過程的兩個重要的步驟,一個是使用訓練數據集訓練分類器,另一個就是使用測試數據集來評價分類器的分類精度。然而,作為文本分類,它還具有文本這樣的約束,所以對於文本來說,需要額外的處理過程,我們結合使用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實現分詞,定義分詞器接口,以便切換其他分詞器實現時,容易擴展,如下所示:

1 package org.shirdrn.document.processor.common;
2  
3 import java.io.File;
4 import java.util.Map;
5  
6 public interface DocumentAnalyzer {
7      Map<String, Term> analyze(File file);
8 }

增加一個外部的停用詞表,這個我們直接封裝到抽象類AbstractDocumentAnalyzer中去了,該抽象類就是從一個指定的文件或目錄讀取停用詞文件,將停用詞加載到內存中,在分詞的過程中對詞進行進一步的過濾。然后基於上面的實現,給出包裹ICTCLAS分詞器的實現,代碼如下所示:

01 package org.shirdrn.document.processor.analyzer;
02  
03 import java.io.BufferedReader;
04 import java.io.File;
05 import java.io.FileInputStream;
06 import java.io.IOException;
07 import java.io.InputStreamReader;
08 import java.util.HashMap;
09 import java.util.Map;
10  
11 import kevin.zhang.NLPIR;
12  
13 import org.apache.commons.logging.Log;
14 import org.apache.commons.logging.LogFactory;
15 import org.shirdrn.document.processor.common.DocumentAnalyzer;
16 import org.shirdrn.document.processor.common.Term;
17 import org.shirdrn.document.processor.config.Configuration;
18  
19 public class IctclasAnalyzer extends AbstractDocumentAnalyzer implements DocumentAnalyzer {
20  
21      private static final Log LOG = LogFactory.getLog(IctclasAnalyzer.class);
22      private final NLPIR analyzer;
23      
24      public IctclasAnalyzer(Configuration configuration) {
25           super(configuration);
26           analyzer = new NLPIR();
27           try {
28                boolean initialized = NLPIR.NLPIR_Init(".".getBytes(charSet), 1);
29                if(!initialized) {
30                     throw new RuntimeException("Fail to initialize!");
31                }
32           catch (Exception e) {
33                throw new RuntimeException("", e);
34           }
35      }
36  
37      @Override
38      public Map<String, Term> analyze(File file) {
39           String doc = file.getAbsolutePath();
40           LOG.info("Process document: file=" + doc);
41           Map<String, Term> terms = new HashMap<String, Term>(0);
42           BufferedReader br = null;
43           try {
44                br = new BufferedReader(new InputStreamReader(new FileInputStream(file), charSet));
45                String line = null;
46                while((line = br.readLine()) != null) {
47                     line = line.trim();
48                     if(!line.isEmpty()) {
49                          byte nativeBytes[] = analyzer.NLPIR_ParagraphProcess(line.getBytes(charSet), 1);
50                          String content = new String(nativeBytes, 0, nativeBytes.length, charSet);
51                          String[] rawWords = content.split("\\s+");
52                          for(String rawWord : rawWords) {
53                               String[] words = rawWord.split("/");
54                               if(words.length == 2) {
55                                    String word = words[0];
56                                    String lexicalCategory = words[1];
57                                    Term term = terms.get(word);
58                                    if(term == null) {
59                                         term = new Term(word);
60                                         // TODO set lexical category
61                                         term.setLexicalCategory(lexicalCategory);
62                                         terms.put(word, term);
63                                    }
64                                    term.incrFreq();
65                                    LOG.debug("Got word: word=" + rawWord);
66                               }
67                          }
68                     }
69                }
70           catch (IOException e) {
71                e.printStackTrace();
72           finally {
73                try {
74                     if(br != null) {
75                          br.close();
76                     }
77                catch (IOException e) {
78                     LOG.warn(e);
79                }
80           }
81           return terms;
82      }
83  
84 }

它是對一個文件進行讀取,然后進行分詞,去停用詞,最后返回的Map包含了的集合,此屬性包括詞性(Lexical Category)、詞頻、TF等信息。
這樣,遍歷數據集目錄和文件,就能去將全部的文檔分詞,最終構建詞袋模型。我們使用Java中集合來存儲文檔、詞、類別之間的關系,如下所示:

01 private int totalDocCount;
02 private final List<String> labels = new ArrayList<String>();
03 // Map<類別, 文檔數量>
04 private final Map<String, Integer> labelledTotalDocCountMap = new HashMap<String, Integer>();
05 //  Map<類別, Map<文檔 ,Map<詞, 詞信息>>>
06 private final Map<String, Map<String, Map<String, Term>>> termTable =
07           new HashMap<String, Map<String, Map<String, Term>>>();
08 //  Map<詞 ,Map<類別, Set<文檔>>>
09 private final Map<String, Map<String, Set<String>>> invertedTable =
10           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的集合來存儲:

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

下面,實現特征向量選擇計算的實現,代碼如下所示:

001 package org.shirdrn.document.processor.component.train;
002  
003 import java.util.Iterator;
004 import java.util.Map;
005 import java.util.Map.Entry;
006 import java.util.Set;
007  
008 import org.apache.commons.logging.Log;
009 import org.apache.commons.logging.LogFactory;
010 import org.shirdrn.document.processor.common.AbstractComponent;
011 import org.shirdrn.document.processor.common.Context;
012 import org.shirdrn.document.processor.common.Term;
013 import org.shirdrn.document.processor.utils.SortUtils;
014  
015 public class FeatureTermVectorSelector extends AbstractComponent {
016  
017      private static final Log LOG = LogFactory.getLog(FeatureTermVectorSelector.class);
018      private final int keptTermCountEachLabel;
019       
020      public FeatureTermVectorSelector(Context context) {
021           super(context);
022           keptTermCountEachLabel = context.getConfiguration().getInt("processor.each.label.kept.term.count"3000);
023      }
024  
025      @Override
026      public void fire() {
027           // compute CHI value for selecting feature terms
028           // after sorting by CHI value
029           for(String label : context.getVectorMetadata().getLabels()) {
030                // for each label, compute CHI vector
031                LOG.info("Compute CHI for: label=" + label);
032                processOneLabel(label);
033           }
034            
035           // sort and select CHI vectors
036           Iterator<Entry<String, Map<String, Term>>> chiIter =
037                     context.getVectorMetadata().chiLabelToWordsVectorsIterator();
038           while(chiIter.hasNext()) {
039                Entry<String, Map<String, Term>> entry = chiIter.next();
040                String label = entry.getKey();
041                LOG.info("Sort CHI terms for: label=" + label + ", termCount=" + entry.getValue().size());
042                Entry<String, Term>[] a = sort(entry.getValue());
043                for (int i = 0; i < Math.min(a.length, keptTermCountEachLabel); i++) {
044                     Entry<String, Term> termEntry = a[i];
045                     // merge CHI terms for all labels
046                     context.getVectorMetadata().addChiMergedTerm(termEntry.getKey(), termEntry.getValue());
047                }
048           }
049      }
050       
051      @SuppressWarnings("unchecked")
052      private Entry<String, Term>[] sort(Map<String, Term> terms) {
053           Entry<String, Term>[] a = new Entry[terms.size()];
054           a = terms.entrySet().toArray(a);
055           SortUtils.heapSort(a, true, keptTermCountEachLabel);
056           return a;
057      }
058  
059      private void processOneLabel(String label) {
060           Iterator<Entry<String, Map<String, Set<String>>>> iter =
061                     context.getVectorMetadata().invertedTableIterator();
062           while(iter.hasNext()) {
063                Entry<String, Map<String, Set<String>>> entry = iter.next();
064                String word = entry.getKey();
065                Map<String, Set<String>> labelledDocs = entry.getValue();
066                 
067                // A: doc count containing the word in this label
068                int docCountContainingWordInLabel = 0;
069                if(labelledDocs.get(label) != null) {
070                     docCountContainingWordInLabel = labelledDocs.get(label).size();
071                }
072                 
073                // B: doc count containing the word not in this label
074                int docCountContainingWordNotInLabel = 0;
075                Iterator<Entry<String, Set<String>>> labelledIter =
076                          labelledDocs.entrySet().iterator();
077                while(labelledIter.hasNext()) {
078                     Entry<String, Set<String>> labelledEntry = labelledIter.next();
079                     String tmpLabel = labelledEntry.getKey();
080                     if(!label.equals(tmpLabel)) {
081                          docCountContainingWordNotInLabel += entry.getValue().size();
082                     }
083                }
084                 
085                // C: doc count not containing the word in this label
086                int docCountNotContainingWordInLabel =
087                          getDocCountNotContainingWordInLabel(word, label);
088                 
089                // D: doc count not containing the word not in this label
090                int docCountNotContainingWordNotInLabel =
091                          getDocCountNotContainingWordNotInLabel(word, label);
092                 
093                // compute CHI value
094                int N = context.getVectorMetadata().getTotalDocCount();
095                int A = docCountContainingWordInLabel;
096                int B = docCountContainingWordNotInLabel;
097                int C = docCountNotContainingWordInLabel;
098                int D = docCountNotContainingWordNotInLabel;
099                int temp = (A*D-B*C);
100                double chi = (double) N*temp*temp / (A+C)*(A+B)*(B+D)*(C+D);
101                Term term = new Term(word);
102                term.setChi(chi);
103                context.getVectorMetadata().addChiTerm(label, word, term);
104           }
105      }
106  
107      private int getDocCountNotContainingWordInLabel(String word, String label) {
108           int count = 0;
109           Iterator<Entry<String,Map<String,Map<String,Term>>>> iter =
110                     context.getVectorMetadata().termTableIterator();
111           while(iter.hasNext()) {
112                Entry<String,Map<String,Map<String,Term>>> entry = iter.next();
113                String tmpLabel = entry.getKey();
114                // in this label
115                if(tmpLabel.equals(label)) {
116                     Map<String, Map<String, Term>> labelledDocs = entry.getValue();
117                     for(Entry<String, Map<String, Term>> docEntry : labelledDocs.entrySet()) {
118                          // not containing this word
119                          if(!docEntry.getValue().containsKey(word)) {
120                               ++count;
121                          }
122                     }
123                     break;
124                }
125           }
126           return count;
127      }
128       
129      private int getDocCountNotContainingWordNotInLabel(String word, String label) {
130           int count = 0;
131           Iterator<Entry<String,Map<String,Map<String,Term>>>> iter =
132                     context.getVectorMetadata().termTableIterator();
133           while(iter.hasNext()) {
134                Entry<String,Map<String,Map<String,Term>>> entry = iter.next();
135                String tmpLabel = entry.getKey();
136                // not in this label
137                if(!tmpLabel.equals(label)) {
138                     Map<String, Map<String, Term>> labelledDocs = entry.getValue();
139                     for(Entry<String, Map<String, Term>> docEntry : labelledDocs.entrySet()) {
140                          // not containing this word
141                          if(!docEntry.getValue().containsKey(word)) {
142                               ++count;
143                          }
144                     }
145                }
146           }
147           return count;
148      }
149  
150 }

輸出量化數據文件

特征向量已經從訓練數據集中計算出來,接下來需要對每個詞給出一個唯一的編號,從1開始,這個比較容易,輸出特征向量文件,為測試驗證的數據集所使用,文件格式如下所示:

01 認識     1
02 代理權     2
03 病理     3
04 死者     4
05 影子     5
06 生產國     6
07 容量     7
08 螺絲扣     8
09 大錢     9
10 壯志     10
11 生態圈     11
12 好事     12
13 全人類     13

由於libsvm使用的訓練數據格式都是數字類型的,所以需要對訓練集中的文檔進行量化處理,我們使用TF-IDF度量,表示詞與文檔的相關性指標。
然后,需要遍歷已經構建好的詞袋模型,並使用已經編號的類別和特征向量,對每個文檔計算TF-IDF值,每個文檔對應一條記錄,取出其中一條記錄,輸出格式如下所示:

1 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,可以根據實際需要選擇。我們看一下命令:

1 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代碼,輸入參數如下所示:

1 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,內容示例如下所示:

01 svm_type c_svc
02 kernel_type linear
03 nr_class 10
04 total_sv 54855
05 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
06 label 8 4 7 5 10 9 3 2 6 1
07 nr_sv 6542 5926 5583 4058 5347 6509 5932 4050 6058 4850
08 SV
09 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
10 ... ...

上面,我們只是選擇了非默認的核函數,還有其他參數可以選擇,比如代價系數c,默認是1,表示在計算線性分類面時,可以容許一個點被分錯。這時候,可以使用交叉驗證來逐步優化計算,選擇最合適的參數。
使用libsvm,指定交叉驗證選項的時候,只輸出經過交叉驗證得到的分類器的精度,而不會輸出模型文件,例如使用交叉驗證模型運行時的參數示例如下:

1 -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個類別的交叉驗證運行結果:

1 Cross Validation Accuracy = 71.10428571428571%

在選好各個參數以后,就可以使用最優的參數來計算輸出模型文件。

使用libsvm驗證文本分類器精度

前面已經訓練出來分類模型,就是最后輸出的模型文件。現在可以使用測試數據集了,通過使用測試數據集來做對基於文本分類模型文件預測分類精度進行驗證。同樣,需要做尺度變換,例如:

1 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類的輸入參數:

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

這樣,預測結果就在predict.txt文件中,同時輸出分類精度結果,例如:

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

如果覺得分類器精度不夠,可以使用交叉驗證去獲取更優的參數,來訓練並輸出模型文件,例如,下面是幾組結果:

01 train -h 0 -t 0 C:\\Users\\thinkpad\\Desktop\\vector\\train-scale.txt C:\\Users\\thinkpad\\Desktop\\vector\\model.txt
02 Accuracy = 67.10000000000001% (6710/10000) (classification)
03  
04 train -h 0 -t 0 -c 32 -v 5 C:\\Users\\thinkpad\\Desktop\\vector\\train-scale.txt C:\\Users\\thinkpad\\Desktop\\vector\\model.txt
05 Cross Validation Accuracy = 71.10428571428571%
06 Accuracy = 66.81% (6681/10000) (classification)
07  
08 train -h 0 -t 0 -c 8 -m 1024 C:\\Users\\thinkpad\\Desktop\\vector\\train-scale.txt
09 C:\\Users\\thinkpad\\Desktop\\vector\\model.txt
10 Cross Validation Accuracy = 74.3240057320121%
11 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

參考鏈接


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM