开启NLP的大魔法阵——通过Keras-Bert操纵Bert

开启NLP的大魔法阵——通过Keras-Bert操纵Bert

在上一篇文章

可乐小埋酱:开启NLP的大魔法阵——一步一步教上手Bert的Fine Tuningzhuanlan.zhihu.com图标

小埋酱尝试了如何对Bert这个魔法阵进行微调,这一次,我们要尝试使用Bert来干点不可描述的事情啦。

由于谷歌放出来的源码结构比较乱,不那么工程化使用还得进行封装,实在是有点麻烦,所以小埋酱推荐使用Keras-Bert这个开源库,工程化做的比较好,适合在项目上使用

CyberZHG/keras-bertgithub.com图标

Bert有挺多使用场景,这次我们会从四个场景来进行尝试。分别是获取Embedding,句子对语义相似度,完形填空,上下文推断

获取Embedding

要能够愉快的把文本在机器学习或者机器学习上应用,Embedding是必不可少的一步,不然后续的运算几乎就没法进行下去了。所以第一步,我们先用Keras-Bert把Embedding取出来吧。Keras-Bert取编码有两种方式,一种是直接获取,不做任何调整的获取

embeddings = extract_embeddings(model_path, texts)

这样可以获得一个(n,768)的编码。不过不太推荐这种模式,总觉得灵活性太缺乏了,推荐下面这种方式去获取编码

tokens = self.tokenizer.tokenize(text)
indices, segments = self.tokenizer.encode(first=text, max_len=max_len)
predicts = self.model.predict([np.array([indices]), np.array([segments])])[0]

得到的依然是一个(n,768)的矩阵。拿到编码矩阵,接下来就可以为所欲为了

句子对语义相似度

得到了(n,768)的矩阵之后,要拿来做语义相似度的比较依然不那么方便,应该说拿来做各种下游任务都不方便,因为两个数组维度不一致,这个时候我们可以直接对数据进行加权,统一成(1,768)这样的矩阵。这样就很方便后续的计算啦。我们可以用Embedding之后的结果做一次余弦相似度的计算,来简单计算一下句子之间的语义相似度

    def extract_single_features(self, text, max_length=768, max_len=512):
        tokens = self.tokenizer.tokenize(text)
        indices, segments = self.tokenizer.encode(first=text, max_len=max_len)
        predicts = self.model.predict([np.array([indices]), np.array([segments])])[0]

        embeddings = []
        for i, token in enumerate(tokens):
            embeddings.append(predicts[i].tolist()[:max_length])
        embeddings = np.array(embeddings)
        return np.sum(embeddings, axis=0)

然后跑个单测看看效果

    def test_extract_embeddings(self):
        model = BertUtils(self.model_path)
        texts = ['机器故障啦', '今天天气真好', '系统无法开机']
        query_vec = model.extract_single_features('服务器开不了机了')
        arr = [model.extract_single_features(x) for x in texts]
        score = np.sum(np.array(query_vec) * np.array(arr), axis=1) / np.linalg.norm(np.array(arr), axis=1)
        topk_idx = np.argsort(score)[::-1]
        for idx in topk_idx:
            logger.info('> %s\t%s' % (score[idx], texts[idx]))

结果如下

[I 190903 09:40:49 bert_test:22] > 137.06061627796765	机器故障啦
[I 190903 09:40:49 bert_test:22] > 131.246721517941	系统无法开机
[I 190903 09:40:49 bert_test:22] > 108.0012904591702	今天天气真好

嗯,这个相似度看起来像模像样的

完型填空

Bert在训练的时候,就带了完成完型填空的这种能力,我们也用Keras-Bert来尝试一下完型填空,看看效果如何。在完成完型填空任务的时候,需要给定一下MASK的位置

    def predict_mask_sentence(self, text, start_mask_position, stop_mast_position):
        tokens = self.tokenizer.tokenize(text)
        for i in range(start_mask_position, stop_mast_position):
            tokens[i] = '[MASK]'
        indices = np.array([[self.token_dict[token] for token in tokens] + [0] * (512 - len(tokens))])
        segments = np.array([[0] * len(tokens) + [0] * (512 - len(tokens))])
        arr = [[0] * 512]
        for i in range(start_mask_position, stop_mast_position):
            arr[0][i] = 1
        masks = np.array(arr)
        print('Tokens:', tokens)
        predicts = self.model.predict([indices, segments, masks])[0].argmax(axis=-1).tolist()
        print('Fill with: ',
              list(map(lambda x: self.token_dict_inv[x], predicts[0][start_mask_position:stop_mast_position])))

写个单测试试

    def test_predict_mask_sentence(self):
        model = BertUtils(self.model_path, trainning_model=True)
        model.predict_mask_sentence('我们要做的最后一个试验就是上下文推断了', 10, 12)

结果

Tokens: ['[CLS]', '我', '们', '要', '做', '的', '最', '后', '一', '个', '[MASK]', '[MASK]', '就', '是', '上', '下', '文', '推', '断', '了', '[SEP]']
Fill with:  ['事', '情']

看起来还像模像样的嘛

上下文推断

我们要做的最后一个试验就是上下文推断了,这个特性我们可以用来做对话的上下文判断,事件的异常检测,属于一个挺有意思的特性。

    def predict_next_sentence(self, first_sentence, second_sentence):
        indices, segments = self.tokenizer.encode(first=first_sentence, second=second_sentence, max_len=512)
        masks = np.array([[0] * 512])
        predicts = self.model.predict([np.array([indices]), np.array([segments]), masks])[1]
        return bool(np.argmax(predicts, axis=-1)[0])

同样写个单测

    def test_predict_next_sentence(self):
        model = BertUtils(self.model_path, trainning_model=True)
        print(model.predict_next_sentence('我们要做的最后一个试验就是上下文推断了', '利用这个特性我们可以用来做对话的上下文判断'))

然鹅。。他判断这两个不是上下文关系=。=

好吧,换一个句子对

    def test_predict_next_sentence(self):
        model = BertUtils(self.model_path, trainning_model=True)
        print(model.predict_next_sentence('数学是利用符号语言研究數量、结构、变化以及空间等概念的一門学科', '任何一个希尔伯特空间都有一族标准正交基。'))

这个又判断是上下文了=。=,看来,上下文推断的场景,不做微调还真是不那么好用

发布于 2019-09-03

文章被以下专栏收录