前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >利用bert系列预训练模型在非结构化数据抽取数据

利用bert系列预训练模型在非结构化数据抽取数据

原创
作者头像
用户1750490
修改于 2020-01-13 04:25:54
修改于 2020-01-13 04:25:54
2.2K00
代码可运行
举报
文章被收录于专栏:钛问题钛问题
运行总次数:0
代码可运行

本文代码来源苏剑林老师bert4keras example中的例子。

https://github.com/bojone/bert4keras

中文数据中有一个数据是从非结构化文本中找到演艺圈相关实体的任务。

数据集是百度公开的一个数据集。

https://ai.baidu.com/broad/download?dataset=sked

今天这个文章主要讲的就是,怎么从非结构化文本中抽取出我们希望得到的结构化数据的任务。

下面是当前数据集中的例子,就是这样子。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
{
    "text": "《新駌鸯蝴蝶梦》是黄安的音乐作品,收录在《流金十载全记录》专辑中",
    "spo_list": [
        {
            "subject": "新駌鸯蝴蝶梦",
            "predicate": "所属专辑",
            "object": "流金十载全记录",
            "subject_type": "歌曲",
            "object_type": "音乐专辑"
        },
        {
            "subject": "新駌鸯蝴蝶梦",
            "predicate": "歌手",
            "object": "黄安",
            "subject_type": "歌曲",
            "object_type": "人物"
        }
    ]
}








我们选择的加载bert的模块是bert4keras

安装bert4keras

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
pip install git+https://www.github.com/bojone/bert4keras.git

训练代码如下

三元组抽取任务,基于“半指针-半标注”结构

文章介绍:https://kexue.fm/archives/7161

数据集:http://ai.baidu.com/broad/download?dataset=sked

最优f1=0.82198

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制


import json
import codecs
import numpy as np
import tensorflow as tf
from bert4keras.backend import keras, set_gelu, K
from bert4keras.layers import LayerNormalization
from bert4keras.tokenizer import Tokenizer
from bert4keras.bert import build_bert_model
from bert4keras.optimizers import Adam, ExponentialMovingAverage
from bert4keras.snippets import sequence_padding, DataGenerator
from keras.layers import *
from keras.models import Model
from tqdm import tqdm


maxlen = 128
batch_size = 64
config_path = 'wwm/bert_config.json'
checkpoint_path = 'wwm/bert_model.ckpt'
dict_path = 'wwm/vocab.txt'


def load_data(filename):
    D = []
    with codecs.open(filename, encoding='utf-8') as f:
        for l in f:
            l = json.loads(l)
            D.append({
                'text': l['text'],
                'spo_list': [
                    (spo['subject'], spo['predicate'], spo['object'])
                    for spo in l['spo_list']
                ]
            })
    return D


# 加载数据集
train_data = load_data('kg_huge/train_data.json')
valid_data = load_data('kg_huge/dev_data.json')
predicate2id, id2predicate = {}, {}

with codecs.open('kg_huge/all_50_schemas') as f:
    for l in f:
        l = json.loads(l)
        if l['predicate'] not in predicate2id:
            id2predicate[len(predicate2id)] = l['predicate']
            predicate2id[l['predicate']] = len(predicate2id)

# 建立分词器
tokenizer = Tokenizer(dict_path, do_lower_case=True)


def search(pattern, sequence):
    """从sequence中寻找子串pattern
    如果找到,返回第一个下标;否则返回-1"""
    n = len(pattern)
    for i in range(len(sequence)):
        if sequence[i:i + n] == pattern:
            return i
    return -1


class data_generator(DataGenerator):
    """数据生成器
    """
    def __iter__(self, random=False):
        idxs = list(range(len(self.data)))
        if random:
            np.random.shuffle(idxs)
        batch_token_ids, batch_segment_ids = [], []
        batch_subject_labels, batch_subject_ids, batch_object_labels = [], [], []
        for i in idxs:
            d = self.data[i]
            token_ids, segment_ids = tokenizer.encode(d['text'], max_length=maxlen)
            # 整理三元组 {s: [(o, p)]}
            spoes = {}
            for s, p, o in d['spo_list']:
                s = tokenizer.encode(s)[0][1:-1]
                p = predicate2id[p]
                o = tokenizer.encode(o)[0][1:-1]
                s_idx = search(s, token_ids)
                o_idx = search(o, token_ids)
                if s_idx != -1 and o_idx != -1:
                    s = (s_idx, s_idx + len(s) - 1)
                    o = (o_idx, o_idx + len(o) - 1, p)
                    if s not in spoes:
                        spoes[s] = []
                    spoes[s].append(o)
            if spoes:
                # subject标签
                subject_labels = np.zeros((len(token_ids), 2))
                for s in spoes:
                    subject_labels[s[0], 0] = 1
                    subject_labels[s[1], 1] = 1
                # 随机选一个subject
                start, end = np.array(list(spoes.keys())).T
                start = np.random.choice(start)
                end = np.random.choice(end[end >= start])
                subject_ids = (start, end)
                # 对应的object标签
                object_labels = np.zeros((len(token_ids), len(predicate2id), 2))
                for o in spoes.get(subject_ids, []):
                    object_labels[o[0], o[2], 0] = 1
                    object_labels[o[1], o[2], 1] = 1
                # 构建batch
                batch_token_ids.append(token_ids)
                batch_segment_ids.append(segment_ids)
                batch_subject_labels.append(subject_labels)
                batch_subject_ids.append(subject_ids)
                batch_object_labels.append(object_labels)
                if len(batch_token_ids) == self.batch_size or i == idxs[-1]:
                    batch_token_ids = sequence_padding(batch_token_ids)
                    batch_segment_ids = sequence_padding(batch_segment_ids)
                    batch_subject_labels = sequence_padding(batch_subject_labels, padding=np.zeros(2))
                    batch_subject_ids = np.array(batch_subject_ids)
                    batch_object_labels = sequence_padding(batch_object_labels, padding=np.zeros((len(predicate2id), 2)))
                    yield [
                        batch_token_ids, batch_segment_ids,
                        batch_subject_labels, batch_subject_ids, batch_object_labels
                    ], None
                    batch_token_ids, batch_segment_ids = [], []
                    batch_subject_labels, batch_subject_ids, batch_object_labels = [], [], []


def batch_gather(params, indices):
    """params.shape=[b, n, d],indices.shape=[b]
    从params的第i个序列中选出第indices[i]个向量,返回shape=[b, d]"""
    indices = K.cast(indices, 'int32')
    batch_idxs = K.arange(0, K.shape(indices)[0])
    indices = K.stack([batch_idxs, indices], 1)
    return tf.gather_nd(params, indices)


def extrac_subject(inputs):
    """根据subject_ids从output中取出subject的向量表征
    """
    output, subject_ids = inputs
    start = batch_gather(output, subject_ids[:, 0])
    end = batch_gather(output, subject_ids[:, 1])
    subject = K.concatenate([start, end], 1)
    return subject


# 补充输入
subject_labels = Input(shape=(None, 2), name='Subject-Labels')
subject_ids = Input(shape=(2, ), name='Subject-Ids')
object_labels = Input(shape=(None, len(predicate2id), 2), name='Object-Labels')

# 加载预训练模型
bert = build_bert_model(
    config_path=config_path,
    checkpoint_path=checkpoint_path,
    return_keras_model=False,
)

# 预测subject
output = Dense(units=2,
               activation='sigmoid',
               kernel_initializer=bert.initializer)(bert.model.output)
subject_preds = Lambda(lambda x: x**2)(output)

subject_model = Model(bert.model.inputs, subject_preds)

# 传入subject,预测object
# 通过Conditional Layer Normalization将subject融入到object的预测中
output = bert.model.layers[-2].get_output_at(-1)
subject = Lambda(extrac_subject)([output, subject_ids])
output = LayerNormalization(conditional=True)([output, subject])
output = Dense(units=len(predicate2id) * 2,
               activation='sigmoid',
               kernel_initializer=bert.initializer)(output)
output = Reshape((-1, len(predicate2id), 2))(output)
object_preds = Lambda(lambda x: x**4)(output)

object_model = Model(bert.model.inputs + [subject_ids], object_preds)

# 训练模型
train_model = Model(bert.model.inputs + [subject_labels, subject_ids, object_labels],
                    [subject_preds, object_preds])

mask = bert.model.get_layer('Sequence-Mask').output

subject_loss = K.binary_crossentropy(subject_labels, subject_preds)
subject_loss = K.mean(subject_loss, 2)
subject_loss = K.sum(subject_loss * mask) / K.sum(mask)

object_loss = K.binary_crossentropy(object_labels, object_preds)
object_loss = K.sum(K.mean(object_loss, 3), 2)
object_loss = K.sum(object_loss * mask) / K.sum(mask)

train_model.add_loss(subject_loss + object_loss)
train_model.compile(optimizer=Adam(1e-5))


def extract_spoes(text):
    """抽取输入text所包含的三元组
    """
    tokens = tokenizer.tokenize(text, max_length=maxlen)
    token_ids, segment_ids = tokenizer.encode(text, max_length=maxlen)
    # 抽取subject
    subject_preds = subject_model.predict([[token_ids], [segment_ids]])
    start = np.where(subject_preds[0, :, 0] > 0.6)[0]
    end = np.where(subject_preds[0, :, 1] > 0.5)[0]
    subjects = []
    for i in start:
        j = end[end >= i]
        if len(j) > 0:
            j = j[0]
            subjects.append((i, j))
    if subjects:
        spoes = []
        token_ids = np.repeat([token_ids], len(subjects), 0)
        segment_ids = np.repeat([segment_ids], len(subjects), 0)
        subjects = np.array(subjects)
        # 传入subject,抽取object和predicate
        object_preds = object_model.predict([token_ids, segment_ids, subjects])
        for subject, object_pred in zip(subjects, object_preds):
            start = np.where(object_pred[:, :, 0] > 0.6)
            end = np.where(object_pred[:, :, 1] > 0.5)
            for _start, predicate1 in zip(*start):
                for _end, predicate2 in zip(*end):
                    if _start <= _end and predicate1 == predicate2:
                        spoes.append((subject, predicate1, (_start, _end)))
                        break
        return [
            (
                tokenizer.decode(token_ids[0, s[0]:s[1] + 1], tokens[s[0]:s[1] + 1]),
                id2predicate[p],
                tokenizer.decode(token_ids[0, o[0]:o[1] + 1], tokens[o[0]:o[1] + 1])
            ) for s, p, o in spoes
        ]
    else:
        return []


class SPO(tuple):
    """用来存三元组的类
    表现跟tuple基本一致,只是重写了 __hash__ 和 __eq__ 方法,
    使得在判断两个三元组是否等价时容错性更好。
    """
    def __init__(self, spo):
        self.spox = (
            tuple(tokenizer.tokenize(spo[0])),
            spo[1],
            tuple(tokenizer.tokenize(spo[2])),
        )

    def __hash__(self):
        return self.spox.__hash__()

    def __eq__(self, spo):
        return self.spox == spo.spox


def evaluate(data):
    """评估函数,计算f1、precision、recall
    """
    X, Y, Z = 1e-10, 1e-10, 1e-10
    f = codecs.open('dev_pred.json', 'w', encoding='utf-8')
    pbar = tqdm()
    for d in data:
        R = set([SPO(spo) for spo in extract_spoes(d['text'])])
        T = set([SPO(spo) for spo in d['spo_list']])
        X += len(R & T)
        Y += len(R)
        Z += len(T)
        f1, precision, recall = 2 * X / (Y + Z), X / Y, X / Z
        pbar.update()
        pbar.set_description('f1: %.5f, precision: %.5f, recall: %.5f' %
                             (f1, precision, recall))
        s = json.dumps(
            {
                'text': d['text'],
                'spo_list': list(T),
                'spo_list_pred': list(R),
                'new': list(R - T),
                'lack': list(T - R),
            },
            ensure_ascii=False,
            indent=4)
        f.write(s + '\n')
    pbar.close()
    f.close()
    return f1, precision, recall


class Evaluator(keras.callbacks.Callback):
    """评估和保存模型
    """
    def __init__(self):
        self.best_val_f1 = 0.

    def on_epoch_end(self, epoch, logs=None):
        EMAer.apply_ema_weights()
        f1, precision, recall = evaluate(valid_data)
        if f1 >= self.best_val_f1:
            self.best_val_f1 = f1
            train_model.save_weights('best_model.weights')
        EMAer.reset_old_weights()
        print('f1: %.5f, precision: %.5f, recall: %.5f, best f1: %.5f\n' %
              (f1, precision, recall, self.best_val_f1))


if __name__ == '__main__':

    train_generator = data_generator(train_data, batch_size)
    evaluator = Evaluator()
    EMAer = ExponentialMovingAverage(0.999)

    train_model.fit_generator(train_generator.forfit(),
                             steps_per_epoch=len(train_generator),
                             epochs=20,
                             callbacks=[evaluator, EMAer])

else:

    train_model.load_weights('best_model.weights')

中文wwm下载地址

ymcui/Chinese-BERT-wwm​github.com

wwm小数据集训练截图

全量数据集第一轮

一轮就已经有79.5的准确率了

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
ASP.Net请求处理机制初步探索之旅 - Part 3 管道
开篇:上一篇我们了解了一个ASP.Net页面请求的核心处理入口,它经历了三个重要的入口,分别是:ISAPIRuntime.ProcessRequest()、HttpRuntime.ProcessRequest()以及HttpApplication.Init()。其中,在HttpApplication的Init()方法中触发了请求处理管道事件的执行,本篇我们就来看看所谓的请求处理管道。
Edison Zhou
2018/08/20
1.2K0
ASP.Net请求处理机制初步探索之旅 - Part 3 管道
ASP.NET MVC Controller激活系统详解:总体设计
我们将整个ASP.NET MVC框架划分为若干个子系统,那么针对请求上下文激活目标Controller对象的子系统被我们成为Controller激活系统。在正式讨论Controller对象具体是如何被创建爱之前,我们先来看看Controller激活系统在ASP.NET MVC中的总体设计,了解一下组成该子系统的一些基本的组件,以及它们对应的接口或者抽象类是什么。 目录 一、Controller 二、 ControllerFactory 三、ControllerBuilder     实例演示:如何提
蒋金楠
2018/01/15
1.8K0
ASP.NET MVC Controller激活系统详解:总体设计
ASP.NET MVC是如何运行的(3): Controller的激活
ASP.NET MVC的URL路由系统通过注册的路由表对HTTP请求进行解析从而得到一个用于封装路由数据的RouteData对象,而这个过程是通过自定义的UrlRoutingModule对HttpApplication的PostResolveRequestCache事件进行注册实现的。RouteData中已经包含了目标Controller的名称,现在我们来进一步分析真正的Controller对象是如何被激活的。我们首先需要了解一个类型为MvcRouteHandler的类型。 一、MvcRouteHandle
蒋金楠
2018/02/07
9300
ASP.NET MVC Preview生命周期分析
做ASP.NET WebForm开发都知道,ASP.NET有复杂的生命周期,学习ASP.NET MVC就要深入理解它的生命周期。今天从CodePlex上下载了ASP.NET Preview 2 的源代码,还有两个程序集Routing与Abstractions并未发布,不过这两个程序集的类并不多,可以用NET反编译工具 Reflector解开来看看,可惜这两个程序集用的是VS2008使用.net 3.5开发的,用了c# 3.0的很多特性,Reflector反编译不完全。 ASP.NET MVC通过HttpMo
张善友
2018/01/30
1.5K0
ASP.NET MVC是如何运行的[2]: URL路由
在一个ASP.NET MVC应用来说,针对HTTP请求的处理和相应定义Controller类型的某个Action方法中,每个HTTP请求的目标对象不再像ASP .NET Web Form应用一样是一个物理文件,而是某个Controller的某个Action。目标Controller和Action的名称包含在HTTP请求中,而ASP.NET MVC的首要任务就是通过当前HTTP请求的解析得到正确的Controller和Action的名称。这个过程是通过ASP.NET MVC的URL路由机制来实现的。 一、Ro
蒋金楠
2018/02/07
1.9K0
ASP.NET路由系统实现原理:HttpHandler的动态映射
我们知道一个请求最终通过一个具体的HttpHandler进行处理,而我们熟悉的用于表示一个Web页面的Page对象就是一个HttpHandler,被用于处理基于某个.aspx文件的请求。我们可以通过HttpHandler的动态映射来实现请求地址与物理文件路径之间的分离。实际上ASP.NET路由系统就是采用了这样的实现原理。如下图所示,ASP.NET路由系统通过一个注册到当前应用的自定义HttpModule对所有的请求进行拦截,并通过对请求的分析为之动态匹配一个用于处理它的HttpHandler。HttpHa
蒋金楠
2018/01/15
1.7K0
ASP.NET路由系统实现原理:HttpHandler的动态映射
通过一个模拟程序让你明白ASP.NET MVC是如何运行的
ASP.NET MVC的路由系统通过对HTTP请求的解析得到表示Controller、Action和其他相关的数据,并以此为依据激活Controller对象,调用相应的Action方法,并将方法返回的ActionResult写入HTTP回复中。为了更好的演示其实现原理,我创建一个简单的ASP.NET Web应用来模拟ASP.NET MVC的路由机制。这个例子中的相关组件基本上就是根据ASP.NET MVC的同名组件设计的,只是我将它们进行了最大限度的简化,因为我们只需要用它来演示大致的实现原理而已。[源代码
蒋金楠
2018/01/15
1.2K0
通过一个模拟程序让你明白ASP.NET MVC是如何运行的
.NET/ASP.NET Routing路由(深入解析路由系统架构原理)
王清培
2018/01/08
1.7K0
.NET/ASP.NET Routing路由(深入解析路由系统架构原理)
ASP.NET MVC涉及到的5个同步与异步,你是否傻傻分不清楚?[上篇]
Action方法的执行具有两种基本的形式,即同步执行和异步执行,而在ASP.NETMVC的整个体系中涉及到很多同步/异步的执行方式,虽然在前面相应的文章中已经对此作了相应的介绍,为了让读者对此有一个整
蒋金楠
2018/01/15
9010
Asp.Net MVC3 简单入门第一季(四)详解Request Processing Pipeline
      很久没更新了,今天写点关于Asp.Net MVC的PipeLine。首先我们确认一点,Asp.Net WebFrom和Asp.Net MVC是在.Net平台下的两种web开发方式。其实他们都是基于Asp.Net Core的不同表现而已。看下面一张图,我们就能理解了WebForm和Asp.Net MVC的一个关系了。
老马
2022/05/10
6690
Asp.Net MVC3 简单入门第一季(四)详解Request Processing Pipeline
自己动手写一个简单的MVC框架(第二版)
  在ASP.NET MVC中,最核心的当属“路由系统”,而路由系统的核心则源于一个强大的System.Web.Routing.dll组件。
Edison Zhou
2018/08/21
1.5K0
自己动手写一个简单的MVC框架(第二版)
ASP.NET MVC5请求管道和生命周期
请求管道是一些用于处理HTTP请求的模块组合,在ASP.NET中,请求管道有两个核心组件:IHttpModule和IHttpHandler。所有的HTTP请求都会进入IHttpHandler,有IHttpHandler进行最终的处理,而IHttpModule通过订阅HttpApplication对象中的事件,可以在IHttpHandler对HTTP请求进行处理之前对请求进行预处理或IHttpHandler对HTTP请求处理之后进行再次处理。
雪飞鸿
2018/09/05
1.8K0
ASP.NET MVC5请求管道和生命周期
精通MVC3摘译(3)-自定义路由系统
如果你不喜欢标准路由对象匹配URL的方式,或者你想实现一些特殊的接口,你可以从RouteBase中继承一个类。让你可以控制URL匹配,参数如何解析,URL链接如何生成。从RouteBase继承,你需要实现2个方法:
py3study
2020/01/03
5980
精通MVC3摘译(3)-自定义路由系统
快速入门系列--MVC--02路由
    现在补上URL路由的学习,至于蒋老师自建的MVC小引擎和相关案例就放在论文提交后再实践咯。通过ASP.NET的路由系统,可以完成请求URL与物理文件的分离,其优点是:灵活性、可读性、SEO优化。接下来通过一个最简单的路由例子进入这部分的学习,这是一个蒋老师提供的WebForm路由的例子,回想起刚做ASP.NET时,每次看到.aspx页面的前台代码时的茫然和无措,茫茫多的标签,属性,数据源的绑定吓死小兄弟俺了,也花过不少时间去理解记忆,效果不也不大。现在回头看看感觉好了很多,看到IsPostback老
用户1216676
2018/01/24
9010
快速入门系列--MVC--02路由
.NET/ASP.NET MVC Controller 控制器(深入解析控制器运行原理)
王清培
2018/01/08
1.2K0
.NET/ASP.NET MVC Controller 控制器(深入解析控制器运行原理)
在ASP.NET MVC中使用Unity进行依赖注入的三种方式第一种方法第二种方法第三种方法
     在ASP.NET MVC4中,为了在解开Controller和Model的耦合,我们通常需要在Controller激活系统中引入IoC,用于处理用户请求的Controller,让Controller依赖于ModelRepository的抽象而不是它的实现。      我们可以在三个阶段使用IoC实现上面所说的解耦操作,首先需要简单介绍一下默认情况下Controller的激活过程: 用户发送请求黑ASP.NET,路由系统对请求进行解析,根据注册的路由规则对请求进行匹配,解析出Controller和A
小白哥哥
2018/03/07
1K0
ASP.NET MVC路由扩展:路由映射
上周我写了三篇文章(一、二、三)详细地介绍了ASP.NET的路由系统。ASP.NET的路由系统旨在通过注册URL模板与物理文件之间的映射进而实现请求地址与文件路径之间的分离,但是对于ASP.NET MVC应用来说,请求的目标不再是一个具体的物理文件,而是定义在某个Controller类型中的Action方法。出于自身路由特点的需要,ASP.NET对ASP.NET的路由系统进行了相应的扩展。 目录 一、基本路由映射 二、实例演示:注册路由映射与查看路由信息 三、基于A
蒋金楠
2018/01/15
1.4K0
ASP.NET MVC路由扩展:路由映射
自己动手写一个简单的MVC框架(第一版)
  路由(Route)、控制器(Controller)、行为(Action)、模型(Model)、视图(View)
Edison Zhou
2018/08/20
1.1K0
自己动手写一个简单的MVC框架(第一版)
Asp.Net Web API中使用Session,Cache和Application的几个方法
    在ASP.NET中,Web Api的控制器类派生于ApiController,该类与ASP.NET的Control类没有直接关系,因此不能像在Web MVC中直接使用HttpContext,Cache,Session等,要使用的话,一般是从System.Web.HttpContext.Current静态对象引用HttpContext,从而使用Session等状态数据。
莫问今朝
2018/08/31
1.5K0
ASP.NET MVC Controller激活系统详解:默认实现
Controller激活系统最终通过注册的ControllerFactory创建相应的Conroller对象,如果没有对ControllerFactory类型或者类型进行显式注册(通过调用当前ControllerBuilder的SetControllerFactory方法),默认使用的是一个DefaultControllerFactory对象,我们现在就来讨论实现在DefaultControllerFactory类型中的默认Controller激活机制。 目录 一、Controller类型的解
蒋金楠
2018/01/15
1.3K0
推荐阅读
相关推荐
ASP.Net请求处理机制初步探索之旅 - Part 3 管道
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档