@Hcy(黃燦奕)
文本分類,首先它是分類問題,應該對應着分類過程的兩個重要的步驟,一個是使用訓練數據集訓練分類器,另一個就是使用測試數據集來評價分類器的分類精度。然而,作為文本分類,它還具有文本這樣的約束,所以對於文本來說,需要額外的處理過程,我們結合使用libsvm從宏觀上總結一下,基於libsvm實現文本分類實現的基本過程,如下所示:
- 選擇文本訓練數據集和測試數據集:訓練集和測試集都是類標簽已知的;
- 訓練集文本預處理:這里主要包括分詞、去停用詞、建立詞袋模型(倒排表);
- 選擇文本分類使用的特征向量(詞向量):最終的目標是使得最終選出的特征向量在多個類別之間具有一定的類別區分度,可以使用相關有效的技術去實現特征向量的選擇,由於分詞后得到大量的詞,通過選擇降維技術能很好地減少計算量,還能維持分類的精度;
- 輸出libsvm支持的量化的訓練樣本集文件:類別名稱、特征向量中每個詞元素分別到數字編號的映射轉換,以及基於類別和特征向量來量化文本訓練集,能夠滿足使用libsvm訓練所需要的數據格式;
- 測試數據集預處理:同樣包括分詞(需要和訓練過程中使用的分詞器一致)、去停用詞、建立詞袋模型(倒排表),但是這時需要加載訓練過程中生成的特征向量,用特征向量去排除多余的不在特征向量中的詞(也稱為降維);
- 輸出libsvm支持的量化的測試樣本集文件:格式和訓練數據集的預處理階段的輸出相同。
- 使用libsvm訓練文本分類器:使用訓練集預處理階段輸出的量化的數據集文件,這個階段也需要做很多工作(后面會詳細說明),最終輸出分類模型文件
- 使用libsvm驗證分類模型的精度:使用測試集預處理階段輸出的量化的數據集文件,和分類模型文件來驗證分類的精度。
- 分類模型參數尋優:如果經過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; |
6 |
public interface DocumentAnalyzer { |
7 |
Map<String, Term> analyze(File file); |
增加一個外部的停用詞表,這個我們直接封裝到抽象類AbstractDocumentAnalyzer中去了,該抽象類就是從一個指定的文件或目錄讀取停用詞文件,將停用詞加載到內存中,在分詞的過程中對詞進行進一步的過濾。然后基於上面的實現,給出包裹ICTCLAS分詞器的實現,代碼如下所示:
01 |
package org.shirdrn.document.processor.analyzer; |
03 |
import java.io.BufferedReader; |
05 |
import java.io.FileInputStream; |
06 |
import java.io.IOException; |
07 |
import java.io.InputStreamReader; |
08 |
import java.util.HashMap; |
11 |
import kevin.zhang.NLPIR; |
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; |
19 |
public class IctclasAnalyzer extends AbstractDocumentAnalyzer implements DocumentAnalyzer { |
21 |
private static final Log LOG = LogFactory.getLog(IctclasAnalyzer. class ); |
22 |
private final NLPIR analyzer; |
24 |
public IctclasAnalyzer(Configuration configuration) { |
26 |
analyzer = new NLPIR(); |
28 |
boolean initialized = NLPIR.NLPIR_Init( "." .getBytes(charSet), 1 ); |
30 |
throw new RuntimeException( "Fail to initialize!" ); |
32 |
} catch (Exception e) { |
33 |
throw new RuntimeException( "" , e); |
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 ; |
44 |
br = new BufferedReader( new InputStreamReader( new FileInputStream(file), charSet)); |
46 |
while ((line = br.readLine()) != null ) { |
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); |
59 |
term = new Term(word); |
61 |
term.setLexicalCategory(lexicalCategory); |
62 |
terms.put(word, term); |
65 |
LOG.debug( "Got word: word=" + rawWord); |
70 |
} catch (IOException e) { |
77 |
} catch (IOException e) { |
它是對一個文件進行讀取,然后進行分詞,去停用詞,最后返回的Map包含了的集合,此屬性包括詞性(Lexical Category)、詞頻、TF等信息。
這樣,遍歷數據集目錄和文件,就能去將全部的文檔分詞,最終構建詞袋模型。我們使用Java中集合來存儲文檔、詞、類別之間的關系,如下所示:
01 |
private int totalDocCount; |
02 |
private final List<String> labels = new ArrayList<String>(); |
04 |
private final Map<String, Integer> labelledTotalDocCountMap = new HashMap<String, Integer>(); |
06 |
private final Map<String, Map<String, Map<String, Term>>> termTable = |
07 |
new HashMap<String, Map<String, Map<String, Term>>>(); |
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)技術來實現選擇,這里根據計算公式:

其中,公式中各個參數的含義,說明如下:
- N:訓練數據集文檔總數
- A:在一個類別中,包含某個詞的文檔的數量
- B:在一個類別中,排除該類別,其他類別包含某個詞的文檔的數量
- C:在一個類別中,不包含某個詞的文檔的數量
- D:在一個類別中,不包含某個詞也不在該類別中的文檔的數量
要想進一步了解,可以參考這篇論文。
使用卡方統計量,為每個類別下的每個詞都進行計算得到一個CHI值,然后對這個類別下的所有的詞基於CHI值進行排序,選擇出最大的topN個詞(很顯然使用堆排序算法更合適);最后將多個類別下選擇的多組topN個詞進行合並,得到最終的特征向量。
其實,這里可以進行一下優化,每個類別下對應着topN個詞,在合並的時候可以根據一定的標准,將各個類別都出現的詞給出一個比例,超過指定比例的可以刪除掉,這樣可以使特征向量在多個類別分類過程中更具有區分度。這里,我們只是做了個簡單的合並。
我們看一下,用到的存儲結構,使用Java的集合來存儲:
2 |
private final Map<String, Map<String, Term>> chiLabelToWordsVectorsMap = new HashMap<String, Map<String, Term>>( 0 ); |
4 |
private final Map<String, Term> chiMergedTermVectorMap = new HashMap<String, Term>( 0 ); |
下面,實現特征向量選擇計算的實現,代碼如下所示:
001 |
package org.shirdrn.document.processor.component.train; |
003 |
import java.util.Iterator; |
004 |
import java.util.Map; |
005 |
import java.util.Map.Entry; |
006 |
import java.util.Set; |
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; |
015 |
public class FeatureTermVectorSelector extends AbstractComponent { |
017 |
private static final Log LOG = LogFactory.getLog(FeatureTermVectorSelector. class ); |
018 |
private final int keptTermCountEachLabel; |
020 |
public FeatureTermVectorSelector(Context context) { |
022 |
keptTermCountEachLabel = context.getConfiguration().getInt( "processor.each.label.kept.term.count" , 3000 ); |
029 |
for (String label : context.getVectorMetadata().getLabels()) { |
031 |
LOG.info( "Compute CHI for: label=" + label); |
032 |
processOneLabel(label); |
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]; |
046 |
context.getVectorMetadata().addChiMergedTerm(termEntry.getKey(), termEntry.getValue()); |
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); |
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(); |
068 |
int docCountContainingWordInLabel = 0 ; |
069 |
if (labelledDocs.get(label) != null ) { |
070 |
docCountContainingWordInLabel = labelledDocs.get(label).size(); |
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(); |
086 |
int docCountNotContainingWordInLabel = |
087 |
getDocCountNotContainingWordInLabel(word, label); |
090 |
int docCountNotContainingWordNotInLabel = |
091 |
getDocCountNotContainingWordNotInLabel(word, label); |
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); |
103 |
context.getVectorMetadata().addChiTerm(label, word, term); |
107 |
private int getDocCountNotContainingWordInLabel(String word, String label) { |
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(); |
115 |
if (tmpLabel.equals(label)) { |
116 |
Map<String, Map<String, Term>> labelledDocs = entry.getValue(); |
117 |
for (Entry<String, Map<String, Term>> docEntry : labelledDocs.entrySet()) { |
119 |
if (!docEntry.getValue().containsKey(word)) { |
129 |
private int getDocCountNotContainingWordNotInLabel(String word, String label) { |
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(); |
137 |
if (!tmpLabel.equals(label)) { |
138 |
Map<String, Map<String, Term>> labelledDocs = entry.getValue(); |
139 |
for (Entry<String, Map<String, Term>> docEntry : labelledDocs.entrySet()) { |
141 |
if (!docEntry.getValue().containsKey(word)) { |
輸出量化數據文件
特征向量已經從訓練數據集中計算出來,接下來需要對每個詞給出一個唯一的編號,從1開始,這個比較容易,輸出特征向量文件,為測試驗證的數據集所使用,文件格式如下所示:
由於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,內容示例如下所示:
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 |
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 |
上面,我們只是選擇了非默認的核函數,還有其他參數可以選擇,比如代價系數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) |
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) |
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分類,或者其他的工具,都不要忘記,有如下幾點需要考慮到:
- 其實文本在預處理過程進行的很多過程對最后使用工具進行分類都會有影響。
- 最重要的就是文本特征向量的選擇方法,如果文本特征向量選擇的很差,即使再好的分類工具,可能訓練得到的分類器都未必能達到更好。
- 文本特征向量選擇的不好,在訓練調優分類器的過程中,雖然找到了更好的參數,但是這本身可能會是一個不好的分類器,在實際預測中可以出現誤分類的情況。
- 選擇訓練集和測試集,對整個文本預處理過程,以及使用分類工具進行訓練,都有影響。
相關資源
最后,附上文章中有關文本分類預處理的實現代碼,僅供參考,鏈接為:
這個是最早的那個版本,默認分詞使用了中科院ICTCLAS分詞器,不過我當時使用的現在好像因為License的問題可能用不了了。
這個是我最近抽時間將原來document-processor代碼進行重構,使用Maven進行構建,分為多個模塊,並做了部分優化:
- 默認使用Lucene的SmartChineseAnalyzer進行分詞,默認沒有使用詞典,分詞效果可能沒有那么理想。
- 增加了api層,對於某些可以自定義實現的,預留出了接口,並可以通過配置來注入自定義實現功能,主要是接口:FeatureTermSelector。
- 分詞過程中,各個類別采用並行處理,處理時間大大減少:DocumentWordsCollector。
- 將train階段和test階段通用的配置提出來,放在配置文件config.properties中。
- 默認給予CHI卡方統計量方法選擇特征向量,實現了各個類別之間並行處理的邏輯,可以查看實現類ChiFeatureTermSelector。
另外,使用的語料庫文件,我放在百度網盤上了,可以下載:http://pan.baidu.com/s/1pJv6AIR
參考鏈接