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

原文:Bag of Words Meets Bags of Popcorn

译者:飞龙

协议:CC BY-NC-SA 4.0

自豪地采用谷歌翻译

第三部分:词向量的更多乐趣

代码

第三部分的代码在这里

单词的数值表示

现在我们有了训练好的模型,对单词有一些语义理解,我们应该如何使用它? 如果你看它的背后,第 2 部分训练的 Word2Vec 模型由词汇表中每个单词的特征向量组成,存储在一个名为syn0numpy数组中:

>>> # 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 数据集上使用了一种名为段落向量的算法,来生成迄今为止最先进的一些结果。在某种程度上,它比我们在这里尝试的方法更好,因为向量平均和聚类会丢失单词顺序,而段落向量会保留单词顺序信息。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容