用Python编写用户定义的函数
除了DDlog中的正常派生规则之外,DeepDive还支持用于数据处理的用户定义函数(UDF)。UDF可以是任何从标准输入采用TAB分隔的JSON(TSJ)格式或TAB分隔值(TSV或PostgreSQL的文本格式)的程序,并将相同的格式输出到标准输出。TSJ在每行中以固定顺序放置固定数量的JSON值,并用TAB分隔。TSJ可以被认为是一种更为高效的编码方式,而不是简单地在每行中放置一个JSON对象,每行必须重复字段名称,特别是每行的固定数据模式已知并提前修复时。
以下各节介绍了DeepDive在Python中编写UDF的建议方式,以及它们如何在DDlog程序中使用。
在DDlog中使用自定义函数(UDF)
要在DDlog中使用用户定义的函数,必须首先声明它们,然后使用特殊语法调用它们。
首先,我们来为我们的运行示例定义两个关系的模式。
article(
id int,
url text,
title text,
author text,
words text[]
).
classification(
article_id int,
topic text
).
在这个例子中,我们假设我们想写一个简单的UDF,article
通过向关系中添加元组,使用UDF分类成不同的主题classification
。以下两节详细说明如何声明这样的函数以及如何在DDlog中调用它。
函数声明
函数声明包括输入/输出模式以及指向其实现的指针。
function <function_name> over (<input_var_name> <input_var_type>,...)
returns [(<output_var_name> <output_var_type>,...) | rows like <relation_name>]
implementation "<executable_path>" handles tsj lines.
在我们的例子,假设我们将只使用author
和words
每一个article
来确定其标识的主题id
,并实施将保持在相对路径的可执行文件udf/classify.py
。下面显示了这个函数的确切声明。
function classify_articles over (id int, author text, words text[])
returns (article_id int, topic text)
implementation "udf/classify.py" handles tsj lines.
请注意,关系的列定义classification
在该returns
子句中重复。这可以通过使用rows like
下面的语法来省略。
function classify_articles over (id int, author text, words text[])
return rows like classification
implementation "udf/classify.py" handles tsj lines.
另请注意,函数输入与articles
关系相似,但是有些列缺失。这是因为函数不会像前面提到的那样使用其余的列,所以放弃不必要的效率值是个好主意。下一节将展示如何导出这些输入元组并将其输入到一个函数中。
函数调用规则
可以调用上面声明的函数来为输出类型的另一个关系派生元组。函数调用的输入元组使用类似于正常派生规则的语法来导出。例如,下面显示的规则调用classify_articles
函数来classification
使用关系中的一个子集来填充articles
关系。
classification += classify_articles(id, author, words) :-
article(id, _, _, author, words).
函数调用规则可以被视为具有不同头部语法的常规推导规则的特殊情况,其中+=
头部关系名称附加了函数名称。
例如:
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).
备注:把sentences表和mention表做join,得到的结果输入函数,输出到transaction_feature表中
用Python编写UDF
DeepDive提供了一种用Python编写用户定义函数的模板化方法。它提供了几个Python函数装饰器,以分别简化分析和格式化输入和输出。在每一个输入行被调用的Python生成器都应该被装饰@tsj_extractor
,即在该def
行@tsj_extractor
被放置之前。(Python生成器是一个Python函数,它用来yield
代替return
每个调用产生多个结果的迭代)生成器期望的输入和输出列类型可以分别使用参数默认值和@returns
装饰器声明。他们告诉输入解析器和输出格式化器应该如何工作。
让我们看一个现实的例子来描述如何在代码中使用它们。下面是一个接近完成的代码,udf/classify.py
声明为DDlog函数的实现classify_articles
。
#!/usr/bin/env python
from deepdive import * # Required for @tsj_extractor and @returns
compsci_authors = [...]
bio_authors = [...]
bio_words = [...]
@tsj_extractor # Declares the generator below as the main function to call
@returns(lambda # Declares the types of output columns as declared in DDlog
article_id = "int",
topic = "text",
:[])
def classify( # The input types can be declared directly on each parameter as its default value
article_id = "int",
author = "text",
words = "text[]",
):
"""
Classify articles by assigning topics.
"""
num_topics = 0
if author in compsci_authors:
num_topics += 1
yield [article_id, "cs"]
if author in bio_authors:
num_topics += 1
yield [article_id, "bio"]
elif any (word for word in bio_words if word in words):
num_topics += 1
yield [article_id, "bio"]
if num_topics == 0:
yield [article_id, None]
这个简单的UDF根据作者在已知类别中的成员资格为文章分配一个主题。如果作者无法识别,我们会尝试查找出现在预定义集合中的单词。最后,如果什么都不匹配,我们只是把它放到另一个全面的话题。请注意,这里的主题本身是完全由用户定义的。
请注意,要使用这些Python装饰器,你需要有from deepdive import *
。另请注意,输入列的类型可以用与生成器参数相同的方式声明为默认值@returns
。
@tsj_extractor装饰器
对于@tsj_extractor
,将采取一个输入行的时间和所述第一装饰yield
零个或多个输出行作为值的列表。这基本上让DeepDive知道在运行Python程序时要调用哪个函数。(对于TSV,有@tsv_extractor
装饰,但强烈推荐TSJ。)
注意事项
一般来说,这个生成器应该放在程序的底部,除非在处理完所有的输入行之后有一些清理或拆卸任务。修饰的生成器使用的任何函数或变量都应该出现在修饰器之前,因为@tsj_extractor
修饰器会立即开始解析输入并调用生成器。发电机不应该print
或sys.stdout.write
任何会损坏标准输出的东西。相反,print >>sys.stderr
或者sys.stderr.write
可以用来记录有用的信息。更多的信息可以在debugging-udf部分找到
参数的默认值和@returns装饰
为了将输入的TSJ行正确地解析为Python值,并将@tsj_extractor
在TSJ中正确生成的值格式化,需要在Python程序中写入列类型。它们应该与DDlog中的函数声明一致。输入列的类型可以直接在@tsj_extractor
生成器的签名中声明为参数默认值,如上例所示。@returns
装饰器的参数可以是名称和类型对的列表,也可以是所有参数的默认值都设置为其类型的函数。使用lambda
是优选的,因为对的列表需要更多符号来使得声明混乱。例如,比较以上@returns([("article_id", "int"), ("topic", "text")])
。原因dict(column="type", ... )
还是{ "column": "type", ... }
不工作是因为Python用这些语法忘记了列的顺序,这对TSJ解析器和格式化程序是至关重要的。传递的函数永远不会被调用,所以主体可以被保留为任何值,比如空的list([]
)。DeepDive还@over
为输入列对称提供装饰器@returns
,但不建议使用这种装饰器,因为它会产生冗余声明。
运行和调试UDF
一旦写入UDF的第一个剪辑,就可以使用deepdive do
和deepdive redo
命令运行。例如,classify_articles
我们的运行示例中的派生classification
关系的函数可以使用以下命令运行:
deepdive redo classification
这将调用Python程序udf/classify.py
,输入包含article
表的三列的TSJ行,并将其输出添加到classification
数据库中的表中。