NLP入门系列五:基于双塔结构的语义匹配模型
语义匹配
文本语义匹配一般是计算两端文本之前的语义相关性或相似度,在搜索和自动问答中应用广泛。用深度学习方法解决这类问题一般有两种思路,以搜索中的query和doc为例:一种是query和doc分别学习一个向量表示,最后计算cos相似度;另一种是query和doc先融合,然后接多层网络,最后变成分类问题来优化。
详细可参考:《搜索与推荐中的深度学习匹配》之搜索篇
本文主要将的是第一种情况,就是所谓的双塔结构。
双塔结构
双塔结构总体如下图,一般有两个输入,然后分别接一个表示层,表示层输出向量,最后计算两个向量的相似度得到匹配分。这个结构的可改动点一般就是中间的表示层,表示层可大可小,小点就是RNN和CNN,大一点就是Transformer和Bert模型,都可以使用。输出的匹配层一般用cos相似度计算。
下面以TextCNN和CDSSM模型为基础表示层,构建双塔结构模型做语义匹配。
基于TextCNN的语义匹配
完整示例代码参考zh_sent_semantic_match.ipynb:https://github.com/huanghao128/zh-nlp-demo
TextCNN模型主要用在文本分类任务,通过不同的卷积窗口大小,来捕捉文本序列信息。模型结构如下图所示。
下面利用TextCNN的倒数第2层的输出来作为文本向量表示,完成双塔模型构建。
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import backend as K
print(tf.__version__)
class BaseTextCNN(keras.Model):
def __init__(self, filters, kernel_sizes, output_dim, name):
super(BaseTextCNN, self).__init__(name=name)
self.kernel_sizes = kernel_sizes
self.conv_layers = []
self.max_poolings = []
for kernel_size in kernel_sizes:
self.conv_layers.append(
keras.layers.Conv1D(filters=filters, kernel_size=kernel_size,
activation='relu', padding="same")
)
self.max_poolings.append(keras.layers.GlobalMaxPool1D())
self.concatenate = keras.layers.Concatenate()
self.dense = keras.layers.Dense(output_dim, activation='tanh')
def call(self, inputs):
convs = []
for i in range(len(self.kernel_sizes)):
x = self.conv_layers[i](inputs)
x = self.max_poolings[i](x)
convs.append(x)
x = self.concatenate(convs)
output = self.dense(x)
return output
语义匹配的完整模型:
def match_model(vocab_size, max_len, embedding_size, filters, kernel_sizes, output_dim):
input1 = keras.layers.Input(name='sent1', shape=(max_len,))
input2 = keras.layers.Input(name='sent2', shape=(max_len,))
embedding = keras.layers.Embedding(vocab_size, embedding_size)
sent1_embed = embedding(input1)
sent2_embed = embedding(input2)
output_sent1 = BaseTextCNN(filters, kernel_sizes, output_dim, name="textcnn_left")(sent1_embed)
output_sent2 = BaseTextCNN(filters, kernel_sizes, output_dim, name="textcnn_right")(sent2_embed)
cosine_output = keras.layers.Dot(axes=[1, 1], normalize=True)([output_sent1, output_sent2])
outputs = keras.layers.Dense(1, activation='linear', name="output")(cosine_output)
model = keras.models.Model(inputs=[input1, input2], outputs=outputs)
return model
初始化一些参数,并打印模型结构。
K.clear_session()
max_len = 15 # 文本最大长度
vocab_size = len(vocab2id) # 词典大小
embedding_size = 128
filters = 200
kernel_sizes = [3,4,5]
output_dim = 100
model = match_model(vocab_size, max_len, embedding_size, filters, kernel_sizes, output_dim)
print(model.summary())
训练完后保存模型需要注意,上面BaseTextCNN是自定义模型,直接model.save()好像会有问题,可以用save_weights()或者tf.saved_model.save()
# 只保存模型权重,使用时模型结构需要重新初始化
model.save_weights("output/match_model_weight.h5")
# 保存为pb格式,可以直接使用tf-serving部署调用
tf.saved_model.save(model, "output/match_model")
基于CNN-DSSM的语义匹配
CNN-DSSM主要针对英文这种以字符为最小单元的语言,来做双塔结构的语义匹配。其中卷积的单元是letter-trigram,也就是把单词拆分成3个连续的字符,作为语义表示的最小单元。这种方法可以解决出现不在词典中的词问题,也能学习到语义表示。CNN-DSSM在双胎模型中的表示层结构图如下:
这里写一个简单的实现版本。
from tensorflow import keras
from tensorflow.keras import backend as K
def base_network(inputs, filters, kernel_size, output_dim, name):
x = keras.layers.Conv1D(filters=filters, kernel_size=kernel_size, strides=1,
activation='relu', padding="same")(inputs)
x = keras.layers.Dropout(0.2)(x)
x = keras.layers.GlobalMaxPool1D()(x)
output = keras.layers.Dense(output_dim, activation='tanh', name=name)(x)
return output
def create_model(max_len, trigram_size, filters, kernel_size, output_dim):
sent_left = keras.layers.Input(name='sent_left', shape=(max_len, trigram_size))
sent_right = keras.layers.Input(name='sent_right', shape=(max_len, trigram_size))
left_output = base_network(sent_left, filters, kernel_size, output_dim, name="left")
right_output = base_network(sent_right, filters, kernel_size, output_dim, name="right")
cosine_output = keras.layers.Dot(axes=[1, 1], normalize=True)([left_output, right_output])
outputs = keras.layers.Dense(num_classes, activation='linear')(cosine_output)
model = keras.models.Model(inputs=[sent_left, sent_right], outputs=outputs)
return model
模型初始化例子
max_len = 10 # 句子最大长度
trigram_size = 30000 # trigram大小
filters = 300
kernel_size = 3
output_dim = 128
model = create_model(max_len, trigram_size, filters, kernel_size, output_dim)
print(model.summary())
其他语义匹配模型
上面讲的是representation-based的方法,还有一类是interaction-based方法,如ARC-II、MatchPyramid、Match-SRNN等,这些在开源项目MatchZoo中都有实现。
参考文档
[1]. https://www.jiqizhixin.com/articles/2017-06-15-5
[2]. https://zhuanlan.zhihu.com/p/38296950
[3]. https://www.cnblogs.com/guoyaohua/p/9229190.html
[4]. https://github.com/NTMC-Community/MatchZoo