用于模型监控的 BigQuery 和 Data Studio
监控模型性能的简单方法
这篇文章将讨论机器学习(ML)模型生命周期中的一个阶段:模型的性能监控。这是您在处理需要持续维护的简单在线系统时面临的事情之一。在许多地方,您可以看到一旦您离线评估了模型,ML 模型的生命周期就结束了。然而,在大多数实际用例中,情况并非如此。
我们生活在一个动态的世界中,数据分布随时都在变化,这就意味着两个月前您认为是好的模型可能不再是好的了。让我们以销售预测为例:人们的行为每次都在变化,改变你的销售的外部变量随时都可能发生。另一个简单的例子是预测天气,一年前训练的模型的表现可能不如有新数据的新模型。许多预测应用程序也是如此。因此,我们需要某种方法来持续监控所有这些变化,并且如果必要的话,根据更新的数据重新训练我们的模型。
在这篇文章中,我们将展示一种简单的方法来监控我们模型的性能,方法是遵循两个简单的步骤:调度和报告。
计划您的预测并保存在 BigQuery 上
如果你读到这篇文章,你已经离线训练并验证了一个模型*。你已经经历了很多工作:数据探索、数据清洗、数据处理、特征工程、模型优化等。但是你仍然觉得你没有完成任务。你可能想知道什么时候*你应该重新培训你的模型。让我假设我们已经有了可以运行的东西:**
*python launch_predictions.py --date current*
我们可以得到任何需要的预测。这太棒了!我们已经有了最重要的东西。现在是时候安排这些预测并将其存储在 BigQuery 中了。
为了安排我们的预测,我建议两种可能性:一种简单而好,另一种更复杂但对许多情况来说更好。先说最简单的。
克朗。UNIX 系统中使用最广为人知的调度程序。通过简单的语法,您可以安排任何您想要的进程。在您的 UNIX 机器上,运行 crontab -e 并添加如下内容。
** * * 15 0 0 docker run launch_predictions --date current*
按照前面的示例,您可以使用简单的语法来调度预测器。这个 docker 容器将每 15 分钟被触发一次。如果您的过程是轻量级的和简单的,这是非常有用的。然而,我们的工作流越复杂,维护 cron 作业就变得越复杂。所以我们转向第二个候选人,一个工作流程编排者。
工作流协调器。第二种选择是使用一些工作流程编排器,如**。如果您的用例更复杂,最好使用工具来调度和监控工作流。例如,您希望监控您的数据管道以及一些管道崩溃的位置和原因。有了 Airflow,我们可以将我们的数据管道开发成有向无环图,使用 GUI 对它们进行调度和监控。这非常有用,比 cron 调度程序覆盖了更多的用例,尤其是在数据科学领域。
Airflow GUI
**让我们将数据存储在 BigQuery 上。 Data Studio 兼容更多数据源,不仅仅是 BigQuery,但是在本帖中,我们将只使用 BigQuery。这里我有一个好消息,将数据保存到 BigQuery 就像将熊猫数据帧导出到 CSV 一样简单!
Pandas 的库已经实现了将我们的数据帧直接转储到 BigQuery 的功能。我们将需要安装 python 包pandas-gbq及其依赖项。然后使用功能熊猫。DataFrame.to_gbq 。如果你想做的不仅仅是将数据存储到 BigQuery 中,我推荐使用包Google-cloud-big query。
使用 Data Studio 构建仪表板
现在我们已经准备好在 BigQuery 中探索所有的预测。在潜入仪表板之前只是一小步。我们需要创建已处理数据的视图。在 BigQuery 中,这非常容易,因为它提供了一个网站 IDE 来生成 SQL 查询。我们需要进行 SQL 查询并生成一个视图。现在,我们准备好可视化我们的数据了!
Creating a view using BigQuery web’s UI
现在让我们转到数据工作室。我们可以使用一些模板来创建我们的仪表板。不过,我鼓励你从头开始写一份报告。在短短几个小时内,我们将成为 Data Studio 的大师,并能够创建有价值的报告。我们可以使用多个数据源来构建我们的仪表板。如果我们关注了这篇文章,我们可能需要选择 BigQuery 和我们的视图。如果我们的数据已经导入,我们需要知道 Data Studio 会缓存它,我们就可以开始播放,而不用担心每次都认为它在查询 BigQuery。我们可以决定数据刷新的频率:每 12 小时(默认)、每 4 小时或每小时。当然,你也可以手动恢复数据——在谷歌文档中有更多关于它如何内部工作的信息。
我们已经导入了数据,是时候开始试验和创建我们的仪表板了。我们有多种方法来可视化我们的数据:动态表格、气泡图、条形图等。我鼓励您使用最适合我们用例的产品!
Data Studio report for sales forecasting online evaluation
上图显示了一个真实案例中的示例,在该案例中,我们对销售预测模型进行了评估,以确定最终销售额。预测存储在 BigQuery 中,用于以后的分析。这个仪表板和一些离线评估的不同之处在于,它会每天更新新的销售额。我们将能够看到我们的性能是否随着时间的推移而下降,或者我们的模型是否稳定。
安排并通过电子邮件发送我们的报告
最后,Data Studio 的另一个有用特性是报告调度。我们可以安排用电子邮件发送报告,这样我们的团队就不会错过任何东西!
Report email scheduling with Data Studio
结论
这篇文章解释了一个简单但强大的方法来监控我们的模型的性能。这里我们不讨论模型的系统健康监控;这超出了本文的范围,Data Studio 可能不是我们最好的工具。
随着时间的推移,您会监控我们模型的性能吗?您有自动化的培训和部署吗?我期待着阅读我们对这一主题的经验的评论。
免费使用 BigQuery 无需信用卡:发现、学习和分享
如果您在注册 BigQuery 时遇到了困难,不用担心——现在注册并开始查询比以往任何时候都容易。新的沙盒模式甚至包括免费存储,不需要信用卡。
重要更新 : 我在 2020 年离开了谷歌,加入了雪花——所以我无法保持更新我的旧帖子。如果你想尝尝雪花啤酒,加入我们吧——我在❄️.玩得很开心
[## 雪花审判
signup.snowflake.com](https://signup.snowflake.com/)
详见官方博文《无信用卡查询:BigQuery 沙箱介绍》。在这里,我们将把重点放在尽快开始使用和查询上。
步骤 0:创建帐户
进入 BigQuery web U I,然后按照提示进行操作。在 60 秒内,不到 4 个步骤,你就可以开始查询了。
步骤 1:我的第一个查询
根据维基百科,让我们找出谁是最著名的艾伦。将此查询输入到新的 BigQuery web UI 中,然后单击“运行”:
SELECT title, SUM(views) views
FROM `fh-bigquery.wikipedia_v3.pageviews_2019`
WHERE DATE(datehour) BETWEEN '2019-01-01' AND '2019-01-10'
AND wiki='en'
AND title LIKE r'Alan\_%'
GROUP BY title
ORDER BY views DESC
LIMIT 10
原来艾伦·图灵是 2019 年前 10 天在英文维基百科中浏览量最多的艾伦。尝试查找其他名称或时间段。你能猜出谁是最有名的史蒂夫吗?
如果您不熟悉 BigQuery 的标准 SQL,我们来分析一下这个查询:
- 这给了我每个标题的总浏览量。
FROM
fh-bigquery.wikipedia_v3.pageviews_2019``:扫描维基百科 2019 年浏览量表。这张桌子是我共享的。WHERE datehour BETWEEN ‘2019–01–01’ AND ‘2019–01–10’
:我们只打算扫描 2019 年的前 10 天。- 有很多维基百科——我只想把重点放在英文版上。这个过滤器工作得很好,因为表 primary clustering 是我想要查询的 Wikipedia。哦,如果我过滤掉
en.m
——手机版英文维基百科——我会收到不同的结果。 AND title LIKE r’Alan\_%’
:查询所有以“艾伦 _”开头的维基百科页面。该过滤器与通过标题的次级聚类一起工作得很好。注意,我在做LIKE
的时候需要躲避_
。GROUP BY title
:我们将为每个标题获得SUM(views)
。ORDER BY views DESC
:根据哪个页面的浏览量最多对结果进行排序。LIMIT 10
:我们只想看到前 10 名。
这里发生了一些有趣的事情:
- 在这 10 天里,维基百科的浏览量超过了 56 亿次。在 BigQuery 中,这由超过 72 GB 的数据表示。在更大的范围内,2018 年维基百科所有浏览量的表格是 2.25 TB。
- 使用 BigQuery 的成本与被扫描的列的大小成正比——这里我们有一个好消息:当使用已经进行了日期分区和聚集的表时,效果会好得多。例如,Alan 查询将花费我 73 GB,但是由于分区和集群,它最终只扫描了 2 GB。这是一个巨大的差异——这意味着我每个月可以免费执行 500 次这样的查询,而不是只有 12 次。
步骤 2:创建自己的表格
假设我们想深入研究关于栈溢出的张量流的所有问题。我们可以编写这样的查询:
SELECT view_count, answer_count, DATE(creation_date) date, title
FROM `bigquery-public-data.stackoverflow.posts_questions`
WHERE 'tensorflow' IN UNNEST(SPLIT(tags, '|'))
ORDER BY view_count DESC
LIMIT 10
Top ten Tensorflow questions on Stack Overflow, by total # of views
这个查询使用了 1.66 GB 的免费配额,每个类似的查询都有类似的成本。我们可以做得更好:将您感兴趣的数据提取到一个新表中。有了 BigQuery 的沙盒模式,现在你还可以免费获得 10 GB 的存储空间。因此,我们可以将所有 Tensorflow 问题提取到一个新的表中,而不是每次都在整个数据集上运行新的查询。
要创建新表,首先创建一个数据集。请注意,如果没有与您的帐户相关联的信用卡,BigQuery 会将任何表的生存期限制为 60 天。
Create a new dataset inside your BigQuery project. Lifetime of a table will be limited to 60 days in sandbox mode.
为了在之前的查询中创建一个新表,BigQuery 现在支持 DDL 和 DML SQL 命令:
CREATE TABLE `deleting.tensorflow_questions`
AS
SELECT view_count, answer_count, DATE(creation_date) date, title
FROM `bigquery-public-data.stackoverflow.posts_questions`
WHERE 'tensorflow' IN UNNEST(SPLIT(tags, '|'))
现在,我可以在新表上编写这样的查询:
SELECT view_count, answer_count, date, title
FROM `deleting.tensorflow_questions`
ORDER BY view_count DESC
好消息:这个查询现在只扫描 3 MB,这给了我更多的自由去试验。我每个月可以免费执行 30 多万次这样的查询!
分享
使用 BigQuery,你可以与你最亲密的朋友或全世界分享你的结果和发现。这在新的 web UI 上还没有实现,但是在 BigQuery classic web UI 上真的很容易:
How to share a BigQuery table on the classic UI
使用 Data Studio 可视化
多种工具和框架可以直接连接到 big query——我们喜欢它们。现在,在新的 BigQuery web UI 中,您可以快速地将结果导入 Data Studio:
Visualizing query results with Data Studio
更进一步
- 订阅 /r/bigquery ,随时了解所有 bigquery 最新消息。
- 查看我们的官方公开数据集和一些非官方数据集。
- 卡住了?询问关于堆栈溢出的社区。
- 想我吗?关注我在 Medium 和 Twitter @felipehoffa 上的最新帖子。
纽约的自行车
Photograph by Hannah McCaughey/Map by Norman Garbush.
有很多关于花旗自行车的故事,以及它如何改变了一些纽约人的出行方式。但是,花旗自行车不仅仅是一种交通工具,它是一个丰富数据的深井,等待着被争论、分割和转换,以找到一两颗关于城市如何运动的智慧珍珠,甚至可能是一些有趣的事实。
作为一名有抱负的数据科学家,使用花旗自行车数据集是一次有趣丰富的经历,其中不乏令人沮丧的插曲,还有*啊哈!*瞬间。
因为数据集适合伟大的可视化,我想我会在不到一周的时间里自学如何使用 D3.js 可视化库,并将其用于这个项目——天真,我知道。我对精通 D3 的陡峭学习曲线知之甚少,更不用说成为专家了。
这时,关键部分出现了,我决定专注于一些我力所能及的事情。
使用大数据集的挑战
花旗自行车的数据集不止几个,所以我决定把重点放在最后一年,从 2018 年 1 月到 2019 年 2 月。仅此一项就给我留下了 19,459,370 个数据点。是的,你没看错。这是一个爆炸加载,在一个熊猫数据帧上——而不是。甚至不要让我开始尝试绘制变量之间的任何关系。
我一直有这样的想法,数据越多越好,所以我从来没有真正想过有太多的数据是一个挑战。但它真的是。不仅仅是因为加载时间,而且正如我之前提到的,在如此大的数据集上使用可视化库只会给我留下一串错误和超时消息,此外还有 RAM 内存有限的问题。所以为了处理大数据集,内存管理应该是你的首要任务之一。
幸运的是,经过一点努力和创造性的思考,我能够扭转局面。首先,我开始从数据集中随机抽取样本,然后我有了自己的*啊哈!*时刻:我可以使用 SQL 查询创建我想要可视化的数据和关系的子集,然后将 SQL 查询结果转换成大多数可视化库可以处理的 CSV 文件。问题解决了。
数据
既然我已经和你分享了我的一些痛苦,我也应该分享一些我的结果。
如前所述,从 2018 年 1 月到 2019 年 2 月,共登记了 19,459,370 次旅行。经过一些清理、切片和争论,我的最终工作数据集减少到 17,437,855 次。这些只是订户的旅行,因为我决定放弃临时乘客和一日游顾客。
根据花旗自行车的月度报告,花旗自行车的年度会员总数现已达到 150,929 人。让我们来看看他们是谁。
谁在骑花旗自行车?
没有太多关于每个用户的信息,但是从数据中,我们可以得到基于总用户数的年龄和性别。这些汇总并没有给出准确的订阅者数量,而是给出了样本的基本分布。
这是一个很好的学习机会,可以利用 Plotly 制作一些互动的情节。我发现一开始理解图层次可能有点繁琐。Plotly 的图表令人惊叹,因为尽管它是使用 Python 和 Django 框架构建的,但在前端它使用了 JavaScript 和 Dr.js 库— 所以毕竟,我确实使用了一点 D3 . js。
订阅者的最高出生年份类别是从 1987 年到 1991 年。让我再次声明一下,Citi Bike 目前有 150,929 名订户,为了获得这些订户的分布情况,我对乘客数据使用了聚合函数,如下面的代码片段所示。
Pandas DataFrame Capturing Birth Year SQL Query
Subscribers by Year of Birth
Code for Interactive Plotly Bar Chart
从性别来看,大多数骑手都是男性。
Pandas DataFrame from SQL query to identify Gender distribution
An interactive bar chart showing subscribers by gender. Male (1), Female (2)
他们要骑多久?
平均行程持续时间为 13 分钟,这意味着用户不会长途骑行——记住,我放弃了临时骑手和一次性客户。
我们还可以看看一周中每天的平均出行次数,正如所料,工作日的出行次数略高于周末。展示了周末乘车和工作日通勤之间的区别。
An interactive plot where circle size represents the average trip duration.
最后,我对骑行的长度和一年中的某一天之间的关系感兴趣。
An interactive plot showing the number of trips per day of the year.
该图给出了全年对花旗自行车需求波动的完整图像,当天气较暖时,4 月至 10 月期间的需求较高。这让我对天气和一天旅行次数之间的关系感到好奇,所以我用来自国家海洋和大气管理局的数据创建了一个新的熊猫数据框架,在我的原始数据框架中有每天的天气摘要。
然后我使用 Scikit Learn 库运行了一个多元回归算法。
Multiple Regression using Scikit Learn
事实证明,每天旅行次数的 62%的差异可以用天气来解释。
他们要去哪里?
一些花旗自行车停靠站肯定比其他更受欢迎。为了绘制最受欢迎的地图,包括骑行的开始和结束,我使用了叶子库。
但在我实际制作一个交互式地图之前,我运行了一个 SQL 来获取骑行量排名靠前的花旗自行车停靠站。我第一次试着把它们都映射出来,但是最终,程序崩溃了。所以我决定接受 100 美元。
A video that captures the functionality of a Folium interactive map
Code to create an interactive map using the Folium library
乘客量排名前五的车站是:
- 潘兴广场北,1,576,381 人次
- 西 21 街和第六大道,1,148,192 次出行
- 东 17 街&百老汇,1,121,953 次出行
- 百老汇和东 22 街,109,7314 次出行
- 百老汇和东 14 街 96,901 次旅行
具体来说,这些是中央车站、麦迪逊广场公园、联合广场公园和熨斗大厦附近的码头站。
30657 号自行车呢?
最后,我想表彰去年出行次数最多的自行车,30657 — 你现在想知道你是否曾经骑过它,是吗?。
骑了 2776 次,总共行驶了 36448 分钟,这辆自行车可能比我更了解纽约。所以,作为临别礼物,我将把自行车 30657 留给你,这样你就可以看到它的运行。
来源
用逻辑回归进行二元分类
在线广告点击率的估算
在绩效营销中,一个重要的关键绩效指标(KPI)是由点击率(CTR)给出的。点击率是点击特定链接的用户与查看页面、电子邮件或广告(ad)的用户总数的比率。
估计 CTR 是一个二元分类问题。当用户观看广告时,他要么点击(y=1)
要么不点击(y=0)
。只有两种可能的结果,让我们使用逻辑回归作为我们的模型。与用于推断连续变量的线性回归相反,逻辑回归用于估计任意数量的离散类。我给出了一个简单的可视化,它为三个主要的数据科学问题提供了正确的模型:
How to choose a model
在这个故事中,在将学到的一切应用于 Kaggle 的“点击率预测”挑战之前,我想先引导您了解逻辑回归的技术细节。
二元逻辑回归:理论
逻辑回归的特征在于一个逻辑函数来模拟标签 Y 变量 X 的条件概率
The conditional probability.
在我们的例子中,Y 表示被点击或未被点击的状态,X 表示我们想要选择的特征(例如设备类型)。
我们将使用 m 个观察值,每个包含 n 个特征。对于它们中的每一个,我们将有 m 个 n+1 维的行向量 xᵢ。我们的标签 Y 只能是 0 或 1。参数将在 n+1 维的列向量θ中给出。
Definitions of Y, X and Θ.
用户点击给定观察值 X 的条件概率可以建模为 sigmoid 函数。
The conditional probability modeled with the sigmoid logistic function.
逻辑回归的核心是 sigmoid 函数。sigmoid 函数将连续变量映射到闭集[0,1],然后可以将其解释为概率。右手边的每个数据点被解释为y=1
,左手边的每个数据点被推断为y=0
。
A plot of the sigmoid function with labeled sample data.
衍生(可选)
在推导条件概率时,sigmoid 函数自然出现。我们可以用贝叶斯定理来表示 P(Y|X)
Bayes’ theorem
根据贝叶斯解释
- P(Y|X)为后验,
- P(Y)为先验,
- 和 P(X)作为归一化因子。
我们将拟合数据的后验和先验,并且必须去掉未知的概率 P(X)。这可以通过使用补充条件概率来完成。
Complement conditional probability.
当将后验概率除以互补条件概率并取对数时,我们得到对数优势(logit)
The logit can be modeled as a linear function of X.
**这里我们假设 logit 是 X 中的线性函数!**现在,我们只需撤销对数并求解后验概率,即可导出 sigmoid 函数
Sigmoid derivation.
最大似然估计
到目前为止,我们已经用一组参数θ模拟了后验概率。我们如何确定θ的最佳选择?用户点击的条件概率等于 sigmoid 函数。所有情况的概率总和必须等于 1。因为我们只有两种情况,所以我们可以找到一种优雅的方式在一个表达式中表达这两种概率:
The probability mass function of the Bernoulli distribution.
右边是伯努利分布的概率质量函数(PMF)。伯努利分布描述了一个随机变量,它可以采取两种结果中的一种,比如我们的标签被点击或未被点击。现在,为了确定我们的参数θ,我们需要在只给定一个样本的情况下,最大化复制总体分布的概率。这种方法被称为最大似然估计 (MLE)。我们主要是将样本中每个事件的所有概率结合起来。这种联合概率被称为似然性,它与概率有许多共同之处,但主要集中在参数上
The likelihood.
我们可以将上面的函数最大化,但是为了方便起见(为了获得更漂亮的导数),我们将对数应用于可能性。我们可以这样做,因为对数是单调递增的,因此保留了最大值的位置。通过应用对数,乘积变成和
The log-likelihood.
为了最大化对数似然,我们可以使用微积分。极值点的导数必须等于零
The first derivative of the log-likelihood.
sigmoid 函数的导数(可选)
在最后一个结果中,我们使用了 sigmoid 函数对θ的导数。推导过程如下
Derivation of the derivative of the sigmoid function.
牛顿-拉夫森
为了执行 MLE,我们必须找到对数似然的一阶导数的根。我们可以使用牛顿-拉夫森求根算法来完成这项任务。牛顿-拉夫森法是最大化对数似然的标准方法。它需要计算二阶导数。在我们的例子中,我们可以通过分析来确定它。在其他情况下,二阶导数在计算上是昂贵的,我们可以使用*梯度下降(上升)*进行优化。二阶导数由下式给出
The second derivative of the log-likelihood.
牛顿-拉夫森方法告诉我们如何更新每次迭代的参数。
Newton-Raphson iteration.
估计 CTR
在这个故事的第二部分,我们想编写我们自己的逻辑回归实现。我制作的 Jupyter 笔记本已经作为要点出版。我们将使用“点击率预测”Kaggle 竞赛的数据。下载数据后,我们对其进行解包,并在对完整集进行训练之前准备一个 10000 行的样本。
unzip avazu-ctr-prediction.zip
gunzip train.gz
head -n10000 train > train_sample.csv
然后,我们将 CSV 加载到 panda 数据帧中,并将其分为训练集和测试集
df = pd.read_csv('train_sample.csv')
msk = np.random.rand(len(df)) < 0.8
train = df[msk]
test = df[~msk]
现在你应该关注特性探索,但是为了简单起见,我选择了列 device_type、C1、C15 和 C16 作为特性列。然后我可以准备我的特征矩阵 X 并使用点击列作为标签
m = len(train)
X_train = np.ones((m, 5))
X_train[:,1] = train.device_type.to_numpy()
X_train[:,2] = train.C1.to_numpy()
X_train[:,3] = train.C15.to_numpy()
X_train[:,4] = train.C16.to_numpy()y_train = train.click.to_numpy()
为了使我们的算法工作,我们需要先前导出的对数似然的一阶和二阶导数,其可以编码如下
def DLogLikelihood(X, y, theta):
res = np.zeros(theta.shape[0])
for i in range(0, X.shape[0]):
x_i = X[i]
y_i = y[i]
res += x_i * (y_i - sigmoid(np.dot(theta, x_i)) )
return resdef DDLogLikelihood(X, theta):
res = np.zeros((theta.shape[0], theta.shape[0]))
for i in range(0, X.shape[0]):
x_i = X[i]
sigma = sigmoid(np.dot(theta, x_i))
res += np.outer(x_i, x_i) * sigma * ( 1 - sigma )
return -res
迭代 Netwon-Raphons 步骤和我们的逻辑回归算法然后
def NewtonRaphsonTheta(X, y, theta):
return theta - np.dot(
np.linalg.inv(DDLogLikelihood(X, theta)),
DLogLikelihood(X, y, theta))def logisticRegression(X, y, epochs=100):
theta = np.zeros(X.shape[1])
for i in range(epochs):
theta = NewtonRaphsonTheta(X, y, theta)
return theta
通过调用logisticRegression(X, y)
,我们将迭代计算参数θ,然后可以用它来预测用户的点击概率
def predict(X, theta):
res = np.zeros(X.shape[0])
for i in range(len(res)):
x = X[i]
res[i] = sigmoid(np.dot(theta, x))
return res
对于试运行,我们得到以下概率
theta = logisticRegression(X_train, y_train, epochs=100)
y_pred = predict(X_test, theta)print(y_pred)
[0.18827126 0.16229901 … 0.16229901 0.16229901 0.16229901]
为了评估该模型,我将测试集的预测与它们的实际值进行了比较,结果显示该模型相当差。为了改进,我们可以在特征选择上花更多的时间,在更多的数据上进行训练,同时不断地用评估指标测量模型性能,如对数损失或 ROC 曲线。
总结
- 逻辑回归用于多分类问题
- 如果我们只有两个类,则使用二元逻辑回归
- P(Y|X)由 sigmoid 函数建模,该函数从(-∞,∞)映射到(0,1)
- 我们假设 logit 可以建模为线性函数
- 为了估计参数θ,我们最大化对数似然
- 伯努利分布是具有两种可能结果的离散分布,用于二元分类
- 我们使用牛顿-拉夫森作为求根器,因为我们可以很容易地计算对数似然的二阶导数
[2]:统计学习的要素,t .哈斯蒂,r .蒂布拉尼,j .弗里德曼https://web.stanford.edu/~hastie/ElemStatLearn/
HandySpark 简化二元分类器评估
TLDR;
HandySpark 是一个 Python 包,旨在改善 PySpark 用户体验,尤其是在涉及到探索性数据分析时,包括可视化功能,以及现在的二进制分类器的扩展评估指标。
使用 Google Colab 亲自尝试一下:
使用 HandySpark 探索泰坦尼克号
colab.research.google.com](https://colab.research.google.com/github/dvgodoy/handyspark/blob/master/notebooks/Exploring_Titanic.ipynb)
检查存储库:
HandySpark——带来熊猫般的能力,激发数据帧——dvgodoy/HandySpark
github.com](https://github.com/dvgodoy/handyspark)
介绍
在我之前的帖子中,我介绍了 HandySpark ,这是我为 PySpark 开发的一个包,用来帮助缩小熊猫和 Spark 数据帧之间的差距。
今天,我很高兴地宣布新版本的发布,它不仅解决了分层操作的一些性能问题(现在应该快几倍了!),而且使得评估二进制分类器更加容易。
二元分类任务
我们将使用一个简单的二进制分类任务来说明 HandySpark 提供的扩展评估功能:再一次使用泰坦尼克号数据集预测乘客存活率。
让我们首先设置好一切,然后加载我们的数据:
Setting everything up and loading the Titanic dataset 😃
要评估一个模型,我们需要先训练它。为了训练一个模型,我们需要首先清理数据集。所以,就从这点开始吧!
使用 HandySpark 清理数据
我们知道Age, Cabin
和Embarked
缺少值。你可以使用[isnull](https://dvgodoy.github.io/handyspark/handyspark.html#handyspark.HandyFrame.isnull)
方法很容易地检验这一点。
为了让我们的模型和管道尽可能简单,让我们只使用数字 变量,比如Age
、Fare
、SibSp
和Parch.
对于输入Age
的缺失值,我们可以只对每个缺失值使用一个简单平均值,对吗?但是你真的认为一等舱、二等舱和三等舱的男女乘客的年龄差不多吗?
让我们使用[stratify](https://dvgodoy.github.io/handyspark/handyspark.sql.html#handyspark.sql.dataframe.HandyFrame.stratify)
操作来检查一下,就像这样:
hdf.stratify(['Pclass', 'Sex']).cols['Age'].mean()Pclass Sex
1 female 34.611765
male 41.281386
2 female 28.722973
male 30.740707
3 female 21.750000
male 26.507589
Name: Age, dtype: float64
这显然是有区别的……女性比男性年轻,阶级越低,乘客越年轻。毫不奇怪,我会说…
那么离群值呢?我们可以使用图基的栅栏方法来识别然后栅栏值被认为是极端的。对于Fare
,我们有多少离群值?
hdf.cols['Fare'].outliers(k=3)Fare 53
Name: outliers, dtype: int64
请记住,图基的栅栏非常敏感——他们假设高于 k 乘以四分位数间距(IQR) 的一切都是极值。在我们的例子中,它导致了 53 个异常值!你可以尝试不同的 k 值来校准它,尽管…
所以,让我们用我们发现的数据来清理它。首先,我们根据给定Pclass
和Sex
的平均值填充缺失的Age
值。接下来,我们使用 Tukey 的方法保护 Fare
值:
Cleaning up!
建立模型
一旦我们有了干净的数据集,我们就可以建立一个简单的分类器来预测乘客是否在泰坦尼克号灾难中幸存。让我们使用 Spark 的RandomForestClassifier
来完成这个任务。
但是,记住 Spark ML 算法需要将所有特征整齐地组装成一个特征向量。此外,该特征向量不接受缺失的值。
这个怎么处理?我们可以简单地使用 HandySpark 的imputer
和fencer
方法来创建相应的变压器来填充缺失值和隔离异常值。然后,我们将这些变压器添加到我们的管道中,我们就可以开始了!
Training a classifier and making predictions
我们的预测是什么样子的?让我们把数据帧做成一个手帧,看看我们的标签和分类器生成的预测:
predictions.toHandy().cols[['probability', 'prediction', 'Survived']][:5]
First 5 rows from our predictions
概率列包含一个向量,其概率分别与类别 0 和 1 相关联。为了评估的目的,我们需要成为阳性病例的概率,因此我们应该查看概率向量的第二个元素。预测栏显示了相应的类,假设阈值为 0.5 。
评估模型
我们的模型有多好?我们可以只计算具有匹配预测和幸存列的行的比例,这就是我们的准确度。然而,事实证明,准确度是而不是评价二元分类器的一个很好的标准。
为了真正说明我们的模型有多好,我们需要其他指标,如真阳性率 ( TPR ,也称为召回)、假阳性率 ( FPR )和精度,这将 随我们选择的阈值而变化*,从而将预测概率转化为预测类别(0 或 1*
如果我们检查每一个可能的阈值并计算这些度量,我们可以为给定的模型建立接收器工作特性 ( ROC )曲线和精度召回 ( PR )曲线。
这就带来了另一个问题:如何利用这些曲线比较两个模型?它们可能在不同的点交叉,对吗?解决这个问题的一个方法是查看曲线下的面积:粗略地说,面积越大,模型越好。这样我们就可以计算 ROC** 曲线下的面积( AUROC 、 ROC AUC ,或者有时只是 AUC )和 PR** 曲线下的面积( PR AUC )。
如果你想了解所有这些指标的更多信息,请查看这些帖子:被困惑矩阵、接收器操作特性曲线揭秘、了解 AUC — ROC 曲线和了解 ROC 曲线(互动)。
使用 PySpark 评估模型
我们既有好的也有一些坏的消息……好的消息是: PySpark 给了我们两个 ROC AUC 和 PR AUC。坏消息是: PySpark 只给了我们那个:-(
Using PySpark Evaluator to get ROC AUC and PR AUC
如果我们想用不同的阈值做实验呢?出手相救:-)
使用 HandySpark 评估模型
HandySpark 扩展了 PySpark 的BinaryClassificationMetrics
,因为它的 Java 对应物已经有了几个检索指标和阈值的方法,现在由 HandySpark 公开。但是也有新实现的方法。
HandySpark 还可以使用包含预测概率和标签的数据帧作为自变量,如下要点所示:
Plotring ROC and PR curves, getting metrics by threshold and confusion matrices with HandySpark!
让我们深入研究评估模型的所有这些新的可能性。
绘制曲线
一张图片胜过千言万语!所以,让我们从使用plot_roc_curve
和plot_pr_curve
方法的两条曲线的图开始。就这么简单:
fig, axs = plt.subplots(1, 2, figsize=(12, 4))
bcm.plot_roc_curve(ax=axs[0])
bcm.plot_pr_curve(ax=axs[1])
ROC and PR curves
瞧啊!现在我们可以说,如果我们愿意接受 20%的假阳性率,我们将得到 60%以上的真阳性率。够好吗?酷!
你会问,我们应该使用哪个阈值来实现这一点?
阈值和指标
我们可以使用getMetricsByThreshold
方法获得所有阈值和相应的指标。它返回一个 Spark 数据帧,然后我们可以过滤出感兴趣的指标(在我们的例子中,FPR 介于 19%和 21%之间):
bcm.getMetricsByThreshold().filter('fpr between 0.19 and 0.21').toPandas()
Thresholds for FPR between 19% and 21%
我们需要的误报率最多为 20%,那么对应的阈值为 0.415856 。这将给我们 68.1%的真阳性率 ( 召回)和 68.3%的精度。
我们还可以查看使用特定的阈值构建的混淆矩阵,这产生了我们刚刚获得的指标。
混淆矩阵
混淆矩阵可以,呃… 混淆!;-)为了尽量减少阅读时可能产生的任何误解,我已经用标记了列和行,所以你不需要猜测哪个类先出现,哪个是预测值和实际值。
用你选择的阈值调用print_confusion_matrix
就行了,就这么简单:
bcm.print_confusion_matrix(.415856)
Confusion Matrix — not so confusing anymore 😃
最后的想法
我的目标是改善 PySpark 用户体验,让执行数据清理和模型 评估变得更加容易。不用说,这是一项正在进行的工作,我已经计划了更多的改进。
如果你是使用 PySpark 的数据科学家,我希望你尝试一下 HandySpark 并让我知道你对它的想法:-)
HandySpark——带来熊猫般的能力,激发数据帧——dvgodoy/HandySpark
github.com](https://github.com/dvgodoy/handyspark)
如果你有任何想法、评论或问题,请在下方留言或联系我 推特 。
生物信息学:代码与生物学相遇的地方
医疗保健的未来简介
你和我有 30 亿个共同点。或者接近它。不只是你和我,其他人都一样。
这些东西会是什么呢?
我喜欢运动。你知道吗?
机器学习怎么样?我很喜欢。
我们可以继续努力,找出答案,但要达到 30 亿美元还需要一段时间。
即使我们成功了,我们也会比较错误的东西。
好吧,什么是正确的事情?
我们的 DNA。或者更具体地说,结合形成核酸的核碱基,这些核酸链在一起构成我们的 DNA。
DNA 是生命的语言。这是为所有生物奠定基础的密码。
就像一本书是由字母表的字母组合而成,你和我是由大约 32 亿个核碱基组合而成——腺嘌呤(A)、胸腺嘧啶(T)、胞嘧啶©和鸟嘌呤(G)。这个集合序列被称为基因组。
如果我们把 32 亿左右的 A、C、G 和 T 放在一起,30 亿左右是匹配的,但 2000 万到 3000 万是不同的。这些差异可以解释为什么我是金发而你是不同的颜色。如果你也是金发碧眼,我们可以把它添加到共同点列表中。
Although most of our DNA is the same, if we looked closely, we’d find a few differences. A difference of 1 letter is known as a SNP (single nucleotide polymorphism).
我在这里漏掉了一些东西,例如,不同的字母组如何编码蛋白质,其他组如何组成基因,基因内部如何有不同的版本等等。
为什么?
因为 DNA 是一种复杂的动物。理解所有 32 亿个字母是如何相互作用的仍然是一个很大的研究课题。众所周知,一个人不可能理解这样一种语言。
这就是计算机力量的来源。当生命的语言遇到自然的语言。这也是生物信息学出现的地方。
生物信息学结合了生物学、计算机科学、数学、统计学的原理来理解生物学数据。
寻找复制的起源
在 Coursera 生物信息学专业中的第一个示例问题涉及寻找复制的起源。复制起点是复制开始的基因组序列。
在你出生之前,你开始是一个单细胞,然后一个在两个之间,两个变成四个,最后,四个变成你。对于第一个分裂的细胞,它必须复制自己的基因组。后续的每个细胞也是如此。
了解了这一点,你就可以开始想象寻找复制的起源是多么有价值。
比如说有一组细胞内含有非常擅长抗癌的蛋白质。我们怎样才能获得更多这样的细胞来增强我们的防御呢?
一种方法可能是在我们的好细胞中寻找原始复制,找到它,然后利用这些信息在体外产生更多的细胞,然后再把它们放回体内。
让我们试试。
故事时间
因为你的生物学家朋友知道你一直在练习你的编码技能,她来找你寻求帮助。
A fictional sequence of 1000 nucleobases which combine to create a DNA sequence of a strong cancer-fighting cell.
经过多次实验,从上面的细胞样本中切下部分 DNA,观察细胞是否会复制。她认为她已经找到了复制的起源。
可以肯定的是,她想知道它在整个序列中出现了多少次,以及它是否足够重要,足以成为复制的真正起源。
她给你看文件,然后你开始工作。
The code you write for your friend to find how many times a pattern occurs in a sequence of DNA.
稍加修改后,您认为您的代码可以运行了。它通过 DNA 序列寻找模式,如果找到匹配,它更新计数器。当它到达 DNA 模式的末尾时,它返回计数(该模式在 DNA 序列中出现的次数)。
“什么格局?”你问。
“TGTAGTGTG。”
你帮你的朋友运行代码。
结果返回为 18。
她发现的模式在好细胞的 DNA 序列中出现了 18 次。
“这有意义吗?”你问。
“它发生一次的概率小于 0.004%,所以 18 次肯定意味着什么,但我必须核实一下。”
你擅长编码,但不擅长统计,你的生物学家朋友也不擅长。她回到实验室找到她的统计学家朋友,进行更多的测试。
这里发生的是几个领域的结合。你的生物学家朋友通过实验发现了一个潜在的复制起源的方法,但这是漫长而乏味的。为了帮忙,你提供了一些你的计算机科学技能。然后找出你的结果是否有统计学意义。
这种不同领域的交叉是生物信息学发挥作用的一个例子。每个领域都带来了真知灼见,但将它们放在一起会让它们更有价值。
医疗保健的未来
上面的场景是一个简化的例子,真实世界中的生物信息学需要更多的步骤。
首先,你如何得到一个基因组?
幸运的是,在过去的几十年里,这部分已经有了快速的发展。第一个人类基因组测序花了 23 年时间,世界各地的团队和数十亿美元。现在,你可以在几天内以不到 15,000 美元的价格完成这项工作。
好了,你有了一个基因组,现在你要做什么?
好问题。这是用不同的统计和计算方法探索 DNA 的新方法正在积极研究的地方。
你在上面看到的 DNA 序列是线性的,单个字符串。但实际的 DNA 是不同的,它由两条链组成(一条链是另一条链的反面),不同的区域并不总是以相同的方式相互作用。所以找到一个模式可能不像我们看到的那样简单。
好吧,假设你对基因组有了更多的了解,它是如何被应用的呢?
生物信息学最有趣的应用是个性化医疗。为个人量身定制的医疗保健,而不是为个人量身定制的医疗保健。
我有幸亲眼看到的一个项目是由 Max Kelsen 开发的免疫疗法结果预测或 IOP。目标是使用全基因组数据来开发预测癌症患者免疫治疗结果的工具。有些患者对免疫疗法的反应比其他人好,为什么?答案会隐藏在它们的基因组中吗?
生物信息学的应用也不仅限于预测癌症治疗的结果。
营养基因组学研究食物和基因组的相互作用。想象一个专门针对你的 DNA 制定的饮食计划。
我爸爸每天都要服用一些药物来缓解他的帕金森氏症和老年痴呆症的症状。服用它们的最初几年并不顺利。只是在尝试了几轮不同的药物后,他才发现一些没有让他感觉更糟的药物。药物基因组学——根据一个人的基因组制造药物——能帮助其他人更快找到合适的药物吗?
健康和科技不会很快消失。我们只是触及了这两个领域交汇处的表面。
我进入生物信息学是几天前开始的,在 Coursera 上开始了生物信息学专业。但是随着我学习和经历的增多,我一定会分享我的发现。请务必继续关注。
这篇文章的视频版本可以在 YouTube 上找到。
为了计算 0.004%的数字,将(1000–9+1)*(0.25)⁹
(长度为 9 的 A,C,G,T 的某个模式出现在 A,C,G,T 的随机序列中的概率)的结果四舍五入到最接近的千分之一。
像 23 和 me 这样的公司进行基因测试的费用要低得多,然而,这些服务使用基因分型而不是全基因组测序。
生物数据科学和为什么领域专业知识和背景是王道
如果我给你看一张猫的照片,告诉你那是一只粉红色的熊猫,你会相信我吗?
如果我给你看一张猫的照片,告诉你那是一只粉红色的熊猫,你会相信我吗?你将如何证实我告诉你的是真的?你可以天生做到这一点,因为你有一个内置的神经元网络(不要与神经网络混淆),你可以自己决定算法的图像分类是否正确。在 ImageNet 的情况下,这大约是 94%的精确度。在生物数据科学中,这是不可能的,这就是为什么它如此依赖于专业知识和背景。
Stanford CS231n
生物数据科学和验证结果
如果我给你看一个金发的基因序列,并告诉你它是雀斑的编码,你会怎么做?你会相信我的话吗?我是说,我们才刚认识,而且是在网上认识的。你已经那么信任我了吗?!
不,很可能你必须依靠第二种算法,最有可能是爆炸,来告诉你我给你的基因序列是什么。
这就是生物数据科学如此依赖领域专业知识和背景的原因。如果你不能相信产生结果的算法,那么你也不能相信你的结果。
随着每天收集的生物学数据的爆炸式增长,将领域专业知识和生物学背景与数据科学技能结合起来变得更加重要。虽然查看基因组信息的庞大数据集并进行分析很容易,但这往往会导致重大错误。
举个例子,使用一个n×m矩阵的 n 个特征和 m 个患者。如果你用这个矩阵来运行一个二维卷积神经网络,你将会暗示相邻的样本有关系。对于独立的人来说,这是不正确的。
由于背景的重要性,确保你将生物数据科学工作与健康剂量的领域专业知识相结合,以确保你以正确的方式做酷的事情。
我叫亚历山大·泰特斯,我有旅行癖。我经常陷入沉思,迷失在树林中,而且经常同时迷失在两者之中。我的人生使命是重新定义职业成功,将个人和职业追求都包括在内。你可以在LinkedInTwitter上找到我,在网上分享一些想法 。**
生物与非生物/人工智能
Photo by richard thomposn on Unsplash
什么是智能?
伟大的哲学家苏格拉底(公元前 470/469-公元前 399)说过“我知道我很聪明,因为我知道我什么都不知道”。阿尔伯特·爱因斯坦(1879-1955)认为想象力是智慧而非知识的真正标志。而斯蒂芬·霍金(1942–2018)将智力视为适应变化的能力。人们创造了许多理论来解释生物智能,如查尔斯·斯皮尔曼(1863-1945)的理论,霍华德·厄尔·加德纳(1943-)的多元智能理论和罗伯特·斯腾伯格(1949-)的三元智能理论。根据斯皮尔曼的理论,智力被定义为以富有成效的方式获取和使用知识的能力。多元智能理论将智能定义为创造一种有效的产品或提供一种在文化中受到重视的服务的能力,以及使一个人有可能解决生活中的问题的一套技能,以及找到或创造问题解决方案的潜力,这涉及到收集新知识。智力三元论可能是最全面的理论。它将人类智力定义为一种精神活动,旨在有目的地适应、选择和塑造与个人生活相关的现实世界环境。这意味着智力是一个人在一生中理性处理环境变化的能力。这个定义与斯蒂芬·霍金的定义不谋而合,因为两人都认为适应能力是智力的主要标志。适应意味着处理复杂情况的能力,以及对新情况迅速而成功地做出反应的能力。智慧的其他标志包括但不限于,从模糊或矛盾的信息中理解意义,使用理性解决问题,以普通、理性的方式理解和推断,应用知识操纵环境,认识到不同因素在一种情况下的相对重要性,以及从经验中学习或理解。这就产生了区分生物智能和非生物或人工智能的需要。
人类对抗机器
如果你看了德国的“心算器”,鲁迪格·甘姆和他惊人的快速心算大型算术表达式的能力,以及记忆 81^100 等大幂幂的能力,你很可能会称他为天才。
然而,由一台机器执行复杂的算术和逻辑运算并不意味着这台机器就是一台智能机器。另一方面,人类过去常常低估智力的真正标志。我们过去常常认为认知过程是理所当然的,例如辨别面孔、识别物体、识别语言声音、解决问题和从经验中学习。具有这些认知能力的机器绝对是真正的智能机器。下表提供了人机之间的快速比较。
人工智能
人工智能旨在模仿/逆向工程和增强生物智能,以建立能够在结构化/非结构化、静态/动态和完全/部分可观察的环境中自主运行和交互的智能系统/过程。这通常包括借鉴人类智能的特征,如情境意识、决策、解决问题、从环境中学习并适应环境的变化。机器可以增强我们的肌肉和认知能力(智力增强——IA)。
Credit: Alaa Khamis
人工智能是一种不断发展的技术,而不是一种全新的技术,因为它的种子可以追溯到古典哲学家,以及他们在将人类思维建模为符号系统方面的努力,这导致了作为思维过程的“连接主义”。人工智能包含许多子领域,如感知、知识表示、认知推理、机器学习、数据分析、问题解决、分布式人工智能和表演。机器学习(ML)是最著名的人工智能形式,因为它在汽车、精准农业/智能农业、认知医疗、金融科技和消费电子等不同领域具有强大且日益多样化的商业收入流…
Credit: Alaa Khamis
在下一篇文章中,我将谈论人工智能的三次浪潮:人工狭义智能(AI)、人工通用智能(AGI)和人工超级智能(ASI)。
生物医学图像分割:注意力 U 网
通过在标准 U-Net 上附加注意门来提高模型的灵敏度和准确性
医学图像分割已经被积极地研究以自动化临床分析。深度学习模型一般需要大量的数据,但获取医学图像既繁琐又容易出错。
Attention U-Net 旨在自动学习关注不同形状和大小的目标结构;因此,Oktay 等人的论文名称“学习在哪里寻找胰腺”
关注优图网之前的相关作品
优信网
U-Nets 通常用于图像分割任务,因为它的性能和对 GPU 内存的有效使用。它的目标是以较少的训练样本实现可靠的临床使用的高精度,因为获取带注释的医学图像可能是资源密集型的。阅读更多关于优信网的信息。
尽管 U-Net 具有出色的表示能力,但它依赖于多级级联卷积神经网络来工作。这些级联框架提取感兴趣的区域并进行密集预测。这种方法导致计算资源的过度和冗余使用,因为它重复提取低级特征。
注意模块
【需要注意】Jetley 等推出端到端可训练的注意模块。注意门常用于自然图像分析和自然语言处理。
注意力被用于执行特定于类的池化,这导致更准确和鲁棒的图像分类性能。这些注意力地图可以放大相关区域,从而显示出优于几个基准数据集的泛化能力。
软硬注意
注意力函数的工作原理是通过迭代区域提议和裁剪来使用图像区域。但这通常是不可微的,并且依赖于强化学习(一种基于采样的技术,称为强化)进行参数更新,这导致优化这些模型更加困难。
另一方面,软注意是概率性的,并且利用标准的反向传播,而不需要蒙特卡罗采样。Seo 等人的的软注意方法通过实现非均匀、非刚性的注意图来展示改进,该注意图更适合于在真实图像中看到的自然物体形状。
关注优信网有什么新内容?
注意门
为了提高分割性能, Khened 等人和 Roth 等人依靠附加的在先对象定位模型来分离定位和后续分割步骤。这可以通过在 U-Net 架构之上集成注意力门来实现,而无需训练额外的模型。
因此,结合到 U-Net 中的注意门可以提高模型对前景像素的灵敏度和准确性,而不需要显著的计算开销。注意门可以逐渐抑制不相关背景区域的特征反应。
注意门在连接操作之前实现,以仅合并相关的激活。源自背景区域的梯度在反向传递期间被向下加权。这允许基于与给定任务相关的空间区域来更新先前层中的模型参数。
基于网格的门控
为了进一步改善注意机制, Oktay 等人提出了网格注意机制。通过实现基于网格的选通,选通信号不是用于所有图像像素的单个全局向量,而是适应于图像空间信息的网格信号。每个跳过连接的选通信号聚集来自多个成像尺度的图像特征。
通过使用基于网格的选通,这允许关注系数更具体地针对局部区域,因为它增加了查询信号的网格分辨率。与基于全局特征向量的门控相比,这实现了更好的性能。
软注意技术
附加软注意用于句子到句子的翻译( Bahdanau 等人和 Shen 等人)和图像分类( Jetley 等人和 Wang 等人)。虽然这在计算上更昂贵,Luong 等人已经表明,软注意可以比乘法注意实现更高的准确性。
体系结构
下面是注意力 U-Net 的图解。
我的注意力 U-Net 实验
我将使用 Drishti-GS 数据集,其中包含 101 幅视网膜图像,以及光盘和光学杯的注释掩膜。50 幅图像用于训练,51 幅用于验证。
实验设置和使用的指标将与 U-Net 相同。
模型在 13 分钟内完成训练;每个时期大约需要 15 秒。
用于比较的几个 U-Net 模型之间的度量,如下所示。
测试从模型处理一些看不见的样本开始,以预测光盘(红色)和光学杯(黄色)。下面是对注意力优网、 UNet++ 和优网的测试结果,以供对比。
结论
Attention U-Net 旨在通过在标准 U-Net 上附加注意门来进一步提高分割精度,并使用更少的训练样本。
Attention U-Net 消除了某些分割架构所需的外部对象定位模型的必要性,从而提高了模型对前景像素的灵敏度和准确性,而没有显著的计算开销。
Attention U-Net 还结合了基于网格的门控,这使得注意力系数更具体地针对局部区域。
阅读其他 U-Net 架构:
使用非常少的训练图像,并产生更精确的分割。
towardsdatascience.com](/biomedical-image-segmentation-u-net-a787741837fa) [## 生物医学图像分割:UNet++
通过一系列嵌套、密集的跳过路径提高分段准确性
towardsdatascience.com](/biomedical-image-segmentation-unet-991d075a3a4b)
这里是 PyTorch 代码的注意 U-Net 架构:
40%的吸尘器,40%的看门人,20%的算命师。
towardsdatascience.com](/data-scientist-the-dirtiest-job-of-the-21st-century-7f0c8215e845)
生物医学图像分割:U-Net
使用非常少的训练图像工作,并产生更精确的分割
图象分割法
假设我们想知道一个物体在图像中的位置以及这个物体的形状。我们必须给图像中的每个像素分配一个标签,这样具有相同标签的像素就属于那个对象。与对象检测模型不同,图像分割模型可以提供图像中对象的精确轮廓。
图像分类、目标检测和图像分割的区别
图像分类帮助我们对图像中包含的内容进行分类。目标是回答“这个图像里有猫吗?”,通过预测是或否。
物体检测指定物体在图像中的位置。目标是识别“这张图片中的猫在哪里?”通过围绕感兴趣的对象绘制边界框。
图像分割为图像中的每个对象创建一个像素式遮罩。目标是通过对所需标签中的每个像素进行分类来识别图像中不同对象的位置和形状。
优信网
在本文中,我们将探索由奥拉夫·龙内伯格、菲利普·费舍尔和托马斯·布罗克斯撰写的 U-Net 。这篇论文发表在 2015 MICCAI 上,在 2019 年 11 月有超过 9000 次引用。
关于优信网
U-Net 用于生物医学图像的许多图像分割任务,尽管它也用于自然图像的分割。U-Net 已经超越了先前 Ciresan 等人的最佳方法,赢得了 ISBI 2012 EM(电子显微镜图像)分割挑战赛。
需要更少的训练样本 深度学习模型的成功训练需要数千个带注释的训练样本,但是获取带注释的医学图像是昂贵的。U-Net 可以用较少的训练样本进行端到端的训练。
精确分割 精确分割掩模在自然图像中可能并不重要,但是医学图像中的边缘分割误差导致临床设置中的结果不可靠。尽管训练样本较少,但 U-Net 可以产生更精确的分段。
U-Net 之前的相关工作
如上所述, Ciresan 等人致力于神经网络分割神经元膜,用于电子显微镜图像的分割。网络使用滑动窗口通过提供像素周围的局部区域(小块)作为输入来预测每个像素的类别标签。
相关工作的限制:
- 由于滑动窗口、扫描每个补丁以及重叠造成的大量冗余,它非常慢
- 无法确定滑动窗口的大小,这会影响定位精度和上下文使用之间的权衡
体系结构
U-Net 具有优雅的架构,扩展路径或多或少与收缩路径对称,并产生 u 形架构。
收缩路径(下采样) 看起来像一个典型的 CNN 架构,通过连续堆叠两个 3×3 卷积(蓝色箭头),然后是一个 2×2 最大池(红色箭头)进行下采样。在每一个下采样步骤中,通道的数量都会翻倍。
扩展路径(上卷积) 一个用于上采样的 2x2 上卷积(绿色箭头)和两个 3x3 卷积(蓝色箭头)。在每个上采样步骤中,通道数量减半。
在每个 2×2 上卷积之后,由于每个卷积中边界像素的丢失,特征图与来自收缩路径(灰色箭头)的相应层的连接提供了从收缩路径到扩展路径的定位信息。
最终层 一个 1x1 卷积,将特征图映射到所需数量的类。
我在优信网上的实验
我将使用 Drishti-GS 数据集,这与 Ronneberger 等人在论文中使用的数据集不同。该数据集包含 101 个视网膜图像,以及光盘和视杯的注释掩模,用于检测青光眼,青光眼是世界上失明的主要原因之一。50 幅图像将用于训练,51 幅用于验证。
韵律学
我们需要一组指标来比较不同的模型,这里我们有二元交叉熵、Dice 系数和交集。
二元交叉熵 二元分类的常用度量和损失函数,用于衡量误分类的概率。
我们将使用 PyTorch 的 binary _ cross _ entropy _ with _ logits。与 Dice 系数一起用作训练模型的损失函数。
骰子系数
预测值和实际值之间重叠的常用度量标准。计算方法是 2 预测值和实际值之间的重叠面积()除以预测值和实际值组合的总面积(*)。**
该度量的范围在 0 和 1 之间,其中 1 表示完美和完全的重叠。
我将使用这个度量和二进制交叉熵作为训练模型的损失函数。
交集超过并集
一个简单(然而有效!)用于计算预测遮罩与地面真实遮罩的准确度的度量。计算预测值与实际值之间的重叠面积( )并除以并集面积( 预测值与实际值)的计算。
类似于 Dice 系数,该度量的范围从 0 到 1,其中 0 表示没有重叠,而 1 表示预测值和实际值之间完全重叠。
培训和结果
为了优化该模型以及随后的 U-Net 实现以进行比较,使用具有 1e-4 学习率的 Adam 优化器和每 10 个历元具有 0.1 衰减(伽马)的 Step LR 来训练超过 50 个历元。损失函数是二进制交叉熵和 Dice 系数的组合。
该模型在 11 分 33 秒内完成训练,每个历元用时约 14 秒。总共有 34,527,106 个可训练参数。
具有最佳性能的时段是时段# 36(50 个中的一个)。
- 二元交叉熵:0.3319
- 骰子系数:0.8367
- 并集上的交集:0.8421
用几个看不见的样本测试模型,预测光盘(红色)和光学杯(黄色)。
从这些测试样本来看,结果相当不错。我选择了第一张图片,因为它的左上角有一个有趣的边缘,这里有一个错误的分类。第二个图像有点暗,但没有问题得到部分。
结论
U-Net 架构非常适合生物医学图像分割,尽管仅使用 50 幅图像进行训练,但仍取得了非常好的性能,并且具有非常合理的训练时间。
阅读周等人关于 UNet++的文章:
通过一系列嵌套、密集的跳过路径提高分段准确性
towardsdatascience.com](/biomedical-image-segmentation-unet-991d075a3a4b)
关注 U-Net:
通过在标准 U-Net 上附加注意门来提高模型的灵敏度和准确性
towardsdatascience.com](/biomedical-image-segmentation-attention-u-net-29b6f0827405)
嗨!,我是叮当。我喜欢构建机器学习项目/产品,我在向数据科学写关于它们的文章。在媒体上关注我或者在 LinkedIn 上联系我。
以下是优信网的 PyTorch 代码:*
生物医学图像分割:UNet++
通过一系列嵌套、密集的跳过路径提高分段准确性
在这篇文章中,我们将探索由美国亚利桑那州立大学的周等人编写的用于医学图像分割的嵌套 U-Net 架构。这篇文章是 U-Net 文章的延续,我们将比较 UNet++和 Ronneberger 等人的原始 U-Net。
UNet++旨在通过在编码器和解码器之间包含密集块和卷积层来提高分割精度。
分割精度对于医学图像是至关重要的,因为边际分割误差会导致不可靠的结果;因此将被拒绝用于临床设置。
尽管数据样本较少,但为医学成像设计的算法必须实现高性能和高精度。获取这些样本图像来训练模型可能是一个消耗资源的过程,因为需要由专业人员审查的高质量的未压缩和精确注释的图像。
UNet++有什么新功能?
下面是 UNet++和 U-Net 架构的图解。
UNet++在原有的 U-Net 基础上增加了 3 项功能:
- 重新设计的跳过路径(显示为绿色)
- 密集跳跃连接(以蓝色显示)
- 深度监督(以红色显示)
重新设计的跳过路径
在 UNet++中,重新设计的跳过路径(显示为绿色)被添加进来,以弥合编码器和解码器子路径之间的语义鸿沟。
这些卷积层的目的是减少编码器和解码器子网的特征图之间的语义差距。因此,对于优化者来说,这可能是一个更直接的优化问题。
U-Net 中使用的 Skip 连接直接连接编码器和解码器之间的特征映射,从而融合语义不同的特征映射。
然而,使用 UNet++时,来自同一密集块的先前卷积层的输出与较低密集块的相应上采样输出融合。这使得编码特征的语义级别更接近在解码器中等待的特征映射的语义级别;因此,当接收到语义相似的特征图时,优化更容易。
skip 路径上的所有卷积层都使用大小为 3×3 的核。
密集跳跃连接
在 UNet++中,密集跳过连接(蓝色显示)在编码器和解码器之间实现了跳过路径。这些密集块受 DenseNet 的启发,目的是提高分割精度并改善梯度流。
密集跳跃连接确保所有先前的特征图被累积并到达当前节点,因为沿着每个跳跃路径的密集卷积块。这在多个语义级别生成全分辨率特征图。
深度监督
在 UNet++中,增加了深度监督(红色显示),这样就可以修剪模型来调整模型复杂度,平衡速度(推理时间)和性能。
对于精确模式,所有分割分支的输出被平均。
对于快速模式,从一个分割分支中选择最终分割图。
周等人进行了实验以确定具有不同修剪水平的最佳分割性能。使用的度量标准是交集/并集和推理时间。
他们对四个分割任务进行了实验:a)细胞核,b)结肠息肉,c)肝脏,以及 d)肺结节。结果如下:
与 L4 相比,L3 的推理时间平均减少了 32.2%,同时略微降低了 Union 的交集。
L1 和 L2 等更激进的修剪方法可以进一步减少推理时间,但代价是显著的分割性能。
当使用 UNet++时,我们可以调整用例的层数。
我在 UNet++上的实验
我将使用 Drishti-GS 数据集,这与 Ronneberger 等人在论文中使用的数据集不同。该数据集包含 101 个视网膜图像,以及光盘和视杯的注释掩模,用于检测青光眼,青光眼是世界上失明的主要原因之一。50 幅图像将用于训练,51 幅用于验证。
韵律学
我们需要一组指标来比较不同的模型,这里我们有二元交叉熵、Dice 系数和交集。
二元交叉熵 二元分类常用的度量和损失函数,用于度量误分类的概率。
我们将使用 PyTorch 的 binary _ cross _ entropy _ with _ logits。与 Dice 系数一起用作训练模型的损失函数。
骰子系数
预测值和实际值之间重叠的常用度量标准。计算是 2 *重叠面积(预测值和实际值之间)除以总面积(预测值和实际值的组合)。
该度量的范围在 0 和 1 之间,其中 1 表示完美和完全的重叠。
我将使用这个度量和二进制交叉熵作为训练模型的损失函数。
并集上的交集
一个简单(然而有效!)用于计算预测遮罩与地面真实遮罩的准确度的度量。计算重叠面积( 在预测值和实际值之间)并除以并集面积( 预测值和实际值)的计算。
类似于 Dice 系数,该度量的范围从 0 到 1,其中 0 表示没有重叠,而 1 表示预测值和实际值之间完全重叠。
培训和结果
为了优化该模型,训练超过 50 个时期,使用具有 1e-4 学习率的 Adam 优化器,以及每 10 个时期具有 0.1 衰减(伽马)的 Step LR 。损失函数是二进制交叉熵和 Dice 系数的组合。
模型在 27 分钟内完成了 36.6M 可训练参数的训练;每个时期大约需要 32 秒。
具有最佳性能的纪元是纪元# 45(50 个中的一个)。
- 二元交叉熵:0.2650
- 骰子系数:0.8104
- 并集上的交点:0.8580
用于比较的几个 U-Net 模型之间的度量,如下所示。
测试从模型处理一些看不见的样本开始,以预测光盘(红色)和光学杯(黄色)。以下是 UNet++和 U-Net 的测试结果,以供比较。
从指标表来看,UNet++在交集方面超过了 U-Net,但在骰子系数方面落后。从定性测试结果来看,UNet++已经成功地正确分割了第一幅图像,而 U-Net 做得并不太好。同样由于 UNet++的复杂性,训练时间是 U-Net 的两倍。人们必须根据它们的数据集来评估每种方法。
结论
UNet++旨在通过一系列嵌套的密集跳过路径来提高分割精度。
重新设计的跳过路径使得语义相似的特征图的优化更加容易。
密集跳跃连接提高了分割精度并改善了梯度流。
深度监督允许模型复杂性调整,以平衡速度和性能优化。
阅读另一个 U-Net:
使用非常少的训练图像,并产生更精确的分割。
towardsdatascience.com](/biomedical-image-segmentation-u-net-a787741837fa) [## 生物医学图像分割:注意力 U 网
通过在标准 U-Net 上附加注意门来提高模型的灵敏度和准确性
towardsdatascience.com](/biomedical-image-segmentation-attention-u-net-29b6f0827405) [## 数据科学家:21 世纪最肮脏的工作
40%的吸尘器,40%的看门人,20%的算命师。
towardsdatascience.com](/data-scientist-the-dirtiest-job-of-the-21st-century-7f0c8215e845) [## 7 个必不可少的人工智能 YouTube 频道
如何跟上最新最酷的机器学习进展
towardsdatascience.com](/7-essential-ai-youtube-channels-d545ab401c4)
以下是 UNet++架构的 PyTorch 代码:
洛杉矶的鸟类数据探索[第一部分]
这是我正在进行的一系列文章的第 1 部分,在这些文章中,我正在试验收集和建模 Bird scooter 数据。在这一部分中,我将介绍我的项目目标,并讨论一个地理区域的鸟类数据收集。请继续关注我在未来几周开发这个项目的其余部分。查看我的代码 这里 。
动机
作为洛杉矶或其周边城市的居民,很难错过微动力运动(电动滑板车、电动自行车等)的影响。)对景观的影响。作为这些产品的狂热用户,以及总体上微移动性的支持者,我长期以来一直对研究这些车辆的用户行为感兴趣。具体来说,我想研究这些车辆如何在特定地区行驶,并研究微型汽车公司如何最大限度地利用它们。
直到最近,我还以为是因为提供这些车辆的公司(伯德、莱姆、优步、Lyft 等。)没有公开的 API,我的研究将受到数据缺乏的限制。然而,我看到了 Conor McLaughlin 的一篇很棒的博客文章(链接此处),展示了如何获取和询问鸟类数据以获得洞察力。
介绍
**背景:**在我的简化模型中,公司员工(通常称为“充电者”)有两种类型的机会来干预/影响特定区域的滑板车分销。第一个是在一天开始的时候,踏板车的初始位置被设定。第二种是全天,当充电器拿起滑板车,给它们的电池充电,然后把它们放在某个地方供骑车人使用。
**目标:**我的模型将尝试回答以下问题:
- 全天最大化使用(从而最大化收入)的踏板车的最佳初始位置是什么?现实世界中看到的摆放是否可以改进?
- 我们能找到充电器重新分配踏板车位置的最佳“干预点”吗?
**注意事项:**从一开始,我的模型就有一些明显的限制:
- 我不了解伯德的成本模型,即派出充电器进行干预的成本。因此,我将不得不做出一些假设,以确定干预成为更有利可图的选择的“断点”。
- 我将离散时间和空间,并可能粗粒度收集数据,以产生可靠的结果。因此,我将无法准确地获取收入(波导的收入模式涉及骑行启动费,随后是每分钟使用费)。这意味着我将使用试探法和平均值来量化收入,这可能不会与每次乘坐获得的收入完全一致。
- 正如将在收集 Bird 数据一节中讨论的那样,我实际上可以获得的数据受到 Bird API 的限制。由于公司 Bird 可以访问更多自己的数据,我的模型不太可能超过 Bird 数据科学团队使用的优化器。也就是说,我对这个项目的个人目标是获得关于这个数据集的实践经验,并希望实现体面的模型性能。
程序概述
我的方法如下:
- 收集地理数据以建立一个感兴趣的区域:这将是我随着时间的推移监测鸟类分布的区域。
- 收集搜索区域的鸟类数据:我计划在足够长的时间内频繁地收集给定区域内所有鸟类的数据,以捕捉重要的趋势。
- 执行探索性数据分析并开始模型开发。
- 使用收集的数据来预测未来鸟类的位置。
- 通过使用未来鸟的位置预测来预测一天中鸟群的效用。
- 确定最佳干预点/初始位置,以最大化车队效用。
收集数据
尽管我希望我的模型适用于任意的地理区域,但我希望使用洛杉矶的社区进行初始测试和概念验证开发。因此,我需要做以下事情:
- 收集感兴趣区域的地理数据
- 收集这些地区的鸟类踏板车数据
收集地理数据
为了搜索一组邻域,我需要 GIS 数据来提供这些邻域边界的坐标。令人欣慰的是,洛杉矶县维护着一个强大的 GIS 数据门户(链接此处),它提供了一个包含洛杉矶县所有社区的 shapefile。
首先,我使用geopandas
将洛杉矶县邻域 shapefile 的内容读入一个数据帧。
L.A. County GIS dataset type: <class 'geopandas.geodataframe.GeoDataFrame'>
正如我们所见,洛杉矶县 shapefile 以shapely
多边形的形式提供了边界坐标,这对于可视化和地理空间分析非常理想。具体来说,我想关注洛杉矶西边的社区(靠近我的公寓),所以我过滤了数据帧,并将所有需要的社区合并到一个多边形列表中。
注意,对于每个邻域,在将它们展平到最终的search_areas
列表之前,我强制所有的多边形变成多重多边形。这是为了解释一些实际上是多多边形的邻域几何图形。例如,观察威尼斯的几何图形:
Venice geography type: <class 'shapely.geometry.multipolygon.MultiPolygon'>
Number of Venice constituent Polygons: 2
对于由不连续部分组成的邻域,洛杉矶县数据集将几何表示为多多边形。我的方法将这些邻域展平,并将所有的邻域/子区域存储在一个列表中。然后,我将邻近区域可视化,以确认 GIS 数据代表了我预期的区域。
gmaps visualization of L.A. neighborhoods.
在尝试了一系列不同的绘图/可视化方法后,我选定了gmaps
,这是一个在 Jupyter 笔记本上显示谷歌地图的插件(https://github.com/pbugnion/gmaps))。它为添加图层/数据点提供了直观的命令,并为额外的上下文提供了开箱即用的关键地图细节显示。唯一的缺点是该工具不是免费的,需要一个谷歌云平台账号。提供的令牌是我自己的,所以请谨慎刷新请求。
收集河边数据
另外,我还想探索为我的家乡加州河滨收集地理数据。我从下载河岸城市限制数据开始(链接此处)。
正如我们所见,Riverside 提供的边界数据与洛杉矶不同:作为路径列表(shapely
linestring)。因此,我使用了polygonize_full
的方法将片段合并在一起。
gmaps visualization of Riverside.
收集鸟类数据
接下来,我需要为我的搜索区域捕获鸟的位置数据。
据我所知,Bird 没有公开可用的 API。幸运的是,WoBike 资源库的贡献者(https://github.com/ubahnverleih/WoBike/blob/master/Bird.md)似乎已经找到了一种为 Bird 逆向开发 RESTful API 的方法。他们的方法似乎是通过假冒 iOS 用户登录来获得 API 认证。
使用这个 API,我做了以下工作:
- 通过模拟 iOS 登录获取身份验证令牌。
- 搜索特定区域(表示为坐标多边形)并获取单个踏板车的数据
'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJBVVRIIiwidXNlcl9pZCI6IjlhNjVkYjRjLTU4NDctNDU1OS05ZDFmLWIzY2Y5Mjg1ODhmNyIsImRldmljZV9pZCI6IjI5M2ZlYzhjLWE2YTUtMTFlOS05YjViLWEwOTk5YjEwNTM1NSIsImV4cCI6MTU5NDY5MjM4OH0.09T6VCGDt-mWz6oYiGawzl0gJa-a4Fq2Y3qaOqVE8nA'
我们已经成功获得了一个 API 令牌!现在,我将测试查找附近鸟类的功能。
成功!我们有附近鸟类的名单。我们将使用gmaps
做一些快速可视化,以确认鸟的位置是合理的。但是首先,我们将定义几个辅助函数。
gmaps visualization of Birds around test location.
看起来这些鸟大概在圣莫尼卡市中心,这一点得到了证实。正如将在下一节中讨论的,很难确定鸟儿离搜索点的确切距离(即搜索半径)。
收集地理区域的鸟类数据
我们现在将把它们放在一起。我们已经确定我们可以捕捉和可视化地理区域和鸟类的位置。使用这两个函数,我们将尝试为一个地理区域(而不是一个搜索点周围)收集鸟类的位置。
虽然 Bird 的“附近的踏板车”API 端点有一个半径参数,但我无法成功使用它。经过一些实验,我观察到无论我为搜索半径指定什么值,都会返回相同的一组鸟(除了将半径设置为 0,这会产生一个空结果)。也许其他用户发现指定半径更成功;然而,鉴于我无法使用它,我构建了一个方法来收集我感兴趣区域内所有鸟类的位置。
下面更详细地提到了所有的底层功能;然而,我将在这里给出我的方法的概述。给定一个搜索区域,我定位包含的鸟类如下:
- 我在搜索区域内的一个有代表性的点放下一个搜索点,收集附近的鸟。
- 收集的鸟被添加到集合中。由于每只鸟都有一个(纬度,经度)位置,所以可以在收集到的鸟周围形成一个凸多边形。我们称之为覆盖区域。
- 当被覆盖的区域仍然包含在搜索区域内时,我们继续搜索。
- 覆盖区域的顶点现在成为收集附近鸟类的一组新的搜索点。
- 如果在特定搜索点周围没有发现鸟在搜索区域内,该搜索点被认为是“死胡同”,其搜索结果被丢弃。
- 然而,如果在特定搜索点周围找到的任何鸟在搜索区域内,其搜索的全部结果都被添加到找到的鸟的集合中,新的覆盖区域(凸多边形)被计算,并且新的搜索点集合由新的覆盖区域的顶点形成。
- 为了确保多个搜索点不会恢复相同的鸟类,使用重复数据删除将搜索结果限制为仅新的(即,先前未发现的)鸟类。
- 这个过程一直持续到覆盖区域包含搜索区域或者所有搜索点都成为死胡同。
如果一个搜索区域由多个搜索区域组成(洛杉矶的例子就是这种情况),那么这个过程在每个组成搜索区域上独立运行,并且对结果进行重复数据删除以保持唯一性。
我们现在将把我们的搜索算法应用到之前可视化的洛杉矶社区。此算法肯定不是获取此数据的最有效方式,因此请等待几分钟来完成此搜索。
现在我们已经验证了我们的搜索算法定位了一些鸟,我们将可视化位置以及搜索区域(显示为红色)和我们的搜索算法覆盖的区域(蓝色)。请注意该算法如何从搜索区域的中间开始搜索,并在每次迭代中逐渐向外扩展。由于发现的鸟的数量,这种可视化可能需要一两分钟。
gmaps visualization of the Bird search algorithm searching over several L.A. neighborhoods. Note how the covered areas (blue) start from the center of the search areas (red) and expand outward with each iteration until all Birds (pink) are found.
这种可视化势不可挡,运行缓慢。我们可以使用gmaps
热图功能提高可视化速度并简化视图。
好多了!正如我们所观察到的,鸟类的分布因我们观察的区域而异。这些观察还取决于一天中的时间(在本例中,是太平洋标准时间下午 6 点)和一周中的日期(星期日)。大体上,我们看到鸟类最密集地集中在威尼斯和圣莫尼卡的市中心/沿海地区,越是内陆的郊区人口越稀少。
我们现在将收集的数据保存到一个 csv 文件中,这样我们就可以在以后引用它,而不必运行我们的搜索算法。然而,首先我们将附加数据收集时间的时间戳,因为这对于评估我们搜索区域内的鸟类分布如何随时间变化至关重要。
后续步骤
在这一部分中,我能够构建一个可靠的算法来捕获数据(位置、电池电量等。)用于特定地理区域内的鸟类滑行车。从这里开始,我的下一步将是设置一个 cron 作业,在一天中以一定的间隔重复执行这个任务。一天的鸟类数据足以进行一些早期探索性数据分析和假设生成。从那里,这个过程将被扩展到捕获几周的数据,从而创建用于全面分析的数据集。
Bist-Parser:依赖解析器的端到端实现
本文是关于依赖解析的第二篇也是最后一篇文章。我们将为您提供一些简单的实施指南和工具来帮助您改进它。
词汇
- 树库是注释句法或语义句子结构的解析文本语料库。依赖树库是使用不同的方法创建的:要么直接感谢人工注释器,要么使用自动解析器提供第一次解析,然后由注释器检查。一种常见的方法是使用确定性过程,通过 head 规则将现有的树库翻译成新的语言。建立一个高质量的树木库既费时又费钱。
- CoNLL-U —计算自然语言学习-Universal 是 CoNLL-X 格式的修订版。来自树库的句子被分开,每个单词或标点符号被放置在不同的行上。以下每一项都跟在单词后面,用表格隔开:
–ID:句子中的单词索引,从 1 开始
–FORM:词形或标点符号
–LEMMA:词形的引理或词干
–UPOS:通用词性标签
–XPOS:语言特定词性标签;将不会在我们的模型
中使用–专长:形态特征的无序列表,由通用依赖关系定义;表示名词的性别和数量,动词的时态等。
–HEAD:单词的头,表示与当前单词相关的单词的索引
–DEPREL:通用依赖关系;表示两个词(动词的主语或宾语,名词的限定词等)之间的关系。)
–DEPS:特定于语言的部分依赖;不会在我们的型号
中使用–杂项:评论或其他注释
An example of CoNLL-U format
- 条目是一个单词,或者一个句子中的标点符号。它有多个属性,如上所述。一个句子通常是条目的串联(单词本身是条目的属性:它的形式),由空格分隔。
实施
Bist 解析器的实现来自其论文的作者。Xiezhq Hermann 在 GitHub 上发布了一个更新。你可以在这里找到。它在 Python 3.x 上工作,使用 torch 0.3.1(有或没有 Cuda)。它非常完整,可以按原样使用。然而,为了使代码适应您的数据或对其进行升级,您必须完成每个模块,这可能是一项艰巨的任务。文章的这一部分将引导您浏览所有文件和流程。
通用依赖(UD)是一个语法注释的开放社区框架。它提供了极大地帮助开发依赖解析器的语料库和工具。
从 UD,你可以下载你选择的句子语料库(任何可用的语言,甚至古法语!),按原样使用它们,并开始用这种类型的命令训练您的 Bist 解析器:
python src/parser.py --outdir [results directory] --train training.conll --dev development.conll --epochs 30 --lstmdims 125 --lstmlayers 2 [--extrn extrn.vectors]
您可以在这里详细描述超参数,通过文件 parser.py 被模型捕获
你可能知道,当你在一个语料库上训练一个模型的时候,模型是偏向这个语料库的。你可以在多个语料库上训练你的模型,以使它更加一般化。有几种技术可以让你增加分数,以 TreeBank 嵌入为例。这里,我们只是连接了一些树库,没有任何进一步的处理。
实用工具
- 创建一个 ConllEntry 类:每个条目都有众所周知的属性:id,form,lemma,Universal PoS tag,language Specific PoS tag,morphological features,head of current word,dependency relation,enhanced dependency relation,commentation。这些属性是根据通用依赖项 CoNLL-U 格式定义的。这种格式有助于模型理解它的输入是什么,以及它应该预测什么。
- 读取一个 CoNLL-U 文件,并将每个句子转换成一个 ConllEntry。
- Count 词汇表:这个函数创建一个 ConllEntry 属性的计数器,并允许您了解这些属性是如何分布在您的数据集中的。如果您想要确定数据集中最常用的单词或关系,此函数会很有用。
mstlstm
该文件包含您的模型。你所有的超参数和大部分的监测工作都发生在这个文件中。
方法 forward 遍历句子中的每个条目。它首先计算每个条目属性的向量。通过我们的模型,我们得到了描述单词、词性标签和专长的多个向量。然后将这些向量连接起来,为每个条目形成一个维度更大的向量。这些条目然后连接在一起形成句子向量。
首先,它将条目转换成向量。这里,主要属性是单词、词条(onto)和词性标签(PoS)的嵌入。但是,我们建议您尽可能添加更多功能。例如,你可以使用单词的特征来表示名词是单数还是复数,它的性别或者时态…嵌入这些特征可以让你的 BiLSTM 找到更多的模式。
Evolution of PoS embedding on two dimensions
然后,它向 BiLSTM 提供这些向量(for = forward,back = backward)。第 52 行评估句子的得分。这是创建完全加权有向图的部分。在第 57 行,它计算关系分数。这是这个模型中一个有趣的技巧:不是同时评估所有的可能性(|可能性| = |弧|。|labels|,这太高了),它首先预测依赖项,然后预测关系。关于错误、错误和错误我们以后再看。
在下面的插图中,您可以看到依赖项评估在批处理中的演变。深蓝色单元格对应一个加重的弧线。这个例子来自一个典型的法语句子,“在被认为是例行公事的情况下,体育运动中发生的骚乱。“你可以发现句子中的拼写错误;这在树银行中并不罕见。
Les commotions cérébrales sont devenu si courantes dans ce sport qu’on les considére presque comme la routine.
这句话中值得注意的是单词“ devenu ”,它是词根(即主词),以及由单词“ que ”和“ comme”分隔的三个明确定义的命题。
Evolution of dependency scores for the sentence “root Les commotions cerebrales sont devenu si courantes dans ce sport que on les considere presque comme la routine.”
Scores figure (1) — Initialization
Scores figure (2) — 200 sentences
Scores figure (3) — 1000 sentences
Score figure (4) — 6000 sentences
在上面的插图中,我们可以更好地理解我们的神经网络的进化。每一列对应一个作为头的标记,每一行对应一个作为从属的标记,每一个单元格对应从头到从属的弧的分数(或权重)。
我们认识到图 1 中的随机初始化,其中所有的分数都在零附近,我们在矩阵中看不到任何形状。
在第二张图中,我们可以看到 PoS =行列式向量已被考虑在内,我们可以在它们的列中辨认出一个形状。目前,行的变化不太明显。
在第三幅插图中,我们可以清楚地看到由“阙”分隔的两个命题弧线在它之前定义得很好,在它之后就不那么清晰了。标点符号与词根“ devenu 联系得很好。
在最后一个中,我们对主弧线有了清晰的认识,模型获得了信心,分数值也更加扩张。
一旦预测了依赖关系,模型就预测了关系类型。这里是每个关系的得分图,考虑到预测的关系,给出正确的依赖关系。
Relation score figure (1)— Initialization
Relation score figure (2) — 200 sentences
Relation score figure (3) — 1000 sentences
Relation score figure (4) — 6000 sentences
预测的关系类型用黄色表示,最不正确的关系类型用红色表示。
经过一些训练,预测变得越来越有信心。
该模式的主要方法是*。它首先打乱句子,每次以不同的顺序训练模型。然后,为每个句子调用方法 前进 。这将更新权重,返回一个分数 e 并更新两个列表 errs 和 lerrs 。回想一下, e 对预测人头不同于金人头(参考,来自语料库)的每个令牌进行计数。 errs 计算弧预测损失,而 lerrs 计算标签预测损失。然后将误差和 lerrs 相加,产生反向传播损失:eers。*
改进和一些结果
为了建立一个更有效的模型,您应该尝试添加单词 dropout 作为一个超参数,并尝试不同的值。我们试过 0,0.25,0.33,0.5,0.9,1 作为数值,最好的是 0.9。它强制模型从其他向量中学习比单词更多的东西。
您还可以分离每个特征,并在每个特征上使用嵌入。这将允许更大的灵活性和对模型更多的理解。
通过这样的实现,我们在法国红杉(通用依存)树库上获得了 87.70 的 LAS 分数,而没有在这个语料库上进行专门的训练(在标签翻译之后,因为我们的标签与 UD 的不同)。
一旦训练好了依赖关系解析器,就可以使用依赖关系和关系来更好地理解数据。即使 NLU 模型能够找到许多模式,添加这些输入也是你应该考虑的真正收益。
本文的来源如下:
- Bist-Parser 的论文:Kiperwasser,e .,& Goldberg,Y. (2016)。使用双向 LSTM 特征表示的简单而精确的依存句法分析。《计算语言学协会会刊》,第 4 卷,第 313-327 页。检索自https://www . trans ACL . org/ojs/index . PHP/tacl/article/view/885/198
- 本文的一个基线实现:https://github.com/xiezhq-hermann/bist-parser
脸书先知的比特币预测价格建模
Two Bitcoin price predictions (blue and red lines) generated using Facebook’s Prophet package. The actual price data is in green, while the shaded areas denote the respective uncertainty in the estimate. As you can the uncertainty increases into the future. This is particularly the case with the tighter fitting price model (red).
这是一个快速浏览脸书的先知机器学习包使用比特币的例子。它假设您具备基本的 Python 知识,并对熊猫有所了解。
Prophet 是脸书为时间序列数据开发的开源预测程序。这个想法是,它应该使完全自动的预测变得容易,即使有杂乱的数据,而且它目前在 R 和 Python 中可用。在这篇文章中,我将使用 Python 2.7。
注来自《走向数据科学》的编辑: 虽然我们允许独立作者根据我们的 规则和指导方针 发表文章,但我们不认可每个作者的贡献。你不应该在没有寻求专业建议的情况下依赖一个作者的作品。详见我们的 读者术语 。
安装 Prophet
如果你已经安装了 pip,你需要做的就是打开一个控制台,输入
pip install fbprophet
在 Windows 上,我遇到了一些与 Anaconda 相关的依赖问题,不得不首先运行
conda clean --all
conda update pystan
下载和准备数据
一旦你安装好了,我们将导入我们需要的模块
import quandl as qd
import pandas as pd
import numpy as npimport fbprophet
import matplotlib.pyplot as plt
Quandl 是一个惊人的存储库,拥有大量极其不同的数据集。你需要注册一个免费账户并生成一个 API。那我们准备开始了。首先从 Quandl 下载比特币市场数据,去掉零值(由于某种原因,价格数据在初始行中包含零)
KEY = "YOUR_API_KEY"
qd.ApiConfig.api_key = KEYbitcoin = qd.get("BCHAIN/MKPRU")
bitcoin = bitcoin.loc[(bitcoin !=0).any(1)]
这将价格数据放入一个名为比特币的熊猫数据框架中。要检查前几行,请键入
bitcoin.head()
您应该得到一个名为“值”的列和一个日期时间索引
Value
Date
2010–08–17 0.076900
2010–08–18 0.074000
2010–08–19 0.068800
2010–08–20 0.066700
2010–08–21 0.066899
现在,我们想将记录的数据绘制在线性图表上(原因将在后面变得明显),所以用比特币为记录的价格值创建一个新列
bitcoin["log_y"] = np.log(bitcoin["Value"])
与先知一起工作
我们需要做的第一件事是做点家务。Prophet 要求我们将“日期”列的名称改为“ds ”,而我们的 y 列应该简单地称为“y”
bitcoin = bitcoin.rename(columns={"Date": "ds", "log_y" : "y"})
Prophet 所做的大部分工作都发生在幕后,但是有一些超参数允许我们轻松地微调我们的模型。我们将只调整change point _ prior _ scale参数。
转折点基本上是趋势突然改变方向的点,例如反转。Prophet 可以自动为您找到这些点,尽管您可以自己定义它们,尽管对于大型和特殊的数据集来说这是不切实际的。嘿,这就是机器的作用,对吧?change point _ prior _ scale参数基本上允许您选择您希望变点与数据匹配的紧密程度。
构建任何模型的目标是能够将其推广到其他数据/场景,并获得类似的结果。所以我们不希望我们的模型过多地跟随训练数据,这叫做过拟合。同样,拟合不足也会产生泛化能力差的模型。
因此,我们将使用这个超参数进行所有微调。该值越高,拟合越紧密,同样,该值越低,拟合越松散。在这个演示中,我选择的值(0.0015 和 0.015)之间相隔一个数量级。
priors = [0.0015, 0.015]
prophets, labels = [], []
for prior in priors:
prophet = fbprophet.Prophet(changepoint_prior_scale=prior)
prophet.fit(bitcoin)
prophets.append(prophet)
labels.append(r"CP Prior = " + str(prior))
此时,您应该有一个包含两个 prophet 对象的列表和一个用于绘制的标签列表。下一步是使用这些 prophet 对象来生成预测对象。(我们还将使用 changepoint_prior_scale 值前缀重命名“ds”列,以便跟踪数据。)
forecasts = []
for prophet in prophets:
forecast = prophet.make_future_dataframe(periods=365 * 2, freq="D")
forecast = prophet.predict(forecast)
forecast = forecast.rename(columns={"ds" : str(priors[prophets.index(prophet)]) + "_ds"})
forecasts.append(forecast)
差不多就是这样。如果一切顺利,您应该已经生成了两个预测对象的列表(实际上只是熊猫数据帧)。让我们将它们合并成一个数据帧,并将其中一个’ _ds ‘列重命名为’ Date ',删除另一个。
output = pd.merge(forecasts[0], forecasts[1], how = "inner", left_on = "0.0015_ds", right_on = "0.015_ds")
output = output.rename(columns={"0.0015_ds": "Date"}).drop("0.015_ds", axis=1)
最后,我们将索引设置为“日期”列
output = output.set_index(‘Date’)
我们现在将在一张图表上绘制所有内容
fig, ax = plt.subplots(1, 1, figsize=(10, 8))
ax.plot(output.index, output["yhat_x"], label=labels[0])
ax.fill_between(output.index, output["yhat_upper_x"], output["yhat_lower_x"], alpha=0.6, edgecolor = "k")ax.plot(output.index, output["yhat_y"], "r", label=labels[1]);
ax.fill_between(output.index, output["yhat_upper_y"], output["yhat_lower_y"], alpha=0.6, edgecolor = "k")
ax.plot(bitcoin.ds, bitcoin.y, color="green", linewidth=3, label=r"Bitcoin price (logged)")
a=ax.get_yticks().tolist()
ax.set_yticklabels(np.round(np.exp(a), 1))
plt.legend(loc="upper left")
plt.ylabel(r"Price /$")
plt.show()
“y_hat”标签是 Prophet 提供的估计值,x 或 y 后缀表示数据最初来自哪个数据框架(这里 x 是 0.0015 参数值,y 是 0.015)。
结论
这里的目标只是简单地摆弄一下 Prophet 包,我认为比特币会成为一个有趣的数据集。
仅仅通过目测,我认为从比特币最大化的角度来看,这两个预测都非常合理,我的意思是它们符合许多看涨投资者认为价格会发生的情况。
蓝色拟合可能有点不合适,但你可以看到,随着时间的推移,估计的不确定性保持相当稳定,甚至到未来。一般来说,它提供了更多的概括空间,但对趋势变化的反应可能太慢。因此我认为这是不可靠的。我不认为价格会长期保持这种趋势。
另一方面,红色拟合似乎过度拟合了数据。它紧紧跟随历史价格波动,并对趋势变化做出快速反应,然而不确定性真的会在未来爆发,使得这个预测也不可靠,尽管红色趋势更可信一些。
就我个人而言,我并不觉得这种价格预测特别有用,但关注这一点并观察脸书算法的结果会很有趣。
一口大小的蟒蛇食谱
Python 中有用的小函数的集合
Photo by Jordane Mathieu on Unsplash
*免责声明:*这是我在网上找到的一些有用的小函数的集合,主要在 Stack Overflow 或者 Python 的文档页面上。有些人可能会看,但无论如何,我都在我的项目中使用过它们,我认为它们值得分享。你可以在我试图保持更新的笔记本中找到所有这些,以及一些额外的评论。
除非必要,我不打算过度解释这些功能。那么,让我们开始吧!
从两个列表中创建字典:
>>> prod_id = [1, 2, 3]
>>> prod_name = ['foo', 'bar', 'baz']
>>> prod_dict = dict(zip(prod_id, prod_name))>>> prod_dict
{1: 'foo', 2: 'bar', 3: 'baz'}
从列表中删除重复项并保留顺序:
>>> from collections import OrderedDict>>> nums = [1, 2, 4, 3, 0, 4, 1, 2, 5]
>>> list(OrderedDict.fromkeys(nums))
[1, 2, 4, 3, 0, 5]*# As of Python 3.6 (for the CPython implementation) and
# as of 3.7 (across all implementations) dictionaries remember
# the order of items inserted. So, a better one is:*
>>> list(dict.fromkeys(nums))
[1, 2, 4, 3, 0, 5]
创建多级嵌套字典:
创建一个字典作为字典中的值。本质上,它是一本多层次的字典。
from collections import defaultdict**def** multi_level_dict():
*""" Constructor for creating multi-level nested dictionary. """* **return** defaultdict(multi_level_dict)
例 1:
>>> d = multi_level_dict()
>>> d['a']['a']['y'] = 2
>>> d['b']['c']['a'] = 5
>>> d['x']['a'] = 6>>> d
{**'a'**: {**'a'**: {**'y'**: 2}}, **'b'**: {**'c'**: {**'a'**: 5}}, **'x'**: {**'a'**: 6}}
例二:
给出了一个产品列表,其中每个产品需要从其原产地运送到其配送中心(DC),然后到达其目的地。给定这个列表,为通过每个 DC 装运的产品列表创建一个字典,这些产品来自每个始发地,去往每个目的地。
**import** random
random.seed(20)# Just creating arbitrary attributes for each Product instance
**class** Product:
**def** __init__(self, id):
self.id = id
self.materials = random.sample(**'ABCD'**, 3)self.origin = random.choice((**'o1'**, **'o2'**))
self.destination = random.choice((**'d1'**, **'d2'**, **'d3'**))
self.dc = random.choice((**'dc1'**, **'dc2'**))
**def** __repr__(self):
**return f'P{**str(self.id)**}'**products = [Product(i) **for** i **in** range(20)]# create the multi-level dictionary
**def** get_dc_origin_destination_products_dict(products):
dc_od_products_dict = multi_level_dict()
**for** p **in** products:
dc_od_products_dict[p.dc][p.origin].setdefault(p.destination, []).append(p)
**return** dc_od_products_dictdc_od_orders_dict = get_dc_origin_destination_products_dict(products)
>>> dc_od_orders_dict
{**'dc1'**: {**'o2'**: {**'d3'**: [P0, P15],
**'d1'**: [P2, P9, P14, P18],
**'d2'**: [P3, P13]},
**'o1'**: {**'d1'**: [P1, P16],
**'d3'**: [P4, P6, P7, P11],
**'d2'**: [P17, P19]}},
**'dc2'**: {**'o1'**: {**'d1'**: [P5, P12],
**'d3'**: [P10]},
**'o2'**: {**'d1'**: [P8]}}}
请注意,当您运行以上两个示例时,您应该在输出中看到defaultdict(<function __main__.multi_level_dict()>...)
。但是为了结果的易读性,这里删除了它们。
返回嵌套字典最内层的键和值:
**from** collections **import** abc
**def** nested_dict_iter(nested):
*""" Return a generator of the keys and values from the innermost layer of a nested dict. """* **for** key, value **in** nested.items():
*# Check if value is a dictionary* **if** isinstance(value, abc.Mapping):
**yield from** nested_dict_iter(value)
**else**:
**yield** key, value
关于此功能,有几点需要说明:
nested_dict_iter
函数返回一个生成器。- 在每个循环中,字典值被递归地检查,直到到达最后一层。
- 在条件检查中,为了通用性,使用了
[collections.abc.Mapping](https://docs.python.org/3/library/collections.abc.html#collections.abc.Mapping)
而不是dict
。这样就可以检查容器对象,比如dict
、collections.defaultdict
、collections.OrderedDict
和collections.Counter
。 - 为什么是
yield from
?简短而不完整的回答:它是为需要从生成器内部调用生成器的情况而设计的。我知道一个简短的解释不能做到任何公正,所以检查这个 so 线程以了解更多信息。
例 1:
>>> d = {'a':{'a':{'y':2}},'b':{'c':{'a':5}},'x':{'a':6}}
>>> list(nested_dict_iter(d))
[('y', 2), ('a', 5), ('a', 6)]
示例 2: 让我们从上面的dc_od_orders_dict
中检索键和值。
>>> list(nested_dict_iter(dc_od_orders_dict))
[('d3', [P0, P15]),
('d1', [P2, P9, P14, P18]),
('d2', [P3, P13]),
('d1', [P1, P16]),
('d3', [P4, P6, P7, P11]),
('d2', [P17, P19]),
('d1', [P5, P12]),
('d3', [P10]),
('d1', [P8])]
多个集合的交集:
def get_common_attr(attr, *args):
""" intersection requires 'set' objects """ return set.intersection(*[set(getattr(a, attr)) for a in args])
*例:*找出前 5 个products
中的共同组成材料(如果有的话)。
>>> get_common_attr(**'materials'**, *products[:5])
{'B'}
第一场比赛:
从符合条件的 iterable 中查找第一个元素(如果有的话)。
first_match = next(i **for** i **in** iterable **if** check_condition(i))# Example:
>>> nums = [1, 2, 4, 0, 5]
>>> next(i for i in nums if i > 3)
4
如果没有找到匹配,上面的实现抛出一个StopIteration
异常。我们可以通过返回一个默认值来解决这个问题。既然来了,就让它成为一个函数吧:
**def** first_match(iterable, check_condition, default_value=**None**):
**return** next((i **for** i **in** iterable **if** check_condition(i)), default_value)
例如:
>>> nums = [1, 2, 4, 0, 5]
>>> first_match(nums, lambda x: x > 3)
4
>>> first_match(nums, lambda x: x > 9) # returns nothing
>>> first_match(nums, lambda x: x > 9, 'no_match')
'no_match'
动力组:
集合 S 的幂集是 S 的所有子集的集合。
import itertools as it**def** powerset(iterable):s = list(iterable)
**return** it.chain.from_iterable(it.combinations(s, r)
**for** r **in** range(len(s) + 1))
例如:
>>> list(powerset([1,2,3]))
[(), (1,), (2,), (3,), (1, 2), (1, 3), (2, 3), (1, 2, 3)]
定时器装饰器:
显示每个类/方法/函数的运行时。
**from** time **import** time
**from** functools **import** wraps
**def** timeit(func):
*"""* **:param** *func: Decorated function* **:return***: Execution time for the decorated function
"""* @wraps(func)
**def** wrapper(*args, **kwargs):
start = time()
result = func(*args, **kwargs)
end = time()
print(**f'{**func.__name__**} executed in {**end - start**:.4f} seconds'**)**return** result
**return** wrapper
例如:
**import** random# An arbitrary function
@timeit
**def** sort_rnd_num():
numbers = [random.randint(100, 200) **for** _ **in** range(100000)]
numbers.sort()
**return** numbers>>> numbers = sort_rnd_num()
sort_rnd_num executed in 0.1880 seconds
计算文件中的总行数:
**def** file_len(file_name, encoding=**'utf8'**):
**with** open(file_name, encoding=encoding) **as** f:
i = -1
**for** i, line **in** enumerate(f):
**pass
return** i + 1
*举例:*你当前目录的 python 文件有多少行代码?
>>> from pathlib import Path>>> p = Path()
>>> path = p.resolve() # similar to os.path.abspath()
>>> print(sum(file_len(f) for f in path.glob('*.py')))
745
只是为了好玩!创建长标签:
>>> s = "#this is how I create very long hashtags"
>>> "".join(s.title().split())
'#ThisIsHowICreateVeryLongHashtags'
以下不是一口大小的食谱,但不要被这些错误咬到!
注意不要混淆可变和不可变对象!
*示例:*用空列表作为值初始化字典
>>> nums = [1, 2, 3, 4]
# Create a dictionary with keys from the list.
# Let's implement the dictionary in two ways
>>> d1 = {n: [] for n in nums}
>>> d2 = dict.fromkeys(nums, [])
# d1 and d2 may look similar. But list is mutable.
>>> d1[1].append(5)
>>> d2[1].append(5)
# Let's see if d1 and d2 are similar
>>> print(f'd1 = {d1} \nd2 = {d2}')
d1 = {1: [5], 2: [], 3: [], 4: []}
d2 = {1: [5], 2: [5], 3: [5], 4: [5]}
不要在遍历列表时修改它!
*示例:*从列表中删除所有小于 5 的数字。
*错误实现:*迭代时移除元素!
nums = [1, 2, 3, 5, 6, 7, 0, 1]
for ind, n in enumerate(nums):
if n < 5:
del(nums[ind])# expected: nums = [5, 6, 7]
>>> nums
[2, 5, 6, 7, 1]
正确实施:
使用列表理解创建一个新列表,只包含您想要的元素:
>>> id(nums) # before modification
2090656472968
>>> nums = [n for n in nums if n >= 5]
>>> nums
[5, 6, 7]
>>> id(nums) # after modification
2090656444296
你可以在上面看到,[id](https://docs.python.org/3/library/functions.html#id)(nums)
在前面和后面被检查,以表明实际上这两个列表是不同的。因此,如果在其他地方使用该列表,并且改变现有列表很重要,而不是创建一个同名的新列表,则将它分配给切片:
>>> nums = [1, 2, 3, 5, 6, 7, 0, 1]
>>> id(nums) # before modification
2090656472008
>>> nums[:] = [n for n in nums if n >= 5]
>>> id(nums) # after modification
2090656472008
目前就这样了(查看第二个小型博客这里)。如果你也有一些经常使用的小函数,请告诉我。我会尽量让的笔记本在 GitHub 上保持最新,你的也可以在那里结束!
一口大小的蟒蛇食谱——第 2 卷
Python 中有用的小函数的集合
Photo by Jordane Mathieu on Unsplash
*免责声明:*这是我在网络上找到的一些有用的小函数的集合,主要在堆栈溢出或 Python 的文档页面上。有些可能看起来微不足道,但无论如何,我都在我的项目中使用过它们,我认为它们值得分享。你可以在我试图保持更新的笔记本中找到它们(以及一些额外的评论)。如果你感兴趣,你可以在这里查看我的第一篇关于小型函数的博客!
除非必要,我不打算过度解释这些功能。那么,让我们开始吧!
返回可迭代的前 N 项
**import** itertools **as** it**def** first_n(iterable, n):
*""" If n > len(iterable) then all the elements are returned. """***return** list(it.islice(iterable, n))
示例:
>>> d1 = {3: 4, 6: 2, 0: 9, 9: 0, 1: 4}
>>> first_n(d1.items(), 3)
[(3, 4), (6, 2), (0, 9)]
>>> first_n(d1, 10)
[3, 6, 0, 9, 1]
检查一个可迭代的所有元素是否都相同
**import** itertools **as** it**def** all_equal(iterable):
*""" Returns True if all the elements of iterable are equal to each other. """*g = it.groupby(iterable)
**return** next(g, **True**) **and not** next(g, **False**)
例如:
>>> all_equal([1, 2, 3])
False
>>> all_equal(((1, 0), (True, 0)))
True
>>> all_equal([{1, 2}, {2, 1}])
True
>>> all_equal([{1:0, 3:4}, {True:False, 3:4}])
True
当您有一个序列时,下面的替代方法通常会更快。(如果您正在处理非常大的序列,请确保自己进行测试。)
**import** itertools **as** it**def** all_equal_seq(sequence):
*""" Only works on sequences. Returns True if the sequence is empty or all the elements are equal to each other. """***return not** sequence **or** sequence.count(sequence[0]) == len(sequence)
例如:你有一份卡车清单,可以查看它们是在仓库里还是在路上。随着时间的推移,每辆卡车的状态都会发生变化。
**import** random
random.seed(500)*# Just creating an arbitrary class and attributes* **class** Truck:
**def** __init__(self, id):
self.id = id
self.status = random.choice((**'loading-unloading'**, **'en route'**))**def** __repr__(self):
**return f'P{**str(self.id)**}'** trucks = [Truck(i) **for** i **in** range(50)]
早上你查看了一下,发现第一辆卡车是en route
。你听说另外三个人也离开了仓库。让我们验证一下:
>>> all_equal_seq([t.status **for** t **in** trucks[:4]])
True
用和**None**
求和
当你有numpy
阵列或者pandas
系列或者数据帧时,选择是显而易见的:[numpy.nansum](https://docs.scipy.org/doc/numpy/reference/generated/numpy.nansum.html)
或者[pandas.DataFrame/Series.sum](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.sum.html)
。但是如果你不想或者不能使用这些呢?
**def** sum_with_none(iterable):**assert not** any(isinstance(v, str) **for** v **in** iterable), **'string is not allowed!'** **return** sum(filter(**None**, iterable))
这是因为[filter](https://docs.python.org/3/library/functions.html#filter)
将None
视为身份函数;即删除 iterable 的所有 falsy 元素。
例如:
>>> seq1 = [None, 1, 2, 3, 4, 0, True, False, None]
>>> sum(seq1)
**TypeError**: unsupported operand type(s) for +: 'int' and 'NoneType'>>> sum_with_none(seq1) # Remember True == 1
11
检查 N 或更少的项目是否真实
**def** max_n_true(iterable, n):
*""" Returns True if at most `n` values are truthy. """***return** sum(map(bool, iterable)) <= n
例如:
>>> seq1 = [None, 1, 2, 3, 4, 0, True, False, None]
>>> seq2 = [None, 1, 2, 3, 4, 0, True, False, 'hi']
>>> max_n_true(seq1, 5)
True
>>> max_n_true(seq2, 5) # It's now 6
False
检查 Iterable 中是否只有一个元素为真
**def** single_true(iterable):
*""" Returns True if only one element of iterable is truthy. """*i = iter(iterable)
**return** any(i) **and not** any(i)
函数的第一部分确保迭代器有任何真值值。然后,它从迭代器中的这一点开始检查,以确保没有其他真值。
*示例:*使用上面的几个函数!
*# Just creating an arbitrary class and attributes* **class** SampleGenerator:
**def** __init__(self, id, method1=**None**, method2=**None**, method3=**None**,
condition1=**False**, condition2=**False**,
condition3=**False**):
*"""
Assumptions:
1) One and only one method can be active at a time.
2) Conditions are not necessary, but if passed, maximum one can have value.
"""
# assumption 1* **assert** single_true([method1, method2, method3]), **"Exactly one method should be used"** *# assumption 2* **assert** max_n_true([condition1, condition2, condition3], 1), **"Maximum one condition can be active"**self.id = id
下面的第一个样本(sample1
)是有效的,但是其他样本违反了至少一个假设,导致了一个AssertionError
(为了避免混乱,我没有在这里显示任何一个。)在笔记本上运行它们,亲自查看错误。
>>> sample1 = SampleGenerator(1, method1=**'active'**) *# Correct* >>> sample2 = SampleGenerator(2, condition2=**True**) *# no method is active* >>> sample3 = SampleGenerator(3, method2=**'active'**, method3=**'not-active'**) *# more than one method has truthy value* >>> sample4 = SampleGenerator(4, method3=**'do something'**, condition1=**True**, condition3=**True**) *# multiple conditions are active* >>> sample5 = SampleGenerator(5) *# nothing is passed*
写入 CSV 时跳过冗余标题
假设你需要运行一系列的模拟。在每次运行结束时(可能需要几个小时),您记录一些基本的统计数据,并希望创建或更新一个用于跟踪结果的restults.csv
文件。如果是这样,您可能希望在第一次之后跳过将头写入文件。
首先,让我们创建一些数据来使用:
**import** pandas **as** pd
**import** random*# An arbitrary function* **def** gen_random_data():
demands = [random.randint(100, 900) **for** _ **in** range(5)]
costs = [random.randint(100, 500) **for** _ **in** range(5)]
inventories = [random.randint(100, 1200) **for** _ **in** range(5)]
data = {**'demand'**: demands,
**'cost'**: costs,
**'inventory'**: inventories} **return** pd.DataFrame(data)*# Let's create a few df* df_list = [gen_random_data() **for** _ **in** range(3)]
现在,让我们假设我们需要在df_list
数据帧一创建好就把它们写入orders.csv
。
**import** osfilename = **'orders.csv'
for** df **in** df_list:
df.to_csv(filename, index=**False**, mode=**'a'**,
header=(**not** os.path.exists(filename)))
如果您不需要一次循环一个相似的数据帧,下面的替代方法是将它们写入文件的简洁方法:
pd.concat(df_list).to_csv(‘orders2.csv’, index=False)
将 CSV 文件转换为 Python 对象
假设您需要创建 Python 对象的集合,其中它们的属性来自 CSV 文件的列,并且文件的每一行都成为该类的一个新实例。然而,假设您事先不知道 CSV 列是什么,因此您不能用期望的属性初始化该类。
下面,您可以看到实现这一点的两种方法:
**class** MyClass1(object):
**def** __init__(self, *args, **kwargs):
**for** arg **in** args:
setattr(self, arg, arg) **for** k, v **in** kwargs.items():
setattr(self, k, v) **class** MyClass2:
**def** __init__(self, **kwargs):
self.__dict__.update(kwargs)
在MyClass1
中,我们可以同时传递args
和kwargs
,而在MyClass2
中,我们利用了特殊的[__dict__](https://stackoverflow.com/a/19907498)
属性。
*示例:*让我们使用这两种实现将上面示例中的orders.csv
文件转换成对象。
**import** csvfilename = **'orders.csv'** class1_list = []
class2_list = []**with** open(filename) **as** f:
reader = csv.DictReader(f)**for** row **in** reader:class1_list.append(MyClass1(**row))
class2_list.append(MyClass2(**row))# Let's check the attributes of the first row of class1_list
>>> print(f'first row = {vars(class1_list[0])}')
first row = {'demand': '821', 'cost': '385', 'inventory': '1197'}
暂时就这样了。如果你也有一些经常使用的小函数,请告诉我。我会尽量在 GitHub 上保持笔记本的最新状态,你的笔记本也可以放在那里!
Ruby 中的按位排序
想象一下,你运营着一个 NBA 篮球招募网站。您希望根据以下三个属性对全国所有大学篮球运动员进行排名:
- 玩家的个人评分(1-10)
- 玩家的团队等级(1-10)
- 玩家的会议等级(1-10)
- 玩家的 ID(任意数字)
几个玩家的例子,排名是这样的:
Player 1: rating 10, team rating 10, conference rating 10
Player 2: rating 10, team rating 10, conference rating 5
Player 3: rating 10, team rating 5, conference rating 10
Player 4: rating 10, team rating 5, conference rating 5
Player 5: rating 5, team rating 10, conference rating 10
Player 6: rating 5, team rating 10, conference rating 5
如果你采用天真的方法,对所有球队的所有球员进行排序可能是一个昂贵的计算。举例来说,让我们假设有 1,000,000 个玩家需要评估。使用嵌套排序,我们可以构建如下内容:
Player = Struct.new(:id, :rating, :team_rating, :conf_rating)# setupid = 1.step
[@players](http://twitter.com/players) = 1_000_000.times.map do
Player.new(id.next, rand(0..10), rand(0..10), rand(0..10))
end# I want to sort by player rating descending,
# then team rating descending,
# then conference rating descending,
# then by id descending.def sort_naively
[@players](http://twitter.com/players).group_by(&:rating).sort.reverse.map do |rb|
rb[1].group_by(&:team_rating).sort.reverse.map do |tb|
tb[1].group_by(&:conf_rating).sort.reverse.map do |cb|
cb[1].group_by(&:id).sort.reverse.map do |ib|
ib[1]
end
end.flatten
end.flatten
end.flatten
end
平均来说,使用这种方法对 1,000,000 条记录进行排序需要大约 2000 毫秒。考虑到记录集的大小,这还不错。然而,深度嵌套的数组结构使用了大量的内存,大约 219MB。这是 Ruby 应用程序中的一个常见问题,但是因为我们有一个 GC,所以我们通常是安全的。总而言之,对于较小的数据集来说,这是一个可用的算法,但我认为我们可以对它进行改进。
我们可以使用Array.sort_by
进行排序,它看起来更漂亮,并传入一个值数组进行排序:
@players.sort_by do |p|
[p.rating, p.team_rating, p.conf_rating, p.id]
end.reverse
…但这更慢。在我的测试中,平均需要 10 秒钟。然而,它使用了 80MB 的内存,所以这是一个小的改进。在低内存环境中,这可能会派上用场,但让我们探索另一种排序方法,一种通过利用巧妙的位运算来节省时间和内存的方法。
如果你不熟悉按位运算,它们在 C 等低级语言中被广泛用于对位进行运算。在 Ruby 中,主要的按位运算符有:
& -> AND
| -> OR
^ -> XOR
~ -> NOT
<< -> LEFT SHIFT
>> -> RIGHT SHIFT
关于这些功能的详细解释可以在这里找到,这超出了本文的范围。对于这篇文章,我们只需要一个关于| (OR)
和<< (LEFT SHIFT)
的解释者
如果你已经了解了这么多,你可能对计算机的工作原理有了很好的了解,这些位就是 1 和 0。按位运算符对这些位进行运算。
Bitwise | (OR)
基本上接受两个参数,第一个和第二个位,并返回一个新位,其中第一个位的值为 1,第二个位的值为 1,在缺少 1 的地方添加 1。例如:
integer | bit | operator | other bit | new bit | result
0 | 0000 | | | 0001 | 0001 | 1
1 | 0001 | | | 0001 | 0000 | 1
2 | 0010 | | | 0001 | 0011 | 3
3 | 0011 | | | 0001 | 0011 | 3
4 | 0100 | | | 0001 | 0101 | 5
5 | 0101 | | | 0010 | 0111 | 7
10 | 1010 | | | 0101 | 1111 | 15
另一方面,按位<< (LEFT SHIFT)
将位左移一定数量的位置,在该位的末尾添加 0。例如:
integer | bit | operator | number | result | integer
1 | 1 | << | 1 | 10 | 2
2 | 10 | << | 1 | 100 | 4
3 | 11 | << | 1 | 110 | 6
10 | 1010 | << | 2 | 101000 | 40
我们可以利用这两个操作来使我们的排序算法变得更加智能。由于排序优先顺序总是rating > team_rating > conf_rating > id
,因此,无论其他等级是什么,等级为 10 的玩家将总是排在等级为 9 的玩家之上,以此类推。在评级相同的玩家之间,具有较好团队评级的玩家将被整体评级更高,等等。
为了使用按位操作符实现这种排序,我们应该在 Player 结构中添加一个新参数bit_rank
。新代码如下所示:
Player = Struct.new(
:id,
:rating,
:team_rating,
:conf_rating,
:bit_rank # <- new attribute
)# setupid = 1.step
[@players](http://twitter.com/players) = 1_000_000.times.map do
Player.new(id.next, rand(0..2), rand(0..2), rand(0..2))
end#now, calculate the bit rank for each player
@players.each do |p|
p.bit_rank = p.rating << 30 |
p.team_rating << 25 |
p.conf_rating << 20 |
p.id
end
简而言之,这个新的bit_rank
属性是一个数字(一个很大的数字),代表玩家的整体等级。我们在 30 个位置上移动评级,团队评级 25 个位置,会议评级 20 个位置,然后对所有三个位置加上 ID 运行按位OR
。例如,一个 ID 为 1 并且在所有三个类别中都被评为 10 的玩家将拥有11_083_448_321
的bit_rank
。当查看该值的位表示时,这是很直观的,它是:
0101001010010100000000000000000000101010 01010 01010 0000000000000000000001
^ ^ ^ ^
| | | |__player ID = 1
| | |
| | |__player conference rating (10 = 01010)
| |
| |__player team rating (10 = 01010)
|
|__player rating (10 = 01010)
同一个全是 5 的玩家会有一个5_541_724_161
的bit_rank
,当在 bit 镜头中看到时:
0010100101001010000000000000000000100101 00101 00101 00000000000000000001
^ ^ ^ ^
| | | |__player ID = 1
| | |
| | |__player conference rating (5 = 00101)
| |
| |__player team rating (5 = 00101)
|
|__player rating (5 = 00101)
…有道理。
bit_rank
将排序优先级嵌入到自身中,其中玩家各自的评级被转移到位等级的适当区域,并且它们都被向左移动足够远,以在最后仍然按 ID 排序。
现在我们已经了解了bit_rank
正在做什么,让我们看看运行这样一个复杂的操作需要什么代码:
@players.sort_by(&:bit_rank)
对,就是这样。因为bit_rank
是 Struct 的一个属性,所以可以使用旧的Symbol.to_proc
ruby magic 来调用它。如果我们在 Rails 领域,这可能是模型的一个属性,使得排序和排名非常容易。
但是,当您查看时间和内存使用情况时,使用这种方法确实很出色。
平均而言,将该属性添加到百万个玩家对象中的每一个会使设置阶段增加 200 毫秒,但是基于该属性的排序会将排序时间减少到 500 毫秒。总之,以前的800ms build + 2000ms sort = 2800ms
操作现在变成了1000ms build + 500ms sort = 1500ms
!我们将排序时间减少了 1300 毫秒,提高了 46%!我们添加的排序属性越多,效率也会成倍提高。
然而,内存的使用是难以置信的。为了刷新,最初的简单排序使用了 219MB 的内存,主要是因为它要排序到10 + (10*10) + (10*10*10) + (10*10*10*10) = 11,110
个独立的、已排序的数组中。当使用bit_rank
排序方法时,我们的操作只使用了 16MB,也就是少了 203MB,减少了 92%,因为我们没有任何嵌套数组。实际上,所有的内存占用都来自于构建 1,000,000 个玩家的数组。
使用这种方法非常适合大型数据集上的简单排序,提供了一种简单的方法来对多个值进行排序,并且在内存不足的环境中非常有用。
探索位操作符背后的想法来自参与代码的问世,我强烈推荐参与其中。如果你还没有完成挑战,你也可以完成。
希望你喜欢这篇文章。你可以在推特或 github 上找到我。
黑箱模型实际上比逻辑回归更容易解释
SHAP 价值观是不可理解的。但是,从它们开始,可以用对概率的影响来表达模型选择(这是一个人类更容易理解的概念)
可解释的和强大的之间永恒的斗争
从事数据科学家工作的人更清楚这一点:围绕机器学习的一个主要陈词滥调是,你必须在以下两者之间做出选择:
- 简单、可靠和可解释的算法,如逻辑回归;
- 强大的算法可以达到更高的精度,但代价是失去任何可懂度,如梯度增强或支持向量机。这些模型通常被称为“黑箱”,意味着你知道什么进入什么出来,但没有办法了解在引擎盖下实际发生了什么。
在下文中,我们将表明,不仅不需要在能力和可解释性之间做出选择,而且更强大的模型甚至比更肤浅的模型更具可解释性。
数据
举例来说,我们将使用最著名的数据集之一:标志性的泰坦尼克号数据集。我们有一堆关于泰坦尼克号乘客的变量,我们想预测每个乘客生还的可能性有多大。
我们可以处理的变量有:
- Pclass:票类;
- 性:乘客性;
- 年龄:乘客年龄;
- SibSp:泰坦尼克号上兄弟姐妹/配偶的数量;
- Parch:泰坦尼克号上父母/孩子的数量;
- 票价:客运票价;
- 装船:装船港。
(注:为了简单起见,去掉了一些其他变量,几乎没有进行任何数据预处理)。
数据看起来是这样的:
Data, first 5 passengers
好的老罗格
对于分类问题,逻辑回归常被用作基线。
在对定性特征(机票等级、乘客性别和装货港)进行一次性编码后,我们对训练数据进行简单的逻辑回归拟合。在验证集上计算的准确度总计为 81.56% 。
从这个模型中我们能得到什么启示?因为逻辑回归是这样建立的
分析围绕着 β 进行。很容易看出,在 xⱼ 每增加 1 个单位,几率就会增加exp(βⱼ倍。
解释很简单:年龄增加一岁将导致几率增加 0.961 倍。
那就是:
- 盖伊 A,25 哟:生存几率= 3
- 除了年龄(26 岁)之外,其他变量都与 A 相同:生存几率= 3× 0.961 = 2.883
这个模型在可解释性方面的局限性是不言而喻的:
- exp(βⱼ) 和赔率之间的关系是线性的,例如,2 或 12 yo 之间的差异与 52 和 62 yo 之间的差异相同;
- 不允许相互作用,例如,无论乘客是在一等舱还是三等舱,60 岁对生存概率有相同的影响。
尝试黑盒:Catboost 和 SHAP
现在让我们尝试一个“黑盒”模型。在这个例子中,我们将使用 Catboost ,这是一种在决策树上进行梯度提升的算法。
对相同的训练数据执行快速 Catboost(没有任何超参数调整)(这次不需要一键编码)会导致验证数据的 87.15% 准确度。
正如我们所料, Catboost 的表现大大超过了 LogReg (87.15%对 81.56%) 。到目前为止,不足为奇。
现在,机器学习中 64,000 美元的问题是:如果 Catboost 在预测未知数据的存活率方面比 LogReg 好得多,我们难道不应该信任它,而不要太担心齿轮?
嗯,看情况。如果是 Kaggle 比赛,答案可能是“是”,但总体来说是“肯定不是!”。如果你想知道为什么,只要看看一些高调的事件,如亚马逊"性别歧视人工智能招聘工具"或微软"种族主义聊天机器人"。
因此,为了弄清楚 Catboost 正在做出什么决定,另一个叫做SHAP(SHapley Additive explaints)的工具来帮助我们了。SHAP(可能是机器学习可解释性的最先进水平)诞生于 2017 年,这是一种对任何预测算法的输出进行逆向工程的聪明方法。
对我们的数据和 Catboost 输出应用 SHAP 分析会生成一个矩阵,它与原始数据矩阵具有相同的维数,包含 SHAP 值。SHAP 价值观是这样的:
SHAP values, first 5 passengers
SHAP 值越高,生存的概率越高,反之亦然。此外,大于零的 SHAP 值导致概率增加,小于零的值导致概率减少。
每个 SHAP 值表示,这是这里的重要部分,个体变量的观测水平对该个体最终预测概率的边际效应。
这意味着:假设对于第一个人,我们知道除了年龄以外的所有变量。那么它的 SHAP 和就是-0.36-0.76+0.05-0.03-0.3-0.08 =-1.48。一旦我们知道了个人的年龄(也考虑了年龄和其他变量之间的相互作用),我们就可以更新总和:-1.48 +0.11 =-1.37。
SHAP:房间里有一头大象
你可以使用SHAP 专用的 Python 库来获得奇特的图(基本上包含关于 SHAP 值的描述性统计)。例如,您可以很容易地获得一个按特征细分的总结每个观测值的 SHAP 值的图。在我们的例子中:
你能看见房间里的大象吗?
如果你把这幅图给外行(甚至是你的老板)看,他可能会说:“漂亮的颜色。但是下面的尺子是什么?那些是美元吗?公斤?年?”
简而言之, SHAP 值对于人类(甚至对于数据科学家)来说都是不可理解的。
从形状到预测概率
概率的概念更容易理解。其实还不止这些:是先天的(毕竟这也是人们一直坐飞机的原因)。
From SHAP to Predicted Probability
希望从 SHAP 转移到概率,最显而易见的事情是绘制相对于 SHAP 和(每个个体)的预测生存概率(每个个体)。
显而易见,这是一个确定性函数。也就是说,我们可以无误地从一个量转换到另一个量。毕竟,两者唯一的区别是概率必然在[0,1]内,而 SHAP 可以假设任何实数。因此:
其中 f(。)是单调递增的 s 形函数,它将任何实数映射到[0,1]区间(为简单起见,f()可以是有界在[0,1]中的普通插值函数)。
From SHAP to Predicted Probability — An example
就拿个人来说吧。假设,已知除年龄以外的所有变量,其 SHAP 和等于 0。现在假设相对于年龄的 SHAP 值是 2。
我们知道函数 f()来量化年龄对预测生存概率的影响就足够了:简单来说就是 f(2)-f(0)。在我们的例子中,它是 f(2)-f(0) = 80%-36% = 44%
毫无疑问,的生存概率比的 SHAP 更容易理解。更容易理解的是:说这个个体年龄导致 SHAP 上升 2 或概率上升 44%?
那又怎样?
基于我们刚刚看到的,从上面看到的 SHAP 矩阵开始,应用公式就足够了:
获取以下内容:
Impact on predicted probabilities, first 5 passengers
例如,拥有一张三等票将第一个乘客的生存概率降低了-4.48%(对应于-0.36 SHAP)。请注意,3 号乘客和 5 号乘客也在三等舱。由于与其他特征的相互作用,它们对概率的影响(分别为-16.65%和-5.17%)是不同的。
可以对这个矩阵进行几种分析。我们只是报道一些情节作为例子。
Marginal Effect of Passenger Age
Marginal Effect of Passenger Fare
Interaction Effect: Passenger Fare vs. Ticket Class
红线表示平均效应(一组中所有个体的平均年龄效应),而蓝带(平均标准偏差)表示同一组中个体之间年龄效应的可变性。可变性是由于年龄和其他变量之间的相互作用。
这种方法的附加价值在于:
- 我们可以用概率而不是 SHAP 值来量化影响。例如,我们可以说,平均而言,与 0-10 岁相比,60-70 岁的人存活概率会降低 27%(从+14%到-13%);
- 我们可以想象非线性效应。比如看客运票价,生存概率上升到一点后略有下降;
- 我们可以表现互动。例如,乘客票价与机票等级。如果这两个变量之间没有相互作用,这三条线将是平行的。他们表现出不同的行为。蓝线(头等舱乘客)票价略有下降。特别有趣的是红线(三等舱乘客)的趋势:在乘坐三等舱的两个相同的人之间,支付 50-75 英镑的人比支付高达 50 英镑的人生存的可能性高约 15%(从-10%到+5%)。
包扎
像逻辑回归这样的简单模型做了大量的简化。黑盒模型更灵活,因此更适合复杂(但非常直观)的现实世界行为,如非线性关系和变量之间的相互作用。
可解释性意味着以人类可以理解的方式表达模型的选择,基于他们对现实的感知(包括复杂的行为)。
我们已经展示了一种将 SHAP 值转化为概率的方法。这使得我们有可能将黑盒功能可视化,并确保它与我们对世界的认识(定性和定量)一致:一个比简单模型描述的世界更具多面性的世界。
把密码给我!
代码可在此处找到:
[## smazzanti/TDS _ black _ box _ models _ more _ explaible
此时您不能执行该操作。您已使用另一个标签页或窗口登录。您已在另一个选项卡中注销,或者…
github.com](https://github.com/smazzanti/tds_black_box_models_more_explainable/blob/master/Shap2Probas.ipynb)
尽情享受吧!