文本情感分析(二):基於word2vec、glove和fasttext詞向量的文本表示


上一篇博客用詞袋模型,包括詞頻矩陣、Tf-Idf矩陣、LSA和n-gram構造文本特征,做了Kaggle上的電影評論情感分類題。

這篇博客還是關於文本特征工程的,用詞嵌入的方法來構造文本特征,也就是用word2vec、glove和fasttext詞向量進行文本表示,訓練隨機森林分類器。

一、訓練word2vec和fasttext詞向量

Kaggle情感分析題給出了三個數據集,一個是帶標簽的訓練集,共25000條評論,一個是測試集,無標簽的,用來做預測並提交結果,這兩個數據集是上一篇文章里我們用過的。

此外還有一個無標簽的數據集,有50000條評論,不用太可惜了。我們可以想到,用無標簽的數據可以訓練word2vec詞向量,進行詞嵌入。與詞袋模型相比,word2vec詞向量能解決文本表示維度過高的問題,並且把單詞之間的位置信息考慮進去了。或許,用word2vec詞向量進行文本表示,能取得更好的預測結果。

另外,我們也可以訓練fasttext詞向量。fasttext這個模型就是為了文本分類而造出來的,詞向量是其副產品,它的結構和word2vec的CBOW模型的結構類似,但是輸入是整篇文本而不是上下文信息,而且用字符級別的n-gram來得到單詞的詞向量表示,捕捉有相同后綴的詞的語義關聯。

gensim中集成了訓練word2vec詞向量和fasttext詞向量的包,用法非常類似。不過貌似gensim中的fasttext包只能用來訓練詞向量,不能用來做fasttext文本分類。

首先導入所需要的庫。

import os,re
import numpy as np
import pandas as pd

from bs4 import BeautifulSoup
from gensim import models

接着讀取有標簽的訓練數據和無標簽的數據,把影評合並到一個列表中。

"""讀取數據,包括有標簽的和無標簽的數據"""

# 定義讀取數據的函數
def load_dataset(name, nrows=None):
    datasets = {
        'unlabeled_train': 'unlabeledTrainData.tsv',
        'labeled_train': 'labeledTrainData.tsv',
        'test': 'testData.tsv'
    }
    if name not in datasets:
        raise ValueError(name)
    data_file = os.path.join('..', 'data', datasets[name])
    df = pd.read_csv(data_file, sep='\t', escapechar='\\', nrows=nrows)
    print('Number of reviews: {}'.format(len(df)))
    return df

# 讀取有標簽和無標簽的數據
df_labeled = load_dataset('labeled_train')
df_unlabeled = load_dataset('unlabeled_train')

sentences = []

for s in df_labeled['review']:
    sentences.append(s)

for s in df_unlabeled['review']:
    sentences.append(s)
    
print("一共加載了",len(sentences),"條評論。")
Number of reviews: 25000
Number of reviews: 50000
一共加載了 75000 條評論。

接着進行數據預處理,處理成gensim所需要的格式。這里非常關鍵,我還摸索了一陣,才知道什么輸入格式是正確的。

其實輸入格式是這樣的,假設有兩篇文本,那么處理成 [ ['with', 'all', 'this', 'stuff', 'going',...], ['movie', 'but', 'mj', 'and', 'most',...]]的格式,每篇文本是一個列表,列表元素為單個單詞。這個很容易做到,因為英文不需要進行分詞,用text.split()按照空格進行切分就行。

由於word2vec依賴於上下文,而上下文有可能就是停詞,所以這里選擇不去停用詞。

"""數據預處理,去html標簽、去非字母的字符"""

eng_stopwords = {}.fromkeys([ line.rstrip() for line in open('../stopwords.txt')])

# 可以選擇是否去停用詞,由於word2vec依賴於上下文,而上下文有可能就是停詞。
# 因此對於word2vec,我們可以不用去停詞。
def clean_text(text, remove_stopwords=False):
    text = BeautifulSoup(text,'html.parser').get_text()
    text = re.sub(r'[^a-zA-Z]', ' ', text)
    words = text.lower().split()
    if remove_stopwords:
        words = [w for w in words if w not in eng_stopwords]
    return words

sentences = [clean_text(s) for s in sentences]
# 這里可以說是最關鍵的,gensim需要的格式就是把每條評論弄成['with', 'all', 'this', 'stuff', 'going',...]的格式。
# 再次強調,這里最關鍵,格式不對則沒法學習。

現在就可以輸入進去訓練詞向量了。訓練好之后,有兩種保持模型的方式,一種是把訓練的模型本身保存下來,是一個二進制格式的文件,打開以后看不到單詞和詞向量,但是以后可以繼續用更多的數據進行訓練,第二種是把單詞和對應的詞向量以txt的格式保存,不能再追加訓練,但是打開后可以看到單詞和詞向量。下面的代碼以這兩種方式分別保存了模型。

"""打印日志信息"""

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

""""設定詞向量訓練的參數,開始訓練詞向量"""

num_features = 300      # 詞向量取300維
min_word_count = 40     # 詞頻小於40個單詞就去掉
num_workers = 4         # 並行運行的線程數
context = 10            # 上下文滑動窗口的大小
model_ = 0              # 使用CBOW模型進行訓練

model_name = '{}features_{}minwords_{}context.model'.format(num_features, min_word_count, context)

print('Training model...')
model = models.Word2Vec(sentences, workers=num_workers, \
            size=num_features, min_count = min_word_count, \
            window = context, sg=model_)

# 保存模型
# 第一種方法保存的文件不能利用文本編輯器查看,但是保存了訓練的全部信息,可以在讀取后追加訓練
# 后一種方法保存為word2vec文本格式,但是保存時丟失了詞匯樹等部分信息,不能追加訓練

model.save(os.path.join('..', 'models', model_name))

model.wv.save_word2vec_format(os.path.join('..','models','word2vec_txt.txt'),binary = False)

加載保存好的模型,並取出詞向量。

# 加載模型,根據保持時的格式不同,有兩種加載方式

model = models.Word2Vec.load(os.path.join('..', 'models', model_name))
model_txt = models.KeyedVectors.load_word2vec_format(os.path.join('..','models','word2vec_txt.txt'),binary = False)

# 可以同時取出一個句子中單詞的詞向量
model.wv[['man','woman','guy']]

檢驗一下模型訓練的效果,查看和 man 這個單詞最相關的詞,可以看到,結果還不錯。

model.wv.most_similar("man")
[('woman', 0.6039960384368896),
 ('lady', 0.5690498948097229),
 ('lad', 0.5434065461158752),
 ('guy', 0.4913134276866913),
 ('person', 0.4771265387535095),
 ('monk', 0.47647857666015625),
 ('widow', 0.47423964738845825),
 ('millionaire', 0.4719209671020508),
 ('soldier', 0.4717007279396057),
 ('men', 0.46545034646987915)]

接着訓練fasttext詞向量,輸入數據的格式要求和word2vec一樣,所以我們直接用上面的數據開始訓練。

要吐槽一下,盡管據說fasttext做文本分類賊快,可是訓練詞向量的過程非常慢,感覺比word2vec慢多了,內存也時不時顯示99%,嚇死人。

model_name_2 = 'fasttext.model'

print('Training model...')
model_2 = models.FastText(sentences, size=num_features, window=context, min_count=min_word_count,\
                        sg = model_, min_n = 2 , max_n = 3)

# 保存模型
# 第一種方法保存的文件不能利用文本編輯器查看,但是保存了訓練的全部信息,可以在讀取后追加訓練
# 后一種方法保存為word2vec文本格式,但是保存時丟失了詞匯樹等部分信息,不能追加訓練
model_2.save(os.path.join('..', 'models', model_name_2))
model_2.wv.save_word2vec_format(os.path.join('..','models','fasttext.txt'),binary = False)

 查看和man這個單詞最相關的詞,還是非常慢。從結果來看,和word2vec大有不同,找出來的詞后綴都是man。

model_2.wv.most_similar("man")
[('woman', 0.6353151798248291),
 ('boman', 0.6015676856040955),
 ('wolfman', 0.5951900482177734),
 ('wyman', 0.5888750553131104),
 ('snowman', 0.5807067155838013),
 ('madman', 0.5781949162483215),
 ('gunman', 0.5617127418518066),
 ('henchman', 0.5536723136901855),
 ('guffman', 0.5454517006874084),
 ('kidman', 0.5268094539642334)]

二、用word2vec、glove和fasttext詞向量進行文本表示

好,下面分別用word2vec、glove和fasttext詞向量做電影評論的文本表示,再次訓練隨機森林分類器,看哪種詞向量的效果更好。

重開了一個jupyter notebook。首先導入所需要的庫。

import os
import re
import numpy as np
import pandas as pd

from bs4 import BeautifulSoup

from nltk.corpus import stopwords

from gensim import models

from sklearn.ensemble import RandomForestClassifier

from sklearn import metrics 

 讀取訓練集數據。

"""讀取訓練集數據"""

def load_dataset(name, nrows=None):
    datasets = {
        'unlabeled_train': 'unlabeledTrainData.tsv',
        'labeled_train': 'labeledTrainData.tsv',
        'test': 'testData.tsv'
    }
    if name not in datasets:
        raise ValueError(name)
    data_file = os.path.join('..', 'data', datasets[name])
    df = pd.read_csv(data_file, sep='\t', escapechar='\\', nrows=nrows)
    print('Number of reviews: {}'.format(len(df)))
    return df

df = load_dataset('labeled_train')

讀取訓練好的word2vec詞向量,和預訓練的glove詞向量(需要先下載glove詞向量),備用。怕內存受不了,先不加載fasttext詞向量。

"""讀取訓練好的word2vec模型"""

model_name_w2v = '300features_40minwords_10context.model'
word2vec_embedding = models.Word2Vec.load(os.path.join('..', 'models', model_name_w2v))

"""讀取glove詞向量"""

glove_embedding = {}
f = open('../glove.6B/glove.6B.300d.txt', encoding='utf-8')
for line in f:
    values = line.split()
    word = values[0]
    coefs = np.asarray(values[1:], dtype='float32')
    glove_embedding[word] = coefs
f.close()

將訓練集中的每條電影評論用向量表示,首先要得到每條評論中每個單詞的詞向量,然后把所有單詞的詞向量做平均,當作是句子或文本的向量表示。

於是得到電影評論的word2vec表示和golve表示。

"""數據預處理,得到單詞的詞向量,並得到句子的向量"""

#編碼方式有一點粗暴,簡單說來就是把這句話中的詞的詞向量做平均

eng_stopwords = set(stopwords.words('english'))

# 清洗文本數據
def clean_text(text, remove_stopwords=False):
    text = BeautifulSoup(text, 'html.parser').get_text()
    text = re.sub(r'[^a-zA-Z]', ' ', text)
    words = text.lower().split()
    if remove_stopwords:
        words = [w for w in words if w not in eng_stopwords]
    return words

# 取word2vec詞向量,或者glove詞向量
def to_review_vector(review,model='word2vec'):
    words = clean_text(review, remove_stopwords=True)
    if model == 'word2vec':
        array = np.asarray([word2vec_embedding[w] for w in words if w in word2vec_embedding],dtype='float32')
    elif model == 'glove':
        array = np.asarray([glove_embedding[w] for w in words if w in glove_embedding],dtype='float32')
    elif model == 'fasttext':
        array = np.asarray([fasttext_embedding[w] for w in words if w in fasttext_embedding],dtype='float32')
    else:
        raise ValueError('請輸入:word2vec、glove或fasttext')
    return array.mean(axis=0)

"""word2vec表示的樣本"""
train_data_word2vec = [to_review_vector(text,'word2vec') for text in df['review']]

"""用glove表示的樣本"""
train_data_glove = [to_review_vector(text,'glove') for text in df['review']]

 用word2vec表示的樣本訓練隨機森林模型,並用包外估計作為泛化誤差的評估指標。

從結果可以看到,包外估計為0.83568,之前用詞頻矩陣訓練的模型包外估計為0.84232,所以比之前用詞袋模型訓練的效果差一點。

def model_eval(train_data):

    print("1、混淆矩陣為:\n")
    print(metrics.confusion_matrix(df.sentiment, forest.predict(train_data)))

    print("\n2、准確率、召回率和F1值為:\n")
    print(metrics.classification_report(df.sentiment,forest.predict(train_data)))

    print("\n3、包外估計為:\n")
    print(forest.oob_score_)
    
    print("\n4、AUC Score為:\n")
    y_predprob = forest.predict_proba(train_data)[:,1]
    print(metrics.roc_auc_score(df.sentiment, y_predprob))
   
    
"""用word2vec詞向量表示訓練模型和評估模型"""

forest = RandomForestClassifier(oob_score=True,n_estimators = 200, random_state=42)
forest = forest.fit(train_data_word2vec, df.sentiment)
print("\n====================評估以word2vec為文本表示訓練的模型==================\n")
model_eval(train_data_word2vec)

再用glove詞向量表示的訓練集進行模型訓練。很不幸,包外估計為0.78556,泛化性能比較差。

"""用glove詞向量表示訓練模型和評估模型"""

forest = RandomForestClassifier(oob_score=True,n_estimators = 200, random_state=42)
forest = forest.fit(train_data_glove, df.sentiment)
print("\n====================評估以glove為文本表示訓練的模型==================\n")
model_eval(train_data_glove)

 

最后用fasttext詞向量表示的樣本訓練分類器。包外估計為0.81112,比word2vec效果差不少。

del word2vec_embedding
del glove_embedding
del train_data_word2vec
del train_data_glove
del forest

"""讀取訓練好的fasttext模型"""

model_name_fast = 'fasttext.model'
fasttext_embedding = models.FastText.load(os.path.join('..', 'models', model_name_fast))

"""fasttext表示的樣本"""
train_data_fasttext = [to_review_vector(text,'fasttext') for text in df['review']]

"""用fasttext詞向量表示訓練模型和評估模型"""

forest = RandomForestClassifier(oob_score=True,n_estimators = 200, random_state=42)
forest = forest.fit(train_data_fasttext, df.sentiment)
print("\n====================評估以fasttext為文本表示訓練的模型==================\n")
model_eval(train_data_fasttext)

三、后記

之前就用gensim訓練過中文詞向量,一段時間不用,連輸入格式都忘記了,這次正好鞏固一下。

從上面的結果可以看到,至少在這個任務中,word2vec的表現比glove、fasttext要優秀。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM