常言道:“实业救国,实干兴邦”。不是说实业就是让你去摆地摊卖红薯,在数字化时代,0和1才是最实在的,算法、数据、代码就是实业。
0. 引言
历经文艺复兴后几百年的科技大发展,人类的知识储备总量也呈爆炸式增长。除了知识的总量的庞大,知识表示的方法也是多种多样。文字、数学公式、物理定律、化学反应式、乐符、画作、雕像等等,本质上都是知识的其中一种表示方式。
自从20世纪70年代超级计算机问世以来,计算机科学和信息技术很快就将大部分人类文明的遗产和知识库存储在以二进制为基础的比特空间中。在这种时代背景下,一个人将自己所遇到的知识编码 (Encoding) 成一种表示方式,然后其他人接收到这种编码后,再将这种表示方法解码还原 (Decoding) 成本来的知识,成为了沟通、交流、表达 (英文中这三个词均为Communication)的核心问题。
打个比方,一个青年即兴哼唱一首山歌,表达的是对爱情的向往。但是这种知识表示方式只能被有限的人听到。在信息时代,所有的表达内容都应该从原子组成的世界向比特组成的空间转化并存储。在这个例子中,用手机将哼唱录像并上传到某音、某字母站便是知识的编码。而观看视频的人将听到的歌曲在脑海中还原为“对爱情的向往”便是知识的解码。
相信你已经看出了问题。知识的编码和解码之所以是核 (Tou) 心 (Teng) 问题,是因为知识在转化、存储、重现的过程中必然伴随着信息的损耗。例如这个哼唱的青年可能面对镜头紧张、录音设备质量太差、观众的耳机十块钱一副、观众心情不好吃了头孢等等,各种因素都会影响信息表达的完整性。
《道德经》云:“大成若缺。”残缺的也是美。信息在沟通、交流、表达中出现损耗、失真是不可避免的,我们将其视作为了知识的更有效和广泛地传递而牺牲的代价。算法和程序的目的就是尽力将信息有效地广而告之,有时为了传递的速度,甚至刻意的缩减信息量。例如对视频和图片的压缩。
这个世界本来就已经被掩盖了很多细节。不精简信息的话,无论是人的大脑还是普通的电脑根本无法处理过于复杂的信息。你事无巨细的将所有的实验结果 (可能长达数月)、技术细节写成厚厚一摞的评估报告,但是决策层可能只会花5分钟时间看报告的最后的一页,然后不采用该页的结论。参考2020年1月-2月武汉发生的种种事件。
人类智能的标志就是用有限的方法表达无限的知识。类似于搭积木的过程,利用已知的有限资料和方法来构造新的知识,这个过程称之为组合泛化 (combinatorial generalization) 。由于计算机资源的限制,我们只能用有限的资料来表达知识;由于人脑和感官功能的限制,我们只能尽可能在保留信息有效性的前提下压缩信息。《论语》中子曰:“”辞达而已矣”。简洁有效的知识表达就是最好的方式。
“A key signature of human intelligence is the ability to make “infinite use of finite means.” by 威廉·冯·洪堡 (Wilhelm von Humboldt), 1836年。
《庄子》云:“吾生也有涯,而知也无涯。”用有限的数据来表示无尽的知识,取决于人类智能怎样理解世间万物以及万物之间的联系。如果某个高级星云的外星人初来乍到地球,对天地之间的万物建立模型,那么一开始他们很有可能把每一个原子都编上号码,并假设所有的东西之间都是有联系的。但是人类智能知道,雨林中的树木和云雾之间联系密切,而和深海的珊瑚礁联系微弱,通过海洋吸收二氧化碳而间接联系。
怎样将知识编码成最通用、最有效的表示方式,并借此打穿各个被人为割裂的学科之间的界限,是我们这一代人艰巨的任务。最近几年来,人工智能技术的发展,尤其是图网络理论的出现,将给“最好的知识表示”这个古老的命题带来新的生机,这次革命可能永久地改变我们认识和改变这个世界的方式。我们只能主动去迎接这次革命,要不然,总不能让文科生占据了知识的话语权吧?理科生出手,那就不是知识付费了,是秋风扫落叶般的摧枯拉朽。因为所有的颠覆性创新,都有一个共同特点: **Democratization,翻译过来就是平民化、开放、共享**。
1. 图网络模型:怎么理解?
图网络模型的难点在于怎么理解。
用组合泛化来表示任何形式的知识即是人类智能的核心问题,也是人工智能的核心问题 (废话AI本来就是要像人类一样思考)。图网络是组合泛化的其中一种方法。其他方式例如逻辑函数 (Logic) 、贝叶斯 (Bayesian) 概率推算、Peal等人因果推断 (causal reasoning) 模型。
图网络模型的灵魂是端到端 (end-to-end) 设计。 端到端指的是输入是最原始的数据,输出直接就是最终的结果。没有中间商赚差价,呃不,没有中间的步骤。
典型的反例如地球科学中的一些模型:先需要气象模型处理气象数据,或者需要先预处理地形数据成网格、开边界数据 (海洋水文模型),再将气象模拟结果输入下一个模型,例如化学反应模型、生物生长模型等,最终得到的数值计算结果居然不能直接用,还要经过一些专门的数据处理软件导出。这样知识表示方法繁琐冗长,明显不是我们要的理想模型。
就像当年的大秦帝国“书同文车同轨”对后世文明的影响一样,统一的知识表示方法将给后代的知识学习带来海量的益处。这个过程前途是光明的,道路是曲折的,方法是残酷的。端到端的图网络模型会带来非常强的关系推理偏差 (strong relational inductive biases),亦即非常强的先验偏见。所谓先验偏见,其实就是先入为主地认为事物之间存在某种正确的联系。举例说明,统一度量衡时,秦始皇干脆选择了最长的23.1 cm铜尺,认为这样最有利。
理解图网络,首先得理解建筑。图网络的设计本身就是一个建造“知识宫殿”的过程。打造一座建筑需要尺寸标准的砖块和黏合剂。在图网络中,砖块称为实体或节点 (Entity & Node), 粘合剂称为联系或连边 (Relation & Edge)。一个实体可以有很多属性,例如尺寸、颜色、质量、温度、能量等。一个连边是连接两个实体之间的规则,例如物理定律。连边本身也可以有很多属性,例如质量守恒定律、能量守恒定律等。图网络本身是有全局属性的,例如在真空中和在空气中石头和羽毛降落时的加速度是不一样的,“在空气中”和“在真空中”就是全局属性。
图网络尤其适用于输入的数据是稀疏矩阵的情况。数值为0的元素数目远远多于非0元素的数目,并且非0元素分布没有规律,称之为稀疏矩阵。现实世界中很多过程都是稀疏矩阵。例如在人脑中有大量的神经元,但是大多数自然图像通过我们视觉进入人脑时,只会刺激到少部分神经元,而大部分神经元都是出于抑制状态的。试想,如果万物之间的联系都很强,全是非零值,那就不需要用有限边的图模型了。无限边者,面也。
图网络适用的另外一个假设是马尔可夫过程。现实世界中很多过程都是隐式马尔可夫过程 (hidden Markov model)。一个实体未来的属性,只与当下的状态有关,于历史状态无关。学习这个词本身就包含了与当下状态的交互,试探的结果如果错了那就改正。
学习的本质是大脑对信息的加工,而有效的加工来自于有效的情境互动。在大多数情况下,大脑加工信息的方式没有优劣之分。有的人一生下来就会叫爸爸,而爱因斯坦三岁才开口说话。但是图网络的先验偏见,会选择一种知识表示方式为最优。所以在引入学科领域知识 (domain knowledge) 时要非常注意,不恰当的先验偏见会加大图网络模型的分析和预测误差。
图网络组成成分,节点和连边,是需要事先定义次序的。现实世界中的多个客观实体,本身是没有次序的。次序是人为定的,例如根据节点和连边的属性排上次序 (按照出生的顺序命名大娃二娃)。但是这种人为定义的次序并不会影响图网络的输出结果,亦即图网络具有排列不变性 (permutation invariance):输入的顺序改变不会影响输出的值。
一个图数据是个三元组 (3-tuple) 数据: G = (u, V, E)。其中,u是全局数据,对每一个节点、每一条边都有潜在影响,例如重力场。V是是一系列节点数据的集合,每一个节点数据包含着该节点的属性,例如该实体的位置、质量、势能、动能、温度等信息。而连边的集合 E = {(ek, rk, sk)}, k=1:Ne。ek是连边的属性,例如拉力、压力、剪切应力等信息。rk是连边的接受节点的索引编号 (receiver),sk是连边的发射节点的索引编号 (sender)。一个完整的图模型需要自定义三个更新函数φ和三个集成函数ρ。
图网络模型内核的伪代码如下:
图网络模型的输入数据和输出数据可以是向量 (vector) 或张量 (tensor)。模型输出结果是向量或张量的列表,每一条连边和每一个节点都对应着一个向量或张量。全局变量的输出只有一个向量或张量。
图网络在输入时就可以事先给出假定的客观实体之间的联系。例如知识图谱和社交网络。但是有时候节点之间的联系不确定,这个时候也可以只输入一个张量,亦即没有连边的节点矩阵,例如图像数据。当然,如果计算资源足够丰富,例如欧洲气象中心 (ECMWF) 的云物理和边界层方案用的是穷举法,也可以假设所有的节点之间都有联系,反正最终的输出是一定要更新成有限边图网络的。
2. 模块代码:怎么应用?
谷歌公司DeepMind实验室研发的GraphNet模型,现在已经在GitHub上公开了适用于Python的版本。
import graph_nets as gn
from graph_nets import blocks
from graph_nets import graphs
from graph_nets import modules
from graph_nets import utils_np
from graph_nets import utils_tf
import matplotlib.pyplot as plt
import networkx as nx
import tensorflow as tf
import numpy as np
# 定义一个图网络graph 0的全局变量.
globals_0 = [1., 2., 3.] # 1*3 向量
# 定义graph 0的节点属性.
nodes_0 = [[10., 20., 30.], # Node 0,1*3向量
[11., 21., 31.], # Node 1
[12., 22., 32.], # Node 2
[13., 23., 33.], # Node 3
[14., 24., 34.]] # Node 4
# 定义graph 0的连边属性.
edges_0 = [[100., 200.], # Edge 0,1*2 向量
[101., 201.], # Edge 1
[102., 202.], # Edge 2
[103., 203.], # Edge 3
[104., 204.], # Edge 4
[105., 205.]] # Edge 5
# 定义graph 0各个连边的发射节点和接受节点.
senders_0 = [0, # Index of the sender node for edge 0
1, # Index of the sender node for edge 1
1, # Index of the sender node for edge 2
2, # Index of the sender node for edge 3
2, # Index of the sender node for edge 4
3] # Index of the sender node for edge 5
receivers_0 = [1, # Index of the receiver node for edge 0
2, # Index of the receiver node for edge 1
3, # Index of the receiver node for edge 2
0, # Index of the receiver node for edge 3
3, # Index of the receiver node for edge 4
4] # Index of the receiver node for edge 5
# 将各个向量组合为词典型dict数据。
data_dict_0 = {
"globals": globals_0,
"nodes": nodes_0,
"edges": edges_0,
"senders": senders_0,
"receivers": receivers_0
}
#使用utils_np.data_dicts_to_graphs_tuple将图形词典放入GraphsTuple中。
graphs_tuple = utils_np.data_dicts_to_graphs_tuple([data_dict_0])
# GraphsTuple可以转换为networkx图形对象的`列表’,以便于可视化。
graphs_nx = utils_np.graphs_tuple_to_networkxs(graphs_tuple)
ax = plt.figure(dpi=300, figsize=(3, 3)).gca()
nx.draw(graphs_nx[0], ax=ax)
_ = ax.set_title("Graph 0")
运行结果:
这是只有一个图数据的结果。如果词典数据中有多个图数据时,称为GraphTuple。
# Working with tensor GraphsTuple's
# 定义绘制多个图
def plot_graphs_tuple_np(graphs_tuple):
networkx_graphs = utils_np.graphs_tuple_to_networkxs(graphs_tuple)
num_graphs = len(networkx_graphs)
_, axes = plt.subplots(1, num_graphs, dpi=300,figsize=(5*num_graphs, 5))
if num_graphs == 1:
axes = axes,
for graph, ax in zip(networkx_graphs, axes):
plot_graph_networkx(graph, ax)
# 定义绘制图网络节点和连边的标签为其属性的第一个数值。
def plot_graph_networkx(graph, ax, pos=None):
node_labels = {node: "{:.3g}".format(data["features"][0])
for node, data in graph.nodes(data=True)
if data["features"] is not None}
edge_labels = {(sender, receiver): "{:.3g}".format(data["features"][0])
for sender, receiver, data in graph.edges(data=True)
if data["features"] is not None}
global_label = ("{:.3g}".format(graph.graph["features"][0])
if graph.graph["features"] is not None else None)
# 定义绘制图的位置。
if pos is None:
pos = nx.spring_layout(graph)
nx.draw_networkx(graph, pos, ax=ax, labels=node_labels)
if edge_labels:
nx.draw_networkx_edge_labels(graph, pos, edge_labels, ax=ax)
if global_label:
plt.text(0.05, 0.95, global_label, transform=ax.transAxes)
ax.yaxis.set_visible(False)
ax.xaxis.set_visible(False)
return pos
# 以下定义四个随机数组成的图网络。
GLOBAL_SIZE = 4
NODE_SIZE = 5
EDGE_SIZE = 6
def get_graph_data_dict(num_nodes, num_edges):
return {
"globals": np.random.rand(GLOBAL_SIZE).astype(np.float32),
"nodes": np.random.rand(num_nodes, NODE_SIZE).astype(np.float32),
"edges": np.random.rand(num_edges, EDGE_SIZE).astype(np.float32),
"senders": np.random.randint(num_nodes, size=num_edges, dtype=np.int32),
"receivers": np.random.randint(num_nodes, size=num_edges, dtype=np.int32),
}
graph_3_nodes_4_edges = get_graph_data_dict(num_nodes=3, num_edges=4)
graph_5_nodes_8_edges = get_graph_data_dict(num_nodes=5, num_edges=8)
graph_7_nodes_13_edges = get_graph_data_dict(num_nodes=7, num_edges=13)
graph_9_nodes_25_edges = get_graph_data_dict(num_nodes=9, num_edges=25)
graph_dicts = [graph_3_nodes_4_edges, graph_5_nodes_8_edges,
graph_7_nodes_13_edges, graph_9_nodes_25_edges]
tf.reset_default_graph()
graphs_tuple_tf = utils_tf.data_dicts_to_graphs_tuple(graph_dicts)
with tf.Session() as sess:
graphs_tuple_np = sess.run(graphs_tuple_tf)
plot_graphs_tuple_np(graphs_tuple_np)
运行结果:
定义图数据只是搭建图网络的第一步 ,但是最难的一步。最终的目的是用自定义的算法更新图数据,对于社交网络,最常见的是更新连边属性。以下是一个简单的更新边的例子。
# 图网络中可以很方便的使用广播。
# 定义多图绘制函数 (该函数不重要)
def plot_compare_graphs(graphs_tuples, labels):
pos = None
num_graphs = len(graphs_tuples)
_, axes = plt.subplots(1, num_graphs, dpi=300,figsize=(5*num_graphs, 5))
if num_graphs == 1:
axes = axes,
pos = None
for name, graphs_tuple, ax in zip(labels, graphs_tuples, axes):
graph = utils_np.graphs_tuple_to_networkxs(graphs_tuple)[0]
pos = plot_graph_networkx(graph, ax, pos=pos)
ax.set_title(name)
tf.reset_default_graph()
graphs_tuple = utils_tf.data_dicts_to_graphs_tuple([data_dict_0])
# 将每个连边的数值设置为输入边、发送节点、接收节点和全局特征的第一个特征元素的总和。
updated_graphs_tuple = graphs_tuple.replace(
edges=(graphs_tuple.edges[:, :1] +
blocks.broadcast_receiver_nodes_to_edges(graphs_tuple)[:, :1] +
blocks.broadcast_sender_nodes_to_edges(graphs_tuple)[:, :1] +
blocks.broadcast_globals_to_edges(graphs_tuple)[:, :1]))
with tf.Session() as sess:
output_graphs = sess.run([
graphs_tuple,
updated_graphs_tuple])
plot_compare_graphs(output_graphs, labels=[
"Input graph",
"Updated graph"])
运行结果:
3. 学科知识:怎么嵌入?
学科知识的难点在于怎么嵌入到图网络中,使知识、数据、算法和网络结构融合在一起。注意:该小节只做抛砖引玉使用,欢迎留言讨论。
前面提到了简单的更新连边的例子。在地球科学中,我们实际上更希望预测节点的属性。例如预测一个地方未来的降水量和空气污染物浓度。
由图网络的定义可知,全局变量对节点的属性无直接影响,但是节点属性的更新会伴随着全局变量的更新。对于多站点时间序列数据来说,其同时具备时间和空间属性。此时节点是各个地理位置的站点 (例如气象和环境监测站)。由各个物理参数计算得到的、概念的定义与“一个大气”有关的参数,例如边界层高度、大气环境容量、反演的地表污染源排放强度是全局变量。
站点位置直接能测量到的气象参数 (风温U、V、T,颗粒物浓度PM2.5,测量得到的降水量等) 是节点属性。节点之间的连边是大气中能量的传输与转换,连边的两个属性分别是势能与动能。 例如两站点之间850 hPa高度的动能、500 hPa高度的势能的差值可以看作是节点之间的联系。补充一点说明,搭建动力学物理系统 (Dynamics of Physical Systems) ,意味着完全摈弃微观视角上的热力学,也就是忽略热能。
位势能 (动力学机械能) 与内能 (热力学能) 本来是两个不相关的概念,然而大气有其特殊性。在静力平衡条件假设下,在一个垂直气柱中,位势能和内能是成正比的。气柱从外部接受到热量,增温以后就会垂直膨胀,重力位能增加,内能作为热能同时也增加。位能将增加20%,内能将增加71%,因此没有必要把位能和内能分开讨论,而是把它们合并在一起成为总位能。全球平均而言,在总位能中,内能大约占70%,位能占30%。潜热能表示的是内能的增加量或减少量,可达总位能的20%。
由于边界层高度、大气环境容量、地表污染源排放强度这三个全局变量是最终计算得到的量,事先未知。所以实际在搭建图网络时,是个信息传播网络 (Message-passing neural network),也就是输入时不需要全局变量U,输出时也不需要连边E的情况。
对于单一站点数据,情况就更加简化了: 所有的物理变量都是节点。节点之间的连边表示的是一个物理量对另外一个物理量的影响。例如改变边界层高度和降水对颗粒物浓度产生的影响。
前面提到过,图结构数据的准备是搭建图网络的第一步,也是最难的一步。最致命的问题是: 图在哪?节点之间的联系有可能非常复杂,想想学术界为了全球变暖的成因吵了多少年吧!所以给出具体的、能服众的图结构数据是非常困难的。这个问题笔者目前也没有完美的解决办法。
尾声
笔者和志同道合的小伙伴们并非真心喜欢整天对着电脑敲代码,摸着不断僵硬的颈椎仰天叹气,而是更喜欢创造性的、趣味性的工作。但是现在我们必须要死磕算法、数据和代码,因为只有把需要死记硬背的知识用算法和程序表示,然后我们才能发挥创造性天赋,去做更重要的事情。