第16章 項目: 開發用於情感分析的n-gram CNN模型

前面用於文本分類和情感分析的標準深度學習模使用了詞嵌入層和一維卷積神經網路,可以通過使用多個並行卷積層來擴展該模型,擴展後的網路使用不同的卷積核大小讀取源文檔,實際上,這是為文本分析創建一個多通道卷積神經網路,用於讀取具有不同n-gram大小(單片語)的文本。在本教程中,您將了解如何開發一個多通道卷積神經網路,用於文本電影評論數據的情緒預測。完成本教程後,您將了解:

  • 如何準備電影評論文本數據進行建模。
  • 如何在Keras中開發用於文本分析的多通道卷積神經網路。
  • 如何使用新的電影評論數據評估訓練好的模型。

16.1 教程概述

本教程分為以下幾部分:

  1. 電影評論數據集。
  2. 數據準備。
  3. 開發多通道模型。
  4. 評估模型。

16.2 電影評論數據集

在本教程中,我們將使用Movie Review數據集。這個為情緒分析設計的數據集在前面的第9章中有描述。您可以從這裡下載數據集:電影評論Polarity Dataset(評論polarity.tar.gz,3MB)。 http://www.cs.cornell.edu/people/pabo/movie-review-data/review_polarity.tar.gz

解壓縮文件後,您將擁有一個名為txt sentoken的目錄,其中包含neg和pos兩個子目錄,用存儲負面和正面評論。對於neg和pos目錄走中的每個文件存儲一個評論,命名約定為cv000到cv999。

16.3 數據準備

意:電影評論數據集的準備工作在第9章中已介紹。在本節中,我們介紹3件事:

  1. 將數據分成訓練和測試集。
  2. 載入和清理數據以刪除標點符號和數字。
  3. 清洗所有評論並保存。

16.3.1 分為訓練和測試集

我們假設正在開發一個系統,可以預測電影評論的情緒是積極的還是消極的,這意味著在開發模型之後,我們需要對新的文本評論進行預測。這將要求對新評論執行所有相同的數據準備工作,就像對模型的訓練數據執行得一樣。我們在數據準備之前要將數據集拆分為訓練和測試集,這個約束一直貫穿與整個模型評估的過程中。這意味著在準備數據和模型訓練期間,測試集中任何知識對我們來說都是未知的,而且在訓練過程中都是不可用的。

我們使用最後的100個正面評論和100個負面評論作為測試集(200條評論),其餘1,800條評論作為訓練數據集。這是90%用於訓練,10%用於測試。通過評論的文件名可以輕鬆實現拆分,其中評論為000至899的評論用做訓練數據,而評論為900以上的評論用於測試。

16.3.2 裝載和清洗評論

文本數據已經相當乾淨,因此不需要太多準備工作。在不了解細節的情況下,我們將使用以下方法準備數據:

  • 以空格為分隔符分詞。
  • 從單詞中刪除所有標點符號。
  • 刪除所有不完全由字母字元組成的單詞。
  • 刪除所有已知停用詞。
  • 刪除長度≤1個字元的所有單詞。

我們可以將所有這些步驟放入clean_doc()函數中,該函數以從文件載入的原始文本作為參數,並返回已清理的標記列表。我們還定義了一個load_doc()函數,它從文件中載入文檔,以便與clean_doc()函數一起使用。下面列出了清洗的第一次正面評價

from nltk.corpus import stopwords
import string
import re
# load doc into memory
def load_doc(filename):
# open the file as read only
file = open(filename, r )
# read all text
text = file.read()
# close the file
file.close()
return text
# turn a doc into clean tokens
def clean_doc(doc):
# split into tokens by white space
tokens = doc.split()
# prepare regex for char filtering
re_punc = re.compile( [%s] % re.escape(string.punctuation))
# remove punctuation from each word
tokens = [re_punc.sub( , w) for w in tokens]
# remove remaining tokens that are not alphabetic
tokens = [word for word in tokens if word.isalpha()]
# filter out stop words
stop_words = set(stopwords.words( english ))
tokens = [w for w in tokens if not w in stop_words]
# filter out short tokens
tokens = [word for word in tokens if len(word) > 1]
return tokens
# load the document
filename = txt_sentoken/pos/cv000_29590.txt
text = load_doc(filename)
tokens = clean_doc(text)
print(tokens)

代碼清單16.1:清理電影評論的示例

運行該示例會列印一長串乾淨的分詞。我們可以嘗試更多清洗步驟,並將其留作進一步練習。

......

place, even, acting, hell, solid, dreamy, depp, turning, typically, strong, performance, deftly, handling, british, accent, ians, holm, joe, goulds, secret, richardson, dalmatians, log, great, supporting, roles, big, surprise, graham, cringed, first, time, opened, mouth, imagining, attempt, irish, accent, actually, wasnt, half, bad, film, however, good, strong, violencegore, sexuality, language, drug, content]

代碼清單16.2:清理影片評論的示例輸出

16.3.3 清洗所有評論並保存

我們使用上面定義的函數來清洗所有的電影評論,我們定義process_docs()的新函數,它遍歷目錄中的所有評論,清洗並將它們作為列表返回,在為函數添加一個參數,以指示函數是處理訓練數據還是測試評論,這樣可以過濾文件名(如上所述),並且只清理和返回所請求的那些用於訓練或測試的評論。完整功能如下所列。

# load all docs in a directory
def process_docs(directory, is_train):
documents = list()
# walk through all files in the folder
for filename in listdir(directory):
# skip any reviews in the test set
if is_train and filename.startswith( cv9 ):
continue
if not is_train and not filename.startswith( cv9 ):
continue
# create the full path of the file to open
path = directory + / + filename
# load the doc
doc = load_doc(path)
# clean doc
tokens = clean_doc(doc)
# add to list
documents.append(tokens)
return documents

代碼清單16.3:清理多個審閱文檔的功能

我們可以調用此函數處理負面訓練評論,同時需要為訓練和測試數據集添加對應的分類標籤,我們知道我們有900份訓練文件和100份測試文件,我們使用Python的list來來創建電影評論的正面負面標籤:負面為0、正面為1。下面名為load_clean_dataset()的函數將載入並清理選定的電影評論文本,並為評論創建分類標籤

# load and clean a dataset
def load_clean_dataset(is_train):
# load documents
neg = process_docs( txt_sentoken/neg , is_train)
pos = process_docs( txt_sentoken/pos , is_train)
docs = neg + pos
# prepare labels
labels = [0 for _ in range(len(neg))] + [1 for _ in range(len(pos))]
return docs, labels

代碼清單16.4:為數據集準備評論和標籤的函數

最後,我們將準備好的訓練集和測試集保存到文件中,以便我們以後建模和模型評估中使用。定義save_dataset()函數將給定的數據集(X和y元素)保存到一個packle API可以載入的文件中(這個使用的函數是Python中用於保存對象的標準API)。

# save a dataset to file
def save_dataset(dataset, filename):
dump(dataset, open(filename, wb ))
print( Saved: %s % filename)

代碼清單16.5:將乾淨文檔保存到文件的功能

16.3.4 完整的例子

我們可以將所有這些數據準備步驟結合在一起。下面列出了完整的示例。

import string
import re
from os import listdir
from nltk.corpus import stopwords
from pickle import dump
# load doc into memory
def load_doc(filename):
# open the file as read only
file = open(filename, r )
# read all text
text = file.read()
# close the file
file.close()
return text
# turn a doc into clean tokens
def clean_doc(doc):
# split into tokens by white space
tokens = doc.split()
# prepare regex for char filtering
re_punc = re.compile( [%s] % re.escape(string.punctuation))
# remove punctuation from each word
tokens = [re_punc.sub( , w) for w in tokens]
# remove remaining tokens that are not alphabetic
tokens = [word for word in tokens if word.isalpha()]
# filter out stop words
stop_words = set(stopwords.words( english ))
tokens = [w for w in tokens if not w in stop_words]
# filter out short tokens
tokens = [word for word in tokens if len(word) > 1]
tokens = .join(tokens)
return tokens
# load all docs in a directory
def process_docs(directory, is_train):
documents = list()
# walk through all files in the folder
for filename in listdir(directory):
# skip any reviews in the test set
if is_train and filename.startswith( cv9 ):
continue
if not is_train and not filename.startswith( cv9 ):
continue
# create the full path of the file to open
path = directory + / + filename
# load the doc
doc = load_doc(path)
# clean doc
tokens = clean_doc(doc)
# add to list
documents.append(tokens)
return documents
# load and clean a dataset
def load_clean_dataset(is_train):
# load documents
neg = process_docs( txt_sentoken/neg , is_train)
pos = process_docs( txt_sentoken/pos , is_train)
docs = neg + pos
# prepare labels
labels = [0 for _ in range(len(neg))] + [1 for _ in range(len(pos))]
return docs, labels
# save a dataset to file
def save_dataset(dataset, filename):
dump(dataset, open(filename, wb ))
print( Saved: %s % filename)
# load and clean all reviews
train_docs, ytrain = load_clean_dataset(True)
test_docs, ytest = load_clean_dataset(False)
# save training datasets
save_dataset([train_docs, ytrain], train.pkl )
save_dataset([test_docs, ytest], test.pkl )

代碼清單16.6:清理和保存所有電影評論的完整示例

運行該示例清洗電影評論文檔,創建分類標籤,並保存訓練和測試數據集分別為train.pkl和test.pkl。現在我們準備開發我們的模型了。

16.4 開發多通道模型

在本節中,我們將開發一個用於情感分析預測問題的多通道卷積神經網路。本節分為3部分:

  1. 編碼數據
  2. 定義模型。
  3. 完整的例子。

16.4.1 編碼數據

第一步是載入已清理的訓練數據集。下面定義load_dataset()函數完成載入pickle訓練數據集的工作。

# load a clean dataset
def load_dataset(filename):
return load(open(filename, rb ))
trainLines, trainLabels = load_dataset( train.pkl )

代碼清單16.7:載入保存已清洗的評論的示例

接下來,我們在訓練數據集上擬合Keras的Tokenizer類的實例tokenizer,使用tokenzier實例來定義Embedding層的辭彙表,並通過它將電影評論文檔編碼為整數。下面的函數create_tokenizer()將創建一個Tokenizer的實例tokenizer並擬合訓練數據。

# fit a tokenizer
def create_tokenizer(lines):
tokenizer = Tokenizer()
tokenizer.fit_on_texts(lines)
return tokenizer

代碼清單16.8:創建Tokenizer的函數

我們還需要知道輸入文本序列中的最長的文本的長度,以此作為模型的輸入長度,並將所有序列填充到固定這個最大長度。下面的函數max_length()將計算訓練數據集中所有評論的最大長度(單詞數)。

# calculate the maximum document length
def max_length(lines):
return max([len(s.split()) for s in lines])

代碼清單16.9:計算最大電影評論長度的函數

我們還需要知道Embedding層的辭彙量大小,這可以從定義的Tokenizer實例中獲取,如下:

# calculate vocabulary size
vocab_size = len(tokenizer.word_index) + 1

代碼清單16.10:計算辭彙表的大小。

最後,我們對乾淨的電影評論文本進行整數編碼並填充,函數名稱encode_text():

# encode a list of lines
def encode_text(tokenizer, lines, length):
# integer encode
encoded = tokenizer.texts_to_sequences(lines)
# pad encoded sequences
padded = pad_sequences(encoded, maxlen=length, padding= post )
return padded

代碼清單16.11:編碼和填充電影評論文本的函數

16.4.2 定義模型

用於文檔分類的標準模型是使用Embedding層作為輸入,接著是一維卷積神經網路,池化層,然後是預測輸出層。卷積層中的卷積核大小定義了在輸入文本文檔中傳遞卷積時要考慮的單詞數量,並提供了一個分組參數。用於文檔分類的多通道卷積神經網路涉及使用具有不同大小卷積核的多個版本的標準模型。這允許一次以不同的解析度或不同的n-gram(單片語)處理文檔,同時模型學習如何最好地整合這些解釋。

Yoon Kim在其2014年題為「Convolutional Neural Networks for Sentence Classification」的論文中首次描述了這種方法。在論文中,Kim嘗試了靜態和動態(更新)嵌入層,我們對此做了一定的簡化,只關注使用不同的內核大小。使用Kim的論文中的圖表可以最好地理解這種方法,參見第14章。

在Keras中,可以使用函數式API定義多輸入模型,我們將定義一個帶有三個輸入通道的模型,用於處理4-gram,6-gram和8-gram的電影評論文本。每個頻道由以下元素組成:

  • 輸入層,用於定義輸入序列的長度。
  • 嵌入圖層設置為辭彙表的大小和100維實值表示。
  • Conv1D層具有32個卷積核,核大小設置為一次讀取的字數。
  • MaxPooling1D層用於合併卷積層的輸出。
  • Flatten層以將三維輸出展平為二維以進行連接。

三個通道的輸出連接成一個矢量,並由Dense層和輸出層處理。下面的函數定義並返回模型。作為定義模型的一部分,將列印已定義模型的摘要,並創建模型圖並將其保存到文件中。

# define the model
def define_model(length, vocab_size):
# channel 1
inputs1 = Input(shape=(length,))
embedding1 = Embedding(vocab_size, 100)(inputs1)
conv1 = Conv1D(filters=32, kernel_size=4, activation= relu )(embedding1)
drop1 = Dropout(0.5)(conv1)
pool1 = MaxPooling1D(pool_size=2)(drop1)
flat1 = Flatten()(pool1)
# channel 2
inputs2 = Input(shape=(length,))
embedding2 = Embedding(vocab_size, 100)(inputs2)
conv2 = Conv1D(filters=32, kernel_size=6, activation= relu )(embedding2)
drop2 = Dropout(0.5)(conv2)
pool2 = MaxPooling1D(pool_size=2)(drop2)
flat2 = Flatten()(pool2)
# channel 3
inputs3 = Input(shape=(length,))
embedding3 = Embedding(vocab_size, 100)(inputs3)
conv3 = Conv1D(filters=32, kernel_size=8, activation= relu )(embedding3)
drop3 = Dropout(0.5)(conv3)
pool3 = MaxPooling1D(pool_size=2)(drop3)
flat3 = Flatten()(pool3)
# merge
merged = concatenate([flat1, flat2, flat3])
# interpretation
dense1 = Dense(10, activation= relu )(merged)
outputs = Dense(1, activation= sigmoid )(dense1)
model = Model(inputs=[inputs1, inputs2, inputs3], outputs=outputs)
# compile
model.compile(loss= binary_crossentropy , optimizer= adam , metrics=[ accuracy ])
# summarize
model.summary()
plot_model(model, show_shapes=True, to_file= multichannel.png )
return model

代碼清單16.12:定義分類模型的函數

16.4.3 完整的例子

將所有這些結合在一起,下面列出了完整的示例。

from pickle import load
from numpy import array
from keras.preprocessing.text import Tokenizer
from keras.preprocessing.sequence import pad_sequences
from keras.utils.vis_utils import plot_model
from keras.models import Model
from keras.layers import Input
from keras.layers import Dense
from keras.layers import Flatten
from keras.layers import Dropout
from keras.layers import Embedding
from keras.layers.convolutional import Conv1D
from keras.layers.convolutional import MaxPooling1D
from keras.layers.merge import concatenate
# load a clean dataset
def load_dataset(filename):
return load(open(filename, rb ))
# fit a tokenizer
def create_tokenizer(lines):
tokenizer = Tokenizer()
tokenizer.fit_on_texts(lines)
return tokenizer
# calculate the maximum document length
def max_length(lines):
return max([len(s.split()) for s in lines])
# encode a list of lines
def encode_text(tokenizer, lines, length):
# integer encode
encoded = tokenizer.texts_to_sequences(lines)
# pad encoded sequences
padded = pad_sequences(encoded, maxlen=length, padding= post )
return padded

# define the model
def define_model(length, vocab_size):
# channel 1
inputs1 = Input(shape=(length,))
embedding1 = Embedding(vocab_size, 100)(inputs1)
conv1 = Conv1D(filters=32, kernel_size=4, activation= relu )(embedding1)
drop1 = Dropout(0.5)(conv1)
pool1 = MaxPooling1D(pool_size=2)(drop1)
flat1 = Flatten()(pool1)
# channel 2
inputs2 = Input(shape=(length,))
embedding2 = Embedding(vocab_size, 100)(inputs2)
conv2 = Conv1D(filters=32, kernel_size=6, activation= relu )(embedding2)
drop2 = Dropout(0.5)(conv2)
pool2 = MaxPooling1D(pool_size=2)(drop2)
flat2 = Flatten()(pool2)
# channel 3
inputs3 = Input(shape=(length,))
embedding3 = Embedding(vocab_size, 100)(inputs3)
conv3 = Conv1D(filters=32, kernel_size=8, activation= relu )(embedding3)
drop3 = Dropout(0.5)(conv3)
pool3 = MaxPooling1D(pool_size=2)(drop3)
flat3 = Flatten()(pool3)
# merge
merged = concatenate([flat1, flat2, flat3])
# interpretation
dense1 = Dense(10, activation= relu )(merged)
outputs = Dense(1, activation= sigmoid )(dense1)
model = Model(inputs=[inputs1, inputs2, inputs3], outputs=outputs)
# compile
model.compile(loss= binary_crossentropy , optimizer= adam , metrics=[ accuracy ])
# summarize
model.summary()
plot_model(model, show_shapes=True, to_file= model.png )
return model
# load training dataset
trainLines, trainLabels = load_dataset( train.pkl )
# create tokenizer
tokenizer = create_tokenizer(trainLines)
# calculate max document length
length = max_length(trainLines)
print( Max document length: %d % length)
# calculate vocabulary size
vocab_size = len(tokenizer.word_index) + 1
print( Vocabulary size: %d % vocab_size)
# encode data
trainX = encode_text(tokenizer, trainLines, length)
# define model
model = define_model(length, vocab_size)
# fit model
model.fit([trainX,trainX,trainX], array(trainLabels), epochs=7, batch_size=16)
# save the model
model.save( model.h5 )

代碼清單16.13:擬合n-gram CNN模型的完整示例

運行該示例,將列印準備好的訓練數據集的摘要。

Max document length: 1380

Vocabulary size: 44277

代碼清單16.14:準備訓練數據的示例輸出

該模型相對較快,並且在訓練數據集上顯示出良好的性能。

Epoch 1/7

1800/1800 [==============================] - 3s 1ms/step - loss: 0.6913 - acc: 0.5178

Epoch 2/7

1800/1800 [==============================] - 2s 857us/step - loss: 0.4916 - acc: 0.7533

Epoch 3/7

1800/1800 [==============================] - 2s 869us/step - loss: 0.0825 - acc: 0.9733

Epoch 4/7

1800/1800 [==============================] - 2s 913us/step - loss: 0.0065 - acc: 0.9994

Epoch 5/7

1800/1800 [==============================] - 2s 944us/step - loss: 0.0018 - acc: 1.0000

Epoch 6/7

1800/1800 [==============================] - 2s 946us/step - loss: 0.0011 - acc: 1.0000

Epoch 7/7

1800/1800 [==============================] - 2s 980us/step - loss: 6.8402e-04 - acc: 1.0000

代碼清單16.15:擬合模型的示例輸出

已定義模型的圖表將保存到文件中,清楚地顯示模型的三個輸入通道。

圖16.1:文本多通道卷積神經網路的圖。

該模型訓練了多個epoch並保存到文件model.h5中以供以後評估使用。

16.5 評估模型

在本節中,我們可以通過預測未見測試數據集中所有評論的情感來評估擬合模型。使用上一節中開發的數據載入函數,我們載入和編碼訓練和測試數據集。

# load datasets
trainLines, trainLabels = load_dataset( train.pkl )
testLines, testLabels = load_dataset( test.pkl )
# create tokenizer
tokenizer = create_tokenizer(trainLines)
# calculate max document length
length = max_length(trainLines)
# calculate vocabulary size
vocab_size = len(tokenizer.word_index) + 1
print( Max document length: %d % length)
print( Vocabulary size: %d % vocab_size)
# encode data
trainX = encode_text(tokenizer, trainLines, length)
testX = encode_text(tokenizer, testLines, length)
print(trainX.shape, testX.shape)

代碼清單16.16:準備訓練和測試數據以評估模型。

我們可以載入保存的模型並在訓練和測試數據集上進行評估。下面列出了完整的示例。

from pickle import load
from numpy import array
from keras.preprocessing.text import Tokenizer
from keras.preprocessing.sequence import pad_sequences
from keras.models import load_model

# load a clean dataset
def load_dataset(filename):
return load(open(filename, rb))
# fit a tokenizer
def create_tokenizer(lines):
tokenizer = Tokenizer()
tokenizer.fit_on_texts(lines)
return tokenizer
# calculate the maximum document length
def max_length(lines):
return max([len(s.split()) for s in lines])
# encode a list of lines
def encode_text(tokenizer, lines, length):
# integer encode
encoded = tokenizer.texts_to_sequences(lines)
# pad encoded sequences
padded = pad_sequences(encoded, maxlen=length, padding=post)
return padded
# load datasets
trainLines, trainLabels = load_dataset(train.pkl)
testLines, testLabels = load_dataset(test.pkl)
# create tokenizer
tokenizer = create_tokenizer(trainLines)
# calculate max document length
length = max_length(trainLines)
print(Max document length: %d % length)
# calculate vocabulary size
vocab_size = len(tokenizer.word_index) + 1
print(Vocabulary size: %d % vocab_size)
# encode data
trainX = encode_text(tokenizer, trainLines, length)
testX = encode_text(tokenizer, testLines, length)
# load the model
model = load_model(model.h5)
# evaluate model on training dataset
_, acc = model.evaluate([trainX, trainX, trainX], array(trainLabels), verbose=0)
print( Train Accuracy: %.2f % (acc * 100))
# evaluate model on test dataset dataset
_, acc = model.evaluate([testX, testX, testX], array(testLabels), verbose=0)
print( Test Accuracy: %.2f % (acc * 100))

代碼清單16.17:評估擬合模型的完整示例

運行該示例將在訓練和測試數據集上列印模型的性能,可以看到,正如預期的那樣,在訓練集的模型表現非常出色,準確率達100%。我們還可以看到模型在看不見的測試數據集上的性能也人印象深刻,達到了88.0%,這高於2014年論文中報告的模型的性能(儘管不是直接的蘋果對蘋果比較)。

意:鑒於神經網路的隨機性,您的具體結果可能會有所不同。考慮運行幾次示例。

Max document length: 1380

Vocabulary size: 44277

Train Accuracy: 100.00

Test Accuracy: 88.00

代碼清單16.18:評估擬合模型的示例輸出

16.6 擴展

本節列出了一些擴展,您可能逐一嘗試這些想法。

  • 同的n-gram。通過更改模型中通道使用的內核大小(n-gram的數量)來探索模型,以了解它如何影響模型性能。
  • 多或更少的通道。嘗試在模型中使用更多或更少的通道,並了解它如何影響模型性能。
  • 享嵌入。嘗試每個通道共享相同單詞嵌入的配置,並報告對模型性能的影響。

  • 深的網路。深層卷積神經網路在計算機視覺中表現更好。在這裡嘗試使用更深層的模型,看看它如何影響模型性能。
  • 斷序列。如果最長序列與所有其他評論非常不同,則將所有序列填充到最長序列的長度可能是極端的,研究評論長度的分布並將評論截斷為平均長度。
  • 斷辭彙。我們刪除了不常出現的單詞,但仍然有超過25,000個單詞的大辭彙量,進一步嘗試減少辭彙量的大小和對模型性能的影響。
  • epoch和批量大小。該模型很快擬合訓練數據集,嘗試訓練epoch和批量大小的多樣配置,並使用測試數據集作為驗證集,為訓練模型選擇更好的epoch和批量大小的數值。
  • 使用預訓練嵌入模型。嘗試在模型中預先訓練Word2Vec單詞嵌入以及在訓練期間進行進一步微調對模型性能的影響。
  • 使用GloVe嵌入。嘗試載入預訓練的GloVe嵌入和對模型性能的影響,無論是否在訓練期間進一步微調。
  • 練最終模型。使用所有可用數據訓練最終模型,並使用它來預測來自互聯網的真實臨時電影評論。

如果你嘗試其他任何擴展,可以提高模型性能,請告訴我。

16.7 進一步閱讀

如果您希望深入了解,本節將提供有關該主題的更多資源。

  • 用於句子分類的卷積神經網路,2014。 arxiv.org/abs/1408.5882
  • 句子分類的卷積神經網路(代碼)。 github.com/yoonkim/CNN_
  • Keras函數式API。 keras.io/getting-starte

16.8 摘要

在本教程中,您了解了如何為文本電影評論數據開發多通道卷積神經網路以進行情感預測。具體來說,你學到了:

  • 如何準備電影評論文本數據進行建模。
  • 如何在Keras中開發用於文本的多通道卷積神經網路。

如何評估看不見的電影評論數據的擬合模型。

推薦閱讀:

相关文章