家庭关系本体#

此示例是 AI for Beginners Curriculum 的一部分,灵感来源于这篇博客文章

我总是觉得记住家庭成员之间的各种关系很困难。在这个示例中,我们将使用一个定义家庭关系的本体,以及实际的家谱树,展示如何通过自动推理来找到所有的亲属关系。

获取家谱树#

作为示例,我们将使用罗曼诺夫沙皇家族的家谱。描述家庭关系最常见的格式是 GEDCOM。我们将使用 GEDCOM 格式的罗曼诺夫家族树:

In [1]:
!head -15 data/tsars.ged
0 HEAD
1 CHAR UTF8
1 GEDC
2 VERS 5.5
0 @0@ INDI
1 NAME Mihail Fedorovich /Romanov/
1 SEX M
1 BIRT
2 DATE 1613
1 DEAT 
2 DATE 1645
1 FAMS @41@
0 @1@ INDI
1 NAME Evdokija Lukjanovna /Streshneva/
1 SEX F

要使用GEDCOM文件,我们可以使用python-gedcom库:

In [2]:
import sys
!{sys.executable} -m pip install python-gedcom
Collecting python-gedcom
  Downloading python_gedcom-1.0.0-py2.py3-none-any.whl (35 kB)
Installing collected packages: python-gedcom
Successfully installed python-gedcom-1.0.0

这个库解决了一些文件解析的技术问题,但它仍然为我们提供了对树中所有个人和家庭的相当低级的访问。以下是我们如何解析文件并显示所有个人列表的方法:

In [3]:
from gedcom.parser import Parser
from gedcom.element.individual import IndividualElement
from gedcom.element.family import FamilyElement
g = Parser()
g.parse_file('data/tsars.ged')
In [4]:
d = g.get_element_dictionary()
[ (k,v.get_name()) for k,v in d.items() if isinstance(v,IndividualElement)]
[('@0@', ('Mihail Fedorovich', 'Romanov')),
 ('@1@', ('Evdokija Lukjanovna', 'Streshneva')),
 ('@2@', ('Aleksej Mihajlovich', 'Romanov')),
 ('@3@', ('Marija Ilinichna', 'Miloslavskaja')),
 ('@4@', ('Natalja Kirillovna', 'Naryshkina')),
 ('@5@', ('Marfa Matveevna', 'Apraksina')),
 ('@6@', ('Fedor Alekseevich', 'Romanov')),
 ('@7@', ('Sofja Aleksevna', 'Romanova')),
 ('@8@', ('Ivan V Alekseevich', 'Romanov')),
 ('@9@', ('Praskovja Fedorovna', 'Saltykova')),
 ('@10@', ('Ekaterina Ivanovna', 'Romanova')),
 ('@11@', ('Anna Ivanovna', 'Romanova')),
 ('@12@', ('Fridrih Vilgelm', 'Kurlandskij')),
 ('@13@', ('Karl Leopold', 'Meklenburg-Shverinskij')),
 ('@14@', ('Anna Leopoldovna', 'Meklenburg-Shverinskaja')),
 ('@15@', ('Anton Ulrih', 'Braunshvejg-Volfenbjuttelskij')),
 ('@16@', ('Ivan VI Antonovich', 'Braunshvejg-Volfenbjuttelskij')),
 ('@17@', ('Petr I Alekseevich', 'Romanov')),
 ('@18@', ('Evdokija Fedorovna', 'Lopuhina')),
 ('@19@', ('Ekaterina I Alekseevna', 'Mihajlova')),
 ('@20@', ('Aleksej Petrovich', 'Romanov')),
 ('@21@', ('Sharlotta Kristina', 'Braunshvejg-Volfenbjuttelskaja')),
 ('@22@', ('Petr II Alekseevich', 'Romanov')),
 ('@23@', ('Anna Petrovna', 'Romanova')),
 ('@24@', ('Elizaveta Petrovna', 'Romanova')),
 ('@25@', ('Karl Fridrih', 'Golshtejn-Gottorpskij')),
 ('@26@', ('Petr III Fedorovich', 'Romanov')),
 ('@27@', ('Ekaterina II', 'Alekseevna')),
 ('@28@', ('Pavel I Petrovich', 'Romanov')),
 ('@29@', ('Natalja Alekseevna', 'Gessen-Darmshtadskaja')),
 ('@30@', ('Marija Fedorovna', 'Vjurtembergskaja')),
 ('@31@', ('Aleksandr I Pavlovich', 'Romanov')),
 ('@32@', ('Elizaveta Alekseevna', 'Baden-Durlahskaja')),
 ('@33@', ('Nikolaj I Pavlovich', 'Romanov')),
 ('@34@', ('Aleksandra Fedorovna', 'Prusskaja')),
 ('@35@', ('Aleksandr II Nikolaevich', 'Romanov')),
 ('@36@', ('Marija Aleksandrovna', 'Gessenskaja')),
 ('@37@', ('Aleksandr III Aleksandrovich', 'Romanov')),
 ('@38@', ('Marija Fedorovna', 'Datskaja')),
 ('@39@', ('Nikolaj II Aleksandrovich', 'Romanov')),
 ('@40@', ('Aleksandra Fedorovna', 'Gessenskaja'))]

以下是我们获取家庭信息的方法。请注意,这会给我们一个标识符列表,如果我们想要更清楚,需要将它们转换为名称:

In [5]:
d = g.get_element_dictionary()
[ (k,[x.get_value() for x in v.get_child_elements()]) for k,v in d.items() if isinstance(v,FamilyElement)]
[('@41@', ['@0@', '@1@', '@2@']),
 ('@42@', ['@2@', '@3@', '@6@', '@7@', '@8@']),
 ('@43@', ['@8@', '@9@', '@10@', '@11@']),
 ('@44@', ['@13@', '@10@', '@14@']),
 ('@45@', ['@15@', '@14@', '@16@']),
 ('@46@', ['@2@', '@4@', '@17@']),
 ('@47@', ['@17@', '@18@', '@20@']),
 ('@48@', ['@20@', '@21@', '@22@']),
 ('@49@', ['@17@', '@19@', '@23@', '@24@']),
 ('@50@', ['@25@', '@23@', '@26@']),
 ('@51@', ['@26@', '@27@', '@28@']),
 ('@52@', ['@28@', '@30@', '@31@', '@33@']),
 ('@53@', ['@33@', '@34@', '@35@']),
 ('@54@', ['@35@', '@36@', '@37@']),
 ('@55@', ['@37@', '@38@', '@39@'])]

获取家庭本体#

接下来,让我们看看家庭本体,它被定义为一组语义网三元组。这个本体定义了诸如 isUncleOfisCousinOf 等许多关系。所有这些关系都是基于基本谓词 isMotherOfisFatherOfisBrotherOfisSisterOf 定义的。我们将使用自动推理,通过本体推导出所有其他关系。

以下是 isAuntOf 属性的一个示例定义,它被定义为 isSisterOfisParentOf 的组合(姑/姨是某人父母的姐妹)。

fhkb:isAuntOf a owl:ObjectProperty ;
    rdfs:domain fhkb:Woman ;
    rdfs:range fhkb:Person ;
    owl:propertyChainAxiom ( fhkb:isSisterOf fhkb:isParentOf ) .
In [6]:
!head -20 data/onto.ttl
@prefix fhkb: <http://www.example.com/genealogy.owl#> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix xml: <http://www.w3.org/XML/1998/namespace> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

<http://www.example.com/genealogy.owl#> a owl:Ontology .

fhkb:DomainEntity a owl:Class .

fhkb:Man a owl:Class ;
    owl:equivalentClass [ a owl:Class ;
            owl:intersectionOf ( fhkb:Person [ a owl:Restriction ;
                        owl:onProperty fhkb:hasSex ;
                        owl:someValuesFrom fhkb:Male ] ) ] .

fhkb:Woman a owl:Class ;
    owl:equivalentClass [ a owl:Class ;
            owl:intersectionOf ( fhkb:Person [ a owl:Restriction ;

构建推理本体#

为了简化操作,我们将创建一个本体文件,其中包括家庭本体中的原始规则,以及来自我们 GEDCOM 文件的个人事实。我们将遍历 GEDCOM 文件,提取有关家庭和个人的信息,并将其转换为三元组。

In [7]:
!cp data/onto.ttl .

gedcom_dict = g.get_element_dictionary()
individuals, marriages = {}, {}

def term2id(el):
    return "i" + el.get_pointer().replace('@', '').lower()

out = open("onto.ttl","a")

for k, v in gedcom_dict.items():
    if isinstance(v,IndividualElement):
        children, siblings = set(), set()
        idx = term2id(v)

        title = v.get_name()[0] + " " + v.get_name()[1]
        title = title.replace('"', '').replace('[', '').replace(']', '').replace('(', '').replace(')', '').strip()

        own_families = g.get_families(v, 'FAMS')
        for fam in own_families:
            children |= set(term2id(i) for i in g.get_family_members(fam, "CHIL"))

        parent_families = g.get_families(v, 'FAMC')
        if len(parent_families):
            for member in g.get_family_members(parent_families[0], "CHIL"): # NB adoptive families i.e len(parent_families)>1 are not considered (TODO?)
                if member.get_pointer() == v.get_pointer():
                    continue
                siblings.add(term2id(member))

        if idx in individuals:
            children |= individuals[idx].get('children', set())
            siblings |= individuals[idx].get('siblings', set())
        individuals[idx] = {'sex': v.get_gender().lower(), 'children': children, 'siblings': siblings, 'title': title}

    elif isinstance(v,FamilyElement):
        wife, husb, children = None, None, set()
        children = set(term2id(i) for i in g.get_family_members(v, "CHIL"))

        try:
            wife = g.get_family_members(v, "WIFE")[0]
            wife = term2id(wife)
            if wife in individuals: individuals[wife]['children'] |= children
            else: individuals[wife] = {'children': children}
        except IndexError: pass
        try:
            husb = g.get_family_members(v, "HUSB")[0]
            husb = term2id(husb)
            if husb in individuals: individuals[husb]['children'] |= children
            else: individuals[husb] = {'children': children}
        except IndexError: pass

        if wife and husb: marriages[wife + husb] = (term2id(v), wife, husb)

for idx, val in individuals.items():
    added_terms = ''
    if val['sex'] == 'f':
        parent_predicate, sibl_predicate = "isMotherOf", "isSisterOf"
    else:
        parent_predicate, sibl_predicate = "isFatherOf", "isBrotherOf"
    if len(val['children']):
        added_terms += " ;\n    fhkb:" + parent_predicate + " " + ", ".join(["fhkb:" + i for i in val['children']])
    if len(val['siblings']):
        added_terms += " ;\n    fhkb:" + sibl_predicate + " " + ", ".join(["fhkb:" + i for i in val['siblings']])
    out.write("fhkb:%s a owl:NamedIndividual, owl:Thing%s ;\n    rdfs:label \"%s\" .\n" % (idx, added_terms, val['title']))

for k, v in marriages.items():
    out.write("fhkb:%s a owl:NamedIndividual, owl:Thing ;\n    fhkb:hasFemalePartner fhkb:%s ;\n    fhkb:hasMalePartner fhkb:%s .\n" % v)

out.write("[] a owl:AllDifferent ;\n    owl:distinctMembers (")
for idx in individuals.keys():
    out.write("    fhkb:" + idx)
for k, v in marriages.items():
    out.write("    fhkb:" + v[0])
out.write("    ) .")
out.close()
In [8]:
!tail onto.ttl
    fhkb:hasFemalePartner fhkb:i34 ;
    fhkb:hasMalePartner fhkb:i33 .
fhkb:i54 a owl:NamedIndividual, owl:Thing ;
    fhkb:hasFemalePartner fhkb:i36 ;
    fhkb:hasMalePartner fhkb:i35 .
fhkb:i55 a owl:NamedIndividual, owl:Thing ;
    fhkb:hasFemalePartner fhkb:i38 ;
    fhkb:hasMalePartner fhkb:i37 .
[] a owl:AllDifferent ;
    owl:distinctMembers (    fhkb:i0    fhkb:i1    fhkb:i2    fhkb:i3    fhkb:i4    fhkb:i5    fhkb:i6    fhkb:i7    fhkb:i8    fhkb:i9    fhkb:i10    fhkb:i11    fhkb:i12    fhkb:i13    fhkb:i14    fhkb:i15    fhkb:i16    fhkb:i17    fhkb:i18    fhkb:i19    fhkb:i20    fhkb:i21    fhkb:i22    fhkb:i23    fhkb:i24    fhkb:i25    fhkb:i26    fhkb:i27    fhkb:i28    fhkb:i29    fhkb:i30    fhkb:i31    fhkb:i32    fhkb:i33    fhkb:i34    fhkb:i35    fhkb:i36    fhkb:i37    fhkb:i38    fhkb:i39    fhkb:i40    fhkb:i41    fhkb:i42    fhkb:i43    fhkb:i44    fhkb:i45    fhkb:i46    fhkb:i47    fhkb:i48    fhkb:i49    fhkb:i50    fhkb:i51    fhkb:i52    fhkb:i53    fhkb:i54    fhkb:i55    ) .

推理操作#

现在我们希望能够使用这个本体进行推理和查询。我们将使用 RDFLib,一个用于读取不同格式的 RDF 图、查询等操作的库。

对于逻辑推理,我们将使用 OWL-RL 库,它允许我们构建 RDF 图的闭包,即添加所有可以推导出的概念和关系。

In [10]:
!{sys.executable} -m pip install rdflib
!{sys.executable} -m pip install git+https://github.com/RDFLib/OWL-RL.git
Requirement already satisfied: rdflib in /home/rg/anaconda3/envs/ai4beg/lib/python3.11/site-packages (6.3.2)
Requirement already satisfied: isodate<0.7.0,>=0.6.0 in /home/rg/anaconda3/envs/ai4beg/lib/python3.11/site-packages (from rdflib) (0.6.1)
Requirement already satisfied: pyparsing<4,>=2.1.0 in /home/rg/anaconda3/envs/ai4beg/lib/python3.11/site-packages (from rdflib) (3.0.9)
Requirement already satisfied: six in /home/rg/anaconda3/envs/ai4beg/lib/python3.11/site-packages (from isodate<0.7.0,>=0.6.0->rdflib) (1.16.0)
Collecting git+https://github.com/RDFLib/OWL-RL.git
  Cloning https://github.com/RDFLib/OWL-RL.git to /tmp/pip-req-build-lbfzwi3m
  Running command git clone --filter=blob:none --quiet https://github.com/RDFLib/OWL-RL.git /tmp/pip-req-build-lbfzwi3m
  Resolved https://github.com/RDFLib/OWL-RL.git to commit a77e1791b88b54aace609bc6000aac14c7add4ff
  Preparing metadata (setup.py) ... [?25ldone
[?25hRequirement already satisfied: rdflib>=6.0.2 in /home/rg/anaconda3/envs/ai4beg/lib/python3.11/site-packages (from owlrl==6.0.2) (6.3.2)
Requirement already satisfied: isodate<0.7.0,>=0.6.0 in /home/rg/anaconda3/envs/ai4beg/lib/python3.11/site-packages (from rdflib>=6.0.2->owlrl==6.0.2) (0.6.1)
Requirement already satisfied: pyparsing<4,>=2.1.0 in /home/rg/anaconda3/envs/ai4beg/lib/python3.11/site-packages (from rdflib>=6.0.2->owlrl==6.0.2) (3.0.9)
Requirement already satisfied: six in /home/rg/anaconda3/envs/ai4beg/lib/python3.11/site-packages (from isodate<0.7.0,>=0.6.0->rdflib>=6.0.2->owlrl==6.0.2) (1.16.0)

让我们打开本体文件,看看它包含多少个三元组:

In [11]:
import rdflib
from owlrl import DeductiveClosure, OWLRL_Extension

g = rdflib.Graph()
g.parse("onto.ttl", format="turtle")

print("Triplets found:%d" % len(g))
Triplets found:669

现在让我们构建闭包,看看三元组的数量如何增加:

In [12]:
DeductiveClosure(OWLRL_Extension).expand(g)
print("Triplets after inference:%d" % len(g))
Triplets after inference:4246

查询亲属关系#

现在我们可以查询图谱,查看人与人之间的不同关系。我们可以结合使用 SPARQL 语言和 query 方法。在我们的例子中,让我们看看家谱中所有的叔叔

In [13]:
qres = g.query(
    """SELECT DISTINCT ?aname ?bname
       WHERE {
          ?a fhkb:isUncleOf ?b .
          ?a rdfs:label ?aname .
          ?b rdfs:label ?bname .
       }""")

for row in qres:
    print("%s is uncle of %s" % row)
Fedor Alekseevich Romanov is uncle of Ekaterina Ivanovna Romanova
Aleksandr I Pavlovich Romanov is uncle of Aleksandr II Nikolaevich Romanov
Fedor Alekseevich Romanov is uncle of Anna Ivanovna Romanova

可以尝试不同的家庭关系。例如,可以查看 isAncestorOf 关系,它递归地定义了某个人的所有祖先。

最后,让我们整理一下!

In [14]:
!rm onto.ttl
In [ ]:

免责声明
本文档使用AI翻译服务Co-op Translator进行翻译。虽然我们尽力确保翻译的准确性,但请注意,自动翻译可能包含错误或不准确之处。原始语言的文档应被视为权威来源。对于关键信息,建议使用专业人工翻译。我们不对因使用此翻译而产生的任何误解或误读承担责任。