启发式什么为什么和如何?
启发式算法在算法开发中的理解和应用
试探法是我们使用的一种近似方法,它不能保证在实现目标时是最优的。在本文中,我将通过一个简单易懂的例子来深入解释我们如何使用启发式算法进行算法开发。所以我将使用 A-Star (A)* 算法,用一个涉及最短路径的完整例子来说明启发式的使用和实现。
确定性方法
确定性方法保证提供最佳解决方案。例如, Dijkstra 的算法可以认为是一种确定性算法,用于获得边权重为正的图中任意两点之间的最短路径。
作为确定性算法的 Dijkstra 算法
该算法最基本的形式如下。
function Dijkstra(Graph, Start)
vertex_set = {}
for each vertex v in Graph
dist[v] = INFINITY
parent[v] = NULL
dist[start] = 0 while vertex_set not empty
u = min dist vertex from vertex_set
remove u from vertex_set
for neighbour v of u
path = dist[u]+length(u,v)
if path < dist[v]
dist[v] = path
parent[v] = u
return parent, dist
我们可以看到,在算法的开始,没有考虑关于目标节点的知识。因此,我们本质上执行到图的每一个其他可通过顶点的最短路径的穷举搜索。想象这张图是你国家的道路网。这能扩展到那个水平吗?Dijkstra 算法的最佳最小优先级队列实现的时间复杂度为 O(|V|+|E|log|V|) 。
在 Dijkstra 算法中引入启发式算法
与其穷尽搜索源 顶点的所有邻居,我们还不如挑选邻居,这样可能会给我们一条到图中目标 顶点的更短路径。单词可能很重要,因为试探法经常以次优输出告终。
我们如何猜测一个特定的节点是否会把我们引向一条更短的路径?
这就是启发性的来源。例如,在道路网络的情况下,我们可以假设我们选择的交叉点越近,它到达目标城市的速度可能越快。为此,我们可以利用现成的 GPS 信息来计算出一个不是那么精英的,而是从空中两点之间的大致距离。尽管这可能不正确,但考虑到你可能会看到弯道和环形路,我们可以确定移动的大致方向一定是朝着目标城市。
比较上面的猜测,你可能会注意到,如果我们选择了 Dijkstra 的算法,我们也会计算到目标节点的距离,而这些节点不在我们预期目标的方向上。最著名的启发式最短路径算法的可靠实现被称为 A* 或 A-Star 算法。
a 星算法
伪代码如下所示。在 wiki 中阅读更多内容。
function AStar(start, end, heuristic=h)
open_set = {start}
closed_set = {} # distance so far to node
distance = lookup_table(default=INFINITY)
# guess to the end
guess = lookup_table(default=INFINITY) distance[start] = 0
guess[start] = h(start) while open_set not empty
current = node with lowest guess from open_set
if current is goal: END
open_set.remove(current)
closed_set.add(current) for neighbour of current
score = distance[current]+length(current,neighbour)
if score < guess[neighbour]
distance[neighbour] = score
parent[neighbour] = current
guess[neighbour] = distance[neighbour]+h(neighbour)
if neighbour not in closed_set
open_set.add(neighbour)
上面代码中的一个问题是,您必须扫描一个数组来获得目标节点的最近邻居。但是,如果您想使用到目标节点的距离作为关键字将邻居存储在最小堆中,您可以在 O(1) 时间内完成此操作。我们可以用数据结构的知识来改进算法,这很简单(阅读这篇文章以了解更多)。让我们看看这个算法在 python 中的实现。
在 Python 中实现 A-Star
我将在我的算法实现中使用以下导入。
import numpy as np
import matplotlib.pyplot as plt # for visualizations
import matplotlib.cm as cm # for visualizations
from collections import defaultdict
import networkx as nx # storing the graph structure
import heapq # pythons heap implementation
我将创建一个包装类来包含图的顶点(节点),这样我就可以在 python 的heapq
实现中立即使用它们。我覆盖了包装器的__lt__
函数,这样heapq
将使用它的val
属性到heappush
和heappop
元素。这个val
对应于到目标节点的距离。
class NodeWrap:
def __init__(self, nid, dist_to_end):
self.nid = nid
self.val = dist_to_end
def __lt__(self, other):
return self.val < other.val
一旦算法运行并且我们有了遍历的顶点的父顶点,我就使用下面的代码来追踪最短路径。
def trace_path(parent_map, end):
node = end
path = []
while (node in parent_map):
path.append(node)
node = parent_map[node]
return path[::-1]
实现图形和可视化矩阵
因为这是为了学习的目的,所以我认为实时观察算法的运行是非常有趣的。因此,让我们将我们的问题建模如下。假设我们有一个 2D 地形,我们想从左下角到达右上角。我们的道路上有一个黑色的障碍物。
要穿越的地形
我使用下面的代码填充这个地形。这是一个大小为50x50
的简单矩阵。图像是一个矩阵,有三个通道用于 RGB 。所以障碍简单来说就是带[0, 0, 0]
的索引。
mat = np.zeros((50, 50, 3), dtype=np.float32)for x in range(50):
for y in range(50):
mat[x, y, 0] = 0.95
mat[x, y, 1] = 0.95
mat[x, y, 2] = 0.95
g.add_node("{},{}".format(x,y), x=x, y=y)# end
mat[49, 0, 0] = .0
mat[49, 0, 1] = .0
mat[49, 0, 2] = 0.8
# start
mat[0, 49, 0] = .0
mat[0, 49, 1] = 0.8
mat[0, 49, 2] = .0for x in range(20, 50):
for y in range(20, 50):
if x>y+5:
mat[x, y, 0] = .0
mat[x, y, 1] = .0
mat[x, y, 2] = .0for x in range(0, 30):
for y in range(0, 30):
if x<y-5:
mat[x, y, 0] = .0
mat[x, y, 1] = .0
mat[x, y, 2] = .0for x in range(26, 30):
for y in range(10, 20):
mat[x, y, 0] = .0
mat[x, y, 1] = .0
mat[x, y, 2] = .0
mat_copy = np.copy(mat)
创建网络 x 图
因为图形操作最好在具有图形功能的类似图形的数据结构上执行,所以让我们将地形迁移到图形上。每个像素将是我们图中的一个 节点/顶点 。从每个 节点/顶点 我们可以像棋盘上的女王一样在所有 8 个方向上遍历。在我们的例子中,边是存在的,除非可用的可逆邻居是黑色的。
g = nx.Graph()def is_black(arr):
return sum(arr) == 0
for x in range(50):
for y in range(50):
xi = g.nodes["{},{}".format(x,y)]['x']
yi = g.nodes["{},{}".format(x,y)]['y']
for xj in range(xi-1, xi+2):
for yj in range(yi-1, yi+2):
if xi==xj and yi==yj or \
xj<0 or yj<0 or \
xj>49 or yj>49:
continue
# if black not a neighbour
if is_black(mat[xi, yi]) \
or is_black(mat[xj, yj]):
continue
g.add_edge("{},{}".format(xi,yi), "{}, \
{}".format(xj,yj))
用检查点实现算法
现在我已经准备好了我的图表,我想实现并运行一个 A-Star。同时,当算法搜索邻居时,我想对初始的 2D 地形进行快照,以便以后可视化。完整的 python 代码如下。这里我使用欧几里德距离作为距离启发式,这是从任何节点到目标的猜测。
def store_image(world, node, i):
global mat
x = world.nodes[node]['x']
y = world.nodes[node]['y']
mat[x, y, 0] = 1
mat[x, y, 1] = .5
mat[x, y, 2] = .0
fig = plt.figure(figsize=(10, 10))
plt.imshow(mat)
plt.axis('off')
plt.savefig("im-{}.png".format(i))
plt.close()def euclidean_dist(world, node1, node2):
x1 = world.nodes[node1]['x']
x2 = world.nodes[node2]['x']
y1 = world.nodes[node1]['y']
y2 = world.nodes[node2]['y']
return ((x1-x2)**2 + (y1-y2)**2)**.5def a_star(world, start, end):
stepper = 1
open_set = []
close_set = set()
parent_map = {}
start_node = NodeWrap(start, 0)
heapq.heappush(open_set, start_node)
store_image(world, start, stepper)
stepper += 1
cost_to_reach_node = defaultdict(lambda: float('inf'))
cost_to_reach_node[start] = 0
guess_to_destination = defaultdict(lambda: float('inf'))
guess_to_destination[start] = euclidean_dist(world, start, end)
while len(open_set) > 0:
current = heapq.heappop(open_set)
if current.nid == end:
path = trace_path(parent_map, end)
return path
close_set.add(current.nid)
for neighbor in world.neighbors(current.nid):
tentative_score = cost_to_reach_node[current.nid] + \
euclidean_dist(world, current.nid, neighbor)
if tentative_score < cost_to_reach_node[neighbor]:
parent_map[neighbor] = current.nid
cost_to_reach_node[neighbor] = tentative_score
guess_to_destination[neighbor] =
cost_to_reach_node[neighbor] + \
euclidean_dist(world, neighbor, end)
if neighbor not in close_set:
neighbor_node = NodeWrap(neighbor,
euclidean_dist(world, neighbor, end))
heapq.heappush(open_set, neighbor_node)
store_image(world, neighbor, stepper)
stepper += 1
我们可以使用下面的 runner 代码片段运行上面的算法。注意,我使用了矩阵的副本,因为在算法中我们更新了初始的mat
对象。
path = a_star(g, "49,0", "0,49")# trace path and visualize on terrain
for node in path:
x = g.nodes[node]['x']
y = g.nodes[node]['y']
mat_copy[x, y, 0] = 1
mat_copy[x, y, 1] = .5
mat_copy[x, y, 2] = .0fig = plt.figure(figsize=(10, 10))
plt.imshow(mat_copy)
plt.axis('off')
最终执行并创建 GIF
当我们在动画中看到事情是如何发生的时候,这种美就更加明显了。所以我会把我创作的所有图片组合起来,形成一个华丽的 GIF。我使用下面的代码从我的快照图像中创建 GIF。
import imageio
import globanim_file = 'out.gif'with imageio.get_writer(anim_file, mode='I', fps=10) as writer:
filenames = glob.glob('im-*.png')
filenames = sorted(filenames, \
key=lambda x: int(x.replace("im-", "").replace(".png", "")))
last = -1
for i,filename in enumerate(filenames):
frame = 2*(i**0.5)
if round(frame) > round(last):
last = frame
else:
continue
image = imageio.imread(filename)
writer.append_data(image)
image = imageio.imread(filename)
writer.append_data(image)
image = imageio.imread(filename)
writer.append_data(image)
image = imageio.imread(filename)
writer.append_data(image)
这样做后,我们将有以下动画。
动画
结论
我们可以清楚地看到,遍历发生在一个非常狭窄的搜索空间。这使得搜索速度更快,我们不需要对所有节点进行彻底的搜索。
在大得多的图中,节点标记和索引等技术已经完成,因此不需要将图完全加载到运行时内存中。我希望以后能写更多关于这个话题的文章。现在,我希望你喜欢阅读这篇文章。你可以在这里看到完整的笔记本。
嘿,数据,2020 年有鲨鱼吗?
高级 SQL 技术第 1 部分:用 case 语句美化数据
真实数据五花八门,乱七八糟。如果您使用 SQL 从数据库中获取数据,您可以在数据到达您之前清除数据。这种技术使得直接从查询结果进行分析变得更加容易,例如,它将减少您以后在 python 脚本中处理它的时间。在某些情况下,使用 case 语句返回标志而不是 varchar 文本将会大大减小输出的大小。
简而言之, case 语句类似于 if,else 语句。
CASE *case_value*
WHEN *when_value*
THEN *statement_list*
[WHEN *when_value* THEN *statement_list*] ...
[ELSE *statement_list*]
END CASE
我广泛使用 case 语句来评估数据,以创建新的、更清晰的数据元素。例如,如果一周中的某一天列有“星期三”、“星期三”、“星期三”或“驼峰日”,您可以从查询开始就清除这些内容。
case when lower(day_of_week) in (‘wednesday’, ‘wed’, ‘hump day’) then ‘Wedneday’
when lower(day_of_week) in ('friday', 'fri', 'tgif')
then 'Friday'
......
else day_of_week
end
数据
在之前的帖子中,我详细介绍了如何在 AWS 中创建 MySQL 数据库。我收集了美国 2020 年的恶劣天气事件细节。
加载到 MySQL 数据库的恶劣天气事件数据——作者截图
这个问题
知道您可以访问恶劣天气事件数据,您的老板问您:
2020 年有鲨鱼吗?
这是疯狂的一年,他想去度假。他害怕 Sharknado,并希望避开最近受影响的州。
你提出的问题是:有报道说有鲨鱼的龙卷风的州吗?
你的回应
你可以用 case 语句来回答这个问题。您实际上只需要第三个 case 语句,但是前两个是验证的良好实践。
使用 case 语句评估数据—作者截图
您可以查看这个列表,寻找正的 Sharknado 标志, 或 您还可以在 where 子句中使用 case 语句。
简化您的结果:
使用 case 语句过滤结果—作者截图
你可以告诉你的老板,2020 年还没有 Sharknados。
嘿 Siri,我能帮你吗?
如果人类能做到,机器也应该能做到。
细微差别、表达、上下文、行话、不精确或社会文化深度。
人类的语言是多样的,有它自己的特点,并且可以导致多种多样的解释。机器能够理解我们的意思吗?
更重要的是… *他们能以我们能理解的方式交流吗?*如果这是真的,我们已经实现了人工智能的第一个目标之一。
人类语言对人工智能来说是一个具有挑战性的领域。
有什么规律可循?
如果你想了解更多,请访问 oscargarciaramos.com
对话互动
人工智能的七种模式之一,包括超个性化、自主系统、预测分析和决策支持、模式和异常、识别系统、目标驱动系统以及我们今天讨论的对话/人类交互。
主要目标是什么?
让机器能够通过人类语言模式与人类互动,让机器能够以人类能够理解的方式与人类交流。
再也不用点击,打字,滑动,或者编程… 我们太懒了。
现在,我们希望机器能够像我们相互交流一样与我们互动。这包括声音、文字或任何我们的大脑能够理解的方式。
有三种场景:机器对人、机器对机器和人对机器的交互。
在语音助手、意图分析、内容生成、情绪分析、情感分析或聊天机器人中找到一些例子;在金融部门或远程医疗等交叉部门开发解决方案。
心情、意图、情绪、视觉手势 ,…这些形状或概念已经是机器可以理解的了。
自然语言处理
****ASR(自动语音识别)***STT(语音转文本) 和 NLP(自然语言处理) 。前两个,ASR & STT,是基于转换或产生声波转换成文字,第三个,NLP,解释它听到的数据。不是因为这个原因,AI *(和深度学习)在 ASR & STT 领域已经不再重要,因为它已经帮助语音到文本转换更加精确,文本到语音转换更加人性化。
然而,自然语言处理(NLP) 不仅仅是将波形转换成文字。 我们需要理解并提供理解 。
正是在这里,我们进入了一个巨大的世界,从演讲或文本的生成,实体的提取和理解,话题或主题的检测和识别,句子、概念、意图和意义的连接。
基本上,明白。
自然语言处理分为两部分:自然语言理解和自然语言生成。
让我们看看。
自然语言理解(NLU)
NLU 侧重于对语音或文本等人类输入数据的解释,以根据其意图使用它。主要目标是理解意图。它是如何工作的?
AI 使用不同的工具,如词汇分析,来理解句子及其语法规则,然后将它们分成结构组件。
我们使用工具如**(又名词干)或 解析 来执行这些功能。**
- 标记化
将一串字符分解成可以分析的语义上有意义的部分。没有空格或注释。 - **词条化
(又名词干)识别并返回句子的词根,探索各种附加信息。 - 解析
确定文本的句法结构。有两种不同的方法: 依存解析 ,单词作为节点并显示其依存的链接。;以及 选区解析 ,使用上下文无关文法显示具有句子句法结构的树。
自然语言生成(NLG)
我们先理解,再回答。
NLG 能够准备好和人类进行有效的交流,这样说话的人就不会像一台机器。
广义地说,这是一个将结构化数据转换为自然语言的软件过程,因此它可以从数据集产生书面或口头叙述。
要做到这一点,正如我们之前所说,机器首先需要解读内容,理解其含义,然后做出反应,进行有效的沟通。概括地说,我们执行以下步骤:
- ****步骤 1:内容确定,决定哪些信息应该包含在文本中
- ****第二步:文本结构,设置组织文本的合理顺序
- 第三步:句子聚合,在同一个句子中组合信息。
- ****第四步:语法化,采用自然语言。
- ****步骤 5:参考表达式生成,类似于步骤 4,但是识别内容的域。
- ****第六步:语言实现,构建一个结构良好的完整句子。
基本公式→ NLP = NLU+ NLG
那么,下一步是什么?
不再有静态内容,这些内容只会给用户带来挫败感和时间浪费→人类希望与高效的机器进行交互。
1966 年开发的第一个伊莱扎聊天机器人(ELIZA chatbot)向我们展示了这个领域可能提供的机会,现在已经一去不复返了。然而,当前的助手如Alexa谷歌助手苹果 Siri** 或 微软 Cortana 在理解人类并以有效、智能和一致的方式做出反应方面必须有所提高。**
他们别无选择。
让我们利用人工智能的对话模式。****
欢迎留下评论或分享这篇文章。以后的帖子关注 me 。
如果你想了解更多,你可以在oscargarciaramos.com找到我
嘿 Siri,作为人类意味着什么?
在提出人工智能可以增强人类潜力之前,问一下是什么将人类与技术的不断进步区分开来可能是有用的。
图片来源:Jess Szmajda
如果人工智能的远见者承诺利用技术改善我们日常生活的几乎每个方面,数据主义的支持者认为,我们与我们生产的模仿我们行为的技术没有什么区别。人类的思想和感情只不过是对环境刺激的程序化生化反应吗?在问机器智能如何让我们变得更好之前,让我们考虑四个独特的人类特征:人类能动性的概念、抽象和移情的能力、人类智能的多种形式以及区分对错的能力。
人类的一个基本特征是人类能动性的概念:做出明智的、不受胁迫的决定,从而独立于我们的环境采取行动的能力。代理通常被归类为潜意识的,非自愿的行为,或有目的的,目标导向的活动。一个人能在多大程度上独立于他/她的环境而行动是有争议的:在决定一个人的行动范围时,社会结构和个人意志的首要地位一直是社会科学中经常讨论的问题。
例如,我们的行为在多大程度上受到我们使用的技术的制约;也就是说,我们在媒体上的交流方式是否不同于在 Twitter、Instagram 或 SnapChat 上?这就是说,我们大多数人都会同意人类至少有某种程度的自由选择:人们是自我组织、积极主动、自我调节的,并进行自我反思,而不是由外部事件塑造和引导的被动有机体。
人类的第二个特征是我们的抽象能力(概念源于经验的概念过程)和移情能力(理解和联系我们周围世界的能力)。抽象让我们按照自己的理想行动,而不是按照移情“基本上是理解他人情感的能力”的事件行动。"
同理心可以是认知的(与思想和理解有关)、情感的(与感觉和知觉有关)或同情的(关注智力和情感)。例如,抽象让我们可以将 HAL 5000、WALL-E 和 RD-D2 都视为机器人,移情让我们可以认同他们在每部电影的故事情节中面临的挑战。
人类的第三个显著特征是我们使用多种形式的智力——我们既能用心灵进行直觉思考,也能用头脑进行逻辑思考。毫无疑问,人类智慧远远不止这两种表现形式,不同的作者谈到了五种、七种甚至九种人类智慧。语言智能唤起了从概念上思考和使用语言探索复杂性的能力。个人内部智能是在日常生活中应用知识时理解自己、自己的想法和感受的能力。
人际智能是指我们理解、联系以及与家人、同事和熟人互动的能力。灵性智能意味着探索有关人类生存和状况的形而上学问题的敏感性和能力。增强人性是否意味着增强其中一种形式的智力,如果是的话,是哪一种?
最后,人性的第四个特征是伦理——帮助我们区分对错的共同价值观。商业道德通常与几个道德概念联系在一起,包括诚实、正直、透明、责任和隐私。数据伦理包括研究和采用尊重基本个人权利和社会价值观的数据实践、算法和应用。
因为技术影响这些道德标准——即人工智能反映了人类决策的愿景、偏见和逻辑,所以我们需要考虑技术在多大程度上可以与它旨在解决的更大的经济和社会挑战相隔离。诸如个人隐私、哪些指标允许我们评估人类进步以及信息和治理之间的关系等新兴问题表明,数据决定了我们如何看待和评估我们周围的世界。
单单机器智能的进步就能帮助我们理解对人类来说意味着什么吗?
这篇文章是我们对 Jay Liebowitz 即将出版的“数据分析和人工智能”一书的贡献的一部分。关于数字伦理的进一步想法可以在我们关于数字伦理的电子书中,以及我们在商业分析研究所的管理会议、课程和暑期学校中找到。
Lee Schlenker 是商业分析和社区管理教授,也是 BAI 的校长。他的 LinkedIn 个人资料可以在查看你可以在的 Twitter 上关注白
Chrome DevTools 的隐藏功能
使用这些 Chrome DevTools 提示和技巧,成为一个更高效的 web 开发人员。
如果你是一名 web 开发人员,那么你肯定会花相当多的时间在浏览器、开发工具或 web 控制台上闲逛。检查元素、修改 CSS 或在控制台中运行命令——这些是每个 web 开发人员都知道如何使用浏览器开发工具做的一些基本事情。然而,你可以在浏览器中做更多的事情来使你的调试、开发和网页设计更加有效。这里有几个隐藏的或者不太为人所知的 Chrome DevTools 的特性,你应该知道这些特性,并且你每天都会用到它们…
paweczerwi324ski 在 Unsplash 上拍摄的照片
将数组打印成表格
当在 JavaScript 中处理大量数据时(例如,创建数据可视化),第一次尝试不会成功,您不可避免地要去 web 控制台查看数据——可能用console.log
。这将产生 JavaScript 对象(JSON ),在 2D 数组的情况下,它将很难阅读,也很难找到有用的信息。但是有一个简单的解决方法:
console.table()
所要做的就是用console.table
代替console.log
。这个函数可以很容易地显示 1D 和 2D 数组,但是让这个函数特别有用的是,它还可以正确地显示列名,最重要的是,它还允许您按这些列中的每一列进行排序。因此,要获得如上表所示的表格,您需要使用以下格式的数据:
使用多光标
每个代码编辑器中最基本的功能和快捷方式之一是多光标和多选择。如果你想像我一样尽可能高效,那么你一定会好好利用这些捷径:
多光标使用CTRL + Click
:
多光标
多光标是有帮助的,但是当你需要选择和替换一个文本的大量出现时,它不是很准确而且有点麻烦。对于那些情况你可以使用更合适的CTRL + D
快捷键:
使用 CTRL + D 的多光标
在上面的 GIF 中,你可以看到如何使用CTRL + D
来选择(也可以选择CTRL + U
来取消选择)文本的出现,从而轻松地修改或替换它们。
使用命令选项板
有很多工具、标签、文件、命令等等。在 Chrome DevTools 中,记住每个人的名字或位置可能是不可行的。为此,你可以使用命令面板,它可以通过CTRL + Shift + P
打开。在这个面板中,你可以找到所有的快捷键、面板、控制台设置、标签、设置等等。
命令选项板
同样,如果你在这个快捷方式中省略了Shift
而使用了CTRL + P
,它会给你一个列表,列出所有你可以打开的文件。如果你的网站有很多源文件,这也很方便。
方便的颜色选择器
我可能不是唯一一个在 CSS 中无休止地修改字体、颜色等等的人。为了使颜色调整变得简单一点,你可能想要使用颜色选择器,你可以通过找到你想要改变颜色的元素并点击它的 CSS 颜色域来打开它。
这是一个很好的功能,但真正的游戏改变者是在颜色选择器打开时,只需点击它就可以从网站上选择任何颜色——就像这样:
颜色选择器
深色模式
从上面的截图和 gif,你可能已经注意到我在 Chrome DevTools 中使用了黑暗模式。因此,如果你想知道如何从亮模式切换到暗模式,那么你可以导航到 DevTools 的右上角——点击 3 个垂直点图标,然后选择更多工具,然后选择设置。在设置菜单中选择偏好,最后将主题设置为暗。就是这样!欢迎来到黑暗面!
黑暗模式设置
查找 CSS 属性的定义位置
使用 CSS 涉及到大量的试验和错误(至少对我来说是这样),与其在 IDE 中编辑代码和刷新浏览器标签之间来回切换,为什么不节省一些时间并在 DevTools 中完成所有工作呢?一个节省时间的技巧是使用CTRL + Click
找到 CSS 属性的定义位置,这样您就可以在它的源文件中编辑它:
查找 CSS 属性的定义位置
使网站在设计模式下可编辑
CSS/Design 的另一个窍门是使用设计模式直接编辑网站上的任何东西。无需修改 HTML 和 CSS 源文件——只需点击/突出显示页面上的任何内容并进行更改!要打开这种模式,只需在控制台中键入document.designMode = "on"
并开始设计(嗯,真的只是搞乱任何东西):
设计模式
条件断点
使用 IDE 正确调试浏览器中运行的 JavaScript 通常很难/很烦人。因此,让我们利用 DevTools 调试器,而不是使用 IDE。
在调试器中设置基本断点并不有趣,你肯定知道如何去做。那么条件断点呢?有时,您可能有一个循环的*,它迭代超过 1000 条或更多记录,并且您知道 bug 仅在满足特定条件时出现——例如,当所述循环中的if
语句返回false
时。为了仅在满足该条件时在断点处停止,我们可以设置条件断点😗
条件断点
我们首先右击现有断点(红点),然后点击编辑断点并插入我们想要的表达式。当这个表达式的值为true
时,断点将被触发,我们将有机会四处查看。这个条件断点不必只添加在有if
语句的行上——它可以在任何行上,每次代码执行经过它时,它的表达式都会被求值。
当您在暂停执行期间四处寻找 bug 时,您可能还会考虑将可疑变量添加到 Watch 选项卡,这样您就可以在值发生变化时监视它们。要将变量添加到手表中,您可以执行以下操作:
调试器监视
模拟慢速互联网
大多数人都知道 DevTools 中的网络标签,在这里你可以看到每个功能、操作或文件加载需要多长时间。然而大多数人不知道的是,你也可以使用网络选项卡来模拟使用网络节流的慢速互联网连接。下面是如何做到这一点:
网络节流
在添加和选择这个配置文件后,剩下要做的就是刷新页面,看看它在糟糕的互联网连接下表现如何。注意——通过尝试——你可能会意识到你的网站真的有多慢(有点压抑的个人体验)。
衡量网站绩效
说到性能,让我们探索一下 Chrome DevTools 在应用分析方面能为我们做些什么。要运行 profiler,我们需要切换到 DevTools 中的 Performance 选项卡。在这个选项卡上,我们只需点击CTRL + Shift + E
。此快捷方式与启动 profiler 同时刷新页面。页面加载后,我们需要再次按下这个快捷键来停止 profiler 记录。
DevTools 探查器
从这里我们可以深入了解网络性能、动画、功能时间等。你可以查看很多东西,但有一个特别的是检查函数计时。要检查这些功能,您可以点击图中的橙色条,并选择底部的自下而上选项卡。如果你接着按照总时间对它们进行排序,你可能会发现你的代码中有一些花了太多时间才完成的部分。
结论
这些只是我最喜欢的一些技巧和诀窍,这绝对不是 Chrome DevTools 所有特性的详尽列表。你可能会在 Chrome DevTools 指南中找到许多适合你工作流程的有用特性。此外,我建议继续关注新功能部分的最新更新,它为您的浏览器带来了更多有用的工具。
如果你错过了任何特定的功能,值得检查 Chrome 网络商店上的 DevTools 扩展,因为谷歌和用户社区都有额外的工具。如果你不能为你的特定问题找到工具/扩展,也许可以考虑使用 扩展 DevTools 教程自己构建一些东西。😉
本文最初发布于martinheinz . dev
如果你喜欢这篇文章,你应该看看我下面的其他文章!
让我们使用 D3.js 创建交互式蜂群图表,以便更好地可视化您的数据。
towardsdatascience.com](/better-data-visualization-using-beeswarm-chart-bb46a229c56b) [## Python 调试终极指南
让我们探索使用 Python 日志记录、回溯、装饰器等等进行调试的艺术…
towardsdatascience.com](/ultimate-guide-to-python-debugging-854dea731e1b) [## 用 Javascript 实现 2D 物理学
让我们在实现真实的 2D 物理模拟和可视化的同时享受一下 JavaScript 的乐趣吧!
towardsdatascience.com](/implementing-2d-physics-in-javascript-860a7b152785)
隐藏的宝石:寻找美国最好的秘密步道
有了数据科学
(图片来源:David Marcu via unsplash )
人们想要在荒野中寻找孤独有很多原因,从沉浸在大自然中的治疗效果,到不想在繁忙的小径上造成小径退化和土壤侵蚀。
现在比以往任何时候都更需要户外的缓刑。但在后 COVID 19 的世界里,当在狭窄的小径上经过徒步旅行者时,几乎不可能保持适当的社交距离,因此找到较少人去徒步旅行的小径尤为重要。
我开始了一项任务,利用数据科学和机器学习来寻找美国最好的鲜为人知的小径。
方法
如果你和我一样,在你去任何地方或买任何东西之前,你会阅读所有的评论。
当我第一次着手这个项目的时候,我想回答这个问题,“什么样的步道是好的?”也就是说,什么样的特征和统计数据的组合会导致它具有高的总体评级?
不过,我很快就发现,在我搜集和分析的 35,000 条徒步旅行路线中,基本上所有路线都被评为“相当好”——也就是说,平均用户评分为 4.2 分(满分为 5 星),标准偏差小于 0.6,仅从 5 星评级来看,很难区分哪些路线是优秀的,哪些只是一般。
然而,所有路线之间的巨大差异是,它们的受欢迎程度由每条路线的评论总数来表示。虽然绝大多数的路径只有 100 个左右的评论,但少数几个却有几千个!是什么让这些小径如此受欢迎?
因此,我转而试图预测的不是一条线索的评级,而是通过一个数据驱动的模型来确定一条给定线索的各种特征与其受欢迎程度之间的关系。在寻找共性的过程中,我可以将这个模型应用到冷门的试验中,找出哪些试验符合所有相同的条件,可能会很棒,即使它们还没有被发现。
方法学
- )用硒和美汤,web scrape 获取全美约 35000 条步道的步道数据。这些信息包括徒步旅行的长度、海拔高度、位置,以及该路线的所有自然特征(如瀑布、野花、路面)的列表。
- )清理这些数据并创建一个熊猫数据框架。这包括对所有分类特征列的虚拟变量进行一次性编码。
- 2)利用 VADER 情感分析模块,通过简单的自然语言处理来分析每个踪迹的文本评论,并确定平均合成分数。
- )使用包括 Statsmodels OLS 在内的线性回归建模方法来确定踪迹特征与其受欢迎程度之间的关系。
- )通过 LassoCV 执行要素工程和正则化,以移除这些要素之间的多重共线性并优化模型。
- )将该模型应用于被描述为“轻度交通”的步道,以找到基于其特征组合而预期会受欢迎的步道,但只是还没有被发现。
调查的结果
一个线性回归模型以评论的数量(以及受欢迎程度)作为目标变量来拟合这条线索的统计数据。该模型产生了一个最有影响力的功能列表。这些包括有费,有高情感分析分数,有洛基,有争夺和无阴影等等。
我是这样解释这些重要特征的:
- 收费:如果最受欢迎的步道需要收费,这表明它们可能位于国家公园内。由于许多国家公园因 COVID 而关闭,或者可能非常繁忙,因此寻找替代方案就更加重要。
- 情感分析评分:由于所有的线索都有大致相同的评分(5 颗星),很难仅从这个评分中收集到大量关于其质量的可靠信息。通过使用自然语言处理来分析书面文本评论本身,我能够获得一个实际有用的度量标准来确定人们对这条路线的实际感受。分数越高(在-1 =非常消极到+1 =非常积极的范围内),人们对线索的积极感觉越强,这对寻找隐藏的宝石非常有用。
- 岩石/攀爬/没有树荫:这告诉我非常受欢迎的小径都在树线以上!在那些海拔更高、难度更大的徒步旅行中,你会遇到这些特征。海拔越高,你可能会看到更好的景色!事实证明,人们喜欢这些更难走的路。
该模型的其余部分被优化到 0.19。虽然这不是一个很高的分数,但你可以在下面看到,这是因为踪迹特征和流行度之间的关系并不是线性的。下面的残差图显示了预测的流行值和实际值之间的差异,非常清楚地证明了这一点(如果这是线性相关的,残差将全部落在大约 0!)那么,如果一条小径不具备流行小径的所有特征,那么到底是什么决定了它的流行呢?
我的主要发现是,在浏览线索时,用户首先看到的是评论最多的线索,这导致了某种形式的递归确认偏差。如果所有的步道都有大致相同的评级,用户将转向评论来确定一条步道是否好,将选择有很多评论的步道,从而使非常少的最繁忙的步道变得更加繁忙。与此同时,其他类似的路径可能有大量的机会,但被忽视。
那么,是什么让一条小径受欢迎呢?
美国有成千上万的徒步旅行可以在网上搜索到。一般来说,评论最多的小路会得到最多的徒步旅行,因此会有更多的评论;虽然鲜为人知的路径可能只是一个好的,但更难在网站上找到,很难确定他们是否会是一个好的路径,如果他们有这么少的评级。
那么,是什么让一条小径受欢迎呢?最终,搜索算法做到了。
是时候打破这种反馈循环,找到一些令人惊叹的替代远足方式,在那里我们可以避开人群。但是你怎么知道一条线索是否值得你花费时间呢?我用机器学习来帮你做这些工作。
我在被指定为“轻度交通”的小径子集上拟合了最佳模型,这些小径的 R 为 0.08。这实际上是令人鼓舞的,考虑到这些是特别挑选出来的不太受欢迎的路线,但是根据这些路线的特点,应该会受欢迎。
该项目未来工作的一个潜在领域可能是拟合多项式要素模型,而不是线性模型。对这种方法的早期探索产生了对 0.26 的有希望的 R 改进,但是由于复制特征而引起了一些特征共线性,这将需要通过特征工程来消除。一旦我有了更多的机器学习工具,我期待着继续这项工作!但是我非常激动地向你们展示这份美国最好的鲜为人知的路径列表,作为我的第一个端到端数据科学项目。
徒步旅行
看看下面你所在州隐藏的宝石吧!
隐马尔可夫模型——一个早晨精神错乱的故事
适马。这里真正的女主角。
介绍
在本文中,我们给出了一个隐马尔可夫模型(HMM)的(不)实际应用的例子。这是一个人为构造的问题,我们为一个模型创建一个案例,而不是将一个模型应用到一个特定的案例中…尽管,可能两者都有一点。
在这里,我们将依赖于我们早先开发的代码(参见 repo ),以及在早先的文章中讨论过的:“隐马尔可夫模型——从零开始实现”,包括数学符号。请随意看一看。我们将要讲述的故事包含了问题的建模,揭示隐藏的序列和模型的训练。
让故事开始吧…
想象以下场景:早上 7 点,你正准备去上班。实际上,这意味着你在不同的房间之间疯狂地奔跑。你在每个房间里随意花一些时间,做一些事情,希望在你离开之前把你需要的东西都整理好。
听起来很熟悉?
更糟糕的是,你的女朋友(或男朋友)养猫。小毛球想吃东西。由于早上的拥挤,不确定你是否记得喂它。如果你不这样做,猫会不高兴的…如果你的女朋友发现了,她也会不高兴的。
模拟情况
假设你的公寓有四个房间。那就是包括厨房、浴室、客厅和卧室。你在每一个房间里花了一些随机的时间,并以一定的概率在房间之间转换。与此同时,无论你走到哪里,你都可能会发出一些不同的声音。你的女朋友听到了这些声音,尽管她还在睡觉,但她可以推断出你在哪个房间度过你的时间。
所以她每天都这样做。她想确定你确实喂过猫。
然而,因为她不能在那里,她所能做的就是把猫粮袋放在一个房间里,据说你在那里呆的时间最长。希望这能增加你喂饱“野兽”的机会(并拯救你的夜晚)。
马尔可夫观点
从马尔可夫的角度来看,有 N 个房间是隐藏状态 Q = {q1,q2,…} (在我们的例子中 N = 4 )。每一分钟(或任何其他时间常数),我们从一个房间过渡到另一个房间气在 t → qj 在 t + 1 。与转变相关联的概率是矩阵**A _ ij的元素。
同时,你的女朋友可以听到明显的噪音:
- 冲水马桶(最有可能:卫生间),
- 刷牙声(最有可能:卫生间),
- 咖啡机(最有可能:厨房)、
- 打开冰箱(极有可能:厨房),
- 电视广告(最有可能:客厅),
- 收音机上的音乐(最有可能是:厨房),
- 洗碗(最有可能:厨房),
- 洗澡(最有可能是浴室),
- 开/关衣柜(最有可能:卧室),
- 尴尬的沉默……(可以是任何地方……)。
给定一个状态 qj* 在 t 时它们出现的概率由矩阵 B 的 bj(k) 系数给出。原则上,这些都可能源于你在一个任意的房间(状态)。然而,实际上,当你在厨房的时候,有一点点物理上的机会你扣动了马桶扳机,因此一些 b 将接近于零。*
最重要的是,当你从一个房间跳到另一个房间时,合理的假设是你去哪个房间只取决于你刚刚去过的那个房间。换句话说,当时的状态只取决于时间 t -1 时的状态,特别是如果你是一个注意力持续时间像金鱼一样的半脑死亡…
目标
*对于第一次尝试,让我们假设概率系数是**已知的。*这意味着我们有一个模型λ = ( A , B ,π) ,我们的任务是在给定观测序列的情况下估计潜在序列 X = (x_1,x_2,…),x_t = q_j ,对应于寻找 p(X|O,λ) 。换句话说,考虑到她所听到的,女友想要确定我们在哪个房间花的时间最多。
初始化
先初始化我们的A**B和π。**
例如,矩阵 A 被初始化为 :
**| | bathroom | bedroom | kitchen | living room |
|:------------|:----------:|:---------:|:---------:|:-------------:|
| bathroom | 0.90 | 0.08 | 0.01 | 0.01 |
| bedroom | 0.01 | 0.90 | 0.05 | 0.04 |
| kitchen | 0.03 | 0.02 | 0.85 | 0.10 |
| living room | 0.05 | 0.02 | 0.23 | 0.70 |**
矩阵 B 以类似的方式初始化。对于其他状态,初始概率向量π被设置为p('bedroom') = 1
和0
。
A 初始化后,B 和π如前文所述被实例化。
**hml = HiddenMarkovLayer(A, B, pi)
hmm = HiddenMarkovModel(hml)**
模拟
定义了 A,B 和π之后,让我们来看看一个典型的《清晨精神错乱》可能是什么样子的。这里,我们假设整个“马戏”持续 30 分钟,粒度为一分钟。
**observations, latent_states = hml.run(30)
pd.DataFrame({'noise': observations, 'room': latent_states})| t | noise | room |
|:---|:--------:|:-----------:|
| 0 | radio | bedroom |
| 1 | wardrobe | bedroom |
| 2 | silence | bedroom |
| 3 | wardrobe | bedroom |
| 4 | silence | living room |
| 5 | coffee | bedroom |
| 6 | wardrobe | bedroom |
| 7 | wardrobe | bedroom |
| 8 | radio | bedroom |
| 9 | wardrobe | kitchen |
| ...| ... | ... |**
上表显示了序列的前十分钟。我们可以看到,这有点道理,尽管我们必须注意到,女朋友不知道我们参观了哪个房间。这个序列对她是隐藏的。
然而,正如在上一篇文章中提出的,我们可以猜测在给定观察结果的情况下,统计上最有利的房间顺序是什么。这个问题用.uncover
方法解决。
**estimated_states = hml.uncover(observables)
pd.DataFrame({'estimated': estimated_states, 'real': latent_states})| t | estimated | real |
|:---|:-----------:|:-----------:|
| 0 | bedroom | bedroom |
| 1 | bedroom | bedroom |
| 2 | bedroom | bedroom |
| 3 | bedroom | bedroom |
| 4 | bedroom | living room |
| 5 | bedroom | bedroom |
| 6 | bedroom | bedroom |
| 7 | bedroom | bedroom |
| 8 | bedroom | bedroom |
| 9 | bedroom | kitchen |
| ...| ... | ... |**
比较结果,我们得到以下计数:
**| | est. time proportion | real time proportion |
|:------------|-----------------------:|-----------------------:|
| bathroom | 1 | 2 |
| bedroom | 18 | 17 |
| kitchen | 4 | 10 |
| living room | 8 | 2 |**
估计时间比例
得到的估计给出了 12 个正确的匹配。虽然这看起来不多(只有 40%的准确率),但它比随机猜测要好 1.6 倍。
此外,我们对匹配序列的元素不感兴趣。我们更感兴趣的是找到你呆的时间最长的房间。根据模拟,你在卧室里的时间长达 17 分钟。这一估计与真实序列相差一分钟,相当于约 6%的相对误差。没那么糟。
根据这些结果,猫粮站应该放在卧室。
训练模型
在最后一节中,我们依赖于一个假设,即转移和观察的内在概率是已知的。换句话说,你的女朋友一定一直在密切关注你,基本上是在收集关于你的数据。否则,她怎么能制定一个模型呢?
虽然这听起来完全疯狂,但好消息是我们的模型也是可训练的。给定一系列观察结果,可以训练模型,然后用它来检查隐藏变量。
让我们举一个例子,你的女朋友可能在某个疯狂的星期一早上听到了什么。你醒了。完全沉默了大约 3 分钟,你开始在衣柜里找你的袜子。找到你需要的(或不需要的)东西后,你又沉默了五分钟,冲了马桶。紧接着,你冲了个澡(5 分钟),接着刷牙(3 分钟),尽管你在中间打开了收音机。完成后,你打开咖啡机,看电视(3 分钟),然后洗碗。
因此,可观察到的序列如下:
**what_she_heard = ['silence']*3 \
+ ['wardrobe'] \
+ ['silence']*5 \
+ ['flushing'] \
+ ['shower']*5 \
+ ['radio']*2 \
+ ['toothbrush']*3 \
+ ['coffee'] \
+ ['television']*3 \
+ ['dishes']
rooms = ['bathroom', 'bedroom', 'kitchen', 'living room']
pi = PV({'bathroom': 0, 'bedroom': 1, 'kitchen': 0,'living room': 0}**
起点是寝室,但 A 和 B 不详。让我们初始化模型,并在观察序列上训练它。
**np.random.seed(3)
model = HiddenMarkovModel.initialize(rooms,
list(set(what_she_heard)))
model.layer.pi = pi
model.train(what_she_heard, epochs=100)
fig, ax = plt.subplots(1, 1, figsize=(10, 5))
ax.semilogy(model.score_history)
ax.set_xlabel('Epoch')
ax.set_ylabel('Score')
ax.set_title('Training history')
plt.grid()
plt.show()**
图一。使用 100 个时期的训练历史。训练意味着分数最大化。
现在,在模型的训练之后,预测序列如下:
**pd.DataFrame(zip(
what_she_heard,
model.layer.uncover(what_she_heard)),
columns=['sounds', 'guess'])| t | sound | guess |
|:---|:----------:|:-----------:|
| 0 | silence | bedroom |
| 1 | silence | bedroom |
| 2 | silence | bedroom |
| 3 | wardrobe | bedroom |
| 4 | silence | bedroom |
| 5 | silence | bedroom |
| 6 | silence | bedroom |
| 7 | silence | bedroom |
| 8 | silence | bedroom |
| 9 | flushing | bathroom |
| 10 | shower | bathroom |
| 11 | shower | bathroom |
| 12 | shower | bathroom |
| 13 | shower | bathroom |
| 14 | shower | bathroom |
| 15 | radio | kitchen |
| 16 | radio | kitchen |
| 17 | toothbrush | living room |
| 18 | toothbrush | living room |
| 19 | toothbrush | living room |
| 20 | coffee | living room |
| 21 | television | living room |
| 22 | television | living room |
| 23 | television | living room |
| 24 | dishes | living room || state (guessed) | total time steps |
|:---------------------:|:------------------:|
| bathroom | 6 |
| bedroom | 9 |
| kitchen | 2 |
| living room | 8 |**
根据上表,很明显猫粮应该放在卧室里。
然而,值得注意的是,这个结果是一个很好的巧合,因为这个模型是从一个完全随机的状态初始化的。因此,我们无法控制它在标签环境中的发展方向。换句话说,隐藏状态的命名对模型来说是抽象的。他们是我们的约定,不是模特的。因此,模型也可以将“淋浴”与“厨房”相关联,将“咖啡”与“浴室”相关联,在这种情况下,模型仍然是正确的,但是为了解释结果,我们需要交换标签。
尽管如此,在我们的例子中,模型似乎已经训练输出一些相当合理的东西,而不需要交换名称。
结论
希望,我们已经用隐马尔可夫模型方法揭示了早晨精神错乱的整个故事。
在这个短篇故事中,我们讲述了两个案例。第一种情况假设概率系数是已知的。使用这些系数,我们可以定义模型并揭示给定观察序列的潜在状态序列。第二个案例代表了相反的情况。概率是未知的,因此必须首先训练模型,以便输出隐藏序列。
结束语
这里描述的情况是作者每天面临的真实情况。是的…猫活了下来。;)
还会有更多…
我计划把文章带到下一个层次,并提供简短的视频教程。
如果您想了解关于视频和未来文章的更新,订阅我的 简讯 。你也可以通过填写表格让我知道你的期望。回头见!
原载于https://zerowithdot.com。**
隐马尔可夫模型(HMM)——高层次的简单解释
入门
用直观的例子简单解释 HMM,而不是复杂的数学公式
HMM 是一种非常强大的统计建模工具,用于语音识别、手写识别等。我想使用它,但是当我开始深入研究时,我发现并不是所有的事情都解释得足够清楚,例子也不够简单。关于 HMM 的科学出版物也经常以复杂的方式撰写,缺乏简洁性。所以我决定为我和所有对这个话题感兴趣的人创建一个简单易懂的高层次的 HMM 解释。
嗯问题
嗯回答这些问题:
评估——可观察到的事情发生的可能性有多大?换句话说,观察序列的概率是多少?
- 正向算法
- 向后算法
- …
解码——观察发生的原因是什么?换句话说,当你有观察序列时,最有可能的隐藏状态序列是什么?
- 维特比算法
- …
学习——我能从现有的观察数据中学到什么?换句话说,如何从观察到的数据中创建 HMM 模型?
- 鲍姆-韦尔奇
- …
这些问题的答案将在以后的文章中给出。现在我将详细解释 HMM 模型。
HMM 模型
HMM 模型由这些基本部分组成:
- 隐藏状态
- 观察符号(或状态)
- 从初始状态过渡到初始隐藏状态概率分布
- 转移到终端状态概率分布(在大多数情况下,从模型中排除,因为一般情况下所有概率等于 1)
- 状态转移概率分布
- 状态排放概率分布
在下一节中,我将详细解释这些 HMM 部分。
隐藏状态和观察符号
HMM 有两部分:隐藏和观察。隐藏部分由不被直接观察到的隐藏状态组成,它们的存在通过隐藏状态发出的观察符号来观察。
例 1 。你不知道你的女朋友或男朋友是什么心情(心情是隐藏状态),但是你观察他们的行动(可观察的符号),并且从你观察的那些行动中你对她或他的隐藏状态做出猜测。
例 2 。你想知道你朋友的活动,但是你只能观察外面的天气。你的朋友的活动是隐藏状态“发射”可观察的符号,这是天气状况。你可能会认为应该是其他方式,天气状况是隐藏的状态,你朋友的活动是可观察的符号,但关键是天气你可以观察,但你朋友的活动你不能,这使得状态是这样的。
你可以看到,在情绪的例子中,观察到的符号实际上是从隐藏状态发出的,而在朋友活动的例子中,观察到的符号就像是你朋友活动的原因。所以观察符号可以像隐藏状态的直接原因,观察符号可以像隐藏状态的结果。可以两者兼得,这就是 HMM 的妙处。
一般来说,你选择那些你无法直接观察到的隐藏状态(情绪、朋友活动等。)并且你选择你可以一直观察的观察符号(动作,天气状况等。).
隐藏状态和观察状态可视化示例 2。
您朋友的活动:
- 篮球(乙)
- 足球(女)
- 视频游戏(G)
可观察符号:
- 晴天
- 多云©
- 雨天®
图表 1。隐藏状态和可观察符号|作者图片
状态转移概率分布
当你为你的问题决定隐藏状态时,你需要一个状态转移概率分布来解释隐藏状态之间的转移。一般来说,你可以从一个状态转换到另一个状态或者转换到同一个状态。例如,如果你有 9 个状态,你将需要一个 9×9 的矩阵,这意味着你需要 N×N 的矩阵来表示 N 个状态。
此外,如果你把当前状态的每个转移概率相加,你会得到 1。
表 1。状态转移概率分布表|作者图片
图表 2。状态转移概率分布图|图片由作者提供
状态发射概率分布
你有隐藏的状态,你有观察符号,这些隐藏的和可观察的部分被状态发射概率分布所束缚。这是怎么做到的: 每次转换到隐藏状态都会放出观察符号 。而且,每个隐藏状态都可以发射所有的观察符号,只是发射一个或另一个符号的概率不同。注意,每个隐藏状态的所有发射概率总和为 1。
表二。州排放概率分布表|图片由作者提供
图表 3。州排放概率分布图|图片由作者提供
在图 3 中,你可以直观地看到态发射概率分布。它是表 2 的直接表示。
初始/最终状态概率分布
当您有隐藏状态时,还有两个状态与模型不直接相关,但用于计算。它们是:
- 初态
- 终端状态
如前所述,这些状态用于计算。当观察符号序列与隐藏状态相关时,转换到隐藏状态时会发出观察符号,这时有两种情况:观察序列开始和结束。
当观察序列开始时,你已经发射了一个符号,例如 S,但是发射只发生在转换到隐藏状态时,这里初始状态开始起作用。例如,如前所述,你发射了 S 符号,但是这个符号,可以以不同的概率从跃迁发射到所有隐藏状态,那么哪个跃迁到隐藏状态最有可能发射符号呢?在图 3 中,你可以看到转移到特定隐藏状态的概率将发出 S 状态,但从什么状态转移发生,答案是初始状态。这意味着当观察序列开始时,发射符号的初始隐藏状态由初始状态转移概率决定。当你只有一个符号的观察序列时,它是什么样子,如图 5 所示。
现在你知道,当你有观察序列开始时,你需要决定初始隐藏状态,初始状态概率分布有帮助。当您到达观察序列的末尾时,您基本上转换到终止状态,因为每个观察序列都作为单独的单元进行处理。这种转变通常是隐含的,没有明确提到。此外,通常从每个隐藏状态到终态的转移概率等于 1。
图表 4。初始/最终状态概率分布图|作者图片
在图 4 中可以看到,当观察序列开始时,发出第一个观察序列符号最可能隐藏状态是隐藏状态 f
观察序列
观察序列是从 1 个符号到 N 个符号的观察符号序列。每个观察序列都被视为独立的单元,没有任何关于过去或未来的知识。因为隐藏状态需要初始和终止状态。
重要注意是,相同的观察序列可以从不同的隐藏状态序列中发出(图 6 和图 7)。除了观测序列必须至少有一个符号(图 5)并且可以是任意长度之外,唯一的条件是观测序列必须是连续的。连续观察序列意味着观察序列不能有任何间隙。
图表 5。观察序列 S |作者提供的图像
图表 6。观察序列 SSCRCSC |图片由作者提供
图表 7。观察序列 SSCRCSC |图片由作者提供
图表 8。观察序列 RCS |图片由作者提供
摘要
现在你知道了 HMM 的基本组件,以及 HMM 模型的工作原理和表示方式。而且,你知道观察序列是如何从隐藏态产生的。我希望现在你有高层次的视角。
隐马尔可夫模型—从头开始实施
酷暑中的短暂停顿。葡萄牙, 2019 。
我想把这项工作扩展成一系列教程视频。如果你感兴趣,请订阅我的简讯保持联系。
介绍
互联网上充满了很好地解释隐马尔可夫模型(HMM)背后理论的好文章(例如 1 、 2 、 3 和 4 )。然而,许多这些作品包含了相当数量的相当先进的数学方程。如果想要解释理论,方程是必要的,我们决定将它带到下一个层次,创建一个温和的一步一步的实际实现来补充其他人的良好工作。
在这个简短的系列文章的两篇文章中,我们将专注于将所有复杂的数学转换成代码。我们的起点是马克·斯坦普写的文件。我们将使用这篇文章来定义我们的代码(本文),然后使用一个有点特殊的例子“晨昏”来展示它在实践中的表现。
注释
在我们开始之前,让我们回顾一下我们将使用的符号。顺便说一句,如果有些内容你不清楚,也不要担心。我们会握住你的手。
- T -观察序列的长度。
- N——潜在(隐藏)状态的数量。
- M——可观察到的数量。
- 问 = {q₀,q₁,…} -潜州。
- V = {0,1,…,M — 1} -一组可能的观察值。
- 一个 -状态转移矩阵。
- B -排放概率矩阵。
- π-初始状态概率分布。
- O -观察顺序。
- X = (x₀,x₁,…),x_t ∈ Q -隐态序列。
定义了该集合后,我们可以使用矩阵计算任何状态和观察值的概率:
- A = {a_ij} —开始一个转换矩阵。
- B = {b_j(k)} —为发射矩阵。
与跃迁和观察(发射)相关的概率为:
因此,模型被定义为一个集合:
基本定义
因为 HMM 是基于概率向量和矩阵的,所以让我们首先定义表示基本概念的对象。为了有用,对象必须反映某些属性。例如,概率向量的所有元素必须是数字 0 ≤ x ≤ 1,并且它们的总和必须为 1。因此,让我们设计对象的方式,它们将固有地保护数学属性。
import numpy as np
import pandas as pd
class ProbabilityVector:
def __init__(self, probabilities: dict):
states = probabilities.keys()
probs = probabilities.values()
assert len(states) == len(probs),
"The probabilities must match the states."
assert len(states) == len(set(states)),
"The states must be unique."
assert abs(sum(probs) - 1.0) < 1e-12,
"Probabilities must sum up to 1."
assert len(list(filter(lambda x: 0 <= x <= 1, probs))) == len(probs), \
"Probabilities must be numbers from [0, 1] interval."
self.states = sorted(probabilities)
self.values = np.array(list(map(lambda x:
probabilities[x], self.states))).reshape(1, -1)
@classmethod
def initialize(cls, states: list):
size = len(states)
rand = np.random.rand(size) / (size**2) + 1 / size
rand /= rand.sum(axis=0)
return cls(dict(zip(states, rand)))
@classmethod
def from_numpy(cls, array: np.ndarray, state: list):
return cls(dict(zip(states, list(array))))
@property
def dict(self):
return {k:v for k, v in zip(self.states, list(self.values.flatten()))}
@property
def df(self):
return pd.DataFrame(self.values, columns=self.states, index=['probability'])
def __repr__(self):
return "P({}) = {}.".format(self.states, self.values)
def __eq__(self, other):
if not isinstance(other, ProbabilityVector):
raise NotImplementedError
if (self.states == other.states) and (self.values == other.values).all():
return True
return False
def __getitem__(self, state: str) -> float:
if state not in self.states:
raise ValueError("Requesting unknown probability state from vector.")
index = self.states.index(state)
return float(self.values[0, index])
def __mul__(self, other) -> np.ndarray:
if isinstance(other, ProbabilityVector):
return self.values * other.values
elif isinstance(other, (int, float)):
return self.values * other
else:
NotImplementedError
def __rmul__(self, other) -> np.ndarray:
return self.__mul__(other)
def __matmul__(self, other) -> np.ndarray:
if isinstance(other, ProbabilityMatrix):
return self.values @ other.values
def __truediv__(self, number) -> np.ndarray:
if not isinstance(number, (int, float)):
raise NotImplementedError
x = self.values
return x / number if number != 0 else x / (number + 1e-12)
def argmax(self):
index = self.values.argmax()
return self.states[index]
初始化这个对象最自然的方法是使用一个字典,因为它将值与唯一的键相关联。不幸的是,字典不提供任何断言机制来约束值。因此,我们构建自定义的 ProbabilityVector 对象来确保我们的值行为正确。最重要的是,我们实施以下措施:
- 值的数量必须等于键的数量(我们各州的名称)。虽然这在从字典初始化对象时不是问题,但我们稍后将使用其他方法。
- 所有州的名称必须是唯一的(同样的参数也适用)。
- 概率总和必须为 1(达到一定的容差)。
- 所有概率必须是 0 ≤ p ≤ 1。
确保了这一点,我们还提供了两种可选的方法来实例化ProbabilityVector
对象(用@classmethod
修饰)。
- 我们随机实例化对象——这在训练时会很有用。
- 我们使用现成的 numpy 数组并在其中使用值,并且只提供状态的名称。
为了方便和调试,我们提供了另外两种请求值的方法。用修饰,它们将 PV 对象的内容作为字典或熊猫数据帧返回。
PV 对象需要满足以下数学运算(为了构建 HMM):
- 比较(
__eq__
)——要知道任何两个 PV 是否相等, - 两个 PV 的逐元素乘法或标量乘法(
__mul__
和__rmul__
)。 - 点积(
__matmul__
) -执行向量矩阵乘法 - 按数字划分(
__truediv__
), argmax
找出哪个州的概率最高。__getitem__
通过按键选择数值。
注意,当一个 PV 乘以一个标量时,返回的结构是一个 numpy 数组,而不是另一个 PV。这是因为乘以 1 以外的任何值都会破坏 PV 本身的完整性。
在内部,这些值存储为大小为(1 × N)的 numpy 数组。
例子
a1 = ProbabilityVector({'rain': 0.7, 'sun': 0.3})
a2 = ProbabilityVector({'sun': 0.1, 'rain': 0.9})
print(a1.df)
print(a2.df)
print("Comparison:", a1 == a2)
print("Element-wise multiplication:", a1 * a2)
print("Argmax:", a1.argmax())
print("Getitem:", a1['rain'])
# OUTPUT
>>> rain sun
probability 0.7 0.3
rain sun
probability 0.9 0.1
>>> Comparison: False
>>> Element-wise multiplication: [[0.63 0.03]]
>>> Argmax: rain
>>> Getitem: 0.7
随机阵
另一个对象是一个Probability Matrix
,它是 HMM 定义的核心部分。形式上, A 和 B 矩阵必须是行随机的,这意味着每一行的值总和必须为 1。因此,我们可以通过堆叠几个 PV 来定义我们的 PM,我们以保证这种约束的方式构建了这些 PV。
class ProbabilityMatrix:
def __init__(self, prob_vec_dict: dict):
assert len(prob_vec_dict) > 1, \
"The numebr of input probability vector must be greater than one."
assert len(set([str(x.states) for x in prob_vec_dict.values()])) == 1, \
"All internal states of all the vectors must be indentical."
assert len(prob_vec_dict.keys()) == len(set(prob_vec_dict.keys())), \
"All observables must be unique."
self.states = sorted(prob_vec_dict)
self.observables = prob_vec_dict[self.states[0]].states
self.values = np.stack([prob_vec_dict[x].values \
for x in self.states]).squeeze()
@classmethod
def initialize(cls, states: list, observables: list):
size = len(states)
rand = np.random.rand(size, len(observables)) \
/ (size**2) + 1 / size
rand /= rand.sum(axis=1).reshape(-1, 1)
aggr = [dict(zip(observables, rand[i, :])) for i in range(len(states))]
pvec = [ProbabilityVector(x) for x in aggr]
return cls(dict(zip(states, pvec)))
@classmethod
def from_numpy(cls, array:
np.ndarray,
states: list,
observables: list):
p_vecs = [ProbabilityVector(dict(zip(observables, x))) \
for x in array]
return cls(dict(zip(states, p_vecs)))
@property
def dict(self):
return self.df.to_dict()
@property
def df(self):
return pd.DataFrame(self.values,
columns=self.observables, index=self.states)
def __repr__(self):
return "PM {} states: {} -> obs: {}.".format(
self.values.shape, self.states, self.observables)
def __getitem__(self, observable: str) -> np.ndarray:
if observable not in self.observables:
raise ValueError("Requesting unknown probability observable from the matrix.")
index = self.observables.index(observable)
return self.values[:, index].reshape(-1, 1)
在这里,我们实例化 PM 的方法是向类的构造函数提供一个 PV 的字典。通过这样做,我们不仅确保 PM 的每一行都是随机的,而且还提供了每个可观察的名称。
因此,我们的 PM 可以给出任何可观测值的系数数组。数学上,PM 是一个矩阵:
其他方法的实现方式与 PV 类似。
例子
a1 = ProbabilityVector({'rain': 0.7, 'sun': 0.3})
a2 = ProbabilityVector({'rain': 0.6, 'sun': 0.4})
A = ProbabilityMatrix({'hot': a1, 'cold': a2})
print(A)
print(A.df)
>>> PM (2, 2) states: ['cold', 'hot'] -> obs: ['rain', 'sun'].
>>> rain sun
cold 0.6 0.4
hot 0.7 0.3
b1 = ProbabilityVector({'0S': 0.1, '1M': 0.4, '2L': 0.5})
b2 = ProbabilityVector({'0S': 0.7, '1M': 0.2, '2L': 0.1})
B = ProbabilityMatrix({'0H': b1, '1C': b2})
print(B)
print(B.df)
>>> PM (2, 3) states: ['0H', '1C'] -> obs: ['0S', '1M', '2L'].
>>> 0S 1M 2L
0H 0.1 0.4 0.5
1C 0.7 0.2 0.1
P = ProbabilityMatrix.initialize(list('abcd'), list('xyz'))
print('Dot product:', a1 @ A)
print('Initialization:', P)
print(P.df)
>>> Dot product: [[0.63 0.37]]
>>> Initialization: PM (4, 3)
states: ['a', 'b', 'c', 'd'] -> obs: ['x', 'y', 'z'].
>>> x y z
a 0.323803 0.327106 0.349091
b 0.318166 0.326356 0.355478
c 0.311833 0.347983 0.340185
d 0.337223 0.316850 0.345927
实现隐马尔可夫链
在我们继续计算分数之前,让我们使用 PV 和 PM 定义来实现隐马尔可夫链。
同样,我们将这样做作为一个类,称之为HiddenMarkovChain
。它将在 A 、 B 和π处进行分页。稍后,我们将实现更多适用于这个类的方法。
计算分数
计算分数意味着在给定我们的(已知)模型λ = ( A , B ,π)的情况下,找出特定观察链 O 的概率是多少。换句话说,我们对寻找 p(O|λ) 感兴趣。
通过边缘化隐藏变量 X 的所有可能链,我们可以找到 p(O|λ) ,其中 X = { x₀, …} :
由于 p(O|X,λ) = ∏ b(O) (与可观测值相关的所有概率的乘积)和 p(X|λ)=π ∏ a (从在 t 的 x 到在 t + 1 的 x 跃迁的所有概率的乘积),我们要寻找的概率(分数【T45
这是一种简单的计算分数的方法,因为我们需要计算每个可能的链 X 的概率。不管怎样,让我们用 python 来实现它:
from itertools import product
from functools import reduce
class HiddenMarkovChain:
def __init__(self, T, E, pi):
self.T = T # transmission matrix A
self.E = E # emission matrix B
self.pi = pi
self.states = pi.states
self.observables = E.observables
def __repr__(self):
return "HML states: {} -> observables: {}.".format(
len(self.states), len(self.observables))
@classmethod
def initialize(cls, states: list, observables: list):
T = ProbabilityMatrix.initialize(states, states)
E = ProbabilityMatrix.initialize(states, observables)
pi = ProbabilityVector.initialize(states)
return cls(T, E, pi)
def _create_all_chains(self, chain_length):
return list(product(*(self.states,) * chain_length))
def score(self, observations: list) -> float:
def mul(x, y): return x * y
score = 0
all_chains = self._create_all_chains(len(observations))
for idx, chain in enumerate(all_chains):
expanded_chain = list(zip(chain, [self.T.states[0]] + list(chain)))
expanded_obser = list(zip(observations, chain))
p_observations = list(map(lambda x: self.E.df.loc[x[1], x[0]], expanded_obser))
p_hidden_state = list(map(lambda x: self.T.df.loc[x[1], x[0]], expanded_chain))
p_hidden_state[0] = self.pi[chain[0]]
score += reduce(mul, p_observations) * reduce(mul, p_hidden_state)
return score
例子
a1 = ProbabilityVector({'1H': 0.7, '2C': 0.3})
a2 = ProbabilityVector({'1H': 0.4, '2C': 0.6})
b1 = ProbabilityVector({'1S': 0.1, '2M': 0.4, '3L': 0.5})
b2 = ProbabilityVector({'1S': 0.7, '2M': 0.2, '3L': 0.1})
A = ProbabilityMatrix({'1H': a1, '2C': a2})
B = ProbabilityMatrix({'1H': b1, '2C': b2})
pi = ProbabilityVector({'1H': 0.6, '2C': 0.4})
hmc = HiddenMarkovChain(A, B, pi)
observations = ['1S', '2M', '3L', '2M', '1S']
print("Score for {} is {:f}.".format(observations, hmc.score(observations)))
>>> Score for ['1S', '2M', '3L', '2M', '1S'] is 0.003482.
如果我们的实现是正确的,那么对于一个给定的模型,所有可能的观察链的所有分值应该加起来是 1。即:
all_possible_observations = {'1S', '2M', '3L'}
chain_length = 3 # any int > 0
all_observation_chains = list(product(*(all_possible_observations,) * chain_length))
all_possible_scores = list(map(lambda obs: hmc.score(obs), all_observation_chains))
print("All possible scores added: {}.".format(sum(all_possible_scores)))
>>> All possible scores added: 1.0.
确实如此。
向前传球得分
用我们上面的方法计算分数有点天真。为了找到特定观察链或的数字,我们必须计算所有可能的潜在变量序列 X 的分数。这需要 2TN^T 乘法,即使是小数字也需要时间。
另一种方法是计算到时间 t 为止的序列的部分观测值。
For 和 i ∈ {0,1,…,N-1} 和 t ∈ {0,1,…,T-1} :
因此,
和
然后
注意 α_t 是一个长度为 N 的向量。乘积之和 α a 实际上可以写成点积。因此:
其中星号表示元素间的乘法。
通过这种实现,我们将乘法次数减少到 N T,并且可以利用向量化。
class HiddenMarkovChain_FP(HiddenMarkovChain):
def _alphas(self, observations: list) -> np.ndarray:
alphas = np.zeros((len(observations), len(self.states)))
alphas[0, :] = self.pi.values * self.E[observations[0]].T
for t in range(1, len(observations)):
alphas[t, :] = (alphas[t - 1, :].reshape(1, -1)
@ self.T.values) * self.E[observations[t]].T
return alphas
def score(self, observations: list) -> float:
alphas = self._alphas(observations)
return float(alphas[-1].sum())
例子
hmc_fp = HiddenMarkovChain_FP(A, B, pi)
observations = ['1S', '2M', '3L', '2M', '1S']
print("Score for {} is {:f}.".format(observations, hmc_fp.score(observations)))
>>> All possible scores added: 1.0.
…是的。
模拟和收敛
我们再测试一个东西。基本上,我们就拿我们的λ=(A, B *,【π】*来说,用它来生成一个随机可观测量的序列,从某个初始状态概率 π 开始。
如果期望的长度 T 足够大,我们将期望系统收敛到一个序列上,该序列平均起来给出与我们直接从 A 和 B 矩阵中期望的相同数量的事件。换句话说,对于每一步,跃迁矩阵和发射矩阵以一定的概率分别“决定”下一个状态是什么,以及我们将得到什么样的观察结果。因此,最初看起来像随机事件的东西,平均起来应该反映矩阵本身的系数。让我们也检查一下。
class HiddenMarkovChain_Simulation(HiddenMarkovChain):
def run(self, length: int) -> (list, list):
assert length >= 0, "The chain needs to be a non-negative number."
s_history = [0] * (length + 1)
o_history = [0] * (length + 1)
prb = self.pi.values
obs = prb @ self.E.values
s_history[0] = np.random.choice(self.states, p=prb.flatten())
o_history[0] = np.random.choice(self.observables, p=obs.flatten())
for t in range(1, length + 1):
prb = prb @ self.T.values
obs = prb @ self.E.values
s_history[t] = np.random.choice(self.states, p=prb.flatten())
o_history[t] = np.random.choice(self.observables, p=obs.flatten())
return o_history, s_history
例子
hmc_s = HiddenMarkovChain_Simulation(A, B, pi)
observation_hist, states_hist = hmc_s.run(100) # length = 100
stats = pd.DataFrame({
'observations': observation_hist,
'states': states_hist}).applymap(lambda x: int(x[0])).plot()
图一。马尔可夫过程的一个例子。显示了状态和可观察序列。
潜在状态
状态矩阵 A 由以下系数给出:
因此,在 t +1 时处于状态“1H”的概率等于:
如果我们假设在之前处于某个状态 at 的概率是完全随机的,那么 p(1H) = 1 和 p(2C) = 0.9,在重正化之后分别给出 0.55 和 0.45。
如果我们计算每个状态出现的次数,并除以序列中元素的数量,随着序列长度的增长,我们会越来越接近这些数字。
例子
hmc_s = HiddenMarkovChain_Simulation(A, B, pi)
stats = {}
for length in np.logspace(1, 5, 40).astype(int):
observation_hist, states_hist = hmc_s.run(length)
stats[length] = pd.DataFrame({
'observations': observation_hist,
'states': states_hist}).applymap(lambda x: int(x[0]))
S = np.array(list(map(lambda x:
x['states'].value_counts().to_numpy() / len(x), stats.values())))
plt.semilogx(np.logspace(1, 5, 40).astype(int), S)
plt.xlabel('Chain length T')
plt.ylabel('Probability')
plt.title('Converging probabilities.')
plt.legend(['1H', '2C'])
plt.show()
图二。概率相对于链长的收敛性。
让我们的HiddenMarkovChain
类更上一层楼,补充更多的方法。这些方法将帮助我们发现观察序列背后最可能的隐藏变量序列。
扩展类
我们已经把α定义为到目前为止部分观察到序列的概率。
现在,我们来定义“相反”的概率。即观察到从 T - 1 到 t 序列的概率。
对于 t= 0,1,…,T-1 和 i=0,1,…,N-1 ,我们定义:
c ` 1 如前,我们可以 β(i) 递归计算:
然后对于 t ≠ T-1 :
其矢量化形式为:
最后,我们还定义了一个新的量 γ 来表示在时间 t 的状态 q_i ,对于该状态,概率(向前和向后计算)是最大的:
因此,对于任何步骤 t = 0,1,…,T-1 ,最大似然的状态可以使用下式找到:
class HiddenMarkovChain_Uncover(HiddenMarkovChain_Simulation):
def _alphas(self, observations: list) -> np.ndarray:
alphas = np.zeros((len(observations), len(self.states)))
alphas[0, :] = self.pi.values * self.E[observations[0]].T
for t in range(1, len(observations)):
alphas[t, :] = (alphas[t - 1, :].reshape(1, -1) @ self.T.values) \
* self.E[observations[t]].T
return alphas
def _betas(self, observations: list) -> np.ndarray:
betas = np.zeros((len(observations), len(self.states)))
betas[-1, :] = 1
for t in range(len(observations) - 2, -1, -1):
betas[t, :] = (self.T.values @ (self.E[observations[t + 1]] \
* betas[t + 1, :].reshape(-1, 1))).reshape(1, -1)
return betas
def uncover(self, observations: list) -> list:
alphas = self._alphas(observations)
betas = self._betas(observations)
maxargs = (alphas * betas).argmax(axis=1)
return list(map(lambda x: self.states[x], maxargs))
确认
为了验证,让我们生成一些可观察的序列或。为此,我们可以使用我们模型的.run
方法。然后,我们将使用.uncover
方法找到最有可能的潜在变量序列。
例子
np.random.seed(42)
a1 = ProbabilityVector({'1H': 0.7, '2C': 0.3})
a2 = ProbabilityVector({'1H': 0.4, '2C': 0.6})
b1 = ProbabilityVector({'1S': 0.1, '2M': 0.4, '3L': 0.5})
b2 = ProbabilityVector({'1S': 0.7, '2M': 0.2, '3L': 0.1})
A = ProbabilityMatrix({'1H': a1, '2C': a2})
B = ProbabilityMatrix({'1H': b1, '2C': b2})
pi = ProbabilityVector({'1H': 0.6, '2C': 0.4})
hmc = HiddenMarkovChain_Uncover(A, B, pi)
observed_sequence, latent_sequence = hmc.run(5)
uncovered_sequence = hmc.uncover(observed_sequence)| | 0 | 1 | 2 | 3 | 4 | 5 |
|:------------------:|:----|:----|:----|:----|:----|:----|
| observed sequence | 3L | 3M | 1S | 3L | 3L | 3L |
| latent sequence | 1H | 2C | 1H | 1H | 2C | 1H |
| uncovered sequence | 1H | 1H | 2C | 1H | 1H | 1H |
正如我们所看到的,最有可能的潜在状态链(根据算法)与实际引起观察的潜在状态链并不相同。这是可以预料的。毕竟每个观察序列只能以一定的概率显现,依赖于潜序列。
下面的代码评估了产生我们的观察序列的不同潜在序列的可能性。
all_possible_states = {'1H', '2C'}
chain_length = 6 # any int > 0
all_states_chains = list(product(*(all_possible_states,) * chain_length))
df = pd.DataFrame(all_states_chains)
dfp = pd.DataFrame()
for i in range(chain_length):
dfp['p' + str(i)] = df.apply(lambda x:
hmc.E.df.loc[x[i], observed_sequence[i]], axis=1)
scores = dfp.sum(axis=1).sort_values(ascending=False)
df = df.iloc[scores.index]
df['score'] = scores
df.head(10).reset_index()| index | 0 | 1 | 2 | 3 | 4 | 5 | score |
|:--------:|:----|:----|:----|:----|:----|:----|--------:|
| 8 | 1H | 1H | 2C | 1H | 1H | 1H | 3.1 |
| 24 | 1H | 2C | 2C | 1H | 1H | 1H | 2.9 |
| 40 | 2C | 1H | 2C | 1H | 1H | 1H | 2.7 |
| 12 | 1H | 1H | 2C | 2C | 1H | 1H | 2.7 |
| 10 | 1H | 1H | 2C | 1H | 2C | 1H | 2.7 |
| 9 | 1H | 1H | 2C | 1H | 1H | 2C | 2.7 |
| 25 | 1H | 2C | 2C | 1H | 1H | 2C | 2.5 |
| 0 | 1H | 1H | 1H | 1H | 1H | 1H | 2.5 |
| 26 | 1H | 2C | 2C | 1H | 2C | 1H | 2.5 |
| 28 | 1H | 2C | 2C | 2C | 1H | 1H | 2.5 |
上面的结果显示了潜在序列的排序表,给出了观察序列。实际的潜在序列(引起观察的序列)位于第 35 位(我们从零开始计算索引)。
dfc = df.copy().reset_index()
for i in range(chain_length):
dfc = dfc[dfc[i] == latent_sequence[i]]
dfc
| index | 0 | 1 | 2 | 3 | 4 | 5 | score |
|:-------:|:----|:----|:----|:----|:----|:----|--------:|
| 18 | 1H | 2C | 1H | 1H | 2C | 1H | 1.9 |
训练模型
是时候展示训练程序了。形式上,我们感兴趣的是找到λ=(A, B ,π) ,使得给定期望的观察序列 O ,我们的模型λ将给出最佳拟合。
扩展类
这里,我们的起点是我们之前定义的HiddenMarkovModel_Uncover
。我们将增加新的方法来训练它。
知道了我们的潜在状态 Q 和可能的观察状态 O ,我们就自动知道了矩阵 A 和 B 的大小,由此得到 N 和 M 。但是,我们需要确定 a 和 b 和 π 。
对于 t = 0,1,…,T-2 和 i,j =0,1,…,N -1 ,我们定义“双γ”:
γ(i,j) 是 q 在 t 到 t + 1 的转移概率。用 *α、*β、 A、、 **B、**来写,我们有:
现在,从实现的角度考虑,我们希望避免同时循环遍历 i、j 和 t ,因为这会非常慢。幸运的是,我们可以对等式进行矢量化:
有了 γ(i,j) 的等式,我们可以计算
要找到λ=()A*,* B ,【π】,我们做
对于 i = 0,1,…,N-1 :
或者
对于 i,j = 0,1,…,N-1:
对于 j = 0,1,…,N-1 和 k = 0,1,…,M-1:
class HiddenMarkovLayer(HiddenMarkovChain_Uncover):
def _digammas(self, observations: list) -> np.ndarray:
L, N = len(observations), len(self.states)
digammas = np.zeros((L - 1, N, N))
alphas = self._alphas(observations)
betas = self._betas(observations)
score = self.score(observations)
for t in range(L - 1):
P1 = (alphas[t, :].reshape(-1, 1) * self.T.values)
P2 = self.E[observations[t + 1]].T * betas[t + 1].reshape(1, -1)
digammas[t, :, :] = P1 * P2 / score
return digammas
有了补充了._difammas
方法的“层”,我们应该能够执行所有必要的计算。然而,将该层的“管理”委托给另一个类是有意义的。事实上,模型训练可以概括如下:
- 初始化 A 、 B 和π。
- 计算 γ(i,j) 。
- 更新模型的 A 、 B 和π。
- 我们重复 2。第三。直到分数 p (O|λ)不再增加。
class HiddenMarkovModel:
def __init__(self, hml: HiddenMarkovLayer):
self.layer = hml
self._score_init = 0
self.score_history = []
@classmethod
def initialize(cls, states: list, observables: list):
layer = HiddenMarkovLayer.initialize(states, observables)
return cls(layer)
def update(self, observations: list) -> float:
alpha = self.layer._alphas(observations)
beta = self.layer._betas(observations)
digamma = self.layer._digammas(observations)
score = alpha[-1].sum()
gamma = alpha * beta / score
L = len(alpha)
obs_idx = [self.layer.observables.index(x) \
for x in observations]
capture = np.zeros((L, len(self.layer.states), len(self.layer.observables)))
for t in range(L):
capture[t, :, obs_idx[t]] = 1.0
pi = gamma[0]
T = digamma.sum(axis=0) / gamma[:-1].sum(axis=0).reshape(-1, 1)
E = (capture * gamma[:, :, np.newaxis]).sum(axis=0) / gamma.sum(axis=0).reshape(-1, 1)
self.layer.pi = ProbabilityVector.from_numpy(pi, self.layer.states)
self.layer.T = ProbabilityMatrix.from_numpy(T, self.layer.states, self.layer.states)
self.layer.E = ProbabilityMatrix.from_numpy(E, self.layer.states, self.layer.observables)
return score
def train(self, observations: list, epochs: int, tol=None):
self._score_init = 0
self.score_history = (epochs + 1) * [0]
early_stopping = isinstance(tol, (int, float))
for epoch in range(1, epochs + 1):
score = self.update(observations)
print("Training... epoch = {} out of {}, score = {}.".format(epoch, epochs, score))
if early_stopping and abs(self._score_init - score) / score < tol:
print("Early stopping.")
break
self._score_init = score
self.score_history[epoch] = score
例子
np.random.seed(42)
observations = ['3L', '2M', '1S', '3L', '3L', '3L']
states = ['1H', '2C']
observables = ['1S', '2M', '3L']
hml = HiddenMarkovLayer.initialize(states, observables)
hmm = HiddenMarkovModel(hml)
hmm.train(observations, 25)
图 3。训练过程中的得分函数示例。
确认
让我们看看生成的序列。“需求”顺序是:
| | 0 | 1 | 2 | 3 | 4 | 5 |
|---:|:----|:----|:----|:----|:----|:----|
| 0 | 3L | 2M | 1S | 3L | 3L | 3L |RUNS = 100000
T = 5
chains = RUNS * [0]
for i in range(len(chains)):
chain = hmm.layer.run(T)[0]
chains[i] = '-'.join(chain)
下表总结了基于 100000 次尝试(见上文)的模拟运行,以及出现的频率和匹配观察的数量。
底线是,如果我们真的训练了模型,我们应该会看到它产生类似我们需要的序列的强烈趋势。让我们看看它是否会发生。
df = pd.DataFrame(pd.Series(chains).value_counts(), columns=['counts']).reset_index().rename(columns={'index': 'chain'})
df = pd.merge(df, df['chain'].str.split('-', expand=True), left_index=True, right_index=True)
s = []
for i in range(T + 1):
s.append(df.apply(lambda x: x[i] == observations[i], axis=1))
df['matched'] = pd.concat(s, axis=1).sum(axis=1)
df['counts'] = df['counts'] / RUNS * 100
df = df.drop(columns=['chain'])
df.head(30)
---
|---:|---------:|:----|:----|:----|:----|:----|:----|----------:|
| 0 | 8.907 | 3L | 3L | 3L | 3L | 3L | 3L | 4 |
| 1 | 4.422 | 3L | 2M | 3L | 3L | 3L | 3L | 5 |
| 2 | 4.286 | 1S | 3L | 3L | 3L | 3L | 3L | 3 |
| 3 | 4.284 | 3L | 3L | 3L | 3L | 3L | 2M | 3 |
| 4 | 4.278 | 3L | 3L | 3L | 2M | 3L | 3L | 3 |
| 5 | 4.227 | 3L | 3L | 1S | 3L | 3L | 3L | 5 |
| 6 | 4.179 | 3L | 3L | 3L | 3L | 1S | 3L | 3 |
| 7 | 2.179 | 3L | 2M | 3L | 2M | 3L | 3L | 4 |
| 8 | 2.173 | 3L | 2M | 3L | 3L | 1S | 3L | 4 |
| 9 | 2.165 | 1S | 3L | 1S | 3L | 3L | 3L | 4 |
| 10 | 2.147 | 3L | 2M | 3L | 3L | 3L | 2M | 4 |
| 11 | 2.136 | 3L | 3L | 3L | 2M | 3L | 2M | 2 |
| 12 | 2.121 | 3L | 2M | 1S | 3L | 3L | 3L | 6 |
| 13 | 2.111 | 1S | 3L | 3L | 2M | 3L | 3L | 2 |
| 14 | 2.1 | 1S | 2M | 3L | 3L | 3L | 3L | 4 |
| 15 | 2.075 | 3L | 3L | 3L | 2M | 1S | 3L | 2 |
| 16 | 2.05 | 1S | 3L | 3L | 3L | 3L | 2M | 2 |
| 17 | 2.04 | 3L | 3L | 1S | 3L | 3L | 2M | 4 |
| 18 | 2.038 | 3L | 3L | 1S | 2M | 3L | 3L | 4 |
| 19 | 2.022 | 3L | 3L | 1S | 3L | 1S | 3L | 4 |
| 20 | 2.008 | 1S | 3L | 3L | 3L | 1S | 3L | 2 |
| 21 | 1.955 | 3L | 3L | 3L | 3L | 1S | 2M | 2 |
| 22 | 1.079 | 1S | 2M | 3L | 2M | 3L | 3L | 3 |
| 23 | 1.077 | 1S | 2M | 3L | 3L | 3L | 2M | 3 |
| 24 | 1.075 | 3L | 2M | 1S | 2M | 3L | 3L | 5 |
| 25 | 1.064 | 1S | 2M | 1S | 3L | 3L | 3L | 5 |
| 26 | 1.052 | 1S | 2M | 3L | 3L | 1S | 3L | 3 |
| 27 | 1.048 | 3L | 2M | 3L | 2M | 1S | 3L | 3 |
| 28 | 1.032 | 1S | 3L | 1S | 2M | 3L | 3L | 3 |
| 29 | 1.024 | 1S | 3L | 1S | 3L | 1S | 3L | 3 |
这是我们不想让模型创建的序列。
| | counts | 0 | 1 | 2 | 3 | 4 | 5 | matched |
|----:|---------:|:----|:----|:----|:----|:----|:----|----------:|
| 266 | 0.001 | 1S | 1S | 3L | 3L | 2M | 2M | 1 |
| 267 | 0.001 | 1S | 2M | 2M | 3L | 2M | 2M | 2 |
| 268 | 0.001 | 3L | 1S | 1S | 3L | 1S | 1S | 3 |
| 269 | 0.001 | 3L | 3L | 3L | 1S | 2M | 2M | 1 |
| 270 | 0.001 | 3L | 1S | 3L | 1S | 1S | 3L | 2 |
| 271 | 0.001 | 1S | 3L | 2M | 1S | 1S | 3L | 1 |
| 272 | 0.001 | 3L | 2M | 2M | 3L | 3L | 1S | 4 |
| 273 | 0.001 | 1S | 3L | 3L | 1S | 1S | 1S | 0 |
| 274 | 0.001 | 3L | 1S | 2M | 2M | 1S | 2M | 1 |
| 275 | 0.001 | 3L | 3L | 2M | 1S | 3L | 2M | 2 |
正如我们所看到的,我们的模型倾向于生成与我们需要的序列相似的序列,尽管精确的序列(与 6/6 匹配的序列)已经位于第 10 位!另一方面,根据该表,前 10 个序列仍然与我们请求的序列有些相似。
为了最终验证我们的模型的质量,让我们将结果与出现频率一起绘制出来,并将其与新初始化的模型进行比较,该模型应该给我们完全随机的序列——只是为了进行比较。
hml_rand = HiddenMarkovLayer.initialize(states, observables)
hmm_rand = HiddenMarkovModel(hml_rand)
RUNS = 100000
T = 5
chains_rand = RUNS * [0]
for i in range(len(chains_rand)):
chain_rand = hmm_rand.layer.run(T)[0]
chains_rand[i] = '-'.join(chain_rand)
df2 = pd.DataFrame(pd.Series(chains_rand).value_counts(), columns=['counts']).reset_index().rename(columns={'index': 'chain'})
df2 = pd.merge(df2, df2['chain'].str.split('-', expand=True), left_index=True, right_index=True)
s = []
for i in range(T + 1):
s.append(df2.apply(lambda x: x[i] == observations[i], axis=1))
df2['matched'] = pd.concat(s, axis=1).sum(axis=1)
df2['counts'] = df2['counts'] / RUNS * 100
df2 = df2.drop(columns=['chain'])
fig, ax = plt.subplots(1, 1, figsize=(14, 6))
ax.plot(df['matched'], 'g:')
ax.plot(df2['matched'], 'k:')
ax.set_xlabel('Ordered index')
ax.set_ylabel('Matching observations')
ax.set_title('Verification on a 6-observation chain.')
ax2 = ax.twinx()
ax2.plot(df['counts'], 'r', lw=3)
ax2.plot(df2['counts'], 'k', lw=3)
ax2.set_ylabel('Frequency of occurrence [%]')
ax.legend(['trained', 'initialized'])
ax2.legend(['trained', 'initialized'])
plt.grid()
plt.show()
图 4。模型训练后的结果。虚线代表匹配的序列。线条代表特定序列的出现频率:已训练的模型(红色)和刚初始化的模型(黑色)。初始化导致序列的几乎完美的均匀分布,而训练的模型对可观察的序列给出强烈的偏好。
看来我们已经成功实施了训练程序。如果我们观察这些曲线,那么初始化的仅模型以几乎相等的概率生成观察序列。完全是随机的。然而,经过训练的模型给出的序列与我们期望的序列高度相似,而且频率要高得多。尽管真正的序列仅在总运行的 2%中被创建,但是其他相似的序列被生成的频率大致相同。
结论
在本文中,我们提出了一个逐步实现的隐马尔可夫模型。我们通过采用第一原则方法创建了代码。更具体地说,我们已经展示了通过方程表达的概率概念是如何作为对象和方法实现的。最后,我们通过发现分数、揭示潜在变量链和应用训练程序来演示模型的使用。
PS。我为这里糟糕的方程式表达道歉。基本上,我需要全部手动完成。然而,请随意阅读我的博客上的这篇文章。在那里,我处理了它;)
还会有更多…
我计划把文章带到下一个层次,并提供简短的视频教程。
如果您想了解关于视频和未来文章的更新,订阅我的 简讯 。你也可以通过填写表格让我知道你的期望。回头见!
聊天机器人对话的隐私风险
基于实体过滤和可搜索加密的隐私保护自然语言处理
摘要 。聊天机器人越来越受欢迎,在不同的垂直领域得到越来越多的采用,例如健康、银行、约会;用户与聊天机器人分享越来越多的私人信息——研究已经开始强调聊天机器人的隐私风险。在这篇文章中,我们提出了两种聊天机器人会话的隐私保护方法。第一种方法应用基于“实体”的隐私过滤和转换,并且可以直接应用于应用(客户端)端。然而,它需要了解聊天机器人的设计才能启用。我们提出了第二个基于可搜索加密的方案,它能够保护用户聊天隐私,而不需要任何聊天机器人设计的知识。最后,我们基于一个真实的员工帮助台聊天机器人给出了一些实验结果,验证了所提出方法的必要性和可行性。
该论文在 2020 年第 34 届 NeurIPS 隐私保护机器学习联合研讨会(PPML-普里姆)上发表。( pdf ) ( SlidesLive 视频录制)。
一.导言
聊天机器人被吹捧为“下一个交互层”,这意味着我们目前通过与网站/应用程序交互来消费信息的方式在许多情况下将被聊天机器人(对话)取代。最近的一份 Forrester 报告[1]对聊天机器人的最新发展水平总结如下:
“尽管消费者对聊天机器人有着复杂的感觉和强烈的反对,但企业理解聊天机器人的价值,并继续将其作为客户支持的主要参与渠道。新冠肺炎疫情的影响增强了组织使用聊天机器人改善服务和参与以及缓解危机局势的决心。在过去的两年中,聊天机器人技术供应商也迅速将对话计算应用于跨领域和垂直领域的客户服务。”
聊天机器人的研究主要集中在提高底层自然语言处理(NLP)的精度[2],[3],这样聊天机器人在理解和响应用户查询方面更加精通。
聊天机器人越来越受欢迎,在不同的垂直领域得到越来越多的应用,例如健康、银行、约会等。,用户用聊天机器人分享越来越多的隐私信息;研究已经开始强调聊天机器人的隐私风险。
然而,提议的方法仅限于明确共享的个人身份信息(PII),例如信用卡号码、银行账户详情、健康状况、约会偏好;以及局限于传统软件安全技术的解决方案,例如存储加密和多因素认证。虽然安全基础知识是绝对需要的,但是文献中还没有解决用户提出的开放式查询的更高级和隐含的隐私风险。
例如,让我们考虑下面的两个用例来理解这种隐私风险的重要性:
用例 1。 情绪分析的隐私风险:情绪分析基本上是一个 NLP 分类任务,它允许聊天机器人根据用户的聊天响应(例如,在帮助台机器人中使用的)来确定用户的(当前)情绪,以便机器人能够根据用户的情绪来调整其响应。
虽然这样做是出于“好的”原因,但现在让我们考虑它们在电子商务场景中的使用。通过动态定价,机器人可以根据用户非常“热情”的查询报出更高的价格。
比如“哇,这件衣服真好看!它的价格是多少?”可能会导致比更中性的查询更高的报价:“这件衣服符合我的要求。它的价格是多少?”
用例 2。 开放式查询(基于位置):让我们考虑开放式查询相对于用户位置所带来的隐私风险。大多数聊天机器人通常是为特定地区设计/部署的。例如,人力资源信息机器人可能是为公司有办事处的地方设计的,类似地,电子商务机器人也将只部署在供应商目前运送其产品的那些国家。鉴于此,诸如“嗨,我现在在日内瓦。到日内瓦的运费是多少?”鉴于供应商不在瑞士交货,不必要地泄露了用户的位置。
在组织方面,一个人力资源聊天机器人由外包供应商维护(在云平台上),部署在日内瓦和克拉科夫;员工经常会问:“我们米兰办公室的餐厅在哪里?”可能会向供应商透露公司员工最近经常出差到米兰办公室。
传统的安全机制,例如限制对聊天机器人日志的访问(通过加密、访问控制策略等。)是不充分的;因为需要分析日志来持续改进机器人[7]。
为了解决聊天机器人对话带来的上述隐私风险,
我们在本文中提出了两种隐私保护方法:基于“实体”的(I)可应用于客户端/应用程序端的隐私过滤和转换(第 II-B 节),以及(II)可独立于聊天机器人设计应用的可搜索加密(第 II-D 节)。
实施/验证结果见第二-C 节,第三节总结了本文件,并为今后的工作提供了一些方向。
二。隐私保护聊天
A.聊天机器人基础知识
我们首先提供一些当前聊天机器人如何工作的背景。在理想的情况下,给定一个自然语言的用户查询,机器人会做出如下响应:
- 理解用户意图;
- 从知识库中检索相关内容;
- 合成答案并回复用户(同样,用自然语言);
- 保留对话内容,以回答用户的任何后续问题。
不幸的是,众多的技术限制使我们无法实现上述工作流程。如今的企业聊天机器人(例如基于 IBM Watson Assistant 、 AWS Lex 、微软 LUIS 、谷歌 Dialogflow 的聊天机器人)首先需要通过提供一组问题、问题变体及其对应的答案来进行训练。这些问题可以分为“意图”几类。问题变体,在机器人术语中称为“话语”,指的是最终用户可以提出相同问题的样本变体。这个想法是提供 5 到 10 个这样的话语(每个问题)作为输入,基于此,机器人将有希望能够理解问题的 50 种不同的变化。大多数机器人引擎使用统计(例如 tf-idf、词袋)和深度学习(例如 BERT)技术的混合来执行意图匹配和情感分析。当没有符合 30%以上置信度(可配置)的意图时,聊天机器人会返回一个后备答案。对于所有其他情况,引擎会随响应一起返回相应的可信度。
B.基于实体的隐私保护
在本节中,我们概述了基于“实体”的隐私保护方法。除了“意图”和“话语”,定制聊天机器人的一个重要部分是提供“实体”[8]。在用例 2 中概述的人力资源机器人的上下文中,实体指特定于领域的词汇,例如,它们可以指办公室位置;并可用于根据用户(查询)位置定制聊天机器人响应。
基于实体的方法通过称为隐私保护聊天模块(PPCM)的模块应用于客户端/应用程序侧。PPCM 的设计依赖于对原始聊天机器人内容和聊天机器人平台所使用的底层 NLP 技术的了解。例如,参考用例 2,PPCM 需要知道在原始聊天机器人设计中使用的实体列表(允许的办公室位置),以便它可以相应地应用必要的隐私保护措施。PPCM 解决方案架构如图 1 所示。它应用了基于过滤和转换的隐私保护技术来解决用户聊天隐私问题。
图一。PPCM 建筑强调基于实体的隐私保护—用例 1(图片由作者提供)
- 过滤:对于用例 2,参考用户查询:“我们米兰办公室的餐厅在哪里?”,PPCM 使用与聊天机器人 NLP 引擎相同的文本提取技术来推断“米兰”是“位置”实体类型的值。随后过滤/删除查询,使其不会被发送到后端 NLP 引擎,并向用户转发适当的消息。
- 转换:为了抵消用例 1 中突出的定价劣势,PPCM 需要将原始的用户查询调整为具有相同语义的更“中性”的响应;使得经转换的查询的用户情感也被聊天机器人 NLP 引擎分类为“中性”,如图 1 所示。抽象形式的转换也可以应用于用例 2 中的“位置”实体类型,其中“米兰”被抽象为“欧洲的某个地方”,以解决用户聊天隐私问题。
C.确认
我们在日内瓦和克拉科夫办事处的员工可以使用的帮助台聊天机器人上验证了提议的基于实体的隐私保护方法。聊天机器人是在 IBM Watson Assistant 上开发的,有大约 400 个意图,涵盖了一系列与办公设备、交通、餐馆、休闲等相关的主题。设施。意向配置文件可在(链接)获得。聊天机器人现已上线 6 个多月,我们注意到聊天机器人在日内瓦和克拉科夫的新员工、短期员工和正式员工中同样受欢迎,这使我们的测试受众达到约 5000 名独立用户。
我们基于对前 10000 个提出的查询的分析报告了一些观察结果。
这些结果验证了我们的假设,即许多员工仍然会像对人类一样对聊天机器人说话。他们不是直接提问,而是先提供一些背景知识来开始提问。
以下是一些示例查询(已编辑以删除公司特定信息):
“救命,我的手机 outlook 又卡住了。服务台在哪里?”“开完第十次会议后,我感到压力很大。Y 餐厅的菜单是什么?”
“你好,我是雅加达办事处的。这座大楼里的邮局在哪里?”
“这里的健身课程和我在伦敦上的一样吗?”
不用说,从员工精神状态(HR)的角度来看,前两个问题是敏感的——揭示了员工在压力和痛苦方面的情绪。最后两个查询不必要地暴露了雇员的基本位置。我们注意到几乎 20%的查询中都嵌入了这种隐私敏感信息。
为了解决上述隐私问题,我们用 Python 实现了一个 PPCM 客户端,使用 AWS Lex 进行情感分析。虽然 Lex 不同于 IBM Tone Analyzer ,聊天机器人用于情感分析的 API 它们都以 0 到 1 之间的值返回情感。因此,我们能够交叉验证情感值,并表明不同的 NLP 引擎可以用于聊天机器人和 PPCM,只要它们具有“相似”的功能。由于 location 是这里唯一的隐私敏感实体,我们可以将其实现为基于位置值字典的解析器。在这两种情况下,情感值大于特定阈值的隐私敏感查询和不支持的实体(位置)都得到了令人满意的处理。
在性能方面,我们能够在允许的 2 秒响应时间内处理两个 API 调用。唯一的缺点是由于额外的 PPCM API 调用,成本加倍。然而,聊天机器人 API 调用越来越便宜,如果成本成为瓶颈,可以利用开源 NLP/聊天机器人引擎,如 RASA 。
D.基于可搜索加密的分布式隐私保护
我们现在处理聊天机器人实现是封闭的(黑盒)的场景——主要适用于外部聊天机器人。我们提出了一个基于可搜索加密的方案,能够保护用户聊天隐私,而不需要任何聊天机器人设计和 NLP 引擎算法的知识。
可搜索加密(SE) [9]是一种保护敏感数据的技术,同时保留了在服务器端搜索的能力(云[10])。SE 允许服务器搜索加密数据,而不会泄漏明文数据中的信息。SE 的两个主要分支是可搜索对称加密(SSE)和带有关键字搜索的公钥加密(PEKS)。在这项工作中,我们关注 PEKS,它允许许多知道公钥的用户生成密文,但只允许私钥持有者创建陷门。
基于[11]中的 PERK 方案,我们为(聊天)实体(见)提出了一个基于可搜索加密的方案。它由以下多项式时间随机化算法组成:
- KGEN(1^k) 输出一个公私钥对: (A_pub,A_priv) 。
- SENC(A_pub,w,m) 在实体 w 和公钥 A_pub 下输出聊天消息 m 的可搜索加密 s_w 。
- DOOR(A_priv,w) 输出一个陷门 t_w ,允许通过实体 w 进行搜索。
- TEST(A_pub,s_w,t _ w’)如果w=*w’*则输出消息 m 。
加密方案 *SEE = (KGEN,SENC,门,测试)*在假设判定性双线性 Diffie-Hellman (DBDH)问题难以解决的情况下,在随机预言模型中抵抗选择明文攻击是语义安全的【12】。
注意,在这种情况下,PPCM 是分布式的,因此我们将 PPCM 的客户端和服务器(云)端组件分别称为 PPCM_C 和 PPCM_S 。解决方案架构如图 2 所示。
图二。分布式 PPCM 突出基于实体的搜索加密—用例 2(图片由作者提供)
给定见方案,启用隐私保护聊天的 PPCM 步骤如下:
初始化
- 手机聊天 App ( PPCM_C )和聊天机器人平台( PPCM_S ),分别简称为 C 和 P ,运行算法 KGEN(1^k) 生成它们的公私钥对: (C_pub,C_priv) 和 (P_pub,P_priv) 。
- C 为实体列表 L 中的所有实体 w 生成陷门 t_w = DOOR(C_priv,w) ,并发送给 P 。
针对每个用户的聊天消息 m:
- C 解析 m 提取 W_m 中的所有 tokens(单词) m 。然后它为 W_m 中的每个令牌 w 生成一个可搜索的加密 s_w = SENC(C_pub,W,m) 。
- C 向 S 发送 W_m 中所有令牌 w 的可搜索加密 s_w 对应的列表 S_w 。
- 对于 L 中的每个实体w,P将其陷门值 t_w 与由 C 共享的可搜索加密值 s_w 进行比较。(仅)在成功匹配时, P 获得聊天消息 m = TEST(C_pub,s_f,t_f) 并基于其设计的对话流使用聊天机器人响应对其进行处理(就像在没有隐私约束的情况下通常会做的那样)。
如果匹配不成功,P 会回复一条“错误处理”消息,并且永远不会看到用户的原始聊天消息,从而保护了用户隐私。
三。结论
我们在本文中概述了两种基于(聊天)实体执行隐私保护对话的方法——应用哪种方法取决于 chatbot 设计和实现架构的透明度(仅客户端/应用程序端与分布式部署)。我们希望,通过解决聊天机器人中日益增长的隐私风险问题,提议的方法将导致企业更多地采用聊天机器人。
参考
- 动词 (verb 的缩写)Srinivasan,A. Sharma,D. Hong,S. Dangi 和 R. Birrell,“对话聊天机器人买家指南”,Forrester,2020 年。
- I. V. Serban、C. Sankar、M. Germain、S. Zhang、Z. Lin、S. Subramanian、T. Kim、M. Pieper、S. Chandar、N. R. Ke、S. Rajeshwar、A. de Brebisson、J. M. R. Sotelo、D. Suhubdy、V. Michalski、A. Nguyen、J. Pineau 和 Y. Bengio,“深度强化学习聊天机器人”,ArXiv,第 abs/1709.02349 卷
- B.Hancock,A. Bordes,P.-E. Mazaré和 J. Weston,“从部署后的对话中学习:喂饱自己,聊天机器人!”2019.
- discover.bot,“聊天机器人安全:将客户隐私放在第一位”,2019 年。
- 僵尸工具,“如何让你的聊天机器人 GDPR 兼容”,2020 年。
- B.Ondrisek,“聊天机器人的隐私和数据安全”,Medium,2020 年。
- E.Ricciardelli 和 D. Biswas,“基于强化学习的自我改进聊天机器人”,第四届强化学习和决策制定多学科会议(RLDM),2019 年。
- W.Shalaby,A. Arantes,T. G. Diaz 和 C. Gupta,“从大规模特定领域知识库构建聊天机器人:挑战和机遇”,ArXiv,第 abs/2001.00100 卷,2020 年。
- Y.王,王,陈,“安全可搜索加密:一个调查”,通讯杂志。Inf。Netw。,第 1 卷,第 52–65 页,2016 年。
- D.Biswas 和 K. Vidyasankar,“移动服务的隐私保护和交易广告”,《计算》,第 96 卷,第 613–630 页,2014 年。
- D.Biswas,S. Haller 和 F. Kerschbaum,“保护隐私的外包分析”,第 12 届 IEEE 商业和企业计算会议,2010 年,第 136-143 页。
- D.Boneh,G. D. Crescenzo,R. Ostrovsky 和 G. Persiano,“使用关键字搜索的公钥加密”,2004 年密码技术理论和应用国际会议,第 506–522 页。
Azure 机器学习 SDK 中运行 AutoML 实验的隐藏技巧
自动化机器学习是机器学习社区中一个快速发展的领域,它使用户能够尝试多种算法并对他们的数据进行预处理转换。与可扩展的基于云的计算相结合,可以找到性能最佳的数据模型,而无需大量耗时的手动反复试验。
这篇博客简要概述了如何从 Azure 机器学习 SDK 运行 AutoML 实验。
自动化机器学习任务和算法
Azure Machine Learning 包括对自动化机器学习的支持,称为 AutoML ,作为 Azure 云产品之一,通过Azure Machine Learning studio中的可视界面,或者使用SDK提交实验。SDK 使数据科学家能够更好地控制自动化机器学习实验的设置,而可视化界面更容易用于代码经验较少或没有代码经验的用户。
Azure 机器学习为以下类型的机器学习任务训练模型:
- 分类
- 回归
- 时间数列预测法
此外,Azure AutoML 包括对这些任务的许多常用算法的支持,包括:
分类算法
- 逻辑回归
- 光梯度推进机
- 决策图表
- 随机森林
- 朴素贝叶斯
- 线性支持向量机(SVM)
- XGBoost
- 深度神经网络(DNN)分类器
- 其他…
回归算法
- 线性回归
- 光梯度推进机
- 决策图表
- 随机森林
- 弹性网
- 拉斯·拉索
- XGBoost
- 其他…
预测算法
- 线性回归
- 光梯度推进机
- 决策图表
- 随机森林
- 弹性网
- 拉斯·拉索
- XGBoost
- 其他…
有关支持的算法的完整列表,请参见文档中的如何定义机器学习任务。
使用 SDK 配置自动化机器学习实验
虽然用户界面提供了一种直观的方式来为您的自动化机器学习实验选择选项,但使用 SDK 可以为用户提供更大的灵活性来设置实验和监控运行。在这里,我列出了指导用户通过 SDK 运行 AutoML 的七个步骤。
步骤 1:创建计算目标
在 Azure 机器学习中,计算目标是运行实验的物理或虚拟计算机。
将实验运行分配给特定计算目标的能力有助于您通过以下方式实施灵活的数据科学生态系统:
- 代码可以在本地或低成本计算上开发和测试,然后移动到更具可扩展性的计算上,以满足生产工作负载的需求。
- 您可以在计算目标上运行最适合其需求的单个进程。例如,通过使用基于 GPU 的计算来训练深度学习模型,并切换到更低成本的纯 CPU 计算来测试和注册训练好的模型。
云计算的核心优势之一是能够通过只为您使用的东西付费来管理成本。在 Azure 机器学习中,您可以通过定义计算目标来利用这一原则:
- 按需启动,不再需要时自动停止。
- 基于工作负载处理需求自动扩展。
有关计算目标的完整文档,请查看此处:
Azure Machine Learning 包括在工作空间中创建计算实例的能力,以提供一个开发环境(Jupyter Notebook、Jupyer Lab、RStudio 和 SSH),该环境由工作空间中的所有其他资产管理。
步骤 2:为 Python 安装 Azure 机器学习 SDK
pip install azureml-sdk
SDK 包括可选的额外功能*,这些功能不是核心操作所必需的,但在某些情况下会很有用。例如,notebooks extra 包括用于在 Jupyter 笔记本中显示详细输出的小部件,automl extra 包括用于自动化机器学习培训的包,explain extra 包括用于生成模型解释的包。要安装 extras,请在括号中指定它们,如下所示:*
*pip install azureml-sdk[notebooks, automl,explain]*
更多信息:关于为 Python 安装 Azure 机器学习 SDK 的更多信息,请参见 SDK 文档。此外,您应该知道 SDK 会定期更新,并查看最新版本的发行说明。
步骤 3:指定培训数据
自动化机器学习旨在使您能够简单地带来您的数据,并让 Azure Machine Learning 找出如何最好地从中训练一个模型。
在 Azure Machine Learning studio 中使用自动化机器学习用户界面时,可以创建或选择一个 Azure Machine Learning 数据集 作为自动化机器学习实验的输入。
使用 SDK 运行自动化机器学习实验时,可以通过以下方式提交数据:
- 指定训练数据的数据集或数据帧,包括待预测的特征和标签。
- 可选地,指定将用于验证训练模型的第二个验证数据数据集或数据帧。如果没有提供,Azure Machine Learning 将使用训练数据应用交叉验证。
或者:
- 指定包含训练特征的 dataset、dataframe 或 numpy 数组的 X 值,以及相应的标签值的 y 数组。
- 或者,指定用于验证的 X_valid 和 y_valid 数据集、数据帧或 X_valid 值的 numpy 数组。
提示 1: AML 具有数据分析的嵌入功能,允许用户浏览他们注册的数据集:
如何监控 AML 中的数据集(微软官方文档)
如果您想在您的 SDK 实验中拥有这个特性,您可以使用实际的 python 包(pandas_profiling
)并且在安装这个包之后,生成[profile report,运行:
*profile = ProfileReport(df, title="Pandas Profiling Report")*
这可以通过简单地显示报告来实现。在 Jupyter 笔记本中,运行:
*profile.to_widgets()*
HTML 报告可以包含在 Jupyter 笔记本中:
包熊猫 _ 剖析(官方 Github 回购)
运行以下代码:
*profile.to_notebook_iframe()*
包熊猫 _ 简介(Github 官方回购)
保存报告
如果您想生成一个 HTML 报告文件,将ProfileReport
保存到一个对象并使用to_file()
函数:
*profile.to_file("your_report.html")*
或者,您可以获取 json 格式的数据:
**# As a string*
json_data = profile.to_json()*# As a file*
profile.to_file("your_report.json")*
包熊猫 _ 剖析(官方 Github 回购)
步骤 4:连接到工作区
在 Python 环境中安装 SDK 包后,您可以编写代码来连接到您的工作空间并执行机器学习操作。连接到工作区的最简单方法是使用工作区配置文件,该文件包括 Azure 订阅、资源组和工作区详细信息,如下所示:
*{
"subscription_id": "<subscription-id>",
"resource_group": "<resource-group>",
"workspace_name": "<workspace-name>"
}*
要使用配置文件连接到工作区,可以使用 SDK 中 workspace 类的 from_config 方法,如下所示:
*from azureml.core import Workspacesubscription_id = '<subscription-id>'
resource_group = '<resource-group>'
workspace_name = '<workspace-name>'try:
ws = Workspace(subscription_id = subscription_id, resource_group = resource_group, workspace_name = workspace_name)
ws.write_config()
print('Library configuration succeeded')
except:
print('Workspace not found')*
步骤 5:配置自动化机器学习实验
用户界面提供了一种直观的方式来为您的自动化机器学习实验选择选项。使用 SDK 时,您有更大的灵活性,并且可以使用 AutoMLConfig 类设置实验选项,如以下示例所示:
*automl_settings = {
"n_cross_validations": 3,
"primary_metric": 'average_precision_score_weighted',
"enable_early_stopping": **True**,
"max_concurrent_iterations": 2, *# This is a limit for testing purpose, please increase it as per cluster size*
"experiment_timeout_hours": 0.25, *# This is a time limit for testing purposes, remove it for real use cases, this will drastically limit ablity to find the best model possible*
"verbosity": logging.INFO,
}
automl_config = AutoMLConfig(task = 'classification',
debug_log = 'automl_errors.log',
compute_target = compute_target,
training_data = training_data,
label_column_name = label_column_name,
**automl_settings
)*
步骤 6:提交自动化机器学习实验
像任何科学学科一样,数据科学包括运行实验*;通常是为了探索数据或构建和评估预测模型。在 Azure 机器学习中,实验是一个命名的过程,通常是脚本或管道的运行,它可以生成指标和输出,并在 Azure 机器学习工作区中被跟踪。*
一个实验可以用不同的数据、代码或设置运行多次;Azure Machine Learning 跟踪每次运行,使您能够查看运行历史并比较每次运行的结果。
您可以像提交任何其他基于 SDK 的实验一样提交自动化机器学习实验:
*from azureml.core.experiment import Experiment
automl_experiment = experiment(ws,'automl_experiment')
automl_run = automl_experiment.submit(automl_config)
automl_run.wait_for_completion(show_output=True)*
步骤 7:检索最佳运行及其模型
你可以很容易地在 Azure Machine Learning studio 中确定最佳运行,并下载或部署它生成的模型。要使用 SDK 以编程方式实现这一点,您可以使用如下示例所示的代码:
*best_run, fitted_model = automl_run.get_output()
print(best_run)
print(fitted_model)*
提示 2:实验运行上下文
除了最佳模型,当您提交一个实验时,您使用它的运行上下文来初始化和结束 Azure Machine Learning 中跟踪的实验运行,如以下代码示例所示:
*automl_run = experiment.start_logging()run = automl_run.get_context() # allow_offline=True by default, so can be run locally as well
...
run.log("Accuracy", 0.98)
run.log_row("Performance", epoch=e, error=err)*
日志记录指标
每个实验都会生成日志文件,其中包含在交互执行期间将被写入终端的消息。这使您能够使用简单的print
语句将消息写入日志。但是,如果您想要记录命名的度量,以便在运行之间进行比较,您可以通过使用 Run 对象来实现;它提供了一系列专门用于此目的的日志功能。其中包括:
- 日志:记录单个命名值。
- log_list:记录一个命名的值列表。
- log_row:记录一个有多列的行。
- log_table:将字典记录为表格。
- log_image:记录图像文件或绘图。
更多信息:有关实验运行期间记录指标的更多信息,请参见 Azure 机器学习文档中的 Monitor Azure ML 实验运行和指标 。
检索和查看记录的指标
您可以查看 Azure Machine Learning studio 中运行的实验记录的指标,或者使用笔记本中的 RunDetails 小部件,如下所示:
*from azureml.widgets import RunDetails
RunDetails(automl_run).show()*
您还可以使用 Run 对象的 get_metrics 方法检索指标,该方法返回指标的 JSON 表示,如下所示:
*best_run_metrics = best_run.get_metrics() # or other runs with runID
for metric_name in best_run_metrics:
metric = best_run_metrics[metric_name]
print(metric_name, metric)*
run 的另一个好方法是get_properties
,它允许您从服务中获取运行的最新属性,并返回一个 dict 类型,可以查询特定的属性,如迭代、算法名、类名和许多其他需要提取的有用特性。
另一个有用的方法get_status
返回的常见值包括“正在运行”、“已完成”和“失败”。
*while automl_run.get_status() not in ['Completed','Failed']:
print('Run {} not in terminal state'.format(atoml_run.id))
time.sleep(10)*
下面的代码示例展示了list
方法的一些用法。
*favorite_completed_runs = automl_run.list(experiment, status='Completed', tags = 'favorite')all_distinc_runs = automl_run.list(experiment)and_their_children = automl_run.list(experiment, include_children=True)only_script_runs = Run.list(experiment,, type=ScriptRun.RUN_TYPE)*
有关方法的完整列表,请参见 Azure ML API 文档 。