1. 程式人生 > >《機器學習系統設計》之應用scikit-learn做文字分類(上)

《機器學習系統設計》之應用scikit-learn做文字分類(上)

前言:

    本系列是在作者學習《機器學習系統設計》([美] WilliRichert)過程中的思考與實踐,全書通過Python從資料處理,到特徵工程,再到模型選擇,把機器學習解決問題的過程一一呈現。書中設計的原始碼和資料集已上傳到我的資源:http://download.csdn.net/detail/solomon1558/8971649

       第3章通過詞袋模型+K均值聚類實現相關文字的匹配。本文主要講解文字預處理部分內容,涉及切分文字、資料清洗、計算TF-IDF值等內容。

1. 統計詞語

    使用一個簡單的資料集進行實驗,它包括5個文件:

    01.   txt     This is a toy post about machine learning.Actually, it contains not much interesting stuff.

    02.   txt     Imaging databases provide storagecapabilities.

    03.   txt     Most imaging databases safe imagespermanently.

    04.   txt     Imaging databases store data.

    05.   txt     Imaging databases store data. Imagingdatabases store data. Imaging databases store data.

    在這個文件資料集中,我們想要找到和文件”imaging database”最相近的文件。為了將原始文字轉換成聚類演算法可以使用的特徵資料,首先需要使用詞袋(bag-of-word)方法來衡量文字間相似性,最終生成每個文字的特徵向量。

     詞袋方法基於簡單的詞頻統計;統計每一個帖子中的詞頻,表示成一個向量,即向量化。Scikit-learn的CountVectorizer可以高效地完成統計詞語的工作,Scikit的函式和類可以通過sklearn包引入進來:

posts = [open(os.path.join(DIR, f)).read() for f in os.listdir(DIR)]
vectorizer = CountVectorizer(min_df=1, stop_words="english")
X_train = vectorizer.fit_transform(posts)
    假設待訓練的文字存放在目錄DIR下,我們將資料集傳給CountVectorizer。引數min_df決定了CounterVectorizer如何處理那些不經常使用的詞語(最小文件詞頻)。當min_df為一個整數時,所有出現次數小於這個值的詞語都將被扔掉;當它是一個比例時,將整個資料集中出現比例小於這個值的詞語都將被丟棄。

    我們需要告訴這個想量化處理器整個資料集的資訊,使它可以預先知道都有哪些詞語:

X_train = vectorizer.fit_transform(posts)
num_samples, num_features = X_train.shape
print ("#sample: %d, #feature: %d" % (num_samples, num_features))
print(vectorizer.get_feature_names())

程式的輸出如下,5個文件中包含了25個詞語

            #sample: 5, #feature: 25

[u'about', u'actually', u'capabilities', u'contains',u'data', u'databases', u'images', u'imaging', u'interesting', u'is', u'it',u'learning', u'machine', u'most', u'much', u'not', u'permanently', u'post',u'provide', u'safe', u'storage', u'store', u'stuff', u'this', u'toy']

    對新文件進行向量化:

#a new post
new_post = "imaging databases"
new_post_vec = vectorizer.transform([new_post])

    把每個樣本的詞頻陣列當做向量進行相似度計算,需要使用陣列的全部元素[使用成員函式toarray()]。通過norm()函式計算新文件與所有訓練文件向量的歐幾里得範數(最小距離),從而衡量它們之間的相似度。

#------- calculate raw distances betwee new and old posts and record the shortest one-------------------------
def dist_raw(v1, v2):
    delta = v1 - v2
    return sp.linalg.norm(delta.toarray())
best_doc = None
best_dist = sys.maxint
best_i = None
for i in range(0, num_samples):
    post = posts[i]
    if post == new_post:
        continue
post_vec = X_train.getrow(i)
    d = dist_raw(post_vec, new_post_vec)
    print "=== Post %i with dist = %.2f: %s" % (i, d, post)
    if d<best_dist:
        best_dist = d
        best_i = i
print("Best post is %i with dist=%.2f" % (best_i, best_dist))

=== Post 0 with dist = 4.00:This is a toy post about machine learning. Actually, it contains not muchinteresting stuff.

=== Post 1 with dist =1.73:Imaging databases provide storage capabilities.

=== Post 2 with dist =2.00:Most imaging databases safe images permanently.

=== Post 3 with dist =1.41:Imaging databases store data.

=== Post 4 with dist =5.10:Imaging databases store data. Imaging databases store data. Imaging databasesstore data.

Best post is 3 with dist=1.41

    結果顯示文件3與新文件最為相似。然而文件4和文件3的內容一樣,但重複了3遍。所以,它和新文件的相似度應該與文件3是一樣的。

#-------case study: why post 4 and post 5 different ?-----------
print(X_train.getrow(3).toarray())
print(X_train.getrow(4).toarray())

[[0 0 0 0 1 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0]]

[[0 0 0 0 3 3 0 3 0 0 0 0 0 0 0 0 0 0 0 0 0 3 0 0 0]]

2. 文字處理

2.1 詞頻向量歸一化

       對第2節中的dist_raw函式進行擴充套件,在歸一化的向量上(向量各分量除以其模長)計算向量間的距離。

def dist_norm(v1, v2):
    v1_normalized = v1 / sp.linalg.norm(v1.toarray())
    v2_normalized = v2 / sp.linalg.norm(v2.toarray())
    delta = v1_normalized - v2_normalized
    return sp.linalg.norm(delta.toarray())

=== Post 0 with dist = 1.41: This is a toy post aboutmachine learning. Actually, it contains not much interesting stuff.

=== Post 1 with dist = 0.86: Imaging databases providestorage capabilities.

=== Post 2 with dist = 0.92: Most imaging databasessafe images permanently.

=== Post 3 with dist = 0.77:Imagingdatabases store data.

=== Post 4 with dist = 0.77:Imaging databases store data. Imaging databases store data. Imaging databasesstore data.

Best post is 3 with dist=0.77   

    詞頻向量歸一化之後,文件3和文件4與新文件具有了相同的相似度。從詞頻統計的角度來說,這樣處理更為正確。

2.2 排除停用詞

    文字中類似”the”、”of”這些單詞經常出現在各種不同的文字中,被稱為停用詞。由於停用詞對於區分文字沒有多大幫助,因此刪除停用詞是文字處理中的一個常見步驟。CountVectorizer中有一個簡單的引數stop_words()可以完成該任務:

vectorizer = CountVectorizer(min_df=1, stop_words='english')

2.3詞幹處理

    為了將語義類似但形式不同的詞語放在一起統計,我們需要一個函式將詞語歸約到特定的詞幹形式。自然語言處理工具包(NLTK)提供了一個非常容易嵌入到CountVectorizer的詞幹處理器。

    把文件傳入CountVectorizer之前,我們需要對它們進行詞幹處理。該類提供了幾種鉤子,可以用它們定製預處理和詞語切分階段的操作。前處理器和詞語切分器可以當作引數傳入建構函式。我們並不想把詞幹處理器放入它們任何一個當中,因為那樣的話,之後還需要親自對詞語進行切分和歸一化。相反,我們可以通過改寫build_analyzer方法來實現:

import nltk.stem
english_stemmer = nltk.stem.SnowballStemmer('english')
class StemmedCountVectorizer(CountVectorizer):
    def build_analyzer(self):
        analyzer = super(StemmedCountVectorizer, self).build_analyzer()
        return lambda doc: (english_stemmer.stem(w) for w in analyzer(doc))
vectorizer = StemmedCountVectorizer(min_df=1, stop_words='english')

    按照如下步驟對每個帖子進行處理:

(1)   在預處理階段將原始文件變成小寫字母形式(這在父類中完成);

(2)   在詞語切分階段提取所有單詞;

(3)   將每個詞語轉換成詞幹形式。

3. 計算TF-IDF

    至此,我們採用統計詞語的方式,從充滿噪聲的文字中提取了緊湊的特徵向量。這些特徵的值就是相應詞語在所有訓練文字中出現的次數,我們預設較大的特徵值意味著合格詞語對文字更為重要。但是在訓練文字中,不同的詞語對文字的可區分性貢獻更大。

    這需要通過統計每個文字的詞頻,並且對出現在多個文字中的詞語在權重上打折來解決。即當某個詞語經常出現在一些特定的文字中,而在其他地方很少出現時,應該賦予該詞語更大的權值。

    這正是詞頻-反轉文件頻率(TF-IDF)所要做的:TF代表統計部分,而IDF把權重摺扣考慮了進去。一個簡單的實現如下:

import scipy as sp
def tfidf(t, d, D):
    tf = float(d.count(t)) / sum(d.count(w) for w in set(d))
    idf = sp.log(float(len(D)) / (len([doc for doc in D if t in doc])))
    return tf * idf

    在實際應用過程中,scikit-learn已經將該演算法封裝進了TfidfVectorizer(繼承自CountVectorizer)中。進行這些操作後,我們得到的文件向量不會再包含詞語擁擠值,而是每個詞語的TF-IDF值。

程式碼清單:

import os
import sys
import scipy as sp
from sklearn.feature_extraction.text import CountVectorizer

DIR = r"../data/toy"
posts = [open(os.path.join(DIR, f)).read() for f in os.listdir(DIR)]
new_post = "imaging databases"

import nltk.stem
english_stemmer = nltk.stem.SnowballStemmer('english')

class StemmedCountVectorizer(CountVectorizer):
    def build_analyzer(self):
        analyzer = super(StemmedCountVectorizer, self).build_analyzer()
        return lambda doc: (english_stemmer.stem(w) for w in analyzer(doc))
#vectorizer = StemmedCountVectorizer(min_df=1, stop_words='english')

from sklearn.feature_extraction.text import TfidfVectorizer

class StemmedTfidfVectorizer(TfidfVectorizer):
    def build_analyzer(self):
        analyzer = super(StemmedTfidfVectorizer, self).build_analyzer()
        return lambda doc: (english_stemmer.stem(w) for w in analyzer(doc))

vectorizer = StemmedTfidfVectorizer(min_df=1, stop_words='english')
print(vectorizer)
X_train = vectorizer.fit_transform(posts)

num_samples, num_features = X_train.shape
print("#samples: %d, #features: %d" % (num_samples, num_features))

new_post_vec = vectorizer.transform([new_post])
print(new_post_vec, type(new_post_vec))
print(new_post_vec.toarray())
print(vectorizer.get_feature_names())

def dist_raw(v1, v2):
    delta = v1 - v2
    return sp.linalg.norm(delta.toarray())
def dist_norm(v1, v2):
    v1_normalized = v1 / sp.linalg.norm(v1.toarray())
    v2_normalized = v2 / sp.linalg.norm(v2.toarray())
    delta = v1_normalized - v2_normalized
    return sp.linalg.norm(delta.toarray())

dist = dist_norm
best_dist = sys.maxsize
best_i = None

for i in range(0, num_samples):
    post = posts[i]
    if post == new_post:
        continue
    post_vec = X_train.getrow(i)
    d = dist(post_vec, new_post_vec)
    print("=== Post %i with dist=%.2f: %s" % (i, d, post))
    if d < best_dist:
        best_dist = d
        best_i = i
print("Best post is %i with dist=%.2f" % (best_i, best_dist))

4. 總結

    文字預處理過程包含的步驟總結如下:

 (1)   切分文字;

 (2)   扔掉出現過於頻繁,而又對匹配相關文件沒有幫助的詞語;

 (3)   扔掉出現頻率很低,只有很小可能出現在未來帖子中的詞語;

 (4)   統計剩餘的詞語;

 (5)   考慮整個預料集合,從詞頻統計中計算TF-IDF值。

     通過這一過程,我們將一堆充滿噪聲的文字轉換成了一個簡明的特徵表示。然而,雖然詞袋模型及其擴充套件簡單有效,但仍然有一些缺點需要注意:

 (1)   它並不涵蓋詞語之間的關聯關係。採用之前的向量化方法,文字”Car hits wall”和”Wall hits car”會有相同的特徵向量。

 (2)   它沒法捕捉否定關係。例如”I will eat ice cream”和”I will not eat ice cream”,儘管它們意思截然相反,但從特徵向量來看它們非常相似。這個問題其實很容易解決,只需要既統計單個詞語(又叫unigrams),又考慮成隊的詞語(bigrams)或者trigrams(一行中的三個詞語)即可。

(3)   對於拼寫錯誤的詞語會處理失敗。