1. 詞袋模型 (Bag of Words, BOW)
文本分析是機器學習算法的一個主要應用領域。然而,原始數據的這些符號序列不能直接提供給算法進行訓練,因為大多數算法期望的是固定大小的數字特征向量,而不是可變長度的原始文本。
為了解決這個問題,scikit-learn提供了從文本內容中提取數字特征的常見方法,即:
- tokenizing: 標記字符串並為每個可能的token提供整數id,例如使用空白和標點作為token分隔符;(分詞標記)
- counting: 統計每個文檔中出現的token次數;(統計詞頻)
- normalizing: 通過減少大多數樣本/文檔中都會出現的一般性標記來進行標准化和加權。(標准化/歸一化)
在此方案中,特征和樣本定義如下:
每個獨立token出現的頻率(已標准化或未標准化)作為特征。
給定文檔的所有token頻率的向量作為多元樣本。
因此,文本語料庫可以由矩陣表示,每一行代表一個文本,每一列代表一個token(例如一個單詞)。
向量化:將文本集合轉換為數字特征向量的一般過程。
這種方法(tokenizing,counting和normalizing)稱為“詞袋”或“n-gram”模型。 即只通過單詞頻率來描述文檔,而完全忽略文檔中單詞的相對位置信息。
2. 稀疏表示
由於大多數文本通常只使用語料庫中的很小一部分單詞,因此生成的矩陣將具有許多為零的特征值(通常超過99%)。
例如,有一個文本集合,包含一萬個文本(郵件等),它使用的詞匯表大約為十萬個詞,而其中每個文檔單獨使用的詞只有100到1000個。
為了能夠將這樣的矩陣存儲在內存中並且加快矩陣/向量的代數運算,實現上通常會使用稀疏表示,在scipy.sparse包中有實現方法。
3. 常用的Vectorizer的用法
CountVectorizer在單個類中同時實現tokenizing和counting:
from sklearn.feature_extraction.text import CountVectorizer
該模型具有許多參數,但是默認值是相當合理的:
vectorizer = CountVectorizer()
vectorizer
CountVectorizer(
analyzer='word', binary=False, decode_error='strict',
dtype=<class 'numpy.int64'>, encoding='utf-8', input='content',
lowercase=True, max_df=1.0, max_features=None, min_df=1,
ngram_range=(1, 1), preprocessor=None, stop_words=None,
strip_accents=None, token_pattern='(?u)\b\w\w+\b',
tokenizer=None, vocabulary=None)
示例:標記和計算簡單文本語料庫中的詞頻:
corpus = [
'This is the first document.',
'This is the second second document.',
'And the third one.',
'Is this the first document?',
]
X = vectorizer.fit_transform(corpus)
X
<4x9 sparse matrix of type '<class 'numpy.int64'>'
with 19 stored elements in Compressed Sparse Row format>
其默認參數配置是通過提取單詞(至少2個字母)來標記字符串。也可以通過顯式請求來查看這一步驟:
analyze = vectorizer.build_analyzer()
analyze("This is a text document to analyze.")
['this', 'is', 'text', 'document', 'to', 'analyze']
在擬合過程中,將由分析器找到的每一項分配一個唯一的整數索引,該索引對應於所得矩陣中的一列。 可以按以下方式檢索這些列:
vectorizer.get_feature_names()
['and', 'document', 'first', 'is', 'one', 'second', 'the', 'third', 'this']
X.toarray()
array([[0, 1, 1, 1, 0, 0, 1, 0, 1],
[0, 1, 0, 1, 0, 2, 1, 0, 1],
[1, 0, 0, 0, 1, 0, 1, 1, 0],
[0, 1, 1, 1, 0, 0, 1, 0, 1]])
從特征名到列索引的反向映射存儲在Vectorizer的vocabulary_屬性中:
vectorizer.vocabulary_.get('first')
2
因此,在之后對transform方法的調用中,訓練語料庫中未出現的單詞將被完全忽略:
vectorizer.transform(['Something completely new.']).toarray()
array([[0, 0, 0, 0, 0, 0, 0, 0, 0]])
注意,在前一語料庫中,第一個文檔和最后一個文檔具有完全相同的詞,因此被編碼為同樣的向量。 但這就失去了最后一個文檔是疑問句的信息。
為了保留一些局部信息,我們可以提取單詞的1-gram(單個單詞)以外的2-gram信息:
bigram_vectorizer = CountVectorizer(ngram_range=(1, 2), token_pattern=r'\b\w+\b', min_df=1)
analyze = bigram_vectorizer.build_analyzer()
analyze('Bi-grams are cool!') == (['bi', 'grams', 'are', 'cool', 'bi grams', 'grams are', 'are cool'])
因此,這個Vectorizer提取的詞匯量要更大,而且現在可以解決局部定位模式中的編碼歧義:
X_2 = bigram_vectorizer.fit_transform(corpus).toarray()
X_2
array([[0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0],
[0, 0, 1, 0, 0, 1, 1, 0, 0, 2, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0],
[1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0],
[0, 0, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 1]])
特別是疑問句形式 “Is this” 僅出現在最后一個文檔中:
feature_index = bigram_vectorizer.vocabulary_.get('is this')
X_2[:, feature_index]
array([0, 0, 0, 1])
4. 停用詞
停用詞是指諸如“and”,“the”,“him”之類的詞,它們被認為在表示文本內容方面沒有提供任何信息,可以將其刪除以避免其影響預測效果。
但是,有時候,類似的單詞對於預測很有用,例如在對寫作風格或語言個性進行分類時。
請謹慎選擇停用詞列表。 通用的停用詞列表也可能包含對某些特定任務(例如計算機領域)非常有用的詞。
此外,還應該確保停用詞列表的預處理和標記化與Vectorizer中使用的預處理和標記化相同。
CountVectorizer的默認標記器將單詞"we've"分為we和ve,因此,如果“we've”在stop_words中,而ve則沒有,則在轉換后的文本中會保留ve。
我們的Vectorizer將嘗試識別並警告某些不一致之處。
TF-IDF模型
在大型文本語料庫中,會經常出現一些單詞(例如英語中的“ the”,“ a”,“ is”),而這些單詞幾乎不包含關於文檔實際內容的有意義的信息。
如果我們將直接計數數據不加處理地提供給分類器,那么那些高頻詞會影響低頻但更有意義的詞的出現概率。
為了將計數特征重新加權為適合分類器使用的浮點值,通常使用tf–idf變換。
tf表示詞頻,而tf–idf表示詞頻乘以逆文檔頻率:
\(\text{tf-idf(t,d)}=\text{tf(t,d)} \times \text{idf(t)}\)
TfidfTransformer的默認參數為,TfidfTransformer(norm='l2', use_idf=True, smooth_idf=True, sublinear_tf=False)。
詞頻,即一個單詞在文檔中出現的頻率,乘以idf:
\(\text{idf}(t) = \log{\frac{1 + n}{1+\text{df}(t)}} + 1\)
n是文本集中文本總數,df(t)是包含t詞的文本數,然后將所得的tf-idf向量通過歐幾里得范數歸一化:
\(v_{norm} = \frac{v}{||v||_2} = \frac{v}{\sqrt{v{_1}^2 + v{_2}^2 + \dots + v{_n}^2}}\)
這最初是信息檢索的詞加權方案,作為搜索引擎結果的排名方法,目前也在文檔分類和聚類中廣泛應用。
以下各節包含進一步的說明和示例,這些示例說明了如何精確計算tf-idf,以及在scikit-learn的TfidfTransformer和TfidfVectorizer中怎樣計算的。
與標准教科書的符號稍微不同,idf定義為:
\(\text{idf}(t) = \log{\frac{n}{1+\text{df}(t)}}.\)
在TfidfTransformer和TfidfVectorizer中設置smooth_idf=False,將“ 1”計數添加到IDF中,而不是IDF的分母中:
\(\text{idf}(t) = \log{\frac{n}{\text{df}(t)}} + 1\)
這一規范化由TfidfTransformer類實現:
from sklearn.feature_extraction.text import TfidfTransformer
transformer = TfidfTransformer(smooth_idf=False)
transformer
TfidfTransformer(norm='l2', smooth_idf=False, sublinear_tf=False, use_idf=True)
讓我們以以下的統計數為例。 第一個詞出現的概率100%,因此它的出現沒有什么代表性。 其他兩個詞僅在不到50%的時間內出現,因此可能更能代表文檔的內容:
counts = [[3, 0, 1],
[2, 0, 0],
[3, 0, 0],
[4, 0, 0],
[3, 2, 0],
[3, 0, 2]]
tfidf = transformer.fit_transform(counts)
tfidf
<6x3 sparse matrix of type '<class 'numpy.float64'>'
with 9 stored elements in Compressed Sparse Row format>
tfidf.toarray()
array([[0.81940995, 0. , 0.57320793],
[1. , 0. , 0. ],
[1. , 0. , 0. ],
[1. , 0. , 0. ],
[0.47330339, 0.88089948, 0. ],
[0.58149261, 0. , 0.81355169]])
每行均經過單位歐幾里得范數計算以進行標准化:
\(v_{norm} = \frac{v}{||v||_2} = \frac{v}{\sqrt{v{_1}^2 + v{_2}^2 + \dots + v{_n}^2}}\)
例如,我們可以如下計算counts數組中第一個文檔中第一項的tf-idf:
\(n = 6\)
\(\text{df}(t)_{\text{term1}} = 6\)
\(\text{idf}(t)_{\text{term1}} = \log \frac{n}{\text{df}(t)} + 1 = \log(1)+1 = 1\)
\(\text{tf-idf}_{\text{term1}} = \text{tf} \times \text{idf} = 3 \times 1 = 3\)
現在,如果我們對文檔中剩余的2個詞重復此計算,我們將得到:
\(\text{tf-idf}_{\text{term2}} = 0 \times (\log(6/1)+1) = 0\)
\(\text{tf-idf}_{\text{term3}} = 1 \times (\log(6/2)+1) \approx 2.0986\)
原始的tf-idf向量:
\(\text{tf-idf}_{\text{raw}} = [3, 0, 2.0986].\)
然后,應用歐幾里得(L2)范數,我們為文本1獲得以下tf-idfs:
\(\frac{[3, 0, 2.0986]}{\sqrt{\big(3^2 + 0^2 + 2.0986^2\big)}} = [ 0.819, 0, 0.573].\)
此外,默認參數smooth_idf = True將“ 1”添加到分子和分母,就好像看到一個額外的文檔恰好包含一次集合中的每個術語一次,從而避免了分母為零的問題:
\(\text{idf}(t) = \log{\frac{1 + n}{1+\text{df}(t)}} + 1\)
使用此修改,文檔1中第3項的tf-idf更改為1.8473:
\(\text{tf-idf}_{\text{term3}} = 1 \times \log(7/3)+1 \approx 1.8473\)
並且L2歸一化的tf-idf變為:
\(\frac{[3, 0, 1.8473]}{\sqrt{\big(3^2 + 0^2 + 1.8473^2\big)}} = [0.8515, 0, 0.5243]\)
transformer = TfidfTransformer()
transformer.fit_transform(counts).toarray()
array([[0.85151335, 0. , 0.52433293],
[1. , 0. , 0. ],
[1. , 0. , 0. ],
[1. , 0. , 0. ],
[0.55422893, 0.83236428, 0. ],
[0.63035731, 0. , 0.77630514]])
由fit方法調用計算出的每個特征的權重存儲在model屬性中:
transformer.idf_
array([1. , 2.25276297, 1.84729786])
由於tf–idf通常用於文本特征,因此還有另一個名為TfidfVectorizer的類,它將CountVectorizer和TfidfTransformer的所有選項組合在一個模型中:
from sklearn.feature_extraction.text import TfidfVectorizer
vectorizer = TfidfVectorizer()
vectorizer.fit_transform(corpus)
<4x9 sparse matrix of type '<class 'numpy.float64'>'
with 19 stored elements in Compressed Sparse Row format>
盡管tf–idf歸一化通常非常有用,但是在某些情況下,二進制頻率標記法可能會提供更好的特性。 這可以通過使用CountVectorizer的二進制參數來實現。
特別是,某些估計量(例如Bernoulli Naive Bayes)明確地對離散的布爾型隨機變量建模。 同樣,很短的文本可能帶有tf–idf值的噪聲,而二進制出現信息則更穩定。
通常,調整特征提取參數的最佳方法是使用交叉驗證的網格搜索,例如用分類器將特征提取器進行流水線化。