最近打算深入研究一下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来构建模型。
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%以内。考虑到模型比较简单,应该还有进一步提高的空间,留待以后继续改进。
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模型稍微低一些。