docs-merge-05
TowardsDataScience 2024 中文翻译(六)
使用 LangGraph 和 LangChain 创建任务导向对话系统
又一篇 LangGraph 教程
·发布于Towards Data Science ·阅读时间 11 分钟·2024 年 9 月 14 日
--

任务导向对话系统(ToD)是帮助用户完成特定任务的系统,例如预定餐厅、规划旅行行程或订餐。
我们知道,使用提示语(prompts)来指导大型语言模型(LLMs),但我们如何实现这些任务导向对话系统(ToD),使得对话始终围绕我们希望用户完成的任务展开呢?一种方法是使用提示语、记忆和工具调用。幸运的是,LangChain + LangGraph 可以帮助我们将这些要素结合起来。
在这篇文章中,您将学习如何构建一个任务导向对话系统,帮助用户以高质量创建用户故事。该系统完全基于 LangGraph 的根据用户需求生成提示教程。
为什么我们需要使用 LangGraph?
在本教程中,我们假设您已经知道如何使用 LangChain。用户故事包含一些组件,如目标、成功标准、执行计划和交付物。用户应提供每一个组件,而我们需要一步步地“引导”他们逐个提供。仅使用 LangChain 实现这一点会需要大量的条件判断语句(if/else)。
使用 LangGraph,我们可以使用图抽象来创建循环来控制对话。它还具有内置持久性,因此我们无需担心主动追踪图内发生的交互。
LangGraph 的主要抽象是 StateGraph,它用于创建图工作流。每个图都需要用state_schema 初始化:一个每个节点使用的模式类,用于读取和写入信息。
我们系统的流程将由LLM和用户消息的轮次组成。主要循环包含以下步骤:
-
用户说了些什么
-
LLM 读取状态中的消息,并决定是否准备好创建用户故事,或者是否需要用户再次响应
我们的系统很简单,因此架构仅包含对话中交换的消息。
from langgraph.graph.message import add_messages
class StateSchema(TypedDict):
messages: Annotated[list, add_messages]
add_messages 方法用于将每个节点的输出消息合并到图中现有状态的消息列表中。
说到节点,另两个主要的 LangGraph 概念是节点和边。每个图的节点运行一个函数,每个边控制一个节点到另一个节点的流动。我们还有START和END虚拟节点,告诉图从哪里开始执行以及执行应该在哪里结束。
要运行系统,我们将使用 .stream() 方法。在构建并编译图后,每一轮交互都会经过图的 START,直到 END,它的路径(哪些节点应运行或不运行)由我们的工作流与图的状态共同控制。以下代码包含我们系统的主要流程:
config = {"configurable": {"thread_id": str(uuid.uuid4())}}
while True:
user = input("User (q/Q to quit): ")
if user in {"q", "Q"}:
print("AI: Byebye")
break
output = None
for output in graph.stream(
{"messages": [HumanMessage(content=user)]}, config=config, stream_mode="updates"
):
last_message = next(iter(output.values()))["messages"][-1]
last_message.pretty_print()
if output and "prompt" in output:
print("Done!")
在每次交互时(如果用户没有输入“q”或“Q”退出),我们运行 graph.stream(),传递用户消息,并使用“updates” stream_mode,它会流式传输图每一步后状态的更新(langchain-ai.github.io/langgraph/concepts/low_level/#stream-and-astream)。然后我们从 state_schema 获取最后一条消息并打印出来。
在本教程中,我们仍然会学习如何创建图的节点和边,但首先让我们更多地了解 ToD 系统的架构,并学习如何用LLMs、提示和工具调用实现一个系统。
ToD 系统的架构
构建端到端任务导向对话系统的框架的主要组件是 [1]:
-
自然语言理解(NLU)用于提取用户的意图和关键信息
-
对话状态跟踪(DST)用于追踪用户在对话中的信念状态
-
对话策略学习(DPL)用于确定下一步操作
-
自然语言生成(NLG)用于生成对话系统响应

ToD 系统的主要组件(图片来自秦立波等人 [1])
通过使用 LLM,我们可以将这些组件中的一些组合成一个。NLP和NLG组件使用 LLM 实现起来轻而易举,因为理解和生成对话回应正是它们的专长。
我们可以通过使用 LangChain 的SystemMessage来实现对话状态跟踪(DST)和对话策略学习(DPL),以引导 AI 行为,并在每次与 LLM 互动时始终传递此消息。对话的状态也应始终在每次与模型互动时传递给 LLM。这意味着我们将确保对话始终围绕我们希望用户完成的任务进行,通过始终告诉 LLM 对话的目标是什么以及它应如何行为。我们首先通过使用提示来做到这一点:
prompt_system_task = """Your job is to gather information from the user about the User Story they need to create.
You should obtain the following information from them:
- Objective: the goal of the user story. should be concrete enough to be developed in 2 weeks.
- Success criteria the sucess criteria of the user story
- Plan_of_execution: the plan of execution of the initiative
- Deliverables: the deliverables of the initiative
If you are not able to discern this info, ask them to clarify! Do not attempt to wildly guess.
Whenever the user responds to one of the criteria, evaluate if it is detailed enough to be a criterion of a User Story. If not, ask questions to help the user better detail the criterion.
Do not overwhelm the user with too many questions at once; ask for the information you need in a way that they do not have to write much in each response.
Always remind them that if they do not know how to answer something, you can help them.
After you are able to discern all the information, call the relevant tool."""
然后每次我们向 LLM 发送消息时,都附加这个提示:
def domain_state_tracker(messages):
return [SystemMessage(content=prompt_system_task)] + messages
我们 ToD 系统 LLM 实现的另一个重要概念是工具调用。如果你再次阅读prompt_system_task的最后一句话,它说:“在你能够辨识出所有信息后,调用相关工具”。通过这种方式,我们在告诉 LLM,当它判断用户提供了所有用户故事参数时,它应该调用工具来创建用户故事。我们为此创建的工具将使用 Pydantic 模型与用户故事参数来构建。
通过仅使用提示和工具调用,我们可以控制我们的 ToD 系统。很棒对吧?实际上,我们还需要使用图的状态来使这一切正常工作。我们将在下一节中完成这一部分,届时我们将最终构建 ToD 系统。
创建对话系统以构建用户故事
好的,开始编码吧。首先我们将指定使用哪个 LLM 模型,然后设置提示并绑定工具来生成用户故事:
import os
from dotenv import load_dotenv, find_dotenv
from langchain_openai import AzureChatOpenAI
from langchain_core.pydantic_v1 import BaseModel
from typing import List, Literal, Annotated
_ = load_dotenv(find_dotenv()) # read local .env file
llm = AzureChatOpenAI(azure_deployment=os.environ.get("AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"),
openai_api_version="2023-09-01-preview",
openai_api_type="azure",
openai_api_key=os.environ.get('AZURE_OPENAI_API_KEY'),
azure_endpoint=os.environ.get('AZURE_OPENAI_ENDPOINT'),
temperature=0)
prompt_system_task = """Your job is to gather information from the user about the User Story they need to create.
You should obtain the following information from them:
- Objective: the goal of the user story. should be concrete enough to be developed in 2 weeks.
- Success criteria the sucess criteria of the user story
- Plan_of_execution: the plan of execution of the initiative
If you are not able to discern this info, ask them to clarify! Do not attempt to wildly guess.
Whenever the user responds to one of the criteria, evaluate if it is detailed enough to be a criterion of a User Story. If not, ask questions to help the user better detail the criterion.
Do not overwhelm the user with too many questions at once; ask for the information you need in a way that they do not have to write much in each response.
Always remind them that if they do not know how to answer something, you can help them.
After you are able to discern all the information, call the relevant tool."""
class UserStoryCriteria(BaseModel):
"""Instructions on how to prompt the LLM."""
objective: str
success_criteria: str
plan_of_execution: str
llm_with_tool = llm.bind_tools([UserStoryCriteria])
正如我们之前所讨论的,我们图的状态仅包括交换的消息和一个标志,用于判断用户故事是否已创建。让我们首先使用StateGraph和这个模式来创建图:
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
class StateSchema(TypedDict):
messages: Annotated[list, add_messages]
workflow = StateGraph(StateSchema)
下一张图展示了最终图的结构:

用于创建用户故事的 ToD 图的结构(图像由作者创建)
在顶部我们有一个talk_to_user节点。这个节点可以是:
-
完成对话(跳转到finalize_dialogue节点)
-
决定是时候等待用户输入了(跳转到END节点)
由于主循环是永远运行的(while True),每当图到达 END 节点时,它会再次等待用户输入。等到我们创建循环时,这一点会变得更加清晰。
让我们从talk_to_user节点开始创建图中的节点。这个节点需要跟踪任务(在整个对话中保持主要提示),并且还需要保持消息交换,因为它是存储对话状态的地方。这个状态还会跟踪哪些用户故事的参数已经填写,哪些还没有,使用的是消息。因此,每次此节点应该添加SystemMessage并附加来自 LLM 的新消息:
def domain_state_tracker(messages):
return [SystemMessage(content=prompt_system_task)] + messages
def call_llm(state: StateSchema):
"""
talk_to_user node function, adds the prompt_system_task to the messages,
calls the LLM and returns the response
"""
messages = domain_state_tracker(state["messages"])
response = llm_with_tool.invoke(messages)
return {"messages": [response]}
现在我们可以将talk_to_user节点添加到此图中了。我们通过为它命名,然后传递我们创建的函数来完成这个操作:
workflow.add_node("talk_to_user", call_llm)
这个节点应该是图中运行的第一个节点,所以我们通过边来指定这一点:
workflow.add_edge(START, "talk_to_user")
到目前为止,图是这样的:

这是只有一个节点的图(由作者创建的图像)
为了控制图的流程,我们还将使用 LangChain 中的消息类。我们有四种类型的消息:
-
SystemMessage: 用于引导 AI 行为的消息
-
HumanMessage: 来自人类的消息
-
AIMessage: 从聊天模型返回的作为对提示的响应的消息
-
ToolMessage: 包含工具调用结果的消息,用于将执行工具的结果传递回模型
我们将使用图状态中最后一条消息的类型来控制talk_to_user节点的流程。如果最后一条消息是AIMessage并且它包含tool_calls键,那么我们将跳转到finalize_dialogue节点,因为该是创建用户故事的时候了。否则,我们应该跳转到END节点,因为该是用户回答的时候,应该重启循环。
finalize_dialogue节点应该构建ToolMessage来将结果传递给模型。tool_call_id字段用于将工具调用请求与工具调用响应关联。让我们创建这个节点并将它添加到图中:
def finalize_dialogue(state: StateSchema):
"""
Add a tool message to the history so the graph can see that it`s time to create the user story
"""
return {
"messages": [
ToolMessage(
content="Prompt generated!",
tool_call_id=state["messages"][-1].tool_calls[0]["id"],
)
]
}
workflow.add_node("finalize_dialogue", finalize_dialogue)
现在让我们创建最后一个节点,create_user_story节点。此节点将使用提示调用 LLM 来创建用户故事,并将对话过程中收集到的信息包含在内。如果模型决定该是调用工具的时机,那么tool_calls键的值应该包含创建用户故事所需的所有信息。
prompt_generate_user_story = """Based on the following requirements, write a good user story:
{reqs}"""
def build_prompt_to_generate_user_story(messages: list):
tool_call = None
other_msgs = []
for m in messages:
if isinstance(m, AIMessage) and m.tool_calls: #tool_calls is from the OpenAI API
tool_call = m.tool_calls[0]["args"]
elif isinstance(m, ToolMessage):
continue
elif tool_call is not None:
other_msgs.append(m)
return [SystemMessage(content=prompt_generate_user_story.format(reqs=tool_call))] + other_msgs
def call_model_to_generate_user_story(state):
messages = build_prompt_to_generate_user_story(state["messages"])
response = llm.invoke(messages)
return {"messages": [response]}
workflow.add_node("create_user_story", call_model_to_generate_user_story)
所有节点创建完成后,接下来是添加边。我们将向talk_to_user节点添加一个条件边。记住,这个节点可以:
-
如果该是调用工具的时机,就结束对话(跳转到finalize_dialogue节点)
-
决定我们需要收集用户输入(跳转到END节点)
这意味着我们只检查最后一条消息是否是 AIMessage,并且是否包含 tool_calls 键;否则我们应该跳转到 END 节点。让我们创建一个函数来检查这个,并将它作为一条边添加:
def define_next_action(state) -> Literal["finalize_dialogue", END]:
messages = state["messages"]
if isinstance(messages[-1], AIMessage) and messages[-1].tool_calls:
return "finalize_dialogue"
else:
return END
workflow.add_conditional_edges("talk_to_user", define_next_action)
现在让我们添加其他边:
workflow.add_edge("finalize_dialogue", "create_user_story")
workflow.add_edge("create_user_story", END)
这样图的工作流程就完成了。是时候编译图并创建循环来运行它了:
memory = MemorySaver()
graph = workflow.compile(checkpointer=memory)
config = {"configurable": {"thread_id": str(uuid.uuid4())}}
while True:
user = input("User (q/Q to quit): ")
if user in {"q", "Q"}:
print("AI: Byebye")
break
output = None
for output in graph.stream(
{"messages": [HumanMessage(content=user)]}, config=config, stream_mode="updates"
):
last_message = next(iter(output.values()))["messages"][-1]
last_message.pretty_print()
if output and "create_user_story" in output:
print("User story created!")
最后让我们测试系统:

助手在行动中(图片由作者创建)
最后的思考
有了 LangGraph 和 LangChain,我们可以构建引导用户通过结构化互动的系统,借助 LLMs 帮助我们控制条件逻辑,从而减少创建它们的复杂性。
通过结合提示、记忆管理和工具调用,我们可以创建直观且有效的对话系统,开启用户互动和任务自动化的新可能性。
我希望这个教程能帮助你更好地理解如何使用 LangGraph(我花了好几天时间琢磨如何让这个库的所有部分协同工作)。
本教程的所有代码可以在这里找到:dmesquita/task_oriented_dialogue_system_langgraph (github.com)
感谢阅读!
参考文献
[1] Qin, Libo, 等人。“端到端任务导向对话:任务、方法及未来方向的调查。” arXiv 预印本 arXiv:2311.09008 (2023)。
[2] 从用户需求生成提示。可在以下地址查看:langchain-ai.github.io/langgraph/tutorials/chatbots/information-gather-prompting
创意画布:使用 AI 绘画、编辑和风格化图像
我探索了用于创意性的 AI 图像转换的商业和开源照片编辑系统
·发表于Towards Data Science ·18 分钟阅读·2024 年 7 月 8 日
--

AI 生成的抽象画(左),InstructPix2Pix 修改版(中),和Runway ML 图像到图像(右),图像由作者提供
在过去四年里,我一直在尝试和写作关于 AI 和 ML 的创意用途。我研究并保持更新最新的 2D 和 3D 图像编辑、音乐创作和创意写作工具。我的工作聚焦于这些工具如何帮助并增强各种形式的艺术表达。
在这篇文章中,我探索了 AI/ML 在图像编辑中的应用,评估了商业工具和开源工具。我研究了 Adobe Photoshop 和 Runway ML,以及像 Stable Diffusion 2 和 InstructPix2Pix 这样的开源系统。每个工具都提供了各种图像编辑功能,例如修补和风格迁移。我的目标是评估它们的实际应用、可用性和局限性,以了解它们如何帮助创意表达。我通过编辑我为之前的文章创作的图像进行了实验,展示了这些工具在增强和修改现有艺术作品方面的潜力。
在下面的章节中,我将展示如何使用 AI 模型通过文本提示进行图像修补和编辑。我还将讨论这些系统的许可协议以及社会…
使用不同抽样技术的信用卡欺诈检测
信用卡
如何处理不平衡数据
·发表于 Towards Data Science ·10 分钟阅读·2024 年 12 月 15 日
--

图片由 Bermix Studio 提供,来自 Unsplash
信用卡欺诈检测是所有金融机构面临的一个问题。一般来说,欺诈检测非常具有挑战性,因为欺诈者不断提出新的创新方式来进行欺诈,因此很难找到可以检测到的模式。例如,在图示中,所有图标看起来都一样,但其中有一个图标与其他图标略有不同,我们需要找出那个。你能找出来吗?

这就是计划:

图片来源:作者
在此背景下,让我为今天的内容提供一个计划,并向你介绍在我们的用例“信用卡欺诈检测”中你将学到的内容:
1. 什么是数据不平衡
2. 数据不平衡的可能原因
3. 为什么类别不平衡在机器学习中是一个问题
使用 Python 裁剪 Landsat 场景的边界框
使用 stac 文件移除 Landsat 卫星图像的外边框
·发表于 Towards Data Science ·阅读时间 8 分钟·2024 年 2 月 4 日
--

(来源:作者)
用卫星图像讲故事是非常直接的。迷人的景观做了大部分工作。然而,视觉化它们需要一些工作,例如 选择和缩放 RGB 通道。在这篇文章中,我们将更进一步。我们将看到如何去除那个丑陋的边界框。具体来说,我们将:
-
使用stac文件裁剪和旋转 Landsat 场景
-
讨论如何在保持像素地理位置的同时完成此操作
我们将讨论一些关键的 Python 代码,并且你可以在 GitHub 上找到完整的项目。
下载 Landsat 场景
我们首先下载一个 Landsat 场景。你可以通过 EarthExplorer 门户网站来实现。如果你想使用 Python,下面的文章会指导你完成整个过程:
使用 landsatxplore Python 包简化 Landsat 场景下载
towardsdatascience.com
最终,你应该会有一个像图 1这样的文件夹。这里面包含了所有文件…
使用 XGBoost 进行交叉验证——通过 Tidymodels 增强客户流失分类
使用 XGBoost 和 Tidymodels 实施交叉验证、特征工程和模型评估的逐步指南
·发表于Towards Data Science ·阅读时间:6 分钟·2024 年 6 月 6 日
--

在我之前的文章中,我展示了如何使用逻辑回归和随机森林构建客户流失预测模型。在本文中,我想展示如何通过交叉验证,确保模型的表现不依赖于单一的训练/测试集划分,从而使建模过程更加稳健。
什么是交叉验证?
交叉验证通过将数据划分为训练集和测试集来实现,模型在不同的子集上进行拟合。其目的是评估模型的表现,并确保其在未见过的数据上的泛化能力。这还可以帮助防止过拟合,尤其是在模型在训练集上的表现远优于测试集时,从而提高模型的鲁棒性。
由于我在上一篇文章中介绍了逻辑回归和随机森林,这篇文章将展示如何使用相同的数据集进行 XGBoost 的交叉验证——银行流失数据的二分类,由 Simarpreet Singh 提供,并根据创作共用署名 4.0协议进行授权…
求职数据分析
从 20 年来最糟糕的技术职位市场中学到的教训
·发表于Towards Data Science ·阅读时间:8 分钟·2024 年 10 月 11 日
--
长话短说. . .
. . . 这个职位市场很艰难。真的很艰难。我们都知道过去几年的过山车般的变化,尤其是对于那些从事技术工作的人来说。我们从创纪录的薪资过渡到招聘冻结和裁员,所有这一切都发生在几个月之内。2023 年,技术行业裁员人数创下历史第二高,仅次于 2001 年的互联网泡沫破裂。尽管其他行业的招聘仍保持韧性,但当前的技术职位市场被许多人认为是自世纪之交以来最糟糕的。
多轮裁员的影响导致大量技术工作者申请数量较少的空缺岗位。再加上许多大型雇主实施的极不受欢迎的返办公室要求,几乎每个人都在寻找新工作。这导致了对少数岗位的竞争加剧,薪资和福利似乎在下降。雇主有能力变得更加挑剔,面试过程也变得更长更复杂。
求职者追踪系统和人工智能简历筛选器已经成为常态,创造了一个循环,申请数百个职位变得很容易,但几乎不可能让简历被真正的人看到。许多职位可能根本就不是“真实”的工作,有些公司发布的职位永远也不会被填补。
SQL 中的 CTE 与子查询——3 个实用技巧帮助做出正确选择
数据科学
掌握 CTE 和子查询之间的关键区别,准确了解何时以及如何使用它们!
·发表于 Towards Data Science ·6 分钟阅读·2024 年 9 月 16 日
--

照片由作者拍摄
我该在 SQL 中使用 CTE 还是子查询?——你一定经常会问这个问题。
我也有过这样的困惑!
子查询,顾名思义,是在另一个查询内部的查询,而 CTE 或称公共表表达式则是一个临时结果集,你需要单独定义。无论是子查询还是 CTE,都可以像使用简单的筛选一样简单,也可以像进行复杂的数据转换一样复杂。
而这种相似性常常是关于为什么以及何时使用它们的混淆源。
当我刚开始接触数据科学时,我总是很难做出这个选择。然而,随着时间的推移,我意识到,这其实不在于“更好”,而是找到合适的方法来完成任务。
例如,当我在一个逻辑复杂的项目中工作,需要分解数据转换或处理层次数据时——我会使用 CTE。但当我需要进行一次性计算或筛选时,我会使用子查询。
关于 CTE 的详细讨论,我强烈推荐阅读——
AI 的 CUDA —— 直观且详尽的解释
人工智能 | GPU 编程 | AI 理论
从零开始的并行化 AI
·发布于 Towards Data Science ·47 分钟阅读·2024 年 6 月 14 日
--

“Thread Master”由 Daniel Warfield 使用 Midjourney 创建。除非另有说明,所有图片均由作者提供。文章最初发布于 Intuitively and Exhaustively Explained。
在这篇文章中,我们将使用 CUDA 在 GPU 上训练 AI 模型,基本上是从零开始实现 AI,几乎不需要任何先验知识。
首先,我们将探讨现代计算机的一些核心组件,然后深入 GPU,讲解它是什么、如何工作以及它为何对 AI 很有用。接下来,我们将介绍 CUDA,解释它是什么以及它如何使我们能够编写利用 CPU 和 GPU 的应用程序。在理解 CUDA 编程的基本原理之后,我们将使用 CUDA 来构建、训练并测试一个用于分类任务的神经网络。
这篇文章适合谁? 任何想深入理解 AI 的人。
这篇文章有多高阶? 鉴于其内容较为高级,本篇文章可能更适合具有一定机器学习经验的人。如果你没有机器学习经验,阅读这篇文章你也一定会学到很多。只需要一节一节地读,遇到不懂的就多查查 Google。
前提条件: 基本的软件开发技能。有一定的 C++ 接触可能会有帮助,但并不是必须的。它……
构建一个自定义 AI Jira 代理
我是如何使用 Google Mesop、Django、LangChain Agents、CO-STAR 以及链式思维(CoT)提示结合 Jira API 来更好地自动化 Jira 的。
·发表于Towards Data Science ·阅读时间 19 分钟·2024 年 12 月 25 日
--

图片由Google DeepMind提供,来源于Unsplash
这个项目的灵感来自于我为内部用户开发的一个 Web 应用程序中托管的 Jira 工单创建工具。我还在系统错误时加入了自动创建 Jira 工单的功能。
用户和系统错误通常会创建类似的工单,因此我想看看 LLM 的推理能力是否可以用来通过链接相关问题、创建用户故事、验收标准和优先级,自动分流工单。
此外,给用户和产品/管理利益相关者提供更便捷的方式,通过自然语言直接与 Jira 互动,而无需任何技术能力,也是一个很有趣的前景。
Jira已经在软件开发中无处不在,现在已成为项目管理的领先工具。
具体来说,大型语言模型(LLM)和智能代理研究的进展表明,在这一领域有机会实现显著的生产力提升。
与 Jira 相关的任务非常适合自动化,因为这些任务以文本形式呈现,具有高度重复性,风险较低,复杂性也较低。
在下文中,我将展示我的开源项目——AI Jira 助手:一个与 Jira 交互的聊天界面,通过自定义 AI 代理工具来分诊新创建的 Jira 票据。
所有代码都已通过文章末尾的 GitHub 仓库提供。
该项目利用 LangChain 代理,通过 Django(配合 PostgreSQL)和 Google Mesop 提供服务。服务在 Docker 中运行,以便本地使用。
提示策略包括 CO-STAR 系统提示、链式思维(CoT)推理和少量示例提示。
本文将包括以下章节。
-
定义
-
Mesop 接口:Streamlit 还是 Mesop?
-
Django REST 框架
-
自定义 LangChain 代理工具和提示
-
Jira API 示例
-
下一步
1. 定义
首先,我想介绍一些对项目至关重要的高层定义。
AI Jira 助手
本文展示的开源项目,在本地运行时如下所示。
包括一个用于用户提示的聊天界面,示例提示预填充聊天界面,一个用于显示模型响应的框,以及一个清除模型响应的按钮。
讨论了项目中各个技术难题的代码片段。

AI Jira 助手(图像由作者提供)
什么是 Google Mesop?
Mesop 是一个相对较新的(2023 年)Python web 框架,Google 用于快速 AI 应用程序开发。
“Mesop 提供 30 个多功能组件,从低级构建模块到高级 AI 聚焦组件。这个灵活性使您能够快速原型化机器学习应用程序或构建自定义 UI,所有这些都在一个适应您项目用例的单一框架内进行。” — Mesop 首页
什么是 AI 代理?
代理软件范式的起源来自“代理”一词,指的是一种能够观察其环境并采取行动的软件程序。
“人工智能(AI)代理是一种能够与其环境交互、收集数据并使用这些数据执行自我决定任务以实现预定目标的软件程序。”
“人类设定目标,但 AI 代理独立选择为实现这些目标所需执行的最佳行动。”
AI 代理是理性代理。它们基于感知和数据做出理性决策,以产生最佳表现和结果。
“AI 代理通过物理或软件接口感知其环境。” — AWS 官网
什么是 CO-STAR 提示?
本文是一个提示格式指南,要求包含以下标题:上下文、目标、风格、语气、受众和回应。广泛接受这种格式,以提升大型语言模型(LLM)的输出质量。
“CO-STAR 框架是 GovTech 新加坡的数据科学与 AI 团队的创意,是一个便捷的模板,用于构建提示结构。”
它考虑了所有影响 LLM 响应效果和相关性的关键因素,从而生成更优化的响应。” — Sheila Teo 的 Medium 文章
什么是链式思维(CoT)提示?
最初由谷歌的一篇论文提出;Wei 等人(2022 年),链式思维(CoT)提示意味着提供少量示例的中间推理步骤。这被证明能够改善模型输出的常识推理能力。
什么是 Django?
Django 是一个广泛使用的复杂 Python 框架之一。
“Django 是一个高级 Python Web 框架,鼓励快速开发和干净、务实的设计。它是免费的,也是开源的。” — Django 官网
什么是 LangChain?
LangChain 是支持 LLM 应用程序的更为知名的开源库之一,包括与本项目相关的代理和提示。
“LangChain 的灵活抽象和以 AI 为先的工具包使其成为开发者在构建生成式 AI 时的首选工具。”
加入超过 1 百万的构建者,在 LangChain 的 Python 和 JavaScript 框架中标准化他们的 LLM 应用开发。” — LangChain 官网
2. Mesop 接口:Streamlit 还是 Mesop?

图片由Konsepta Studio提供,发布于Unsplash
我在专业工作中广泛使用了Streamlit来托管生成式 AI 应用,我的一个工作示例可以在这里找到。
从高层次来看,Streamlit 是一个类似的开源 Python Web 框架。
关于 Streamlit 的更多信息,请参见我在 Medium 上的另一篇文章,那里详细讨论了这一话题。
[## AI 资助申请写作工具 — AutoGen, PostgreSQL RAG, LangChain, FastAPI 和 Streamlit]
写资助申请可能是一个既耗时又乏味的工作,生成式 AI 似乎是一个自然的解决方案,尽管它可能显得有些天真……
这是第一次在实际使用中应用 Mesop——所以我认为做个比较可能会很有用。
Mesop 旨在提供对组件的 CSS 样式更精细的控制,并且原生集成了 JS Web 评论。Mesop 还提供了有用的调试工具,可以在本地运行时使用。从经验来看,我还想说,多个页面应用功能的使用要更为便捷。
然而,这确实意味着,对于不太精通 CSS 样式的机器学习从业者(包括我自己),存在更大的门槛。Streamlit 也有更大的社区支持。
从代码片段来看,我们可以设置不同的页面路由。项目只包含两个页面:主页面和错误页面。
import mesop as me
# local imports
try:
from .utils import ui_components
except Exception:
from utils import ui_components
@me.page(path="/")
def page(security_policy=me.SecurityPolicy(dangerously_disable_trusted_types=True)):
with me.box(
style=me.Style(
background="#fff",
min_height="calc(100% - 48px)",
padding=me.Padding(bottom=16),
)
):
with me.box(
style=me.Style(
width="min(800px, 100%)",
margin=me.Margin.symmetric(horizontal="auto"),
padding=me.Padding.symmetric(
horizontal=16,
),
)
):
ui_components.header_text()
ui_components.example_row()
ui_components.chat_input()
ui_components.output()
ui_components.clear_output()
ui_components.footer()
@me.page(path="/error")
def error(security_policy=me.SecurityPolicy(dangerously_disable_trusted_types=True)):
with me.box(
style=me.Style(
background="#fff",
min_height="calc(100% - 48px)",
padding=me.Padding(bottom=16),
)
):
with me.box(
style=me.Style(
width="min(720px, 100%)",
margin=me.Margin.symmetric(horizontal="auto"),
padding=me.Padding.symmetric(
horizontal=16,
),
)
):
ui_components.header_text()
ui_components.render_error_page()
ui_components.footer()
错误页面包含一个按钮,用于重定向到主页。

AI Jira 助手错误页面(图片来自作者)
触发重定向到主页的代码在这里包含。
def navigate_home(event: me.ClickEvent):
me.navigate("/")
def render_error_page():
is_mobile = me.viewport_size().width < 640
with me.box(
style=me.Style(
position="sticky",
width="100%",
display="block",
height="100%",
font_size=50,
text_align="center",
flex_direction="column" if is_mobile else "row",
gap=10,
margin=me.Margin(bottom=30),
)
):
me.text(
"AN ERROR HAS OCCURRED",
style=me.Style(
text_align="center",
font_size=30,
font_weight=700,
padding=me.Padding.all(8),
background="white",
justify_content="center",
display="flex",
width="100%",
),
)
me.button(
"Navigate to home page",
type="flat",
on_click=navigate_home
)
我们还必须创建 State 类,这样可以在事件循环中保持数据的持久性。
import mesop as me
@me.stateclass
class State:
input: str
output: str
in_progress: bool
要清除界面中的模型输出,我们可以将输出变量赋值为空字符串。也有不同的按钮支持类型,截至撰写时,包括:默认、提升、平面和带边框。
def clear_output():
with me.box(style=me.Style(margin=me.Margin.all(15))):
with me.box(style=me.Style(display="flex", flex_direction="row", gap=12)):
me.button("Clear output", type="flat", on_click=delete_state_helper)
def delete_state_helper(ClickEvent):
config.State.output = ""
为了自动填充聊天界面中的示例提示,我们使用按钮的 onclick 事件,通过更新状态来实现。
def example_row():
is_mobile = me.viewport_size().width < 640
with me.box(
style=me.Style(
display="flex",
flex_direction="column" if is_mobile else "row",
gap=10,
margin=me.Margin(bottom=40),
)
):
for example in config.EXAMPLE_PROMPTS:
prompt_box(example, is_mobile)
def prompt_box(example: str, is_mobile: bool):
with me.box(
style=me.Style(
width="100%" if is_mobile else 200,
height=250,
text_align="center",
background="#F0F4F9",
padding=me.Padding.all(16),
font_weight=500,
line_height="1.5",
border_radius=16,
cursor="pointer",
),
key=example,
on_click=click_prompt_box,
):
me.text(example)
def click_prompt_box(e: me.ClickEvent):
config.State.input = e.key
类似地,要向 Django 服务发送请求,我们使用以下代码片段。我们使用海象运算符 (:=) 来判断请求是否收到了有效的响应(即状态码为 200 且非 None),并将输出追加到状态中,以便在 UI 中渲染,否则我们会将用户重定向到前面讨论的错误页面。
def chat_input():
with me.box(
style=me.Style(
padding=me.Padding.all(8),
background="white",
display="flex",
width="100%",
border=me.Border.all(me.BorderSide(width=0, style="solid", color="black")),
border_radius=12,
box_shadow="0 10px 20px #0000000a, 0 2px 6px #0000000a, 0 0 1px #0000000a",
)
):
with me.box(
style=me.Style(
flex_grow=1,
)
):
me.native_textarea(
value=config.State.input,
autosize=True,
min_rows=4,
placeholder="Enter your prompt",
style=me.Style(
padding=me.Padding(top=16, left=16),
background="white",
outline="none",
width="100%",
overflow_y="auto",
border=me.Border.all(
me.BorderSide(style="none"),
),
),
on_blur=textarea_on_blur,
)
with me.content_button(type="icon", on_click=click_send):
me.icon("send")
def click_send(e: me.ClickEvent):
if not config.State.input:
return
config.State.in_progress = True
input = config.State.input
config.State.input = ""
yield
if result := api_utils.call_jira_agent(input):
config.State.output += result
else:
me.navigate("/error")
config.State.in_progress = False
yield
def textarea_on_blur(e: me.InputBlurEvent):
config.State.input = e.value
为了完整起见,我提供了向 Django 端点发送请求的代码,用于运行 AI Jira Agent。
import requests
# local imports
from . import config
def call_jira_agent(request):
try:
data = {"request": request}
if (response := requests.post(f"{config.DJANGO_URL}api/jira-agent/", data=data)) and \
(response.status_code == 200) and \
(output := response.json().get("output")):
return f"Request: {request}<br>Output: {output}<br><br>"
except Exception as e:
print(f"ERROR call_jira_agent: {e}")
为了在本地运行,我已包含了相关的 Docker 和 Docker compose 文件。
运行 Mesop 的 Docker 文件是通过 Mesop 项目主页提供的。
Docker compose 文件由三个服务组成:后端 Django 应用、前端 Mesop 应用和一个与 Django 应用配合使用的 PostgreSQL 数据库实例。
我想特别指出传递给 Mesop Docker 容器的环境变量,PYTHONUNBUFFERED=1 确保 Python 的输出、标准输出(stdout)和标准错误输出(stderr)流被发送到终端。在使用 Mesop 应用程序推荐的 Docker 镜像时,我花了一些时间才找出未能看到应用程序输出的根本原因。
DOCKER_RUNNING=true 环境变量是一种约定,用于简单地判断应用程序是否在 Docker 中运行,或者例如在虚拟环境中运行。
需要指出的是,环境变量将通过配置文件 'config.ini' 填充,该文件位于 Docker compose 文件中的 env_file 元素所引用的 config 子目录中。
要运行该项目,你必须用你的 Open AI 和 Jira 凭证填充这个配置文件。
3. Django REST 框架

Django 是一个 Python web 框架,内置了大量有用的功能。
它可以与 Flask 或 FastAPI 等框架进行比较,尽管它需要一些额外的配置,并且入门时学习曲线较陡峭。
如果你想了解更多关于 Flask 的内容,请参阅我下面的文章。
[## Canal Boat Pricing With Catboost Machine Learning
来自 YouTube 推荐引擎的一个更有趣的建议,结合了完全远程工作的转变……
在本文中,我将涵盖应用程序、模型、序列化器、视图以及 PostgreSQL 数据库的集成。
一个应用程序是一个逻辑上分离的 web 应用,具有特定的目的。
在我们的实例中,我们将应用命名为“api”,并通过运行以下命令创建。
django-admin startapp api
在 views.py 文件中,我们定义了我们的 API 端点。
“视图函数,简称 视图,是一个 Python 函数,它接收一个 web 请求并返回一个 web 响应。这个响应可以是一个网页的 HTML 内容,或者是重定向,或者是 404 错误,或者是 XML 文档,或者是图片……实际上可以是任何内容。视图本身包含返回该响应所需的任意逻辑。” — Django 官网
路由到 Django 视图的端点定义在应用的 urls.py 文件中,如下所示。urls.py 文件是在应用初始化时创建的。我们在这个项目中有三个端点;一个健康检查端点,一个用于返回数据库中所有记录的端点,以及一个用于处理调用 AI 代理的端点。
视图是声明为类的,这是 Django 中的标准约定。请查看完整的文件内容。
大部分代码都不言自明,尽管这个代码片段很重要,因为它将把模型的数据保存到数据库中。
modelRequest = models.ModelRequest(request=request, response=response)
modelRequest.save()
以下代码片段返回数据库中 ModelRequest 模型的所有记录,接下来我将介绍模型。
class GetRecords(APIView):
def get(self, request):
"""Get request records endpoint"""
data = models.ModelRequest.objects.all().values()
return Response({'result': str(data)})
“模型是关于你的数据的唯一、权威的信息来源。它包含了你存储的数据的基本字段和行为。通常,每个模型对应一个数据库表。” — Django 官网
我们的模型对于这个项目来说很简单,因为我们只需要存储用户请求和最终模型输出,这两个字段都是文本字段。
str 方法是 Python 中的一个常见约定,例如,它在 print 函数中默认被调用。该方法的目的是返回对象的可读字符串表示。
序列化器将模型中的字段映射到验证输入和输出,并将更复杂的数据类型转换为 Python 数据类型。这可以在之前详细介绍的 views.py 中看到。
“ModelSerializer 通常指的是 Django REST 框架(DRF)中的一个组件。Django REST 框架是一个用于在 Django 应用中构建 Web API 的流行工具包。它提供了一组工具和库,以简化构建 API 的过程,其中包括序列化器。”
ModelSerializer 类提供了一种快捷方式,允许你自动创建一个 Serializer 类,该类的字段与 Model 字段相对应。
ModelSerializer 类与常规的 Serializer 类相同,不同之处在于:
它将基于模型自动生成一组字段。
它将自动为序列化器生成验证器,如 unique_together 验证器。
它包括.create()和.update()的简单默认实现。” — Geeks for geeks
项目的完整 serializers.py 文件如下所示。
对于 PostgreSQL 数据库集成,settings.py 文件中的配置必须与 database.ini 文件匹配。
默认的数据库设置必须更改为指向 PostgreSQL 数据库,因为这不是 Django 的默认数据库集成。
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': 'vectordb',
'USER': 'testuser',
'PASSWORD': 'testpwd',
'HOST': 'db' if DOCKER_RUNNING else '127.0.0.1',
'PORT': '5432',
}
}
database.ini 文件在初始化时定义了 PostgreSQL 数据库的配置。
为确保在 Docker 容器启动后应用数据库迁移,我们可以使用 bash 脚本来应用迁移,然后启动服务器。自动运行迁移将意味着每当 Django 源代码中的定义发生变化时,数据库总是会被修改,这从长远来看能节省时间。
Dockerfile 的入口点然后通过 CMD 指令修改为指向 bash 脚本。
4. 自定义 LangChain 代理工具和提示

照片由 Vlad Tchompalov 提供,来源于 Unsplash
我正在使用现有的 LangChain 代理功能,并结合 Jira 工具包,这是 Atlassian Python API 的一个封装。
默认库开箱即用相当有用,尽管有时需要在提示上进行一些反复试验,不过我认为随着该领域的研究进展,应该会有所改进。
然而,对于这个项目,我希望在代理中添加一些自定义工具。这可以通过下面的 triage 函数与 @tool 装饰器来实现。
工具的函数类型提示和注释描述是必要的,以便向代理传达调用时的预期。当返回的字符串被代理观察到时,在这个实例中,我们只是返回“任务完成”,以便代理停止执行下一个步骤。
自定义分诊工具执行以下步骤;
-
获取该项目的所有未解决的 Jira 工单
-
获取代理正在进行分诊的 Jira 问题键的描述和摘要
-
与所有未解决的工单进行基于异步 LLM 的比较,并通过文本到文本的比较自动标记那些看起来相关的工单,然后使用 Jira API 将它们链接起来
-
然后,使用 LLM 生成用户故事、验收标准和优先级,并将此模型结果作为注释留在主工单上
from langchain.agents import AgentType, initialize_agent
from langchain_community.agent_toolkits.jira.toolkit import JiraToolkit
from langchain_community.utilities.jira import JiraAPIWrapper
from langchain_openai import OpenAI
from langchain.tools import tool
from langchain_core.prompts import ChatPromptTemplate, FewShotChatMessagePromptTemplate
llm = OpenAI(temperature=0)
@tool
def triage(ticket_number:str) -> None:
"""triage a given ticket and link related tickets"""
ticket_number = str(ticket_number)
all_tickets = jira_utils.get_all_tickets()
primary_issue_key, primary_issue_data = jira_utils.get_ticket_data(ticket_number)
find_related_tickets(primary_issue_key, primary_issue_data, all_tickets)
user_stories_acceptance_criteria_priority(primary_issue_key, primary_issue_data)
return "Task complete"
jira = JiraAPIWrapper()
toolkit = JiraToolkit.from_jira_api_wrapper(jira)
agent = initialize_agent(
toolkit.get_tools() + [triage],
llm,
agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION,
verbose=True,
max_iterations=5,
return_intermediate_steps=True
)
这两个 LLM 任务都使用了 CO-STAR 系统提示和链式思维的少量示例提示策略。因此,我将这些任务抽象成了一个 LLMTask 类。
它们在以下代码片段中被实例化。可以说,我们本可以为每个任务尝试不同的 LLM,尽管出于时间考虑,我并没有进行相关实验——如果你拉取了代码库并有经验分享,请随时在下方评论。
class LLMTask:
def __init__(self, system_prompt, examples, llm):
self.system_prompt = system_prompt
self.examples = examples
self.llm = llm
def construct_prompt(self):
example_prompt = ChatPromptTemplate.from_messages(
[
("human", "{input}"),
("ai", "{output}"),
]
)
few_shot_prompt = FewShotChatMessagePromptTemplate(
example_prompt=example_prompt,
examples=self.examples,
)
return ChatPromptTemplate.from_messages(
[
("system", self.system_prompt),
few_shot_prompt,
("human", "{input}"),
]
)
def run_llm(self, input):
chain = self.construct_prompt() | self.llm
return chain.invoke({"input": input})
product_model = LLMTask(system_prompts.get("system_prompt_product"), example_prompts.get("examples_product"), llm)
linking_model = LLMTask(system_prompts.get("system_prompt_linking"), example_prompts.get("examples_linking"), llm)
对于链接任务,CO-STAR 系统提示如下。Context(上下文)、Objective(目标)、Style(风格)、Tone(语气)、Audience(受众)和 Response(响应)是 CO-STAR 方法的标准标题。我们定义了上下文和输出,包括标记模型结果的每个元素。
明确界定受众、风格和语气有助于确保模型输出适合商业环境。
# CONTEXT #
I want to triage newly created Jira tickets for our software company by comparing them to previous tickets.
The first ticket will be in <ticket1> tags and the second ticket will be in <ticket2> tags.
# OBJECTIVE #
Determine if two tickets are related if the issue describes similar tasks and return True in <related> tags, also include your thinking in <thought> tags.
# STYLE #
Keep reasoning concise but logical.
# TONE #
Create an informative tone.
# AUDIENCE #
The audience will be business stake holders, product stakeholders and software engineers.
# RESPONSE #
Return a boolean if you think the tickets are related in <related> tags and also return your thinking as to why you think the tickets are related in <thought> tags.
对于执行产品风格的工单评估(用户故事、验收标准和优先级),系统提示如下。我们明确将优先级定义为低(LOW)、中(MEDIUM)或高(HIGH)。
我们还要求模型具有产品负责人/经理的风格,因为这项任务通常由该职位执行。
# CONTEXT #
You are a product owner working in a large software company, you triage new tickets from their descriptions in <description> tags as they are raised from users.
# OBJECTIVE #
From the description in <description> tags, you should write the following; user stories in <user_stories> tags, acceptance criteria in <acceptance_criteria> tags and priority in <priority>.
Priority must be either LOW, MEDIUM OR HIGH depending on the what you deem is most appropriate for the given description.
Also include your thinking in <thought> tags for the priority.
# STYLE #
Should be in the style of a product owner or manager.
# TONE #
Use a professional and business oriented tone.
# AUDIENCE #
The audience will be business stake holders, product stakeholders and software engineers.
# RESPONSE #
Respond with the following format.
User stories in <user_stories> tags.
Acceptance criteria in <acceptance_criteria> tags.
Priority in <priority> tags.
现在,我将提供用于链接 Jira 工单的链式思维少量示例提示,我们将分别在 <issue1> 和 <issue2> 标签中附上两个工单的摘要和描述。模型的思考过程通过 <thought> 标签捕获,这就是链式思维元素。
少量示例(few-shot)这个术语来自于向模型输入多个示例的做法。
我们稍后会正则解析模型输出,并使用一个辅助函数通过 Jira API 链接相关工单,所有此项目的 Jira API 辅助函数将在文章后续部分提供。
"examples_linking": [
{
"input": "<issue1>Add Jira integration ticket creation Add a Jira creation widget to the front end of the website<issue1><issue2>Add a widget to the front end to create a Jira Add an integration to the front end to allow users to generated Jira tickets manually<issue2>",
"output": "<related>True<related><thought>Both tickets relate to a Jira creation widget, they must be duplicate tickets.<thought>"
},
{
"input": "<issue1>Front end spelling error There is a spelling error for the home page which should read 'Welcome to the homepage' rather than 'Wellcome to the homepage'<issue1><issue2>Latency issue there is a latency issue and the calls to the Open AI should be made asynchronous<issue2>",
"output": "<related>False<related><thought>The first ticket is in relation to a spelling error and the second is a latency, therefore they are not related.<thought>"
},
{
"input": "<issue1>Schema update We need to add a column for model requests and responses<issue1><issue2>Update schema to include both model requests and model responses Add to two new additional fields to the schema<issue2>",
"output": "<related>True<related><thought>Both tickets reference a schema update with two new fields for model requests and model responses, therefore they must be related.<thought>"
}
]
同样地,对于工单评估,用户故事被提供在<user_stories>标签中,验收标准在<acceptance_criteria>标签中,优先级在
所有示例都由我手动标注。
"examples_product": [
{
"input": "<description>Add Jira integration ticket creation Add a Jira creation widget to the front end of the website<description>",
"output": "<user_stories>As a user, I want to be able to create a Jira ticket directly from the website.\nAs a product owner, I want to streamline the process of creating Jira tickets for our team.<user_stories>\n<acceptance_criteria>The Jira creation widget should be easily accessible on the front end of the website.\nThe widget should allow users to input all necessary information for a Jira ticket, such as title, description, and assignee.\nOnce submitted, the widget should create a new Jira ticket in the designated project.\nThe widget should have proper error handling in case of any issues with creating the ticket.<acceptance_criteria>\n<priority>MEDIUM<priority>\n<thought>By adding a Jira integration and creation widget to the website, we can improve the efficiency and accuracy of creating Jira tickets for our team. This will ultimately save time and resources for both the business and the development team. Though there is no immediate need for this to be completed. Therefore, I believe this ticket should be prioritized as MEDIUM.<thought>"
},
{
"input": "<description>Urgent schema update We need to add a column for model requests and responses for compliance<description>",
"output": "<user_stories>As a developer, I want to be able to view and track model requests and responses for compliance purposes.\nAs a product owner, I want to ensure our system is compliant with regulations and standards.<user_stories>\n<acceptance_criteria>A new column should be added to the schema for model requests and responses.\nThe column should accurately track and store all relevant information for compliance purposes.\nThe column should be easily accessible and viewable for authorized users.\nThe update should not disrupt any existing functionality of the system.<acceptance_criteria>\n<priority>HIGH<priority>\n<thought>This ticket is marked as urgent, indicating that it is a high priority for the business. Compliance is a critical aspect of our business and it is important that we address this issue as soon as possible. By adding a new column for model requests and responses, we can ensure that our system is compliant and avoid any potential legal or regulatory issues. Therefore, I believe this ticket should be prioritized as HIGH.<thought>"
},
{
"input": "<description>Homepage CSS error There is a CSS error for the homepage which is affecting a call to action button and negatively impacting conversion<description>",
"output": "<user_stories>As a user, I want the CSS error on the homepage to be fixed so that I can easily access the call to action button.\nAs a product owner, I want to improve the conversion rate of our website by fixing any CSS errors.<user_stories>\n<acceptance_criteria>The CSS error should be identified and fixed on the homepage.\nThe call to action button should be easily visible and accessible for users.\nThe fix should not affect any other functionality of the website.<acceptance_criteria>\n<priority>HIGH<priority>\n<thought>This CSS error is directly impacting the conversion rate of our website, which is a key metric for our business. It is important that we address this issue as soon as possible to improve the user experience and ultimately increase conversions. Therefore, I believe this ticket should be prioritized as HIGH.<thought>"
}
],
这段代码使用了多线程方法来并发链接 Jira 问题。这将大大减少与项目中所有开放工单进行配对比较所需的时间,以确定它们是否相关。
def check_issue_and_link_helper(args):
key, data, primary_issue_key, primary_issue_data = args
if key != primary_issue_key and \
llm_check_ticket_match(primary_issue_data, data):
jira_utils.link_jira_issue(primary_issue_key, key)
def find_related_tickets(primary_issue_key, primary_issue_data, issues):
args = [(key, data, primary_issue_key, primary_issue_data) for key, data in issues.items()]
with concurrent.futures.ThreadPoolExecutor(os.cpu_count()) as executor:
executor.map(check_issue_and_link_helper, args)
def llm_check_ticket_match(ticket1, ticket2):
llm_result = linking_model.run_llm(f"<ticket1>{ticket1}<ticket1><ticket2>{ticket2}<ticket2>")
if ((result := jira_utils.extract_tag_helper(llm_result))) \
and (result == 'True'):
return True
工具的一个示例工作流程:创建工单并进行分诊。

工作工单创建和分诊(图片由作者提供)
这些操作的结果被记录在 Jira 工单中。相关的工单已被自动链接,用户故事、验收标准、优先级和思考过程已作为 Jira 评论进行记录。

已链接的工单、用户故事、验收标准和优先级(图片由作者提供)
我们可以在 Docker 容器的打印语句中看到代理的中间步骤。

代理输出的分诊步骤(图片由作者提供)
5. Jira API 示例

AI Jira 助手正在寻找进行中的工单(图片由作者提供)
在本项目中,所有我明确使用的Jira REST API示例都已在下方列出,供参考。
用于解析模型结果的正则表达式提取辅助函数也包含在内。此外,还有一个 Jira 的 Python SDK,尽管在此示例中我选择使用了 requests 库,以便更容易地转化为其他编程语言。
6. 下一步
自然的下一步是通过与源代码管理系统集成来实现代码生成,从而实现几乎完全自动化的软件开发生命周期。通过在此过程中加入人工参与,这可能是一个可行的解决方案。
我们已经可以看到,AI 代码生成正在对企业产生影响——如果业务日常任务可以部分自动化,那么软件开发人员/产品实践者就可以专注于更有趣和更有意义的工作。
如果这篇文章引起了广泛的兴趣,或许我可以将其作为后续项目进行研究。
谷歌 CEO 桑达尔·皮查伊表示,公司正在使用 AI 编写代码,然后由工程师进行审核。但这会如何发展……
我希望你觉得这篇文章有启发性,正如承诺的那样——你可以在 Github 仓库这里找到所有代码,也可以随时在LinkedIn上与我联系。
参考资料
除非另有说明,所有图片均由作者提供。
为了更安全的代码更改创建自定义预提交钩子
编写你的第一个预提交钩子的逐步指南
·发表于Towards Data Science ·8 分钟阅读·2024 年 3 月 14 日
--

预提交运行结果,包括我们的 Hamilton 钩子!
大多数软件使用git版本控制系统来更新和分发代码。一个协作编写代码的挑战是,在每个贡献者有自己关于什么是干净代码的风格和看法时,如何确保遵循特定的标准。
预提交钩子是一些在提交代码更改之前自动执行的脚本或命令。它们可以强制执行样式规则,并在代码提交和分发之前捕捉错误。值得注意的钩子包括检查文件的语法错误、排序导入以及规范化引号。它们是任何项目中不可或缺的工具,尤其是那些有多个贡献者的开源项目。
为什么要创建自定义的预提交钩子?
我想创建预提交钩子来验证 Python 库Hamilton的数据流定义,但我发现大多数在线资源分散且仅限于基本用法。
在这篇文章中,你将找到:
-
如何在你的项目中开始使用预提交钩子
-
开发自定义预提交钩子的逐步教程
为了引导讨论,我将通过这个 GitHub 仓库介绍我为 Hamilton 开发的预提交钩子。
开始使用预提交钩子
钩子是直接内嵌在git版本控制系统中的机制。你可以在.git/hooks目录下找到项目的钩子(该目录默认可能是隐藏的)。虽然通常称之为“预提交钩子”,但 git 钩子涵盖了整个git 生命周期。例如,你可以在提交后或推送前触发钩子。此外,钩子可以用任何编程语言编写。特别地,Ruff库为了性能提升,使用 Rust 重新实现了许多基于 Python 的钩子。
与专注于代码行为的软件测试不同,钩子可以看作是你在每次保存文件时执行的轻量级检查。虽然你可以预期测试会随着代码库的变化而变化,但你的代码编写指南和预提交钩子可能会保持不变。
项目设置
假设我们在目录/my-project中开始一个新的 Python 项目(或使用一个现有项目)。与预提交钩子协作的推荐方式是通过pre-commit Python 库。我们可以通过以下步骤进行设置:
-
使用
git init为你的项目创建一个 git 仓库 -
使用
pip install pre-commit安装预提交库 -
将
.pre-commit-config.yaml添加到你的代码库中。以下是一个示例:
# .pre-commit-config.yaml
repos:
# repository with hook definitions
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v2.3.0 # release version of the repo
hooks: # list of hooks from the repo to include in this project
- id: end-of-file-fixer
- id: trailing-whitespace
- id: check-yaml
args: ['--unsafe'] # add arguments to `check-yaml`
# download another repository with hooks
- repo: https://github.com/psf/black
rev: 22.10.0
hooks:
- id: black
4. 使用pre-commit install安装钩子。它将从.pre-commit-config.yaml读取指令,并在.git/hooks/pre-commit中本地安装钩子
5. 提交更改或手动运行钩子,使用pre-commit run --all-files来触发钩子
创建一个自定义的预提交钩子
社区维护的钩子提供了灵活性,可以根据你的编码规范进行定制。它们通常能满足你 98%的需求。然而,现成的解决方案并不了解你使用的具体工具或团队的内部惯例。例如,你可能希望验证内部配置或强制执行项目的目录结构。
在我们的案例中,我们希望创建一个钩子来验证 Python 代码中的 Hamilton 数据流定义。我们的钩子脚本将利用[hamilton](https://blog.dagworks.io/p/a-command-line-tool-to-improve-your) CLI 工具来进行验证,这将为我们提供一个简单的代码示例。
1. 设置你的预提交钩子仓库
如项目设置部分所介绍,要使项目能够在.pre-commit-config.yaml中引用并通过pre-commit install本地安装,预提交钩子需要存在于公共代码库中。
之前,我们在项目目录/my-project中定义了.pre-commit-config.yaml并安装了钩子。现在,我们将创建一个/my-hooks目录,在其中定义我们自定义的钩子。你可以参考我们的[hamilton-pre-commit](https://github.com/DAGWorks-Inc/hamilton-pre-commit) 仓库查看整体结构。

2. 编写钩子的逻辑
在hooks/目录下,我们有一个文件__init__.py来使目录成为可发现的 Python 模块,以及我们的脚本cli_command.py。它包含一个单一的函数main(),该函数从sys.argv读取一组hamilton CLI 命令。然后,它将这些命令逐一作为子进程执行,并封装在一个try/except语句中。
# hooks/cli_command.py
import sys
import json
import subprocess
PASS = 0
FAIL = 1
def main() -> int:
"""Execute a list of commands using the Hamilton CLI"""
commands = sys.argv[1:]
if len(commands) == 0:
return PASS
exit_code = PASS
for command in commands:
try:
args = command.split(" ")
# insert `--json-out` for proper stdout parsing
args.insert(1, "--json-out")
result = subprocess.run(args, stdout=subprocess.PIPE, text=True)
response = json.loads(result.stdout)
if response["success"] is False:
raise ValueError
except Exception:
exit_code |= FAIL
return exit_code
if __name__ == "__main__":
raise SystemExit(main())
一开始,我们将exit_code = PASS,但任何异常或失败的命令都会将exit_code = FAIL。main()函数将退出码返回给SystemExit异常。为了使预提交钩子成功,我们需要在所有命令成功后返回PASS。将PASS=0和FAIL=1的做法可能违背直觉,但这些值是标准的系统退出码。
我们使用 Python 是为了方便,但这个简单的逻辑也可以用像 Bash 这样的轻量级脚本语言来实现。你可以访问预提交团队维护的钩子查看更多示例。
3. 定义钩子的入口点
现在,你的钩子仓库(/my-hooks)必须包含一个.pre-commit-hooks.yaml文件,该文件指定了可用的钩子以及如何在安装后执行它们。
- id: cli-command
name: Execute `hamilton` CLI commands
description: This hook executes a command using the `hamilton` CLI.
entry: cli-command
language: python
types: [python]
stages: [pre-commit, pre-merge-commit, manual]
pass_filenames: false
在我们的案例中,我们设置了id: cli-command和entry: cli-command,添加了一些元数据,并将编程语言指定为 Python。重要的是,files属性没有设置为让我们的钩子在每次提交时运行。在你的情况下,你可能希望将files: "*.py"设置为在每次编辑的 Python 文件上运行你的钩子(了解更多可用选项)。
到目前为止,我们在hooks/cli_command.py下创建了一个 Python 脚本,并在.pre-commit-hooks.yaml中添加了一个入口点为cli-command的钩子。然而,你需要在 Python 项目的文件pyproject.toml中显式地将这两者连接起来。
[project.scripts]
cli-command = "hooks.cli_command:main"
这一行的意思是“入口点cli-command指向hooks.cli_command中的main函数”。
如果你在 Python 项目中使用
setup.cfg,可以参考这个示例。
4. 本地测试你的钩子
首先,你应该通过单元测试验证钩子逻辑。然而,我们不会深入讨论测试,因为它值得单独的一篇文章。我们的hamilton-pre-commit仓库目前没有测试,因为底层 CLI 在主 Hamilton 仓库中已进行测试。你可以访问官方维护的 pre-commit 钩子查看测试示例。
其次,你应该通过在本地尝试运行你的 pre-commit 钩子来验证.pre-commit-hooks.yaml和入口点是否正确配置。理想情况下,你应该避免每次想测试更改时都添加提交来触发钩子。pre-commit 库提供了一些工具来简化这个过程,但它需要一些手动步骤,详细步骤可以参考pre-commit GitHub 问题。
-
转到你希望测试钩子的目录
/my-project。 -
执行
pre-commit try-repo ../LOCAL/PATH/TO/my-hooks,然后你应该会看到一个本地初始化消息。

一个限制是,你不能通过此命令直接将args传递给钩子。
3. 将Using config:下找到的配置复制到本地文件,并添加args部分。我们创建了.local-pre-commit-config.yaml,但你可以使用任何名称。
# my-project/.local-pre-commit-config.yaml
repos:
- repo: ../../dagworks/hamilton-pre-commit
rev: e4b77a499ba0ff3446a86ebbe4c2cbca82eb54f8
hooks:
- id: cli-command
args: [
hamilton build my_func2.py
]
4. 使用本地钩子通过pre-commit run --config .local-pre-commit-config.yaml --all-files。--all-files标志会将钩子应用到仓库中的所有文件,而不是仅应用于当前暂存的文件。

添加测试时,始终从让测试失败开始。你可不想添加一个总是成功的测试
(:^)
5. 发布你的 pre-commit 钩子
你快完成了!你有一个工作的钩子脚本,已经在 git 仓库中进行了测试和打包。现在,你只需将它发布到线上即可。我们将展示 GitHub 托管项目的步骤,但你的 pre-commit 钩子可以放在任何可以通过git clone访问的地方。
- 从你的 GitHub 仓库,进入发布部分

GitHub 仓库的主页。
2. 点击草拟新版本

GitHub 仓库的发布部分
3. 在新发布页面上,你需要添加版本标签、标题和描述。如果这是你的第一次发布,我建议将标签设置为v0.1.0,以遵循语义版本控制,这是 GitHub 推荐的做法。
当你在进行更改并想要发布实验版本时,你可以将版本设置为v0.1.1-rc(即“发布候选版”),并使用复选框将其标记为预发布版本。

GitHub 上的新版本表单。
.pre-commit-config.yaml文件中的rev值需要与你设置的版本标签匹配。
repos:
- repo: https://github.com/DAGWorks-Inc/hamilton-pre-commit
rev: v0.1.3rc
hooks:
- id: cli-command
# ...
结束语
恭喜!你已经完成了这篇文章!现在,你可以使用 pre-commit 钩子来提高你项目中的代码质量。了解了它们的内部机制后,你可以开始编写自己的钩子了!
在重新发明轮子之前,不要忘记查看社区维护的许多钩子:pre-commit.com/hooks.html
查看Hamilton库,以便用 Python 编写数据流!
在LinkedIn上找到我,并查看我在DAGWorks 博客上的更多文章
客户流失:如何定义客户未告知其离开的流失
作者详细说明了在某些情况下如何定义客户流失,这些情况下流失并不明显:零售和银行业。
·发表于Towards Data Science ·阅读时长 9 分钟·2024 年 2 月 15 日
--

图片来源:Mantas Hesthaven 由Unsplash提供
介绍
留住客户比获取新客户更容易且成本更低(查看这里 和 查看这里)。话虽如此,客户保持策略并非免费,最有效的策略仍然可能需要花费不少。由此可见,企业必须识别出最容易流失的客户,并集中精力在这个特定的客户群体上。这就是著名的流失模型,或称流失预测模型发挥作用的地方。它通常是数据科学团队在营销背景下首个需要构建的模型之一。
互联网和电话服务提供商、视频和音频流媒体网站、实体和在线新闻出版物、剃须俱乐部……这些都是客户明确告知公司他们要离开它们的例子——客户通过取消订阅或会员资格来告知公司他们的离开。
然而,在许多其他情况下,行业面临着如何定义流失的问题,尤其是当客户悄然脱离服务或产品时。很难判断客户何时已流失…
使用人工智能进行顾客画像:通过 OpenAI 从日常清单构建购物券
这就是我如何在 Python 中使用 OpenAI API,通过几行代码创建优惠券。
·发表于Towards Data Science ·阅读时间:9 分钟·2024 年 1 月 21 日
--

图像由作者使用Midjourney制作
我来自意大利,最近刚成为美国的永久居民。我住在俄亥俄州,并且从一家知名的大型超市购买食品。
引起我注意的一件事是,收银员要求顾客注册他们的购物卡,并设置用户名和条形码。
这张购物卡是奖励那些在购买日常生活用品时,能比没有卡的人节省更多钱的顾客。
这也非常好,因为它可以节省在购物中心旁的加油站的油费。
当然,作为一名数据科学家(而且还是个相当痴迷的科学家),我开始思考将我的所有购物清单保存在一张卡上的含义。这让我想到了这样一句话:
“数据是新的石油”
作者:Clive Humby,意思是,对于非数据科学家来说,一个人的购物清单只是一个八卦工具,而对于数据科学家来说,它是做顾客画像 的工具,去描绘这个购物清单背后的顾客。
客户细分项目:训练、测试、调整、重复
使用 K-Means 聚类学习客户细分。
·发布于Towards Data Science ·阅读时间 33 分钟·2024 年 8 月 23 日
--

我现在为你带来一个数据科学在营销中的应用——客户基础细分(聚类)项目。这个项目是一个高层次的项目,可以应用于任何需要理解客户差异的公司。知道公司迫切想要 👹 了解他们的客户是谁。 你认为所有客户都一样吗?
不,客户并不都一样。他们有不同的口味、不同的偏好,而且他们处在人生的不同阶段,在这些阶段,某些产品特性可能会有更大或更小的价值。
但是,首先,我诚恳地请求你,如果你能👏支持这个项目,对我帮助非常大,真的。我重申,这项我系统化展示和解释的工作,是为了那些想要学习的人;如果只是为了我自己……好吧,那不过是乱写一通。无论如何,我依赖你的支持。接着说……
细分目的
细分将具有相似特征的客户分为一组。例如,在考虑同一产品时:
-
某个客户可能因为其当前的生活阶段,而更看重价格。
-
另一个可能更看重质量而非成本,理解……
定制大型语言模型

图片来源:作者(Ideogram)
使用 OLLAMA 和 Modelfile 定制、运行并保存 LLM
·发表于 Towards Data Science ·阅读时长:11 分钟·2024 年 3 月 31 日
--
在这篇文章中,我将向你展示如何在 Ollama 中使用 Modelfile 来更改与现有 LLM(Llama2)交互时的行为。我还将展示如何将你新定制的模型保存到 Ollama 服务器上的个人命名空间。
我知道有很多不同的“llama”可能会让人有点困惑。只需记住,Ollama 是一家让你能够下载并在本地运行多种不同 LLM 的公司。而 Llama2 是 Meta(Facebook 的拥有者)创建的一个特定 LLM。除了这种关系,它们之间没有其他关联。
如果你之前从未听说过 Ollama,我建议你查看下面我的文章,我将在文章中详细讲解 Ollama 是什么,以及如何在你的系统上安装它。
使用 Ollama 在本地运行 LLM
levelup.gitconnected.com](https://levelup.gitconnected.com/introduction-to-ollama-part-1-1156f9563b8d?source=post_page-----612bbe13bb20--------------------------------)
什么是 modelfile?
在 Ollama 中,modelfile是指一个配置文件,用于定义创建和共享模型的蓝图。
使用 Docker Compose 定制 RStudio 容器
一步步指南,帮助你定制和启动在容器内运行的 RStudio Server,使用 Docker Compose
·发布在 Towards Data Science ·6 分钟阅读·2024 年 3 月 23 日
--
在这篇文章中,我们将回顾如何设置一个 Docker-Compose 工作流,在容器内启动 RStudio Server。我们将介绍 Docker Compose 设置过程,并讨论何时应考虑使用它。最后,我们将演示如何将 上一篇文章 中介绍的 docker run 工作流转换为 Docker Compose 流程,并使用 Docker Compose 启动 Rocker RStudio 镜像。
相关文章:
一步步指南,帮助你在容器内设置 RStudio 服务器,并使用本地 RStudio 设置
towardsdatascience.com
完成本教程后,你将能够将 docker run 设置转换为 docker-compose.yml 文件,并通过 docker-compose 命令无缝启动 RStudio 容器。

使用 Docker Compose 在容器内启动 RStudio Server(截图由作者提供)
动机
在开始之前,让我们先解释一下 Docker Compose 是什么以及何时应考虑使用它。
从 什么 开始 — Docker Compose 是一个简化的框架,用于启动单个或多个容器。它是 docker run 命令的封装器,并通过 YAML 文件管理容器启动设置。建议在以下情况下使用 Docker Compose 工作流,而不是直接使用 docker run 命令 当:
-
docker run命令的参数数量增加,且通过 CLI 管理变得更加复杂。 -
你定期使用并与容器进行工作
-
复杂度较高(例如,启动多个容器并行等)
我们使用 docker-compose.yml 文件来设置 Docker Compose 框架,并通过 docker-compose 命令启动它。这个过程包括将 docker run 参数映射为 YAML 格式。简单的 docker-compose.yml 文件设置将包括以下两个参数:
-
版本 — 当前的 Docker Compose 版本为 3.9
-
服务 — 启动容器的列表及其相应的参数
让我们通过以下示例来说明将 docker run 命令参数映射到 docker-compose.yml 文件的过程:
docker run -argument_a argument_a_values \
-argument_b argument_b_values \
-argument_c argument_c_values \
IMAGE_NAME/IMAGE_TAG
该命令有以下三个参数 — argument_a、argument_b、argument_c,它们对应的值为 argument_a_values、argument_b_values、argument_c_values,并调用以下镜像 — IMAGE_NAME/IMAGE_TAG。
以下 docker-compose.yml 文件表示上述 docker run 参数的映射:
version: "3.9"
services:
my_service:
image: "IMAGE_NAME/IMAGE_TAG"
argument_a:
- "argument_a_values"
argument_b:
- "argument_b_values"
argument_c:
- "argument_c_values"
如上所述,version 和 services 参数分别定义了 Docker Compose 的版本以及运行时要启动的镜像列表。在此案例中,我们使用最新版本 3.9,并在 services 参数下定义了一个名为 my_service 的容器。在 my_service 部分下,我们按照标准的 YAML 格式定义了与上述 docker run 命令参数对应的运行时参数。
需要注意的是,docker run 命令参数与其在 docker-compose.yml 文件中的设置之间的命名约定映射并非总是逐一对应的。Docker Compose 文档是识别参数设置的极好资源。
在接下来的部分,我们将连接各个部分,并将我们在上一教程中设置的 docker run 命令映射到 docker-compose.yml 文件。
使用 Docker Compose 设置 RStudio
回顾在 上一教程 中,我们使用以下 docker run 命令在容器内启动了 RStudio 服务器:
docker run --rm -ti \
-v .:/home/rstudio \
-v $HOME/.config/rstudio:/home/rstudio/.config/rstudio \
-v $HOME/.Renviron:/home/rstudio/.Renviron \
-e PASSWORD=yourpassword \
-p 8787:8787 rocker/rstudio
简而言之,上述运行命令使用了以下参数:
-
卷或
v用于将本地文件夹与容器文件系统挂载 -
环境变量或
e用于将 RStudio 服务器密码设置为环境变量 -
端口或
p用于映射本地端口与容器端口之间的关系
以下 YAML 文件表示上述 docker run 命令的映射:
version: "3.9"
services:
rstudio:
image: "rocker/rstudio"
ports:
- "8787:8787"
volumes:
- type: "bind"
source: "."
target: "/home/rstudio"
- type: "bind"
source: "$HOME/.config/rstudio"
target: "/home/rstudio/.config/rstudio"
- type: "bind"
source: "$HOME/.Renviron"
target: "/home/rstudio/.Renviron"
environment:
- PASSWORD=yourpassword
我们在 services 参数下设置了一个名为 rstudio 的单一服务,并定义了相应的运行参数:
-
image— 定义镜像名称,在这种情况下,使用 RStudio Rocker 镜像rocker/rstudio -
ports— 设置本地机器与容器之间的端口映射。 -
volumes— 映射文件夹挂载,使用type参数定义挂载类型,使用source和target参数定义本地和容器文件夹路径映射。更多关于卷参数的细节可以在 这里 找到。 -
environment— 定义环境变量,在这种情况下,我们设置PASSWORD变量来定义 RStudio 服务器的密码。
一旦 YAML 文件设置完成,我们可以在 CLI 上使用 docker-compose 命令来启动 RStudio 容器:
docker-compose up
up 参数用于启动容器。你应该期待以下输出:
[+] Running 2/2
✔ Network rstudio-docker_default Created 0.1s
✔ Container rstudio-docker-rstudio-1 Created 0.1s
Attaching to rstudio-docker-rstudio-1
rstudio-docker-rstudio-1 | [s6-init] making user provided files available at /var/run/s6/etc...
rstudio-docker-rstudio-1 | exited 0.
rstudio-docker-rstudio-1 | [s6-init] ensuring user provided files have correct perms...
rstudio-docker-rstudio-1 | exited 0.
rstudio-docker-rstudio-1 | [fix-attrs.d] applying ownership & permissions fixes...
rstudio-docker-rstudio-1 | [fix-attrs.d] done.
rstudio-docker-rstudio-1 | [cont-init.d] executing container initialization scripts...
rstudio-docker-rstudio-1 | [cont-init.d] 01_set_env: executing...
rstudio-docker-rstudio-1 | skipping /var/run/s6/container_environment/HOME
rstudio-docker-rstudio-1 | skipping /var/run/s6/container_environment/PASSWORD
rstudio-docker-rstudio-1 | skipping /var/run/s6/container_environment/RSTUDIO_VERSION
rstudio-docker-rstudio-1 | [cont-init.d] 01_set_env: exited 0.
rstudio-docker-rstudio-1 | [cont-init.d] 02_userconf: executing...
rstudio-docker-rstudio-1 | [cont-init.d] 02_userconf: exited 0.
rstudio-docker-rstudio-1 | [cont-init.d] done.
rstudio-docker-rstudio-1 | [services.d] starting services
rstudio-docker-rstudio-1 | [services.d] done.
启动容器后,你可以通过浏览器使用本地主机地址和端口号访问 RStudio 服务器,在这种情况下是 — localhost:8787:

容器内的 RStudio 服务器(截图由作者提供)
注意: 使用 docker-compose up 命令启动容器后,CLI 会保持附加到终端,直到停止它。或者,你可以添加 d 参数以脱离模式运行:
docker-compose up -d
同样,docker-compose down 命令停止容器的运行时间。
总结
在本教程中,我们回顾了如何设置 Docker Compose 框架来启动 RStudio 容器。这包括设置 docker-compose.yml 文件并使用 docker-compose 命令简洁地启动容器。
使用 Docker Compose 封装 docker run 命令的动机是:
-
高效简洁 — 只需一次设置,之后使用
docker-compose命令启动简单(相比长的docker run命令)。 -
更高复杂度 — 它简化了单个或多个容器无缝启动的过程。例如,一个好的使用案例是同时运行 RStudio 和 Postgres 数据库。在这种情况下,你可以设置 Docker Compose 进程来启动这两个容器,使它们并行工作。
资源
-
在容器内运行 RStudio —
towardsdatascience.com/running-rstudio-inside-a-container-e9db5e809ff8 -
Docker Compose —
docs.docker.com/compose/ -
Rocker 项目 —
rocker-project.org/ -
Docker Hub —
hub.docker.com/ -
RStudio Docker Compose 模板 —
github.com/RamiKrispin/rstudio-docker-template
CV VideoPlayer — 一劳永逸
为计算机视觉研究专门开发的 Python 视频播放器包
·发表于 Towards Data Science ·6 分钟阅读·2024 年 12 月 12 日
--

图片由作者提供
在开发计算机视觉算法时,从概念到可用实现的过程通常涉及无数次观看、分析和调试视频帧的迭代。随着我深入计算机视觉项目,我发现自己反复编写相同的样板代码来进行视频可视化和调试。
有一天,我决定不再忍受,于是我创建了 CV VideoPlayer,这是一个基于 Python 的开源视频播放器包,专为计算机视觉从业者设计,旨在一劳永逸地解决这个问题。

CV 视频播放器“双帧模式”加入了可视化和键盘快捷键。图片由作者提供
调试视频相关算法很困难
如果你曾经开发过视频分析算法,你可能已经编写过以下某个版本的代码,帮助你进行可视化和调试:
import cv2
cap = cv2.VideoCapture(<video_path>)
ret = True
while ret:
ret, frame = cap.read()
algo_output = some_video_analsys_algorithm(frame)
frame_to_display = visualizer(frame, algo_output)
cv2.imshow(frame_to_display)
cv2.waitKey()
但在我参与的几乎所有项目中,这段代码都远远不够。随着项目的推进,我发现自己不断添加更多功能,以帮助我理解发生了什么。
例如:
-
在视频中逐帧前后导航。
-
具备将输出记录到文件的能力。
-
支持除简单视频文件之外的其他数据源(帧文件夹、流、远程存储等)
但最让我烦恼的事是缺乏交互性。使用这种代码,视觉化效果在渲染之前就已创建,且一旦显示出来便无法更改。而且,虽然这种方式适用于简单的算法,但对于更复杂的算法来说,每一帧所需的信息量实在太多了。而且,如果不能动态决定要显示什么,你会发现自己一次又一次地重复播放同一个视频,每次使用不同的可视化参数。
这个过程既繁琐又令人疲惫。
进入 CV VideoPlayer

作者提供的图片
CV VideoPlayer的诞生源于对一个简单可定制解决方案的需求,该方案能够交互式地渲染视频和帧。它允许添加任意数量的叠加层、侧边栏或其他帧编辑,用户可以在运行时轻松开启或关闭这些功能。让我们看看如何实现这一点的示例:
安装
我们首先通过pip install cvvideoplayer安装这个包。
播放原始视频
然后,我们可以导入视频播放器并使用以下代码运行未编辑的视频:
from cvvideoplayer import create_video_player
VIDEO_OR_FRAME_FOLDER_PATH = "<add local path here>"
video_player = create_video_player(video_source=VIDEO_OR_FRAME_FOLDER_PATH)
video_player.run()
这将打开视频播放器,并允许你使用空格键或箭头键播放视频,还会添加一些默认的内置frame-edit-callbacks,我们将在接下来的部分详细阐述。

作者提供的图片
编辑帧以添加可视化效果
要为视频添加自定义的可视化效果,我们可以像这样使用create_video_player构造函数的frame_edit_callbacks参数:
from cvvideoplayer import VideoPlayer
VIDEO_OR_FRAME_FOLDER_PATH = "<add local path here>"
video_player = create_video_player(
video_source=VIDEO_OR_FRAME_FOLDER_PATH,
frame_edit_callbacks=[
FitFrameToScreen(),
FrameInfoOverlay(),
KeyMapOverlay(),
]
)
video_player.run()
如果未指定,默认列表将与上面示例中的列表完全一致。
内置回调
有许多内置的回调可以使用,例如:
-
FitFrameToScreen— 自动调整帧的大小以适应屏幕尺寸。 -
FrameInfoOverlay— 在左上角打印帧编号和原始帧分辨率。 -
KeyMapOverlay— 自动检测并打印所有可用的快捷键(包括用户自定义的快捷键)。 -
DetectionCsvPlotter— 绘制 CSV 文件中指定的边界框,CSV 的表头为:frame_id, label, x1, y1, width, height, score -
FrameNormlizer— 允许用户调整图像的动态范围。 -
HistogramEqulizer— 不言自明
每个版本都会添加更多功能。
创建自定义回调
这里正是这个包的有用之处。为了添加你自己的自定义可视化,你需要创建一个继承自BaseFrameEditCallback的新类,并实现edit_frame方法,例如:
class MyCallback(BaseFrameEditCallback):
def __init__(
self,
enable_by_default: bool = True,
enable_disable_key: Optional[str] = None,
additional_keyboard_shortcuts: Optional[List[KeyFunction]] = None
**any_other_needed_params
):
super().__init__(
enable_by_default,
enable_disable_key,
additional_keyboard_shortcuts
)
def edit_frame(
self,
video_player: "VideoPlayer",
frame: np.ndarray,
frame_num: int,
original_frame: np.ndarray,
) -> np.ndarray:
"""
This function receives the displayed frame and should return it
after it has been altered in any way desirable by the user
Args:
video_player: an instance fo VideoPlayer
frame (): the frame to be edited and displayed
frame_num ():
original_frame () the frame before any alterations
Returns: the edited frame
"""
frame = add_any_visalizations(frame)
return frame
此外,你还可以通过重写父类中的这些方法来添加设置和清理方法:
class MyCallback(BaseFrameEditCallback):
...
def setup(self, video_player: "VideoPlayer", frame) -> None:
"""
Optionally configure more parameters according to the
first incoming frame
"""
def teardown(self) -> None:
"""
Optionally define how the callback should close when the
video player is closed
"""
添加自定义快捷键
对于每个回调,CV Video Player 允许你添加自定义快捷键,这些快捷键可以在运行时改变可视化效果。
最基本的快捷键是启用/禁用回调,它是通过 enable_disable_key 参数创建的,像这样:
my_callback = MyCallback(
enable_disable_key="ctrl+a"
)
传递的字符串可以是任何修饰符(ctrl、alt 和 shift)与字母或数字的组合,例如:“ctrl+alt+s”、“g”、“shift+v”、“ctrl+1”,等等。
要添加更改可视化本身的快捷键,你可以覆盖 additional_keyboard_shortcuts 属性,该属性返回一个包含 KeyFunction 数据类的列表。
from cvvideoplayer import KeyFunction
class MyCallback(BaseFrameEditCallback):
...
@property
def additional_keyboard_shortcuts(self) -> List[KeyFunction]:
[
KeyFunction(
key="alt+r",
function=self.a_function_to_modify_the_visualiztion,
description="what this does"
)
]
KeyFunction 是通过三个参数构造的:
-
key参数——与enable_disable_key相同,传递的字符串可以是任何修饰符(ctrl、alt 和 shift)与字母或数字的组合,例如:“ctrl+alt+s”、“g”、“shift+v”、“ctrl+1”。 -
description参数——这个参数由KeyMapOverlay回调使用,用于在屏幕上打印所有可用的快捷键。 -
function参数——必须是一个不接受任何参数的函数。
在许多情况下,KeyFunction 将接收一个函数,该函数切换回调的某些布尔属性,从而改变 edit_frame 方法的某些行为。所以像这样:
from cvvideoplayer import KeyFunction
class MyCallback(BaseFrameEditCallback):
...
@property
def additional_keyboard_shortcuts(self) -> List[KeyFunction]:
[
KeyFunction(
key="alt+r",
function=self.a_function_to_modify_the_visualiztion,
description="what this does"
)
]
def a_function_to_modify_the_visualiztion():
self._draw_something = bool(1 - self._draw_somthing)
双帧模式
很多时候,我发现自己想要并排比较两种不同的可视化。例如,比较两个探测器,或将算法的输出与原始帧进行对比(没有修改),等等。
为了做到这一点,我添加了 double_frame_mode,可以通过以下方式开启:
video_player = create_video_player(
...
double_frame_mode=True
)
本博客开头的视频展示了此模式的样子。
在此模式下,你可以使用“ctrl+1”和“ctrl+2”来决定用键盘控制哪个帧的可视化。
默认情况下,两个帧将具有相同的回调可用,但如果你希望右帧使用不同的回调,你可以使用 right_frame_callback 参数为右帧提供不同的回调集合(左帧将使用传递给 frame_edit_callback 参数的回调)。
video_player = create_video_player(
...
double_frame_mode=True
right_frame_callbacks = [callback1, callback2, ...]
)
总结
希望这个工具能对大家有所帮助。如果你有任何改进的想法,请在项目的 GitHub 页面 的问题标签中告诉我,顺便别忘了给项目加个星标 😃 …
循环划分:一种最多提高 1.5 倍速度的划分算法
一种最小化重新排列值的序列划分算法
·发布于Towards Data Science ·19 分钟阅读·2024 年 10 月 10 日
--

1. 引言
序列划分是计算机编程中一种基础且常用的算法。给定一个数字序列“A”以及一个名为“p”的值(称为枢轴值),划分算法的目的是按照某种方式重新排列“A”中的数字,使得所有小于‘p’的数字排在前面,其余的数字排在后面。

一个示例,展示了在枢轴值“p=20”划分前后的序列。
在算法执行后,所有小于 20 的值(浅绿色)
出现于其他值之前(黄色)。*
划分有不同的应用,但最常见的应用包括:
-
快速排序——它通常只不过是一个划分算法,通过递归多次调用在给定数组的不同子数组上,直到数组被排序。
-
查找给定序列的中位数值——利用划分方法有效地缩小搜索范围,最终在线性时间内找到中位数。
排序一个序列是加速大规模数据导航的关键步骤。在两种常见的搜索算法中——线性搜索和二分搜索——只有当数组中的数据已经排序时,二分搜索才能使用。查找中位数或第 k 阶统计量对于理解给定无序数据的分布非常关键。
目前有不同的划分算法(也称为划分方案),但比较著名的有“Lomuto 方案”和“Hoare 方案”。Lomuto 方案通常更容易直观理解,而 Hoare 方案在给定数组内部的重新排列较少,因此在实际应用中更为常见。
我在这个故事中将要建议的是一种新的划分方案,叫做“循环划分”,它类似于 Hoare 方案,但在数组内部进行的重新排列(赋值)减少了 1.5 倍。因此,正如后续所展示的,赋值的数量几乎等于最初“未在其位置”的值的数量,而这些值应该以某种方式被移动。这一事实让我认为这个新的划分方案几乎是最优的。
接下来的章节将按照以下方式组织:
-
在第二章中,我们将回顾什么是原地划分(一个使得划分任务并非易事的特性),
-
在第三章中,我们将回顾广泛使用的 Hoare 划分方案,
-
在第四章中,我将介绍“赋值周期”,并展示为什么某些序列的重新排列可能需要比其他重新排列更多的赋值,
-
第五章将使用“赋值周期”的一些性质,推导出新的“循环划分”方案,作为 Hoare 方案的优化变体,
-
最后,第六章将展示 Hoare 方案和循环划分在小型和大型数据类型数组上的实验比较。
“循环划分”在 C++语言中的实现,以及与当前标准 Hoare 方案的基准比较,已经在 GitHub 上展示,并在本故事的最后引用[1]。
2. 回顾原地序列划分
如果输入和输出序列位于计算机内存中的两个不同数组中,划分序列将不再是一个困难的任务。如果是这种情况,可能的方法之一是:
-
计算在“A”中有多少个值小于‘p’(这将给出输出序列左部分的最终长度),
-
从左到右扫描输入数组“A”,并根据当前值“A[i]”是否小于‘p’,将其分别追加到左部分或右部分。
这里展示了一些运行该算法的状态:

在第一阶段,我们计算出有 7 个值小于“p=20”(这些是浅绿色的),因此我们准备从索引 7 开始将较大的值写入输出序列。

*在第二阶段,在扫描了输入序列中的 5 个值后,
我们将其中的 3 个值追加到输出序列的左部分,
其余的 2 个追加到右部分。*

*继续第二阶段,我们现在已经从输入序列中扫描了 9 个值,
将其中 5 个放在输出序列的左侧部分,另 4 个放在右侧部分。*

*算法完成。输出序列的两个部分现在已正确填充到最后。
注意,左侧或右侧部分的值的相对顺序被保留,
基于它们在输入数组中的原始写法。
也存在其他较短的解决方案,诸如代码中只有一个循环的那些。
现在,问题在于我们希望不使用额外的内存,因此输入序列将通过仅在唯一数组内移动值来转换为划分后的输出序列。顺便说一下,这种不使用额外内存的算法被称为原地算法。

*将相同的输入序列“A”原地划分,使用相同的枢轴值“p=20”。
所呈现的值的顺序对应于序列的输入状态,每个值的箭头显示
如果要将该值移动到哪里,以便整个序列变得划分。*
在介绍我的划分方案之前,让我们回顾一下现有的、常用的原地划分解决方案。
3. 当前使用的划分方案
在观察了几种编程语言标准库中的排序实现后,似乎最广泛使用的划分算法是霍尔方案。我发现它例如在以下实现中被使用:
-
在 C++的 STL 中,使用了“std::sort()”实现,
-
在 Java 的 JDK 中,对于原始数据类型,使用了“Arrays.sort()”实现。
在基于霍尔方案的划分中,我们同时从序列的两端向中间扫描,在左侧部分寻找一个值 A[i],它大于或等于‘p’,在右侧部分寻找一个值 A[j],它小于‘p’。一旦找到,我们知道这两个值 A[i] 和 A[j] 是“没有放在正确的位置”(记住,划分后的序列应当先放置小于‘p’的值,然后是大于或等于‘p’的值),因此我们交换 A[i] 和 A[j]。交换后,我们继续以相同的方式,继续同时扫描数组“A”,使用索引 i 和 j,直到它们相等。一旦它们相等,划分完成。
让我们在另一个示例中观察霍尔方案:

*输入序列“A”长度为’N’,应根据枢轴值“p=20”进行划分。
索引 i 从 0 开始向上扫描,索引 j 从“N-1”开始向下扫描。*

当索引 i 增大时,我们遇到值“A[2]=31”,它大于‘p’。然后,在索引 j 减小时,
我们遇到另一个值“A[10]=16”,它小于‘p’。这两个值将被交换。

在交换“A[2]”与“A[10]”之后,我们继续从 2 开始增加 i,从 10 开始减少 j。索引 i 将在值“A[4]=28”大于‘p’时停止,索引 j 将在值“A[9]=5”小于‘p’时停止。这两个值也将被交换。

算法按相同的方式继续进行,数字“A[5]=48”和“A[7]=3”也将被交换。

之后,索引‘i’和‘j’将变得相等。划分完成。
如果编写霍尔方案的划分伪代码,我们将得到如下内容:
// Partitions sequence A[0..N) with pivot value 'p'
// upon Hoare scheme, and returns index of the first value
// of the resulting right part.
function partition_hoare( A[0..N) : Array of Integers, p: Integer ) : Integer
i := 0
j := N-1
while true
// Move left index 'i', as much as needed
while i < j and A[i] < p
i := i+1
// Move right index 'j', as much as needed
while i < j and A[j] >= p
j := j-1
// Check for completion
if i >= j
if i == j and A[i] < p
return i+1 // "A[i]" also refers to left part
else
return i // "A[i]" refers to right part
// Swap "A[i]" and "A[j]"
tmp := A[i]
A[i] := A[j]
A[j] := tmp
// Advance by one both 'i' and 'j'
i := i+1
j := j-1
在第 5 和第 6 行,我们设置了两个扫描的起始索引。
第 8 到 10 行从左侧查找应该属于右部分的值,划分之后它应该属于右部分。
类似地,第 11 到 13 行从右侧查找应该属于左部分的值。
第 15 到 19 行检查扫描是否完成。一旦索引‘i’和‘j’相遇,就有两种情况:要么“A[i]”属于左部分,要么属于右部分。根据情况,我们返回‘i’或‘i+1’,因为函数的返回值应该是右部分的起始索引。
接下来,如果扫描尚未完成,第 20 到 23 行将交换那些未放置到正确位置的值。
最后,第 24 到 26 行推进两个索引,以避免重新检查已交换的值。
算法的时间复杂度是O(N),无论两个扫描何时相遇,因为它们总共扫描N个值。
这里有一个重要的备注,如果数组“A”有‘L’个“未放置到正确位置”的值,且需要交换,那么按照霍尔方案操作时,我们将进行“3**L*/2”次赋值,因为交换两个值需要进行 3 次赋值:

交换两个变量‘a’和‘b’的值需要通过‘tmp’变量进行 3 次赋值。
这些赋值是:
tmp := a
a := b
b := tmp
我还在这里强调,‘L’始终是一个偶数。这是因为,对于每个原本位于左侧区域、满足“A[i]>=p”的值,都有另一个原本位于右侧区域、满足“A[j]<p”的值,它们正被交换。所以,每次交换都会重新排列两个这样的值,霍尔方案中的所有重新排列都仅通过交换进行。这就是为什么‘L’——需要重新排列的总值数量,始终是一个偶数。
4. 赋值循环
本章看起来可能偏离了故事的议程,但实际上并非如此,因为在下一章优化霍尔划分方案时,我们将需要了解赋值的循环。
假设我们想以某种方式重新排列给定序列“A”中的值。这不一定是划分操作,但可以是任何形式的重新排列。让我展示一下,某些重新排列比其他重新排列需要更多的赋值。
情况#1:序列的循环左移
如果我们想将序列 “A” 循环左移 1 位,应该进行多少次赋值?

*长度为 N=12 的序列 “A” 循环左移的示例\。
我们看到,所需的赋值次数是 N+1=13,因为我们需要:
-
将 “A[0]” 存储到临时变量 “tmp” 中,然后
-
将右侧相邻值赋给当前值,共 “N-1” 次,最后
-
将 “tmp” 赋值给序列 “A[N-1]” 的最后一个值。*
所需的操作是:
tmp := A[0]
A[0] := A[1]
A[1] := A[2]
...
A[9] := A[10]
A[10] := A[11]
A[11] := tmp
… 结果是 13 次赋值。
案例 #2: 循环左移 3 位
在下一个示例中,我们仍然希望对相同的序列进行循环左移,但这次是左移 3 位:

*循环左移 3 位的 “A” 序列示例,长度 N=12。
我们看到,值 A[0]、A[3]、A[6] 和 A[9] 正在彼此之间交换(蓝色箭头),
同时,值 A[1]、A[4]、A[7] 和 A[10] 也会这样做(粉色箭头),
而且值 A[2]、A[5]、A[8] 和 A[11] 仅在彼此之间交换(黄色箭头)。
“tmp” 变量被赋值并读取了 3 次。*
在这里,我们有 3 个独立的链/循环赋值,每个长度为 4。
为了正确地交换 A[0]、A[3]、A[6] 和 A[9] 之间的值,需要进行以下操作:
tmp := A[0]
A[0] := A[3]
A[3] := A[6]
A[6] := A[9]
A[9] := tmp
… 这会导致 5 次赋值。类似地,交换组内的值(A[1]、A[4]、A[7]、A[10])和(A[2]、A[5]、A[8]、A[11])也需要各 5 次赋值。将这些加在一起,总共需要 53=15 次赋值来对长度为 N=12 的序列 “A*” 进行 3 位的循环左移。
案例 #3: 反转一个序列
当反转长度为 ’N’ 的序列 “A” 时,执行的操作是:
-
将其第一个值与最后一个值交换,然后
-
将第二个值与从右边数来的第二个值交换,
-
将第三个值与从右边数来的第三个值交换,
-
… 依此类推。

*反转数组 “A” 的示例,长度 N=12。
我们看到,值对(A[0]、A[11])、(A[1]、A[10])、(A[2]、A[9])等正在彼此交换,互不影响。变量 “tmp” 被赋值并读取了 6 次。*
由于每次交换需要 3 次赋值,而且对于反转整个序列 “A” 我们需要做 ⌊N/2⌋ 次交换,总的赋值次数为:
3⌊N/2⌋ = 3⌊12/2⌋ = 3*6 = 18
反转 “A” 所需的具体赋值顺序如下:
tmp := A[0] // Cycle 1
A[0] := A[11]
A[11] := tmp
tmp := A[1] // Cycle 2
A[1] := A[10]
A[10] := tmp
...
tmp := A[5] // Cycle 6
A[5] := A[6]
A[6] := tmp
总结
我们已经看到,对同一序列 “A” 进行重新排列可能需要不同数量的赋值,具体取决于值是如何重新排列的。
在这 3 个示例中,序列的长度始终为 N=12,但所需的赋值次数是不同的:

更精确地说,赋值次数等于 N+C,其中“C”是重排过程中产生的循环次数。在这里,所谓的“循环”是指“A”中的一组变量,其值在彼此之间进行旋转。
在情况 1(左移 1)中,我们只有 C=1 次赋值循环,且所有“A”中的变量都参与了这个循环。这就是为什么总体赋值次数为:
N+C = 12+1 = 13。
在情况 2(左移 3)中,我们有 C=3 次赋值循环,其中:
— 第一轮应用于变量(A[0],A[3],A[6],A[9]),
— 第二轮应用于变量(A[1],A[4],A[7],A[10])并且
— 第三轮应用于变量(A[2],A[5],A[8],A[11])。
这就是为什么总体赋值次数为:
N+C = 12+3 = 15。
而在我们的情况 3(反转)中,我们有 ⌊N/2⌋ = 12/2 = 6 次循环。所有这些都是最短的循环,并且应用于对(A[0],A[11]),(A[1],A[10]),…… 等等。这就是为什么总体赋值次数为:
N+C = 12+6 = 18。
当然,在所展示的示例中,赋值数量的绝对差异非常小,在编写高性能代码时不会产生任何影响。但那是因为我们考虑的是一个非常短的数组,长度为“N=12”。对于更长的数组,这些赋值数量的差异将与 N 成比例增长。
总结本章内容时,让我们记住,重新排列一个序列所需的赋值次数与由这种重新排列引入的循环次数是成正比的。如果我们想要更快速的重新排列,我们应该尝试通过一个赋值循环最少的方案来实现。
5. 优化 Hoare 分区方案
现在让我们再次观察 Hoare 分区方案,这次关注它引入了多少赋值循环。
假设我们有一个相同长度为 N 的数组“A”,并且有一个分区值‘p’,根据该值必须进行分区。同时假设数组中有‘L’个值需要以某种方式重新排列,以将“A”带入分区状态。事实证明,Hoare 分区方案以最慢的方式重新排列这些‘L’个值,因为它引入了最大数量的赋值循环,每个循环仅涉及 2 个值。

*给定分区值“p=20”,需要重新排列的“L=8”个值是那些需要被重新安排的值,
箭头正在出现(或从中移动)。
Hoare 分区方案引入了“L/2=4”个赋值循环,每个循环作用于 2 个值。*
在一个长度为 2 的循环中交换 2 个值,本质上是交换它们,需要 3 次赋值。因此,Hoare 分区方案的总体赋值次数是“3**L*/2”。
我即将描述的优化背后的思想来源于这样一个事实:在划分序列后,我们通常不关心“A[i]<p”的值的相对顺序,它们应该位于划分序列的左侧部分;同样我们也不关心应位于右侧部分的那些值的相对顺序。我们唯一关心的,是所有小于‘p’的值应该出现在其他值之前。这个事实使我们能够改变霍尔方案中的赋值循环,并得出一个只包含所有需要重新排列的‘L’值的赋值循环。
让我先通过以下插图描述这个改进后的划分方案:

*改进后的划分方案,应用于相同的序列“A”。
由于枢轴“p=20”没有改变,应该重新排列的“L=8”个值也保持不变。
所有箭头表示新方案中唯一的赋值循环。
在将所有‘L’值移动到其上之后,我们将得到一个替代的划分序列。*
那么我们在这里做了什么?
-
与原始的霍尔方案一样,首先我们从左侧扫描,找到这样的值“A[i]>=p”,它应该移到右侧部分。但是我们并不与其他值交换,而是记住它:“tmp := A[i]”。
-
接下来我们从右侧扫描,找到这样的值“A[j]<p”,它应该移到左侧部分。然后我们执行赋值操作“A[i] := A[j]”,不会丢失“A[i]”的值,因为它已经存储在“tmp”中。
-
接下来我们从左侧继续扫描,找到这样的值“A[i]>=p”,它也应该移到右侧部分。因此我们执行赋值操作“A[j] := A[i]”,不会丢失“A[j]”的值,因为它已经被赋值到‘i’的前一个位置。
-
这个模式会继续下去,当索引 i 和 j 相遇时,只需将大于‘p’的某个值放到“A[j]”,我们只需执行“A[j] := tmp”,因为最初变量“tmp”存储的是第一个大于‘p’的值。划分完成。
如我们所见,这里只有 1 次赋值循环,遍历所有的‘L’值,为了正确地重新排列它们,只需要“L+1”次赋值,相比之下,霍尔方案需要“3**L*/2”次赋值。
我更喜欢称这个新的划分方案为“循环划分”,因为所有应该重新排列的‘L’值现在都位于一个赋值循环中。
这是循环划分算法的伪代码。与霍尔方案的伪代码相比,变化微小,但现在我们始终执行 1.5 倍更少的赋值。
// Partitions sequence A[0..N) with pivot value 'p'
// by "cyclic partition" scheme, and returns index of
// the first value of the resulting right part.
function partition_cyclic( A[0..N) : Array of Integers, p: Integer ) : Integer
i := 0
j := N-1
// Find the first value from left, which is not on its place
while i < N and A[i] < p
i := i+1
if i == N
return N // All N values go to the left part
// The cycle of assignments starts here
tmp := A[i] // The only write to 'tmp' variable
while true
// Move right index 'j', as much as needed
while i < j and A[j] >= p
j := j-1
if i == j // Check for completion of scans
break
// The next assignment in the cycle
A[i] := A[j]
i := i+1
// Move left index 'i', as much as needed
while i < j and A[i] < p
i := i+1
if i == j // Check for completion of scans
break
// The next assignment in the cycle
A[j] := A[i]
j := j-1
// The scans have completed
A[j] := tmp // The only read from 'tmp' variable
return j
这里第 5 行和第 6 行设置了两个扫描的起始索引(‘i’ — 从左到右,‘j’ — 从右到左)。
第 7 至 9 行从左侧查找应当移到右侧部分的值“A[i]”。如果没有找到这样的值,且所有 N 个项都属于左侧部分,则第 10 和 11 行报告这一点并结束算法。
否则,如果找到了该值,第 13 行我们将其记住在‘tmp’变量中,从而为索引‘i’的槽开启了一个位置,以便放入另一个值。
第 15 至 19 行从右侧查找应当移动到左侧部分的值“A[j]”。一旦找到,第 20 至 22 行将其放入索引‘i’的空槽中,之后索引‘j’的槽变为空,并等待另一个值。
同样,第 23 至 27 行从左侧查找应当移动到右侧部分的值“A[i]”。一旦找到,第 28 至 30 行将其放入索引‘j’的空槽中,之后索引‘i’的槽变为空,并等待另一个值。
这种模式在算法的主循环中得以延续,位于第 14 至 30 行。
一旦索引‘i’和‘j’相遇,我们就在该位置有一个空槽,第 31 和 32 行将最初记住的‘tmp’变量中的值放入该槽中,之后索引‘j’成为持有该值的第一个位置,这个值属于右侧部分。
最后一行返回那个索引。
这样,我们可以将循环体中的两个赋值操作合并在一起,因为正如第三章所证明的那样,‘L’总是一个偶数。
这个算法的时间复杂度也是O(N),因为我们仍然是从两端扫描序列。它只是做了 1.5 倍更少的值赋值操作,因此加速仅反映在常数因子上。
在 GitHub 上有一个用 C++语言实现的循环分区,可以在故事的最后找到引用[1]。
我还想表明,在 Hoare 方案中出现的值‘L’无论我们使用什么分区方案,都无法降低。假设分区后,左侧部分的长度为“left_n”,右侧部分的长度为“right_n”。现在,如果查看原始未分区数组的左对齐“left_n”长区域,我们会发现其中有一些‘t1’值,它们还没有放到最终的位置。所以,这些是大于或等于‘p’的值,应该无论如何都要移动到右侧部分。

分区前后序列的示意图。
左侧部分的长度是“left_n=7”,右侧部分的长度是“right_n=5”。
在未分区序列的前 7 个值中,有“t1=3”个值。
这些值大于“p=20”(黄色的值),应该以某种方式移动到右侧部分。
而在未分区序列的最后 5 个值中,有“t2=3”个值。
这些值小于‘p’(浅绿色的值),应该以某种方式移动到左侧部分。*
类似地,如果观察原始未划分数组的右对齐的“right_n”长区域,我们将找到一些‘t2’值,它们也没有处于最终位置。这些值小于‘p’,应该移动到左边部分。我们不能把小于‘t1’的值从左移到右,也不能把小于‘t2’的值从右移到左。
在霍尔划分方案中,‘t1’和‘t2’值是相互交换的。所以这里我们有:
t1 = t2 = L/2,
或者
t1 + t2 = L。
这意味着‘L’实际上是应该以某种方式重新排列的最小值数量,以使得序列能够被划分。而循环划分算法通过执行“L+1”次赋值来完成重新排列。这就是为什么我允许自己称这种新的划分方案为“几乎最优”的原因。
6. 实验结果
已经证明新的划分方案进行的赋值操作更少,因此我们可以期望它运行得更快。然而,在发布算法之前,我希望以实验的方式收集结果。
我已经比较了使用霍尔(Hoare)方案和循环划分(Cyclic partition)方案时的运行时间。所有实验都是在随机打乱的数组上进行的。
实验之间的不同参数是:
-
N — 数组的长度,
-
“left_part_percent” — 划分后左边部分的长度百分比(相对于N),
-
在原始数据类型变量数组(32 位整数)vs. 大对象数组(256 长静态数组,包含 16 位整数)上的运行。
我想澄清一下为什么我认为有必要对原始数据类型的数组和大对象的数组进行划分。在这里,所谓的“大对象”是指占用的内存比原始数据类型要多得多的值。当划分原始数据类型时,将一个变量赋值给另一个变量的速度与算法中几乎所有其他指令(如递增索引或检查循环条件)一样快。与此同时,当划分大对象时,将一个此类对象赋值给另一个将比其他使用的指令需要更多的时间,而这正是我们希望减少赋值总次数的原因。
我稍后会解释为什么我决定在本章稍后进行不同的实验,使用不同的“left_part_percent”值。
实验是在以下系统上使用 Google Benchmark 执行的:
CPU: Intel Core i7–11800H @ 2.30GHz
内存: 16.0 GB
操作系统: Windows 11 Home, 64 位
编译器: MSVC 2022 ( /O2 /Ob2 /MD /GR /Gd )
原始数据类型数组的划分
以下是对原始数据类型数组(32 位整数)运行划分算法的结果:


在包含 32 位整数的数组上的分区算法运行时间,数组长度为 N=10'000。
蓝色柱状图对应于霍尔分区算法,
而红色柱状图则对应于循环分区算法。
分区算法在 5 种不同的情况下运行,基于“left_part_percent”——分区后数组左部分所占百分比(基于 N)。时间以纳秒为单位表示。*
我们可以看到,“left_part_percent”的值与两个算法运行时间的相对差异之间没有明显的关联。这种行为是预期中的。
分区“large objects”数组
下面是运行两个分区算法的结果,针对所谓的“large objects”数组——每个数组是一个包含 256 个 16 位随机整数的静态数组。


在“large objects”数组上的分区算法运行时间
(256 长的静态数组,包含随机的 16 位整数),长度为 N=10'000。
蓝色柱状图对应于霍尔分区算法,
而红色柱状图则对应于循环分区算法。
分区算法在 5 种不同的情况下运行,基于“left_part_percent”——分区后数组左部分所占百分比(基于 N)。时间以纳秒为单位表示。*
现在我们看到一个明显的关联:当“left_part_percent”接近 50%时,循环分区算法的性能优于霍尔分区算法。换句话说,当分区后数组的左右部分长度接近时,循环分区算法的运行速度相对较快。这也是预期的行为。
结果解释
— 为什么当“left_part_percent”接近 50%时,分区通常需要更长时间?
让我们暂时设想一个极端情况——当分区后几乎所有值出现在左边(或右边)部分时。这意味着几乎所有的数组值都小于(或大于)枢轴值。这还意味着在扫描过程中,所有这些值被认为已经位于最终位置,只有很少的值进行了赋值操作。如果设想另一种情况——当分区后,左右部分的长度几乎相等时,这将意味着进行了大量的值赋值操作(因为最初数组中的所有值都是随机打乱的)。
— 为什么在分区“large objects”时,当“left_part_percent”接近 50%时,两个算法的运行时间差异会更大?
之前的解释表明,当“left_part_percent”接近 50% 时,就需要在数组中进行更多的值赋值。在前面的章节中,我们也展示过,相较于 Hoare 算法,循环分区总是会减少 1.5 倍的值赋值次数。因此,这 1.5 倍的差异在整体运行时间上会带来更大的影响,尤其是在我们通常需要做更多值重新排列的时候。
— 为什么在分区“大型对象”时,绝对时间(以纳秒为单位)比分区 32 位整数时更长?
这个问题很简单——因为将一个“大型对象”赋值给另一个对象,所花费的时间要比将一个原始数据类型赋值给另一个原始数据类型要长得多。
我也在不同长度的数组上进行了所有实验,但整体结果并没有改变。
7. 结论
在本故事中,我介绍了一种修改过的分区方案,称为“循环分区”。与当前使用的 Hoare 分区方案相比,它总是能减少 1.5 倍的值赋值次数。
当然,在对序列进行分区时,值赋值并不是唯一的操作类型。除了赋值之外,分区算法还需要检查输入序列“A”中的值是否小于或大于枢轴值‘p’,并且还会对“A”中的索引进行增减操作。比较次数、增减操作的次数与引入“循环分区”没有关系,因此我们不能仅仅期待它能跑得 1.5 倍更快。然而,当分区一个包含复杂数据类型的数组时,值赋值操作比简单的索引增减操作要耗时得多,因此整体算法的执行时间可能会加快最多 1.5 倍。
分区过程是快速排序算法的主要步骤,同时也是查找无序数组中中位数或查找其 k 次序统计量的算法步骤。所以我们也可以预期,当处理复杂数据类型时,这些算法的性能提升可以达到 1.5 倍。
我感谢:
— Roza Galstyan,感谢她审阅故事草稿并提出有益的改进建议,
— David Ayrapetyan,感谢他进行拼写检查(
www.linkedin.com/in/davidayrapetyan/),— Asya Papyan,感谢她精心设计了所有使用的插图(
www.behance.net/asyapapyan)。如果你喜欢这个故事,可以在 LinkedIn 上找到我并联系我(
www.linkedin.com/in/tigran-hayrapetyan-cs/)。所有使用的图片,除非另有说明,都是根据作者的要求设计的。
参考文献:
[1] — C++ 中循环分区的实现: github.com/tigranh/cyclic_partition
循环编码:时间序列特征的替代方法
循环编码为你的模型提供了相同的信息,但使用了显著更少的特征。
·发表于 Towards Data Science ·阅读时长 7 分钟·2024 年 5 月 3 日
--
在训练时间序列的机器学习模型时,通常你需要使用以下时间特征:
-
小时
-
一周中的天数
-
月份
-
一年中的周数或天数
-
等等。
将时间戳列转换为这些特征是相当容易的。在确保你已经将时间列转换为日期时间对象(使用 pd.to_datetime)后,你可以通过 .dt 提取一系列时间序列特征。
df['Hour']=df['Datetime'].dt.hour
df['Month']=df['Datetime'].dt.month
df['Dayofweek']=df['Datetime'].dt.dayofweek
作为参考,我将在这个示例中使用的数据集(CC0 公共领域许可)是一个小时电力消耗数据集。能源消耗数据集通常是时间序列数据,目标最终是通过过去的数据预测未来的消耗量,因此这是一个很好的应用案例。尽管其他外部特征,如温度、湿度和风速也会影响能源消耗,但在这里我将专注于提取和转换时间序列特征。
Cypher 生成:好、坏与混乱
创建用于文本到 Cypher 生成的微调数据集的方法。
·发布于 Towards Data Science ·13 分钟阅读·2024 年 1 月 29 日
--

由 ChatGPT-DALLE 创建
引言
Cypher 是 Neo4j 的图查询语言。它的灵感来源于 SQL,并且与 SQL 有许多相似之处,能够从知识图谱中检索数据。随着生成式 AI 的兴起以及大型语言模型(LLMs)的广泛应用,自然而然地会有人问,哪些 LLM 能生成 Cypher 查询,或者我们如何微调自己的模型从文本生成 Cypher 查询。
这个问题提出了相当大的挑战,主要是由于微调数据集的稀缺,并且在我看来,这样的数据集将极大地依赖于特定的图谱模式。
在这篇博客文章中,我将讨论几种创建微调数据集的方法,旨在从文本中生成 Cypher 查询。第一种方法基于大型语言模型(LLMs),并使用预定义的图谱模式。第二种方法完全基于 Python,提供了一种灵活的方式,可以生成各种各样的问题和 Cypher 查询,适用于任何图谱模式。在实验中,我创建了一个基于 ArXiv 数据集子集的知识图谱。
当我完成这篇博客时,Tomaz Bratanic 启动了一个 倡议项目,旨在开发一个全面的微调数据集,涵盖各种图谱模式,并结合人工智能参与的方法来生成和验证 Cypher 语句。我希望这里讨论的见解也能对该项目有所帮助。
知识图谱模型
我喜欢使用 ArXiv 科学文章数据集,因为它具有干净且易于集成到知识图谱中的格式。利用我最近的Medium 博客文章中的技术,我通过添加关键词和聚类增强了这个数据集。由于我的主要关注点是构建一个微调数据集,我将省略构建该图谱的具体细节。对于感兴趣的读者,详细信息可以在这个Github 仓库中找到。
该图谱的规模合理,包含超过 38K 个节点和近 96K 个关系,拥有 9 种节点标签和 8 种关系类型。其架构如下图所示:

图片来源:作者
虽然这个知识图谱尚未完全优化,还可以进一步改进,但它在这篇博客文章中能够有效地实现目的。如果你更倾向于直接测试查询而不构建图谱,我已将数据转储文件上传至这个Github 仓库。
使用 LLM 生成训练对
我实现的第一种方法灵感来自 Tomaz Bratanic 的博客文章,关于构建知识图谱聊天机器人和使用 H2O Studio 微调 LLM 模型。最初,在提示中提供了一些示例查询。然而,最近的一些模型增强了直接从图谱架构生成 Cypher 查询的能力。因此,除了 GPT-4 或 GPT-4-turbo 之外,现在还有可访问的开源替代方案,如 Mixtral-8x7B,我预期它能够有效生成高质量的训练数据。
在这个项目中,我尝试了两种模型。为了方便起见,我决定将 GPT-4-turbo 与 ChatGPT 结合使用,详情请见这个Colab 笔记本。然而,在这个笔记本中,我对 Mixtral-7x2B-GPTQ 模型进行了一些测试,它是一个足够小的量化模型,可以在 Google Colab 上运行,并且能够提供令人满意的结果。
为了保持数据的多样性并有效监控生成的提问和 Cypher 语句对,我采用了一个两步法:
-
第 1 步:提供完整的架构给 LLM,并请求其生成 10–15 种不同类别的与图谱相关的潜在问题及其描述。
-
第 2 步:提供架构信息并指示 LLM 为每个识别的类别创建特定数量 N 的训练对。
提取样本类别:
在这一步,我使用了 ChatGPT Pro 版本,尽管我确实多次调整提示,结合并增强了输出结果。
-
将图的模式提取为字符串(有关更多信息,请参见下一部分)。
-
构建一个提示来生成类别:
chatgpt_categories_prompt = f"""
You are an experienced and useful Python and Neo4j/Cypher developer.
I have a knowledge graph for which I would like to generate
interesting questions which span 12 categories (or types) about the graph.
They should cover single nodes questions,
two or three more nodes, relationships and paths. Please suggest 12
categories together with their short descriptions.
Here is the graph schema:
{schema}
"""
-
请 LLM 生成类别。
-
审查、修正并根据需要增强类别。以下是一个示例:
'''Authorship and Collaboration: Questions about co-authorship and collaboration patterns.
For example, "Which authors have co-authored articles the most?"''',
'''Article-Author Connections: Questions about the relationships between articles and authors,
such as finding articles written by a specific author or authors of a particular article.
For example, "Find all the authors of the article with tile 'Explorations of manifolds'"''',
'''Pathfinding and Connectivity: Questions that involve paths between multiple nodes,
such as tracing the relationship path from an article to a topic through keywords,
or from an author to a journal through their articles.
For example, "How is the author 'John Doe' connected to the journal 'Nature'?"'''
💡提示💡
-
如果图的模式非常大,拆分为重叠的子图(这也取决于图的拓扑结构),并对每个子图重复上述过程。
-
在使用开源模型时,选择最适合你计算资源的模型。 TheBloke 发布了一个详尽的量化模型清单, Neo4j GenAI 提供了在你自己的硬件上工作的工具, LightningAI Studio 是一个最近发布的平台,提供对多种 LLMs 的访问。
生成训练对:
这一步是通过 OpenAI API 执行的,使用 GPT-4-turbo,它也有输出 JSON 格式的选项。同样,图的模式通过提示提供:
def create_prompt(schema, category):
"""Build and format the prompt."""
formatted_prompt = [
{"role": "system",
"content": "You are an experienced Cypher developer and a
helpful assistant designed to output JSON!"},
{"role": "user",
"content": f"""Generate 40 questions and their corresponding
Cypher statements about the Neo4j graph database with
the following schema:
{schema}
The questions should cover {category} and should be phrased
in a natural conversational manner. Make the questions diverse
and interesting.
Make sure to use the latest Cypher version and that all
the queries are working Cypher queries for the provided graph.
You may add values for the node attributes as needed.
Do not add any comments, do not label or number the questions.
"""}]
return formatted_prompt
构建一个函数来提示模型并检索输出:
def prompt_model(messages):
"""Function to produce and extract model's generation."""
response = client.chat.completions.create(
model="gpt-4-1106-preview", # work with gpt-4-turbo
response_format={"type": "json_object"},
messages=messages)
return response.choices[0].message.content
遍历类别并将输出收集到一个列表中:
def build_synthetic_data(schema, categories):
"""Function to loop through the categories and generate data."""
# List to collect all outputs
full_output=[]
for category in categories:
# Prompt the model and retrieve the generated answer
output = [prompt_model(create_prompt(schema, category))]
# Store all the outputs in a list
full_output += output
return full_output
# Generate 40 pairs for each of the categories
full_output = build_synthetic_data(schema, categories)
# Save the outputs to a file
write_json(full_output, data_path + synthetic_data_file)
在项目的这一阶段,我收集了近 500 对问题和 Cypher 语句。以下是一个示例:
{"Question": "What articles have been written by 'John Doe'?",
"Cypher": "MATCH (a:Author {first_name:'John', last_name:'Doe'})-
[:WRITTEN_BY]-(article:Article) RETURN article.title, article.article_id;"}
数据需要大量清理和整理。虽然并不复杂,但过程既耗时又繁琐。以下是我遇到的一些挑战:
-
由于 Cypher 语句不完整,存在非 JSON 条目;
-
期望的格式是 {’question’: ‘某个问题’, ‘cypher’:’某个 cypher’},但偏差很常见,需要进行标准化;
-
在问题和 Cypher 语句聚集在一起的情况下,需要将它们分开并进行组织。
💡提示💡
与其从一开始就试图找到最好的提示格式,不如多次迭代提示的变体。根据我的经验,即使经过仔细调整,像这样的大量数据生成不可避免地会出现一些偏差。
现在说说内容方面。GPT-4-turbo 很有能力生成关于图的好问题,但并非所有的 Cypher 语句都是有效的(工作 Cypher)和正确的(提取预期信息)。在生产环境中进行微调时,我会纠正或删除这些错误的语句。
我创建了一个函数 execute_cypher_queries(),它将查询发送到 Neo4j 图数据库。如果出现错误,它会记录一条消息,或者从数据库中检索输出。此函数可以在此 Google Colab 笔记本 中找到。
从提示中,你可能会注意到我指示 LLM 生成模拟数据以填充属性值。虽然这种方法更简单,但它导致图中的许多空输出。而且,这需要额外的努力来识别涉及幻觉的语句,如虚构的属性:
'MATCH (author:Author)-[:WRITTEN_BY]-(article:Article)-[:UPDATED]-
(updateDate:UpdateDate)
WHERE article.creation_date = updateDate.update_date
RETURN DISTINCT author.first_name, author.last_name;"
Article 节点在 ArXiv 图中没有 creation_date 属性!
💡提示💡
为了减少空输出,我们可以直接从图中提取实例。这些实例可以被纳入提示中,并指示 LLM 使用这些信息来丰富 Cypher 语句。
构建功能查询
这种方法可以生成从数百到数十万条正确的 Cypher 查询,具体取决于图的大小和复杂性。然而,至关重要的是要在查询的数量和多样性之间找到平衡。尽管这些查询在任何图中都是正确且适用的,但它们有时会显得公式化或僵化。
提取有关图结构的信息
对于这个过程,我们需要从一些数据提取和准备开始。我使用了 Cypher 查询和来自 neo4j_graph.py 模块的一些代码。
-
连接到现有的 Neo4j 图形数据库。
-
提取 JSON 格式的模式。
-
从图中提取几个节点和关系实例,即从图中提取数据作为样本,用于填充查询。
我创建了一个执行这些步骤的 Python 类,它可以在 Github 仓库中的 utils/neo4j_schema.py 找到。有了这些,只需几行代码即可提取图的相关数据:
# Initialize the Neo4j connector
graph = Neo4jGraph(url=URI, username=USER, password=PWD)
# Initialize the schema extractor module
gutils = Neo4jSchema(url=URI, username=USER, password=PWD)
# Build the schema as a JSON object
jschema = gutils.get_structured_schema
# Retrieve the list of nodes in the graph
nodes = get_nodes_list(jschema)
# Read the nodes with their properties and their datatypes
node_props_types = jschema['node_props']
# Check the output
print(f"The properties of the node Report are:\n{node_props_types['Report']}")
>>>The properties of the node Report are:
[{'property': 'report_id', 'datatype': 'STRING'}, {'property': 'report_no', 'datatype': 'STRING'}]
# Extract a list of relationships
relationships = jschema['relationships']
# Check the output
relationships[:1]
>>>[{'start': 'Article', 'type': 'HAS_KEY', 'end': 'Keyword'},
{'start': 'Article', 'type': 'HAS_DOI', 'end': 'DOI'}]
从图中提取数据
这些数据将为我们填充 Cypher 查询提供真实的值。
- 首先,我们提取几个节点实例,这将检索图中所有节点的数据,包括标签、属性及其值:
# Extract node samples from the graph - 4 sets of node samples
node_instances = gutils.extract_node_instances(
nodes, # list of nodes to extract labels
4) # how many instances to extract for each node
- 接下来,提取关系实例,包括起始节点、关系的类型和属性以及终止节点信息:
# Extract relationship instances
rels_instances = gutils.extract_multiple_relationships_instances(
relationships, # list of relationships to extract instances for
8) # how many instances to extract for each relationship
💡提示💡
-
上述两种方法适用于完整的节点列表、关系或它们的子列表。
-
如果图中包含缺少某些属性记录的实例,建议收集更多实例,以确保覆盖所有可能的场景。
下一步是序列化数据,通过将 Neo4j.time 的值替换为字符串,并将其保存到文件中。
解析提取的数据
我把这个阶段称为 Python 体操。在这里,我们处理前一步获得的数据,包括图模式、节点实例和关系实例。我们重新格式化这些数据,以使其更易于访问我们正在开发的函数。
- 我们首先使用以下方法识别图中的所有数据类型:
dtypes = retrieve_datatypes(jschema)
dtypes
>>>{'DATE', 'INTEGER', 'STRING'}
-
对于每种数据类型,我们提取具有该数据类型的属性(及相应的节点)。
-
我们解析每种数据类型的实例。
-
我们还会处理和筛选关系,使得起始节点和结束节点具有特定数据类型的属性。
所有代码都可以在Github 仓库中找到。这样做的原因将在下一节中透明化。
如何构建一个或一千个 Cypher 语句
作为一个数学家,我通常通过潜在的函数来理解陈述。让我们考虑以下示例:
q = "Find the Topic whose description contains 'Jordan normal form'!"
cq = "MATCH (n:Topic) WHERE n.description CONTAINS 'Jordan normal form' RETURN n"
上述内容可以视为多个变量f(x, y, z)和g(x, y, z)的函数,其中
f(x, y, z) = f"Find the {x} whose {y} contains {z}!"
q = f('Topic', 'description', 'Jordan normal form')
g(x, y, z) = f"MATCH (n:{x}) WHERE n.{y} CONTAINS {z} RETURN n"
qc = g('Topic', 'description', 'Jordan normal form')
我们能构建多少这种类型的查询?为了简化论证,假设有N个节点标签,每个标签平均有n个属性,这些属性的数据类型为STRING。因此,我们至少可以构建Nxn个查询,不考虑字符串选择z的选项。
💡提示💡
仅仅因为我们能够通过一行代码构建所有这些查询,并不意味着我们应该将整个查询示例集纳入我们的微调数据集中。
制定一个流程和模板
主要挑战在于创建一个足够多样化的查询列表,覆盖与图谱相关的广泛方面。由于专有和开源的大型语言模型(LLM)能够生成基本的 Cypher 语法,我们的重点可以转向生成有关图谱中节点和关系的查询,而忽略特定语法的查询。为了收集查询示例并将其转换为功能形式,可以参考任何 Cypher 语言书籍或访问Neo4j Cypher 文档网站。
在GitHub 仓库中,有大约 60 种此类查询,它们被应用于 ArXiv 知识图谱。这些查询具有多功能性,适用于任何图谱模式。
以下是创建一组相似查询并将其纳入微调数据集的完整 Python 函数:
def find_nodes_connected_to_node_via_relation():
def prompter(label_1, prop_1, rel_1, label_2):
subschema = get_subgraph_schema(jschema, [label_1, label_2], 2, True)
message = {"Prompt": "Convert the following question into a Cypher query using the provided graph schema!",
"Question": f"""For each {label_1}, find the number of {label_2} linked via {rel_1} and retrieve the {prop_1} of the {label_1} and the {label_2} counts in ascending order!""",
"Schema": f"Graph schema: {subschema}",
"Cypher": f"MATCH (n:{label_1}) -[:{rel_1}]->(m:{label_2}) WITH DISTINCT n, m RETURN n.{prop_1} AS {prop_1}, count(m) AS {label_2.lower()}_count ORDER BY {label_2.lower()}_count"
}
return message
sampler=[]
for e in all_rels:
for k, v in e[1].items():
temp_dict = prompter(e[0], k, e[2], e[3])
sampler.append(temp_dict)
return sampler
- 函数 find_nodes_connected_to_node_via_relation()接受生成的提示并对 all_rels 中的所有元素进行评估,all_rels 是提取和处理后的关系实例集合,条目的形式为:
['Keyword',
{'name': 'logarithms', 'key_id': '720452e14ca2e4e07b76fa5a9bc0b5f6'},
'HAS_TOPIC',
'Topic',
{'cluster': 0}]
-
提示输入为两个节点,分别表示为
label_1和label_2,属性prop_1针对label_1,关系rel_1, -
message包含了相应条目在微调数据集中的提示组件, -
subschema提取表示label_1和label_2的两个节点的第一邻居,这意味着:列出的两个节点,它们所有相关的节点(图中的距离为 1),以及关系和所有对应的属性。
💡提示💡
将 *subschema* 包括在微调数据集中并非必要,尽管提示与微调数据的对齐程度越高,生成的输出往往越好。从我的角度来看,将 subschema 纳入微调数据仍然具有优势。
结论
总结来说,本文探索了多种构建微调数据集以从文本生成 Cypher 查询的方法。以下是这些技术的概述,包括它们的优缺点:
LLM 生成的问题和 Cypher 语句对:
-
这种方法在数据收集方面看似直接,但它通常需要过多的数据清理。
-
虽然某些专有的 LLM 产生了较好的结果,但许多开源的 LLM 仍然缺乏生成多种准确 Cypher 语句的能力。
-
当图模式复杂时,这种技术会变得繁琐。
功能性方法或参数查询生成:
-
这种方法可以适应不同的图模式,并允许轻松扩展样本大小。然而,重要的是确保数据不会变得过于重复,并保持多样性。
-
它需要大量的 Python 编程。生成的查询常常显得机械,缺乏对话的语气。
要扩展这些方法之外:
- 图模式可以无缝地融入创建功能查询的框架中。考虑以下问题和 Cypher 语句对:
Question: Which articles were written by the author whose last name is Doe?
Cypher: "MATCH (a:Article) -[:WRITTEN_BY]-> (:Author {last_name: 'Doe') RETURN a"
我们可以通过基本的解析(例如将 WRITTEN_BY 替换为 written by),而不是直接使用参数化,来增强生成问题的自然性。
这突显了图形模式设计和图形实体标注在微调解析构建中的重要性。遵循一般规范,比如使用名词作为节点标签,使用富有提示性的动词作为关系,证明是有益的,并且能够在元素之间创建更自然的对话式联系。
- 最后,至关重要的是不要忽视从图形交互中收集实际用户生成查询的价值。若有可能,通过参数化这些查询或通过其他方法增强它们会非常有用。最终,这种方法的有效性取决于图形设计的具体目标。
为此,重要的是要提到,我的重点是在较简单的 Cypher 查询上。我没有涉及创建或修改图中的数据,也没有涉及图模式,且没有包括 APOC 查询。
你是否能提出其他方法或想法,用于生成这样的微调问题和 Cypher 语句对?
资源
代码
Github 仓库:Knowledge_Graphs_Assortment — 用于构建 ArXiv 知识图谱
Github 仓库:Cypher_Generator — 包含与本文相关的所有代码
数据
• 学术文章仓库:arXiv 数据集,该数据集拥有CC0: 公共领域许可证。
稠密和稀疏嵌入之间的舞蹈:启用 LangChain-Milvus 中的混合搜索
如何在 langchain-milvus 中创建和搜索多向量存储
·发表于 Towards Data Science ·6 分钟阅读·2024 年 11 月 19 日
--
这篇博客由 Omri Levy和 Ohad Eytan 共同撰写,作为我们在* IBM Research Israel 所做工作的一个部分。
简介
最近,我们——IBM 研究院——需要在Milvus向量存储中使用混合搜索。由于我们已经在使用LangChain框架,因此我们决定动手贡献所需的功能,以便在langchain-milvus中启用它。我们添加了对稀疏嵌入的支持(PR)和通过langchain接口进行多向量搜索(PR)。
在这篇博客中,我们将简要介绍稠密嵌入和稀疏嵌入之间的区别,以及如何利用混合搜索来同时使用这两者。我们还将提供一段代码演示,展示如何在langchain-milvus中使用这些新功能。
要使用这篇博客中的代码,你需要安装一些包:
pip install langchain_milvus==0.1.6
pip install langchain-huggingface==0.1.0
pip install "pymilvus[model]==2.4.8"
并导入以下内容:
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_milvus.utils.sparse import BM25SparseEmbedding
from langchain_milvus.vectorstores import Milvus
你还可以在这个 Gist中查看并克隆完整代码。
让我们开始吧。
稠密嵌入
使用向量存储的最常见方式是稠密嵌入。在这里,我们使用一个预训练模型将数据(通常是文本,但也可以是其他媒体如图片等)嵌入到高维向量中,并将其存储在向量数据库中。这些向量有几百个(甚至几千个)维度,每个条目是一个浮动点数。通常,向量中的所有条目都被非零值占据,因此称为“稠密”。给定查询,我们使用相同的模型将其嵌入,向量存储根据向量相似性检索相关的相似数据。使用langchain-milvus,只需要几行代码。让我们看看是如何完成的。
首先,我们使用来自 HuggingFace 的模型来定义向量存储:
dense_embedding = HuggingFaceEmbeddings(model_name=
"sentence-transformers/all-MiniLM-L6-v2")
vector_store = Milvus(
embedding_function=dense_embedding,
connection_args={"uri": "./milvus_dense.db"}, # Using milvus-lite for simplicity
auto_id=True,
)
第二步,我们将数据插入到向量存储中:
document = [
"Today was very warm during the day but cold at night",
"In Israel, Hot is a TV provider that broadcasts 7 days a week",
]
vector_store.add_texts(documents)
在后台,每个文档都通过我们提供的模型嵌入到一个向量中,并与原始文本一起存储。
最后,我们可以搜索查询并打印出得到的结果:
query = "What is the weather? is it hot?"
dense_output = vector_store.similarity_search(query=query, k=1)
print(f"Dense embeddings results:\n{dense_output[0].page_content}\n")
# output: Dense embeddings results:
# Today was very warm during the day but cold at night
在这里,查询被嵌入,向量存储进行(通常是近似的)相似性搜索,并返回它找到的最接近的内容。
稠密嵌入模型经过训练,能够捕捉数据的语义意义并将其表示在多维空间中。这个优点非常明显——它使得语义搜索成为可能,这意味着结果是基于查询的含义进行的。但有时这还不够。如果你寻找特定的关键词,甚至是没有更广泛意义的词(如名称),语义搜索可能会误导你,这种方法将会失败。
稀疏嵌入
在大规模语言模型(LLM)流行之前,且学习模型尚未广泛应用时,搜索引擎使用了传统方法,如TF-IDF或其现代增强版BM25(它在Elastic中的应用而闻名),来搜索相关数据。使用这些方法时,维度的数量是词汇表的大小(通常是数万个,远大于稠密向量空间),每个条目代表关键词与文档的相关性,同时考虑到该术语的频率及其在文档语料库中的稀有性。对于每个数据点,大多数条目都是零(对于不出现的词),因此被称为“稀疏”。尽管在底层实现有所不同,但通过langchain-milvus接口,它变得非常相似。让我们看看它是如何运作的:
sparse_embedding = BM25SparseEmbedding(corpus=documents)
vector_store = Milvus(
embedding_function=sparse_embedding,
connection_args={"uri": "./milvus_sparse.db"},
auto_id=True,
)
vector_store.add_texts(documents)
query = "Does Hot cover weather changes during weekends?"
sparse_output = vector_store.similarity_search(query=query, k=1)
print(f"Sparse embeddings results:\n{sparse_output[0].page_content}\n")
# output: Sparse embeddings results:
# In Israel, Hot is a TV provider that broadcast 7 days a week
BM25 在精确的关键词匹配中非常有效,对于缺乏明确语义意义的术语或名称非常有用。然而,它无法捕捉查询的意图,并且在许多需要语义理解的情况下会产生不好的结果。
注意:“稀疏嵌入”一词还指代像 SPLADE 或 Elastic Elser 这样的先进方法。这些方法也可以与 Milvus 一起使用,并且可以集成到混合搜索中!

图片来源:作者
混合搜索
如果你交换上面两个示例中的查询,并用对方的嵌入进行搜索,两者都会产生错误的结果。这证明了每种方法都有其优点,但也有其缺点。混合搜索将两者结合,旨在发挥两者的最佳优势。通过使用稠密和稀疏嵌入索引数据,我们可以执行既考虑语义相关性又考虑关键词匹配的搜索,并根据自定义权重平衡结果。同样,内部实现更为复杂,但langchain-milvus使得使用起来相当简单。让我们看看它是如何工作的:
vector_store = Milvus(
embedding_function=[
sparse_embedding,
dense_embedding,
],
connection_args={"uri": "./milvus_hybrid.db"},
auto_id=True,
)
vector_store.add_texts(documents)
在这个设置中,应用了稀疏和稠密嵌入。让我们用相等的权重来测试混合搜索:
query = "Does Hot cover weather changes during weekends?"
hybrid_output = vector_store.similarity_search(
query=query,
k=1,
ranker_type="weighted",
ranker_params={"weights": [0.49, 0.51]}, # Combine both results!
)
print(f"Hybrid search results:\n{hybrid_output[0].page_content}")
# output: Hybrid search results:
# In Israel, Hot is a TV provider that broadcast 7 days a week
这会使用每个嵌入函数搜索相似的结果,给每个得分加上权重,并返回得分最高的结果。我们可以看到,稍微增加对稠密嵌入的权重后,我们得到了期望的结果。第二个查询也是如此。
如果我们给稠密嵌入更多的权重,我们将再次得到无关的结果,就像只有稠密嵌入时那样:
query = "When and where is Hot active?"
hybrid_output = vector_store.similarity_search(
query=query,
k=1,
ranker_type="weighted",
ranker_params={"weights": [0.2, 0.8]}, # Note -> the weights changed
)
print(f"Hybrid search results:\n{hybrid_output[0].page_content}")
# output: Hybrid search results:
# Today was very warm during the day but cold at night
在稠密和稀疏之间找到正确的平衡并非一项简单的任务,可以看作是更广泛的超参数优化问题的一部分。当前在这一领域有正在进行的研究和工具,试图解决这类问题,例如IBM 的 AutoAI for RAG。
你可以通过多种方式适应并使用混合搜索方法。例如,如果每个文档都有一个相关的标题,你可以使用两个稠密嵌入函数(可能使用不同的模型)——一个用于标题,另一个用于文档内容——并在两个索引上执行混合搜索。Milvus 目前支持最多 10 个不同的向量字段,为复杂的应用提供了灵活性。还提供了用于索引和重新排序方法的额外配置。你可以查看Milvus 文档,了解可用的参数和选项。
结束语
现在通过 LangChain 可以轻松访问 Milvus 的多向量搜索功能,你可以轻松地将混合搜索集成到你的应用程序中。这为你在应用中应用不同的搜索策略打开了新的可能性,使得根据具体用例定制搜索逻辑变得更加容易。对我们来说,这是一个为开源项目做贡献的好机会。我们日常使用的许多库和工具都是开源的,能够回馈社区感到非常高兴。希望它能对其他人有所帮助。
最后,特别感谢Erick Friis和Cheng Zi在langchain-milvus项目中的所有努力,特别是在这些 PR 中。如果没有他们的付出,这项工作是无法完成的。
Dask DataFrame 现在非常快速
Dask 如何高效地在 TB 级别处理数据
·发布于Towards Data Science ·阅读时间 9 分钟·2024 年 5 月 27 日
--

Performance Improvements for Dask DataFrames — 所有图片由作者创建
介绍
Dask DataFrame 将 pandas DataFrame 扩展到 100GB-100TB 的规模进行操作。
从历史上看,Dask 相比于该领域的其他工具(如 Spark)要慢得多。通过专注于性能的多项改进,它现在非常快速(比以前快大约 20 倍)。新的实现使 Dask 从在每个基准测试中都被 Spark 完全碾压,变成了在 TPC-H 查询中经常大幅超越 Spark。
Dask DataFrame 的工作负载面临许多挑战。性能和内存使用常常是主要的痛点,数据集较大时洗牌操作不稳定,导致扩展变得困难。编写高效代码需要了解 Dask 的很多内部细节。
新的实现改变了这一切。那些无法正常工作的部分被完全从头重写,现有的实现也得到了改进。这为 Dask DataFrame 打下了坚实的基础,未来可以进行更快速的迭代。
我们将讨论三项最显著的变化,介绍它们如何提升性能并使 Dask 更易于使用,即使是对分布式计算较为陌生的用户。我们还将讨论未来改进的计划。
我是 Dask 核心团队的一员,还是Coiled的开源工程师,参与了本文中讨论的一些改进的实现。
1. Apache Arrow 支持:高效的字符串数据类型
Dask DataFrame 由多个 pandas DataFrame 组成。历史上,pandas 使用 NumPy 处理数值数据,但对文本数据使用 Python 对象,这样做效率低下,并且会极大增加内存使用。对对象数据的操作也会占用 GIL(全局解释器锁),这对于 pandas 影响不大,但对于像 Dask 这样的并行系统来说,性能将受到灾难性的影响。
pandas 2.0 发布时引入了对通用 Arrow 数据类型的支持,因此 Dask 现在默认使用 PyArrow 支持的字符串。这些字符串要好得多。PyArrow 字符串将内存使用量减少了多达 80%,并且解锁了字符串操作的多线程功能。以前因内存不足而遇到问题的工作负载,现在能够在更少的内存中顺利运行,而且速度更快,因为它们不再不断将多余的数据溢写到磁盘。

与 Arrow 字符串相比,传统 DataFrame 的内存使用
我写了一篇关于此内容的文章,详细探讨了 Arrow 集成,如果你想了解更多,可以阅读。
2. 使用新洗牌算法加速连接操作
洗牌是分布式系统中的一个关键组件,用于实现排序、连接和复杂的分组操作。它是一种全对全、网络密集型的操作,通常是工作流中最昂贵的部分。我们重写了 Dask 的洗牌系统,这对整体性能产生了巨大影响,尤其是在复杂的数据密集型工作负载下。
洗牌操作本质上是一种全对全的通信操作,其中每个输入分区必须将一小部分数据提供给每个输出分区。Dask 最初使用了自己的基于任务的算法,成功地将 O(n * n) 的任务复杂度减少到了 O(log(n) * n),其中 n 是分区数量。这大大减少了任务数量,但非线性扩展性最终使得 Dask 无法处理任意大的数据集。
Dask 引入了一种新的 P2P(点对点)洗牌方法,将任务复杂度降低到了 O(n),并且随着数据集大小和集群规模的增长呈线性扩展。它还集成了高效的磁盘功能,使得可以轻松地洗牌大于内存的数据集。新系统极其稳定,并且在任何规模的数据上都能“正常工作”。

与 P2P 相比,传统洗牌的内存使用
我的一个同事写了关于此内容的文章,其中包含了更为详细的解释和大量技术细节。
3. 优化器
Dask 本身是惰性计算的,这意味着它会在执行任何实际工作之前注册整个查询。这是一个强大的概念,使得许多优化成为可能,但历史上 Dask 并没有充分利用这一知识。Dask 在隐藏内部复杂性方面做得不好,用户在应对分布式计算和执行大规模查询的困难时只能靠自己。这使得非专家编写高效代码变得非常痛苦。
3 月发布的 Dask 版本包含了 DataFrame API 的完全重新实现,以支持查询优化。这是一个大事件。新的引擎围绕查询优化器展开,优化器会重写我们的代码,使其更加高效,更好地发挥 Dask 的优势。让我们深入了解一些优化策略,它们如何使我们的代码运行得更快,并更好地扩展。
在深入探讨一些针对分布式系统和 Dask 更具体的技术之前,我们将从一些对每个类似 DataFrame 的工具都有用的通用优化开始。
3.1 列投影
大多数数据集的列数超过我们实际需要的列数。去除不必要的列需要前瞻性思维(“我在这个查询中需要哪些列?🤔”),因此大多数人在加载数据时不会考虑这一点。这对性能不好,因为我们携带了许多不需要的数据,导致一切变得更慢。列投影在不再需要列时立即去除它们。这是一个简单的优化,但却非常有益。
传统实现总是从存储中读取所有列,只有在我们主动要求时才去除列。仅仅减少操作的数据量就能大幅提升性能和内存使用效率。
优化器查看查询并确定每个操作所需的列。我们可以将其想象成从查询的最终步骤开始,然后一步步回溯到数据源,并注入丢弃操作以去除不必要的列。

最终我们只需要一个子集的列。替换操作不需要访问所有列,因此我们可以在 IO 步骤中直接丢弃不必要的列。
3.2 过滤下推
过滤下推是另一种通用优化,其目标与列投影相同:操作较少的数据。传统实现只是将过滤器保留在我们放置的位置。新的实现尽可能早地执行过滤操作,同时保持相同的结果。
优化器识别我们查询中的每个过滤器,并查看之前的操作,判断是否可以将过滤器移得更靠近数据源。它会重复这一过程,直到找到一个不能与过滤器交换的操作。这比列投影要难一些,因为我们需要确保这些操作不会改变数据框的值。例如,交换过滤器和合并操作是可以的(值不变),但交换过滤器和替换操作是无效的,因为我们的值可能会改变,原本会被过滤掉的行现在可能不会被过滤掉,反之亦然。

最初,过滤操作发生在 Dropna 之后,但我们可以在不改变结果的情况下,在 Dropna 之前执行过滤操作。这允许我们将过滤器推送到 IO 步骤中。
此外,如果我们的过滤器足够强大,那么我们可能在 IO 步骤中直接丢弃完整的文件。这是最佳情况,其中早期的过滤操作带来了巨大的性能提升,甚至需要从远程存储读取更少的数据。
3.3 自动调整分区大小
除了实现上述常见的优化技术外,我们还改进了一个普遍存在的痛点,特别是对于分布式系统和 Dask 用户:最佳分区大小。
Dask DataFrame 由许多称为分区的小型 pandas DataFrame 组成。通常,分区的数量是由系统决定的,Dask 用户被建议在减少或扩展数据后手动“重新分区”(例如通过删除列、过滤数据或通过连接操作进行扩展)(参见Dask 文档)。如果没有这一步,Dask 的(通常很小的)开销可能会成为瓶颈,特别是当 pandas DataFrame 变得过小时,这会使 Dask 的工作流变得非常缓慢。
手动控制分区大小是一项困难的任务,我们作为 Dask 用户不应该为此担忧。这也很慢,因为它需要通过网络传输一些分区。现在,Dask DataFrame 会自动做两件事来帮助处理当分区变得过小的情况:
-
保持每个分区的大小恒定,基于你想要计算的数据与原始文件大小的比例。例如,如果你筛选掉原始数据集的 80%,Dask 会自动将结果中较小的分区合并成更少、更大的分区。
-
基于绝对最小值(默认为 75MB),将过小的分区合并为更大的分区。例如,如果你的原始数据集被拆分成许多很小的文件,Dask 会自动将它们合并。

我们从整个文件的 200MB 中选择了两个占用 40MB 内存的列。
优化器会查看列的数量以及这些列中的数据大小。它会计算一个比率,用于将多个文件合并为一个分区。

40/200 的比率导致将五个文件合并为一个分区。
目前,这一步骤仅限于 IO 操作(如读取 Parquet 数据集),但我们计划将其扩展到其他允许廉价合并分区的操作。
3.4 简单的合并和连接操作
在单机上使用 Pandas 时,合并和连接操作通常是便宜的,但在分布式环境中则很昂贵。在共享内存中合并数据是便宜的,而通过网络合并数据则非常慢,因为前面提到的洗牌操作。
这是分布式系统中最昂贵的操作之一。传统实现每次合并操作都会触发输入 DataFrame 的网络传输。这在某些情况下是必要的,但代价高昂。

两个连接操作都是在同一列上进行的。左侧的 DataFrame 在第一次连接后已经正确分区,因此我们可以避免在新实现中再次进行洗牌操作。
优化器将判断何时需要洗牌,何时仅仅是简单的连接足够,因为数据已经对齐。这可以使个别合并操作变得更快。这个原则同样适用于其他通常需要洗牌的操作,如groupby().apply()。
Dask 的合并操作曾经效率低下,导致运行时间过长。优化器解决了这些操作在彼此之后发生时的简单情况,但该技术目前还不够先进。仍然有很大的改进空间。

当前的实现会对来自同一表的两个分支进行洗牌。通过在更高层次插入一个洗牌节点,可以避免其中一个昂贵的操作。
优化器会查看表达式,并在必要时插入洗牌节点,以避免不必要的洗牌操作。
相较于传统实现,这些改进的效果如何?
Dask 现在比以前快了 20 倍。这个改进适用于整个 DataFrame API(不仅仅是某些独立的组件),且没有已知的性能回归。Dask 现在能够运行以前无法在可接受的时间内完成的工作负载。这个性能提升是由于多个改进的叠加。它不是做一件事情特别好,而是避免做任何事情特别差。

TPC-H 基准测试中的查询 3 性能提升,来自[github.com/coiled/benchmarks/tree/main/tests/tpch](https://github.com/coiled/benchmarks/tree/main/tests/tpch_)
性能虽然是最吸引人的改进,但并不是唯一得到改善的地方。优化器隐藏了很多复杂性,使得用户从 pandas 迁移到 Dask 变得更加容易,因为现在写出性能差的代码变得更加困难。整个系统变得更加健壮。
新的 API 架构也更容易使用。旧版实现将许多内部复杂性泄露到高层 API 中,导致更改变得繁琐。现在,改进几乎是轻松易行的。
接下来会发生什么?
在过去 18 个月中,Dask DataFrame 发生了很大变化。旧版 API 通常难以使用,且在扩展性方面表现较差。新的实现去除了不奏效的部分,并改进了现有实现。现在,繁重的工作已经完成,这使得加快迭代周期、改进现状成为可能。增量改进现在变得轻而易举。
一些即将实施的事项:
-
自动重新分区: 这部分已经实现,但在优化过程中仍有更多潜力选择更高效的分区大小。
-
更快的连接: 这里仍有很多精细调整的空间。例如,我们有一个 PR 正在进行,其中带来了 30-40%的性能提升。
-
连接重排序: 我们还没有做到这一点,但它在即将实施的事项中。
了解更多
本文重点介绍了对 Dask DataFrame 的多个改进,以及这些改进带来了更快、更可靠的表现。如果你在选择 Dask 和其他流行的 DataFrame 工具时,可能还需要考虑:
- 大规模数据框比较: TPC-H,该报告比较了 Dask、Spark、Polars 和 DuckDB 在从 10 GB 到 10 TB 的数据集上的性能,涵盖本地和云端。
感谢阅读。欢迎随时联系我们,分享您的想法和反馈。
使用命名的 Lambda 函数进行数据分析
PYTHON 编程
你不应该在 Python 中使用命名的匿名函数。永远不可以。真的吗?
·发表于Towards Data Science ·5 分钟阅读·2024 年 3 月 13 日
--

Lambda 函数在数据科学中可以非常有用——不仅仅是匿名的函数。照片来自Daniel Monteiro在Unsplash
从技术上讲,你不应该使用命名的 lambda 函数,因为这就像给一个本质上是匿名的函数命名:
## 你应该使用 lambda 定义命名的 Python 函数吗?
这样做违反了 PEP8 规范,那为什么那么多作者还建议这么做呢?
towardsdatascience.com
在实际代码中,尤其是在生产环境中,我从不做这样的事情——你也不应该这么做。Lambda 函数是为特定情况保留的——而这些特定情况不包括给匿名函数命名。
这是我在上文中提到的文章中写的内容:
我真的希望我已经说服了你。即使两种函数定义方式看起来对你来说都没问题——即便如此,我也不会使用命名的
lambda定义。原因在于,使用它们,你并不会获得任何好处,同时还冒着别人不同意你的风险。如果这一点还不能说服你,请记住,这样做是违背 PEP8 规范的。
这完全正确。但……
数据分析代码是个例外吗?
数据架构:经验教训
作为数据工程师和架构师,我在这段旅程中学到的三条重要经验
·发表于Towards Data Science ·阅读时间 10 分钟·2024 年 10 月 4 日
--

图片由Michael SKOPAL提供,来源于Unsplash
仅仅过了几个月,我再次不得不经历我有时所说的“IT 的自我满足”。
这听起来可能有些严厉,但不幸的是,我一次又一次地经历了这种情况。当看到IT 部门实际上与业务背道而驰时,确实令人沮丧。
我记得有一个具体的案例,一个正在运行的业务解决方案必须迁移到另一个执行平台,仅仅是因为‘技术’原因。当然,业务部门被告知这个目标平台在维护上会便宜很多,但 IT 部门没有提供任何实质性的证据来支持这一说法。最终,迁移的决定是由‘专家知识’和所谓的‘最佳实践’推动的,但这完全是从 IT 中心的视角出发。迁移原本正常运行的系统花费了大量资金,最终发现承诺的成本节约并未实现,更糟糕的是,某些业务功能甚至退化了。
IT 专业人员,不仅在特定的技术导向型公司中,往往认为技术、IT 工具,甚至如今的数据本身就是目的。
没有什么比这更远离事实了。
AI 在科学中的数据增强:一项地球科学案例研究
用物理仿真增强观测数据能够提高 AI 模型在科学研究中的表现
·发表于Towards Data Science ·6 分钟阅读·2024 年 5 月 22 日
--

来源:USGS(Unsplash)
背景
在大数据时代,AI 通过预测、仿真和知识挖掘推动科学研究。AI for Science(AI4Science)正在成为一种新的科学范式。使用 AI 的一个障碍是数据有限——尤其是观测的地面真实数据。幸运的是,这些数据可以通过科学界的另一种数据来源得到增强,即通过物理模型生成的模拟数据。
为什么使用物理仿真来增强数据?
数据增强是提高 AI 模型性能的强大技术。以下是结合模拟数据的三个关键原因:

图 1. 使用模拟数据进行数据增强的好处。来源:作者。
-
更多数据:由于数据收集成本高,观测数据通常是有限的。模拟数据提供了一个更大规模的数据集,且成本更低。
-
泛化:观测数据分布不均…
优秀的数据业务是什么?
包括一个易于使用的数据业务评估备忘单
·发表于Towards Data Science ·7 分钟阅读·2024 年 9 月 16 日
--

作者通过 Microsoft Designer 制作的图片
最近,我受邀向一群私募股权投资者演讲,内容是关于我的职业生涯、我从中学到的东西以及我从这些经验中看到的机会。
鉴于我对数据业务的熟悉,我决定围绕这一主题进行总结,特别是如何快速识别和评估数据业务。
为什么选择数据业务?因为它们可以是具有极高毛利率的杰出业务——与软件即服务(SaaS)相媲美,甚至更好。通常,数据业务可以是它们所服务行业中的最佳业务。
为了帮助我在这个话题上理清思路,并为投资者提供一个框架,帮助他们从中分辨出优劣,我创建了一个数据业务评估备忘单。
对于那些渴望了解数据业务定义的人,让我们宽泛地定义——指的是销售某种形式的数据或信息的企业。
我将评估标准分为四类:数据来源、数据用途、可选项和商业模式。
每个评估标准都有不同的细节,虽然这些细节按它们通常能创造的价值多少列在备忘单的各列中,但需要强调的是,许多成功的数据业务都是基于每个标准创立的,甚至包括排名较低的标准,或它们的组合。

图片由 Dan Beltramo 提供
数据来源:
一个数据业务如何生产其出售的数据对业务的价值至关重要,主要体现在建立相对于竞争对手的护城河和/或创造有意义的差异化点上。
通常,最好的数据来源是公司通过一个难以复制的成本高昂过程有意生产的专有数据,例如由Onclusive运营的电视广播捕捉网络。有时,一家公司足够幸运,通过其核心业务操作产生这种数据资产,如来自Glassdoor的薪资数据。数据排放物可以用于支持产生它的同一公司的其他部分,比如亚马逊利用购买数据来推动其广告业务。有时,数据排放物用于推动独立的数据业务或多个业务,例如NCS,它利用来自 Catalina Marketing 优惠券业务的注册数据。
在有价值的数据来源中,紧随其后的是那些在收集数据时受益于网络效应的企业。通常,这里有一个互惠互利的结构,在这个结构中,每个数据提供者都会因提供数据而获得一些有价值的数据。当每个新增参与者的参与使每个人都能受益时,就会出现网络效应。TruthSet就是一个典型例子,它通过收集来自数据提供者的 demographic 数据,并将其数据的准确性评级返回给他们,从而运行着一个由网络效应驱动的互惠互利模式。
数据聚合也是构建数据资产的一种有价值方式,但其价值通常取决于组装数据的难度……如果太容易做到,其他人也会这样做并造成价格竞争。通常,价值来自于聚合那些一次性组装成本较高的数据,无论是对供应商还是对竞争聚合商而言都如此。例如,Equilar通过汇总无数公司文件中的薪酬数据来创建薪酬基准数据。有趣的是,Equilar 利用其薪酬数据业务中的数据排放物,这些数据业务是它卖给人力资源部门的,用于创建一个关系映射业务,并将其出售给销售和营销部门以及金融服务组织中的交易团队。
关键数据来源的补充包括数据增强和数据分析。数据增强通过增强其他数据源来创造价值,有时通过添加专有数据、将数据与其他数据混合、结构化数据等方式进行。数据分析公司通过从其他来源获取数据并通过洞察将庞大的数据变得更加易于理解和可操作,进而在行业中占据一席之地。
数据用途:
当你能够找到一个在公司之间使用的数据集时,它可能非常有价值,因为它通常很难被取代。最好的例子就是当其他公司基于这些数据进行价值交换,而这些数据会定期波动。在这种情况下,数据已经变成了货币。这方面的例子包括 FICO 的 FICO 评分或尼尔森的电视收视数据。在特殊情况下,公司之间使用的数据还可以受益于网络效应,这种效应往往会为数据提供者创造价值。事实上,正是网络效应推动了数据集成为一种货币。
用于驱动工作流的数据也非常有价值,因为如果要远离它,可能需要客户改变他们的工作方式。数据支持的行为和决策越频繁、价值越高,就越好。
通常,数据集在客户中的使用者越多,且能够利用这些数据的公司类型越多,它的价值就越高。评估这一点的一种方法是观察一个数据集与其他数据集结合的频率和容易程度。相关地,一些公司通过提供唯一标识符来支持数据的结合,举例如Datavant提供的患者 ID,以促进医疗数据的交换。其他公司,如邓白氏(Dun & Bradstreet),则使用诸如 DUNS 号码这样的唯一标识符来支持其业务的其他部分——在这种情况下是信用报告。
数据附加功能:
数据附加功能是数据业务的特点,这些功能通常不足以独立支撑一个数据业务,但可以显著提升数据业务的价值。基准/规范和历史/纵向数据通常是存在已久的数据业务的特征,且很难复制。同样的情况也适用于品牌数据,尽管有时一旦品牌建立,品牌可以扩展到新的数据集——例如 J.D. Power 将其名称应用于其多年来的收购。
更新:我想特别提一下的一个附加功能是公司的数据模型,即公司如何组织数据以及这些数据元素之间是如何关联的,尤其是这些数据如何在最终交付物或软件/商业智能平台中结合在一起。理想情况下,数据模型的一部分应该包括一种管理“数据血统”的方式,即理解数据在系统中流动的来源和用途。有时,数据模型的架构(数据架构)可能会在未来影响某些产品决策。如果产品需求发生重大变化,或者产品需要合并(比如在收购之后),这可能会成为一个特别的困扰。
数据商业模型:
最高毛利率的数据业务通常是那些采用联合商业模式的企业,这些企业通过不进行定制或者通过由客户自动处理的配置,反复向不同的客户销售相同的数据集。最稳定的数据业务倾向于采用订阅商业模式,客户订阅一个数据集并持续较长时间。显然,当订阅长期或至少是自动续订时,订阅模式更为理想。
毫不奇怪,最佳的数据业务通常是联合订阅模式。而另一方面,定制数据业务以一次性或基于项目的方式为客户提供数据,通常难以获得高利润率和可预测性,但如果数据制造过程得到优化,并且从其他一些关键的数据业务特性中受益,它们也可以成为稳固的业务。
财务情况:
本概述及附带的备忘单旨在分析数据业务的基本原理,而非深入探讨典型财务情况。话虽如此,典型的健康数据业务通常拥有 65%到 75%的毛利率和 25%到 30%的 EBITDA(息税折旧摊销前利润)。对于那些具备上述最佳特性的杰出企业,这两个数字可以更高。即使在相对增长较慢的行业中,由于它们的韧性以及通常只需要适度的研发和营销费用,这些企业的 EBITDA 倍数也可以达到 15 到 18 倍。
我给这群专注于各个垂直领域的投资者的离别寄语是,尽管他们可能不认为自己是数据业务专家,但他们应该关注自己专长领域中的优秀数据业务。正是他们对各自行业的详细理解以及他们的公司依赖的数据,能指引他们走上正确的道路,并可能为那些投资数据业务的合作伙伴发现一些数据业务的宝藏。
我想感谢几位数据业务爱好者,他们帮助启发并完善了我对这个话题的思考:John Burbank, David Clark, Conor Flemming, Andrew Feigenson, Travis May, 以及 Andrew Somosi。特别是,我推荐 Travis 的相关 文章,这是我在此话题上的重要灵感来源,可以作为额外的阅读材料。
最小化医疗人工智能偏差的数据策划实践
确保医疗人工智能应用程序的公平和公正的健康结果
·发表于 Towards Data Science ·阅读时间 8 分钟·2024 年 7 月 17 日
--

人工智能训练数据中的潜在偏差来源。图形由作者创建。
人工智能偏见 是指当人工智能系统由于训练数据中的偏见而对不同群体产生不平等的结果时,所发生的歧视现象。如果不加以缓解,人工智能和机器学习模型中的偏见可能会使历史上被边缘化的群体面临的歧视制度化并加剧,将歧视嵌入到决策算法中。
训练数据中的问题,如数据集不具代表性或不平衡、历史偏见嵌入数据中以及数据收集方法存在缺陷,都会导致模型产生偏差。例如,如果一个 贷款决策应用程序是基于历史决策进行训练的,而历史决策中黑人贷款申请者遭受了系统性的歧视,那么模型将在其决策过程中嵌入这一歧视模式。偏差还可能在特征选择和工程阶段引入,其中某些特征可能无意中充当种族、性别或社会经济地位等敏感特征的替代指标。例如,美国的 种族和邮政编码通常是密切相关的,因此,使用邮政编码数据训练的算法可能会间接地将种族信息嵌入到其决策过程中。
医疗领域中的人工智能涉及使用机器学习模型和算法来辅助诊断、治疗计划制定和患者护理。在这些情况下,AI 偏见可能特别有害,导致医疗服务和结果的显著差异。例如,使用主要以浅色皮肤图像训练的皮肤癌预测模型可能在肤色较深的患者身上表现不佳。这样的系统可能导致误诊或延迟治疗,从而导致较高的死亡率。鉴于医疗应用中的高风险,数据科学家必须采取措施减少其应用中的 AI 偏见。本文将重点讨论数据科学家可以采取哪些数据策划技术,以在模型训练之前消除训练集中的偏见。
如何衡量 AI 偏见?
为了减轻 AI 偏见,了解模型偏见和公平性如何定义(PDF)并进行衡量是非常重要的。公平/无偏的模型确保其预测在不同群体之间是公平的。这意味着模型的行为(如准确性和选择概率)在由敏感特征(如种族、性别、社会经济地位)定义的子群体之间是可比的。
通过使用量化的 AI 公平性/偏见指标,我们可以衡量并改善自己的模型。这些指标比较历史上处于特权群体和非特权群体之间的准确率和选择概率。常用的三种衡量 AI 模型对不同群体是否公平的指标是:
统计平衡差异——比较群体之间有利结果的比率。此测试表明,模型的预测与敏感群体成员身份无关,旨在实现群体间的平等选择率。在需要群体之间有相等正向结果的情况下非常有用,例如招聘。

平均赔率差异——比较不同群体之间的假阳性和真阳性率差异。此指标比统计平衡差异更为严格,因为它旨在确保群体之间假阳性和真阳性率相等。在假阳性和真阳性都有重大后果的情况下非常有用,例如刑事司法领域。

机会平等差异——比较不同群体之间的真阳性率。它检查不同群体中的合格个体是否有平等的机会被 AI 系统选中。它不考虑假阳性率,这可能会导致群体之间在错误正预测上的差异。

数据科学家可以使用 Python 库,如微软的 Fairlearn 包或 IBM 的 AI Fairness 360 工具包,来计算模型的公平性/偏见度量指标。对于所有这些指标,值为零表示数学上公平的结果。
什么样的数据管理实践可以最小化人工智能偏见?
为了减少人工智能训练数据集中的偏见,模型构建者拥有一系列的数据管理技术,这些技术可以分为定量方法(使用数学包进行数据转换)和定性方法(数据收集的最佳实践)。
定量实践

在一个示例数据集中移除相关性的示意图。图片来自作者。
即使在模型训练中排除了敏感特征(例如,种族、性别),其他特征仍可能与这些敏感特征相关联,从而引入偏见。例如,在美国,邮政编码与种族有很强的相关性。为了确保这些特征不会引入隐藏的偏见,数据科学家应当预处理输入数据,移除其他输入特征与敏感特征之间的相关性。
这可以通过 Fairlearn 的 CorrelationRemover 函数来实现。该函数通过数学方法转换特征值,移除相关性,同时保留特征的大部分预测价值。下面是一个示例代码。
from fairlearn.preprocessing import CorrelationRemover
import pandas as pd
data = pd.read_csv('health_data.csv')
X = data[["patient_id", "num_chest_nodules", "insurance", "hospital_code"]]
X = pd.get_dummies(X)
cr = CorrelationRemover(sensitive_feature_ids=['insurance_None'])
cr.fit(X)
X_corr_removed = cr.transform(X)
重新加权和重采样是类似的过程,它们通过创建更平衡的训练数据集来纠正特定群体在输入集中的过度或不足表示。重新加权涉及为数据样本分配不同的权重,以确保被低估的群体对模型学习过程有相应的影响。重采样则是通过过采样少数类实例或欠采样多数类实例来实现数据集的平衡。
如果某个敏感群体在与一般人群相比时被低估,数据科学家可以使用 AI Fairness 的 Reweighing 函数来转换数据输入。下面是示例代码。
from aif360.algorithms.preprocessing import Reweighing
import pandas as pd
data = pd.read_csv('health_data.csv')
X = data[["patient_id", "num_chest_nodules", "insurance_provider", "hospital_code"]]
X = pd.get_dummies(X)
rw = Reweighing(unprivileged_groups=['insurance_None'],
privileged_groups=['insurance_Aetna', 'insurance_BlueCross'])
rw.fit(X)
X_reweighted = rw.transform(X)
移除训练数据中嵌入的偏见的另一种技术是使用不均衡影响移除器转换输入特征。这项技术通过调整特征值,在由敏感特征定义的群体之间提高公平性,同时保持群体内数据的排序。这既能保持模型的预测能力,又能减少偏见。
要转化特征以消除不公平影响,可以使用AI 公平性工具中的不公平影响消除器。请注意,该工具仅会根据单一保护属性转化输入数据的公平性,因此无法改善多个敏感特征或敏感特征交集中的公平性。以下是示例代码。
from aif360.algorithms.preprocessing import disparate_impact_remover
import pandas as pd
data = pd.read_csv('health_data.csv')
X = data[["patient_id", "num_chest_nodules", "insurance_provider", "hospital_code"]]
dr = DisparateImpactRemover(repair_level=1.0, sensitive_attribute='insurance_provider')
X_impact_removed = dr.fit_transform(X)
对于监督学习的应用场景,通常需要对响应变量进行人工数据标注。在这些情况下,不完美的人工数据标注员会将个人偏见引入数据集,然后这些偏见会被机器学习到。当标注员群体较小且缺乏多样性时,这种偏差会更加严重。
为了最小化数据标注过程中的偏差,使用高质量的数据标注解决方案,利用多样化的专家意见,如Centaur Labs。通过使用优越的标签置信度度量来算法性地合成多个意见,这样的解决方案可以缓解个体偏差的影响,并推动标注准确性的巨大提升,从而提高数据集的标注质量。

展示如何通过汇总多个专家意见来提高医疗数据标注的准确性。图像由作者提供。
定性实践
实施包容性和具有代表性的数据收集实践
医疗 AI 训练数据必须包含来自所有患者人口群体和条件的足够样本,以便准确地为多样化的患者群体做出预测。为了确保数据集满足这些需求,应用开发者应与相关的医学专家和利益相关者合作,代表受影响的患者群体来定义数据要求。数据科学家可以使用分层抽样来确保他们的训练集不会过度或不足地代表感兴趣的群体。
数据科学家还必须确保收集技术不会引入数据偏差。例如,如果医疗影像设备在不同样本之间不一致,这将会引入系统性的差异。
确保数据清理实践不引入偏差
为避免在数据清洗过程中产生偏差,数据科学家必须谨慎处理缺失数据并填补缺失值。当数据集中存在敏感特征(如患者年龄)的缺失值时,简单的策略(如填补平均年龄)可能会扭曲数据,特别是当某些年龄组的样本不足时。相反,可以使用分层插补技术,根据相关子组(例如按年龄段或人口统计类别)内的分布填补缺失值。根据具体情况,像多重插补这样的高级方法,也可以生成多个合理的值并对其进行平均,以考虑不确定性。数据清洗完成后,数据科学家应记录插补过程,并确保清理后的数据集仍然代表性强且无偏,符合预定的标准。
发布数据整理实践,征求利益相关者的意见
数据科学家在制定数据整理程序时,应将其发布以征求利益相关者的意见,从而促进透明度和问责制。当利益相关者(例如患者群体代表、研究人员和伦理学家)审查并提供有关数据整理方法的反馈时,有助于在开发过程中尽早识别和解决潜在的偏差来源。此外,利益相关者的参与通过展示对道德和包容性实践的承诺,促进了对 AI 系统的信任和信心。这种信任对于推动 AI 系统的部署后使用至关重要。
定期审计和审查输入数据和模型性能
定期审计和审查实时模型的输入数据可确保训练集中的偏差不会随着时间的推移而产生。随着医学、患者人口统计和数据来源的变化,之前无偏的模型如果输入数据不再准确代表当前的群体,可能会变得有偏。持续监控有助于识别并修正任何新出现的偏差,确保模型保持公平和有效。
总结

图片由Planet Volumes提供,来自Unsplash
数据科学家必须采取措施,减少医疗 AI 模型中的偏见,以实现公平的患者结果,推动利益相关者的支持,并获得监管批准。数据科学家可以利用来自如Fairlearn(微软)或AI Fairness 360 Toolkit(IBM)等库中的新兴工具来衡量和改善 AI 模型的公平性。尽管这些工具和像统计平衡差异(Statistical Parity Difference)这样的量化指标非常有用,开发人员仍需记住,要采取一种全面的公平性方法。这需要与专家和受影响群体的利益相关者合作,以理解患者群体和 AI 应用的影响。如果数据科学家遵循这一实践,他们将迎来一个更加公正且优越的全民医疗新时代。
数据脏乱度评分
测量表格数据集质量的新方法
·发表于Towards Data Science ·阅读时间:11 分钟·2024 年 3 月 2 日
--
本文是关于涉及大型语言模型(LLM)数据清洗实践系列文章的第一篇,重点讨论如何量化数据集的清洁度或脏乱度

图片来自Fabizio Conti 在Unsplash
从为什么开始
本文介绍了一种用于评估数据集脏乱度的概念,这个话题由于缺乏与数据清洗相关的具体评分或损失函数而面临挑战。这里的主要目标是建立一个可以有效衡量数据集清洁度的指标,将这一概念转化为具体的优化问题。
数据清洗定义为一个两阶段过程:
-
首先,检测数据错误,如格式问题、重复记录和离群值;
-
其次,修复这些错误。
每个阶段的评估通常依赖于将脏乱的数据集与清洁(真实)版本进行比较,使用分类指标如召回率、精确度和 F1 得分进行错误检测(例如,参见大型基础模型能处理你的数据吗?,检测数据错误:我们在哪儿,需要做什么?)以及用于数据修复任务的准确率或重叠度度量(例如,参见自动数据修复:我们准备好部署了吗?或HoloClean:通过概率推理进行整体数据修复)。
然而,这些指标是任务特定的,并未提供一个统一的衡量标准来评估包含各种错误类型的数据集的整体清洁度。
本讨论聚焦于结构化且整洁的表格数据集(见《整洁数据 | 统计软件期刊》),将数据清洗与更广泛的数据质量问题区分开来,后者包括数据治理、数据溯源、目录管理、数据漂移等。
评分蓝图
以下所有假设都是数据脏污评分所依赖的基础。这些假设大多来自文章《如何量化数据质量?》。当然,所有这些假设都可以被辩论和批评,但明确陈述这些假设对促进讨论至关重要。
数据错误与违反约束有关,这些约束来源于对数据的期望。例如,如果期望 ID 列不应有缺失值,那么 ID 列中存在缺失值就构成了约束违反。
无期望则无忧。缺乏期望意味着不会对评分产生影响。换句话说,若没有预定义的期望,就无法识别数据问题,因此也无法违反不存在的约束。
数据问题应能定位到具体单元格。评分依赖于能够将错误精确定位到数据集中特定单元格的能力。
数据错误的置信度分数。并非所有数据错误都能以相同的确定性被识别。每个检测到的问题应标记一个置信度级别,表示该问题确实是一个错误的可能性有多大。这种方法承认一些错误可能存在解释空间。我们建议将置信度级别分为四个顺序类别:低、中、高和确定。这些类别分别对应 0.25、0.5、0.75 和 1 的概率值。
单元格对整体评分的均匀影响。数据集中的每个单元格对脏数据评分都有相同的潜在影响。解决与某个单元格相关的问题可能会解决其他单元格的问题,这表明在评分计算中单元格的权重是均匀分布的。
一个简化示例以便说明
在检查数据集时,凭一眼就能发现潜在的数据质量问题并不罕见。请考虑以下用于分析的简单数据集:
Student#,Last Name,First Name,Favorite Color,Age
1,Johnson,Mia,periwinkle,12
2,Lopez,Liam,blue,green,13
3,Lee,Isabella,,11
4,Fisher,Mason,gray,-1
5,Gupta,Olivia,9,102
6,,Robinson,,Sophia,,blue,,12
该示例来自书籍《有效数据科学的数据清洗》,它展示了一个代表六年级班级的数据集中存在的数据质量问题。该数据集包含每个学生的多个变量,组织方式是每个学生有 6 个学生和 5 个变量。
在检查时,某些条目可能因明显的不一致性或错误而引起关注:
-
学号为
Student#2(Liam Lopez)的学生条目似乎在Favorite Color列中有一个额外的值,看起来像是两个值('blue,green')合并在了一起。通常情况下,这一列应只包含一个值。鉴于不确定性,该问题被标记为high置信度级别,需进一步检查。 -
下一位学生,Isabella Lee,缺少
Favorite Color值。鉴于该列不应有任何缺失项,因此该问题已被以certain置信度识别为需要修正。 -
学号为 4 的学生 Mason Fisher 的记录列出了
-1的年龄,这是一个不可信的值。这可能代表一个表示缺失数据的哨兵值,因为使用这种占位符是常见做法。然而,年龄应该是正整数,因此需要审查这一条目。 -
学号为 5 的学生 Olivia Gupta 所在的行虽然没有结构性错误,但却呈现出一个不寻常的情况,因为有多个解释是合理的。
Favorite Color和First Name字段可能被交换,因为Olivia既可以是名字,也可以是颜色。此外,数字9可能表示颜色代码,但这一假设缺乏支持证据。而且,一名六年级学生的年龄为102是不太可能的,这表明可能存在拼写错误(例如将102写成了12)。 -
最后一行包含多余的逗号,表示可能存在数据摄取问题。然而,除了这个格式问题之外,条目本身似乎有效,因此在识别该错误的性质时,给出了
high的置信度级别。
根据我们的指导方针计算数据脏度分数,我们可以通过引入一个 Python 中的DataIssue类来采用系统化的方法,该类旨在封装数据问题的各个方面:
@dataclass
class DataIssue:
type_of_issue: str
expectation: str
constraint_violated: str
confidence_score: float
location: np.ndarray
为了定位特定错误,使用一个大小为(6, 5)的numpy数组,其中每个元素对应数据集中的一个单元格。该数组由 0 和 1 组成,1 表示数据集中相应单元格可能存在问题。
所有已识别的数据问题将在此之后实例化:
# Issue with Student# 2 - Extra value in 'Favorite Color'
issue_1 = DataIssue(
type_of_issue="Extra Value",
expectation="Single value in 'Favorite Color'",
constraint_violated="It looks like two values ('blue,green') have been merged",
confidence_score=0.75, # `high`
location=np.array([
[0, 0, 0, 0, 0],
[0, 0, 0, 1, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0]
],
)
)
# Issue with Student# 3 - Missing 'Favorite Color'
issue_2 = DataIssue(
type_of_issue="Missing Value",
expectation="No missing values in 'Favorite Color'",
constraint_violated="Non-null constraint",
confidence_score=1.0, # `certain`
location=np.array([
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 1, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0]
],
)
)
# Issue with Student# 4 - Implausible Age
issue_3 = DataIssue(
type_of_issue="Implausible Value",
expectation="Positive integer for 'Age'",
constraint_violated="Positive integer constraint",
confidence_score=1.0, # `certain`
location=np.array([
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 1],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0]
],
)
)
# Issues with Student# 5 - Multiple potential issues
issue_4 = DataIssue(
type_of_issue="Structural/Typographical Error",
expectation="Consistent and plausible data entries",
constraint_violated="The `Favorite Color` and `First Name` fields might be swapped, considering `Olivia` can be both a name and a colour",
confidence_score=0.25, # `low`
location=np.array([
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 1, 1, 0],
[0, 0, 0, 0, 0]
],
)
)
issue_5 = DataIssue(
type_of_issue="Typecasting error",
expectation="`Favorite Color` must only contain values from known color strings",
constraint_violated="`9` is not a valid colour name",
confidence_score=0.75, # `high`
location=np.array([
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 1, 0],
[0, 0, 0, 0, 0]
],
)
)
issue_6 = DataIssue(
type_of_issue="Anomaly",
expectation="Realistic age values for 6th-grade students",
constraint_violated="An age of `102` is highly improbable",
confidence_score=0.75, # `high`
location=np.array([
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 1],
[0, 0, 0, 0, 0]
],
)
)
# Issue with last row - Superfluous commas
issue_7 = DataIssue(
type_of_issue="Formatting Error",
expectation="Correct delimiter usage",
constraint_violated="Duplicate commas as separators",
confidence_score=1.0, # `certain`
location=np.array([
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[1, 1, 1, 1, 1]
],
)
)
将多个数据错误归类为特定的DataIssue实例可能带有一定的主观性,类似于软件开发中的错误报告过程。字段—type_of_issue、expectation和constraint_violated—有助于阐明错误的性质,便于在调查或审查时理解。
计算数据脏度分数时,关键元素是错误的位置和相关的置信度分数。在这个示例中,置信度分数是基于对错误存在性的感知确定的。
指向相同单元格的重复问题显著增加了问题存在的可能性。
现在我们已经拥有所需的所有信息,让我们看看如何计算这个小数据集的脏度分数。
计算数据脏度分数
数据脏污得分 代表了 数据集中包含错误的单元格的预期比例。*
该得分的理论和计算详见附录中的得分理论部分。
通过使用针对各个问题的置信得分作为每个单元格中错误独立概率的估计值,我们可以应用基本的概率原理来计算每个单元格出现问题的可能性,从而得出数据脏污得分。
以下是一个 Python 函数,用于根据已识别的数据问题列表计算该指标:
def compute_data_dirtiness_score(data_issues: List[DataIssue]) -> float:
"""
Computes the Data Dirtiness Score based on a list of data issues.
Each issue's impact on data quality is represented by a confidence score
and its location within the dataset.
The function aggregates these impacts to estimate the overall 'dirtiness'
of the dataset, with higher scores indicating lower quality.
Parameters:
data_issues: A list of DataIssue instances,
each detailing a specific data quality issue.
Returns:
The overall Data Dirtiness Score for the dataset, as a float.
"""
# Stack the probability arrays of a cell being error-free per issue
stacked_error_free_probs = np.stack(
[(1 - issue.confidence_score*issue.location) for issue in data_issues],
axis=-1,
)
# Calculate the combined matrix probabilities of an issue for each cell
probs_issue = 1 - np.prod(stacked_error_free_probs, axis=-1)
# Find the average probability across all cells to get the dirtiness score
data_dirtiness_score = np.mean(probs_issue)
return data_dirtiness_score
让我们计算之前展示的数据集的得分:
compute_data_dirtiness_score(data_issues)
数据脏污得分:31.87%
为了改善(降低)这一得分,一个自然的步骤是解决最简单的错误,例如纠正最后一行中作为分隔符的重复逗号。
这是数据集的新版本:
Student#,Last Name,First Name,Favorite Color,Age
1,Johnson,Mia,periwinkle,12
2,Lopez,Liam,blue,green,13
3,Lee,Isabella,,11
4,Fisher,Mason,gray,-1
5,Gupta,Olivia,9,102
6,Robinson,Sophia,blue,12
让我们再次重新计算得分,以查看改善效果。
compute_data_dirtiness_score(data_issues)
数据脏污得分:15.21%
在纠正后重新评估得分,显示出显著的改善,由于错误影响了一个相对较小的数据集中的整个行,得分减少了一半。
总结来说,这一度量为监控和改进数据集的清洁度提供了一种定量方法,通过迭代纠正已识别的数据错误。
后续步骤与挑战
为数据创建预期或约束条件可能具有挑战性并且成本较高,因为它需要人工标注和领域知识。一个解决方案是自动化生成约束条件和数据错误检测,之后由人工审查并调整这些自动化的约束条件,可以通过删除问题或修改置信得分来完成。为此,LLMs(大语言模型)是非常好的候选者(参见 Jellyfish: A Large Language Model for Data Preprocessing、Can language models automate data wrangling? 或 Large Language Models as Data Preprocessors)。
某些约束条件和违规情况的可能性并不总是非常明确,这就需要一个置信得分来考虑这种不确定性。即便是专家,对于特定的数据问题也可能并不总是达成一致,因此,当自动化技术被用于检测这些问题时,估算的可能性就显得特别有用。
那么对于缺失的期望或漏掉的数据错误怎么办?错误检测的有效性直接影响清洁度得分,可能导致得分过于乐观。然而,也有一个反论点需要考虑:那些更难检测到、因此更隐蔽的错误,可能对数据可用性或下游应用的影响并不那么重要。这表明,当这些错误被识别为问题时,应该赋予较低的置信度得分,反映其较低的重要性。尽管这种方法并非没有缺陷,但它有助于通过相应地加权这些被忽视的错误的重要性,从而限制它们对总体脏污得分的影响。
另一个需要考虑的方面是得分的动态特性。解决一个问题可能会影响其他问题,这引发了如何高效更新得分而不产生太多麻烦的问题。
还有一个问题是,在计算清洁度得分时,是否应将索引和列名作为数据集单元的一部分,因为它们的准确性也会影响数据清理过程(例如,参见 使用 ChatGPT 进行列类型标注)。
本系列的未来文章将探讨与此相关的各种主题,包括数据错误的分类法、利用 LLM 进行自动化问题检测,以及数据修正和修复的策略。敬请关注!
-> 第二篇文章链接:LLMs 驱动的数据质量错误检测。
参考文献
-
如何量化数据质量?
得分理论
让我们深入探讨如何计算数据集的数据脏污得分,表示为 𝒟。该数据集包含 I 行,代表个体,以及 J 列,代表不同的变量。
我们引入一个与 𝒟 维度相同的矩阵 X,具有 I 行和 J 列:

在这个矩阵中,每个元素 X_{ij} 遵循一个参数为 π_{ij} 的伯努利分布。如果数据集 𝒟 中单元格 (i, j) 没有数据问题,则 X_{ij} 的值为 0;如果有问题,则 X_{ij} 的值为 1,概率 𝔼[X_{ij}] = π_{ij} 表示问题发生的可能性。
接下来,我们定义一个随机变量 Y,表示数据集 𝒟 中存在问题的单元格的比例。Y 的公式如下:

数据脏污度分数是 Y 的期望值:

为了将此与我们之前的讨论联系起来,每个单元格数据错误的置信度分数与概率 π_{ij} 之间的关系由以下公式表示:

这意味着,单元格无错误的概率是通过该单元格潜在错误的置信度分数的补数相乘计算出来的。
如果所有的置信度分数都设置为 1,表示对错误的绝对确定性,则脏污度分数简化为数据集中有错误的单元格的比例。
计算数据集的脏污度分数或清洁度分数本质上给出了相同的见解,只是从不同的角度来看。数据清洁度分数的公式只是 1 减去 数据脏污度分数:

通过这种方式,完全没有错误的数据集将具有 100% 的清洁度分数和 0% 的脏污度分数。
变更
编辑-2024–03–21:将置信度值转换为序数类别:低、中、高 和 确定。这些分别表示概率 0.25、0.5、0.75 和 1。
数据破坏以提升实体嵌入
在神经网络训练过程中注入随机值可以帮助你从类别特征中获得更多信息。
·发布于 Towards Data Science ·阅读时长 11 分钟·2024 年 6 月 4 日
--

图片来源:dylan nolte 于Unsplash
今天,我将讨论一种随机正则化方法,旨在提高神经网络模型中实体嵌入的泛化能力。我使用数据生成器在训练过程中随机注入选定的输入值,帮助模型学习如何处理未见过的代码。
对于层次类别特征,性能提升尤其显著。 随机化有助于模型利用更高级别的组信息来弥补未见过的低级别代码。
添加噪声、移除信息或以其他方式破坏数据,通常用于提高模型的鲁棒性并减少过拟合[1,2]。在这里,它被用来帮助模型学习如何处理缺失的类别信息。我将检查一个公共测试数据集,比较未修改数据与以两种方式进行随机化的数据。
当未见过的代码起作用时,随机注入值有助于模型进行泛化。
使用数据生成器随机打乱值,这样每个小批量都看到不同的场景,效果优于静态数据修改。
一个警告是,当编码层次结构与实际情况无关时,可能会发生过拟合...
数据驱动的旅程优化:使用深度学习设计客户旅程
机器学习模型能否学会构建最优的客户旅程?
·发表于Towards Data Science ·阅读时间 8 分钟·2024 年 11 月 6 日
--

使用束搜索优化客户旅程
市场营销归因传统上是回顾性的:分析过去的客户旅程,了解哪些接触点对转化做出了贡献。但如果我们能利用这些历史数据来设计最优的未来旅程呢?在这篇文章中,我将展示如何将深度学习与优化技术相结合,设计高转化率的客户旅程,同时尊重现实世界中的约束条件。我们将通过使用 LSTM 预测高转化概率的旅程,然后使用束搜索来找到具有高转化机会的序列来实现这一目标。所有图片均由作者创作。
介绍
客户与企业的互动可以被称为客户旅程。在这一旅程中,他们通过所谓的接触点(例如社交媒体、谷歌广告等)与公司接触。在任何时刻,用户都可能发生转化(例如购买你的产品)。我们希望了解在这一旅程中的哪些接触点对转化做出了贡献,从而优化转化率。
传统归因的局限性
在深入了解我们的解决方案之前,了解传统归因模型为何存在不足是很重要的。
1. 位置无关的归因
传统的归因模型(首接触、末接触、线性等)通常会为每个渠道分配一个单一的重要性分数,无论该渠道在客户旅程中处于什么位置。这是根本性的缺陷,因为:
-
社交媒体广告在意识阶段可能非常有效,但在考虑阶段影响力较小
-
邮件的有效性通常取决于它是欢迎邮件、培养序列还是重新参与活动
-
重新定向广告作为第一次接触点意义不大,但在客户旅程后期可以非常有力
2. 上下文盲点
大多数归因模型(即使是数据驱动的)忽略了关键的上下文因素:
- 客户特征:年轻的科技精通型客户可能会对数字渠道与传统渠道有不同的响应
Customer 1 (Young, Urban): Social → Video → Purchase
Customer 2 (Older, Rural): Print → Email → Purchase
-
先前购买历史:现有客户通常需要与新客户不同的参与策略
-
一天中的时间/一周中的时间:渠道效果可能会根据时机有显著变化
-
设备/平台:相同的渠道在不同的平台上可能表现不同
-
地理/文化因素:在一个市场有效的方法可能在另一个市场失败
3. 静态渠道值
传统模型假设渠道效果可以用一个数字表示,其中所有其他影响效果的因素被边缘化。如上所述,渠道效果高度依赖于上下文,应该是该上下文的一个函数(例如位置、其他接触点等)。
深度学习进入舞台
客户旅程本质上是顺序的——接触点的顺序和时机非常重要。我们可以将归因建模框架设为二元时间序列分类任务,我们希望从接触点序列中预测客户是否转化。这使得它们成为使用 R递归神经网络(RNN)特别是长短期记忆(LSTM)网络的顺序建模的完美候选。这些模型能够捕捉顺序数据中的复杂模式,包括:
-
不同渠道组合的效果
-
接触点排序的重要性
-
时间敏感性
-
渠道交互效应
从历史数据中学习
第一步是训练一个 LSTM 模型,基于历史客户旅程数据。对于每个客户,我们需要:
-
他们遇到的接触点的顺序
-
客户是否最终转化
-
客户的特征
LSTM 学会根据任何一系列接触点预测转化概率。这为我们提供了一个强大的“模拟器”,可以评估任何提出的客户旅程的可能效果。
由于我没有找到合适的数据集(特别是包含客户特征作为上下文数据的),我决定生成自己的合成数据。数据生成的笔记本可以在这里找到。我们为每个客户生成一些特征和随机数量的客户旅程。这些旅程长度是随机的。在旅程的每一个点,客户都会与一个接触点互动,并且有转化的概率。这个概率由多个因素组成。
-
渠道的基础转化率
-
位置乘数。某些渠道在旅程的某些位置上更加或更少有效。
-
一个细分乘数。渠道的有效性取决于客户所在的细分群体。
-
我们还会有交互效应。例如,如果用户年轻,那么像社交和搜索这样的接触点会更有效。
-
此外,前一个接触点对当前接触点的有效性也有影响。


旅程数据(左)和用户数据(右)
接着,我们通过合并这两张表格、缩放数值特征以及对类别特征进行 OneHot 编码来预处理数据。然后,我们可以建立一个 LSTM 模型,处理在嵌入后按顺序排列的接触点数据。在最后的全连接层中,我们还加入了客户的上下文特征。数据预处理和训练的完整代码可以在这个notebook中找到。
然后,我们可以使用二元交叉熵损失函数训练神经网络。我已经绘制了在测试集上实现的召回率,见下图。在这种情况下,我们更关注召回率而非准确率,因为我们希望尽可能多地检测到会转化的客户。如果错误地预测某些客户会转化,而他们实际上不会,这比错过潜力较大的客户要轻微得多。

训练 JourneyLSTM
此外,我们会发现,大多数旅程并未导致转化。我们通常会看到 2%到 7%之间的转化率,这意味着我们有一个高度不平衡的数据集。出于同样的原因,准确率并不是特别有意义。总是预测多数类(在这种情况下是“无转化”)会得到一个很高的准确率,但我们无法找到任何转化的用户。
从预测到优化
一旦我们有了训练好的模型,就可以用它来设计最优的旅程。我们可以对一组客户强加一系列渠道(如下例,先是渠道 1 然后是渠道 2),并观察模型预测的转化概率。我们已经可以看到,这些概率根据客户的特征差异很大。因此,我们希望为每个客户单独优化旅程。

强加的旅程和预测的转化概率
此外,我们不能仅仅选择最高概率的序列。现实世界中的营销有很多约束:
-
渠道特定限制(例如,电子邮件频率上限)
-
在特定位置所需的接触点
-
预算约束
-
时间要求
因此,我们将此问题框定为一个受约束的组合优化问题:找到一个触点序列,在满足所有约束的同时,最大化模型预测的转化概率。在这种情况下,我们将仅在旅程中的特定位置限制触点的出现。也就是说,我们有一个从位置到触点的映射,指定某个触点必须出现在给定的位置。
还需要注意,我们的目标是优化预定义的旅程长度,而不是任意长度的旅程。根据模拟的性质,由于在每个触点上都有非零的转化概率,因此整体转化概率会严格单调递增。因此,更长的旅程(更多的非零条目)通常会优于较短的旅程,我们可能会构造出无限长的旅程。
使用束搜索进行优化
以下是使用递归实现束搜索的代码。在每一层,我们优化旅程中的某个位置。如果该位置处于约束条件中并且已经固定,我们将跳过它。如果我们达到了要优化的最大长度,我们将停止递归并返回结果。
在每一层,我们查看当前的解决方案并生成候选解。在任何时刻,我们保留由束宽定义的最佳 K 个候选解。这些最佳候选解将作为输入用于下一轮的束搜索,在其中我们优化序列中的下一个位置。
def beam_search_step(
model: JourneyLSTM,
X: torch.Tensor,
pos: int,
num_channels: int,
max_length: int,
constraints:dict[int, int],
beam_width: int = 3
):
if pos > max_length:
return X
if pos in constraints:
return beam_search_step(model, X, pos + 1, num_channels, max_length, constraints, beam_width)
candidates = [] # List to store (sequence, score) tuples
for sequence_idx in range(min(beam_width, len(X))):
X_current = X[sequence_idx:sequence_idx+1].clone()
# Try each possible channel
for channel in range(num_channels):
X_candidate = X_current.clone()
X_candidate[0, extra_dim + pos] = channel
# Get prediction score
pred = model(X_candidate)[0].item()
candidates.append((X_candidate, pred))
candidates.sort(key=lambda x: x[1], reverse=True)
best_candidates = candidates[:beam_width]
X_next = torch.cat([cand[0] for cand in best_candidates], dim=0)
# Recurse with best candidates
return beam_search_step(model, X_next, pos + 1, num_channels, max_length, constraints, beam_width)
这种优化方法是贪婪的,我们很可能会错过高概率的组合。然而,在许多场景下,特别是在有许多渠道的情况下,暴力求解最优解可能不可行,因为可能的旅程数量随着旅程长度的增加呈指数级增长。

使用束搜索优化客户旅程
在上图中,我们为单个客户优化了转化概率。在位置 0,我们已将“电子邮件”指定为固定触点。然后,我们探索与电子邮件的可能组合。由于我们设置的束宽为五,所有组合(例如,电子邮件 -> 搜索)都进入下一轮。在那一轮中,我们发现了高潜力的旅程,该旅程将用户展示两次电子邮件,最终进行再营销。
结论
从预测到优化的转变,在归因建模中意味着我们从预测性建模转向指导性建模,即模型告诉我们需要采取的行动。这有可能实现更高的转化率,特别是当我们面对具有许多渠道和上下文变量的复杂场景时。
同时,这种方法也有几个缺点。首先,如果我们没有一个足够准确地检测转化客户的模型,我们可能会降低转化率。其次,模型输出的概率必须经过良好的校准。否则,我们优化的转化概率可能没有实际意义。最后,当模型需要预测超出其数据分布的旅程时,我们将遇到问题。因此,使用强化学习(RL)方法也是一种值得考虑的选择,在这种方法中,模型可以主动生成新的训练数据。
数据赋能商业
发掘普遍数据供应的全部潜力
·发表于Towards Data Science ·8 分钟阅读·2024 年 9 月 22 日
--

关于数据价值是新黄金的讨论很多。因此,许多公司将大量资金投入到成为数据驱动型企业中。它被宣传为一种全新的商业模式,几乎就像商业之前从未被数据所驱动一样。
当然,许多公司在实现完全数据驱动方面面临困难,但我认为更大的问题是,这根本不够。
我们都希望从数据中提取出一些神奇的东西。这就是为什么我们在大数据湖、大数据仓库以及所有其他尚未获得吸引眼球的营销标签的大数据集合中收集数据。我相信我们都陷入了想要发掘数据宝藏的陷阱。我们似乎无法摆脱这样的想法:这些庞大的数据集合是某种分析超能力的基础,最终将推动我们的企业达到新的高度。
数据技术和数据工程团队通常被视为将数据转化为商业利益的决定性因素。尽管技术和工程无可否认地重要,但要真正将企业转型为数字化强国,必须采取更为全面的方法。
在这篇文章中,我将从商业角度探讨这个问题,并解释为什么…
数据工程:增量数据加载策略
制定策略和解决方案架构,以实现从各种数据源逐步加载数据。
·发布于Towards Data Science ·阅读时间:10 分钟·2024 年 3 月 17 日
--
大数据时代需要高效且具成本效益的数据处理策略。在处理各类关键数据源时,增量数据摄取成为了首选方案,尤其是在这些数据源以高速度和低延迟生成数据的情况下。

图片由Santshree Sinha提供,来源:Unsplash
多年来,我作为数据工程师和分析师,致力于将多个数据源集成到企业数据平台中。在尝试逐步摄取并加载数据到目标数据湖和数据库时,我遇到了一个又一个复杂性。当数据像散落在灰尘中、遗留系统角落里的碎片时,复杂性尤为显现。我需要深入这些系统,找到黄金接口、时间戳和标识符,期望能够实现无缝且增量的数据集成。
这是一个常见的场景,当新的数据源需要用于分析用例时,工程师和分析师常常会面临这种情况。顺利实施数据摄取是一项技艺,许多工程师和分析师都致力于完善这一技能。然而,这有时是遥不可及的,尤其是当考虑到源系统及其提供的数据时,事情往往会变得混乱且复杂……
数据工程,重新定义
今天数据工程的实践方式以及为什么我们应该重新定义它
·发布于 Towards Data Science ·阅读时间 7 分钟·2024 年 6 月 28 日
--
今天数据工程的问题是什么?
如果你搜索关于数据工程到底是什么的清晰定义,你会发现有太多不同的提议,结果让你更加困惑,而非得到答案。
但为了更好地解释需要重新定义的内容,我将使用一个更流行的定义,它清楚地代表了我们目前面临的现状和混乱:
数据工程是开发、实施和维护系统与流程的过程,这些系统与流程接收原始数据并生成支持下游应用场景(如分析和机器学习)所需的高质量、一致性信息。数据工程是安全、数据管理、DataOps、数据架构、编排和软件工程的交集。数据工程师管理数据工程生命周期,从获取源系统的数据开始,直到为应用场景(如分析或机器学习)提供数据为止。
— Joe Reis 和 Matt Housley 在《数据工程基础》中
这是一个很好的定义,那么问题是什么呢?
让我们看一下第一句话,我将重点突出我们应该深入探讨的部分:
…接收原始数据并生成支持下游应用场景的高质量、一致性的信息…
因此,数据工程将原始数据转化为(生成)支持应用场景的信息。虽然这里只给出了分析或机器学习两个示例,但我认为这包括所有其他潜在的应用场景。
数据转换是让我和我所有的同事数据工程师抓狂的原因。数据转换是将正确的逻辑应用于原始数据的巨大任务,以将其转化为能够支持各种智能用例的信息。
应用正确的逻辑实际上是应用程序的主要任务。应用程序是实现推动业务(用例)逻辑的系统——我继续称其为应用程序,并隐含地也指代那些足够小以适应微服务架构的服务。应用程序通常由应用开发人员(如果你愿意,可以称之为软件工程师)构建。但为了符合我们当前对数据工程的定义,数据工程师现在必须实现业务逻辑。整个混乱开始于这一错误的做法。
我写过一篇关于这个话题的文章,我在文中强调了“数据工程是软件工程…”。不幸的是,我们已经有了数百万个脆弱的数据管道,这些管道是由数据工程师实现的。这些管道有时——甚至可惜的是,很多时候——没有达到你对应用程序所期望的软件质量。但更大的问题是,这些管道往往包含不协调的、因此不正确的,有时甚至是隐藏的业务逻辑。
然而,解决方案并不是让所有数据工程师都转变为应用开发人员。数据工程师仍然需要是合格的软件工程师,但他们绝不应转变为应用开发人员。相反,我主张重新定义数据工程为“全关于数据的流动、操作和管理”。这个定义来自《What Is Data Engineering? by Lewis Gavin *(O’Reilly, 2019)》。然而,这是与当前实践的明显不同,我们应该将操作限制为纯技术性操作。
我们不应再允许在应用程序之外开发和使用业务逻辑。
很明确,数据工程不应该实现业务逻辑。现代应用开发的趋势实际上是将无状态的应用逻辑与状态管理分开。我们不会将应用逻辑放入数据库,也不会将持久化状态(或数据)放入应用程序。在函数式编程社区中,他们开玩笑说:“我们相信教会与国家的分离”。如果你现在想问:“笑话在哪里?”,那么这可能有帮助。但现在没有任何笑话:“我们应该相信业务逻辑与业务数据的分离”。因此,我认为我们应该明确将数据问题留给数据工程师,逻辑问题留给应用开发人员。
那么,仍然允许数据工程师进行的“技术操作”是什么呢?我将其定义为任何不改变或添加新的业务信息的数据操作。我们仍然可以进行分区、桶分、重新格式化、归一化、索引、技术聚合等操作,但一旦需要涉及实际的业务逻辑时,我们应当将其交给负责相应数据集的业务领域的应用程序开发人员。
为什么数据工程被偏离了

分离关注点的简单而明显的原则 —— 作者提供的图像
为什么我们偏离了这个简单而明显的原则?
我认为这种转变可以归因于数据库快速发展为多功能系统的趋势。最初,数据库只是作为企业数据的简单持久存储解决方案。它们提供了非常有用的抽象,能够将数据持久化的功能从应用程序的实际业务逻辑中卸载出来。然而,供应商迅速通过在其数据库产品中嵌入软件开发功能来吸引应用程序开发人员,从而增强了这些系统的功能。这一整合使得数据库从简单的数据存储库转变为全面的平台,结合了复杂的编程语言和工具,支持全面的软件开发。因此,数据库演变成了强大的转换引擎,使得数据专家能够在传统应用程序之外实现业务逻辑。随着大型数据仓库的出现,这一转变的需求得到了进一步加强,数据仓库旨在整合分散的数据存储——这一问题在微服务架构兴起后变得更加突出。这一技术进步使得在数据库内结合业务逻辑和业务数据变得既实际又高效。
最终,并非所有软件工程师都屈服于将应用程序逻辑捆绑在数据库中的诱惑,仍然保留了清晰分离的希望。随着数据量和复杂性的不断增长,大数据工具如 Hadoop 及其后继者应运而生,甚至在某些领域取代了传统数据库。这一转变为将业务逻辑从数据库中移出并返回给应用程序开发人员提供了机会。然而,数据工程不仅仅包括数据迁移和管理的观念已经深入人心。我们开发了许多工具来支持商业智能、先进分析和复杂的转换管道,从而能够实现复杂的业务逻辑。
这些工具已成为现代数据架构(MDS)的核心组成部分,使得数据工程成为一门独立的学科。MDS 包括一整套用于数据处理和转化的工具,但这些工具对于典型的应用开发人员或软件工程师来说仍然相对陌生。尽管有可能“将数据库翻转过来”并将业务逻辑重新迁移到应用层,但我们未能完全把握这一机会。实现业务逻辑的这一不幸做法至今依然由数据工程师承担。
数据工程如何被重新定义
让我们更精确地定义一下“关于数据的流动、操作和管理”到底包含了什么。
数据工程师可以并且应该提供最成熟的工具和平台,供应用开发人员使用,以便处理数据。这也是“自服务数据平台”在数据网格中的主要思想。然而,定义和维护业务逻辑的责任仍然在业务领域内。这些人对业务了解得更多,知道应如何将业务转型逻辑应用于数据。
好吧,那么像数据仓库系统这样的好主意以及更广泛的“数据工程生命周期”(由 Joe Reis 和 Matt Housley 定义)怎么办呢?

数据工程生命周期已完成 — 图片由作者根据 Joe Reis 和 Matt Housley 的观点制作
“数据管道”实际上只是应用程序之间的代理,但如果业务逻辑要在管道中实现,这些系统应被视为企业中的独立应用程序。这些应用程序应由业务领域的应用开发人员维护,而不是由数据工程师维护。
数据从源头(在他们的参考中称为“生成”)到提供给消费者的流动其实在理想化的情况下被简化了。从“反向 ETL”输出的数据再次作为下游应用程序的输入。而不仅仅是“反向 ETL”,还有“分析”和“机器学习”会创建输出,供下游的分析和操作性应用程序使用。数据在组织中的流动并不会在机器学习模型的训练或应用,或商业分析的创建时结束。这些应用的结果需要在公司内部进一步处理,因此必须集成到整体的业务流程中(通过蓝色框和箭头表示)。这种观点模糊了仍然在操作性和分析性层面之间实行的严格区分,我已经将其消除作为数据网格的核心目标。
那么,数据工程师的真正任务是什么?我们应该发展企业架构,使得应用开发人员能够将业务逻辑重新带入他们的应用程序,同时允许这些应用程序之间无缝交换和共享数据。数据工程师实际上构建了“数据基础设施”和所有通过数据网格将业务逻辑整合到互联的业务应用程序中的工具。这比仅提供不同类型的数据库、一种数据仓库或数据湖(无论是在本地还是在云中)要复杂得多。它包括工具和基础设施的实施,治理和数据共享原则的定义和应用(包括建模),最终实现企业中的普遍数据供应。然而,业务逻辑的实现应该绝对不在数据工程的范围之内。

数据基础设施作为数据工程师为所有应用程序开发人员提供的服务 — 作者图片
这种重新定义的数据工程实践与我在这三部分系列文章中提出的调整后的数据网格方法相一致:
为什么由 Zhamak Dehghani 定义的数据网格面临挑战,以及如何解决这些问题。
towardsdatascience.com [## 数据网格中的挑战与解决方案 — 第二部分
“数据即产品”是数据网格中的核心原则。为什么当前的定义需要调整,以便完全实现…
通过联合企业数据建模实现数据网格中的互操作性的实用方法
数据泄露在预处理中的解释:带代码示例的视觉指南
数据预处理
10 种隐蔽的预处理管道泄露方式
·发表于Towards Data Science ·阅读时间 14 分钟·2024 年 10 月 30 日
--
⛳️ 更多[数据预处理](https://medium.com/@samybaladram/list/data-preprocessing-17a2c49b44e4)解释:· 缺失值填补 · 类别编码 · 数据缩放 · 离散化 · 过采样与欠采样 ▶ 数据泄露在预处理中的应用
在我教授机器学习的经验中,学生们经常遇到这样的问题:“我的模型表现得很好——准确率超过 90%!但是当我提交给隐藏数据集进行测试时,结果不如预期。哪里出了问题?”这种情况几乎总是指向数据泄露。
数据泄露发生在测试数据的某些信息在数据准备步骤中悄悄地(或泄露)进入训练数据时。这种情况通常发生在常规的数据处理任务中,而你未曾察觉。当这种情况发生时,模型从不该看到的测试数据中学习,使得测试结果具有误导性。
让我们看看常见的数据预处理步骤,并准确了解数据泄漏时会发生什么——希望你可以在自己的项目中避免这些“管道问题”。

所有视觉图:作者使用 Canva Pro 创建。优化为移动端显示;在桌面端可能会显得过大。
定义
数据泄漏是机器学习中一个常见的问题,它发生在不应该被模型看到的数据(例如测试数据或未来数据)被意外用来训练模型时。这可能导致模型过拟合,在新的、未见过的数据上表现不佳。
现在,让我们集中讨论以下数据预处理步骤中的数据泄漏问题。进一步,我们还将看到这些步骤对应的 scikit-learn 预处理方法名称,并且在文章的最后会看到代码示例。

缺失值填充
在处理真实数据时,你经常会遇到缺失值。与其删除这些不完整的数据点,不如用合理的估计值来填充它们。这可以帮助我们保留更多的数据进行分析。
填充缺失值的简单方法包括:
-
使用
SimpleImputer(strategy='mean')或SimpleImputer(strategy='median')用该列的平均值或中值来填充 -
使用
KNNImputer()查看相似数据点并使用它们的值 -
使用
SimpleImputer(strategy='ffill')或SimpleImputer(strategy='bfill')用数据中前一个或后一个值来填充 -
使用
SimpleImputer(strategy='constant', fill_value=value)用相同的数字或文本填充所有缺失的位置
这个过程被称为填充,虽然它很有用,但我们需要小心如何计算这些替代值,以避免数据泄漏。
数据泄漏案例:简单填充(均值)
当你使用所有数据的均值来填充缺失值时,均值本身包含了来自训练集和测试集的信息。这个组合均值不同于仅使用训练数据计算的均值。由于这个不同的均值进入了训练数据,你的模型从它本不该看到的测试数据中学习。总结一下:
🚨 问题所在 使用完整数据集计算均值
❌ 我们做错了什么 使用训练集和测试集的统计数据来计算填充值
💥 后果 训练数据包含受测试数据影响的平均值

均值填充泄漏发生在使用从所有数据行计算得到的平均值(4)填充缺失值,而不是正确地只使用训练数据的平均值(3),导致错误的填充值。
数据泄漏案例:KNN 填充
当你使用 KNN 填充缺失值时,算法会从训练集和测试集找到相似的数据点。它创建的替代值是基于这些邻近点的,这意味着测试集的值直接影响训练数据中的内容。由于 KNN 会查看实际的邻近值,因此这种训练和测试信息的混合比简单的均值填充更为直接。总结来说:
🚨 问题 在完整数据集上寻找邻居
❌ 我们做错了什么 使用测试集样本作为潜在的邻居进行填充
💥 后果 使用直接的测试集信息填充缺失值

KNN 填充泄漏发生在使用训练集和测试集数据共同寻找最近邻(生成 3.5 和 4.5 的值),而不是正确地只使用训练数据的模式来填充缺失值(生成 6 和 6 的值)。
类别编码
有些数据以类别的形式出现,而不是数字——比如颜色、名称或类型。由于模型只能处理数字,我们需要将这些类别转换为数值。
转换类别的常见方法包括:
-
使用
OneHotEncoder()为每个类别创建单独的 1 和 0 的列(也叫虚拟变量) -
使用
OrdinalEncoder()或LabelEncoder()为每个类别分配一个数字(如 1,2,3) -
使用
OrdinalEncoder(categories=[ordered_list])与自定义类别顺序反映自然层级(如 small=1,medium=2,large=3) -
使用
TargetEncoder()根据类别与我们试图预测的目标变量的关系将类别转换为数字
我们转换这些类别的方式会影响模型学习的效果,因此在这个过程中需要小心不要使用测试数据的信息。
数据泄露案例:目标编码
当你使用目标编码在所有数据上转换类别值时,编码值是基于来自训练集和测试集的目标信息计算的。替换每个类别的数字是包含测试数据的目标值的平均值。这意味着你的训练数据会被赋予已经包含来自测试集的目标值信息,这些信息它本不应该知道。总结来说:
🚨 问题 计算类别时使用了完整的数据集
❌ 我们做错了什么 使用所有目标值计算类别替换
💥 后果 训练特征包含未来目标信息

目标编码泄漏发生在使用所有数据替换类别的平均目标值(A=3,B=4,C=2),而不是正确地只使用训练数据的平均值(A=2,B=5,C=1),导致类别值错误。
数据泄露案例:独热编码
当你使用所有数据将类别转换为二进制列,然后选择要保留的列时,选择是基于训练集和测试集中的模式。这意味着你决定保留或删除某些二进制列时,受到了测试数据中目标预测能力的影响,而不仅仅是训练数据。这意味着你选择的列集部分受到了本不应该使用的测试集关系的影响。总结:
🚨 问题所在 从完整数据集中确定类别
❌ 我们做错了什么 基于所有唯一值创建二进制列
💥 后果 特征选择受到测试集模式的影响

当使用所有唯一值(A、B、C、D)从完整数据集创建类别列,而不是仅正确使用训练数据中存在的类别(A、B、C)时,会发生独热编码泄漏,导致错误的编码模式。
数据缩放
数据中的不同特征往往具有非常不同的范围——有些可能在千位,而其他则可能是很小的小数。我们调整这些范围,使所有特征具有相似的尺度,从而帮助模型更好地工作。
常见的缩放调整方法包括:
-
使用
StandardScaler()使得值集中在 0 附近,大多数值在-1 和 1 之间(均值=0,方差=1) -
使用
MinMaxScaler()将所有值缩放到 0 和 1 之间,或者使用MinMaxScaler(feature_range=(min, max))来设置自定义范围 -
使用
FunctionTransformer(np.log1p)或PowerTransformer(method='box-cox')来处理非常大的数字,使数据分布更加接近正态分布 -
使用
RobustScaler()通过不受异常值影响的统计量调整缩放(使用四分位数而非均值/方差)
虽然缩放有助于模型公平地比较不同的特征,但我们需要仅使用训练数据来计算这些调整,以避免数据泄漏。
数据泄漏案例:标准化缩放
当你使用所有数据标准化特征时,用于计算的平均值和分布值来自训练集和测试集。这些值与仅使用训练数据时得到的不同。这意味着你训练数据中的每个标准化值都使用了关于测试集分布的信息来进行调整。总结:
🚨 问题所在 使用完整数据集计算统计量
❌ 我们做错了什么 使用所有值计算均值和标准差
💥 后果 使用测试集分布缩放训练特征

标准化泄漏发生在使用完整数据集的平均值(μ=0)和分布(σ=3)来规范化数据时,而不是仅正确使用训练数据的统计量(μ=2,σ=2),导致错误的标准化值。
数据泄漏案例:最小-最大缩放
当你使用所有数据的最小值和最大值进行特征缩放时,这些边界值可能来自测试集。你训练数据中的缩放值是根据这些边界计算的,这些边界可能与仅使用训练数据时得到的边界不同。这意味着训练数据中的每个缩放值都使用了来自测试集的完整值范围进行调整。总结如下:
🚨 问题所在 使用完整数据集查找边界
❌ 我们做错了什么 从所有数据点中确定最小/最大值
💥 后果 使用测试集范围对训练特征进行归一化

使用完整数据集的最小值(-5)和最大值(5)来进行缩放,而不是仅正确使用训练数据的范围(最小值=-1,最大值=5)进行缩放,从而导致值的缩放错误。
离散化
有时,将数字分组为类别比使用精确的值更好。这有助于机器学习模型更容易地处理和分析数据。
创建这些分组的常见方法包括:
-
使用
KBinsDiscretizer(strategy='uniform')使每个组覆盖相同大小的值范围 -
使用
KBinsDiscretizer(strategy='quantile')使每个组包含相同数量的数据点 -
使用
KBinsDiscretizer(strategy='kmeans')通过聚类找到数据中的自然分组 -
使用
QuantileTransformer(n_quantiles=n, output_distribution='uniform')基于数据中的分位数创建分组
虽然分组值可以帮助模型更好地发现模式,但我们决定分组边界的方式需要仅使用训练数据,以避免泄漏。
数据泄漏案例:等频分箱
当你使用所有数据创建具有相等数据点数量的箱时,箱之间的分割点是通过训练集和测试集共同确定的。这些分割值与仅使用训练数据时得到的值不同。这意味着,当你将数据点分配到训练数据的箱中时,你使用的是受测试集值影响的分割点。总结如下:
🚨 问题所在 使用完整数据集设置阈值
❌ 我们做错了什么 使用所有数据点确定箱的边界
💥 后果 使用测试集分布对训练数据进行分箱

等频分箱泄漏发生在使用所有数据设置箱的分割点(-0.5,2.5)时,而不是仅正确使用训练数据来设置边界(-0.5,2.0),从而导致值的分组错误。
数据泄漏案例:等宽分箱
当你使用所有数据创建相等大小的箱时,确定箱宽度所使用的范围来自训练集和测试集。这一总范围可能比仅使用训练数据得到的范围更宽或更窄。这意味着当你将数据点分配到训练数据中的箱时,使用的箱边界是基于整个测试集的范围计算得出的。总结如下:
🚨 问题 使用完整数据集计算范围
❌ 我们做错了什么 基于完整数据分布设置箱宽
💥 后果 使用测试集边界对训练数据进行分箱

等宽分箱泄漏发生在使用完整数据集范围(-3 到 6)将数据划分为相等大小的组时,而不是仅正确使用训练数据的范围(-3 到 3),从而导致错误的分组。
重采样
当你的数据中某些类别的示例比其他类别多得多时,我们可以使用imblearn中的重采样技术,通过创建新样本或删除现有样本来平衡它们。这有助于模型公平地学习所有类别。
添加样本的常见方法(过采样):
-
使用
RandomOverSampler()从较小类别中复制现有示例 -
使用
SMOTE()为较小类别创建新的合成示例,通过插值法生成 -
使用
ADASYN()在模型最难处理的区域创建更多示例,重点关注决策边界
删除样本的常见方法(欠采样):
-
使用
RandomUnderSampler()随机删除较大类别中的示例 -
使用
NearMiss(version=1)或NearMiss(version=2)根据与较小类别的距离从较大类别中删除示例 -
使用
TomekLinks()或EditedNearestNeighbours()根据与其他类别的相似度仔细选择要删除的示例
平衡数据有助于模型更好地学习,但创建或删除样本的过程应该仅使用来自训练数据的信息,以避免数据泄漏。
数据泄漏案例:过采样(SMOTE)
当你在所有数据上使用 SMOTE 创建合成数据点时,算法会从训练集和测试集中挑选附近的点来创建新样本。这些新点是通过将测试集样本的值与训练数据混合生成的。这意味着你的训练数据得到了直接使用测试集信息创建的新样本。总结如下:
🚨 问题 使用完整数据集生成样本
❌ 我们做错了什么 使用测试集邻居创建合成点
💥 后果 训练数据被包含测试集影响的样本增强

过采样泄漏发生在基于整个数据集的类别计数(A×4, B×3, C×2)重复数据点时,而不是仅根据训练数据(A×1, B×2, C×2)来决定每个类别重复的次数。
数据泄漏案例:欠采样(Tomek 链接)
当你使用 Tomek 链接在所有数据中删除数据点时,算法会找到来自训练集和测试集的最近邻成对数据点,它们的标签却不同。根据这些数据点与测试集数据点的距离来决定是否从训练集中移除这些点。这意味着你的最终训练数据是由它与测试集值之间的关系决定的。总结如下:
🚨 问题 使用完整数据集移除样本
❌ 我们做错了什么 使用测试集关系识别成对数据
💥 后果 基于测试集模式减少训练

欠采样泄漏发生在基于整个数据集的类别比例移除数据点时(A×4,B×3,C×2),而不是正确地仅使用训练数据(A×1,B×2,C×2)来决定从每个类别中保留多少样本。
最后的评论
在预处理数据时,你需要确保训练数据和测试数据完全分开。任何时候你使用所有数据的信息来转换值——无论是填补缺失值、将类别转换为数字、特征缩放、创建区间,还是平衡类别——都会有将测试数据的信息混入训练数据的风险。这会使得模型的测试结果不可靠,因为模型已经从它本不该看到的模式中学习了。
解决方案很简单:始终先转换你的训练数据,保存这些计算结果,然后将其应用到你的测试数据上。
🌟 数据预处理 + 分类(带有泄漏)代码总结
让我们看看在预测一个简单的高尔夫比赛数据集时,数据泄漏是如何发生的。这是一个不好的示例,不应当被遵循,仅用于演示和教育目的。
import pandas as pd
import numpy as np
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OrdinalEncoder, KBinsDiscretizer
from sklearn.impute import SimpleImputer
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import accuracy_score
from imblearn.pipeline import Pipeline
from imblearn.over_sampling import SMOTE
# Create dataset
dataset_dict = {
'Outlook': ['sunny', 'sunny', 'overcast', 'rain', 'rain', 'rain', 'overcast', 'sunny', 'sunny', 'rain', 'sunny', 'overcast', 'overcast', 'rain', 'sunny', 'overcast', 'rain', 'sunny', 'sunny', 'rain', 'overcast', 'rain', 'sunny', 'overcast', 'sunny', 'overcast', 'rain', 'overcast'],
'Temperature': [85.0, 80.0, 83.0, 70.0, 68.0, 65.0, 64.0, 72.0, 69.0, 75.0, 75.0, 72.0, 81.0, 71.0, 81.0, 74.0, 76.0, 78.0, 82.0, 67.0, 85.0, 73.0, 88.0, 77.0, 79.0, 80.0, 66.0, 84.0],
'Humidity': [85.0, 90.0, 78.0, 96.0, 80.0, 70.0, 65.0, 95.0, 70.0, 80.0, 70.0, 90.0, 75.0, 80.0, 88.0, 92.0, 85.0, 75.0, 92.0, 90.0, 85.0, 88.0, 65.0, 70.0, 60.0, 95.0, 70.0, 78.0],
'Wind': [False, True, False, False, False, True, True, False, False, False, True, True, False, True, True, False, False, True, False, True, True, False, True, False, False, True, False, False],
'Play': ['No', 'No', 'Yes', 'Yes', 'Yes', 'No', 'Yes', 'No', 'Yes', 'Yes', 'Yes', 'Yes', 'Yes', 'No', 'No', 'Yes', 'Yes', 'No', 'No', 'No', 'Yes', 'Yes', 'Yes', 'Yes', 'Yes', 'Yes', 'No', 'Yes']
}
df = pd.DataFrame(dataset_dict)
X, y = df.drop('Play', axis=1), df['Play']
# Preprocess AND apply SMOTE to ALL data first (causing leakage)
preprocessor = ColumnTransformer(transformers=[
('temp_transform', Pipeline([
('imputer', SimpleImputer(strategy='mean')),
('scaler', StandardScaler()),
('discretizer', KBinsDiscretizer(n_bins=4, encode='ordinal'))
]), ['Temperature']),
('humid_transform', Pipeline([
('imputer', SimpleImputer(strategy='mean')),
('scaler', StandardScaler()),
('discretizer', KBinsDiscretizer(n_bins=4, encode='ordinal'))
]), ['Humidity']),
('outlook_transform', OrdinalEncoder(handle_unknown='use_encoded_value', unknown_value=-1),
['Outlook']),
('wind_transform', Pipeline([
('imputer', SimpleImputer(strategy='constant', fill_value=False)),
('scaler', StandardScaler())
]), ['Wind'])
])
# Transform all data and apply SMOTE before splitting (leakage!)
X_transformed = preprocessor.fit_transform(X)
smote = SMOTE(random_state=42)
X_resampled, y_resampled = smote.fit_resample(X_transformed, y)
# Split the already transformed and resampled data
X_train, X_test, y_train, y_test = train_test_split(X_resampled, y_resampled, test_size=0.5, shuffle=False)
# Train a classifier
clf = DecisionTreeClassifier(random_state=42)
clf.fit(X_train, y_train)
print(f"Testing Accuracy (with leakage): {accuracy_score(y_test, clf.predict(X_test)):.2%}")
上面的代码使用了ColumnTransformer,这是 scikit-learn 中的一个工具,允许我们对数据集中的不同列应用不同的预处理步骤。
这是数据集中每一列预处理策略的详细说明:
**温度****:
-** 均值填充处理任何缺失值
-
标准化缩放以规范化值(均值=0,标准差=1)
-
等宽离散化为 4 个区间,意味着将连续值分类为 4 个等宽区间
**湿度****:
-** 与温度相同的策略:均值填充 → 标准化缩放 → 等宽离散化(4 个区间)
**展望**(类别):
-
序数编码:将类别值转换为数值
-
通过将未知值设置为-1 来处理
**风速** (二元):
-
对缺失值进行常数填充(填充为 False)
-
标准化缩放以规范化 0/1 值
**比赛** (目标):
-
标签编码将“是/否”转换为 1/0
-
在预处理后应用 SMOTE 通过创建少数类别的合成示例来平衡类别
-
使用简单的决策树来预测目标
整个流水线演示了数据泄漏,因为所有的转换在拟合过程中都看到整个数据集,在实际的机器学习场景中这将是不合适的,我们需要确保测试数据与训练过程完全分开。
这种方法也可能会显示出人为提高的测试准确度,因为在预处理步骤中使用了测试数据的特征!
🌟 数据预处理 + 分类(无泄漏)代码总结
这是没有数据泄漏的版本:
import pandas as pd
import numpy as np
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OrdinalEncoder, KBinsDiscretizer
from sklearn.impute import SimpleImputer
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import accuracy_score
from imblearn.pipeline import Pipeline
from imblearn.over_sampling import SMOTE
# Create dataset
dataset_dict = {
'Outlook': ['sunny', 'sunny', 'overcast', 'rain', 'rain', 'rain', 'overcast', 'sunny', 'sunny', 'rain', 'sunny', 'overcast', 'overcast', 'rain', 'sunny', 'overcast', 'rain', 'sunny', 'sunny', 'rain', 'overcast', 'rain', 'sunny', 'overcast', 'sunny', 'overcast', 'rain', 'overcast'],
'Temperature': [85.0, 80.0, 83.0, 70.0, 68.0, 65.0, 64.0, 72.0, 69.0, 75.0, 75.0, 72.0, 81.0, 71.0, 81.0, 74.0, 76.0, 78.0, 82.0, 67.0, 85.0, 73.0, 88.0, 77.0, 79.0, 80.0, 66.0, 84.0],
'Humidity': [85.0, 90.0, 78.0, 96.0, 80.0, 70.0, 65.0, 95.0, 70.0, 80.0, 70.0, 90.0, 75.0, 80.0, 88.0, 92.0, 85.0, 75.0, 92.0, 90.0, 85.0, 88.0, 65.0, 70.0, 60.0, 95.0, 70.0, 78.0],
'Wind': [False, True, False, False, False, True, True, False, False, False, True, True, False, True, True, False, False, True, False, True, True, False, True, False, False, True, False, False],
'Play': ['No', 'No', 'Yes', 'Yes', 'Yes', 'No', 'Yes', 'No', 'Yes', 'Yes', 'Yes', 'Yes', 'Yes', 'No', 'No', 'Yes', 'Yes', 'No', 'No', 'No', 'Yes', 'Yes', 'Yes', 'Yes', 'Yes', 'Yes', 'No', 'Yes']
}
df = pd.DataFrame(dataset_dict)
X, y = df.drop('Play', axis=1), df['Play']
# Split first (before any processing)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.5, shuffle=False)
# Create pipeline with preprocessing, SMOTE, and classifier
pipeline = Pipeline([
('preprocessor', ColumnTransformer(transformers=[
('temp_transform', Pipeline([
('imputer', SimpleImputer(strategy='mean')),
('scaler', StandardScaler()),
('discretizer', KBinsDiscretizer(n_bins=4, encode='ordinal'))
]), ['Temperature']),
('humid_transform', Pipeline([
('imputer', SimpleImputer(strategy='mean')),
('scaler', StandardScaler()),
('discretizer', KBinsDiscretizer(n_bins=4, encode='ordinal'))
]), ['Humidity']),
('outlook_transform', OrdinalEncoder(handle_unknown='use_encoded_value', unknown_value=-1),
['Outlook']),
('wind_transform', Pipeline([
('imputer', SimpleImputer(strategy='constant', fill_value=False)),
('scaler', StandardScaler())
]), ['Wind'])
])),
('smote', SMOTE(random_state=42)),
('classifier', DecisionTreeClassifier(random_state=42))
])
# Fit pipeline on training data only
pipeline.fit(X_train, y_train)
print(f"Training Accuracy: {accuracy_score(y_train, pipeline.predict(X_train)):.2%}")
print(f"Testing Accuracy: {accuracy_score(y_test, pipeline.predict(X_test)):.2%}")
与泄漏版本的主要区别
-
首先拆分数据,然后再进行任何处理
-
所有的转换(预处理,SMOTE)都在流水线内
-
流水线确保:
-
预处理参数仅从训练数据中学习
-
SMOTE 仅适用于训练数据
-
测试数据在预测之前完全不可见
-
这种方法提供了更现实的性能估计,因为它保持了训练数据和测试数据之间的正确分离。
技术环境
本文使用 Python 3.7,scikit-learn 1.5 和 imblearn 0.12。虽然讨论的概念通常适用,但具体的代码实现可能会因版本不同略有差异。
关于插图
除非另有说明,所有插图均由作者创建,并结合了 Canva Pro 授权的设计元素。
𝙎𝙚𝙚 𝙢𝙤𝙧𝙚 𝘿𝙖𝙩𝙖 𝙋𝙧𝙚𝙥𝙧𝙤𝙘𝙚𝙨𝙨𝙞𝙣𝙜 𝙢𝙚𝙩𝙝𝙤𝙙𝙨 𝙝𝙚𝙧𝙚:

数据预处理
查看列表6 个故事!

𝙔𝙤𝙪 𝙢𝙞𝙜𝙝𝙩 𝙖𝙡𝙨𝙤 𝙡𝙞𝙠𝙚:

分类算法
查看列表8 篇文章



回归算法
查看列表5 篇文章


数据最小化并不保证隐私
仅仅因为数据被最小化了,并不意味着它是安全的!
·发表于Towards Data Science ·阅读时长 4 分钟·2024 年 10 月 28 日
--
基于我们的论文 机器学习中的数据最小化原则 由 Prakhar Ganesh、Cuong Tran、Reza Shokri 和 Ferdinando Fioretto 撰写
数据最小化原则
数据驱动系统和机器学习应用的激增加剧了许多隐私风险,包括未授权访问敏感信息的风险。为应对这些风险,国际数据保护框架,如欧洲通用数据保护条例(GDPR)、加利福尼亚隐私权法案(CPRA)、巴西通用数据保护法(LGPD)等,已将数据最小化作为缓解这些风险的关键原则。

来自全球六个不同数据保护法规的数据最小化原则摘录。图片来源:作者。
数据最小化原则的核心要求组织仅收集、处理和保留足够、相关且仅限于为特定目标所必需的个人数据。其基础是认为并非所有数据都是必要的,数据的过度收集反而会增加信息泄露的风险。数据最小化原则建立在两个核心支柱上,目的限制和数据相关性。
目的限制
数据保护法规要求收集数据的目的是为了合法、具体且明确的目的(LGPD,巴西),并且禁止将收集的数据用于与公开声明目的不兼容的任何其他目的(CPRA,美国)。因此,数据收集者必须在数据收集前定义明确的法律目标,并且仅将数据用于该目标。在机器学习(ML)环境中,这个目的可以被看作是为了训练模型以在特定任务上实现最佳表现而收集数据。
数据相关性
像 GDPR 这样的法规要求所有收集的数据必须是充分的、相关的,并且仅限于为其收集目的所必需的数据。换句话说,数据最小化旨在删除不符合上述目的的数据。在机器学习的环境中,这意味着仅保留对模型性能有贡献的数据。

数据最小化。 作者提供的图像
通过最小化的隐私期望
正如你可能已经注意到的,数据保护法规通过最小化隐含地期望隐私保护。数据最小化原则甚至在公共讨论中被许多人推崇(EDPS,Kiteworks,The Record,Skadden,k2view)作为保护隐私的原则。
欧盟《人工智能法案》在第 69 条说明中指出,“隐私权和个人数据保护权必须在整个人工智能系统生命周期中得到保障。在这方面,数据最小化原则以及通过设计和默认方式进行的数据保护原则,按照欧盟数据保护法,适用于个人数据的处理”。
然而,这种通过最小化隐私期望忽视了现实数据中的一个关键方面——各种特征之间的内在关联性! 个人信息很少是孤立的,因此,仅仅进行数据最小化,仍然可能允许进行有信心的重构。这就造成了一个空白,在这个空白中,使用数据最小化操作化尝试的个人或组织,可能期望隐私得到改善,尽管使用的框架仅限于最小化。
正确的隐私讨论方式
隐私审计通常包括进行攻击来评估实际的信息泄露。这些攻击是揭示潜在漏洞的有力工具,通过模拟现实场景,审计员可以评估隐私保护机制的有效性,并识别可能泄露敏感信息的领域。
在这种情况下,可能相关的一些对抗性攻击包括重建攻击和重新识别攻击。重建攻击旨在从目标数据集中恢复缺失的信息。 重新识别攻击旨在使用部分或匿名化数据重新识别个体。

总体框架。 图片由作者提供
数据最小化与隐私之间的差距
考虑一个示例:最小化图像中的数据,并去除那些对模型性能没有贡献的像素。解决这个优化问题将给你一个最小化后的数据,看起来像这样。

图片由作者提供
这个例子中的趋势很有意思。如你所见,数字“1”的图像保留了中央的垂直线,而数字“0”则保留了外部曲线。换句话说,虽然去除了 50%的像素,但似乎没有丢失任何信息。通过应用一种非常简单的数据填充重建攻击,甚至可以证明这一点。

图片由作者提供
尽管数据集减少了 50%,但仍然可以使用整体统计信息重建图像。这强烈表明存在隐私风险,并表明最小化数据集并不等同于提高隐私保护!
那么我们能做什么呢?
虽然数据保护法规旨在限制数据收集,并期望提供隐私保护,但当前的数据最小化实现不足以提供强有力的隐私保障。然而,需要注意的是,这并不是说最小化与隐私不兼容;而是强调需要将隐私融入目标的方式,而不是将其视为事后的考虑。
我们在我们的论文中提供了更深入的实证研究,探讨了数据最小化与隐私保护之间的错位,并提出了潜在的解决方案。我们试图回答一个关键问题:“当前各种法规中的数据最小化要求是否真正符合法律框架中的隐私预期?” 我们的评估结果表明,不幸的是,答案是否。
数据模型设计 101:复合键与代理键
何时知道在数据模型中使用哪种类型的键
·发布于Towards Data Science ·阅读时长 4 分钟·2024 年 2 月 9 日
--

最近,我一直在编写一个数据模型,用来表示我们业务的一个新部分。这些数据需要提出很多问题,因为它非常难以直观理解。
这个数据模型要求我将来自两个不同来源的相似但不同的数据集合并成一个数据集。每当你合并两个数据集时,考虑那个将作为新数据集主键的唯一键是至关重要的。
不幸的是,你不能假设每个数据集中的主键会延续到结果集中。这是因为这些键,如果是递增的整数,往往会在不同数据集之间重复。
然而,你可以创建一个新的键。
在本文中,我们将讨论在数据模型中创建唯一键的两种选项——代理键和复合键。这两者之间有什么区别?你应该在什么情况下使用一个而不是另一个?
代理键与复合键
复合键由多个标识字段组成,这些字段共同构成唯一性。 它们是由真实世界的值创建的,其含义是可以理解的……
后现代数据架构的数据建模技术
一套通用的技术和原则,用于为您的后现代数据架构设计一个强大、成本高效、可扩展的数据模型。
·发表于数据科学探索 ·8 分钟阅读·2024 年 7 月 20 日
--

图片由Michael Dziedzic提供,来自Unsplash
在过去的几年里,随着现代数据架构(MDS)为数据的移动、转换和交互引入了新的模式和标准,维度数据建模逐渐成为过去的遗物。取而代之的是,数据团队依赖于一张大表(One-Big-Tables,OBT)并层层叠加 dbt 模型来应对新的使用场景。然而,这些方法导致了数据团队成为成本中心,并且过程不可扩展。因此,随着我们进入一个“后现代”数据架构时代,这一时代的特点是追求降低成本、整理数据平台和限制模型泛滥,数据建模正在经历一次复兴。
这一转变使数据团队面临一个两难选择:我们是应该回归到几十年前为完全不同的数据生态系统定义的严格数据建模方法,还是可以根据今天的技术和业务问题引入基于新的原则的建模方法?
我认为,对于大多数公司而言,正确的答案介于两者之间。本文将讨论一套数据建模标准,旨在避免模型泛滥,同时不限制新数据产品的交付速度。这些标准足够通用,适用于大多数“后现代”数据平台,但总会有一些边缘情况或行业需要不同的方法。
三层(Medallion)架构仍然是一个安全的选择,但有一些需要注意的事项。
从历史来看,当谈到数据建模时,数据团队通常指的是我们为仓库的“消费”层定义的模型,这一层是我们展示给消费者的,也是外部工具与之交互的层。然而,数据的旅程开始得远远早于它到达这一层,许多数据建模的决策是在这个过程中做出的。
就层次化方法而言,随着数据湖(以及其后的数据湖仓)兴起,三层/Medallion 架构依然是大多数使用案例中最合适的方法。你从第一层开始,该层包含原始数据;然后是执行技术清洗但没有业务逻辑转换的层;最后是存储已准备好使用的转换后的消费表的层。

三层/Medallion 架构的鸟瞰图(图片来源:作者)
然而,在定义架构各个层次的细节时,有一些需要注意的事项:
-
从右侧开始,定义左侧需要发生的事情:在任何技术公司中,都有许多内部系统生成潜在有用的数据。无论是 CRM、RevOps 还是营销软件,采取“带来一切”的方法,往往会让你背负管理成千上万可能永远不会使用的表格的负担。避免这种不必要复杂性的关键是首先关注你的数据使用(位于数据平台右侧的消费用例),然后定义来自公司各个系统的所需数据输入。一旦你有了所有实际有用的上游表的列表,就只将这些表带入原始层,并随着新业务需求的出现逐步引入新的表格。
-
最小化中间节点:虽然 Medallion 架构在(非常)高层次上由三个(理论上)层次构成,但你可能最终会在 Curated 层和 Consumption 层之间增加一些中间步骤。这些中间步骤可能是执行某些任务(如跟踪历史或处理敏感数据)所必需的,但如果你为每一个需求都开辟了中间步骤的空间,它们也可能变得冗余。理想情况下,你希望精简你的层次,并将不同的操作归类到同一个逻辑步骤中。
-
定义清晰且可执行的层之间的边界:尽管这看起来很明显,但在很多情况下,你可能会忍不住说,“让我直接在我的消费模型中引用这个原始表格。”这就像午夜后想吃不健康的零食一样,你最终会后悔屈服于这种诱惑。如果你决定仅在清洗层中执行快照和格式更新,那么就不要开始添加过滤器以更快地交付数据。这会为不同团队在使用共享清洗后的模型时带来不一致性和混淆。相反,如果你注意到某个边界过于严格或不必要,完全可以将其移动,并开始在不同的层次上执行某些操作,但这需要沟通并标准化。这同样适用于数据消费:例如,如果你希望在清洗后的数据上直接运行实验,你应该确保必要的防护措施到位。
更宽广、度量增强的维度表是前进的道路
在“传统”的维度数据建模中,维度表并不是度量的存储地。相反,度量应该通过事实表中存储的度量来计算。当时,这种做法是有道理的:它简化了维度表的管理(特别是在跟踪历史数据时),将定量数据和描述性数据分开,并允许依赖于中心事实表并由不同维度丰富的分析。
然而,在今天的世界中,这种分离似乎显得有些勉强。在大多数情况下,维度表的历史通过每日或每小时的快照来跟踪(便宜的存储,值得一提),而我们今天构建的分析通常是围绕维度表本身展开的。考虑到这一点,给定度量的计算可以嵌入到这些表中,而不会产生重大负面后果。这种方法的转变显著减少了为了回答基本问题而需要进行的维度表和事实表之间的连接。此外,它还与今天需要多个来自不同事实表的度量来计算派生度量的分析方法保持一致,所有这些都是分析给定维度的一部分。

带有中心度量增强的维度表的示例数据模型(图片由作者提供)
Maxime Beauchemin,他对数据工程领域做出了巨大贡献,基于这一原则开发了全面的数据建模方法:面向实体的数据建模。他关于这种方法的文章是非常重要的阅读资料,我相信,和他的功能性数据工程范式类似,面向实体的数据建模将引领许多数据团队(以及整个领域)朝着正确的方向前进。
我也相信,数据团队通过采用“分层”指标的方法将受益匪浅。与其把所有指标视为同等重要,或每次都需要决定是否将某个指标添加到维度表中,不如将指标分为两类:
-
通用指标:这些是需要在公司内部高度管理和标准化的关键指标(比如年复合收入(ARR)或日活跃用户(DAU))。作为一个通用定义,你可以认为任何在至少两个独立用例中相关或使用的指标都是通用指标。这些指标应该是你维度表的一部分,并具有清晰的多方所有权(例如,一个团队可以拥有该指标的定义,而另一个团队则拥有它所依赖的数据的一部分)。确定某个指标是否应该是某个维度表的一部分的另一种方式是,查看它是否是实体/维度本身的特征,而不仅仅是在特定场景下才相关的计算。
-
实验性指标:与通用指标不同,这些指标用于特定的用例,或仍在不断完善中。可以把它想象成一个仅与构建该功能的团队相关的特定指标。在这种情况下,若将该指标作为数据模型的一部分,可能会导致不必要的摩擦并增加复杂性。此时更合适的方法是将该指标定义为语义层的一部分(如果有的话),或在数据仓库消费层的下游计算该指标。然而,值得注意的是,如果实验性指标开始在多个用例中使用,或进入生产/关键数据产品,它可以晋升为通用指标。
保持灵活性并限制继承逻辑
随着新用例的出现,并且你不断增强和更新消费层以支持它们,重要的是要牢记以下关键原则。
保持数据的最精细粒度
在添加新的数据流时,如果最细粒度的数据并不立即需要,你可以决定在数据到达消费层之前对其进行预聚合,以降低成本并避免可能的昂贵查询(毕竟已经不是 2021 年了)。虽然这一决定听起来合情合理,但通常会导致不必要的摩擦和挫败感,因为数据消费者在需要时无法深入分析。
当你决定将某个实体或事件流引入数据平台时(因为某个用例需要它),通常更安全的做法是将最细粒度的数据作为消费层的一部分呈现出来,然后在其上构建聚合。更一般来说,在设计数据模型时,尽量始终优先考虑灵活性,并最小化你所做的单向决策的数量。
避免继承的逻辑
即使你在理论上是公司唯一的“官方”数据团队,也不要感到惊讶,发现公司内的其他部门有他们的“幽灵”数据团队。随着每个人都试图利用数据来获得优势并做出数据驱动的决策(这很不错!),会出现一些情况,其他团队(比如从事 SaaS 应用程序的团队或你的功能团队)会在他们使用的工具中定义自己的业务逻辑和指标(因为大多数 B2B SaaS 工具提供内置的分析功能)。
这种情况让你处于一个微妙的境地,因为你要么可以导入你不拥有(且无法管理)的“继承”业务逻辑,要么可以通过一个漫长的过程重新定义数据仓库中的逻辑。我的建议是始终选择第二种方案,因为这最终将帮助你最小化潜在问题的范围,避免隐性逻辑的变化。然而,这种努力在某些情况下可能代价过高——在这种情况下,你可以选择在数据到达数据平台后定义详细的检查,以确保你的假设仍然有效,且上游工具中定义的逻辑没有发生变化(这也有助于确保你的数据仓库逻辑始终与上游逻辑保持同步)。
随着数据团队在预算有限、质量重于数量的环境中不断过渡,做出适合自己情况的设计决策并能带来长期最大回报变得尤为重要。按照某种数据建模方法照搬使用,或完全忽视数据建模,都是导致错误方向的做法。相反,重要的是为自己武装上匹配需求的技术和原则,帮助你做出明智的设计决策,并能够构建适合团队发展的数据模型。
本文讨论的技术通常是一个可靠的选择,有助于为你打下正确的基础,但最终最重要的是理解自己(当前和未来)使用案例的需求,与数据消费者沟通,了解他们当前的困难,并监控数据平台的使用情况和成本。
如果你想获取更多数据工程的内容,可以订阅我的新闻通讯《Data Espresso》,在其中我讨论与数据工程和技术相关的各种话题:
[## Data Espresso | Mahdi Karabiben | Substack
数据工程的更新与评论,伴随你下午的浓缩咖啡。点击阅读由 Mahdi 编写的《Data Espresso》…
dataespresso.substack.com](https://dataespresso.substack.com/?source=post_page-----03fc2e4a210c--------------------------------)
AI 开发中的数据隐私:数据本地化
为什么你应该关心你的数据存储在哪里?
·发表于 Towards Data Science ·阅读时长:11 分钟·2024 年 6 月 18 日
--

图片来自 Luke Stackpoole 在 Unsplash
在为即将在 6 月 25 日于旧金山举行的 AI 质量大会准备讲座时(门票仍然有售!),我遇到了许多值得深入讨论的话题,但由于时间有限,我只能简要提及。在我的讲座中,无法详细展开这些内容。因此,为了给大家提供更多信息并更好地解释这些话题,我决定开始一系列小专栏,讲解与机器学习和 AI 开发相关的内容,同时注意数据隐私和安全。今天,我将从数据本地化开始。
在开始之前,我们应该澄清数据隐私和安全法规所涵盖的内容。简而言之,它适用于“个人数据”。但是,什么算是个人数据呢?这取决于司法管辖区,但通常包括 PII(个人身份信息,如姓名、电话号码等),以及可以合并在一起使某人可识别的数据(如邮政编码、生日、性别、种族、政治倾向、宗教等)。这还包括某人的照片、视频或音频记录、计算机或浏览器的详细信息、搜索历史、生物识别信息等。GDPR 关于此的规定可以在这里查看。
既然这个问题已经解决,让我们深入探讨数据本地化以及它与我们作为机器学习开发者的关系。
什么是数据本地化?
很高兴你提到这个问题!数据本地化本质上是关于你的数据存储在哪个地理位置——如果你进行数据本地化,那么你就是将数据存储在数据创建的地方。(这有时也被称为“数据驻留”,与之相对的是“数据可移植性”。)如果你的数据集在 AWS S3 的 us-east-1 区域,你的数据实际上是存储在美国某个地方,具体是在弗吉尼亚州北部。为了更精确,AWS 在弗吉尼亚州北部有几个具体的数据中心,你可以在线获取它们的准确地址。但对我们大多数人来说,知道这个地区的大致位置就足够了。
我为什么要关心数据中心的位置?难道云端不就是“无处不在”吗?
知道你的数据存储在哪里是有充分理由的。首先,数据加载/写入云端的速度可能会受到物理距离的影响,这取决于你和计算机与数据中心所在区域的远近。但除非你在进行极高速计算,否则这通常不会构成大问题。
关心数据存储地点的一个更重要的原因(也是数据隐私的一部分),是全球范围内的隐私法规(以及你与客户的合同和顾客填写的同意书)对于数据本地化有规定。关于数据本地化的监管要求个人数据必须存储在该地区的服务器上,尤其是关于该地区公民或居民的个人数据。
一般注意事项:
-
这并不总是适用于所有类型的数据(财务数据通常会涉及到)
-
并非所有类型的企业都适用数据本地化要求(科技公司更常涉及此类规定)
-
这可能是由政府要求触发的,也可能是自动的(例如越南)
-
有时你可以获得同意来移动数据,有时则不能。
-
有时你只需将数据最初存储在本国,然后可以稍后再移动(参见俄罗斯)
-
有时你可以将数据存储在原始国家以外的地方,但对于数据可以去往的其他地方有一定限制(参见欧盟)
此外,私人公司有时会在合同中施加数据本地化要求,可能是为了遵守这些法律,或者是为了减少数据泄露或其他政府对数据的监控风险。
这意味着,字面上讲,你可能会受到法律的限制,无法在某些地点存储特定数据,主要是基于数据的主体是谁,或原始数据所有者是谁。
示例
使用一个具体的(简化的)例子可能会更容易理解。
-
你运营一个人们可以进行购买的网站。你在这些购买过程中收集数据,比如信用卡详情、地址、姓名、IP 地址和其他一些信息。你的同意条款/细则没有提到任何关于数据本地化的内容。
-
你从俄罗斯、印度和阿联酋获得了客户。
-
除非你获得了明确同意,否则来自这些访客的所有个人数据都要遵守不同的数据本地化规则。
这对你意味着什么?所有这些数据需要以不同的方式进行处理。
-
俄罗斯客户的数据需要最初存储在俄罗斯的服务器上,然后可能根据适用的规则进行转移。
-
来自欧盟客户的数据可以存储在具有充分数据安全法律的国家(尤其是俄罗斯除外)。
-
由于你没有从阿联酋客户那里获得将数据存储到其他地方的同意,因此阿联酋客户的数据需要存储在阿联酋境内。
这为数据工程带来了明显的问题,因为你需要为所有数据建立独立的管道。这对建模和训练也是一个挑战——你如何构建数据集来实际使用?
获取同意
如果你已经获得了阿联酋客户的同意来移动数据,可能就没问题了。数据工程仍然需要通过特殊路径将俄罗斯客户的数据传输,但你可以将数据汇总用于训练。然而,因为你没有获得同意,所以现在卡住了!确保你知道你的同意工具包含了哪些权限和授权,以免陷入这种困境。
实时组合
假设现在已经太晚,另一种解决方案是使用一个计算平台,在训练时从不同的数据库加载数据,实时汇总数据集并训练模型,而不会将任何数据写入磁盘的单一位置。一般共识(非法律建议)是,模型本身不是个人数据,因此不受法律规则的约束。但这需要工作和基础设施,所以要戴上你的开发运维帽子。
如果你的数据量非常庞大,这可能会很快变得计算上昂贵。如果你基于这些数据生成特征,但案件的个人数据仍然是可解释的,那么你不能将所有内容都保存在一个地方,而是需要将去标识化/汇总后的特征单独保存,或将其写回到原始地区,或者每次实时重新计算它们。所有这些都是棘手的挑战。
去标识化和/或汇总
幸运的是,还有另一种选择。一旦你汇总、总结或彻底(不可逆地)去标识化数据,它就失去了个人数据保护的法律效力,你可以更轻松地处理它。这也是一个强有力的激励,让你不要存储可以识别身份的个人数据!(此外,这可以减少数据泄露和被黑客攻击的风险。)一旦数据不再受到法律保护,因为它不再是高风险数据,你就可以按自己的需要处理它,随意保存数据。提取不可识别的特征,并尽可能放弃可识别数据。
然而,决定何时数据已经充分聚合或去标识化,以至于本地化法律不再适用,有时是一个难以判断的问题,因为正如我上面所描述的,许多类型的人口统计数据是个人数据,因为与其他数据点结合时可能会导致身份可识别性。我们通常习惯于认为,去除个人身份信息(如全名、社会保险号等)后,数据就可以随意使用了。但在许多司法管辖区,法律并不这么认为!请咨询你的法律部门,并对什么构成风险保持谨慎。理想情况下,最安全的做法是数据不再是个人数据,例如不包括个人姓名、人口统计信息、地址、电话号码等,或者不再以未哈希化、可读的明文格式呈现。这不是法律建议,请咨询你的法律部门。
我们已经习惯了可以随时携带数据,进行处理和计算,然后存储数据——无论是在笔记本电脑、S3、GCS,还是其他你想要的地方。但随着我们收集越来越多的个人数据,并且越来越多的全球数据隐私法律生效,我们需要更加小心我们的操作。
常见问题
如果你不知道数据的来源该怎么办?
这是一种棘手的情况。如果你拥有一些关于人们的个人数据,但不知道这些数据来自哪里,也不知道这些人在哪里(可能也不知道他们填写了哪些同意书),我认为安全的解决方案是把这些数据当作敏感数据来处理,对其进行去标识化处理,如果能适应你的使用场景的话,可以将其聚合,并确保它不会在数据隐私法下被视为个人或敏感数据。但如果由于数据使用方式的限制无法这样处理,那就该找律师咨询了。
如果你的公司无法负担全球范围内的 数据中心怎么办?
基本上,这是相同的答案。理想情况下,你应该确保同意解决方案到位,但如果没有,我建议在从客户或用户那里接收到数据时,立即采取去标识化措施。收到用户的数据后,将数据哈希化,使其不可逆,并使用这些哈希数据。特别小心人口统计或其他敏感个人数据,但绝对要立即去标识化个人身份信息(PII)。如果你从不存储可能会被反向工程识别个人身份的敏感数据,那么你就不需要担心数据本地化的问题。这不是法律建议,请咨询你的法律部门。
为什么各国要制定这些法律?
这有几个原因,其中一些比其他的更为合理。首先,如果数据确实存储在该国,那么您在该国就有一定的商业存在(或您的数据存储提供商有),这样如果您滥用他们公民的数据,当局更容易行使管辖权并对您进行处罚。其次,这支持任何国家的科技行业经济发展,因为有人需要为数据中心提供电力、冷却、人员、建筑等支持。第三,不幸的是,一些国家对其公民实施监控制度,在该国建立数据中心使得极权政府更容易访问这些数据。
作为数据科学家,我该如何减少这些影响?
提前规划!与公司相关方合作,确保初步数据处理符合规定,同时还能获取所需的数据。并确保您了解客户所提供的同意,以及这赋予的权限。如果您仍然持有受本地化规则约束的数据,那么您需要找到一种方法来管理这些数据,确保它永远不会保存到位于错误位置的磁盘上,或者将数据去标识化和/或汇总,使其不再具有敏感性,从而使数据隐私法规不再适用。
我需要了解哪些主要的数据本地化法律?
这里列出了一些要点,但这并不全面,因为有许多此类法律,并且新的法律不断出台。(再次提醒,这不是法律建议):
-
印度:数字个人数据保护法(DPDP)是该国的国家数据隐私法规。这项法律并不像某些法律那样严格,但是印度政府的各个部门可以对特定类型的数据制定更严格的政策。印度央行就是一个例子,他们实施了比国家法律更为严格的数据本地化规则。像美国运通这样的金融公司曾因将印度金融交易的数据存储在印度境外的服务器上而被罚款。
-
中国:个人信息保护法(PIPL)是他们的国家数据隐私法规,数据本地化规则相对复杂。该法律适用于“向中国境内个人提供产品或服务”以及/或“分析和评估中国自然人的行为”,因此适用范围相当广泛。如果数据被法律视为“重要”或是“能够识别或识别自然人的信息”,那么这些数据很可能会受到数据本地化的限制。和往常一样,这不是法律建议,您应该咨询您的法律部门。
-
俄罗斯:俄罗斯已经有数据本地化法规定了相当长时间,许多公司,包括 Facebook 和 Twitter,因违反这些规定而受到罚款。“数据本地化法第 18 条第 5 款要求,收集俄罗斯公民个人数据的俄罗斯和外国数据运营商,包括通过互联网收集的数据,必须首先使用俄罗斯数据库记录、存储、整理、更新和提取数据。” 还有更多适用的法律(详情请见链接)。在初步收集和存储数据到俄罗斯服务器之后,数据可以转移到其他地方。
-
越南:他们的2018 年法律要求某些数据必须在国内存储 24 个月,应政府要求。这适用于国内公司以及某些外资公司,涉及电子商务、社交网络和其他数字服务领域。此外,任何数据传输到第三方都需要客户同意。
-
欧盟(GDPR):欧盟对某些国家设定了特定规则,规定这些国家的公民数据不能存储(例如俄罗斯),原因是对国家监控和数据隐私的担忧。
-
阿联酋:对于大多数数据,必须获得数据主体的同意才能将其数据传输到阿联酋以外的地方。在某些特定情况下,这种同意是不足够的——例如,支付处理数据必须保存在阿联酋境内。
-
日本:数据主体必须同意其数据被转移到国外,除非另一个国家与日本有特定的数据共享协议。
还有其他潜在的考量因素,例如公司的规模(一些地方对小公司有更宽松的规则,一些地方没有),所以这些内容不应被视为你公司业务的最终答案。
结论
如果你看到这里,感谢你!我知道这可能有些枯燥,但我会用一个故事来奖励你。我曾经在一家公司工作,我们的合同中有数据本地化条款(不是法律,而是另一家公司设置的规则),所以任何在欧盟产生的数据都必须保存在欧盟内,但我们已经在美国为北美设置了数据存储。
由于各种原因,这意味着我们创建了一个只包含欧盟数据的新副本数据库,数据库设在欧盟,我们将这两个版本的整个 Snowflake 数据库并行存放。如你所料,这成了一场噩梦,因为如果你创建了一个新表,或者更改了字段,或者基本上对数据库做了任何更改,你都必须记得在另一个数据库上复制这些操作。自然,大多数人都没记得这样做,因此两个数据库之间的差异变得非常大,直到架构之间存在显著差异。所以我们所有人都需要编写大量的条件代码来处理查询和提取数据的工作,以便根据你提取数据的数据库来确保列名、字段类型、表名等正确,从而能够在不将数据保存到错误位置的情况下进行“即时”合并。(别让我开始谈论 BI 目的下重复的仪表板。)我不推荐这样做!
这些规定给许多领域的数据科学家带来了真正的挑战,但保持对法律义务的了解,并保护自己的工作和公司免受责任是非常重要的。你遇到过本地化挑战吗?如果你找到了解决方案,欢迎在这篇文章下留言,分享我没有提到的内容。
进一步阅读
www.techpolicy.press/the-human-rights-costs-of-data-localization-around-the-world/
[## 什么是欧盟 GDPR 下的个人数据? - GDPR.eu
欧盟的 GDPR 仅适用于个人数据,个人数据是指任何与可识别的个人相关的信息……
carnegieendowment.org/research/2023/10/understanding-indias-new-data-protection-law?lang=en
m.rbi.org.in/Scripts/FAQView.aspx?Id=130
[## 第 53 号法令提供了关于实施越南《网络安全法》的期待已久的指导
越南的《网络安全法》于 2018 年 6 月 12 日发布,并于 2019 年 1 月 1 日生效,得到大多数……
数据缩放基础:标准化与最小-最大缩放解析
何时使用 MinMaxScaler 与 StandardScaler 或其他方法
·发表于Towards Data Science ·5 分钟阅读·2024 年 8 月 10 日
--

图片由Sven Mieke提供,来源于Unsplash
什么是缩放?
当你第一次将数据集加载到 Python 脚本或笔记本中,并查看你的数值特征时,你可能会注意到它们的量纲不同。
这意味着每一列或特征的范围都会有所不同。例如,一个特征的值范围可能是 0 到 1,而另一个特征的值范围可能是 1000 到 10000。
以 UCI 机器学习库中的酒质数据集为例(CC by 4.0 许可证)。

来自 UCI 酒质数据集的几个特征。图像来源:作者
缩放本质上是将所有特征拉近到相似或相同的范围或尺度的过程,比如将它们转换为所有值都在 0 到 1 之间。
何时(以及为什么)需要进行缩放
在训练/拟合机器学习模型之前对特征进行缩放有几个重要原因:
- 确保所有特征对模型的贡献是相等的。 当某个特征的范围很大时...
我希望早点知道的数据科学建议
关于如何学习和实践数据科学的建议
·发表于 Towards Data Science ·阅读时长 8 分钟·2024 年 4 月 29 日
--

图片来自 energepic.com:www.pexels.com/photo/woman-sitting-in-front-of-macbook-313690/
学习和实践数据科学可能会很具挑战性——相信我,我已经做了超过四年!我经历了所有复杂的数学方程式和复杂的代码。不过,我现在知道如何通过我一路上积累的技巧和建议来更好地应对这一切,这些我将在本文中与大家分享。
如何接近数学
这是贝尔曼方程,它是强化学习的核心之一,强化学习是机器学习中最令人兴奋的领域之一。
让我们说实话吧。
如果你第一次看到这个,你可能完全不知道发生了什么——除非你是某位爱因斯坦级别的天才!
课程、大学学位和在线资源常常会用方程式来解释机器学习算法是如何工作的。这些数学“从技术上讲”解释了发生了什么,但它是非常理论化的,远非直观。
数据科学在家:用蒙特卡罗与遗传算法解决保姆日程难题
在为我们的育儿寻找完美保姆的过程中,带来秩序与简化我们的搜索
·发表于Towards Data Science·12 分钟阅读·2024 年 9 月 6 日
--
作为一名数据科学领域的领导者,我习惯了带领团队将混乱转化为清晰。但当混乱发生在自己家庭的保姆日程上时,即使是最精心安排的计划也可能出错。工作会议、午休时间以及不可预测的班次让我们的思绪不停地打转——直到我意识到,我可以用那些解决商业问题的算法来解决一个非常私人的问题。凭借蒙特卡罗模拟、遗传算法和一丝父母的智慧,我开始了将我们混乱的日程一项项通过算法调整的旅程。结果呢?好吧,可以说我们的保姆新日程看起来简直是完美契合。

设置舞台:伟大的日程难题
我们的家庭日程就像是闯入瓷器店的公牛之后的残局。父母 1 的工作时间是标准的朝九晚五,算是这个难题中的简单部分。但接着,父母 2 出现了,他在芝加哥一家医院的急诊科值班,时间完全无法预测。有些天是黎明破晓开始,有些则一直延续到深夜,完全没有规律可循。突然之间,原本简单的日程变成了一个没有解决方案的魔方。

图片由Nick Fewings拍摄,来源于Unsplash
我们把自己想象成这个混乱中的父母。早晨变成了疯狂的冲刺,下午总是充满了猜测,晚上——谁知道呢?我们的家庭似乎要迎来一个“谁在看保姆?”的未来。我们需要一个决策分析解决方案,能够像急诊室一样迅速应对突发情况。
就在那时我突然意识到:如果我能使用我在工作中依赖的工具来解决这个不断变化的难题会怎样?如果我们不再与混乱作斗争,而是能够驾驭它——甚至预测它呢?带着这个想法,到了将我们保姆的时间表置于算法显微镜下的时刻。
数据科学工具箱:当怀疑时,进行模拟
由于我们的家庭时间表像被公牛闯入的瓷器店一样凌乱不堪,显然我们需要的不仅仅是一个日历和一份祈祷。这时,我转向了蒙特卡洛模拟——数据科学家版的水晶球。这个想法很简单:如果我们不能准确预测混乱何时降临,为什么不模拟所有可能出错的情况呢?
蒙特卡洛模拟是一种通过随机抽样来建模系统行为的技术。在这个案例中,我们将使用它随机生成父母 2 的可能工作时间表,从而模拟他们班次的不确定性,经过多次迭代。
想象一下运行数千个“如果”的场景:如果父母 2 被叫去做早班怎么办?如果紧急情况让他们在医院耽搁了怎么办?如果不幸的是,两个父母的时间表在最糟糕的时刻重叠怎么办?蒙特卡洛方法的魅力在于,它不仅仅给你一个答案——它给你数千个答案,每一个都是对未来的不同展望。
这不仅仅是预测父母 2 何时可能被叫去参与抢救;它关乎确保我们的保姆准备好应对急诊室可能抛向我们的每一个变化。无论是早班还是深夜的紧急情况,模拟帮助我们看到所有可能性,以便我们能为最可能发生的情况——以及最糟糕的情况——做好规划。可以把它看作是一种混乱保险,并且附带一点心安。
在以下代码块中,模拟生成了父母 2 的五天工作周(周一至周五)的工作时间表。每天,父母 2 被叫去工作的概率是固定的,如果被叫去工作,就会根据这些概率从一组预定义的班次中随机选择一个班次。我们还增加了一个功能,考虑到周三下午 1 点的固定会议,并据此调整父母 2 的时间表。
import numpy as np
def simulate_parent_2_schedule(num_days=5):
parent_2_daily_schedule = [] # Initialize empty schedule for Parent 2
for day in range(num_days):
if np.random.rand() < parent_2_work_prob: # Randomly determine if Parent 2 works
shift = np.random.choice(
list(parent_2_shift_probabilities.keys()),
p=[parent_2_shift_probabilities[shift]['probability'] for shift in parent_2_shift_probabilities]
)
start_hour = parent_2_shift_probabilities[shift]['start_hour'] # Get start time
end_hour = parent_2_shift_probabilities[shift]['end_hour'] # Get end time
# Check if it's Wednesday and adjust schedule to account for a meeting
if day == 2:
meeting_start = 13
meeting_end = 16
# Adjust schedule if necessary to accommodate the meeting
if end_hour <= meeting_start:
end_hour = meeting_end
elif start_hour >= meeting_end:
parent_2_daily_schedule.append({'start_hour': meeting_start, 'end_hour': end_hour})
continue
else:
if start_hour > meeting_start:
start_hour = meeting_start
if end_hour < meeting_end:
end_hour = meeting_end
parent_2_daily_schedule.append({'start_hour': start_hour, 'end_hour': end_hour})
else:
# If Parent 2 isn't working that day, leave the schedule empty or just the meeting
if day == 2:
parent_2_daily_schedule.append({'start_hour': 14, 'end_hour': 16})
else:
parent_2_daily_schedule.append({'start_hour': None, 'end_hour': None})
return parent_2_daily_schedule
我们可以使用 simulate_parent_2_schedule 函数来模拟父母 2 的工作周排班,并将其与父母 1 更为固定的 9 到 5 的排班相结合。通过重复这一过程 52 周,我们可以模拟一个典型的年份,并识别父母照看空档。这使我们能够计划在最需要保姆的时候。下图总结了在模拟的 52 周期间父母的不可用时间,帮助我们可视化在哪些时段需要额外的育儿支持。

作者特别提供的图片
培育完美保姆:基因算法的威力
拥有了模拟我们排班可能遇到的各种变数,我知道是时候引入一些强力的优化技术了。于是,基因算法登场——这是一种受自然选择启发的优化方法,通过迭代进化候选解的种群来找到最佳解决方案。

图片来自 Sangharsh Lohakare 在 Unsplash
在这种情况下,每个“候选者”都是一个潜在的保姆特征集合,例如她们的可用性和灵活性。算法评估不同的保姆特征,并通过迭代优化这些特征,找到最适合我们家庭需求的保姆。结果是什么?一位高度优化的保姆,她的排班偏好能够平衡我们的父母照看空档和保姆的可用性。
这种方法的核心是我喜欢称之为“保姆染色体”的东西。在基因算法中,染色体只是表示潜在解的一种方式——在我们的案例中,就是不同的保姆特征。每个“保姆染色体”都有一组特征来定义她的排班:每周保姆可以工作的天数、她每天最多可以工作的小时数,以及她调整开始时间的灵活性。这些特征构成了算法将考虑的每个潜在保姆排班的基本构件。
定义保姆染色体
在基因算法中,“染色体”代表一个可能的解决方案,在本例中,它是一组定义保姆排班的特征。以下是我们如何定义保姆的特征:
# Function to generate nanny characteristics
def generate_nanny_characteristics():
return {
'flexible': np.random.choice([True, False]), # Nanny's flexibility
'days_per_week': np.random.choice([3, 4, 5]), # Days available per week
'hours_per_day': np.random.choice([6, 7, 8, 9, 10, 11, 12]) # Hours available per day
}
每个保姆的排班由她的灵活性(是否可以调整开始时间)、每周可以工作的天数以及每天可以工作的最大小时数来定义。这为算法提供了评估各种潜在排班的灵活性。
为每个保姆构建排班表
一旦定义了保姆的特征,我们需要生成一个符合这些约束条件的每周排班表:
# Function to calculate a weekly schedule based on nanny's characteristics
def calculate_nanny_schedule(characteristics, num_days=5):
shifts = []
for _ in range(num_days):
start_hour = np.random.randint(6, 12) if characteristics['flexible'] else 9 # Flexible nannies have varying start times
end_hour = start_hour + characteristics['hours_per_day'] # Calculate end hour based on hours per day
shifts.append((start_hour, end_hour))
return shifts # Return the generated weekly schedule
该函数根据保姆的灵活性和工作时间构建保姆的时间表。灵活的保姆可以在早上 6 点到中午 12 点之间开始工作,而其他保姆则有固定的时间表,工作时间从固定时间开始和结束。这使得算法能够评估一系列可能的每周时间表。
选择最佳候选人
一旦我们生成了初始的保姆时间表群体,我们就使用适应度函数来评估哪些时间表最符合我们的育儿需求。最适合的时间表会被选择进入下一代:
# Function for selection in genetic algorithm
def selection(population, fitness_scores, num_parents):
# Normalize fitness scores and select parents based on probability
min_fitness = np.min(fitness_scores)
if min_fitness < 0:
fitness_scores = fitness_scores - min_fitness
fitness_scores_sum = np.sum(fitness_scores)
probabilities = fitness_scores / fitness_scores_sum if fitness_scores_sum != 0 else np.ones(len(fitness_scores)) / len(fitness_scores)
# Select parents based on their fitness scores
selected_parents = np.random.choice(population, size=num_parents, p=probabilities)
return selected_parents
在选择步骤中,算法通过适应度函数评估保姆时间表群体,衡量保姆的可用性与家庭需求的匹配度。最适合的时间表,即那些最能满足所需工作时间的时间表,会被选为下一代的“父母”。
添加变异保持趣味性
为了避免陷入次优解,我们通过变异引入了一些随机性。这使得算法可以通过偶尔调整保姆的时间表,探索新的可能性:
# Function to mutate nanny characteristics
def mutate_characteristics(characteristics, mutation_rate=0.1):
if np.random.rand() < mutation_rate:
characteristics['flexible'] = not characteristics['flexible']
if np.random.rand() < mutation_rate:
characteristics['days_per_week'] = np.random.choice([3, 4, 5])
if np.random.rand() < mutation_rate:
characteristics['hours_per_day'] = np.random.choice([6, 7, 8, 9, 10, 11, 12])
return characteristics
通过引入小的变异,算法能够探索到一些本来可能没有考虑到的新时间表。这种多样性对于避免局部最优解并通过多个代改进解非常重要。
向完美时间表进化
最后的步骤是进化。在选择和变异机制到位后,遗传算法在多个代中进行迭代,每一轮都会生成更好的保姆时间表。以下是我们如何实现进化过程的:
# Function to evolve nanny characteristics over multiple generations
def evolve_nanny_characteristics(all_childcare_weeks, population_size=1000, num_generations=10):
population = [generate_nanny_characteristics() for _ in range(population_size)] # Initialize the population
for generation in range(num_generations):
print(f"\n--- Generation {generation + 1} ---")
fitness_scores = []
hours_worked_collection = []
for characteristics in population:
fitness_score, yearly_hours_worked = fitness_function_yearly(characteristics, all_childcare_weeks)
fitness_scores.append(fitness_score)
hours_worked_collection.append(yearly_hours_worked)
fitness_scores = np.array(fitness_scores)
# Find and store the best individual of this generation
max_fitness_idx = np.argmax(fitness_scores)
best_nanny = population[max_fitness_idx]
best_nanny['actual_hours_worked'] = hours_worked_collection[max_fitness_idx]
# Select parents and generate a new population
parents = selection(population, fitness_scores, num_parents=population_size // 2)
new_population = []
for i in range(0, len(parents), 2):
parent_1, parent_2 = parents[i], parents[i + 1]
child = {
'flexible': np.random.choice([parent_1['flexible'], parent_2['flexible']]),
'days_per_week': np.random.choice([parent_1['days_per_week'], parent_2['days_per_week']]),
'hours_per_day': np.random.choice([parent_1['hours_per_day'], parent_2['hours_per_day']])
}
child = mutate_characteristics(child)
new_population.append(child)
population = new_population # Replace the population with the new generation
return best_nanny # Return the best nanny after all generations
在这里,算法通过多个代进行进化,基于适应度分数选择最佳的保姆时间表,并通过变异允许新解的出现。经过几个代后,算法会收敛到最好的保姆时间表,优化我们家庭的覆盖需求。
最终思考
采用这种方法,我们应用遗传算法迭代改进保姆时间表,确保所选的时间表能够应对第二位家长不可预测的工作班次,同时平衡我们家庭的需求。遗传算法对于这项任务可能有些过于复杂,但它们使我们能够探索各种可能性,并随着时间的推移优化解。
下图描述了保姆适应度分数随时间的变化过程。算法在经过几个代后,能够迅速收敛到最佳保姆染色体。

作者特别插图

作者特别插图
从混乱到清晰:可视化解决方案
在算法完成工作并优化了我们所寻找的保姆特征后,接下来的步骤是理解结果。这就是可视化派上用场的地方,我必须说,这真是一个改变游戏规则的工具。在我们有了图表和图形之前,我们的排班就像是一个错综复杂的网络,充满了相互冲突的承诺、无法预见的变化和临时的调整。但一旦我们将数据转化为可视化的形式,一切就开始变得清晰。
热力图:一目了然的覆盖情况
热力图提供了一抹美丽的色彩,将抽象的内容转化为可触及的东西。颜色越深,表示需要的保姆覆盖时间越多;颜色越浅,表示我们需要的保姆覆盖时间越少。这使得我们可以一眼就发现潜在的问题。星期五需要更多的覆盖吗?查看热力图。星期三保姆的工作时长是否过长?(是的,这很可能。)热力图会告诉你。它让我们迅速理清思路,帮助我们在需要的地方调整排班,并在一切都完美匹配时带来平静的心态。

作者特别插图
通过可视化结果,我们不仅解决了排班难题——我们还让它变得易于理解和跟随。我们不再急于弄清楚需要什么样的保姆,而是可以直接查看可视化图表,了解他们需要覆盖哪些内容。从混乱到清晰,这些可视化工具将数据转化为洞察,帮助我们轻松挑选保姆。
影响:家庭和谐
在我将数据科学工具包应用到我们家庭的排班问题之前,感觉有些压倒性。我们开始面试保姆,但并没有真正了解我们在寻找什么,或者我们需要什么来维持家里的秩序。
但是在用蒙特卡罗模拟和遗传算法优化了保姆排班之后,差异简直是天壤之别。从前的混乱,现在变成了理解。突然间,我们有了一个清晰的计划,一张谁在何时何地的地图,最重要的是,一张关于要找什么样保姆的路线图。
最大的变化不仅仅是在排班表本身——而是我们感受到的变化。知道你有一个行之有效的计划,它能够在意外发生时灵活适应,这种心态带来了某种平静。对我个人来说,这个项目不仅仅是数据科学的另一个应用,它是一个将我每天在职业生活中使用的技能,应用到直接影响家庭的事务上的机会。
数据科学在家庭中的力量
我们往往认为数据科学是为职场而存在的东西,帮助企业优化流程或做出更聪明的决策。但正如我在保姆排班项目中所学到的那样,数据科学的力量并不必局限于办公室的门外。它是一个可以解决日常挑战、简化混乱局面,甚至给家庭生活带来更多平静的工具包。

图片由Kenny Eliason拍摄,来源于Unsplash
也许你的“保姆难题”并不是关于育儿的。也许它是关于制定最有效的购物清单、管理家庭财务,或是规划家庭假期的行程。无论是什么情况,我们在工作中使用的工具——蒙特卡洛模拟、遗传算法和数据驱动的优化——在家中也能发挥奇妙的作用。你不需要一个复杂的问题来开始,只需要对数据如何帮助解决即使是最平凡挑战的好奇心。
所以这是我给你的挑战:环顾你的生活,找出一个地方,看看数据能带来什么变化。也许你会偶然发现一种节省时间、金钱,甚至只是带来一点心灵宁静的方法。它可能从一个简单的电子表格开始,但谁知道它能引领到哪里呢?也许你会最终构建属于自己的“保姆奥林匹克”或解决自己的一场调度噩梦。
随着我们不断前进,我认为数据科学将成为我们个人生活中更加重要的一部分——不仅仅是我们工作中使用的工具,更是帮助我们管理日常挑战的工具。最终,关键是利用数据的力量让我们的生活变得更加轻松。
Nanny 调度问题的代码和数据可以在 Github 上找到:github.com/agentdanger/nanny-simulation
关于我的专业信息可以在我的网站上找到:courtneyperigo.com
数据科学最佳实践,第二部分 — 一起工作
你不能仅仅投入更多的数据科学家来处理这个模型,并期待准确率神奇地提高。
·发表于Towards Data Science ·阅读时间 10 分钟·2024 年 1 月 5 日
--

图片来源:Joseph Ruwa:www.pexels.com/photo/set-of-chess-pieces-in-daylight-4038397/
(第一部分在这里)
并非所有的数据科学项目都一样。
我所见过和构建的大多数数据科学项目,最初都是作为一次性证明概念的快速实现而诞生的。临时的单次性解决方案,用来让一些边缘性的工作得以完成。
其中一些项目最终可能会变成别的东西,也许会变得更大或在帮助组织目标实现方面更为核心。
只有少数项目能够在长期内不断成长和成熟。
这些特别的项目通常是解决对组织具有特殊意义的问题。例如,在线广告网络的 CTR 预测器,或视觉效果生成器的图像分割模型,或内容过滤服务的脏话检测器。
这些项目也通常会投入大量公司资源进行优化,理应如此。当某些准确性指标的微小改进可以直接带来更高的收入,或成为产品发布和融资轮次的成败关键时——组织应该毫不吝啬地投入资源。
我们在这篇文章中讨论的资源是数据科学家。
如果你从未管理过一个项目、一个团队或公司,可能会觉得把人当作“资源”来对待有些奇怪。但请记住,这些是有着有限时间的专家,我们利用他们的时间完成有益于组织的任务。
现在请注意:资源必须被管理,并且它们的使用应当得到优化。
一旦某个模型变得足够大并且如此核心,以至于有多个数据科学家共同致力于改进它,就必须确保他们能够在不相互干扰、不阻碍对方的情况下进行工作。相反,团队成员应当能够轻松地互相帮助,并在彼此的成功基础上进行建设。
我在不同地方见过的常见做法是,每个团队成员尝试自己的“东西”。根据项目的特殊性,这可能意味着不同的模型、优化算法、深度学习架构、工程特征等。
这种工作模式在成员之间可能看起来是垂直的,因为每个人都可以独立工作,不会创造出可能阻碍或阻挡他人进展的依赖关系。
然而,情况并非完全如此,正如我曾经在这里抱怨过。
例如,如果某个团队成员在某个特别有价值的特征上发现了重要突破,其他成员可能希望在他们的模型中尝试使用相同的特征。
在某个时间点,一个特定的模型可能会表现出性能的飞跃,很快我们会看到基于那个最佳模型的分支版本,每个版本与下一个稍有不同。这是因为优化过程往往会在当前最优解的附近寻找更好的最优解——不仅仅是通过梯度下降,也包括通过人类的创造力。
这种情况可能会导致比预期更高的耦合性和更多的依赖关系。
即便我们确保不是所有数据科学家都朝着同一个方向收敛,我们仍然应该尝试标准化他们的工作,可能还需要强制执行与下游使用者的合同,以便简化部署并节省机器学习工程师的时间。
前提
我们希望数据科学家们能够以一种既允许独立工作,又能同时重用他人工作的方式来解决相同的问题。
为了举例说明,我们假设自己是一个团队的成员,正在处理Iris 花卉数据集。这意味着训练数据足够小,可以放入内存中的 pandas 数据框中,尽管我们提出的工具可以应用于任何类型和规模的数据。
我们希望能够允许创造性的自由,这意味着每个成员可以完全自由地选择自己的建模框架——无论是scikit-learn、Keras、仅用 Python 的逻辑等。
我们的主要工具将是应用面向对象编程(OOP)原则的过程抽象,以及将个人的工作标准化为统一的语言。
免责声明
在这篇文章中,我将举例说明如何将数据科学过程抽象化,以便促进团队协作。重点不是我们所提出的具体抽象。重点是数据科学经理和领导者应努力促进数据科学家的工作,无论是通过抽象、协议、版本控制、流程简化或其他任何方法。
这篇博客文章绝不是在提倡 重新发明轮子。是否使用现成的产品、开源工具或开发内部解决方案的决定,应当与与项目相关的数据科学和机器学习工程团队一起做出。
既然这些已经处理完了,让我们直接切入正题。
从结尾开始
完成后,我们希望有一个统一的框架来将我们的模型贯穿于整个流程,从训练到预测。因此,我们从定义公共流程开始:
-
首先,我们获取训练数据作为输入。
-
我们可能想提取额外的特征来增强数据集。
-
我们创建一个模型并反复训练,直到我们对其损失或指标满意为止。
-
然后我们将保存模型到磁盘或其他持久化机制。
-
我们稍后需要加载模型回到内存中。
-
然后,我们可以对新的、未见过的数据进行预测。
根据上述流程,我们来声明一个基本结构(即接口)供模型使用:
class Model:
def add_features(self, x):
...
def train(self, x, y, train_parameters=None):
...
def save(self, model_dir_path):
...
@classmethod
def load(cls, model_dir_path):
...
def predict(self, x):
...
请注意,这与我们在现有框架中使用的接口没有太大区别——然而,每个框架都有其独特之处,例如命名上的差异:“fit”与“train”或它们如何在磁盘上持久化模型。将流程封装在统一的结构内,可以避免我们在其他地方添加实现细节,例如在使用不同的模型进行部署时。
现在,一旦我们定义了基本结构,让我们讨论一下我们实际如何使用它。
系统设计
特征
我们希望“特征”作为可以轻松传递并添加到不同模型中的元素。我们还应当认识到,每个模型可能会使用多个特征。
我们将尝试为我们的Feature类实现一种插件式的基础设施。我们会有一个所有特征的基类,然后Model类在接收到输入数据时,可以顺序地在内存中实例化不同的特征。
封装的模型
我们还希望将实际的模型封装在我们的系统中,以便在团队成员之间进行传递。但我们希望保持能够在不写大量新代码的情况下更改模型参数的选项。
我们会将它们抽象到一个不同的类中,并命名为ModelInterface,以避免与我们的Model类混淆。后者将转而将相关的方法调用委托给前者。
特征
我们的特征可以视为以 pandas 数据框作为输入的函数。
如果我们为每个特征提供一个唯一名称,并将其封装在与其他特征相同的接口中,我们可以非常容易地重用这些特征。
让我们定义一个基类:
class Feature(ABC):
@abstractmethod
def add_feature(self, data):
...
让我们创建一个实现,比如花萼对角线长度:
class SepalDiagonalFeature(Feature):
def add_feature(self, data):
data['SepalDiagonal'] = (data.SepalLength ** 2 + \
data.SepalWidth ** 2) ** 0.5
我们将使用这个类的一个实例,因此我创建了一个单独的文件来存储所有特征:
sepal_diagonal = SepalDiagonalFeature()
这个特定的实现已经展示了一些我们做出的决策,无论是有意识的还是无意识的:
-
输出列的名称是函数代码中的字面量,并未保存在其他地方。这意味着我们无法轻松构建已知列的列表。
-
我们选择在
add_feature函数中将新列添加到输入数据框中,而不是返回列本身并在外部作用域中添加它。 -
我们不知道,除非通过阅读函数代码,哪些列依赖于这个特征。如果我们知道,我们可以构建一个有向无环图(DAG)来决定特征创建的顺序。
此时,这些决策是容易可逆的,然而当我们建立了几十个这样的特征后,我们可能不得不重构所有这些特征,以便对基类进行更改。也就是说,我们应该提前决定我们期望系统的表现,并且意识到每个选择的影响。
让我们通过实现add_features函数来扩展我们的Model基类:
def __init__(self, features: Sequence[Feature] = tuple()):
self.features = features
def add_features(self, x):
for feature in self.features:
feature.add_feature(x)
现在,任何人都可以在创建模型实例时使用sepal_diagonal特征。
如果我们没有通过抽象来方便地重用这些特征,Alice 可能会选择复制 Bob 的逻辑,并稍作修改以适应她的预处理,同时在过程中使用不同的命名,通常会增加技术债务。
可能出现的问题是“那常见操作,比如加法呢?我们每次想使用加法时都需要实现一个加法吗?”。
答案是否定的。为此,我们可以通过self参数使用实例字段:
@dataclass
class AdditionFeature(Feature):
col_a: str
col_b: str
output_col: str
def add_feature(self, data):
data[self.output_col] = data[self.col_a] + data[self.col_b]
比如说,如果我们想要添加花瓣长度和花瓣宽度,我们会通过petal_sum = AdditionFeature('petalLength', 'petalWidth', 'petalSum')来创建一个实例。
对于每个操作符/函数,你可能需要实现一个类,乍一看这可能让人觉得很有压力,但你会很快发现,这个列表相当简短。
模型接口
这是我为模型接口使用的抽象:
class ModelInterface(ABC):
@abstractmethod
def initialize(self, model_parameters: dict):
...
@abstractmethod
def train(self, x, y, train_parameters: dict):
...
@abstractmethod
def predict(self, x):
...
@abstractmethod
def save(self, model_interface_dir_path: Path):
...
@classmethod
def load(cls, model_interface_dir_path: Path):
...
这里给出了一个使用scikit-learn模型的示例实现:
class SKLRFModelInterface(ModelInterface):
def __init__(self):
self.model = None
self.binarizer = None
def initialize(self, model_parameters: dict):
forest = RandomForestClassifier(**model_parameters)
self.model = MultiOutputClassifier(forest, n_jobs=2)
def train(self, x, y, w=None):
self.binarizer = LabelBinarizer()
y = self.binarizer.fit_transform(y)
return self.model.fit(x, y)
def predict(self, x):
return self.binarizer.inverse_transform(self.model.predict(x))
def save(self, model_interface_dir_path: Path):
...
def load(self, model_interface_dir_path: Path):
...
如你所见,代码主要是将不同的操作委托给现成的模型。在train和predict中,我们还将目标值在枚举值和独热编码向量之间来回转换,实际上是在我们的业务需求和scikit-learn的接口之间转换。
现在我们可以更新我们的Model类,以适应ModelInterface实例。完整代码如下:
class Model:
def __init__(self, features: Sequence[Feature] = tuple(), model_interface: ModelInterface = None,
model_parameters: dict = None):
model_parameters = model_parameters or {}
self.features = features
self.model_interface = model_interface
self.model_parameters = model_parameters
model_interface.initialize(model_parameters)
def add_features(self, x):
for feature in self.features:
feature.add_feature(x)
def train(self, x, y, train_parameters=None):
train_parameters = train_parameters or {}
self.add_features(x)
self.model_interface.train(x, y, train_parameters)
def predict(self, x):
self.add_features(x)
return self.model_interface.predict(x)
def save(self, model_dir_path: Path):
...
@classmethod
def load(cls, model_dir_path: Path):
...
再次强调,我创建了一个文件来管理我的模型,并在其中包含这一行:
best_model_so_far = Model([sepal_diagonal], SKLRFModelInterface(), {})
这个best_model_so_far是一个可重用的实例,但请注意它并未经过训练。要获得一个可重用的训练过的模型实例,我们需要将模型持久化。
保存和加载
我选择在本帖中省略保存和加载的细节,因为内容已经有些冗长,但欢迎查看我的清洁数据科学 GitHub 仓库,里面有一个完整操作的 Hey 示例。
摘要
本文提出的框架绝对不是一种适用于所有情况的解决方案,来规范数据科学团队在单个模型上的工作,也不应被视为一种。每个项目都有其独特的细节和需求,这些都需要特别处理。
相反,本文提出的框架应该仅仅作为进一步讨论的基础,把促进数据科学家工作的主题放在聚光灯下。
精简工作流程应该是数据科学团队领导和经理们的一项目标,而抽象只是工具箱中的一项内容。
常见问题
问:如果你只需要从子类获取特定功能,难道不应该使用协议(Protocol)而不是 ABC 吗?
答:我可以这样做,但这不是一个高级的 Python 课程。有句希伯来谚语说:“书呆子无法教书。”所以,你懂的。
问:那删除特征呢?这也很重要啊!
答:当然可以。你可以选择将它们存放在哪里!你可以使用一个带参数的Feature实现来删除列,或者在ModelInterface类中完成,例如。
问:那如何衡量模型之间的表现呢?
答:有一个更高级的机制来跟踪模型的指标将非常棒。这个问题超出了本帖的范围。
问:我如何跟踪已训练的模型?
答:这可以是你保存训练模型的路径列表。确保给它们起个有意义的名字。
问:我们是不是也应该将数据集的创建抽象出来(在传递给train函数之前)?
答:我本来打算提到这个的,但后来我膝盖中箭了。不过,是的,拥有不同的完整数据集样本,或者是我们像处理特征和模型接口一样可以传递的多个数据集,确实是个不错的主意。
问:我们是不是在给数据科学家增加难度?
答:我们应该权衡这件事的利弊。虽然习惯于这种抽象的限制性可能需要一些时间,但从长远来看,它可能会节省大量时间。
数据科学职业挑战——以及如何克服它们
·发布于 Towards Data Science ·发送为 新闻通讯 ·3 分钟阅读·2024 年 3 月 21 日
--
在最基础的层面上,大多数工作相关的挑战都来自类似的来源,无论是哪个领域或行业:必须处理职业关系,并与那些可能并不总是与你在同一页面上的人进行沟通。而你必须在目标、可用资源和有限时间的限制下完成这些任务——除此之外,你还可能需要应对生活中的其他问题。
但如果我们仔细观察,就会发现,不仅仅是在不同的职业和工作类型之间,即使是在明确的角色和学科内部,也会出现不同的模式。对于数据和机器学习(ML)专业人员来说,尽管他们的技能和责任范围非常广泛,但常常需要解决类似的问题,这似乎就是他们的情况。
本周,我们重点介绍了一些最新的文章,这些文章聚焦于我们反复出现的常见数据科学工作和职业挑战;这些挑战基于作者的个人经验,但提供的见解可能有助于我们社区中的广泛群体。请享受阅读!
-
从零开始建立数据部门指南
对于小型公司中的数据专业人员来说,最常见的场景之一恰恰是最难应对的情况之一:成为第一个(也是唯一一个)处理数据的人员。Marie Lefevre 分享了她从零开始建立数据职能的个人经历,以及她为处于类似情况的其他人提供的经验和启示。
-
教授非技术团队 SQL 的经验教训
在过去几年里,让数据可访问化是许多数据团队的共同目标,但要实现这一目标却从不容易。Jordan Gomes解释了他是如何教授非技术同事使用 SQL 的,并为任何希望围绕这一主题组织内部培训的人提供了建议。

图片由Kelly Sikkema提供,来源于Unsplash。
-
我如何在加入 LinkedIn 之前成为数据科学家
你需要一份工作来获得经验,而你需要经验才能找到工作……听起来很熟悉吧?这个难题并非数据科学所独有,但它在这个行业中以特定的方式呈现,Jimmy Wong描述了他如何走上数据岗位的经历,这是对那些还不确定下一步该如何走的早期数据科学家的一个有用示例(和灵感来源)。
-
我求职马拉松中的 4 个技巧
“天真地,我估计几个月后就能找到理想的工作。但现实是,这个过程比我预期的要长。”即使在最好的情况下,求职也很少是轻松愉快的,更何况是在我们过去几年经历的这种不确定的经济环境中。Ceren Iyim最近花了几个月时间寻找她的下一份工作,并且为其他处于类似境地的数据专业人士提供了一些实用的建议。
我们在过去几周发布了许多关于其他主题的精彩文章,希望你能抽时间去阅读它们:
-
想要深入了解非常详细的 Q-learning 及其基础数学知识,不要错过Cristian Leo对这一主题的深度解析。
-
如果你使用 SQLAlchemy 并希望扩展对这个流行工具包的了解,Lynn G. Kwong的最新指南重点介绍了如何进行异步数据库请求。
-
对当前机器人技术感兴趣吗?Nikolaus Correll分享了关于人形机器人技术最新进展的深刻概述,以及它如何与前沿的多模态模型相交。
-
想要动手实践一下吗?Ida Silfverskiöld耐心地概述了部署 ETL 管道的端到端工作流程,使用 Fargate 将其部署到 ECS。
-
不确定如何做出关于数据科学教育路径的明智决策?Khouloud El Alami有一些重要的经验教训要分享。
-
任何对基于网格的算法感兴趣的人都应该花些时间阅读Rhys Goldstein的关于 3D 网格邻域的迷人探索。
感谢你支持我们作者的工作!如果你有兴趣加入他们的行列,何不写下你的第一篇文章?我们很期待阅读。
直到下次 Variable,
TDS 团队
《学校的数据科学,第一部分:使用 Python 和 OR-Tools 自动化课程表管理》
一种免费的、人类参与的方式来组织教职员工的替代工作
·发表于 Towards Data Science ·阅读时间:10 分钟·2024 年 6 月 12 日
--
想象一下学校的课程表:

图片由作者提供
看起来很简单,对吧?
错了!
制定课程表是一项令人头疼的工作。它需要花费大量时间,而且由于可能有高达 20% 的教职员工在某一天生病或缺席,因此它会不断需要调整。
结果是,一堆由学校的“数据人员”维护的庞大 Excel 表格:

我们总是担心“timetable_8b_final_FINAL_v2.xlsx”的第 21 个标签页崩溃。图片由作者提供
我知道这一点,因为我来自一个教师家庭。我的妻子是教师,我的父母都是教师,甚至我的三位祖父母也是教师。坦率来说,我很惊讶我自己不是教师。
相反,我选择了一个同样疯狂的职业——数据科学。作为一名数据科学家,我经常认为学校在数据驱动的自动化和分析方面有很大的潜力。
Python 来拯救我们
在这篇文章中,我们将使用 Python 来自动化生成教职员工的课程表。
学校的数据科学,第二部分:使用 Python 进行学生选修课程分配
是时候停止依赖allocations_final_FINALv2.xlsx了
·发表于Towards Data Science ·阅读时间 10 分钟·2024 年 6 月 24 日
--
想象以下场景:你是一名教师,被要求帮助为 200 名学生创建一个课外“选项/选修课”项目。
每个学生选择他们的四个优先课程,你需要以最大化学生满意度的方式进行分配,同时考虑到各种约束条件(例如,选修课需要至少 5 名学生才能开设)。
你怎么做的?

作者提供的图片
学校的数据科学
在本文中,我将向你展示如何使用数据科学——特别是一种叫做线性规划的技术——来解决这个问题。
这是一个更广泛系列的一部分,我将在其中展示一些学校如何应用数据科学和人工智能来改善以下方面:
-
排课
-
评估
-
课程计划
我的目标是帮助你自动化那些枯燥的工作,从而腾出时间让教师专注于他们最擅长的事情:教学。
为什么我应该关心(如果我不是教师的话)?
可持续性数据科学——绿色库存管理
模拟店铺配送频率对时尚零售商二氧化碳排放的影响。
·发布于《Towards Data Science》 ·10 分钟阅读·2024 年 3 月 14 日
--

(图片由作者提供)
绿色库存管理可以定义为以环境可持续的方式管理库存。
这涉及到一套减少订单准备和配送对环境影响的过程和规则。

店铺补货系统 —— (图片由作者提供)
大多数零售商使用库存管理系统,该系统采用基于规则的方法进行补货和满足需求。
补货频率是一个重要参数,可以用来优化配送网络。
作为数据科学经理,你能模拟配送频率对二氧化碳排放的影响吗?
由于主要零售集团应承诺在2030 年前减少其二氧化碳排放量,这可以成为你实现这些目标的战略工具。
在本文中,我们将探讨如何使用数据科学来模拟实施绿色库存管理的可持续性举措,以支持一家时尚零售公司的配送。
问题陈述
场景:零售库存管理
你是一个国际服装集团的数据科学经理,该集团在全球拥有门店。

时尚零售公司供应链网络 —— (图片由作者提供)
配送团队正在管理从本地仓库向店铺的补货。
你的同事,库存经理,负责在ERP 系统中设置商店补货规则。

库存管理规则 — 周期性审核政策 —(图片来源:作者)
她已经实施了一个周期性审核政策 订单上限(R, S)
-
ERP 系统每R 天审核商店的库存水平(也称为在手库存):IOH
-
对于每次审核,计算库存水平与目标库存 S之间的差距:S — IOH
-
创建补货订单并将其传送至仓库,数量为Q = S — IOH
其目的是将缺失的数量交付以达到目标库存水平。

补货过程 —(图片来源:作者)
传输后,订单在仓库准备,并在一定的交货时间 LD(天)后送到商店。
目标库存被定义为吸收需求波动和补货提前期,以避免商店缺货(空架子)。

设置目标库存的公式 —(图片来源:作者)
本文将不会详细讲解如何设置这些差异。
然而,如果你需要更多信息,你可以在本文中找到详细的解释。
基于周期性审核政策实施库存管理规则,以减少商店补货次数
towardsdatascience.com
此规则的关键参数之一是两次审核之间的时间间隔,这将决定商店补货的频率。
如果我们改变这个审核周期会怎样?
什么是绿色库存管理?
审核周期设置了商店补货订单创建的频率。
-
对于 R = 2 天:商店的补货非常频繁
你可以设置较低的目标库存水平来覆盖审核周期内的需求。
-
对于 R = 15 天:商店的补货不太频繁
每次补货的订单数量必须更高,因为你的目标库存水平需要在 更长的审核周期 内吸收需求。
在左侧,我们有更多的商店交付(每次发货数量较少),但持续时间相同。

R = 15 天(左)/ R = 4 天(右)—(图片来源:作者)
这将影响你的仓库和运输操作的效率。
我们能否估算这两种不同方法对二氧化碳排放的影响?
你可以模拟这些,并帮助你的同事估算她的库存规则对纸箱消耗和 CO2 排放的影响。
对纸箱使用的影响 商品以包含单独挑选单位的纸箱送达商店。

处理单元 — (作者图片)
如果商店订购 5 个单位的参考 XXX,操作员将
-
打开一个 20 单位的箱子,取出 5 个单位;
-
拿一个新箱子,放入这 5 个单位;
-
用其他商店订购的商品填充纸箱;
我们必须使用额外的纸箱材料来创建这些包含不同商品的混合纸箱(而不是运输整箱商品)。
我们需要准备多少额外的混合纸箱?
你可以使用以下公式计算混合纸箱的总数。

公式 — (作者图片)
这些箱子(或混合纸箱)将需要额外的包装材料,这将影响你的碳足迹。
在较高的补货频率下,每次补货的数量减少,且这种情况可能会更频繁地发生。
我们能否估算对运输效率的影响?
对运输排放的影响 由于它是 CO2 排放的主要来源,你还应该估算使用卡车数量及其装载率的影响。
审核期影响某一时期内的配送次数。

配送频率的影响 — (作者图片)
例如,配送频率加倍将会
-
乘以相同补货数量的配送次数;
-
减少每次补货的数量,并可能增加卡车的空间利用率。
我们如何将这些洞察转化为预计的 CO2 排放增加量?
在下一节中,我们将把这些运营洞察转化为一个模拟模型,以选择最佳库存规则。
你听说过可持续供应链优化吗?
模拟绿色库存管理场景
基于假设构建模型
我将以一个补货十家时尚零售店的物流网络为例(上海,中国)。

问题的物流数据 — (作者图片)
在此模拟中,我将考虑
-
90 天内10 家商店的销售数据,商店位于仓库周围
-
740 种独特商品(SKU)在这些商店中销售
-
主数据提供的每箱单位数量
-
每个混合纸箱 12 个单位
-
1 天交货期 从订单创建到商店配送之间的时间
评估特定库存管理规则时使用哪些指标?
使用这些参数和上一节的公式,我可以估算卡车的装载率以及需要额外纸箱的数量。
如何将其转化为环境影响?
根据混合纸箱的尺寸和厚度,我们可以估算每个纸箱所需的额外材料量:0.3 公斤/纸箱。

排放数据 — (图片来源:作者)
CO2 排放量是使用NTM(运输措施网络)方法估算的,该方法使用距离和排放因子。

运输 CO2 排放公式 — (图片来源:作者)
💡 洞察
-
NTM 方法已根据我们的实际问题进行了调整,因为它考虑了卡车装载对整体排放的影响。
-
我们还可以考虑填充材料(在你的混合纸箱中)和托盘上的包装膜,以改进模型。
我在这篇文章中分享了如何通过数据分析估算运输 CO2 排放的详细教程。
构建关于配送网络 CO2 排放的 ESG 报告的 4 个步骤。了解如何衡量和减少你的碳排放……
towardsdatascience.com
现在你已经开发了一个模型来估算影响,可以与库存经理一起定义几个情景。
不同复审周期的情景
考虑到商店的存储容量和运输资源,她估计最大复审周期可能为10 天。
因此,你决定模拟整体排放和纸箱材料的使用,复审周期从2 天到 10 天不等。

模拟模型 — (图片来源:作者)
对于每种情景,模型可以提供
-
准备的混合纸箱百分比(%)
-
用于配送商店的部分装载卡车的百分比(%)
-
使用的纸箱材料总量(kg)
-
道路运输的总 CO2 排放量(kg CO2eq)
我添加了两个初步输出,以提供你配送网络效率的整体视图,可以与物流团队共享。
在下一部分,我将分享我使用90 天销售交易数据进行的简单模拟结果。
绿色库存管理模拟结果
运输排放
初步假设是,较低的配送频率将提高卡车的装载率并减少排放。
模拟确认了这一假设。

按类型划分的出行次数(左轴)/ 总 CO2 排放量(右轴) — (图片来源:作者)
💡 洞察
-
最少出行次数出现在复审周期为 7 天的情况下。
-
Scenario 1(R=2)与 Scenario 6(R=7)之间,排放减少了 27%。
-
Scenario 1(R=2)与 Scenario 6(R=7)之间,出行次数减少了 51%。
正如我们所见,当 R>7 时,排放量增加,最佳规则与需求的每周季节性相匹配。
纸箱材料使用
在较低的订单频率下,我们假设每个订单的数量增加将减少按件拣选的百分比。
仿真结果验证了这一假设。

准备的纸箱数量(左轴)/ 混合纸箱的材料使用量(右轴) — (图片来自作者)
💡 洞察
-
由于仿真期间的总需求在所有情景中相同,因此准备的纸箱总数保持稳定。
-
混合纸箱的比例从27%(情景 1:R=2 天)下降到9%(情景 9:R=10 天)
-
-65% 纸箱使用量在情景 1(R=2)和情景 9(R=10)之间
趋势显示,R=7 天是最大化足迹减少的最佳频率。
积极效果不仅包括纸箱使用量和排放减少。
生产力与社会影响
在配送中心(DC),在拣货路线中,从一个位置到另一个位置的步行时间可能占到操作员工作时间的60%到 70%。
这个生产力是通过每小时拣选的纸箱数量来衡量的;如果操作员达到了目标,他们会获得额外的奖金。
每个位置拣选的纸箱数量是影响操作员生产力的一个重要参数。

三个停靠点的拣货路线示例 — (图片来自作者)
例如,在目标为 200 箱/小时的情况下,如果操作员每站点拣选 4 箱而非 2 箱,他将付出更少的努力。
如果你想深入探索这个话题,我发布了一系列文章,展示了如何通过减少仓库中的步行时间来提高效率。
设计一个仿真模型,以估算几种单拣选员路径问题策略对拣货的影响……
towardsdatascience.com
复审周期对每个订单行(停靠点)的纸箱数量有什么影响?

三个停靠点的拣货路线示例 — (图片来自作者)
正如预期的那样,减少交货频率增加了每行拣选的纸箱数量。
💡 洞察
-
+65% 每个补货订单行的整箱数
-
操作员将在相同的步行距离下准备多 2.07 箱。
这将减少人力资源变动成本,并帮助操作员以更少的体力达到目标。
缺点:平均库存水平
由于没有什么是完美的,增加复审周期也有一些缺点。
当补货频率较低时,你需要在商店中增加库存覆盖量。

每个情景的平均日库存 — (作者提供的图片)
💡 洞察
- +108% 是在情景 9(R = 10)与情景 1(R = 1)之间,所有商店的平均库存。
这意味着你将需要在商店中为存储腾出额外空间。
决策:找到正确的平衡
你可以与负责分销网络绿色转型的项目团队分享这些结果。

绿色转型项目团队 — (作者提供的图片)
讨论可以通过可持续性来激活,这将设定目标。
商店经理和物流团队之间需要达成妥协。
-
商店经理需要较高的补货频率,以最大限度地减少存储空间并避免缺货。
-
物流团队会提倡较低的补货频率,以优化分配流动。
你的角色是提供数据和见解,帮助做出正确决策,并在实施过程中支持团队。
结论
需要采取一种平衡的方法
就像供应链管理中的一切一样,这是一个平衡的问题。
你可以根据你商店位置的每平方米成本来分配更多或更少的存储空间。

利益与成本之间的平衡 — (作者提供的图片)
然而,这笔额外的成本应该考虑在内,以便评估仓储和运输的潜在节省。
改进模拟
如果你需要额外的节省以说服管理层,你可以通过增加额外的节省计算来改进模型。
-
仓库中的货物处理流程:拣货位置补货、卡车装载、纸箱包装;
-
商店接收:卡车卸货和库存管理;
-
包装材料:填充材料、标签、托盘包装;
大多数可持续性举措都涉及对现代机械、电动卡车或可再生能源的重投资。

可持续性的供应链分析 — (作者提供的图片)
在这里,你可以使用你的模型,通过与这些昂贵的举措相比,比较每投入一欧元所减少的排放量,从而推广你的方法。
这个想法是专注于过程优化(由数据科学支持),而不是依赖大量投资来达成减排目标。
欲了解更多由数据分析支持的可持续发展案例,请查阅我几个月前发布的这篇文章。
了解数据分析如何通过现实世界的案例和实用技巧来改善供应链可持续性。
towardsdatascience.com
关于我
让我们在Linkedin和Twitter上建立联系。我是一名供应链工程师,利用数据分析改善物流运营并降低成本。
若需有关分析和可持续供应链转型的咨询或建议,请通过Logigreen Consulting与我联系。
如果你对数据分析和供应链感兴趣,请查看我的网站。
这是一个专注于数据科学、个人生产力、自动化、运筹学和可持续发展等领域的技术博客……
samirsaci.com](https://samirsaci.com/?source=post_page-----e7ddfd97696f--------------------------------)
📘 你的供应链分析完整指南:分析备忘单
💌 免费订阅,直接将新文章发送到你的收件箱:时事通讯
参考资料
数据科学与可持续性 — 模拟循环经济
利用数据科学模拟循环模式对快时尚零售商 CO2 排放和水资源使用的影响。
·发表于Towards Data Science ·阅读时间 11 分钟·2024 年 3 月 28 日
--

租赁模式 — (作者提供的图片)
循环经济是一种经济模式,旨在最小化浪费并最大化资源效率。
这涉及到设计专注于长寿命、再利用和回收的产品和流程。
为什么不租用你的衣服,而是购买它呢?!
一些时尚零售商已实施订阅模式。

循环租赁模式 — (作者提供的图片)
客户支付定期费用,以在特定期限内使用某个产品或服务。
这种循环经济模式的可持续性如何?
目标是减少产品生命周期中的环境影响。

产品生命周期 — (作者提供的图片)
你能否利用数据科学来估算一个实验性租赁模式的减排效果?
作为供应链部门的数据科学家,你可以建立模拟模型来评估这些举措的有效性。
本文将估算实施一个包含400 个项目的租赁模式对时尚零售商的环境影响。
快时尚的循环租赁模式
支持供应链的脱碳
你是一个国际服装集团供应链部门的数据科学经理,该集团在全球拥有门店。
为了支持联合国可持续发展目标,该公司承诺减少其环境足迹。

可持续发展路线图项目团队 — (图片由作者提供)
因此,您的可持续发展部门的同事们已经准备了一份涉及多个部门的路线图,旨在到 2030 年减少碳足迹。
在这些举措中,已决定在10 家店铺中尝试实施一个 circular economy 模型。
如果您不熟悉循环经济的概念,可以阅读这篇文章:
如何利用数据科学支持快速时尚零售商实施循环经济?
[towardsdatascience.com
这些地点将向客户提供租赁订阅模型,范围限于 400 个商品。
在实施此附加服务之前,物流和可持续发展团队要求您提供支持,以估算我们可以达到的排放削减量。
操作假设
店铺的库存由分销规划员通过ERP系统进行管理。
-
店铺由中央仓库补货
-
工厂为中央仓库提供补货

分销网络 — (图片由作者提供)
对于本次仿真,我将使用为关于绿色库存管理文章设计的模型,并考虑以下假设:
-
365 天的销售交易,涵盖10 个地点
-
3,300 个活跃的 SKU,其中400 个 SKU包含在循环经济模型中
-
库存定期审查政策规则:2 天
这意味着你的店铺每两天都会从中央仓库补货。

物流参数 — (图片由作者提供)
我们将添加与循环模型相关的假设
-
循环商品从仓库的交货周期:2 天
-
仓库清洁和检查:1 天
-
返回仓库交货周期:2 天
当商品在租赁期结束后返回时,需花费两天时间将其送回仓库。

循环过程 — (图片由作者提供)
还需要一天时间来检查并清洁商品。
最终,商品将在额外的两天后返回到店铺。
现在我们已经引入了假设,可以专注于仿真。
循环模型的仿真模型
我们循环模型的主要参数将是租赁周期。
为了最大化减少排放,最佳的租赁周期是多少?
目标是测试多个租赁周期,观察其对 CO2 排放和水使用量减少的影响。
库存管理模型
与标准的线性模型不同,我们的库存将包含两种类型的物品。
如果客户租赁一件裙子,她可能
-
租赁一件前客户退还的裙子(经过清洁处理)。
-
租赁一件来自工厂的新裙子。
我将应用先进先出(FIFO)原则,协调这些流向每个店铺订单。

循环物品上的 FIFO 原则 — (图片由作者提供)
以上面的例子为例,
-
经过清洁和检查后,这四个退还的物品可供订购。
-
店铺 2 发送了第一个补货订单,数量为 3 个单位。
-
店铺 1 随后发送了一个补货订单,数量为 1 个单位。
因为店铺 2首先下单,所以到达仓库的前三个单位将发货到那里。
如果退货商品库存过低,订单将使用新商品完成。
这些额外的参数已在模拟模型中添加,基于我在以下文章中展示的例子。
模拟店铺配送频率对时尚零售商 CO2 排放的影响
towardsdatascience.com
使用生命周期评估估算节省
为了评估你的循环经济表现,我们聚焦于
-
供应链总 CO2e 排放量(千克 CO2e)
-
生产和交付商品到店铺所使用的水量(L)

线性模型(上)与循环模型(下)—(图片由作者提供)
在上面的例子中,我们将从购买 5 次该商品转换为租赁 5 次该商品。
循环模型的足迹包括
-
一个完整的生命周期 从原材料提取到店铺交付。
-
四次退货周期,包括逆向物流和清洁过程。
然后我们可以使用以下公式估算节省,

CO2 排放(线性与循环)—(图片由作者提供)
这种方法背后的主要假设是,物品的重复使用百分比将驱动节省。
如果你需要更多关于生命周期评估的信息,
了解生命周期评估如何帮助企业评估产品在整个生命周期中的环境影响…
towardsdatascience.com
这些公式应与每个项目在分析范围内的生命周期评估指标配对使用。

假设 — (图片来自作者)
-
排放量和水资源数据来自主数据。
-
额外的影响通过上述参数进行估算。
需要定义的另一个参数是租赁时长。
模拟场景
在与商品和物流团队讨论后,您已选择了2、7、14 和 28 天作为潜在的租赁时长。
您将使用实际的销售交易来模拟每个场景的正向和反向流动。
现在,让我们看看这四种不同租赁周期的结果。
你能猜到哪个时期能够提供最高的排放(和水资源使用)减少吗?
💡 在 Medium 上关注我,获取更多关于 🏭 供应链分析、🌳 可持续发展和 🕜 生产力相关的文章。
7 天租赁周期的模拟
既然我们已经在正确的假设下构建了模型,我们可以开始探索结果,假设租赁周期为一周。
循环百分比
新物品的使用百分比是多少?

店铺 2 每天的循环物品百分比 — (图片来自作者)
💡 见解
-
在前 12 天,已归还物品的库存为零,因此店铺仅使用新物品进行租赁。
-
当出现流量高峰时,例如第 16 天,已归还物品的累计库存无法满足需求。
循环百分比(%)是租赁交易中已归还物品的比例。
这是影响循环模型环境表现的重要参数。
在前 12 天,由于我们使用新物品,租赁模型的碳足迹最高。

每日归还物品数量 — (图片来自作者)
这可以通过查看归还物品的数量轻松解释。
确实,我们可以看到第一批租赁物品是在第 8 天被归还的。
经过5 天的归还流程(取货、清洁和店铺发货)后,这些物品将在第 13 天进行新销售。

从第 13 天开始,循环物品的百分比逐渐增加 — (图片来自作者)
从这一天起,我们实现了租赁物品和归还物品的平衡分布,能够获得足够的库存,以便重复使用超过 75%的物品。
每个物品平均租赁多少次?
这个c图展示了物品在模拟期间按租赁周期数量的分布。

六个月内的循环次数 — (图片来自作者)
例如,9.8%的物品已被使用了 10 次。
💡 见解
-
110,458 个独特物品被用来完成951,856 笔租赁交易 每个物品平均租赁 8.61 次循环。
-
一些物品可以达到14 次循环。
-
库存中有一部分物品仅使用了一次。
每件商品的环境影响是什么?

10 个 SKU 的 CO2 排放量 — (图片来源:作者)
让我们以一件外套租给35 名客户为例,使用的仅是10 件独特的外套。
我们将线性模型的排放量(绿色的co2_linear)定义为如果这些客户购买了 35 件外套时的总足迹。
循环模型的排放量(橙色的co2_circ)仅包括10 件独特外套的生产和退货管理的物流。
排放减少与租赁周期数之间是否存在关联?
需求波动性的影响是什么?
正如预期的那样,CO2e 减排量与周期数呈线性关系。

CO2 排放减少 = f(周期)— (图片来源:作者)
什么因素影响了重复使用商品的百分比?
因此,我们希望实现100%的租赁交易为重复使用的商品,并限制新商品购买的数量。
什么因素影响了重复使用商品交易的百分比?
当需求高度波动时,退货商品的库存很快耗尽。因此,你需要购买新生产的商品。

高变动需求 SKU 示例 — (图片来源:作者)
在上述例子中,需求分布高度偏斜。
-
该参考产品的总需求量的 60%发生在第 105 天的高峰期。
-
因此,循环率(通过重复使用商品完成的销售交易百分比)仅为 40%。
如果需求是稳定的,会怎样呢?
以下柱状图显示了 SKU 服装 1018 的销售分布;这款高销量商品有着稳定的分布。

低变动需求 SKU 示例 — (图片来源:作者)
-
除了前几天,需求分布为建立退货商品的库存提供了足够的灵活性。
-
因此,我们可以实现89%的销售交易为重复使用的产品。
通过这两个例子,你开始理解需求波动性与循环系数之间的关系。
引入变异系数
让我介绍一下变异系数 CV:

变异系数 — (图片来源:作者)
当变异系数 CV > 1 时,需求分布可以被视为不稳定。
400 件在范围内的商品的变动性是多少?

(图片来源:作者)
💡 洞察
-
99.9%的CV<1的商品,其循环率销售百分比超过80%
-
然而,一些CV > 1.5的商品,其循环率百分比高于 70%。
从逻辑上讲,我们可以看到每个租赁项目的排放减少影响,

每单位排放 = f(变异系数)— (图片来源:作者)
💡 洞察
- CV<1的物品中有 100%具有高于30 kg CO2e 每单位租赁的排放减少。
由于我们无法控制需求波动,让我们通过调整租赁期来探索如何增加排放减少量。
不同租赁期的模拟
在前一部分中,我们模拟了7 天的租赁期。
现在我们可以在2 天到28 天之间运行模型。

每种情境下的 CO2 排放减少量 — (图片来源:作者)
💡 见解
-
当租赁期增加时,减少的排放水平会下降。
-
最优租赁期为 2 天,CO2 排放减少量达到 35 千吨 CO2e。
-
延长租赁期会减少使用循环物品的比例。
通过查看 2 天租赁期内物品的循环利用率,可以验证这一点,

需求变动的影响(租赁期 = 2 天) — (图片来源:作者)
💡 见解
- 99%的CV < 2物品中,有超过 80%的租赁交易是通过重复使用的物品完成的(与 7 天租赁期时的CV<1相比)。
这意味着短期租赁期在应对需求波动时提供了更多灵活性。
不出所料,我们可以看到水资源节约的趋势也与此相同,

水资源节约 — (图片来源:作者)
随着租赁期的增加,物品的可租赁性下降。
因此,你会失去库存灵活性来应对需求波动。

汇总结果 — (图片来源:作者)
然而,你的循环模型在 28 天租赁期下的表现依然令人印象深刻(60%的 CO2 减少,相比于线性模型)。
这张表格有助于推动讨论,并在商业需求与可持续性目标之间找到平衡。
结论
即使在最坏情况下,28 天的租赁期也能实现60%的 CO2 减少和-74%的水资源使用减少。
这意味着由于退货产品的物流产生的额外排放,并没有抵消通过重新使用产品所带来的节省。
然而,这假设商店和物流操作能够管理 400 个物品的租赁流程。
需要额外的参数来进行完整评估,
-
需要额外的员工和系统来管理退货流程
-
需要额外的包装或处理材料吗?
我们能否减少上游流程的环境影响?
供应链网络优化
为了进一步减少环境影响,你可以探索减少上游流程(采购、生产和仓库交付)环境影响的解决方案。
哪种工厂网络能够最小化 CO2 排放、水资源使用或生产成本?

供应链网络设计问题 — (图片来源:作者)
可持续供应链优化是一种集成经济约束和环境指标的网络设计方法。

可持续供应链优化 [试用: 应用程序] — (图片由作者提供)
在上一篇文章中,我介绍了一款支持供应商选择的网络应用程序,旨在最小化供应链的环境影响。
该解决方案可以与循环模式结合,最大化减少足迹并优化资源使用。
🚀 现在试用应用程序!

访问应用程序试用一下!— [应用程序]
关于我
让我们在Linkedin和Twitter上保持联系。我是一名供应链工程师,利用数据分析改善物流运营并降低成本。
如果您需要供应链转型的咨询或建议,请通过Logigreen Consulting与我联系。
如果您对数据分析和供应链感兴趣,请访问我的网站。
关注数据科学、个人生产力、自动化、运筹学和可持续性等领域的技术博客…
💡 在 Medium 上关注我,获取更多关于🏭供应链分析、🌳可持续性和🕜生产力的相关文章。
参考文献
-
“美国的干洗和洗衣服务”报告,由 IBISWorld 发布
-
“服装清洁和洗衣服务的碳足迹:综述” 由 T. Randell、M. Sohail 和 M. Reynolds 编写(《清洁生产杂志》,2016 年)
可持续采购的数据科学
如何利用数据科学,结合可持续性指标和社会指标,选择最佳供应商?
·发表于Towards Data Science ·阅读时间:10 分钟·2024 年 3 月 7 日
--

可持续采购的数据科学 — (图源:Samir Saci)
可持续采购是将社会、伦理和环境绩效因素整合到供应商选择过程中的方法。
这包括根据可持续性标准对供应商进行评估和评定,例如健康与安全、环境影响或劳动权利。

根据环境因素评估供应商 — (图源:作者)
目标是最小化对环境的负面影响,并最大化对社会的正面影响。
作为数据科学经理,你能开发工具来简化可持续采购的过程吗?
供应链网络设计工具会自动选择最佳的供应商,这些供应商能够最小化成本,满足市场需求,并遵守由可持续发展团队定义的约束条件。
本文将探讨如何在你的组织中运用数据科学来支持可持续采购的实施。
可持续采购问题陈述
场景:T 恤供应商选择
让我们以之前文章中关于生命周期评估的示例为例。
你是一个国际服装集团的数据科学经理,该集团在全球范围内有着众多门店。
公司从位于亚洲的供应商处采购服装、包袋和配饰。

快速时尚公司循环经济 — (图源:作者)
门店从本地仓库进行配送,并由供应商直接补货。
您能否自动选择在遵守环境和社会约束的同时最小化成本的供应商?
采购团队已决定在有限的T 恤供应商范围内实施可持续采购。

从原材料到仓库交付的 T 恤生命周期评估 — (图片由作者提供)
他们希望审查他们的供应商选择过程,以支持公司可持续发展的路线图。
然而,他们面临着若干障碍
-
供应商数据收集和存储
没有中央信息源,因为数据存储在整个组织的非结构化 Excel 文件中。
-
基于许多参数的复杂决策 他们目前的 Excel 计分卡系统在指标数量增加时显示出局限性。
因此,他们请求您的帮助,部署一个产品来监控供应商的 KPI 并自动化选择。
让我们看看数据分析如何解决这个问题。
可持续采购指标
本文将使用一个简单的例子,涵盖有限范围的指标。
-
环境指标包括碳排放、能源消耗、废物产生和水资源使用
-
社会指标, 包括劳工和人权
我们可以快速回顾它们,以理解它们的定义及如何衡量。
碳排放 作为主要指标,将推动您的可持续发展路线图,供应链的碳足迹是其中的重要部分。
这可以通过计算温室气体(如二氧化碳、甲烷和一氧化二氮)的总排放量来衡量。
💡 我们应该考虑什么?
-
用于 T 恤的棉花种植过程中排放的污染物
-
T 恤生产中的电力生成和使用的化学品的排放
-
运输从工厂到中央分销中心的成品(T 恤)海运
第两个指标应该由供应商提供,但您的团队可以计算第三个指标。
公式非常简单
使用运输排放因子的公式 — (图片由作者提供)
With,
**E_CO2**: emissions in kilograms of CO2 equivalent (kgCO2eq)
**W_goods**: weight of the goods (Ton)
**D**: distance from your warehouse to the final destination(km)
**F_mode**: emissions factor for each transportation mode (kgCO2eq/t.km)
数据源包括
-
运输管理系统数据库,包括发货信息(日期、来源、目的地、运输数量)
-
主数据用于将运输数量转换为重量
如果您需要更多信息,我在这篇文章中详细说明了这一过程
4 个步骤来构建分销网络的 ESG 报告,了解如何衡量和减少碳排放……
towardsdatascience.com
能源消耗 另一个指标是供应商为生产和交付产品所消耗的能源量。
它可以用 每生产单位的焦耳消耗量 来表示(生命周期评估方法中的功能单位)。
💡 我们应该考虑什么?
-
棉花种植的能源消耗 (kJ)
-
纺纱、织布和染色的能源消耗 (kJ)
-
运输的能源消耗 (kJ)
采购团队应该收集这个指标。
水资源使用 由于水资源日益稀缺,新的法规正在推动公司重新设计其水消耗减少的过程。
通过这个指标,你可以关注那些不影响当地水资源供应的供应商。
💡 我们应该考虑什么?
-
棉花种植的水消耗 (L/单位)
-
纺纱、织布和染色的水消耗 (L/单位)
废物生成 测量生产过程中产生的废物量可以帮助识别采纳更可持续生产方法的领域。
💡 我们应该考虑什么?
- 生产过程中的固体废物生成 (kg/单位)
社会指标 这些指标评估供应商在社会可持续性方面的表现。
你可以进行自我审计或使用第三方评估。
💡 如何衡量它? 你的采购团队为下方记分卡中的每个标准分配一个权重。

记分卡示例 — (作者提供的图片)
审计员计算与每个标准相关联的得分,以表示供应商的表现。
总分是各项评分的加权平均值,用以提供供应商的整体评估。
我们如何组织数据收集和存储?
你可以为你的采购团队提供的首要支持是使用数据分析工具简化数据收集和存储过程。
数据湖可以是结构化和非结构化数据(数据库、Excel 文件等)的中央来源。
用于报告的数据湖 — (作者提供的图片)
你可以实施自动化管道来提取、处理和分析这些数据,以构建报告并提供洞察。
这可以促使实施数据产品,用于报告和优化模型,我将在下一节中介绍这些模型。
测量你价值链上的环境足迹 — (Samir Saci 提供的图片)
本文关于生命周期评估提供了更多关于自动化此过程的最佳实践和方法的信息。
了解生命周期评估如何帮助企业评估产品在整个生命周期中的环境影响……
towardsdatascience.com
采购网络设计
现在我们已经收集了关于供应商的信息并进行评分审核,我们可以利用这些数据来推动决策。
采购团队的主要障碍是问题的复杂性,考虑到不同的指标和供应商数量。

供应商选择过程 —(图片由作者提供)
他们使用有限自动化的 Excel 文件,这限制了他们的处理能力。
因此,想法是使用先进的分析方法自动设计采购和物流网络,以便为你的商店提供服务。

可持续采购问题示例 —(图片由作者提供)
我将使用一个包含虚拟数据的示例来理解如何实施一种数据驱动的方法,考虑到环境因素来选择供应商。
问题陈述
你希望重新定义你快时尚零售公司未来五年的供应链网络。

你在全球五个不同的市场(美国、德国、日本、印度和巴西)都有门店。
在每个市场中,你可以选择一个供应商来生产你的 T 恤,并选择从海外采购。

供应商网络设计 —(图片由作者提供)
你希望选择合适的供应商组合,以最小化生产和交付成本。
-
每个市场的需求(每月单位数)
-
每个国家生产的固定和变动成本($)
-
从工厂到市场的海运运输成本(每单位$)

生产和交付的总成本 —(图片由作者提供)
你的海外供应商较便宜,但你必须支付较高的运费,且运输过程中会产生更多的 CO2 排放。

按生产国划分的固定成本与变动成本 —(图片由作者提供)
此外,可持续发展团队已经实施了每件 T 恤生产的最大环境影响限制,包括水使用、CO2 排放、能源使用和废物产生。
采购团队需要线性编程支持来管理这些复杂的权衡,并决定选择哪些供应商。
让我用下面的例子来说明我的观点,在这个例子中,我将逐步加入环境限制,以查看其对足迹的影响。
初步解决方案:最小化成本
如果你定义目标函数以最小化成本而不加其他约束,你可以轻松猜测这将会最大化印度和巴西的生产量。

商品从生产到市场流动 —— (图像由作者提供)
💡 见解
- 在这种情况下,海外采购增加的运费由印度和巴西低廉的生产成本来抵消。

最低成本全球化解决方案 —— (图像由作者提供)
如果我们想添加可持续性约束会怎样?
实施可持续采购约束
随着你的公司实施可持续发展路线图,你必须添加额外的约束,限制每件 T 恤生产和交付的环境影响。
因此,你的采购团队已联系了你主要市场(美国、日本)的当地供应商。
你的采购网络会受到什么影响?
测试 1:能源和水资源节约约束
我们可以从每单位生产的平均能源和水消耗量的约束开始。
-
平均水消耗低于3000 L/单位
-
平均能源使用量低于685 MJ/单位

能源和水消耗 —— (图像由作者提供)
你在德国和日本的供应商已大量投资于现代化设备,以减少其运营对环境的影响。

测试 1 解决方案 —— (图像由作者提供)
💡 见解
-
该模型将生产从巴西流转到德国,以遵守附加约束。
-
该模型决定继续在印度生产,因为成本具有竞争力。
-
德国和日本现在正在本地化他们的生产。
如果我们添加 CO2 排放约束会怎样?
测试 2:添加 CO2 排放约束
运输占你供应链 CO2 足迹的相当一部分。

生产和运输的 CO2 排放 —— (图像由作者提供)
因此,你希望通过限制
-
每单位千克 CO2e/单位的种植和生产
-
海运运输千克 CO2e/单位
-
总排放量必须低于110 千克 CO2e/单位

测试 2 解决方案 —— (图像由作者提供)
💡 见解
-
印度首次出现最低的生产输出。
-
巴西是保持低生产成本同时限制 CO2 排放的解决方案。
-
美国供应商在可持续生产设施上的投资不足,无法保留用于国内市场供应。
通过这个简单的练习,你可以测试几种情景,以确定这些解决方案加速决策过程的潜力。
如果你想自己尝试,我已经在 VIKTOR 平台上部署了这个解决方案的 Web 应用程序。
可持续供应链优化应用程序 — (作者图片)
有关此解决方案的更多细节,请查看介绍该应用程序及其功能的文章。
帮助您的组织将可持续采购与供应链优化相结合,以减少成本和环境影响…
towardsdatascience.com
💡 在 Medium 上关注我,获取更多与🏭 供应链分析、🌳 可持续性和🕜 生产力相关的文章。
结论
在实施绿色转型时,公司面临着许多与变革管理和复杂决策过程相关的挑战。
作为数据分析专业人士,您可以通过促进数据收集并使用优化模型部分自动化决策过程来支持这一转型。
采购团队可以通过遵循可持续性团队的指导,轻松地在供应商选择过程中添加约束条件。
不同目标函数的情景 — (作者图片)
通过展示每种情景的财务影响,你可以通过突显可持续性与成本效益之间的平衡,来促进决策过程。
结论 — (作者图片)
这些结果可以激发战略性讨论,找到可持续采购与成本降低之间的最佳平衡。
ESG 支柱展示 — (作者图片)
随着利益相关者日益要求企业承担社会责任(CSR),以 ESG 为主导的转型已成为公司长期战略的关键部分。
报告类别示例 — (作者图片)
本文介绍的方法论(和工具)可以帮助您的组织减少数千吨二氧化碳排放,以实现 2030 年目标并提升您的 ESG 评分。
类似的收集数据和基于数据的决策方法也可以应用于这一特定的非财务报告。
我已发布这篇文章,以便提供更多有关数据分析如何支持 ESG 报告的信息。
利用数据分析为公司的环境、社会和治理(ESG)报告提供全面有效的支持
towardsdatascience.com
关于我
让我们在Linkedin和Twitter上联系;我是一名供应链工程师,利用数据分析改善物流操作并降低成本。
如需有关分析和可持续供应链转型的咨询或建议,欢迎通过Logigreen Consulting与我联系。
如果你对数据分析和供应链感兴趣,欢迎访问我的网站。
一篇聚焦于数据科学、个人生产力、自动化、运筹学和可持续发展的技术博客…
💡 在 Medium 上关注我,了解更多关于🏭供应链分析、🌳可持续发展和🕜生产力的文章。
数据科学在价值链管理中的应用
你如何利用数据科学优化运营并提升盈利能力?
·发布于 Towards Data Science ·阅读时间:12 分钟·2024 年 5 月 15 日
--

价值链管理 — (图像来源:作者)
价值链管理 (VCM) 是指组织那些为商品或服务增加价值的活动,以在市场上获得竞争优势。
这种方法帮助组织有效响应市场趋势并提高效率,以提升盈利能力。

价值链与供应链 — (图像来源:作者)
作为一名数据科学家和分析经理,您如何对公司价值链产生影响?
在本文中,我们将快速探讨价值链管理的基本组成部分。
接下来,我们将探讨四个数据科学应用示例,以支持战略性主要活动。
Summary
**I. The Pillars of Value Chain Management**
**1\. Activities to create value**
Understanding the fundamental components to create value
**2\. What are the primary activities?**
Core functions directly involved in product creation, marketing, and delivery.
**3\. What are the support activities?**
Essential functions that indirectly contribute to value creation
**4\. Data Science to Support Primary Activities**
Discussing how data science tools and techniques can be employed to
optimize primary activities and enhance overall value chain management.
**II. Inbound Logistics: Supply of Raw Materials**
**1\. Supplier Mapping with the Graph Theory**
Analyze supplier networks for risk assessment and optimization.
**2\. Sustainable Sourcing Network Optimization**
Select suppliers based on economic and sustainable criteria
**III. Operations: From Raw Materials to Finished Goods**
**1\. Production Planning Optimization with Wagner-Whitin Algorithm**
Optimize production planning, balancing setup costs and inventory management.
**2\. How to measure the impact of your solution?**
**IV. Outbound Logistics: Distribute your Final Products**
**1\. Automate a Supply Chain Control Tower**
Automate monitoring and improve the efficiency of outbound distribution.
**2\. How can we improve the performance using these diagnostics?**
**V. Conclusion**
价值链管理的两大支柱
企业不断寻求通过提高效率和最大化盈利能力来获得竞争优势的方法。
客户:Samir,我们希望将物流成本降低 20%。我们希望你重新设计整个分销网络。
这是我作为供应链工程师或数据科学家进行的大多数项目的共性。
因此,本博客中发布的大多数文章都专注于使用数据分析来优化供应链流程,以降低成本。
时尚零售商:我们如何才能以最低成本生产和交付产品?
价值链管理 (VCM) 是一种战略性方法,旨在简化业务流程的每个阶段,以优化绩效。
这涵盖了从生产到交付的所有环节,以在最小化成本的同时创造最大客户价值。
创造价值的活动
价值链框架最早由迈克尔·波特在他的著作《竞争优势:创造和维持卓越表现》中提出。
这种方式通过将任何业务拆解为一系列互联的活动,这些活动有助于创造和向客户传递价值,从而彻底改变了企业对其运营的看法。

价值链的组成部分 — (作者提供的图像)
-
主要活动直接涉及产品或服务的创建、销售、维护和支持。
-
支持活动包括基础设施、技术开发、人力资源管理和采购。
让我们通过一个时尚零售商在亚洲生产 T 恤并在欧洲销售的例子,来探讨它们的定义。
主要活动有哪些?
主要活动包括入境物流、运营、出境物流、营销和服务。

价值链管理中的主要活动 — (作者提供的图像)
这些核心功能涉及创建、生产、营销和向最终客户交付产品或服务。

时尚零售公司供应链运营 — (作者提供的图像)
以我们 T 恤的价值链为例:
-
入境物流包括从供应商处采购棉花并运输到制造设施。
-
运营包括将棉花转化为面料图案,缝合起来以制作 T 恤。
-
出境物流涉及所有包装、储存和将 T 恤送达最终目的地的物流过程。
-
营销和销售专注于推广以促进销售。
-
服务包括售后支持、客户服务及其他附加服务,如定制。
如何支持和协调这些活动?
此外,支持活动在确保这些主要活动顺利进行方面发挥着关键作用。
让我们深入探讨这些支撑整个价值链的关键支持功能。
支持活动有哪些?
支持活动通过提升主要活动的效率和效果,间接地增加了产品或服务的价值。

聚焦支持活动 — (作者提供的图像)
支持活动涵盖基础设施、技术开发、人力资源管理和采购。
作为数字化转型部门的数据科学家,你是支持功能的一部分。
-
公司基础设施包括组织结构、控制系统和行政任务,这些任务确保其价值链的顺利运行。
-
技术开发活动与支持价值链的技术和系统相关。
-
人力资源管理涉及招聘、培训和留住为每个价值链阶段做出贡献的员工。
-
采购涉及为价值链采购和购买所需的输入,从原材料到办公设备。
作为技术开发活动中的关键角色,你能产生什么样的影响?
数据科学支持主要活动
作为新聘的数据科学经理,你希望提出一份路线图,以实施先进的分析工具来支持主要活动。
目标是支持精心挑选的价值链活动,并使你的团队成为公司的一项战略资产。
入境物流:原材料供应
这包括所有与接收、存储和分配生产前输入相关的过程和活动。
在我们的例子中,这些输入可能是原材料,如棉花、染料和制造 T 恤所需的其他物料。

入境物流优化的数据分析解决方案示例 ——(图片由作者提供)
💡 基于你的经验,你提议一组分析工具来应对运营挑战并优化流程。
我们如何使用描述性分析来监控采购表现?
使用图论的供应商映射
你想提出解决方案以支持供应商风险评估。
💾 输入数据:购买订单、供应商信息、工厂/仓库容量和 Excel 文件中的发货记录。
采购团队需要一个工具,可以提供跨供应商网络的可见性,用于执行风险评估、供应商整合和物流网络设计。
❔ 问题陈述:你有一组工厂,这些工厂从供应商那里接收关键组件和原材料。
你如何估算特定供应商故障对整体制造足迹的影响?*
网络图示例 ——(图片由作者提供)
B-45 工厂有多少个关键供应商?
🚀 解决方案:图论是一个数学领域,研究图中作为顶点和边表示的对象之间的关系。
在这个特定的案例中,可以使用图论来
-
可视化所有参与特定项目价值链的供应商
-
可视化所有参与同一价值链的工厂:原材料加工厂和组装零件。
我使用这个理论来分析一家零售公司路线策略。
目标是可视化在同一路线交付的所有商店。
用于运输优化的图论示例 [文章] — (图片来源:作者)
这个例子可以很容易地调整用于审计供应商网络
-
如果供应商正在向工厂 B-45 交付,请创建一个链接
-
创建原材料加工工厂 B-45 与装配线 C-78 之间的链接。
有关图论的更多信息,
使用图论优化零售公司道路运输网络
towardsdatascience.com
什么样的分析可以支持图论?
-
风险评估:有多少项目依赖于特定供应商?
🎯 直观评估该供应商的重要性,并评估其风险。
-
网络设计:特定工厂的供应商位于哪里?🚛 为了减少采购提前期并减少对海运的依赖,你可以推动供应商将工厂搬迁至靠近该工厂的地方。
此可视化提供了足够的透明度,支持采购团队讨论风险评估或供应商优化。
可持续采购网络优化
采购团队要求一个工具,用于选择合适的供应商,以减少进货流的环境足迹。
💾 输入数据:工厂需求(单位/月)、供应商和工厂位置、每个供应商的环境影响(CO2、水等)以及每个供应商的生产成本。
你的同事需要一个工具,能够根据以下条件选择合适的供应商
-
约束条件如供应量 ≥ 需求量、供应商的容量限制和每单位产品的最大排放或水使用量。
-
特定目标:最小化成本、水使用量或 CO2 排放。
❔ 问题声明:我们应该选择哪些供应商以最小化 CO2 排放?
可持续采购问题声明 — (图片来源:作者)
🚀 解决方案:使用 Python 中的线性规划,你可以根据用户选择的目标自动选择合适的供应商。
多目标的不同解决方案 [尝试应用] — (图片来源:作者)
在我部署的应用程序中,用户可以根据经济或可持续标准轻松模拟几种场景,以便于决策。
用户可以可视化四个目标函数的成本影响 [试试这个应用程序]— (图片来自作者)
本文详细介绍了该工具背后的理论。
我们如何使用数据科学来选择最佳供应商,同时考虑可持续性和社会等指标……
[towardsdatascience.com
你的解决方案有什么影响?
部署这些分析解决方案可以帮助你的组织可持续地确保以最低成本供应原材料。
我们现在可以进入第二个活动,即原材料的转化。
运营:从原材料到成品
这是 T 恤的实际生产阶段,通过将从入境物流获得的输入转化为成品。
这一活动包括诸如裁剪、缝纫、染色和印刷等过程。

运营优化的数据分析解决方案示例 — (图片来自作者)
💡 为了吸收原材料价格上涨,制造部门请求你的支持,以减少生产成本。
我们如何使用规范分析来优化生产过程?
使用 Wagner-Whitin 算法优化生产计划
你希望提出一个解决方案来优化生产计划。
生产计划 — (图片来自作者)
💾 输入数据:客户发送 采购订单 ,并指定 交付数量 和 交付时间。
客户订单 — (图片来自作者)
在上面的示例中,客户共享了未来 12 个月要交付的数量。
❔ 问题陈述:如何组织生产批次,以最小化每单位的总生产成本?
对于这个练习,你需要找到在以下方面的平衡:
-
设置成本:每次设置生产线时发生的固定成本
如果你每个月只生产要求的数量,那么库存将会很低,但设置成本将会爆炸式增长。
-
持有成本:每单位每单位时间的存储成本
如果你在第一个月生产全部数量,设置成本将会最小化,但你将会积累过多的库存。
🚀 解决方案:生产规划通过寻找在 最小化库存 与 最大化每次设置生产数量 之间的平衡, 最小化生产总成本 *。
生产规划输出 — (图片来自作者)
Wagner 和 Whitin 开发了一种通过动态规划寻找最优规划的算法,该算法平衡了设置和库存成本。
如果你想了解更多关于理论的内容,这篇文章提供了详细的介绍。
使用 Python 实现 Wagner-Whitin 算法进行生产规划,以最小化生产总成本
towardsdatascience.com
如何衡量你解决方案的影响?
我会尝试使用一个支持特定工厂规划的原型
-
从评估当前的生产规划开始:库存、特定期间的设置次数和成本。
-
运行工具以相同的时间段进行。
-
验证结果与生产规划员,并计算潜在的节省。
你现在可以尝试我几个月前部署的这个原型
生产规划优化应用 [链接] — (图片来自作者)
我们现在可以转向出口物流,将这些成品交付给客户。
出口物流:分发你的最终产品
在分销中心存储成品的情况下,物流操作团队管理流程,准备并将订单交付给客户。
对于我们的时尚零售商,这包括存储、订单履行、运输和整个欧洲的门店配送。

出口物流优化分析解决方案示例 — (图片来自作者)
💡 因为店铺经理抱怨配送交货时间,运营总监要求你提供支持来改进分销流程。
我们如何能够通过诊断分析自动监控分销网络?
自动化供应链控制塔
你与分销规划经理保持联系;她的团队监控所有门店补货订单。
零售公司分销过程 — (图片来自作者)
配送计划员在 ERP 系统中监控每个在商店销售的商品的库存水平(单位)。
当库存水平达到规划人员设置的最小水平时
-
系统自动创建补货订单,包含商品数量和请求的交货日期
-
仓库运营团队准备订单进行发货
-
运输团队组织交付到各个商店
你可以使用哪些指标来监控这个复杂的过程?
参与配送链的不同系统在每个关键步骤记录时间戳。
配送过程中的关键步骤及截止时间——(图示:作者)
从订单创建到店铺交付
-
在过程完成时记录时间戳;
-
完成预计时间是根据服务水平协议计算的;
为了支持她的根本原因分析,她希望你实现一个系统,自动标记中间步骤中的延迟。
已交付货物的示例(顶部:准时,底部:延迟)——(图示:作者)
例如,底部的示例错过了“发货时间”目标,导致了交货延迟。
带有原因代码的延迟——(图示:作者)
使用这些规则,你可以自动创建迟交理由代码以支持诊断。
有关如何实施此解决方案的更多细节,
使用 Python 优化你的供应链网络,通过自动化解决方案跟踪你的货物并评估……
towardsdatascience.com
我们如何通过这些诊断来提高性能?
配送计划经理可以利用这些洞察力推动运营团队:
-
每周按原因代码报告延迟数量;
-
通过原因代码映射,挑战运营团队进行根本原因分析;
-
通过一个整体 KPI 衡量商店的影响,KPI 衡量按时完整交付(OTIF)的订单百分比;
作为数据分析经理,你通过提供足够的洞察力,支持持续改进计划,贡献了交货周期的缩短。
结论
价值链管理是一种方法,帮助企业深入了解其运营,简化流程并为客户创造更大的价值。
作为数据分析经理,您扮演着关键角色。
将数据科学融入价值链管理为企业提供了提升运营效率、降低成本并最终推动盈利的机会。
通过探索从入库物流到出库分销的各种案例,我们展示了数据科学对战略流程的变革性影响。
“绿色转型的四大隐形敌人”示例文章:[链接] — (图片由作者提供)
这可以成为任何重大转型的推动力,影响整个价值链。
随着组织不断拥抱数字化转型,您拥有展示团队潜力的工具,使其成为公司战略资产。
了解如何利用分析克服规模化绿色倡议所面临的挑战,这些挑战阻碍了组织的…
towardsdatascience.com
关于我
让我们在Linkedin和Twitter上联系。我是一名供应链工程师,利用数据分析改善物流运营并降低成本。
如果您需要供应链转型的咨询或建议,请通过Logigreen Consulting与我联系。
如果您对数据分析和供应链感兴趣,请访问我的网站。
这是一个专注于数据科学、个人生产力、自动化、运筹学和可持续发展的技术博客……
samirsaci.com](https://samirsaci.com/?source=post_page-----efb31c780807--------------------------------)
📘 供应链分析完整指南:分析备忘单
💌 免费将最新文章直送到您的邮箱:时事通讯
参考文献
- “竞争优势:创造并维持卓越的表现”, 迈克尔·波特
营销中的数据科学:使用 Python 进行倾向性建模实战
所有你需要的代码,用于预测客户购买你产品的可能性
·发表于Towards Data Science ·8 分钟阅读·2024 年 11 月 23 日
--

图片来自Campaign Creators 在Unsplash
倾向性模型是机器学习在营销中的一种强大应用。这些模型使用客户行为的历史实例来预测未来行为。倾向性模型生成的预测通常用于了解客户在特定时间范围内购买特定产品或接受某项优惠的可能性。
本质上,倾向性模型是机器学习技术中称为分类的示例。使倾向性模型独特的是它们解决的问题陈述,以及如何根据营销需求构建输出。
倾向性模型的输出是一个概率分数,描述了预测的客户行为的可能性。这个分数可以用来创建客户群体,或者对客户进行排名,以提高个性化和新产品或优惠的定向精准度。
在本文中,我将提供一个从头到尾的实用教程,描述如何构建一个准备好供营销团队使用的倾向性模型。
这是我将写的一系列实用 Python 教程中的第一篇…
数据科学并没有那么特别

这张图片是作者使用 MidJourney 并购买了付费许可生成的。
作为数据科学家从与工程师的摩擦中得到的教训
·发表于 Towards Data Science ·阅读时间 9 分钟 ·2024 年 2 月 24 日
--
作为一名具有软件工程背景的数据科学家,同时还从事各种开发工作,我经常发现自己身兼多职,有时也充当工程师和数据科学家之间的桥梁。
如果我说两者之间没有任何摩擦,那我就是在撒谎。
在这篇博客文章中,我将深入探讨作为数据科学家的矛盾心情,以及我对数据科学家的一些反思。读完这篇文章后,您应该能获得以下几点启示:
-
5 个关于数据科学的认识,
-
15 条关于如何打造一个高效数据科学团队的经验
-
这是我一点干巴巴的幽默。
认识 1:数据科学完全是关于假设的
大型科技公司和 YouTube 上那些关于通过数据科学致富的快速视频给数据科学描绘了一个美好的前景。但请考虑以下这些:
数据科学不仅仅是建模或调试超参数与模型架构。
还记得我们在高中学的科学方法吗?这就是科学的精髓……
数据科学与政治的结合
揭示国会动态与网络
·发表于Towards Data Science ·阅读时间:8 分钟·2024 年 9 月 27 日
--

巴西国会 — Gustavo Leighton拍摄的照片,来自Unsplash
谁能理解政治人物?
很少有一群人能像国家国会成员那样难以理解。在几乎每个国家,虚伪的政治人物形象在民众中臭名昭著。后台交易和米色信封常常出现在政治剧集中。与此同时,他们是最需要理解的群体之一,因为他们的行动直接影响着国家的未来。
为了理解国会,我将基于一句流行的格言——“以行动评判人,而非言辞”。因此,我将根据议员的投票历史对国会议员进行比较和分组。通过这种方式,我们可以揭示那些隐晦的模式,理解国会的真正动态。
对于这个项目,我将重点关注我的祖国——巴西的众议院(Camara dos Deputados),但这一方法也可以应用于任何国家。
数据收集
首先,我们需要数据。
我下载了 2023 年至 2024 年 5 月 18 日之间所有投票法案的数据,以及每位国会议员的投票记录。所有数据都可以在巴西国会的开放数据门户找到。然后,我创建了两个不同的 pandas 数据框,一个包含所有投票法案,另一个记录了每个国会议员在每次投票中的投票情况。
votacoes = pd.concat([pd.read_csv('votacoes-2023.csv', header=0, sep=';'), pd.read_csv('votacoes-2024.csv', header=0, sep=';')])
votacoes_votos_dep = pd.concat([pd.read_csv('votacoesVotos-2023.csv', sep=';', quoting=1) , pd.read_csv('votacoesVotos-2024.csv', sep=';', on_bad_lines='warn', quoting=1, encoding='utf-8')])
对于votacoes数据框,我只选择了idOrgao为 180 的条目,这意味着它们是在国会的主会议厅投票的。所以,我们有大部分国会议员的投票数据。然后,我使用votacoes_Ids列表来过滤votacoes_votos_dep数据框。
plen = votacoes[votacoes['idOrgao'] == 180]
votacoes_ids = plen['id'].unique()
votacoes_votos_dep = votacoes_votos_dep[votacoes_votos_dep['idVotacao'].isin(votacoes_ids)]
现在,在votacoes_votos_dep中,每个投票都是一行,包含国会议员的姓名和投票会话 ID,用于识别谁进行了投票以及投票的内容。因此,我创建了一个透视表,使每行代表一位国会议员,每列表示一项投票,将“是”编码为 1,“否”编码为 0,并删除了超过 280 位代表没有投票的投票。
votacoes_votos_dep['voto_numerico'] = votacoes_votos_dep['voto'].map({'Sim': 1, 'Não':0})
votes_pivot = votacoes_votos_dep.pivot_table(index='deputado_nome', columns='idVotacao', values='voto_numerico').dropna(axis=1, thresh=280)
在计算相似度矩阵之前,我将所有剩余的 NAs 填充为 0.5,以避免干扰国会议员的位置设置。最后,我们计算每个代表的向量之间的余弦相似度,并将其存储在数据框中。
from sklearn.metrics.pairwise import cosine_similarity
similarity_matrix = cosine_similarity(votes_pivot)
similarity_df = pd.DataFrame(similarity_matrix, index=votes_pivot.index, columns=votes_pivot.index)

相似度矩阵 - 图片由作者提供
构建国会网络图
现在,使用有关国会议员投票相似度的信息,通过[Networkx](https://networkx.org/)构建网络。每个节点将代表一位成员。
import networkx as nx
names = similarity_df.columns
# Create the graph as before
G = nx.Graph()
for i, name in enumerate(names):
G.add_node(name)
然后,连接两个节点的边代表这两位国会议员投票行为相似度至少为 75%。此外,为了解决一些国会议员与其他高相似度成员的关系问题,我只选择了前 25 位相似度最高的国会议员来添加边。
threshold = 0.75
for i in range(len(similarity_matrix)):
for j in range(i + 1, len(similarity_matrix)):
if similarity_matrix[i][j] > threshold:
# G.add_edge(names[i], names[j], weight=similarity_matrix[i][j])
counter[names[i]].append((names[j], similarity_matrix[i][j]))
for source, target in counter.items():
selected_targets = sorted(target, key=lambda x: x[1], reverse=True)[:26]
for target, weight in selected_targets:
G.add_edge(source, target, weight=weight)
为了可视化网络,你需要决定每个节点在平面上的位置。我决定使用弹簧布局,它将边作为弹簧,将节点拉近,同时尽量分开。添加种子值可以保证结果的可复现性,因为这是一个随机过程。
pos = nx.spring_layout(G, k=0.1, iterations=50, seed=29)
最后,我们使用 Go 图形绘制网络,并根据节点的位置单独添加边和节点。
# Create Edges
edge_x = []
edge_y = []
for edge in G.edges():
x0, y0 = pos[edge[0]]
x1, y1 = pos[edge[1]]
edge_x.extend([x0, x1, None])
edge_y.extend([y0, y1, None])
# Add edges as a scatter plot
edge_trace = go.Scatter(x=edge_x, y=edge_y, line=dict(width=0.5, color='#888'), hoverinfo='none', mode='lines')
# Create Nodes
node_x = []
node_y = []
for node in G.nodes():
x, y = pos[node]
node_x.append(x)
node_y.append(y)
# Add nodes as a scatter plot
node_trace = go.Scatter(x=node_x, y=node_y, mode='markers+text', hoverinfo='text', marker=dict(showscale=True, colorscale='YlGnBu', size=10, color=[], line_width=2))
# Add text to the nodes
node_trace.text = list(G.nodes())
# Create a figure
fig = go.Figure(data=[edge_trace, node_trace],
layout=go.Layout(showlegend=False, hovermode='closest', margin=dict(b=0,l=0,r=0,t=0), xaxis=dict(showgrid=False, zeroline=False, showticklabels=False), yaxis=dict(showgrid=False, zeroline=False, showticklabels=False)))
fig.show()
结果:

图片由作者提供
好的,这是一个不错的开始。可以看到国会议员的不同簇,表明这准确地捕捉到了国会中的政治立场和联盟。但它还是有点乱,很难真正看出其中的关系。
为了改善可视化效果,我设置只有在鼠标悬停在节点上时才显示名称。同时,我根据国会网站上的政党和联盟为节点着色,并根据它们连接的边数调整大小。

图片由作者提供
看起来好多了。我们有三个簇,簇与簇之间有些节点,且每个簇中有一些较大的节点。同时,在每个簇中,某一特定颜色占据了多数。好了,让我们来分析一下。
解释结果
首先,让我解释一下颜色的含义。红色代表当前左翼政府的基础,也就是总统所在党派的国会议员或公共盟友,PT、PSOL、PCdoB 等。蓝色代表反对派,由 PL(前总统所在党派)领导,另有一个右翼党派,NOVO。
绿色和黄色代表巴西政治中的一种现象,称为“中间派”或“大中间”。中间派由未与任何党派结盟的政党组成,这些政党总是与当前政府结盟,并将他们的支持交换为政府职位或国有企业的任命。黄色代表以 UNIAO 为中心的群体,UNIAO 是巴西最大的政党。绿色则是以 MDB 为中心的群体,MDB 是一个历史悠久的党派,曾经掌握大部分“中间派”的控制权。
那么,我们回到图表:

图像来源:作者
第一组似乎主要由红色组成,代表当前政府及其最亲密的盟友。内部的黄色点主要来自 AVNATE,尽管他们公开与 UNIAO 在同一个联盟中,但似乎在政治上更多地倾向于左翼。
网络模型捕捉到的另一个有趣的动态是每个更大群体内部特定党派和意识形态的分组。在第一组中,紧靠第一号基础的下方,有七个节点非常接近。这些节点代表 PSOL 的国会议员,PSOL 是一个激进的左翼党派。有趣的是,即使在左翼阵营内部,他们也被表示为网络中的一个子群体。
第二组似乎主要由我们之前所称的“中间派”(Centrao)构成。和往常一样,他们是政府的基础部分;他们比第三组更接近第一组,我们可以看到预期中的绿色和黄色混合,以及一些蓝色的点。这意味着许多本应在反对派的国会议员投票方式与政府相似。为什么?嗯,PL,现在的“反对派”党,曾经是一个典型的“中间派”党派。因此,历史上的成员仍然像典型的“中间派”那样行事。
值得注意的是,我们在第二组中看到了最大的节点。一个是黄色的,代表政府在众议院的领袖瓦尔德马尔·奥利维拉(Waldemar Oliveira)。他在国会中非常重要,因为大部分议员会根据他的指示投票。其他两个最大的节点也在第二组中,但位于我所称的第二组半(Group 2.5)中。

图像来源:作者
第二组半(Group 2.5)行为背后的原因超出了本文讨论的范围;简单来说,这是一个由标榜为“右翼”的国会议员组成的群体,但他们的行为更像是“中间派”。他们只有在偶尔与右翼投票时才会接近第三组,但每当涉及第二组关心的投票时,他们会脱离并与第二组投票。
最后,第三组与另外两组的分布不同。它是国会中最小的一组,且主要由 PL 党派的代表组成。它呈现出更“分散”的特点,因为其成员和第二组之间有很多空隙,表明他们并非总是一起投票。同时,没有超大的节点,因此没有明确的领导者对整个区块施加影响。这种模式是合理的,并与现实相符,因为反对派在当前国会中未能取得太多成功。
结论
总结来说,使用网络分析巴西国会提供了关于政治联盟和投票行为的宝贵见解。我们对投票模式的可视化展示揭示了三个不同的群体:政府的支持基础、“中间派”(Centrao)以及反对派。该图有效地展示了各党派之间微妙的关系,揭示了公众立场的偏差,并突出了具有影响力的人物。
这种数据驱动的方法增强了我们对政治动态的理解,提供了对立法机构内部复杂互动的清晰视觉呈现。通过利用这样的分析工具,我们可以促进政治过程中的透明度,并推动关于国家政治状况的更有根据的讨论。
现在图表已经准备好,我们可以用它来回答类似的问题:
-
中间派何时与政府或反对派投票?
-
网络的重要性与员工支出之间有联系吗?
-
网络中的对齐程度与地理位置之间有相关性吗?
如果你喜欢这篇文章并想阅读更多关于用数据科学分析政治的见解,可以在这里关注我,不要错过。感谢阅读。
请在评论中告诉我你对这种分析政治动态的方法的看法,以及是否有任何地方做得不对。
数据科学作品集、加速 Python、KANs 及其他 5 月必读文章
·发布于 Towards Data Science ·以 新闻简报 形式发送 ·4 分钟阅读·2024 年 5 月 30 日
--
感到受启发,准备写下你的第一篇 TDS 帖子吗? 我们始终欢迎新作者的投稿。
随着五月即将结束,夏天也即将到来,尤其是对我们北半球的人来说,现在是时候回顾一下过去一个月我们发布的那些突出的文章了:那些在数据科学和机器学习领域广泛的学习者和从业者中产生共鸣的故事。
我们很高兴看到一系列特别多元的文章引起了读者的共鸣。这证明了 TDS 作者带来的多样化兴趣和经验,也表明了对能够编写干净代码、跟上最新的 LLM 进展、并且在此过程中能够讲述好故事的全能数据专业人员的需求正在增加。让我们深入了解一下吧。
每月亮点
-
Python 十亿行挑战——从 10 分钟到 4 秒
由于长期以来声名狼藉的慢速性能,你可能认为 Python 在热门的“十亿行”挑战中毫无胜算。Dario Radečić的这篇病毒式文章旨在展示,通过一些灵活性和创新思维,你仍然可以从代码中获得显著的时间节省。
-
N-BEATS — 第一个适用于时间序列预测的可解释深度学习模型
任何喜欢深入了解模型内部运作的人都应该收藏Jonte Dancker关于 N-BEATS 的精彩解释文章,这一方法被称为“首个超越传统统计方法的纯深度学习方法”,专门用于时间序列预测任务。
-
使用 ChatGPT 构建数据科学作品集网站:完整教程在竞争激烈的就业市场中,数据科学家不能对自己的成就和专业知识保持低调。一个作品集网站是展示这两者的强大工具,而Natassha Selvaraj的耐心指南展示了如何借助生成式 AI 工具从零开始构建这样一个网站。

图片由Tim Mossholder提供,来自Unsplash
-
BERT 完全指南(附代码)为什么不稍微回顾一下那些为今天的创新铺路的先驱模型呢?Bradney Smith邀请我们回到 2018 年(或 AI 界几十年前的时光),深入了解开创性的 BERT(双向编码器表示从转换器)模型。
-
为什么 LLM 不适合编码——第二部分
回到现在,我们常常听到关于程序员即将被淘汰的论调,尤其是 LLM 不断进步的背景下。Andrea Valenzuela的最新文章为我们提供了一个“慢着!”的有益提醒,她聚焦于 LLM 在跟进最新库和代码功能方面的固有局限性。
-
Python 中的 PCA 与 K-Means 在交通数据中的应用在我们每月精选内容中,怎样能比一篇关于核心数据科学工作流的实践教程更好地收尾呢?在她的首次 TDS 文章中,Beth Ou Yang带领我们走进一个现实世界的例子——这次是来自台湾的交通数据,展示如何使用主成分分析(PCA)和 K-means 聚类。
KANs 在聚光灯下
如果我们必须选出最近几周最受关注的话题,KAN(Kolmogorov-Arnold 网络)无疑是最容易的选择。以下是三篇优秀资源,帮助你熟悉这种在广泛传播的论文中介绍的新型神经网络。
-
Kolmogorov-Arnold 网络:神经网络的最新进展,简单易懂的解释
如果你想了解一篇清晰易懂的 KAN 入门文章,Theo Wolf的这篇简明易懂的文章绝对是最佳选择。
-
Kolmogorov-Arnold 网络(KAN)在时间序列预测中的应用
从更专业的应用角度来看,Marco Peixeiro展示了 KAN 如何在时间序列预测的背景下应用。
-
理解 Kolmogorov–Arnold 网络(KAN)
最后,如果你想阅读一篇更完整(但仍然易读)的论文式讲解,可以参考Hesam Sheikh的 TDS 首篇文章。
我们最新的一批新作者
每个月,我们都很高兴看到一批新作者加入 TDS,他们各自带来了独特的声音、知识和经验与我们的社区分享。如果你正在寻找新的作家来探索和关注,不妨浏览一下我们最新加入的作者们的作品,包括 Eyal Aharoni、Eddy Nahmias、Hesam Sheikh、Michał Marcińczuk, Ph.D.、Alexander Barriga、Sasha Korovkina、Adam Beaudet、Gurman Dhaliwal、Ankur Manikandan、Konstantin Vasilev、Nathan Reitinger、Mandy Liu、Beth Ou Yang、Maicol Nicolini、Alex Shpurov、Geremie Yeo、W Brett Kennedy、Rômulo Pauliv、Ananya Bajaj、林育任 (Yu-Jen Lin)、Sumit Makashir、Subarna Tripathi、Yu-Cheng Tsai、Nika、Bradney Smith、Katia Gil Guzman、Miguel Dias, PhD、Bào Bùi、Baptiste Lefort、Sheref Nasereldin, Ph.D.、Marcus Sena、Atisha Rajpurohit、Jonathan Bennion、Dunith Danushka、Bernd Wessely、Barna Lipics、Henam Singla、Varun Joshi 和 Gauri Kamat,以及 Yu Dong。
感谢您支持我们作者的工作!我们非常喜欢发布新作者的文章,因此如果您最近撰写了关于我们核心主题的有趣项目教程、教程或理论思考,请不要犹豫,与我们分享。
直到下一个 Variable,
TDS 团队
数据科学项目管理
一个五步走的数据科学项目管理框架
·发布于Towards Data Science ·7 分钟阅读·2024 年 4 月 25 日
--
这篇文章是关于全栈数据科学的系列文章的一部分。在上一篇文章中,我介绍了全栈数据科学家的概念及其所涉及的四个角色。在本文中,我将讨论这四个角色中的第一个——项目经理(PM)。尽管有无数种方法可以处理数据科学项目管理,但在这里我提出了一种可能的框架,并讨论了项目经理在执行该框架中的角色。

图片由Scott Blake提供,来源:Unsplash
数据科学项目通常涉及开发机器学习(ML)模型以解决业务问题。虽然在今天的商业环境中这看似司空见惯,但它仍然伴随一些风险。
换句话说,开发机器学习模型本质上是不确定的、技术要求高、成本昂贵且耗时。这些风险促使了为数据科学项目专门设计的项目管理框架的出现。
在这里,我将描述一种这样的管理方法,并阐述项目经理在这一背景下的关键贡献。
一个五步走的项目…
数据科学薪资分解 2024
Glassdoor 与 ZipRecruiter 与 PayScale:美国与 2022 年的对比
·发表于Towards Data Science ·阅读时间 10 分钟·2024 年 12 月 19 日
--

图片由Kenny Eliason提供,来源于Unsplash[1]。
目录
-
介绍
-
平均薪资(以一些知名公司为例)
-
美国城市分布
-
高级职位薪资分解
-
总结
-
参考文献
1. 介绍
本文面向那些对 2024 年美国数据科学薪资分解感兴趣的人。如果你关注我已经有几年,你会发现这篇文章很熟悉,它通过将 2022 年平均薪资与 2024 年进行对比,提供了一些有趣的对比数据。这些信息对于做出职业决策非常有用,无论是你当前的职位,还是在面试新工作时。正如你所知,数据会有所不同,因此报告也会有所不同。薪资的增长或下降可能由许多因素造成,例如通货膨胀[2],比如 2022 年通货膨胀率为 6.5%,而 2024 年为 2.7%。话虽如此,我们可以查看三个常见的报告数据科学薪资的网站,以更好地了解薪资预期。如果你想了解今年数据科学薪资的详情,或者想要比较这三大网站的数据,继续阅读吧。
数据科学技能 101:如何解决任何问题
六种可以在现实生活中应用的简单技巧。第一部分。
·发表于Towards Data Science ·6 分钟阅读·2024 年 5 月 13 日
--

图片来源:Unsplash(Alan De La Cruz)
当人工智能和机器接管越来越多的任务时,数据科学家还剩下什么?
随着自动化和人工智能塑造我们的工作场所,有一件事变得明确:人类问题解决技能比以往任何时候都更加重要。尽管人工智能变得越来越复杂,但人类在应对复杂挑战和进行创造性思考方面依然独具优势。
因此,毫不奇怪的是,研究,包括世界经济论坛 2023 年的报告,突出了问题解决是雇主最看重的技能之一[1],而且对于复杂问题解决技能的需求只会越来越大。
认知问题解决技能、分析性和创造性思维是 2023 年需求量最大的两项技能,也被预测是未来将进一步增长的重要技能。
来源:世界经济论坛。未来就业报告 2023
令人惊讶的是,尽管这一技能日益重要,但却缺乏如何提升这一技能的指导。
本文将提供策略和实例,帮助你为未来做好技能准备,在问题解决仍然是人类独有的强项的领域。
数据科学技能 101:如何解决任何问题,第二部分
六种你可以在现实生活中应用的简单技巧
·发表于 Towards Data Science ·阅读时间 8 分钟·2024 年 5 月 30 日
--
本系列的第一部分讨论了解决问题技能日益增长的需求。随着我们的世界越来越多地被 AI 自动化,这些技能比以往任何时候都更加重要。
本文继续概述了解决任何问题的实际策略。接下来将介绍三种技巧,并通过实际应用示例进行说明。
提醒:测试你的问题解决能力:
第一篇文章还包括了一个问题,用来测试你的问题解决技能。
提醒一下,任务是绘制一条线,在下图中将蓝色点和绿色点完全分开:

一个需要解决的问题;你能否只用一条直线将两组点分开? 来源:作者
还在找答案吗?以下三种技巧应该有助于解决这个难题(如果没有,答案会在文章末尾给出!)
4. 降低精度

来源:作者
理解我们的太阳系:N 体问题
数据科学支持循环经济实施
您如何利用数据科学来支持快时尚零售商实施循环经济?
·发表于 Towards Data Science ·阅读时间 10 分钟·2024 年 3 月 20 日
--

(图片来自作者)
循环经济是一种经济系统,其中资源在一个封闭的循环中被使用、重复使用或回收,而不是被提取并作为废弃物丢弃。
您退还给您最喜欢的快时尚零售商的旧衣物会发生什么?
目标是最大化资源利用,并在每个生命周期结束时再生产品或材料。

服装退还后的处理流程 — (图片来自作者)
它们被收集并分类为三种不同的类别
-
可穿戴衣物:这些将作为二手衣物转售。
-
再利用的衣物:这些将被转化为其他产品,如“重制系列”或清洁用布。
-
回收衣物:这些将被粉碎成纺织纤维,用来制造绝缘材料。
作为数据科学经理,您如何支持公司向循环经济的转型?
这需要对您的物流和供应链运营进行重大转型,以将这些过程纳入价值链。
在本文中,我们将探讨如何通过提供监控、诊断和优化工具,利用数据科学支持向循环经济的转型。
什么是循环经济?
数据科学支持绿色转型
您是一个国际服装集团的物流部门的数据科学经理,该公司在全球拥有门店。
该公司在亚洲的工厂生产服装、包包和配饰。

供应链网络 — (图片来自作者)
这些工厂向中央仓库交付货物,以补充商店的库存。
去年,你的首席执行官公开承诺支持联合国可持续发展目标,并特别关注地球的目标。

17 个可持续发展目标 [文章:链接] — (图片来源:作者)
因此,可持续发展团队正在制定一个路线图,计划到 2030 年减少碳足迹。

可持续发展路线图项目团队 — (图片来源:作者)
已经组建了一个项目团队,团队成员包括来自运营部门(制造和物流)、财务经理和 IT 专家的专家。
作为数据科学经理,你为物流部门在转型中的实施挑战提供分析解决方案。
分销网络的转型
需要对分销网络进行彻底重组,以包括这个逆向流动。

逆向物流 — (图片来源:作者)
目标是实施一套过程来
-
提供客户在商店的“收集箱”中投放旧衣服的选项。
-
组织将这些箱子转移到一个中央仓库。
-
在仓库实施过程,将物品分类为三类以进一步处理。
由于本节重点是物流管理,我们将不会深入探讨回收过程的细节。
如何通过数据支持这些解决方案的设计和实施?
你的任务是设计先进的分析工具,以监控运营的 KPI 并优化过程。
在下一节中,我们将详细介绍组织在实施这些过程变更时可能面临的运营挑战。
数据科学支持运营
多年来,你与物流团队合作,实施报告和优化工具用于前向物流。

逆向物流 — (图片来源:作者)
他们有额外的要求,需要管理收集和分类过程,为向循环经济过渡做好准备。
我们需要监控和优化哪些额外的过程?
在本节中,我们将简要介绍几个过程设计需求的示例以及相关的分析解决方案。
描述性分析:产品跟踪与可追溯性
因为法规要求,你必须确保产品的使用和处理尽可能可持续。
因此,物流团队需要能够追踪和追溯产品的整个生命周期。
产品追踪与可追溯性 — (图片来源:作者)
对于正向物流,产品从工厂到商店的追踪。
-
主数据数据库将包括(库存单位)SKU 编号、产品信息(尺寸、颜色、包装)
-
生产系统可以提供批次号(如果需要调用商品时很有用)、生产日期和工厂位置。
-
仓库与运输管理可以沿着物流链追踪产品,从仓库接收直到店铺交付。
如何通过高级分析支持退货商品的追踪?
对于逆向物流,物流部门需要你的支持来监控“退货”产品从回收箱到分拣中心的流动。
物流管理系统 — (图片来源:作者)
不同的物流系统记录交易数据,可以作为数据源用来构建自动化的流动监控工具:
-
企业资源计划(ERP)记录店铺的客户退货信息,包括店铺位置、SKU ID、数量、回收箱 ID和回收时间。
-
运输管理系统记录了回收箱的取件情况,包括商店位置、回收箱 ID 和取件时间。
-
仓库管理系统跟踪从接收商品到分拣过程结束的商品信息,包括 SKU ID、回收箱、接收时间、分拣结束时间和最终目的地。
数据仓库结合来自多个来源的数据 — (图片来源:作者)
使用中央数据仓库的商业智能方法可以支持创建统一的追踪数据源,这些数据源可用于审计或报告。
如果你需要更多关于如何实现它的细节,可以查看这篇文章。
发现适用于供应链优化的数据驱动决策工具。
towardsdatascience.com
现在你已经为操作提供了透明度,接下来让我们专注于流程优化。
预测性分析:分拣网络设计
物流团队希望设计一个分拣中心网络,以最小化逆向流动的环境影响。
最优网络可能与当前网络不同,因为你需要在销售后收集商品并将回收材料重新引入链条中。

网络设计 — (图片来源:作者)
它们与您分享了
-
一个潜在排序地点的列表及其能力(单位/天)
-
来自仓库的反向流量预报(单位/天)
问题:我们应该将排序中心放置在哪里,以最小化成本和二氧化碳排放?
这将提醒您供应链网络优化问题,您需要为此设计一个工厂网络。

供应链网络问题 — (图片来源:作者)
市场需求必须由具有有限产能和不同成本的工厂来满足,以生产和交付商品。
线性规划用于选择合适的工厂,在满足一组约束条件的同时最小化总体成本。
更多细节,请查看这个详细示例
在设计供应链网络时,您是否考虑了需求波动?
towardsdatascience.com
我们如何将这个解决方案调整到排序网络设计问题上?
这个问题可以很容易地适应新的问题。
-
需求 => 从仓库收集的单位(单位/天)。
-
工厂产能 => 每个中心的排序能力(单位/天)。
然后,您可以根据目标选择最佳的排序地点集。
-
最小化总成本?
算法将考虑运输成本(从仓库到每个排序地点)和每个中心的排序成本。
-
最小化二氧化碳排放?
算法将尽力最小化从仓库到选定中心的距离。
现在,您已选择了最佳的排序中心集,您可以组织反向流。
处方分析:反向流优化
运输团队请求您支持设计一个工具,将正确的排序中心分配给每个仓库。

反向物流规划问题 — (图片来源:作者)
事实上,随着体积和产能的变化,最好每周或每天动态地为每个仓库分配一个排序中心。
问题:对于每个仓库 i,应该由哪个中心对退货进行排序,以最小化运输成本?
在上一篇文章中,我讨论了一个类似的问题:供应计划问题。
供应计划问题 — (图像来源:作者)
多个工厂为配送中心补货,配送中心存储商品并将其配送到门店。
为了解决这个问题,我们还使用线性规划来
-
优化从工厂到配送中心的上游流动
-
从正确的仓库向每个门店配送
该解决方案在稍作调整后可以使用
-
定义(或预测)从分拣中心到最终回收地点的货物流量
-
定义每个分拣中心的能力
你可以按照本文中详细介绍的方法(并使用代码)进行操作。
你需要将库存分配到哪里,以满足客户需求并减少运输成本?
towardsdatascience.com
现在我们已经设计了分拣位置的网络,并开发了优化反向流动的工具,我们可以着手进行绩效监控。
你听说过可持续供应链优化吗?
分析解决方案的实施
如何实施分析能力?
供应链是指多个方交换物料、信息或资金资源,以履行客户需求的过程。

用于监控流动的系统 — (图像来源:作者)
为了交换和存储信息,这些方使用不同的系统:
-
运输管理系统(TMS),
-
销售点(POS)系统用于管理销售数据
-
仓储和生产管理系统
它们连接到一个中央企业资源规划(ERP)系统,该系统集中管理端到端的操作。

供应链集成:可以从系统中捕获关键数据 — (图像来源:作者)
这些系统可以纳入数据架构中,用于收集、处理和分析来自多个来源的交易数据。
这个集中来源的统一且清洁的数据可以用于
-
部署上一节中介绍的分析产品。
-
实施绩效指标来监控反向流动的“健康”状况。
实施绩效指标
物流管理需要能够可视化一组指标,以跟踪循环经济实施的进度,并在需要时进行调整。

绩效管理分析 — (图像来源:作者)
他们希望跟踪关键绩效指标(KPI)以
物流绩效
-
补货、配送和收货提前期
-
每件商品的仓储和运输成本
-
仓库和门店配送的准时且完整(OTIF)
这些指标侧重于你的收集网络的表现,可以通过仓储和运输管理系统收集。
相关的回收过程的额外关键绩效指标(不在本文范围内)可以被添加来进行测量。
-
生产遵从率:生产的数量/计划数量的百分比。
-
回收率:成功回收的可回收材料的百分比。
-
污染率:流中发现的不可回收材料的百分比。
-
处理前置时间:处理一定量收集材料所需的时间和资源。
按照本节中介绍的方法论和解决方案,你拥有了监控和优化支持循环经济的流程的正确解决方案。
结论
这些额外的约束将需要付出努力和资源来转变组织。

益处 —(作者提供的图片)
然而,时尚零售公司通过实施循环经济原则,可以实现多个环境效益。
-
减少废物和污染,以符合未来的法规要求。
-
减少温室气体排放,以达到目标。
-
通过减少对有限资源的依赖,并通过回收增加原材料供应来源,从而提高韧性。
因此,你可能开发的各种先进分析工具,可以成为你组织的战略资产。
供应链网络优化
作为下一步,你可以着手优化可持续供应链网络,以减少上游流动的影响。

供应链网络设计问题 —(作者提供的图片)
可持续供应链优化是一种将成本减少与环境责任相结合的网络设计方法。

可持续供应链优化应用程序 —(作者提供的图片)
作为分析解决方案的一个例子,我已部署此简单的网络应用程序,以支持选择供应商/工厂并最小化生产和交付产品的环境影响。
🚀 了解更多信息!👇
帮助你的组织结合可持续采购和供应链优化,以控制成本和减少环境影响…
[towardsdatascience.com
为什么不租赁你的连衣裙而不是购买它呢?!
模拟循环经济的表现
在另一篇文章中,我模拟了为时尚零售商实施订阅模型。
循环租赁模式文章:[链接]——(图片由作者提供)
这个想法是让客户支付定期费用,以在特定时期内访问某个产品或服务。
目标是减少产品生命周期中的环境影响。
线性模型(上)与循环模型(下)——(图片由作者提供)
在上述示例中,我们将 5 次购买转换为 5 次租赁相同的物品,这样你只需支付费用即可节省开支。
-
一个完整的周期从原材料提取到商店交付。
-
四个退货周期配合逆向物流和清洗过程。
更多详情,
使用数据科学模拟循环模型对快时尚行业的 CO2 排放和水使用的影响……
towardsdatascience.com
关于我
让我们在Linkedin和Twitter上连接。我是一名供应链工程师,通过数据分析优化物流操作并降低成本。
如果你需要有关供应链转型的咨询或建议,请通过Logigreen Consulting与我联系。
如果你对数据分析和供应链感兴趣,请访问我的网站。
这是一个专注于数据科学、个人生产力、自动化、运筹学和可持续性的技术博客……
samirsaci.com](https://samirsaci.com/?source=post_page-----c9de824e73be--------------------------------)
💡 在 Medium 上关注我,获取更多关于🏭供应链分析、🌳可持续发展和🕜生产力的相关文章。
📘 你的供应链分析完整指南:分析备忘单
💌 免费将最新文章直接送达你的邮箱:订阅邮件
数据科学独角兽、RAG 管道、新的相关系数,以及其他四月必读文章
·发表于 Towards Data Science ·发送至 新闻简报 ·4 分钟阅读·2024 年 5 月 2 日
--
感到受到启发,想写第一篇 TDS 文章吗? 我们始终欢迎新作者的投稿。
有些月份,我们的社区似乎会聚焦在一小圈话题上:一种新的模型或工具出现,大家的注意力迅速集中在最新的热点新闻上。而在其他时候,读者们似乎在向多个不同的方向移动,深入探讨各种各样的工作流程和主题。上个月显然属于后一种情况,当我们回顾最受读者关注的文章时,我们被它们视角和焦点的多样性所震撼(并且印象深刻!)。
我们希望你喜欢我们精选的几篇四月最受欢迎、最被分享和讨论的文章,其中包括今年迄今为止最受欢迎的几篇文章,以及几篇高质量(且适合初学者的)解读文章。
月度亮点
-
神经网络背后的数学到现在为止,几乎没有人需要介绍 Cristian Leo的机器学习核心概念系列指南。毫无疑问,其中没有比神经网络更重要的基础模块了,因此,这篇深入探讨其背后数学的文章在我们读者中取得了巨大成功也就不足为奇了。
-
Pandas:从杂乱到美丽看到一位作者的第一篇 TDS 文章能够引起广泛读者的共鸣,总是令人欣喜;这正是Anna Zawadzka关于如何优化 Pandas 代码的实用指南所取得的成就,为保持代码“简洁且万无一失”提供了可操作的建议。
-
一种新的相关系数真正的统计学突破如今并不常见——这也解释了为什么Tim Sumner的文章引起了数据专业人士的广泛关注,因为这篇文章介绍了一种“全新的衡量两变量关系的方法,这种方法可能比相关性更好”。

照片来自micheile henderson提供,来源于Unsplash
-
如何构建本地开源 LLM 聊天机器人(RAG)几个月前,在机器学习领域初露锋芒的 RAG 方法似乎并未失去它的光彩。Dr. Leon Eversberg的教程便是一个例证:它为不断增长的工具列表增添了一个新颖的解决方案,使我们能够与 PDF 文档“对话”。
-
深入了解手动构建 TransformerTransformer 的指南和技术演示并不难找到。Srijanie Dey, PhD的贡献之所以与众不同,是因为其易于理解和清晰的表达——这使得这篇文章成为初学者和视觉型学习者的强大资源,尤其是在配合精心设计的插图后。
-
从数据科学家到 ML/AI 产品经理职业转型从来都不是一件简单的事情,尤其是在求职者面临艰难时期时更是如此。Anna Via分享了大量的灵感,同时结合了她个人成功转型为机器学习产品经理的经验,提供了许多可操作的建议和洞察。
-
全栈数据科学家的四顶帽子成为一名真正的“全栈”数据专业人士需要具备什么?Shaw Talebi最近推出了一系列文章,详细探讨(并回答)这个问题;这篇文章是该系列中的第一篇,提供了关于数据科学家核心技能的高层次视角,能够“看到大局并根据需要深入项目的具体方面”。
-
认识 NiceGUI:你即将最喜欢的 Python UI 库
难以跟上每天发布的所有令人兴奋的新库、包和平台——这就是为什么一篇详细、充满见解的第一手评论如此有用的原因。Youness Mansar 正是通过他的 NiceGUI 介绍,旨在实现这一目标,NiceGUI 是一个基于 Python 的开源 UI 框架。
-
线性回归与因果推断往往,简单就是成功的关键。Mariya Mansurova在她的产品分析因果推断指南中一再强调这一点,该指南避免了复杂的算法和方程式,转而采用了经过验证的线性回归方法。
我们最新的一批新作者
每个月,我们都很高兴看到一群新的作者加入 TDS,每个人都与我们的社区分享他们独特的声音、知识和经验。如果你正在寻找新的作家来探索和关注,只需浏览我们最新加入的作者作品,包括Thomas Reid、Rechitasingh、Anna Zawadzka、Dr. Christoph Mittendorf、Daniel Manrique-Castano、Maxime Wolf、Mia Dwyer、Nadav Har-Tuv、Roger Noble 和Martim Chaves、Oliver W. Johnson、Tim Sumner、Jonathan Yahav、Nicolas Lupi、Julian Yip、Nikola Milosevic (Data Warrior)、Sara Nóbrega、Anand Majmudar、Wencong Yang、Shahzeb Naveed、Soyoung L、Kate Minogue、Sean Sheng、John Loewen, PhD、Lukasz Szubelak、Pasquale Antonante, Ph.D.、Roshan Santhosh、Runzhong Wang、Leonardo Maldonado、Jiaqi Chen、Tobias Schnabel、Jess.Z、Lucas de Lima Nogueira、Merete Lutz、Eric Boernert、John Mayo-Smith、Hadrien Mariaccia、Gretel Tan、Sami Maameri、Ayoub El Outati、Samvardhan Vishnoi、Hans Christian Ekne、David Kyle、Daniel Pazmiño Vernaza、Vu Trinh、Mateus Trentz、Natasha Stewart、Frida Karvouni、Sunila Gollapudi,以及Haocheng Bi等人。
感谢您支持我们作者的工作!我们热衷于发布新作者的文章,如果您最近写了一篇有趣的项目演示、教程或关于我们核心主题的理论反思,欢迎随时与我们分享。
直到下一个变量,
TDS 团队
数据科学家回答最受欢迎的数据科学问题
针对未来数据科学家的全方位指导
·发表于 Towards Data Science ·阅读时长 6 分钟·2024 年 11 月 26 日
--

图片来自 Emily Morter ,来源于 Unsplash
我已经做了三年多的数据科学家,因此我想写一篇文章,回答我在 YouTube 频道和 Medium 文章的评论区收到的最受欢迎的数据科学问题。
问题按技术、职业建议以及其他杂项进行分类。希望你能找到你想要的答案!
技术
我应该掌握多少 SQL 知识?
SQL 是数据科学家的基础语言,因此你应该掌握它。SQL 的优点是它比 Python 更容易学习,因为它是一个非常小巧的语言。
我有一篇文章解释了成为一名优秀入门级和中级数据科学家所需掌握的 SQL 知识,强烈推荐你阅读。
数据科学家若不掌握这些函数,无法在 Python 中取得优异表现
Python 核心函数、使用案例、脚本和底层机制介绍
·发布于Towards Data Science ·阅读时长 10 分钟·2024 年 8 月 31 日
--

在我上一篇文章中,我讨论了 SQL 用户定义函数。但是与 SQL 相比,Python 凭借其在函数设计上的多样性脱颖而出。从我在科技公司工作的经验来看,数据科学项目无法在没有广泛使用 Python 函数的情况下完成。Python 已经成为数据科学家高效管理和分析数据、处理复杂任务和部署产品功能的基础工具。凭借其核心的广泛功能,Python 证明了自己在数据科学领域是一种强大的工具。然而,由于可用的函数种类繁多,数据科学家很难也不可能对它们都熟悉。今天的文章将介绍 8 种在实际数据科学中常用的函数,解释它们背后复杂的逻辑和机制,这些在其他教程中很少提及。本文还将澄清不同类型函数之间的混淆,这些类型经常被误认为是相同的。最后,通过一个小项目展示如何在实践中有效地应用其中几种函数。
数据科学家在云端工作。作为学生,如何练习这一技能(第一部分:SQL)
忘掉本地的 Jupyter Notebooks 和充满泡沫的编程课程——这里是你可以在真实云平台上练习的地方。第一部分:在 Google BigQuery 中使用 SQL
·发表于 Towards Data Science ·阅读时间 6 分钟·2024 年 5 月 8 日
--

图像由作者通过 GPT-4 生成
在 2020 年 3 月第一次封锁期间,我和我的室友在看完虎王和最后的舞蹈的 Netflix 剧集间隙,决定学习 Python。
他在某个大学的网站上找到了一门免费的课程。那真糟糕,他在一周内就放弃了。
我找到了一门在 Udemy 上花费£10 的课程,真的很棒。
但是即使是一个很棒的课程,也无法与“真实世界”的编程项目相比,所以我还是在一个月内感到厌烦并放弃了。
在线课程非常棒——但也只是到一定程度为止。
当你刚开始学习编程时,你不想浪费时间设置环境和下载软件。
你只想打开浏览器,点击开始课程,然后开始编程。
(那就是为什么我的朋友放弃的原因——他的课程有太多的环境设置麻烦,对初学者来说太令人沮丧了。)
数据科学家在云端工作。作为学生,如何实践这一点(第二部分:Python)
因为数据科学家不会在 Udemy 的代码编辑器中编写生产代码。
·发布于Towards Data Science ·9 分钟阅读·2024 年 5 月 29 日
--

图片来自Luke Chesser的Unsplash
如果你想成为一名数据科学家,光会编程是不够的——你还必须知道如何在云端运行你的代码。
这是我在申请第一份数据科学工作时遇到的一个真正的问题。
职位描述中经常会有“AWS”或“GCP”这样的要求,但我所参加的编程课程主要集中在如何编写正确的语法,并没有教我很多关于实际在云环境中运行代码所需的系统。
在本系列中,我将提供一些关于如何实践云端编程以进行数据科学的建议。它面向两类人群:
-
有志成为数据科学家的——如果你有一些数据经验(例如,你曾在本地的 Anaconda 环境/Jupyter Notebook 中使用过 Python,或者你在 Udemy 或 DataCamp 的沙盒中运行过 SQL 查询),这将帮助你填补在准备进入数据科学行业工作时技能上的一个重要空白。
-
想要跳出本地 Jupyter Notebook 的 data scientists——正如Pau Labarta Bajo所说:“Jupyter Notebook 中的 ML 模型……”
关于 Voronoi 图的一切:分析东京公共交通站点的服务区域
在获取一些略显有趣的统计见解的同时,探索数据科学技术
·发布于Towards Data Science ·12 分钟阅读·2024 年 8 月 16 日
--

数据科学与公共交通:梦之队。(图片来源:作者,插图由三船隆志提供,遵循免费使用协议)
随着世界日益城市化[1],公共交通已经成为城市生活中无处不在的一部分。世界上可能最具城市化特点的地方是东京[2]—这座规模无与伦比的繁忙大都市,其中大多数人都主要依赖公共交通[3]来处理日常事务。
本文将向你介绍在城市规划背景下的Voronoi 图概念,并用它来划分东京火车站的服务区域。我们将利用获得的服务区域来获取一些或许略显有趣的关于车站周边的统计数据。
介绍

Voronoi 图(图片来源:作者)
Voronoi 图和 Delaunay 三角剖分在许多科学领域得到了广泛应用。[4] Voronoi 图,也被称为Voronoi 网格,用于将平面表面划分为对应于特定点的独立区域。
这个问题在许多不同的情况下频繁出现。[5]
下面是一些例子:
-
墨尔本政府(2024-),当他们将学生分配到最近的学校时[6]
-
约翰·斯诺(1813–1853),当他将伦敦霍乱爆发与水泵的位置相关联时[4]
-
勒内·笛卡尔(1596–1650),当他研究物质相对于恒星的分布时[4]
如今,Voronoi 图在许多领域中被广泛应用,包括计算机科学、地理学,尤其是城市规划。城市规划是我想要向你们详细介绍的领域——我们将确定世界最大都市——东京的公共交通站点的服务区域。
Voronoi 的组成部分

Voronoi 图的组成部分(图片来自作者)
Voronoi 图由多个不同的结果集组成,每个结果集有不同的名称和用途:
-
🔵 Voronoi 站点是计算Voronoi 区域的参考位置。
-
🟣 Voronoi 区域包含表面上所有距离相关Voronoi 站点比任何其他站点更近的点。
-
🟢 Voronoi 弧线是两条Voronoi 区域之间的边界直线段[4]
-
🟠 Voronoi 顶点是 Voronoi 弧线交点的地方。
Voronoi 的距离函数

比较各种距离函数(图片来自作者)
城市规划中的Voronoi 图通常基于距离/参考系统关系。距离是根据某些度量标准计算的,例如实际距离或旅行时间。计算该距离的方法有多种:

欧几里得距离概述(图片来自作者)
欧几里得距离。是坐标系中两点之间的航空距离。它假设两点之间有开阔空间,没有任何阻碍物。它是最基本的计算距离的方法。

曼哈顿距离概述(图片来自作者)
曼哈顿距离。忽略航空距离,并引入一种距离函数来近似城市网格中的旅行时间——就像在曼哈顿一样。它更适合某些城市环境。

基于时间的距离概述(图片来自作者)
基于时间的距离。这是最准确的度量标准,但也是获取最复杂的度量标准。
此外,基于时间的距离可能会导致异常,因为不同区域之间的旅行时间可能与实际距离不成线性关系,从而可能导致沃罗诺伊区域的不均匀划分 [10]——就像下面的例子一样。

图 5. 选定区域的沃罗诺伊图,包括地理(a)、道路(b)和旅行时间距离(c)。宽蓝线表示沃尔塔河,而品红色的块状区域表示桥梁的位置 [11]
现在,我们已经掌握了开始进行现实案例——东京公共交通所需的所有基础知识。让我们开始吧!
评估东京的交通系统
城市规划中最重要的方面之一就是公共交通网络——尤其是在像东京这样的地方。东京是许多人向往的地方——尤其是公共交通爱好者。
因此,东京为我们提供了一个完美的例子,可以在现实生活中应用沃罗诺伊图。
概念
想象一张城市地图,上面标有公共交通站点。每个站点服务其周围的区域,这被称为服务区。那么,我们如何确定这些服务区呢?
为了更好地理解如何实现我们的目标——获取公共交通车站的服务区,我将沃罗诺伊图融入我们的公共交通视角:

一个关于如何使用沃罗诺伊图来获取服务区的概念(图源:作者)
-
🔵 沃罗诺伊站点 现在是东京的一个火车站
-
🟢 沃罗诺伊弧线 现在是划分火车站服务区的边界
-
🟣 沃罗诺伊区域 现在是基于我们距离函数定义的一个特定火车站所服务的区域
这就是沃罗诺伊图能为我们提供的东西。通过根据到最近车站的距离将地图划分为多个区域,我们就有了一个非常简单的方法来定义火车站的服务区。
现在,概念已经清晰,我们可以开始实际的实施了。
确定东京
我们需要定义我们想要为其创建沃罗诺伊图的区域。这个过程通常被称为定义边界框。

东京市与东京府的区别(图源:作者)
当有人提到“东京”时,它对不同的人可能意味着不同的事情。
有些人可能会将其看作是:
1) 繁华的城市(东京市),有着高耸的摩天大楼和充满活力的街头文化,而其他人可能会将其视为一个
2) 都道府县(东京府),拥有迷人的风景和自然美景,而其他人可能会提到
3) 大东京地区,这是世界上人口最多的都市区 [8],由多个独立的城市组成(例如东京、埼玉、横滨等)。
然而,重要的是要明确我们所说的“东京”到底指的是什么,以避免混淆。
使用府县定义使我们能够区分东京与其邻近的城市,如埼玉、千叶和横滨,同时包含大多数人通常所说的“在东京”的位置。
东京府将成为我们所有后续Voronoi计算的基本边界框。
为了获取有关东京府的地理空间信息,我们求助于官方政府来源,具体如下:
数据集: 国家土地数值信息 | 行政边界数据 (mlit.go.jp),根据开放数据政策授权,允许商业使用。 利用規約 (mlit.go.jp)
定位火车站

视觉检查我们的数据集。我在正确的位置找到了高田马场。(图片来自作者)
日本政府提供了一个可靠的火车站信息来源。他们的网站提供了一个详细的数据集,包含全国各地的所有火车站,以及其他相关元数据。
值得注意的是,该数据集仅关注日本政府定义的火车站,可能不包括地铁和单轨列车等大众交通工具,但包括了人们通常不认为是火车的交通方式,例如特定的缆车。
通过结合额外的数据来源,可能有机会提高检索数据的准确性。[9] 在本文中,我将继续使用政府数据集,不进行进一步的增强。
数据集: 国家土地数值信息 | 铁路数据 (mlit.go.jp),根据开放数据政策授权,允许商业使用。 利用規約 (mlit.go.jp)
获取服务区域
现在我们已经确定了东京的边界以及相关火车站的列表,接下来可以开始计算我们的服务区域。我们将使用KNIME,这是一款功能强大的科学计算工具,通过抽象化许多复杂性,最大程度减少了对深入数学知识或编程的需求。
免费和开源,包含所有数据分析工具。通过视觉工作流构建器创建数据科学解决方案…
1. 提取数据
我们从连接数据源到工具开始。幸运的是,KNIME 提供了一套地理空间操作工具,我们可以直接使用这些工具。
我们继续创建两个节点来导入我们的数据。GeoFile Reader node 能够处理 Shapefile 和 geojson 数据类型。

在 KNIME 中进行数据导入(图片来源:作者)
要使用这两个数据集,必须进行一些准备工作。
-
我们使用 Column Filter node 删除了一些字段,因为每个数据集中的字段数量过多。
-
为了增强可读性,我们使用 Column Renamer node 对数据集中的某些列进行了重命名。
-
为了避免后续的混淆,我们使用 Column Renamer node 给每个数据集中相同的列赋予唯一的名称。

在 KNIME 中进行数据提取(图片来源:作者)
完成数据提取和准备工作后,我们现在可以继续进行计算了。
2. 处理数据
我们的下一个目标是获取每个车站的 Voronoi 多边形,从而使我们能够推导出它们各自的服务区域。
-
我们使用东京县数据集创建了 Voronoi 图的边界框,利用 Bounding Box node。
-
为了执行计算,我们需要的是车站的点而不是多边形表示。Geometry to Point node 用于将它们转换为点。
-
我们使用Voronoi (Thiessen) Polygon 节点进行 Voronoi 计算,生成多边形和关联的 ID。然而,由于我们还需要车站元数据,我们必须通过空间连接节点将 Voronoi 多边形与这些信息再次连接。

KNIME 中的整个工作空间(图片来自作者)
这就是我们在 KNIME 中获取车站服务区所需的一切。让我们看看结果。
服务区(V1)
现在我们可以看到 Voronoi 图已经将我们的地图划分,给每个车站分配了一个独特的区域。

一站多服务区——出了点问题。使用 QGIS 进行可视化(图片来自作者)
我们应当记住,像高田马场这样的车站,在现实中看起来像一个车站,但实际上由多个车站组成。因此,我们需要做一些额外的工作,确保我们的计算能够准确反映这一点。
清理数据和服务区(V2)
车站数据集包含一个额外的 ID,该 ID 根据车站的公共名称和/或实际外观对车站进行分组。通过利用这个 ID 和Group By 节点,我们可以将各个车站合并成一个。

KNIME 中的整个工作空间(图片来自作者)
在合并各个车站后,我们能够创建一个更准确的服务区数据集,更好地反映人们在现实中看到车站的方式。看看高田马场——它现在位于一个单一的 Voronoi 区域内。

清理后的数据集:现在每个车站有一个服务区,通过 QGIS 进行可视化(图片来自作者)
结果
我们已经完成了计算,并得到了可以提供有用见解和统计数据的服务区域。
若要自己探索结果,您可以在这个 GitHub 仓库中找到所有内容:
[## GitHub - martinjurran/KNIME-Tokyo-StationServiceAreas: 计算服务区的 KNIME 工作流…
计算东京车站服务区和餐厅密度分析的 KNIME 工作流……
接下来,我们将获取一些可以从服务区中导出的现实生活统计数据。
统计示例 — 餐馆密度

在日本找餐馆并不难——但它们的密度最高在哪里呢?(插图由 高桥三船提供,使用许可为自由使用)
在规划度假时,最大的麻烦之一就是决定住在哪里。我的意思是,你肯定想选择一个靠近餐馆、商店和其他有趣场所的位置,对吧?
但面对这么多选择,找到完美的地点可能会让人困惑。这就是我们新获取的 交通站点服务区 能够帮助我们的地方:
目标:识别周围有最多兴趣点 (POI) 的车站。为了简化问题,我们将专注于餐馆。
获取数据
对商业用途有价值的数据似乎经常受到保护,并且难以获取。对于餐馆来说,目前没有官方的来源可供使用。
最准确的来源,如企业注册信息或谷歌地图,要么价格高昂,要么未获得大规模处理的批准。
Overpass API 是由 OpenStreetMap 基金会提供的,它是我们获取所需数据的唯一来源之一。通过在 Overpass Turbo 中执行一个简单的查询,我们可以获取东京所有的餐馆数据。
nwramenity=restaurant;
out center;
数据会立即在 Overpass Turbo 中显示,并且可以导出为我们选择的文件格式:

Overpass Turbo 用户界面(图片由作者提供)
我们现在拥有了东京所有餐馆的完整数据集。尽管数据有其局限性,因为这些数据是众包的,未经验证,且可能主要集中在市区最受欢迎的地点——毕竟人们通常在这些地方进行贡献。但因为它是我们能获得的最佳数据,我们将继续使用它。
数据集: overpass turbo (overpass-turbo.eu),数据遵循 开放数据库许可证 (ODbL)
将兴趣点 (POI) 与其服务区匹配
要计算车站服务区内餐馆的数量,我们需要将兴趣点 (POI) 与其各自的服务区匹配。
我已将车站服务区作为图层导入到 QGIS 中。该应用程序提供了直接计算区域内节点数量的功能。

在 QGIS 中计算多边形内的点数(图片由作者提供)
每个服务区的餐厅数量未能满足我们的要求,因为一些大型区域有很多餐厅——但这些餐厅之间的距离较长。因此,我们需要开发一个新的指标来解决这一情形。最简单的方法是计算餐厅的密度。
POI 密度公式
对于简单的排名,餐厅/km²每个服务区可能是一个好的表现方式。这样,我们可以找到餐厅密度最高的服务区。
在某些情况下,服务区可能很小,但餐厅数量很多,这可能会夸大它们的得分。然而,在我们的情况中,这不是一个问题。小型服务区可能意味着附近有其他车站和更多的餐厅。
公式如下:

计算 POI 密度的公式
其中:
R = POI 密度因子,单位为 n/km²
A = 多边形面积,单位为 km²
n = POI 的数量
我们将POI 数量/服务区面积数据集导入 KNIME,并对每个服务区运行我们的公式。

在 KNIME 中计算 POI 密度(图像由作者提供)
最后,我们获得了餐厅密度最高的区域。让我们来看看结果。
结果
餐厅密度最高的前 20 个车站服务区是:

按餐厅密度排序的前 25 个车站,用 Tableau Public 可视化(图像由作者提供)
我们还可以通过地图查看结果,以获得更多的洞察:

按餐厅密度排序的前 20 个车站服务区,用 Tableau Public 可视化(图像由作者提供)
我们可以看到,密度最高的区域呈现出集群的形式。我做了一些进一步的研究,发现东京由多个独立的城市组成(例如台东区、涩谷区、千代田区)。这些集群某种程度上代表了东京所由的各个城市——有趣!

我们识别到的高餐厅密度集群(图像由作者提供)
需要注意的是,我们的数据集是众包收集的,可能并不完全具有代表性或完整性,因为它可能偏向那些经过特别详细调查的区域。
然而,根据我们所拥有的数据,上野御徒町站无疑是赢家。
餐厅密度最高的车站服务区:上野御徒町站
如果你有兴趣进一步探索数据,可以查看 Tableau Public 页面,在那里你可以与可视化进行交互,并深入了解结果:
public.tableau.com/app/profile/martin.jurran/viz/Tokyo-RestaurantDensity/Map#1
结论

服务区域密度最高的车站。Ameya Yokocho 是上野-御徒町车站服务区域的一部分。
Voronoi 图比我们常常意识到的更具多功能性和实用性。它们使我们能够发现洞察,例如识别出上野-御徒町车站是东京府餐馆密度最高的区域。
即使是像 Uber 这样的主要公司,可能也在使用 Voronoi 图来高效地分配司机到接送位置。它们的广泛应用使得 Voronoi 图在多个行业中都具有很高的价值,尤其是因为它们可以用极少的资源进行计算。
我鼓励你探索 Voronoi 图的功能,看看它们如何能带给你好处。通过将它们纳入你的工具集中,你可以提升数据分析能力,并获得更有洞察力的统计数据。
来源
[1] 联合国 (2018 年 9 月 13 日),城市化,www.un.org/development/desa/pd/content/urbanization-0
[2] Demographia (2023 年 1 月 24 日),全球城市区第 19 次年度报告,www.demographia.com/db-worldua.pdf
[3] Demographia (2003 年 1 月 1 日),铁路交通有效的地方及原因,demographia.com/db-htld-rail.htm
[4] Vera Galishnikova, Peter Jan Pahl (2018 年 3 月 15 日),无翻转约束平面 Delaunay 三角剖分的构建,www.researchgate.net/publication/325582898_Constrained_Construction_of_Planar_Delaunay_Triangulations_without_Flipping
[5] Liebling T.M., Pournin L. (2010),Voronoi 图与 Delaunay 三角剖分:无处不在的“连体双胞胎”。《数学文献》 数学主题分类:01A65, 49-03, 52C99, 68R99, 70–08, 92–08
[6] 墨尔本政府 (2024),学校学区图,www.findmyschool.vic.gov.au/
[7] 维基百科 (2024),出租车几何,en.wikipedia.org/wiki/Taxicab_geometry
[8] 维基百科 (2024),大东京地区,en.wikipedia.org/wiki/Greater_Tokyo_Area
[9] 公共交通开放数据中心 (2024),数据集 — 公共交通オープンデータセンター 数据目录网站,ttps://www.odpt.org/
[10] D.T. Lee, Chung-Shou Liao, Wei-Bung Wang (N/A), 基于时间的 Voronoi 图,alumni.cs.ucr.edu/~weiw/paper/VD_highways.pdf
[11] 智能混合公共交通系统规划解决方案 — 以波兹南都市圈作为卫星城连接的案例研究 — ResearchGate 上的科学图表。 www.researchgate.net/figure/Voronoi-diagrams-of-selected-areas-for-geographical-a-road-b-and-travel-time_fig5_336071639
图标来自可爱免费素材集 いらすとや (irasutoya.com),© 高桥三舟

(图像由作者提供,插图来自高桥三舟根据自由使用许可)
使用 Pandera 进行 Python 数据验证
验证你的数据框以用于生产机器学习管道
·发表于 Towards Data Science ·9 分钟阅读·2024 年 11 月 18 日
--

图像由 NightCafe 生成。
数据验证是生产应用中至关重要的一步。你需要确保你获取的数据与管道兼容,并且没有意外的值出现。此外,验证数据是一种安全措施,可以防止任何损坏或不准确的信息被进一步处理,并在初步步骤中及时发出警告。
Python 已经有一个很棒的开源项目来处理这个任务,叫做 Pydantic。然而,当处理像机器学习中那样的大型数据框对象时,Pandera 是一种更快、更可扩展的验证数据方式(请查看这篇文章和公开的笔记本)。

Pandera 和 Pydantic 的逐行验证性能比较,适用于不同大小的 pandas.DataFrame 对象。图片来源于 source)-,Benchmarking%20Pandera%E2%80%99s%20row%2Dwise%20validation%20with%20Pydantic,-Because%20Pandera%20validates)。
此外,Pandera 提供对多种数据框库的支持,如 pandas、polars、dask、modin 和 pyspark.pandas。有关更多信息,请参考 Pandera 文档📄。
免责声明。 Pandera 是一个开源项目,采用 MIT 许可证。我与 Pandera 团队或 Union.ai 没有任何关系。此帖子没有商业目的。
使用 Pandera 验证数据
Pandera 有两种定义验证器的方式:Schemas和Models。我将专注于第二种方式,因为它与 Pydantic 模型相似,并且代码更简洁。
要定义 Pandera 模型,创建一个继承自 DataframeModel 的子类,并开始声明数据框必须具有的列和数据类型:
import pandera as pa
class UserModel(pa.DataFrameModel):
id: int
username: str
email: str
is_active: bool
membership: str
creation_date: pd.DatetimeTZDtype
# Use
df = pd.DataFrame(...)
UserModel.validate(df) # <- If invalidad raises SchemaError
请注意,为了定义用户创建时间戳,我使用了 Pandas 原生的日期类型,而不是像datetime.datetime这样的其他类型。Pandera 仅支持内建的 Python、NumPy 和 Pandas 数据类型。你也可以创建自定义数据类型,但这是一个高级话题,在大多数情况下不常用。
验证列属性
使用 Pandera,除了验证数据类型外,你还可以验证其他列属性:
class UserModel(pa.DataFrameModel):
id: int = pa.Field(unique=True, ge=0)
username: str = pa.Field(str_matches=r"^[a-zA-Z0-9_]+$")
email: str = pa.Field(str_matches=r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$")
is_active: bool
membership: str = pa.Field(isin=["premium", "free"])
creation_date: pd.DatetimeTZDtype = pa.Field(dtype_kwargs={"unit": "ns", "tz": "UTC"})
在这里,我像使用 pydantic 一样使用 Pandera 的 Field。
-
首先,我指定
id列不得包含重复值,并且这些值必须大于或等于 0。 -
在
username和email中,我使用正则表达式检查字符串是否有效。用户名只能包含字母数字字符和下划线,而电子邮件地址还可以包含短横线和点,但必须始终遵循“smth@smth.smth”的模式。 -
membership只能取列表中的值。更好的方法是使用 StrEnum 来定义有效值,而不是硬编码它们。 -
最后,
creation_date必须以纳秒为单位,并使用 UTC 时区。这行代码可以更简洁,使用typing库中的 Annotated:creation_date: Annotated[pd.DatetimeTZDtype, "ns", "UTC"]。
查看文档,阅读所有 Field 选项😋
自定义验证
有时需要添加自定义验证。Pandera 允许你注入列/索引检查(单列的自定义检查)和数据框检查(多个列之间的检查)。
import pandera as pa
from pandera.typing import Series
class UserModel(pa.DataFrameModel):
id: int = pa.Field(unique=True, ge=0)
username: str = pa.Field(str_matches=r"^[a-zA-Z0-9_]+$")
email: str = pa.Field(
str_matches=r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$"
)
is_active: bool
membership: str = pa.Field(isin=["premium", "free"])
creation_date: Annotated[pd.DatetimeTZDtype, "ns", "UTC"]
# column/index checks
@pa.check("username", name="username_length")
def username_length(cls, x: Series[str]) -> Series[bool]:
"""
Check username length is between 1 and 20 characters
"""
return x.str.len().between(1, 20)
@pa.check("creation_date", name="min_creation_date")
def min_creation_date(cls, x: Series[pd.DatetimeTZDtype]) -> Series[bool]:
"""
Check creation date is after 2000-01-01
"""
return x >= dt.datetime(2000, 1, 1, tzinfo=dt.timezone.utc)
# dataframe check
@pa.dataframe_check
def membership_is_valid(
cls, df: pd.DataFrame, name="membership_is_valid"
) -> Series[bool]:
"""
Check account age for free memebers is <= 30 days
"""
current_time = dt.datetime.now(dt.timezone.utc)
thirty_days = dt.timedelta(days=30)
return (df["membership"] == "premium") | (
(df["membership"] == "free")
& ((current_time - df["creation_date"]) <= thirty_days)
)
请记住,你正在处理整个列对象(Series),因此检查中的操作应向量化以提高性能。
其他配置
别名 当列名由于语言语法无法声明为 Python 变量时,Pandera 允许为列验证器设置别名,以匹配数据框。
class MyModel(pa.DataFrameModel):
alias_column: int = pa.Field(..., alias="Alias Column")
...
严格和强制 当strict选项设置为 true 时,它会强制验证的数据框仅包含在 Pandera DataFrameModel 中定义的列。另一方面,当启用coerce选项时,Pandera 会尝试将列数据强制转换为与模型的数据类型匹配。
class MyModel(pa.DataFrameModel):
...
class Config:
strict = True # defaul: False
coerce = True # default: False
coerce选项也可以在字段级别使用,方法是pa.Field(..., coerce=True)
懒惰验证 默认情况下,当验证检查未通过时,Pandera 会抛出错误。这可能会令人烦恼,因为它只显示遇到的第一个验证错误,并阻止其余数据的检查。
在某些情况下,最好让整个数据框架进行验证,并在一次运行中收集所有错误,而不是一个个修复它们并等待验证再次运行。前者是懒惰验证所做的。
df = pd.DataFrame(...)
Mymodel.validate(df, lazy_validation=True)
带有数据验证的机器学习生产管道

使用NightCafe生成的图像。
因为大多数机器学习管道是在 Python 中使用表格数据编码成数据框架结构进行训练的,所以Pandera是一个验证其输入和输出的强大工具。
# pipeline.py
class MLPipeline:
"""General ML Pipeline"""
def __init__(self, model_id: str):
self.model_id = model_id
def load_model(self) -> None:
...
def transform_data(self, df: pd.DataFrame) -> pd.DataFrame:
... # <- Potential invalid data error
return df_transform
def predict(self, df: pd.DataFrame) -> pd.DataFrame:
self.load_model()
df_transform = self.transform(df)
df['score'] = self.model.predict(df_transform) # <- Potential invalid data error
return df
我们希望避免模型因无效数据而抛出错误。那样意味着我们已经做了将模型加载到内存中和处理原始数据的所有工作,但结果是白费力气,浪费资源,还阻止了其余数据点的评估。
同样,如果模型的输出结构不正确,我们的后处理管道(将结果上传到数据库,通过 RESTful API 返回结果等)将会失败。
在使用 Pandera 定义验证模型后,我们可以利用其用于管道集成的装饰器来执行输入/输出验证。
# models.py
import pandera as pa
class InputModel(pa.DataFrameModel):
...
class PredictorModel(pa.DataFrameModel):
...
# OutputModel inherits all InputModel validation fields
# and also includes the score
class OutputModel(InputModel):
score: float = pa.Field(ge=0, le=1) # assuming model returns probab.
# pipeline.py
import pandera as pa
from .models import InputModel, PredictorModel, OutputModel
class MLPipeline:
"""General ML Pipeline"""
def __init__(self, model_id: str):
self.model_id = model_id
def load_model(self) -> None:
...
@pa.check_io(df=InputModel.to_schema(), out=PredictorModel.to_schema(), lazy=True)
def transform_data(self, df: pd.DataFrame) -> pd.DataFrame:
...
return df_transform
@pa.check_output(OutputModel.to_schema(), lazy=True)
def predict(self, df: pd.DataFrame) -> pd.DataFrame:
self.load_model()
df_transform = self.transform(df)
df['score'] = self.model.predict(df_transform)
return df
因为我们在机器学习管道中生成了一个中间数据框对象df_transform,所以最好也对它进行验证以防止错误。predict方法的输入不需要验证,因为它已经在transform_data中完成了。
处理无效行
我们不希望我们的管道因为某些数据点包含不正确的数据而中断。如果发生验证错误,策略应该是将有问题的数据点单独处理,并继续使用其余数据运行管道。管道不能停止!🔥
Pandera 模型有自动移除所有无效行的选项:
class MyModel(pa.DataFrameModel):
...
class Config:
drop_invalid_rows = True
然而,丢弃所有无效行而不记录它们可能是危险的。你需要知道为什么这些数据点无效,以便稍后可以向客户或数据工程师沟通错误的原因。
这就是为什么我不使用 Pandera 装饰器,而是创建我自己的验证辅助函数:
from typing import Tuple
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def log_pandera_errors(exc: pa.errors.SchemaErrors) -> None:
"""
Logs all errors from a SchemaErrors exception.
"""
for err_type, categories in exc.message.items():
for _, errors in categories.items():
for err in errors:
logger.error(f"{err_type} ERROR: {err['column']}. {err['error']}")
def handle_invalid(
df: pd.DataFrame, exc: pa.errors.SchemaErrors
) -> Tuple[pd.DataFrame, pd.DataFrame]:
"""
Handles invalid data in a DataFrame based on a SchemaErrors exception.
"""
log_pandera_errors(exc)
df_failure = exc.failure_cases
# Check for errors that cannot be resolved
# i.e. they aren't associated with a specific row index
nan_indices = df_failure["index"].isna()
if nan_indices.any():
error_msg = "\n".join(
f" - Column: {row['column']}, check: {row['check']}, "
f"failure_case: {row['failure_case']}"
for row in df_failure[nan_indices].to_dict("records")
)
raise ValueError(
f"Schema validation failed with no possibility of continuing:\n{error_msg}\n"
"The pipeline cannot continue 😢. Resolve before rerunning"
)
invalid_idcs = df.index.isin(df_failure["index"].unique())
df_invalid = format_invalid_df(df.loc[invalid_idcs, :], exc)
df_valid = df.iloc[~invalid_idcs]
return df_valid, df_invalid
def validate(
df: pd.DataFrame, model: pa.DataFrameModel
) -> Tuple[pd.DataFrame, pd.DataFrame]:
"""
Validates a DataFrame against a DataFrameModel and handles errors.
"""
try:
return model.validate(df, lazy=True), pd.DataFrame()
except pa.errors.SchemaErrors as ex:
return handle_invalid(df, ex)
输出强制某些错误并移除列id:
# Error output
ERROR:__main__:SCHEMA ERROR: UserModel. column 'id' not in dataframe. Columns in dataframe: ['username', 'email', 'membership', 'is_active', 'creation_date']
ERROR:__main__:DATA ERROR: username. Column 'username' failed element-wise validator number 0: str_matches('^[a-zA-Z0-9_]+$') failure cases: b%09
ERROR:__main__:DATA ERROR: email. Column 'email' failed element-wise validator number 0: str_matches('^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$') failure cases: ef.com
ERROR:__main__:DATA ERROR: UserModel. DataFrameSchema 'UserModel' failed element-wise validator number 0: <Check membership_is_valid> failure cases: c, ef.com, free, True, 2000-12-31 00:00:00+00:00
ValueError: Schema validation failed with no possibility of continuing:
- Column: UserModel, check: column_in_dataframe, failure_case: id
The pipeline cannot continue 😢. Resolve before rerunning
如果发生无法解决的错误且涉及整个列,则管道无法继续。
测试
最后但同样重要的是,Pandera 模型和模式还包括根据定义生成示例数据的方法。你需要安装 [hypothesis](https://hypothesis.readthedocs.io/en/latest/) 库才能使用它。
然而,在用一些示例测试后,我不推荐使用它。一旦你开始添加一些约束,生成合成数据的速度就会变得很慢,而且大多数时候生成的数据缺乏多样性(生成的数据无法覆盖整个限制空间,并且会重复)。我找到的最佳替代方法是为你想要测试的每个模型添加数据生成器——毕竟,在管道中需要验证的数据框架也不多——。
class UserModel(pa.DataFrameModel):
...
def sample(size: int = 10) -> pd.DataFrame:
"""Added method to generate valid test data manually"""
current_time = dt.datetime.now(dt.timezone.utc)
return pd.DataFrame(
{
"id": range(size),
"username": [f"user_{i}" for i in range(size)],
"email": [f"user_{i}@example.com" for i in range(size)],
"is_active": [True] * size,
"membership": ["premium"] * size, # All premium to pass checks
"creation_date": [current_time] * size,
}
)
结论
数据验证对于每个数据处理管道都至关重要,尤其是在机器学习中。Pandera 通过提供一种灵活且高效的基于模型的方法来简化数据框架中的数据验证工作。
使用 Pandera,你可以定义强制列类型、范围,甚至复杂条件约束的模型类。这样可以在数据管道的早期阶段轻松发现数据质量问题,确保数据在到达下一步之前符合预期标准。
通过将 Pandera 集成到机器学习管道中,你可以创建强大的数据检查,帮助防止错误并提高模型输出的可靠性。
最终在测试中使用的 pandera.DataFrameModel:
import pandas as pd
import pandera as pa
from pandera.typing import Series
from typing import Annotated
import datetime as dt
class UserModel(pa.DataFrameModel):
id: int = pa.Field(unique=True, ge=0, coerce=False)
username: str = pa.Field(str_matches=r"^[a-zA-Z0-9_]+$")
email: str = pa.Field(
str_matches=r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$"
)
is_active: bool
membership: str = pa.Field(isin=["premium", "free"])
creation_date: Annotated[pd.DatetimeTZDtype, "ns", "UTC"]
@pa.check("username", name="username_length")
def username_length(cls, x: Series[str]) -> Series[bool]:
"""
Check username length is between 1 and 20 characters
"""
return x.str.len().between(1, 20)
@pa.check("creation_date", name="min_creation_date")
def min_creation_date(cls, x: Series[pd.DatetimeTZDtype]) -> Series[bool]:
"""
Check creation date is after 2000-01-01
"""
return x >= dt.datetime(2000, 1, 1, tzinfo=dt.timezone.utc)
@pa.dataframe_check
def membership_is_valid(
cls, df: pd.DataFrame, name="membership_is_valid"
) -> Series[bool]:
"""
Check account age for free memebers is <= 30 days
"""
current_time = dt.datetime.now(dt.timezone.utc)
thirty_days = dt.timedelta(days=30)
return (df["membership"] == "premium") | (
(df["membership"] == "free")
& ((current_time - df["creation_date"]) <= thirty_days)
)
class Config:
strict = True
coerce = True
def sample(size: int = 10) -> pd.DataFrame:
"""Added method to generate valid test data manually"""
current_time = dt.datetime.now(dt.timezone.utc)
return pd.DataFrame(
{
"id": range(size),
"username": [f"user_{i}" for i in range(size)],
"email": [f"user_{i}@example.com" for i in range(size)],
"is_active": [True] * size,
"membership": ["premium"]
* size, # All premium to avoid date restrictions
"creation_date": [current_time] * size,
}
)
嗨,我是 Gabriel Furnieles,一名专注于人工智能、数据管道和 MLOps 的数学工程师。我希望你喜欢这篇文章并觉得它有帮助,如果是这样,请考虑关注我 Gabriel Furnieles,并订阅我的通讯,这样故事就会直接发送到你那里 👇
[## Gabriel Furnieles - Medium
在 Medium 上阅读 Gabriel Furnieles 的文章。他是专注于 AI 和机器学习的数学工程师。我偶尔在这里写作……
medium.com](https://medium.com/@gabrielfurnieles?source=post_page-----f07b0f845040--------------------------------)
数据估值——简明概述
理解数据的价值:挑战、方法和应用
·发布于Towards Data Science ·阅读时间 9 分钟·2024 年 12 月 12 日
--
ChatGPT 和类似的大型语言模型(LLM)是在庞大的数据量上进行训练的。OpenAI 等公司从互联网上抓取数据,收集书籍、文章和社交媒体帖子来训练它们的模型。很容易想象,一些文本(如科学文章或新闻报道)比其他文本(如随机推文)更为重要。几乎所有用于训练机器学习模型的数据集都是如此;它们几乎总是包含噪声样本,标签错误,或者包含误导性的信息。
尝试理解不同训练样本对于机器学习模型训练过程的重要性的过程被称为数据估值。数据估值也被称为数据归因、数据影响分析和代表点。这里有许多不同的方法和应用,我将在本文中讨论其中一些。

数据估值可视化。每个训练样本都会被分配一个重要性分数。(图像来源:作者。)
我们为什么需要数据估值?
数据市场
人工智能将在未来几年成为一个重要的经济因素,但它们对数据有着极大的需求。高质量数据对训练 AI 模型至关重要,因此成为了一种宝贵的商品。这就引出了数据市场的概念,买卖双方可以用钱交换数据。数据评估是定价数据的基础,但有一个问题:卖家不想在数据被买走之前将其公开,但对于买家而言,在没有看到数据之前,很难理解卖家数据的重要性。为了更深入地了解这个话题,可以参考论文“A Marketplace for Data: An Algorithmic Solution”和“A Theory of Pricing Private Data”。
数据投毒
数据投毒对 AI 模型构成威胁:恶意行为者可能会尝试以一种损害机器学习训练过程的方式篡改训练数据。这可以通过微妙地改变训练样本来完成,这种改变对人类几乎是不可见的,但对 AI 模型却非常有害。数据评估方法可以应对这一点,因为它们自然地会为有害样本分配一个非常低的重要性分数(无论它们是自然发生的,还是出于恶意)。
可解释性
近年来,可解释的人工智能(AI)获得了广泛关注。欧盟的人工智能高级专家组甚至呼吁将 AI 的可解释性作为创建可信赖 AI 的基础。理解不同训练样本对 AI 系统或 AI 系统的特定预测的重要性,对于解释其行为至关重要。
主动学习
如果我们能更好地理解机器学习模型中哪些训练样本更重要,那么我们就可以使用这种方法来获取对模型更具信息量的新训练样本。比如,你正在训练一个新的大型语言模型,发现葡萄牙语维基百科页面上的文章对你的 LLM 非常重要。那么,接下来的自然步骤就是尝试为你的模型获取更多这类文章。以类似的方式,我们在我们的论文“LossVal”中使用了数据评估方法,以获取更多的车辆碰撞测试数据,来改善汽车的被动安全系统。
数据评估方法概述
现在我们知道数据评估对不同应用有多么重要。接下来,我们将了解数据评估是如何工作的。正如在我们的论文中所描述的,数据评估方法大致可以分为四个分支:
-
基于再训练的方法
-
基于梯度的方法
-
基于数据的方法
-
“其他”
基于再训练的方法
基于重新训练的方法的常见方案是,它们通过多次训练机器学习模型来洞察模型的训练动态,最终揭示每个训练样本的重要性。最基本的方法(该方法由 Dennis Cook 于 1977 年提出)是通过去掉一个数据点重新训练机器学习模型,以确定该数据点的重要性。如果去掉该数据点后,模型在验证集上的表现下降,那么我们知道这个数据点对模型有害。反过来,如果去掉该数据点后,模型在验证集上的表现提升,我们知道该数据点对模型有益(或具有信息性)。对每个数据点重复这一过程,就能得到完整数据集的宝贵重要性分数。这种分数叫做留一法误差(LOO)。完全重新训练机器学习模型以处理每一个数据点非常低效,但对于简单模型和小数据集来说是可行的。
Data Shapley 扩展了这个概念,使用了Shapley 值。这个概念由Ghorbani & Zou和Jia et al于 2019 年同时发布。Shapley 值是博弈论中的一个构造,它告诉你联盟中每个玩家对最终回报的贡献。一个更贴近生活的例子是:假设你和你的朋友 Bob 与 Alice 在从派对回家的路上共享一辆出租车。Alice 住得离起点很近,而 Bob 住得远得多,你则介于两者之间。当然,如果你们三个人平摊最终的车费,那就不公平了,尽管你和 Bob 的路程比 Alice 远。Shapley 值通过考察所有可能的子联盟来解决这个问题:如果只有你和 Alice 共享出租车会怎样?如果只有 Bob 独自驾驶呢?依此类推。通过这种方式,Shapley 值可以帮助你们三个人按公平的方式分担最终的车费。这同样可以应用到数据上:通过在不同的训练数据子集上重新训练机器学习模型,公平地为每个训练样本分配“重要性”。不幸的是,这种做法效率极低:计算精确的 Shapley 值需要对你的机器学习模型进行超过 O(2ⁿ)次的重新训练。然而,使用蒙特卡洛方法可以更高效地近似计算 Data Shapley。
已经提出了许多替代方法,例如Data-OOB和平均边际效应(AME)。基于重新训练的方法在面对大规模训练集时会遇到困难,因为需要重复训练。使用重新训练计算的重要性分数可能不精确,因为神经网络中的随机性效应。
基于梯度的方法
基于梯度的方法仅适用于基于梯度的机器学习算法,如人工神经网络或线性回归与逻辑回归。
影响函数是统计学中的重要工具,由Dennis Cook提出,他在前文中已有提及。影响函数利用海森矩阵(或其近似)来理解如果某个训练样本被排除,模型性能将如何变化。使用影响函数时,无需重新训练模型。这种方法适用于简单的回归模型,也适用于神经网络。虽然计算影响函数效率较低,但已经提出了近似方法。
一些替代方法,如TraceIn和TRAK,跟踪机器学习模型训练过程中的梯度更新。它们可以成功利用这些信息来理解某个数据点对训练的重要性,而无需重新训练模型。梯度相似性是另一种跟踪梯度的方法,但它利用这些梯度来比较训练和验证梯度的相似性。
在我的硕士论文中,我研究了一种新的基于梯度的数据估值方法,利用损失函数中的梯度信息,称为LossVal。我们在标准的损失函数中引入了自加权机制,如均方误差和交叉熵损失。这使得在第一次训练时就能为训练样本分配重要性分数,从而无需进行梯度追踪、海森矩阵计算和重新训练,同时仍能达到最先进的结果。
基于数据的方法
上述所有方法都围绕着机器学习模型展开。这有一个优势,那就是它们能告诉你训练样本对特定应用场景和具体机器学习模型的重要性。然而,一些应用(例如数据市场)可能从“与模型无关”的重要性分数中受益,这些分数并不基于特定的机器学习模型,而是完全建立在数据之上。
这可以通过不同的方式完成。例如,可以分析训练集和干净验证集之间的距离,或使用体积度量来量化数据的多样性。
“其他”
在这一类别下,我将所有不适合其他类别的方法归类在一起。例如,使用 K-最近邻(KNN)可以更高效地计算Shapley 值,而无需重新训练。通过零掩码得到的子网络可以被分析以理解不同数据点的重要性。DAVINZ通过观察泛化边界来分析训练数据变化时的性能变化。Simfluence运行模拟训练并能够根据训练结果估计每个训练样本的重要性。强化学习和进化算法也可以用于数据估值。

一些其他数据估值方法的概述。(来自arxiv.org/abs/2412.04158的截图。)
你应该为你的应用选择哪种方法?
与大多数机器学习问题一样,数据估值中没有免费的午餐。要理解哪种方法最适合你的问题,你需要关注你使用的数据集、模型以及数据集中出现的噪声类型。数据估值的一个常见应用是识别数据集中的噪声样本。下图展示了不同方法在这方面的表现。首先,噪声被添加到数据集中,然后使用不同的方法来识别噪声样本。x 轴显示有多少样本是噪声样本,y 轴显示每种方法识别噪声样本的能力(越高越好)。
第一张和第二张图取自OpenDataVal 论文。他们在这两张图中都使用了逻辑回归作为分类任务的基础模型。在第一张图中,作者打乱了部分训练样本的标签。在这里,Data-OOB 的表现优于所有测试的其他方法。

在应用了标签噪声后的多个数据集上的比较,使用逻辑回归作为基础模型。越高越好。(来自OpenDataVal的截图。)
下一张图描绘了一个截然不同的画面:数据集和数据估值方法保持不变,但噪声不同!在这里,作者为部分训练样本添加了高斯噪声。现在,LAVA 和 KNN Shapley 表现出最佳的性能。

在多个数据集上进行的比较,应用特征噪声后,使用逻辑回归作为基础模型。得分越高越好。(截图来自OpenDataVal.)
如果我们使用多层感知器(MLP)代替逻辑回归模型,我们可以观察到类似的变化。现在,Data-OOB 的表现似乎大大下降。上面的图仅考虑了分类任务,但正如你在下面的图中看到的那样,许多方法在应用于回归任务时表现截然不同。

在多个数据集上进行的比较,使用 MLP 作为基础模型。得分越高越好。(截图来自arxiv.org/abs/2412.04158)
对于大多数实际问题,你无法准确知道你的样本和特征有多嘈杂。这使得决定应使用哪种数据估值方法变得困难。在最好的情况下,你可以尝试所有方法。否则,我可以给你一些指导:
-
如果你不知道将使用哪种机器学习模型:选择一种与模型无关的方法,如 KNN Shapley、LAVA 或 SAVA。
-
如果你使用简单的模型,如线性或逻辑回归:Data-OOB 会表现非常好。然而,这些简单的模型非常高效,你可以尝试使用计算开销较大但理论上有益的方法,如 Data Shapley。
-
如果你使用 MLP:LossVal 可能表现最佳。然而,深度学习的数据估值在计算上相当昂贵。如果在你的案例中不可行,你可以尝试使用与模型无关的方法。
请记住,本指南基于一组有限的数据估值方法,这些方法仅在嘈杂样本检测方面进行了评估。如果可能的话,尽量测试哪种方法最适合你的具体问题、数据集和模型。
当前的研究方向
目前,存在一些朝不同方向发展的研究趋势。有些研究正在将其他博弈论概念引入数据估值,如Banzhaf 值或Winter 值。其他方法尝试创建联合重要性得分,将学习过程中的其他方面包含在估值中,如学习算法。进一步的方法致力于研究隐私(数据无需公开)和个性化数据估值(通过使用元数据来丰富数据)。
结论
数据估值是一个日益发展的主题,本文没有提到许多其他的数据估值方法。数据估值是理解和解释机器学习模型的宝贵工具。如果你想了解更多关于数据估值的内容,我可以推荐以下文章:
-
训练数据影响分析与估计:一项调查(Hammoudeh & Lowd,2024 年)
-
机器学习中的数据评估:“成分”、策略和开放挑战(Sim & Xu 等,2022 年)
-
OpenDataVal: 一个统一的数据评估基准(Jian & Liang 等,2023 年)
随时通过 LinkedIn 与我联系
数据价值血统,终于有了意义?
最大化你数据的商业价值
·发布于Towards Data Science ·8 分钟阅读·2024 年 8 月 4 日
--

图片由作者提供(其中一些我已经读过!)
介绍
我一直对那些能够完美捕捉概念本质的词汇情有独钟。在一次前往日本的旅行中,我发现了一个词——Tsundoku。它指的是购书并将其堆积,却没有阅读的习惯。我立即爱上了这个词,因为像许多人一样,我也有买书多于能读完的习惯。有些书我最终会读到,而另一些则会悄悄堆积起来。
这些未读书籍的堆积,既让人觉得有趣,又带来一种奇异的满足感——它们象征着潜在的知识和收藏的乐趣。它们作为我们智力追求的见证,即使我们并不总是能实现这些追求。
作为Kindata.io的创始人,以及在数据价值方面为大公司提供咨询的顾问,我经常遇到一些让我想大喊“Tsundoku!”的情况。这些数据目录,就像一堆未读的书,充满了对数据的详细描述,而这些数据却闲置在那里,给人一种虚假的成就感。一本未读的书是浪费知识;同样,未被用于商业价值的数据也是浪费潜力。
没有人喜欢浪费,而我合作的专业人士也不例外。他们渴望看到自己的数据被充分利用。他们也理解,要实现这一目标需要改变思维方式。作为数据领域的从业者,他们也深知良好命名的概念的力量。我听到过很多用于描述这项工作与业务价值对齐的术语,但我不得不说,我已经喜欢上了一个正在流行的术语:数据价值血统。这个术语深深地触动了我,因为它完美地捕捉了我所提倡的内容。它突出了一个必要性,即通过确保数据团队所做的一切都与价值创造紧密相关,从而将闲置的数据转化为可操作的洞察。
乍一看,选择用一种方式命名概念而不是另一种方式似乎无关紧要。然而,命名概念是很有力量的,尤其是在你需要带领整个组织时。
在接下来的章节中,我们将深入探讨数据价值血统的具体细节。在定义这个概念及其包含的内容后,我们将探讨如何在数据组织中以务实的方式开始实施。
数据价值血统——一个正式的定义
数据价值血统是数据专业人士常用的两个术语的聚合:数据血统和数据价值。让我们回到这些概念的根本。
数据血统
数据血统由IBM 知识中心定义:
“数据血统是跟踪数据流动过程的过程,提供清晰的理解,说明数据的来源、如何变化以及最终在数据管道中的去向。”
数据价值
“数据价值是组织可以从其数据中获得的经济价值。这包括像收入创造和成本节约这样的有形利益,也包括像改进决策、提升客户体验和增强竞争优势这样的无形利益。”
这个定义是从行业中普遍接受的原则中提炼出来的,目前没有直接的权威来源。
调和两个不同的世界
前两个定义把我们引向了不同的方向。数据血统完全是关于理解依赖关系、偏差和潜在质量问题的。它提供了可解释性,并帮助数据工程团队修复断裂的管道。实际上,它是你数据供应链的正式描述。然而,对于大多数人来说,它停留在数据被应用程序有效使用的阶段。
数据价值则关注完全不同的东西:数据对组织业务目标的贡献。其潜在假设是,这个数据价值是可以正式衡量的。我们已经完全进入了战略、财务和商业案例的领域,而不是管道调试的世界。
现在我想象着尚-克劳德·范·达美正在两堆未读的书上做他那传奇的劈叉,并且非常认真地思考如何统一这两个概念……
数据价值血统的正式定义
这是数据价值血统的第一个正式定义。
“数据价值血统是一个随着时间推移,调和企业数据资产及其成本与它们对企业价值驱动因素的实际贡献的过程。”
到这里,我们有了正式的定义!我想强调这个定义中的几个方面,我认为它们是基础性的:
-
过程:这不仅仅是事后提供文档,而是确保组织在其标准的工作方式中,考虑到与业务价值之间的联系。
-
随着时间的推移:事物会发生变化,贡献对业务目标的影响也不例外。在某一时刻为价值提供元素的数据,仅仅是那时的价值,没有更多,也没有更少,并且没有任何保证这个价值会随着时间的推移保持不变。
-
企业数据资产:需要考虑的主要资产是数据产品(即产品化的数据集)和业务用例(如人工智能/机器学习或商业智能项目,为企业创造商业价值)。
-
它们的成本:创建并保持数据资产供消费使用是有成本的。部分成本来自云资源(这与财务操作实践相连),但更大一部分是数据工程师花费的时间,用于创建、维护、修复和发展这些资产。
-
实际贡献:我们希望尽可能地正式化对业务价值的贡献。关键是,这些测量应该由业务赞助人来执行,而不是在数据组织内完成。我们还必须在这些测量中保持务实,这只是手段,而非目的。
-
企业价值驱动因素:任何企业所重视的东西,无论是财务方面(如成本节省、创造新收入)还是非财务方面(如客户满意度、可持续性目标等)。
正如你所见,数据价值血统虽然本质上是一个简单的概念,但涵盖了非常广泛的范围。不可避免地,这涉及将组织内具有截然不同背景、使命和关切的人员汇聚在一起。让我们探讨如何务实地推动事情朝着正确的方向发展。
一种务实的方式
一些变革管理的考虑
跨越组织边界和思维方式的思考与行动并非易事。惯性是一个强大的力量,而在大公司中,有时唯一能推动进展的办法就是给自己戴上“眼罩”。
通过我亲自推动或观察的多个变革管理项目,我发现有三个关键的成功因素是始终有效的:
-
意义和价值:当变革“看起来合情合理”时,更容易被采纳。这最终会导致一种情感:“我们为什么一直没这么做?”当新的方法感觉直观且明显更好时,抵抗情绪会显著减少。
-
现有实践的自然延伸:如果变革对当前实践的偏离最小,变革更有可能成功。通过让人们继续做他们之前在做的大部分事情,并持续地进行小的变革,你可以将新的实践顺利地融入他们的日常工作中。
-
即时感知价值:为了让任何变革被接受,个体需要看到立即的好处。如果那些以前困难或难以触及的事情变得容易和自然,人们更有可能热情地采纳新实践。
好消息是,通过数据价值谱系实现这三个目标是相对容易的。
-
意义和价值:不需要进行大量研究,就可以发现资源配置和数据生产者与消费者之间的协作中存在着大量低效之处。同时,数据价值谱系的概念对不同角色而言也非常自然。
-
现有实践的自然延伸:你并不会从根本上改变所做的事情,只是在原有工作之上添加一层薄薄的额外层,使得那些原本难以建立的联系变得可能。你可以利用现有的数据治理和财务控制投资,并激活这些资源,专注于创造业务价值。
-
即时感知价值:通过连接各个点,你可以快速识别数据中的潜在未开发价值,提升数据资源的业务吞吐量,改善数据发现流程,并促进数据民主化。
开始入手
我发现在组织内部推广数据价值谱系方法时,尤其有用的是从三个基本问题开始:什么、如何、为什么。虽然听起来简单,但实际上大多数组织会将这三个问题混在一起,最终无法得到明确的答案。
什么指的是企业重视的数据倡议。在我们执行的项目中,这涵盖了传统分析项目(仪表板、报告等)以及数据科学/人工智能倡议(预测模型、推荐引擎、聊天机器人等)。我们用来描述一个能够提供切实商业价值的数据驱动项目的术语是商业用例。
如何涵盖了多个方面,但从数据价值谱系的角度来看,最重要的一个方面是数据源的选择。越来越多的组织,受到数据网格原则的启发,开始采用数据产品或产品化数据集的概念。与商业用例不同,在这种术语下,数据产品并不直接贡献商业价值,但它们会带来维护成本。
为什么是关于对业务价值的贡献。你期望每个商业用例如何贡献一个或多个价值驱动因素?一旦项目交付并进入维护阶段,这种贡献是否能够持续?
一旦我们清楚地理解了这三个问题,我们就可以开始记录基本构建块:
-
价值驱动因素及其度量指标
-
商业用例组合
-
数据产品目录
数据价值血统的第一个实际层面是定义这三个层级之间的关系。

连接点 — 由 Gilles Lenoble 为 Kindata.io 创作的图片
让我们举一个非常简单的例子:
你希望通过数据驱动的方法优化能源消耗。商业用例(降低能源成本)从两个数据产品(公司能源消耗和公用事业账单及费用)中获取数据。它为两个商业驱动因素做出贡献:成本降低和可持续性。

一个数据价值血统的例子 — 作者通过 Kindata.io 的截图制作
数据产品、商业用例和价值驱动因素之间的箭头是数据价值血统的骨干。它们将三个基本问题连接起来。你会注意到这些箭头是双向的:
-
当你从数据产品到价值驱动因素进行导航时,你可以清楚地看到数据产品的实际有用性。每个单独的数据产品确实是许多类似消费链条的一部分,你可以随时轻松了解所生成的价值。如果生成的价值未达到你的预期,你可以采取纠正措施,如内部推广、重构或停用。
-
当你从商业驱动因素到数据产品进行导航时,你可以获得一个最新的自上而下的关于数据驱动贡献的视图。同样,如果该视图未能符合你的预期,你可以采取战略决策,从投资数据供应链、交付特定的新商业用例或提升现有用例的使用等方面入手。
总结
数据价值血统是一个强大的概念,它提供了一种结构化的方法,确保你组织中的每一条数据都能为商业价值做出贡献。通过协调数据资产、任务和商业结果,你可以最大化数据项目的影响力。
这也是一个很好的名称,可以将数据、业务和财务控制组织的人们团结在一起。
不要满足于创建几十个未被充分利用的数据产品,陷入自我欺骗的价值生成幻觉中。避免像禅修般沉浸于未使用的数据堆积中,类似于积读书风格。相反,采取行动,通过数据价值血统充分利用数据的全部潜力,确保每一个努力都能直接为商业价值的生成做出贡献。
数据可视化入门:吸引眼球的可视化操作手册
使用 Plotly 进行引人注目的可视化交流的实用技巧
·发表于 Towards Data Science ·阅读时间:15 分钟·2024 年 2 月 5 日
--

图片由 DALL-E 3 生成
在上一篇文章中,我们讨论了如何选择最适合你任务的可视化类型。我们确定了七种不同的使用场景(时间序列、名义比较、偏差、排名、部分与整体、频率分布和相关性),以及最适合它们的图表类型。毫不奇怪,在大多数情况下,你可以使用基本的图表类型,如折线图、柱状图或散点图。
选择最合适的可视化类型是基础。然而,这并不是你在进行数据可视化时需要记住的全部。
让我们回归基础。可视化的主要目标是将信息传达给观众。这就是为什么我们需要考虑观众如何感知我们的图表。在实践中,观众的感知和理解取决于许多小细节。
在这篇文章中,我将分享数据可视化的一些关键方面。我们将通过三个步骤来获取更加清晰、锐利和智能的可视化效果:
-
减少噪音,避免观众困惑或分心,
-
为了集中观众的注意力,增加高亮效果,
-
增加上下文信息,以提供所有必要的背景知识,确保清晰理解。
我将使用 Plotly,因为它是我进行数据可视化的主要工具。
除非特别说明,以下所有图表示例均基于合成数据集。
第一步:减少噪音
我们可能会想把所有的信息都包含在可视化中,例如,为图表上的每个点添加标签。在这些时刻,我们通常出于好意——给观众一个全面的视图。
然而,人脑的工作方式是不同的。你在可视化中添加的每一个元素都会增加认知负担。人们的思维能力是有限的,因此不能浪费这份认知资源在不必要的元素上。添加过多的细节可能会让观众完全失去兴趣,因为他们可能会觉得你的图表比实际更加复杂。这种感知的复杂性可能会让观众感到害怕,因为他们可能不愿意花时间和精力去理解它。
我们可以参考一些数据可视化领域的经典书籍来思考这个问题。爱德华·塔夫特(Edward Tufte)是数据可视化领域的先驱。在他的书《定量信息的可视化展示》中,他介绍了数据墨水比(来源)的概念。
图形中大部分的墨水应该用于展示数据——墨水会随着数据的变化而变化。数据墨水是图形中不可擦除的核心部分,是响应所表示数字变化而安排的非冗余墨水。
我们的目标是最大化图表中有意义元素(或墨水)的比例。为此,我们可以从图表中去除杂乱(或噪音),以减少感知上的认知负担。杂乱是指那些存在于图表中但没有提供任何附加理解的图形元素。
不幸的是,我们工具中的默认设置有时并不能帮助我们制作清晰的可视化。因此,我首先建议你更改 Plotly 中的默认模板——这将帮助你简化图表。
默认模板是plotly。它与 seaborn 风格相似(你可以在画廊中查看),包括背景颜色和网格线。在我看来,它给图表增加了太多噪音。我更喜欢一个轻量级的模板,叫做simple_white。
你可以在图表上对比这两种风格,感受它们的差异。

作者图表
使用下面的代码可以更改你所有 Plotly 可视化的模板。你可以查看其他内置模板,甚至在文档中学习如何创建自己的自定义模板。
import plotly.io as pio
pio.templates.default = 'simple_white'
将模板更改为simple_white后,你的所有图表将自动变得更加简洁。然而,这只是我们走向无杂乱数据可视化之旅的开始。由于每个图形元素都会增加认知负担,因此值得考虑它们是否必要。图表中的每个元素都应该是你经过深思熟虑的决定,而不是工具的默认行为。
在许多情况下,图形元素并没有增加对理解的帮助,因此我们可以(且应该)去除它们。让我们看几个这样的例子。
如果你创建一个只有一个数据系列的条形图,Plotly 仍然会显示图例。然而,我们可以在不失去任何信息的情况下去除它,并通过 y 轴标题提供度量的上下文。

作者图表
让我们隐藏图表上的图例。
# create chart
fig = px.bar(df, text_auto = ',.6r', width = 600)
# hide legend
fig.update_layout(showlegend = False)
这让我感到惊讶,但确实有些情况下你不仅可以去除图例,还可以去除其中一个轴。看下面的两张图表:我们为每个条形添加了标签,确保观众可以轻松解读并比较基于条形长度的数值。因此,x 轴不再是必要的。理想情况下,我们应该将关于使用的度量的一些背景信息添加到图表标题中——我们稍后会讨论如何做。

作者图表
要隐藏其中一个轴,我们需要更改其可见性。
fig.update_xaxes(visible = False)
# you can similarly hide y-axes using fig.update_yaxes(visible = False)
我们已经学会了如何调整 Plotly 的默认设置,使图表更加简洁明了。然而,这并不是唯一需要注意的事项——事实上,我们自己常常会添加噪音和杂乱。让我给你展示一些我经常看到的嘈杂图表的例子。
分析师喜欢数字(至少我是)。因此,我们经常希望向观众展示这些数字,比如过去几个月我们有多少客户。结果常常是这样的图表。

作者图表
在所有这些数字的杂乱中,你完全忽略了数据中的趋势和洞察力。我会采取以下措施来解决这个问题:
-
不用说,我会去掉这些标签。如果你的观众需要知道确切的数字,只保留最重要的(例如,只保留最近一个月或两个月的数据)。
-
将你展示的数值四舍五入是一个好习惯,例如,使用
184.1K代替184051。在大多数情况下,观众并不会在乎是 184 051 还是 184 063 个客户。 -
此外,如果你希望将观众的注意力集中在数据的趋势上,我建议你去掉标记,仅保留线条。
另一个诱惑是让你的可视化更加多彩。除非颜色有特定作用,比如编码某些数据或突出显示最值得注意的部分,否则请避免这么做。我们稍后会讨论如何明智地使用颜色。同时,你可以看看下面的例子,观察一下首先吸引你眼球的是什么,以及你需要花费多少精力去理解底层数据。当我看第一张图时,我感到有些困惑,一直在思考这些颜色是什么意思,为什么每个条形有所不同。

作者图表
此外,这张图表告诉我们,使用过多的重点(在我们这个例子中是鲜艳的颜色)是行不通的——我们只是被分散了注意力,无法专注于某个部分。
我们已经学会了如何从图表中去除噪音。经过这一步,我们得到了中立的可视化效果。它们就像一块画布。接下来的步骤是战略性地加入重点。
第 2 步:添加重点
明智地使用重点颜色可以引导观众的注意力,并强调主要信息。人们通常首先会注意到较亮或较暗的颜色。然而,重要的是要记住,你不能突出显示所有内容。相反,你应该将观众的注意力集中在数据的一个或两个关键方面。
你还可以建立一个重点的层次结构,最大程度地强调主要信息,并将不那么重要(但仍然必要)的部分推到背景中。这可以帮助你避免分散注意力,但仍然保留所有必要的背景信息。我们将在下面看到此类方法的示例。
如果你想了解数据可视化中哪些元素会吸引注意力,试试以下这个简单的测试:闭上眼睛,睁开眼睛,然后观察最初吸引你注意的是什么。另一种方法是将你的可视化图展示给别人,并请他们评论他们的思考过程。
颜色
在我看来,颜色是驱动观众注意力的最强大工具。这就是为什么我想详细讨论它。让我们从一个例子开始。看看下面的可视化图。你首先注意到的是什么?你认为作者想通过这个图表告诉你什么?

图表由作者提供,数据来自 StackOverflow 调查
你可能已经开始查看 SQL 并将其与其他语言进行对比。在 我之前的文章 中,我使用了这个图表来阐明以下观点:
根据年度 StackOverflow 调查,SQL 仍然是世界上最受欢迎的编程语言之一。对于专业开发者来说,SQL 排名前三(仅次于 Javascript 和 HTML/CSS)。超过一半的专业人员使用它。令人惊讶的是,SQL 甚至比 Python 还要流行。
我使用了灰色和亮蓝色之间的对比来集中你的注意力在我所提到的 SQL 上。如果我现在制作这个可视化图,我还会让标题更粗,以使其更加突出,因为它是一个有意义的背景。
让我们将其与完全灰色中性的版本进行对比。在没有任何视觉提示的情况下,你将花费更多的时间和精力来查看所有数据。

图表由作者提供
希望你现在能看到颜色的所有潜力。让我们学习如何在 Plotly 中使用颜色
我们将从一个像上面示例中的条形图开始。我用更亮的颜色突出显示了那些转化率低于阈值的部分。为此,我根据转化率值定义了颜色列表,并将其作为颜色传递给 Plotly,用于线条和标记。我还指定了希望标签显示在条形图外部,并通过调整透明度使颜色变得更加淡化。
# defining colors based on conversion value
colors = list(map(
lambda x: 'silver' if x >= 40 else 'purple',
conv_df.conversion.values
))
# creating default plot
fig = px.bar(conv_df, text_auto='.2f', labels = {'value': 'conversion, %'})
# updating colors
fig.update_traces(marker_color=colors, marker_line_color=colors,
textposition='outside', marker_line_width=1.5, opacity=0.9)
# hiding legend
fig.update_layout(showlegend = False)
# updating range to add some space on the top
fig.update_yaxes(range = [0, 70])

图表由作者绘制
让我们稍微讨论一下如何定义颜色。在上面的示例中,我使用了预定义的 SVG 颜色"silver"和"purple"。你可以在这里找到预定义颜色的完整列表。
如果你想要更多自定义选项,可以将颜色作为 HEX 代码传递。例如,你可以使用品牌颜色为你的演示文稿增添公司风格。
获取 HEX 代码最简单的方法是截取你的界面截图,上传到颜色选取器(我通常会搜索“在线颜色选取器从图片”),然后查找所有需要的代码。例如,我目前工作的公司 Wise 的品牌色之一是明亮的绿色,HEX 代码为#9FE870。

图片由作者提供
由于我在图表中经常使用品牌色,我将它们保存在本地配置文件中,这样可以通过名称轻松访问。
colours = {
"light_green": "#9FE870",
"dark_green": "#163300",
"light_blue": "#7CECF1",
"dark_blue": "#000146",
"light_orange": "#FFC828"
}
现在,我希望你不会因为不理解如何告诉 Plotly 你想要的颜色而卡住。所以,让我们继续下一个例子,使用线性图,并学习其他指定颜色的方法。
如果你希望精确手动定义每个部分的颜色,可以使用color_discrete_map。当我需要在多个图表中保持一致的颜色编码时,我经常使用这种方法。如果在一个图表中你把 Android 用蓝色表示,iOS 用橙色表示,而在另一个图表中反过来,观众可能会感到困惑。所以,值得注意这些细节。
在下面的图表中,我用紫色突出显示了增长的 iOS 受众,并使用灰色阴影表示其他平台,因为我不希望你关注它们。
colormap = {'Android': 'silver', 'Windows': 'gray', 'iOS': 'purple'}
px.line(ts_df, color_discrete_map = colormap)

图片由作者提供
如果我想展示不同的群体且不关心每个群体的特定颜色,我可以仅在color_discrete_sequence参数中指定颜色的顺序。
px.area(df, color_discrete_sequence = px.colors.qualitative.Prism)

图表由作者绘制
我使用了一个预定义的 Plotly 调色板来设置颜色,但你也可以通过字符串列表指定自定义颜色。以下是 Plotly 中可用的调色板:
-
离散色调调色板主要包括分离色,这些颜色在需要区分不同群体时非常有用。
-
在连续颜色尺度中,你可以找到许多适合顺序类别的颜色调色板(例如,客户成熟度为
"< 1 month"、"1–3 months"、"4–6 months"、"6–12 months"和"> 12 months")。
当你需要使用颜色编码值时,连续尺度也可以使用,例如热力图。
px.imshow(
gmv_df.values,
x = gmv_df.columns,
y = gmv_df.index,
color_continuous_scale='pubugn'
text_auto=',.6r', aspect="auto",
labels=dict(x="age group", y="region", color="GMV in GBP")
)

图表由作者提供
当你使用颜色时,需要记住有些人是色盲。最常见的困难是区分红色和绿色的不同阴影。因此,尽量避免这两种颜色的组合,或者同时使用其他视觉提示(如文本或图标)。这样可以帮助你不失去部分观众。
绿色和红色的阴影常用于表示事物的正面和负面方面(例如,在热力图中显示更高和更低的转化率)。你也可以使用蓝色和橙色阴影代替。
大小
另一种突出显示某物的方法是调整大小。我们通常会认为较大的东西更重要。例如,为了突出某条线,我们可以增加其宽度。
在 Plotly 中,我们需要使用图形对象来调整线条宽度。
import plotly.graph_objects as go
fig = go.Figure()
fig.add_trace(
go.Scatter(
mode='lines', x=ts_df.index,
y=ts_df.Android, showlegend=True,
name = 'Android', line = {'width': 1}
)
)
fig.add_trace(
go.Scatter(
mode='lines', x=ts_df.index,
y=ts_df.Windows, showlegend=True,
name = 'Windows', line = {'width': 1}
)
)
fig.add_trace(
go.Scatter(
mode='lines', x=ts_df.index,
y=ts_df.iOS, showlegend=True,
name = 'iOS', line = {'width': 3}
)
)
fig.show()

图表由作者提供
现在,iOS 线条相比其他平台更为突出。我们还可以通过加粗或斜体字体来吸引观众的注意力。让我们给图表添加标题,并突出其中的中心部分。为此,我们可以使用 HTML 标签 <b>。
fig.update_layout(
title = '<b>Monthly sessions:</b> sky-rocketing trend for iOS'
)

图表由作者提供
第三步:讲故事
我们已经学会了如何突出重点,现在可以进入最后一部分——讲故事。我们之前已经讨论过,背景对于理解信息至关重要。所以,在这一部分,我们将讨论如何将背景添加到你的图表中。
为了增加更多的上下文,你可以使用最简单的方法:指定标题和标签。这样可以防止观众询问他们究竟看到了什么。你可以使用 title 参数来设置图表标题(就像我们之前做的那样),并用 labels 来覆盖默认的轴标签和图例标题。
px.line(ts_df, width = 600,
labels = {'value': 'sessions', 'os': 'platform', 'month_date': 'month'},
title = 'Monthly sessions over time')

图表由作者提供
详细的标题是一个好习惯,因此标题可能会变得很长。Plotly 是一个强大的可视化工具,但仍有改进的空间。例如,它无法处理过长的图表标题——标题的尾部将不可见。
然而,Plotly 足够灵活,我们可以自己修复这个问题。如果行长度超过阈值(70 个字符),我们将使用 <br> HTML 标签在单词之间添加换行符。让我们来做这个。
def format_string_by_lines(s, line_limit = 70):
lines = []
curr_line_words = []
curr_line_length = 0
for word in s.split(' '):
if curr_line_length + len(word) > line_limit:
lines.append(' '.join(curr_line_words))
curr_line_words = []
curr_line_length = 0
curr_line_words.append(word)
curr_line_length += len(word)
lines.append(' '.join(curr_line_words))
return ' <br> '.join(lines)
chart_title = '<b>Monthly sessions over time:</b> we can see sky-rocketing trend on iOS while Android and Windows are pretty stagnant.'
px.line(ts_df, width = 600,
labels = {'value': 'sessions', 'os': 'platform', 'month_date': 'month'},
title = format_string_by_lines(chart_title))

图表由作者提供
此外,我们可能还想展示一些度量的数值。我们已经讨论过,标记所有数据点会导致过多的杂乱,但显示最后的数值似乎是合理的。
我将展示在 Plotly 中实现这一目标的两种方法:使用text字段和注释功能。我通常偏好使用text,但这还是相当主观的。
让我们从text选项开始。首先,让我们查看原始数据集。

图片来自作者
现在,让我们将text_val字段添加到数据集中,该字段等于上个月的值,对于其他月份则为空。我还指定了格式化方式,将数字显示为千位数,以去除不必要的细节。
raw_ts_df['text_val'] = list(map(
lambda v, d: '' if d != raw_ts_df.month_date.max() else '%.1fK' % (v/1000),
raw_ts_df.value,
raw_ts_df.month_date
))
我们准备好创建我们的可视化了。我将新创建的text_val列作为可视化的text参数,更新mode为"lines+text"并指定middle right文本位置。我还移动了图例,以免干扰我们的注释。
fig = px.line(raw_ts_df, x = 'month_date', y = 'value',
color = 'platform', text = 'text_val',
width = 1000, height = 500,
labels = {'value': 'sessions', 'os': 'platform', 'month_date': 'month'},
title = '<b>Monthly sessions</b>')
fig.update_traces(textposition="middle right", mode='lines+text')
fig.update_layout(legend=dict(orientation="h", yanchor="bottom",
y=0.05, xanchor="right", x=1))

作者绘制的图表
另一种标记数值的方式是使用注释功能。首先,让我们计算每个平台的最后一个值并格式化文本。
annotations = raw_ts_df.groupby('platform', as_index = False)\
.aggregate({'value': 'last', 'month_date': 'last'})\
.rename(columns = {'value': 'y', 'month_date': 'x'})
annotations['text'] = annotations.y.map(lambda v: '%.1fK' % (v/1000))
annotations = annotations.drop('platform', axis = 1)
让我们添加更多我们将在注释格式化中使用的参数,并将数据框转换为我们可以传递给 Plotly 的列表。
annotations['showarrow'] = False
annotations['xanchor'] = 'left'
annotations['yanchor'] = 'middle'
annotations_list = annotations.to_dict('records')
现在,我们可以通过类似的方式使用注释来创建可视化,并获得相同的结果。所以,选择使用哪种方式完全取决于你。
fig = px.line(raw_ts_df, x = 'month_date', y = 'value',
color = 'platform',
width = 1000, height = 500,
labels = {'value': 'sessions', 'os': 'platform', 'month_date': 'month'},
title = '<b>Monthly sessions</b>')
fig.update_layout(annotations = annotations_list,
legend=dict(orientation="h", yanchor="bottom",
y=0.05, xanchor="right", x=1))
垂直或水平线也可以为你的观众提供所需的上下文。例如,你可以突出显示重要的日期,比如营销活动的启动日期,或者展示你的指标的 SLA。让我们在图表中添加一条垂直线。
你可以通过使用fig.add_vline轻松实现这一点。不幸的是,Plotly 存在一个 BUG,无法与日期一起使用。不过,我们可以使用解决方法:看起来有点奇怪,但它有效。
fig.add_vline(
x=datetime.datetime.strptime("2023-09-01", "%Y-%m-%d").timestamp() * 1000, line_width=3, line_dash="dash",
line_color='black', annotation_text="Marketing <br> campaign ",
annotation_position="top left"
)

作者绘制的图表
如果你想突出显示图表上的整个区域,可以添加水平线甚至矩形。你可以在文档中找到更多详细信息。
总结
在这篇文章中,我们已经介绍了数据可视化的关键方面:
-
去除不必要的噪声,以避免分心,
-
使用强调来吸引观众的注意力,通过颜色和大小,
-
添加上下文,帮助你的观众理解你的信息。
非常感谢你阅读这篇文章。如果你有任何后续问题或评论,请在评论区留言。
参考资料
本文受到了有关数据可视化的优秀书籍《Storytelling with Data: A Data Visualization Guide for Business Professionals》的极大影响,作者是 Cole Nussbaumer Knaflic。
基本机器学习算法数据可视化备忘单
显示机器学习结果的指导图表。
·发布于Towards Data Science ·8 分钟阅读·2024 年 3 月 5 日
--

图片由Charlesdeluvio提供,来源于Unsplash
备忘单可以作为指导方针,给我们一些初步的想法。就个人而言,我有时会使用一些备忘单,发现它们非常有帮助,尤其是在我刚开始学习机器学习算法时。
除了理解和应用,检查获得的结果是一个重要的步骤,帮助我们了解或观察数据的变化。在这种情况下,使用数据可视化是一个不错的选择,因为它可以直观地展示算法的结果。
尽管有多种图表可供选择,但选择合适的图表可以帮助我们有效地展示结果。因此,我认为制作一个快速选择图表的备忘单是一个好主意。最终的结果就是下面展示的基本机器学习数据可视化备忘单。
嗯哼!!

基本机器学习数据可视化备忘单 — 由作者创建。
在继续之前,请考虑到备忘单中推荐的数据可视化仅仅是一些初步的快速想法。可能会有这些图表不适用的情况。接下来,我将引导你...
使用大型语言模型和图像生成模型进行数据可视化生成——结合 LIDA
LIDA 库概述,包括如何入门、示例和未来的考虑事项
·发布于Towards Data Science ·阅读时间 12 分钟·2024 年 6 月 25 日
--
最近我发现了LIDA——这是一个与语法无关的库,旨在使用大型语言模型(LLMs)和图像生成模型(IGMs)自动生成数据可视化和信息图。LIDA 支持多种大型语言模型提供商,例如 OpenAI 和 Hugging Face。在这篇文章中,我将提供该库的高层次概述,展示如何入门,列出一些示例,并分享我对在数据可视化和商业智能(BI)领域中使用 LLMs 和 IGMs 的思考与考虑。

概述¹
创建数据可视化通常是一项复杂的任务——涉及数据处理、编码和设计技能。LIDA 是一个开源库,通过减少开发时间、错误数量和整体复杂性,自动化数据可视化创建过程。
LIDA 包含 4 个模块,如下图所示。每个模块在这一多阶段可视化生成方法中都有独特的作用。

图像来源:Victor Dibia,来自LIDA GitHub
-
SUMMARIZER:该模块将数据转换为自然语言的摘要。摘要分为两个阶段实现。在第一阶段,基础摘要生成,应用规则从数据集中提取属性,使用 pandas 库生成一般统计数据,并从每列中提取一些样本。在第二阶段,摘要丰富,通过 LLM 或用户通过 UI 丰富来自基础摘要阶段的内容,加入数据集和字段的语义描述。¹
-
GOAL EXPLORER:该模块根据 SUMMARIZER 模块生成的摘要创建数据探索目标。该模块生成的目标表示为 JSON 数据结构,包含问题、解决该问题的可视化和理由。¹
-
VIZ GENERATOR:该模块由 3 个子模块组成(代码框架构造器、代码生成器和代码执行器)。该模块的目标是根据 GOAL EXPLORER 模块中的数据可视化目标规范,或根据用户创建的新可视化目标,生成、评估、修复、过滤并执行可视化代码。¹
-
INFOGRAPHER:该模块利用 IGM 根据 VIZ GENERATOR 模块的输出,以及可视化和风格提示,创建风格化的信息图。¹
LIDA 利用了 LLM 的两项关键能力:
-
语言建模 —— 这些功能有助于生成语义上有意义的可视化目标。¹
-
代码编写(即代码生成) —— 这些功能有助于生成用于创建数据可视化的代码,随后这些代码作为输入用于图像生成模型(如 DALL-E 和 Latent Diffusion)生成风格化的信息图。¹
此外,LIDA 工具中也使用了提示工程。
“提示工程是设计、优化和完善与 AI 语言模型进行交流的提示的过程。提示是输入到 AI 系统中的问题、陈述或请求,用于引发特定的响应或输出。”²
LIDA 中结合提示工程的几种方式包括使用提示来创建和定义六个评估维度,以及用户能够指定样式提示来格式化可视化。
本文后续的示例展示了本节中提到的部分功能,您可以在此处了解更多关于 LIDA 的信息。
开始使用
有两种方式可以开始使用 LIDA —— 通过 Python API 或通过混合用户界面。此部分展示如何使用 LIDA 库中可选的捆绑 UI 和 Web API,从本地机器开始使用用户界面。
注意:在此示例中使用的是 OpenAI。如果想使用其他 LLM 提供商,或者使用 Python API,请查看 GitHub 文档 这里。
步骤 1:安装必要的库
在计算机上安装以下库。
pip install lida
pip install -U llmx openai
步骤 2:创建一个变量来存储您的 OpenAI API 密钥
要创建 OpenAI API 密钥,请导航到您的个人资料 > 用户 API 密钥,然后选择+ 创建新密钥。

作者图片:获取 API 密钥
复制 API 密钥。在一个新的终端窗口中,将 API 密钥保存为名为OPENAI_API_KEY的变量。
export OPENAI_API_KEY=""
步骤 3:启动 UI Web 应用
从终端窗口启动 LIDA UI Web 应用,使用以下命令。
lida ui --port=8080 --docs
在 Web 浏览器中,导航到“localhost:8080”, 然后就可以开始了!选择Live demo或Demo标签来查看 Web 应用。

作者图片:访问 LIDA Web 应用
Web 应用示例
本部分展示了一些使用来自 Kaggle 的美国票房前 10 电影数据集³的示例和提示。
步骤 1:选择可视化库/语法
在创建任何数据可视化或总结之前,选择一个可视化库。有 4 个选项可供选择:Altair、Matplotlib、Seaborn 和 GGPlot。首先,选择Seaborn —— 这是一个基于 Matplotlib 的 Python 数据可视化库。

作者图片:选择可视化库/语法
提示: 不确定从哪个库开始?选一个,之后可以更换!即使在上传数据后,您仍然可以更改可视化库/语法。如果您在加载数据后更换库并看到错误,快速刷新应该可以解决问题。
步骤 2:查看生成设置
右侧有一个选项可以修改生成设置。在这里,您可以选择模型提供商、用于生成的模型,并调整其他字段,如最大令牌数、温度和消息数量。目前,可以保持默认设置。

作者图片:查看生成设置
步骤 3:上传数据
在设置基本参数后,上传数据集。点击或拖动文件将数据集上传到 Web 应用。或者,您可以使用提供的样本文件之一。

作者图片:上传文件
提示: 如果在尝试上传文件时遇到错误,请检查所选模型提供商的使用情况和计费权限。访问权限问题可能导致 LIDA 中的数据文件上传问题。此外,终端窗口将显示任何可能对排除问题有帮助的错误消息。
注意:请小心是否/何时切换回 LIDA 主页——这将导致丢失当前显示中的工作!
步骤 4:查看数据摘要
数据概述部分提供了数据集的描述,以及数据集中各个字段的总结,包括列类型、唯一值数量、列描述和示例值。这一输出是之前提到的总结模块的结果。
以下图片展示了美国票房前 10 名电影数据集的数据概述。其中包含了整个数据集的描述,以及数据集中所有 9 列的内容。

作者提供的图片:关于“美国票房前 10 名电影数据集”的数据概述
选择查看原始摘要?来以 JSON 字典格式查看数据概述。

作者提供的图片:查看原始摘要
第 5 步:审查目标探索
本部分展示了根据上传的数据集自动生成的目标或假设列表。每个目标都以问题的形式呈现,并包括可视化将展示的内容的描述。这一输出是之前提到的目标探索模块的结果。
在这里,你可以浏览不同的目标,并选择一个在可视化生成部分进行可视化展示。

作者提供的图片:关于“美国票房前 10 名电影数据集”的目标探索结果
第 6 步:可视化生成
根据之前部分选择的目标目标探索,你将看到相应的可视化结果,以及用于生成该可视化的 Python 代码。
以下图片展示了目标“电影上映月份分布是怎样的?”的结果。左侧是可视化结果,一个垂直条形图,右侧是用于生成该可视化的 Python 代码。这段代码可以复制用于外部使用。

作者提供的图片:关于“电影上映月份分布”的可视化生成输出
另外,你也可以输入一个新的可视化目标,超出目标探索部分列出的目标。
例如,以下图片展示了“预算平均值最大的前 5 个电影类型”的输出。

作者提供的图片:关于“预算平均值最大的前 5 个电影类型”的可视化生成输出
注意: 选择目标右侧的生成按钮将刷新可视化结果。这可能会导致一些细微的变化,例如颜色方案的变化。
第 7 步:可视化修改与评估
一旦生成了可视化,有四个标签可以使用:优化、解释、评估和推荐。

作者提供的图片:可视化生成部分下的优化、解释、评估和推荐标签
第一个标签,修改,使用自然语言命令修改图表。
下图显示了使用修改标签对图表“电影发行月份分布是什么?”所做的修改。图表通过自然语言命令修改,将月份按时间顺序排列,显示为水平条形图,并为每个条形添加计数值。

作者提供的图片:在“修改”标签中输入自然语言命令后的可视化生成输出
提示:确保你的样式提示清晰、简洁且具体!否则,你可能会得到扭曲的可视化效果、意外的结果,或者你的自然语言命令可能无法生成图表。记住,当写作提示时,垃圾进,垃圾出!编写提示是一门艺术,因此,编写有效的样式提示可能需要一些精细调整。
如果你需要在几个样式提示未按预期生成结果后重置可视化,请使用清除聊天历史按钮来重置可视化。

作者提供的图片:清除聊天历史按钮
第二个标签,解释,提供关于如何创建可视化的文本解释——涉及数据转换、图表元素、代码等。

作者提供的图片:图表解释示例
第三个标签,评估,从 6 个维度评估生成的图表:缺陷、转换、合规性、类型、编码和美学。每个维度有一个 5 分制的评分,并提供了为什么给出该评分的描述。

作者提供的图片:图表评估示例
右下角有一个按钮可以自动修复图表,按钮名称为自动修复图表,如上图所示。如果你同意图表评估中提供的建议,那么这是一个快捷的修复方式!下图显示了在根据美学评估自动修复图表后更新的图表。

作者提供的图片:选择自动修复图表后的更新条形图
第四个标签,推荐!,生成类似的图表及相应的代码片段——这些不与初始目标绑定。这对于集思广益、思考从数据中获取的其他图表或洞见非常有用。

作者提供的图片:图表推荐示例
思考与考虑事项
本节强调了在数据可视化和商业智能领域使用 LLM(大语言模型)和 IGM(智能生成模型)时需要考虑的几个方面——包括但不限于自动生成数据可视化。
评估指标
LIDA 使用两个指标——可视化错误率(VER)和自评可视化质量(SEVQ)。
VER 显示了生成的可视化中有多少导致了代码编译错误,以百分比形式表示。
SEVQ 使用大型语言模型(LLMs),例如 GPT-4,来评估生成的可视化质量。它通过对 6 个维度的平均评分来进行评估——代码准确性、数据转换、目标符合性、可视化类型、数据编码和美学。这些维度每个都根据向 LLM 提供的提示生成评分(如需查看使用的提示草图,请阅读论文此处)¹。你可能还记得,这些维度出现在 LIDA 网络应用程序的评估标签中。
这些指标评估可视化生成,并提出了一个重要观点——在评估 LLMs 和 IGMs 用于数据可视化和商业智能(BI)工具时,需要牢记这个问题。随着这一领域的不断发展,从业人员在为其组织实施 LLMs 和 IGMs 进行数据可视化和 BI 解决方案时,需要考虑以下问题——我们未来需要考虑哪些指标?需要建立哪些流程?如何确保输出是准确、可信、可解释和受治理的?
部署 — 环境设置考虑事项
在组织内部使用 LLMs 和 IGMs 进行数据可视化时,有几个方面需要考虑与部署相关的事项。
使用这些模型进行数据可视化,或者一般使用这些模型,可能需要大量资源,具体取决于模型大小、数据集大小和用户数量等因素。如果没有正确和高效的规划,这可能会导致高昂的成本。因此,确保部署正确的基础设施,以确保顺利实施是非常重要的。针对特定用例测试更精细的 LLMs,也有助于减少整体的资源消耗。
此外,在使用 LLMs 和 IGMs 进行数据可视化时,数据安全和治理也非常重要。不论使用哪种工具,确保数据在工具中是安全的,并且在使用过程中得到治理,都是至关重要的。
图表解释
如前面示例所示,LIDA 中生成的图表解释专注于图表创建的细节——包括数据转换、图表元素和生成的代码。虽然这对于开发人员使用数据集创建图表很有帮助,但这种上下文对业务用户没有太大帮助。业务用户和分析师更需要包含数据点洞察的图表解释,而不仅仅是图表元素和结构。
无论个人的角色如何,图表旁的自然语言文本都可以帮助提供数据可视化的关键洞察。目前已经有一些自然语言生成(NLG)工具能够集成到商业智能(BI)工具中。随着 LLMs、IGMs 和数据可视化解决方案的发展,看看这个领域如何持续演变将会非常有趣。
之前没见过结合 BI 的自然语言生成(NLG)吗?查看这个 GitHub 页面 快速了解一下。
展望未来,考虑最终用户并理解什么样的 LLM + IGM + 数据可视化解决方案适合该受众群体,基于他们的目标和兴趣,是至关重要的。
使用提示进行图表设计
之前的示例展示了如何使用 LLM 和 IGM 生成数据可视化图表。虽然这些图表是自动生成的,但它们仍然需要修改以确保设计良好。通常,你不能将第一个图表保持不变。这需要使用 LIDA 中的自动修复功能,虽然它能够捕捉到部分变化,但并不能覆盖所有应做的修改,同时还需要样式提示,这要求一定的数据可视化领域经验和知识。
这些样式提示由用户用自然语言输入,可以包括诸如修改图表标题、改变图表颜色或排序图表值等请求。
使用这些样式提示可以帮助用户节省时间,在开发图表时减少编写代码的时间,以及减少调试和格式化代码所需的时间。
然而,随着数据可视化生成中提示语的引入,理解什么构成一个好的提示语变得同样重要。清晰、简洁且具体的提示语将比模糊的请求产生更好的结果。不清楚的请求可能导致差的可视化效果或意外的结果。
这并不意味着我们不应该在创建数据可视化时利用提示语——而是指出,在开始时可能会有一个学习曲线。找出正确的提示语可能需要一些测试,并且需要明确表达的命令。
总的来说,LIDA 是一个很好的开源工具,适合开始学习 LLM、IGM 和数据可视化领域的一些最新进展。查看 Victor Dibia 的完整论文 这里,并试用网页应用程序或 Python API,进一步了解 LLM 和 IGM 如何改变我们创建数据可视化的方式。
Payal 是一位数据与 AI 专家。在业余时间,她喜欢阅读、旅行,并在 Medium 上写作。如果你喜欢她的作品, 关注或订阅 她的列表,永不错过任何故事!
以上文章是个人观点,不一定代表 IBM 的立场、战略或意见。
参考文献
[1]: Dibia, Victor. LIDA:一种使用大型语言模型自动生成语法无关的可视化和信息图表的工具, 微软研究院, 2023 年 5 月 8 日, aclanthology.org/2023.acl-demo.11.pdf.
[2]: Vagh, Avinash. “NLP 和提示工程:理解基础知识。” DEV 社区, DEV 社区, 2023 年 4 月 6 日, dev.to/avinashvagh/understanding-the-concept-of-natural-language-processing-nlp-and-prompt-engineering-35hg.
[3]: 影片,Will’s。“2000–2023 年美国票房前十名电影。”Kaggle,2024 年 3 月 20 日,www.kaggle.com/datasets/willsfilms/top-10-films-at-the-us-box-office-2000-2023.(CC0)
数据可视化技术,医疗数据分析 — 第三部分。
从有效的条形图到像 3D 可视化这样的陷阱。
·发表在Towards Data Science ·阅读 32 分钟·2024 年 11 月 22 日
--

比较 3D 和 2D 散点图 by Leo Anello
概览
我们现在正在进行一个专注于数据可视化技术的项目。把这个项目看作是特征工程技术:现实世界医疗数据挑战 — 第一部分 & 第二部分的延伸。
基于这些结果,我们将进行全面的数据探索、分析,现在重点特别放在通过可视化进行数据分析上。
我将介绍各种图表,并提出一些次要问题,以及关于何时使用特定类型的图表的提示,具体取决于您想传达的信息。
[## GitHub - Anello92/feature-engineering-techniques-python
通过在 GitHub 上创建帐户为 Anello92/feature-engineering-techniques-python 项目做出贡献。
最终,您将获得构建真正有效的数据可视化的基本知识,这些技能在日常任务中将非常宝贵。准备好了吗?
目录
-
Python 库与设置
-
初步探索: 形状…
数据仓库设计模式
我如何在我的新数据仓库中组织一切
·发表于 Towards Data Science ·阅读时间 11 分钟·2024 年 1 月 29 日
--

图片来源:Lidia Nikole 于 Unsplash
最近,我为我的新数据项目需要一个数据仓库工具。这个故事讲述了我是如何从零开始构建它,并在其中组织一切的。设计一个数据平台不是一项简单的任务,现代的数据仓库解决方案通常是其架构的核心。它提供了强大的数据治理功能、使用 ANSI SQL 简化的数据查询以及增强的数据建模能力。由于数据源数量庞大且所需转换复杂,组织内部的一切——即数据环境、测试、命名规范、数据库、架构和表格——可能是一项具有挑战性的任务。这个故事可能对希望学习高级数据仓库技术的初学者和中级用户有帮助。对于有经验的数据从业者,我希望能讨论他们对数据仓库设计的看法,以及他们通常如何在其中组织一切。
设计数据平台
作为一名数据工程师,我每天都在设计数据管道。这正是现代数据平台的核心内容,它必须具有成本效益、可扩展性,并且在长期内易于维护。为数据密集型应用设计管道总是具有挑战性的,而现代数据仓库(DWH)的目标就是简化并增强这一过程……
数据仓库,重新定义
重新思考数据仓库:为什么即使在现代数据仓库(MDW)和湖仓模型之外,也有必要进行重新定义
·发表于Towards Data Science ·8 分钟阅读·2024 年 7 月 30 日
--

图片来自Ruchindra Gunasekara在Unsplash上的分享
好吧,我再次使用了“重新定义”——在我关于需要重新定义数据工程的文章中我已经用过了。天哪,你可能会问,他还想重新定义什么呢?
嗯,我认为当今数据架构中有很多值得深入研究的内容。而批判性地审视当前数据仓库定义的动机,源于我一位读者的问题。
我写了一篇三部分系列文章,讨论数据网格中的挑战与解决方案。文章中描述的改进型数据网格,作为数据收集方法(如数据仓库:传统、现代以及数据湖/湖仓等变体)的一种替代架构。然而,我强调我们仍然需要数据仓库,作为许多应用程序之一,向数据网格贡献信息。

数据网格中的挑战与解决方案
查看列表3 个故事


数据库数据转换(面向数据工程师)
初学者的高级技术
·发表于 Towards Data Science ·14 分钟阅读·2024 年 2 月 17 日
--

使用 Kandinsky 生成的 AI 图像
在这个故事中,我想引发一个关于如何进行数据转换的讨论。无论是数据库、数据仓库还是报告解决方案,我们都基于数据模型执行数据转换,但我们如何组织它们呢?我想谈谈你使用的现代数据转换工具。我们将涉及模块化方法、调度和数据转换测试的一些细节。在本文的结尾,我将提供一个示例应用,用于执行数据建模任务,并具备数据血缘和自文档化功能。我非常想知道你对此的看法。
我见证了许多不同方式的数据转换。在我超过十五年的大数据和分析领域职业生涯中,我构建了多种不同设计模式的数据管道,我相信还有更多方式。这就是我如此喜欢技术世界的原因。它提供的无限可能性简直令人惊叹。
你使用什么操作系统来管理你的数据仓库?
现代数据转换工具
现代数据转换工具,也称为数据建模工具或数据仓库(DWH)操作系统,旨在简化 SQL 数据操作任务,以...
数据流架构
健康与健身数据管道的(不那么)简短历史:第二部分
关于衍生数据视图和最终一致性
·发表于Towards Data Science ·阅读时长 19 分钟·2024 年 10 月 15 日
--
欢迎来到 第二部分 ,我们关于公共健康与健身数据管道的成长三部曲。
在本章中,我们将后端系统重新构想为一个分布式状态机,并探索实现一致性的艺术——带有函数式风格。
第一部分简要回顾
健康与健身数据管道的(不那么)简短历史:第一部分
数据管道的演变
在第一部分中,我们观看了SmartGym成长为(2.1 版),一个集成的健康与健身平台,流式传输、处理和保存来自一系列健身器材传感器和医疗设备的数据。这些数据提供了洞察力,帮助用户更积极地掌控个人的健康与健身。

SmartGym 肩推
随着我们的系统从单纯保存和检索数据发展到响应现实世界的事件,我们的架构必须反映这一范式转变——从请求驱动到事件驱动。
一个事件驱动的摄取管道
有两个管道维持着系统的运行:
-
流式处理管道:数据从传感器中持续流式传输,经过处理并存储到缓冲区。
-
保存管道:当用户结束一个会话时,缓存的数据会被处理并保存到数据库中,作为表示用户会话(一次锻炼)的记录。

流式处理和保存管道
事件驱动架构是把双刃剑,既引入了秩序,也带来了混乱。最终,我们看着它演变成了一台运转良好的机器,能够驯服它的复杂性。
第二部分:演变继续
在这一部分中,我们将探讨系统的下三个版本,每个版本以不同的方式增强用户的锻炼体验:
-
v3.0:将健身作为一种个性化体验
-
v4.0:将健身作为一种集体体验
-
v5.0:将健身作为一种个性化-集体体验
但首先,让我们认识一下本文的主角!

神奇的黑盒
无论是分布式、事件驱动的,还是其他形式的,我们都可以把 SmartGym/SENSEI 的后台系统看作一个神奇的黑盒。

输入:我们将新信息输入到这个黑盒中——例如:用户信息、传感器数据。
状态转换:这些新信息根据特定的逻辑与现有状态进行交互,产生新的内部状态。
输出:我们可以随时查询其内部状态,以检索相关信息——例如用户的锻炼信息。
神奇的黑盒是确定性的:如果你有两个相同的黑盒,状态和逻辑完全相同,按相同的顺序输入相同的内容,两个黑盒最终会得出相同的内部状态。
黑盒内部
如果我们把它拆开来看,我们会发现其中没有什么真正神奇的东西——只有一个由一堆数据视图和摄取管道组成的数据流架构。

事实来源(实线)、派生数据视图(虚线)和流式/保存管道逻辑(箭头)
数据视图主要有两种类型:
1. 事实来源(实线)——例如:用户、传感器流
-
新的数据首先写入这里。这些是原始的、权威的数据——通常以规范化的方式精确表示一次。
-
系统的状态是这些事实来源和状态转换逻辑的函数——它反映了随时间推移、改变系统状态的事件序列。
2. 派生数据(虚线)——例如:锻炼记录
-
这些数据是从其他视图中的现有数据中处理出来的,通常涉及反规范化、聚合或转换。它是为高效的未来查询而预计算的。
-
派生数据是冗余的,实际上是“复制”了现有的信息;如果丢失,它总是可以从原始数据源重新派生出来。
从真实数据源派生数据视图的过程称为 物化,这是一个由摄取管道中的工作者处理的确定性任务。
魔法黑盒子内部的所有 状态 都被封装在这些 数据视图 中,而 摄取管道 — 即物化过程背后的机械运作 — 保持 无状态。
请注意,每当真实数据源发生变化时,派生的数据必须重新派生。否则,状态转换不完整,魔法黑盒子将处于 不一致 状态。

v2.1 数据流架构
在前面的例子中,我们向魔法黑盒子输入了:
-
通过我们的 CRUD API 获取 用户详情
-
通过事件流传输的 传感器遥测 (流式管道)
这些输入作为真实世界实体或事件的来源,反映了实际情况。
从这两项信息中,我们推导出一个单一的 锻炼 会话记录,进入 保存管道。
现在,你可能会对“魔法黑盒子先生”在向你解释看似常识的东西时感到不悦。但请耐心听他讲,因为他将证明自己在本文中是一个有用的抽象概念。
v3.0 — 指标、仪表盘、洞察:将健身转变为个性化体验
随着流式管道和保存管道不懈地将数据摄取到系统中,我们的数据库现在储存着大量的用户和锻炼记录。我们能够通过分析趋势、分组、平均值和总计,提供有意义的宏观洞察,服务于我们的用户和利益相关者。

SmartGym 产品指标仪表盘原型
用户洞察和健身指标
在这里,SmartGym 成为 “每个公民的首选健身伴侣” 的愿景开始逐渐成形。除了在我的锻炼过程中提供实时反馈、回顾我的历史表现并告诉我做得多么出色之外,一个勤勉的健身伴侣还会提供可量化的指标,用来衡量我随时间推移的表现提升。

SmartGym 用户锻炼洞察页面
通过利用最近的锻炼数据和用户信息 —— 比如通过 SmartGym 体重秤捕捉的身体数据 —— 我们可以估算各种表现指标,包括:
-
1RM(最大单次重复重量) 用于基于重量的锻炼(例如腿举)
-
每分钟最大重复次数 用于体重训练(例如俯卧撑)
-
VO2 最大值 或 MET(代谢当量) 用于有氧运动(例如跑步机)
推导产品和健身指标通常涉及去规范化和聚合记录,这在内存使用、数据库读取和网络吞吐量方面可能非常消耗资源。
由于每次用户加载仪表盘时都不需要执行这些操作,因此我们需要一个预计算的中间数据表示,准备好进行查询和可视化 —— 另一个 派生数据视图 用于 用户健身指标!
周期性处理管道
让我们回到我们的魔法黑盒子。

v3.0 数据流架构
随着新用户和锻炼记录不断流入,用户健身指标—一种衍生的数据视图—需要根据其依赖的上游数据视图的变化不断更新。
为了解决这个问题,我们决定定期重新计算用户健身指标,接受这些指标可能会滞后几个小时的事实。
现在,我们的摄取管道包括一个定时任务服务,该服务根据预设的时间表在周期性管道内安排批量处理作业,从而确保及时更新并避免系统过载。

流式、保存和周期性管道
v4.0 — 游戏化:将健身作为一种集体体验
从个人到社区的转变
保持健康是一项艰苦的工作。但通过适当的竞争和集体的艰难时光,这可以成为一种超越生活本身的体验。想象一下,如果每一个重复动作、每一组锻炼、每一节锻炼课程都为更伟大的目标做出贡献,那会是什么样?
推出运动排行榜和健身挑战。
运动排行榜
排行榜展示了本月努力最多的用户——通过跑步距离、举重重量等数据进行衡量。
哇,这个功能可真是让一些人感到自豪!一些常去健身房的人开始把他们随机生成的用户名改成像“Beefy”或“Armstrong”这样的称号。对于很多人来说,查看排行榜成了他们进入健身房后的第一件事,同样也是每次锻炼结束后的仪式,带着新获得的自信昂首离开。

SmartGym 排行榜
类似于我们计算产品和用户健身指标的方式,排行榜数据会定期批量更新,数据来源于用户个人资料和他们的历史锻炼记录。

健身挑战系统
与健身房管理团队合作,我们发起了一项健身挑战,并与新加坡国庆日等特殊时期同步进行。

SmartGym 健身挑战在我们的 Tampines Hub 举行
每天,用户会收到一个挑战,要求他们在加重器械上完成一定次数的重复,或在有氧器械上锻炼一定时间,并因他们的努力获得奖励。
这启动了一系列多样化的健身挑战,每个挑战都有不同的游戏玩法,涉及持续时间、锻炼类型、强度、连续次数等多种变化。

SmartGym 健身挑战用户界面
可配置的规则:规则引擎和语法树
本质上,健身挑战是由管理员指定的一组独特锻炼要求。通过将用户的锻炼历史与这些要求进行对比,我们可以评估他们在挑战中的进展和完成状态。

规则语法树:表示一组胸推/腿推/跑步机锻炼
我们并没有用一大堆 if-else 语句来应对每个健身挑战的变体,而是通过将这些逻辑规则表示为语法树来将业务逻辑外部化。在运行时,规则引擎解析这棵树,并根据用户的实际锻炼历史进行评估,从而追踪他们的挑战进度。

语法树的运行时评估
当程序管理员修改健身挑战的参数时,他们实际上是在直接更新底层规则语法树。这个相同的数据结构在后台规则引擎和前端规则配置页面之间共享,从而确保了一致性和管理的简便性。

SmartGym 健身挑战配置页面
按需处理管道
让我们重新审视一下我们的神奇黑箱。

v4.0 数据流架构
通过规则引擎从训练和用户健身挑战数据派生的用户健身挑战结果,每当其所依赖的上游数据视图发生变化时——例如每次用户完成一次锻炼时,都需要重新计算。
在我们充满热情的用户群体中,这些健身挑战是一种荣耀和荣誉的象征。如果他们在完成一组训练后没有立即看到更新的挑战结果,他们会感到困惑和沮丧。因此,我们不能依赖周期性批量处理用户健身挑战结果;训练数据视图的每次变化必须立即传播。
为了实现这一点,我们通过引入变更数据捕获机制扩展了摄取管道,添加了一个服务,该服务持续监听相关数据视图中的变化,使用内置数据库触发器或变更流。这一按需管道触发了下游派生数据视图的一连串更新。
在这种情况下,一个按需工作者实现规则引擎的逻辑,实时评估用户健身挑战结果。

揭开我们最新的直列四缸发动机,其包含流处理、保存处理、定期处理和按需管道
我们的摄取管道各个阶段的回顾:
-
流处理:负责将实时传感器数据流摄取、处理并存储到缓冲区
-
保存处理:负责将来自流缓冲区的数据整合、处理,并保存到数据库中,作为代表单次锻炼或用户会话的记录。
-
定期处理:负责定期预计算派生的数据视图
-
按需处理:负责立即传播来自上游数据视图的更新到派生数据视图
v5.0 — 推荐:将健身体验作为个性化-集体的体验
如果我们能为健身挑战增添一些个人化的元素呢?
NSFIT x SmartGym
2021 年底,来自新加坡军队的一个团队描述了他们的困境:每年,军人必须达到特定的健身基准。如果达不到标准,他们将被加入一个结构化的训练项目,称为 NSFIT。然而,这些训练课是有限时段的,需提前报名,并且需要工作人员来促进和监控进度。考虑到当时正在进行的疫情和社交距离措施,集结军人进行集体课程变得不可行。
使用 SmartGym 健身挑战系统,军人可以根据自己的时间安排进行训练——无需工作人员在每一节课上都跟随。只需要工作人员验证训练是否完成并符合标准即可。

基于健身档案的跑步机强度实时推荐
但这里有个转折:军人们的身形、体型和健身水平各不相同。一个适合所有人的健身挑战是行不通的。他们需要的是能够从他们的现有状况出发的东西,才能将他们的健身推向新的高度。
那么,为什么不在策划用户健身挑战之前加入推荐步骤呢?通过利用我们已经计算的健身数据(例如在 v3.0 中),我们可以定制他们随后的训练课程强度。
我们的个性化健身挑战现在遵循三个关键步骤:

步骤 1 — 个人档案 使用历史训练数据,我们为每个用户制定健身水平档案。
为了进一步优化这一过程,我们可以将我们的个人资料方法扩展到简单的启发式方法之外,结合机器学习方法提取更复杂的特征——从而产生新的派生数据视图。
步骤 2 — 推荐 从一个通用的挑战模板开始(包括诸如地点、总训练次数、参与者组、开始/结束日期等信息),推荐引擎将这个骨架模板扩展成一个适合每个用户健身档案的规则语法树。
为了实现更大的个性化,一位领域专家可以手动微调挑战要求,提供一种超越算法推断的专业视角。
步骤 3 — 评估 一旦个性化参数被嵌入到规则语法树中,评估可以在训练保存后按需触发,甚至可以在实时传感器流中进行评估,并显示在前端控制台上。

实时个性化健身挑战评估

v5.0 数据流架构
数据流范式
魔法黑盒重访
在文章早些时候,我们提到过魔法黑盒由数据视图组成,其中包含状态,以及一个无状态的数据摄取管道。
为了从魔法黑盒中获得可靠的输出,这些数据视图必须是一致的,这一点通过在数据摄取管道中的确定性物化序列来实现。
系好安全带,准备好,因为我们将深入探讨一致性和确定性——数据流的潜在动力。
一致性的挑战:一个必要的恶性问题
乍一看,创建一个包含所有原始数据细节的大型数据视图似乎更简单。只有一个数据源时,一致性是隐含的。然而,尽管维护一致性复杂,我们依然需要衍生数据视图,原因有几个:
数据多态性
数据可以以多种形式表示——在不同的组合和多个粒度级别上——每种形式都有其独特的用途。
例如,事实证明,用户并不关心他在 2020 年 9 月做胸推时,第 2 组的第 3 次重复动作是否完全伸展——在这个实时窗口过后,低级别的原始细节变得越来越不相关,而高级别的衍生洞察变得更有价值。
为了避免必须假设数据在未来如何使用和表示——原始数据更好,即寿司原则。
读写性能
通过这种方式,我们将写入模型与潜在的读取模型范围解耦,并通过一系列物化阶段弥合差距。这种分离通常被称为命令和查询责任分离(CQRS)。

拥有物化路径为一条数据提供了空间和时间——让它演变并发现其不同的面貌,从而实现:
-
更快更简单的写入:通过将数据处理和复杂数据模型推迟到后续阶段,从而缩短写入路径。
-
高效灵活的读取:通过预先计算不同的衍生视图,缩短读取路径。
一致性的基础
通过将写入模型指定为推理的权威真理来源,更容易实现一致性——而无需处理多个权威系统尝试达成共识的复杂性。
有时,原始数据增长得过快。例如,每秒 1 条消息的跑步机传感器,多个健身房的话,一天内传感器流就可能积累数百万条消息。

锻炼记录取代传感器流成为新的权威数据视图
当传感器流增长到难以承受的规模时,我们可以将锻炼记录视为传感器流的“有损压缩”,清除处理后的传感器流,并将衍生出的锻炼记录提升为新的权威数据源。
由于物化的单向链条仍然始于单一的真理来源,我们保持了我们的一致性基础。
反脆弱性:故障恢复与应用演化
派生视图提供了弹性。如果出现错误导致输出损坏,我们可以回滚到先前的版本,并重新运行物化过程,从而确保数据的准确性。
派生视图还支持应用的渐进式演变。你可以引入新的数据视图,而不删除或重构旧的视图,保持它们作为同一数据的独立视图,并且如果出现问题,还可以选择回退。

非破坏性的推导逻辑演变
在系列第一部分中,我们看到发布-订阅(pub/sub)模式(通过分发交换机)使得在不干扰现有管道或要求上游修改的情况下,能够轻松扩展系统功能,达到即插即用的效果。
敏捷开发和构建抗脆弱系统(即那些通过每次修复错误或添加新特性而变得更强)的关键是恢复和演变的便捷性。发布/订阅模式和派生视图所实现的解耦使这一点成为可能。
一致性和控制的艺术
接下来,让我们剖析数据流架构的一致性和控制流的本质。
广义来说,分布式系统可以通过两种一致性级别——强一致性或最终一致性;以及两种控制流类型——协调式(集中式)或编排式(分散式)进行分类。

一致性级别与控制流之间的关系
强一致性保证每次读取都反映最新的写入数据。它确保所有数据视图在更改后立即并准确地更新。强一致性通常与协调式相关联,因为它通常依赖于中央协调器来管理跨多个数据视图的原子更新——要么一次性更新所有,要么一个都不更新。这种“过度工程”可能是某些系统所必须的,尤其是在轻微的差异可能带来灾难性后果的情况下,例如金融交易,但在我们的场景中并不需要。
最终一致性允许数据视图之间暂时存在差异,但只要给定足够的时间,所有视图最终将收敛到相同的状态。这种方法通常与编排式相结合,其中每个工作单元独立且异步地响应事件,而不需要中央协调器。
数据流架构的异步和松耦合设计的特点是数据视图的最终一致性,通过编排式的物化逻辑实现。
这样做是有好处的。
好处:在系统层面
对部分故障的韧性:编排的异步性对于组件故障或性能瓶颈更加健壮,因为中断被局部控制。相反,协调可能会在系统中传播故障,通过紧耦合放大问题。
简化的写路径:编排还减少了写路径的责任,从而减少了代码的表面积,减少了错误破坏真相源的可能性。相反,协调将使写路径变得更加复杂,随着不同数据表示的增多,维护难度也会越来越大。
优势:在人类层面
编排的去中心化控制逻辑允许不同的实现阶段独立且并行地开发、专业化和维护。
决定性:事件驱动的工作伦理(重访)
电子表格理想
一个可靠的数据流系统类似于电子表格:当一个单元格发生变化时,所有相关单元格会立即更新——无需手动操作。
在理想的数据流系统中,我们希望实现相同的效果:当上游数据视图发生变化时,所有相关视图无缝更新。就像电子表格一样,我们不应担心它是如何工作的,它应该自然地完成。
但在分布式系统中确保这种级别的可靠性远非易事。网络分区、服务中断和机器故障是常态而非例外,且数据接收管道中的并发性只会增加复杂性。
由于数据接收管道中的消息队列提供了可靠性保证,确定性重试可以使瞬时故障看起来像是从未发生过。为了实现这一点,我们的数据接收工作者需要采纳事件驱动的工作伦理:
纯函数没有自由意志
在计算机科学中,纯函数表现出决定性,意味着它们的行为是完全可预测和可重复的。
它们是短暂的——此时此刻存在,下一刻便消失,生命周期结束后不再保持任何状态。赤裸裸地来,赤裸裸地去。从它们诞生时刻刻画的不可变消息中,它们的遗产就已经注定。它们总是为相同的输入返回相同的输出——一切如命中注定般展开。
这正是我们希望我们的数据接收工作者具备的特性。
不可变输入(无状态性) 这个不可变的消息封装了所有必要的信息,消除了对外部可变数据的依赖。本质上,我们是通过值而非引用将数据传递给工作者,这样处理一个消息在明天时得到的结果将与今天相同。
任务隔离
为了避免并发问题,工作者不应共享可变状态。
工作者内部的过渡状态应当被隔离,就像纯函数中的局部变量一样——不依赖共享缓存进行中间计算。
确保任务的独立性同样至关重要,确保每个工作者处理的任务不会共享输入或输出空间,从而允许并行执行而不发生竞争条件。例如,通过特定的user_id对用户健身分析任务进行作用域限定,因为输入(锻炼记录)和输出(用户健身指标)与唯一的用户相关联。
确定性执行 非确定性可能轻易潜入:使用系统时钟、依赖外部数据源、基于随机数的概率/统计算法等,都可能导致不可预测的结果。为防止这种情况,我们将所有“动态部分”(例如随机种子或时间戳)直接嵌入不可变的消息中。
确定性排序 使用消息队列的负载均衡(每个队列有多个工作者)可能导致消息处理的顺序错误,特别是在消息重试时,若后续消息已处理完毕。比如,用户健身挑战结果的评估顺序错误,出现从 50%到 70%再回到 60%的情况,而实际上它应该是单调递增的。对于需要顺序执行的操作,如插入记录后通知第三方服务,顺序错误的处理可能会破坏这种因果依赖关系。
在应用层,这些顺序操作应该要么在单个工作者上同步运行,要么拆分成多个独立的顺序物化阶段。
在数据摄取管道层面,我们可以为每个队列分配一个工作者,以确保串行处理,直到重试成功为止。为了维持负载均衡,你可以使用多个队列,并通过一致性哈希交换来路由消息,路由的依据是路由键的哈希值。这实现了类似于 Kafka 的哈希分区键方法的效果。
幂等输出
幂等性是一种属性,多次执行同一段代码应该始终产生相同的结果,无论它执行多少次。
例如,一个简单的数据库“插入”操作不是幂等的,而一个“如果不存在则插入”操作是幂等的。
这确保了你得到的结果就像工作者只执行了一次一样,无论实际重试了多少次。
警告:请注意,与纯函数不同,工作者并不会“返回”一个对象(从编程的角度来看)。相反,它们会覆盖数据库的一部分。虽然这看起来像是副作用,但你可以将这种覆盖视为类似于纯函数的不可变输出:一旦工作者提交结果,它就反映了一个最终的、不可更改的状态。
数据流递归
客户端应用中的数据流
传统上,我们认为 Web/移动应用是无状态的客户端,它们与中央数据库进行通信。然而,现代的“单页应用”框架改变了这种格局,提供了“有状态”的客户端交互和持久化的本地存储。
这将我们的数据流架构扩展到后端系统的范围之外,涵盖了多种客户端设备。可以将设备上的状态(即“模型-视图-控制器”中的“模型”)视为服务器状态的派生视图——屏幕显示的是设备本地状态的物化视图,它反映了中心后端的状态。
推送协议,如服务器推送事件和 WebSockets,将这个类比进一步扩展,使得服务器可以主动将更新推送到客户端,而不依赖于轮询——实现从端到端的最终一致性。

v5.0 数据流架构(扩展版)
事实上,这种实时同步正是我们如何在前端控制台中评估个性化健身挑战的方式——作为驻留在客户端设备上的派生数据视图。

实时个性化健身挑战评估
数据库中的数据流
即使在技术栈的底层,我们也能看到数据库中的数据流的雏形。数据库触发器、存储过程和物化视图维护例程与按需和定期处理管道并无太大不同;B 树索引和关系数据库的物化视图本质上是派生的数据视图——谈谈数据流中的数据流!
数据流,数据流,无处不在
“数据集成的目标是确保数据以正确的形式出现在所有正确的位置。”
—《设计数据密集型应用程序》(马丁·克莱普曼)
随着数据系统的扩展,我们应超越将其视为被应用程序操作的被动数据库(如全局变量)的局限。
相反,重新构想组织中的数据系统是非常有益的,它们作为数据视图的相互作用,其中一个从另一个派生,状态变化从一个中心真实源传播,通过功能应用代码传播。数据流,基于数据流。
这是魔法黑盒子——一直到底。
总结
恭喜你坚持到这里!
在第一部分中,我们从一个简单的请求-响应系统发展到一个事件驱动系统,流式传输、处理和保存来自各种健身器械传感器和医疗设备的数据。
在这第二部分中,我们扩展了那些保存的记录,并对其进行了定期和按需处理。这使得增强用户运动体验的新功能成为可能,体验变得更加集体却又个性化。随着我们的摄取管道的发展,我们的数据流架构也在扩展,能够满足新的需求。

基于数据摄取管道的 SmartGym 功能总结
我们的演变故事并未就此结束。
在下一部分,也是最后一部分,我们将探讨如何以即插即用的方式添加和移除功能,为我们的平台孕育出一个生态系统。
敬请期待……
本文中的所有图片和 GIF 都是作者原创作品。
感谢数据工程圣经,即《设计数据密集型应用》一书——马丁·克莱普曼(Martin Kleppmann)为我提供了清晰思考这些分布式系统的词汇。
一个(不那么)简短的健康与健身数据管道历史 — 系列
一个(不那么)简短的健康与健身数据管道历史:第一部分
medium.com [## 基于数据流架构的衍生数据视图与最终一致性]
一个(不那么)简短的健康与健身数据管道历史:第二部分
towardsdatascience.com
了解更多关于我队友们的功能开发内容
用户洞察仪表盘
这是我与 GovTech 的 SmartGym 团队一起度过的短暂而有意义的实习经历!
产品指标仪表盘
我与一个非常规的网页开发框架:Plotly Dash 的工作经验
medium.com [## 实习经验 — 不要匆匆完成数据分析]
作为数据分析师与 SmartGym 团队一起度过的充实实习之旅
排行榜和健身挑战分析仪表盘
介绍
medium.com](https://medium.com/ytpo-govtech/internship-blog-7b021006e020?source=post_page-----e3bc25176cf8--------------------------------) [## 不要在黑暗中投掷飞镖
介绍
medium.com](https://medium.com/siot-govtech/dont-throw-darts-in-the-dark-11e3404f8436?source=post_page-----e3bc25176cf8--------------------------------) [## 我在 SmartGym 最有成就感的时刻
作者: https://medium.com/@dharmil.shah_35509
为物理治疗定制的个性化锻炼计划
介绍
DBSCAN,5 分钟讲解
Python 中最快的实现🐍
·发表于Towards Data Science ·阅读时间 5 分钟·2024 年 8 月 23 日
--

图片由作者提供。
什么是DBSCAN [1]?如何用 Python 构建它?有很多文章介绍了这个话题,但我认为这个算法本身如此简单直观,以至于可以在仅仅5 分钟内解释清楚它的基本思想,所以让我们试着这样做。
DBSCAN = 基于密度的空间聚类算法,带噪声
这是什么意思?
-
该算法基于对象之间的空间距离在数据中搜索聚类。
-
该算法可以识别异常值(噪声)。
为什么你根本需要 DBSCAN 呢???
-
提取新特征。 如果你处理的数据集很大,找出数据中明显的聚类并分别处理每个聚类(为不同的聚类训练不同的模型)可能会很有帮助。
-
压缩数据。 我们经常需要处理数百万行数据,这在计算上是昂贵且耗时的。通过聚类数据,然后从每个聚类中仅保留 X%的数据,可能会拯救你宝贵的数据科学灵魂。因此,你将保持数据集内的平衡,但减少其大小。
-
新颖性检测。 前面提到过,DBSCAN 可以检测噪声,但噪声可能是数据集中的一个之前未知的特征,你可以保留它并在建模中使用。
然后你可能会说:但是有一个超可靠且有效的k 均值算法。
从零开始用 Python 实现🐍
towardsdatascience.com
是的,但 DBSCAN 最棒的地方是它克服了 k-means 的缺点,而且你不需要指定聚类的数量。DBSCAN 为你检测聚类!
DBSCAN 有两个由用户定义的组件:邻域或半径(𝜀)和邻居数(N)。
对于一个由若干对象组成的数据集,该算法基于以下思想:
-
核心对象。如果一个对象在距离𝜀范围内至少有N个其他对象,那么它被称为核心对象。
-
一个位于核心点𝜀邻域内的非核心对象称为边界对象。
-
一个核心对象与所有位于𝜀邻域内的核心和边界对象形成一个聚类。
-
如果一个对象既不是核心对象也不是边界对象,它被称为噪声(离群点)。它不属于任何聚类。
为了实现 DBSCAN,必须创建一个距离函数。在本文中,我们将使用欧几里得距离:

图片由 作者 提供。
我们算法的伪代码如下:

图片由 [2] 提供。
和往常一样,本文的代码可以在我的 GitHub** 上找到。**
让我们从距离函数开始:
def distances(object, data):
euclidean = []
for row in data: #iterating through all the objects in the dataset
d = 0
for i in range(data.shape[1]): #calculating sum of squared residuals for all the coords
d+=(row[i]-object[i])**2
euclidean.append(d**0.5) #taking a sqaure root
return np.array(euclidean)
现在让我们构建算法的主体:
def DBSCAN(data, epsilon=0.5, N=3):
visited, noise = [], [] #lists to collect visited points and outliers
clusters = [] #list to collect clusters
for i in range(data.shape[0]): #iterating through all the points
if i not in visited: #getting in if the point's not visited
visited.append(i)
d = distances(data[i], data) #getting distances to all the other points
neighbors = list(np.where((d<=epsilon)&(d!=0))[0]) #getting the list of neighbors in the epsilon vicinity and removing distance = 0 (it's the point itself)
if len(neighbors)<N: #if the number of object is less than N, it's an outlier
noise.append(i)
else:
cluster = [i] #otherwise it forms a new cluster
for neighbor in neighbors: #iterating trough all the neighbors of the point i
if neighbor not in visited: #if neighbor isn't visited
visited.append(neighbor)
d = distances(data[neighbor], data) #get the distances to other objects from the neighbor
neighbors_idx = list(np.where((d<=epsilon)&(d!=0))[0]) #getting neighbors of the neighbor
if len(neighbors_idx)>=N: #if the neighbor has N or more neighbors, than it's a core point
neighbors += neighbors_idx #add neighbors of the neighbor to the neighbors of the ith object
if not any(neighbor in cluster for cluster in clusters):
cluster.append(neighbor) #if neighbor is not in clusters, add it there
clusters.append(cluster) #put the cluster into clusters list
return clusters, noise
完成!
让我们检查一下我们实现的正确性,并与sklearn进行比较。
让我们生成一些合成数据:
X1 = [[x,y] for x, y in zip(np.random.normal(6,1, 2000), np.random.normal(0,0.5, 2000))]
X2 = [[x,y] for x, y in zip(np.random.normal(10,2, 2000), np.random.normal(6,1, 2000))]
X3 = [[x,y] for x, y in zip(np.random.normal(-2,1, 2000), np.random.normal(4,2.5, 2000))]
fig, ax = plt.subplots()
ax.scatter([x[0] for x in X1], [y[1] for y in X1], s=40, c='#00b8ff', edgecolors='#133e7c', linewidth=0.5, alpha=0.8)
ax.scatter([x[0] for x in X2], [y[1] for y in X2], s=40, c='#00ff9f', edgecolors='#0abdc6', linewidth=0.5, alpha=0.8)
ax.scatter([x[0] for x in X3], [y[1] for y in X3], s=40, c='#d600ff', edgecolors='#ea00d9', linewidth=0.5, alpha=0.8)
ax.spines[['right', 'top', 'bottom', 'left']].set_visible(False)
ax.set_xticks([])
ax.set_yticks([])
ax.set_facecolor('black')
ax.patch.set_alpha(0.7)

图片由 作者 提供。
让我们应用我们的实现并可视化结果:

图片由 作者 提供。
对于 sklearn 的实现,我们得到了相同的聚类:

图片由 作者 提供。
就这样,它们是相同的。5 分钟就搞定了!当你自己尝试 DBSCAN 时,别忘了调整 epsilon 和邻居数,因为它们会显著影响最终结果。
===========================================
参考文献:
[1] Ester, M., Kriegel, H. P., Sander, J., & Xu, X. (1996, August). Density-based spatial clustering of applications with noise. In Int. Conf. knowledge discovery and data mining (Vol. 240, №6).
[2] Yang, Yang, et al. “An efficient DBSCAN optimized by arithmetic optimization algorithm with opposition-based learning.” The journal of supercomputing 78.18 (2022): 19566–19604.
===========================================
我在 Medium 上的所有出版物都是免费的开放访问,因此如果你在这里关注我,我将不胜感激!
P.s. 我对(地理)数据科学、机器学习/人工智能以及气候变化充满极大热情。如果你想在某个项目上合作,请通过LinkedIn联系我。
🛰️关注以获取更多信息🛰️
在 Google Cloud 上大规模部署 dbt 项目
使用 Artifact Registry、Cloud Composer、GitHub Actions 和 dbt-airflow 容器化并运行 dbt 项目
·发布于 Towards Data Science ·阅读时间 11 分钟·2024 年 7 月 29 日
--

图片由 Photo Boards 提供,来源 Unsplash
管理大规模的数据模型是数据团队使用 dbt(数据构建工具) 常见的挑战。最初,团队通常从易于管理和部署的简单模型开始。然而,随着数据量的增加和业务需求的发展,这些模型的复杂性也在增加。
这种发展通常会导致一个单体的仓库,所有依赖关系交织在一起,使得不同团队之间的协作变得困难。为了解决这个问题,数据团队可能会发现将数据模型分布到多个 dbt 项目中是有益的。这种方法不仅促进了更好的组织和模块化,还增强了整个数据基础设施的可扩展性和可维护性。
由处理多个 dbt 项目引入的一个重要复杂性是它们的执行和部署方式。管理库依赖关系变得至关重要,尤其是当不同项目需要不同版本的 dbt 时。虽然 dbt Cloud 提供了一个强大的解决方案,用于调度和执行多仓库 dbt 项目,但它也需要巨大的投资,并非每个组织都能承受或找到合适的资源来使用……
利用双重机器学习去偏处理效应
因果 AI,探索因果推理与机器学习的整合
·发布于 Towards Data Science ·阅读时长 11 分钟·2024 年 4 月 5 日
--

图片由 Ales Nesetril 提供,来自 Unsplash
这系列文章的主题是什么?
欢迎来到我的因果 AI 系列,我们将探讨因果推理如何与机器学习模型相结合。期望在不同的商业环境中探索多个实际应用。
在上一篇文章中,我们探讨了如何让因果发现应用于现实世界的商业环境。这次我们将介绍利用双重机器学习去偏处理效应。
如果你错过了上一篇关于因果发现的文章,请点击这里查看:
因果 AI,探索因果推理与机器学习的整合
towardsdatascience.com
介绍
本文将展示为什么双重机器学习是因果 AI 工具箱中的一个重要部分:
期望深入理解:
-
平均处理效应(ATE)
-
使用线性回归估计 ATE 的挑战
-
双重机器学习及其如何克服线性回归面临的挑战
-
一个用 Python 实现的案例研究,展示了如何应用双重机器学习。
完整的笔记本可以在这里找到:
[## causal_ai/notebooks/estimating average treatment effects with double machine learning.ipynb at main…
本项目介绍了因果人工智能(Causal AI)及其如何推动商业价值。 - causal_ai/notebooks/estimating average…
平均处理效应(ATE)
ATE
ATE 是处理或干预对一个群体的平均影响。我们可以通过比较处理组和控制组之间某一选择指标的平均变化来计算 ATE。
例如,考虑一个营销团队正在进行促销活动。处理组由收到优惠的客户组成,而控制组由未收到优惠的客户组成。我们可以通过比较处理组和控制组中的平均订单数来计算 ATE。
潜在结果框架
潜在结果框架由唐纳德·鲁宾(Donald Rubin)提出,已成为因果推断中的基础概念。让我们通过上面营销团队的例子来理解它。
-
处理分配: 每个客户有两个潜在结果,一个是处于处理组(收到优惠)的结果,另一个是处于控制组(未收到优惠)的结果。然而,对于每个客户,只能观察到其中一个潜在结果。
-
反事实: 未观察到的潜在结果是反事实的,例如,如果这个客户处于控制组(未收到优惠),会发生什么。
-
因果效应: 处理的因果效应是不同处理条件下潜在结果之间的差异(收到优惠 vs 未收到优惠)。
-
估计: 可以使用一系列因果技术,利用实验数据或观察数据来估计因果效应。
为确保估计效应的有效性,需做出若干假设:
-
稳定单元处理值假设(SUTVA): 任何客户的潜在结果不受其他客户处理分配的影响。
-
积极性: 对于任何特征组合,必须有一定的概率使得客户可以接受处理或控制。
-
可忽略性: 所有对处理和结果都有影响的混杂因素都是可观察的。
实验数据
使用实验数据估计 ATE 相对简单。
随机对照试验(RCT)或 AB 测试的设计是随机将参与者分配到处理组和控制组。这确保了任何结果的差异都可以归因于处理效应,而不是参与者的先天特征。
回到市场营销团队的例子。如果他们随机将客户分为治疗组和控制组,那么订单的平均差异就是所发送优惠的因果效应。
观察数据
使用观察数据估计 ATE 更具挑战性。
最常见的挑战是混杂变量,它们同时影响治疗和结果。如果没有控制混杂因子,将导致治疗效应的估计偏差。我们将在文章后续的案例研究中回到这个问题。
其他挑战包括:
-
选择偏倚——治疗分配受结果相关因素的影响。
-
异质性治疗效应——治疗效应在不同子群体之间变化。
使用线性回归估计 ATE
概述
线性回归可以用于使用观察数据估计 ATE。治疗(T)和控制特征(X)作为变量包含在模型中。

使用生成的图片
治疗变量的系数就是 ATE——与治疗变量的单位变化相关的结果变量的平均变化,同时保持控制特征不变。
数据生成过程
我们可以使用一个简单的数据生成过程,包含一个结果变量、治疗和混杂因子,来说明如何使用线性回归估计 ATE。
首先,我们可以可视化因果图:
# Create node lookup variables
node_lookup = {0: 'Confounder',
1: 'Treatment',
2: 'Outcome'
}
total_nodes = len(node_lookup)
# Create adjacency matrix - this is the base for our graph
graph_actual = np.zeros((total_nodes, total_nodes))
# Create graph using expert domain knowledge
graph_actual[0, 1] = 1.0 # Confounder -> Treatment
graph_actual[0, 2] = 1.0 # Confounder -> Outcome
graph_actual[1, 2] = 1.0 # Treatment -> Outcome
plot_graph(input_graph=graph_actual, node_lookup=node_lookup)

用户生成的图片
然后我们可以使用简单的数据生成过程创建样本。请特别注意治疗变量的系数(0.75)——这就是我们的真实 ATE。
np.random.seed(123)
# Create dataframe with a confounder, treatment and outcome
df = pd.DataFrame(columns=['Confounder', 'Treatment', 'Outcome'])
df['Confounder'] = np.random.normal(loc=100, scale=25, size=1000)
df['Treatment'] = np.random.normal(loc=50, scale=10, size=1000) + 0.50 * df['Confounder']
df['Outcome'] = 0.25 * df['Confounder'] + 0.75 * df['Treatment'] + np.random.normal(loc=0, scale=5, size=1000)
sns.pairplot(df, corner=True)

用户生成的图片
线性回归
然后,我们可以训练一个线性回归模型,并提取治疗变量的系数——我们可以看到它正确估计了 ATE(0.75)。
# Set target and features
y = df['Outcome']
X = df[['Confounder', 'Treatment']]
# Train model
model = RidgeCV()
model = model.fit(X, y)
# Extract the treatment coefficient
ate_lr = round(model.coef_[1], 2)
print(f'The average treatment effect using Linear Regression is: {ate_lr}')

用户生成的图片
挑战
线性回归可以是估计 ATE(平均处理效应)非常有效的方法。然而,需要注意一些挑战:
-
当我们处理高维数据时,它会遇到困难。
-
“干扰参数”(控制特征,这些是“干扰”估计的因素)可能过于复杂,无法通过线性回归估计。
-
它假设治疗效应在不同的子群体中是恒定的(例如,没有异质性)。
-
假设没有未观察到的混杂因素。
-
假设治疗效应是线性的。
双重机器学习(DML)
概述
双重机器学习(Double Machine Learning)是一种因果方法,首次在 2017 年发表于《双重/去偏机器学习用于处理和结构参数》一文中:
大多数现代的监督统计/机器学习(ML)方法都是专门设计来解决预测问题的……
它旨在减少偏差并改善因果效应的估计,适用于高维数据和/或复杂的干扰参数的情况。
它的灵感来源于弗里希-沃-洛维尔定理(Frisch-Waugh-Lovell theorem),因此让我们从理解这一点开始。
弗里希-沃-洛维尔定理
FWL 定理用于分解多个回归变量对结果变量的影响,从而使我们能够隔离感兴趣的效应。
假设你有两组特征,X1 和 X2。你可以像之前那样使用线性回归来估计模型参数。然而,你也可以通过以下步骤得到 X1 的相同参数:
-
仅使用 X2 来预测结果
-
仅使用 X2 来预测 X1
-
计算结果模型(步骤 1)和特征模型(步骤 2)的残差
-
将结果模型的残差对特征模型的残差进行回归,以估计 X1 的参数
一开始这可能很难理解,因此让我们用 Python 来演示一下。我们使用之前相同的数据,但将处理列作为 X1,将混杂变量列作为 X2:
# Set treatment, outcome and confounder samples
treatment = df['Treatment'].to_numpy().reshape(-1,1)
outcome = df['Outcome'].to_numpy().reshape(-1,1)
confounder = df['Confounder'].to_numpy().reshape(-1,1)
# Train treatment model and calculate residuals
treatment_model = RidgeCV()
treatment_model = treatment_model.fit(confounder, treatment)
treatment_pred = treatment_model.predict(confounder)
treatment_residuals = treatment - treatment_pred
# Train outcome model and calculate residuals
outcome_model = RidgeCV()
outcome_model = outcome_model.fit(confounder, outcome)
outcome_pred = outcome_model.predict(confounder)
outcome_residuals = outcome - outcome_pred
# Train residual model and calculate average treatment effect
final_model = RidgeCV()
final_model = final_model.fit(treatment_residuals, outcome_residuals)
ate_dml = round(final_model.coef_[0][0], 2)
print(f'The average treatment effect is: {ate_fwl}')

用户生成的图像
我们可以看到,它正确估计了处理变量的系数(0.75)。
双重机器学习
双重机器学习(Double Machine Learning)通过隔离处理和控制特征的效应,并使用灵活的机器学习模型,基于 FWL 定理进行构建。
第一阶段通常被称为正交化,因为干扰参数是独立于处理效应估计的。
第一阶段:
-
处理模型(去偏化): 用于估计治疗分配概率的机器学习模型(通常称为倾向评分)。然后计算治疗模型的残差。
-
结果模型(去噪): 用于仅使用控制特征估计结果的机器学习模型。然后计算结果模型的残差。
第一阶段:
- 处理模型的残差用于预测结果模型的残差。
第二阶段模型的系数是 ATE(平均处理效应)。值得注意的是,第二阶段模型是一个线性模型,这意味着我们假设我们的处理效应是线性的(这也是我们称 DML 为部分线性模型的原因)。
我们可以使用微软的包 EconML,而不是自己编写代码。EconML 实现了广泛的因果机器学习(Causal ML)技术,其中包括多个 DML 的实现:
[## 欢迎访问 econml 文档! - econml 0.15.0 文档
动态处理方案的估计方法
# Train DML model
dml = LinearDML(discrete_treatment=False)
dml.fit(df['Outcome'].to_numpy().reshape(-1,1), T=df['Treatment'].to_numpy().reshape(-1,1), X=None, W=df['Confounder'].to_numpy().reshape(-1,1))
# Calculate average treatment effect
ate_dml = round(dml.ate()[0], 2)
print(f'The average treatment effect using the DML is: {ate_dml}')

用户生成的图片
我们再次看到它正确估计了处理变量的系数(0.75)。
市场营销案例研究
背景
市场营销团队向选定的客户发送吸引人的优惠。他们目前并没有从随机选择的客户中挑选出样本来衡量优惠的影响。
数据科学团队被要求估计优惠如何影响客户订单。
混杂偏差
天真地比较收到与未收到优惠的客户是有偏差的。这是由于混杂因素所驱动:
-
选择退出电子邮件的客户无法收到优惠——这些客户的参与度较低,不太可能下单。
-
CRM 团队根据客户的订单历史来定位客户——订单历史会影响你再次下单的可能性。
数据生成过程
我们设置了一个具有以下特征的数据生成过程:
-
难以处理的干扰参数
-
简单的处理效应(无异质性)
X 特征是治疗前收集的客户特征:

用户生成的图片
T 是一个二进制标志,表示客户是否收到了优惠。

用户生成的图片
np.random.seed(123)
# Set number of observations
n=100000
# Set number of features
p=10
# Create features
X = np.random.uniform(size=n * p).reshape((n, -1))
# Nuisance parameters
b = (
np.sin(np.pi * X[:, 0] * X[:, 1])
+ 2 * (X[:, 2] - 0.5) ** 2
+ X[:, 3]
+ 0.5 * X[:, 4]
+ X[:, 5] * X[:, 6]
+ X[:, 7] ** 3
+ np.sin(np.pi * X[:, 8] * X[:, 9])
)
# Create binary treatment
T = np.random.binomial(1, expit(b))
# Set treatment effect
tau = 0.75
# Calculate outcome
y = b + T * tau + np.random.normal(size=n)
生成数据的 Python 代码基于 Ubers Causal ML 包中的合成数据创建器。能够创建真实的合成数据在评估因果推断方法时至关重要,因此我强烈推荐你查看一下:
[## causalml/causalml/dataset/regression.py at master · uber/causalml
使用机器学习算法进行提升建模和因果推断 - causalml/causalml/dataset/regression.py at…
线性回归
我们从使用线性回归开始估计 ATE。我们的预期是它可能难以捕捉干扰参数,并可能错误指定处理效应。
# Append features and treatment
X_T = np.append(X, T.reshape(-1, 1), axis=1)
# Train linear regression model
model = RidgeCV()
model = model.fit(X_T, y)
y_pred = model.predict(X_T)
# Extract the treatment coefficient
ate_lr = round(model.coef_[-1], 2)
print(f'The average treatment effect using Linear Regression is: {ate_lr}')

用户生成的图片
双重机器学习
然后我们使用 LightGBM 作为灵活的第一阶段模型训练 DML 模型。这应该能帮助我们捕捉难以处理的干扰参数,同时正确计算处理效应。
np.random.seed(123)
# Train DML model using flexible stage 1 models
dml = LinearDML(model_y=LGBMRegressor(), model_t=LGBMClassifier(), discrete_treatment=True)
dml.fit(y, T=T, X=None, W=X)
# Calculate average treatment effect
ate_dml = round(dml.ate(), 2)
print(f'The average treatment effect using the DML is: {ate_dml}')

用户生成的图片
比较
当我们比较结果时,我们观察到线性回归给出了一个有偏的估计,而 DML 非常接近真实值。这真正展示了 DML 的强大之处!
# Plot comparison of results
categories = ['Ground truth', 'DML', 'Linear Regression']
sns.barplot(x=categories, y=[tau, ate_dml, ate_lr])
plt.ylabel('ATE')
plt.title('Average Treatment Effect comparison')
plt.show()

用户生成的图片
其他方法
还有一些其他因果方法可以用来估计平均处理效应(ATE)(其中很多方法已经在 EconML 和 CausalML 包中实现):
-
倾向得分匹配(PSM)
-
逆倾向得分匹配(IPSM)
-
S-学习者
-
T-学习者
-
双重稳健学习者(DR)
-
工具变量学习者(IV)
如果你想进一步深入这些方法,我建议从 S-学习者和 T-学习者(通常被称为元学习者)开始。以下是几个关键点,帮助你了解何时以及如何应用它们:
-
当你的处理是二元的,并且处理组和对照组的规模相等时,T-学习者通常是 DML 的一个更简单的替代方案。
-
当你的处理是连续型的,并且你怀疑处理效应可能是非线性的时,S-学习者可能比 DML 更合适。
-
元学习者可能会受到正则化偏差的影响(尤其是 S-学习者)——当我们看到双重机器学习(DML)优于元学习者时,通常就是因为这个原因。
如果你想继续这段因果人工智能的旅程,请关注我——在下一篇文章中,我们将探讨如何利用双重机器学习估计条件平均处理效应(CATE),帮助我们在客户层面个性化治疗。
解码:理解 Transformer 模型的上下文窗口
你需要了解的关于上下文窗口如何影响 Transformer 训练和使用的所有内容
·发表于 Towards Data Science ·9 分钟阅读·2024 年 1 月 27 日
--
上下文窗口是 Transformer 一次能够处理的最大序列长度。随着限制令牌数量以及因此限制提示大小的专有大型语言模型(LLM)的兴起,以及对检索增强生成(RAG)等技术日益增长的兴趣,理解上下文窗口及其影响的关键概念变得越来越重要,因为在讨论不同模型时,这一点常常被提及。
Transformer 架构是自然语言处理中的一项强大工具,但在处理长文本序列时存在一些局限性。在本文中,我们将探讨不同因素如何影响 Transformer 模型能够处理的最大上下文长度,以及在选择模型时,是否更大的模型总是更好的问题。

由Azure OpenAI 服务 DALL-E 模型生成的图像,提示语为:“一个机器人在读书。超现实,高质量”
我可以在 Transformer 中放入多少个词?
在撰写本文时,像Llama-2 变体这样的模型具有 4k 标记的上下文长度,GPT-4 Turbo 具有 128k 标记,Claude 2.1 具有 200k 标记!仅从标记数来看,很难想象这如何转化为词汇;尽管这取决于所使用的分词器,但一个好的经验法则是,100k 标记大约等于 75,000 个词。为了让这一点更有意义,我们可以将其与一些流行的文学作品进行比较:
-
《指环王》(J.R.R.托尔金):564,187 个词,752k 个标记
-
《德古拉》(布莱姆·斯托克):165,453 个词,220k 个标记
-
《格林童话》(雅各布·格林和威廉·格林):104,228 个词,139k 个标记
-
《弗兰肯斯坦》(玛丽·雪莱):78,100 个词,104k 个标记
-
《哈利·波特与魔法石》(J.K.罗琳):77,423 个词,103k 个标记
-
《金银岛》(罗伯特·路易斯·史蒂文森):72,036 个词,96k 个标记
-
《世界大战》(赫伯特·乔治·威尔斯):63,194 个词,84k 个标记
-
《巴斯克维尔的猎犬》(阿瑟·柯南·道尔):62,297 个词,83k 个标记
-
《丛林之书》(吉卜林):54,178 个词,72k 个标记
总结一下,100k 标记大约相当于一本短篇小说,而 200k 标记则几乎可以容纳完整的《德古拉》,一本中等大小的书!要处理大量文本,如《指环王》,我们需要 6 次请求 GPT-4,而只需 4 次请求 Claude 2!

由Azure OpenAI 服务 DALL-E 模型生成的图像,提示语为:“一台机器正在咬书。超现实主义,高质量”
什么决定了上下文窗口的大小?
在这一点上,你可能会想,为什么某些模型比其他模型具有更大的上下文窗口。
为了理解这一点,我们首先来看下面的图示,回顾注意力机制是如何工作的;如果你不熟悉注意力的细节,可以参考我之前的文章,这里详细介绍了该内容。最近,已经有多个注意力机制的改进和变体,旨在提高该机制的效率,但核心挑战依然不变。这里,我们将重点关注原始的缩放点积注意力。

解码器仅 Transformer 模型的注意力机制。仅对 Q 和 K 输入进行位置编码的方式,在每个 Transformer 层内部应用,遵循了现代架构,如 LLama-2。
从上图中,我们可以看到,包含注意力得分的矩阵大小由输入模型的序列长度决定,并且可以无限制地增大!因此,我们可以看出,上下文窗口的大小并不是由架构决定的,而是由训练时输入给模型的序列长度决定的。
这种计算可能非常昂贵,因为在没有任何优化的情况下,矩阵乘法通常在空间复杂度上是二次的(O(n²))。简而言之,这意味着如果输入序列的长度加倍,所需的内存量将增加四倍!因此,训练一个长度为 128k 的序列模型将需要大约 1024 倍的内存,相较于训练长度为 4k 的序列模型!
需要注意的是,这一操作会针对每一层和每一个头部在 Transformer 中重复进行,这会导致大量的计算。由于 GPU 内存的可用空间还需要与模型的参数、计算出的梯度以及合适大小的输入数据批次共享,因此在训练大型模型时,硬件很快会成为上下文窗口大小的瓶颈。
我们可以扩展预训练模型的上下文窗口吗?
在理解了训练模型时面临的长序列长度的计算挑战后,可能会有诱惑想要在短序列上训练模型,寄希望于这种方法能推广到更长的上下文。

由Azure OpenAI 服务 DALL-E 模型生成的图像,提示词为:“一只机器人手持扳手。超现实主义,高清。”
一个障碍是位置编码机制,它被用来使 Transformer 捕捉序列中标记的位置。在原始论文中,提出了两种位置编码策略。第一种是使用针对序列中每个位置的可学习嵌入,这显然无法推广到模型训练时未见过的最大序列长度。然而,作者假设他们偏好的正弦方法可能会对更长的序列进行外推;后续的研究证明了事实并非如此。
在许多近期的 Transformer 模型中,如 PaLM 和 Llama-2,绝对位置编码已被相对位置编码所取代,例如RoPE,其目标是在编码后保持标记之间的相对距离。虽然这些方法比以前的方法在推广到更长的序列时稍微更好,但对于模型未见过的远超其训练序列长度的序列,性能很快会崩溃。
虽然有几种方法旨在改变或完全移除位置编码,但这些方法需要对变压器架构进行根本性改变,并且需要重新训练模型,这既昂贵又耗时。由于在本文撰写时,许多顶尖的开源模型都是基于 Llama-2 的预训练版本,因此目前有很多积极的研究正在进行,旨在扩展使用 RoPE 嵌入的现有模型的上下文长度,取得了不同程度的成功。
这些方法中的许多都使用了一些变体,通过插值输入序列;缩放位置嵌入,使其适应模型原始的上下文窗口。其直觉是,模型应该更容易填补单词之间的空隙,而不是试图预测单词之后的内容。

使用插值方法时,目标不是让模型推断出更长的序列,而是创造出模型当前序列长度中的中间位置。图片来源:[2306.15595] 通过位置插值扩展大语言模型的上下文窗口 (arxiv.org)
一种名为YaRN的方法,成功将 Llama-2 的 7B 和 13B 模型的上下文窗口扩展到 128k,而且没有明显的性能下降!
尽管一种在所有情境下都能有效工作的确定性方法尚未出现,但这仍然是一个令人兴奋的研究领域,具有巨大的潜在影响!
更长的上下文窗口总是更好吗?
现在我们已经理解了在更长序列长度上训练模型的一些实际挑战,以及为克服这些挑战而采取的一些潜在缓解措施,我们可以提出另一个问题——这些额外的努力值得吗?乍一看,答案似乎显而易见;为模型提供更多信息应该能够更容易地注入新知识并减少幻觉,使其在几乎所有可以想象的应用中都更有用。然而,事情并非如此简单。

该图片由Azure OpenAI 服务的 DALL-E 模型生成,使用的提示词是:“两位机器人,每人拿着一本书,比较书本的大小。超现实主义,高质量”
在 2023 年的论文迷失在中间, 斯坦福大学和伯克利的研究人员调查了模型如何使用和访问其上下文窗口中提供的信息,并得出了以下结论:
“我们发现,改变输入上下文中相关信息的位置可以显著影响模型性能,这表明当前的语言模型在长输入上下文中并不能稳健地访问和使用信息。”
对于他们的实验,作者创建了一个数据集,在该数据集中,对于每个查询,包含一个答案的文档和k — 1个不包含答案的干扰文档;通过调整输入上下文长度,改变检索到的、不包含答案的文档数量。他们随后通过改变文档的顺序来调整相关信息在输入上下文中的位置,将相关文档放在上下文的开始、中间或末尾,并评估是否有任何正确的答案出现在预测的输出中。
具体来说,他们观察到,当相关信息位于上下文窗口的开始或末尾时,研究中的模型表现最佳;而当所需信息位于上下文的中间时,性能显著下降。
从理论上讲,Transformer 中的自注意力机制使得模型在生成下一个词时能够考虑输入的所有部分,而不管它们在序列中的位置。因此,我认为模型关于如何定位重要信息的任何偏见更可能来自训练数据,而非架构。我们可以通过进一步研究作者在评估 Llama-2 系列模型时观察到的结果,来探讨这个想法,这些结果涉及根据文档位置检索的准确性,展示在下图中。

图像来源:[2307.03172] Lost in the Middle: How Language Models Use Long Contexts (arxiv.org)
看着基础模型,我们可以清楚地观察到作者对于 Llama-2 13B 和 70B 模型的结论。有趣的是,对于 7B 模型,我们可以看到它几乎完全依赖于上下文的末尾;由于大量的无监督微调是在从各种来源抓取的数据流上进行的,当模型的参数相对较少,无法在不断变化的上下文中预测下一个词时,专注于最新的标记是有意义的!
更大的模型在相关信息位于文本开头时表现也很好;这表明,随着模型参数的增加,它们学习更多地关注文本的开头。作者假设这是因为,在预训练过程中,模型会看到来自像 StackOverflow 这样的数据源,它们通常从重要信息开始。我怀疑 13B 模型在前置信息上的轻微优势并不显著,因为两种情况的准确性相似,而且 70B 模型并没有展示出这种模式。
“聊天”模型经过进一步的指令调优和强化学习(RLHF)训练,整体表现更好,并且似乎对文本中相关信息的位置变得不那么敏感。对于 13B 模型,这种变化更为明显,而对于 70B 模型则不太明显。7B 模型变化不大,可能是因为它的参数较少。这可能意味着这些模型在更多的训练后,能够更好地利用文本其他部分的信息,但它们仍然偏好最近的信息。考虑到后续训练阶段明显较短,它们尚未完全克服第一次无监督训练的偏差;我怀疑 70B 模型可能需要更大且更多样化的后续训练,才能表现出与 13B 模型在此观察到的表现类似的变化幅度。
此外,我对一项研究感兴趣,该研究探讨了在用于 SFT 的数据集中相关信息的位置。正如 人类表现出类似行为,即在序列的开始和结束部分更容易回忆起信息,因此,如果这种行为在给定的许多示例中有所体现,也不足为奇。
结论
总结来说,上下文窗口并非固定,可以根据需要扩大,只要有足够的内存可用!然而,较长的序列意味着更多的计算——这也导致模型变慢——并且除非模型已经在类似长度的序列上进行过训练,否则输出可能没有太大意义!然而,即便是拥有大上下文窗口的模型,也不能保证它们会有效利用提供给它们的所有信息——这确实是 没有免费的午餐!
Chris Hughes 在 LinkedIn 上
除非另有说明,所有图片均由作者创作。
参考文献
-
解码:用通俗英语解释 Transformers | Chris Hughes | Towards Data Science
去嵌套 Google Analytics 数据在 BigQuery 中
扁平化表格的正确方法
·发表于 Towards Data Science ·阅读时间:5 分钟·2024 年 3 月 26 日
--

新加坡的照片由 Mike Enerio 提供,来源于 Unsplash
BigQuery 是一个优化的分析引擎,适合处理预先连接(或嵌套)数据。在分析场景中,子关系是有意义的,因为我们不想在更大的数据集上进行连接——想象一下过去三年的每日同比比较,聚合着数 TB 的数据——但是连接会增加复杂性。
子关系或子表通常实现为结构体数组。数组作为一种类似列表的数据类型提供行,而结构体类似于映射或字典,提供列。子模式在整个表中是一致的——与 JSON 类型不同,后者可以在每行中改变其模式。
BigQuery 可以非常强大,因为嵌套数据意味着处理预先连接的表。但分析师在使用时会遇到困难……
towardsdatascience.com
似乎唯一一个沿着这种嵌套数据路线发展的引擎是AWS Redshift Spectrum。然而,如果我们想在另一个系统中使用 Google Analytics (GA) 数据,几乎总是需要将数据去连接(de-join),以便获得平面表格,因为聚合或修改结构数组的能力非常有限。大多数分析型数据库引擎似乎都在优化……
像专家一样处理缺失数据:多变量和迭代插补算法
使用 LightGBM、kNN 和自编码器进行插补,并通过迭代方法 MICE 进一步优化
·发布于 Towards Data Science ·阅读时长 15 分钟·2024 年 12 月 12 日
--
现实世界中的数据通常是杂乱无章的,在用于任何机器学习(ML)模型之前需要仔细的预处理。我们几乎总是会在数据集中遇到空值,如果能加以观察,这些值可能对我们的分析或建模非常有价值。我们称之为数据中的缺失性。
缺失值可能有多种原因,例如设备故障、ERP 系统中的非必填字段,或是调查中对参与者不适用的问题。根据原因的不同,缺失的性质也会有所不同。如何理解这种性质在我之前的文章中有详细解释。在本文中,重点主要放在如何正确处理这些缺失值,避免通过删除或插补造成偏差或丧失重要洞察。
红酒质量 数据来自 UCI 机器学习库,本文使用了该数据集 [1]。这是一个开源数据集,可以通过此链接进行下载。
理解缺失值的性质(MCAR、MAR、MNAR)对于选择正确的处理方法至关重要。因此,如果您认为需要更多相关信息,建议您首先阅读我之前的文章。
以 AI 方式应对认知失调
语言模型如何处理提示中的冲突指令?
·发布于 Towards Data Science ·阅读时间 7 分钟·2024 年 7 月 4 日
--

在系统消息、提示和示例中给出矛盾的指令时,LLM 会遵循哪条指令来生成回答?由作者创作。
语言模型如何处理提示中的冲突指令?
认知失调是一个心理学术语,用来描述个体在持有两个或多个矛盾信念时所经历的心理不适。例如,如果你在杂货店看到一个标明“10 件物品或更少”的结账通道,但排队的每个人都有 10 件或更多物品,那么你应该怎么办?
在 AI 的背景下,我想了解大型语言模型(LLM)如何处理以矛盾指令形式呈现的认知失调(例如,提示 LLM 从英语翻译成韩语,但却给出英语到法语的翻译示例)。
在这篇文章中,我通过给 LLM 提供矛盾信息来进行实验,以确定 LLM 更可能与哪些矛盾信息保持一致。
系统消息、提示指令和少量示例
作为用户,你可以通过以下三种方式之一告诉 LLM 该做什么:
- 直接在系统消息中描述任务
喜马拉雅山的死亡事件
一个完整的数据可视化项目:使用 Python、D3 和 Illustrator。
·发表于 Towards Data Science ·20 分钟阅读·2024 年 1 月 23 日
--

所有图片由作者提供。
学习 D3
我一直打算学习 D3。说实话,D3 一直是我所从事的工作中问题的“杀鸡用牛刀”(因为在那些场景中,数据可视化只是达成目标的手段,而非最终产品)。作为一名 Python 开发者,我常常使用 matplotlib、plotly、seaborn、pandas(或 geopandas)以及 bokeh 等工具来“完成任务”。然而,最近我开始花时间仅仅为了好玩去创建数据可视化,看起来现在正是学习 D3 的绝佳时机。
在这篇文章中,我将展示如何使用 Python、D3 和 Illustrator 创建类似上面图形的五座山峰的图表(珠穆朗玛峰、阿玛达布兰峰、卓奥友峰、洛子峰 和 马纳斯鲁峰)。我将介绍:
-
灵感。
-
获取数据。
-
初步数据准备。
-
选择 5 座山峰进行可视化。
-
为绘图准备数据。
-
使用 D3 创建 SVG。
-
保存 SVG 并导入到 Illustrator 中。
-
在 Illustrator 中处理 SVG。
-
添加最后的修饰。
-
学到的经验教训。
技术债务的终结?
深入探讨 AI 驱动的技术债务减少方法以及剩余的技术限制。
·发表于Towards Data Science ·13 分钟阅读·2024 年 7 月 31 日
--

死神可能正在逼近技术债务。图片由Leonardo.ai生成
本文由 David Meiborg 共同撰写。
我们在与一位经验丰富的技术高管合作过程中,创建了这篇深入分析文章。他希望在“AI 将解决技术债务”领域建立一个创业公司。现在,他仍在寻找团队成员加入他的创业项目——如果你感兴趣,欢迎联系!
定义技术债务
技术债务,根据Gartner的定义,是由于在软件开发过程中采取了捷径和做出牺牲所导致的“欠”IT 系统的累积工作。这些妥协通常是为了满足交付期限而必须做出的,但可能导致软件的非功能性需求出现偏差,最终影响性能、可扩展性和韧性。
Gartner 还强调,尽管传统应用程序基于过时的技术,但它们通常对于日常运营至关重要。当这些系统发生故障时,后果可能非常严重,带来诸如代码复杂性、高开发成本以及安全漏洞风险等挑战,甚至可能使最具创新性的公司陷入瘫痪。
技术债务可以在领导者将快速进展置于最优解之上时有意产生...
决策树分类器,解释:适合初学者的可视化指南与代码示例
分类算法
我们最喜爱的倒立树的新视角
·发表于Towards Data Science ·阅读时间:10 分钟·2024 年 8 月 30 日
--

⛳️ 更多[分类算法](https://medium.com/@samybaladram/list/classification-algorithms-b3586f0a772c),解释如下: · 虚拟分类器 · K 近邻分类器 · 伯努利朴素贝叶斯 · 高斯朴素贝叶斯 ▶ 决策树分类器 · 逻辑回归 · 支持向量机分类器 · 多层感知器
决策树在机器学习中无处不在,因其直观的输出而深受喜爱。谁不喜欢一个简单的“如果-那么”流程图呢?尽管它们很受欢迎,但令人惊讶的是,找到一个清晰的、逐步解释决策树工作原理的教程竟然如此困难。(实际上,我都为自己花了那么长时间才真正理解这个算法感到有些不好意思。)
在这篇分析中,我将重点讲解树构建的核心要点。我们将逐一解析每个节点发生了什么以及为什么发生,从根节点到最终的叶节点(当然会配有视觉示例)。

所有视觉效果:作者使用 Canva Pro 创建,已优化为移动端显示;在桌面端可能显示过大。
定义
决策树分类器创建一个倒置的树来进行预测,从顶部开始,提出一个关于数据中重要特征的问题,然后根据答案分支。沿着这些分支往下走,每一个停靠点都会问另一个问题,逐步缩小可能性。这个问答游戏会一直持续,直到到达最底部——一个叶节点——在这里你将得到最终的预测或分类。

决策树是最重要的机器学习算法之一——它是一个一系列的“是”或“否”问题。
使用的数据集
在本文中,我们将使用这个人工高尔夫数据集(灵感来自[1])作为示例。这个数据集预测一个人在特定天气条件下是否会打高尔夫。

列:‘Outlook’(已经进行独热编码为晴天、多云、雨天),‘Temperature’(华氏温度),‘Humidity’(湿度%),‘Wind’(是否有风),‘Play’(目标特征)
# Import libraries
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
import pandas as pd
import numpy as np
# Load data
dataset_dict = {
'Outlook': ['sunny', 'sunny', 'overcast', 'rainy', 'rainy', 'rainy', 'overcast', 'sunny', 'sunny', 'rainy', 'sunny', 'overcast', 'overcast', 'rainy', 'sunny', 'overcast', 'rainy', 'sunny', 'sunny', 'rainy', 'overcast', 'rainy', 'sunny', 'overcast', 'sunny', 'overcast', 'rainy', 'overcast'],
'Temperature': [85.0, 80.0, 83.0, 70.0, 68.0, 65.0, 64.0, 72.0, 69.0, 75.0, 75.0, 72.0, 81.0, 71.0, 81.0, 74.0, 76.0, 78.0, 82.0, 67.0, 85.0, 73.0, 88.0, 77.0, 79.0, 80.0, 66.0, 84.0],
'Humidity': [85.0, 90.0, 78.0, 96.0, 80.0, 70.0, 65.0, 95.0, 70.0, 80.0, 70.0, 90.0, 75.0, 80.0, 88.0, 92.0, 85.0, 75.0, 92.0, 90.0, 85.0, 88.0, 65.0, 70.0, 60.0, 95.0, 70.0, 78.0],
'Wind': [False, True, False, False, False, True, True, False, False, False, True, True, False, True, True, False, False, True, False, True, True, False, True, False, False, True, False, False],
'Play': ['No', 'No', 'Yes', 'Yes', 'Yes', 'No', 'Yes', 'No', 'Yes', 'Yes', 'Yes', 'Yes', 'Yes', 'No', 'No', 'Yes', 'Yes', 'No', 'No', 'No', 'Yes', 'Yes', 'Yes', 'Yes', 'Yes', 'Yes', 'No', 'Yes']
}
df = pd.DataFrame(dataset_dict)
# Preprocess data
df = pd.get_dummies(df, columns=['Outlook'], prefix='', prefix_sep='', dtype=int)
df['Wind'] = df['Wind'].astype(int)
df['Play'] = (df['Play'] == 'Yes').astype(int)
# Reorder the columns
df = df[['sunny', 'overcast', 'rainy', 'Temperature', 'Humidity', 'Wind', 'Play']]
# Prepare features and target
X, y = df.drop(columns='Play'), df['Play']
# Split data
X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=0.5, shuffle=False)
# Display results
print(pd.concat([X_train, y_train], axis=1), '\n')
print(pd.concat([X_test, y_test], axis=1))
主要机制
决策树分类器通过递归地根据最具信息量的特征来划分数据。以下是它的工作原理:
-
从根节点开始,包含整个数据集。
-
选择最佳特征来划分数据(基于如基尼杂质等衡量标准)。
-
为所选特征的每个可能值创建子节点。
-
对每个子节点重复步骤 2-3,直到满足停止标准(例如,最大深度达到、每叶最小样本数、或纯叶节点)。
-
将多数类别分配给每个叶节点。

训练步骤
在 scikit-learn 中,决策树算法称为 CART(分类与回归树)。它构建二叉树,通常遵循以下步骤:
- 从根节点开始,包含所有训练样本。

从根节点开始,根节点包含所有 14 个训练样本,我们将找出最佳特征及最佳切分点,开始构建树。
- 对每个特征:
a. 对特征值进行排序。
b. 考虑相邻值之间的所有可能阈值作为潜在的切分点。

在这个根节点中,有 23 个切分点需要检查。二元列仅有一个切分点。
def potential_split_points(attr_name, attr_values):
sorted_attr = np.sort(attr_values)
unique_values = np.unique(sorted_attr)
split_points = [(unique_values[i] + unique_values[i+1]) / 2 for i in range(len(unique_values) - 1)]
return {attr_name: split_points}
# Calculate and display potential split points for all columns
for column in X_train.columns:
splits = potential_split_points(column, X_train[column])
for attr, points in splits.items():
print(f"{attr:11}: {points}")
- 对于每个潜在的切分点:
a. 计算当前节点的杂质(例如,基尼杂质)。
b. 计算杂质的加权平均值。

例如,对于特征“sunny”及分裂点 0.5,计算数据集两部分的杂质(如“基尼杂质”)。

另一个例子,类似的过程也可以应用于像“Temperature”这样的连续特征。
def gini_impurity(y):
p = np.bincount(y) / len(y)
return 1 - np.sum(p**2)
def weighted_average_impurity(y, split_index):
n = len(y)
left_impurity = gini_impurity(y[:split_index])
right_impurity = gini_impurity(y[split_index:])
return (split_index * left_impurity + (n - split_index) * right_impurity) / n
# Sort 'sunny' feature and corresponding labels
sunny = X_train['sunny']
sorted_indices = np.argsort(sunny)
sorted_sunny = sunny.iloc[sorted_indices]
sorted_labels = y_train.iloc[sorted_indices]
# Find split index for 0.5
split_index = np.searchsorted(sorted_sunny, 0.5, side='right')
# Calculate impurity
impurity = weighted_average_impurity(sorted_labels, split_index)
print(f"Weighted average impurity for 'sunny' at split point 0.5: {impurity:.3f}")
4. 在计算所有特征和分裂点的杂质后,选择最低的杂质值。

特征“overcast”在分裂点 0.5 时具有最低的杂质值。这意味着这个分裂点将是所有其他分裂点中最纯净的!
def calculate_split_impurities(X, y):
split_data = []
for feature in X.columns:
sorted_indices = np.argsort(X[feature])
sorted_feature = X[feature].iloc[sorted_indices]
sorted_y = y.iloc[sorted_indices]
unique_values = sorted_feature.unique()
split_points = (unique_values[1:] + unique_values[:-1]) / 2
for split in split_points:
split_index = np.searchsorted(sorted_feature, split, side='right')
impurity = weighted_average_impurity(sorted_y, split_index)
split_data.append({
'feature': feature,
'split_point': split,
'weighted_avg_impurity': impurity
})
return pd.DataFrame(split_data)
# Calculate split impurities for all features
calculate_split_impurities(X_train, y_train).round(3)
5. 根据选择的特征和分裂点创建两个子节点:
-
左子节点:特征值 <= 分裂点的样本
-
右子节点:特征值 > 分裂点的样本

选定的分裂点将数据分为两部分。由于一部分已经是纯净的(右侧!这就是为什么它的杂质值较低!),我们只需要在左侧节点继续构建决策树。
6. 递归地重复步骤 2–5,直到达到每个子节点。你也可以在满足停止条件时停止(例如,达到最大深度、每个叶节点的最小样本数或最小杂质减少)。




# Calculate split impurities forselected index
selected_index = [4,8,3,13,7,9,10] # Change it depending on which indices you want to check
calculate_split_impurities(X_train.iloc[selected_index], y_train.iloc[selected_index]).round(3)
from sklearn.tree import DecisionTreeClassifier
# The whole Training Phase above is done inside sklearn like this
dt_clf = DecisionTreeClassifier()
dt_clf.fit(X_train, y_train)
最终完整的树
叶节点的类别标签是到达该节点的训练样本的多数类。

右侧的树是最终将用于分类的决策树。在这一点上,我们不再需要样本。
import matplotlib.pyplot as plt
from sklearn.tree import plot_tree
# Plot the decision tree
plt.figure(figsize=(20, 10))
plot_tree(dt_clf, filled=True, feature_names=X.columns, class_names=['Not Play', 'Play'])
plt.show()

在这个 scikit-learn 输出中,还存储了非叶节点的信息,例如该节点的样本数和每个类别的样本数(值)。
分类步骤
训练好决策树后,预测过程是这样进行的:
-
从训练好的决策树的根节点开始。
-
评估当前节点的特征和分裂条件。
-
在每个后续节点重复步骤 2,直到达到叶节点。
-
叶节点的类别标签成为新实例的预测结果。

我们只需要决策树所要求的列。除了“overcast”和“Temperature”之外,其他值在做出预测时并不重要。
# Make predictions
y_pred = dt_clf.predict(X_test)
print(y_pred)
评估步骤

决策树提供了足够的准确性。由于我们的树只检查了两个特征,它可能无法很好地捕捉到测试集的特征。
# Evaluate the classifier
print(f"Accuracy: {accuracy_score(y_test, y_pred)}")
关键参数
决策树有几个重要参数,控制它们的生长和复杂度:
1 . 最大深度:设置树的最大深度,这可以有效地防止过拟合。
👍 有用提示:考虑从一个浅层树开始(可能是 3 到 5 层深),然后逐渐增加深度。

从一个浅层树开始(例如,深度为 3–5),逐步增加,直到找到模型复杂性和验证数据性能之间的最佳平衡。
- 最小样本分割:此参数决定了拆分内部节点所需的最小样本数。
👍 有用提示:将此值设置为较高(大约训练数据的 5%–10%)可以帮助防止决策树创建过多的小而具体的划分,这些划分可能不能很好地推广到新数据。

- 最小样本叶节点:此参数指定叶节点所需的最小样本数。
👍 有用提示:选择一个值,确保每个叶子节点表示一个有意义的数据子集(大约占训练数据的 1%–5%)。这可以帮助避免过于具体的预测。

- 标准:用于衡量划分质量的函数(通常使用“gini”表示 Gini 不纯度,或“entropy”表示信息增益)。
👍 有用提示:虽然 Gini 一般更简单且计算速度更快,但熵在多分类问题中通常表现更好。不过,它们通常会给出相似的结果。

计算“晴天”熵的示例,分割点为 0.5。
优缺点
像任何机器学习算法一样,决策树也有其优点和局限性。
优点:
-
可解释性:容易理解和可视化决策过程。
-
无特征缩放:可以处理数值型和类别型数据,无需归一化。
-
处理非线性关系:能够捕捉数据中的复杂模式。
-
特征重要性:提供了一个清晰的指示,表明哪些特征对于预测最为重要。
缺点:
-
过拟合:容易生成过于复杂的树,导致泛化能力差,尤其是在数据集较小时。
-
不稳定性:数据的微小变化可能会导致生成完全不同的决策树。
-
偏向不平衡的数据集:可能会偏向占主导地位的类别。
-
无法外推:无法对训练数据范围之外的情况做出预测。
在我们的高尔夫例子中,决策树可能会基于天气条件创建非常准确且可解释的规则来决定是否打高尔夫。然而,如果没有适当修剪,或者数据集较小,它可能会过拟合特定条件的组合。
最终总结
决策树分类器是解决机器学习中许多问题的绝佳工具。它们易于理解,能够处理复杂数据,并能展示它们如何做出决策。这使得它们在多个领域都非常有用,从商业到医学。虽然决策树强大且可解释,但它们通常作为更高级集成方法的构建模块,如随机森林或梯度提升机。
🌟 决策树分类器简化版
# Import libraries
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np
from sklearn.tree import plot_tree, DecisionTreeClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
# Load data
dataset_dict = {
'Outlook': ['sunny', 'sunny', 'overcast', 'rainy', 'rainy', 'rainy', 'overcast', 'sunny', 'sunny', 'rainy', 'sunny', 'overcast', 'overcast', 'rainy', 'sunny', 'overcast', 'rainy', 'sunny', 'sunny', 'rainy', 'overcast', 'rainy', 'sunny', 'overcast', 'sunny', 'overcast', 'rainy', 'overcast'],
'Temperature': [85.0, 80.0, 83.0, 70.0, 68.0, 65.0, 64.0, 72.0, 69.0, 75.0, 75.0, 72.0, 81.0, 71.0, 81.0, 74.0, 76.0, 78.0, 82.0, 67.0, 85.0, 73.0, 88.0, 77.0, 79.0, 80.0, 66.0, 84.0],
'Humidity': [85.0, 90.0, 78.0, 96.0, 80.0, 70.0, 65.0, 95.0, 70.0, 80.0, 70.0, 90.0, 75.0, 80.0, 88.0, 92.0, 85.0, 75.0, 92.0, 90.0, 85.0, 88.0, 65.0, 70.0, 60.0, 95.0, 70.0, 78.0],
'Wind': [False, True, False, False, False, True, True, False, False, False, True, True, False, True, True, False, False, True, False, True, True, False, True, False, False, True, False, False],
'Play': ['No', 'No', 'Yes', 'Yes', 'Yes', 'No', 'Yes', 'No', 'Yes', 'Yes', 'Yes', 'Yes', 'Yes', 'No', 'No', 'Yes', 'Yes', 'No', 'No', 'No', 'Yes', 'Yes', 'Yes', 'Yes', 'Yes', 'Yes', 'No', 'Yes']
}
df = pd.DataFrame(dataset_dict)
# Prepare data
df = pd.get_dummies(df, columns=['Outlook'], prefix='', prefix_sep='', dtype=int)
df['Wind'] = df['Wind'].astype(int)
df['Play'] = (df['Play'] == 'Yes').astype(int)
# Split data
X, y = df.drop(columns='Play'), df['Play']
X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=0.5, shuffle=False)
# Train model
dt_clf = DecisionTreeClassifier(
max_depth=None, # Maximum depth of the tree
min_samples_split=2, # Minimum number of samples required to split an internal node
min_samples_leaf=1, # Minimum number of samples required to be at a leaf node
criterion='gini' # Function to measure the quality of a split
)
dt_clf.fit(X_train, y_train)
# Make predictions
y_pred = dt_clf.predict(X_test)
# Evaluate model
print(f"Accuracy: {accuracy_score(y_test, y_pred)}")
# Visualize tree
plt.figure(figsize=(20, 10))
plot_tree(dt_clf, filled=True, feature_names=X.columns,
class_names=['Not Play', 'Play'], impurity=False)
plt.show()
进一步阅读
若想详细了解决策树分类器及其在 scikit-learn 中的实现,读者可参考官方文档,文档中提供了关于其使用和参数的全面信息。
技术环境
本文使用 Python 3.7 和 scikit-learn 1.5。虽然讨论的概念一般适用,但具体的代码实现可能会因版本不同而略有差异。
关于插图
除非另有说明,所有图片均由作者创作,并融合了来自 Canva Pro 的授权设计元素。

若想查看决策树分类器的简明视觉总结,请访问配套的 Instagram 帖子。
参考文献
[1] T. M. Mitchell, 机器学习(1997),McGraw-Hill Science/Engineering/Math,第 59 页
𝙎𝙚𝙚 𝙢𝙤𝙧𝙚 𝘾𝙡𝙖𝙨𝙨𝙞𝙛𝙞𝙘𝙖𝙩𝙞𝙤𝙣 𝘼𝙡𝙜𝙤𝙧𝙞𝙩𝙝𝙢𝙨 𝙝𝙚𝙧𝙚:

分类算法
查看列表8 个故事


𝙔𝙤𝙪 𝙢𝙞𝙜𝙝𝙩 𝙖𝙡𝙨𝙤 𝙡𝙞𝙠𝙚:

回归算法
查看列表5 个故事



集成学习
查看列表4 个故事


决策树回归器解释:带有代码示例的可视化指南
回归算法
明智地修剪分支,通过成本复杂度修剪
·发表于Towards Data Science ·阅读时间:11 分钟·2024 年 10 月 10 日
--
对我们喜爱的倒立树的全新视角
towardsdatascience.com
决策树不仅仅限于数据分类——它们同样擅长预测数值!分类树经常占据焦点,但决策树回归器(或回归树)在连续变量预测的世界中是强大且多功能的工具。
虽然我们将讨论回归树构建的机制(这些机制与分类树大致相同),但在这里,我们还将超越分类器文章中介绍的前修剪方法,如“最小样本叶节点”和“最大树深度”。我们将探索最常见的后修剪方法——成本复杂度修剪,它为决策树的成本函数引入了一个复杂度参数。

所有可视化图:作者使用 Canva Pro 创作。优化为移动端显示;在桌面端可能显得过大。
定义
回归决策树是一种使用树状结构预测数值的模型。它根据关键特征拆分数据,从根问题开始并分支。每个节点询问一个特征,继续拆分数据,直到达到叶节点并作出最终预测。要获得结果,你需要从根节点到叶节点,沿着匹配数据特征的路径进行跟踪。

回归决策树通过一系列基于数据的问题来预测数值结果,逐步缩小范围直到最终结果。
📊 使用的数据集
为了演示我们的概念,我们将使用我们的标准数据集。该数据集用于预测某一天访客高尔夫球场的人数,包含天气展望、温度、湿度和风力等变量。

列:‘天气展望’(通过独热编码转为晴天、阴天、雨天),‘温度’(以华氏度表示),‘湿度’(以百分比表示),‘风力’(是/否)和‘玩家数量’(数值型,目标特征)
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
# Create dataset
dataset_dict = {
'Outlook': ['sunny', 'sunny', 'overcast', 'rain', 'rain', 'rain', 'overcast', 'sunny', 'sunny', 'rain', 'sunny', 'overcast', 'overcast', 'rain', 'sunny', 'overcast', 'rain', 'sunny', 'sunny', 'rain', 'overcast', 'rain', 'sunny', 'overcast', 'sunny', 'overcast', 'rain', 'overcast'],
'Temperature': [85.0, 80.0, 83.0, 70.0, 68.0, 65.0, 64.0, 72.0, 69.0, 75.0, 75.0, 72.0, 81.0, 71.0, 81.0, 74.0, 76.0, 78.0, 82.0, 67.0, 85.0, 73.0, 88.0, 77.0, 79.0, 80.0, 66.0, 84.0],
'Humidity': [85.0, 90.0, 78.0, 96.0, 80.0, 70.0, 65.0, 95.0, 70.0, 80.0, 70.0, 90.0, 75.0, 80.0, 88.0, 92.0, 85.0, 75.0, 92.0, 90.0, 85.0, 88.0, 65.0, 70.0, 60.0, 95.0, 70.0, 78.0],
'Wind': [False, True, False, False, False, True, True, False, False, False, True, True, False, True, True, False, False, True, False, True, True, False, True, False, False, True, False, False],
'Num_Players': [52, 39, 43, 37, 28, 19, 43, 47, 56, 33, 49, 23, 42, 13, 33, 29, 25, 51, 41, 14, 34, 29, 49, 36, 57, 21, 23, 41]
}
df = pd.DataFrame(dataset_dict)
# One-hot encode 'Outlook' column
df = pd.get_dummies(df, columns=['Outlook'],prefix='',prefix_sep='')
# Convert 'Wind' column to binary
df['Wind'] = df['Wind'].astype(int)
# Rearrange columns
column_order = ['sunny', 'overcast', 'rain', 'Temperature', 'Humidity', 'Wind', 'Num_Players']
df = df[column_order]
# Split features and target
X, y = df.drop('Num_Players', axis=1), df['Num_Players']
X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=0.5, shuffle=False)
主要机制
回归决策树通过递归地根据最能减少预测误差的特征来拆分数据。以下是一般过程:
-
从根节点开始,使用整个数据集。
-
选择最小化特定误差指标(如均方误差或方差)的特征来拆分数据。
-
根据拆分创建子节点,每个子节点表示与相应特征值对齐的数据子集。
-
对每个子节点重复步骤 2–3,继续拆分数据直到达到停止条件。
-
为每个叶节点分配一个最终的预测值,通常是该节点中目标值的平均值。

训练步骤
我们将探索决策树算法 CART(分类与回归树)中的回归部分。它构建二叉树,通常遵循以下步骤:
- 从根节点开始,使用所有训练样本。

- 对于数据集中的每个特征:
a. 按升序排序特征值。
b. 将相邻值之间的所有中点视为潜在的拆分点。

总共需要检查 23 个拆分点。
- 对每个潜在拆分点:
a. 计算当前节点的均方误差(MSE)。
b. 计算结果拆分的加权平均误差。

例如,在这里,我们计算了拆分点“温度”值为 73.5 时的均方误差(MSE)的加权平均值。
def calculate_split_mse(X_train, y_train, feature_name, split_point):
# Create DataFrame and sort by feature
analysis_df = pd.DataFrame({
'feature': X_train[feature_name],
'y_actual': y_train
}).sort_values('feature')
# Split data and calculate means
left_mask = analysis_df['feature'] <= split_point
left_mean = analysis_df[left_mask]['y_actual'].mean()
right_mean = analysis_df[~left_mask]['y_actual'].mean()
# Calculate squared differences
analysis_df['squared_diff'] = np.where(
left_mask,
(analysis_df['y_actual'] - left_mean) ** 2,
(analysis_df['y_actual'] - right_mean) ** 2
)
# Calculate MSEs and counts
left_mse = analysis_df[left_mask]['squared_diff'].mean()
right_mse = analysis_df[~left_mask]['squared_diff'].mean()
n_left = sum(left_mask)
n_right = len(analysis_df) - n_left
# Calculate weighted average MSE
weighted_mse = (n_left * left_mse + n_right * right_mse) / len(analysis_df)
# Print results
print(analysis_df)
print(f"\nResults for split at {split_point} on feature '{feature_name}':")
print(f"Left child MSE (n={n_left}, mean={left_mean:.2f}): {left_mse:.2f}")
print(f"Right child MSE (n={n_right}, mean={right_mean:.2f}): {right_mse:.2f}")
print(f"Weighted average MSE: {weighted_mse:.2f}")
# Example usage:
calculate_split_mse(X_train, y_train, 'Temperature', 73.5)

- 在评估所有特征和拆分点后,选择均方误差(MSE)加权平均值最低的一个。

def evaluate_all_splits(X_train, y_train):
"""Evaluate all possible split points using midpoints for all features"""
results = []
for feature in X_train.columns:
data = pd.DataFrame({'feature': X_train[feature], 'y_actual': y_train})
splits = [(a + b)/2 for a, b in zip(sorted(data['feature'].unique())[:-1],
sorted(data['feature'].unique())[1:])]
for split in splits:
left_mask = data['feature'] <= split
n_left = sum(left_mask)
if not (0 < n_left < len(data)): continue
left_mean = data[left_mask]['y_actual'].mean()
right_mean = data[~left_mask]['y_actual'].mean()
left_mse = ((data[left_mask]['y_actual'] - left_mean) ** 2).mean()
right_mse = ((data[~left_mask]['y_actual'] - right_mean) ** 2).mean()
weighted_mse = (n_left * left_mse + (len(data) - n_left) * right_mse) / len(data)
results.append({'Feature': feature, 'Split_Point': split, 'Weighted_MSE': weighted_mse})
return pd.DataFrame(results).round(2)
# Example usage:
results = evaluate_all_splits(X_train, y_train)
print(results)

5. 根据选择的特征和切分点创建两个子节点:
-
左子节点:特征值 <= 切分点的样本
-
右子节点:特征值 > 切分点的样本

6. 对每个子节点递归重复步骤 2 到 5。(继续直到满足停止准则。)


7. 在每个叶节点,分配该节点中样本的平均目标值作为预测值。

from sklearn.tree import DecisionTreeRegressor, plot_tree
import matplotlib.pyplot as plt
# Train the model
regr = DecisionTreeRegressor(random_state=42)
regr.fit(X_train, y_train)
# Visualize the decision tree
plt.figure(figsize=(26,8))
plot_tree(regr, feature_names=X.columns, filled=True, rounded=True, impurity=False, fontsize=16, precision=2)
plt.tight_layout()
plt.show()

在这个 scikit-learn 的输出中,展示了叶节点和中间节点的样本及其值。
回归/预测步骤
这是回归树如何对新数据进行预测的过程:
1. 从树的顶部(根节点)开始。
2. 在每个决策点(节点):
-
查看特征和切分值。
-
如果数据点的特征值较小或相等,向左走。
-
如果它更大,向右走。
3. 一直向下移动直到到达树的末端(叶节点)。
4. 预测值是该叶节点中存储的平均值。

评估步骤

这个 RMSE 值比虚拟回归器的结果要好得多。
预剪枝与后剪枝
在构建完树之后,我们唯一需要担心的就是如何使树变小,以防止过拟合。通常,修剪的方法可以分为以下几类:
预剪枝
预剪枝,也称为早期停止,是指在训练过程中基于某些预定义的标准,停止决策树的生长。此方法旨在防止树变得过于复杂并导致过拟合。常见的预剪枝技术包括:
-
最大深度:限制树的最大深度。
-
每次切分的最小样本数:要求切分一个节点时,必须满足最小样本数的条件。
-
每个叶节点的最小样本数:确保每个叶节点至少包含一定数量的样本。
-
最大叶节点数:限制树中叶节点的总数。
-
最小纯度减少:仅允许那些减少纯度达到指定值的切分。
这些方法在满足指定条件时停止树的生长,有效地在树的构建阶段进行“修剪”。
(我们之前已经讨论过这些方法,它们在回归问题中完全相同。)
后剪枝
后修剪方法允许决策树生长到最大,然后修剪回去以减少复杂性。这种方法首先构建完整的树,然后移除或合并那些对模型表现贡献不大的分支。一种常见的后修剪技术被称为成本复杂度修剪。
成本复杂度修剪
步骤 1:计算每个节点的不纯度
对于每个中间节点,计算不纯度(回归问题中的 MSE)。然后我们将这个值从小到大排序。

# Visualize the decision tree
plt.figure(figsize=(26,8))
plot_tree(regr, feature_names=X.columns, filled=True, rounded=True, impurity=True, fontsize=16, precision=2)
plt.tight_layout()
plt.show()

在这个 scikit-learn 的输出中,节点的不纯度显示为每个节点的“squared_error”。

我们给这些中间节点(从 A 到 J)命名。然后我们根据它们的 MSE 从小到大进行排序。
步骤 2:通过修剪最弱的链接来创建子树
目标是从均方误差(MSE)最小的节点开始,逐渐将中间节点转化为叶子节点。我们可以基于此创建一个修剪路径。

我们将它们命名为“子树i”,基于它被修剪的次数(i)。从原始树开始,树会在具有最低 MSE 的节点上修剪(从节点 J 开始,M(已经被 J 修剪掉),L,K,依此类推)。
步骤 3:计算每棵子树的总叶子不纯度
对于每一棵子树T,可以计算总叶子不纯度(R(T)):
R(T) = (1/N) Σ I(L) * n_L
其中:
· L 遍历所有叶子节点
· n_L 是叶子L中样本的数量 · N 是树中样本的总数
· I(L) 是叶子L的不纯度(MSE)

我们修剪得越多,总的叶子不纯度就越高。
步骤 4:计算成本函数
为了控制何时停止将中间节点转化为叶子节点,我们首先使用以下公式检查每棵子树T的成本复杂度:
Cost(T) = R(T) + α * |T|
其中:
· R(T) 是总叶子不纯度
· |T| 是子树中的叶子节点数· α 是复杂度参数

步骤 5:选择 Alpha
α的值控制我们最终得到哪一棵子树。具有最低成本的子树将是最终的树。

当α较小时,我们更关心准确性(较大的树)。当α较大时,我们更关心简洁性(较小的树)。
虽然我们可以自由设定α,在 scikit-learn 中,你也可以获取最小的α值来获得特定的子树。这被称为有效的α。

这个有效的α 也可以计算出来。
# Compute the cost-complexity pruning path
tree = DecisionTreeRegressor(random_state=42)
effective_alphas = tree.cost_complexity_pruning_path(X_train, y_train).ccp_alphas
impurities = tree.cost_complexity_pruning_path(X_train, y_train).impurities
# Function to count leaf nodes
count_leaves = lambda tree: sum(tree.tree_.children_left[i] == tree.tree_.children_right[i] == -1 for i in range(tree.tree_.node_count))
# Train trees and count leaves for each complexity parameter
leaf_counts = [count_leaves(DecisionTreeRegressor(random_state=0, ccp_alpha=alpha).fit(X_train_scaled, y_train)) for alpha in effective_alphas]
# Create DataFrame with analysis results
pruning_analysis = pd.DataFrame({
'total_leaf_impurities': impurities,
'leaf_count': leaf_counts,
'cost_function': [f"{imp:.3f} + {leaves}α" for imp, leaves in zip(impurities, leaf_counts)],
'effective_α': effective_alphas
})
print(pruning_analysis)

最终备注
预剪枝方法通常更快且更节省内存,因为它们从一开始就防止了树形过大。
后剪枝可能创建出更优的树形结构,因为它会在做出剪枝决定之前考虑整个树的结构。然而,这可能会消耗更多的计算资源。
两种方法的目标都是在模型复杂度和性能之间找到平衡,目的是创建一个能很好地对未见数据进行泛化的模型。选择预剪枝还是后剪枝(或两者结合)通常取决于具体的数据集、当前问题以及可用的计算资源。
在实际应用中,通常会结合使用这些方法,比如先应用一些预剪枝标准来防止树形过大,再使用后剪枝对模型的复杂度进行微调。
🌟 决策树回归器(带成本复杂度剪枝)代码总结
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.metrics import root_mean_squared_error
from sklearn.tree import DecisionTreeRegressor
from sklearn.preprocessing import StandardScaler
# Create dataset
dataset_dict = {
'Outlook': ['sunny', 'sunny', 'overcast', 'rain', 'rain', 'rain', 'overcast', 'sunny', 'sunny', 'rain', 'sunny', 'overcast', 'overcast', 'rain', 'sunny', 'overcast', 'rain', 'sunny', 'sunny', 'rain', 'overcast', 'rain', 'sunny', 'overcast', 'sunny', 'overcast', 'rain', 'overcast'],
'Temperature': [85.0, 80.0, 83.0, 70.0, 68.0, 65.0, 64.0, 72.0, 69.0, 75.0, 75.0, 72.0, 81.0, 71.0, 81.0, 74.0, 76.0, 78.0, 82.0, 67.0, 85.0, 73.0, 88.0, 77.0, 79.0, 80.0, 66.0, 84.0],
'Humidity': [85.0, 90.0, 78.0, 96.0, 80.0, 70.0, 65.0, 95.0, 70.0, 80.0, 70.0, 90.0, 75.0, 80.0, 88.0, 92.0, 85.0, 75.0, 92.0, 90.0, 85.0, 88.0, 65.0, 70.0, 60.0, 95.0, 70.0, 78.0],
'Wind': [False, True, False, False, False, True, True, False, False, False, True, True, False, True, True, False, False, True, False, True, True, False, True, False, False, True, False, False],
'Num_Players': [52,39,43,37,28,19,43,47,56,33,49,23,42,13,33,29,25,51,41,14,34,29,49,36,57,21,23,41]
}
df = pd.DataFrame(dataset_dict)
# One-hot encode 'Outlook' column
df = pd.get_dummies(df, columns=['Outlook'], prefix='', prefix_sep='', dtype=int)
# Convert 'Wind' column to binary
df['Wind'] = df['Wind'].astype(int)
# Split data into features and target, then into training and test sets
X, y = df.drop(columns='Num_Players'), df['Num_Players']
X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=0.5, shuffle=False)
# Initialize Decision Tree Regressor
tree = DecisionTreeRegressor(random_state=42)
# Get the cost complexity path, impurities, and effective alpha
path = tree.cost_complexity_pruning_path(X_train, y_train)
ccp_alphas, impurities = path.ccp_alphas, path.impurities
print(ccp_alphas)
print(impurities)
# Train the final tree with the chosen alpha
final_tree = DecisionTreeRegressor(random_state=42, ccp_alpha=0.1)
final_tree.fit(X_train_scaled, y_train)
# Make predictions
y_pred = final_tree.predict(X_test)
# Calculate and print RMSE
rmse = root_mean_squared_error(y_test, y_pred)
print(f"RMSE: {rmse:.4f}")
进一步阅读
关于决策树回归器、成本复杂度剪枝及其在 scikit-learn 中的实现,读者可以参考其官方文档。该文档提供了关于使用方法和参数的全面信息。
技术环境
本文使用 Python 3.7 和 scikit-learn 1.5。虽然讨论的概念通常适用,但具体的代码实现可能会因版本不同而略有差异。
关于插图
除非另有说明,所有图像均由作者创作,并结合了 Canva Pro 授权的设计元素。
𝙎𝙚𝙚 𝙢𝙤𝙧𝙚 𝙍𝙚𝙜𝙧𝙚𝙨𝙨𝙞𝙤𝙣 𝘼𝙡𝙜𝙤𝙧𝙞𝙩𝙝𝙢𝙨 𝙝𝙚𝙧𝙚:

回归算法
查看列表5 个故事


𝙔𝙤𝙪 𝙢𝙞𝙜𝙝𝙩 𝙖𝙡𝙨𝙤 𝙡𝙞𝙠𝙚:

分类算法
查看列表 8 个故事!

声明式与命令式绘图
快速成功的数据科学
为 Python 初学者提供概览
·发表于 Towards Data Science ·阅读时间 11 分钟 ·2024 年 1 月 10 日
--

声明式与命令式由列奥纳多的绝对现实 v16 进行想象
如果你正在学习 Python,预计你会用Matplotlib制作你的第一个图表。Matplotlib 不仅极其流行,而且是一个命令式绘图库。这意味着它使用逐步的方式生成图形,这对初学者来说很容易理解。
Python 也支持声明式绘图库,例如seaborn、Altair和HoloViews,它们让你专注于图表应展示什么内容,而不是如何绘制它。引用 Altair 文档中的话,“关键思想是你在声明数据列与视觉编码通道之间的联系,例如 x 轴、y 轴和颜色。其余的绘图细节会自动处理。基于这个声明式系统,可以通过简洁的语法创建出从简单到复杂的各种图表。”
科学家和工程师应该会觉得声明式方法很有吸引力,因为它能让他们把更多时间花在实际工作上,而不是编写代码。正如我喜欢说的,“科学优先,编码其次!”
在这篇快速成功的数据科学文章中,我们将探讨命令式和声明式绘图。主要的重点将放在声明式风格上,使用来自 seaborn、Plotly Express和hvplot的示例。
解码独热编码:类别数据的初学者指南
学习将类别数据转化为机器学习模型能够理解的格式
·发表于 Towards Data Science ·6 分钟阅读·2024 年 11 月 11 日
--

引言
在学习机器学习时,理解最基本算法的内部工作原理至关重要。这样可以帮助我们理解算法在流行的库和框架中的运作方式,如何调试它们,更轻松地选择更好的超参数,并确定哪种算法最适合特定问题。
虽然算法是机器学习的核心,但如果没有高质量的数据,它们无法产生有效的结果。由于在某些问题中数据可能是稀缺资源,因此学习如何有效地预处理数据以提取最大价值变得至关重要。此外,预处理不当的数据可能会削弱算法的性能。

即使是一个非常简单的模型,如果数据准备得当,也能优于一个复杂的模型。
在本文中,我们将探讨独热编码,这是数据预处理过程中最基础的技术之一。为了有效地进行这一过程,我们将首先理解数据编码的基本动机,然后探索其原理和…
解密准确天气预报背后的技巧:变分数据同化
学习如何实现变分数据同化,包括数学细节和利用 PyTorch 进行高效实现
·发表于 Towards Data Science ·阅读时长 11 分钟·2024 年 12 月 25 日
--

1. 快速入门:为什么选择数据同化
天气预报模型是混沌动力系统,其中预报会因为模型状态的微小扰动而变得不稳定,这使得盲目信任预报结果存在风险。虽然当前的天气预报服务,如欧洲中期天气预报中心(ECMWF),在中期(15 天)到季节性天气预测方面取得了较高的准确性。良好预报背后的技巧就在于自 1997 年以来在 ECMWF 中使用的 4 维变分数据同化(4D-Var)。该算法通过结合实时观测数据来改进预报。作为减少蝴蝶效应——对初始条件的高度敏感性——的主要技术,4D-Var 还广泛应用于其他领域的时间序列预测系统中。

ECMWF 数据同化的示意图。来源:www.ecmwf.int/sites/default/files/2023-08/Fact%20sheet%20-%20Reanalysis%20-v3_1.pdf。版权所有 © ECMWF,依据 CC BY-SA 4.0 协议授权。
解码时间:揭开 LSTM 与 N-BEATS 在精确时间序列预测中的力量

图片来源:Aron Visuals 在 Unsplash
比较两个深度学习模型在短期和长期的表现
·发表于 Towards Data Science ·6 分钟阅读·2024 年 4 月 19 日
--
时间序列预测在各个领域中扮演着至关重要的角色,通过预测未来的趋势提供了重要的决策支持。本次探索聚焦于两个著名的深度学习模型,深入分析它们各自的优势与局限。
LSTM(长短期记忆)作为 RNN 的一个特殊变种,在捕捉具有长期依赖关系的序列数据模式方面表现突出。它通过有效地解决梯度消失问题,增强了传统的 RNN,能够建模更长时间的依赖关系。LSTM 通过引入记忆单元和门控机制(包括输入门、输出门和遗忘门)来实现这一点,能够选择性地保留或遗忘信息。LSTM 模型的一个固有限制是,预测的时间范围必须与训练时使用的输入序列的长度相匹配。

LSTM 架构基于 RNN,[1]
N-BEATS(时间序列的神经基础扩展分析)代表了一种非递归架构,以其准确预测多个时间序列的能力而闻名。该模型由不同的构建模块组成,采用层次化结构,其中每个模块专门负责预测特定的时间范围。 “回溯”模块深入历史时间范围,而“预测”模块则专注于预测未来的时间段,这些时间段的长度可能不同。

N-BEATS 的回溯窗口和预测时间范围可能具有不同的大小[2]

N-BEATS 架构[2]

N-BEATS 架构中的每个堆栈(左)和每个堆栈内的每个块(右)[2]
N-BEATS 在建模季节性方面表现突出,它通过将时间序列分解为趋势和季节性,类似于 STL(基于 LOESS 的季节性趋势分解)方法。这种分解方法使得模型能够分别捕捉短期波动(季节性)和长期趋势。它利用快速傅里叶变换(Fast Fourier Transform)有效建模季节性,这是时间序列分析中的一个关键组成部分。N-BEATS 被设计为能够适应不同类型的季节性模式,包括规律性和不规律性模式。
实际示例
为了更好地理解上述理论概念,让我们通过应用两种模型来深入探讨一个实际的例子。首先,我们将生成具有上升趋势和季节性模式的每日频率合成数据。
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
# Generate synthetic time series data
def generate_synthetic_data():
# Create a time range with daily frequency
start_time = pd.Timestamp("1990-01-01 00:00:00")
end_time = pd.Timestamp("2023-12-31 23:59:00")
time = pd.date_range(start=start_time, end=end_time, freq='D')
# Weekly seasonality and ascending trend
weekly_seasonality = 10 * np.sin(2 * np.pi *
np.arange(len(time)) / (7 * 24 * 60))
ascending_trend = 0.01 * np.arange(len(time))
# Combine the components to generate synthetic data
uplift = 100
x = weekly_seasonality + ascending_trend + uplift
noise_level = 5
noise = white_noise(len(time), noise_level, seed=42)
x += noise
return time, x
def white_noise(length, noise_level=1, seed=None):
rnd = np.random.RandomState(seed)
return rnd.randn(length) * noise_level
time_series_data = generate_synthetic_data_minute()
data = pd.DataFrame({'ds': time_series_data[0], 'y': time_series_data[1]})
data= data.set_index("ds")
# Plot the generated synthetic data
plt.plot(data["y"])
plt.title("Synthetic Time Series Data")
plt.show()
模拟数据如下所示:

合成时间序列数据
数据集展示了季节性、上升趋势和噪声,使得大多数模型很难进行准确的预测。这一难点源于许多模型专门识别长期或短期的模式,通常是各自独立的。因此,准确预测这类数据需要一个能够同时捕捉这两种模式的模型。为此,数据已被缩放,以确保与 LSTM 模型兼容。
data = data.reset_index()
train_data = data[data.ds<='2022-01-01']
test_data = data[data.ds>'2022-01-01']
# Normalize the data
scaler = MinMaxScaler()
train_data['x'] = scaler.fit_transform(train_data[['x']])
test_data['x'] = scaler.transform(test_data[['x']])
# Create sequences for the LSTM model
sequence_length = 10
train_sequences = []
test_sequences = []
for i in range(len(train_data) - sequence_length):
train_sequences.append(train_data['x'].iloc[i:i+sequence_length].values)
for i in range(len(test_data) - sequence_length):
test_sequences.append(test_data['x'].iloc[i:i+sequence_length].values)
train_sequences = np.array(train_sequences)
test_sequences = np.array(test_sequences)
# Prepare train and test targets
train_targets = train_data['x'].iloc[sequence_length:].values
test_targets = test_data['x'].iloc[sequence_length:].values
import time
start_time = time.time()
# Create and train an LSTM model
model = Sequential()
model.add(LSTM(50, activation='relu', input_shape=(sequence_length, 1)))
model.add(Dense(1))
model.compile(optimizer='adam', loss='mean_squared_error')
model.fit(train_sequences.reshape(-1, sequence_length, 1), train_targets,
epochs=5, batch_size=32)
# Make predictions
test_predictions = model.predict(
test_sequences.reshape(-1, sequence_length, 1))
print(time.time() - start_time)
# Inverse transform the predictions to the original scale
test_predictions = scaler.inverse_transform(test_predictions).flatten()
test_targets = scaler.inverse_transform(test_targets.reshape(-1, 1))
该数据集的模型训练用了 23 秒。测试集和预测覆盖了 2022 年 1 月 1 日之后的时间段。以下是预测的值:
# Plot the original data and LSTM predictions
plt.figure(figsize=(10, 6))
plt.plot(test_data['ds'].iloc[sequence_length:], test_targets,
label="Actual Data", linestyle='-')
plt.plot(test_data['ds'].iloc[sequence_length:], test_predictions,
label="LSTM Predictions", linestyle='--')
plt.xlabel("Time")
plt.ylabel("Value")
plt.legend()
plt.grid(False)
plt.show()

LSTM 模型对测试集的预测

LSTM 模型对整个数据集的预测
预测结果似乎有效地捕捉到了上升趋势和季节性,这令人乐观。然而,预测中似乎存在相当大的噪声。此外,随着预测范围的延长,预测值与测试数据之间的偏差变得明显。
现在,让我们评估加权绝对百分比误差(WAPE):
test_predictions_df = pd.DataFrame(test_predictions, columns = ["LSTM"])
test_targets_df = pd.DataFrame(test_targets, columns = ["actuals"])
predictions = pd.concat([test_predictions_df, test_targets_df], axis=1)
wape = (predictions['actuals'] - predictions['LSTM']).abs().sum()
/ predictions['actuals'].sum()
print(wape * 100)
WAPE = 1.89%
结果确实很有前景。
接下来,让我们探索如何将 N-BEATS 模型拟合到相同的数据集,并检查其性能。
data = generate_data_specific_column(col_name = "y")
# create column unique_id specifically for N-BEATS model with only 1.0
data['unique_id'] = 1.0
data = data.reset_index()
train_data = data[data.ds<='2022-01-01']
test_data = data[data.ds>'2022-01-01']
from neuralforecast.models import NBEATS, NHITS
from neuralforecast import NeuralForecast
import time
start_time = time.time()
horizon = len(test_data)
models = [NBEATS(input_size=2 * horizon, h=horizon, max_steps=50)]
nf = NeuralForecast(models=models, freq='D')
nf.fit(df=train_data) # default optimizer is MAE
test_predictions = nf.predict().reset_index()
print(time.time() - start_time)
N-BEATS 模型在 83 秒内完成了训练,虽然速度较快,但仍然比 LSTM 模型慢。
现在,让我们来看看 N-BEATS 模型生成的预测。
# Plot predictions
predictions = test_data.merge(test_predictions, how='left',
on=['unique_id', 'ds'])
plt.figure(figsize=(10, 6))
plt.plot(predictions['ds'], predictions['y'], label="Actual Data",
linestyle='-')
plt.plot(predictions['ds'], predictions['NBEATS'],
label="NBEATS Predictions", linestyle='--')
plt.xlabel("Time")
plt.ylabel("Demand per Minute")
plt.legend()
plt.grid(False)
plt.show()

N-BEATS 在测试集中的预测

N-BEATS 在整个数据集中的预测
wape = (predictions['y'] - predictions['NBEATS']).abs().sum()
/ predictions['y'].sum()
print(wape * 100)
WAPE = 1.80%
两个模型根据所选指标展示了高准确性。有趣的是,N-BEATS 生成的预测似乎并不会随着预测时间段的结束而恶化。这表明 N-BEATS 可能特别适合需要长期预测的项目。
总结
N-BEATS 和 LSTM 为预测领域带来了独特的优势。N-BEATS 在捕捉长期预测中的多样模式方面表现突出,同时具有较高的可解释性。而 LSTM 则是一个快速、稳定且复杂的神经网络机制,能够实现准确的预测。
参考文献
*除非另有说明,所有图像均由作者生成
[1] Van Houdt, Greg & Mosquera, Carlos & Nápoles, Gonzalo. (2020). 长短期记忆模型综述。人工智能评论。53。10.1007/s10462–020–09838–1.
[2] Binte Habib, Adria. (2022). N-BEATS 架构工作流程的详细解释。10.13140/RG.2.2.36379.34083.
长短期记忆(LSTM)网络是最著名的递归神经网络类型之一。最初…
medium.com](https://medium.com/mlearning-ai/building-a-neural-network-zoo-from-scratch-the-long-short-term-memory-network-1cec5cf31b7?source=post_page-----ca5fdd20dbc--------------------------------) [## 使用 N-BEATS 进行 Python 中的多时间序列预测
想象一下拥有一个强大的预测解决方案,能够处理多时间序列数据,而不依赖于复杂的…
解码 Medium 上的写作成功
快速成功的数据科学
数据驱动的方法
·发布于 Towards Data Science ·12 分钟阅读·2024 年 5 月 7 日
--

Medium 关注者和收入的折线图(按作者)
如果你在 Medium 上写作,可能已经注意到平台提供的关于你文章和观众的丰富统计数据。深入分析这些数据可以揭示你当前表现的有用洞见,并帮助你在未来提升表现。
我从 2023 年 1 月开始在 Medium 上写作,制作了一些实用的 Python 编程教程,偶尔还涉及物理学、超自然现象和数据分析。直到 2023 年 2 月底,我才获得了 100 个关注者,并加入了 Medium 合伙人计划(是的,我们这些老手必须通过乞讨和借用才能获得第一批 100 个关注者,才能开始赚取收入)。经过 15 个月和 57 篇文章后,我决定回顾并反思我的进展。
在这篇文章中,我分享了我的结果,并展示了如何通过深入分析 Medium 的故事和观众指标来帮助你提升读者量和收入。
关键问题
以下是我想要解决的主要问题:
-
是基础课程还是高级课程更受欢迎?
-
哪些话题最受欢迎?
-
什么内容提升最多?
-
提升对收入的影响是什么?**
深入探讨 Anthropic 的稀疏自编码器 ✍️
探索大语言模型(LLMs)可解释性追求背后的概念
·发表于 Towards Data Science ·阅读时长:11 分钟·2024 年 5 月 31 日
--

图片来源:作者(Zephyra,卢玛利亚的守护者,由我的 4 岁孩子绘制)
“在神秘的卢玛利亚大地上,古老的魔法弥漫在空气中,生活着 Zephyra,这只以狮身鹰翼为特征的空灵狮鹫。Zephyra 是《真理法典》的崇高守护者,这本古老的文献蕴藏着宇宙的秘密。
“在一座神圣的洞窟中,法典由 Zephyra 那双绿色的眼睛守护,她能看破虚伪,揭示纯粹的真理。一天,一位黑暗巫师降临卢玛利亚大地,试图通过隐藏《法典》将世界笼罩在无知之中。村民们呼唤 Zephyra,她如希望的灯塔在空中翱翔。随着一阵雄伟的翅膀挥动,Zephyra 在树林周围创造了一个保护光障,击退了巫师,揭示了真理。”
“经过一场长久的决斗,最终结论是黑暗巫师无法与 Zephyra 的光芒抗衡。通过她的勇气和警觉,真正的光明继续照耀着卢玛利亚。随着时间流逝,卢玛利亚在 Zephyra 的守护下走向繁荣,而她所守护的真理也始终照亮着这片土地。这就是 Zephyra 传奇延续下去的方式!”
Anthropic 的“提取可解释特征之旅”
追随 Zephyra 的故事,Anthropic AI 深入探讨了从模型中提取有意义特征的探索过程。这项研究的核心思想在于理解神经网络中不同组件如何相互作用,以及每个组件所扮演的角色。
根据论文“迈向单一语义性:通过字典学习分解语言模型”,稀疏自动编码器能够成功地从模型中提取有意义的特征。换句话说,稀疏自动编码器帮助解决‘多义性’问题——神经激活同时对应多个意义/解释,通过专注于稀疏激活那些只包含单一解释的特征——换句话说,它们更具单向性。
为了理解这一切是如何完成的,我们可以参考自动编码器和稀疏自动编码器的精彩作品,这些作品由Tom Yeh 教授创作,解释了这些神奇机制的幕后工作原理。
(以下所有图片,除非另有说明,均来自 Tom Yeh 教授上面提到的 LinkedIn 帖子,已获他的许可进行编辑。)
首先,让我们首先探讨一下什么是自动编码器,它是如何工作的。
什么是自动编码器?
想象一个作家,他的桌子上散乱着各种纸张——有的是他写故事的笔记,有的是最终稿的副本,还有的是为他的动作故事绘制的插图。在这种混乱中,很难找到重要的部分——尤其是当作家很匆忙,出版社在电话中催促他两天内交书时。幸运的是,作家有一个非常高效的助手——这个助手确保杂乱的桌面被定期清理,类似的物品被分组整理,并把东西放到合适的位置。而且,当作家需要时,助手会帮助他快速找到正确的物品,帮助他按时完成出版社设定的截止日期。
嗯,这个助手的名字叫做自动编码器。它主要有两个功能——编码和解码。编码是指压缩输入数据并提取出关键特征(组织)。解码则是从编码表示中重建原始数据的过程,目标是尽量减少信息丢失(恢复)。
现在让我们看看这个助手是如何工作的。
自动编码器是如何工作的?
给定:四个训练样本 X1, X2, X3, X4.
[1] 编码器
第一步是将训练样本复制到目标 Y’。自动编码器的工作是重建这些训练样本。由于目标就是训练样本本身,所以使用了词‘Auto’,它是希腊语的‘自我’意思。
[2] 编码器:第一层 +ReLU
正如我们在之前的所有模型中所看到的,简单的权重和偏置矩阵结合 ReLU 非常强大,能够做出惊人的效果。因此,通过使用第一个编码层,我们将原始特征集的大小从 4x4 减少到 3x4。

简要回顾:
线性变换:输入嵌入向量与权重矩阵 W 相乘,然后与偏置向量b相加,
z = Wx + b,其中W是权重矩阵,x 是我们的词嵌入,b是偏置向量。
ReLU 激活函数:接下来,我们将 ReLU 应用于这个中间的 z。
ReLU 返回输入和零之间的逐元素最大值。数学上,h = max{0, z}。
[3] 编码器:第二层 + ReLU
上一层的输出由第二个编码器层处理,该层进一步将输入大小减小到 2x3。这时,相关特征的提取发生在此层。这个层也叫做“瓶颈”,因为该层的输出特征远小于输入特征。

[4] 解码器:第一层 + ReLU
一旦编码过程完成,下一步是解码相关特征,以构建最终输出。为此,我们将上一步的特征与相应的权重和偏置相乘,并应用 ReLU 层。结果是一个 3x4 的矩阵。

[5] 解码器:第二层 + ReLU
第二个解码器层(权重、偏置 + ReLU)应用于之前的输出,给出最终结果,即重建的 4x4 矩阵。我们这样做是为了恢复原始维度,以便将结果与我们的原始目标进行比较。

[6] 损失梯度和反向传播
一旦获得解码器层的输出,我们计算输出(Y)和目标(Y’)之间的均方误差(MSE)的梯度。为此,我们求解2*(Y-Y’),这给出了最终的梯度,激活反向传播过程,并相应地更新权重和偏置。

现在我们了解了自编码器的工作原理,接下来就该探讨它的稀疏变体如何实现大语言模型(LLMs)的可解释性。
稀疏自编码器——它是如何工作的?
首先,假设我们给定了:
- 在经过前馈层处理后的变换器输出,即假设我们有五个标记(X)的模型激活值。它们很好,但并不能揭示模型如何得出决策或进行预测。

这里的主要问题是:
是否有可能将每个激活(3D)映射到一个更高维度空间(6D),以帮助理解?
[1] 编码器:线性层
编码器层的第一步是将输入X与编码器权重相乘,并添加偏置(如在自编码器的第一步中所做)。

[2] 编码器:ReLU
下一子步骤是应用 ReLU 激活函数,加入非线性并抑制负激活。这种抑制导致许多特征被设为 0,从而实现稀疏性概念——输出稀疏且可解释的特征f.
当我们只有一个或两个正特征时,就会发生可解释性。如果我们检查f6,我们可以看到X2和X3是正的,并可以说它们两个有‘山’这一共同点。

[3] 解码器 : 重建
一旦完成编码器步骤,我们就进入解码器步骤。我们将f与解码器的权重相乘并添加偏置。这样输出的X’就是从可解释特征重建的X。

如同在自编码器中所做的那样,我们希望X’尽可能接近X。为了确保这一点,进一步的训练是必不可少的。
[4] 解码器 : 权重
作为一个中间步骤,我们计算每个权重的 L2 范数,并将它们保留下来以便稍后使用。

L2-范数
也称为欧几里得范数,L2 范数使用以下公式计算向量的大小:||x||₂ = √(Σᵢ xᵢ²)。
换句话说,它对每个分量的平方进行求和,然后对结果取平方根。这个范数提供了一种直接的方式来量化向量在欧几里得空间中的长度或距离。
训练
如前所述,稀疏自编码器通过广泛的训练使得重建的X’尽可能接近X。为此,我们接下来执行以下步骤:
[5] 稀疏性 : L1 损失
这里的目标是尽可能获得接近零/零的值。我们通过调用L1 稀疏性来惩罚权重的绝对值——核心思想是我们希望使得和尽可能小。

L1-损失
L1-损失是权重绝对值之和:L1 = λΣ|w|,其中λ是正则化参数。
这促使许多权重变为零,从而简化了模型,并增强了可解释性。
换句话说,L1 有助于将注意力集中在最相关的特征上,同时防止过拟合,改善模型的泛化能力,并减少计算复杂性。
[6] 稀疏性 : 梯度
下一步是计算L1的梯度,对于正值为-1。因此,对于所有值为f >0的情况,结果将被设定为-1。

L1 惩罚如何将权重推向零?
L1 惩罚的梯度通过一个过程将权重推向零,这个过程施加一个恒定的力,无论权重的当前值如何。下面是它的工作原理(本小节中的所有图片均为作者提供):
L1 惩罚表示为:

这个惩罚相对于权重w的梯度是:

其中sign(w)是:

在梯度下降过程中,权重的更新规则是:

其中𝞰是学习率。
常量减法(或加法)操作中的λ(根据其符号)会减小权重值的绝对值。如果权重足够小,这个过程可以将其完全驱动为零。
[7] 稀疏性:零
对于所有已经是零的其他值,我们保持它们不变,因为它们已经被归零。

[8] 稀疏性:权重
我们将第 6 步中获得的梯度矩阵的每一行与第 4 步中获得的相应解码器权重相乘。这个步骤至关重要,因为它防止模型学习到较大的权重,避免在重建结果时加入错误的信息。

[9] 重建:均方误差损失
我们使用均方误差或L2损失函数来计算X’和X之间的差异。如前所述,目标是将误差最小化到最低值。

[10] 重建:梯度
L2损失的梯度是2*(X’-X)。
因此,正如原始自编码器所示,我们运行反向传播来更新权重和偏差。这里的关键是找到稀疏性和重建之间的良好平衡。

这样,我们就结束了这一非常巧妙且直观的学习方法,帮助我们理解模型是如何理解一个概念,并在生成响应时采取的方向。
总结:
-
一个自编码器整体由两个部分组成:编码器和解码器。编码器利用权重和偏差以及 ReLU 激活函数将初始输入特征压缩到一个较低的维度,力图仅捕捉相关的部分。另一方面,解码器则接收编码器的输出,并努力将输入特征重建回其原始状态。由于自编码器的目标就是初始特征本身,因此才称之为“自”。目标和标准神经网络一样,是通过传播误差的梯度并更新权重和偏差来实现目标特征和输入特征之间的最小误差(差异)。
-
稀疏自编码器由与标准自编码器相同的所有组件以及一些额外的组成部分构成。这里的关键是训练步骤中的不同方法。由于目标是提取可解释的特征,我们希望将那些相对意义较小的值置为零。一旦编码器使用 ReLU 抑制负值,我们进一步使用 L1 损失对结果进行处理,通过惩罚权重的绝对值来促进稀疏性。这是通过向损失函数中添加一个惩罚项来实现的,该惩罚项是权重绝对值之和:λΣ|w|。那些保持非零的权重是对模型性能至关重要的。
利用稀疏性提取可解释特征
作为人类,我们的大脑仅对特定刺激激活一小部分神经元。同样,稀疏自编码器通过利用稀疏性约束,如L1正则化,学习输入的稀疏表示。通过这样做,稀疏自编码器能够从复杂数据中提取可解释的特征,从而增强所学习特征的简洁性和可解释性。这种类似生物神经过程的选择性激活有助于聚焦于输入数据中最相关的部分,使模型更加鲁棒和高效。
随着 Anthropic 致力于理解 AI 模型中的可解释性,其倡议强调了透明和易理解的 AI 系统的必要性,尤其是在 AI 系统越来越多地融入关键决策过程时。通过专注于创建既强大又可解释的模型,Anthropic 为开发可信赖且能有效应用于现实世界的 AI 做出了贡献。
总之,稀疏自编码器对于提取可解释的特征、增强模型的鲁棒性并确保效率至关重要。对这些强大模型的理解工作以及它们如何进行推理,突显了 AI 可解释性日益重要的趋势,为更加透明的 AI 系统铺平了道路。未来如何发展这些概念,并推动我们迈向一个将 AI 安全地融入我们生活的未来,值得期待!
附言:如果你想自己完成这个练习,下面有一个空白模板的链接供你使用。
现在去玩吧,帮助 Zephyr 保持《真理法典》的安全!

再次特别感谢 Tom Yeh 教授 对本项工作的支持!
参考文献:
[1] 朝着单义性:通过字典学习分解语言模型,Bricken 等人,2023 年 10 月 transformer-circuits.pub/2023/monosemantic-features/index.html
[2] 扩展单义性:从 Claude 3 Sonnet 中提取可解释特征,Templeton 等人,2024 年 5 月 transformer-circuits.pub/2024/scaling-monosemanticity/
手动深度探讨 LlaMA 3 ✍️
探索 Llama 3 背后的变压器架构及其在 GenAI 生态系统中的前景
·发表于 Towards Data Science ·11 分钟阅读·2024 年 5 月 3 日
--

图片由作者提供(这幅 LlaMA 3 的闪亮图像是我的 4 岁孩子创作的。)
在安第斯山脉的崎岖山巅,生活着三只非常美丽的生物——Rio、Rocky 和 Sierra。凭借着光亮的毛皮和闪烁的眼睛,他们成了力量与韧性的象征。
故事是这样的,从小他们对知识的渴望便是无尽的。他们会去寻找羊群中的智者,专心聆听他们的故事,像海绵一样吸收他们的智慧。正因为如此,他们的超能力也与日俱增,那就是与他人合作,学会了团队合作是克服安第斯山脉挑战的关键。
如果他们遇到迷路或需要帮助的旅行者,Rio 会从他们的视角出发,引导他们并给予安慰,Rocky 提供迅速的解决方案,而 Sierra 确保他们有足够的力量继续前行。凭借这一点,他们赢得了敬佩,并鼓励每个人效仿他们的榜样。
随着太阳在安第斯山脉落下,Rio、Rocky 和 Sierra 一起站立,他们的精神如同这些山脉般交织在一起。于是,他们的故事成为了知识、智慧、合作和改变世界的意志的象征。
他们是超级羊驼,三人组被亲切地称为 LlaMA3!
Meta 的 LlaMA 3
这个故事与 Meta 的开源大型语言模型(LLM)— LlaMA 3(Large Language Model Meta AI)的故事并不遥远。2024 年 4 月 18 日,Meta 发布了他们的 LlaMa 3 家族的大型语言模型,参数规模为 8B 和 70B,声称比 LlaMA 2 有了重大进展,并力争成为该规模下最先进的 LLM 模型。
根据 Meta,在构建 LlaMA 3 时有四个关键焦点 — 模型架构、预训练数据、扩大预训练规模和指导微调。这让我们思考如何在企业规模和基层水平上充分利用这个非常有竞争力的模型。
为了探索这些问题的答案,我与 Edurado Ordax(AWS 的生成 AI 负责人)和 Tom Yeh 教授(科罗拉多大学博尔德分校的计算机科学教授)合作。
那么,让我们开始这段旅程:
我们如何利用 LlaMA 3 的强大功能?
API 与 Fine-Tuning
根据最近的实践,目前主要有两种方式可以访问和使用这些 LLMs — API 和 Fine-Tuning。即使使用了这两种非常不同的方法,过程中还有其他因素,如下图所示,这些因素变得至关重要。
(本节中的所有图片均由 Eduardo Ordax* 提供。)*

用户与 LlaMA 3 互动的主要6 个阶段。
第 1 阶段:通过直接使用模型来满足广泛的用例。
第 2 阶段:根据用户定义的应用程序使用模型。
第 3 阶段:使用提示工程来训练模型以产生期望的输出。
第 4 阶段:在用户端使用提示工程,同时深入研究数据检索和微调,这仍然主要由 LLM 提供商管理。
第 5 阶段:将大部分事务交给用户自己,从提示工程到数据检索和微调(RAG 模型,PEFT 模型等)。
第 6 阶段:从头开始创建整个基础模型 — 从预训练到后训练。
要充分利用这些模型,建议最佳方法是进入第 5 阶段,因为这样用户的灵活性会更大。根据领域需求定制模型非常关键,以最大化收益。而为此,不参与系统将无法获得最佳回报。
为了做到这一点,以下是可能会有用的工具的高层视图:

图片表明,为了从模型中获得最大利益,关键是需要一个结构和路线图。它包括三个组成部分:
-
人员:不仅仅是最终用户,还包括数据工程师、数据科学家、MLOps 工程师、ML 工程师以及提示工程师等全体人员。
-
过程:不仅仅是将 LLM 插入 API,而是聚焦于模型评估、模型部署和微调的整个生命周期,以满足特定需求。
-
工具:不仅仅是 API 访问和 API 工具,还包括整个环境范围、不同的 ML 流水线、用于访问和运行检查的独立账户。
当然,这对于企业级部署来说至关重要,只有在基础元素得到严格验证并设置好后,模型的实际效益才能得以实现。为了做到这一点,MLOps 下的工具和实践变得非常重要。结合 FMOps,这些模型可以证明非常有价值,并且丰富 生成式人工智能生态系统。
FMOps ⊆ MLOps ⊆ DevOps
MLOps,也称为 机器学习运维,是机器学习工程的一部分,专注于机器学习模型的开发、部署和维护,确保它们可靠高效地运行。
MLOps 属于 DevOps(开发与运维),但专门针对 ML 模型。
FMOps(基础模型运维)则专注于生成式 AI 场景,通过选择、评估和微调 LLMs 来工作。

话虽如此,然而有一件事是恒定不变的,那就是 LlaMA 3 毕竟是一个 LLM,只有在基础要素得到充分设置并经过严格验证后,其企业级的实现才有可能并且具有实际的好处。为了做到这一点,我们来探索一下 LlaMA 3 背后的技术细节。
LlaMa 3 成名的秘密武器是什么?
从基础层面来说,的确是 transformer。如果我们在过程中的层次稍微往上看,答案将是 transformer 架构,但高度优化,以便在常见的行业基准测试中实现更优的性能,同时也能支持新能力的实现。
好消息是,由于 LlaMa 3 是开放的(由 Meta 决定是否开源),我们可以访问模型卡,了解这个强大架构是如何配置的。
所以,让我们深入探讨并解开其中的奥秘:
Transformer 架构如何与自注意力机制结合,在 LlaMA 3 中发挥作用?
首先,快速回顾一下 transformer 是如何工作的:
-
Transformer 架构可以被看作是注意力层和前馈层的结合。
-
注意力层在特征之间横向结合,生成新的特征。
-
前馈层(FFN)结合了特征的各个部分或特征的特性,生成新的部分/特性。它是跨维度垂直执行的。
(本节中的所有图片,除非另有说明,均由 Tom Yeh 教授* 提供,且已获得他的许可进行编辑。)*
下面是架构的基本形式以及其功能方式。

包含注意力和前馈块的变压器架构。
这里是深入研究变压器和自注意力的文章链接,其中详细讨论了整个过程。
LlaMA 3 的要点
是时候深入了解变压器数字在现实生活中的应用,发现它们在 LlaMa 3 模型中的作用。在我们的讨论中,我们只考虑 8B 变体。我们开始吧:
- LlaMA 3 — 8B 模型参数是什么?
我们需要探讨的主要数字/值是在变压器架构中起关键作用的参数,它们如下:
-
层:这里的层指的是变压器的基本块 —— 注意力层和前馈网络,如上图所示。这些层被堆叠在一起,其中输入流入一个层,其输出传递到下一个层,逐渐转换输入数据。
-
注意力头:注意力头是自注意机制的一部分。每个头独立扫描输入序列并执行注意力步骤(记住:QK 模块,SoftMax 函数。)
-
词汇量:词汇量指的是模型识别或了解的单词数量。本质上,可以将其视为人类建立词汇库的方式,以便我们在语言中发展知识和多样性。大多数情况下,词汇量越大,模型性能越好。
-
特征维度:这些维度指定了表示输入数据中每个标记的向量大小。这个数字在整个模型中保持一致,从输入嵌入到每个层的输出。
-
隐藏维度:这些维度是模型内部层的内部大小,更常见的是前馈层的隐藏层的大小。通常情况下,这些层的大小可以大于特征维度,帮助模型从数据中提取和处理更复杂的表示。
-
上下文窗口大小:这里的“窗口大小”指的是模型在计算注意力时一次考虑的输入序列中的标记数量。
定义了这些术语后,让我们参考 LlaMA 3 模型中这些参数的实际数字。(这些数字所在的原始源代码可以在这里找到。)

这些数字所在的原始源代码可以在这里找到。
牢记这些值,接下来的步骤说明了它们如何在模型中发挥作用。它们按照在源代码中出现的顺序列出。
[1] 上下文窗口
在实例化LlaMa 类时,变量max_seq_len定义了上下文窗口。类中还有其他参数,但这个参数与变压器模型相关。这里的max_seq_len是 8K,这意味着注意力头能够一次扫描 8K 个标记。

[2] 词汇量和注意力层
接下来是Transformer 类,它定义了词汇量和层数。再次强调,这里的词汇量指的是模型可以识别和处理的单词(和标记)集合。这里的注意力层指的是模型中使用的变压器块(注意力和前馈层的组合)。

根据这些数字,LlaMA 3 的词汇量为 128K,相当大。此外,它有 32 个变压器块的副本。
[3] 特征维度和注意力头
特征维度和注意力头进入自注意力模块。特征维度指的是嵌入空间中标记的向量大小,注意力头包括 QK 模块,它为变压器中的自注意力机制提供动力。

[4] 隐藏维度
隐藏维度出现在前馈类中,指定模型中隐藏层的数量。对于 LlaMa 3,隐藏层是特征维度的 1.3 倍。更多的隐藏层允许网络在将它们投影回较小的输出维度之前在内部创建和操作更丰富的表示。

[5] 将上述参数组合成变压器
-
第一个矩阵是输入特征矩阵,通过注意力层创建注意力加权特征。在这个图像中,输入特征矩阵只有一个 5 x 3 的矩阵,但在现实世界的 Llama 3 模型中,它增长到 8K x 4096,这是巨大的。
-
接下来是前馈网络中的隐藏层,它从 5325 增长到最终层的 4096 再回落。

[6] 变压器块的多层
LlaMA 3 将这 32 个以上的变压器块组合在一起,其中一个的输出传递到下一个块,直到达到最后一个块。

[7] 让我们把所有东西放在一起
一旦我们启动了上述所有部分,就是时候把它们放在一起,看看它们如何产生 LlaMA 效果。

那么,这里发生了什么?
第 1 步:首先,我们有输入矩阵,其大小为 8K(上下文窗口)x 128K(词汇大小)。这个矩阵经过嵌入过程,将这个高维矩阵转换为低维。
第 2 步:在这种情况下,这个低维度为 4096,这是 LlaMA 模型中特征维度的指定维度,如我们之前所见。(从 128K 降到 4096 是巨大的,值得注意。)
第 3 步:此特征经过变压器块处理,首先由注意力层处理,然后是 FFN 层。注意力层在特征上进行横向处理,而 FFN 层则在维度上进行纵向处理。
第 4 步:第 3 步在 32 层变压器块中重复进行。最终,得到的矩阵具有与特征维度相同的尺寸。
第 5 步:最后,这个矩阵被转换回原始的词汇矩阵大小,即 128K,以便模型可以选择并映射词汇中可用的单词。
这就是 LlaMA 3 如何在基准测试中取得高分并创造 LlaMA 3 效应的原因。
LlaMA 3 效应
LlaMA 3 发布了两个版本——8B 和 70B 参数,以服务于广泛的应用场景。除了在标准基准测试上实现了最先进的性能外,还开发了一套新的严格的人类评估集。同时,Meta 承诺会发布更好、更强的版本,使其变得多语言且多模态。最新的消息是更大且更强的模型即将发布,参数超过 400B(早期的报告 在此 显示,它已经通过比 LlaMA 3 高出近 20% 的得分,彻底压制了基准测试)。
然而,必须指出的是,尽管所有即将到来的变化和更新,始终有一点是不会改变的——那就是一切的基础——变压器架构和使这一技术进步成为可能的变压器块。
LlaMA 模型的命名可能只是巧合,但根据安第斯山脉的传说,真正的美洲驼一直以其力量和智慧受到敬仰。这与生成式 AI —— ‘LlaMA’ 模型并没有太大区别。
所以,让我们在这段激动人心的生成式 AI 安第斯之旅中,牢记支撑这些大型语言模型的基础!
附言:如果你想自己完成这个练习,这里有一个空白模板链接,供你使用。
现在,去享受乐趣吧,创造一些 LlaMA 3 效应!

图片由作者提供
深入探索 LlamaIndex 工作流:基于事件驱动的 LLM 架构
我对实践后进展和不足之处的看法
·发布于Towards Data Science ·14 分钟阅读·2024 年 12 月 17 日
--

深入探索 LlamaIndex 工作流:基于事件驱动的 LLM 架构。图片来源:DALL-E-3
最近,LlamaIndex 在其版本之一中引入了一个新功能工作流,为 LLM 应用程序提供了基于事件驱动和逻辑解耦的能力。
在今天的文章中,我们将通过一个实用的小项目深入探讨这一功能,探索其中的新特性和仍然存在的不足之处。让我们开始吧。
引言
为什么是事件驱动的?
越来越多的 LLM 应用程序正在转向智能代理架构,期望 LLM 能够通过调用不同的 API 或多次迭代调用来满足用户需求。
然而,这一转变带来了一个问题:随着代理应用程序发起更多的 API 调用,程序响应变得缓慢,代码逻辑也变得更加复杂。
一个典型的例子是ReActAgent,其中包括思考、行动、观察和最终答案等步骤,至少需要进行三次 LLM 调用和一次工具调用。如果需要循环,I/O 调用的次数会更多。
手动深入探讨 LSTMs 和 xLSTMs ✍️
探索 LSTM 的智慧,通向 xLSTMs——一个可能成为当今 LLMs 竞争对手的技术
·发表于 Towards Data Science ·阅读时间:12 分钟·2024 年 7 月 9 日
--

作者提供的图片(古老的巫师,由我的四岁孩子创作)
“在塞伦蒂亚的神秘领域中,那里古老的森林低语着久已遗忘的咒语,居住着谜行者——一位尊敬的巫师,永恒智慧的守护者。”
“有一天,当塞伦蒂亚面临巨大危险时,谜行者使用赋予过去、现在与未来精华的本质之石,编织了一场神秘的仪式。借助古老的魔法,他召唤出了 LSTM,一个知识的传导体,能够保存塞伦蒂亚的历史并预见其命运。犹如一条无尽的智慧之河,LSTM 流动着,超越了当下,揭示了地平线之外的未来。”
“从他隐居的住所,谜行者观察着塞伦蒂亚的重生,向新的高峰攀升。他知道,正是他那深奥的智慧和不懈的努力,再一次在这个神奇的世界中守护了一个遗产。”
随着这个故事,我们开始了对最吸引人的递归神经网络之一——长短期记忆网络(LSTMs)的深入探讨。为什么我们要再次回顾这个经典?因为随着语言建模中长上下文长度的重要性日益增长,它们可能再次变得非常有用。
LSTMs 能否再次超越 LLMs?
最近,奥地利的研究人员提出了一个有前景的举措,旨在复兴 LSTM 的失落辉煌——通过发展更为进化的扩展长短期记忆网络(Extended Long-short Term Memory,简称 xLSTM)。可以说,在变压器(Transformers)出现之前,LSTM 曾经在深度学习的成功中独占鳌头。现在的问题是,随着其能力的最大化和缺点的最小化,LSTM 能否与当今的大型语言模型(LLM)竞争?
为了得到答案,我们不妨回顾一下 LSTM 的历史,并回顾一下它们的独特之处:
长短期记忆网络(LSTM)最早由Hochreiter 和 Schmidhuber于 1997 年提出——旨在解决循环神经网络(RNN)面临的长期依赖问题。该论文已被引用约 106518 次,LSTM 成为经典也就不足为奇了。
LSTM 的核心思想是能够学习在任意时间间隔内,何时记住和何时忘记相关信息。这就像我们人类一样。我们不会从零开始每次想法——我们依赖于更久远的信息,并能非常巧妙地将它们联系起来。当然,说到 LSTM 时,大家会问——RNN 不也做一样的事情吗?
简短的答案是:可以。然而,存在一个很大的区别。RNN 架构并不支持过多关注过去的内容——只能关注即时的过去。而这并不太有帮助。
举个例子,我们来看约翰·济慈在《秋天》中的这些诗句:
“Season of mists and mellow fruitfulness,
Close bosom-friend of the maturing sun;”
作为人类,我们理解“mists”(雾气)和“mellow fruitfulness”(丰盈的果实)这两个词与秋季的季节是概念上相关的,唤起了特定时节的想法。同样,LSTM 可以捕捉到这种概念,并利用它进一步理解“maturing sun”(成熟的太阳)一词的上下文。尽管这些词在序列中分隔开,LSTM 网络依然能够学习到它们之间的关联,并保持前面的联系不变。这与原始的循环神经网络(RNN)框架有着明显的区别。
LSTM 之所以能做到这一点,是通过一个门控机制。如果我们对比 RNN 和 LSTM 的架构,差异非常明显。RNN 的架构非常简单——过去的状态和当前的输入通过一个激活函数传递,输出下一个状态。另一方面,LSTM 块在 RNN 块的基础上添加了三个门控:输入门、遗忘门和输出门,它们共同处理过去的状态和当前的输入。这种门控机制正是所有区别的所在。
为了进一步理解,让我们深入探讨这些令人惊叹的关于LSTM和xLSTM的杰出作品,作者是Tom Yeh 教授。
首先,让我们了解 LSTM 背后的数学机制,然后再探讨其更新版本。
(下方的所有图片,除非另有说明,均来自 Tom Yeh 教授的 LinkedIn 帖子,我已获得他的许可进行编辑。)
所以,接下来我们开始:
LSTM 是如何工作的?
[1] 初始化
第一步从随机分配先前的隐藏状态 h0 和记忆单元 C0 的值开始。为了与图示同步,我们设置:
h0 → [1,1]
C0 → [0.3, -0.5]

[2] 线性变换
在下一步中,我们通过将四个权重矩阵(Wf,Wc,Wi和Wo)与当前输入 X1 和先前隐藏状态进行拼接后相乘,来执行线性变换。
结果值称为特征值,它们是当前输入和隐藏状态的组合。

[3] 非线性变换
这一步在 LSTM 过程中至关重要。它是一个非线性变换,包含两个部分——sigmoid σ和tanh。
sigmoid 用于获取介于 0 和 1 之间的门值。这个层实际上决定了保留哪些信息,遗忘哪些信息。值始终在 0 和 1 之间——‘0’表示完全删除信息,而‘1’表示保留信息。
-
忘记门 (f1):[-4, -6] → [0, 0]
-
输入门 (i1):[6, 4] → [1, 1]
-
输出门 (o1):[4, -5] → [1, 0]
在下一部分中,应用 tanh 来获得新的候选记忆值,这些值可以添加到先前的信息上。
- 候选记忆 (C’1):[1, -6] → [0.8, -1]

[4] 更新记忆
一旦获得上述值,就可以使用这些值来更新当前状态。
前一步已经决定了需要做什么,在这一步我们执行那个决定。
我们将其分为两部分:
-
忘记:将当前记忆值 (C0) 与获得的忘记门值按元素相乘。其作用是更新当前状态中被决定遗忘的值。→ C0 .* f1
-
输入:将更新后的记忆值 (C’1) 与输入门值按元素相乘,得到‘输入缩放’后的记忆值。→ C’1 .* i1
最后,我们将上述两项相加,得到更新后的记忆 C1,即C0 .* f1 + C’1 .* i1 = C1

[5] 候选输出
最后,我们决定输出结果的形式:
首先,我们像之前一样对新的记忆 C1 应用 tanh,得到候选输出 o’1。这样会将值压缩到 -1 到 1 之间。

[6] 更新隐藏状态
为了得到最终输出,我们将之前步骤中获得的候选输出 o’1 与步骤 3 中输出门 o1 的 sigmoid 结果相乘,得到的结果就是网络的第一个输出,也是更新后的隐藏状态 h1,即 o’1 * o1 = h1。

— — 过程 t = 2 — -
我们继续进行以下的后续迭代:
[7] 初始化
首先,我们复制来自前面步骤的更新结果,即更新后的隐藏状态 h1 和记忆 C1。

[8] 线性变换
重复步骤 [2],即逐元素的权重和偏置矩阵乘法。

[9] 更新记忆 (C2)
重复步骤 [3] 和 [4],即使用 sigmoid 和 tanh 层进行非线性变换,随后决定遗忘相关部分并引入新信息——这将给我们更新后的记忆 C2。

[10] 更新隐藏状态 (h2)
最后,我们重复步骤 [5] 和 [6],这会将结果加起来得到第二个隐藏状态 h2。

接下来,我们进入最后的迭代。
— — 过程 t = 3 — -
[11] 初始化
再次,我们复制来自上一迭代的隐藏状态和记忆值,即 h2 和 C2。

[12] 线性变换
我们执行与步骤 2 相同的线性变换。

[13] 更新记忆 (C3)
接下来,我们执行非线性变换,并根据变换中获得的值执行记忆更新。

[14] 更新隐藏状态 (h3)
完成后,我们使用这些值来获得最终的隐藏状态 h3。

总结:
总结上面的工作,关键点是记住 LSTM 依赖于三个主要的门控:输入门、遗忘门和输出门。正如这些名称所示,这些门控决定了哪些部分的信息是相关的,以及要保留多少信息,哪些部分可以被丢弃。
简要来说,执行的步骤如下:
-
从前一状态初始化隐藏状态和记忆值。
-
执行线性变换,帮助网络开始关注隐藏状态和记忆值。
-
对数据应用非线性变换(sigmoid 和 tanh),以确定保留/丢弃哪些值,并获得新的候选记忆值。
-
基于步骤 3 中获得的决策(值),我们进行记忆更新。
-
接下来,我们根据前一步骤中获得的记忆更新来确定输出的样子。我们在这里获得一个候选输出。
-
我们将候选输出与步骤 3 中获得的门控输出值结合,最终得出中间隐藏状态。
这个循环将根据需要继续进行多次迭代。
扩展长短期记忆(xLSTM)
xLSTM 的需求
当 LSTM 出现时,它们无疑为做一些之前无法做到的事情奠定了基础。循环神经网络可以具有记忆,但它的记忆非常有限,因此诞生了 LSTM——为了支持长期依赖。然而,这还不够。因为将输入分析为序列阻碍了并行计算的使用,而且由于长期依赖,还导致了性能下降。
因此,作为所有问题的解决方案,transformers 诞生了。但问题仍然存在——我们是否可以通过解决 LSTM 的局限性,再次利用 LSTM 实现 transformers 的功能?为了解答这个问题,xLSTM 架构应运而生。
xLSTM 与 LSTM 有何不同?
xLSTM 可以看作是 LSTM 的一个非常进化的版本。xLSTM 保留了 LSTM 的基础结构,但引入了新的元素,这些元素有助于处理原始形式的缺点。
指数门控与标量记忆混合——sLSTM
最关键的区别是引入了指数门控。在 LSTM 中,当我们执行步骤[3]时,会对所有门进行 sigmoid 门控,而对于 xLSTM,它已经被指数门控所替代。
例如:对于输入门 i1-

现在,

图片由作者提供
由于指数门控提供了更大的范围,xLSTM 能够比 sigmoid 函数更好地处理更新,因为 sigmoid 函数将输入压缩到(0, 1)的范围内。然而,有一个问题——指数值可能会变得非常大。为了缓解这个问题,xLSTM 引入了归一化,并且下面方程中出现的对数函数在这里发挥了重要作用。

图像来源于参考文献[1]
现在,对数确实逆转了指数的效果,但正如xLSTM 论文所声称的,它们的结合应用为平衡状态指明了方向。
这种指数门控以及不同门之间的记忆混合(如原始 LSTM 中所示)形成了sLSTM块。
矩阵记忆单元——mLSTM
xLSTM 架构的另一个新特点是将标量记忆扩展为矩阵记忆,这使得它能够并行处理更多信息。它还通过引入键、查询和值向量,并将它们作为加权的键向量之和使用在归一化器状态中,从而与 transformer 架构相似,其中每个键向量的权重由输入门和遗忘门确定。
一旦 sLSTM 和 mLSTM 模块准备好后,它们会通过残差连接一个个堆叠在一起,形成 xLSTM 模块,最终构成 xLSTM 架构。
因此,指数门控(结合适当的归一化)和更新的记忆结构的引入为 xLSTM 提供了一个强有力的基础,使其能够实现类似于变换器的结果。
总结:
- LSTM 是一种特殊的循环神经网络(RNN),它允许像人类一样将先前的信息与当前状态连接,保持思维的持续性。LSTM 因其能够回顾较远的过去,而不仅仅依赖于眼前的即时过去,变得非常流行。使这一切成为可能的是特殊门控元素的引入到 RNN 架构中——
-
遗忘门:决定应当保留或忘记前一个单元状态中的哪些信息。通过有选择地遗忘不相关的过去信息,LSTM 保持了长期依赖关系。
-
输入门:决定应当存储在单元状态中的新信息。通过控制单元状态的更新方式,它融合了预测当前输出所需的新信息。
-
输出门:决定应当作为隐藏状态输出的信息。通过选择性地将单元状态的部分内容作为输出,LSTM 可以向后续层提供相关信息,同时抑制不相关的细节,从而只传递重要信息到更长的序列中。
2. xLSTM 是 LSTM 的一种进化版本,旨在解决 LSTM 面临的缺陷。虽然 LSTM 能够处理长期依赖问题,但信息是顺序处理的,因此没有利用当前变换器所强调的并行性。为了解决这个问题,xLSTM 引入了:
-
sLSTM:指数门控,帮助包括比 sigmoid 激活函数更广的范围。
-
mLSTM:新型的记忆结构,采用矩阵记忆以增强记忆容量并提高信息检索效率。
LSTM 会重返舞台吗?
LSTM 总体上属于循环神经网络家族,它们以递归的方式按顺序处理信息。变换器的出现彻底消除了递归应用,但它们在处理极长序列时依然面临严峻问题。研究表明,对于长范围或长上下文,二次时间复杂度是相关的。
因此,探索能够至少启发解决方案路径的选项似乎是值得的,一个好的起点可能是回到 LSTM——简而言之,LSTM 很有可能卷土重来。当前的 xLSTM 结果无疑看起来很有前景。最后,作为总结——Mamba 对递归的使用作为一个良好的例证,表明这可能是一个值得探索的有利路径。
所以,让我们一起跟随这个旅程,看到它的展开,同时牢记递归的强大力量!
附言:如果你想自己完成这个练习,这里有一个空白模板的链接供你使用。
现在去玩得开心,创造一些长短期效应吧!

图片来源:作者
参考文献:
-
xLSTM:扩展的长短期记忆,Maximilian 等,2024 年 5 月
arxiv.org/abs/2405.04517 -
长短期记忆,Sepp Hochreiter 和 Jürgen Schmidhuber,1997 年,Neural Comput. 9, 8(1997 年 11 月 15 日),1735–1780。
doi.org/10.1162/neco.1997.9.8.1735
深入探讨多线程、多进程和 Asyncio
如何选择正确的并发模型
·发表于 Towards Data Science ·8 分钟阅读·2024 年 12 月 28 日
--

图片由 Paul Esch-Laurent 提供,来源于 Unsplash
Python 提供了三种主要的方式来同时处理多个任务:多线程、多进程和 asyncio。
选择正确的模型对于最大化程序性能和高效使用系统资源至关重要。(顺便说一句,这也是一个常见的面试问题!)
如果没有并发,一个程序每次只能处理一个任务。在诸如文件加载、网络请求或用户输入等操作过程中,它会处于空闲状态,浪费宝贵的 CPU 周期。并发通过允许多个任务高效运行来解决这个问题。
但是,我应该使用哪种模型呢?让我们深入了解!
目录
-
并发的基础
-
并发与并行
-
程序
-
进程
-
线程
-
操作系统如何管理线程和进程?
-
-
Python 的并发模型
-
多线程
-
Python 的全局解释器锁(GIL)
-
多进程
-
Asyncio
-
-
我应该在什么时候使用哪种并发模型?
并发的基础
在深入 Python 的并发模型之前,让我们回顾一下几个基础概念。
1. 并发与并行

并发与并行的视觉表现(由我绘制)
并发就是同时管理多个任务,而不一定是同时进行。任务可能会轮流进行,从而产生多任务的假象。
并行是指通过利用多个 CPU 核心同时运行多个任务。
2. 程序
现在让我们继续探讨一些基础的操作系统概念——程序、进程和线程。

在单一进程内可以同时存在多个线程——这就是多线程(由我绘制)。
程序只是一个静态文件,例如 Python 脚本或可执行文件。
程序存储在磁盘上,在操作系统(OS)将其加载到内存中运行之前是被动的。一旦发生这种情况,程序就变成了进程。
3. 进程
进程是运行程序的独立实例。
进程有自己的内存空间、资源和执行状态。进程彼此隔离,这意味着一个进程不能干扰另一个进程,除非通过如进程间通信(IPC)等机制显式设计来实现。
进程通常可以分为两种类型:
-
I/O 密集型进程:
大部分时间等待输入/输出操作完成,如文件访问、网络通信或用户输入。在等待时,CPU 处于空闲状态。
-
CPU 密集型进程:
大部分时间进行计算(例如视频编码、数值分析)。这些任务需要大量的 CPU 时间。
进程的生命周期:
-
进程在创建时处于新建状态。
-
它进入就绪状态,等待 CPU 时间。
-
如果进程等待某个事件(如 I/O),它会进入等待状态。
-
最后,进程在完成任务后终止。
4. 线程
线程是进程中最小的执行单元。
进程充当线程的“容器”,在进程的生命周期内,可以创建和销毁多个线程。
每个进程至少有一个线程——主线程,但它也可以创建额外的线程。
线程在同一进程内共享内存和资源,从而实现高效的通信。然而,这种共享可能导致同步问题,如竞争条件或死锁,如果管理不当。与进程不同,单一进程中的多个线程并不是隔离的——一个不正常的线程可能会导致整个进程崩溃。
5. 操作系统如何管理线程和进程?
CPU 每次只能执行一个核心上的任务。为了处理多个任务,操作系统使用抢占式上下文切换。
在上下文切换过程中,操作系统暂停当前任务,保存其状态并加载下一个任务的状态以执行。
这种快速的切换创造了在单一 CPU 核心上同时执行的错觉。
对于进程,上下文切换更为资源密集,因为操作系统必须保存并加载独立的内存空间。对于线程,切换较快,因为线程在同一进程内共享相同的内存。然而,频繁的切换会带来开销,从而可能降低性能。
进程的真正并行执行只有在有多个 CPU 核心可用时才能发生。每个核心同时处理一个独立的进程。
Python 的并发模型
现在让我们探讨 Python 特定的并发模型。

不同并发模型的总结(由我绘制)
1. 多线程
多线程允许一个进程同时执行多个线程,这些线程共享相同的内存和资源(见图示 2 和 4)。
然而,Python 的全局解释器锁(GIL)限制了多线程在 CPU 密集型任务中的效果。
Python 的全局解释器锁(GIL)
GIL 是一个锁,允许在任何时候只有一个线程控制 Python 解释器,这意味着一次只能有一个线程执行 Python 字节码。
GIL 的引入是为了简化 Python 中的内存管理,因为许多内部操作(如对象创建)默认情况下不是线程安全的。如果没有 GIL,多个线程试图访问共享资源时将需要复杂的锁或同步机制来防止竞争条件和数据损坏。
何时 GIL 成为瓶颈?
-
对于单线程程序,GIL 无关紧要,因为该线程对 Python 解释器拥有独占访问权。
-
对于多线程 I/O 密集型程序,GIL 的问题较小,因为线程在等待 I/O 操作时会释放 GIL。
-
对于多线程的 CPU 密集型操作,GIL 成为一个显著的瓶颈。多个线程争夺 GIL,必须轮流执行 Python 字节码。
一个值得注意的有趣情况是 time.sleep 的使用,Python 实际上将其视为一个 I/O 操作。time.sleep 函数不是 CPU 密集型的,因为它在休眠期间不涉及主动计算或 Python 字节码的执行。相反,跟踪经过的时间的责任委托给了操作系统。在此期间,线程释放 GIL,允许其他线程运行并利用解释器。
2. 多进程
多进程使得系统能够并行运行多个进程,每个进程拥有独立的内存、全局解释器锁(GIL)和资源。在每个进程中,可能有一个或多个线程(见图示 3 和 4)。
多进程绕过了 GIL 的限制,这使其适用于需要大量计算的 CPU 密集型任务。
然而,由于独立的内存和进程开销,多进程比单进程更加消耗资源。
3. Asyncio
与线程或进程不同,asyncio 使用单个线程处理多个任务。
当使用 asyncio 库编写异步代码时,你将使用 async/await 关键字来管理任务。
关键概念
-
协程: 这些是使用
async def定义的函数。它们是 asyncio 的核心,表示可以暂停并稍后恢复的任务。 -
事件循环: 它管理任务的执行。
-
任务: 协程的封装。当你希望一个协程真正开始执行时,你将其转换为任务——例如,使用
asyncio.create_task()。 -
**await**:暂停协程的执行,将控制权交还给事件循环。
它是如何工作的
Asyncio 运行一个事件循环,调度任务。当任务在等待某些东西时,如网络响应或文件读取,它会自愿“暂停”。当任务暂停时,事件循环会切换到另一个任务,确保不会浪费等待的时间。
这使得 asyncio 非常适用于需要等待大量时间的小任务的场景,如处理成千上万的 Web 请求或管理数据库查询。由于所有操作都在一个线程上运行,asyncio 避免了线程切换的开销和复杂性。
asyncio 和多线程的关键区别在于它们如何处理等待任务。
-
多线程依赖操作系统在一个线程等待时切换线程(抢占式上下文切换)。
当一个线程在等待时,操作系统会自动切换到另一个线程。
-
Asyncio 使用单个线程,并依赖任务通过在需要等待时“合作”来暂停(协作式多任务处理)。
编写 async 代码的两种方式:
**方法 1:await 协程**
当你直接await一个协程时,当前协程会在await语句处暂停,直到被await的协程执行完毕。任务在当前协程内是按顺序执行的。
当你需要协程的结果立即以继续下一步时,使用这种方法。
虽然这听起来像是同步代码,但其实不是。在同步代码中,整个程序会在暂停期间被阻塞。
使用 asyncio 时,只有当前协程会暂停,而程序的其余部分可以继续运行。这使得 asyncio 在程序级别上是非阻塞的。
示例:
事件循环会暂停当前协程,直到fetch_data完成。
async def fetch_data():
print("Fetching data...")
await asyncio.sleep(1) # Simulate a network call
print("Data fetched")
return "data"
async def main():
result = await fetch_data() # Current coroutine pauses here
print(f"Result: {result}")
asyncio.run(main())
**方法 2:asyncio.create_task(协程)**
协程被安排在后台并发运行。与await不同,当前协程会立即继续执行,而无需等待已调度的任务完成。
已调度的协程一旦事件循环找到机会就开始运行,无需等待显式的await。
不会创建新的线程;相反,协程会在与事件循环相同的线程内运行,事件循环管理每个任务的执行时间。
这种方法实现了程序内的并发,使多个任务能够高效地重叠执行。稍后你需要await任务来获取其结果并确保任务完成。
当你希望并发地运行任务而不需要立即获取结果时,使用这种方法。
示例:
当执行到asyncio.create_task()这一行时,协程fetch_data()会被安排在事件循环可用时立即开始运行。这可以发生在你显式await任务之前,甚至在此之前。相比之下,在第一个await方法中,协程只有在到达await语句时才会开始执行。
总的来说,这通过重叠执行多个任务使得程序更高效。
async def fetch_data():
# Simulate a network call
await asyncio.sleep(1)
return "data"
async def main():
# Schedule fetch_data
task = asyncio.create_task(fetch_data())
# Simulate doing other work
await asyncio.sleep(5)
# Now, await task to get the result
result = await task
print(result)
asyncio.run(main())
其他重要的点
-
你可以混合同步和异步代码。
由于同步代码是阻塞的,它可以通过
asyncio.to_thread()分配到一个单独的线程中。这样可以使你的程序实现多线程。在下面的示例中,
asyncio事件循环运行在主线程中,而一个单独的后台线程用于执行sync_task。
import asyncio
import time
def sync_task():
time.sleep(2)
return "Completed"
async def main():
result = await asyncio.to_thread(sync_task)
print(result)
asyncio.run(main())
- 你应该将计算密集型的 CPU 绑定任务分配到一个单独的进程中。
我应该在什么时候使用哪种并发模型?
这个流程是一个很好的决策方式,帮助你决定何时使用哪种方法。

流程图(由我绘制),参考了这个stackoverflow讨论
-
多进程
-
最适合计算密集型的 CPU 绑定任务。
-
当你需要绕过 GIL 时——每个进程都有自己的 Python 解释器,允许真正的并行处理。
-
-
多线程
-
最适合快速 I/O 绑定任务,因为减少了上下文切换的频率,Python 解释器会长时间保持在一个线程中。
-
由于全局解释器锁(GIL),不适合 CPU 绑定任务。
-
-
异步编程(Asyncio)
-
适用于慢速 I/O 绑定任务,例如长时间的网络请求或数据库查询,因为它高效地处理等待,使得程序具有可扩展性。
-
如果不将工作分配到其他进程,不适合 CPU 绑定任务。
-
总结
就这些了,大家。这一话题还有很多内容要讨论,但我希望我已经向你介绍了各种概念,以及何时使用每种方法。
感谢阅读!我定期写关于 Python、软件开发和我构建的项目的文章,所以关注我,不要错过。下篇文章见 😃
手动深入了解自注意力机制✍︎
探索驱动 Transformer 的注意力机制的复杂性
·发表于 Towards Data Science ·阅读时间:12 分钟·2024 年 4 月 22 日
--
注意!注意!
因为‘注意力即你所需’。
不,我并不是这么说,Transformer 是。

图片来自作者(Robtimus Prime 寻求关注。根据我儿子的话,鲜艳的彩虹色更能吸引注意,因此采用了这种配色方案。)
到今天为止,Transformer 的强大力量已经席卷了整个世界。不是像‘Robtimus Prime’那样的变形金刚,而是构成神经网络的那些。而这种力量正是源于‘注意力’的概念。那么,Transformer 上下文中的注意力究竟意味着什么呢?让我们试着在这里找到一些答案:
首先:
什么是 Transformer?
Transformer 是一种专注于从数据中学习上下文的神经网络。它们与我们尝试从 Transformer 的角度理解 ‘注意力与上下文’ 的含义非常相似。
Transformer 如何从数据中学习上下文?
通过使用注意力机制。
什么是注意力机制?
注意力机制帮助模型在每一步扫描序列的所有部分,并确定需要关注哪些元素。注意力机制被提出作为对编码器-解码器架构中固定长度向量的‘严格/硬性’解决方案的替代方案,提供了一种‘软性’解决方案,只关注相关部分。
什么是自注意力机制?
注意力机制最初是为了改善递归神经网络(RNN)的性能,效果随后也渗透到了卷积神经网络(CNN)。然而,随着 2017 年变压器架构的引入,RNN 和 CNN 的需求悄然消失。而这一切的核心原因正是自注意力机制。
自注意力机制的特殊之处在于,它旨在融入输入序列的上下文,以增强注意力机制。这个思想变得具有变革性,因为它能够捕捉语言中的复杂细微差别。
举个例子:
当我问我 4 岁的孩子“变压器是什么”时,他的回答仅包含机器人和汽车这两个词。因为那是他所理解的唯一上下文。但对我来说,变压器也意味着神经网络,因为对于我这个稍微有些经验的人来说,第二种上下文也是存在的。正是这种不同的上下文提供了不同的解决方案,因此它们通常非常重要。
“自”一词指的是注意力机制检查它正在处理的相同输入序列。
自注意力的执行方式有很多种变体。但缩放点积机制是最流行的方式之一。这也是在 2017 年原始变压器架构论文“Attention is All You Need”中介绍的方式。
自注意力在变压器中扮演什么角色,如何发挥作用?
我喜欢把变压器架构看作是由两层结构组成——外部结构和内部结构。
-
外部结构是注意力加权机制和前馈层的结合,关于这一点,我在这篇文章中有详细介绍。
-
内部结构由自注意力机制组成,是注意力加权功能的一部分。
所以,事不宜迟,让我们深入探讨自注意力机制背后的细节,并揭开其工作原理。查询-键模块和SoftMax函数在这一技术中起着至关重要的作用。
这段讨论基于 Tom Yeh 教授的精彩《人工智能手工系列》中的自注意力内容。(以下所有图片,除非另有说明,均来自 Tom Yeh 教授的 LinkedIn 文章,我已获得他的许可并进行了编辑。)
那么,我们开始吧:
自注意力
为了提供一些背景,这里有一个指针,展示我们如何在变压器的外部结构中处理‘注意力加权’。
注意力权重矩阵(A)
注意力权重矩阵A是通过将输入特征输入查询-键(QK)模块获得的。这个矩阵试图找到输入序列中最相关的部分。在创建注意力权重矩阵A时,自注意力机制开始发挥作用,利用 QK 模块进行计算。

QK 模块是如何工作的?
让我们来看看自注意力(Self-Attention)的不同组成部分:查询(Q)、键(K)和值(V)。
我喜欢用聚光灯的比喻来帮助我形象化这个模型,理解它如何照亮序列中的每个元素,并尝试找到最相关的部分。把这个比喻延伸一下,让我们用它来理解自注意力的不同组成部分。
想象一个巨大的舞台正在为世界上最大的《麦克白》制作做准备。外面的观众兴奋不已。
-
主角走上舞台,聚光灯照在他身上,他用洪亮的声音问道:“我应该夺取王冠吗?”观众低声耳语,猜测这个问题将会引领到何种结局。因此,麦克白本身代表了 查询(Q) 的角色,他提出关键问题并推动故事向前发展。
-
根据麦克白的提问,聚光灯转向了其他掌握答案信息的重要人物。故事中其他关键人物,如麦克白夫人,通过其行动激发了麦克白的野心和行动。这些人物可以视为 键(K) ,他们根据所知的不同细节解开故事的各个方面。
-
最后,扩展的角色——家人、朋友、支持者和反对者通过他们的行动和视角为麦克白提供了足够的动机和信息。这些可以视为 值(V)。* 值(V)推动麦克白做出决策并塑造故事的命运。*
随之而来的是世界上最精彩的演出之一,它将深深铭刻在敬畏的观众脑海中,成为多年之后仍然难以忘怀的经典。
现在我们已经见证了Q、K、V在戏剧世界中的作用,让我们回到矩阵的世界,学习QK 模块背后的数学原理。这是我们将要遵循的路线图:

自注意力机制的路线图
于是,过程开始了。
我们已知:
一组 4 个特征向量(维度 6)

我们的目标:
将给定的特征转化为注意力加权特征。
[1] 创建查询、键和值矩阵
为此,我们将特征与线性变换矩阵 W_Q、W_K 和 W_V 相乘,分别得到查询向量(q1,q2,q3,q4)、键向量(k1,k2,k3,k4)和值向量(v1,v2,v3,v4),如下面所示:
为了得到Q,将 W_Q 与 X 相乘:

为了得到K,将 W_K 与 X 相乘:

类似地,为了得到V,将 W_V 与 X 相乘。
需要注意:
-
如上所述的计算所示,我们对查询和键使用相同的特征集。这也是“自我”概念的体现,即模型使用相同的特征集来生成其查询向量和键向量。
-
查询向量表示当前的单词(或标记),我们想要计算其相对于序列中其他单词的注意力得分。
-
键向量表示输入序列中的其他单词(或标记),我们计算每个键向量相对于当前单词的注意力得分。
[2] 矩阵乘法
下一步是将 K 的转置与 Q 相乘,即 K^T . Q。
这里的想法是计算每一对查询和键向量之间的点积。通过计算点积,我们可以估计每个“键-查询”对之间的匹配得分,利用余弦相似度的概念。这就是缩放点积注意力中的 ‘点积’ 部分。
余弦相似度
余弦相似度是向量之间夹角的余弦值;也就是说,它是向量的点积除以它们长度的乘积。它大致衡量了两个向量是否朝同一方向指向,从而意味着这两个向量是相似的。
记住 cos(0°) = 1, cos(90°) = 0 , cos(180°) = -1
如果两个向量之间的点积大约为 1,意味着它们之间的夹角接近零,即它们非常相似。
如果两个向量之间的点积大约为 0,意味着它们是正交的,且不太相似。
如果两个向量之间的点积大约为 -1,意味着它们之间的夹角接近 180°,即它们是相反的。

[3] 缩放
下一步是将每个元素按维度 ‘d_k’ 的平方根进行缩放/归一化。在我们的例子中,数字是 3。缩放有助于保持维度对匹配得分的影响在可控范围内。
它是如何做到的呢?根据原始的 Transformer 论文,并回顾一下概率论 101,如果两个独立同分布(i.i.d)变量 q 和 k 具有均值为 0,方差为 1,维度为 d,则它们相乘后的结果是一个新的随机变量,其均值保持为 0,但方差变为 d_k。
现在,想象一下如果我们的维度增加到 32、64、128 或甚至 4960 时,匹配得分会是什么样的。更大的维度会增加方差,并将值推入“未知”区域。
为了简化计算,考虑到 sqrt [3] 大约是 1.73205,我们将其替换为 [ floor(□/2) ]。
取整函数
取整函数接受一个实数作为输入,并返回不大于该实数的最大整数。
例如:floor(1.5) = 1, floor(2.9) = 2, floor (2.01) = 2, floor(0.5) = 0。
floor 函数的反函数是 ceil 函数。

这是“缩放”部分的缩放点积注意力。
[4] Softmax
这一步分为三个部分:
-
将每个单元格中的数字取 e 的幂(为了简化,我们使用 3 的幂来计算每个单元格中的数字。)
-
求出每列中这些新值的总和。
-
对于每一列,将每个元素除以其相应的总和(归一化)。归一化每一列的目的是使数字总和为 1。换句话说,每一列然后成为一个注意力的概率分布,这给我们带来了我们的注意力权重矩阵(A)。

这个注意力权重矩阵是我们在转换器部分的第 2 步中通过 QK 模块传递特征矩阵后获得的。
(备注:注意力权重矩阵中的第一列存在一个拼写错误,因为当前元素的总和不为 1。请仔细检查。我们允许这些错误,因为我们是人类。)
Softmax 步骤很重要,因为它为前面步骤中获得的分数分配概率,从而帮助模型决定在当前查询下需要给每个单词多少重要性(更高/更低的注意力权重)。正如预期的那样,更高的注意力权重表示更大的相关性,使模型能够更准确地捕捉依赖关系。
再次强调,前一步中的缩放在这里变得很重要。没有缩放,结果矩阵的值会被推到 Softmax 函数处理不好的区域,可能导致梯度消失。
[5] 矩阵乘法
最后,我们将值向量(Vs)与注意力权重矩阵(A)相乘。这些值向量很重要,因为它们包含与序列中每个单词相关的信息。

这一步中最终乘法的结果是注意力加权特征 Zs,这是自注意机制的最终解决方案。这些注意力加权特征基本上包含了特征的加权表示,根据上下文的相关性为具有更高相关性的特征分配更高的权重。
现在有了这些信息,我们继续转换器架构中的下一步,其中前馈层进一步处理这些信息。
这就是辉煌的自注意力技术的结束!
根据我们上面讨论的想法,回顾所有关键点:
-
注意机制是为了改善 RNN 的性能而产生的,解决了编码器-解码器架构中固定长度向量表示的问题。软长度向量的灵活性,重点放在序列中相关部分上,是注意力背后的核心优势。
-
自注意力机制被引入作为一种将上下文思想融入模型的方法。自注意力机制评估它所处理的相同输入序列,因此使用了“自”这个词。
-
自注意力机制有许多变体,且目前仍在不断努力使其更加高效。然而,缩放点积注意力是其中最受欢迎的一种,也是 Transformer 架构被认为如此强大的关键原因之一。
-
缩放点积自注意力机制包括查询-键模块(QK 模块)以及Softmax 函数。QK 模块负责通过计算注意力分数来提取输入序列中每个元素的相关性,而 Softmax 函数通过为注意力分数分配概率来补充它。
-
一旦计算出注意力分数,它们会与值向量相乘,以获得加权注意力特征,然后这些特征会传递到前馈层。
多头注意力
为了适应对序列的多样化和全面表示,多个自注意力机制副本会并行实现,然后它们会被连接起来,生成最终的加权注意力值。这被称为多头注意力。
Transformer 概述
这就是 Transformer 架构的内部工作原理。将其与外部结构结合起来,以下是 Transformer 机制的总结:
-
Transformer 架构中的两个关键思想是加权注意力和前馈层(FFN)。这两者结合在一起,使得 Transformer 可以从两个方向分析输入序列。注意力基于位置查看序列,FFN则基于特征矩阵的维度来分析。
-
驱动注意力机制的部分是缩放点积注意力,它由QK 模块组成,并输出加权注意力特征。
‘注意力才是你所需要的一切’
Transformer 只存在了几年,AI 领域已经在此基础上取得了巨大的进展,且这一努力仍在持续。当论文的作者给他们的论文起这个标题时,他们可不是开玩笑。
再次看到一个基本思想——‘点积’与某些修饰结合,竟然能变得如此强大,真是令人感到有趣!

图片来自作者
附言:如果你希望独立完成这项练习,下面是可以使用的空白模板。
现在去享受一下练习吧,同时关注你的Robtimus Prime!
相关工作:
以下是《深入了解手动系列》中的其他文章:
-
手动深入探讨向量数据库 ✍ 探索向量数据库背后到底发生了什么。
-
手动深入探讨 Sora 的扩散 Transformer (DiT) ✍ 探索 Sora 最新视频背后的秘密。
-
手动深入探讨 Transformers ✍ 探索 transformers 强大功能背后的力量。
参考文献:
-
Vaswani, Ashish, Noam Shazeer, Niki Parmar, Jakob Uszkoreit, Llion Jones, Aidan N. Gomez, Łukasz Kaiser 和 Illia Polosukhin. “注意力即一切”。神经信息处理系统进展 30 (2017)。
-
Bahdanau, Dzmitry, Kyunghyun Cho 和 Yoshua Bengio. “神经机器翻译:通过联合学习对齐和翻译”。CoRR abs/1409.0473 (2014)。
深入了解 Sora 的扩散变换器(DiT)手工分析 ✍︎
探索 Sora 最先进视频背后的秘密
·发布于 Towards Data Science ·12 分钟阅读·2024 年 4 月 2 日
--

图片来源:作者
“在古老的 DiTharos 大陆,曾经有一个传说,叫做 Sora。一个象征着无限潜力的传说,涵盖了天空的辽阔与宏伟。”
当它展开色彩斑斓的翅膀,飞翔在辽阔的天空,光线在它引人注目的身体上反射时,人们能听到“Sora 就是天空”的声音回响在天际。使它成为传说的,不仅仅是它那宏大的体量,还有它驾驭散落在旋云中的光元素的力量。凭借强大的力量,Sora 凭一旋所创造的魔法,堪称一绝!
他们说,Sora 依然存在,日复一日磨砺技艺,变得愈加强大,准备在黄金时刻翱翔。当你今天在天空中看到一抹深红,你会知道那是传说的一颗星星正飞入光明的领域!
这是我讲给儿子听的一个关于远方土地上神话般的龙的故事。我们称它为“Sora 的传说”。他非常喜欢这个故事,因为 Sora 既巨大又强大,能够照亮天空。当然,现在他还不理解变换器和扩散的概念,他才四岁,但他确实明白一个使用光的力量并统治着 DiTharos 的宏伟龙的概念。

图片来源:作者(强大的 Sora 由我的儿子创作——色彩选择和大胆的笔触都是他的作品。)
Sora by Open AI
这个故事与我们世界上 Sora(Open AI 的文本到视频模型)如何在人工智能领域诞生并席卷全球的过程非常相似。从原则上讲,Sora 是由William Peebles 和 Saining Xie 在 2023 年开发的扩散变换器(DiT)。
换句话说,它利用扩散的概念来预测视频,并通过变换器的强大功能实现下一阶段的扩展。为了更好地理解这一点,让我们尝试回答这两个问题:
-
当 Sora 接收到一个提示词时,它会做什么?
-
它是如何结合扩散变换器的理念的?
说到 Sora 制作的视频,这是我最喜欢的一部,内容是一只可爱的斑点狗在意大利街头。它的动作多么自然!
视频使用的提示词是:“相机直接面对意大利布拉诺的彩色建筑。一只可爱的斑点狗透过一栋一楼建筑的窗户看外面。许多人在建筑前的运河街道上走路和骑车。”
Sora 是如何做到这一点的?
不再多说,让我们深入了解细节,看看 Sora 如何根据文本提示生成这些超现实的视频。
Sora 是如何工作的?
再次感谢 Tom Yeh 教授精彩的《手工 AI 系列》,我们有了这篇关于 Sora 的精彩文章作为讨论材料。(以下所有图片,除非另有注明,均来自 Tom Yeh 教授的上述 LinkedIn 文章,我已获得他的许可进行编辑。)
那么,我们开始吧:
我们的目标 — 根据文本提示生成视频。
我们给定了:
-
训练视频
-
文本提示
-
扩散步骤 t = 3
对于我们的例子,您能猜到我们的文本提示是什么吗?你猜对了。它是“天空是 Sora”。当扩散步骤 t = 3 时,意味着我们在三步内加入噪声或扩散模型,但为了说明方便,我们将在这个例子中只进行一步。
什么是扩散?
扩散主要指的是粒子散射的现象——想象一下我们如何享受阳光透过云层照射出来的柔和光线。这种柔和的光辉可以归因于阳光穿过云层时发生的散射现象,导致光线向不同方向扩散。
粒子的随机运动推动了这种扩散。这正是图像生成中扩散模型的工作原理。向图像中添加随机噪声,导致图像中的元素偏离原始状态,从而为创造更精细的图像铺平道路。
当我们谈论图像模型中的扩散时,记住的关键概念是“噪声”。
过程从这里开始:
[1] 将视频转换为补丁
在处理文本生成时,模型将大语料库分解成小块,称为 tokens,并使用这些 tokens 进行所有计算。类似地,Sora 将视频分解为更小的元素,称为视觉补丁,以简化工作。
由于我们讨论的是视频,因此涉及的是多帧的图像。在我们的示例中,我们有四个帧。每一个帧或矩阵包含了组成图像的像素。

这里的第一步是将这个训练视频转换成如下的 4 个时空补丁:

[2] 减少这些视觉补丁的维度:编码器
接下来是降维。降维的概念已经存在超过一个世纪了 (小知识:主成分分析,简称 PCA,是由卡尔·皮尔逊在 1901 年提出的),但它的重要性至今未曾褪色。
Sora 也使用了这个方法!
当我们讨论神经网络时,降维的一个基本思想是编码器。编码器通过设计,将高维数据转换为低维数据,重点捕捉数据中最相关的特征。这样做是双赢:它提高了计算效率和速度,同时算法也能获得有用的数据进行处理。
Sora 使用相同的思路,将高维像素转化为低维的潜在空间。为此,我们将补丁与权重和偏置相乘,再经过 ReLU 激活函数。
注意:
线性变换:输入的嵌入向量与权重矩阵 W 相乘。
然后加上偏置向量 b,
z = Wx + b,其中 W 是权重矩阵,x 是我们的词嵌入,b 是偏置向量。
ReLU 激活函数:接下来,我们将 ReLU 应用到这个中间的 z 上。
ReLU 返回输入与零的逐元素最大值。数学上表示为 h = max{0, z}。
-
这里的权重矩阵是一个 2x4 的矩阵 [ [1, 0, -1, 0], [0, 1, 0, 1] ],偏置为 [0,1]。
-
这里的补丁矩阵是 4x4。
权重矩阵 W 的转置与偏置 b 及补丁相乘,再经过 ReLU,得到的潜在空间仅是一个 2x4 的矩阵。因此,通过使用视觉编码器,“模型”的维度从 4(2x2x1)降到 2(2x1)。

在原始的 DiT 论文中,这个降维是从 196,608(256x256x3)降到 4096(32x32x4),这是一个巨大的降维。想象一下,从 196,608 像素降到 4096 像素——降维了 48 倍!
在这个降维之后,我们进入整个过程中的一个最重要的步骤——扩散。
[3] 通过噪声扩散模型
为了引入扩散,我们将采样噪声添加到前一步得到的潜在特征中,从而找到噪声潜在特征。这里的目标是让模型检测噪声是什么。

本质上,这是图像生成的扩散思想。
通过向图像添加噪声,模型被要求猜测噪声是什么以及它的样子。作为回报,模型可以根据它的猜测和从噪声图像中学到的东西,生成一个全新的图像。
它也可以看作是从语言模型中删除一个词,并要求模型猜测被删除的词是什么。
由于训练视频已被减少并加入了噪声,接下来的步骤是利用文本提示来生成符合提示的视频。我们通过使用自适应归一化层来进行条件化。
[4]-[6] 通过自适应归一化层进行条件化
“条件化”本质上意味着我们尝试使用可用的附加信息来影响模型的行为。例如:由于我们的提示是‘Sora 是天空’,我们希望模型关注像天空或云朵这样的元素,而不是过多关注其他概念,如帽子或植物。因此,自适应归一化层可以更好地解释——动态地根据输入数据调整和移动网络中的数据。
什么是缩放和位移?
缩放发生在我们进行乘法运算时,例如我们可能从变量 A 开始。如果我们假设将其与 2 相乘,那么我们得到 2*A,这会将 A 的值放大 2 倍。如果我们将其乘以 1/2,那么值会缩小到 0.5 倍。
位移用加法表示,例如我们可能在数轴上行走。我们从 1 开始,需要移动到 5。我们该怎么做?我们可以加 4,得到 1+4=5,或者我们也可以加一百个 0.04,得到 1+(100*0.04)=5。最终取决于我们是想走大步(4)还是小步(0.04)来达成目标。
[4] 编码条件
为了利用条件,在我们的例子中就是我们用于构建模型的信息,首先我们将其转换为模型能够理解的形式,即向量。
-
该过程的第一步是将提示转换为文本嵌入向量。
-
下一步是将步骤t = 3 转换为二进制向量。
-
第三步是将这些向量连接在一起。

[5] 估算缩放/位移
请记住,在这里我们使用的是“自适应”层归一化,这意味着它根据模型的当前条件调整其值。因此,为了捕捉数据的正确本质,我们需要包括数据中每个元素的重要性。这是通过估算缩放和位移来实现的。
为了估算我们模型的这些值,我们将提示和扩散步骤的连接向量与权重相乘,并添加偏差。这些权重和偏差是可学习参数,模型会学习并更新这些参数。

(备注:根据我理解,结果向量中的第三个元素应该是 1。这可能是原文中的一个小错误,但作为人类,我们可以容忍一点错误,不是吗?为了保持一致性,我在这里继续使用原文中的值。)
这里的目标是估计尺度 [2,-1] 和偏移 [-1,5](由于我们的模型大小为 2,因此有两个尺度和两个偏移参数)。我们将它们分别保持为 'X' 和 '+'。

[6] 应用尺度/偏移
为了应用在上一步骤中获得的尺度和偏移,我们将步骤 3 中的噪声潜在变量乘以 [2, -1] 并通过添加 [-1,5] 来进行偏移。

结果是 ‘条件化’ 的噪声潜在变量。
[7]-[9] 变换器
最后三个步骤是将变换器元素添加到上述扩散和条件化步骤中。这一步帮助我们找到模型预测的噪声。
[7] 自注意力
这是变换器背后的关键思想,使其如此出色!
什么是自注意力?
这是一种机制,其中句子中的每个单词都会分析其他所有单词,并衡量它们对彼此的重要性,从而理解文本中的上下文和关系。
为了启用自注意力,将条件化噪声潜在变量输入到查询-键(Query-Key)函数中,以获得自注意力矩阵。这里为了简便省略了 QK 值。
[8] 注意力池化
接下来,我们将条件化噪声潜在变量与自注意力矩阵相乘,以获得注意力加权特征。

[9] 逐点前馈网络
再次回到基础,我们将注意力加权特征与权重和偏置相乘,以获得预测的噪声。

训练
最后一步是使用 均方误差(MSE)在 预测噪声 和 采样噪声(地面真实值)之间训练模型。
[10] 计算 MSE 损失梯度并更新可学习参数
使用 MSE 损失的梯度,我们通过反向传播来更新所有可学习的参数(例如,自适应归一化层中的权重和偏置)。

编码器和解码器的参数被冻结,不可学习。
(备注:第二行的第二个元素应该是 -1,这是一个小错误,但它使得事情变得更好)。
[11]-[13] 生成新样本
[11] 去噪
现在我们准备生成新的视频(耶!),首先需要去除我们引入的噪声。为此,我们从噪声潜在变量中减去预测的噪声,以获得无噪声的潜在变量。

请注意,这与我们原始的潜在空间不同。原因是我们在其中经历了多个条件和注意力步骤,这些步骤将我们问题的上下文纳入了模型。因此,使得模型在生成视频时能更好地理解其目标应该是什么。
[12] 将潜在空间转换回像素:解码器
就像我们为编码器所做的那样,我们将潜在空间补丁与权重和偏置相乘,之后应用 ReLU。在这里,我们可以观察到,经过解码器的处理后,模型已经恢复到原始的 4 维度,而在使用编码器时,我们将维度降至 2。

[13] 视频时间!
最后的步骤是将上述矩阵的结果排列成一系列帧,最终给我们带来新的视频。太棒了!

随着这一点,我们来到了这一极具力量的技术的结尾。恭喜你,已经创建了一个 Sora 视频!
总结以上所说的内容,这里有五个关键点:
-
将视频转换为视觉补丁并减少其维度是至关重要的。视觉编码器在这里是我们的朋友。
-
正如名字所示,扩散是此方法的核心。在视频中添加噪声,并在后续的每一步(以不同的方式)与之进行处理,是这项技术的基础。
-
接下来是增强扩散过程能力并放大模型规模的变换器架构。
-
一旦模型训练完成并准备好收敛到解决方案,两个 D——去噪器和解码器便派上用场。一个去除噪声,另一个则将低维空间投射回其原始维度。
-
最后,来自解码器的结果像素被重新排列以生成所需的视频。
(一旦你完成了文章,我建议你再读一遍开头的故事。你能发现 DiTharos 中的 Sora 和我们世界中的 Sora 之间的相似之处吗?)
扩散-变换器(DiT)组合
Sora 能够制作的那种视频,值得一提的是,扩散-变换器(Diffusion-Transformer)二重奏非常强大。与此同时,视觉补丁的概念为我们提供了一条可供尝试不同图像分辨率、纵横比和时长的路径,从而能够进行最广泛的实验。
总体而言,可以毫不夸张地说,这个想法是开创性的,并且无疑将长期存在。根据这篇纽约时报的文章,Sora 的名字源自日语中的“天空”一词,旨在唤起无限潜力的概念。鉴于我们已经见证了其最初的潜力,确实可以说 Sora 已经开辟了人工智能的新前沿。现在,剩下的就是看看它如何经得起安全性和时间的考验。
根据DiTharos的传说——“Sora 继续生存,不断磨练技能,随着每一天的过去变得更强,准备在黄金时刻起飞!”
附言:如果你想自己完成这个练习,这里有一个空白模板供你使用。
现在,去和 Sora 一起在‘DiTharos’的土地上玩得开心吧!
深入探讨变形金刚 ✍︎
探索变形金刚背后强大功能的细节
·发表于 Towards Data Science ·6 分钟阅读·2024 年 4 月 12 日
--
我们小区发生了一件新事。
一辆我儿子喜欢叫做“机器人卡车”的新车已经在我们的街道上安家了。
那是一辆特斯拉 Cyber Truck,我已经尝试多次向儿子解释这个名字的含义,但他坚持称其为“机器人卡车”。现在每次我看着机器人卡车并听到这个名字时,它让我想起电影《变形金刚》,其中的机器人能够在汽车和机器人之间变形。
难道不奇怪吗,今天我们所知道的变形金刚很可能正在为这些机器人卡车提供动力?这几乎是一个完整的循环。那么,我在说这些有什么目的呢?
好吧,我的目的地就是——变形金刚。不是指机器人汽车,而是神经网络中的变形金刚。你被邀请了!

图片由作者提供(我们的变形金刚 — ‘机器人霸主’。颜色由我的艺术家儿子指定。)
什么是变形金刚?
变形金刚本质上是神经网络。专门用于从数据中学习上下文的神经网络。
但使它们与众不同的是,存在一些机制,消除了对标注数据集和卷积或递归的需求。
这些特殊的机制是什么?
变形金刚有很多种。但是,真正驱动变形金刚的两个机制是注意力加权和前馈网络(FFN)。
什么是注意力加权?
注意力加权是一种技术,通过它,模型学习要集中关注输入序列中的哪个部分。可以把它想象成“索隆之眼”始终扫描所有内容,并将光照射到相关部分。
有趣的事实:显然,研究人员几乎把 Transformer 模型命名为“Attention-Net”,因为注意力在其中起着如此关键的作用。
什么是 FFN?
在 Transformer 的上下文中,FFN 本质上是一个常规的多层感知器,作用于一批独立的数据向量。结合注意力机制,它生成正确的“位置-维度”组合。
注意力和 FFN 是如何工作的?
所以,不再拖延,让我们深入了解注意力加权和FFN如何使 Transformer 如此强大。
这篇讨论基于 Tom Yeh 教授的精彩 AI 系列《手把手教你理解Transformer》。 (除非另有说明,下面的所有图片都来自 Tom Yeh 教授的 LinkedIn 帖子,我已获得他的许可对其进行编辑。)
所以我们开始吧:
这里的关键思想是:注意力加权和前馈网络(FFN)。
记住这些,假设我们给定了:
- 来自前一模块的 5 个输入特征(这里是一个 3x5 矩阵,其中 X1、X2、X3、X4 和 X5 是特征,每一行分别表示它们的特征)。

[1] 获取注意力权重矩阵 A
过程中的第一步是获取注意力权重矩阵 A。这部分正是自注意力机制发挥作用的地方。它试图做的是在这个输入序列中找到最相关的部分。
我们通过将输入特征输入查询-键(QK)模块来完成此操作。为了简单起见,这里未包含 QK 模块的详细信息。

[2] 注意力加权
一旦我们得到了注意力权重矩阵 A(5x5),就可以将输入特征(3x5)与其相乘,从而得到注意力加权特征 Z。

这里重要的一点是,这些特征是根据它们的位置P1、P2 和 P3 进行组合的,即水平组合。
进一步细化,考虑按行执行的计算:
P1 X A1 = Z1 → 位置 [1,1] = 11
P1 X A2 = Z2 → 位置 [1,2] = 6
P1 X A3 = Z3 → 位置 [1,3] = 7
P1 X A4 = Z4 → 位置 [1,4] = 7
P1 X A5 = Z5 → 位置 [1,5] = 5
.
.
.
P2 X A4 = Z4 → 位置 [2,4] = 3
P3 X A5 = Z5 → 位置 [3,5] = 1
举个例子:

一开始似乎有些繁琐,但按照行乘法进行,结果应该是相当直接的。
有趣的是,由于我们的注意力权重矩阵A的排列方式,新的特征Z实际上是X的组合,如下所示:
Z1 = X1 + X2
Z2 = X2 + X3
Z3 = X3 + X4
Z4 = X4 + X5
Z5 = X5 + X1
(提示:查看矩阵A中 0 和 1 的位置)。
[3] FFN : 第一层
下一步是将注意力加权特征输入到前馈神经网络中。
然而,这里与之前步骤的不同之处在于跨维度组合值,而不是在位置上进行组合。其操作如下:

这样做的效果是,它从另一个方向查看数据。
- 在注意力步骤中,我们基于原始特征将输入进行组合,以获得新的特征。
- 在这个 FFN 步骤中,我们考虑它们的特征,即将特征垂直组合以获得我们的新矩阵。
例如:P1(1,1) * Z1(1,1)
P2(1,2) * Z1 (2,1)
P3(1,3) * Z1(3,1) + b(1) = 11,其中 b 是偏置。
再次进行逐元素的行操作。注意到这里新矩阵的维度增加到了 4。
[4] ReLU
我们最喜欢的步骤:ReLU,在这个步骤中,前面矩阵中得到的负值被归零,正值保持不变。

[5] FFN:第二层
最后,我们将其传递到第二层,在那里结果矩阵的维度从 4 降低回 3。

这里的输出已经准备好输入到下一个模块(请参见其与原始矩阵的相似性),并且整个过程从头开始重复。
这里要记住的两件关键事是:
-
注意力层在位置间(水平方向)进行组合。
-
前馈层在维度间(垂直方向)进行组合。
这就是变形金刚强大之处的秘密——从不同方向分析数据的能力。
总结以上观点,以下是关键要点:
-
Transformer 架构可以被看作是注意力层和前馈层的组合。
-
注意力层结合了特征以生成新的特征。例如,想象将两个机器人 Robo-Truck 和 Optimus Prime 结合成一个新机器人 Robtimus Prime。
-
前馈(FFN)层结合了特征的部分或特征,生成新的部分/特征。例如,Robo-Truck 的轮子和 Optimus Prime 的离子激光可以组合成一个带轮子的激光。
强大的变形金刚
神经网络已经存在了一段时间。卷积神经网络(CNN)和递归神经网络(RNN)曾一度占据主导地位,但自从 2017 年引入 Transformer 之后,局面发生了翻天覆地的变化。从那时起,人工智能领域以指数速度增长——每天都有新的模型、新的基准和新的学习成果涌现。只有时间才能证明,这一卓越的理念是否会引领我们迈向更大的突破——一个真正的“变形金刚”。
但目前来说,说一个理念可以真正改变我们生活的方式并不为过!

图片由作者提供
附言:如果你想自己完成这个练习,下面是供你使用的空白模板。
现在去玩得开心一点,创造属于你自己的Robtimus Prime!
手工深入了解向量数据库 ✍︎
探索向量数据库背后究竟发生了什么
·发表于Towards Data Science ·阅读时间 8 分钟·2024 年 3 月 20 日
--
前几天,我请我最喜欢的大型语言模型(LLM)帮助我向将近 4 岁的孩子解释向量。几秒钟内,它就编出了一个充满神话生物、魔法和向量的故事。结果!我得到了一个新的儿童书籍草图,而且这很令人印象深刻,因为独角兽被叫做‘LuminaVec’。

图片由作者提供(‘LuminaVec’,由我将近 4 岁的孩子解释)
那么,模型是如何帮助编织这种创造性魔法的呢?答案是通过使用向量(在现实生活中),而且很可能是向量数据库。怎么做到的呢?让我来解释一下。
向量与嵌入
首先,模型并不理解我输入的具体单词。帮助它理解这些单词的是它们的数字表示形式,这些表示是以向量的形式存在的。这些向量帮助模型在不同单词之间找到相似性,同时聚焦于每个单词的有意义信息。它通过使用嵌入来做到这一点,嵌入是低维度的向量,旨在捕捉信息的语义和上下文。
换句话说,嵌入中的向量是由一系列数字组成的,这些数字指定了一个对象相对于参考空间的位置。这些对象可以是定义数据集变量的特征。在这些数字向量值的帮助下,我们可以判断一个特征与另一个特征之间的距离——它们是相似(接近)还是不相似(远离)?
这些向量确实很强大,但当我们谈论大语言模型(LLM)时,我们需要格外小心,因为有一个“large”字眼。正如这些“大”模型的情况一样,这些向量可能会迅速变得非常长且复杂,跨越数百甚至数千维。如果不小心处理,处理速度和不断增加的成本可能会很快变得沉重!
向量数据库
为了解决这个问题,我们有我们的强大战士:向量数据库。
向量数据库是包含这些向量嵌入的特殊数据库。相似的对象在向量数据库中的向量彼此靠近,而不相似的对象的向量则相距较远。因此,与其每次查询时都解析数据并生成这些向量嵌入,这会消耗大量资源,不如一次性将数据通过模型运行,存储在向量数据库中,按需检索。这样,向量数据库就成为解决大规模和高速度问题的最强大方案之一,尤其是对于这些大语言模型(LLM)。
所以,回到关于彩虹独角兽、闪亮魔法和强大向量的故事——当我问模型这个问题时,它可能遵循了这样的过程——
-
嵌入模型首先将问题转化为一个向量嵌入。
-
然后,这个向量嵌入与与 5 岁小朋友的有趣故事和向量相关的向量数据库中的嵌入进行了比较。
-
基于这种搜索和比较,返回了最相似的向量。结果应包括按与查询向量的相似度排序的向量列表。
它到底是如何工作的?
为了更进一步简化,怎么让我们以微观层面来解决这些步骤呢?是时候回到基础了!感谢 Tom Yeh 教授,我们有了这份精美的手工作品,它解释了向量和向量数据库背后的工作原理。(除非另有说明,以下所有图片均来自 Tom Yeh 教授在上述 LinkedIn 帖子中的内容,我已在他的许可下进行了编辑。)
所以,我们开始吧:
在我们的示例中,我们有一个包含三句话的数据集,每句话包含 3 个单词(或标记)。
-
你好吗
-
你是谁
-
我是谁
而我们的查询是句子“我是不是你”。

在现实生活中,数据库可能包含数十亿句子(想象一下维基百科、新闻档案、期刊论文或任何文档集合),每个句子有成千上万的标记。现在,舞台已经搭建好,开始过程吧:
[1] 嵌入:第一步是为我们想要使用的所有文本生成向量嵌入。为此,我们需要在一个包含 22 个向量的表格中查找对应的单词,其中 22 是我们示例中的词汇量。

在实际生活中,词汇表的大小可以达到数万个。词嵌入的维度通常为几千(例如,1024、4096)。
通过在词汇表中查找单词how are you,它的词嵌入如下所示:

[2] 编码:下一步是对词嵌入进行编码,以获得每个词的特征向量序列。以我们的示例为例,编码器是一个简单的感知机,由一个带有 ReLU 激活函数的线性层组成。
快速回顾:
线性变换:输入嵌入向量与权重矩阵 W 相乘,然后与偏置向量b相加。
z = Wx+b,其中W是权重矩阵,x 是我们的词嵌入,b是偏置向量。
ReLU 激活函数:接下来,我们对这个中间值 z 应用 ReLU。
ReLU 返回输入和零之间的逐元素最大值。数学表达式为h = max{0,z}。
因此,对于这个例子,文本嵌入看起来像这样:

为了演示它是如何工作的,让我们以最后一列为例计算值。
线性变换:
[1.0 + 1.1 + 0.0 + 0.0] + 0 = 1
[0.0 + 1.1 + 0.0 + 1.0] + 0 = 1
[1.0 + (0).1 + 1.0 + 0.0] + (-1) = -1
[1.0 + (-1).1 + 0.0 + 0.0] + 0 = -1
ReLU
max {0,1} = 1
max {0,1} = 1
max {0,-1} = 0
max {0,-1} = 0
因此,我们得到了特征向量的最后一列。我们可以对其他列重复相同的步骤。
[3] 均值池化:在此步骤中,我们通过对列进行平均化来将特征向量合并为一个单一的向量。这通常被称为文本嵌入或句子嵌入。

可以使用其他池化技术,如 CLS、SEP,但均值池化是使用最广泛的方法。
[4] 索引:下一步涉及减少文本嵌入向量的维度,这是通过投影矩阵来完成的。这个投影矩阵可以是随机的。这里的想法是获得一个简短的表示形式,从而允许更快的比较和检索。

这个结果被保存在向量存储中。
[5] 重复:上述步骤[1]-[4]会对数据集中的其他句子“who are you”和“who am I”重复进行。
现在我们已经在向量数据库中对数据集进行了索引,接下来我们开始进行实际查询,看看这些索引如何发挥作用,最终为我们提供解决方案。
查询:“am I you”
[6] 要开始,我们重复上面的相同步骤——嵌入、编码和索引,以获得查询的 2D 向量表示。

[7] 点积(寻找相似性)
一旦完成了之前的步骤,我们就会执行点积计算。这一步非常重要,因为这些点积是查询向量与数据库向量之间进行比较的核心。为了执行这一步,我们将查询向量转置,并与数据库向量相乘。

[8] 最近邻
最后一步是执行线性扫描以找到最大的点积,在我们的示例中是 60/9\。这就是“我是谁”的向量表示。在实际生活中,线性扫描可能极其缓慢,因为它可能涉及数十亿个值,替代方法是使用近似最近邻(ANN)算法,如分层可导航小世界(HNSW)。

这也为我们带来了这优雅方法的结尾。
因此,通过使用向量数据库中数据集的向量嵌入并执行上述步骤,我们能够找到最接近查询的句子。嵌入、编码、均值池化、索引以及点积构成了这个过程的核心。
‘大’图景
然而,为了再一次引入‘大’的视角——
-
一个数据集可能包含数百万或数十亿个句子。
-
它们每个的标记数量可能达到数万。
-
单词嵌入的维度可以达到数千。
当我们将所有这些数据和步骤整合在一起时,我们实际上是在处理尺寸庞大的维度。因此,为了应对这一壮丽的规模,向量数据库发挥了重要作用。既然我们在文章开头提到了 LLMs,那么值得一提的是,由于向量数据库的规模处理能力,它们在检索增强生成(RAG)中发挥了重要作用。向量数据库提供的可扩展性和速度,使得 RAG 模型的高效检索成为可能,从而为高效生成模型铺平了道路。
总的来说,毫无疑问,向量数据库是强大的。难怪它们已经存在了一段时间——从开始帮助推荐系统,到如今为大规模语言模型(LLM)提供动力,它们的统治地位持续不断。随着向量嵌入在不同人工智能模式中的快速发展,似乎向量数据库在未来一段时间内将继续保持其主导地位!

作者提供的图片
附言:如果你想独立完成这个练习,这里有一个空白模板供你使用。
现在去尽情享受并创造一些‘辉煌的向量魔法’吧!
深入探讨使用 Python 绘制的累积局部效应图(ALEs)
使用 ALEs 解释机器学习模型的直觉、算法和代码
·发布于 Towards Data Science ·阅读时间:10 分钟·2024 年 5 月 20 日
--

(来源:作者)
高度相关的特征可能会对你的模型解释产生严重影响。它们违反了许多 XAI 方法的假设,并使得很难理解特征与目标之间的关系。同时,移除这些特征并不总是可行的,因为这可能会影响模型的表现。我们需要一种即使在多重共线性的情况下也能提供清晰解释的方法。幸运的是,我们可以依赖 ALEs [1]。
ALEs 是一种全局解释方法。像 PDPs 一样,它们展示了模型捕捉到的趋势。也就是说,特征与目标变量之间是否存在线性、非线性或无关系。然而,我们将看到,识别这些趋势的方法是完全不同的。我们将:
-
让你直观理解 ALEs 是如何创建的。
-
正式定义用于创建 ALEs 的算法。
-
使用 Alibi Explain 包应用 ALEs。
我们将看到,与其他 XAI 方法如 SHAP、LIME、ICE Plots 和 Friedman 的 H-stat 不同,ALEs 提供的解释对多重共线性具有稳健性。
深度优先搜索 — 基本图算法
Python 中 DFS 算法的递归实现,并附带示例和逐步讲解
·发表于 Towards Data Science ·阅读时间 5 分钟·2024 年 9 月 26 日
--

图片由 Shubham Dhage 提供,来源:Unsplash
介绍
在计算机科学中,算法是各种有用技术(如人工智能)的核心,帮助我们高效地解决一系列现实生活中的挑战。其中,图算法因其广泛的应用而占有特殊地位,因为许多问题可以通过图来建模。图是由节点(或顶点)和连接节点的边组成,表示节点之间的关系(或连接)。
一些例子:
-
社交网络:在社交网络中,如 Facebook 或 Instagram,用户可以被表示为节点,用户之间的友谊或连接则是边。[ 1 ]
-
路线规划与导航:在地图中,地点(城市、交叉口或地标)被建模为节点,而它们之间的道路或路径则是边。[ 2 ]
-
网页爬取:互联网可以看作一个庞大的图,其中网页是节点,它们之间的超链接则是边。
-
调度与任务管理:许多任务之间的依赖关系可以建模为有向无环图(DAGs),其中任务是节点,任务之间的依赖关系是边。
-
网络连通性:在计算机网络中,设备(路由器、计算机)是节点,通信链接是边。
用于遍历或探索图的基本算法之一是 深度优先搜索(DFS)。它用于探索(访问)图中的所有节点。另一种补充且基础的算法是广度优先搜索(BFS),这将是另一个单独文章的主题。
在这里,我们将深入了解 DFS 是如何工作的,通过简单的代码、直观的例子以及一些很酷的动画,逐步展示这个算法是如何执行的。
DFS 可以通过两种方式实现:迭代式和递归式。这里,我将向你展示如何通过递归实现,因为在我看来,递归更容易理解和编码。如果你还不熟悉递归,这也是一个学习递归如何工作的绝佳机会。DFS 实现将使用纯 Python 代码。
以下是实现 DFS 算法的代码。
函数有三个输入:一个已访问节点的集合(通常最初为空)、一个图的定义和一个起始节点。逻辑简单而有效:
- 首先,我们检查是否已经访问了给定节点
a. 如果是,跳过检查它的邻居
b. 如果没有,打印该节点并开始访问它的邻居(“for 循环”)
- 重复,直到所有节点都在已访问节点列表中
在这种情况下,函数返回 None(即什么都不返回),因为它打印了已访问的节点并将它们写入外部定义的集合。我们可以更改它的行为,使其返回一个包含所有已访问节点的集合,而不是像这样打印值:
示例 1
首先,我们必须定义我们的示例图。为此,我们将使用邻接矩阵作为 Python 字典。在每个键值对中,键是一个节点,值是与该节点连接的节点列表(邻居)。
以下是创建第一个示例图的代码。在这种情况下,它是一个有向图(为了清晰和简便),但 DFS 同样适用于无向图。
执行函数调用命令后,输出将是已访问的节点系列:

图片来自作者
或者使用下面的替代版本的代码。在这里,我们可以通过小小的改变输入,避免使用任何全局变量,而直接传递一个空集合。输出将是:

图片来自作者
让我们可视化地展示函数栈和最终集合是如何一步步构建的。下面的动画展示了这一过程。

图片来自作者
示例 2
在这个例子中,我们将构建并遍历一种特殊类型的图——决策树。图的定义如下。
在对这个图执行 DFS 后,输出将是:

图片来自作者
下方的动画展示了图形的样子以及深度优先搜索(DFS)如何遍历它。

DFS 遍历一棵树;图片来源:作者
总结
深度优先搜索(DFS)是图论中一个重要的算法,广泛应用于多个领域,从社交网络到决策树。它的递归性质使得理解和实现变得简单,正如本文中的示例所展示的那样。DFS 的简单性以及它能够高效地遍历图中的所有节点,使其成为解决各种计算问题的强大工具。理解 DFS 的工作原理为掌握其他算法,如广度优先搜索(BFS)和路径寻找算法(如 Dijkstra 或 A*)打下了基础。
尝试使用更大更复杂的图形进行实验,并探索它在不同数据结构下的表现。在未来的文章中,我们将探讨其他遍历方法,如广度优先搜索(BFS),并进一步研究它们的使用案例、优点和局限性。
继续练习并挑战自己的极限,很快像深度优先搜索(DFS)这样的图算法就会成为你的第二天性。祝编程愉快!
参考文献
[1] Tsok, Samuel & Yakubu, Hosea & Solomon, Rwat. (2023). 社交媒体网络的图模型及其在 Facebook 和 Facebook Messenger 群组中的应用。《国际计算机科学与工程杂志》。第 9 卷,第 1 页,10.56201/ijcsmt.v9.no1.2023.pg1.12。[链接]
[2] Tianlun Dai, Wenchao Zheng, Jiayue Sun, Cun Ji, Tao Zhou, Mingtong Li, Wei Hu, Ziqiang Yu, 实时动态图上的连续路线规划,《计算机科学文集》,第 174 卷,2020 [链接]
大规模深度学习:并行模型训练
概念与 Pytorch Lightning 示例
·发表于Towards Data Science ·阅读时间 7 分钟·2024 年 4 月 5 日
--

图片由作者使用 Midjourney 创建。
在大量 GPU 上进行并行训练是深度学习的前沿技术。开源图像生成算法 Stable Diffusion在 256 个 GPU 集群上进行了训练。Meta 的AI 研究超级集群包含超过 24,000 个 NVIDIA H100 GPU,用于训练像 Llama 3 这样的模型。
通过使用多个 GPU,机器学习专家减少了训练过程的墙时。训练 Stable Diffusion花费了 150,000 个 GPU 小时,相当于超过 17 年。并行训练将其减少到 25 天。
存在两种类型的并行深度学习:
-
数据并行性,将大型数据集分布到多个 GPU 上。
-
模型并行性,将过大无法在单个 GPU 上运行的深度学习模型分布到多个设备上。
我们在这里将重点讨论数据并行性,因为模型并行性仅在非常大的模型(超过 500M 参数)时才相关。
除了减少壁钟时间外,进行并行训练还有一个经济学上的理由:云计算提供商如AWS 提供单台机器,配备最多 16 个 GPU。并行训练可以利用所有可用的 GPU,从而让你用更少的钱获得更多的价值。
并行计算
并行性是高性能计算中的主流范式。程序员识别出可以独立执行的任务,并将它们分配到大量设备上。程序的串行部分负责分发任务并收集结果。
并行计算可以减少计算时间。一程序如果在四个设备上并行运行,相比于在单个设备上运行,理想情况下可以快四倍。实际上,通信开销限制了这种扩展。
打个比方,想象一群画家在画一面墙。通信开销出现在工头告诉每个人使用什么颜色、涂哪里时。涂画的过程可以并行进行,只有最后的收尾工作是串行的任务。
训练深度学习模型
训练深度学习算法需要数据和一个优化程序,如随机梯度下降。
训练数据被打乱并分成固定大小的批次。每批次,训练程序都会计算实际标签和预测标签之间的损失。模型参数会根据损失相对于模型参数的梯度进行调整。

随机梯度下降。图片由作者制作。
当模型已经遍历过所有训练数据批次一次时,epoch就完成了。
分布式数据并行
下图描述了使用 3 个 GPU 进行深度学习模型训练的数据并行。未训练的模型副本被复制到每个 GPU 上。数据集被分成 3 个部分,每个部分在不同的 GPU 上并行处理。

分布式数据,相同的模型。图片由作者制作。
请注意,每个模型副本看到的训练数据子集是不同的。完成一个 epoch 后,每个模型副本中的权重和偏差都会有所不同。
通过在所有模型实例之间平均权重来协调这三个模型副本。更新后的模型会广播到所有 3 个 GPU,训练继续进行到下一个 epoch。
分布式训练将有效批次大小改变为设备数量 * 原始批次大小。这可能会影响训练、收敛性和模型表现。
在 Pytorch Lightning 中的实现
数据并行需要以下几个要素:
-
一个可以处理分布式训练的数据加载器
-
一个all-reduce函数,用于协调模型副本
-
一个用于不同并行部分之间相互通信的框架
在 PyTorch Lightning 中,Lightning Trainer 处理整个训练过程。通过指定以下关键字参数,它可以开箱即用地执行分布式数据并行训练:
-
节点数量——你想要使用的云计算集群中的机器数量
-
每个节点的设备数量(GPU)每个节点
-
训练策略,这里是分布式数据并行。对于分布式训练,有不同的策略可供选择,它们负责调整训练过程。
对于在 2 个节点,每个节点有 2 个 GPU 的分布式数据并行(ddp)训练,请使用:
import lightning as L
trainer = L.Trainer(nodes=2, devices=2, strategy='ddp')
trainer.fit(model, dataloader)
默认情况下,Lightning Trainer 会在计算环境中使用所有可用的 GPU。这意味着,如果你只想在多 GPU 机器上使用单个 GPU,则需要明确指定。
实验:使用 Country211 数据集进行并行训练
Country211 数据集包含来自 211 个国家的地理标签图像。在我的实验中,我使用了一个预训练的视觉 Transformer,并在 Country211 数据集上微调此模型。代码可在GitHub上获取。
## 如何在卫星数据上微调预训练的视觉 Transformer
PyTorch Lightning 中的逐步教程
towardsdatascience.com
共有 31,650 张训练图像。我在整个过程中使用了批量大小为 256,并且实验了 GPU 数量,从单个节点上的单个 GPU 到分布在四个节点上的八个 GPU。我应用了提前停止条件,如果验证准确率在五个轮次内没有改善,则停止训练。
训练的墙时间,即训练模型所需的总时间,随着使用的 GPU 数量增加而减少。在单个 GPU 上,墙时间为 45 分钟,而在使用 8 个 GPU 时,墙时间降至 11 分钟。
理想的扩展性应为 45 分钟 / 8 个 GPU = 5.6 分钟,但在更多 GPU 上训练时,由于通信开销和训练轮次增多,我们无法达到这一最优值。

不同数量设备的墙时间。图像由作者创建。
为了比较泛化能力,我计算了所有训练模型的测试集准确率。在单一设备上的单个 GPU 训练时,获得了最佳的测试集准确率,但即便如此,模型在泛化方面仍然存在问题:测试集准确率仅为 15%。
面板展示了不同数量 GPU 下准确率的下降,相对于最佳结果。使用的设备越多,应用到测试集时模型的准确性就越差。

测试集准确率。图像由作者创建。
准确率的变化可能是由于有效批次大小的变化——请注意,依据训练策略,我们不会得到完全相同的模型。
在本次实验中,泛化准确性与速度之间存在权衡。使用 8 个 GPU 训练比单设备训练快了四倍,但准确性比单设备训练低了 11%。
平行训练概述
在训练日志文件中,你将看到像这样的行:
Initializing distributed: GLOBAL_RANK: 2, MEMBER: 3/4
Initializing distributed: GLOBAL_RANK: 0, MEMBER: 1/4
Initializing distributed: GLOBAL_RANK: 3, MEMBER: 4/4
Initializing distributed: GLOBAL_RANK: 1, MEMBER: 2/4
----------------------------------------------------------------------------------------------------
distributed_backend=nccl
All distributed processes registered. Starting with 4 processes
----------------------------------------------------------------------------------------------------
LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0,1]
LOCAL_RANK: 1 - CUDA_VISIBLE_DEVICES: [0,1]
LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0,1]
LOCAL_RANK: 1 - CUDA_VISIBLE_DEVICES: [0,1]
并行训练过程必须能够互相通信。这里的通信后端是 NVIDIA 集体通信库(nccl),根据平台会自动初始化。
我们请求了两个节点,每个节点有两个 GPU,共四个并行进程。它们通过全局排名(0–3)进行枚举。在每个节点上,本地排名标识了使用的 GPU(0 或 1)。
当模型很大时,all-reduce 操作可能会花费很长时间。计算集群通过高效的网络连接 GPU,但发送整个模型副本会加重通信负担。
模型并行
Lightning 提供了模型并行的策略。请注意,这仅应在具有数十亿参数的非常大模型中探索,其中模型参数和梯度超过了 GPU 内存。
总结
并行训练可以加速训练过程,帮助你充分利用计算资源。
我们区分了数据并行,其中模型的副本在训练过程中看到数据的子集,以及模型并行,其中模型本身分布在多个设备上。
使用 PyTorch Lightning,数据并行训练就像指定分布式数据并行策略以及每个节点和设备(GPU)的数量一样简单。
数据并行训练减少了壁钟时间并改变了有效批次大小。使用不同数量设备训练的模型可能表现不同,因此建议进行仔细评估。
模型并行训练应该仅在非常大的模型(超过 5 亿参数)中考虑。
重现实验的代码可以在我的 GitHub 上找到:
[## GitHub - crlna16/pretrained-vision-transformer: 使用 PyTorch 的预训练 Vision Transformer…
使用 PyTorch Lightning 的预训练 Vision Transformer - crlna16/pretrained-vision-transformer
参考文献
-
Tal Ben-Nun 等,“揭秘并行和分布式深度学习:深入的并发分析”,2018,
arxiv.org/abs/1802.09941 -
Vision Transformer 模型:Dosovitskiy 等,一张图片值 16x16 个词:用于大规模图像识别的变换器,ICLR,2021。 Paper 和代码
-
用于数据并行训练的 PyTorch Lightning 项目:docs
-
Will Falcon 讲解分布式训练 (TDS, 2019)
深度学习在作物产量预测中的应用(第一部分 — 模型)
提高作物产量并优化灌溉:一种基于深度学习的多变量分析方法
·发布于Towards Data Science ·21 分钟阅读·2024 年 9 月 11 日
--

图片来源:Lumin Osity 于Unsplash
在本文中,我将介绍一个基于深度学习技术的作物产量预测与灌溉优化项目。
深度学习是一种强大的多变量分析方法,尤其适用于处理包含多个变量的复杂数据集。这种技术能够捕捉数据中的复杂模式,为涉及多个因素和交互的难题提供强有力的解决方案。
本项目的目的是提供一个完整的示例,逐步展示如何在实际场景中应用深度学习,涵盖从数据准备到模型构建和评估的各个方面。
我们将共同探索每一个阶段,重点关注支持模型开发的战略决策和技术依据。
如果你喜欢这篇文章并觉得内容有用,请留下反馈并给我 👏。这有助于体现我在这里的工作价值,也能让更多人获得这些知识。让我们开始吧……
数据字典:
本项目中我们使用的数据是虚构的,创建目的是展示如何……
深度学习图解,第二部分:神经网络是如何学习的?
一份关于神经网络如何学习的图解和直观指南
·发表于Towards Data Science ·阅读时间 14 分钟·2024 年 2 月 8 日
--
欢迎来到《深度学习图解》系列的第二部分。在上一篇文章(一定要先读那篇!)中,我们讲解了神经网络是如何工作的,以及训练过的神经网络如何进行预测。我们还了解到,在训练过程中,神经网络通过优化获得了最佳的权重和偏置值。
神经网络的图解和直观指南
[towardsdatascience.com
在本文中,我们将深入探讨训练过程,并详细讲解神经网络是如何学习的。
📣 如果你还没有阅读我之前的文章,我强烈建议你从我的机器学习基础系列开始,特别是那篇关于梯度下降的文章,因为你会发现那篇文章中的很多内容在这里也非常相关。

机器学习入门包
查看列表3 个故事!

假设我们想要创建一个神经网络,通过以下特征预测冰淇淋销售的日收入…
深度学习图解,第三部分:卷积神经网络
一份关于卷积神经网络(CNN)内部工作原理的图解和直观指南
·发表于Towards Data Science ·阅读时间 15 分钟·2024 年 5 月 11 日
--
欢迎来到我们图解深度学习之旅的第三部分。如果你错过了之前的文章,一定要回去阅读,它们为我们今天要深入探讨的内容打下了基础。

深度学习图解
查看列表5 篇故事!

为了快速回顾,我们之前通过构建一个简单模型来预测冰淇淋店的每日收入来讨论神经网络的内部工作原理。我们发现,神经网络通过利用多个神经元的结合力量,能够处理复杂问题。这使得它们能够发现数据中可能难以识别的模式。我们还了解到,神经网络主要解决两种类型的问题:回归和分类。
就像我们构建了一个收入预测模型一样,通过修改结构,我们可以创建模型来解决各种问题。卷积神经网络(CNN)是专为图像识别任务设计的模型。然而,它们依然依赖于我们迄今为止遇到的模型的相同基本原理(再加上几个额外的步骤)。今天,我们将探索卷积神经网络的内部工作原理,并准确了解它到底是怎样的……
深度学习插图版,第四部分:循环神经网络
一本关于 RNN 及 Softmax 激活函数内在工作的插图和直观指南
·发表于Towards Data Science ·17 分钟阅读·2024 年 6 月 11 日
--
欢迎来到我们插图版深度学习之旅的第四部分!今天,我们将深入探讨循环神经网络(RNN)。我们将讨论一些你可能已经熟悉的概念,如输入、输出和激活函数,但会带有一些新的变化。如果这是你第一次加入我们的学习之旅,强烈建议你先阅读之前的文章,特别是第1 部分和第2 部分,然后再继续阅读这篇文章。

深度学习,插图版
查看列表5 个故事!

循环神经网络(RNN)是专门设计用于处理基于序列的问题的独特模型,在这种问题中,当前位置依赖于前一个状态。
让我们通过一个来自MIT 课程的简单例子来解读什么是基于序列的问题。想象一下一个球体在特定时间点 tn 的位置。

如果我们被要求预测球的方向,在没有进一步信息的情况下,这就是一个猜测游戏——它可能朝任何方向移动。
深度学习图解,第五部分:长短期记忆(LSTM)
关于 LSTM 内部工作原理的图解和直观指南
·发布于 Towards Data Science ·9 分钟阅读·2024 年 6 月 21 日
--
欢迎来到我们通过深度学习的图解旅程的第五部分!

深度学习,图解
查看列表5 篇故事!

今天我们要讲解长短期记忆(LSTM)网络,它是对我们在上一篇文章中讨论的常规递归神经网络(RNN)的升级版。我们看到,RNN 被用来解决基于序列的问题,但在长距离信息保留方面存在困难,导致短期记忆问题。此时,LSTM 就能挺身而出,拯救局面。它们使用与 RNN 相同的递归结构,但进行了改进。那么,让我们看看它们是如何实现这一点的。
旁注——这是我写过的最喜欢的文章之一,所以我迫不及待想带你们一起踏上这段旅程!
首先,让我们回顾一下之前我们在 RNN 中所做的事情。我们有一个神经网络,输入为 x,一个隐藏层,包含一个带有 tanh 激活函数的神经元,以及一个带有 sigmoid 激活函数的输出神经元。因此,RNN 的第一步看起来大概是这样的:
使用深度学习预测在线平台中的参与度
训练 LSTM 模型理解在线平台的使用情况。
·发布于 Towards Data Science ·11 分钟阅读·2024 年 6 月 17 日
--

深度学习在时间序列用例中的应用示意图。由作者使用 DALL-E/GPT4 创建。
概要
在我爱丁堡大学的硕士论文中,我使用了深度学习技术来预测用户在 Zooniverse 上的参与度。Zooniverse 是一个在线公民科学平台,允许非科学家贡献于特定领域,如行星探索。Zooniverse 已经促成了超过 100 篇论文的发表。 在实际环境中,公民科学家证明了英国的农业径流正在增加污染,超过了安全阈值。
在这个背景下,参与度是指我们是否能够根据用户的历史行为预测他们未来对平台的使用情况。这可以表现为多种形式,例如:
-
预测在时间阈值内完成的任务数量
-
用户将在当前工作会话中停留的时间量
我们聚焦于任务段的定义,并试图预测用户是否会继续工作 10、20 或 30 分钟。这是通过将用户任务T分组到工作会话W中来计算的,其中:
这个定义之所以被使用,是因为它的灵活性,并且之前在 Zooniverse、StackOverflow 和 Coursera 中也有使用。
深度学习与数据科学:谁将胜出?
更重要的是,你的数据还是你的模型?
·发表于 Towards Data Science ·12 分钟阅读·2024 年 10 月 22 日
--

来源:作者提供的图片
这两位对手走进擂台,各自宣称自己占有优势。数据科学家拿出一把银色的尺子,深度学习开发者拿出一把闪闪发光的锤子——谁将构建出最好的模型?
在我之前的职位上,我曾同时担任数据科学家和深度学习算法开发者。如果你问我两者之间有什么不同,我必须说,这并不明确。
两者都涉及数据和机器学习模型,并且都使用类似的成功指标和工作原理。
那么,是什么让它们有所不同呢?
我认为这是态度问题。
我敢大胆地总结,从我的经验来看,深度学习开发者(尤其是初级开发者)往往更专注于模型,而数据科学家则恰恰相反——他们分析并操作数据,使得几乎任何模型都能奏效。
或者,我该大胆地简化一下,说:
深度学习 = 以模型为导向
深度强化学习:迈向集成化与统一的人工智能
人工智能能否为人类智能提供一扇透镜?
·发表于Towards Data Science ·15 分钟阅读·2024 年 6 月 15 日
--

图片来自Elena Popova于Unsplash
今天的大多数人工智能(AI)模型,包括卷积神经网络(CNN)和大型语言模型(LLM),都是为特定任务构建的,并且需要人工策划大量的训练数据。它们缺乏与世界互动或持续学习的能力。然而,深度强化学习(RL)作为一个杰出的例外,展示了无需监督的可扩展自学习。RL 模型拥有一个通过与环境互动来最大化奖励的智能体,其行为模仿了动物在自然环境中的活动。这种工程学与心理学的交汇使得 RL 成为一个理想的模型,用于探索大脑奖励学习与决策过程的基础。
强化学习(RL)诞生于 20 世纪 50 年代,并自那时起经历了显著的改进。值得注意的是,美国数学家理查德·贝尔曼首先通过贝尔曼方程和动态规划算法(DP)建立了理论基础。在 1980 年代,加拿大计算机科学家理查德·萨顿及其博士导师安德鲁·巴托提出了时间差分学习(TD),极大地提升了学习过程的规模。在 2010 年代,英国计算机科学家大卫·西尔弗及其在 DeepMind 的团队…
将你的数据作为产品交付,但不是作为应用
数据作为产品是一个有趣的概念,但要小心应用陷阱
·发表于Towards Data Science ·9 分钟阅读·2024 年 7 月 12 日
--

数据作为产品可以以两种形式提供 — 作者提供的图片
为了将你的数据作为产品交付,而不仅仅是一个没有进一步业务背景的表格或文件,这是数据网格框架的一个关键原则。考虑是通过应用服务(API)还是作为纯数据结构来交付数据,都是一个重要的设计决策。我之前已经在我的数据网格三部分系列的第二部分中探讨了这个具体挑战。然而,本文将讨论超越数据网格概念的问题,因为我认为它是如此基础。我将概述关键区别,并阐明为什么你应该偏向“数据作为纯结构”而非“数据作为应用”。
数据作为产品
将数据转化为产品的概念在数据工程领域并不新鲜,甚至在数据网格框架定义之前就已被使用。然而,在创建一个由数据驱动的产品和将数据本身视为产品之间,有一个重要的区别 — 这里有一个很好的解释,说明了“数据作为产品”和“数据产品”之间的微妙差异。在本文中,我专注于“数据作为产品”,即使我为了简洁也使用了“数据产品”这个术语。
Delta Lake 乐观并发控制:是锁定还是不锁定?
Delta Lake 及其相关性
·发表于 Towards Data Science ·11 分钟阅读·2024 年 7 月 10 日
--
随着数据世界朝着通过 AI、ML 及其他趋势技术生成、存储、处理和消费海量数据的方向发展,独立可扩展的存储和计算能力的需求不断增加,以应对不断增加的数据集添加(APPEND)和变更(UPSERT & MERGE)需求,这些数据集正通过 AI、ML 等技术进行训练和使用。
尽管不同云服务提供商提供的基于 Parquet 的数据湖存储在数据湖实施的初期为我们提供了极大的灵活性,但随着当前商业和技术需求的发展,这些实现正面临越来越多的挑战。尽管我们仍然喜欢使用 Parquet 这种开放的存储格式,但现在我们需要像 ACID 事务、时间旅行和模式强制等功能来满足数据湖的需求。这些正是 Delta Lake 作为一个抽象层在基于 Parquet 的数据存储之上诞生的主要驱动因素。ACID 的简要参考见下图。

图片来源:作者
Delta Lake(当前 GA 版本 3.2.0)为我们带来了许多不同的功能,其中一些在上文中已经提到。但在本文中,我想讨论 ACID 事务的一个特定领域,即一致性,以及如何决定是直接使用 Delta Lake 的这一功能,还是在该功能周围添加我们自己的定制,以适应我们的用例。在此过程中,我们还将讨论一些 Delta Lake 的内部工作原理。让我们深入了解吧!
什么是数据一致性?
数据一致性是我们使用并滥用的基本数据库或数据术语,自从我们以某种形式存储数据以来,它就一直存在。简单来说,它是数据库或数据集中的数据的准确性、完整性和正确性,保护数据消费应用程序免受在持续事务改变底层数据时部分或意外的数据状态。

图片由作者提供
如上图所示,数据集上的数据查询应当获取左侧数据的一致性状态,直到事务完成并且更改已提交,创建出右侧的下一个一致性状态。在事务进行过程中,事务所做的更改不能被看到。
Delta Lake 中的一致性
Delta Lake 实现的一致性方式与关系型数据库的实现方式非常相似;然而,Delta Lake 必须解决一些挑战:
-
数据以 parquet 格式存储,因此是不可变的,这意味着你不能修改现有的文件,但可以删除或覆盖它们。
-
存储层和计算层是解耦的,因此事务和读取之间没有协调层。
这种协调和一致性是在 Delta Lake 中通过 Delta Lake 的核心——Delta 事务日志来实现的。

图片由作者提供
这个概念简单却极其强大。仅仅数据(parquet)文件的存在不足以使内容数据成为查询输出的一部分,直到该数据文件也被事务日志追踪。如果一个文件被标记为过时或移除,它依然会存在于指定位置(直到 Delta Lake 的清理过程删除文件),但不会被视为当前表格一致性状态的一部分。

图片由作者提供
事务日志还用于执行模式验证,以确保所有数据文件中数据结构的一致性。模式验证是基于原始 parquet 数据存储的一个主要缺失功能,每个应用程序都必须构建并维护模式验证机制。
这些基于 Delta 事务日志的概念使 Delta Lake 能够通过三个步骤实现事务流:
- 读取:
-
此阶段仅适用于更新插入(UPSERT)和合并(MERGE)。当事务提交到 Delta 表时,Delta 使用事务日志来确定当前一致版本中需要修改(重写)的底层文件。当前一致版本是通过事务日志中跟踪的 Parquet 文件内容来确定的。如果数据集位置中存在某个文件,但它没有在事务日志中被跟踪(要么被另一个事务标记为过时,要么由于事务失败而没有被添加到日志中),那么该文件的内容不被视为当前一致版本的一部分。一旦对受影响文件的判断完成,Delta Lake 会读取这些文件。
-
如果事务仅为追加(APPEND)类型,这意味着不会影响现有文件,因此 Delta Lake 不需要将任何现有文件读取到内存中。需要注意的是,Delta Lake 不需要读取底层文件来定义模式。模式定义是通过事务日志进行维护的,并且日志被用来验证并强制执行 Delta 表中所有底层文件的模式定义。
-
生成事务的输出并写入文件:在此阶段,Delta Lake 首先在内存中执行事务(追加、删除或更新插入),然后将输出写入定义 Delta 表时使用的数据集位置的新的 Parquet 数据文件中。但请记住,仅仅数据文件的存在并不会使这些新文件的内容成为 Delta 表的一部分,至少在此时还不是。这个写入过程被视为 Delta 表中数据的“暂存”。
-
验证当前表的一致性状态并提交新的事务:
-
Delta Lake 现在进入生成表的新一致状态的最终阶段。为了实现这一点,会检查现有的事务日志,以确定提议的更改是否与任何自上次读取一致状态以来可能已并发提交的其他更改发生冲突。当两个并发事务试图更改同一底层文件的内容时,就会发生冲突。让我们通过一个例子来详细说明这一点。假设,两个并发事务在 HR 表上试图更新存在于同一底层文件中的两行数据。因此,两个事务都会重写同一文件的内容,并带入它们的更改,同时在事务日志中尝试废弃该文件。第一个提交的事务不会遇到任何问题,它将生成一个新的事务日志,将新写入的文件添加到日志中,并将旧文件标记为废弃。现在考虑第二个事务进行提交并经过相同的步骤。该事务也需要将相同的旧文件标记为废弃,但它发现该文件已被另一个事务标记为废弃。这种情况现在被认为是第二个事务的冲突,Delta Lake 将不允许该事务提交,从而导致第二个事务的写入失败。
-
如果没有检测到冲突,所有在写入阶段所做的“暂存”更改都会通过将新文件添加到新的 Delta 事务日志中,以新的一致状态提交 Delta 表,并且写操作会被标记为成功。
-
如果 Delta Lake 检测到冲突,写操作会失败并抛出异常。此失败会阻止将任何 Parquet 文件添加到表中,从而避免可能导致数据损坏的情况。请注意,这里的数据损坏是逻辑上的,而非物理上的。这是一个非常重要的部分,值得记住,以便理解我们即将讨论的 Delta Lake 另一个伟大特性——乐观并发控制。
Delta Lake 中的乐观并发控制是什么?
简单来说,Delta Lake 允许在 Delta 表上进行并发事务,前提是大多数并发事务之间不会发生冲突,主要因为每个事务都会写入自己的 Parquet 文件;然而,如果两个并发事务试图修改同一底层文件的内容,就可能会发生冲突。值得再次注意的是,乐观并发控制不会导致数据损坏。
让我们通过流程图进一步阐明,展示可能的并发操作组合、冲突的可能性以及 Delta Lake 如何防止数据损坏,不论事务的类型如何。

图片由作者提供
让我们稍微详细地讨论一下这些场景。上面的流程图展示了每种组合场景中的两个并发事务。但你可以将并发事务的数量扩展到任意多个,逻辑保持不变。
- 仅追加: 这是所有组合场景中最简单的一种。每个追加事务都会将新内容写入新文件,因此在验证和提交阶段永远不会发生任何冲突。

作者提供的图片
2. 追加与插入/删除组合:同样,追加操作不会与任何其他事务发生冲突。因此,即使是这个组合场景也不会导致任何冲突。

作者提供的图片
3. 多个并发的插入/删除操作没有冲突:请参考我之前用来解释冲突的示例。现在考虑两笔对 HR 表的事务,它们试图更新存在于两个不同文件中的两行数据。这意味着这两笔事务正在重写两个不同文件的新版本。因此,这两笔事务之间没有冲突,它们都会成功。这就是乐观并发控制的最大好处……允许对同一张表的并发修改,但修改的底层文件不同。同样,增量事务日志在其中起着重要作用。当第一笔事务提交时,它使文件 X 失效并将文件 Y 添加到新的事务日志中;而第二笔事务在提交时使文件 P 失效,并添加文件 Q,生成另一个事务日志。

作者提供的图片
4. 多个并发的插入/删除操作有冲突:我在解释冲突时已经讲过这个场景,这是唯一可能发生冲突导致事务失败的场景。

作者提供的图片
你需要缓解与冲突相关的失败吗?
为什么我们需要讨论如何减轻失败的影响呢?一个简单的缓解方法就是重新运行失败的事务……就这么简单!!是吧?嗯,实际上并非如此!
是否需要在重新运行失败的事务之外采取缓解措施,取决于使用增量表的应用程序类型。应用程序的类型将决定可能出现多少次失败,以及重新运行事务在成本、服务级别协议(SLA)和功能上的开销。重新运行事务的成本将决定我们是否需要构建一个自定义解决方案,以避免这些高昂的重新运行费用。不过,请记住,定制解决方案将以其他一些权衡为代价。并发控制的实施将取决于组织以及它对优先事项的决策。
假设我们有一个基于 Delta 表的客户关系管理应用。该应用的分布式多用户特性以及通过应用触发的频繁行级数据更改意味着,在这种情况下,冲突事务的比例将非常高,因此重新运行失败的事务将会非常昂贵,从执行成本、SLA 和用户体验的角度来看也是如此。在这种情况下,管理并发所需的将是一个自定义事务管理解决方案。
另一方面,纯数据湖场景中,大多数事务是附加操作,偶尔进行更新或删除,这意味着冲突的情况可能低至 1%或更少。在这种情况下,重新运行失败的事务将比构建和维护一个自定义解决方案的成本低得多。更不用说,针对 1%或更少的失败机会,惩罚(通过实现自定义缓解措施)99%成功的事务是相当不合理的。
并发管理的可能缓解方案
在 Delta 表上实现锁机制是管理并发的常见方法。在这种方式下,一个事务将“获取”表的锁,所有其他事务将等待该事务完成并释放锁。获取锁的过程可以像更新一个文件并添加 Delta 表名一样简单。一旦事务完成,它将从文件中删除表名,从而“释放”锁。
这就是我最终回到本文标题的地方。是否要锁定。由于附加操作永远不会与其他事务发生冲突,因此对于附加事务,无论是事务应用还是数据湖,都不需要实现锁机制。而如上所述,关于 UPSERT 和 MERGE 是否需要加锁的决策,将取决于事务失败的百分比规模。您还可以考虑实现附加无需加锁与UPSERT 和 MERGE 加锁的结合方式。
本文的目标是解释 Delta Lake 在并发管理方面的内部工作原理,并帮助您理解是否需要构建自定义的并发管理解决方案。我希望本文能为您做出这一决策提供一个良好的起点。
Delta Lake 的未来版本将带来更加成熟的并发管理解决方案,可能会彻底消除对自定义并发管理解决方案的需求。一旦这些功能对所有用户普遍可用,我将及时撰写一篇文章来介绍。直到那时,祝您在 Delta Lake 上进行数据工程时一切顺利!
注意:我想说明的是,我主要是在 Delta Lake 产品的官网上进行关于该 Delta Lake 特性的研究。其余的内容则基于我个人的经验以及该特性的概念验证工作。促使我写这篇文章的原因是,我观察到我们常常忽视了产品的一些开箱即用的特性,反而去开发那些已经存在并且可以重复使用的解决方案。
Delta Lake — 类型扩展
什么是类型扩展,为什么它很重要?
·发表于 Towards Data Science ·5 分钟阅读·2024 年 4 月 29 日
--

图片来源:Luca Florio 在 Unsplash
Delta Lake 正在发布一个新版本,当然,社区内每个人都在广泛期待许多功能。其中之一就是所谓的类型扩展,本文将专门解释它是什么以及为什么它有用。
唯一不变的就是变化 —— 赫拉克利特
赫拉克利特的这句话不仅适用于我们生活的世界,也适用于谈论数据,即描述数据的信息。我们正处于一个快节奏的商业环境中,在这个环境下,适应能力是跟上新要求的关键,而这些要求源自我们不断变化的世界。数据的基础结构发生变化是非常常见的现象,因此我们需要能够适应这些变化,以确保所描述的内容始终准确无误。
在 Delta Lake 中,我们使用 Delta 表来描述我们的数据,并且有几种方式可以利用当前的模式演化功能无缝地适应这些变化。我们可以:添加一个更好地描述我们实体的新列;将一个列重命名为更相关的名称;重新排列列的顺序,如果列顺序没有优化的话;删除不再需要的列;或者根据需要改变某列的类型,例如因为数字超出了原有范围(数字无法适应…)。
需求预测——以价值驱动的方法提供 5 个洞察
预测的最终目标不是准确性,而是切实的价值创造。让我们一起探索 5 个洞察,最大化需求预测的价值。
·发表于Towards Data Science ·7 分钟阅读·2024 年 1 月 25 日
--

图片由Daoudi Aissa提供,来自Unsplash
介绍
嗨!1 月是规划和产生重大影响的最佳时机。作为一名数据科学家,你经常被要求构建预测模型,可能你认为准确性始终是黄金标准。然而,事情有个转折:真正的魔力不仅仅在于准确性,而在于理解更大的画面,专注于价值和影响。让我们一起来揭示这些重要的方面吧。
1. 价值是需求预测的终极目标
关于预测,我们首先需要对一件事达成一致:我们的最终目标是创造真实的价值。 真实的价值可以表现为可触及的财务利益,比如成本降低和收入增加,或者是你从预测过程中解放出来的时间和资源。有很多路径是从需求预测开始,最终以价值创造为目标。预测准确性就像我们的可靠指南针,帮助我们朝着目标前进,但它并不是我们追求的宝藏。
作为数据科学家的任务:
-
与你的经理和团队讨论需求预测的目的。其目标是设定准确的销售目标吗?还是降低库存水平?这些预测数字背后隐藏着哪些深层次的顾虑?
-
创建一个简单的商业案例,将预测准确度指标(偏差、MAPE)转化为财务术语。如果这项任务看起来很有挑战性,可以向你在业务领域的朋友寻求帮助。这样,你将学到很多关于业务和需求预测价值的知识。
-
评估你的商业案例,找出预测过程中的最关键方面。是减少偏差(特别是过度预测)来降低库存水平吗?是评估折扣对各类产品的影响(这可能更适合用弹性模型)吗?还是更侧重于降低 MAPE,防止供应团队在危机中不断应对不可预测的采购订单?
通过清晰地将预测要素与其价值关联起来,你会更有信心知道该将精力和智力集中在哪些部分。
2. 过程中的小调整可以带来巨大变化
在预测中,你可以在两个方面增加价值:过程和模型。作为数据科学家,我们可能会过度关注模型,但有时候,过程中的一个小调整可以带来很大的影响。产生预测的过程可以决定其质量,通常是以负面的方式影响质量。同时,从预测开始的过程是通向价值创造的路径。没有良好的过程,即便是最好的模型也难以创造任何价值。
作为数据科学家的待办事项:
-
了解预测中的“最佳实践”。这可能有些棘手,因为不同行业和商业模式对“最佳实践”的定义各不相同。但一些原则是普遍有效的。例如,预测应定期自动生成;手动覆盖应尽量减少,仅在有充分理由的情况下使用;预测应触发明确的决策和行动,如准备生产、调整库存或增加促销力度。
-
查看这些“最佳实践”,看看你是否涵盖了所有要点。如果是的话,太棒了!你已经准备好迎接下一个挑战。如果没有,深入挖掘一下。问问自己是谁或什么因素在阻碍进展。有哪些最小的变化可以改善整个预测过程?我真心建议你与这一领域的关键人物喝杯咖啡。你可能会惊讶于通过影响预测过程中的一个人所能带来的变化。
3. 更清晰的过程映射有助于更好的模型构建
即便是过程已经根深蒂固,无法改变,清晰理解过程依然极为宝贵。 它能让你集中精力于决策与行动链条中最相关的关键特征。
例如,如果生产计划需要提前两周确定,那么就没有必要关注下周的预测。同样地,如果关键决策是在产品系列层面做出的,那么关注单个产品层面的准确性就会浪费时间。让(不可更改)的流程细节定义建模的边界,避免做无谓的“煮海”工作。
作为数据科学家的待办事项:
-
与一位精通业务的同事搭档,绘制出预测流程的图示。确保每个步骤都包括以下元素:正在做出的决策、决策所需的输入、决策的执行者以及随之而来的结果。记住,这不是一项轻松的任务,我们的目标不是追求完美。尽量收集信息,并将其整理成图示。
-
接下来,查看你的图示(它可能看起来有些令人不知所措,充满了各种圆圈之类的元素),试着找出整个过程中最关键的决策点。弄清楚在这些节点上做出坚实决策所需的预测类型:你是需要产品系列层面的六个月预测,还是每个特定产品包变种的每周预测?这些就是你的顶尖建模技能和数据科学知识将要解决的关键问题。
4. 提高可解释性以更好地对齐和采纳
在建模方面,可解释性应该是重中之重,因为它显著提高了预测的采纳率。由于我们的最终目标是创造价值,预测必须在商业运营中使用,以产生实际的价值。
这可能包括将预测应用于促销计划以增加收入,或者设置库存目标以减少库存水平。在日常工作中,人们通常可以选择信任或不信任预测。(你有没有经历过在会议中,预测数据被忽视,因为没有人理解这些数字?)没有信任,就无法采纳预测,因此也难以创造价值。
相反,当预测数据伴随直观的解释时,人们更可能信任并使用这些预测。因此,准确预测的价值可以在他们的日常任务和决策中实现。
作为数据科学家的待办事项:
-
思考一下预测过程,并考虑人们是否希望和需要更好地理解你的预测模型。我认为,如果预测是供人们做中长期决策使用的(例如预算、定价或产能规划),那么解释它对于建立对数据的信任并促成决策至关重要。
-
你还需要理解决策者如何直观地解读或预期预测数据。然后,调整你的解释,使其符合他们的语言。这部分比较棘手——你需要将特征重要性、Shap 值和回归系数重新表述为“1%的价格上涨的影响”等术语。不要犹豫,向你那些有商业头脑的朋友寻求帮助,并在他们身上测试你的解释,看看是否合理。
5. 模拟场景以促进决策制定
场景模拟自然是可解释性的延伸。虽然可解释模型帮助你根据预期的关键驱动因素(例如,10%的价格上涨)理解预测,场景模拟则使你能够探索并评估这些预期或计划的不同替代方案。你可以评估每个选项的风险和收益。这种方法在战略决策中极具威力。
所以,如果你的任务是创建预测来确定明年的促销预算,至关重要的是与你的利益相关者就你想要探索的关键驱动因素(如折扣水平、包装形式、时机等)以及潜在的场景达成一致。围绕这些关键驱动因素构建你的预测,确保不仅准确,而且模型的解释和场景能够“讲得通”。这可能意味着当价格下降或节假日临近时,需求会增加。但当然,你需要和关键利益相关者一起搞清楚,在你的业务中,“讲得通”究竟是什么意思。
作为数据科学家的任务:
-
与决策者沟通,弄清楚他们希望为哪些假设场景做好准备。让他们识别出关键因素并设置场景:10%的通货膨胀激增、关键原材料的供应中断、自然灾害等。让他们按重要性对这些场景和因素进行排序,这样你就可以优先处理。
-
接下来,看看你的预测模型如何表现。尽量为一些场景和因素创建模拟预测,始终从最重要的因素开始。
-
向你那些有商业头脑的朋友确认,确保你的模拟是现实的。你可能需要尝试几次,调整模型,直到一切恰到好处。就像解释一样,使用商业语言来讲述故事是这项任务的关键。不要犹豫,寻求帮助。这对你和帮助你的人来说,都是一个学习机会。
总结
好的,我知道这看起来可能有点复杂。你可能在想,“所以,除了分析数据和训练模型,我还需要深入进行流程分析,制定一个解释模型,甚至为预测构建一个模拟引擎吗?”
不用担心,这并不是完全的预期结果。看看更大的图景,会帮助你找出预测模型的关键方面,弄清楚建立它们的最佳方式,并与合适的人建立联系,从而提升预测的价值。当然,你需要在通常的数据处理和模型调整工作之外增加一些额外的任务,但我保证这将是一次有价值的经历——而且,你还会在过程中结交一些商业敏锐的朋友!
如果你想深入了解这个简单框架,我还编写了一个全面的问题清单在这篇文章中,涵盖了与需求预测相关的各个方面。祝你在预测项目中玩得开心,并最大化你对世界的影响!
像专业人士一样演示 AI 产品
一份关于如何使用 Gradio 向专家和非技术观众展示产品价值的专家指南简介。
·发布于 Towards Data Science ·15 分钟阅读·2024 年 5 月 7 日
--

图片来自 Austin Distel 在 Unsplash
我们每个人至少都经历过一次失败的演示。在数据科学领域,这个问题尤为突出,因为在演示当天可能会出现很多问题。数据科学家在向不同经验水平的观众演示时,通常需要平衡各种挑战。如何在面向广泛观众时,既展示解决方案的价值,又解释核心概念,往往是一个挑战。
本文旨在帮助克服障碍,帮助你展示你的辛勤工作!我们总是非常努力地改进模型、处理数据和配置基础设施。理应我们也要努力确保别人能看到这项工作的价值。我们将探索如何使用 Gradio 工具来分享 AI 产品。Gradio 是 Hugging Face 生态系统中的一个重要部分。Google、Amazon 和 Facebook 等公司也在使用它,因此你将与这些大公司同行!虽然我们将使用 Gradio,但许多核心概念可以在常见的替代工具中复制,比如使用 Python 的 StreamLit 或使用 R 的 Shiny。
利益相关者/客户参与在数据科学中的重要性
推销时的首个挑战是确保你以正确的层次进行推介。为了理解你的 AI 模型是如何解决问题的,客户首先需要了解它的功能,以及它所解决的问题。客户可能拥有数据科学的博士学位,或者他们可能从未听说过模型。你不需要教他们线性代数,也不应通过白皮书讲解你的解决方案。你的目标是向所有观众传达你的解决方案所带来的附加价值。
这是一个实用演示的场景。Gradio 是一个轻量级的开源工具包,用于制作实用演示[1]。有充分的文献表明,现场演示往往更具个人化,能够促进对话/产生新线索[2]。实用演示在与新用户建立信任和理解方面至关重要。信任来源于看到你使用工具,甚至更好的是通过用户自己输入进行测试。当用户可以进行工具演示时,他们知道没有“聪明汉斯效应”[3],他们看到的就是他们得到的。理解来自于用户看到你的解决方案如何运作中的“如果-那么”模式。
接下来是反面…每个人都参加过糟糕的现场演示。我们都曾经历过或让别人经历过技术问题。
但技术问题并不是我们对现场演示感到恐惧的唯一原因。还有一些常见的让人反感的因素:
-
信息倾销:向客户推销时绝不应像一场讲座。添加难以访问的演示会让客户在短时间内接收到过多信息,造成学习压力过大。
-
开发演示:演示的开发可能很慢,实际上可能会减慢开发进程。定期在“展示与讲解”中反馈是敏捷团队面临的一个特定问题。获取展示与讲解的内容可能是一个折磨。尤其是当客户习惯了现场演示时。
-
断开的依赖关系:如果你负责开发演示,可能会依赖一些保持不变的事物。如果它们发生变化,你就需要重新开始。
介绍 Gradio
现在进入技术部分。Gradio 是一个用于展示机器学习/AI 模型的框架,并与 Hugging Face 生态系统的其他部分集成。该框架可以通过 Python 或 JavaScript SDK 实现。这里我们将使用 Python。在我们构建演示之前,下面是一个命名实体识别的 Gradio 应用示例:

图片来源:Hugging Face 文档 [4]
你可以在当前工作的任何地方实现 Gradio,这是使用该框架的一个关键优势。如果你在笔记本中快速进行代码原型设计,并希望立即从利益相关者/同事那里获取反馈,你可以添加 Gradio 界面。根据我使用 Gradio 的经验,我曾在 Jupyter 和 Google Colab 笔记本中实现过。你还可以通过 HuggingFace 托管的公共链接将 Gradio 实现为独立站点。稍后我们将探讨部署选项。
Gradio 演示帮助我们解决了上述问题,并帮助我们克服了对现场演示的恐惧:
-
信息泛滥:Gradio 提供了一个简单的接口,抽象掉了许多复杂的信息。客户不会在一次性了解如何与我们的工具交互和工具本身是什么的过程中感到不堪重负。
-
开发演示:Gradio 演示和 StreamLit、Shiny 有相同的优点。演示代码简单,并且建立在你已经为产品编写的 Python 代码之上。这意味着你可以快速做出修改并获得即时反馈。你还可以从客户的角度查看演示。
-
断裂的依赖:没有任何框架能够克服完全的项目大改。Gradio 被设计为能够适应新的数据、数据类型甚至新的模型。由于允许的输入/输出的简单性和范围,Gradio 演示保持得相当稳定。不仅如此,如果你有很多工具、很多客户和很多项目,好消息是,大部分演示代码不会发生变化!你可以将文本输出换成图像输出,就可以轻松地从 LLM 转到 Stable Diffusion!
使用 Gradio 创建演示的逐步指南
本文的实践部分将带你从完全初学者成长为 Gradio 演示专家。话虽如此,有时候少即是多,如果你只需要一个非常简单的演示来突出展示你工作的影响,那就坚持基础吧!
欲了解更多关于 StreamLit 等替代品的信息,查阅我之前的文章:
[## 使用 StreamLit 和 PyDeck 构建轻量级地理空间数据查看器
使用两种前沿库构建和部署 Python 的交互式 web 应用,用于地理空间数据可视化。
基础知识
让我们从一个 Hello World 风格的示例开始,这样我们可以更多地了解构成 Gradio 演示的基本要素。我们有三个基本组件:
-
输入变量:我们提供任何数量的输入变量,用户可以使用切换开关、滑块或其他输入小部件在我们的演示中输入。
-
功能:演示的作者编写一个执行重活的函数。这是演示之间代码变化最多的地方。该函数会将输入变量转化为用户看到的输出。这是我们可以调用模型、转换数据或执行其他必要操作的地方。
-
接口:接口将输入变量、输入小部件、功能和输出小部件结合到一个演示中。
那么让我们看看这在代码中的表现:
这将给我们以下演示。注意,我们定义的输入和输出都是文本类型,如上所述:

图片来源:作者提供
现在我们了解了 Gradio 的基本组件,让我们来深入一点,探讨一些技术细节。
为了演示如何将 Gradio 应用到机器学习问题中,我们将使用最简单的算法——线性回归。作为第一个示例,我们将使用加利福尼亚房价数据集来构建线性回归模型。首先,我们更新基本代码,使得该函数能够基于线性模型进行预测:
然后我们更新界面,使得输入和输出与我们的需求相匹配。请注意,我们在这里也使用了Number类型作为输入:
然后点击运行,看看效果如何:

图片来源:作者提供
为什么停下来呢!我们可以在 Gradio 中使用Blocks,使我们的演示更加复杂、深入且引人入胜。
控制界面
Blocks 就如其名,是 Gradio 应用的构建块。到目前为止,我们只使用了更高级的Interface封装。在下面的示例中,我们将使用 Blocks,它具有稍微不同的编码模式。让我们更新最后一个示例,使用Blocks,以便更好地理解它们的工作原理:
与之前有输入、函数和界面不同,现在我们已经将所有内容简化到 Gradio 的最基本形式。我们不再设置界面并要求它为我们添加数字输入!现在,我们提供每个单独的数字输入和一个数字输出。这样构建使我们能够对显示进行更多的控制。
通过这种新的控制方式,我们甚至可以添加新的标签。标签使我们能够控制用户的操作流程和体验。我们可以先解释一个概念,比如我们的预测是如何分布的。然后在下一个标签中,我们可以为用户提供一个全新的区域,让他们自己向模型提问,获取预测结果。我们还可以利用标签来应对技术难题。第一个标签为用户提供了很多关于模型性能的信息。这些都是通过之前实现的功能完成的。如果模型代码当天无法运行,我们依然有一些有价值的信息可以分享。虽然不完美,但总比空白屏幕强!
注意:这并不意味着我们可以通过标签隐藏技术难题!我们只是可以利用标签为观众提供一些线索,万一出现问题时可以参考。如果技术问题解决后,我们再重新分享演示。

图片来源:作者提供
提高复杂度展示了 Gradio 在展示各种信息时的强大功能!到目前为止,我们一直保持着一个相对简单的模型。现在让我们探索一下如何在更复杂的场景中使用 Gradio。
Gradio 用于 AI 模型和图像
接下来的应用将展示如何使用 Gradio 演示生成式 AI。我们再次将使用 Blocks 来构建界面。这次演示将包含两个核心组件:
-
一个介绍标签,解释模型的限制以及适用范围内外的使用。
-
一个灵感标签,展示之前生成的一些图像。
-
一个互动标签,用户可以提交提示生成图像。
在这篇博客中,我们仅演示一个预训练的模型。要了解更多关于 Stable Diffusion 模型的信息,包括关键概念和微调,查看我之前的博客:
[## Stable Diffusion:AI 如何将文本转换为图像
记录我学习并亲自操作 Stable Diffusion 的过程,供其他学习者参考。
由于这是一个演示,我们将从最难的组件开始。这确保我们有足够的时间来完成最困难的工作。互动标签可能是最具挑战性的部分,因此我们将从这里开始。为了让我们知道目标是什么,我们的演示页面最终看起来会像这样:

图片来源:作者提供的图片。Stable Diffusion 图像是由 AI 生成的。
为了实现这一点,演示代码将结合上面两个示例。我们将使用块、函数、输入和按钮。按钮使我们能够以类似之前的方式工作,其中我们有输入、输出和函数。我们将按钮作为事件监听器。事件监听器有助于控制我们的逻辑流程。
假设我们正在尝试启动我们的演示。在运行时(即演示开始时),我们没有输入。由于没有输入,演示使用的模型没有提示。没有提示,模型无法生成图像。这将导致错误。为了克服错误,我们使用事件监听器。按钮监听一个事件,在这种情况下是按钮点击事件。一旦它“听到”事件,或者被点击,它会触发一个动作。在这个例子中,动作将是将一个完成的提示提交给模型。
让我们回顾一些使用按钮的新代码,并将其与之前的界面示例进行对比:
按钮代码看起来像界面代码,但有一些大的概念变化:
-
按钮代码使用了块。这是因为虽然我们像界面一样使用按钮,但我们仍然需要某种方式来确定演示的外观。
-
输入和输出小部件作为对象使用,而不是字符串。如果你回到第一个示例,我们的输入是“text”类型为string,但这里它是prompt类型为gr.Text()。
-
我们使用 button.click() 而不是 Interface.launch()。这是因为之前界面是我们的整个演示。这一次,事件是按钮点击。
这是演示的最终效果:


图片来源:作者提供。Stable Diffusion 图像为 AI 生成。
你能看到事件监听器的重要性吗!它为我们节省了很多工作,确保事情按正确的顺序发生。Gradio 的美妙之处在于,我们还能够得到一些反馈,了解我们需要等待多久才能看到图片。左侧的进度条和时间信息对于用户反馈和互动非常有帮助。
演示的下一部分是分享我们之前生成的图片。这将为客户提供灵感。他们将能够看到工具能做什么。为此,我们将实现另一个新的输出小部件,图库。图库显示了我们刚刚生成的图片:
一个重要的提醒:我们实际上是利用了之前的generate_images()函数。正如我们之前所说,所有这些轻量级的应用程序库使我们能够简单地在现有代码的基础上构建。
演示现在看起来是这样的,用户可以在两个核心功能之间切换:

图片来源:作者提供。Stable Diffusion 图像为 AI 生成。
最后,我们将通过一个着陆页将一切联系在一起。在现场或录制的演示中,着陆页将为我们提供讨论的内容。它很有用,但不是必须的。我们包括着陆页的主要原因是为了那些没有我们在场的情况下测试工具的用户。这有助于提高工具的可访问性,并增加用户的信任和理解。如果你每次都需要在客户使用你的产品时在场,那就无法产生价值。
这次我们不会使用任何新的东西。相反,我们将展示Markdown()组件的强大功能。你可能已经注意到我们已经使用了一些 Markdown。对于那些熟悉的人来说,Markdown 可以帮助以文本形式表达各种信息。下面的代码包含了一些想法,但在你的演示中,发挥创意,看看你能将 Markdown 在 Gradio 中应用到多远:

图片来源:作者提供
完成的演示如下。欢迎在评论中告诉我你的想法!



图片来源:作者提供。Stable Diffusion 图像为 AI 生成。
与客户分享
无论你是经验丰富的专业人士,还是刚开始的初学者,分享演示都可能让人感到畏惧。构建演示和推介是两项非常不同的技能。到目前为止,本文已经帮助你构建了演示。网络上有很多资源可以帮助推介[5]。现在让我们专注于这两者的交集——如何有效地分享你所构建的演示。
考虑到你偏好的风格,现场演示绝对能让你的展示更加生动有趣(笑话自带!)。对于技术观众,我们可以直接在笔记本中启动演示。这对于那些想深入了解代码的人很有用。我推荐将这种方式分享给新同事、高级开发人员以及任何希望合作或扩展你工作的人员。如果你使用的是 Gradio 以外的工具,我仍然推荐以高层次的方式与这个观众分享代码。这有助于吸引新开发人员加入,或向高级开发人员解释你最新的变化。
另一种方法是只使用“前端”进行现场演示。这可以通过运行演示时提供的链接来完成。通过这种方式分享时,客户无需深入代码就能看到你的演示。这就是迄今为止截取屏幕截图的方式。我推荐这种方式用于现场的非技术观众、新客户以及敏捷反馈/展示会议。如果你在 Gradio 中构建了演示,可以通过提供的链接访问这个功能。
我们可以用来分享的链接还允许我们将演示分享给其他人。通过在启动演示时设置分享参数:
demo.launch(debug=True, share=True)
这种方式对于无法参加直播会议的用户非常有效,或者那些希望有更多时间进行产品实验的用户也很适用。此链接可用 72 小时。此时需要小心,因为演示是通过你的机器公开托管的。建议在以这种方式分享之前,考虑系统的安全性。我们可以通过设置密码保护来使其更加安全:
demo.launch(debug=True, auth=('trusted_user', 'trusted123'))
这会为演示添加一个密码弹出窗口。
你可以进一步通过使用授权技术来增强安全性。例如,使用 Hugging Face 或 Google 的 OAuth 身份提供者 [6]。还可以通过设置文件和主机机器上文件路径的限制来进一步加强保护 [6]。
这种方式并不能完全解决分享时的安全问题。如果你想要私密分享,使用云提供商的容器化可能是一个更好的选择 [7]。
如果你想让更多人参与,你可能会想将演示公开分享给在线观众。这对寻找潜在客户、建立口碑或获取最新 AI 项目的反馈非常有帮助。我多年来一直在 Medium、Kaggle 和 GitHub 上公开分享作品获取反馈。通过这些反馈,我的作品确实有了显著改善。
如果你使用的是 Gradio,演示可以通过 Hugging Face 公共分享。Hugging Face 提供的Spaces用于分享 Gradio 应用。Spaces 提供了一个免费的平台来分享你的演示。GPU 实例有相关费用(每小时 $0.40 至 $5)。有关分享到 Spaces 的文档请参见 [6]。文档解释了你可以如何:
-
分享到 Spaces
-
使用 GitHub Actions 实现空间的 CI/CD
-
将 Gradio 演示嵌入你自己的网站中!
Spaces 对于覆盖更广泛的受众非常有帮助,而不需要担心资源问题。它也是一个为潜在客户提供的永久链接。这使得提供尽可能多的指导显得尤为重要。再次提醒,这是一个你不拥有的计算平台上的公共共享平台。如果有更高的安全需求,容器化和专用托管可能更为合适。一个特别好的例子是这个 Minecraft 皮肤生成器[8]。

图片来源:Nick088,Hugging Face [Stable Diffusion Finetuned Minecraft Skin Generator — a Hugging Face Space by Nick088]
额外的考虑因素
当前整个 AI 社区中最受关注的话题当然是大型语言模型(LLMs)。Gradio 提供了许多专为 LLM 设计的组件。这包括使用代理工作流和模型即服务[9]。
还值得一提的是自定义组件。自定义组件是由其他数据科学家和开发者开发的。它们是建立在 Gradio 框架之上的扩展。以下是一些很棒的例子:
-
图像注释组件:gradio_image_annotation V0.0.6 — a Hugging Face Space by edgargg
-
使用上传的 PDF 进行问答:gradio_pdf V0.0.6 — a Hugging Face Space by awacke1
扩展并非 Gradio 独有。如果你选择使用 StreamLit 或 Shiny 来构建演示,这些框架也有很棒的扩展:
-
StreamLit 附加组件,StreamLit UI 组件的扩展:
extras.streamlit.app/ -
超棒的 R Shiny,Shiny 的附加反应式/UI/主题组件:
github.com/nanxstats/awesome-shiny-extensions
最后谈谈在敏捷背景下分享工作。当通过定期的展示和反馈会议分享时,轻量级的演示可以改变游戏规则。从 MVP 到最终产品的轻松层叠能力,确实能帮助客户看到他们与产品的旅程。
总结来说,Gradio 是一个轻量级的开源工具,用于共享 AI 产品。根据你的需求,可能需要考虑一些重要的安全步骤。希望你在准备演示时感觉更有准备!
如果你喜欢这篇文章,请考虑关注我、分享这篇文章或留下评论。我写了许多关于数据科学领域的内容,欢迎查看我个人主页上的更多内容。
参考文献
[1] Gradio 文档。www.gradio.app/
[2] 用户引导产品演示。userpilot.com/blog/product-demos/
[3] 聪明汉斯维基百科。en.wikipedia.org/wiki/Clever_Hans
[4] Gradio 命名实体识别应用。命名实体识别 (gradio.app)
[5] 《哈佛商业评论》。什么是一个优秀的推介。What Makes a Great Pitch (hbr.org)
[6] Gradio 部署到 Spaces。分享你的应用 (gradio.app)。
[7] 将 Gradio 部署到 Docker。使用 Docker 部署 Gradio
[8] 神奇的 Minecraft 皮肤生成器示例。Stable Diffusion 微调版 Minecraft 皮肤生成器——Nick088 在 Hugging Face 上的空间
[9] Gradio for LLM。Gradio 和 LLM 代理
普及化 LLMs:4 位量化以实现最佳 LLM 推理
深入探讨使用 GGUF 和 llama.cpp 进行模型量化,并使用 LlamaIndex 进行模型评估
·发表于 Towards Data Science ·阅读时间 15 分钟·2024 年 1 月 15 日
--

图像由作者使用 DALL-E 3 生成
对模型进行量化是一种将模型中使用的数字精度从更高精度(如 32 位浮点数)转换为更低精度(如 4 位整数)的技术。量化是在效率和准确性之间的平衡,因为它可能会以模型准确性的轻微下降为代价,因为精度的降低可能会影响模型表示数据中微妙差异的能力。
这是我从各种来源学习 LLMs 时的假设。
在本文中,我们将探讨将Mistral-7B-Instruct-v0.2模型量化为 5 位和 4 位模型的详细步骤。然后,我们将把量化后的模型上传到 Hugging Face 平台。最后,我们将加载这些量化后的模型,并对它们及基础模型进行评估,以了解量化对 RAG 流水线性能的影响。
它符合我最初的假设吗?继续阅读。
为什么我们要对模型进行量化?
量化模型的好处包括以下几点:
- 减少内存使用:更低的…
演示销售中的优先排序效果
机器学习在联系人优先排序中的力量
·发表于 Towards Data Science ·6 分钟阅读·2024 年 6 月 28 日
--

这张图片是使用 DALL-E 创建的
让我们想象两个处于 B2B/B2C 背景下的公司,它们是直接竞争对手,且规模相同。两家公司都有自己的销售团队,每天重复处理入站潜在客户的销售流程,但它们采用的销售策略截然不同。
他们的流程如下:
-
在A 公司,销售团队早上开始时会拨打最新的潜在客户电话,确信这些新联系人是他们最好的选择。
-
在B 公司,情况截然不同。这个团队从数据驱动的洞察开始他们的一天。他们投资了一个预测性潜在客户评分优先排序系统,分析多种因素——从用户档案到参与历史。
你怎么看?你认为哪种方法在优先排序潜在客户时更有效?
不再使用霰丨弹丨枪策略
在多年的优先排序算法实施工作中,我已经对来自各个行业的数十个不同系统进行了比较。
在今天的销售环境中,企业花费大量资源在 SDR(销售开发代表)或销售代理商身上,进行初步的接触和潜在客户资格认证。但他们通常缺乏精准的方法来识别最有潜力的客户,而是将所有潜在客户一视同仁,毫无优先排序。
大多数代理商根据自己的人类标准来优先排序潜在客户,这往往受到个人和未经验证的观点的偏见。相反,在少数实施优先排序方法的代理商中,主流策略是基于‘新联系’标准,这仍然非常初级。
在人工智能时代,这一事实让我感到震惊,但不幸的是,它仍在发生。
作为开发不同领域预测潜在客户评分系统的主要数据科学家,我可以明确表示,采用这些技术的公司通过减少在低质量潜在客户上的工作来降低运营成本,从而显著提高了投资回报率(ROI)。
此外,通过提高潜在客户管理的效率和效果,他们变得更加精确地判断潜在客户的决策时间框架,并推动更高的收入增长。
我观察到,正确采用预测潜在客户评分的公司转化率提高了超过 12%,在某些情况下甚至达到了 300%以上。
针对这一重要推动因素,本文讨论了利用预测潜在客户评分模型作为优先级系统的好处,与传统策略相比,以及使用这些方法最大化转化率的最有效行动。
一如既往,我将用实际数据支持我的论点。
机器学习的力量
以下图表展示了只使用“最新鲜”策略与“预测潜在客户评分”优先级排序的公司之间的转化增益比较。
该分析基于一个真实的商业案例,涉及一家 B2C 公司的 67,000 个联系人(其中 1500 人转化为客户)。
收益通过探索在特定比例的已处理潜在客户中达成的转化率,按照优先级标准进行排序来体现。
对于上述揭示的方法,其表现如下:

收益或表现:预测潜在客户评分 vs 最新鲜策略
黑线代表随机优先级排序,在处理 50%的潜在客户时提供 50%的转化率。
“最新鲜”策略提供的表现略优于随机处理,提供了 50%的潜在客户转化率,处理了 50%的潜在客户。
相比之下,机器学习方法在仅处理 50%的潜在客户的情况下实现了 92%的转化率。
尽管“最新鲜”方法提供的表现与随机处理相似,但预测潜在客户评分展现了更好的优先级排序效果。
请注意,预测潜在客户评分通过处理 30%的潜在客户就达到了 81%的转化率,展现了令人印象深刻的帕累托效应。
成功的机器学习
到达这一点时,已经证明了公司 B将提供比公司 A更好的结果***。
公司 A认为他们最近有兴趣的潜在客户是最优质的潜在客户。他们相信最近的兴趣表明他们正在考虑购买。然而,事实可能并非如此。
一个最近的潜在客户可能只是好奇,但不一定准备购买。
一些潜在客户可能仅仅出于随意的兴趣填写表单或注册,而没有真正的购买意图。相反,其他那些可能近期没有联系过的潜在客户,可能对产品或服务有更强烈的持续需求。
公司 B考虑了其他相关因素,如用户档案、过去的互动、购买信号和行为指标,这些都整合在一个工具中。
他们的预测性潜在客户评分也考虑了潜在客户的最近性,但并非仅仅依赖这一因素,而是将其视为一种附加信号,具体是否相关或更为重要,取决于潜在客户的档案。
这种基于数据驱动的方法使他们能够优先考虑那些具有最高转化潜力的潜在客户,而不仅仅是最近的那些。
通过使用预测性潜在客户评分,他们能够有效识别并专注于那些更可能转化的潜在客户,从而最大化他们的销售效率和整体转化率。
总结来说,尽管公司 A 认为最近性是与兴趣等同的唯一特征,公司的 B 基于数据驱动的方法提供了一个更精细和有效的潜在客户优先排序和转化策略。
附加部分
尽管“最新鲜”的策略并不是优先排序的最佳方法,但实际上一些潜在客户可能对最近性比较敏感。大多数时候,我观察到以下情形:
转化可能性更高的潜在客户也是对最近性最敏感的
这非常有意义,因为识别出高兴趣水平的潜在客户,即具有高转化概率的客户,使他们离达成购买更近。可以理解的是,他们对销售措施的反应更快、更积极,快速行动的影响尤其显著。
对于这种类型的潜在客户,最初几小时的联系对于转化用户至关重要。快速回应能够在兴趣减弱之前利用这一高水平的兴趣,同时防止竞争对手的介入。
让我们在我们的商业案例中看看。
以下图表展示了三种不同评分或级别的转化率:前 30%(A 评分或最佳潜在客户),中间 40%(B 评分或普通潜在客户),底部 30%(C 评分或最差潜在客户)。这些评分是通过简单地对预测性潜在客户评分工具的概率输出进行分类得到的。
结果按首次行为时间(电话联系)进行划分。它区分了那些在表现出兴趣后的前三小时内尝试联系的潜在客户,以及那些在潜在客户创建时间后三到 48 小时内才尝试联系的潜在客户。

转化率与潜在客户评级,按首次销售行为时间进行划分
如图所示,我们的说法得到了验证:
如果在前三小时内未与最高评分的潜在客户取得联系,其转化率将下降 3 个百分点(-37%),而其他潜在客户则没有显著下降。
这个结论强调了优先排序高分联系人(top-scoring contacts)的重要性。这些高价值潜在客户不仅具有更高的转化率,并且占据了销售的大部分份额,而且他们对快速联系反应最为敏感,进一步证明了优先排序的必要性。
最后说明
上述内容讨论了通过机器学习模型实现优先级排序的收益,并展示了近期效应对最佳潜在客户的影响。
除了近期效应之外,还有一些与优先级相关的其他方面需要认真考虑,以促进销售,例如坚持性、重新参与和任务分配。
如果你喜欢这篇文章并对类似话题感兴趣,关注我并保持关注,获取更多更新内容。更多内容正在发布中!
解密 Azure 存储账户网络访问
服务端点和私有端点实操:包括 Azure 主干网、存储账户防火墙、DNS、VNET 和 NSG
·发表于 Towards Data Science ·阅读时间:7 分钟·2024 年 10 月 30 日
--

连接的网络 — 图片来自 Nastya Dulhiier on Unsplash
1. 引言
存储账户在建立企业数据湖的勋章架构中发挥着至关重要的作用。它们充当中心化的存储库,促进数据生产者与消费者之间的无缝数据交换。这种设置使得消费者能够执行数据科学任务并构建机器学习(ML)模型。此外,消费者还可以利用数据进行检索增强生成(RAG),通过类似 ChatGPT 的大型语言模型(LLM)与公司数据进行交互。
高度敏感的数据通常存储在存储账户中。在数据科学家和机器学习管道能够访问数据之前,必须采取深度防御措施。为了实现深度防御,必须采取多种措施,例如:1) 高级威胁防护以检测恶意软件,2) 使用 Microsoft Entra 进行身份验证,3) 通过授权进行精细化访问控制,4) 审计跟踪以监控访问,5) 数据外泄防护,6) 加密,最后但同样重要的 7) 使用服务端点或私有端点进行 网络访问控制。
本文重点讨论存储账户的网络访问控制。在下一章中,将解释(解密)存储账户网络访问的不同概念。之后,将进行服务端点与私有端点的实践比较。最后,得出结论。
2. 讨论网络访问的可能性
一个典型场景是虚拟机需要访问存储帐户的网络。这台虚拟机通常充当 Spark 集群来分析来自存储帐户的数据。下图提供了可用网络访问控制的概览。

2.1 虚拟机和存储帐户之间的网络概述 — 作者提供的图片
图中的组件可以描述如下:
Azure 全球网络 — 主干网: 流量始终通过 Azure 主干网在两个区域之间传输(除非客户强制不这样做),详见 Microsoft 全球网络 — Azure | Microsoft Learn。这与存储帐户使用的防火墙规则以及是否使用服务端点或私有端点无关。
Azure 存储防火墙: 防火墙规则可以限制或禁用公共访问。常见规则包括将 VNET/子网、公共 IP 地址、系统分配的托管标识作为资源实例进行白名单管理,或允许受信任的服务。当 VNET/子网被列入白名单时,Azure 存储帐户会识别流量的来源及其私有 IP 地址。然而,存储帐户本身并未集成到 VNET/子网中 — 需要私有端点来实现这一目的。
公共 DNS 存储帐户: 存储帐户将始终具有一个可以通过网络工具访问的公共 DNS,详见 Azure 存储帐户 — 公共访问禁用 — 但仍然有一定的连接性 — Microsoft Q&A。也就是说,即使在存储帐户防火墙中禁用公共访问,公共 DNS 仍然存在。
虚拟网络 (VNET): 部署虚拟机的网络。虽然存储帐户从未在 VNET 内部部署,但 VNET 可以在 Azure 存储防火墙中列入白名单。或者,VNET 可以创建私有端点以实现安全的私有连接。
服务端点: 当在存储帐户防火墙中将 VNET/子网列入白名单时,必须为该 VNET/子网启用服务端点。当 VNET 和存储帐户位于同一地区时,服务端点应设置为 Microsoft.Storage;当 VNET 和存储位于不同地区时,应设置为 Microsoft.Storage.Global。请注意,服务端点也作为一个总括性术语,涵盖了在 Azure 存储防火墙中列入 VNET/子网白名单以及在 VNET/子网中启用服务端点这两个操作。
私有端点: 将存储帐户的网络接口卡 (NIC) 集成到虚拟机所在的 VNET 中。这一集成为存储帐户分配了一个私有 IP 地址,使其成为 VNET 的一部分。
私有 DNS 存储账户: 在 VNET 内,可以创建一个私有 DNS 区域,其中存储账户的 DNS 解析到私有端点。这是为了确保虚拟机仍能连接到存储账户的 URL,并且存储账户的 URL 解析为私有 IP 地址,而不是公共地址。
网络安全组 (NSG): 部署一个 NSG 以限制虚拟机所在 VNET 的入站和出站访问。这可以防止数据外泄。然而,NSG 仅与 IP 地址或标签一起工作,而不与 URL 一起工作。要实现更高级的数据外泄保护,请使用 Azure 防火墙。为简便起见,本文省略了这一部分,改用 NSG 来阻止出站流量。
在下一章节中,将讨论服务端点和私有端点。
3. 实践操作:服务端点和私有端点
本章首先探索了网络访问不受限制的场景。然后,详细讨论了服务端点和私有端点,并提供了实际示例。
3.1 不限制网络访问 — 启用公共访问
假设以下场景,其中创建了一个虚拟机和一个存储账户。存储账户的防火墙启用了公共访问,见下图。

3.1.1 创建具有公共访问权限的虚拟机和存储账户
使用此配置,虚拟机可以通过网络访问存储账户。由于虚拟机也部署在 Azure 中,流量将通过 Azure 骨干网络并被接受,见下图。

3.1.2 流量未被阻止 — 启用公共网络访问
企业通常会建立防火墙规则来限制网络访问。这涉及禁用公共访问,或仅允许特定网络并将其列入白名单。下图展示了禁用公共访问并通过防火墙阻止流量的情况。

3.1.3 流量被阻止 — 在存储账户防火墙中阻止流量
在下一段中,使用服务端点和选定的网络防火墙规则再次授予网络访问存储账户的权限。
3.2 通过服务端点限制网络访问
要启用虚拟机 VNET 访问存储账户,请在 VNET 上激活服务端点。对于同一区域,使用 Microsoft.Storage;对于跨区域,使用 Microsoft.Storage.Global。接下来,在存储账户防火墙中将 VNET/子网列入白名单。流量再次被允许,见下图。

3.2.1 流量未被阻止 — 启用服务端点并添加到存储账户防火墙
现在流量被接受。当 VNET/子网从 Azure 存储账户防火墙中移除或禁用公共访问时,流量将再次被阻止。
如果在虚拟机的 VNET 中使用 NSG 阻止公共出站 IP,则流量也会被再次阻止。这是因为使用了存储帐户的公共 DNS,另请参见下图。

3.2.2 流量被阻止 — 虚拟机的 NSG 阻止公共出站流量
在这种情况下,应使用私有端点来确保流量不离开 VNET。此内容将在下一章节讨论。
3.3 通过私有端点限制访问
要为虚拟机恢复对存储帐户的网络访问,请使用私有端点。此操作将在虚拟机的 VNET 内为存储帐户创建一个网络接口卡(NIC),确保流量保持在 VNET 内。下图提供了进一步的说明。

3.3.1 流量未被阻止 — 已为存储帐户创建私有端点,禁用了公共访问
再次强调,可以使用 NSG 阻止所有流量,见下图。

3.3.2 流量被阻止 — 虚拟机的 NSG 阻止所有出站流量
然而,这一点与直觉相悖,因为首先在 VNET 中创建了私有端点,然后流量被同一 VNET 中的 NSG 阻止。
3. 结论
企业始终需要设置网络规则,以限制对其存储帐户的网络访问。在这篇博客中,既考虑了服务端点,也考虑了私有端点来限制访问。
对于服务端点和私有端点,以下内容都适用:
-
流量始终通过 Azure 主干网络在两个区域之间传输(除非客户强制不这样做),另请参见 Microsoft 全球网络 — Azure | Microsoft Learn。无论存储帐户中使用的是什么防火墙规则,这一点始终成立。此外,是否使用服务端点或私有端点也不受影响。
-
存储帐户始终具有一个可以通过网络工具访问的公共 DNS,另请参见 Azure 存储帐户 — 公共访问禁用 — 但仍有一定程度的连接性 — Microsoft Q&A。也就是说,即使在存储帐户防火墙中禁用了公共访问,DNS 仍然会保留。
对于服务端点,以下条件适用:
-
需要在 VNET/子网上启用服务端点,并在 Azure 存储帐户防火墙中将 VNET/子网列入白名单。
-
需要流量离开连接到存储帐户的虚拟机的 VNET。见上文,流量将留在 Azure 主干网络上。
对于私有端点,以下条件适用:
-
可以在 Azure 存储防火墙中禁用公共访问。见上文,存储帐户的公共 DNS 条目将保留。
-
流量不会离开虚拟机所在的 VNET。
是否使用服务端点或私有端点需要考虑许多其他因素(成本、迁移工作量,因为服务端点比私有端点出现得早,使用私有端点时的网络复杂性,新版 Azure 服务对服务端点的支持有限,存储帐户中私有端点的硬性限制为 200)。
然而,如果要求是“必须满足”的条件:1)流量绝不能离开虚拟机的 VNET/子网,或 2)不允许在 Azure 存储防火墙中创建防火墙规则且必须进行严格限制,则服务端点不可行。
在其他情况下,可以考虑两种解决方案,最适合的方案应根据每个场景的具体需求来确定。
破解 CDC:用简单的语言理解变更数据捕获
你必备的变更数据捕获指南
·发布于Towards Data Science ·阅读时间:4 分钟·2024 年 3 月 18 日
--
在我的工作经验中(大数据分析和数据工程领域),项目通常各不相同,但它们总是遵循一个已确立的架构:目标是创建一个数据平台,从不同的数据源收集数据,执行一系列处理,然后将汇总后的数据提供给那些需要使用它的人。

图片来自ian dooley在Unsplash
上述描述的架构通常总结为数据湖(Data Lake)/数据湖屋(Data Lakehouse)和 ETL(提取-转换-加载)流程。提取数据的不同方式可以分为两类:
-
批处理:整个数据集在一次操作中从源系统提取
-
流式处理:数据提取是持续进行的,实时监控源系统的任何变化。数据一旦发生修改就会被提取。
每年都会出现新技术、新架构和新方法,但有一种方法持续被广泛使用,那就是变更数据捕获。
什么是变更数据捕获(CDC)?🤓
变更数据捕获是一种设计模式,使你能够捕获数据源中发生的变化。它提供了一个持续的数据更新流,可以……
通过示例揭秘置信区间
在数据中驾驭不确定性以提取全局统计洞察
·发表于 Towards Data Science ·10 分钟阅读·2024 年 1 月 13 日
--

引言
置信区间是统计学中最重要的概念之一。在数据科学中,我们常常需要为给定的数据变量计算统计量。我们常遇到的问题是缺乏完整的数据分布。因此,统计量仅针对数据的一个子集进行计算。显而易见的缺点是,基于这个数据子集计算出的统计量可能与所有可能值的真实值相差很大。

仅根据数据子集计算平均值的示例。当只有部分数据时,计算出的统计量可能与真实的总体值有显著差异。
完全消除这个问题是不可能的,因为我们总是会与真实值有所偏差。然而,引入置信区间并结合多个算法,可以估算出在某一置信水平下,期望统计量所属的值范围。
定义
在理解了置信区间的最终动机之后,让我们非正式地了解它们的含义:
对于给定的数字…
高效总结海量文档的基本指南,第一部分
文档摘要在生成式人工智能(GenAI)应用中非常重要,但如果文档太大了怎么办!?继续阅读,了解我是如何解决这个问题的。
·发表于Towards Data Science ·8 分钟阅读·2024 年 9 月 14 日
--

“总结大量文本”——使用 GPT-4o 生成的图像
如今,文档摘要已成为使用现代生成式人工智能(GenAI)技术解决的最常见问题之一(如果不是最常见的话)。检索增强生成(RAG)是一个常见且有效的解决架构,用于解决这一问题(如果你想深入了解 RAG 是什么,可以查看这篇博客!)。但是,如果文档本身太大,以至于无法在一次 API 请求中全部发送该怎么办?或者,如果它生成了太多的片段,导致著名的“迷失于中间”上下文问题怎么办?在本文中,我将讨论我们在面对此类问题时所遇到的挑战,并逐步介绍我使用 Greg Kamradt 在其GitHub 仓库提供的指导所应用的解决方案。
一些“上下文”
RAG 是一种广泛讨论并且广泛实施的解决方案,用于利用 GenAI 技术优化文档总结。然而,像任何新技术或解决方案一样,它也容易面临边缘案例的挑战,尤其是在今天的企业环境中。两个主要问题是上下文长度与每次提示的成本以及前面提到的“迷失在中间”的上下文问题。让我们深入探讨一下这些挑战。
注意:我将使用 LangChain、Scikit-Learn、Numpy 和 Matplotlib 库来进行 Python 练习,以便快速迭代。
上下文窗口和成本限制
如今,借助 GenAI 启用的自动化工作流,分析大型文档已经成为行业的期望/要求。人们希望通过简单地提示 LLM,就能快速从医疗报告或财务审计中找到相关信息。但有一个警告,企业文档与我们在学术领域处理的文档或数据集不同,文档的大小通常要大得多,相关信息可能出现在文档的任何地方。因此,像数据清理/过滤这样的方式通常并不是一个可行的选择,因为这些文档的领域知识并不总是可用的。
此外,即使是像 OpenAI 的 GPT-4o 这样最新的 大型语言模型(LLM),上下文窗口为 128K 令牌,也不能一次性处理这些文档,即使能,响应的质量也无法达到标准,尤其是考虑到它所产生的成本。为了展示这一点,我们来看一个真实世界的例子:尝试总结 GitLab 的员工手册,可以从这里下载。该文档在 MIT 许可证下免费提供,可以在他们的 GitHub 仓库找到。
1 我们首先加载文档,并初始化我们的 LLM,为了使这个练习更有相关性,我将使用 GPT-4o。
from langchain_community.document_loaders import PyPDFLoader
# Load PDFs
pdf_paths = ["/content/gitlab_handbook.pdf"]
documents = []
for path in pdf_paths:
loader = PyPDFLoader(path)
documents.extend(loader.load())
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4o")
2 然后,我们可以将文档分割成更小的块(这是为了 嵌入,我会在后续步骤中解释为什么这么做)。
from langchain.text_splitter import RecursiveCharacterTextSplitter
# Initialize the text splitter
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=0)
# Split documents into chunks
splits = text_splitter.split_documents(documents)
3 现在,让我们计算这个文档包含多少个令牌,为此我们将遍历每个文档块并计算出文档总共包含的令牌数。
total_tokens = 0
for chunk in splits:
text = chunk.page_content # Assuming `page_content` is where the text is stored
num_tokens = llm.get_num_tokens(text) # Get the token count for each chunk
total_tokens += num_tokens
print(f"Total number of tokens in the book: {total_tokens}")
# Total number of tokens in the book: 254006
正如我们所看到的,令牌数量为 254,006,而 GPT-4o 的上下文窗口限制为 128,000。这个文档无法通过 LLM 的 API 一次性发送出去。除此之外,考虑到该模型的定价为每 1K 输入令牌 $0.00500,一次请求 OpenAI 处理这个文档将花费 $1.27!直到你把它放在企业范畴中,面对多个用户以及大量类似的文档日常交互,尤其是在许多 GenAI 解决方案正在诞生的初创公司场景下,这个费用听起来并不那么可怕。
迷失在中间
大型语言模型(LLMs)面临的另一个挑战是中间丢失的上下文问题,具体内容可以参见这篇论文。我的研究和 RAG 系统在处理多个文档时的经验表明,LLMs 在从长上下文输入中推断信息时并不十分强大。当相关信息位于上下文的中间时,模型的表现会显著下降。然而,当所需信息位于上下文的开始或结尾时,表现则会有所提升。文档重排序(Document Re-ranking)已成为解决这个特定问题的研究主题。我将在另一篇文章中探讨这些方法。现在,让我们回到我们正在探索的解决方案,它利用了 K 均值聚类。
什么是 K 均值聚类?!
好吧,我承认在上一节我偷偷引入了一个技术概念,让我为你解释一下(如果你不熟悉该方法,我会为你详细说明)。
先从基础开始
为了理解 K 均值聚类,我们首先应当知道什么是聚类。假设我们有一张凌乱的桌子,上面有钢笔、铅笔和便签纸杂乱无章。为了整理,可以将相似的物品归为一组,比如所有钢笔放在一组,铅笔放在另一组,便签纸放在第三组,最终形成 3 个独立的群组(并非提倡隔离)。聚类就是这个过程,在数据集合中(在我们的案例中是文档文本的不同片段),将相似的数据或信息归为一组,从而在模型中形成清晰的分隔,使得我们的 RAG 系统能够更有效率地选择和提取信息,而不是像贪婪算法那样需要处理所有数据。
K,是什么意思?
K 均值是一种特定的聚类方法(虽然还有其他方法,但我们先不讨论这些)。让我通过 5 个简单的步骤来解释它是如何工作的:
-
选择群组数量(K):我们希望将数据划分为多少个群组。
-
选择群组中心:最初,为每个 K 个群组随机选择一个中心值。
-
群组分配:然后,根据每个数据点与先前选择的中心的距离,将数据点分配给各个群组。示例:距离中心 1 最近的项被分配到群组 1,距离中心 2 最近的项被分配到群组 2……依此类推,直到 Kth 群组。
-
调整中心点:在所有数据点被归类后,我们计算每个群组中项目位置的平均值,这些平均值将成为新的中心,以提高准确性(因为我们最初是随机选择的)。
-
重复进行:通过新的中心点,数据点的分配会再次更新为 K 个群组。这一过程会持续进行,直到群组内部的差异(从数学角度来说是欧几里得 *距离)最小,同时与其他群组的其他数据点的差异最大,从而实现最优的分隔。
虽然这可能是一个相对简化的解释,但对于我的小伙伴们(科技爱好者),这算法的更详细和技术性的解释可以在这里找到。
理论够了,让我们开始编码吧。
现在我们已经讨论了 K-means 聚类,这是我们优化之旅中的主角,让我们看看这个强大的算法如何在实际中应用,来总结我们的手册。
现在我们已经有了文档文本的块,我们将把它们嵌入到向量中。
from langchain_openai import OpenAIEmbeddings
embeddings = OpenAIEmbeddings()
# Embed the chunks
chunk_texts = [chunk.page_content for chunk in splits] # Extract the text from each chunk
chunk_embeddings = embeddings.embed_documents(chunk_texts)
可能稍微有点理论
好吧,好吧,也许这里还有更多要学习的——什么是嵌入?向量?!为什么呢?
嵌入与向量
想想计算机是如何做事的——它把所有东西看作二进制的,因此,教/指令它的最佳语言就是数字。因此,让复杂的机器学习系统理解我们的数据的最佳方法,就是把所有的文本看作数字,而我们进行这种转换的方法叫做嵌入(Embedding)。描述文本或单词的数字列表称为向量(Vectors)。
嵌入(Embeddings)可以根据我们如何描述数据以及选择的启发式方法而有所不同。假设我们想描述一个苹果,我们需要考虑它的颜色(红色)、形状(圆形)和大小。每一个特征都可以用数字来编码,比如‘红色’在 1 到 10 的范围内可以用 8 表示,圆形度可以用 9 表示,大小可以用 3 表示(宽度为 3 英寸)。因此,我们描述苹果的向量会是[8,9,3]。当描述文档的不同特征时,这一概念会变得更加复杂,我们希望每个数字都能映射到主题、语义关系等方面。这会导致向量包含数百个或更多的数字。
但是,为什么?!
现在,这种方法提供了什么改进呢?首先,正如我之前提到的,它使得 LLMs(大型语言模型)的数据解读变得更加容易,从而提高了模型推理的准确性。其次,它在内存优化(技术术语中称为空间复杂度)方面也有巨大帮助,通过将数据转换为向量来减少内存消耗。向量的范式被称为向量空间,例如:一篇包含 1000 个单词的文档可以被压缩成一个 768 维的向量表示,从而产生一个 768 个数字的表示,而不是 1000 个单词。
更深一点的数学(再次为我的小伙伴们准备的),在计算机语言中,"1234"(作为单词或字符串)将消耗 54 字节的内存,而数字形式的 1234(作为整数)只会消耗 8 字节!因此,如果考虑文档消耗的兆字节,我们也在减少内存管理成本(太棒了,预算!)。
我们回来了!
5 使用 Scikit-Learn Python 库进行简单实现时,我们首先选择我们想要的聚类数,在我们的例子中是 15 个。然后我们运行算法,将嵌入的文档拟合成 15 个聚类。参数‘random_state = 42’意味着我们正在打乱数据集,以防止模型中的模式偏差。
还需要注意的是,我们正在将嵌入向量列表转换为 Numpy 数组(Numpy 库中用于高级操作的向量数学表示)。这是因为 Scikit-learn 要求 K-means 操作使用 Numpy 数组。
from sklearn.cluster import KMeans
import numpy as np
num_clusters = 15
# Convert the list of embeddings to a NumPy array
chunk_embeddings_array = np.array(chunk_embeddings)
# Perform K-means clustering
kmeans = KMeans(n_clusters=num_clusters, random_state=42).fit(chunk_embeddings)
课程结束了...暂时。
我认为这是一个休息的好时机!我们已经在代码和理论方面覆盖了很多内容。但别担心,我将发布第二部分,介绍我们如何利用这些聚类生成大型文档的丰富摘要。将会展示更多有趣的技术,当然,我也会尽我所能解释所有的理论和理解!
所以敬请关注!此外,我非常期待您的反馈和任何评论,这对我改进内容非常有帮助,像往常一样,非常感谢您的阅读,希望它值得您花时间阅读!

摄影:由Priscilla Du Preez 🇨🇦提供,照片来源于Unsplash
解密图神经网络
揭示一种新兴深度学习算法的力量和应用
·发表于Towards Data Science ·阅读时长 6 分钟·2024 年 1 月 5 日
--

通过这篇文章,我旨在向大家介绍一种日益流行的深度学习算法——图神经网络(GNNs)。GNNs 正逐渐从研究领域走向实际应用,已经在现实世界的问题上展示了令人印象深刻的成果,显示出它们巨大的潜力。本文的主要目标是解密这一算法。如果到最后,你能回答像“为什么要使用 GNN?”“GNN 是如何工作的?”这样的问题,我会认为我的任务已经完成。
在深入讨论之前,有必要回顾两个与我们话题密切相关的概念:
图与嵌入
计算机科学中的图
让我们首先简要回顾一下图是什么。图在无数领域中都有应用,特别是在计算机科学中,图是一种由两个元素组成的数据结构:一组顶点或节点,以及一组连接这些节点的边。
图可以是有向图或无向图。有向图是指边具有方向性的图,如下所示。
所以,图是对象(节点)之间关系(边)的表示。
揭开 Mixtral of Experts 的神秘面纱
Mistral AI 的开源 Mixtral 8x7B 模型引起了广泛关注——这里是其内部的秘密
·发表于Towards Data Science ·阅读时长 8 分钟·2024 年 3 月 17 日
--

图像由 GPT-4 生成
Mixtral 8x7B,Mistral AI 的全新稀疏专家混合(Mixtures of Experts)大语言模型,最近引起了轰动,标题如“ Mistral AI 推出 Mixtral 8x7B:一种稀疏专家混合(SMoE)语言模型变革机器学习”或“Mistral AI 的 Mixtral 8x7B 超越 GPT-3.5,震撼 AI 世界”。
Mistral AI 是一家法国的人工智能初创公司,成立于 2023 年,由 Meta 和 Google 的前工程师创办。该公司发布了 Mixtral 8x7B——这可能是大语言模型历史上最不拘礼节的发布,2023 年 12 月 8 日,他们仅仅在 Twitter 账号上发布了 Torrent 磁力链接。

激发了许多关于 Mistral 发布模型的恶搞图片。
“Mixtral of Experts”(Jiang 等,2024)这篇研究论文在一个月后于今年 1 月 8 日发布在 Arxiv 上。让我们来看看,看看这些炒作是否有其道理。
(剧透警告:从技术层面来看,其实并没有太多新内容。)
同行评审揭秘:什么,为什么,如何
作为 AI 与机器人学副主编,拥有 100 篇同行评审经验
·发表于Towards Data Science ·阅读时长 10 分钟·2024 年 9 月 4 日
--

图片来自Annie Spratt拍摄,来源:Unsplash
我将通过个人经历分享我对学术同行评审过程的理解,从一名犹豫不决的评审到成为IEEE 机器人与自动化通讯副主编(影响因子 4.6)。
虽然大多数传统的科学与工程出版物要求具有先前的出版经验和学术资质才能担任评审,但机器学习和数据科学可能是一个例外。数据科学广泛采用和应用的一个重要推动力是开源项目和代码库。许多开源数据科学的有影响力的贡献者并不总是已发表的研究人员,但通过实践和实验积累了深厚的领域知识。此外,机器学习的正式学位课程仅存在了几年,许多当前的研究人员来自不同的背景。举例来说,我的背景是机械工程。
考虑到上述因素,我希望如果你是对评审过程感到好奇并希望参与的机器学习从业者,这篇文章能够为你提供一些价值。
目录
· 我的故事
· 什么是同行评审?
∘ 编辑委员会成员不应该是专家吗?
· 同行评审流程
· 为什么你应该考虑进行同行评审
· 如何参与其中?
∘ 通过 Web of Science 追踪同行评审
∘ 我需要是已发表的研究者吗?
· 这需要多少时间?
· 结论
· 冷邮件模板
· 免责声明
我的故事
在 2024 年 8 月,我达到了100 篇经过验证的同行评审,涉及 9 本不同的学术期刊和会议。尽管我在 2016 年完成了第一次审稿,但直到 2022 年中期,我才真正开始享受这一过程。

来自我的 Web of Science 个人资料的同行评审指标。图片由作者提供。
作为研究生(2015–2020),我从未真正享受审稿的过程。相反,我大多是在导师要求时出于学术义务才进行审稿。而且,鉴于我只有很少的出版物,我对自己批评他人工作的能力缺乏信心。
毕业后,我发现很难跟上最新的研究进展。作为学生,阅读论文是工作的一部分。然而,在行业中,我只阅读最受欢迎的论文。为了跟上最新的研究,履行我的学术责任,并建立更强的研究档案,我开始给各个期刊的编辑发邮件,表达我成为审稿人的兴趣。尽管几乎所有期刊都回复了我,但最初只有 2 到 3 个期刊分配了我的审稿任务。随着时间的推移,我开始收到那些我没有联系过的期刊的审稿请求。
在 2023 年底,我申请了IEEE RA-L的副编辑职位,并最终被选中担任人机交互领域的副编辑。
在本文的其余部分,我将解释:
-
同行评审的重要性以及这一过程包括哪些内容,
-
为什么你应该考虑为学术出版物进行审稿以及如何开始这一过程
-
时间投入和其他需要考虑的因素
最后,我还将分享一个冷邮件模板,供你用来联系期刊编辑。
尽管同行评审过程的有效性存在一些争议,但我并不认为自己足够精通该方面的内容来发表评论。相反,我将专注于分享我的经验和所学。
什么是同行评审?
同行评审是一个至关重要的工具,理想情况下,它能够确保科学工作的高质量。它是一个在研究或学术工作发布或接受展示之前,用来评估研究或学术工作的质量、有效性和相关性的过程。这个评估过程由相关领域的专家同行进行。同行评审过程帮助确保已发布的研究具有高质量,并且对该领域做出有意义的贡献,从而保持学术标准和信誉。
期刊依赖于一网络志愿的同行评审者来完成上述工作。这主要是由于两个原因:
-
提交量: 学术出版物每年可能会收到数千篇潜在稿件。例如,IEEE 计算机视觉与模式识别大会(CVPR)在 2024 年收到了11,532 篇提交。尽管热门期刊/会议的编辑委员会可能有几百名成员,但其数量远低于提交稿件的数量。此外,大多数出版物至少有两轮审稿,实际上是将所需审稿数量翻倍。
-
多领域专长: 尽管大多数出版物的范围相对狭窄,但它们仍覆盖了特定领域内的广泛科学知识。为此,编辑委员会通常由来自多个子领域的专家组成,但学术研究的性质高度具体,编辑人员几乎不可能具备适当的专长来公平地评价每一篇提交的稿件。
编辑委员会成员不应该是专家吗?
是的,但期刊的范围(或关注领域)往往过于广泛。例如,考虑一下我作为副编辑所服务的 IEEE 机器人与自动化通讯(RA-L)的范围:
期刊发布同行评审的文章,及时且简明地报告创新研究思想和应用成果,报道机器人技术和自动化领域的重要理论发现和应用案例研究。
术语机器人技术和自动化领域描述了期刊所关注的广泛工作领域。这可能包括仿生机器人、生物医学机器人、野外机器人、人机交互、类人机器人、软体机器人——仅举几例。此外,自动化部分可能基于机器学习、规则基础方法或传统的控制理论。
大多数机器人研究人员专注于某一特定领域。我本人获得了医学机器人学的博士学位。在这个领域中,我专注于物理治疗辅助机器人。在这个范围内,我关注上肢中风康复。进一步来说,我探索了先进深度学习和生物力学信号在自动化辅助中的应用。因此,尽管在表面上我被称为医学机器人领域的“专家”,但我并没有深入了解比如——深度学习在外科机器人中的应用。
然而,我确实认识一些在这些专业领域有专长的研究人员和同事。我可以依赖他们的知识,提供反馈和推荐,这些同行对出版过程至关重要,确保提交的稿件能够得到有根据且全面的评估。
同行评审流程
简而言之,同行评审过程通常包括以下步骤:
-
提交:作者将稿件提交给期刊或会议。
-
初步筛选:编辑检查提交的稿件是否符合期刊的范围和标准。
-
审稿分配:编辑将稿件发送给该领域的专家(同行评审人)。
-
审查:审稿人评估稿件的质量、方法论和重要性,并提供反馈。他们可能会推荐接受、要求修改或拒绝稿件。
-
编辑决定:编辑根据多位审稿人的反馈决定接受、要求修改或拒绝稿件。
-
修改:如果需要,作者修改稿件并重新提交进行进一步审查。
-
重新审查:审稿人重新审查修改后的稿件,并推荐接受、要求修改或拒绝。大多数期刊在这个阶段只允许二元的接受或拒绝。尽管这一点有所不同。
-
出版:接受的稿件将被编辑并出版。
为什么您应该考虑同行评审?
同行评审可以是一次令人愉快的经历,并且是为科学研究进步做出贡献的宝贵途径,即使您不是一个活跃的研究者。以下是同行评审重要性的几个原因:
-
学术责任:如果您是发表论文的研究者,一般的指导方针是保持
3:1的同行评审与出版比例。这意味着您每发表一篇论文,理想情况下应该审阅三篇论文。这个比例反映了大多数出版物将三位审稿人分配给每篇稿件的常规做法。 -
保持更新:审阅论文涉及阅读尚未出版的工作,通常代表了您领域的前沿。尽管您不能披露或使用未出版的审阅结果,但您仍能获得新技术和当前研究趋势的洞察,提升您在该领域的专业知识。
-
建立研究网络和个人档案:担任同行评审员能够突出您在特定领域的专业知识,是扩展研究网络的绝佳方式。它将您与全球的其他研究人员联系起来,并直接接触编辑委员会成员,提升您的职业能见度和联系。
-
提升论文写作:大多数期刊允许您查看其他审稿人在相同投稿上的反馈。这种曝光为您提供了宝贵的见解,让您了解同行研究人员认为强项和弱项在哪里,从而帮助您完善和提升自己的写作技能。
-
绿卡标准:以下内容不是法律建议,仅反映我的个人经验。 如果您需要更多信息,请咨询移民律师。
如果你是美国的移民并且寻求基于就业的绿卡(EB 绿卡),这点特别相关。EB1-A、EB1-B 和 EB1-NIW 等类别通常要求“提供作为评审员参与同一或相关学术领域他人工作的证据,无论是以小组成员还是单独身份”,作为证明专业性的标准之一。因此,审阅更多的论文可以增强你的申请,并提高满足此标准的机会。事实上,我本人就利用了我的同行评审背景作为 EB1-B 绿卡的一个标准。
你如何参与其中?
同行评审可能看起来令人生畏,但其实相当可控,并且类似于代码审查过程。就像创建一个需要审查后合并的拉取请求一样,手稿在发布之前也需要被审阅。
编辑们经常寻找同行评审人员,如果你主动联系他们,他们通常非常愿意接纳。一封简单的冷邮件可能非常有效。我将在本文末尾提供一个邮件模板供你使用。
只要你的目标是提供无偏见的反馈,帮助作者改进他们的作品,你就是以正确的心态进行这个过程。大多数编辑会重视并尊重你的贡献。
使用 Web of Science 跟踪同行评审
我强烈建议你创建一个Web of Science个人资料。这使你能够验证你的审稿记录,并将所有信息汇总在一个地方。它还有一个方便的导出功能,可以作为你的审稿经验证明,且大多数组织都接受。它还提供有趣的统计数据,例如你的审稿平均时长。

Web of Science 可以生成的一些有趣的统计数据。图片来自作者。
我需要是一个已发表的研究者吗?
不一定。
虽然许多顶级期刊和会议要求一定的发表经验,但也有一些并不要求。如果你没有发表过论文且是千禧一代(即患有冒名顶替综合症),你可以从审阅小型本地会议的海报或摘要提交开始,来建立自己的个人资料和信心。然后,你可以以此为起点,逐步过渡到国际期刊的发表,并填补缺乏发表历史的空白。
请记住,每篇手稿通常会被 2 到 3 位处于不同职业阶段的审稿人审阅。作为新手,你可能会提供与经验丰富的研究者不同的独特视角。编辑们非常重视所有反馈,而多样化的观点是非常有益的。
这需要多少时间?
同行评审的时间投入因人而异,完全由你决定。我通常将自己限制在每月1–2 篇论文(这包括我的 AE 任务)。在 2023 年,我评审的频率较高,但现在我变得更加挑剔。如果需要,你始终可以拒绝邀请,因为同行评审是自愿的,编辑会尊重你的时间。
每次评审的时长也有所不同。根据我的经验,评审可能需要从几小时到几天不等。如果论文与我的研究密切相关,我可以在一个下午完成。但如果论文与我的领域相近或涉及复杂的方程式,可能需要更长时间,尤其是当需要大量验证时。就个人而言,我避免评审包含过多方程式的论文,因为我不喜欢阅读它们。更多的图片,少一点数学,拜托!
有时候我确实会收到质量极差的论文,看起来像是在浪费时间。但这些论文通常是我评审时花费时间最少的。
总结来说,时间投入因人而异,但你可以根据个人偏好和可用时间选择评审的论文数量和类型。
结论
总结来说,同行评审过程是学术出版中的一个关键组成部分,确保了科学研究的质量和诚信。虽然这个过程具有挑战性,但它带来了显著的好处,包括紧跟前沿研究、提升个人学术形象,并为学术共同体做出有意义的贡献。
最终,同行评审不仅是一项责任,也是一种个人成长和职业发展的机会。它为研究人员提供了一个影响自己领域发展的平台,建立有价值的网络,并提升自己的研究技能。虽然同行评审制度并非没有批评,但它仍然是研究生态系统中的重要组成部分,促进学术严谨性和创新。
冷邮件模板
如约,这里是我过去使用过的冷邮件模板。
主题:申请担任[出版物名称]的同行评审人
亲爱的[编辑的名字],
希望这条消息找到你时你一切安好。
我写信是为了表达我有意担任[出版物名称]的同行评审人。目前,我是[您的角色],在[您的组织]工作,拥有[大学]的[学士/硕士/博士]学位,专业领域为[领域]。我的专业包括[专业 1,专业 2,专业 3],并通过[简要提及相关经验或成就]展现了我的专业能力。
我曾在[出版物 1,出版物 2,出版物 3]上发表过文章,并参与过[项目 1,项目 2]以及[博客 1,博客 2]等开源项目。我也是[出版物 1,出版物 2,出版物 3]的评审人。
你可以在我的[Google Scholar 和/或 GitHub]个人资料中找到更多关于我工作的资料,同时我也已附上我的简历供你参考。
我相信我的背景和专业知识使我成为审阅提交材料的合适人选,特别是在与[列出兴趣领域]相关的领域。我将非常荣幸为[期刊名称]做出贡献,并支持这些领域研究的进展。
感谢您考虑我的申请,期待您的回复。
此致敬礼,
[您的全名]
[您的联系信息]
免责声明
ChatGPT 被用作本文的 校对 工具。根据反馈进行了少量修改。内容由 作者创作。
通过大脚怪目击应用程序转型揭开 R Shiny 模块的神秘面纱
深入学习如何使用模块构建 Shiny 应用程序的指南
·发表于 Towards Data Science ·9 分钟阅读·2024 年 5 月 3 日
--

图片由 Luke Chesser 提供,来源于 Unsplash
当我多年前发现 Shiny 时,我立刻就被吸引了。Shiny 是一个 R 包,用于构建可以在后台运行 R 代码的互动 web 应用程序。我被它所提供的能力所吸引,它使我能够快速创建数据分析和建模原型,并与我的利益相关者分享,以便让他们参与其中。一个这样的例子是我创建的异常检测仪表盘,它帮助我的利益相关者理解我对分析方法的理解,并在我们将该方法投入生产之前,带领他们一起走过这一过程。
在这篇文章中,我将带你了解如何使用模块化方法学习构建 Shiny,尤其是如果你之前以单体方式学习过的话。我将通过一个简单的大脚怪目击应用程序进行讲解,应用程序基于一个合成数据集,用户可以选择一个州,并查看该州大脚怪目击最多的前十个县以及目击的时间变化。这个商业场景是什么呢?好吧,我是一个大脚怪爱好者,我想知道我应该搬到哪个县生活,因此这是一个非常关键的仪表盘。
为数据科学家揭开社交媒体的神秘面纱
数据科学家关于 AI 驱动内容创作的指南
·发表于Towards Data Science ·阅读时间:10 分钟·2024 年 1 月 25 日
--

图片来源:Matthew Dockery 于Unsplash
对于那些渴望将自己专业知识转化为引人入胜的社交媒体内容的创作者来说,面对的局面可能令人望而却步。再加上数据科学和人工智能这一复杂且迅速发展的领域,许多数据科学家在努力打造个人品牌时,往往难以发出自己的声音。无数个小时花费在撰写富有洞察力的博客文章上,最终却消失在网络算法的回音室中。然而,作为数据科学家,我们有一些可以利用的技巧,帮助我们应对这一充满挑战的内容创作环境!随着 AI 的最新进展,一种强大的新工具应运而生——RAG 系统。
RAGs 不是抹布
RAGs 是生成式 AI 中的新兴热点,就像洗衣机中的抹布一样,它们通过帮助清理生成式 AI 在训练时无法访问的信息,从而改善其对信息的理解。RAG 代表检索增强生成,其思想是利用当前的或专有的信息来为生成式 AI 的提示增加更多相关的上下文。
通过这种方式,RAGs 不仅帮助清理生成式 AI 输出的知识,还帮助优化和打磨内容创作周期,确保内容更加相关。
揭开数据科学中相关性矩阵的神秘面纱
理解变量之间的联系:相关性矩阵及其应用的全面指南
·发布于Towards Data Science ·阅读时间 13 分钟·2024 年 11 月 13 日
--

数据分析的主要用途是识别和量化变量之间的相关性和模式,以便将其用于未来的预测,并为相应的模型提供训练数据。相关性矩阵是一种关键方法,可以帮助图形化展示数据集中两个变量之间的相关性,即依赖关系。
本文将深入探讨相关性概念,以及相关性矩阵如何帮助展示变量之间的依赖关系。例如,这包括详细分析相关性矩阵的计算和解释,并解释如何在 Python 中创建这样的矩阵。全面的描述还包括展示该方法的局限性,以便能够正确评估其使用和意义。
什么是相关性矩阵?
相关性矩阵是一种统计方法,用于量化和比较数据集中不同变量之间的关系。所有两个变量组合的成对相关性以表格形式展示,每个单元格中包含...
使用 Python 去噪雷达卫星图像从未如此简单
最新版本的 deepdespeckling 发布介绍
·发布于《数据科学之路》 ·阅读时间:8 分钟·2024 年 4 月 23 日
--

法国尼姆附近农业区域的光学和雷达图像
合成孔径雷达(SAR)图像在航空航天、军事、气象等多个领域得到了广泛应用。问题是这种图像在原始格式下存在噪声。虽然这些图像通常文件较大,但从科学角度来看,如何高效去噪是一项具有挑战性的任务,而且在现实世界中也非常有用。
在这篇《数据科学之路》文章中,我们介绍了deepdespeckling,这是一个开源的 Python 包,能够使用新型基于深度学习的方法去噪合成孔径雷达(SAR)图像。
我们很高兴地宣布,我们已发布了deepdespeckling 的新版本,该版本支持使用MERLIN和SAR2SAR方法去噪雷达卫星图像。
快速回顾一下卫星图像
卫星图像有两大类:
-
光学图像:我们在看天气预报时常见的图像类型。这些图像是由光学传感器拍摄的。
尽管这些图像通常提供了高水平的细节,但在捕捉地球复杂情况时,至少面临着两个重大挑战:夜间条件和恶劣天气带来的限制。
-
雷达图像: 光学系统依赖于阳光(传感器是被动的),而雷达则发送电磁波并测量由地面物体反向散射的成分(传感器是主动的)。雷达传感器可以在任何时间和任何气象条件下获取数据,因为发射波的波长使其能够穿透云层。然而,它们遇到了一个固有的问题:斑点噪声。
什么是斑点噪声?
斑点是一种颗粒状干扰,由发射的无线电波的反射特性引起,降低了图像的质量,因此也影响了人眼的可解释性。

一张分别没有和有斑点噪声的图像示例
如何去除斑点噪声
存在多种方法,但深度学习在这项任务中带来了显著的改进。Emanuele Dalsasso,Loïc Denis 和 Florence Tupin 开发了两种基于深度学习的去斑点 SAR 图像方法:
-
MERLIN(coMplex sElf-supeRvised despeckLINg):一种基于单视复合 SAR 图像实部和虚部分离的自监督策略,我们在之前的 Towards Data Science 文章中介绍了这一方法。
-
SAR2SAR:利用多时相时间序列训练神经网络,通过仅查看噪声图像来恢复 SAR 图像。该方法是最新发布的 deepdespeckling 新功能的一部分。因此,我们将在本文中重点介绍该方法。
SAR2SAR
就像 MERLIN 一样,SAR2SAR 也从噪声到噪声(noise2noise)算法中汲取灵感,该算法证明可以在没有噪声干净示例的情况下训练一个去噪模型。这一特性在 SAR 去斑点中尤其重要,因为不存在没有斑点的获取图像。

SAR2SAR 基于这样一个假设:在不同时间获取的同一区域的两张图像分别受到两种不相关的斑点噪声实现的污染,这与应用噪声到噪声(noise2noise)原理的假设相符。这使得开发一个模型来去除地面范围检测(GRD)SAR 图像中的斑点成为可能,而此类图像仅提供幅度数据(相位在检测步骤中被抑制),因此无法在此类数据上使用 MERLIN。利用时间序列数据来生成一个数据集,其中包含同一场景的独立斑点噪声实现(采用基于预训练模型的变化补偿策略,以确保时间序列数据仅在斑点成分上有所不同)。
一旦模型训练完成,在推理过程中,SAR2SAR 只需要一张 GRD 图像,并且可以有效地用于抑制 Sentinel-1 GRD SAR 图像中的斑点噪声。
SAR 图像获取。
根据照射场景(扫幅)与图像分辨率之间的折中,存在不同的获取模式。因此,每种获取模式会生成具有不同分辨率的图像,物体的外观也因此具有每种获取模式的特定性。
因此,必须为每种模式开发特定的模型。考虑到 MERLIN 应用的简便性,它只需要单个 SAR 图像,因此可以无缝收集每种特定模式的数据集。我们已经在以下图像上训练了 MERLIN:
-
使用条带模式获取的 TerraSAR-X 图像。
-
使用高分辨率 SpotLight 模式获取的 TerraSAR-X 图像。
-
使用 TOPS 模式获取的 Sentinel-1 图像。
使用 deepdespeckling 包。
包安装。
在安装 deepdespeckling 之前,确保安装了 gdal 依赖项,可以使用以下命令通过 conda 进行安装:
conda install -c conda-forge gdal
然后,你可以按如下方式安装包:
pip install deepdespeckling
使用 MERLIN 去噪一张图像。
要使用 MERLIN 去噪 SAR 图像,图像需要是.cos 或.npy 格式。
需要设置两个参数:
-
model_name:"spotlight"表示使用聚光模式获取的 SAR 图像,"stripmap"表示使用条带模式获取的 SAR 图像,或"Sentinel-TOPS"表示使用 TOPS 模式获取的图像。 -
symetrise*: 在 MERLIN 的噪声图像预处理步骤中,实部和虚部会被“对称化”(以符合 MERLIN 的理论假设)。如果要跳过此步骤,可以将symetrise参数设置为False。
from deepdespeckling.utils.load_cosar import cos2mat
from deepdespeckling.utils.constants import PATCH_SIZE, STRIDE_SIZE
from deepdespeckling.merlin.merlin_denoiser import MerlinDenoiser
# Path to one image (cos or npy file)
image_path="path/to/cosar/image"
# Model name, can be "spotlight", "stripmap" or "Sentinel-TOPS"
model_name = "spotlight"
symetrise = True
image = cos2mat(image_path).astype(np.float32)
denoiser = MerlinDenoiser(model_name=model_name, symetrise=symetrise)
denoised_image = denoiser.denoise_image(image, patch_size=PATCH_SIZE, stride_size=STRIDE_SIZE)
这段代码将会把去噪后的图像存储在denoised_image变量中的numpy 数组中。

一个完整大小的噪声 SAR 图像示例。

使用 MERLIN 去噪的同一图像。
使用 SAR2SAR 去噪一张图像。
要使用 SAR2SAR 去噪 SAR 图像,图像需要是.tiff 或.npy 格式。
from deepdespeckling.utils.load_cosar import cos2mat
from deepdespeckling.utils.constants import PATCH_SIZE, STRIDE_SIZE
from deepdespeckling.sar2sar.sar2sar_denoiser import Sar2SarDenoiser
# Path to one image (tiff or npy file)
image_path="path/to/cosar/image"
# Works exactly the same as with MERLIN
image = cos2mat(image_path).astype(np.float32)
# Denoise the image with SAR2SAR
denoiser = Sar2SarDenoiser()
denoised_image = denoiser.denoise_image(image, patch_size=PATCH_SIZE, stride_size=STRIDE_SIZE)

使用 SAR2SAR 的结果示例(转换为 png 后显示)。
使用 MERLIN 或 SAR2SAR 去噪一组图像。
对于 MERLIN 和 SAR2SAR,你可以选择3 个不同的函数来去噪存储在文件夹中的一组 SAR 图像:
-
despeckle用于去噪完整大小的图像。 -
despeckle_from_coordinates用于去噪由某些坐标定义的图像子部分。 -
despeckle_from_crop用于去噪通过裁剪工具定义的图像子部分。
去噪完整大小图像
from deepdespeckling.despeckling import despeckle
# Path to a folder of several images
# images have to be in .tiff or .npy formats if using sar2sar
# images have to be in .cos or .npy formats is using merlin ("spotlight", "stripmap" or "Sentinel-TOPS")
image_path="path/to/cosar/image"
# Folder where results are stored
destination_directory="path/where/to/save/results"
# Can be "sar2sar", "spotlight' or "stripmap"
model_name = "spotlight"
# symetrise parameter if using "spotlight", "stripmap" or "Sentinel-TOPS" (harmless if using "sar2sar")
symetrise = True
despeckle(image_path, destination_directory, model_name=model_name, symetrise=symetrise)
despeckle 函数将在destination_directory中创建多个文件夹:
-
processed_images:存储在image_path中定义的文件夹内的原始图像的npy文件(numpy 数组转换)。 -
noisy:预处理后的噪声图像,分别以.npy和.png格式保存。 -
denoised:去噪后的图像,分别以.npy和.png格式保存。
使用自定义坐标去噪图像的部分区域
from deepdespeckling.despeckling import despeckle_from_coordinates
# Path to a folder of several images
# images have to be in .tiff or .npy formats if using sar2sar
# images have to be in .cos or .npy formats is using merlin ("spotlight", "stripmap" or "Sentinel-TOPS")
image_path="path/to/cosar/image"
# Folder where results are stored
destination_directory="path/where/to/save/results"
# Example of coordinates of the subparts of the images to be despeckled
coordinates_dictionnary = {'x_start':2600,'y_start':1000,'x_end':3000,'y_end':1200}
# Can be "sar2sar", "spotlight", "stripmap" or "Sentinel-TOPS"
model_name = "spotlight"
# symetrise parameter if using "spotlight", "stripmap" or "Sentinel-TOPS" (harmless if using "sar2sar")
symetrise = True
despeckle_from_coordinates(image_path, coordinates_dict, destination_directory,
model_name=model_name, symetrise=symetrise)
despeckle_from_coordinates 函数将创建与 despeckle 函数相同的文件夹,并使用指定的坐标裁剪图像。

使用自定义坐标去噪的图像示例(转换为 PNG 格式后显示)
使用裁剪工具去噪图像的部分区域
from deepdespeckling.merlin.inference.despeckling import despeckle_from_crop
# Path to a folder of several images
# images have to be in .tiff or .npy formats if using sar2sar
# images have to be in .cos or .npy formats is using merlin ("spotlight", "stripmap" or "Sentinel-TOPS")
image_path="path/to/cosar/image"
# Folder where results are stored
destination_directory="path/where/to/save/results"
# If True it will crop a 256*256 image from the position of your click
# If False you will draw free-handly the area of your interest
fixed = True
# Can be "sar2sar", "spotlight", "stripmap" or "Sentinel-TOPS"
model_name = "spotlight"
# symetrise parameter if using "spotlight""stripmap" or "Sentinel-TOPS" (harmless if using "sar2sar")
symetrise = True
despeckle_from_crop(image_path, destination_directory, model_name=model_name, fixed=fixed, symetrise=symetrise)
despeckle_from_crop函数将首先启动裁剪工具:只需选择一个区域,满意后按“q”键即可完成裁剪

裁剪工具的实际操作

使用裁剪工具进行去噪的结果
然后,despeckle_from_crop 函数将创建:
-
与
despeckle函数相同的文件夹,图像通过裁剪工具进行裁剪 -
cropping_coordinates.txt文件,包含所选裁剪的坐标
深入了解
现在你已经了解了如何使用 deepdespeckling,要进一步了解其工作原理,你可以查看 GitHub 仓库。我们还提供了一个 Sphinx 文档。
如果有任何问题或反馈,欢迎随时联系我!
作者
除非另有说明,所有图片均由作者提供
联系
如果有任何问题,欢迎随时联系我。
要了解更多关于 Hi! PARIS 及其工程团队的信息:
非规范化:深思熟虑的优化还是不理性的先锋派?
关于性能优化和数据质量的视角
·发布于Towards Data Science ·15 分钟阅读·2024 年 8 月 10 日
--

拆解关系型数据 - (图片来源:DALL-E)
这对一些人来说可能会很惊讶:数据建模通常是一个协作的过程,涉及来自不同领域的人的激烈辩论。这是一个典型的孕育异想天开观点和巧妙技巧的土壤,使其成为关于最佳方法辩论的经典话题。为了让这个话题更具戏剧性,我喜欢把它看作是一个灰胡子纯粹主义者,手握厚厚的规则手册,与一个轻率的先锋派人物之间的对抗,后者对一切都抛出华丽的NoSQL(无论这意味着什么)。
拥有一套规则是有帮助的,因为很难理解所有决策的影响,特别是面对一个新问题时。经验在掌握任何学科中都起着至关重要的作用,数据建模也不例外。既定的规则和惯例作为桥梁,弥补了知识的鸿沟。然而,在遵循这些准则与开放接纳经验和务实推理之间,存在着微妙的平衡。
“要深刻理解规则,这样你才能有效地打破它们。”
达丨赖丨喇嘛的 18 条生活规则
在我看来,这句格言确实非常精准。我很重视做出有根据的决策,而在与传统规则推理时……
使用 GitHub Actions 部署 LightGBM 机器学习模型
初学者指南:如何摆脱 Jupyter 笔记本并部署机器学习模型
·发布于Towards Data Science ·8 分钟阅读·2024 年 6 月 10 日
--

网络上充斥着关于如何在 Jupyter 笔记本中训练和调优机器学习模型的教程。但是,正如Pau Labarta Bajo所完美地表达的:
在 Jupyter 笔记本中的机器学习模型,其商业价值为$0。
高管们对停留在 Jupyter 笔记本中的概念验证模型没有兴趣。他们需要实时的模型,这些模型通过持续响应新数据来提供有形的价值。
在本教程中,我将向你展示如何构建一个。
我们将训练一个 LightGBM 机器学习模型,编写一个脚本,利用该模型为给定的输入 csv 文件生成得分,并将整个过程作为批量预测管道,通过 GitHub Actions 进行部署。
全部免费。
如果你从未将模型投入生产环境(或者你甚至不知道这句话是什么意思),那么这篇指南适合你。
背景:什么是“生产化”一个模型?
通常,这意味着你将模型上传到一个服务器,在那里它可以自动/反复地生成预测。
使用 Cloud Run 和 Cloud Build 部署生产就绪的 Streamlit 应用
如何在无服务器架构和 CI/CD 流水线中部署容器化应用。
·发布于 Towards Data Science ·8 分钟阅读·2024 年 3 月 22 日
--

图片来自 Dominik Lückmann 在 Unsplash
如果你是数据科学家,可能已经熟悉 Streamlit,你可能曾用它来原型设计一个演示、分享仪表盘,甚至构建一个更复杂的应用。
在这篇文章中,你将学习如何在 Google Cloud Platform 上快速且高效地部署 Streamlit 应用。
为此,你需要:
-
一个有效的 GCP 账户和信用卡(尽管按照本教程操作不会花费你一分钱)
-
Pulumi: 一款基础设施即代码(IaC)工具,用于配置部署应用所需的云资源
-
需要一些 Google Cloud 服务(Cloud Run)、Docker 和 Poetry 的基础知识
-
一个 Github 账户,用于通过 Cloud Build CI/CD 平台触发自动部署
如果你不熟悉这些工具,下面会详细介绍,并提供链接让你了解更多。
不再赘述,接下来让我们一探究竟 👇
什么是 Cloud Run?☁️
将长时间运行的 ETL 流水线部署到 ECS 与 Fargate
构建后端应用程序
为了保持简单并将成本降到最低
·发表于 Towards Data Science ·阅读时间 17 分钟·2024 年 3 月 3 日
--

ETL 流水线 | 作者提供的图像
ETL 代表 提取、转换 和 加载。ETL 流水线本质上就是一个数据转换过程——从一个地方提取数据,对其进行处理,然后将其加载回同一位置或不同位置。
如果你通过 API 从事自然语言处理工作,我猜大多数人都会开始这样做,那么在处理数据时,你很容易遇到 AWS Lambda 的超时限制,尤其是当至少一个函数执行超过 15 分钟时。所以,虽然 Lambda 很好,因为它快速且非常便宜,但超时问题可能会带来困扰。
这里的选择是将代码部署为容器,该容器可以根据需要运行并按计划执行。因此,与使用 Lambda 启动一个函数不同,我们可以启动一个容器,在 ECS 集群中使用 Fargate 运行。

我们可以将 EventBridge 用于 Lambda 和 ECS | 作者提供的图像
为了澄清,Lambda、ECS 和 EventBridge 都是 AWS 服务。
**与 Lambda 一样,运行 容器 一两个小时的成本是 最小的。然而,它比运行无服务器函数要 复杂 一些。但如果你正在阅读这篇文章,那么你可能已经遇到了相同的问题,并且在思考过渡的最简单方法是什么。
我创建了一个非常简单的 ETL 模板,使用 Google BigQuery 来提取和加载数据。这个 模板 如果你跟着做,几分钟内就能运行起来。
使用 BigQuery 完全是可选的,但我通常会将长期数据存储在那里。
介绍
与其在这里构建复杂的东西,不如向你展示如何构建最简化的方案,并保持其精简。
如果你不需要并行处理数据,就不需要包含类似 Airflow 的工具。我看到有些文章不必要地设置了复杂的工作流,而这些对于简单的数据转换并不是严格必要的。
此外,如果你以后想对这个过程进行扩展,随时可以这样做。
工作流
我们将使用 Python 编写脚本,因为我们正在进行数据转换,然后将其与 Docker 打包并推送到 ECR 仓库。
从这里开始,我们可以使用 AWS Fargate 创建一个任务定义,并在 ECS 集群中按计划运行它。

我们用来将代码部署到 AWS 的所有工具 | 图片来自作者
如果这感觉有些陌生,不用担心;随着我们继续学习,你会理解这些服务以及它们的作用。
技术
如果你是第一次使用容器,那么可以将 ECS(弹性容器服务)看作一个帮助我们设置环境的平台,在这个环境中我们可以同时运行一个或多个容器。
另一方面,Fargate 帮助我们简化了容器的管理和设置,使用 Docker 镜像 —— 在 AWS 中这些被称为任务。

一个非常简化的图示 —— 任务运行的基础设施由 Fargate 管理 | 图片来自作者
有一种选择是使用 EC2 来设置你的容器,但那样你需要做更多的手动工作。Fargate 为我们管理底层实例,而在使用 EC2 时,你需要管理和部署自己的计算实例。因此,Fargate 常被称为“无服务器”选项。
我在 Reddit 上找到了一篇讨论这个话题的 帖子,如果你有兴趣了解用户如何看待 EC2 和 Fargate 的对比,可以阅读一下。这能让你了解人们如何比较 EC2 和 Fargate。
我并不是说 Reddit 是唯一可信的来源,但它对于了解用户观点非常有用。
成本
我通常关注的主要问题是如何让代码高效运行,同时管理总成本。
由于我们仅在需要时运行容器,因此只需为我们使用的资源付费。我们支付的价格由多个因素决定,例如运行的任务数量、每个任务的执行时长、任务所使用的虚拟 CPU (vCPUs) 数量以及内存使用量。
大致来说,运行一个任务的总 成本 大约为每小时 $0.01384,具体费用取决于你配置的资源(适用于 EU 区域)。

Fargate 和 ECS 的小时定价(EU 区域)| 图片来自作者
如果我们将这个价格与 AWS Glue 进行比较,就能从中得到一些视角,判断它是否合适。
如果一个 ETL 作业需要 4 个 DPU(AWS Glue 作业的默认数量)并运行一个小时,它的费用将是 4 DPU * $0.44 = $1.76。这个费用仅针对一个小时,比运行一个简单的容器要高得多。
这当然是一个简化的计算,实际的 DPU 数量可能会根据任务有所不同。你可以在他们的定价 页面 上查看 AWS Glue 定价的更多细节。
开始使用
为了跟随这篇文章,我已经创建了一个简单的 ETL 模板,帮助你快速入门。
这个模板使用 BigQuery 来提取和加载数据。它将提取几行数据,做一些简单的操作,然后将数据加载回 BigQuery。
当我运行管道时,我还会有其他的东西来转换数据——我使用自然语言处理的 API,它会在早上运行几个小时——但这些可以留给你稍后添加。这只是为了给你一个模板,便于操作。
按照这个教程,主要步骤如下:
-
设置你的本地代码。
-
设置 IAM 用户 和 AWS CLI。
-
构建并推送 Docker 镜像 到 AWS。
-
创建一个 ECS 任务定义。
-
创建一个 ECS 集群。
-
安排你的任务。
总体来说,使用我提供的代码,完成这一切不应该超过 20 分钟。如果你还没有 AWS 账户,可能需要再加 5 到 10 分钟。
代码
首先在本地创建一个新文件夹并进入该文件夹。
mkdir etl-pipelines
cd etl-pipelines
确保你已经安装了 Python。
python --version
如果没有,本地安装它。
一旦准备好,你可以继续克隆我已经设置好的模板。
git clone https://github.com/ilsilfverskiold/etl-pipeline-fargate.git
当它完成获取代码时,打开你的代码编辑器查看。
首先查看 main.py 文件,了解我如何构建代码,以便理解它的功能。

你的根文件夹中的 main.py 代码 | 图片来自作者
本质上,它将从 BigQuery 中你指定的表格中提取所有包含 “Doe” 的名称,转换这些名称后再将它们作为新行插入回同一个数据表。
你可以进入每个辅助函数,查看我们是如何设置 SQL 查询作业、转换数据然后再将其插入回 BigQuery 表的。
当然,目的是让你设置一个更复杂的东西,但这是一次简单的测试运行,目的是让你能够轻松调整代码。
设置 BigQuery
如果你想继续使用我准备好的代码,你需要在 BigQuery 中设置一些内容。否则,你可以跳过这部分。
你将需要以下几样东西:
-
一个包含 ‘name’ 字段为字符串类型的 BigQuery 表。
-
数据表中 包含名称为“Doe” 的 几行数据。
-
一个具有访问此数据集权限的 服务账户。
要获取服务账户,您需要导航到 Google Cloud 控制台中的 IAM,然后进入“服务账户”。
到了那里,创建一个新的服务账户。
创建后,您需要通过 IAM 为您的服务账户提供 BigQuery 用户 的全局访问权限。

在 IAM 中授予对服务账户的访问权限 | 图片来源:作者
您还需要为此服务账户提供对数据集本身的访问权限,您可以直接在 BigQuery 中通过数据集的 共享 按钮进行操作,然后按 添加主体。

为 BigQuery 中的数据集添加权限给服务账户 | 图片来源:作者
在您给用户适当权限后,确保返回到服务账户并下载一个密钥。这将为您提供一个 json 文件,您需要将其放在根文件夹中。

获取服务账户密钥以进行身份验证 | 图片来源:作者
现在,最重要的部分是确保代码能够访问 Google 凭证,并使用正确的数据表。
您需要将您下载的包含 Google 凭证的 json 文件放在根文件夹中,命名为 google_credentials.json,然后指定正确的表 ID。

更改代码中的表 ID 和服务账户密钥 json 文件 | 图片来源:作者
现在您可能会争辩说不想将凭证存储在本地,这是完全正确的。
您以后可以选择将您的 json 文件存储在 AWS Secrets Manager 中。然而,首先这样做会更简单。
本地运行 ETL 流水线
我们将首先在本地运行这段代码,只是为了验证它是否有效。
所以,设置一个 Python 虚拟环境并激活它。
python -m venv etl-env
source etl-env/bin/activate # On Windows use `venv\Scripts\activate`
然后安装依赖项。我们这里只有 google-cloud-bigquery,但理想情况下,您将有更多依赖项。
pip install -r requirements.txt
运行主脚本。
python main.py
这应该会在您的终端中记录 ‘新行已添加’。这将确认代码按我们预期的方式工作。
Docker 镜像
现在,为了将此代码推送到 ECS,我们需要将其打包成一个 Docker 镜像,这意味着您需要在本地安装 Docker。
如果您没有安装 Docker,可以在这里下载。
Docker 帮助我们将应用程序及其依赖项打包成一个镜像,能够在任何系统上轻松识别并运行。使用 ECS 时,我们需要将代码打包成 Docker 镜像,然后通过任务定义引用这些镜像作为容器运行。
我已经在您的文件夹中设置了一个 Dockerfile。您应该能在那里查看它。
FROM --platform=linux/amd64 python:3.11-slim
WORKDIR /app
COPY . /app
RUN pip install --no-cache-dir -r requirements.txt
CMD ["python", "main.py"]
正如您所看到的,我已经将其保持得非常精简,因为我们这里并没有将 Web 流量连接到任何端口。
我们指定了 AMD64 架构,如果你使用的是非 M1 芯片的 Mac,你可能不需要指定,但指定也不会造成问题。这将向 AWS 指明 Docker 镜像的架构,以避免出现兼容性问题。
创建 IAM 用户
在使用 AWS 时,需要指定访问权限。你遇到的大多数问题都是权限问题。我们将使用本地的 CLI 工具,且为使其正常工作,我们必须创建一个 IAM 用户,并赋予它广泛的权限。
访问 AWS 控制台,然后导航到 IAM。创建一个新用户,添加权限,并创建一个新策略并附加到该用户。
我在 aws_iam_user.json 文件中指定了代码所需的权限。你可以在下面看到这个 JSON 文件的一个简短片段。
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"iam:CreateRole",
"iam:AttachRolePolicy",
"iam:PutRolePolicy",
"ecs:DescribeTaskDefinition",
...more
],
"Resource": "*"
}
]
}
你需要进入这个文件以获取 所有 你需要设置的权限,这里只是一个简短的片段。我已经设置了很多权限,你可能想根据自己的需要稍后进行调整。
一旦你创建了 IAM 用户并为其添加了正确的权限,你将需要生成一个访问密钥。当询问你的使用案例时,选择 “命令行接口(CLI)”。
下载凭证。我们稍后将使用这些凭证进行认证。
设置 AWS CLI
接下来,我们将把终端连接到我们的 AWS 账户。
如果你还没有设置 CLI,可以按照这里的说明进行操作。设置过程非常简单。
安装 AWS CLI 后,你需要使用我们刚刚创建的 IAM 用户进行认证。
aws configure
使用我们在上一步中从 IAM 用户下载的凭证。
创建 ECR 仓库
现在,我们可以开始进行 DevOps 操作了。
我们首先需要在 Elastic Container Registry 中创建一个仓库。ECR 是我们存储和管理 Docker 镜像的地方。在设置任务定义时,我们可以从 ECR 引用这些镜像。
要创建一个新的 ECR 仓库,请在终端中运行此命令。这将创建一个名为 bigquery-etl-pipeline 的仓库。
aws ecr create-repository --repository-name bigquery-etl-pipeline
记下你获得的仓库 URI。
从这里我们需要构建 Docker 镜像,然后将其推送到这个仓库。
为此,你可以进入 AWS 控制台,找到我们刚刚创建的 ECR 仓库。在这里,AWS 会让我们看到我们需要运行的所有推送命令,用以认证、构建并将 Docker 镜像推送到这个 ECR 仓库。

在 ECR 中直接找到推送命令 | 图片来源:作者
然而,如果你使用的是 Mac,我建议你在构建 Docker 镜像时指定架构,否则可能会遇到问题。
如果你在跟着我一起操作,那么请先按照如下方式认证你的 Docker 客户端。
aws ecr get-login-password --region YOUR_REGION | docker login --username AWS --password-stdin YOUR_ACCOUNT_ID.dkr.ecr.YOUR_REGION.amazonaws.com
确保根据需要更改区域和账户 ID。
构建 Docker 镜像。
docker buildx build --platform=linux/amd64 -t bigquery-etl-pipeline .
这是我调整了命令以指定 linux/amd64 架构的地方。
为 Docker 镜像打标签。
docker tag bigquery-etl-pipeline:latest YOUR_ACCOUNT_ID.dkr.ecr.YOUR_REGION.amazonaws.com/bigquery-etl-pipeline:latest
推送 Docker 镜像。
docker push YOUR_ACCOUNT_ID.dkr.ecr.YOUR_REGION.amazonaws.com/bigquery-etl-pipeline:latest
如果一切按计划顺利进行,你将在终端中看到类似这样的内容。
9f691c4f0216: Pushed
ca0189907a60: Pushed
687f796c98d5: Pushed
6beef49679a3: Pushed
b0dce122021b: Pushed
4de04bd13c4a: Pushed
cf9b23ff5651: Pushed
644fed2a3898: Pushed
现在我们已经将 Docker 镜像推送到 ECR 仓库,我们可以使用它来通过 Fargate 设置我们的任务定义。
如果你在这里遇到 EOF 问题,它很可能与 IAM 权限有关。确保给予它所需的所有权限,在此案例中是对 ECR 的完全访问权限,以便标记和推送镜像。
角色和日志组
记住我之前告诉你过的,AWS 中你遇到的最大问题与不同服务之间的角色有关。
为了让这个流程顺利进行,我们需要确保在开始设置任务定义和 ECS 集群之前,先设置好一些必要的东西。
为了做到这一点,我们首先必须创建一个任务角色——这个角色需要从容器访问 AWS 生态系统中的服务——然后是执行角色——这样容器就能从 ECR 拉取 Docker 镜像。
aws iam create-role --role-name etl-pipeline-task-role --assume-role-policy-document file://ecs-tasks-trust-policy.json
aws iam create-role --role-name etl-pipeline-execution-role --assume-role-policy-document file://ecs-tasks-trust-policy.json
我已经在你的文件夹中指定了一个名为 ecs-tasks-trust-policy.json 的 JSON 文件,它将用于创建这些角色。
对于我们正在推动的脚本,它不需要访问其他 AWS 服务的权限,因此目前不需要为任务角色附加策略。不过,你可能之后会需要这么做。
然而,对于执行角色,我们需要给予它 ECR 访问权限,以便拉取 Docker 镜像。
要将策略 AmazonECSTaskExecutionRolePolicy 附加到执行角色,运行这个命令。
aws iam attach-role-policy --role-name etl-pipeline-execution-role --policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
我们在此过程中还会创建最后一个角色——服务角色。如果你已经有了这个角色,则无需创建。
aws iam create-service-linked-role --aws-service-name ecs.amazonaws.com
如果你根本没有创建服务角色,当你尝试运行任务时,可能会遇到类似 “无法假设服务链接角色。请验证 ECS 服务链接角色是否存在” 的错误。
最后,我们创建一个日志组。创建日志组对于捕捉和访问容器生成的日志至关重要。
要创建日志组,你可以运行这个命令。
aws logs create-log-group --log-group-name /ecs/etl-pipeline-logs
一旦你创建了执行角色、任务角色、服务角色以及日志组,我们就可以继续设置 ECS 任务定义。
创建 ECS 任务定义
任务定义是你的任务蓝图,指定使用哪个容器镜像、需要多少 CPU 和内存,以及其他配置。我们使用这个蓝图在 ECS 集群中运行任务。
我已经在你的代码中设置了任务定义,位于 task-definition.json 文件中。不过,你需要在其中设置你的账户 ID 和区域,以确保它按预期运行。
{
"family": "my-etl-task",
"taskRoleArn": "arn:aws:iam::ACCOUNT_ID:role/etl-pipeline-task-role",
"executionRoleArn": "arn:aws:iam::ACCOUNT_ID:role/etl-pipeline-execution-role",
"networkMode": "awsvpc",
"containerDefinitions": [
{
"name": "my-etl-container",
"image": "ACCOUNT_ID.dkr.ecr.REGION.amazonaws.com/bigquery-etl-pipeline:latest",
"cpu": 256,
"memory": 512,
"essential": true,
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/etl-pipeline-logs",
"awslogs-region": "REGION",
"awslogs-stream-prefix": "ecs"
}
}
}
],
"requiresCompatibilities": ["FARGATE"],
"cpu": "256",
"memory": "512"
}
还记得我们在创建 ECR 仓库时获得的 URI 吗?我们将在这里使用它。记得执行角色、任务角色和日志组吗?我们也将在这里使用它们。
如果你将 ECR 仓库、角色和日志组的命名与我设置的完全相同,那么你只需更改帐户 ID 和区域在这个 JSON 文件中,或者确保 URI 是正确的。
你也可以在这里设置 CPU 和内存,来满足运行任务 —— 即运行你的代码 —— 的需求。我设置了 .25 vCPU 和 512 MB 的内存。
一旦你满意,可以在终端中注册任务定义。
aws ecs register-task-definition --cli-input-json file://task-definition.json
现在你应该能够进入Amazon 弹性容器服务,然后在任务定义中找到我们创建的任务。

在 ECS 中找到你创建的任务定义 | 作者提供的图片
这个任务 —— 即蓝图 —— 不会自动运行,我们稍后需要调用它。
创建一个 ECS 集群
ECS 集群作为任务或服务的逻辑分组。当运行任务或创建服务时,你需要指定这个集群。
要通过 CLI 创建集群,请运行此命令。
aws ecs create-cluster --cluster-name etl-pipeline-cluster
一旦你运行此命令,你将能够在 AWS 控制台的 ECS 中看到这个集群。
当我们为下一部分运行任务时,我们会将我们刚刚创建的任务定义附加到这个集群。
运行任务
在我们运行任务之前,我们需要获取可用的子网以及安全组 ID。
我们可以通过 CLI 在终端中直接执行此操作。
在终端中运行此命令以获取可用的子网。
aws ec2 describe-subnets
你将返回一个对象数组,你需要查找每个对象的SubnetId。
如果你在这里遇到问题,请确保你的 IAM 拥有适当的权限。请查看根文件夹中的 aws_iam_user.json 文件,了解连接到 CLI 的 IAM 用户所需的权限。我强调这一点,因为这是我总是遇到的主要问题。
要获取安全组 ID,你可以运行此命令。
aws ec2 describe-security-groups
你需要在终端中查找GroupId。
如果你至少得到了一个SubnetId和一个GroupId(安全组的 ID),我们就准备好运行任务,测试蓝图 —— 即任务定义 —— 是否有效。
aws ecs run-task \
--cluster etl-pipeline-cluster \
--launch-type FARGATE \
--task-definition my-etl-task \
--count 1 \
--network-configuration "awsvpcConfiguration={subnets=[SUBNET_ID],securityGroups=[SECURITY_GROUP_ID],assignPublicIp=ENABLED}"
请记得,如果你为集群和任务定义命名不同的名称,请更改这些名称。还要记得设置你的子网 ID 和安全组 ID。
现在,你可以导航到 AWS 控制台查看正在运行的任务。

查看你 ECS 集群中正在运行的任务 | 作者提供的图片
如果你遇到问题,可以查看日志。
如果成功,你应该能看到一些转换后的行被添加到 BigQuery。
EventBridge 计划
现在,我们已经设置好任务在 ECS 集群中运行。但是我们感兴趣的是让它按计划运行。这就是 EventBridge 发挥作用的地方。
EventBridge 将设置我们的计划事件,我们也可以使用 CLI 来设置它。然而,在设置计划之前,我们首先需要创建一个新的角色。
这就是在使用 AWS 时的现实,每个服务都需要有相互交互的权限。
在这种情况下,EventBridge 需要权限代表我们调用 ECS 集群。
在仓库中,你有一个名为trust-policy-for-eventbridge.json的文件,我已经放在那里,我们将使用这个文件来创建 EventBridge 角色。
将此粘贴到终端并运行。
aws iam create-role \
--role-name ecsEventsRole \
--assume-role-policy-document file://trust-policy-for-eventbridge.json
然后我们需要将策略附加到这个角色。
aws iam attach-role-policy \
--role-name ecsEventsRole \
--policy-arn arn:aws:iam::aws:policy/AmazonECS_FullAccess
我们需要至少让它能够执行ecs:RunTask,但我们已经赋予它完全访问权限。如果你更喜欢限制权限,你可以创建一个只包含必要权限的自定义策略。
现在,让我们设置规则,安排任务在每个 UTC 时间上午 5 点使用任务定义运行。这通常是我希望它为我处理数据的时间,这样如果失败了,我可以在早餐后再查看。
aws events put-rule \
--name "ETLPipelineDailyRun" \
--schedule-expression "cron(0 5 * * ? *)" \
--state ENABLED
你应该会收到一个包含RuleArn字段的对象。这只是为了确认它是否成功。
下一步是将规则与 ECS 任务定义关联。
aws events put-targets --rule "ETLPipelineDailyRun" \
--targets "[{\"Id\":\"1\",\"Arn\":\"arn:aws:ecs:REGION:ACCOUNT_NUMBER:cluster/etl-pipeline-cluster\",\"RoleArn\":\"arn:aws:iam::ACCOUNT_NUMBER:role/ecsEventsRole\",\"EcsParameters\":{\"TaskDefinitionArn\":\"arn:aws:ecs:REGION:ACCOUNT_NUMBER:task-definition/my-etl-task\",\"TaskCount\":1,\"LaunchType\":\"FARGATE\",\"NetworkConfiguration\":{\"awsvpcConfiguration\":{\"Subnets\":[\"SUBNET_ID\"],\"SecurityGroups\":[\"SECURITY_GROUP_ID\"],\"AssignPublicIp\":\"ENABLED\"}}}}]"
记得在这里设置你自己的值,包括区域、账户号码、子网和安全组。
使用我们之前获得的子网和安全组。你可以设置多个子网。
一旦你运行了命令,任务将会被安排在每天的 UTC 时间 5 点执行,你可以在 AWS 控制台的“定时任务”中找到它。
你也可以直接在控制台中设置你的定时任务,这样更简单,因为子网 ID 和安全组已经为你设置好了。
AWS Secrets Manager(可选)
所以,将你的 Google 凭证保存在根文件夹中并不是理想的做法,即使你已经限制了 Google 服务账户对数据集的访问权限。
在这里,我们可以选择将这些凭证移动到另一个 AWS 服务,然后从我们的容器中访问它。
为了使其工作,你需要将凭证文件移动到 Secrets Manager,调整代码以便它能够获取凭证进行身份验证,并确保任务角色有权限代表你访问 AWS Secrets Manager。
完成后,你可以将更新的 docker 镜像推送到之前设置的 ECR 仓库。
最终结果
现在,你已经在 AWS 上按计划运行了一个非常简单的 ETL 管道。目的是你可以在此基础上添加自己的数据转换。
希望这对任何过渡到简单、成本效益高且直接的方式在 ECS 上设置长期运行的数据转换脚本的人来说是有帮助的。
如果你遇到任何问题,请告诉我,以防我遗漏了什么。
❤
使用 AWS SageMaker 端点部署模型——逐步实现
这是一个关于创建 SageMaker 端点并调用它的四步教程。
·发表于Towards Data Science ·阅读时间:11 分钟·2024 年 8 月 30 日
--

图片来源:Ayla Verschueren来自Unsplash
在离线实验中,我们习惯于测试各种机器学习模型,训练和/或微调它们,然后用来进行预测(即推理)。现在假设我们希望超越单纯的离线实验,向我们的客户提供访问我们出色模型的权限,使他们也能用来进行预测。在这种情况下,我们可以将我们的模型“部署”到一个 SageMaker“端点”上。然后,我们的客户可以向已部署的端点发送请求,并接收实时预测。这些端点提供了一些好处,包括:
-
访问权限: 端点只是托管(或部署)模型的一个网址。因此,我们可以像使用任何其他网址一样使用它,发送请求(即负载)并接收响应(即模型预测)。
-
可扩展性: 一旦端点创建完成,Amazon/AWS 将负责提供必要的计算资源来服务我们的客户。例如,假设我的笔记本只能处理每秒 10 个请求,但我预期每秒会有 10,000 个客户请求。AWS 会扩展端点并提供足够的硬件来支持所有 10,000 个请求……
在 AWS EC2 上部署 Tiny-Llama

Tiny-Llama logo(来源:github.com/jzhang38/TinyLlama)
学习如何使用 AWS 和 FastAPI 部署一个真实的 ML 应用
·发布于 Towards Data Science ·13 分钟阅读·2024 年 1 月 12 日
--
介绍
我一直认为,即使是世界上最好的项目,如果人们不能使用它,它也没有太大价值。这就是为什么学习如何部署机器学习模型非常重要的原因。在本文中,我们将重点介绍如何在名为 EC2 的 AWS 实例上部署一个小型的大型语言模型——Tiny-Llama。
我为这个项目使用的工具列表:
-
Deepnote: 是一个基于云的笔记本,非常适合协作数据科学项目,适合原型设计
-
FastAPI: 是一个用于构建 Python API 的 Web 框架
-
AWS EC2: 是一种提供可观计算能力的云计算服务
-
Nginx: 是一个 HTTP 和反向代理服务器。我用它将 FastAPI 服务器连接到 AWS
-
GitHub: GitHub 是一个软件项目的托管服务
-
HuggingFace: 是一个托管和协作无限模型、数据集和应用的平台。
关于 Tiny Llama
部署大语言模型:vLLM 与量化
大语言模型加速的逐步指南
·发表于Towards Data Science ·9 分钟阅读·2024 年 4 月 5 日
--

大语言模型(LLMs)的部署
我们生活在一个令人惊叹的时代,大语言模型(如 ChatGPT、GPT-4 和 Claude)可以执行多个惊人的任务。在教育、医疗、艺术和商业等几乎所有领域,大语言模型都被用来提高服务的效率。在过去的一年中,许多杰出的开源大语言模型(如 Llama、Mistral、Falcon 和 Gemma)相继发布。这些开源 LLM 可以供所有人使用,但部署它们可能非常具有挑战性,因为它们可能非常慢,并且需要大量的 GPU 计算能力来实现实时部署。为简化大语言模型的部署,已创建了不同的工具和方法。
为了提供更快的推理速度,许多部署工具已被创建来服务大语言模型(LLMs),如 vLLM、c2translate、TensorRT-LLM 和 llama.cpp。同时,量化技术也被用来优化 GPU,以便加载非常大的语言模型。本文将解释如何通过 vLLM 和量化技术来部署大语言模型。
延迟与吞吐量
影响大型语言模型速度性能的主要因素包括 GPU 硬件要求和模型大小。模型越大,运行所需的 GPU 计算能力就越强。常用的基准度量标准来衡量大型语言模型的速度性能包括延迟和吞吐量。
延迟: 这是大型语言模型生成响应所需的时间,通常以秒或毫秒为单位进行测量。
吞吐量: 这是指每秒或每毫秒由大型语言模型生成的 token 数量。
安装所需的包
以下是运行大型语言模型所需的两个包:Hugging Face transformers和accelerate。
pip3 install transformers
pip3 install accelerate
什么是 Phi-2?
Phi-2是微软推出的一个最先进的基础模型,拥有 27 亿个参数。它通过多种数据源进行预训练,包括代码和教科书等内容。了解更多关于Phi-2的信息,点击这里。
使用 Hugging Face Transformers 基准测试 LLM 的延迟和吞吐量
生成的输出
Latency: 2.739394464492798 seconds
Throughput: 32.36171766303386 tokens/second
Generate a python code that accepts a list of numbers and returns the sum. [1, 2, 3, 4, 5]
A: def sum_list(numbers):
total = 0
for num in numbers:
total += num
return total
print(sum_list([1, 2, 3, 4, 5]))
逐步代码解析
第 6–10 行: 加载了Phi-2模型并对提示“生成一个接受数字列表并返回其和的 Python 代码”进行了分词。
第 12-18 行: 从模型生成了一个响应,并通过计算生成响应所需的时间获得了延迟。
第 21-23 行: 获取了生成响应的 token 总长度,将其除以延迟并计算出吞吐量。
该模型在 A1000(16GB GPU)上运行,达到了延迟为2.7 秒,吞吐量为32 tokens/秒。
使用 vLLM 部署大型语言模型
vLLM 是一个开源的 LLM 库,用于以低延迟和高吞吐量提供大型语言模型服务。
vLLM 的工作原理
Transformer 是大型语言模型的构建模块。Transformer 网络使用一种叫做注意力机制的机制,网络通过它来研究和理解单词的上下文。注意力机制由一系列数学计算矩阵组成,这些矩阵被称为注意力键和值。注意力键和值的交互所使用的内存影响着模型的速度。vLLM 引入了一种新的注意力机制——分页注意力(PagedAttention),它在生成 token 的过程中高效地管理了 Transformer 的注意力键和值的内存分配。vLLM 的内存效率在低延迟和高吞吐量下运行大型语言模型时证明非常有用。
这是 vLLM 工作原理的高层次解释。要了解更多深入的技术细节,请访问 vLLM 文档。
[## vLLM:使用分页注意力机制,轻松、快速且廉价地提供大型语言模型服务
GitHub | 文档 | 论文
安装 vLLM
pip3 install vllm==0.3.3
运行 Phi-2 与 vLLM
生成输出
Latency: 1.218436622619629seconds
Throughput: 63.15334836428132tokens/second
[1, 2, 3, 4, 5]
A: def sum_list(numbers):
total = 0
for num in numbers:
total += num
return total
numbers = [1, 2, 3, 4, 5]
print(sum_list(numbers))
逐步代码解析
第 1–3 行: 从 vLLM 导入了运行Phi-2所需的包。
第 5–8 行: 使用 vLLM 加载了Phi-2,定义了提示词并设置了运行模型的重要参数。
第 10–16 行: 使用llm.generate生成模型的响应,并计算延迟。
第 19–21 行: 获取从响应中生成的总 token 长度,将 token 的长度除以延迟以获得吞吐量。
第 23–24 行: 获取生成的文本。
我在同一个提示词下使用 vLLM 运行了Phi-2,“生成一个接受数字列表并返回其和的 Python 代码。” 在相同的 GPU(A1000,16GB GPU)上,vLLM 的延迟为1.2 秒,吞吐量为63 个 token/秒,而 Hugging Face transformers 的延迟为2.85 秒,吞吐量为32 个 token/秒。使用 vLLM 运行大语言模型与使用 Hugging Face 得出的结果相同,但具有更低的延迟和更高的吞吐量。
注意: 我为 vLLM 获得的度量指标(延迟和吞吐量)是 vLLM 性能的估算基准。模型生成速度受多种因素影响,例如输入提示词的长度和 GPU 的大小。根据官方 vLLM 报告,在生产环境中使用像 A100 这样强大的 GPU 运行 LLM 模型,vLLM 的吞吐量比 Hugging Face Transformers 高出 24 倍。
实时延迟和吞吐量基准测试
我计算 Phi-2 的延迟和吞吐量的方式是实验性的,我这样做是为了说明 vLLM 如何加速大语言模型的性能。在 LLM 的实际使用案例中,例如一个基于聊天的系统,模型在生成时会输出 token,测量延迟和吞吐量要复杂得多。
基于聊天的系统依赖于流式输出 token。一些主要的因素影响 LLM 的度量标准,如首次 token 生成时间(模型生成第一个 token 所需的时间)、每个输出 token 的时间(生成每个输出 token 所花费的时间)、输入序列长度、预期输出、预期的总输出 token 数以及模型大小。在基于聊天的系统中,延迟通常是首次 token 生成时间与每个输出 token 的时间乘以预期的总输出 token 数的组合。
输入序列长度越长,模型的响应速度越慢。一些实时运行 LLM 的方法包括将用户的输入请求或提示批处理,从而并发执行推理,这有助于提高吞吐量。一般来说,使用强大的 GPU 和高效的工具(如 vLLM)来提供 LLM 服务,可以提高实时的延迟和吞吐量。
在 Google Colab 上运行 vLLM 部署
编辑描述
大型语言模型的量化
量化是将机器学习模型从高精度转换为低精度的过程,通过将模型的权重压缩成更小的位数,通常是 8-bit 或 4-bit。像 vLLM 这样的部署工具对于在低延迟和高吞吐量下提供大型语言模型推理服务非常有用。由于 Phi-2 是一个参数较小的 LLM,具有 27 亿参数,因此我们能够方便地在 Google Colab 的 T4 GPU 上使用 Hugging Face 和 vLLM 运行它。然而,像 Mistral 7B 这样的 70 亿参数的模型无法在 Colab 上通过 Hugging Face 或 vLLM 运行。量化最适合管理大型语言模型的 GPU 硬件需求。当 GPU 资源有限且需要运行非常大的语言模型时,量化是将 LLM 加载到受限设备上的最佳方法。
BitsandBytes
这是一个 Python 库,内置了自定义量化函数,用于将模型的权重缩小为更低的位数(8-bit 和 4-bit)。
安装 BitsandBytes
pip3 install bitsandbytes
Mistral 7B 模型的量化
Mistral 7B 是 MistralAI 发布的一个拥有 70 亿参数的模型,是最先进的开源大型语言模型之一。我将逐步讲解如何使用不同的量化技术在 Google Colab 上的 T4 GPU 上运行 Mistral 7B。
使用 8-bit 精度进行量化:这是将机器学习模型的权重转换为 8-bit 精度的过程。BitsandBytes 已与 Hugging Face transformers 集成,以使用相同的 Hugging Face 代码加载语言模型,但进行了少量修改以实现量化。
第 1 行: 导入了运行模型所需的包,包括 BitsandBytesConfig 库。
第 3-4 行: 定义了量化配置,并将参数 load_in_8bit 设置为 true,以便以 8-bit 精度加载模型的权重。
第 7–9 行: 将量化配置传入加载模型的函数,设置参数device_map为bitsandbytes,以自动分配适当的 GPU 内存来加载模型。最后加载了分词器权重。
4 位精度量化:这是将机器学习模型的权重转换为4 位精度。
以 4 位精度加载Mistral 7B的代码与8 位精度的代码类似,除了少数几处更改:
-
将load_in_8bit改为load_in_4bit。
-
新增了一个参数bnb_4bit_compute_dtype,它被引入到BitsandBytesConfig中,用于以bfloat16执行模型的计算。bfloat16是用于加载模型权重以加速推理的计算数据类型。它可以同时与4 位和8 位精度一起使用。如果是8 位精度,您只需将参数从bnb_4bit_compute_dtype更改为bnb_8bit_compute_dtype。
NF4(4 位标准浮动)和双重量化
来自 QLoRA 的NF4(4 位标准浮动)是一种最优的量化方法,比标准的 4 位量化产生更好的结果。它与双重量化相结合,量化过程发生两次;第一次量化的量化权重传递到下一阶段的量化,产生模型权重的最佳浮动范围值。根据 QLoRA 论文的报告,NF4 与双重量化不会导致准确率下降。阅读更多关于 NF4 和双重量化的深入技术细节,请参考 QLoRA 论文:
我们提出了 QLoRA,一种高效的微调方法,能够显著减少内存使用,从而能够微调一个 65B 参数的模型……
第 4–9 行: 设置了额外的参数,BitsandBytesConfig:
-
load_4bit: 以 4 位精度加载模型的设置为真。
-
bnb_4bit_quant_type: 量化类型设置为 nf4。
-
bnb_4bit_use_double_quant: 双重量化设置为 True。
-
bnb_4_bit_compute_dtype: 使用bfloat16计算数据类型,以加速推理。
第 11–13 行: 加载了模型的权重和分词器。
模型量化的完整代码
生成的输出
<s> [INST] What is Natural Language Processing? [/INST] Natural Language Processing (NLP) is a subfield of artificial intelligence (AI) and
computer science that deals with the interaction between computers and human language. Its main objective is to read, decipher,
understand, and make sense of the human language in a valuable way. It can be used for various tasks such as speech recognition,
text-to-speech synthesis, sentiment analysis, machine translation, part-of-speech tagging, name entity recognition,
summarization, and question-answering systems. NLP technology allows machines to recognize, understand,
and respond to human language in a more natural and intuitive way, making interactions more accessible and efficient.</s>
量化是一种非常有效的方法,用于在小型 GPU 上优化大型语言模型的运行,并且可以应用于任何模型,如 Llama 70B、Falcon 40B 和 mpt-30b。根据LLM.int8 论文的报告,量化后,超大型语言模型在准确性下降方面的影响较小,远低于小型模型。量化最适用于超大型语言模型,对于小型模型而言,由于准确性的下降,效果并不好。
在 Google Colab 上运行 Mixtral 7B 量化
编辑描述
结论
在本文中,我提供了一个逐步的方法来衡量大型语言模型的速度性能,解释了 vLLM 是如何工作的,以及它如何被用来提升大型语言模型的延迟和吞吐量。最后,我解释了量化技术及其如何被用来在小规模 GPU 上加载大型语言模型。
通过以下方式联系我:
LinkedIn: www.linkedin.com/in/ayoola-olafenwa-003b901a9/
参考文献
[## vLLM:使用 PagedAttention 轻松、快速、低成本地提供 LLM 服务
GitHub | 文档 | 论文
blog.vllm.ai [## 使用 bitsandbytes、4 位量化和 QLoRA 使 LLM 更加易用
我们正在通过开源和开放科学的方式推进并普及人工智能。
huggingface.co [## 理解 LLM 推理性能基准
本指南帮助你解读 LLM 性能指标,直接对比延迟、吞吐量和成本。
www.baseten.co [## 什么是量化的 LLM?
发现量化 LLMs 的强大功能!了解模型量化如何减少模型大小,提升硬件使用效率,以及…
使用 SageMaker 异步推理部署大型语言模型
为近实时应用程序排队请求
·发布在Towards Data Science ·阅读时间:10 分钟·2024 年 1 月 27 日
--

图片来自Unsplash,由Gerard Siderius提供
大型语言模型(LLMs)继续迅速流行,托管和部署它们进行推理的方式也在不断增加。有关 LLM 托管的挑战,尤其是由于模型的大小以及确保其在部署硬件上得到最佳使用,已经有很多文献记录。LLM 的使用案例也各不相同。有些可能需要基于实时的响应时间,而其他则具有更接近实时的延迟要求。
对于后者以及更多离线推理的使用案例,SageMaker 异步推理是一个不错的选择。正如其名所示,异步推理专注于一种更接近实时的工作负载,在这种情况下,延迟不必严格要求极低,但仍然需要一个可以根据需要调用并扩展的活动端点。特别是在大型语言模型中,这类工作负载正变得越来越流行,使用案例包括内容编辑/生成、摘要等。这些工作负载不需要毫秒级响应,但仍然要求能及时推理,按需调用,而不是完全离线的方式,如 SageMaker 批处理转换。
在这个例子中,我们将看看如何使用HuggingFace Text…
将 LLM 应用程序部署到 AWS,采用开源自助服务方式
部署 LlamaIndex RAGs 到 AWS ECS Fargate 的逐步指南
·发表于Towards Data Science ·12 分钟阅读·2024 年 1 月 8 日
--

由作者使用 DALL-E 3 生成的图像
当 LLM 应用程序开发为使用第三方托管的 LLM(如 OpenAI)时,不需要 MLOps 的额外开销。这类容器化的 LLM 驱动应用程序或微服务可以通过 DevOps 实践进行部署。本文将探索如何将 LLM 应用程序部署到云提供商(如 AWS),并完全自动化基础设施和应用程序管道。LlamaIndex 为社区提供了现成的RAGs 聊天机器人。我们将以 RAGs 作为示例应用程序进行部署。
IaC 自助服务
IaC(基础设施即代码)自动化基础设施的配置,确保配置的一致性和可重复性。实现 IaC 的工具有很多。本文将聚焦于HashiCorp 的 Terraform,主要是因为 Terraform 具有跨云平台的适应性。
IaC 自助服务的主要目的是赋能开发者,使其能够对管道拥有更多的访问权限、控制权和所有权,从而提高生产力。
对此感兴趣的朋友,我大约一年前写过一篇关于 DevOps 自助服务模型的五部分系列文章,详细讲解了与 DevOps 自助服务模型相关的各个方面。
高级部署架构图
使用 TensorRT LLM 将 LLM 部署到生产环境
加速推理性能的指南
·发表于 Towards Data Science ·14 分钟阅读·2024 年 2 月 22 日
--

图片来源:作者 — 使用 Stable Diffusion XL 创建
简介
开源的大型语言模型已经兑现了其炒作的承诺。许多使用 GPT-3.5 或 GPT-4 进行生产的公司已经意识到,这些模型在成本角度上根本不可扩展。因此,企业正在寻找更好的开源替代方案。最近的模型,如 Mixtral 和 Llama 2,在输出质量方面表现出色。但将这些模型扩展到支持数千个并发用户仍然是一个挑战。
虽然像 vLLM 和 TGI 这样的框架在提升推理方面是一个很好的起点,但它们缺乏一些优化,导致它们在生产环境中的扩展性较差。
这就是 TensorRT-LLM 的作用所在。TensorRT-LLM 是 Nvidia 设计的一个开源框架,旨在提升大型语言模型在生产环境中的性能。许多大公司,如 Anthropic、OpenAI、Anyscale 等,已经在使用这个框架为数百万用户提供 LLM 服务。
了解 TensorRT-LLM
与其他推理技术不同,TensorRT LLM 并不使用原始权重来提供模型。相反,它会编译模型并优化内核,以便在 Nvidia GPU 上实现高效服务。运行编译后的模型的性能远高于运行原始模型。这也是 TensorRT LLM 非常快速的主要原因之一。

原始模型被编译成优化后的二进制文件
原始模型权重以及优化选项(如量化级别、张量并行性、流水线并行性等)被传递给编译器。编译器根据这些信息输出一个针对特定 GPU 优化的模型二进制文件。
一个需要注意的重要事项是,整个模型编译过程必须在 GPU 上进行。生成的已编译模型是专门针对运行时使用的 GPU 进行优化的。例如,如果你在 A40 GPU 上编译模型,就不能在 A100 GPU 上运行该模型。因此,无论在编译时使用的是哪个 GPU,推理时必须使用相同的 GPU。
TensorRT LLM 并不支持所有大型语言模型。原因是每种模型架构不同,而 TensorRT 进行的是深度图优化。因此,大多数流行的模型如 Mistral、Llama 和 Qwen 都得到了支持。如果你对支持的模型的完整列表感兴趣,可以查看TensorRT LLM Github 仓库。
使用 TensorRT-LLM 的好处
TensorRT LLM Python 包使开发者能够在无需了解 C++ 或 CUDA 的情况下,以最高性能运行 LLM。此外,它还附带了一些方便的功能,如标记流、分页注意和 KV 缓存。让我们更深入地了解其中的一些主题。
- 分页注意
大型语言模型需要大量内存来存储每个标记的键和值。随着输入序列变长,这种内存使用会急剧增加。
使用常规注意机制时,一个序列的键和值必须连续存储。因此,即使你在序列内存分配中间释放了空间,你也无法将该空间用于其他序列。这会导致碎片化和浪费。

“注意”:所有的标记必须保持在一个连续的内存块中,即使存在空闲空间。
使用分页注意时,每个键/值的页面可以放置在内存中的任何位置,不需要连续存储。因此,如果你释放了中间的一些页面,这部分空间现在可以被用于其他序列。
这可以防止内存碎片化,并提高内存利用率。生成输出序列时,页面可以根据需要动态分配和释放。

“分页注意”:可以通过删除整个页面来从内存中删除之前生成的标记。这为新序列腾出了空间。
2. 高效的 KV 缓存
KV 缓存代表“键值缓存”,用于缓存大型语言模型(LLM)的一部分,以提高推理速度并减少内存使用。
LLM 拥有数十亿个参数,使得它们在进行推理时变得缓慢且内存密集。KV 缓存通过缓存 LLM 的层输出和激活值,避免了每次推理时都需要重新计算这些值,从而帮助解决这个问题。
其工作原理如下:
-
在推理过程中,当 LLM 执行每一层时,输出会被缓存到一个带有唯一键的键值存储中。
-
当后续推理使用相同的层输入时,系统不会重新计算该层,而是通过键从缓存中获取输出。
-
这样可以避免冗余计算并减少激活内存,从而提高推理速度和内存效率。
好了,理论部分讲完了。让我们真正部署一个模型吧!
实践 Python 教程
使用 TensorRT-LLM 部署模型有两个步骤:
-
编译模型
-
将编译好的模型部署为 REST API 端点
步骤 1:编译模型
在本教程中,我们将使用 Mistral 7B Instruct v0.2。如前所述,编译阶段需要 GPU。我发现编译模型最简单的方法是在 Google Colab 笔记本上进行。
[## Mistral 7B 编译器 Google Colaboratory
编辑描述
TensorRT LLM 主要支持高端 Nvidia GPU。我在 A100 40GB GPU 上运行了 Google Colab,并将在部署时使用相同的 GPU。
!git clone https://github.com/NVIDIA/TensorRT-LLM.git
%cd TensorRT-LLM/examples/llama
- 克隆 TensorRT-LLM git 仓库。该仓库包含了我们需要的所有模块和脚本,用于编译模型。
!pip install tensorrt_llm -U --pre --extra-index-url https://pypi.nvidia.com
!pip install huggingface_hub pynvml mpi4py
!pip install -r requirements.txt
- 安装必要的 Python 依赖项。
from huggingface_hub import snapshot_download
from google.colab import userdata
snapshot_download(
"mistralai/Mistral-7B-Instruct-v0.2",
local_dir="tmp/hf_models/mistral-7b-instruct-v0.2",
max_workers=4
)
-
从 hugging face 下载 Mistral 7B Instruct v0.2 模型权重,并将其存储在本地目录
tmp/hf_models/mistral-7b-instruct-v0.2中。 -
如果你查看 Colab 中
tmp/hf_models目录,你应该能看到模型权重。
!python convert_checkpoint.py --model_dir ./tmp/hf_models/mistral-7b-instruct-v0.2 \
--output_dir ./tmp/trt_engines/1-gpu/ \
--dtype float16
-
原始模型权重无法被编译。相反,它们必须转换为特定的 tensorRT LLM 格式。
-
convert_checkpoint.py脚本将原始的 Mistral 权重转换为兼容的格式。 -
--model_dir是原始模型权重的路径。 -
--output_dir是转换后的权重的路径。
!trtllm-build --checkpoint_dir ./tmp/trt_engines/1-gpu/ \
--output_dir ./tmp/trt_engines/compiled-model/ \
--gpt_attention_plugin float16 \
--gemm_plugin float16 \
--max_input_len 32256
-
trtllm-build命令用于编译模型。在此阶段,你还可以传入各种优化标志。为了简化,我没有使用任何额外的优化。 -
--checkpoint_dir是 转换后的 模型权重的路径。 -
--output_dir是编译好的模型保存的路径。 -
Mistral 7B Instruct v0.2 支持 32K 的上下文长度。我通过
--max_input_length标志设置了这个上下文长度。
注意:编译模型可能需要 15 到 30 分钟。
一旦模型编译完成,你可以将编译好的模型上传到 hugging face hub。为了将文件上传到 hugging face hub,你需要一个有效的访问令牌,并且该令牌具有 WRITE 权限。
import os
from huggingface_hub import HfApi
for root, dirs, files in os.walk(f"tmp/trt_engines/compiled-model", topdown=False):
for name in files:
filepath = os.path.join(root, name)
filename = "/".join(filepath.split("/")[-2:])
print("uploading file: ", filename)
api = HfApi(token=userdata.get('HF_WRITE_TOKEN'))
api.upload_file(
path_or_fileobj=filepath,
path_in_repo=filename,
repo_id="<your-repo-id>/mistral-7b-v0.2-trtllm"
)
-
这段代码将编译好的模型,即.engine文件,上传到 Hugging Face 并与您的用户 ID 关联。
-
在代码中将
替换为您的 Hugging Face 仓库,这通常是您的 Hugging Face 用户 ID。
太棒了!这就完成了模型编译部分。接下来是部署步骤。
第 2 步:部署编译好的模型
部署这个编译模型有很多种方式。您可以使用像FastAPI这样的简单工具,也可以使用更复杂的工具,如triton 推理服务器。
使用像 FastAPI 这样的工具时,开发者需要自行设置 API 服务器,编写 Dockerfile,并正确配置 CUDA。管理这些内容可能非常麻烦,并且破坏了整体的开发体验。
为了避免这些问题,我决定使用一个简单的开源工具叫做Truss。Truss 使得开发者能够轻松地将模型打包并支持 GPU,可以在任何云环境中运行。它具有许多很棒的功能,使得模型容器化变得轻松:
-
开箱即用的 GPU 支持,无需处理 CUDA。
-
自动生成 Dockerfile。无需自己编写。
-
生产级 API 服务器
-
简单的 Python 接口
使用 Truss 的主要好处是,您可以轻松地将一个支持 GPU 的模型容器化,并部署到任何云环境。

构建 Truss 一次,随时部署。
创建 Truss
创建或打开一个 Python 虚拟环境,Python 版本需≥ 3.8,并安装以下依赖:
pip install --upgrade truss
(可选) 如果您想从头创建 Truss 项目,可以运行以下命令:
truss init mistral-7b-tensort-llm
系统会提示您为模型命名。任何名称,例如 Mistral 7B TensorRT LLM 都可以。运行上述命令会自动生成部署 Truss 所需的文件。
为了加速这个过程,我有一个包含所需文件的 Github 仓库。请克隆下面的 Github 仓库:
[## GitHub - htrivedi99/mistral-7b-tensorrt-llm-truss
通过在 GitHub 上创建账户,贡献代码给 htrivedi99/mistral-7b-tensorrt-llm-truss 项目。
这就是 mistral-7b-tensorrt-llm-truss 目录结构应该呈现的样子:
├── mistral-7b-tensorrt-llm-truss
│ ├── config.yaml
│ ├── model
│ │ ├── __init__.py
│ │ └── model.py
| | └── utils.py
| ├── requirements.txt
以下是上述文件用途的简要说明:
config.yaml用于设置模型的各种配置,包括其资源、依赖项、环境变量等。您可以在这里指定模型名称、要安装的 Python 依赖项以及要安装的系统软件包。
2. model/model.py是 Truss 的核心。它包含了将在 Truss 服务器上执行的 Python 代码。在model.py中有两个主要方法:load()和predict()。
-
load方法是我们从 Hugging Face 下载已编译模型并初始化 TensorRT LLM 引擎的地方。 -
predict方法接收 HTTP 请求并调用模型。
3. model/utils.py包含了一些供model.py使用的辅助函数。我并不是自己编写的utils.py文件,而是直接从TensorRT LLM 仓库中获取的。
4. requirements.txt包含了运行已编译模型所需的 Python 依赖项。
更深入的代码解释:
model.py包含了要执行的主要代码,让我们深入研究一下这个文件。首先来看一下load函数。
import subprocess
subprocess.run(["pip", "install", "tensorrt_llm", "-U", "--pre", "--extra-index-url", "https://pypi.nvidia.com"])
import torch
from model.utils import (DEFAULT_HF_MODEL_DIRS, DEFAULT_PROMPT_TEMPLATES,
load_tokenizer, read_model_name, throttle_generator)
import tensorrt_llm
import tensorrt_llm.profiler
from tensorrt_llm.runtime import ModelRunnerCpp, ModelRunner
from huggingface_hub import snapshot_download
STOP_WORDS_LIST = None
BAD_WORDS_LIST = None
PROMPT_TEMPLATE = None
class Model:
def __init__(self, **kwargs):
self.model = None
self.tokenizer = None
self.pad_id = None
self.end_id = None
self.runtime_rank = None
self._data_dir = kwargs["data_dir"]
def load(self):
snapshot_download(
"htrivedi99/mistral-7b-v0.2-trtllm",
local_dir=self._data_dir,
max_workers=4,
)
self.runtime_rank = tensorrt_llm.mpi_rank()
model_name, model_version = read_model_name(f"{self._data_dir}/compiled-model")
tokenizer_dir = "mistralai/Mistral-7B-Instruct-v0.2"
self.tokenizer, self.pad_id, self.end_id = load_tokenizer(
tokenizer_dir=tokenizer_dir,
vocab_file=None,
model_name=model_name,
model_version=model_version,
tokenizer_type="llama",
)
runner_cls = ModelRunner
runner_kwargs = dict(engine_dir=f"{self._data_dir}/compiled-model",
lora_dir=None,
rank=self.runtime_rank,
debug_mode=False,
lora_ckpt_source="hf",
)
self.model = runner_cls.from_dir(**runner_kwargs)
发生了什么:
-
在文件顶部,我们导入了必要的模块,特别是
tensorrt_llm。 -
接下来,在
load函数内部,我们使用snapshot_download函数下载已编译的模型。我的编译模型位于以下仓库 ID:htrivedi99/mistral-7b-v0.2-trtllm。如果你将已编译的模型上传到其他地方,请相应地更新此值。 -
然后,我们使用
model/utils.py中提供的load_tokenizer函数下载模型的分词器。 -
最后,我们使用 TensorRT LLM 通过
ModelRunner类加载已编译的模型。
很棒,让我们也看看predict函数。
def predict(self, request: dict):
prompt = request.pop("prompt")
max_new_tokens = request.pop("max_new_tokens", 2048)
temperature = request.pop("temperature", 0.9)
top_k = request.pop("top_k",1)
top_p = request.pop("top_p", 0)
streaming = request.pop("streaming", False)
streaming_interval = request.pop("streaming_interval", 3)
batch_input_ids = self.parse_input(tokenizer=self.tokenizer,
input_text=[prompt],
prompt_template=None,
input_file=None,
add_special_tokens=None,
max_input_length=1028,
pad_id=self.pad_id,
)
input_lengths = [x.size(0) for x in batch_input_ids]
outputs = self.model.generate(
batch_input_ids,
max_new_tokens=max_new_tokens,
max_attention_window_size=None,
sink_token_length=None,
end_id=self.end_id,
pad_id=self.pad_id,
temperature=temperature,
top_k=top_k,
top_p=top_p,
num_beams=1,
length_penalty=1,
repetition_penalty=1,
presence_penalty=0,
frequency_penalty=0,
stop_words_list=STOP_WORDS_LIST,
bad_words_list=BAD_WORDS_LIST,
lora_uids=None,
streaming=streaming,
output_sequence_lengths=True,
return_dict=True)
if streaming:
streamer = throttle_generator(outputs, streaming_interval)
def generator():
total_output = ""
for curr_outputs in streamer:
if self.runtime_rank == 0:
output_ids = curr_outputs['output_ids']
sequence_lengths = curr_outputs['sequence_lengths']
batch_size, num_beams, _ = output_ids.size()
for batch_idx in range(batch_size):
for beam in range(num_beams):
output_begin = input_lengths[batch_idx]
output_end = sequence_lengths[batch_idx][beam]
outputs = output_ids[batch_idx][beam][
output_begin:output_end].tolist()
output_text = self.tokenizer.decode(outputs)
current_length = len(total_output)
total_output = output_text
yield total_output[current_length:]
return generator()
else:
if self.runtime_rank == 0:
output_ids = outputs['output_ids']
sequence_lengths = outputs['sequence_lengths']
batch_size, num_beams, _ = output_ids.size()
for batch_idx in range(batch_size):
for beam in range(num_beams):
output_begin = input_lengths[batch_idx]
output_end = sequence_lengths[batch_idx][beam]
outputs = output_ids[batch_idx][beam][
output_begin:output_end].tolist()
output_text = self.tokenizer.decode(outputs)
return {"output": output_text}
发生了什么:
-
predict函数接受一些模型输入,例如prompt、max_new_tokens、temperature等。我们使用request.pop方法在函数顶部提取所有这些值。 -
接下来,我们使用
self.parse_input辅助函数将提示格式化为 TensorRT LLM 所需的格式。 -
然后,我们调用 LLM 模型,使用
self.model.generate函数生成输出。generate 函数接受多种参数,帮助控制 LLM 的输出。 -
我还添加了一些代码,通过生成
generator对象来启用流式传输。如果流式传输被禁用,分词器会直接解码 LLM 的输出,并将其作为 JSON 对象返回。
太棒了!这部分代码就讲完了。让我们来容器化它。
容器化模型:
为了在云端运行我们的模型,我们需要将其容器化。Truss 会帮我们创建 Dockerfile 并将所有内容打包,所以我们不需要做太多。
在mistral-7b-tensorrt-llm-truss目录外创建一个名为main.py的文件。将以下代码粘贴到文件中:
import truss
from pathlib import Path
tr = truss.load("./mistral-7b-tensorrt-llm-truss")
command = tr.docker_build_setup(build_dir=Path("./mistral-7b-tensorrt-llm-truss"))
print(command)
运行main.py文件并查看mistral-7b-tensorrt-llm-truss目录。你应该会看到一堆文件被自动生成。我们不需要担心这些文件的含义,这只是 Truss 在发挥作用。
接下来,让我们使用 docker 构建我们的容器。按顺序运行下面的命令:
docker build mistral-7b-tensorrt-llm-truss -t mistral-7b-tensorrt-llm-truss:latest
docker tag mistral-7b-tensorrt-llm-truss <docker_user_id>/mistral-7b-tensorrt-llm-truss
docker push <docker_user_id>/mistral-7b-tensorrt-llm-truss
太棒了!我们准备好在云端部署模型了!
在 GKE 上部署模型
在本节中,我们将在 Google Kubernetes Engine 上部署模型。如果你还记得,在模型编译步骤中我们使用了 A100 40GB GPU 运行 Google Colab。为了使 TensorRT LLM 正常工作,我们需要在完全相同的 GPU 上进行推理部署。
我不会深入讲解如何设置 GKE 集群,因为这不在本文的范围内。但我会提供我在集群中使用的规格。以下是规格:
-
1 个节点,标准 Kubernetes 集群(非自动驾驶)
-
1.28.5 gke kubernetes 版本
-
1 个 Nvidia A100 40GB GPU
-
a2-highgpu-1g 机器(12 vCPU,85 GB 内存)
-
谷歌托管的 GPU 驱动程序安装(否则我们需要手动安装 Cuda 驱动程序)
-
所有这些将在一个临时实例上运行
一旦集群配置完成,我们就可以启动它并连接到集群。在集群激活并成功连接后,创建以下 Kubernetes 部署:
apiVersion: apps/v1
kind: Deployment
metadata:
name: mistral-7b-v2-trt
namespace: default
spec:
replicas: 1
selector:
matchLabels:
component: mistral-7b-v2-trt-layer
template:
metadata:
labels:
component: mistral-7b-v2-trt-layer
spec:
containers:
- name: mistral-container
image: htrivedi05/mistral-7b-v0.2-trt:latest
ports:
- containerPort: 8080
resources:
limits:
nvidia.com/gpu: 1
nodeSelector:
cloud.google.com/gke-accelerator: nvidia-tesla-a100
---
apiVersion: v1
kind: Service
metadata:
name: mistral-7b-v2-trt-service
namespace: default
spec:
type: ClusterIP
selector:
component: mistral-7b-v2-trt-layer
ports:
- port: 8080
protocol: TCP
targetPort: 8080
这是一个标准的 Kubernetes 部署,它运行一个带有镜像 htrivedi05/mistral-7b-v0.2-trt:latest 的容器。如果你在前一节创建了自己的镜像,可以使用自己的镜像。否则,随意使用我的镜像。
你可以通过运行以下命令来创建部署:
kubectl create -f mistral-deployment.yaml
Kubernetes pod 配置需要几分钟时间。一旦 pod 启动,之前编写的加载函数将被执行。你可以通过运行以下命令查看 pod 的日志:
kubectl logs <pod-name>
模型加载完成后,你将在 pod 日志中看到类似 Completed model.load() execution in 449234 ms 的内容。为了通过 HTTP 向模型发送请求,我们需要进行端口转发。你可以使用以下命令来实现:
kubectl port-forward svc/mistral-7b-v2-trt-service 8080
太棒了!我们终于可以开始向模型发送请求了!打开任意 Python 脚本并运行以下代码:
import requests
data = {"prompt": "What is a mistral?"}
res = requests.post("http://127.0.0.1:8080/v1/models/model:predict", json=data)
res = res.json()
print(res)
你将看到如下输出:
{"output": "A Mistral is a strong, cold wind that originates in the Rhone Valley in France. It is named after the Mistral wind system, which is associated with the northern Mediterranean region. The Mistral is known for its consistency and strength, often blowing steadily for days at a time. It can reach speeds of up to 130 kilometers per hour (80 miles per hour), making it one of the strongest winds in Europe. The Mistral is also known for its clear, dry air and its role in shaping the landscape and climate of the Rhone Valley."}
当令牌被流式传输时,TensorRT LLM 的性能是显而易见的。以下是如何做到这一点的示例:
data = {"prompt": "What is mistral wind?", "streaming": True, "streaming_interval": 3}
res = requests.post("http://127.0.0.1:8080/v1/models/model:predict", json=data, stream=True)
for content in res.iter_content():
print(content.decode("utf-8"), end="", flush=True)
这个 mistral 模型有一个相当大的上下文窗口,因此可以随意尝试不同的提示。
性能基准测试
仅通过查看流式传输的令牌,你可能就能看出 TensorRT LLM 的速度非常快。不过,我希望得到真实的数字,以捕捉使用 TensorRT LLM 带来的性能提升。我运行了一些自定义基准测试并得到了以下结果:
小提示:

Hugging Face 与 TensorRT LLM 在小提示下的基准测试
中等提示:

Hugging Face 与 TensorRT LLM 在中等提示下的基准测试
大提示:

Hugging Face 与 TensorRT LLM 在大提示下的基准测试
结论
在这篇博客文章中,我的目标是演示如何使用 TensorRT LLM 实现最先进的推理。我们从编译 LLM 到在生产环境中部署模型,涵盖了所有内容。
虽然 TensorRT LLM 比其他推理优化器更为复杂,但其性能不言自明。该工具提供最先进的 LLM 优化,同时完全开源,并设计用于商业用途。这个框架仍处于早期阶段,并在积极开发中。我们今天看到的性能将在未来几年得到提升。
希望你在这篇文章中找到了有价值的内容。感谢阅读!
喜欢这个故事吗?
考虑 免费订阅 吧。
[## 每当 Het Trivedi 发布新内容时,您将收到电子邮件。
每当 Het Trivedi 发布新内容时,您将收到电子邮件。通过注册,如果您尚未拥有 Medium 账号,系统将为您创建一个…
medium.com](https://medium.com/@het.trivedi05/subscribe?source=post_page-----ed36e620dac4--------------------------------)
图片
除非另有说明,所有图片均由作者创建。
使用苹果的 MLX 框架在本地部署 LLM
对新的深度学习库 MLX 的技术深入探讨
·发布于Towards Data Science ·9 分钟阅读·2024 年 1 月 20 日
--

图片由作者提供(使用 DALL-E 3)
这是什么内容?
2023 年 12 月,苹果发布了他们的新MLX 深度学习框架,这是一个为苹果自家芯片设计的机器学习数组框架,由他们的机器学习研究团队开发。本文将探讨该框架,并演示如何在 MacBook Pro(MBP)上本地部署 Mistral-7B 模型。我们将设置一个本地聊天界面,与部署的模型进行交互,并测试其推理性能,即每秒生成的 tokens 数量。此外,我们还将深入了解 MLX API,理解可用的控制手段,用于调整模型行为并影响生成的文本。
一如既往,代码可在公开的 GitHub 仓库中获取:github.com/marshmellow77/mlx-deep-dive
为什么这很重要?
苹果的新机器学习框架 MLX,在其统一内存架构方面提供了相较于其他深度学习框架的显著优势,专为苹果自家芯片上的机器学习设计。与传统框架(如 PyTorch 和 Jax)不同,后者需要在 CPU 和 GPU 之间进行昂贵的数据复制,MLX 则将数据保存在可供两者访问的共享内存中。这个设计消除了数据复制的开销……
通过 vLLM 使用 SageMaker 端点部署 Llama 模型
利用 AWS 的 MLOps 平台为 LLM 模型提供服务
·发表于Towards Data Science ·8 分钟阅读·2024 年 9 月 12 日
--

需要推理端点的 MLOps 工作流中的实例(由作者创建)。
在任何机器学习项目中,目标是训练一个可以供他人使用,以得出良好预测的模型。为了实现这一目标,需要提供模型进行推理。在这个工作流中,几个部分需要这个推理端点,即在模型评估时,模型在发布到开发、预发布环境,最后到生产环境供最终用户使用之前。
在本文中,我将演示如何使用 AWS 的 SageMaker 端点和其 DJL 镜像,部署最新的 LLM 和服务技术,即 Llama 和 vLLM。这些组件是什么,它们如何组成推理端点?

各个组件如何协同工作,服务于 AWS 中的模型。SageMaker 端点是 GPU 实例,DJL 是模板 Docker 镜像,vLLM 是模型服务器(由作者创建)。
SageMaker是 AWS 提供的一项服务,包含一整套工具和服务,用于管理机器学习生命周期。其推理服务被称为 SageMaker 端点。在底层,它本质上是由 AWS 自主管理的虚拟机。
DJL(Deep Java Library)是由 AWS 开发的开源库,用于开发 LLM 推理 Docker 镜像,包括 vLLM[2]。该镜像用于…
Depth Anything — 单目深度估计的基础模型
🚀Sascha 的论文俱乐部
Depth Anything:释放大规模未标记数据的力量,L. Yang 等人
·发布于Towards Data Science ·阅读时间:11 分钟·2024 年 3 月 20 日
--

图片来自Sascha Kirch的出版物
单目深度估计,即从二维图像预测三维空间中的距离。正如几乎所有关于深度估计的论文所指出的那样,这个“病态且固有模糊的问题”是计算机视觉和机器人学中的一个基础性问题。同时,基础模型主导了基于深度学习的自然语言处理(NLP)和计算机视觉领域。若我们能够将它们的成功应用于深度估计,岂不是太棒了?
在今天的论文讲解中,我们将深入探讨 Depth Anything,这是一种用于单目深度估计的基础模型。我们将了解它的架构、训练过程中使用的技巧以及它如何用于度量深度估计。
论文: Depth Anything: Unleashing the Power of Large-Scale Unlabeled Data,Lihe Yang 等,2024 年 1 月 19 日
会议: CVPR2024
类别: 基础模型,单目深度估计
其他讲解:
[BYOL] — [CLIP] — [GLIP] — [Segment Anything] — [DINO] — [DDPM]
设计一个易于使用的深度学习框架
作为开源贡献者,我学到的三条软件设计原则
·发布于Towards Data Science ·9 分钟阅读·2024 年 4 月 10 日
--

深度学习框架的变迁非常迅速。如果你将今天人们使用的深度学习框架与八年前的框架进行比较,你会发现整个生态环境已经完全不同。曾经有 Theano、Caffe2 和 MXNet,它们都已经过时。今天最流行的框架,如 TensorFlow 和 PyTorch,刚刚向公众发布。
经过这些年的发展,Keras 作为一个支持不同后端(包括 TensorFlow、PyTorch 和 JAX)的高级用户库得以存活。作为 Keras 的贡献者,我学到了团队是多么重视软件的用户体验,并且他们如何通过遵循一些简单而强大的设计原则,确保了良好的用户体验。
在本文中,我将分享过去几年我通过为 Keras 做贡献而学到的三条最重要的软件设计原则,这些原则可能适用于所有类型的软件,并帮助你在开源社区中通过自己的项目产生影响。
为什么用户体验对开源软件很重要
在深入讨论主要内容之前,让我们快速讨论一下为什么用户体验如此重要。我们可以通过 PyTorch 与 TensorFlow 的案例来了解这一点。
这些框架由两家科技巨头 Meta 和 Google 开发,它们具有截然不同的文化优势。Meta 擅长产品,而 Google 擅长工程。因此,Google 的框架如 TensorFlow 和 JAX 运行速度最快,技术上也优于 PyTorch,因为它们在稀疏张量和分布式训练方面做得很好。然而,PyTorch 仍然从 TensorFlow 手中夺走了半壁江山,因为它优先考虑用户体验,而不是软件的其他方面。
更好的用户体验对于构建模型并将其传播给工程师的研究人员来说至关重要,因为工程师们不总是愿意将他们从研究人员那里获得的模型转换为另一个框架。他们会围绕 PyTorch 构建新的软件,以简化他们的工作流程,这样就会在 PyTorch 周围建立一个软件生态系统。
TensorFlow 也犯了一些错误,导致其用户流失。TensorFlow 的整体用户体验良好。然而,其 GPU 支持的安装指南多年来一直存在问题,直到 2022 年才得以修复。TensorFlow 2 破坏了向后兼容性,导致用户在迁移过程中损失了数百万美元。
所以,我们从这里学到的教训是,尽管技术上具有优势,用户体验决定了开源用户会选择哪款软件。
所有深度学习框架都在用户体验上投入了大量资金
所有深度学习框架——TensorFlow、PyTorch 和 JAX——都在用户体验上投入了大量资金。一个很好的证据是,它们的代码库中 Python 的比例相对较高。
深度学习框架的所有核心逻辑,包括张量操作、自动微分、编译和分布式处理,都是用 C++实现的。为什么他们要向用户暴露一套 Python API?这仅仅是因为用户喜欢 Python,并且他们想要打磨用户体验。
投资用户体验的回报率很高
想象一下,要让你的深度学习框架比其他框架稍微快一些需要多少工程努力。很多。
然而,为了更好的用户体验,只要你遵循一定的设计过程和一些原则,就能够实现这一目标。为了吸引更多用户,用户体验和框架的计算效率同样重要。因此,投资用户体验的回报率很高。
这三条原则
我将分享我在为 Keras 贡献代码过程中学到的三条重要软件设计原则,并附上来自不同框架的好坏代码示例。
原则 1:设计端到端工作流
当我们考虑设计一款软件的 API 时,可能会像这样。
class Model:
def __call__(self, input):
"""The forward call of the model.
Args:
input: A tensor. The input to the model.
"""
pass
定义类并添加文档。现在,我们知道了所有的类名、方法名和参数。然而,这并不能帮助我们更好地理解用户体验。
我们应该做的是类似这样的事情。
input = keras.Input(shape=(10,))
x = layers.Dense(32, activation='relu')(input)
output = layers.Dense(10, activation='softmax')(x)
model = keras.models.Model(inputs=input, outputs=output)
model.compile(
optimizer='adam', loss='categorical_crossentropy'
)
我们希望写出整个用户使用软件的工作流。理想情况下,它应该是一个关于如何使用软件的教程。它提供了关于用户体验的更多信息。与仅仅写出类和方法相比,它可以帮助我们在设计阶段发现更多的用户体验问题。
让我们看另一个例子。这就是我在实现 KerasTuner 时,通过遵循这一原则发现用户体验问题的方式。
当使用 KerasTuner 时,用户可以使用这个 RandomSearch 类来选择最佳模型。我们有度量和目标作为参数。默认情况下,目标等于验证损失。因此,它帮助我们找到具有最小验证损失的模型。
class RandomSearch:
def __init__(self, ..., metrics, objective="val_loss", ...):
"""The initializer.
Args:
metrics: A list of Keras metrics.
objective: String or a custom metric function. The
name of the metirc we want to minimize.
"""
pass
再次强调,它并没有提供太多关于用户体验的信息。所以,现在一切看起来都没问题。
然而,如果我们编写一个端到端的工作流,如下所示,它暴露了更多问题。用户试图定义一个名为 custom_metric 的自定义度量函数。目标变得不再那么直观。我们现在应该传递什么给目标参数呢?
tuner = RandomSearch(
...,
metrics=[custom_metric],
objective="val_???",
)
它应该只是"val_custom_metric"。只需使用前缀"val_"和度量函数的名称。这样并不够直观。我们希望改进它,而不是强迫用户学习这个。通过编写这个工作流,我们很容易发现了一个用户体验问题。
如果你通过包含 custom_metric 函数的实现来更加全面地编写设计,你会发现你甚至需要学习如何编写 Keras 自定义度量。你必须遵循函数签名才能使其工作,如下面的代码片段所示。
def custom_metric(y_true, y_pred):
squared_diff = ops.square(y_true - y_pred)
return ops.mean(squared_diff, axis=-1)
发现这个问题后,我们特别设计了一个更好的自定义度量工作流。你只需要重写HyperModel.fit()来计算自定义度量并返回它。不需要用字符串来命名目标。也不需要遵循函数签名。只需一个返回值。现在的用户体验要好得多。
class MyHyperModel(HyperModel):
def fit(self, trial, model, validation_data):
x_val, y_true = validation_data
y_pred = model(x_val)
return custom_metric(y_true, y_pred)
tuner = RandomSearch(MyHyperModel(), max_trials=20)
还有一点需要记住的是,我们应该始终从用户体验开始。设计的工作流会反向影响实现。
原则 2:最小化认知负担
除非真的必要,否则不要强迫用户学习任何东西。让我们看看一些好的例子。
Keras 模型构建 API 是一个好的例子,如下面的代码片段所示。模型构建者已经掌握了这些概念,例如,模型是一个层的堆叠。它需要一个损失函数。我们可以用数据来拟合它,或者让它对数据进行预测。
model = keras.Sequential([
layers.Dense(10, activation="relu"),
layers.Dense(num_classes, activation="softmax"),
])
model.compile(loss='categorical_crossentropy')
model.fit(...)
model.predict(...)
所以基本上,使用 Keras 并不需要学习新的概念。
另一个好的例子是 PyTorch 模型构建。代码就像 Python 代码一样执行。所有张量都是真正的张量,具有实际值。你可以依赖一个张量的值来决定路径,用纯 Python 代码实现。
class MyModel(nn.Module):
def forward(self, x):
if x.sum() > 0:
return self.path_a(x)
return self.path_b(x)
你也可以使用 Keras 和 TensorFlow 或 JAX 后端来做这件事,但需要以不同的方式编写。所有的 if 条件需要通过这个 ops.cond 函数来编写,如下面的代码片段所示。
class MyModel(keras.Model):
def call(self, inputs):
return ops.cond(
ops.sum(inputs) > 0,
lambda : self.path_a(inputs),
lambda : self.path_b(inputs),
)
这教会用户学习一种新的操作,而不是使用他们熟悉的 if-else 语句,这样做并不好。作为补偿,它在训练速度上带来了显著的提升。
这就是 PyTorch 灵活性的陷阱。如果你曾经需要优化模型的内存和速度,你将不得不自己使用以下 API 和新概念来实现,包括操作的 inplace 参数、并行操作 API 和显式设备分配。这为用户引入了相当高的学习曲线。
torch.relu(x, inplace=True)
x = torch._foreach_add(x, y)
torch._foreach_add_(x, y)
x = x.cuda()
其他一些好的例子包括keras.ops、tensorflow.numpy、jax.numpy。它们只是 numpy API 的重新实现。当引入一定的认知负担时,只需重用人们已经熟悉的内容。每个框架都必须在这些框架中提供一些低级操作。与其让人们学习一套全新的 API(可能有上百个函数),不如直接使用最流行的现有 API。numpy 的 API 文档详尽,并且有大量相关的 Stack Overflow 问题和答案。
你在用户体验中能做的最糟糕的事情就是欺骗用户。让用户相信你的 API 是他们熟悉的,但实际上不是。我将给出两个例子,一个是 PyTorch 的,另一个是 TensorFlow 的。
如果你想将形状为(100, 3, 32, 32)的输入张量填充到(100, 3, 1+32+1, 2+32+2)或(100, 3, 34, 36),那么在F.pad()函数中应该传递什么作为 pad 参数?
import torch.nn.functional as F
# pad the 32x32 images to (1+32+1)x(2+32+2)
# (100, 3, 32, 32) to (100, 3, 34, 36)
out = F.pad(
torch.empty(100, 3, 32, 32),
pad=???,
)
我的第一直觉是它应该是((0, 0), (0, 0), (1, 1), (2, 2)),其中每个子元组对应四个维度中的一个,两个数字是现有值之前和之后的填充值。我的猜测来源于 numpy API。
然而,正确的答案是(2, 2, 1, 1)。没有子元组,只有一个普通的元组。此外,维度是反转的,最后一个维度变成了第一个。
以下是 TensorFlow 的一个坏例子。你能猜出以下代码片段的输出是什么吗?
value = True
@tf.function
def get_value():
return value
value = False
print(get_value())
如果没有tf.function装饰器,输出应该是 False,这个很简单。然而,使用了装饰器后,输出变为 True。这是因为 TensorFlow 会编译函数,任何 Python 变量都会被编译成一个新的常量。修改旧变量的值不会影响已创建的常量。
它让用户相信这是他们熟悉的 Python 代码,但实际上并不是。
原则 3:互动优于文档
如果用户能通过运行一些示例代码并自己调试就能搞明白问题,那么没有人愿意阅读冗长的文档。所以,我们尽量让软件的用户工作流遵循相同的逻辑。
这里有一个很好的例子,展示在下面的代码片段中。在 PyTorch 中,所有带有下划线的方法都是原地操作(inplace ops),而没有下划线的是非原地操作。从交互的角度来看,这样做很好,因为它们容易跟随,用户在需要使用方法的原地版本时无需查阅文档。然而,当然,这也带来了一些认知负担。用户需要知道什么是原地操作,以及何时使用它们。
x = x.add(y)
x.add_(y)
x = x.mul(y)
x.mul_(y)
另一个好例子是 Keras 层。它们严格遵循相同的命名约定,正如下面的代码片段所示。通过清晰的命名约定,用户可以轻松记住层的名称,而无需查阅文档。
from keras import layers
layers.MaxPooling2D()
layers.GlobalMaxPooling1D()
layers.GlobalAveragePooling3D()
用户与软件之间的交互中,另一个重要的部分就是错误信息。你不能指望用户第一次就能完全正确地编写代码。我们应该在代码中进行必要的检查,并尽量输出有用的错误信息。
让我们来看一下代码片段中的两个例子。第一个例子信息不多,只是简单地说了张量形状不匹配。
第二个例子则包含了更多有用的信息,帮助用户找到 bug。它不仅告诉你错误是由于张量形状不匹配,还显示了期望的形状以及收到的错误形状。如果你并不打算传递这个形状,你就能更清楚地知道问题所在。
现在的 bug。
# Bad example:
raise ValueError("Tensor shape mismatch.")
# Good example:
raise ValueError(
"Tensor shape mismatch. "
"Expected: (batch, num_features). "
f"Received: {x.shape}"
)
最好的错误信息应该是直接指引用户如何修复问题。下面的代码片段展示了一个典型的 Python 错误信息。它猜测了代码中的问题,并直接指向了修复方法。
import math
math.sqr(4)
"AttributeError: module 'math' has no attribute 'sqr'. Did you mean: 'sqrt'?"
最后总结
到目前为止,我们已经介绍了我在为深度学习框架贡献时学到的三条最有价值的软件设计原则。首先,编写端到端工作流程以发现更多用户体验问题。其次,减少认知负担,除非必要,否则不要教给用户任何东西。第三,在 API 设计中遵循相同的逻辑,抛出有意义的错误信息,让用户通过与软件的交互来学习,而不是不断查阅文档。
然而,如果你想让你的软件更好,还有许多原则需要遵循。你可以参考 Keras API 设计指南 作为完整的 API 设计指南。


浙公网安备 33010602011771号