Kaggle NLP Disaster Twitter竞赛的解决方案(基于TensorFlow 2.x实现)

最近打算深入研究一下NLP,先拿Kaggle上面的NLP的竞赛来练一下手。

之前我已经参加过一个Kaggle NLP的竞赛,题目是根据推特的内容以及情感分类标签,判断推特里面的那一部分内容支持这个情感分类标签的,具体可见我之前的博客,https://blog.csdn.net/gzroy/article/details/107739963

这次的NLP竞赛,是Kaggle专门为NLP准备的一个练手的竞赛,需要根据推特的内容来判断是否和灾难主题相关,竞赛的具体内容和介绍可见链接,https://blog.csdn.net/gzroy/article/details/107739963

TensorFlow的最新版本里面推出了Tensorflow Text的库,里面集成了很多预训练好的模型,例如BERT等等。因此我也基于这个最新的Text库,来构建代码。这里列出了两种思路,一个是基于BERT模型对推特内容进行编码,得到编码向量之后再添加一个Dense层,最后做Sigmoid激活得到分类的概率值。另一个思路是用传统的word embedding+双向LSTM来构建模型。

基于BERT模型进行文本分类

1. 数据预处理

数据集包括了train.csv和test.csv,train.csv的内容如下:

id keyword location text target
1 Our Deeds are the Reason of this #earthquake May ALLAH Forgive us all 1
4 Forest fire near La Ronge Sask. Canada 1
5 All residents asked to 'shelter in place' are being notified by officers. No other evacuation or shelter in place orders are expected 1
49 ablaze Est. September 2012 - Bristol We always try to bring the heavy. #metal #RT http://t.co/YAo1e0xngw 0

其中target为1表示这条推特的内容和灾难相关。

从train.csv看到,大部分的推特都缺失了keyword和location的信息。在推特的内容里面,有很多@XXX或者http://t.co/xxxx的信息,这些和判断是否灾难是没有太大关系的。因此在数据的预处理的时候,我将只保留text和target,并且对text内容删去@xxx以及http链接的内容。

以下代码是对数据集进行预处理并构建一个tensorflow的dataset

import tensorflow as tf

LABEL_COLUMN = 'target'
def get_dataset(file_path):
    dataset = tf.data.experimental.make_csv_dataset(
      file_path,
      batch_size=32, 
      label_name=LABEL_COLUMN,
      select_columns=['text', 'target'],
      na_value="?",
      shuffle=True,
      num_epochs=1,
      ignore_errors=True)
    return dataset
raw_train_data = get_dataset('train.csv')
processed_train_data = raw_train_data\
    .map(lambda x,y:(tf.strings.regex_replace(x['text'], '@\w+', ''), y))\
    .map(lambda x,y:(tf.strings.regex_replace(x, 'http[s]*://[\w\.\-\/]+', ''), y))

2. 对文本内容进行token编码

以上步骤生成的数据集输出的是推特内容的byte编码,我们需要把这些byte编码转换成token id。因为我们要用BERT模型进行训练,因此也需要按照BERT模型的要求来进行token转换。TensorFlow Text提供了很方便的方式可以加载预训练好的BERT模型和Token转换工具。

以下代码列出了所有可选的BERT模型,我在这里选择了其中一个比较小的模型。

bert_model_name = 'small_bert/bert_en_uncased_L-4_H-512_A-8' 

map_name_to_handle = {
    'bert_en_uncased_L-12_H-768_A-12':
        'https://hub.tensorflow.google.cn/tensorflow/bert_en_uncased_L-12_H-768_A-12/3',
    'bert_en_cased_L-12_H-768_A-12':
        'https://hub.tensorflow.google.cn/tensorflow/bert_en_cased_L-12_H-768_A-12/3',
    'bert_multi_cased_L-12_H-768_A-12':
        'https://hub.tensorflow.google.cn/tensorflow/bert_multi_cased_L-12_H-768_A-12/3',
    'small_bert/bert_en_uncased_L-2_H-128_A-2':
        'https://hub.tensorflow.google.cn/tensorflow/small_bert/bert_en_uncased_L-2_H-128_A-2/1',
    'small_bert/bert_en_uncased_L-2_H-256_A-4':
        'https://hub.tensorflow.google.cn/tensorflow/small_bert/bert_en_uncased_L-2_H-256_A-4/1',
    'small_bert/bert_en_uncased_L-2_H-512_A-8':
        'https://hub.tensorflow.google.cn/tensorflow/small_bert/bert_en_uncased_L-2_H-512_A-8/1',
    'small_bert/bert_en_uncased_L-2_H-768_A-12':
        'https://hub.tensorflow.google.cn/tensorflow/small_bert/bert_en_uncased_L-2_H-768_A-12/1',
    'small_bert/bert_en_uncased_L-4_H-128_A-2':
        'https://hub.tensorflow.google.cn/tensorflow/small_bert/bert_en_uncased_L-4_H-128_A-2/1',
    'small_bert/bert_en_uncased_L-4_H-256_A-4':
        'https://hub.tensorflow.google.cn/tensorflow/small_bert/bert_en_uncased_L-4_H-256_A-4/1',
    'small_bert/bert_en_uncased_L-4_H-512_A-8':
        'https://hub.tensorflow.google.cn/tensorflow/small_bert/bert_en_uncased_L-4_H-512_A-8/1',
    'small_bert/bert_en_uncased_L-4_H-768_A-12':
        'https://hub.tensorflow.google.cn/tensorflow/small_bert/bert_en_uncased_L-4_H-768_A-12/1',
    'small_bert/bert_en_uncased_L-6_H-128_A-2':
        'https://hub.tensorflow.google.cn/tensorflow/small_bert/bert_en_uncased_L-6_H-128_A-2/1',
    'small_bert/bert_en_uncased_L-6_H-256_A-4':
        'https://hub.tensorflow.google.cn/tensorflow/small_bert/bert_en_uncased_L-6_H-256_A-4/1',
    'small_bert/bert_en_uncased_L-6_H-512_A-8':
        'https://hub.tensorflow.google.cn/tensorflow/small_bert/bert_en_uncased_L-6_H-512_A-8/1',
    'small_bert/bert_en_uncased_L-6_H-768_A-12':
        'https://hub.tensorflow.google.cn/tensorflow/small_bert/bert_en_uncased_L-6_H-768_A-12/1',
    'small_bert/bert_en_uncased_L-8_H-128_A-2':
        'https://hub.tensorflow.google.cn/tensorflow/small_bert/bert_en_uncased_L-8_H-128_A-2/1',
    'small_bert/bert_en_uncased_L-8_H-256_A-4':
        'https://hub.tensorflow.google.cn/tensorflow/small_bert/bert_en_uncased_L-8_H-256_A-4/1',
    'small_bert/bert_en_uncased_L-8_H-512_A-8':
        'https://hub.tensorflow.google.cn/tensorflow/small_bert/bert_en_uncased_L-8_H-512_A-8/1',
    'small_bert/bert_en_uncased_L-8_H-768_A-12':
        'https://hub.tensorflow.google.cn/tensorflow/small_bert/bert_en_uncased_L-8_H-768_A-12/1',
    'small_bert/bert_en_uncased_L-10_H-128_A-2':
        'https://hub.tensorflow.google.cn/tensorflow/small_bert/bert_en_uncased_L-10_H-128_A-2/1',
    'small_bert/bert_en_uncased_L-10_H-256_A-4':
        'https://hub.tensorflow.google.cn/tensorflow/small_bert/bert_en_uncased_L-10_H-256_A-4/1',
    'small_bert/bert_en_uncased_L-10_H-512_A-8':
        'https://hub.tensorflow.google.cn/tensorflow/small_bert/bert_en_uncased_L-10_H-512_A-8/1',
    'small_bert/bert_en_uncased_L-10_H-768_A-12':
        'https://hub.tensorflow.google.cn/tensorflow/small_bert/bert_en_uncased_L-10_H-768_A-12/1',
    'small_bert/bert_en_uncased_L-12_H-128_A-2':
        'https://hub.tensorflow.google.cn/tensorflow/small_bert/bert_en_uncased_L-12_H-128_A-2/1',
    'small_bert/bert_en_uncased_L-12_H-256_A-4':
        'https://hub.tensorflow.google.cn/tensorflow/small_bert/bert_en_uncased_L-12_H-256_A-4/1',
    'small_bert/bert_en_uncased_L-12_H-512_A-8':
        'https://hub.tensorflow.google.cn/tensorflow/small_bert/bert_en_uncased_L-12_H-512_A-8/1',
    'small_bert/bert_en_uncased_L-12_H-768_A-12':
        'https://hub.tensorflow.google.cn/tensorflow/small_bert/bert_en_uncased_L-12_H-768_A-12/1',
    'albert_en_base':
        'https://hub.tensorflow.google.cn/tensorflow/albert_en_base/2',
    'electra_small':
        'https://hub.tensorflow.google.cn/google/electra_small/2',
    'electra_base':
        'https://hub.tensorflow.google.cn/google/electra_base/2',
    'experts_pubmed':
        'https://hub.tensorflow.google.cn/google/experts/bert/pubmed/2',
    'experts_wiki_books':
        'https://hub.tensorflow.google.cn/google/experts/bert/wiki_books/2',
    'talking-heads_base':
        'https://hub.tensorflow.google.cn/tensorflow/talkheads_ggelu_bert_en_base/1',
}

map_model_to_preprocess = {
    'bert_en_uncased_L-12_H-768_A-12':
        'https://hub.tensorflow.google.cn/tensorflow/bert_en_uncased_preprocess/3',
    'bert_en_cased_L-12_H-768_A-12':
        'https://hub.tensorflow.google.cn/tensorflow/bert_en_cased_preprocess/3',
    'small_bert/bert_en_uncased_L-2_H-128_A-2':
        'https://hub.tensorflow.google.cn/tensorflow/bert_en_uncased_preprocess/3',
    'small_bert/bert_en_uncased_L-2_H-256_A-4':
        'https://hub.tensorflow.google.cn/tensorflow/bert_en_uncased_preprocess/3',
    'small_bert/bert_en_uncased_L-2_H-512_A-8':
        'https://hub.tensorflow.google.cn/tensorflow/bert_en_uncased_preprocess/3',
    'small_bert/bert_en_uncased_L-2_H-768_A-12':
        'https://hub.tensorflow.google.cn/tensorflow/bert_en_uncased_preprocess/3',
    'small_bert/bert_en_uncased_L-4_H-128_A-2':
        'https://hub.tensorflow.google.cn/tensorflow/bert_en_uncased_preprocess/3',
    'small_bert/bert_en_uncased_L-4_H-256_A-4':
        'https://hub.tensorflow.google.cn/tensorflow/bert_en_uncased_preprocess/3',
    'small_bert/bert_en_uncased_L-4_H-512_A-8':
        'https://hub.tensorflow.google.cn/tensorflow/bert_en_uncased_preprocess/3',
    'small_bert/bert_en_uncased_L-4_H-768_A-12':
        'https://hub.tensorflow.google.cn/tensorflow/bert_en_uncased_preprocess/3',
    'small_bert/bert_en_uncased_L-6_H-128_A-2':
        'https://hub.tensorflow.google.cn/tensorflow/bert_en_uncased_preprocess/3',
    'small_bert/bert_en_uncased_L-6_H-256_A-4':
        'https://hub.tensorflow.google.cn/tensorflow/bert_en_uncased_preprocess/3',
    'small_bert/bert_en_uncased_L-6_H-512_A-8':
        'https://hub.tensorflow.google.cn/tensorflow/bert_en_uncased_preprocess/3',
    'small_bert/bert_en_uncased_L-6_H-768_A-12':
        'https://hub.tensorflow.google.cn/tensorflow/bert_en_uncased_preprocess/3',
    'small_bert/bert_en_uncased_L-8_H-128_A-2':
        'https://hub.tensorflow.google.cn/tensorflow/bert_en_uncased_preprocess/3',
    'small_bert/bert_en_uncased_L-8_H-256_A-4':
        'https://hub.tensorflow.google.cn/tensorflow/bert_en_uncased_preprocess/3',
    'small_bert/bert_en_uncased_L-8_H-512_A-8':
        'https://hub.tensorflow.google.cn/tensorflow/bert_en_uncased_preprocess/3',
    'small_bert/bert_en_uncased_L-8_H-768_A-12':
        'https://hub.tensorflow.google.cn/tensorflow/bert_en_uncased_preprocess/3',
    'small_bert/bert_en_uncased_L-10_H-128_A-2':
        'https://hub.tensorflow.google.cn/tensorflow/bert_en_uncased_preprocess/3',
    'small_bert/bert_en_uncased_L-10_H-256_A-4':
        'https://hub.tensorflow.google.cn/tensorflow/bert_en_uncased_preprocess/3',
    'small_bert/bert_en_uncased_L-10_H-512_A-8':
        'https://hub.tensorflow.google.cn/tensorflow/bert_en_uncased_preprocess/3',
    'small_bert/bert_en_uncased_L-10_H-768_A-12':
        'https://hub.tensorflow.google.cn/tensorflow/bert_en_uncased_preprocess/3',
    'small_bert/bert_en_uncased_L-12_H-128_A-2':
        'https://hub.tensorflow.google.cn/tensorflow/bert_en_uncased_preprocess/3',
    'small_bert/bert_en_uncased_L-12_H-256_A-4':
        'https://hub.tensorflow.google.cn/tensorflow/bert_en_uncased_preprocess/3',
    'small_bert/bert_en_uncased_L-12_H-512_A-8':
        'https://hub.tensorflow.google.cn/tensorflow/bert_en_uncased_preprocess/3',
    'small_bert/bert_en_uncased_L-12_H-768_A-12':
        'https://hub.tensorflow.google.cn/tensorflow/bert_en_uncased_preprocess/3',
    'bert_multi_cased_L-12_H-768_A-12':
        'https://hub.tensorflow.google.cn/tensorflow/bert_multi_cased_preprocess/3',
    'albert_en_base':
        'https://hub.tensorflow.google.cn/tensorflow/albert_en_preprocess/3',
    'electra_small':
        'https://hub.tensorflow.google.cn/tensorflow/bert_en_uncased_preprocess/3',
    'electra_base':
        'https://hub.tensorflow.google.cn/tensorflow/bert_en_uncased_preprocess/3',
    'experts_pubmed':
        'https://hub.tensorflow.google.cn/tensorflow/bert_en_uncased_preprocess/3',
    'experts_wiki_books':
        'https://hub.tensorflow.google.cn/tensorflow/bert_en_uncased_preprocess/3',
    'talking-heads_base':
        'https://hub.tensorflow.google.cn/tensorflow/bert_en_uncased_preprocess/3',
}

tfhub_handle_encoder = map_name_to_handle[bert_model_name]
tfhub_handle_preprocess = map_model_to_preprocess[bert_model_name]

bert_preprocess_model = hub.KerasLayer(tfhub_handle_preprocess)
bert_model = hub.KerasLayer(tfhub_handle_encoder)

让我们测试一下这个token编码的结果

bert_model = hub.KerasLayer(tfhub_handle_encoder)
text_preprocessed = bert_preprocess_model(['This is a test!'])

print(f'Keys       : {list(text_preprocessed.keys())}')
print(f'Shape      : {text_preprocessed["input_word_ids"].shape}')
print(f'Word Ids   : {text_preprocessed["input_word_ids"][0, :12]}')
print(f'Input Mask : {text_preprocessed["input_mask"][0, :12]}')
print(f'Type Ids   : {text_preprocessed["input_type_ids"][0, :12]}')

输出结果如下:

Keys       : ['input_word_ids', 'input_mask', 'input_type_ids']
Shape      : (1, 128)
Word Ids   : [ 101 2023 2003 1037 3231  999  102    0    0    0    0    0]
Input Mask : [1 1 1 1 1 1 1 0 0 0 0 0]
Type Ids   : [0 0 0 0 0 0 0 0 0 0 0 0]

其中的Word Ids是对文本进行小写转换以及分词后,根据词汇表映射得到的ID。Input Mask用于表示哪些ID是有效的,哪些是Padding。Type Ids是用于当每个文本是由两个句子组成的场景下,0表是第一个句子,1表示第2个句子,在我们这个场景中用不到。

下载这个预处理模型的词汇表,可以看到Word Ids对应的token如下:

['[CLS]', 'this', 'is', 'a', 'test', '!', '[SEP]', '[PAD]', '[PAD]', '[PAD]', '[PAD]', '[PAD]', '[PAD]', '[PAD]', '[PAD]', '[PAD]', '[PAD]', '[PAD]', '[PAD]', '[PAD]', '[PAD]', '[PAD]', '[PAD]', '[PAD]', '[PAD]']

3. 搭建文本分类模型

预处理完成后就可以搭建一个文本分类模型了。这个模型的输入是Dataset的文本内容,然后通过一个预处理层转换为word id,之后通过预训练好的BERT模型处理后输出一个维度为512的嵌入向量(pooled_output),这个向量反映了文本的隐含的意思。然后通过一个dropout层(随机屏蔽10%的神经元连接以避免过拟合),最后通过一个512*1的Dense Layer输出一个二分类的数值,通过Sigmoid激活之后即可得到一个概率值,表示这个文本是否和灾难相关。

代码如下:

def build_classifier_model():
    text_input = tf.keras.layers.Input(shape=(), dtype=tf.string, name='text')
    preprocessing_layer = hub.KerasLayer(tfhub_handle_preprocess, name='preprocessing')
    encoder_inputs = preprocessing_layer(text_input)
    encoder = hub.KerasLayer(tfhub_handle_encoder, trainable=True, name='BERT_encoder')
    outputs = encoder(encoder_inputs)
    net = outputs['pooled_output']
    net = tf.keras.layers.Dropout(0.1)(net)
    net = tf.keras.layers.Dense(1, activation=None, name='classifier')(net)
    return tf.keras.Model(text_input, net)

epochs = 10
steps_per_epoch = 238
num_train_steps = steps_per_epoch * epochs
num_warmup_steps = int(0.1*num_train_steps)
init_lr = 3e-5

loss = tf.keras.losses.BinaryCrossentropy(from_logits=True)
metrics = tf.metrics.BinaryAccuracy()

4. 模型训练

考虑到训练数据集比较小,这里采用kfold(k=5)来构建训练集和验证集。每个Fold训练的时候都训练一个新的模型。最后再对测试集做预测的时候,集成这个5个模型的预测结果,通过投票的方式来进行预测。

Tensorflow里面没有直接的方法来构建KFOLD,不像SKLEARN。这里我采用先把数据集通过SHARD的方式分成5份,每次训练的时候把其中4份组合成一个训练集,剩下1份作为验证集。

每个Fold训练10个EPOCH,如果验证集的LOSS有降低则保存模型参数。

代码如下:

kfold = 5
dataset_list = []
for i in range(kfold):
    dataset_list.append(processed_train_data.shard(num_shards=kfold, index=i))

#Train for 5 fold, after each fold training there is a model saved
for i in range(kfold):
    print("#######################################################")
    print("###Start Fold " + str(i) + " Training")
    print("#######################################################")
    dataset_list_copy = dataset_list.copy()
    val_ds = dataset_list_copy.pop(i)
    train_ds = dataset_list_copy[0]
    for j in range(1, kfold-1):
        train_ds = train_ds.concatenate(dataset_list_copy[j])
    #For each fold training, there will train 10 epochs, only save the model weights with best val_loss
    sv = tf.keras.callbacks.ModelCheckpoint(
        'twitter-%i.h5'%(i), monitor='val_loss', verbose=1, save_best_only=True,
        save_weights_only=True, mode='auto', save_freq='epoch')
    optimizer = optimization.create_optimizer(
        init_lr=init_lr,
        num_train_steps=num_train_steps,
        num_warmup_steps=num_warmup_steps,
        optimizer_type='adamw')
    classifier_model = build_classifier_model()
    classifier_model.compile(optimizer=optimizer, loss=loss, metrics=metrics)
    history = classifier_model.fit(
        x=train_ds,
        validation_data=val_ds,
        callbacks=[sv],
        epochs=epochs)

5. 对测试数据集进行预测

模型训练完成之后,我们有了5个模型,通过集成学习的方式来做一个组合,通过投票的方式来进行预测。

首先是构建测试数据集,这里需要把每条数据的ID加进去,因为最后生成submission.csv文件的时候需要有ID,代码如下:

def get_test_dataset(file_path):
    dataset = tf.data.experimental.make_csv_dataset(
      file_path,
      batch_size=1,
      select_columns=['id', 'text'],
      na_value="?",
      shuffle=False,
      num_epochs=1,
      ignore_errors=True)
    return dataset
raw_test_data = get_test_dataset('test.csv')
processed_test_data = raw_test_data\
    .map(lambda x:(x['id'], tf.strings.regex_replace(x['text'], '@\w+', '')))\
    .map(lambda x,y:(x, tf.strings.regex_replace(y, 'http[s]*://[\w\.\-\/]+', '')))

result_dict = {}
result = ['id,target']
classifier_model = build_classifier_model()
for i in range(kfold):
    test_data = iter(processed_test_data)
    classifier_model.load_weights('twitter-%i.h5'%kfold)
    while(True):
        try:
            test_id, test_text = test_data.next()
            prob = tf.nn.sigmoid(classifier_model(test_text, training=False)).numpy()[0,0]
            if prob>=0.5:
                prob = 1
            else:
                prob = 0
            test_id = test_id.numpy()[0]
            if test_id in result_dict:
                result_dict[test_id] += prob
            else:
                result_dict[test_id] = prob
        except StopIteration:
            break

for key in result_dict:
    prob = result_dict[key]/kfold
    if prob>=0.5:
        prob = 1
    else:
        prob = 0
    result.append(str(key)+","+str(prob))
        
result_text = '\n'.join(result)
with open('submission.csv', 'w') as f:
    f.write(result_text)

把submission.csv文件上传之后,得到的分数是0.81581,这个成绩在3140个参赛选手之中排名665(剔除掉140个分数为1.0的选手,因为测试集的数据泄漏了),这个成绩为前20%以内。考虑到模型比较简单,应该还有进一步提高的空间,留待以后继续改进。

基于RNN模型进行分类

1. 文本预处理

这里对文本的预处理以及Token与ID的转换,和以上BERT模型的处理是一样的。

2. 模型的搭建

模型的搭建首先是一个Embedding层,把输入的文本ID映射为嵌入向量,这里我设置嵌入向量的维度为64,然后是搭建1个双向LSTM层,输出的维度也是64。之后可以再搭建一个双向LSTM层,输出的维度是32,最后再接一个全连接层,输出维度为1。

代码如下:

def build_rnn_classifier_model():
    text_input = tf.keras.layers.Input(shape=(), dtype=tf.string, name='text')
    preprocessing_layer = hub.KerasLayer(tfhub_handle_preprocess, name='preprocessing')
    encoder_inputs = preprocessing_layer(text_input)["input_word_ids"]
    embedding = tf.keras.layers.Embedding(vocab_size, 64, mask_zero=True)(encoder_inputs)
    net = tf.keras.layers.Bidirectional(tf.keras.layers.LSTM(64, return_sequences=True))(embedding)
    net = tf.keras.layers.Bidirectional(tf.keras.layers.LSTM(32))(net)
    net = tf.keras.layers.Dense(32, activation='relu')(net)
    net = tf.keras.layers.Dense(1, activation=None, name='classifier')(net)
    return tf.keras.Model(text_input, net)

3. 模型的训练

同样采用kfold的方式训练,如以下代码

kfold = 5
epochs = 20
dataset_list = []
for i in range(kfold):
    dataset_list.append(processed_train_data.shard(num_shards=kfold, index=i))

#Train for 5 fold, after each fold training there is a model saved
for i in range(kfold):
    print("#######################################################")
    print("###Start Fold " + str(i) + " Training")
    print("#######################################################")
    dataset_list_copy = dataset_list.copy()
    val_ds = dataset_list_copy.pop(i)
    train_ds = dataset_list_copy[0]
    for j in range(1, kfold-1):
        train_ds = train_ds.concatenate(dataset_list_copy[j])
    #For each fold training, there will train 10 epochs, only save the model weights with best val_loss
    sv = tf.keras.callbacks.ModelCheckpoint(
        'twitter-rnn-%i.h5'%(i), monitor='val_loss', verbose=1, save_best_only=True,
        save_weights_only=True, mode='auto', save_freq='epoch')
    rnn_model = build_rnn_classifier_model()
    rnn_model.compile(
        loss=tf.keras.losses.BinaryCrossentropy(from_logits=True),
        optimizer=tf.keras.optimizers.Adam(1e-4),
        metrics=['accuracy']
    )
    history = rnn_model.fit(
        x=train_ds,
        validation_data=val_ds,
        callbacks=[sv],
        epochs=epochs)

最后在测试集上的准确率比BERT模型稍微低一些。

你可能感兴趣的:(人工智能,Python编程,机器学习,tensorflow,自然语言处理)