零基礎入門NLP - 基於深度學習的文本分類2

介紹

在上一節中, 介紹了FastText中的兩種詞向量方法, CBoW和Skip-gram. 這裏咱們介紹一種相似的方法word2vec, 並使用Gensim來訓練咱們的word2vec.html

word2vec

來自Google的Tomas Mikolov等人於2013年在論文Distributed Representations of Words and Phrases and their Compositionality中提出了word2vec詞表示方法, word2vec能夠分爲兩種CBoW和Skip-gram模型, 但和上一節中提到的CBoW和Skip-gram有所不一樣.python

能夠設想, 按照上一節的思路, 咱們訓練CBoW或Skip-gram模型, 最終網絡輸出的是每一個詞機率分佈(softmax的輸出), 而一般而言, 咱們的字典都包含了大量的詞, 這會致使大量的softmax計算, 顯然, 這是很難接受的. 那麼如何提升效率呢.
下面就介紹兩種提升效率的兩種方法網絡

Hierarchical Softmax

word2vec也使用了CBoW和Skip-gram來訓練模型, 但並無採用傳統的DNN結構.數據結構

最早優化使用的數據結構是用霍夫曼樹來代替隱藏層和輸出層的神經元,霍夫曼樹的葉子節點起到輸出層神經元的做用,葉子節點的個數即爲詞彙表的小大。 而內部節點則起到隱藏層神經元的做用。app

具體如何用霍夫曼樹來進行CBOW和Skip-Gram的訓練咱們在下一節講,這裏咱們先複習下霍夫曼樹。dom

霍夫曼樹的創建其實並不難,過程以下:post

輸入:權值爲(w1,w2,...wn)的n個節點優化

輸出:對應的霍夫曼樹ui

  1. 將(w1,w2,...wn)看作是有n棵樹的森林,每一個樹僅有一個節點。
  2. 在森林中選擇根節點權值最小的兩棵樹進行合併,獲得一個新的樹,這兩顆樹分佈做爲新樹的左右子樹。新樹的根節點權重爲左右子樹的根節點權重之和。
  3. 將以前的根節點權值最小的兩棵樹從森林刪除,並把新樹加入森林。
  4. 重複步驟2和3直到森林裏只有一棵樹爲止。

下面咱們用一個具體的例子來講明霍夫曼樹創建的過程,咱們有(a,b,c,d,e,f)共6個節點,節點的權值分佈是(20,4,8,6,16,3)。編碼

首先是最小的b和f合併,獲得的新樹根節點權重是7.此時森林裏5棵樹,根節點權重分別是20,8,6,16,7。此時根節點權重最小的6,7合併,獲得新子樹,依次類推,最終獲得下面的霍夫曼樹。

通常獲得霍夫曼樹後咱們會對葉子節點進行霍夫曼編碼,因爲權重高的葉子節點越靠近根節點,而權重低的葉子節點會遠離根節點,這樣咱們的高權重節點編碼值較短,而低權重值編碼值較長。這保證的樹的帶權路徑最短,也符合咱們的信息論,即咱們但願越經常使用的詞擁有更短的編碼。如何編碼呢?通常對於一個霍夫曼樹的節點(根節點除外),能夠約定左子樹編碼爲0,右子樹編碼爲1.如上圖,則能夠獲得c的編碼是00。

假設字典包含$N$個詞, 則使用哈夫曼二叉樹以前的softmax層的複雜度爲$O(N)$, 而使用哈夫曼二叉樹後, 複雜度降爲$O(log(N))$.

Negative Sample

Hierarchical Softmax確實能夠在很大程度上提升模型的效率, 使用霍夫曼樹來代替傳統的神經網絡,能夠提升模型訓練的效率。可是若是咱們的訓練樣本里的中心詞w是一個很生僻的詞,那麼就得在霍夫曼樹中辛苦的向下走好久了。能不能不用搞這麼複雜的一顆霍夫曼樹,將模型變的更加簡單呢?

nagative sampling(負採樣)就是一種替代Hierarchical softmax的方法.

好比咱們有一個訓練樣本,中心詞是$w$,它周圍上下文共有2c個詞,記爲context(w)。因爲這個中心詞w,的確和context(w)相關存在,所以它是一個真實的正例。經過Negative Sampling採樣,咱們獲得neg個和w不一樣的中心詞$w_i,i=1,2,..neg$,這樣context(w)和$w_i$就組成了neg個並不真實存在的負例。利用這一個正例和neg個負例,咱們進行二元邏輯迴歸,獲得負採樣對應每一個詞$w_i$對應的模型參數$\theta_i$,和每一個詞的詞向量。

從上面的描述能夠看出,Negative Sampling因爲沒有采用霍夫曼樹,每次只是經過採樣neg個不一樣的中心詞作負例,就能夠訓練模型,所以整個過程要比Hierarchical Softmax簡單。

具體細節能夠查閱paper

使用gensim訓練word2vec

導入庫

import logging
import random

import numpy as np
import torch

logging.basicConfig(level=logging.INFO, format='%(asctime)-15s %(levelname)s: %(message)s')

# set seed 
def seed_all(seed):
    random.seed(seed)
    np.random.seed(seed)
    torch.cuda.manual_seed(seed)
    torch.manual_seed(seed)
    
seed_all(0)

劃分數據

# split data to 10 fold
fold_num = 10
data_file = '../data/train_set.csv'
import pandas as pd

def all_data2fold(fold_num, num=10000):
    fold_data = []
    f = pd.read_csv(data_file, sep='\t', encoding='UTF-8')
    texts = f['text'].tolist()[:num]
    labels = f['label'].tolist()[:num]

    total = len(labels)

    index = list(range(total))
    np.random.shuffle(index)

    all_texts = []
    all_labels = []
    for i in index:
        all_texts.append(texts[i])
        all_labels.append(labels[i])

    label2id = {}
    for i in range(total):
        label = str(all_labels[i])
        if label not in label2id:
            label2id[label] = [i]
        else:
            label2id[label].append(i)

    all_index = [[] for _ in range(fold_num)]
    for label, data in label2id.items():
        # print(label, len(data))
        batch_size = int(len(data) / fold_num)
        other = len(data) - batch_size * fold_num
        for i in range(fold_num):
            cur_batch_size = batch_size + 1 if i < other else batch_size
            # print(cur_batch_size)
            batch_data = [data[i * batch_size + b] for b in range(cur_batch_size)]
            all_index[i].extend(batch_data)

    batch_size = int(total / fold_num)
    other_texts = []
    other_labels = []
    other_num = 0
    start = 0
    for fold in range(fold_num):
        num = len(all_index[fold])
        texts = [all_texts[i] for i in all_index[fold]]
        labels = [all_labels[i] for i in all_index[fold]]

        if num > batch_size:
            fold_texts = texts[:batch_size]
            other_texts.extend(texts[batch_size:])
            fold_labels = labels[:batch_size]
            other_labels.extend(labels[batch_size:])
            other_num += num - batch_size
        elif num < batch_size:
            end = start + batch_size - num
            fold_texts = texts + other_texts[start: end]
            fold_labels = labels + other_labels[start: end]
            start = end
        else:
            fold_texts = texts
            fold_labels = labels

        assert batch_size == len(fold_labels)

        # shuffle
        index = list(range(batch_size))
        np.random.shuffle(index)

        shuffle_fold_texts = []
        shuffle_fold_labels = []
        for i in index:
            shuffle_fold_texts.append(fold_texts[i])
            shuffle_fold_labels.append(fold_labels[i])

        data = {'label': shuffle_fold_labels, 'text': shuffle_fold_texts}
        fold_data.append(data)

    logging.info("Fold lens %s", str([len(data['label']) for data in fold_data]))

    return fold_data


fold_data = all_data2fold(10)

訓練word2vec

# build train data for word2vec
fold_id = 9

train_texts = []
for i in range(0, fold_id):
    data = fold_data[i]
    train_texts.extend(data['text'])
    
logging.info('Total %d docs.' % len(train_texts))
logging.info('Start training...')
from gensim.models.word2vec import Word2Vec

num_features = 100     # Word vector dimensionality
num_workers = 8       # Number of threads to run in parallel

train_texts = list(map(lambda x: list(x.split()), train_texts))
model = Word2Vec(train_texts, workers=num_workers, size=num_features)
model.init_sims(replace=True)

# save model
model.save("./word2vec.bin")

加載模型

# load model
model = Word2Vec.load("./word2vec.bin")

# convert format
model.wv.save_word2vec_format('./word2vec.txt', binary=False)

總結

word2vec使用了CBoW和Skip-gram模型, 但又和它們有所不一樣, word2vec提出了2種不一樣的方式來解決機率輸出層複雜度太高的問題.

水平有限, 有關Hierarchical Softmax​和Negative sample的具體實現方式以及訓練細節有待進一步探索. 只能以後有時間再好好讀一讀paper了.

Reference

[1] paper link

[2] 使用gensim訓練word2vec

[3] word2vec原理(二) 基於Hierarchical Softmax的模型

[4] word2vec原理(三) 基於Negative Sampling的模型

[5] gensim train word2vec tutorial

相關文章
相關標籤/搜索