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

Luv_GEM發表於2019-05-19

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

這篇部落格還是關於文字特徵工程的,用詞嵌入的方法來構造文字特徵,也就是用word2vec詞向量和glove詞向量進行文字表示,訓練隨機森林分類器。

一、訓練word2vec詞向量

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

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

下面我們先用gensim訓練word2vec詞向量。

首先匯入所需要的庫。

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

from bs4 import BeautifulSoup
from gensim.models import word2vec

接著讀取有標籤的訓練資料和無標籤的資料,把影評合併到一個列表中。

"""讀取資料,包括有標籤的和無標籤的資料"""

# 定義讀取資料的函式
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',...]的格式。
# 再次強調,這裡最關鍵,格式不對則沒法學習。

現在就可以輸入進去訓練詞向量了。

"""列印日誌資訊"""

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 = word2vec.Word2Vec(sentences, workers=num_workers, \
            size=num_features, min_count = min_word_count, \
            window = context, sg=model_)

# 儲存模型
model.save(os.path.join('..', 'models', model_name))

檢驗一下模型訓練的效果,檢視和 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)]

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

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

首先匯入所需要的庫。

import os
import re
import numpy as np
import pandas as pd
from bs4 import BeautifulSoup

from nltk.corpus import stopwords

from gensim.models.word2vec import Word2Vec

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詞向量),備用。

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

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

"""讀取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')
    else:
        raise ValueError('請輸入:word2vec或glove')
    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)

 

重新用word2vec詞向量表示的樣本訓練分類器,進行預測,並儲存預測結果。

"""重新用word2vec向量表示的樣本訓練模型"""
forest = RandomForestClassifier(oob_score=True,n_estimators = 200, random_state=42)
forest = forest.fit(train_data_word2vec, df.sentiment)

del df
del train_data_word2vec
del train_data_glove

"""進行預測,並儲存預測結果"""
df = load_dataset('test')

test_data_word2vec = [to_review_vector(text,'word2vec') for text in df['review']]

result = forest.predict(test_data_word2vec)
output = pd.DataFrame({'id':df.id, 'sentiment':result})
# 儲存
output.to_csv(os.path.join('..', 'data', 'word2vec_model.csv'), index=False)

三、後記

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

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

相關文章