打破界限:探索 LLM 的函数调用
原文:
towardsdatascience.com/breaking-boundaries-exploring-function-calling-for-llms-73d063d46fcb
函数调用如何为大型语言模型(LLM)与外部工具和 API 的无缝集成铺平道路
·发表于 Towards Data Science ·阅读时间 8 分钟·2023 年 8 月 10 日
–
使用 SDXL 生成的图像
当我发现大型语言模型(LLM)具备与外部工具和 API 互动的能力时,我知道一切将不再相同。
这是否使我们更接近实现人工通用智能(AGI)?也许没有,但它无疑开启了人工智能的全新时代:一个 LLM 可以执行任何你能放入函数中的任务的时代。现在,这可能看起来有些异端,但对我来说,这使我们更接近我对 AGI 的愿景,因为我不是那种幻想有意识机器或其他科幻叙事的人。
想象一下这样一个场景:一个人工智能代理完全掌控一个任务,与你在线运行的其他代理进行沟通,获取数据,并返回你所期望的结果。这种变革性的能力不仅重新定义了我们与互联网的互动方式,而且还重塑了我们的思维过程。
快进几年:假设你想计划一次假期。第一步不是在网上搜索票务,而是指示一个 AI 代理从头到尾规划和组织一切。你只有在收到几封确认邮件时才会知道任务完成了——如果邮件仍然存在的话——并且你的信用卡上显示了一笔费用。
但我们不要走得太远。即使在我们当前的现实中,你也可以创建能够执行任何程序上可能的任务的代理。不过,这需要你付出一些努力。你需要熟悉 OpenAI 模型系列的新函数调用能力,并创建模型将使用的自定义工具。
这就是这篇博客文章的用武之地。把它当作一个动手指南,帮助你将 AI 代理变为现实——一个可以在你的个人 Google Calendar 账户上创建事件的代理。然而,通过简单地更改 LLM 调用的函数,世界将由你掌控。准备好深入了吗?
学习率 是一本面向对 ML 和 MLOps 世界感兴趣的人的通讯。如果你想了解更多类似的主题,请 订阅。你将在每个月的最后一个星期天收到我的更新和关于最新 MLOps 新闻和文章的想法!
工具包
我们待办事项的首要任务是构建工具,即我们的 LLM 将调用的函数来创建新的日历事件。我们的选择武器?LangChain——可能是发展最快的 Python 库。
LangChain 已经配备了大量现成的 工具。然而,它的武器库中没有提供将 LLM 与 Google Calendar API 集成的工具。但没关系——我们自己来创建!
我们从深入了解 Google Calendar Event 参考指南 开始。这有助于我们概述函数签名的样子。但请记住,我们并不需要为每一项提供值,所以让我们标记出我们认为最关键的参数,并定义函数的输入。小提示:LangChain 期望我们使用 Pydantic 模型来实现这一点。
from pydantic import BaseModel, Field
class CalendarEventInput(BaseModel):
summary: str = Field(description="The title of the event")
location: str = Field(description="The location of the event")
description: str = Field(description="The description of the event")
start: EventDateTime = Field(description="The start datetime of the event")
end: EventDateTime = Field(description="The end datetime of the event")
attendees: List[Attendee] = Field(description="The attendees of the event")
reminders: Reminders = Field(description="The reminders of the event")
conferenceDataVersion: int = Field(
description="Set to `1` if you need to create a new Google Meet link.")
recurrence: List[str] = Field(
description="A list of RRULE, EXRULE, RDATE and EXDATE lines for a"
" recurring event, as specified in RFC5545")
正如你所看到的,我们期望我们的模型提供事件的各种细节:摘要、位置、参与者、开始和结束时间等等。
这些字段有些是简单的类型,如字符串或列表,而其他字段则要求我们挽起袖子定义 Python 类。那么,事不宜迟,让我们深入探讨下一步:
from typing import List
from dataclasses import dataclass
@dataclass()
class EventDateTime:
dateTime: str
timeZone: str
@dataclass()
class Attendee:
displayName: str
email: str
optional: bool
@dataclass()
class ReminderOverride:
method: str
minutes: str
@dataclass()
class Reminders:
useDefault: bool
overrides: List[ReminderOverride]
很好,我们已经准备好函数模型。接下来一步——制作自定义工具。我们的任务是创建一个继承自 LangChain 的 BaseTool
类的类,至少需要实现 _run
或 _arun
方法。
我们还需要设置一些关键值,如工具的 name
、description
(提示模型何时使用它),以及 args_schema
属性,后者描述了工具的模式——我们之前定义的模式。让我们开始吧:
from langchain.tools.base import BaseTool
from googleapiclient.discovery import build
class CalendarEventTool(BaseTool):
name: str = "calendar_event"
description: str = "Useful tool for creating new Google calendar events"
args_schema: Type[BaseModel] = CalendarEventInput
def _create_event(self, calendar_id: str, body: dict,
conferenceDataVersion: int):
"""Create a new Google calendar event.
Args:
calendar_id (str): The calendar id.
body (str): The event body.
conferenceDataVersion (int): Set to `1` to create a new Google
Meet Event.
Returns:
dict: The event response.
"""
service = build("calendar", "v3", credentials=get_credentials())
event = (service.events() # type: ignore
.insert(calendarId=calendar_id, body=body,
conferenceDataVersion=conferenceDataVersion)
.execute())
return event
def _run(self, summary: str, location: str, description: str,
start: EventDateTime, end: EventDateTime,
attendees: List[Attendee], reminders: Reminders,
conferenceDataVersion: int, recurrence: List[str]):
"""Run the CalendarEventTool with the given parameters.
Args:
summary (str): The summary or title of the event.
location (str): The location of the event.
description (str): The description or details of the event.
start (EventDateTime): The start date and time of the event.
end (EventDateTime): The end date and time of the event.
attendees (List[Attendee]): A list of attendees for the event.
reminders (Reminders): The reminders for the event.
conferenceDataVersion (int): The version of the conference data.
recurrence (List[str]): A list of recurrence rules for the event.
"""
body = create_request_body(summary, location, description,
start, end, attendees, reminders,
recurrence)
event = self._create_event(CALENDAR_ID, body, conferenceDataVersion)
def _arun(self):
raise NotImplementedError("calendar_event does not support async")
我们的工具非常简单。我们只定义了 _run
函数,因为我们不需要为这个任务支持异步调用。_run
函数的作用是组装请求体并调用另一个私有函数,恰如其分地命名为 _create_event
。
让我们逐步处理。首先,我们需要定义 create_request_body
实用函数:
import random
import string
from typing import List
def _create_attendee_list(attendees):
attendee_list = [{"displayName": attendee.displayName,
"email": attendee.email,
"optional": attendee.optional}
for attendee in attendees]
return attendee_list
def _create_reminder_list(reminders):
reminder_list = {"useDefault": reminders.useDefault,
"overrides": [{"method": override.method,
"minutes": override.minutes}
for override in reminders.overrides]}
return reminder_list
def create_request_body(summary: str, location: str, description: str,
start: EventDateTime, end: EventDateTime,
attendees: List[Attendee], reminders: Reminders,
recurrence: List[str]) -> dict:
attendee_list = _create_attendee_list(attendees)
reminder_list = _create_reminder_list(reminders)
request_id = ''.join(random.choice(string.ascii_letters) for _ in range(8))
body = {
"summary": summary,
"location": location,
"description": description,
"start": {
"dateTime": start.dateTime,
"timeZone": start.timeZone
},
"end": {
"dateTime": end.dateTime,
"timeZone": end.timeZone
},
"attendees": attendee_list,
"reminders": reminder_list,
"conferenceData": {
"createRequest": {
"requestId": request_id,
"conferenceSolutionKey": {
"type": "hangoutsMeet"
}
}
},
"recurrence": [r for r in recurrence]}
return body
我们的create_request_body
函数依赖于几个其他的实用函数来创建 JSON 对象。我们在它上面几行定义了这些私有函数。
我们的下一个目标?_create_event
函数。这个函数构建了我们将用于执行 API 调用的服务。为了完成这项工作,你需要获取一个凭据文件。不确定怎么做?只需遵循这个指南:OAuth 客户端 ID 凭据。下载 JSON 文件后,将其重命名为credentials.json
,然后使用下面的函数来获取和存储令牌:
import os
from typing import Union
from pathlib import Path
import google.oauth2.credentials as oauth2
import google.auth.external_account_authorized_user as auth
from google.oauth2.credentials import Credentials
from google.auth.transport.requests import Request
from google_auth_oauthlib.flow import InstalledAppFlow
HOME = Path.home()
TOKEN_FILE = "token.json"
CREDS_FILE = "credentials.json"
SCOPES = ["https://www.googleapis.com/auth/calendar"]
def get_credentials() -> Union[oauth2.Credentials, auth.Credentials]:
creds = None
# If token.json exists, read it and check if it's valid
if os.path.exists(TOKEN_FILE):
creds = Credentials.from_authorized_user_file(TOKEN_FILE, SCOPES)
# If there's no valid token.json, refresh it or create a new one
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
flow = InstalledAppFlow.from_client_secrets_file(
CREDS_FILE, SCOPES)
creds = flow.run_local_server(port=0)
with open(TOKEN_FILE, "w") as token:
token.write(creds.to_json())
return creds
将所有这些代码整合在一起应该能为你提供所需的工具。这看起来可能有点复杂,但这不是你自己无法做到的,对吧?
现在,让我们换个方向,创建一个可以有效利用这个工具的 AI 代理。
代理
LangChain 确实消除了这一部分的猜测。你只需初始化一个新的[OPENAI_FUNCTIONS](https://python.langchain.com/docs/modules/agents/agent_types/openai_functions_agent)
类型的代理。接下来,你需要为这个代理提供可以访问的工具,然后在用户查询上运行它。听起来很激动人心?那就让我们直接进入吧:
from langchain.chat_models import ChatOpenAI
from langchain.agents import AgentType, initialize_agent
# Initialize the language model
llm = ChatOpenAI(temperature=0, model_name="gpt-4-0613") # type: ignore
# Define the list of tools that the agent can use
tools = [CalendarEventTool()]
# Initialize the agent
agent = initialize_agent(tools, llm,
agent=AgentType.OPENAI_FUNCTIONS,
verbose=True)
user_input = input(f"{Emojis.ASSISTANT} How can I assist you today?\n"
f"{Emojis.USER} > ")
agent.run(input=user_input)
最后但同样重要的是,是时候将这一切付诸实践了。运行你的 Python 文件,并尝试一个测试查询,例如:
“为我与 John 的会议创建一个新的事件,时间定在 8 月 10 日中午(雅典/希腊)。别忘了邀请 John(john@example.com)并设置一个在线会议室。”
这可能会非常顺利,不过你可能需要一些提示工程。但是你做到了!现在,你已经具备了创建可以执行任何你能编写代码的 AI 代理的能力。这有多令人兴奋?
结论
在这次激动人心的旅程中,我们探索了 OpenAI 的大型语言模型(LLMs)如何与外部工具和 API 集成。我们深入探讨了这一改变游戏规则的特性如何重新定义我们与 AI 和互联网的互动,承诺未来 AI 代理可以无缝地执行任何可以编程定义的任务。
从理解函数签名的重要参数到定义 Python 类,我们采取了实践的方法来创建一个可以与 Google 日历 API 交互的模型,使用了一个快速发展的 Python 库 LangChain。
我们的尝试结果是一个能够在个人 Google 日历上创建事件的 AI 代理。然而,你现在应该能够创建一个可以执行任何你能放入函数中的任务的代理。
AI 的未来以及它与数字世界的互动前景光明,随着我们继续利用 AI 的力量,我们可以实现的可能性是无限的。让我们看看你接下来会构建什么!
关于作者
我的名字是 Dimitris Poulopoulos,我是一名在 HPE 工作的机器学习工程师。我为主要客户如欧盟委员会、国际货币基金组织、欧洲中央银行、宜家、Roblox 等设计和实施了人工智能和软件解决方案。
如果你对阅读更多关于机器学习、深度学习、数据科学和数据操作的文章感兴趣,可以在 Medium、LinkedIn 或在 Twitter 上关注 @james2pl。
所表达的观点仅代表我个人,并不反映我雇主的观点或意见。
用一种新的 AI 模型打破蛋白质设计的界限,该模型理解与任何类型分子的相互作用
这一新模型有助于 扩展 ML 模型在工程蛋白质中所需功能的适用性,通过调整其与任何类型分子的特定相互作用,从而有效影响生物技术和临床应用。
LucianoSphere (Luciano Abriata, PhD)
·发表于 Towards Data Science ·阅读时间 5 分钟·2023 年 6 月 21 日
–
作者通过编辑 Dall-E-2 生成的概念艺术图“蛋白质工程”(最初使用 此处)。
在 Deepmind 的 AlphaFold 在结构生物学领域引发革命之后,紧密相关的蛋白质设计领域最近通过深度学习的力量进入了一个新的进步时代。然而,现有的蛋白质设计机器学习 (ML) 模型在将非蛋白质实体纳入设计过程中存在局限,只能处理蛋白质组件。在我们的新预印本中,我们介绍了一种新的深度学习模型,“CARBonAra”,该模型考虑了蛋白质周围的任何分子环境,因此能够设计能够结合任何类型分子的蛋白质:药物类似的配体、辅因子、底物、核酸,甚至其他蛋白质。通过利用我们之前的 ML 模型中的几何变换器架构,CARBonAra 从骨架支架中预测蛋白质序列,同时考虑到任何性质的分子施加的约束。这种开创性的方法有助于通过调整与任何类型细胞成分的特定相互作用来扩展 ML 模型在工程蛋白质中所需功能的多样性。
方案概述了这个新深度学习模型可以做什么:计算蛋白质设计中氨基酸的概率,从一个目标蛋白质骨架开始,该骨架周围有其他分子(在此处由顶部的绿色分子示例)。图像由作者制作。
引言
作为数据科学家,我们不断努力突破可能性的界限。蛋白质设计,即创造具有期望功能和属性的新蛋白质,是一个这样的行动领域;特别是一个在生物学、医学、生命技术和材料科学等多个学科中具有深远影响的领域。尽管基于物理的方法在找到折叠成给定蛋白质结构的氨基酸序列方面取得了进展,但深度学习技术已经成为游戏规则的改变者,显著提高了设计成功率和多样性。
我最近在这里讨论了四种现代机器学习模型用于蛋白质设计和工程:
由于这些基于人工智能的方法和工具,蛋白质生物技术进入了前所未有的激动人心的时代
[towardsdatascience.com
虽然这些模型在许多蛋白质设计任务中取得了成功,但它们在设计过程中考虑非蛋白质实体的能力有限——它们根本无法处理这些实体,这一限制影响了它们的多样性并缩小了它们的应用范围。
为了克服这一挑战,我们在最新的预印本中介绍了一种名为 CARBonAra 的新模型,该模型通过接受作为输入的目标蛋白质支架和任何类型的相互作用分子来彻底改变蛋白质序列设计。这里是预印本:
蛋白质设计和工程在利用深度学习的进展方面正以空前的速度发展。当前……
CARBonAra 基于我们的蛋白质结构变换器(PeSTo),一种几何变换器架构,该架构处理原子点云,忽略原子类型以元素名称直接表示分子。我之前详细描述了 PeSTo:
详细了解新预印本 [## 新预印本描述了一种新型的无参数几何变换器,用于…
并且运行速度如此之快,以至于它甚至可以扫描大量蛋白质结构以搜索易于交互的氨基酸…
CARBonAra 的核心基于 PeSTo 模型,使其能够将任何种类的非蛋白质分子,包括核酸、脂质、离子、小配体、辅因子或其他蛋白质,纳入新蛋白质的设计过程中。因此,给定一个带有一个或多个配体的输入蛋白质结构,CARBonAra 预测氨基酸的残基信心,通过这些信心的最大值可以重建蛋白质序列。为此,CARBonAra 以骨架作为输入,并生成潜在序列的空间,这些序列可以通过特定的功能或结构要求进一步约束——例如,固定某些氨基酸,若它们对某一功能至关重要。CARBonAra 通过考虑蛋白质的分子背景,为蛋白质设计提供了前所未有的灵活性和深度,这意味着它可以为绑定离子、底物、核酸、脂质、其他蛋白质等特定区域进行设计。
在我们的评估中,CARBonAra 的表现与诸如 ProteinMPNN 和 ESM-IF1 等最先进的方法相当,同时展示了类似的计算效率——都非常快速。该模型在设计蛋白质单体和蛋白质复合物方面实现了与 ProteinMPNN 和 ESM-IF1 相似的序列恢复率,但除此之外,它还能处理包含非蛋白质分子的蛋白质设计,这是其他方法无法处理的。
CARBonAra 的一个显著特点是其能够通过整合各种约束来定制序列以满足特定目标。例如,它可以优化序列相似性、最小化相似度或实现低序列相似性。此外,通过利用 CARBonAra 与分子动力学模拟的结构轨迹,我们观察到可以提高序列恢复率,特别是在以前的方法显示成功率较低的情况下。
要了解更多关于该方法,特别是 ML 架构的细节,请查看我们在 bioRxiv 上的预印本:
相关文献 [## 上下文感知的几何深度学习用于蛋白质序列设计
蛋白质设计和工程正在以空前的速度发展,利用深度学习的进步。目前…
一些与结构生物学中的人工智能相关的文章
[## 超过一年 AlphaFold 2 免费使用及其在生物学中引发的革命
自信地建模蛋白质结构,预测它们与其他生物分子的相互作用,甚至蛋白质…
medium.com [## 通过共识方法设计稳定蛋白质的网络应用程序,使用 JavaScript、ESMFold 创建…
结合现代技术和工具进行高效工作,创建一个实现最简单但如今最有效的应用程序…
javascript.plainenglish.io ## “ML-Everything”?平衡科学中机器学习方法的数量和质量
需要适当的验证和良好的数据集,客观且平衡,并且预测在现实中有用…
[towardsdatascience.com ## 巨大的蛋白质语言模型如何颠覆结构生物学
结构预测的准确性与 AlphaFold 相似,但速度提高了高达 60 倍——同时开发了新的人工智能方法…
[towardsdatascience.com
www.lucianoabriata.com 我写作和拍摄的内容涵盖了我广泛兴趣领域中的一切:自然、科学、技术、编程等
在这里给我小费或者成为 Medium 会员以获取所有它的故事(我将获得少量收入,你无需付费)。订阅以获取我的新故事通过电子邮件*。在我的服务页面上咨询有关小型工作。您可以 在这里联系我。*
什么是组合优化?
展示组合爆炸的真正威力以及它们如何影响简单问题。
·发表于 Towards Data Science ·阅读时间 5 分钟·2023 年 4 月 10 日
–
图片来源 Shubham Dhage 于 Unsplash
什么是组合优化?
想象一下你是一名数据科学家,为一家航空公司工作,你被要求找到一周内的最佳航班安排,以在燃料和空域等限制条件下最大化航班数量。
你会怎么做?
好吧,你可以尝试每一个可能的解决方案,这称为 蛮力搜索,但如果我告诉你我们要进行 500 次航班呢?你需要尝试的不同 组合 数量将达到 ~500! 的规模,这大约等于 1.22 x 10¹¹³⁴. 这个数字非常巨大,使得蛮力搜索变得 不可处理。
那么,你如何解决这个问题?
组合优化!
组合优化 处理从有限对象中寻找最优解的问题,而该问题通常是难以处理的。这听起来很像我们上面的问题!
实际上,组合优化在许多领域中都有应用:
-
物流和供应链
-
制造业
-
金融
-
医疗保健
这使得理解和开发处理这些类型问题的技能成为数据科学家必备的能力。
要了解更多关于组合优化方法的知识,这些方法主要是元启发式,你可以查看我以前的一些帖子:
使用模拟退火优化算法获得旅行商问题的最佳解决方案
towardsdatascience.com ## Tabu 搜索简单解释
对 Tabu 搜索优化算法的直观解释及其如何应用于旅行商问题…
towardsdatascience.com ## 爬山算法简单解释
最受欢迎的优化算法之一的直观解释
towardsdatascience.com
然而,在这篇文章中,我想真正探讨这些问题为何如此困难,并演示一个简单的问题如何在复杂性上‘爆炸’。
旧的最爱:旅行商问题
组合优化问题的经典例子是旅行商问题 (TSP)。这是一个非常简单的问题,提出了以下问题:
‘找到一组城市中最短的路线,访问每个城市一次并回到起点城市’
听起来很简单,对吧?在现实场景中,情况远非如此。
这是因为随着城市数量的增加,问题的可能解决方案数量会导致组合爆炸。TSP 的解决方案数量是:
作者在 LaTeX 中的方程。
其中 n 是城市的数量。
让我们用一些实际数字来真正展示这种‘爆炸’:
-
n = 4: 解决方案 = 3
-
n = 8: 解决方案 = 2,520
-
n = 16: 解决方案 = 1.0461395 x 10¹³
更值得注意的是,对于20个城市,通过暴力破解解决 TSP 需要大约 1,900 年!
展示组合爆炸
让我们真正深入探讨一些代码,以全面理解暴力算法的内部机制,并描绘组合爆炸。
以下是使用暴力搜索解决 TSP 的一个模板类:
GitHub Gist 作者。
现在让我们使用算法对一些虚拟‘假’城市数据进行测试,以确认它确实返回了最佳解决方案:
GitHub Gist 作者。
作者在 Python 中生成的图表。
作者在 Python 中生成的图表。
看起来不错!初始解决方案显然不是最优的,而最终的最佳解决方案在视觉上也显得是最好的。
我们现在将测量暴力算法的持续时间与城市数量的关系,以图形方式描绘组合爆炸:
GitHub Gist 作者。
作者在 Python 中生成的图表。
从上面的图表中,我们看到暴力搜索的持续时间在大约12个城市时急剧增加。用实际数字来讲,11个城市时花费了***0.5***分钟,但***12***个城市时花费了***36分钟!所以,仅仅增加一个城市,算法计算时间增加了72***倍!
这真正展示了这些组合优化问题的强大和难以解决性。
总结与进一步思考
在这篇文章中,我们看到简单问题如何在复杂性上“爆炸”。在处理大规模系统和网络的行业中,这是很常见的。优化这些大规模系统中的业务问题的过程被称为组合优化。这个领域的需求源于暴力搜索的难以处理性,它可能需要数千年才能找到相对搜索空间的最佳解决方案。我们通过描绘旅行商问题中发生的组合爆炸来展示这种现象。
本文中使用的完整代码可以在我的 GitHub 上找到:
## Medium-Articles/Optimisation/brute-force at main · egorhowell/Medium-Articles
你当前无法执行该操作。你在另一个标签或窗口中登录了。你在另一个标签或…
另一件事!
我有一个免费的通讯稿,Dishing the Data,在其中我每周分享成为更好数据科学家的技巧。没有“废话”或“点击诱饵”,只有来自实践数据科学家的纯粹可操作的见解。
## Dishing The Data | Egor Howell | Substack
如何成为更优秀的数据科学家。点击阅读由 Egor Howell 撰写的《Dishing The Data》,这是一个 Substack 出版物,内容包括…
与我联系!
参考文献与进一步阅读
-
优化算法。 Mykel J. Kochenderfer 和 Tim A. Wheeler。 2019。
-
组合优化:理论与算法 Bernhard Korte 和 Jens Vygen。 2018。
广义线性模型介绍
原文:
towardsdatascience.com/breaking-down-generalized-linear-models-d9212526e51d
扩展你的建模技能,超越线性回归
·发表于 Towards Data Science ·阅读时间 6 分钟·2023 年 7 月 10 日
–
图片来源:Roman Mager在Unsplash
背景
线性回归是我们在数据科学中学习的最常见算法。每个从业者都听说过并使用过它。然而,对于某些问题,它并不适用,我们需要对其进行‘推广’。这就是广义线性模型 (GLMs)的用武之地,它们为回归建模提供了更大的灵活性,并且是数据科学家必须了解的宝贵工具。
什么是 GLMs?
正如我们上面所说,GLMs‘推广’了普通线性回归,但我们真正是什么意思呢?
让我们考虑一个更简单的线性回归模型:
作者通过 LaTeX 生成的方程。
其中 β 是系数***, x*** 是解释变量,ε 是正态分布的误差。
假设我们想要模拟保险公司在一小时内接到的索赔电话数量。线性回归是否适合这个问题?
不!
原因如下:
-
线性回归假设误差服从正态分布,而正态分布可以取负值。然而,我们不能有负的索赔电话。
-
第二点是正态分布,因此线性回归是连续的。而索赔电话都是整数和离散的,我们不能有 1.1 个电话。
因此,线性回归模型不能正确处理这个确切的问题。然而,我们可以将回归模型推广到符合上述要求的概率分布。在这种情况下,就是 泊松分布(稍后将详细介绍)。
GLM 简单地提供了一个框架,说明我们如何将输入与目标分布的期望输出链接起来。它们帮助将许多回归模型统一在一个“数学伞”下。
关于泊松分布的补充视频。
理论框架
概述
GLM 的基础依赖于三个关键因素:
现在我们来逐一解释这些内容的含义。
线性预测器
这是最简单理解的一个。线性预测器,η,仅仅意味着我们有一个输入(解释变量/协变量)x 的线性和,乘以其对应的系数 β:
由作者在 LaTeX 中生成的方程。
链接函数
链接函数,g,实际上负责将线性预测器与目标分布的均值响应 μ 进行“链接”:
由作者在 LaTeX 中生成的方程。
指数族
概述
GLM 的一个要求是输出的目标分布需要是 指数族。 这个分布族包含了许多你可能听说过的著名分布,如 泊松分布、 二项分布、 伽马分布、 和 指数分布。
在 GLM 框架中,我们实际上使用 指数离散模型,这是指数族的进一步推广。
为了属于指数族,概率密度(PDF)或质量函数(PMF)需要重新因式分解并参数化成以下形式:
由作者用 LaTeX 生成的公式。
这种形式是为了统计上的方便选择的,但在本文中我们不需要过多关注为什么会这样。
注意有两个参数 θ,这是自然或 canonical 参数,将输入与输出相关联,以及 ϕ,这是离散参数。
另一个有趣的事实是,指数族中的分布都有 共轭先验。这使得它们对于 贝叶斯问题 非常有用。如果你想了解更多关于共轭先验的信息,请查看我的相关文章:
一种计算上有效的贝叶斯统计方法
towardsdatascience.com
规范链接函数
有一种叫做规范链接函数的东西,定义为:
由作者用 LaTeX 生成的公式。
因此,如果我们能够用 μ 描述 θ,那么我们已经推导出了目标分布的自然链接函数!
均值和方差
数学上可以证明,指数族的均值,E(Y),由以下公式给出:
由作者用 LaTeX 生成的公式。
同样,方差,Var(Y),由以下公式给出:
由作者用 LaTeX 生成的公式。
如果你想查看这一推导的证明,请参考以下 linked 书的第 29 页。一般来说,解决方法是对 θ 的对数似然函数取导数。
泊松回归示例
泊松分布
泊松分布是一个著名的离散概率分布,用于建模在已知均值发生率下,事件发生特定次数的概率。如果你想了解更多,请查看我之前的帖子:
对最著名概率分布之一的概述
towardsdatascience.com
其 PMF 由以下公式给出:
由作者在 LaTeX 中生成的方程。
其中:
-
e: 欧拉数(约 2.73)
-
x: 出现次数(≥ 0)
-
λ: 期望出现次数(≥ 0),这也是 GLM 表示法中的均值μ
指数形式
我们可以通过对两边取自然对数,将上述泊松 PMF 写成指数形式:
由作者在 LaTeX 中生成的方程。
然后,我们对两边进行欧拉数的指数运算:
由作者在 LaTeX 中生成的方程。
现在,泊松 PMF 已经是指数形式了!
通过将系数与上述方程和指数家族 PDF 对应,我们得出以下结果:
由作者在 LaTeX 中生成的方程。
因此,泊松分布的均值和方差为:
由作者在 LaTeX 中生成的方程。
这是泊松分布的一个已知结果,我们只是推导出了一种不同的方法!
泊松 GLM
泊松分布的标准链接函数为:
由作者在 LaTeX 中生成的方程。
因此,泊松回归方程为:
由作者在 LaTeX 中生成的方程。
我们可以验证这个方程的输出只能为正,因此符合预测保险公司接到的索赔电话数量的问题的要求。
然后你可以通过 最大似然估计 或 迭代加权最小二乘法 来求解估计量。
重点是什么?
你可能会想,为什么我刚刚带你经历了这么繁琐的数学。好吧,让我快速总结一下关键要点:
-
检查问题的要求和目标分布以避免不合理的结果至关重要。
-
GLM 提供了一种从数学原理出发的方法,帮助你将输入与特定问题的期望输出联系起来。
总结与进一步思考
标准的线性回归模型很强大,但不适用于所有类型的问题,比如输出是非负的情况。对于这些特定的问题,我们必须使用其他分布,如泊松分布,广义线性模型(GLMs)提供了一个框架来执行这个过程。它们通过从基本原理推导一个链接函数,使你能够将输入转换为期望的目标输出分布。GLMs 是一个强大的建模工具,大多数数据科学家应该至少了解它们,因为它们的多功能性。
还有其他内容!
我有一个免费的新闻通讯,数据分析分享,每周分享成为更好的数据科学家的实用技巧。没有“虚 fluff”或“点击诱饵”,只有来自实践数据科学家的纯粹可操作见解。
[## 数据分析分享 | Egor Howell | Substack
如何成为更好的数据科学家。点击阅读由 Egor Howell 编写的《数据分析分享》,这是一个 Substack 出版物,内容涵盖…
newsletter.egorhowell.com](https://newsletter.egorhowell.com/?source=post_page-----d9212526e51d--------------------------------)
与我联系!
参考资料与进一步阅读
-
一些进一步有趣的理论和推导:
www.cs.princeton.edu/courses/archive/fall11/cos597C/lectures/exponential-families.pdf
-
GLMs 的经典文献:
www.utstat.toronto.edu/~brunner/oldclass/2201s11/readings/glmbook.pdf
解构 YouTube 的推荐算法
原文:
towardsdatascience.com/breaking-down-youtubes-recommendation-algorithm-94aa3aa066c6
打开“窍门箱”,让现代推荐系统得以运作
·发表在Towards Data Science ·阅读时间 7 分钟·2023 年 4 月 17 日
–
(Logo 设计 Eyestetix Studio,背景设计 Dan Cristian Pădureț)
推荐系统已成为我们时代最普遍的工业机器学习应用之一,但关于它们在实践中如何运作的出版物却很少。
一个显著的例外是 Paul Covington 的论文“深度神经网络用于 YouTube 推荐”,其中充满了关于 YouTube 深度学习驱动的推荐算法的许多实际见解和学习,提供了一个罕见的视角,不仅揭示了现代工业推荐系统的内部工作,还揭示了今天的机器学习工程师正在尝试解决的问题。
如果你想深入了解现代推荐系统,为机器学习设计面试做准备,或者只是对 YouTube 如何吸引用户感到好奇,请继续阅读。在这篇文章中,我们将探讨论文中的 8 个关键见解,帮助解释 YouTube(以及任何现代推荐系统)的成功。
让我们开始吧。
1 — 推荐 = 候选生成 + 排序
YouTube 的推荐系统分为两个阶段:候选生成阶段,将数十亿的视频筛选到几百个,并且排序阶段,进一步缩小和排序最终展示给用户的候选视频。
从技术上讲,这两个阶段都包含一个双塔神经网络——一种具有两个分支分别处理用户 ID 和视频 ID 的特殊架构——但它们的训练目标不同:
-
对于候选生成模型,学习问题被表述为一个极端多类分类问题:在所有现有视频中,预测用户互动的视频。
-
对于排名模型,学习问题被表述为一个(加权的)逻辑回归问题:给定一个用户/视频对,预测用户是否与该视频互动。
这一设计选择的动机是将找到最佳内容的问题分解为召回优化和精度优化:候选生成优化召回,即确保我们捕捉到所有相关内容,而排名优化精度,即确保我们首先展示最佳内容。以这种方式分解问题是使推荐系统能够在数十亿用户和视频的规模上运行的关键。
YouTube 的两阶段推荐漏斗。来自 Covington 2016,YouTube 推荐的深度神经网络
2 — 隐式标签比显式标签效果更好
显式用户反馈,如点赞、分享或评论,非常稀少:在所有观看特定视频的用户中,只有少数人会留下显式反馈。因此,仅根据点赞训练的模型会留下大量信息未被利用。
隐式标签,如用户点击和观看时间,稍微有些噪声——用户可能会偶然点击——但数量级上要多得多。在 YouTube 的规模下,标签数量优于标签质量,因此在他们的模型中使用隐式反馈作为训练目标效果更好。
3 — 观看顺序很重要
特定用户的观看历史形成的序列不是随机的,而是包含具有不对称共同观看概率的特定模式。例如,在观看了两个来自同一创作者的视频之后,用户很可能会观看来自该创作者的另一个视频。一个简单地学习预测一个展示的视频是否会被观看的模型,忽略了用户最近的观看历史表现不好:同样,它也留下了信息未被利用。
相反,YouTube 的模型通过给定用户最新的观看(和搜索)历史来学习预测下一个观看的视频。从技术上讲,它通过将用户最新的 50 个观看视频和 50 个搜索查询作为特征输入到模型中来实现这一点。
4 — 排名模型使用加权逻辑回归进行训练
正面训练示例(点击的展示)根据其观察到的观看时间进行加权,而负面训练示例(没有点击的展示)则收到单位权重。这个加权方案的目的是减少点击诱饵内容的权重,增加导致更有意义和更长时间参与的内容的权重。
从数学上讲,这样的加权逻辑回归模型学到的赔率大致等于预期观看时间。在推理时,我们可以通过应用指数函数将预测的赔率转换为观看时间。能够预测观看时间可以得出下一个关键洞察:
5 — 通过预测观看时间排名优于通过点击率排名
这是因为点击率排名会促进低观看时间的点击诱饵内容:用户点击但很快返回。通过预测观看时间进行排名可以降低点击诱饵内容的排名,从而提供更具吸引力的推荐。
6 — 多样的特征集是高模型性能的关键
深度学习相对于线性或树模型的优势在于它可以处理多样的输入信号。YouTube 的模型会考虑:
-
观看历史:用户最近观看了哪些视频?
-
搜索历史:用户最近搜索了哪些关键词?
-
人口统计特征,例如用户性别、年龄、地理位置和设备,这些特征为“冷启动”用户,即没有历史记录的用户,提供了先验信息。
确实,特征多样性是实现高模型性能的关键:作者们展示了,使用所有这些特征训练的模型相较于仅使用观看历史训练的模型,持出 MAP 从 6%提高到 13%。
YouTube 的排名神经网络模型接受多样的特征集。从 Covington 2016,Deep Neural Networks for YouTube Recommendations
7 — 由于“示例年龄”特征,内容保持新鲜
ML 系统往往对过去有偏见,因为它们是在历史数据上训练的。对于 YouTube 来说,这是一个问题,因为用户通常更喜欢最近上传的“新鲜”内容,而不是很久以前上传的内容。
为了修复这种“过去偏见”,YouTube 将训练示例的年龄作为模型中的一个特征,并在推理时将其设置为 0,以反映模型在训练窗口的最后阶段进行预测。例如,如果训练数据包含 30 天的数据窗口,这一“示例年龄”特征会从 31(训练数据中的第一天)变化到 1(最后一天)。
作者们展示了引入这一特性使得模型更倾向于推荐新内容,这正是 YouTube 所希望的。
8 — 稀疏特征被编码为低维嵌入
YouTube 的排名模型使用了大量高维度(“稀疏”)的分类特征,例如
-
视频 ID 和用户 ID,
-
分词搜索查询,
-
用户最近观看的最后 50 个视频,或
-
启动当前观看会话的“种子”视频。
这些稀疏特征被独热编码,并映射到在模型训练过程中学习的 32 维嵌入中,然后作为嵌入表存储用于推理。
为了限制嵌入表所造成的内存占用,ID 空间被截断,只包括最常见的 ID。例如,如果一个视频在训练期间只被观看过一次,那么它不值得在嵌入表中占有一席之地,因此会被视为与从未观看过的视频相同。
另一个值得注意的技巧是,相同 ID 空间内的稀疏特征共享相同的基础嵌入。例如,存在一个全球性的、用于许多不同特征的单一视频 ID 嵌入,例如印象的视频 ID、用户最近观看的视频 ID 或当前会话的种子视频 ID。以这种方式共享嵌入有 3 个好处:
-
它节省了内存,因为需要存储的嵌入表更少,
-
它加快了模型训练,因为需要学习的参数更少,并且
-
它提高了泛化能力,因为它使模型能够获得有关每个 ID 的更多上下文。
附录:技巧包
总结来说,YouTube 的推荐系统没有真正让它特别的单一因素。它是一个“技巧包”,每个技巧解决一个特定的问题:
-
2 阶段漏斗设计解决了可扩展性问题,
-
使用加权逻辑回归和按预期观看时间排序解决了点击诱饵问题,
-
添加“示例年龄”特征解决了过去偏见问题。
-
添加人口统计特征解决了冷启动问题,
-
预测“下一次观看”(而不是随机观看)解决了不对称共同观看概率问题,
-
在具有相同 ID 的类别特征之间共享嵌入解决了有限内存问题,
等等。
这个技巧包不仅为现代推荐系统的内部运作提供了极大的洞察力,也展示了现代 ML 工程师的工作:我们解决问题以使模型更好。最优秀的 ML 工程师是那些随着时间积累了最佳技巧包的人。
最后,Covington 的论文现在已经好几年了,肯定有些技巧已经被更新更好的技巧所取代。有编码稀疏特征的新技巧,也有去偏排名模型的新技巧。这是工业 ML 应用程序固有的另一个方面:随着新突破的出现,我们的模型不断演变。
ML 工程师永远不会“完成”。
想扩展你的个人 ML “技巧包”?想深入了解现代工业 ML 应用背后的原理?查看我的电子书, 《地面上的机器学习:现实世界 ML 应用的设计与操作》。
用 ReLU 打破线性
原文:
towardsdatascience.com/breaking-linearity-with-relu-d2cfa7ebf264
解释 ReLU 激活函数如何以及为何是非线性的
·发表于 数据科学前沿 ·阅读时间 4 分钟·2023 年 3 月 1 日
–
图片由 Alina Grubnyak 提供,来源于 Unsplash
介绍
神经网络 和 深度学习 是人们转行进入数据科学的最受欢迎的原因之一。然而,这种兴奋可能会导致忽视神经网络的核心概念。在这篇文章中,我想讨论神经网络的一个关键特性,我认为大多数从业者应该了解,以充分理解其内部运作。
我们为什么需要激活函数?
激活函数 在数据科学和机器学习中无处不在。它们通常指的是应用于神经网络中神经元的 线性 输入的变换***:***
作者在 LaTeX 中的方程。
其中 f 是激活函数,y 是输出,b 是偏置,w_i 和 x_i 是 权重 和它们对应的特征值。
但是,我们为什么需要激活函数呢?
简单的答案是,它们使我们能够建模复杂的模式,且通过使神经网络变得 非线性 来实现。如果网络中没有非线性激活函数,那么整个模型就会变成一个 线性回归 模型!
非线性是指输入的变化与相应输出的变化不成比例。
例如,考虑一个前馈的两层神经网络,中间层有两个神经元(忽略偏置项):
作者用 LaTeX 写的方程。
我们已经成功将我们的两层网络简化为单层网络!上述推导中的最终方程仅仅是一个具有特征 x_1 和 x_2 及其对应系数的线性回归模型***。***
因此,我们的“深度神经网络”将会简化为单层,变成传统的线性回归模型!这不好,因为神经网络将无法对数据建模或拟合复杂函数。
线性函数的正式数学定义是:
作者用 LaTeX 写的方程。
这是一个非常简单的例子:
作者用 LaTeX 写的方程。
所以函数 f(x) = 10x 是线性的!
注意,如果我们在上面的方程中添加一个偏置项,它就不再是线性函数,而是一个 仿射函数。请参阅这个状态交换讨论讨论为什么会这样。
ReLU
整流线性单元 (ReLU) 是最流行的激活函数,因为它计算高效,并且解决了梯度消失问题。
数学上,该函数表达为:
作者用 LaTeX 写的方程。
我们可以用 Python 进行图形化展示:
由作者用 Python 生成的图。
为什么 ReLU 是非线性的?
ReLU 函数可能看起来是线性的,因为有两条直线。实际上,它是分段线性的。然而,正是这两条不同的直线使其成为非线性。
我们可以通过执行与上述相同的示例但使用 ReLU 函数来证明它是非线性的:
作者用 LaTeX 写的方程。
让我们分解一下:
作者用 LaTeX 写的方程。
因此,ReLU 是非线性的!
我已经在这里链接了一篇很好的文章,展示了如何使用 ReLU 创建任何函数。
总结和进一步的思考
非线性在神经网络中是至关重要的,因为它允许算法推断数据中的复杂模式。非线性是通过激活函数来实现的,其中最著名的是 ReLU,它在计算效率和解决训练神经网络时已知的问题方面表现优异。ReLU 函数是分段线性的,这就是它如上所述在数学上表现为非线性的原因。
完整的代码可以在我的 GitHub 上找到:
[## Medium-Articles/relu.py at main · egorhowell/Medium-Articles
你现在不能执行该操作。你在另一个标签页或窗口中登录了。你在另一个标签页或窗口中登出了……
github.com](https://github.com/egorhowell/Medium-Articles/blob/main/Data%20Science%20Basics/relu.py?source=post_page-----d2cfa7ebf264--------------------------------)
另一个事项!
我有一个免费的通讯,Dishing the Data,在其中我每周分享成为更好的数据科学家的技巧。没有“虚浮内容”或“点击诱饵”,只有来自实际数据科学家的纯粹可操作的见解。
[## Dishing The Data | Egor Howell | Substack
如何成为更好的数据科学家。点击阅读《Dishing The Data》,作者为 Egor Howell,这是一个 Substack 出版物……
与我联系!
利用这个 Python 库弥合数据与人类之间的差距
让你的 Python 输出更易于理解
·发布于 Towards Data Science ·4 分钟阅读·2023 年 2 月 22 日
–
介绍
我们无需依赖任何统计数据就能意识到,Python 是软件开发人员、数据科学家等最常用的编程语言之一。这不仅因为其灵活性和易用性,还因为有大量的库可以使我们的日常任务更加轻松。
本文介绍了另一个强大的库:humanize
。它通过使输出更易于理解,帮助弥合人类与 Python 输出之间的差距。让我们看看一些例子。
开始使用
为了使用humanize
,第一步是使用 Python 包管理器pip
进行安装,如下所示:
!pip3 install humanize
接下来,你需要导入以下相关库以成功完成教程。
-
getsize()
来自os
库,用于获取给定文件的大小。 -
datetime
用于处理时间。 -
最后,是本文的核心——
humanize
库。
from os.path import getsize
import datetime as dt
import humanize as h
一切准备就绪,开始探索大数字。
使大数字更具可读性
这个数字 1034503576643 是什么?
理解这个数字是否在十亿还是万亿范围内需要一些脑力。humanizer
通过提供更友好的输出,试图减轻这种负担。
一种方法是使用正确的逗号**','**
来分隔,方法是使用intcomma
函数,如下所示:
big_num = 1034503576643
human_big_num_coma = h.intcomma(big_num)
print(human_big_num_coma)
上述代码的输出是1,034,503,576,643,比没有分隔符的原始数字要好得多。
此外,结果可以使用intword
函数生成自然语言格式,如下所示:
human_big_num = h.intword(big_num)
print(human_big_num)
这会产生以下结果:1.0 trillion.
处理日期时间
2022 年 9 月 6 日(YYYY/MM/DD 格式)是 2022 年 9 月 6 日
第二种格式(Sep 6 2022)比第一种 YYYY/MM/DD 格式更容易被理解,因为它符合我们日常的口头交流。这种结果可以通过naturaldate
函数获得。
date = dt.date(2022, 9, 6)
human_date = h.naturaldate(date)
print(human_date)
这将生成以下结果:Sep 06 2022
。
可以使用naturalday
函数将结果限制为月份和日期,而不是使用naturaldate
。
human_day = h.naturalday(date)
print(human_day)
结果是Sep 06
处理持续时间
类似于 DateTime,也可以使用naturaltime
函数使持续时间具有可读性,如下所示。
# Get today's date
current_time = dt.datetime.now()
# Get the date of 3 days before
few_days_before = dt.timedelta(days=3, hours=23, minutes=40)
# Compute the difference of time
past_time = current_time - few_days_before
human_time = h.naturaltime(past_time)
print(human_time)
之前的代码生成了3 days ago
,这是任何人都可以理解的。
获取文件的大小和单位
我的文件大小是 278。
这个声明最明显的问题是
你使用的是什么单位?字节、千字节、兆字节、千兆字节、太字节?
这个谜题可以通过使用naturalsize
函数解决,如下所示:
-
首先,使用
getsize
函数获取CSV
文件的大小。 -
然后使用
naturalsize
函数生成更合适的输出。
fize_size = getsize("./candidates.csv")
# Before Humanize
print(fize_size)
# After Humanize
print(h.naturalsize(fize_size))
-
人性化处理前的结果是 278。
-
人性化处理后,我们得到了278 Bytes。
科学记数法和分数
给定数字的科学记数法在某些场景中可能更有用,例如使用power of the ten
记法。这可以通过使用scientific
函数来实现。
使用precision
参数,用户可以指定小数点后的精度值数量。如果未指定,精度值为 2。
下面是一个示例。
# Number to convert to scientific format
value = 2304355
# Without Precision
scientic_notation = h.scientific(value)
print(scientic_notation)
# With precision
scientic_notation = h.scientific(value, precision = 5)
print(scientic_notation)
输出结果按print
语句的顺序给出。
-
使用默认函数:2.30 x 10⁶
-
使用
precision
参数:2.30436 x 10⁶
你认为 0.4646 的分数表示是什么?
避免过多的数学计算,只需使用fractional
函数,如下所示:
float_value = 0.4646
# Get the fractional representation
fraction = h.fractional(float_value)
print(fraction)
答案是 105/226。这真的很酷,不是吗!
如果我处理的是另一种语言怎么办
之前的所有结果都是用英语给出的。其他语言如法语、俄语等也可以实现相同的效果。
实现这一点的第一步是使用i18n.activate
函数激活国际化(i18n
)功能。
例如,可以创建一个持续时间为 3 秒的时间差对象,但这次用法语
。
# Activate the French Language
_t = h.i18n.activate("fr")
# Generate the time delta
h.naturaltime(dt.timedelta(seconds=3))
结果是il y a 3 secondes
,这在英语中表示3 seconds ago
。
结论
感谢阅读! 🎉 🍾
希望你觉得这篇文章有帮助!
如果你喜欢阅读我的故事并希望支持我的写作,可以考虑成为 Medium 会员。通过每月$5 的承诺,你可以无限制访问 Medium 上的故事。
你想请我喝咖啡吗☕️?→ 在这里请我!
随时欢迎关注我的 Medium、Twitter 和 YouTube,或者在 LinkedIn 上打个招呼。讨论人工智能、机器学习、数据科学、自然语言处理和 MLOps 的内容总是很愉快的!
在你离开之前,请查看下面的该系列的最后两部分:
Pandas 和 Python 数据科学与数据分析技巧 — 第一部分
Pandas 和 Python 数据科学与数据分析技巧 — 第二部分
Pandas 和 Python 数据科学与数据分析技巧 — 第三部分
跨领域桥接:将金融、隐私和软件最佳实践融入机器学习风险管理
负责任的人工智能
理解超越传统模型风险管理的策略
·发表于 Towards Data Science ·阅读时间 10 分钟·2023 年 10 月 10 日
–
作者图像 | 图标:Flaticon(个人及商业用途免费)
“航空法则是以血的代价写成的。我们不应当用人工智能再现这种方法论” — Siméon Campos
在 2018 年,彭博社的报道“Zillow 的算法驱动的购买狂潮使其房屋翻转实验注定失败”引起了广泛关注。报道概述了 Zillow 大胆进入iBuying领域,寄希望于其基于机器学习的Zestimate算法来革新房屋翻转的盈利模式。尽管开始时结构安排得很周密,聘请了本地房地产专家来验证算法定价,Zillow 还是转向完全算法驱动的方法以追求更快的报价。然而,这一举动并没有取得预期的效果。
照片由 Tierra Mallorca 提供,来源于 Unsplash
Zestimate 试图适应 2021 年房地产市场的快速通货膨胀,这促使 Zillow 采取行动以增强其报价的吸引力。该公司开始了一场雄心勃勃的购房狂潮,报告称每季度收购多达 10,000 套房产。然而,人力资源难以跟上这些收购的庞大规模和速度,这一挑战因疫情的同时爆发而加剧。在面临不断增加的困难,包括未售出的房产积压时,Zillow 决定在 2021 年 10 月暂停其报价。随后几个月,房产以亏损价被转售,导致超过 5 亿美元的大规模库存减记。
除了其失败的投资所造成的巨大财务损失,Zillow 宣布将裁员约 2,000 名员工——公司的四分之一。
我们从一个相当不幸的事件开始讨论,因为 Zillow 的 iBuying 风险投资的崩溃嵌入了一个复杂的原因框架中。尽管无法将这一事件与 2020 年全球大流行从住房市场中剥离开来,但它确实为深入分析铺平了道路。在本文中,我们将以此为例,揭示我们系列中讨论的治理和风险管理原则如何可能避免未来类似的不幸事件。
在继续阅读之前
在继续之前,请注意这是我们 AI 风险管理系列的第三篇文章。建议阅读前两篇文章,以便全面理解。
• 第一篇文章展开了 机器学习风险管理的文化能力,探讨了在这一复杂领域中所需的人文维度。
/ ## 机器学习风险管理的文化能力
组织的文化是负责任 AI 的一个重要方面。
[towardsdatascience.com
• 第二篇文章将重点转向 ML 系统背景下的另一个重要元素:组织流程。与我们一起踏上这段启发之旅,以更好地掌握 AI 和风险管理的交织领域。
/ ## 机器学习风险管理的组织流程
组织流程是 ML 系统可靠性的一个关键非技术性决定因素。
[towardsdatascience.com
超越模型风险管理
在上一篇文章中,我们详细讨论了**机器学习风险管理(MRM)**如何构成一个全面的框架,以及一系列旨在识别、评估、缓解和监控与机器学习系统的开发、部署和操作相关的风险的程序。在这一部分,我们将探索超越传统模型风险管理的各种策略和实践,这些策略和实践在 ML 安全方面表现尤为出色。我们将从讨论 AI 事件响应开始。
📝AI 事件响应计划
Zillow 事件突显了 AI 失败的一个事件,展示了一个精心设计的算法如何无法跟上快速变化的房地产市场,导致重大财务和声誉损失。尽管经过最佳的模型训练和验证测试,但即便在SR-11 指南中也提到,消除模型风险是不可能的,这突显了制定可靠事件响应计划的紧迫性。
AI 事件计划是一个预先制定的策略,旨在快速有效地解决 AI 问题,帮助组织迅速识别、遏制和消除 AI 事件,并防止代价高昂或危险的情况,这对较小或新兴的组织尤其重要。这是计算机安全中的一项受认可的做法,像NIST和SANS等组织强调了它在管理机器学习和人工智能复杂性方面的重要性。与计算机事件响应一样,AI 事件计划分为六个明确的阶段,每个阶段都对降低 AI 风险至关重要。
人工智能事件计划的六个阶段 | 图片来源:作者
- 阶段 1:准备
为有效准备 AI 事件,组织应定义事件参数,分配响应预算,制定沟通计划,并实施技术保障措施。通过与关键人员进行桌面演练来模拟场景可以提高准备程度。
AI 事件准备阶段的起始问题 | 图片来源:作者
- 阶段 2:识别
识别涉及检测系统故障、攻击或滥用。它结合了一般安全方法和专门的 AI 监控,如检测概念漂移或算法歧视。一旦发现问题,将会提醒相关利益相关者,包括管理层。
- 阶段 3:遏制
控制指的是减轻事件造成的直接危害,目标是减少初始损害。事件可能会有蔓延的趋势,影响业务和客户的其他方面。解决这些问题的方法可能会根据其根本原因而有所不同,无论是外部攻击、内部错误还是 AI 系统的误用。在必要时,建议在控制阶段与公众进行沟通。
- 第四阶段:根除
根除意味着修复受影响的系统以停止问题。这可能通过阻止被攻击的系统以防止进一步的损害,或关闭故障的 AI 系统并暂时使用一个可信的、更简单的系统来实现。根除之后,事件不应再造成任何进一步的伤害。
- 第五阶段:恢复
恢复过程涉及修复受影响的系统、预防未来的问题,并且可能需要审查或改进技术程序,尤其是当问题是由于错误或恶意行为引起时。
- 第六阶段:经验教训
经验教训意味着根据在当前问题中有效和无效的措施,对我们对 AI 事件的响应进行更改或改进。这些改进可以涉及过程或使用的技术。
从 Zillow iBuying 事件中获得的经验:AI 事件响应的见解
在审查我们的 AI 事件响应计划后,让我们回到 Zillow iBuying 的事件中。我们能从这一章节中关于 Zillow iBuying 情况中获得什么见解呢?根据对此话题的公开报告,显然存在潜在的警示信号🚩。这些包括 Zillow 缺乏人工监督、对财务风险的评估不足,以及缺乏适当的治理结构。虽然关于 Zillow 的具体答案仍不确定,但这突显了从这一案例中提取有价值的经验以提高我们对 AI 相关挑战的准备和响应的重要性,包括:
-
经验教训 1:与领域专家验证。
-
经验教训 2:预见失败模式。
-
经验教训 3:治理至关重要。
-
经验教训 4:AI 事件可能会迅速扩大。
-
经验教训 5:新兴技术总是伴随风险。
提升风险管理的额外实践
除了上述讨论的 AI 事件响应,来自财务审计、数据隐私、软件开发最佳实践和 IT 安全的实践对这一领域也带来了重要价值。
🔍模型审计和评估
模型审计是针对机器学习系统的正式评估过程,确保符合特定政策、法规或法律。这些正式评估通常由独立第三方进行,重点关注透明性和全面测试。模型评估类似但更加非正式,可能由内部或外部团队进行,检查各种问题,如偏见、安全、数据隐私危害和安全漏洞。
想深入了解模型审计和评估,可以参考两篇论文:算法偏见和风险评估:实践经验的教训 和 弥合人工智能问责制缺口:定义内部算法审计的端到端框架,这些论文提供了进行这些审计和评估的宝贵见解和框架。
📈影响评估
影响评估在机器学习政策和拟议法律中越来越受到重视,用于预测和记录系统可能面临的挑战。这些评估使得人工智能设计师和操作员更容易理解和对其系统可能造成的问题负责。然而,这只是一个初步步骤。它们应定期进行,并与其他因素一起考虑,以全面了解风险。由非机器学习团队成员进行评估尤为重要,以避免任何偏见并确保彻底检查。
虽然影响评估在风险管理和治理策略中发挥了关键作用,但由独立专业人员执行以及与其他风险因素结合至关重要,以确保整体效能。
⚖️上诉、覆盖和选择退出
你是否见过 Google 搜索栏中的 举报不当预测 功能?这是用户指出问题的基本方式。这个功能允许用户对机器学习系统的决策进行挑战或更正。这一想法,也被称为可操作的补救或救济,可能有所不同。另一种方法是 选择退出 选项,允许用户跳过自动处理。这两个选项被许多数据隐私和美国消费者金融法律认可,对于保护消费者权益免受自动化机器学习错误至关重要。然而,由于需要规划和资源来从一开始就集成这些功能,许多机器学习系统仍然缺乏这些功能。
通过 Google 举报不当预测 | 图片作者
👩💻👨💻对编程和双重编程
机器学习算法可能复杂且不可预测,使得确保它们正常工作变得困难。一些顶级机器学习组织使用两种主要方法来进行双重检查:
-
对编程
— 两名专家分别编写相同的算法。
— 然后,他们合作解决任何差异,确保两个版本的工作方式相同。
有趣的是,大型语言模型(LLMs)现在正在被纳入对编程的配对编程中。最近的一门课程标题为与大型语言模型的配对编程深入探讨了与 LLMs 在实时编码场景中协作的细节。
-
双重编程
— 一个人用不同的编程语言两次编写相同的算法。
— 然后,他们比较并调和两个版本之间的任何差异。
两种方法都有助于早期发现和修复漏洞,确保算法在实际应用前是可靠的。
🔒模型部署的安全权限
IT 安全中有一个概念叫做最小权限,它强调系统用户不应拥有过多权限。尽管其重要性,但在机器学习系统中常常被忽视,这可能导致安全和性能问题。公认的做法是,让产品经理或高管等不同角色做出软件发布的最终决定,以避免偏见并确保全面评估。
最小权限原则通过 Intel x86| 由 Hertzsprung 于英语维基百科,CC BY-SA 3.0 演示。
在开发冲刺期间,数据科学家和工程师必须对他们的环境拥有完全的控制权。然而,随着重要发布或审查的临近,IT 权限应转移给组织内的其他角色。此控制权的转移作为一个检查点,确保未经批准或有缺陷的代码不会被部署,从而增强系统的安全性和可靠性。
💰漏洞奖励
漏洞奖励是组织提供给发现其软件问题的人,包括机器学习系统的奖励。它们不仅仅用于发现安全问题,还包括与安全性、隐私和可靠性等相关的问题。
通过提供金钱或其他奖励,组织鼓励人们反馈意见并发现他们机器学习系统中的问题,从而使这些系统更加可靠和安全。如果组织担心公开其漏洞奖励,他们可以举办内部活动,让不同团队在机器学习系统中寻找问题。关键是提供良好的激励措施以获得最佳结果。
通过漏洞奖励,我们使用货币奖励来激励社区反馈,形成标准化的过程。
许多公司已经推出了漏洞奖励计划,以检测和修复其系统中的漏洞。以下是一些例子:
- 在 2021 年,Twitter(现在称为X)宣布了首个 算法偏见悬赏挑战,以探索其图像裁剪算法中的潜在偏见。该算法使用了一种名为显著性图的 XAI 技术来确定用户上传图像中最吸引人的部分。
图像裁剪算法 由 Twitter(现在称为 X) 用于展示时间线上的图像。该算法后来被弃用| 作者图片
一些用户观察到基于机器学习的图像裁剪工具似乎偏向白人图像,并且不成比例地放大了女性的胸部和腿部等区域,这暗示了男性凝视的偏见。此外,当这些问题被指出时,用户没有机制来修改自动裁剪。挑战的目的是识别这样的算法可能带来的潜在危害。
## 你看到的就是你会得到的 — Twitter 在时间线中展示图像的新策略
总结 Twitter 关于其图像裁剪算法的论文
towardsdatascience.com
2. 在 2023 年 4 月 11 日,OpenAI 宣布了一个漏洞悬赏计划,邀请安全研究社区参与。发现问题的奖励从 $200(低严重性问题)到最高 $20,000(特殊发现)。
3. Meta 在其平台上有运行漏洞悬赏计划的历史。然而,当他们在 2023 年 2 月推出LLaMA 2——他们的开源大语言模型时,他们也发布了一个责任使用指南。该指南包括 报告漏洞和安全问题的选项。
结论
这篇文章强调了在负责任的人工智能开发中,治理、事件响应和专家验证的重要性。当我们深入探讨超越传统模型风险管理的策略,包括人工智能事件响应计划以及借鉴金融审计、数据隐私、软件开发和 IT 安全的做法时,显然,一个多方面的方法对于以负责任和安全的方式应对人工智能不断变化的挑战至关重要。从 Zillow 的经验中得到的教训提醒我们需要在人工智能中实施强有力的风险管理,这将引导我们在未来创建更可靠和更具伦理的人工智能系统。
参考资料
对应分析简要介绍
原文:
towardsdatascience.com/brief-introduction-to-correspondence-analysis-a88297ebba2a
学习如何在 R 中运行多重对应分析的基本步骤
·发布在Towards Data Science ·阅读时间 7 分钟·2023 年 1 月 16 日
–
图片由John Barkiple拍摄,来源于Unsplash
介绍
数据集由数字和/或文本组成。因此,我们应该预期,并非所有变量都仅涉及数字,数字变量有许多技术可以进行分析、测试和处理。
当我们处理数值变量时,有像相关性、PCA、缩放、归一化以及一系列测试等工具。另一方面,如果我们处理文本,更具体地说,是类别,我们应该寻找其他技术来应用于我们的数据分析。
其中一个工具是对应分析[CA]。
对应分析是一种统计技术,可以显示基于给定的列联表数据,两个变量内类别之间的关系。
正如定义所示,它是一种统计工具。概念上,它类似于主成分分析[PCA],但应用于分类数据,因为它使我们能够以 2D 图形展示数据集,显示哪些类别对应(或相关)于什么。
对于数据科学家来说,CA 可以在许多方面发挥作用,例如了解不同类型的客户如何购买一组产品、每个年龄段偏好的电影类型,或者本教程的示例:哪些产品在注册 1 和注册 2 中被购买。
导入和创建一些数据
我们将从导入必要的库和创建一些数据开始。
# Imports
library(tidyverse)
library(ggrepel)
library(sjPlot) #contigency tables
library(FactoMineR) #CA functions
library(ade4) # Create CA
创建数据。
# Dataset
df <- data.frame(
trans_id = 1:30,
register = as.factor(c('rgs1', 'rgs1', 'rgs1', 'rgs2', 'rgs1', 'rgs2',
'rgs1', 'rgs2', 'rgs1', 'rgs2', 'rgs1', 'rgs2',
'rgs1', 'rgs2', 'rgs1', 'rgs2', 'rgs1', 'rgs2',
'rgs1', 'rgs2', 'rgs1', 'rgs2', 'rgs1', 'rgs2',
'rgs1', 'rgs2', 'rgs1', 'rgs2', 'rgs1', 'rgs2')),
product1 = as.factor(c('banana', 'banana', 'pasta', 'milk', 'yogurt',
'milk', 'pasta', 'milk', 'pasta', 'milk', 'banana',
'milk', 'banana', 'banana', 'pasta', 'bread', 'bread',
'milk', 'yogurt', 'bread', 'banana', 'pasta', 'yogurt','milk',
'yogurt', 'bread', 'bread', 'pasta', 'milk', 'banana')),
product2 = as.factor(c('strawberries', 'strawberries', 'sauce', 'bread', 'water',
'bread', 'sauce', 'bread', 'sauce', 'bread', 'strawberries',
'bread', 'strawberries', 'bread', 'water', 'bread', 'water',
'bread', 'bread', 'yogurt','strawberries', 'sauce',
'strawberries', 'bread', 'strawberries', 'milk', 'bread',
'sauce', 'bread', 'strawberries'))
)
这是数据的一个样本。所以我们有注册号和每次交易的一对产品。
创建的数据集样本。图片由作者提供。
统计数据
执行 CA 的第一步是进行统计检验。由于我们正在处理多个变量对,因此我们必须为每对变量执行卡方检验,所有的检验结果都必须在至少一对变量上具有统计显著性。例如,product1 必须与 product2 或 register 之一通过检验。
要执行的测试是一个假设检验,其中:
Ho (p-value > 0.05) 意味着变量之间没有关联。
Ha (p-value ≤ 0.05) 意味着变量之间存在关联。
一种快速测试变量对的方法是使用 for 循环。
for (var1 in 2:4){
for (var2 in 4:2) {
contingency <- table(df[,var1], df[, var2])
chi2 <- chisq.test(contingency)
writeLines( paste("p-Value for",
colnames(df)[var1], "and", colnames(df)[var2],
chi2$p.value))
}
}
p-Value for register and product2 0.0271823155904414
p-Value for register and product1 0.0318997966416755
p-Value for register and register 3.2139733725587e-07
p-Value for product1 and product2 9.51614574849618e-06
p-Value for product1 and product1 5.49284039685425e-18
p-Value for product1 and register 0.0318997966416755
p-Value for product2 and product2 8.43312760405718e-20
p-Value for product2 and product1 9.51614574849618e-06
p-Value for product2 and register 0.0271823155904414
结果显示所有的卡方检验都低于 p-Value < 0.05 的阈值,因此我们可以拒绝原假设,支持替代假设,并理解变量之间存在统计学上显著的关联。
另一种选择是使用 sjPlot 库中的 stj.xtab()
函数。
# Register x product1
sjt.xtab(var.row = df$register,
var.col = df$product1,
show.exp = TRUE,
show.row.prc = TRUE,
show.col.prc = TRUE)
它展示了一个格式良好的表格,已经包含了观察值、用绿色表示的期望值,以及每个类别的百分比、p 值和卡方统计量。
来自 stj.xtab() 函数的结果。图片由作者提供。
多重对应分析
现在是创建我们的多重对应分析(MCA)的时间。我们可以使用 ade4 库中的 dudi.acm()
函数。scannf= FALSE
参数只是为了防止显示特征值条形图。
# Creating the Multiple Correspondence Analysis
ACM <- dudi.acm(df[,2:4],
scannf = FALSE)
一旦运行这个,输出将是 R 中的一个包含 12 个对象的 List
。例如,如果我们运行 ACM$co
,我们将看到为计算出的两个主成分的每个类别的坐标。这意味着 X 和 Y 坐标,或者每个点在二维图形上的位置。
ACM$co
Comp1 Comp2
register.rgs1 0.7660067 0.05610284
register.rgs2 -0.8754363 -0.06411753
product1.banana 0.8060812 0.99732829
product1.bread -0.6784873 -0.03550393
product1.milk -1.2068148 0.23776669
product1.pasta 0.6008691 -1.82914217
product1.yogurt 0.9497931 0.56723529
product2.bread -0.9315168 0.26905544
product2.milk -1.1707641 -0.10768356
product2.sauce 0.5351564 -1.96850658
product2.strawberries 1.0569306 1.00621404
product2.water 0.7961676 -0.40682587
product2.yogurt -1.1707641 -0.10768356
如果我们运行 ACM$cw
,可以看到数据集中每个类别的百分比。
ACM$cw
register.rgs1 register.rgs2 product1.banana product1.bread product1.milk
0.17777778 0.15555556 0.07777778 0.05555556 0.08888889
product1.pasta product1.yogurt product2.bread product2.milk product2.sauce
0.06666667 0.04444444 0.13333333 0.01111111 0.05555556
product2.strawberries product2.water product2.yogurt
0.08888889 0.03333333 0.01111111
在 MCA 中,我们将能够提取 n = num_categories — n_variables
维度。在这个练习中,我们有 3 个变量(register1, register2
,product1
和 product2
)和 13 个类别(banana, bread, milk, yogurt, sauce, water, strawberries, pasta, sauce
,一些类别在 product 1 和 2 中重复)。因此,13–3 = 10
维度。
因此,我们可以使用ACM$eig
查看每个类别的 10 个 特征值。这些值表示每个类别所捕获的方差量,以一种简单的方式表示。
ACM$eig
[1] 0.77575767 0.64171051 0.54102510 0.44643851 0.33333333 0.25656245 0.15516469 0.10465009 0.05690406 0.02178693
# Variance from each dimension
perc_variance <- (ACM$eig / sum(ACM$eig)) * 100
[1] 23.272730 19.251315 16.230753 13.393155 10.000000 7.696873 4.654941 3.139503 1.707122 0.653608
创建感知图
最后一步是创建感知图,我们将在图形上看到类别的绘制。为此,我们必须创建一个基础数据框来保存类别名称及其相应的 X 和 Y 坐标。首先,让我们检查每个变量包含多少个类别。
# How many categories by variable
qty_categories <- apply( df[,2:4], 2, function(x) nlevels(as.factor(x)) )
register product1 product2
2 5 6
很好。现在我们将创建一个 data.frame
对象,获取坐标 ACM$co
,该对象将加载类别名称及 X 和 Y 坐标,并且包含一个 Variable
列,列出变量的名称(product1 或 product2)以便标记。
# Create the df with coordinates
df_ACM <- data.frame(ACM$co,
Variable = rep(names(qty_categories),
qty_categories) )
X 和 Y 坐标。图片由作者提供。
从这里开始,现在只需要使用 ggplot2 创建图表即可。
我们将从 df_ACM
对象开始,提取行名并将其创建为一列(rownames_to_column()
),然后将该列重命名为 Category
。接下来,我们将名称如 product1.banana
变更为仅 banana
。然后,我们将这个新数据框与 ggplot 函数进行管道操作,提供 x=Comp1
和 y=Comp2
,标签为 Category
,每个变量有不同的颜色。将创建一个散点图(geom_point
),并使用 geom_label_repel
使名称不覆盖点。vline
和 hline
用于创建 0 的参考线。
# Plotting the perceptual map
df_ACM %>%
rownames_to_column() %>%
rename(Category = 1) %>%
mutate(Category = gsub("register.","", Category),
Category = gsub("product1.","", Category),
Category = gsub("product2.","", Category)) %>%
ggplot(aes(x = Comp1, y = Comp2, label = Category, color = Variable)) +
geom_point() +
geom_label_repel() +
geom_vline(aes(xintercept = 0), linetype = "longdash", color = "grey48") +
geom_hline(aes(yintercept = 0), linetype = "longdash", color = "grey48") +
labs(x = paste("Dimensão 1:", paste0(round(perc_variancia[1], 2), "%")),
y = paste("Dimensão 2:", paste0(round(perc_variancia[2], 2), "%"))) +
theme_bw()
这是结果。
MCA 的感知图。图片由作者提供。
结果给我们一些有趣的见解:
-
Register 1 收到更多的水果,如草莓和香蕉,一些水,少量意大利面和酱料。
-
Register 2 处理了更多的面包和牛奶或酸奶交易。
-
请注意,意大利面和酱料在两个收银台的距离较远。这是因为在 register1 上有 4 笔交易,而在 register2 上有 2 笔。其他组合要么由 rgs1 处理,要么由 rgs2 处理。
在你离开之前
MCA 是一个强大的工具。如果你处理的是分类数据,应该查阅并尽可能使用它来进行良好的分析。然而,请记住,随着变量和类别数量的增加,它的应用会变得更困难。
例如,想象一个包含 30 个变量、每个变量有 5 个类别的数据集!这需要进行大量测试和分析。在这种情况下,其他技术可能更好,或者对数据进行一些变换以减少类别数量,或创建一个子集进行分析。
如果你喜欢这些内容,请关注我的博客以获取更多信息。
在 Medium 上阅读 Gustavo Santos 的文章。数据科学家。我从数据中提取见解,以帮助个人和公司……
参考
在线性代数中,特征向量(Eigenvector)或特征向量是线性变换的一个非零向量……
对应分析(CA)是一种由赫尔曼·奥托·哈特利(Hirschfeld)提出的多变量统计技术……
en.wikipedia.org [## ade4 包 - RDocumentation
多变量数据分析工具。提供了几种方法用于分析(即排序)单表数据……
将正确性带回机器学习
原文:
towardsdatascience.com/bring-correctness-back-to-machine-learning-a56a96262f17
我们是否在错误的假设上构建我们的领域?
·发布于 Towards Data Science ·9 分钟阅读·2023 年 10 月 13 日
–
由 Andrea De Santis 提供的照片,来自 Unsplash
介绍:什么是正确性?
研究论文仍然是传达机器学习领域新发现的主要方式。然而,论文的结果不能被复现的情况相当频繁,而原因却不清楚。
在这里,我想提出我对研究论文作为沟通工具的利弊的看法。我将介绍我对科学及其在发展集体人类知识中的作用的看法。
最近的一篇论文发现了研究出版过程中的漏洞。我觉得这篇论文非常有说服力,我将向你展示其主要主张,以提高对代码正确性在传播机器学习知识中作用的认识。
这个问题不仅限于机器学习,但在这个领域,许多研究人员缺乏强大的工程技能——并且通常试图逃避工程工作——这导致了可靠、正确、可用、有效的软件的问题。
你是否从事机器学习工作,并阅读研究论文寻找新想法?那么,这篇文章将帮助你以原则性的方法对你阅读的内容进行更批判的分析。
你是研究人员和研究论文的作者吗?希望你会对这个话题感兴趣,阅读引用的论文,并参与讨论。
现在,足够的介绍,让我们深入讨论吧!
科学即知识
根据所有可以衡量的指标,科学作为职业比以往任何时候都更受欢迎。这些指标包括科学家(和博士)的数量、可用资金、资助申请等。所有科学家中增长的一部分是机器学习领域的研究人员,理论的和应用的都有。
科学家的工作是什么?就是在某一研究领域发现新知识,从而扩展人类已知的知识范围,通过扩展或推翻现有知识。
在许多情况下,科学家在现有知识的基础上进一步推进。有时,先前的科学证据可能被证明是错误的或具有误导性的。例如,选择的样本可能不能代表整个群体,因此结果可能不具有普遍性。
另一个原因可能是研究是在特定条件下进行的,而其结果被推广到不同的条件。例如,在机器学习中,一个方法可能在训练集人为缩小的情况下表现优于当前最先进的方法…但在正常的数据条件下,它的表现不如基线。
传播科学发现的主要工具,虽然被一些人认为过时,但毫无疑问还是经过同行评审的研究论文。通过研究论文,科学家们以结构化和有组织的方式描述他们的发现。他们描述发现的领域、他们解决的问题、知识领域中的漏洞、他们的假设以及旨在为假设提供证据或证伪的实验。
根据描述,其他科学家决定论文是否值得信赖以及发现是否值得发表,这一过程称为同行评审。需要注意的是,这是一个棘手且不完美的系统,同行评审常常未能发表值得的研究,或者相反,允许发表质量较差的论文。没有任何系统是完美的,这也是游戏的一部分。这个游戏是科学传播的主要过程,通常决定了研究资金的未来和研究人员的职业生涯。
现在,可以提出一些关于过程基本原理的问题:“我们如何相信论文中的声明?”,“一篇论文是否足以将一项知识视为已获得?”,“如果一篇新论文与之前发表的论文结果相矛盾怎么办?”
为了回答这些问题,我们需要介绍一些概念。
可重复性、可靠性、正确性
机器学习会议越来越重视可重复性的问题,他们这样做完全正确。其他科学领域,特别是心理学和医学,在经历了著名的“复制危机”之后,遭遇了严重的可信度问题:论文结果无法被不同的独立研究者复制,这对该领域的整个工作产生了大量质疑。然而,这个问题远远超出了这两个提到的领域。
图片来源:Julia Koblitz 在 Unsplash
虽然极为重要,但可重复性只是产生新科学知识过程的一部分。机器学习会议通常要求审稿人也评估实验的“严谨性”。
让我们尝试用更明确的术语来定义它们:
可重复性是指通过遵循论文中解释的协议,另一组能够重复一项科学研究的可能性。这意味着论文中描述了所有相关细节,也许所使用的软件已发布(我们在这里讨论的是机器学习),并且可以获取相同的训练数据。
如果独立研究者遵循所描述的方法论,但他们的结果与论文中描述的结果有显著差异,则该研究是不可重复的。
严谨性是对协议正确性的判断。实验是否与要证明的假设一致?结果是否确实显示了作者所声称的内容?实验是否存在偏差或不完整性?
实际上,严谨性分数反映了从科学角度看研究的技术正确性。
这两个方面在机器学习会议中广泛讨论,但还有第三个方面虽然讨论较少但仍然重要。
代码正确性是一个是/否问题:代码是否实际实现了论文中描述的方法论?
代码正确性通常被认为是理所当然的,因此不会被检查或强制执行。
建立正确的知识
可重复性对建立知识至关重要。让我再重复一遍:无法被独立复制的结果不代表科学知识。它们不一定是由于欺诈(尽管有时确实如此),但可能是因为一些被作者认为是“次要”的细节没有描述,而这些细节在所提议的方法中或比提议的方法更为重要,以获得声称的结果。
另一方面,正确性是建立正确知识的基础。如果我们阅读一篇论文并发现有趣的结果,然后尝试复制这些结果并获得完全相同的结果,我们可以对自己新获得的领域知识感到满意。
如果参考实现隐藏了一些“bug”,并且在某些基本方面没有遵循所描述的思想,会怎么样呢?
图片由 Dmitry Bukhantsov 提供,来源于 Unsplash
在这种情况下,我们是在错误的事实基础上建立知识。积累许多错误的事实,我们就不知道什么是重要的,什么是不重要的。这对我们这些想要在某个领域成为“专家”的人来说,确实很可怕,不是吗?
这个问题在论文 When Good and Reproducible Results are a Giant with Feet of Clay: The Importance of Software Quality in NLP 中由 Papi 和同事们 [1] 讨论,其中以一些广泛使用的 Conformer 实现 [2] 为例。这些实现通常在常见的开源框架中加速了许多组的研究,这些组不需要自己实现 Conformer 和训练方法。问题是,所有被检验的实现中都包含三类 bugs,当推断批量大小大于一时,这些 bugs 会影响结果。注意,在正常情况下,推断批量大小应影响推断过程中的资源使用,从而影响推断速度,但不应影响输出。
关于这些 bug 的详细信息,我强烈推荐阅读这篇论文。在这里,我只是想强调,差异通常足够小,以至于不会注意到实现中存在问题,这也可能是它们被忽视了这么久的原因。然而,在一种情况下,当批量大小非常大时,降级是巨大的。
有些人可能会疑惑为什么我们要关心 BLEU 或 WER 的小数点后几位,但我认为这种观点忽视了问题的关键。本文仅以流行的 Conformer 实现为例,这些实现被全球数百或数千名研究人员和实践者使用。然而,它发现了所有这些实现中的 bug,其中一个在保持高效实现的同时并不容易解决。
记住,“bug”只是软件行业对软件错误的友好称呼。一个可爱的“bug”总是软件需求与实际行为之间的差异。
想象一下,每周提出的数百个深度学习网络的所有私人实现会发生什么。在许多情况下,这些网络由小组或单个开发者开发,没有人审查他们的代码。实现与论文中的描述是否匹配,留给作者自己判断,也不作为科学同行评审讨论的重点。
那么问题是:在这些假设下,我们如何可靠地信任我们阅读的论文中的声明?
不幸的是,这个问题没有明确的答案。[1]的作者提出了一些受软件开发如何解决正确性问题启发的解决方案。我认为这朝着正确的方向发展,但不幸的是,太多人过于看重感知到的额外工作(而不是错误软件的成本),我不认为这种方法会很快被广泛采纳。
结论
良好的科学话题无疑是一个艰难的领域。研究工作涉及许多不同的方面,研究人员最不希望的就是为了同样数量的出版物而增加更多的工作。
然而,当我们依赖不正确的软件时,我们正在构建错误的知识,并可能基于这些错误的知识走上错误的方向。试想一下,如果你使用一个你认为可靠的软件工具工作了一年或两年,然后有人发现了一个 bug,当它被修复时,你的新模型的性能突然下降。我认为没有人愿意陷入这种情况。我们从最佳工程实践中知道,错误应该在早期发现,以减少其成本。
像论文中提出的那种机器学习测试工具可以帮助研究人员以简单的方式生成更正确的软件,我真的很期待它的发布。如果是开源的,整个社区都可以贡献力量,整体提升编码标准。
你怎么看?你有什么解决方案来确保 ML 研究的正确性吗?在评论中告诉我!
如果你读到这里,非常感谢你的时间!我知道你的时间有限且宝贵,但你仍然决定花时间阅读我的想法,非常感谢!
更多我的文章
## 阅读和撰写 ML 研究论文的技巧 ## Tips for Reading and Writing an ML Research Paper
从数十次同行评审中获得的经验教训
## 无需多言:自动化开发环境和构建 ## Without Further Ado: Automate Dev Environments and Build
通过环境和构建自动化,使您的软件易于使用,从而给您的同事开发者带来快乐。通过…
## 3 个常见的 bug 来源及如何避免 ## 3 Common Bug Sources and How to Avoid Them
一些编码模式更容易隐藏 bug。编写高质量代码并了解我们的大脑如何工作可以帮助…
通向数据科学 [## 语音增强介绍:第一部分 — 概念和任务定义
介绍改善降级语音质量的概念、方法和算法…
通向数据科学](https://towardsdatascience.com/introduction-to-speech-enhancement-part-1-df6098b47b91?source=post_page-----a56a96262f17--------------------------------)
Medium 会员
你喜欢我的写作吗?你是否考虑订阅 Medium 会员以无限访问文章?
如果你通过这个链接订阅,你将通过你的订阅支持我,而对你来说没有额外费用 medium.com/@mattiadigangi/membership
参考文献
[1] Papi, S 等。“当良好且可重复的结果成为泥足巨人:软件质量在自然语言处理中的重要性” arxiv.org/2303.16166
[2] Gulati, Anmol 等。“Conformer:用于语音识别的卷积增强变换器” Proc. Interspeech 2020(2020 年):5036–5040。
冒泡排序解释——数据科学家的算法指南
原文:
towardsdatascience.com/bubble-sort-explained-a-data-scientists-algorithm-guide-853b2286e5ab
冒泡排序的直观解释及其在 Python 中的实现
·发表于 Towards Data Science ·阅读时间 10 分钟·2023 年 1 月 5 日
–
作为软件工程师和数据科学家,我们经常理所当然地看待排序函数。这些算法可能不是我们工作中最光鲜亮丽或讨论最多的方面,但它们在我们每天使用的技术中发挥着至关重要的作用。例如,想象一下在没有按字母顺序排序的功能时,如何整理手机上的联系人列表,或者如何在电子商务网站上按价格和类别排序产品。很容易忽视排序算法的重要性,但它们对我们作为程序员的工作至关重要。
尽管大多数编程语言,如 Java、Python、C# 等,内置了常见排序算法的函数,但我们仍然需要对这些算法的工作原理有基本的了解。这些知识使我们能够根据算法的空间和时间复杂度做出明智的决策,特别是当数据科学家处理大型数据集时。因此,不要小看那些不起眼的排序函数——它们可能不是焦点,但却是科技行业的无名英雄。
在这篇文章中,我们将深入探讨冒泡排序算法,考察其在 Python 和 JavaScript 中的实现。我们还将更详细地了解算法背后的直觉,并讨论时间和空间复杂度的考虑因素。阅读完这篇文章,你将对何时在程序中使用冒泡排序算法有一个扎实的理解,并了解其空间和时间复杂度的概述。
在免费的电子书 硬件 > 软件 > 过程 中,详细了解硬件创新如何改变数据团队构建分析和机器学习应用的方式。
冒泡排序直观理解
在试图理解并回忆一个算法时,首先掌握概念总是更有帮助。在跳入实现之前,通过熟悉这个概念,你将更好地保留信息以备将来使用。冒泡排序也不例外。
为了使用冒泡排序算法将数组[2, 3, 4, 5, 1]按升序排序,我们从第一个元素[2]开始,并将其与第二个元素[3]进行比较。如果第一个元素大于第二个元素,我们交换它们。我们继续比较元素对,直到到达数组的末尾。这样,最大的元素将被移到数组的末尾,最小的元素将被移到数组的开头。
“冒泡排序”这个名字指的是较大的元素在被反复比较和交换较小元素时,“冒泡”到数组的顶部或末端。排序过程结束时,数组将完全按升序排序。
冒泡排序算法逐步解释
以下是我们将使用冒泡排序进行组织的无序数字列表:
作者提供的图片
第一步是只关注前两个数字,在这个例子中是 5 和 9。你可以将只考虑这两个元素 5 和 9 的过程可视化,如下图所示:
作者提供的图片
然后,你必须确定气泡中的数字是否按顺序排列。如果它们的顺序不正确,则需要进行交换以使其正确。幸运的是,它们已经按升序排列。5 小于 9,所以它在 9 之前。这意味着我们无需做更多的操作——我们将气泡再移动一步,如下所示:
作者提供的图片
在数组的下一次迭代中,我们进行相同的步骤。然而,这一次 9 大于 1,但它也在 1 的前面。因此,为了纠正这个问题,我们交换了这两个元素的位置。现在列表的样子如下:
作者提供的图片
现在元素已经交换,冒泡排序将继续处理后续的元素对。这个过程会重复进行,直到数组中的最后一对元素也被检查和交换。第一次遍历数组的结果如下:
作者提供的图片
冒泡排序算法是一种简单但有效的排序方法。它通过反复遍历数组,比较元素对的顺序,如果顺序错误则交换位置。这一过程会重复进行,直到整个数组被排序完成。
需要记住的一点是,对数组进行排序所需的遍数等于数组中的元素个数。例如,一个 6 个元素的数组需要进行 6 次遍历才能完全按升序排序。
然而,通过限制对数组进行的操作次数或遍历次数,可以使冒泡排序算法更高效。这是因为数组的最后一个元素始终是最大值,因此在未来的遍历中不需要继续比较这个位置之后的所有元素。我们将在下面的 Python 和 JavaScript 实现中看到这种优化的实际效果。
用 Python 实现的冒泡排序算法
本节使用 Python 编程语言实现冒泡排序算法。我们将观察到一种朴素的实现方式和一种更高效的冒泡排序算法。
初始化一个包含整数元素的 Python 数组
unsortedData = [20, 33, 12, 53, 24, 65, 23, 4, 53, 1];
定义一个名为‘bubbleSort’的函数,该函数接受一个名为‘data’的数组作为参数。首先,我们尝试遍历数组,并交换满足条件的元素,即在特定索引处的左边元素大于右边元素时,我们对这两个元素执行交换操作。
需要注意的一点是,在任何迭代中,将左边元素分配给临时变量‘tempValue’,然后将右边元素分配给临时变量。
def bubbleSort(data):
for i in range(0, len(data)):
if i+1 < len(data):
if data[i] > data[i+1]:
tempValue = data[i]
data[i] = data[i+1]
data[i+1] = tempValue
return data
上述代码片段在用未排序的数组作为参数调用时,将对数组进行一次冒泡排序函数的遍历。在大多数情况下,可能不会完全将数组排序为升序。
sortedData = bubbleSort(unsortedData)
print(sortedData)
>>>[20, 12, 33, 24, 53, 23, 4, 53, 1, 65]
为了解决这个问题,我们必须按照配对组合的次数来遍历我们想要排序的数组。简单来说,进行的迭代次数是未排序数组长度的平方(len(unsortedArrray)²)。这是一种朴素的实现方式。
def bubbleSort(data):
# Iterate through the array enough times to consider every possible swap pairs
for _ in range(0, len(data)):
for i in range(0, len(data)):
if i+1 < len(data):
if data[i] > data[i+1]:
tempValue = data[i]
data[i] = data[i+1]
data[i+1] = tempValue
return data
再次运行冒泡排序函数,并将未排序的数组作为参数传递,将得到一个升序排列的数组作为输出。
sortedData = bubbleSort(unsortedData)
print(sortedData)
>>> [1, 4, 12, 20, 23, 24, 33, 53, 53, 65]
优化版冒泡排序
虽然朴素版的冒泡排序算法有效,但存在一些不必要和冗余的操作。特别是,它比较了数组末尾已经是最大值的元素。这是因为在每次遍历数组时,冒泡排序算法将最大元素值移动到数组末尾。
为了优化冒泡排序算法,我们可以通过跟踪我们想要比较的数组部分来减少所需的交换操作次数。我们可以通过从数组的最大长度开始,并在每次遍历后将其减少 1,从而减少交换操作作用的数组区域。这样,我们可以避免在每次遍历时与数组末尾的元素进行比较,因为这些元素已经在正确的位置。
通过使用这种优化,我们可以使冒泡排序算法更高效,并减少其执行的多余操作次数。
unsortedData = [20, 33, 12, 53, 24, 65, 23, 4, 53, 1];
end = len(unsortedData)
def bubbleSort(data):
global end
for _ in range(0, end):
for i in range(0, end):
if i+1 < end:
if data[i] > data[i+1]:
tempValue = data[i]
data[i] = data[i+1]
data[i+1] = tempValue
end = end - 1
return data
sortedData = bubbleSort(unsortedData)
print(sortedData)
>>> [1, 4, 12, 20, 23, 24, 33, 53, 53, 65]
可以进一步重构以确保上述代码可读且高效。此外,正如 Robert Kübler 博士 在评论中指出的,通过检查是否发生了交换操作,可以进一步优化该算法。如果没有发生交换操作,我们可以中断循环以避免不必要的数组遍历。
unsortedData = [20, 33, 12, 53, 24, 65, 23, 4, 53, 1]
n = len(unsortedData)
def bubbleSort(data):
for i in range(n):
swapped = False
for j in range(0, n-i-1):
if data[j] > data[j+1]:
data[j], data[j+1] = data[j+1], data[j]
swapped = True
if not swapped:
break
return data
sortedData = bubbleSort(unsortedData)
print(sortedData)
以下是用 JavaScript 实现的相同算法,JavaScript 是一种受到数据从业者和软件工程师欢迎的编程语言。
用 JavaScript 实现的冒泡排序算法
const unsortedData = [20, 33, 12, 53, 24, 65, 23, 4, 53, 1];
let end = unsortedData.length - 1
const bubbleSort = (data) => {
for (let i = 0; i < end; i++) {
if (data[i] > data[i + 1]) {
const valueInRight = data[i]
data[i] = data[i+1]
data[i+1] = valueInRight
}
}
end--
}
for (let i = 0; i < unsortedData.length; i++) {
bubbleSort(unsortedData)
}
console.log(unsortedData)
时间和空间复杂度(大 O 记号)
数据科学家必须了解排序算法的性能及其所需的时间/空间。这使你能够根据具体情况选择最佳的排序算法,因为有很多选项可供选择。
当冒泡排序用于已经按升序排列的数组时,只需遍历整个数组一次。这被视为最佳情况。然而,实际上,这种情况只偶尔发生,冒泡排序通常需要 n(n-1)/2 次交换或比较才能实现排序数组。
冒泡排序算法的平均/最坏时间复杂度为 O(n²),因为我们需要通过数组多次,与提供的数组中的对数一样。因此,当时间是一个因素时,可能会有更好的选择。
-
时间复杂度 最坏情况:O(n²)
-
时间复杂度 平均情况:O(n²)
-
时间复杂度 最佳情况:O(n),数组已经排序
在空间复杂度方面,由于我们只是交换了元素而没有存储任何东西,我们不需要额外的空间来运行算法。这非常了不起,因为这意味着空间复杂度为常数,即 O(1)。这使得它成为一个 原地 算法,通过直接修改输入来工作。
关键要点
冒泡排序算法可能不是最知名或评价最高的排序算法,但正如我们所见,它也不是一个糟糕的选择。其时间复杂度为 O(n²),空间复杂度为 O(1),这是一个简单的算法,容易为初学者理解。然而,它的慢速度可能使得它在某些应用中不够实际。
尽管有其局限性,冒泡排序算法可以作为学习排序算法和数据结构的一个有用起点。这是一个很好地理解这些算法如何工作的基础方式,并且可以帮助你为以后学习更复杂的算法奠定基础。
话虽如此,冒泡排序算法可能不是处理时间敏感材料的最佳选择,因为其速度较慢可能会成为限制。然而,如果你愿意为了时间牺牲一些空间,它可能会对你有用。最终,排序算法的选择将取决于你的具体需求和目标。通过了解冒泡排序算法,你可以做出更明智的决策,选择最适合你需求的算法。
常见问题解答
- 冒泡排序是什么?
冒泡排序是一种排序算法,它使用比较方法来排序数组。该算法比较数组中一对对的元素,并在左侧对(位置)大于右侧对(位置+1)时交换它们。这个过程重复进行,直到整个数组排序完成。
- 冒泡排序需要多少次遍历?
冒泡排序需要 n(n-1)/2 次遍历所有元素,以便最终数组按升序排列。
- 冒泡排序的最坏时间复杂度是多少?
冒泡排序的最坏时间复杂度是 O(n2)。
- 冒泡排序的最坏时间复杂度是多少?
冒泡排序的最佳时间复杂度是 O(n),当数组已经排序时会出现这种情况。
- 冒泡排序的空间复杂度是多少?
冒泡排序具有 O(1) 的空间复杂度,因为它通过直接修改输入来原地操作。
我希望你觉得这篇文章有用。
要与我联系或找到更多类似于这篇文章的内容,请执行以下操作:
-
支持我的写作 成为推荐的 Medium 会员
-
订阅我的 YouTube 频道
-
订阅我的播客 Apple Podcasts | Spotify | Audible
-
订阅我的 邮件列表 获取我的新闻通讯
接下来读什么 👇🏾
一种合并排序算法的解释及其在 Python 中的实现
towardsdatascience.com
Python 中的错误?pdb
来拯救!
原文:
towardsdatascience.com/bugs-in-python-pdb-to-the-rescue-d88a56a2ca71
PYTHON 编程
pdb
调试器值得学习和使用吗?
·发布于Towards Data Science ·13 分钟阅读·2023 年 9 月 21 日
–
调试有助于你从失败中学习。照片由Brett Jordan拍摄,Unsplash提供
各种工具可以用于调试 Python 代码,从最简单的print()
函数,通过静态但更高级的[icecream](https://github.com/gruns/icecream)
及其兄弟[ycecream](https://github.com/salabim/ycecream)
,到各种 IDE 提供的交互式调试器。然而,我一直选择的是内置的pdb
调试器,以及内置的breakpoint()
函数。
调试
调试是编程的核心。你在开始学习编程时就开始调试,当你承诺你刚刚写完最后一行代码时你才会停止调试——如果你能遵守这个承诺的话。
你可能会认为,减少调试时间的一种方法是编写优质代码。让我们面对现实吧:往往,编写优质代码意味着……在开发过程中调试很多。诚然,一个好的程序员会编写更好的代码并犯更少的错误——但这并不意味着他或她不需要调试。
不过,有一种方法可以减少调试的次数:减少调试的方法就是编写良好的单元测试。
为了减少调试的次数,编写良好的单元测试。
无论你是否使用测试驱动开发,都要编写良好的测试。编写良好的测试意味着编写足够数量的精心编写的测试。我在这里不打算讨论测试,所以我给你留个想法;我在这里写了更多关于测试的内容:
大多数开发者不喜欢编写测试。如果你也是其中之一,尽力改变这一点。
我们可以假设所有程序员都需要调试他们的代码。有些人可能会说他们不需要,但这不是真的。他们确实需要;只不过他们不使用专门的调试工具,即调试器。相反,他们会运行代码来处理特定的输入,然后检查结果,然后发现有问题后修改代码并重复这个过程。因此,尽管他们不使用调试器,他们依然在调试代码;只是需要花费更多时间。调试器是有其存在意义的!
有时,单独调用 print()
函数就能解决问题。但不要自欺欺人:这不是一种非常有效的调试方法。我不是说你不应该使用它——但这是一个过于简单的方法,只能在最简单的情况下起作用。
许多使用 IDE 进行代码开发的人喜欢使用内置的调试器。Visual Studio Code 有自己的调试器,Pycharm 有一个,甚至 Thonny 也有一个。
你还可以使用作为 Python 包提供的各种调试器,这些包可以从 PyPi 安装。打开 PyPi 并搜索“debugger”一词;你会找到很多结果,但你可能需要花费一些耐心来找到能帮助你调试代码的工具。
你可以在下面的 Towards Data Science 文章中阅读关于 Python 调试器的内容:
更快、更高效地调试你的代码。
towardsdatascience.com
文章讨论了——尽管没有展示如何使用——pdb
、PyCharm 和 Visual Studio(以及 VS Code)的调试器、Komodo 和 Jupyter Visual Debugger。
静态调试器与交互式调试器
调试器可以是静态的也可以是交互式的。前者只是展示对象;后者则允许你操作它们。
两者都可能有帮助,但交互式调试器提供了最大的调试能力,因为它们能够暂停程序并查看当前状态。你可以查看和使用本地和全局作用域中的所有对象;你可以检查特定命令或命令集是否有效。这就是为什么我常常偏好交互式调试而不是静态调试。
print()
函数是静态调试的一个完美示例。IDE 调试器通常是交互式的。
然而,有一个调试器同时提供了简单性和强大功能。它就是 pdb
,一个内置的交互式 Python 调试器:
该模块定义了一个用于 Python 程序的交互式源代码调试器。它支持设置(条件)…
是的,pdb
是内置的,所以你不需要安装它。它随 Python 安装包一起提供,你可以在任何环境中使用它。而且,pdb
是交互式的。这实际上是我对调试器的主要期望!
是的,
pdb
是内置的,所以你不需要安装它。它随 Python 安装包一起提供,你可以在任何环境中使用它。而且,pdb
是交互式的。
在本文中,我们将讨论pdb
的基础知识。我们将介绍这个强大工具的基础知识,但要注意,它提供的功能远不止这些。好在这些基础知识足以让你开始使用pdb
。说实话,我很少使用pdb
的高级选项。因此,阅读这篇文章将为你提供调试 Python 代码的强大工具。
关于 pdb 的几点说明
pdb
的一个优点是你可以在任何地方使用它,而无需安装任何额外的东西。即使是远程环境——pdb
也能正常工作。只需运行它,瞧,你就有了一个可以远程使用的交互式调试器。或者在本地使用也没问题。
首先,让我解释如何使用pdb
,然后你可以决定它是否适合你。
基本上,你可以在两种模式下使用pdb
。首先,你可以在pdb
模式下运行你的 Python 程序。这意味着程序会逐行执行,直到完成执行或发生错误。然后程序会在死后模式下重新运行,这意味着它会在错误发生之前停下来,你将能够查看局部和全局作用域中的情况。
其次,你可以在代码中添加一个所谓的断点,调试器将会在断点处停止程序。你还可以添加更多的断点。当然,调试器只有在断点之前没有抛出错误的情况下才能停止程序。下面,我们将讨论这两种情况。
pdb
模式
要在pdb
模式下运行你的程序,只需按照以下方式运行它:
$ python -m pdb myapp.py
这意味着pdb
控制台将打开,myapp.py
脚本将逐行运行。你可以更改这种行为,将其运行到第一个错误或程序结束。最好通过一些示例来展示这如何工作。
我们将使用以下脚本,保存为myapp.py
:
def foo(s):
if not isinstance(s, str):
raise TypeError
return s.upper()
if __name__ == "__main__":
for s in ("string1", "string2"):
_ = foo(s)
(这是一个示例脚本,没有什么值得自豪的。我们确实需要简单的案例来进行分析。)
我们还将使用其错误版本,其中 Python 将抛出一个错误;该脚本保存在myapp_error.py
文件中:
def foo(s):
if not isinstance(s, str):
raise TypeError
return s.upper()
if __name__ == "__main__":
for s in ("string1", 10):
_ = foo(s)
如你所见,正确的程序将运行一个 for
循环,在每次循环中,它将对不同的 s
参数值运行 foo()
函数:首先是 "string1"
然后是 "string2"
,这两个值都是正确的。在错误的版本中,foo("string2")
应该被 foo()
替换为不正确的值 10
,这将导致 TypeError
被抛出。
目前,你需要知道的唯一 pdb
命令是
-
c
,或continue
;命令的另一个版本是cont
; -
n
,或next
;和 -
q
,或quit
。
有时你需要使用quit
两到三次,甚至更多次,才能退出调试器。
continue
命令会执行程序直到以下两种情况之一发生:程序结束或抛出错误。为了查看这如何工作,我们来运行我们脚本的正确版本 myapp.py
:
$ python -m pdb myapp.py
> /{path}/myapp.py(1)<module>()
-> def foo(s: str):
(Pdb) c
The program finished and will be restarted
> /{path}/myapp.py(1)<module>()
-> def foo(s: str):
(Pdb)
(在代码块中,{path}
代表从我的计算机上的长路径。)
如你所见,在运行了 shell 命令 python -m pdb myapp.py
之后,我们进入了一个新的 pdb
会话,调试器正在等待我们的第一个命令。如上所示,c
命令将继续程序运行直到第一个错误或程序结束。由于我们运行的是正确的脚本,调试器没有遇到任何问题,并且打印了程序已完成,将重新启动
。这将我们移回到程序的第一行,调试器再次等待我们的命令。现在我们可以逐行调试(如下所示)。
让我们看看如果我们对错误的脚本使用 c
命令会发生什么:
$ python -m pdb myapp_error.py
> /{path}/myapp_error.py(1)<module>()
-> def foo(s: str):
(Pdb) c
Traceback (most recent call last):
File "/usr/lib/python3.9/pdb.py", line 1726, in main
pdb._runscript(mainpyfile)
File "/usr/lib/python3.9/pdb.py", line 1586, in _runscript
self.run(statement)
File "/usr/lib/python3.9/bdb.py", line 580, in run
exec(cmd, globals, locals)
File "<string>", line 1, in <module>
File "/{path}/myapp_error.py", line 1, in <module>
def foo(s: str):
File "/{path}/myapp_error.py", line 3, in foo
raise TypeError
TypeError
Uncaught exception. Entering post mortem debugging
Running 'cont' or 'step' will restart the program
> /{path}/myapp_error.py(3)foo()
-> raise TypeError
(Pdb)
如你所见,这次程序引发了一个错误(TypeError
,没有消息)。当抛出未捕获的错误时,程序会停止执行,调试器进入所谓的事后调试阶段。这时你可以了解你的程序发生了什么以及为何失败。
按 n
,pdb
将执行代码的下一行。不是下一条命令,而是下一行,因此如果下一条命令被拆分成两行或更多行,你需要调用每一行,最终执行命令。请注意这个 pdb
会话:
$ python -m pdb myapp.py
> /{path}/myapp.py(1)<module>()
-> def foo(s: str):
(Pdb) n
> /{path}/myapp.py(1)<module>()
-> if __name__ == "__main__":
(Pdb)
> /{path}/myapp.py(1)<module>()
-> for s in ("string1", 10, "string2"):
(Pdb)
> /{path}/myapp.py(1)<module>()
-> _ = foo(s)
(Pdb)
> /{path}/myapp.py(1)<module>()
-> for s in ("string1", 10, "string2"):
(Pdb)
> /{path}/myapp.py(1)<module>()
-> _ = foo(s)
(Pdb)
TypeError
> /{path}/myapp.py(1)<module>()
-> _ = foo(s)
(Pdb)
首先,请注意当你使用一个命令(在这里是 n
)时,你不需要重复它来运行。pdb
会记住你最后的命令,按回车键将再次执行它。在按了几次之后,它把我们带到了停止程序的错误。
请注意,在 pdb
模式下,Tab 自动补全的行为并不完全正常。这并不意味着它完全无效;你只需要在输入其他内容之前使用 p
命令。例如,在这种情况下按下 Tab 键:
(Pdb) al
将不会有任何结果。但在这里按:
(Pdb) p al
将会完成 alpha
名称:
(Pdb) p alpha
有很多 pdb
命令可供你使用。你可以在这里找到它们:
源代码:Lib/pdb.py 模块 pdb 定义了一个用于 Python 程序的交互式源代码调试器。它支持…
在继续之前,我想与您分享一个简单的命令;也许它不是最重要的,但在我过去的经验中我非常欣赏它。它是pp
,用于漂亮打印:
(Pdb) {f"{x_i = }, {alpha = }, and {beta = }": (x_i + alpha)/(1 + beta) for x_i in x}
{'x_i = 1, alpha = 4, and beta = 0': 5.0, 'x_i = 2, alpha = 4, and beta = 0': 6.0, 'x_i = 3, alpha = 4, and beta = 0': 7.0}
(Pdb) pp {f"{x_i = }, {alpha = }, and {beta = }": (x_i + alpha)/(1 + beta) for x_i in x}
{'x_i = 1, alpha = 4, and beta = 0': 5.0,
'x_i = 2, alpha = 4, and beta = 0': 6.0,
'x_i = 3, alpha = 4, and beta = 0': 7.0}
正如您所见,调用一个表达式和使用 pp
命令调用它之间差别很大。因此,记住它是好的。
还有一件事。即使上面的字典推导很长,我也没有将其拆分成两行或更多行。这是因为 pdb
不允许这样做,至少在其调试模式下是如此——但您可以使用其交互模式,您可以通过 interact
命令运行它:
(Pdb) interact
>>> {f"{x_i = }, {alpha = }, and {beta = }":
... (x_i + alpha)/(1 + beta) for x_i in x}
{'x_i = 1, alpha = 4, and beta = 0': 5.0, 'x_i = 2, alpha = 4, and beta = 0': 6.0, 'x_i = 3, alpha = 4, and beta = 0': 7.0}
记住,在交互模式下,pdb
命令不起作用。要离开此模式并返回 pdb
模式,请按 <Ctrl + D>
。
使用 breakpoint() 函数进行调试
上面我们讨论了在 pdb
模式下调试。然而,通常情况下,设置一个所谓的断点会更容易。断点是代码中的一个位置,在这个位置您希望程序暂停并进行分析;您可以在代码中创建多个断点,代码会在每个断点处停止——除非抛出错误。
要创建一个,请在您希望调试器停止并让您进入的代码位置添加对 breakpoint()
函数的调用:
def y(x, alpha, beta):
breakpoint()
return [(xi + alpha)/(1 + beta) for xi in x]
x = [1, 2, 3]
y(x)
运行此脚本将引导您进入此调试会话:
-> return [(xi + alpha)/(1 + beta) for xi in x]
(Pdb) l
1 def y(x, alpha, beta):
2 breakpoint()
3 -> return [(xi + alpha)/(1 + beta) for xi in x]
4
5
6 x = [1, 2, 3]
7 y(x, 4, 0)
[EOF]
(Pdb)
l
(list
)命令显示您当前所在位置周围的十一行。您还可以使用 ll
(longlist
),它将打印当前函数或帧的整个源代码。
其余部分与之前相同,因为您已经进入了我们上面讨论的 pdb
模式。使用 breakpoint()
函数的明显优势是可以精确地在您希望的地方停止程序。坦白说,我在几乎所有的调试会话中都使用 breakpoint()
。
代码中的断点让您暂停片刻,检查您希望检查的代码位置的内部情况。照片由 Malte Helmhold 在 Unsplash 上拍摄
对象丢失了?
您可能会遇到一种奇怪的情况——虽然它只对那些不知道如何处理的人来说很奇怪。有时,您可能会发现 pdb
的行为非常特殊:虽然它可以看到局部变量,但它……看不到这些局部变量。
听起来像完全的废话?让我解释一下。考虑这个非常简单的函数:
def y(x, alpha, beta):
return [(xi + alpha)/(1 + beta) for xi in x]
它计算一个简单模型的值,针对一个值列表 x
,给定两个模型参数 alpha
和 beta
。例如:
>>> def y(x, alpha, beta):
... return [(xi + alpha)/(1 + beta) for xi in x]
...
>>> x = [1, 2, 3]
>>> y(x, .25, 0)
[1.25, 2.25, 3.25]
现在想象一下,您希望进入函数并检查多个 x
列表的函数。您可以通过 pdb
的帮助来做到这一点:
>>> def y(x, alpha, beta):
... breakpoint()
... return [(xi + alpha)/(1 + beta) for xi in x]
...
>>> y(x, .25, 0)
> <stdin>(3)y()
(Pdb) alpha, beta
(0.25, 0)
(Pdb) [(xi + alpha)/(1 + beta) for xi in x]
*** NameError: name 'alpha' is not defined
什么?刚刚发生了什么?为什么pdb
看不到alpha
——它不是刚刚看到的吗?确实,在这一行:
(Pdb) alpha, beta
(0.25, 0)
所以,它能看到alpha
和beta
——但它看不到它们?
也许我们应该再次给这些变量赋值?让我们检查一下:
(Pdb) alpha = .25; beta = 0
(Pdb) alpha
0.25
(Pdb) [(xi + alpha)/(1 + beta) for xi in x]
*** NameError: name 'alpha' is not defined
不,这根本没有帮助。
问题是,列表推导式——以及其他推导式——有自己的作用域,局部变量在那里是不可见的。幸运的是,你有很多解决方案,如下所示。
交互模式
交互模式实际上在各种情况下都非常有用。你可以使用pdb
shell 中的interact
命令来启动它:
(Pdb) interact
*interactive*
>>> [(xi + alpha)/(1 + beta) for xi in x]
[1.25, 2.25, 3.25]
如你所见,在交互模式下,代码的运行方式是正常的。
将缺失的对象添加到 globals
缺少一个特定的对象,所以只需将其添加到globals()
中:
(Pdb) globals()['alpha'] = alpha
(Pdb) [(xi + alpha)/(1 + beta) for xi in x]
*** NameError: name 'beta' is not defined
如你所见,pdb
可以看到alpha
但看不到beta
。一种解决方案是将其添加到globals()
中,就像我们添加alpha
一样,但逐个提供所有全局变量并不好玩;下一个解决方案只需一条命令即可完成。
将所有局部变量添加到 globals
locals()
和globals()
都是字典,因此我们可以简单地将前者添加到后者中。你可以按照以下方式进行:
(Pdb) globals().update(locals())
(Pdb) [(xi + alpha)/(1 + beta) for xi in x]
[1.25, 2.25, 3.25]
希望你喜欢这篇文章。虽然文章没有涵盖pdb
的所有知识,但它提供了足够的知识来在大多数情况下使用这个调试工具。
在我超过 5 年的 Python 实践中,我注意到很少有人使用pdb
来调试代码。我不知道为什么。IDE 调试工具确实能提供更多,但pdb
的强大之处在于它在 Python 标准库中的可用性。
我不确定这是否值得自豪,但我会对你诚实:pdb
是我选择的调试工具。我几乎不使用其他调试工具。我从未遇到过任何问题;相反,它在我所有的 Python 项目中都提供了帮助。
当我在尝试其他调试工具时,确实遇到了各种问题。也许是我自己的问题;也许是我没有足够长时间地使用它们以体验它们的强大。这可能是真的——但我可以说我已经足够长时间地使用pdb
,尽管它很简单,但它可以是一个很棒的调试工具。
感谢阅读。如果你喜欢这篇文章,你可能也会喜欢我写的其他文章;你可以在这里看到它们。如果你想加入 Medium,请使用下面的推荐链接:
链接 [## 使用我的推荐链接加入 Medium - Marcin Kozak