0%

比赛介绍

Quora 平台,简单的来说就是美国版的知乎。最近 Quora 拿出 25,000 美元作为奖金,举办了一场 Kaggle 比赛:Quora Insincere Questions Classification。那么什么是虚假问题呢?就是那些并非真心发问而另有用意的问题。
该竞赛是个典型的文本二分类问题,即判断用户的提问是否 “有害”,竞赛中最关键的要求有三点:

  1. 只能使用 Kaggle Kernel 中生成的 submission.csv 来提交;
  2. 不能使用外部数据,也就是说 embedding 也只能用 Kernel 里提供的四个 embedding 文件;
  3. Kernel 的 GPU 运行时间不能超过 120 分钟。也就是说不能使用 Bert 这样的大型模型。
    目前的排名也不是最终排名,最后官方会用另一部分数据集来测试模型,然后给出最终的排名。

    官方的四个 embedding 文件

    从公开的 Kernel 来看,目测有 99% 都是使用 RNN 来解题。这 99% 使用 RNN 模型的,目测有 80% 都是使用了 Keras。

    文本分类

    最常见的文本分类应用场景就是垃圾邮件分类,情感分类等等。

    检查数据

    首先加载数据集,然后对数据集进行检查。可以随机打印一些样本然后查看是不是和标签相对应 (df.sample)

    探索数据集并收集指标

    收集以下有助于表征文本分类问题的重要指标:
  4. 样本数:数据中的示例总数。
  5. 课程数量:数据中的主题或类别总数。
  6. 每个类的样本数:每个类的样本数(主题 / 类别)。在平衡数据集中,所有类都将具有相似数量的样本;在不平衡的数据集中,每个类中的样本数量会有很大差异。
  7. 每个样本的单词数:一个样本中的单词中位数。
  8. 单词的频率分布:显示数据集中每个单词的频率(出现次数)的分布。
  9. 样本长度分布:分布显示数据集中每个样本的单词数。

explore_data.py contains functions to calculate and analyse these metrics.

选择模型

这里谷歌给出了一个文本分类模型选择的流程图。

我们针对不同类型的问题(特别是情绪分析和主题分类问题)运行了大量(~450K)实验,使用 12 个数据集,交替用于不同数据预处理技术和不同模型体系结构之间的每个数据集。这有助于我们识别影响最佳选择的数据集参数。下面的模型选择算法和流程图是我们实验的总结。

Algorithm for Data Preparation and Model Building

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
1. Calculate the number of samples/number of words per sample ratio.
2. If this ratio is less than 1500, tokenize the text as n-grams and use a
simple multi-layer perceptron (MLP) model to classify them (left branch in the
flowchart below):
a. Split the samples into word n-grams; convert the n-grams into vectors.
b. Score the importance of the vectors and then select the top 20K using the scores.
c. Build an MLP model.
3. If the ratio is greater than 1500, tokenize the text as sequences and use a
sepCNN model to classify them (right branch in the flowchart below):
a. Split the samples into words; select the top 20K words based on their frequency.
b. Convert the samples into word sequence vectors.
c. If the original number of samples/number of words per sample ratio is less
than 15K, using a fine-tuned pre-trained embedding with the sepCNN
model will likely provide the best results.
4. Measure the model performance with different hyperparameter values to find
the best model configuration for the dataset.
````
翻译一下:
1. 计算每个样本比例的样本数/单词数的比率。
2. 如果此比率小于1500,则将文本标记为n-gram并使用简单的多层感知器(MLP)模型对它们进行分类(左侧分支)下面的流程图):一个。将样本分成单词n-gram; 将n-gram转换为向量。湾 评分向量的重要性,然后使用分数选择前20K。C。建立MLP模型。
3. 如果比率大于1500,则将文本标记为序列并使用sepCNN模型对它们进行分类(右下图在下面的流程图中):一个。将样本分成单词; 根据频率选择前20K字。湾 将样本转换为单词序列向量。C。如果原始样本数/每个样本的单词数比例较小超过15K,使用经过微调的预训练嵌入sepCNN模型可能会提供最好的结果。
4. 使用不同的超参数值测量模型性能以进行查找数据集的最佳模型配置。

![TextClassificationFlowchart.png](https://i.loli.net/2019/01/19/5c42cec7588ae.png)
文本分类模型选择流程图

>在下面的流程图中,黄色框表示数据和模型准备过程。灰色框和绿色框表示我们为每个过程考虑的选择。绿色框表示我们对每个过程的建议选择。您可以使用此流程图作为构建第一个实验的起点,因为它可以以较低的计算成本为您提供良好的准确性。然后,您可以在后续迭代中继续改进初始模型。

此流程图回答了两个关键问题:
- 我们应该使用哪种学习算法或模型?
- 我们应该如何准备数据以有效地学习文本和标签之间的关系?
第二个问题的答案取决于第一个问题的答案; 我们预先处理数据的方式将取决于我们选择的模型。模型可以大致分为两类:使用单词排序信息的那些(序列模型),以及仅将文本视为“包”(组)单词(n-gram模型)的模型。序列模型的类型包括卷积神经网络(CNN),递归神经网络(RNN)及其变体。n-gram模型的类型包括逻辑回归,简单的多层感知器(MLP或完全连接的神经网络),梯度增强树和支持向量机。
根据我们的实验,我们观察到“样本数”(S)与“每个样本的单词数”(W)的比率与哪个模型表现良好相关。
当该比率的值很小(<1500)时,以n-gram为输入的小型多层感知器(我们称之为选项A)表现得更好或至少与序列模型一样好。MLP很容易定义和理解,并且它们比序列模型花费更少的计算时间。当此比率的值很大(> = 1500)时,使用序列模型(选项B)。在接下来的步骤中,您可以根据样本/单词样本比率跳过所选模型类型的相关小节(标记为A或B)。

### 数据集的平衡性
对于分类的数据集来说,每个类中的样本数量不会过度失衡,也就是说,每个类中应该有相当数量的样本。但是这个比赛就是一个严重不平衡的数据集。
### 常用深度学习模型
![](/Quora-Insincere-Questions-Classification/20190119051521899.png)
## 常用代码总结
这里是英文数据集数据处理和Keras搭建模型的一些常用代码。Using pre-trained word embeddings in a Keras model](https://blog.keras.io/using-pre-trained-word-embeddings-in-a-keras-model.html)。
### Token and Padding
```python
from keras.preprocessing.text import Tokenizer
from keras.preprocessing.sequence import pad_sequences

tokenizer = Tokenizer(nb_words=MAX_NB_WORDS)
tokenizer.fit_on_texts(texts)
sequences = tokenizer.texts_to_sequences(texts)

word_index = tokenizer.word_index
print('Found %s unique tokens.' % len(word_index))

data = pad_sequences(sequences, maxlen=MAX_SEQUENCE_LENGTH)

labels = to_categorical(np.asarray(labels))
print('Shape of data tensor:', data.shape)
print('Shape of label tensor:', labels.shape)

# split the data into a training set and a validation set
indices = np.arange(data.shape[0])
np.random.shuffle(indices)
data = data[indices]
labels = labels[indices]
nb_validation_samples = int(VALIDATION_SPLIT * data.shape[0])

x_train = data[:-nb_validation_samples]
y_train = labels[:-nb_validation_samples]
x_val = data[-nb_validation_samples:]
y_val = labels[-nb_validation_samples:]

Preparing the Embedding layer

  1. Reading the data dump of pre-trained embeddings
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    embeddings_index = {}
    f = open(os.path.join(GLOVE_DIR, 'glove.6B.100d.txt'))
    for line in f:
    values = line.split()
    word = values[0]
    coefs = np.asarray(values[1:], dtype='float32')
    embeddings_index[word] = coefs
    f.close()

    print('Found %s word vectors.' % len(embeddings_index))
  2. Compute our embedding matrix:
    1
    2
    3
    4
    5
    6
    embedding_matrix = np.zeros((len(word_index) + 1, EMBEDDING_DIM))
    for word, i in word_index.items():
    embedding_vector = embeddings_index.get(word)
    if embedding_vector is not None:
    # words not found in embedding index will be all-zeros.
    embedding_matrix[i] = embedding_vector
  3. Build embedding layer
    1
    2
    3
    4
    5
    6
    7
    from keras.layers import Embedding

    embedding_layer = Embedding(len(word_index) + 1,
    EMBEDDING_DIM,
    weights=[embedding_matrix],
    input_length=MAX_SEQUENCE_LENGTH,
    trainable=False)

    Keras F1 Socre

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    def F1(y_true, y_pred):
    def recall(y_true, y_pred):
    """Recall metric.

    Only computes a batch-wise average of recall.

    Computes the recall, a metric for multi-label classification of
    how many relevant items are selected.
    """
    true_positives = K.sum(K.round(K.clip(y_true * y_pred, 0, 1)))
    possible_positives = K.sum(K.round(K.clip(y_true, 0, 1)))
    recall = true_positives / (possible_positives + K.epsilon())
    return recall

    def precision(y_true, y_pred):
    """Precision metric.

    Only computes a batch-wise average of precision.

    Computes the precision, a metric for multi-label classification of
    how many selected items are relevant.
    """
    true_positives = K.sum(K.round(K.clip(y_true * y_pred, 0, 1)))
    predicted_positives = K.sum(K.round(K.clip(y_pred, 0, 1)))
    precision = true_positives / (predicted_positives + K.epsilon())
    return precision
    precision = precision(y_true, y_pred)
    recall = recall(y_true, y_pred)
    return 2*((precision*recall)/(precision+recall+K.epsilon()))
    使用的时候:
    1
    2
    3
    4
    model.compile(
    loss='binary_crossentropy',
    optimizer=adam,metrics=[F1])
    print(model.summary())

    Keras Attention Layer

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    class Attention(Layer):
    def __init__(self, step_dim,
    W_regularizer=None, b_regularizer=None,
    W_constraint=None, b_constraint=None,
    bias=True, **kwargs):
    self.supports_masking = True
    # self.init = initializations.get('glorot_uniform')
    self.init = initializers.get('glorot_uniform')

    self.W_regularizer = regularizers.get(W_regularizer)
    self.b_regularizer = regularizers.get(b_regularizer)

    self.W_constraint = constraints.get(W_constraint)
    self.b_constraint = constraints.get(b_constraint)

    self.bias = bias
    self.step_dim = step_dim
    self.features_dim = 0
    super(Attention, self).__init__(**kwargs)

    def build(self, input_shape):
    assert len(input_shape) == 3

    self.W = self.add_weight((input_shape[-1],),
    initializer=self.init,
    name='{}_W'.format(self.name),
    regularizer=self.W_regularizer,
    constraint=self.W_constraint)
    self.features_dim = input_shape[-1]

    if self.bias:
    self.b = self.add_weight((input_shape[1],),
    initializer='zero',
    name='{}_b'.format(self.name),
    regularizer=self.b_regularizer,
    constraint=self.b_constraint)
    else:
    self.b = None

    self.built = True

    def compute_mask(self, input, input_mask=None):
    # do not pass the mask to the next layers
    return None

    def call(self, x, mask=None):
    input_shape = K.int_shape(x)

    features_dim = self.features_dim
    # step_dim = self.step_dim
    step_dim = input_shape[1]

    eij = K.reshape(K.dot(K.reshape(x, (-1, features_dim)), K.reshape(self.W, (features_dim, 1))), (-1, step_dim))

    if self.bias:
    eij += self.b[:input_shape[1]]

    eij = K.tanh(eij)

    a = K.exp(eij)

    # apply mask after the exp. will be re-normalized next
    if mask is not None:
    # Cast the mask to floatX to avoid float64 upcasting in theano
    a *= K.cast(mask, K.floatx())

    # in some cases especially in the early stages of training the sum may be almost zero
    # and this results in NaN's. A workaround is to add a very small positive number ε to the sum.
    a /= K.cast(K.sum(a, axis=1, keepdims=True) + K.epsilon(), K.floatx())

    a = K.expand_dims(a)
    weighted_input = x * a
    # print weigthted_input.shape
    return K.sum(weighted_input, axis=1)

    def compute_output_shape(self, input_shape):
    # return input_shape[0], input_shape[-1]
    return input_shape[0], self.features_dim
    # end Attention

    加载 IMDB 电影评论数据集

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    def load_imdb_sentiment_analysis_dataset(data_path, seed=123):
    """Loads the IMDb movie reviews sentiment analysis dataset.

    # Arguments
    data_path: string, path to the data directory.
    seed: int, seed for randomizer.

    # Returns
    A tuple of training and validation data.
    Number of training samples: 25000
    Number of test samples: 25000
    Number of categories: 2 (0 - negative, 1 - positive)

    # References
    Mass et al., http://www.aclweb.org/anthology/P11-1015

    Download and uncompress archive from:
    http://ai.stanford.edu/~amaas/data/sentiment/aclImdb_v1.tar.gz
    """
    imdb_data_path = os.path.join(data_path, 'aclImdb')

    # Load the training data
    train_texts = []
    train_labels = []
    for category in ['pos', 'neg']:
    train_path = os.path.join(imdb_data_path, 'train', category)
    for fname in sorted(os.listdir(train_path)):
    if fname.endswith('.txt'):
    with open(os.path.join(train_path, fname)) as f:
    train_texts.append(f.read())
    train_labels.append(0 if category == 'neg' else 1)

    # Load the validation data.
    test_texts = []
    test_labels = []
    for category in ['pos', 'neg']:
    test_path = os.path.join(imdb_data_path, 'test', category)
    for fname in sorted(os.listdir(test_path)):
    if fname.endswith('.txt'):
    with open(os.path.join(test_path, fname)) as f:
    test_texts.append(f.read())
    test_labels.append(0 if category == 'neg' else 1)

    # Shuffle the training data and labels.
    random.seed(seed)
    random.shuffle(train_texts)
    random.seed(seed)
    random.shuffle(train_labels)

    return ((train_texts, np.array(train_labels)),
    (test_texts, np.array(test_labels)))

    Keras EarlyStopping

    1
    2
    3
    4
    5
    6
    7
    8
    from keras.callbacks import EarlyStopping
    early_stopping = EarlyStopping(monitor='val_loss', patience=3)

    model.fit(x_train, y_train,
    batch_size=batch_size,
    epochs=train_epochs,
    validation_data=(x_val, y_val),verbose = 2,
    callbacks=[early_stopping])
  • monitor: 需要监视的量,val_loss,val_acc
  • patience: 当 early stop 被激活 (如发现 loss 相比上一个 epoch 训练没有下降),则经过 - —patience 个 epoch 后停止训练
  • verbose: 信息展示模式,默认为 1,显示详细信息,2 是一轮显示最终信息,0 是不显示。
  • mode: ‘auto’,’min’,’max’之一,在 min 模式训练,如果检测值停止下降则终止训练。在 max 模式下,当检测值不再上升的时候则停止训练。

Keras Adam 默认参数

Adam 优化器由 Kingma 和 Lei Ba 在 Adam: A method for stochasticoptimization。默认参数是文章中建议的。

1
keras.optimizers.Adam(lr=0.001, beta_1=0.9, beta_2=0.999, epsilon=1e-8, kappa=1-1e-8)

可以在模型early_stopping后换用低一点的学习率继续训练两个epoch。

一些经验

Reference

之前在处理 QA 语料库的时候,在分词和去停止词的时候消耗时间很长,所以专门搜了一些资料针对这个问题进行了一次优化,总结如下。

文本分词

使用 jieba 自带的并行分词

在分词前添加 jieba.enable_parallel(4) 就行了。但是我这里并没有这么做,主要是怕分词顺序出错了。

使用 jieba_fast

这是一个 cpython 的库,使用方法和 jieba 一致,Github 官网。官网的描述如下:

使用 cpython 重写了 jieba 分词库中计算 DAG 和 HMM 中的 vitrebi 函数,速度得到大幅提升。

去停止词

构建字典加速

我最开始使用的是把停止词读成列表,然后去列表里面查找,速度很慢。原先的代码如下:

1
2
3
4
5
6
7
8
9
10
11
def get_stopwords(self,stopwords_path):
stop_f = open(stopwords_path, "r", encoding='utf-8')
stop_words = list()
for line in stop_f.readlines():
line = line.strip()
if not len(line):
continue
stop_words.append(line)
stop_f.close()
# print('哈工大停止词表长度为:' + str(len(stop_words)))
return stop_words

改进之后,构建了停止词字典,速度提高了一倍左右。代码如下:
1
2
3
4
5
6
7
8
9
10
11
def get_stopwords(self,stopwords_path):
stop_f = open(stopwords_path, "r", encoding='utf-8')
stop_words = {}
for line in stop_f.readlines():
line = line.strip()
if not len(line):
continue
stop_words[line] = line
stop_f.close()
# print('哈工大停止词表长度为:' + str(len(stop_words)))
return stop_words

总结

经过以上改进,代码加速了 4 倍左右,提升还是很明显的。

数据总体概述

百度在 2017 年发布了大规模的中文 MRC 数据集:DuReader。相比以前的 MRC 数据集,DuReader 有以下特点:

  • 所有的问题、原文都来源于实际数据(百度搜索引擎数据和百度知道问答社区),答案是由人类回答的。
  • 数据集中包含大量的之前很少研究的是非和观点类的样本。
  • 每个问题都对应多个答案,数据集包含 200k 问题、1000k 原文和 420k 答案,是目前最大的中文 MRC 数据集。
    图 1 展示了 Reader 数据集与其他数据集的比较情况。

    图 1 Reader 数据集与其他数据集的比较
    阅读全文 »

  • 首先把进程放到后台
    1
    nohup python main.py &
  • 然后保持退出终端继续运行
    1
    2
    ctrl-z
    bg
  • 输出在 nohup.out 里面
  • 输入 fg,可以把任务调到前台并取消
  • 输入 jobs 显示后台进程

这是我找的一个 Tensorflow 的书,作者是刘光聪。书写的非常不错,我也借此机会学习一波。书中的 TensorFlow 使用的是 1.2 版本,目前来说算是很新的。
作者在前言里面写到:

这是一本剖析 TensorFlow 内核工作原理的书籍,并非讲述如何使用 TensorFlow 构建机器学习模型,也不会讲述应用 TensorFlow 的最佳实践。本书将通过剖析 TensorFlow 源代码的方式,揭示 TensorFlow 的系统架构、领域模型、工作原理、及其实现模式等相关内容,以便揭示内在的知识。

可以看出,这必定是一本干货满满的书。Github

阅读全文 »

招聘

陕西庆华汽车安全系统有限公司是中国兵器工业集团所属特种能源集团直管单位,是集科研、生产、销售为一体的高新技术企业,是国内最大的汽车安全系统用火工品制造商,具有年产点火具 3000 万发,微型气体发生器 770 万发,产气药 500 吨的能力。

阅读全文 »

在 NLP 领域中,文本分类舆情分析等任务相较于文本抽取,和摘要等任务更容易获得大量标注数据。因此在文本分类领域中深度学习相较于传统方法更容易获得比较好的效果。
文本分类领域比较重要的的深度学习模型主要有 FastText,TextCNN,HAN,DPCNN。

FastText

FastText 是 Facebook 于 2016 年开源的一个词向量计算和文本分类工具,在学术上并没有太大创新。但是它的优点也非常明显,在文本分类任务中,fastText(浅层网络)往往能取得和深度网络相媲美的精度,却在训练时间上比深度网络快许多数量级。在标准的多核 CPU 上, 能够训练 10 亿词级别语料库的词向量在 10 分钟之内,能够分类有着 30 万多类别的 50 多万句子在 1 分钟之内。

Reference

Memory Networks

论文简介

  1. Memory NetworksICLR 2015,Facebook AI Research
  2. Answering Reading Comprehension Using Memory Networks,Stanford

(2018 年 12 月 3 日补充:第二篇不是论文,应该是斯坦福写的一个类似教程之类的东西,但是写的太像论文了 (ˇˍˇ),我都搞混了 )

看名字就知道第一篇是原论文,第二篇是第一篇的实现。(这里必须吐槽一下,第一篇论文一个图都没有,让我这种看论文先看图的人瞬间丧失好感。第二篇就人性化的多,简单的一个模型架构图就能瞬间让人领悟许多。)

  • 起因:作者认为 RNN、Attention 机制把很长的语句编码为定长向量 (Context Vector) 作为模型的记忆,会带来很大的信息压缩损失
  • 想法: 既然是因为向量太小带来的问题,那就直接加一个外部记忆模块,称之为 Memory
  • 评价:开山之作,但是模型比较简单,也没有特别具体的说明。

在第二篇论文里面的引言部分有一些比较精辟的话,我也摘录出来,不妨作为 NLP 的部分真理以供参考。

  1. QA 是最容易评估 AI 的标准。(这个容易理解,图灵测试不就是这样吗?)
  2. AI 要想很好的完成 QA 任务,要在两个方面得到提高:检索和推理。
  3. 单纯的记忆是不够的,他们需要一些知识去检索。(人不也经常使用浏览器吗?)
阅读全文 »