剑指 Offer2(Python 版)解析(Ch6)

具体实现和测试代码

系列解析(TBD):

  • Python 单例模式
  • 好玩儿的 DP
  • 递归还是递归
  • 双指针的威力
  • 双列表的威力
  • 有趣的排列组合

特别说明:下文中的实例代码一般仅包括核心算法(不一定能直接运行),完整代码可以参考对应的链接。

More

Few-Shot Charge Prediction with Discriminative Legal Attributes 论文笔记

Paper: coling2018_attribute.pdf

code: thunlp/attribute_charge

核心思想:基于类别属性的注意力机制共同学习属性感知和无属性的文本表示。

这是 COLING2018 上的一篇老论文了,最近因为一些事情正好遇上,当时大概看了一下就发现这篇文章正好解决了我之前在做多分类任务时没有解决的问题。所以拿来记录一下,顺便研究下代码。

More

关系提取简述

之前整理过一篇关于信息提取的笔记,也是基于大名鼎鼎的 SLP 第 18 章的内容,最近在做一个 chatbot 的 NLMLayer 时涉及到了不少知识图谱有关的技术,由于 NLMLayer 默认的输入是 NLU 的 output,所以实体识别(包括实体和类别)已经自动完成了。接下来最重要的就是实体属性和关系提取了,所以这里就针对这块内容做一个整理。

属性一般的形式是(实体,属性,属性值),关系的一般形式是(实体,关系,实体)。简单来区分的话,关系涉及到两个实体,而属性只有一个实体。属性提取的文章比较少,关系提取方面倒是比较成熟,不过这两者之间其实可以借鉴的。具体的一些方法其实这里已经提到不少了,这里单独提出来再梳理一遍。

More

AINLP GPU 使用体验指南

AINLP-DBC GPU 是一个 GPU 算力服务平台,采用 DBC TOKEN 进行结算。在这里可以租用 GPU,也可以将自己的 GPU 出租出去。

注册

第一步:创建钱包

这里需要输入密码,之后会产生一个加密后的私钥文件,下载继续后会产生你真正的私钥。一定要记住你的密码并在物理介质上保存好加密后的私钥文件以及你的私钥。只有通过密码+加密的私钥文件,或者私钥才能打开你的钱包,如果都丢了,就等于你的钱包没了。

第二步:充值 DBC

点击 “如何购买 DBC” 链接,选择自己喜欢的方式充值即可,推荐使用支付宝,点击 “继续” 后,充值一定金额(比如 1块或者 0.1 块)就好了。这步其实就是给你的钱包地址充值一定数额的 DBC。大概等个几十秒就能在 “我的钱包” 里看到你购买金额对应的 DBC 数量了。

第三步:绑定邮箱

点击 “绑定邮箱” 后,输入邮箱地址,会给你发送一个类似 请输入如下数量dbc:0.7311,验证有效期为30分钟 内容的邮件,将对应的额度(比如这里的 0.7311)输入 “验证的 DBC 数量” 框即可完成绑定。

第四步:选择机器

在列表中选择一台符合自己要求的机器,点击 “租用” 后,填写租用时长(最短 1 小时),等待大约 1 分钟左右(验证机器环境),确认支付后就可以正式使用了。

More

The Rust Programming Language Brief Note (Vol4-Advance)

15 Smart Pointers

Reference counting smart pointer enables you to have multiple owners of data by keeping track of the number of owners and, when no owners remain, cleaning up the data.

References are pointers that only borrow data; in contrast, in many cases, smart pointers own the data they point to.

Smart pointers are usually implemented using structs. The characteristic that distinguishes a smart pointer from an ordinary struct is that smart pointers implement the Deref and Drop traits.

  • The Deref trait allows an instance of the smart pointer struct to behave like a reference so you can write code that works with either references or smart pointers.
  • The Drop trait allows you to customize the code that is run when an instance of the smart pointer goes out of scope.

More

The Rust Programming Language Brief Note (Vol5-Project)

2 Programming Guess

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let foo = 5; // immutable 
let mut bar = 5; // mutable

io::stdin().read_line(&mut guess)
.expect("Failed to read line");

let guess: u32 = guess.trim().parse()
.expect("input a number");

let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};

match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
1
cargo doc --open // documents

More

The Rust Programming Language Brief Note (Vol3-Style)

More

自然语言记忆模块(NLM)

本文主要介绍自然语言的记忆(存储与查询)模块,初衷是作为 chatbot 的 Layer 之一,主要功能是记忆(存储)从对话或训练数据学到的 “知识”,然后在需要时唤起(查询) 。目前成熟的方法是以图数据库作为载体,将知识存储为一系列的 ”节点“ 和 ”关系“。之后再基于这些存储的 ”节点“ 和 ”关系“ 进行相关查询。也可以理解为构建 Data Model 的问题。

项目地址:https://github.com/hscspring/NLM

设计思想

图数据库的典型代表是 Neo4j,Neo4j 中有几个很重要的概念:标签、节点和关系。标签是一类节点,可以看作是节点的类别,节点一般是某一个实体;关系存在于两个实体间,可以有多种不同的关系。节点和关系可以有多个属性。实践来看,Python 语言可以使用社区的 technige/py2neo,当然还可以使用官方的 neo4j/neo4j-python-driver: Neo4j Bolt driver for Python,两者的目的都是将数据 import 进 database 并进行相应的查询。

Neo4j 的特点要求导入的数据尽量是结构化的,也就是我们要事先有实体和它的类别(实体的属性可有可无),实体与实体间的关系(关系的属性可有可无)。我们期待能从对话或无监督的语料中自动提取实体和关系,然后自动 import 进 Neo4j。为了避免导入数据的混乱,自然最好能有先验的 “类别”,比如节点类别 Person,Movie 等,关系类别 LOVES,ACTS 等。所以,对于文本输入,我们需要一个信息提取器,将文本中的符合先验类别的节点和关系提取出来。如果输入是 NLU 模块输出的 ”意图和实体“ ,则需要一个分类器,将意图分类到对应的 Relation 类别,将实体分类到 Node 类别。

接下来的问题是:“我们如何确定先验的类别?”设想当然是能包括所有可能的类别,比如我们可以在大规模语料上使用 LDA 之类的模型自动获取 topic,每个 topic 作为一个类别标签。对话中的句子使用该模型预测 topic 并在 query 无结果时加入 Database。但这样可能导致知识图谱比较泛,无法 “专注” 在特定领域。因此实际可能还是需要针对垂直领域手动设计好 Node 和 Relation 的类别。

综上所述,我们的 NLM 模块需要具备以下基本功能:

  • 批量导入结构化数据,根据设计好的 Node 和 Relation(统一在 scheme 中设计)自动创建实体和关系
  • 自动根据文本或 NLU 的输出存储或查询实体和关系(无论是否有事先设计的实体和关系类别)
    • 对于商业应用,建议事先设计好,实际就变成:
      • 将 NLU 的结果(实体和意图)自动分类到知识图谱已有的 Node 和 Relation
      • 从文本中自动提取事先设计好类别的 Node 和 Relation
    • 对于闲聊机器人,不妨让它自由进化,看看最后能成什么样子
  • 自动根据文本或 NLU 的输出 Query
  • 对 Query 结果进行解析,输出为 NLG 或 NLI 模块需要的结构
  • 模块高内聚,可以作为独立的 Layer 对外提供服务;低耦合,分类器、提取器、解析器均可以自由更换

基本流程如下:

NLU Output/TEXT => Classifier/Extractor => Graph Input => Query/Add/Update => Parser => NLG (NLI) Input

批量导入

主要是明确一下规范,这个规范是看过几个项目后的感悟,暂时没有想到更好的,等有了更好的再来调整吧。最好 Input 不依赖某个具体的数据库(如 Neo4j)。核心思想是这样的:

  • 首先假设每次导入的数据是某一个类别,而这些数据每条对应一个不重复的 item name。比如类别 “电影” 每条 item 的 name 是电影名;再比如类别 “疾病” 每条 item 的 name 是疾病名。
  • item name 相关的其他信息均作为该 item 的属性。比如某个电影的属性可能包括:上映时间、导演、演员、类别,甚至豆瓣评分都可以。
  • 每个 item name 就是一个 Node,label 自然就是类别,item 的属性是 Node 的属性,这个可以动态调整。Relation 除了两个 Node 和它的 name 外还可以有自己的属性。比如演员 Y 是一个 Person,“演员 Y act A 电影” 这是一个关系,它同时可以有一个 “角色” 的属性(即在电影里演了谁)。
  • Node 和 Relation 均通过 scheme 创建。

举个栗子,首先是数据:

1
2
3
4
5
6
# 结构化的 data
[
{"id": "1", "title": "Wall Streat", "year": "1987", "actors": ["Charlie->Bud", "Martin->Gordon"], "director": "Robot"},
{"id": "2", "title": "The Matrix", "year": "1997", "actors": ["Keanu->Neo", "Tom->Forrest"], "director": "Robot"},
...
]

接着是 scheme ,可以使用 GraphObject 来直接创建 Graph scheme 对象,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# batch scheme
class Movie(GraphObject):
__primarykey__ = "title"

title = Property()
released = Property("year")

actors = RelatedFrom(Person, "ACTED_IN")
directors = RelatedFrom(Person, "DIRECTED")

class Person(GraphObject):
__primarykey__ = "name"

name = Property()

然后将结构化的数据处理后批量导入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# execute
def batch():
for item in data:
movie = Movie()
movie.title=item["title"]
movie.released=item["year"]
director = Person()
director.name = item["director"]
movie.directors.add(director, {"name": "执导了"})
for iitem in item["actors"]:
actor = Person()
actor.name, role = iitem.split("->")
movie.actors.add(actor, {"role": role, "name": "扮演了"})
graph.push(movie)

具体可参考这里的例子。

实时处理

从 NLU Output 或文本到 Graph Input 这步一般就是深度学习模型 + 传统的信息提取方法 + Naive 的兜底(比如类别字符串匹配)。如果看过《思考,快与慢》的话,这个 NLM 记忆层相当于系统 2,进到这里后出去是需要做一系列推理和判断的。至于系统 1,则直接从历史对话中得到,这方面可以借鉴这个项目,这时候就不需要图数据库了。

目前从 Graph Input 到存储、Query 这步已经完成了,并且两步自动合并为一步,即 NLM 会根据输入的 Node 或 Relation 的部分信息找到存储的对应的完整信息,同时它会自动判断(可以全局配置或在 Query 时配置)是不是要添加或更新。项目主页在这里需要说明的是:属性不作为 Query 信息,仅作为对 Query 结果排序的依据。NLM 可以作为Python 模块使用,也可以作为 RPC 服务使用。在使用前需要做一些配置和操作,具体如下:

第一步:安装依赖

1
2
3
4
5
6
# 使用 pipenv
$ pipenv install --dev
# 没有 pipenv
$ python3 -m venv env
$ source env/bin/activate
$ pip install -r requirements.txt

第二步:启动一个 Neo4j 实例

1
$ docker run --rm -it -p 7475:7474 -p 7688:7687 neo4j

这里我们使用 7475 和 7688 两个端口,和正式环境区分开,并且也不持久化存储数据。启动 docker 后,在浏览器中打开 http://localhost:7475/browser/,端口改成 7688,密码输入 neo4j,然后将密码改为 password

如果你是在正式的环境下使用,可以这样:

1
2
3
4
$ docker run --rm -it \
--p=7474:7474 --p=7687:7687 \
--v=/your/persist/path/to/neo4j/data:/data \
neo4j

同时你需要创建环境变量

1
2
3
4
5
NEO_SCHE:scheme
NEO_HOST:host
NEO_PORT:port
NEO_USER:username
NEO_PASS:password

举个例子:

1
2
3
4
5
NEO_SCHE:bolt
NEO_HOST:localhost
NEO_PORT:7687
NEO_USER:neo4j
NEO_PASS:complex_password_for_neo4j

如果你不是通过配置文件,那建议使用 inishchith/autoenv: Directory-based environments.,将配置写到 .env 文件下,切换目录会自动加载目录下 .env 中的环境变量。注意,不要把 .env 文件提交到代码仓库。

测试环境下不需要配置环境变量,用的都是上面的默认值,比如端口用 7688,密码用 password 等。

第三步:运行测试

这步的主要目的是生产一点数据:

1
$ pytest

运行完后打开 http://localhost:7475/browser/,在 Query 框中输入查询语句就能看到节点和关系信息了,一共 8 个节点和 8 个关系:

1
MATCH (_) RETURN _

作为模块使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
from py2neo.database import Graph
from nlm import NLMLayer, GraphNode, GraphRelation
# 这里的配置可以在具体运行时覆盖
mem = NLMLayer(graph=Graph(port=7688),
fuzzy_node=False,
add_inexistence=False,
update_props=False)

########## 节点 ##########

# 基本查询
node = GraphNode("Person", "AliceThree")
mem(node)

# 添加一个新节点,如果不是新节点,就会返回查到的那个节点
new = GraphNode("Person", "Bob")
mem(new, add_inexistence=True)

# 模糊查询,只支持 name 上的模糊
node = GraphNode("Person", "AliceT")
mem(node, fuzzy_node=True)

# 更新属性,Node 的属性会同步返回更改后的
node = GraphNode("Person", "AliceThree", props={"age": 24})
mem(node, update_props=True)

# 多个节点,辅助功能
node = GraphNode("Person", "AliceT")
mem(node, fuzzy_node=True, topn=2)

########## 关系 ##########

# 基本查询
start = GraphNode("Person", "AliceThree")
end = GraphNode("Person", "AliceOne")
relation = GraphRelation(start, end, "LOVES")
mem(relation)

# 添加新关系
start = GraphNode("Person", "AliceThree")
end = GraphNode("Person", "Bob")
relation = GraphRelation(start, end, "KNOWS")
mem(relation, add_inexistence=True)

# 模糊查询
start = GraphNode("Person", "AliceTh")
end = GraphNode("Person", "AliceO")
relation = GraphRelation(start, end, "LOVES")
mem(relation, fuzzy_node=True)

# 多个关系
start = GraphNode("Person", "AliceThree")
end = GraphNode("Person", "AliceOne")
relation = GraphRelation(start, end)
mem(relation, topn=3)

# 更新属性,Relation 属性不会同步返回,需再次调用后返回
start = GraphNode("Person", "AliceThree")
end = GraphNode("Person", "Bob")
relation = GraphRelation(start, end, "KNOWS", {"roles": "classmate"})
mem(relation, update_props=True)

# 同时更新 Node 和 Relation 的属性
start = GraphNode("Person", "AliceThree")
end = GraphNode("Person", "Bob", {"sex": "male"})
relation = GraphRelation(start, end, "KNOWS", {"roles": "friend"})
mem(relation, update_props=True)

# 没有关系类别的查询
start = GraphNode("Person", "AliceThree")
end = GraphNode("Person", "Bob")
mem(GraphRelation(start, end), topn=2)

############ 数据库 ############

# 所有的 label,即实体类别
mem.labels

# 所有的关系类别
mem.relationship_types

# 实体数量
mem.nodes_num

# 关系数量
mem.relationships_num

# 所有的实体,是一个 generator
mem.nodes

# 所有的关系,是一个 generator
mem.relationships

# CQL 查询
mem.query("MATCH (a:Person) RETURN a.age, a.name LIMIT 5")
[{'a.age': 21, 'a.name': 'AliceTwo'},
{'a.age': 23, 'a.name': 'AliceFour'},
{'a.age': 22, 'a.name': 'AliceOne'},
{'a.age': 24, 'a.name': 'AliceFive'},
{'a.age': None, 'a.name': 'Bob'}
]

# CQL 执行
mem.excute("MATCH (a:Person) RETURN a.age, a.name LIMIT 5")

NLMLayer 本质上是继承了 py2neo.Graph,所有 py2neo.Graph 的函数和方法,mem 都可以使用,比如:

1
2
# 一个 Node Matcher
mem.nmatcher

详细可参考:3. py2neo.matching – Entity matching — The Py2neo v4 Handbook

另外,如果模糊查询开启,则不会自动更新属性(即便配置了也不会),因为不确定模糊查到的节点是不是具备这些属性。但会自动添加节点,因为模糊查询都找不到的话,自动添加肯定是没问题的。

作为服务使用

作为 RPC 服务,必须在启动时将 NLMLayer 的参数给配置好(当然不配置的话默认都是 False),因为你不能像模块那样在实际调用时覆盖。这样设计的目的是让接口简单、清晰,客户端不用(也不需要)考虑这些东西。

1
2
3
4
5
6
$ python server.py [OPTIONS]

Options:
-fn fuzzy_node
-ai add_inexistence
-up update_props

客户端可以使用任何编程语言,详细情况可以阅读 gRPC 相关知识。

目前只有四个接口,但其实后两个并不能提供真正的服务:

  • NodeRecall
  • RelationRecall
  • StrRecall
  • NLURecall

仓库里有一个 Python 版本的客户端使用代码:client.py

特别说明

如果考虑到初衷,项目其实是个半成品,之所以发布出来是想听听更多人的建议,看看实际中到底有哪些应用场景,然后再做针对性地开发。这个毕竟是比较新的领域,我自己也没有很多实践经验。

对返回的结果数量,最开始的想法是只返回一个,后来给留了个 topn 的参数。这个功能在 RPC 中给取消了,主要还是因为后续没有完成,还有是考虑到最终其实只要一个结果,并不需要返回多个,以及 proto 写起来稍微清晰一些。设计的出发点是尽量让使用傻瓜式,比如模块主要功能的入口只有一个。

由于考虑到 Query 中可能有 props,而 props 实际上是 key value 都不确定的字典,这在 proto 中定义起来比较麻烦,一直没找到很合适的方法。所以干脆统一将 props 给序列化了,这样的做法导致 RPC Server 处理起来有一点点复杂。

Resources