观点 | 用于文本的最牛神经网络架构是什么?

选自GitHub

作者:Nadbor Drozd

机器之心编译

参与:路雪、刘晓坤

用于文本的最牛神经网络架构是什么?数据科学家 Nadbor 在多个文本分类数据集上对大量神经网络架构和 SVM + NB 进行了测试,并展示了测试结果。

去年,我写了一篇关于使用词嵌入如 word2vec 或 GloVe 进行文本分类的文章(http://nadbordrozd.github.io/blog/2016/05/20/text-classification-with-word2vec/)。在我的基准测试中,嵌入的使用比较粗糙,平均文档中所有单词的词向量,然后将结果放进随机森林。不幸的是,最后得出的分类器除了一些特殊情况(极少的训练样本,大量的未标注数据),基本都不如优秀的 SVM,尽管它比较老。

当然有比平均词向量更好的使用词嵌入的方式,上个月我终于着手去做这件事。我对 arXiv 上的论文进行了简单的调查,发现大部分先进的文本分类器使用嵌入作为神经网络的输入。但是哪种神经网络效果最好呢?LSTM、CNN,还是双向长短期记忆(BLSTM)CNN?网上有大量教程展示如何实现神经分类器,并在某个数据集上进行测试。问题在于它们给出的指标通常没有上下文。有人说他们在某个数据集上的准确率达到了 0.85。这就是好吗?它比朴素贝叶斯、SVM 还要好吗?比其他神经架构都好?这是偶然吗?在其他数据集上的效果也会一样好吗?

为了回答这些问题,我在 Keras 中实现了多个神经架构,并创建了一个基准,使这些算法与经典算法,如 SVM、朴素贝叶斯等,进行比较。地址:http://github.com/nadbordrozd/text-top-model。

模型

该 repository 中所有模型都用 .fit(X, y)、.predict(X)、.get_params(recursive) 封装在一个 scikit-learn 相容类中,所有的层大小、dropout 率、n-gram 区间等都被参数化。为清晰起见,下面的代码已经简化。

由于我本来想做一个分类器基准,而不是预处理方法基准,因此所有的数据集都已被符号化,分类器得到一个符号 id 列表,而不是字符串。

朴素贝叶斯

朴素贝叶斯分为两种:伯努利(Bernoulli)和多项式(Multinomial)。我们还可以使用 tf-idf 加权或简单的计数推断出 n-gram。由于 sklearn 的向量器的输入是字符串,并给它一个整数符号 id 列表,因此我们必须重写默认预处理器和分词器。

  • from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer

  • from sklearn.naive_bayes import BernoulliNB, MultinomialNB

  • from sklearn.pipeline import Pipeline

  • from sklearn.svm import SVC

  • vectorizer = TfidfVectorizer(

  •    preprocessor=lambda x: map(str, x),

  •    tokenizer=lambda x: x,

  •    ngram_range=(1, 3))

  • model = Pipeline([("vectorizer", vectorizer), ("model", MultinomialNB())])

  • SVM

    SVM 是所有文本分类任务的强大基线。我们可以对此重用同样的向量器。

  • from sklearn.svm import SVC

  • model = Pipeline([("vectorizer", vectorizer), ("model", SVC())])

  • 多层感知器

    又叫作 vanilla 前馈神经网络。该模型不使用词嵌入,输入是词袋。

  • from keras.models import Sequential

  • from keras.layers import Dense, Dropout, Activation

  • from keras.preprocessing.text import Tokenizer

  • vocab_size = 20000

  • num_classes = 3

  • model = Sequential()

  • model.add(Dense(128, input_shape=(vocab_size,)))

  • model.add(Activation("relu"))

  • model.add(Dropout(0.2))

  • model.add(Dense(128, input_shape=(vocab_size,)))

  • model.add(Activation("relu"))

  • model.add(Dropout(0.2))

  • model.add(Dense(num_classes))

  • model.add(Activation("softmax"))

  • model.compile(loss="categorical_crossentropy",

  •              optimizer="adam",

  •              metrics=["accuracy"])

  • 该模型的输入需要和标签一样进行 One-hot 编码。

  • import keras

  • from keras.preprocessing.text import Tokenizer

  • tokenizer = Tokenizer(num_words=vocab_size)

  • X = tokenizer.sequences_to_matrix(X, mode="binary")

  • y = keras.utils.to_categorical(y, num_classes)

  • (双向)长短期记忆

    从这里开始事情就变得有趣了。该模型的输入不是词袋而是一个词 id 序列。首先需要构建一个嵌入层将该序列转换成 d 维向量矩阵。

  • import numpy as np

  • from keras.layers import Embedding

  • max_seq_len = 100

  • embedding_dim = 37

  • # we will initialise the embedding layer with random values and set trainable=True

  • # we could also initialise with GloVe and set trainable=False

  • embedding_matrix = np.random.normal(size=(vocab_size, embedding_dim))

  • embedding_layer = Embedding(

  •    vocab_size,

  •    embedding_dim,

  •    weights=[embedding_matrix],

  •    input_length=max_seq_len,

  •    trainable=True)

  • 以下适用于该模型:

  • from keras.layers import Dense, LSTM, Bidirectional

  • units = 64

  • sequence_input = Input(shape=(max_seq_len,), dtype="int32")

  • embedded_sequences = embedding_layer(sequence_input)

  • layer1 = LSTM(units,

  •    dropout=0.2,

  •    recurrent_dropout=0.2,

  •    return_sequences=True)

  • # for bidirectional LSTM do:

  • # layer = Bidirectional(layer)

  • x = layer1(embedded_sequences)

  • layer2 = LSTM(units,

  •    dropout=0.2,

  •    recurrent_dropout=0.2,

  •    return_sequences=False)  # last of LSTM layers must have return_sequences=False

  • x = layer2(x)

  • final_layer = Dense(class_count, activation="softmax")

  • predictions = final_layer(x)

  • model = Model(sequence_input, predictions)

  • 该模型以及其他使用嵌入的模型都需要独热编码的标签,词 id 序列用零填充至固定长度:

  • from keras.preprocessing.sequence import pad_sequences

  • from keras.utils import to_categorical

  • X = pad_sequences(X, max_seq_len)

  • y = to_categorical(y, num_classes=class_count)

  • Fran?ois Chollet 的 cnn

    该架构(稍作修改)来自 Keras 教程(http://blog.keras.io/using-pre-trained-word-embeddings-in-a-keras-model.html),专门为长度为 1000 的文本设计,因此我使用它进行文本分类,而不用于语句分类。

  • from keras.layers import Conv1D, MaxPooling1D

  • units = 35

  • dropout_rate = 0.2

  • x = Conv1D(units, 5, activation="relu")(embedded_sequences)

  • x = MaxPooling1D(5)(x)

  • x = Dropout(dropout_rate)(x)

  • x = Conv1D(units, 5, activation="relu")(x)

  • x = MaxPooling1D(5)(x)

  • x = Dropout(dropout_rate)(x)

  • x = Conv1D(units, 5, activation="relu")(x)

  • x = MaxPooling1D(35)(x)

  • x = Dropout(dropout_rate)(x)

  • x = Flatten()(x)

  • x = Dense(units, activation="relu")(x)

  • preds = Dense(class_count, activation="softmax")(x)

  • model = Model(sequence_input, predictions)

  • Yoon Kim 的 CNN

    该架构来自 Yoon Kim 的论文(http://arxiv.org/abs/1408.5882v2.pdf),我基于 Alexander Rakhlin 的 GitHub 页面(http://github.com/alexander-rakhlin/CNN-for-Sentence-Classification-in-Keras)实现该架构。这个架构不需要规定文本必须为 1000 词长,更适合语句分类。

  • from keras.layers import Conv1D, MaxPooling1D, Concatenate

  • z = Dropout(0.2)(embedded_sequences)

  • num_filters = 8

  • filter_sizes=(3, 8),

  • conv_blocks = []

  • for sz in filter_sizes:

  •    conv = Conv1D(

  •        filters=num_filters,

  •        kernel_size=sz,

  •        padding="valid",

  •        activation="relu",

  •        strides=1)(z)

  •    conv = MaxPooling1D(pool_size=2)(conv)

  •    conv = Flatten()(conv)

  •    conv_blocks.append(conv)

  • z = Concatenate()(conv_blocks) if len(conv_blocks) > 1 else conv_blocks[0]

  • z = Dropout(0.2)(z)

  • z = Dense(units, activation="relu")(z)

  • predictions = Dense(class_count, activation="softmax")(z)

  • model = Model(sequence_input, predictions)

  • BLSTM2DCNN

    论文作者称,结合 BLSTM 和 CNN 将比使用任意一个效果要好(论文地址:http://arxiv.org/abs/1611.06639v1)。但是很奇怪,这个架构与前面两个模型不同,它使用的是 2D 卷积。这意味着神经元的感受野不只覆盖了文本中的近邻词,还覆盖了嵌入向量的近邻坐标。这有些可疑,因为他们使用的嵌入之间(如 GloVe 的连续坐标)并没有关系。如果一个神经元在坐标 5 和 6 学习到了一种模式,那么我们没有理由认为同样的模式会泛化到坐标 22 和 23,这样卷积就失去意义。但是我又知道些什么呢!

  • from keras.layers import Conv2D, MaxPool2D, Reshape

  • units = 128

  • conv_filters = 32

  • x = Dropout(0.2)(embedded_sequences)

  • x = Bidirectional(LSTM(

  •    units,

  •    dropout=0.2,

  •    recurrent_dropout=0.2,

  •    return_sequences=True))(x)

  • x = Reshape((2 * max_seq_len, units, 1))(x)

  • x = Conv2D(conv_filters, (3, 3))(x)

  • x = MaxPool2D(pool_size=(2, 2))(x)

  • x = Flatten()(x)

  • preds = Dense(class_count, activation="softmax")(x)

  • model = Model(sequence_input, predictions)

  • 堆叠

    除了那些基础模型外,我还实现了堆叠分类器,来组合不同模型之间的预测。我使用 2 个版本的堆叠。一个是基础模型返回概率,概率由一个简单的 logistic 回归组合;另一个是基础模型返回标签,使用 XGBoost 组合标签。

    数据集

    对于文档分类基准,我使用的所有数据集均来自:http://www.cs.umb.edu/~smimarog/textmining/datasets/,包括 20 个新闻组、不同版本的 Reuters-21578 和 WebKB 数据集。

    对于语句分类基准,我使用的是影评两极化数据集(http://www.cs.cornell.edu/people/pabo/movie-review-data/)和斯坦福情绪树库数据集(http://nlp.stanford.edu/~socherr/stanfordSentimentTreebank.zip)。

    结果

    一些模型仅用于文档分类或语句分类,因为它们要么在另一个任务中表现太差,要么训练时间太长。神经模型的超参数在基准中测试之前,会在一个数据集上进行调整。训练和测试样本的比例是 0.7 : 0.3。每个数据集上进行 10 次分割,每个模型接受 10 次测试。下表展示了 10 次分割的平均准确率。

    文档分类基准

    观点 | 用于文本的最牛神经网络架构是什么?

    观点 | 用于文本的最牛神经网络架构是什么?

    语句分类基准

    观点 | 用于文本的最牛神经网络架构是什么?

    观点 | 用于文本的最牛神经网络架构是什么?

    结论

    带嵌入的神经网络没有一个打败朴素贝叶斯和 SVM,至少没有持续打败。只有一层的简单前馈神经网络比任何其他架构效果都好。

    我把这归咎于我的超参数,它们没有得到足够的调整,尤其是训练的 epoch 数量。每个模型只训练 1 个 epoch,但是不同的数据集和分割可能需要不同的设置。但是,神经模型显然在做正确的事,因为将它们添加至整体或者堆叠能够大大提高准确率。观点 | 用于文本的最牛神经网络架构是什么?

    原文地址:http://nadbordrozd.github.io/blog/2017/08/12/looking-for-the-text-top-model/

    本文为机器之心编译,转载请联系本公众号获得授权

    ?------------------------------------------------

    加入机器之心(全职记者/实习生):hr@jiqizhixin.com

    投稿或寻求报道:content@jiqizhixin.com

    广告&商务合作:bd@jiqizhixin.com