1. 程式人生 > >文本分布式表示(二):用tensorflow和word2vec訓練詞向量

文本分布式表示(二):用tensorflow和word2vec訓練詞向量

sig 財經 left 調用 采樣 cto imp gensim average

博客園的markdown用起來太心塞了,現在重新用其他編輯器把這篇博客整理了一下。

目前用word2vec算法訓練詞向量的工具主要有兩種:gensim 和 tensorflow。gensim中已經封裝好了word2vec這個包,用起來很方便,只要把文本處理成規範的輸入格式,寥寥幾行代碼就能訓練詞向量。這樣比較適合在做項目時提高效率,但是對理解算法的原理幫助不大。相比之下,用tensorflow來訓練word2vec比較麻煩,生成batch、定義神經網絡的各種參數,都要自己做,但是對於理解算法原理,還是幫助很大。

所以這次就用開源的tensorflow實現word2vec的代碼,來訓練詞向量,並進行可視化。這次的語料來自於一個新聞文本分類的項目。新聞文本文檔非常大,有 120多M,這裏提供百度網盤下載:https://pan.baidu.com/s/1yeFORUVr3uDdTLUYqDraKA 提取碼:c98y 。

詞向量訓練出來有715M,真是醉了!好,開始吧。

一、用tensorflow和word2vec訓練中文詞向量

這次用到的是skip-gram模型。新聞文本的訓練語料是一個txt文檔,每行是一篇新聞,開頭兩個字是標簽:體育、財經、娛樂等,後面是新聞的內容,開頭和內容之間用制表符 ‘\t‘ 隔開。

(一)讀取文本數據,分詞,清洗,生成符合輸入格式的內容

這裏是用jieba進行分詞的,加載了停用詞表,不加載的話會發現 “的、一 ”之類的詞是排在前列的,而負采樣是從詞頻高的詞開始,因此會對結果產生不好的影響。

處理得到的規範格式的輸入是這樣的,把所有新聞文本分詞後做成一個列表:[‘體育‘, ‘馬‘, ‘曉‘, ‘旭‘, ‘意外‘, ‘受傷‘, ‘國奧‘, ‘警惕‘, ‘無奈‘, ‘大雨‘, ...]。

#encoding=utf8
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import collections
import math
import os
import random
import zipfile

import numpy as np
from six.moves import xrange  
import tensorflow as tf
import jieba
from itertools import chain """第一步:讀取數據,用jieba進行分詞,去除停用詞,生成詞語列表。""" def read_data(filename): f = open(filename, r, encoding=utf-8) stop_list = [i.strip() for i in open(ChineseStopWords.txt,r,encoding=utf-8)] news_list = [] for line in f: if line.strip(): news_cut = list(jieba.cut(‘‘.join(line.strip().split(\t)),cut_all=False,HMM=False)) news_list.append([word.strip() for word in news_cut if word not in stop_list and len( word.strip())>0]) # line是:‘體育\t馬曉旭意外受傷讓國奧警惕 無奈大雨格外...‘這樣的新聞文本,標簽是‘體育’,後面是正文,中間用‘\t‘分開。 # news_cut : [‘體育‘, ‘馬‘, ‘曉‘, ‘旭‘, ‘意外‘, ‘受傷‘, ‘讓‘, ‘國奧‘, ‘警惕‘, ‘ ‘, ‘無奈‘,...], 按‘\t‘來拆開 # news_list為[[‘體育‘, ‘馬‘, ‘曉‘, ‘旭‘, ‘意外‘, ‘受傷‘, ‘國奧‘, ‘警惕‘, ‘無奈‘, ...],去掉了停用詞和空格。 news_list = list(chain.from_iterable(news_list)) # 原列表中的元素也是列表,把它拉成一個列表。[‘體育‘, ‘馬‘, ‘曉‘, ‘旭‘, ‘意外‘, ‘受傷‘, ‘國奧‘, ‘警惕‘, ‘無奈‘, ‘大雨‘, ...] f.close() return news_list filename = data/cnews/cnews.train.txt words = read_data(filename) # 把所有新聞分詞後做成了一個列表:[‘體育‘, ‘馬‘, ‘曉‘, ‘旭‘, ‘意外‘, ‘受傷‘, ‘國奧‘, ‘警惕‘, ‘無奈‘, ‘大雨‘, ...]

(二)建立詞匯表

這一步是把得到的詞列表中的詞去重,統計詞頻,然後按詞頻從高到低排序,構建一個詞匯表:{‘UNK‘: 0, ‘中‘: 1, ‘月‘: 2, ‘年‘: 3, ‘說‘: 4, ‘中國‘: 5,...},key是詞,‘中’的詞頻最高,放在前面,value是每個詞的索引。

為了便於根據索引來取詞,因此把詞匯表這個字典進行反轉得到:reverse_dictionary: {0: ‘UNK‘, 1: ‘中‘, 2: ‘月‘, 3: ‘年‘, 4: ‘說‘, 5: ‘中國‘,...}。

同時還得到了上面words這個列表中每個詞的索引: [259, 512, 1023, 3977, 1710, 1413, 12286, 6118, 2417, 18951, ...]。

詞語(含重復)共有1545萬多個,去重復後得到19萬多個。

"""第二步:建立詞匯表"""
words_size = len(words)      
vocabulary_size = len(set(words))     
print(Data size, vocabulary_size)     
# 共有15457860個,重復的詞非常多。
# 詞匯表中有196871個不同的詞。

def build_dataset(words):
    
    count = [[UNK, -1]]
    count.extend(collections.Counter(words).most_common(vocabulary_size - 1))    
    dictionary = dict()
    # 統計詞頻較高的詞,並得到詞的詞頻。
    # count[:10]: [[‘UNK‘, -1], (‘中‘, 96904), (‘月‘, 75567), (‘年‘, 73174), (‘說‘, 56859), (‘中國‘, 55539), (‘日‘, 54018), (‘%‘, 52982), (‘基金‘, 47979), (‘更‘, 40396)]
    #  盡管取了詞匯表前(196871-1)個詞,但是前面加上了一個用來統計未知詞的元素,所以還是196871個詞。之所以第一個元素是列表,是為了便於後面進行統計未知詞的個數。
    
    for word, _ in count:
        dictionary[word] = len(dictionary)
    # dictionary: {‘UNK‘: 0, ‘中‘: 1, ‘月‘: 2, ‘年‘: 3, ‘說‘: 4, ‘中國‘: 5,...},是詞匯表中每個字是按照詞頻進行排序後的,字和它的索引構成的字典。
    
    data = list()
    unk_count = 0
    for word in words:
        if word in dictionary:
            index = dictionary[word]
        else:
            index = 0  
            unk_count += 1
        data.append(index)
        # data是words這個文本列表中每個詞對應的索引。元素和words一樣多,是15457860個
        # data[:10] : [259, 512, 1023, 3977, 1710, 1413, 12286, 6118, 2417, 18951]
        
    count[0][1] = unk_count       
    reverse_dictionary = dict(zip(dictionary.values(), dictionary.keys()))       
    return data, count, dictionary, reverse_dictionary
   # 位置詞就是‘UNK‘本身,所以unk_count是1。[[‘UNK‘, 1], (‘中‘, 96904), (‘月‘, 75567), (‘年‘, 73174), (‘說‘, 56859), (‘中國‘, 55539),...]
   # 把字典反轉:{0: ‘UNK‘, 1: ‘中‘, 2: ‘月‘, 3: ‘年‘, 4: ‘說‘, 5: ‘中國‘,...},用於根據索引取詞。

data, count, dictionary, reverse_dictionary = build_dataset(words)
# data[:5] : [259, 512, 1023, 3977, 1710]
# count[:5]: [[‘UNK‘, 1], (‘中‘, 96904), (‘月‘, 75567), (‘年‘, 73174), (‘說‘, 56859)]
# reverse_dictionary: {0: ‘UNK‘, 1: ‘中‘, 2: ‘月‘, 3: ‘年‘, 4: ‘說‘, 5: ‘中國‘,...}

del words        
print(Most common words (+UNK), count[:5])
print(Sample data, data[:10], [reverse_dictionary[i] for i in data[:10]])
# 刪掉不同的數據,釋放內存。
# Most common words (+UNK) [[‘UNK‘, 1], (‘中‘, 96904), (‘月‘, 75567), (‘年‘, 73174), (‘說‘, 56859)]
# Sample data [259, 512, 1023, 3977, 1710, 1413, 12286, 6118, 2417, 18951] [‘體育‘, ‘馬‘, ‘曉‘, ‘旭‘, ‘意外‘, ‘受傷‘, ‘國奧‘, ‘警惕‘, ‘無奈‘, ‘大雨‘]

data_index = 0

(三)為skip-gram模型生成訓練的batch

skip-gram模型是根據中心詞來預測上下文詞的,拿[‘體育‘, ‘馬‘, ‘曉‘, ‘旭‘, ‘意外‘, ‘受傷‘, ‘國奧‘, ‘警惕‘, ‘無奈‘, ‘大雨‘]來舉例,滑動窗口為5,那麽中心詞前後各2個詞,第一個中心詞為 ‘曉’時,上下文詞為(體育,馬,旭,意外)這樣一個沒有順序的詞袋。

那麽生成的樣本可能為:[(曉,馬),(曉,意外),(曉,體育),(曉,旭)],上下文詞不是按順序排列的。

""" 第三步:為skip-gram模型生成訓練的batch """
def generate_batch(batch_size, num_skips, skip_window):
    global data_index
    assert batch_size % num_skips == 0
    assert num_skips <= 2 * skip_window
    batch = np.ndarray(shape=(batch_size), dtype=np.int32)          
    labels = np.ndarray(shape=(batch_size, 1), dtype=np.int32)     
    span = 2 * skip_window + 1  
    buffer = collections.deque(maxlen=span)      
    # 這裏先取一個數量為8的batch看看,真正訓練時是以128為一個batch的。
    #  構造一個一列有8個元素的ndarray對象
    # deque 是一個雙向列表,限制最大長度為5, 可以從兩端append和pop數據。
    
    for _ in range(span): 
        buffer.append(data[data_index])
        data_index = (data_index + 1) % len(data)      
        # 循環結束後得到buffer為 deque([259, 512, 1023, 3977, 1710], maxlen=5),也就是取到了data的前五個值, 對應詞語列表的前5個詞。
        
    for i in range(batch_size // num_skips):      
        target = skip_window        
        targets_to_avoid = [skip_window] 
        
         # i取值0,1,是表示一個batch能取兩個中心詞
         # target值為2,意思是中心詞在buffer這個列表中的位置是2。
         # 列表是用來存已經取過的詞的索引,下次就不能再取了,從而把buffer中5個元素不重復的取完。
         
        for j in range(num_skips):                                                    # j取0,1,2,3,意思是在中心詞周圍取4個詞。
            while target in targets_to_avoid:
                target = random.randint(0, span - 1)                            # 2是中心詞的位置,所以j的第一次循環要取到不是2的數字,也就是取到0,1,3,4其中的一個,才能跳出循環。
            targets_to_avoid.append(target)                                       # 把取過的上下文詞的索引加進去。
            batch[i * num_skips + j] = buffer[skip_window]               # 取到中心詞的索引。前四個元素都是同一個中心詞的索引。
            labels[i * num_skips + j, 0] = buffer[target]                     # 取到中心詞的上下文詞的索引。一共會取到上下各兩個。
        buffer.append(data[data_index])                                          # 第一次循環結果為buffer:deque([512, 1023, 3977, 1710, 1413], maxlen=5),
                                                                                                       # 所以明白了為什麽限制為5,因為可以把第一個元素去掉。這也是為什麽不用list。
        data_index = (data_index + 1) % len(data)
    return batch, labels

batch, labels = generate_batch(batch_size=8, num_skips=4, skip_window=2)
# batch是 array([1023, 1023, 1023, 1023, 3977, 3977, 3977, 3977], dtype=int32),8個batch取到了2個中心詞,一會看樣本的輸出結果就明白了。

for i in range(8):
    print(batch[i], reverse_dictionary[batch[i]],
        ->, labels[i, 0], reverse_dictionary[labels[i, 0]])
‘‘‘
打印的結果如下,突然明白說為什麽說取樣本的時候是用bag of words

1023 曉 -> 3977 旭
1023 曉 -> 1710 意外
1023 曉 -> 512 馬
1023 曉 -> 259 體育
3977 旭 -> 512 馬
3977 旭 -> 1023 曉
3977 旭 -> 1710 意外
3977 旭 -> 1413 受傷

‘‘‘

(四)定義skip-gram模型

這裏面涉及的一些tensorflow的知識點在第二部分有寫,這裏也說明一下。

首先 tf.Graph().as_default() 表示將新生成的圖作為整個 tensorflow 運行環境的默認圖,如果只有一個主線程不寫也沒有關系,tensorflow 裏面已經存好了一張默認圖,可以使用tf.get_default_graph() 來調用(顯示這張默認紙),當你有多個線程就可以創造多個tf.Graph(),就是你可以有一個畫圖本,有很多張圖紙,而默認的只有一張,可以自己指定。

tf.random_uniform這個方法是用來產生-1到1之間的均勻分布, 看作是初始化隱含層和輸出層之間的詞向量矩陣。

nce_loss函數是tensorflow中常用的損失函數,可以將其理解為其將多元分類分類問題強制轉化為了二元分類問題,num_sampled參數代表將選取負例的個數。

這個損失函數通過 sigmoid cross entropy來計算output和label的loss,從而進行反向傳播。這個函數把最後的問題轉化為了(num_sampled ,num_True)這個兩分類問題,然後每個分類問題用了交叉熵損失函數。

""" 第四步:定義和訓練skip-gram模型"""

batch_size = 128            
embedding_size = 300  
skip_window = 2             
num_skips = 4                
num_sampled = 64        
# 上面那個數量為8的batch只是為了展示以下取樣的結果,實際上是batch-size 是128。
# 詞向量的維度是300維。
# 左右兩邊各取兩個詞。
# 要取4個上下文詞,同一個中心詞也要重復取4次。
# 負采樣的負樣本數量為64

graph = tf.Graph()         

with graph.as_default():                   
    #  把新生成的圖作為整個 tensorflow 運行環境的默認圖,詳見第二部分的知識點。
    
    train_inputs = tf.placeholder(tf.int32, shape=[batch_size])        
    train_labels = tf.placeholder(tf.int32, shape=[batch_size, 1])      
    
    embeddings = tf.Variable(tf.random_uniform([vocabulary_size, embedding_size], -1.0, 1.0)) 
    embed = tf.nn.embedding_lookup(embeddings, train_inputs)    
    #產生-1到1之間的均勻分布, 看作是初始化隱含層和輸出層之間的詞向量矩陣。
    #用詞的索引在詞向量矩陣中得到對應的詞向量。shape=(128, 300)

    
    nce_weights = tf.Variable(tf.truncated_normal([vocabulary_size, embedding_size], stddev=1.0 / math.sqrt(embedding_size)))
    # 初始化損失(loss)函數的權重矩陣和偏置矩陣
    # 生成的值服從具有指定平均值和合理標準偏差的正態分布,如果生成的值大於平均值2個標準偏差則丟棄重新生成。這裏是初始化權重矩陣。
    # 對標準方差進行了限制的原因是為了防止神經網絡的參數過大。
    
    nce_biases = tf.Variable(tf.zeros([vocabulary_size])) 
    loss = tf.reduce_mean(tf.nn.nce_loss(weights=nce_weights, biases=nce_biases,
                     labels=train_labels, inputs=embed, num_sampled=num_sampled, num_classes=vocabulary_size))
    # 初始化偏置矩陣,生成了一個vocabulary_size * 1大小的零矩陣。
    # 這個tf.nn.nce_loss函數把多分類問題變成了正樣本和負樣本的二分類問題。用的是邏輯回歸的交叉熵損失函數來求,而不是softmax  。
    
    optimizer = tf.train.GradientDescentOptimizer(1.0).minimize(loss)

    norm = tf.sqrt(tf.reduce_sum(tf.square(embeddings), 1, keepdims=True))       
    normalized_embeddings = embeddings / norm
    # shape=(196871, 1), 對詞向量矩陣進行歸一化
    
    init = tf.global_variables_initializer()          

(五)訓練skip-gram模型

接下來就開始訓練了,這裏沒什麽好說的,就是訓練神經網絡,不斷更新詞向量矩陣,然後訓練完後,得到最終的詞向量矩陣。源碼中還有一個展示鄰近詞語的代碼,我覺得沒啥用,刪掉了。

num_steps = 10    
with tf.Session(graph=graph) as session:
    
    init.run()
    print(initialized.)
    
    average_loss = 0
  
    for step in xrange(num_steps):
        batch_inputs, batch_labels = generate_batch(batch_size, num_skips, skip_window)
        feed_dict = {train_inputs: batch_inputs, train_labels: batch_labels}
        
        _, loss_val = session.run([optimizer, loss], feed_dict=feed_dict)
        average_loss += loss_val
        final_embeddings = normalized_embeddings.eval()
        print(final_embeddings)        
        print("*"*20)
        if step % 2000 == 0:
            if step > 0:
                average_loss /= 2000
            print("Average loss at step ", step, ": ", average_loss)
            average_loss = 0
            
    final_embeddings = normalized_embeddings.eval()      
    # 訓練得到最後的詞向量矩陣。
    print(final_embeddings)
    fp=open(vector.txt,w,encoding=utf8)
    for k,v in reverse_dictionary.items():
        t=tuple(final_embeddings[k])         
        s=‘‘
        for i in t:
            i=str(i)
            s+=i+" "               
        fp.write(v+" "+s+"\n")         
        # s為‘0.031514477 0.059997283 ...‘  , 對於每一個詞的詞向量中的300個數字,用空格把他們連接成字符串。
        #把詞向量寫入文本文檔中。不過這樣就成了字符串,我之前試過用np保存為ndarray格式,這裏是按源碼的保存方式。

    fp.close()

(六)詞向量可視化

用sklearn.manifold.TSNE這個方法來進行可視化,實際上作用不是畫圖,而是降維,因為詞向量是300維的,降到2維或3維才能可視化。

這裏用到了t-SNE這一種集降維與可視化於一體的技術,t-SNE 的主要目的是高維數據的可視化,當數據嵌入二維或三維時,效果最好。

值得註意的一點是,matplotlib默認的字體是不含中文的,所以沒法顯示中文註釋,要自己導入中文字體。在默認狀態下,matplotlb無法在圖表中使用中文。

matplotlib中有一個字體管理器——matplotlib.Font_manager,通過該管理器的方法——matplotlib.Font_manager.FontProperties(fname)可以指定一個ttf字體文件作為圖表使用的字體。

"""第六步:詞向量可視化 """
def plot_with_labels(low_dim_embs, labels, filename=tsne.png):
    assert low_dim_embs.shape[0] >= len(labels), "More labels than embeddings"
    plt.figure(figsize=(18, 18))  # in inches
    myfont = font_manager.FontProperties(fname=/home/dyy/Downloads/font163/simhei.ttf)          #加載中文字體
    for i, label in enumerate(labels):
        x, y = low_dim_embs[i, :]
        plt.scatter(x, y)
        plt.annotate(label,
                 xy=(x, y),
                 xytext=(5, 2),                           #添加註釋, xytest是註釋的位置。然後添加顯示的字體。
                 textcoords=offset points,
                 ha=right,
                 va=bottom,
                 fontproperties=myfont)
    
    plt.savefig(filename)
    plt.show()

try:
    from sklearn.manifold import TSNE
    import matplotlib.pyplot as plt
    from matplotlib import font_manager            
    #這個庫很重要,因為需要加載字體,原開源代碼裏是沒有的。

    tsne = TSNE(perplexity=30, n_components=2, init=pca, n_iter=5000)         
    plot_only = 500
    low_dim_embs = tsne.fit_transform(final_embeddings[:plot_only, :])           
    labels = [reverse_dictionary[i] for i in xrange(plot_only)]   
    # tsne: 一個降維的方法,降維後維度是2維,使用‘pca‘來初始化。
    # 取出了前500個詞的詞向量,把300維減低到2維。
    
    plot_with_labels(low_dim_embs, labels)

except ImportError:
    print("Please install sklearn, matplotlib, and scipy to visualize embeddings.")

可視化的結果:
技術分享圖片

詞向量(太大了,打開也要花不少時間)
技術分享圖片

文本分布式表示(二):用tensorflow和word2vec訓練詞向量