開發用於情感分析的n-gram CNN模型(翻譯)
第16章 項目: 開發用於情感分析的n-gram CNN模型
前面用於文本分類和情感分析的標準深度學習模使用了詞嵌入層和一維卷積神經網路,可以通過使用多個並行卷積層來擴展該模型,擴展後的網路使用不同的卷積核大小讀取源文檔,實際上,這是為文本分析創建一個多通道卷積神經網路,用於讀取具有不同n-gram大小(單片語)的文本。在本教程中,您將了解如何開發一個多通道卷積神經網路,用於文本電影評論數據的情緒預測。完成本教程後,您將了解:
- 如何準備電影評論文本數據進行建模。
- 如何在Keras中開發用於文本分析的多通道卷積神經網路。
- 如何使用新的電影評論數據評估訓練好的模型。
16.1 教程概述
本教程分為以下幾部分:
- 電影評論數據集。
- 數據準備。
- 開發多通道模型。
- 評估模型。
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件事:
- 將數據分成訓練和測試集。
- 載入和清理數據以刪除標點符號和數字。
- 清洗所有評論並保存。
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部分:
- 編碼數據
- 定義模型。
- 完整的例子。
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:擬合模型的示例輸出
已定義模型的圖表將保存到文件中,清楚地顯示模型的三個輸入通道。