Deepdive 教程--数据准备

Deepdive 教程–数据准备

教程列表

0 基础工作

本文的数据和工具都来自于http://www.openkg.cn/dataset/cn-deepdive,然后将其文档按照自己的理解写了出来,并且对其中的一些细节内容加入了写详细的描述

本文假设读者已经安装好deepdive了,如果没有安装,请参见上面链接中的教程文档,或者自行百度

我们的DeepDive的数据(包括输入,输出,中间media)全都存在关系数据库中,强烈推荐Postgres(说明其他类型的数据库也是支持的)我们配置db.url文件来设置本地数据库。

echo "postgresql://localhost:5432/deepdive_$USER" >db.url
#$符号是shell中用来表示变量的,所以这个蓝色部分大概就是 用户名@主机名:。。。。
#deepdive_spouse_$USER 我们这个项目的数据库名,也可以自定义。

1 数据准备

数据处理要有如下几个部分:

  • 载入原始输入数据
  • 添加自然语言处理(NLP)标记
  • 抽取候选关系
  • 为每个候选关系抽取特征

1.1 载入原始数据

我们需要在把articles文件放到input文件夹里面去,按照deepdive官方的Tutorial的方法,无法实现不知道为什么。

然后在app.ddlog 中添加如下内容:

aritcles(
	id		text,
	content	text
).

其实就是为了标记我们的articles文档中的列名,然后存到数据库中。

每次app.ddlog变动 后我们都要进行编译操作,也就是执行下面的代码:

deepdive compile

他会执行当前目录下的工作的编译。

编译完成后,我们要执行

deepdive do articles

这里do 后面应该和我们app.ddlog 中的名字相同,和input 文件夹中的文件名也相同,他们三个应该都是一致的。他会把input文件中的对应的文件导入postgresql数据库中。注意这个语句执行后,他会进入一个类似vi的界面让你审查他自动生成的处理代码是不是正确,这时候输入:wq 就可以保存并退出,继续执行后面的步骤。

执行完成后,可以执行下面代码在数据库中查询,看看是不是成功导入了。

deepdive query '?- articles(id, _).'

如果成功导入,那么他就会有几行数据,如果不成功,那么就会显示0 rows

1.2 添加自然语言标记

deepdive默认用standford nlp进行文本处理,可以返回句子的分词、lemma、pos、NER

自然语言处理 Natural Language Processing

分词:首先是中文分词,在一句话中,我们要把词分出来,而不是光看单独的子。比如 今天 高兴 选择合适的字组成合适的词来构成句子

lemma:词元,这个是指这个词实质上的含义,比如cat,cats他们有相同词元。

pos:词性标注,最基本的是动词、名词等等

NER:Named Entity Recognition,可以识别出地名、人名、组织等等

具体内容请自行学习自然语言处理。。。

现在我们有了文章了,所以下一步就是把文章拆分成更细致的东西sentences,所以,我们在数据库中需要有一个表来存储sentences的数据。同articles,我们要在app.ddlog 中新建一个表的定义:

sentences(
	doc_id			text,
	sentence_index 	int,
	sentence_text	text,
	tokens			text[],
	lemmas 			text[],
	pos_tags		text[],
	ner_tags		text[],
	doc_offsets		int[],
	dep_types		text[],
	dep_tokens		int[]
).

光有存储用的表的定义还不够,对吧,我们还需要对数据处理的方法,deepdive只是个框架,具体要怎么处理需要我们告诉他。所以我们要定义函数来处理articles让他变成sentences。

function nlp_markup over (
	doc_id text,
	content text
) returns rows like sentences
implementation "udf/nlp_markup.sh" handles tsv lines.

这里的语法我们记住这样用就可以了

function用来定义函数,后面nlp_markup 是函数名 over后面接的是参数表。

returns 说明了函数返回的形式,返回就像我们前面定义的sentences那样的一行。

最后一句说明了我们这个程序文件是udf/nlp_markup.sh ,输入是tsv的一行。

当然,光声明了函数是不行的,我们还要调用才会起作用对不啦,所以下面就是调用函数啦。

sentences+=nlp_markup(doc_id,content) :-
articles(doc_id,content).

上面的+= 其实和其他语言差不多,就是对于来源是articles中的每一行的doc_idcontent 我们都调用nlp_markup 然后结果添加到sentences表中。

#!/usr/bin/env bash
# A shell script that runs Bazaar/Parser over documents passed as input TSV lines
#
# $ deepdive env udf/nlp_markup.sh doc_id _ _ content _
##
set -euo pipefail
cd "$(dirname "$0")"

: ${BAZAAR_HOME:=$PWD/bazaar}
[[ -x "$BAZAAR_HOME"/parser/target/start ]] || {
    echo "No Bazaar/Parser set up at: $BAZAAR_HOME/parser"
    exit 2
} >&2

[[ $# -gt 0 ]] ||
    # default column order of input TSV
    set -- doc_id content

# convert input tsv lines into JSON lines for Bazaar/Parser


# start Bazaar/Parser to emit sentences TSV
tsv2json "$@" |
"$BAZAAR_HOME"/parser/run.sh -i json -k doc_id -v content

所有#开头的(除了#!)都是普通注释

参数的用法(

  • $0:调用文件使用的文件名,带有前面的路径,
  • $1-∞:传给脚本的各个参数,
  • $@,$*:这两个都表示传入的所有参数,
  • $#:表示传入参数的个数)

第一行指定了脚本的执行程序

第六行指定了一些程序的错误处理方式等(详见Shell相关文档)

第七行改变当前目录到nlp_markup.py 所在目录,也就是udf 目录

第九行设置了一个变量BAZZER_HOME 他的值是bazaar 的路径

第10-13行执行/parser/target/start 文件,如果有错会不正常退出,并提示

第15-17行检查输入参数的正确性,看参数个数是不是大于0个,如果没有参数,自己设定参数名

第23-24行把全部输入的参数用tsv2json 工具转换成json 格式,然后在执行parser/run.sh 并以刚才的json 作为参数输入。

现在我们应该知道,想对文本进行分词、词性标注等等自然语言处理,仅靠我们这点代码是不够的,所以我们有了bazaar 文件夹下的程序,对于这个文件我们不过多了解,但是现在我们要知道的是,如果我们想使用它,我们需要对它进行编译。来到bazaar/parser 目录下,然后在终端中执行下面代码

sbt/sbt stage

现在,这一步的准备工作完成了,可以回到我们的transaction 目录下去编译app.ddlog 并执行相应工作

deepdive compile
deepdive do sentences

这一步需要耐心等待,可能要很久很久很久。

请注意上面这一步,如果报错,就是找不到进程啊,没有文件啊类似的错误可以使用下面的办法处理

#在执行前面的两条命令之前,进入superuser的模式,使用su进行一次登录
#具体步骤:按照下面的代码来
su (username)  #这里的username是你的用户名,就是终端前面‘@’这个符号前面的部分
#它会提示你输入密码,登录即可。
#接下来在执行前面的
deepdive compile
deepdive do transaction 

1.3 抽取候选关系

到这里,前面都是通用的步骤不论我们抽取什么样的关系,什么类型的实体,我们肯定都要先对我们的文章进行处理,分词啊,标记啊啥的。但是到了这一步,我们就是要按照我们的任务去安排了。

本文档是按照前面下载的deepdive中的transaction样例来演示的,所以现在我们的任务是抽取不同公司之间的交易关系。如果你要抽取和我这里不同的东西,just对号入座、按图索骥就可以了。

既然我们要抽取公司间的交易信息,所以我们是不是应该先得到我文本中的公司是谁,才能进一步知道他们有没有关系,这一步就是要抽取这些公司啦。一共分两步

  • 抽取候选实体
  • 得到实体间的候选关系
1.3.1 抽取候选实体

和前面的sentence一样,我们需要建立一个数据表,来存储我们接下来会的到公司(不然放在哪),还有用来抽取实体的方法,并且要调用这个方法。所以我们要在app.ddlog中添加下面的内容,告诉Deepdive如何处理数据

company_mention(
	mention_id		text,
	mention_text	text,
	doc_id			text,
	sentence_index	int,
	begin_index		int,
	end_index		int
).

function map_company_mention over(
	doc_id			text,
	sentence_index	 int,
	tokens			text[],
	ner_tags		text[]
)returns rows like company_mention
implementation "udf/map_company_mention.py" handles tsv lines.

company_mention += map_company_mention(
	doc_id, sentence_index, tokens, ner_tags
) :-
sentences(doc_id, sentence_index, _, tokens, _, _,ner_tags, _, _, _).

现在我们应该想了,处理方式我选好了,那么这个udf/map_company_mention.py应该怎么写才行啊,我要如何从这一堆文本里得到公司的名字。好!前面1.2 已经讲过了,我们nlp处理中有一个步骤是命名实体识别(NER),这个东西会把每个词的实体识别出来,比如公司名字就应是属于ORG类的实体。所以我们只要在每个sentence中找到其中的ner_tags 为连续的ORG标记的就可以了。

好我们来看看map_company_mention中的代码

#!/usr/bin/env python
#encoding:utf-8
from deepdive import *
from transform import *
import re

@tsv_extractor
@returns(lambda
        mention_id       = "text",
        mention_text     = "text",
        doc_id           = "text",
        sentence_index   = "int",
        begin_index      = "int",
        end_index        = "int",
    :[])
def extract(
        doc_id         = "text",
        sentence_index = "int",
        tokens         = "text[]",
        ner_tags       = "text[]",
    ):
    """
    Finds phrases that are continuous words tagged with ORG.
    """
    num_tokens = len(ner_tags)
    # find all first indexes of series of tokens tagged as ORG
    first_indexes = (i for i in xrange(num_tokens) if ner_tags[i] == "ORG" and (i == 0 or ner_tags[i-1] != "ORG") and re.match(u'^[\u4e00-\u9fa5\u3040-\u309f\u30a0-\u30ffa-zA-Z]+$', unicode(tokens[i], "utf-8")) != None)
    for begin_index in first_indexes:
        # find the end of the ORG phrase (consecutive tokens tagged as ORG)
        end_index = begin_index + 1
        while end_index < num_tokens and ner_tags[end_index] == "ORG" and re.match(u'^[\u4e00-\u9fa5\u3040-\u309f\u30a0-\u30ffa-zA-Z]+$', unicode(tokens[end_index], "utf-8")) != None:
            end_index += 1
        end_index -= 1
        # generate a mention identifier
        mention_id = "%s_%d_%d_%d" % (doc_id, sentence_index, begin_index, end_index)
        mention_text = "".join(map(lambda i: tokens[i], xrange(begin_index, end_index + 1)))
        temp_text = link(mention_text, entity_dict)
        if temp_text == None or temp_text == '':
            continue
        if end_index - begin_index >= 25:
            continue
        # Output a tuple for each ORG phrase
        yield [
            mention_id,
            mention_text,
            doc_id,
            sentence_index,
            begin_index,
            end_index,
        ]

程序分析:

  • 不常用的知识
    • python中以函数定义前,@标识符表示的是python中装饰器的意思

    • lambda关键字,用来定义一个匿名函数

    • x for x in range(100) if x%2 == 0 类似这样的用法,叫做列表解析

    • re.match()是用来匹配正则表达式

    • yield:用来创建生成器,具体点这里

      内容太多了,自己学习对应的只是吧,呵呵。

  • 逻辑分析:
1.3.2 抽取候选关系

​ 候选实体已经有了,就是文中出现的公司名,我们要找的是公司之间的交易关系。所以这里候选关系简单来说,就是把不同的公司名两两组合,最终得到的关系表其实就相当于对两个候选实体表进行笛卡尔积(当然,我们还需要一些简单的过滤的处理,比如两个公司名不能相同啊等等)

笛卡尔乘积是指在数学中,两个集合XY的笛卡尓积(Cartesian product),又称直积,表示为X×Y,第一个对象是X的成员而第二个对象是Y的所有可能有序对的其中一个成员

假设有

​ A:a1,a2,a3 B:b1,b2

​ A×B:(a1,b1),(a1,b2),(a2,b1),(a2,b2),(a3,b1),(a3,b2)

​ 好了,现在定义一个表来存储我们的候选关系吧

transaction_candidate(
	p1_id		text,
	p1_name 	text,
	p2_id		text,
	p2_name 	text
)

​ 这个候选关系同样是要经过我们函数处理的,所以输入的参数和表的Schema一样,这样定义一个函数:

function map_transaction_candidate over (
	p1_id		text,
	p1_name		text,
	p2_id		text,
	p2_name		text
) returns rows like transaction_candidate
implementation "udf/map_transaction_candidate.py" handles tsv lines.

这里的函数要怎么调用呢,首先我们的输入数据应该是候选实体,又因为deepdive处理笛卡尔积这种关系还是很简单的,直接使用数据库表的操作就可以。

transaction_candidate += map_transaction_candidate(p1, p1_name, p2, p2_name) :-
company_mention(p1, p1_name, same_doc, same_sentence, p1_begin, _),
company_mention(p2, p2_name, same_doc, same_sentence, p2_begin, _),
p1_name != p2_name,
p1_begin != p2_begin.

上面的处理,要求相关联的两个候选实体,要是在同一句话里的,不再同一句话中怎么知道他们两个有关系。而且两个名字不能一样,识别的时候也有可能一个部分识别了两个,所以开始位置也不能一样。

那现在来看看map_transaction_candidate.py的内容。

#!/usr/bin/env python
#encoding:utf-8
from deepdive import *
import re

@tsv_extractor
@returns(lambda
        p1_id       = "text",
        p1_name     = "text",
        p2_id           = "text",
        p2_name  = "text",
    :[])
def extract(
        p1_id       = "text",
        p1_name     = "text",
        p2_id           = "text",
        p2_name   = "text",
    ):
    if not(set(p1_name) <= set(p2_name) or set(p2_name) <= set(p1_name)):
        yield [
            p1_id,
            p1_name,
            p2_id,
            p2_name,
        ]

这里多了的内容是set对象,他是集合的意思

这里把字符串给进去,得到的集合是字符串中出现的字母、汉字、符号的组合(还被去重了,就是每个东西只有一个)

比如set(‘hello’) 转换为字符串就是{‘h’,‘e’,‘l’,‘o’} 只有一个’l’哦

而他们之间的a <= b操作符用来判断,是不是a中的所有元素都在b中出现。

这里就是过滤掉,可能是同一个公司名字,但是有简略写法的可能。

这一切都尘埃落定,编译并执行这个操作吧

deepdive compile && deepdive do transaction_candidate

1.4 特征提取

​ 对于前面提取出来的公司间的候选关系,我们肯定是要使用机器学习的算法,通过一些我们找的训练集,让计算机去分类,哪个关系可能有交易关系(哇塞,还有机器学习 ?。。不然呢,人工分类吗,那直接让人看文章不是更方便 ?),所以特征首先要得到。

​ 对于自然语言来说,他的特征就是上下文,就是上下文,一般重要的事说两边。

​ 所以定义一个特征表:

transaction_feature(
	p1_id		text,
	p2_id		text,
	feature		text
).

​ 这个特征明显也是抽取出来的,所以要定义一个函数来抽取它。

function extract_transaction_features over (
	p1_id			text,
	p2_id			text,
	p1_begin_index	int,
	p1_end_index	int,
	p2_begin_index 	int,
	p2_end_index	int,
	doc_id			text,
	sent_index		int,
	tokens			text[],
	lemmas			text[],
	pos_tags		text[],
	ner_tags		text[],
	dep_types		text[],
	dep_tokens		int[]
) returns rows like transaction_feature
implementation "udf/extract_transaction_features.py" handles tsv lines.

然后定义如何调用这个函数,输入对应的参数:

transaction_feature += extract_transaction_features(
p1_id, p2_id, p1_begin_index, p1_end_index, p2_begin_index, p2_end_index,
doc_id, sent_index, tokens, lemmas, pos_tags, ner_tags, dep_types, dep_tokens
) :-
company_mention(p1_id, _, doc_id, sent_index, p1_begin_index, p1_end_index),
company_mention(p2_id, _, doc_id, sent_index, p2_begin_index, p2_end_index),
sentences(doc_id, sent_index, _, tokens, lemmas, pos_tags, ner_tags, _, dep_types, dep_tokens).

老规矩,现在看看这个py脚本里的内容:

#!/usr/bin/env python
from deepdive import *
import ddlib

@tsv_extractor
@returns(lambda
        p1_id   = "text",
        p2_id   = "text",
        feature = "text",
    :[])
def extract(
        p1_id          = "text",
        p2_id          = "text",
        p1_begin_index = "int",
        p1_end_index   = "int",
        p2_begin_index = "int",
        p2_end_index   = "int",
        doc_id         = "text",
        sent_index     = "int",
        tokens         = "text[]",
        lemmas         = "text[]",
        pos_tags       = "text[]",
        ner_tags       = "text[]",
        dep_types      = "text[]",
        dep_parents    = "int[]",
    ):
    """
    Uses DDLIB to generate features for the spouse relation.
    """
    # Create a DDLIB sentence object, which is just a list of DDLIB Word objects
    sent = []
    for i,t in enumerate(tokens):
        sent.append(ddlib.Word(
            begin_char_offset=None,
            end_char_offset=None,
            word=t,
            lemma=lemmas[i],
            pos=pos_tags[i],
            ner=ner_tags[i],
            dep_par=dep_parents[i] - 1,  # Note that as stored from CoreNLP 0 is ROOT, but for DDLIB -1 is ROOT
            dep_label=dep_types[i]))

    # Create DDLIB Spans for the two person mentions
    p1_span = ddlib.Span(begin_word_id=p1_begin_index, length=(p1_end_index-p1_begin_index+1))
    p2_span = ddlib.Span(begin_word_id=p2_begin_index, length=(p2_end_index-p2_begin_index+1))

    # Generate the generic features using DDLIB
    for feature in ddlib.get_generic_features_relation(sent, p1_span, p2_span):
        yield [p1_id, p2_id, feature]

enumerate(iter)在迭代中的作用是返回集合iter的索引序号和其value的组合

这里调用了ddlib.Word()ddlib.Span()ddlib.get_generic_features_relation()东西,下面一个个看

ddlib.Word():其实这个是Python使用collections.namedtuple(需要先导入collections) 定义的带有名字的格式化的元组,类似于C语言中的struct(结构体),其中一个有八个item,就和代码里写的一样。

ddlib.Span():这个和ddlib.Word()类型相似,都是类似结构体的东西,它内部存储的item也在代码里写了。

注意WordSpan这两个都不是方法,只是定义了数据的存储结构而已。

ddlib.get_generic_features_relation():这个就是普通函数了,它返回(用yield的方法)给定的句子中关于p1_spanp2_span的标准的特征(就是最普通的),注意这个函数参数的类型:sent是Word类型的,p?_span要求是Span类型的。其实这个函数还有最后一个参数,可以设置提取的特征的最大长度,默认是5,所以结果最长是5个特征的。

以上三个被调用的东西,都是Deepdive定义的比较简单的东西,供用户直接调用。

具体参见~/local/lib/python/ddlib/中的源代码文件。

现在编译并执行

deepdive compile && deepdive do transaction_feature

可以执行下面的语句来查看结果

deepdive query '| 20 ?- transaction_feature(_, _, feature).'

它显示的内容就是一个特征的内容。

比如下面:

feature                        
————————————————————————————
 WORD_SEQ_[郴州市 城市 建设 投资 发展 集团 有限 公司]
 LEMMA_SEQ_[郴州市 城市 建设 投资 发展 集团 有限 公司]
 NER_SEQ_[ORG ORG ORG ORG ORG ORG ORG ORG]
 POS_SEQ_[NR NN NN NN NN NN JJ NN]
 W_LEMMA_L_1_R_1_[]_[提供]
 W_NER_L_1_R_1_[O]_[O]
 W_LEMMA_L_1_R_2_[]_[提供 担保]
 W_NER_L_1_R_2_[O]_[O O]
 W_LEMMA_L_1_R_3_[]_[提供 担保 公告]
 W_NER_L_1_R_3_[O]_[O O O]
 W_LEMMA_L_2_R_1_[公司 为]_[提供]
 W_NER_L_2_R_1_[ORG O]_[O]
 W_LEMMA_L_2_R_2_[公司 为]_[提供 担保]
 W_NER_L_2_R_2_[ORG O]_[O O]
##这个是我的注释,原先没有的哦
##所以你看,下面最长的就是左2右3,或者左3右2的格式,最长是五个。
 W_LEMMA_L_2_R_3_[公司 为]_[提供 担保 公告]
 W_NER_L_2_R_3_[ORG O]_[O O O]
 W_LEMMA_L_3_R_1_[有限 公司 为]_[提供]
 W_NER_L_3_R_1_[ORG ORG O]_[O]
 W_LEMMA_L_3_R_2_[有限 公司 为]_[提供 担保]
 W_NER_L_3_R_2_[ORG ORG O]_[O O]

(20 rows)

1.5 样本打标

​ 对于监督学习,必然需要标注数据,那么已标注数据是怎么来的呢?当然正经的来说,应该是我们给这个系统提供大量的我们之前已经标注好了的数据,但是现在我们没有。所以我们可以对前面几步我们抽取出来的关系,利用一些先验的数据(比如人工标记的关系,还有先验的规则)对那些关系进行标记(标注出某些标记是已知的存在交易关系的,还有已知不存在交易关系的候选关系

​ 所以对于这里来说,我们同样需要数据库中有一个表,来存储我们的被标记数据。在app.ddlog中添加

transaction_label(
    @key
    @references(relation="has_transaction", column="p1_id", alias="has_transaction")
    p1_id   text,
    @key
    @references(relation="has_transaction", column="p2_id", alias="has_transaction")
    p2_id   text,
    @navigable
    label   int,
    @navigable
    rule_id text
).

​ 其中的label用来表示当前关系的相关性,数字越大就越相关,正数为正相关,负数为负相关。前面解释了,这个被标记数据,是我们从已经抽取出来的候选关系中进行打标得到的,对吧,在我们打标操作之前,其实我们要把我们的候选关系放到这里,然后对它们打标(因为提前也不知道谁会被标记正负)。所以这里通过数据库的操作,把我们的候选关系的公司id直接拷贝过来(在app.ddlog中添加)

transcation_label(p1,p2,0,NULL) :- transaction_candidate(p1,_,p2,_).

​ 好的,那么现在应该导入我们的先验数据了,这个其实就是我们知道的有交易关系的实体对,也就是两个公司的名字,同样用一个表来存储(在app.ddlog中添加),数据文件放到input/下面

transaction_dbdata(
    @key
    company1_name text,
    @key
    company2_name text
).

​ 我们应该先导入先验数据才能够进行打标,对吧,现在表已经定义好了,我们还要进行真正的导入操作。所以终端执行下面命令:

deepdive compile && deepdive do transaction_dbdata

​ 下面来定义我们的打标规则,如果候选的待打标的关系中的两个公司名,和我已知的两个公司名字相同,他们就应该是有关系的,应该被标上:那是相当有关系啊! 。所以下面进行打标的操作(在app.ddlog中添加):

transaction_label(p1,p2, 3, "from_dbdata") :-
    transaction_candidate(p1, p1_name, p2, p2_name), transaction_dbdata(n1, n2),
    [ lower(n1) = lower(p1_name), lower(n2) = lower(p2_name) ;
      lower(n2) = lower(p1_name), lower(n1) = lower(p2_name) ].

​ 上面的数据库操作就是对于公司名字和先验数据中的公司名字相同的关系,被标记为相当有关系(给的label是3,这个数越大就说明关系就越大,或者说越有可能有关系),标记规则就写来自数据“from_dbdata"就是这样。

​ 后面还有一个操作,我还不知道他要干啥(在app.ddlog中添加)。

function supervise over (
    p1_id text, p1_begin int, p1_end int,
    p2_id text, p2_begin int, p2_end int,
    doc_id         text,
    sentence_index int,
    sentence_text  text,
    tokens         text[],
    lemmas         text[],
    pos_tags       text[],
    ner_tags       text[],
    dep_types      text[],
    dep_tokens     int[]
) returns (
    p1_id text, p2_id text, label int, rule_id text
)
implementation "udf/supervise_transaction.py" handles tsv lines.

​ 下面调用函数(在app.ddlog中添加):

transaction_label += supervise(
p1_id, p1_begin, p1_end,
p2_id, p2_begin, p2_end,
doc_id, sentence_index, sentence_text,
tokens, lemmas, pos_tags, ner_tags, dep_types, dep_token_indexes
) :-
transaction_candidate(p1_id, _, p2_id, _),
company_mention(p1_id, p1_text, doc_id, sentence_index, p1_begin, p1_end),
company_mention(p2_id, p2_text, _, _, p2_begin, p2_end),
sentences(
    doc_id, sentence_index, sentence_text,
    tokens, lemmas, pos_tags, ner_tags, _, dep_types, dep_token_indexes
).

老规矩,看看这个udf/supervise_transaction.py

#!/usr/bin/env python
#encoding:utf-8
from deepdive import *
import random
from collections import namedtuple

TransactionLabel = namedtuple('TransactionLabel', 'p1_id, p2_id, label, type')

@tsv_extractor
@returns(lambda
        p1_id   = "text",
        p2_id   = "text",
        label   = "int",
        rule_id = "text",
    :[])
# heuristic rules for finding positive/negative examples of transaction relationship mentions
def supervise(
        p1_id="text", p1_begin="int", p1_end="int",
        p2_id="text", p2_begin="int", p2_end="int",
        doc_id="text", sentence_index="int", sentence_text="text",
        tokens="text[]", lemmas="text[]", pos_tags="text[]", ner_tags="text[]",
        dep_types="text[]", dep_token_indexes="int[]",
    ):

    # Constants
    TRANSLATION = frozenset(["转让", "交易", "卖出", "购买","收购","购入","拥有", "持有", "卖给", "买入", "获得"])
    STOCK = frozenset(["股权", "股份", "股"])
    TRANSLATION_COM = frozenset(["持股","买股","卖股"])
    TRANSLATION_AFTER = frozenset(["融资", "投资"])
    OTHERS_AFTER = frozenset(["产品", "委托", "贷款", "保险"])
    OTHERS_COM = frozenset(["购买", "提供", "申请", "销售"])
    CONF = frozenset(["对", "向"])
    OTHERS = frozenset(["销售产品", "提供担保","提供服务"])


    COMMAS = frozenset([":", ":","1","2","3","4","5","6","7","8","9","0","、", ";", ";"])
    MAX_DIST = 40

    # Common data objects
    p1_end_idx = min(p1_end, p2_end)
    p2_start_idx = max(p1_begin, p2_begin)
    p2_end_idx = max(p1_end,p2_end)
    intermediate_lemmas = lemmas[p1_end_idx+1:p2_start_idx]
    intermediate_ner_tags = ner_tags[p1_end_idx+1:p2_start_idx]
    tail_lemmas = lemmas[p2_end_idx+1:]
    transaction = TransactionLabel(p1_id=p1_id, p2_id=p2_id, label=None, type=None)

    # Rule: Candidates that are too far apart
    if len(intermediate_lemmas) > MAX_DIST:
        yield transaction._replace(label=-1, type='neg:far_apart')

    # Rule: Candidates that have a third company in between
    if 'ORG' in intermediate_ner_tags:
        yield transaction._replace(label=-1, type='neg:third_company_between')

    # Rule: Sentences that contain wife/husband in between
    
    if len(TRANSLATION_COM.intersection(intermediate_lemmas)) > 0:
        yield transaction._replace(label=1, type='pos:A持股B')

    if len(TRANSLATION.intersection(intermediate_lemmas)) > 0 and len(STOCK.intersection(tail_lemmas)) > 0:
        yield transaction._replace(label=1, type='pos:A购买B股权')
    
    if len(TRANSLATION_AFTER.intersection(intermediate_lemmas)) > 0:
        yield transaction._replace(label=1, type='pos:A增资B')

    if len(OTHERS_COM.intersection(intermediate_lemmas)) > 0 and len(OTHERS_AFTER.intersection(tail_lemmas)) > 0:
        yield transaction._replace(label=-1, type='neg:A购买B的产品')

    if len(CONF.intersection(intermediate_lemmas)) > 0 and len(OTHERS.intersection(tail_lemmas)) > 0:
        yield transaction._replace(label=-1, type='neg:A向B提供担保')

    # Rule: Sentences that contain and ... married
    #         (<P1>)(and)?(<P2>)([ A-Za-z]+)(married)
    if ("与" in intermediate_lemmas) and ("签署股权转让协议" in tail_lemmas):
        yield transaction._replace(label=1, type='pos:A和B签署股权转让协议')
        
    if len(COMMAS.intersection(intermediate_lemmas)) > 0:
        yield transaction._replace(label=-1, type='neg:中间有特殊符号')


上面我对源文件删掉了一些无效的注释

但是就这样其实可以看出来,这个代码中的程序就是一堆if..else..的结构,进行普通的规则的判断,然后给出标记和所使用的规则。整体的处理都是对于上下文(或者说特征的判断)

没有什么特殊的地方了,每个领域都有领域自身的一些能体现在文本上的规则。

因为上面的打标过程,可能多个规则都对同一个关系打标了,但是他们打标的值不同,所以这里要把这种同一个关系的都整合起来,让他们的label相加,得到最终的标记数据(在app.ddlog中添加):

 transaction_label_resolved(p1_id, p2_id, SUM(vote)) :-
 transaction_label(p1_id, p2_id, vote, rule_id).

现在,打标的一系列操作我们都定义好了,接下来就执行打标吧,刺激不?在终端执行下面代码:

deepdive compile && deepdive do transaction_label_resolved

2 模型构建

未完待续…

评论 114
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值