Python-本体论教程-全-
Python 本体论教程(全)
一、简介
在过去的十年中,形式本体已经广泛应用于计算机科学中来构建数据和知识。与此同时,Python 编程语言在教学、商业和研究中越来越广泛。然而,直到最近,只有很少的工具和资源致力于使用 Python 中的本体。事实上,大多数关于本体的书籍或教程都是相当理论化的,并没有涉及编程,或者它们局限于像 Java 这样更复杂的语言。
这个问题在生物医学领域尤其重要,在这个领域,本体和 Python 被广泛使用。在我作为索邦巴黎大学医学信息学教师和研究人员的日常生活中,我经常看到学生和工程师构建后来没有被使用的本体。这些文件保存在一个 u 盘上,因为不容易将本体与现有的软件整合。
这本书的存在就是为了填补这个空白。它展示了如何使用 Python 轻松访问本体并将其发布为动态网站,以构建新的本体,执行自动推理,将实体链接到医学术语,或在 DBpedia 中进行一些研究…使用 Owlready,这是我从 2013 年开始为“面向本体的编程”开发的 Python 模块。并且,在这本书里,我们不会害怕实现基于本体的程序:你会看到比数学公式更多的源代码!
1.1 这本书是给谁的?
这本书是为任何想用 Python 操作和构建本体,或者想从实用的角度发现本体世界的人准备的,尤其是为计算机科学家和语义网应用开发人员、生物信息学家、人工智能领域的科学家、这些学科的学生…或者只是为好奇的人准备的!
要读这本书,建议了解一下面向对象编程,用 Python 或者另一种面向对象语言(Java,C++等)。).另一方面,不需要了解 Python 语言或掌握正式的本体,章节 2 和 3 包含提醒。
1.2 为什么是本体论?
本体论的概念来源于柏拉图的哲学和著作。在计算机科学中,本体是“一个领域中所有实体以及这些实体之间存在的关系的正式描述”。这个定义可能看起来很复杂!事实上,它是以一种机器可以利用的方式来描述知识,并关注完整性和“普遍性”。本体是所谓的“符号”人工智能的一部分,它由结构化知识组成,使其可以被计算机访问,这与机器学习(如神经网络、深度学习等)相反。).
下图展示了一个非常简单的生态学领域的本体论的例子,用图解法表示(注意:“狗鱼”和“蟑螂”是两种鱼类):
这里,我们有八个实体,用矩形表示,以及这些实体之间的关系。存在几类关系:
-
层级“是-a”关系:它们将一个实体链接到一个更一般的实体。例如,人是动物,狗鱼是动物,多氯联苯是污染物,等等。在编程中,术语“继承”也用于命名这些关系。
-
地理关系(“生活”、“存在于”):它们指示实体的位置,将实体链接到一个地方。例如,狗鱼生活在湖里。
-
各种横向关系(“吃”,“集中”):例如,人类吃梭鱼。
通过查阅这个图表,你会很容易地推断出一个人很可能是被多氯联苯所陶醉。本体的优势是使这种推理不仅可以被人类使用,也可以被机器使用:在一种名为 reasoner 的软件的帮助下,计算机将能够重现这种推理,并推断出人类有被 PCB 毒害的风险。
为此,本体依赖于描述逻辑(参见附录 A)。OWL 语言(万维网联盟 W3C 标准化的网络本体语言)是最常用于形式化本体的语言之一。OWL 支持大量不同的描述逻辑。OWL 语言可以翻译成 RDF(资源描述框架),RDF 本身通常用 XML(可扩展标记语言)表示。
本体有两个主要目的:
-
自动推理:由于概念、关系及其属性的集合是以一种正式的方式描述的,因此自动执行逻辑推理成为可能。
-
知识的重用:所有的本体共享相同的名称空间,并且可以链接在一起,导致了语义网。
此外,还有许多为本体设计的工具,如 Protégé editor 或 HermiT and Pellet reasoners。使用本体论允许你使用所有这些工具,尽管对于一个给定的项目,你可能不需要本体论的全部潜力。
1.3 为什么选择 Python?
最常用于处理本体的编程语言是 Java。然而,Java 是一种复杂的语言,而且在某些领域很少使用,例如生物医学领域。
相反,今天兴起的语言是 Python,尤其是在生物医学领域(的确,这本书的几个例子将来自生物学或医学)。与其他编程语言相比,Python 的主要优势在于它优化了程序员的时间:与大多数其他语言相比,Python 允许程序员更快地开发他/她的程序。15 年前,当我意识到用 Python 只需要一天就可以完成用 Java 需要三天的任务时,这就是我选择 Python 的原因!
如今,Python 经常被用作链接其他组件的粘合剂,如数据库、网站、文本文件…或本体,正如我们将在本书中看到的。
1.4 为什么 Owlready?
Owlready 允许“面向本体的编程”,即对象和类是一个本体的实体的面向对象编程。面向本体的编程是一种比通常的 Java 应用编程接口(API)更简单和更强大的方法,如由 OWLAPI 和 JENA 提出的,其中本体的实体的行为不像编程语言的对象和类。
Owlready 提供了三个世界中最好的:
-
形式本体的表达能力,也就是说,详细描述复杂知识、将它们联系在一起以及对这些知识进行推理的能力
-
关系数据库的访问速度,具有快速存储和搜索能力
-
Python 等面向对象编程语言的灵活性,能够执行“命令性”的代码行,向计算机发出“命令”,这是本体或数据库本身无法做到的
Owlready 包含了一个带有 OWL 语义层的图形数据库。这个数据库被称为 quadstore ,因为它以 RDF 格式存储四元组,也就是说,RDF 三元组的形式(subject,property,object)添加了一个本体标识符(参见第十一章,了解 RDF 和 Owlready 的 quadstore 结构的更详细的解释)。
这个 quadstore 以一种紧凑的格式存储来自已加载的本体的所有信息。它可以以 SQLite3 数据库文件的形式放在 RAM 或磁盘上。然后,Owlready 在需要时将本体实体加载到 Python 中,并在不再需要时自动从 RAM 中删除它们。此外,如果在 Python 中修改了这些实体,Owlready 会自动更新 quadstore。
下图显示了 Owlready 的一般架构:
这种架构使得加载大量本体(几十或几百千兆字节)同时非常快速地访问特定实体成为可能,例如,通过文本搜索。它还允许对应于 OWL 本体的语义级别(不像许多图形数据库局限于 RDF 级别)。然而,Owlready 也可以用作简单的对象数据库、图形数据库或对象关系映射器(ORM ),而没有利用本体的表达能力所带来的好处。
Owlready 是作为自由软件发布的(GNU LGPL 许可证)。本书涵盖了 owl ready 2-0.25 版本(owlready2
模块)。关于其安装,您可以参考第 2.11 节。如果您在学术环境中使用 Owlready,请引用以下文章:
- 拉米 JB。owl ready:Python 中面向本体的编程,具有生物医学本体的自动分类和高级构造 。医学中的人工智能 2017;80:11-28
http://www.lesfleursdunormal.fr/_downloads/article_owlready_aim_2017.pdf
1.5 图书大纲
前两章包含提醒:第二章介绍 Python,第三章介绍 OWL 本体。如果你已经掌握了这些概念,你可以快速阅读这些章节。
然后第 4 、 5 和 6 章解释了如何用 Owlready 在 Python 中操作和创建本体。这些章节介绍了 Owlready 的基本特性。
以下章节描述了更具体的功能。第章 7 涉及自动推理,第章 8 涉及注释和文本搜索,第章 9 涉及医学术语的管理。
最后,最后两章描述了高级特性。第十章展示了如何将 Python 方法集成到 OWL 本体的类中,第十一章展示了如何直接访问 Owlready 的 RDF quadstore。
这本书的源代码可以在 GitHub 上通过这本书的产品页面获得,位于 www.apress.com/978-1-4842-6551-2
。
1.6 摘要
在这一介绍性章节中,我们介绍了形式本体论、Python 和 Owlready,并且我们画出了本书内容的大纲。
二、Python 语言:玩转一条蛇!
Python 是一种通用且易学的编程语言。它已经存在了近 30 年,但多年来一直保密,现在取得了巨大的成功——成为当今教授范围最广的编程语言之一。Python 的主要优势在于它的简单性和为用户节省时间:使用 Python,我可以在一天内完成用 Java 三天、用 c 语言一周才能完成的工作。Python 极大地提高了生产率。
Python 是一个开源软件,而且是免费的。它可以在几乎所有现有的操作系统上运行(Linux PC、Windows PC、Mac、Android 等)。).Python 历史上有两个版本:版本 2.x(不再支持,但仍被旧程序使用)和版本 3.x(目前支持和推荐)。Owlready 需要版本 3.x,所以我们将在本书中使用这个版本。然而,这两个版本之间的差异微乎其微。
在本章中,我们将快速介绍 Python 语言及其语法的基础知识。然而,如果你还没有编程技能,我们建议你首先查阅一本专门学习 Python 的书。相反,如果你已经知道 Python 语言,你可以直接进入 2.11 节安装 Owlready。
2.1 安装 Python
在 Linux 下,几乎所有的发行版都提供了 Python 的包(通常这些包已经安装好了)。您可以检查它们是否存在于您的发行版的包管理器中,并在必要时安装包 python3。此外,如果您的发行版将 python3-pip 和 python3-idle 包与主 python3 包区分开来,请安装它们。
在 Windows 上,需要安装 Python。您可以从以下地址下载它:
在 Mac OS 上,很可能已经安装了 Python 您可以通过在终端中运行命令“python3 -v”来验证它。否则,请从前面的网站安装。
2.2 启动 Python
要用 Python 编程,可以使用集成开发环境(IDE)或者使用文本编辑器和终端。如果您是 Python 的新手,第一个选项可能是最简单的;我们建议使用通常随 Python 3 一起安装的空闲环境。
Python 是一种解释型语言,因此它可以在两种不同的模式下使用:
- “shell”模式,在这种模式下,当程序员输入代码时,计算机会一行一行地解释它们。这种模式便于执行快速测试。由 IDLE 打开的默认“Shell”窗口对应于这种模式(参见下面的示例)。行首的“
>>>
”符号是 Python 的命令提示符:解释器提示你输入新的一行代码。
注意,在“shell”模式下,输入的代码行不保存,在关闭终端或空闲时会丢失!
- “程序”模式,用户编写一个多行程序,然后计算机执行整个程序。这种模式允许你执行复杂的程序。与闲置,你可以创建一个新的程序与文件➤新文件菜单。将出现一个新窗口,您将在其中编写程序(参见下面的示例)。然后文件将被保存(扩展名为。py)并可通过运行➤运行模块菜单(或按 F5 键)执行。
在 Linux 上,您可能更喜欢使用文本编辑器输入程序(例如 Emacs、Vi)和终端来执行它们:
- 要拥有“shell”模式,在终端中执行命令“
python3
”:
[Bash prompt]# python3
Python 3.7.1 (default, Oct 22 2018, 10:41:28)
[GCC 8.2.1 20180831] on linux
Type "help", "copyright", credits or "license" for more information.
>>>
要退出 Python,请按 Ctrl+D。
- 要运行一个程序,在终端中运行命令“
python3 file_name.py
”(很明显,用保存程序的文件名替换file_name.py
,如果需要,用路径替换)。
按照惯例,在本书中,我们将以“shell”模式的方式编写 Python 代码的简短示例:Python 代码的前面是命令提示符“>>>
”,而这些行显示的最终输出不带此前缀,例如:
>>> print("Hello again!")
Hello again!
要执行这个示例,永远不要输入“>>>
”提示符(无论是在“shell”模式下还是在“program”模式下)。只需输入提示后面的代码。当命令占用多行时,Python 会在“shell”模式中添加“...
”,如下例所示:
>>> print(
... "Still here ?")
Still here ?
这是一个“命令结束”提示。和以前一样,不应输入“...
”。
较长的代码示例将以程序的形式呈现,如下所示:
# File file_name.py
print("It's me again!")
print("See you soon.")
第一行只表示文件名;它不必输入到程序中。
最后,在代码行中,由于本书页面的宽度有限,↲字符将被用在一行的末尾来表示换行符。在这种情况下,您不必在编程时回到该行,例如:
>>> x = "This is a very long text here, isn't it?"↲
+ "Indeed, it is."
2.3 语法
评论
在 Python 中,哈希字符“#”后面的任何内容都是注释,Python 解释器不会考虑这些内容。注释是用来给将要阅读程序的程序员提供指导的,但是被机器忽略了。这里有一个例子:
>>> # This text is a comment, and thus it is ignored by Python!
屏幕上的书写
print()
函数用于在屏幕上书写(在 shell 中,或在“程序”模式下的标准输出上);我们以前已经遇到过。可以显示几个用逗号分隔的值:
>>> print("My age is", 40)
My age is 40
print()
功能在“shell”模式下可以省略,但在“program”模式下是必须的。
>>> print(2 + 2)
4
>>> 2 + 2
4
帮助
Python 有大量预定义的函数。在“shell”模式中,help()
函数用于获取某个函数的帮助,例如,print()
函数:
>>> help(print)
然后,在“shell”模式下,您可以通过按键盘上的“Q”键退出手册页。
变量
变量是与值相关联的名称。通常,该值只有在程序被执行时才是已知的(例如,当它是计算的结果时)。
变量名必须以字母或下划线“_”开头,可以包含字母、数字和下划线。Python 3 接受变量名中的重音字符,但禁止使用空格。
在 Python 中,变量不需要声明,也不需要类型化。因此,同一个变量可以包含任何类型的数据,并且其值的类型可以在程序中改变。运算符“=”用于定义(或重新定义)变量的值;可以读作“取值”(注意,这不是数学中“=”的通常含义)。
>>> age = 39
>>> print(age)
39
在计算中,变量的名称由它们的值代替:
>>> age + 1
40
“=”运算符也可用于重新定义变量的值。例如,要将变量 age 的值增加 1,我们将:
>>> age = age + 1
>>> age
40
缩进
缩进对应于代码行左边的空格。不像大多数其他编程语言,缩进只是一种约定,在 Python 中缩进是有意义的。因此,一个坏的缩进在 Python 中是一个语法错误!特别是,我们不应该在条件和循环的左边添加空格(我们将在后面看到)。以下示例显示了一个缩进错误:
>>> age
File "<stdin>", line 1
age
^
IndentationError: unexpected indent
此外,建议您在缩进 Python 程序时不要混用空格和制表符。
2.4 主要数据类型
Python 可以操作各种数据类型:整数(缩写为 int )、实数(通常称为 float )、Unicode 字符串(缩写为 str )、布尔值(true 或 false value,缩写为 bool )。变量的数据类型不必声明,在程序执行过程中可能会改变。以下是各种数据类型的示例:
>>> age = 31 # Integer
>>> weight = 64.5 # Floating-point number
>>> name = "Jean-Baptiste Lamy" # Character string
>>> teacher = True # Boolean
>>> student = False # Boolean
2.4.1 整数(int
)和浮点数(float
)
整数是没有小数部分的数字。Python 中对整数值没有限制。
在计算机科学中,实数通常由浮点数表示(它们被称为“float ”,因为逗号的位置据说是浮动的:小数部分之前可以有很多位,之后很少,反之亦然)。点用于表示小数部分,如下例所示:
>>> poids = 64.4
在 Python 中,floats 的精度实际上相当于许多其他编程语言(包括 C、C++和 Java)中的“double”数。
注意,10.0 是浮点数,而 10 是整数。
下表总结了数字的主要代数运算:
|代数运算
|
例子
|
| --- | --- |
| 添加 | >>> 2 + 2
four |
| 减法 | >>> 4 - 2
Two |
| 增加 | >>> 3 * 4
Twelve |
| 分开 | >>> 10 / 3``3.3333333333333
|
| 整数除法 | >>> 10 // 3``3
|
| 力量 | >>> 3 ** 2``9
|
布尔函数(bool
)
布尔值可以接受两个值,在 Python 中分别写成 True(真,整数值 1)和 False(假,整数值 0)。
2.4.3 字符串(str
)
字符串是文本或文本的一部分。字符数没有限制(零个、一个或多个)。字符串总是用引号括起来(单引号或双引号;最好使用双引号,因为单引号与撇号是同一个字符)。在 Python 3 中,所有字符串都是 Unicode 的,因此可以包含任何语言的任何字符。
>>> name = "Jean-Baptiste Lamy"
>>> empty_string = ""
要在字符串中插入特殊字符,请使用以反斜杠开头的转义码。以下是最常见的:
|特殊字符
|
转义码
|
| --- | --- |
| 换行符 | \n
|
| 标签 | \t
|
| 反斜线符号 | \\
|
| 简单引用 | \'
|
| 双引号 | \
|
特别是,在 Windows 上,文件名和路径中的反斜杠必须是双重的,例如,“C:\\directory\\file.py
”。
Python 还允许长字符串,它可以跨越多行并包含引号。一个长字符串以三个引号开始,也以三个引号结束,如下例所示:
>>> long_string = """This character string is long
... and may contain line breaks and
... quotation marks " without problems.
... Backslashs \\ must still be doubled, though."""
单引号也可以用于长字符串。
在 Python 中,一切都是对象,包括字符串。因此它们有方法,我们可以用指向符号“object.method (parameters,...)".下表总结了对字符串的主要操作和方法。
|字符串操作
|
例子
|
| --- | --- |
| 获取字符串的长度 | >>> s =``Goodbye``>>> len(s)``7
|
| 获取字符串中的一个字符(注意,第一个字符是零而不是一;负数从末尾算起) | >>> s[0]``G``# First character``>>> s[-1]``"e" # Last character
|
| 得到绳子的一部分 | >>> s[0:4]``"Good"
|
| 查找一个字符串是否包含在另一个中 | >>> s.find("bye")``4 # Found in position 4``# (return -1 if not found)
|
| 从字符串末尾开始搜索(R 代表右) | >>> s.rfind("o")``2 # Found in position 2``# (return -1 if not found)
|
| 根据分隔符拆分字符串 | >>> "alpha;beta;gamma".↲ split(";")``["alpha", "beta", "gamma"]
|
| 根据空格(空格、换行符和制表符)剪切字符串 | >>> "alpha beta gamma".↲split()``["alpha", "beta", "gamma"]
|
| 用另一个字符串替换一个字符串的一部分 | >>> "Come here!".↲replace("here", "there")``"Come there!"
|
| 把两根绳子连接起来。注意,如果你想要一个空格,你必须添加一个 | >>> "JB" + "LAMY"``"JBLAMY"
|
| 用值格式化字符串 | >>> last_name = "LAMY"``>>> first_name = "JB"``>>> "Hello %s!" %↲ first_name``"Hello JB!"``>>> "Hello %s %s!" %↲ (first_name, last_name)``"Hello JB LAMY!"``>>> rate = 90``>>> "Success rate: %s %%"↲% rate``"Success rate: 90 %"
|
2.4.4 清单(list
列表包含零个、一个或多个元素(它们类似于其他编程语言中的数组,但是它们的大小可以不同)。元素可以是不同的类型(整数、字符串等。).列表是用方括号创建的;元素放在方括号中,用逗号分隔,例如:
>>> my_list = [0, "Lamy", True]
>>> empty_list = [ ]
在一个由 n 个元素组成的列表中,这些元素从 0 到n1 进行编号。按照惯例,列表通常会收到一个复数变量名,例如,动物列表的“animals”。
Python 列表也是对象。下表总结了列表上可用的主要操作和方法。
|列表操作
|
例子
|
| --- | --- |
| 创建列表 | >>> animals = ["elephant",``... "giraffe",``... "rhinoceros",``... "gazelle"]
|
| 获取列表的长度 | >>> len(animals)``4
|
| 从列表中获取一个元素(注意,列表是从零而不是一开始编号的) | >>> animals[0]``"elephant" # First``>>> animals[-1]``"gazelle" # Last
|
| 获得列表的一部分 | >>> animals[0:2]``["elephant", "giraffe"]
|
| 在末尾添加一个元素 | >>> animals.append("lion")
|
| 向给定位置添加一个元素(0:第一个位置,依此类推。) | >>> animals.insert(0, "lynx")
|
| 连接两个列表 | >>> [1, 2] + [3, 4, 5]``[1, 2, 3, 4, 5]
|
| 移除给定元素 | >>> animals.↲remove("gazelle")
|
| 移除给定位置的元素 | >>> del animals[-2]
|
| 查找列表中是否存在某个元素 | >>> "lion" in animals``True
|
| 对列表排序(默认为升序/字母顺序) | >>> animals.sort()
|
| 从列表中获取最高的元素,或者最低的元素 | >>> max([2, 1, 4, 3])``4``>>> min([2, 1, 4, 3])``1
|
2.4.5 元组(" ?? ")
元组非常类似于列表,区别在于它们是不可修改的。元组写在圆括号中,而不是方括号中:
>>> triple = (1, 2, 3)
>>> pair = (1, 2)
>>> single_element_tuple = (1,) # Do not forget the comma here!
2.4.6 字典(dict
和defaultdict
)
字典(或关联数组、哈希表或哈希表)将键映射到值。例如,字典可以将一个单词与其定义相匹配(因此有了字典名)。使用大括号创建字典,在大括号中放置零个、一个或多个“键:值”对,用“,”分隔。例如(请记住,行首的“...
”是 Python 提示的一部分,不应由程序员输入):
>>> my_dict = {
... "fruit" : "a plant food with a sweet taste",
... "apple" : "a fleshy fruit with a red or green skin",
... "orange" : "a juicy fruit with an orange skin",
... }
在前面的例子中,关键字是“水果”、“苹果”和“橘子”,值是定义。每个键有且只有一个值。
字典的键必须是不可变的(即不可修改的)。因此,我们不能使用列表作为键(通常使用元组代替)。
Python 字典也是对象。下表总结了对字典的主要操作和方法。
|字典操作
|
例子
|
| --- | --- |
| 获取字典中键(或值)的数量 | >>> len(my_dict)``3
|
| 获取与键关联的值 | >>> my_dict["apple"]``"a fleshy fruit with a red or green skin"
|
| 添加或修改给定键的值 | >>> my_dict["clef"] = "value"
|
| 删除一个键(及其相关值) | >>> del my_dict["clef"]
|
| 搜索字典中是否有关键字 | >>> "apple" in my_dict``True
|
| 找回所有的钥匙 | >>> for key in my_dict: ...``or``>>> keys = list(my_dict.↲keys())
|
| 恢复所有值 | >>> for value in my_dict.↲values(): ...``or``>>> values = list(my_dict.↲values())
|
| 收集所有(键,值)对(作为元组) | >>> for key, value in↲my_dict.items(): ...``or``>>> pairs = list(my_dict.↲items())
|
Python 还提供了一个默认的字典,叫做defaultdict
,这通常很有用。它是在collections
模块中定义的(我们将在后面看到模块;在下面的例子中,第一行对应于模块的导入;参见 2.10.1)。当您从默认字典中获取一个值,而字典中没有该键时,它会自动添加一个默认值。当它被创建时,defaultdict
接受一个默认数据类型的参数(它可以是一个数据类型、一个函数或者一个类,我们将在后面看到)。
以下示例创建一个 int 类型的defaultdict
。默认值是整数 0。
>>> from collections import defaultdict
>>> d = defaultdict(int)
>>> d["new_key"]
0
>>> d["new_key"] = d["new_key"] + 1
>>> d["new_key"]
1
>>> d["new_key"] = d["new_key"] + 1
>>> d["new_key"]
2
下面是第二个创建列表类型的defaultdict
的例子。因此,默认值是一个空列表。当每个键可能被映射到几个值(即一个值列表)时,通常使用列表的defaultdict
。
>>> from collections import defaultdict
>>> d = defaultdict(list)
>>> d["new_key"]
[]
>>> d["new_key"].append("a")
>>> d["new_key"]
['a']
>>> d["new_key"].append("b")
>>> d["new_key"]
['a', 'b']
2.4.7 设置(set
)
从功能的角度来看,集合非常接近列表,从实现的角度来看,集合非常接近字典。与列表不同,元素是没有顺序的,并且不能重复。集合用大括号写,像字典一样,但是用元素代替“键:值”对:
>>> my_set = {1, 2, 1, 3}
>>> len(my_set)
3
请注意,副本(第二个 1)已被删除。
必须使用set()
函数创建空集,以避免与空字典(记为{}
)混淆:
>>> empty_set = set()
add()
方法允许你添加一个元素到集合中(它代替了列表的append()
方法)和remove()
方法来删除一个元素。
经典集合运算(并集、交集等。)可通过方法和操作符获得(交集用“&”,并集用“|”)。不可变集合(frozenset
)被用作字典中的键,而不是集合。它们对于集合就像元组对于列表一样。
2.4.8 文件(open
)
使用open()
功能打开文件:
>>> my_file = open("path/filename.ext", "r")
第二个参数是“模式”;它可以是下列值之一:
-
“r”表示读取文本文件(默认值)
-
“w”写文本文件
-
“rb”来读取二进制文件
-
“wb”写二进制文件
如果文件不存在,打开文件进行写入会自动创建该文件,否则会将其覆盖。Python 处理换行符的转换(Unix/Windows/Mac)和文本文件的编码(默认为 UTF 8)。
|文件操作
|
例子
|
| --- | --- |
| 以字符串形式读取文件的全部内容 | >>> content = my_file.read()
|
| 写入文件 | >>> my_file.write("content")
|
| 关闭文件(当文件对象被 Python 销毁时自动调用) | >>> my_file.close()
|
数据类型之间的转换
有时需要将一种数据类型转换成另一种数据类型。int()
、float()
、str()
、list()
、tuple()
、set()
和frozenset()
函数分别允许将值转换为整数、浮点数、字符串、列表、元组、集合或不可变集合。
转换为
|
句法
|
| --- | --- |
| 整数 | int(x)
|
| 浮动 | float(x)
|
| 布尔代数学体系的 | bool(x)
|
| 目录 | list(x)
|
| 元组 | tuple(x)
|
| 一组 | set(x)
|
| 不可变集合 | frozenset(x)
|
| 词典 | dict(x)``# x is of the form [(key1, value1), (key2, value2)...]
|
| 线 | str(x) # String for displaying to the user``repr(x) # String for displaying to the programmer
|
以下示例将整数 8 转换为字符串:
>>> str(8)
'8'
2.5 条件(if
)
这些条件只允许在某些情况下执行命令,这将在程序执行时确定。条件的一般 Python 语法如下:
if condition1:
instruction executed if condition1 is true
instruction executed if condition1 is true...
elif condition2:
instruction executed if condition1 is false
and condition2 is true...
else:
instruction executed if condition1
and condition2 are false...
continuation of the program
(executed whether the conditions are true or false)
elif
是else
和if
的缩写。“elif”和“else”部分是可选的,并且可能存在几个“elif”部分。缩进(即行首的空格)很重要,因为它指示条件的结束位置。空格的数量是程序员的选择,但必须保持不变,建议避免混合空格字符和制表符。
条件可以使用标准的比较运算符:
-
<
(小于) -
>
(大于) -
<=
(小于或等于) -
>=
(大于或等于) -
==
(等于,不要与用于定义变量的简单“=”混淆) -
!=
(不同于) -
is
(测试两个对象之间的同一性)
逻辑运算符“and”、“or”和“not”可用于将多个条件组合在一起,如下例所示:
>>> if (age > 18) and (age < 65):
... print("You are an adult.")
当只有一条指令要执行时,可以将所有内容放在一行中:
>>> if age >= 65: print("You are an elderly person.")
条件可以嵌套,使用多级缩进:
>>> if age == 0:
... print("You are a newborn.")
... if weight > 10.0:
... print("I think there is an error in the weight!")
2.6 循环(for
)
循环使得多次执行相同的命令成为可能。在 Python 中,循环遍历列表并为列表中的每个元素执行一系列指令(这是一种在其他编程语言中通常称为“for each”的循环)。当前元素放在您选择的变量中。for
循环的一般语法如下:
for variable in my_list:
if conditions1: continue # Move to the next item in the list
if conditions2: break # Stop the loop
repeated instructions...
else:
instructions executed only if the loop went all the way
(i.e. no break was encountered)
continuation of the program (executed once)
continue
指令中断当前迭代,并立即移动到下一个元素。break
指令中断循环并立即从循环中退出。最后,else
部分仅在循环结束时执行(即,未被break
中断)。当然,在给定的循环中,continue
、break
和else
的存在不是强制性的。
迭代列表可以是包含列表的变量,也可以是字符串(循环遍历字符串的每个字符)、集合(循环遍历集合的元素,以任意顺序)、字典(循环遍历字典的键)等等。也可以是用range()
函数生成的索引列表:
>>> range(4)
注意,Python 的range()
函数与一个 OWL 属性的“范围”无关,这个我们后面会看到!
这是一个循环的例子。它考虑动物名称列表,每行显示一种动物:
>>> animals = ["elephant", "zebra", "rhinoceros", "dolphin"]
>>> for animal in animals:
... print(animal)
elephant
zebra
rhinoceros
dolphin
如果你想同时显示列表中每种动物的数量,我们可以使用range()
:
>>> for i in range(len(animals)):
... print(i, animals[i])
0 elephant
1 zebra
2 rhinoceros
3 dolphin
循环也可以集成到列表的定义中:它们是理解列表。这里有一个例子:
>>> integers = [1, 2, 3]
>>> even_integers = [2 * i for i in integers]
>>> even_integers
[2, 4, 6]
该理解列表与通过以下循环创建的列表相同:
>>> even_integers2 = []
>>> for i in integers:
... even_integers2.append(2 * i)
>>> even_integers2
[2, 4, 6]
类似地,Python 提出了理解集和字典,例如:
>>> twofold = { i: 2 * i for i in integers }
>>> twofold
{1: 2, 2: 4, 3: 6}
>>> twofold[2]
4
当希望在多个列表上循环时,可能会出现两种情况:
-
这些列表没有配对。在这种情况下,我们将使用嵌套循环,如下例所示:
-
列表是成对的,两个两个(或三个三个,等等。也就是说,列表 1 的第一元素与列表 2 的第一元素相关联,列表 1 的第二元素与列表 2 中的第二元素相关联,等等。).
zip()
函数允许你在两个(或更多)配对列表上循环。在以下示例中,我们有一个配对的动物列表和一个环境列表,即动物#1 与环境#1 匹配,动物#2 与环境#2 匹配,依此类推:
>>> animals = ["elephant", "zebra", "rhinoceros",↲ "dolphin"]
>>> environments = ["savanna", "forest", "river"]
>>> for animal in animals:
... for environment in environments:
... print("a", animal, "in the", environment)
a elephant in the savanna
a elephant in the forest
a elephant in the river
a zebra in the savanna
a zebra in the forest
a zebra in the river
a rhinoceros in the savanna
a rhinoceros in the forest
a rhinoceros in the river
a dolphin in the savanna
a dolphin in the forest
a dolphin in the river
>>> animals = ["elephant", "zebra", "rhinoceros",↲ "dolphin"]
>>> environments = ["savanna", "forest", "savanna",↲ "river"]
>>> for animal, environment in zip(animals,↲ environments):
... print("a", animal, "live in the", environment)
a elephant live in the savanna
a zebra live in the forest
a rhinoceros live in the savanna
a dolphin live in the river
2.7 发电机
生成器使得浏览一系列元素成为可能(以列表的方式);然而,它不像列表那样在存储器中存储所有的元素:生成器一个接一个地产生元素,并且这些元素必须被立即处理(例如,使用循环)。因此,生成器可以提高性能,尤其是在处理大量数据时。这就是为什么许多 Owlready 方法返回生成器而不是列表。
也可以使用list()
功能将发生器转换成列表,例如,用于显示,如下所示:
>>> print(list(my_generator))
相反,要在发电机上循环,最好不要使用list()
来提高性能,如下所示:
>>> for x in my_generator: print(x)
2.8 功能(def
)
函数用于定义一组指令(或“子例程”),以便在主程序的不同位置执行几次。这组指令可以接收参数:这些参数将被传递给函数的调用,并将作为局部变量在函数内部可用。这些函数是用def
语句创建的,其一般语法是:
def function_name(parameter1, parameter2 = default_value,...):
function body
return return_value
函数可以接收多个参数,每个参数都有一个默认值。
return
语句指示函数的返回值,并中断它。
然后,可以用括号调用函数(括号是强制的,即使没有参数):
returned_value = function_name(parameter1_value, parameter2_value)
returned_value = my_function_with_no_parameter()
下面是一个简单的函数示例:
>>> def twofold(x):
... return x * 2
>>> twofold(3)
6
>>> twofold("bla")
'blabla'
请注意,函数参数不是类型化的。这就是为什么在前面的例子中,我们能够对整数和字符串都使用我们的twofold()
函数。
调用函数时,可以命名参数,这允许以任何顺序传递参数:
returned_value = function_name(parameter2 = parameter2_value,
parameter1 = parameter1_value)
函数也可以有可变数量的参数:
def my_function(*args, **kargs):
function body
return returned_value
args
( arguments )将接收一个带有非命名参数值的元组,kargs
(关键字 arguments)一个带有命名参数的字典。这里有一个例子:
>>> def function_with_variable_parameters(*args, **kargs):
... print(args, kargs)
>>> function_with_variable_parameters(1, 2, 3, extra_param = 4)
(1, 2, 3) { "extra_param": 4 }
调用函数时也可以使用以下语法:
>>> kargs = { "parameter1": 1, "parameter2": 2 }
>>> function(**kargs)
# equivalent
to function(parameter1 = 1, parameter2 = 2)
2.9 类(class
)
类别和实例
类是面向对象编程的基础。一个类代表一个创建对象的模板,例如,我们可能有一个创建动物或书籍的类。一个类也可以被看作是一个对象的一般类别,例如,一本书是一个一般的类别,许多不同的书以不同的标题、作者等等存在。
按照惯例,类名总是以大写字母开头(例如,“Book”)。该类定义了该类的每个对象的可用属性(例如,对于类 Book: title、author 和 price)以及可以应用于每个对象的方法(例如,对于类 Book: format a book citation)。
然后,该类将创建该类的对象,称为“实例”,例如,“指环王”和“琥珀中的九个王子”将是同一个“书”类的两个实例。因此,该类使得“分解”实例共有的部分成为可能:属性定义和方法,而属性值是特定于每个实例的。
在 Python 中,类是用class
语句创建的。方法是用def
语句在类内部创建的(对于函数);第一个参数表示应用该方法的对象(按照惯例它被称为self
;它相当于 Java 或 C++中的关键字this
,但在方法参数中显式出现)。属性不是类型化的,就像变量一样。它们是通过给它们一个值来定义的,语法为“self.attribute_name = value
”。
class
语句的一般语法是:
class my_class(parent_class1, parent_class2,...):
class_attribute_name = value
def __init__(self, parameters...): # constructor
self.object_attribute_name = value
def method1(self, parameters...):
method_body
return returned_value
def method2(self, parameters...):
method_body
return returned_value
当一个类为空时(它不包含任何方法),有必要添加一个pass
语句,以向 Python 指示它:
class my_empty_class(parent_class1, parent_class2,...):
pass
在方法体中,当想要获取或修改其属性(self.attribute
)或调用其方法(self.method(parameters...)
)时,必须始终指定“self
”活动对象。
__init__()
是一种叫做“构造函数”的特殊方法。如果存在,则在创建新实例时会自动调用构造函数。构造函数可以接收参数,这些参数的值将在创建实例时给出。
以下是“Book”类的定义示例:
>>> class Book(object):
... def __init__(self, title, author, price):
... self.title = title
... self.author = author
... self.price = price
... def format_citation(self):
... return '%s' by %s (price: %s€) % (self.title,↲
self.author, self.price)
在前面的定义中,我们从object
类定义了 Book 类,这是 Python 中最通用的类。
然后,为了创建一个类的实例,以函数的方式调用该类。任何参数都将被传递给__init__()
构造函数。
my_object = my_class(constructor_parameters...)
点符号用于访问对象的属性和方法:
print(my_object.attribute)
my_object.attribute = value
my_object.method(parameters...)
在实例上调用方法时,self
参数是 never 给定的。前面的调用相当于
my_object_class.method(my_object, parameters...)
例如,我们可以创建 Book 类的一个或多个实例,获取或修改它们的属性值,并调用它们的方法:
>>> ldr = Book("The Lord of the Rings", "JRR Tolkien", 24)
>>> npa = Book("Nine Princes in Amber", "R Zelazny", 12)
>>> npa.author
'R Zelazny'
>>> npa.price = 10
>>> npa.format_citation()
"'Nine Princes in Amber' by R Zelazny (price: 10€)"
继承
继承是面向对象编程中的一个基本机制。这种机制允许您创建与给定类共享相似蓝图的新类,也就是说,在一个类中定义子类。例如,漫画是图书的一个特殊子类:漫画类是从图书继承而来的一个子类。一般的类(这里是书)称为“超类”或“父类”,更具体的类(这里是漫画)称为“子类”或“子类”。
子类继承其父类的所有属性和方法:就像 Book 类的实例一样,Comic 类的实例有标题、作者和价格(属性),并且可以格式化引文(方法)。然而,子类可能有额外的属性和方法。例如,一本漫画书由其作者(或编剧)来描述,但也由其插图画家来描述:因此我们可以为漫画类添加一个“插图画家”属性。通过避免重复父类及其子类共有的属性和方法,继承使得“分解”源代码和简化源代码成为可能。
此外,可以在子类中重新定义父类的方法。例如,可以重新定义 Comic 类的构造函数来接受一个额外的“illustrator
”参数,并且可以重新定义format_citation()
方法来显示 illustrator 名称。重新定义方法时,可以通过使用关键字super()
调用父类来委托给父类方法,如下例所示:
class my_child_class(my_parent_class1, my_parent_class2,...):
def my_method(self, parameters...):
parent_returned_value = super().my_method(parameters...)
additional child class method body
return child_return_value
下面的示例定义了从 Book 类继承而来的 Comic 类:
>>> class Comic(Book):
... def __init__(self, title, author, illustrator, price):
... super().__init__(title, author, price)
... self.illustrator = illustrator
... def format_citation(self):
... return "'%s' written by %s and illustrated by %s↲
(price: %s€)" % (self.title, self.author, self.illustrator,↲
self.price)
构造函数方法__init__()
和format_citation()
方法已经在漫画子类中重新定义。新的构造函数定义支持illustrator
属性,并委托给父类方法来管理标题、作者和价格属性。
以下示例创建了一个 Comic 实例:
>>> re = Comic("Return to the Earth", "Yves Ferry",↲
"Manu Larcenet", 10)
>>> re.format_citation()
"'Return to the Earth' written by Yves Ferry and illustrated by↲
Manu Larcenet (price: 10€)"
注意,我们可以调用format_citation()
方法,而不知道我们调用它的对象是一本书还是一部漫画。Python 会根据对象的类别自动选择正确的方法。这种机制被称为多态性。
下面的例子遍历了我们创建的三个实例,并显示了它们的引用。x
变量有时包含一本书,有时包含一部漫画,在不知道对象x
的确切类的情况下调用format_citation()
方法。
>>> for x in [ldr, npa, re]:
... print(x.format_citation())
"'The Lord of the Rings' by JRR Tolkien (price: 24€)"
"'Nine Princes in Amber' by R Zelazny (price: 10€)"
"'Return to the Earth' written by Yves Ferry and illustrated↲
by Manu Larcenet (price: 10€)"
Python 也允许多重继承:当定义一个子类时,可以给出几个父类,用逗号分隔。
特殊方法名称
在 Python 中,开头和结尾带有两个下划线的方法名是特殊的方法。以下是主要的几个:
-
__init__(self, parameters...)
:构造器 -
__del__(self)
:毁灭者 -
__repr__(self)
:返回显示给程序员的字符串 -
__str__(self)
:返回显示给最终用户的字符串
2.9.4 面向对象编程的函数和运算符
以下三个属性和函数可用于分析对象和/或类之间的关系:
-
object.__class__
返回对象的类或类型,例如: -
isinstance(object, Class)
测试给定对象是否属于给定类(包括子类、孙类、等)。),例如:
>>> ldr.__class__
<class 'Book'>
>>> "blabla".__class__
<class 'str'>
issubclass(Class, parent_class)
测试给定的类是否继承自parent_class
,例如:
>>> isinstance(ldr, Book)
True
>>> isinstance(ldr, Comic)
False
>>> isinstance(re, Book)
True
>>> isinstance(re, Comic)
True
>>> issubclass(Comic, Book)
True
>>> issubclass(Book, Comic)
False
is
运算符允许测试两个对象是否相同:
>>> ldr is ldr
True
>>> ldr is npa
False
最后,当属性的名称在编写程序时未知,但在变量执行期间可用(作为字符串)时,使用以下函数来操作对象的属性:
-
hasattr(object, attribute_name)
测试对象是否有名为attribute_name
的属性。 -
getattr(object, attribute_name)
返回对象名为attribute_name
的属性的值。 -
setattr(object, attribute_name, value)
为对象定义名为attribute_name
的属性值。 -
delattr(object, attribute_name)
从对象中删除名为attribute_name
的属性。
>>> attribute_name = "author"
>>> hasattr(ldr, attribute_name)
True
>>> getattr(ldr, attribute_name)
'JRR Tolkien'
这些方法对于自省特别有用,也就是说,在不知道对象的类或属性的情况下,以通用的方式操作对象。
2.10 Python 模块
Python 模块定义了特定领域(如数学、生物信息学、3D 图形等)的附加函数和类。).Owlready2 是 Python 模块的一个例子。这些模块中包含的函数和类在 Python 中默认不可用;在访问和使用模块之前,必须导入相应的模块。
导入模块
在 Python 中有两种导入模块的方法:
-
导入模块及其名称。使用这种方法,有必要在模块名后面加上一个“.”在模块的每个函数和类的前面。下面是一个关于
math
模块的例子:>>> import math >>> math.cos(0.0) 1.0
-
导入模块的内容。使用这种方法,可以直接使用模块的函数和类,而不必在每次调用时都提到模块的名称。另一方面,如果几个模块定义了同名的函数或类,这可能会有问题:在这种情况下,最后一次导入会覆盖前一次导入。这里是另一个关于
math
模块的例子:
>>> from math import *
>>> cos(0.0)
1.0
Python 语言包括大量的“标准”模块,这些模块随 Python 本身一起安装。官方 Python 文档描述了每个模块;可从以下地址在线获得:
https://docs.python.org/3/py-modindex.html
其他模块可以从 PyPI (Python 包索引)安装,可从以下网址获得
安装附加模块
“pip3”工具允许从 PyPI 通过互联网自动下载、安装和更新 Python 3 模块。该工具可以在 shell 命令行(在 Unix/Mac 下)或 MS-DOS 命令提示符(在 Windows 下)中使用。以下命令行安装 Python 模块(如果已经安装,则更新它):
pip3 install -U name_of_the_module_to_install
最好以“root”(或 Linux/Mac 下的超级用户)或“administrator”(Windows 下)的身份安装这些模块,这样所有用户都可以使用它们。但是,这不是必须的:如果您没有全局安装所需的权限,您可以只为当前用户安装模块,使用参数“--user
”。以下命令行为当前用户安装模块:
pip3 install -U --user name_of_the_module_to_install
2.11 安装 Owlready2
Owlready 版本 2 可以通过“pip3”工具从互联网上安装;对应的模块名为“owlready2”(注意不要忘记版本号 2)。
此外,Owlready 还提供了一个在 Cython 中优化的版本,cython 是一种从代码 c 中的 Python 编译派生的语言。为了从这个优化的版本中受益,必须预先安装“cyt hon”模块。然而,如果 Cython 的安装出错,或者如果您没有 C 编译器(特别是在 Windows 上),您可以安装没有 Cython 的 Owlready,代价是加载本体时性能(稍微)降低。
最后,以下 Python 模块也将用于本书的其余部分:“Flask”、“MyGene”和“RDFlib”。
2.11.1 从终端安装 Owlready2
以下命令可用于在终端(Linux/Mac 下的 Bash 终端,Windows 下的 DOS 命令行界面)中安装 Owlready2 和其他模块:
pip3 install -U cython
pip3 install -U owlready2 Flask mygene rdflib
如果您没有 root 或管理员权限,请使用以下命令为活动用户安装模块:
pip3 install -U --user cython
pip3 install -U --user owlready2 Flask mygene rdflib
2.11.2 从 IDLE 或 Spyder(或任何 Python shell)安装 Owlready2
您可以使用以下 Python 命令从任何 Python 3.7.x 控制台安装 Owlready2,包括在集成开发环境中找到的命令,包括 IDLE 或 Spyder3:
>>> import pip.__main__
>>> pip.__main__._main(["install", "-U", "--user", "cython")
>>> pip.__main__._main(["install", "-U", "--user", "owlready2", "rdflib")
>>> pip.__main__._main(["install", "-U", "--user", "Flask", "mygene")
2.11.3 手动安装 Owlready2
如果出现问题,Owlready2 也可以通过五个步骤手动安装:
-
从 PyPI 下载压缩源代码:
https://pypi.org/project/Owlready2/#files
.
-
解压缩压缩的源代码,例如,在 Windows 下的“C:\”下。
-
源目录被命名为“Owlready2-0.xx”,其中“xx”是版本号(例如,“Owlready2-0.25”)。将此目录重命名为“owlready2”,例如“C:\owlready2”。
-
在 PYTHONPATH 中添加包含源目录的目录(在我们的例子中是“C:\ ”);这可以用 Python 来实现,如下所示(注意:不要忘记将任何反斜杠加倍!):
>>> import sys >>> sys.path.append("C:\\")
-
现在可以导入 Owlready2 了!
>>> import owlready2
2.12 摘要
在本章中,我们已经了解了如何用 Python 进行基本编程,包括语言语法、控制结构(如条件和循环)以及面向对象的编程。我们还回顾了主要的 Python 数据类型,比如字符串或列表。最后,我们看到了如何安装 Python 模块,特别是 Owlready 和本书剩余部分中的示例所需的其他模块。
三、OWL 本体论
“本体论”一词来源于哲学,对应的是“存在的科学”。这个术语随后在计算机科学中被用于指定一个领域中所有对象的正式定义以及这些对象之间存在的关系。因此它是一种“形式本体论”。因此,本体旨在构造和形式化一个领域中的对象,尽可能独立于预期的应用:本体因此可以被同一领域中的其他应用重用。
具体地说,形式本体可以用来实现两个目标:
-
执行自动推理:形式本体允许使用推理器进行逻辑推理。比如一个动物的本体可以推导出一个黑白条纹的动物其实是斑马。自动推理将在本书第七章的主题上更加具体。
-
链接不同来源的知识:正式的本体使用互联网地址(称为 IRI,国际化资源标识符)来标识不同的实体(或对象)。因此,所有本体共享相同的名称空间:任何本体都可以引用任何其他本体。此外,本体允许等价关系的定义:因此,如果相同的事物已经由两个不同的人在两个不同的本体中声明为两个不同的实体,第三个人可以在这些实体之间添加等价关系,以便它们成为一个。
这两个目标是互补的,因为联系知识可以使新的推理成为可能。
在这一章中,我们将解释什么是形式本体论,而不涉及理论方面。我们将强调本体和编程中使用的对象模型之间的相似性和差异,我们将构建一个简单的本体示例,然后我们将再次使用它来说明后面章节中的示例。
3.1 一个本体……它是什么样子的?
从理论的角度来看,一个本体包含了公理。描述逻辑用于形式化实体的定义,并以逻辑公理的形式表示它们。附录 A 简要描述了这些逻辑。然而,如果你不理解描述逻辑和相关的公式也没关系——我自己早在知道或理解这些公式之前就开始使用形式本体论了!这不会妨碍你在本书的其余部分编写你的第一个本体,或者有效地使用这些本体。
从实用的角度来看,一个本体使得定义一个模型成为可能,就像 Python(见 2.9)这样的编程语言的类和实例的方式,但是具有更高的表达水平,也就是说,更详细。本体和面向对象编程因此共享许多共同的元素,但是经常使用不同的术语来指代相同或非常相似的事物。下表给出了面向对象编程领域和正式本体领域的词汇表之间的对应关系:
|面向对象编程
|
形式本体
|
| --- | --- |
| 目标 | 实体 |
| 组件 | 本体论 |
| 班级 | 班级 |
| 类继承 | 类继承,也称为“是-a”关系 |
| —(无同等物) | 财产继承 |
| 情况 | 个人 |
| 属性或特性 | 属性、角色或谓词 |
| 实例的属性值 | 关系 |
| 类别名 | 伊利 |
| 数据类型 | 数据类型 |
| 方法 | —(无同等物) |
| —(无同等物) | 逻辑构造函数限制解体 |
面向本体的编程,我们将在下一章看到,将把这两个世界结合在一起。
因此,本体是一组实体,可以是类、属性或个体。与 Python(或任何其他面向对象的编程语言)的对象模型相比,我们有三个主要区别:
-
属性是在类之外独立定义的。
-
个体可以属于一个类,但也可以属于几个类(这是多重实例化,类似于多重继承,只是针对实例)。
-
本体论是基于开放世界的假设:也就是说,任何没有被明确禁止的东西都被认为是可能的。例如,如果我们定义“指环王”这本书的作者是“JRR Tolkien”,那么开放世界假设为这本书留下了其他额外作者存在的可能性。由于 JRR 托尔金是唯一的作者,我们还必须指出“指环王”除了“JRR 托尔金”之外没有其他作者(通常使用 OWL 限制)。
本体有几种语言;OWL (Web 本体语言)是目前应用最广泛的语言。OWL 本体可以保存在 RDF/XML 格式(最常见的格式)的文件中,也可以保存在 OWL/XML、N-Triples、Turtle 和其他格式的文件中。
3.2 使用 Protégé编辑器手动创建本体
可以用本体编辑器手工创建一个本体。迄今为止,使用最多的编辑器是 Protégé。可在以下地址免费获取: https://protege.stanford.edu
。稍后我们将使用它来构建我们关于细菌的示例本体。
3.3 示例:细菌的本体
为了说明本体的构建和它所能提供的可能性,我们将以细菌本体为例。该本体旨在描述细菌及其物理和化学特性。然而,为了简洁起见,我们将把自己限制在几个简单的特征和少数物种上。我提前向我的生物学家读者道歉,有时我们不得不进行粗糙的简化——完整和精确的细菌本体论的概念本身就构成了一项真正的研究工作!
我们将只保留以下三个特征来描述细菌:
-
它们的形状:细菌可以是圆形或杆状(细长形)。
-
它们的分组:细菌可以彼此隔离,也可以成对、成簇或成链分组,链可以是小链或长链。
-
它们的革兰氏状态:革兰氏阳性细菌通过革兰氏试验染色,不像革兰氏阴性细菌。
图 3-1 显示了根据这些特征对细菌进行的分类。圆形细菌称为“球菌”,杆状细菌称为“杆菌”。
此外,我们将只保留以下三个致病细菌家族:
-
葡萄球菌:圆形,聚集成簇,革兰氏阳性
-
链球菌:圆形,聚集成小链,但从未分离,革兰氏阳性
-
假单胞菌:杆状,成对分组或分离,革兰氏阳性
此后,我们将考虑一个细菌可以有几个群体:事实上,观察从来没有涉及到一个单一的细菌,但对几个。因此,观察同一种细菌的几个群是很常见的:例如,成簇的葡萄球菌可能偶尔单独或成对出现。然而,链球菌从来不是孤立的,而是成组的(成对的,成簇的,当然,最好是成链的)。
图 3-2
细菌本体的 UML 类图
图 3-1
根据三个标准对细菌进行简单分类
图 3-2 给出了 UML(统一建模语言)中的类图。然而,请注意,本体允许表示比类图上显示的更多的信息。例如,(实际上)所有聚集成簇的圆形革兰氏阳性细菌都是葡萄球菌。因此,对于这种细菌,有可能推断出细菌的种类、形状、类群和革兰氏状态。相反,假单胞菌不是唯一的杆状、孤立或成对的细菌。这是一个重要的区别,因为它会影响自动推理;然而,一个“经典”的对象模型(比如 Python 见 2.9)不允许将其考虑在内。
在本章的开始,我们将本体定义为“尽可能独立于预期的应用”。例如,细菌的本体可以有多种应用,例如:
-
创建一个描述不同细菌特性的百科网站(见 4.12)
-
促进细菌信息的输入或提取(见 5.14)
-
帮助识别未知细菌(见 7.7)
-
用关于细菌的信息丰富已经存在的本体或资源,如 UMLS(见 9.10)
-
通过允许相似细菌的分组来促进医院中的统计研究(以回答诸如“上个月厌氧菌感染的数量增加了吗?”)
这些应用中的每一个都可以通过特定的知识库来实现。例如,可以使用由如下规则组成的知识库来识别细菌:
-
如果 shape = round,grouping = in cluster,gram = '+'
-
然后是葡萄球菌
然而,一个本体能够实现来自同一知识源的所有这些应用,这极大地方便了这些知识的维护和重用。
在接下来的几节中,我们将使用 Protégé editor 从这种细菌分类中构建一个(小的)形式本体。
3.4 创建新的本体
当您启动 Protégé editor 时,它会自动创建一个新的空本体。编辑器包括几个选项卡;默认情况下,会显示活动的本体选项卡。
在这个选项卡中,我们将定义我们的本体的 IRI。IRI 是本体的“名称”,这个名称采用互联网地址的形式。但是,请注意,IRI 必须是互联网地址的形式,但是本体不需要在互联网上的这个地址可用!因此,通常创建其 IRI 以“http://www.semanticweb.org/
或“http://www.test.org/
开头的本体,而不拥有这些互联网域名的权利。
我们将称我们的细菌为本体论:
(注意:这个互联网地址指向我的个人网站,在那里你实际上可以下载完整的本体)。您可以在 Protégé的“本体 IRI”字段中输入这个 IRI,如下图所示:
然后,您可以将本体以 RDF/XML 格式保存在一个名为“bacteria.owl”的文件中。此后,不要忘记在编辑期间定期保存本体。
3.4.1 类别
在 Protégé中,“Classes”选项卡允许您浏览现有的类并创建新的类。按钮和
允许您分别创建所选类的新的子类或姐妹类。使用这些按钮,我们可以创建一个与我们先前的 UML 模型相对应的类层次结构,如下面的屏幕截图所示:
在本体论中,遗传也被称为“是一种关系”:例如,我们可以说一个假单胞菌是一个细菌。
分离
本体和对象模型的一个重要区别如下:在本体中,一个个体可以属于几个类。因此,一个给定的形状很可能是既圆又棒的!开放世界假设允许这种类型的解释:任何没有被正式禁止的事情都被认为是可能的。
在我们的细菌本体论中,我们想要禁止这一点:给定的形状要么是圆形,要么是杆状,但不能同时是两者。为此,我们必须将 Round 和 Rod 这两个类声明为不相交的。两个不相交的类不能有共同的个体。
不相交的类在“类”选项卡的“描述”面板中声明。我们将选择杆类,然后单击“Disjoint with”部分右侧的“+”按钮,并在对话框的“Class hierarchy”选项卡中选择圆形类。您应该会得到以下结果:
这两个类现在是不相交的。注意,没有必要声明第二个类(圆)与第一个类(圆)不相交:这是从前面的声明中自动推导出来的。
同样,必须声明 InSmallChain 类与 InLongChain 类不相交。
Isolated、InPair、InCluster 和 InChain 类必须声明为 pairwise disjoint :也就是说,由这个列表中的两个类组成的任何对都是不相交的。为此,只需选择其中一个类(例如,隔离),单击“Disjoint with”右侧的“+”按钮,并同时选择其他三个类(通过按下控制键,而不是单击三次“+”按钮!).结果应该如下所示:
注意,关于分组的亚类,不相交并不意味着给定的细菌不能用两个不同的分组来观察(例如,分离的或成对的,如假单胞菌)。分离仅仅意味着一个给定的类群不能既是孤立的又是成对的,但它并不禁止一个细菌有两个不同的类群,一个类群是孤立的,另一个类群是成对的。
同样,类细菌、形状和分组必须声明为不相交的:例如,一个几何形状不能与一个细菌是同一个东西!这对人类来说似乎是显而易见的,但请记住,对机器来说并非如此。本体寻求全面地形式化知识,包括最明显的知识。
3.4.3 分区
我们已经定义了两类形状,圆形和杆形,它们现在是不相交的。然而,我们没有排除其他形状的存在,例如三角形。同样,开放世界假设使这种解释成为可能。然而,细菌只有两种可能的形状:圆形或杆状。我们必须声明所有的形状不是圆的就是棒的:它是一个分区(我们将说圆和棒的类构成了类形状的一个分区)。
为此,我们选择 Shape 类,并在“描述”面板中,单击“子类”右侧的“+”。这个“+”按钮允许您向类中添加超类;这些可以是命名类,也可以是 OWL 逻辑构造函数,就像这里。在出现的对话框中,我们选择“类表达式编辑器”选项卡,并输入构造函数“Round 或 Rod”。您应该获得以下结果:
这个构造函数“or”允许两个类用一个逻辑“or”链接起来(当我们用集合逻辑思考时,也称为 union )。这意味着 Shape 类是 Rod 和 Round 类的并集的子类。因此,现在任何形状不是圆形就是杆状,因此没有其他可能的形状。
同样,我们必须划分 in chain(“in small chain 或 InLongChain”的子类)和 Grouping(“Isolated or in pair or InCluster or in chain”的子类)。
数据属性
我们现在将处理属性。在本体中,与面向对象编程不同,属性是独立于类定义的。OWL 考虑了三类属性:值为数据的数据属性(数字、文本、日期、布尔值等。)、其值是实体(即本体个体)的对象属性,以及不干预语义或推理并因此可以无限制地混合数据和实体的注释属性。
在 Protégé中,“数据属性”选项卡允许您创建数据属性。OWL 除了支持类之间的继承,还支持属性之间的继承;但是,我们不会在这里使用它。使用和
按钮,其工作方式类似于类的按钮,我们将创建两个名为“gram_positive”和“nb_colonies”的新数据属性。最后一个属性对于描述细菌来说并不是很有用,但是它可以作为数值数据属性的一个例子。
您应该会得到以下结果:
可以通过指定以下内容来配置每个数据属性:
-
itsdomain(protégé中的“Domains (intersection)”):这是为其定义属性的类。
-
它的范围(“范围”):这是相关的数据类型。它可以是整数或实数、布尔值、字符串、日期等等。请注意:为了以后使用 Python 和 Owlready,最好使用 integer 类型表示整数,decimal 类型表示实数(更多信息请参考表 4-1 )。注意,OWL 属性的范围与 Python
range()
函数无关,Python 函数允许你创建数字列表(见 2.6)。 -
它的功能性状态(“功能性”复选框):当属性是功能性的时,给定的个人对于该属性只能有(最多)一个值。相反,如果属性不是功能性的,一个给定的个体可以有几个值。
域和范围是可选的。可以定义几个域和范围;然而,考虑的是不同域/范围的交集,而不是它们的联合,这通常不是期望的结果。例如,考虑属性“has_shape”和两个类(细菌和病毒),其中个体可以具有形状。如果我们定义两个域,细菌和病毒,只有属于细菌类和病毒类的个体才能有形状!如果有人想说所有的病毒和所有的细菌都可能有一个形状,有必要将这个域定义为类的联合,也就是说,“细菌或病毒”。
这里,我们将如下配置我们的两个数据属性:
-
革兰氏阳性:功能性(勾选方框),域:细菌,范围:布尔
-
nb _ 菌落:功能性(打勾),域:细菌,范围:整数
对象属性
在 Protégé中,“对象属性”选项卡允许您创建对象属性。使用和
按钮,我们创建了四个名为“has_shape”、“has_grouping”、“is_shape_of”和“is_grouping_of”的新对象属性,如下图所示:
可以通过指定以下内容来配置每个对象属性:
-
itsdomain(protégé中的“Domains (intersection)”):这是为其定义属性的类。
-
其范围(“范围(交集)”):这是关联对象的类。
如前所述,如果指示了几个域或范围,则考虑它们的交集。
-
其逆属性(“的逆”):逆属性对应于当该属性被反向读取时存在的关系;如果属性存在于 A 和 B 之间,则它的逆属性存在于 B 和 A 之间。例如,属性“is_shape_of”是“has_shape”的逆属性:如果细菌 X 具有形状 A,则 A 是 X 的形状。这些逆属性在 Python 中使用关系 has_shape/is_shape_of 双向导航时非常有用。
-
它的功能性状态(“功能性”复选框):当属性是功能性的时,给定的个人对于该属性只能有(最多)一个值。相反,如果属性不是功能性的,一个给定的个体可以有几个值。
-
其逆功能状态(“逆功能”复选框):如果逆属性是功能性的,则该属性是逆功能性的。例如,属性 is_father_of 是反函数:一个男人 A 可以是几个孩子 B、C、D 等等的父亲,但是对于这些孩子中的每一个,A 都是他们唯一的父亲。
-
它的可传递的状态(“可传递的”复选框):如果一个属性可以“链接”到几个对象上,那么这个属性就是可传递的。例如,性质“is _ larger _ than”是传递性的:如果个体 A 大于 B,如果 B 本身大于 C,那么我们可以推导出 A 大于 C。
-
它的对称状态(“对称”复选框):一个属性是对称的,如果它可以在两个方向上被无差别地读取(因此它是它自己的逆)。例如,属性“is_married_to”是对称的:如果人 A 与人 B 结婚,则 B 与 A 结婚。
-
它的非对称状态(“非对称”复选框):如果一个属性从来不是对称的,那么它就是非对称的。例如,属性“has_father”是非对称的:如果 A 对父亲 B 有,那么 B 不可能对父亲 A 有。
-
它的自反状态(“自反”复选框):如果一个属性总是应用于任何对象和它自身之间,那么它就是自反的。例如,属性“知道”是反身的:每个人 X 知道他自己。
-
它的非自反状态(“非自反”复选框):如果一个属性从不自反,那么它就是非自反的。例如,属性“is_married_to”是非反射性的:一个人不能与他/她自己结婚。
这里,我们将按如下方式配置对象属性:
-
has_shape:功能性(勾选方框),域:细菌,范围:形状
-
has_grouping:无功能(不要勾选该框),域:细菌,范围:分组
-
is_shape_of:无功能,域:形式,范围:细菌,逆:has_shape
-
is_grouping of:无功能,域:分组,范围:细菌,反向:has_grouping
注意,只定义对的两个属性中的一个属性的逆属性就足够了:比如这里,我们不需要指定 has _ shape has for inverse is _ shape _ of。这很容易从 is_shape_of 的逆性质推导出来。
限制
现在我们已经创建了属性,我们可以返回到类并基于这些属性添加限制。
通过单击“描述”部分中“子类”右侧的“+”按钮,可以在 Protégé的“类别”选项卡中添加限制。“子类化”允许你添加超类到类中;它可以是之前创建的 OWL 命名的类,也可以是构造函数,比如分区(见 3.4.3)和限制。
例如,假单胞菌具有革兰氏阴性染色。这导致 OWL 受到以下限制:布尔属性“gram_positive”必须具有 false 值。这种限制相当于一个类别:它是“革兰氏阳性”属性值为假的细菌类别。因此,我们可以将假单胞菌类定义为这个限制类的一个亚类。
OWL 提供了几类限制。以下限制用于模拟两个类之间的关系:
- 存在限制( some ):它代表了与属于某一类的个人在某一属性上至少有一种关系的那一类个人。
这个限制写在 Protégé的“property some class”中。例如,我们已经看到(图 3-1 )假单胞菌都是杆状的。rod 是一个类,这意味着 Rod 形状可能有几个子类型(例如,我们可以区分规则和不规则的 Rod 形状)。因此,这个限制将被写成“具有 _ 形状某个杆”。
- 基数限制(确切地说是、最小、最大):它表示某个属性与某个属于某个类的个体有一定数量关系的个体的类。该数字可以是精确值(精确值)或最小值(最小值)或最大值(最大值)。
这些限制写在被保护人的“财产确切地说是数量级”、“财产最小数量级”或“财产最大数量级”。它是存在性限制的一个更具体的版本:存在性限制等价于基数“min 1”的限制。
- 普遍限制( only ):代表与属于某一类(包括其子类)的一个(或多个)个体只有某一属性关系的个体的类。
这个限制在 Protégé中被写成“property only class”。例如,观察到假单胞菌仅具有杆状,我们将写为“仅具有杆状”。
注意不要将通用限制“仅具有杆的形状”与之前的存在限制“具有杆的形状”相混淆。存在限制说明所有假单胞菌至少有一个杆状,而普遍限制说明所有假单胞菌除杆状外没有其他形状。将两个相似的限制(一个是普遍性的,另一个是存在性的)与同一个目标类结合起来是很常见的。
另一方面,我们不会对分组使用一个通用的限制,因为我们以前已经看到,细菌偶尔可以呈现不同于其典型分组的其他分组。
以下限制使得对类和个体或数据类型值之间的关系建模成为可能:
- 价值限制( value ,有时被称为角色填充):它代表对某个属性具有某个价值的个人的类别。
这个限制被写成 Protégé中的“属性值个体/数据类型”。例如,假单胞菌总是与革兰氏阴性染色有关。这个限制将被写成“gram_positive value false”。
要在 Protégé中添加限制,单击“+”按钮后,您可以:
-
在“类表达式编辑器”选项卡中手动输入限制(提示:制表键允许您完成部分输入,例如,“Bact”代表“细菌”),
-
或者使用“对象限制创建者”或“数据限制创建者”选项卡(取决于属性的类型)并从下拉列表中选择值。
为了进一步描述假单胞菌类,我们将增加以下限制:
-
“有 _ 形一些杆”
-
"只有形状的杆"
-
1 的【克 _ 正值假】
请注意,我们对形状使用了一个存在性和一个普遍性限制,因为 Rod 是一个类,而不是一个个体或数据,相反,对 Gram 着色使用了值限制,因为 false 是一个数据类型值。
并集、交集和补集
OWL 还允许使用逻辑操作符作为构造函数。这些运算符有不同的名称,这取决于它们是从逻辑观点还是从集合论观点来考虑;但是,确实是一回事。有三种运算符可用:
- 逻辑 AND or 交集:这些是同时属于几个类的个体。
该交叉点在 Protégé中写为“class1 和 class2”。当然,更多的类别可以包含在交集中,例如,“类别 1 和类别 2 和类别 3”。
- 逻辑 OR 或 union:这些个体属于几个类中的一个类。
在 Protégé中,union 被写成“class1 或 class2”。同样,工会也不限于两类,例如,“一类或二类或三类”。例如,假单胞菌可分为两类:分离的和成对的。因此,我们可以建立这两个类的并集,这将写成“孤立的或成对的”。
此外,我们之前已经使用了并集来表示分区(参见 3.4.3)。
- 逻辑非或补语:这些人不属于某一特定的阶层。补语在 Protégé中写的是“而不是类”。
OWL 还允许您通过将括号中的不同元素分组,将逻辑运算符与限制和类结合起来。
为了细化假单胞菌类,我们将添加以下超类:
- " has_grouping 一些(孤立的或成对的)"
这一限制规定所有假单胞菌至少有一个分离的或成对的类群。
3.4.8 定义(相当于关系)
在前两节中,我们使用了限制和构造函数来描述类的属性。然而,这不是一个正式意义上的定义,因为我们还没有完整和唯一地描述这个类。比如所有的假单胞菌都是杆状的,但不是所有杆状的细菌都是假单胞菌!
OWL 允许你给一个类一个形式上的等价定义,通过一个等价关系。然后,定义的类允许在自动推理过程中对个体进行重新分类(我们将在 3.5 节和第七章中看到)。
例如,球菌类是具有圆形形状的细菌类(即,至少一个圆形形状且只有一个圆形形状)。因此,我们可以将其定义如下:
- 球菌:“细菌和(形状有些圆)
和(仅具有 _shape 圆形)"
注意,与我们之前作为假单胞菌的超类使用的限制和构造函数不同,等价类必须“在一块”定义。除非我们完全改变它的意思,否则我们不能把这个定义分成三个部分“细菌”、“有一些圆形”和“只有圆形”!
要在 Protégé中添加限制,请单击“等效于”右侧的“+”按钮,然后在“类表达式编辑器”选项卡中手动输入限制(同样,您可以使用制表键完成)。
Protégé用不同的图标标记已定义的类:一个棕色的圆圈,圆圈中出现符号“≡在描述逻辑中表示“等价于”。
同样,我们将杆菌、葡萄球菌和链球菌分类定义如下:
- 芽孢杆菌:“细菌和(有 _ 形状的一些杆)
和(has_shape only 杆)”
- 葡萄球菌:“细菌和(形状有些圆)
和(仅具有圆形形状)
和(有 _ 分组一些包含者)
and (gram_positive value true)"
- 链球菌:“细菌和(形状有些圆)
和(仅具有圆形形状)
和(has_grouping some InSmallChain)
and(仅 has_grouping(非隔离))
and (gram_positive value true)"
对于链球菌,限制条件“仅具有分组(非分离的)”表示链球菌只能具有非分离的分组:从未观察到分离的。
个人
Protégé的“个人”选项卡允许您浏览个人并创建新的个人。为了测试我们的本体,我们将创建几个个体。为此,在“类层次结构”面板中选择类,然后单击“成员列表”面板中的按钮(该面板列出了属于该类的个人)。我们将首先选择 Round 类并创建一个名为“round1”的形状,如下图所示:
同样,我们创建一个名为“in_cluster1”的个体,它属于“InCluster”类。
然后,我们创建一个名为“未知 _ 细菌”的个体,属于“细菌”类。最后,在“属性断言”面板中,我们通过单击“对象属性断言”和“数据属性断言”右侧的“+”按钮来输入此人的关系。我们将输入以下关系:
-
对象属性:
-
has_shape:圆形 1
-
散列分组:in_cluster1
-
-
数据属性:
-
革兰氏阳性:是
-
nb _ 菌落数:6
-
下面的屏幕截图显示了预期的结果:
其他结构
OWL 和 Protégé还提供了其他不常用的构造函数。
-
一组个体(也称为中的)允许创建一个仅限于一组个体的类。它写在大括号之间:“{个人 1,个人 2,...}".它还可以用来将一个个体转换成一个类(也称为 singleton 类,因为它只有一个实例/个体),如下:“{个体}”。
-
一个属性的逆写成“逆(属性)”。比如“逆(has_shape)”就相当于我们细菌本体中的“is_shape_of”。当本体没有定义命名逆属性时,这个构造函数特别有用。
-
属性链写为“property1 o property2”(圆圈对应小写字母“o”)。它们也被称为属性组合。它们使得“链”几个属性成为可能,例如,“是形状或分组”直接从一种形状传递到具有这种形状的细菌分组。
3.5 自动推理
现在我们的细菌本体已经准备好了!
为了验证本体中不存在不一致并测试自动推理,我们可以使用“推理机➤启动推理机”菜单来执行自动推理机。几个推理机是可用的;我推荐用隐士。
一旦推理完成,个人就被重新归类为受保护者。例如,我们创建的个体“未知细菌”属于细菌类。我们可以看到它被重新分类为一个新类别:葡萄球菌(新类别出现在 Protégé的黄色背景上)。事实上,这种细菌满足葡萄球菌的条件(圆形,成簇,革兰氏阳性)。
此外,推理机还重组了类。为了观察这一点,我们将返回到 Classes 选项卡并单击“类层次结构(推断)”。类别树已被更改。例如,我们可以看到假单胞菌类被重新归类为芽孢杆菌类的一个子类。事实上,该类的所有个体都满足芽孢杆菌类的定义,因为假单胞菌属都具有杆状。
你也可以尝试以下两种体验:
-
创建一个细菌类个体,呈杆状,成对分组和/或隔离,革兰氏阴性状态。该个体将被重新分类为杆菌类,而不是假单胞菌类。事实上,我们还没有给出假单胞菌类的正式定义;推理者因此不能推断这种细菌是假单胞菌。在设计本体时,缺乏定义是一个理想的选择,因为假单胞菌不是唯一具有杆状、分离或成对的革兰氏阴性细菌(见图 3-1 )。
-
创建一个细菌类的个体,具有圆形形状,以小链分组,并且具有革兰氏阳性状态。该个体将被重新分类为球菌类,而不是链球菌类。但是,这个类确实包含了一个定义!然而,我们刚刚创建的个体并不完全符合链球菌类的定义。
事实上,在定义中,我们指明了“仅 has_grouping(非孤立)”。在个体中,我们指出了一个小的连锁群;但是,属性“has_grouping”不起作用,因此可能有几个值。开放世界的假设意味着推理者不能排除另一个群体的存在,这个群体在本体论中没有提到,但可能是孤立的。
因此,为了能够推断出我们的个体是链球菌,有必要在本体中指出该个体除了那些明确提到的分组之外没有其他分组,或者他没有分离的类的分组。
另一方面,在正式定义中,我们还对“has_shape”属性使用了通用约束(“only”)。然而,这并不妨碍将个体分为球菌、杆菌和葡萄球菌类。为什么呢?因为该属性是功能性的,并且 Round 和 Rod 类是不相交的。因此,当细菌呈杆状时,它不可能呈圆形,反之亦然。相反,属性“has_grouping”不起作用,因此这种推理不再可能。
我们将回到这个问题,解决方案将在 7.3 中提供。
3.6 建模练习
这里有一些练习来训练你进行本体建模:
-
在细菌本体中,添加一个棒状的葡萄球菌类个体。运行推理器;你观察到了什么?
-
使用 Protégé editor,通过添加过氧化氢酶测试来扩展细菌的本体。这种生物测试有助于识别细菌,其结果可以是阳性或阴性。过氧化氢酶试验对葡萄球菌和假单胞菌呈阳性,对链球菌呈阴性。
-
使用 Protégé editor,通过添加细菌的颜色来扩展细菌本体。葡萄球菌是白色或金黄色的(这就是著名的金黄色葡萄球菌),链球菌是半透明的,假单胞菌一般是有色的(也就是说不是白色的)。
-
使用 Protégé editor,添加一类新的细菌:麻风分枝杆菌(汉森氏杆菌,导致麻风病)。这种细菌呈革兰氏阳性,杆状,成对分离或聚集。过氧化氢酶试验与这种细菌无关,因为它很难在体外生长。颜色是黄色的。最后,所有这些特征足以识别细菌。
-
在 Protégé编辑器中,添加一个细菌类个体,杆状,孤立,颜色为黄色。检查该个体是否被正确归类为麻风分枝杆菌。
-
在细菌本体论中,在细菌的不同亚类(葡萄球菌、链球菌、假单胞菌等)之间增加一个分界点。).这是否改变了对未知细菌的推理结果?
-
进行 OWL 本体论以构建药物相互作用。该本体旨在使用推理机自动检测医生处方中的交互。开放世界假设会在推理过程中造成问题吗?
-
使用 Protégé editor,构建一个描述书籍、作者和编辑的本体。你从 2.9 中的对象模型中获得灵感。
3.7 摘要
在这一章中,我们通过一个简单的细菌本体的例子介绍了 OWL 本体和 Protégé editor 的使用。我们已经看到了主要的 OWL 构造,也看到了一些经常遇到的困难,比如那些与开放世界假设相关的困难。
四、使用 Python 访问本体
在这一章中,我们将看到如何使用 Owlready 访问 Python 中本体的内容。我们将使用我们在第三章中创建的细菌本体,以及基因本体,一种在生物信息学中广泛使用的本体。
4.1 导入 Owlready
Owlready(版本 2)是以如下方式导入 Python 的:
>>> from owlready2 import *
注意,用“from owlready2 import *”导入模块内容比用“import owlready2”导入模块更好(见 2.10.1),因为 owl 已经重新定义了一些 Python 函数,比如issubclass()
函数。
4.2 加载本体
Owlready 允许您将 OWL 本体加载到 Python 中,并像从 Python 模块访问“传统”对象一样访问 OWL 实体。
本体可以通过三种不同的方式加载:
- 从其 IRI ( 国际化资源标识符),即互联网地址:
>>> onto = get_ontology("http://lesfleursdunormal.↲fr/static/
_downloads/bacteria.owl").load()
然后从互联网上下载并加载本体。
- 从包含本体副本的本地文件,例如,在 Linux/Unix/Mac 下:
>>> onto = get_ontology("/home/jiba/owlready/↲bacteria.owl").load()
或者在 Windows 下:
>>> onto = get_ontology("C:\\owlready\\bacteria.↲owl").load()
也可以将本体从本地副本加载到当前目录:
>>> onto = get_ontology("bacteria.owl").load()
然后从一个已经存在的 OWL 文件中加载本体(如果用引号括起来的文件不存在,显然会出现错误;当然,前面几行代码中的文件名只是示例)。小心,在 Windows 下,不要忘记在文件名中加双反斜杠!
- 从用
open()
、urlopen()
和其他函数获得的 Python 文件对象(见 2.4.8)。这种情况非常少见,但有时很有用(我们将在 8.8.1 中使用它来加载 DBpedia)。这里有一个例子:
>>> my_file = open("/path/to/file.owl")
>>> onto = get_ontology("http://lesfleursdunormal.↲fr/static/
_downloads/bacteria.owl")
>>> onto.load(fileobj = my_file)
Owlready 目前支持以下文件格式进行读取:
-
RDF/XML(OWL 本体最常用的文件格式)
-
OWL/XML
-
n-三元组
Owlready 维护一个已加载本体的缓存:如果第二次加载相同的本体,将返回相同的本体对象,而不必重新读取相应的文件。为了强制重载一个本体,我们将使用load()
方法的可选的reload
参数:
>>> onto.load(reload = True)
本体的base_iri
属性允许获得它的 IRI:
>>> onto.base_iri
'http://lesfleursdunormal.fr/static/_downloads/bacteria.owl#'
请注意,Owlready 已经自动确定了分隔符“#”或“/”,放在本体的 IRI 之后,并将其添加到末尾(这里是“#”)。然而,当调用get_ontology()
时,也可以在 IRI 的末尾显式包含分隔符。
4.3 导入的本体
本体的imported_ontologies
属性包含它导入的其他本体的列表:
>>> onto.imported_ontologies
[]
这里,我们的细菌本体没有导入任何其他本体(因此出现了前面的空列表)。Owlready 递归地自动加载导入的本体。
4.4 清单本体的内容
本体对象有许多方法来根据它们的类型遍历包含在本体中的实体。下表总结了所有这些方法:
|方法
|
遍历的实体
|
| --- | --- |
| individuals()
| 所有个人 |
| classes()
| 所有类别 |
| properties()
| 所有属性 |
| object_properties()
| 所有对象属性 |
| data_properties()
| 所有数据属性 |
| annotation_properties()
| 所有注释属性 |
| disjoints()
| 所有成对分离(包括成对不同个体和分离/不同对) |
| disjoint_classes()
| 所有成对不相交的类(包括不相交的类对) |
| disjoint_properties()
| 所有成对分离的属性(包括分离的属性对) |
| different_individuals()
| 所有成对的不同个体(包括不同的个体对) |
| rules()
| 所有 SWRL 规则 |
| variables()
| 所有 SWRL 变量 |
| general_axioms()
| 所有一般公理 |
这些方法返回生成器(参见 2.7);要显示内容,使用list()
Python 函数,该函数将生成器转换成一个列表:
>>> onto.classes()
<generator object _GraphManager.classes at 0x7f5a000fae58>
>>> list(onto.classes())
[bacteria.Bacterium, bacteria.Shape, bacteria.Grouping, bacteria.Round, bacteria.Rod, bacteria.Isolated, bacteria.InPair, bacteria.InCluster, bacteria.InChain, bacteria.InSmallChain, bacteria.InLongChain, bacteria.Pseudomonas, bacteria.Coccus, bacteria.Bacillus, bacteria.Staphylococcus, bacteria.Streptococcus]
然而,当发电机出现在回路中时,最好不要使用list()
,以提高性能:
>>> for c in onto.classes(): print(c.name)
Bacterium
Shape
Grouping
[...]
4.5 访问实体
在加载本体时,Owlready 已经分析了本体文件,并自动将其翻译成“主语-动词-宾语”三元组形式的 RDF 图(我们将在第十一章中更详细地返回 RDF)。这个 RDF 图以 SQLite3 格式存储在数据库中,默认情况下存储在 RAM 中(但是数据库也可以存储在磁盘上,我们将在 4.7 中看到)。然后,按需动态创建用于访问包含在本体中的实体的 Python 对象。因此,如果一个本体包括 100 个类,但是 Python 中只使用了 1 个,那么 Python 中将只创建这个类,其他 99 个将保留在数据库中的 RDF 图级别。
IRIS
伪字典允许从它的 IRI 访问任何实体。例如,要访问个人“未知 _ 细菌”,其完整 IRI 如下:
http://lesfleursdunormal.fr/static/_downloads/bacteria.owl#unknown_bacterium
我们将使用
>>> IRIS["http://lesfleursdunormal.fr/static/_downloads/↲
bacteria.owl#unknown_bacterium"]
然而,这种表示法相当冗长。Owlready 允许更容易地访问存在于本体中的实体,用点符号“.”,好像本体是一个包含类和对象的 Python 模块。例如,我们也可以访问单个“未知 _ 细菌”,如下所示:
>>> onto.unknown_bacterium
当使用点符号时,Owlready 获取本体的基本 IRI(onto.base_iri
,这里是“”http://lesfleursdunormal.fr/static/_downloads/bacteria.owl#
“”),并附加出现在点之后的内容(这里是“未知 _ 细菌”),以获得所请求实体的 IRI。
一些 IRI 可能在属性名称中包含 Python 不支持的字符(例如空格);在这种情况下,也可以使用以下替代语法:
>>> onto["unknown_bacterium"]
最后,一些本体定义的实体的 IRI 不是从本体的 IRI 开始的;在这种情况下,可以通过IRIS
伪字典或名称空间来访问它(见 4.8)。
实体的iri
属性包含其完整的 IRI,而name
属性包含 IRI 的最后一部分(在“#”字符或最后一个“/”之后):
>>> onto.unknown_bacterium.iri
'http://lesfleursdunormal.fr/static/_downloads/↲
bacteria.owl#unknown_bacterium'
>>> onto.unknown_bacterium.name
'unknown_bacterium'
4.5.1 个人
个体可以像“正常的”Python 对象一样被操纵。特别是,可以用isinstance()
函数测试它们在给定类中的成员资格,就像对任何 Python 对象一样:
>>> isinstance(onto.unknown_bacterium, onto.Bacterium)
True
__class__
属性允许获取对象的类别:
>>> onto.unknown_bacterium.__class__
bacteria.Bacterium
然而,在本体中,一个对象可以同时属于几个类,这在 Python 中是不允许的。在这种情况下,Owlready 自动创建一个“merge”类,该类继承自所有个体的类。为了获得个体的类的列表,因此最好使用is_a
属性(它也包含限制和逻辑构造器,如果有的话):
>>> onto.unknown_bacterium.is_a
[bacteria.Bacterium]
最后,equivalent_to
属性包含等价个体的列表(在 OWL 或 Protégé editor 中通常称为“same as”)。
>>> onto.unknown_bacterium.equivalent_to
[]
这里,列表是空的,因为我们未知的细菌还没有被定义为等同于任何其他细菌。
4.5.2 关系
个体的关系可以通过指向“个体.属性”的点符号来获得,例如:
>>> onto.unknown_bacterium.gram_positive
True
>>> onto.unknown_bacterium.has_shape
bacteria.round1
>>> onto.unknown_bacterium.has_grouping
[bacteria.in_cluster1]
关系返回一个值列表(对于前面的属性“has_grouping ”),或者如果属性是功能性的,返回一个值(对于前面的另外两个属性)。
值列表的first()
方法可用于检索第一个结果(当列表为空时默认为None
)。
>>> onto.unknown_bacterium.has_grouping.first()
bacteria.in_cluster1
此外,Owlready 在查询关系时会自动考虑反向属性。例如,我们可以问哪种细菌与“in_cluster1”组相关联:
>>> onto.in_cluster1.is_grouping_of
[bacteria.unknown_bacterium]
关系“in _ cluster 1 is _ grouping _ of unknown _ bacterium”没有出现在本体中(我们在第三章 Protégé中没有输入)。但是,从我们输入的关系“未知细菌在 _cluster1 中具有 _for_grouping”很容易推导出来。
存在于本体中的值被自动翻译成 Python 数据类型(int
、float
、str
等)。),根据表 4-1 中给出的对应关系。
当属性名以“INDIRECT_
”为前缀时,Owlready 也返回间接定义的关系,考虑到:
-
传递性、对称性和自反性
-
属性(即子属性)之间的继承关系
-
个人所属的阶级(对阶级的存在或价值限制)
-
类之间的继承(超类的存在或值限制)
-
等价关系(类和等价属性以及相同的个体)
这里有一个例子:
表 4-1
OWL 和 Python + Owlready 数据类型之间的对应关系。当多个 OWL 数据类型对应于同一个 Python 类型时,粗体的 OWL 数据类型是 Owlready 在保存本体时默认使用的数据类型
|猫头鹰
|
python+owlrayed
|
| --- | --- |
| XMLSchema#integer | (同 Internationalorganizations)国际组织 |
| XMLSchema #字节 | |
| XMLSchema#short | |
| XMLSchema#int | |
| XMLSchema#long | |
| XMLSchema#unsignedByte | |
| XML schema # unsigned hort | |
| XML schema # unsigned nt | |
| XML schema # anonymous | |
| XMLSchema#negativeInteger | |
| XMLSchema#nonNegativeInteger | |
| XMLSchema#positiveInteger | |
| XMLSchema #布尔值 | 弯曲件 |
| XMLSchema#decimal | 漂浮物 |
| XMLSchema#double | |
| XMLSchema#float | |
| 猫头鹰#真实 | |
| XMLSchema #字符串 | 潜艇用热中子反应堆(submarine thermal reactor 的缩写) |
| XML schema # normal string | owlready2.normstr |
| XMLSchema#anyURI | |
| XMLSchema#Name | |
| 简单的文字 | str(如果没有指定语言)owlready2.locstr(如果指定了语言,请参见 8.2) |
| XMLSchema#dateTime | datetime.datetime |
| XMLSchema #日期 | 日期时间.日期 |
| XMLSchema #时间 | 日期时间.时间 |
>>> onto.unknown_bacterium.INDIRECT_has_grouping
[bacteria.in_cluster1]
get_properties()
方法返回一个生成器,列出个人至少有一个关系的所有属性,例如:
>>> list(onto.unknown_bacterium.get_properties())
[bacteria.has_shape, bacteria.has_grouping,
bacteria.gram_positive, bacteria.nb_colonies]
最后,get_inverse_properties()
方法对反向属性做同样的处理,并返回“(subject,property)”形式的对,例如:
>>> list(onto.round1.get_inverse_properties())
[(bacteria.unknown_bacterium, bacteria.has_shape)]
4.5.3 类别
可以通过与其他实体相同的方式获得类:
>>> onto.Bacterium
本体类是真正的 Python 类,可以这样使用。例如,issubclass()
函数测试一个类是否是后代(子类、子类、等等)。另一种说法:
>>> issubclass(onto.Coccus, onto.Bacterium)
True
属性用来获得父类的列表。然而,对于个人来说,最好使用is_a
属性(它也包含限制和逻辑构造函数):
>>> onto.Coccus.is_a
[bacteria.Bacterium]
subclasses()
方法获取子类的列表(注意subclasses()
返回一个生成器,因此使用了list()
):
>>> list(onto.Bacterium.subclasses())
[bacteria.Pseudomonas, bacteria.Coccus, bacteria.Bacillus]
ancestors()
和descendants()
方法用于获得祖先类的集合(父母、祖父母、等)。)和后代类(子女、孙辈、等)。),分别为。
>>> onto.Bacterium.descendants()
{bacteria.Bacterium, bacteria.Pseudomonas, bacteria.Streptococcus,
bacteria.Staphylococcus, bacteria.Bacillus, bacteria.Coccus}
默认情况下,起始类包含在结果中(这就是为什么我们在前一个结果中找到bacteria.Bacterium
)。可选参数include_self
删除起始类。它的用法如下:
>>> onto.Bacterium.descendants(include_self = False)
{bacteria.Pseudomonas, bacteria.Streptococcus,
bacteria.Staphylococcus, bacteria.Bacillus, bacteria.Coccus}
instances()
方法用于获得属于一个类的个体列表(包括子类和子类的实例):
>>> onto.Bacterium.instances()
[bacteria.unknown_bacterium]
direct_instances()
方法以同样的方式工作,但是仅限于直接实例。
equivalent_to
属性包含等价类的列表:
>>> onto.Streptococcus.equivalent_to
[bacteria.Bacterium
& bacteria.has_shape.some(bacteria.Round)
& bacteria.has_shape.only(bacteria.Round)
& bacteria.has_grouping.some(bacteria.InSmallChain)
& bacteria.has_grouping.only(Not(bacteria.Isolated))
& bacteria.gram_positive.value(True)]
我们用不同的 OWL 构造函数获得了我们在第三章中输入的正式定义;我们将在第六章中看到如何操作这些。
如前所述,可以通过前缀“INDIRECT_
”来获得间接等价(例如,如果 A 等价于 B and B 等价于 C,我们将获得 A 等价于 B 和 C)。
>>> onto.Streptococcus.INDIRECT_equivalent_to
最后,disjoints()
和constructs()
方法返回生成器,分别列出引用该类的所有分离和构造函数。
存在性限制
Owlready 允许您访问存在性限制(some 和 value 类型的限制),就好像它们是“类属性”,使用点符号“Class.property ”,例如,在类链球菌上:
>>> onto.Streptococcus.gram_positive
True
>>> onto.Streptococcus.has_grouping
[bacteria.InSmallChain]
Owlready 还提供了对类定义中使用的所有构造函数的详细访问(参见 6.2)。
属性
超属性、子属性、祖先、后代和等效属性可以用与类相同的方式获得。
domain
和range
属性用于获取属性的域和范围。注意,这些属性都返回一个列表。当存在多个值时,OWL 认为域或范围是不同值的交集。
>>> onto.has_grouping.domain
[bacteria.Bacterium]
>>> onto.has_grouping.range
[bacteria.Grouping]
range_iri
属性用于以 IRI 列表的形式获取属性的范围,这有助于区分 OWL 支持的不同类型的数据(例如,XMLSchema#decimal、XMLSchema#double 和 XMLSchema#float,而对于 Owlready 中的所有三种类型,范围属性都是 Python float
类型,Python 只有一种类型的浮点数)。
python_name
属性用于改变一个属性的名字,在这个名字下可以用点符号访问属性。这允许您使用更符合 Python 精神的名称。事实上,OWL 属性通常被称为“has_ ...”,而在 Python 中,属性名很少这样开头。类似地,在 Python 中,我们更喜欢在包含值列表的属性末尾加上复数“s”。例如,我们可以将属性“has_grouping”的名称更改为“grouping ”,如下所示:
>>> onto.has_grouping.python_name = "groupings"
>>> onto.unknown_bacterium.groupings
[bacteria.in_cluster1]
请注意,只有在 Python 中使用点符号时,属性的名称才会改变。另一方面,object 属性仍然可以作为onto.has_grouping
访问,并且它的 IRI 不会改变。可以返回到以前的名称,如下所示:
>>> onto.has_grouping.python_name = onto.has_grouping.name
get_relations()
方法返回一个生成器,列出属性的所有(subject,object)对,例如:
>>> for subject, object in onto.has_grouping.get_relations():
... print(subject, "has for grouping" , object)
bacteria.unknown_bacterium has for grouping bacteria.in_cluster1
还可以使用另一种语法“property[individual]”来获取给定个人的属性值。与通常的语法“individual.property”不同,这种替代语法总是返回一个值列表(即使是在函数属性的情况下),这在某些情况下可能很有用:
>>> prop = onto.gram_positive
>>> prop[onto.unknown_bacterium]
[True]
如果属性名包含 Python 中的无效字符(例如," "),此语法也很有用)或者如果本体包括具有不同 IRI 但以相同名称结尾的几个属性。以下示例显示了如何在 Python 中访问具有无效名称的属性:
onto["my.propertyindividual"]
4.6 搜索实体
本体对象的search()
方法使得从实体的 IRI 和/或关系中搜索实体成为可能。搜索时,下列关键字是可用的,并且可以相互组合:
-
iri
由 IRI 去搜索 -
搜索给定类别的个人
-
subclass_of
搜索给定类的后代类 -
is_a
搜索给定类的个体和后代类 -
按关系搜索的任何属性名
此外,在字符串中,“*”可以用作通配符。以下示例搜索 IRI 包含“Coccus”的所有实体:
>>> onto.search(iri = "*Coccus*")
[bacteria.Coccus]
默认情况下,搜索区分大小写。_case_sensitive
参数用于改变这种行为,例如:
>>> onto.search(iri = "*Coccus*", _case_sensitive = False)
[bacteria.Coccus, bacteria.Staphylococcus, bacteria.Streptococcus]
这一次,我们发现更多的结果,因为“葡萄球菌”和“链球菌”确实包含“球菌”,但带有小写的“c”而不是大写的。
search()
返回的结果看起来像 Python 列表,可以作为列表使用。然而,它不是一个经典的列表;我们可以用 class 属性来检查它:
>>> r = onto.search(iri = "*Coccus*", _case_sensitive = False)
>>> r.__class__
<class 'owlready2.triplelite._SearchList'>
这是一个特殊的列表,称为“惰性”列表,其元素只在最后一刻才确定。例如,在下面的代码中,第一行创建了“lazy”列表,但是还没有执行搜索。只有在最后时刻,当我们请求访问列表的内容时,才会这样做(例如,使用print()
进行显示)。
>>> r = onto.search(iri = "*Coccus*", _case_sensitive = False)
>>> print(r) # The search is only performed here
[bacteria.Coccus, bacteria.Staphylococcus, bacteria.Streptococcus]
search()
方法可以接受多个参数。以下示例搜索属于革兰氏阳性菌类(=与True
有gram_positive
关系)的所有个体:
>>> onto.search(type = onto.Bacterium, gram_positive = True)
[bacteria.unknown_bacterium]
字符串“*”可以用作“通配符”,也就是说,不管关联的值是什么(包括非文本值:数字、对象等),都可以搜索关系的存在。).以下示例搜索革兰氏状态已知的所有细菌(无论它是什么):
>>> onto.search(type = onto.Bacterium, gram_positive = "*")
[bacteria.unknown_bacterium]
几个值的列表也可以用作search()
的参数。在这种情况下,只返回与列表中每个元素都有关系的实体。这里有一个例子(你必须在本体中创建个体isolated1
和by_two1
来测试这个例子):
>>> onto.search(type = onto.Bacterium,↲
has_grouping = [onto.isolated1, onto.by_two1])
也可以使用None
值搜索没有关系的个人。例如,我们可以搜索没有形状的细菌,如下所示:
>>> onto.search(type = onto.Bacterium, has_shape = None)
要在所有本体中进行搜索(如果已经加载了几个本体),可以搜索默认的“世界”default_world
,如下所示:
>>> default_world.search(iri = "*Coccus*")
带有search()
的搜索也可以嵌套。在这种情况下,Owlready 自动组合搜索,在 quadstore 中生成一个优化的 SQL 查询。以下示例搜索具有 InChain 分组的所有细菌(包括 InSmallChain 和 InFilament)。为此,我们嵌套了对search()
的两个调用:一个用于查找链分组,另一个用于查找相关的细菌。
>>> onto.search(type = onto.Bacterium,↲
has_grouping = onto.search(type = onto.InChain))
最后,search_one()
方法的工作方式与search()
相同,但是只返回一个结果,而不是一个列表。
要执行更复杂的搜索,可以通过结合使用 Owlready 和 RDFlib 来使用 SPARQL 查询语言(参见 11.3 节)。
4.7 巨大的本体和磁盘缓存
基因本体(GO)是生物信息学中广泛使用的本体,它非常庞大(近 200 MB)。使用以下命令加载 GO 需要几十秒甚至几分钟,这取决于计算机的功率和 OWL 文件的下载时间:
>>> go = get_ontology("http://purl.obolibrary.org/obo/go.owl").↲ load()
默认情况下,Owlready 在 RAM 中以 RDF 格式存储包含本体的 quadstore。在 Python 程序执行结束时,quadstore 丢失,每次新的执行都必须重新加载 OWL 文件。为了避免这些长时间的重新加载,可以使用default_world.set_backend()
方法将 quadstore 放在磁盘上。然后,default_world.save()
会保存它,例如:
>>> default_world.set_backend(filename = "quadstore.sqlite3")
>>> go = get_ontology("http://purl.obolibrary.org/obo/go.owl").↲ load()
>>> default_world.save()
这里,我们为 quadstore 使用了相对文件路径;我们可以使用绝对路径(例如,Linux/Mac 上的“/home/jiba/owlready/quadstore.sqlite3
”或 Windows 上的“C:\\quadstore.sqlite3
”)。
为了从 quadstore 加载本体,在 Python 的新的执行期间,通过使用与之前相同的三行代码,重新定义 quadstore 文件并加载本体就足够了(行default_world.save()
可以被忽略或保留;它将没有任何效果,因为没有要保存的更改)。加载是即时的,因为本体是直接从 quadstore 中恢复的,不需要任何下载或解析 OWL 文件的操作。
注意,如果内存中加载了几个本体(例如前面的细菌和 GO 的本体),所有的本体都存储在同一个 quadstore 中,因此保存在同一个文件中。
最后,default_world.save()
方法用于保存 quadstore 中所做的更改(该方法对应于数据库上的“提交”操作,因此如果没有要记录的更改,即使对于非常大的本体,它的性能成本也几乎为零)。如果更改没有保存,它们将在程序执行结束时丢失。
4.8 名称空间
一些本体在不属于它们自己的名称空间中定义实体。围棋就是这种情况:围棋 IRI 是“ http://purl.obolibrary.org/obo/go.owl
,但是围棋实体有以“ http://purl.obolibrary.org/obo/
”开头的虹膜(没有“go.owl”后缀)。因此,不可能使用go
本体对象来访问带点符号的实体:
>>> go.GO_0035065
None
的确,前面一行对应的是 IRI 的“ http://purl.obolibrary.org/obo/go.owl#GO_0035065
”,而预料中的 IRI 的概念是“ http://purl.obolibrary.org/obo/GO_0035065
”(所以不带“go.owl”后缀)。
要访问 GO 实体,可以使用 IRIS 全局伪字典(见 4.5)。另一个选择是为“”http://purl.obolibrary.org/obo/
”创建一个命名空间,如下:
>>> obo = get_namespace("http://purl.obolibrary.org/obo/")
然后,obo
名称空间可以用来访问带点符号的实体:
>>> obo.GO_0035065
obo.GO_0035065
>>> obo.GO_0035065.label
['regulation of histone acetylation']
4.9 修改实体渲染为文本
默认情况下,Owlready 显示实体的名称,前面有一个句点和 IRI 的最后一部分(不带扩展名“.”。猫头鹰”,如果存在的话)。但是,当实体的名称是任意标识符时,这种显示是不令人满意的,如下例所示:
>>> obo.GO_0035065
obo.GO_0035065
全局函数set_render_func()
允许重新定义 Owlready 呈现实体的方式。在下面的示例中,我们使用“label”注释属性来呈现实体的名称(即,其标识符),如果没有,则使用实体的名称:
>>> def my_rendering(entity):
... return entity.label.first() or entity.name
>>> set_render_func(my_rendering)
>>> obo.GO_0035065
regulation of histone acetylation
在围棋中,几乎所有的实体都是类(而不是个体;这在生物医学本体论中是相当普遍的做法)。如前所述(在 4.5.4 中),可以使用点符号访问这些类的存在性限制,如下例所示(其中RO_0002211
是“regulates”属性的名称 GO):
>>> obo.GO_0035065.RO_0002211
[histone acetylation]
然而,像以前一样,当属性名是任意代码时,使用属性名有时会很费力。接下来的三行允许您使用属性标签而不是它们的名称(在用下划线替换空格之后):
>>> for prop in go.properties():
... if prop.label:
... prop.python_name = prop.label.first().replace(" " , "_")
这使得查询本体更容易:
>>> obo.GO_0035065.regulates
[histone acetylation]
但是要小心,因为 GO 不能保证标签从一个版本到另一个版本的守恒性!因此,在设计持久的程序时,应该避免这个提示。
如前所述,可以在属性名前加上前缀"INDIRECT_
,以获得间接定义的限制,例如,从超类继承的限制:
>>> obo.GO_0035065.INDIRECT_regulates
[cellular component organization,
metabolic process,
protein metabolic process,
protein acetylation,
histone acetylation,
...]
4.10 本体的本地目录
Owlready 还可以处理一个或多个包含本地副本的目录。本地副本将被优先使用,而不是从互联网上下载本体。在 Unix/Linux/Mac 下,本地目录必须按照以下方式填入全局变量onto_path
:
>>> onto_path.append("/home/jiba/owlready")
>>> onto = get_ontology("http://lesfleursdunormal.fr/static/↲
_downloads/bacteria.owl#").load()
或者在 Windows 下:
>>> onto_path.append("C:\\owlready")
>>> onto = get_ontology("http://lesfleursdunormal.fr/static/↲
_downloads/bacteria.owl#").load()
全局变量onto_path
包含本地本体目录列表;默认情况下,它是空的。在从互联网上下载一个本体之前,owl 已经检查了在onto_path
中的一个目录中是否没有本地副本。在前面的示例中,如果缓存目录中存在一个“bacteria.owl”文件(在 Linux/Unix/Mac 示例中为/home/jiba/owlready,在 Windows 示例中为“C:\owlready”),将使用该文件。否则,本体将从互联网上下载。
onto_path
的工作类似于sys.path
列表,它允许 Python 找到 Python 模块和包,或者类似于 Java 中的CLASSPATH
环境变量。
此外,可选参数only_local
允许您禁止从互联网加载本体,如下例所示:
>>> onto = get_ontology("http://lesfleursdunormal.fr/static/↲
_downloads/bacteria.owl#").load(only_local = True)
如果你想使用在互联网上找到的不同版本的本体,例如,旧版本或“外来”格式,本地本体目录特别有用。特别是,一些在线可用的本体是 Owlready 无法阅读的格式。使用本地目录,有可能为 Owlready 提供先前翻译成 RDF/XML 或 N-Triples(例如,通过 Protégé手动翻译)的这些本体的版本。
4.11 在 quadstore 中重新加载本体
当使用本地文件(如前所述的本地本体目录或从本地 OWL 文件加载本体)和存储在磁盘上的四元存储时,更新四元存储中的本体的问题出现了。当修改本地 OWL 文件时,必须在 quadstore 中更新本体。这可以通过前面看到的load()
方法的reload
选项来完成,但是也可以通过reload_if_newer
选项来完成,只有当 OWL 文件比存储在 quadstore 中的版本新时,该选项才会重新加载本体:
>>> go = get_ontology("http://purl.obolibrary.org/obo/↲go.owl#")
.load(reload_if_newer = True)
请注意,从 OWL 文件重新加载本体会覆盖存储在 quadstore 中的版本。因此,您必须避免同时修改本体的 OWL 文件及其存储在 quadstore 中的版本!
4.12 示例:从本体创建动态网站
在这个例子中,我们将生成一个动态网站来呈现一个本体的类和个体。为此,我们将使用 Owlready 和 Flask,这是一个 Python 模块,允许您轻松创建网站。Flask 允许您将网站上的 URL 路径与 Python 函数相关联;当请求这个路径时,函数被调用,它必须返回相应的 HTML 页面。路径是通过在函数前一行添加@app.route('/path')
来定义的(这是一个 Python 函数装饰器)。路径可以包含参数(在路径中用尖括号<...>
表示),这些参数将作为参数传递给 Python 函数。
以下函数显示了一个带有 Flask 的网页的简单示例:
from flask import Flask, url_for
app = Flask(__name__)
@app.route(’/path/<parameter>’)
def generate_web_page(parameter):
html = "<html><body>"
html += "The value of the parameter is: %s % parameter"
html += "</body></html>"
return html
我们网站的完整程序如下:
# File dynamic_website.py
from owlready2 import *
onto = get_ontology("bacteria.owl").load()
from flask import Flask, url_for
app = Flask(__name__)
@app.route('/')
def ontology_page():
html = """<html><body>"""
html += """<h2>'%s' ontology</h2>""" % onto.base_iri
html += """<h3>Root classes</h3>"""
for Class in Thing.subclasses():
html += """<p><a href="%s">%s</a></p>""" %↲
(url_for("class_page", iri = Class.iri), Class.name)
html += """</body></html>"""
return html
@app.route('/class/<path:iri>')
def class_page(iri):
Class = IRIS[iri]
html = """<html><body><h2>'%s' class</h2>""" % Class.name
html += """<h3>superclasses</h3>"""
for SuperClass in Class.is_a:
if isinstance(SuperClass, ThingClass):
html += """<p><a href="%s">%s</a></p>""" %↲
(url_for("class_page", iri = SuperClass.iri), SuperClass.name)
else:
html += """<p>%s</p>""" % SuperClass
html += """<h3>equivalent classes</h3>"""
for EquivClass in Class.equivalent_to:
html += """<p>%s</p>""" % EquivClass
html += """<h3>Subclasses</h3>"""
for SubClass in Class.subclasses():
html += """<p><a href="%s">%s</a></p>""" %↲
(url_for("class_page", iri = SubClass.iri), SubClass.name)
html += """<h3>Individuals</h3>"""
for individual in Class.instances():
html += """<p><a href="%s">%s</a></p>""" %↲
(url_for("individual_page", iri = individual.iri),↲ individual.name)
html += """</body></html>"""
return html
@app.route('/individual/<path:iri>')
def individual_page(iri):
individual = IRIS[iri]
html = """<html><body><h2>'%s' individual</h2>""" %↲ individual.name
html += """<h3>Classes</h3>"""
for Class in individual.is_a:
html += """<p><a href="%s">%s</a></p>""" %↲
(url_for("class_page", iri = Class.iri), Class.name)
html += """<h3>Relations</h3>"""
if isinstance(individual, onto.Bacterium):
html += """<p>shape = %s</p>""" % individual.has_shape
html += """<p>grouping = %s</p>""" % individual.has_grouping
if individual.gram_positive == True:
html += """<p>Gram +</p>"""
elif individual.gram_positive == False:
html += """<p>Gram -</p>"""
html += """</body></html>"""
return html
import werkzeug.serving
werkzeug.serving.run_simple("localhost", 5000, app)
在这个程序中,我们定义了三个函数,每个函数都与一个 URL 路径相关联,并且对应于网站上三种不同类型的页面:
-
“本体”页面(对应于网站根的路径“/”)显示本体的 IRI,并列出根类(即,事物的直接子类)。对于每个类,我们在指向相应类页面的互联网链接中显示其名称。这些链接的 URL 是通过 Flask 的
url_for()
函数获得的,该函数根据相应函数的名称和任何参数返回给定网页的 URL。 -
“类”页面(路径“/class/IRI_of_the_class”)显示所请求的类的名称,并列出其超类、等价类、子类和个体。对于超类,当它是一个实体(即 ThingClass 的一个实例)时,我们用一个链接显示超类的名称,或者当它是一个 OWL 逻辑构造函数(例如一个限制)时,简单地显示该类(没有链接)。
-
个人页面(路径“/个人/个人的 IRI”)显示所请求的个人的姓名并列出其类别。如果它是一种细菌,我们也显示它的形状,它的分组,和它的革兰氏状态。
最后两行用于通过 Werkzeug 服务器(Flask 安装的 Python 模块)启动网站。一旦程序被执行,就可以通过浏览器中的地址“http://127.0.0.1:5000
”访问该网站。以下截图显示了动态网站的“本体”和“类”页面:
4.13 总结
在这一章中,你已经学会了如何使用 Owlready 来访问和阅读 Python 中的 OWL 本体。我们已经使用了上一章设计的细菌本体,但是还有一个更大更复杂的资源,基因本体。最后,我们看到了如何在基于 Flask 的动态网站中使用本体。
五、使用 Python 创建和修改本体
在这一章中,我们将看到如何用 Python 创建一个新的本体,以及如何修改或丰富一个已经存在的本体。在前一章中看到的 Owlready 的几乎所有对象、属性和列表都可以修改:当这些的值被修改时,Owlready 自动更新 quadstore 中相应的 RDF 三元组(但是,如果它存储在磁盘上,不要忘记保存它;参见 4.7)。
5.1 创建空的本体
get_ontology()
函数允许你从它的 IRI 创建一个空的本体(最好在 IRI 的末尾标明分隔符," # "或"/",因为 Owlready 不能猜测它,因为本体是空的!):
>>> from owlready2 import *
>>> onto = get_ontology("http://test.org/onto.owl#")
注意,与我们在第四章中所做的相反,我们没有调用load()
方法。它负责加载本体;如果load()
没有被调用,那么本体保持为空。
随后,当创建 OWL 实体或 RDF 三元组时,指出它们放在哪个本体中是很重要的。事实上,与属于创建它们的模块的 Python 类不同,OWL 实体并不特别“属于”一个本体:一个类可以在本体 A 中定义,然后在本体 B 中丰富,例如,用新的父类。
Owlready 使用了语法“with ontology:...”要指示将接收新 RDF 三元组的本体:
with onto:
<Python code>
在代码块“onto
中。
5.2 创建类
要创建 OWL 类,只需创建一个继承自Thing
的 Python 类。例如,我们可以创建细菌、形状和分组类,如下所示:
>>> with onto:
... class Bacterium(Thing): pass
... class Shape(Thing): pass
... class Grouping(Thing): pass
注意,由于这些类是空的(也就是说它们没有方法),我们必须使用关键字pass
(见 2.9)。
为了观察 Owlready quadstore 内部发生了什么,我们可以使用函数set_log_level()
来修改日志记录的级别。通过将级别设置为最大值(9),Owlready 指示在 quadstore 中添加、删除或修改的 RDF 三元组。这里有一个例子:
>>> set_log_level(9)
>>> with onto:
... class TestClass(Thing): pass
* Owlready2 * ADD TRIPLE http://test.org/onto.owl#TestClass↲
http://www.w3.org/1999/02/22-rdf-syntax-ns#type↲
http://www.w3.org/2002/07/owl#Class
* Owlready2 * ADD TRIPLE http://test.org/onto.owl#TestClass↲
http://www.w3.org/2000/01/rdf-schema#subClassOf↲
http://www.w3.org/2002/07/owl#Thing
这里,TestClass 类的创建触发了两个 RDF 三元组的添加:第一个指示 TestClass 是一个 OWL 类,第二个指示 TestClass 从 Thing 继承。
要停止日志记录,我们只需执行以下操作:
>>> set_log_level(0)
我们也可以通过从一个类继承来创建新的 OWL 类,这个类本身从Thing
继承而来,例如,Shape 或 Grouping 类:
>>> with onto:
... class Rod(Shape): pass
... class Isolated(Grouping): pass
... class InPair(Grouping): pass
所创建的类的 IRI 是通过将本体的基本 IRI 与类名连接起来获得的:
>>> Bacterium.iri
'http://test.org/onto.owl#Bacterium'
父类也可以来自另一个本体,例如来自细菌本体的细菌类(它必须是先前从位于当前目录中的 OWL 文件“bacteria.owl”加载的,这里是在本体onto_chap3
变量中;确保您正在运行第三章中使用的工作目录中的示例:
>>> onto_chap3 = get_ontology("bacteria.owl").load()
>>> with onto:
... class MyBacteriumClass(onto_chap3.Bacterium): pass
多重继承是可能的,可以像在 Python 中一样使用。以下示例创建了核酶类,它继承了 RNA 和酶(核酶是一种行为类似于酶的 RNA):
>>> with onto:
... class RNA(Thing): pass
... class Enzyme(Thing): pass
... class Ribozyme(RNA, Enzyme): pass
动态创建类
Python types
模块允许你动态地创建类,当类的名字在编写程序时是未知的,但是在运行时在一个变量中是可用的。这里有一个例子:
>>> import types
>>> class_name = "MyClass"
>>> SuperClasses = [Thing]
>>> with onto:
... NewClass = types.new_class(class_name, tuple↲ (SuperClasses))
请注意,new_class()
函数期望父类的元组,而不是列表!这就是为什么在这个例子中我们使用了tuple()
函数将列表转换成一个元组。
5.3 创建属性
在 Owlready 中,属性类似于类,因为 OWL 属性的行为类似于类(特别是继承支持)。事实上,OWL 属性实际上是“关系的类”。通过定义一个从DataProperty
、ObjectProperty
或AnnotationProperty
继承的类来创建属性。此外,类FunctionalProperty
、InverseFunctionalProperty
、TransitiveProperty
、SymmetricProperty
、AsymmetricProperty
、ReflexiveProperty
和IrreflexiveProperty
可以用作附加的超类(使用多重继承),以便创建函数、反函数、传递和其他属性。
domain
和range
类属性用于以列表的形式查询或定义属性的域和范围。
以下示例创建了函数 functional 属性 has_shape:
>>> with onto:
... class has_shape(ObjectProperty, FunctionalProperty):
... domain = [Bacterium]
... range = [Shape]
对于DataProperty
,可能的范围出现在表 4-1 的右栏。
此外,Owlready 提供了一个简化的符号“domain >> range ”,用于代替父属性(注意:属性的类型,DataProperty
或ObjectProperty
,是从范围中自动推导出来的),例如:
>>> with onto:
... class has_grouping(Bacterium >> Grouping):
... pass
... class has_shape(Bacterium >> Shape, FunctionalProperty):
... pass
... class gram_positive(Bacterium >> bool, FunctionalProperty):
... pass
OWL 还允许您创建子属性,即从另一个属性继承的属性,如下所示:
>>> with onto:
... class has_rare_shape(has_shape): pass
5.4 创建个人
像 Python 中的任何其他实例一样,通过调用类来创建个体:
>>> my_bacterium = Bacterium()
默认情况下,新个体以“本体名称.个体名称”的格式显示,其中本体名称是本体文件名(没有。owl 扩展名),个人名是小写的类名加上一个数字:
>>> my_bacterium
onto.bacterium1
Owlready 自动为个体分配一个新的 IRI,它是通过获取本体的 IRI 而创建的...:或者默认情况下是与该类关联的名称)并添加小写的类名,后跟一个从 1 开始的数字:
>>> my_bacterium.iri
'http://test.org/onto.owl#bacterium1'
注意不要将包含本地个体的 Python 变量的名称(这里是“my_bacterium”)与实体的名称(这里是“bacterium1”)混淆。当从本体访问个体时,必须使用实体的名称,而不是变量的名称,例如onto.bacterium1
。另一方面,在 Python 中直接访问个体时,必须使用他的变量名,因为是 Python 变量,例如my_bacterium
。
>>> my_bacterium is onto.bacterium1
True
通过将个人的名字作为第一个参数传递给类的构造函数,可以指定个人的名字:
>>> my_bacterium = Bacterium("my_bacterium")
>>> my_bacterium
onto.my_bacterium
在创建个体时,还可以使用命名参数提供一个或多个关系的值:
>>> my_bacterium = Bacterium("my_bacterium",
... gram_positive = True,
... has_shape = Rod(),
... has_grouping = [Isolated()] )
这里,我们为属性 has_shape 的值创建了一个新的Rod
类实例,为属性 has_grouping 创建了一个新的Isolated
类实例。对于后者,我们给出了一个列表作为值,因为该属性没有功能。
最后,Owlready 还允许您创建匿名个体(由 RDF 图中的匿名节点表示)。它们是通过传递 0(零)而不是个人姓名(获得的数字是任意的,因此您可能有另一个数字)来创建的:
>>> anonymous_bacterium = Bacterium(0)
>>> anonymous_bacterium
_:52
5.5 修改实体:关系和存在限制
个体和存在限制之间的关系可以像 Python 中的任何其他属性一样进行修改。例如,可以按如下方式修改个人关系:
>>> my_bacterium.gram_positive = True
如果它是类型为ObjectProperty
的属性,则可以创建预期类的新实例(这里是Rod
类):
>>> my_bacterium.has_shape = Rod()
>>> my_bacterium.has_shape
onto.rod1
当属性起作用时,Owlready 需要一个值(如前两行所示)。否则,Owlready 需要一个值列表。然而,Owlready 使用的列表不是“普通的”Python 列表,通过查看这些列表的类并将其与普通的 Python 列表进行比较,我们可以看到:
>>> my_bacterium.has_grouping.__class__
<class 'owlready2.prop.IndividualValueList'>
>>> [].__class__
<class ’list’>
Owlready 的列表是“CallbackList ”,它能够检测列表中元素的添加或删除,以便自动更新 quadstore。因此,可以直接修改这些列表,例如,使用append()
、insert()
或remove()
方法:
>>> my_bacterium.has_grouping = [Isolated()]
>>> my_bacterium.has_grouping.append(InPair())
请注意,Owlready 已经自动将分配给关系的任何列表转换为 CallbackList(例如前面示例中的列表“[Isolated()]
”)。
如前一章(第 4.5.4 节)所见,存在限制和值限制(“一些”和“值”限制)在 Owlready 中作为类属性是可访问的。它不仅适用于阅读,也适用于写作。
例如,我们可以按如下方式更改假单胞菌类的革兰氏状态:
>>> onto_chap3.Pseudomonas.gram_positive = True
并将其恢复如下:
>>> onto_chap3.Pseudomonas.gram_positive = False
我们还将在 6.3 中更详细地讨论类属性。除了存在性限制,Owlready 允许您创建任何类型的 OWL 构造函数,我们将在后面看到(6.1)。
5.6 在名称空间中创建实体
默认情况下,Owlready 在本体名称空间中创建实体,也就是说,实体的 IRI 以本体的 IRI 开始。然而,有时有必要创建其 IRI 不是从本体开始的实体。为此,您需要创建一个名称空间,然后在with
块中使用该名称空间。与我们之前在 4.8 中所做的相反,这里必须从本体创建名称空间,以便 RDF 三元组被添加到给定的本体中。下面的例子在本体onto
中定义了一个类,它的 IRI 是“ http://purl.obolibrary.org/obo/OBOBacterium
”:
>>> obo = onto.get_namespace("http://purl.obolibrary.org/obo/")
>>> with obo:
... class OBOBacterium(Thing): pass
>>> OBOBacterium.iri
'http://purl.obolibrary.org/obo/OBOBacterium'
同样的方法也可以用于个人:
>>> with obo:
... my_bacterium = OBOBacterium("my_bacterium")
>>> my_bacterium.iri
'http://purl.obolibrary.org/obo/my_bacterium'
5.7 重命名实体(重构)
可以修改任何实体的name
和iri
属性来改变实体的 IRI(这种操作有时被称为重构)。修改name
属性允许您更改实体的名称,同时保持它在相同的名称空间中,而修改iri
属性允许您更改名称空间和名称。
>>> my_bacterium.iri = "http://test.org/other_onto.↲owl#bacterium1"
注意,重命名实体会改变它在本体中的名称,但不会改变 Python 变量的名称!在前一行之后,该个体在 Python 变量my_bacterium
中仍然可用。但是,它不再作为onto.my_bacterium
可用,而是可以通过创建相应的名称空间来检索:
>>> get_namespace("http://test.org/other_onto.owl").bacterium1
同样要小心,重命名一个实体并不会将它移动到另一个本体。
5.8 多重定义和远期声明
当用相同的 IRI 定义了几个实体时,Owlready 不会创建新的实体,而是返回已经存在的实体。如有必要,这将被更新,例如,使用新的关系、父类(对于个人)和/或继承(对于类)。在下面的例子中,只创建了一个细菌类个体,因为bacterium_a
和bacterium_b
具有相同的 IRI。然而,第二次创建添加了带有值False
的关系“gram_positive”。
>>> with onto:
... bacterium_a = Bacterium("the_bacterium")
... bacterium_b = Bacterium("the_bacterium",
... gram_positive = False)
>>> bacterium_a is bacterium_b
True
这样,就可以为类或个人进行前向声明。在下面的示例中,首先创建 Bacterium 类,然后在 has_shape 属性域中使用它。然后,细菌类的定义继续添加存在限制“至少具有一种形状”。
>>> with onto:
... class Bacterium(Thing): pass
... class Shape(Thing): pass
... class has_shape(Bacterium >> Shape): pass
... class Bacterium(Thing):
... has_shape = Shape
这里,属性 has_shape 的定义使用了类 Bacterium,而类 Bacterium 的(完整)定义需要属性 has_shape。因此,如果没有前瞻性声明,就不可能实现这一目标。
5.9 销毁实体
全局函数destroy_entity()
允许销毁一个实体(类、个人、财产等)。).
>>> temporary_bacterium = Bacterium()
>>> destroy_entity(temporary_bacterium)
5.10 销毁本体
destroy()
方法允许你永久删除一个本体。这个方法释放了本体在 quadstore 中占据的空间。
>>> onto_temp = get_ontology("http://tmp.org/onto.owl")
>>> onto_temp.destroy()
5.11 保存本体
save()
方法允许在磁盘上保存一个本体:
onto.save(file)
其中 file 可以是文件名或 Python 文件对象。如果没有指定文件,Owlready 将本体保存在onto_path
中的相应目录下(参见 4.10 节)。
可选的format
属性指定了本体文件的格式。目前,owl 已经支持两种文件格式:RDF/XML ( format = rdfxml
)和 N-Triples ( format = ntriples
)。默认情况下,使用 RDF/XML 格式。例如,您可以将一个本体保存为 N-Triples,如下所示:
>>> onto.save("file.owl", format = "ntriples")
5.12 导入本体
要导入一个本体,只需将其添加到目标本体的imported_ontologies
列表中:
>>> onto.imported_ontologies.append(another_onto)
要删除导入,只需用remove()
从列表中删除本体:
>>> onto.imported_ontologies.remove(another_onto)
5.13 同步
当一个多线程程序使用 Owlready 来创建或修改本体时,几个线程可能想同时写入 quadstore,这会导致数据库损坏。注意,即使每个线程写入不同的本体,问题仍然是一样的,因为所有的本体实际上共享同一个 quadstore。在多线程程序的情况下,因此有必要同步写入(相反,读取不需要同步)。
特别是,用 Flask 生成的 web 服务器通常是多线程的(默认情况下,Flask 使用 Werkzeug 服务器,它以多线程模式启动服务器)。在前一章中,我们没有同步问题,因为服务器只读取了本体,而没有修改它。
Owlready 自动管理同步,如下所示。Owlready 自动将写数据库锁定在具有本体的的条目处:...块,并在该块的出口处解锁它。同样,下面的函数和方法也是自动同步的:get_ontology()
、ontology.load()
、ontology.destroy()
和sync_reasoner()
(我们将在下一章看到)。
总之,只要你使用语法“with ontology:……”,你(实际上)不用担心同步!我们将在 7.7 节的动态多线程网站中看到一个同步的例子。
5.14 示例:从 CSV 文件填充本体
填充一个本体包括创建大量的个体(或者可能的类)。这通常是通过外部资源完成的,例如电子表格文件(如 LibreOffice Calc、Excel 等)。).这些电子表格文件可以保存为 CSV 格式,在 Python 中易于阅读。
Python csv
模块使得用 Python 读写 CSV 文件变得很容易。它包含两个类,csv.reader
和csv.writer
,分别用于读和写。每个都将一个打开的 Python 文件对象作为参数。next()
功能允许您从reader
中获取下一行。
在接下来的两节中,我们将看到一个用个体,然后用类来填充细菌本体的例子。
用个人填充
下图显示了描述细菌类个体的 CSV 文件的简单示例。该文件命名为“population _ individuals . CSV”;它看起来是这样的:
当细菌有几个类群时,可以用几行来描述(例如,上图中的细菌“bact3”)。
以下程序用于使用从 CSV 文件中的数据创建的个人来填充本体:
# File population_individuals.py
from owlready2 import *
import csv
onto = get_ontology("bacteria.owl").load()
onto_individuals = get_ontology("http://lesfleursdunormal.fr/↲static/_downloads/bacteria_individuals.owl")
onto_individuals.imported_ontologies.append(onto)
f = open("population_individuals.csv")
reader = csv.reader(f)
next(reader)
with onto_individuals:
for row in reader:
id, gram_positive, shape, grouping, nb_colonies = row
individual = onto.Bacterium(id)
if gram_positive:
if gram_positive == "True": gram_positive = True
else: gram_positive = False
individual.gram_positive = gram_positive
if nb_colonies:
individual.nb_colonies = int(nb_colonies)
if shape:
shape_class = onto[shape]
shape = shape_class()
individual.has_shape = shape
if grouping:
grouping_class = onto[grouping]
grouping = grouping_class()
individual.has_grouping.append(grouping)
onto_individuals.save("bacteria_individuals.owl")
该程序包括以下步骤:
-
导入
owlready
和csv
Python 模块。 -
加载细菌本体。
-
创建一个新的本体来存储个体,命名为“细菌 _ 个体.猫头鹰”。事实上,当内容自动生成时,最好将其存储在一个单独的本体中,以便能够将其与 Protégé中手工生成的内容区分开来。这个新的本体导入了以前的“bacteria.owl”本体。
-
打开 CSV 文件进行读取,用
next()
函数跳过第一行。实际上,第一行包含列标题(“id”、“gram_positive”等)。)但不描述细菌。 -
开始一个“用:……”块,表明所有创建的 RDF 三元组都将保存在新的本体中。
-
对于 CSV 文件中剩余的每一行:
-
对于“形状”和“分组”属性,步骤是相似的。我们在细菌的本体中检索相应的类(语法为“本体[实体的名称]”),并创建一个新的实例。然后我们把这个值赋给个人。对于“grouping”属性,Owlready 中的值是一个列表,因为该属性不起作用,所以我们将新值添加到这个列表中。
-
检查 CSV 文件中是否缺少该值。
-
将该值转换为所需的格式。事实上,从 CSV 文件中提取的所有值都是字符串。在这里,我们将这些值转换为布尔值(表示克数)和整数(表示菌落数)。
-
给个人分配一个值。
-
检索当前行中的标识符、Gram 状态、表单、分组和菌落数。
-
创建具有所需标识符的细菌类个体。请注意,如果已经用相同的标识符创建了一个个体,则返回已经存在的标识符。
-
对于“革兰氏阳性”和“nb 菌落”属性:
-
-
保存新的本体。
随后,如果修改了 CSV 文件,很容易删除“bacteria_individuals.owl”本体,通过再次运行程序重新创建。
用类填充
本体也可以从描述类而不是个体的 CSV 文件中填充。事实上,在生物医学领域,所考虑的实体几乎总是可以细分的:例如,细菌的物种分为亚种,然后是菌株,药物根据剂量,制造商的品牌或批号,疾病根据严重程度,慢性,等等。在这种情况下,通常只使用类来建模一个域。
下图显示了描述细菌子类的 CSV 文件的简单示例:
该文件被命名为“population_classes.csv”。这个文件类似于前面的用于个人的 CSV 文件,但是它有一个新的列“parent”,用于继承。此外,删除了“nb _ colonies”一栏,因为可以对给定的观察结果进行菌落计数,但不能对某一种细菌进行计数。当一个类有几个父类和/或几个分组时,可以用几行来描述它(就像上图中的双歧杆菌类)。
以下程序使用从 CSV 文件中的数据创建的类来填充本体:
# File population_classes.py
from owlready2 import *
import csv, types
onto = get_ontology("bacteria.owl").load()
onto_classes = get_ontology("http://lesfleursdunormal.fr/↲static/_downloads/bacteria_classes.owl")
onto_classes.imported_ontologies.append(onto)
f = open("population_classes.csv")
reader = csv.reader(f)
next(reader)
with onto_classes:
for row in reader:
id, parent, gram_positive, shape, grouping = row
if parent: parent = onto[parent]
else: parent = Thing
Class = types.new_class(id, (parent,))
if gram_positive:
if gram_positive == "True": gram_positive = True
else: gram_positive = False
Class.gram_positive = gram_positive
if shape:
shape_class = onto[shape]
Class.has_shape = shape_class
if grouping:
grouping_class = onto[grouping]
Class.has_grouping.append(grouping_class)
onto_classes.save("bacteria_classes.owl")
该程序遵循与上一个程序相同的结构,但有三点不同:
-
父类的名称出现在“父”行中。类本身是从本体中获取的,默认为
Thing
。 -
新类是使用
types
Python 模块动态创建的,其名称可在变量中找到(见第 5.2.1 节)。如果先前已经创建了具有相同名称的类,则返回相同的类(如果指定了新的父类,则在更新之后)。 -
对于形状和分组属性,我们直接使用该类,而不创建实例。因此,新类的属性被定义为存在限制,在 Owlready 中用作类属性(参见 4.5.4 和 5.5 节)。
我们选择在类中不使用正式的定义(使用“equivalent_to”,正如我们在第三章中对假单胞菌类所做的)。我们可以做出不同的选择,使用等价关系和构造函数来创建定义(我们将在下一章中这样做;参见 6.6)。
为了检查程序是否正常工作,我们可以在 Protégé编辑器中打开用 Python 创建的本体,如下图所示:
5.15 摘要
在这一章中,你已经学习了如何在 Python 中修改现有的本体,以及如何从头开始创建新的本体。我们还讨论了多线程程序中的同步问题。最后,我们看到了如何从简单的 CSV 文件中填充一个本体,可以用任何电子表格软件访问。
六、构造、限制和类属性
在这一章中,我们将看到如何用 Owlready 处理 Python 中所有的 OWL 构造函数。我们还将看到 owl 已经提供的不同的“快捷方式”,以方便构造函数的使用,特别是限制。
6.1 创建构件
OWL 构造器允许你从类、个体和属性中定义逻辑结构(参见 3.4.6 和 3.4.7 节)。
在 Owlready 中,使用语法“property . restriction _ type(value)”创建限制,对限制类型使用与 Protected:
-
property.some(Class)
对于存在性限制 -
property.only(Class)
对于普遍的限制 -
property.value(individual or data)
用于值限制(也称为角色填充 -
property.exactly(cardinality, Class)
对于精确的基数限制 -
property.min(cardinality, Class)
和property.max(cardinality, Class)
分别用于最小和最大基数限制
逻辑运算符 NOT(补码)、AND(交集)和 OR(并集)按如下方式获得:
-
Not(Class)
-
And([Class1, Class2,...])
或Class1 & Class2 & ...
-
Or([Class1, Class2,...])
或Class1 | Class2 | ...
如下获得一组个体:
OneOf([individual1, individual2,...])
属性的倒数按如下方式获得:
Inverse(Property)
属性链(也称为组合)按如下方式获得:
PropertyChain([Property1, Property2,...])
在前面的定义中,类可以是实体,也可以是其他构造函数。因此,构造函数可以相互嵌套。
构造函数可以用在类的is_a
和equivalent_to
属性中。例如,我们可以创建Coccus
类(它将圆形细菌分组;参见 3.3),完全用 Python 编写,如下所示:
>>> from owlready2 import *
>>> onto = get_ontology("bacteria.owl").load()
>>> with onto:
... class Coccus(onto.Bacterium):
... equivalent_to = [
... onto.Bacterium & onto.has_shape.some(onto.Round)
... & onto.has_shape.only(onto.Round)
... ]
类似地,我们可以用四个限制来定义假单胞菌类:
>>> with onto:
... class Pseudomonas(onto.Bacterium):
... is_a = [
... onto.has_shape.some(onto.Rod),
... onto.has_shape.only(onto.Rod),
... onto.has_grouping.some(onto.Isolated |↲onto.InPair),
... onto.gram_positive.value(False)
... ]
Owlready 将自动完成用 Python 中声明为父类的类(这里是Bacterium
类)创建的类的is_a
列表。我们可以验证如下:
>>> Pseudomonas.is_a
[bacteria.Bacterium,
bacteria.has_shape.some(bacteria.Rod),
bacteria.has_shape.only(bacteria.Rod),
bacteria.has_grouping.some(bacteria.Isolated | bacteria.InPair),
bacteria.gram_positive.value(False)]
6.2 访问构造参数
以下属性提供对主构造函数中包含的信息的访问(有关构造函数及其属性的完整列表,请参考《参考手册》附录 C 中的 C.6):
-
逻辑运算符 AND 和 OR(交集和并集,分别为类
And
和Or
):- 属性
Classes
:交集或并集相关的类的列表
- 属性
-
限制(类别
Restriction
):-
属性
property
:与限制相关的属性 -
属性
type
:限制类型(从常量SOME
、ONLY
、VALUE
、MAX
、MIN
和EXACTLY
中选择的值) -
属性
value
:与限制相关的值(类型SOME
、ONLY
、MAX
、MIN
和EXACTLY
的类,个体或类型VALUE
的值) -
属性
cardinality
:关系的数量(仅适用于MAX
、MIN
和EXACTLY
限制)
-
例如,如果我们采用类链球菌及其等效定义,我们可以在 Python 中对其进行如下分析:
>>> onto.Streptococcus.equivalent_to[0]
bacteria.Bacterium
& bacteria.has_shape.some(bacteria.Round)
& bacteria.has_shape.only(bacteria.Round)
& bacteria.has_grouping.some(bacteria.InSmallChain)
& bacteria.has_grouping.only(Not(bacteria.Isolated))
& bacteria.gram_positive.value(True)
>>> onto.Streptococcus.equivalent_to[0].Classes[1]
bacteria.has_shape.some(bacteria.Round)
>>> onto.Streptococcus.equivalent_to[0].Classes[1].property
bacteria.has_shape
>>> onto.Streptococcus.equivalent_to[0].Classes[1].type == SOME
True
>>> onto.Streptococcus.equivalent_to[0].Classes[1].value
bacteria.Round
如果我们不知道所使用的构造函数的类别,那么isinstance()
Python 函数允许我们测试它,例如:
>>> constructor = onto.Streptococcus.equivalent_to[0]
>>> if isinstance(constructor, And):
... print("And", constructor.Classes)
... elif isinstance(constructor, Or):
... print("Or", constructor.Classes)
... elif isinstance(constructor, Restriction):
... print("Restriction", constructor.property,↲ constructor.type, constructor.value)
And [bacteria.Bacterium,
bacteria.has_shape.some(bacteria.Round),
bacteria.has_shape.only(bacteria.Round),
bacteria.has_grouping.some(bacteria.InSmallChain),
bacteria.has_grouping.only(Not(bacteria.Isolated)),
bacteria.gram_positive.value(True)]
此外,这里列出的属性都是可修改的:当属性被修改时,Owlready 会自动更新 quadstore。例如,我们可以更改链球菌类别的革兰氏状态限制,如下所示:
>>> onto.Streptococcus.equivalent_to[0].Classes[-1].value = False
6.3 作为类别属性的限制
Owlready 提供了对所有 OWL 构造函数的访问,正如我们在前面两节中看到的。然而,创建构造函数或访问其中包含的信息通常是复杂而乏味的。这就是为什么 Owlready 也提供了几个“快捷方式”来方便构造函数的使用。在 4.5.4 中,我们已经看到了一个访问存在限制的快捷方式的例子,就好像它们是类属性一样。
事实上,限制经常被用来表示类之间的关系。两个类之间的关系比两个个体之间的关系更复杂:在两个个体之间,关系要么存在(对应于 quadstore 中的三元组),要么不存在。相反,一个类集合了几个个体,这就导致了几个场景:
-
第一类中的所有个体都与第二类中的至少一个个体相关联:这是存在性限制(“被保护者中的一些”)。
-
第一类的所有个人只与第二类的个人有关系:这是普遍的限制(“只”在被保护者中)。
-
第一个类中的每个个体都与第二个类中的每个个体相关联:OWL 不直接允许在类之间创建这种类型的关系。然而,通过具体化属性,也就是说,通过将其转换成与两个属性相关联的类,可以获得等效的结果。
Owlready 允许将两个类之间的关系转换成类属性,反之亦然。这使您可以轻松地创建或读取对应于以下形式的构造函数:
-
(属性某类)
-
(个人财产价值)
-
(仅属性(类或...))
-
(仅财产({个人,...}))
-
(仅属性(类或...或者{个人,...}))
特殊注释“class_property_type
”表示给定属性使用哪种类型的限制。可能的值有
-
["some"]
:使用该值时,类属性对应于存在性限制(“一些”)。如果未指定注释“class_property_type
”,这是属性的默认值。 -
["only"]
:使用该值时,类属性对应通用限制(“only”)。 -
["some, only"]
:使用该值时,类属性对应于存在性和普遍性限制。 -
["relation"]
:这个值使用 RDF 三元组创建类之间的直接关系。请注意,这些直接关系在 OWL 中是无效的,推理器不会考虑它们。然而,许多 RDF 图数据库使用类之间的直接关系;这个值使得读取或产生这样的数据库成为可能。这些 RDF 数据库缺乏形式语义,因此不是 OWL 本体。
我们可以使用这些类属性来更容易地定义第三章中细菌本体中的细菌类。我们将从修改三个属性开始,gram_positive
、has_shape
和has_grouping
,以便指定与每个属性相关的类属性的类型。我们选择保持与第三章相同的建模选择:我们将对gram_positive
和has_grouping
使用存在性限制,对has_shape
使用存在性和普遍性限制:
>>> with onto:
... onto.gram_positive.class_property_type = ["some"]
... onto.has_shape.class_property_type = ["some, only"]
... onto.has_grouping.class_property_type = ["some"]
然后,我们可以用比以前更简单的方式创建一个新的假单胞菌类,称为Pseudomonas2
(见 6.1),只需定义类属性的值:
>>> with onto:
... class Pseudomonas2(onto.Bacterium): pass
... Pseudomonas2.gram_positive = False
... Pseudomonas2.has_shape = onto.Rod
... Pseudomonas2.has_grouping = [onto.Isolated | onto.InPair]
下面的语法是等效的,但是更简单。它包括在类的主体中定义类属性(参见 2.9 Python 中 class 语句的语法和类属性的使用):
>>> with onto:
... class Pseudomonas3(onto.Bacterium):
... gram_positive = False
... has_shape = onto.Rod
... has_grouping = [onto.Isolated | onto.InPair]
然后,我们可以验证是否已经按照预期创建了限制:
>>> Pseudomonas3.is_a
[bacteria.Bacterium,
bacteria.has_shape.some(bacteria.Rod),
bacteria.has_shape.only(bacteria.Rod),
bacteria.has_grouping.some(bacteria.Isolated | bacteria.InPair),
bacteria.gram_positive.value(False)]
获得的假单胞菌 3 类与这里使用构造函数定义的类相同(见 6.1)。
我们还可以使用类别属性创建新的细菌类别:
>>> with onto:
... class Listeria(onto.Bacterium):
... gram_positive = True
... has_shape = onto.Rod
... has_grouping = [onto.InLongChain]
>>> Listeria.is_a
[bacteria.Bacterium,
bacteria.has_shape.some(bacteria.Rod),
bacteria.has_shape.only(bacteria.Rod),
bacteria.has_grouping.some(bacteria.InLongChain),
bacteria.gram_positive.value(True)]
可以更改类及其类属性;Owlready 将自动更新 quadstore 中的限制。在下面的示例中,我们向 Listeria 类添加了一个分组:
>>> Listeria.has_grouping.append(onto.Isolated)
>>> Listeria.is_a
[bacteria.Bacterium,
bacteria.has_shape.some(bacteria.Rod),
bacteria.has_shape.only(bacteria.Rod),
bacteria.has_grouping.some(bacteria.InLongChain),
bacteria.has_grouping.some(bacteria.Isolated),
bacteria.gram_positive.value(True)]
类属性也用于“读取”,即分析类的构造函数,即使它们不是通过 Owlready 类属性创建的。例如,如果我们像这样创建Listeria2
类,使用构造函数:
>>> with onto:
... class Listeria2(onto.Bacterium):
... is_a = [onto.has_shape.some(onto.Rod),
... onto.has_shape.only(onto.Rod),
... onto.has_grouping.some(onto.InLongChain),
... onto.gram_positive.value(True)]
我们仍然可以使用类属性来分析构造函数(或者修改它们):
>>> Listeria2.has_grouping
[bacteria.InLongChain]
这解释了为什么我们能够使用第四章中的类属性来访问外部本体。
下表总结了 Owlready 支持的类属性类型及其生成的构造函数。在这个表中,C
、C1
、C2
是类,i1
、i2
是个体,p
是属性。
p.class_property_type
|
翻译< <
C.p = [C1, C2,..., i1, i2,...]
>
|
| --- | --- |
| ["some"]
| C subclassof: p some C1``C subclassof: p some C2``...``C subclassof: p value i1``C subclassof: p value i2``...
|
| ["only"]
| C subclassof: p only (C1 or C2... or {i1, i2...})
|
| ["some, only"]
| C subclassof: p some C1``C subclassof: p some C2``...``C subclassof: p value i1``C subclassof: p value i2``...``C subclassof: p only (C1 or C2... or {i1, i2...})
|
| ["relation"]
| 断言以下 RDF 三元组:(C, p, C1)``(C, p, C2)``...``(C, p, i1)``(C, p, i2)``...
|
6.4 定义的类别
Owlready 还允许您使用类属性创建已定义的类,定义如下:
- Parent_class1 和 Parent_class2...
和(属性某些类)...
和(个人财产价值)...
和(仅属性(类...或者{个人,...}))
为此,Owlready 使用特殊的布尔注释“defined_class
”来表示一个类已被定义。如果这个注释对于一个类是True
,Owlready 将从类属性中生成一个定义,而不是上一节中看到的限制。该注释的默认值为False
。
以下示例创建了一个新定义的细菌类别:
>>> with onto:
... class Corynebacterium(onto.Bacterium):
... defined_class = True
... gram_positive = False
... has_shape = onto.Rod
... has_grouping = [onto.InCluster]
请注意类体的第一行“defined_class = True
”,这表明它是一个已定义的类。
我们可以验证定义已经创建:
>>> Corynebacterium.equivalent_to
[bacteria.Bacterium
& bacteria.gram_positive.value(False)
& bacteria.has_shape.some(bacteria.Rod)
& bacteria.has_shape.only(bacteria.Rod)
& bacteria.has_grouping.some(bacteria.InCluster)]
另一方面,Owlready 没有创建一个简单的限制(也就是说,除了定义中存在的那些限制之外):
>>> Corynebacterium.is_a
[bacteria.Bacterium]
和以前一样,可以修改类属性,Owlready 将自动更新定义。类似地,类的属性也允许访问定义中的信息,即使它不是用 Owlready 创建的。
当创建定义时,Owlready 将类属性的不同值组合成一个定义。通常,如果C
、P1
、P2
、S1
、S2
、O1
和O2
是类,s
是使用存在性限制的属性,o
是使用普遍性限制的属性,s1
、s2
、o1
和o2
是个体,当我们定义:
C.is_a = [P1, P2,...]
C.s = [S1, S2,..., s1, s2,...]
C.o = [O1, O2,..., o1, o2,...]
Owlready 将生成以下定义:
C equivalent_to P1 and P2...
and (s some S1) and (s some S2)...
and (s value s1) and (s value s2)...
and (o only (O1 or O2... or {o1, o2...}))
6.5 示例:用 Python 创建细菌本体
下面的程序以举例说明的方式给出,它可以使用构造函数,完全用 Python 从头开始重新创建第三章的细菌本体。用 Python 创建本体可能比用 Protégé更费力,但它也有优点:特别是,它可以复制和粘贴定义,这允许您快速创建相似的类。
# File create_onto.py
from owlready2 import *
onto = get_ontology("http://lesfleursdunormal.fr/static/↲
_downloads/bacteria.owl#")
with onto:
class Shape(Thing): pass
class Round(Shape): pass
class Rod(Shape): pass
AllDisjoint([Round, Rod])
class Grouping(Thing): pass
class Isolated(Grouping): pass
class InPair(Grouping): pass
class InCluster(Grouping): pass
class InChain(Grouping): pass
class InSmallChain(InChain): pass
class InLongChain(InChain): pass
AllDisjoint([Isolated, InPair, InCluster, InChain])
AllDisjoint([InSmallChain, InLongChain])
class Bacterium(Thing): pass
AllDisjoint([Bacterium, Shape, Grouping])
class gram_positive(Bacterium >> bool, FunctionalProperty): pass
class nb_colonies(Bacterium >> int, FunctionalProperty): pass
class has_shape(Bacterium >> Shape, FunctionalProperty): pass
class has_grouping(Bacterium >> Grouping): pass
class is_shape_of(Shape >> Bacterium):
inverse = has_shape
class is_grouping_of(Grouping >> Bacterium):
inverse = has_grouping
class Pseudomonas(Bacterium):
is_a = [ has_shape.some(Rod),
has_shape.only(Rod),
has_grouping.some(Isolated | InPair),
gram_positive.value(False) ]
class Coccus(Bacterium):
equivalent_to = [ Bacterium
& has_shape.some(Round)
& has_shape.only(Round) ]
class Bacillus(Bacterium):
equivalent_to = [ Bacterium
& has_shape.some(Rod)
& has_shape.only(Rod) ]
class Staphylococcus(Coccus):
equivalent_to = [ Bacterium
& has_shape.some(Round)
& has_shape.only(Round)
& has_grouping.some(InCluster)
& gram_positive.value(True) ]
class Streptococcus(Coccus):
equivalent_to = [ Bacterium
& has_shape.some(Round)
& has_shape.only(Round)
& has_grouping.some(InSmallChain)
& has_grouping.only( Not(Isolated) )
& gram_positive.value(True) ]
unknown_bacterium = Bacterium(
"unknown_bacterium",
has_shape = Round(),
has_grouping = [ InCluster("in_cluster1") ],
gram_positive = True,
nb_colonies = 6
)
onto.save("bacteria.owl")
6.6 示例:用已定义的类填充本体
在这个例子中,我们将继续细菌本体的种群。这一次,我们将用类填充本体(如 5.14.2 中一样),但是使用具有等价关系的定义。我们将像以前一样重用相同的 CSV 文件(称为“population_classes.csv”):
我们可以用两种方式填充本体:要么使用类属性(这是最简单的选择),要么使用构造函数(这更复杂,但是如果您想要创建比 Owlready 生成的定义更复杂的定义,这可能是必要的)。
6.6.1 使用类属性填充
下面的程序使用类属性从前面的 CSV 文件中的数据用定义的类来填充细菌的本体:
# File population_defined_classes1.py
from owlready2 import *
import csv, types
onto = get_ontology("bacteria.owl").load()
onto.gram_positive.class_property_type = ["some"]
onto.has_shape.class_property_type = ["some", "only"]
onto.has_grouping.class_property_type = ["some"]
onto_classes = get_ontology("http://lesfleursdunormal.fr/↲static/_downloads/bacteria_defined_classes.owl")
onto_classes.imported_ontologies.append(onto)
f = open("population_classes.csv")
reader = csv.reader(f)
next(reader)
with onto_classes:
for row in reader:
id, parent, gram_positive, shape, grouping = row
if parent: parent = onto[parent]
else: parent = Thing
Class = types.new_class(id, (parent,))
Class.defined_class = True
if gram_positive:
if gram_positive == "True": gram_positive = True
else: gram_positive = False
Class.gram_positive = gram_positive
if shape:
shape_class = onto[shape]
Class.has_shape = shape_class
if grouping:
grouping_class = onto[grouping]
Class.has_grouping.append(grouping_class)
onto_classes.save("bacteria_defined_classes.owl")
这个程序与我们在上一章看到的创建未定义类的程序非常相似(见 5.14.2)。只有两点不同:
-
在程序的开始,对于三个属性中的每一个,我们指出了相关类属性的类型。
-
当我们创建一个类时,我们指出它是一个已定义的类(带有“Class.defined_class = True”)。
为了验证程序执行后的正常运行,我们可以在 Protégé编辑器中打开用 Python 创建的本体,如下面的屏幕截图所示:
使用构造填充
下面的程序还从 CSV 文件中的数据用定义的类填充细菌的本体。与前一个不同,它不使用类属性,而是直接创建构造函数。这第二个程序比上一个更复杂,可见对 Owlready 的类属性的兴趣!
# File population_defined_classes2.py
from owlready2 import *
import csv, types
onto = get_ontology("bacteria.owl").load()
onto_classes = get_ontology("http://lesfleursdunormal.fr/↲static/
_downloads/bacteria_defined_classes.owl")
onto_classes.imported_ontologies.append(onto)
f = open("population_classes.csv")
reader = csv.reader(f)
next(reader)
id_2_parents = defaultdict(list)
id_2_gram_positive = {}
id_2_shape = {}
id_2_groupings = defaultdict(list)
for row in reader:
id, parent, gram_positive, shape, grouping = row
if parent:
id_2_parents[id].append(onto[parent])
if gram_positive
:
if gram_positive == "True": gram_positive = True
else: gram_positive = False
id_2_gram_positive[id] = gram_positive
if shape:
shape_class = onto[shape]
id_2_shape[id] = shape_class
if grouping:
grouping_class = onto[grouping]
id_2_groupings[id].append(grouping_class)
with onto_classes:
for id in id_2_parents:
if id_2_parents[id]:
Class = types.new_class(id,↲tuple(id_2_parents[id]))
else:
Class = types.new_class(id, (Thing,))
conditions = []
if id in id_2_gram_positive:
conditions.append(onto.gram_positive.value(↲
id_2_gram_positive[id]))
if id in id_2_shape
:
conditions.append(onto.has_shape.some↲(id_2_shape[id]))
conditions.append(onto.has_shape.only↲(id_2_shape[id]))
for grouping in id_2_groupings[id]:
conditions.append(onto.has_grouping.some(grouping))
if len(conditions) == 1:
Class.equivalent_to.append(conditions[0])
elif len(conditions) > 1:
Class.equivalent_to.append( And(conditions) )
onto_classes.save("bacteria_defined_classes.owl")
该计划有两个部分。第一部分读取整个 CSV 文件并将所有数据存储在字典中,第二部分创建类和等价关系。事实上,等价关系必须“整体”定义(如我们在 3.4.8 中所见)。因此,有必要了解一个类的所有信息,以便能够创建它的定义。然而,在我们的 CSV 文件中,一个类可以在几行中定义(例如,这里的类双歧杆菌)。在这种情况下,我们不能在只读完第一行之后创建定义。
第一部分使用标准字典和defaultdict
,即带有默认值的字典(见 2.4.6),以简化程序。这个字典自动创建缺失的条目,并用空列表的值初始化它们。
程序的第二部分遍历id_2_parents
字典中的所有类标识符。对于每个标识符,我们创建一个从父类继承的类,或者,如果失败,从Thing
继承。接下来,我们创建一个名为conditions
的条件列表,最初为空。然后,我们查看字典中属性gram_positive
、has_shape
和has_grouping
的可用值,并在列表条件中添加相应的限制:
-
对于
gram_positive
属性,我们使用了一个值限制,因为它是一个数据属性。 -
对于
has_form
属性,我们使用了两个限制,一个存在限制和一个普遍限制,就像我们在第三章中所做的那样。 -
对于
has_grouping
属性,我们使用了一个单一的、存在的限制,以便为其他分组的可能性留有余地。
最后,我们在Class.equivalent_to
中添加一个等价关系。如果conditions
列表只包含一个条件,我们将这个唯一的条件添加到Class.equivalent_to
中。如果conditions
列表包含条件,我们用操作符And()
执行这些条件的交集,然后我们将这个交集添加到Class.equivalent_to
。这意味着当几个条件都存在时,它们都必须满足。
这个程序创建的本体相当于前一个程序创建的本体。
6.7 总结
在这一章中,你已经学习了如何处理 OWL 结构和类限制,这是 OWL 的一个主要特性。我们还介绍了 owl 已经提出的各种快捷方式,比如类属性或定义类。最后,我们看到了如何从 CSV 文件创建定义的类,以及如何完全用 Python 创建第三章的细菌本体。你现在可以用 Python 创建几乎所有可以用 Protégé创建的东西。
七、自动推理
在这一章中,我们将看到如何在 Python 中使用 HermiT 和 Pellet 推理机,以便检查本体的一致性,并基于逻辑构造器执行自动推理和分类。
7.1 分离
Owlready 允许使用 AllDisjoint 类在类之间创建分离。例如,我们可以将类 Isolated、InPair、InCluster 和 InChain 声明为成对分离,如下所示:
>>> from owlready2 import *
>>> onto = get_ontology("bacteria.owl").load()
>>> AllDisjoint([onto.Isolated, onto.InPair, onto.InCluster,
... onto.InChain])
注意,Owlready 和 Protégé一样,不区分两个实体之间的分离和几个实体之间的成对分离,这与 OWL 不同。Owlready 会根据不相交中涉及的实体数量自动选择正确的 OWL 方法。此外,AllDisjoint 还可以处理属性列表(不相交的属性)或个体列表(不同的个体)。
使用disjoints()
方法可以找到一个类所属的所有不相交部分。它返回一个生成器来列出涉及给定实体的所有 AllDisjoints。然后,AllDisjoint 的entities
属性使得获得声明为 Disjoint 的实体列表成为可能。
7.2 用开放世界假设进行推理
sync_reasoner()
功能允许您运行推理器,并自动将推导出的事实应用到 quadstore。默认情况下,使用隐士推理器。sync_reasoner_pellet()
和sync_reasoner_hermit()
函数用于指定推理机;Pellet 和隐士在 Owlready 中的工作方式是一样的。
注意:HermiT 和 Pellet 是 Java 程序,所以您需要安装一个 Java 虚拟机来使用它们。如果你没有 Java,你可以从 www.java.com/
(对于 Windows 和 Mac OS)或者从你的 Linux 发行版的包(对于 Java Runtime Environment 和 Java Development Kit,这些包通常被命名为“jre”或“jdk”)安装它。另一方面,推理器本身已经准备好了。如果 Java 安装在非标准目录中(尤其是在 Windows 上),可以按如下方式输入 Java 的路径(用您的路径替换):
import owlready2
owlready2.JAVA_EXE = "C:\\Program Files\\Java\\jre8\\bin\\↲java.exe"
举个例子,我们以细菌的本体为例,从检查个体“未知 _ 细菌”所属的类开始:
>>> onto.unknown_bacterium.__class__
bacteria.Bacterium
然后我们运行推理器:
>>> sync_reasoner()
* Owlready2 * Running HermiT...
java [...]
* Owlready2 * HermiT took 0.5354642868041992 seconds
* Owlready * Reparenting bacteria.unknown_bacterium [...]
默认情况下,Owlready 会显示推理器的命令行和执行的重新分类(参数debug = 0
可以避免显示)。
这里,我们注意到,单个“未知细菌”已被重新分类为葡萄球菌类,就像它在被保护生物中一样:
>>> onto.unknown_bacterium.__class__
bacteria.Staphylococcus
推理机推导出的事实默认存储在本体“http://inferrences
/”中。可以通过使用“with:...”将它们放在另一个本体中块,如下例所示:
>>> onto_inferences = get_ontology("http://lesfleursdunormal.↲fr/
static/_downloads/bacteria_inferences.owl#")
>>> with onto_inferences:
... sync_reasoner()
这个本体包含了推论;然后可以保存(见 5.11):
>>> onto_inferences.save("bacteria_inferences.owl")
推理因此可以从本体“细菌 _ 推理. owl”加载。这将避免再次调用推理器,从而节省时间。
Owlready 还允许您通过向sync_reasoner()
函数传递一个本体列表,将推理限制到某些本体:
>>> sync_reasoner([onto])
最后,可选参数infer_property_values
和infer_data_property_values
(仅由颗粒推理器支持)使得推断个体的属性值成为可能(对于两个选项,分别为对象属性和数据属性):
>>> sync_reasoner(infer_property_values = True)
>>> sync_reasoner_pellet(infer_data_property_values = True)
这两个选项也可以同时使用:
>>> sync_reasoner_pellet(infer_property_values = True,
... infer_data_property_values = True)
7.3 封闭世界或局部封闭世界中的推理
OWL 推理器根据开放世界假设运行:任何不被禁止的事情都被认为是可能的(见 3.5 节)。然而,通常希望将推理局限于已知的事实,对于整个本体或者仅仅对于某些实体。这被称为“在一个封闭的世界中”的推理,或者有时被称为“否定即失败”:也就是说,没有明确知道的一切都被认为是假的。
在 Owlready 中,close_world()
函数允许“封闭世界”,并将推理限制于本体中存在的事实,对于个人、类或在争论中传递的本体。这个函数以构造函数的形式自动添加必要的约束。当所有的本体都是封闭的时候,我们说“在封闭的世界中推理”,当只有一些实体是封闭的时候,我们说“在局部封闭的世界中推理”。
我们已经在第三章 3 中遇到了链球菌类的“开放或封闭世界”的问题(见 3.5 节末尾的第 2 点):一个圆形细菌,成链分组,革兰氏染色阳性,被归类为球菌,但不被归类为链球菌,因为推理者无法证明没有其他分组(未知,因此在本体中不存在)是分离的类型。
在以下示例中,我们创建了符合上述标准的 unknown_bacterium2:
>>> with onto:
... unknown_bacterium2 = onto.Bacterium(
... gram_positive = True,
... has_shape = onto.Round(),
... has_grouping = [onto.InSmallChain()] )
由于前面解释的原因,当推理机被执行时,细菌被重新分类到球菌类中,而不是链球菌类中:
>>> sync_reasoner()
>>> unknown_bacterium2.__class__
bacteria.Coccus
事实上,尽管这种细菌只有一个小的链群,推理者在一个开放的世界中工作。他假设可能存在另一种类型的分组,但这是未知的(has_grouping 属性不起作用;因此,对于同一个人,它可以有几个值)。然而,我们将链球菌类定义为没有独立的类群。在这里,未知细菌 2 没有一个独立的类群,但是我们可以假设这样一个类群是存在的。
要禁止这个假设,解决我们的问题,就必须“封闭世界”,也就是说,当我们说这个细菌在一个小链条上有一个分组的时候,它必然有这个分组,除了这个之外,它没有别的分组。close_world()
函数实现了这一点,如下例所示:
>>> close_world(unknown_bacterium2)
>>> unknown_bacterium2.is_a
[bacteria.Coccus,
bacteria.has_grouping.only(OneOf([bacteria.insmallchain1]))]
我们注意到close_world()
给个体增加了一个普遍的限制(即 only ):这个限制表明该细菌只有 SmallChain1 中的分组。注意close_world()
没有为属性“gram_positive”和“has_shape”添加任何限制,因为它们是功能性的:每个属性只能有一个值,因此不需要关闭。
现在,如果我们再次运行推理器,我们会发现这种细菌被很好地归类为链球菌:
>>> sync_reasoner()
>>> unknown_bacterium2.__class__
bacteria.Streptococcus
类似地,close_world()
函数可以用来关闭整个类或本体。该函数的完整语法如下:
close_world(entity, Properties = None, close_instance_list =↲ True, recursive = True)
entity
是封闭世界中要考虑的实体。Properties
是要考虑的属性列表(如果值是None
,这是默认值,所有属性都将被关闭)。如果close_instance_list
是True
(缺省值),封闭类(或多个类)的实例被限制为在本体中断言的实例。当要关闭的实体是一个类时,如果 recursive 是True
(默认值),那么close_world()
将递归应用于所有的子类。
7.4 不一致的类和不一致的本体
在推理过程中,推理器可能会检测到不一致的类。这些类别是不合逻辑的,因此,不应该有个人属于这些类别。例如,在我们的细菌本体中,从链球菌类继承并与杆状形式( via a restriction)相关联的以下类将是不一致的:
>>> with onto:
... class RodStreptococcus(onto.Streptococcus):
... is_a = [onto.has_shape.some(onto.Rod)]
只要没有属于这个阶层的个体,这就不是问题。推理器将不一致的类重新分类为等价于Nothing
。因此,可以通过测试Nothing
是否是等价类来测试 Python 中的类是否不一致,例如,为了检查 RodStreptococcus 类是否一致,我们可以这样做:
>>> sync_reasoner()
>>> if Nothing in RodStreptococcus.equivalent_to:
... print("The class is inconsistent!")
... else:
... print("The class is consistent.")
The class is inconsistent!
此外,default_world.inconsistent_classes()
方法提供了一个生成器来迭代所有不一致的类。
相反,当存在至少一个属于不一致类的个体时,整个本体变得不一致。一个不一致的本体包含一个逻辑问题,使它变得荒谬。任何关于本体论的推理都变得不可能。注意不要混淆不一致的类和不一致的本体!在第一种情况下,这并不妨碍推理者做他的工作,而在第二种情况下,推理变得不可能。
在本体不一致的情况下,sync_reasoner()
函数引发异常OwlReadyInconsistentOntologyError
。让我们继续前面的示例,创建一个 RodStreptococcus 类的个体:
>>> rod_strepto = onto.RodStreptococcus()
>>> sync_reasoner()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/jiba/src/owlready2/reasoning.py", line 120,↲
in sync_reasoner_hermit
raise OwlReadyInconsistentOntologyError()
owlready2.base.OwlReadyInconsistentOntologyError
这个异常可以在 Python 中捕获,如下所示:
>>> try:
... sync_reasoner()
... print("Ok, the ontology is consistent.")
... except OwlReadyInconsistentOntologyError:
... print("The ontology is inconsistent!")
The ontology is inconsistent!
随着“rod_strepto”个体的加入,细菌的本体变得不一致。按照现在的情况,我们不能再对 quadstore 进行推理,直到这种不一致性得到解决!这就是为什么我们要删除细菌的本体(添加了“rod_strepto”进行修改),以便能够在本章的剩余部分继续使用推理器。
>>> onto.destroy()
然后,我们可以在接下来的章节中从 OWL 文件中重新加载细菌本体(因此没有“rod_strepto”个体)。
7.5 对数字和字符串的限制和推理
ConstrainedDatatype
类用于根据一个或多个方面创建受约束的数据类型,例如,正整数或至少三个字符的字符串。语法如下:
ConstrainedDatatype(base_datatype, facet1 = value1, facet2 = value2,...)
base_datatype
是初始数据类型,例如:int
、float
、bool
、str
、norm_str
等等(见表 4-1 )。
以下方面由 XMLSchema 提出,并得到 Owlready 的支持:
-
对于数值类型:
-
max_inclusive
:最大值,包括在内(该值必须小于或等于给定值) -
max_exclusive
:最大值,不包含(该值必须严格小于给定值) -
min_inclusive
:最小值,包括在内(该值必须大于或等于给定值) -
min_exclusive
:最小值,不包含(该值必须严格高于给定值) -
total_digits
:存在的位数 -
fraction_digits
:小数点后的位数
-
-
对于字符串:
-
length
:准确的字符数 -
min_length
:最小字符数 -
max_length
:最大字符数 -
pattern
:约束字符串可能值的正则表达式 -
white_space
:表示如何处理空白,有三个可能的值:* preserve
:空格按原样保留。* replace
:所有空格(空格、制表、换行等。)被空格替换。* collapse
: As replace,但是链的开头/结尾的空格被删除,连续的多个空格被替换为一个空格。
-
请注意 Owlready 允许刻面的定义,但是不考虑它们:例如,即使使用了white_space
,空格也不会被替换。另一方面,其他工具可以考虑到这一点(包括推理器)。
例如,对于范围 0,20 内的整数,我们将:
>>> ConstrainedDatatype(int, min_inclusive = 0, max_inclusive = 20)
这些受约束的数据类型可以用在 OWL 限制中(一些和仅),特别是表示“属性值大于/小于 X”形式的范围或限制。在这种情况下,Owlready 还提供了一个缩写符号“property > value”(或> =、<、< =),它允许自动创建一个具有适当的受约束数据类型的存在性限制(“some”)。
下面的例子用一个Person
类和两个age
和size
数字属性创建了一个本体。然后,我们创建了Elderly
和TallPerson
类,它们是使用对带有受约束数据类型的age
和size
属性的限制来定义的(对于第二个限制,我们使用了一个简化的符号来代替size.some (ConstrainedDatatype (int, max_inclusive = 1.8))
)。
>>> onto_person = get_ontology("http://test.org/person.owl#")
>>> with onto_person:
... class Person(Thing): pass
... class age (Person >> int , FunctionalProperty): pass
... class size(Person >> float, FunctionalProperty): pass
... class Elderly(Person):
... equivalent_to = [
... Person & age.some(ConstrainedDatatype(int,
... min_inclusive = 65))
... ]
... class TallPerson(Person):
... equivalent_to = [
... Person & (size >= 1.8) # Shortened notation
... ]
...
... p1 = Person(age = 25, size = 2.0)
... p2 = Person(age = 39, size = 1.7)
... p3 = Person(age = 65, size = 1.6)
... p4 = Person(age = 71, size = 1.9)
注意,当将运算符“&
”与前面的缩写符号组合用于交集时,有必要使用括号,因为在 Python 语言中,这些运算符之间的优先级顺序不对(在 Python 中,&
优先于、< =、和> =)。这就是为什么在前面的例子中,我们用括号将“size >= 1.8
”括起来写为“Person & (size >= 1.8)
”。
然后,我们可以运行推理器,对我们创建的四个人进行重新分类:
>>> sync_reasoner(onto_person)
最后,我们显示他们的新类(注意,个体p4
现在属于两个类,即多重实例化,这在 OWL 中是允许的):
>>> print("p1", p1.is_a)
p1 [person.TallPerson]
>>> print("p2", p2.is_a)
p2 [person.Person]
>>> print("p3", p3.is_a)
p3 [person.Elderly]
>>> print("p4", p4.is_a)
p4 [person.Elderly, person.TallPerson]
注意,在 Protégé中,如果您想要使用浮点值定义具有受约束数据类型的类,请记住 Owlready 默认情况下将“decimal”XML 模式数据类型与 Python 的浮点数相关联。因此,TallPerson
类应该用被保护对象中的小数来定义:
Person and (size some decimal[>= "1.8"^^decimal])
但不是用花车。
以下是《门徒》中的结果:
或者,您也可以修改 Owlready 为 floats 使用的数据类型,如下所示:
>>> set_datatype_iri(float, "http://www.w3.org/2001/↲XMLSchema#float")
7.6 SWRL 规则
SWRL(语义网规则语言)是一种允许你将推理规则集成到本体中的语言。可以使用 Owlready 在 Protégé编辑器或 Python 中编写规则,然后通过集成的 HermiT 或 Pellet 推理机执行。
在细菌的本体中,以下示例规则使得可以将所有圆形的革兰氏阳性菌分类为葡萄球菌,并分组为簇:
Bacterium(?b),
gram_positive(?b, true),
has_shape(?b, ?f), Ronde(?f)
has_grouping(?b, ?r), InCluster(?r)
-> Staphylococcus(?b)
7.6.1 SWRL 语法
SWRL 规则包括一个或多个条件和一个或多个结果,由箭头“->”(由两个字符组成:减号和大于号)分隔。如果规则有几个条件或结果,它们之间用逗号“,”隔开,这隐含着逻辑“和”的意思。构成条件和结果的元素称为原子,同样的原子可以用在条件和结果中。
此外,SWRL 规则使用变量,其名称以“?”开头,比如,“?上例中的“b”。这些变量可以代表个体或值(整数、实数、字符串、布尔值等。),但从不包含类或属性。
可用的原子有:
-
一个类的成员:“类(?x)”,意思是:
-
“如果换个人呢?x 属于类 class”(当原子被用作条件时)
-
“个人?除了他/她当前的类之外,x 现在属于类 class(当作为结果使用时)
-
-
对象属性值:“对象 _ 属性(?x,?y)”,意思是:
-
“如果换个人呢?x 有 property_object?y "(条件)
-
“添加关系?x object_property?y”(结果)
变量也可以由特定的单个名称替换,例如,“object_property(?x,individual1)”或“object_property (individual2,y)”。
-
-
数据属性值:" data_property(?x,?y)”,意思是:
-
“要是个人呢?x 有 data_property?y "(条件)
-
“添加关系?x data_property?y”(结果)
变量也可以用一个特定的名字来代替(对于?x)或特定值(对于?y),例如“data_property(?x,9.2)"或" data_property(?x,charactersstring)”。
-
-
相同的个体:“SameAs(?x,?y)”,意思是:
-
“要是个人呢?x 和个人一样?y "(条件)
-
“个人?x 现在和个人一样?y”(结果)
-
-
不同的个体:“不同于(?x,?y)”,意思是:
-
“要是个人呢?x 与个体截然不同?y "(条件)
-
“个人?x 现在不同于个体了?y”(结果)
-
-
数据类型中的成员资格:"数据类型(?x)”,意思是:
-
“如果值呢?x 属于给定的数据类型"(条件)
最常见的数据类型是“int”、“decimal”(用于浮点数)、“bool”、“string”和“normalizedString”。
-
-
预定义函数(内置):“函数(?x,?y,...)".存在大量预定义的函数;以下是最常用的:
-
添加(?结果呢?x,?y):计算机?结果=?x +?y.
-
减去(?结果呢?x,?y):计算机?结果=?x -?y.
-
乘(?结果呢?x,?y):计算机?结果=?x ×?y.
-
分(?结果呢?x,?y):计算机?结果=?x /?y.
-
相等(?x,?y):测试是否?x =?y.
-
notEqual(?x,?y):测试是否?x≦?y.
-
小于(?x,?y):测试是否?x
-
greaterThan(?x,?y):测试是否?x >?y.
-
lessThanOrEqual(?x,?y):测试是否?x ≤?y。
-
greaterThanOrEqual(?x,?y):测试是否?x ≥?y.
-
stringConcat(?结果呢?x,?y):串联?x 和?然后把结果放进去。结果。
-
子串(?结果呢?str,?开始,?length):测试字符串的一部分。str 开始于?开始和结束?长度等于?结果。?长度是可选的。
-
stringLength(?长度,?str):计算字符串的长度?str 然后把结果放进去?长度。
-
包含(?str,?part):测试字符串是否。部分包含在?字符串。
-
containsIgnoreCase(?字符串,?part):与 contains()相同,但忽略大小写。
-
以(?str,?start):测试字符串是否。str 以字符串开头?开始吧。
-
endsWith(?str,?end):测试字符串是否。str 以字符串结尾?结束。
-
7.6.2 受保护人的 SWRL 规则
可以在 Protégé的“活动本体”选项卡和“规则”子选项卡中输入 SWRL 规则,如下图所示。请注意,被保护人并不总是保持规则中元素的顺序;然而,这并没有改变它们的意义。注意, Owlready 只能读取以 RDF/XML 或 N-Triples 文件格式保存的本体中的 SWRL 规则。SWRL 规则不支持 OWL/XML 格式!
Protégé中定义的规则可以用推理器执行(例如,使用 Owlready 的sync_reasoner()
函数)。当规则的结果为对象属性创建新的关系时,有必要使用选项infer_property_value = True
运行推理器。当他们为数据属性创建新的关系时,有必要运行带有infer_data_property_value = True
选项的推理器,并使用颗粒推理器。HermiT 不允许推断数据属性的值(参见 7.2)。
当规则在 Protégé中定义并且使用实数时,Protégé默认为浮点数,而 Owlready 需要小数(参见 7.5)。然后有必要强制十进制类型,如下例所示:2.2^^decimal.
7.6.3 SWRL 规则已准备就绪
在 Owlready 中,Imp
类允许您创建 SWRL 规则,“Imp”是“implies”的缩写。Owlready 允许您从语法与 Protégé相当的书面规则或者通过手动创建每个原子(这更复杂)来创建 SWRL 规则。
在下面的例子中,我们将创建一个人的本体,其规则是根据人的大小和体重来计算身体质量指数(身体质量指数)。计算身体质量指数的公式如下:
身体质量指数很重要,因为它决定了肥胖:如果一个人的身体质量指数大于或等于 30,就被认为是肥胖。
我们首先创建本体,有一个 Person 类和三个数据属性:体重、尺寸和 bmi。
>>> onto_person = get_ontology("http://test.org/person2.owl#")
>>> with onto_person:
... class Person(Thing): pass
... class weight(Person >> float, FunctionalProperty): pass
... class size (Person >> float, FunctionalProperty): pass
... class bmi (Person >> float, FunctionalProperty): pass
然后,我们为肥胖人群创建一个定义的类别:任何身体质量指数大于或等于 30 的人都将被重新划分到肥胖类别中。
>>> with onto_person:
... class Obese(Person):
... equivalent_to = [Person & (bmi >= 30.0)]
最后,我们创建一个 SWRL 规则。该规则将使用以下变量:
-
?x:类人的个体
-
?w:他的体重
-
?s:他的尺码
-
?s2:他的尺寸是平方
-
?他的身体质量指数
Imp
类允许你在本体中创建新的规则。然后,方法set_as_rule()
使得使用类似 Protégé的语法定义规则成为可能:
>>> with onto_person:
... imp = Imp()
... imp.set_as_rule("Person(?x), weight(?x, ?w),↲ size(?x, ?s),↲
multiply(?s2, ?s, ?s),↲ divide(?b, ?w, ?s2)↲
-> bmi(?x, ?b)")
然后,我们可以创建 Person 类的两个个体:
>>> p1 = Person(size = 1.7, weight = 65.0)
>>> p2 = Person(size = 1.7, weight = 90.0)
运行颗粒推理器:
>>> sync_reasoner_pellet(infer_property_values = True,
... infer_data_property_values = True)
最后,对 BMI 和两人被重新分类的类别提出质疑:
>>> p1.bmi
22.491348
>>> p1.is_a
[person2.Person]
>>> p2.bmi
31.141868
>>> p2.is_a
[person2.Obese]
str()
Python 函数以类似 Protégé的语法显示规则:
>>> str(imp)
'Person(?x), weight(?x, ?w), size(?x, ?s),↲multiply(?s2, ?s, ?s), divide(?b, ?w, ?s2) -> bmi(?x, ?b)'
Owlready 还通过body
(对于条件)和head
(对于结果)属性提供对规则的条件和结果的访问:
>>> imp.body
[Person(?x), weight(?x, ?w), size(?x, ?s),↲multiply(?s2, ?s, ?s), divide(?b, ?w, ?s2)]
>>> imp.head
[bmi(?x, ?b)]
然后就有可能接近每一个原子。例如,我们可以从条件部分检索第一个原子,并请求它的类和属性:
>>> atom = imp.body[0]
>>> atom
Person(?x)
>>> atom.is_a
[swrl.ClassAtom]
>>> atom.class_predicate
person2.Person
>>> atom.arguments
[?x]
参考手册(见 C.7)给出了原子类别列表和每一类的属性。注意,Owlready 系统地使用了属性“arguments”来访问原子的自变量(即括号中放置的元素),其中 SWRL 有时使用“arguments”,有时使用“argument1”和“argument2”。和 Owlready 一样,这些属性可以直接更改。
最后,rules()
和variables()
方法使得获得一个生成器来迭代本体中的所有 SWRL 规则和 SWRL 变量成为可能。
>>> list(onto_person.variables())
[?x, ?w, ?s, ?s2, ?b]
>>> list(onto_person.rules())
[Person(?x), weight(?x, ?w), size(?x, ?s),↲multiply(?s2, ?s, ?s), divide(?b, ?w, ?s2) -> bmi(?x, ?b)]
7 . 6 . 4 SWRL 规则的优势和局限性
SWRL 规则允许涉及几个变量(称为“自由变量”)的推理。相反,类别定义(通过等价关系,见 3.4.8)没有变量,实际上对应于具有单个自由变量的“伪规则”。所以有些复杂的推理是类定义无法实现的。例如,如果我们考虑有友谊和敌意关系的人,我们可以创建“AmbiguousPerson”类,对应于任何有朋友同时也是敌人的人。这个类不能用等价关系来定义:的确,我们可以定义一类人有一个朋友和一个敌人,但是 OWL 不允许指明这个朋友和这个敌人是同一个人。为此,我们需要两个自由变量:一个是模糊的人,另一个是他的朋友/敌人。
另一方面,这种类型的推理可以很容易地用 SWRL 规则进行,如下所示:
Person(?a),
Person(?b),
friend(?a, ?b),
enemy(?a, ?b),
-> AmbiguousPerson(?a)
然而,SWRL 规则有一个主要的缺点:它们依赖于一个给定的应用,这违背了本体独立性的目标(见 3.3)。例如,假设我们之前使用 SWRL 规则来识别葡萄球菌,而不是正式定义(在 3.4.8 中创建)。在这种情况下,我们无法推断出葡萄球菌的特性:该规则规定所有革兰氏阳性菌,无论是圆形的还是成簇的,都是葡萄球菌,但并没有肯定所有的葡萄球菌都是革兰氏阳性菌,无论是圆形的还是成簇的。因此,SWRL 规则允许识别葡萄球菌,但不允许我们可以重用我们的细菌本体的其他应用。
因此,当两种选择都有可能时,最好使用正式定义,当推理对定义来说太复杂时,最好使用 SWRL 规则。
7.7 示例:基于本体的决策支持系统
决策支持系统帮助专家做出决策,例如,通过提出建议。这里,我们感兴趣的是细菌的鉴定:从细菌上观察到的特征(革兰氏状态、形状、分组等。)和细菌本体中表达的知识,系统试图确定细菌的类型。该系统也可以弃权:当数据不足时,不作出决定。
这个决策支持系统是用 Flask(我们已经在 4.12 节使用过)以动态网站的形式实现的。它包括两个页面:一个“数据输入”页面,包含一个描述观察到的细菌的表格;一个“结果”页面,执行推理并显示结果。
以下程序创建了决策支持网站:
# File decision_support.py
from owlready2 import *
onto = get_ontology("bacteria.owl").load()
from flask import Flask, request
app = Flask(__name__)
@app.route('/')
def entry_page():
html = """<html><body>
<h3>Enter the bacteria characteristics:</h3>
<form action="/result">
Gram:<br/>
<input type="radio" name="gram" value="True"/> Positive<br/>
<input type="radio" name="gram" value="False"/> Negative<br/>
<br/>
Shape:<br/>
<input type="radio" name="shape" value="Round"/> Round<br/>
<input type="radio" name="shape" value="Rod"/> Rod<br/>
<br/>
Groupings:<br/>
<select name="groupings" multiple="multiple">
<option value="Isolated">Isolated</option>
<option value="InPair">InPair</option>
<option value="InCluster">InCluster</option>
<option value="InSmallChain">InSmallChain</option>
<option value="InLongChain">InLongChain</option>
</select><br/>
<br/>
<input type="submit"/>
</form>
</body></html>"""
return html
ONTO_ID = 0
@app.route('/result')
def page_result():
global ONTO_ID
ONTO_ID = ONTO_ID + 1
onto_tmp = get_ontology("http://tmp.org/onto_%s.owl#" %↲ ONTO_ID)
gram = request.args.get("gram", "")
shape = request.args.get("shape", "")
groupings = request.args.getlist("groupings")
with onto_tmp:
bacterium = onto.Bacterium()
if gram == "True": bacterium.gram_positive = True
elif gram == "False": bacterium.gram_positive = False
if shape:
shape_class = onto[shape]
bacterium.has_shape = shape_class()
for grouping in groupings:
grouping_class = onto[grouping]
bacterium.has_grouping.append(grouping_class())
close_world(bacterium)
sync_reasoner([onto, onto_tmp])
class_names = []
for bacterium_class in bacterium.is_a:
if isinstance(bacterium_class, ThingClass):
class_names.append(bacterium_class.name)
class_names = "," .join(class_names)
html = """<html><body>
<h3>Result: %s</h3>
</body></html>""" % class_names
onto_tmp.destroy()
return html
import werkzeug.serving
werkzeug.serving.run_simple("localhost", 5000, app)
该站点的第一个页面用于数据输入,是一个简单的 HTML 页面,包含三个字段(我们可以使用静态 HTML 页面)。对于克状态和形状,使用“单选按钮”字段。对于分组,它是一个“选择”字段,以便允许选择几个值。
用于显示结果的第二个页面执行以下步骤:
-
创建一个新的临时本体(为了不“污染”细菌的本体),使用“
http://tmp.org/onto_XXX.owl#
”形式的 IRI,其中 XXX 是一个数字。 -
检索表单参数的值。这是通过函数
request.args.get()
(当参数只能取一个值时)和request.args.getlist()
(当参数可以有多个值时;这里,分组就是这种情况)。 -
根据输入的值创建一个单独的细菌。
-
关闭这种新细菌的世界。这将防止推理者对输入值之外的值做出假设(见第 7.3 节)。
-
在两个本体上执行推理机:细菌本体和临时本体(另一方面,由其他线程或进程创建的其他临时本体将不会被考虑)。推论放在临时本体中(原因和之前一样:为了避免污染细菌的主本体)。
-
推理后检索细菌所属类的名称。条件“
isinstance(bacterium_class, ThingClass)
”允许您将自己限制为“真正的”类,排除构造函数(特别是由close_world()
创建的限制)。 -
破坏临时本体(这破坏了我们创造的细菌,也破坏了它可能的形状和分组,以及推论)。
在程序执行期间,可通过地址“http://127.0.0.1:5000
”查阅网站。下面的截图显示了获得的站点,带有“入口”页面和“结果”页面。我们输入了一种革兰氏阳性细菌,圆形,聚集成一条小链。经过验证,系统告诉我们是链球菌。
只有定义的类(通过等价关系:“equivalent_to”)允许对个体进行重新分类。由于在第三章的细菌本体中定义的唯一种类是葡萄球菌、链球菌、球菌和杆菌,我们的系统目前只能识别葡萄球菌、链球菌、球菌和杆菌。然而,如果本体用其他类别和其他定义来丰富,则有可能识别更多数量的细菌。这可能需要考虑更多的参数(厌氧、其他颜色等。)来更详细地描述细菌。
注意,web 服务器是多线程的,我们在 quadstore 中编写。在这种情况下,我们在 5.13 节中看到,有必要考虑同步。Owlready 自动同步get_ontology()
和ontology.destroy()
指令,以及“与本体:……”街区。所以我们没剩下多少时间来同步了!
我们必须考虑同步的唯一一点是:每个临时本体必须有一个不同的名称。我们用数字给它们命名(onto_1.owl,onto_2.owl 等。).这是必要的,因为page_result()
函数可以被几个线程同时调用。因此,每个人都必须有自己的本体:事实上,如果他们共享相同的本体,当第一个线程破坏了本体(onto_tmp.destroy()
)时,第二个线程就不能再继续工作了。
7.8 摘要
在这一章中,你已经学习了如何使用 Pellet 和隐士猫头鹰推理机进行自动推理。您还学习了如何在一个封闭的世界或局部封闭的世界中进行推理,以及如何使用 SWRL 规则来补充类别定义。
八、注释、多语言文本和全文搜索
在这一章中,我们将看到如何用 Owlready 处理 Python 中的注释。我们还将了解如何处理注释中经常使用的多语言文本,以及如何优化全文搜索。
8.1 注释实体
注释允许您添加关于实体和本体关系的元数据。他们可以描述作者,修改日期,以及实体的描述,有可能有不同语言的文本。注释不同于属性,因为它们不会干扰推理。特别是,当在类上定义时,注释不会被子类继承。
OWL 中默认定义了以下注释属性:
-
标签(实体标签)
-
注释(您可以添加的关于实体或关系的任何注释)
-
那就去吧
-
versionInfo(版本信息)
-
早期版本
-
已弃用(用于指示不应再存在且仅为兼容目的而保留的实体)
-
与不相容
-
backwardCompatibleWith(与早期版本的实体兼容)
-
被定义为
像任何关系一样,可以用点符号访问注释。注意,注释从来都不是功能性的,所以它们的值总是一个列表。可以用append()
方法添加到列表中或重新定义整个列表:
>>> from owlready2 import *
>>> onto = get_ontology("bacteria.owl").load()
>>> onto.unknown_bacterium.label.append("An unknown bacterium")
>>> onto.unknown_bacterium.comment = [
... "Found in the lab at the bottom of a drawer.",
... "Remember to analyze it soon." ]
在本例中,我们重用了之前在第三章 Protégé中创建的 unknown _ bacterium 个体。
任何类型的实体都可以被注释:个体,还有类和属性。
像任何关系一样,可以用点符号获得注释。要删除注释,只需从列表中删除它(使用 Python del
语句或remove()
方法;参见 2.4.4)。
8.2 多语言文本
注释中的字符串可以与语言(例如,英语、法语等)相关联。).locstr
对象(本地化字符串)使字符串与其语言(由两个字母的代码标识:英语为“en”,法语为“fr”,等等)相关联成为可能。):
>>> s = locstr("An unknown bacterium", "en")
>>> s.lang
'en'
locstr
对象可以作为 Python 字符串使用。它们通常用在注释中:
>>> onto.unknown_bacterium.label = [
... locstr("An unknown bacterium", "en"),
... locstr("Une bactérie inconnue", "fr") ]
此外,可以按语言过滤注释列表,如下所示:
>>> onto.unknown_bacterium.label
['An unknown bacterium', 'Une bactérie inconnue']
>>> onto.unknown_bacterium.label.en
['An unknown bacterium']
>>> onto.unknown_bacterium.label.fr
['Une bactérie inconnue']
与其他 Owlready 列表一样,first()
方法返回第一个元素(如果列表为空,则返回None
):
>>> onto.unknown_bacterium.label.en.first()
'An unknown bacterium'
通过语言过滤的列表允许您添加新的注释,而不需要创建一个locstr
对象。在下面的例子中,注释字符串将自动与英语相关联(就像使用了一个locstr
对象一样):
>>> onto.unknown_bacterium.comment.en.append("Comment in↲ English.")
8.3 注释构造
还可以使用另一种语法“annotation[constructor]”对构造函数进行注释。以下示例创建了一个带有限制的新细菌子类,并对该限制进行了注释:
>>> with onto:
... class GramPositiveBacterium(onto.Bacterium):
... is_a = [onto.gram_positive.value(True)]
>>> comment[GramPositiveBacterium.is_a[-1]].append(
... "comment on the value
restriction on gram_↲positive.")
8.4 注释属性和关系
属性和关系也可以被注释。属性可以像任何其他实体一样进行注释:
>>> onto.has_shape.comment = ["A comment on the has_shape↲ property."]
OWL 还使得注释关系成为可能,也就是说,您可以注释一个 RDF 三元组,它通过属性(或谓词)将一个主题链接到一个对象。如果您想要以元数据的形式提供关系的附加细节,例如,指示关系的作者或日期,这是很有用的。我们可以在 Owlready 中使用特殊的语法“annotation[subject,property,object]”来实现这一点,例如:
>>> shape = onto.unknown_bacterium.has_shape
>>> comment[onto.unknown_bacterium, onto.has_shape, shape] =↲
["A comment on the fact that the bacterium has this shape."]
对于涉及 OWL 内置属性的关系,可以使用特殊值 rdf_type、rdfs_subclassof、owl_equivalentclass 等:
>>> comment[onto.unknown_bacterium, rdf_type, onto.Bacterium] =↲
["a comment on belonging to the Bacterium class."]
8.5 创建新的注释类
新的注释属性可以用与其他属性相同的方式创建,继承自AnnotationProperty
类,例如:
>>> with onto:
... class observer(AnnotationProperty): pass
>>> onto.unknown_bacterium.observer = ["Observed by JB Lamy."]
>>> observer[onto.unknown_bacterium, rdf_type,↲ onto.Bacterium] = [
... "Also observed by JB Lamy."
... ]
8.6 本体元数据
本体的元数据由直接放置在本体上的注释组成(可以在“活动本体”选项卡的“注释”列表中的 Protégé中添加)。它们可能描述版本号、本体的历史、作者的名字等等。在 Owlready 中,这些注释可以通过本体的metadata
属性获得。例如,基因本体(GO)的注释“comment”是这样获得的:
>>> go = get_ontology("http://purl.obolibrary.org/obo/go.owl").↲load()
>>> go.metadata.comment
['cvs version: $Revision: 38972 $',
'Includes Ontology(OntologyID(OntologyIRI(<http://purl.↲obolibrary.org/
obo/go/never_in_taxon.owl>))) [Axioms: 18↲ Logical Axioms: 0]']
也可以用 Owlready 修改元数据:
>>> go.metadata.comment.append("Here is a new comment!")
>>> go.metadata.comment
['cvs version: $Revision: 38972 $',
'Includes Ontology(OntologyID(OntologyIRI(<http://purl.↲obolibrary.org/
obo/go/never_in_taxon.owl>))) [Axioms: 18↲ Logical Axioms: 0]',
'Here is a new comment!']
8.7 全文搜索
全文搜索允许您优化本体中的文本搜索。在大型本体上,速度增益可以达到 1000 倍。
默认情况下,不会激活全文搜索,因为它会增加 quadstore 的大小。有必要为将要使用它的每个属性激活它。default_world.full_text_search_properties
列表包含启用全文搜索的属性列表。默认情况下,它是空的。要激活属性的全文搜索,只需将其添加到列表中。
例如,要激活 OWL comment
属性的全文搜索:
>>> default_world.full_text_search_properties.append(comment)
我们现在可以用search()
方法执行全文搜索,使用包含要搜索的文本的FTS
对象(“全文搜索”的缩写)。与普通搜索不同,全文搜索是从关键字列表(而不是所搜索的确切值)开始的,并且忽略大小写(也就是说,它不区分大小写)。例如,要搜索注释中包含单词“English”的所有实体,我们将执行以下操作:
>>> default_world.search(comment = FTS("English"))
[bacteria.unknown_bacterium]
可以给出几个关键字(关键字的顺序无关紧要)并使用字符“*”作为通配符,但只能在关键字的末尾。例如,要搜索注释中包含单词“English”和以“comm”开头的单词的所有实体,我们将执行以下操作:
>>> default_world.search(comment = FTS("English comm*"))
[bacteria.unknown_bacterium]
FTS
对象还允许您指定搜索的语言(默认情况下,搜索以所有语言执行)。以下示例仅在英文注释中搜索:
>>> default_world.search(comment = FTS("English comm*", "en"))
请注意,如果您没有使用FTS
对象,owl 已经执行了一个普通的、非优化的搜索,如下例所示:
>>> default_world.search(comment = "English comm*") # Without FTS !
[]
要停用全文搜索,只需使用remove()
从default_world.full_text_search_properties
列表中删除属性。
8.8 示例:在 Python 中使用 DBpedia
DBpedia 是结构化数据的自动提取,源自免费开放的维基百科全书。值得注意的是,DBpedia 不仅包含维基百科页面之间的关系,还包含更具体的数据,比如出现在维基百科上的人的出生日期。OWL 本体构建了所有的数据。因此,它是面向一般数据集的“一般文化”。最新版本是 2016 年的,可以从以下地址下载: https://wiki.dbpedia.org/downloads-2016-10
。DBpedia 的 2020 版本还在开发中,一些重要的特性仍然缺失。
dbpedia 以几个文件的形式存在(参见下面的网站截图):本体部分以 OWL 格式下载(文件“dbpedia_2016-10.owl”),数据以 TTL 格式下载(相当于 N-Triples)在它们的规范化版本中(在 DBpedia 网站上标注为“ttl *”)。有几种语言可供选择;我们将使用英文版本。
[...]
8.8.1 加载 DBpedia
因为 DBpedia 非常大,所以并不是所有的文件都常用。下表列出了可以下载的主要工具(请注意数据很大:大约需要 20 GB)。
|DBpedia 中的名称
|
文件名和描述
|
| --- | --- |
| 本体猫头鹰 | dbpedia_2016-10.owl 本体(包含类,但没有个体) |
| 实例类型 | instance _ types _ wkd _ uri _ en . TTL . bz2 个体和类之间的“is_instance_of”关系(对应于 RDF“type”关系) |
| 文章类别 | article _ categories _ wkd _ URIs _ en . TTL . bz2 维基百科文章和类别之间的关系(“主题”属性) |
| 基于映射的文字 | mapping based _ literals _ wkd _ URIs _ en . TTL . bz2 维基百科信息框中的数据属性 |
| 基于映射的对象 | mapping based _ objects _ wkd _ URIs _ en . TTL . bz2 维基百科信息框中的对象属性 |
| 类别标签 | category _ labels _ wkd _ uri _ en . TTL . bz2-类别 _ 标签 _ wkd _ uri _ en . TTL . bz2 类别的标签 |
| 标签 | labels _ wkd _ uri _ en . TTL . bz2 实体标签(全文搜索需要) |
| 个人数据 | person data _ wkd _ uri _ en . TTL . bz2 与人相关的数据(出生日期等。) |
| 页面链接 | 页 _ links _ wkd _ uri _ en . TTL . bz2 对应于维基百科页面之间的链接的关系(“wikiPageWikiLink”属性) |
DBpedia 是一个 OWL 本体,因此可以用 Owlready 加载。然而,DBpedia 比通常的本体要“干净”得多。特别地,实体的虹膜从一个文件到另一个文件不是恒定的,以下前缀可互换使用:
因此,在加载本体之前,预处理步骤将是必要的,以便清除这些不一致。
此外,DBpedia 是一个非常庞大的本体。这需要采取几项预防措施:
-
将 quadstore 保存在磁盘上(见 4.7),以避免每次使用时都要重新加载本体。
-
将临时文件放在磁盘上可以容纳几千兆字节数据的目录中。特别是,在 Linux 下,临时文件默认放置在/tmp 中,但是/tmp 通常存储在 RAM 中,这限制了可用空间。这可能导致“数据库或磁盘已满”类型的错误。
对于非常大的本体,最好将临时文件放在其他地方,而不是/tmp 中。这可以在定义 quadstore 时用sqlite_tmp_dir
来完成:
default_world.set_backend(
filename = "/path/to/quadstore.sqlite3",
sqlite_tmp_dir = "/path/to/temporary/files",
)
以下程序用于将 DBpedia 加载到 Owlready quadstore 中:
# File import_dbpedia.py
from owlready2 import *
import io, bz2
# DBPEDIA_DIR is the directory where you downloaded DBpedia
DBPEDIA_DIR = "/home/jiba/telechargements/dbpedia/"
TMP_DIR = "/home/jiba/tmp"
QUADSTORE = "dbpedia.sqlite3"
default_world.set_backend(filename = QUADSTORE,↲ sqlite_tmp_dir = TMP_DIR)
dbpedia = get_ontology("http://wikidata.dbpedia.org/ontology/")
contenu = open(os.path.join(DBPEDIA_DIR, "dbpedia_2016-10.↲owl")).read()
contenu = contenu.replace("http://dbpedia.org/ontology,↲
"http://wikidata.dbpedia.org/ontology")
contenu = contenu.replace("http://www.wikidata.org/entity,↲
"http://wikidata.dbpedia.org/resource")
dbpedia.load(fileobj = io.BytesIO(contenu.encode("utf8")))
for fichier in os.listdir(DBPEDIA_DIR):
if fichier.endswith(".ttl.bz2"):
print("Import de %s..." % fichier)
onto = get_ontology("http://dbpedia.org/ontology/%s/" %↲
fichier.replace(".ttl.bz2", ""))
f = bz2.open(os.path.join(DBPEDIA_DIR, fichier))
onto.load(fileobj = f)
print("Indexing...")
default_world.full_text_search_properties.append(label)
default_world.save()
在这个程序中,必须根据你的配置修改三个全局变量:DBPEDIA_DIR
表示你从 DBpedia 下载文件的目录(OWL 和 TTL。BZ2 文件),TMP_DIR
表示可以容纳几 GB 数据的临时目录(见前面),而QUADSTORE
是将存储 Owlready quadstore 的文件的名称(注意,为 quadstore 预留 13 GB 空间用于前面的文件,并预留一个小时或更多的加载时间)。
DBpedia 本体(OWL 文件)被作为文本文件读取,并使用replace()
方法进行纠正,然后用 Owlready 加载。为了避免保存文件的修正版本,我们直接从 RAM 中加载本体:本体的修正版本在作为字符串包含的变量中。我们分两个阶段将这个字符串转换成一个文件对象:首先,我们用 UTF8 对字符串进行编码,然后用io.BytesIO()
将其转换成一个文件对象。最后,我们从这个文件对象中加载本体,用load(filobj = ...)
(见 4.2)。
然后,一个循环遍历DBPEDIA_DIR
目录中的所有文件,并处理 TTL。BZ2 档案。这些文件用 Python 模块bz2
解压缩,然后从压缩文件对象中加载。我们在这里选择为每个文件创建一个单独的本体(这将允许您单独重新加载每个文件,或者删除不再需要的本体)。
注意倒数第二行,它激活了对label
属性的全文搜索。
然后,我们可以通过以下方式加载结果 quadstore,例如,在 Python 控制台模式下。(注意不要重用已经加载了 Owlready2 并在 quadstore 中创建了实体的 Python 控制台;否则,您将得到一个错误。这是因为如果内存中的 quadstore 不为空,则不可能从文件中加载 quad store。)
>>> from owlready2 import *
>>> QUADSTORE = "dbpedia.sqlite3"
>>> default_world.set_backend(filename = QUADSTORE)
* Owlready2 * WARNING: http://wikidata.dbpedia.org/ontology/senator
belongs to more than one entity types (e.g. Class, Property, Individual):
[owl.ObjectProperty, ontology.MemberOfParliament, DUL.sameSettingAs];
I'm trying to fix it...
[...]
注意,加载比从 OWL 和 TTL 导入要快得多。BZ2 档案。“警告”表示某些属性没有在 DBpedia 中正确声明;我们可以放心地忽略它们。
还要注意,DBpedia 的初始加载和处理这个本体的第一个命令可能需要很长时间才能执行,因为底层数据库非常大(大约 12 GB)。然而,一旦下了第一批订单,索引和缓存将在 RAM 中,访问 DBpedia 将会快得多。
然后我们可以加载 DBpedia 本体:
>>> dbpedia = get_ontology("http://wikidata.dbpedia.org/↲ontology/")
我们修改实体的渲染(见 4.9)来显示它们的标签:
>>> def render(e):
... return "%s:%s" % (e.name, e.label.en.first())
>>> set_render_func(render)
我们现在可以在 DBpedia 中执行优化的全文搜索,例如,查找标签中带有“French”和“revolution”的所有文章:
>>> default_world.search(label = FTS("french Revolution"))
[Q1154330:10 August (French Revolution),
Q207318:French Revolutionary Wars,
Q7216178:French Revolution,
[...] ]
请注意,最初的搜索可能会很长,需要几秒钟。但是,当您的计算机已经将索引加载到高速缓存中时,下面的搜索会进行得更快!
要访问文章,我们需要创建一个名称空间(见 4.8),因为这些是在“ http://wikidata.dbpedia.org/resource/
”中定义的,而不是在“ http://wikidata.dbpedia.org/ontology/
”中定义的:
>>> dbpedia_resource = default_world.get_namespace(↲
"http://wikidata.dbpedia.org/resource/")
我们现在可以访问文章 Q207318(“法国革命战争”,这是最完整的之一),并请求其属性列表:
>>> revolution = dbpedia_resource.Q207318
>>> list(revolution.get_properties())
[combatant:combatant, date:date, result:result, commander:commander,
place:place of military conflict, territory:territory, label:None,
wikiPageWikiLink:Link from a Wikipage to another Wikipage]
然后我们可以显示关于法国大革命战争的文章中提到的人员列表:
>>> persons = [i for i in revolution.wikiPageWikiLink
... if isinstance(i, dbpedia.Person)]
>>> print(persons)
[Q10088:Tipu Sultan, Q1096347:Claude Lecourbe,
Q112009:Michael von Melas, Q128019:Pope Pius VI,
...]
在使用 DBpedia 本体时,有一点需要注意:DBpedia 使用经典的 RDFS“注释”标注对类进行注释;但是,它也重新定义了自己的“评论”属性。这两者之间的混淆使得使用“entity.comment”语法很难得到注释!在名称冲突的情况下,最后加载的属性优先,因此 DBpedia 的属性优先。这就是为什么在下面的例子中,我们没有得到注释:
>>> dbpedia.SongWriter.comment # DBpedia comments
[]
要强制使用 RDFS“注释”注释,有两种选择。首先,我们可以使用替代语法“property[entity]”,如下例所示:
>>> comment[dbpedia.SongWriter] # RDFS comments
['a person who writes songs.', 'een persoon die de muziek en/of
de tekst voor populaire muzieknummers schrijft.']
我们还可以重新定义语法“entity.annotation”使用的属性,如下所示:
>>> default_world._props["comment"] = comment
>>> dbpedia.SongWriter.comment # RDFS comments now!
['a person who writes songs.', 'een persoon die de muziek en/of
de tekst voor populaire muzieknummers schrijft.']
8 . 8 . 2 DBpedia 的搜索引擎
使用之前创建的 quadstore 和全文搜索函数,我们可以轻松地为 DBpedia 制作一个搜索引擎。我们将依靠一个带有 Flask 的动态网站(我们已经在 4.12 节使用过了)。
以下程序加载 DBpedia quadstore,然后创建动态网站:
# File search_dbpedia.py
from flask import Flask, request
app = Flask(__name__)
from owlready2 import *
QUADSTORE = "dbpedia.sqlite3"
default_world.set_backend(filename = QUADSTORE)
dbpedia = get_ontology("http://wikidata.dbpedia.org/↲ontology/")
resource = default_world.get_namespace(↲
"http://wikidata.dbpedia.org/resource/")
@app.route('/')
def page_query():
html = """
<html><body>
<form action="/result">
<input type="text" name="keywords"/>
<input type="submit"/>
</form>
</body></html>"""
return html
@app.route('/result')
def page_result():
keywords = request.args.get("keywords", "")
html = """<html><body>Search results for "%s":<br/>\n""" %↲ keywords
keywords =" ".join("%s*" % keyword for keyword in↲ keywords.split())
articles = default_world.search(label = FTS(keywords))
html += """<ul>"""
for article in articles:
html += """<li><a href=/article/%s>%s:%s</a></li>"""↲
% (article.name, article.name, article.label.first())
html += """</ul></body></html>"""
return html
@app.route('/article/<name>')
def page_article(name):
article = resource[name]
html = """<html><body><h2>%s:%s</h2>"""↲
% (article.name, article.label.first())
html += """belongs to classes: %s<br/><br/>\n """↲
% "," .join(repr(clazz) for clazz in article.is_a)
html += """has link to page:<br/>\n"""
html += """<ul>"""
for cite in article.wikiPageWikiLink:
html += """<li><a href=/article/%s>%s:%s</a></li>"""↲
% (cite.name, cite.name, cite.label.first())
html += """</ul></body></html>"""
return html
import werkzeug.serving
werkzeug.serving.run_simple("localhost", 5000, app)
该网站包括三种类型的页面:
-
“查询”页面(对应于网站根目录的路径“/”)以 HTML 形式显示搜索字段。该页面本身不是动态的;我们可以完全用普通的 HTML 来制作。
-
“结果”页面(路径)/结果?关键字=
))显示搜索结果。我们获取用户输入的关键字,然后通过在每个关键字的末尾添加一个星号“*”来转换它们。然后我们用 search()
和FTS
进行搜索。最后,我们生成一个显示结果列表的 HTML 页面,为找到的每篇文章显示其标识符、标签和到相应文章页面的链接。 -
“文章”页面(path)/article/
/),显示文章的标识符和标题、文章所属的一个或多个类别以及它引用的其他文章(通过关系“wikiPageWikiLink”获得)。
一旦程序被执行,就可以在网址“http://127.0.0.1:5000”上查阅该网站。同样,DBpedia 的初始加载和第一次搜索可能会很长。然而,一旦第一次搜索完成,接下来的搜索将会立即进行。
下图显示了动态网站的“查询”页面和“结果”页面的屏幕截图:
8.9 总结
在这一章中,你已经学习了如何阅读和创建 OWL 注释,以及如何使用多语言文本。我们还看到了如何使用 Owlready 在 Python 中访问 DBpedia。
九、使用医学术语、PyMedTermino 和 UMLS
在这一章中,我们将看到如何使用 PyMedTermino 将 UMLS 的主要医学术语导入 Python,PyMedTermino 是一个允许将这些术语集成到 Owlready 中的模块。我们还将看到如何使用 UMLS 统一概念(CUI)将这些术语联系在一起。
9.1 UML
UMLS(统一医学语言系统)是来自生物医学领域的 400 多个术语的集合。UMLS 还集成了不同术语之间的映射。UMLS 由美国国家医学图书馆(NLM)制作,可在以下地址在线注册后免费下载:
www.nlm.nih.gov/research/umls/licensedcontent/umlsknowledgesources.html
但是,请注意,UMLS 中包含的某些术语不能在某些国家自由使用。SNOMED CT 公司的情况尤其如此,该公司必须获得由国家或机构或公司支付的许可证。
在撰写本文时,最新的版本是 2019AB(完整版(umls-2019AB-full.zip))。要将 UMLS 与 Owlready 和 PyMedTermino 一起使用,您需要下载这个文件(大约 4.8 GB)。但是,不需要解压(PyMedTermino 会帮你做)。
9.2 从 UMLS 引进术语
PyMedTermino 是一个 Python 模块,允许访问医学术语。PyMedTermino 的第 2 版直接包含在 Owlready 中,所以不需要安装。但是,请注意,导入 UMLS 数据需要 Python 的 3.7 版本(或更高版本)(另一方面,一旦 UMLS 数据被导入,如果您愿意,您可以在 Python 3.6 中使用它)。
owlready2.pymedtermino2
模块允许您通过全局函数import_umls()
将全部或部分 UMLS 数据导入 Owlready quadstore:
import_umls("./path/to/umls-2019AB-full.zip",↲
terminologies = [...],↲
langs = [...] )
该函数的第一个参数是包含 UMLS 数据的 ZIP 文件的路径,这是我们之前下载的。在前面的示例中,这是一个本地路径,但也可以是一个完整的路径,例如,“/home/jbl Amy/download/umls-2020 aa-full . zip”或“C:\ \ Downloads \ \ umls-2020 aa-full . zip”,这取决于您保存该文件的位置。
第二个参数是要导入的术语列表。如果缺少此参数,将导入所有术语。以下网页列出了 UMLS 的术语和相关代码:
www.nlm.nih.gov/research/umls/sourcereleasedocs/index.html
第三个参数指示要导入的语言,例如,“en”表示英语,“fr”表示法语。如果缺少此参数,将导入所有语言。
例如,我们可以导入术语 CIM10 和 SNOMED CT(英文版,UMLS 代码为 ICD10 和 SNOMEDCT_US)以及 CUI UMLS(被视为伪术语),如下所示:
>>> from owlready2 import *
>>> from owlready2.pymedtermino2 import *
>>> from owlready2.pymedtermino2.umls import *
>>> default_world.set_backend(filename = "pymedtermino.sqlite3")
>>> import_umls("umls-2020AA-full.zip",↲
terminologies = ["ICD10", "SNOMEDCT_US", "CUI")
>>> PYM = get_ontology("http://PYM/").load()
>>> default_world.save()
UMLS 数据导入大约需要 5-10 分钟。“PYM
”在这里是“PyMedTermino”的缩写。
默认情况下,PyMedTermino 激活注释属性label
(对应于术语概念的术语)和synonyms
(对应于同义词)的全文搜索(参见 8.7)。如果您不想激活它,您必须在调用import_umls()
功能时添加选项fts_index = False
。
请注意,UMLS 包括几个术语翻译,例如,ICD10 有德语(代码“DMDICD10”)和荷兰语(代码“ICD10DUT”)。但是,不包括法文翻译。PyMedTermino 2 有一个用于导入法语 ICD10(代码“CIM10”)的特定模块,可以按如下方式使用:
>>> from owlready2.pymedtermino2.icd10_french import *
>>> import_icd10_french()
>>> default_world.save()
9.3 初始进口后装载术语
显然,下一次我们想要使用导入的术语时,我们将不再需要调用导入函数。我们现在只需要以下三行代码:
>>> from owlready2 import *
>>> default_world.set_backend(filename = "pymedtermino.sqlite3")
>>> PYM = get_ontology("http://PYM/").load()
这三行代码重新加载了 quadstore(使用set_backend()
方法)和 PYM (PyMedTermino)本体。不要忘记调用load()
;有必要加载与本体相关联的 Python 方法。
9.4 使用 ICD10
PyMedTermino 使用相同的界面提供对所有术语的访问。我们将在这里看到 ICD10 和 SNOMED CT 术语,但对于其他术语,功能仍然类似。
国际疾病分类第 10 版(ICD10)是一个被广泛使用的疾病分类。例如,它在法国用于医院的医疗经济编码。ICD10 包括大约 12,000 个概念。它以树的形式组织,21 个根概念对应于疾病的主要章节:癌症、传染病、心血管疾病、肺病等等。(注意,在美国,ICD9(第 9 版)仍在大量使用。可以使用术语代码“ICD9CM”获得。)
我们可以如下获得英文 ICD10 术语:
>>> ICD10 = PYM["ICD10"]
>>> ICD10
PYM["ICD10"] # ICD10
>>> ICD10.name
'ICD10'
PyMedTermino 以如下方式显示概念:“terminals[code]# concept label”(对于有多个标签的概念,只显示一个,从首选标签中选择)。请注意,概念标签前面有一个#字符,因此如果您在 Python 中复制粘贴概念,它将被视为注释。这允许复制粘贴 PyMedTermino 概念或概念列表。
术语对象和术语概念是已经存在的类。因此,我们可以使用类方法来操作术语,例如,subclasses()
方法可以获得 ICD10 的子类,也就是说,前面提到的 21 个疾病章节:
>>> list(ICD10.subclasses())
[ ICD10["K00-K93.9"] # Diseases of the digestive system
, ICD10["C00-D48.9"] # Neoplasms
...]
但是,PyMedTermino 提供了额外的属性和方法来方便术语的操作。例如,children
属性直接返回子概念列表,而不必像以前那样通过生成器。此外,子概念按代码排序(在 UMLS 并非总是如此),如下例所示:
>>> ICD10.children
[ ICD10["A00-B99.9"] # Certain infectious and parasitic diseases
, ICD10["C00-D48.9"] # Neoplasms
, ICD10["D50-D89.9"] # Diseases of blood and blood-forming organs and
# certain disorders involving the immune mechanisms
, ICD10["E00-E90.9"] # Endocrine, nutritional and metabolic diseases
, ICD10["F00-F99.9"] # Mental, behavioural disorders
, ICD10["G00-G99.9"] # Diseases of the nervous system
, ICD10["H00-H59.9"] # Diseases of the eye and adnexa
, ICD10["H60-H95.9"] # Diseases of the ear and mastoid process
, ICD10["I00-I99.9"] # Diseases of the circulatory system
, ICD10["J00-J99.9"] # Diseases of the respiratory system
, ICD10["K00-K93.9"] # Diseases of the digestive system
, ICD10["L00-L99.9"] # Diseases of the skin and subcutaneous tissue
, ICD10["M00-M99.9"] # Diseases of the musculoskeletal system and
# connective tissue
, ICD10["N00-N99.9"] # Diseases of the genitourinary system
, ICD10["O00-O99.9"] # Pregnancy, childbirth and the puerperium
, ICD10["P00-P96.9"] # Certain conditions originating in the
# perinatal period
, ICD10["Q00-Q99.9"] # Congenital malformations, deformations
# and chromosomal abnormalities
, ICD10["R00-R99.9"] # Symptoms, signs and abnormal clinical and
# laboratory findings, not elsewhere classified
, ICD10["S00-T98.9"] # Injury, poisoning and certain other
# consequences of external causes
, ICD10["V01-Y98.9"] # External causes of morbidity and mortality
, ICD10["Z00-Z99.9"] # Factors influencing health status and
# contact with health services
]
我们可以在层次结构中向下显示,例如,第一章(传染病)的儿童:
>>> ICD10.children[0].children
[ ICD10["A00-A09.9"] # Intestinal infectious diseases
, ICD10["A15-A19.9"] # Tuberculosis
, ICD10["A20-A28.9"] # Certain zoonotic bacterial diseases
, ICD10["A30-A49.9"] # Other bacterial diseases
, ICD10["A50-A64.9"] # Infections with a predominantly sexual mode
# of transmission
...]
为了从代码中直接访问一个概念,我们可以索引术语。例如,在 ICD10 中,编码为“I10”的概念对应于原发性高血压:
>>> ICD10["I10"]
ICD10["I10"] # Essential (primary) hypertension
派梅特米诺将 IRI 与每个概念联系起来,形式为“http://PYM/ ”, for example:
>>> ICD10["I10"].iri
'http://PYM/ICD10/I10'
因此,概念的名称(或标识符)与其代码相对应:
>>> ICD10["I10"].name
'I10'
terminology
属性用于获取概念所属的术语:
>>> ICD10["I10"].terminology
PYM["ICD10"] # ICD10
概念标签可通过首选标签的label
注释和其他标签的synonyms
注释进行访问;这些 OWL 注释可以作为 Python 属性来访问:
>>> ICD10["I10"].label
['Essential (primary) hypertension']
>>> ICD10["I10"].synonyms
[]
根据术语的不同,概念可以有一个或多个标签和零个或多个同义词(在 ICD10 中,概念只有一个标签,没有同义词)。
parents
属性提供了对父概念的访问(即更一般的概念):
>>> ICD10["I10"].parents
[ICD10["I10-I15.9"] # Hypertensive diseases
]
ICD10 是一个单轴分类,也就是说,每个概念只有一个亲本(除了没有亲本的主要章节)。然而,PyMedTermino 可以用相同的接口处理所有的术语;这就是为什么在 CIM10 中,parents
属性只返回一个父对象的列表。
ancestor_concepts()
和descendant_concepts()
方法分别返回祖先和后代概念的列表。它们类似于ancestors()
和descendants()
;但是,它们返回列表(而不是集合),它们只返回 UMLS 概念(特别是,ancestor_concepts()
返回的列表不包括Thing
)。
>>> ICD10["I10"].ancestor_concepts()
[ ICD10["I10"] # Essential (primary) hypertension
, ICD10["I10-I15.9"] # Hypertensive diseases
, ICD10["I00-I99.9"] # Diseases of the circulatory system
]
>>> ICD10["I00-I99.9"].descendant_concepts()
[ ICD10["I00-I99.9"] # Diseases of the circulatory system
, ICD10["I00-I02.9"] # Acute rheumatic fever
, ICD10["I00"] # Rheumatic fever without mention of heart↲ involvement
, ICD10["I01"] # Rheumatic fever with heart involvement
, ICD10["I01.0"] # Acute rheumatic pericarditis
, ICD10["I01.1"] # Acute rheumatic endocarditis
, ICD10["I01.2"] # Acute rheumatic myocarditis
, ICD10["I01.8"] # Other acute rheumatic heart disease
, ICD10["I01.9"] # Acute rheumatic heart disease, unspecified
, ICD10["I02"] # Rheumatic chorea
, ICD10["I02.0"] # Rheumatic chorea with heart involvement
...]
默认情况下,这两种方法在它们返回的列表中包含初始概念。如果要避免这种情况,必须使用可选参数include_self = False
,例如:
>>> ICD10["I10"].ancestor_concepts(include_self = False)
[ ICD10["I10-I15.9"] # Hypertensive diseases
, ICD10["I00-I99.9"] # Diseases of the circulatory system
]
当应用于术语对象时,descendant_concepts()
方法还可以浏览术语的所有概念(注意,这需要加载所有 CIM10 概念,即内存中超过 10,000 个概念,这需要一些时间!):
>>> ICD10.descendant_concepts(include_self = False)
[ ICD10["A00-B99.9"] # Certain infectious and parasitic↲ diseases
, ICD10["A00-A09.9"] # Intestinal infectious diseases
, ICD10["A00"] # Cholera
, ICD10["A00.0"] # Cholera due to Vibrio cholerae 01, biovar↲ cholerae
...]
使用 Python 函数issubclass()
可以测试一个概念是否是另一个概念的后代:
>>> issubclass(ICD10["I10"], ICD10["I00-I99.9"])
True
search()
方法允许您通过标签和同义词搜索概念。字符“*”可以用作单词末尾的通配符,并且可以包括由空格分隔的几个关键字(至于这种方法所基于的全文搜索,请参见 8.7)。例如,我们可以用一个以“高血压”开头的词来搜索所有概念:
>>> ICD10.search("hypertension*")
[ ICD10["K76.6"] # Portal hypertension
, ICD10["I15.0"] # Renovascular hypertension
, ICD10["G93.2"] # Benign intracranial hypertension
, ICD10["I10"] # Essential (primary) hypertension
, ICD10["I27.0"] # Primary pulmonary hypertension
, ICD10["I15"] # Secondary hypertension
, ICD10["I15.9"] # Secondary hypertension, unspecified
...]
同样,我们可以用一个以“高血压”开头的词和另一个以“pulmo”开头的词来搜索所有概念:
>>> ICD10.search("hypertension* pulmo*")
[ICD10["I27.0"] # Primary pulmonary hypertension
]
9.5 使用 SNOMED CT
SNOMED CT(医学系统命名法—临床术语)是比 ICD10 更丰富、更完整的医学术语。注意,如前所述,SNOMED CT 在某些国家不能自由使用。
与 ICD10 一样,我们可以访问 SNOMED CT 术语及其概念,以及标签、父代、子代、祖先和后代概念。
>>> SNOMEDCT_US = PYM["SNOMEDCT_US"]
>>> SNOMEDCT_US["45913009"]
SNOMEDCT_US["45913009"] # Laryngitis
>>> SNOMEDCT_US["45913009"].parents
[ SNOMEDCT_US["129134004"] # Inflammatory disorder of
# upper respiratory tract
, SNOMEDCT_US["363169009"] # Inflammation of specific body organs
, SNOMEDCT_US["60600009"] # Disorder of the larynx
]
>>> SNOMEDCT_US["45913009"].children
[ SNOMEDCT_US["1282001"] # Perichondritis of larynx
, SNOMEDCT_US["14969004"] # Catarrhal laryngitis
, SNOMEDCT_US["17904003"] # Hypertrophic laryngitis
...]
SNOMED CT 定义了标签(label
)和同义词(synonyms
):
>>> SNOMEDCT_US["45913009"].label
['Laryngitis']
>_>_> SNOMEDCT_US["45913009"].synonyms
['Laryngitis (disorder)']
与 ICD10 不同,SNOMED CT 允许一个概念有几个父概念:因此它是一个多轴术语。在前面的例子中,概念“喉炎”有三个父代:“炎性上呼吸道疾病”、“特定器官炎症”和“喉部疾病”。
此外,SNOMED CT 不仅限于疾病:它还描述解剖结构(器官、器官的一部分等。,称为“身体结构”或“发现部位”),形态学(也就是说,疾病的类型,“相关形态学”),活体物种,化学物质,等等。SNOMED CT 还包括连接这些不同元素的横向链接。
这个信息在概念的父类中找到,以限制的形式(类型为一些或【仅 ):
>>> SNOMEDCT_US["45913009"].is_a
[ SNOMEDCT_US["363169009"] # Inflammation of specific body organs
, SNOMEDCT_US["60600009"] # Disorder of the larynx
, SNOMEDCT_US["129134004"] # Inflammatory disorder
# of upper respiratory tract
, PYM.unifieds.some(CUI["C0023067"] # Laryngitis
), PYM.mapped_to.some(ICD10["J04.0"] # Acute laryngitis
), PYM.groups.some(<Group 22731_0> # mapped_to=Acute↲ laryngitis
), PYM.has_associated_morphology.some(SNOMEDCT_US["23583003"] #Inflammation
), PYM.groups.some(<Group 22731_1>
#has_associated_morphology=Inflammation;↲
has_finding_site=Laryngeal structure
), PYM.has_finding_site.some(SNOMEDCT_US["4596009"] # Laryngeal↲ structure
), PYM.unifieds.only(CUI["C0023067"] # Laryngitis
)]
然而,限制并不容易处理。幸运的是,Owlready 允许将它们作为类属性来访问(参见 6.3)。例如,从像喉炎这样的疾病中,我们可以获得相应的解剖结构和形态:
>>> SNOMEDCT_US["45913009"].has_finding_site
[SNOMEDCT_US["4596009"] # Laryngeal structure
]
>>> SNOMEDCT_US["45913009"].has_associated_morphology
[SNOMEDCT_US["409774005"] # Inflammatory morphology
]
get_class_properties()
方法允许您获得给定概念的所有可用属性:
>>> SNOMEDCT_US["45913009"].get_class_properties()
{PYM.mapped_to,
PYM.unifieds,
PYM.has_associated_morphology,
PYM.groups,
PYM.has_finding_site,
PYM.terminology, rdf-schema.label,
PYM.synonyms,
PYM.subset_member,
PYM.ctv3id,
PYM.type_id,
PYM.case_significance_id,
PYM.definition_status_id,
PYM.active,
PYM.effective_time}
我们在属性集中找到了注释label
和synonyms
,以及has_associated_morphology
和has_finding_site
。
当涉及几个解剖结构和/或形态时,有趣的是知道哪个形态适用于哪个解剖结构。团体允许这样做。在下面的例子中,概念“肝脾肿大”与两种解剖结构和一种形态相关联:
>>> SNOMEDCT_US["36760000"]
SNOMEDCT_US["36760000"] # Hepatosplenomegaly
>>> SNOMEDCT_US["36760000"].has_finding_site
[ SNOMEDCT_US["181268008"] # Entire liver
, SNOMEDCT_US["181279003"] # Entire spleen
]
>>> SNOMEDCT_US["36760000"].has_associated_morphology
[SNOMEDCT_US["442021009"] # Enlargement
]
我们可能想知道形态学是否与第一解剖结构(即肝脏)、第二解剖结构(即脾脏)或两者都相关。群组允许回答此问题;它们可通过group
属性获得:
>>> SNOMEDCT_US["36760000"].groups
[ <Group 18807_4> # has_finding_site=Entire liver ;
# has_associated_morphology=Enlargement
, <Group 18807_3> # has_finding_site=Entire spleen ;
#has_associated_morphology=Enlargement
, <Group 18807_0> # mapped_to=Hepatomegaly with splenomegaly,
# not elsewhere classified
]
在前面的示例中,我们有三个组:
-
第一个描述了肝脏的扩大。
-
第二个描述了脾脏的扩大。
-
第三个描述了与另一个术语的对应关系,但不包含解剖结构或形态学。
因此,在这里,形态学涉及两种解剖结构。请注意,组的确切顺序可能会有所不同:您将拥有相同的组,但顺序不一定相同。
可以单独查询每个组,例如,前面的第二个组:
>>> SNOMEDCT_US["36760000"].groups[0].get_class_properties()
{PYM.has_associated_morphology,
PYM.has_finding_site}
>>> SNOMEDCT_US["36760000"].groups[0].has_associated_morphology
[SNOMEDCT_US["442021009"] # Enlargement
]
>>> SNOMEDCT_US["36760000"].groups[0].has_finding_site
[SNOMEDCT_US["181268008"] # Entire liver
]
PyMedTermino 还允许您在另一个方向上导航,也就是说,从解剖结构或形态学出发,前往疾病。例如,我们可以得到所有涉及玻璃体的疾病如下:
>>> SNOMEDCT_US["181268008"].finding_site_of
[ SNOMEDCT_US["80660001"] # Mauriac's syndrome
, SNOMEDCT_US["93369005"] # Congenital microhepatia
, SNOMEDCT_US["192008"] # Congenital syphilitic hepatomegaly
, SNOMEDCT_US["80378000"] # Neonatal hepatosplenomegaly
...]
当然,在 SNOMED CT 中可以进行全文搜索,其工作方式与 CIM10 相同。
9.6 运用统一概念(崔)
UMLS 定义了统一的概念(CUI,概念唯一标识符),允许在术语之间导航。这些 CUI 可以与 PyMedTermino 一起导入,使用特殊的术语代码“CUI”。请注意,当仅导入某些术语时,PyMedTermino 仅导入所选术语使用的 Cui。如果您想要访问所有 Cui,您将需要导入所有 UMLS。
>>> CUI = PYM["CUI"]
unifieds
属性使得获得与任何术语概念相关联的统一概念成为可能(这里我们取 ICD10):
>>> ICD10["I10"]
ICD10["I10"] # Essential (primary) hypertension
>>> ICD10["I10"].unifieds
[CUI["C0085580"] # Essential hypertension
]
统一的概念都有一个标签和同义词(来自导入的术语,因此取决于对这些术语的选择):
>>> CUI["C0085580"].synonyms
['Essential (primary) hypertension',
'Idiopathic hypertension',
'Primary hypertension',
'Systemic primary arterial hypertension',
'Essential hypertension (disorder)']
originals
属性是unifieds
的逆属性;它允许获取与统一概念相关联的原始术语的概念;
>>> CUI["C0085580"].originals
[ SNOMEDCT_US["59621000"] # Essential hypertension
, ICD10["I10"] # Essential (primary) hypertension
]
这些统一的概念允许在术语之间导航,我们将在下一节中看到。
最后,“SRC”伪术语(来源的缩写)列出了 UMLS 和/或 PyMedTermino 的所有术语。这是一种“术语的术语”。所以,PyMedTermino 的根本概念是http://PYM/SRC/SRC
:
>>> PYM["SRC"]["SRC"]
PYM["SRC"] # Metathesaurus Source Terminology Names
>>> PYM["SRC"]["SRC"].iri
'http://PYM/SRC/SRC'
9.7 术语之间的映射
使用 UMLS 现有的链接,>>
操作符允许你从一个术语转换到另一个术语。注意,这个操作符不应该与 Python 提示>>>
混淆(三个>
字符对两个)。这种操作通常被称为“映射”、“代码转换”或“对应”。对于映射概念,如果存在 UMLS“mapped _ to”关系,PyMedTermino 会使用它们。当它们不存在时,PyMedTermino 使用统一概念(CUI)在术语之间导航。以下示例将 ICD10 概念“E11”映射到 SNOMED CT:
>>> ICD10["E11"]
ICD10["E11"] # Non-insulin-dependent diabetes mellitus
>>> ICD10["E11"] >> SNOMEDCT_US
Concepts([
SNOMEDCT_US["44054006"] # Type 2 diabetes mellitus
])
这里,ICD10 概念“E11”对应于 SNOMEDCT 概念“44054006”,两者都代表二型糖尿病。概念 CIM 10“E11”没有“mapped_to”关系;我们可以验证如下:
>>> ICD10["E11"].mapped_to
[]
因此,Cui 用于执行映射。
我们也可以反方向映射,从 SNOMED CT 到 ICD10:
>>> SNOMEDCT_US["44054006"] >> ICD10
Concepts([
ICD10["E11.9"] # Non-insulin-dependent diabetes mellitus↲ without complications
])
我们注意到获得的概念不是我们以前在 IC D10(“E11”,非胰岛素依赖型糖尿病)中获得的概念。实际上,SNOMED CT 认为没有任何并发症说明的一般概念“二型糖尿病糖尿病”对应于没有并发症的糖尿病。UMLS 在 SNOMED CT 概念上有一个“映射到”关系,我们可以验证如下:
>>> SNOMEDCT_US["44054006"].mapped_to
[ICD10["E11.9"] # Non-insulin-dependent diabetes mellitus↲
without complications
]
从 SNOMED CT 映射到 ICD10 时,PyMedTermino 使用了此关系。
映射总是返回一组概念(在下一节中描述)。当起始概念对应于到达术语中的几个概念时,该集合可以包含几个概念,如下例所示:
>>> ICD10["N80.0"]
ICD10["N80.0"] # Endometriosis of uterus
>>> ICD10["N80.0"] >> SNOMEDCT_US
Concepts([
SNOMEDCT_US["784314006"] # Uterine adenomyosis
, SNOMEDCT_US["76376003"] # Endometriosis of uterus
, SNOMEDCT_US["237115002"] # Endometriosis of myometrium
, SNOMEDCT_US["198247003"] # Endometriosis interna
])
9.8 操作概念集
PYM.Concepts
类用于创建一组概念。这个类继承自 Python 的set
类(参见 2.4.7 ),因此具有相同的方法来计算两个集合的交集、并集、减法等。它为术语添加了特定的方法。例如,lowest_common_ancestors()
方法允许计算几个概念最接近的共同祖先:
>>> PYM.Concepts([ICD10["E11.1"], ICD10["E12.0"]]).lowest_↲common_ancestors()
Concepts([
ICD10["E10-E14.9"] # Diabetes mellitus
])
这种方法适用于“概括”几个概念,并将它们组合成一个更高层次的概念。
find()
方法使得寻找一个集合中的第一个概念成为可能,该集合是一个给定概念(包括概念本身)的后代。例如,我们可以创建一组四个概念:
>>> cs = PYM.Concepts([
... SNOMEDCT_US["49260003"], SNOMEDCT_US["371438008"],
... SNOMEDCT_US["373137001"], SNOMEDCT_US["300562000"],
... ])
>>> cs
Concepts([
SNOMEDCT_US["300562000"] # Genitourinary tract problem
, SNOMEDCT_US["373137001"] # Immobile heart valve
, SNOMEDCT_US["49260003"] # Idioventricular rhythm
, SNOMEDCT_US["371438008"] # Urolith
])
然后,我们可以搜索心脏概念的存在(这里,301095005 是“心脏发现”的 SNOMED CT 代码):
>>> cs.find(SNOMEDCT_US["301095005"])
SNOMEDCT_US["373137001"] # Immobile heart valve
extract()
方法类似,但是返回从作为参数传递的概念开始的所有概念的子集,例如,在这里,所有心脏概念:
>>> cs.extract(SNOMEDCT_US["301095005"])
Concepts([
SNOMEDCT_US["373137001"] # Immobile heart valve
, SNOMEDCT_US["49260003"] # Idioventricular rhythm
])
subtract()
方法返回一个包含相同概念的新集合,除了那些从参数中传递的概念派生的概念。subtract_update()
方法执行相同的操作,但是修改了传入的参数集,而不是返回一个新的。
keep_most_generic()
和keep_most_specific()
方法分别只允许保留最通用或最具体的概念。在下面的例子中,概念 SNOMED CT 300562000(“泌尿生殖道问题”)已被删除,因为它没有 371438008(“尿结石”)具体:
>>> cs.keep_most_specific()
>>> cs
Concepts([
SNOMEDCT_US["373137001"] # Immobile heart valve
, SNOMEDCT_US["371438008"] # Urolith
, SNOMEDCT_US["49260003"] # Idioventricular rhythm
])
all_subsets()
方法返回集合中所有可能的子集,例如:
>>> cs = PYM.Concepts([
... SNOMEDCT_US["49260003"],
... SNOMEDCT_US["371438008"],
... SNOMEDCT_US["373137001"],
... ])
>>> cs.all_subsets()
[Concepts([
]), Concepts([
SNOMEDCT_US["373137001"] # Immobile heart valve
]), Concepts([
SNOMEDCT_US["371438008"] # Urolith
]), Concepts([
SNOMEDCT_US["373137001"] # Immobile heart valve
, SNOMEDCT_US["371438008"] # Urolith
]), Concepts([
SNOMEDCT_US["49260003"] # Idioventricular rhythm
]), Concepts([
SNOMEDCT_US["373137001"] # Immobile heart valve
, SNOMEDCT_US["49260003"] # Idioventricular rhythm
]), Concepts([
SNOMEDCT_US["49260003"] # Idioventricular rhythm
, SNOMEDCT_US["371438008"] # Urolith
]), Concepts([
SNOMEDCT_US["373137001"] # Immobile heart valve
, SNOMEDCT_US["49260003"] # Idioventricular rhythm
, SNOMEDCT_US["371438008"] # Urolith
])]
方法is_semantic_subset()
、is_semantic_superset()
、is_semantic_disjoint()
和semantic_intersection()
类似于 Python 集合的同名方法,但是它们考虑了概念之间的层次关系。在下面的例子中,两个集合的交集是空的,但不是语义交集,因为尿结石是一个泌尿问题:
>>> cs1 = PYM.Concepts([SNOMEDCT_US["371438008"]])
>>> cs2 = PYM.Concepts([SNOMEDCT_US["106098005"]])
>>> cs1
Concepts([
SNOMEDCT_US["371438008"] # Urolith
])
>>> cs2
Concepts([
SNOMEDCT_US["106098005"] # Urinary system finding
])
>>> cs1.intersection(cs2)
Concepts([
])
>>> cs1.semantic_intersection(cs2)
Concepts([
SNOMEDCT_US["371438008"] # Urolith
])
但是,要小心,这些语义操作没有考虑到概念的可能的共同后代。在下面的例子中,“传染病”和“泌尿系统问题”这两个概念的交集是空的,而尿路感染确实存在:
>>> cs1 = PYM.Concepts([SNOMEDCT_US["40733004"]])
>>> cs2 = PYM.Concepts([SNOMEDCT_US["106098005"]])
>>> cs1
Concepts([
SNOMEDCT_US["40733004"] # Disorder due to infection
])
>>> cs2
Concepts([
SNOMEDCT_US["106098005"] # Urinary system finding
])
>>> cs1.semantic_intersection(cs2)
Concepts([
])
我们将在后面(10.7)看到如何实现一个真正的语义交集,它考虑到了共同的后代。
PyMedTermino 还允许映射一组概念,总是使用运算符“>>
”。由于映射操作本身返回一组概念,因此可以将这些操作链接起来。例如,我们可以通过强制通过 CUI 从 SNOMED CT 映射到 CIM10,如下所示:
>>> SNOMEDCT_US["44054006"] >> CUI >> ICD10
Concepts([
ICD10["E11"] # Non-insulin-dependent diabetes mellitus
])
相反,当“mapped_to”关系出现在 UMLS 时,直接映射(如前所述)可能返回不同的结果:
>>> SNOMEDCT_US["44054006"] >> ICD10
Concepts([
ICD10["E11.9"] # Non-insulin-dependent diabetes
# mellitus without complications
])
通过 CUI 保证了可逆映射。
9.9 将所有术语导入 UMLS
当terminologies
参数丢失时,import_umls()
函数导入 UMLS 中存在的所有术语。因此,我们可以按如下方式导入所有 UMLS(注意,这至少需要 20 GB 的磁盘空间、16 GB 的 RAM 和一个多小时):
>>> from owlready2 import *
>>> from owlready2.pymedtermino2 import *
>>> from owlready2.pymedtermino2.umls import *
>>> default_world.set_backend(filename = "pymedtermino.sqlite3",
... sqlite_tmp_dir = "/home/jiba/tmp")
>>> import_umls("umls-2020AA-full.zip")
>>> PYM = get_ontology("http://PYM/").load()
注意,当调用set_backend()
方法时,我们添加了可选的sqlite_tmp_dir
参数,该参数指示存储大型临时文件的目录路径(见 8.8.1)。
然后,要一次搜索所有术语中的概念,可以使用PYM.search()
方法:
>>> PYM.search("hypertension*")
[ SNOMEDCT_US["123800009"] # Goldblatt hypertension
, SNOMEDCT_US["70272006"] # Malignant hypertension
, ICD10["K76.6"]# Portal hypertension
, SNOMEDCT_US["34742003"] # Portal hypertension
, SNOMEDCT_US["70995007"] # Pulmonary hypertension
, SNOMEDCT_US["28119000"] # Renal hypertension
, ICD10["I15.0"] # Renovascular hypertension
...]
9.10 示例:将细菌本体论与 UMLS 联系起来
我们现在可以把细菌的本体论和 UMLS 联系起来。为此,我们将在这个本体论的概念和的统一概念(崔)之间建立联系。由于这些是类,我们将使用 Owlready 的类属性(见 6.3)。
下面几行代码可以将三类细菌(假单胞菌、链球菌和葡萄球菌)链接到相应的 CUI(我们使用search()
进行搜索)。这些关系放在一个新的本体中,命名为“bacteria_umls.owl”。
>>> onto = get_ontology("bacteria.owl").load()
>>> onto_bacteria_umls = get_ontology("http://↲lesfleursdunormal.fr/
static/_downloads/bacteria_umls.owl")
>>> CUI = PYM["CUI"]
>>> with onto_bacteria_umls:
... onto.Pseudomonas .mapped_to = [ CUI["C0033808"] ]
... onto.Streptococcus .mapped_to = [ CUI["C0038402"] ]
... onto.Staphylococcus.mapped_to = [ CUI["C0038170"] ]
>>> onto_bacteria_umls.save("bacteria_umls.owl")
我们已经为我们的关系重用了 UMLS mapped_to object 属性。
我们可以验证它确实是一个类属性,也就是说,一个 OWL 限制:
>>> onto.Pseudomonas.mapped_to
[CUI["C0033808"] # Pseudomonas
]
>>> onto.Pseudomonas.is_a
[bacteria.Bacterium,
bacteria.has_shape.some(bacteria.Rod),
bacteria.has_shape.only(bacteria.Rod),
bacteria.has_grouping.some(bacteria.Isolated | bacteria.InPair),
bacteria.gram_positive.value(False),
PYM.mapped_to.some(CUI["C0033808"] # Pseudomonas
)]
例如,可以将这些 Cui 映射到 SNOMED CT:
>>> SNOMEDCT_US = PYM["SNOMEDCT_US"]
>>> onto.Pseudomonas.mapped_to[0] >> SNOMEDCT_US
Concepts([
SNOMEDCT_US["5274006"] # Chryseomonas
, SNOMEDCT_US["57032008"] # Pseudomonas
])
这里,UMLS 的“假单胞菌”概念对应于 SNOMED CT 中的假单胞菌,但也对应于 Chryseomonas,一种后来被归入假单胞菌属的细菌属(命名为绿脓假单胞菌)。
还可以将统一概念 CUI 翻译成 SNOMED CT,然后通过“causative_agent_of”关系(与“has_causative_agent”相反)恢复相关疾病:
>>> diseases = [
... disease
... for snomedct in onto.Pseudomonas.mapped_to[0] >>↲ SNOMEDCT_US
... for disease in snomedct.causative_agent_of
... ]
>>> diseases
[ SNOMEDCT_US["127201000119101"] # Septic shock co-occurrent
# with acute organ dysfunction due to Pseudomonas
, SNOMEDCT_US["16664009"] # Malignant otitis media
, SNOMEDCT_US["448813005"] # Sepsis due to Pseudomonas
...]
这给了我们一个由细菌引起的疾病列表。
9.11 示例:多术语浏览器
在 Python 终端中用 PyMedTermino 进行医学术语的咨询是完全可能的;然而,这很快就变得费力了。因此,我们将建立一个多术语的“迷你浏览器”,既可以通过关键字搜索概念,也可以在各种术语中导航。该浏览器将使用 Python Flask
模块制作一个动态网站(见 4.12),并将整合 PyMedTermino 中所有可用的术语。
以下程序描述了多术语“迷你浏览器”:
# File termino_browser.py
from owlready2 import *
default_world.set_backend(filename = "pymedtermino.sqlite3")
PYM = get_ontology("http://PYM/").load()
from flask import Flask, url_for, request
app = Flask(__name__)
def repr_concept(concept):
return """[<a href="%s">%s:%s</a>] %s""" % (
url_for("concept_page", iri = concept.iri),
concept.terminology.name,
concept.name,
concept.label.first() )
def repr_relations(entity, border = False):
if border: html = """<table style="border: 1px solid #aaa;">"""
else: html = """<table>"""
for Prop in entity.get_class_properties():
for value in Prop[entity]:
if issubclass(value, PYM.Concept):
value = repr_concept(value)
elif issubclass(value, PYM.Group):
value = repr_relations(value, True)
html += """<tr><td>%s:""" % Prop.name
html += """</td><td> %s</td></tr>""" % value
html += """</table>"""
return html
@app.route('/')
def homepage():
html ="""
<html><body>
Search in all terminologies:
<form action="/search">
<input type="text" name="keywords"/>
<input type="submit"/>
</form>
Or <a href="%s">browse the entire hierarchy</a>
</body></html>""" % url_for("concept_page", iri = "http://PYM/↲SRC/SRC")
return html
@app.route('/search')
def search_page():
keywords = request.args.get("keywords", "")
html = """<html><body>Recherche "%s":<br/>\n""" % keywords
keywords = " " .join("%s"* % word for word in keywords.↲split())
results = PYM.search(keywords)
for concept in results:
html += """%s<br/>""" % repr_concept(concept)
html += """</body></html>"""
return html
@app.route('/concept/<path:iri>')
def concept_page(iri):
concept = IRIS[iri]
html = """<html><body>"""
html += """<h2>%s</h2>""" % repr_concept(concept)
html += """<h3>Ancestor concept (except parents)</h3>"""
html += """%s<br/>""" % repr_concept(concept.terminology)
ancestors = set(concept.ancestor_concepts(include_self =↲ False))
ancestors = ancestors - set(concept.parents)
ancestors = list(ancestors)
ancestors.sort(key = lambda t: len(t.ancestor_concepts()))
for ancestor in ancestors
:
html += """%s<br/>""" % repr_concept(ancestor)
html += """<h3>Parent concepts</h3>"""
for parent in concept.parents:
html += """%s<br/>""" % repr_concept(parent)
html += """<h3>Relations</h3>"""
html += repr_relations(concept)
if not concept.name == "CUI":
html += """<h3>Child concepts</h3>"""
for child in concept.children:
html += """%s<br/>""" % repr_concept(child)
html += """</body></html>"""
return html
import werkzeug.serving
werkzeug.serving.run_simple("localhost", 5000, app)
该程序首先导入 Owlready 并使用 PyMedTermino 加载 quadstore,然后导入 Flask。然后,它创建两个效用函数:
-
repr_concept()
,它将用于在 HTML 中表示一个概念,使用它的标签、术语和代码,并带有一个到概念页面的链接。 -
repr_relations()
,它将用于在 HTML 中表示概念或组的(非层次)关系。该函数返回一个 HTML 表,每个属性和属性值对应一行。这个函数是递归的:如果为一个概念调用它,如果必要的话,它将为这个概念的每个组调用它自己。
然后,程序创建三个网页:
-
根页面(路径“/”),它提供了一个搜索字段和一个到 PyMedTermino 的根概念的链接。
-
搜索页面(路径)/搜索?keywords = entered _ keywords”),它列出了文本搜索的结果。这个页面的工作方式类似于我们为 DBpedia 创建的页面(见 8.8.2)。
-
概念页面(路径“/概念/概念 _IRI”),显示给定概念的属性:祖先概念(不包括父概念)、父概念、关系和子概念。
为了方便阅读,我们去掉了祖先的父母,我们按照祖先自己拥有的祖先概念的数量对祖先列表进行了排序。这允许在列表的开始具有较少祖先的概念,因此是最一般的,而在列表的底部是最具体的。
此外,对于“CUI”分类,子概念的显示已被停用,因为它没有层次结构。因此,所有 Cui(超过 20,000 个)都是该分类的直接子分类,这会导致页面太长!
以下截图显示了最终的术语浏览器:
9.12 摘要
在本章中,您已经学习了如何从 UMLS 导入医学术语,以及如何将它们作为本体来访问。我们已经看到了如何将概念从一个术语映射到另一个术语,以及如何设计一个简单的术语浏览器。
十、混合 Python 和 OWL
在这一章中,我们将看到如何在同一个类中混合 Python 方法和 OWL 逻辑构造函数。
10.1 向 OWL 类添加 Python 方法
有了 Owlready,OWL 类(几乎)和其他类一样是 Python 类。因此,在这些类中包含 Python 方法是可能的。下面是一个简单的例子,根据药品的单价(每盒)和盒中的药片数量来计算每片药品的价格:
>>> from owlready2 import *
>>> onto = get_ontology("http://test.org/drug.owl#")
>>> with onto:
... class Drug(Thing): pass
...
... class price(Drug >> float, FunctionalProperty): ... pass
... class nb_tablet(Drug >> int, FunctionalProperty): ... pass
...
... class Drug(Thing):
... def get_price_per_tablet(self):
... return self.price / self.nb_tablet
请注意,药品类定义了两次:第一次定义是一个前向声明,以便能够在属性定义中使用该类(参见 5.8)。还要注意,由于我们创建了一个新的本体,我们在本体 IRI 的末尾集成了分隔符(这里是# )(参见 5.1)。
然后可以对该类的个体调用该方法:
>>> my_drug = Drug(price = 10.0, nb_tablet = 5)
>>> my_drug.get_price_per_tablet()
2.0
在本体中,通常只使用类和子类,而不使用个体(例如在基因本体中就是这种情况),因为类表示的能力更强。在这种情况下,Python 允许您定义“类方法”,这些方法将在类(或它的一个子类)上调用,并将该类(或子类)作为第一个参数。
下面是与前面相同的示例,但是使用了类:
>>> with onto:
... class Drug(Thing): pass
...
... class price(Drug >> float, FunctionalProperty): ... pass
... class nb_tablet(Drug >> int, FunctionalProperty): ... pass
...
... class Drug(Thing):
... @classmethod
... def get_price_per_tablet(self):
... return self.price / self.nb_tablet
然后可以在该类及其子类上调用该方法:
>>> class MyDrug(Drug): pass
>>> MyDrug.price = 10.0
>>> MyDrug.nb_tablet = 5
>>> MyDrug.get_price_per_tablet()
2.0
但是,要小心,要使两种类型的方法(个体和类)共存在一起;有必要使用不同的方法名。
10.2 将 Python 模块与本体相关联
当本体不是完全用 Python 创建的(如我们在前面的例子中所做的那样),而是从 OWL 文件中加载时,Python 方法可以在单独的 Python 模块(.py
文件)中定义。这个文件可以手动导入或者通过注释链接到本体;在这种情况下,Owlready 将在加载本体时自动导入 Python 模块。
例如,以下名为“细菌. py”的文件在细菌本体的细菌和葡萄球菌类中添加了一个方法:
# File bacteria.py
from owlready2 import *
onto = get_ontology("http://lesfleursdunormal.fr/static/↲
_downloads/bacteria.owl#")
with onto:
class Bacterium(Thing):
def my_method(self):
print("It is a bacterium!")
class Staphylococcus(Thing):
def my_method(self):
print("It is a staphylococcus!")
请注意,我们没有加载细菌本体(即,我们没有调用.load()
),因为它将由主程序来完成。还要注意,我们没有指出葡萄球菌的超类(即细菌):事实上,它已经出现在 OWL 文件中,所以没有必要在这里第二次断言它!另一方面,有必要将 Thing 作为一个超类来说明这个新类是由 Owlready 管理的 OWL 类,而不是普通的 Python 类。一般来说,当用方法创建一个单独的 Python 文件时,最好只把方法放在里面,而保留其余的本体(超类、属性、关系、等)。)以猫头鹰来限制冗余。
手动导入
然后,我们可以加载本体并手动导入文件“bacteria.py”:
>>> from owlready2 import *
>>> onto = get_ontology("bacteria.owl").load()
>>> import bacteria
然后,我们创造了一个葡萄球菌,我们称我们的方法为:
>>> my_bacterium = onto.Staphylococcus()
>>> my_bacterium.my_method()
It is a staphylococcus!
自动导入
为此,有必要用 Protégé编辑本体,并添加一个注释,指明相关 Python 模块的名称。这个标注叫做 python_module,在本体“owlready_ontology.owl”中定义,需要导入。以下是步骤:
-
启动 Protégé,加载细菌本体。
-
转到 Protégé的“活动本体”选项卡。
-
Import the ontology “owlready_ontology” by clicking the “+” button to the right of “Direct imports”. The ontology can be imported from the local copy which is in the installation directory of Owlready or from its IRI:
www.lesfleursdunormal.fr/static/_downloads/owlready_ontology.owl
.图 10-1
Protégé中的“python_module”注释
-
在“本体头”部分添加注释。标注类型为“python_module”,值为模块的名称,这里是细菌(见图 10-1 )。您也可以使用 Python 包,例如“my_module.my_package”。
现在,我们不再需要导入“细菌”模块:owl 已经在每次加载细菌本体时自动完成了这项工作。在下面的例子中,我们将细菌本体(带有注释“python_module”)保存在一个名为“bacteria_owl_python.owl”的新 OWL 文件中:
>>> from owlready2 import *
>>> onto = get_ontology("bacteria_owl_python.owl").load()
>>> my_bacterium = onto.Staphylococcus()
>>> my_bacterium.my_method()
It is a staphylococcus!
10.3 带类型推断的多态性
我们在 7.2 节中已经看到,在推理过程中,个体的类和类的超类可以被修改。在这种情况下,可用的方法可能会改变。此外,在多态的情况下,也就是说,当几个类不同地实现同一方法时,给定个体或类的方法实现可能会改变。这就是“带类型推断的多态性”。
这里有一个简单的例子:
>>> my_bacterium = onto.Bacterium(gram_positive = True,
... has_shape = onto.Round(),
... has_grouping = [onto.InCluster()] )
>>> my_bacterium.my_method()
It is a bacterium!
我们创造了一种细菌。当我们执行该方法时,它是类 Bacterium 的实现,因此被称为。我们现在叫推理者。
>>> sync_reasoner()
推理者推断该细菌实际上是一种葡萄球菌(由于其亲缘关系)。现在,如果我们调用方法,它是葡萄球菌类的实现,被称为:
>>> my_bacterium.my_method()
It is a staphylococcus!
10.4 自省
自省是一种高级的对象编程技术,它包括在不知道对象的情况下“分析”对象,例如,为了获得其属性及其值的列表。
对于个体的内省,get_properties()
方法允许获得个体至少有一个关系的属性列表。
>>> onto.unknown_bacterium.get_properties()
{bacteria.has_shape,
bacteria.has_grouping,
bacteria.gram_positive,
bacteria.nb_colonies}
然后有可能获得和/或修改这些关系。getattr(object, attribute)
和setattr(object, attribute, value)
Python 函数允许您读取或写入任何 Python 对象的属性,前提是该属性的名称在变量中是已知的(参见 2.9.4),例如:
>>> for prop in onto.unknown_bacterium.get_properties():
... print(prop.name, "=",
... getattr(onto.unknown_bacterium, prop.python_name))
has_grouping = [bacteria.in_cluster1]
has_shape = bacteria.round1
gram_positive = True
nb_colonies = 6
返回的值与通常的语法“individual.property”相同:它是功能属性的单个值和其他属性的值列表。然而,在进行自省时,更容易的是一般地对待所有属性,不管它们是否是功能性的。在这种情况下,替代语法“property[individual]”是更可取的,因为它总是返回一个值列表,即使在函数属性上调用时也是如此,例如:
>>> for prop in onto.unknown_bacterium.get_properties():
... print(prop.name, "=", prop[onto.unknown_bacterium])
has_grouping = [bacteria.in_cluster1]
has_shape = [bacteria.round1]
gram_positive = [True]
nb_colonies = [6]
对于类内省来说,get_class_properties()
方法的工作方式类似于个体的工作方式。它返回属性,对于这些属性,该类至少有一个存在限制(或者是通用的,这取决于类属性的类型;参见 6.3):
>>> onto.Pseudomonas.get_class_properties()
{bacteria.gram_positive,
bacteria.has_shape,
bacteria.has_grouping}
owl 已经考虑了父类,也考虑了等价类。语法“property[class]”可用于获取和/或修改类的存在限制。
最后,INDIRECT_get_properties()
和INDIRECT_get_class_properties()
方法以相同的方式工作,但是也返回间接属性(即,从父类继承)。
另外,constructs()
方法允许你浏览所有引用一个类或属性的构造函数。例如,我们可以查找引用 InSmallChain 类的构造函数:
>>> list(onto.InSmallChain.constructs())
[ bacteria.Bacterium
& bacteria.has_shape.some(bacteria.Round)
& bacteria.has_shape.only(bacteria.Round)
& bacteria.has_grouping.some(bacteria.InSmallChain)
& bacteria.has_grouping.only(Not(bacteria.Isolated))
& bacteria.gram_positive.value(True)]
这里,我们只得到一个构造,它是一个交集,包含一个存在性限制,以类 InSmallChain 为值。然后我们可以使用这个构造函数的subclasses()
方法来获得使用它的所有类的列表:
>>> constructor = list(onto.InSmallChain.constructs())[0]
>>> constructor.subclasses()
[bacteria.Streptococcus]
因此,我们找到了我们设置了这种限制的链球菌类别(见 3.4.8)。
10.5 向后阅读限制
这些限制使得在本体的类的级别上定义关系成为可能,例如,“假单胞菌具有 _ 形状一些杆”。Owlready 通过语法“Class.property”提供了对这些关系的简单访问(参见 4.5.4):
>>> onto.Pseudomonas.has_shape
bacteria.Rod
但如何“向后”解读这个存在限制,也就是说,从杆类,回到假单胞菌类?即使我们定义了反向属性(我们可以称之为“is_shape_of”),它也不能回答我们的问题,如下例所示:
>>> with onto:
... class is_shape_of(ObjectProperty):
... inverse = onto.has_shape
>>> onto.Rod.is_shape_of
[]
的确,从逻辑的角度来看,以下两个命题是不同的:
-
“假单胞菌有 _ 形一些杆状”
-
“杆是某些假单胞菌的形状”
第一个表明所有假单胞菌都有杆状,这是真的。第二个表示所有的杆形状都是假单胞菌的形状,这不是同一个意思(也不是真的)。例如,橄榄球的杆状形状不是假单胞菌的形状。
同样,对于以下两个命题:
-
“细胞核是某些细胞的一部分”
-
“细胞有部分细胞核”
第一个表明每个细胞核都是细胞的一部分。第二种表示每个细胞都有细胞核,这是不一样的:在生物学中,第一个命题成立,第二个命题不成立(如细菌是没有细胞核的细胞)。
然而,能够反向阅读存在关系有时是有用的。Owlready 允许这样做:这可以通过组合constructs()
和subclasses()
方法来实现,正如我们在上一节中所做的那样。inverse_restrictions()
方法自动化了这一点:
>>> set(onto.Rod.inverse_restrictions(onto.has_shape))
{bacteria.Pseudomonas, bacteria.Bacillus}
注意,在删除重复项之后,我们使用了set()
来显示由inverse_restrictions()
返回的生成器。
10.6 示例:使用基因本体和管理“部分”关系
基因本体(GO)是生物信息学中广泛使用的本体(见 4.7)。GO 由三部分组成:生物过程、分子功能和细胞成分。第三部分描述了细胞的不同组成部分:细胞膜、细胞核、细胞器(如线粒体)等等。管理起来特别复杂,因为它既包括使用 is-a 关系的“经典”继承层次,也包括“部分”关系层次。在第二个层次中,称为部分关系,单元被分解成子部分,然后分解成子部分,依此类推。因此,这个层次的根是整个细胞,叶子是不可分割的部分。
OWL 和 Owlready 有关系和方法来管理继承层次结构(subclasses()
、descendants()
、ancestors()
等)。;参见 4.5.3)。另一方面,在 Owlready 中既没有用于部分关系的标准 OWL 关系,也没有特定的方法。我们将在这里看到如何在 GO 类中添加访问子部分和超部分的方法,同时考虑部分和 is-a 关系。
GO 相当大(几乎 200 MB),加载需要几十秒甚至几分钟,这取决于计算机的能力和 OWL 文件的下载时间。因此,我们将加载 GO 并将 Owlready quadstore 存储在一个文件中(参见 4.7)。此外,我们将在这里使用手动导入将我们的 Python 方法与 OWL 类关联起来(参见 10.2.1),这样就不必通过添加“python_module”注释来修改 GO。
GO 使用人类无法直接理解的任意标识符。下表总结了我们以后需要的 GO 标识符:
|GO identifier
|
标签
|
| --- | --- |
| GO_0005575 | 细胞 _ 成分 |
| BFO_0000050 | 的一部分 |
| BFO_0000051 | has_part |
# File go_part_of.py
from owlready2 import *
default_world.set_backend(filename = "quadstore.sqlite3")
go = get_ontology("http://purl.obolibrary.org/obo/go.owl#").↲load()
obo = go.get_namespace("http://purl.obolibrary.org/obo/")
default_world.save()
def my_render(entity):
return "%s:%s" % (entity.name, entity.label.first())
set_render_func(my_render)
with obo:
class GO_0005575(Thing):
@classmethod
def subparts(self):
results = list(self.BFO_0000051)
results.extend(self.inverse_restrictions↲(obo.BFO_0000050))
return results
@classmethod
def transitive_subparts(self):
results = set()
for descendant in self.descendants():
results.add(descendant)
for subpart in descendant.subparts():
results.update(subpart.transitive_↲subparts())
return results
@classmethod
def superparts(self):
results = list(self.BFO_0000050)
results.extend(self.inverse_restrictions(obo.↲BFO_0000051))
return results
@classmethod
def transitive_superparts(self):
results = set()
for ancestor in self.ancestors():
if not issubclass(ancestor, GO_0005575):↲ continue
results.add(ancestor)
for superpart in ancestor.superparts():
if issubclass(superpart, GO_0005575):
results.update(superpart.transitive_↲superparts())
return results
该模块在类 GO_0005575(即 cellular_component)中定义了四个类方法。subparts()
允许获取组件的所有子零件。该方法考虑了关系 BFO_0000051 (has-part)以及关系 BFO_0000050 (part-of)的反向读取,这与我们使用。间接 _BFO_0000051(见 6.3)。transitive_subparts()
方法以传递的方式返回子部分,考虑了子类和传递性(如果 A 是 B and B 的子部分,是 C 的子部分,那么 A 也是 C 的子部分)。对于超级零件,superparts()
和transitive_superparts()
方法的工作方式相同。
然后,我们可以导入该模块并访问 GO 和“部分”关系。在下面的例子中,我们正在查看核仁的部分关系,核仁是位于细胞核中的一个组件:
>>> from owlready2 import *
>>> from go_part_of import *
>>> nucleolus = go.search(label = "nucleolus")[0]
>>> print(nucleolus.subparts())
[GO_0005655:nucleolar ribonuclease P complex,
GO_0030685:nucleolar preribosome,
GO_0044452:nucleolar part,
GO_0044452:nucleolar part,
GO_0101019:nucleolar exosome (RNase complex)]
>>> print(nucleolus.superparts())
[GO_0031981:nuclear lumen]
这里,直接关系(不考虑传递性)不太能提供信息。传递关系要丰富得多:
>>> nucleolus.transitive_subparts()
{GO_0034388:Pwp2p-containing subcomplex of 90S preribosome,
GO_0097424:nucleolus-associated heterochromatin,
GO_0005736:DNA-directed RNA polymerase I complex,
GO_0005731:nucleolus organizer region,
GO_0101019:nucleolar exosome (RNase complex),
[...] }
>>> nucleolus.transitive_superparts()
{GO_0031981:nuclear lumen,
GO_0005634:nucleus,
GO_0043226:organelle,
GO_0044464:cell part,
GO_0005623:cell
,
GO_0005575:cellular_component,
[...] }
10.7 示例:蛋白质的“约会网站”
现在,我们将使用“go_part_of.py”模块的功能为蛋白质创建一个“约会网站”。这个网站允许你输入两个蛋白质的名字,这个网站决定了它们可以在细胞的哪个区域相遇(如果相遇是可能的!).从生物学的角度来看,这很重要,因为没有共同“交汇点”的两种蛋白质无法相互作用。
为此,我们将使用
-
Flask Python 模块制作一个动态网站(见 4.12)。
-
MyGene Python 模块,用于在 MyGene 服务器上执行搜索,并检索与这两种蛋白质相关联的 GO 概念。这个模块允许你搜索基因(和它们编码的蛋白质)。MyGene 的用法如下:
import mygene
mg = mygene.MyGeneInfo()
dico = mg.query(’name:"<gene_name>"’,
fields = "<searched fields>",
species = "<species>",
size = <number of genes to search for>)
对 MyGene 的调用返回一个字典,该字典本身包含列表和其他字典。例如,我们可以搜索与胰岛素相关的所有 GO 术语,如下所示:
>>> import mygene
>>> mg = mygene.MyGeneInfo()
>>> dict = mg.query(’name:"insulin"’,
... fields = "go.CC.id,go.MF.id,go.BP.id,"
... species = "human",
... size = 1)
>>> dict
{’max_score’: 13.233688, ’took’: 17, ’total’: 57,
’hits’: [{’_id’: ’3630’, ’_score’: 13.233688,
’go’: {’BP’: [{’id’: ’GO:0002674’},
{’id’: ’GO:0006006’}, [...] ]}}]}
“去吧。CC.id”,“去吧。MF.id”和“go。BP.id”代表 GO 的三个主要部分(分别是细胞成分、分子功能和生物过程)。对于我们的交友网站,我们只会用“CC”。虽然它们来源于基因本体论,但它们实际上描述了基因产物在细胞中的定位,即蛋白质(一般而言),而不是基因本身(对于真核细胞,基因通常保留在细胞核中)。
更多信息请访问 MyGene 网站:
http://docs.mygene.info/en/latest/
- Owlready 和基因本体(GO)来生成描述两种蛋白质的细胞区室的 GO 术语的语义交集。一个“简单的”交集(在术语的集合意义上)是不够的:交集必须考虑继承的“是-a”关系和“部分-的”关系。例如,只存在于膜中的蛋白质 A 和只存在于线粒体中的蛋白质 B 可以在线粒体的膜中相遇。的确,线粒体膜是一种膜,它是线粒体的一部分,如下图所示:
下面的程序描述了蛋白质年代测定网站。它从导入和初始化所有模块开始:
-
已经准备好了
-
我们在上一节中创建的“go_part_of”模块
-
瓶
-
我的基因
然后,定义search_protein()
函数。它将一个蛋白质名称(英文)作为输入,例如“insulin”,并返回 MyGene 中与之相关的细胞成分类型(“CC”)的所有 GO 术语。为此,我们检查至少找到一个结果(在英语中为 hit ),然后我们得到“CC”。如果只找到一个 CC,MyGene 返回它;否则,它就是一个列表。为了便于处理,我们系统地创建了一个名为cc
的列表。然后我们遍历这个列表,提取 go 标识符。MyGene 返回的标识符的格式是“GO: 0002674 ”,而不是 OWL 版本的 GO 中的“GO_0002674”。所以我们把所有的“:”都换成“_”。最后,我们使用obo
名称空间(从 go_part_of 模块导入)恢复相应本体的概念。
semantic_intersection()
函数分四步执行包含细胞成分 GO 概念的两个集合的语义交集:
-
我们创建了两个集合,subparts1 和 subparts2,包含与这两个蛋白质中的每一个相关联的成分以及它们的子部分。为此,我们重用了静态方法 transitive_subparts(),它是我们在上一节的 go_part_of.py 模块中定义的。然后,考虑到 is-a 和 part-of 关系,我们有了两种蛋白质中每一种都能遇到的所有成分的集合。
-
我们用操作符“&”计算这两个集合的交集(Python 中的集合见 2.4.7),我们称结果为
common_components
。 -
我们现在必须简化
common_components
集合。事实上,它包括我们正在寻找的概念,也包括它们的所有后代和子部分(在前面的例子中有“膜”和“线粒体”,因此我们有“线粒体的膜”,也有“线粒体的内膜”和“线粒体的外膜”)。为了加快下一步的处理,我们首先创建一个缓存(使用字典)。该缓存将common_components
中的每个 GO 概念与其所有(可传递的)子部分相匹配。 -
我们创建一个新的集合,
largest_common_components
,它在开始时是空的。我们给它添加了所有的概念common_components
,它不是common_components
中另一个概念的子部分。请注意“for”循环中“else”的使用,它允许您在循环迭代完所有项时执行指令(也就是说,没有遇到“break”;参见 2.6)。最后,我们返回largest_common_components
。
程序的其余部分用 Flask 定义了两个网页。第一个(路径“/”)是一个基本表单,有两个文本字段用于输入蛋白质的名称,还有一个按钮用于验证。第二个(path)/result”)计算并显示结果。它首先调用两次search_protein()
函数,每个蛋白质调用一次,然后调用semantic_intersection()
函数。最后,它生成一个网页,显示与第一种蛋白质、第二种蛋白质相关的成分,以及它们可能相遇的成分。
# File dating_site.py
from owlready2 import *
from go_part_of import *
from flask import Flask, request
app = Flask(__name__)
import mygene
mg = mygene.MyGeneInfo()
def search_protein(protein_name):
r = mg.query(’name:"%s"’ % protein_name, fields =↲"go.CC.id", sspecies = "human", size = 1)
if not "go" in r["hits"][0]: return set()
cc = r["hits"][0]["go"]["CC"]
if not isinstance(cc, list): cc = [cc]
components = set()
for dict in cc:
go_id = dict["id"]
go_term = obo[go_id.replace(":", "_")]
if go_term: components.add(go_term)
return components
def semantic_intersection(components1, components2):
subparts1 = set()
for component in components1:
subparts1.update(component.transitive_subparts())
subparts2 = set()
for component in components2:
subparts2.update(component.transitive_subparts())
common_components = subparts1 & subparts2
cache = { component: component.transitive_subparts()↲
for component in common_components }
largest_common_components = set()
for component in common_components:
for component2 in common_components:
if (not component2 is component) and↲
(component in cache[component2]): break
else:
largest_common_components.add(component)
return largest_common_components
@app.route(’/’)
def entry_page():
html = """
<html><body>
<form action="/result">
Protein 1: <input type="text" name="prot1"/><br/><br/>
Protein 2: <input type="text" name="prot2"/><br/><br/>
<input type="submit"/>
</form>
</body></html>"""
return html
@app.route(’/result’)
def result_page():
prot1 = request.args.get("prot1", " ")
prot2 = request.args.get("prot2", " ")
components1 = search_protein(prot1)
components2 = search_protein(prot2)
common_components = semantic_intersection(components1,↲ components2)
html = """<html><body>"’"
html += """<h3>Components for protein #1 (%s)</h3>""" % prot1
if components1:
html += "<br/>".join(sorted(str(component)↲
for component in components1))
else:
html += "(none)<br/>"
html += """<h3>Components for protein #2 (%s)</h3>""" %↲ prot2
if components2:
html += "<br/>".join(sorted(str(component)↲
for component in components2))
else:
html += "(none)<br/>"
html += """<h3>Possible dating sites</h3>"""
if common_components:
html += "<br/>".join(sorted(str(component)↲
for component in common_components))
else:
html += "(none)<br/>"
html += """</body></html>"""
return html
import werkzeug.serving
werkzeug.serving.run_simple("localhost", 5000, app)
为了测试我们的约会网站,这里有一些蛋白质名称的例子:胰蛋白酶,细胞色素 C,胰岛素,胰岛素降解酶,胰岛素受体,胰高血糖素,血红蛋白,弹性蛋白酶,颗粒酶 B,核心蛋白聚糖,β-2-微球蛋白,等等。
以下截图显示了获得的交友网站及其用途:
10.8 摘要
在这一章中,你已经学习了如何混合 Python 和 OWL,以便将 Python 方法与具有丰富语义的 OWL 类相关联。我们还看到了如何对 OWL 类和实体进行自省。
十一、使用 RDF 三元组和世界
在这一章中,我们将看到如何直接访问 Owlready 的 RDF quadstore,以及如何创建几个隔离的“世界”,每个世界都有自己的 quadstore。
11.1 RDF 三元组
RDF(资源描述框架)是资源和元数据的形式化描述的图形模型。特别是,任何 OWL 本体都可以用 RDF 图的形式来表达。RDF 图由一组 RDF 三元组(主语、谓语、宾语)组成。谓词对应于 OWL 意义上的属性。在细菌的本体中,描述个体“未知 _ 细菌”的三元组的两个例子是
(http://lesfleursdunormal.fr/static/_downloads/bacteria.↲owl#unknown_bacterium,
http://www.w3.org/1999/02/22-rdf-syntax-ns#type,
http://lesfleursdunormal.fr/static/_downloads/bacteria.↲owl#Bacterium)
(http://lesfleursdunormal.fr/static/_downloads/bacteria.↲owl#unknown_bacterium,
http://lesfleursdunormal.fr/static/_downloads/bacteria.↲owl#gram_positive, true)
第一个三元组通过 RDF 谓词“类型”指示个体所属的类,第二个指示细菌的革兰氏状态。
其他更复杂的 OWL 构造函数,比如类限制,可以使用图中的几个 RDF 三元组和空白节点来描述。例如,假单胞菌类的限制“gram_positive value false”被转换成四个 RDF 三元组,如下所示:
(http://lesfleursdunormal.fr/static/_downloads/bacteria.↲owl#Pseudomonas,
http://www.w3.org/1999/02/22-rdf-syntax-ns#type,
_:7)
(_:7,
http://www.w3.org/1999/02/22-rdf-syntax-ns#type,
http://www.w3.org/2002/07/owl#Restriction)
(_:7,
http://www.w3.org/2002/07/owl#onProperty,
http://lesfleursdunormal.fr/static/_downloads/bacteria.↲owl#gram_positive)
(_:7,
http://www.w3.org/2002/07/owl#hasValue,
false)
这里,“_:7”是一个空节点(即一个匿名实体)。此节点的名称没有意义(并且可能因执行而异);只有它参与的关系才是重要的。
Owlready 允许用dump()
方法显示一个本体的所有 RDF 三元组:
>>> from owlready2 import *
>>> onto = get_ontology("bacteria.owl").load()
>>> onto.graph.dump()
<http://lesfleursdunormal.fr/static/_downloads/bacteria.owl>↲
<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>↲
<http://www.w3.org/2002/07/owl#Ontology> .
<http://lesfleursdunormal.fr/static/_downloads/bacteria.↲owl#Shape>↲
<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>↲
<http://www.w3.org/2002/07/owl#Class> .
[...]
也可以在default_world
上调用dump()
,以显示四元组存储中的所有 RDF 三元组(即default_world.graph.dump()
)
11.2 用 RDFlib 操作 RDF 三元组
11.2.1 读取 RDF 三元组
RDFlib 是一个 Python 模块,允许你操作 RDF 图和三元组。对于存储在 RDF 图中的 OWL 本体,我们可以使用 RDFlib 来操作这个图。然而,与 Owlready 不同,RDFlib 没有考虑 OWL 特有的语义,因此不允许利用 OWL 的表达能力或执行自动推理。
Owlready 使用了与 RDFlib 不同的 quadstore。但是,可以使用as_rdflib_graph()
方法使 Owlready quadstore 与 RDFlib 兼容,如下所示:
>>> from rdflib import *
>>> graph = default_world.as_rdflib_graph()
产生的 graph 对象是一个与 RDFlib 兼容的 quadstore。
RDFlib 图由三个元素组成:实体(由 URI 标识并用URIRef()
函数创建)、空白节点(用BNode()
函数创建)和数据(整数或实数、字符串等)。在名称文字下分组,并用Literal()
函数创建)。RDF 图的triples(subject, predicate, object)
方法允许您浏览三元组的子集;这三个参数中的每一个都可以取值None
,该值被视为通配符。因此,为了浏览具有给定主题的三元组集合,我们将为两个参数predicate
和object
传递值None
。
例如,我们可以显示关于 Staphylococcus 类的所有 RDF 三元组,如下所示(注意:为了便于阅读,已经添加了换行符,但是如果运行这个示例,这些换行符将不会出现在屏幕上):
>>> list(graph.triples((URIRef("http://lesfleursdunormal.fr/↲static/
_downloads/bacteria.owl#Staphylococcus"), None, None)))
[(rdflib.term.URIRef('http://lesfleursdunormal.fr/static/↲
_downloads/bacteria.owl#Staphylococcus'),
rdflib.term.URIRef('http://www.w3.org/1999/02/22-rdf-syntax-↲ns#type'),
rdflib.term.URIRef('http://www.w3.org/2002/07/owl#Class')),
(rdflib.term.URIRef('http://lesfleursdunormal.fr/static/↲
_downloads/bacteria.owl#Staphylococcus'),
rdflib.term.URIRef('http://www.w3.org/2000/01/rdf-↲schema#subClassOf'),
rdflib.term.URIRef('http://lesfleursdunormal.fr/static/↲
_downloads/bacteria.owl#Coccus')),
(rdflib.term.URIRef('http://lesfleursdunormal.fr/static/↲
_downloads/bacteria.owl#Staphylococcus'),
rdflib.term.URIRef('http://www.w3.org/2002/07/↲owl#equivalentClass'),
rdflib.term.BNode('20'))
]
11.2.2 使用 RDFlib 创建新的 RDF 三元组
RDFlib 允许您访问三元组,也可以使用add((subject, predicate, object))
方法创建新的三元组。在添加三元组时,需要指明它们将被添加到哪个本体中。这可以通过两种不同的方式实现:
-
要么以 Owlready 的方式,用一个“有本体:……”阻止:
-
或者像 RDFlib 一样,使用我们通过
get_context()
方法获得的上下文图:
>>> with onto:
... graph.add((
... URIRef("http://www.test.org/t.owl#MyClass"),
... URIRef("http://www.w3.org/1999/02/22-rdf-↲syntax-ns#type"),
... URIRef("http://www.w3.org/2002/07/↲owl#Class"),
... ))
>>> graph2 = graph.get_context(onto)
>>> graph2.add((
... URIRef("http://www.test.org/t.owl#MyClass2"),
... URIRef("http://www.w3.org/1999/02/22-rdf-↲syntax-ns#type"),
... URIRef("http://www.w3.org/2002/07/owl#Class"),
... ))
get_context()
方法将前一示例中的 Owlready 本体或其 IRI(以来自 RDFlib 的URIRef
对象的形式)作为参数,如下例所示:
>>> graph2 = graph.get_context(URIRef("http://↲lesfleursdunormal.fr
/static/_downloads/bacteria.owl"))
RDF 空白节点可以使用图来创建。BNode()方法,如下所示:
>>> with onto:
... new_blank_node = graph.BNode()
然后,空白节点可以与 RDFlib 一起使用:
>>> with onto:
... graph.add((
... URIRef("http://www.test.org/t.owl#MyClass"),
... URIRef("http://www.w3.org/1999/02/22-rdf-syntax-↲ns#type"),
... new_blank_node,
... ))
请注意,通过 RDFlib 添加 RDF 三元组可能不会更新 Owlready 中的相应对象,如果它们已经从 quadstore 加载。另一方面,如果对象还没有被加载,可以在创建后使用 RDFlib 加载。
11.2.3 使用 RDFlib 删除 RDF 三元组
最后,RDFlib 允许您用remove((subject, predicate, object))
方法删除三元组:
>>> graph.remove((
... URIRef("http://www.test.org/t.owl#MyClass"),
... URIRef("http://www.w3.org/1999/02/22-rdf-syntax-↲ns#type"),
... URIRef("http://www.w3.org/2002/07/owl#Class"),
... ))
remove()
方法接受使用None
作为主语、谓语和/或宾语的通配符。例如,我们可以删除主题为“ http://www.test.org/t.owl#MyClass2
的所有三元组,如下所示:
>>> graph.remove((
... URIRef("http://www.test.org/t.owl#MyClass2"),
... None,
... None,
... ))
再次注意,通过 RDFlib 删除 RDF 三元组可能不会更新 Owlready 中相应的对象。
11.3 执行 SPARQL 请求
SPARQL (SPARQL 协议和 RDF 查询语言)是一种用于在 RDF 图中搜索的查询语言。这种语言有点像关系数据库的 SQL(结构化查询语言),但它专用于 RDF 图数据库。
RDFlib 包含一个 SPARQL 引擎,可以与 Owlready 一起使用。
11.3.1 使用 SPARQL 搜索
SPARQL 允许您进行比 Owlready 的search()
方法更复杂的搜索;但是,对于简单的搜索,最好使用search()
,因为性能更好。
RDFlib graph 对象的query()
方法允许您执行 SPARQL 查询,并以 RDFlib 格式(也就是说,以URIRef
、BNode
和Literal
的形式)返回结果。查询的 WHERE 子句由一个或多个 RDF 三元组组成,它可以包含实体(由实体的 IRI 标识)和变量,变量的名称以“?”为前缀。在下面的例子中,我们寻找所有的实体。b 类细菌,在哪里?b 是一个变量:
>>> graph = default_world.as_rdflib_graph()
>>> list(graph.query("""
... SELECT ?b WHERE {
... ?b
... <http://www.w3.org/1999/02/22-rdf-syntax-ns#type>
... <http://lesfleursdunormal.fr/static/_downloads/bacteria.↲owl#Bacterium>.
... }""" ))
[(rdflib.term.URIRef('http://lesfleursdunormal.fr/static/↲
_downloads/bacteria.owl#unknown_bacterium'),)]
query_owlready()
方法以同样的方式工作,但是以 Owlready 格式返回结果(即作为 Owlready 对象或 Python 数据类型):
>>> list(graph.query_owlready("""
... SELECT ?b WHERE {
... ?b
... <http://www.w3.org/1999/02/22-rdf-syntax-ns#type>
... <http://lesfleursdunormal.fr/static/_downloads/bacteria.↲owl#Bacterium>.
... }""" ))
[[bacteria.unknown_bacterium]]
SPARQL 允许您执行涉及多个变量的搜索。例如,我们可以寻找所有类型为“包含”的细菌。这项研究需要两个变量,在这里注明?b(细菌)和?r(它的分组),和三个三元组。可以按如下方式完成:
>>> list(graph.query_owlready("""
... SELECT ?b WHERE {
... ?b
... <http://www.w3.org/1999/02/22-rdf-syntax-ns#type>
... <http://lesfleursdunormal.fr/static/_downloads/bacteria.↲owl#Bacterium>.
...
... ?b
... <http://lesfleursdunormal.fr/static/_downloads/bacteria.↲owl#has_grouping>
... ?r .
...
... ?r
... <http://www.w3.org/1999/02/22-rdf-syntax-ns#type>
... <http://lesfleursdunormal.fr/static/_downloads/bacteria.↲owl#InCluster>.
... }""" ))
[[bacteria.unknown_bacterium]]
最后,default_world.sparql_query()
方法是直接执行 SPARQL 搜索并以 Owlready 格式检索结果的快捷方式(与query_owlready()
一样):
>>> list(default_world.sparql_query("""..."""))
使用尖括号(<...>)之间的 IRI,可以很容易地将 Owlready 对象集成到 SPARQL 查询中,例如:
>>> individual = onto.unknown_bacterium
>>> list(default_world.sparql_query("""
... SELECT ?class WHERE {
... <%s> a ?class .
... ?class a <http://www.w3.org/2002/07/owl#Class> .
... }""" % individual.iri))
[[bacteria.Bacterium]]
这个 SPARQL 查询搜索单个“未知细菌”所属的所有类。在这个例子中,属性“a”是“<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>
”的快捷方式。
SPARQL 前缀
Owlready 和 RDFlib 还允许使用 SPARQL 前缀,以便简化 SPARQL 查询的编写并缩短 IRI。前缀是用bind()
方法声明的,如下所示:
graph.bind("prefix", "base IRI")
然后,以基本 IRI 开始的虹膜可以写成“前缀:IRI 的结束”的形式,没有尖括号。在下面的例子中,我们为 OWL 定义了一个前缀,然后我们使用这个前缀继续前面的例子:
>>> graph = default_world.as_rdflib_graph()
>>> graph.bind("owl", "http://www.w3.org/2002/07/owl#")
>>> individual = onto.unknown_bacterium
>>> list(default_world.sparql_query("""
... SELECT ?class WHERE {
... <%s> a ?class .
... ?class a owl:Class .
... }""" % individual.iri))
[[bacteria.Bacterium]]
11.3.3 使用 SPARQL 创建 RDF 三元组
“INSERT”类型的 SPARQL 查询允许创建 RDF 三元组。它们是用 RDFlib graph 对象的update()
方法执行的。和前面的 RDFlib 一样(见 11.2.2),有必要指出三元组将在哪个本体中创建。这可以通过两种不同的方式实现:
-
要么以 Owlready 的方式,用一个“有本体:……”阻止:
-
或者像 RDFlib 一样,使用我们通过
get_context()
方法获得的上下文图:
>>> with onto:
... graph.update("""
... INSERT {
... <http://www.test.org/t.owl#MyClass>
... <http://www.w3.org/1999/02/22-rdf-syntax-↲ns#type>
... <http://www.w3.org/2002/07/owl#Class> .
... } WHERE {}""")
>>> graph = default_world.as_rdflib_graph()
>>> graph2 = graph.get_context(onto)
>>> graph2.update("""
... INSERT {
... <http://www.test.org/t.owl#MyClass2>
... <http://www.w3.org/1999/02/22-rdf-syntax-ns↲#type>
... <http://www.w3.org/2002/07/owl#Class> .
... } WHERE {}""")
更复杂的查询可以包括“WHERE”部分。以下示例查找所有类(“WHERE”部分)并向它们添加注释(“INSERT”部分):
>>> graph2.update("""
... INSERT {
... ?class
... <http://www.w3.org/2000/01/rdf-schema#comment>
... "This entity is a class!."
... } WHERE {
... ?class a <http://www.w3.org/2002/07/owl#Class> .
... }
... """)
请注意,通过 SPARQL 添加 RDF 三元组可能不会更新 Owlready 中的相应对象,如果它们已经加载到 Python 中的话。
11.3.4 使用 SPARQL 删除 RDF 三元组
RDFlib graph 对象的update()
方法也允许执行“删除”查询来删除 RDF 三元组。这些请求可以包含“WHERE”部分。以下示例删除了先前添加的注释:
>>> graph2.update("""
... DELETE {
... ?class
... <http://www.w3.org/2000/01/rdf-schema#comment>
... "This entity is a class!."
... } WHERE {
... ?class a <http://www.w3.org/2002/07/owl#Class> .
... }
... """)
请注意,通过 SPARQL 删除 RDF 三元组可能不会更新 Owlready 中的相应对象,如果它们已经加载到 Python 中的话。
11.4 使用 Owlready 访问 RDF 三元组
Owlready 也有直接访问 RDF quadstore 的方法。这些方法比使用 RDFlib 更复杂,也不太标准,但它们也更快。
为了减少 quadstore 的体积,owl 已经用名为 storid (store ID)的“缩写 IRIs”替换了实体的 IRIs。这些是严格正整数形式的任意代码。_abbreviate()
和_unabbreviate()
方法分别允许将 IRI 转换成 storid 或 storid 转换成 IRI。如果 IRI 尚未收到缩写,则_abbreviate()
会自动创建一个新代码,并保存在 quadstore 中。
在下面的例子中,葡萄球菌类的 IRI 对应于 storid 324(注意:storid 的确切值可能因 quadstore 的不同而不同,这取决于本体中实体的创建顺序):
>>> default_world._abbreviate(onto.Staphylococcus.iri)
323
>>> default_world._unabbreviate(323)
'http://lesfleursdunormal.fr/static/_downloads/bacteria.owl#Staphylococcus'
任何实体的storid
属性都允许您检索其 storid:
>>> onto.Staphylococcus.storid
323
_get_by_storid()
方法允许相反的操作,也就是说,从 storid 中获取一个实体:
>>> default_world._get_by_storid(323)
bacteria.Staphylococcus
空节点在 quadstore 中也由 storid 表示,但它们是严格的负整数。
Owlready quadstore 使用传统形式的 RDF 三元组(主语、谓语、宾语)存储两个实体之间的关系。另一方面,实体和数据类型值之间的关系以(主语、谓语、值、数据类型)四元组的形式存储。该值可以是整数(Python int
类型)、实数(Python float
类型)或字符串(Python str
类型)。类型可以是表示本地化字符串语言的字符串,前缀为“@
”(例如“@en
”或“@fr
”),也可以是数据类型的 storid(参见表 4-1 中 OWL 支持的 IRIs),如果没有指定数据类型,则为 0(对应于 OWL 中的 PlainLiteral)。
_get_triples_spod_spod(subject, predicate, object_or_value, datatype)
方法的行为类似于 RDFlib 的triples()
方法。我们可以获得葡萄球菌类在 quadstore 中的三元组,如下所示:
>>> default_world._get_triples_spod_spod(323, None, None, None)
[(323, 6, 11, None),
(323, 9, 321, None),
(323, 33, -20, None)]
因为这些是对应于两个实体之间关系的三元组,所以不使用数据类型(每个元组的第四个值),它等于None
。
_get_obj_triples_spo_spo(subject, predicate, object)
和_get_data_triples_spod_spod(subject, predicate, value, type)
方法的工作方式类似于_get_triples_spod_spod()
,但是它们仅限于两个实体之间的关系(前者)以及实体和数据类型值之间的关系(后者)。
_unabbreviate()
方法可用于解码之前获得的结果:
>>> default_world._unabbreviate(6)
'http://www.w3.org/1999/02/22-rdf-syntax-ns#type'
>>> default_world._unabbreviate(11)
'http://www.w3.org/2002/07/owl#Class'
[...]
default_world._to_rdf(entity_or_data)
方法使得将任何实体或数据类型值转换成 RDF 成为可能。当对实体调用时,它返回一对(storid,None
),当对数据类型值调用时,它返回一对(value,type),如下例所示:
>>> default_world._to_rdf(8)
(8, 43)
>>> default_world._to_rdf(onto.Staphylococcus)
(323, None)
它的对应物default_world._to_python(object_or_value, datatype)
方法执行相反的操作。
>>> default_world._to_python(8, 43)
8
>>> default_world._to_python(323, None)
bacteria.Staphylococcus
_has_obj_triple_spo(subject, predicate, object)
和_has_data_triple_spod(subject, predicate, data, datatype)
方法用于验证四元组存储中 RDF 三元组的存在。例如,我们可以验证葡萄球菌类的第一个三联体的存在,如下所示:
>>> default_world._has_obj_triple_spo(323, 6, 11)
True
_del_obj_triple_spo(subject, predicate, object)
和_del_data_triple_spod(subject, predicate, data, datatype)
方法允许删除一个或多个 RDF 三元组(在参数是None
的情况下,几个三元组被删除,它充当小丑)。例如,我们可以删除葡萄球菌类的第一个三元组,如下所示:
>>> default_world._del_obj_triple_spo(323, 6, 11)
但是要小心,这些方法不会更新相应的 Owlready 对象:Staphylococcus 类在 Python 中仍然是 coccus 的子类:
>>> onto.Staphylococcus.is_a
[bacteria.Coccus]
_add_obj_triple_spo(subject, predicate, object)
和_add_data_triple_spod (subject, predicate, data, datatype)
方法添加了一个 RDF 三元组。它们必须被应用于一个本体(而不是default_world
),以便指定三元组将被插入的本体。例如,要重新创建之前删除的三元组,我们将执行以下操作:
>>> onto._add_obj_triple_spo(323, 6, 11)
同样,这两个方法不更新 Owlready Python 对象。
最后,Owlready 有许多优化的方法,用于在 quadstore 中执行特定类型的搜索。这些方法的名称遵循以下模式:
_get_<element>_triple<plural>_<inputs>_<output>()
在哪里
-
表示在 quadstore 的哪个部分进行搜索: -
=(空):用于在整个 quadstore 中搜索 -
= obj:用于搜索两个实体之间的关系 -
=数据:用于搜索实体和数据类型值之间的关系
-
-
表示返回多少结果: -
= s:返回在 quadstore 中找到的所有结果 -
= (empty):返回在 quadstore 中找到的第一个结果
-
-
表示方法的参数。它是以下字符的组合:
-
c:本体标识符
-
s:三元组的主题(一个实体的 storid 或一个空白节点)
-
p:三元组的谓词(一个属性的 storid)
-
o:三元组的对象(实体的 storid、空白节点或数据类型值)
-
d:数据类型(空字符串、数据类型的 storid 或以“
@
”为前缀的两个字母的语言代码)
-
以下是可用的优化方法:
-
_get_triples_s_p(), _get_triples_s_pod(), _get_triples_sp_od(), _get_triples_spod_spod()
-
_get_triple_sp_od()
-
_get_obj_triples_sp_o(), _get_obj_triples_sp_co(), _get_obj_triples_spo_spo(), _get_obj_triples_cspo_cspo(), _get_obj_triples_s_po( ), _get_obj_triples_po_s()
-
_get_obj_triple_po_s(), _get_obj_triple_sp_o()
-
_get_data_triples_s_pod(), _get_data_triples_sp_od(), _get_data_triples_spod_spod()
-
_get_data_triple_sp_od()
例如,_get_obj_triples_sp_o()
方法只在两个实体(“对象”)之间的关系中进行搜索;它将一个主语和一个谓语(“sp”)作为参数,并返回一个对象列表(“o”)。我们可以如下获得葡萄球菌类的父类的 storid(storid 323)(6 是典型 RDF 属性的 storid):
>>> list(default_world._get_obj_triples_sp_o(323, 6))
[11]
11.5 直接查询 SQLite3 数据库
Owlready quadstore 实现为一个 SQLite3 数据库。它包含三个主表:
-
资源,它将 IRIs 映射到 storids
-
objs,它包含两个实体之间关系的四元组
-
datas,它包含实体和数据之间关系的四元组
最后,“quads”视图是一个只读的伪表,包含来自 objs 表和 datas 表的记录(对于 objs,d = NULL
)。
下表显示了这些表的模式:
|四边形视图
|
| --- |
| rowid INTEGER | c 整数 | s 整数 | p 整数 | BLOB(肉球) | d 整数 |
约会对象
|
| --- |
| rowid INTEGER | c 整数 | s 整数 | p 整数 | BLOB(肉球) | d 整数 |
对象表
|
| --- |
| rowid INTEGER | c 整数 | s 整数 | p 整数 | o 整数 |
资源表
|
| --- |
| 斯托里德整数 | iri TEXT |
这些字段是
-
storid:quad store 中的标识符
-
鸢尾:与鹳鸟有关的 iri
-
rowid:SQL 表中的行标识符
-
c:一个本体标识符——1 代表加载的第一个本体,2 代表第二个本体,依此类推
-
学生:三元组的主题,也就是鹳鸟
-
p:三元组的谓词,即属性的 storid
-
o:三元组的对象,即 storid(用于 objs 表)、datatype 值(用于 datas 表的 integer、float 或 string)或两者中的任何一个(用于 quads 视图)
-
d:o 中值的数据类型是下列之一:
-
实体的
None
(SQL 中的NULL
)(在四元视图中) -
表示数据类型的 storid(对于 datas 或 quads 表)
-
由两个字母组成的语言代码,形式为“@langue”,例如,“
@en
”表示英语,或者“@fr
”表示法语(表示 datas 和 quads 表中的本地化文本)
-
execute()
方法允许您直接在数据库上执行 SQL 查询。例如,下面的 SQL 查询可以搜索所有类型为 InCluster 的细菌(我们已经在第 11.3.1 节的 SPARQL 中执行了该查询):
>>> default_world.graph.execute("""
... SELECT q1.s
... FROM objs q1, objs q2, objs q3
... WHERE q1.p=%s AND q1.o=%s
... AND q2.s=q1.s AND q2.p=%s
... AND q3.s=q2.o AND q3.p=%s AND q3.o=%s
... """% (rdf_type, onto.Bacterium.storid,
... onto.has_grouping.storid,
... rdf_type, onto.InCluster.storid)
... ).fetchall()
[(327,)]
>>> default_world.graph._unabbreviate(327)
'http://lesfleursdunormal.fr/static/_downloads/bacteria.owl#↲
unknown_bacterium'
这个查询使用了三次 objs 表(这对应于 SPARQL 查询的三个三元组)。
为了帮助编写 SQL 查询,可以从 Owlready 的search()
方法产生的查询中获得灵感。由search()
返回的伪列表的sql_request()
方法显示 SQL 查询和相应的参数(其值将替换“?”查询的)。这里有一个例子:
>>> default_world.search(iri = "*Bacteri*").sql_request()
('SELECT DISTINCT q1_1.s FROM objs q1_1, resources↲
WHERE resources.storid = q1_1.s AND resources.iri GLOB ?',↲
['*Bacteri*'])
11.6 添加对自定义数据类型的支持
declare_datatype()
全局函数允许在 Owlready 中声明附加的数据类型。该函数有四个参数:数据类型 Python 类、其 IRI、解析器函数和序列化器函数。serializer 函数负责数据类型的序列化,也就是说,将它转换为字符串。解析器函数负责相反的操作:它读取字符串并返回 Python 数据类型值。
以下示例添加了对“hexBinary”数据类型(在 XML-Schema 中定义)的支持。它首先创建一个名为“Hex”的 Python 类来管理十六进制值。然后,它定义解析器函数。这个函数读取一个十六进制值(以 XML-Schema 的格式)并返回一个十六进制实例。serializer 函数接受一个十六进制实例,并返回一个字符串格式的十六进制值(我们删除了前两个字符,用“[2:]
”,因为 Python 在十六进制值的开头添加了“0x
”,而 XML-Schema 没有)。最后,我们声明新的数据类型。
>>> class Hex(object):
... def __init__(self, value):
... self.value = value
>>> def parse_hex(s):
... return Hex(int(s, 16))
>>> def serialize_hex(x):
... h = hex(x.value)[2:]
... if len(h) % 2 != 0: return "%s" % h
... return h
>>> declare_datatype(Hex, "http://www.w3.org/2001/↲XMLSchema#hexBinary", parse_hex, serialize_hex)
我们现在可以创建一个本体,并在数据属性中使用十六进制值:
>>> onto = get_ontology("http://www.test.org/test_hex.owl")
>>> with onto:
... class has_hex(Thing >> Hex): pass
... class MyClass(Thing): pass
... c = MyClass()
... c.has_hex.append(Hex(14))
我们可以验证 quadstore 的内容:
>>> onto.graph.dump()
<http://www.test.org/t.owl>
<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>
<http://www.w3.org/2002/07/owl#Ontology> .
[...]
<http://www.test.org/t.owl#myclass1>
<http://www.test.org/t.owl#has_hex>
0e^^<http://www.w3.org/2001/XMLSchema#hexBinary> .
注意,该值以十六进制格式存储在 quadstore 中(这里,“0e
”是 14 的十六进制表示)。
我们也可以使用 XML 模式 hexBinary 数据类型加载本体。但是,注意在加载这样的本体之前必须调用declare_datatype()
。
11.7 创造几个孤立的世界
Owlready 使得创造几个孤立的“世界”成为可能,有时被称为“语言的宇宙”。这使得可以独立地多次加载相同的本体,也就是说,在一个副本上进行的修改不会影响另一个副本。它对于同时加载同一个本体的几个不兼容版本也很有用。
默认情况下,Owlready 只创建了一个名为default_world
的世界。World
类允许你创造一个独立于default_world
的新世界。
>>> from owlready2 import *
>>> my_world = World()
每个世界都存储在一个单独的 quadstore 中。每个都可以通过set_backend()
方法存储在 RAM 和/或磁盘的不同文件中(参见第 4.7 节)。一般来说,我们应用于default_world
的所有方法都可以应用于世界,例如search()
或as_rdflib_graph()
。此外,几个全局函数实际上是default_world
方法的快捷方式。因此,当使用多个世界时,您必须调用方法,而不是全局快捷键。以下是这些简化函数和相应方法的列表:
快捷全局功能
|
对应方法
|
| --- | --- |
| get_ontology()
| World.get_ontology()
|
| get_namespace()
| World.get_namespace()
|
| IRIS[iri]
| World[iri]
|
| sync_reasoner()
| sync_reasoner(world)
|
下面的例子说明了世界的隔离,创建一个与default_world
分离的新世界,然后在每个世界中加载细菌的本体。在default_world
中,我们删除了葡萄球菌类,但它仍然存在于另一个世界。
>>> onto = get_ontology("http://lesfleursdunormal.fr/↲
static/_downloads/bacteria.owl#").load()
>>> onto2 = my_world.get_ontology("http://lesfleursdunormal.↲fr/
static/_downloads/bacteria.owl#").load()
>>> destroy_entity(onto.Staphylococcus)
>>> onto.Staphylococcus
None
>>> onto2.Staphylococcus
bacteria.Staphylococcus
最后,OWL Thing
和Nothing
类的subclasses()
和descendants()
方法假设它们是为default_world
调用的(事实上,这些类是所有世界共享的)。如果不是这种情况,则需要将所需的世界作为参数传递,例如:
>>> list(Thing.descendants(world = my_world))
11.8 摘要
在本章中,您已经学习了如何直接访问 Owlready quadstore 中的 RDF 三元组并执行 SPARQL 查询。我们还看到了如何创建几个孤立的世界,例如,加载同一个本体的几个副本。