天天看點

Kaggle word2vec NLP 教程 第三部分:詞向量的更多樂趣

原文: Bag of Words Meets Bags of Popcorn 譯者: 飛龍 協定: CC BY-NC-SA 4.0 自豪地采用 谷歌翻譯

第三部分:詞向量的更多樂趣

代碼

第三部分的代碼在

這裡

單詞的數值表示

現在我們有了訓練好的模型,對單詞有一些語義了解,我們應該如何使用它? 如果你看它的背後,第 2 部分訓練的 Word2Vec 模型由詞彙表中每個單詞的特征向量組成,存儲在一個名為

syn0

numpy

數組中:

>>> # Load the model that we created in Part 2
>>> from gensim.models import Word2Vec
>>> model = Word2Vec.load("300features_40minwords_10context")
2014-08-03 14:50:15,126 : INFO : loading Word2Vec object from 300features_40min_word_count_10context
2014-08-03 14:50:15,777 : INFO : setting ignored attribute syn0norm to None

>>> type(model.syn0)
<type 'numpy.ndarray'>

>>> model.syn0.shape
(16492, 300)
           

syn0

中的行數是模型詞彙表中的單詞數,列數對應于我們在第 2 部分中設定的特征向量的大小。将最小單詞計數設定為 40 ,總詞彙量為 16,492 個單詞,每個詞有 300 個特征。 可以通過以下方式通路單個單詞向量:

>>> model["flower"]
           

…傳回一個 1x300 的

numpy

數組。

從單詞到段落,嘗試 1:向量平均

IMDB 資料集的一個挑戰是可變長度評論。 我們需要找到一種方法來擷取單個單詞向量并将它們轉換為每個評論的長度相同的特征集。

由于每個單詞都是 300 維空間中的向量,我們可以使用向量運算來組合每個評論中的單詞。 我們嘗試的一種方法是簡單地平均給定的評論中的單詞向量(為此,我們删除了停止詞,這隻會增加噪音)。

以下代碼基于第 2 部分的代碼建構了特征向量的平均值。

import numpy as np  # Make sure that numpy is imported

def makeFeatureVec(words, model, num_features):
    # 用于平均給定段落中的所有單詞向量的函數
    #
    # 預初始化一個空的 numpy 數組(為了速度)
    featureVec = np.zeros((num_features,),dtype="float32")
    #
    nwords = 0.
    # 
    # Index2word 是一個清單,包含模型詞彙表中的單詞名稱。
    # 為了獲得速度,将其轉換為集合。 
    index2word_set = set(model.index2word)
    #
    # 周遊評論中的每個單詞,如果它在模型的詞彙表中,
    # 則将其特征向量加到 total
    for word in words:
        if word in index2word_set: 
            nwords = nwords + 1.
            featureVec = np.add(featureVec,model[word])
    # 
    # 将結果除以單詞數來獲得平均值
    featureVec = np.divide(featureVec,nwords)
    return featureVec


def getAvgFeatureVecs(reviews, model, num_features):
    # 給定一組評論(每個評論都是單詞清單),計算每個評論的平均特征向量并傳回2D numpy數組
    # 
    # 初始化計數器
    counter = 0.
    # 
    # 為了速度,預配置設定 2D numpy 數組
    reviewFeatureVecs = np.zeros((len(reviews),num_features),dtype="float32")
    # 
    # 周遊評論
    for review in reviews:
       #
       # 每 1000 個評論列印一次狀态消息
       if counter%1000. == 0.:
           print "Review %d of %d" % (counter, len(reviews))
       # 
       # 調用生成平均特征向量的函數(定義如上)
       reviewFeatureVecs[counter] = makeFeatureVec(review, model, \
           num_features)
       #
       # 增加計數器
       counter = counter + 1.
    return reviewFeatureVecs
           

現在,我們可以調用這些函數來為每個段落建立平均向量。 以下操作将需要幾分鐘:

# ****************************************************************
# 使用我們在上面定義的函數,
# 計算訓練和測試集的平均特征向量。
# 請注意,我們現在删除停止詞。

clean_train_reviews = []
for review in train["review"]:
    clean_train_reviews.append( review_to_wordlist( review, \
        remove_stopwords=True ))

trainDataVecs = getAvgFeatureVecs( clean_train_reviews, model, num_features )

print "Creating average feature vecs for test reviews"
clean_test_reviews = []
for review in test["review"]:
    clean_test_reviews.append( review_to_wordlist( review, \
        remove_stopwords=True ))

testDataVecs = getAvgFeatureVecs( clean_test_reviews, model, num_features )
           

接下來,使用平均段落向量來訓練随機森林。 請注意,與第 1 部分一樣,我們隻能使用标記的訓練評論來訓練模型。

# 使用 100 棵樹讓随機森林拟合訓練資料
from sklearn.ensemble import RandomForestClassifier
forest = RandomForestClassifier( n_estimators = 100 )

print "Fitting a random forest to labeled training data..."
forest = forest.fit( trainDataVecs, train["sentiment"] )

# 測試和提取結果
result = forest.predict( testDataVecs )

# 寫出測試結果
output = pd.DataFrame( data={"id":test["id"], "sentiment":result} )
output.to_csv( "Word2Vec_AverageVectors.csv", index=False, quoting=3 )
           

我們發現這産生了比偶然更好的結果,但是表現比詞袋低了幾個百分點。

由于向量的元素平均值沒有産生驚人的結果,或許我們可以以更聰明的方式實作? 權重單詞向量的标準方法是應用

“tf-idf”

權重,它衡量給定單詞在給定文檔集中的重要程度。 在 Python 中提取 tf-idf 權重的一種方法,是使用 scikit-learn 的

TfidfVectorizer

,它具有類似于我們在第 1 部分中使用的

CountVectorizer

的接口。但是,當我們嘗試以這種方式權重我們的單詞向量時,我們發現沒有實質的性能改善。

從單詞到段落,嘗試 2:聚類

Word2Vec 建立語義相關單詞的簇,是以另一種可能的方法是利用簇中單詞的相似性。 以這種方式來分組向量稱為“向量量化”。 為了實作它,我們首先需要找到單詞簇的中心,我們可以通過使用

聚類算法

(如

K-Means

)來完成。

在 K-Means 中,我們需要設定的一個參數是“K”,或者是簇的數量。 我們應該如何決定要建立多少個簇? 試錯法表明,每個簇平均隻有5個單詞左右的小簇,比具有多個詞的大簇産生更好的結果。 聚類代碼如下。 我們使用

scikit-learn 來執行我們的 K-Means

具有較大 K 的 K-Means 聚類可能非常慢;以下代碼在我的計算機上花了 40 多分鐘。 下面,我們給 K-Means 函數設定一個計時器,看看它需要多長時間。

from sklearn.cluster import KMeans
import time

start = time.time() # Start time

# 将“k”(num_clusters)設定為詞彙量大小的 1/5,或每個簇平均 5 個單詞
word_vectors = model.syn0
num_clusters = word_vectors.shape[0] / 5

# 初始化 k-means 對象并使用它來提取質心
kmeans_clustering = KMeans( n_clusters = num_clusters )
idx = kmeans_clustering.fit_predict( word_vectors )

# 擷取結束時間并列印該過程所需的時間
end = time.time()
elapsed = end - start
print "Time taken for K Means clustering: ", elapsed, "seconds."
           

現在,每個單詞的聚類分布都存儲在

idx

中,而原始 Word2Vec 模型中的詞彙表仍存儲在

model.index2word

中。 為友善起見,我們将它們壓縮成一個字典,如下所示:

# 建立單詞/下标字典,将每個詞彙表單詞映射為簇編号
word_centroid_map = dict(zip( model.index2word, idx ))
           

這有點抽象,是以讓我們仔細看看我們的簇包含什麼。 你的簇可能會有所不同,因為 Word2Vec 依賴于随機數種子。 這是一個循環,列印出簇 0 到 9 的單詞:

# 對于前 10 個簇
for cluster in xrange(0,10):
    #
    # 列印簇編号
    print "\nCluster %d" % cluster
    #
    # 找到該簇編号的所有單詞,然後将其列印出來
    words = []
    for i in xrange(0,len(word_centroid_map.values())):
        if( word_centroid_map.values()[i] == cluster ):
            words.append(word_centroid_map.keys()[i])
    print words
           

結果很有意思:

Cluster 0
[u'passport', u'penthouse', u'suite', u'seattle', u'apple']

Cluster 1
[u'unnoticed']

Cluster 2
[u'midst', u'forming', u'forefront', u'feud', u'bonds', u'merge', u'collide', u'dispute', u'rivalry', u'hostile', u'torn', u'advancing', u'aftermath', u'clans', u'ongoing', u'paths', u'opposing', u'sexes', u'factions', u'journeys']

Cluster 3
[u'lori', u'denholm', u'sheffer', u'howell', u'elton', u'gladys', u'menjou', u'caroline', u'polly', u'isabella', u'rossi', u'nora', u'bailey', u'mackenzie', u'bobbie', u'kathleen', u'bianca', u'jacqueline', u'reid', u'joyce', u'bennett', u'fay', u'alexis', u'jayne', u'roland', u'davenport', u'linden', u'trevor', u'seymour', u'craig', u'windsor', u'fletcher', u'barrie', u'deborah', u'hayward', u'samantha', u'debra', u'frances', u'hildy', u'rhonda', u'archer', u'lesley', u'dolores', u'elsie', u'harper', u'carlson', u'ella', u'preston', u'allison', u'sutton', u'yvonne', u'jo', u'bellamy', u'conte', u'stella', u'edmund', u'cuthbert', u'maude', u'ellen', u'hilary', u'phyllis', u'wray', u'darren', u'morton', u'withers', u'bain', u'keller', u'martha', u'henderson', u'madeline', u'kay', u'lacey', u'topper', u'wilding', u'jessie', u'theresa', u'auteuil', u'dane', u'jeanne', u'kathryn', u'bentley', u'valerie', u'suzanne', u'abigail']

Cluster 4
[u'fest', u'flick']

Cluster 5
[u'lobster', u'deer']

Cluster 6
[u'humorless', u'dopey', u'limp']

Cluster 7
[u'enlightening', u'truthful']

Cluster 8
[u'dominates', u'showcases', u'electrifying', u'powerhouse', u'standout', u'versatility', u'astounding']

Cluster 9
[u'succumbs', u'comatose', u'humiliating', u'temper', u'looses', u'leans']
           

我們可以看到這些簇的品質各不相同。 有些是有道理的 - 簇 3 主要包含名稱,而簇 6- 8包含相關的形容詞(簇 6 是我最喜歡的)。 另一方面,簇 5 有點神秘:龍蝦和鹿有什麼共同之處(除了是兩隻動物)? 簇 0 更糟糕:閣樓和套房似乎屬于一個東西,但它們似乎不屬于蘋果和護照。 簇 2 包含…可能與戰争有關的詞? 也許我們的算法在形容詞上效果最好。

無論如何,現在我們為每個單詞配置設定了一個簇(或“質心”),我們可以定義一個函數将評論轉換為質心袋。 這就像詞袋一樣,但使用語義相關的簇而不是單個單詞:

def create_bag_of_centroids( wordlist, word_centroid_map ):
    #
    # 簇的數量等于單詞/質心映射中的最大的簇索引
    num_centroids = max( word_centroid_map.values() ) + 1
    #
    # 預配置設定質心向量袋(為了速度)
    bag_of_centroids = np.zeros( num_centroids, dtype="float32" )
    #
    # 周遊評論中的單詞。如果單詞在詞彙表中,
    # 找到它所屬的簇,并将該簇的計數增加 1
    for word in wordlist:
        if word in word_centroid_map:
            index = word_centroid_map[word]
            bag_of_centroids[index] += 1
    #
    # 傳回“質心袋”
    return bag_of_centroids
           

上面的函數将為每個評論提供一個

numpy

數組,每個數組的特征都與簇數相等。 最後,我們為訓練和測試集建立了質心袋,然後訓練随機森林并提取結果:

# 為訓練集質心預配置設定一個數組(為了速度)
train_centroids = np.zeros( (train["review"].size, num_clusters), \
    dtype="float32" )

# 将訓練集評論轉換為質心袋
counter = 0
for review in clean_train_reviews:
    train_centroids[counter] = create_bag_of_centroids( review, \
        word_centroid_map )
    counter += 1

# 對測試評論重複
test_centroids = np.zeros(( test["review"].size, num_clusters), \
    dtype="float32" )

counter = 0
for review in clean_test_reviews:
    test_centroids[counter] = create_bag_of_centroids( review, \
        word_centroid_map )
    counter += 1
           
# 拟合随機森林并提取預測
forest = RandomForestClassifier(n_estimators = 100)

# 拟合可能需要幾分鐘
print "Fitting a random forest to labeled training data..."
forest = forest.fit(train_centroids,train["sentiment"])
result = forest.predict(test_centroids)

# 寫出測試結果
output = pd.DataFrame(data={"id":test["id"], "sentiment":result})
output.to_csv( "BagOfCentroids.csv", index=False, quoting=3 )
           

我們發現與第 1 部分中的詞袋相比,上面的代碼給出了相同(或略差)的結果。

深度和非深度學習方法的比較

你可能會問:為什麼詞袋更好?

最大的原因是,在我們的教程中,平均向量和使用質心會失去單詞的順序,這使得它與詞袋的概念非常相似。性能相似(在标準誤差範圍内)的事實使得所有三種方法實際上相同。

一些要嘗試的事情:

首先,在更多文本上訓練 Word2Vec 應該會大大提高性能。谷歌的結果基于從超過十億字的語料庫中學到的單詞向量;我們标記和未标記的訓練集合在一起隻有 1800 萬字左右。友善的是,Word2Vec 提供了加載由谷歌原始 C 工具輸出的任何預訓練模型的函數,是以也可以用 C 訓練模型然後将其導入 Python。

其次,在已發表的文獻中,分布式單詞向量技術已被證明優于詞袋模型。在本文中,在 IMDB 資料集上使用了一種名為段落向量的算法,來生成迄今為止最先進的一些結果。在某種程度上,它比我們在這裡嘗試的方法更好,因為向量平均和聚類會丢失單詞順序,而段落向量會保留單詞順序資訊。