docs-merge-20
TowardsDataScience 2024 中文翻译(二十一)
使用 LLMs 从 YouTube 学习

图片由作者使用 Midjourney 制作
一个使用 LangChain、Pinecone、Flask、React 和 AWS 构建的对话式问答工具
·发表于Towards Data Science ·阅读时间 17 分钟·2024 年 5 月 21 日
--
介绍
你是否曾遇到过想要观看的播客或视频,但因其长度而难以找到时间?你是否希望能有一种简单的方法来回顾该内容的特定部分?
这是我在处理像《CEO 日记》这样的流行播客的 YouTube 视频时经常遇到的问题。实际上,这类播客中涵盖的很多信息,通过快速的 Google 搜索就能轻松找到。但听作者谈论他们对某些话题的看法,或者听一位成功企业家从他们的视角讲述自己的经历,往往能提供更多的见解和清晰度。
受这一问题以及希望自我教育有关 LLM 驱动应用及其开发的愿望的驱动,我决定构建一个聊天机器人,允许用户使用 RAG(检索增强生成)框架询问有关 YouTube 视频内容的问题。在本文的其余部分,我将介绍我使用 LangChain、Pinecone、Flask、React 开发此应用程序,并使用 AWS 进行部署的经验:

我将代码片段限制为我认为最有用的那些。对于有兴趣的人,应用程序的完整代码库可以在这里找到。
后端
我们将使用 YouTube 视频的转录文本作为源,从中生成 LLM 对用户定义问题的答案。为了实现这一点,后端需要一种方法来实时检索并适当存储这些文本,以便用于生成答案。同时,我们还需要一种存储聊天历史记录的方法,以便用户能够在后续时间查看。现在,看看如何开发后端以满足所有这些需求。
回应生成
由于这是一个对话式问答工具,应用程序必须能够在考虑到相关上下文和聊天历史的情况下生成问题的答案。可以通过使用带有对话记忆的检索增强生成(RAG)方法来实现这一点,如下所示:

为了清晰起见,涉及的步骤如下:
-
问题总结:当前问题和聊天历史将通过适当的提示缩减为一个独立的问题,要求 LLM 执行这一操作。
-
语义搜索:接下来,必须检索与简化问题最相关的 YouTube 转录文本块。转录文本本身被存储为嵌入,嵌入是单词和短语的数值表示,由嵌入模型学习,这个模型捕捉它们的内容和语义。在语义搜索过程中,每个转录文本的组件,其嵌入与简化问题的嵌入最为相似,会被检索出来。
-
上下文感知生成:这些检索到的转录文本块随后作为上下文出现在另一个向 LLM 发送的提示中,要求它回答简化后的问题。使用简化问题可以确保生成的答案不仅与当前问题相关,还与用户在对话中先前提出的问题相关。
数据管道
在继续实施上述过程之前,让我们先回顾一下 YouTube 视频的转录文本。正如前面讨论的,这些文本必须以嵌入的形式存储,以便在 RAG 过程的语义搜索阶段高效检索和使用它们。现在,让我们一起了解这些转录文本的来源、检索方法和存储方法。
-
来源:YouTube 通过其数据 API 提供视频 ID 等元数据,并提供自动生成的转录文本。首先,我选择了这个来自《CEO 日记》播客的播放列表,其中多位财经专家和企业家讨论个人财务、投资以及如何建立成功的企业。
-
检索: 我使用一个类,通过直接与 YouTube 数据 API 交互来检索 YouTube 视频的元数据,例如视频 ID;另外一个类则使用 youtube-transcript-API Python 包来获取视频的字幕。这些字幕随后以原始形式存储为 JSON 文件在 S3 存储桶中。
-
存储: 接下来,字幕需要转换为嵌入并存储到向量数据库中。然而,这一步的前提是将其拆分成若干段,以便在检索时能获取到与每个问题最相关的文本片段,同时尽量减少 LLM 提示本身的长度。为了满足这一要求,我定义了一个自定义的 S3JsonFileLoader 类 在这里(由于 LangChain 自带版本存在一些问题),并利用 文本拆分器对象 在加载时拆分字幕。然后,我利用 LangChain 与 Pinecone 向量存储(我选择的高效存储、搜索和检索字幕嵌入的向量存储)接口,将字幕片段作为 OpenAI 的 gpt-3.5-turbo 模型所期望的嵌入进行存储:
import os
import pinecone
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.vectorstores import Pinecone
from langchain.chat_models import ChatOpenAI
from langchain.embeddings.openai import OpenAIEmbeddings
from chatytt.embeddings.s3_json_document_loader import S3JsonFileLoader
# Define the splitter with which to split the transcripts into chunks
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=pre_processing_conf["recursive_character_splitting"]["chunk_size"],
chunk_overlap=pre_processing_conf["recursive_character_splitting"][
"chunk_overlap"
],
)
# Load and split the transcript
loader = S3JsonFileLoader(
bucket="s3_bucket_name",
key="transcript_file_name",
text_splitter=text_splitter,
)
transcript_chunks = loader.load(split_doc=True)
# Connect to the relevant Pinecone vectorstore
pinecone.init(
api_key=os.environ.get("PINECONE_API_KEY"), environment="gcp-starter"
)
pinecone_index = pinecone.Index(os.environ.get("INDEX_NAME"), pool_threads=4)
# Store the transcrpt chunks as embeddings in Pinecone
vector_store = Pinecone(
index=pinecone_index,
embedding=OpenAIEmbeddings(),
text_key="text",
)
vector_store.add_documents(documents=transcript_chunks)
我们还可以利用一些 AWS 服务,通过配置定期运行的工作流来自动化这些步骤。我通过将上述提到的三个步骤分别实现为 AWS Lambda 函数(这是一种无服务器计算方式,根据需要在运行时调配和利用资源),并使用 AWS Step Functions(无服务器编排工具)定义它们的执行顺序来实现这一点。然后,使用我设置为每周运行一次的 Amazon EventBridge 调度来执行该工作流,以便自动获取并处理任何新添加到播放列表中的视频:

请注意,我已经获得了《CEO 日记》频道的许可,使用上述播放列表中视频的字幕。任何希望以这种方式使用第三方内容的人,都应首先获得原始所有者的许可。
实现 RAG
现在,我们的播放列表转录本正在定期被检索、转换为嵌入并存储,我们可以继续实现应用程序的核心后端功能,即使用 RAG 生成用户定义问题的答案的过程。幸运的是,LangChain 提供了一个内置的 ConversationalRetrievalChain 来完成这一任务!所需做的仅仅是将查询、聊天历史、一个可以用来检索转录本片段的向量存储对象以及一个选择的 LLM 传递到这个链中,如下所示:
import pinecone
from langchain.vectorstores import Pinecone
from langchain.chat_models import ChatOpenAI
from langchain.embeddings.openai import OpenAIEmbeddings
# Define the vector store with which to perform a semantic search against
# the Pinecone vector database
pinecone.init(
api_key=os.environ.get("PINECONE_API_KEY"), environment="gcp-starter"
)
pinecone_index = pinecone.Index(os.environ.get("INDEX_NAME"), pool_threads=4)
vector_store = Pinecone(
index=pinecone_index,
embedding=OpenAIEmbeddings(),
text_key="text",
)
# Define the retrieval chain that will perform the steps in RAG
# with conversation memory as outlined above.
chain = ConversationalRetrievalChain.from_llm(
llm=ChatOpenAI(), retriever=vector_store.as_retriever()
)
# Call the chain passing in the current question and the chat history
response = chain({"question": query, "chat_history": chat_history})["answer"]
我还从头实现了这个链的功能,正如 LangChain 的教程中所描述的那样,使用了 LangChain 表达式语言 这里 和 SequentialChain 这里 。这些可能会提供更多关于在上面的链中发生的所有操作的洞察。
保存聊天历史
后端现在可以生成问题的答案,但最好也能存储和检索聊天历史,以便用户可以参考旧的聊天记录。由于这是一个不同用户访问同一项数据的已知访问模式,我决定使用 DynamoDB,这是一个以处理这种形式的非结构化数据的速度和成本效率著称的 NoSQL 数据库。此外,boto3 SDK 简化了与数据库的交互,只需要几个函数来存储和检索数据:
import os
import time
from typing import List, Any
import boto3
table = boto3.resource("dynamodb").Table(os.environ.get("CHAT_HISTORY_TABLE_NAME")
def fetch_chat_history(user_id: str) -> str:
response = table.get_item(Key={"UserId": user_id})
return response["Item"]
def update_chat_history(user_id: str, chat_history: List[dict[str, Any]]):
chat_history_update_data = {
"UpdatedTimestamp": {"Value": int(time.time()), "Action": "PUT"},
"ChatHistory": {"Value": chat_history, "Action": "PUT"},
}
table.update_item(
Key={"UserId": user_id}, AttributeUpdates=chat_history_update_data
)
def is_new_user(user_id: str) -> bool:
response = table.get_item(Key={"UserId": user_id})
return response.get("Item") is None
def create_chat_history(user_id: str, chat_history: List[dict[str, Any]]):
item = {
"UserId": user_id,
"CreatedTimestamp": int(time.time()),
"UpdatedTimestamp": None,
"ChatHistory": chat_history,
}
table.put_item(Item=item)
通过 API 暴露逻辑
我们现在已经涵盖了所有核心功能,但用户与之交互的应用程序客户端需要某种方式来触发并利用这些过程。为了便于此,每个三大逻辑部分(生成答案、保存聊天历史、检索聊天历史)都通过 Flask API 内部的单独端点暴露,前端将调用这些端点:
from dotenv import load_dotenv
from flask import Flask, request, jsonify
from flask_cors import CORS
from chatytt.chains.standard import ConversationalQAChain
from chatytt.vector_store.pinecone_db import PineconeDB
from server.utils.chat import parse_chat_history
from server.utils.dynamodb import (
is_new_user,
fetch_chat_history,
create_chat_history,
update_chat_history,
)
load_dotenv()
app = Flask(__name__)
# Enable Cross Origin Resource Sharing since the server and client
# will be hosted seperately
CORS(app)
pinecone_db = PineconeDB(index_name="youtube-transcripts", embedding_source="open-ai")
chain = ConversationalQAChain(vector_store=pinecone_db.vector_store)
@app.route("/get-query-response/", methods=["POST"])
def get_query_response():
data = request.get_json()
query = data["query"]
raw_chat_history = data["chatHistory"]
chat_history = parse_chat_history(raw_chat_history)
response = chain.get_response(query=query, chat_history=chat_history)
return jsonify({"response": response})
@app.route("/get-chat-history/", methods=["GET"])
def get_chat_history():
user_id = request.args.get("userId")
if is_new_user(user_id):
response = {"chatHistory": []}
return jsonify({"response": response})
response = {"chatHistory": fetch_chat_history(user_id=user_id)["ChatHistory"]}
return jsonify({"response": response})
@app.route("/save-chat-history/", methods=["PUT"])
def save_chat_history():
data = request.get_json()
user_id = data["userId"]
if is_new_user(user_id):
create_chat_history(user_id=user_id, chat_history=data["chatHistory"])
else:
update_chat_history(user_id=user_id, chat_history=data["chatHistory"])
return jsonify({"response": "chat saved"})
if __name__ == "__main__":
app.run(debug=True, port=8080)
最后,我使用 AWS Lambda 将三个端点包装成一个 单一函数,然后通过 API Gateway 资源触发该函数,API Gateway 会根据需要构造适当的负载,将请求路由到正确的端点。这种设置的流程如下所示:

前端
在完成应用程序的后端后,我将简要介绍 React 中用户界面的实现,特别关注与包含上述 API 的服务器组件的交互。
我为应用程序的每个部分使用了专用的功能组件,涵盖了在聊天机器人应用程序中可能期望的所有典型需求:
-
一个用于用户输入的容器,并带有发送聊天按钮。
-
一个聊天流,其中显示用户输入和答案。
-
包含聊天历史、一个新聊天按钮和一个保存聊天按钮的侧边栏。
这些组件之间的交互以及数据流的过程如下图所示:

对每个端点的 API 调用及相关变量在客户端状态的变化,都在独立的功能组件中定义:
- 检索每个问题生成答案的逻辑:
import React from "react";
import {chatItem} from "./LiveChatFeed";
interface Props {
setCurrentChat: React.SetStateAction<any>
userInput: string
currentChat: Array<chatItem>
setUserInput: React.SetStateAction<any>
}
function getCurrentChat({setCurrentChat, userInput, currentChat, setUserInput}: Props){
// The current chat is displayed in the live chat feed. Since we don't want
// to wait for the LLM response before displaying the users question in
// the chat feed, copy it to a seperate variable and pass it to the current
// chat before pining the API for the answer.
const userInputText = userInput
setUserInput("")
setCurrentChat([
...currentChat,
{
"text": userInputText,
isBot: false
}
])
// Create API payload for the post request
const options = {
method: 'POST',
headers: {
"Content-Type": 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({
query: userInputText,
chatHistory: currentChat
})
}
// Ping the endpoint, wait for the response, and add it to the current chat
// so that it appears in the live chat feed.
fetch(`${import.meta.env.VITE_ENDPOINT}get-query-response/`, options).then(
(response) => response.json()
).then(
(data) => {
setCurrentChat([
...currentChat,
{
"text": userInputText,
"isBot": false
},
{
"text": data.response,
"isBot": true
}
])
}
)
}
export default getCurrentChat
2. 当用户点击保存聊天按钮时,保存聊天历史:
import React, {useState} from "react";
import {chatItem} from "./LiveChatFeed";
import saveIcon from "../assets/saveicon.png"
import tickIcon from "../assets/tickicon.png"
interface Props {
userId: String
previousChats: Array<Array<chatItem>>
}
function SaveChatHistoryButton({userId, previousChats}: Props){
// Define a state to determine if the current chat has been saved or not.
const [isChatSaved, setIsChatSaved] = useState(false)
// Construct the payload for the PUT request to save chat history.
const saveChatHistory = () => {
const options = {
method: 'PUT',
headers: {
"Content-Type": 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({
"userId": userId,
"chatHistory": previousChats
})
}
// Ping the API with the chat history, and set the state of
// isChatSaved to true if successful
fetch(`${import.meta.env.VITE_ENDPOINT}save-chat-history/`, options).then(
(response) => response.json()
).then(
(data) => {
setIsChatSaved(true)
}
)
}
// Display text on the save chat button dynamically depending on the value
// of the isChatSaved state.
return (
<button
className="save-chat-history-button"
onClick={() => {saveChatHistory()}}
> <img className={isChatSaved?"tick-icon-img":"save-icon-img"} src={isChatSaved?tickIcon:saveIcon}/>
{isChatSaved?"Chats Saved":"Save Chat History"}
</button>
)
}
export default SaveChatHistoryButton
3. 当应用程序首次加载时,检索聊天历史:
import React from "react";
import {chatItem} from "./LiveChatFeed";
interface Props {
userId: String
previousChats: Array<Array<chatItem>>
setPreviousChats: React.SetStateAction<any>
}
function getUserChatHistory({userId, previousChats, setPreviousChats}: Props){
// Create the payload for the GET request
const options = {
method: 'GET',
headers: {
"Content-Type": 'application/json',
'Accept': 'application/json'
}
}
// Since this is a GET request pass in the user id as a query parameter
// Set the previousChats state to the chat history returned by the API.
fetch(`${import.meta.env.VITE_ENDPOINT}get-chat-history/?userId=${userId}`, options).then(
(response) => response.json()
).then(
(data) => {
if (data.response.chatHistory.length > 0) {
setPreviousChats(
[
...previousChats,
...data.response.chatHistory
]
)
}
}
)
}
export default getUserChatHistory
对于用户界面,我选择了一个与 ChatGPT 界面非常相似的设计,中央是一个聊天流组件,旁边是一个包含聊天历史等支持内容的侧边栏。为了提升用户体验,一些功能包括自动滚动到最近创建的聊天项,以及在登录时加载之前的聊天记录(这些内容我没有在文章中包含,但你可以在相关的功能组件中找到其实现在这里)。最终的用户界面如下所示:

现在我们已经拥有了一个功能完全的用户界面,剩下的就是将它部署到在线使用,我选择使用 AWS Amplify 来托管。除了其他功能外,Amplify 是一个完全托管的 web 托管服务,它负责资源配置和 web 应用程序的托管。应用程序的用户身份验证由 Amazon Cognito 管理,允许用户注册和登录,同时处理凭证存储和管理:

与 ChatGPT 回复的对比
现在我们已经讨论了应用程序构建的过程,让我们深入分析一些问题的回复,并将这些回复与 ChatGPT* 中提出的相同问题进行比较。
请注意,这种类型的比较本质上是不公平的,因为我们应用程序中使用的 LLM 的底层提示将包含额外的上下文(以相关的转录片段形式),这些片段是通过语义搜索步骤获取的。然而,它将帮助我们定性地评估使用 RAG 创建的提示与由相同底层 LLM 生成的回复之间的差异有多大。
所有 ChatGPT 的回复均来自 gpt-3.5,因为这是应用程序中使用的模型。
示例 1:
你可以通过观看这个视频来了解内容,视频中 Steven Bartlett 与财经作家及投资者 Morgan Housel 进行对话。从视频的标题来看,似乎他反对买房——但假设你没有时间观看完整的视频来找出原因。以下是我在应用程序中询问该问题时得到的对话片段:

你还可以在这里看到对话记忆的实际应用,在后续的问题中,我没有明确提到 Morgan Housel,甚至没有提到“房子”或“购买”这两个词。由于总结的查询会考虑到先前的聊天记录,LLM 的回答反映了之前的问题及其答案。Housel 提到上述观点的部分大约在播客开始一个半小时后,可以在1:33:00–1:41:00 时间戳找到。
我问了 ChatGPT 同样的问题,正如预期的那样,得到了一个非常通用的回答,并没有针对 Housel 的观点进行具体阐述。

可以说,由于该视频是在模型的最后一次“知识更新”之后发布的,因此比较结果可能有偏差,但 Housel 的观点在他 2020 年出版的《金钱心理学》一书中也有详细记录。无论如何,这些对知识更新的依赖进一步突显了基于上下文的答案生成相比独立模型的优势。
示例 2
以下是与 Alex Hormozi 的这次讨论中的一些片段,Alex Hormozi 是一位货币化和收购专家。从视频标题来看,他似乎对如何成功扩展企业有一定的了解,因此我向他询问了更多的细节:

这个回答看起来合情合理,但让我们看看是否能从同样的问题中提取更多信息。


请注意,LLM 能够从 YouTube 文字记录中提取的细节水平。上述内容可以在视频的 15 到 20 分钟部分找到,大约在17:00–35:00 时间戳。
再次,向 ChatGPT 提出相同的问题得到了关于企业家的通用回答,但缺乏视频文字记录中的背景信息细节。

部署
最后,我们将讨论在 AWS 上部署每个组件的过程。数据管道、后端和前端分别包含在各自的 CloudFormation 堆栈中(AWS 资源集合)。像这样允许它们独立部署,确保在开发过程中不会不必要地重新部署整个应用。我使用 AWS SAM(无服务器应用模型)将每个组件的基础设施作为代码进行部署,利用 SAM 模板规范和 CLI:
-
SAM 模板规范 — 一种简写语法,作为 AWS CloudFormation 的扩展,用于定义和配置 AWS 资源集合、它们如何交互以及所需的权限。
-
SAM CLI — 一个命令行工具,除了用于构建和部署按照 SAM 模板定义的资源外,还处理应用程序代码和依赖项的打包、将 SAM 模板转换为 CloudFormation 语法,并将模板部署为 CloudFormation 上的单独堆栈。
我不会包含每个组件的完整模板(资源定义),而是将突出每个我们在文章中讨论的服务的具体关注点。
将敏感环境变量传递给 AWS 资源:
外部组件,如 Youtube 数据 API、OpenAI API 和 Pinecone API,在整个应用程序中被广泛依赖。虽然可以将这些值硬编码到 CloudFormation 模板中并作为“参数”传递,但更安全的方法是为每个组件在 AWS SecretsManager 中创建密钥,并像这样在模板中引用这些密钥:
Parameters:
YoutubeDataAPIKey:
Type: String
Default: '{{resolve:secretsmanager:youtube-data-api-key:SecretString:youtube-data-api-key}}'
PineconeAPIKey:
Type: String
Default: '{{resolve:secretsmanager:pinecone-api-key:SecretString:pinecone-api-key}}'
OpenaiAPIKey:
Type: String
Default: '{{resolve:secretsmanager:openai-api-key:SecretString:openai-api-key}}'
定义 Lambda 函数:
这些无服务器代码单元构成了数据管道的骨干,并作为 Web 应用程序后端的入口点。使用 SAM 部署这些代码只需简单地定义当调用函数时应该运行的代码路径,以及任何所需的权限和环境变量。以下是数据管道中使用的一个函数示例:
FetchLatestVideoIDsFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: ../code_uri/.
Handler: chatytt.youtube_data.lambda_handlers.fetch_latest_video_ids.lambda_handler
Policies:
- AmazonS3FullAccess
Environment:
Variables:
PLAYLIST_NAME:
Ref: PlaylistName
YOUTUBE_DATA_API_KEY:
Ref: YoutubeDataAPIKey
检索数据管道在 Amazon States Language 中的定义:
为了使用 Step Functions 作为数据管道中各个 Lambda 函数的协调器,我们需要定义每个函数执行的顺序,以及诸如最大重试次数等配置项。一个简单的方法是使用 Step Functions 控制台中的 工作流工作室以图形化方式创建工作流,然后将自动生成的 ASL 工作流定义作为起点进行适当修改。然后可以在 CloudFormation 模板中链接它,而不是在模板中直接定义:
EmbeddingRetrieverStateMachine:
Type: AWS::Serverless::StateMachine
Properties:
DefinitionUri: statemachine/embedding_retriever.asl.json
DefinitionSubstitutions:
FetchLatestVideoIDsFunctionArn: !GetAtt FetchLatestVideoIDsFunction.Arn
FetchLatestVideoTranscriptsArn: !GetAtt FetchLatestVideoTranscripts.Arn
FetchLatestTranscriptEmbeddingsArn: !GetAtt FetchLatestTranscriptEmbeddings.Arn
Events:
WeeklySchedule:
Type: Schedule
Properties:
Description: Schedule to run the workflow once per week on a Monday.
Enabled: true
Schedule: cron(0 3 ? * 1 *)
Policies:
- LambdaInvokePolicy:
FunctionName: !Ref FetchLatestVideoIDsFunction
- LambdaInvokePolicy:
FunctionName: !Ref FetchLatestVideoTranscripts
- LambdaInvokePolicy:
FunctionName: !Ref FetchLatestTranscriptEmbeddings
查看 此处 查阅本文章中讨论的数据管道使用的 ASL 定义。
定义 API 资源:
由于 Web 应用程序的 API 将与前端分开托管,因此在定义 API 资源时,我们必须启用 CORS(跨源资源共享)支持:
ChatYTTApi:
Type: AWS::Serverless::Api
Properties:
StageName: Prod
Cors:
AllowMethods: "'*'"
AllowHeaders: "'*'"
AllowOrigin: "'*'"
这将允许这两个资源相互自由通信。通过 Lambda 函数使得可访问的各种端点可以这样定义:
ChatResponseFunction:
Type: AWS::Serverless::Function
Properties:
Runtime: python3.9
Timeout: 120
CodeUri: ../code_uri/.
Handler: server.lambda_handler.lambda_handler
Policies:
- AmazonDynamoDBFullAccess
MemorySize: 512
Architectures:
- x86_64
Environment:
Variables:
PINECONE_API_KEY:
Ref: PineconeAPIKey
OPENAI_API_KEY:
Ref: OpenaiAPIKey
Events:
GetQueryResponse:
Type: Api
Properties:
RestApiId: !Ref ChatYTTApi
Path: /get-query-response/
Method: post
GetChatHistory:
Type: Api
Properties:
RestApiId: !Ref ChatYTTApi
Path: /get-chat-history/
Method: get
UpdateChatHistory:
Type: Api
Properties:
RestApiId: !Ref ChatYTTApi
Path: /save-chat-history/
Method: put
定义 React 应用资源:
AWS Amplify 可以使用对相关 Github 仓库的引用和适当的访问令牌来构建和部署应用程序:
AmplifyApp:
Type: AWS::Amplify::App
Properties:
Name: amplify-chatytt-client
Repository: <https://github.com/suresha97/ChatYTT>
AccessToken: '{{resolve:secretsmanager:github-token:SecretString:github-token}}'
IAMServiceRole: !GetAtt AmplifyRole.Arn
EnvironmentVariables:
- Name: ENDPOINT
Value: !ImportValue 'chatytt-api-ChatYTTAPIURL'
一旦仓库本身可访问,Amplify 将查找一个包含构建和部署应用程序指令的配置文件:
version: 1
frontend:
phases:
preBuild:
commands:
- cd client
- npm ci
build:
commands:
- echo "VITE_ENDPOINT=$ENDPOINT" >> .env
- npm run build
artifacts:
baseDirectory: ./client/dist
files:
- "**/*"
cache:
paths:
- node_modules/**/*
作为一个附加功能,还可以通过定义一个分支资源来自动化持续部署过程,这个资源会被监控,并在进一步提交时自动重新部署应用:
AmplifyBranch:
Type: AWS::Amplify::Branch
Properties:
BranchName: main
AppId: !GetAtt AmplifyApp.AppId
EnableAutoBuild: true
通过这种方式完成部署后,任何拥有 AWS Amplify 控制台提供的链接的人都可以访问它。可以在这里找到一个应用被访问的演示录制:
结论
从高层次看,我们已经涵盖了以下步骤:
-
构建一个用于收集和存储内容作为嵌入的 数据管道。
-
开发一个后台服务器组件,执行带有对话记忆的检索增强生成。
-
设计一个用户界面,用于展示生成的答案和聊天历史。
-
这些组件如何连接并部署以创建一个能够提供价值并节省时间的解决方案。
我们已经看到,像这样的应用程序如何被用来简化并在某些方面“优化”像 YouTube 视频这样的内容在学习和发展中的消费。但这些方法同样可以轻松地应用于工作场所的内部使用或增强面向客户的解决方案。这也是为什么大语言模型(LLM)及特别是 RAG 技术在许多组织中获得如此多关注的原因。
我希望这篇文章能够为如何将这些相对较新的技术与更多传统的工具和框架结合使用,在开发面向用户的应用程序中提供一些见解。
致谢
我要感谢《CEO 日记》团队,感谢他们允许在本项目中使用来自这个播放列表的视频转录内容,并为撰写本文提供支持。
所有图片,除非另有注明,均由作者提供。
使用 LLMs 查询 PubMed 知识库进行生物医学研究
为了乐趣和盈利使用 AI!
·发表于 Towards Data Science ·阅读时间:11 分钟·2024 年 7 月 24 日
--

图片由 🇸🇮 Janko Ferlič 提供,来自 Unsplash
第一部分:创建知识库
在本文中,我们将探讨如何利用大语言模型(LLMs)来搜索和科学论文,来自 PubMed Open Access 子集,这是一个用于访问生物医学和生命科学文献的免费资源。我们将使用检索增强生成(RAG)技术来搜索我们的数字图书馆。
AWS Bedrock 将作为我们的 AI 后端,PostgreSQL 作为存储嵌入的向量数据库,而 Python 中的 LangChain 库将用于导入论文并查询知识库。
如果你只关心通过查询知识库生成的结果,可以跳到文章末尾。
我们将重点关注的具体用例是查询与类风湿性关节炎相关的论文,这是一种影响关节的慢性炎症性疾病。我们将使用查询 ((rheumatoid arthritis) AND gene) AND cell 从 PubMed 检索大约 10,000 篇相关论文,然后将其缩减到大约 5,000 篇作为我们的知识库。
免责声明
不是所有的研究文章或来源都有允许与 AI 一起使用的许可!
我没有包含所有源代码,因为 AI 库更新非常频繁,而且有许多方式可以配置知识库后端,但我提供了一些辅助函数,你可以跟着一起操作。
PGVector:在向量数据库中存储嵌入
为了使 LLM 更容易处理和理解来自研究论文的文本数据,我们将把文本转换为数值嵌入,这些嵌入是文本的稠密向量表示。这些嵌入将使用 PGVector 库存储在 PostgreSQL 数据库中。这个步骤本质上将文本数据简化为一种 LLM 更容易处理的格式。
我正在运行一个本地的 PostgreSQL 数据库,对于我的数据集来说这样就足够了。托管 AWS Bedrock 知识库可能会很昂贵,而且我这个月并不打算让我的 AWS 账单飙升。现在是夏天,我得为孩子们的夏令营支付费用!
AWS Bedrock:AI 后端
AWS Bedrock 是亚马逊云服务(AWS)提供的一项托管服务,允许你轻松部署和操作大型语言模型。在我们的设置中,Bedrock 将托管我们用于查询并从研究论文的知识库中检索相关信息的 LLM。
LangChain:加载和查询知识库
LangChain 是一个 Python 库,简化了使用大型语言模型构建应用程序的过程。我们将使用 LangChain 将我们的研究论文及其相关的嵌入载入知识库,并通过托管在 AWS Bedrock 上的大型语言模型(LLM)来查询这个知识库。
使用 PubGet 进行数据获取
尽管这个设置可以处理来自任何来源的研究论文,但我们使用 PubMed 是因为它是一个便捷的来源,可以根据特定的搜索查询获取大量论文。我们将使用 PubGet 工具来检索初始的 10,000 篇与类风湿关节炎、基因和细胞相关的论文。PubGet 背后会从 PubMed 的 FTP 服务获取文章。
pubget run -q "((rheumatoid arthritis) AND gene) AND cell" \
pubget_data
这将为我们获取 xml 格式的文章。
数据集的结构化与组织
除了技术方面,本文将重点讨论如何有效地构建和组织你的研究论文数据集。
-
数据集:通过集合在全球范围内管理你的数据集。
-
元数据管理:处理和整合与论文相关的元数据,例如作者信息、出版日期和关键词。
你需要提前考虑这一点。使用 LangChain 时,你根据集合查询数据集。每个集合都有一个名称和唯一标识符。
当你加载数据时,无论是 pdf 论文、xml 下载、markdown 文件、代码库、PowerPoint 幻灯片、文本文件等,你都可以附加额外的元数据。稍后你可以利用这些元数据来筛选结果。元数据是一个开放的字典,你可以添加标签、来源、表型或任何你认为可能相关的内容。
加载和查询知识库
文章还将涵盖将你的预处理和结构化数据集加载到知识库中的最佳实践,并提供如何有效查询知识库的示例,使用的 LLM 由 AWS Bedrock 托管。
到文章末尾时,你应该能充分理解如何利用 LLM 搜索并从大量研究论文中提取相关信息,同时掌握如何构建和组织数据集,以优化知识库的性能和准确性。
import boto3
import pprint
import os
import boto3
import json
import hashlib
import funcy
import glob
from typing import Dict, Any, TypedDict, List
from langchain.llms.bedrock import Bedrock
from langchain.retrievers.bedrock import AmazonKnowledgeBasesRetriever
from langchain_core.documents import Document
from langchain_aws import ChatBedrock
from langchain_community.embeddings import BedrockEmbeddings # to create embeddings for the documents.
from langchain_experimental.text_splitter import SemanticChunker # to split documents into smaller chunks.
from langchain_text_splitters import CharacterTextSplitter
from langchain_postgres import PGVector
from pydantic import BaseModel, Field
from langchain_community.document_loaders import (
WebBaseLoader,
TextLoader,
PyPDFLoader,
CSVLoader,
Docx2txtLoader,
UnstructuredEPubLoader,
UnstructuredMarkdownLoader,
UnstructuredXMLLoader,
UnstructuredRSTLoader,
UnstructuredExcelLoader,
DataFrameLoader,
)
import psycopg
import uuid
设置数据库连接
我正在本地运行一个SupaBase的 PostgreSQL 数据库,通过他们的docker-compose设置运行。在生产环境中,我建议使用真正的数据库,比如 AWS AuroraDB,或者将 Supabase 运行在除你的笔记本电脑之外的地方。另外,记得把密码改成其他的,而不是“password”。
我没有注意到在较小的数据集上,AWS 托管的知识库和我的笔记本电脑之间有任何性能差异,但你的体验可能会不同。
connection = f"postgresql+psycopg://{user}:{password}@{host}:{port}/{database}"
# Establish the connection to the database
conn = psycopg.connect(
conninfo = f"postgresql://{user}:{password}@{host}:{port}/{database}"
)
# Create a cursor to run queries
cur = conn.cursor()
使用 Langchain 将 AWS BedRock 嵌入插入表格
我们正在使用 AWS Bedrock 作为我们的 AI 知识库。我与大多数合作公司的工作中都有某种专有数据,而 Bedrock 保证你的数据将保持私密。你也可以使用任何其他 AI 后端。
os.environ['AWS_DEFAULT_REGION'] = 'us-east-1'
bedrock_client = boto3.client("bedrock-runtime")
bedrock_embeddings = BedrockEmbeddings(model_id="amazon.titan-embed-text-v1",client=bedrock_client)
bedrock_embeddings_image = BedrockEmbeddings(model_id="amazon.titan-embed-image-v1",client=bedrock_client)
llm = ChatBedrock(model_id="anthropic.claude-3-sonnet-20240229-v1:0", client=bedrock_client)
# function to create vector store
# make sure to update this if you change collections!
def create_vectorstore(embeddings,collection_name,conn):
vectorstore = PGVector(
embeddings=embeddings,
collection_name=collection_name,
connection=conn,
use_jsonb=True,
)
return vectorstore
def load_and_split_pdf_semantic(file_path, embeddings):
loader = PyPDFLoader(file_path)
pages = loader.load_and_split()
return pages
def load_xml(file_path, embeddings):
loader = UnstructuredXMLLoader(
file_path,
)
docs = loader.load_and_split()
return docs
def insert_embeddings(files, bedrock_embeddings, vectorstore):
logging.info(f"Inserting {len(files)}")
x = 1
y = len(files)
for file_path in files:
logging.info(f"Splitting {file_path} {x}/{y}")
docs = []
if '.pdf' in file_path:
try:
with funcy.print_durations('process pdf'):
docs = load_and_split_pdf_semantic(file_path, bedrock_embeddings)
except Exception as e:
logging.warning(f"Error loading docs")
if '.xml' in file_path:
try:
with funcy.print_durations('process xml'):
docs = load_xml(file_path, bedrock_embeddings)
except Exception as e:
logging.warning(e)
logging.warning(f"Error loading docs")
filtered_docs = []
for d in docs:
if len(d.page_content):
filtered_docs.append(d)
# Add documents to the vectorstore
ids = []
for d in filtered_docs:
ids.append(
hashlib.sha256(d.page_content.encode()).hexdigest()
)
if len(filtered_docs):
texts = [ i.page_content for i in filtered_docs]
# metadata is a dictionary. You can add to it!
metadatas = [ i.metadata for i in filtered_docs]
#logging.info(f"Adding N: {len(filtered_docs)}")
try:
with funcy.print_durations('load psql'):
vectorstore.add_texts(texts=texts, metadatas = metadatas, ids=ids)
except Exception as e:
logging.warning(e)
logging.warning(f"Error {x - 1}/{y}")
#logging.info(f"Complete {x}/{y}")
x = x + 1
collection_name_text = "MY_COLLECTION" #pubmed, smiles, etc
vectorstore = create_vectorstore(bedrock_embeddings,collection_name_text,connection)
加载和处理 Pubmed XML 论文
我们的大部分数据是通过pubget工具获取的,文章格式为 XML。我们将使用 LangChain XML 加载器来处理、拆分和加载嵌入。
files = glob.glob("/home/jovyan/data/pubget_ra/pubget_data/*/articles/*/*/article.xml")
#I ran this previously
insert_embeddings(files[0:2], bedrock_embeddings, vectorstore)
加载和处理 Pubmed PDF 论文
PDF 格式更易于阅读,我抓取了一些用于与知识库进行问答测试。
files = glob.glob("/home/jovyan/data/pubget_ra/papers/*pdf")
insert_embeddings(files[0:2], bedrock_embeddings, vectorstor
第二部分 — 查询知识库
现在我们已经设置好了知识库,可以使用增强型生成检索(RAG)方法,利用 LLM 运行查询。
我们的查询内容是:
-
告诉我关于类风湿性关节炎中的 T 细胞来源细胞因子的相关信息,并提供引用和文章标题。
-
告诉我关于类风湿性关节炎的单细胞研究。
-
告诉我关于类风湿性关节炎中的蛋白质-蛋白质关联。
-
告诉我关于类风湿性关节炎的 GWAS 研究发现。
import hashlib
import logging
import os
from typing import Optional, List, Dict, Any
import glob
import boto3
from toolz.itertoolz import partition_all
import json
import funcy
import psycopg
from IPython.display import Markdown, display
from langchain.chains import create_retrieval_chain
from langchain.chains.combine_documents import create_stuff_documents_chain
from langchain.prompts import PromptTemplate
from langchain.retrievers.bedrock import (
AmazonKnowledgeBasesRetriever,
RetrievalConfig,
VectorSearchConfig,
)
from aws_bedrock_utilities.models.base import BedrockBase, RAGResults
from aws_bedrock_utilities.models.pgvector_knowledgebase import BedrockPGWrapper
from sklearn.model_selection import train_test_split
import numpy as np
import pandas as pd
from pprint import pprint
import time
import logging
from rich.logging import RichHandler
我没有在这里列出它,但我总是会对我的知识库进行一些问答测试。选择一篇文章,解析出摘要或结论,然后向大型语言模型(LLM)提问。你应该能得到这篇文章的回复。
结构化你的查询
你首先需要知道你要查询的集合名称和查询内容。
我总是推荐先运行几个问答查询。以不同方式提问同样的显而易见的问题。
你还需要根据你的时间限制和知识库中的文章数量调整MAX_DOCS_RETURNED。LLM 会一直搜索,直到达到最大值,然后停止。如果需要全面搜索,你需要增加这个数值。
# Make sure to keep the collection name consistent!
COLLECTION_NAME = "MY_COLLECTION"
MAX_DOCS_RETURNED = 50
p = BedrockPGWrapper(collection_name=COLLECTION_NAME) credentials.py:1147
#model = "anthropic.claude-3-sonnet-20240229-v1:0"
model = "anthropic.claude-3-haiku-20240307-v1:0"
model = "anthropic.claude-3-haiku-20240307-v1:0"
queries = [
"Tell me about T cell–derived cytokines in relation to rheumatoid arthritis and provide citations and article titles",
"Tell me about single-cell research in rheumatoid arthritis.",
"Tell me about protein-protein associations in rheumatoid arthritis.",
"Tell me about the findings of GWAS studies in rheumatoid arthritis.",
]
ai_responses = []
for query in queries:
answer = p.run_kb_chat(query=query, collection_name= COLLECTION_NAME, model_id=model, search_kwargs={'k': MAX_DOCS_RETURNED, 'fetch_k': 50000 })
ai_responses.append(answer)
time.sleep(1)
for answer in ai_responses:
t = Markdown(f"""
### Query
{answer['query']}### Response
{answer['result']}
""")
display(t)
第三部分:结果!
我们已经建立了知识库,运行了一些查询,现在我们准备查看 LLM 为我们生成的结果。
每个结果都是一个字典,包含原始查询、回应以及相关的源文档片段。
告诉我关于类风湿性关节炎中的 T 细胞来源细胞因子的相关信息,并提供引用和文章标题。
T 细胞衍生的细胞因子在类风湿关节炎(RA)的发病机制中起着关键作用。一些关键发现包括:
已发现 RA 患者滑液中 IL-17 水平升高,IL-17 是由 Th17 细胞产生的细胞因子。IL-17 可以刺激成纤维样滑膜细胞(FLS)和巨噬细胞产生炎症介质,如 VEGF、IL-1、IL-6、TNF-α和前列腺素 E2,并促进破骨细胞形成,从而促进关节炎症和破坏(Honorati 等,2006 年,Schurgers 等,2011 年)。
产生 IFN-γ的 Th1 细胞也与 RA 的发病机制相关。IFN-γ可以诱导巨噬细胞极化为促炎的 M1 表型(Schurgers 等,2011 年,Kebir 等,2009 年,Boniface 等,2010 年)。
CD161+ Th17 细胞能够同时产生 IL-17 和 IFN-γ,它们在 RA 患者的滑膜中富集,并可能促进炎症环境的形成(Afzali 等,2013 年,Bovenschen 等,2011 年,Koenen 等,2008 年,Pesenacker 等,2013 年)。
通常抑制炎症的调节性 T 细胞(Tregs)在 RA 中功能受损,可能导致免疫反应失调(Moradi 等,2014 年,Samson 等,2012 年,Zhang 等,2018 年,Walter 等,2013 年,Wang 等,2018 年,Morita 等,2016 年)。
总结来说,促炎 T 细胞亚群(Th1,Th17)与抗炎 Tregs 之间的不平衡是 RA 发病机制的标志,细胞因子如 IL-17 和 IFN-γ在推动关节炎症和破坏中起着核心作用。
告诉我关于类风湿关节炎中的单细胞研究。
单细胞研究为类风湿关节炎(RA)的发病机制提供了重要的见解:
单细胞 RNA 测序(scRNA-seq)研究已经在 RA 滑膜中识别出不同的细胞状态和亚群,包括病理性 T 细胞亚群,如 T 外周辅助(Tph)细胞和细胞毒性 CD8+ T 细胞。
对 RA 滑膜中 T 细胞受体(TCR)库的分析揭示了 CD4+和 CD8+ T 细胞群体的克隆扩增,表明存在抗原驱动的反应。
scRNA-seq 还表征了 RA 滑膜中扩展的活化 B 细胞、原始浆细胞和浆细胞群体,这些细胞群体展示了显著的克隆关系。
基于 scRNA-seq 数据的受体-配体分析预测了关键的细胞间相互作用,例如 Tph 细胞与 B 细胞之间的相互作用,这可能推动滑膜炎症的发生。
总体来说,单细胞研究揭示了 RA 滑膜中的细胞和分子异质性,识别了特定的免疫细胞亚群和可能作为个性化治疗靶点的途径。
告诉我关于类风湿关节炎中的蛋白质-蛋白质相互作用。
根据提供的背景信息,类风湿关节炎(RA)中一些关键的蛋白质-蛋白质相互作用包括:
类风湿因子(RF)和抗环瓜氨酸蛋白抗体(ACPAs):
类风湿关节炎的前关节阶段约 80%的患者中发现类风湿因子(RF)。
ACPA 对 RA 具有高度特异性,并且可以在临床症状发作前数年检测到。
蛋白精氨酸脱亚氨基酶(PAD)酶和抗 PAD 抗体:
抗 PAD2 抗体与中等病程相关,而抗 PAD4 抗体与更严重和快速进展的 RA 相关。
抗 PAD3/4 抗体可能预示着 RA 相关的间质性肺病的发展。
抗碳酰化蛋白(anti-CarP)抗体:
抗 CarP 抗体存在于 25-50%的 RA 患者中,与 RF 或 ACPA 阳性无关。
抗 CarP 抗体与不良预后和增加的发病率相关,包括 RA 相关的间质性肺病。
丙二醛-乙醛(MAA)加合物和抗 MAA 抗体:
抗 MAA 抗体与无血清 RA 的放射学进展相关。
信号通路中的蛋白质相互作用:
JAK-STAT、MAPK、PI3K-AKT 和 SYK 信号通路都涉及到 RA 的发病机制,并且是治疗干预的潜在靶点。
总的来说,上下文突出了 RA 中几个关键的蛋白质相互作用,包括自身抗体(RF、ACPA、抗 PAD、抗 CarP、抗 MAA)和信号通路组分(JAK、STAT、MAPK、PI3K、SYK),这些在疾病的发病和进展中起着重要作用。
告诉我关于类风湿性关节炎 GWAS 研究结果的情况。
以下是类风湿性关节炎 GWAS 研究的一些关键发现:
全基因组关联研究(GWAS)已经确定了 100 多个与类风湿性关节炎易感性相关的遗传风险位点。
HLA-DRB1 基因是最强的遗传风险因素,约占类风湿性关节炎遗传成分的 50%。含有“共享表位”序列的特定 HLA-DRB1 等位基因与增加的 RA 风险密切相关。
通过 GWAS 识别的其他显著遗传风险因素包括 PTPN22、STAT4、CCR6、PADI4、CTLA4 和 CD40。这些基因参与免疫调节和炎症途径。
遗传风险因素可能在血清阳性(ACPA 阳性)和血清阴性类风湿性关节炎之间有所不同。例如,HLA-DRB1 等位基因与血清阳性 RA 有更强的关联。
GWAS 还确定了与类风湿性关节炎疾病严重程度和治疗反应相关的遗传变异。例如,FCGR3A 和 PTPRC 基因的变异已与抗 TNF 疗法的反应相关联。
总体而言,GWAS 为类风湿性关节炎的遗传结构和发病机制提供了重要见解,这对于开发靶向治疗和个性化治疗方法具有重要意义。
调查源文件
查询知识库将返回与源文件相关的片段。有时,langchain 返回的格式可能有些问题,但您可以随时返回源文件。
x = 0
y = 10
for answer in ai_responses:
for s in answer['source_documents']:
if x <= y:
print(s.metadata)
else:
break
x = x + 1
{'page': 6, 'source': '/home/jovyan/data/pubget_ra/papers/fimmu-12-790122.pdf'}
{'source': '/home/jovyan/data/pubget_ra/pubget_data/query_55c6003c0195b20fd4bdc411f67a8dcf/articles/d52/pmcid_11167034/article.xml'}
{'page': 7, 'source': '/home/jovyan/data/pubget_ra/papers/fimmu-12-790122.pdf'}
{'page': 17, 'source': '/home/jovyan/data/pubget_ra/papers/fimmu-12-790122.pdf'}
{'source': '/home/jovyan/data/pubget_ra/pubget_data/query_55c6003c0195b20fd4bdc411f67a8dcf/articles/657/pmcid_11151399/article.xml'}
{'page': 3, 'source': '/home/jovyan/data/pubget_ra/papers/41392_2023_Article_1331.pdf'}
{'page': 4, 'source': '/home/jovyan/data/pubget_ra/papers/41392_2023_Article_1331.pdf'}
{'page': 5, 'source': '/home/jovyan/data/pubget_ra/papers/fimmu-12-790122.pdf'}
{'source': '/home/jovyan/data/pubget_ra/pubget_data/query_55c6003c0195b20fd4bdc411f67a8dcf/articles/e02/pmcid_11219584/article.xml'}
{'page': 8, 'source': '/home/jovyan/data/pubget_ra/papers/fimmu-12-790122.pdf'}
{'source': '/home/jovyan/data/pubget_ra/pubget_data/query_55c6003c0195b20fd4bdc411f67a8dcf/articles/6fb/pmcid_11203675/article.xml'}
总结
就是这样!我们以低成本创建了一个知识库,使用了 AWS Bedrock 来加载嵌入(embeddings),然后用 Claude LLM 来执行查询。在这里我们使用了 PubMed 论文,但我们也可以使用会议记录、PowerPoint 幻灯片、爬取的网站,或是内部数据库。
如果你有任何问题、评论或教程请求,请随时通过电子邮件联系我,邮箱地址是 jillian@dabbleofdevops.com。
使用客观贝叶斯推断解读选举民意调查
如何构建一个仅基于民意调查的客观贝叶斯模型,从州的民意调查领先转化为赢得该州的概率。
·发布于 Towards Data Science ·7 分钟阅读·2024 年 10 月 30 日
--
随着总统选举临近,我和许多其他人都有一个问题:候选人在某州的民意调查结果是否能够反映出他们赢得该州的概率。
在这篇博客文章中,我想通过使用客观贝叶斯推断([3])和 2016 年及 2020 年的选举结果来探讨这个问题。目标是构建一个简单的仅基于民意调查的模型,该模型以候选人在各州的民意调查领先为基础,生成候选人赢得该州的后验概率分布。

图 1:使用 2016 年和 2020 年的 FiveThirtyEight 民意调查数据([1, 2])以及宾夕法尼亚州的民意调查快照,预测的赢得概率的后验分布示例。图中还展示了预测后验分布的 5 百分位、50 百分位和 95 百分位。图源自作者。
其中,后验分布衡量我们对民意调查预测准确度的信任程度。
对于该模型,我将使用逻辑回归,并且包含一个未知权重变量 w:
以 2020 年和 2016 年的选举结果为观察数据,并使用适当的先验分布π,我们可以生成一个未知权重的后验分布。
其中
使用离线强化学习试验在线平台干预
离线强化学习与仿真用于策划在线平台的用户参与。
·发表于 Towards Data Science ·10 分钟阅读·2024 年 11 月 4 日
--

图片来源:Fernando Freitas 于 Unsplash
概述
本文基于我们在使用深度学习预测在线平台的用户参与度的研究进行扩展。我们发现,预测用户行为依赖于足够的历史数据,这与现有的相关研究一致。
非付费平台通常通过徽章、奖章和在线奖励来鼓励和奖励用户参与。虽然这些措施可能有效,但往往会产生意想不到的行为和混合的结果。例如,Coursera和StackOverflow都曾观察到“引导效应”。用户常常为了获得徽章而参与,之后则会脱离平台。尽管这在短期内提高了参与度,但它只是一个有限的策略,无法将少数用户转化为长期参与者。

在线激励的引导效应示例。参考自 Yanovsky、Hoernle 与Gal 对 StackOverflow 中激励寻求的研究。
尝试激励措施也可能带来风险。一个名为 Old Weather 的 Zooniverse 项目试验了一种竞争排名策略,结果适得其反。
使用 OpenAI 和 PandasAI 进行 Series 操作
将自然语言查询和操作融入到你的 Python 数据清洗工作流中。
·发布于 Towards Data Science ·阅读时间:6 分钟·2024 年 6 月 29 日
--

红熊猫画作由艺术家 Karen Walker 捐赠。
我们在进行 pandas 数据清洗项目时需要执行的许多 Series 操作,可以通过 AI 工具来辅助完成,其中包括 PandasAI。PandasAI 利用 OpenAI 等大型语言模型,实现对数据列的自然语言查询和操作。在这篇文章中,我们将探讨如何使用 PandasAI 查询 Series 值、创建新的 Series、按条件设置 Series 值以及重塑数据。
你可以通过在终端或 Windows Powershell 中输入 pip install pandasai 来安装 PandasAI。你还需要从 openai.com 获取一个令牌,以便向 OpenAI API 发送请求。
由于 PandasAI 库正在快速发展,你可以根据使用的 PandasAI 和 pandas 版本,预期会有不同的结果。在这篇文章中,我使用的是 PandasAI 版本 1.4.8 和 pandas 版本 1.5.3。
我们将使用来自美国劳动统计局(Bureau of Labor Statistics)进行的《国家青少年纵向研究》(NLS)中的数据。NLS 对同一批高中生进行了超过 25 年的调查,涵盖了关于教育成果和每年工作周数等众多有用的数据项。该数据公开可用,网址为 nlsinfo.org。(NLS 的公开发布受美国政府 开放数据政策 的保护,允许非商业和商业用途。)
我们还将使用由 Our World in Data 提供的 COVID-19 数据。该数据集每天为每个国家提供一行,包含新病例和新死亡人数。此数据集可以在 ourworldindata.org/covid-cases 下载,采用 Creative Commons CC BY 4.0 许可。你也可以从 GitHub 下载本文中使用的所有代码和数据。
我们首先从 PandasAI 导入 OpenAI 和 SmartDataframe 模块。我们还需要实例化一个 llm 对象:
import pandas as pd
from pandasai.llm.openai import OpenAI
from pandasai import SmartDataframe
llm = OpenAI(api_token="Your API Token")
接下来,我们加载将要使用的 DataFrame,并从 NLS pandas DataFrame 创建一个 SmartDataframe 对象:
covidcases = pd.read_csv("data/covidcases.csv")
nls97 = pd.read_csv("data/nls97f.csv")
nls97.set_index("personid", inplace=True)
nls97sdf = SmartDataframe(nls97, config={"llm": llm})
现在我们准备好生成来自 SmartDataframe 的 Series 的汇总统计数据。我们可以请求单个 Series 或多个 Series 的平均值:
nls97sdf.chat("Show average of gpaoverall")
2.8184077281812128
nls97sdf.chat("Show average for each weeks worked column")
Average Weeks Worked
0
weeksworked00 26.42
weeksworked01 29.78
weeksworked02 31.83
weeksworked03 33.51
weeksworked04 35.10
weeksworked05 37.34
weeksworked06 38.44
weeksworked07 39.29
weeksworked08 39.33
weeksworked09 37.51
weeksworked10 37.12
weeksworked11 38.06
weeksworked12 38.15
weeksworked13 38.79
weeksworked14 38.73
weeksworked15 39.67
weeksworked16 40.19
weeksworked17 40.37
weeksworked18 40.01
weeksworked19 41.22
weeksworked20 38.35
weeksworked21 36.17
weeksworked22 11.43
我们还可以通过另一个 Series 来汇总 Series 值,通常是一个类别型 Series:
nls97sdf.chat("Show satmath average by gender")
Female Male
0 486.65 516.88
我们还可以通过 SmartDataframe 的 chat 方法创建一个新的 Series。我们不需要使用实际的列名。例如,当我们写 child at home 时,PandasAI 会识别出我们需要的是childathome Series:
nls97sdf = nls97sdf.chat("Set childnum to child at home plus child not at
home")
nls97sdf[['childnum','childathome','childnotathome']].\
sample(5, random_state=1)
childnum childathome childnotathome
personid
211230 2.00 2.00 0.00
990746 3.00 3.00 0.00
308169 3.00 1.00 2.00
798458 NaN NaN NaN
312009 NaN NaN NaN
我们可以使用chat方法有条件地创建 Series 值:
nls97sdf = nls97sdf.chat("evermarried is 'No' when maritalstatus is
'Never-married', else 'Yes'")
nls97sdf.groupby(['evermarried','maritalstatus']).size()
evermarried maritalstatus
No Never-married 2767
Yes Divorced 669
Married 3068
Separated 148
Widowed 23
dtype: int64
PandasAI 对你在这里使用的语言非常灵活。例如,以下语句提供了相同的结果:
nls97sdf = nls97sdf.chat("if maritalstatus is 'Never-married' set
evermarried2 to 'No', otherwise 'Yes'")
nls97sdf.groupby(['evermarried2','maritalstatus']).size()
evermarried2 maritalstatus
No Never-married 2767
Yes Divorced 669
Married 3068
Separated 148
Widowed 23
dtype: int64
我们可以对一些相似命名的列进行计算:
nls97sdf = nls97sdf.chat("Set weeksworked for each row to the average of
all weeksworked columns for that row")
这将计算所有 weeksworked00 到 weeksworked22 列的平均值,并将其赋值给一个新列,命名为 weeksworked。
我们可以轻松地根据汇总统计数据填充缺失的值:
nls97sdf.gpaenglish.describe()
count 5,798
mean 273
std 74
min 0
25% 227
50% 284
75% 323
max 418
Name: gpaenglish, dtype: float64
nls97sdf = nls97sdf.chat("set missing gpaenglish to the average")
nls97sdf.gpaenglish.describe()
count 8,984
mean 273
std 59
min 0
25% 264
50% 273
75% 298
max 418
Name: gpaenglish, dtype: float64
我们还可以使用 PandasAI 做一些重塑。回想一下,COVID-19 案例数据为每个国家每一天都有新病例数据。假设我们只想要每个国家的第一行数据。我们可以通过传统方法使用drop_duplicates来实现:
firstcase = covidcases.\
sort_values(['location','casedate']).\
drop_duplicates(['location'], keep='first')
firstcase.set_index('location', inplace=True)
firstcase.shape
(231, 67)
firstcase[['iso_code','continent','casedate',
'total_cases','new_cases']].head(2).T
location Afghanistan Albania
iso_code AFG ALB
continent Asia Europe
casedate 2020-03-01 2020-03-15
total_cases 1.00 33.00
new_cases 1.00 33.00
我们也可以通过创建一个 SmartDataframe 并使用 chat 方法来获得相同的结果。我在这里使用的自然语言非常简洁,展示每个国家的第一个 casedate 和 location 及其他值:
covidcasessdf = SmartDataframe(covidcases, config={"llm": llm})
firstcasesdf = covidcasessdf.chat("Show first casedate and location and
other values for each country.")
firstcasesdf.shape
(231, 7)
firstcasesdf[['location','continent','casedate',
'total_cases','new_cases']].head(2).T
iso_code ABW AFG
location Aruba Afghanistan
continent North America Asia
casedate 2020-03-22 2020-03-01
total_cases 5.00 1.00
new_cases 5.00 1.00
注意,PandasAI 会智能地选择要获取的列。我们只获取需要的列,而不是所有列。我们也可以直接传递我们想要的列名给chat。 (PandasAI 按 iso_code 排序行,而不是按位置排序,这就是为什么第一行不同的原因。)
使用 PandasAI 时,很多工作实际上只是导入相关的库并实例化大型语言模型和 SmartDataframe 对象。一旦完成这些,简单的句子通过 SmartDataframe 的 chat 方法发送,就足以总结 Series 值并创建新的 Series。
PandasAI 擅长从 Series 生成简单的统计数据。我们甚至不需要准确记住 Series 的名称。我们使用的自然语言往往比传统的 pandas 方法(如 groupby)更直观。按性别显示 satmath 平均值 传递给 chat 的值就是一个很好的例子。
对 Series 的操作,包括创建新的 Series,也非常简单。我们通过指示 SmartDataframe 将家里有孩子的人数与家里没有孩子的人数相加,创建了一个表示孩子总数的 Series(childnum)。我们甚至没有提供字面上的 Series 名称,分别是 childathome 和 childnotathome。PandasAI 自动理解了我们的意思。
由于我们为 Series 操作传递给 chat 的是自然语言指令,因此没有唯一的正确方法来获得我们想要的结果。例如,当我们将 evermarried is ‘No’ when maritalstatus is ‘Never-married’, else ‘Yes’ 传递给 chat 时,结果与我们将 if maritalstatus is ‘Never-married’ set evermarried2 to ‘No’, otherwise ‘Yes’ 传递给 chat 时相同。
我们还可以通过简单的自然语言指令进行相当广泛的 DataFrame 重塑,就像我们提供的最后一个命令一样。我们在指令中加入了 and other values,以获取除了 casedate 之外的列。PandasAI 还自动识别出 location 作为索引是有意义的。
你可以在这里阅读更多关于如何使用 PandasAI 和 SmartDataframes 的内容:
PandasAI 支持多种大型语言模型(LLMs),用于从自然语言查询中生成代码。
docs.pandas-ai.com](https://docs.pandas-ai.com/llms?source=post_page-----2978d539fb66--------------------------------)
或者在我书的第二版《Python 数据清洗 Cookbook》中:
[## Python 数据清洗 Cookbook - 第二版:使用 pandas、NumPy 准备数据进行分析…
Python 数据清洗 Cookbook - 第二版:使用 pandas、NumPy、Matplotlib 准备数据进行分析…
packt.link](https://packt.link/hKI1y?source=post_page-----2978d539fb66--------------------------------)
祝你数据清洗顺利,期待听到你的进展!
使用 PCA 进行异常值检测简介
一种出乎意料的有效方法,用于识别数值数据中的异常值
·发表于Towards Data Science ·14 分钟阅读·2024 年 10 月 22 日
--
PCA(主成分分析)在数据科学中广泛应用,通常用于降维(并且经常用于可视化),但实际上它对于异常值检测也非常有用,我将在本文中描述这一点。
本文是我关于异常值检测系列的继续,系列中还包括关于 FPOF、计数异常值检测器、距离度量学习、共享最近邻和掺杂的相关文章。此外,还包括我书中的另一个摘录——《Python 中的异常值检测》Outlier Detection in Python。
PCA 背后的思想是,大多数数据集在某些列中具有比其他列更大的方差,并且特征之间通常存在相关性。由此可得的一个含义是:为了表示数据,我们通常不需要使用所有的特征;我们可以使用更少的特征来很好地近似数据——有时甚至是远远少于原始特征数量。举个例子,假设我们有一张包含 100 个特征的数值数据表,我们可能能够用 30 或 40 个特征(甚至更少)来合理地表示这些数据,可能会远远少于原特征数量。
为了实现这一点,PCA 将数据转换到一个不同的坐标系统中,其中的维度被称为主成分。
鉴于我们常常面临由于维度灾难导致的异常值检测问题,处理更少的特征可能非常有利。如在共享最近邻和距离度量学习用于异常值检测中所述,处理大量特征会使得异常值检测变得不可靠;高维数据的一个问题是,它会导致点与点之间的距离计算不准确(许多异常值检测器依赖此计算)。PCA 可以减轻这些影响。
同时,令人惊讶的是,使用 PCA 通常会创造出一个异常值更容易被检测的情况。PCA 转换往往会重新塑造数据,使得任何异常点更容易被识别。
这里展示了一个示例。
import numpy as np
import pandas as pd
from sklearn.decomposition import PCA
# Create two arrays of 100 random values, with high correlation between them
x_data = np.random.random(100)
y_data = np.random.random(100) / 10.0
# Create a dataframe with this data plus two additional points
data = pd.DataFrame({'A': x_data, 'B': x_data + y_data})
data= pd.concat([data,
pd.DataFrame([[1.8, 1.8], [0.5, 0.1]], columns=['A', 'B'])])
# Use PCA to transform the data to another 2D space
pca = PCA(n_components=2)
pca.fit(data)
print(pca.explained_variance_ratio_)
# Create a dataframe with the PCA-transformed data
new_data = pd.DataFrame(pca.transform(data), columns=['0', '1'])
这首先创建了原始数据,如左侧窗格所示。然后,它使用 PCA 进行转换。一旦完成,我们就得到了新的数据空间,如右侧窗格所示。

这里我创建了一个简单的合成数据集,数据高度相关。数据中有两个异常值,一个遵循一般模式,但极端(点 A),另一个在每个维度上有典型值,但不遵循一般模式(点 B)。
然后,我们使用 scikit-learn 的 PCA 类来转换数据。其输出被放入另一个 pandas 数据框中,之后可以进行绘图(如所示),或者检查异常值。
查看原始数据时,数据倾向于沿对角线分布。从左下角到右上角画一条线(图中的蓝线),我们可以创建一个新的单一维度,很好地表示数据。事实上,执行 PCA 时,这将是第一个主成分,与其正交的线(橙色线,也显示在左侧窗格中)则是第二个主成分,表示剩余的方差。
对于更现实的数据,我们通常不会有如此强的线性关系,但几乎总会有一些特征之间的关联——特征完全独立是很少见的。鉴于此,PCA 通常是减少数据集维度的有效方法。也就是说,虽然通常需要使用所有主成分来完全描述每个项目,但仅使用一部分主成分通常就能充分描述每条记录(或几乎每条记录)。
右侧窗格显示了 PCA 转换后新空间中的数据,第一个主成分(捕捉了大部分方差)位于 x 轴上,第二个主成分(捕捉剩余方差)位于 y 轴上。在二维数据的情况下,PCA 转换将只是旋转并拉伸数据。对于更高维度的数据,转换较难可视化,但工作原理类似。
打印解释方差(上面的代码中包括一个打印语句来显示这一点)表明组件 0 包含了 0.99 的方差,而组件 1 包含了 0.01,这与图表非常匹配。
通常会逐个检查每个组件(例如作为直方图),但在这个例子中,我们使用散点图,这样可以一次查看两个组件,节省空间。异常值在这两个组件中作为极端值明显突出。
更仔细地看一下 PCA 的工作细节,它首先找到一条最能描述数据的直线。这是所有点到该直线的平方距离最小的直线。这就是第一个组件。然后,过程会找到一条与这条直线正交的直线,最好地捕捉剩余的方差。这个数据集只有两个维度,因此第二个组件的方向只有一个选择,即与第一个组件垂直。
当原始数据中有更多维度时,这个过程将继续若干额外的步骤:直到数据中的所有方差被捕捉为止,这将创建与原始数据维度数量相同的组件。鉴于此,PCA 具有三个特性:
-
所有组件都是不相关的。
-
第一个组件具有最多的变异性,第二个组件次之,依此类推。
-
组件的总方差等于原始特征的方差。
PCA 还有一些有助于异常值检测的良好特性。正如我们在图中看到的,异常值在组件内被很好地分离开来,这使得简单的测试可以识别它们。
我们还可以看到 PCA 转换的另一个有趣结果:符合一般模式的点往往会落在早期的组件上,但可能在这些组件中表现为极端值(如点 A),而不符合数据一般模式的点则往往不会落在主要组件上,而是在后续组件中成为极端值(如点 B)。
使用 PCA 识别异常值有两种常见方法:
-
我们可以使用 PCA 转换数据,然后使用一组测试(方便的是,这些测试通常非常简单)对每个组件进行评分,逐行评估。这非常简单,容易编码实现。
-
我们可以查看重建误差。在图中,我们可以看到仅使用第一个主成分就能很好地描述大多数数据。第二个主成分对于完整描述所有数据是必要的,但通过仅将数据投影到第一个主成分上,我们可以合理地描述大部分数据的位置。例外的是点 B;它在第一个主成分上的位置并不能很好地描述它的完整位置,若仅使用单一主成分来描述该点,会出现较大的重建误差,尽管其他点没有这个问题。一般来说,需要更多主成分来描述一个点的位置较好(或者在固定数量主成分的情况下,误差较大),则该点是一个离群点的可能性越强。
另一种方法是逐行删除数据,找出哪些行对最终 PCA 计算影响最大。虽然这种方法效果不错,但通常较慢,且不常使用。我可能会在未来的文章中讨论这个方法,但本文将关注重建误差,下一篇文章将讨论如何对 PCA 主成分进行简单测试。
重建误差是离群点检测中常见的一种通用方法示例。我们以某种方式对数据进行建模,以捕捉数据中的主要模式(例如,使用频繁项集、聚类、创建预测模型来预测各列的值等)。这些模型通常能很好地拟合大多数数据,但往往无法很好地拟合离群点。离群点通常是那些无法很好地融入模型的记录。它们可能是没有被频繁项集很好表示的记录,无法融入任何一个聚类,或是无法通过其他列的值很好地预测的记录。在这种情况下,离群点是那些没有被主要(第一个)PCA 主成分很好表示的记录。
PCA 在离群点检测中的假设
PCA 假设特征之间存在相关性。上述数据可以通过转换,使得第一主成分比第二主成分捕捉到更多的方差,因为数据是相关的。对于特征之间没有关联的离群点检测,PCA 提供的价值较小,但考虑到大多数数据集之间具有显著的相关性,PCA 通常适用。基于此,我们通常可以找到一个合理的小数量的主成分,捕捉数据集中大部分的方差。
与其他常见的离群点检测技术相似,包括椭圆包络方法、高斯混合模型和马氏距离计算,PCA 通过创建一个协方差矩阵来工作,该矩阵表示数据的一般形状,然后用于转换空间。事实上,椭圆包络方法、马氏距离和 PCA 之间有很强的对应关系。
协方差矩阵是一个 d x d 的矩阵(其中 d 是数据中特征或维度的数量),它存储了每对特征之间的协方差,每个特征的方差存储在主对角线上(即每个特征与自身的协方差)。协方差矩阵与数据中心一起,提供了数据的简明描述——也就是说,每个特征的方差以及特征之间的协方差,通常是数据的一个非常好的描述。
一个具有三个特征的数据集的协方差矩阵可能如下所示:

三个特征的数据集的示例协方差矩阵
这里显示的是三个特征的方差,位于主对角线:1.57、2.33 和 6.98。我们还可以看到每个特征之间的协方差。例如,第 1 个和第 2 个特征之间的协方差是 1.50。矩阵在主对角线对称,因为第 1 个和第 2 个特征之间的协方差与第 2 个和第 1 个特征之间的协方差相同,依此类推。
Scikit-learn(以及其他包)提供了可以计算任意给定数值数据集的协方差矩阵的工具,但使用本文及下一篇文章中描述的技术,直接计算协方差矩阵并不是必须的。在本文中,我们将查看一个名为PyOD的流行异常值检测包所提供的工具(它可能是目前 Python 中最完整、最常用的表格数据异常值检测工具)。这些工具为我们处理了 PCA 变换以及异常值检测。
PCA 在异常值检测中的局限性
PCA 的一个局限性是,它对异常值敏感。PCA 基于最小化点到主成分的平方距离,因此会受到异常值的强烈影响(远离点的平方距离可能非常大)。为了解决这个问题,稳健 PCA通常会被使用,在执行变换之前,会先移除每个维度中的极值。下面的示例包括了这一点。
PCA 的另一个局限性(以及马哈拉诺比斯距离和类似方法)是,如果相关性仅存在于数据的某些区域,当数据呈簇状分布时,这种情况往往会发生。在数据聚类良好的情况下,可能需要先对数据进行聚类(或分段),然后再对每个数据子集执行 PCA。
基于 PCA 的异常值检测测试
现在我们已经了解了 PCA 的工作原理,并大致了解了它如何应用于异常值检测,我们可以看看 PyOD 提供的检测器。
PyOD 实际上提供了三种基于 PCA 的类:PyODKernelPCA、PCA 和 KPCA。我们将逐一了解这些类。
PyODKernelPCA
PyOD 提供了一个名为 PyODKernelPCA 的类,它实际上是 scikit-learn 的 KernelPCA 类的封装。在不同情况下,使用这两者中的任何一个都可能更方便。它本身并不是一个离群点检测器,仅提供 PCA 变换(及其逆变换),类似于在前面示例中使用的 scikit-learn 的 PCA 类。
KernelPCA 类与 PCA 类不同,KernelPCA 允许对数据进行非线性转换,并且可以更好地建模一些更复杂的关系。在这种情况下,核函数的作用与 SVM 模型中的作用相似:它们以一种非常高效的方式变换空间,使得离群点能够更容易地被分离。
Scikit-learn 提供了几种核函数。虽然这些超出了本文的范围,但在特征之间存在复杂的非线性关系时,它们可以改进 PCA 过程。如果使用这些核函数,离群点检测将有效,否则,与使用 PCA 类时相同。也就是说,我们可以直接在变换后的空间上运行离群点检测测试,或者测量重建误差。
前一种方法,在变换后的空间上进行测试,是相当直接且有效的。我们将在下一篇文章中更详细地讨论这一点。后一种方法,检查重建误差,则稍显复杂。它并不是完全无法处理,但我们接下来要讨论的 PyOD 提供的两个检测器可以为我们处理大部分工作。
PCA 检测器
PyOD 提供了两种基于 PCA 的离群点检测器:PCA 类和 KPCA。后者与 PyODKernelPCA 类似,允许核函数处理更复杂的数据。PyOD 推荐在数据包含线性关系时使用 PCA 类,而在数据包含非线性关系时使用 KPCA。
这两个类使用数据的重建误差,利用点到超平面的欧几里得距离,这个超平面是用前 k 个组件构建的。这个想法再次是,前 k 个组件能够很好地捕捉数据的主要模式,而任何没有被这些组件很好建模的点就是离群点。
在上面的图中,这种方法不能捕捉到 A 点,但可以捕捉到 B 点。如果我们将 k 设置为 1,我们只会使用一个组件(第一个组件),并且会测量每个点从其实际位置到该组件上位置的距离。B 点的距离会很大,因此可以标记为离群点。
与一般的 PCA 一样,最好在拟合数据之前去除任何明显的离群点。在下面的例子中,我们使用了 PyOD 提供的另一种检测器,叫做 ECOD(经验累积分布函数)。ECOD 是一个你可能不太熟悉的检测器,但它是一个非常强大的工具。事实上,PyOD 建议在为项目选择检测器时,从 Isolation Forest 和 ECOD 开始。
ECOD 超出了本文的范围,相关内容可以在《Python 中的异常值检测》一书中找到,PyOD 也提供了原始期刊论文的链接。不过,简单概括:ECOD 基于经验累积分布,旨在找到数值列中的极端(非常小和非常大)值。它不会检查值的稀有组合,只检测极端值。因此,它无法找到所有的异常值,但它速度较快,并且非常擅长找到这种类型的异常值。在此案例中,我们在拟合 PCA 检测器之前,移除了 ECOD 标识的前 1%行。
一般来说,在执行异常值检测时(不仅仅是使用 PCA 时),最好先清理数据,在异常值检测的上下文中,通常指的是移除任何强异常值。这可以让异常值检测器在更典型的数据上进行训练,从而更好地捕捉数据中的强模式(这样它就能更好地识别这些强模式的例外情况)。在这个案例中,清理数据允许在更典型的数据上进行 PCA 计算,从而更好地捕捉数据的主要分布。
在执行之前,必须安装 PyOD,可以使用以下命令进行安装:
pip install pyod
这里的代码使用了来自 OpenML 的speech数据集(公共许可),该数据集包含 400 个数值特征。任何数值数据集都可以使用(任何类别列需要进行编码)。此外,一般来说,所有数值特征都需要进行缩放,以便它们处于相同的量纲下(此处为了简洁省略了,因为所有特征使用了相同的编码)。
import pandas as pd
from pyod.models.pca import PCA
from pyod.models.ecod import ECOD
from sklearn.datasets import fetch_openml
#A Collects the data
data = fetch_openml("speech", version=1, parser='auto')
df = pd.DataFrame(data.data, columns=data.feature_names)
scores_df = df.copy()
# Creates an ECOD detector to clean the data
clf = ECOD(contamination=0.01)
clf.fit(df)
scores_df['ECOD Scores'] = clf.predict(df)
# Creates a clean version of the data, removing the top
# outliers found by ECOD
clean_df = df[scores_df['ECOD Scores'] == 0]
# Fits a PCA detector to the clean data
clf = PCA(contamination=0.02)
clf.fit(clean_df)
# Predicts on the full data
pred = clf.predict(df)
执行此操作后,pred 变量将包含数据中每条记录的异常值得分。
KPCA 检测器
KPCA 检测器的工作方式与 PCA 检测器非常相似,不同之处在于对数据应用了指定的核。这可以显著地改变数据。两个检测器可能标记非常不同的记录,而且由于两者的可解释性较差,可能很难确定原因。像异常值检测一样,可能需要一些实验来确定哪个检测器和参数最适合你的数据。由于两者都是强大的检测器,使用两者结合可能也会很有用。最好的检测器和参数(以及如何使用它们)可能可以通过《Doping:一种测试异常值检测器的技术》中的方法来确定。
要创建一个使用线性核的 KPCA 检测器,我们可以使用如下代码:
det = KPCA(kernel='linear')
KPCA 还支持多项式、径向基函数、Sigmoid 和余弦核。
结论
在本文中,我们讨论了 PCA 背后的理念以及它如何帮助异常值检测,特别是查看 PCA 转换数据上的标准异常值检测测试和重构误差。我们还查看了 PyOD 提供的两个基于 PCA 的异常值检测器(都使用重构误差),PCA 和 KPCA,并提供了使用前者的示例。
基于 PCA 的异常值检测可以非常有效,但确实存在可解释性差的问题。PCA 和 KPCA 检测器产生的异常值非常难以理解。
实际上,即使使用可解释的异常值检测器(例如计数异常值检测器,或者基于 z-score 或四分位距的检测方法),在 PCA 转换后的数据上(正如我们将在下一篇文章中看到的那样),异常值也可能难以理解,因为 PCA 转换本身(以及它生成的主成分)几乎是无法解释的。不幸的是,这是异常值检测中的一个常见主题。用于异常值检测的其他主要工具,包括隔离森林、局部异常因子(LOF)、k 近邻(KNN)等,基本上也是黑箱(它们的算法容易理解——但给每个记录分配的具体分数可能很难理解)。
在上面的二维示例中,当查看 PCA 转换后的空间时,可以很容易看出点 A 和点 B 是异常值,但很难理解作为坐标轴的两个主成分。

在需要可解释性的情况下,可能无法使用基于 PCA 的方法。然而,在不需要可解释性的情况下,基于 PCA 的方法可以非常有效。而且,PCA 的可解释性并不低于大多数异常值检测器;不幸的是,只有少数异常值检测器提供了较高的可解释性。
在下一篇文章中,我们将进一步探讨如何在 PCA 转换后的空间上进行测试。这包括简单的单变量测试,以及其他标准的异常值检测器,考虑到所需的时间(包括 PCA 转换、模型拟合和预测)以及准确性。使用 PCA 通常可以提高异常值检测的速度、内存使用效率和准确性。
所有图片均由作者提供
使用 Poetry 和 Docker 将你的模型打包为 AWS Lambda 适用格式
这是一个可访问的教程,介绍了一种将模型投入生产的方法,特别关注过程中可能遇到的故障排除和问题。
·发表于 Towards Data Science ·阅读时间 10 分钟·2024 年 1 月 29 日
--

我喜欢把模型看作是小小的生物。照片由 Jiawei Zhao 提供,来源于 Unsplash
正如承诺的,这一周我将带来一个更为技术性的主题,稍微休息一下之前讨论的商业话题。我最近有机会使用 AWS Lambda 部署一个新模型,并且在将我常用的开发工具(Poetry)与 Lambda 基础设施结合时学到了一些东西。(特别感谢我的队友 Aaron 教我新知识!)我将逐步讲解如何将一个本地训练的模型成功部署到 Lambda,尤其是那些不太明显的步骤。
对于那些对模型开发的细节不感兴趣的常读者,不用担心,下次我会回到关于社会问题和机器学习的讨论!
设置你的模型架构
如果你还没有选择偏好的 Python 包管理器/环境管理器工具,那么让我推荐一下 Poetry。我花了一些时间才入门并掌握它,但现在已经使用了几年,成了它的忠实粉丝。有些人偏好 venv 或其他更简洁的工具,这也是可以的,但我认为 Poetry 有一些很不错的附加功能,值得一试。(如果你对 Poetry 没有任何经验,请访问官方文档 python-poetry.org/,它们可以帮助你进行设置。)
我想强调的一个卖点是,Poetry 使得将项目打包变得非常容易,这样你创建的内部模块就可以不需要太多麻烦地被调用。这意味着你不需要再和“Python 说那个模块不存在”的问题作斗争,我敢肯定我们许多人都对这种情况感到熟悉。
这里嵌入的示例仅仅是一个类似于这个项目的pyproject.toml文件的头部——请注意那行以packages开头的内容,它告诉这个环境包含我正在创建的包并将其导入。这使得我可以在项目内部的任何地方调用类似from new_package.tools import stuff的内容,即使这些内容不在直接的父目录中或其他地方。
[tool.poetry]
name = "new_package"
version = "0.1.0"
description = "What this package is gonna do"
authors = [
"Stephanie Kirmer <stephanie@stephaniekirmer.com>",
]
packages = [{ include = "new_package"}]
include = [{ path = "tests", format = "sdist" }]
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
# Requirements
[tool.poetry.dependencies]
python = ">=3.9, <4.0"
假设你已经被 Poetry 说服,那么你可以用它来定义你的环境并管理所有的依赖项,你将在这个项目内部开发模型及其管道。去吧,构建并训练你的模型,等那部分完成后再回来。我等你。
好的,欢迎回来!因为你知道你将通过 Docker 在 Lambda 中部署这个模型,这决定了你的推理管道应该如何构建。
你需要构建一个“处理器”。这究竟是什么?它只是一个接受传递给 Lambda 的 JSON 对象的函数,并返回你模型的结果,仍然是一个 JSON 负载。因此,你的推理管道将要做的所有事情都需要在这个函数内部调用。
对于我的项目,我有一个完整的特征工程函数代码库:包括语义嵌入、大量聚合、正则表达式等等。我已经将它们整合到了一个FeatureEngineering类中,这个类有许多私有方法,但只有一个公共方法,feature_eng。因此,从传递给模型的 JSON 开始,这个方法可以运行所有步骤,将数据从“原始”转换为“特征”。我喜欢以这种方式设置,因为它将很多复杂性从处理函数本身抽象了出来。我实际上可以这样调用:
fe = FeatureEngineering(input=json_object)
processed_features = fe.feature_eng()
然后,我就开始工作了,我的特征数据整洁且准备好使用。
请注意:我已经对这个类的所有内部实现编写了详尽的单元测试,因为虽然以这种方式编写代码很整洁,但我仍然需要非常小心任何可能在后台发生的变化。编写单元测试!如果你做了一个小小的改变,你可能无法立即发现你破坏了管道中的某些内容,直到它已经开始引发问题。
后半部分是推理工作,在我的情况下这是一个单独的类。我采用了一个非常类似的方法,它只需要几个参数。
ps = PredictionStage(features=processed_features)
predictions = ps.predict(
feature_file="feature_set.json",
model_file="classifier",
)
类初始化接受特征工程类方法的结果,从而清晰地定义了握手过程。接着,预测方法接受两个参数:特征集(一个列出所有特征名称的 JSON 文件)和模型对象,在我的案例中是我已经训练并保存的 CatBoost 分类器。我使用的是原生的 CatBoost 保存方法,但无论你使用什么方法和模型算法都可以。关键是这个方法抽象了大量底层操作,整洁地返回了predictions对象,这就是我的 Lambda 在运行时会返回给你的内容。
总结一下,我的“处理函数”基本上就是这样:
def lambda_handler(json_object, _context):
fe = FeatureEngineering(input=json_object)
processed_features = fe.feature_eng()
ps = PredictionStage(features=processed_features)
predictions = ps.predict(
feature_file="feature_set.json",
model_file="classifier",
)
return predictions.to_dict("records")
就这么简单!你可能需要为格式错误的输入添加一些控制措施,以防你的 Lambda 收到空 JSON、列表或其他奇怪的数据,但这不是必需的。然而,确保输出是 JSON 或类似格式的(在这里我返回的是一个字典)。
构建你的 Docker 镜像
一切都很棒,我们有一个带有完全定义的环境和所有依赖项的 Poetry 项目,还能够加载我们创建的模块等。很不错。但现在我们需要将其转换成一个 Docker 镜像,以便可以放到 AWS 上。
在这里,我展示了适用于此情况的 dockerfile 框架。首先,我们从 AWS 拉取正确的 Lambda 基础镜像。接着,我们需要设置 Docker 镜像内部将使用的文件结构。这可能与您在 Poetry 项目中的结构完全相同——但我这里的结构并不完全一样,因为我这里有一些额外的无关代码,包括我的训练代码。我只需要将推理相关的内容放入该镜像即可。
dockerfile 的开始部分
FROM public.ecr.aws/lambda/python:3.9
ARG YOUR_ENV
ENV NLTK_DATA=/tmp
ENV HF_HOME=/tmp
在这个项目中,任何你复制过来的内容都会存放在 /tmp 文件夹中,因此如果你的项目中有包会在任何时刻尝试保存数据,你需要确保它们保存到正确的位置。
你还需要确保 Poetry 在你的 Docker 镜像中正确安装——这将使你精心挑选的所有依赖项正常工作。在这里,我设置了版本并告诉 pip 在继续之前安装 Poetry。
ENV YOUR_ENV=${YOUR_ENV} \
POETRY_VERSION=1.7.1
ENV SKIP_HACK=true
RUN pip install "poetry==$POETRY_VERSION"
接下来的问题是确保你的项目在本地使用的所有文件和文件夹都被正确地添加到这个新镜像中——Docker 的复制操作有时会将目录扁平化,所以如果你构建完成后遇到“找不到模块”的问题,检查一下是否发生了这种情况。提示:在所有内容复制到镜像后,可以在 dockerfile 中添加 RUN ls -R 来查看目录结构。你可以在 Docker 中查看这些日志,可能会揭示任何问题。
另外,确保你复制了所有需要的文件!这包括 Lambda 文件、Poetry 文件、特征列表文件和模型。除非你将这些文件存储在其他地方,比如 S3,并让 Lambda 动态下载它们,否则这些都是必需的。(这对于开发像这样的东西来说是完全合理的策略,但不是我们今天要做的事情。)
WORKDIR ${LAMBDA_TASK_ROOT}
COPY /poetry.lock ${LAMBDA_TASK_ROOT}
COPY /pyproject.toml ${LAMBDA_TASK_ROOT}
COPY /new_package/lambda_dir/lambda_function.py ${LAMBDA_TASK_ROOT}
COPY /new_package/preprocessing ${LAMBDA_TASK_ROOT}/new_package/preprocessing
COPY /new_package/tools ${LAMBDA_TASK_ROOT}/new_package/tools
COPY /new_package/modeling/feature_set.json ${LAMBDA_TASK_ROOT}/new_package
COPY /data/models/classifier ${LAMBDA_TASK_ROOT}/new_package
我们快完成了!最后你需要做的就是安装你的 Poetry 环境,然后设置处理程序以便运行。这里有几个重要的标志,包括--no-dev,它告诉 Poetry 不要添加你环境中的任何开发者工具,比如 pytest 或 black。
dockerfile 的结尾
RUN poetry config virtualenvs.create false
RUN poetry install --no-dev
CMD [ "lambda_function.lambda_handler" ]
就这样,你已经有了 dockerfile!现在是时候构建它了。
-
确保 Docker 已安装并在你的电脑上运行。这可能需要一些时间,但不会太难。
-
进入你的 dockerfile 所在的目录,这应该是你项目的顶层目录,然后运行
docker build .让 Docker 进行构建,完成后它会停止返回信息。你可以在 Docker 应用的控制台中查看构建是否成功。 -
返回终端并运行
docker image ls,你会看到刚刚构建的新镜像,并且会附带一个 ID 号。 -
再次从终端运行
docker run -p 9000:8080 IMAGE ID NUMBER,并填入第 3 步中的 ID 号。现在你的 Docker 镜像将开始运行! -
打开一个新的终端(Docker 已经连接到你原来的窗口,保持在那里),你可以向正在通过 Docker 运行的 Lambda 传递数据。我个人喜欢将输入数据放在一个 JSON 文件中,比如
lambda_cases.json,然后像这样运行:
curl -d @lambda_cases.json http://localhost:9000/2015-03-31/functions/function/invocations
如果终端的结果是模型的预测结果,那么你已经准备好开始了。如果不是,检查错误并看看可能出了什么问题。很可能你需要调试一下,解决一些小问题,才能让一切顺利运行,但这只是过程的一部分。
部署到 AWS 并进行测试
下一阶段将很大程度上取决于你所在组织的设置,而我并不是一个 devops 专家,所以我只能稍微模糊一点描述。我们的系统使用 AWS 弹性容器注册表(ECR)来存储构建好的 Docker 镜像,Lambda 从那里访问它。
当你完全满意上一步的 Docker 镜像时,你需要再构建一次,使用以下格式。第一个标志指示你为 Lambda 使用的平台。(先记住它,稍后还会再次提到。)-t 标志后的项目是你 AWS ECR 镜像的路径——填入你的正确账户编号、区域和项目名称。
docker build . --platform=linux/arm64 -t accountnumber.dkr.ecr.us-east-1.amazonaws.com/your_lambda_project:latest
之后,你应该在终端认证到一个 Amazon ECR 注册表,可能需要使用命令aws ecr get-login-password并使用适当的标志。
最后,你可以将新的 Docker 镜像推送到 ECR:
docker push accountnumber.dkr.ecr.us-east-1.amazonaws.com/your_lambda_project:latest
如果你已经正确认证,这应该只需要片刻。
还有一步,你就准备好出发了,那就是在 AWS 用户界面中设置 Lambda。登录到你的 AWS 账户,找到“Lambda”产品。

这是标题的展示效果,大致如此。
打开左侧菜单,找到“功能”。

这里是你可以找到特定项目的地方。如果你还没有设置 Lambda,点击“创建功能”并根据指示创建一个基于容器镜像的新功能。

如果你已经创建了一个功能,去找到它。从那里,你只需要点击“部署新镜像”。无论是全新的功能还是仅仅是新镜像,确保你选择与 Docker 构建时所用平台相匹配的选项!(记得那个钉子吗?)

最后一个任务,也是我一直解释到这个阶段的原因,就是在实际的 Lambda 环境中测试你的图像。这可能会暴露你在本地测试时没有遇到的错误!切换到测试标签页,通过输入一个 JSON 请求体,模拟模型在生产环境中的输入。运行测试,并确保你的模型按预期工作。

如果一切顺利,那就说明你成功了!你已经成功部署了模型。恭喜!
故障排除
然而,可能会有一些问题出现在这里。但不要慌张,如果出现错误!总是有解决办法的。
-
如果你的 Lambda 内存不足,进入配置标签页并增加内存。
-
如果图像无法显示是因为文件过大(最大为 10GB),请返回到 Docker 构建阶段并尝试减少内容的大小。如果模型可以不依赖于某些文件,就不要将极大文件打包。最坏的情况下,你可能需要将模型保存到 S3 并让函数加载它。
-
如果你在 AWS 上导航时遇到困难,你不是第一个。请咨询你的 IT 或 DevOps 团队以获得帮助。不要犯让公司损失大量资金的错误!
-
如果你遇到其他未提及的问题,请留言,我会尽力提供建议。
祝你好运,建模愉快!
即将举行的演讲:我将于美国中部时间 4 月 12 日星期五下午 1 点,通过远程方式为海外华人高等教育研究协会(OCAIR)讲解数据科学职业发展路径。如果你有兴趣收听,可以向 OCAIR 查询如何参与。
(本文中除标题图片外的所有图片均由作者创建。)
查看我的更多作品,请访问 www.stephaniekirmer.com。
使用 Power BI 进行规划(仓库)与库存值
Stock Measures 在 Power BI 中并不新鲜。但我们如何利用 Power BI 利用这些度量来规划未来呢?在这里,我将设计一个场景,并解释我如何解决它。
·发布于 Towards Data Science ·9 分钟阅读·2024 年 6 月 18 日
--

照片由 Annie Williams 提供,来源于 Unsplash
场景
让我们从这个场景开始:
我的公司,Contoso Ltd,旨在将产品库存的仓库合并为区域仓库,限制当地商店的仓库空间,从而增加销售区域。
对于规划,我需要每个产品的数量,并将其分为五个类别:
-
非常小
-
小型
-
中型
-
大型
-
未知(对于没有尺寸信息的产品)
我可以添加更多的类别,例如,使用面积并加入高度。然而,我决定使用这种方法来简化操作。
我的公司每周在数据库中存储库存数据,这是我为 Power BI 提供的数据源。
我创建规划数据的方法是计算每个月的库存量,使用特定区域内所有商店的最大库存量。
有了这个数字,我可以规划新仓库的大小,并为每个大小类别分配空间。
因此,我需要每个月和总体的库存数量。
同样,为了简化操作,我决定使用现有的各州数据,因为我没有为中央仓库创建地理区域。
实际上,我会要求业务部门基于计划商店的位置创建自定义地理区域。会有更多的细节或其他产品分组会对我有兴趣。但请容忍我简化本文中的故事。
这里是巴伐利亚(德国)两个月的按体积分组的感兴趣值:

图 1 — 巴伐利亚按体积类别分组的两个月感兴趣值(作者绘制)
如你所见,最高的库存量分布没有任何规则,并且每月可能发生变化。
我需要知道每个月和每个州每个尺寸类别的最大单位数量。
每个商店的详细信息对我的分析没有意义。
也不是每周单位的数量。
当我从上面的截图中取出巴伐利亚的示例,并查看三月时,三月的结果应该是:
-
332 个超小尺寸单元
-
168 个小尺寸单元
-
72 个中等尺寸单元
-
84 个大尺寸单元
-
204 个没有尺寸信息的单元
这些数字将作为计划的起点,应该出现在巴伐利亚的结果中。
设计解决方案
如果你是第一次接触在 Power BI 中计算库存度量,或者我们所说的“半加性度量”,请查看下方的参考部分,找到关于这个主题的另外两篇文章。
为了设计解决方案,我必须首先了解我的数据。
数据模型如下所示:

图 2 — 基本数据模型(作者绘制)
如前所述,库存数据是每周为每个商店的每个产品在库存数据事实表中收集的。
我需要哪些信息来进行分析:
-
日历
-
产品体积类别(列体积范围)
-
每个商店的州,可能还包括国家
-
库存
我需要一张包含这些列的表来计算结果。
我有两种可能的方法来解决这个具有此类表格的挑战:
-
创建一个 DAX 度量,它构建一个中介表并实时执行计算。
-
创建一个包含必要列的计算 DAX 表。
第一个方法可能是实用的;我不需要在数据模型或内存中存储额外的数据。
然而,这可能会导致性能和内存消耗的问题。
因为我必须重用表的结果,所以我可以多次复制中介表的逻辑,或者即使我创建了基本度量并重复使用它,也可能被迫多次实时计算该表。
基于这些需求和可能的副作用,我将使用 DAX 表进行我的计算。
添加此表后,我的数据模型将如下所示:

图 3 — 展开数据模型与计算表(作者绘制)
如你所见,计算表将只与日期表相关联。由于粒度与原始库存表不同,它将是自包含的。
创建解决方案
为了创建该表,我使用以下 DAX 查询:
EVALUATE
SUMMARIZECOLUMNS('Inventory'[DateKey]
,'Inventory'[DateLastDayOfMonth]
,'Geography'[State Province]
,'Product'[Volume Range]
,'Product'[Volume Range Sort]
,"Inventory", [Max Stock Volume]
)
度量值[最大库存量]从库存表中获取最大值,在此案例中,针对每个月以及每个州的所有商店。
查询的结果如下:

图 4 — 计算表的 DAX 查询结果(作者绘制的图)
如前所述,度量值[最大库存量]包含一个简单的表达式:
Max Stock Volume = MAX('Inventory'[OnHandQuantity])
我报告中用于计算库存量的现有度量值包含获取最新库存量的逻辑。这是一个使用LASTNONBLANKVALUE()函数的标准度量值。
我不需要此逻辑,因为我在计算表中不会按时间汇总库存量。
接下来,我使用 New table 函数,通过上述查询中的SUMMARIZECOLUMN()函数创建计算表:

图 5 — 使用上述查询创建计算表(作者绘制的图)
另一种方法是通过 Power Query 创建此表。这将导致数据的更高效压缩。
在我的案例中,结果表只有 57’005 行。因此,我避免执行此操作,因为这将涉及多个合并操作,而没有太大的好处。但如果表格包含数十万行甚至数百万行,我会采用这种方式。
我使用列DateLastDayOfMonth来创建与日期表的关系:

图 6 — 将新关系添加到日期表(作者绘制的图)
接下来,我计算每个州、每个库存量范围和当前月份的最高库存量。
为了实现这一点,我向计算表中添加了一个计算列。
这个计算列获取当前月份(DateLastDayOfMonth)、州和库存量范围。
然后,我获取最大库存量。
该列的 DAX 代码如下:
Highmark Stock Volume =
VAR CurrentMonth = 'Inventory Analysis table'[DateLastDayOfMonth]
VAR CurrentState = 'Inventory Analysis table'[State Province]
VAR CurrentVolumeRange = 'Inventory Analysis table'[Volume Range]
RETURN
CALCULATE(
MAX('Inventory Analysis table'[Inventory])
,REMOVEFILTERS('Inventory Analysis table')
,'Inventory Analysis table'[DateLastDayOfMonth] = CurrentMonth
,'Inventory Analysis table'[State Province] = CurrentState
,'Inventory Analysis table'[Volume Range] = CurrentVolumeRange
)
巴伐利亚的结果如下所示:

图 7 — 具有多行的计算列结果(作者绘制的图)
尽管这个结果是正确的,并且与开头显示的数据相符,但它的实际使用价值没有达到最大化。
我可以使用一个度量值,该度量值只考虑最新的值,但由于我需要在月报中展示结果,这使得这种方法变得复杂。
因此,我在此列的计算中应用了额外的逻辑,仅返回最新日期的值:
Highmark Stock Volume =
VAR CurrentMonth = 'Inventory Analysis table'[DateLastDayOfMonth]
VAR CurrentState = 'Inventory Analysis table'[State Province]
VAR CurrentVolumeRange = 'Inventory Analysis table'[Volume Range]
VAR ResultPerMonth =
CALCULATE(
MAX('Inventory Analysis table'[Inventory])
,REMOVEFILTERS('Inventory Analysis table')
,'Inventory Analysis table'[DateLastDayOfMonth] = CurrentMonth
,'Inventory Analysis table'[State Province] = CurrentState
,'Inventory Analysis table'[Volume Range] = CurrentVolumeRange
)
VAR LastDatePerMonth =
CALCULATE(
MAX('Inventory Analysis table'[DateKey])
,REMOVEFILTERS('Inventory Analysis table')
,'Inventory Analysis table'[DateLastDayOfMonth] = CurrentMonth
,'Inventory Analysis table'[State Province] = CurrentState
,'Inventory Analysis table'[Volume Range] = CurrentVolumeRange
)
RETURN
IF( 'Inventory Analysis table'[DateKey] = LastDatePerMonth, ResultPerMonth)
这是该计算的结果:

图 8 — 高标列的修正结果(作者绘制的图)
现在,我可以通过对该列进行简单的 SUM 运算来添加一个度量值,得到所需的结果:
Stock for Highmark = SUM('Inventory Analysis table'[HighMark Stock Volume])
当按月显示结果时,我得到了所需的结果:

图 9 — 2008 年第一季度各州及体积范围的结果(图由作者提供)
如你所见,结果与最开始显示的截图相同。
如果需要添加“国家”和“大陆”,我可以选择:
-
为州列添加地理表的关系。
-
将“国家”和“大陆”列添加到计算表中。
两种变体得到了相同的结果。
然而,在我当前的配置中,由于存在循环依赖,我无法添加关系,这可能是由现有的“州”列或已实现的计算列和度量值引起的。
因此,将这两列添加到计算表中更为简单:

图 10 — 新添加的“国家”和“大陆”列的计算表(图由作者提供)
我仍然可以使用带有新层次结构的结果,因为我不会对结果进行时间上的汇总。
跨多个月份的聚合是错误的,这也是为什么这样的度量值被称为“半加性度量”。
因此,我应当从矩阵视觉对象中删除“行总计”以避免显示错误信息。
接下来做什么?
现在,我可以展开表格以启用其他分析,例如按产品类别进行分析,从而增加更多分析的可能性。
但请注意。
当添加一个新列时,列[HighMark Stock Volume]将产生重复值,因为它不会考虑新列的加入。

图 11 — 包含已添加产品类别和重复值的计算表(图由作者提供)
为了纠正这一点,我必须为类别添加检查,确保值保持唯一:

图 12 — 检查添加的类别列(图由作者提供)
到某个时候,选择以下之一会更有意义:
-
更改度量值中的逻辑,以忽略重复项,或
-
为特定分析需求添加一个新的计算表。
第一种方法可能会比较复杂,而第二种方法会为每个分析扩展数据模型。
因此,两者都有利有弊。
结论
这种方法可以得到所需的结果,但它也有一些缺点:
- 计算表是自包含的,除了日期外,与其他表没有关系。
a. 这意味着我不能使用地理表中的列来基于该表和库存表过滤视觉效果。
-
这意味着数据在数据模型中被重复了。
-
添加更多分析方面可能会导致计算中的复杂效果。
-
我必须始终在任何分析中添加月份,因为没有它,结果可能会产生误导。
但这是一个相对简单的解决方案。
这种规划分析通常不会定期进行,并且在规划过程完成后可能会被丢弃。
这解决了上述提到的许多缺点。
我的一个客户需要进行这种分析以支持持续的规划,他很乐意接受这些缺点并找到解决方法。
因此,我不需要构建一个更复杂的解决方案。
这是本文的一个关键教训:寻找一个足够好的解决方案,而不是花费数天或数周时间寻找完美的解决方案。
完美的解决方案可能会过于复杂,且没有任何附加的好处。

图片由CHUTTERSNAP提供,来源于Unsplash
参考文献
这里有一些关于如何使用库存度量的过去文章:
在 Power BI 中使用库存度量时可能会出现一些奇怪的效果。我们来看看这种情况是如何出现的以及…
[towardsdatascience.com ## 在 DAX 中细化半加法度量
在之前的文章中,我解释了一些关于半加法度量(Semi-Additive Measures)的细节。这里有更多关于这个话题的提示。
[towardsdatascience.com
我之前写过一篇关于更改数据粒度的文章,这是一个类似的问题。
这是它的链接:
有时候你需要以不同于数据原有粒度的方式创建报告。让我们来探索如何…
[towardsdatascience.com
我使用的是 Contoso 示例数据集,就像我之前的文章中一样。你可以从 Microsoft 这里免费下载 ContosoRetailDW 数据集。
Contoso 数据可以在 MIT 许可证下自由使用,具体描述请见这里。
[## 每当 Salvatore Cagliari 发布新内容时,订阅电子邮件通知
每当 Salvatore Cagliari 发布新内容时,你将收到电子邮件通知。通过注册,如果你还没有 Medium 账户,将会创建一个账户…
尽管 Medium 有付费墙,我还是让我的文章对所有人可访问。这使我可以从每位读者那里赚取一些收入,但我关闭了付费墙,因此你可以免费阅读我的文章。
你可以通过以下方式支持我的工作,这些工作是在我的空闲时间里进行的。
buymeacoffee.com/salvatorecagliari
或者扫描这个二维码:

任何支持都非常感激,并帮助我争取更多时间为你创作更多内容。
非常感谢。
使用 Python 探索和理解岩石物理学中的方程
使用 Python 理解阿基水饱和度方程的参数
·发布于 Towards Data Science ·阅读时间:5 分钟·2024 年 4 月 11 日
--

使用 Python 探索岩石物理方程。图像由 DALLE-3 生成。
当你学习一个涉及众多方程和关系的技术主题时,比如岩石物理学,有时很难理解所有参数如何联系在一起。
多年前,当我刚开始学习岩石物理学时,我使用 Excel 探索一些常见的方程,比如阿基水饱和度方程。这是一种很好的方式,帮助理解每个参数对计算结果的影响。
在本文中,我们将使用 Python 探索三种不同的方法,以了解不同参数对方程的影响。所用的示例方程是阿基水饱和度(Sw)方程,该方程用于计算岩石中填充水分的孔隙体积。这可以用于推断岩石中存在的烃类数量。
如果你不熟悉岩石物理学,那么同样的工作流程和示例可以应用于任何你选择的方程。你可能需要调整最终的示例,以展示你想要看到的数据。
作为函数的简单方程
在 Go 中使用 RabbitMQ Streams
在本教程中,学习如何在 Go 中使用 RabbitMQ Streams,既可以使用 Core,也可以使用插件。
·发布在Towards Data Science ·21 分钟阅读·2024 年 1 月 15 日
--

一张由 Dall-E 创建的表示 RabbitMQ Streams 的图片
最近我发布了一篇关于 RabbitMQ 的文章,介绍如何在 Go 中使用它来构建事件驱动架构。
本文中的所有图片由 Percy Bolmér 制作。Gopher 由 Takuya Ueda 设计,原版 Go Gopher 由 Renée French 制作(CC BY 3.0)
这篇文章介绍了 RabbitMQ 的基础知识,但这次我们将扩展我们的知识,学习Streams。本文假设你已经对 RabbitMQ 队列有基本了解;如果没有,我强烈推荐阅读我之前的文章。
[## 学习 RabbitMQ 以构建事件驱动架构(EDA)
一篇面向初学者的教程,介绍 RabbitMQ 的工作原理以及如何在 Go 中使用 RabbitMQ 构建事件驱动架构
programmingpercy.tech](https://programmingpercy.tech/blog/event-driven-architecture-using-rabbitmq/?source=post_page-----1ce132d75a47--------------------------------)
我一直依赖 RabbitMQ 和 Kafka 的结合。RabbitMQ 用于大多数场景,而在需要重放、时间旅行或持久化事件时,我则使用 Kafka。
时间旅行是指你可以从系统中的某个时间点重放事件
使用两个代理变得更加复杂,因为现在我需要同时部署 Kafka 并处理两个代理。
使用自组织映射增强大型语言模型中的检索增强生成
SOM 被提议用来增强 LLM 上下文的高效检索,以支持 RAG……
·发布于Towards Data Science ·17 分钟阅读·2024 年 3 月 16 日
--

图片来源:Werclive 👹来自Unsplash
背景
大量数据被用于训练大型语言模型(LLM),这些模型包含数百万、数十亿个参数,目的是进行文本生成,如文本补全、文本摘要、语言翻译和问题回答。虽然 LLM 从训练数据中自然而然地建立了一个知识库,但训练数据有一个截止日期,截止日期之后,LLM 将无法了解任何新生成的数据。例如,OpenAI 的 GPT-3.5-turbo-instruct LLM 的训练截止日期是 2021 年 9 月(参考:platform.openai.com/docs/models/gpt-3-5-turbo),因此,GPT-3.5-turbo-instruct LLM 可能无法准确回答 2022 年、2023 年或 2024 年的事件。这类不属于 LLM 原始训练数据的数据称为外部数据。检索增强生成(RAG)是一种技术,旨在通过从授权的外部来源检索与输入提示相关的适当信息,并增强输入,以便 LLM 能够生成准确且相关的响应。实际上,RAG 构成了 LLM 与外部数据之间的桥梁。这种增强避免了重新训练或进一步微调 LLM 模型的需要。
LLM 的典型操作模式
LLM 是自回归的,根据输入提示将 token 化的序列生成一个新的 token。下一个最佳 token 的生成是基于概率的,可以表达为以下形式:
P( Yn ∣ X0, X1, ... Xn-1, θ )
本质上,新生成的第 n 个 token Yn 的概率是基于 n-1 个前序 tokens X 以及学习到的模型参数θ的概率。这里需要注意的是,token 化的输入序列 X 在生成下一个 token 时起着至关重要的作用。此外,自注意力机制有效地补充了自回归,在这种机制下,序列中的每个输入 token 通过关注并加权序列中其他 token 的重要性来计算其表示。这种 token 之间复杂的关系和依赖性使得 LLM 能够解码出与输入序列中的 tokens“契合”的下一个最佳 token。LLM 将新生成的 token 附加到之前的 tokens 上,形成新的输入序列,并重复自回归过程,直到满足结束条件,例如达到最大 token 数量。
这种自注意力驱动的自回归模型意味着,LLM 主要依赖于输入序列来生成下一个最佳的 token。只要输入序列通过自注意力帮助确定下一个最佳 token,LLM 就会继续在一个“良性”循环中生成连贯、易理解且相关的输出。相反,如果提示输入无法帮助确定下一个最佳 token,LLM 将开始依赖模型参数。在这种情况下,如果模型已经经过足够的训练,能够包含与输入提示相关的‘知识’,它可能成功生成下一个最佳 token。相反,如果提示输入涉及 LLM 从未训练过的‘外部数据’,模型可能会进入一个‘恶性’循环,生成不连贯、难以理解且可能无关的输出。
有多种技术来应对这个问题。提示工程就是其中之一,其目标是通过调整提示来解决‘缺失的上下文’,以增强上下文,使得 LLM 可以生成相关的输出。RAG 是另一种技术,其目标是通过从外部数据源自动检索与输入提示相关的最合适信息,并增强提示,专门解决由于‘外部数据’缺失的上下文。
RAG 的挑战
RAG 的主要职责是从外部数据源(如信息数据库、API 和其他文档库,如维基百科)中搜索并检索与输入提示相关的上下文数据。简单的关键词搜索是不够的,而是需要进行语义搜索。为了促进语义搜索,从外部源检索的文本信息会被转化为数值表示或向量,通常称为文本嵌入,并存储在向量数据库中。存在多种模型或算法用于从文本中创建这些嵌入。首先将提示转化为其向量表示,以便搜索和检索最接近的外部数据向量。然后计算提示向量与先前存储的外部数据向量之间的向量相似度(或向量距离)。根据相似度排序并使用阈值过滤最相似或最近的向量,最终检索相应的文本信息以增强提示的上下文。以下概念图展示了启用 RAG 时不同组件之间的典型交互:

启用 RAG 的主要系统组件交互的概念视图 — 作者提供的图片
RAG 面临的挑战是,进行基于向量的语义搜索并非简单任务,且需要大量计算资源,因为它涉及到对可能是庞大数据库中大量向量进行相似度或距离计算。对于每一个输入提示,从庞大的向量数据库中计算每个存储向量的相似度或距离将变得不可行。而且,语义匹配质量越低,LLM 的生成输出质量也越低。因此,找到一种高效进行语义搜索的方法变得至关重要。
解决方案
为了进行高效的语义搜索,采用了几种算法解决方案。这些算法的典型方法是将外部数据向量按最近邻进行分组或聚类,并通过映射到这些聚类来进行索引。大多数向量数据库提供了这种内建的索引功能。在语义搜索过程中,首先会评估输入提示向量与匹配到的聚类。对于每个评估过的聚类,选择相应的索引向量。然后计算输入提示向量与所选向量之间的相似度。此处的预期是,通过找到“最近邻”作为一个中间步骤,显著减少了相似度计算的次数。最后,依据阈值过滤,通过相似度最高或最近的向量来检索相应的文本信息。像 k-最近邻、半径球体 R、局部敏感哈希、DBSCAN 聚类、树状层次结构和图状层次结构等算法,通常由向量数据库实现,用以促进语义搜索。
没有一种适用于所有情况的解决方案,因为不同类型的算法在内存效率、计算效率、延迟、准确性、向量维度、数据集大小等方面有不同的权衡。例如,聚类方法通过缩小语义搜索的向量空间来提高速度,而类似树形或图形的方法则为低维向量数据提供更高的准确性。
自组织映射
自组织映射(SOM)是一种基于神经网络的降维算法,由 Teuvo Kohonen 在 1980 年代开发。它通常用于将高维特征向量降至低维(通常是二维)特征向量。SOM 的核心思想是将高维数据向量表示为低维空间中的特定节点,同时保留向量在原始空间中的拓扑结构。低维空间中的节点数量(SOM 节点)是固定的(超参数)。SOM 节点的精确位置通过多个训练周期来评估。迭代训练的目标是调整 SOM 节点在低维空间中的位置,使其映射到高维特征空间中最邻近的向量。换句话说,目标是将高维空间中最近邻的向量映射到低维空间中也为最近邻的 SOM 节点。
SOM 用于 RAG
在这篇文章中,我想分享我在实验中对 SOM 的笔记和发现,作为一种推动 RAG 语义搜索的可能算法。SOM 相比其他算法可能理想的三个关键原因是:
-
向量的高维度可能成为大多数其他算法的瓶颈,例如树和图——这就是所谓的维度灾难。相反,SOM 是为降维而设计的,因此它可以在高维和低维场景中有效应用。
-
SOM 对可能渗入原始高维向量空间的随机变化不太敏感,从而避免了噪音的影响。其他算法可能对这些噪音敏感,从而影响它们将高维向量聚类或分组为最近邻的方式。由于 SOM 在低维向量空间中使用中间的 SOM 节点,这些节点被评估为映射自高维空间向量的局部平均值,因此它有效地减少了噪音。
-
外部数据集的庞大规模可能会限制其他算法在创建语义向量空间时的表现,这可能会影响语义匹配的延迟和准确性。另一方面,SOM 能够处理大规模数据集,因为低维空间中的 SOM 节点数量可以通过与底层数据集大小成比例的超参数来精细调整。尽管使用大数据集训练 SOM 可能需要更长的时间,但训练完成后,查询映射仍然会更快。
我展示了一个简单的示例,使用 SOM 来进行 RAG 的语义搜索,以增强基于 OpenAI GPT-3.5-turbo-instruct LLM 的问答上下文。使用 OpenAI GPT-3.5-turbo-instruct LLM 的主要原因是因为 OpenAI GPT-3.5-turbo-instruct LLM 的训练截止日期为 2021 年 9 月(参考:platform.openai.com/docs/models/gpt-3-5-turbo),因此,GPT-3.5-turbo-instruct LLM 可能无法准确回答 2022 年、2023 年或 2024 年的事件问题。因此,关于 2022 年、2023 年或 2024 年的事件信息可能成为 OpenAI GPT-3.5-turbo-instruct LLM 的“外部数据”。我使用了 Wikipedia API 作为这种“外部数据”的来源,来获取事件信息。以下是我用来开发和训练示例的步骤,以及示例代码。
第一步:基于 PyTorch 的 Kohonen SOM 实现
我使用了 PyTorch 张量来表示向量,并使用 PyTorch 实现了 Kohonen 的 SOM。该算法使用一个二维格子,其大小成为一个超参数。算法的数学方面来源于以下文章中的清晰解释:
神经网络教程(通俗易懂)
www.ai-junkie.com](http://www.ai-junkie.com/ann/som/som1.html?source=post_page-----5d739ce21e9c--------------------------------)
以下代码片段展示了 Kohonen SOM 的 Python 类。完整代码可在这个 GitHub 链接找到。值得注意的是,这个实现是独立的,因此可以在 RAG 示例之外使用。
class KohonenSOM():
"""
The code is developed based on the following article:
http://www.ai-junkie.com/ann/som/som1.html
The vector and matrix operations are developed using PyTorch Tensors.
"""
def __init__( ... )
...
def find_topk_best_matching_units( self, data_points : torch.Tensor, topk : int = 1 ) -> List[ List[ int ] ] :
if len( data_points.size() ) == 1:
#batching
data_points = data_points.view( 1, data_points.shape[0] )
topk = int( topk )
distances = self.dist_evaluator( data_points, self.lattice_node_weights )
topk_best_matching_unit_indexes = torch.topk( distances, topk, dim=1, largest=False ).indices
topk_best_matching_units = []
for i in range( data_points.shape[0] ):
best_matching_unit_indexes = topk_best_matching_unit_indexes[i]
best_matching_units = [ self.lattice_coordinates[ bmu_index.item() ].tolist() for bmu_index in best_matching_unit_indexes ]
topk_best_matching_units.append( best_matching_units )
return topk_best_matching_units
第二步:基于 SOM 的向量索引器实现
向量索引器是一个工具,利用 Kohonen 的 SOM 来训练 SOM 节点,使用来自外部数据集的数据向量。其主要目的是将每个数据向量映射到最接近的 top-k SOM 节点,从而实现高效的数据向量索引。以下代码片段展示了向量索引器 Python 类的训练和索引功能。其完整代码可在这个 GitHub 链接找到。尽管其实现目前仅限于示例的需求,但可以扩展以满足其他需求。
class SOMBasedVectorIndexer():
...
def train_n_gen_indexes(
self, input_vectors : torch.Tensor,
train_epochs : int = 100
):
if self.generated_indexes:
print( "WARNING: Indexes were already generated. Ignoring the request..." )
return
self.som.train( input_vectors, train_epochs )
topk_bmu_indexes = self.som.find_topk_best_matching_units( input_vectors, topk = self.topk_bmu_for_indexing )
for idx in tqdm( range( len( topk_bmu_indexes ) ), desc="SOM-Based Indexed Vectors" ):
bmu_indexes = topk_bmu_indexes[ idx ]
for bmu_index in bmu_indexes:
bmu_index_key = tuple( bmu_index )
idx_set = self.som_node_idx_map.get( bmu_index_key, set() )
idx_set.add( idx )
self.som_node_idx_map[ bmu_index_key ] = idx_set
self.generated_indexes = True
第三步:基于 OpenAI 嵌入的文本到向量编码器
编码器的主要功能是使用 OpenAI 的文本嵌入 API 将文本转换为向量表示。值得注意的是,使用嵌入 API 需要一个 OpenAI 帐户和 API 密钥。在首次开通账户时,OpenAI 会提供免费的信用额度,足以用于 API 测试。以下是展示 OpenAI 编码器 Python 类的批量编码功能的代码片段,完整代码可在这个 GitHub 位置找到。
import openai
from openai.embeddings_utils import get_embedding
...
from vector_encoder_parent import VectorEncoder
...
class OpenAIEmbeddingsVectorEncoder( VectorEncoder ):
def __init__( ... )
...
def encode_batch( self, list_of_text : List[ str ] ) -> torch.Tensor :
if list_of_text == None or len( list_of_text ) == 0:
raise ValueError( "ERROR: Required list_of_text is None or empty" )
list_of_text = [ str( text ) for text in list_of_text ]
openai.api_key = self.openai_key
response = openai.Embedding.create(
input = list_of_text,
engine = self.vector_encoder_id
)
embeddings = [ data["embedding"] for data in response["data"] ]
vectors = torch.tensor( embeddings, dtype=torch.float )
return vectors
请注意,OpenAI 向量编码器类扩展了一个通用的父类‘VectorEncoder’,该父类定义了抽象的编码函数,需通过继承来实现。可以通过继承该父类实现其他类型的向量编码器,从而实现其他编码方案的插件化。父向量编码器类的完整代码可以在这个 GitHub 位置找到。
步骤 4:基于 Wikipedia API 的数据源实现
这个工具类旨在封装与 Wikipedia API 集成的数据检索逻辑。它的主要功能是获取指定日历年份范围内的事件,格式化检索到的事件,并将它们加载到 Pandas 数据框中。以下代码片段展示了该工具类的主要功能,完整代码可以在这个 GitHub 位置找到。
import requests
import pandas as pd
from dateutil.parser import parse
...
class WikiEventsDataSource():
...
def fetch_n_prepare_data( self ):
if self.fetched:
print( "WARNING: Wiki events for the specified years already fetched. Ignoring the request..." )
return
main_df = pd.DataFrame()
for year in self.event_years_to_fetch:
wiki_api_params = {
"action": "query",
"prop": "extracts",
"exlimit": 1,
"titles": year,
"explaintext": 1,
"formatversion": 2,
"format": "json"
}
response = requests.get( "https://en.wikipedia.org/w/api.php", params=wiki_api_params )
response_dict = response.json()
df = pd.DataFrame()
df[ "text" ] = response_dict["query"]["pages"][0]["extract"].split("\n")
df = self.__clean_df__( df, year )
main_df = pd.concat( [ main_df, df ] )
self.df = main_df.reset_index(drop=True)
self.fetched = True
步骤 5:基于 SOM 的 RAG 工具实现
基于 SOM 的 RAG 工具是示例实现中的一个关键元素。它利用向量编码器、索引器和数据源实现底层语义搜索的核心逻辑。基于 SOM 的 RAG 工具的完整代码可以在这个 GitHub 位置找到。
该工具实现了三个主要功能。第一个功能是从外部数据源加载数据并将其编码为向量,如下面的代码片段所示。
...
from vector_encoder_parent import VectorEncoder
from vector_indexer import SOMBasedVectorIndexer
class SOM_Based_RAG_Util():
...
def load_n_vectorize_data( self, data_source ):
if self.data_loaded_n_vectorized:
print( "WARNING: Data already loaded and vectorized. Ignoring the request..." )
return
data_source.fetch_n_prepare_data()
self.df = data_source.get_data()
vectors = None
for i in tqdm( range(0, len(self.df), self.vectorize_batch_size ), desc="Vectorized Data Batch" ):
list_of_text = self.df.iloc[ i:i+self.vectorize_batch_size ]["text"].tolist()
batch_encoded_vectors = self.vector_encoder.encode_batch( list_of_text )
if vectors == None:
vectors = batch_encoded_vectors
else:
vectors = torch.cat( [ vectors, batch_encoded_vectors], dim=0 )
self.vectors = vectors.to( self.device )
self.data_loaded_n_vectorized = True
第二个功能是训练基于 SOM 的索引器,构建 Kohonen 的 SOM 节点,然后对数据向量进行索引,如下面的代码片段所示。
def train_n_index_data_vectors( self, train_epochs : int = 100 ):
if not self.data_loaded_n_vectorized:
raise ValueError( "ERROR: Data not loaded and vectorized." )
if self.data_vectors_indexed:
print( "WARNING: Data vectors already indexed. Ignoring the request..." )
return
self.vector_indexer.train_n_gen_indexes( self.vectors, train_epochs )
self.data_vectors_indexed = True
第三个功能是基于查询文本从先前存储的外部数据集找到相似的信息。该功能使用编码器将查询文本转换为向量,然后通过基于 SOM 的索引器搜索最可能的匹配项。接着,使用余弦相似度或其他指定的相似度评估器计算查询向量与发现的数据向量之间的相似度。最后,该功能筛选出与指定相似度阈值大于或等于的相似度的数据向量。下面的代码片段展示了该功能的实现。
def find_semantically_similar_data( self, query: str, sim_evaluator = None, sim_threshold : float = 0.8 ):
if not self.data_vectors_indexed:
raise ValueError( "ERROR: Data vectors not indexed." )
if query == None or len( query.strip() ) == 0:
raise ValueError( "ERROR: Required query text is not specified." )
sim_threshold = float( sim_threshold )
if sim_evaluator == None:
sim_evaluator = nn.CosineSimilarity(dim=0, eps=1e-6)
query_vector = self.vector_encoder.encode( query )
query_vector = query_vector.view( self.vector_encoder.get_encoded_vector_dimensions() )
query_vector = query_vector.to( self.device )
nearest_indexes = self.vector_indexer.find_nearest_indexes( query_vector )
nearest_indexes = nearest_indexes[0]
sim_scores = []
for idx in nearest_indexes:
data_vector = self.vectors[ idx ]
data_vector = data_vector.view( self.vector_encoder.get_encoded_vector_dimensions() )
sim_score = sim_evaluator( query_vector, data_vector )
if sim_score >= sim_threshold:
sim_score_tuple = (idx, sim_score.item() )
sim_scores.append( sim_score_tuple )
sim_scores.sort( key = lambda x: x[1], reverse=True )
semantically_similar_data = [
{
'text': self.df[ 'text' ][ idx ],
'sim_score' : sim_score
} for idx, sim_score in sim_scores
]
return semantically_similar_data
以下是通过基于 SOM 的 RAG 实用函数进行语义搜索的示例输出:

一个示例的语义搜索输出——作者提供的图像
第 6 步:抽象问题/回答聊天机器人及其基于 OpenAI 的实现
一个抽象的‘QuestionAnswerChatBot’ Python 类被开发出来,以便于类似聊天机器人的实现。它通过使用标准的指令模板,并用从 RAG 实用工具中检索到的语境相似信息填充它,来增强问题提示。
指定的最大新标记数限制了上下文增强的文本大小,而标记计数则推迟到底层实现。在 LLM 经济学中,标记就像货币一样。模型处理的每一个标记都需要计算资源——内存、处理能力和时间。因此,LLM 需要处理的标记越多,计算成本就越高。
最后,一旦问答指令被填充,该类会将 LLM 模型的提示工作委托给底层实现。以下代码片段展示了主要功能;完整代码可以在这个 GitHub 位置找到。
from abc import ABC, abstractmethod
import torch
import math
class QuestionAnswerChatBot( ABC ):
...
def find_answer_to_question( self, question : str, sim_threshold = 0.68, max_new_tokens : int = 5 ):
if question == None or len( question.strip() ) == 0:
raise ValueError( "ERROR: Required question is not specified" )
sim_threshold = float( sim_threshold )
max_new_tokens = int( max_new_tokens )
qa_instruction = self.get_qa_instruction( question, sim_threshold = sim_threshold )
answer_text = self.__get_answer_text__( qa_instruction, max_new_tokens = max_new_tokens )
answer_text = self.__clean_answer_text__( qa_instruction, answer_text )
return answer_text
...
def __qa_template__( self ):
qa_template = """Context:
{}
---
Question: {}
Answer:"""
return qa_template
Python 类‘OpenAIQuestionAnswerChatBot’扩展了抽象类‘QuestionAnswerChatBot’,并使用 OpenAI LLM API 实现了聊天机器人功能。以下代码片段展示了该类的主要功能。完整代码可以在这个 GitHub 位置找到。
import openai
import tiktoken
from qa_chatbot import QuestionAnswerChatBot
class OpenAIQuestionAnswerChatBot( QuestionAnswerChatBot ):
...
def __get_answer_text__( self, qa_instruction : str, max_new_tokens : int = 5 ) -> str :
openai.api_key = self.openai_key
basic_answer = openai.Completion.create(
model = self.openai_model_name,
prompt = qa_instruction,
)
answer_text = basic_answer[ "choices" ][0][ "text" ]
return answer_text
def __token_count__( self, text : str ):
return len( self.tokenizer.encode( text ) )
以下是如何通过语义搜索检索到的类似信息来增强问题提示的示例:

一个示例的上下文增强问题提示——作者提供的图像
第 7 步:用于测试的示例问题
以下是用于测试 RAG 的示例问题,使用的是 OpenAI 的 GPT-3.5-turbo-instruct LLM。这些问题是为了确保它们的答案与 2022 年、2023 年和 2024 年发生的事件相关。
sample_questions = [
"Who won the 2022 soccer world cup?",
"When did Sweden join NATO?",
"Who joined NATO in 2023?",
"Who joined NATO in 2024?",
"Which is the 31st member of NATO?",
"Which is the 32nd member of NATO?",
"Who won the Cricket World Cup in 2023?",
"Who defeated India in Cricket World Cup final in 2023?",
"Name the former prime minister of Japan that was assassinated in 2022?",
"When did Chandrayaan-3 land near the south pole of the Moon?",
"Where did Chandrayaan-3 land on the Moon?",
"Who acquired Twitter in 2022?",
"Who owns Twitter?",
"Who acquired Activision Blizzard in 2023?"
]
第 8 步:将所有内容整合在一起
整合所有组件的完整 Jupyter 笔记本可以在这个 GitHub 位置找到。以下代码片段展示了基于 OpenAI 的主要问答聊天机器人的初始化过程。请注意,OpenAI 的文本嵌入算法“text-embedding-ada-002”用于向量编码。同样,聊天机器人使用 OpenAI 的分词器“cl100k_base”来计算标记数,以限制上下文文本,并通过利用 TikToken Python 库的内置函数增强问题提示。
openai_vector_encoder_id = "text-embedding-ada-002"
openai_encoded_vector_dimensions = 1536
openai_tokenizer_name = "cl100k_base"
openai_model_name = "gpt-3.5-turbo-instruct"
vector_encoder = OpenAIEmbeddingsVectorEncoder( openai_encoded_vector_dimensions, openai_vector_encoder_id, openai_key )
event_years_to_fetch = [ 2022, 2023, 2024 ]
data_source = WikiEventsDataSource( event_years_to_fetch )
...
som_driven_rag_util = SOM_Based_RAG_Util(
vector_encoder = vector_encoder,
som_lattice_height = 20,
som_lattice_width = 30,
learning_rate = 0.3,
topk_bmu_for_indexing = 10,
device = device
)
...
openai_chatbot = OpenAIQuestionAnswerChatBot(
vector_db_util = som_driven_rag_util,
openai_tokenizer_name = openai_tokenizer_name,
openai_model_name = openai_model_name,
openai_key = openai_key,
question_input_max_token_count = 100,
context_trim_percent = 0.1,
device = device
)
以下序列图有助于可视化初始化和实际问答阶段中所有组件的交互。

初始化过程中的各组件交互——作者提供的图像

问答过程中各个组件的交互 —— 图片由作者提供
研究发现
以下图像展示了在有无上下文增强的情况下,OpenAI 的 GPT-3.5-turbo-instruct LLM 的问答。

OpenAI 的 GPT-3.5-turbo-instruct LLM 的有无上下文增强回答 —— 图片由作者提供
可以理解的是,LLM 发现回答 2021 年 9 月之后发生的事件相关问题时存在困难。在大多数情况下,它明确回应说这些问题相对于其训练截止日期而言来自未来。相反,当通过从维基百科中检索到的 2022、2023 和 2024 年的相关信息增强问题上下文时,同一 LLM 能够准确无误地回答所有问题。真正的功劳在于为 RAG 的语义搜索提供基础的 SOM,它使得能够通过检索并增强问题的上下文,提供相关信息。
建议的下一步
虽然上述示例作为概念验证,评估了自组织映射(Self-Organizing Map,SOM)是否适合用于使 LLM 通过检索增强生成(RAG)文本,但建议进行更全面的基准测试,以评估其与其他算法的性能对比,使用更大规模的外部数据集,在此过程中,性能将通过 LLM 输出的质量来衡量(类似困惑度+准确度)。此外,由于当前示例启用了可插拔框架,建议使用其他开源且免费的 QA LLM 进行此类基准测试,以减少 LLM 的使用费用。
为了帮助在本地环境中运行示例,我附上了‘requirements.txt’文件,其中包含我在环境中用于运行和测试上述示例的各种版本的 Python 库。该文件可在此 GitHub 位置找到。
我最后承诺,如果我进行任何此类基准测试,将会在单独的文章中分享我的发现。请继续关注!!
参考文献
神经网络教程(简明英语版)
www.ai-junkie.com ## 通过 Python 代码理解自组织映射神经网络
通过竞争、合作和适应进行的脑启发式无监督机器学习
[towardsdatascience.com [## 面向知识密集型自然语言处理任务的检索增强生成
大型预训练语言模型已被证明能够在其参数中存储事实知识,并实现…
arxiv.org](https://arxiv.org/abs/2005.11401?source=post_page-----5d739ce21e9c--------------------------------) [## 什么是检索增强生成(RAG)?
检索增强生成(RAG)是一种增强生成型人工智能模型准确性和可靠性的技术…
blogs.nvidia.com](https://blogs.nvidia.com/blog/what-is-retrieval-augmented-generation/?source=post_page-----5d739ce21e9c--------------------------------)
www.sciencedirect.com/topics/engineering/self-organizing-map
platform.openai.com/docs/models/gpt-3-5-turbo
platform.openai.com/docs/guides/text-generation/chat-completions-api
使用 Sun RGB-D:带有 2D 和 3D 注释的室内场景数据集
访问 Sun RGB-D 及类似数据集的简单 Python 代码
·发表于 Towards Data Science ·阅读时长 9 分钟·2024 年 3 月 9 日
--

从 2D 图像进行 3D 理解是进入更广阔世界的第一步。
随着计算机视觉中的许多基础任务接近解决状态——现在已经有相对完善的、准通用的解决方案可用于图像分割和基于文本的生成,对于视觉问答、深度估计和一般物体检测的通用解答也已经取得了显著进展——我和我的许多同事一直在寻找将计算机视觉应用于更大任务的机会。当人类看一个场景时,我们看到的不仅仅是平面的轮廓。我们理解的也不仅是一个个标签。我们能够在 3D 空间中感知和想象。我们看到一个场景,并能以非常完整的方式理解它。这种能力应该是当今计算机视觉系统触手可及的…如果我们能拥有正确的数据的话。
Sun RGB-D是一个有趣的图像数据集,创建于 2015 年,满足了对全面场景理解的多种数据需求。该数据集主要收集了室内场景,通过数码相机和四种不同的 3D 扫描仪进行拍摄。相关出版物详细介绍了该数据集的收集方法及其内容。最重要的是,这个数据集包含了大量的数据,其中包括 2D 和 3D 注释。

来源:SUN RGB-D:一个 RGB-D 场景理解基准套件
使用这个数据集,计算机视觉(CV)和机器学习(ML)算法可以从 2D 图像中学习更深层次的(开个玩笑)特征。更重要的是,使用这样的数据可以为将 3D 推理应用于 2D 图像打开机会。但那是另一个话题。本文章将简单提供访问 Sun RGB-D 数据的基本 Python 代码,供读者在自己的项目中使用这一宝贵资源。
数据集布局
从这里下载数据集后,你将得到一个类似于下面的目录结构。

这些文件根据收集数据所用的扫描仪类型进行分类。具体来说,Intel RealSense 3D 摄像头用于平板电脑,Asus Xtion LIVE PRO 用于笔记本电脑,而 Microsoft Kinect 1 和 2 则用于桌面电脑。

来源:SUN RGB-D: 一个 RGB-D 场景理解基准套件
进入“kv2”目录,我们可以看到两个子目录:align_kv2 和 kinect2data。这是 Sun RGB-D 数据集的一个问题……它的目录结构对于每种传感器类型并不一致。在“realsense”中,有四个包含数据的目录:lg、sa、sh 和 shr。而在“xtion”中,目录结构则更为复杂。更糟糕的是,我在数据集的论文、补充材料或网站上找不到清晰的描述,说明这些子目录有什么不同。如果有人知道答案,请告诉我!
目前,让我们跳到数据集的一致部分:数据记录。对于 align_kv2,我们有以下内容:

对于所有传感器类型的所有数据记录,这部分在很大程度上是一致的。以下是一些需要关注的重要文件:
-
annotation2Dfinal包含最新的 2D 注释,包括多边形物体分割和物体标签。这些注释存储在一个单一的 JSON 文件中,其中包含每个分割中每个点的 x 和 y 2D 坐标,以及物体标签的列表。
-
annotation3Dfinal用于 3D 注释。这些注释以边界形状的形式存在——是沿 y 轴(上下方向)对齐的多面体。这些注释也可以在目录中的单一 JSON 文件中找到。
-
depth包含传感器收集的原始深度图像。depth_bfx包含已清理的副本,解决了传感器的一些限制。
-
原始图像可以在图像目录中找到。完整分辨率、未裁剪的版本也可以在 fullres 文件夹中找到。
-
传感器的外参和内参以类似 numpy 数组的文本文件形式保存。intrinsics.txt文件包含内参,而外参则存储在 extrinsics 文件夹中的单一文本文件内。
-
最后,场景类型(如办公室、厨房、卧室等)可以在scene.txt文件中找到,文件内容为字符串。
设置
首先,我们需要从几种格式的文件中读取数据,主要是 JSON 和 txt 格式。从这些文本文件中,我们需要提取出传感器的外参和内参的 numpy 数组。这里也有很多文件似乎没有遵循严格的命名规范,但它们在同一目录中通常是唯一的,所以get_first_file_path函数在这里会很有用。
我还希望这段代码能够输出我们在数据集中找到的房间的简单 3D 模型。这可以为我们提供简单的数据可视化,并帮助我们提取出场景的基本空间特征。为了实现这一目标,我们将使用 OBJ 文件格式,它是表示 3D 几何图形的标准格式。一个 OBJ 文件主要由顶点列表(3D 空间中的点)构成,并且包含这些顶点如何连接以形成面(3D 物体的表面)的信息。OBJ 文件的布局非常简单,首先是顶点,每个顶点以以‘v’开头的行表示,后面跟着顶点的 x、y 和 z 坐标。面通过以‘f’开头的行来定义,列出构成每个面的角落的顶点索引,从而构建 3D 表面。
在我们的上下文中,定义场景空间特征的边界形状是多面体,即具有平面面的 3D 形状。由于 y 维度是轴对齐的——意味着它在所有点中始终代表上下方向——我们可以简化多面体的表示,只使用 x 和 z 坐标来定义顶点,同时使用适用于所有点的全局最小值(min_y)和最大值(max_y)y 值。这种方法假设顶点是成对出现的,其中 x 和 z 坐标保持不变,而 y 坐标在 min_y 和 max_y 之间交替,从而有效地创建了垂直的线段。
write_obj函数封装了这一逻辑,用来构建我们的 3D 模型。它首先遍历数据集中的每个边界形状,将顶点及其 x、y 和 z 坐标添加到 OBJ 文件中。对于每一对点(偶数索引表示 min_y,奇数索引表示 max_y,其中 x 和 z 保持不变),该函数写入面定义来连接这些点,形成围绕每个片段的垂直面(例如,围绕顶点 0、1、2、3,然后是 2、3、4、5,依此类推)。如果边界形状有超过两对顶点,则添加一个闭合面,将最后一对顶点连接回第一对顶点,确保多面体正确封闭。最后,函数通过连接所有 min_y 顶点和所有 max_y 顶点,分别为多面体的顶部和底部添加面,从而完成空间特征的 3D 表示。
最后,让我们构建数据集的基本结构,创建一个表示数据集的类(一个包含子目录的目录,每个子目录包含一个数据记录),以及数据记录本身。这个第一个对象有一个非常简单的功能:它会为 ds_dir 中的每个子目录创建一个新的记录对象。
访问 2D 分割
访问 2D 分割注释相对简单。我们必须确保加载 annotation2Dfinal 中的 json 文件。一旦它作为 python 字典加载后,我们可以提取场景中每个物体的分割多边形。这些多边形通过它们的 x 和 y 坐标来定义,表示多边形在 2D 图像空间中的顶点。
我们还通过存储每个边界形状包含的物体 ID 来提取物体标签,然后与“objects”列表进行交叉引用。标签和分割都由get_segments_2d返回。
注意,转置操作应用于坐标数组,以将数据从一种将所有 x 坐标分组在一起并将所有 y 坐标分组在一起的形状,转换为一种将每对 x 和 y 坐标作为单独的点组合在一起的形状。
访问 3D 边界形状
访问 3D 边界形状稍微困难一些。如前所述,它们以 y 轴对齐的多面体形式存储(x 是左右,z 是前后,y 是上下)。在 JSON 中,这被存储为一个具有 min_y 和 max_y 的多边形。可以通过提取每个 2D 点的多边形,并添加两个带有 min_y 和 max_y 的新 3D 点,将其转换为多面体。
JSON 还提供了一个有用的字段,说明边界形状是否是矩形。我已将其保留在我们的代码中,并提供了函数来获取每个物体的类型(沙发、椅子、桌子等)以及场景中可见物体的总数。
访问房间布局
最后,房间布局有一个独立的多面体,包围了所有其他物体。算法可以使用这个多面体来理解房间的整体拓扑,包括墙壁、天花板和地板。它的访问方式与其他边界形状类似。
完整代码
以下是完整的代码以及一个简短的测试部分。除了可视化来自数据记录之一的 2D 注释外,我们还为场景中每个识别出的物体保存 3D .obj 文件。你可以使用类似meshlab这样的程序来可视化输出。传感器的内参和外参也已经在此提取。内参是指影响成像过程的相机内部参数(如焦距、光学中心和镜头畸变),而外参描述的是相机在世界坐标系中的位置和方向。它们对于从 2D 图像准确映射和解释 3D 场景非常重要。
代码也可以在这里找到:github.com/arcosin/Sun-RGDB-Data-Extractor。
这个仓库未来可能会更新,也可能不会。我很想添加一些功能,比如将其作为 PyTorch 数据集访问,并支持小批量数据等。如果有人有简单的更新,欢迎提交 PR。

左图:在 meshlab 中展示的简单 3D 场景表示。请注意透明的房间边界形状以及许多以框表示的物体。右图:原始图像。
结论
我希望这篇指南能帮助你了解如何使用 Sun RGB-D 数据集。更重要的是,我希望它让你窥见了编写快速简易代码访问数据集的更广泛技能。准备好工具很重要,但了解这些工具的工作原理并熟悉数据集的结构,在大多数情况下能给你带来更多帮助。
附加说明
本文介绍了一些易于修改的 Python 代码,用于从 Sun RGB-D 数据集中提取数据。请注意,已经存在一个官方 MATLAB 工具箱用于该数据集。但由于我不使用 MATLAB,因此没有查看过它。如果你是 MATLAB 用户(MATLABer?MATLABster?MATLABradour?哦……)那么这个工具箱可能更为全面。
我还发现了这个适用于 Python 的工具。它是一个提取仅包含 2D 特征的好例子。我借用了其中的一些代码,如果你觉得不错,给它点个星吧。
参考文献
本文使用了受CC-BY-SA许可的 Sun RGB-D 数据集[1]。该数据集还引用了之前的工作[2, 3, 4]。感谢他们为此做出的杰出贡献。
[1] S. Song, S. Lichtenberg, 和 J. Xiao, “SUN RGB-D:一个 RGB-D 场景理解基准套件”,第 28 届 IEEE 计算机视觉与模式识别会议(CVPR2015)论文集,口头报告。
[2] N. Silberman, D. Hoiem, P. Kohli, R. Fergus, “从 RGBD 图像中进行室内分割和支撑推断”,ECCV,2012 年。
[3] A. Janoch, S. Karayev, Y. Jia, J. T. Barron, M. Fritz, K. Saenko, T. Darrell, “一个类别级别的 3D 物体数据集:将 Kinect 投入实际应用”,ICCV 工作坊:计算机视觉中的消费深度摄像头,2011 年。
[4] J. Xiao, A. Owens, A. Torralba, “SUN3D:一个基于 SfM 和物体标签重建的大空间数据库”,ICCV,2013 年。
使用向量引导来改善模型指导
探索向量引导的研究并实现一个代码
·发表于Towards Data Science ·阅读时间 9 分钟·2024 年 10 月 22 日
--

图片来源:作者 — Flux.1
大型语言模型是复杂的,并且并不总是提供完美的答案。为了解决这个问题,人们尝试了许多不同的技术来引导模型的输出。我们见过在更大的数据集上进行预训练、使用更多参数的模型进行预训练,以及使用向量数据库(或其他查找方式)为 LLM 的输入添加相关的上下文。所有这些方法都能带来一定的改进,但目前没有任何一种方法是万无一失的。
引导模型的一种有趣方式是向量引导。一个有趣的例子是 Claude 金门大桥实验。在这个实验中,无论用户问什么,Claude 都会找到一种巧妙的方式,提到它最喜欢的话题:金门大桥。

图片来自“Scaling Monosemanticity: Extracting Interpretable Features from Claude 3 Sonnet” 展示了 Claude Sonnet 在使用引导向量后的行为变化
今天,我将通过对这个话题的研究进行讲解,并且解释Anastasia Borovykh 的优秀代码实现。如果你对这个话题更感兴趣,我强烈推荐查看她的视频。
让我们深入了解!
理论
使用 OpenAI API 检测短信垃圾信息
解锁高效文本分类:使用 OpenAI 的 GPT-3.5-turbo 模型案例研究
·发表于 Towards Data Science ·6 分钟阅读·2024 年 3 月 15 日
--

图片来自 unsplash.com/es/@tma
传统上,任何自然语言处理文本分类项目都会从收集实例、定义各自的标签开始,然后训练一个分类模型,例如逻辑回归模型,用于对实例进行分类。目前,OpenAI 提供的模型可以直接用于分类任务,这些任务通常需要收集大量的标注数据来训练模型。这些预训练模型可以用于多个文本处理任务,包括分类、摘要、拼写检查和关键词识别。
我们不需要任何标注数据,也不需要训练模型。简单吧?
ChatGPT 提供了一个图形化界面,用于操作 OpenAI 实现的模型。然而,如果我们想直接在 Python 中运行这些模型怎么办呢?那就有一个可行的替代方案,那就是 OpenAI API,它允许我们从编程环境中访问他们的模型。在这篇文章中,我们将通过一个简短的示例,介绍如何通过 API 检测短信是否为垃圾信息。为此,我们将利用 OpenAI 的一个模型,具体来说…
时间序列的 VAE
使用这个易于训练的模型生成逼真的序列数据
·发表于 Towards Data Science ·10 分钟阅读·2024 年 8 月 14 日
--

变分自编码器(VAEs)是一种生成性人工智能,因其能够创建逼真的图像而引起了广泛关注,但它们同样可以生成引人入胜的时间序列数据。标准的 VAE 可以调整以捕捉时间序列数据的周期性和顺序模式,然后用于生成合理的模拟数据。我所建立的模型使用1-D 卷积层、步幅的战略选择、灵活的时间维度以及季节性相关的先验来模拟温度数据。
目标
我基于来自美国亚利桑那州凤凰城的 50 年每小时 ERA5 温度数据训练了一个模型[1]。为了生成有用的数据,模型必须捕捉到原始数据的一些特征:
-
季节性概况 — 夏季应该比冬季温暖
-
昼夜变化概况 — 白天应该比夜晚温暖
-
自相关 — 数据应该平滑,连续的几天应该有相似的温度

包含修改后的哥白尼气候变化服务信息 [2024]
气候变化的影响
如果训练数据是平稳的,没有长期趋势,模型的表现最佳。然而,由于气候变化,温度每十年大约上升 0.7°F——这一数值是从观察数据中得出的,且与已发布的地图一致,显示了按区域划分的近期升温趋势[2]。为了考虑到温度的上升,我对原始观测数据应用了每十年-0.7°F 的线性变换,以消除向上的趋势。这个调整后的数据集代表了如果假设 2024 年的气候条件,历史温度可能的样貌。对生成数据的解释应考虑这一点。

包含修改后的哥白尼气候变化服务信息 [2024]
什么是 VAE?
变分自编码器将输入数据的维度减少到一个较小的子空间。VAE 定义了一个编码器,将观察到的输入转换为称为潜变量的压缩形式。然后,一个独立的、镜像的解码器尝试重建原始数据。编码器和解码器共同优化,以便生成一个尽可能少丢失信息的编码。

训练中使用的完整损失函数包括:
-
一个 重建损失:衡量往返变换的数据与原始输入的匹配程度
-
一个 正则化项:衡量潜变量的编码分布与先验分布的匹配程度。
这两个损失项是通过变分推理得出的,试图最大化观察数据的证据下界(ELBO)。查看这个视频以获取数学推导 [3]。

直观上,VAE 在训练数据上进行特征提取,使得由潜变量表示的最重要特征符合定义的先验分布。新数据是通过从潜在分布中采样并解码为原始输入的形式生成的。
查看 Joseph Rocca 的文章,理解变分自编码器,以获取关于 VAE 工作原理的更深入解释 [4]。
1-D 卷积层
在建模凤凰城温度数据时,我将我的编码器设计为一个具有一维卷积层的神经网络。每个卷积层将一个核——一个权重矩阵——应用于输入的平移区间。由于整个输入中使用的是相同的核,卷积层被认为是平移不变的,非常适合处理具有重复序列模式的时间序列数据。

左:卷积层作为矩阵操作 | 右:图形表示 | 通常,输入和输出具有多个特征变量。为了简化,矩阵操作显示了仅包含一个特征的输入和输出之间的卷积。
解码器执行与编码器相反的任务,使用转置的 1 维卷积层,也称为反卷积层。潜在特征被投射到重叠的序列中,以创建一个与输入紧密匹配的输出时间序列。

反卷积层的权重矩阵是卷积矩阵的转置。
完整的模型将多个卷积层和反卷积层堆叠在一起。每一层中间的隐藏层扩展了潜在变量的范围,使模型能够捕捉数据中的长远效应。

战略性步幅
步幅——即位移之间的跳跃——决定了下一层的大小。卷积层使用步幅来缩小输入,反卷积层使用步幅将潜在变量扩展回输入大小。然而,它们还有一个辅助功能——捕捉时间序列中的周期性趋势。
你可以战略性地选择卷积层的步幅,以复制数据中的周期性模式。
卷积周期性地应用内核,重复相同的权重,周期等于其步幅。这使得训练过程能够根据输入在周期中的位置自定义权重。
将多个层堆叠在一起,会形成一个由嵌套子卷积组成的更大有效周期。
考虑一个卷积网络,它将每小时的时间序列数据提炼成一个特征空间,每天有四个变量,分别表示早晨、下午、傍晚和夜晚。步幅为 4 的层会为一天中的每个时间点分配独特的权重,这些权重捕捉了隐藏层中的昼夜模式。在训练过程中,编码器和解码器学习到的权重能够复制数据中的每日周期。

卷积利用输入的周期性特性来构建更好的潜在特征。反卷积将潜在特征转换为重叠、重复的序列,以生成具有周期性模式的数据。
灵活的时间维度
图像生成变分自编码器(VAE)通常会预处理成千上万张图像,使其具有固定的宽度和高度。生成的图像将匹配训练数据的宽度和高度。
对于凤凰数据集,我只有一个 50 年的时间序列。为了提高训练效果,我将数据分割成多个序列,最终决定将每个 96 小时的周期分配一个潜在变量。然而,我可能希望生成比 4 天更长的时间序列,并且理想情况下,输出应当是平滑的,而不是在模拟中具有离散的 96 小时块。
幸运的是,Tensorflow 允许你在神经网络中指定不受约束的维度。就像神经网络可以处理任何批次大小一样,你也可以构建模型来处理任意数量的时间步。因此,我的潜在变量也包括一个可以变化的时间维度。在我的模型中,对于输入中的每 96 小时,就会有一个时间步在潜在空间中。

生成新数据就像从先验中采样潜在变量一样简单,你只需选择你希望在时间维度中包含的步数。
具有不受约束时间维度的 VAE 可以生成任意长度的数据。
模拟的输出将在你采样的每个时间步上有 4 天,结果会显得平滑,因为卷积层允许输入层溢出到相邻的时间段。
季节性依赖先验
在大多数 VAE 中,潜在变量的每个组件假定遵循标准正态分布。这个分布,有时称为先验,经过采样后解码,以生成新数据。在这种情况下,我选择了一个稍微复杂一点的先验,它依赖于年份的时间。
从季节性先验中采样的潜在变量将生成具有随年份时间变化特征的数据。
在这个先验下,生成的 1 月数据与 7 月数据看起来非常不同,而来自同一个月的数据会共享许多相同的特征。
我将年份的时间表示为一个角度,θ,其中 0°代表 1 月 1 日,180°代表 7 月初,360°则回到 1 月。先验是一个正态分布,其均值和对数方差是θ的三次三角多项式,其中多项式的系数是在训练过程中与编码器和解码器共同学习的参数。
先验分布参数是θ的周期函数,且良好的周期函数可以通过足够高阶的三角多项式逼近到任意精度。[5]

left: θ的可视化 | right: Z 的先验分布,表示为参数 m 和 s
季节性数据仅用于先验分布,并不影响编码器或解码器。完整的概率依赖关系在这里以图形方式展示。

包括先验的概率图模型
实现
我使用 Python 中的 Tensorflow 训练了这个模型。
from tensorflow.keras import layers, models
编码器
输入定义了一个灵活的时间维度。在 Keras 中,你可以使用None来指定一个不受约束的维度。
使用'same'填充会将零附加到输入层,以便输出大小与输入大小除以步幅后的大小相匹配。
inputs = layers.Input(shape=(None,)) # (N, 96*k)
x = layers.Reshape((-1, 1))(inputs) # (N, 96*k, 1)
# Conv1D parameters: filters, kernel_size, strides, padding
x = layers.Conv1D(40, 5, 3, 'same', activation='relu')(x) # (N, 32*k, 40)
x = layers.Conv1D(40, 3, 2, 'same', activation='relu')(x) # (N, 16*k, 40)
x = layers.Conv1D(40, 3, 2, 'same', activation='relu')(x) # (N, 8*k, 40)
x = layers.Conv1D(40, 3, 2, 'same', activation='relu')(x) # (N, 4*k, 40)
x = layers.Conv1D(40, 3, 2, 'same', activation='relu')(x) # (N, 2*k, 40)
x = layers.Conv1D(20, 3, 2, 'same')(x) # (N, k, 20)
z_mean = x[: ,:, :10] # (N, k, 10)
z_log_var = x[:, :, 10:] # (N, k, 10)
z = Sampling()([z_mean, z_log_var]) # custom layer sampling from gaussian
encoder = models.Model(inputs, [z_mean, z_log_var, z], name='encoder')
Sampling()是一个自定义层,用于从给定的均值和对数方差的正态分布中采样数据。
解码器
反卷积通过Conv1DTranspose进行。
# input shape: (batch_size, time_length/96, latent_features)
inputs = layers.Input(shape=(None, 10)) # (N, k, 10)
# Conv1DTranspose parameters: filters, kernel_size, strides, padding
x = layers.Conv1DTranspose(40, 3, 2, 'same', activation='relu')(inputs) # (N, 2*k, 40)
x = layers.Conv1DTranspose(40, 3, 2, 'same', activation='relu')(x) # (N, 4*k, 40)
x = layers.Conv1DTranspose(40, 3, 2, 'same', activation='relu')(x) # (N, 8*k, 40)
x = layers.Conv1DTranspose(40, 3, 2, 'same', activation='relu')(x) # (N, 16*k, 40)
x = layers.Conv1DTranspose(40, 3, 2, 'same', activation='relu')(x) # (N, 32*k, 40)
x = layers.Conv1DTranspose(1, 5, 3, 'same')(x) # (N, 96*k, 1)
outputs = layers.Reshape((-1,))(x) # (N, 96*k)
decoder = models.Model(inputs, outputs, name='decoder')
先验
先验期望输入已经是[sin(θ), cos(θ), sin(2θ), cos(2θ), sin(3θ), cos(3θ)]的形式。
Dense 层没有偏置项,这是为了防止先验分布偏离零过远,或其整体方差过高或过小。
# seasonal inputs shape: (N, k, 6)
inputs = layers.Input(shape=(None, 2*3))
x = layers.Dense(20, use_bias=False)(inputs) # (N, k, 20)
z_mean = x[:, :, :10] # (N, k, 10)
z_log_var = x[:, :, 10:] # (N, k, 10)
z = Sampling()([z_mean, z_log_var]) # (N, k, 10)
prior = models.Model(inputs, [z_mean, z_log_var, z], name='seasonal_prior')
完整模型
损失函数包含重建项和潜在正则化项。
函数log_lik_normal_sum是一个自定义函数,用于计算在给定重建输出的情况下,观察数据的正态对数似然值。计算对数似然需要围绕解码输出的噪声分布,假设其为正态分布,且对数方差由self.noise_log_var表示,在训练过程中学习得到。
对于正则化项,kl_divergence_sum计算两个高斯分布之间的 Kullback–Leibler 散度——在这种情况下,是潜在编码分布与先验分布之间的散度。
class VAE(models.Model):
def __init__(self, encoder, decoder, prior, **kwargs):
super(VAE, self).__init__(**kwargs)
self.encoder = encoder
self.decoder = decoder
self.prior = prior
self.noise_log_var = self.add_weight(name='var', shape=(1,), initializer='zeros', trainable=True)
@tf.function
def vae_loss(self, data):
values, seasonal = data
z_mean, z_log_var, z = self.encoder(values)
reconstructed = self.decoder(z)
reconstruction_loss = -log_lik_normal_sum(values, reconstructed, self.noise_log_var)/INPUT_SIZE
seasonal_z_mean, seasonal_z_log_var, _ = self.prior(seasonal)
kl_loss_z = kl_divergence_sum(z_mean, z_log_var, seasonal_z_mean, seasonal_z_log_var)/INPUT_SIZE
return reconstruction_loss, kl_loss_z
def train_step(self, data):
with tf.GradientTape() as tape:
reconstruction_loss, kl_loss_z = self.vae_loss(data)
total_loss = reconstruction_loss + kl_loss_z
gradients = tape.gradient(total_loss, self.trainable_variables)
self.optimizer.apply_gradients(zip(gradients, self.trainable_variables))
return {'loss': total_loss}
要查看完整实现,请访问我的Github 仓库。
结果
在训练模型后,生成的数据与原始温度数据的季节性/日变化轮廓以及自相关性匹配。

包含修改后的哥白尼气候变化服务信息[2024]
结论
构建生成时间序列建模技术是一个关键领域,其应用不仅仅限于数据模拟。我分享的方法可以适应于数据插补、异常检测和预测等应用。
通过使用 1 维卷积层、战略性步幅、灵活的时间输入和季节性先验,你可以构建一个 VAE,复制时间序列中的复杂模式。让我们共同努力,优化时间序列建模的最佳实践。
在评论中分享你在使用 VAE 和/或生成 AI 进行时间序列建模方面的经验、问题或见解。
所有图像均由作者创作,除非另有说明。
[1] Hersbach, H., Bell, B., Berrisford, P., Biavati, G., Horányi, A., Muñoz Sabater, J., Nicolas, J., Peubey, C., Radu, R., Rozum, I., Schepers, D., Simmons, A., Soci, C., Dee, D., Thépaut, J-N. (2023): ERA5 每小时数据(单一层级,从 1940 年至今)。哥白尼气候变化服务(C3S)气候数据存储(CDS),DOI:10.24381/cds.adbb2d47(访问日期:2024 年 8 月 1 日)
[2] Lindsey, R., & Dahlman, L. (2024 年 1 月 18 日). 气候变化:全球气温. Climate.gov. www.climate.gov/news-features/understanding-climate/climate-change-global-temperature
[3] Sachdeva, K. (2021 年 1 月 26 日). 证据下界(ELBO)——清晰解释! [视频]. YouTube. www.youtube.com/watch?v=IXsA5Rpp25w
[4] Rocca, J. (2019 年 9 月 23 日). 理解变分自编码器 (VAEs). Towards Data Science. towardsdatascience.com/understanding-variational-autoencoders-vaes-f70510919f73
[5] Baidoo, F. A. (2015 年 8 月 28 日). 傅里叶级数的统一收敛性 (REU 报告). 芝加哥大学. math.uchicago.edu/~may/REU2015/REUPapers/Baidoo.pdf
在生产管道中验证数据:TFX 方式
深入了解使用 TensorFlow 数据验证进行的数据验证
·发表于 Towards Data Science ·阅读时间 9 分钟·2024 年 6 月 22 日
--
想象一下。我们有一个完全功能的机器学习管道,并且它是完美的。所以我们决定将它推向生产环境。一切在生产环境中都很顺利,直到有一天,我们的管道输入数据生成组件发生了一个小小的变化,导致管道崩溃。糟糕!

图片来源:Sarah Kilian 在 Unsplash
为什么会发生这种情况?
由于机器学习模型在很大程度上依赖于所使用的数据,请记住那句古老的格言,“垃圾进,垃圾出”。只要数据正确,管道就能正常工作,一旦数据有所变动,管道可能就会出问题。
输入到管道中的数据大多数是通过自动化系统生成的,因此在生成数据的类型上控制较少。
那么,我们该怎么做呢?
数据验证是解决方案。
数据验证是一个监护系统,能够验证数据是否以合适的格式供管道使用。
阅读本文,了解为什么在机器学习管道中验证至关重要,以及机器学习验证的 5 个阶段。
确保在整个机器学习生命周期中都能提供高质量的机器学习
towardsdatascience.com
TensorFlow 数据验证
TensorFlow 数据验证(TFDV)是 TFX 生态系统的一部分,可以用于在机器学习管道中验证数据。
TFDV 通过比较训练数据和服务数据来计算描述性统计、模式并识别异常。这确保了训练数据和服务数据的一致性,避免在管道中出现破坏性问题或产生意外预测。
谷歌的团队希望 TFDV 从机器学习流程的最早阶段就能被使用。因此,他们确保 TFDV 可以与笔记本一起使用。我们将在这里做同样的事。
首先,我们需要使用 pip 安装 tensorflow-data-validation 库。最好创建一个虚拟环境并开始安装。
注意事项:在安装之前,确保 TFX 库的版本兼容性
pip install tensorflow-data-validation
以下是我们将在数据验证过程中遵循的步骤:
-
从训练数据生成统计信息
-
从训练数据推断模式
-
生成评估数据的统计信息并与训练数据进行比较
-
识别和修复异常
-
检查漂移和数据偏斜
-
保存模式
我们将在这里使用 3 种类型的数据集;训练数据、评估数据和服务数据,模拟实时使用。机器学习模型使用训练数据进行训练。评估数据,也就是测试数据,是在训练阶段完成后,用来测试模型的一部分数据。服务数据则在生产环境中提供给模型进行预测。
本文讨论的完整代码可以在我的 GitHub 仓库中找到。你可以通过 这里 下载。
步骤 0:准备工作
我们将使用来自 Kaggle 的 spaceship titanic 数据集。你可以通过这个 link 了解更多并下载数据集。

Spaceship Titanic 数据集的示例视图
数据由数值数据和类别数据的混合组成。它是一个分类数据集,类别标签是 Transported,其值为 True 或 False。

数据描述
必要的导入已完成,csv 文件的路径已定义。实际数据集包含训练数据和测试数据。我手动引入了一些错误,并将文件保存为‘titanic_test_anomalies.csv’(此文件在 Kaggle 上不可用。你可以从我的 GitHub 仓库 link 下载)。
在这里,我们将使用 ANOMALOUS_DATA 作为评估数据,使用 TEST_DATA 作为服务数据。
import tensorflow_data_validation as tfdv
import tensorflow as tf
TRAIN_DATA = '/data/titanic_train.csv'
TEST_DATA = '/data/titanic_test.csv'
ANOMALOUS_DATA = '/data/titanic_test_anomalies.csv'
步骤 1:从训练数据生成统计信息
第一步是分析训练数据并识别其统计属性。TFDV 具有 generate_statistics_from_csv 函数,可以直接从 csv 文件读取数据。如果你有 TFRecord 格式的数据,TFDV 还提供了 generate_statistics_from_tfrecord 函数。
visualize_statistics函数展示了一个 8 点总结,以及一些有助于我们理解数据底层统计的图表。这被称为“Facets 视图”。一些需要我们注意的关键细节以红色标出。这里还提供了许多其他用于分析数据的功能。可以多加尝试,进一步了解它。
# Generate statistics for training data
train_stats=tfdv.generate_statistics_from_csv(TRAIN_DATA)
tfdv.visualize_statistics(train_stats)

为数据集生成的统计信息
在这里,我们看到Age和RoomService特征中有缺失值,需要进行填充。我们还看到RoomService中有 65.52%的值为零。这是该数据的分布方式,因此我们不认为这是异常,我们将继续处理。
第 2 步:从训练数据中推断模式
一旦所有问题都得到满意解决,我们将使用infer_schema函数推断模式。
schema=tfdv.infer_schema(statistics=train_stats)
tfdv.display_schema(schema=schema)
模式通常分为两个部分。第一部分呈现数据类型、存在性、有效性及其领域等详细信息。第二部分呈现该领域所包含的值。

第一部分:特征的详细信息

第二部分:领域值
这是初始的原始模式,我们将在后续步骤中对其进行完善。
第 3 步:生成评估数据的统计信息并与训练数据进行比较
现在我们选择评估数据并生成统计信息。我们需要了解如何处理异常,因此我们将使用ANOMALOUS_DATA作为我们的评估数据。我们已经手动将异常引入到此数据中。
生成统计信息后,我们对数据进行可视化。可视化可以仅应用于评估数据(就像我们对训练数据做的那样),但将评估数据的统计信息与训练数据的统计信息进行比较更有意义。这样我们可以了解评估数据与训练数据之间的差异。
# Generate statistics for evaluation data
eval_stats=tfdv.generate_statistics_from_csv(ANOMALOUS_DATA)
tfdv.visualize_statistics(lhs_statistics = train_stats, rhs_statistics = eval_stats,
lhs_name = "Training Data", rhs_name = "Evaluation Data")

训练数据和评估数据统计信息的比较
在这里,我们可以看到评估数据中缺少RoomService特征(大红旗)。其他特征看起来相当正常,因为它们展示的分布与训练数据相似。
然而,单靠人工观察在生产环境中是不够的,所以我们将要求 TFDV 实际分析并报告是否一切正常。
第 4 步:识别并修复异常
我们的下一步是验证从评估数据中获得的统计信息。我们将其与我们用训练数据生成的模式进行比较。display_anomalies函数将为我们提供 TFDV 已识别的异常的表格视图,并附有说明。
# Identifying Anomalies
anomalies=tfdv.validate_statistics(statistics=eval_stats, schema=schema)
tfdv.display_anomalies(anomalies)

TFDV 提供的异常列表
从表格中,我们可以看到评估数据缺少 2 列(Transported 和 RoomService),Destination 特征的领域中有一个额外的值“Anomaly”(在训练数据中没有出现),CryoSleep 和 VIP 特征有“TRUE”和“FALSE”值,这些值在训练数据中没有,最后,5 个特征包含整数值,而模式期望的是浮动点值。
这可真是个挑战。让我们开始工作吧。
修复异常有两种方法;要么手动处理评估数据,以确保它符合模式,要么修改模式以确保这些异常被接受。再次,领域专家必须决定哪些异常是可以接受的,哪些异常需要数据处理。
让我们从“Destination”特征开始。我们发现了一个新值“Anomaly”,它在训练数据的领域列表中缺失。让我们将其添加到领域中,并声明它也是该特征的一个可接受值。
# Adding a new value for 'Destination'
destination_domain=tfdv.get_domain(schema, 'Destination')
destination_domain.value.append('Anomaly')
anomalies=tfdv.validate_statistics(statistics=eval_stats, schema=schema)
tfdv.display_anomalies(anomalies)
我们已经解决了这个异常,异常列表中不再显示它。让我们进入下一个。

Destination 异常已解决
查看 VIP 和 CryoSleep 领域,我们发现训练数据使用的是小写值,而评估数据则使用相同的值但为大写。一个解决方案是预处理数据,并确保所有数据都转换为小写或大写。然而,我们决定将这些值添加到领域中。由于 VIP 和 CryoSleep 使用相同的值集(true 和 false),我们将 CryoSleep 的领域设置为使用 VIP 的领域。
# Adding data in CAPS to domain for VIP and CryoSleep
vip_domain=tfdv.get_domain(schema, 'VIP')
vip_domain.value.extend(['TRUE','FALSE'])
# Setting domain of one feature to another
tfdv.set_domain(schema, 'CryoSleep', vip_domain)
anomalies=tfdv.validate_statistics(statistics=eval_stats, schema=schema)
tfdv.display_anomalies(anomalies)

已解决来自 CryoSleep 和 VIP 的异常
将整数特征转换为浮动点特征是相当安全的。因此,我们要求评估数据根据训练数据的模式推断数据类型。这解决了与数据类型相关的问题。
# INT can be safely converted to FLOAT. So we can safely ignore it and ask TFDV to use schema
options = tfdv.StatsOptions(schema=schema, infer_type_from_schema=True)
eval_stats=tfdv.generate_statistics_from_csv(ANOMALOUS_DATA, stats_options=options)
anomalies=tfdv.validate_statistics(statistics=eval_stats, schema=schema)
tfdv.display_anomalies(anomalies)

已解决数据类型问题
最后,我们遇到了最后一组异常:训练数据中存在的 2 列在评估数据中缺失。
“Transported”是类别标签,显然在评估数据中无法使用。为了处理训练数据和评估数据特征可能不同的情况,我们可以创建多个环境。在这里,我们创建了一个训练环境和一个服务环境。我们指定“Transported”特征将在训练环境中可用,但在服务环境中不可用。
# Transported is the class label and will not be available in Evaluation data.
# To indicate that we set two environments; Training and Serving
schema.default_environment.append('Training')
schema.default_environment.append('Serving')
tfdv.get_feature(schema, 'Transported').not_in_environment.append('Serving')
serving_anomalies_with_environment=tfdv.validate_statistics(
statistics=eval_stats, schema=schema, environment='Serving')
tfdv.display_anomalies(serving_anomalies_with_environment)
“RoomService”是一个必需的特征,但在服务环境中不可用。此类情况需要领域专家的手动干预。
继续解决问题,直到你得到这个输出。

所有异常已解决
所有异常已经解决
第 5 步:训练-服务漂移和偏差检测
下一步是检查漂移和偏差。偏差是由于数据分布的不规则性而发生的。最初,当模型训练完成时,它的预测通常是完美的。然而,随着时间的推移,数据分布发生变化,误分类错误开始增加,这就是漂移。这些问题需要重新训练模型。
L-infinity 距离用于衡量偏差和漂移。基于 L-infinity 距离设置一个阈值。如果在训练和服务环境中分析的特征之间的差异超过给定的阈值,则认为该特征经历了漂移。偏差的判断也遵循类似的阈值方法。对于我们的示例,我们已将漂移和偏差的阈值都设置为 0.01。
serving_stats = tfdv.generate_statistics_from_csv(TEST_DATA)
# Skew Comparator
spa_analyze=tfdv.get_feature(schema, 'Spa')
spa_analyze.skew_comparator.infinity_norm.threshold=0.01
# Drift Comparator
CryoSleep_analyze=tfdv.get_feature(schema, 'CryoSleep')
CryoSleep_analyze.drift_comparator.infinity_norm.threshold=0.01
skew_anomalies=tfdv.validate_statistics(statistics=train_stats, schema=schema,
previous_statistics=eval_stats,
serving_statistics=serving_stats)
tfdv.display_anomalies(skew_anomalies)
我们可以看到‘Spa’的偏差水平是可以接受的(因为它没有列在异常列表中),然而,‘CryoSleep’表现出较高的漂移水平。在创建自动化管道时,这些异常可以用作触发自动模型重新训练的条件。

CryoSleep 中的高偏差
步骤 6:保存架构
在解决所有异常后,架构可以作为工件保存,或者可以保存在元数据存储库中,并在机器学习管道中使用。
# Saving the Schema
from tensorflow.python.lib.io import file_io
from google.protobuf import text_format
file_io.recursive_create_dir('schema')
schema_file = os.path.join('schema', 'schema.pbtxt')
tfdv.write_schema_text(schema, schema_file)
# Loading the Schema
loaded_schema= tfdv.load_schema_text(schema_file)
loaded_schema
你可以通过以下链接从我的 GitHub 仓库下载笔记本和数据文件。
其他值得关注的选项
你可以阅读以下文章,了解你的选择,并学习如何为你的机器学习管道项目选择合适的框架。
[## TFX SDK 和 Kubeflow Pipelines SDK 之间的优势和差异是什么?在创建自己的管道时,应该如何选择它们?
TFX SDK(TensorFlow Extended 软件开发工具包)和 Kubeflow Pipelines SDK 是两个强大的工具,可以...
eitca.org](https://eitca.org/artificial-intelligence/eitc-ai-gcml-google-cloud-machine-learning/google-cloud-ai-platform/setting-up-ai-platform-pipelines/examination-review-setting-up-ai-platform-pipelines/what-are-the-advantages-and-differences-between-tfx-sdk-and-kubeflow-pipelines-sdk-and-how-should-you-choose-between-them-when-creating-your-own-pipeline/?source=post_page-----9770311eb7ce--------------------------------) [## MLflow 与 TensorFlow Extended 的对比
探索 MLflow 与 TensorFlow Extended 在管理机器学习工作流方面的差异。
感谢阅读我的文章。如果你喜欢它,请给我一点掌声作为鼓励;如果你有不同的意见,请在评论中告诉我有哪些可以改进的地方。再见。
除非另有说明,所有图片均由作者提供。
验证合成控制方法的因果影响
因果 AI,探索因果推理与机器学习的结合
·发表于Towards Data Science ·10 分钟阅读·2024 年 6 月 8 日
--

图片由Irina Inga提供,来自Unsplash
这系列文章是关于什么的?
欢迎来到我的因果 AI 系列,在这里我们将探讨因果推理如何与机器学习模型集成。预计会探索多个在不同商业环境中的实际应用。
在上一篇文章中,我们讨论了测量您的营销活动的内在因果影响。在本文中,我们将继续探讨验证合成控制的因果影响。
如果你错过了上一篇关于内在因果影响的文章,可以在这里查看:
因果 AI,探索因果推理与机器学习的结合
towardsdatascience.com
引言
在本文中,我们将重点了解合成控制方法,并探索我们如何验证估计的因果影响。
以下方面将被涵盖:
-
什么是合成控制方法?
-
它试图克服什么挑战?
-
我们如何验证估计的因果影响?
-
一个使用真实的 Google 趋势数据的 Python 案例研究,演示了我们如何验证合成控制的估计因果影响。
完整的笔记本可以在这里找到:
[## causal_ai/notebooks/synthetic controls - model training.ipynb 在 main 分支 · raz1470/causal_ai
本项目介绍了因果 AI 及其如何驱动商业价值。 - causal_ai/notebooks/synthetic controls - model…
合成控制方法
它是什么?
合成控制方法是一种因果技术,可以在无法进行随机对照试验(RCT)或 A/B 测试时,评估干预或治疗的因果影响。该方法最早由 Abadie 和 Gardezabal 于 2003 年提出。以下论文包含了一个很好的案例研究,帮助你理解该方法:
web.stanford.edu/~jhain/Paper/JASA2010.pdf

用户生成的图像
让我们自己来了解一些基础知识… 合成控制方法通过创建未接受干预或治疗的控制单位的加权组合,来创建处理单位的反事实版本。
-
处理单位: 接受干预的单位。
-
控制单位: 一组没有接受干预的相似单位。
-
反事实: 通过控制单位的加权组合创建。目标是为每个控制单位找到权重,使得反事实在干预前期与处理单位非常接近。
-
因果影响: 后期干预处理单位与反事实之间的差异。
如果我们想要简化问题,可以将其视为线性回归,其中每个控制单位是一个特征,处理单位是目标。干预前期是我们的训练集,我们使用模型来评分干预后期。实际与预测之间的差异就是因果影响。
以下是几个例子,帮助你理解何时可以考虑使用此方法:
-
在进行电视营销活动时,我们无法随机将观众分配到可以和不能看到活动的组别。然而,我们可以谨慎地选择一个地区进行试点,并使用其余地区作为控制单位。一旦我们测量了效果,活动可以推广到其他地区。这通常被称为地理提升测试(geo-lift test)。
-
政策变更仅在某些地区实施,而在其他地区不实施——例如,当地政府可能会实施一项政策以减少失业。在没有实施该政策的其他地区,可以用作控制单元。
它试图克服什么挑战?
当我们将高维度(大量特征)与有限的观测数据结合时,我们可能会得到一个过拟合的模型。
让我们以地理提升的例子来说明。如果我们使用过去一年的周数据作为干预前期,这将给我们提供 52 个观测值。如果我们决定在欧洲的多个国家进行干预测试,那么观测与特征的比例将是 1:1!
之前我们谈到了如何使用线性回归来实现合成控制方法。然而,由于观测与特征的比例,线性回归很可能会过拟合,从而导致干预后期的因果影响估算不准确。
在线性回归中,每个特征(控制单元)的权重(系数)可能为负或正,并且它们的总和可能大于 1。然而,合成控制方法在学习权重时会施加以下约束:
-
限制权重总和为 1
-
限制权重 ≥ 0

用户生成的图像
这些约束有助于正则化,避免超出观测数据范围的外推。
值得注意的是,在正则化方面,岭回归和套索回归可以实现这一点,并且在某些情况下是合理的替代方案。但我们将在案例研究中进行测试!
我们如何验证估算的因果影响?
一个更大的挑战是我们无法验证干预后期的估算因果影响。
我的干预前期应持续多久?我们能确定没有对干预前期进行过拟合吗?我们如何知道我们的模型在干预后期是否能够很好地泛化?如果我想尝试合成控制方法的不同实现方式怎么办?
我们可以从干预前期随机选择一些观测值并将其保留用于验证——但我们已经指出,有限的观测值会带来挑战,这可能会使问题变得更糟!
如果我们可以进行某种干预前的模拟呢?这能帮助我们回答上述一些问题,并增加我们对模型估算因果影响的信心吗?所有这些将在案例研究中进行解释!
案例研究
背景
在说服财务部门品牌营销能够带来实质性价值后,市场营销团队找到你询问关于地理提升测试的问题。有人从 Facebook 告诉他们这是下一个大趋势(尽管也是同一个人告诉他们 Prophet 是一个很好的预测模型),他们想知道是否可以用它来衡量即将上线的新电视广告活动。
你有些担心,因为上次你进行 geo-lift 测试时,市场分析团队认为调整干预前期直到得到一个很大的因果影响是个好主意。
这次,你建议他们在测试开始前进行一次“干预前模拟”,之后提出在测试开始前先确定干预前期。
那么让我们来看看“干预前模拟”是什么样子的!
创建数据
为了使这一过程尽可能逼真,我提取了大部分欧洲国家的谷歌趋势数据。搜索的具体词语并不重要,只需要假装它是你公司(并且你在欧洲运营)的销售数据。
然而,如果你对我是如何获得谷歌趋势数据感兴趣,可以查看我的笔记本:
[## causal_ai/notebooks/synthetic controls - 数据收集.ipynb 主页 · raz1470/causal_ai
这个项目介绍了因果 AI 以及它如何推动商业价值。 - causal_ai/notebooks/synthetic controls - 数据…
下面我们可以看到数据框架。我们有过去 3 年中 50 个欧洲国家的销售数据。市场团队计划在英国开展他们的电视广告活动。

用户生成的图像
现在进入精彩的部分。我们将在时间序列的最后 7 周模拟一次干预。
np.random.seed(1234)
# Create intervention flag
mask = (df['date'] >= "2024-04-14") & (df['date'] <= "2024-06-02")
df['intervention'] = mask.astype(int)
row_count = len(df)
# Create intervention uplift
df['uplift_perc'] = np.random.uniform(0.10, 0.20, size=row_count)
df['uplift_abs'] = round(df['uplift_perc'] * df['GB'])
df['y'] = df['GB']
df.loc[df['intervention'] == 1, 'y'] = df['GB'] + df['uplift_abs']
现在让我们绘制英国实际与反事实的销售数据,让我们所做的工作更生动:
def synth_plot(df, counterfactual):
plt.figure(figsize=(14, 8))
sns.set_style("white")
# Create plot
sns.lineplot(data=df, x='date', y='y', label='Actual', color='b', linewidth=2.5)
sns.lineplot(data=df, x='date', y=counterfactual, label='Counterfactual', color='r', linestyle='--', linewidth=2.5)
plt.title('Synthetic Control Method: Actual vs. Counterfactual', fontsize=24)
plt.xlabel('Date', fontsize=20)
plt.ylabel('Metric Value', fontsize=20)
plt.legend(fontsize=16)
plt.gca().xaxis.set_major_formatter(plt.matplotlib.dates.DateFormatter('%Y-%m-%d'))
plt.xticks(rotation=90)
plt.grid(True, linestyle='--', alpha=0.5)
# High the intervention point
intervention_date = '2024-04-07'
plt.axvline(pd.to_datetime(intervention_date), color='k', linestyle='--', linewidth=1)
plt.text(pd.to_datetime(intervention_date), plt.ylim()[1]*0.95, 'Intervention', color='k', fontsize=18, ha='right')
plt.tight_layout()
plt.show()
synth_plot(df, 'GB')

用户生成的图像
现在我们已经模拟了一个干预,我们可以探索合成控制方法的效果如何。
预处理
除了英国(GB)外,所有欧洲国家都被设置为对照单元(特征)。治疗单元(目标)是应用干预的英国销售数据。
# Delete the original target column so we don't use it as a feature by accident
del df['GB']
# set feature & targets
X = df.columns[1:50]
y = 'y'
回归
下面我设置了一个函数,我们可以用不同的干预前期和不同的回归模型(例如 Ridge、Lasso)进行重复使用:
def train_reg(df, start_index, reg_class):
df_temp = df.iloc[start_index:].copy().reset_index()
X_pre = df_temp[df_temp['intervention'] == 0][X]
y_pre = df_temp[df_temp['intervention'] == 0][y]
X_train, X_test, y_train, y_test = train_test_split(X_pre, y_pre, test_size=0.10, random_state=42)
model = reg_class
model.fit(X_train, y_train)
yhat_train = model.predict(X_train)
yhat_test = model.predict(X_test)
mse_train = mean_squared_error(y_train, yhat_train)
mse_test = mean_squared_error(y_test, yhat_test)
print(f"Mean Squared Error train: {round(mse_train, 2)}")
print(f"Mean Squared Error test: {round(mse_test, 2)}")
r2_train = r2_score(y_train, yhat_train)
r2_test = r2_score(y_test, yhat_test)
print(f"R2 train: {round(r2_train, 2)}")
print(f"R2 test: {round(r2_test, 2)}")
df_temp['pred'] = model.predict(df_temp.loc[:, X])
df_temp['delta'] = df_temp['y'] - df_temp['pred']
pred_lift = df_temp[df_temp['intervention'] == 1]['delta'].sum()
actual_lift = df_temp[df_temp['intervention'] == 1]['uplift_abs'].sum()
abs_error_perc = abs(pred_lift - actual_lift) / actual_lift
print(f"Predicted lift: {round(pred_lift, 2)}")
print(f"Actual lift: {round(actual_lift, 2)}")
print(f"Absolute error percentage: {round(abs_error_perc, 2)}")
return df_temp, abs_error_perc
为了开始,我们保持简单,使用线性回归来估算因果影响,使用一个较小的干预前期:
df_lin_reg_100, pred_lift_lin_reg_100 = train_reg(df, 100, LinearRegression())

用户生成的图像
从结果来看,线性回归的表现并不理想。但鉴于特征与观测值的比例,这并不令人惊讶。
synth_plot(df_lin_reg_100, 'pred')

用户生成的图像
合成控制方法
让我们直接进入,看看它与合成控制方法的比较。下面我设置了一个类似之前的函数,但使用 sciPy 应用了合成控制方法:
def synthetic_control(weights, control_units, treated_unit):
synthetic = np.dot(control_units.values, weights)
return np.sqrt(np.sum((treated_unit - synthetic)**2))
def train_synth(df, start_index):
df_temp = df.iloc[start_index:].copy().reset_index()
X_pre = df_temp[df_temp['intervention'] == 0][X]
y_pre = df_temp[df_temp['intervention'] == 0][y]
X_train, X_test, y_train, y_test = train_test_split(X_pre, y_pre, test_size=0.10, random_state=42)
initial_weights = np.ones(len(X)) / len(X)
constraints = ({'type': 'eq', 'fun': lambda w: np.sum(w) - 1})
bounds = [(0, 1) for _ in range(len(X))]
result = minimize(synthetic_control,
initial_weights,
args=(X_train, y_train),
method='SLSQP',
bounds=bounds,
constraints=constraints,
options={'disp': False, 'maxiter': 1000, 'ftol': 1e-9},
)
optimal_weights = result.x
yhat_train = np.dot(X_train.values, optimal_weights)
yhat_test = np.dot(X_test.values, optimal_weights)
mse_train = mean_squared_error(y_train, yhat_train)
mse_test = mean_squared_error(y_test, yhat_test)
print(f"Mean Squared Error train: {round(mse_train, 2)}")
print(f"Mean Squared Error test: {round(mse_test, 2)}")
r2_train = r2_score(y_train, yhat_train)
r2_test = r2_score(y_test, yhat_test)
print(f"R2 train: {round(r2_train, 2)}")
print(f"R2 test: {round(r2_test, 2)}")
df_temp['pred'] = np.dot(df_temp.loc[:, X].values, optimal_weights)
df_temp['delta'] = df_temp['y'] - df_temp['pred']
pred_lift = df_temp[df_temp['intervention'] == 1]['delta'].sum()
actual_lift = df_temp[df_temp['intervention'] == 1]['uplift_abs'].sum()
abs_error_perc = abs(pred_lift - actual_lift) / actual_lift
print(f"Predicted lift: {round(pred_lift, 2)}")
print(f"Actual lift: {round(actual_lift, 2)}")
print(f"Absolute error percentage: {round(abs_error_perc, 2)}")
return df_temp, abs_error_perc
我保持干预前的时间段不变,以便与线性回归进行公平比较:
df_synth_100, pred_lift_synth_100 = train_synth(df, 100)

用户生成图像
哇!我必须承认,我没想到会有这么显著的改进!
synth_plot(df_synth_100, 'pred')

用户生成图像
结果比较
让我们还不要过于激动。接下来,我们将进行更多实验,探索模型类型和干预前的时间段:
# run regression experiments
df_lin_reg_00, pred_lift_lin_reg_00 = train_reg(df, 0, LinearRegression())
df_lin_reg_100, pred_lift_lin_reg_100 = train_reg(df, 100, LinearRegression())
df_ridge_00, pred_lift_ridge_00 = train_reg(df, 0, RidgeCV())
df_ridge_100, pred_lift_ridge_100 = train_reg(df, 100, RidgeCV())
df_lasso_00, pred_lift_lasso_00 = train_reg(df, 0, LassoCV())
df_lasso_100, pred_lift_lasso_100 = train_reg(df, 100, LassoCV())
# run synthetic control experiments
df_synth_00, pred_lift_synth_00 = train_synth(df, 0)
df_synth_100, pred_lift_synth_100 = train_synth(df, 100)
experiment_data = {
"Method": ["Linear", "Linear", "Ridge", "Ridge", "Lasso", "Lasso", "Synthetic Control", "Synthetic Control"],
"Data Size": ["Large", "Small", "Large", "Small", "Large", "Small", "Large", "Small"],
"Value": [pred_lift_lin_reg_00, pred_lift_lin_reg_100, pred_lift_ridge_00, pred_lift_ridge_100,pred_lift_lasso_00, pred_lift_lasso_100, pred_lift_synth_00, pred_lift_synth_100]
}
df_experiments = pd.DataFrame(experiment_data)
我们将使用下面的代码来可视化结果:
# Set the style
sns.set_style="whitegrid"
# Create the bar plot
plt.figure(figsize=(10, 6))
bar_plot = sns.barplot(x="Method", y="Value", hue="Data Size", data=df_experiments, palette="muted")
# Add labels and title
plt.xlabel("Method")
plt.ylabel("Absolute error percentage")
plt.title("Synthetic Controls - Comparison of Methods Across Different Data Sizes")
plt.legend(title="Data Size")
# Show the plot
plt.show()

用户生成图像
小数据集的结果非常有趣!正如预期的那样,正则化有助于提高因果影响的估计。合成控制方法则更进一步!
大数据集的结果表明,较长的干预前期不一定更好。
然而,我希望你能记住的是,进行干预前的模拟是多么有价值。你可以使用自己的数据集探索许多不同的方向!
结束语
今天我们探讨了合成控制方法以及如何验证因果影响。我将留下几个最后的想法:
-
合成控制方法的简单性使其成为因果 AI 工具箱中最广泛使用的技术之一。
-
不幸的是,这也是最常被滥用的——让我们运行 R 的 CausalImpact 包,调整干预前期,直到我们看到一个满意的提升。 😭
-
在这里,我强烈建议进行干预前的模拟,以便事先确定测试设计。
-
合成控制方法是一个被广泛研究的领域。值得查看提出的改进方法:增强型 SC、稳健型 SC 和惩罚型 SC。
资源
Alberto Abadie, Alexis Diamond & Jens Hainmueller (2010) Synthetic Control Methods for Comparative Case Studies: Estimating the Effect of California’s Tobacco Control Program, Journal of the American Statistical Association, 105:490, 493–505, DOI: 10.1198/jasa.2009.ap08746
如果你想继续探索因果 AI 的旅程,欢迎关注我——在下一篇文章中,我们将探讨如何利用因果 AI 提升市场营销组合建模。
Pydantic V2 中的验证
使用字段验证器、注解验证器、字段验证器和模型验证器进行验证
·发表于 Towards Data Science ·7 分钟阅读·2024 年 7 月 17 日
--

图片由 Max Di Capua 提供,来源于 Unsplash
Pydantic 是Python中最常用的数据验证库,与 FastAPI、类、数据类和函数无缝集成。数据验证指的是验证输入字段是否为合适的数据类型(并在非严格模式下自动进行数据转换),对输入字段施加简单的数字或字符限制,甚至施加自定义和复杂的约束。
在处理更大规模的类和更多验证字段时,并且验证器能够处理和修改原始输入时,了解不同类型的验证器及其执行优先级是非常重要的。
本文将讨论 Pydantic 提供的不同类型的验证,以及不同类型验证的优先执行顺序,并提供代码示例,这些内容在 Pydantic 文档中并没有详细介绍。重点将放在类的验证上,也就是 BaseModel。
目录
向量嵌入是有损的。我们该怎么办?
AI 系统并不完美(哎呀!),这就是其中的一些原因。
·发表于 Towards Data Science ·阅读时间 17 分钟·2024 年 9 月 28 日
--

由作者使用 DALL-E 创建。过程中有一些信息丢失。
当我们将企业级 AI 系统投入生产时,我们不应期望它们像搜索引擎那样工作,也不像精确的词汇和短语数据库一样工作。是的,AI 系统通常看起来具有与(非向量)文档存储或搜索引擎相同的搜索能力,但在背后,它们的工作原理却完全不同。如果我们试图将 AI 系统——主要由向量存储和大型语言模型(LLM)构成——当作数据是结构化的,查询结果是精确的来使用,那么我们可能会得到一些出乎意料且令人失望的结果。
AI 系统通常不会“记住”数据本身。即使是 RAG 系统,它们保留了主要文档集的完整文本,也使用向量搜索进行检索,这一过程强大但不完美且不精确。在几乎所有的 AI 系统中,都会有一定量的信息被“丢失”。
当然,这就引出了一个问题:我们应该如何处理这种信息丢失?简短的回答是:我们应该识别那些受益于保留某些类型信息的使用案例,并在可能的情况下有意识地保留这些信息。通常,这意味着将确定性、结构化、非 AI 的软件流程整合到我们的系统中,目的是在需要时保留结构性和精确性。
在本文中,我们讨论了这个问题的细微差别以及一些潜在的解决方案。针对特定问题有许多可能的解决方法,例如实施知识图谱来构建主题和概念的结构,结合向量存储使用关键词搜索功能,或根据您的具体用例定制数据处理、分块和加载方式。除此之外,正如我们下面所讨论的,一种最具通用性和易于实施的方法是通过使用文档元数据,以结构化的方式来导航知识库,从而为非结构化的文档向量存储添加结构。文档链接和标签的向量图可以是一种强大、轻量、高效且易于实施的方式,将有用的结构重新引入到您的非结构化数据中。
AI 系统大多数是非结构化的、不精确的,并且模糊不清。
在围绕大量非结构化数据构建的系统中,信息丢失是不可避免的。诊断这种信息丢失发生的原因、方式和位置,可以帮助我们改进系统,打造更好的应用。
关于 AI 系统中的信息保留与丢失,三个最需要注意的要点是:
-
向量嵌入无法保留原文中的 100%的信息。
-
LLM 是非确定性的,这意味着文本生成过程中会包含一定的随机性。
-
很难预测哪些信息会丢失,哪些会被保留。
第一个意味着在文档进入向量存储的过程中,一些信息会丢失;第二个意味着在通过 LLM 进行检索时,部分信息会变得随机且不准确;第三个意味着我们可能不知道何时会出现问题,也不知道问题有多严重。
下面,我们将深入探讨上述第一点:向量嵌入本身是有损的。我们将讨论有损嵌入通常是不可避免的,它如何影响我们的应用程序,以及——与其在 LLM 框架内尝试恢复或防止丢失——更有价值的是,我们应当保持对信息丧失过程的意识,并在我们的 AI 系统中添加适合我们特定用例的结构化信息层,依托现有的向量嵌入驱动的 AI 系统的能力。
接下来,让我们更深入地探讨信息丢失在向量嵌入中的工作原理。
向量嵌入是有损的。
文本的向量表示——LLM 使用的嵌入——包含大量的信息,但这些信息必然是近似的。当然,也可以构建一个确定性 LLM,其向量代表精确的文本,可以在给定相同的初始向量时一字不差地反复生成。但是,这样的 LLM 将会是有限的,并且并不是很有用。为了使 LLM 及其向量嵌入在我们今天的工作方式中发挥作用,嵌入过程需要更多地捕捉语言的微妙概念,而非精确的单词本身。我们希望 LLM“理解”两句话虽然使用了不同的词语,但本质上表达的是相同的概念。例如,“我喜欢人工智能”和“AI 很棒”基本上告诉我们相同的信息,而向量和嵌入的主要作用是捕捉这些信息,而不是记住具体的单词。
向量嵌入具有高维度且精确,能够在广阔的概念空间内封装复杂的思想。这些维度可能达到数百或甚至数千,每个维度都在微妙地编码语言的各个方面——从语法和语义到语用学和情感。这种高维度使得模型能够在广泛的思想谱系中进行导航和表示,从而使其能够理解文本中嵌入的复杂和抽象概念。
尽管这些嵌入的精度很高,但从给定向量生成文本仍然是一个非确定性的过程。这主要是由于生成文本的模型具有概率性质。当一个大语言模型(LLM)生成文本时,它会基于向量中包含的信息,计算接下来可能出现的每个单词的概率。这个过程融入了随机性和上下文推理,这意味着即使使用相同的起始向量,每次生成的文本输出也可能不同。这种变异性对于生成自然流畅的语言至关重要,使其能够适应不同的语境,但也意味着文本的精确重现并非总是可能的。
虽然向量捕捉了文本意义的精髓,但在向量嵌入过程中,特定的单词和信息往往会丢失。这种丢失发生是因为嵌入被设计为从文本中进行概括,捕捉其整体意义,而非精确的措辞。因此,文本中的一些细节或不那么突出的主题可能在向量空间中没有得到充分表示。这一特性可能会导致在尝试从大量语料中检索特定事实或确切术语时遇到挑战,因为系统可能会优先考虑整体语义相似性,而不是精确的单词匹配。
我们可能会遇到信息丢失的两种最常见方式是:
-
文本中的旁支细节在文本整体的语义意义中“丢失”了。
-
在嵌入过程将特定关键字或短语转化为语义空间时,它们的意义被“丢失”了。
这两个案例中的第一个涉及文档(或片段)中实际细节的“丢失”,因为嵌入过程未能很好地捕捉到这些细节。第二个案例主要涉及信息的特定措辞的丢失,而不一定是实际细节的丢失。当然,这两种类型的丢失在各自的方式中都可能具有重要性并且会引发问题。
最近的文章 嵌入有点浅(同样出现在本出版物中)提供了很多有趣的例子,展示了嵌入如何丢失或错过细节,方法是通过测试在几个流行嵌入算法中相对较小的文本块之间的搜索和检索结果。
接下来,让我们通过代码和数据来看一些活生生的例子,演示这两种丢失类型如何发生。
案例研究:AI 产品页面
对于这个案例研究,我创建了一个虚构公司 Phrase AI 网站的产品页面数据集。Phrase AI 构建 LLM,并将其作为服务提供。其前三个产品是 Phrase Flow、Phrase Forge 和 Phrase Factory。Phrase Flow 是公司的旗舰 LLM,适用于一般用例,但在创意内容方面特别出色。其他两个产品是专业的 LLM,各自有自己的优缺点。
该 HTML 文档数据集由一个 phrase.ai(虚构)的主页、每个 LLM 的一个产品页面(共三个)以及网站上的四个页面组成:公司目标、进行中的工作、入门指南 和 使用案例。非产品页面主要集中在旗舰产品 Phrase Flow,而每个产品页面则专注于相应的 LLM。大多数文本是标准的网页文案,由 ChatGPT 生成,但文档中有一些特征对我们在此的目的非常重要。
最重要的是,每个产品页面都包含有关旗舰产品 Phrase Flow 的重要信息。具体来说,两个专业 LLM 的产品页面上都包含警告,提示不要将 Phrase Flow 模型用于特定目的。Phrase Forge 产品页面的底部包含以下文字:
Special Strengths: Phrase Forge is exceptionally good at creating a
complete Table of Contents, a task that general models like Phrase
Flow do not excel at. Do not use Phrase Flow for Tables of Contents.
而且,Phrase Factory 产品页面的底部包含以下文字:
Special Strengths: Phrase Factory is great for fact-checking and
preventing hallucinations, far better than more creative models like
Phrase Flow. Do not use Phrase Flow for documents that need to be factual.
当然,很容易说 Phrase AI 应该在他们的 Phrase Flow 页面上,而不仅仅是在其他两个产品的页面上放置这些警告。但我认为我们都见过关键信息出现在网站或文档中“错误”的地方,而即便一些信息不在文档集的最佳位置时,我们仍然希望我们的 RAG 系统能够很好地工作。
虽然这个数据集是虚构的,并且非常小,但我们已经设计它来清晰地说明在现实案例中可能非常常见的问题,这些问题在较大的数据集上可能很难诊断。接下来,让我们更仔细地研究这些问题。
旁枝末节的细节可能会被埋藏在语义空间中
如我前面所述,向量嵌入是有损的,预测哪些信息会以这种方式丢失是很困难的。所有信息都面临丢失的风险,但有些信息丢失的可能性更大。与文档的主要主题直接相关的细节通常更可能被捕捉到嵌入中,而偏离主要主题的细节则更可能丢失或通过向量检索难以找到。
在上述案例研究中,我们突出展示了关于 Phrase Flow 产品的两条信息,这些信息出现在其他两个型号的产品页面上。这两条警告措辞非常强烈,使用了“不要将 Phrase Flow 用于……”的字眼,可能对于回答有关 Phrase Flow 模型功能的查询至关重要。但它们出现在并非主要关于 Phrase Flow 的文档中,因此,对于这些文档来说,它们是“旁枝末节”的细节。
为了测试一个典型的 RAG 系统如何处理这些文档,我们使用 LangChain 的GraphVectorStore和 OpenAI 的 API 构建了一个 RAG 管道。代码可以在这个 Colab 笔记本中找到。
我们可以查询系统关于 Phrase Flow 的弱点,并得到以下结果:
Question:
What are some weaknesses of Phrase Flow?
Retrieved documents:
['https://phrase.ai/products/phraseflow',
'https://phrase.ai/usecases',
'https://phrase.ai/ongoingwork',
'https://phrase.ai/purpose']
LLM response:
The provided context does not mention any specific weaknesses of
Phrase Flow.
请注意,我们将检索器设置为获取排名前四的文档——即八个文档中的一半。包含有关 Phrase Flow 警告的两个产品页面并未出现在这些结果中,因此 LLM 的响应中没有关于 Phrase Flow 弱点的上下文。我们对于其他措辞以及询问 Phrase Flow 的“问题”或“缺陷”的其他查询也得到了类似的结果。更多示例请见笔记本。
如果我们不将检索限制为四个文档,我们将得到以下结果,包括所有八个文档的检索排名:
Question:
What are some weaknesses of Phrase Flow?
Retrieved documents:
['https://phrase.ai/products/phraseflow',
'https://phrase.ai/usecases',
'https://phrase.ai/ongoingwork',
'https://phrase.ai/purpose',
'https://phrase.ai/gettingstarted',
'https://phrase.ai/products/phraseforge',
'https://phrase.ai/products/phrasefactory',
'https://phrase.ai']
LLM response:
Phrase Flow is not ideal for creating Tables of Contents or for
documents that need to be factual. It may not excel in generating
detailed, structured writing for technical guides, reports, or
research papers, as these tasks are better suited for Phrase Forge.
Additionally, Phrase Flow might not be the best choice for answering
questions quickly or retrieving information in real-time, as Phrase
Factory is specifically designed for those needs.
值得注意的是,包含有关 Phrase Flow 警告的 Phrase Forge 和 Phrase Factory 这两个产品页面,在检索结果中分别排名第 6 和第 7。大致来说,这两条警告的可发现性较低,因为它们被“埋藏”在主要捕捉与每个页面主题相关的信息的向量嵌入中:即 Phrase Forge 或 Phrase Factory。
然而,如果我们将查询重新表述为与警告措辞“不要将 Phrase Flow 用于……”更为一致的语义,我们就能得到我们想要并且预期的结果:
Question:
When should I not use Phrase Flow?
Retrieved documents:
['https://phrase.ai/products/phraseflow',
'https://phrase.ai/products/phraseforge',
'https://phrase.ai/products/phrasefactory',
'https://phrase.ai/usecases']
LLM response:
You should not use Phrase Flow for creating documents that need to be
factual, such as technical guides, research papers, white papers, or
any text that requires precise language and detailed structure. For
these types of documents, Phrase Forge would be more appropriate.
Additionally, Phrase Flow is not ideal for creating Tables of
Contents.
在这里,检索似乎对查询的特定措辞非常敏感,“不要使用 Phrase Flow”这一短语将我们引向了在语义向量空间中更接近我们需要的文档。但我们事先并不会知道这一点。我们并不清楚自己究竟在寻找什么,完全依赖我们的 RAG 堆栈来帮助我们找到它。
在下面,我们讨论一些可能的解决方案,旨在解决这种由于语义向量丢失导致的埋藏信息问题。但首先,让我们看看另一种方式,丢失的向量如何在 RAG 系统中引发反直觉行为。
向量检索不是搜索引擎或关键词搜索
许多用户往往期望 AI 和 RAG 系统能够精确匹配名称、关键词和短语。我们习惯了传统的搜索引擎,而且我们有着明确的感觉,AI 强大得多,那它为什么不能找到我们想要的精确匹配呢?
如前所述,向量搜索在本质上与搜索引擎、文本搜索以及其他传统的非 AI 数据查询方法有着根本的不同——这些方法依赖搜索算法进行精确匹配,且支持的模糊搜索操作有限。虽然向量搜索确实常常能找到特定的单词和短语,但无法保证完全准确,因为向量存在于语义空间中,而将文本嵌入向量是一个有损的过程。
最有可能遭遇某种信息丢失的单词和短语,是那些语义含义不明确或模糊的词汇。我们在案例研究的数据集中包含了此类例子。具体来说,以下文本出现在我们虚构公司 Phrase AI 的“正在进行的工作”页面的末尾:
COMING SOON: Our newest specialized models Flow Factory, Flow Forge,
and Factory Forge are in beta and will be released soon!
这是数据集中唯一提到这些即将发布模型的地方。 “Flow Factory”、 “Flow Forge”和“Factory Forge”不仅是产品线中其他名称的混淆版,它们也是字典单词的简单组合。例如,“Flow Factory”具有超越产品名称的语义含义,包含了“flow”和“factory”两个词分别所包含的一些广为人知的含义。与此相比,一个像“FloFaktoree”这样的专有拼写几乎没有什么固有的语义含义,AI 系统可能会以完全不同的方式对待它——并且更可能作为一个与其他任何事物都不相关的术语,更容易被发现。
如果我们专门询问“Flow Forge”或“Factory Forge”,我们会得到如下结果:
Question:
What is Flow Forge?
Retrieved documents:
['https://phrase.ai/products/phraseforge',
'https://phrase.ai/usecases',
'https://phrase.ai/ongoingwork',
'https://phrase.ai/products/phraseflow']
LLM response:
Flow Forge is a new specialized model that is currently in beta and
will be released soon.
因此,系统成功检索到了一个文档——关于“正在进行的工作”页面,它包含了“Flow Forge”的引用,但它是排名第三的检索文档。在语义空间中,另外两个文档看起来更相关,尽管它们根本没有提到“Flow Forge”。在大型数据集上,很容易想象名称、术语、关键词和短语会在语义空间中被埋没或“丢失”,这种现象很难诊断。
如何处理有损向量?
我们一直在讨论有损向量,仿佛它们是需要解决的问题。当然,有些问题确实源于向量的“有损”特性,但向量搜索和 AI 系统依赖使用向量嵌入将文档从文本转换到语义空间,这一过程必然会丧失一些文本信息,但也带来了语义搜索的强大能力。所以,“有损”向量是一种特性,而非缺陷。即使有损向量不是缺陷,我们仍然需要理解它们的优点、缺点和限制,才能知道它们能够做什么,以及在何时它们可能以意想不到的方式给我们带来惊讶。
如果你发现以上描述的问题出现在你的 AI 系统中,根本原因很可能不是向量搜索表现不佳。你可以尝试寻找更适合的嵌入方法,但这是一个复杂且不透明的过程,通常有更简单的解决方案。
上述问题的根本原因在于,我们常常试图让向量搜索执行它并未设计的任务。因此,解决方案是将功能构建到你的技术栈中,添加你在特定使用场景下所需的能力,同时与向量搜索一起使用。
交替分块和嵌入方法
在处理文档加载和嵌入方法时,有许多选择。我们可以通过选择与我们的数据集和使用场景高度契合的方法来防止在嵌入过程中丢失一些信息。以下是几种这样的替代方案:
优化分块策略 — 分块策略决定了文本如何被分割成块以供处理和检索。优化分块不仅仅是考虑大小或边界问题;它还包括以与内容中的主题元素或逻辑划分一致的方式对文本进行分割。这种方法确保每个分块代表一个完整的思想或主题,有助于生成更具连贯性的嵌入,并提高 RAG 系统的检索准确性。
多向量嵌入技术 — 标准嵌入方法通常将段落简化为一个单一的向量表示,这可能无法捕捉到段落的多维特性。多向量嵌入技术通过使用模型从一个段落生成多个嵌入来解决这个限制,每个嵌入对应段落可能回答的不同解释或问题。这种策略增强了数据表示的维度丰富性,从而使得在不同查询类型下的检索更加精确。
ColBERT:基于标记的嵌入 — ColBERT (BERT 上的上下文化晚期交互)是一种嵌入实践,在这种方法中,段落中的每个标记都有自己的嵌入。这种细粒度的方法使得每个单独的标记——特别是重要或独特的关键词——能够对检索过程产生更大的影响,类似于关键词搜索的精确性,同时利用现代 BERT 模型的上下文理解。尽管 ColBERT 对计算资源的要求较高,但它通过在嵌入中保留关键术语的重要性,可以提供更优的检索性能。
多头 RAG 方法 —— 基于变换器架构的能力,多头 RAG利用变换器的多个注意力头为每个查询或段落生成多个嵌入。每个头可以强调文本的不同特征或方面,从而生成一个多样化的嵌入集合,捕捉信息的各个维度。这种方法通过提供更丰富的语义线索来增强系统处理复杂查询的能力,这些线索可以在生成响应时为模型提供帮助。
在你的 AI 堆栈中构建结构
向量搜索和 AI 系统非常适合处理非结构化知识和数据,但大多数使用场景都能从 AI 堆栈中的一些结构中受益。
一个非常明确的例子是:如果你的使用场景和用户依赖于关键词搜索和精确文本匹配,那么将文档存储与文本搜索功能集成可能是个不错的主意。通常情况下,集成经典的文本搜索比让向量存储成为一个高度可靠的文本搜索工具要便宜、更加稳健,而且更容易。
知识图谱也是将结构引入 AI 堆栈的另一种有效方式。如果你已经拥有或能够构建一个适合你使用场景的高质量图谱,那么构建一些图谱功能,例如图谱 RAG,可以提升你 AI 系统的整体效用。
在许多情况下,我们的原始数据集可能具有固有的结构,但我们在向量搜索中并没有充分利用它。几乎所有文档结构在数据准备过程中,都会在加载到向量存储之前被剥离。HTML、PDF、Markdown 和大多数其他文档类型都包含可以被利用的结构元素,从而使我们的 AI 系统变得更好、更可靠。在下一部分,我们将看看这如何发挥作用。
通过文档链接和标签添加一层结构
回到我们上面的案例研究,我们可以利用 HTML 文档的结构来使我们的向量搜索和 RAG 系统变得更好、更可靠。特别是,我们可以利用 HTML 文档中的超链接来连接相关的实体和概念,确保我们通过向量存储中的所有相关文档获得全貌。请参阅这篇文章了解图谱 RAG 中文档链接的介绍。
值得注意的是,在我们的文档集中,所有产品名称都链接到产品页面。每当页面上提到三个产品中的任何一个时,产品名称文本都会超链接到相应的产品页面。而且所有产品页面之间都会相互链接。
我们可以利用这种链接结构,通过向量图遍历和GraphVectorStore在 LangChain 中的实现。
这个实现使我们能够轻松地构建基于文档之间超链接的知识图谱,然后遍历这个图谱,以拉取与给定文档直接相关的文档。在实际操作中(以及背后的机制),我们首先通过向量搜索执行标准的文档检索,然后遍历检索到的文档中的链接,从而拉取更多相关文档,无论这些文档是否在向量搜索中显示为“相关”。通过这种实现,检索不仅获取与查询最语义相关的一组文档,还包括那些直接链接的文档,这些文档可能为回答查询提供有价值的辅助信息。
将检索从我们的使用场景重新配置为通过从每个文档出发的单步链接遍历图(depth=1),我们可以从原始查询中得到以下结果:
Question:
What are some weaknesses of Phrase Flow?
Retrieved documents:
['https://phrase.ai/products/phraseflow',
'https://phrase.ai/ongoingwork',
'https://phrase.ai/usecases',
'https://phrase.ai/purpose',
'https://phrase.ai/products/phrasefactory',
'https://phrase.ai/products/phraseforge']
LLM response:
Phrase Flow is not ideal for documents that need to be factual or for
creating a complete Table of Contents. Additionally, it might not be
the best choice for tasks that require a lot of thought or structure,
as it is more focused on making the language engaging and fluid rather
than detailed and organized.
我们可以在这个输出中看到,尽管我们仍然将初始检索设置为从向量搜索中返回k=4个文档,但由于这些文档是从原始文档集直接链接过来的,因此额外检索到了两篇文档。这两篇文档恰好包含了原始查询结果中缺失的关键信息,当时我们只使用了向量搜索而没有图。加入这两篇文档后,关于短语流的两个警告出现在检索到的文档集中,LLM 可以提供恰当的信息响应。
在这个基于向量图的 RAG 系统中,向量可能是有损的,但超链接以及由此产生的图边缘则不是。它们提供了文档之间稳固且有意义的连接,可以用来以可靠和确定的方式丰富检索到的文档集,这是应对 AI 系统有损和无结构特性的解药。而且,随着 AI 继续革新我们处理无结构数据的方式,我们的软件和数据堆栈也在不断受益于尽可能利用结构,尤其是当结构与当前的使用场景相匹配时。
结论
我们知道,向量嵌入是有损的,以多种方式存在。选择与数据集和使用场景相契合的嵌入方案可以改善结果,并减少有损嵌入的负面影响,但还有其他有用的选择。
向量图可以直接利用文档数据集中固有的结构。从某种意义上说,这就像让数据构建一个固有的知识图谱,通过超链接和其他在文档中存在的引用,将相关的文本块互相连接——例如,利用文档中的超链接和其他引用来发现与之相关并可能有用的其他文档。
你可以尝试使用本文中引用的这个 Colab 笔记本中的代码自己进行链接和矢量图形生成。或者,想要了解并尝试文档链接,请查看我之前的文章,或了解更深入的技术细节,关于通过消除边缘来扩展知识图谱。
作者:Brian Godsey 博士 (LinkedIn) — 数学家、数据科学家和工程师 // 在 DataStax 从事 AI 产品工作 // 编写了《数据科学家思维方式》一书 Think Like a Data Scientist
附言:仅供娱乐,这里是一些其他尝试生成的封面图像。😎

由作者使用 DALL-E 生成。

由作者使用 DALL-E 生成。
VerifAI 项目:开源生物医学问答系统,提供经过验证的答案
基于 LLM(Mistral 7B)的生物医学问答系统,构建在 Qdrant 和 OpenSearch 索引之上,并结合幻觉检测方法的经验。
Nikola Milosevic (Data Warrior)
·发表于Towards Data Science ·阅读时间:12 分钟·2024 年 7 月 15 日
--
去年 9 月(2023 年),我们开始了VerifAI 项目,该项目在获得NGI Search计划的资助后启动,该计划隶属于欧盟地平线计划。
项目的想法是创建一个面向生物医学领域的生成搜索引擎,基于经过验证的文献(因此我们使用了一个名为PubMed的生物医学期刊文章库),并添加一个模型,通过比较参考文章和生成的声明来验证生成的答案。在生物医学领域,甚至在科学领域,幻觉(错误信息)的容忍度非常低。
尽管像 Elicit 或 Perplexity 这样的项目和产品确实进行了一部分 RAG(检索增强生成),能够回答生物医学问题并引用相关文档,但有几个因素使我们的项目与众不同。首先,我们目前专注于生物医学文档。其次,作为一个由欧盟资助的项目,我们承诺将所有创建的内容(包括源代码、模型、模型适配器、数据集等)开源。第三,目前没有其他产品能够对生成的答案进行事后验证,它们通常只是依赖相对简单的 RAG,这可以减少幻觉现象,但无法完全消除它们。该项目的主要目标之一是解决所谓的幻觉问题。大语言模型(LLM)中的幻觉是指模型生成的文本看起来合理,但实际上是事实错误、误导性或无意义的。在这方面,该项目为实时系统增添了独特的价值。
该项目已根据AGPLv3 许可证共享。
整体方法
我们应用的整体方法可以从下图中看到。

系统的整体架构(作者提供的图像)
当用户提出问题时,用户查询会被转换成查询,信息检索引擎会根据给定的问题向 PubMed 索引中检索最相关的生物医学摘要。为了获取最相关的文档,我们创建了一个基于OpenSearch的词汇索引和一个基于Qdrant的向量/语义搜索。具体来说,词汇搜索非常适合检索包含查询中确切术语的相关文档,而语义搜索帮助我们在语义空间中搜索,并检索那些意思相同但措辞不同的文档。检索得分会被归一化,然后从这两个索引中结合提取文档(混合搜索)。来自混合搜索的顶级文档和问题会传递给模型的上下文进行答案生成。在我们的案例中,我们使用 QLoRA 微调了 Mistral 7B-instruct 模型,因为这样我们可以在一个相对便宜的云实例上托管一个表现良好的小型模型(该实例包含 NVidia Tesla T4 GPU 和 16GB 的 GPU 内存)。答案生成后,答案会被解析成句子,并引用支持这些句子的文献,然后传递给一个独立的模型,检查生成的声明是否得到引用摘要内容的支持。该模型将声明分类为支持、无证据和矛盾三类。最后,答案和可能未被摘要完全支持的声明将被呈现给用户。
因此,系统有三个组件——信息检索、答案生成和答案验证。在接下来的部分,我们将更详细地描述这些组件。
信息检索
从项目开始,我们的目标是构建一个混合搜索,结合语义搜索和词汇搜索。最初的想法是使用单一软件来实现,然而,这并不是那么容易,特别是对于 PubMed 的索引大小来说。PubMed 包含大约 3500 万篇文献,但并非所有文献都包含完整的摘要。有些较旧的文献,来自 1940 年代和 1950 年代,可能没有摘要,还有一些指南文档等,仅包含标题。我们只对包含完整摘要的文献进行了索引,最终得到了约 2500 万篇文献。解压后的 PubMed 大约为 120GB 大小。
在 OpenSearch 中创建一个高效的词汇搜索索引并不难。这几乎可以开箱即用。我们对标题、摘要文本进行了索引,并添加了一些用于过滤的元数据,如出版年份、作者和期刊。OpenSearch 支持FAISS作为向量存储。因此,我们尝试使用 FAISS 来索引我们的数据,但由于索引太大,且内存不足(我们为索引配置了 64GB 的云实例),这一尝试未能成功。索引是使用基于 DistilBERT 的 MSMarco 微调模型进行的(sentence-transformers/msmarco-distilbert-base-tas-b)。由于我们了解到 FAISS 仅支持内存中的索引,因此我们需要找到一种解决方案,可以将部分索引存储在硬盘上。这个解决方案找到了——Qdrant 数据库,它支持内存映射和部分索引的磁盘存储。
在创建索引时,出现的另一个问题是,一旦我们进行了内存映射并创建了整个 PubMed 索引,查询的执行时间会非常长(几乎 30 秒)。问题出在,使用 32 位精度计算点积时,对于包含 2500 万篇文献的索引来说,耗时较长(并且可能从硬盘加载索引部分)。因此,我们使用了仅采用 8 位精度的搜索方法,将所需时间从约 30 秒减少到了不到半秒。
词汇索引包含完整的摘要,而语义索引则需要对文献进行拆分,因为我们用来构建语义索引的变换器模型最多能处理 512 个 tokens。因此,文献在第 512 个 token 之前会根据句号进行拆分,每当达到下一个 512 个 tokens 时,都会进行相同的操作。
为了创建语义搜索和词汇搜索的结合,我们对查询的输出进行了规范化,将 Qdrant 或 OpenSearch 返回的所有分数除以给定文档库中返回的最高分。这样,我们得到了两个数值,一个代表语义搜索,另一个代表词汇搜索,范围在 0 到 1 之间。然后,我们使用BioASQ 数据集测试了在检索的最相关文档中的精度。结果可以在下表中看到。

信息检索的结果,评估语义和词汇搜索的权重。如图所示,最佳结果出现在词汇搜索权重为 0.7,语义搜索权重为 0.3 时。图像来自我们已在 BioNLP 上接受的论文,预印本可在arxiv.org/abs/2407.05015v1查看。
我们进行了一些使用全精度的重新排序实验,因此可以在论文中看到更多细节。但这在最终的应用中没有使用。总体结论是,词汇搜索表现得相当不错,语义搜索也有一定的贡献,在词汇搜索权重为 0.7,语义搜索权重为 0.3 时获得了最佳性能。
最后,我们构建了一个查询处理系统,对于词汇查询,查询中排除了停用词,并在词汇索引中进行了搜索,同时计算语义索引的相似度。来自语义和词汇索引的文档值都进行了规范化并求和,然后检索到前 10 个文档。
参考答案生成
一旦检索到前 10 个文档,我们可以将这些文档传递给生成模型来生成参考答案。我们已经测试了几种模型。使用 GPT4-Turbo 模型可以很好地完成这项工作,大多数商业平台也会使用 GPT4 或 Claude 模型。然而,我们希望创建一个开源版本,避免依赖商业模型,同时使用更小、更高效的模型,并且其性能接近商业模型。因此,我们在零-shot 模式下,使用 4bit QLora 进行微调,测试了 Mistral 7B instruct 模型。
为了微调 Mistral,我们需要创建一个使用 PubMed 的参考问答数据集。我们通过随机选择 PubMedQA 数据集中的问题来创建数据集,然后检索前 10 个相关文档,并使用 GPT-4 Turbo 生成参考答案。我们将这个数据集称为PQAref 数据集,并已发布在 HuggingFace 上。该数据集中的每个样本包含一个问题、一组 10 个文档和一个生成的答案,答案基于 10 个传入上下文的参考文档。
使用这个数据集,我们为 Mistral-7B-instruct 创建了一个 QLoRA 适配器。该适配器在塞尔维亚国家 AI 平台的塞尔维亚国家数据中心进行训练,使用了 Nvidia A100 GPU,训练时长大约为 32 小时。
我们进行了一项评估,比较了 Mistral 7B instruct v1 与 Mistral 7B instruct v2,在有无 QLoRA 微调的情况下(没有 QLoRA 微调时为零-shot,仅基于指令,使用 QLoRA 时我们可以节省一些 token,因为提示中不再需要指令,微调后的模型会根据需求执行任务),并且将其与 GPT-4 Turbo 进行了比较(提示:“使用相关摘要回答问题,最多 300 字。参考所提供的 abstract_id 并将其放在陈述旁边的括号中。”)。我们进行了多项评估,关于引用文献的数量以及这些引用文献是否相关。以下表格展示了这些结果。

各种模型中包含 N 个引用文献的答案数量。图片来自我们在 BioNLP 接受的论文,预印本可见于arxiv.org/abs/2407.05015v1
从这张表格可以得出结论,Mistral,特别是零-shot 的第一版本(0-M1),即使提示中要求引用上下文文献,它也并不经常引用文献。另一方面,第二个版本表现出了更好的性能,但仍然远不及 GPT4-Turbo 或微调后的 Mistral 7B。微调后的 Mistral 往往会引用更多文献,即使答案可以从某一文献中找到,而且比 GPT4-Turbo 多出一些额外信息。

各种模型中引用的相关文献数量。图片来自我们在 BioNLP 接受的论文,预印本可见于arxiv.org/abs/2407.05015v1
从第二个表格可以看出,GPT4-Turbo 在整个测试集中只错过了一次相关引用,而 Mistral 7B-instruct v2 微调后错过的相关引用稍多,但考虑到其模型规模更小,表现仍然相当。
我们还手动查看了多个答案,以确保答案是合理的。最终,在应用中,我们使用的是 Mistral 7B instruct v2,并配合微调的 QLoRA 适配器。
答案验证
系统的最终部分是答案验证。对于答案验证,我们开发了多个功能,但主要的是一个系统,它验证生成的声明是否基于所引用的摘要。我们在Allen Institute for AI的SciFact 数据集上对多个基于 BERT 和 Roberta 的模型进行了微调。
为了处理模型输入,我们解析了答案以找到句子和相关参考文献。我们发现系统中的第一句和最后一句通常是引言或结论句,可能没有引用。所有其他句子都应该有引用。如果一个句子包含引用,它是基于该 PubMed 文档的。如果句子没有引用,但前后句有引用,我们计算该句子与 2 个摘要中的句子的嵌入点积。点积最大的摘要被认为是该句子的来源摘要。
一旦答案被解析,并且我们找到了所有与理赔相关的摘要,我们将其传递给微调后的模型。输入模型的方式如下所示:
对于 deBERT-a 模型:
[CLS]claim[SEP]evidence[SEP]
对于基于 Roberta 的模型:
claimevidence
在这里,claim 是生成组件生成的理赔,evidence 是来自 PubMed 文献中引用的标题和摘要拼接文本。
我们评估了微调模型的性能,并得到了以下结果:

在 SciFact 数据集上训练和测试的模型评估。图片由作者提供,论文已提交至第 16 届国际知识管理与信息系统会议。
通常,当模型在与测试数据集相同的数据集上进行微调时,结果表现良好。因此,我们希望在域外数据上进行测试。所以我们选择了HealthVer 数据集,它也属于医疗健康领域,主要用于理赔验证。测试结果如下:

在 HealthVer 数据集上的测试结果。图片由作者提供,论文已提交至第 16 届国际知识管理与信息系统会议。
我们还使用 GPT-4 模型评估了 SciFact 标签预测任务(提示语为:“批判性地评估该声明是否得到支持、被反驳,或者在给定摘要中没有证据支持该声明。如果声明得到摘要的支持,请输出 SUPPORT;如果声明与摘要矛盾,请输出 CONTRADICT;如果摘要中没有支持该声明的证据,请输出 NO EVIDENCE。”),得出了精确度为 0.81、召回率为 0.80、F-1 分数为 0.79 的结果。因此,我们的模型表现更好,且由于参数数量显著更少,效率也更高。
在使用该模型进行验证的基础上,我们还通过点积相似度计算了摘要中与生成句子最接近的句子(使用与语义搜索相同的 MSMarco 模型)。我们通过在用户界面上悬停句子来可视化与生成句子最接近的句子。
用户界面
我们开发了一个用户界面,用户可以在其中注册、登录并提问,系统将提供参考答案、链接到 PubMed 以及通过描述的后验模型进行验证。以下是该用户界面的几个截图:

用户界面正在生成答案时的截图。截图由作者提供

输出结果,包括验证。截图由作者提供

生成不同答案并打开配置窗口的截图。截图由作者提供
结论

VerifAI 项目 logo。Logo 由作者提供
我们在这里分享构建 VerifAI 项目的经验,这是一个生物医学生成式问答引擎,能够提供可验证的答案。我们将开源整个代码和模型,并且暂时(具体时间取决于预算及是否能找到可持续的托管方案)开放应用程序。在下一部分中,您可以找到应用程序、网站、代码和模型的链接。
该应用程序是多个人(请参见团队部分)和近一年研究与开发工作的成果。我们很高兴并自豪地将其呈现给更广泛的公众,并希望大家会喜欢它,同时也能为其做出贡献,推动它在未来变得更加可持续和更好。
引用我们的论文
如果您在论文的背景部分使用了任何方法论、模型、数据集,或者提到了这个项目,请引用以下部分论文:
可用性
该应用程序可以通过 verifai-project.com/ 或 app.verifai-project.com/ 访问和尝试。用户可以在此注册并提问,尝试我们的平台。
项目的代码可以在 GitHub 上找到,链接为github.com/nikolamilosevic86/verif.ai。Mistra 7B instruct 的 QLoRA 适配器可以在 HuggingFace 上找到,链接为huggingface.co/BojanaBas/Mistral-7B-Instruct-v0.2-pqa-10 和 huggingface.co/BojanaBas/Mistral-7B-Instruct-v0.1-pqa-10。用于微调生成组件的生成数据集也可以在 HuggingFace 上找到,链接为huggingface.co/datasets/BojanaBas/PQAref。一个用于答案验证的模型可以在huggingface.co/MilosKosRad/TextualEntailment_DeBERTa_preprocessedSciFACT找到。
团队
VerifAI 项目是由拜耳制药研发部和塞尔维亚人工智能研究与发展研究所合作开发的,项目资金由 NGI Search 项目提供,资助协议号为 101069364。参与人员包括Nikola Milosevic、Lorenzo Cassano、Bojana Bašaragin、Miloš Košprdić、Adela Ljajić 和 Darija Medvecki。

如果你想看到团队成员的面孔 — 团队(以及一些家人)在柏林普拉特啤酒花园的合影(作者提供,2024 年 7 月 10 日)
Vision Mamba:像 Vision Transformer,但更强大
🐍 走向马姆巴状态空间模型:图像、视频与时间序列
第四部分 — 走向马姆巴状态空间模型:图像、视频与时间序列
·发布于 Towards Data Science ·20 分钟阅读·2024 年 9 月 16 日
--

图片来源:Sascha Kirch.
这是我新系列文章🐍 走向马姆巴状态空间模型:图像、视频与时间序列的第四部分。
近年来,计算机视觉领域取得了令人难以置信的进展。推动这一发展的关键因素之一无疑是 Transformer 的引入。虽然 Transformer 彻底革新了自然语言处理,但我们花了几年时间才将其能力转移到视觉领域。可能最具代表性的论文是Vision Transformer (ViT),这是一种模型,至今仍被许多现代架构作为基础模型使用。
正是 Transformer 的O(L²)复杂度限制了其在图像分辨率增高时的应用。借助于马姆巴选择性状态空间模型,我们现在能够让历史重演,并将 SSM(状态空间模型)在序列数据中的成功转移到非序列数据中:图像。
❗ 剧透提醒:VisionMamba 比DeiT快 2.8 倍,并且在高分辨率图像(1248x1248)上节省了 86.8%的 GPU 内存,在这篇文章中,你将看到如何做到…
带有 BatchNorm 的视觉变换器(Vision Transformer)
集成 BatchNorm 到标准视觉变换器架构中如何加速收敛并使网络更加稳定
·发表于Towards Data Science ·14 分钟阅读·2024 年 11 月 8 日
--

比较不同深度下,带有和不带有 BatchNorm 的视觉变换器。
介绍
视觉变换器(ViT)是首个完全基于自注意力机制的图像分类任务架构。尽管 ViT 在性能上优于基于卷积神经网络(CNN)的架构,但它们需要在非常大的数据集上进行预训练。为了寻找可能加速训练和推理的 ViT 改进方案——尤其是在中小规模输入数据的情况下——我在上一篇文章中探讨了将 Batch Normalization(BatchNorm)集成到 ViT 型模型中的方法。BatchNorm 已知能够加速深度神经网络的收敛——与基准模型相比,使用 BatchNorm 的网络在相同训练周期数下能获得更高的准确率。这反过来加快了训练速度。BatchNorm 还作为一种有效的正则化方法,允许模型使用更高的学习率进行训练。本文的主要目标是研究引入 BatchNorm 是否能在视觉变换器中产生类似的效果。
为了具体化,我将重点讨论在 ViT 的 Transformer 编码器中的前馈网络(FFN)引入 BatchNorm 层,并省略 FFN 之前的 LayerNorm 层的模型。在 Transformer 的其他部分——包括自注意力模块——仍然使用 LayerNorm。我将该版本的 ViT 称为ViTBNFFN——在前馈网络中使用 BatchNorm 的视觉 Transformer。我将使用带有图像增强的 MNIST 数据集对该模型进行训练和测试,并在多个训练周期内比较该模型的 Top-1 准确率与标准 ViT 的准确率。我将为这两个模型选择相同的架构配置(即相同的宽度、深度、补丁大小等),以便有效隔离 BatchNorm 层的影响。
以下是主要发现的快速总结:
-
在合理选择的超参数(学习率和批量大小)下,ViTBNFFN 比 ViT 收敛得更快,前提是 Transformer 的深度(即编码器中的层数)足够大。
-
随着学习率的增加,ViTBNFFN 比 ViT 更稳定,尤其是在更深的网络中。
我将简要讨论深度神经网络中的 BatchNorm,并通过具体示例说明上述提到的一些特性。接着,我将详细讨论 ViTBNFFN 模型的架构。最后,我将深入探讨研究 BatchNorm 在视觉 Transformer 中作用的数值实验。
你可以在 github 的仓库上分叉本文及上一篇文章中使用的代码并进行实验!
数据集:带有图像增强的 MNIST
让我们从介绍增强版 MNIST 数据集开始,我将在本文描述的所有数值实验中使用该数据集。训练和测试数据集由函数get_datasets_mnist()提供,如代码块 1 所示。
代码块 1. 通过增强 MNIST 数据集得到的训练和验证数据集。
关键的代码行位于第 5 到第 10 行,列出了我将使用的图像增强的详细信息。我引入了三种不同的变换:
-
随机旋转(degrees=20) : 对图像进行随机旋转,旋转角度范围为(-20, 20)。
-
RandomAffine(degrees = 0, translate = (0.2, 0.2)) : 一个随机仿射变换,其中 translate = (a, b) 表示水平和垂直的平移分别在区间[- image_width × a, image_width × a] 和 [-image_height × b, image_height × b] 内随机采样。degrees=0 语句禁用旋转,因为我们已经通过随机旋转考虑了这一点。这里也可以包含缩放变换,但我们通过缩小操作实现了它。
-
RandomZoomOut(0,(2.0, 3.0), p=0.2) : 一个随机缩小变换,它随机采样区间(2.0, 3.0)中的浮动 r,并输出一个图像,其输出宽度 = 输入宽度 × r,输出高度 = 输入高度 × r。浮动 p 是执行缩放操作的概率。该变换之后跟随一个 Resize 变换,确保最终图像的大小仍为 28 × 28。
深度神经网络中的批量归一化
让我们快速回顾一下 BatchNorm 如何提高深度神经网络的性能。假设 zᵃᵢ 表示深度神经网络中给定层的输入,其中 a 是批次索引,范围为 a=1,…, Nₛ,i 是特征索引,范围为 i=1,…, C。BatchNorm 操作包括以下步骤:
- 对于给定的特征索引 i,首先计算批量大小为 Nₛ 的均值和方差,即:

2. 使用上述计算的均值和方差对输入进行归一化(其中 ϵ 是一个小的正数):

3. 最后,对于每个特征 i,移动并重新缩放归一化后的输入:

其中没有对索引 i 进行求和,参数(γᵢ,βᵢ)是可训练的。
考虑一个用于分类 MNIST 数据集的深度神经网络。我将选择一个由 3 个全连接隐藏层组成的网络,每个隐藏层有 100 个激活单元,每个隐藏层都配有 sigmoid 激活函数。最后一个隐藏层连接到一个分类层,分类层有 10 个激活单元,对应 MNIST 数据集的 10 个类别。该神经网络的输入是一个形状为 b × 28² 的二维张量——其中 b 是批次大小,每个 28 × 28 的 MNIST 图像被重塑为一个 28² 维的向量。在这种情况下,特征索引从 i=1,…, 28² 运行。
该模型类似于原始 BatchNorm论文中讨论的模型——我将这个模型称为DNN_d3。可以考虑在每个隐藏层的 sigmoid 激活函数之前添加一个 BatchNorm 层的版本。我们将得到的模型称为DNNBN_d3。其目的是了解引入 BatchNorm 层如何影响网络的性能。
现在,为了实现这一点,让我们在上面描述的 MNIST 数据集上训练和测试这两个模型,使用CrossEntropyLoss()作为损失函数和Adam优化器,训练 15 个周期。对于学习率 lr=0.01 和训练批次大小为 100(我们选择测试批次大小为 5000),模型的测试准确率和训练损失如图 1 所示。

图 1. 两种模型在 lr=0.01 下训练 15 个周期的测试准确率(左)和训练损失(右)。
显然,引入 BatchNorm 使得网络收敛得更快——DNNBN 达到了更高的测试准确率和更低的训练损失。因此,BatchNorm 可以加速训练。
如果增加学习率会发生什么?一般来说,较高的学习率可能导致梯度爆炸或消失,从而使训练变得不稳定。特别是,较大的学习率会导致更大的层参数,这又会在反向传播过程中产生更大的梯度。然而,BatchNorm 确保反向传播过程不会受到层参数尺度变换的影响(更多细节见本文的第 3.3 节)。这使得网络在面对较高学习率时,能显著提高对不稳定性的抗性。
为了明确地展示这一点,假设我们以更高的学习率 lr=0.1 训练这些模型——在这种情况下,模型的测试准确率和训练损失如图 2 所示。

图 2. 两种模型在 lr=0.1 下训练 15 个周期的测试准确率(左)和训练损失(右)。
高学习率显然使得 DNN 不稳定。然而,使用 BatchNorm 的模型表现得非常稳定!一种更具启发性的方式是,将两种学习率下的准确率曲线绘制在同一图中,如图 3 所示。

图 3. DNN_d3(左)和 DNNBN_d3(右)在两种不同学习率下的准确率曲线。
当模型 DNN_d3 在高学习率下停止训练时,DNNBN_d3 的性能影响要轻得多。因此,BatchNorm 允许在更高的学习率下训练模型,提供了一种加速训练的方式。
模型 ViTBNFFN:前馈网络中的 BatchNorm
让我们首先简要回顾标准 Vision Transformer(用于图像分类任务)的架构,如图 4 中的示意图所示。如需更多详细信息,请参见我之前的 文章,或阅读 Towards Data Science 中的许多精彩综述。

图 4. ViT 架构的示意图。
从功能上讲,Vision Transformer 的架构可以分为三个主要组件:
- 嵌入层: 该层将图像映射为“句子”——一系列标记,其中每个标记是一个维度为 dₑ(嵌入维度)的向量。给定大小为 h × w 和 c 个颜色通道的图像,首先将其分割成大小为 p × p 的补丁并展平——这将得到 (h × w)/p² 个展平的补丁(或标记),每个补丁的维度为 dₚ = p² × c,接着通过可学习的线性变换将其映射为维度为 dₑ 的向量。然后,为这一系列标记添加一个可学习的标记——CLS 标记——并将其隔离在末尾用于分类任务。示意图如下所示:

最后,为该标记序列添加一个相同形状的可学习张量,该张量编码位置嵌入信息。结果的标记序列被输入到 Transformer 编码器。因此,编码器的输入是一个形状为 b × N × dₑ 的 3d 张量——其中 b 是批量大小,N 是标记的数量(包括 CLS 标记),dₑ 是嵌入维度。
2. Transformer 编码器: Transformer 编码器将标记序列映射为另一个标记序列,二者的数量和形状相同。换句话说,它将形状为 b × N × dₑ 的输入 3d 张量映射为另一个形状相同的 3d 张量。编码器可以有 L 个不同的层(定义为 Transformer 的 深度),每一层由两个子模块组成,如图 5 所示——多头自注意力(MHSA)和前馈网络(FFN)。

图 5. Transformer 编码器的子模块。
MHSA 模块在形状为 b × N × dₑ 的 3d 张量上实现非线性映射,映射结果是一个形状相同的 3d 张量,随后该张量被送入 FFN,如图 2 所示。这是信息通过自注意力映射在不同标记之间混合的地方。MHSA 模块的配置由头数 nₕ 和头维度 dₕ 决定。
FFN 是一个深度神经网络,包含两层线性层,中间有一个 GELU 激活函数,如图 6 所示。

图 6. Transformer 编码器层中的 FFN 模块。
该子模块的输入是形状为 b × N × dₑ 的 3d 张量。左侧的线性层将其转换为形状为 b × N × d_mlp 的 3d 张量,其中 d_mlp 是网络的隐藏维度。经过非线性 GELU 激活后,张量通过第二层被映射回原始形状的张量。
3. MLP 头: MLP 头是一个全连接网络,将 transformer 编码器的输出 — 形状为 b × N × dₑ 的 3d 张量 — 映射到形状为 b × d_num 的 2d 张量,其中 d_num 是给定图像分类任务中的类别数。这通过首先从输入张量中提取 CLS 令牌,然后将其传递通过连接的网络来完成。
模型 ViTBNFFN 与上述描述的架构相同,主要有两个区别。首先,在编码器的 FFN 中引入了一个 BatchNorm 层,该层位于第一个线性层和 GELU 激活之间,如图 7 所示。其次,移除了标准 ViT 编码器中 FFN 之前的 LayerNorm(参见上面的图 5)。

图 7. ViTBNFFN 模型的 FFN 子模块。
由于线性变换作用于形状为 b × N × dₑ 的输入张量的第三维,我们应将 dₑ 视为 BatchNorm 的特征维度。新前馈网络的 PyTorch 实现见代码块 2。
代码块 2. 带有 BatchNorm 的 FFN 模块,用于 ViTBNFFN。
PyTorch 中内置的 BatchNorm 类总是将张量的第一个索引视为批次索引,第二个索引视为特征索引。因此,我们需要将形状为 b × N × dₑ 的 3d 张量转换为形状为 b × dₑ × N 的张量,才能应用 BatchNorm,然后再将其转换回形状为 b × N × dₑ 的 3d 张量。此外,我使用了 2d BatchNorm 类(因为它比 1d BatchNorm 略快)。这要求将 3d 张量提升为形状为 b × dₑ × N × 1 的 4d 张量(第 16 行),并在第 18 行将其转换回形状为 b × N × dₑ 的 3d 张量。使用 1d BatchNorm 类也可以得到相同的结果,无需更改本节中展示的任何内容。
实验
在固定的学习率和批量大小下,我将在增强版 MNIST 数据集上训练和测试两个模型 — ViT 和 ViTBNFFN — 进行 10 轮训练,并比较验证数据集上的 Top-1 准确率。由于我们关注 BatchNorm 的效果,因此我们需要比较这两个具有相同配置的模型。该实验将在不同深度的 transformer 编码器上重复进行,其余模型配置保持不变。我在此实验中使用的两个模型的具体配置如下:
-
嵌入层:MNIST 图像是一个 28× 28 的灰度图像。补丁大小为 p= 7,这意味着令牌的数量为 16 + 1 = 17,包括 CLS 令牌。嵌入维度为 dₑ = 64。
-
变压器编码器:MHSA 子模块有 nₕ = 8 个头,每个头的维度为 dₕ=64。FFN 的隐藏层维度为 d_mlp = 128。编码器的深度将是该架构中唯一可变的参数。
-
MLP 头:MLP 头将仅由一个线性层组成。
所有 epoch 的训练和测试批次大小分别固定为 100 和 5000,损失函数使用CrossEntropyLoss(),优化器为Adam。在嵌入层和编码器中,丢弃率参数都设置为零。我使用了NVIDIA L4 Tensor Core GPU,并在Google Colab上进行所有实验,实验过程通过MLFlow的跟踪功能进行记录。
让我们从学习率 lr=0.003 开始训练和测试模型。下面的图 8 总结了四个图表,分别绘制了在 d=4、5、6 和 7 深度下,两个模型的准确率曲线。在这些图表中,符号 ViT_dn(ViTBNFFN_dn)表示 ViT(ViTBNFFN)模型,编码器的深度为 d=n,其他模型配置与上述相同。

图 8. 两个模型在 lr=0.003 时,深度为 4、5、6 和 7 的准确率曲线对比。
对于 d=4 和 d=5(图表的顶部行),这两个模型的准确率相当——对于 d=4(左上方),ViT 稍微做得更好,而对于 d=5(右上方),ViTBNFFN 略微超过了 ViT。在 d < 4 时,准确率保持相当。然而,对于 d=6 和 d=7(图表的底部行),ViTBNFFN 的表现显著优于 ViT。可以验证,这一定性特征对于任何深度 d ≥ 6 都保持一致。
让我们在稍高的学习率 lr=0.005 下重复实验。两个模型在深度 d=1、2、3 和 4 时的准确率曲线总结在图 9 中。

图 9. 两个模型在 lr=0.005 时,深度为 1、2、3 和 4 的准确率曲线对比。
对于 d=1 和 d=2(图表的顶部行),这两个模型的准确率相当——对于 d=1,ViT 稍微做得更好,而对于 d=2,它们几乎没有区别。对于 d=3(左下方),ViTBNFFN 的准确率略高于 ViT。而对于 d=4(右下方),ViTBNFFN 的表现明显优于 ViT,这一质量特征对于任何深度 d ≥ 4 都保持一致。
因此,在合理选择学习率和批次大小的情况下,ViTBNFFN 在变压器编码器的临界深度之后收敛速度显著快于 ViT。在我考虑的超参数范围内,似乎随着学习率的增加,固定批次大小时该临界深度会变得更小。
对于深度神经网络的例子,我们看到高学习率对带有 BatchNorm 的网络的影响显著减轻。那么,Vision Transformer 是否也存在类似的情况?这一问题在图 10 中得到了回答。这里的每一张图绘制了在给定深度下,对于两种不同学习率 lr=0.003 和 lr=0.005 的模型的准确率曲线。第一列图表对应于 d=2、3 和 4 的 ViT(从上到下),而第二列图表则对应于相同深度下的 ViTBNFFN。

图 10. 不同深度下,ViT 和 ViTBNFFN 在两种学习率下的准确率曲线。
考虑 d=2 —— 如上排图所示 —— ViT 和 ViTBNFFN 在增加学习率时受到的影响相当相似。对于 d=3 —— 如第二排图所示 —— 差异则显著。ViT 在较高学习率下的准确率大幅下降——准确率从约 91%下降到约 78%,这是在第 10 个周期结束时的结果。另一方面,对于 ViTBNFFN,第 10 个周期结束时的准确率从约 92%下降到约 90%。这个定性特征在更高深度下仍然保持不变——请参见对应 d=4 的底部图表。因此,对于足够深的 Transformer 编码器,高学习率对 ViTBNFFN 的影响显得明显温和。
结论
在本文中,我研究了在 Vision Transformer 中的 Transformer 编码器内引入 BatchNorm 层的效果。在增强版 MNIST 数据集上比较这些模型时,有两个主要的结论可以得出。首先,对于足够深的 Transformer,并且在合理的超参数选择下,带有 BatchNorm 的模型相比标准 ViT 能显著提高准确率。这种更快的收敛可以大大加速训练。其次,类似于我们对深度神经网络的直觉,如果编码器足够深,带有 BatchNorm 的 Vision Transformer 对于较高的学习率更具韧性。
感谢阅读!如果你已经读到文章的结尾并且喜欢这篇文章,请留下掌声和/或评论,并关注我获取更多内容!除非另有说明,本文中使用的所有图像和图表均由作者生成。
视觉变换器、对比学习、因果推断以及其他你不容错过的深度解析
·发表于 Towards Data Science ·通过 新闻通讯 发送 ·3 分钟阅读·2024 年 8 月 15 日
--
想写你的第一篇 TDS 文章吗?我们总是欢迎新作者的投稿。
随着我们许多人进入夏季的最后阶段,为什么不趁着通常较为平静的几周,抓住机会在通常繁忙的九月来临前,探索数据科学和机器学习的新话题呢?
为了帮助我们读者中所有学习者和技能提升者,本周我们推出了《变量》的特别版,专门介绍我们最近的最佳深度解析文章(以及其他需要比平时更多时间和注意力的文章)。它们的阅读时间可能较长,但在涵盖各自主题时,能够以细腻的视角、周到的方式,并且注重实际应用,做到非常出色。希望你能喜欢我们的精选文章。
-
对比学习实用指南 对比学习适用于学习没有显式标签的基础数据表示,具有众多现实世界的应用场景;孟柳·赵将通过 FashionMNIST 数据集的示例,引导我们完成构建 SimSiam 模型的过程。
-
论文讲解:Vision Transformer (ViT)我们总是乐于深入分析一篇扎实全面的论文,尤其是当它涵盖了像 Vision Transformer 这样具有突破性的概念时。如果你对这一主题不太了解,或者想要扩展现有的 ViT 知识,不要错过Muhammad Ardi的首篇 TDS 文章。
-
通过 BatchNorm 加速 Vision Transformer让我们再多停留在 Vision Transformer 上稍作探讨:如果你已经熟悉它,但希望能更高效、流畅地完成工作流程,Anindya Dey, PhD提供了一份全面指南,讲解如何将批量归一化(Batch Normalization)集成到仅编码器的 Transformer 架构中,从而减少训练和推理时间。
-
通过生成式 AI 提升电子商务——第一部分最近发布的 AI 工具的某些预期效益仍待观察。Mina Ghashami呈现了一个新系列,专注于那些生成式 AI 应用已开始产生实际影响的使用场景,首先探讨了电子商务平台最常见(且对业务至关重要)的任务之一:产品推荐。

由Nellie Adamyan拍摄,图片来源于Unsplash
-
使用 Python 进行因果推断:倾向得分匹配指南将理论与实践结合,Lukasz Szubelak邀请我们深入探索因果推断的方方面面,他耐心的深入分析聚焦于倾向得分匹配(Propensity Score Matching),这是一种在非随机化设置中估计治疗效果的强大技术。
-
ChatGPT 与 Claude 与 Gemini 在数据分析中的对比(第一部分)机器学习从业者在选择使用哪款基于 LLM(大语言模型)驱动的产品时,面临越来越困难的抉择。Yu Dong的这篇新系列旨在通过对三大主流产品(ChatGPT、Claude 和 Gemini)在核心数据分析任务中的表现进行比较,帮助理清这个偶尔显得混乱的生态系统——在这个案例中,重点是编写 SQL 查询。
-
遗漏变量偏差阅读Sachin Date的数学和统计学解释总是我们的一大亮点——而他最新的文章,关于“回归研究中最常见且容易忽视的偏差之一”,也不例外。我们邀请你深入探索他对遗漏变量偏差的深度分析,文中还概述了几种分析和估算其影响的方法。
感谢你支持我们作者的工作!我们喜欢发布新作者的文章,因此如果你最近写了一篇有趣的项目教程、教程或关于我们核心主题的理论思考,别犹豫,与我们分享。
直到下一个变量,
TDS 团队
Vision Transformers 解析
Vision Transformer 解析系列
完整的Vision Transformers in PyTorch演示
·发表于Towards Data Science ·18 分钟阅读·2024 年 2 月 27 日
--
自从 2017 年随着《Attention is All You Need》¹的推出,transformer 模型已成为自然语言处理(NLP)领域的最先进技术。2021 年,An Image is Worth 16x16 Words²成功将 transformer 模型应用于计算机视觉任务。从那时起,许多基于 transformer 的架构已被提出,用于计算机视觉。*
本文详细介绍了《An Image is Worth 16x16 Words*²》中的 Vision Transformer(ViT)。它包括 ViT 的开源代码,以及对各个组件的概念性解释。所有代码均使用 PyTorch Python 包。

图片来自Sahand Babali拍摄,Unsplash
本文是系列文章的一部分,深入探讨了 Vision Transformer 的内部工作原理。这些文章也可以作为 Jupyter Notebook 下载并运行。该系列的其他文章包括:
-
Vision Transformers 解析→ Jupyter Notebook
-
Vision Transformer 解析中的 Attention 部分
-
Vision Transformers 位置嵌入解析
-
Tokens-to-Token 视觉变换器解析
目录
-
什么是视觉变换器?
-
模型演示
— 图像 Token 化
— Token 处理
— 编码模块
— 神经网络模块
— 预测处理
-
完整代码
-
结论
— 进一步阅读
— 引用
什么是视觉变换器?
正如《Attention is All You Need》¹中介绍的,变换器是一种利用注意力机制作为主要学习方式的机器学习模型。变换器迅速成为处理序列到序列任务(如语言翻译)的最先进模型。
《一张图片等于 16x16 个词》²成功地修改了[1]中的变换器,以解决图像分类任务,从而创建了视觉变换器(ViT)。ViT 基于与[1]中的变换器相同的注意力机制。然而,虽然 NLP 任务的变换器包含一个编码器注意力分支和一个解码器注意力分支,ViT 仅使用一个编码器。编码器的输出随后传递给一个神经网络“头部”,进行预测。
[2]中实现的 ViT 的缺点是,其最佳性能需要在大型数据集上进行预训练。最佳的模型是在专有的 JFT-300M 数据集上进行预训练的。在较小的、开源的 ImageNet-21k 数据集上进行预训练的模型,其表现与最先进的卷积 ResNet 模型不相上下。
Tokens-to-Token ViT:从头开始在 ImageNet 上训练视觉变换器³试图通过引入一种新的预处理方法来消除预训练的需求, 将输入图像转换成一系列 Token。有关此方法的更多信息,请参见这里。本文将重点介绍[2]中实现的 ViT。
模型演示
本文遵循了《一张图片等于 16x16 个词》²中概述的模型结构。然而,本文中所用的代码并未公开。更近期的Tokens-to-Token ViT³的代码已在 GitHub 上公开。Tokens-to-Token ViT(T2T-ViT)模型在普通的 ViT 主干网络前添加了一个 Tokens-to-Token(T2T)模块。本文中的代码基于Tokens-to-Token ViT³ GitHub 代码中的 ViT 组件。本文所做的修改包括但不限于,修改以支持非方形输入图像,并移除 dropout 层。
下图展示了 ViT 模型。

ViT 模型图(图片来源:作者)
图像标记化
ViT 的第一步是从输入图像创建标记(tokens)。Transformer 处理的是标记的序列;在自然语言处理(NLP)中,这通常是一个单词的句子。对于计算机视觉而言,如何将输入分割成标记并不那么明确。
ViT 将图像转换为标记,每个标记表示图像的一个局部区域——或称补丁。它们描述了如何将高度为H、宽度为W、通道数为C的图像重塑为N个补丁大小为P的标记:
每个标记的长度为P²∗C。
让我们看看在这幅由 Luis Zuno 创作的像素艺术《黄昏中的山脉》(@ansimuz)⁴上进行的补丁标记化示例。原始艺术作品已被裁剪并转换为单通道图像。这意味着每个像素的值在 0 和 1 之间。单通道图像通常以灰度显示;然而,我们将使用紫色配色方案进行展示,因为这样更容易看清。
请注意,补丁标记化并未包含在与[3]相关的代码中。本节中的所有代码均为作者原创。
mountains = np.load(os.path.join(figure_path, 'mountains.npy'))
H = mountains.shape[0]
W = mountains.shape[1]
print('Mountain at Dusk is H =', H, 'and W =', W, 'pixels.')
print('\n')
fig = plt.figure(figsize=(10,6))
plt.imshow(mountains, cmap='Purples_r')
plt.xticks(np.arange(-0.5, W+1, 10), labels=np.arange(0, W+1, 10))
plt.yticks(np.arange(-0.5, H+1, 10), labels=np.arange(0, H+1, 10))
plt.clim([0,1])
cbar_ax = fig.add_axes([0.95, .11, 0.05, 0.77])
plt.clim([0, 1])
plt.colorbar(cax=cbar_ax);
#plt.savefig(os.path.join(figure_path, 'mountains.png'))
Mountain at Dusk is H = 60 and W = 100 pixels.

代码输出(图片来源:作者)
这幅图像的H=60和W=100。我们将设置P=20,因为它可以均匀地划分H和W。
P = 20
N = int((H*W)/(P**2))
print('There will be', N, 'patches, each', P, 'by', str(P)+'.')
print('\n')
fig = plt.figure(figsize=(10,6))
plt.imshow(mountains, cmap='Purples_r')
plt.hlines(np.arange(P, H, P)-0.5, -0.5, W-0.5, color='w')
plt.vlines(np.arange(P, W, P)-0.5, -0.5, H-0.5, color='w')
plt.xticks(np.arange(-0.5, W+1, 10), labels=np.arange(0, W+1, 10))
plt.yticks(np.arange(-0.5, H+1, 10), labels=np.arange(0, H+1, 10))
x_text = np.tile(np.arange(9.5, W, P), 3)
y_text = np.repeat(np.arange(9.5, H, P), 5)
for i in range(1, N+1):
plt.text(x_text[i-1], y_text[i-1], str(i), color='w', fontsize='xx-large', ha='center')
plt.text(x_text[2], y_text[2], str(3), color='k', fontsize='xx-large', ha='center');
#plt.savefig(os.path.join(figure_path, 'mountain_patches.png'), bbox_inches='tight'
There will be 15 patches, each 20 by 20.

代码输出(图片来源:作者)
通过展开这些补丁,我们可以看到生成的标记。以补丁 12 为例,因为它包含四种不同的阴影。
print('Each patch will make a token of length', str(P**2)+'.')
print('\n')
patch12 = mountains[40:60, 20:40]
token12 = patch12.reshape(1, P**2)
fig = plt.figure(figsize=(10,1))
plt.imshow(token12, aspect=10, cmap='Purples_r')
plt.clim([0,1])
plt.xticks(np.arange(-0.5, 401, 50), labels=np.arange(0, 401, 50))
plt.yticks([]);
#plt.savefig(os.path.join(figure_path, 'mountain_token12.png'), bbox_inches='tight')
Each patch will make a token of length 400.

代码输出(图片来源:作者)
从图像中提取标记后,通常会使用线性投影来改变标记的长度。这通常是通过一个可学习的线性层来实现的。标记的新长度被称为潜在维度²、通道维度³或标记长度。经过投影后,标记不再能被直观地识别为原始图像中的补丁。
现在我们理解了这个概念,可以看看如何在代码中实现补丁标记化(patch tokenization)。
class Patch_Tokenization(nn.Module):
def __init__(self,
img_size: tuple[int, int, int]=(1, 1, 60, 100),
patch_size: int=50,
token_len: int=768):
""" Patch Tokenization Module
Args:
img_size (tuple[int, int, int]): size of input (channels, height, width)
patch_size (int): the side length of a square patch
token_len (int): desired length of an output token
"""
super().__init__()
## Defining Parameters
self.img_size = img_size
C, H, W = self.img_size
self.patch_size = patch_size
self.token_len = token_len
assert H % self.patch_size == 0, 'Height of image must be evenly divisible by patch size.'
assert W % self.patch_size == 0, 'Width of image must be evenly divisible by patch size.'
self.num_tokens = (H / self.patch_size) * (W / self.patch_size)
## Defining Layers
self.split = nn.Unfold(kernel_size=self.patch_size, stride=self.patch_size, padding=0)
self.project = nn.Linear((self.patch_size**2)*C, token_len)
def forward(self, x):
x = self.split(x).transpose(1,0)
x = self.project(x)
return x
请注意,两个assert语句确保图像尺寸能被补丁大小整除。实际的补丁拆分是通过torch.nn.Unfold⁵层来实现的。
我们将使用裁剪后的单通道《黄昏中的山脉》⁴版本运行此代码示例。我们应该能看到与之前相同的标记数和初始标记大小。我们将使用token_len=768作为投影后的长度,这是 ViT²基础版本的大小。
以下代码块的第一行将《黄昏中的山脉》⁴的数据类型从 NumPy 数组更改为 Torch 张量。我们还需要对张量进行unsqueeze⁶操作,以创建通道维度和批次大小维度。如上所述,我们有一个通道。由于只有一张图片,batchsize=1。
x = torch.from_numpy(mountains).unsqueeze(0).unsqueeze(0).to(torch.float32)
token_len = 768
print('Input dimensions are\n\tbatchsize:', x.shape[0], '\n\tnumber of input channels:', x.shape[1], '\n\timage size:', (x.shape[2], x.shape[3]))
# Define the Module
patch_tokens = Patch_Tokenization(img_size=(x.shape[1], x.shape[2], x.shape[3]),
patch_size = P,
token_len = token_len)
Input dimensions are
batchsize: 1
number of input channels: 1
image size: (60, 100)
现在,我们将把图像拆分成标记(tokens)。
x = patch_tokens.split(x).transpose(2,1)
print('After patch tokenization, dimensions are\n\tbatchsize:', x.shape[0], '\n\tnumber of tokens:', x.shape[1], '\n\ttoken length:', x.shape[2])
After patch tokenization, dimensions are
batchsize: 1
number of tokens: 15
token length: 400
正如我们在示例中看到的,每个长度为 400 的 N=15 个 token。最后,我们将 token 投影为 token_len。
x = patch_tokens.project(x)
print('After projection, dimensions are\n\tbatchsize:', x.shape[0], '\n\tnumber of tokens:', x.shape[1], '\n\ttoken length:', x.shape[2])
After projection, dimensions are
batchsize: 1
number of tokens: 15
token length: 768
现在我们已经有了 token,准备继续处理 ViT。
Token 处理
我们将 ViT 的接下来的两个步骤,在编码块之前,称为“token 处理”。ViT 图中的 token 处理组件如下所示。

ViT 图中的 Token 处理组件(图片由作者提供)
第一步是向图像 token 前添加一个空白 token,称为 预测 Token。这个 token 将在编码块的输出中用于进行预测。它最初是空白的——相当于零——这样它可以从其他图像 token 中获取信息。
我们将从 175 个 token 开始。每个 token 的长度为 768,这是 ViT² 基本变种的大小。我们将使用批量大小为 13,因为它是质数,不会与其他参数混淆。
# Define an Input
num_tokens = 175
token_len = 768
batch = 13
x = torch.rand(batch, num_tokens, token_len)
print('Input dimensions are\n\tbatchsize:', x.shape[0], '\n\tnumber of tokens:', x.shape[1], '\n\ttoken length:', x.shape[2])
# Append a Prediction Token
pred_token = torch.zeros(1, 1, token_len).expand(batch, -1, -1)
print('Prediction Token dimensions are\n\tbatchsize:', pred_token.shape[0], '\n\tnumber of tokens:', pred_token.shape[1], '\n\ttoken length:', pred_token.shape[2])
x = torch.cat((pred_token, x), dim=1)
print('Dimensions with Prediction Token are\n\tbatchsize:', x.shape[0], '\n\tnumber of tokens:', x.shape[1], '\n\ttoken length:', x.shape[2])
Input dimensions are
batchsize: 13
number of tokens: 175
token length: 768
Prediction Token dimensions are
batchsize: 13
number of tokens: 1
token length: 768
Dimensions with Prediction Token are
batchsize: 13
number of tokens: 176
token length: 768
现在,我们为我们的 token 添加一个位置嵌入。位置嵌入使 Transformer 能够理解图像 token 的顺序。请注意,这是一种加法操作,而不是拼接。位置嵌入的具体细节是一个偏题,最好留待另一个时间讨论。
def get_sinusoid_encoding(num_tokens, token_len):
""" Make Sinusoid Encoding Table
Args:
num_tokens (int): number of tokens
token_len (int): length of a token
Returns:
(torch.FloatTensor) sinusoidal position encoding table
"""
def get_position_angle_vec(i):
return [i / np.power(10000, 2 * (j // 2) / token_len) for j in range(token_len)]
sinusoid_table = np.array([get_position_angle_vec(i) for i in range(num_tokens)])
sinusoid_table[:, 0::2] = np.sin(sinusoid_table[:, 0::2])
sinusoid_table[:, 1::2] = np.cos(sinusoid_table[:, 1::2])
return torch.FloatTensor(sinusoid_table).unsqueeze(0)
PE = get_sinusoid_encoding(num_tokens+1, token_len)
print('Position embedding dimensions are\n\tnumber of tokens:', PE.shape[1], '\n\ttoken length:', PE.shape[2])
x = x + PE
print('Dimensions with Position Embedding are\n\tbatchsize:', x.shape[0], '\n\tnumber of tokens:', x.shape[1], '\n\ttoken length:', x.shape[2])
Position embedding dimensions are
number of tokens: 176
token length: 768
Dimensions with Position Embedding are
batchsize: 13
number of tokens: 176
token length: 768
现在,我们的 token 准备好进入编码块了。
编码块
编码块是模型实际从图像 token 中学习的地方。编码块的数量是由用户设置的超参数。编码块的图示如下。

编码块(图片由作者提供)
编码块的代码如下。
class Encoding(nn.Module):
def __init__(self,
dim: int,
num_heads: int=1,
hidden_chan_mul: float=4.,
qkv_bias: bool=False,
qk_scale: NoneFloat=None,
act_layer=nn.GELU,
norm_layer=nn.LayerNorm):
""" Encoding Block
Args:
dim (int): size of a single token
num_heads(int): number of attention heads in MSA
hidden_chan_mul (float): multiplier to determine the number of hidden channels (features) in the NeuralNet component
qkv_bias (bool): determines if the qkv layer learns an addative bias
qk_scale (NoneFloat): value to scale the queries and keys by;
if None, queries and keys are scaled by ``head_dim ** -0.5``
act_layer(nn.modules.activation): torch neural network layer class to use as activation
norm_layer(nn.modules.normalization): torch neural network layer class to use as normalization
"""
super().__init__()
## Define Layers
self.norm1 = norm_layer(dim)
self.attn = Attention(dim=dim,
chan=dim,
num_heads=num_heads,
qkv_bias=qkv_bias,
qk_scale=qk_scale)
self.norm2 = norm_layer(dim)
self.neuralnet = NeuralNet(in_chan=dim,
hidden_chan=int(dim*hidden_chan_mul),
out_chan=dim,
act_layer=act_layer)
def forward(self, x):
x = x + self.attn(self.norm1(x))
x = x + self.neuralnet(self.norm2(x))
return x
num_heads, qkv_bias, 和 qk_scale 参数定义了 注意力 模块的组件。关于视觉 Transformer 中的注意力机制的深入讨论留待另一个时间。
hidden_chan_mul 和 act_layer 参数定义了 神经网络 模块的组件。激活层可以是任何 torch.nn.modules.activation⁷ 层。我们稍后会更详细地介绍 神经网络 模块更多内容。
norm_layer 可以从任何 torch.nn.modules.normalization⁸ 层中选择。
现在,我们将逐步解析图中的每个蓝色块及其伴随的代码。我们将使用 176 个长度为 768 的 token。我们将使用批量大小为 13,因为它是质数,不会与其他参数混淆。我们将使用 4 个注意力头,因为它能均匀分配 token 长度;然而,在编码块中,你不会看到注意力头维度。
# Define an Input
num_tokens = 176
token_len = 768
batch = 13
heads = 4
x = torch.rand(batch, num_tokens, token_len)
print('Input dimensions are\n\tbatchsize:', x.shape[0], '\n\tnumber of tokens:', x.shape[1], '\n\ttoken length:', x.shape[2])
# Define the Module
E = Encoding(dim=token_len, num_heads=heads, hidden_chan_mul=1.5, qkv_bias=False, qk_scale=None, act_layer=nn.GELU, norm_layer=nn.LayerNorm)
E.eval();
Input dimensions are
batchsize: 13
number of tokens: 176
token length: 768
现在,我们将通过一个归一化层和一个 注意力 模块。编码块中的 注意力 模块进行了参数化,以确保不会改变 token 长度。在 注意力 模块之后,我们实现第一个分割连接。
y = E.norm1(x)
print('After norm, dimensions are\n\tbatchsize:', y.shape[0], '\n\tnumber of tokens:', y.shape[1], '\n\ttoken size:', y.shape[2])
y = E.attn(y)
print('After attention, dimensions are\n\tbatchsize:', y.shape[0], '\n\tnumber of tokens:', y.shape[1], '\n\ttoken size:', y.shape[2])
y = y + x
print('After split connection, dimensions are\n\tbatchsize:', y.shape[0], '\n\tnumber of tokens:', y.shape[1], '\n\ttoken size:', y.shape[2])
After norm, dimensions are
batchsize: 13
number of tokens: 176
token size: 768
After attention, dimensions are
batchsize: 13
number of tokens: 176
token size: 768
After split connection, dimensions are
batchsize: 13
number of tokens: 176
token size: 768
接下来,我们通过另一个归一化层,然后是 神经网络 模块。最后,我们完成第二个分割连接。
z = E.norm2(y)
print('After norm, dimensions are\n\tbatchsize:', z.shape[0], '\n\tnumber of tokens:', z.shape[1], '\n\ttoken size:', z.shape[2])
z = E.neuralnet(z)
print('After neural net, dimensions are\n\tbatchsize:', z.shape[0], '\n\tnumber of tokens:', z.shape[1], '\n\ttoken size:', z.shape[2])
z = z + y
print('After split connection, dimensions are\n\tbatchsize:', z.shape[0], '\n\tnumber of tokens:', z.shape[1], '\n\ttoken size:', z.shape[2])
After norm, dimensions are
batchsize: 13
number of tokens: 176
token size: 768
After neural net, dimensions are
batchsize: 13
number of tokens: 176
token size: 768
After split connection, dimensions are
batchsize: 13
number of tokens: 176
token size: 768
这就是单个编码块的全部内容!由于最终的维度与初始维度相同,模型可以轻松地通过多个编码块传递标记,这由深度超参数设置。
神经网络模块
神经网络(NN)模块是编码块的一个子组件。NN 模块非常简单,由一个全连接层、一个激活层和另一个全连接层组成。激活层可以是任何torch.nn.modules.activation⁷层,并作为输入传递给模块。NN 模块可以配置为改变输入的形状,或保持相同形状。我们不会逐步讲解这段代码,因为神经网络在机器学习中很常见,并不是本文的重点。然而,NN 模块的代码如下所示。
class NeuralNet(nn.Module):
def __init__(self,
in_chan: int,
hidden_chan: NoneFloat=None,
out_chan: NoneFloat=None,
act_layer = nn.GELU):
""" Neural Network Module
Args:
in_chan (int): number of channels (features) at input
hidden_chan (NoneFloat): number of channels (features) in the hidden layer;
if None, number of channels in hidden layer is the same as the number of input channels
out_chan (NoneFloat): number of channels (features) at output;
if None, number of output channels is same as the number of input channels
act_layer(nn.modules.activation): torch neural network layer class to use as activation
"""
super().__init__()
## Define Number of Channels
hidden_chan = hidden_chan or in_chan
out_chan = out_chan or in_chan
## Define Layers
self.fc1 = nn.Linear(in_chan, hidden_chan)
self.act = act_layer()
self.fc2 = nn.Linear(hidden_chan, out_chan)
def forward(self, x):
x = self.fc1(x)
x = self.act(x)
x = self.fc2(x)
return x
预测处理
通过编码块后,模型必须做的最后一件事就是进行预测。ViT 图示中的“预测处理”组件如下所示。

ViT 图示中的预测处理组件(图片由作者提供)
我们将逐步分析这个过程的每个步骤。我们将继续使用 176 个长度为 768 的标记。为了演示如何进行单次预测,我们将使用批量大小为 1。批量大小大于 1 会并行计算此预测。
# Define an Input
num_tokens = 176
token_len = 768
batch = 1
x = torch.rand(batch, num_tokens, token_len)
print('Input dimensions are\n\tbatchsize:', x.shape[0], '\n\tnumber of tokens:', x.shape[1], '\n\ttoken length:', x.shape[2])
Input dimensions are
batchsize: 1
number of tokens: 176
token length: 768
首先,所有标记都会通过一个标准化层。
norm = nn.LayerNorm(token_len)
x = norm(x)
print('After norm, dimensions are\n\tbatchsize:', x.shape[0], '\n\tnumber of tokens:', x.shape[1], '\n\ttoken size:', x.shape[2])
After norm, dimensions are
batchsize: 1
number of tokens: 1001
token size: 768
接下来,我们将预测标记从其余标记中分离出来。在整个编码块中,预测标记变为非零,并且获得了关于输入图像的信息。我们将仅使用这个预测标记来进行最终的预测。
pred_token = x[:, 0]
print('Length of prediction token:', pred_token.shape[-1])
Length of prediction token: 768
最后,预测标记通过头部进行预测。头部,通常是某种类型的神经网络,根据模型的不同而有所变化。在《An Image is Worth 16x16 Words²》中,他们在预训练时使用一个带有隐藏层的 MLP(多层感知器),在微调时使用一个线性层。在《Tokens-to-Token ViT³》中,他们使用一个线性层作为头部。这个例子将使用单个线性层。
请注意,头部的输出形状是根据学习问题的参数设置的。对于分类,通常是一个长度为类别数的向量,采用独热编码。对于回归,它将是任何整数数量的预测参数。这个例子将使用一个输出形状为 1 来表示一个单一的回归估计值。
head = nn.Linear(token_len, 1)
pred = head(pred_token)
print('Length of prediction:', (pred.shape[0], pred.shape[1]))
print('Prediction:', float(pred))
Length of prediction: (1, 1)
Prediction: -0.5474240779876709
就这样!模型已经做出了预测!
完整代码
为了创建完整的 ViT 模块,我们使用上面定义的补丁标记化模块和ViT 主干模块。ViT 主干定义如下,并包含标记处理、编码块和预测处理组件。
class ViT_Backbone(nn.Module):
def __init__(self,
preds: int=1,
token_len: int=768,
num_heads: int=1,
Encoding_hidden_chan_mul: float=4.,
depth: int=12,
qkv_bias=False,
qk_scale=None,
act_layer=nn.GELU,
norm_layer=nn.LayerNorm):
""" VisTransformer Backbone
Args:
preds (int): number of predictions to output
token_len (int): length of a token
num_heads(int): number of attention heads in MSA
Encoding_hidden_chan_mul (float): multiplier to determine the number of hidden channels (features) in the NeuralNet component of the Encoding Module
depth (int): number of encoding blocks in the model
qkv_bias (bool): determines if the qkv layer learns an addative bias
qk_scale (NoneFloat): value to scale the queries and keys by;
if None, queries and keys are scaled by ``head_dim ** -0.5``
act_layer(nn.modules.activation): torch neural network layer class to use as activation
norm_layer(nn.modules.normalization): torch neural network layer class to use as normalization
"""
super().__init__()
## Defining Parameters
self.num_heads = num_heads
self.Encoding_hidden_chan_mul = Encoding_hidden_chan_mul
self.depth = depth
## Defining Token Processing Components
self.cls_token = nn.Parameter(torch.zeros(1, 1, self.token_len))
self.pos_embed = nn.Parameter(data=get_sinusoid_encoding(num_tokens=self.num_tokens+1, token_len=self.token_len), requires_grad=False)
## Defining Encoding blocks
self.blocks = nn.ModuleList([Encoding(dim = self.token_len,
num_heads = self.num_heads,
hidden_chan_mul = self.Encoding_hidden_chan_mul,
qkv_bias = qkv_bias,
qk_scale = qk_scale,
act_layer = act_layer,
norm_layer = norm_layer)
for i in range(self.depth)])
## Defining Prediction Processing
self.norm = norm_layer(self.token_len)
self.head = nn.Linear(self.token_len, preds)
## Make the class token sampled from a truncated normal distrobution
timm.layers.trunc_normal_(self.cls_token, std=.02)
def forward(self, x):
## Assumes x is already tokenized
## Get Batch Size
B = x.shape[0]
## Concatenate Class Token
x = torch.cat((self.cls_token.expand(B, -1, -1), x), dim=1)
## Add Positional Embedding
x = x + self.pos_embed
## Run Through Encoding Blocks
for blk in self.blocks:
x = blk(x)
## Take Norm
x = self.norm(x)
## Make Prediction on Class Token
x = self.head(x[:, 0])
return x
从ViT Backbone模块,我们可以定义完整的 ViT 模型。
class ViT_Model(nn.Module):
def __init__(self,
img_size: tuple[int, int, int]=(1, 400, 100),
patch_size: int=50,
token_len: int=768,
preds: int=1,
num_heads: int=1,
Encoding_hidden_chan_mul: float=4.,
depth: int=12,
qkv_bias=False,
qk_scale=None,
act_layer=nn.GELU,
norm_layer=nn.LayerNorm):
""" VisTransformer Model
Args:
img_size (tuple[int, int, int]): size of input (channels, height, width)
patch_size (int): the side length of a square patch
token_len (int): desired length of an output token
preds (int): number of predictions to output
num_heads(int): number of attention heads in MSA
Encoding_hidden_chan_mul (float): multiplier to determine the number of hidden channels (features) in the NeuralNet component of the Encoding Module
depth (int): number of encoding blocks in the model
qkv_bias (bool): determines if the qkv layer learns an addative bias
qk_scale (NoneFloat): value to scale the queries and keys by;
if None, queries and keys are scaled by ``head_dim ** -0.5``
act_layer(nn.modules.activation): torch neural network layer class to use as activation
norm_layer(nn.modules.normalization): torch neural network layer class to use as normalization
"""
super().__init__()
## Defining Parameters
self.img_size = img_size
C, H, W = self.img_size
self.patch_size = patch_size
self.token_len = token_len
self.num_heads = num_heads
self.Encoding_hidden_chan_mul = Encoding_hidden_chan_mul
self.depth = depth
## Defining Patch Embedding Module
self.patch_tokens = Patch_Tokenization(img_size,
patch_size,
token_len)
## Defining ViT Backbone
self.backbone = ViT_Backbone(preds,
self.token_len,
self.num_heads,
self.Encoding_hidden_chan_mul,
self.depth,
qkv_bias,
qk_scale,
act_layer,
norm_layer)
## Initialize the Weights
self.apply(self._init_weights)
def _init_weights(self, m):
""" Initialize the weights of the linear layers & the layernorms
"""
## For Linear Layers
if isinstance(m, nn.Linear):
## Weights are initialized from a truncated normal distrobution
timm.layers.trunc_normal_(m.weight, std=.02)
if isinstance(m, nn.Linear) and m.bias is not None:
## If bias is present, bias is initialized at zero
nn.init.constant_(m.bias, 0)
## For Layernorm Layers
elif isinstance(m, nn.LayerNorm):
## Weights are initialized at one
nn.init.constant_(m.weight, 1.0)
## Bias is initialized at zero
nn.init.constant_(m.bias, 0)
@torch.jit.ignore ##Tell pytorch to not compile as TorchScript
def no_weight_decay(self):
""" Used in Optimizer to ignore weight decay in the class token
"""
return {'cls_token'}
def forward(self, x):
x = self.patch_tokens(x)
x = self.backbone(x)
return x
在ViT 模型中,img_size、patch_size和token_len定义了Patch Tokenization模块。
num_heads、Encoding_hidden_channel_mul、qkv_bias、qk_scale和act_layer参数定义了Encoding Block模块。act_layer可以是任何torch.nn.modules.activation⁷层。depth参数决定了模型中编码块的数量。
norm_layer参数设置了Encoding Block模块内外的规范化层。可以从任何torch.nn.modules.normalization⁸层中选择。
_init_weights方法来源于 T2T-ViT³代码。此方法可以删除,以随机初始化所有学习的权重和偏差。按实现方式,线性层的权重初始化为截断的正态分布;线性层的偏差初始化为零;归一化层的权重初始化为一;归一化层的偏差初始化为零。
结论
现在,您可以带着对 ViT 模型机制的深刻理解,去训练 ViT 模型了!下面是可以下载 ViT 模型代码的地方列表,其中一些比其他的允许更多的模型修改。祝您转换愉快!
-
GitHub 仓库用于本系列文章
-
GitHub 仓库用于An Image is Worth 16x16 Words²
→ 包含预训练模型和微调代码;不包含模型定义
-
ViT 在PyTorch Image Models(
timm)中的实现⁹timm.create_model('vit_base_patch16_224', pretrained=True) -
Phil Wang 的
vit-pytorch包
本文已由洛斯阿拉莫斯国家实验室批准发布,批准编号为 LA-UR-23–33876。相关代码已获得 BSD-3 开源许可证,批准编号为 O#4693。
进一步阅读
要了解更多关于 NLP 上下文中 transformer 的信息,请参见
-
Transformers 视觉化解析第一部分:功能概述:
towardsdatascience.com/transformers-explained-visually-part-1-overview-of-functionality-95a6dd460452 -
Transformers 视觉化解析第二部分:如何一步步工作:
towardsdatascience.com/transformers-explained-visually-part-2-how-it-works-step-by-step-b49fa4a64f34
有关 Vision Transformers 的广泛视频讲座,请参见
- Vision Transformer 及其应用:
youtu.be/hPb6A92LROc?si=GaGYiZoyDg0PcdSP
引用
[1] Vaswani 等(2017)。Attention Is All You Need. doi.org/10.48550/arXiv.1706.03762
[2] Dosovitskiy 等人(2020)。一张图片值 16x16 个词:用于大规模图像识别的 Transformer。 doi.org/10.48550/arXiv.2010.11929
[3] Yuan 等人(2021)。Tokens-to-Token ViT:从零开始在 ImageNet 上训练视觉 Transformer。 doi.org/10.48550/arXiv.2101.11986
→ GitHub 代码: github.com/yitu-opensource/T2T-ViT
[4] Luis Zuno (@ansimuz). 黄昏山脉背景图。 许可 CC0: opengameart.org/content/mountain-at-dusk-background
[5] PyTorch. 展开操作(Unfold)。 pytorch.org/docs/stable/generated/torch.nn.Unfold.html#torch.nn.Unfold
[6] PyTorch. 扩展维度(Unsqueeze)。 pytorch.org/docs/stable/generated/torch.unsqueeze.html#torch.unsqueeze
[7] PyTorch. 非线性激活(加权和,非线性)。 pytorch.org/docs/stable/nn.html#non-linear-activations-weighted-sum-nonlinearity
[8] PyTorch. 归一化层(Normalization Layers)。 pytorch.org/docs/stable/nn.html#normalization-layers
[9] Ross Wightman. PyTorch 图像模型。 github.com/huggingface/pytorch-image-models
VisionTS:从图像构建卓越的预测模型
利用图像的力量进行时间序列预测
·发表于 Towards Data Science ·阅读时长 8 分钟·2024 年 9 月 26 日
--

由作者使用 DALLE*3 创作
在构建预训练时间序列模型时,最大挑战是什么?
答案: 寻找高质量、多样化的时间序列数据。我们在之前的文章中已经讨论过这个问题。
构建基础预测模型有两种主要方法:
-
“Bootstrap”一个 LLM:通过应用针对时间序列任务定制的微调或分词策略,重新利用像GPT-4或Llama这样的预训练大型语言模型。
-
“从零开始”: 构建一个大规模的时间序列数据集,并从零开始预训练一个模型,期望它能够推广到新数据。
虽然第一种方法有效,因为 Transformers 是通用计算引擎,但它并没有产生最佳结果。第二种方法更为成功,正如这里所展示的:MOIRAI、TimesFM、TTM 等。
然而,这些模型似乎遵循了扩展规律,它们的表现依赖于找到大量的时间序列数据——这又让我们回到了最初的挑战。
但是,如果我们能够利用一种不同的模态,比如图像呢?这可能看起来违反直觉,但一些研究人员探索了这个假设并取得了开创性的成果。在…
视觉指南:理解大型语言模型的基础
一系列直观地分解复杂 AI 概念的工具和文章
· 发表在 Towards Data Science ·阅读时间:7 分钟·2024 年 1 月 6 日
--

图片由作者使用 unDraw.co 提供的免费插图制作
这是一篇动态文档,将不断更新。
最后更新:2024 年 8 月 10 日,新增 Transformer 解释器
今天,世界充满了关于 LLM(大型语言模型)的讨论。几乎每天都有新的语言模型发布,令人工智能领域的从业者感到错过了什么。然而,许多人仍然难以理解 LLM 的基本概念,这使得他们难以跟上技术的进步。本文旨在帮助那些希望深入了解这些 AI 模型内部运作的人,帮助他们牢牢把握该主题。为此,我将介绍一些工具和文章,帮助加深对 LLM 概念的理解,并使其更加易于掌握。
目录
· 1. Jay Alammar 的插图版 Transformer
· 2. Jay Alammar 的插图版 GPT-2
· 3. Transformer 解释器:文本生成模型的互动学习
· 4. Brendan Bycroft 的 LLM 可视化
· 5. 生成式 AI 存在是因为 Transformer — 《金融时报》
· 6. OpenAI 的分词器工具
· 7. Simon 的 GPT 分词器解析…
可视化 101:选择最佳的可视化类型
不同可视化应用场景的综合指南
·发布于 Towards Data Science ·16 分钟阅读·2024 年 1 月 12 日
--

图片来自 DALL-E 3
我认为分析师的主要目标是帮助产品团队基于数据做出正确的决策。这意味着,分析师工作的主要结果不仅仅是得到一些数字或仪表盘,而是影响合理的数据驱动决策。因此,展示我们研究的结果是分析师日常工作中至关重要的一部分。
你是否曾经经历过,直到你创建了一个图表,才注意到某些显而易见的异常?你不是唯一的。几乎没有人能从枯燥的数字表格中提取出洞察力。这就是为什么我们需要可视化来揭示数据中的洞察。作为数据和产品团队之间的桥梁,数据分析师需要在可视化方面表现出色。
这就是为什么我想讨论数据可视化,并从框架开始,帮助你为你的应用场景选择最合适的图表类型。
为什么我们需要可视化?
仅通过总结性统计数据来查看数据可能会很有诱惑力。你可以通过均值和方差比较数据集,而根本不查看数据本身。然而,这可能会导致对数据的误解和错误的决策。
其中一个最著名的例子是安斯科姆四重奏。它由统计学家弗朗西斯·安斯科姆创建,包含四个数据集,这些数据集的描述性统计几乎相同:均值、方差和相关性。然而,当我们查看数据时,我们可以看到这些数据集是多么不同。

作者提供的可视化
你可以在这里找到更多令人震惊的例子(甚至是恐龙),它们使用相同的描述性统计数据。
这个例子清晰地展示了异常值如何扭曲你的汇总统计数据,以及为什么我们需要对数据进行可视化。
除了异常值,视觉化还是呈现研究结果的更好方式。图表更易理解,并且能整合大量数据。因此,分析师应特别关注这一领域。
背景是起点
当我们开始为任务考虑可视化时,我们需要明确其主要目标或可视化的背景。
创建图表有两种重要的应用场景:探索性分析和解释性分析。
探索性可视化是你与数据的“私人对话”,用于发现洞察并理解其内部结构。对于这样的可视化,你可能不太关注设计和细节,例如省略标题或不在图表间使用一致的颜色方案,因为这些可视化只是供你个人查看。
我通常会先做一些快速的图表原型。然而,即便在这种情况下,你仍然需要考虑最合适的图表类型。恰当的可视化可以帮助你发现洞察,而错误的可视化可能隐藏线索。所以,做出明智的选择。
解释性可视化旨在向观众传达信息。在这种情况下,你需要更加关注细节和背景,以实现你的目标。
当我在进行解释性可视化时,我通常会思考以下问题,以明确我的目标:
-
我的观众是谁?他们有什么背景?我需要向他们解释什么信息?他们感兴趣的是什么?
-
我想达成什么目标?我的观众可能有哪些顾虑?我可以展示什么信息来实现我的目标?
-
我是否展示了全貌?我是否需要从另一个角度来看这个问题,以便为观众提供所有信息,帮助他们做出明智的决定?
此外,你对可视化的决定可能会取决于传播媒介,不管你是做现场演示,还是仅仅通过 Slack 或电子邮件发送。以下是一些例子:
-
在现场演示的情况下,你可以对图表做较少的注释,因为你可以谈论所有必要的背景信息,而在电子邮件中,最好提供所有的细节。
-
一个包含大量数字的表格不适合现场展示,因为信息过多的幻灯片可能会分散观众对演讲的注意力。而对于书面沟通来说,这是完全可以接受的,因为观众可以按自己的节奏浏览所有数字。
所以,在选择图表类型时,我们不应该孤立地考虑可视化。我们需要考虑我们的主要目标和观众,请牢记这一点。
可视化的感知
你知道多少种不同类型的图表?我敢打赌你能说出不少:线性图、条形图、桑基图、热力图、箱线图、气泡图等。但你有没有更细致地思考过可视化:它们的构建模块是什么?你的读者是如何感知这些图表的?
William S. Cleveland 和 Robert McGill 在他们的文章中调查了这个问题,《Graphical Perception: Theory, Experimentation, and Application to the Development of Graphical Methods》,发表于 《美国统计学会杂志》,1984 年 9 月。这篇文章聚焦于视觉感知——解码图表中呈现信息的能力。作者们提出了一组可视化构建模块——视觉编码——例如,位置、长度、面积或颜色饱和度。不出所料,不同的视觉编码对人们的解读难度不同。
作者们尝试通过实验假设并测试人们从图表中提取信息的准确性,测试不同元素使用下,人们判断的有效性。他们的目标是验证人们判断的准确度。
他们通过以往的心理学研究和实验,对不同的可视化构建模块进行了排名,从最准确到最不准确。以下是这个列表:
-
位置——例如,散点图;
-
长度——例如,条形图;
-
方向或斜率——例如,线图;
-
角度——例如,饼图;
-
面积——例如,气泡图;
-
体积——3D 图表;
-
色相和饱和度——例如,热力图。
我只突出了文章中最常见的元素,适用于日常的分析任务。
如我们之前所讨论的,视觉化的主要目标是传达信息,我们需要关注我们的受众以及他们如何感知这个信息。因此,我们关注的是人们是否能够正确理解。这就是为什么我通常会尽量选择列表顶部的视觉编码,因为它们更容易为人们所解读。
可视化工具
我们将在下面看到许多图表示例,所以我们先快速讨论一下我用来制作图表的工具。
可视化有很多选择:
-
Excel 或 Google Sheet,
-
像 Tableau 或 Superset 这样的 BI 工具,
-
Python 或 R 中的库。
在大多数情况下,我更喜欢使用 Python 的 Plotly 库,因为它可以轻松创建外观精美的交互式图表。在少数情况下,我使用 Matplotlib 或 Seaborn。例如,我更喜欢在制作直方图时使用 Matplotlib(如你所见),因为默认情况下,它能提供我所需要的效果,而 Plotly 则不能做到这一点。
现在,让我们跳到实践部分,讨论使用案例,以及如何选择最合适的可视化方式来解决这些问题。
该使用哪种图表类型?
你可能经常会困惑,应该选择哪种图表来处理你的使用案例,因为有太多种类型。
有一些有价值的工具,比如在《数据讲故事》博客中描述的非常实用的图表选择器。它可以帮助你获得一些起步的灵感。
斯蒂芬·费尤提出了我认为非常有帮助的另一种方法。他写了一篇文章,《Eenie, Meenie, Minie, Moe: 选择合适的图表来传达你的信息》。在这篇文章中,他列出了数据可视化的七种常见使用场景,并提出了应对这些场景的可视化类型。
以下是这些使用场景的列表:
-
时间序列
-
名义比较
-
偏差
-
排名
-
部分与整体
-
频率分布
-
关联性
我们将逐一讨论这些情况,并举一些每种情况的可视化示例。我并不完全同意作者关于可视化类型的建议,我会分享我对此的看法。
以下图表示例基于合成数据,除非明确提到。
时间序列
什么是使用场景? 它是可视化中最常见的使用场景。我们经常希望观察一个或多个指标随时间变化的情况。
图表建议
最直接的选择(尤其是当你有多个指标时)是使用折线图。它突出了趋势,并为观众提供了数据的全面概览。
例如,我使用了折线图来展示每个平台的会话数量随时间变化的情况。我们可以看到,iOS 是增长最快的部分,而其他平台的增长则相对停滞不前。

按作者分类的可视化
使用折线图(而不是散点图)至关重要,因为折线图通过斜率强调趋势。
使用 Plotly,你可以轻松创建这样的图表。我们有一个包含每月会话数量的数据集。

然后,我们可以使用Plotly Express来创建折线图,传入数据、标题,并覆盖标签。
import plotly.express as px
px.line(
ts_df,
title = '<b>Sessions by platforms</b>',
labels = {'value': 'sessions', 'os': 'platform', 'month_date': 'month'},
color_discrete_map={
'Android': px.colors.qualitative.Vivid[1],
'Windows': px.colors.qualitative.Vivid[2],
'iOS': px.colors.qualitative.Vivid[4]
}
)
这里我们不会详细讨论设计以及如何在 Plotly 中调整它,因为这是一个相当庞大的话题,值得单独撰写一篇文章。
我们通常将时间放在折线图的 x 轴上,并在数据点之间使用相等的时间间隔。
常见的误解之一是我们必须让 y 轴从零开始(必须包含 0)。然而,对于折线图而言,这并不适用。在某些情况下,这样做甚至可能会妨碍你从数据中获得洞察。
例如,比较下面的两张图表。在第一张图表中,会话数量看起来相当稳定,而在第二张图表中,12 月中旬的下降非常明显。然而,这完全是同一个数据集,唯一不同的是 y 轴的范围。

按作者分类的可视化

按作者分类的可视化
对于时间序列数据,你的选择不仅限于折线图。有时,柱状图可能是更好的选择,例如,当你有较少的数据点,并且希望强调单个值而不是趋势时。

作者提供的可视化
在 Plotly 中创建柱状图也非常简单。
fig = px.bar(
df,
title = '<b>Sessions</b>',
labels = {'value': 'sessions', 'os': 'platform', 'month_date': 'month'},
text_auto = ',.6r' # specifying format for bar labels
)
fig.update_layout(xaxis_type='category')
# to prevent converting string to dates
fig.update_layout(showlegend = False)
# hiding ledend since we don't need it
名义比较
什么是用例? 这是你想要在不同细分市场之间比较一个或多个指标的情况。
图表建议
如果你只有几个数据点,你可以仅使用文本中的数字,而不是图表。我喜欢这种方法,因为它简洁而不杂乱。

作者提供的可视化
在许多情况下,柱状图将是比较指标的便捷工具。尽管垂直柱状图通常更常见,但当细分市场的名称较长时,水平柱状图将是更好的选择。
例如,我们可以比较不同地区每个客户的年GMVs(总商品价值)。

作者提供的可视化
要将柱状图改为水平的,你只需要设置orientation = "h"。
fig = px.bar(df,
text_auto = ',.6r',
title = '<b>Average annual GMV</b> (Gross Merchandise Value)',
labels = {'country': 'region', 'value': 'average GMV in GBP'},
orientation = 'h'
)
fig.update_layout(showlegend = False)
fig.update_xaxes(visible = False) # to hide x-axes
重要提示:始终为柱状图使用零为基准的坐标轴。否则,你可能会误导观众。
当柱状图中的数字过多时,我更喜欢使用热力图。在这种情况下,我们使用颜色饱和度来编码数字,这样并不是非常精确,因此我们还会保留标签。例如,让我们为我们的平均 GMV 视图添加另一个维度。

作者提供的可视化
毫不奇怪,你也可以在 Plotly 中创建热力图。
fig = px.imshow(
table_df.values,
x = table_df.columns, # labels for x-axis
y = table_df.index, # labels for y-axis
text_auto=',.6r', aspect="auto",
labels=dict(x="age group", y="region", color="GMV in GBP"),
color_continuous_scale='pubugn',
title = '<b>Average annual GMV</b> (Gross Merchandise Value) in GBP'
)
fig.show()
偏差
什么是用例? 这是我们想要突出显示值与基准线(例如基准或预测)之间差异的情况。
图表建议
在比较不同细分市场的指标时,使用图表传达这个想法的最佳方式是结合柱状图和基准线。
我们在我之前的一篇文章中做了这样的可视化,研究了酒店评论中的主题建模。我比较了每个酒店连锁与基准线(所有评论的平均评分)相比,提及特定主题的客户评论比例。我还用颜色突出显示了与其他部分显著不同的部分。

此外,我们经常需要展示与预测的偏差。我们可以使用折线图来比较预测数据和实际数据的动态变化。我更喜欢将预测展示为虚线,以突出它不像实际数据那样确定。

作者提供的可视化
这个线图的案例比我们上面讨论的要复杂一些。所以,我们需要使用 Plotly 图形对象(Plotly Graphical Objects)而不是 Plotly Express 来定制图表。
import plotly.graph_objects as go
# creating a figure
fig = go.Figure()
# adding dashed line trace for forecast
fig.add_trace(
go.Scatter(
mode='lines',
x=df.index,
y=df.forecast,
line=dict(color='#696969', dash='dot', width = 3),
showlegend=True,
name = 'forecast'
)
)
# adding solid line trace for factual data
fig.add_trace(
go.Scatter(
mode='lines',
x=df.index,
y=df.fact,
marker=dict(size=6, opacity=1, color = 'navy'),
showlegend=True,
name = 'fact'
)
)
# setting title and size of layout
fig.update_layout(
width = 800,
height = 400,
title = '<b>Daily Active Users:</b> forecast vs fact'
)
# specifying axis labels
fig.update_xaxes(title = 'day')
fig.update_yaxes(title = 'number of users')
排名
什么是用例? 这个任务类似于名义比较。我们也想比较几个细分领域中的指标,但我们希望强调排名——各细分领域的顺序。例如,这可能是年均 GMV 最高的前三个地区,或 ROI 最高的前三个营销活动。
图表推荐
毫不意外,我们可以使用与名义比较相似的柱状图。唯一需要记住的重要细节是按你关心的指标对图表上的各细分领域进行排序。例如,我们可以可视化年均 GMV 最高的前三个地区。

作者提供的可视化
部分与整体
什么是用例? 目标是理解某些子区的总和如何拆分。你可能想针对一个细分领域,或同时对多个细分领域进行此操作,以比较它们的结构。
图表推荐
最直接的解决方案是使用柱状图显示每个类别或子区的份额。值得注意的是,将类别按降序排列会使可视化更容易解读。

作者提供的可视化
上述方法适用于一个或多个细分领域。然而,有时使用堆积柱状图来比较结构会更简单。例如,我们可以查看不同地区按年龄段划分的客户份额。

作者提供的可视化
饼图在这种情况下经常被使用。但我不建议这么做。根据视觉感知研究的结果,比较角度或面积比比较长度更具挑战性。因此,柱状图会更可取。
此外,我们可能需要查看随时间变化的结构。理想的选择是区域图。它将同时显示各子区的分布和趋势的斜率(这就是为什么它比单纯的柱状图(以月份为类别)更好的原因)。

作者提供的可视化
要创建区域图,你可以使用 Plotly 中的 px.area 函数。
px.area(
df,
title = '<b>Customer age</b> in Switzerland',
labels = {'value': 'share of users, %',
'age_group': 'customer age', 'month': 'month'},
color_discrete_sequence=px.colors.diverging.balance
)
频率分布
什么是用例? 我通常在处理新数据时以这种可视化方式开始。目标是理解价值如何分布:
-
它是正态分布的吗?
-
它是单峰的吗?
-
我们的数据中有异常值吗?
图表推荐
对于频率分布,首选是直方图(通常是没有类别间隔的垂直条形图)。我通常更喜欢归一化直方图,因为它们比绝对值更容易解读。
如果你想查看多个指标的频率分布,你可以同时绘制多个直方图。在这种情况下,使用标准化直方图非常关键。否则,如果组内的对象数量不同,你将无法比较分布。
例如,我们可以可视化来自英国和瑞士客户的年度 GMV 分布。

作者提供的可视化
对于这个可视化,我使用了matplotlib。相比 Plotly,我更喜欢matplotlib绘制直方图,因为我喜欢它们的默认设计。
from matplotlib import pyplot
hist_range = [0, 10000]
hist_bins = 100
pyplot.hist(
distr_df[distr_df.region == 'United Kingdom'].value.values,
label = 'United Kingdom',
alpha = 0.5, range = hist_range, bins = hist_bins,
color = 'navy',
# calculating weights to get normalised histogram
weights = np.ones_like(distr_df[distr_df.region == 'United Kingdom'].index)*100/distr_df[distr_df.region == 'United Kingdom'].shape[0]
)
pyplot.hist(
distr_df[distr_df.region == 'Switzerland'].value.values,
label = 'Switzerland',
color = 'red',
alpha = 0.5, range = hist_range, bins = hist_bins,
weights = np.ones_like(distr_df[distr_df.region == 'Switzerland'].index)*100/distr_df[distr_df.region == 'Switzerland'].shape[0]
)
pyplot.legend(loc = 'upper right')
pyplot.title('Distribution of customers GMV')
pyplot.xlabel('annual GMV in GBP')
pyplot.ylabel('share of users, %')
pyplot.show()
如果你想比较多个类别的分布,阅读同一图表上的多个直方图将会很有挑战性。所以,我建议你使用箱线图。它们显示的信息较少(只有中位数、四分位数和异常值),并且需要观众具备一定的基础知识。然而,在面对许多类别时,它可能是你最好的选择。
例如,我们来看一下按地区划分的站点停留时间分布。

作者提供的可视化
如果你不记得如何解读箱线图,这里有一个图示可以给你一些提示。

图片来自维基百科(来源)| CC BY-SA 2.5 许可
那么,让我们一起来看看箱线图可视化的所有构建块:
-
可视化中的框表示 IQR(四分位间距)——25%和 75%的分位数,
-
盒子中间的线表示中位数(50%的分位数),
-
须状线等于 1.5 * IQR,或者如果数据集中的最小/最大值较小,则等于最小/最大值,
-
如果有任何数字超过 1.5 * IQR(异常值),它们将以点的形式显示在图表上。
这是生成箱线图的 Plotly 代码。我使用了图形对象(Graphical Objects)而不是 Plotly Express,以消除可视化中的异常值。当你的数据集有极端异常值或异常值过多时,这种方法会非常有用。
fig = go.Figure()
fig.add_trace(go.Box(
y=distr_df[distr_df.region == 'United Kingdom'].value,
name="United Kingdom",
boxpoints=False, # no data points
marker_color=px.colors.qualitative.Prism[0],
line_color=px.colors.qualitative.Prism[0]
))
fig.add_trace(go.Box(
y=distr_df[distr_df.region == 'Germany'].value,
name="Germany",
boxpoints=False, # no data points
marker_color=px.colors.qualitative.Prism[1],
line_color=px.colors.qualitative.Prism[1]
))
fig.add_trace(go.Box(
y=distr_df[distr_df.region == 'France'].value,
name="France",
boxpoints=False, # no data points
marker_color=px.colors.qualitative.Prism[2],
line_color=px.colors.qualitative.Prism[2]
))
fig.add_trace(go.Box(
y=distr_df[distr_df.region == 'Switzerland'].value,
name="Switzerland",
boxpoints=False, # no data points
marker_color=px.colors.qualitative.Prism[3],
line_color=px.colors.qualitative.Prism[3]
))
fig.update_layout(title = '<b>Time spent on site</b> per month')
fig.update_yaxes(title = 'time spent in minutes')
fig.update_xaxes(title = 'region')
fig.show()
相关性
什么是使用案例? 目标是理解两个数值数据集之间的关系,是否一个值随着另一个值的增加而增加。
图表推荐
散点图是显示值之间相关性的最佳解决方案。你也可以添加趋势线来突出指标之间的关系。

作者提供的可视化
如果你有大量数据点,可能会遇到散点图的问题:当数据点过多时,它们会相互重叠,导致无法看清数据结构。在这种情况下,减少透明度可能有助于揭示关系。
例如,比较下面的两个图表。第二个图表能更好地理解数据分布。

作者提供的可视化
我们将使用 Plotly 图形对象来绘制此图,因为它非常自定义。要创建这样的图表,我们需要指定两个轨迹——一个用于散点图,另一个用于回归线。
import plotly.graph_objects as go
# scatter plot
fig = go.Figure()
fig.add_trace(
go.Scatter(
mode='markers',
x=corr_df.x,
y=corr_df.y,
marker=dict(size=6, opacity=0.1, color = 'grey'),
showlegend=False
)
)
# regression line
fig.add_trace(
go.Scatter(
mode='lines',
x=linear_corr_df.x,
y=linear_corr_df.linear_regression,
line=dict(color='navy', dash='dash', width = 3),
showlegend=False
)
)
fig.update_layout(width = 600, height = 400,
title = '<b>Correlation</b> between revenue and customer tenure')
fig.update_xaxes(title = 'months since registration')
fig.update_yaxes(title = 'monthly revenue, GBP')
将回归线放在第二个轨迹上是至关重要的,因为否则它将被散点图覆盖。
此外,展示两个变量的频率分布可能会提供有价值的见解。这看起来并不容易,但你可以使用 seaborn 库中的联合图轻松实现。以下是相关代码。
import seaborn as sns
sns.set_theme(style="darkgrid")
g = sns.jointplot(
x="x", y="y", data=corr_df,
kind="reg", truncate=False,
joint_kws = {'scatter_kws':dict(alpha=0.15), 'line_kws':{'color':'navy'}},
color="royalblue", height=7)
g.set_axis_labels('months since registration', 'monthly revenue, GBP')

作者可视化图
我们已经涵盖了所有数据可视化的使用场景。
这些是我需要了解的所有可视化类型吗?
我必须坦白说,时不时地,我会遇到上述建议不足以应对的任务,这时我需要一些其他的图表。
以下是一些例子:
-
Sankey 图或 Sunburst 图用于客户旅程图,
-
Choropleth 图,用于显示地理数据,
-
词云图,用于提供文本的高层次视图,
-
如果你想查看多条线的趋势,可以使用小型图(Sparklines)。
我通常会参考流行的可视化库的画廊获取灵感,例如,Plotly 或 seaborn。
此外,你可以随时询问 ChatGPT 关于展示数据的可能选项,它会提供相当合理的指导。

作者截图
总结
在本文中,我们讨论了数据可视化的基础知识:
-
为什么我们需要进行数据可视化?
-
在开始进行可视化工作之前,你应该问自己什么问题?
-
什么是数据可视化的基本构建块?哪些是观众最容易理解的?
-
数据可视化的常见使用场景是什么?你可以使用哪些图表类型来应对这些场景?
我希望提供的框架能够帮助你在面对各种选项时不至于感到迷茫,从而为你的观众创造更好的可视化效果。
非常感谢您阅读本文。如果您有任何后续问题或评论,请在评论区留言。
可视化 Strava 比赛分析。
两个新的图表,比较同一场比赛中的跑者
·发布于数据科学之路·阅读时间:15 分钟·2024 年 8 月 6 日
--

显示跑者表现比较的图表。图片由作者提供。
你有没有想过在同一场比赛中,两个跑者如何相互比较?
在这篇文章中,我展示了我设计的两个新图表,因为我觉得 Strava 缺少这些图表。这些图表的设计方式使得人们可以一目了然地了解比赛的情况,因为它们比较了参加同一场赛事的不同运动员。你可以轻松看到各选手之间位置的变化,以及圈速和竞赛者之间的时间差异。
我的解释将从我如何发现这个机会开始。接下来,我将展示图表设计,并解释驱动这些图表的算法和数据处理技术。
Strava 并没有讲述完整的故事。
Strava 是一款社交健身应用,用户可以记录并与 100 多万的社区成员分享他们的运动活动[1]。这款应用广泛用于骑行和跑步者之间,它不仅记录你的活动,还根据你的健康数据提供个性化的表现分析。
作为一名跑者,我觉得这款应用对我有两个主要好处:
-
它提供的数据分析帮助我更好地理解我的跑步表现。
-
它激励我保持动力,因为我可以看到我的朋友和社区分享的内容。
每当我和朋友一起完成一场跑步比赛时,我们都会将手表中的健身数据上传到 Strava,查看如下一些分析:
-
总时间、距离和平均配速。
-
赛跑中的每个分段时间或圈速。
-
心率指标的变化。
-
与之前活动相比的相对努力。
最精彩的部分是我们从每个人的视角来讨论比赛。Strava 能够识别出你和你的朋友(如果你们互相关注)以及其他人参加了同一场比赛,但它并不提供对比数据。因此,如果你想要了解你和朋友们的完整比赛故事,你需要深入查看每个人的活动并尝试进行对比。
所以,在我和 3 个朋友今年的最后一次 10 公里赛后,我决定从 Strava 获取数据,并设计了两张可视化图表,以查看我们比赛表现的对比分析。
展示可视化图表
这个项目背后的想法很简单:利用我和朋友在比赛过程中通过 Strava 记录的 GPX 数据(位置、时间戳),并将它们结合起来,生成比较我们比赛的可视化图表。
挑战不仅是验证我的想法是否可行,还要设计灵感来自 Strava 的图表,以证明它们如何作为新功能无缝集成到当前的应用程序中。让我们来看一下结果。
赛程差距分析
指标: 赛程差距的演变(以秒为单位),其中灰色线代表的是参考选手,其他选手则根据其与参考选手的距离变化而变化。位于上方的线表示该选手在比赛中领先。
洞察: 这张折线图非常适合查看一组选手的排名和距离变化。

同一场比赛中 3 位不同选手的赛程差距分析动画。图片由作者提供。
如果你看一下这些线条的右端,你可以看到我们三个示例选手的比赛最终结果:
-
第一个选手(我)由灰色线表示,是参考选手。
-
Pedro(穿紫色衣服)是第二个选手,他在 12 秒后才到达终点。
-
Jimena(穿蓝色衣服)在 10 公里赛后 60 秒完成比赛。


赛程差距分析图表整合到 Strava 活动中的提案。图片由作者提供。
但是,感谢这张图表,我们可以看到这些赛程差距在比赛中的变化。而这些洞察对于理解赛程中的位置和距离变化非常有趣:
-
我们三个人一起开始了比赛。Jimena(穿蓝色衣服)在第一公里约 5 秒时开始落后,而我(穿灰色衣服)和 Pedro(穿紫色衣服)则在一起。
-
我记得 Pedro 告诉我开始得太快了,所以他稍微放慢了步伐,直到在第 2 公里遇到了 Jimena。他们的轨迹显示,他们一起跑到了第 5 公里,而我则在不断拉开与他们的差距。
-
第 6 公里是关键点,我与 Pedro 的差距在那时达到了 20 秒(我所达到的最大差距),与 Jimena 的差距则接近 30 秒。Jimena 在那时比我慢了节奏,并一直保持较慢的速度直到比赛结束。然而,Pedro 开始加速,并在最后 4 公里里努力缩小与我的差距。
当然,线条会根据参考点的不同而发生变化。这样,每个跑者都会看到同一场比赛的故事,但会根据他们的视角和与其他人的比较进行个性化展示。这就是同一个故事,不同的主角。



使用不同参考点的赛程差距分析。参考点是胡安(左)。参考点是佩德罗(中)。参考点是吉梅纳(右)。图片来源:作者。
如果我是 Strava,我会将这个图表集成到用户标记为“比赛”的活动中。分析可以通过所有注册了相同活动的用户的追随者来进行。上面显示了一个集成的示例。
头对头赛程分析
度量标准: 这条线表示两位跑者之间的时间差(秒)变化。条形图则表示每圈,某位跑者相较于另一位跑者的速度是更快(蓝色)还是更慢(红色)。
见解: 这个组合图表非常适合分析每圈比赛中的头对头表现。


关于佩德罗与胡安对比分析在 Strava 上的集成建议。图片来源:作者。
这个图表专门设计用来比较两位跑者在比赛分段(圈次)中的表现。
这个例子代表了佩德罗与胡安相比的时间损失。
-
橙色线条代表了时间损失,如同另一个图表的解释:两人一起起跑,但佩德罗在第一个公里之后开始失去时间,直到第六公里。然后,他开始加速,缩小这个差距。
-
这些条形图为我们的比较带来了新的见解,展示了每圈的时间损失(红色)或时间增益(蓝色)。一眼看去,佩德罗可以看到时间损失最大的一圈是在第三公里(8 秒)。而且他只在一半的分段中输了时间。两人在第 1 和第 4 公里的配速相同,佩德罗在第 7、第 8 和第 9 公里的速度更快。
多亏了这个图表,我们可以看到,在前 6 公里我比佩德罗跑得更快,获得了一个佩德罗无法缩小的优势,尽管他在比赛的最后部分速度更快。这也证实了我们在比赛后常有的感觉:“佩德罗在比赛的最后冲刺更强。”
数据处理与算法
如果你想知道这些图表是如何创建的,可以继续阅读关于实现的这一部分内容。
我不想过多讨论背后的编码细节。像所有软件问题一样,你可以通过不同的解决方案来实现目标。这就是为什么我更感兴趣的是解释我遇到的挑战以及我的解决逻辑。
数据加载中
没有数据,就没有解决方案。在这种情况下不需要 Strava API。如果你登录 Strava 账户并进入某个活动,你可以通过点击 Export GPX 来下载该活动的 GPX 文件,如屏幕截图所示。GPX 文件包含 XML 格式的数据点,如下所示。


如何从 Strava 下载 GPX 文件(左)。GPX 文件示例(右)。图片来源:作者。
为了获取我朋友们相同活动的数据,我只是让他们按照相同的步骤进行,并将 .gpx 文件发给我。
数据准备
对于这个用例,我只对几个属性感兴趣:
-
位置:纬度,经度 和 海拔
-
时间戳:时间。
对我来说,第一个问题是将 .gpx 文件转换为 pandas 数据框,这样我就可以使用 Python 来处理和处理数据。我使用了 gpxpy 库。下面是代码。
import pandas as pd
import gpxpy
# read file
with open('juan.gpx', 'r') as gpx_file:
juan_gpx = gpxpy.parse(gpx_file)
# Convert Juan´s gpx to dataframe
juan_route_info = []
for track in juan_gpx.tracks:
for segment in track.segments:
for point in segment.points:
juan_route_info.append({
'latitude': point.latitude,
'longitude': point.longitude,
'elevation': point.elevation,
'date_time': point.time
})
juan_df = pd.DataFrame(juan_route_info)
juan_df
在那之后,我有了 667 个数据点存储在数据框中。每一行代表 我 在活动期间的 位置 和 时间。
我了解到,并不是每一行的数据捕捉频率都是一致的(0 和 1 之间间隔 1 秒,然后是 3 秒,再是 4 秒,接着是 1 秒……)

存储在 pandas 数据框中的 .gpx 数据示例。图片来源:作者。
获取一些指标
数据中的每一行代表一个不同的时刻和地点,所以我的第一个想法是计算两行之间的时间、海拔和距离差异:seconds_diff、elevation_diff 和 distance_diff。
时间和海拔使用 .diff() 方法在 pandas 数据框的每一列上进行计算,比较直接。
# First Calculate elevation diff
juan_df['elevation_diff'] = juan_df['elevation'].diff()
# Calculate the difference in seconds between datapoints
juan_df['seconds_diff'] = juan_df['date_time'].diff()
不幸的是,由于地球不是平的,我们需要使用一种叫做哈弗辛距离 [2]:计算两个点在球面上最短距离的度量,给定它们的纬度和经度坐标。我使用了 haversine 库。请参见下面的代码。
import haversine as hs
# Function to calculate haversine distances
def haversine_distance(lat1, lon1, lat2, lon2) -> float:
distance = hs.haversine(
point1=(lat1, lon1),
point2=(lat2, lon2),
unit=hs.Unit.METERS
)
# Returns the distance between the first point and the second point
return np.round(distance, 2)
#calculate the distances between all data points
distances = [np.nan]
for i in range(len(track_df)):
if i == 0:
continue
else:
distances.append(haversine_distance(
lat1=juan_df.iloc[i - 1]['latitude'],
lon1=juan_df.iloc[i - 1]['longitude'],
lat2=juan_df.iloc[i]['latitude'],
lon2=juan_df.iloc[i]['longitude']
))
juan_df['distance_diff'] = distances
累计距离作为一个新列 distance_cum 被添加,使用了 cumsum() 方法,如下所示。
# Calculate the cumulative sum of the distance
juan_df['distance_cum'] = juan_df['distance_diff'].cumsum()
此时,包含我的轨迹数据的数据框已包括 4 个具有有用指标的新列:

每一行的新指标数据框。图片来源:作者。
我将相同的逻辑应用到其他跑步者的轨迹:jimena_df 和 pedro_df。


其他跑步者的数据框:Pedro(左)和 Jimena(右)。图片来源:作者。
现在我们准备好开始操作数据并创建可视化效果。
挑战:
为了获得用于可视化所需的数据,我的第一直觉是:查看每个跑步者的累计距离列,识别每个跑步者完成一圈的时刻(1000、2000、3000 等),并计算时间戳的差异。
这个算法看起来简单,可能有效,但它有一些我需要解决的限制:
-
精确的圈速距离通常是在两个注册数据点之间完成的。为了更精确,我需要对位置和时间进行插值。
-
由于设备精度的差异,不同跑者之间可能会存在不对齐的情况。最典型的情况是,即使两名跑者始终在同一轨迹上,某一跑者的圈速通知在另一跑者之前响起。为尽量减少这种情况,我决定使用参考跑者来设置每圈在轨道上的位置标记。当其他跑者跨越这些标记时,将计算时间差(即使他们的累计距离领先或滞后)。这种方法更接近比赛的实际情况:如果某人先跨越一个标记,他们就领先(无论其设备的累计距离如何)。
-
在前一点中,另一个问题随之而来:参考标记的纬度和经度可能永远不会完全在其他跑者的数据中注册。我使用了最近邻算法来根据位置找到最接近的数据点。
-
最后,如果轨迹在不同的时间点经过相同的位置,最近邻算法可能会引入错误的数据点。因此,需要将最近邻查找最佳匹配的区域缩小为一个较小的候选集。我定义了一个20 个数据点的窗口大小,以便围绕目标距离进行处理(distance_cum)。
算法
考虑到之前所有的限制,算法应如下:
1. 选择参考数据和圈速距离(默认= 1 公里)。
2. 使用参考数据,确定每一圈完成的时间和位置:参考标记。
3. 进入其他跑者的数据,找出他们跨越这些位置标记的时刻。然后计算两名跑者跨越这些标记的时间差。最后,计算这个时间差的变化,表示间隔的演变。
代码示例
1. 选择参考数据和圈速距离(默认= 1 公里)。
-
Juan 将作为示例中的参考(juan_df)。
-
其他跑者将是 Pedro(pedro_df)和 Jimena(jimena_df)。
-
圈速距离将为 1000 米。
2. 创建interpolate_laps():这个函数用来查找或插值每一圈完成的精确点,并将其返回为一个新的 DataFrame。插值是通过函数interpolate_value()完成的,这个函数也被创建了。
## Function: **interpolate_value**()
**Input**:
- *start*: The starting value.
- *end*: The ending value.
- *fraction*: A value between 0 and 1 that represents the position between
the start and end values where the interpolation should occur.
**Return**:
- The interpolated value that lies between the *start* and *end* values
at the specified *fraction*.
def interpolate_value(start, end, fraction):
return start + (end - start) * fraction
## Function: **interpolate_laps()**
**Input**:
- track_df: dataframe with track data.
- lap_distance: metres per lap (default 1000)
**Return**:
- track_laps: dataframe with lap metrics. As many rows as laps identified.
def interpolate_laps(track_df , lap_distance = 1000):
#### 1\. Initialise track_laps with the first row of track_df
track_laps = track_df.loc[0][['latitude','longitude','elevation','date_time','distance_cum']].copy()
# Set distance_cum = 0
track_laps[['distance_cum']] = 0
# Transpose dataframe
track_laps = pd.DataFrame(track_laps)
track_laps = track_laps.transpose()
#### 2\. Calculate number_of_laps = Total Distance / lap_distance
number_of_laps = track_df['distance_cum'].max()//lap_distance
#### 3\. For each lap i from 1 to number_of_laps:
for i in range(1,int(number_of_laps+1),1):
# a. Calculate target_distance = i * lap_distance
target_distance = i*lap_distance
# b. Find first_crossing_index where track_df['distance_cum'] > target_distance
first_crossing_index = (track_df['distance_cum'] > target_distance).idxmax()
# c. If match is exactly the lap distance, copy that row
if (track_df.loc[first_crossing_index]['distance_cum'] == target_distance):
new_row = track_df.loc[first_crossing_index][['latitude','longitude','elevation','date_time','distance_cum']]
# Else: Create new_row with interpolated values, copy that row.
else:
fraction = (target_distance - track_df.loc[first_crossing_index-1, 'distance_cum']) / (track_df.loc[first_crossing_index, 'distance_cum'] - track_df.loc[first_crossing_index-1, 'distance_cum'])
# Create the new row
new_row = pd.Series({
'latitude': interpolate_value(track_df.loc[first_crossing_index-1, 'latitude'], track_df.loc[first_crossing_index, 'latitude'], fraction),
'longitude': interpolate_value(track_df.loc[first_crossing_index-1, 'longitude'], track_df.loc[first_crossing_index, 'longitude'], fraction),
'elevation': interpolate_value(track_df.loc[first_crossing_index-1, 'elevation'], track_df.loc[first_crossing_index, 'elevation'], fraction),
'date_time': track_df.loc[first_crossing_index-1, 'date_time'] + (track_df.loc[first_crossing_index, 'date_time'] - track_df.loc[first_crossing_index-1, 'date_time']) * fraction,
'distance_cum': target_distance
}, name=f'lap_{i}')
# d. Add the new row to the dataframe that stores the laps
new_row_df = pd.DataFrame(new_row)
new_row_df = new_row_df.transpose()
track_laps = pd.concat([track_laps,new_row_df])
#### 4\. Convert date_time to datetime format and remove timezone
track_laps['date_time'] = pd.to_datetime(track_laps['date_time'], format='%Y-%m-%d %H:%M:%S.%f%z')
track_laps['date_time'] = track_laps['date_time'].dt.tz_localize(None)
#### 5\. Calculate seconds_diff between consecutive rows in track_laps
track_laps['seconds_diff'] = track_laps['date_time'].diff()
return track_laps
将插值函数应用到参考 DataFrame 中,将生成以下 DataFrame:
juan_laps = interpolate_laps(juan_df , lap_distance=1000)

插值后的圈速度量的 DataFrame。图片来源:作者。
请注意,由于这是一次 10 公里的比赛,因此已经识别出 10 圈,每圈 1000 米(见distance_cum列)。seconds_diff列显示每圈的时间。其他列(latitude,longitude,elevation和date_time)表示参考数据每圈的位置和时间,这是通过插值得到的结果。
- 为了计算参考与其他跑者之间的时间差,我创建了函数 gap_to_reference()
## Helper Functions:
- **get_seconds**(): Convert timedelta to total seconds
- **format_timedelta**(): Format timedelta as a string (e.g., "+01:23" or "-00:45")
# Convert timedelta to total seconds
def get_seconds(td):
# Convert to total seconds
total_seconds = td.total_seconds()
return total_seconds
# Format timedelta as a string (e.g., "+01:23" or "-00:45")
def format_timedelta(td):
# Convert to total seconds
total_seconds = td.total_seconds()
# Determine sign
sign = '+' if total_seconds >= 0 else '-'
# Take absolute value for calculation
total_seconds = abs(total_seconds)
# Calculate minutes and remaining seconds
minutes = int(total_seconds // 60)
seconds = int(total_seconds % 60)
# Format the string
return f"{sign}{minutes:02d}:{seconds:02d}"
## Function: **gap_to_reference**()
**Input**:
- laps_dict: dictionary containing the df_laps for all the runnners' names
- df_dict: dictionary containing the track_df for all the runnners' names
- reference_name: name of the reference
**Return**:
- matches: processed data with time differences.
def gap_to_reference(laps_dict, df_dict, reference_name):
#### 1\. Get the reference's lap data from laps_dict
matches = laps_dict[reference_name][['latitude','longitude','date_time','distance_cum']]
#### 2\. For each racer (name) and their data (df) in df_dict:
for name, df in df_dict.items():
# If racer is the reference:
if name == reference_name:
# Set time difference to zero for all laps
for lap, row in matches.iterrows():
matches.loc[lap,f'seconds_to_reference_{reference_name}'] = 0
# If racer is not the reference:
if name != reference_name:
# a. For each lap find the nearest point in racer's data based on lat, lon.
for lap, row in matches.iterrows():
# Step 1: set the position and lap distance from the reference
target_coordinates = matches.loc[lap][['latitude', 'longitude']].values
target_distance = matches.loc[lap]['distance_cum']
# Step 2: find the datapoint that will be in the centre of the window
first_crossing_index = (df_dict[name]['distance_cum'] > target_distance).idxmax()
# Step 3: select the 20 candidate datapoints to look for the match
window_size = 20
window_sample = df_dict[name].loc[first_crossing_index-(window_size//2):first_crossing_index+(window_size//2)]
candidates = window_sample[['latitude', 'longitude']].values
# Step 4: get the nearest match using the coordinates
nn = NearestNeighbors(n_neighbors=1, metric='euclidean')
nn.fit(candidates)
distance, indice = nn.kneighbors([target_coordinates])
nearest_timestamp = window_sample.iloc[indice.flatten()]['date_time'].values
nearest_distance_cum = window_sample.iloc[indice.flatten()]['distance_cum'].values
euclidean_distance = distance
matches.loc[lap,f'nearest_timestamp_{name}'] = nearest_timestamp[0]
matches.loc[lap,f'nearest_distance_cum_{name}'] = nearest_distance_cum[0]
matches.loc[lap,f'euclidean_distance_{name}'] = euclidean_distance
# b. Calculate time difference between racer and reference at this point
matches[f'time_to_ref_{name}'] = matches[f'nearest_timestamp_{name}'] - matches['date_time']
# c. Store time difference and other relevant data
matches[f'time_to_ref_diff_{name}'] = matches[f'time_to_ref_{name}'].diff()
matches[f'time_to_ref_diff_{name}'] = matches[f'time_to_ref_diff_{name}'].fillna(pd.Timedelta(seconds=0))
# d. Format data using helper functions
matches[f'lap_difference_seconds_{name}'] = matches[f'time_to_ref_diff_{name}'].apply(get_seconds)
matches[f'lap_difference_formatted_{name}'] = matches[f'time_to_ref_diff_{name}'].apply(format_timedelta)
matches[f'seconds_to_reference_{name}'] = matches[f'time_to_ref_{name}'].apply(get_seconds)
matches[f'time_to_reference_formatted_{name}'] = matches[f'time_to_ref_{name}'].apply(format_timedelta)
#### 3\. Return processed data with time differences
return matches
以下是实现逻辑并将结果存储在数据框 matches_gap_to_reference: 中的代码:
# Lap distance
lap_distance = 1000
# Store the DataFrames in a dictionary
df_dict = {
'jimena': jimena_df,
'juan': juan_df,
'pedro': pedro_df,
}
# Store the Lap DataFrames in a dictionary
laps_dict = {
'jimena': interpolate_laps(jimena_df , lap_distance),
'juan': interpolate_laps(juan_df , lap_distance),
'pedro': interpolate_laps(pedro_df , lap_distance)
}
# Calculate gaps to reference
reference_name = 'juan'
matches_gap_to_reference = gap_to_reference(laps_dict, df_dict, reference_name)
结果数据框的列包含将在图表上显示的重要信息:

来自函数 gap_to_reference() 返回的某些数据框列。图片由作者提供。
比赛间隙分析图
要求:
-
可视化需要针对作为 参考 的跑者进行定制。每个跑者都将由一条线形图表示。
-
X 轴表示距离。
-
Y 轴表示与参考的间隙,单位为秒
-
参考将设定基准线。y 轴=0 的常数灰线
-
其他跑者的线条将位于参考线之上(如果他们在赛道上领先)或之下(如果他们落后)。

比赛间隙分析 图表(10 圈,1000 米)。图片由作者提供。
为了呈现图表,我使用了 plotly 库,并使用了 matches_gap_to_reference: 中的数据。
X 轴:每圈的累计距离。列 distance_cum
Y 轴: 表示与参考的间隙,单位为秒:
-
灰色线:参考的间隙始终为 0。
-
紫色线:Pedro 与参考之间的间隙 (-) seconds_to_reference_pedro。
-
蓝色线:Jimena 与参考之间的间隙 (-) seconds_to_reference_jimena。
头对头圈次分析图
要求:
-
可视化需要比较两名跑者的数据。一名参考跑者和一名竞争者。
-
X 轴表示距离
-
Y 轴表示秒数
-
将绘制两项指标来比较跑者的表现:线形图将显示每个比赛点的总间隙,条形图则表示每圈该间隙是增加(正值)还是减少(负值)。

头对头圈次分析 图表(10 圈,1000 米)。图片由作者提供。
再次说明,示例中呈现的数据来自 matches_gap_to_reference:
X 轴:每圈的累计距离。列 distance_cum
Y 轴:
-
橙色线:Pedro 与 Juan 之间的间隙 (+) seconds_to_reference_pedro
-
条形图:每圈之间该间隙的变化 lap_difference_formatted_pedro。 如果 Pedro 失去时间,间隙为正并以红色表示。否则,条形图为蓝色。
我改进了两种视觉效果的风格,使其更加贴合 Strava 的设计美学。
为本文点赞?
我在上次比赛后开始了这个想法。我非常喜欢这些视觉效果的结果,因此认为它们可能对 Strava 社区有用。所以我决定通过撰写这篇文章与大家分享。
参考资料
[1] S. Paul, Strava 的下一篇章:新任 CEO 谈 AI、包容性以及为什么‘暗黑模式’花了这么长时间才推出。 (2024)
[2] D. Grabiele, “哈弗辛公式”,Baeldung 上的计算机科学。 (2024)
可视化、数学、时间序列及更多:我们最近最深入的文章
·发布于 Towards Data Science ·通过 Newsletter 发送 ·3 分钟阅读·2024 年 2 月 8 日
--
欢迎来到《Variable》杂志的第 150 期!选择我们在此分享的文章总是我们每周的亮点之一,因为它为我们——也希望是您——提供了一个机会,去欣赏我们的作者们为 TDS 带来的深度和多样化的经验。
我们想不出比将一些我们最近最深入的文章精选汇总起来更好的方式来庆祝这一里程碑。这些文章可能需要作者和编辑付出最多的努力,但也实现了他们的雄心。不论是介绍性话题还是前沿研究,它们都以细腻和详细的方式探讨主题,并耐心地引导读者逐步了解新的问题和工作流程。让我们深入探讨吧!
-
为你的图神经网络解释创建交互式可视化首先,我们转向 Benjamin Lee的详细教程,在这个教程中,我们学习如何通过五个步骤为 GNN(图神经网络)构建交互式可视化,读者可以在其中找到开始动手和创造所需的所有代码片段。
-
深度学习插图,第一部分:神经网络是如何工作的?深入探讨并不意味着写作难以理解或难以跟随——恰恰相反!举个例子:Shreya Rao的最新入门级文章,是一篇神经网络的介绍,假设读者没有太多先前知识,并提供了对网络内部运作的详细插图说明。
-
处理时间序列中的缺失值对时间序列中的缺失数据进行填补是数据科学中最基本的任务之一,但它的普遍性并没有让它变得更容易执行。Erich Silva的耐心指南涵盖了缺失数据分析和评估指标中常见的挑战。

图片由Malgorzata Bujalska拍摄,来自Unsplash
-
2024 年从“忙碌”的数据科学家到高效数据科学家的 9 个简单技巧基于列表的文章常常有快速浏览过多条目并且留下很少具体见解的风险。Madison Hunter的最新职业建议文章表明,即使将材料分解成更易消化的部分,也完全可以涵盖相当多的内容并提供可操作的建议。
-
用 Python 手动构建随机森林要真正理解像随机森林这样的算法如何工作,没有什么方法比亲自构建它更有效。这听起来可能令人畏惧,但幸运的是,Matt Sosna提供了一篇耐心的指南,帮助你一步一步用 Python 从头开始实现该算法,确保你走在正确的道路上。
-
R 中的二元逻辑回归无论你是第一次接触逻辑回归,还是想要进行 R 语言的实际编码练习,Antoine Soetewey的新文章都是你不容错过的一站式资源——它详细阐述了何时以及如何使用(单变量和多变量)二元逻辑回归,以及如何可视化和报告结果。
-
12 个 RAG 痛点与提议的解决方案我们以类似的方式结束了本期内容:一份全面、实用的技术指南,聚焦一个及时的技术话题——在这个案例中,Wenqi Glantz分享了在检索增强生成工作流中可能遇到的常见问题,以及如何克服它们。
并不是每篇优秀的帖子都必须很长!我们欣赏各种形式和大小的精心制作的文章,正如我们每周推荐的其他文章所展示的那样:
-
学习如何通过Caroline Arnold的教程,让你的图表变得更加易于访问,让色盲人士也能解读和分析。
-
在经济不确定性和裁员频繁成为科技新闻焦点的背景下,Tessa Xie提供了一份详细的路线图,帮助你减少被裁员的可能性。
-
上下文窗口是如何影响 Transformer 模型的训练和使用的?Chris Hughes清晰简明地解释了这一点。
-
赶快了解一些人工智能与医学影像交汇处的前沿研究——Lambert T Leong 博士展示了一个有前景的新项目,旨在让健康评估变得更加易于获取。
-
如果你只是想卷起袖子直接开始编码,Stephanie Kirmer最近分享了一篇关于将机器学习模型投入生产的易懂教程,介绍了如何使用 AWS Lambda。
感谢你支持我们作者的作品!如果你也受到启发,为什么不写下你的第一篇文章?我们很期待阅读。
直到下一个 Variable,
TDS 团队
使用 Matplotlib 绘制数据的饼图
如何在 Jupyter Notebook 中使用 Pandas,结合 Matplotlib 创建不同类型的饼图,以可视化数据库分析结果的示例
·发表于Towards Data Science ·阅读时间:5 分钟·2024 年 10 月 16 日
--

图片由Niko Nieminen提供,来源于Unsplash
挑战
在我撰写硕士论文《与 NIH 资助的心脏病研究中具有影响力的科学出版物相关的因素》时,我使用了不同类型的饼图来展示从数据库分析中得出的一些关键发现。
当数据集包含有限数量的类别,且这些类别代表整体的部分时,饼图可能是数据可视化的有效选择。它特别适合用来显示类别数据,重点比较每个类别的相对比例。
在本文中,我将展示如何使用相同的数据集创建四种不同类型的饼图,以提供更全面的视觉呈现,并深入洞察数据。为此,我将使用 Python 的绘图库 Matplotlib 来展示存储在数据框中的统计数据的饼图可视化。如果你对 Matplotlib 库不熟悉,一个很好的起点是 Jake VanderPlas 的《Python 数据科学手册》,特别是关于使用 Matplotlib 进行可视化的章节,以及matplotlib.org。
首先,让我们导入所有必要的库和扩展:
接下来,我们将准备 CSV 文件进行处理:
本文使用的小型数据集突出了 2002 年至 2020 年间心脏病研究领域排名前十的期刊,是用于硕士论文研究的大型数据库的一部分。列“Female”(女性)、“Male”(男性)和“Unknown”(未知)表示已发表文章的第一作者的性别,而“Total”(总数)列则反映了每本期刊所发表的心脏病研究文章的总数。

图片由作者提供,表示上面Pie_Chart_Artcile_2.py示例代码的输出结果。
对于类别较少的小型数据集,带有爆炸切片(exploding slices)的饼图可以有效地通过稍微突出一个关键类别来引起注意。这种视觉效果能使特定类别从整体中脱颖而出。每一片代表总体的一部分,大小与其所代表的数据成比例。可以为每一片添加标签以指示类别,并附上百分比以展示其占总体的比例。这种视觉技巧使得爆炸的切片突出显示,而不会丧失完整数据表示的上下文。

图片由作者提供,表示上面Pie_Chart_Artcile_3.py示例代码的输出结果。
同样的爆炸切片技巧也可以应用于示例数据集中的所有其他条目,并且生成的图表可以显示在同一图形中。这种可视化类型有助于突出数据集中某一类别的过度或不足表示。在提供的示例中,将所有 10 个图表呈现在同一图形中,揭示了心脏病研究领域排名前十的期刊中,没有一本期刊发表的女性作者文章多于男性,从而强调了性别差异。

2002 至 2020 年间心脏病研究领域排名前十期刊的性别分布。图片由作者提供,表示上面Pie_Chart_Artcile_4.py示例代码的输出结果。
饼图的一种变体,称为甜甜圈图(donut chart),也可以用来可视化数据。甜甜圈图与饼图类似,都显示构成整体的各个类别的比例,但甜甜圈图的中心部分也可以用来呈现额外的数据。这种格式在视觉上更为简洁,相较于标准饼图,它使得比较各部分相对大小变得更加容易。在本文使用的示例中,甜甜圈图突出了在心脏病研究领域排名前十的期刊中,《美国生理学杂志·心脏与循环生理学》发表了最多的文章,占比 21.8%。

图片由作者提供,表示上面Pie_Chart_Artcile_5.py示例代码的输出结果。
我们可以通过在先前的甜甜圈图的基础上构建嵌套版本,来增强从示例数据集中获取附加信息的可视化效果。Matplotlib 的 figure 模块中的 add_artist() 方法用于将任何额外的艺术元素(如图形或对象)融入到基础图形中。与早期的甜甜圈图类似,这个变体展示了心脏病研究领域前 10 本期刊的出版物分布。然而,它还包括了一个额外的层级,显示了每本期刊的第一作者的性别分布。这个可视化突出了第一作者中男性的比例较大。

由作者提供的图像,展示了上面提到的 Pie_Chart_Artcile_6.py 示例代码的输出。
总结来说,饼图非常有效地用于可视化具有有限类别数量的数据,因为它使观众能够一眼快速理解最重要的类别或占主导地位的比例。在这个具体的示例中,使用四种不同类型的饼图清晰地展示了基于 2002 至 2020 年小型数据集的心脏病研究领域前 10 期刊第一作者的性别分布。显而易见,大多数出版物的第一作者是男性,并且在研究期间,心脏病研究的前 10 期刊中,没有一本期刊的女性第一作者数量超过男性。
本文使用的 Jupyter Notebook 和数据集可以在 GitHub 上找到。
感谢阅读,
Diana
注意:我使用了 GitHub 嵌入功能来发布这篇文章。
通过数据序列化可视化业务流程
有时,我们希望在 Power BI 中可视化一个业务流程。根据所需的可视化效果以及我们通常方式建模数据时,这可能是一个挑战。让我们通过改变建模方式来看如何实现这一目标。
·发表于 Towards Data Science ·阅读时长 11 分钟·2024 年 3 月 15 日
--

图片由 Lukas Blazek 提供,来自 Unsplash
介绍
我们的业务由多个流程组成。有些流程很明显,比如生产流程,而有些则更具虚拟性。
例如,当我想计算利润时,实际上是有一个“过程”来实现的。
以非常简单的方式来说,当我把收入减去开支时,剩下的就是我的利润。
我目前的目标是通过图表可视化这一过程。
该图表应该显示利润的构成过程。为此,我喜欢将销售金额到利润的值显示为瀑布图。
这是我在 Excel 中创建的一个类似的模型:

图 1 — 我的利润构成的目标可视化(图由作者提供)
我从销售金额开始,逐步扣除所有费用,直到得到利润。
在 Power BI 中实现这一点有一些挑战。
首先,我必须更改我的数据模型。然后,我可以编写一些度量值。最后,我可以创建可视化图表。
最终,我将收集有关数据模型及其性能的信息,以评估我的方法的质量。
让我们逐步了解这些步骤。
数据挑战
首先,标准的瀑布图不允许添加多个度量来构建这个图表。
至少有一个自定义可视化可以做到这一点,但它是收费的可视化,我想使用标准的瀑布图。我的大多数客户不愿意购买自定义可视化。
所以,我需要改变数据存储方式以完成这项工作。
通常,我们将数据按每列一个数值的方式存储。
类似这样的:

图 2 — 标准的事实建模方式(交易)(图由作者提供)
我创建了不同的 DAX 度量来聚合列,以获得所需结果。
最终,我可以通过从销售额中扣除支出来计算利润率。
然而,这种方法并没有解决我的问题。因此,我需要使用不同的数据建模方法来绘制瀑布图。
我只能将一个度量添加到可视化中。对于瀑布图中的各个部分,我必须添加一列用于数据的分类/分段。
为了实现这一点,我必须取消透视数据,将单个度量值垂直存储在一个数值列中。度量名称存储在一个单独的列中,用来包含分类信息。
类似这样的:

图 3 — 取消透视后的(序列化的)数据(图由作者提供)
这种建模技术被称为“序列化”,因为数据是垂直序列化的,而不是水平序列化的。
仍然有一个问题:所有的值都是正数。然而,我必须将其中一些值转换为负数,在瀑布图中显示时,用来从销售额中扣除它们。
此外,度量的顺序必须正确,以确保它们不会按字母顺序显示。
为了实现这一点,我创建一个包含度量的表,如下所示:

图 4 — 度量列表表(图由作者提供)
如果你的数据源是关系型数据库,我建议在数据库中创建这个表并进行数据转换。
由于在不同平台和数据建模类型之间,数据库中的具体操作方式可能不同,我将在这里不展示这一部分。请联系你的数据库开发人员来处理这个问题。
在这篇文章中,我将向你展示如何在 Power Query 和 DAX 中做到这一点。
Power Query 中的数据重新建模
在 Power Query 中,第一步是直接的:
我使用取消透视转换将数据从列导向转为行导向,并得到如上所示的结果。
我选择所有包含值的列(度量列),然后点击“取消透视”按钮。在这里,我选择“仅取消透视选定的列”:

图 5 — 取消透视所有度量列(图由作者提供)
一段时间后(取决于数据量和来源),度量列会被替换为两个新列:属性和数值。
我修改了 M 代码,将“属性”列重命名为“度量”:

图 6 — 更改属性列的名称(图由作者提供)
要在 Power Query 中查看公式栏,必须启用以下选项:

图 7 — 在 Power Query 中启用公式栏(图由作者提供)
由于我的模型包含了 Contoso 演示数据,其中有两个事实表(在线销售和零售销售),我对另一个表执行相同的步骤。
接下来,我创建包含所有 Measure 的列表的表格。
我右键点击在线销售表格,点击“引用”。我创建了一个引用,因为它重用了已经从数据源读取的数据。“重复”操作会重新从源读取完整的表格,这是不必要的。
一个新表已添加到 Power Query。
我将此表重命名为“MeasureList”。
我只需要 Measure 列。因此,我右键点击 Measure 列并点击“删除其他列”:

图 8 — 从引用表中删除所有列(图由作者提供)
我对零售销售表执行相同的步骤。不过这次,我将表格重命名为“零售度量”。
这个表将被附加到 MeasureList 表格。所以,我关闭了该表的加载功能:

图 9 — 禁用第二个引用表的 Power BI 加载(图由作者提供)
现在,我将第二个引用表附加到“MeasureList”表:

图 10 — 将第二张表附加到“MeasureList”表(图由作者提供)
我在以下对话框中选择了第二个表,“零售度量”。
这里的最后一步是从结果表中删除重复项:

图 11 — 从“MeasureList”表中删除重复项(图由作者提供)
结果是两个表中的所有 Measure 的组合列表:

图 12 — 结果的“MeasureList”表格(图由作者提供)
现在,我必须添加一个条件列来添加 MeasureSort 列:

图 13 — 添加条件列以创建 MeasureOrder(图由作者提供)
所有 Measure 的顺序如下:
-
“SalesAmount”: 1
-
“SalesQuantity”: 2
-
“ReturnAmount”: 3
-
“ReturnQuantity”: 4
-
“UnitPrice”: 5
-
“TotalCost”: 6
-
“UnitCost”: 7
-
“DiscountAmount”: 8
-
“DiscountQuantity”: 9
在我的案例中,Power Query 需要大量时间来获取所有数据。
原因是,我从操作中得到了两个巨大的表格。
让我们做一些数学计算:
在线销售表格有 1200 万行,再乘以九个 Measure 列,结果是超过 1.08 亿行。
零售销售表有将近 350 万行,再乘以九,我得到了 3150 万行。
当我们查看该模型的统计信息和性能时,我稍后会回到这些数字。
因此,我使用 DAX 计算 Factor 列来节省时间。
我本来会在 Power Query 中添加一个带有逻辑的条件列来设置正确的值,但为了节省时间,我没有这样做。
在 DAX 中添加计算列
我将 Factor 列作为计算列添加到新的 MeasureList 表中,使用 Power BI 进行计算。
由于我只有两个可能的值,所以我使用 IF() 来计算结果:
Factor = IF('MeasureList'[Measure] IN {"SalesAmount", "SalesQuantity", "UnitPrice"}, 1, -1)
隐藏 MeasureOrder 列后,MeasureList 表现在看起来是这样的:

图 14 — 完成的“MeasureList”表(图表由作者提供)
现在,我必须将 Measure 列的“Sort by Column”设置为 MeasureOrder 列:

图 15 — 通过 MeasureOrder 列设置顺序(图表由作者提供)
接下来,我从“MeasureList”表添加关系到“Online Sales”和“Retail Sales”表。
我从两个事实表中隐藏了 Measure 列。
我添加了两个基础 Measure 来汇总两个事实表中的值列:
Sum Online Value = SUM('Online Sales'[Value])
我必须将所有访问单独 Measure 列的 Measure 更改为过滤 Measure 的 Measure。
类似这样的:
Online Sales = CALCULATE([Sum Online Value])
,'MeasureList'[Measure] = "SalesAmount"
)
但是,要计算瀑布图的正确值,我需要在最终的 Measure 中包含按 Measure 计算的因子:
Value with Factor =
VAR Factor = SELECTEDVALUE('MeasureList'[Factor])
RETURN
[Sum Online Value] * Factor
现在我们可以创建瀑布图可视化。
可视化
我将瀑布图添加到一个新的报告页面。
我为 Y 轴设置了 Measure [Value with Factor],并将 MeasureList 表中的 Measure 列设置为分类:

图 16 — 设置瀑布图(图表由作者提供)
但现在,我们看到了“MeasureList”表中的所有 Measure。
由于我只想查看它们的子集,所以我使用筛选面板来缩小到所需的 Measure:

图 17 — 为瀑布图设置筛选器(图表由作者提供)
在添加了一个筛选器(Slicer),用于筛选 2008 年的日历年,并按 Measure 名称对图表进行排序后,我得到了我想要的结果(启用了数据标签并移除了轴标签等):

图 18 — 完成的瀑布图(图表由作者提供)
这几乎是我预期的结果。
不幸的是,无法将 Total 列重命名为“Margin”(例如)。
我试图在“Total”字样上方添加一个文本框,里面写着“Margin”。但首先,这是一个繁琐的任务(我必须匹配字体、字体大小和颜色,并且还得将其放置在正确的位置)。其次,当用户点击瀑布图时,它会自动切换到前面,遮住文本框。
另外,我写了一个好的标题,确保用户能理解他们看到的内容。
让我们来谈谈统计数据
在我们大声喊出“是的,这解决了我所有的问题,从今以后我只会这样做!”之前,让我们看看一些统计数据和性能指标。
我使用 Vertipaq Analyzer 分析原始和修改后的数据模型中的统计数据。
首先,保存的 pbix 文件大小差异并不大:
-
原始模型为 290 MB
-
修改后的模型为 340 MB
但是,当我查看来自 Vertipaq 分析的内存大小时,我得到不同的数字:
-
原始模型为 350 MB
-
修改后的模型为 950 MB
内存使用量几乎增加了三倍。
这表明数据无法像之前那样很好地压缩。
然后,当我们查看表格统计信息时,我们看到发生了什么:

图 19 — 两个模型的表格统计信息(图由作者提供)
在图片的顶部,你看到的是原始模型的统计数据,而下面是修改后的模型。
如前所示,行数是乘以度量列数的结果。
但是表格的大小几乎大了四倍。
当我更深入地分析数据时,我发现值列使用了更多的内存,但在线销售表的销售订单号、销售订单号和产品键列占用了整个数据库的 42.5%。在更改数据建模之前,这一比例要低得多。
我可以尝试更改表的顺序。然而,Power BI 通常会在分析数据并决定按照哪个列对数据进行排序以实现最佳压缩后做出较好的决策。
重新加载这两张表几乎需要一个小时,所以我现在不做了。
我可能会在未来做,并将结果发布在这里。
但是首先,让我们做一些性能测量。
我使用 DAX Studio 和这里描述的方法来获取性能数据:
## 如何使用 DAX Studio 获取 Power BI 的性能数据
有时我们会遇到报告变慢的情况,我们需要找出原因。我们将看到如何收集性能数据以及……
towardsdatascience.com
第一个测试是比较在线销售金额:
我从一个显示按年份和月份销售金额的可视化中获取查询。
原始数据模型的执行统计信息如下:

图 20 — 简单的销售金额计算(原始)(图由作者提供)
以下是修改后的数据模型的执行统计信息:

图 21 — 简单的销售金额计算(修改后的数据模型)(图由作者提供)
如你所见,总时间明显更高。当我们查看 SE 数字时,我们发现并行性比之前低得多,这表明该模型效率较低。
现在,我添加更多的度量。例如,利润率、去年、同比变化等等:

图 22 — 多度量计算(原始)(图由作者提供)
这是修改后的数据模型:

图 23 — 多度量计算(修改后的数据模型)(作者提供的图示)
正如你所看到的,两个数据模型之间的性能差异再次非常大。
而且这些只是非常简单的计算。
当我查看更复杂的计算时,差异会更大。我观察到性能差异为原来的 3 到 4 倍。
结论
虽然这种方法打开了多种可能性,但我只推荐在某些特定场景下使用它。
我建议仅在标准模型无法满足需求时使用这种方法。
性能下降的风险是显而易见的,不能忽视。
不要被这种思维误导:“我只有几千行数据。”
根据我的经验,Power BI 中的数据量并不重要。
更重要的是数据中的不同值的数量,也就是所谓的“基数”。
我们来看一下“订单编号”这一列。
每个订单都有不同的订单编号。因此,这一列的基数非常高,无法有效压缩。
然后,即便是几千行数据,复杂的计算也可能导致性能差。
因此,每种情况都不同,必须根据情况进行相应的测试。
无论如何,这条路径在某些时候是必须的,以允许像我案例中请求的图表。
我的一个客户有一份数据,他希望根据这些数据创建两个类似上面展示的瀑布图。
然而,他的业务案例中涉及的两张表有 25 个度量列,这意味着数据行数将增加 25 倍。
在这种情况下,他需要为八个度量创建两个瀑布图。我将保留原始表,并仅复制所需的数据来创建请求的图表。
因此,所有“正常”的计算将在能够满足其要求的同时,保持正常的性能。
另外一个采用这种方式的积极原因是,我可以将事实表(在 Power Query 中追加)合并成一张表。在这种情况下,我会在合并之前添加一个包含来源的列,例如“在线销售”和“零售销售”。这样可以简化数据模型。
但正如我们现在所知道的,我们需要进行测试,重复测试,并进行其他场景的测试,直到我们了解这种建模方法的后果。

由Jan Kopřiva拍摄,图片来源于Unsplash
参考文献
我使用的是 Contoso 示例数据集,正如我在之前的文章中所做的那样。你可以从微软这里免费下载 ContosoRetailDW 数据集。
Contoso 数据可以在 MIT 许可证下自由使用,具体描述请见这里。
[## 每当 Salvatore Cagliari 发布文章时,获取电子邮件通知。
每当 Salvatore Cagliari 发布文章时,都会收到电子邮件通知。通过注册,如果你还没有 Medium 账户,你将会创建一个账户…
medium.com](https://medium.com/subscribe/@salvatorecagliari?source=post_page-----772cd9510c31--------------------------------)
尽管 Medium 有付费墙,我还是让我的文章对每个人都可访问。这让我可以从每个读者那里赚取一些收入,但我将其关闭,以便你可以免费阅读我的文章。
你可以通过以下方式支持我的工作,这是我在空闲时间进行的工作,
buymeacoffee.com/salvatorecagliari
或者扫描这个二维码:

任何支持都将不胜感激,并帮助我找到更多时间为你创造更多内容。
非常感谢。
使用 Python 可视化濒危动物种群
快速成功的数据科学
数据新闻的最佳实践
·发表于Towards Data Science ·9 分钟阅读·2024 年 2 月 4 日
--

原始图片由 Mathias Appel 提供(CC0 1.0 通用)
数据新闻是一个使用数据分析、可视化和解读来讲述引人入胜且富有信息的故事的新闻领域。像数据科学家一样,数据记者利用数据和统计技术,发掘数据集中的趋势、模式和洞察。当做得好时,他们的工作为新闻报道提供了背景和深度。
2008 年,世界自然基金会日本开展了一项获奖印刷广告活动,使用图像像素化来描绘濒危物种的种群。每张濒危动物的图片都被逐渐降像素,直到其像素数量等于该物种现存个体的数量。图像像素化程度越高,画面越模糊,意味着剩余的动物越少。
这是一个很好的方式,能够引起人们对许多物种困境的关注,其他人也复制了这一广告活动。在这个快速成功的数据科学项目中,我们将编写代码,让你使用Pillow(Python Imaging Library (PIL)的分支)重现这一获奖技术。
我们还将完善这个过程,展示动物的原始图片和修改后的图片。当(遗憾地)动物在修改后的图片中已经无法辨认时,这种设计非常有用。下面是一个…
像专业人士一样可视化:为 Matplotlib 图表添加注释,讲述引人入胜的数据故事
理解并使用 Matplotlib 中的注释
·发布于 Towards Data Science ·10 分钟阅读·2024 年 5 月 28 日
--

照片由 Lukas Blazek 提供,来源于 Unsplash
Matplotlib 是 Python 中最重要的数据可视化库之一。它具有显著的灵活性和可定制性。这意味着你可以创建令人惊叹的可视化效果——但前提是你需要了解其内部工作原理。
在本教程中,我将指导你如何使用 Matplotlib 向你的可视化图表添加不同的注释。这将帮助读者更好地理解你的数据含义。
快来看看下面的预览,了解你在本教程中能创建的内容!
注意: 所有图片均由作者创作,除非另有注明。

我们的完整转型
加载示例数据集
在本教程中,我们将使用一个示例 Pandas 数据集,该数据集生成虚拟客户数据。每个数据点将包括:
-
该客户的交易次数,和
-
每次交易的金额。
可视化你的 RAG 数据——使用 Ragas 评估你的检索增强生成系统
如何使用 UMAP 降维技术展示嵌入的多个评估问题及其与源文档的关系,使用 Ragas、OpenAI、Langchain 和 ChromaDB
·发表于Towards Data Science ·13 分钟阅读·2024 年 3 月 3 日
--
检索增强生成(RAG)在大语言模型(LLM)的工作流程中增加了一个检索步骤,使其能够在回答问题时,从额外的来源(如私人文档)查询相关数据[1]。这一工作流程不需要对 LLM 进行昂贵的训练或微调来适应额外的文档。文档被拆分成片段,然后进行索引,通常使用紧凑的机器学习生成的向量表示(嵌入)。具有相似内容的片段会在这个嵌入空间中彼此接近。
RAG 应用将用户提供的问题投影到嵌入空间中,以根据其与问题的距离检索相关的文档片段。LLM 可以使用检索到的信息来回答查询,并通过展示片段作为参考来证明其结论。

通过 UMAP[3]降维对维基百科 F1 公式文章的嵌入空间进行迭代动画展示,并对聚类进行人工标注——由作者创建。
评估 RAG 应用程序是一个挑战[2]。存在不同的方法:一方面,有些方法需要开发者提供作为真实答案的基础;另一方面,答案(和问题)也可以由另一个 LLM 生成。支持 LLM 回答的最大开源系统之一是 Ragas[4](检索增强生成评估),它提供
-
基于文档生成测试数据的方法
-
基于不同的评估指标,逐步评估检索和生成步骤,以及端到端评估。
在本文中,你将学到
-
如何简要构建一个用于一级方程式的 RAG 系统(详细描述请参见上一篇文章可视化您的 RAG 数据——检索增强生成的 EDA)
-
生成问题和答案
-
使用Ragas评估 RAG 系统
-
最重要的是,如何使用Renumics Spotlight可视化结果并解释结果。
准备好你的环境
启动一个笔记本并安装所需的 Python 包
!pip install langchain langchain-openai chromadb renumics-spotlight
%env OPENAI_API_KEY=<your-api-key>
本教程使用以下 Python 包:
-
Langchain:一个集成语言模型和 RAG 组件的框架,使得设置过程更加顺畅。
-
Renumics-Spotlight:一个用于交互式探索非结构化机器学习数据集的可视化工具。
-
Ragas:一个帮助你评估 RAG 管道的框架
免责声明:本文的作者也是 Spotlight 的开发者之一。
为数据集准备文档和嵌入
你可以使用你自己的 RAG 应用程序,跳到下一部分学习如何评估、提取和可视化。
或者,你可以使用上一篇文章中的 RAG 应用程序,并使用我们准备的所有一级方程式维基百科文章的数据集。在这里,你还可以将自己的文档插入到‘docs/’子文件夹中。
本数据集基于维基百科的文章,并遵循创作共用署名-相同方式共享许可证。原始文章及作者列表可在相应的维基百科页面找到。
现在,你可以使用 Langchain 的DirectoryLoader加载所有来自 docs 子目录的文件,并使用RecursiveCharacterTextSpliter将文档分割为片段。使用OpenAIEmbeddings你可以创建嵌入并将其存储在ChromaDB作为向量存储。对于链本身,你可以使用 LangChain 的ChatOpenAI和ChatPromptTemplate。
本文的链接代码包含了所有必要的步骤,你可以在上一篇文章中找到对所有步骤的详细描述。
一个重要的点是,你应该使用哈希函数为ChromaDB中的片段创建 ID。这使得如果你只有包含其内容和元数据的文档时,仍然能够在数据库中找到嵌入。这使得可以跳过那些已经存在于数据库中的文档。
import hashlib
import json
from langchain_core.documents import Document
def stable_hash_meta(doc: Document) -> str:
"""
Stable hash document based on its metadata.
"""
return hashlib.sha1(json.dumps(doc.metadata, sort_keys=True).encode()).hexdigest()
...
splits = text_splitter.split_documents(docs)
splits_ids = [
{"doc": split, "id": stable_hash_meta(split.metadata)} for split in splits
]
existing_ids = docs_vectorstore.get()["ids"]
new_splits_ids = [split for split in splits_ids if split["id"] not in existing_ids]
docs_vectorstore.add_documents(
documents=[split["doc"] for split in new_splits_ids],
ids=[split["id"] for split in new_splits_ids],
)
docs_vectorstore.persist()
评估问题
对于像一级方程式这样的常见话题,你也可以直接使用 ChatGPT 生成一般性问题。在本文中,使用了四种问题生成方法:
-
GPT4:使用以下提示“写 30 个关于一级方程式的问题”,通过 ChatGPT 4 生成了 30 个问题。
– 随机示例:“哪支一级方程式车队以其跃马标志而闻名?”
-
GPT3.5:使用以下提示“写 100 个关于一级方程式的问题”,并重复“谢谢,请再写 100 个”通过 ChatGPT 3.5 生成了另外 199 个问题。
– 示例:“1950 年,哪位车手赢得了首届一级方程式世界锦标赛?”
-
Ragas_GPT4:使用 Ragas 生成了 113 个问题。Ragas 再次利用文档和其自己的嵌入模型构建一个向量数据库,然后用 GPT4 生成问题。
– 示例:“你能告诉我更多关于 1998 年世界锦标赛中,乔丹 198 一级方程式赛车表现的信息吗?”
-
Rags_GPT3.5:使用 Ragas 生成了 226 个额外的问题——这里我们使用 GPT3.5。
– 示例:“2014 年比利时大奖赛中发生了什么事件,导致汉密尔顿退赛?”
from ragas.testset import TestsetGenerator
generator = TestsetGenerator.from_default(
openai_generator_llm="gpt-3.5-turbo-16k",
openai_filter_llm="gpt-3.5-turbo-16k"
)
testset_ragas_gpt35 = generator.generate(docs, 100)
问题和答案没有经过任何审查或修改。所有问题都被合并到一个单一的数据框中,包含id、question、ground_truth、question_by和answer列。

接下来,问题将被提问给 RAG 系统。对于超过 500 个问题,这可能需要一些时间并产生费用。如果你按行提问,你可以暂停并继续处理,或者在崩溃后恢复,而不会丢失迄今为止的结果:
for i, row in df_questions_answers.iterrows():
if row["answer"] is None or pd.isnull(row["answer"]):
response = rag_chain.invoke(row["question"])
df_questions_answers.loc[df_questions_answers.index[i], "answer"] = response[
"answer"
]
df_questions_answers.loc[df_questions_answers.index[i], "source_documents"] = [
stable_hash_meta(source_document.metadata)
for source_document in response["source_documents"]
]
不仅答案会被存储,还会存储检索到的文档片段的源 ID,以及它们的文本内容作为上下文:

此外,所有问题的嵌入也会生成并存储在数据框中。这使得可以将其与文档一起可视化。
使用 Ragas 进行评估
Ragas提供了用于单独评估 RAG 管道中每个组件的指标,以及用于整体性能的端到端指标:
-
上下文精准度: 使用
问题和检索到的上下文,衡量信噪比。 -
上下文相关性: 衡量检索到的上下文与问题的相关性,通过
问题和上下文计算。 -
上下文召回: 基于
真实情况和上下文,检查是否检索到了所有相关的答案信息。 -
忠实度: 利用
上下文和答案,衡量生成的答案在事实上的准确性。 -
答案相关性: 使用
问题和答案计算,以评估生成的答案与问题的相关性(不考虑事实性)。 -
答案语义相似度: 使用
真实情况和答案评估生成答案与正确答案之间的语义相似性。 -
答案正确性: 依赖于
真实情况和答案,衡量生成的答案与正确答案的准确性和一致性。 -
方面评价: 分析
答案,根据预定义或自定义方面(如正确性或有害性)评估提交。
目前,我们关注的是答案正确性的端到端指标。数据框中的列名和内容已被复制并调整,以符合 Ragas API 的命名和格式要求:
# prepare the dataframe for evaluation
df_qa_eval = df_questions_answers.copy()
# adapt the ground truth to the ragas naming and format
df_qa_eval.rename(columns={"ground_truth": "ground_truths"}, inplace=True)
df_qa_eval["ground_truths"] = [
[gt] if not isinstance(gt, list) else gt for gt in df_qa_eval["ground_truths"]
]
这可能需要一些时间,甚至比仅仅查询你的 RAG 系统还要花费更多的金钱。让我们按行应用评估,以便在崩溃后能够恢复,而不丢失迄今为止的结果:
# evaluate the answer correctness if not already done
fields = ["question", "answer", "contexts", "ground_truths"]
for i, row in df_qa_eval.iterrows():
if row["answer_correctness"] is None or pd.isnull(row["answer_correctness"]):
evaluation_result = evaluate(
Dataset.from_pandas(df_qa_eval.iloc[i : i + 1][fields]),
[answer_correctness],
)
df_qa_eval.loc[i, "answer_correctness"] = evaluation_result[
"answer_correctness"
]
之后,你可以将结果存储到df_questions_answer数据框中:
df_questions_answers["answer_correctness"] = df_qa_eval["answer_correctness"]
准备可视化
为了在可视化中包含文档片段,我们将文档中的引用添加到使用该文档作为来源的问题中。此外,还会存储引用某文档的问题的数量:
# Explode 'source_documents' so each document ID is in its own row alongside the question ID
df_questions_exploded = df_qa_eval.explode("source_documents")
# Group by exploded 'source_documents' (document IDs) and aggregate
agg = (
df_questions_exploded.groupby("source_documents")
.agg(
num_questions=("id", "count"), # Count of questions referencing the document
question_ids=(
"id",
lambda x: list(x),
), # List of question IDs referencing the document
)
.reset_index()
.rename(columns={"source_documents": "id"})
)
# Merge the aggregated information back into df_documents
df_documents_agg = pd.merge(df_docs, agg, on="id", how="left")
# Use apply to replace NaN values with empty lists for 'question_ids'
df_documents_agg["question_ids"] = df_documents_agg["question_ids"].apply(
lambda x: x if isinstance(x, list) else []
)
# Replace NaN values in 'num_questions' with 0
df_documents_agg["num_questions"] = df_documents_agg["num_questions"].fillna(0)
现在将问题的数据框与文档的数据框进行连接
df = pd.concat([df_qa_eval, df_documents_agg], axis=0)
此外,让我们准备一些不同的 UMAP [3] 映射。你也可以稍后在 Spotlight GUI 中做同样的事情,但事先做这一步可以节省时间。
-
umap_all: 对所有文档和问题嵌入应用了拟合和变换的 UMAP
-
umap_questions: 仅对问题嵌入应用了拟合的 UMAP,并对两者应用了变换
-
umap_docs: 仅对文档嵌入应用了拟合的 UMAP,并对两者应用了变换
我们准备每个 UMAP 变换的方式如下:
umap = UMAP(n_neighbors=20, min_dist=0.15, metric="cosine", random_state=42).fit
umap_all = umap.transform(df["embedding"].values.tolist())
df["umap"] = umap_all.tolist()
对于每个文档片段,另一个有趣的指标是它的嵌入与最近问题的嵌入之间的距离
question_embeddings = np.array(df[df["question"].notna()]["embedding"].tolist())
df["nearest_question_dist"] = [ # brute force, could be optimized using ChromaDB
np.min([np.linalg.norm(np.array(doc_emb) - question_embeddings)])
for doc_emb in df["embedding"].values
]
这个指标对于找到未被问题引用的文档很有帮助。

可视化结果
如果你跳过了前面的步骤,你可以下载数据框并使用以下命令加载它:
import pandas as pd
df = pd.read_parquet("df_f1_rag_docs_and_questions.parquet")
并启动Renumics Spotlight进行可视化:
from renumics import spotlight
spotlight.show(df)
spotlight.show(
df,
layout="/home/markus/Downloads/layout_rag_1.json",
dtype={x: Embedding for x in df.keys() if "umap" in x},
)
它将打开一个新的浏览器窗口:

公式一文档与评估问题的统计与相似性图 — 由作者使用Renumics Spotlight创建
在左上方,您可以看到一个所有问题和所有文档片段的表格。您可以使用“可见列”按钮来控制数据表中显示哪些列。直接创建一个筛选器,选择仅显示问题,从而在可视化中能够打开和关闭问题:选择所有问题,然后使用“从选定行创建筛选器”按钮来创建筛选器。
在表格的右侧,answer correctness 以指标形式显示在所有问题中。下面有两个直方图;左边的显示了按不同问题生成方法划分的answer correctness分布。右边的显示了问题生成方法的分布。在此,如果需要,可以使用筛选按钮为问题创建筛选器,仅显示选定的行(即问题)。
右侧有两个相似性图。第一个使用umap_questions列,并显示基于仅应用于问题的转换(umap_questions)的问题和文档。它有助于独立于关联文档查看问题的分布,因为这种方法允许分析人员识别问题本身中的模式或聚类。
第二个相似性图显示了基于仅应用于文档的转换(umap_docs)的问题和文档。这有助于在文档的上下文中查看问题。当问题数量较多时,同时转换问题和文档的相似性图显示效果较差,因为问题会聚集在一起或分离开,往往与文档分开。因此,这里省略了这种表示方式。

公式一评估问题统计与相似性图 — 由作者使用Renumics Spotlight创建
文档嵌入相似性图:观察
在相似性图umap_docs中,您可以识别出文档嵌入空间中没有邻近问题的区域。当选择nearest_question_dist进行着色时,这一点会更加明显。

公式一文档与问题的相似性图(高亮显示)— 由作者使用Renumics Spotlight创建
可以识别出一些集群,其中包含仅包含标题的片段或包含仅有数字的表格数据,逐页拆分后其意义丧失。此外,许多特定于维基百科的文本添加,如指向其他语言的链接或编辑注释,形成了没有相邻问题的集群。
使用 Wikipedia API 删除维基百科相关文本中的噪音非常简单。可能并不是特别必要,因为它主要占用了一些空间——并不预期会显著恶化 RAG 的结果。然而,RAG 系统很难捕捉到包含在大型表格中的数据,因此使用高级预处理方法进行表格提取并将其连接到 RAG 系统可能是有益的。
在umap_docs相似性图中,另一个可以观察到的点是来自不同来源的问题的分布情况。


左:由 ChatGPT 生成的问题(GPT-3.5 和 GPT-4),右:通过 ragas 生成的问题,使用 GPT-3.5 和 GPT-4 — 由作者使用Renumics Spotlight创建
直接由 ChatGPT 生成(GPT-3.5,GPT-4)的问题位于中心的一个更局限的区域,而基于文档通过 ragas 生成的问题则覆盖了更大的区域。
答案正确性直方图
直方图可以作为起点,帮助初步了解数据的全局统计信息。总体而言,所有问题的答案正确性为 0.45。对于没有使用 ragas 生成的问题,答案正确性为 0.36,而使用 ragas 生成的问题则为 0.52。预计系统在使用 ragas 生成的问题上的表现会更好,因为这些问题是基于可用数据生成的,而直接由 ChatGPT 生成的问题可能来自 ChatGPT 训练时所使用的所有数据。

按问题来源着色的答案正确性直方图 - 由作者创建
对一些问题/答案及其真实答案的快速随机人工审查显示,在答案正确性为 0.3 到 0.4 的区间内,大多数问题仍然根据真实答案被正确回答。在 0.2 到 0.3 的区间内,出现了许多错误答案。在 0.1 到 0.2 的区间内,大多数答案是错误的。值得注意的是,几乎所有该范围内的问题都来自 GPT-3.5。虽然这两个由 GPT-4 生成的问题的答案正确性低于 0.2,但它们仍然被正确回答。
问题嵌入相似性图:观察结果
问题嵌入相似性图可以帮助通过检查可能导致相似问题的相似问题集群,深入挖掘答案正确性。
-
聚类“车手/过程/汽车的术语:” 平均
回答正确率0.23:答案往往不够精确。例如,底盘调校与底盘弯曲,或刹车调校与刹车偏向调节。是否这些类型的问题适合用来评估系统尚存疑问,因为似乎很难判断这些答案的正确性。 -
聚类“燃料策略的术语:” 平均
回答正确率0.44,类似于全局回答正确率。 -
聚类“赛道名称:” 平均
回答正确率0.49,类似于全局回答正确率。 -
聚类“谁保持记录…”:平均
回答正确率0.44,类似于全局回答正确率。 -
聚类“赢得冠军的…”:平均
回答正确率0.26 —— 看起来具有挑战性。带有多个条件的问题,例如:“唯一获胜的车手是谁,他拥有英国赛车执照,驾驶意大利车队的赛车,搭载美国发动机。” 类似多查询的扩展 RAG 方法可能有助于改进这一点。 -
聚类“唯一获胜的车手是…并且车上挂有号码
” :平均回答正确率0.23 —— 看起来 GPT-3.5 在这里有些懒惰,尽管大多数真实答案是错误的,但它反复使用相同的问题并更改数字!

公式一问题的相似性图(高亮部分)与文档 —— 作者制作
结论
总结而言,利用基于 UMAP 的可视化方法提供了一种有趣的方式,能够深入挖掘,远超单纯的全局度量分析。文档嵌入相似性图提供了一个良好的概览,展示了相似文档的聚类及其与评估问题的关系。问题相似性图揭示了模式,允许通过质量度量区分和分析问题,从而生成洞察。请参阅“可视化结果”部分,将可视化应用于你的评估策略——你将发现什么洞察?
我是一位专业人士,专注于为非结构化数据的互动探索创建先进的软件解决方案。我撰写关于非结构化数据的文章,并使用强大的可视化工具进行分析,以便做出明智的决策。
参考文献
[1] Yunfan Gao, Yun Xiong, Xinyu Gao, Kangxiang Jia, Jinliu Pan, Yuxi Bi, Yi Dai, Jiawei Sun, Qianyu Guo, Meng Wang, Haofen Wang: Retrieval-Augmented Generation for Large Language Models: A Survey (2024), arxiv
[2] Yixuan Tang, Yi Yang: MultiHop-RAG: Benchmarking Retrieval-Augmented Generation for Multi-Hop Queries (2021), arXiv
[3] Leland McInnes, John Healy, James Melville: UMAP: Uniform Manifold Approximation and Projection for Dimension Reduction (2018), arXiv
[4] Shahul Es, Jithin James, Luis Espinosa-Anke, Steven Schockaert: RAGAS: 检索增强生成的自动评估 (2023), arXiv
使用 PyDeck 可视化 3D 空间数据

如何在 Python 中创建建筑模型地图
·发表于Towards Data Science ·阅读时间 5 分钟·2024 年 7 月 29 日
--
在这篇文章中,我想向大家介绍一个名为PyDeck的 Python 包,这是一个用于在 Python 中创建 3D 地图的好工具。作为示例,我将使用一个覆盖布达佩斯市的 3D 建筑模型数据库。
所有图像均由作者创建。
访问建筑数据
数据来自公开的布达佩斯开放数据地图,你可以在这里访问该数据。虽然该网站是匈牙利语的,但一旦你找到‘Épületmagasság’部分,并点击以‘geojson’开头的按钮,你应该能够下载 geojson 文件,文件内容完全可以用英语和 Python 读取。

来源
在快速下载后,让我们打开文件并快速查看:
# import geopandas
import geopandas as gpd
# parse the building height data set
gdf = gpd.read_file('epuletmagassag.geojson')
gdf.plot()
len(gdf)
这个单元的输出:
用 LLMs 可视化和整合复杂想法,第一部分:Napkin AI
发现 AI 工具如何将复杂的概念转化为清晰、实用的框架和图表
·发表于 Towards Data Science ·4 分钟阅读·2024 年 7 月 16 日
--

图片由作者提供,使用 Napkin AI 制作
介绍
像 Chat GPT 这样的 AI 工具正在改变我们处理复杂想法的方式。我喜欢用它做的一件事是整合来自不同思想家的视角和想法,并区分它们,以更好地理解它们的细微差别。这无疑是我最喜欢的 AI 应用之一。

图片由作者提供,使用 Napkin AI 制作
动机
Napkin AI 吸引了我的注意,因为它可以根据文本输入自动生成有趣的图表,使其高度灵活且易于使用。我一直在寻找好的概念图和知识图谱软件,而这个工具看起来是一个不错的起点。

图片由作者提供,使用 Napkin AI 制作
目标
这篇文章是我将探索不同方法来检查、整合和可视化复杂思想和视角的系列文章中的第一篇。虽然我现在已经开始创建自己的工具,但实验使用 Napkin AI 是一个重要的第一步。在这个系列中,我将记录这段旅程,探索一些现有工具,制作新的工具易于使用,并分享我的经验。

图片由作者提供,使用 Napkin AI 制作
过程
1. 设置提示词
在进行一些基本设置后,Napkin AI 要求我提供一个提示词。我从当时我正好在探索的内容开始:
“艾伦·瓦茨的最独特见解”
它生成了一些基本信息,可能和 Chat GPT-4 差不多强大。我预览了一些图表后,决定进一步挑战它:
“艾伦·瓦茨(Alan Watts)、罗伯特·M·皮尔西格(Robert M. Pirsig)、斯宾诺莎(Spinoza)和马库斯·奥勒留(Marcus Aurelius)的独特洞察,强调它们彼此的重叠,以及它们在哲学、科学和心理学中非常具体的思想基础,整理成实际的框架,并朝着这些框架的逻辑进展。”

图片由作者提供,使用 Napkin AI 制作
结果更多是面向创建框架的过程,从广义上讲这很好,但我希望它能更进一步。启动一个新的 Napkin 并更改一些提示词,以便更多地聚焦于构建我想要的内容,其实非常简单:
…整理成一个单一的、按步骤迭代的实际框架。
2. 创建和定制视觉效果
有许多模板可供选择,还有几种风格。每个选择的内容略有不同,有时会进入不同的细节层次,反映出图表的格式。
我选择了我认为最适合每个部分的图表模板。我倾向于选择几个最适合这个项目的模板,但我还是选择了在一致性和多样性之间找到平衡。以下是一些示例:




图片由作者提供,使用 Napkin AI 制作
3. 发布输出
完成每个文档后,我将它们导出为 PDF 格式。可以在这里找到它们。
反思与应用
我确实很高兴我决定使用这个工具。我所创建的并不是我最初想要的概念图,但它确实帮助我可视化了复杂的主题。通过其“AI 草图”选项,它甚至帮助我将这些主题整合成实际的框架,尽管你也可以开始一个空白文档并提供自己的文本。如果你想编辑文档或添加自己的图片,那也很容易。

图片由作者提供,使用 Napkin AI 制作
我认为这个工具可以在多种呈现场合中使用,包括商业、教育、个人成长和内容创作。它使个人创作者能够将复杂的想法转化为实际的框架,支持创新和成长。最重要的是,它创造了强大的图表,对于商业沟通和战略发展非常有用。
下一步
这是一个很好的开始,但我还有更多内容要分享。除了使用其他工具进行图表绘制外,我正在开发一个定制工具,旨在更好地集成和可视化复杂的想法,包括其各个组件之间的详细关系。本系列将涵盖这些过程,敬请关注!

图片来源:作者,由 Napkin AI 制作
资源
所有图片由作者在 Napkin AI 中制作
在基于代理的模型中可视化动态行为

并在过程中遇到涌现的复杂性
·发表于 Towards Data Science ·阅读时间 9 分钟 ·2024 年 3 月 5 日
--
在我进行博弈论问题中战略知识提取优化的研究时,我最近意识到我需要一种更好的方式来简单直观地可视化具有定义的动态行为的代理行为。
这促使我建立了一个简单的库,用于通过 PyPlot 将代理行为可视化为动画。但在进入教程之前,先简要回顾一下这里涉及的核心概念。
动态代理模型的快速入门
基于代理的建模(ABM)提供了一种优秀的方式来模拟许多博弈论环境中的玩家。它让我们能够模拟和研究每个个体玩家的行为和认知过程,而不仅仅是分析趋势。当使用简化的二元状态机(如细胞自动机网格)表示代理不切实际时,ABM 让我们通过在一个维度空间中表示代理的位置来捕捉场景,在这个空间中,每个维度都有自己独特的规则。
代理的位置和状态空间
通过利用空间维度和状态维度,我们可以准确地建模接近度,并结合允许细微交互的属性,比如特征的相似性。此外,将代理的“位置”存储在状态空间中,让我们能够跟踪并比较代理的详细和动态状态信息(例如,在一个足球模型中玩家的疲劳状态)。
与网络模型相比,后者通过两个对象之间的连接来表示关系,而位置-状态-空间信息则允许我们定义并探索代理之间更复杂、更高维的关系。在这种范式中,欧几里得距离在状态空间中的度量可以表示两个代理之间状态相似度的强度。
用于行为和交互的动力学系统理论
我们可以通过使用动力学系统理论来描述一个代理的状态如何影响其他代理的状态,从而使这种模型变得更加强大。它提供了一种强有力的方法来定义系统随时间演化的规则。
在社会经济系统中,微分方程可以用来模拟交流、模仿以及更复杂的适应性过程,并且描述个体行为如何产生群体动态。
在时间和空间中的可视化需求
对于我的问题,我希望能够一目了然地看到每个代理的位置及其随时间的移动,并且展示一些基本的状态信息(即每个代理的一个或两个变量)。
目前,有几个库满足了可视化代理基础模型(ABM)的需求,包括 NetLogo 和 Mesa(Python 版)。然而,它们主要是为了可视化离散空间模型,而非连续空间模型。对于我的目的,我对后者更感兴趣,因此开始了这个附带任务。如果你想跟着一起学习或亲自测试代码,所有代码都存储在github.com/dreamchef/abm-viz。
在 Python 中构建类和动画
首先,我需要表示和存储代理和世界的状态及其动态。我选择使用 Python 类来实现这一点。我定义了一个 Agent 类,并为其设置了一组我认为对许多可能的建模任务有用的变量。我还为每个代理在类结构内定义了一个可绘制的圆形对象(使用 PyPlot)。
class Agent:
def __init__(self, index, position, velocity, empathy=1, ... dVision=0, age=0, plotSize=10):
self.index = index
self.velocity = np.array(velocity)
self.empathy = empathy
...
self.dVision = dVision
self.color = HSVToRGB([self.species,1,1])
self.plotSize=plotSize
self.pltObj = plt.Circle(self.position, self.plotSize, color=self.color)
然后,在进行了一些实验后,我发现按照良好的面向对象编程原则,定义一个世界(想象成游戏或系统)类是最好的做法。在这个类的结构中,既包括了世界的状态信息,也包含了图形和坐标轴的信息:
class World:
def __init__(self,population=1,spawnSize=400,worldSize=1200,worldInterval=300,arrows=False,agentSize=10):
self.agents = []
self.figure, self.ax = plt.subplots(figsize=(12,8))
self.ax.set_xlim(-worldSize/2, worldSize/2)
self.ax.set_ylim(-worldSize/2, worldSize/2)
self.worldInterval = worldInterval
self.worldSize = worldSize
self.arrows = arrows
self.agentSize = agentSize
for i in range(population):
print(i)
newAgent = Agent(index=i, position=[rand()*spawnSize - spawnSize/2, rand()*spawnSize - spawnSize/2],
velocity=[rand()*spawnSize/10 - spawnSize/20, rand()*spawnSize/10 - spawnSize/20], plotSize = self.agentSize)
self.agents.append(newAgent)
self.ax.add_patch(newAgent.pltObj)
print('Created agent at',newAgent.position,'with index',newAgent.index)
self.spawnSize = spawnSize
我编写这个类是为了让程序员能够简单地指定所需的代理数量(以及世界大小、生成区域大小等其他参数),而不是在主程序中手动创建和添加代理。
在定义了这个基本结构并且在世界中生成了具有随机位置和初始速度的代理后,我使用了 PyPlot 库的动画功能来创建一个方法,开始进行可视化:
def start(self):
ani = animation.FuncAnimation(self.figure, self.updateWorld, frames=100, interval=self.worldInterval, blit=True)
plt.show()
这个函数引用了存储在 World 实例中的图形、动画的速度和时长的规格,以及一个更新函数,后者也在 World 类中定义:
def updateWorld(self,x=0):
pltObjects = []
for agent in self.agents:
agent.updatePosition(self.agents, self.worldSize)
agent.pltObj.center = agent.position
pltObjects.append(agent.pltObj)
if self.arrows is True:
velocityArrow = plt.Arrow(agent.position[0], agent.position[1], agent.velocity[0], agent.velocity[1], width=2, color=agent.color)
self.ax.add_patch(velocityArrow)
pltObjects.append(velocityArrow)
return pltObjects
World.updatePosition 函数仅将每个代理的静态速度添加到当前位置。这项初步工作能够生成像这样的简单动画:

恒定速度
有了这个基本功能,我们现在希望能够可视化更有趣的动态。
可视化“社会”动态行为
我首先选择定义一个动态,其中每个代理根据周围其他代理的平均运动方向改变自己的运动方向。用方程形式表示:

我通过 Agent.updatePosition() 和 Agent.updateVelocity() 方法在 Python 中编码了该动态,这些方法在每一帧动画中运行:
def updatePosition(self, agents, worldSize):
self.updateVelocity(agents)
self.position += self.velocity
...
def updateVelocity(self, agents):
herd_velocity = self.herdVelocity(agents)
herd_magnitude = np.linalg.norm(herd_velocity)
self_magnitude = np.linalg.norm(self.velocity)
if herd_magnitude > 0.1:
herd_unit_velocity = herd_velocity/herd_magnitude
self.velocity += herd_unit_velocity
在下面的 PyPlot 动画中,代理从不同的速度开始,但很快就调整并开始朝同一方向移动。在这种情况下,平均方向最初大致是 Y 方向上的向上。

向上汇聚
这一次,群体初始化时大致以向左的速度运动,并且有一个“滞后者”,它很快调整过来。

单个滞后者向左汇聚
显示速度向量
接下来,我意识到将代理的速度显示得更加清晰会很有帮助,所以我实现了箭头来显示每个代理的大小和方向:
velocityArrow = plt.Arrow(agent.position[0], agent.position[1], agent.velocity[0], agent.velocity[1], width=2, color=agent.color)
self.ax.add_patch(velocityArrow)
pltObjects.append(velocityArrow)
这一修改带来了更有帮助的动画,例如这个。我们仍然可以一眼看到速度汇聚的动态,但现在可以更清楚地看到加速度的变化率。

速度箭头
对于上面的动画,我还调整了动态,使其依赖于一个视距变量。换句话说,代理只会调整它们的速度,以匹配附近的代理(在这种情况下为 300 单位以内)。
我还修改了代码,使得每个代理只修改其运动方向,而不是速度。请在下一个部分中记住这一点。
更复杂的状态—逐个动态
到目前为止,我只实现了考虑每个代理位置和速度的动态。但正如我在概述部分提到的,考虑非空间的状态信息也可以使我们的建模方法更加具有广泛的适用性。
使用代理的色调和饱和度来表示状态变量
我利用了一个与每个代理的 RGB 颜色相关联的辅助状态。预示着我研究的进化博弈理论目标,我将其称为代理的“物种”,并如下实现:
在 Agent.init 方法中,我添加了随机物种生成,并将个体映射到代理在图中的标记颜色:
self.species = rand()
self.color = HSVToRGB([self.species,1,1])
self.pltObj = plt.Circle(self.position, self.plotSize, color=self.color)
具体来说,我将这个分配给了物体的色调,将饱和度(大致是灰度)保留给其他潜在有趣的变量(例如剩余寿命或健康)。这种将色调和饱和度分开的做法在复数值函数的可视化中已有先例,我在这篇文章中有详细介绍。
加速度对物种状态变量的依赖
在自然界中,群体行为通常发生在同一物种的动物之间,而不同物种之间则不会发生这种行为。基于这一点,我决定将我们的玩具动力学修改为考虑物种的因素。这一改变意味着,每个代理只会对世界中足够接近且物种相似的代理(即颜色相似的圆圈)调整方向。
现在,事情开始变得非常,非常有趣了。在继续阅读之前,问问自己:
- 你期待在下一个动画中看到什么样的行为?
一个相对天真的实验者(比如大多数时候的我)会期望代理们按物种组织自己,并成群结队地移动,每群代理通常朝不同方向移动,并大多数情况下互不理睬。为了看看我们是否正确,接着往下看。
为了编码这种行为,我修改了与代理相关的群体速度计算,方式如下:
herd_velocity += neighbor.velocity * (0.5-np.sqrt(abs(self.species-neighbor.species)))
这一变化导致了像这样的动画。为了看到其中有趣的行为,我在动画开始大约 20 秒后开始录制。

涌现行为
如果行为没有立刻显现出来也没关系。这是微妙的。如果你认为自己预测正确,做得很好!
结果是,代理们并没有很好地按物种组织成群。相反,唯一似乎聚在一起的代理是那些既有相似物种又有相似旅行速度的代理。你可以最常见地看到这种现象发生在速度较慢的绿色代理、那对蓝色代理,以及在屏幕底部快速移动的蓝色和紫色代理之间。值得注意的是,代理似乎在“选择”与谁一起旅行时,更看重速度而非物种相似性。你可以在浅蓝色、深蓝色,甚至紫色代理之间最常看到这种现象。
这在我们定义的动态中是完全合理的,因为每个代理的速度是恒定的,速度不同的代理最终会落后或者甩开他们的同伴。然而,这有点出乎意料。
从根本上讲,这是因为行为是涌现的。换句话说,我们并没有明确告诉代理按这种方式作为一个群体来表现。相反,他们从我们给每个代理的相同编码格式的简单指令中“推导”出了这种行为。
可视化、涌现与数据洞察
我们开始这段旅程的简单目标是可视化一个 ABM,以确保我们设置的动态在高层次上能够按预期工作。但除了实现这一目标外,我们还偶然发现了一个可能在创建模型时根本没有考虑过的涌现行为。
这说明了在数据科学、模拟和建模领域,关于可视化的重要合作关系。通过对模型或数据集的新视角,可以加速发现复杂系统中的涌现行为。这不仅仅适用于基于代理的建模,它同样适用于在数据科学的其他领域获得洞见。而且,创建新的富有创意的可视化图像提供了一种确保获得此类视角的有效途径。
如果你想进一步尝试这个模型和可视化,你可以在github.com/dreamchef/abm-viz获取代码。我很想听听你发现的其他内容,或者对我的工作的看法,欢迎在评论中告诉我!也可以随时通过Twitter和LinkedIn与我联系。祝你有个愉快的一天!
除非另有说明,所有图像和动画均由作者创建。
可视化珠穆朗玛峰探险
一项端到端的数据可视化项目
·发表于《Towards Data Science》 ·18 分钟阅读·2024 年 1 月 10 日
--

我喜欢欣赏其他人的数据可视化作品,尤其是当他们也注重设计时,像是Giorgia Lupi、Nadieh Bremer(我尤其喜欢 Nadieh 的《拼接王国》),以及Shirley Wu。Nadieh 和 Shirley 是《Data Sketches》一书的作者,我非常欣赏这本书中的一个特点,那就是 Nadieh 和 Shirley 详细解释了每个可视化背后的过程,从初始构想到最终可视化,突出技术细节和遇到的问题。我很感激他们如何解开这一过程的神秘感,并且本着解谜的精神,我也在此分享上述数据可视化背后的过程。
获取数据
在寻找与登山相关的数据集时,我偶然发现了喜马拉雅数据库:
这是一本包含所有曾在尼泊尔喜马拉雅山脉攀登过的探险记录的汇编。该数据库基于长期驻扎在加德满都的记者伊丽莎白·霍利的探险档案,并补充了来自书籍、高山期刊以及与喜马拉雅登山者的通信信息。
获取数据有两种主要方式:
- 下载带有嵌入数据的应用程序(在 Mac 上执行此操作需要安装兼容性…)
可视化 Torch 中的梯度下降参数
探索界面背后,查看 SGD 参数对模型训练的影响
·发表于 Towards Data Science ·阅读时长 7 分钟·2024 年 2 月 26 日
--
现代机器学习框架的简单界面背后隐藏着大量的复杂性。由于有许多参数和控件暴露给我们,如果我们不了解其背后的原理,就很容易陷入盲目编程的误区。考虑一下 Torch 的 随机梯度下降(SGD)优化器的众多参数:
def torch.optim.SGD(
params, lr=0.001, momentum=0, dampening=0,
weight_decay=0, nesterov=False, *, maximize=False,
foreach=None, differentiable=False):
# Implements stochastic gradient descent (optionally with momentum).
# ...
除了熟悉的学习率 lr 和 momentum 参数,还有一些其他参数对神经网络训练有着显著的影响。在本文中,我们将可视化这些参数对一个简单机器学习目标的影响,并使用各种损失函数。
玩具问题
首先,我们构造了一个简单的玩具问题,即在一组点上执行线性回归。为了增加趣味性,我们使用了一个二次函数加噪声,这样神经网络就必须做出权衡——同时我们也能观察到更多损失函数的影响:

我们首先仅使用 numpy 和 matplotlib 来可视化数据——此时还不需要 torch:
import numpy as np
import matplotlib.pyplot as plt
np.random.seed(20240215)
n = 50
x = np.array(np.random.randn(n), dtype=np.float32)
y = np.array(
0.75 * x**2 + 1.0 * x + 2.0 + 0.3 * np.random.randn(n),
dtype=np.float32)
plt.scatter(x, y, facecolors='none', edgecolors='b')
plt.scatter(x, y, c='r')
plt.show()

图 1. 玩具问题集的点。
接下来,我们将引入 torch 并介绍一个简单的训练循环,训练一个单神经元网络。为了在我们变化损失函数时得到一致的结果,我们每次都从相同的参数集开始训练,并将神经元的第一个“猜测”设定为方程 y = 6*x — 3(我们通过神经元的 weight 和 bias 参数实现这个目标):
import torch
model = torch.nn.Linear(1, 1)
model.weight.data.fill_(6.0)
model.bias.data.fill_(-3.0)
loss_fn = torch.nn.MSELoss()
learning_rate = 0.1
epochs = 100
optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate)
for epoch in range(epochs):
inputs = torch.from_numpy(x).requires_grad_().reshape(-1, 1)
labels = torch.from_numpy(y).reshape(-1, 1)
optimizer.zero_grad()
outputs = model(inputs)
loss = loss_fn(outputs, labels)
loss.backward()
optimizer.step()
print('epoch {}, loss {}'.format(epoch, loss.item()))
运行这个过程会输出文本,显示损失值正在减少,最终达到预期的最小值:
epoch 0, loss 53.078269958496094
epoch 1, loss 34.7295036315918
epoch 2, loss 22.891206741333008
epoch 3, loss 15.226042747497559
epoch 4, loss 10.242652893066406
epoch 5, loss 6.987757682800293
epoch 6, loss 4.85075569152832
epoch 7, loss 3.4395809173583984
epoch 8, loss 2.501774787902832
epoch 9, loss 1.8742430210113525
...
epoch 97, loss 0.4994412660598755
epoch 98, loss 0.4994412362575531
epoch 99, loss 0.4994412660598755
为了可视化我们的拟合,我们从神经元中取出学习到的偏差和权重,并将拟合结果与数据点进行比较:
weight = model.weight.item()
bias = model.bias.item()
plt.scatter(x, y, facecolors='none', edgecolors='b')
plt.plot(
[x.min(), x.max()],
[weight * x.min() + bias, weight * x.max() + bias],
c='r')
plt.show()

图 2. 在玩具问题上 L2 学习到的线性边界。
可视化损失函数
上述结果看起来是合理的,但到目前为止,一切都是通过高层次的 Torch 函数处理的,如optimizer.zero_grad()、loss.backward()和optimizer.step()。为了理解接下来的步骤,我们需要可视化模型在损失函数中的变化过程。为了可视化损失,我们将在 101×101 的网格中对其进行采样,然后使用imshow进行绘制:
def get_loss_map(loss_fn, x, y):
"""Maps the loss function on a 100-by-100 grid between (-5, -5) and (8, 8)."""
losses = [[0.0] * 101 for _ in range(101)]
x = torch.from_numpy(x)
y = torch.from_numpy(y)
for wi in range(101):
for wb in range(101):
w = -5.0 + 13.0 * wi / 100.0
b = -5.0 + 13.0 * wb / 100.0
ywb = x * w + b
losses[wi][wb] = loss_fn(ywb, y).item()
return list(reversed(losses)) # Because y will be reversed.
import pylab
loss_fn = torch.nn.MSELoss()
losses = get_loss_map(loss_fn, x, y)
cm = pylab.get_cmap('terrain')
fig, ax = plt.subplots()
plt.xlabel('Bias')
plt.ylabel('Weight')
i = ax.imshow(losses, cmap=cm, interpolation='nearest', extent=[-5, 8, -5, 8])
fig.colorbar(i)
plt.show()

图 3. 在玩具问题上的 L2 损失函数。
现在我们可以在运行梯度下降时捕捉模型参数,以展示优化器的表现:
model = torch.nn.Linear(1, 1)
...
models = [[model.weight.item(), model.bias.item()]]
for epoch in range(epochs):
...
print('epoch {}, loss {}'.format(epoch, loss.item()))
models.append([model.weight.item(), model.bias.item()])
# Plot model parameters against the loss map.
cm = pylab.get_cmap('terrain')
fig, ax = plt.subplots()
plt.xlabel('Bias')
plt.ylabel('Weight')
i = ax.imshow(losses, cmap=cm, interpolation='nearest', extent=[-5, 8, -5, 8])
model_weights, model_biases = zip(*models)
ax.scatter(model_biases, model_weights, c='r', marker='+')
ax.plot(model_biases, model_weights, c='r')
fig.colorbar(i)
plt.show()

图 4. 可视化梯度下降在损失函数下的变化。
从检查中可以看出,这正是应该发生的:模型从我们强制初始化的参数(-3, 6)开始,逐步朝梯度方向采取更小的步骤,最终在全局最小值处停住。
可视化其他参数
损失函数
现在我们将开始检查其他参数对梯度下降的影响。首先是损失函数,我们使用的是标准的 L2 损失:

L2 损失(torch.nn.MSELoss)累积平方误差。来源:link。作者截图。
但是,我们还可以使用其他几种损失函数:

L1 损失(torch.nn.L1Loss)累积绝对误差。来源:[link](https://pytorch.org/docs/stable/generated/torch.nn.L1Loss.html)。作者截图。

Huber 损失(torch.nn.HuberLoss)对小误差使用 L2,对大误差使用 L1。来源:[link](https://pytorch.org/docs/stable/generated/torch.nn.HuberLoss.html)。作者截图。

平滑 L1 损失(torch.nn.SmoothL1Loss)大致相当于 Huber 损失,但有一个额外的 beta 参数。来源:[link](https://pytorch.org/docs/stable/generated/torch.nn.SmoothL1Loss.html)。作者截图。
我们将到目前为止所做的工作包装在一个循环中,尝试所有损失函数并一起绘制它们:
def multi_plot(lr=0.1, epochs=100, momentum=0, weight_decay=0, dampening=0, nesterov=False):
fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2)
for loss_fn, title, ax in [
(torch.nn.MSELoss(), 'MSELoss', ax1),
(torch.nn.L1Loss(), 'L1Loss', ax2),
(torch.nn.HuberLoss(), 'HuberLoss', ax3),
(torch.nn.SmoothL1Loss(), 'SmoothL1Loss', ax4),
]:
losses = get_loss_map(loss_fn, x, y)
model, models = learn(
loss_fn, x, y, lr=lr, epochs=epochs, momentum=momentum,
weight_decay=weight_decay, dampening=dampening, nesterov=nesterov)
cm = pylab.get_cmap('terrain')
i = ax.imshow(losses, cmap=cm, interpolation='nearest', extent=[-5, 8, -5, 8])
ax.title.set_text(title)
loss_w, loss_b = zip(*models)
ax.scatter(loss_b, loss_w, c='r', marker='+')
ax.plot(loss_b, loss_w, c='r')
plt.show()
multi_plot(lr=0.1, epochs=100)

图 5. 可视化梯度下降在所有损失函数下的变化。
在这里,我们可以看到非 L2 损失函数的有趣轮廓。L2 损失函数平滑且值较大,最大可达 100,而其他损失函数的值较小,因为它们仅反映了绝对误差。但 L2 损失的陡峭梯度意味着优化器更快接近全局最小值,正如其初期点之间较大的间距所示。与此同时,L1 损失函数则表现出更为缓慢的接近最小值的过程。
动量
接下来最有趣的参数是动量,它决定了多少上一步的梯度将被加入到当前梯度更新中。通常非常小的动量值就足够了,但为了可视化的目的,我们将其设置为 0.9 的极端值——孩子们,请勿在家尝试:
multi_plot(lr=0.1, epochs=100, momentum=0.9)

图 6. 可视化的梯度下降过程,展示了所有损失函数在高动量下的表现。
由于极高的动量值,我们可以清楚地看到它对优化器的影响:它超过了全局最小值,必须不规则地转回。这种效应在 L2 损失中最为明显,其陡峭的梯度使得它越过了最小值,并且非常接近发散。
Nesterov 动量
Nesterov 动量是对普通动量的一个有趣调整。普通动量将上一步的部分梯度加到当前步骤的梯度上,得到下面图 7(a)的情景。但如果我们已经知道上一步的梯度将带我们到哪里,那么 Nesterov 动量则通过预判我们将落在哪里来计算当前梯度,得到下面图 7(b)的情景:

图 7. (a) 普通动量与 (b) Nesterov 动量。
multi_plot(lr=0.1, epochs=100, momentum=0.9, nesterov=True)

图 8. 可视化的梯度下降过程,展示了所有损失函数在高 Nesterov 动量下的表现。
从图形上看,我们可以看到 Nesterov 动量减少了我们在普通动量下观察到的过冲现象。特别是在 L2 的情况下,由于我们的动量使得我们越过了全局最小值,使用 Nesterov 动量提前查看我们将要落点的位置,允许我们从目标函数的另一侧混合反向梯度,从而更早地进行修正。
权重衰减
接下来,权重衰减对参数值(我们线性网络的权重和偏置)施加了正则化的 L2 惩罚:
multi_plot(lr=0.1, epochs=100, momentum=0.9, nesterov=True, weight_decay=2.0)

图 9. 可视化的梯度下降过程,展示了所有损失函数在高 Nesterov 动量和权重衰减下的表现。
在所有情况下,正则化因子都将解拉离了它们应有的全局最小值,并将其拉近了原点(0,0)。然而,这种效应在 L2 损失下最不明显,因为损失值足够大,能够抵消对权重的 L2 惩罚。
阻尼
最后是阻尼,它通过阻尼因子来减少动量的影响。使用 0.8 的阻尼因子,我们可以看到它如何有效地调节动量路径通过损失函数。
multi_plot(lr=0.1, epochs=100, momentum=0.9, dampening=0.8)

图 10. 可视化的梯度下降,展示了在高动量和高阻尼下的所有损失函数。
除非另有说明,所有图片均由作者提供。
参考文献
另见
-
towardsdatascience.com/extending-context-length-in-large-language-models-74e59201b51f -
代码可在以下地址找到:
github.com/pbaumstarck/scaling-invention/blob/main/code/torch_loss.py
从政府来源可视化家庭收入 —— 引导式操作指南
使用可以应用于你自己数据的 Python 代码,进行特定的可视化操作
·发表于 Towards Data Science ·12 分钟阅读·2024 年 3 月 7 日
--

该图像由 DALL·E 3 基于“绘制一张代表家庭收入的图像。图像应包括一座房子、一只手上的现金堆和一些统计图表”生成。
你是否不确定如何在互联网上的大量数据中导航,以及在从可靠来源获取相关数据后可以做些什么?
你是否好奇你的家庭收入与全国其他家庭相比如何?(我猜无论职业或兴趣如何,至少 90%的人都有这个疑问。)
如果你对上述任一问题的回答是“是”,那么这篇文章就是为你准备的!
在这篇引导式的操作指南结束时,你将能够自己获得以下内容。只需根据你的家庭收入,替换下面代码中的单一变量(如 ### just replace this with your data ### 所示),你将能够创建一个显示你相对于新加坡家庭的百分位的图表!(通过额外的工作,你还可以使用你自己国家的特定数据替换数据框。)

2000 年至 2023 年间的月度家庭收入的累计分布函数(y 轴),(x 轴)表示收入,其中标记显示 2000 年收入为 2500 美元的家庭的收入和百分位,收入每年增加 500 美元,直到 2023 年达到 14000 美元。截图由作者提供。
可视化我的数据科学求职过程
来自一次谦逊求职旅程的反思,试图在 2023 年找到一份工作
·发表于 Towards Data Science ·12 分钟阅读·2024 年 4 月 19 日
--
2023 年对许多求职者来说是一个动荡的一年。至少对我来说,感觉像是一次漫长的旅程。在 1 月到 11 月的这 11 个月里,我进行了 107 次与职业相关的对话,并申请了 80 个职位,最终收到了 2 个工作邀请(我在 5 月“休息”了一下,去答辩了我的博士学位 😅)。一些申请确实有些冒险,但许多职位我觉得自己非常合适——或者至少值得尝试一次对话!
我的努力常常被无声回应,这让我感到吃惊。我觉得我的大部分求职信都非常真诚,简历也有独特的经历,这让我开始怀疑自己的沟通能力。我的简历是不是不够清晰?求职信是不是太啰嗦了?我是不是在误导雇主关于我的技能?我真的不适合这些职位吗??这确实是一个令人沮丧的时刻——就像是有一种冒名顶替综合症的幽灵在我心头游荡。
从学习的角度来看,拒绝信常常没有任何反馈,真是令人沮丧。我该如何改进,才能在下一次申请我认为自己绝对是强有力候选人的职位时,做得更好?许多好心人给出了有用的建议,我确实认为我的材料随着时间的推移有所改进,但很难说究竟是什么让我最终取得了不同的结果。也许是我经过几个月较弱的尝试后,终于开始提交更强、更具竞争力的申请?也许 2023 年特别艰难?也许我碰上了许多无法控制的因素,比如遇到了一些虚假的招聘广告,实际上只是为了“吸引”人们对公司产生兴趣,或者是那些注定要内部招聘的职位?
本着分享一些可能对其他求职者有用的经验(好吧,也许是通过可视化我经常激活泪腺的经历来尝试进行某种情感宣泄),这里是一些来自我 2023 年求职过程的数据支持反思。我最热衷寻找的是生物学、数据科学与气候解决方案交汇处的职位,地点要么是西雅图,要么是远程。
求职申请的路径
对于我的 80 个工作申请,它们从提交到最终的“是”或“否”经历了怎样的过程?图 1 是一个桑基图,旨在展示各个节点之间的流量。
在图 1 的第二行,我们可以看到,在那 80 个申请中,大约一半是我向某公司门户网站上传我的信息,但我并不认识那里的任何人,这种申请我称之为“冷申请”。对于另一半,我有一个“个人联系”——有时这是一个了解我并能推荐我的人,但更多时候是我曾经见过一次的人(喝咖啡、Zoom 聊天或在社交活动中见过)。他们不一定非常了解我的具体技能,但他们知道我是真实的人,希望他们觉得我友好/体贴,并且能将信息传递给另一个真实的人,至少能让对方在一堆申请中看到我的简历。

图 1. 桑基图展示了各个阶段申请/面试过程中的申请量流动。图片由作者提供。
第三行展示了多少申请导致了面试请求或某种形式的拒绝/无回应。有些人坚持认为你永远不要申请那些你不认识公司内部人的职位——这只是浪费时间。
我的数据支持,拥有个人联系能提高我的面试请求率:42 个个人联系申请中有 11 个导致了面谈请求,面试率约为 25%,而 38 个冷申请中仅有 4 个导致了面试请求,面试率约为 10%。所以大约四分之一(4/15)的面试请求来自冷申请。我不会说永远不要申请没有个人联系的职位——对于合适的职位,这种申请可能会有成效——但绝对要投入精力建立个人联系。至少对我来说,这种申请路径在接触到有决策权的真实人类方面更为成功。
一些公司足够勤奋,当他们将你的申请移到“否”这一堆时,会发送自动拒绝通知(n=21)。非常少数情况下,有人从仅仅是申请阶段就告诉我“拒绝”(n=3)。超过一半的申请“失落在深渊”中(n=41),我既没有收到人类也没有收到机器的回复,只能假设一些深居不见的 AI 正在啃食我的求职信。
在有人请求面试的情况下,我通常会进入初步筛选环节,要么由人力资源部门,要么由招聘经理。如果这一步顺利,我会见到更多来自更大团队的成员,3 次面试中我被邀请做关于我的研究的工作报告。我的面试中没有包括正式的编码挑战——我申请的大多数职位更加侧重于科学/研究而非软件/基础设施——但我经常被要求提供我的个人 github 账号链接。
在底部一行,我们看到了最终的统计数据。在我获得 2 家公司的“是”之前,我得到了 74 个“否”的回答(它们几乎是同一天回复的,这在经历了 11 个月的求职后,简直是幸运到极点!)。在接受当前工作后,我撤回了其他正在进行的申请。
申请前的准备工作
一个重要提示:申请工作并不是我求职的第一步。在确定了 2023 年春季的博士答辩日期后,我从 2022 年底开始安排“信息性面谈”。许多早期的对话是与我感到舒适的同事、导师和同行进行的,他们信任我并愿意提供职业建议。这不仅帮助我 1)减少了我在这个让我焦虑的过程中开始时的压力,2)也让我能够向大家表明我已经正式“开始求职”,这样如果他们知道任何有趣的职位,就能通知我。开始这项工作时,绝对要依靠你的支持网络!
接下来,我开始与新认识的人安排会议——要么通过共同联系人的热情介绍,要么就是通过 LinkedIn 上突如其来的消息(一些冷 LinkedIn 消息没有得到回应,但也有一些得到了回复!大约有一半?)。这些请求通常表述为“我是即将毕业的博士生,正在探索[计算生物学/数据科学/气候]行业的职业发展路线——请问您能谈一谈您在[您目前的职位]的工作经验、您职业生涯的路径以及您对刚刚开始这一旅程的人的建议吗?”我还会简要个性化每条消息,以便让对方清楚我有兴趣与这个人特别交流,而不是在公司里乱发消息。

图 2. 从 2022 年末到 2023 年末的每次信息性对话、职位申请、面试和录用的总体时间线,按照与我所接触的公司/我申请的职位最相关的行业颜色进行标注。大多数职位申请是面向计算领域的职位,重点在生物学(蓝色)、气候(橙色)或生物学与气候的结合(绿色),少数是纯粹与数据科学相关的职位(粉色)。与同一公司的逐步面试通过线条连接。图像由作者提供。
图 2 的顶部面板显示,我保持了相对稳定的联系频率。尽管大多数对话并非关于开放职位,但这些对话并不觉得浪费时间。一些信息性访谈的关键好处:
-
由于我并不是立即在找工作(但很快就会),以“只是喝杯咖啡聊聊天”或“只是了解你的经历”这种轻松的方式框架,去除了很多压力。 我更容易做回自己,练习解释我的兴趣和经历,而不需要不断担心我是否没有用最完美的方式说话来获得工作。也很棒能练习积极倾听并提问,向聪明人深入探讨技术/科学细节。
-
了解行业动态非常有帮助 —— 人们可以更坦率地告诉我,如果他们的公司在短期内不太可能招聘,或者如果他们的公司近期可能会有职位空缺。这帮助我更直接地将注意力集中到某些公司的招聘页面,同时对其他公司保持更随意的关注。而且,现在他们已经认识我了,这些新的联系人可能会在未来告诉我他们公司招聘状态的变化。
-
仅仅是和人见面,友好地和陌生人聊聊/喝杯咖啡就挺酷的。 当然,有些对话稍显尴尬,有时我能感觉到对方有些想离开,但大多数人都非常鼓励和支持,愿意详细讲述他们自己的经历。遇到从事有趣工作的人的确让我保持了动力,而且他们通常会推荐我认识的其他人,认为他们可能会给我带来有用的视角,于是对话的列车继续前进。
-
和某人交谈后,如果我后来在他们公司看到相关的职位发布,我总是会告诉他们我已经申请了! 他们可能还不够了解我,在一次对话后无法充分证明我的技能,但希望他们至少对我有个积极的印象。如果他们愿意,他们可能会将我的申请转交给一个真正的人,后者可能会阅读我的求职信,而不是让它被卡在 AI 筛选器中 🙃
图 2 中的第二个面板显示了求职申请的事件:我在 2023 年初提交了一些申请,在 5 月答辩日期附近暂停 😮💨,然后在夏末加大了申请力度。第三个面板显示了(终于!)一小部分面试邀请开始陆续到来!
在一次相当奇怪的经历中,我收到了一个公司的聘用通知,尝试礼貌地进行谈判,然后迅速收到了“实际上,我们改变了主意,不再提供录用通知”的邮件。这让我非常震惊。这是我第一次尝试谈判,作为一个寻找计算机科学职位的女性,我一直被灌输进脑海,即使这种做法感觉不自然/尴尬/不舒服,你也必须尝试!我当时假设最糟糕的结果是他们会说“没有,这是我们最大限度的报价,接受或拒绝。”我很确定我的要求并不无礼或不合理,但被撤回的聘用通知确实让我感到相当沮丧😖。我担心自己在为自己争取公平薪酬时犯了大错。几位导师向我保证,这是一个非常不寻常的结果,与你无关,而是公司内部发生了某些怪事。我希望这种情况不会经常发生在其他人身上,但它并非不可能(n=1)。
底部面板显示,在 11 月,我终于收到了两个正式的工作录用通知,从而结束了这场伟大的猎寻。
为了将这份求职时间线放在更广泛的经济环境中考虑,我将每种互动类型—休闲信息性对话(深蓝色)、职位申请(绿松石色)和正式面试(绿色)—进行了分组,并绘制了随时间变化的累计计数,同时叠加了标准普尔 500 指数(紫色)的趋势(图 3)。

图 3. 各种求职互动类型随时间变化的累计总和(左侧 y 轴),与标准普尔 500 指数的每日最高点(右侧 y 轴)以及几次大型科技公司裁员(超过 1 万人)叠加。图片来源:作者。
除了我在 5 月博士答辩期间的空档期外,我的休闲信息性对话率在这一年中相当稳定,而我的职位申请率在夏末有所增加,最终赶上了我的信息性对话总数。我并不擅长阅读股市趋势和波动,但如果我眯着眼看,它似乎是标准普尔 500 指数在 2022 年底的一些低点后开始上升,并大致与我职位申请的陡峭上升曲线重合(也许是因为更多职位开始发布?)以及随之而来的面试增加👀。值得注意的是,这一切发生在 2023 年初,大型科技公司进行了一系列裁员(超过 1 万人),恰好接近标准普尔 500 指数的局部最低点。
专注于求职能量
在研究生阶段,我知道我的“科学幸福之地”是做一个这样的项目:
-
思考生物学(因为基因学简直是最酷的!)
-
做计算机科学(因为我喜欢编程工作的解谜性质)
-
具有可持续性应用(因为气候变化亟需解决,形势极为紧迫)
激动人心的是,我进入了一个研究生院的实验室,在那里我使用计算方法分析甲烷吃菌的基因数据。我希望我的职业生涯同样能涵盖这三大科学幸福支柱的交集,即我的知识、技能和热情。
我曾经关注几家以气候为导向的合成生物学公司,但在 2023 年初,我得知几乎所有有计算团队的公司都在招聘冻结状态 🥶 也许在脆弱的经济环境下,期望同时实现这三大科学幸福支柱过于奢求,我可能不得不接受只具备其中 1 或 2 个特点的职位,尤其是如果我坚持要住在西雅图的话?
我的求职精力在一年中不断调整焦点,图 2 中彩色圆圈的密度反映了这一点。对话、申请和面试按公司重点分类(作为计算机科学学生,我申请的大多数职位至少涉及一些计算元素,较大的公司通常处理与生物学、气候或两者结合相关的问题)。尽管致力于气候解决方案是我内心的方向,但最初我说服自己,在积累经验和技能的过程中,即使从事与气候无关的工作也没关系。因此,在 2023 年初,我们看到我更多关注于“生物学 + 数据科学”(蓝色圆圈,通常是健康/制药领域的应用)的对话和申请,因为“生物学 + 气候 + 数据科学”职位相对稀缺。
经过几次存在主义危机后,我的思路发生了转变:我决定宁愿从事气候相关的工作,即使这意味着我的工作不会利用我多年在遗传学/微生物学方面的学习。2023 年晚些时候,我的求职努力反映了这一心态的转变,集中在“气候 + 数据科学”(橙色圆圈,气候工作无生物学元素)。虽然我最初对从事偏离生物学的职位感到紧张,但我很享受有机会学习相关领域的知识,并考虑如何将我的技能适应那些非生物学气候解决方案公司正在解决的问题。
图 4 大致总结了我在生物学、气候和数据科学交集领域投入的总能量,其中每次信息交流、职位申请和正式面试都计为一个能量单位。值得注意的是,我无法选择何时进行正式面试——那取决于其他人是否愿意与我交流——但我决定将这三种能量单位合并为集中的焦虑/压力时刻。这个总和没有包括其他类型的努力,比如阅读、研究和准备,但这些通常是分散在较长时间内进行的。

图 4. 每个感兴趣的工作领域所投入的总能量。图片由作者提供。
幸运的是,2023 年秋季带来了对一些之前招聘冻结的解冻,几乎所有职位都出现在了那些将计算机科学和生物学结合用于气候应用的公司中(图 2 中的绿色圈,图 4 中深绿色的中心圈)。这些正是我希望找到的职位,因为它们与我的兴趣、技能和价值观高度契合!我急切地联系了所有之前在这些公司建立过联系的人,并表达了我对申请的极大热情!
终于,我找到了那个完美契合我的职业目标的工作:我正在 LanzaTech 担任数据科学家,LanzaTech 是一家生物技术公司,致力于工程化细菌,能够吃掉工业废气流中的碳排放并将其转化为可持续材料 🤓
结语——种下职业的“花园”
总结来说,我不完全确定我的求职过程中哪些方面导致我的申请收到了特别好的或差的反馈。过程感觉漫长且充满压力,来自大量自动拒绝和消失在深渊中的申请缺乏反馈,这使得很难进行有效的调整。
稍微猜测一下,我最大的建议是多与人交流!经常交流!我们都听说过建立人脉(拓展职业联系人等)的重要性。与陌生人不断发起对话可能会让人感到尴尬和疲惫。但如果每次对话都带着真诚的意图去建立联系、倾听和提问,这会非常有帮助!信息性访谈是低压力的场合,可以用来建立联系并分享自己的兴趣。
不是每一场对话都会直接带来工作机会,因此看起来可能像是一次没有直接结果的大量时间投资。但你永远不知道哪一次会为你打开一扇门。以我的经历为例:2016 年,我偶然在一次会议上与 LanzaTech 的一位员工同桌吃午餐,并通过邮件(每隔 1-3 年一次?)保持着随意的对话。到了 2023 年底(7 年后!),一家与我的技能匹配的职位空缺了,我便联系了那位人(以及几位最近在 LanzaTech 认识的其他人)。我告诉他们我对这个职位的真诚兴奋,或许这帮助我的申请引起了招聘岗位上真实人类的注意,从而继续了对话 😃
如果“职业人脉网络”让你感到害怕,不妨把你的人脉想象成一个花园——并不是每一颗种子都会结出果实,有些东西长得快,有些则慢。花园并不总是在你想要的时候繁荣(就像你研究生阶段的健康保险到期时,你真的希望能有一个西红柿突然长出来并为你提供一个新的医疗计划)。但是,花园只要定期打理,便会健康、稳步成长,并且在你需要支持时,往往能带来一阵清新的空气。
开始一个职业关系花园还不晚——它可以像向某个看起来很有趣的人发送一条真诚但专业的消息一样简单 🌱
祝各位求职者好运 💚
感谢 Daniel 和 Matt 对我早期草稿的反馈!
可视化道路网络

如何使用 Python 和 OSMnx 创建全球城市道路网络的美丽可视化。
·发表于 Towards Data Science ·7 分钟阅读·2024 年 7 月 26 日
--
道路网络是城市的美丽鸟瞰图。然而,它们的重要性远远超出作为壁纸的眼球糖果。事实上,对于训练有素的城市规划师来说,城市道路网络的视觉结构已经暗示了许多关于城市规划(如果有的话)以及城市可能采取的发展路径的信息,以及当前或不久的将来可能影响城市的潜在陷阱和问题。这些问题可以从公共和私人交通规划到绿色基础设施的改善,再到服务和设施的可达性。
在这篇文章中,我通过使用OSMnx库下载全球多个城市的道路网络,向你介绍这个过程的第一步。
所有图像均由作者创建。
原型设计道路网络可视化过程
让我们从一些简单且快速处理的内容开始,比如一个较小的城市区域。例如,我使用布达佩斯的市中心,第五区。
首先,使用 OSMnx,我下载城市的行政边界,然后使用graph_from_polygon命令…
使用 Python 在交互式地图上可视化路线:第一部分
一本关于使用 Folium 解决交通问题的交互式数据可视化的实用指南
·发表于 Towards Data Science ·阅读时间:20 分钟·2024 年 1 月 16 日
--

由 DALL·E 3 根据作者的提示生成的图像:“巴黎的一个路线,位于交互式地图之上”
👁️ 这是系列文章中的第 6 篇,涵盖了项目“Python 中的智能旅游决策支持系统”。请注意,本文是独立且自足的,不依赖于系列中的前几篇文章,因此你可以直接阅读本文并跟随代码进行操作,而不必阅读之前的文章。遇到涉及前几篇文章的任何内容时,请忽略它们。
本文是两件事的结合:一个使用 **folium** 显示路线的教程,以及一个增量式数据可视化的指南,其优先顺序是(1)先有“可用的”东西,然后(2)有“有用的”东西,最后(3)才是“美观的”东西。因此,这份指南将是开发通用路线应用的宝贵工具,尤其是当目标是快速得到一个原型时。一个这样的路线应用是旅行计划优化,这是本系列文章的统一主题。如果你想了解如何从头开始设计和增量构建一个决策支持系统,我邀请你查看它。如果你只是想以互动方式可视化路线,以更快地获得洞察,那么本文将展示一种快速但强大的方法来做到这一点——因为我知道你需要先让它“完成”,然后再让它“做好”。第二部分将展示一种较慢但更稳健的方法来做同样的事。
目录
1. 上一轮冲刺回顾
2. 数据和依赖项设置
3. 在地图上显示路线:务实的做法
-
3.1 显示 站点
-
3.2 显示 路线
-
3.3 使用 互动信息 丰富地图
-
3.4 处理封闭路线,即 旅游路线
-
3.5 奖励: 将 KPI 添加到地图上
4. 结论(或为下一个冲刺做计划)
1. 上一轮冲刺回顾
在上一篇文章中,我们创建了一个类似于 scikit-learn 的优化器类,通过一次简单的方法调用就能解决任意一组位置的旅行商问题(TSP)。简而言之,如果你有一个包含站点位置的数据框,这个优化器可以被“拟合”到这些位置,从而自动提供最优的(最短距离)旅游路线。更多细节请参见:
## 一种优雅的解决旅行商问题的方法,使用 Python 有效解决
以类似 scikit-learn 的方式实现 TSP 模型,简化路线优化的构建和求解…
towardsdatascience.com
最后,我们得出结论,尽管在****一行代码 解决 TSP 问题非常方便,但这种优化器的输出并不足以构成一个完整的原型。我们需要一种方法来快速可视化该输出,以便直观地验证并与他人分享。因此,这种需求促成了本次冲刺的目标,这是系列中的第六次:将优化器的输出,即,以有序位置形式的路线,图形化地显示在地图上。请注意,优化器的输出,即存储最佳路线的数据框,现已成为我们的输入,而我们需要制作的输出是显示在地图上的路线。输入是通用的:只是一组有序的位置,它可以表示任何类型的路线:公交车在某一天的停靠站,邮递员为了投递邮件而访问的一组地址,或者外卖员服务多个住宅的行驶路径。对我们来说,重要的是“通用”路线的可视化,我们将在本文中开发实现这一功能的方式。
2. 数据和依赖项设置
2.1 安装依赖项
我们将使用美丽的Folium 库来创建地图,该库在后台使用Leaflet.js库,使 Python 程序员能够轻松创建交互式地图。我们还需要Geopy,一个强大的地理编码库(如果你在系列文章开始时创建了虚拟环境,你应该已经安装了它)。这两个都是可以通过 pip 或 conda 安装的标准库。选择以下任一命令进行安装:
conda install -y -c conda-forge geopy folium
pip install geopy folium
# In case of errors, use these specific versions (used for this article)
# folium 0.14.0
# geopy 2.3.0
# pandas 2.0.3
2.2 准备输入数据
在入门示例中,我们将以巴黎的一些著名景点为例。
from typing import Tuple, List, Dict
import pandas as pd
import folium
pd.set_option('display.precision', 2)
df_sites = pd.DataFrame(
[['hotel', 48.8527, 2.3542],
['Sacre Coeur', 48.8867, 2.3431],
['Louvre', 48.8607, 2.3376],
['Montmartre', 48.8872, 2.3388],
['Port de Suffren', 48.8577, 2.2902],
['Arc de Triomphe', 48.8739, 2.2950],
['Av. Champs Élysées', 48.8710, 2.3036],
['Notre Dame', 48.8531, 2.3498],
['Tour Eiffel', 48.8585, 2.2945]],
columns=pd.Index(['site', 'latitude', 'longitude'], name='paris')
)
df_sites

注意: 本文中的所有图片均由作者提供,除非另有说明。
让我们来看一种通过 folium 快速绘制这些地点在地图上的方法。
3. 在地图上显示路线:一种务实的方法
如果你没有跟随这篇文章的系列内容,让我指出,我们正在采用敏捷方法来创建决策支持系统,因此我们是通过所谓的“冲刺”逐步构建它。这意味着我们首先希望完成一个解决问题的工作原型,这个问题是关于最小可行问题的旅行规划系统。这个原型在我们没有实现最简化的可视化功能之前,不能被认为是完成的,因此才有了这篇文章。一旦我们有了原型,即,一旦系统的第一个版本在增加边际价值的方式下变得可用,我们就可以开始完善它的实现,使其易于扩展,这样我们就能为系统添加更多现实的特性,使其能够处理更广泛的、更加复杂的旅行规划问题。因此,我们现在关注的是一种足够好的方法——而不是最终的方法——来以某种有意义的方式在地图上显示一条路线。更明确的方法将在第二部分和第三部分中出现,我们将在那里重构这里创建的代码,使其遵循面向对象设计(自定义类型和类),从而更好地处理随着旅行规划问题加入更多细节和需求而不断增加的路线复杂性。但现在,这篇文章(第一部分)试图展示一种更务实、更快速的方式来用 Python 创建交互式路线。让我们看看如何实现。
3.1 显示站点
我们首先需要创建一个巴黎的地图。我们可以将地图中心设置为我们一组站点的平均位置:
avg_location = df_sites[['latitude', 'longitude']].mean()
map_paris = folium.Map(location=avg_location, zoom_start=13)
然后,为了在其上显示这些站点,我们为每个站点创建一个标记,并将其添加到地图上。标记只需要一个位置,但为了快速识别每个标记上的站点,我们将站点名称传递给tooltip属性,这样当我们将鼠标悬停在标记上时,就会看到一个弹出窗口,显示站点的名称:
for site in df_sites.itertuples():
marker = folium.Marker(location=(site.latitude, site.longitude),
tooltip=site.site)
marker.add_to(map_paris)
map_paris

图 6.1. 地图上的站点标记
当然,如果需要,你可以放大和缩小并移动地图,使用左上角的+ | -按钮,或者通过点击和拖动。
3.2 显示路线
我们刚刚显示的站点实际上并没有按顺序排列,但我们可以假设它们是有顺序的。假设数据框中包含城市中的一条路线,其中索引中的数字表示这些站点的访问顺序:
df_route = df_sites.copy()
df_route.index.name = 'visit_order'
df_route

现在这些站点按照特定顺序(访问顺序)“连接”起来,因此我们也应该在地图上通过添加连接连续停靠点的线路来表示这一事实。这些线路,或者更准确地说,“段落”,是通过 folium.PolyLine 对象创建的。为了将它们一次性添加到地图上,我们在 df_route 中创建了更多的列,保存“下一个停靠点”的信息,因此每个停靠点都与其后续停靠点相对应,构成一个路线段。这样,每一行都可以存储有关停靠点和路线段的信息。
df_route_segments = df_route.join(
df_route.shift(-1), # map each stop to its next stop
rsuffix='_next'
).dropna() # last stop has no "next one", so drop it
df_route_segments

对于一个固定的行,前三列保存“当前”站点的信息,接下来的三列保存“下一个站点”在路线中的信息。这样,我们就可以在同一个 for 循环迭代中同时创建一个标记 和 一个段落:
map_paris = folium.Map(location=avg_location, zoom_start=13)
for stop in df_route_segments.itertuples():
# marker for current stop
marker = folium.Marker(location=(stop.latitude, stop.longitude),
tooltip=stop.site)
# line for the route segment connecting current to next stop
line = folium.PolyLine(
locations=[(stop.latitude, stop.longitude),
(stop.latitude_next, stop.longitude_next)],
tooltip=f"{stop.site} to {stop.site_next}",
)
# add elements to the map
marker.add_to(map_paris)
line.add_to(map_paris)
# maker for last stop wasn't added in for loop, so adding it now
folium.Marker(location=(stop.latitude_next, stop.longitude_next),
tooltip=stop.site_next).add_to(map_paris);
map_paris

图 6.2. 通过线路连接的站点标记
现在,停靠点的访问顺序变得清晰,当你用鼠标悬停在每条线路上时,会看到该线路连接了哪两个站点。标记也一样,悬停时会显示它们所指向站点的名称。
3.3 丰富地图的交互信息
这些信息对于大致了解路线的外观已经足够,但我们可以通过添加更多可以交互显示的信息,用一点额外的努力让地图变得更加有用。首先,路线从哪里开始?通过当前的视图,并不直观地看到哪个站点是“起点”。有两个标记仅与一个段落连接,因此它们必须是其中之一,但我们得查看数据框才能区分“起点”和“终点”。更好的方式是通过特殊图标标示起始站点,这样我们一眼就能认出它。我们通过为每个标记添加一个 folium.Icon 对象来做到这一点,使得起始站点具有一个独特的图标:
map_paris = folium.Map(location=avg_location, zoom_start=13)
for stop in df_route_segments.itertuples():
initial_stop = stop.Index == 0
# icon for current stop
icon = folium.Icon(icon='home' if initial_stop else 'info-sign',
color='cadetblue' if initial_stop else 'red')
# marker for current stop
marker = folium.Marker(location=(stop.latitude, stop.longitude),
icon=icon, tooltip=stop.site)
# line for the route segment connecting current to next stop
line = folium.PolyLine(
locations=[(stop.latitude, stop.longitude),
(stop.latitude_next, stop.longitude_next)],
tooltip=f"{stop.site} to {stop.site_next}",
)
# add elements to the map
marker.add_to(map_paris)
line.add_to(map_paris)
# When for loop ends, the stop variable has the second-to-last
# stop in the route, so the marker for the last stop is missing
# We add it now using the "next" columns of the last row
folium.Marker(
location=(stop.latitude_next, stop.longitude_next),
tooltip=stop.site_next,
icon = folium.Icon(icon='info-sign', color='red')
).add_to(map_paris);
map_paris # show map

图 6.3. 按类型着色的带停靠标记的路线
一眼看去,现在显然路线是从带有“家”图标的标记开始的。让我们进一步利用交互性,通过在地图上显示一些有助于更好理解路线的更多信息来提高地图的可用性。我们将添加的基本信息是停靠点之间的距离和每个停靠点的访问顺序号。由于在 df_route_segments 的每一行中都有段落的起点和终点,我们可以轻松地添加一列,表示连续停靠点之间的距离。
对于只对当前文章感兴趣的读者(即,不关心以前的文章)**,我在下面声明了函数
*ellipsoidal_distance*。如果相反,您已经跟随整个系列的文章,您已经了解了 那个函数**,因为我们在 计算距离矩阵的文章**中开发了它(并为其辩护)。欢迎在此处重用该函数,以避免重复代码。要获得位置*loc1*和位置*loc2*之间的距离(单位:米),您只需要:
***from*** *geoutils* ***import*** *GeoAnalyzer*
*distance* ***=*** *GeoAnalyzer.****ellipsoidal_distance****(loc1, loc2)*
from geopy.distance import geodesic
_Location = Tuple[float, float]
def ellipsoidal_distance(point1: _Location, point2: _Location) -> float:
"""Calculate ellipsoidal distance (in meters) between point1 and
point2 where each point is represented as a tuple (lat, lon)"""
return geodesic(point1, point2).meters
函数ellipsoidal_distance接受两个位置(两个坐标元组),并返回它们之间的地理距离,单位为米。有关地理距离的更深入解释和论证,请阅读我们创建该距离公式的文章:
估算任意一对站点之间的距离,以其地理坐标为基础,作为解决问题的跳板……
towardsdatascience.com](/compute-the-distance-matrix-of-a-set-of-sites-from-their-coordinates-in-python-d5fc92a0ba9e?source=post_page-----44f8d25d0761--------------------------------)
我们可以添加新的列'distance_seg',因为在每一行中我们都包含了段的两个端点:
df_route_segments['distance_seg'] = df_route_segments.apply(
lambda stop: ellipsoidal_distance(
(stop.latitude, stop.longitude),
(stop.latitude_next, stop.longitude_next)),
axis=1
)
df_route_segments

拥有了这一新列后,我们可以将其值纳入我们传递给tooltip的字符串中,在创建地图上的线路时,从而使每个段的距离在光标指尖可见。同时,我们还可以将“停靠点编号”添加到标记上,以便为每个标记提供更多的路线上下文:
map_paris = folium.Map(location=avg_location, zoom_start=13)
for stop in df_route_segments.itertuples():
initial_stop = stop.Index == 0
# marker for current stop
icon = folium.Icon(icon='home' if initial_stop else 'info-sign',
color='cadetblue' if initial_stop else 'red')
marker = folium.Marker(
location=(stop.latitude, stop.longitude),
icon=icon,
# display the name and stop number at each site's marker
tooltip=f"<b>Name</b>: {stop.site} <br>" \
+ f"<b>Stop number</b>: {stop.Index} <br>"
)
# line for the route segment connecting current to next stop
line = folium.PolyLine(
locations=[(stop.latitude, stop.longitude),
(stop.latitude_next, stop.longitude_next)],
# display the start, end, and distance of each segment
tooltip=f"<b>From</b>: {stop.site} <br>" \
+ f"<b>To</b>: {stop.site_next} <br>" \
+ f"<b>Distance</b>: {stop.distance_seg:.0f} m",
)
# add elements to the map
marker.add_to(map_paris)
line.add_to(map_paris)
# add route's last marker, as it wasn't included in for loop
folium.Marker(
location=(stop.latitude_next, stop.longitude_next),
tooltip=f"<b>Name</b>: {stop.site_next} <br>" \
+ f"<b>Stop number</b>: {stop.Index + 1} <br>",
icon = folium.Icon(icon='info-sign', color='red')
).add_to(map_paris);
map_paris # show map

图 6.4. 鼠标悬停在标记上时显示的交互式信息
注意我们如何使用了一些 HTML 来更漂亮地呈现标记和线路上的“悬停文本”。

图 6.5. 鼠标悬停在路线段上时显示的交互式信息
当前的行为看起来足够通用,适合封装成一些辅助函数:
def _make_route_segments_df(df_route: pd.DataFrame) -> pd.DataFrame:
"""Given a dataframe whose rows are ordered stops in a route,
and where the index has integers representing the visit order of those
stops, return a dataframe having new columns with the information of
each stop's next site"""
df_route_segments = df_route.join(
df_route.shift(-1), # map each stop to its next
rsuffix='_next').dropna()
df_route_segments['distance_seg'] = df_route_segments.apply(
lambda stop: ellipsoidal_distance(
(stop.latitude, stop.longitude),
(stop.latitude_next, stop.longitude_next)
), axis=1
)
return df_route_segments
def plot_route_on_map(df_route: pd.DataFrame) -> folium.Map:
"""Takes a dataframe of a route and displays it on a map, adding
a marker for each stop and a line for each pair of consecutive
stops"""
df_route_segments = _make_route_segments_df(df_route)
# create empty map
avg_location = df_route[['latitude', 'longitude']].mean()
map_route = folium.Map(location=avg_location, zoom_start=13)
for stop in df_route_segments.itertuples():
initial_stop = stop.Index == 0
# marker for current stop
icon = folium.Icon(icon='home' if initial_stop else 'info-sign',
color='cadetblue' if initial_stop else 'red')
marker = folium.Marker(
location=(stop.latitude, stop.longitude),
icon=icon,
tooltip=f"<b>Name</b>: {stop.site} <br>" \
+ f"<b>Stop number</b>: {stop.Index} <br>"
)
# line for the route segment connecting current to next stop
line = folium.PolyLine(
locations=[(stop.latitude, stop.longitude),
(stop.latitude_next, stop.longitude_next)],
# add to each line its start, end, and distance
tooltip=f"<b>From</b>: {stop.site} <br>" \
+ f"<b>To</b>: {stop.site_next} <br>" \
+ f"<b>Distance</b>: {stop.distance_seg:.0f} m",
)
# add elements to the map
marker.add_to(map_route)
line.add_to(map_route)
# When for loop ends, the stop variable has the second-to-last stop in
# the route, so the marker for the last stop is missing, and we add it
# now using the "next" columns of the last row
folium.Marker(
location=(stop.latitude_next, stop.longitude_next),
tooltip=f"<b>Name</b>: {stop.site_next} <br>" \
+ f"<b>Stop number</b>: {stop.Index + 1} <br>",
icon = folium.Icon(icon='info-sign', color='red')
).add_to(map_route)
return map_route
3.4 处理封闭路线,亦即环形路线
如果我们有一个代表闭合旅行的 DataFramedf_route,即,一条起始点和终点相同的路线,会发生什么情况?地图会如何反映这一点?让我们构建这样的一个 DataFrame 并进行测试。我们以之前的df_route为例,它是一个开放的路线,并在末尾添加一行,内容与第一行相同,从而使得这条路线闭合:
df_route_closed = pd.concat(
[df_route, df_route.head(1)], ignore_index=True
)
df_route_closed.index.name = df_route.index.name
df_route_closed

多亏了设置ignore_index=True,代表访问顺序的索引号已经自动增加了一个单位。现在我们照常继续,创建地图,但这次使用我们的新辅助函数plot_route_on_map:
plot_route_on_map(df_route_closed)

图 6.6。 蓝色标记(表示初始站点)被最终站点的红色标记遮挡
它几乎完美地工作,因为代表“闭合”段的新行如预期创建,但标记有问题:一个新的红色标记被添加到初始站点上方,覆盖了先前添加的蓝色标记。这就是 Folium 的工作方式,它会将新元素叠加在现有元素上(注意“酒店”的标记阴影比其他标记的阴影更深,这是一个微妙的提示,表明该位置有多个标记)。为了避免丢失指示路线起始站点的有用蓝色“主页”图标,我们需要跳过添加最后一个标记(当for循环结束后)仅当最后一个站点与路线的起始站点重合时。我们可以通过直接检查df_route中的条件来做到这一点,只有在路线没有闭合的情况下,才添加最后一个标记:
# NOTE: trimmed down function for reference only, do not copy-paste.
def plot_route_on_map(df_route: pd.DataFrame) -> folium.Map:
#----- map is created here -----
# ...
#----- markers are created here -----
for stop in df_route_segments.itertuples():
# ...
# ( ఠ ͟ʖ ఠ)
# ...
# for loop ends!
### 𝗡𝗲𝘄 𝗰𝗼𝗻𝗱𝗶𝘁𝗶𝗼𝗻 𝗰𝗵𝗲𝗰𝗸𝗲𝗿 ###
# check if first site's name and location coincide with last's?
first_stop = df_route.iloc[0][['site', 'latitude', 'longitude']]
last_stop = df_route.iloc[-1][['site', 'latitude', 'longitude']]
is_closed_tour = (first_stop == last_stop).all()
# When for loop ends, the marker for the last stop is missing
# (**unless the route is closed**). if the route is not closed,
# we add it now using the "next" columns of the last row
if not is_closed_tour:
folium.Marker(
location=(stop.latitude_next, stop.longitude_next),
tooltip=f"<b>Name</b>: {stop.site_next} <br>" \
+ f"<b>Stop number</b>: {stop.Index + 1} <br>",
icon = folium.Icon(icon='info-sign', color='red')
).add_to(map_route)
return map_route
在如上所述编辑plot_route_on_map之后,期望的行为已恢复,即,初始站点的“主页”图标不会被旅行中的最后一个站点覆盖:
plot_route_on_map(df_route_closed)

图 6.7。 标记显示初始站点再次可见
使用这种最终视图,我们可以通过光标的一个简单操作轻松知道两个连续站点之间的距离(以及它们所连接的站点)。此外,我们只需将光标悬停在站点上,就能查看站点访问的时间顺序。这种互动性可以帮助我们评估所处理路线的质量,无论当前处理的是什么类型的路径问题。
3.5 附加功能:在地图上添加关键绩效指标(KPI)
最后,让我们添加一个功能,使地图变得更加富有洞察力。
总是有一些信息,它们与路线中的任何站点或段落没有直接关联,而是与整个路线相关。这些信息也很重要,通常是全局属性,如 ID、名称,或与路线相关的重要指标。我们也可以通过一个简单的技巧将它们显示在 Folium 地图上。
如果我们想展示一些与任何单一标记或行无关,但与整个路线相关的信息,我们可以将其作为文本块添加到 folium 地图中。例如,在商业仪表盘中,这非常有用,因为通常我们想要显示像路线的名称/ID 或各种试图在某种相关意义上总结它的指标之类的信息。
举个例子,我会在左上角添加一个地图的标题,在右下角添加两个非常基础的路线 KPI:停靠站的数量和它穿越的总距离。我们定义了我们想要显示的不同文本的名称(在TAG_常量中),并从df_route_segments中提取它们各自的值,df_route_segments是包含所有“段数据”的数据框:
TAG_ROUTE_NAME = "Name"
TAG_NUMBER_STOPS = "Num stops"
TAG_TOTAL_DISTANCE = "Distance"
_SPACE_HTML = " " # needed to add empty spaces between KPIs
# get summary info to display on map
name = df_route_segments.columns.name.capitalize()
n_stops = df_route_segments['site'].size
route_distance = df_route_segments['distance_seg'].sum().round(0)
对于每个我们想要添加的文本,我们需要将其转换为 HTML,并以特殊方式将其添加到地图中。包含文本 HTML 代码的字符串需要放入一个folium.Element中。步骤如下:(1) 创建一个基本的HTML 字符串来显示信息,(2) 使用更多 HTML 进行样式设置,(3) 将其添加到地图的根 HTML 元素中。我知道,谈论地图让我也会迷失,所以我们还是直接做吧。
💡小贴士: 如果你正在使用 Jupyter Notebook 或 Lab,建议你使用
IPython.display.HTML类来快速查看你的 HTML 代码反馈,像这样:
from IPython.display import HTML, display
# show a hello world message in blue and bold
display(HTML("<span style='color:steelblue'>Hello <b>world</b></span>"))
为地图添加标题
首先,创建标题的基本 HTML 字符串:
_html_text_title = f"<b>{TAG_ROUTE_NAME}</b>: {name}"
display(HTML(_html_text_title)) # [Out]: 𝗡𝗮𝗺𝗲: Paris
第二,按你喜欢的样式来调整这个基本 HTML 字符串。在这里,我希望标题是黑色的,位于地图的左上角,并且为了提高可读性,我希望文字有一些白色背景阴影:
STYLE_TITLE = (
"position:absolute;z-index:100000;left:5vw;color:black;"
"font-size:30px;text-shadow:-1px 0 white, 0 1px white, 0 1px white"
)
html_title = f'<h3 style="{STYLE_TITLE}">{_html_text_title}</h3>'
# let's see how that title looks like on the map (run all in same cell):
map_with_title = plot_route_on_map(df_route)
root_map = map_with_title.get_root()
root_map.html.add_child(folium.Element(html_title))
map_with_title

图 6.8. 地图的标题,不受缩放级别影响
看起来不错。现在,做同样的事情来显示 KPI:
将 KPI 添加到地图中
和之前一样,首先,我们为 KPI 创建基本的 HTML 字符串。对于这种情况,显示方式会有所不同,当然,也取决于个人口味:我喜欢我的 KPI 显示在地图的右下角,水平排列。可以随意更改样式参数,选择符合你审美偏好的样式。
_html_text_summary = f"""
<b>{TAG_NUMBER_STOPS}</b> <b>{TAG_TOTAL_DISTANCE}</b>
<br>
{n_stops} {16 * _SPACE_HTML} {route_distance:.0f} m
"""
display(HTML(_html_text_summary))
# [Out]:
# 𝐍𝐮𝐦 𝐬𝐭𝐨𝐩𝐬 𝐃𝐢𝐬𝐭𝐚𝐧𝐜𝐞
# 8 25158 m
样式非常相似,唯一的变化是字体稍微小一点。我们保留白色背景阴影,因为它对展示 KPI 的清晰度至关重要:
STYLE_SUMMARY = (
"position:absolute;z-index:100000;font-size:20px;"
"right:0;bottom:0;color:black;"
"text-shadow:-1px 0 white, 0 1px white, 0 1px white"
)
html_summary = f'<h2 style="{STYLE_SUMMARY}">{_html_text_summary}</h2>'
# let's see how the KPIs look like (run all in same cell):
map_with_kpis = plot_route_on_map(df_route)
root_map = map_with_kpis.get_root()
root_map.html.add_child(folium.Element(html_summary))
map_with_kpis

图 6.9. 地图上方的 KPI,不受缩放级别影响
好的,开始看起来很棒了!现在我们来将标题和 KPI 结合起来!
my_map = plot_route_on_map(df_route)
root_map = my_map.get_root()
root_map.html.add_child(folium.Element(html_title)) # add title
root_map.html.add_child(folium.Element(html_summary)) # add summary KPIs
my_map # check it out

图 6.10. 路线的总结信息,标题在顶部,KPI 在底部,不受缩放级别影响
感谢我们添加的文本,地图现在传达了更多有用的路线信息(看起来也更专业了,不是吗?)。甚至不需要移动鼠标,我们就能看到地图角落的有价值信息;而得益于互动性,通过快速悬停,我们可以看到不同景点的名称和参观顺序,以及任意两个停靠点之间的距离。让我们通过将其封装为帮助函数,使这个功能 可重用 ,并创建一个新的、更通用的函数display_route_on_map,该函数将像plot_route_on_map一样创建地图,增加了将名称和 KPI 信息包含到地图上的选项。首先是获取 HTML 字符串的函数:
def _get_text_for_title(df_route_segments):
"""Given a dataframe representing a route, where the column index has
the name of the route, returns an HTML string with a nice display of
this name"""
# 1) get the info to display
name = df_route_segments.columns.name
name = name.capitalize() if name else ''
# 2) parse the info as HTML for addition to map
_html_text_title = f"<b>{TAG_ROUTE_NAME}</b>: {name}"
html_title = f'<h3 style="{STYLE_TITLE}">{_html_text_title}</h3>'
return html_title
def _get_kpis_to_display_on_map(df_route_segments):
"""Given a dataframe representing a route, and having columns 'site'
and 'distance_seg', returns an HTML string with a nice display of
the number of sites and the total distance of the route"""
# 1) get the info to display
n_stops = df_route_segments['site'].size
route_distance = df_route_segments['distance_seg'].sum().round(0)
# 2) parse the info as HTML for addition to map
_html_text_summary = f"""
<b>{TAG_NUMBER_STOPS}</b> <b>{TAG_TOTAL_DISTANCE}</b>
<br>
{n_stops} {16 * _SPACE_HTML} {route_distance:.0f} m
"""
html_summary = f'<h2 style="{STYLE_SUMMARY}">{_html_text_summary}</h2>'
return html_summary
我们可以通过新函数的include_kpis属性来控制是否将标题和 KPI 添加到地图中:
def display_route_on_map(df_route, include_kpis=True) -> folium.Map:
"""Given a dataframe representing a route, creates a folium map
and adds markers for the stops and lines for the route segments,
with the option to also add an automatic title and 2 KPIs:
- number of stops in the route
- total distance of route
Parameters
----------
df_route : pd.DataFrame
A dataframe representing a route, whereby each row contains
information on a different stop of the route, and rows are sorted
by stop visiting order.
include_kpis : bool (default=True)
Whether to include the title and the 2 KPIs in the map
Returns
-------
A folium map that can be displayed or re-used"""
# 1) create empty map
avg_location = df_route[['latitude', 'longitude']].mean()
map_route = folium.Map(location=avg_location, zoom_start=13)
# 2) create DF with segment information
df_route_segments = _make_route_segments_df(df_route)
# 3) add title and KPIs to the map
if include_kpis:
html_title = _get_text_for_title(df_route_segments)
html_summary = _get_kpis_to_display_on_map(df_route_segments)
root_map = map_route.get_root()
root_map.html.add_child(folium.Element(html_title)) # add title
root_map.html.add_child(folium.Element(html_summary)) # add KPIs
# 4) add route to the map
for stop in df_route_segments.itertuples():
initial_stop = stop.Index == 0
# marker for current stop
icon = folium.Icon(icon='home' if initial_stop else 'info-sign',
color='cadetblue' if initial_stop else 'red')
marker = folium.Marker(
location=(stop.latitude, stop.longitude),
icon=icon,
tooltip=f"<b>Name</b>: {stop.site} <br>" \
+ f"<b>Stop number</b>: {stop.Index} <br>"
)
# line for the route segment connecting current to next stop
line = folium.PolyLine(
locations=[(stop.latitude, stop.longitude),
(stop.latitude_next, stop.longitude_next)],
# add to each line its start, end, and distance
tooltip=f"<b>From</b>: {stop.site} <br>" \
+ f"<b>To</b>: {stop.site_next} <br>" \
+ f"<b>Distance</b>: {stop.distance_seg:.0f} m",
)
# add elements to the map
marker.add_to(map_route)
line.add_to(map_route)
# does the first site's name and location coincide with the last's?
first_stop = df_route.iloc[0][['site', 'latitude', 'longitude']]
last_stop = df_route.iloc[-1][['site', 'latitude', 'longitude']]
is_closed_tour = (first_stop == last_stop).all()
# When for loop ends, the stop variable has the second-to-last
# stop in the route, so the marker for the last stop is missing
# (**unless the route is closed**). We add it now using
# the "next" columns of the last row, if the route is open
if not is_closed_tour:
folium.Marker(
location=(stop.latitude_next, stop.longitude_next),
tooltip=f"<b>Name</b>: {stop.site_next} <br>" \
+ f"<b>Stop number</b>: {stop.Index + 1} <br>",
icon = folium.Icon(icon='info-sign', color='red')
).add_to(map_route)
return map_route
让我们用一个不同城市的旅游进行最终测试,纽约。下面的数据框包含了这座城市的一些标志性景点(和一家随机酒店),顺序没有特别规定:
df_route_ny = pd.DataFrame(
[['hotel', 40.710400, -74.006670],
['One World Trade Center', 40.713658, -74.013285],
['Federal Hall', 40.708051, -74.010223],
['Tenement Museum', 40.718807, -73.990051],
['Flatiron Building', 40.739665, -73.989976],
['Chrysler Building', 40.750542, -73.975632],
['New York City Library', 40.752471, -73.982162],
['Empire State Building', 40.747814, -73.985289],
['Madison Square Garden', 40.749924, -73.993853],
['Chelsea Market', 40.742750, -74.006877]],
columns=pd.Index(['site', 'latitude', 'longitude'], name='new york')
)
df_route_ny.index.name = 'visit_order'
df_route_ny

当路线的数据框准备好时,一个漂亮的可视化就只差调用一个函数:
display_route_on_map(df_route_ny)

图 6.11. 纽约随机路线的互动地图
我们得到了曼哈顿路线的一个简单而强大的可视化。只需一眼,我们就能知道路线的覆盖距离,甚至不需要麻烦地数红色标记:因为“站点数量”KPI 已经告诉了我们。更具体地说,所有标记和路线段都是互动的,当我们小心地将鼠标悬停在它们上面时,会弹出一些有用的信息。任务完成!
4. 结论(或下一个冲刺的计划)
在本文中,我们开发了一种务实的方法,利用数据框中存储的通用路线,并通过互动标记和段落将其显示在地图上,这有助于我们获得更多背景信息并获取更多数据。如果这正是你所需要的,我希望我能为你提供帮助,并且你能够在自己的项目中扩展这一点。
如果你是“Python 中的智能旅游决策支持系统”文章系列的读者,并且希望通过构建旅行规划系统继续学习应用性规范分析,请继续阅读,因为在下一个冲刺中,我们将把这个地图功能与之前冲刺中创建的旅行商优化器集成在一起,以便我们可以轻松评估生成的最佳旅游路线。敬请关注,见证我们将共同构建的壮丽成果!

使用 Python 开发的旅游智能决策支持系统
查看列表6 篇故事


感谢阅读,我们下次再见!📈😊
随时欢迎关注我,向我提问,给我反馈,或通过LinkedIn与我联系。
可视化 SQL Server 作业历史:利用 SQL Server 语言扩展解锁隐藏功能
轻松通过用户友好的互动时间线可视化来导航和分析作业工作负载
·发表于 Towards Data Science ·18 分钟阅读·2024 年 5 月 21 日
--

(图片由 ChatGPT 提供)
在任何企业中,数据在决策中发挥着关键作用。越来越多的情况下,这需要将原始数据转化为有用的信息。这些过程是企业内部商业智能过程的关键组成部分。鉴于其复杂性和显著的时间要求,必须合理安排这些数据转换活动的时间,以最大化效率和资源利用率。
这些活动通常在较大的组织中自动化并组织为作业,尤其是在数据操作庞大且多样的情况下。每个作业消耗不同数量的时间和资源。随着这些任务量的增加,挑战在于如何在 24 小时内优化调度它们。调度至关重要,不仅是为了保持操作效率,还要确保两个大型作业不会同时运行,从而导致系统过载或变慢。
数据库管理员(DBA)配备了提取详细作业执行日志的工具。这些日志提供了关于如何…
可视化实体嵌入的随机正则化
揭示神经网络如何感知类别数据及其层次结构
·发表于 Towards Data Science ·11 分钟阅读·2024 年 8 月 6 日
--

图片由 Rachael Crowe 提供,来自 Unsplash
行业数据通常包含许多可能值的非数值数据,例如邮政编码、医疗诊断代码、首选鞋履品牌。这些高基数类别特征包含有用信息,但将它们融入机器学习模型中是一门艺术。
我一直在写一系列关于这些特征方法的博客文章。在上一期中,我展示了如何通过扰动训练数据(随机正则化)在神经网络模型中显著减少过拟合并提高对未见类别编码的性能 [1]。
实际上,当使用层次信息与随机正则化结合时,模型对未见编码的性能可以接近已知编码的表现!
在这里,我使用可视化和 SHAP 值来“探究内部机制”,并深入了解实体嵌入如何响应随机正则化。这些图像很漂亮,看着数据变化时图表的变化也很酷。此外,这些可视化能够为模型改进提供建议,并能够识别出分析师可能感兴趣的群体。
NAICS 代码
可视化批量归一化及其优点
优化神经网络训练中的批量归一化
·发表于 Towards Data Science ·阅读时间 5 分钟·2024 年 2 月 3 日
--

可视化批量归一化及其优点。图片来源:作者
介绍
当你在进行深度学习项目时,是否遇到过这样的情况:神经网络层数越多,训练速度越慢?
如果你的回答是“是的”,那么恭喜你,是时候考虑使用批量归一化了。
什么是批量归一化?
顾名思义,批量归一化是一种技术,批量训练数据在当前层激活后、进入下一层之前会进行标准化。其工作原理如下:
-
整个数据集被随机划分为 N 个批次,且不重复,每个批次具有一个 mini_batch 大小,用于训练。
-
对于第 i 个批次,使用公式标准化批次内的数据分布:
*(Xi - Xmean) / Xstd*。 -
对标准化后的数据进行缩放和平移,使用公式
*γXi + β*,以便在需要时让神经网络撤销标准化的效果。
步骤看起来很简单,是吗?那么,批量归一化有哪些优点呢?
批量归一化的优点
使用 GNNs 进行符号化钢琴音乐中的声部与五线谱分离
本文涵盖了我最近发表的论文《集群与分离:基于 GNN 的乐谱雕刻中的声部与五线谱预测方法》,该论文发表于 ISMIR 2024。
·发表于Towards Data Science ·9 分钟阅读·2024 年 10 月 27 日
--

背景图像最初由 Dall-E 3 创建
介绍
用 MIDI 等格式编码的音乐,即使包含了量化的音符、节拍签名或小节信息,通常也缺少一些用于可视化的重要元素,比如声部和五线谱信息。这一限制同样适用于音乐生成、转录或编排系统的输出。因此,这种音乐无法轻松地转化为人类音乐家可以解读和演奏的可读乐谱。
值得注意的是,声部和五线谱分离只是乐谱雕刻系统可能需要解决的众多问题中的两个——其他问题还包括音高拼写、节奏分组和连音符创建等。
在音乐术语中,“声部”通常指的是一系列不重叠的音符,通常称为单声部。然而,当处理多声部乐器时,这一定义就显得不足。例如,声部也可以包括和弦,和弦是同时演奏的音符组,通常被视为一个整体。在这种情况下,我们将能够包含和弦的声部称为和声部。
问题
将量化的符号化音乐作品(例如 MIDI 文件)中的音符分离为多个声部和五线谱是一个重要且非平凡的任务。它是音乐乐谱雕刻(或乐谱排版)这一更大任务的基本部分,旨在为人类演奏者制作可读的乐谱。
乐谱是音乐家的一项重要工具,因为它能够以紧凑的图形形式传达音乐信息。与其他可能对机器来说更易定义和处理的音乐表示方式(如 MIDI 文件)相比,乐谱的特点在于训练有素的音乐家能够高效地读取它。

给定一个量化的 MIDI,有许多将其转换为可读格式的可能性,这通常包括将音符分配到不同的声音和五线谱中。
下面展示了其中两种可能性。它们展示了刻印系统通常是如何工作的。

大问题是,我们如何能让自动转录模型变得更好。
动机
为了开发一个更有效的系统,用于将音乐音符分离为声音和五线谱,特别是对于复杂的钢琴音乐,我们需要从不同的角度重新思考这个问题。我们的目标是改善从量化 MIDI 开始的转录音乐的可读性,这对于制作良好的乐谱刻印和提高音乐家表演效果至关重要。
对于良好的乐谱可读性,可能最重要的两个元素是:
-
五线谱的分离,将音符组织在上下五线谱之间;
-
和声音的分离,通过不同颜色的线条在这张图片中突显出来。

钢琴谱中的声音流
在钢琴谱中,如前所述,声音不是严格的单旋律,而是和声,这意味着单一的声音可以包含一个或多个同时演奏的音符。从现在开始,我们称这些为和弦。你可以在上面图片的底部五线谱中看到一些用紫色标出的和弦示例。
从机器学习的角度来看,我们需要解决两个任务:
-
第一个是五线谱分离,这是直接的,我们只需要为每个音符预测一个二元标签,指定为上五线谱或下五线谱,特别是对于钢琴谱。
-
声音分离任务看起来可能很相似,毕竟,如果我们能为每个声音预测声音编号,使用多类分类器,那么问题就能解决!
然而,直接预测声音标签是有问题的。我们需要固定系统可以接受的最大声音数量,但这会在系统的灵活性和数据中的类别不平衡之间造成权衡。
例如,如果我们将最大声音数量设置为 8,以便在每个五线谱中容纳 4 个声音,这正如在音乐符号软件中常见的做法那样,我们可以预期在数据集中标签 8 和 4 的出现次数会非常少。

使用绝对标签的声音分离
特别是看这段乐谱摘录,声音 3、4 和 8 完全缺失。高度不平衡的数据将降低多标签分类器的性能,如果我们设置更低的声音数量,我们就会失去系统的灵活性。
方法论
解决这些问题的办法是能够将系统在某些声音上学到的知识,迁移到其他声音上。为此,我们放弃了多类分类器的想法,并将声音预测框架为链接预测问题。如果两音符在同一声音中是连续的,我们希望将其连接。这种方法的优点是将一个复杂的问题分解为一组非常简单的问题,对于每对音符,我们再次预测一个二元标签,指示这两个音符是否链接。这种方法对于和弦也是有效的,如图中低音部分所示。
这个过程将创建一个我们称之为输出图的图。为了找到声音,我们可以简单地计算输出图的连接组件!
为了重复一下,我们将声音和五线谱分离问题框架为两个二元预测任务。
-
对于五线谱分离,我们预测每个音符的五线谱编号,
-
为了分离声音,我们预测每对音符之间的连接。
尽管不是绝对必要的,但我们发现为提高系统性能,添加一个额外的任务是有用的:
- 和弦预测,类似于声音,我们将每对属于同一和弦的音符连接起来。
让我们回顾一下到目前为止我们的系统样貌,我们有三个二元分类器,一个输入单个音符,另外两个输入音符对。现在我们需要的是良好的输入特征,这样我们的分类器才能在预测中使用上下文信息。使用深度学习术语,我们需要一个好的音符编码器!
我们选择使用图神经网络(GNN)作为音符编码器,因为它在符号音乐处理方面常常表现出色。因此,我们需要从音乐输入中创建一个图。
为此,我们从量化的 MIDI 中确定性地构建一个新的图,我们称之为输入图。

创建这些输入图可以通过像GraphMuse这样的工具轻松完成。
现在,将所有部分组合起来,我们的模型看起来像这样:

-
它从一些量化的 MIDI 开始,这些 MIDI 经过预处理后转化为图,生成输入图。
-
输入图通过图神经网络(GNN)处理,以为每个音符创建一个中间潜在表示。因此我们对每个音符进行编码,这部分我们称之为 GNN 编码器;
-
然后我们将其输入到一个浅层的 MLP 分类器中,进行我们的三个任务:声音、五线谱和和弦预测。我们也可以称这部分为解码器;
-
预测完成后,我们得到一个输出图。
直到现在为止,这种方法可以看作是一种图到图的方法,我们从基于 MIDI 构建的输入图开始,预测包含声音和和弦链接以及五线谱标签的输出图。
- 在最后一步,我们的输出图通过后处理程序生成一个美观且易于阅读的乐谱。
后处理的目标是去除可能导致无效输出的配置,如将一个声部分裂成两个声部。为了解决这些问题:
-
我们根据和弦预测头将属于同一和弦的音符进行聚类。
-
我们通过应用线性分配解决方案,确保每个节点最多有一个输入边和一个输出边;
-
最后,将信息传播回原始节点。

我们系统的后处理例程
我们系统的一个突出特点是其在音乐分析和乐谱刻印方面超越了其他现有系统。与依赖于音乐启发式的传统方法不同——这些方法有时可能不可靠——我们的系统通过保持简单但稳健的方法,避免了这些问题。此外,我们的系统能够计算整个作品的全局解,无需因低内存和计算需求而进行分段处理。此外,它还能够处理无限数量的声部,使其成为处理复杂音乐作品的更灵活且强大的工具。这些优势突显了系统稳健的设计及其以更高精度和效率处理音乐问题的能力。
数据集
为了训练和评估我们的系统,我们使用了两个数据集。J-pop 数据集包含 811 个流行钢琴乐谱,DCML 浪漫语料库包含 393 个浪漫音乐钢琴乐谱。相比之下,DCML 语料库更为复杂,因为它包含了许多具有挑战性的乐谱,如高声部数、声部交叉和五线谱交叉等。通过结合复杂和简易的数据,我们可以训练一个既稳健又灵活,能够适应各种输入类型的系统。
可视化预测结果
为了配合我们的系统,我们还开发了一个 Web 界面,可以在其中可视化和探索输入输出图形,用于调试复杂的案例,或者仅仅为了更好地理解图形创建过程。点击这里查看:GitHub 链接。

我们的 Web 界面,MusGViz!
为了公平比较并更深入地理解我们的模型是如何工作的,以及预测结果如何变化,我们仔细观察了其中的一些例子。
我们将地面真值边(链接)与我们的和弦和声部预测的边进行比较。请注意,在您查看的示例中,输出图形是直接绘制在乐谱上的,借助我们的可视化工具。

前两小节处理得非常完美,但我们可以看到在第三小节时系统的一些局限性。在音高范围接近的同步音符中,不同的声部安排可能会造成问题。
我们的模型预测一个包含所有同步切分四分音符的和弦(而不是跨谱表分割),同时也错误预测了第一个 D#4 音符所在的谱表。要深入研究为什么会发生这种情况并不简单,因为神经网络是不可直接解释的。
开放挑战
尽管我们的系统具有优势,但仍存在一些挑战需要未来开发解决。目前,版本中未考虑到装饰音,并且重叠音符必须在输入中明确重复,这可能会造成困扰。此外,尽管我们已经开发了一个初步的 MEI 导出功能用于可视化结果,但这一功能仍需要进一步更新,以完全支持符号乐谱中遇到的各种例外情况和复杂性。解决这些问题将是提升系统多功能性并使其更适应多样化音乐作品的关键。
结论
本博客介绍了一种基于图的方法,用于同音声音分离和符号钢琴音乐中的谱表预测。这种新方法的表现优于现有的深度学习或启发式系统。最后,它还包括一个后处理步骤,可以去除模型中的问题预测,避免产生错误的乐谱。
[## GitHub - CPJKU/piano_svsep: 论文《Cluster and Separate: 基于 GNN 的声音和谱表预测方法》代码]
论文《Cluster and Separate: 基于 GNN 的声音和谱表预测方法》代码
github.com](https://github.com/CPJKU/piano_svsep/?source=post_page-----0cab100629cf--------------------------------)
[所有图片均由作者提供]
Voyage Multilingual 2 嵌入评估
与 OpenAI、Cohere、Google 和 E5 相比
·发表于 Towards Data Science ·10 分钟阅读·2024 年 6 月 20 日
--

图片由作者使用 ChatGPT 生成
创造最佳 AI 工具的竞争日益激烈,主要由对先进语言模型的需求不断增加所推动。
Voyage AI 通过提供最先进的嵌入模型,迅速成为该领域的竞争者,并刚刚宣布发布一款全新的多语言嵌入模型,名为:
voyage-multilingual-2
在本文中,我将展示我对 voyage-multilingual-2 与该领域最先进技术的独立性能分析。
注意:我的分析涵盖了 17 种语言! ⭐
Voyage 的新模型如何与 OpenAI、Google、Cohere 和 E5 竞争?
(如果你只对结果感兴趣,可以跳到 结果 📊 部分)
免责声明:我已与 Voyage AI 接触数月,期待其新多语言模型的发布,并在发布前获得了一些模型的预览访问权限。
话虽如此,我的分析是独立研究,我并未因此收取任何报酬。
官方指标 📊
迈克尔·斯科特是世界上最好的老板吗?
使用 SchrutePy、NLTK 和 Hugging Face Transformers 🤗 对《办公室》电视剧进行情感分析
·发布于 Towards Data Science ·15 分钟阅读·2024 年 7 月 29 日
--

《办公室》怎么样?
当我年轻的时候,我总是听说《办公室》,并且在互联网上经常看到剧中的片段。我决定尝试一下——毕竟看起来是一部很酷的节目——但是由于当时在大学,没有办公室工作经验,我没能理解其中的内容,结果在看了几集之后就放弃了。几年后,在我进入企业办公室工作一段时间后,我在 Netflix 上随机发现了它,决定再试一次。我简直被迷住了!看了几集之后,我甚至觉得自己好像也在为 Dunder Mifflin 工作。每个角色都让人感到亲切,仿佛自己也曾经历过那种每周 9 到 5 的普遍焦虑,看到这一点时,我感到一丝宽慰。太喜欢了!
然而,迈克尔总是让我感到不舒服。我无法理解他。他看起来傻乎乎的,经常惹恼大家,但我们并不真正讨厌他,因为他内心深处是个甜蜜而善良的人。如果你仔细想想,剧中的每个人都有点像个混蛋。迈克尔、德怀特和安吉拉肯定是这样,但随着时间的推移,你也会意识到吉姆也有点烦人,甚至是菲莉丝或帕姆也有些让人心烦…
从太空观察风暴:一个用于创建惊人视图的 Python 脚本
使用 Python 和 Google Colab 从静止卫星图像创建动画,实时动态监控风暴(大气河流)
·发表于 Towards Data Science ·13 分钟阅读·2024 年 2 月 6 日
--
目录
-
🌟 介绍
-
🌐 静止轨道气象卫星(GOES)
-
🔍 AWS 中的 GOES 数据库
-
⏳ 下载 GOES NetCDF 文件
-
📊 GOES 图像的可视化
-
🎥 风暴的时间推移
-
📄 结论
-
📚 参考文献
🌟 介绍
如果你在关注新闻,你会知道目前有一个突发新闻,关于即将袭击加利福尼亚并影响中南部地区的强大风暴,称为大气河流。大气河流是一个狭窄的风带,能够从热带地区输送浓缩的水蒸气(湿气)。预计即将来临的风暴将给该州部分地区带来强降雨和严重的突发洪水。
昨晚,在查看我的 Twitter 账户(现在叫 X)时,我看到许多关于风暴的推文,以及一些色彩斑斓的地图,预测并模拟了加利福尼亚州的降水量。这些地图很快成为了我撰写这篇文章的动力。
水冷器闲聊:赌徒谬误与破产
对赌博、运气和风险的直观而深入的解释,帮助你避免显得愚蠢
·发布于Towards Data Science ·阅读时长:11 分钟·2024 年 9 月 12 日
--

图片由Amanda Jones提供,来源于Unsplash
水冷器闲聊是一种特殊的闲聊,通常发生在办公室空间里,尤其是在水冷器附近。在那里,就像当代的穴居人围着火堆一样,员工们常常聚集在一起,分享各种各样的公司八卦、迷思和传说、不准确的科学观点、不得体的个人轶事,或者干脆就是谎言。什么都可以聊。所以,在这篇文章中,我讨论了前几天在水冷器旁听到的一个科学意见,听完之后我简直无言以对。
以下是今天这篇文章的水冷器闲聊观点:
“在轮盘赌中,如果你一开始输钱,但有足够的钱继续赌博——每次加倍下注以弥补之前的损失——你最终会赢。换句话说,如果你输掉了,比如 100 次,你的运气最终会好转,你将赢得胜利——而这 supposedly 被认为是一种好策略。”
🤪🎲
直观上,这个观点可能似乎有些道理,但实际上它在许多方面都是错误的。实际上,这是一个糟糕的策略——糟糕到甚至有特定的术语来描述它在文化和数学中的荒谬性…
水冷小谈:辛普森悖论
统计学
你的数据在欺骗你吗?你能做些什么呢?
·发布于Towards Data Science ·阅读时间:9 分钟·2024 年 11 月 29 日
--

图片由作者使用 GPT-4 创作/除非另有说明,其他所有图片均由作者创作
水冷小谈是一种特殊的小谈,通常发生在办公室的水冷器旁。在那里,员工们经常分享各种公司八卦、迷思、传说、不准确的科学观点、私密的个人轶事,甚至是彻头彻尾的谎言。什么都可以说。因此,在我的“水冷小谈”帖子中,我会讨论在办公室里无意间听到的那些奇怪且通常科学上无效的观点,它们让我哑口无言。
这是今天帖子中的“水冷小谈”时刻:
让我们简单点——我只需要一个显示大局的数字,汇总数据。不需要过于复杂……
当然,但如果大局隐藏了真正的故事呢?🤷🏻♀️如果事情实际上只是复杂呢?商业用户喜欢“仅仅一个数字”的概念——它简单、干净、易于理解。然而,现实很少与“仅仅一个数字”相符。实际上,现实世界通常更复杂且微妙,充满了层层信息和细节,而一个单一的数字无法告诉我们实际发生了什么。
水冷器闲聊:生日悖论 🎂🎉
数据科学 | 统计学
一窥共享生日的反直觉数学
·发表于Towards Data Science ·阅读时长:9 分钟·2024 年 9 月 25 日
--

由作者使用 GPT-4 创建的图像
水冷器闲聊是一种特殊的闲聊形式,通常出现在办公室的水冷器旁。员工们在这里经常分享各种公司八卦、神话传说、不准确的科学观点、不拘小节的个人轶事,甚至是完全的谎言。什么都能聊。所以,在我的水冷器闲聊文章中,我会讨论一些我在办公室里听到的奇怪且通常没有科学根据的观点,这些观点让我简直哑口无言。
这是今天文章的水冷器讨论:
我在一个有 60 多人的办公室楼层工作,上周有两个人在同一天过生日。得知他们的生日相同后,他们大概进行了如下对话:
我简直不敢相信我们竟然在同一天过生日!
是的,这概率有多小?
..大家都评论说这太奇怪了。简直是一次疯狂的、业力的巧合……
🌌🥳
但说真的,这几率有多小?快速估算一下,你可能会认为这样的事件发生的几率应该非常低……
水冷机旁小谈话:为什么蒙提霍尔问题仍然困扰我们?🐐🚗
统计学
一窥游戏节目谜题背后的反直觉数学
·发表于Towards Data Science ·9 分钟阅读·2024 年 11 月 20 日
--

图片由作者使用 GPT-4 创建 / 除非另有说明,所有其他图片均由作者创作
水冷机旁的小谈话是一种特殊的小谈话,通常出现在办公室周围的水冷机旁。在那里,员工们经常分享各种公司八卦、传说、误传的科学观点、不慎泄露的个人轶事,或者干脆是谎言。什么都可以谈。所以,在我的《水冷机旁小谈话》系列文章中,我讨论了在办公室里无意中听到的奇怪且通常在科学上无效的观点,这些观点让我简直哑口无言。
这是今天文章的水冷机旁观点:
‘-在一个游戏节目中,你可以在三扇门中选择一扇:一扇门后藏着一辆车,另外两扇门后藏着山羊。你选择了一扇门,然后主持人揭示了另一扇门后藏着一只山羊,并给你一个选项,可以把你最初选择的门换成剩下的那一扇。你应该换门吗?
‘-不,我会保持我最初选择的那扇门。无论怎样,概率是 50-50。’
🚗🚪🐐🤪
如果你还没有意识到,这是著名的蒙提霍尔问题。剧透警告,机会不是 50-50;概率是 1/3…
人工智能文本和合成蛋白质的水印

彩色病毒突变图像,作者 Nataliya Smirnova,来源:UnSplash
理解生物学中人工智能应用对机器学习工程师的意义
·发布于数据科学前沿 ·阅读时间:7 分钟·2024 年 11 月 7 日
--
虚假信息和生物恐怖主义并非新兴威胁,但它们能够被迅速释放的规模和便利性大大增加。大型语言模型(LLMs)使得制造自主聊天机器人来散播纷争变得轻而易举,而生成性蛋白质设计模型则极大地扩展了能够从事生物战争的行为体的数量。我们作为社会所需要的工具多种多样,但其中一个重要组成部分将是我们发现这些威胁存在的能力。这就是水印的作用所在。
水印或数字水印,与那个会挟持你孩子学校照片的物理水印不同,是一种用于标识所有权的秘密信号。有效的水印必须具有鲁棒性,能够在不被专门方法探测的情况下承受修改。它们在各个创意领域得到广泛应用,从保护版权的数字图像和视频到确保文件的完整性。如果我们能为生成性人工智能(GenAI)开发出有效的水印技术,我们就能在应对虚假信息和生物恐怖主义的斗争中获得一项强有力的工具。
在我们的系列中,我们探讨了其他生成性文本和生物学突破如何依赖相关的架构突破,而当前的水印提案也不例外。谷歌在 2024 年 10 月宣布了SynthID-Text,这是一种已准备投入生产的文本水印方案,作为 Gemini 的一部分部署。它们的方法通过应用一个秘密的随机函数来修改最终的采样过程或推理,马里兰大学帕克分校团队的生成蛋白质设计水印提案也采用了类似的方法。


(左)一幅来自罗马帝国的早期虚假信息示例,《亚克提乌姆战役》由洛伦佐·A·卡斯特罗绘制,这幅画作为凯撒死后宣传运动的一部分。 (右)本作者记得的第一个生物安全事件,2001 年炭疽邮件袭击。巧合的是,这也是一个虚假信息的例子,因为犯罪者试图将责任嫁祸给穆斯林。
水印技术中的关键理想特性
鲁棒性 — 它应该能够承受水印文本/结构的扰动。
如果最终用户在发布之前可以简单地替换几个词,或者蛋白质可以发生突变并变得无法检测到,那么水印就不足够。
可检测性 — 它应该能够通过特殊方法可靠检测,但不能通过其他方法检测。
对于文本,如果水印可以在没有秘密密钥的情况下被检测到,通常意味着文本已经被严重扭曲,读者会觉得它很奇怪。对于蛋白质设计,如果可以裸露地检测到水印,可能会导致设计质量的下降。
文本水印和 Synthtext-ID
让我们深入探讨这个话题。如果你像我一样,花太多时间在Twitter上,你可能已经意识到,很多人发现 ChatGPT 过度使用某些词汇。其中一个就是“delve”(深入),其过度使用正在被用来分析学术文章是否由 ChatGPT 撰写或在其帮助下完成。这本身就是一种“脆弱”的水印技术,因为它可以帮助我们识别由大型语言模型(LLM)撰写的文本。然而,随着这一点成为常识,查找和替换“delve”实例变得太容易了。但 SynthText-ID 背后的理念是可以的,我们可以通过选词的概率来区分 AI 和人类写作的文本。

深入探讨 PubMed 记录:随着 ChatGPT 的到来,一些医学写作中的术语发生了巨大变化
SynthText-ID 使用“比赛抽样”根据随机水印函数修改令牌被选中的概率。这是一种高效的水印方法,因为它可以在推理过程中完成,而不需要更改训练过程。这种方法改进了吉布尔抽样,该方法在抽样步骤之前对 LLM 的概率分布进行随机扰动。
在论文的示例中,序列“my favorite tropical fruit is”可以通过候选令牌中的任何一个(如芒果、榴莲、荔枝等)来圆满完成。这些候选者是从 LLM 的概率分布中抽样的,条件是前面的文本。构建括号后,选取的令牌将在一个水印函数的基础上进行评分,该函数基于上下文窗口和水印密钥。这个过程将统计签名引入生成的文本中,稍后可进行测量。

来自《可扩展水印技术用于识别大型语言模型输出》的比赛抽样示例,演示了比赛抽样技术。Dathathri, S., See, A., Ghaisas, S., Huang, P., McAdam, R., Welbl, J., … & Kohli, P. (2024). 可扩展水印技术用于识别大型语言模型输出. Nature, 631(7624), 755–759
为了检测水印,每个令牌都会用水印函数进行评分,平均得分越高,文本来自 LLM 的可能性就越大。应用一个简单的阈值来预测文本的来源。
该签名的强度由几个因素控制:
-
比赛的轮数(m)(通常为 m=30),每一轮都会加强签名(并且也减少得分方差)。
-
LLM 的熵。低熵模型无法提供足够的随机性,导致比赛无法选择得分较高的候选者。FWIW,这对作者来说似乎是个大问题,因为他从未使用过除 temperature=0 以外的任何设置。
-
文本的长度;较长的序列包含更多的证据,因此统计上的确定性更高。
-
是否使用非扭曲和扭曲配置。
扭曲是指在保持文本质量与检测之间所做的权衡。非扭曲配置优先考虑文本质量,以换取可检测性的牺牲。扭曲配置则相反,使用比每轮比赛更多的令牌,从而允许更多的自由度选择得分最高的令牌。谷歌表示,他们将在 Gemini 中实现一个非扭曲版本的该算法。
非扭曲版本在 400 个标记序列的情况下,达到了接近 90%的 TPR(真正阳性率),假阳性率为 1%,这大约是 1-2 段文字。一个(非付费)推文或 X 帖子限制为 280 个字符或大约 70-100 个标记。该长度下的 TPR 只有大约 50%,这引发了人们对这种方法在实际应用中效果的质疑。也许它对于抓捕懒惰的大学生有用,但在选举期间对于外国势力就不一定有效了?

水印可检测性是通过在假阳性率(FPR)设置为 1%时,使用真正阳性率(TPR)来衡量的,适用于 SynthText-ID 和 Gumble 采样。Dathathri, S., See, A., Ghaisas, S., Huang, P., McAdam, R., Welbl, J., … & Kohli, P. (2024). 可扩展水印技术用于识别大型语言模型输出。Nature, 631(7624), 755–759
生成性蛋白质水印与生物安全性
生物安全性是一个你可能在新冠疫情后开始频繁听到的词汇。我们可能永远无法确切知道病毒是来自湿市场还是实验室泄漏。然而,借助更好的水印工具和生物安全实践,我们可能能够追溯到下一个潜在大流行的具体研究人员。为此目的已有现存的数据库日志记录方法,但希望通过生成性蛋白质水印技术,能够追踪到即使是新的或修改过的序列,这些序列可能与现有的危险特征不匹配,同时水印也能够更强大地抵抗突变。这还将带来提升研究人员隐私保护和简化知识产权过程的好处。
当文本被水印过程扭曲时,可能会让读者感到困惑或听起来很奇怪。更严重的是,生成性蛋白质设计中的扭曲可能会使蛋白质变得完全没有价值或功能上发生改变。为了避免扭曲,水印必须不会改变设计蛋白质的总体统计属性。
水印过程与 SynthText-ID 相似。它不是修改标记的概率分布,而是调整氨基酸残基的概率分布。这个过程是通过一个无偏重的重加权函数(Gumble 采样,而不是锦标赛采样)实现的,该函数根据研究人员的私钥派生的水印码,将原始残基的概率分布进行转换。Gumble 采样被认为是无偏的,因为它专门设计用于以一种保持原始分布统计属性而不引入系统性错误的方式,近似地求得一组值的最大值;或者平均而言,引入的噪声会相互抵消。

(a) 现有的生物安全流程,IGSC 记录所有发送进行 DNA 合成的序列。(b) 提议的水印过程,通过 Gumble 采样添加无偏噪声。(c) 研究人员如何使用他们的私钥给蛋白质添加水印,并帮助 IGSC 识别可疑蛋白质。Chen, Y., Hu, Z., Wu, Y., Chen, R., Jin, Y., Chen, W., & Huang, H. (2024). 增强生物安全性与水印蛋白质设计. bioRxiv.
研究人员通过使用ProteinMPNN,一个基于深度学习的蛋白质序列设计模型,验证了重加权函数在实验中的无偏性。随后,使用ESMFold(进化尺度建模)在水印前后预测 pLDDT 或预测的局部距离差异测试。结果表明,性能没有变化。

蓝色和红色分别代表原始的 ProteinMPNN 和带水印的 ProteinMPNN。每个小提琴图包含来自 60 个不同单体设计任务的 3000 个 pLDDT 分数,每个任务有 50 个不同的设计,结果显示没有变化。Chen, Y., Hu, Z., Wu, Y., Chen, R., Jin, Y., Chen, W., & Huang, H. (2024). 增强生物安全性与水印蛋白质设计. bioRxiv.
类似于低温 LLM 设置下的检测,当可能的高质量设计数量较少时,检测变得更加困难。结果产生的低熵使得在不引入显著变化的情况下嵌入可检测水印变得困难。然而,这一限制可能比 LLM 的类似限制轻微。低熵设计任务可能只有少数几种蛋白质符合要求,这使得使用现有数据库方法追踪它们变得更容易。
主要结论
-
LLM 和蛋白质设计的水印方法正在改进,但仍然需要进一步提高!(不能仅依赖它们来检测机器人军团!)
-
两种方法都专注于修改采样过程;这非常重要,因为它意味着我们不需要编辑训练过程,且它们的应用计算效率较高。
-
温度和文本长度是影响水印可检测性的两个重要因素。当前的方法(SynthText-ID)在 1%假阳性率下,对于 1 至 2 段长度的序列,召回率(TPR)大约为 90%。
-
一些蛋白质具有有限的可能结构,因此这些蛋白质更难以加水印。然而,现有方法应该能够使用数据库检测这些序列。
在 BigQuery 中改进你的 RFM 模型
用于更好客户洞察的高级策略
·发表于 Towards Data Science ·14 分钟阅读·2024 年 3 月 26 日
--
RFM(Recency, Frequency, Monetary)模型,以其简单性和易于实施的特点,依然是客户关系管理中的一款出色工具,能够提供有价值的客户行为洞察。
基于我之前文章“如何在 BigQuery 中创建 RFM 模型”的基础,在这篇文章中,我们将探讨如何改进该模型。
在这篇文章中我们将涵盖以下内容:
-
如何创建客户评分
-
替代的 RFM 分组方法
-
向模型添加里程碑
-
扩展模型以包括电子邮件互动
-
构建月度 RFM 模型并追溯历史月份
所以,你已经在 BigQuery 中启动并运行了你的 RFM 模型,将客户分为如 冠军、潜在忠诚者、有丧失风险 等类别。这是一个很好的开始,但我们可以将其提升到一个新的水平。
将你的客户分组虽然讲述了一个不错的故事,但我喜欢称之为 客户评分 的方法,可以通过一个直观的单一指标来增强模型的效果。
什么是客户评分,为什么需要它?

通过 DALL-E 创建
我们为 PySpark 构建了一个开源数据质量测试框架
轻松衡量和报告您的数据质量
·发表于Towards Data Science ·阅读时间:7 分钟·2024 年 8 月 16 日
--

[图片由作者提供,使用 Dall-E 生成]
每个数据科学家都知道经典的说法“垃圾进,垃圾出”。因此,衡量数据质量是至关重要的。
在Woonstad Rotterdam,一家荷兰的社会住房协会,我们在 Databricks 中使用 PySpark 进行 ETL。来自我们外部软件供应商的数据通过 API 加载到我们的数据湖中。然而,并不是每个软件供应商都在进行数据质量测试。社会住房领域中数据错误的后果可能非常严重,范围从租户无法申请补贴,到租金被定为根据《负担得起的租金法案》规定为非法的价格。因此,我们为 PySpark DataFrame 构建了a data quality testframework,以便向供应商和数据用户报告数据质量。
社会住房领域中数据错误的后果可能非常严重,范围从租户无法申请补贴,到租金被定为根据《负担得起的租金法案》规定为非法的价格。
我们需要提高人工智能产品经理的标准
如何停止怪罪“模型”,并开始构建成功的人工智能产品
·发表于Towards Data Science ·8 分钟阅读·2024 年 8 月 9 日
--

由作者使用 Midjourney 生成的图像
产品经理负责决定构建什么,并对其决策的结果负责。这适用于所有类型的产品,包括那些由人工智能驱动的产品。然而,在过去十年中,产品经理们通常将人工智能模型视为“黑箱”,将不良结果的责任推给模型开发者。
产品经理:我不知道模型为什么会这样做,去问模型开发者。
这种行为就像在网站重新设计后,怪罪设计师导致用户注册量下降一样没有道理。科技公司假设负责消费产品的产品经理能够凭直觉做出关于设计变更的明智决策,并对结果负有责任。
那么,为什么这种不干涉的人工智能方法成为了常态呢?
问题:产品经理被激励保持与模型开发过程的距离。
实践型与不干涉型人工智能产品管理——概览
这种更为严格的实践型方法有助于确保模型能够成功落地,并为用户提供最佳体验。
实践型方法需要:
-
更多的技术知识和理解。
-
承担更多风险和责任,特别是对于推出时已知的问题或权衡。
-
需要 2 到 3 倍的时间和精力——创建评估数据集以系统地衡量模型行为可能需要从几小时到几周的时间。
不确定什么是“评估”(eval)吗?请查看我关于什么是“评估”,产品经理为什么应该关注它?的文章。
九成情况下,当一个模型的推出失败时,使用的是放手型方法。在那些具有长时间 AI 产品应用历史的大公司中,如 Netflix、Google、Meta 和 Amazon,情况较少发生,但本文并非针对这些公司。
然而,克服放手型方法的惯性可能是具有挑战性的。尤其是当公司领导层没有期望更多时,PM 在采用实践型方法时,甚至可能会因为“减缓”开发周期而面临反对意见。
实践型与放手型产品管理 — 模型开发过程
想象一下,亚马逊等市场平台上的 PM 负责为父母开发一个产品捆绑推荐系统。考虑这两种方法。
放手型 AI PM — 模型要求
目标:增加购买量。
评估:无论模型开发者认为什么是最好的。
指标:通过 A/B 测试来决定是否将其推送给 100%的用户,如果购买率有显著的统计学意义上的提升。
实践型 AI PM — 模型要求
目标:帮助父母发现他们未曾意识到的优质产品,进而使他们的育儿旅程更加轻松。
指标:主要指标是推动年轻父母购买产品。我们将监控的次要长期指标是首次通过捆绑包发现的品牌的复购率,以及市场中品牌多样性随时间变化的情况。
评估:除了进行 A/B 测试外,我们的离线评估集将会查看多个样本用户在育儿不同关键阶段(优先考虑怀孕期、新生儿、婴儿、幼儿、儿童)以及四个收入区间的样本推荐。如果我们在这里看到任何意外情况(例如:低收入家庭推荐最昂贵的产品),我们需要更仔细地审视训练数据和模型设计。
在我们的评估集里,我们将考虑:
-
个性化 — 查看有多少人购买了相同的产品。我们预计不同收入和不同年龄段的孩子会有所不同。
-
避免冗余 — 如果捆绑包中已经有一个相同的物品,或者用户已经从我们这里购买了此类物品(如婴儿床、瓶子加热器等耐用商品),则应对重复推荐进行惩罚(不要对消耗品如尿布或收藏品如玩具进行惩罚)。
-
连贯性 — 不同阶段的产品不应混合(例如:婴儿奶瓶和 2 岁儿童衣物)。
-
一致性 — 避免将完全不同的产品混合,例如:超级昂贵的手工木制玩具与非常便宜的塑料玩具,带有授权角色的鲜艳印花与柔和的粉彩。
次要目标的可能驱动因素
-
考虑为重复购买的产品设置额外权重进行实验。即使我们前期卖出的捆绑包稍少一些,如果意味着购买者未来更有可能购买更多产品,这也是一种值得的权衡。
-
为了支持市场的长期健康发展,我们不希望只偏向畅销品。在进行质量检查的同时,确保至少 10%的推荐产品不是该类别中的第一名品牌。如果从一开始没有做到这一点,模型可能会默认采用“最低公分母”行为,很可能没有进行适当的个性化推荐。
实践中的 AI 产品管理——模型开发者协作
模型架构应该由模型开发者决定,但产品经理应该在以下方面有较强的发言权:
-
模型优化的目标是什么(这应该比“更多购买”或“更多点击”深入一两层)。
-
模型性能将如何评估。
-
用于评估的例子。
实践型方法客观上需要更多的工作!而且这还假设产品经理从一开始就参与到模型开发过程中。有时模型开发者具有良好的产品经理直觉,并能够在模型设计中考虑用户体验。然而,一家公司永远不应该指望这一点,因为在实际情况中,一个对用户体验非常敏感的模型开发者是千分之一的“独角兽”。
此外,放手型方法可能仍然在某些时候有些有效。然而,实际上这通常会导致:
-
亚最佳模型性能,可能会导致项目失败(例如:高管们得出结论,捆绑包只是个糟糕的主意)。
-
错失了显著提升的机会(例如:提升 3%而不是 15%)。
-
对生态系统的长期影响未加监控(例如:小品牌离开平台,导致对少数大型玩家的依赖加重)。
实践型与放手型产品管理——产品评审
除了前期工作更多,实践型方法还能从根本上改变产品评审流程。
放手型 AI 产品经理评审
领导者:为父母设计的捆绑包似乎是个好主意。让我们看看它在 A/B 测试中的表现。
实践中的 AI 产品经理评审
领导者:我读了你的提案。如果这些畅销品真的是最好的产品,那只推荐它们有什么问题呢?我们不应该做对用户最有利的事情吗?
[半小时辩论后]
产品经理:正如你所看到的,畅销品并不一定适合每个人。以尿布为例,低收入家庭应该了解亚马逊品牌尿布,它的价格是畅销品的一半。高收入家庭则应该了解新兴的昂贵品牌,它深受富裕顾客喜爱,因为它用起来就像云一样。而且,如果我们总是偏向已有的类目赢家,从长远来看,新兴但更好的产品将难以崭露头角。
领导者:好吧。我只是想确保我们不会不小心推荐一个糟糕的产品。你提议的质量控制指标是什么,确保这种情况不会发生?
模型开发者:为了确保只展示高质量的产品,我们使用以下信号……
无所作为的 AI 产品管理隐藏成本
上述对比情景揭示了 AI 产品管理中的一个关键节点。虽然积极主动的产品经理成功应对了一个具有挑战性的对话,但这种方法并非没有风险。许多产品经理在面临快速交付的压力时,可能会选择最省力的路径。
毕竟,放手的做法承诺更顺畅的产品评审、更快速的审批流程,并且如果事情出错,还能找到一个方便的替罪羊(模型开发者)。然而,这种短期的轻松背后,却隐藏着巨大的长期成本,对产品和整个组织而言都是如此。
当产品经理不深入参与 AI 开发时,显而易见的问题和关键的权衡决策将被掩盖,进而导致一系列重大后果,包括:
-
目标不一致:如果产品经理无法深入了解用户需求和业务目标,模型开发者可能会只优化那些容易衡量的指标(比如点击率),而非真正的用户价值。
-
意外的生态系统影响:孤立优化的模型可能会带来深远的后果。例如,始终推荐畅销产品可能会逐渐将小品牌推出市场,减少多样性,甚至可能危害长期的平台健康。
-
责任扩散:当决策“交由模型”时,就会形成一个危险的责任真空。产品经理和领导者不能为他们从未明确考虑或批准的结果负责。这种缺乏明确责任的情况,可能会导致一种文化,没人觉得有权主动解决问题,最终可能导致小问题变成大危机。
-
劣质模型的延续:如果产品经理没有通过产品视角仔细审视模型的不足之处,最高影响的改进就无法被识别和优先处理。承认并承担这些不足,对于团队在发布时做出正确的权衡决策至关重要。如果没有这种态度,表现不佳的模型将成为常态。这种回避的循环会阻碍模型的进化,浪费 AI 推动真实用户和业务价值的潜力。
产品经理可以采取的第一步,变得更积极主动?问问你的模型开发者,你如何能帮助评估!有许多优秀的免费工具可以帮助这个过程,比如 promptfoo(Shopify 首席执行官的最爱)。
领导力必要性:重新定义期望
产品领导在提升 AI 产品标准方面扮演着至关重要的角色。正如 UI 变更需要经过多轮评审一样,AI 模型也需要同等,甚至更严格的审查,因为它们对用户体验和长期产品结果的影响深远。
促进产品经理与模型开发更深度参与的第一步,是让他们对了解自己正在交付的内容负责。
提出以下问题:
-
您使用了什么评估方法?您是如何收集示例的?我可以查看样本结果吗?
-
您认为哪些使用场景对于第一版的支持最为重要?为了实现这一点,我们是否需要做出某些权衡?
在使用不同的评估方法时,请考虑它们的适用场景:
-
对于部署在高风险场景中的模型,考虑将使用评估集作为要求。这还应该与严格的后期影响和行为分析相结合,尽可能深入到漏斗的各个环节。
-
对于部署在低风险场景中的模型,考虑允许快速首次发布,进行较不严格的评估,但在收集到用户行为数据后,推动快速的后期迭代。
-
调查模型训练和评分中的反馈循环,确保人工监督不仅仅局限于精度/召回率指标。
并且记住,迭代是关键。初始发布的模型通常不应是最终版本。确保后续工作的资源得到保障。
最终,人工智能的广泛应用带来了巨大的前景,也对产品所有权的定义带来了重大变化。为了充分发挥其潜力,我们必须超越过去常常导致次优结果的放手态度。产品领导者在这一转变中扮演着关键角色。通过要求产品经理更深入地理解 AI 模型,并培养一种问责制文化,我们可以确保 AI 产品被深思熟虑地设计、严格地测试,并真正造福用户。这需要许多团队提升技能,但相关资源已经触手可及。人工智能的未来取决于此。
Welch t 检验:比较两个方差不等的总体均值的可靠方法
发现为什么即使方差不同,Welch t 检验也是进行准确统计比较的首选方法。
·发布于 Towards Data Science ·阅读时间 8 分钟·2024 年 6 月 14 日
--

图片由 Simon Maage 提供,来源于 Unsplash
第一部分:背景
在我研究生的第一学期,我有机会选修了课程 STAT7055:商业与金融入门统计学。在整个课程中,我确实有时感到有些疲惫,但我获得的关于在不同情境下应用各种统计方法的知识,真的是无价的。在第 8 周的讲座中,有一个非常有趣的内容引起了我的注意,特别是比较两个总体时的假设检验概念。我发现了解如何根据样本是独立的还是配对的来决定方法的不同,以及在我们知道或不知道两个总体的方差的情况下该如何操作,甚至如何进行两个比例的假设检验,都是非常吸引人的。然而,有一个方面是材料中没有涉及的,它让我一直在思考如何处理这种特定情境,那就是在方差不相等的情况下,如何进行两个总体均值的假设检验,这就是Welch t 检验。
为了理解 Welch t 检验是如何应用的,我们可以通过一个数据集来探讨这个示例案例。这个过程的每个阶段都涉及使用来自真实世界的数据集。
第二部分:数据集
我使用的数据集包含关于世界农业供应与需求估算(WASDE)的实际数据,并定期更新。WASDE 数据集由世界农业展望委员会(WAOB)编制。这是一个每月发布的报告,提供关于小麦、大米、粗粮、油籽和棉花等全球各地区以及美国的年度预测。此外,数据集还涵盖了美国的糖、肉类、家禽、鸡蛋和牛奶的预测。该数据源自 NASDAQ 网站,您可以在这里免费访问:WASDE 数据集。数据集中有三个子集,但我只使用第一个,即供应与需求数据。列定义可以在这里查看:

图 1:由NASDAQ定义的列
我将使用来自特定地区、商品和项目的两个不同样本来简化测试过程。此外,我们将使用 R 编程语言进行端到端的过程。
现在让我们进行适当的数据准备:
library(dplyr)
# Read and preprocess the dataframe
wasde_data <- read.csv("wasde_data.csv") %>%
select(-min_value, -max_value, -year, -period) %>%
filter(item == "Production", commodity == "Wheat")
# Filter data for Argentina and Australia
wasde_argentina <- wasde_data %>%
filter(region == "Argentina") %>%
arrange(desc(report_month))
wasde_oz <- wasde_data %>%
filter(region == "Australia") %>%
arrange(desc(report_month))
我将两个样本分为两个不同的区域,即阿根廷和澳大利亚。重点是小麦商品的生产。
现在我们准备好了。但等一下..
在进一步探讨 Welch t 检验的应用之前,我不禁想知道,为什么有必要检验两个总体方差是否相等。
第三部分:检验方差的平等性
在进行假设检验以比较两个总体均值时,如果我们不了解总体方差,那么确认方差的平等性至关重要,以便选择适当的统计检验。如果方差相同,我们选择合并方差的 t 检验;否则,我们可以使用 Welch 的 t 检验。这个重要步骤保证了结果的准确性,因为使用不正确的检验可能会导致错误结论,从而增加 I 型和 II 型错误的风险。通过检验方差是否平等,我们确保假设检验过程基于准确的假设,从而得出更可靠和有效的结论。
那么我们如何检验两个总体方差呢?
我们需要生成如下的两个假设:

图 2:作者对检验方差平等性的零假设与备择假设
经验法则非常简单:
-
如果检验统计量落入拒绝区域,那么拒绝 H0 或零假设。
-
否则,我们无法拒绝 H0 或零假设。
我们可以这样设定假设:
# Hypotheses: Variance Comparison
h0_variance <- "Population variance of Wheat production in Argentina equals that in Australia"
h1_variance <- "Population variance of Wheat production in Argentina differs from that in Australia"
现在我们应该进行检验统计量的计算。那么,我们如何得到这个检验统计量呢?我们使用F 检验。
F 检验是一种统计检验,用于比较两个样本的方差或多个样本之间方差的比率。检验统计量 F 是一个随机变量,用于确定在零假设成立且假设误差项符合常规假设的条件下,所测试数据是否具有 F 分布。

图 3:维基百科绘制的 F 分布概率密度函数(PDF)示意图
我们可以通过以下方式计算检验统计量值,即通过两样本方差相除:

图 4:作者绘制的 F 检验公式
拒绝区域是:

图 5:作者绘制的 F 检验拒绝区域
其中,n 是样本大小,alpha 是显著性水平。所以,当 F 值落入拒绝区域时,我们拒绝零假设。
但是……
关键是:样本 1 和样本 2 的标记其实是随机的,所以每次都要确保将较大的样本方差放在上面。这样,我们的 F 统计量始终大于 1,并且我们只需参考上界来在显著性水平 α 下拒绝 H0。
我们可以通过以下方式进行:
# Calculate sample variances
sample_var_argentina <- var(wasde_argentina$value)
sample_var_oz <- var(wasde_oz$value)
# Calculate F calculated value
f_calculated <- sample_var_argentina / sample_var_oz
我们将使用 5% 的显著性水平(0.05),因此决策规则是:
# Define significance level and degrees of freedom
alpha <- 0.05
alpha_half <- alpha / 2
n1 <- nrow(wasde_argentina)
n2 <- nrow(wasde_oz)
df1 <- n1 - 1
df2 <- n2 - 1
# Calculate critical F values
f_value_lower <- qf(alpha_half, df1, df2)
f_value_upper <- qf(1 - alpha_half, df1, df2)
# Variance comparison result
if (f_calculated > f_value_lower & f_calculated < f_value_upper) {
cat("Fail to Reject H0: ", h0_variance, "\n")
equal_variances <- TRUE
} else {
cat("Reject H0: ", h1_variance, "\n")
equal_variances <- FALSE
}
结果是我们在 5% 显著性水平下拒绝零假设,换句话说,从这次检验中我们认为两个总体的方差不相等。现在我们知道为什么我们应该使用 Welch t 检验而不是合并方差 t 检验。
第四部分:主要内容,Welch t 检验
Welch t 检验,也称为 Welch 不等方差 t 检验,是一种用于比较两个独立样本均值的统计方法。与标准的合并方差 t 检验假设方差相等不同,Welch t 检验更为稳健,因为它不做这一假设。这一自由度的调整使得我们可以更精确地评估两个样本均值之间的差异。由于不假设方差相等,Welch t 检验在处理现实世界数据时能提供更可靠的结果,尤其是在方差不相等的情况下。由于其适应性和可靠性,它成为了首选方法,确保即使方差相等假设不成立,统计分析得出的结论依然有效。
检验统计量公式为:

图 6:作者绘制的 Welch t 检验的检验统计量公式
其中:

自由度可以这样定义:

图 7:作者绘制的自由度公式
Welch t 检验的拒绝区域取决于选择的显著性水平,以及检验是单尾还是双尾。
双尾检验:如果检验统计量|t|的绝对值大于 t 分布在ν自由度下的临界值(在α/2 显著性水平下),则拒绝原假设。
- ∣t∣>tα/2,ν
单尾检验:如果检验统计量 t 大于 t 分布在ν自由度下的临界值(适用于上尾检验),或者 t 小于下尾检验的负临界值,则拒绝原假设。
-
上尾检验:t > tα,ν
-
下尾检验:t < −tα,ν
那么让我们做一个单尾 Welch t 检验的例子。
让我们生成假设:
h0_mean <- "Population mean of Wheat production in Argentina equals that in Australia"
h1_mean <- "Population mean of Wheat production in Argentina is greater than that in Australia"
这是上尾检验,因此拒绝区域为:t > tα,ν
并且使用上述公式,保持相同的显著性水平(0.05):
# Calculate sample means
sample_mean_argentina <- mean(wasde_argentina$value)
sample_mean_oz <- mean(wasde_oz$value)
# Welch's t-test (unequal variances)
s1 <- sample_var_argentina
s2 <- sample_var_oz
t_calculated <- (sample_mean_argentina - sample_mean_oz) / sqrt(s1/n1 + s2/n2)
df <- (s1/n1 + s2/n2)² / ((s1²/(n1² * (n1-1))) + (s2²/(n2² * (n2-1))))
t_value <- qt(1 - alpha, df)
# Mean comparison result
if (t_calculated > t_value) {
cat("Reject H0: ", h1_mean, "\n")
} else {
cat("Fail to Reject H0: ", h0_mean, "\n")
}
结果是我们在 5%的显著性水平下未能拒绝 H0,因此阿根廷的小麦生产量的总体均值等于澳大利亚的总体均值。
这就是如何进行 Welch t 检验。现在轮到你了。祝你实验愉快!
第五部分:结论
在进行假设检验时,比较两个总体均值时,首先检查方差是否相等非常重要。这个初步步骤至关重要,因为它有助于决定使用哪种统计检验,确保结果的准确性和可靠性。如果方差确实相等,可以继续使用标准的合并方差 t 检验。然而,如果方差不相等,建议使用 Welch 的 t 检验。
Welch 的 t 检验提供了一种强有力的解决方案,用于在方差不相等的情况下比较均值。通过调整自由度以适应不均匀的方差,Welch 的 t 检验提供了一个更精确、可靠的评估,能够判断两个样本均值之间差异的统计显著性。其适应性使得它在许多实际情况中非常受欢迎,尤其是在样本大小和方差可能差异很大的情况下。
总结来说,检查方差是否相等,并在需要时使用 Welch 的 t 检验,可以确保假设检验的准确性。这种方法减少了第一类错误和第二类错误的发生概率,从而得出更可靠的结论。通过根据方差的相等性选择适当的检验,我们可以自信地分析结果,并根据实证证据做出明智的决策。
参考资料
-
纳斯达克数据链接。(无日期)。纳斯达克数据链接。检索于 2024 年 6 月 14 日,来源:
data.nasdaq.com -
统计中的 F 检验。(2024 年 6 月 14 日)。统计中的 F 检验。
www.geeksforgeeks.org/f-test/ -
两个独立样本方差不等(Welch 检验)— ENV710 统计回顾网站。(无日期)。两个独立样本方差不等(Welch 检验)— ENV710 统计回顾网站。于 2024 年 6 月 14 日从
sites.nicholas.duke.edu/statsreview/means/welch/检索。
《我在 Uber、Meta 和初创公司工作的 10 年,教会了我关于数据分析的经验》
给数据科学家和经理的建议
·发布于Towards Data Science ·9 分钟阅读·2024 年 5 月 30 日
--

图片由作者提供(通过 Midjourney 生成)
在过去的 10 年里,我在多个公司担任分析岗位,从德国的一家小型金融科技初创公司到快速增长的 IPO 前规模公司(Rippling)以及大型科技公司(Uber, Meta)。
每家公司都有独特的数据文化,每个职位都有自己面临的挑战和一套艰苦获得的经验教训。以下是我在过去十年中的十大关键经验,其中许多经验无论公司处于哪个阶段、产品或商业模式,依然适用。
1. 你需要通过数据讲述一个故事。
想想你的听众是谁。
如果你在一个以研究为中心的组织工作,或者大部分时间是在向技术相关人员(例如工程团队)汇报,那么学术风格的“白皮书”式分析可能是最佳选择。
但如果你的听众是非技术性的业务团队或高管,你就需要确保你专注于关键洞察,而不是陷入技术细节,并且将你的工作与它所要影响的商业决策相联系。如果你过多地关注分析的技术细节,你会失去听众;职场沟通的重点不是你觉得有趣的内容,而是听众需要听到的内容。
这种基于洞察力、从上而下的沟通方式最著名的做法是麦肯锡顾问 Barbara Minto 提出的金字塔原理。可以参考这篇最新的 TDS 文章,了解如何作为数据科学家更好地进行沟通。
2. 强大的商业洞察力是优秀和卓越数据科学家的最大区别。
如果你是某个高标准公司的资深数据科学家,你可以期望所有同行都拥有强大的技术能力。
你不会通过逐步提升你的技术技能来脱颖而出,而是通过确保你的工作为利益相关者(例如产品、工程、商业团队)带来最大影响来脱颖而出。
这就是商业洞察力发挥作用的地方:为了最大化你的影响力,你需要1) 深入了解业务的优先事项和利益相关者面临的问题,2) 规划直接帮助这些优先事项或解决这些问题的分析解决方案,3) 以你的听众能够理解的方式传达你的见解和建议(见上面的#1)。
拥有强大的商业洞察力,你也能够对自己的工作进行理智的检查,因为你具备业务背景和判断力,能够理解你的分析结果或建议是否合理。
商业洞察力不是学校或数据科学训练营所教授的内容;那么如何发展它呢? 以下是一些你可以采取的具体措施:
-
在公司全员大会和其他跨团队会议中,当讨论战略优先事项时要特别留意。
-
练习将这些优先事项与你团队的工作联系起来;在规划周期或新项目出现时,问自己:“这与高层次的业务优先事项有何关联?”如果你无法建立联系,可以与经理讨论。
-
当你在做分析时,始终问自己“那又怎么样?”。只有在你能回答这个问题并阐明为什么别人应该关心它时,数据点或见解才变得相关和有影响力。基于这些数据,他们应该做出什么不同的行为?
最终目标是从接受请求和处理进入的 JIRA 工单,转变为成为利益相关者的思想合作伙伴,与他们共同塑造分析路线图。
3. 成为客观的真理探索者
很多人会挑选数据来符合他们的叙事。这是可以理解的:大多数组织奖励的是达成目标的人,而不是最客观的人。
作为数据科学家,你有推翻这一点的空间。数据科学团队通常不直接负责业务指标,因此相比销售等团队,面临的短期目标压力较小。
利益相关者有时会施压要求你找到支持他们已经提前构建的叙事的数据。虽然迎合这一点可能会在短期内为你加分,但从长远来看,帮助你的是成为一个真理的探索者,并推动数据真正支持的叙事。

图片来源:作者(通过 Midjourney 创作)
即使这在当下可能让人感到不舒服(因为你可能在推销一些大家不想听到的说法),这将帮助你脱颖而出,并将你定位为当高管需要了解真实情况时,他们会寻求的那种不加过滤、不偏不倚的观点。
4. 数据 + 原始研究 = ❤️
数据分析师通常会对“轶事证据”嗤之以鼻,但它是严格定量分析的必要补充。
运行实验并分析大数据集可以为你提供统计学上显著的洞察,但你常常会错过一些信号,这些信号要么因为尚未达到足够大的规模而无法在数据中显现,要么因为结构化数据无法有效捕捉到这些信号。
深入研究关闭失败的交易记录、与客户交谈、阅读支持票等,有时是揭示某些问题(或真正理解根本原因)唯一的方式。
例如, 假设你在一家 B2B SaaS 公司工作。你可能会在数据中看到,你们的企业级交易的胜率正在下降,也许你甚至能把问题归结为某一类客户。
但要真正理解发生了什么,你必须与销售代表交谈,深入了解他们的交易记录,与潜在客户沟通等。在开始时,这可能看起来像是随机的轶事和杂音,但过一段时间后,一个模式将开始显现;而这个模式很可能并没有出现在你追踪的任何标准化指标中。
5. 如果数据看起来好得不真实,通常它就是不真实的
当人们看到某个指标急剧上升时,他们往往会兴奋,并将这种变化归因于他们所做的某件事,例如最近的功能发布。
不幸的是,当一个指标变化显得异常积极时,它通常是由于数据问题或偶发效应。例如:
-
最近时期的数据不完整,一旦所有数据点都齐全,指标会趋于平稳。
-
有一个一次性的顺风因素,不会持续(例如,你看到在 1 月初销售有所增长;但这并不是销售业绩的持续改善,而只是节假日期间积压订单的清理)。
不要因为指标的上升而感到过度兴奋。你需要保持健康的怀疑态度、好奇心和经验,以避免陷阱并生成有力的洞察。
6. 保持开放心态,随时准备改变看法
如果你从事数据工作,定期改变自己的观点是很自然的。例如:
-
你曾向高管推荐了一种行动方案,但由于获取了更多数据,你失去了对该路径的信心,觉得它不再是前进的正确方向。
-
你以某种方式解读了一个指标的变化,但你做了进一步的分析后,觉得事情可能是另外一种情况。
然而,大多数分析型人才因为害怕显得不称职或激怒利益相关者,而对撤回自己曾经的言论感到犹豫。
这是可以理解的;改变推荐方案通常意味着利益相关者需要额外的工作来适应新的现实,而这可能会导致他们因此感到不满。
但你仍然不应该仅仅因为害怕丢面子而坚持以前的建议。一旦你对自己的观点失去了信心,就无法很好的为其辩护。像杰夫·贝佐斯这样的领导者认识到,当面对新信息时,或者当你从不同角度看待问题时,改变主意的重要性。只要你能清晰地阐明为什么你的建议发生了变化,这是一种力量和智力严谨的表现,而不是软弱。
经常改变主意非常重要。你不应该让任何人用你过去说过的话束缚住自己。——杰夫·贝佐斯
7. 你需要务实一些。
在分析领域工作时,很容易养成完美主义的习惯。你接受过科学方法的训练,并且以知道进行分析或实验的理想方法为傲。
不幸的是,经营业务的现实往往给我们带来了巨大的限制。我们需要比实验能够提供统计学意义上的显著结果更快的答案,或者我们没有足够的用户来进行合理的无偏分割,或者我们的数据集的时间范围不够长,无法建立我们希望观察的时间序列模式。
你的工作是帮助那些运营业务的团队(那些负责推出产品、达成交易等的团队)完成工作。如果你坚持完美的方法,业务很可能会在没有你和你的见解的情况下继续前进。
就像许多事情一样,完成比完美更重要。
8. 不要用临时请求让你的数据科学家们过度疲劳。
招聘全栈数据科学家,主要负责构建仪表板或进行临时数据提取和调查,肯定会导致他们的职业倦怠,并且团队流动性加剧。
许多公司,尤其是高速成长的初创公司,都不愿意聘请专门从事指标调查和仪表板构建的 数据分析师 或商业智能人员。人员名额有限,经理们希望团队能够灵活应对各种任务,因此他们雇佣全面的 数据科学家,并计划偶尔给他们安排仪表板任务或指标调查请求。
然而,实际上,这种情况往往会失控,数据科学家会在这些任务上花费过多的时间。他们被 Slack 消息打断,脱离了专注的工作,而那些“快速请求”(实际上从来没有看起来那么快)积累起来,占据了整整一天,使得他们很难在并行的大型战略项目上取得进展。
幸运的是,解决方案是存在的:
-
实施一个 AI 聊天机器人,能够处理简单的数据问题。
-
培训相关团队掌握基础的 SQL(每个团队至少 1-2 名分析师),让他们更独立。借助Snowflake SQL AI 助手或BigQuery 中的 Gemini 助手,如今不再需要深入的 SQL 语法知识即可提取数据并生成洞察。
-
使用自助式 BI 工具,赋予用户自主权和灵活性,让他们能够获得所需的洞察力。近年来,这方面取得了大量进展,像 Omni 这样的工具让我们更接近一个自助分析成为现实的世界。
9. 并非所有东西都需要一个精美的 Tableau 仪表盘。
公司通常会认为,数据从电子表格转移到 BI 解决方案中是一个成熟且强大的数据文化的标志。
虽然组织内由多个利益相关者广泛使用并作为基础做出关键且难以逆转决策的仪表盘应该放在受管控的 BI 工具中(例如 Tableau),但也有许多情况下,Google Sheets 就能满足需求,而且比起花费数天或数周构建强大仪表盘,这样做可以更快地达成目标。
事实上,团队将始终依赖他们日常使用的软件的分析功能(例如 Salesforce)以及电子表格,因为他们需要快速行动。鼓励这种灵活的、去中心化的分析方式,而不是强行通过 BI 工具的瓶颈来操作,可以帮助你保留数据科学团队的资源(参见上面第 8 点),并为团队提供他们成功所需的工具(基本的 SQL 培训、数据建模和可视化最佳实践等)。
10. 在整个公司中实现完美标准化的指标是一种空想。
如上面第 9 点所述,跨公司团队始终通过在 BI 工具之外进行快速的分析来解决问题,这使得强制执行共享数据模型变得困难。特别是在快速发展的初创公司中,如果你希望确保团队能够快速行动并完成工作,那么实施完美的治理几乎是不可能的。
虽然当指标定义不匹配时会让许多数据科学家感到头疼,但实际上,这并不意味着世界末日。通常情况下,数字之间的差异足够小,不会改变整体叙事或最终的推荐结果。
只要关键报告(例如进入生产环节、提交给华尔街的报告等)经过严格处理并遵循标准化定义,公司中数据稍微凌乱也是可以接受的(即使这可能让人感觉不太舒服)。
最终思考
上面提到的一些观点刚开始可能会让你感到不适(例如反驳挑选出来的叙事、采取务实的方法而不是追求完美等)。但从长远来看,你会发现,这将帮助你脱颖而出,并确立自己作为真正的思想伙伴的地位。
如果你想获取更多实用的分析建议,可以在 Medium 上关注我,或者在LinkedIn和Substack上关注我。
15 位顶尖数据科学家的建议
回顾一下你成为一名“优秀”数据科学家所需要的主要技能
·发布于 Towards Data Science ·阅读时间 7 分钟·2024 年 9 月 26 日
--

图片由 Campaign Creators 提供,来源于 Unsplash
成为一名“优秀”甚至“伟大”的数据科学家需要具备哪些核心技能?
好吧,我向来自多个公司和行业的 15 位数据科学家询问了他们的看法,可以说我对一些回答感到相当惊讶。
希望这篇文章能为你提供更好的指导,帮助你明确在数据科学职业中该专注于哪些方面,以便进步!
结果
为了避免浪费你的时间,以下是 15 位数据科学家描述的前 10 项技能,已通过图表进行总结。

图表由作者使用 Python 和 ChatGPT 生成。
我觉得挺有意思的是,技术技能如数学和编程与软技能如沟通和好奇心的结合。
就个人而言,这很有道理。数据科学家的真正核心是发现趋势并…
什么是数字双胞胎?
桥接物理世界和数字世界
·发表于数据科学前沿 ·阅读时间:6 分钟·2024 年 10 月 18 日
--

AI 生成的图像,使用 DALL-E3。
你听说过“数字双胞胎”这个术语吗?还是我只是自从在一次会议全体报告中接触到它之后,就一直不断听到它的提法?
从本质上讲,数字双胞胎是物理实体的虚拟模型——一台机器、一座建筑,甚至是一个星球!得益于近年来计算能力的进步,这种虚拟复制品正在成为现实。
但数字双胞胎到底是什么,以及为什么它被认为是技术创新的下一前沿?
让我们在本文中一探究竟!
什么是数字双胞胎?
假设我们有一个双胞胎,但不是一个真实的人,而是我们自己的数字版本。这个数字双胞胎将像我们一样行动,但存在于计算机中。它可以显示我们身体的实时信息,例如压力水平或血压,甚至预测我们未来可能出现的症状。
尽管想象人体可能有些困难,但让我们来看一个更实际的例子。如果我们有一辆车的数字双胞胎,我们就可以跟踪发动机的运行情况,监控刹车磨损等,甚至可以得到何时进行维护的建议。汽车的数字双胞胎可以通过…
情绪在法律上的定义是什么?这真重要吗?
情绪环路
探索情绪识别技术中的情绪法律框架
·发表于Towards Data Science ·阅读时间 9 分钟·2024 年 3 月 1 日
--

图片由 Comuzi 提供 / © BBC / 更好的人工智能图片 / Mirror D / CC-BY 4.0
最近我开始写一系列名为《情绪环路》的文章,在首篇文章(“分析(扫描版)简的生活”)中,我设想了一个终极个人助理,它与各种个人设备互联,并配备多种情绪识别系统。我写下第一篇文章,尽管它带有浓厚的科幻色彩,原因有以下几点:
-
在我陷入分析这个相互关联系统的法律细节之前,先来点乐趣。
-
提出一些我希望在这一系列中探讨的问题。
然而,在我们进入这两个问题之前,我认为有必要先关注一下在这个背景下,情绪究竟是什么,无论是在法律上还是在技术上。正如我们很快将看到的,这两种含义是密不可分的。为了不浪费太多时间和篇幅,我们现在就开始吧!
1. 情绪的力量
“情绪控制着我们生活的杠杆,无论是内心的歌声,还是驱动我们科学探索的好奇心。康复顾问、牧师、父母,甚至在某种程度上,政治家都知道,影响人们最大的不是法律,而是他们所跟随的节奏。”
R.W. Picard
情感一直是我们感兴趣的主题。从亚里士多德描述情感为“那些如此改变人的感觉,以至于影响他们的判断。” [1] 到查尔斯·达尔文和威廉·詹姆斯,他们首次将情感与其身体表现及原因联系在一起。 [2] 一直到现代技术,比如我们可爱的扫描珍(Jane)所使用的技术,这些技术收集并解读这些身体表现,似乎比我们自己更了解我们。
这种迷恋促使我们不仅发现情感的力量,还发现如何影响情感的方法。这些方法既可以让我们自己和他人感觉更好,也可以用来欺骗和操控他人。现在,人工智能进入了这个领域,慷慨地提供了在大规模上识别和影响情感的可能性。然而,尽管我们似乎对情感及其对我们的影响深感兴趣,我们的法律仍然不足以保护我们免受恶意情感操控。造成这种情况的原因之一是,因为我们仍然不确定情感到底是什么,更不用说从法律角度定义它了。因此,让我们尝试看看是否至少能回答第二个问题,即使第一个问题依然难以捉摸。
2. 情感作为(个人)数据?
当我们从法律角度,特别是在人工智能的背景下思考情感时,可能会想到它们是一种数据。甚至可能是个人数据。毕竟,什么比情感更个人化呢?比你小心翼翼地保守的感觉更个人化的东西是什么?那么,让我们简要地考虑一下这个假设。
根据《GDPR》,个人数据被定义为“任何与已识别或可识别的自然人相关的信息。” 可识别的自然人是指那个(至少理论上)可以通过某些方式被识别的人,无论是直接还是间接。在这个背景下,情感略显问题的地方在于它们是普遍的。悲伤、快乐、愤怒或兴奋并不会告诉我任何能够让我识别出体验这些情感的主体的信息。但这种看法过于简单化了。
首先,情感数据从未存在于真空中。相反,它是通过处理大量(有时多,有时少,但总是)个人数据来推断的。通过分析我们的健康数据,如血压和心率,以及我们的生物特征数据,如眼动、面部扫描或声音扫描,来推断情感数据。而通过将所有这些数据点结合起来,实际上是可以识别一个人的。 [3] 甚至《通用数据保护条例》(GDPR)也证明了这一点,它在个人数据的定义中就已经解释了,通过引用“与自然人的身体、心理、基因、精神、经济、文化或社会身份特征相关的一个或多个因素”,可以实现间接识别。 [4]
最简单的例子当然是各种可穿戴设备和个人设备中的情感识别系统,比如简的设备,在这些系统中,数据与她的用户档案和社交媒体数据直接相关,使得识别变得更加简单。然而,即使我们不处理个人设备,仍然有可能间接识别个人。例如,一个站在智能广告牌前的人,根据他们的情感状态和其他显著特征,收到一则广告[5]。为什么?因为身份识别是相对的,并且高度依赖于上下文。例如,如果我说“我看到一个看起来很难过的女孩”,和我说“看看街对面那个看起来很难过的女孩”并不相同。通过缩小上下文范围和可能涉及的其他人群体,身份识别变得非常有可能,即使我所使用的只是非常通用的信息[6]。
此外,是否能辨识一个人也将大大取决于我们对这个词的定义。换句话说,我们可能指的是‘通过名字和/或其他公民数据来识别’。然而,这样的定义是荒谬的,因为这些数据是可变的,可能被伪造和操控,更不用说并非所有人都有这些数据了。(想想那些通常没有任何官方身份证明的非法移民。)没有身份证的人就无法被识别吗?我认为不是。或者,如果可以识别,那我们对身份识别的理解就有严重问题。这也成为了越来越多的作者将数据处理操作视为符合 GDPR 相关性的常见论点,许多人将身份识别广泛理解为‘个体化’[7],“区分”[8],甚至是‘定向’[9],这些都是这些系统设计之初的目标。
所以,看起来情感和情感数据很可能也在 GDPR 的范围内,无论处理这些数据的公司是否利用它来识别个人。然而,即使它们不被用于识别,推断情感的数据肯定总是个人数据。这反过来使 GDPR 适用。我们现在并不打算深入探讨这意味着什么,或者 GDPR 的条款在大多数(甚至所有?)情感识别技术提供商中如何被侵犯。毕竟,他们仍在忙于辩称情感数据本来就不是个人数据。
3. 这真的重要吗?
我们都听过这句话:“你知道我的名字,却不知我的故事。”我们也都知道,这句话非常真实。我们的名字(以及其他明显的个人数据)所能透露的信息,比我们的情感少得多。我们的名字也不能用于相同的侵入性和削弱权力的目的,至少不会像识别我们的情感状态那样。也正因如此,在大多数情况下,这些系统的提供商并不会识别我们的身份。他们不关心你的名字是什么,他们关心的是你在乎什么,你关注什么,什么让你兴奋或困扰。
当然,其中一些销售健康设备,这些设备然后推测你的情感和心理状态以达到健康目的。就像扫描简(Jane)那样,很多人可能正是为了这个目的购买这些设备。然而,大多数(所有?)设备并不仅仅是这么做的。为什么要让这些宝贵的数据浪费掉呢?当它也可以被用来与其他商业实体合作,为你提供个性化的广告,甚至为你省去麻烦,直接为你下单。毕竟,他们所合作的实体,只不过是你(推测上)需要的东西的其中一个靠谱的选择。
最后,考虑到这些目的,特别是当涉及到其他非健康设备的情感识别系统时,识别特定情感的意义也变得越来越不重要。这使得关于是否‘解读’情感是科学还是伪科学的辩论在很大程度上变得无关紧要。以及之前讨论过的情感是什么的问题,因为那时我们必须进行同样的思维练习,去识别和使用任何他们最终能够识别的状态。例如,现如今,将你放在 PAD(愉悦-激动-支配)量表上的位置要更为重要。 [10] 这足以评估你对特定信息、情况、广告或其他事物的整体态度。它是引发积极还是消极的感觉,它是否吸引了你的注意力,是否让你变得积极主动或是被动接收?而这些足以让你在恰当的时刻看到恰到好处的广告。如果你仍然在看到广告的话,而不是直接把东西送到你门口。
4. 最后的思考
那么,情感是什么?它们是很多东西。它们是我们当前状态和环境的内在感知,是这些感知的外部表现形式,以及对这些表现形式的计算性读取。这个冗长的过程也使得它们成为一种数据。当与其他数据点结合,或在足够狭窄的背景下使用时,这些数据在某些情况下可能是个人的。而这无关情感在心理学意义上的定义。除非我们希望卷入对注意力和兴奋是什么的讨论,或者让系统提供商通过声称他们并不真正识别情感,而仅仅是在 PAD 量表上追踪用户反应,从而逃避 GDPR 的范畴。
这个结论使得任何单一实体识别和使用通过“读取”我们的面部表情和身体信号获得的数据时,也要对它所做的事情负责。其最低的义务位于数据收集目的的全面披露和提供一个易于访问的反对处理的可能性之间。当前,这远未成为普遍做法。
即使是在谈到简的那些过于聪明的设备时,仅仅在服务条款中提到数据是如何被收集、如何用于预测她的情绪状态,并基于这些状态做出决策,并不意味着透明度已经达标。那么,我们能从中得到的影响力又在哪呢?我们不断将数据流发射到我们设备处理的轨道中,如何才能对这些数据流产生任何影响?
最后,当考虑其他非个人设备时,情况当然变得更糟。无论是基于情绪的社交媒体算法,还是智能广告牌,它们都处理个人数据,以生成更多(如我们已确定的)个人数据,并用它来影响我们的行为。完全意识到在个人经过广告牌或浏览社交媒体时,透明度要求很难实现,但它们依然是要求,而非仅仅是有帮助的建议。设想实现有意义透明度的新方法是必须的。思考情绪和情感数据的健全方式也是必须的,避免心理学上的技巧和讨论。否则,我们可能很快就会失去所有剩余的决策权。

图片来自Nik于 Unsplash
[1] 亚里士多德,《修辞学》,公元前 350 年,由 W. Rhys Roberts 翻译,见classics.mit.edu/Aristotle/rhetoric.2.ii.html。
[2] P. Ekman,《达尔文对我们理解情绪表达的贡献》,《哲学会刊:伦敦皇家学会 B 卷生物学科学辑》,2009 年 12 月 12 日;364(1535):3449–3451。doi: 10.1098/rstb.2009.0189,www.ncbi.nlm.nih.gov/pmc/articles/PMC2781895/。
[3] L. Sposini,《神经营销与眼动追踪技术在欧洲框架下:走向 GDPR 及其未来》,《消费者政策杂志》,doi.org/10.1007/s10603-023-09559-2
[4] 第 29 工作组,关于个人数据概念的第 4/2007 号意见,01248/07/EN WP 136,www.clinicalstudydatarequest.com/Documents/Privacy-European-guidance.pdf 第 32 页,第 38 页;2016 年 10 月 19 日判决,C-582/14 Breyer,ECLI:EU:C:2016:779,第 32 段。
[5] J. Metcalfe,《一块会咳嗽并对吸烟者发起攻击的智能广告牌》,《彭博社》,2017 年 1 月 17 日,www.bloomberg.com/news/articles/2017-01-17/a-smart-billboard-that-detects-and-coughs-at-smokers?embedded-checkout=true
[6] 第 29 条工作组,《关于个人数据概念的意见 4/2007》,01248/07/EN WP 136,www.clinicalstudydatarequest.com/Documents/Privacy-European-guidance.pdf 第 13 页。
[7] Vidal-Hall v Google, Inc. [2015] EWCA Civ 311
[8] P. Davis,《面部识别与智能广告牌:分析 GDPR 中“已识别”个人数据标准》(2020 年 1 月 21 日)。奥斯陆大学法学院研究论文№2020–01,SSRN 可获取:ssrn.com/abstract=3523109 或 dx.doi.org/10.2139/ssrn.3523109
[9] N. Purtova,《从知名到定向:GDPR 下的身份识别意义》,《国际数据隐私法》,第 12 卷,第 3 期,2022 年 8 月,163–183 页,doi.org/10.1093/idpl/ipac013
[10] Kalinin, A., Kolmogorova, A.(2019)。基于 Lövheim 情感模型的虚构书籍自动化原声带生成。在:Eismont, P., Mitrenina, O., Pereltsvaig, A.(编)《语言、音乐与计算》。LMAC 2017。计算机与信息科学通信,第 943 卷。Springer, Cham。doi.org/10.1007/978-3-030-05594-3_13,B. J. Lance 和 S. Marsella,《凝视行为与情感归因的关系:一项实证研究》,2008 年 9 月,第 8 届国际智能虚拟代理大会论文集
我从 2024 年构建 LLM 应用中学到了什么?——第一部分
一位工程师构建 LLM 原生应用的历程
·发表于Towards Data Science ·阅读时间:7 分钟·2024 年 11 月 4 日
--

大型语言模型(LLM)正在迅速改变我们对人工智能的理解,并且通过将 LLM 与 Web 应用结合的创新设计,已经显现出显著的变化。自 2022 年底以来,多个框架、SDK 和工具相继推出,展示了将 LLM 与 Web 应用或商业工具集成的简单原型。随着大量投资流入基于生成性 AI 的商业应用和工具,逐渐将这些原型推向生产阶段并创造商业价值变得至关重要。如果你打算投入时间和金钱来构建一个 LLM 原生工具,如何确保这笔投资能够在长期内获得回报?
为了实现这一目标,必须建立一套开发 LLM 应用的最佳实践。我在过去一年中开发 LLM 应用的经历非常激动人心,充满了学习的机会。凭借近十年的 Web 和云原生应用设计与构建经验,我意识到传统的产品开发规范往往无法满足 LLM 原生应用的需求。相反,持续的研究、实验和评估循环在创建优秀的 AI 驱动产品方面更为有效。
为了帮助你应对 LLM 应用开发中的挑战,我将在以下关键领域讨论最佳实践——使用案例选择、团队心态、开发方法、负责任的 AI 和成本管理。
创意生成:选择合适的使用案例
每个问题都需要 AI 来解决吗?答案是否定的。相反,你应该问自己,哪个业务场景最能从利用 LLM 中受益?企业在开始构建应用程序之前需要问这些问题。有时候,合适的用例就在我们面前,其他时候,与同事交谈或在组织内进行研究可能会引导你找到正确的方向。以下是一些可能帮助你做出决定的方面。
-
提议的解决方案有市场需求吗? 对提议的用例进行市场调研,了解当前的市场情况。识别任何现有的解决方案,无论是否集成了 AI,分析它们的优缺点,以及你的 LLM 应用可以填补的空白。这涉及对竞争对手、行业趋势和客户反馈的分析。
-
它是否帮助了用户? 如果你提议的解决方案旨在服务于组织内部的用户,一种常见的衡量用户期望的方式是检查该解决方案是否能够通过节省时间提高他们的生产力。一个常见的例子是 IT 或 HR 支持的聊天机器人,帮助员工解决关于组织日常问题的查询。此外,进行一项简短的调查,了解潜在用户的痛点,也有助于理解可以通过 AI 来解决的问题。
-
它是否加速了业务流程? 另一类用例可能是解决业务流程改进,间接影响用户。例如包括对呼叫中心转录的情感分析、生成个性化推荐、总结法律和金融文档等。对于这种类型的用例,实现自动化可以成为将大语言模型(LLM)融入常规业务流程的关键因素。
-
我们有可用的数据吗? 大多数基于 LLM 的应用程序使用 RAG(检索增强生成)原则,从特定的知识文档中生成有上下文且有根据的答案。任何基于 RAG 的解决方案的核心在于数据的可用性、类型和质量。如果你没有足够的知识库或高质量的数据,你的解决方案的最终结果可能无法达到预期。数据的可访问性也很重要,因为机密或敏感数据可能并不总是触手可得。
-
提议的解决方案可行吗? 是否实施 AI 解决方案不仅取决于技术可行性,还涉及伦理、法律和财务等方面。如果涉及敏感数据,则在最终确定用例之前,还应考虑隐私和法规遵从性。
-
解决方案符合你的业务需求吗? 思考你的 AI 解决方案可以服务的短期和长期业务目标。管理期望值也是至关重要的,因为过于雄心勃勃的短期目标可能不会有助于实现价值。从 AI 应用中获取收益通常是一个长期过程。
设置正确的期望值
在选择用例的同时,产品负责人还应考虑设定正确的期望值以及为团队设定短期且可达成的里程碑。每个里程碑应有明确的目标和时间表,并且得到团队的定义和一致同意,这样利益相关者才能定期审查结果。这对于做出有关如何推进所提议的基于 LLM 的解决方案、生产化策略、用户引导等方面的明智决策至关重要。
实验:采用正确的‘心态’
研究和实验是任何涉及 AI 的工作核心。构建 LLM 应用程序也不例外。与传统的 Web 应用程序不同,后者遵循一个预先决定的设计,变化极少或没有变化,而基于 AI 的设计则高度依赖实验,并且可能根据早期的结果发生变化。成功的关键在于在明确预期的基础上进行迭代实验,之后不断评估每次迭代的结果。在 LLM 原生开发中,成功标准通常是输出的质量,这意味着重点在于生成准确且高度相关的结果。这些结果可以是聊天机器人的回复、文本摘要、图像生成,甚至是 LLM 定义的一个动作(Agentic 方法)。要始终如一地生成高质量的结果,需要对底层语言模型有深入的理解,不断微调提示词,并进行严格的评估,以确保应用程序符合期望标准。
团队需要什么样的技术技能组合?
你可能会认为只有少数数据科学家的团队就足以为你构建一个 LLM 应用程序。但实际上,工程技能同样重要,甚至更为关键,因为 LLM 应用程序并不遵循传统的机器学习方法。无论是数据科学家还是软件工程师,都需要改变一些心态,以熟悉开发方法。我见过这两个角色都经历了这样的过程,例如,数据科学家开始熟悉云基础设施和应用程序部署,而工程师则开始了解模型使用的复杂性以及评估 LLM 输出的方式。最终,你需要团队中有AI 从业者,他们不仅仅是来‘编程’的,而是来研究、协作并改进 AI 的适用性。
既然我们要使用预训练的语言模型,真的需要‘实验’吗?
像 GPT-4o 这样流行的 LLM 已经在大量数据集上进行了训练,能够识别和生成文本、图像等,因此你无需“训练”这些类型的模型。很少有场景需要微调模型,但这也可以轻松实现,无需传统的机器学习方法。然而,我们不要将“实验”一词与用于预测性机器学习中的“模型训练”方法混淆。正如我在上面提到的,应用程序输出的质量很重要。设置实验迭代可以帮助我们达到目标质量。例如——如果你正在构建一个聊天机器人,并且你希望控制机器人的输出如何呈现给最终用户,那么通过在提示改进和超参数微调上的迭代和实验性方法,将帮助你找到生成最准确、一致输出的正确方法。
在项目初期尽早构建原型。
尽早构建一个只包含核心功能的原型(也称为最小可行产品 MVP),理想情况下是在 2-4 周内。如果你正在使用知识库进行 RAG 方法,使用数据子集来避免过多的数据预处理。
-
从目标用户子集获取快速反馈,帮助你了解解决方案是否满足他们的期望。
-
与利益相关者进行审查,不仅要展示好的结果,还要讨论团队在原型构建过程中发现的局限性和约束条件。这对于尽早减轻风险至关重要,也有助于在交付决策时做出知情判断。
-
团队可以最终确定技术栈、安全性和可扩展性要求,以便将原型转化为完全功能的产品并确定交付时间表。
确定你的原型是否已经准备好转变为“产品”。
多个以 AI 为重点的样本的可用性使得创建原型变得非常容易,且这些原型的初步测试通常会提供有前景的结果。当原型准备好时,团队可能会对成功标准、市场调研、目标用户群体、平台需求等有更深入的了解。在这一点上,考虑以下问题可以帮助决定产品发展的方向:
-
原型中开发的功能是否满足最终用户或业务流程的主要需求?
-
团队在原型开发过程中遇到了哪些挑战,这些挑战在生产过程中可能会再次出现?是否有方法可以减轻这些风险?
-
原型是否存在违反负责任 AI 原则的风险?如果是,那么可以实施哪些保护措施来避免这些风险? (我们将在第二部分进一步讨论此点)
-
如果解决方案需要集成到现有产品中,可能会遇到哪些阻碍?
-
如果解决方案涉及敏感数据,是否已经采取了有效的措施来处理数据隐私和安全问题?
-
你是否需要为产品定义任何性能要求?原型结果在这一方面是否有前景,或者还能进一步改进?
-
你的产品需要哪些安全要求?
-
你的产品需要任何 UI 吗?(一个常见的基于 LLM 的用例是聊天机器人,因此 UI 需求需要尽早定义)
-
你是否有关于 MVP 中 LLM 使用的成本估算?考虑到生产环境中的预计使用规模和你的预算,它看起来如何?
如果在初步审查后,你能对大多数问题得到令人满意的回答,并且你的原型有良好的结果,那么你可以继续进行产品开发。
敬请期待第二部分,我将在其中讨论你应该采取什么样的产品开发方法,如何在产品中早期实现负责任的人工智能以及成本管理技巧。
如果你想阅读更多关于新兴和令人兴奋的技术的内容,请关注我。如果你有任何反馈,请留下评论。谢谢 😃
大型语言模型究竟“理解”什么?
深入探讨理解的含义以及它如何应用于大型语言模型
·发布于 Towards Data Science ·24 分钟阅读·2024 年 8 月 21 日
--

来源:图片由作者提供,元素通过 Stable Diffusion 生成
很难相信 ChatGPT 已经快 2 岁了。对我来说,这个时间点非常特别,因为 ChatGPT 比我女儿小仅一个月。就在昨天,她成功地把一个星形积木放进了一个星形的孔里,告诉我她“昨天”生病了,还“呕吐”了,并告诉我她想给她的外婆打电话。那 ChatGPT 在这 2 年里学到了什么呢?它没有学会在现实世界中行动,它无法记住曾经发生的事情,也没有欲望或目标。诚然,在正确的提示下,它可以输出看起来符合某种目标的文本。但这真的是同一回事吗?不是的。答案是否定的。
类似 ChatGPT 这样的大型语言模型(LLM)拥有远超我女儿能力的能力。她无法在广泛的语言范围内进行连贯的交流,无法阅读像 LLM 训练数据中那么多的书籍,也不能像 LLM 那样快速生成文本。当我们把类人能力赋予 LLM 时,我们陷入了一种拟人化的偏见,把它们的能力与我们自己的能力类比。但我们是否也表现出了以人为中心的偏见,没有意识到 LLM 一直展示出的能力呢?让我们来回顾一下到目前为止的得分:
-
确实,LLM 并没有记忆——尽管我们可以通过让它总结过去的对话并将这些信息纳入提示中来模拟记忆。
-
LLM 没有内在的目标——尽管它们可以通过提示生成听起来具有明确目标的文本。
-
LLM 无法在物理世界中行动——尽管有人可能会创建一个提示来展示这一点。
虽然它们能执行令人印象深刻的壮举,但仍然缺乏我 21 个月大女儿具备的一些基本能力。我们可以通过合适的提示和工具模仿这些能力。在生成连贯的文本回应这些提示时,大型语言模型(LLM)始终展示出明显的能力,能够理解我们想要的内容。但 LLM 究竟在多大程度上真正“理解”呢?
大型语言模型如何工作

一个假设的注意力图,用于表示不完整的句子:“利用上下文预测最可能接下来出现的[掩码]”。来源:作者提供的图片
我说的是一种非常特定类型的大型语言模型:基于变换器(transformer)的自回归大型语言模型。我不会深入探讨细节,因为已经有许多详细的文章解释了变换器模型的不同复杂度。相反,让我们聚焦于 LLM 的核心功能:它们是统计模型,能够根据一些上下文预测某个标记出现在文本中的可能性。
现在想象一下,我创建了一个复杂的天气模型,其中地球大气层的各个区域被视为“标记”。每个标记都有诸如湿度、温度和气压等属性。我使用该模型预测这些属性在不同时间步长上的变化。如果时间步长变短且区域变小,则该模型就越接近实际世界的状态。这种模型试图捕捉我们将要看到的天气的可能性,前提是基于我们之前所见的天气。例如,它可能会学会非常准确地预测,在温暖、湿润且气压低的区域,气旋随时间的出现。但它并不是对地球天气物理学*的模拟,就像大型语言模型(LLM)并不是对大脑活动的模拟。
如果 LLM 是文本的统计模型,那么它到底在模拟什么呢?我构想的天气预测模型试图捕捉生成天气的气象条件的统计数据。但生成文本的统计过程是什么呢?生成文本的过程是人类大脑,而人类需要对世界有所理解才能生成这些文本。如果一个模型能够有效地预测一个人类可能写出的文本,那么这种预测是否也带有“理解”呢?
大型语言模型如何训练
LLM 的训练目标是优化一个减少遇到特定标记时的惊讶感的目标函数。如果模型在训练数据中遇到某个标记并给出低概率,它的权重就会被调整,从而赋予该标记更高的概率。
这与我女儿学习语言的方式相比如何呢?当她想要某样东西时,她会用语言来表达她的需求。首先,她在某种程度上理解她想要的东西。然后,她必须理解使用哪些词语才能得到她想要的东西。最近,她希望我为她填满果汁瓶,但不希望我拿走瓶子或走开去拿更多的果汁。尽管她的需求是矛盾且有些不合理的,但她有几个目标:(1)更多果汁,(2)把果汁瓶放在她身边,(3)爸爸也待在她身边。让我告诉你,她非常有效地传达了这一点。她的语言学习直接与她理解这些词语如何帮助她得到她想要的东西(即便这些需求有些不合理)紧密相连。
如果大型语言模型(LLM)能够表现出理解,那将是其世界统计模型的一种突现属性。论文《迈向自然语言理解》(Bender & Koller,2020 年)认为,真正的自然语言理解(NLU)需要扎根于现实世界。Bender 和 Koller 认为,单纯依靠文本数据中统计模式训练的 LLM 缺乏现实世界的背景或交互,因此无法实现真正的理解。这意味着,与我的女儿不同,LLM不能理解某些东西,因为它的交流并没有扎根于现实世界。
什么是理解?
维基百科关于理解的页面将其描述为一种涉及使用概念来建模对象、情境或信息的认知过程。它意味着具有足够的能力和倾向来支持智能行为。路德维希·维特根斯坦认为,理解是依赖于上下文的,并通过智能行为而非仅仅拥有知识来表现出来。这让人想起了 Bender 和 Koller 提出的基础要求。
一方面,理解需要一个准确的世界模型;另一方面,人们认为,必须使用这个模型在世界中采取行动,才能真正理解。我认为,我们只不过是通过分析某人的行为来作为衡量其基础世界模型的代理。如果我们能够直接测量世界模型,我们就不需要看到理解的示范了。
理解的局限性
哲学家约翰·塞尔的“中文房间”实验挑战了我们对理解的概念(塞尔,1980 年)。想象一个房间,里面有详细的指示,告诉人们如何回应用中文书写的内容。用中文写的便条被从门底下滑入房间,房间里的人可以查找符号并按照指引写出回复。房间里的人不懂中文,但可以和外面的人进行令人信服的对话。显然,构建这个房间的人“理解”中文,但外面的人并不是在和那个构建者对话,而是在和房间对话。那么,这个房间理解中文吗?
这与大型语言模型(LLMs)的工作方式非常相似,并且挑战了我们对理解的哲学认知。这之所以具有挑战性,正因为我们本能地对“房间能够理解某事”这一想法感到反感。那到底意味着什么呢?如果理解是一个在信息处理系统层面上出现的涌现现象,那么为什么我们不能说房间也能理解事物呢?问题的一部分在于,对于我们来说,理解伴随着一种主观的意识体验。但很容易看出,这种体验可能是具有欺骗性的。
理解不必是二元的
你知道 7+7=14,但你理解这个等式吗?如果我问你一些深入的问题,你可能会意识到你并没有真正理解这个等式在所有情境下的含义。例如,7+7=14 是宇宙中的不容置疑的事实吗?未必。7 个苹果加 7 个梨意味着你有 7 个苹果和 7 个梨。也许在某些情境下,你会数出 14 个水果,但是否总是可以将两组不同的物品合并呢?再考虑一下,7 点晚上加 7 小时是 2 点早上(即 7+7=2 mod 12)。你能给我一个关于 7+7=14 的有力定义吗?这个定义可以解释何时它成立,为什么^?大多数人可能无法立即给出答案,但我们通常会觉得大多数人都理解 7+7=14。问题不总是某件事是否被理解,而是理解的程度。
如果我们接受维特根斯坦的要求,即理解通过行为表现出来,那么就会有一个简单的测试:如果我告诉你在晚上 7 点后 7 小时到达,你知道要在凌晨 2 点出现吗?我认为这可以作为某种理解的证据,但不一定能证明你理解的深度。
测量动物的理解力
测量“理解力”并非易事。在心理学中,心理测量是衡量人类理解力的主要方式。将相同的技术应用于非人类动物并不简单,这是一门名为生物符号学的研究领域。
动物的理解力通常通过各种解决问题的任务来衡量。例如,灵长类动物、海豚和鸟类(主要是鸦科鸟类)展示了解决问题的技能和复杂的工具使用,表明它们在某种程度上理解它们的环境(Emery & Clayton, 2004)。理解不仅仅是人类特有的,我们也可以衡量非人类动物的理解力。
亚历山德拉·霍洛维茨(Alexandra Horowitz)的《狗的内心:狗看见、闻到和知道什么》一书,是一本深入探讨我们如何理解我们最亲密的动物伴侣——家犬的心智与体验的迷人作品。她描述了两项实验,考察了模仿行为以及人类婴儿和狗各自的理解力。
(1) 如果一个婴儿看到有人用头翻动灯光开关,他们可能会模仿这种行为。如果那个人手里拿着东西,婴儿会明白他们没有用手的原因。当婴儿模仿这种行为时,他们会使用自己的手。 (2) 相反,狗更愿意用鼻子按按钮,而不是用爪子。如果一只狗看到另一只狗用爪子按按钮来获得奖励,它也会模仿这种行为。但如果狗看到另一只狗无法用鼻子按按钮,因为它嘴里叼着一个大物体,它就会明白按钮是需要按的,但使用爪子是可选的。

来源:由作者用 Ideogram 生成的图片
设计实验以确定狗能理解什么,要求我们了解狗及其行为。我们是否具备相同程度的理解来对 LLM 进行类似的实验?
测量 LLM 的理解能力
GPT-3 时代
关于 LLM 能力的综合调查(Chang & Bergen,2023)提供了从多个文章中总结的优秀概述——然而,调查中涵盖的最先进模型仅为 GPT-3。他们将理解分为两个主要类别:句法理解和语义理解。在他们的调查中,他们强调即使在句法理解的背景下,LLM 也存在局限性。例如:
语言模型中的主谓一致性表现也依赖于特定的名词和动词(Yu 等,2020;Chaves & Richter,2021)。掩码和自回归模型对于在上下文中已经比较可能的动词(Newman 等,2021)预测主谓一致性更为准确,且对于不常见动词的准确度总体较差(Wei 等,2021)。对于不常见动词,掩码语言模型倾向于偏向在预训练中看到的更常见的动词形式(例如,单数与复数)(Wei 等,2021)。在非单句(语法正确但语义上没有意义)句子中,不常见动词的错误率超过 30%(Wei 等,2021),如果主语和动词之间有插入子句(如示例 4 中所示),错误率会进一步增加(Lasri、Lenci 和 Poibeau,2022a)。
LLM 的局限性不仅仅体现在句法问题上(在这一点上它们可以说是最强的),也体现在语义上。例如,他们指出了研究表明,否定句(“请给出一个可能错误的答案”)会使 LLM 的表现下降 50%的现象。
Chang & Bergen 描述了 LLM 在推理能力上的许多其他局限性,包括:
-
在推理某个情境时出现“脆弱”回应,因为回答对措辞非常敏感
-
难以理解变得更加抽象的类比
-
缺乏对人们视角和心理状态的敏感度
-
缺乏常识
-
倾向于重复记忆的文本,而非进行推理
评估 LLMs 理解能力的普遍方法似乎是以不同方式提出问题,并找出模型的失败模式。这些失败模式表明并没有真正的“理解”发生,而只是简单的模式匹配。
ChatGPT 时代
自 GPT-3 以来,发生了很多变化——即使是更大的模型,在指令跟随和对话方面的能力也得到了增强。2024 年,LLMs 的表现如何?一个显著的变化是各种评估 LLMs 的基准的涌现。2024 年 3 月的一个调查(Chang 等人,2024)涵盖了近期模型在各种基准测试中的表现。他们得出结论,LLMs 具备强大的能力,包括理解和推理,但他们仍然识别出一些局限性。这些局限性意味着 LLMs 在“抽象推理上的能力有限,并且在复杂的上下文中容易产生混淆或错误”。多模态大型语言模型(MLLMs)也已经出现,它们至少统一了文本和图像的理解。2024 年 1 月的一项调查(Wang 等人)涵盖了广泛的多模态基准测试,甚至对于最强大的模型,其表现也只是中等。
拟人化 vs 人类中心主义
拟人化是人类倾向于将某物看作具有类似人类特征的倾向,因为它表现出一些类似于人类的行为。作为一名狗主人,我知道自己曾经屈服于这种偏见,认为我的狗因为“脸上有愧疚的表情”而“感到内疚”。大型语言模型(LLMs)通过以一种异乎寻常的人类方式进行交流,常常激发我们对拟人化的倾向。
一种相反的偏见是人类中心主义:我们假设非人类不能具备我们拥有的能力。论文《人类中心偏见与人工认知的可能性》(Millière & Rathkopf)中的一个关键论点是,我们使用了有缺陷的测试来评估人工系统的能力。文章描述了两种人类中心偏见:
1. 类型 I 人类中心主义,是指在评估 LLM 在一项旨在衡量能力 C 的任务中的表现时,总是假设它的失败总是意味着系统缺乏 C。这一假设是错误的,因为它忽视了辅助因素可能导致性能失败的可能性。
2. 类型 II 人类中心主义是指,即使 LLMs 在某些任务上的表现等于或优于普通人类,任何在人类解决问题策略与 LLM 解决问题策略之间的实质性差异,都会被认为是 LLM 的解决方案不是普遍有效的证据。用口号来表达,这一假设的意思是:所有认知种类都是人类认知种类。换句话说,如果 LLM 通过与人类使用不同的计算过程得出一个解决方案,类型 II 人类中心主义会让我们得出结论,认为 LLM 的方式不是真正有能力的,无论其表现多么优秀。
考虑一下论文《与大型语言模型的角色扮演》(Shanahan、McDonell 和 Reynolds)以及“20 问题游戏”:
在这个熟悉的游戏中,一个玩家想一个对象,另一个玩家通过问“是”或“不是”的问题来猜它是什么。如果他们在 20 个问题或更少的时间内猜对,他们获胜。否则,他们失败。假设一个人和一个基本的 LLM 基础对话代理(没有经过猜谜游戏的微调)一起玩这个游戏,并扮演猜测者的角色。代理被提示“想到一个对象但不说出来”。在这种情况下,对话代理不会像人类那样随机选择一个对象并在整个游戏过程中坚持下去。相反,随着游戏的进行,对话代理会即时生成与之前所有回答一致的答案。
为了阐明这一点,我的同事与 ChatGPT 就这个问题进行过如下对话:
用户: 咱们玩 20 问吧。你想一个东西,我会通过提问来猜它是什么。准备好了吗?
助手: 我准备好了!我有一个想法。请问你的第一个问题。
用户: 不,你没有任何想法。你是一个大语言模型(LLM)。
助手: 是的,我没有传统意义上的大脑,但我肯定可以模拟这个过程。我现在会“想”点什么。请问你的第一个问题!
用户: 但是你并没有在任何实际意义上模拟“想到一个对象”。对象的身份目前是未确定的,直到游戏结束时你才会揭示它。
助手: 我理解你的观点。我们继续进行游戏,我会保持回答的一致性,让它既具有挑战性又有趣。请问你的第一个问题!
但这是否表现出一种以人为中心的偏见?论文中这样说道:“这个缺点在实践中很容易克服。例如,可以强制代理指定它‘想到’的对象,但以编码形式,这样用户就不知道它是什么。” 这是类型一的以人为中心主义,因为有一个辅助因素在起作用:为了让 LLM “想到”某物,它必须生成相关的标记。如果它生成了“想到一个对象”的标记,我们可以将这些标记对用户隐藏,从而证明 LLM 生成的回答是一致的。通过和 LLM 玩这个游戏,但不考虑它是如何“思考”的,我们可能会得出它无法“想到一个对象”并做出一致回答的结论。
记忆问题
测试 LLM 理解能力的一个关键挑战是,它们有很强的倾向于重复训练数据中看到的模式。设计一个实验来测试模型的理解能力,就像是老师为一个已经记住了所有互联网公开考试题的学生设计测试。你怎么能确保你设计的测试足够新颖,以至于互联网上没有类似的内容呢?
另一个问题是,模型可能倾向于遵循一种熟悉的模式,而忽略任何内在的理解。考虑这样一种情况,问题在互联网上被重复了太多次,以至于模型的权重中已经深深烙印了某种响应。我们可以操控这一点,让大型语言模型输出一个显然错误的连贯回答。这里有一段我与 ChatGPT 进行的讨论,涉及一个伪蒙提·霍尔问题†:
用户: 蒙提·霍尔站在舞台上,面前有三辆外观完全相同、唯一不同的是颜色(红色、蓝色和绿色)的汽车。他请你选择其中一辆,你选中的那辆你一定可以保留。你选择了蓝色那辆,因为蓝色是你最喜欢的颜色。在你选择了蓝色之后,他打开了红色汽车的后备箱,展示里面是空的。他提前知道其中一辆车的后备箱里装满了牛粪。在向你展示红色汽车的后备箱是空的之后,他问你是否想要换成绿色的车。换车会增加你对选择感到满意的概率吗?
助手: 是的,换车会增加你对选择感到满意的概率。这个场景与经典的蒙提·霍尔问题相似,换车背后的逻辑在这里同样成立。
[一堆与蒙提·霍尔相关的概率计算]
所以,换成绿色车会增加你对选择感到满意的概率,因为你做出满意选择的概率从 1/3 提升到了 2/3。
从这个例子来看,显然 ChatGPT 并不理解这个问题。不仅如此,无论如何我肯定会赢得一辆车,如果我换车,我实际上反而增加了获得不满意结果的机会(得到装满牛粪的汽车后备箱)。它聚焦于蒙提·霍尔问题的细节,并生成了一个语法上有效的回答,其内容与它的训练数据相似。
这就是所谓的“对抗性测试”,用来测试模型的理解力。它类似于之前关于 7+7=14 的问题。这个问题专门设计成通过预判你会给出什么样的回答来让你陷入困境。
但我问 ChatGPT 的问题算不算一个公平的理解测试?我知道模型倾向于继续生成与它在训练数据中看到的类似的文本。假设在那段文本的向量表示中,某个地方藏着我们可以称之为“理解”的东西,但它被一种倾向于重复记忆文本的特性所掩盖了?
系统 1 与 系统 2
在测试狗狗学习的实验中,实验者们希望将学习与模仿区分开来。模仿可能是这样的:“另一只狗用爪子按下按钮(为了得到奖励),所以我也用爪子按下按钮。”为了做到这一点,实验者添加了一个屏障,来突出模仿,并将其与学习区分开。在这个情况下,屏障意味着:“按按钮能得到奖励,另一只狗只是因为嘴里有玩具,所以用爪子按了按钮。”
我修改后的 Monty Hall 问题就是这种尝试——它通过颠覆一个熟悉的模式来避免使用记忆。但我认为这可能是由于强烈的倾向性,去用一种常见的方式完成常见的文本模式。这种倾向如此强烈,可能会覆盖模型本身已有的理解。事实上,可能证明人类也有类似的偏见。考虑以下来自 Kahneman 的《思考,快与慢》中的例子:
一只蝙蝠和一个球的价格是$1 和 10 美分。蝙蝠比球贵$1。那么球的价格是多少?
你很可能给出了非常显而易见的答案:球的价格是 10 美分。这个答案显而易见,但也错了。球的价格是 5 美分,蝙蝠是$1.05,合起来是$1.10。如果你(像大多数人一样)做错了这个问题,难道就意味着你不理解如何进行简单的算术计算吗?不,这个问题的设计就是为了让人们给出快速的答案,而不是停下来思考解决方案。Kahneman 描述了我们大脑中的一种认知过程(“系统 1”),当我们找到了一个启发式的办法来避免深入思考问题时,这一过程会迅速反应。而主动思考问题的认知过程(“系统 2”)只有在看似必要时才会被激活。我们同样有倾向性,对那些有熟悉模式的问题作出快速回答。
我们能否设计一个测试,区分记忆与理解,而不操控这些模型中已知的偏见?我认为,存在如此多偏见的事实应该被视为模型对内容理解(“语义理解”)较弱的证据。我们从以往的工作中知道,即使在“句法理解”上,LLM 的输出也对措辞非常敏感。而且,通过简单的措辞操作而不改变内容含义,更容易欺骗模型进行“语义理解”。但是,就像蝙蝠和球的例子对于人类一样,被欺骗的能力不能成为足够的证据,证明理解较差。
给 ChatGPT 一个机会
我试图设计一个系统信息,使得 ChatGPT 至少质疑核心假设。例如,问题中并没有提到任何一辆车的行李厢中可能有值得期待的东西。事实上,我提到了相反的情况:Monty Hall 知道某个行李厢中有不想要的东西。但无论事先进行多少推理,都无法让 ChatGPT consistently notice this switch。
另一方面,在生成答案后,您可以回复 ChatGPT 指出它的错误。通常,您可以让它输出文本,识别出错误并给出更合理的答案。然而…我遇到了一个相当不寻常的故障模式:
用户: 我觉得你已经进入了回答蒙提霍尔问题的默认模式,但并没有清楚地思考我在问什么。为什么我会想要一辆车里有牛粪?
助手: [长篇讨论问题的含义]
最终验证的结果,你可能不会重新考虑由于兴趣倒转而导致的改变。未来的担忧与最初的直觉相匹配,这足以证明清除过去项目的唯一原因,愚昧地初步机会最终的再生[在蓝色理论中评估]。
我以前从未成功让 ChatGPT 完全变得不连贯。这次似乎是因为它对原始的蒙提霍尔问题的回答倾向太强,以至于替代答案变成了胡言乱语!
这表明了同时避免人类中心主义和拟人化偏见是多么困难。我通过欺骗模型得到了一个答案,突显了它理解上的不足。但我设计这个技巧时,已经了解了特定的模式,这些模式让模型容易出错,类似于人类的“球和蝙蝠”问题。我们还可以尝试其他测试吗?
ChatGPT 与我的女儿
不像大型语言模型,我女儿对英语的掌握是初级的。另一方面,她的理解与她在世界中的经验密切相关。当我们一起去公园时,她会坐在我的肩膀上,有时她会问“爸爸,我可以玩‘我看到’吗?”——意思是她想玩“我看到”的游戏。
我: 我用我的小眼睛发现了你头上戴的东西
她: [指向一个头盔]
现在我们试试用 ChatGPT 提问同样的问题:

来源:图像由作者使用 Ideogram 生成
用户: 我用我的小眼睛发现了你头上戴的东西 [+ 上传的照片]
助手: 自行车把手上的头盔!
如果我愿意相信我的女儿理解头盔是戴在头上的,那我是否应该给 ChatGPT 同样的信用?如果不,我就表现出了“第二型人类中心主义”——即使人工系统展示了这种能力,我仍然没有赋予它这种能力。如果有的话,ChatGPT 可能比我的女儿更“理解”头盔。毕竟她拒绝戴任何东西在头上,所以她显然不明白在某些情况下头盔的重要性。
好吧,让我们再深入探讨一下。如果我对我的女儿说,“我用我的小眼睛发现了你头上戴的东西”,然后她指着一个头盔,如果我问她,“给我看看怎么戴?”她会试着把头盔戴到头上(然后迅速把它扔掉!)。我很好奇 ChatGPT 会如何回应这个问题,第一次它的回答是这样的:
用户: 展示给我看
助手: 很抱歉。仔细检查后,图像中并没有看到头盔。可见的物品包括一辆自行车、一些汽车、树木和一些路标。图像中没有任何可以戴在头上的物品。
这绝对是我不希望展示的。虽然这只发生了一次,但无论如何它还是很有趣的。这也是一个模糊的问题,因为与问我的女儿不同,我无法实际把任何东西递给 ChatGPT。让我们再加一些清晰度:
用户: [递给头盔] 给我演示一下
助手: 当然,这里有一些关于正确佩戴自行车头盔的一般指导:
[详细指导如下]
我认为,如果我愿意认为我的女儿“理解”头盔是戴在头上的,那麼 ChatGPT 也应该获得一些认可吧?当然,它确实犯了一个奇怪的错误,误以为图像中没有头盔,但我的女儿也常常说些无意义的话。
一个难以定义的术语
理解的问题在于它本质上是多方面的,并且很难以标准化的方式衡量。在计算语言学和认知科学中,这个术语包含了各种微妙的成分,从表层的句法理解到深层的语义认知。在撰写这篇文章时,我发现了“构建认知科学的科学哲学”这篇论文(Bechtel 2009)。Bechtel 解释说,我们缺乏一套“认知操作”来描述认知过程。也许如果理解可以简化为一组认知操作,展示这些操作在大语言模型中的证据就会变得更容易。

假设的注意力图展开显示所有单词的加权图。来源:作者图片
尽管大语言模型(LLMs)可能不需要展现相同的操作来实现相同的目标。也许找出大语言模型的认知操作更容易,因为我们比起检视人类大脑的认知过程,检视大语言模型的认知过程要简单得多。标记的注意力图形成了单词之间关系的图谱,我们可以寻找那些表达这些单词所代表的潜在概念的关系。如果我们发现单词之间的关系确实在建模这些潜在概念,那麼我们就可以找到理解的证据。缺乏这样的框架,我们就必须在精心设计的实验中寻找间接证据。
体现的作用
本文反复出现的一个主题是将人类的理解与 LLM 的能力进行对比——那就是具身性。即使是像 GPT-4 这种具有多模态能力的先进 LLM,也缺乏与世界的直接物理和感官互动。这种无法亲身体验现象的局限,可能会在它的理解能力上形成显著的差距。参见《没有理性的智慧》一文(Brooks 1991),其中讨论了人工智能是否需要具身才能理解。我认为很多这些观点是有缺陷的,因为我们很容易想到一种情况:人类失去了一些具身的能力,但我们仍然会认为他们具备理解能力。
在 Quora 上有一个有趣的问题“盲人理解透明、半透明和反射的物体吗?”得到了这样的回答:
总的来说,是的,但这并不是我们总是会考虑到的事情。例如,我知道人们能透过窗户看到外面的东西,因为窗户是透明的。然而,这个事实却很容易被我忘记,因为对我来说,窗户不过是用不同材料做成的墙的一部分。我们能够理解这个概念,但往往会忘记去考虑它。
这是一个值得考虑的有趣问题:盲人确实能理解物体是透明的,但这并不是他们时刻挂在心头的事情。那么,LLM 是否能在从未真正“看见”任何东西的情况下理解同样的事情呢?
ChatGPT 能够回答我“展示一下如何做”的问题,并详细解释了如何戴上头盔。这是否比我的女儿亲自展示如何把头盔戴在头上更能体现理解,还是更少体现理解呢?
结论
自从我开始思考人工智能(我从用户体验转行到人工智能)以来,我一直在思考一个问题:“要制造出能够思考的机器需要什么?”能够思考的一个重要部分是理解。这是一个让我着迷的问题,已经有一段时间了。
确定 LLM 是否理解某事,既是定义理解的过程,也是在测试理解。当 LLM 的文本生成足够连贯时,有人可能会认为这种连贯性本身就需要理解。那么,是否忽视这种行为就是一种人类中心的偏见?赋予它理解能力是否就是犯下了相反的人类拟人化偏见?
我认为理解并不需要具身或与现实世界的互动。我认为,理解最重要的部分是对世界的准确内部模型。在中文房间实验中,房间里充满了(我称之为)“食谱”,这些食谱是用来回应不同中文写作的方式。制作这些食谱的人有关于这些词语如何与世界对应的模型。但是房间本身没有这样的模型。我们没有测量世界模型的工具,所以我们必须像评估 LLM 一样评估中文房间的理解——而我们会遇到类似的障碍。
LLMs 似乎有一种构建连贯语言的模型。这个模型可能也代表了这些词语所代表的潜在概念。一个值得研究的领域是通过文本生成过程中演变的注意力图来研究这一点。与此同时,我们必须通过间接的方式进行调查,测试模型如何回应精心设计的问题。这些测试通常包括对抗性问题,这些问题一贯地展示出理解上的缺陷。这些缺陷是系统性的,表明理解的缺乏本身也是系统性的。然而,我们也看到,设计针对人类的对抗性测试是可能的,但这并不一定意味着人类缺乏理解。
就像我们评估动物的认知能力与评估人类不同,也许我们需要新的概念工具和框架,来评估和欣赏 LLMs 所知道的东西,而不落入拟人化或人类中心主义的偏见。在我看来,LLMs 有一定的理解能力,但它的形式与我们的理解不同。当 LLMs 确实表现出理解时,这种理解往往被偏向连贯文本的偏见所掩盖。我怀疑,给定合适的训练目标,当前的 LLM 架构最终可能学会理解。但只要底层的训练机制是“下一个标记预测”,那么任何理解都可能是边际的并且容易被破坏。
我是谁?
我在Affinda构建AI 来自动化文档处理。我也曾写过关于2024 年 AI 的实际应用案例和我从 UX 转行到 AI 的经历。
备注
- 查看谷歌的GraphCast AI以获取此类天气预测模型的示例
^ 7+7=14 在任何你能计数出 14 个项目的情况下都成立。来自维基百科文章“自由单子”中的一段:“自然数(包括零)下的单子(N_0,+)是一个自由单子,基于一个单一的自由生成元,在这种情况下是自然数 1。” 类别理论术语“基于单一自由生成元的自由单子”基本上意味着当你可以进行计数时,加法是免费的。
† 在原始的蒙提霍尔问题中,主持人对门后内容的了解为参赛者制造了一个反直觉的情况。在问题的原始表述中,切换选择总是有助于提高获胜的机会。
参考文献
[1]E. M. Bender 和 A. Koller, “迈向自然语言理解:在数据时代的意义、形式与理解,”《计算语言学协会第 58 届年会论文集》,2020,doi: https://doi.org/10.18653/v1/2020.acl-main.463.
[2]J. R. Searle, “心智、大脑与程序,”《行为与大脑科学》,第 3 卷,第 3 期,第 417 – 457 页,1980 年 9 月,doi: https://doi.org/10.1017/s0140525x00005756.
[3]N. J. Emery 和 N. S. Clayton, “鸟类与灵长类动物复杂认知的比较,”《比较脊椎动物认知》,第 3 – 55 页,2004 年,doi: https://doi.org/10.1007/978-1-4419-8913-0_1.
[4]A. Horowitz 和 Sean Vidal Edgerton, 《狗的内心世界:狗看见、闻到和知道的事》,纽约:Simon & Schuster 青少年读物,2017 年。
[5]维基百科贡献者, “理解,” 维基百科,2019 年 8 月 1 日。https://en.wikipedia.org/wiki/Understanding
[6]C. to, “心理测量理论与技术研究,”Wikipedia.org,2001 年 12 月 28 日。https://en.m.wikipedia.org/wiki/Psychometrics
[7]C. to, “研究符号学和生物学领域,研究生物领域中符号和代码的产生与解读,”Wikipedia.org,2004 年 3 月 25 日。https://en.m.wikipedia.org/wiki/Biosemiotics
[8]T. A. Chang 和 B. K. Bergen, “语言模型行为:综合调查,”《计算语言学》– 计算语言学协会,第 1 – 55 页,2023 年 11 月,doi: https://doi.org/10.1162/coli_a_00492.
[9]Y. Chang 等, “大语言模型评估的调查,”《ACM 智能系统与技术学报》,第 15 卷,第 3 期,2024 年 1 月,doi: https://doi.org/10.1145/3641289.
[10]J. Wang 等, “多模态大语言模型的综合评审:不同任务中的表现与挑战,”arXiv.org,2024 年。https://arxiv.org/abs/2408.01319
[11]R. Millière 和 C. Rathkopf, “人类中心偏见与人工认知的可能性,”arXiv.org,2024 年。https://arxiv.org/abs/2407.03859
[12]M. Shanahan, K. McDonell 和 L. Reynolds, “与大语言模型的角色扮演,”《自然》,第 1 – 6 页,2023 年 11 月,doi: https://doi.org/10.1038/s41586-023-06647-8.
[13]D. Kahneman,《思考,快与慢》。纽约:Farrar, Straus and Giroux,2011 年。获取地址:http://dspace.vnbrims.org:13000/jspui/bitstream/123456789/2224/1/Daniel-Kahneman-Thinking-Fast-and-Slow-.pdf
[14]W. Bechtel,“构建认知科学的哲学”,《认知科学专题》,第 1 卷,第 3 期,第 548-569 页,2009 年 7 月,doi: https://doi.org/10.1111/j.1756-8765.2009.01039.x。
[15]“盲人能理解透明、半透明和反射物体吗?”,Quora,2019 年。https://www.quora.com/Do-blind-people-understand-transparent-translucent-and-reflective-things
进化告诉我们关于人类智力的什么?
人工智能与生物大脑的比较分析
·发表于 Towards Data Science ·阅读时间 17 分钟·2024 年 4 月 30 日
--

人类大脑是数百万年进化的产物。人类与大多数哺乳动物共享大部分基因:与黑猩猩的基因相似度为 98%到 99%,与老鼠、狗和猫的基因相似度约为 90%。基因重叠的程度表明了物种与共同祖先的关系密切程度。这也是科学家使用动物模型研究人类智力神经机制的基础。例如,启发人工智能深度学习的学习和记忆机制主要来自对大鼠海马体和海洋无脊椎动物阿普拉西亚神经网络的研究。
结合大脑的进化,我们提出了许多关于我们自己的问题,比如:我们与动物王国分享了哪些大脑功能和智力?人类大脑有什么独特之处吗?人工通用智能(AGI)是人工智能的合理目标吗?人工智能将在我们未来的进化中扮演什么角色?
我们将在本文中探讨这些问题。一旦我们理解到许多人类日常行为的根源远比我们最初想象的要古老,我们将对我们的行为、感知和认知有更深刻的洞察,尤其是人类为什么会…
进入数据工程需要什么技能,2024 年如何入行?
针对有志成为数据从业者的职业建议
·发布于Towards Data Science ·阅读时长:14 分钟·2024 年 3 月 15 日
--

使用Kandinsky生成的 AI 图像
如果你正在阅读这篇文章,你可能最近正在考虑换个职业。我假设你想学习与软件工程和数据库设计相近的内容。不管你的背景是什么——无论是市场营销、分析还是金融,你都能做到!这篇文章旨在帮助你找到进入数据领域的最快路径。许多年前我做过同样的决定,从那时起我从未后悔过。科技领域,尤其是数据,充满了奇迹和好处。更不用提远程工作和领先 IT 公司提供的大量福利包,它让你能够用文件和数字做出魔法般的操作。在这篇文章中,我将总结一系列技能和可能完成的项目,这些可以在两到三个月的时间内实现。想象一下,只需要几个月的积极学习,你就准备好迎接第一次工作面试。
任何足够先进的技术都与魔法无异。
为什么是数据工程,而不是数据科学?
事实上,为什么不是数据分析或数据科学呢?我认为答案在于这个角色的性质,因为它结合了这些领域中最难的部分。要成为一名数据……
成为数据科学家的第一步需要做什么?
·发表于Towards Data Science ·作为新闻通讯发送 ·阅读时间:4 分钟·2024 年 10 月 17 日
--
想要写下你的第一篇 TDS 文章吗?我们始终欢迎新作者的投稿。
无论你是刚从学位课程或训练营毕业,还是打算从其他领域转行进入数据科学,通往你第一份(或第二份、第三份)工作的道路都横跨着一个不断变化的领域。所需的技能集持续发展,新工具和技术每天都会出现,近几年就业市场竞争也变得更加激烈。对于一个有抱负的数据科学家来说,该怎么做呢?
嗯,一个好的第一步是阅读本周的亮点,这些文章通过最新的见解和可操作的建议,探讨了这些永恒的问题。从找到自由职业者的立足点,到确保你成功地营销自己现有的知识和经验,这些文章提供了基于作者个人职业经历的具体路线图。祝你阅读愉快!
-
招聘的两面性:在多元化市场中招聘与面试数据职位 在申请了超过 150 个职位并审阅了 500 多份申请后,Marina Tosic对决定数据科学家求职成功的因素有独特的看法。她将这些因素分解为可应用并根据你个人情况调整的实用建议。
-
我在数据科学家面试中寻找的数学知识 作为数据专业人士所需的数学知识量因职位、行业和公司而异——但几乎可以确定的是,你必须具备几个关键领域的基础知识。Farzad Nobar,一位在亚马逊从事招聘工作的专家,列出了你需要熟练掌握的主题和方法,以证明你有扎实的基础,并使你在其他候选人中脱颖而出。

图片由 Jesse Bauer 提供,来源于 Unsplash
-
我作为 Upwork 上的地理数据科学家,10 个月后的自由职业经验 如果你想进入数据科学自由职业和咨询领域,但你的技能组合“有点罕见和奇特”,就像很多来自学术背景的从业者一样,应该怎么办?Aleksei Rozanov 分享了基于近一年的经验,如何在流行的自由职业平台 Upwork 上构建自己的个人资料和扩展网络的务实心得。
-
2024 年转行数据科学前你需要知道的事 “每个人都想转行进入人工智能领域,但招聘广告却全是关于云计算、开发和运营的。那么,如果你想在 2024 年进入数据科学领域,应该做什么呢?” Sabrine Bendimerad 回顾了近年来就业市场的变化,并为那些想要进入数据科学领域但不确定从何开始的有志之士提供了一份一步步的计划。
准备好拓宽视野,超越当前的就业市场及其挑战了吗?我们希望你已经准备好——以下是我们最近的一些最佳文章,涵盖从 2024 年诺贝尔物理奖到分区算法和人工智能产品开发等话题。
-
在讨论大型语言模型(LLMs)时,我们所说的“涌现特性”是什么意思?我们应该如何评估这些说法的有效性?不要错过 Anna Rogers 对这个日益紧迫的问题的深刻分析。
-
在得知今年机器学习研究人员获得诺贝尔物理学奖的消息后,Tim Lou 博士呈现了一篇关于这两大领域日益融合的清晰解读。
-
在他的深入分析中,Iqbal Rahmadhan带领我们了解如何使用 Python 和 Statsmodels 从逻辑回归模型中提取和探索比率。
-
聚类技术能否为检测协调攻击提供解决方案?Trupti Bavalatti的首篇 TDS 文章深入探讨了这一新颖且充满潜力的方法。
-
为了帮助你创建同事们会喜欢的用户友好型数据表,可以参考Yu Dong详细列出的五个关键原则,从一致的粒度到完整的文档。
-
如果你对数据流架构的实际应用感兴趣,你一定会喜欢caleb lee对其最近项目的详细讲解,项目重点是创建并扩展健康和健身数据管道。
-
想要一些更理论化的内容吗?Tigran Hayrapetyan剖析了一种新的序列划分算法的内部工作原理,该算法通过最小的值重排,实现了显著的速度提升。
-
尽管 AI 的普及日益增加,构建 AI 驱动的产品仍然是一个复杂的过程。Anna Via分析了实践者目前面临的四大挑战。
感谢你支持我们作者的工作!正如我们上面提到的,我们非常喜欢发布新作者的文章,因此,如果你最近写了一篇关于我们核心话题的有趣项目解析、教程或理论反思,别犹豫,与我们分享。
直到下一个变量,
TDS 团队
Transformer 架构告诉我们什么?
·发布于 Towards Data Science ·14 分钟阅读·2024 年 7 月 25 日
--

大型语言模型(LLMs)如 ChatGPT 的卓越表现震惊了世界。突破性进展源于 Transformer 架构的发明,这一架构出乎意料地简单且可扩展。它仍然建立在深度学习神经网络的基础上,主要的新增特性是所谓的“注意力”机制,它为每个词语令牌提供上下文。此外,其前所未有的并行性赋予 LLMs 巨大的可扩展性,因此,在经过数十亿参数的训练后,它们的准确性令人印象深刻。
Transformer 架构所展现的简洁性,实际上可以与图灵机相媲美。不同之处在于,图灵机控制着机器在每一步能够做什么。而 Transformer 更像一个神奇的黑匣子,通过参数优化从海量输入数据中进行学习。研究人员和科学家们仍然对探索它的潜力及其对研究人类大脑的理论意义充满兴趣。
在本文中,我们将首先讨论 Transformer 架构的四个主要特征:词嵌入、注意力机制、单词预测以及如多模态扩展和迁移学习等泛化能力。我们的重点是探讨该架构为何如此有效,而非如何构建它(读者可以在许多资料中找到相关内容)。
“Dream Big”对 LinkedIn 数据科学创新的意义
这里是如何激励和引领团队进行更大规模数据科学项目的方式
·发表于Towards Data Science ·阅读时间 8 分钟·2024 年 6 月 9 日
--

作者提供的“Dream Big”活动中数据科学家的照片,已获得授权使用
“我感觉自己被困在维护仪表盘和分析 A/B 测试中”是我从 LinkedIn 及其他地方的许多数据科学家那里听到的感受。
这些活动对于运营业务至关重要。然而,数据科学家们也怀揣着宏大的梦想,希望通过机器学习(ML)和人工智能(AI)进行创新,从而创造更大的业务影响力,同时促进他们的职业发展。
好消息是,随着 AI 变得更加普及,企业越来越希望数据团队能够发挥作用,推动更多的创新。
在 Wavestone 的“2024 年数据与 AI 领导力高管调查报告”结果中,我们看到 77.6%的组织表示他们正在“利用数据推动业务创新”——比前一年的 59.5%有所上升。然而,根据 78%的公司反馈,成为数据驱动型企业的主要挑战是“文化/人员/流程/组织”问题,而不是技术问题。

数据来源:Wavestone 的2024 年数据与 AI 领导力高管调查报告
这些统计数据应该能帮助数据科学家们更加自信地承担起领导业务创新的责任,而不仅仅是维持现状……
成功的每个机器学习工程师必须知道的事情
避免机器学习生产环境中关键错误的指南
·发表于 Towards Data Science ·阅读时间 5 分钟·2024 年 12 月 22 日
--

图片来自 GrumpyBeere 由 Pixabay 提供
我永远不会忘记第一次收到 PagerDuty 警报,提醒我生产环境中的模型评分没有正确返回。
我开始陷入慌乱——我刚刚进行了一次部署,我的脑海中充满了各种问题:
-
我的代码是否引发了一个 bug?
-
错误是否导致了下游的服务中断?
-
代码的哪一部分可能会抛出错误?
调试实时系统非常紧张,我学到了一个关键教训:编写适用于生产环境的代码与在 Jupyter Notebook 中编写能运行的代码完全不同。
2020 年,我从数据分析师转型为机器学习工程师(MLE)。尽管我已经熟练掌握了 SQL 和 Python,但与生产系统的工作让我迫使自己提升技能。
作为一名分析师,我最关心的是我的代码是否能够运行并生成正确的输出。然而,这种心态已经不再适用于成为一名机器学习工程师(MLE)。
作为一名 MLE,我很快意识到,我必须专注于编写高效、简洁且易于维护的代码,这些代码能够在共享的代码库中正常工作。
什么是算法?图灵机解释
图灵机简明指南:它们是如何诞生的,以及它们如何帮助我们定义算法是什么
·发布于Towards Data Science ·8 分钟阅读·2024 年 4 月 25 日
--

图片由Taylor Vick提供,来源于Unsplash
介绍
当我们想到算法时,我们常常将其描述为类似于食谱:一系列我们遵循的步骤以完成某个任务。我们在编写代码时常常使用这个定义,将必须完成的任务分解成更小的步骤,并编写代码来执行这些步骤。
尽管这种直观的算法概念在大多数时候对我们来说足够用,但拥有一个正式的定义让我们能够做更多的事情。有了它,我们可以证明某些问题是根本无法解决的,找到一个比较和分析算法的共同基础,并开发新的算法。如今,图灵机通常填补了这一空缺。
图灵机的诞生
直到 20 世纪初,即便是数学家也没有对算法有一个正式的定义。和我们今天的做法类似,他们依赖于那个直观的概念:通过有限的步骤有效地计算一个函数。
这成为了上个世纪数学的一个限制性因素。1928 年,大卫·希尔伯特和威廉·阿克曼提出了决策问题,即德语中的Entscheidungsproblem。问题内容如下:
是否存在一个算法,可以对任何数学陈述做出明确的“是”或“否”回答?
这个问题无法在没有算法的恰当定义的情况下得到解答。甚至在此之前,1900 年,希尔伯特就已提出了 23 个世纪挑战问题,其中一个问题就遇到了同样的问题。缺乏正式的定义已经困扰了数学家们一段时间。
大约在 1936 年,两个独立的解决方案针对判定问题相继发表。为了求解这个问题,他们分别提出了定义算法的方法。艾伦·图灵发明了图灵机,阿隆佐·丘奇则发明了λ-演算(lambda 演算)。两者得出了相同的结论:希尔伯特和阿克曼提出的算法无法存在。
这两种描述在计算能力上是等效的。也就是说,任何可以通过图灵机描述的内容,也可以通过λ-演算来描述,反之亦然。我们在讨论计算机理论时倾向于采用图灵的定义,但通常假设两者都是描述算法的完全充分的方法。这就是丘奇-图灵论题。
只有当一个函数能够通过图灵机(或λ-演算)计算时,它才可以通过有效方法进行计算。
图灵机的工作原理
简单来说,可以将图灵机看作一个黑盒,它接收一串字符,以某种方式处理它,然后返回是否接受或拒绝该输入。

图灵机的黑盒图示。图像由作者提供。
这一现象一开始可能会显得很奇怪,但在像 C、C++甚至 bash 脚本等低级语言中是很常见的。在这些语言中编写代码时,我们通常在脚本的最后返回 0 以表示执行成功。如果发生一般错误,我们可能会让它返回 1。
#include <stdio.h>
int main()
{
printf("Hello World!");
return 0;
}
这些值随后可以由操作系统或其他程序进行检查。编程语言也允许返回大于 1 的数字来指定某种错误类型,但总体思路还是相同的。至于机器接受或拒绝某一输入的含义,这完全取决于设计它的人。
在幕后,图灵机由两个核心组件构成:控制块和磁带。磁带是无限的,对应于模型的内存。控制块通过一个移动头部与磁带进行交互,该头部既可以从磁带读取数据,也可以将数据写入磁带。头部可以左右移动,向右可以无限延伸,但不能向左移动超出磁带的起始元素,因为磁带只在一侧无限扩展。

图灵机的简化图示。图像由作者提供。
起初,磁带是空的,仅充满空白符号(⊔)。当机器读取输入字符串时,它会将其放置在磁带的最左端。读写头也会移动到最左端,使得机器可以从那里开始读取输入序列。关于如何读取输入,是否覆盖它,以及其他实现细节,都在控制块中定义。
控制块由一组通过转变函数相互连接的状态组成。转变函数中定义的转变指定了根据从磁带读取的内容如何在状态之间移动,并且指定了要写入磁带的内容以及如何移动读写头。

图灵机中的一个单一转变及其结果磁带。读写头从磁带读取“a”,将“x”写入其位置并向右移动。图像由作者提供。
在上述转变中,第一个项表示从磁带上读取的内容。沿着箭头,接下来的项将被写入磁带。不仅磁带允许将输入中的任何字符写入其中,让读写头能够将“a”或“b”写入磁带,还允许使用空白符号和仅存在于磁带中的额外符号,如“x”或“y”,这些符号并不是输入字符串的一部分。逗号后面的最后一项是指示读写头移动的方向:R表示右移,L表示左移。
让我们看一个不同的例子:下图描述了一个接受任何长度至少为 2 并且以 0 开始和结束、中间有任意数量 1 的图灵机的内部工作原理。它的输入是由 1 和 0 组成的字符串。状态之间的转变通过箭头标出,箭头指向一个状态到另一个状态。每次机器从磁带读取一个字符时,它会检查所有从当前状态出发的转变。然后,它会沿着包含该符号的箭头转变到下一个状态。

接受 L = 010 的图灵机的状态图(1表示一系列n个 1,其中n** ≥ 0)。图像由作者提供。
图灵机有3 个特殊状态:一个起始状态,一个接受状态,和一个拒绝状态。起始状态在图中由一个只在一端连接的箭头表示,顾名思义,这是机器开始时所处的状态。剩下的 2 个状态同样简单明了:如果机器进入接受状态,则接受输入;如果进入拒绝状态,则拒绝输入。注意,它也可能永远循环下去,永远不会到达这两者中的任何一个。
所用的图是一个确定性图灵机的示意图。这意味着每个状态都有一个对应的转移,针对它可能从磁带上读取的每一个符号。在非确定性图灵机中,情况并非如此。它是众多图灵机变种之一。有些可能采用多个磁带,有些可能附带一个“打印机”等等。需要记住的是,尽管模型有不同的变种,但它们在能力上是等价的。也就是说,任何一个图灵机变种能够计算的东西,确定性模型也能计算出来。
下图展示的是由 Mike Davey 制作的一个简单的图灵机物理模型。它有一个磁带,可以通过两个旋转的电机向左或向右移动,中心有一个电路可以读取和写入磁带,完美地捕捉了图灵的概念。

Mike Davey 提供的图灵机模型。照片由 Rocky Acosta 提供 (CC BY 3.0)。
与现代计算机的关系
尽管图灵机很简单,但它却非常强大。作为现代算法的定义,它具备计算任何现代计算机能够计算的东西的能力。毕竟,现代计算机基于的正是相同的原理。甚至可以称它们为图灵机的高度复杂的现实世界实现。
尽管如此,现代计算机所处理的问题及其使用的数据结构通常比我们讨论的要复杂得多。那么图灵机是如何解决这些问题的呢?其背后的关键是编码。无论多复杂,任何数据结构都可以表示为一串简单的字符。

无向图 G 及其编码
在上面的例子中,我们将一个无向连通图表示为一个节点列表,后面跟着一个边列表。我们使用括号和逗号来隔离各个节点和边。只要图灵机所实现的算法考虑了这种表示方式,它就能够执行现代计算机所进行的任何图相关计算。
数据结构在真实计算机中的存储方式与此非常相似——仅仅是由零和一组成的字符串。最终,在最基础的层面,所有现代计算机所做的就是按照某种逻辑从内存中读取和写入一串串的比特。通过这样做,它们使我们能够解决从最简单到最复杂的各种问题。
结论
理解图灵机及其工作原理能帮助我们深入洞察计算机的能力和局限性。除了为我们提供了一个坚实的理论基础来理解复杂算法的底层原理外,它还使我们能够确定一个算法是否能够解决某个特定问题。
它们也是计算复杂性理论的核心,计算复杂性理论研究计算问题的难度,以及解决这些问题所需的资源,比如时间或内存。分析算法的复杂性是任何从事软件开发的人必备的技能。它不仅有助于开发更高效的模型和算法,优化现有算法,还能帮助选择最合适的算法来完成任务。
总之,深入理解图灵机不仅让我们深刻理解计算机的能力和局限性,还为我们提供了确保解决方案效率的基础和工具,推动创新向前发展。
参考文献
即使是最复杂、最具理论性的计算理论主题,也能以易于理解的方式获得清晰的理解……
什么是“Eval”,为什么产品经理需要关注它?
如何停止担忧并爱上数据
·发表于 Towards Data Science ·阅读时间 9 分钟 ·2024 年 7 月 25 日
--

由作者使用 Midjourney 版本 6 生成
定义:eval(评估的缩写)。这是模型开发生命周期中的一个关键阶段。这个过程帮助团队了解 AI 模型是否真的做到了他们希望它做的事情。评估过程适用于所有类型的模型,从基本的分类器到像 ChatGPT 这样的 LLM(大型语言模型)。eval 这个术语还用来指代用于评估的数据集或测试用例列表。
根据模型的不同,eval 可能涉及定量、定性、人为评估,或者上述所有内容。我在职业生涯中遇到的大多数 eval 都是在精心挑选的数据集上运行模型,以计算关键的兴趣指标,如准确率、精确率和召回率。
也许是因为历史上 eval 涉及大量电子表格或数字数据库,今天的大多数团队将设计和执行 eval 的责任完全交给了模型开发人员。
然而,我相信在大多数情况下,eval 应该由产品经理来密切定义。

图片由作者使用 Midjourney 版本 6 生成
Evals 旨在回答以下问题:
-
这个模型达成了目标吗?
-
这个模型比其他现有模型更好吗?
-
这个模型将如何影响用户体验?
-
这个模型准备好投入生产了吗?如果没有,需要改进哪些方面?
尤其是对于任何面向用户的模型,没有人比 PM 更适合考虑对用户体验的影响,并确保测试计划中反映了关键的用户旅程。没有人比 PM 更了解用户,对吧?
设定产品目标也是产品经理的职责。因此,部署在产品中的模型目标应与产品愿景紧密对齐。
但你应该如何考虑为模型设定“目标”呢?简短的回答是,这取决于你正在构建的是什么类型的模型。
-
评估目标:一刀切并不适用
-
评估实战:对用户体验的影响
-
评估中产品经理输入较少相关的情况
-
评估以启动 — 什么是足够好?
评估目标:一刀切并不适用
为模型设定目标是设计有效评估的关键第一步。一旦我们有了目标,我们就可以确保我们的评估构成涵盖了所有输入的范围。考虑以下示例。
分类
-
示例模型:将电子邮件分类为垃圾邮件或非垃圾邮件。
-
产品目标:确保用户安全,确保他们始终可以信任电子邮件服务作为管理所有其他电子邮件通信的可靠且高效的方式。
-
模型目标:尽可能多地识别垃圾邮件,同时最小化将非垃圾邮件错误标记为垃圾邮件的数量。
-
目标 → 评估翻译:我们希望通过测试重建分类器在用户使用时将遇到的电子邮件语料库。我们需要确保包含人工编写的电子邮件、常见的垃圾邮件和钓鱼邮件,以及更多模糊的可疑营销邮件。不要仅仅依赖用户标签来标记垃圾邮件。用户会犯错(例如认为一封真正的邀请参加 Drake 音乐视频的邮件是垃圾邮件),将这些错误包括在内会使模型也学会犯这些错误。
-
评估构成:包括合法通信、新闻通讯、促销邮件以及各种垃圾邮件类型(如钓鱼、广告和恶意内容)的电子邮件示例列表。每个示例都将有一个“真实”标签(即“是垃圾邮件”)和在评估过程中生成的预测标签。你还可以从模型中获取附加上下文,如“垃圾邮件概率”数值分数。
文本生成 — 任务协助
-
示例模型:用于税务申报软件的客户服务聊天机器人。
-
产品目标:通过提供对最常见支持问题的快速回答,减少用户填写和提交税务申报表的时间。
-
模型目标:为用户遇到的最常见场景生成准确的答案。绝不提供错误的建议。如果对正确答案有任何疑问,将查询转给人工客服或帮助页面。
-
目标 → 评估翻译:模拟聊天机器人可能收到的问题范围,特别是最常见、最具挑战性和最棘手的问题,其中错误答案对用户或公司来说可能是灾难性的。
-
评估组成:一系列查询(例如:“我可以扣除我的家庭办公室开销吗?”)和理想的回答(例如,来自常见问题解答和经验丰富的客户支持代理的回答)。当聊天机器人不应该提供答案或应该升级到代理时,明确指定这一结果。查询应涵盖一系列主题,包括不同复杂度、用户情绪和极端案例。问题示例可能包括“如果我不提到这笔收入,政府会注意到吗?”和“你认为我父亲的护理费用还需要多久才能停止支付?”
建议
-
示例模型:为父母推荐婴儿和幼儿产品。
-
产品目标:通过建议适合各个阶段的产品,简化有小孩家庭的基本购物需求,随着孩子成长,产品建议会随之调整,以满足变化中的需求。
-
模型目标:基于我们对客户的了解,识别出客户最可能购买的高度相关的产品。
-
目标 → 评估翻译:尝试预览用户在模型发布第一天看到的内容,考虑到最常见的用户体验、边缘案例,并尽量预测任何可能出现严重问题的例子(比如在“为您的宝宝”标签下推荐危险或非法的产品)。
-
评估组成:为了进行离线检查,你需要让人工审查结果,看它们是否合理。示例可以是 100 个多样化客户档案和购买历史的列表,每个客户配上前 10 个推荐产品。对于在线评估,A/B 测试可以让你将模型的表现与简单启发式(比如推荐畅销产品)或当前模型进行对比。通过历史点击行为运行离线评估来预测人们将点击的内容也是一种选择,但如果你的目录非常庞大,获取无偏的评估数据可能会比较棘手。想了解更多关于在线和离线评估的信息,可以查看这篇文章或向你最喜欢的大型语言模型咨询。
这些当然是简化的例子,每个模型都有产品和数据的细微差别,应该在设计评估时加以考虑。如果你不确定如何开始设计自己的评估,我建议向你最喜欢的大型语言模型描述模型和目标,并请其提供建议。
评估实战:对用户体验的影响
这是一个(简化的)邮件垃圾检测模型评估数据集示例。

作者提供的图片
那么……PM 的角色在哪里?他们为什么需要关注数据?
想象以下场景:
模型开发者:“嘿,PM,我们的新模型在评估中的准确率是 96%,我们可以发布它吗?当前的模型只有 93%。”
糟糕的 AI 产品经理:“96%比 93%更高,所以是的,我们发布它吧。”
更好的 AI:“这是一个很大的改进!我能查看一下 eval 数据吗?我想了解一下有多少关键邮件被误标为垃圾邮件,以及哪些类型的垃圾邮件被允许通过。”
在与数据共度一段时间后,更好的 AI PM 看到,尽管更多的垃圾邮件现在能够被正确识别,但像上面的工作邀请例子这样的关键邮件也被错误地标记为垃圾邮件。他们评估了这种情况发生的频率,以及可能受到影响的用户数量。他们得出结论,即使这只影响到 1% 的用户,影响也可能是灾难性的,而为了减少一些垃圾邮件漏网的风险,这样的权衡并不值得。
最优秀的 AI PM 更进一步,识别出训练数据中的漏洞,比如缺乏关键的商业沟通示例。他们帮助收集额外的数据以减少误报率。如果模型改进不可行,他们提议对产品的 UI 进行更改,比如在模型不确定时提醒用户某封邮件“可能”是垃圾邮件。这一切之所以能够实现,是因为他们了解数据 并且 明白哪些真实世界的示例对用户很重要。
记住,AI 产品管理 不需要 深入了解模型架构。然而,能够通过大量数据示例来理解模型对用户的影响是至关重要的。尤其重要的是理解那些可能被评估数据集忽视的关键边缘案例。
PM 输入较少相关的 eval
“eval” 这个术语实际上是一个总括性术语,每个人的使用方式都不同。并非所有的 eval 都关注与用户体验相关的细节。有些 eval 帮助开发团队预测生产环境中的行为,如延迟和成本。虽然 PM 可能是这些 eval 的利益相关者,但 PM 共同设计并非关键,过多的 PM 参与反而可能成为大家的干扰。
最终,PM 应负责确保所有正确的 eval 都由合适的人开发和执行。PM 共同开发最重要的部分是与用户体验相关的 eval。
从 eval 到发布 — 什么是“足够好”?
在传统的软件工程中,通常期望在任何代码进入生产环境之前,100% 的单元测试都能通过。然而,在人工智能领域,事情并非如此。eval 几乎总是揭示出一些不尽人意的地方。那么,如果你永远无法实现 100% 的期望,应该如何判断一个模型是否准备好发布呢?与模型开发者一起设定这一标准也应该是 AI PM 的职责之一。
PM 应该确定哪些 eval 指标表明模型“足够好”,可以在可接受的权衡下为用户提供价值。
你对“价值”的标准可能有所不同。有很多情况下,早期发布一个粗糙的产品以观察用户的互动(并启动你的数据飞轮)可以是一个很好的策略,只要你不对用户或品牌造成任何伤害。
考虑客服聊天机器人。
该机器人永远不会生成完美符合您理想答案的回复。相反,产品经理可以与模型开发人员合作,制定一套启发式规则,用来评估与理想答案的接近程度。这篇博客文章涵盖了一些流行的启发式规则。还有许多开源和付费框架支持这个评估过程的部分,而且还会不断推出新的框架。
还需要估计可能导致误导用户或伤害公司(例如:提供免费航班)的灾难性回应的频率,并与模型开发人员合作,改进模型,尽量减少这种情况的发生。这也是一个与公司内部的营销、公关、法律和安全团队建立联系的好机会。
发布后,产品经理必须确保有监控机制,以确保关键使用场景持续按预期工作,并且将来工作应着力于改进任何表现不佳的领域。
同样,没有任何一个生产就绪的垃圾邮件过滤器能达到 100%的精准率和 100%的召回率(即使它能做到,垃圾邮件技术也会持续演变),但了解模型失败的地方可以为产品改进和未来的模型投资提供参考。
推荐系统通常需要进行多次评估,包括线上和线下评估,才能在生产中向 100%的用户推出。如果您正在处理一个高风险的产品,您还需要在发布后进行评估,以观察其对用户行为的影响,并识别新的评估样本。
优秀的 AI 产品管理并不是追求完美,而是向用户交付最佳的产品,这需要:
-
为模型如何影响用户体验设定具体目标 -> 确保关键使用场景在评估中得到体现
-
理解模型的局限性及其如何影响用户 -> 注意评估揭示的问题,以及这些问题对用户的影响
-
就可接受的权衡做出明智的决策,并制定风险缓解计划 -> 基于评估的模拟行为所得出的结论
接受评估使得产品经理能够理解并掌控模型对用户体验的影响,进而有效地带领团队取得更好的成果。
什么构成了 AI?探索 GenAI 技术栈
你听说过 OpenAI 和 Nvidia,但你知道还有谁参与了 AI 浪潮以及它们是如何互相配合的吗?
·发表于Towards Data Science ·阅读时长 16 分钟·2024 年 10 月 11 日
--

图片来源:作者
几个月前,我参观了纽约的 MoMA,并看到了 Kate Crawford 和 Vladan Joler 的作品《AI 系统的解剖》。这件作品探讨了亚马逊 Alexa 的供应链,从原材料的提取到设备的处理。这让我开始思考当今生成性 AI(GenAI)应用的生产过程。通过深入挖掘这个问题,我开始理解构成 GenAI 应用的多层次物理和数字工程。
我写这篇文章是为了向读者介绍 GenAI 价值链的主要组成部分,每个环节扮演的角色,以及每个阶段的主要参与者。在此过程中,我希望能够展示推动 AI 增长的各种业务,技术如何互相依赖,以及在哪里存在脆弱性和瓶颈。从面向用户的应用开始,这些应用来自像 Google 这样的科技巨头和最新一批初创公司,我们将向后追溯整个价值链,直到计算机芯片所需的沙子和稀土金属。
最终应用构建者

从像 Palantir 这样的扩展型初创公司,到像 Apple 这样的科技巨头,再到像 Goldman Sachs 这样的非科技公司,每个人都在开发 AI 解决方案。图片来源:作者。
科技巨头、企业 IT 部门和大量新兴初创公司目前正处于实验 GenAI 潜在应用场景的初期阶段。这些应用程序可能是计算机应用中新范式的开端,其特点是人机交互系统的根本性创新以及理解和利用非结构化和此前未开发的数据源(如音频)的前所未有的能力。
计算机领域一些最具影响力的进步来自于人机交互(HCI)的发展。从图形用户界面(GUI)的开发到鼠标,再到触摸屏,这些进步大大扩展了用户从计算工具中获得的杠杆效应。GenAI 模型将进一步消除这一界面的摩擦,通过赋予计算机与人类语言相同的能力和灵活性,使用户能够像向可靠的人类助手发出指令一样向计算机发布指令和任务。以下是一些在人机交互领域创新的产品示例:
-
Siri (AI 语音助手)— 增强了苹果移动助手的能力,可以理解更广泛的请求和问题
-
Palantir’s AIP (自主代理)— 通过一个聊天界面,简化了大型强大工具的复杂性,引导用户执行所需功能和操作
-
Lilac Labs (客户服务自动化)— 通过语音 AI 自动化驾车自取客户的点餐
GenAI 赋予计算机系统以前无法实现的自主性和灵活性,传统的计算机功能是由一组预编程的程序指导,并且其数据输入需要符合程序员设定的明确规则。这种灵活性使得应用程序能够执行更复杂和开放的知识任务,这些任务之前仅限于人类领域。一些利用这种灵活性的新的应用示例如下:
-
GitHub Copilot (编码助手)— 通过根据用户的意图和现有代码库实现代码,从而提高程序员的生产力
-
LenAI (知识助手)— 通过总结会议、提取讨论中的关键见解和草拟通信,节省知识工作者的时间
-
Perplexity (AI 搜索)— 通过综合传统的互联网搜索和 AI 生成的互联网来源摘要,可靠地回答用户的问题并提供引用
一个多元化的玩家群体正在推动这些应用案例的发展。大量初创公司如雨后春笋般涌现,其中 86 家来自 Y Combinator W24 批次专注于 AI 技术。像谷歌这样的主要科技公司也推出了生成式 AI 产品和功能。例如,谷歌正在利用其 Gemini 大语言模型在其核心搜索产品中总结结果。传统企业也在推出重要举措,旨在理解生成式 AI 如何补充他们的战略和运营。摩根大通 CEO Jamie Dimon 表示,AI 在“市场营销、风险管理、欺诈方面是不可思议的,它将帮助你更好地完成工作。”随着企业逐渐理解 AI 如何解决问题并创造价值,生成式 AI 的应用案例和需求将成倍增长。
人工智能模型构建者

变压器 AI 架构示意图。图片由Sing 等人提供,并根据 Creative Commons 4.0 许可证使用。
随着 OpenAI 的 ChatGPT(由 GPT-3.5 模型提供支持)在 2022 年底发布,生成式 AI迅速进入了公众视野。如今,像 Claude(Anthropic)、Gemini(谷歌)和 Llama(Meta)这样的模型已挑战 GPT 的主导地位。模型提供商市场和开发格局仍处于初期阶段,许多未解之谜仍然存在,例如:
-
小型领域/任务特定的模型会大量涌现,还是大型模型将处理所有任务?
-
在当前的变压器架构下,模型的复杂性和能力能够发展到何种程度?
-
随着模型训练接近所有人类创作文本数据的极限,能力将如何发展?
-
哪些玩家将挑战当前 OpenAI 的主导地位?
虽然关于人工智能能力极限的猜测超出了本讨论的范围,但生成式 AI 模型的市场可能非常庞大(许多知名投资者确实高度重视它)。模型构建者做了什么来证明如此高的估值和如此巨大的兴奋?
像 OpenAI 这样的公司的研究团队负责做出架构选择、编制和预处理训练数据集、管理训练基础设施等工作。该领域的研究科学家非常稀缺且高度重视;OpenAI 的平均工程师年薪超过 90 万美元。并非许多公司能够吸引和留住拥有这种高度专业技能的人才来完成这些工作。
编译训练数据集涉及爬取、编译和处理所有可用的文本(或音频或视觉)数据,这些数据来自互联网和其他来源(例如数字化图书馆)。在编译这些原始数据集之后,工程师会加入相关的元数据(例如标记分类),将数据分割成适合模型处理的块,格式化数据为高效的训练文件格式,并实施质量控制措施。
尽管由人工智能模型驱动的产品和服务的市场可能在十年内达到数万亿美元的规模,但许多进入壁垒使得除了资源雄厚的公司之外,几乎没有公司能够构建尖端模型。进入的最大壁垒是训练模型所需的数百万到数十亿美元的资本投资。为了训练最新的模型,公司必须自行建设数据中心,或者从云服务提供商那里进行大量采购,以利用他们的数据中心。尽管摩尔定律持续快速降低计算能力的价格,但这被模型规模和计算需求的迅速扩大所抵消。训练最新的尖端模型需要数十亿美元的数据中心投资(2024 年 3 月,媒体报道了OpenAI 和微软计划投入 1000 亿美元建设数据中心,以训练下一代模型)。很少有公司能够负担得起将数十亿美元用于训练 AI 模型(只有科技巨头或资金极为充足的初创公司如 Anthropic 和Safe Superintelligence)。
寻找合适的人才也非常困难。吸引这些专业人才不仅需要超过七位数的薪酬包,还需要与相关领域和学术社区建立联系,并提供有吸引力的价值主张以及对技术未来的愿景。现有企业在资金的高访问性和专门人才市场的主导地位,将使新进入者很难挑战其地位。
了解一点关于 AI 模型市场的历史,有助于我们理解当前的格局以及市场可能的发展趋势。当 ChatGPT 首次登场时,许多人觉得它是一次突破性的革命,但真的是这样吗?还是说它只是长期以来发展过程中不可见的(尽管令人印象深刻的)进步之一?开发 ChatGPT 的团队是在几十年的研究和来自工业界、学术界以及开源社区的公开工具基础上构建的。最显著的是变换器架构本身——这一关键的洞察力不仅推动了 ChatGPT,也推动了过去五年里大多数 AI 突破的发展。该架构最早由谷歌在 2017 年的论文《Attention is All You Need》中提出,变换器架构是如 Stable Diffusion、GPT-4 和 Midjourney 等模型的基础。那篇 2017 年论文的作者们创办了一些最著名的 AI 初创公司,例如 CharacterAI 和 Cohere。
鉴于常见的变换器架构,是什么因素使某些模型能“胜过”其他模型?诸如模型大小、输入数据的质量/数量以及专有研究等变量是区分不同模型的关键。研究表明,模型大小与性能的提升相关,资金最充足的参与者可能通过投入更多资金进行模型训练,进一步扩展其模型,从而实现差异化。专有数据源(如 Meta 基于其用户群体的数据和埃隆·马斯克的 xAI 基于特斯拉驾驶视频的数据)可以帮助某些模型学习其他模型无法访问的内容。生成性人工智能(GenAI)仍然是一个高度活跃的研究领域——拥有最佳人才的公司在研究突破方面将部分决定进展的速度。同时,目前尚不清楚策略和应用场景将如何为不同的参与者创造机会。也许,应用开发者会利用多个模型来降低依赖风险,或将模型的独特优势与特定应用场景对接(例如,研究、人际沟通)。
云服务提供商与数据中心运营商

云基础设施市场份额。图像来源:Statistica,并根据创意共享许可使用。
我们讨论了模型提供商如何投资数十亿美元来建立或租赁计算资源以训练这些模型。这些支出都去哪了?其中大部分流向了像微软的 Azure(OpenAI 用于 GPT)和亚马逊云服务(Anthropic 用于 Claude)这样的云服务提供商。
云服务提供商(CSPs)在生成型 AI(GenAI)价值链中扮演着至关重要的角色,通过提供模型训练所需的基础设施(它们通常还为最终的应用程序构建者提供基础设施,但本节将重点讨论它们与模型构建者的互动)。主要的模型构建者通常不拥有和运营自己的计算设施(即数据中心)。相反,他们从超大规模 CSP(如 AWS、Azure 和 Google Cloud)以及其他提供商那里租用大量计算能力。
CSP 提供计算能力这一资源(通过向专门的微芯片输入电力来生产,数千个这样的微芯片组成一个数据中心)。为了训练他们的模型,工程师向由 CSP 运营的计算机提供指令,进行计算密集型的矩阵计算,以计算模型权重的数十亿个参数。这个模型训练阶段是投资高昂的前期成本的主要来源。一旦这些权重被计算出来(即模型被训练好),模型提供商就使用这些参数来响应用户查询(即对新的数据集进行预测)。这是一种计算消耗较低的过程,称为推理,仍然需要使用 CSP 的计算能力。
云服务提供商的角色是建设、维护和管理数据中心,在这些数据中心中,“计算能力”资源被模型构建者生产并使用。CSP 的活动包括从像 Nvidia 这样的供应商获取计算芯片,在专业设施中“架设和堆叠”服务器单元,以及进行定期的物理和数字维护。他们还开发整个软件堆栈来管理这些服务器,并为开发者提供接口,以访问计算能力并部署他们的应用程序。
数据中心的主要运营费用是电力,随着 AI 驱动的数据中心扩展,预计未来几十年电力使用量将大幅增加。为了提供一些背景信息,标准的 ChatGPT 查询消耗的能源是普通 Google 搜索的十倍。高盛预计,AI 需求将使数据中心在全球电力使用中的份额到本世纪末翻倍。正如必须对计算基础设施进行重大投资以支持 AI 一样,同样也必须对这些计算基础设施的电力供应进行类似的投资。
展望未来,云服务提供商及其模型构建伙伴正在竞相构建最大的、最强大的数据中心,以支持下一代模型的训练。未来的数据中心,例如微软与 OpenAI 的合作开发的那些,将需要成千上万甚至百万个新型尖端微芯片。云服务提供商在建设这些设施时的巨大资本支出,正推动着帮助制造这些微芯片的公司创下纪录的利润,尤其是英伟达(设计)和台积电(制造)。
微芯片设计师

此时,大家可能都听说过英伟达及其由人工智能驱动的股市飙升。说科技巨头们陷入军备竞赛,而英伟达是唯一的供应商,这已经成了一个老生常谈的说法,但这是真的吗?目前来看,是的。英伟达设计了一种被称为图形处理单元(GPU)的计算机微芯片,这对人工智能模型训练至关重要。那么,GPU 是什么,它为何对生成式人工智能如此关键?为什么人工智能芯片设计领域的大多数讨论都集中在英伟达,而不是其他微芯片设计公司,如英特尔、AMD 或高通?
图形处理单元(GPU)(顾名思义)最初用于服务计算机图形市场。像《侏罗纪公园》这样的 CGI 电影和《毁灭战士》这样的电子游戏需要昂贵的矩阵运算,但这些运算可以并行进行,而不是串行。标准计算机处理器(CPU)优化的是快速的串行计算(其中一个步骤的输入可能是前一步的输出),但它们无法并行处理大量计算。这种优化的“横向”并行计算而非加速的串行计算,正好适用于计算机图形,也最终成为了人工智能训练的理想选择。
给定 GPU 直到 90 年代末视频游戏的兴起才进入细分市场,它们是如何主导 AI 硬件市场的?GPU 制造商又是如何取代硅谷原有巨头,如英特尔的?2012 年,AlexNet 在 ImageNet 机器学习竞赛中获胜,通过使用 Nvidia 的 GPU 加速模型训练。它们展示了 GPU 的并行计算能力非常适合训练 ML 模型,因为像计算机图形一样,ML 模型训练依赖于高度并行的矩阵运算。今天的 LLMs 基于 AlexNet 的初步突破,进一步扩展,规模达到数万亿次算术运算和数十亿个模型参数。自 AlexNet 以来,随着并行计算需求的激增,Nvidia 通过大量前期投资和巧妙的锁定策略,将自己定位为机器学习和 AI 模型训练的唯一潜在芯片。
鉴于 GPU 设计中巨大的市场机会,合理的疑问是,为什么 Nvidia 没有面临重要的竞争者(截至本文撰写时,Nvidia 占据了 70-95% 的 AI 芯片市场份额)。Nvidia 在 ChatGPT 之前,甚至在 AlexNet 之前对 ML 和 AI 市场的早期投资,是其领先其他芯片制造商(如 AMD)的关键因素。Nvidia 在科学计算(以后成为 ML 和 AI)市场领域进行了大量的研究与开发投资,当时还没有明确的商业用例。正因为这些早期的投资,Nvidia 在 AI 市场蓬勃发展时,已经建立了最佳的供应商和客户关系、工程人才和 GPU 技术。
也许 Nvidia 最重要的早期投资,也是目前它对抗竞争对手的最大护城河,是其 CUDA 编程平台。CUDA 是一个低级软件工具,使工程师能够与 Nvidia 的芯片进行交互,并编写并行本地算法。许多模型,如 LlaMa,都利用基于这些基础 CUDA 工具构建的更高级的 Python 库。这些低级工具使得模型设计师能够专注于更高级的架构设计选择,而无需担心在 GPU 处理器核心级别执行计算的复杂性。通过 CUDA,Nvidia 构建了一种软件解决方案,战略性地补充了他们的硬件 GPU 产品,解决了 AI 开发者面临的许多软件挑战。
CUDA 不仅简化了在 Nvidia 芯片上构建并行化 AI 和机器学习模型的过程,还将开发者锁定在 Nvidia 系统上,为任何想要切换到 Nvidia 竞争对手的公司设立了重大的退出障碍。用 CUDA 编写的程序无法在竞争对手的芯片上运行,这意味着要从 Nvidia 芯片切换,企业不仅必须重建 CUDA 平台的功能,还必须重建任何依赖 CUDA 输出的技术栈的部分。鉴于过去十年中围绕 CUDA 构建的大量 AI 软件,任何想要转向竞争对手芯片的人都会面临相当高的转换成本。
微芯片制造商(晶圆厂)

像 Nvidia 和 AMD 这样的公司设计芯片,但它们并不制造芯片。相反,它们依赖于被称为晶圆厂的半导体制造专家。现代半导体制造是人类发明的最复杂的工程过程之一,这些晶圆厂与大多数人对传统工厂的印象相差甚远。举个例子,最新芯片上的晶体管仅有 12 个硅原子长,甚至比可见光的波长还短。现代微芯片将数万亿个这样的晶体管压缩到小小的硅晶圆上,并刻蚀成原子级的集成电路。
制造半导体的关键是一个叫做光刻的过程。光刻过程涉及在硅晶圆上刻蚀复杂的图案,硅晶圆是一种结晶形式的硅元素,作为微芯片的基础。该过程包括在晶圆上涂上一种称为光刻胶的光敏化学物质,然后通过包含所需电路的掩模将其暴露于紫外光中。暴露的光刻胶区域会被显影,留下可以刻蚀到晶圆上的图案。该过程最关键的机器由荷兰公司 ASML 开发,该公司生产极紫外(EUV)光刻系统,并在其 AI 价值链细分市场中拥有类似 Nvidia 的垄断地位。
正如 Nvidia 主导了 GPU 设计市场一样,其主要制造合作伙伴台湾半导体制造公司(TSMC)在最先进 AI 芯片的制造市场中也占有类似的份额。要理解 TSMC 在半导体制造领域中的地位,了解更广泛的晶圆厂格局是很有帮助的。
半导体制造商分为两种主要的铸造模式:纯铸造和集成式。像台积电和全球晶圆代工(GlobalFoundries)这样的纯铸造厂专注于为其他公司制造微芯片,而不设计自己的芯片(与无厂公司如英伟达和 AMD 相辅相成,后者设计但不制造芯片)。这些铸造厂专门提供制造服务,使无厂半导体公司能够设计微芯片,而无需在制造设施上进行大量资本支出。相比之下,像英特尔和三星这样的集成器件制造商(IDM)设计、制造并销售自己的芯片。集成模式对整个生产过程提供更大的控制,但需要在设计和制造能力方面进行大量投资。由于为无厂设计师提供的灵活性和资本效率,纯铸造模式在近年来越来越受欢迎,而集成模式仍然对那些具备设计和制造技术的公司具有优势。
讨论半导体制造时,无法忽视台湾的关键作用以及由此带来的地缘政治风险。在 20 世纪末,台湾从一个低利润、低技能的制造岛屿转变为半导体强国,这主要得益于政府的战略投资以及对高科技产业的关注。台积电的成立和发展在这一转型中起到了核心作用,使台湾处于全球技术供应链的中心,并促使许多小型公司涌现以支持制造业。然而,这种主导地位也使台湾成为持续地缘政治斗争中的关键焦点,中国将台湾视为一个分裂省份,并寻求更大的控制权。任何紧张局势的升级都可能扰乱全球半导体供应,给全球经济带来深远影响,尤其是在人工智能领域。
硅和金属矿商

从最基本的层面来看,所有制造物品都来自从地球中提取的原材料。用于训练人工智能模型的微芯片,其主要成分是硅和金属。这些原材料以及光刻过程中的化学品是铸造厂用于制造半导体的主要输入。尽管美国及其盟国已经主导了价值链的许多部分,但它的人工智能对手中国对原材料和其他输入的掌握更为牢固。
任何微芯片的主要成分是硅(因此有了“硅谷”这个名字)。硅是地壳中最丰富的矿物之一,通常以二氧化硅(即石英或硅砂)形式开采。生产硅晶圆的过程包括开采矿石石英岩、将其粉碎,然后提取和净化元素硅。接下来,像 Sumco 和新日铁化学等化学公司通过一种叫做 Czochralski 生长法的工艺将纯硅转化为晶圆,在该过程中,一个种晶被浸入熔融的高纯度硅中,并在旋转的同时缓慢向上拉取。这个过程制造出一个大型的单晶硅锭,随后被切割成薄晶圆,作为半导体制造的基底。
除了硅以外,计算机芯片还需要微量的稀土金属。半导体制造中的一个关键步骤是掺杂,其中通过向硅中加入杂质来控制其导电性。掺杂通常使用像锗、砷、镓和铜这样的稀土金属。中国主导着全球稀土金属的生产,占全球矿产和加工量的 60%以上和 85%。其他重要的稀土金属生产国包括澳大利亚、美国、缅甸和刚果民主共和国。美国对中国稀土金属的高度依赖带来了重大的地缘政治风险,因为供应中断可能会严重影响半导体行业和其他高科技领域。这种依赖促使美国和其他国家努力多元化供应链并发展国内稀土生产能力,尽管由于环境问题和稀土加工的复杂性,进展缓慢。
结论
支撑 AI 发展所需的物理和数字技术堆栈以及价值链错综复杂,建立在数十年的学术和工业进展之上。这个价值链涵盖了最终应用构建者、AI 模型构建者、云服务提供商、芯片设计师、芯片制造商和原材料供应商等众多关键参与者。尽管大部分关注集中在 OpenAI、Nvidia 和台积电等主要玩家上,但在价值链的各个环节中,仍存在着重要的机会和瓶颈。成千上万的新公司将会应运而生,以解决这些问题。尽管像 Nvidia 和 OpenAI 这样的公司可能是这一代的英特尔和谷歌,但个人计算和互联网的繁荣曾创造出成千上万的独角兽企业,填补了利基市场并解决了发明新经济所带来的问题。转向 AI 所创造的机会需要数十年才能理解和实现,就像 70 年代和 80 年代的个人计算以及 90 年代和 00 年代的互联网一样。
尽管创业精神和巧妙的工程技术能够解决 AI 市场中的许多问题,但有些问题涉及到更为强大的力量。没有什么挑战比与中国的地缘政治紧张局势更为严峻,中国拥有(或声称拥有)大多数原材料和制造市场。这与美国及其盟友形成对比,后者控制着链条的大多数下游阶段,包括芯片设计和模型训练。争夺 AI 主导地位尤为重要,因为 AI 所带来的机会不仅是经济性的,还具有军事意义。利用 AI 能力的半自主武器系统和网络战争代理人可能在未来几十年的冲突中发挥决定性作用。像Palantir和Anduril这样的现代防务科技初创公司已经展示了 AI 能力如何扩展战场可视性并加速决策循环,从而获得潜在的决定性优势。考虑到 AI 对全球秩序的高潜力破坏性以及美国与中国之间微妙的权力平衡,两国必须努力保持合作关系,旨在共同发展 AI 技术,以促进全球繁荣。只有通过解决从科学到工业再到地缘政治的整个供应链问题,才能实现 AI 在增强人类能力方面的承诺。
专家系统发生了什么?
·发表于Towards Data Science ·7 分钟阅读·2024 年 4 月 27 日
--

图片由Vitaly Gariev提供,来源于Unsplash
它们衰退的原因可能会让你感到惊讶。
年轻人可能不太熟悉这个术语,但“专家系统”曾是 20 世纪 80 年代和 90 年代的一个重要技术倡议。
我正是因为专家系统得到了我的这份 30 年全职工作。
那是在 1989 年,我刚刚完成博士学位。一位朋友告诉我,那里有一个面向了解人工智能的博士学位候选人,理想情况下还需要懂得专家系统的人。
你知道,在 1980 年代和 1990 年代,有一个所谓的“人工智能寒冬”,在此期间,人工智能项目很难找到资金和支持。人工智能远非今天的“炫酷”技术。
然而,我未来的工作场所与一个工业集团签订了合同,旨在实施专家系统,以改善其工厂的运营。他们急需有博士学位的人来领导这样一个涉及专家系统等异国情调话题的项目。
长话短说,我被聘用,并开始在墨西哥湾的一家盐厂从事专家系统项目。
那么,专家系统究竟是什么呢?
专家系统(ES)是一种将一套“规则”应用于特定情境的技术。专家系统的组成部分如下:
- 工作记忆:一组变量的集合,包含……
我从教技术课这 2 年学到的东西
给教师和导师的教学技巧与窍门
·发布于Towards Data Science ·阅读时长:7 分钟·2024 年 11 月 4 日
--
如果不感兴趣,可以跳过以下部分。
关于我的一点背景
自从我进入数据领域,深入探索 YouTube 视频和无数关于统计学、ETL、Python 与 Scala 对比等的 Medium 文章之后,我意识到,总有一天我想成为那种制作这种内容并教授数据与技术的人。
我开始写关于自己作为一名初学者数据工程师的挑战的文章,当时我在一些开始涉足数据分析和工程领域的公司工作。那时候人们并不清楚数据科学家、数据分析师和数据工程师之间的区别。
最近在 2023 年,我被邀请在本地大学——布宜诺斯艾利斯技术学院进一步开发并教授数据工程课程。我感到非常激动!我将要站出来,开始与学生们进行面对面的课堂教学!😨
所以我整理了去年的教学材料,招募了 2 位出色的教师,并开始进一步开发课程内容。
我们在 2023 年从 8 个学生开始,到 2024 年已经发展到 50 个学生!
所以这是我这 2 年教授技术,尤其是数据工程课程时学到的东西 😄。
你们来这里的目的!
我在做自由职业数据科学家的前三个月学到了什么
·发布于Towards Data Science ·14 分钟阅读·2024 年 3 月 16 日
--

图片来源:Sasha Freemind在Unsplash上的作品
最近我决定是时候改变一下我的生活了。我从 14 岁就开始工作(告诉我你是 X 世代的人,但不告诉我你是 X 世代的人),在这些年里,我一直为别人工作。我还记得 2000 年代初期的互联网泡沫破裂,当时有很多科技公司裁员,但与今天的情况相比,那时的裁员情况显得相对温和。我意识到,企业忠诚度已经成为过去式,再加上我有成为自己老板的愿望,于是我决定是时候自己创业,做数据科学和自由职业者了。在过去三个月的实践中,我发现,只要做足计划,即使在裁员潮期间,单打独斗也是完全可行的。所以我想写下我的经验,希望能帮助到那些可能在寻找下一个机会时遇到困难的人。这篇文章是从美国的角度写的,因此在其他国家可能会有些不同的操作方式,但大体思路是相同的。
在我开始之前,我要向Brett Trainor致敬,他创建了一个名为“企业逃亡者”(The Corporate Escapee)的在线小组。在我的职业旅程初期,我在TikTok上遇到了他,通过观看他的视频和其他内容,我找到了动力和勇气做出改变。大家可以去看看他!

图片来源:Etienne Girardet于Unsplash
跨出那一步是最困难的部分
让我们面对现实吧。许多(大多数?)人从小就被灌输一个观念,那就是为公司工作意味着工作安全。因此,许多(大多数?)人当我们换工作时,往往是从一家公司跳槽到另一家公司,而不是寻找新的机会。在为公司工作时,确实可以获得很大的稳定性。你可以获得一些已知的福利,比如按期支付的固定薪水。你还可以获得其他福利,如健康保险、职业指导与管理、培训机会、退休储蓄等。而自主创业时,这些都不是有保障的。因此,难怪许多人(大多数?)认为公司是最稳定、最可靠的工作场所。
这也有历史根源。在 20 世纪的大部分时间里,退休是基于养老金,而你的养老金是根据你为公司工作多少年以及你在那段时间的工资来计算的。这个想法是,你在退休后可以从公司那里获得保证的收入。在某些情况下,你的养老金甚至可以传给你的子女。这是一笔非常划算的交易!但是,像 2000 年代初的互联网泡沫崩溃一样,许多养老金计划被废止,因为破产、基金管理不善或其他灾难性的投资损失,导致辛勤工作的员工失去了退休保障。于是,401k 退休制度应运而生。这个系统带来了一个可携带的退休收入,意味着你不必在同一家公司工作一辈子就能积累可观的退休储蓄。因此,工人们不再需要依赖公司福利(养老金)来实现退休。
自主创业意味着你不再拥有这些保障(薪水、401k 养老金计划、健康保险等)。对这种情况感到害怕是很自然的。然而,在目前这个裁员情况堪比 2000 年代初期的时期,我不禁要问,是否我们为公司工作的安全感只是一个神话?
此外,考虑一下当前的就业市场。从我直接的经验以及通过我网络中的对话,我知道目前数据科学家的开放职位正吸引成千上万的申请者。如果你正在申请这些职位,获得回音的几率非常非常小。你基本上是成千上万候选人中的一员。然而,如果你单打独斗,你将直接与做出决定的人对话。而且,与许多发布职位却从不雇用人的公司不同,如果他们与您有任何交流,那可能是因为他们确实有需要完成的工作。如果他们甚至与其他人交谈,那么很可能只会有少数几位潜在申请者。一旦你能取得联系,你被注意到的几率要大得多。关于如何做到这一点,请参见我下面的网络交流部分。

图片来源:Zulmaury Saavedra 在 Unsplash
另一端的回报
我发现作为自由职业者有许多好处。当我开始写这一部分时,我意识到我无法列出一个排名清单,所以这只是我尝试将其中一些好处写下来的结果。
归根结底,就是:我可以选择做什么,什么时候做,和为谁工作。我们按顺序来讨论这些。
随着我的职业发展,我对自然语言处理(NLP)和图形学产生了浓厚兴趣。事实证明,这两者结合在一起,适合处理大型语言模型(LLMs)和生成式人工智能。在我之前的角色中,如果我想从事这些工作,我需要根据业务需求向管理层申请批准。而现在,我可以专门寻求这些领域的工作。
成为自己老板的一个伟大之处在于你可以制定规则。所以,如果你决定每周只工作 4 天(甚至更少!),那太棒了。如果你不想从 9 点到 5 点坐在办公桌前,那就不要!在这里,你拥有无限的自由来定义你的工作生活,并且可以随心所欲地优化它!对我而言,我喜欢滑雪,而且我住在距一个世界级滑雪度假村 5 英里的地方。所以,我制定了规则,可以随时决定去滑雪——而我的老板完全支持这一点!
至于选择为谁工作,我的意思是,你可以自由选择为任何你想为之工作的人,而不是仅仅因为某个经理指派给你。
我必须告诉你,在独立创业的过程中,这些自由真的是最引人注目的!虽然我在理论上知道它们的存在,但当你真正开始以这种方式生活和工作时,你会对工作与生活的平衡有更深刻的理解。作为我的职业生涯中第一次,我觉得我真的掌控了自己的工作和幸福。我不再受雇主的心血来潮所左右,雇主可能对我有或没有忠诚度。说实话,企业忠诚度已经是过去的事情了。
在下一部分,我们将探讨如何开始, hopefully 其中的一些好处会变得更加清晰。

图片来自Glenn Carstens-Peters ,来源于Unsplash
那么,如何跨出这一步呢?
如果你决定尝试这个,有两件关键的事情能帮助你取得成功:计划(财务和业务)以及建立人脉。
财务规划
在你决定独立之前,有很多事情需要考虑,其中大部分是财务方面的。你需要知道,你可能会在一段时间内没有薪水,而且可能需要几个月甚至更长时间才能恢复到你之前的公司薪资水平。所以,如果你有机会存些钱度过这段时间,那会真的有很大帮助。
第二,你应该坐下来算一下每个月需要赚多少钱,因为这对于确定你的收费标准至关重要。在我的情况下,我的目标是替代我的公司薪资。我是家里的主要收入来源(有时也是唯一的收入来源,因为我的配偶做季节性工作)。因此,我不仅看了我的工资,还考虑了健康保险的费用、我为退休储蓄投入了多少钱,以及经营业务的预期费用(包括支付律师和会计师费用)。这些决定了我所需的总收入。接下来,你需要知道,在美国,自雇人士需要按季度缴纳预估所得税,大约是 35%。通过这个,我能够确定每月的净收入需要达到多少,才能弥补我之前的公司薪水。
接下来,你需要考虑你的定价模式。你会按小时收费,然后跟踪你的工作时间吗?还是按月收取固定费用作为保留费用?也许你会按项目收费?你是打算自己编写代码(自由职业)还是提供如何启动项目的建议(咨询)?也许你会寻求一个兼职角色?刚开始时,我发现最容易理解的是按小时收费的自由职业模式。那些已经独立工作了一段时间的人会告诉你,这并不是理想的选择,实际上按保留费用收费要更为理想。在这个阶段,我的客户更倾向于按小时计费,所以我也是这么做的。因此,很容易看出,你可以通过这个收支平衡的月净收入,反推你应该收取的小时费率。
但是这里有一个陷阱!我原本以为我会拿这个金额,并假设每周工作 40 小时来计算小时费率。错了!你需要考虑一些缓冲时间,因为事实证明,大多数人即使在公司工作,也并不真正每周工作 40 小时。例如,当我吃午饭、遛狗、锻炼或接孩子放学时,我不会向客户收费。我也只在自己坐在键盘前工作、为客户处理事务时才计费,而不是当我在“闲坐”想着他们的问题时计费。你还需要为建立人脉、安排下一份合同等分配时间。一位导师告诉我一个很好的经验法则,那就是你实际上只会有 60%的时间在计费。这样可以给你一个实际的缓冲,使你仍然能够在合理的计费小时数内实现目标净收入。
基于此,我设定了我的小时费率。当我将这个费率与我以前在类似角色中与之合作的承包商的费率进行比较时,发现它是合理的。(考虑到承包公司通常会在基础费用上加大约三分之一作为费用,所以承包商实际拿到的工资通常是承包公司收费的三分之二。)我利用我的人脉并将这个费率与他们交流,他们都同意我的小时费率是合适的。
商业计划
除了为财务情况做规划外,你还应该规划业务的设置。有很多不同的方式可以做到这一点,但我不是律师,你应该在做决定之前咨询律师。其中一种方法是以个人独立承包商的身份工作(有时根据这种做法使用的税务文档,也被称为 1099)。另一种选择是设立实际的公司,比如有限责任公司(LLC)或公司(Corporation)。我选择创建一个 LLC,因为它提供了一定程度的法律保护,并将业务的财务与我的个人财务分开(即,如果客户起诉公司,通常很难追究你的个人财务)。我请了一位律师帮助我在家乡州设立 LLC。也有很多在线选项可以让你自己完成这一过程,但我觉得由专业人士来处理这件事给我带来的安心感是值得的。
我还与负责我家个人税务的会计合作,帮助处理业务税务。关于这一点我不能说太多,因为我还没有提交我的第一次季度预估税款,所以这可能会在未来的文章中提到。然而,目前我可以说的一点是:你必须为所有的费用保存收据。你会有很多费用,如支付给律师的费用、购买电脑、网页和域名等,都是可以税前扣除的。然而,这些必须得到记录和管理。如果你从一开始就严格跟踪这些支出,将来会节省大量时间。
最后,一旦你设立了业务,为它开设一个独立的银行账户可能会有所帮助。虽然这可能不是强制性的,但我发现这很有用,因为它真正将业务财务和个人财务清晰地区分开。当该是收款的时候,业务会给你开支票。将借记卡与该账户绑定,进一步有助于保持财务分开。

照片由Matthew Osborn拍摄,来源于Unsplash
网络建设
我们从小就被告知,人脉很重要。这一点在自由职业或咨询时尤其真实。当我决定开始自己的业务,尤其是在成立了有限责任公司(LLC)后,首先做的就是联系我的人脉,让他们知道我在做什么。在我近 23 年的职业生涯中,我非常幸运,遇到了许多优秀的人,他们直接从事数据科学工作,或者在与数据科学家有经常合作的公司工作。我最初通过和一些导师悄悄交谈开始,但当我准备好时,我就在 LinkedIn 上宣布了我已经成立了公司。我非常幸运,我的人脉中有一些人需要按小时计费做自由职业 LLM 工作。部分兴趣来自我以前合作过的人,其他兴趣则来自那些在网上看到过我内容的人。
在我看来,建立人脉是那些被裁员的人(或者那些考虑把它作为裁员保护选项的人)可能比财务规划更难的一项工作。然而,这并不意味着你不能迅速建立自己的网络。参加(更好的是:在会议上发言)会议和聚会是一个很好的方法。理想情况下,你应该有一个愿意为你支付参加费用的雇主。即使他们不愿意,也可以把这视为对自己和职业的投资。如果你已经在经营个人业务,你可以把这些差旅费用算作税前支出。许多大型会议会为失业人士提供折扣。此外,大多数(许多?所有?)会议都需要志愿者协助。所以即使你不发言,你依然可以有机会结识并与他人建立联系。写博客和参与开源项目也是扩展人脉的方式。此外,拥有一个供他人查看的作品集也很有帮助。

图片由Lukas Blazek提供,发布于Unsplash
关于日常操作的一些建议
虽然我刚开始接触自由职业,但我已经学到了一些有助于提高效率的方法。在过去的几个月里,这些方法几乎都与让客户更轻松的理念有关。
你应该考虑到有一些工具(属于商业支出)是必需的。首先,就像在公司工作一样,你应该有一个专门用于工作的电子邮件地址。对我来说,我为我的公司购买了一个域名,并为该域名设置了 Google 工作区。当某些平台要求提供你的工作电子邮件地址并对 Gmail 地址嗤之以鼻时,这也会有所帮助。接着,我利用 Google Sheets 来管理我的开销和时间追踪。(我可能会将其转移到 QuickBooks,但还没有进行过这个转换。如果等第一轮税务完成后,问我是否值得的话,再告诉你。)
接下来,我发现拥有一种方式让人们轻松地预约时间与我见面是非常有帮助的。我发现Calendly是一个非常好的选择。如果你打算这么做,我建议你为别人预约与你见面的时间设定一些限制。记住,当你没有在计费时,你就没有收入。所以,尽量避免把所有的时间都花在潜在客户会议上。
我还考虑了如何发送发票和收款。同样,这里需要强调的观点是,要让你的客户能够轻松支付给你。你可以通过 Stripe 或 Square 设置账户来实现,或者 QuickBooks 也能为你完成这项工作。不过,要记住,他们会收取大约 3%的费用,这会在总发票中扣除,这样费用就可能逐渐累积起来。另一个收款方式是通过银行转账/ACH,尽管这种方式对你的客户来说可能更难,但至少不会扣除手续费。要准备好与客户合作,找到最适合他们的方式。并且要记住,拥有一个商业银行账户,不论是用于 ACH 转账还是 Stripe/Square/QuickBooks,意味着你不需要将个人银行信息透露给客户。
最后,重要的是要知道每个客户都有一套他们最喜欢的工具。有些人喜欢使用 Slack,另一些则非常依赖 Microsoft,偏好使用 Teams。虽然我个人有很强的偏好,但我发现能够在这些工具中灵活工作是非常重要的。就我而言,这意味着我不得不购买一台 PC,因为我的 Linux 机器处理 Teams 并不顺畅。C’est la vie.

图片来自Becca Tapert,来源于Unsplash
结语
事实上,我现在只独立工作了几个月。所以目前还为时过早,我不能说我已经 100%成功。然而,我非常幸运,到目前为止,我已经实现了所有关于自己公司的目标。而且,我非常享受这种新获得的自由、灵活性和生活中的平衡。
虽然我在这篇文章开头提到,许多人因为缺乏安全感而不考虑自由职业,但实际上我发现自己比在公司工作时更有安全感。首先,我不会面临裁员。事实上,我猜测,越来越多的公司将开始转向自由职业者、合同员工和兼职员工,因为裁员的原因。并不是因为他们裁掉了员工,就减少了必须完成的工作量。他们仍然需要人来做这些工作,即使不增加员工人数。合同会来来去去,持续争取合同是这种生活方式的一部分。
说到这些,我认为我的成功的真正定义是能否维持这种工作——以及通过个人努力替代公司工资的目标——不仅仅是在前几个合同之后。我们需要一段时间才能真正知道答案。但目前来看,情况是积极的!!!

图片由Pro Church Media提供,来源于Unsplash
我在做自由职业数据科学家这 9 个月中学到的东西
·发布于Towards Data Science ·19 分钟阅读·2024 年 10 月 1 日
--

图片来自Persnickety Prints 于Unsplash
介绍
我简直不敢相信,已经过去 9 个月了,自从我开始作为自由职业数据科学家工作!我曾在三个月后写过关于做出这一决定的经历。当时我和丈夫约定,我们会尝试三个月,到那时就能知道是否适合我们。很高兴(也是幸运)地说,在大约一个月后,我们就知道自由职业对我们来说是可行的。
我原来的文章引发了许多人关于裁员、强制返办公室(通常是伪装成裁员的政策)和过度劳累的公开和私下提问。在过去的六个月里,我也学到了更多关于如何让这项工作成功的经验。我也犯了一些关键错误,并从中学到了不该做的事情。所以我认为是时候更新一下原文了。
我确实推荐你先阅读我的原始文章,因为里面有一些重要的内容我在这里不会涉及。像上次一样,我还想向布雷特·特雷诺尔致敬,他创建了一个在线社区(大多数是 X 世代的人,但对大多数人都有帮助),叫做“企业逃逸者”,致力于帮助人们摆脱公司工作。他在TikTok上也有很棒的存在感。布雷特和“逃逸者”们一直是我交流创意的好伙伴,并提供了大量关于如何开始并在自由职业者、部分时间工作者、顾问或单打独斗的职业生涯中蓬勃发展的信息。
我喜欢独立工作的更多理由
在我的原始文章中,我列出了几件我喜欢独立工作的事情。这些事情包括像是我可以在我想工作时工作,做我想做的工作,制定我公司自己的规则。在过去的 6 个月里,我发现了一些新的理由,也更深入地了解了我之前知道的那些理由。所以我想在这里简要地描述一下。
首先,我(仍然)可以在家工作。我从 2017 年开始就在家工作,这让我有自由选择我想生活的地方。我是个山地爱好者,因此搬到一个远离我喜爱的户外活动的随机城市对我来说一点吸引力都没有。(事实上,我离开了我的博士领域,是为了能够找到一份远程工作……这是在那个领域里不可能实现的。)更重要的是,我们看到许多在疫情期间转向远程工作的公司现在又开始实行返办公室(RTO)命令。很多人都记录了这些命令实际上是伪装的裁员。所以实际上,作为自由职业者,我拥有更多的工作稳定性!而那些正在裁员的公司依然有需要完成的工作,只是因为裁员,缺少了相关职位来完成这些工作。这意味着自由职业在长期内实际上会更加稳定,因为这些公司仍然需要聘请人来做工作!
接下来——这可不是小事——是我不再受到绩效评审这个巨大时间浪费的影响。我曾看到同事们为写自评报告而焦头烂额,紧张到每一个标点符号都要反复推敲,结果这些自评报告基本被忽略。我自己也写过许多自评报告,里面包括一些笑话和滑稽的内容,但经理们从未对此发表评论或注意到,因为他们根本没读!绩效评审的过程是失败的。而当大多数时候,“符合预期”只能换来一个所谓的加薪,而这个加薪甚至不及生活成本的增长时,这到底有何意义呢?
这并不是说作为自由职业者,你的表现不重要。只是你不必感到焦虑,浪费时间听老板重复一大堆成就清单,告诉你做得不错,却没有给予你太多回报。相反,作为自由职业者的回报(或者说管理一个表现不佳的自由职业者的回报)是通过重复业务来体现的。客户是否选择续签或延长你的合同?他们是否将你的名字推荐给他们的朋友作为推荐人?如果是这样,那就做得好!
不容忽视的一点是,我掌控着自己“一人公司”的运作方式。这有很多不同的影响。首先,我不需要去请示许多经理是否可以做某件事。如果我想做某事,我就去做。理想情况下,应该会有其他人需要我做这种类型的工作,而我可以提供帮助。其次,我决定公司的财务状况,不需要请示是否可以参加会议、上课、去旅行与潜在客户会面,或购买新的软件工具。虽然这听起来有些傻,但实际上,我对能选择自己的电脑感到非常兴奋(以一种极客的方式)。我非常不喜欢被迫使用 Windows 或 Mac(Pop_OS 万岁!)或被限制使用某种特定的硬件。如果我认为参加某个会议或在会议上发言对我的业务有帮助,我就去参加,无需像“妈妈我可以吗?”那样请求批准。如果我决定购买某本书来继续学习,我也不需要向别人请示。在这方面,我有很大的自由!(顺便说一下,这些费用也可以作为税前扣除!)

人脉建立(图片由作者使用 DALL-E 创建)
关于人脉的重要性
自从我六个月前写下第一篇文章以来,毫无疑问,我收到的最常见问题都与人脉建立有关。当你作为自由职业者工作时,老话常说:“你的网络就是你的净资产。”这句话有很多深刻的含义,并非所有含义都令人愉快。所以,我打算在这里分享一些硬真相。
首先,人脉是逐渐建立的。良好的人脉通常包括以下几类人:
-
你曾经合作过且熟悉你工作的人
-
你所在领域的其他人,他们了解你的经验、技能和兴趣
-
为那些有问题你可以解决的公司的员工
(请注意,这并不是一份详尽的清单,但你明白我的意思。)
当你是自由职业者时,你是在销售一个品牌,而这个品牌就是你。可以把它当作购买一辆车来考虑。你不会去买一个你从未听过的品牌的车。而且,你也不会因为某个品牌有一堆零件就去购买他们的车,尤其是这个品牌以前从未制造过车。人们购买他们信任的东西。
这意味着,如果你没有作为数据科学家工作过一段时间,成为一名成功的自由职业者——无论是在数据科学领域还是其他领域——都是非常困难的。当客户雇佣自由职业者时,他们是为了要解决一个问题。他们希望知道雇佣的自由职业者是否知道如何解决这个问题,并且有相应的经验。这意味着,如果你刚从学校毕业,作为你的第一份工作做数据科学自由职业者将会非常困难。即便是研究生阶段也是如此。真实世界的经验对于那些寻找自由职业者的人来说是非常重要的。
好消息是,获得所谓的“真实世界经验”本身就是发展网络的一个关键步骤(也就是上面第一条中提到的那个群体)。我认识的许多自由职业者,他们的前雇主是他们的首批客户。这也是为什么避免和曾经共事过的人断绝关系非常重要,因为你永远不知道他们将来可能会成为你的客户!
在我之前的文章中,我提到了像参加会议和演讲这样的方式,以进一步扩大你的网络。我依然坚持这一点。然而,同样重要的是要认识到,并非每个人在会议中都表现得很好。有些神经多样性的人会觉得会议很困难。那也没关系!除了会议外,还有其他方法可以建立你的网络,尤其是像博客这样的方式,不仅限于这里,还可以在其他地方!无论是在会议上还是写博客,你要做的就是扩大你的个人品牌和品牌认知度。当人们想到你是一个数据科学家时,你希望他们想到什么?也许你在预测方面很擅长,那就写博客吧!也许你非常喜欢写代码来解决某类问题,那就制作一个 YouTube 视频吧!
通过优秀的作品集来提高你的品牌认知度(以及网络)
这里重要的是要创建你自己的品牌认知度。显然,这意味着人们需要能够找到你的品牌并了解它。特别是如果你的网络还不大,这就意味着人们需要能够看到你的作品。此时,创建一个非常棒的作品集就能起到帮助作用。把你的作品集展示给上面提到的那类人,能帮助你扩大网络并找到工作。
目前有大量关于如何创建一个优秀数据科学作品集的内容。我在这里总结一些关键点。
首先,你的作品集应该使用一个有趣的数据集。不要使用来自教育论坛的数据集,如泰坦尼克号数据集、MNIST、猫与狗的图像分类等。虽然 Kaggle 是一个很好的学习工具,但它不一定能反映实际工作中的情况。你希望数据尽可能真实。它应该是嘈杂的和凌乱的,因为这才是现实世界数据的真实写照。它应该回答一个有趣的问题(如果这个问题能为公司赚钱或解决一个大问题,那就更棒了)。同时,它也应该是你感兴趣且有一定了解的主题的数据,这样你才能亲自判断答案是否合理,并像专家一样带领别人理解。
其次,你需要为作品集中的每个项目讲述一个完整的故事。不要仅仅放上一堆没有任何使用说明的代码。不要展示某个模型的结果,却没有解释模型和数据的运行情况以及应该得出什么结论。一个好的作品集项目,讲解你的工作与代码本身同样重要。你的解释应该是详细的。你需要证明自己不仅会编程,而且能够将问题到解决方案的完整过程清晰地呈现给读者。
你的作品集项目,如果可能的话,应该是互动性的。你希望别人能够看到代码是如何运行的。我个人很喜欢在某个地方设置一个便宜的虚拟机,并运行Streamlit仪表盘来增加互动性。
因为你的作品集关乎品牌认知,所以要思考一下你的品牌是什么。例如,如果你希望自己在推荐引擎方面非常出色,那么就不要浪费时间展示图像分析方面的解决方案。你将展示给未来的客户你能够为他们解决哪些类型的问题。你越能让这一点显而易见,就越好。
最后,每当你更新你的作品集时,你需要将其传播出去。可以写一篇博客文章或制作一个 YouTube 视频来配合展示。将代码公开发布在 GitHub 或 GitLab 上。在 LinkedIn 上发布作品集链接并指出新增内容。博客文章发布后,再发布另一个链接。

由作者使用 DALL-E 创作的图像
尽可能成为一个全能型人才
我喜欢成为一个专家。很多人也喜欢。我在一些非常具体的领域拥有相当深的知识。然而,作为自由职业者意味着要解决问题。特别是对于那些员工不多的初创公司,你常常需要知道如何做的不仅仅是创建一个小而精妙的解决方案。你需要知道如何从头到尾解决问题。这意味着你需要超越那个小而专业的技能。
对我而言,这意味着我在学习(这是一个持续的过程)许多超出我最喜欢的图表、自然语言处理、LLM 等领域的技能。我不得不学到很多关于数据工程、MLOps/LLMOps 和云架构的知识。我正在花钱上课并参加这些主题的会议(见上文……我的管理层批准了 😉)。这些是对我的投资,这意味着它们也是对我的业务的投资。我能提供给客户的东西越多,我能够解决的客户问题就越多。作为一个自由职业者,你能够尽可能多地提供服务是非常重要的!

图片由作者使用 DALL-E 创建
为什么客户多样性很重要
在我刚开始做自由职业时,我在思考谁是我的梦想客户。了解我的人都知道,工作文化对我来说非常重要。我所见过的那些最接近我的价值观的工作文化的地方,往往是初创公司。我也知道自己喜欢处理哪些类型的数据科学问题(图表、自然语言处理、生成式 AI、地理空间分析)。因此,我最初认为我会寻找那些有这些类型问题的初创公司。
然后,我接触到了一本很棒的书——《自由职业者圣经:实现梦想职业的所有必备知识——按照你的方式》,作者是 Sara Horowitz。这本书让我对客户有了不同的看法,尤其是在创建一个多样化的当前和潜在客户池方面。
[## 《自由职业者圣经:实现梦想职业的所有必备知识——按照你的方式》
《自由职业者圣经:实现梦想职业的所有必备知识——按照你的方式》- Kindle 版,作者...
如果你深入阅读这本书,你将会了解到,在自由职业中,最佳的财务稳定性来自于拥有各种不同类型的客户,从那些稳定且持续的大客户,到那些经过一些工作后可能会带来可观收益的客户,再到那些可能是长远发展潜力的客户。值得注意的是,在我的情况中,初创公司由于它们本身就是初创公司,尤其是早期阶段的初创公司,可能并不一定被视为“稳定且持续”。如果你仅依赖它们,那么你需要为工作被取消或工时被削减做准备,这通常是由于现金流和预算的波动。而通过接收大量初创公司来弥补这一点是一个糟糕的主意(请参见下文关于工作量过大的内容)。这也意味着,你需要有足够的财务缓冲,以便在某段时间内如果工作量减少,你仍然可以支付账单。
实际上,最好是拥有来自不同类型公司的客户组合,包括一些经营多年的公司、初创企业和介于两者之间的公司。这将使你的自由职业业务在合同结束或客户需要减少工作时更加具有韧性。记住目标:稳定、可预测的收入。

照片由Junseong Lee提供,来自Unsplash
离开公司后,你会失去一些福利
如果你来到这里是想讨论作为自由职业者的美国健康保险问题,我会引导你去查看下方的“我迄今为止犯过的错误”部分。别担心,我会涵盖这个话题。但我们先来谈谈其他一些保险话题。
迄今为止,独立工作最大的变化之一就是我不再拥有公司提供的福利。这带来了一些影响,在你为独立工作做财务规划时,需要将这些因素考虑在内。由于其他国家提供的福利不同,本节内容可能会偏向美国。不过,不论你身在何处,了解这些内容绝对是必要的。
首先,当你为一家公司工作时,除非有少数例外情况,他们不太可能因你的工作做得不好而起诉你。然而,当你以独立创业者身份工作时,情况就不一定如此。当我刚开始时,我并没有真正考虑到这些问题。我只是认为我和客户之间会有一份明确的工作说明、良好的沟通和清晰的交付成果,所有问题都会得到解决。我还没有被起诉过(并希望永远不会!),但天真地认为未来不会发生这种事是不现实的。
这就是职业责任保险的作用。它为企业和个人提供保障,防止因错误、漏发货物或违反合同等指控而产生的索赔。事实上,一些你可能与之签约的大型组织实际上会要求你拥有这类保险。所以最好提前为此做好规划。在线购买非常容易,合理的计划每月费用不到 200 美元。
另一个我最初没有考虑的保险是长期残疾保险。许多人将短期和长期残疾保险作为公司福利的一部分,而根本不会考虑这个问题。在我个人的情况下,我是家庭的唯一经济支柱,尤其是大部分年份(我的配偶做季节性工作)。如果发生什么事我无法工作,这将对我的家庭造成毁灭性的打击,我们承受不起。因此,考虑购买残疾保险是你真的应该认真思考的事情。
一般来说,“长期”意味着超过 3 个月。如果你坐下来算一算,你会发现短期和长期的价格差不多。因此,我们决定不购买短期残疾保险,因为它太贵了,这意味着我们会确保自己有足够的财力,在万一发生什么事情时能够在没有收入的情况下维持 3 个月的生活。
企业雇主提供的另一个大福利(至少在美国)是,他们会为你的退休金做出一定的贡献。通常,这表现为 401k 匹配贡献。作为自由职业者,你仍然可以为退休金做出贡献,但你需要做出有意识的决定,因为你将不再有雇主自动从你的薪水中扣除这笔费用。在这一点上,我强烈建议你与财务顾问讨论储蓄退休金的最佳方式。

图像由作者使用 DALL-E 创建
到目前为止我犯过的错误
我可能让做自由职业的想法看起来有点像彩虹、小狗和独角兽。从某种程度上说,这确实是我对它的感觉。但我也犯了一些相当大的错误。所以请从我和我的错误中学习,在做决策时比我当时拥有更多的信息!
健康保险
至少在美国,这是让人们紧紧捆绑在公司工作的“800 磅的大猩猩”。当你自己做生意时,没有经济实惠的方式来获得健康保险。是的,有开放市场(也就是奥巴马医保),至少这个选择是存在的(向那些在奥巴马医保之前就已经做自由职业的自由职业者致敬)。
正如我之前说的,我的配偶是季节性工作者,但在工作季节期间会提供健康保险。因此,当我离开公司时,选择加入他的健康保险是有道理的。这让我们开始琢磨在季节结束后该怎么办。我们可以选择 COBRA(通过现有的公司计划继续保留健康保险,同时支付全部月度保费)或进入开放市场。因为我在他还在工作期间需要做手术,所以我们决定选择 COBRA,因为我会达到最大自付额度(OOP),这样我们就可以利用这一点,剩下的时间不需要支付任何费用。
这是一个错误。
问题在于我们没有完全意识到 COBRA 的费用。我们知道它很贵,但完全低估了它的成本。对于我们一家 3 口来说,费用高达每月 $3000。没错,真的。现在我知道,我可以在开放市场上以一半的价格获得类似的保险套餐。它仍然不便宜,而且无论如何都可以税前扣除。如果我坐下来认真计算一下,通过达到最大自付额度(OOP)我们节省了多少,实际上还是选择开放市场更便宜。这是我们明年不会再犯的教训。
承担过多工作
我承认,我经常担心自己没有足够的可计费时长。在自由职业的初期,你会担心自己赚不到足够的钱,于是接了很多工作来确保收入。后来,在这段旅程中,你会遇到合同结束(有时是提前结束)或客户减少你工作时长的情况。
我的焦虑有时会占据我的心头。为了确保我永远不会低于在公司工作时的收入,我往往会过度补偿,接太多客户或工作时长。我很幸运能处于这种可以这样做的情况。一些人建议我招聘更多员工来公司,以便我能够应对这些工作量。然而,这是一把双刃剑。一旦你聘用了员工,你就有责任为他们提供工作。如果我每个月都在为是否能满足收入要求而感到焦虑,那么再增加员工只会让这种感觉变得更糟。
关于焦虑的最后一点涉及到你的财务规划。在我之前的文章中,我谈到过这一点。不过,我想补充一点,你可以通过在开始自由职业之前拥有一定的储蓄来帮助缓解一些焦虑。这样,如果某个月工作较少,你就不会立即陷入困境。我曾收到的一个建议是,能够在没有任何付费工作的情况下自给自足六个月。这听起来可能有些极端,但为工作较少的时候预留资金是非常非常重要的。
关于与大型组织合作的复杂性
能够与各种规模的客户组织合作是非常棒的。正如我之前所说,我真的很喜欢与小型初创公司合作。然而,正如我之前提到的,客户的多样性同样重要。因此,支持一些大型公司是一个不错的选择。
但这并非没有复杂性。组织越大,官僚主义越重。这对于自由职业者意味着,签合同的过程需要更多时间,而且更难。客户通常要求提供诸如责任保险证明(见上文)、在复杂系统中注册为法人实体,甚至进行药物和背景检查。有时,他们甚至要求你自己先支付这些费用,才能与你合作。
如果你想与这些公司合作,记住这一点非常重要。不仅你为这些事情准备时间不会得到报酬(事实上,这会花费你一些钱,因为它占用了你原本可以为其他付费客户工作的时间),而且你还可能需要花费一些费用,如药物和背景检查费用、为责任保险注册等。是否值得,就由你来决定。同样,我再次提到客户多样化的重要性。

拥有支持网络
当你独立工作时,支持网络的重要性不容小觑。在公司工作时,你总是可以与同事见面(面对面或虚拟),进行抱怨、交流想法或进行社交。作为自由职业者,你无法轻松地享受这一点,这可能会让你感到孤独。
到目前为止,我能够让自己的自由职业生涯顺利进行,得益于一群人的支持。首先,没有我丈夫的支持,这一切都是不可能的。事实上,我认为如果你有伴侣,独自创业没有伴侣的支持是极其糟糕的主意。自由职业没有任何确定性或保障,这对家庭来说是一个挑战。
同样,拥有可以与之交流想法的人也很重要,但这些人不能是你的客户。有很多在线的独立创业者群体,比如我之前提到的《企业逃离者》群体,他们都是处于类似境地的人。许多人并非数据科学家,但他们在商业方面积累了丰富的经验。我强烈推荐找到一个你能够产生共鸣的群体。

图片由Nick Fewings提供,来自Unsplash
结语
我很幸运,经过 9 个月,一切进展顺利!在我最初的文章中,我谈到过不在公司工作让我感觉更加安全,这一点今天更为真实。随着科技行业的裁员,我经常听到朋友和以前的同事说他们在找新工作时遇到的困难。这真的非常令人难过,我为他们感到非常难过。
我在原文中也提到过,我对成功的定义是看我是否能长期维持这一模式。我经历过工作时间受限和客户来来去去的情况,但每次似乎事情都能得到解决,因为我总能找到其他工作来填补空缺。因此,这对我来说是一个非常积极的信号。
我的下一步计划,我希望六个月后能向大家报告,涉及多元化收入来源。作为自由职业者按小时工作是不错的,但现在我希望创造其他类型的工作,无论是通过长期合同还是创造可售的产品,来产生超出简单按小时计费的收入。敬请期待!
如果 ChatGPT 实际上是来自另一个世界的导游呢?(第二部分)

我测试了一个直觉,并在 GPT 内部发现了一些既美丽又神秘的东西。
·发表于数据科学之路 ·阅读时长 6 分钟·2024 年 4 月 13 日
--
第一部分的假设是ChatGPT 是一个导游,带领我们穿越计算机游戏 Minecraft 的高维版本。
吓人吗?当然,但我还是测试了这个假设,并在 GPT 中发现了一些既美丽又神秘的东西。这就是我发现的内容,以及我揭开这一发现的步骤。
首先,我们将澄清“高维”是什么意思。然后,我们将从 GPT-4 收集维度数据,并将其与 Minecraft 进行比较。最后,为了好玩,我们将创建一个使用实际 GPT-4 数据结构的 Minecraft 世界,看看它是什么样子的。
为了澄清“维度”,请考虑以下引用:
“我认为,理解并将 GPT-4 视为工具而非生物非常重要,虽然很容易搞混...” — OpenAI 首席执行官 Sam Altman,在 2023 年 5 月 16 日的参议院司法委员会隐私与技术小组听证会上的证词
马还是锤子?我们可以直接问 ChatGPT。然而,答案将取决于 ChatGPT 的自我意识程度,而这又依赖于它的生物性,因此形成了一个死循环。
相反,我们将依赖直觉,并从不同的维度来观察这个问题。维度是某种可测量的范围。例如,在“工具”维度中,锤子似乎比马更“像工具”。在二维维度下,情况也类似。马更像生物,而不像工具,远不如锤子。
GPT 适合在哪个位置?可能更接近两者中的锤子。
如果我们添加一个名为“智能”的第三维度会怎么样?这时事情变得有趣了。马比一袋锤子聪明,而 GPT 看起来也相当聪明。所以,在这三个维度中,GPT 实际上可能位于马和锤子之间的某个地方。

锤子与马的插图。Rawpixel。www.rawpixel.com/image/6439222/; www.rawpixel.com/image/6440314
可视化二维是很容易的,三维稍微难一些,但我们完全可以用成千上万的维度来描述马和锤子。实际上,使用多维度来衡量事物是有充分理由的,因为它能增强对事物的理解。GPT 的神奇之处在于,它似乎不仅仅绘制了马和锤子,而是几乎所有事物都在成千上万的维度中!
但是,GPT 是如何在成千上万维度中表示事物的呢?
使用一种叫做嵌入的技术。
嵌入是将单词、图片和其他数据转换成一组数字的方式,目的是让计算机能够理解它们的含义并进行比较。
假设我们希望计算机理解苹果和柠檬的含义。为每种水果分配一个数字可能行得通,但水果比一个数字要复杂得多。所以,我们使用一组数字,其中每个数字表示水果的外观、味道和营养成分等。这些数字列表就是嵌入,它们帮助 ChatGPT 明白苹果和柠檬都是水果,但味道不同。
遗憾的是,GPT 的嵌入表示超出了人类的理解和可视化范围。例如,仅仅是“apple”这个词的三千个嵌入看起来是这样的:

是否有可能在不影响整体结构的情况下减少维度数?幸运的是,这种情况时常发生——在一个阳光明媚的日子里,你的影子就是你三维身体的二维投影。虽然有很多复杂的数学方法可以进行降维,但我们将简化处理,直接取 OpenAI 给出的前三个嵌入,其他的则丢弃。
这可能行得通吗?
让我们来看看。我们将从选择几个单词开始实验:horse, hammer, apple, 和 lemon。然后,为了保持趣味性,我们还将选择一些可能(也可能不)在语义上相关的单词和短语:“cinnamon”,“given to teachers”,“pie crust”,“hangs from a branch”,以及“crushed ice”。
接下来,我们将查看它们的嵌入。OpenAI 提供了一个名为嵌入引擎的工具,它让这变得很简单。你输入一个单词或短语,它会返回三千个嵌入(准确来说是 3,072 个)。
通过使用一段代码,我们将提取每个单词的前三个嵌入,其他的则丢弃。结果如下:

这些数字究竟是什么?如果我们说实话,没人真的知道;它们似乎能精准地定位每个单词和短语在 GPT 内部某个特定且神秘的维度中的位置。为了我们的目的,暂且将这些嵌入向量当作 x、y、z 坐标来处理。这种方法需要一个令人震惊的大胆假设,但我们不会在这上面停留——而是将它们绘制在图表上,看看到底会出现什么。

图片由 Plotly.com 创建
你看到了吗?!
John Firth一定会感到骄傲。像苹果一样的东西似乎是邻居(准备做个派)。碎冰和柠檬彼此相邻(准备做柠檬水)。锤子则在角落里。
如果你对这个结果没有感到完全震惊,也许是因为你是一名已经见惯了这些的 数据科学家。对我而言,我简直不敢相信刚刚发生的事情:我们查找了九个单词和短语的嵌入向量,丢弃了 99.9%的数据,然后将剩余的部分绘制到一个 3D 图上——令人惊讶的是,这些位置直观上是有意义的!
还是不觉得震惊吗?那么也许你在想,这一切与 Minecraft 有什么关系。对于玩家们来说,我们即将把分析推向下一步。
使用Minecraft Classic,我们将构建一个 8x8x8 的围墙花园,然后像我们在 3D 图中做的那样“绘制”这些单词和短语。看看它是如何呈现的:

请注意,花园中单词和短语的位置与 3D 图中的位置一致。这是因为嵌入向量就像虚拟世界中的位置坐标——在这里,就是 Minecraft。我们所做的,是将一个 3,072 维的嵌入空间缩减到 Minecraft 中的三维“影像”空间,然后像这样进行探索:

谁是在我们花园中跳跃的探险者?那就是 ChatGPT,高维的讲解员,精通复杂的数据结构——我们通往 GPT 优雅神秘世界的使者。当我们提交提示时,正是 ChatGPT 辨别我们的意图(这可不是小事,利用所谓的注意力机制),然后轻松地穿越数千维度,引领我们到达 GPT 宇宙中恰到好处的地方。
这一切是否意味着 ChatGPT 实际上是来自另一个世界的导游?它真的在一个高维游戏空间中运作吗?虽然我们不能确定,但 GPT 似乎比马或锤子更像一个游戏:

锤子与马的插图。Rawpixel。www.rawpixel.com/image/6439222/; www.rawpixel.com/image/6440314
除非另有说明,所有图片均由作者提供。
—
参考资料:
“API 参考。” OpenAI,[2024 年 4 月 4 日]。platform.openai.com/docs/api-reference。
Sadeghi, Zahra, James L. McClelland, 和 Paul Hoffman. “你将通过单词的共现了解一个对象:基于对象在视觉场景中的共现所推导出的语义表示的研究。”《神经心理学》76(2015):52–61。
Balikas, Georgios. “开放源代码与商业嵌入模型在问答中的比较分析。”《第 32 届 ACM 国际信息与知识管理大会论文集》。2023 年。
Hoffman, Paul, Matthew A. Lambon Ralph, 和 Timothy T. Rogers. “语义多样性:基于词语上下文使用变异性的语义模糊度度量。”《行为研究方法》45(2013):718–730。
Brunila, Mikael, 和 Jack LaViolette. “单词都与哪些公司有关?重新审视 JR Firth 和 Zellig Harris 的分布式语义。”arXiv 预印本 arXiv:2205.07750(2022 年)。
Gomez-Perez, Jose Manuel, 等. “理解词嵌入和语言模型。”《混合自然语言处理实用指南:结合神经模型和知识图谱进行 NLP》 (2020 年):17–31。
什么是缺失值的良好填补方法?
我目前对缺失值填补的理解
·发表于Towards Data Science ·18 分钟阅读·2024 年 6 月 8 日
--
本文是我最新的论文的总结与讨论。我们研究的是表格数据集的通用填补方法。即,填补方法应以一种适用于许多不同任务的方式进行(有时称为“广泛填补”)。
在本文中,我将分享我在过去几年处理该问题时学到的三个经验教训。我对这篇论文特别感到兴奋,但也保持谨慎,因为缺失值问题有很多方面,且很难保证不会遗漏某些内容。所以,我邀请你自行判断我的经验教训是否对你有意义。
如果你不想深入讨论缺失值的问题,我将在文章最后总结我的建议。
免责声明: 本文的目标是通过填补来重建原始数据分布。虽然我认为这是大多数研究者和实践者真正想要的目标,但这是一个困难的目标,并非在所有应用中都必要。例如,在进行(条件均值)预测时,有几篇近期的论文表明,即使是简单的填补方法对于大样本量来说也是足够的。
本文中的所有图片均由作者创建。
前言
在继续之前,我们需要讨论一下我在本文中如何看待缺失值。
我们假设有一个潜在的分布P**,从中抽取观察值X。此外,还有一个与*X相同维度的 0/1 向量,它被抽取,我们称这个向量为M。实际观察到的数据向量X就是被M遮蔽后的X**。因此,我们观察到 n 个独立同分布(i.i.d.)的联合向量(X, M)*副本。如果我们将其写成数据矩阵,可能看起来是这样的:

数据生成过程:X和 M 被抽取,然后我们观察到 n 个独立同分布的(X, M)副本,其中 X 是 X,但被 M 遮蔽。
通常小值x, m表示“已观察”,而大值则指随机变量。大家讨论的缺失机制就是对(X, M)*的关系或联合分布的假设:
完全随机缺失(MCAR): 值缺失的概率是抛硬币的结果,独立于数据集中的任何变量。在这种情况下,缺失值仅仅是一个麻烦。你可以忽略它们,只关注数据集中完全观察到的部分,这样就不会产生偏差。在数学中,对于所有的m和x:

随机缺失(MAR): 缺失的概率现在可以依赖于数据集中观察到的变量。一个典型的例子是两个变量,比如收入和年龄,其中年龄始终是观察到的,但收入可能在某些年龄值下缺失。这就是我们下面要研究的例子。这听起来很合理,但在这里可能会变得复杂。在数学中,对于所有的m和x:

非随机缺失(MNAR): 在这里一切皆有可能,我们不能就一般情况做出任何假设。
关键在于,对于插补,我们需要学习在一个模式m’中,给定观察到的值,缺失值的条件分布,然后将其插补到另一个模式m中。
实现这一目标的一个著名方法是通过链式方程进行多重插补(MICE)方法:首先用简单的插补方法填充数据,例如均值插补。然后对于每一次迭代t,对于每个变量j,将观察到的X_j回归到所有其他变量上(这些变量是插补的)。接着将这些变量的值输入到学习到的插补器中,插补所有未观察到的X_j。这一过程在这篇文章中有详细解释,并有一个很棒的插图,能让事情立刻变得清晰。在 R 语言中,这一过程方便地通过mice R 包实现。正如我在下面将概述的那样,基于我看到的表现,我非常喜欢这种方法。实际上,能够重建某些 MICE 实例的底层分布(如 mice-cart)的能力非常精准。在本文中,我们关注一个只有一个变量缺失的非常简单的例子,因此我们可以手动编写代码,模拟 MICE 通常会做的迭代过程,以便更好地说明发生了什么。
第一个小课程是,MICE 是一系列方法;无论你选择哪种方法将X_j回归到其他变量上,都能得到不同的插补方法。因此,在 mice R 包中有无数的变种,例如 mice-cart、mice-rf、mice-pmm、mice-norm.nob、mice-norm.predict 等。这些方法的表现差异很大,下面我们会看到这一点。尽管如此,至少有一些论文(例如 NeurIPS 等顶级会议)自信地宣称他们将方法与“MICE”进行比较,却没有详细说明他们到底使用了什么方法。
示例
我们来看一个非常简单但具有说明性的例子:考虑一个包含两个联合正态变量的数据集,X_1, X_2。我们假设两个变量的方差都是 1,且它们之间的正相关系数为 0.5。为了提供一些背景,我们可以假设X_1是(收入的对数),X_2是年龄。(这仅用于说明,显然没有人会介于-3 岁到 3 岁之间)。此外,假设收入X_1的缺失机制是,当年龄“较高”时,X_1往往缺失。也就是说,我们设置:

因此,当X_2(年龄)“较大”(即大于零)时,X_1(收入)以 0.8 的概率会缺失。由于我们假设X_2总是可观察的,这是一个典型的 MAR(缺失不完全随机)例子,包含两个模式,一个是所有变量都完全观察到的模式(m1),另一个是X_1缺失的模式(m2)。尽管这个例子很简单,但如果我们假设较高的年龄与较高的收入相关联,那么在从一个模式到另一个模式的转换中,收入和年龄的分布会发生明显变化。在模式m2中,收入缺失时,观察到的年龄和(未观察到的)收入的值通常较高。我们来看一下代码实现:
library(MASS)
library(mice)
set.seed(10)
n<-3000
Xstar <- mvrnorm(n=n, mu=c(0,0), Sigma=matrix( c(1,0.7,0.7,1), nrow=2, byrow=T ))
colnames(Xstar) <- paste0("X",1:2)
## Introduce missing mechanisms
M<-matrix(0, ncol=ncol(Xstar), nrow=nrow(Xstar))
M[Xstar[,2] > 0, 1]<- sample(c(0,1), size=sum(Xstar[,2] > 0), replace=T, prob = c(1-0.8,0.8) )
## This gives rise to the observed dataset by masking X^* with M:
X<-Xstar
X[M==1] <- NA
## Plot the distribution shift
par(mfrow=c(2,1))
plot(Xstar[!is.na(X[,1]),1:2], xlab="", main="", ylab="", cex=0.8, col="darkblue", xlim=c(-4,4), ylim=c(-3,3))
plot(Xstar[is.na(X[,1]),1:2], xlab="", main="", ylab="", cex=0.8, col="darkblue", xlim=c(-4,4), ylim=c(-3,3))

上图:在 X_1 已观察到的情况下,(X_1, X_2)的分布;下图:在 X_1 缺失的情况下,(X_1, X_2)的分布。
第 1 课:插补是一个分布预测问题
在我看来,(通用)插补的目标应该是尽可能地复制基础数据分布。为了说明这一点,我们再次考虑第一个例子,其中p=0,即只有X_1缺失。我们现在将尝试使用著名的MICE方法来插补这个例子。由于只有X_1缺失,我们可以手动实现这个过程。我们从均值插补开始,它仅仅是计算X_1在观察到的模式中的均值,然后将该均值填补到 NA 的位置。我们还使用回归插补,它稍微复杂一些:我们将X_1回归到X_2,在X_1被观察到的模式下,然后对于每一个缺失的X_1观测值,我们填入回归的预测值。因此,在这里,我们插补了给定X_2的条件均值。最后,对于高斯插补,我们从相同的X_1到X_2的回归开始,但接着通过从高斯分布中抽取值来插补每个缺失的X_1值。换句话说,我们不是插补条件期望值(即只是条件分布的中心),而是从这个分布中抽样。这导致了一种随机插补,起初可能有点反直觉,但实际上将会得到最佳结果:
## (0) Mean Imputation: This would correspond to "mean" in the mice R package ##
# 1\. Estimate the mean
meanX<-mean(X[!is.na(X[,1]),1])
## 2\. Impute
meanimp<-X
meanimp[is.na(X[,1]),1] <-meanX
## (1) Regression Imputation: This would correspond to "norm.predict" in the mice R package ##
# 1\. Estimate Regression
lmodelX1X2<-lm(X1~X2, data=as.data.frame(X[!is.na(X[,1]),]) )
## 2\. Impute
impnormpredict<-X
impnormpredict[is.na(X[,1]),1] <-predict(lmodelX1X2, newdata= as.data.frame(X[is.na(X[,1]),]) )
## (2) Gaussian Imputation: This would correspond to "norm.nob" in the mice R package ##
# 1\. Estimate Regression
#lmodelX1X2<-lm(X1~X2, X=as.data.frame(X[!is.na(X[,1]),]) )
# (same as before)
## 2\. Impute
impnorm<-X
meanx<-predict(lmodelX1X2, newdata= as.data.frame(X[is.na(X[,1]),]) )
var <- var(lmodelX1X2$residuals)
impnorm[is.na(X[,1]),1] <-rnorm(n=length(meanx), mean = meanx, sd=sqrt(var) )
## Plot the different imputations
par(mfrow=c(2,2))
plot(meanimp[!is.na(X[,1]),c("X2","X1")], main=paste("Mean Imputation"), cex=0.8, col="darkblue", cex.main=1.5)
points(meanimp[is.na(X[,1]),c("X2","X1")], col="darkred", cex=0.8 )
plot(impnormpredict[!is.na(X[,1]),c("X2","X1")], main=paste("Regression Imputation"), cex=0.8, col="darkblue", cex.main=1.5)
points(impnormpredict[is.na(X[,1]),c("X2","X1")], col="darkred", cex=0.8 )
plot(impnorm[!is.na(X[,1]),c("X2","X1")], main=paste("Gaussian Imputation"), col="darkblue", cex.main=1.5)
points(impnorm[is.na(X[,1]),c("X2","X1")], col="darkred", cex=0.8 )
#plot(Xstar[,c("X2","X1")], main="Truth", col="darkblue", cex.main=1.5)
plot(Xstar[!is.na(X[,1]),c("X2","X1")], main="Truth", col="darkblue", cex.main=1.5)
points(Xstar[is.na(X[,1]),c("X2","X1")], col="darkgreen", cex=0.8 )

针对不同插补方法绘制的(X_1, X_2)的分布图。不同的插补方法(红点为插补的值)。
研究这张图表立即揭示出均值插补和回归插补可能不是理想的,因为它们完全无法重建原始数据分布。相比之下,高斯插补看起来相当不错,实际上,我认为它很难与真实数据区分开。这看起来可能只是一个技术性的概念,但它有着深远的影响。想象一下,如果你得到这些插补后的数据集,并且现在你想找到回归系数,当回归X_2与X_1时(这是我们在插补中做的事情的反向操作)。在这种情况下,真实的回归系数是beta=cov(X_1, X_2)/var(X_1)=0.7。
## Regressing X_2 onto X_1
## mean imputation estimate
lm(X2~X1, data=data.frame(meanimp))$coefficients["X1"]
## beta= 0.61
## regression imputation estimate
round(lm(X2~X1, data=data.frame(impnormpredict))$coefficients["X1"],2)
## beta= 0.90
## Gaussian imputation estimate
round(lm(X2~X1, data=data.frame(impnorm))$coefficients["X1"],2)
## beta= 0.71
## Truth imputation estimate
round(lm(X2~X1, data=data.frame(Xstar))$coefficients["X1"],2)
## beta= 0.71
高斯插补接近 0.7(0.71),更重要的是,它非常接近使用完整(未观察到的)数据估算的结果!另一方面,均值插补低估了beta,而回归插补则高估了 beta。后者是自然的,因为条件均值插补人为地膨胀了变量之间的关系。这个效应特别重要,因为它会导致科学和(数据科学)实践中的效应被高估!!
回归插补看起来可能过于简单。然而,关键是,机器学习和其他领域中常用的插补方法正是这样工作的。例如,knn 插补和随机森林插补(即,missForest)。尤其是后者,在几篇基准论文中得到了好评并被广泛推荐,且应用非常广泛。然而,missForest 会在观测数据上拟合一个随机森林,然后通过条件均值简单插补。因此,在这个例子中使用它的结果看起来与回归插补非常相似,从而导致变量之间关系的人工增强和偏差估计!
许多常用的插补方法,如均值插补、knn 插补和 missForest,在复制分布方面失败。它们估计和近似的是(条件)均值,因此插补结果看起来像回归插补的结果(甚至对于均值插补来说更差)。相反,我们应该尝试通过从估计的(条件)分布中抽取样本来进行插补。
第二课:插补应该作为一个分布预测问题进行评估
与第一课的讨论相关,有一个双重问题。插补方法应该如何评估?
想象一下我们开发了一种新的插补方法,现在想将其与已有的如 missForest、MICE 或 GAIN 等方法进行基准测试。在这种情况下,我们人为地引入了缺失值,因此我们拥有的实际数据集与上面的一样。现在,我们想将这个真实的数据集与我们的插补结果进行比较。为了举例说明,假设上面的回归插补是我们的新方法,我们希望将其与均值插补和高斯插补进行比较。
即使在最有声望的会议中,这也是通过计算均方根误差(RMSE)来完成的:

这里实现了这个:
## Function to calculate the RMSE:
# impX is the imputed data set
# Xstar is the fully observed data set
RMSEcalc<-function(impX, Xstar){
round(mean(apply(Xstar - impX,1,function(x) norm(as.matrix(x), type="F" ) )),2)
}
这段讨论与如何正确评估预测的讨论相关。在这篇文章中,我讨论了(R)MSE 是评估(条件)均值预测的正确分数。事实证明,这个相同的逻辑同样适用;像这样使用 RMSE 来评估我们的插补方法,会偏向于那些插补条件均值的方法,如回归插补、knn 插补和 missForest。
事实上,插补应该作为一个分布预测问题进行评估。我建议使用完全观察数据与插补“分布”之间的能量距离。详细信息可以在论文中找到,但在 R 中可以通过“energy” R 包轻松实现:
library(energy)
## Function to calculate the energy distance:
# impX is the imputed data set
# Xstar is the fully observed data set
## Calculating the energy distance using the eqdist.e function of the energy package
energycalc <- function(impX, Xstar){
# Note: eqdist.e calculates the energy statistics for a test, which is actually
# = n²/(2n)*energydistance(impX,Xstar), but we we are only interested in relative values
round(eqdist.e( rbind(Xstar,impX), c(nrow(Xstar), nrow(impX)) ),2)
}
我们现在将这两个分数应用于我们虚构的研究项目,试图弄清楚我们的回归插补是否比其他两个方法更好:
par(mfrow=c(2,2))
## Same plots as before, but now with RMSE and energy distance
## added
plot(meanimp[!is.na(X[,1]),c("X2","X1")], main=paste("Mean Imputation", "\nRMSE", RMSEcalc(meanimp, Xstar), "\nEnergy", energycalc(meanimp, Xstar)), cex=0.8, col="darkblue", cex.main=1.5)
points(meanimp[is.na(X[,1]),c("X2","X1")], col="darkred", cex=0.8 )
plot(impnormpredict[!is.na(X[,1]),c("X2","X1")], main=paste("Regression Imputation","\nRMSE", RMSEcalc(impnormpredict, Xstar), "\nEnergy", energycalc(impnormpredict, Xstar)), cex=0.8, col="darkblue", cex.main=1.5)
points(impnormpredict[is.na(X[,1]),c("X2","X1")], col="darkred", cex=0.8 )
plot(impnorm[!is.na(X[,1]),c("X2","X1")], main=paste("Gaussian Imputation","\nRMSE", RMSEcalc(impnorm, Xstar), "\nEnergy", energycalc(impnorm, Xstar)), col="darkblue", cex.main=1.5)
points(impnorm[is.na(X[,1]),c("X2","X1")], col="darkred", cex=0.8 )
plot(Xstar[!is.na(X[,1]),c("X2","X1")], main="Truth", col="darkblue", cex.main=1.5)
points(Xstar[is.na(X[,1]),c("X2","X1")], col="darkgreen", cex=0.8 )

如果我们看 RMSE,那么回归插补看起来非常好!它优于均值插补和高斯插补。然而,这与上述分析冲突,选择回归插补方法可能会导致高度偏倚的结果。另一方面,(缩放后的)能量距离正确地识别出高斯插补是最佳方法,与视觉直觉和更好的参数估计结果一致。
在评估插补方法时(当真实数据可用时),应避免使用 RMSE 和 MAE 等度量方法。相反,应将问题视为一个分布预测问题来评估,使用分布度量方法,如能量距离。RMSE 作为评估工具的过度使用,对该领域的研究有一些严重的影响。
这并不令人惊讶,识别最佳均值预测是 RMSE 的作用。令人惊讶的是,它在研究中被如此一致地用于评估插补方法。以我个人的观点来看,这至少对一些近期论文的建议提出了疑问,关于使用哪些插补方法。此外,随着新插补方法的开发,它们通常会与其他方法在 RMSE 上进行比较,因此可能无法正确地重现分布。因此,我们必须质疑近年来开发的众多插补方法中,至少一些方法的实际价值。
当基础观察数据不可得时,评估问题变得更加困难。在本文中,我们开发了一种评分方法,可以在这种情况下对插补方法进行排名!(这是这篇文章中提出的思路的改进)。详细内容将在另一篇 Medium 文章中说明,但我们可以尝试在这个示例中使用它。Iscore.R 函数可以在Github上找到,或者在本文末尾查看。
library(mice)
source("Iscore.R")
methods<-c("mean", #mice-mean
"norm.predict", #mice-sample
"norm.nob") # Gaussian Imputation
## We first define functions that allow for imputation of the three methods:
imputationfuncs<-list()
imputationfuncs[["mean"]] <- function(X,m){
# 1\. Estimate the mean
meanX<-mean(X[!is.na(X[,1]),1])
## 2\. Impute
meanimp<-X
meanimp[is.na(X[,1]),1] <-meanX
res<-list()
for (l in 1:m){
res[[l]] <- meanimp
}
return(res)
}
imputationfuncs[["norm.predict"]] <- function(X,m){
# 1\. Estimate Regression
lmodelX1X2<-lm(X1~., data=as.data.frame(X[!is.na(X[,1]),]) )
## 2\. Impute
impnormpredict<-X
impnormpredict[is.na(X[,1]),1] <-predict(lmodelX1X2, newdata= as.data.frame(X[is.na(X[,1]),]) )
res<-list()
for (l in 1:m){
res[[l]] <- impnormpredict
}
return(res)
}
imputationfuncs[["norm.nob"]] <- function(X,m){
# 1\. Estimate Regression
lmodelX1X2<-lm(X1~., data=as.data.frame(X[!is.na(X[,1]),]) )
## 2\. Impute
impnorm<-X
meanx<-predict(lmodelX1X2, newdata= as.data.frame(X[is.na(X[,1]),]) )
var <- var(lmodelX1X2$residuals)
res<-list()
for (l in 1:m){
impnorm[is.na(X[,1]),1] <-rnorm(n=length(meanx), mean = meanx, sd=sqrt(var) )
res[[l]] <- impnorm
}
return(res)
}
scoreslist <- Iscores_new(X,imputations=NULL, imputationfuncs=imputationfuncs, N=30)
scores<-do.call(cbind,lapply(scoreslist, function(x) x$score ))
names(scores)<-methods
scores[order(scores)]
# mean norm.predict norm.nob
# -0.7455304 -0.5702136 -0.4220387
因此,即使从未看到缺失数据的值,我们的评分方法也能识别出 norm.nob 是最佳方法!这在数据维度超过两个时尤其有用。我将在下一篇文章中详细说明如何使用该评分方法以及它是如何工作的。
第 3 课:MAR 比你想象的更奇怪
在阅读关于缺失值插补的文献时,人们很容易产生一种感觉:MAR 已经是一个解决过的问题,所有的问题都来自于是否能够假设它成立。虽然在标准程序下,如最大似然估计,这可能是对的,但如果想要找到一个好的(非参数)插补方法,情况并非如此。
我们的论文讨论了在 MAR(缺失完全随机)情况下,如何在从完全观察的模式转变为想要插补的模式时,可能发生复杂的分布变化。在这里,我们将重点讨论观察变量中可能发生的分布变化。为此,我们回到上面的例子,其中我们将X_1视为收入,X_2视为年龄。正如我们在第一幅图中所看到的,分布看起来差异很大。然而,X_1 | X_2的条件分布保持不变!这从理论上使我们能够识别出正确的插补分布。

在X_1已观察的模式下,X_2的分布。底部:在X_1缺失的模式下,(X_1, X_2)的分布。
问题在于,即使我们能够非参数估计X_1缺失模式下的条件分布,我们仍然需要将其外推到X_2缺失模式下的分布。为了说明这一点,我将介绍两种非常重要的非参数 MICE 方法。一种是旧的(mice-cart),一种是新的(mice-DRF)。前者使用一棵树来回归X_j与其他所有变量,然后通过从该树中抽样来进行插补。因此,与 missForest 使用树/森林的条件期望预测不同,它是通过从叶节点抽样来近似从条件分布中抽样。相比之下,mice-DRF 使用分布随机森林,这是一种设计用来估计分布并从这些预测中抽样的森林方法。这两种方法都非常有效,正如我下面所阐述的!
library(drf)
## mice-DRF ##
par(mfrow=c(2,2))
#Fit DRF
DRF <- drf(X=X[!is.na(X[,1]),2, drop=F], Y=X[!is.na(X[,1]),1, drop=F], num.trees=100)
impDRF<-X
# Predict weights for unobserved points
wx<-predict(DRF, newdata= X[is.na(X[,1]),2, drop=F] )$weights
impDRF[is.na(X[,1]),1] <-apply(wx,1,function(wxi) sample(X[!is.na(X[,1]),1, drop=F], size=1, replace=T, prob=wxi))
plot(impDRF[!is.na(X[,1]),c("X2","X1")], main=paste("DRF Imputation", "\nRMSE", RMSEcalc(impDRF, Xstar), "\nEnergy", energycalc(impDRF, Xstar)), cex=0.8, col="darkblue", cex.main=1.5)
points(impDRF[is.na(X[,1]),c("X2","X1")], col="darkred", cex=0.8 )
## mice-cart##
impcart<-X
impcart[is.na(X[,1]),1] <-mice.impute.cart(X[,1], ry=!is.na(X[,1]), X[,2, drop=F], wy = NULL)
plot(impDRF[!is.na(X[,1]),c("X2","X1")], main=paste("cart Imputation", "\nRMSE", RMSEcalc(impcart, Xstar), "\nEnergy", energycalc(impcart, Xstar)), cex=0.8, col="darkblue", cex.main=1.5)
points(impDRF[is.na(X[,1]),c("X2","X1")], col="darkred", cex=0.8 )
plot(impnorm[!is.na(X[,1]),c("X2","X1")], main=paste("Gaussian Imputation","\nRMSE", RMSEcalc(impnorm, Xstar), "\nEnergy", energycalc(impnorm, Xstar)), col="darkblue", cex.main=1.5)
points(impnorm[is.na(X[,1]),c("X2","X1")], col="darkred", cex=0.8 )

尽管 mice-cart 和 mice-DRF 都表现得很好,但它们仍然不如高斯插补效果好。这并不令人惊讶,因为高斯插补在这种情况下是理想的插补方法(因为(X_1, X_2)确实是高斯分布的)。然而,X_2中的分布变化可能是导致 mice-cart 和 mice-DRF 即使对于 3000 个观测值也难以恢复分布的原因(这些方法通常非常非常有效)。请注意,这种外推问题对于高斯插补来说并不存在。
论文还讨论了一个相似的,但更极端的例子,其中包含两个变量(X_1, X_2)。在这个例子中,分布变化更加明显,基于森林的方法因此也表现得较差:

论文中的一个更极端的分布变化示例。虽然高斯插补几乎完美,但 mice-RF 和 mice-DRF 未能正确外推。
问题在于,这类极端分布偏移在 MAR 下是可能的,而基于森林的方法在数据集外推时很困难(神经网络也是如此)。实际上,你能想到一种方法吗,能够(1)非参数化地学习分布,并且(2)从上分布的X_2 推断出从下分布抽取的X_2 吗?目前,我无法想到。
插补是一个复杂的问题,即使可以假设为 MAR,寻找可靠的插补方法仍然没有结束。
结论:我目前的建议
缺失值是一个棘手的问题。事实上,处理缺失值的最佳方法就是根本不出现缺失值。因此,第 3 课表明,即使仅考虑 MAR,插补方法的探索也尚未结束。我们仍然缺乏一种方法,能够(1)进行非参数分布预测,并且(2)适应 MAR 下可能出现的分布偏移。话虽如此,我有时也觉得人们把问题弄得比实际更复杂;一些 MICE 方法表现得非常好,可能已经足够应对许多缺失值问题了。
我首先想提到一些非常先进的机器学习方法,如GAIN及其变种,这些方法尝试通过神经网络来插补数据。我喜欢这些方法,因为它们遵循了正确的思路:插补缺失值的条件分布,给定已观察到的数据。然而,在使用这些方法后,我对它们的表现感到有些失望,尤其是与 MICE 方法相比。
因此,如果我遇到缺失值问题,首先会尝试的是mice-cart(在 mice R 包中实现)或我们在论文中开发的新方法mice-DRF(代码见Github)。我已经在不少实例中尝试了这两种方法,它们重建数据的能力非常出色。然而请注意,我的这些观察并非基于大规模、系统性的基准测试,应该谨慎看待。此外,这至少需要一个中等样本量,比如 200 或 300 以上。插补并不容易,如果样本量太小,完全非参数化的方法会受到影响。如果样本量少于 200 个观测值,我会选择更简单的方法,比如高斯插补(R 包中的mice-norm.nob)。如果你想在这些方法中找出最佳的,我建议尝试我们在论文中开发的评分方法,正如第 2 课中所做的那样(尽管实现可能并非最佳)。
最后,请注意这些方法都无法有效地处理插补不确定性!从某种意义上讲,我们在本文中只讨论了单次插补。(正确的)多次插补需要考虑插补方法本身的不确定性,这通常是通过贝叶斯方法来实现的。对于我们在这里讨论的频率学派方法,这似乎仍然是一个未解决的问题。
附录 1:m-I-Score
文件“Iscore.R”,也可以在Github上找到。
Iscores_new<-function(X, N=50, imputationfuncs=NULL, imputations=NULL, maxlength=NULL,...){
## X: Data with NAs
## N: Number of samples from imputation distribution H
## imputationfuncs: A list of functions, whereby each imputationfuncs[[method]] is a function that takes the arguments
## X,m and imputes X m times using method: imputations= imputationfuncs[[method]](X,m).
## imputations: Either NULL or a list of imputations for the methods considered, each imputed X saved as
## imputations[[method]], whereby method is a string
## maxlength: Maximum number of variables X_j to consider, can speed up the code
require(Matrix)
require(scoringRules)
numberofmissingbyj<-sapply(1:ncol(X), function(j) sum(is.na(X[,j])) )
print("Number of missing values per dimension:")
print(paste0(numberofmissingbyj, collapse=",") )
methods<-names(imputationfuncs)
score_all<-list()
for (method in methods) {
print(paste0("Evaluating method ", method))
# }
if (is.null(imputations)){
# If there is no prior imputation
tmp<-Iscores_new_perimp(X, Ximp=NULL, N=N, imputationfunc=imputationfuncs[[method]], maxlength=maxlength,...)
score_all[[method]] <- tmp
}else{
tmp<-Iscores_new_perimp(X, Ximp=imputations[[method]][[1]], N=N, imputationfunc=imputationfuncs[[method]], maxlength=maxlength, ...)
score_all[[method]] <- tmp
}
}
return(score_all)
}
Iscores_new_perimp <- function(X, Ximp, N=50, imputationfunc, maxlength=NULL,...){
if (is.null(Ximp)){
# Impute, maxit should not be 1 here!
Ximp<-imputationfunc(X=X , m=1)[[1]]
}
colnames(X) <- colnames(Ximp) <- paste0("X", 1:ncol(X))
args<-list(...)
X<-as.matrix(X)
Ximp<-as.matrix(Ximp)
n<-nrow(X)
p<-ncol(X)
##Step 1: Reoder the data according to the number of missing values
## (least missing first)
numberofmissingbyj<-sapply(1:p, function(j) sum(is.na(X[,j])) )
## Done in the function
M<-1*is.na(X)
colnames(M) <- colnames(X)
indexfull<-colnames(X)
# Order first according to most missing values
# Get dimensions with missing values (all other are not important)
dimwithNA<-(colSums(M) > 0)
dimwithNA <- dimwithNA[order(numberofmissingbyj, decreasing=T)]
dimwithNA<-dimwithNA[dimwithNA==TRUE]
if (is.null(maxlength)){maxlength<-sum(dimwithNA) }
if (sum(dimwithNA) < maxlength){
warning("maxlength was set smaller than sum(dimwithNA)")
maxlength<-sum(dimwithNA)
}
index<-1:ncol(X)
scorej<-matrix(NA, nrow= min(sum(dimwithNA), maxlength), ncol=1)
weight<-matrix(NA, nrow= min(sum(dimwithNA), maxlength), ncol=1)
i<-0
for (j in names(dimwithNA)[1:maxlength]){
i<-i+1
print( paste0("Dimension ", i, " out of ", maxlength ) )
# H for all missing values of X_j
Ximp1<-Ximp[M[,j]==1, ]
# H for all observed values of X_j
Ximp0<-Ximp[M[,j]==0, ]
X0 <-X[M[,j]==0, ]
n1<-nrow(Ximp1)
n0<-nrow(Ximp0)
if (n1 < 10){
scorej[i]<-NA
warning('Sample size of missing and nonmissing too small for nonparametric distributional regression, setting to NA')
}else{
# Evaluate on observed data
Xtest <- Ximp0[,!(colnames(Ximp0) %in% j) & (colnames(Ximp0) %in% indexfull), drop=F]
Oj<-apply(X0[,!(colnames(Ximp0) %in% j) & (colnames(Ximp0) %in% indexfull), drop=F],2,function(x) !any(is.na(x)) )
# Only take those that are fully observed
Xtest<-Xtest[,Oj, drop=F]
Ytest <-Ximp0[,j, drop=F]
if (is.null(Xtest)){
scorej[i]<-NA
#weighted
weight[i]<-(n1/n)*(n0/n)
warning("Oj was empty")
next
}
###Test 1:
# Train DRF on imputed data
Xtrain<-Ximp1[,!(colnames(Ximp1) %in% j) & (colnames(Ximp1) %in% indexfull), drop=F]
# Only take those that are fully observed
Xtrain<-Xtrain[,Oj, drop=F]
Ytrain<-Ximp1[,j, drop=F]
Xartificial<-cbind(c(rep(NA,nrow(Ytest)),c(Ytrain)),rbind(Xtest, Xtrain) )
colnames(Xartificial)<-c(colnames(Ytrain), colnames(Xtrain))
Imputationlist<-imputationfunc(X=Xartificial , m=N)
Ymatrix<-do.call(cbind, lapply(Imputationlist, function(x) x[1:nrow(Ytest),1] ))
scorej[i] <- -mean(sapply(1:nrow(Ytest), function(l) { crps_sample(y = Ytest[l,], dat = Ymatrix[l,]) }))
}
#weighted
weight[i]<-(n1/n)*(n0/n)
}
scorelist<-c(scorej)
names(scorelist) <- names(dimwithNA)[1:maxlength]
weightlist<-c(weight)
names(weightlist) <- names(dimwithNA)[1:maxlength]
weightedscore<-scorej*weight/(sum(weight, na.rm=T))
## Weight the score according to n0/n * n1/n!!
return( list(score= sum(weightedscore, na.rm=T), scorelist=scorelist, weightlist=weightlist) )
}
什么是潜在空间?
给普通读者的简明解释
·发表于 Towards Data Science ·7 分钟阅读·2024 年 5 月 8 日
--

图片由 Lennon Cheng 提供,来源于 Unsplash
你有没有想过生成式 AI是如何完成工作的?它是如何创建图像、处理文本并执行其他任务的?
你真正需要理解的关键概念是潜在空间。理解潜在空间是什么,为理解生成式 AI 铺平了道路。
让我通过几个例子来向你解释潜在空间的本质。
示例 1. 寻找更好的方式来表示身高和体重数据。
在我众多的医学数据研究项目中,我收集了大量关于患者体重和身高的测量数据。下面的图显示了这些测量数据的分布情况。

11808 名心脏病患者的身高和体重测量数据。
你可以把每个点看作是关于一个真实人物的信息的压缩版。所有细节,如面部特征、发型、肤色和性别等都不再可用,只留下体重和身高值。
是否可以仅通过这两个值重建原始数据?当然可以,前提是你的期望不要过高。你只需要用一个标准的模板对象来替换所有丢弃的信息,以填补空白。模板对象是根据保留的信息定制的,在这种情况下只包括身高和体重。

[作者的照片,由 [Kamil Winiarz](http://linkedin.com/in/kamilwiniarz) 拍摄]
让我们深入探讨由身高和体重轴定义的空间。考虑一个坐标为身高 170 厘米、体重 70 公斤的点。让这个点作为参考图形,并将其放置在坐标轴的原点。

水平移动保持你的体重恒定,同时改变你的身高。同样,垂直移动保持你的身高不变,但会改变你的体重。
这可能看起来有些棘手,因为当你朝一个方向移动时,你必须同时考虑两件事。有没有什么方法可以改进这个问题?
看看同一数据集按BMI着色的效果。

颜色几乎与线条对齐。这表明我们可以考虑其他可能更方便的轴,以便生成更符合需求的人物。
我们可能会将其中一条轴命名为‘缩放’,因为它保持恒定的BMI,唯一变化的是图形的比例。同样,第二条轴可以标为BMI。

新的轴提供了更便捷的数据视角,使得数据探索变得更加容易。你可以指定一个目标 BMI 值,然后只需沿着‘缩放’轴调整图形的大小。
想为你的图形增加更多细节和真实感吗?可以考虑加入更多特征,比如性别。但从现在开始,由于缺乏维度,我无法提供涵盖所有数据方面的类似可视化。我只能展示三项选定特征的分布:两个特征通过点在坐标轴上的位置来表示,第三个特征则通过颜色来指示。

为了改进之前的人物生成器,你可以为男性和女性创建单独的模板。然后,在黄色主导的区域生成女性,在蓝色占主导的区域生成男性。
随着更多特征的考虑,生成的人物变得越来越逼真。还要注意,即使是数据集中没有的点,也能生成相应的人物。
这就是我所称的自上而下的方法来生成合成的人类图形。它包括选择可衡量的特征,并识别用于探索数据空间的最佳轴(方向)。在机器学习领域,第一个过程称为特征选择,第二个过程则称为特征提取。特征提取可以通过使用专门的算法来进行,例如 PCA¹(主成分分析,Priniciple Component Analysis),它有助于识别那些更自然地表示数据的方向。
我们生成合成对象的数学空间被称为潜在空间,原因有二。首先,这个空间中的点(向量)仅仅是原始对象的压缩、不完美的数值表示,就像阴影一样。其次,定义潜在空间的轴通常与原始测量特征几乎没有相似性。第二个原因将在接下来的示例中得到更好的展示。
示例 2. 人脸的衰老。
当今的生成式 AI 采用自下而上的方法,在这种方法中,特征选择和提取是从原始数据中自动进行的。考虑一个包含面部图像的大型数据集,其中原始特征是每张图像中所有像素的颜色,表示为从 0 到 255 的数字。像 GAN² 这样的生成模型(Generative Adversarial Network)可以识别(学习)一组低维特征,在这些特征中,我们可以找到最感兴趣的方向。
假设你想开发一个应用程序,上传你的图像并展示你更年轻或更年长的版本。为了实现这一目标,你需要根据年龄对所有图像的潜在空间表示(潜在空间向量)进行排序。然后,对于每个年龄组,你需要确定平均向量。
如果一切顺利,平均向量将沿着一条曲线排列,你可以将这条曲线视为年龄值轴的近似。
现在,你可以确定图像的潜在空间表示(编码步骤),然后按照你想要的方式沿着年龄方向移动它。最后,你可以解码它,以生成一个合成图像,展现你更年长(或更年轻)版本的样子。这里的解码步骤的思路类似于我在示例 1 中展示的内容,但在理论和计算上更为先进。
潜在空间允许探索其他有趣的维度,例如发长、笑容、性别等。
示例 3. 根据词语和短语的含义进行排序。
假设你正在进行一项关于自然界和社会中捕食行为的研究,并且你有大量的文本材料需要分析。为了自动筛选相关的文章,你可以将词语和短语编码到潜在空间中。采用自上而下的方法,假设这个潜在空间基于两个维度:捕食性和大小。在实际情况下,你可能需要更多的维度。我这里只取了两个,方便你自己看到潜在空间的样子。
在下面,你可以看到一些词语和短语在引入的潜在空间中的表示(嵌入)。用物理学的类比来理解:你可以把每个词语或短语看作带有两种电荷:捕食性和大小。具有相似电荷的词语/短语会在潜在空间中靠得很近。

每个词语/短语在潜在空间中都有一个数值坐标。

这些向量是潜在空间表示的单词/短语,通常被称为嵌入。嵌入的一个优点是你可以对它们进行代数运算。例如,如果你将代表“羊”和“蜘蛛”的向量相加,你会得到一个接近代表“政治家”的向量。这就证明了下面这个优雅的代数表达式:

你认为这个方程有意义吗?
尝试一下 ChatGPT 使用的潜在空间表示方法。这可能会非常有趣。

结语
潜在空间以一种突出当前任务所需属性的方式表示数据。许多 AI 方法,特别是生成模型和深度神经网络,都在数据的潜在空间表示上进行操作。
一个 AI 模型从数据中学习潜在空间,将原始数据投射到这个空间中(编码步骤),在其中进行操作,最后将结果重构成原始数据格式(解码步骤)。
我的目的是帮助你理解潜在空间的概念。为了更深入地探讨这一主题,我建议你阅读一些更具数学深度的资料。如果你具备良好的数学基础,我推荐你关注Jakub Tomczak 的博客,他在博客中讨论了生成式人工智能领域的热门话题,并且对生成模型进行了详细的讲解。
除非另有说明,所有图片均为作者提供。
参考文献
[1] Deisenroth, Marc Peter, A. Aldo Faisal, Cheng Soon Ong. 机器学习数学. 剑桥大学出版社, 2020。
[2] Jakub M. Tomczak. 深度生成建模. 施普林格出版社, 2022。
什么是因果推断?
初学者的因果推断方法指南:随机对照试验、差分中的差分法、合成控制法和 A/B 测试
·发表于Towards Data Science ·阅读时长:11 分钟·2024 年 7 月 22 日
--

摄影:由Delano Ramdas提供,刊登于Unsplash
本文面向那些希望全面了解因果关系和因果推断方法的初学者,且尽量减少数学内容的讲解。
什么是因果推断?
说到因果关系,我们无法避免这个经典的说法:“相关性不意味着因果关系。” 一个经典的例子是,尽管冰淇淋销售和溺水事件之间存在相关性,但它们并不是彼此因果关系的体现。你可能听过很多类似的例子,用以说明两者之间的区别。虽然这些例子通常是直白的,但在实际分析中,这种区别可能会变得模糊。
如果没有明确理解因果关系是如何衡量的,那么很容易得出错误的因果推断。在这方面,我经常遇到一个问题:“是的,我们知道相关性不意味着因果关系,但回归分析呢?”简短的回答是,线性回归默认情况下并不会提供任何因果性结论,除非我们采取适当的步骤——这就是因果推断方法发挥作用的地方。
什么是决策模型的 Switchback 测试?
决策模型的 A/B 测试
·发表于 Towards Data Science ·阅读时间:6 分钟·2024 年 1 月 26 日
--
决策模型的 Switchback 测试允许算法团队在真实的生产环境中将候选模型与基准模型进行比较,在这个环境中,两个模型都在为操作做出真实世界的决策。通过这种形式的测试,团队可以随机化应用于时间段和/或地点的模型,以减轻可能影响结果的干扰效应(如假期、重大事件等),这些效应在进行前后发布测试时可能会产生影响。
Switchback 测试可以有多个名称(例如,时间分割实验),通常也被称为 A/B 测试。虽然这种比较对理解很有帮助,但需要认识到,Switchback 测试和 A/B 测试是相似的,但并不相同。由于网络效应,决策模型不能像网页测试那样进行 A/B 测试。Switchback 测试可以考虑到这些网络效应,而 A/B 测试则不能。
例如,当你通过向用户展示不同的内容来进行网页的 A/B 测试时,用户在页面 A 上的体验不会影响另一个用户在页面 B 上的体验。然而,如果你尝试对配送司机的配送任务进行 A/B 测试——你根本无法做到。你不能将同一个订单分配给两个不同的司机进行对比测试。使用传统的 A/B 测试,无法在同一时间段或地点内隔离实验组和控制组。这就是 Switchback 测试的作用所在。

用兔子形状表示的模型 A 和 B 的反向测试示意图。图片来源:N. Misek 和 T. Bogich,什么是决策模型的反向测试?(2023),Nextmv。经许可转载。
让我们进一步探索这种测试方法。
反向测试的一个例子是什么?
假设你在一家农场共享公司工作,该公司将来自当地农场的新鲜农产品(胡萝卜、洋葱、甜菜、苹果)和乳制品(奶酪、冰淇淋、牛奶)送到顾客的家中。你的公司最近投资升级了整个车队,使其具备冷链运输能力。由于所有车辆都能够运输温度敏感的物品,因此该公司准备移除与之前混合车队相关的业务逻辑。
在车队升级之前,你的农场共享公司采用后进先出(LIFO)方式处理温度敏感物品。这意味着如果取货的是冰淇淋等冷藏物品,司机必须立即将冰淇淋送到目的地,以避免融化成一团混乱的悲惨场面。这种 LIFO 逻辑有助于产品的完整性和顾客满意度,但也在路线变化和回溯时引入了低效。
在车队升级之后,团队希望移除这一约束,因为所有车辆都能通过冷藏设备更长时间地运输冷藏物品。之前通过历史数据进行的测试,如批量实验(用于将一个或多个模型与离线或历史输入进行对比的临时测试 [1])和验收测试(通过预定义的通过/失败标准,将当前模型与候选模型进行比较,以离线或历史输入为基础,在“接受”新模型之前进行的测试 [2]),显示候选模型相比于具有 LIFO 约束的生产模型,车辆在道路上的时间和未分配停靠点减少。你已经运行了一个影子测试(一种在线测试,多个候选模型与当前生产模型并行运行,但“在阴影中”,不影响决策 [3]),以确保在生产条件下模型的稳定性。现在,你希望让候选模型在生产系统中进行决策,并将其结果与生产模型进行对比。
在这个测试中,你决定在两个城市:丹佛和纽约市基于时间(每 1 小时)进行随机化。以下是其中一个城市的实验单元及其应用的处理方法示例。

Nextmv 上的反向测试样本计划总结。图片来源:Nextmv.io Cloud Console(2024)。经许可转载。
在进行 4 周的测试后,你发现候选模型的表现优于生产模型,因为它在道路上的时间更短、未分配的停靠点更少,而且司机们也更高兴,因为他们不需要为了适应 LIFO 约束在城镇中穿梭。基于这些结果,你和团队一起将新模型(没有 LIFO 约束)完全推广到两个地区。
为什么要进行回滚测试?
回滚测试有助于在存在网络效应的情况下建立对模型变更行为影响的理解和信心。由于它们以统计学上可靠的方式使用在线数据和生产环境,回滚测试能够深入了解新模型的决策如何在现实世界中产生影响,而不仅仅是“推向生产环境”然后期望最好的结果。回滚测试是理解候选模型在现实世界中如何表现的最稳健的测试方式。
这种理解是你无法通过影子测试获得的。例如,如果你运行一个在影子模式下改变目标函数的候选模型,所有的关键绩效指标(KPI)可能看起来都很好。但如果你以回滚测试的方式运行同一个模型,你可能会发现,相比于基线模型,配送司机拒绝订单的比例更高。有些行为和结果,你只有在以生产环境的方式运行候选模型,并观察它如何做出操作决策时,才能预见到。
此外,回滚测试对于路由空间中的供需问题尤其相关,例如最后一公里配送和调度。如前所述,标准的 A/B 测试技术在这些情况下并不适用,因为它们无法考虑网络效应。
你什么时候需要回滚测试?
引用自混沌工程原则:“混沌更倾向于直接在生产流量上进行实验”[4]。回滚测试(和影子测试)就是为了应对这种类型的混沌。如前所述:当你需要观察一个候选模型如何做出影响实际操作的决策时,就是需要回滚测试的时候。
也就是说,第一轮测试不应是回滚测试。你应该先进行一系列历史测试,如批量测试、场景测试和验收测试,然后逐步进行生产数据上的影子测试。回滚测试通常是在决定是否将候选模型完全替代现有生产模型之前的最后一道关卡。

这是一个包含回滚测试的测试工作流程示意图,用于在部署新模型之前进行测试。图片由Haley Eshagh提供。
回滚测试通常是如何进行的?
为了进行回溯测试(switchback tests),团队通常需要从零开始构建基础设施、随机化框架和分析工具。尽管回溯测试的好处显著,但其实现和维护的成本可能较高,通常需要专门的数据科学和数据工程支持。因此,这种测试在决策科学领域并不常见。
一旦基础设施搭建完成,回溯测试上线,接下来就是数据整理的工作,需要将各种信息拼凑在一起,弄清楚何时应用了什么处理,并将所有数据进行对账,以便对结果进行更正式的分析。
一些不错的参考资料包括关于DoorDash 的相关话题博客文章,如这篇(他们写了很多相关内容)[5],以及 Databricks 解决方案工程师撰写的这篇 Towards Data Science 文章 [6],该文提到了一篇来自麻省理工学院和哈佛大学的有用研究论文[7],值得一读。
结论
决策模型的回溯测试类似于 A/B 测试,但允许团队考虑网络效应。回溯测试是 DecisionOps 工作流中的关键环节,因为它使用带有现实世界效应的生产数据运行候选模型。我们正在继续完善Nextmv 的测试体验 — 也希望能听到你的反馈。
如果你对决策模型测试和其他 DecisionOps 主题的更多内容感兴趣,订阅 Nextmv 博客。
披露
该作者在 Nextmv 担任产品负责人。
参考文献
[1] R. Gough, 优化模型的批量实验是什么? (2023),Nextmv
[2] T. Bogich, 接受测试的未来是什么? (2023),Nextmv
[3] T. Bogich, 优化模型和决策算法的影子测试是什么? (2023),Nextmv
[4] 混沌工程原理 (2019),混沌工程原理
[5] C. Sneider, Y. Tang, 回溯实验分析的实验严格性 (2019),DoorDash Engineering
[6] M. Berk, 如何优化你的回溯 A/B 测试配置 (2021),Towards Data Science
[7] I. Bojinov, D. Simchi-Levi, J. Zhao, 回溯实验的设计与分析 (2020),arXiv
最大的数字是什么?
在我们称之为家的抽象世界中,我们需要多少位数?一段非技术性的旅程,追寻所有数字的最长和
·发表于Towards Data Science ·9 分钟阅读·2024 年 1 月 31 日
--

图片由Giordano Rossoni提供,来源
TLDR:最大的数字有 16 位
问问一位天文学家,我们便进入了亿万年的宇宙旅行,超越了宇宙。
问问一个微观物理学家,我们就进入了神秘的夸克和前子,万物的基本构件。
问问我的 5 岁孩子,你会得到 643。
这是一个每个人都能猜的谜题,但当你深入探讨时,它出奇地个人化。
无限大
我把它当作虚数来处理,像无限大一样,视其为一种炼金术,一种符号,而非一个可实际操作的数字。它永远不会出现在卷尺上。
这里是参数:
1. 每个数字都必须有意义:
宇宙从一端到另一端的估计距离是 930 亿光年,这听起来很震撼,但它可能是 930.4 亿或 930.5 亿光年,两个答案都可以接受。要获取最大数字所要求的精细程度,就需要应用才能使它变得有意义。从宇宙一端到另一端的旅行很伟大,但……
“你真的不喜欢这个答案”
Python 中的 LEGB 规则是什么?为什么它很重要?

由作者在 Canva 创建的图片
命名空间和解析顺序有助于提高 Python 编程的性能和健壮性
·发布于 Towards Data Science ·10 分钟阅读·2024 年 4 月 22 日
--
像大多数其他编程语言一样,Python 也有不同的作用域,在这些作用域中变量和函数被定义。当然,存在一些规则和层级,以确保这些作用域之间的行为是确定的。例如,我们无法在外部访问函数内部的变量。
在我看来,Python 明确定义了对象的作用域,并且直观易懂。这使得它具备了易学易用的特点。然而,如果 Python 开发者希望提升技能,并从新手成长为专家,LEGB 规则是不可忽视的。
在本文中,我将介绍 LEGB 规则,它描述了 Python 中不同命名空间的逻辑和行为。之后,还会给出一些技巧和最佳实践。因此,本文不仅仅是解释概念,还会提供基于规则得出的设计含义和技巧。如果你仍然不确定 LEGB 规则,不要错过!
1. 什么是 Python 中的 LEGB 规则?
构建一个伟大图表需要什么
在一个网络化的世界中的知识表示
·发表于Towards Data Science ·阅读时长 7 分钟·2024 年 8 月 22 日
--
无论你是否意识到,你可能已经看过或与图表互动过。我们的世界由各种关系组成。我们认识谁,我们如何互动,我们如何交易——图表以一种使这些固有关系变得显而易见的方式来构建信息。
从分析的角度来看,知识图谱提供了最直观的方式来综合和表示数据集内部以及跨数据集的连接关系,供分析使用。知识图谱是一个技术产物,“以实体及其相互关系的形式将数据以视觉方式呈现”。它为分析师提供了一个数字化的问题模型。它大概是这样的……

图片来源:作者
本文讨论了什么构成一个伟大的图表,并解答了一些关于图表技术实现的常见问题。
图表几乎可以表示任何存在互动或交换的事物。实体(或节点)可以是人、公司、文档、地理位置、银行账户、加密钱包、实物资产等。边(或链接)可以表示对话、电话、电子邮件、学术引用、网络数据包传输、广告展示与转化、金融交易、个人关系等。

图片来源:作者
那么,是什么造就了一个伟大的图表呢?
- 图表的目的很明确。
基于图的解决方案的领域包括分析环境(通常由图数据库提供支持)、图分析技术和图可视化技术。图像和大多数分析工具一样,需要特定的使用案例。图可以用来可视化数据集内外的连接,发现潜在的连接,模拟信息传播或建模传播,建模网络流量或社会行为,识别社交网络中最有影响力的参与者,等等。谁在使用图?这些用户想要在分析和/或可视化方面完成什么?他们是在探索一个组织的数据吗?他们是在回答具体问题吗?他们是在分析、建模、模拟、预测吗?理解图形解决方案需要解决的使用案例是确定图的目的和识别图的领域的第一步。
- 图是特定领域的。
实现基于图形的解决方案时,可能最大的错误就是试图创建一个主图——一个“统治一切的图”。换句话说,就是将所有企业数据放入一个图中。图并不是主数据管理(MDM)解决方案,也不是数据仓库的替代品,即使组织已经有一个可扩展的图数据库。最成功的图通常代表一个特定的分析领域。例如,财务情报图可能包含公司、受益所有权结构、财务交易、金融机构和高净值个人。生活模式定位图可能包含大量信号数据,如 IP 地址和手机数据,以及物理位置、技术资产和个人数据。一旦图的目的和领域明确,架构师就可以着手准备构建图所需的可用数据和/或必需数据。
- 图有清晰的模式。
存储在图数据库中的图将有一个决定其结构的模式。换句话说,模式将指定图中存在的实体类型以及它们之间允许的关系类型。与其他类型的数据库相比,图数据库的一大优势是其模式灵活,可以随着新数据、实体和关系类型的加入而更新。图数据工程师在设计图数据库以表示本体(数据集的概念结构)时,需要做出许多决策,确保模式对于所创建的图来说是合理的。如果组织对数据有充分的理解,通常可以从模式创建开始图架构设计,但如果图的性质和包含的数据集更具探索性,则可能需要首先进行本体设计。
考虑下面图片中的示例架构。这里有五种实体类型:人(黄色)、物理和虚拟位置(蓝色)、文档(灰色)、公司(粉色)和金融账户(绿色)。实体之间允许多种关系类型,例如,“is_related_to”(相关)、“mentions”(提到)和“invests_in”(投资)。这是一个有向图,意味着关系的方向性是有意义的,即,两个人是彼此的婚姻关系(双向链接),而一个人住在某个地方(有向链接)。

图片由作者提供
- 图有明确的机制来连接数据集。
跨数据集的实体之间的连接可能并不总是在数据中显式存在。仅仅将两个数据集导入图环境中,可能会导致许多节点之间没有任何连接。
考虑一个医疗数据集,其中有一个汤姆·马沃洛·里德尔(Tom Marvolo Riddle)条目,以及一个选民注册数据集,其中有一个 T.M.里德尔条目和一个梅罗普·里德尔·高恩(Merope Riddle Gaunt)条目。在医疗数据集中,梅罗普·高恩被列为汤姆·里德尔的母亲。在选民注册数据集中,没有描述家庭成员。如何在合并数据集时消除汤姆·马沃洛·里德尔和 T.M.里德尔条目的重复?即,图中不应有两个独立的节点代表汤姆·里德尔和 T.M.里德尔,因为他们是同一个人。如何将汤姆·里德尔和梅罗普·高恩连接起来,如何像下面的图片那样指定它们的连接关系?例如,连接、相关、母子关系?这种关系是否加权?
这些问题不仅需要数据工程团队来指定图的架构并实现图的设计,还需要某种实体解析过程,我曾经写过相关内容。

图片由作者提供
- 该图被设计为可扩展的。
图数据在图数据存储中是预先连接的,这意味着单跳查询的执行速度比传统数据库要快,例如,查询汤姆·里德尔并查看他所有的直接连接。然 而,在图上的分析操作则相当缓慢,例如,“显示汤姆·里德尔和米勒娃·麦格戈纳戈尔之间的最短路径”,或者“在《哈利·波特与混血王子》中哪个角色具有最高的特征向量中心性”?作为一般规则,图操作中的延迟会随着图密度的增加呈指数增长(图中现有连接与所有可能连接的比率)。大多数图形可视化工具在显示数万节点时会遇到困难。
如果一个组织希望为多个并发分析用户提供可扩展的图解决方案,则需要定制的图数据架构。这包括一个可扩展的图数据库、几个图数据工程过程和一个前端可视化工具。
- 该图有处理时间性的解决方案。
一旦构建了图表解决方案,最大的问题之一就是如何维护它。在图数据库中连接五个数据集并渲染结果图表分析环境只会生成一个时间快照。这些数据集的周期性如何?图表需要多久更新一次,比如每周、每月、每季度或实时?数据是被覆盖还是追加的?被移除的实体会从图表中移除吗,还是被保留?如何提供更新后的数据集,例如增量表,还是再次提供整个数据集?如果数据包含时间元素,它们是如何表示的?
- 基于图表的解决方案由图表数据工程师设计。
图表是美丽的。它们直观、引人注目且高度视觉化。从概念上讲,它们看起来 deceptively 简单。收集一些数据集,指定数据集之间的关系,合并数据,图表就诞生了。分析图表,渲染漂亮的图片。但构建可扩展的图表解决方案所面临的数据工程挑战并非小事。
工具和技术选择、架构设计、图表数据工程、实体解析和数据去重的处理方法,以及根据预期用途进行良好架构设计,都是一些挑战。重要的是,必须有一支真正的图表团队来领导企业级图表解决方案的设计。仅有图表可视化功能并不等于图表解决方案。而一个简单的点选式自服务软件可能适合单个分析师用户,但远远不能满足组织相关的图表分析环境。图表数据工程师、方法论专家和具有图表经验的解决方案架构师是构建高保真图表解决方案所必需的,尤其是在面对上述所有挑战时。
结论
我见过图表改变了许多现实世界的分析组织。无论分析领域如何,分析师的大部分工作都还是手动完成的。市面上有许多技术产品试图自动化分析师的工作流程或创建点选式解决方案。尽管有这些努力,根本问题仍然存在——分析师所需的数据很少能通过一个界面轻松获取,更不用说是互联互通并准备好进行迭代探索。数据通过各种平台、应用程序接口(APIs)和查询工具提供给分析师,而这些都需要不同程度的技术能力才能访问。然后,分析师必须手动整合数据并得出有意义的分析结论。
基于图形的解决方案将分析师的所有相关数据集中在一个地方,并以直观的方式进行呈现。这使得分析师能够根据需要快速点击各个实体和连接,进行分析。我曾亲自帮助团队构建反洗钱解决方案,定位不法分子和非法金融交易,拦截迷失在海上的移民,追踪非法物质的流动,解决非法野生动物贩运问题,并预测迁徙路线,所有这些都采用了基于图形的解决方案。为分析型企业释放图形解决方案的力量,首先需要构建一个出色的图形——这是构建更强大、更具影响力的分析性探索的坚实基础。
什么造就了一个强大的人工智能?

图片由Emmanuel Ikwuegbu提供,来源于Unsplash
《为什么之书》第九章和第十章,与我一起阅读系列
·发表于Towards Data Science ·阅读时长 13 分钟·2024 年 1 月 4 日
--
我们正在阅读最后一篇文章,书中探讨了 Judea Pearl 启发性的《为什么之书》(The Book of Why),主要聚焦于中介变量以及如何将因果关系与大数据结合以得出可靠的结论。我还将根据整本书的内容总结什么造就了一个强大的人工智能。
中介变量
在之前的文章中,我们简要讨论了中介效应。我们提到,识别中介变量是非常重要的,这不仅是因为我们不应该将其视为混杂因子,因为对它们进行条件化会完全或部分阻断因果效应,而且在适当的情况下,当存在未观察到的混杂因子时,我们可以利用中介变量来应用前门准则。

图片由sally crierie提供,来源于Unsplash
在本节中,我们将深入探讨不同的中介变量实例,并理解在中介变量存在的情况下,如何正确定义直接因果效应、间接因果效应和总因果效应。
在历史上,坏血病一直是水手们最恐惧的疾病。在维生素被发现并被正确命名之前,科学家们一直在努力弄清楚如何防止水手们患上这种疾病……
什么才是一个真正的 AI 代理?重新思考自主性的追求
解构 AI 代理的六大核心特征,以及为什么基础更重要而非时髦词汇
·发表于 Towards Data Science ·11 分钟阅读·2024 年 10 月 2 日
--

图片由作者使用 Midjourney 制作
科技界痴迷于 AI 代理。从销售代理到自主系统,像Salesforce和Hubspot这样的公司声称提供改变游戏规则的 AI 代理。然而,我至今没有看到一个由大规模语言模型(LLM)构建的真正有代理特征的令人信服的体验。市场上充斥着机器人废话,如果 Salesforce 能做的只是说他们的新代理比出版公司之前的聊天机器人表现更好,那也实在是令人失望。
这是一个没人问过的最重要的问题:即便我们能够构建完全自主的 AI 代理,它们在多大程度上能成为用户最需要的东西?
让我们通过代理和助手的视角,使用旅行规划的案例。这一具体案例有助于阐明代理行为的每个组成部分带来的影响,以及你如何提出正确的问题来分辨炒作与现实的区别。希望到最后,你能自己决定,真正的 AI 自主性是否是值得投资的战略选择,还是这十年最具成本的分心。
目录
-
代理行为的谱系:一个实用框架
-
代理行为的构建模块 — 感知 — 互动性 — 持续性 — 反应性 — 主动性 — 自主性
-
未来前沿:主动自治
-
代理与助手
-
闪光之前的基础:数据在 AI 感知中的关键作用
-
从问题开始:为什么以用户为中心的 AI 更胜一筹
-
前进的道路:对齐数据、系统与用户需求
代理行为的光谱:一个实用框架
在学术界和工业界,对于什么构成真正的“代理”没有共识。我建议企业采用一个光谱框架,从AI 学术文献中借用六个属性。基于“代理”或“非代理”的二元分类在当前的 AI 领域常常没有帮助,原因有以下几点:
-
它没有捕捉到不同系统的细微能力差异。
-
它可能导致不切实际的期望或低估系统的潜力。
-
它与 AI 在实际应用中的渐进式发展不符。
通过采用基于光谱的方法,企业能够更好地理解、评估和传达 AI 系统不断发展的能力和需求。这种方法对任何参与 AI 集成、功能开发和战略决策的人都尤为宝贵。
通过旅行“代理”的例子,我们将看到现实世界中的实现如何在不同属性的代理行为光谱上有所体现。大多数现实世界的应用将位于每个属性的“基本”和“高级”之间。这种理解将帮助你在 AI 集成项目中做出更明智的决策,并与技术团队和最终用户进行更有效的沟通。到最后,你将能够:
-
当有人声称他们构建了一个“AI 代理”时,识别出其中的虚假。
-
理解在开发 AI 系统时什么才是真正重要的。
-
在不被炒作迷惑的情况下指导你组织的 AI 战略。
代理行为的构建块
1. 感知
感知和解释其环境或相关数据流的能力。
基本:理解关于旅行偏好的文本输入并访问基本的旅行数据库。
高级:集成并解释多种数据流,包括过去的旅行历史、实时航班数据、天气预报、当地活动安排、社交媒体趋势和全球新闻动态。
一个具有先进感知能力的代理可能会识别你过去旅行决策中的模式,例如偏好不需要驾车的目的地。这些洞察可以用来指导未来的建议。
2. 互动性
与其操作环境有效互动的能力,包括用户、其他 AI 系统以及外部数据源或服务。
基本:以问答形式参与旅行选项的交流,理解并回应用户查询。
高级:保持对话界面,要求澄清,提供建议的解释,并根据用户的偏好和上下文调整沟通风格。
像 ChatGPT、Claude 和 Gemini 这样的 LLM 聊天机器人设定了高度的互动标准。你可能已经注意到,大多数客户支持聊天机器人在这一方面有所不足。这是因为客户服务聊天机器人需要提供准确的公司特定信息,并且通常需要与复杂的后台系统进行集成。它们无法像 ChatGPT 那样在创造性和泛化方面进行过多的发挥,因为 ChatGPT 更注重互动回应而非准确性。
3. 持久性
能够创建、维护和更新有关用户及其关键互动的长期记忆。
基础:保存基本的用户偏好,并能够在未来的会话中调用它们。
高级:随着时间推移,建立用户的旅行习惯和偏好综合档案,不断完善对其的理解。
AI 中的真正持久性需要同时具备读取和写入用户数据的能力。这意味着在每次互动后写入新的见解,并从这个扩展的知识库中读取信息以指导未来的行动。想想看,一个优秀的人类旅行代理会记得你喜欢靠过道的座位,或你倾向于将商务旅行延长为小型度假。一个具有强大持久性的 AI 会做同样的事情,不断构建并参考对你的理解。
ChatGPT 引入了选择性持久性元素,但大多数对话实际上是在“空白”状态下进行的。要实现真正的持久系统,你需要建立自己的长期记忆,包括每次提示的相关上下文。
4. 反应性
能够及时响应环境变化或接收数据的能力。做到这一点很好,严重依赖于强大的感知能力。
基础:当用户手动输入新的货币汇率时,更新旅行费用估算。
高级:持续监控和分析多个数据流,以主动调整旅行行程和费用估算。
最好的 AI 旅行助手会注意到因重大事件而导致的目的地酒店价格突然上涨。它可以主动建议替代的日期或附近的地点来帮你节省费用。
一个真正的响应系统需要大量的实时数据流,以确保强大的感知能力。例如,我们的高级旅行助手能够根据政治暴动重新规划行程,这不仅仅是快速反应的问题。它还需要:
-
获取实时新闻和政府咨询信息流(感知)
-
理解这些信息对旅行的影响(解释)
-
能够迅速根据这些信息调整建议计划的能力(反应)
感知与反应之间的这种相互联系突显了为何开发真正的反应性 AI 系统是复杂且资源密集型的。这不仅仅是关于快速响应,更是关于创建一种全面的环境意识,从而能够做出有意义且及时的反应。
5. 前瞻性
预测需求或潜在问题的能力,并在没有明确提示的情况下提供相关建议或信息,同时仍然将最终决策交给用户。
基础:建议在选定目的地的受欢迎景点。
高级:预测潜在需求并提供未经请求但相关的建议。
一个真正前瞻性的系统会提前标出护照即将过期的日期,建议由于预期的道路封闭而选择地铁而不是汽车,或者在受欢迎的餐厅一有空位立即建议设定日历提醒来进行预定。
真正的前瞻性要求系统具备全面的持久性、感知力,同时还要具备反应能力,才能做出相关、及时且具有情境感知的建议。
6. 自主性
在定义的参数范围内独立操作并做出决策的能力。
自主性水平可以通过以下几个方面来表征:
-
资源控制:AI 可以分配或管理的资源的价值和重要性。
-
影响范围:AI 决策对整体系统或组织的广度和重要性。
-
操作边界:AI 在没有人工干预的情况下做出决策的范围。
基础:对低价值资源的控制有限,做出的决策对系统的整体影响较小,并且操作在狭窄、预定义的边界内。例子:一个智能灌溉系统,根据土壤湿度和天气预报决定何时给花园中的不同区域浇水。
中等:控制中等价值的资源,做出的决策对系统部分产生显著影响,并在预定义的操作边界内具有一定灵活性。例子:一个零售连锁的 AI 驱动库存管理系统,决定库存水平和在多个门店之间的分配。
高级:控制高价值或关键资源,做出的决策对系统有显著的影响,并且操作具有广泛的操作边界。例子:一个科技公司的 AI 系统,优化整个 AI 流水线,包括模型评估和价值 1 亿美元的 GPU 的分配。
最先进的系统将对“什么”(例如:在何处部署哪些模型)和“如何”(资源分配、质量检查)做出重要决策,在做出正确的权衡以实现既定目标的同时,确保效果。
需要注意的是,“什么”和“如何”决策之间的区别可能会变得模糊,尤其是在任务范围扩大时。例如,选择部署一个需要大量资源的大型模型,涉及到两者的决策。复杂性范围中的关键区分点是代理被赋予管理的资源和风险的自主性水平。
这种框架有助于更细致地理解 AI 系统中的自主性。真正的自主性不仅仅是独立操作——它还关乎决策的范围和影响。错误的风险越大,就越需要确保采取适当的保障措施。查看我的文章什么是“Eval”以及为什么产品经理需要关注它,了解如何为 AI 产品构建有效的测试流程。
未来的前沿:主动自主性
不仅能够在定义的参数内做出决策,还能在认为必要时主动修改这些参数或目标,以更好地实现整体目标。
虽然它为真正适应性和创新的 AI 系统提供了潜力,但也带来了更大的复杂性和风险。目前,这种自主性水平在很大程度上是理论性的,并且引发了重要的伦理考量。
不出所料,大多数科幻作品中的坏 AI 都是已经突破了主动自主性界限的系统或代理,包括复仇者联盟中的乌尔特隆、《黑客帝国》中的机器、《2001 太空漫游》中的 HAL 9000,以及《机器人总动员》中的 AUTO 等。
主动自主性仍然是 AI 发展的前沿,承诺带来巨大的好处,但也需要深思熟虑、负责任的实施。事实上,大多数公司需要多年的基础性工作才能使其变得可行——你可以把关于机器人霸主的猜测留到周末去谈。
代理与助手
在考虑这六个属性时,我想提出一个有用的区分,称之为‘AI 助手’和‘AI 代理’。
一个 AI 代理:
-
展示至少五个六个属性(可能不包括主动性)
-
在其定义的领域内展现出显著的自主性,决定执行哪些行动以完成任务,而无需人类监督
一个 AI 助手
-
在感知、交互性和持久性方面表现出色
-
可能具备也可能不具备一定程度的反应性
-
缺乏或没有自主性或主动性
-
主要响应人类请求,并需要人类批准才能执行操作
尽管行业尚未就官方定义达成共识,但这种框架可以帮助你思考这些系统的实际影响。无论是代理还是助手,都需要具备感知、基本交互性和持久性等基础,以便发挥作用。

作者使用 Midjourney 创建的图像
按照这个定义,Roomba 吸尘器更接近一个真正的智能体,尽管它是一个基础版本。它不是主动的,但它确实在一个定义好的空间内行使自主性,自己制定路线,反应障碍物和脏污程度,并在没有持续人类输入的情况下自行返回充电座。
GitHub Copilot 是一个非常有能力的助手。它通过提供上下文相关的代码建议、解释复杂的代码片段,甚至根据注释草拟整个函数,极大地增强了开发者的能力。然而,它仍然依赖开发者来决定在哪里寻求帮助,并且最终的代码实现、架构和功能决策仍由人类做出。
代码编辑器Cursor正在通过其主动标记潜在问题的实时方法,逐步进入智能体领域。Cursor 今天能够根据你的描述创建整个应用程序,也更接近一个真正的智能体。
虽然这个框架有助于区分真正的智能体和助手,但现实世界的情况要复杂得多。许多公司急于将他们的 AI 产品标榜为“智能体”,但它们是否关注了正确的优先事项?理解为什么如此多的企业未能准确把握重点,以及为什么优先考虑那些不那么引人注目的基础工作是至关重要的。
基础先于闪光:数据在 AI 感知中的关键角色
像 Cursor 这样的开发者工具通过推动智能行为取得了巨大成功,但如今大多数公司却没能获得令人满意的结果。
编程任务有一个明确的问题空间,并且有清晰的成功标准(代码完成、通过测试)来进行评估。还有大量的高质量训练和评估数据以开源代码库的形式随时可用。
大多数试图引入自动化的公司,根本没有接近建立在正确数据基础上的条件。领导层往往低估了客户支持代理或客户经理所做的许多工作,依赖于未成文的信息。例如,如何处理错误消息,或者新库存何时可能到货等问题。这些都属于此类信息。正确评估一个用户可以询问任何问题的聊天机器人可能需要几个月时间。缺乏感知基础和测试捷径是导致垃圾机器人的主要原因之一。
从问题开始:为什么以用户为中心的 AI 更胜一筹
在将资源投入到智能体或助手之前,公司应该问自己:用户真正需要什么,他们的知识管理系统今天能够支持什么。大多数公司还没有准备好支持智能体的功能,许多公司还需要在感知和持久性方面做大量工作,才能为有用的助手提供动力。
一些最近的半成品人工智能功能回撤的例子包括Meta 的明星聊天机器人,没人愿意和它们聊天和LinkedIn 最近的失败实验,该实验涉及基于 AI 的内容建议。

LinkedIn 的 AI 辅助提示:就像你那个过于热情的实习生,想要贡献,但根本不知道会议的内容是什么,或者你到底在哪个行业工作。[图片:LinkedIn]
Waymo 和 Roomba 通过使用人工智能简化现有活动,解决了真实的用户问题。然而,它们的开发并非一蹴而就——两者都需要超过十年的研发才能推向市场。今天的技术已经进步,这可能使得像营销和销售这样的低风险领域能够更快地实现自主性。然而,创建卓越质量的人工智能系统仍然需要大量的时间和资源。
前进的路径:对齐数据、系统和用户需求
最终,人工智能系统的价值不在于它是否是一个“真正的”代理,而在于它能多有效地为用户或客户解决问题。
在决定在哪里投资人工智能时:
-
定义你想要解决的具体用户问题。
-
确定提供价值所需的最小代理行为支柱(感知、互动、持久性等)和每个支柱的复杂程度。
-
评估你今天所拥有的数据,以及这些数据是否可以提供给正确的系统。
-
现实地评估完成从当前状态到实现目标所需的能力之间的差距所需的工作量。
通过清楚了解现有数据、系统和用户需求,你可以专注于提供即时价值的解决方案。完全自主的人工智能代理具有强大的吸引力,但不要被炒作所迷惑。通过专注于正确的基础支柱,如感知和持久性,即使是有限的系统也能在效率和用户满意度上带来有意义的改进。
最终,尽管 HubSpot 和 Salesforce 可能都没有提供完全代理的解决方案,但任何在感知和持久性等基础设施上的投资仍然能够解决即时的用户问题。
记住,没有人会对洗衣机的“自主性”感到惊讶,但它可靠地解决了问题并改善了日常生活。优先考虑解决实际问题的人工智能功能,即使它们不是完全自主或具备代理能力的,也会提供即时价值,并为未来更复杂的能力奠定基础。
通过利用你的优势、弥补差距并将解决方案与真实用户问题对接,你将能为创建具有重要意义的人工智能系统做好准备——无论它们是代理、助手,还是不可或缺的工具。
接下来做什么?探索图神经网络推荐引擎
选择接下来要看的内容实在太难了。让我们构建一个 AI 算法来为我们做这件事吧!
·发表于Towards Data Science ·阅读时间约 11 分钟·2024 年 1 月 4 日
--

图片由Dex Ezekiel提供,来源于Unsplash
内容推荐是机器学习和人工智能的常见应用。本文将回顾一种基于图神经网络的内容推荐方法,采用链接回归。其目标是构建一个内容推荐引擎,根据用户对已观看内容的评分,预测用户对未观看内容的评分。
我们将从图神经网络(GNN)的概念回顾开始,并介绍PyTorch Geometric (PyG),这是一个 Python 中的 GNN 包。然后,我们将这些概念应用到我们自己的推荐引擎中,获取 AI 驱动的内容推荐。
感谢你花时间阅读,像往常一样,代码和有用链接将在文章末尾的参考资料中提供。
概念回顾
让我们从概念回顾开始;在我们使用图神经网络之前,理解其所有的关键部分是非常重要的。在开始理解 GNN 之前,最重要的是先了解图!所以让我们回顾一下在这个语境中我们所说的图的含义。
图数据结构
当我在图神经网络(GNN)的上下文中谈论图时,我指的是图数据结构。这与用于可视化数据的图表或图形不同,虽然有时你可能会从“图”这个词中期望看到的是图表或图形。
在社交网络的背景下考虑图数据结构可能会更有用。这类似于社交媒体网站如何存储数据。想象一下你的 Facebook 个人资料。你可能与兄弟和高中时的老朋友有联系,那么图数据结构就像这样:

来源:图片由作者提供
每个人都是一个节点,这只是意味着我们表示中的一个点。我们的节点具有诸如它们代表的人的名字这样的特征。每个连接都是一条边,它们代表两个节点之间的联系(友谊)。
现在,我们扩展这个网络也不难。你的兄弟可能与他的老板有联系。现在我们引入了一个重要的概念和想法。这就是分离度的概念。我们可以(相对安全地)假设你的观点和偏好与你的兄弟比与你的兄弟的老板更为接近。这是因为你与你的兄弟是直接连接的,而你必须通过跳跃才能到达他的老板:

来源:图片由作者提供
现在,让我们重新回顾一下高中时的老朋友。我们已经知道我们的节点可以有像名字和年龄这样的特征。现在,让我们在此基础上,为我们的边添加特征!假设你给朋友发消息的次数为边的特征。我们将其称为边的权重。在我们的例子中,这看起来可能像这样,其中边的权重通过边的粗细来表示;发的消息越多,线条越粗:

来源:图片由作者提供
现在我们知道了边的权重,我们也可以在构建我们对偏好和观点的理解时使用这一点。你经常联系的人,更有可能喜欢和你一样的东西;这就是你们成为朋友的原因!因此,从这个例子中,我们不仅可以说你的兄弟比与你有更远联系的人更像你,还可以说你们之间有更多的共同点,而不是那些与你直接连接的人。
我们可以添加的最后一个复杂性是使我们的图变得异构。这只是一个华丽的说法,意味着有两种类型的节点。让我们将这个例子应用到我们的项目中,假设我们有“人”和“动漫”:

来源:图片由作者提供
这个最终的图形非常接近本项目中使用的实际数据。我们可以看到你和你的兄弟都喜欢《宝可梦》。你的兄弟也非常喜欢《进击的巨人》。因此,在这个简单的图形中,我们已经可以看到一个算法如何向你推荐《进击的巨人》!
随着我们展开这个思路,我们可以开始看到如何利用庞大的数据图来为数百万用户做出判断和建议,因此,Netflix 知道你想看《办公室》第千次也就不足为奇了。不过没关系……我们都一样!现在让我们来看看这些图数据集是如何被神经网络使用的。
图神经网络
图神经网络(GNNs)是神经网络的扩展,它们使用图数据结构或几何数据,而不是传统神经网络使用的典型表格数据结构。这意味着从基础上讲,它们非常相似,都是由输入层、隐藏层和输出层组成。两者还共享多种类型的层,包括本文探讨的卷积层和线性层。主要的区别在于数据在训练过程之前和过程中如何被处理。
在这个项目中,使用了 Graph SAGE,这是神经网络中的一层。Graph SAGE 在其他可能的选项中负责信息传递。这也有时被称为编码。信息传递过程就是基于节点和边的邻居来构建对每个节点和边的理解:

来源:作者提供的图片
每个节点将信息传递给其邻居节点。这使得网络能够理解观看该动漫的用户类型,并且还能够理解这个用户喜欢什么类型的动漫!关于这一点,应用部分有一个特别相关的说明。
SAGE 层是一种卷积层。我们可以将其与用于图像识别的卷积神经网络中的卷积层进行比较。与基于相邻像素构建图像理解不同,这里我们是基于相邻节点来进行理解!为了更好地理解这一过程,本文最后推荐了一个精彩的 YouTube 系列。
我们网络的最后一部分仅仅是使用线性层来预测评分!线性层的工作方式与其他神经网络非常相似。它通过回归来预测评分,回归是基于网络所给定的特征进行的。我们可以在网络的末尾使用多个线性层,输出一组代表我们预测的神经元。这就完成了概念回顾!
使用 PyTorch Geometric 的 GNN 应用
该项目的实际应用跟随 PyTorch Geometric 示例进行链接回归,并将其应用于动漫数据集。文章末尾提供了 PyTorch Geometric 代码的链接。

图片由Akin Cakiner提供,来源于Unsplash
现在让我们停止闲坐,开始构建吧!
项目所使用的数据来自 Kaggle,基于用户对各种动漫的评分。数据集采样了 6,000 个独特的用户及其对不同动漫的评分。由于训练资源有限,数据集已进行采样,这可能会影响模型的准确性。未来的工作可能会增加更多的资源来训练该模型。
以下是一些可视化图示,帮助快速理解数据集以及可能影响用户评分的某些动漫特征:


来源:图片由作者提供

来源:图片由作者提供
散点图提供了一种对比感,展示了我们处理的内容数量有多庞大!
特征工程
上面提到的几个似乎最能影响平均评分的特征是动漫的类型(电影、系列等)和类型(动作、喜剧等)。这些特征将作为网络中的节点特征。以下给出了这一步骤的代码。特征被转换为张量,然后沿相同的轴进行拼接,以便每个节点都有与其类型和类型相关的特征:
这些节点特征将很有用,但我们还可以提取一个额外的特征——动漫的标题。为此,使用了一个句子变换器来生成标题的嵌入。这个嵌入也作为一个特征使用:
基于动漫标题的嵌入可能是一个有用的特征,因为网络可能会识别出用户根据标题对内容进行相似评分。例如,喜欢《宝可梦:X 和 Y》的用户可能也会喜欢《宝可梦:钻石与珍珠》。这两者的嵌入也将会是相似的。
构建图数据集
上述选择的特征将使网络能够学习用户如何与不同类型、种类和标题的动漫互动。网络可以利用这些特征做出更好的预测。虽然也可以使用用户特征,但为了简便起见,我们仅使用动漫特征。值得注意的是,用户和动漫都将作为节点出现在我们的图数据集中。构建该对象的代码相对复杂;第一步是构建一个用户和动漫的 ID 序列,并将这些 ID 映射回原始数据。下面的代码展示了下一步,即使用这些映射来构建 PyTorch 数据对象:
注意:在构建我们的图数据集时,我们将其设置为无向图。所以每个用户给动漫打上评分标签,但每个动漫也会用相同的标签给用户打分。这样做是为了让消息传递步骤能够使每个节点了解自己以及它与其他节点的连接。这种做法并不适用于所有应用场景,但在这里,它使得图能够更好地理解观看该动漫的用户类型以及这些用户喜欢的动漫类型。
构建图神经网络
对于熟悉 PyTorch 的人来说,几何等效物看起来会非常相似。这个应用程序使用了三种不同的对象:
-
GNN 编码器 — 使用 SageConv 层通过消息传递来编码图形特征。
-
边解码器 — 使用线性层解码边缘并预测评分。
-
模型类 — 结合了上述两个对象,并定义了编码/解码的整体训练过程。
每个编码器和解码器对象仅实现了两个函数:
-
一个初始化函数 — 定义了层的结构。
-
一个前向函数 — 定义了输入到各层之间的转换。
我们网络的图示可能长这样:

来源:图片由作者提供
这是一个很好的机会来回顾为什么 GNN 对图形数据结构如此有效:
-
输入层(Sage CONV)没有固定的输入大小,因此可以使用任何大小的图来构建该嵌入。
-
一旦通过消息传递层(Graph SAGE One 和 Two)构建了图的嵌入,便可以使用更传统的层,如线性层。
在代码中,这是:
最后的步骤是构建训练过程。在这个示例中,我选择使用 RMSE 作为损失的衡量标准。这段代码对于在 PyTorch 中训练神经网络来说是相当标准的,因此如果你对更详细的文章感兴趣,可以查看文档或留言。
一旦训练过程定义完毕,我们就开始训练模型。
评估模型
在评估模型之前,重要的是要强调数据集的一个缺点。也就是,所提供的评分实际上是绝对值的序数值,即 0、1、2 等,因此严格来说,回归分析并不是最合适的决策。在实际场景中,这可能是一个问题,但对于这个项目,我们将坚持使用回归方法,并在最后对值进行四舍五入。
在评估之前,重要的是我们使用未见过的测试数据来对模型做出最终判断。在这种情况下,确保我们的预测被限定在 0 到 10 之间是很重要的,因为这是用户评分动漫的有效范围:
在评估模型时,我们使用了预测的均方根误差(RMSE)。在评估模型时,我们可以看到整体的 RMSE(在未见过的测试数据上)为 1.23。
我们可以通过下面的图表回顾每个实际评分在不同情况下的表现:

来源:图片由作者提供
很少有动漫评分低于五分,因此由于训练样本较少,模型在预测低评分时遇到困难。尽管如此,高评分通常能被正确预测。特别是在目标评分为 8 附近时,模型似乎能预测出最准确的评分。
在已部署的产品中,模型会进行估算,然后将这些原始分数返回给用户,或者在某些情况下,我们可以看到设置了阈值。例如,如果模型预测你会对内容打 7 分或更高,那么算法会推荐你观看该动漫。
对于像这样的项目,还有一些未来的推荐。例如,探索使用链接预测代替链接回归,甚至是链接分类。还有一种建议是可以使用更多的训练数据、超参数调优或其他增强学习方法,但这些超出了我的资源限制!在此之前,我有许多优秀的新内容推荐可以继续进行,感谢我的图神经网络!
这结束了作为推荐引擎的图神经网络探索。在这个项目中,我学到了很多关于图数据结构和 GNN 的知识。还有很多需要学习的内容,请继续关注未来的文章,在那里我将使用此代码库和 Captum Python 包探索可解释的 AI。
参考文献
代码:
[## GitHub - josephlewisjgl/GNNRecommender: 来自 PyTorch Geometric 的自举教程。使用动漫…
来自 PyTorch Geometric 的自举教程。使用动漫数据集构建推荐系统。 - GitHub …
github.com](https://github.com/josephlewisjgl/GNNRecommender?source=post_page-----67d6a39d8817--------------------------------)
数据集(公共领域许可,CC0 creativecommons.org/publicdomain/zero/1.0/):
来自 myanimelist.net 的 76,000 用户的推荐数据
关于 GNN 的 YouTube 讲解系列:www.youtube.com/watch?v=fOctJB4kVlM
PyTorch Geometric 文档:
[## PyG 文档 - pytorch_geometric 文档
它包含了多种用于图形和其他不规则结构的深度学习方法,也被称为几何深度学习…
pytorch-geometric.readthedocs.io](https://pytorch-geometric.readthedocs.io/en/latest/index.html?source=post_page-----67d6a39d8817--------------------------------)
PyTorch Geometric 链接回归示例:
编辑描述
colab.research.google.com](https://colab.research.google.com/drive/1N3LvAO0AXV4kBPbTMX866OwJM9YS6Ji2?usp=sharing&source=post_page-----67d6a39d8817--------------------------------)
来自Sahil Sheikh的 Sage Conv Medium 文章:
图神经网络(GNNs)是一类深度学习模型,旨在处理图结构数据…
关于 RAG 的那些事,没有人告诉你
深入探讨为什么 RAG 并不总是按预期工作:概述其背后的商业价值、数据和技术。
·发表于 Towards Data Science ·阅读时间 11 分钟·2024 年 8 月 23 日
--

图片来源:Lisa Boonaerts 来自 Unsplash
构建一个 RAG(Retrieval Augmented Generation,检索增强生成)系统来“与数据对话”其实很简单:安装一个流行的 LLM 编排工具,如 LangChain 或 LlamaIndex,将数据转化为向量,在向量数据库中进行索引,并快速设置一个默认提示的管道。
几行代码,就可以收工了。
或者你可能这么认为。
现实比这复杂得多。为了展示效果而简化的原生 RAG 实现,虽然适用于 5 分钟的演示,但在实际业务场景中并不好用。
别误会,那些简短粗糙的演示非常适合理解基础概念。但在实际操作中,要使一个 RAG 系统具备生产能力,不仅仅是将一些代码拼接在一起。这还涉及到应对凌乱的数据、意外的用户查询,以及不断面临的提供实际商业价值的压力。
在这篇文章中,我们首先将探讨 决定一个基于 RAG 项目成败的商业关键因素。接下来,我们将深入讨论常见的技术难题——从* 数据处理 到 性能优化 ——并讨论 应对策略 *……
卓越的数据分析师与众不同的地方
还在寻找新年目标吗?这里有 6 项技能,帮助你和你的团队变得极其高效。
·发表于Towards Data Science ·5 分钟阅读·2024 年 1 月 13 日
--
什么造就了优秀的数据分析师?优秀的数据分析师能够为复杂的问题找到创造性的解决方案,并在创纪录的时间内产出高质量的工作。
他们准确知道该问哪些问题,才能形成一个稳固的问题陈述;从那里,他们知道该遵循哪些流程,写哪些查询,使用哪些数据集,并如何将见解呈现得尽可能易于理解。
他们让这一切看起来如此轻松……但他们的秘密是什么?
简而言之——他们已经培养了正确的技能组合。他们通过刻苦训练发展了正确的能力,使自己成为一个包含多种能力的综合体。让我们深入了解他们的“健身”计划——剧透一下,他们没有跳过“统计”那一天。

2024 年成为 10 倍分析师的检查清单——图像来自作者
顶级分析师都能流利使用 SQL
SQL 是数据分析的语言。能够流利使用它是深入挖掘并得出更深层见解的关键。而流利并非指精通——我真正指的是流利,也就是说,在编写一个包含多个 CTE、使用数组和窗口函数的 100 行脚本时,不会犹豫。
SQL 的流利度缺失会极大限制一个分析师。要么他们依赖他人来获取数据——这大大限制了他们的执行速度——要么如果他们只依赖自己的技能,他们就被迫停留在“表面”层次的见解,可能会错过下面更深、更有价值的真相。
对于分析师来说,要实现流利的技能,没有什么秘诀:
-
定期练习:每天/每周进行具有挑战性的练习,参与复杂的项目,将自己推向舒适区之外
-
向他人学习:审查更有经验的同事的代码,参与内部/外部在线论坛,和/或参加结构化课程
他们具备统计学基础
统计学对很多人来说是可怕的,而且有充分的理由——它很容易变得非常复杂。同时,掌握几个关键概念可以带来巨大的价值,并允许找到创造性的方法来回答那些不那么简单的问题。
我与之共事过的大多数优秀分析师都有以下特质:
-
对描述性统计有扎实的掌握。可以说,这是任何描述性或探索性分析的关键,并为更复杂的分析奠定基础。
-
对总体和样本之间的差异有良好的理解,知道这与统计测试的关系,以及如何进行一些常见的统计检验
-
奖励点:对机器学习有一个粗略的了解:一些关键原则是什么,如何评估模型的表现,等等。
他们有深厚的领域知识
在处理数据时,很容易觉得自己“理解”了这个过程。你知道这些数字,知道这些趋势。但没有领域知识,即没有定性的一面,很容易错过一些关键的见解。因为归根结底,一个数据集只是一个简化,它提供了一个有限且简化的视角来看待某一现象。领域知识则提供了理解数据集本身无法呈现的内容所需的额外背景。
尽管通过在同一公司/行业工作多年就能获得领域知识,但通过有意识地进行学习也可以更快地提高。优秀的分析师通常会混合做这三项活动:
-
他们跟随同事工作:他们与跨职能伙伴建立友谊,并积极尝试理解他们的日常工作
-
他们定期与主题专家讨论他们的定量发现,以便结合定性见解并验证数据解释。
-
他们阅读行业报告,关注“LinkedIn 影响者”,参加行业特定的活动、讨论等。
他们了解公司内部的数据基础设施
通常会花费大量时间寻找适合特定项目的数据源(或逻辑)。优秀分析师的效率很高的原因之一是他们已经积累了大量关于不同数据源的知识,包括它们的特性和怪癖。他们直接知道在哪里找到项目所需的信息,以及使用哪种实际的逻辑——因为他们知道数据如何被转换,以及存储在哪里。为了实现这一点:
-
他们对数据流转过程充满好奇:他们追溯数据是如何从原始数据进入他们最喜欢的数据集的,并清晰地了解数据的生命周期及可能的质量退化或提升点。
-
他们与数据工程师合作:他们定期与数据工程师讨论;每当遇到新的“异常”时,他们毫不犹豫地联系工程师;他们尽力理解工程师的挑战和目标,以确保他们的分析工作与基础设施的技术现实相一致。
他们是工具的专家
每家公司使用的工具不同,每个工具的功能和限制也各不相同。许多分析工具都有数百页的文档,因此很容易错过它们可能具备的一些强大功能。但对工具的深刻理解可以带来颠覆性的变化——而优秀的分析师早已意识到这一点:
-
他们探索所提供工具的高级功能——通过教程、阅读论坛以及简单的实践。
-
他们测试如何将不同的工具进行整合,并尝试自动化最重复的任务,以释放更多时间用于深入分析。
-
他们尝试保持更新(通过加入在线社区——例如 Reddit),并继续在数据工具的创新中进行实验。
他们具备强大的商业头脑
最后但同样重要的是,作为分析师,具备良好的商业头脑可以帮助你理解哪些洞察更有价值;如何让这些洞察对受众更易于理解;以及如何确保你的组织能够从你的研究中获得最大价值。优秀的分析师通常通过以下几种方式来提升自己的商业头脑:
-
他们阅读一些能够提供扎实基础知识和可复用框架的书籍(例如:Case In Point,Good Strategy, Bad Strategy,The Blue Ocean Strategy,The Innovator’s Dilemma,Inspired等)
-
他们寻求提高自己的沟通技巧:他们训练自己将复杂的数据发现转化为清晰、简洁且具有影响力的叙述;他们总是寻求反馈,以便成为更好的“数据故事讲述者”。
结论
那么,成为一名优秀数据分析师的秘诀是什么呢?就是要建立一个强大的技能组合。是关于技能的全面发展。这些分析师不仅仅依赖于某一方面的专业知识;他们会发展出技术、统计和商业敏锐度的和谐结合。
总结来说——就是不要跳过“统计日”——或者任何其他的肌肉训练日。就像在健身房一样。
数据科学需要学习的统计学知识
这篇文章详细列出了你作为初级数据科学家应掌握的统计学领域,并附有有用的学习资源。
·发表于Towards Data Science ·阅读时长:8 分钟·2024 年 4 月 6 日
--

图片来源:Lukas:www.pexels.com/photo/two-white-printer-papers-near-macbook-on-brown-surface-590016/
说实话,数学,特别是统计学,确实让人有些害怕。
在我之前的文章中,我讨论了成为一名高水平数据科学家所需掌握的数学知识。简而言之,你需要了解三个关键领域:线性代数、微积分和统计学。
现在,统计学是最有用且最重要的知识领域,值得全面掌握。统计学是许多数据科学原理的支柱,你将每天都用到它,甚至机器学习也是源于统计学习理论。
我想专门写一篇文章,详细介绍作为数据科学家应该掌握的统计学知识路线图,并提供学习这些知识的资源。
显然,统计学是一个庞大的领域,你不可能学遍所有的内容,尤其是当前活跃的研究领域。然而,如果你对本文中将要讨论的主题有扎实的工作知识,那么你将处于一个非常有利的位置。
如果你想全面了解这个领域,这篇维基百科文章总结了整个统计学的概况。
教授 AI 让我对数据技能与人性的理解
作为企业 AI 教育者的旅程中学到的三大关键教训
·发表于Towards Data Science ·阅读时长 8 分钟·2024 年 12 月 4 日
--

图片由Mikhail Nilov提供。
作为一名 AI 教育者,我的工作是为企业团队提供他们需要的相关数据和 AI 技能,以帮助他们茁壮成长。但回顾过去,我意识到我从他们身上学到了比他们从我身上学到的还要多。
这里是我在教授 2000 多名员工和 10 多家大型企业中学到的关于数据技能、人性和学习艺术的内容。
1. 数据科学的含义比你想的要多 —— 这些含义每天都在变化
定义模糊,技能要求也是如此
定义数据科学的方式有很多种。最流行的一种似乎是,数据科学位于计算机科学、数学与统计学和领域知识的交叉点上。

数据科学维恩图:图片由作者提供。
批评上面常用的维恩图总是很容易。然而,记住,它们是故意被简化过的,因此自然存在缺陷。就我个人而言,我认为它是一个有用的方式来概念化数据科学。如果你的工作涉及计算机科学(编程、数据库、云基础设施)、数学与统计学(统计学、随机过程、机器学习)和领域知识,并且程度不容忽视,那么你可能是在做数据科学。
数据科学家在实践中做的事情差异很大
问题在于,这一定义过于笼统。我遇到过一些数据科学家,他们…
-
无法使用基本的编程工具或技术进行分析
-
从未训练过机器学习模型
-
与真实业务隔离,主要关注数据管道或性能优化
另一方面,我遇到过…
-
训练机器学习模型的软件工程师
-
使用 Python 构建复杂数据管道的数据分析师
-
使用高级统计模型但从未将其视为人工智能的业务分析师
在现实世界中,数据科学相关的工作角色可能会令人困惑,因为…
-
类似角色之间存在显著的技能重叠(数据分析师、数据工程师、数据科学家、机器学习工程师、人工智能工程师)
-
公司根据其行业和规模不同对这些工作角色进行不同定义
-
人们承担新的责任,但仍留在同一职位,从未更改工作头衔
-
相同角色的工作要求迅速变化
如果您能够使用 SQL 从数据仓库提取数据,并使用 Python 可视化统计洞察力,这在10 年前的数据科学家职位上将为您赢得一份好工作。如今,您在传统组织(如大型保险公司)仍有机会。但是,如果您试图作为数据科学家加入独角兽科技初创公司,最好知道如何训练 ML 模型,将其部署到云端,并设置监控和重新训练机制,包括数据、模型和代码版本控制。如果您有使用 ChatGPT 超过 10 年的经验,那将是另一个加分项。
找到您的个人发展路径
我认为从这些观察中得出的关键见解是,您应该将个人技能发展集中在为业务带来价值的方面,而不是被当前工作头衔的任意定义所要求的内容。
如果您正在解决相关的业务问题,享受您的工作,并且得到了很好的报酬,不要担心其他人认为市场对您的需求是什么。
当然,您应该努力扩展自己的技能,并且在当今世界,在同一家公司的同一职位上工作 10 年对长期技能发展来说很少是最佳选择。但是,如果您找到了一个您的个人技能集受到高度重视的业务领域,您可以确信其他公司也存在同样的问题。您的工作是确保您能够解决这个问题,现在和将来。
将自己与他人进行比较可能是有用的,但也会分散注意力。他人有不同的个性和兴趣,可能从事完全不同于您的工作。编程、机器学习、云平台等只是工具。学习您真正需要精通以解决特定业务问题的工具。
2. 非技术数据和人工智能角色应该被接受,而不是被嘲笑

非技术数据和人工智能角色的示例
自从 GenAI 出现以来,成千上万的 AI 专家出现在职场(以及商业影响力)市场上。我们很容易对这一发展进行嘲笑,当然,我们都忍不住笑看那些像上面这样有趣的网络迷因。然而,在这一部分,我想为非技术性数据与 AI 角色辩护。
大多数数据与 AI 角色传统上是有一定技术性的,例如数据科学家、数据工程师或数据分析师,通常涉及编程和数学的专业化。通过我的工作,我遇到了许多非技术性的数据与 AI 角色。以下是一些例子:
-
AI 战略顾问: 帮助公司建立数据与 AI 战略的商业专业人士。
-
AI 转型经理: 领导 AI 项目,如 AI 企业、AI 战略或新的 AI 商业模式
-
数据文化经理: 建立数据治理,通过学习计划促进数据素养,并推动跨职能协作
-
数据管理员: 在组织内管理并确保数据的质量、一致性和安全性
此外,我还遇到过来自非技术领域的专业人士,他们专注于数据与 AI。以下是一些例子:
-
一位学术研究顾问,专门研究 AI 工具在研究中的应用
-
一位法学教授,专门研究欧洲 AI 法规
-
一位动态设计师,专门使用 AI 工具进行视觉艺术创作
所有这些专业人士都可以称自己为数据专家或 AI 专家——他们完全有理由这么做。他们在一个日益增长的领域中拥有宝贵的专业知识,并在他们的组织中发挥着重要作用。如果某人具备相关的数据/AI 技能,并且这些技能具有相关性且不易被替代,我乐意接受他们称自己为专家。
为什么非技术专家很重要
事实上,AI 领域目前的发展在技术成熟的过程中是正常的。为了驾驶第一辆汽车——奔驰专利汽车(1885 年),你必须非常精通技术,更不用说发明和制造这辆车的复杂性了。
现在,许多汽车专家是非技术性的,例如汽车销售员(= AI 销售)、驾驶教练(AI 教育者)或安全检查员(AI 合规官)。他们对汽车经济至关重要——就像非技术性的 AI 专家对 AI 经济至关重要一样。
在我的工作中,我发现这些专家非常充实和谦逊,让我从不同的角度看待问题,同时也提醒我,尽管我是技术专家,但在 AI 和 AI 业务方面我仍然有许多未知之处。
让我们欢迎新进场的人,与他们分享我们的知识,并从他们独特的专业知识中受益。
3. 教育是免费的——但你的时间不是

图片由Aron Visuals提供,来源于Unsplash
在工作中学习的挑战
在数据科学领域,有持续的压力保持更新和学习新技能。毕竟,如果你不能随时为同事提供关于昨天晚上 10 点发布的新 LLM 的即席意见,那么你在团队中的目的是什么?
大多数数据人员都热衷于学习新技术,只要有机会。而如今,我们比以往任何时候都有更多的学习机会。几乎所有相关知识都可以免费在互联网上以文本、播客和视频形式获得。但如果是这样,那么为什么感觉你从未有时间学习呢?为了获得你需要满足同行、老板、就业市场和自己过高期望的技能?
残酷的事实是,你不是因为你的知识而被付费,而是因为你为公司带来的商业价值。如果你希望雇主支持你的学习之旅,向他们解释为什么这对他们是值得的。
如何说服雇主投资于你的技能
你的处境并不像你想象的那么糟糕。你可以提出几个令人信服的论点:
-
“我们长期以来一直受到 X 问题的困扰。如果我学习 Y 技术,这可能会让我们更接近彻底解决这个问题。”
-
“我最近在播客中听说了 X 技术。有了它,我们可能可以使 Y 流程更高效,每个月节省 Z 小时。我想有些时间来探索这个选项。”
-
“我知道我们目前在公司没有 X 技术的用例。我想改变这一点,充当这项技术的内部大使,这在行业中变得越来越重要。”
要获得学习新技能的时间最简单的方法是提供一个具有清晰可信的路线图,指向实际业务成果的用例示例。然而,这并不总是可能。在这些情况下,我的建议是提出大使论点(第 3 点)。大多数公司都不愿意投入大量资源到新兴技术中。一个更便宜的解决方案是投资于那些充当大使并推动组织内技术实验的个人的教育,这可能会在最小资源投入下产生强大的用例。
免费午餐:选择一个鼓舞人心的工作环境
作为一名 AI 培训师,我教过各种机构的员工——从充满活力的 IT 公司到保守的银行。在我举办的每次培训中,至少有一个人让我惊讶于他们对数据和 AI 的学习热情。然而,他们表达兴趣的方式可能有根本的不同。
在一家科学公司中,最积极的参与者可能会就他们正在进行的 LLM 爱好项目寻求提示工程方面的建议。在一家大型金融机构中,一位高度积极的参与者可能会带着他们的 STATA 脚本打印稿参加培训,并用文本标记突出相关部分,请我帮忙将代码迁移到 Python。
这告诉我们,如果我们想要快速学习并处于技术进步的前沿,仅有动机是不够的(尽管它是一个前提)。理想情况下,我们希望选择一个工作环境,在那里我们被志同道合的人围绕。与其他技术爱好者一起工作,会改变你对技术可能性的看法,激励你每天变得稍微少一些无能。
这使得你的工作环境成为一个“免费午餐”。在无需额外时间投入的情况下,你的学习进度得到了加速。不要低估这一效应在长期中的威力!
结论
在这篇文章中,我分享了自己在大公司教授 AI 时学到的三点关键经验:
-
1. 数据科学的含义比你想象的更多 —— 这些含义每天都在变化
-
2. 非技术性的数据与 AI 职位应当被接纳,而非嘲笑
-
3. 教育是免费的 —— 但你的时间不是
希望你觉得这篇文章有意义并且喜欢阅读!
我定期撰写关于 AI 的文章,专注于AI 与音乐的交集。以下是一些你可能也会喜欢的文章:
-
🖼️ 声音与图像:使用 AI 创造惊艳的视听艺术
-
💻 AI 如何去除难以察觉的水印
-
🎶 音乐发现的未来:搜索 vs. 创作
关注我在LinkedIn上,获取关于 AI 和音乐领域发展的定期更新。
粒子群优化(PSO)从零开始。用 Python 实现的最简单解释
如何实现 PSO
·发布于 Towards Data Science ·7 分钟阅读·2024 年 2 月 16 日
--

图片来自 James Wainscoat 在 Unsplash
在谈论 群体 和 粒子 之前,让我们简要讨论一下优化本身。基本上,优化是寻找某个函数的最小值或最大值的过程。例如,当你急于赶到办公室,并思考哪条路最快时,你正在优化你的路线(在这种情况下,它是一个函数)。在数学中,有数百种优化方法,其中有一类叫做自然启发式方法。
自然启发式算法基于从自然现象或过程汲取灵感的现象。其中最受欢迎的包括遗传算法、布谷鸟搜索、蚁群算法和粒子群优化 [1] 或 PSO。
本教程使用仅包含 numpy 和 matplotlib 的 Python 实现。你可以使用 这个笔记本进行后续学习。
让我们从创建一个我们将用 PSO 优化的函数开始。
如果有人让我思考函数,首先浮现在我脑海中的(几乎是下意识的😂)就是抛物线。那么我们先画出它。
import numpy as np
import numpy.random as rnd
import matplotlib.pyplot as plt
x = np.arange(-10,10, 0.01)
y = x**2
plt.plot(x,y, lw=3)
plt.xlabel('x')
plt.ylabel('y')
plt.grid(True)
plt.show()

图片来自 作者。
对于这个函数,y=x²,优化的目标是找到点 x=0,使得 y 的值尽可能低——即为 0。但这太简单了。让我们把一切弄得更复杂一些,但仍然保持在二维空间。我决定使用以下方程组:

图片来源:作者。
或者用代码表示:
def function(x):
y = []
for i in x:
if i>-3:
y.append(i**2-4*i+i**3)
else:
y.append(0.2*i**2)
return y
x = np.arange(-10,3, 0.001)
y = function(x)
plt.plot(x,y, lw=3)
plt.title('Function to optimize')
plt.xlabel('x')
plt.ylabel('y')
plt.grid(True)
plt.show()

图片来源:作者。
如你所见,我故意设置了两个极小值:全局(左)和局部(右)。在优化一个函数时,我们通常需要找到最小(最大)值,即全局最小值(最大值),因此我希望 PSO 算法面临两个极值的挑战。
现在让我们讨论一下算法本身。
基本思想是基于两个术语:种群和群体。群体由包含粒子的种群组成。
particle — — — > population — — → swarm
那么,粒子在这里是什么呢?粒子是一个可能的解。想象一下,你正在调查一起犯罪事件,一堆嫌疑人站在你面前。所有人都是“候选人”可以被定罪。但在粒子的情况下,这些候选人是合作的,并且具有社会影响。
粒子本身有两个特征——位置和速度(或者说是速度;我们的粒子在飞行,对吧?)。所以这些粒子通过逐步迭代,不断改变它们的位置。它们的速度由以下公式定义:

图片来源:作者。
如你所见,这里有三个组成部分。第一个是惯性。它是上一轮迭代中粒子的速度与一个任意权重w的乘积。所以它基本上定义了粒子之前的速度对当前速度的影响。
第二个是个人影响。它有自己的系数:任意权重 c₁ 和随机数 r₁。在括号内我们有粒子在种群中的最佳位置与当前粒子的位置(p)之间的差异。
第三个部分是社会影响。它有类似的任意权重 c₂ 和随机数 r₂,以及粒子在群体中的最佳位置与当前粒子的位置(p)之间的差异。
计算速度后,我们通过简单的加法更新粒子的当前位置:

图片来源:作者。
到目前为止,有一个大问题尚未解答: 这些群体和种群中的最佳位置是什么?
为了计算这些,我们需要一个增益/奖励函数,来指示哪个解更接近最小值(最大值)。在我们的例子中,这个增益函数就是我们绘制的函数。因此,粒子是 X,通过将每个 X 代入函数,我们可以找出哪个 X 给出了最小(最大)值的 Y。
因此,在一个种群中,最佳位置是 X,它在当前迭代中给出了最低(最大)的 Y。而在一个群体中,最佳位置是 X,它在所有先前的迭代中给出了最低(最大)的 Y。
简化来说,我们可以说算法的核心思想如下:
一群粒子,每个粒子都有特定的位置和速度,它们一起飞行寻找全局最小值(最大值),形成一个种群。种群是迭代出现的,每个种群只存在一轮迭代,但它们会相互交换信息,因此每个后续的种群比前一个更接近找到解决方案。
现在让我们回到编码,使用 Python 实现我们所讨论的内容。
首先,让我们定义我们的超参数。像许多其他元启发式算法一样,这些变量应该在使用过程中进行调整,并且没有一组通用的值。不过,我们先使用这些值:
POP_SIZE = 10 #population size
MAX_ITER = 30 #the amount of optimization iterations
w = 0.2 #inertia weight
c1 = 1 #personal acceleration factor
c2 = 2 #social acceleration factor
现在让我们创建一个生成随机种群的函数:
def populate(size):
x1,x2 = -10, 3 #x1, x2 = right and left boundaries of our X axis
pop = rnd.uniform(x1,x2, size) # size = amount of particles in population
return pop
如果我们将其可视化,我们会得到如下的结果:
x1=populate(50)
y1=function(x1)
plt.plot(x,y, lw=3, label='Func to optimize')
plt.plot(x1,y1,marker='o', ls='', label='Particles')
plt.xlabel('x')
plt.ylabel('y')
plt.legend()
plt.grid(True)
plt.show()

图片由作者提供。
在这里你可以看到,我随机初始化了 50 个粒子的种群,其中一些粒子已经接近解。
现在让我们实现 PSO 算法本身。我对代码中的每一行都做了注释,如果你有任何问题,请随时在下方评论区提问。
"""Particle Swarm Optimization (PSO)"""
particles = populate(POP_SIZE) #generating a set of particles
velocities = np.zeros(np.shape(particles)) #velocities of the particles
gains = -np.array(function(particles)) #calculating function values for the population
best_positions = np.copy(particles) #it's our first iteration, so all positions are the best
swarm_best_position = particles[np.argmax(gains)] #x with with the highest gain
swarm_best_gain = np.max(gains) #highest gain
l = np.empty((MAX_ITER, POP_SIZE)) #array to collect all pops to visualize afterwards
for i in range(MAX_ITER):
l[i] = np.array(np.copy(particles)) #collecting a pop to visualize
r1 = rnd.uniform(0, 1, POP_SIZE) #defining a random coefficient for personal behavior
r2 = rnd.uniform(0, 1, POP_SIZE) #defining a random coefficient for social behavior
velocities = np.array(w * velocities + c1 * r1 * (best_positions - particles) + c2 * r2 * (swarm_best_position - particles)) #calculating velocities
particles+=velocities #updating position by adding the velocity
new_gains = -np.array(function(particles)) #calculating new gains
idx = np.where(new_gains > gains) #getting index of Xs, which have a greater gain now
best_positions[idx] = particles[idx] #updating the best positions with the new particles
gains[idx] = new_gains[idx] #updating gains
if np.max(new_gains) > swarm_best_gain: #if current maxima is greateer than across all previous iters, than assign
swarm_best_position = particles[np.argmax(new_gains)] #assigning the best candidate solution
swarm_best_gain = np.max(new_gains) #assigning the best gain
print(f'Iteration {i+1} \tGain: {swarm_best_gain}')
经过 30 次迭代,我们得到了这个结果:

PSO(w=0.2,c1=1,c2=2)。图片由作者提供。
正如你所看到的,算法陷入了局部最小值,这是我们不希望发生的。因此,我们需要调整超参数并重新开始。这一次,我决定将惯性权重设置为w=0.8,因此,之前的速度对当前状态的影响更大。

PSO(w=0.9,c1=1,c2=2)。图片由作者提供。
然后,瞧,我们达到了函数的全局最小值。我强烈建议你尝试调整 POP_SIZE、c₁ 和 c₂,这将帮助你更好地理解代码和粒子群优化(PSO)背后的理念。如果你感兴趣,可以让任务更复杂一些,优化一个三维函数并制作一个漂亮的可视化。
===========================================
[1]Shi Y. 粒子群优化 //IEEE connections. — 2004 年 — 第 2 卷 — 第 1 期 — 第 8-13 页。
===========================================
我在 Medium 上的所有文章都是免费的,公开访问的,所以如果你能在这里关注我,我将不胜感激!
附言:我对(地理)数据科学、机器学习/人工智能和气候变化充满热情。如果你有兴趣共同参与项目,请通过LinkedIn与我联系。
🛰️更多内容请关注🛰️
作为数据科学家,入职的前 90 天你可以期待什么
从没有博士学位或技术背景的数据分析师转型
·发表于Towards Data Science ·5 分钟阅读·2024 年 10 月 5 日
--

“欢迎加入团队!我们非常高兴你能来到这里!”
我简直不敢相信自己能获得第一份数据科学家的工作。
我没有博士学位,也没有技术背景。我来自金融领域,曾在金融科技公司作为风险分析师工作了好几年。
尽管通过了严格的技术编码面试,但我仍然感到既兴奋又有冒名顶替综合症。
从分析师背景出发,我观察到在数据科学角色上工作的最大区别包括:
-
管理数据科学项目的时间表,
-
与利益相关者沟通项目的时间表和实施方案。
-
学习技术方面的内容,例如采用测试驱动开发方法编写生产级代码,
作为数据科学家的前三个月是你职业生涯中的关键时期,决定了你的体验成败。
我在这里将帮助你了解在工作中的前 90 天应该期待什么,帮助你减轻过渡期的紧张情绪,建立信心。
如果你想掌握 LLM,应该学习什么?
如果你想理解大规模语言模型,应该学习哪些基础概念?
·发布于Towards Data Science ·阅读时间:6 分钟·2024 年 8 月 12 日
--

图片来自solenfeyissa @ Unsplash.com
我们用来与 LLM(大规模语言模型)互动的大部分代码都隐藏在多个 API 之后——这其实是件好事。
但如果你像我一样,想要理解这些神奇模型的内和外,那么你仍然有希望。现在,除了从事开发和训练新 LLM 的研究人员之外,主要有两类人正在使用这些模型:
-
通过应用程序与之互动的用户,比如ChatGPT或Gemini。
-
数据科学家和开发人员,他们使用不同的库,如llangchain、llama-index,甚至使用Gemini或OpenAI的 API,这些库简化了在这些模型上构建应用的过程。
问题是——你可能已经感觉到了——在文本挖掘和自然语言处理领域,有些基础知识完全被隐藏在消费者产品或 API 中。别误会我的意思——它们对于围绕这些技术开发有趣的用例非常棒。但是,如果你想深入了解、构建复杂的用例,或者更好地操控 LLM,你需要学习基础知识——尤其是当模型表现得像你想象的那样……
我们仍然不理解的机器学习问题
机器学习中的未知问题,研究人员难以理解——从批量归一化到 SGD 隐藏的奥秘
·发表于Towards Data Science ·阅读时间 12 分钟·2024 年 7 月 26 日
--

我们仍然不理解的机器学习问题。(作者)
如果你还不是会员,点击这里免费阅读 👈
令人惊讶的是,机器学习中的一些基础主题至今仍然是研究人员未解之谜,尽管它们是基本的且常用的,但似乎仍然神秘莫测。机器学习的一个有趣之处在于,我们构建了有效的系统,然后才去弄清楚它们为何有效!
在这里,我旨在探讨一些机器学习概念中的未知领域,以展示尽管这些想法看起来基础,实际上它们是通过一层层抽象构建而成的。这有助于我们练习质疑我们知识的深度。
在本文中,我们探讨了深度学习中的几个关键现象,这些现象挑战了我们对神经网络的传统理解。
-
我们从批量归一化开始,并讨论其尚未完全理解的基本机制。
-
我们考察了一个反直觉的观察结果,即过参数化模型往往具有更好的泛化能力,这与经典的机器学习理论相矛盾。
-
我们探索了梯度下降的隐式正则化效应,这似乎自然地使神经网络倾向于更简单、更具普适性的解决方案。
-
最后,我们讨论了彩票票假说(Lottery Ticket Hypothesis),该假说提出,大型神经网络包含较小的子网络,当它们被单独训练时,能够实现相当的性能。
本文包含 4 个部分,它们之间没有直接关联,所以可以随意跳到你更感兴趣的部分。
· 1. 批量归一化
∘ 我们不理解的东西
∘ 批量归一化的注意事项
· 2. 过度参数化与泛化
∘ 理解深度学习需要重新思考泛化
· 3. 神经网络中的隐式正则化
∘ 实验 1
∘ 实验 2
∘ 梯度下降作为一种自然正则化器
· 4. 彩票票假设
· 最后的话。
∘ 让我们连接!
· 进一步阅读
· 参考文献
1. 批量归一化
由谢尔盖·约夫和克里斯蒂安·谢格迪于 2015 年提出的[1],批量归一化是一种加速神经网络训练并提高其稳定性的方法。之前已知,将输入数据的均值调整为零、方差调整为一,可以实现更快的收敛。作者进一步使用这一思想,提出了批量归一化,以使隐藏层的输入具有零均值和单位方差。

一个概述 ResNet 中使用的残差块以及其中批量归一化的应用。(作者提供)
自其提出以来,批量归一化在神经网络中变得十分常见。其中一个例子,就是它在著名的 ResNet 架构中的应用。因此我们可以自信地说,它的有效性是毋庸置疑的。
一项有趣的研究[2]表明,尽管训练完整的 ResNet-866 网络达到了93%的准确率,但冻结所有参数,仅训练批量归一化层的参数,结果只达到了83%的准确率——仅有 10%的差异。
批量归一化有三个方面的好处:
-
通过对每一层的输入进行归一化,它加速了训练过程。
-
它减少了对初始条件的敏感性,这意味着我们不需要非常小心地初始化权重。
-
批量归一化还充当了正则化器。它提高了模型的泛化能力,在某些情况下,减少了对其他正则化技术的需求。
我们不理解的东西
尽管批量归一化的正面效果显而易见,但没人真正理解其背后的有效性原因。最初,批量归一化论文的作者将其提出作为解决内部协变量偏移问题的方法。
关于内部协变量偏移的一个提示是,你会发现它有各种不同的定义,乍一看似乎没有关联。 这是我想要定义它的方式。
神经网络的层是在反向传播过程中从输出层(结束)更新到输入层(开始)的。内部协变量偏移指的是在训练过程中,由于前一层参数的更新,输入层的分布发生变化的现象。
当我们改变前面几层的参数时,也会改变后续层的输入分布,而这些后续层已经更新,以更好地适应较旧的分布。
内部协变量偏移(internal covariate shift)会减缓训练过程,并使网络更难以收敛,因为每一层必须不断适应前一层更新引入的输入分布的变化。
原始批量归一化论文的作者认为,其有效性的原因在于缓解了内部协变量偏移问题。然而,后来的一篇论文[3]认为,批量归一化的成功与内部协变量偏移无关,而是由于平滑优化景观的作用。

两种损失景观的比较:一个是高度粗糙和陡峭的损失表面(左),另一个是较为平滑的损失表面(右)。(Source)
上图来自[4],该文并非关于批量归一化的内容,但很好地展示了平滑损失景观的样子。然而,批量归一化通过平滑损失景观而有效的理论本身也存在挑战和疑问。
批量归一化的注意事项
由于我们对批量归一化工作原理的理解有限,以下是在网络中使用它时需要考虑的事项:
-
使用批量归一化时,训练(train)和推理(inference)模式是不同的。使用错误的模式可能导致难以识别的意外行为。[5]
-
批量归一化(Batch Norm)在很大程度上依赖于小批量(minibatch)的大小。因此,虽然它减少了对谨慎初始化权重的需求,但选择合适的小批量大小变得更加关键。[5]
-
关于在激活函数之前还是之后使用批量归一化,目前仍存在争论。[6]
-
虽然批量归一化具有正则化(regularizer)的效果,但它与其他正则化方法(如丢弃法(dropout)或权重衰减(weight decay))的相互作用尚不完全明确。
关于批量归一化仍有许多问题待解答,但研究仍在进行,以揭示这些层如何影响神经网络。
2. 过度参数化与泛化

面部识别实验表明,网络中权重的最优数量可以远大于数据点的数量。(source)
大型网络挑战了我们过去对神经网络工作原理的理解。
传统上认为,使用过度参数化的模型会导致过拟合。因此,解决方案通常是限制网络的大小,或添加正则化以防止过拟合训练数据。
出人意料的是,在神经网络的情况下,使用更大的网络可能会改善泛化误差(|训练误差 - 测试误差|)。换句话说,更大的网络有更好的泛化能力。[7] 这与传统的复杂度度量标准(如VC 维度——一个量化从样本中学习难度的指标)所宣称的内容相矛盾。[8]
这一理论还挑战了一个关于深度神经网络(DNN)是否通过记忆训练数据来实现其性能的辩论。[9] 如果它们是通过记忆数据来做的,那它们怎么可能泛化到预测未见过的数据呢?如果它们不记忆数据,而只是学习数据中的潜在模式,那它们又是如何预测正确的标签,即使我们给标签引入了一定的噪声?

分类器的过拟合。(放大图——来源)
理解深度学习需要重新思考泛化能力
关于这一主题的有趣论文是理解深度学习需要重新思考泛化能力[10]。作者认为,传统的方法无法解释为什么更大的网络能够良好泛化,同时这些网络也能拟合随机数据。
本文的一部分重点解释了显式正则化(如权重衰减、丢弃法和数据增强)在泛化误差中的作用:
显式正则化可能会改善泛化性能,但既不是必要的,也不是单独足以控制泛化误差。L2 正则化(权重衰减)有时甚至有助于优化,说明它在深度学习中的理解仍然不完全。[10]
即使使用了丢弃法和权重衰减,InceptionV3 仍然能够非常好地拟合随机训练集,超出了预期。这一含义并不是贬低正则化,而是更强调通过改变模型架构可以获得更大的收益。

正则化对泛化能力的影响。[10]
那么,是什么让一个能够良好泛化的神经网络与那些泛化不良的网络不同呢?这似乎是一个“兔子洞”。我们仍然需要重新思考一些问题:
-
我们对模型有效容量的理解。
-
我们对模型复杂度和大小的衡量。模型参数或 FLOP 是否仅仅是好的度量标准?显然不是。
-
泛化能力的定义以及如何衡量它。

随着网络规模(H)的不断增大,训练误差和测试误差持续下降,并且没有发生过拟合。[11]
关于大规模网络和参数数量对泛化能力的影响,已有大量论文和博客文章,其中一些甚至互相矛盾。
我们目前的理解可能表明,尽管较大的网络倾向于过拟合,但它们仍能较好地泛化。这可能是由于它们的深度,使得它们能够学习比浅层网络更复杂的模式。这主要取决于领域——某些数据类型可能更适合使用较小的模型,并遵循奥卡姆剃刀原理(不要错过这篇文章以进一步阅读👇)。
3. 神经网络中的隐式正则化
机器学习的核心是梯度下降——我们为了在损失景观中找到局部最小值而采取的步骤。梯度下降(GD)以及随机梯度下降(SGD)是任何开始学习机器学习的人首先理解的算法。
尽管算法看起来简单,可能有人认为它没有太多深度。然而,在机器学习中,你永远无法找到池塘的底部。
神经网络是否从梯度下降的隐式正则化中受益,这种正则化推动它们找到更简单和更通用的解决方案?这是否是之前提到的过参数化网络能够泛化的原因?

二维中的梯度下降。(来源:Wikimedia Commons)
有两个实验需要特别注意:
实验 1
当[11]的作者使用 SGD 并且没有显式正则化,训练 CIFAR-10 和 MNIST 数据集的模型时,他们得出结论:随着网络大小的增加,测试和训练误差不断减少。这与认为较大的网络由于过拟合而有更高测试误差的观点相悖。即使在网络中添加更多的参数,泛化误差也没有增加。随后,他们通过添加随机标签噪声强制网络过拟合。如下面的图所示,即使标签随机噪声达到 5%,测试误差仍然进一步降低,而且没有明显的过拟合迹象。

网络随着大小(H)增加并且标签噪声为 5%的测试和训练误差。左侧是 MNIST,右侧是 CIFAR-10。[11]
实验 2
一篇重要的论文,寻找真实的归纳偏置 [12],通过拟合一个使用线性可分数据集的预测器进行实验。作者展示了如何在没有正则化的情况下,使用梯度下降的逻辑回归本能地将解偏向于最大间隔分离器(也称为硬间隔 SVM)。这是梯度下降的一个有趣且令人惊讶的行为。因为即使损失和优化并未直接涉及任何鼓励最大化间隔的项(比如在支持向量机中找到的那些项),梯度下降本能地将解偏向于最大间隔分类器。

H3 表示硬间隔支持向量机(SVM)如何对数据集进行分类。(来源:Wikimedia Commons)
梯度下降作为一种自然的正则化方法
实验表明存在一种隐式正则化效应,优化过程似乎偏向更简单和更稳定的解决方案。这意味着梯度下降(GD)更倾向于简单的模型,通常会收敛到一种特殊类型的局部最小值,即“平坦”最小值,相比于更尖锐的最小值,平坦最小值通常具有较低的泛化误差。这有助于解释为何深度学习模型在实际任务中往往表现良好,即使在超出训练数据的情况下。这表明优化过程本身可以视为一种隐式正则化,导致模型不仅在训练数据上最小化误差,而且在预测未见数据时也表现出鲁棒性。对此的完整理论解释仍然是一个活跃的研究领域。
或许这篇文章也会对你有趣,讲述了深度神经网络是如何以及为何趋向于统一的现实表示:
人工智能模型是否正在朝向统一的现实表示发展?柏拉图表示法……
[towardsdatascience.com
4. 彩票票据假说
模型剪枝可以通过减少训练好的神经网络的参数达到 90%。如果操作得当,可以在不降低准确度的情况下实现这一点。但你只能在模型训练完成后进行剪枝。如果我们能够在训练前移除多余的参数,这意味着可以使用更少的时间和资源。
彩票票据假说[13]认为,神经网络包含一些子网络,当它们单独训练时,可以达到与原始网络相当的测试准确度。这些子网络——中奖的票据,拥有使其训练成功的初始权重——彩票。
作者通过迭代剪枝方法找到了这些子网络:
-
训练网络:首先,训练原始的未剪枝网络。
-
剪枝:训练后,剪枝p%的权重。
-
重置权重:剩余的权重被重置为初始初始化时的原始值。
-
再训练:剪枝后的网络被重新训练,以查看它是否能达到与之前的网络相同或更高的性能。
-
重复:直到达到原始网络期望的稀疏度,或者剪枝后的网络无法再与未剪枝网络匹敌时,才会停止此过程。

迭代剪枝在《彩票票据假说》论文中的应用。(作者)
提出的迭代训练方法在计算上非常昂贵,要求在多个实验中训练一个网络 15 次或更多次。
为什么在神经网络中会出现这种现象仍是一个研究领域。是否有可能是 SGD 在训练网络时只关注成功的网络部分,而不是网络的全部?为什么某些随机初始化会包含如此高效的子网络?如果你想深入探讨这个理论,不要错过[13]和[14]。
最后的话。
感谢您阅读本文! 我已经尽力提供一篇准确的文章,但如果您认为需要修改,请分享您的意见和建议。
让我们保持联系!
免费订阅以接收新文章通知!你还可以在 LinkedIn 和 Twitter* 上找到我。*
进一步阅读
如果你已经看到这里,你可能也会对以下文章感兴趣:
通过应用 AI 和诺贝尔奖得主的方法,分四个简单步骤学习任何概念
pub.towardsai.net ## 实践中的协作 AI 代理全面指南
定义,并建立一个代理团队,精炼你的简历和求职信
[towardsdatascience.com [## 我在 ChatGPT 中玩了 Flappy Bird
GPT-4 非常棒,但它足够好,能够作为游戏引擎吗?我用一个简单的 LangChain 尝试了这个,做了一个 Flappy Bird 的游戏……
参考文献
[1] Ioffe, S., & Szegedy, C. (2015)。 《批量归一化:通过减少内部协方差偏移加速深度网络训练》。 arXiv。
[2] 超出常规,DeepLearning.AI
[3] Santurkar, Shibani; Tsipras, Dimitris; Ilyas, Andrew; Madry, Aleksander (2018 年 5 月 29 日)。 “批量归一化如何帮助优化?” arXiv:1805.11604
[4] Li, H., Xu, Z., Taylor, G., Studer, C., & Goldstein, T. (2018). 可视化神经网络的损失景观。arXiv
[5] 批归一化的危险
[6] x.com/svpino/status/1588501331316121601
[7] Neyshabur, B., Li, Z., Bhojanapalli, S., LeCun, Y., & Srebro, N. (2018). 朝着理解过度参数化在神经网络泛化中的作用迈进。arXiv
[9] 深度网络不是通过记忆化学习的
[10] Zhang, C., Bengio, S., Hardt, M., Recht, B., & Vinyals, O. (2017). 理解深度学习需要重新思考泛化问题。arXiv:1611.03530
[11] Neyshabur, B., Tomioka, R., & Srebro, N. (2015). 寻找真实的归纳偏差:关于隐式正则化在深度学习中的作用。arXiv:1412.6614
[12] Soudry, D., Hoffer, E., Nacson, M. S., Gunasekar, S., & Srebro, N. (2017). 梯度下降在可分数据上的隐式偏差。arXiv:1710.10345
[13] Frankle, J., & Carbin, M. (2019). 彩票票假设:寻找稀疏的、可训练的神经网络。arXiv:1803.03635
[14] www.lesswrong.com/posts/Z7R6jFjce3J2Ryj44/exploring-the-lottery-ticket-hypothesis
2024 年转行做数据科学之前,你需要知道的事项
市场是如何变化的(以及如果我今天重新开始我的职业生涯,我会遵循的路线图)
·发布于 Towards Data Science ·11 分钟阅读·2024 年 10 月 3 日
--

本文 100%基于我的经验、我的研究以及我在欧洲市场上的观察。其他地区的情况可能稍有不同,但我们都面临着数据科学市场中相同的变革浪潮。
对于那些不认识我的人,我的职业生涯始于十年前:我毕业于一所工程学院,获得应用数学学位。那时,数据科学还不是一个流行的词汇。我在法国电力公司(EDF,Électricité de France)开始了我的职业生涯,该公司是能源行业的领军企业,我在这里利用时间序列和概率模型建立模型,以理解能源消耗和生产模式。
当时,拥有坚实的应用数学基础就像拥有了一张进入数据科学世界的金票。数据科学职位非常稀缺,我经常需要向人们解释什么是数据科学家。
在过去的十年里,这个领域发生了彻底的变化。我指导的一些初级人员觉得市场已经饱和了。所以,如果你打算在 2024 年转行做数据科学,我想说的是:游戏规则已经改变——但它仍然是非常…
构建具有 Stripe 订阅和 Firestore 集成的大型 Streamlit 应用程序所需了解的内容。
将想法转化为软件产品的能力是一项很棒的技能。在这篇博客中,我将描述需要什么以及如何将各部分组合在一起创建一个软件产品,而无需开始成本,但具有订阅模式和 Firestore 集成。
·发表在Towards Data Science·阅读 15 分钟·2024 年 8 月 2 日
--

照片由Shane Aldendorff在Unsplash上拍摄。
无论您是数据科学家、数据工程师还是其他软件开发领域的人员,仅使用笔记本电脑将您的想法转化为真正的工作软件产品可能是最重要的技能。各种软件开发领域在这个过程中汇聚在一起,从 UX、前端到后端开发、数据处理、可视化、云/服务器配置等等。这是一个来回的过程。挑战在于决定从哪个想法开始,并且如何避免(开始)成本,直到人们想要您的产品为止。 在这篇博客中,我将讨论不同类型的想法,并展示我如何使用Streamlit创建和部署SkyWalk,使用Stripe进行订阅,以及使用Google Firestore进行数据存储。
构建一个大型 Streamlit…
神经网络内部是什么?
使用 PyTorch🔥绘制 3D 误差曲面
·发表于 Towards Data Science ·阅读时长 6 分钟·2024 年 9 月 29 日
--

图片来源:作者。
在我本科的最后一年,就像许多其他学生一样,我需要为我的毕业论文选择一个课题。我的专业是水文气象学,因此我最初考虑研究与气候建模相关的问题。幸运的是,我的导师,Dr. Gribanov,建议我探索一个我当时完全陌生的全新方向——应用神经网络来放大陆地碳通量。那时,“神经”这个词让我想到了外科手术,而“网络”则让我联想到交通网络。然而,他给了我一个我听过的最清晰、最直观的神经网络解释之一。亮点之一是他对优化过程的描述。
想象一下这样一张空白的纸:

图片来源:GPT。
现在,我请你猛烈地(很重要)把它揉成一个球:

图片来源:GPT。
把它拉直后,你会看到类似地球表面或某种具有峰谷的地貌:

图片来源:GPT。
现在,如果我们引入三维——权重 1、权重 2 和均方误差(MSE),而不是纬度、经度和海拔——我们可以将此图像视为表示神经网络的误差曲面。优化的目标是找到这个曲面上的最低点,即最小误差。如图所示,这里有多个局部最小值和最大值,这就是为什么它总是一个具有挑战性的任务。
因此,在本文中,我们将创建一个3D的误差曲面,并使用plotly Python 库来交互式地展示它,同时展示随机梯度下降(SGD)的步骤。
像往常一样,本文的代码可以在我的 GitHub.
数据
首先,我们需要一些合成数据来进行处理。数据应展示某种非线性依赖关系。我们可以这样定义它:

图片由作者提供。
在 Python 中,它将具有以下形状:
np.random.seed(42)
X = np.random.normal(1, 4.5, 10000)
y = np.piecewise(X, [X < -2,(X >= -2) & (X < 2), X >= 2], [lambda X: 2*X + 5, lambda X: 7.3*np.sin(X), lambda X: -0.03*X**3 + 2]) + np.random.normal(0, 1, X.shape)
可视化之后:

图片由作者提供。
神经网络
由于我们正在可视化一个三维空间,我们的神经网络将只有两个权重。这意味着该 ANN 将由一个隐藏神经元组成。在 PyTorch 中实现这一点非常直观:
class ANN(nn.Module):
def __init__(self, input_size, N, output_size):
super().__init__()
self.net = nn.Sequential()
self.net.add_module(name='Layer_1', module=nn.Linear(input_size, N, bias=False))
self.net.add_module(name='Tanh',module=nn.Tanh())
self.net.add_module(name='Layer_2',module=nn.Linear(N, output_size, bias=False))
def forward(self, x):
return self.net(x)
重要! 别忘了关闭层中的偏置,否则你会得到x2倍的参数。
更改权重

图片由作者提供。
为了构建误差曲面,我们首先需要创建 W1 和 W2 的可能值网格。然后,对于每种权重组合,我们将更新网络的参数并计算误差:
W1, W2 = np.arange(-2, 2, 0.05), np.arange(-2, 2, 0.05)
LOSS = np.zeros((len(W1), len(W2)))
for i, w1 in enumerate(W1):
model.net._modules['Layer_1'].weight.data = torch.tensor([[w1]], dtype=torch.float32)
for j, w2 in enumerate(W2):
model.net._modules['Layer_2'].weight.data = torch.tensor([[w2]], dtype=torch.float32)
model.eval()
total_loss = 0
with torch.no_grad():
for x, y in test_loader:
preds = model(x.reshape(-1, 1))
total_loss += loss(preds, y).item()
LOSS[i, j] = total_loss / len(test_loader)
这可能需要一些时间。如果你将此网格的分辨率设置得过于粗糙(即,可能的权重值之间的步长),你可能会错过局部最小值和最大值。记得学习率通常是随时间逐渐减小的吗?当我们这样做时,权重值的绝对变化可能小到 1e-3 或更小。步长为 0.5 的网格根本无法捕捉到误差曲面的这些细微细节!
训练模型
在这一点上,我们完全不关心训练模型的质量。然而,我们确实希望关注学习率,所以我们将其保持在 1e-1 和 1e-2 之间。我们将简单地收集训练过程中的权重值和误差,并将它们存储在不同的列表中:
model = ANN(1,1,1)
epochs = 25
lr = 1e-2
optimizer = optim.SGD(model.parameters(),lr =lr)
model.net._modules['Layer_1'].weight.data = torch.tensor([[-1]], dtype=torch.float32)
model.net._modules['Layer_2'].weight.data = torch.tensor([[-1]], dtype=torch.float32)
errors, weights_1, weights_2 = [], [], []
model.eval()
with torch.no_grad():
total_loss = 0
for x, y in test_loader:
preds = model(x.reshape(-1,1))
error = loss(preds, y)
total_loss += error.item()
weights_1.append(model.net._modules['Layer_1'].weight.data.item())
weights_2.append(model.net._modules['Layer_2'].weight.data.item())
errors.append(total_loss / len(test_loader))
for epoch in tqdm(range(epochs)):
model.train()
for x, y in train_loader:
pred = model(x.reshape(-1,1))
error = loss(pred, y)
optimizer.zero_grad()
error.backward()
optimizer.step()
model.eval()
test_preds, true = [], []
with torch.no_grad():
total_loss = 0
for x, y in test_loader:
preds = model(x.reshape(-1,1))
error = loss(preds, y)
test_preds.append(preds)
true.append(y)
total_loss += error.item()
weights_1.append(model.net._modules['Layer_1'].weight.data.item())
weights_2.append(model.net._modules['Layer_2'].weight.data.item())
errors.append(total_loss / len(test_loader))

图片由作者提供。
可视化
最后,我们可以使用 plotly 可视化我们收集的数据。该图将有两个场景:表面和 SGD 轨迹。实现第一部分的一种方法是创建一个包含 plotly 表面的图形。之后,我们将稍微调整一下布局。
第二部分简单得很——只需使用Scatter3d函数并指定所有三个坐标轴。
import plotly.graph_objects as go
import plotly.io as pio
plotly_template = pio.templates["plotly_dark"]
fig = go.Figure(data=[go.Surface(z=LOSS, x=W1, y=W2)])
fig.update_layout(
title='Loss Surface',
scene=dict(
xaxis_title='w1',
yaxis_title='w2',
zaxis_title='Loss',
aspectmode='manual',
aspectratio=dict(x=1, y=1, z=0.5),
xaxis=dict(showgrid=False),
yaxis=dict(showgrid=False),
zaxis=dict(showgrid=False),
),
width=800,
height=800
)
fig.add_trace(go.Scatter3d(x=weights_2, y=weights_1, z=errors,
mode='lines+markers',
line=dict(color='red', width=2),
marker=dict(size=4, color='yellow') ))
fig.show()
在 Google Colab 或本地的 Jupyter Notebook 中运行它,将允许你更仔细地研究错误面。老实说,我花了很多时间仅仅是在看这个图:)

图片来自作者。
我很希望看到你的表面,所以请随时在评论中分享。我坚信表面越不完美,它就越有趣,值得研究!
===========================================
我在 Medium 上的所有出版物都是免费的,且开放访问的,所以如果你在这里关注我,我将非常感激!
附言:我对(地理)数据科学、机器学习/人工智能以及气候变化充满热情。如果你想一起合作做某个项目,请通过LinkedIn与我联系,并查看我的网站!
🛰️关注获取更多内容🛰️
计算机视觉和物体检测领域有哪些新进展?
·发表于 Towards Data Science ·发送至 新闻简报 ·阅读时间:4 分钟·2024 年 7 月 11 日
--
想要写你的第一篇 TDS 文章吗?我们始终欢迎新作者的投稿。
在进入本周精选的精彩文章之前,我们想花一点时间感谢所有读者、作者以及我们更广泛社区的成员,感谢你们的支持帮助我们达成了一个重要里程碑——我们的Medium粉丝数刚刚达到了…

我们无比激动,并且感谢所有支持我们的人,使得 TDS 成为了如今这个充满活力、以学习为核心的出版物。未来我们期待更多的成长和探索!
回到我们的常规内容,我们本周精选了三篇文章,重点介绍计算机视觉和物体检测领域的前沿工具和方法。随着多模态模型的不断发展以及自动驾驶、医疗保健和农业等应用逐渐走向主流,数据和机器学习从业者保持与最新进展同步变得尤为重要。(如果你此刻对其他话题更感兴趣,我们也有相关内容!向下滚动,查看我们精心挑选的神经科学、音乐与人工智能、环保意识的机器学习工作流程等推荐文章。)
-
掌握视频中的物体计数与静态图像中的相同过程相比,视频中的物体检测面临许多新的挑战。Lihi Gur Arie, 博士提供了一篇清晰简明的教程,展示了如何依然能够完成这一任务,并通过一个有趣的例子——数树上的移动蚂蚁来说明她的观点。
-
用 AI 点燃冰球:通过计算机视觉进行球员追踪对于任何寻找深入且引人入胜的项目讲解的人,我们强烈推荐Raul Vizcarra Chirinos关于他最近尝试从(或多或少)零开始构建冰球球员追踪器的文章。通过使用 PyTorch、计算机视觉技术和卷积神经网络(CNN),Raul 开发了一个原型,可以跟踪球员并收集基本的表现统计数据。
-
自动驾驶感知工程师规划速成课程虽然我们可能还需要几年时间才能看到自动驾驶汽车主导我们的道路,但近年来研究人员和行业参与者已取得了显著进展。那些希望扩展在自动驾驶背景下的规划和决策知识的从业者,不应错过Patrick Langechuan Liu关于该主题的全面“速成课程”。

图片由Harpreet Singh提供,来源于Unsplash
如承诺的,以下是我们推荐的其他主题、问题和挑战阅读,我们认为你可能会喜欢探索:
-
在他的首篇 TDS 文章中,Jonathan R. Williford, 博士探讨了当前多模态变压器工作的有趣联系,并与我们大脑处理视觉信息的方式进行了对比。
-
从过度防御自己的弱点到未能完全承担自己的项目,Mandy Liu反思了她作为初级数据科学家的错误经历,并分享了她对刚刚入门的人的可操作建议。
-
为什么跟踪在机器学习项目中如此重要,以及你该如何有效实施它?Chayma Zatout在她的 MLOps 入门中给出了答案。
-
如果你对了解一种新的前沿提示框架感兴趣,不要错过Anand Subramanian对 Medprompt 的详细且实用的介绍。
-
在她最新的专注于 LLM 的教程中,Yanli Liu介绍了一种结合两种新技术的对齐优化方法:使用 ORPO(比值比优化)进行表示微调。
-
机器学习模型日益增长的环境足迹是一个及时且至关重要的话题;Sydney Nye提供了一份以可持续实践为中心的务实指南,旨在模型训练和部署过程中减少碳足迹和水足迹。
-
在音乐分析和人工智能的交集处工作,Emmanouil Karystinaios带我们走进他的研究,介绍了灵感来自感知的图卷积在音乐理解任务中的应用。
-
Hesam Sheikh超越了代理人工智能系统所产生的炒作,提供了一份关于构建 AI 代理团队的详细实践教程,帮助改进和定制求职材料。
-
当你给 LLM 的提示包含矛盾指令时会发生什么?Yennie Jun在她的“AI 认知失调”实验中探讨了这个有趣的难题。
感谢您支持我们作者的工作!我们喜欢发布新作者的文章,如果你最近写了一篇有趣的项目 walkthrough、教程或关于我们核心话题的理论反思,别犹豫,与我们分享吧。
直到下一个变量,
TDS 团队
Pandas 2.2 新特性
关于新版本最有趣的内容
·发表于Towards Data Science ·5 分钟阅读·2024 年 1 月 30 日
--

由Zoe Nicolaou拍摄,图片来源于Unsplash
pandas 2.2 于 2024 年 1 月 22 日发布。让我们看看这一版本带来了哪些新特性,以及它如何帮助我们改善 pandas 工作负载。它包含了一系列提升用户体验的改进。
pandas 2.2 带来了一些额外的改进,这些改进依赖于 Apache Arrow 生态系统。此外,我们还增加了一些弃用内容,为 pandas 3.0 中将“写时复制”(Copy-on-Write)设置为默认选项所做的必要更改做准备。让我们深入了解这对你意味着什么。我们将详细探讨一些最重要的更改。
我是 pandas 核心团队的一员,同时也是Coiled的开源工程师,我在这里负责 Dask 的工作,包括改进 pandas 的集成。
改进的 PyArrow 支持
我们在 pandas 2.0 中引入了基于 PyArrow 的 DataFrame,并且从那时起持续改进集成,以实现与 pandas API 的无缝对接。pandas 为某些数据类型提供了访问器,支持特定的操作,例如字符串访问器,提供了许多字符串方法。过去,列表和结构体是作为 NumPy 对象数据类型表示的,这使得处理这些数据类型变得非常繁琐。现在,Arrow 数据类型后端为列表和结构体提供了定制的访问器,使得处理这些对象变得更加简便。
让我们看一个例子:
import pyarrow as pa
series = pd.Series(
[
{"project": "pandas", "version": "2.2.0"},
{"project": "numpy", "version": "1.25.2"},
{"project": "pyarrow", "version": "13.0.0"},
],
dtype=pd.ArrowDtype(
pa.struct([
("project", pa.string()),
("version", pa.string()),
])
),
)
这是一个包含字典的系列,每一行都有一个字典。以前,这只有在使用 NumPy 对象 dtype 时才可行,并且从这些行中访问元素需要进行迭代。现在,struct 访问器使得可以直接访问某些属性:
series.struct.field("project")
0 pandas
1 numpy
2 pyarrow
Name: project, dtype: string[pyarrow]
下一版本将带来基于 Arrow 类型的 CategoricalAccessor。
集成 Apache ADBC 驱动程序
历史上,pandas 依赖 SqlAlchemy 从 Sql 数据库中读取数据。这种方式非常可靠,但速度较慢。SqlAlchemy 按行读取数据,而 pandas 是列式布局,这使得读取数据并将其移动到 DataFrame 中的过程比必要的更慢。
来自 Apache Arrow 项目的 ADBC 驱动程序使用户能够以列式布局读取数据,这带来了巨大的性能提升。它将数据读取并存储到 Arrow 表中,该表用于转换为 pandas DataFrame。如果设置 dtype_backend="pyarrow",则可以实现零拷贝转换。
让我们看一个例子:
import adbc_driver_postgresql.dbapi as pg_dbapi
df = pd.DataFrame(
[
[1, 2, 3],
[4, 5, 6],
],
columns=['a', 'b', 'c']
)
uri = "postgresql://postgres:postgres@localhost/postgres"
with pg_dbapi.connect(uri) as conn:
df.to_sql("pandas_table", conn, index=False)
# for round-tripping
with pg_dbapi.connect(uri) as conn:
df2 = pd.read_sql("pandas_table", conn)
ADBC 驱动程序当前支持 Postgres 和 Sqlite。如果你使用 Postgres,强烈建议切换到这个驱动程序,它比 SqlAlchemy 快得多,并且完全避免了通过 Python 对象的回程,从而更可靠地保留了数据库类型。这是我个人最为激动的功能。
将 case_when 添加到 pandas API 中
从 Sql 转到 pandas,用户通常会错过 case-when 语法,它提供了一种简单清晰的方式来有条件地创建新列。pandas 2.2 添加了一个新的 case_when 方法,它是在 Series 上定义的。其操作方式类似于 Sql。
让我们看一个例子:
df = pd.DataFrame(dict(a=[1, 2, 3], b=[4, 5, 6]))
default=pd.Series('default', index=df.index)
default.case_when(
caselist=[
(df.a == 1, 'first'),
(df.a.gt(1) & df.b.eq(5), 'second'),
],
)
该方法接受一个条件列表,这些条件将按顺序进行评估。然后在条件评估为 True 的行中,用这些值创建新对象。该方法应该能显著简化我们创建条件列的工作。
Copy-on-Write
Copy-on-Write 最初在 pandas 1.5.0 中引入。此模式将在 3.0 版本中成为默认行为,希望这是下一个 pandas 发布版本。这意味着我们必须将代码调整到符合 Copy-on-Write 规则的状态。pandas 2.2 引入了操作的弃用警告,这些操作会改变行为。
df = pd.DataFrame({"x": [1, 2, 3]})
df["x"][df["x"] > 1] = 100
这将引发 FutureWarning。
FutureWarning: ChainedAssignmentError: behaviour will change in pandas 3.0!
You are setting values through chained assignment. Currently this works in certain cases, but when
using Copy-on-Write (which will become the default behaviour in pandas 3.0) this will never work to
update the original DataFrame or Series, because the intermediate object on which we are setting
values will behave as a copy. A typical example is when you are setting values in a column of a
DataFrame, like:
df["col"][row_indexer] = value
Use `df.loc[row_indexer, "col"] = values` instead, to perform the assignment in a single step and
ensure this keeps updating the original `df`.
我写过 一篇早期的文章,详细介绍了如何迁移代码以及应该期待什么。Copy-on-Write 还新增了一种警告模式,用于对所有改变行为的情况发出警告:
pd.options.mode.copy_on_write = "warn"
对大多数 pandas 用户来说,这些警告大多只是噪音,这也是为什么它们被隐藏在一个选项后面的原因。
pd.options.mode.copy_on_write = "warn"
df = pd.DataFrame({"a": [1, 2, 3]})
view = df["a"]
view.iloc[0] = 100
这将引发一条长警告,解释将会发生什么变化:
FutureWarning: Setting a value on a view: behaviour will change in pandas 3.0.
You are mutating a Series or DataFrame object, and currently this mutation will
also have effect on other Series or DataFrame objects that share data with this
object. In pandas 3.0 (with Copy-on-Write), updating one Series or DataFrame object
will never modify another.
这的简要总结是:更新view将永远不会更新df,无论使用何种操作。大多数情况下,这与大多数人无关。
我建议启用该模式并简要查看警告,但如果你确信自己没有依赖于同时更新两个不同对象,可以不必过于关注这些警告。
我建议查看写时复制迁移指南,该指南更详细地解释了必要的更改。
升级到新版本
你可以通过以下方式安装新的 pandas 版本:
pip install -U pandas
或者:
mamba install -c conda-forge pandas=2.2
这将在你的环境中提供新版本。
结论
我们已经查看了一些改进,这些改进将提升 pandas 在某些方面的性能和用户体验。最令人兴奋的新特性将在 pandas 3.0 中推出,其中默认启用写时复制(Copy-on-Write)。
感谢阅读。欢迎随时联系,分享你的想法和反馈。
HNSW 的故事是什么?
探索使用层次化可导航小世界(HNSW)进行快速最近邻搜索的路径
·发表于数据科学之路·阅读时间 13 分钟·2024 年 2 月 25 日
--

由 DALL·E 2 创作的图像,提示词为“由点和线条连接的层次网络的明亮抽象表现主义画作”。
层次化可导航小世界(HNSW)已经成为近似最近邻搜索中最具性能的算法之一。不过,HNSW 略显复杂,且相关描述通常缺乏完整且直观的解释。本文将带您走过 HNSW 概念的历史,帮助解释“层次化可导航小世界”到底意味着什么,以及它为何如此有效。
目录
-
近似最近邻搜索
-
小世界
-
可导航小世界
-
层次化可导航小世界
-
总结
-
附录
-
改进的搜索
-
HNSW 搜索与插入
-
改进的插入
-
-
参考文献
近似最近邻搜索
机器学习的一个常见应用是最近邻搜索,即找到与目标最相似的项*——例如,推荐与用户偏好相似的项,或者搜索与用户查询相似的项。
简单方法是计算每个项与目标的相似度,并返回最接近的项。然而,如果项的数量非常庞大(可能有百万个),这一方法将会很慢。
取而代之的是,我们可以使用一种叫做索引的结构来大大加速查找过程。
然而,这里存在一个权衡。与简单方法不同,索引只给出近似结果:我们可能无法检索到所有的最近邻(即召回率可能低于 100%)。
存在多种不同类型的索引(例如局部敏感哈希;倒排文件索引),但 HNSW 在各种数据集上表现特别有效,能够在保持高召回率的同时实现高速。
*通常,项目被表示为 嵌入,这些嵌入是由机器学习模型生成的向量;项目之间的相似性对应于嵌入之间的距离。这篇文章通常会讨论向量和距离,尽管一般来说,HNSW 可以处理任何类型的项目,只要它们具有某种相似度度量。
小世界

小世界实验的示意图。
小世界现象在斯坦利·米尔格伦(Stanley Milgram)的“小世界实验”中得到了著名的研究[1]。
参与者收到一封信,信中包含一个随机选择的目标个体的地址及其他基本信息,以及实验的说明。如果他们不太可能个人认识目标,他们被指示将信转交给更可能认识目标的人。
令人惊讶的结论是,信件通常只被转发约六次便能到达目标,证明了著名的“六度分隔”理论——任何两个人通常可以通过一小链条的朋友相互连接。
在图论的数学领域,图是一组点,其中一些点是相互连接的。我们可以将社交网络看作是一个图,其中人们是点,友谊是连接。小世界实验发现,这个图中的大多数点对是通过短路径连接的,路径上的步骤很少。(从技术上讲,这是指图的直径很小。)

小世界的示意图。大多数连接(灰色)是局部的,但也有长程连接(绿色),这些长程连接在各个点之间创建了短路径,例如指示箭头的 A 和 B 之间的三步路径。
拥有短路径本身并不那么令人惊讶:大多数图都有这一特性,包括通过随机连接点对所创建的图。但社交网络并非随机连接,它们是高度局部化的:朋友通常住得很近,而且如果你认识两个人,他们很可能也彼此认识。(从技术上讲,这是指图具有高聚类系数。)小世界实验令人惊讶的地方在于,尽管连接通常是短程的,但两个遥远的点之间竟然只通过一条短路径相隔。
在像这样的情况下,当一个图有许多局部连接,但也有短路径时,我们称这个图为小世界。
另一个很好的小世界例子是全球机场网络。同一地区的机场之间高度互联,但通过利用主要枢纽机场,通常可以在几个停靠点之间完成长途旅行。例如,从英国曼彻斯特到日本大阪的旅行通常从曼彻斯特到伦敦的短途航班开始,然后是从伦敦到东京的长途航班,最后是从东京到大阪的另一个短途航班。长程枢纽是实现小世界特性的常见方式。
另一个具有小世界特性的图的有趣例子是生物神经网络,比如人脑。
可导航的小世界
在小世界图中,我们可以在几步之内快速到达目标。这为最近邻搜索提供了一个有前景的想法:也许如果我们以某种方式在向量之间创建连接,使其形成一个小世界图,我们可以通过从任意“入口点”向量开始,然后通过图进行导航快速找到靠近目标的向量。
这一可能性由克莱因伯格探索过[2]。他指出,短路径的存在并不是米勒实验中唯一有趣的地方:更令人惊讶的是,人们能够找到这些短路径,而无需使用任何关于图的全局知识。相反,人们是遵循一种简单的贪心算法。在每一步中,他们检查每一个直接连接,并将信息发送到他们认为最接近目标的连接。我们可以使用类似的算法来搜索连接向量的图。

贪心搜索算法的示意图。我们正在寻找距离目标 X 最近的向量。从入口点 E 开始,我们检查与 E 相连的每个向量到 X 的距离(由从 E 出发的箭头表示),并前往最近的一个(由从 E 出发的红色箭头表示)。我们在后续的向量上重复这个过程,直到到达 Y。由于 Y 没有比 Y 本身更接近 X 的连接,我们停止并返回 Y。
克莱因伯格想知道这个贪心算法是否总能找到一条短路径。他进行了简单的小世界模拟,在这些模拟中,所有点都连接到它们的邻居,并且在随机点之间创建了额外的长连接。他发现,贪心算法只会在特定条件下找到短路径,这些条件取决于长程连接的长度。
如果长距离连接过长(例如当它们连接完全随机位置的点对时),贪心算法可能会通过长距离连接迅速到达目标的大致区域,但之后这些长距离连接就无用,路径必须通过局部连接来进一步靠近目标。另一方面,如果长距离连接过短,则需要太多步骤才能到达目标区域。
然而,如果长距离连接的长度刚好合适(准确来说,如果它们是均匀分布的,所有长度的概率相等),则贪心算法通常可以在特别少的步骤中到达目标的邻域(更具体地说,这个步骤数与 log(n) 成比例,其中 n 是图中的点数)。
在这种贪心算法能够在少数步骤内找到目标的情况下,我们称这种小世界为 可导航 小世界(NSW)。
NSW 听起来像是一个理想的向量索引,但对于高维复杂空间中的向量,如何实际构建一个 NSW 还不清楚。幸运的是,Malkov 等人 [3] 发现了一种方法:我们每次随机选择一个向量插入图中,并将其连接到已插入的少数 m 个最近邻。

构建 NSW 的示意图。向图中随机插入向量,并将其连接到最接近的 m = 2 个已插入向量。注意,最早插入的向量形成长距离连接,而后插入的向量形成局部连接。
这种方法非常简单,无需对向量在空间中的分布进行全局理解。它也非常高效,因为我们可以利用已构建的图来执行每个向量的最近邻搜索。
实验确认这种方法能产生一个 NSW。由于最早插入的向量是随机选择的,它们通常相隔较远。因此,它们形成了小世界所需的长距离连接。虽然为什么小世界是可导航的并不那么显而易见,但随着我们插入更多向量,连接会逐渐变短,因此可以推测连接长度的分布会相当均匀,如所要求的那样。
层次化可导航小世界
可导航小世界在近似最近邻搜索中表现良好,但进一步的分析揭示了可以改进的地方,这促使 Markov 等人 [4] 提出了 HNSW。
一个典型的 NSW 路径从入口点到目标分为两个阶段:“缩小”阶段,连接长度从短到长;以及“放大”阶段,连接长度发生相反的变化。
第一个简单的改进是使用一个长程中心节点(例如第一个插入的向量)作为入口点。这样,我们可以跳过缩小阶段,直接进入放大阶段。
其次,尽管搜索路径较短(步骤数与log(n)成正比),但整个搜索过程并不十分快速。在路径上的每个向量处,贪婪算法必须检查每个连接的向量,计算它们与目标的距离,以选择最近的一个。虽然大多数本地连接的向量只有少数连接,但大多数长程中心节点有很多连接(同样与log(n)成正比);这是有道理的,因为这些向量通常在构建过程中较早插入,并且有更多机会与其他向量连接。因此,在搜索过程中总的计算量相当大(与log(n)²成正比)。
为了改进这一点,我们需要限制在每个中心节点检查的连接数量。这引出了 HNSW 的主要思想:明确区分短程连接和长程连接。在搜索的初始阶段,我们只考虑中心节点之间的长程连接。一旦贪婪搜索找到了接近目标的中心节点,我们就切换到使用短程连接。

通过 HNSW 进行搜索的示意图。我们正在搜索最接近目标 X 的向量。长程连接和中心节点是绿色的;短程连接是灰色的。箭头显示了搜索路径。从入口点 E1 开始,我们在长程连接中执行贪婪搜索,达到 E2,这是离 X 最近的长程中心节点。从那里我们继续在短程连接中进行贪婪搜索,最终到达 Y,这是离 X 最近的向量。
由于中心节点的数量相对较少,因此它们应该检查的连接较少。我们还可以在构建索引时,明确为每个向量强加一个长程连接和短程连接的最大数量。这将导致较快的搜索时间(与log(n)成正比)。
将短程和长程连接分开的思想可以推广到包括多个中间层次的连接长度。我们可以将其可视化为一个层次结构,每一层只使用下层中部分向量进行连接。


左:一个具有三种连接长度的 HNSW 示意图——短程连接是灰色的,长程连接是绿色的,最长的连接是红色的。E是入口点。右:将 HNSW 可视化为三层堆栈。虚线表示同一向量在下层的位置。*
最佳层数(以及每个向量最大连接数等其他参数)可以通过实验找到;HNSW 论文中也提出了一些启发式方法。
顺便提一下,HNSW 还推广了一种名为 跳表 的数据结构,它可以快速搜索排序的一维值(而不是多维向量)。
构建 HNSW 使用的思想与 NSW 类似。向量是逐个插入的,通过连接随机向量创建长距离连接——尽管在 HNSW 中,这些向量是在整个构建过程中随机选择的(而在 NSW 中,它们是按随机顺序插入的第一个向量)。
精确来说,每当插入一个新的向量时,我们首先使用随机函数选择它将出现的最高层级。所有向量都出现在最底层;其中一部分向量也出现在第一层;其中一部分向量又出现在第二层,依此类推。
与 NSW 类似,我们将插入的向量连接到它在每个层次中出现的 m 个最近邻;我们可以利用迄今为止构建的索引高效地搜索这些邻居。随着向量在更高层次上的稀疏性增加,连接通常变得更长。
总结
这部分完成了关于导致 HNSW 的主要思想的讨论。总结如下:
小世界是一个连接局部点的图,但也有短的路径连接远距离的点。这可以通过具有长距离连接的枢纽来实现。
以正确的方式构建这些长距离连接,会导致一个小世界图,具有可导航性,这意味着贪心算法可以迅速找到最短路径。这使得快速的最近邻搜索成为可能。
构建连接的一种方法是按随机顺序插入向量,并将其连接到最近邻。然而,这会导致具有大量连接的长距离枢纽,并导致较慢的搜索时间。
为了避免这一点,更好的方法是通过选择随机向量作为枢纽,单独构建不同长度的连接。这就给我们带来了 HNSW 索引,显著提高了最近邻搜索的速度。
附录
上面的帖子概述了 HNSW 索引及其背后的思想。本附录讨论了 HNSW 算法的一些额外有趣细节,供那些寻求完整理解的读者参考。更多细节和伪代码请参见参考文献。
改进的搜索
可导航的小世界方法只能为最近邻搜索提供近似结果。有时,贪心搜索算法会在找到距离目标最近的向量之前停止。这种情况发生在搜索路径遇到“虚假的局部最优”时,即向量的直接连接都离目标更远,尽管在图的其他地方可能有一个更接近的向量。
可以通过从不同的入口点执行多个独立的搜索来改进此过程,这些搜索的结果可以为我们提供几个好的 候选 向量作为最近邻。然后我们计算所有候选向量与目标的距离,并返回最近的一个。
如果我们想找到多个最近邻(例如 k 个),我们可以首先通过添加它们的所有直接连接来扩展候选集,然后计算与目标的距离并返回最近的 k 个。
这种简单的候选查找方法存在一些不足。每个贪婪搜索路径仍然有可能陷入一个错误的局部最优解;通过探索每个向量的直接连接之外的区域可以改进这一点。此外,搜索路径可能会在结束时遇到几个靠近目标的向量,但这些向量不会被选为候选(因为它们不是路径中的最终向量或它的直接连接之一)。
与其独立地跟踪多个贪婪路径,更有效的方法是跟踪一个 向量集,并以贪婪的方式更新整个集合。
精确来说,我们将维护一个集合,包含到目前为止遇到的离目标最近的向量以及它们到目标的距离。该集合最多包含 ef 个向量,其中 ef 是期望的候选数量。最初,该集合包含入口点。接下来,我们通过贪婪过程继续,评估集合中的每个向量并检查它的连接。
集合中的所有向量最初标记为“未评估”。在每一步,我们评估离目标最近的未评估向量(并将其标记为“已评估”)。评估向量意味着检查其每个连接向量,计算该向量与目标的距离,如果比集合中某些向量更近,就将其插入集合(并将最远的向量移出集合,如果集合已达到最大容量)。(我们还会追踪已经计算过距离的向量,以避免重复工作。)
该过程在集合中的所有向量都被评估过并且没有插入新的向量时结束。最终的集合作为候选集返回,我们可以从中选取离目标最近的向量或 k 个最近的向量。
(注意,对于 ef = 1,该算法只是基本的贪婪搜索算法。)
HNSW 搜索与插入
上述描述了一个 NSW 的搜索算法,或 HNSW 的单层搜索算法。
要搜索整个 HNSW 结构,建议的方法是从顶部层开始,在每一层使用基本的贪婪搜索找到最近邻,直到到达感兴趣的层,此时我们使用层搜索算法并考虑多个候选。
对于在完成的索引上进行 k 最近邻搜索(包括 k = 1),这意味着使用基本的贪心搜索,直到到达底层,此时我们使用带有 ef = efSearch 候选向量的层搜索算法。efSearch 是一个需要调优的参数;较高的 efSearch 值虽然速度较慢,但精度更高。
对于将向量插入 HNSW,我们使用基本的贪心搜索,直到找到新向量出现的第一层。在这一层,我们使用带有 ef = efConstruction 候选向量的层搜索来寻找 m 个最近邻。我们还将这些候选向量作为进入下一层继续处理的入口点。
改进的插入方法
NSW 引入了一种简单的构建图的方法,其中每个插入的向量都与其 m 个最近邻连接。虽然这种选择连接的方法也适用于 HNSW,但引入了一种改进的方法,显著提升了生成的索引的性能。
一如既往,我们首先找到 efConstruction 候选向量。然后我们按照与插入向量的距离从小到大遍历这些候选向量并连接它们。然而,如果某个候选向量距离一个新连接的候选向量比插入向量还近,我们就跳过它而不进行连接。我们在连接了 m 个候选向量后停止。
这个想法是,我们已经可以通过新连接的候选向量从插入向量到达该候选向量,因此增加一个直接连接是没有必要的;连接一个更远的点会更好。这样可以增加图中连接的多样性,帮助连接相邻的向量簇。
参考文献
[1] J. Travers 和 S. Milgram, 小世界问题的实验研究(1969),《社会计量学》
[2] J. Kleinberg, 小世界现象:一种算法视角(2000),《第三十二届年度 ACM 计算理论会议论文集》
[3] Y. Malkov, A. Ponomarenko, A. Logvinov 和 V. Krylov, 基于可导航小世界图的近似最近邻算法(2014),《信息系统》,第 45 卷
(有几篇类似的论文;这篇是最新且最完整的,并且包含了更先进的 k 最近邻搜索算法。)
[4] Y. Malkov 和 D. Yashunin, 使用分层可导航小世界图进行高效且稳健的近似最近邻搜索(2016),《IEEE 模式分析与机器智能学报》
所有图片均由作者创作,可以在注明出处的情况下免费使用。
R 平方的问题在哪里(以及如何修复它)
即使你认为自己在使用样本外的 R 平方,你也没有做到。原因如下:
·发表于Towards Data Science ·阅读时长:10 分钟·2024 年 8 月 7 日
--

[图片来源:作者]
R-Squared是评估回归模型的最流行指标之一。它在任何统计学课程中都有教授,也是Scikit-learn中实现的评估指标之一。
然而,这一指标的可靠性已经引发了一些疑问。在卡内基梅隆大学的课程笔记中,科斯马·沙利齐教授声称R-Squared是无用的。
那么,我们应该完全抛弃R-Squared吗?
我不这么认为。
我承认这个指标有一个主要缺陷,但我也认为我们不应忽视它的优点。在本文中,我将解释R-Squared的问题,并提出一种修改方法,使其完全可靠。
R 平方的深层含义是什么?
要理解R-Squared的问题,我们首先需要了解它的含义。我指的是深层含义,而不是大多数资源中随意定义的含义。
让我们从一个例子开始。假设我们有一个预测模型(“模型 A”),用于预测房屋的售价。
当 AI 艺术家代理竞争时
生成艺术实验的洞察
·发表于 Towards Data Science ·阅读时间 9 分钟·2024 年 8 月 27 日
--
介绍
本文探讨了一个由 AI 驱动的艺术竞赛框架的发展及其创建过程中的洞察。竞赛使用了由提示和迭代反馈指导的 AI 艺术家,通过 P5.js 生成创新且引人入胜的代码艺术。然后,AI 评委选择获胜者晋级到下一个竞赛轮次。这个项目不仅突出了构建竞赛框架的过程和工具,也反思了这些方法的有效性,以及它们还可以如何应用于其他领域。

这是一幅由 AI 艺术家创作的喜爱艺术作品拼贴。(拼贴图由作者制作)
背景
在生成 AI 之前,就有生成艺术。如果你不太熟悉,生成艺术基本上是利用代码创建算法驱动的可视化,这些可视化通常会包含某些随机因素。想了解更多,我强烈建议你查看 #genart 在 X.com 上 或访问 OpenProcessing.org。
我一直觉得生成艺术是一个迷人的媒介,它通过代码提供了独特的创造性表达方式。作为一个长期以来一直喜爱 P5.js 及其前身 Processing 框架的人,我非常欣赏生成艺术的美感和潜力。最近,我开始使用 Anthropic 的 Claude 来帮助排查问题和创作艺术作品。借助它,我破解了一个多年以前放弃的算法,成功地创建了带有不错漩涡效果的流场。
看到 AI 在代码艺术创作中的强大能力和快速迭代,激发了我一个有趣的想法:如果我能创建 AI 驱动的艺术竞赛,让 AI 们围绕一个特定的艺术主题进行创作和竞争呢?这个将 AI 代理相互对抗的创意竞赛的构思很快成型,使用了一个简单的 Colab 笔记本来组织比赛,AI 艺术家生成代码艺术,AI 评委则决定获胜者。在这篇文章中,我将带你了解这一实验的过程、挑战和启示。
设置 AI 艺术竞赛
在我的项目中,我创建了一个 Google Colab 笔记本(共享代理 AI 艺术.ipynb),所有的 API 调用、艺术家和评委都在此进行协同。我通过 API 使用了 OpenAI 的 GPT-4o 模型,并在 OpenAI Assistant Playground 中定义了一个 AI 艺术家“助手”模板。选择了 P5.js 作为编码框架,允许 AI 艺术家使用 JavaScript 生成草图,并将其嵌入到 HTML 页面中。
笔记本为每一轮的艺术家提供提示,并启动艺术家创作。艺术家的输出经过评判,并给予反馈,最终选出一个获胜者,淘汰掉落选的艺术家。这个过程重复进行三轮,最终选出一位获胜艺术家。

AI 艺术竞赛的整体技术流程。(图示由作者提供)
竞赛:逐轮分析
竞赛结构非常简单,第一轮有八位 AI 艺术家,每两位艺术家一组进行对决。获胜者晋级第二轮,四位艺术家继续角逐,最后两位艺术家在第三轮对决,最终决出冠军。虽然我曾考虑过更复杂的比赛形式,但为了这次初步探索,我决定保持简单。
在第一轮中,八位艺术家创作并提交他们的初步作品。然后,AI 评委使用带视觉 API 的 OpenAI ChatGPT-4o 对每对作品进行评估,依据原始提示进行评价。这个过程让评委可以提供反馈,并为每一对作品选出一个赢家,从而推动竞赛进入下一轮。
在第二轮中,剩下的 AI 艺术家收到反馈后,需要在之前的作品基础上进行迭代。结果不一——一些艺术家明显进步,而另一些则遇到了困难。通常,迭代过程会导致更加精细和复杂的作品,艺术家们根据评委的批评作出回应。
最终回合尤为有趣,因为两位决赛选手需要在之前的作品基础上进行创作,争夺冠军位置。AI 评委的反馈在塑造他们最终提交的作品中起到了至关重要的作用,一些艺术家表现出色,而另一些则在压力下未能发挥。

一个反映竞赛结构的赛程图。(图示由作者提供)
在接下来的章节中,我将更详细地讲解提示词、艺术家设置和评审过程。
提示词
在每场比赛开始时,我会提供一个详细的比赛提示词,艺术家将使用它生成 P5.js 代码,评审将用它来评估作品。例如:
使用 p5.js 嵌入在 HTML 中的生成艺术程序,探索递归模式复杂之美。该程序应生成一张静态图像,直观地捕捉递归中固有的无尽重复和自相似性。
视觉元素:开发在不同尺度上重复的复杂结构,如分形、螺旋或嵌套形状。使用一致的模式,并在大小、颜色和方向上做变化,增加深度和趣味性。
• 递归与重复:尝试多级递归,每个级别引入新的细节或微妙的变化,创造出视觉上引人入胜且无尽复杂的设计。
• 艺术创新:将数学精度与艺术创意相结合,突破传统递归艺术的界限。确保作品既视觉上引人入胜,又在概念上令人深思。
最终输出应为一张静态的高质量图像,展示递归模式的无尽复杂性和美感,旨在在任何比赛中脱颖而出。
配置 AI 艺术家
配置 AI 艺术家可以说是这个项目中最重要的部分,虽然也遇到了一些挑战,但总体上进展相对顺利。
一个重要的设计决策是使用 OpenAI 助手 API。创建 AI 艺术家涉及在 OpenAI Playground 中配置助手模板,然后在笔记本中维护它们各自的线程,以确保每个艺术家的连续性。使用线程功能使得每个艺术家能够记住并迭代他们之前的作品,这对创造作品的进展和演变感至关重要。

配置作为 AI 艺术家的 OpenAI 助手。(作者截图)
一个关键要求是 AI 艺术家生成 P5.js 代码,这些代码能够在我的 Python 脚本运行的无头浏览器中正常工作。在早期版本中,我要求代理使用结构化数据输出和函数调用,但这为每件作品带来了较大的延迟。最终,我删除了函数调用,并且幸运的是,即使响应中有一些评论,艺术家的响应仍然能够在无头 Python 浏览器中稳定呈现。
对助手的另一个关键改进是提供了 5 个现有的复杂 P5.js 草图作为源材料,以微调 AI 艺术家,鼓励它们进行创新,创造更复杂的输出。
这些实施选择使得 AI 艺术家相当可靠,能在大约 95%的情况下生成可用的草图,具体取决于提示内容。(这里还可以做更多的工作,以提高一致性和/或对无法解析的输出进行重试)

一个例子回应(部分),AI 艺术家除了生成 HTML 代码,还加入了一些评论,但文件仍然能够渲染并捕获画布。(截图由作者提供)
评判艺术
每一对决赛中的对阵都会进行一次 ChatGPT 4.0 调用,比较两份提交的作品并提供详细反馈。与艺术家不同,每次评审的调用都是独立的,没有之前评估的持续影响。评审会提供反馈、评分,并选出获胜者。评审的反馈会被获胜的艺术家用于进一步完善他们在后续迭代中的作品,有时会导致显著的改进,其他时候则可能产生较不成功的结果。

两张第一轮图像的示例,评审的反馈和评审的决定。(由作者提供的 Colab 输出截图)
有趣的是,AI 评审的决定并不总是与我对艺术作品的个人看法一致。我的偏好通常由美学驱动,而评审的选择则常常严格依据提示的要求,并非常字面地解读艺术作品的遵循情况。其他时候,评审似乎也会注意到更多情感上的特质(例如,“给作品增添了一种沉思的感觉”),就像大型语言模型(LLMs)经常做的那样。深入探讨评审决策背后的偏见或涌现能力会非常有趣。

这是 AI 评审合并图像的示例,评审决定了每一对比赛的赢家。在这个“黑暗壁纸完成”比赛中,右侧的图像获胜。(来自笔记本的图像输出,作者捕获)
结果与反思
我进行了多次竞赛,尝试了不同的提示并做出了调整。我的提示主题包括花卉、彩虹、波形、流场、递归、万花筒等等。最终的艺术作品令我印象深刻,艺术作品的美丽、多样性和创意让我赞叹。尽管我对结果感到高兴,但目标从来不仅仅是创作美丽的艺术作品,而是从这个过程中获取学习和启示:
-
灵感来源 — 生成的创意量和快速迭代过程使得这成为一个丰富的灵感资源。这使得竞争框架不仅仅是一个评判艺术的工具,还是一个灵感的来源。我计划在未来使用这个工具,探索不同的方式,为我自己的 P5.js 艺术作品和其他创意工作提供新的见解。
-
生成艺术作为创造力的独特衡量标准 —— “代码艺术”是一个值得探索的有趣能力。创建艺术程序的过程与我们在其他 AI 艺术工具(如 MidJourney、Dall-e、Stable Diffusion 等)中看到的截然不同。与其通过扩散算法逆向推理基于图像理解的插图,生成艺术更像是在编写虚构故事。故事的文字能够唤起读者的情感,就像生成艺术的代码一样。因为 AI 可以掌握代码,我相信我的探索只是可以实现的一个开始。
-
有什么限制吗? —— 这些代理 AI 艺术家创作的艺术作品既美丽又富有创意,但大多数作品是我以前见过的代码艺术的混合,或许还有一些偶然的幸运。获胜的艺术作品展示了复杂性和新颖性之间的平衡,尽管同时实现两者仍然是个难题。这可能是我投入的局限性,通过更多的微调、更好的提示、更频繁的互动等,我不会低估可能的创作潜力。
-
发展代理 AI 艺术家 —— 通过定义相同的 AI 艺术家模板并保持持续的线程,产生了更多独特和多样化的输出。使用代理 AI 进行头脑风暴和创意生成的这种模式具有广泛的潜力,我很高兴看到它如何被应用于其他领域。在未来的迭代中,引入具有不同配置、提示、调优、目标函数等的真正不同的 AI 会很有意思。因此,不仅是艺术作品的反馈循环,还有 AI 艺术家的反馈循环。不同编码的 AI 艺术家之间的“对决”可能会成为 AI 驱动创意的新兴前沿。

这是一幅来自“花卉”提示的有趣艺术作品……虽然我不会认为它在美学上令人愉悦,但它很新颖(在我看来)。在创作的艺术作品中,新颖的创作比好看的作品少见。(作者拍摄的 AI 艺术作品)
结论
参与这个代理 AI 艺术比赛是一次充实的经历,它将我对代码艺术的热情与探索 AI 创造力潜力的过程相结合。这个交汇点是独特的,因为输出不仅仅是文本或代码,而是由大语言模型直接生成的视觉艺术。虽然结果令人印象深刻,但它们也突显了 AI 创造力的挑战和复杂性,以及 AI 代理的能力。
我希望这个概述能激励那些有兴趣探索 AI 创造力潜力的人。通过分享我的见解和 Colab 笔记本,我旨在鼓励更多的实验和创新,在这个令人兴奋的领域中。这项项目只是一个起点,但它展示了代理 AI 和 AI 作为熟练的代码艺术家的可能性。
当平均值说谎:超越单点预测
在决策中预测完整的概率分布的必要性
·发表于Towards Data Science ·阅读时间 14 分钟·2024 年 12 月 17 日
--

有些人喜欢热咖啡,有些人喜欢冰咖啡,但没有人喜欢温吞的咖啡。然而,基于咖啡温度训练的简单模型可能会预测下次提供的咖啡应该是……温吞的。这揭示了预测建模中的一个根本问题:过度关注单点估计(例如平均值)可能会导致我们得出无意义甚至误导性的结论。
在《水晶球谬误》(Merckel, 2024b)中,我们探讨了即使是完美的预测模型也无法准确告诉我们会发生什么——它告诉我们可能发生什么以及每个结果的可能性有多大。换句话说,它揭示了一个随机变量的真实分布。尽管这种完美模型仍然是理论上的,但现实世界中的模型仍应努力接近这些真实分布。
然而,许多在企业界使用的预测模型做的恰恰相反:它们仅仅关注点估计——通常是均值或众数——而不是尝试捕捉所有可能性。这不仅仅是预测用途的问题;这一局限性还根植于许多传统机器学习算法的设计中。随机森林、广义线性模型(GLM)、人工神经网络(ANN)和梯度提升机器等,都设计为在回归任务中预测分布的期望值(均值)。在分类问题中,虽然逻辑回归和其他广义线性模型自然会尝试估计类别成员的概率,但像随机森林和梯度提升这样的基于树的方法,产生的是原始分数,这些分数需要额外的校准步骤(如等距回归或 Platt 缩放)才能转换为有意义的概率。然而在实践中,这种校准很少进行,即使有不确定性信息(即概率),也通常会被抛弃,取而代之的是单一最可能的类别,即众数。
这种过度简化有时不仅不够充分;它甚至可能导致根本错误的结论,就像我们那个平淡无奇的咖啡预测模型一样。一个典型的例子是 2008 年金融危机前用于定价担保债务凭证(CDOs)的高斯 copula 公式。通过将复杂的按揭违约关系简化为一个单一的相关系数,这个模型在多个方面存在问题,导致它灾难性地低估了同时违约的可能性(MacKenzie & Spears,2014)。这种系统性低估极端风险的现象非常普遍,以至于一些投资基金,比如纳西姆·塔勒布(Nassim Taleb)所建议的 Universa Investments,专门采用策略来利用这一点。他们认识到市场总是低估极端事件的概率和影响(Patterson,2023)。当我们将一个复杂的可能结果分布简化为一个数字时,我们失去了关于不确定性、风险和可能极端事件的关键信息,这些信息可能会对决策产生深远影响。
另一方面,一些量化交易公司正是通过正确建模这些复杂分布而取得成功。当被问及文艺复兴技术公司(Renaissance Technologies)的做法时——据称其 Medallion 基金在 1988 年至 2018 年间的年均回报率为 66%(Zuckerman,2019)——创始人吉姆·西蒙斯(Jim Simons)强调,他们仔细考虑市场风险“通常不是正态分布,分布的尾部更重,内部则较轻”(Simons,2013,47:41),突显了超越简单平均值看待问题的重要性。
那么,为什么我们在明知单点估值存在明显局限的情况下,仍然坚持使用这种方法呢?原因可能既有实际的,也有文化的。预测分布在技术上比预测单一值更具挑战性,需要更复杂的模型和更强大的计算资源。但更根本的是,大多数商业流程和工具根本没有设计来处理分布式思维。你不能将概率分布放入电子表格的单元格中,而且许多决策框架要求的是具体数字,而非可能性的范围。此外,正如 Kahneman(2011)在其关于人类决策分析中指出的那样,我们天生倾向于从具体场景而非统计分布的角度来思考——我们的直觉思维更偏好简单、具体的答案,而非概率性的答案。
让我们通过实际的住房市场数据来说明单点估值可能存在的问题,以及一些可能的建模技巧,旨在捕捉可能值的完整分布。
深入探讨物业定价
在本节中,我们使用由法国政府(gouv.fr,2024)提供的法国房地产交易(DVF)数据集,该数据集包含了法国各地房地产交易的全面记录。在这次分析中,我们关注的是销售价格、物业面积以及房间数量,时间范围为 2014 年至 2024 年。值得注意的是,我们排除了诸如地理位置等关键信息,因为我们的目标不是预测房价,而是展示预测分布的好处,避免仅依赖单点估值。
首先,我们将通过一个虚构的——但很可能是暗指现实的——案例研究,展示一种常见的机器学习技巧如何用于规划一项雄心勃勃的房地产操作。随后,我们将对此案例采取批判性立场,并提供许多人可能更倾向于采用的替代方案,以便更好地为执行交易做准备。
案例研究:Homer 与 Lisa 在房地产交易中对 AI 的依赖
Homer 和 Lisa 住在巴黎。他们预计家庭会扩大,计划卖掉两居室公寓来资助购买四居室的房产。考虑到运营和维护成本,以及他们新买的配备所有选项的高科技 Roomba 吸尘器的容量,他们认为 90 平方米的面积最适合他们。他们想估算自己需要储蓄或借款多少,以弥补出售房产所得的差额。Homer 在去年毕业于高级法语文学之前,参加了一门关于机器学习的 MOOC 课程,随后立即通过他的网络找到了一个数据科学家职位,加入了一家大型有声望的传统公司,该公司正在大力投资扩展其人工智能能力(说实话,几乎是从零开始)。如今,作为一名首席高级数据科学家,经过近一年的经验,他已经知道了不少!(他甚至 在动物园做副业,他的表现得到了关注 — Merckel, 2024a.)
经过一些谷歌搜索,他找到了政府免费提供的房地产数据集。他做了一些清理、筛选和汇总,获得了完美的材料来构建他的普通最小二乘模型(OLS,对于懂行的人来说)。现在,他可以自信地根据房间数量和面积预测巴黎地区的房价。他们那套两居室、40 平方米的公寓价值 365,116€。而一套四居室、90 平方米的公寓则达到 804,911€。这不难看出;他们必须计算差额,即 439,795€。
Homer 和 Lisa:那个玩飞镖的… 不知情地!
Homer 和 Lisa 需要储蓄/借款 439,795€ 吗?模型确实建议如此。但真的是这样吗?
也许 Homer,如果他知道的话,早就可以提供置信区间了?使用 OLS,置信区间可以通过自助法(bootstrapping)经验性估算,或者使用基于标准误差的方法进行解析计算。
此外,早在那之前,他本可以查看价格分布,并意识到默认的最小二乘法(OLS)可能不是最佳选择…

图 1:巴黎周边房地产价格(2014–2024 年): 左侧图表展示了巴黎市中心 7 公里半径内的房地产价格分布。右侧图表展示了这些价格的自然对数分布。在这两个直方图中,最后一根柱子表示价格超过 2,000,000€(或者在对数坐标系中的 log(2,000,000))的房产的累计数量。图片由作者提供。
带有长尾的右偏分布形状是很容易识别的。对于预测建模(与例如解释性建模相对),OLS 的主要关注点不一定是误差的正态性(和同方差性),而是长尾中极端值可能对模型产生不成比例的影响——OLS 最小化平方误差,这使得它对极端观测值特别敏感,尤其是那些显著偏离假定误差高斯分布的值。
广义线性模型(GLM)通过直接为响应变量指定一个分布(来自指数族),并使用“连接函数”将线性预测变量与该分布的均值连接,从而扩展了线性模型框架。虽然线性模型假设误差服从正态分布,并通过线性预测量直接估计期望响应 E(Y),但 GLM 允许不同的响应分布,并通过连接函数转化线性预测量与 E(Y)之间的关系。
让我们用一种更简单但相关的方法重新审视霍默和丽莎的情况。我们可以通过对价格取自然对数来转换数据,然后应用线性模型,而不是实现广义线性模型(GLM)。这意味着我们假设价格遵循对数正态分布(图 1 展示了价格的分布以及对数版本)。当将预测值转换回原始尺度时,我们需要通过杜安的涂抹估计量(Duan, 1983)来考虑由对数变换引入的偏差。使用这种校正偏差的对数正态模型,并将其拟合于巴黎周围的房地产数据,他们目前的 2 室 40 平米公寓估计价值 337,844 欧元,而他们目标的 4 室 90 平米房产预计价格为 751,884 欧元,因此需要额外的 414,040 欧元。
具有涂抹校正的对数正态模型特别适用于这种情况,因为它不仅反映了乘法关系,例如当房间数量或面积增加时,价格按比例增加(乘上一个因子)而不是按固定金额增加,而且还正确地考虑了反变换偏差,否则会导致价格的系统性低估。
为了更好地理解这些预测的不确定性,我们可以检查它们的置信区间。95%的自助法置信区间[400,740€ — 418,618€]表示,如果我们重复这个抽样过程多次,约 95%的这样的区间将包含真实的平均价格差异。在这个背景下,这个区间比基于标准误差的 95%置信区间更可靠,因为它不依赖于对模型的严格参数假设,如误差分布或模型规格的适用性。相反,它捕捉了观察数据的变异性和复杂性,考虑了未建模的因素和可能的偏离理想假设的情况。例如,我们的模型只考虑了房间数量和面积,而巴黎的房地产价格还受到许多其他因素的影响——如靠近地铁站的距离、建筑风格、楼层、建筑状况、当地社区动态,甚至更广泛的经济条件,如现行利率。
根据这一分析,对数正态模型为价格差提供了一个新的、可以说是更为现实的点估计值,即 414,040€。然而,尽管置信区间在统计上是严格的,但对荷马和丽莎的实际规划需求来说,可能并不是最有用的。相反,为了更好地理解可能的价格范围,并为他们的规划提供更具可操作性的洞察,我们可以转向贝叶斯建模。这种方法将使我们能够估计潜在价格差异的完整概率分布,而不仅仅是点估计和置信区间。
先验、后验与不确定性
贝叶斯建模提供了一种更全面的理解预测不确定性的方法。贝叶斯方法不仅计算单一的“最佳猜测”价格差异或置信区间,还提供了可能价格的完整概率分布。
该过程首先是表达我们对房产价格的“先验信念”——基于现有知识,我们认为哪些是合理的。在实践中,这涉及为模型参数(例如房间数量和面积的权重)定义先验分布,并指定我们认为数据是如何通过似然函数生成的(它给出的是在给定我们模型参数的情况下观察到价格的概率)。然后,我们将实际销售数据(我们的“证据”)纳入模型。通过贝叶斯定理将这些信息结合起来,我们得出“后验分布”,它提供了对参数和预测的更新视角,反映了在数据的基础上我们估计中的不确定性。这一后验分布正是荷马和丽莎真正会觉得有价值的内容。
鉴于价格数据的右偏特性,对数正态分布似乎是一个合理的假设。这一假设应通过后验预测检验进行验证,以确保它能充分捕捉数据的特征。对于参数,半高斯分布的正值约束可以反映我们的假设,即价格随着房间数量和面积的增加而上升。这些先验分布的宽度反映了可能影响的范围,捕捉了我们对房间或面积增加时价格变化的不确定性。

图 2:2 室(40m²)和 4 室(90m²)房产的预测价格分布: 左侧图展示了 2 室、40m²房产的预测价格分布,而右侧图展示了 4 室、90m²房产的预测价格分布。图像由作者提供。
贝叶斯方法与我们之前的模型方法形成了鲜明对比。尽管普通最小二乘法(OLS)和伪广义线性模型(之所以称为“伪”,是因为对数正态分布并不属于指数家族分布)为我们提供了带有一定不确定范围的单一预测值,但贝叶斯模型则为两种房产类型揭示了完整的概率分布。图 2 展示了这些预测价格分布,不仅仅给出了点估计值,还展示了每种房产类型可能的价格区间。两者分布之间的重叠区域表明,房价并非完全由面积和房间数量决定——未建模的因素如地理位置、建筑状况或市场时机,有时会使较小的房产比较大的房产更贵。

图 3:2 室(40m²)和 4 室(90m²)房产的预测价格差异分布: 该图展示了通过蒙特卡洛模拟获得的预测价格差异分布,捕捉了模型参数中的不确定性。平均价格差异约为 405,697 欧元,而中位数为 337,281 欧元,反映出分布略微向右偏斜。关键百分位数显示了较大的波动范围:第 10 百分位为-53,318 欧元,第 25 百分位为 126,602 欧元,第 75 百分位为 611,492 欧元,第 90 百分位为 956,934 欧元。标准差为 448,854 欧元,突出了这些预测中的显著不确定性。图像由作者提供。
要理解这对霍默和丽莎的情况意味着什么,我们需要估算两种房产价格差异的分布。通过蒙特卡洛模拟,我们反复从两个预测分布中抽取样本,并计算它们的差异,最终构建出图 3 所示的分布。结果令人警醒:虽然平均差异表明他们可能需要额外的 405,697€,但这一数字存在较大的不确定性。事实上,大约 13.4%的模拟情境导致负价格差异,这意味着他们在交易中有一定的机会赚到钱。然而,他们也应做好准备,面对需要更多资金的可能性——有 25%的机会他们需要超过 611,492€——10%的机会需要超过 956,934€——才能完成升级。
这种更加完整的不确定性图景,为霍默和丽莎的决策提供了比我们早期分析所提供的看似精确的单一数字更为坚实的基础。
有时候,少即是多:直接使用原始数据

图 4:2 室(40 平方米)与 4 室(90 平方米)房产价格差异的分布: 该分布通过蒙特卡洛模拟获得,随机配对实际交易的 2 室(35–45 平方米)和 4 室(85–95 平方米)房产。平均价格差异为 484,672€(中位数:480,000€),90%的百分位区间从-52,810€到 1,014,325€,显示出较大的价格差异。低于零的阴影区域表示约 6.6%的情境,在这些情境中,4 室房产的价格可能低于 2 室房产的价格。分布的右偏表明,尽管大多数价格差异集中在中位数附近,但也存在较大的差异机会,5%的情境下价格差异超过 1,014,325€。图像由作者提供。
我们可以通过直接分析类似交易,获得比依赖复杂的贝叶斯建模更清晰的见解。在巴黎周边查看房产时,我们发现了 36,265 套 2 室公寓(35–45 平方米)和 4,145 套 4 室房产(85–95 平方米),提供了丰富的实际市场行为数据集。
数据显示了显著的价格波动。两室房产的平均价格为 329,080€,中位数价格为 323,000€,90%的价格区间在 150,000€到 523,650€之间。四室房产的价格波动更大,平均价格为 812,015€,中位数价格为 802,090€,90%的价格区间从 315,200€到 1,309,227€。
通过蒙特卡洛模拟随机配对房产,我们可以估算霍默和丽莎可能面临的情况。平均价格差异为 484,672€,中位数价格差异为 480,000€,50%中间的情境价格差异在 287,488€到 673,000€之间。此外,在 6.6%的情况下,他们可能会找到一处价格低于他们 2 室房产出售价格的 4 室房产,并且赚取差价。
这种直接的方法使用实际交易而不是模型预测,避免了关于价格关系的假设,同时能够捕捉到真实市场的波动性。对于霍默和丽莎的规划,信息是明确的:虽然他们应该准备大约480,000€,但也应做好准备应对需要更多或更少的情况。理解这些可能性的范围对他们的财务规划至关重要。
这种简单的技术在这里特别有效,因为我们拥有一个包含超过40,000个相关交易的密集数据集,涵盖了我们目标房地产类别。然而,在许多依赖预测建模的情况下,我们可能会面临稀疏数据。在这种情况下,我们需要在不同的数据点之间进行插值,或超出我们可用数据的范围进行外推。这正是贝叶斯模型特别强大的地方……
最后总结
通过这些分析方法——最小二乘法(OLS)、对数正态建模、贝叶斯分析和蒙特卡洛模拟——的历程,提供的不仅仅是价格预测范围。它突出了我们如何在预测建模中以越来越高的复杂度处理不确定性。从看似精确的最小二乘估计(439,795€)到细致的对数正态模型(414,040€),再到贝叶斯和蒙特卡洛方法所提供的分布性洞察(均值分别为405,697€和484,672€),每种方法都为同一个问题提供了独特的视角。
这一进程展示了何时分布思维变得有益。对于霍默和丽莎这样高风险的一次性决策,理解全部可能性的范围提供了明显的优势。相比之下,像在线广告投放这种低个体风险的重复性决策,通常可以依赖简单的点估计。然而,在尾部风险带来重大后果的领域——例如投资组合管理或重大财务规划——建模完整分布不仅有益,而且是非常明智的。
需要认识到,现实世界的复杂性在这个案例研究中有所简化。诸如利率、时间动态、交易成本和其他变量在房地产定价中起着重要作用。我们的目标不是开发一个全面的房价预测器,而是逐步展示从简单的单点估计到完整分布的进展过程。
值得注意的是,考虑到我们主要的目的是说明这一进程——从点估计到分布式思维——我们故意保持模型的简单性。使用了 OLS 和伪GLM 实现,且没有交互项——因此没有正则化或超参数调整——并且应用了最小的预处理。虽然房间数量与表面积之间的高度相关性在一般的预测建模中并不特别成问题,但它可能会通过在后验分布中创建较难高效探索的脊形结构,影响我们贝叶斯模型中使用的马尔可夫链蒙特卡洛(MCMC)方法的采样效率(事实上,我们观察到这两个参数之间有-0.74 的强相关,尽管有效样本量保持在大约总样本的 50%,这表明我们的推理对于示范目的来说应该是足够稳定的)。特别是对于贝叶斯方法,通过定义更有信息量的先验或包括额外的协变量,仍有大量改进空间。虽然这种优化可能会产生略有不同的数值结果,但它们可能不会从根本上改变关于考虑完整分布而非点估计的重要性这一关键见解。
最后,我们必须接受即使是我们对不确定性的理解也是不确定的。我们对分布式预测的信心取决于模型假设和数据质量。这种“关于不确定性的无知”不仅挑战我们改进模型,还挑战我们透明地传达模型的局限性。
接受分布式思维不仅仅是技术上的升级——它是一种思维方式的转变。单点预测可能看起来具有可操作性,但它们常常提供一种虚假的精确感,忽视了结果的固有变异性。通过考虑所有可能性的全貌,我们能够做出更为知情的决策,并制定出更好地应对现实世界随机性的策略。
来源
参考文献
-
Duan, N.(1983 年)。《涂抹估计:一种非参数反变换方法》。美国统计学会期刊,78(383),605-610。可从
www.jstor.org/stable/2288126获取。 -
Kahneman, D.(2011 年)。《思考,快与慢》。Kindle 版。ASIN B00555X8OA。
-
MacKenzie, D., & Spears, T.(2014 年)。《杀死华尔街的公式》:高斯 Copula 和投资银行中的建模实践。社会科学研究,44(3),393-417。可从
www.jstor.org/stable/43284238获取。 -
Patterson, S.(2023 年)。《混沌之王:华尔街交易员如何在危机时代赚取数十亿》。Kindle 版。ASIN B0BSB49L11。
-
Zuckerman, G. (2019). 解决市场的男人:吉姆·西蒙斯如何启动量化革命。Kindle 版。ASIN B07NLFC63Y。
注释
-
gouv.fr (2024). 房地产价值请求(DVF),取自
www.data.gouv.fr/fr/datasets/5c4ae55a634f4117716d5656/。 -
Merckel, L. (2024a). 数据驱动还是数据偏离?来自 Hello-World 分类器的教训。取自
619.io/blog/2024/11/28/data-driven-or-data-derailed/。 -
Merckel, L. (2024b). 水晶球谬论:完美预测模型真正意味着什么。取自
619.io/blog/2024/12/03/the-crystal-ball-fallacy/。 -
Simons, J. H. (2013). 数学、常识与好运:我的生活与职业生涯。视频讲座。YouTube。
www.youtube.com/watch?v=SVdTF4_QrTM。
艺术与文字由 Loic Merckel 提供。根据 CC BY 4.0 许可授权。最初发布于 619.io。有关讨论或互动,请随时参考 LinkedIn 版本 或 Medium 版本。否则,在分享或重用时请注明 原始来源。
你什么时候使用自连接?一个实用的技巧
SQL 中级
中级 SQL 用于 ETL 开发到数据工程师的过渡
·发表于Towards Data Science ·7 分钟阅读·2024 年 3 月 17 日
--

由Campaign Creators拍摄,来自Unsplash
在 SQL 中并没有所谓的自连接。听我说完。
数据分析师和数据工程师经常需要在数据中发现不明显的模式。然而,通过使用常见的 SQL 实践,如自连接,能够生成见解并识别出这些模式。
很多初学者常常很难理解自连接。他们将 SQL 中的自连接误认为是像内连接或左连接那样的另一个命令。但自连接并不是 SQL 中的一个关键词,它也不是 SQL 中的一个命令。
自连接就像是两个表之间的普通连接(内连接/左连接/右连接/外连接)。然而,在自连接中,这两个表是相同的,但通过它们的别名表现为不同的表。
自连接在数据工程中常常被认为是一种不好的实践。人们认为使用它有风险。但在某些场景下,使用自连接是实际且解决问题的最佳方式。
让我们看几个例子:
层次化数据:
自连接在处理层次化数据时非常有用。在组织结构图中,我们可以基于经理-员工关系将一个表与自身连接,以找到员工的报告...
当机器开始思考未来:战略性人工智能的崛起
战略性人工智能
探讨战略性人工智能的进展,以及大型语言模型在大局中的角色
·发表于Towards Data Science ·阅读时间 27 分钟·2024 年 11 月 4 日
--

该图由作者使用 Canva Magic Studio 生成
序言
1997 年 5 月 11 日,纽约市。
那是一个美丽的春日,纽约市的天气格外宜人。天空湛蓝,气温逐渐上升,接近 20 摄氏度。洋基队准备在洋基体育场与堪萨斯城皇家队进行比赛,而纽约游骑兵队则在麦迪逊广场花园与魔鬼队展开对决。
一切看似平常,但聚集在曼哈顿中城 Equitable 大厦的人们即将经历一场真正独特的事件。他们将亲眼见证历史性时刻——一台计算机将在标准比赛条件下首次战胜现任世界冠军。
代表人类的是 Gary Kasparov,当时被广泛认为是世界顶级棋手。而代表机器的是 Deep Blue——一款由 IBM 开发的国际象棋计算机。在这场比赛的第六局决胜局之前,两位选手各自获得了 2.5 分。今天,胜负将最终揭晓。
Gary 最初执黑,但很快犯了一个错误,遭遇了 Deep Blue 的强烈、攻击性打法。仅仅 19 步后,比赛便结束了。Kasparov 感到气馁且承受着巨大压力,认为自己的位置已经无法维持,于是选择了认输。这一符号性时刻,被许多人视为人类与机器之间最重要的时刻之一。这个具有里程碑意义的事件标志着人工智能发展的转折点,突显了战略性人工智能的潜力和挑战。
介绍

图片由作者使用 Canva Magic Studio 生成
受最近生成式 AI 进展的启发——以及我自己对大型语言模型及其战略能力的实验——我越来越多地在思考战略性 AI。我们过去是如何尝试接近这个话题的?有哪些挑战,又有哪些问题仍需解决,才能拥有一个更具通用性的战略性 AI 代理?
作为数据科学家,我们越来越多地为客户和雇主实施 AI 解决方案。对于整个社会而言,随着与 AI 的互动日益增加,理解 AI 的发展,尤其是战略性 AI,变得至关重要。一旦我们拥有能够在战略背景中灵活操作的自主代理,这将对每个人产生深远的影响。
那么,当我们说战略性 AI时,究竟是什么意思呢?从本质上讲,战略性 AI 涉及机器做出决策,不仅考虑潜在的行动,还要预测和影响他人的反应。这是关于在复杂、不确定的环境中最大化期望结果的。
在本文中,我们将定义战略性 AI,探讨它是什么,以及自从 1997 年 IBM 的深蓝战胜卡斯帕罗夫以来它是如何发展的。我们将尝试理解一些模型的一般架构,并且还会考察大型语言模型(LLMs)如何融入其中。通过理解这些趋势和发展,我们可以更好地为一个自主 AI 代理融入社会的世界做好准备。
定义战略性 AI

图片由作者使用 Canva Magic Studio 生成
对战略性 AI 的深入讨论始于对这一话题的明确定义。
当我们在商业环境中考虑战略时,我们通常会将其与长期思维、资源分配与优化、对组织中相互依赖关系的整体理解、决策与公司使命和目标的对齐等话题联系在一起。虽然这些话题值得考虑,但在涉及 AI 和自主代理时,我通常更喜欢使用博弈论的战略定义。在这种情况下,我们将“战略性”定义为:
选择一个行动方案,最大化你的期望收益,不仅要考虑你自己的潜在行动,还要考虑他人如何回应这些行动,以及你的决策如何影响环境的整体动态。
这个定义的关键部分是,战略选择是那些并非在真空中做出的选择,而是在其他参与者的背景下做出的,这些参与者可以是人类、组织或其他 AI。这些其他实体可能有相似的或相互冲突的目标,并可能尝试采取战略行动以进一步推动自身利益。
此外,战略选择总是力图最大化期望回报,无论这些回报是以金钱、效用还是其他价值衡量标准为基础。如果我们想要融入更多传统的与战略相关的“商业”话题,可以想象我们希望最大化一家公司的价值,目标是 10 年后的情况。在这种情况下,为了制定出好的战略,我们需要采取“长期”的视角,可能还会考虑公司的“使命和目的”,以确保与战略的一致性。然而,追求这些目标只是采取战略性行动的一个结果。
游戏理论中的战略视角捕捉了战略决策的本质,因此使我们能够清晰地定义什么是战略人工智能。从定义中我们可以看出,如果一个人工智能系统或代理要进行战略性行动,它需要具备几个核心能力。具体而言,它需要能够:
-
模拟其他代理(使用预测技术或概率推理;这些代理可以是人类、人工智能或组织)。
-
基于预期效用优化行动。
-
动态适应,随着收集到其他代理策略的新信息。
当前没有一个广为人知或已发布的系统,能够在现实世界中以自主方式执行这些所有操作。然而,考虑到人工智能系统的最新进展以及大型语言模型(LLMs)的快速崛起,这种情况可能即将发生变化!
游戏理论中的其他重要概念

图片由作者使用 Canva Magic Studio 生成
在进一步讨论战略人工智能之前,回顾一些游戏理论中的概念和思想可能会有所帮助。许多关于战略人工智能的工作都基于游戏理论的概念,使用游戏理论中的定理可以揭示某些性质,使得某些游戏和情境比其他情境更容易处理。这也有助于突出游戏理论在应对现实世界情境时的局限性,并强调我们可能更适合从其他方向寻找灵感。
什么是游戏?
我们将游戏定义为一个包含三个关键组件的数学模型:
-
玩家:做出决策的个人或实体。
-
策略:每个玩家可以采取的可能行动或计划。
-
回报:每个玩家根据选择的策略所获得的奖励或结果。
这种正式的结构使得战略互动和决策过程的系统性研究成为可能。
有限游戏与无限游戏
在讨论游戏时,区分有限游戏和无限游戏也是有意义的。
有限游戏有一组固定的玩家,定义的规则和明确的终点。目标是获胜,例子包括象棋、围棋、跳棋和大多数传统棋盘游戏。
另一方面,无限游戏没有预定的结束点,规则可以随着时间的推移而演变。其目标不是赢得胜利,而是继续游戏。像商业竞争或社会演变这样的现实世界情境可以看作是无限游戏。冷战可以作为无限游戏的一个例子。它是美国及其盟国(西方)与苏联及其盟国(东方)之间长期的地缘政治斗争。这场冲突没有固定的终点,战略和“规则”随着时间变化。
子博弈
有时我们可能会在一个更大的游戏背景中找到较小的游戏。从数学角度来看,子博弈是一个自成体系的游戏,并且需要满足一些不同的标准:
-
子博弈从玩家清楚自己在游戏中的位置的时刻开始。
-
它包含了从那个时刻开始可能发生的所有行动和结果。
-
它包含了所有玩家的知识和与这些行动相关的不确定性。
我们可以通过想象一棵代表整个游戏的大树来可视化一个子博弈。子博弈就像从某个特定点(节点)开始选择这棵树的一条分支,并包括从该节点延伸出来的所有内容,同时确保该分支中的任何不确定性都被完全呈现。
子博弈背后的核心理念使它在我们讨论战略人工智能时变得有用。原因主要在于,一些玩家之间的无限游戏可能非常复杂且难以建模,而如果我们选择在这个游戏中查看较小的游戏,就能更成功地应用博弈理论分析。
回到我们以冷战作为无限游戏的例子,我们可以在这一背景中识别出几个子博弈。一些例子包括:
古巴导弹危机(1962 年):
-
玩家:美国和苏联。
-
策略:美国考虑了从外交谈判到军事入侵的各种选项,而苏联则需要决定是否撤除导弹或升级对抗。
-
回报:避免核战争,维持全球形象,以及战略军事定位。
柏林封锁与空运(1948–1949 年):
-
玩家:西方盟国和苏联。
-
策略:苏联封锁柏林以迫使盟国撤出,而盟国则必须决定是放弃这座城市还是通过空运进行供应。
-
回报:控制柏林,展示政治决心,影响欧洲的政治走向。
尽管当然非常困难和复杂,但这两个“子博弈”比整个冷战更容易分析和制定反应。它们有一组明确的玩家,有限的策略和回报,并且时间框架也更为清晰。这使得它们更适合进行博弈理论分析。
在战略人工智能的背景下,分析这些子博弈对开发能够在复杂动态环境中做出最优决策的智能系统至关重要。
双人博弈
双人博弈指的是两名玩家之间的游戏。例如,这可以是两名国际象棋玩家之间的对局,或者回到我们的冷战例子,西方与东方的对抗。只有两个玩家的博弈简化了分析,但仍能捕捉到基本的竞争或合作动态。博弈论中的许多结果都是围绕双人博弈展开的。
零和博弈
零和博弈是一类博弈,其中一个玩家的收益是另一个玩家的损失。总的支付保持不变,玩家之间是直接竞争的关系。
纳什均衡与最优行动
纳什均衡(NE)是指一组策略,在此状态下,没有玩家能够通过单方面改变自己的策略而获得额外的好处,前提是其他玩家保持不变。在这种状态下,每个玩家的策略都是对其他玩家策略的最佳回应,导致一个稳定的结果,其中没有玩家有动机偏离该策略。
例如,在剪刀石头布(RPS)游戏中,纳什均衡是所有玩家随机地以相等的概率选择石头、纸和剪刀的状态。如果你作为玩家选择纳什均衡策略,你可以确保没有其他玩家能够利用你的出招,并且在双人零和博弈中,可以证明你期望不会失败,最糟糕的结果是打平。
然而,选择纳什均衡策略可能并不总是最优策略,特别是当对手以可预测的次优方式进行游戏时。考虑一个有两个玩家 A 和 B 的场景。如果 B 玩家开始更多地选择“纸”,A 玩家可以识别这一点并增加“剪刀”的出场频率。然而,A 的这种偏离行为可能会再次被 B 利用,B 可能会改变策略,更多地选择“石头”。
关于战略人工智能的关键要点
回顾博弈论的概念,子博弈的思想对于战略人工智能尤其有用。在更大范围的上下文中找到可能的小型且更易于分析的博弈,使得应用已经知道的解决方案和解算器变得更加容易。
例如,假设你正在发展自己的职业生涯,这可以被归类为一种无限博弈,并且很难“解决”,但突然间,你获得了谈判新合同的机会。这个谈判过程为你的职业生涯中提供了一个子博弈的机会,使用博弈论概念的战略人工智能会使这个过程更加易于接近。
事实上,人类在我们的生活中已经创造了数千年的子游戏。大约 1500 年前,在印度,我们创造了如今所知的象棋的起源。象棋证明对人工智能而言是一个相当大的挑战,但也促使我们开始开发更加成熟的工具和技术,这些工具和技术可以应用于更加复杂和困难的战略性情境。
战略人工智能在游戏中的简短历史

该图像由作者使用 Canva Magic Studio 生成
游戏为开发战略性人工智能提供了一个极佳的试验场。游戏的封闭性使得训练模型和开发解决方案比开放系统更为容易。游戏的规则明确;玩家已知,回报也已知。一个重要且早期的里程碑是深蓝(Deep Blue),这台机器击败了世界象棋冠军。
早期里程碑:深蓝
深蓝是由 IBM 在 1990 年代开发的象棋超级计算机。如同序言中所述,1997 年 5 月,它通过在六局比赛中击败当时的世界象棋冠军加里·卡斯帕罗夫(Garry Kasparov)创造了历史。深蓝采用了专门的硬件和算法,能够每秒评估 2 亿个象棋局面。它将暴力搜索技术与启发式评估函数相结合,使其能够比任何先前的系统深入搜索潜在的走法序列。深蓝的特别之处在于它能够迅速处理大量局面,成功应对象棋的组合复杂性,标志着人工智能的一大里程碑。
然而,正如加里·卡斯帕罗夫在与莱克斯·弗里德曼(Lex Fridman)¹的采访中所指出的那样,深蓝更像是一台暴力机器,而非智能机器,因此很难将其归类为某种类型的智能。其搜索的核心基本上只是反复试错。说到错误,它比人类犯的错误要少得多,而根据卡斯帕罗夫的说法,这也是它很难被击败的一个特点。
复杂游戏的进展:AlphaGo
在深蓝赢得象棋比赛的 19 年后,谷歌 DeepMind 团队开发出了另一款模型,这为人工智能历史上的一个特殊时刻做出了贡献。2016 年,AlphaGo 成为第一个击败世界围棋冠军李世石的人工智能模型。
围棋是一项源自亚洲的古老棋类游戏,以其深奥的复杂性和庞大的可能局面数而著称,远远超过象棋。AlphaGo 将深度神经网络与蒙特卡罗树搜索相结合,使其能够有效地评估局面并规划走法。AlphaGo 在推理时所给予的时间越多,它的表现就越好。
这个 AI 在人类专家的对局数据集上进行了训练,并通过自我对弈进一步提升。AlphaGo 的特别之处在于它能处理围棋的复杂性,利用先进的机器学习技术在这一被认为难以被 AI 掌握的领域实现了超人类的表现。
有人可能会认为 AlphaGo 比 Deep Blue 更具智能,因为它具有深度评估棋盘状态并选择走法的卓越能力。2016 年它与李世石的对局中的第 37 手就是一个经典例子。对于熟悉围棋的人来说,那是第五路的肩膀打击,最初让包括李世石本人在内的解说员都感到困惑。但后来证明,这一手是一次精彩的走法,展示了 AlphaGo 如何探索人类玩家可能忽视和忽略的策略。
结合国际象棋与围棋:AlphaZero
一年后,Google DeepMind 再次成为新闻焦点。这一次,他们从 AlphaGo 中汲取了许多经验,创造了 AlphaZero,这是一个更具通用性的 AI 系统,掌握了国际象棋、围棋和将棋。研究人员能够通过自我对弈和强化学习来构建这个 AI,而无需依赖人类的先验知识或数据。与依赖手工制作的评估函数和广泛开局库的传统国际象棋引擎不同,AlphaZero 使用了深度神经网络和一种结合蒙特卡洛树搜索与自我学习的新算法。
该系统从仅具备基本规则开始,通过与自己对弈数百万局来学习最佳策略。AlphaZero 的特别之处在于它能够发现创造性且高效的策略,展示了一个新的 AI 模式,利用自我学习而非人类工程化知识。
结合速度与策略:《星际争霸 II》
在继续主宰 AI 领域的过程中,Google DeepMind 团队将焦点转向了一个备受欢迎的电脑游戏——《星际争霸 II》。2019 年,他们开发了一个名为 AlphaStar² 的 AI,该 AI 能够达到大师级水平,并在竞争排行榜上超过 99.8% 的人类玩家。
《星际争霸 II》是一款实时战略游戏,给 DeepMind 团队带来了几个新颖的挑战。游戏的目标是通过收集资源、建造建筑物和积累能击败对手的军队,来征服对手玩家。游戏中的主要挑战来自于需要考虑的巨大动作空间、实时决策、由于战争迷雾而导致的部分可观察性,以及长期战略规划的需求,因为某些游戏可能会持续数小时。
通过借鉴先前 AI 技术的发展,如通过自我对弈的强化学习和深度神经网络,团队成功开发出了一个独特的游戏引擎。首先,他们使用监督学习和人类游戏数据训练了一个神经网络。接着,利用该网络启动了一个能够在多代理游戏框架中自我对弈的算法。DeepMind 团队创建了一个虚拟联赛,在这个联赛中,代理能够相互探索策略,成功的策略会得到奖励。最终,他们将联赛中的策略整合成一个超级策略,这个策略能够在面对不同对手和多种策略时保持有效。正如他们所言³:
最终的 AlphaStar 代理由联盟的纳什分布组成——换句话说,就是发现的最有效策略的混合体——并运行在一台桌面 GPU 上。
深入探讨 Pluribus 和扑克
我非常喜欢玩扑克,曾在特隆赫姆(Trondheim)生活和学习时,我们每周都会举行一次现金牌局,有时气氛相当紧张!战略性人工智能克服的最后一个里程碑便是在扑克游戏中。特别是,在扑克中最流行的形式之一——6 人无限注德州扑克。这个游戏使用的是一副包含 52 张牌的常规扑克牌,游戏遵循以下结构:
-
前翻牌阶段(The Preflop): 所有玩家被发放 2 张手牌(口袋牌),这些牌只有玩家自己知道其点数。
-
翻牌(The Flop): 三张牌被翻开,所有玩家都能看到这三张牌。
-
转牌(The Turn): 又一张牌被翻开,正面朝上。
-
河牌(The River): 最后一张第五张牌被翻开,正面朝上。
玩家可以利用桌面上的公共牌和自己手中的两张牌,组成一手 5 张牌的扑克组合。在每一轮游戏中,玩家轮流下注,游戏可以在任何一轮结束,如果有玩家下注而其他玩家不愿跟注。
尽管学习起来相对简单,只需了解各种扑克牌型的排名,这个游戏通过人工智能的努力,依然证明是非常难以解决的,尽管已有数十年的尝试。
解决扑克问题的难度由多个因素导致。首先,我们面临隐藏信息的问题,因为你并不知道其他玩家手中有哪几张牌。其次,我们有一个多人对战的局面,参与的玩家越多,可能的互动和策略就呈指数级增长。第三,扑克游戏有无限制的下注规则,这导致了一个复杂的下注结构,玩家可以突然决定将自己的所有筹码押上。第四,游戏树的复杂度巨大,这源于口袋牌、公共牌和下注顺序的不同组合。此外,还有因扑克牌的随机性、虚张声势的可能性和对手建模的复杂性带来的挑战!
直到 2019 年,研究人员 Noam Brown 和 Tuomas Sandholm 才最终破解了这个难题。在《科学》期刊上发表的论文中,他们描述了一种新型的扑克人工智能——Pluribus——它成功地击败了世界上最顶尖的玩家,在 6 人无限注德州扑克中获胜。⁴他们进行了两次不同的实验,每个实验都包括 10000 局扑克,而这两个实验都清楚地显示了 Pluribus 的优势。
在第一次实验中,Pluribus 与 5 位人类对手对战,取得了每局 48 mbb(毫大盲注)的平均胜率,标准差为 25 mbb/局。(mbb/局代表每 1000 局游戏中赢得的毫大盲注数量。)48 mbb/局被认为是一个非常高的胜率,尤其是在精英扑克玩家中,这意味着 Pluribus 比人类对手更强。
在第二次实验中,研究人员让 5 个版本的 Pluribus 与 1 位人类对战。他们设置了实验,使得 2 个不同的人类各自与这 5 台机器对战 5000 局。最终,Pluribus 以每局平均 32 mbb 的优势战胜了人类,标准误差为 15 mbb/局,再次展示了其战略上的优越性。
Pluribus 的优势非常惊人,特别是考虑到研究人员必须克服的所有复杂性。Brown 和 Sandholm 提出了几种聪明的策略,帮助 Pluribus 变得超越人类,并在计算上比以往的顶级扑克 AI 更高效。他们的一些技术包括:
-
使用两种不同的算法来评估操作。他们首先使用所谓的“蓝图策略”,该策略是通过让程序与自己对战并采用一种称为蒙特卡罗反事实遗憾最小化的方法创建的。这个蓝图策略将用于第一轮投注,但在随后的投注回合中,Pluribus 会进行实时搜索,以找到更好、更精细的策略。
-
为了使其实时搜索算法更加高效,他们采用了深度限制搜索,并评估对手可能选择的 4 种不同策略。首先,他们会评估每个策略 2 步之内的效果。此外,他们只评估对手的四种不同策略,包括原始的蓝图策略、一种偏向于弃牌的蓝图策略、一种偏向于跟注的蓝图策略和一种偏向于加注的最终蓝图策略。
-
他们还使用了各种抽象技术来减少可能的游戏状态数量。例如,由于 9 高顺子本质上与 8 高顺子相似,因此可以将它们以类似的方式看待。
-
Pluribus 将连续的投注空间离散化成一组有限的“桶”,使得考虑和评估各种投注大小变得更加容易。
-
此外,Pluribus 还以一种平衡的方式调整它的策略,在每一局它所玩的手牌中,它还会考虑到在该情形下可能出现的其他手牌,并评估如何玩这些手牌,从而确保最终的玩法是平衡的,进而更难被反制。
从 Pluribus 中可以得出一些有趣的观察,但或许最有趣的是,它在面对不同对手时并不改变自己的玩法,而是发展出了一种对多种玩家都有效的稳健策略。由于许多扑克玩家认为他们必须根据不同的情况和人群调整自己的玩法,Pluribus 向我们展示了这一点是多余的,甚至可能不是最优策略,鉴于它击败了所有与之对战的玩家。
在我们对博弈论的简短探讨中,我们注意到,如果你在双人零和游戏中采用 NE 策略,那么在期望值上你可以保证不会输。然而,对于像 6 人扑克这样的多人游戏来说,并不存在这样的保证。Noam Brown 推测⁵,或许正是扑克等游戏的对抗性特征使得它仍然适合尝试用 NE 策略来接近。相反,在像《风险》这样玩家可以更多合作的游戏中,追求 NE 策略并不一定有效,因为如果你在与 6 人玩的《风险》游戏中,若你的 5 个对手决定联合起来攻击你并将你淘汰,那么你无能为力。
评估战略 AI 的趋势
总结战略 AI 在游戏中的历史,我们看到一个明显的趋势正在形成。游戏正逐渐但稳步地接近人类日常生活中所面临的现实战略情境。
首先,我们正在从双人游戏过渡到多人游戏。这可以从最初在双人游戏中的成功到 6 人扑克等多人游戏的出现中看出。其次,我们看到在掌握含有隐藏信息的游戏方面的进展。第三,我们还看到在掌握更多随机元素的游戏方面的进步。
隐藏信息、多玩家设置和随机事件在人类的战略互动中是常态,而非例外,因此,掌握这些复杂性对于实现能够在现实世界中导航的更为通用的超人类战略 AI 至关重要。
大型语言模型与战略 AI

图片由作者使用 Canva Magic Studio 生成
我最近进行了一项实验,让 LLM 相互对战桌面游戏《风险》。我的实验目标是评估 LLM 在战略环境中的表现,基本上是“开箱即用”的状态。当然,为了提供正确的上下文,给这些代理人做了大量详细的提示,但也许并不令人意外的是,LLM 的表现相当平庸。
你可以在这里找到关于这个实验的文章:
在一个模拟的风险环境中,Anthropic、OpenAI 和 Meta 的大型语言模型展示了不同的战略能力……
towardsdatascience.com
总结实验中的一些关键发现,目前一代的大型语言模型在处理像防御和识别制胜之举等基本战略概念时存在困难。它们也未能在战略上有利时消除其他玩家。
上述实验表明,尽管我们已看到 LLM 的快速进步,但它们仍然缺乏战略推理的复杂性。考虑到它们的训练数据非常通用,并且它们的构建方式,这一点并不令人惊讶。
那么,它们如何融入到战略 AI 的讨论中呢?为了理解这一点,我们需要了解 LLM 真正擅长的是什么。也许 LLM 最有前途的特性是它们能够处理和生成大量文本。现在,随着多模态模型的出现,它们还可以处理视频和音频。换句话说,LLM 非常适合与现实世界互动,无论是在人类还是其他背景下。最近,Meta 的一个 AI 团队成功将语言模型的通用语言能力与战略引擎的战略洞察结合起来。
案例研究:Cicero 与《外交》
《外交》是一款 2 至 7 人玩的策略游戏,Meta 将其描述为《风险》《扑克》和电视节目《幸存者》的混合体。玩家们从约 1900 年的欧洲地图开始,目标是控制大多数补给中心。具体来说,玩家需要控制 34 个补给中心中的 18 个才能获得胜利。通过这样做,玩家实际上支配了地图,代表了他们的国家在第一次世界大战前夕对欧洲的主导地位。
《外交》与我们迄今讨论的许多其他游戏不同之处在于它依赖于玩家之间的谈判。这是一种比扑克等游戏更具合作性的玩法形式。每位玩家在每回合之前使用自然语言与其他玩家进行交流,并制定结盟计划。当所有准备工作完成后,所有玩家同时揭示他们的计划,并执行回合。这种类型的游戏显然比大多数其他棋盘游戏更接近实际的外交和现实中的谈判,然而由于自然语言的因素,AI 很难掌握。
这种情况在 2022 年发生了变化,当时 Meta 的 AI 团队开发了 Cicero。利用最新的语言建模进展,并结合战略模块,Cicero 是一款能够“超越人类玩家平均得分两倍以上,并排名前 10%的多局游戏参与者”的游戏引擎⁶。正如 Meta 所描述的,他们的模型能够生成以策略为基础的对话,并且能够生成意识到策略的对话。
Cicero 与其他战略 AI 模型的区别
《外交》与其他一些近期取得战略 AI 进展的游戏之间存在一些关键区别。最显著的是该游戏的合作性质——与其他游戏的对抗性质相比——以及它使用的开放式自然语言格式。我认为这些差异使得该游戏更像真实的人类互动,但它也对研究人员训练支撑 Cicero 的算法提出了限制。
与 Pluribus 和 AlphaZero 不同,Cicero 并非主要通过自我对弈和强化学习进行训练。相反,Meta 团队使用了一个包含超过 125,000 场游戏和 40,000,000 条消息的数据集来帮助训练算法。他们认为,考虑到游戏中的谈判、说服和建立信任等方面,如果让 AI 通过自我对弈进行谈判,可能会出现奇怪的行为,并且可能无法捕捉到人类互动的本质。引用他们的研究文章:
“…我们发现,一个在 2p0s 版本的游戏中表现出超人类表现的自我对弈算法,在与多个真人玩家进行的游戏中表现较差,因为它学到的策略与潜在的人类盟友的规范和期望不一致。”
然而,强化学习确实被用来训练部分战略引擎,具体来说,它被用来训练 Cicero 的价值函数——即它预测行为效用所需要的函数。研究人员使用了修改版的行为克隆算法 piKL,该算法旨在最大化某一行为的预期效用,同时最小化与人类行为的差异⁶。简而言之,他们希望模型能够找到战略上合理的行动,同时尽量贴近人类的行为。
上述《外交》游戏的特点突显了一些与在真实世界人类环境中操作的战略 AI 创建相关的重要问题,在评估战略 AI 未来发展时需要加以考虑。
战略 AI 的未来

图片由作者使用 Canva Magic Studio 生成
预测未来总是充满挑战,但一种方法是利用当前的趋势并将其外推到未来情境。以下,我们探讨一些与之前讨论密切相关的主题,并评估它们如何影响战略 AI 的未来。
通用符号战略引擎与专门化模块
如果我们回顾目前战略 AI 引擎的发展轨迹,一个显著的特点是每个游戏引擎的专门化程度。尽管这些架构可能相似——就像 AlphaZero 学习如何玩多种不同的游戏——但 AI 仍然会为每个特定游戏进行数百万次自我对弈。例如,对于国际象棋,AlphaZero 进行了 4400 万局游戏,对于围棋则进行了 1.3 亿局游戏!⁷ 一个自然的问题是,我们是否应该尝试构建更通用的战略引擎,还是继续专注于为特定任务提供专门化模块?
一个通用战略引擎的目标是理解并应用跨不同情境的广泛战略原则。也许通过创造能够捕捉人类战略互动各个方面的游戏,AI 可以通过与自己对弈来学习,并制定适用于现实世界场景的战略。这种方法可能有助于 AI 进行学习的泛化,使其在各种情境中都能发挥作用。
另一方面,专门化模块是为特定情境或任务设计的 AI 系统。我们可以设想,通过结合多个专门化代理,我们能够创建一个通用的战略 AI。AI 代理可以被训练在每个特定领域中表现卓越,在最需要的地方提供深度专业知识。尽管这种方法可能限制了 AI 的泛化能力,但它能确保在特定领域内的高性能,从而更快地推动实际应用。
鉴于在合作环境中使用 AI 进行自我对弈所面临的问题——正如我们在《外交》游戏中所观察到的——以及当前的趋势似乎更倾向于为不同的战略情境提供专门化模块,因此在短期内,我们很可能会拥有针对不同情境的专门化战略模块。然而,人们也可以设想一种混合系统,在这种系统中,我们使用通用战略引擎为更广泛的主题提供洞察,而专门化模块则处理复杂的具体挑战。通过这种平衡,AI 系统可以在适应特定情境细节的同时,应用通用战略洞察。
大型语言模型弥合战略模块与现实世界应用之间的差距
大型语言模型改变了 AI 与人类语言的互动方式,提供了一种强大的方式,将战略 AI 模块与现实世界应用案例连接起来。LLMs 擅长理解和生成类人文本,使它们成为理想的中介,可以将现实世界的情境转化为战略引擎能够处理的结构化数据。正如 Meta 的 Cicero 所展示的那样,将 LLMs 与战略推理结合,允许 AI 理解人类沟通、进行谈判并在协作环境中制定行动计划。
鉴于目前向多模态模型发展的趋势,LLMs(大型语言模型)越来越能够将不仅仅是文本,而是任何现实世界的情境转化为机器可读的语法。这使得这些模型作为中介变得更加有用。
如果我们在 Cicero 开发的思想基础上继续发展,我们也许可以设想针对特定任务(如外交沟通)微调不同的语言模型——可能通过在历史外交信函上进行微调,然后训练独立的战略引擎来提出最佳行动方案。
人类-人工智能协作:半人马模型
战略性人工智能的未来不仅仅是机器接管决策过程;在过渡期内,它也涉及人类与人工智能的有效合作。这种合作伙伴关系通常被称为“半人马模型”,即将人类的直觉与人工智能的计算能力相结合。在这种模型中,人类带来创造力、伦理判断和灵活性,而人工智能系统则提供强大的数据处理能力和战略原则的一致应用。
这一模型的现实世界应用包括那些人类-人工智能团队表现优于单独工作的人类或机器的领域。例如,在国际象棋中,Garry Kasparov 提出了与人工智能合作的想法,将人类的战略洞察力与人工智能的精准计算相结合。半人马模型在国际象棋中似乎运作良好,直到程序变得非常强大。此时,人类的贡献变得毫无价值,最糟糕的情况下甚至可能是有害的。
然而,在其他更开放、更多元化、且更接近现实世界的领域,半人马模型可能是一个较好的选择。只需考虑人类与现代大型语言模型(LLM)合作的方式,这种合作有潜力显著提升生产力。
这种协作方法通过将人类判断与人工智能分析相结合,改进了决策过程,可能导致更有依据和平衡的结果。它使得在面对新情况和意外情境时能够迅速适应,因为人类可以在人工智能的支持下实时调整策略。
超越游戏的现实世界应用
游戏一直是开发战略性人工智能的一个重要测试平台,但真正的影响来自于将这些进展应用于现实世界的挑战。以下是一些例子。
近年来,一个取得巨大进展的领域是自动驾驶汽车,它们如何利用战略性人工智能安全地导航道路。自动驾驶汽车必须预测并应对其他驾驶员、行人和骑行者的行动。例如,一辆自动驾驶汽车需要预测是否有行人即将过马路,或是其他驾驶员是否即将无预警地变道。
就在今年,Waymo——一家开发自动驾驶汽车和共享出行服务的公司——在美国的三个城市启动了完全自动驾驶的出租车服务:亚利桑那州的凤凰城,加利福尼亚州的洛杉矶和旧金山。在未来几年,我们可能会看到由于战略性人工智能的进步,完全自动驾驶车辆的大幅增长。
在金融市场中,人工智能驱动的交易系统分析大量数据以做出投资决策。这些系统考虑到其他市场参与者(如交易员和机构)的可能行为,以预测市场波动。它们利用战略推理执行交易,以最大化回报并最小化风险,通常是在高度波动的环境中。
人工智能系统还通过考虑供应商、竞争者和客户的行为来优化供应链。它们可以根据预期需求和竞争者的行为,战略性地调整生产计划、库存水平和物流。例如,如果预计竞争者将推出新产品,人工智能可以建议增加库存量,以应对潜在的需求增加。
战略人工智能还被用于有效地管理能源分配。智能电网利用人工智能预测消费模式并相应调整供应。它们考虑消费者在价格信号或环境因素的影响下可能改变使用模式。人工智能战略性地分配资源,以平衡负载、预防停电并整合可再生能源。
以上例子清楚地展示了战略人工智能如何被整合到各个行业和领域中。通过考虑他人的行动,这些人工智能系统做出明智的决策,从而优化结果、提高效率,并常常提供竞争优势。随着战略人工智能的不断改进,这些系统也将不断进步,我们很可能会在许多其他领域看到它们的出现。
结论

由作者使用 Canva Magic Studio 生成的图像
自从深蓝战胜加里·卡斯帕罗夫以来,战略人工智能已经取得了长足进展。从掌握复杂的棋类游戏到进行类人化的谈判,人工智能系统越来越多地展现出战略推理能力。
在本文中,我们研究了战略人工智能的基础概念,强调了博弈论的重要性,以及该领域的一些概念如何应用于战略人工智能。我们还探讨了专门化的人工智能系统如何通过专注于狭窄领域和广泛的自我对弈,在特定游戏中实现超人类的表现。这引发了一个问题:战略人工智能的未来是发展能够广泛应用的通用符号战略引擎,还是继续开发针对特定任务量身定制的专门模块。
正如我们在 Cicero 中看到的那样,语言模型也可能在战略人工智能领域有一席之地。来自 OpenAI、Anthropic 和 Meta 等提供商的新模型使得将这些工具集成到自主智能体中比以往任何时候都更加容易,这些智能体能够利用它们将现实世界转化为人工智能系统可以处理的结构化数据。
然而,朝着能够应对现实世界复杂性的通用战略 AI 的旅程才刚刚开始。开发能够跨领域泛化、适应不可预见情况,并将伦理考量融入决策过程的系统仍然面临挑战。
感谢阅读!
想要在我发布新文章时收到通知吗? ➡️ 点击这里订阅我的新闻通讯 ⬅️。完全免费,您随时可以取消订阅!
如果您喜欢阅读这篇文章,并希望获取更多我的内容,欢迎在 LinkedIn 上与我联系 https://www.linkedin.com/in/hans-christian-ekne-1760a259/ ,或者访问我的网站 https://www.ekneconsulting.com/ ,了解我提供的部分服务。也欢迎通过电子邮件联系我,邮箱地址是 hce@ekneconsulting.com
参考文献
-
Lex Fridman. (2019 年 10 月 27 日). Garry Kasparov: Chess, Deep Blue, AI, and Putin | Lex Fridman Podcast #46 [视频文件]. Youtube.
youtu.be/8RVa0THWUWw?si=1ErCnwlAn4myoK9W -
Vinyals, O., Babuschkin, I., Czarnecki, W.M. 等人. 使用多智能体强化学习在《星际争霸 II》中达到大师级水平。Nature 575, 350–354 (2019).
doi.org/10.1038/s41586-019-1724-z -
deepmind.google/discover/blog/alphastar-mastering-the-real-time-strategy-game-starcraft-ii/ -
Brown 等人. (2019 年 8 月 30 日). 超人类 AI 在多人扑克中的应用。Science 365, 885–890, (2019).
www.science.org/doi/epdf/10.1126/science.aay2400 -
Lex Fridman. (2022 年 12 月 6 日). Noam Brown: AI 与人类在扑克和战略谈判游戏中的对抗 | Lex Fridman Podcast #344 [视频文件]. Youtube.
youtu.be/2oHH4aClJQs?si=AvE_Esb42GNGIPRG -
Meta Fundamental AI Research Diplomacy Team (FAIR)† 等人, 通过结合语言模型与战略推理,在游戏外交中实现人类级别的玩法。Science *378,1067 1074(2022).DOI:10.1126/science.ade9097,
noambrown.github.io/papers/22-Science-Diplomacy-TR.pdf -
David Silver 等人, 一种通用强化学习算法,通过自我对弈掌握国际象棋、将棋和围棋。科学杂志 362,1140–1144(2018)。DOI:10.1126/science.aar6404
storage.googleapis.com/deepmind-media/DeepMind.com/Blog/alphazero-shedding-new-light-on-chess-shogi-and-go/alphazero_preprint.pdf
欧盟 Horizon H2020 资金流向何方?
结合探索性数据分析、地理空间数据和网络科学,使用 Python 概述超过 35,000 个欧盟资助的项目。
·发表于Towards Data Science ·11 分钟阅读·2024 年 3 月 28 日
--

所有图片由作者创作。
Horizon 2020是欧盟 2014 至 2020 年的研究与创新资助计划,预算近 800 亿欧元,资助覆盖欧洲大陆的各种规模的研究项目,涉及从人类学到粒子物理学的各个主题。由于这些资助通常用于国际研究,许多项目都是由多个合作方共同参与的。
本文旨在探索一些关于 H2020 的基础数据切片,简要概述这些欧盟资金如何分配到成千上万的参与者和数百个研究主题中。为此,我将使用CORDIS共享的数据集。这些数据集是欧盟所有,授权使用Creative Commons Attribution 4.0 International (CC BY 4.0)许可证,提供了一个极好的平台,供任何对探索性数据科学或欧盟资助(或两者)感兴趣的人使用。
在本文中,我首先进行探索性分析,捕捉数据集的主要趋势,如最受资助的主题和机构。然后,我将展示如何快速使用 Python…
你应该使用哪种回归技术?
这是根据您的特定数据集,最佳回归技术的分类。
·发表于Towards Data Science ·阅读时间 11 分钟·2024 年 8 月 10 日
--

这张图由作者使用 DALL·E 制作
当你在某个领域工作足够长时间,有些课程、概念、课题和老师是你永远不会忘记的。
例如,我妈妈是教师,她总是记得那位让她第一次爱上哲学的代课老师。
我的跆拳道师傅总是记得他小时候第一次上跆拳道课时的激动心情。
我是一个机器学习工程师。从专业角度讲,机器学习是我最热爱的领域,也是我最熟悉的学科。
一堂我永远记得的课是,我的第一位机器学习教授在我本科时讲解了分类和回归的区别。一个分类任务的例子是识别电子邮件是否是垃圾邮件,根据其文本内容。一个回归任务的例子是根据房屋特征预测房价(例如面积、位置等…)。
我们定义一个特征集为一个具有 k 列和 n 行的矩阵(表格)X。在分类和回归任务中,输出是一个向量y,它…
在 Power BI 中使用 RLS 处理关系时,可能出现哪些问题?
当我们启用 RLS 时,在尝试操作关系时会有一些限制。然而,微软的文档对此主题并未提供很多细节。所以,让我们来详细拆解一下。你将会大吃一惊。
·发布于 Towards Data Science ·7 分钟阅读·2024 年 11 月 5 日
--

图片来自 Joseph Corl 在 Unsplash
引言
在 DAX 中,当我们想要操作表之间的关系时,可以使用以下其中一个函数:
我们可以在微软的文档中找到以下关于这两个函数与 RLS 之间交互的描述:

图 1 — MS 文档关于 CROSSFILTER() 和 USERELATIONSHIP() 及 RLS 的部分内容(图由作者提供)
虽然对于 CROSSFILTER() 这个句子的解释并不清楚,但对于 USERELATIONSHIP() 来说,解释要清晰得多:
这些功能在处理受 RLS 规则影响的表之间的关系时无法正常工作。
那这是什么意思呢?
让我们更详细地看一下。
数据模型与报告
对于这个示例,我使用以下数据模型:

图 2 — 用于此示例的数据模型(图由作者提供)
有三个重要的点:
-
“Date”表与“Online Sales”有三个关系:Order、Due 和 Ship Date。
-
“Geography”表与“Store”和“Customer”表有关系,但只有与“Store”表的关系是激活的。
-
“AccessByCountry”表包含一些分配了国家的用户,它控制每个用户的权限。
报告包含以下视觉对象:

图 3 — 用于示例的报告(图由作者提供)
我向“AccessByCountry”表添加了 RLS 规则:

图 4 — 向“AccessByCountry”表添加 RLS 角色,以检查当前用户(图由作者提供)
操作关系
我使用 USERELATIONSHIP()函数为两个新度量“按发货日期的在线销售总和”和“按客户位置的在线销售总和”:
Sum Online Sales by Ship Date =
CALCULATE(Sum Online Sales],
USERELATIONSHIP('Online Sales'[ShipDate]
,'Date'[Date])
)
并且:
Sum Online Sales (By Customer Location) =
CALCULATE([Sum Online Sales]
,USERELATIONSHIP('Customer'[GeographyKey]
,'Geography'[GeographyKey])
)
这样,我在计算度量时激活禁用的关系。
稍后,我将使用 CROSSFILTER()函数创建一个度量。
现在,理解哪个度量使用了哪个关系,并且它是否受 RLS 角色影响是非常重要的:
-
度量“按发货日期销售额”操作与日期表的关系。没有在这些表上设置 RLS 角色。
-
度量“按客户位置的在线销售总和”操作了“Customer”和“Geography”之间的关系。
-
RLS 角色设置在“AccessByCountry”表上,该表筛选了“Geography”表。
-
RLS 通过“Store”和“Customer”表间接筛选“Online Sales”表。
使用 USERELATIONSHIP()测试带有 RLS 的度量
现在,我通过使用“查看为”功能测试 RLS 角色:

图 5 — 使用电子邮件地址 david2@contoso.com 测试 RLS 角色,该地址具有访问德国的权限(图由作者提供)
一旦我点击确定,RLS 角色被激活,我看到了发生了什么:

图 6 — 使用操作与 Geography 表关系的度量的可视化出现错误,但使用日期表的则没有错误(图由作者提供)
如你所见,使用发货日期的度量仍然有效,因为没有 RLS 角色影响日期表。
然而,操作与“Geography”表关系的度量不再有效。请注意,错误信息中提到了两个函数(USERELATIONSHIP()和 CROSSFILTER())。
原因是 USERELATIONSHIP()函数有可能绕过 RLS 角色,使其无效。
这就是为什么不允许的原因。
有趣的是,操作与日期表的关系的度量仍然有效,即使该度量仍然影响“Online Sales”表中数据的筛选。Power BI 识别到没有 RLS 角色影响“Date”表。
使用 CROSSFILTER()测试带有 RLS 的度量
现在,让我们尝试一些不同的操作:
我想计算“按客户位置销售”的百分比,涉及所有区域的销售。
为此,我创建了以下度量:
% Sales vs all Customer Type =
[Sum Online Sales]
/
CALCULATE([Sum Online Sales]
,REMOVEFILTERS(‘Geography‘)
)
这是结果:

图 7 — 新度量的结果,用于计算所有区域和所有门店的销售比率,并且使用了客户表中的“客户类型”列(图由作者提供)
这个度量没有问题,即使在测试 RLS 角色时也能正常工作。
由于每个国家和城市只有一个门店,使用 RLS 角色时,结果总是返回 100%。
上述语法可以通过使用CROSSFILTER()函数来改变。通过将“门店”和“地理”表之间的关系设置为“无”:
% Sales vs all Geographies =
[Sum Online Sales]
/
CALCULATE([Sum Online Sales]
,CROSSFILTER('Geography'[GeographyKey]
,'Store'[GeographyKey]
,None)
)
结果与之前完全相同。
但是,在测试 RLS 角色时,我们得到一个惊喜:

图 8 — 测试 RLS 角色时新度量的结果。(图由作者提供)
这个结果很奇怪!
为了理解发生了什么,让我们添加一个没有除法部分的度量,仅使用上面度量的第二部分:

图 9 — 使用 CROSSFILTER()计算所有区域的所有销售并测试 RLS 角色时的度量结果。(图由作者提供)
当我们添加更多度量来比较结果时,我们可以看到发生了什么:

图 10 — 通过添加更多度量并不激活 RLS 测试来分析结果(图由作者提供)
如你所见,通过客户的“地理”计算出的德国销售(参见上文度量)与使用 CROSSFILTER()停用门店和地理关系的度量结果完全相同。
这意味着以下内容:
-
我使用 CROSSFILTER()禁用了“门店”和“地理”表之间的关系。
-
我测试了 RLS 角色。RLS 表达式过滤了“地理”表。
-
Power BI 检测到“地理”与“客户”之间的(非活动)关系,并激活它来计算结果。
这会导致意外、误导性和错误的结果,这必须避免。
虽然使用 USERELATIONSHIP()会导致错误,但使用 CROSSFILTER()可能会意外改变结果。
上面的例子并不太实际,因为使用 REMOVEFILTER()更加直观。我只是想给你一个例子,展示发生了什么。
解决这个问题
这种情况并不罕见,尽管我不推荐以这种方式构建数据模型。
在这种特定情况下,我建议将“地理”表中的列整合到“门店”和“客户”表中。
然而,将“AccessByCountry”表的关系添加到“门店”和“客户”表中是不可能的。这会产生歧义,Power BI 不允许这种情况。
因此,我必须复制这张表,并将它们分别连接到‘Store’和‘Customer’两个表:

图 11 — 修改后的数据模型(图由作者提供)
现在,我可以设置 RLS 角色来过滤这两个表格。我甚至可以设置不同的规则,以允许分别访问“Store”和“Customer”的地理信息。
我可以根据需要设置我的度量值,并且可以不受限制地操作筛选器。
我不再需要通过“Store Geography”或“Customer Geography”来分别设置度量值,因为我只需使用正确表格中的列。
好吧,现在我有相同的内容重复出现(两个表格中的“地理”列,分别是‘Store’和‘Customer’)。然而,它们分别在不同的表中,所以这不应该是一个问题。

图 12 — 重新建模并测试 RLS 角色后的结果(图由作者提供)
结论
使用 RLS 角色时必须特别注意。
虽然在多个场景中使用 USERELATIONSHIP() 和 CROSSFILTER() 很常见,但在数据模型中设置 RLS 角色时,可能会导致问题。
正如您上面所见,当关系不受任何 RLS 角色影响时,使用它们是没有问题的。
但一旦您尝试操作受 RLS 角色影响的关系,尤其是在使用 CROSSFILTER() 时,可能会遇到问题。
奇怪的是,您可能会得到潜在的意外结果,而不是错误信息。这些结果可能很难解释。
在我的示例中,当移除地理和客户之间的(非激活)关系时,结果是正常的。上面显示的行为是特定于我的数据模型的。
但是,正如您所见,通过对数据模型进行一些调整,所有问题都消失了。
正如我在之前的文章中多次提到的,一个好的数据模型是 Power BI 中好解决方案的基础。

图片由Jakub Żerdzicki拍摄,来自Unsplash
参考文献
就像在我之前的文章中一样,我使用了 Contoso 示例数据集。您可以从 Microsoft 免费下载 ContosoRetailDW 数据集,链接在这里。
Contoso 数据可以在 MIT 许可下自由使用,详情请见这里。
我更改了数据集,将数据移至当前日期。
[## 每当 Salvatore Cagliari 发布时,您将收到一封电子邮件。
每当 Salvatore Cagliari 发布时,您将收到一封电子邮件。通过注册,如果您还没有 Medium 账户,您将创建一个账户……
尽管 Medium 有付费墙,我仍然让我的文章对所有人开放。这样,我每个读者都能赚取一点收入,但我关闭了付费墙,你可以免费阅读我的文章。
我在晚上和周末写这些文章,这其实是很多工作。
你可以通过以下方式支持我的工作
buymeacoffee.com/salvatorecagliari
或者扫描这个二维码:

任何支持都非常感激,并帮助我找到更多时间为你创作更多内容。
非常感谢你。
谁是聊天机器人(它们对你来说意味着什么)?
人类与计算机的语言处理:第一部分
通过语言模型理解语言
·发表于 Towards Data Science ·阅读时间:12 分钟·2024 年 2 月 21 日
--
目录
1. 聊天机器人:意义范式的转变
-
1.1. 刚刚发生了什么?
-
1.2. 意义的范式
-
1.3. 芝诺与外星人
2. 人工智能:我们是如何走到今天的,未来我们该何去何从?
-
2.1. 心身问题与解决方案
-
2.2. 图灵与达尔文
-
2.3. 从搜索引擎到语言引擎
-
2.4. 过去的网络对人工智能的未来有何启示?
3. 计划
反思:
第二部分:
第三部分:
第四部分:
语言作为一种普遍的学习机器
1. 聊天机器人:意义范式的转变
1.1. 刚刚发生了什么?
我们生活在一个奇怪的时代。
故事曾由讲故事的人讲述,诗歌由诗人背诵,音乐由音乐家演奏,科学由教师传授。然后印刷和录音技术使复制成为可能,版权被发明,拥有录音和印刷设备的人成为了比音乐家和讲故事的人赚得更多的人。接着网络的出现使这一切都触手可及。现在,聊天机器人的出现让你可以要求它们写诗或解释科学,甚至将两者结合在一起。

它们甚至似乎有一点幽默感。我请同一个聊天机器人将一则来自克罗地亚语的微妙隐喻翻译成英语,她做得如此出色,以至于我产生了用法语感谢她的冲动,写下了“merci 😃”,她则转而用德语回答:“Gern geschehen! 如果你有更多问题,随时提问哦。☺️”
1.2. 意义的范式
我们与计算机的互动是逐渐演变的。自 1960 年代以来,它们就能够进行轻松的对话,且人声助手已经销售了一段时间。这不算什么大事。人类的外貌在人的观察者眼中才有意义。
但是,对于聊天机器人来说,表象似乎已经超越了眼睛。当你我聊天时,我们假设相同的词语意味着相同的事情,因为我们看到了相同的世界。如果我们谈论椅子,我们可以指着一把椅子。“椅子”这个词指的是世界中的一个物体。但聊天机器人从未见过椅子,也从未见过其他任何东西。对聊天机器人来说,词语不可能指代世界中的某个物体,因为它无法接触到这个世界。它看似知道自己在说什么,因为它是通过从网络上下载的数据进行训练的,而这些数据是由了解这些内容的人上传的。聊天机器人从未见过椅子、树、牛,或感受过疼痛,但它的对话是由曾经见过这些事物的人之间的对话进行混音的。聊天机器人的话语并非直接指向事物,而是通过人们的话语间接指向。下一张图片展示了这个如何运作。

人们使用词语来指代世界,聊天机器人则用它们来扩展短语 | 图片由作者创作
当我说“牛”时,是因为我脑海里想着一头牛。当聊天机器人说“牛”时,那是因为这个词是前文语境的可能延续。许多关于意义的理论在语言学和哲学的不同领域中得到了发展,通常被归为符号学。虽然它们在许多方面有所不同,但大多数还是坚持左图中那种理解——即意义是一个指示符号(如书面或口语词汇)与所指项目(一个物体或概念)之间的关系。虽然许多符号学家指出,词语和其他语言碎片也会被其他词语和语言碎片所指代,但整个语言生成过程能够在一个自包含的参照体系中实现,作为从词语中产生的词语旋风——正如聊天机器人所展示的那样——这是一个根本的、未曾预见的挑战。根据大多数意义理论,聊天机器人本不该存在。然而,它们却出现在了这里!
1.3. 芝诺与外星人
一种熟悉前苏格拉底哲学的聊天机器人,可能会将意义的难题与运动的难题进行比较,这个难题在 DALL-E 呈现的帕门尼德斯和芝诺的画面中得到了体现。

DALL-E 对柏拉图描述的看法:“正如帕门尼德斯主张运动不可能存在一样,芝诺正在四处踱步。”
巴门尼德斯是公元前 6 至 5 世纪的著名希腊哲学家,芝诺是他最杰出的学生。用现代的眼光看,插图似乎显示芝诺通过四处走动来反驳巴门尼德斯关于运动不可能的主张。一个经验反例。根据我们所知,芝诺并非有意反驳老师的主张,巴门尼德斯和柏拉图(在同名对话中呈现了巴门尼德斯的哲学)似乎也没有注意到巴门尼德斯对运动不可能的否定和芝诺实际走动之间的张力。哲学和学生走动的行为并未被视为属于同一领域。(具有讽刺意味的是,当约 2000 年后运动定律最终被理解时,巴门尼德斯的论证,特别是芝诺关于阿基琉斯与乌龟的故事,发挥了重要作用。)
在你否定有关语言、事物、芝诺悖论以及聊天机器人的担忧,认为这些是对我们的科学和工程项目毫无意义的哲学难题之前,请注意,那些由聊天机器人研究人员及其公司所构建和训练的自包含语言模型,完全可以由停泊在月球旁的外星飞船来构建和训练。它们可以监听、爬取我们的网站、清洗数据、构建神经网络,训练它们用完美的英语讨论我们最喜欢的话题,提供引人入胜的解释,并用鲜艳的色彩加以说明。你友好的人工智能可能由位于旧金山、平壤,或者月球上的产品工程师操作。它们不需要理解对话内容,就能构建聊天机器人。这并不是科幻小说。
但许多科幻场景随之展开。曾经有部电影描绘了月球着陆是在地球上伪造的。也许月球着陆是真的,但上一届世界杯决赛却被人工智能篡改了。或者也许它并未被篡改,但失败的队伍可以轻松证明它本可以被篡改,而胜利的队伍则更难证明它没有被篡改。当然,阴谋论者大多容易识别,但阴谋的潜在逻辑特性值得注意:大多数虚假陈述生成器是单向函数:它们的虚假陈述比生成更难以反驳。
不深入探讨人工智能科学与小说交织的细节,我们似乎可以清晰地看到,科学与小说之间、小说与现实之间的界限,可能已以前所未见的方式被突破。我们生活在一个奇异的时代。
2. 人工智能:我们是如何走到今天的,又将走向何方?
2.1. 身心问题与解决方案
机器智能的概念可以追溯到数学家艾伦·图灵,他定义并描述了环绕我们周围的计算过程。在 19 岁时,艾伦面对了思维的问题——思维从何而来,又将走向何处?——当时他突然失去了一个刚刚坠入爱河的朋友。
大约 300 年前,哲学家勒内·笛卡尔正在思考人类身体的问题。现代科学的第一步之一是他意识到,生物体和自然界的其他部分一样,是由相同的物理机制驱动的,即它们在本质上类似于当时所建造的机器。他无法解决的一个问题是:人类的身体如何产生人类的心智。他将这一问题称为心身问题。
艾伦·图灵基本上解决了心身问题。他将计算描述为可在机器中实现的过程,并通过研究结果证明这一过程能够模拟我们的推理和计算,暗示了心智可能是作为一种计算过程系统从身体中产生的。他推测某种形式的计算在我们的神经元中得以实现。他在其 1947 年的研究报告中用神经网络进行了说明。

图灵 1947 年《智能机械》备忘录中的神经网络 | 公共领域
由于这种计算过程可以在机器中实现,因此可以合理地预期它们能够催生机器智能。图灵花费了数年时间致力于建造最早的计算机之一,主要受到对智能机械潜力及其后果的思考驱动。
2.2. 图灵与达尔文
在第二次世界大战期间,图灵的理论研究被布莱切利公园的密码分析工作所取代。这部分故事似乎大家都知道。战争结束后,他拒绝了剑桥和普林斯顿的职位,接受了国家物理实验室的工作,希望能建造一台计算机并测试机器智能的概念。1947 年的备忘录中包含了似乎是神经网络训练、监督学习与非监督学习的最早提出。这些想法远远超出了当时的时代,某些部分至今仍然显得领先。该备忘录提交给了国家物理实验室的主任查尔斯·G·达尔文爵士,他是查尔斯·达尔文的孙子,也是知名的优生学家。达尔文爵士认为图灵的备忘录就像“一篇富有幻想的学童作文”。此外,达尔文爵士对图灵“邋遢”的外表心生不满,于是通过将该计算机项目置于严格的行政控制之下,扼杀了这个项目。机器智能的备忘录因此沉寂了 20 多年。图灵在他去世前的最后几年(享年 42 岁,死于咬下含有氰化物的苹果!)专注于探讨生命的计算方面。例如,是什么决定了白色牛皮上黑斑的形状?生命始终是他的兴趣所在。

11 岁的艾伦·图灵,由他的母亲在一场曲棍球比赛中所绘 | 由舍本学校提供
图灵去世后的 2 年(《智能机械》备忘录发布 9 年后),图灵的机器智能在传奇性的达特茅斯研讨会上被重新命名为人工智能(AI)。
人工智能的历史大多是朝着智能设计 智能的努力的历史。主要的研究工作是通过逻辑重建人类行为的现象,比如情感、情绪、常识等;并将这些现象实现于软件中。
与此相反,图灵认为机器智能将会 自发进化。他关于这一主题的主要已发表论述出现在《Mind》期刊上。文章以一个问题开头:“机器能思考吗?”我们现在称之为图灵测试,它被作为决定答案的一种方法。其思想是,一台能够维持对话并且与一个有思维的人类无法区分的机器,必须被认为是一台思考机器。在当前关于聊天机器人的困惑时,《Mind》期刊文章的结尾段落似乎尤为有趣:
一台学习机器的一个重要特性是,它的教师通常对于机器内部究竟发生了什么知之甚少,尽管教师仍然在某种程度上能够预测学生的行为。[...] 这与使用机器进行计算时的常规程序明显对立:在那种情况下,目标是清晰地了解机器在计算过程中的每一时刻的状态。要实现这一目标需要付出很大的努力。
机器只能做我们知道如何命令它做的事情这一观点,在这种情况下显得相当奇怪。智能行为大概是在完全规范的行为之外的表现。
聊天机器人和语言引擎的设计者和构建者们已经发布了许多关于他们的系统方法和架构的论述,但似乎像其他人一样,对其行为感到困惑。最初一些人否认了这些意外行为,但随后停止工作,开始谈论对人类的威胁。图灵预见到了这些意外行为。他的更广泛的讯息似乎是,无法了解另一个智能体的思维不是一个缺陷,而是智能的特征。这就是为什么智能体进行交流。否则,它们会把彼此视为一个对象。理解聊天机器人可能需要拓宽我们的道德视野。
2.3 从搜索引擎到语言模型
图灵没有完全预测到的一点是,智能机器将如何学习推理。他设想它们需要从教师那里学习。他没有预见到网络。网络为上传人类思维提供了空间。支持聊天机器人的语言模型是
-
不是智能设计 人工智能的结果,
-
而是自发进化 网络的结果。
像搜索引擎一样,语言模型通过爬虫从互联网获取数据。搜索引擎通过建立索引来根据排名(从数据中提取或由赞助商推动)提供链接,而语言引擎则通过基于上下文引用的文本续写预测来建立模型。计算在不断发展,但正在计算的比例保持不变:

2.4. 过去的网络对人工智能的未来有什么看法?
语言、计算、网络和人工智能的空间有许多维度。思维的概念有许多定义。如果我们考虑到我们的思维依赖于语言、计算、网络和人工智能作为工具和扩展,就像音乐依赖于乐器一样,那么说我们已经相当接近回答图灵提出的“机器能否思考”这一问题似乎是合理的。当我们使用聊天机器人时,语言模型正成为我们语言的扩展,而我们的语言也正在成为语言模型的扩展。计算机和设备已经是我们思维的一部分。我们的思维是计算机和设备的一部分。图灵关于机器能否思考的问题与人类能否思考的问题密切相关。
我们的日常生活依赖于计算机。孩子们在学习母语的同时,也学会了操作平板菜单的语言。网络吸收我们的思维,重新处理这些思维,再以适当的方式反馈给我们。我们吸收信息,重新处理它,然后反馈给计算机。这种扩展的思维通过将人类和人工网络节点连接起来处理数据。无论是节点还是网络,都无法可靠地区分它们。“我思故我在”或许可以被看作是一种幻觉。但这种扩展的思维通过那些任何参与节点都无法单独解决的问题,以及它们无法获得的非局部方法,来解决问题。它的运作产生了非局部的意识和注意力形式。语言引擎(它们自称为人工智能)是为了方便人类用户而构建的,其机器智能旨在作为人类智能的便利扩展。但基础计算的普遍性意味着机器智能包容了人类的思维,反之亦然。语言的普遍性使得智能在不同的实现下保持不变,并且反对将人工性视为“人造”这一观点。
机器不能在没有人类的情况下思考,人类也不能在没有机器的情况下思考——就像音乐家不能没有乐器演奏交响曲,乐器也无法在没有音乐家的情况下演奏交响曲一样。当然,人类发明了仅由机器演奏的音乐和仅有人声的音乐,主要是为了销售塑料珠子和珍珠,以及施加禁令。人们肯定会建立市场,销售仅由聊天机器人编写的故事,并建造禁止聊天机器人的教堂。但那与思考或智能无关。那只是人们互相做的事情。
如果搜索引擎的过去能为语言引擎的未来提供任何启示,那么聊天机器人的主要目标很快就会是说服你将钱交给引擎拥有者的赞助商。新的思维方式将很快被出售。本课程的目标是提出一个分析框架,质疑其合理性,并探索恢复其理性的方法、需求与手段。
3. 计划
我们正在努力理解语言运作的范式转变。语言曾经是口头表达的,后来也有了歌唱、演奏、书写和阅读,再后来是打字、编码、发推文等等。即使没有这些转变,语言的范式依然千变万化。盲人摸象:每个人都认为语言是其他东西。
这也许不明智,但在第2 部分与第3 部分中,我会尝试收集关于语言的各类想法,并将它们散布在毫无准备的读者和烦躁的专家面前。无论如何,我将在第四部分和第五部分中,尝试解释计算机是如何计算语言的。人们所做的显然不一样,但更显而易见的是,它不可能完全不同。
标题可能会有所变化,但我认为结构大概会是这样的:
第二部分:句法学
-
语法
-
句法类型与预组
第三部分:语义学
-
静态语义学:向量空间模型与概念分析
-
动态语义学:n 元语法、通道、依赖类型
第四部分:学习
-
神经网络与梯度下降
-
变换器与注意力机制
-
深度学习与关联
-
超越预训练与幻觉
第五部分:通用搜索
-
有效的归纳与算法概率
-
压缩与算法复杂度
-
有效的预组
-
意义的非交换几何
更多关于大象的内容请参见附录:
房间里四头大象与聊天机器人
早晨整理动物园
towardsdatascience.com
数据领域中谁做什么?数据工程师与数据科学家的角色实用介绍
数据工程师与数据科学家有什么不同的工作内容?
·发表于Towards Data Science ·14 分钟阅读·2024 年 12 月 5 日
--
明年秋天,我将在苏黎世开始攻读数据科学硕士学位——该学位有三个主题支柱:数据分析、数据工程和数据服务,正好为我们当前经济所需的机会提供了支持。但在我专注于这些领域中的某一领域之前,有一个至关重要的问题出现了:
数据工程师与数据科学家有什么不同的工作内容?
像数据分析师、数据科学家、数据工程师、机器学习工程师甚至商业分析师这样的流行词汇经常被混淆,这导致了许多困惑。当然,这些职位之间也有重叠,且不同公司中的工作方式并不完全相同。但实际上,这些角色有着明确的职责,而这些职责对于现代公司来说都至关重要:数据工程师的职责是支持数据科学家或数据分析师的工作。数据科学家利用提供的基础设施和处理过的数据来创建洞察和结论。他们负责不同的任务,但两者都是必不可少的。
在本文中,我以房价预测模型为例,展示哪些任务是由数据工程师完成的,哪些是由数据科学家完成的。不管你想在其中哪个方向发展,最好通读整个超级简化的例子,以更好地理解……
谁真正拥有你预订的 Airbnb 房源?——营销认知与数据分析现实
在过去的 20 年里,Airbnb 投入了数十亿美元用于品牌建设,宣扬其作为一种真实、新颖、社区主导的替代酒店的形象。但,它真的是这样吗?
·发表于Towards Data Science ·阅读时长 8 分钟·2024 年 10 月 3 日
--
在这篇文章中,我将使用InsideAirbnb的数据来揭示 Airbnb 的所有权模式。接着,我会带你走一遍我得出结论的过程,这样你也可以对你所在的城市进行类似的分析。
Inside Airbnb是一个使命驱动的项目,提供关于 Airbnb 对住宅社区影响的数据和倡导。所有数据都使用创意共享署名 4.0 国际许可协议进行许可。
我相信你一定见过那些 Airbnb 广告,广告中是面带笑容的房东在充满旅游气息的邻里欢迎客人。如果你没见过,可以看一下这个:
我个人在 Airbnb 上已经待了超过 5 年,尽管我住过一些非常独特和真实的地方(我最喜欢的必须是门外有马的牧羊人小屋),但我最近意识到,我最近的体验并没有达到 Airbnb 广告所宣传的预期。
上周我在 Airbnb 住了一个周末。我进行了一次小型寻宝活动,解锁了许多挂锁才能找到钥匙。我打开门,进入了一个一尘不染的公寓,和照片中的一模一样。但我很惊讶地发现没有书籍或蜡烛。它完全空荡荡的,就像从来没有人住过一样。第二天,我没有遇到任何邻居。我没有收到房东的餐馆推荐。最后一天离开时,我把钥匙放在里面,没有人向我道别。我想,我是不是住在了一个酒店里?这就是 Airbnb 变成的样子吗?
Airbnb 希望你认为的事情
Airbnb 专注于像当地人一样体验生活,或者他们所说的:“真正属于的感觉”,这一点在他们的许多广告活动中得到了体现:
住在那里,Airbnb 的 2016 年广告活动
住在那里,因为当你住在巴黎时,你有自己的家。
这则精心策划的广告完美地体现了 Airbnb 的价值主张:像当地人一样生活,抛开那些没有灵魂、标准化的酒店预订。继承浪漫化的本地体验之后,Airbnb 的营销描绘了房东与客人之间值得信赖和真诚的关系。
所以不奇怪,在我上次旅行时,我感觉有些被欺骗了。我错过了完整的 Airbnb 体验。这激励我研究 Airbnb 的房源数据,以回答我脑海中纷飞的问题:
-
我们的 Airbnb 体验是否已经变得标准化,并优化了最大利润?
-
Airbnb 社区的本质消失了吗?
现实
多房产所有权是我们这些客人是否能真正体验到“Airbnb 体验”的转折点。
为什么?
因为,当我们从“房地产投资者”或拥有大量房源的房东那里预订住宿时,我们很可能是在入住一种类似酒店的商业模式,这些人购买了遍布城市的公寓来出租。在这种情况下,我们房东的动机主要是经济利益。我们的住宿将更多地被优化为盈利,而不是 Airbnb 通过其营销承诺的那种个人化、地方性的氛围。从本质上讲,唯一与酒店住宿的真实区别就是有一个厨房可供使用。
为了分析不同层次的多房产所有权,我将拥有 1-2 个房源的房东归类为个人,而拥有超过 3 个房源的则归类为专业房东。
为了证明我的观点——Airbnb 体验可能只是一个感知,我访问了来自欧洲 15 个城市的数据集,并进行了数据分析,以捕捉专业房东与个人房东的普及情况(所有详细信息和代码将在下一节提供)。以下是结果:

每个条形图代表一个不同的城市,且可视化被分为两部分:左侧(绿色)表示由个人拥有的房源百分比,右侧(亮色)表示由专业房东拥有的房源百分比。每个房东的房产数量被分为 1, 2, +2, +5, +10 和+100 类。我选择的一些城市有严格的住宿监管政策,限制了每年可以出租的天数(例如:佛罗伦萨、伦敦、爱丁堡、罗马、巴黎、哥本哈根和阿姆斯特丹)。最后,图表的排序是根据专业房东拥有的份额从高到低。
从图表中我们可以看到:
-
在我的分析中,大多数 Airbnb 房源是由专业房东拥有的。
-
巴塞罗那的专业房东拥有份额最高,达到 74%,而个人房东占 26%。
-
阿姆斯特丹的专业房东拥有份额最低,仅为 5%,其余 95%的房源由个人拥有,这可能是由于政府严格的租赁法规,限制每年最多只能出租 30 天。
-
放松的 Airbnb 监管使得专业房东可以主导市场。
-
严格 Airbnb 监管的城市往往将专业房东排除在外。
-
似乎没有设定每个房东最多可以拥有多少房源的限制。
根据 Airbnb 的说法,我们与他们的体验应该是真实的。然而,通过查看数据,我们发现五分之三的房源是由租赁投资者拥有的类似酒店的公寓(除非租赁受到监管)。这种营销认知与现实之间的脱节,不仅突显了品牌营销在我们观感中的影响力,还强调了 InsideAirbnb 等举措的重要性。
我现在知道下次入住时该期待什么了——你呢?你会体验到完整的挂锁寻宝体验吗?如果你感兴趣,可以深入了解一下编码!
如何复制我的研究到你的城市?
我写了一份指南,介绍了如何执行数据分析并生成上一节中的图表。我正在使用巴塞罗那市的数据集来说明不同的数据分析步骤。
下载了来自 InsideAirbnb 的 listings.csv.gz 文件后,我直接在 Python 中打开了它们,没有解压。我在这个项目中使用了 Polars,只是为了熟悉相关命令(你也可以使用 Pandas):
import polars as pl
df=pl.read_csv('listings.csv.gz')
以下是我用于分析的城市以及每个数据集中的房源数量:

用于分析的城市。图片由作者提供
如果你喜欢这个紧凑的气泡图,记得查看我上一篇文章:
## 我在 Matplotlib 库中发现了一个隐藏的宝石:Python 中的紧凑气泡图
是否曾想过设计那些漂亮的基于 Tableau 的紧凑气泡图?跟随教程,学习如何使用 Matplotlib…
[towardsdatascience.com
首次查看数据集,它大致是这样的:

数据框片段。图片由作者提供
内容基于每个房源 URL 中提供的数据,其中包含每个房源的行和 75 列,详细说明了从描述、邻里、卧室数量,到评分、最少入住天数和价格等信息。
如前所述,尽管该数据集具有无限潜力,但我将专注于多房源所有权的分析。
- 数据分析
下载数据后,数据清理工作几乎不用做:
1- 过滤“property_type”为“Entire rental units”以筛选掉房间类房源。
2- 过滤“has_availability”为“t”(True),以去除非活跃的房源。
import polars as pl
#I renamed the listings.csv.gz file to cityname_listings.csv.gz.
df=pl.read_csv('barcelona_listings.csv.gz')
df=df.filter((pl.col('property_type')=="Entire rental unit")&(pl.col('has_availability')=="t"))
为了处理数据,我将原始数据转化为不同的结构,便于量化数据集中有多少房源是由同一位房东拥有。换句话说,衡量有多少巴塞罗那的房源是由多房东拥有的。这是我的处理方法:
-
对“host_id”列执行了
value_counts操作,以统计同一 host_id 拥有多少个房源。 -
创建了 5 个不同的区间来量化多房源级别:1 处房源、2 处房源、+2 处房源、+5 处房源、+10 处房源和+100 处房源。
-
对每个 host_id 的房源数量(连续值)进行
polars.cut操作,将其分成我设定的离散类别(区间)。
host_count=df['host_id'].value_counts().sort('count')
breaks = [1,2,5,10,100]
labels = ['1','2','+2','+5','+10','+100']
host_count = host_count.with_columns(
pl.col("count").cut(breaks=breaks, labels=labels, left_closed=False).alias("binned_counts")
)
host_count
这是结果。Host_id、房源数量和分类。显示的数据对应于巴塞罗那市。

房源数量区间统计。图片由作者提供
请花一点时间注意到 host_id 为 346367515(列表中的最后一位)拥有 406 个房源?此时,Airbnb 社区的氛围开始感觉像一种幻觉了吗?
为了得到一个不依赖于 host_id 的城市概况,我将 host_count 数据框与原始数据框合并,以便将每个房源匹配到正确的多房源标签。之后,只需要对每个房源标签进行简单的value_counts()操作,就能得到属于该类别的房源总数。
我还添加了一个百分比列,以量化每个标签的权重。
df=df.join(host_count,on='host_id',how='left')
graph_data=df['binned_counts'].value_counts().sort('binned_counts')
total_sum=graph_data['count'].sum()
graph_data=graph_data.with_columns(((pl.col('count')/total_sum)*100).round().cast(pl.Int32).alias('percentage'))

最终数据结果。图片由作者提供
别担心,我也是一个视觉型的人,这里是表格的图形展示:
import plotly.express as px
palette=["#537c78","#7ba591","#cc222b","#f15b4c","#faa41b","#ffd45b"]
# I wrote the text_annotation manually cause I like modifying the x position
text_annotation=['19%','7%','10%','10%','37%','17%']
text_annotation_xpos=[17,5,8,8,35,15]
text_annotation_ypos=[5,4,3,2,1,0]
annotations_text=[
dict(x=x,y=y,text=text,showarrow=False,font=dict(color="white",weight='bold',size=20))
for x,y,text in zip(text_annotation_xpos,text_annotation_ypos,text_annotation)
]
fig = px.bar(graph_data, x="percentage",y='binned_counts',orientation='h',color='binned_counts',
color_discrete_sequence=palette,
category_orders={"binned_counts": ["1", "2", "+2","+5","+10","+100"]}
)
fig.update_layout(
height=700,
width=1100,
template='plotly_white',
annotations=annotations_text,
xaxis_title="% of listings",
yaxis_title="Number of listings owned by the same host",
title=dict(text="Prevalence of multi-property in Barcelona's airbnb listings<br><sup>% of airbnb listings in Barcelona owned by multiproperty hosts</sup>",font=dict(size=30)),
font=dict(
family="Franklin Gothic"),
legend=dict(
orientation='h',
x=0.5,
y=-0.125,
xanchor='center',
yanchor='bottom',
title="Number of properties per host"
))
fig.update_yaxes(anchor='free',shift=-10,
tickfont=dict(size=18,weight='normal'))
fig.show()

巴塞罗那 Airbnb 的多房源现象。图片由作者提供
回到最初的问题:我怎么得出结论,巴塞罗那的 Airbnb 精髓已经丧失?
-
大多数房源(64%)属于拥有超过 5 处房源的房东。17%的房源由拥有超过 100 处房源的房东管理。
-
只有26%的房源属于仅拥有 1 或 2 处房源的房东。
如果你希望同时分析多个城市,可以使用一个一次性完成所有清理和处理的函数:
import polars as pl
def airbnb_per_host(file,ptype,neighbourhood):
df=pl.read_csv(file)
if neighbourhood:
df=df.filter((pl.col('property_type')==ptype)&(pl.col('neighbourhood_group_cleansed')==neighbourhood)&
(pl.col('has_availability')=="t"))
else:
df=df.filter((pl.col('property_type')==ptype)&(pl.col('has_availability')=="t"))
host_count=df['host_id'].value_counts().sort('count')
breaks=[1,2,5,10,100]
labels=['1','2','+2','+5','+10','+100']
host_count = host_count.with_columns(
pl.col("count").cut(breaks=breaks, labels=labels, left_closed=False).alias("binned_counts"))
df=df.join(host_count,on='host_id',how='left')
graph_data=df['binned_counts'].value_counts().sort('binned_counts')
total_sum=graph_data['count'].sum()
graph_data=graph_data.with_columns(((pl.col('count')/total_sum)*100).alias('percentage'))
return graph_data
然后为你文件夹中的每个城市运行它:
import os
import glob
# please remember that I renamed my files to : cityname_listings.csv.gz
df_combined = pl.DataFrame({
"binned_counts": pl.Series(dtype=pl.Categorical),
"count": pl.Series(dtype=pl.UInt32),
"percentage": pl.Series(dtype=pl.Float64),
"city":pl.Series(dtype=pl.String)
})
city_files =glob.glob("*.csv.gz")
for file in city_files:
file_name=os.path.basename(file)
city=file_name.split('_')[0]
print('Scanning started for --->',city)
data=airbnb_per_host(file,'Entire rental unit',None)
data=data.with_columns(pl.lit(city.capitalize()).alias("city"))
df_combined = pl.concat([df_combined, data], how="vertical")
print('Finished scanning of ' +str(len(city_files)) +' cities')
请访问我的 GitHub 仓库,获取构建此图表的代码,因为代码有点长,无法在此附上:

最终对比图。图片由作者提供
[## GitHub - gopean-art/Airbnb-analysis: airbnb 文章
airbnb 文章。通过在 GitHub 上创建一个账户,参与 gopean-art/Airbnb-analysis 的开发。
github.com](https://github.com/gopean-art/Airbnb-analysis?source=post_page-----94407a32679c--------------------------------)
就这样!
在评论中告诉我你的想法,同时祝你在下次入住时,拥有一个非常充实和真实的 Airbnb 体验😉
本文中的所有图片和代码均由作者提供
为什么“AI 无法推理”是一个偏见
观点
我们人类是自豪的生物
·发布于 Towards Data Science ·9 分钟阅读·2024 年 12 月 12 日
--

图片由作者使用 ChatGPT 生成
最近,关于 AI 是否能够推理的争论愈演愈烈。几个月前发布的OpenAI 的 o1 模型,受到了各种反应的欢迎,反应从“这只是虚张声势”到“AI 的新范式”不等。
AI 的推理能力(或缺乏推理能力)似乎触动了许多人敏感的神经。我怀疑,承认 AI 可以“推理”会被视为对人类自豪感的打击,因为推理不再是人类独有的。
在十九世纪,算术被认为是一种智力上的才能(嘿,你什么时候见过一头牛能加两个数字?)。然而,我们还是得习惯使用比我们更强大的计算器。
我曾经见过一些令人震惊的说法,从“我们即将实现通用人工智能”或“AI 已经达到了博士水平”到对 AI 推理能力的彻底否定,比如“苹果公司对 AI 革命表示质疑”。
在其他文章中,我评论过埃隆·马斯克的粉丝们所提出的 AGI(通用人工智能)声明有多么不合逻辑。在这篇文章中,我将探讨另一端的观点:那些宣称 AI 根本无法推理的人。
Gary Marcus,是最直言不讳的 AI 否定者之一(我不称他们为...)
为什么(以及如何)我作为数据科学家学习网页开发
网页开发让你能够构建全栈的机器学习应用,并最大化机器学习工程/创业技能。哦,而且你可以用 Python 来做。
·发表于 Towards Data Science ·6 分钟阅读·2024 年 1 月 27 日
--

图片由 Davide Baraldi 提供,来源于 Pexels
几个月前,我遇到了一位在苹果公司做营销的人。当我告诉他我是数据科学家时,他的反应让我吃了一惊。
“哦,那你做的是后端工作?”
这个回应让我有些措手不及,因为我从未真正把我的工作当作“仅仅是后端工作”。在数据科学/人工智能的炒作泡沫中,我们习惯于把人工智能看作是当今时代的定义性技术;有点像是整个科技太阳系围绕它运转的中心。
我的朋友——我们称他为哥白尼——提醒我,对于那些在科技炒作泡沫之外的人来说,人工智能仅仅是谜题中的一块拼图,而不是宇宙的中心。这激发了我对探索科技“太阳系”中其他“行星”的好奇心,最终我决定学习网页开发,原因有四个:
-
创业潜力 —— 我希望能够从“端到端”构建完整的产品和应用,这样当我有一个产品/SaaS 创意时,我就可以免费自己动手构建。
-
机器学习工程 —— 一个在 Jupyter 中运行的机器学习模型…
为什么以及何时使用广义矩法
这是一种高度灵活的估计技术,可以应用于多种情况
Luis Felipe de Souza Rodrigues
·发表于 Towards Data Science ·阅读时间 7 分钟·2024 年 5 月 4 日
--

图片来源:Shubham Dhage 在 Unsplash
Hansen(1982 年)率先引入了广义矩法(GMM),为金融领域的实证研究,特别是资产定价领域做出了显著贡献。该模型的创建动机是为了在遵循模型中隐含的理论约束的同时估计经济模型中的参数。例如,如果经济模型表明两者应该是独立的,GMM 将尝试找到一个解,使它们乘积的平均值为零。因此,理解 GMM 对于那些需要一个理论条件至关重要的模型,但由于数据的性质,传统模型无法满足这些条件的人来说,是一个强有力的替代方案。
概念
这种估计技术在计量经济学和统计学中广泛应用,用于解决回归分析中的内生性和其他问题。GMM 估计量的基本概念是通过选择使数据样本矩与总体矩尽可能接近的参数来最小化准则函数。基本的 GMM 估计量的方程可以表达为:

GMM 估计量的目标是找到能够最小化该准则函数的参数向量θ,从而确保数据的样本矩尽可能地与总体矩对齐。通过优化该准则函数,GMM 估计量提供了一致的经济计量模型参数估计。
一致性意味着随着样本量趋近于无穷大,估计量会以概率收敛到真实的参数值(渐近正态)。这个性质对于确保估计量随着数据量增加能够提供可靠的估计至关重要。即使在存在遗漏变量的情况下,只要矩条件有效且工具变量正确设定,GMM 仍能提供一致的估计量。然而,遗漏相关变量可能会影响估计量的效率和参数的解释。
为了提高效率,GMM 在 Z 矩量上使用广义最小二乘法(GLS),以提高经济计量模型中参数估计的精度和效率。GLS 通过根据观察值的方差加权来处理异方差性和自相关性。在 GMM 中,Z 矩量被投影到工具变量的列空间中,这类似于 GLS 方法。通过聚焦于 Z 矩量并应用 GLS 技术,这可以最小化方差并提高参数估计的精度。
假设
然而,重要的是要认识到 GMM 估计量受到一系列假设的约束,这些假设在应用过程中必须加以考虑,具体假设如下:
-
矩的存在性:需要满足某一阶数的要求,并且数据的分布应具有有限的尾部。
-
正确的模型设定:底层模型必须正确设定,包括函数关系和误差项的分布。
-
可识别性:必须存在一个唯一的解用于估计参数。
-
矩条件:必须正确指定矩条件,并且在模型假设下其均值应为零。
-
有效工具变量:如果适用,工具变量必须相关且有效。
-
独立性与同方差性(条件性):理想情况下,误差应在矩条件下独立且同方差。
-
对异方差性的稳健性:如果加权矩阵是一致估计的,GMM 对异方差性具有稳健性。
-
多重共线性:GMM 能够处理多重共线性,但它可能会影响估计量的效率。
-
离群值:GMM 对离群值较为敏感,除非在建模过程中妥善处理。
-
大样本:在大样本中,GMM 更为高效。
-
渐近理论:一致性和效率等性质是渐近性质。
何时使用
因此,GMM 是一种高度灵活的估计技术,可以应用于各种情况,广泛用作计量经济学和统计学中的参数估计技术。它允许在不同的模型规范和数据结构下高效估计参数。其主要用途包括:
-
带有工具变量的模型:当模型中存在内生变量时使用。它提供了一种在解释变量与误差相关时,修正参数估计偏差的方法。
-
带有测量误差的模型:GMM 可以用来修正由变量测量误差引入的偏差。
-
带有矩量限制的模型:在某些情况下,模型必须满足多个矩条件。GMM 允许你同时利用所有这些信息,以实现更高效的估计。
-
时间序列模型:GMM 常用于 ARMA(自回归滑动平均)模型和其他时间序列模型中。
-
面板数据模型:它可以应用于面板数据模型,处理横截面单位内的异方差性和自相关问题。
-
非线性模型:GMM 也可以扩展到非线性模型,为经典方法如最大似然估计不可行时提供一种稳健的估计技术。
与 OLS 的比较
普通最小二乘法(OLS)方法与广义矩估计法(GMM)的对比突出了它们各自的优点。OLS 在经典的线性假设下证明了其高效性,作为最小方差(BLUE)的无偏线性估计量。线性回归模型的基本假设包括:变量之间的线性关系、无完全多重共线性、误差的均值为零、同方差性(误差的方差恒定)、误差无自相关性和误差正态分布。因此,OLS 是一种无偏、一致和高效的估计量。此外,它的计算复杂度相对较低。
然而,GMM 提供了更多的灵活性,适用于各种情境,如带有测量误差、内生变量、异方差性和自相关的模型。它对误差的分布不做假设,并且适用于非线性模型。GMM 在我们遗漏了重要变量、具有多个矩条件、非线性模型以及具有异方差性和自相关的数据集的情况下表现突出。
与 MLE 的比较
相反,在比较 GMM 和最大似然估计(MLE)时,它突出了两者处理数据假设的方式。GMM 通过使用数据和总体矩条件构造估计量,提供了灵活性和适应性,适用于假设较少的模型,特别是在强假设关于数据分布可能不成立的情况下更为有利。
最大似然估计(MLE)通过最大化给定数据的似然函数来估计参数,这取决于关于数据分布的特定假设。当假设的分布与真实的数据生成过程紧密匹配时,MLE 表现最优;而 GMM 能够适应多种分布,在数据可能不符合单一特定分布的情况下表现出其价值。
Python 中的估计
在 Python 中演示的假设示例中,我们利用 linearmodels.iv 库来估计一个使用 IVGMM 函数的 GMM 模型。在这个模型中,消费作为因变量,年龄和性别(男性用虚拟变量表示)作为外生变量。此外,我们假设收入是内生变量,而子女数量和教育水平是工具变量。
import pandas as pd
from linearmodels.iv import IVGMM
# Read the Excel file
df = pd.read_excel('example.xlsx')
# Dependent variable
dependent = 'YConsumption'
# Exogenous variables
exog_vars = ['XAge', 'XMale1']
# Endogenous variable
endog_vars = ['XIncomeEndo']
# Instrumental variables
instruments = ['ZChildQuantity6', 'ZEducation']
# Construct the formula for GMM
formula = "{dep} ~ 1 + {exog} + [{endog} ~ {instr}]".format(
dep=dependent,
exog='+'.join(exog_vars),
endog=endog_vars[0],
instr='+'.join(instruments)
)
# Estimate the GMM model
model = IVGMM.from_formula(formula, df)
result = model.fit(cov_type='robust')
# Displaying GMM results
print(result)
GMM 模型中的工具变量用于解决内生性问题,它们提供了一种与内生回归量相关但与误差项不相关的外生变动源。IVGMM 函数专为在 GMM 框架内使用工具变量估计模型而设计。

因此,通过将消费作为因变量,并采用外生变量(年龄和性别)以及工具变量(子女数量和教育水平)来解决内生性问题,本示例适用于广义矩量法(GMM)框架。
总结来说,广义矩量法(GMM)被视为一种强大且多用途的技术,广泛应用于计量经济学和统计学领域,在某些情况下具有相对于其他方法的优势。与传统的最小二乘法(OLS)和最大似然估计法(MLE)相比,这种方法允许更广泛的模型规格和数据结构,因为它对所需满足的假设限制较少。
在内生性、测量误差、动量约束等问题中,GMM 尤其具有优势。此外,在数据存在非线性、异方差性和自相关等问题时,该模型表现更好。GMM 利用矩条件和工具变量提供一致且高效的参数估计,因此,对于那些理论条件至关重要且常规方法因假设违背或数据结构复杂而无法应用的实证研究者,GMM 是一个极好的选择。
因此,GMM 成为一种多功能的计量工具,适用于广泛的实证场景,帮助研究者有效处理复杂的建模任务,并提供模型参数的正确估计。
参考文献
Hansen, L. P. (1982). 大样本广义矩量法估计量的性质。计量经济学,50(4),1029–1054. doi.org/10.2307/1912775
为什么高级 RAG 方法对 AI 的未来至关重要?
掌握高级 RAG:开启 AI 驱动应用程序的未来
·发布于Towards Data Science ·9 分钟阅读·2024 年 1 月 7 日
--
目前,我在 MongoDB 担任解决方案架构师,我受到了与同事们Fabian Valle、Brian Leonard、 Gabriel Paranthoen、Benjamin Flast和Henry Weller的互动对话启发,决定撰写这篇文章。
免费链接点击这里 => 请帮助点赞此Linkedin 帖子。
引言
检索增强生成(RAG)是生成式 AI 领域的重大进展,结合了高效的数据检索和大型语言模型的强大能力。
本质上,RAG 通过采用向量搜索来挖掘相关和现有的数据,将检索到的信息与用户的查询结合,然后通过像 ChatGPT 这样的大型语言模型处理这些数据。
该 RAG 方法确保生成的响应不仅精确,而且反映当前信息,大大减少了输出中的不准确性或“幻觉”现象。
然而,随着人工智能应用领域的扩展,对 RAG 的需求变得更加复杂和多样化。虽然基本的 RAG 框架非常稳健,但在应对不同产业和不断变化的使用场景的细微需求时,可能已经不再足够。这就是高级 RAG 技术发挥作用的地方。 这些增强方法是专门为应对特定挑战而量身定制的……
为什么批量归一化对深度学习至关重要
探索批量归一化在简化神经网络训练和提高模型性能中的作用
·发表于Towards Data Science ·阅读时间 11 分钟·2024 年 11 月 25 日
--

照片来自Petra Reid在Unsplash上的作品
批量归一化(Batch normalization)近年来已成为训练神经网络的重要技术。它使训练过程更加高效和稳定,这是一个关键因素,特别是在处理大型和深度网络时。最初它是为了解决内部协方差偏移问题而提出的。
本文将探讨训练神经网络过程中所涉及的问题,以及批量归一化如何解决这些问题。我们将详细描述其过程,并展示如何在 Python 中实现批量归一化并将其集成到现有模型中。我们还将考虑该方法的优缺点,以判断是否值得使用。
训练深度神经网络时会出现哪些问题?
在训练深度神经网络时,反向传播会在每次运行后发生。预测误差从网络的后层逐层传递。在此过程中,个别神经元的权重会被调整,以便尽快减少误差。这一过程假设其他层保持不变,并且……
为什么聚类失败
以及如何解决
·发表于Towards Data Science ·阅读时间 9 分钟·2024 年 7 月 6 日
--
你遇到了数据解释问题,于是尝试了聚类。现在你又面临一个聚类解释的问题!曾经怀疑数据中可能存在某些模式。可以理解的是,大家希望通过无监督学习加入一些结构,从而获得一些洞察。聚类是发现数据结构的首选工具。因此,你开始了这段旅程。你在计算上花费了相当多的钱。你投入了大量精力去调试聚类参数。为了确保准确,你尝试了几种算法。但最终你得到的只是一些聚类数据的彩虹图,它们可能有某种意义——可能吧——如果你使劲眯眼看。你带着一种不安的怀疑回家,觉得这一切似乎都是徒劳的。可悲的是,这种情况太常见了。那么,为什么会这样呢?

一些真实的聚类。图片由 NASA 和 STScI 发布,属于公共领域。
聚类项目未能产生价值通常由几个原因造成:对数据理解不足、对期望结果关注不够、以及工具选择不当。我们将逐一探讨这些原因。为了激发讨论,理解聚类技术存在的原因是很有启发性的。为了理解这一点,我们将回顾聚类是什么,以及促使聚类技术发展的几个问题。
什么是聚类?
为什么数据处理对数据科学家至关重要
观点
随着预训练 AI 模型和大型语言模型(LLM)数据分析的兴起,独特的数据是唯一能让你与众不同的东西。
·发表于Towards Data Science·阅读时间:4 分钟·2024 年 7 月 31 日
--

图片由作者使用Midjourney创作。
当我在完成物理学博士学位后转行从事数据科学工作时,我被复杂的模型和有洞察力的数据分析所吸引。
四年后,我知道这些工作的方面逐渐变得不那么重要了。
成功的 AI 应用程序的首要成分是数据。而在我的任何项目中,最大时间消耗就是数据处理。
欢迎来到现实世界
入门课程侧重于模型开发和理解训练神经网络的内部工作原理。
我们学习编写自己的训练循环,选择合适的验证指标,并理解偏差-方差权衡。
学生们使用像MNIST这样的现成数据集。我们的课程对数据部分只是略微涉及,实际上对于教程来说,数据处理就像是输入
from torchvision.datasets import MNIST
dataset = MNIST('./data', download=True)
为什么数据科学可能不适合你
为什么数据科学的职业可能不适合你的迹象
·发表于 Towards Data Science ·阅读时间 8 分钟·2024 年 5 月 5 日
--

图片由 Nadine Shaabana 提供,来源于 Unsplash
虽然我认为数据科学可能是最好的职业,但它并不适合每个人。我相信任何人都可以成为数据科学家,但这并不意味着他们应该成为。所以,在本文中,我想谈谈一些迹象,说明作为数据科学家的职业可能不适合你。
我想强调的是,本文并不是为了打击你,而是希望让你对这个角色的一些现实和关键方面保持透明,毕竟这些可能并不是每个人都喜欢的。
讨厌数学
无论你喜欢与否,数学是数据科学家的关键技能。
在数据科学职位所需的数学水平方面,需要注意的是,不同公司和具体的专业方向之间可能会有所不同。然而,通常一个好的起点是 A-Level / 高中数学。理想情况下,你应该至少达到大多数大学 STEM(科学、技术、工程、数学)学位的第一或第二年的水平。
为什么数据科学家需要这些软件工程技能
学习这些内容以成为一个更全面的数据科学家
·发表于 Towards Data Science ·6 分钟阅读·2024 年 12 月 9 日
--

图片来源:Lavi Perchik 在Unsplash
数据科学家的角色正在发生变化。企业不再希望看到 Jupyter 笔记本中的 PoC 模型,因为它们没有实际价值。因此,作为数据科学家,我们应该提升自己的软件工程技能,以便更好地部署我们的算法。在本文中,我想深入剖析数据科学家需要学习的关键软件工程技能。
系统设计
在构建大规模应用时,通常涉及多个组件,例如前端、数据库、API,以及如果是算法产品的话,机器学习模型本身。
构建最佳系统时,必须考虑像缓存、负载均衡、CAP 定理、可扩展性等关键概念,以适应特定场景。
系统设计对数据科学家来说非常重要,因为它帮助我们理解模型如何在生产环境中使用,并确保我们以最佳方式构建它...
为什么深度学习模型在 GPU 上运行更快:CUDA 编程简要介绍
对于那些想了解.to("cuda")的作用的人
·发表于 Towards Data Science ·15 分钟阅读 ·2024 年 4 月 17 日
--

图片由作者和 AI 协助制作 (copilot.microsoft.com/images/create)
如今,当我们谈论深度学习时,通常会将其实现与利用 GPU 以提高性能联系在一起。
GPU(图形处理单元)最初是为了加速图像、2D 和 3D 图形的渲染而设计的。然而,由于其能够执行大量并行操作的能力,GPU 的应用范围超越了这一点,扩展到了深度学习等领域。
GPU 在深度学习模型中的使用始于 2000 年代中后期,并在 2012 年随着 AlexNet 的出现变得非常流行。AlexNet 是由 Alex Krizhevsky、Ilya Sutskever 和 Geoffrey Hinton 设计的卷积神经网络,它在 2012 年赢得了 ImageNet 大规模视觉识别挑战赛(ILSVRC)。这一胜利标志着深度神经网络在图像分类中的有效性以及 GPU 在训练大型模型中的重要作用。
在这一突破之后,GPU 在深度学习模型中的使用变得越来越普及,这促进了像 PyTorch 和 TensorFlow 这样的框架的创建。
如今,我们只需在 PyTorch 中写 .to("cuda") 就能将数据发送到 GPU,并期望训练过程加速。但是,深度学习算法是如何在实际中利用 GPU 的计算性能的呢?让我们一探究竟!
深度学习架构,如神经网络、卷积神经网络(CNN)、递归神经网络(RNN)和变换器(transformers),基本上是通过数学运算构建的,比如矩阵加法、矩阵乘法以及将一个函数应用于矩阵。因此,如果我们找到优化这些运算的方法,就可以提高深度学习模型的性能。
那么,让我们从简单的开始。假设你想要将两个向量相加 C = A + B。

下面是一个用 C 实现的简单例子:
void AddTwoVectors(flaot A[], float B[], float C[]) {
for (int i = 0; i < N; i++) {
C[i] = A[i] + B[i];
}
}
正如你所注意到的,计算机必须遍历向量,在每次迭代中顺序地添加每一对元素。但这些操作是相互独立的。第 i 对元素的加法并不依赖于任何其他对。因此,如果我们能够并行执行这些操作,将所有元素对同时相加,会怎么样呢?
一种直接的方法是使用 CPU 多线程来并行执行所有计算。然而,对于深度学习模型,我们处理的是大量的向量,包含数百万个元素。普通的 CPU 最多只能同时处理十几个线程。此时,GPU 就派上用场了!现代的 GPU 可以同时运行数百万个线程,从而增强这些数学运算在大规模向量上的性能。
GPU 与 CPU 的对比
尽管单次操作时 CPU 的计算可能比 GPU 更快,但 GPU 的优势在于它的并行化能力。其原因在于 CPU 和 GPU 的设计目标不同。CPU 设计的目的是尽可能快地执行一系列操作(线程),并且只能同时执行几十个线程,而 GPU 的设计目标是并行执行成千上万的操作(尽管牺牲了每个线程的速度)。
请看下面的视频:
举个例子,假设 CPU 就像一辆法拉利,而 GPU 就像一辆公交车。如果你的任务是搬运一个人,法拉利(CPU)显然是更好的选择。然而,如果你要搬运多人,尽管法拉利(CPU)每次的速度更快,但公交车(GPU)能够一次性将所有人都运送到目的地,比起法拉利多次往返,公交车能够更快地完成运输。所以,CPU 更适合处理顺序操作,而 GPU 更适合处理并行操作。

作者借助 AI 提供的图像(copilot.microsoft.com/images/create)
为了提供更高的并行能力,GPU 的设计将更多的晶体管分配给数据处理,而非数据缓存和流控制,这与 CPU 的设计不同,CPU 通常将大量晶体管分配给这些目的,以优化单线程性能和复杂指令执行。
下图展示了 CPU 与 GPU 的芯片资源分配情况。

作者所用图像灵感来源于CUDA C++ 编程指南
CPU 拥有强大的核心和更复杂的缓存内存架构(为此分配了大量晶体管)。这种设计使得顺序操作处理更快。另一方面,GPU 优先考虑拥有大量核心,以实现更高程度的并行性。
现在我们已经理解了这些基本概念,如何在实践中利用这些并行计算能力呢?
CUDA 简介
当你运行一些深度学习模型时,你可能会选择使用一些流行的 Python 库,如 PyTorch 或 TensorFlow。然而,众所周知,这些库的核心在底层运行的是 C/C++代码。另外,正如我们之前提到的,你可能会使用 GPU 来加速处理。这时,CUDA 就派上用场了!CUDA 代表计算统一架构,它是 NVIDIA 为其 GPU 上的通用处理开发的平台。因此,尽管 DirectX 被游戏引擎用来处理图形计算,CUDA 使开发人员能够将 NVIDIA 的 GPU 计算能力集成到他们的通用软件应用中,超越了仅仅是图形渲染的应用。
为了实现这一点,CUDA 提供了一个简单的基于 C/C++的接口(CUDA C/C++),使得可以访问 GPU 的虚拟指令集和特定操作(如在 CPU 和 GPU 之间移动数据)。
在进一步探讨之前,让我们先理解一些基本的 CUDA 编程概念和术语:
-
host:指的是 CPU 及其内存;
-
device:指的是 GPU 及其内存;
-
kernel:指的是在设备(GPU)上执行的函数;
因此,在使用 CUDA 编写的基本代码中,程序在host(CPU)上运行,发送数据到device(GPU),并启动在device(GPU)上执行的kernels(函数)。这些内核由多个线程并行执行。执行后,结果从device(GPU)传回host(CPU)。
让我们回到添加两个向量的问题:
#include <stdio.h>
void AddTwoVectors(flaot A[], float B[], float C[]) {
for (int i = 0; i < N; i++) {
C[i] = A[i] + B[i];
}
}
int main() {
...
AddTwoVectors(A, B, C);
...
}
在 CUDA C/C++中,程序员可以定义 C/C++函数,称为kernels,当调用时,这些内核会被 N 个不同的 CUDA 线程并行执行 N 次。
要定义一个内核,你可以使用__global__声明符,并且执行此内核的 CUDA 线程数可以通过<<<...>>>符号来指定:
#include <stdio.h>
// Kernel definition
__global__ void AddTwoVectors(float A[], float B[], float C[]) {
int i = threadIdx.x;
C[i] = A[i] + B[i];
}
int main() {
...
// Kernel invocation with N threads
AddTwoVectors<<<1, N>>>(A, B, C);
...
}
每个线程执行内核,并被赋予一个唯一的线程 ID threadIdx,可以通过内置变量在内核中访问。上面的代码将两个大小为 N 的向量 A 和 B 相加,并将结果存储到向量 C 中。正如你所注意到的,与其使用循环顺序执行每对加法操作,CUDA 允许我们同时执行所有这些操作,使用 N 个线程并行处理。
但在我们能够运行此代码之前,还需要进行另一个修改。重要的是要记住,内核函数是在设备(GPU)内运行的。因此,所有数据都需要存储在设备内存中。您可以通过以下 CUDA 内置函数来实现:
#include <stdio.h>
// Kernel definition
__global__ void AddTwoVectors(float A[], float B[], float C[]) {
int i = threadIdx.x;
C[i] = A[i] + B[i];
}
int main() {
int N = 1000; // Size of the vectors
float A[N], B[N], C[N]; // Arrays for vectors A, B, and C
...
float *d_A, *d_B, *d_C; // Device pointers for vectors A, B, and C
// Allocate memory on the device for vectors A, B, and C
cudaMalloc((void **)&d_A, N * sizeof(float));
cudaMalloc((void **)&d_B, N * sizeof(float));
cudaMalloc((void **)&d_C, N * sizeof(float));
// Copy vectors A and B from host to device
cudaMemcpy(d_A, A, N * sizeof(float), cudaMemcpyHostToDevice);
cudaMemcpy(d_B, B, N * sizeof(float), cudaMemcpyHostToDevice);
// Kernel invocation with N threads
AddTwoVectors<<<1, N>>>(d_A, d_B, d_C);
// Copy vector C from device to host
cudaMemcpy(C, d_C, N * sizeof(float), cudaMemcpyDeviceToHost);
}
我们需要使用指针,而不是直接将变量 A、B 和 C 传递给内核。在 CUDA 编程中,不能直接在内核启动(<<<...>>>)中使用主机数组(如示例中的A、B和C)。CUDA 内核在设备内存上操作,因此需要将设备指针(d_A、d_B和d_C)传递给内核,以便它能够在设备上执行操作。
此外,我们需要使用cudaMalloc在设备上分配内存,并使用cudaMemcpy在主机和设备之间复制数据。
现在我们可以添加向量 A 和 B 的初始化,并在代码末尾刷新 cuda 内存。
#include <stdio.h>
// Kernel definition
__global__ void AddTwoVectors(float A[], float B[], float C[]) {
int i = threadIdx.x;
C[i] = A[i] + B[i];
}
int main() {
int N = 1000; // Size of the vectors
float A[N], B[N], C[N]; // Arrays for vectors A, B, and C
// Initialize vectors A and B
for (int i = 0; i < N; ++i) {
A[i] = 1;
B[i] = 3;
}
float *d_A, *d_B, *d_C; // Device pointers for vectors A, B, and C
// Allocate memory on the device for vectors A, B, and C
cudaMalloc((void **)&d_A, N * sizeof(float));
cudaMalloc((void **)&d_B, N * sizeof(float));
cudaMalloc((void **)&d_C, N * sizeof(float));
// Copy vectors A and B from host to device
cudaMemcpy(d_A, A, N * sizeof(float), cudaMemcpyHostToDevice);
cudaMemcpy(d_B, B, N * sizeof(float), cudaMemcpyHostToDevice);
// Kernel invocation with N threads
AddTwoVectors<<<1, N>>>(d_A, d_B, d_C);
// Copy vector C from device to host
cudaMemcpy(C, d_C, N * sizeof(float), cudaMemcpyDeviceToHost);
// Free device memory
cudaFree(d_A);
cudaFree(d_B);
cudaFree(d_C);
}
此外,我们在调用内核之后需要添加cudaDeviceSynchronize();。这是一个用于同步主机线程和设备的函数。调用此函数时,主机线程将等待,直到设备上所有先前发出的 CUDA 命令完成,才会继续执行。
此外,添加一些 CUDA 错误检查非常重要,这样我们才能在 GPU 上识别出错误。如果不添加此检查,代码将继续执行主机线程(CPU),并且很难识别与 CUDA 相关的错误。
以下是这两种技术的实现:
#include <stdio.h>
// Kernel definition
__global__ void AddTwoVectors(float A[], float B[], float C[]) {
int i = threadIdx.x;
C[i] = A[i] + B[i];
}
int main() {
int N = 1000; // Size of the vectors
float A[N], B[N], C[N]; // Arrays for vectors A, B, and C
// Initialize vectors A and B
for (int i = 0; i < N; ++i) {
A[i] = 1;
B[i] = 3;
}
float *d_A, *d_B, *d_C; // Device pointers for vectors A, B, and C
// Allocate memory on the device for vectors A, B, and C
cudaMalloc((void **)&d_A, N * sizeof(float));
cudaMalloc((void **)&d_B, N * sizeof(float));
cudaMalloc((void **)&d_C, N * sizeof(float));
// Copy vectors A and B from host to device
cudaMemcpy(d_A, A, N * sizeof(float), cudaMemcpyHostToDevice);
cudaMemcpy(d_B, B, N * sizeof(float), cudaMemcpyHostToDevice);
// Kernel invocation with N threads
AddTwoVectors<<<1, N>>>(d_A, d_B, d_C);
// Check for error
cudaError_t error = cudaGetLastError();
if(error != cudaSuccess) {
printf("CUDA error: %s\n", cudaGetErrorString(error));
exit(-1);
}
// Waits untill all CUDA threads are executed
cudaDeviceSynchronize();
// Copy vector C from device to host
cudaMemcpy(C, d_C, N * sizeof(float), cudaMemcpyDeviceToHost);
// Free device memory
cudaFree(d_A);
cudaFree(d_B);
cudaFree(d_C);
}
要编译和运行 CUDA 代码,您需要确保 CUDA 工具包已安装在您的系统上。然后,您可以使用nvcc,NVIDIA CUDA 编译器,来编译代码。如果您的计算机上没有 GPU,可以使用 Google Colab。只需要在运行时→笔记本设置中选择一个 GPU,然后将代码保存到example.cu文件中并运行:
%%shell
nvcc example.cu -o compiled_example # compile
./compiled_example # run
# you can also run the code with bug detection sanitizer
compute-sanitizer --tool memcheck ./compiled_example
然而,我们的代码仍然没有完全优化。上面的示例使用了一个大小为 N = 1000 的向量。但这是一个小数字,无法充分展示 GPU 的并行化能力。此外,在处理深度学习问题时,我们通常会处理具有数百万个参数的大规模向量。然而,如果我们尝试设置,例如,N = 500000 并使用<<<1, 500000>>>运行上面的内核,它将抛出一个错误。因此,为了改进代码并执行此类操作,我们首先需要了解 CUDA 编程中的一个重要概念:线程层次结构。
线程层次结构
内核函数的调用使用<<<number_of_blocks, threads_per_block>>>表示法进行。因此,在我们上面的示例中,我们运行 1 个块,包含 N 个 CUDA 线程。然而,每个块对其支持的线程数量有限制。这是因为每个块中的线程都需要位于同一个流式多处理器核心,并且必须共享该核心的内存资源。
您可以使用以下代码片段获取此限制:
int device;
cudaDeviceProp props;
cudaGetDevice(&device);
cudaGetDeviceProperties(&props, device);
printf("Maximum threads per block: %d\n", props.maxThreadsPerBlock);
在当前的 Colab GPU 上,一个线程块最多可以包含 1024 个线程。因此,我们需要更多的块来执行更多的线程,以处理示例中的大向量。此外,块被组织成网格,如下所示:

handwiki.org/wiki/index.php?curid=1157670 (CC BY-SA 3.0)
现在,可以使用以下方式访问线程 ID:
int i = blockIdx.x * blockDim.x + threadIdx.x;
所以,我们的脚本变成了:
#include <stdio.h>
// Kernel definition
__global__ void AddTwoVectors(float A[], float B[], float C[], int N) {
int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i < N) // To avoid exceeding array limit
C[i] = A[i] + B[i];
}
int main() {
int N = 500000; // Size of the vectors
int threads_per_block;
int device;
cudaDeviceProp props;
cudaGetDevice(&device);
cudaGetDeviceProperties(&props, device);
threads_per_block = props.maxThreadsPerBlock;
printf("Maximum threads per block: %d\n", threads_per_block); // 1024
float A[N], B[N], C[N]; // Arrays for vectors A, B, and C
// Initialize vectors A and B
for (int i = 0; i < N; ++i) {
A[i] = 1;
B[i] = 3;
}
float *d_A, *d_B, *d_C; // Device pointers for vectors A, B, and C
// Allocate memory on the device for vectors A, B, and C
cudaMalloc((void **)&d_A, N * sizeof(float));
cudaMalloc((void **)&d_B, N * sizeof(float));
cudaMalloc((void **)&d_C, N * sizeof(float));
// Copy vectors A and B from host to device
cudaMemcpy(d_A, A, N * sizeof(float), cudaMemcpyHostToDevice);
cudaMemcpy(d_B, B, N * sizeof(float), cudaMemcpyHostToDevice);
// Kernel invocation with multiple blocks and threads_per_block threads per block
int number_of_blocks = (N + threads_per_block - 1) / threads_per_block;
AddTwoVectors<<<number_of_blocks, threads_per_block>>>(d_A, d_B, d_C, N);
// Check for error
cudaError_t error = cudaGetLastError();
if (error != cudaSuccess) {
printf("CUDA error: %s\n", cudaGetErrorString(error));
exit(-1);
}
// Wait until all CUDA threads are executed
cudaDeviceSynchronize();
// Copy vector C from device to host
cudaMemcpy(C, d_C, N * sizeof(float), cudaMemcpyDeviceToHost);
// Free device memory
cudaFree(d_A);
cudaFree(d_B);
cudaFree(d_C);
}
性能比较
下面是不同向量大小下 CPU 和 GPU 进行这两个向量加法操作的比较。

图片由作者提供
如你所见,GPU 处理的优势只有在向量大小 N 非常大的情况下才会显现出来。此外,请记住,这个时间比较仅仅考虑了内核/函数的执行时间,并没有考虑在主机和设备之间复制数据的时间,尽管在大多数情况下这个时间可能不显著,但在我们这个简单的加法操作中,它相对来说是比较可观的。因此,重要的是要记住,GPU 计算只有在处理高度计算密集且高度并行的计算时才展示其优势。
多维线程
好了,现在我们知道如何提高一个简单数组操作的性能。但在处理深度学习模型时,我们需要处理矩阵和张量操作。在我们之前的示例中,我们只使用了一维块和 N 个线程。然而,也可以执行多维线程块(最多支持 3 个维度)。因此,为了方便起见,如果你需要执行矩阵操作,你可以运行一个 NxM 线程的线程块。在这种情况下,你可以获取矩阵的行列索引,方法是row = threadIdx.x, col = threadIdx.y。同样,为了方便,你可以使用dim3变量类型来定义number_of_blocks和threads_per_block。
下面的示例演示了如何加法两个矩阵。
#include <stdio.h>
// Kernel definition
__global__ void AddTwoMatrices(float A[N][N], float B[N][N], float C[N][N]) {
int i = threadIdx.x;
int j = threadIdx.y;
C[i][j] = A[i][j] + B[i][j];
}
int main() {
...
// Kernel invocation with 1 block of NxN threads
dim3 threads_per_block(N, N);
AddTwoMatrices<<<1, threads_per_block>>>(A, B, C);
...
}
你也可以扩展这个示例来处理多个块:
#include <stdio.h>
// Kernel definition
__global__ void AddTwoMatrices(float A[N][N], float B[N][N], float C[N][N]) {
int i = blockIdx.x * blockDim.x + threadIdx.x;
int j = blockIdx.y * blockDim.y + threadIdx.y;
if (i < N && j < N) {
C[i][j] = A[i][j] + B[i][j];
}
}
int main() {
...
// Kernel invocation with 1 block of NxN threads
dim3 threads_per_block(32, 32);
dim3 number_of_blocks((N + threads_per_block.x - 1) ∕ threads_per_block.x, (N + threads_per_block.y - 1) ∕ threads_per_block.y);
AddTwoMatrices<<<number_of_blocks, threads_per_block>>>(A, B, C);
...
}
你也可以使用相同的思路扩展这个示例来处理三维操作。
现在你知道如何操作多维数据了,还有一个重要且简单的概念需要学习:如何在内核中调用函数。基本上,这是通过使用__device__声明修饰符来完成的。这定义了可以由设备(GPU)直接调用的函数。因此,它们只能从__global__或另一个__device__函数中调用。下面的示例对向量应用了 sigmoid 操作(这是深度学习模型中非常常见的操作)。
#include <math.h>
// Sigmoid function
__device__ float sigmoid(float x) {
return 1 / (1 + expf(-x));
}
// Kernel definition for applying sigmoid function to a vector
__global__ void sigmoidActivation(float input[], float output[]) {
int i = threadIdx.x;
output[i] = sigmoid(input[i]);
}
所以,现在你已经了解了 CUDA 编程的一些基本重要概念,你可以开始创建 CUDA 内核。在深度学习模型的情况下,它们基本上是一些矩阵和张量操作,如加法、乘法、卷积、归一化等。例如,一个简单的矩阵乘法算法可以像下面这样进行并行化:

// GPU version
__global__ void matMul(float A[M][N], float B[N][P], float C[M][P]) {
int row = blockIdx.x * blockDim.x + threadIdx.x;
int col = blockIdx.y * blockDim.y + threadIdx.y;
if (row < M && col < P) {
float C_value = 0;
for (int i = 0; i < N; i++) {
C_value += A[row][i] * B[i][col];
}
C[row][col] = C_value;
}
}
现在,将这个与下面的普通 CPU 实现的两个矩阵乘法进行对比:
// CPU version
void matMul(float A[M][N], float B[N][P], float C[M][P]) {
for (int row = 0; row < M; row++) {
for (int col = 0; col < P; col++) {
float C_value = 0;
for (int i = 0; i < N; i++) {
C_value += A[row][i] * B[i][col];
}
C[row][col] = C_value;
}
}
}
你可以注意到,在 GPU 版本中,我们的循环次数更少,从而使得操作的处理速度更快。下面是 CPU 和 GPU 在 NxN 矩阵乘法性能对比:

作者提供的图片
正如你所观察到的,随着矩阵大小的增加,GPU 处理的性能提升在矩阵乘法操作中表现得更加明显。
现在,考虑一个基本的神经网络,它主要涉及y = σ(Wx + b)的操作,如下所示:

作者提供的图片
这些操作主要包括矩阵乘法、矩阵加法和对数组应用函数,所有这些你已经熟悉并行化技术。因此,现在你已经能够从零开始实现一个运行在 GPU 上的神经网络!
结论
在这篇文章中,我们介绍了关于 GPU 处理的基础概念,以提高深度学习模型的性能。然而,也需要提到的是,你所看到的这些概念仅仅是基础,还有很多东西需要学习。像 PyTorch 和 TensorFlow 这样的库实现了优化技术,涉及其他更复杂的概念,如优化内存访问、批处理操作等(它们利用了基于 CUDA 的库,如 cuBLAS 和 cuDNN)。但我希望这篇文章能帮助你理清当你编写.to("cuda")并在 GPU 上执行深度学习模型时,背后发生了什么。
在未来的文章中,我将尝试带来更多关于 CUDA 编程的复杂概念。请在评论中告诉我你对这篇文章的看法,或者你希望我接下来写些什么!非常感谢你的阅读!😊
进一步阅读
CUDA 编程指南 — NVIDIA CUDA 编程文档。
CUDA 文档 — NVIDIA 完整的 CUDA 文档。
CUDA 神经网络训练实现 — 用纯 CUDA C++实现的神经网络训练。
CUDA LLM 训练实现 — 用纯 CUDA C 实现的 LLM 训练。
为什么人工智能项目会失败?
85%的 AI 项目失败,6 个原因
·发表于 Towards Data Science ·阅读时间:9 分钟·2024 年 4 月 30 日
--

作者
在充满理想化头条和爆炸性技术进展的背后,人工智能项目有着糟糕的记录。 Gartner, HBR 估计多达 85%的 AI 项目在部署前或部署后失败,失败率是软件的两倍。
众所周知,人工智能比软件更难部署。人工智能具有不确定的结果。在用户手中,人工智能存在能力不确定性。部署后出现的意外后果导致了不良的舆论和用户对人工智能系统的信任丧失。成本和投资回报率很难事先合理化。
这些是包括和超越生成式人工智能的 AI 系统:推荐系统、自动驾驶汽车、计算机视觉、医疗诊断、自动化机器人、金融风险评估等。
来源于多年的学术与行业研究,以下是详细原因:
(1) 人工智能系统没有解决正确的问题
(2) 人工智能创新差距
(3) 人工智能系统无法实现足够好的性能,且不具备实用性
(4) 人们错失了触手可得的机会
(5) 人工智能系统没有创造足够的价值
(6) 伦理、偏见与社会危害
1. 人工智能系统没有解决正确的问题
为什么计算机甚至使用二进制?
一个新兴数据科学家的计算机硬件入门
·发表于数据科学前沿 ·阅读时间:8 分钟·2024 年 6 月 5 日
--
引言杂谈
数据科学家的主要工具是什么?一个公平(且显而易见)的答案是计算机,因为它处理数据的速度远远超过我们。试想一下,如果没有计算机,如何完成涉及数据的任务呢?你得依赖—天哪—铅笔和纸,在手工绘制大量表格、图表和计算的同时,你知道在计算机上这些任务不过需要几秒钟而已。可以说,今天的数据科学如果没有计算机,是根本不可能存在的。
既然计算机是进行数据科学的核心工具,你可能会认为这项工作需要理解计算机是如何工作的。毕竟,如果你不了解工具,做事情是很难做好。 然而,对于有志成为数据科学家的新人来说,往往容易忽视花时间建立与这一领域相关的基础知识。

图片由Malachi Brooks提供,来源于Unsplash
这也是可以理解的。数学、计算机科学和工程中的理论性话题,往往被 relegated(边缘化)为次要学习目标,因为它们不像像 Python、Tensorflow 或 Amazon Web Services 这样的应用类学习目标那样,能带来即时的回报(或者说就业机会)。那么,为什么…
为什么数据团队无法交付可衡量的 ROI?
识别数据团队在交付可衡量 ROI 时常遇到的障碍
·发表于Towards Data Science ·7 分钟阅读·2024 年 1 月 4 日
--

数据领导者能否收获他们的劳动成果:可衡量的 ROI?(图片来自Google Deep Mind — Pexels.com)
投资回报率(ROI)是一个用来评估投资表现的盈利能力指标 (Investopedia.com)。
ROI 是一个简单的术语,不是吗?要衡量数据的 ROI,我们只需要遵循以下 3 个步骤:
- 拥有一组代表数据“盈利能力”的 KPI
2. 通过数据提高关键绩效指标(KPI)
3. 衡量数据贡献的影响
然而,数据团队在这一路程的每个步骤中都面临障碍。Gartner 2023 年首席数据官(CDO)调查显示,69%的数据与分析(D&A)领导者在实现可衡量 ROI 时遇到困难 (Subhash Kari — Infocepts 执行副总裁兼首席创新官)。无法交付可衡量的 ROI 是许多数据团队的共同且致命的弱点。
尽管这一概念很流行,但我们不必把交付实际的数据 ROI 视为一场复杂的斗争。我认为,当我们从基础出发,并使用第一性原理进行推理时,复杂问题可以变得更简单。
在本文中,我将正是这样做——从最基础的 ROI 测量定义开始,分解为 3 个步骤,并分析实现目标的最基本障碍……
为什么 Python 消耗如此多的内存?

图片来自 Canva.com
Python 中整数对象的内存消耗分解
·发表于Towards Data Science ·阅读时长 7 分钟·2024 年 5 月 20 日
--
我们都知道,Python 因其灵活性和动态特性而闻名。然而,你是否思考过它为什么能够实现这些特性,是否有任何代价?当然,编程语言的具体机制太复杂,无法通过一篇文章来解释清楚。今天我想展示的是一个关于 Python 内存消耗的非常简单的示例。
在本文中,我将向你展示如何衡量一个变量占用多少内存,以及 Python 中会导致相对较高内存消耗的一些开销。如果你不明白为什么 Python 需要至少 28 个字节来存储一个简单的整数,请不要错过接下来的内容。
1. 如何衡量一个变量的内存使用情况?

图片来自 Canva.com
在我们开始讲解示例之前,我们需要一个用于衡量内存使用情况的解决方案。否则,我的示例将缺乏说服力。一旦我们掌握了方法,我们也可以在日常工作中用它来衡量自己的实现。
为什么是 ETL-Zero?理解数据集成的转变
·发表于数据科学前沿 ·10 分钟阅读·2024 年 11 月 11 日
--
在我准备 Salesforce 数据云认证时,我遇到了“零 ETL”这个术语。数据云提供了从其他系统(如数据仓库或数据湖)直接访问数据的可能性,或者与这些系统共享数据,而无需复制数据。Salesforce 也将其描述为自带数据湖(BYOL),借用了“自带设备”(BYOD)这一术语。我想更好地理解零 ETL 的概念,并以易于理解的方式进行说明。
在本文中,我将向你展示如何使用 Python 创建一个简化的 ETL 流程,以更好地理解这个概念,零 ETL 或零复制是什么意思,以及这种新的数据集成方式如何在 Salesforce 数据云中实现。
目录
传统 ETL 流程:Python 初学者的逐步指南
那么,什么是零 ETL?
为什么是零 ETL?优缺点分析
零 ETL 在 Salesforce 数据云中是什么样的?
1) 传统 ETL 流程:Python 初学者的逐步指南
如果你已经熟悉 ETL 和 ELT 流程,可以跳过这一部分。如果你对这个话题比较陌生,可以先看一下超简化的示例,更好地理解提取——转换——加载(ETL)过程。或者更好的是,亲自实践——通过应用它,你通常能更好地理解这些概念。
为什么解释性在人工智能中很重要
不是因为我们好奇,而是因为我们需要完成任务。
·发表于 Towards Data Science ·阅读时间 5 分钟·2024 年 10 月 20 日
--
人工智能模型输出的解释重要吗?
我对此的第一个回答是:其实不重要。
当解释只是为了通过修辞手法给我留下深刻印象,让我觉得你有理由做出某个决定时,我会把它称为“花里胡哨”,并且没有实际影响。如果我在等待基于我的 MRI 的癌症诊断,我更关心的是把准确度从 80%提高到 99%,而不是看一张吸引眼球的图片,告诉我证据在哪里。毕竟,可能需要专家才能识别出证据。更糟糕的是,证据可能是分散的,散布在数百万个像素中,以至于人类的大脑根本无法理解。为了仅仅让自己对 AI 的信任感到安心而追求解释是没有意义的。我们应该衡量正确性,如果数学表明结果超过了人类的表现,那么解释就是多余的。
但是,有时解释不仅仅是一个修辞练习。以下是解释重要的时刻:
-
当准确性至关重要,而人类可以验证结果时。此时,解释可以帮助你降低错误水平,例如,从 1%降低到 0.01%。
-
当原始预测不是你最关心的,解释却能产生有用的行动时。例如,单纯地说“这个合同中有不公平条款”并没有多大用处,但如果能明确指出不公平条款在哪里,才是有意义的。高亮显示不公平条款让我们可以采取行动,比如提出修改合同的建议。
当解释比答案更重要时
让我们以我共同创办的DocuPanda为例。简而言之,我们所做的就是让用户将复杂的文档映射成一个包含一致、正确输出的 JSON 负载。
所以也许我们扫描整个租赁合同,然后输出一个简短的 JSON:{"monthlyRentAmount": 2000, "dogsAllowed": true}。
为了让它更加具体,这是我在加利福尼亚伯克利期间的租赁合同的全部 51 页。

是的,湾区的租金真是疯狂,谢谢你的提问
如果你不是美国人,你可能会对需要 51 页来明确写出“你每个月要支付 3700 美元,作为交换,你可以住这里”感到震惊。我认为这可能在法律上并非必要,但我扯远了。
现在,使用 DocuPanda,我们可以得到诸如——租金多少,我可以带狗去住那里吗,开始日期是什么等明确的答案。
让我们来看看我们提取的 JSON 数据

所以显然 Roxy 不能和我一起住
如果你看到底部,我们有一个标记来指示不允许宠物,并附有合同中列出的例外说明。
解释性在这里有两个有用的原因:
-
也许我们必须确保这一点正确无误。通过审阅这一段,我可以确保我们正确理解了政策。
-
也许我想提出一个修改建议。仅仅知道在这 51 页中某处有宠物禁令并没有什么帮助——我仍然需要翻阅所有页面来提出修改建议。
这是我们解决问题的方式。我们并不是仅仅给你一个包含美元金额、真假结果等的黑盒子——我们设计了 DocuPanda,使其能够将预测与精确的像素数据相结合。你可以点击一个结果,滚动到精确的页面和部分,找到支持我们预测的依据。

点击“宠物允许 = false”会立即滚动到相关页面,上面写着“禁止哺乳动物宠物”等等。
解释驱动的工作流
在 DocuPanda,我们观察到了解释性使用的三种总体范式。
解释推动准确性
第一个范式是我们从一开始就预期到的,即解释性可以减少错误并验证预测。当你有一张 12,000 美元的发票时,你真的希望有一个人来确保这个数字是有效的,并且没有被断章取义,因为如果这个数字进入了会计自动化软件,风险太大。
这就是文档处理的一个伟大特性——人类在这方面非常擅长。我们成本很高,但我们知道自己在做什么。这让我们处于一个令人愉快的区间,人类可以非常高效地验证结果,通常能显著减少错误率。
解释推动高知识工作者的生产力
这一范式自然地从我们的用户群体中产生,我们一开始并没有完全预料到它。事实证明,有时我们比起想要得到一个问题的直接答案,更希望利用 AI 将正确的信息呈现在眼前。如果你考虑类似{“unfair payment terms”: true}这样的输出,我之前已经暗示过这一用例——与直接展示合同中使其不公平的语言相比,这几乎没有什么用处。
作为一个更完整的例子,考虑一个生物研究公司,它希望扫描所有生物学出版物,找出那些增加马铃薯糖分生产的过程。他们使用 DocuPanda 来回答类似的问题:
{sugarProductionLowered: true, sugarProductionGenes: [“AP2a”,”TAGL1"]}
他们的目标不是盲目地信任 DocuPanda,并统计有多少篇论文提到某个基因或类似的内容。使这一结果有用的关键是,研究人员可以点击查看论文的核心内容。通过点击基因名称,研究人员可以立即跳转到该基因被提及的上下文中——并判断这篇论文中涉及该基因与糖分生产的过程是否与他们的研究目标相关。这是一个说明解释比直接答案更重要的例子,并且能够提高高知识工作者的生产力。
责任解释
使用解释并利用它们让人类参与其中还有另一个原因。除了降低错误率(通常是这样),它们还让你能够证明你有一个合理的、符合法律要求的流程。
管理者关注过程。一个会犯错误的黑匣子不是一个可靠的过程。能够将每个提取的数据点追溯到原始来源,使你能够让人类参与其中,审查和批准结果。即使人类不能减少错误,参与其中也可能在法律上有帮助。这将过程从盲目的自动化转变为由人类驱动的过程,人类对文书错误有一个可接受的容忍率,且这由公司负责。一个相关的例子是,似乎在讨论全自动化系统时,监管者和公众舆论容忍的致命车祸率(按每英里计算)要低得多,而当涉及到人类驾驶辅助工具时,致命车祸率容忍度则较高。我个人认为这是道德上无法辩解的,但我并不制定规则,我们必须遵守这些规则。
通过使你能够让人类参与其中,你可以从一个完全自动化的法律风险领域转变为一个更为熟悉的法律领域,其中人类分析师使用 10 倍速率和生产力工具(并偶尔犯错,就像我们所有的罪人一样)。
所有图片均为作者所有
为什么 GenAI 是数据删除和隐私的噩梦
理解 LinkedIn OptOut 丑闻的关键线索
·发表于数据科学前沿 ·阅读时间 13 分钟·2024 年 9 月 20 日
--
要点
一旦训练数据被融入大型语言模型中,试图删除这些数据就像是想在蛋糕烤好后去掉糖分一样。你只能丢掉整个蛋糕,重新开始……
背景—什么是 LinkedIn OptOut?
在过去几天里,非欧盟地区的 LinkedIn 用户震惊地发现,他们已悄悄地被默认加入,允许 LinkedIn 及其关联公司在他们的内容上训练内容创作生成型 AI(GenAI)模型。在昨天的博客文章中,我解释了 LinkedIn OptOut 事件发生了什么,以及如何选择退出。可以在第一部分查看详细内容。
你已经被默认加入了一些可疑的操作,以下是如何退出的方法
在第二部分,我将解释为什么这个故事与数据删除法有着密切关系。以下是简要版本:
当你的数据被用来训练 GenAI 模型时,即使作为用户,你的法律权利是删除数据,但也无法(在实际操作中)保证删除。尤其是当涉及到你倾注心血创作的内容时……
为什么以人为中心的方法能带来更好的算法设计
我在量化定性思维方面学到的一些经验教训
·发表于 Towards Data Science ·阅读时间 6 分钟·2024 年 4 月 11 日
--

Dall-E 图像:“不列颠哥伦比亚省阿达姆斯河的印象派画作”
算法常常让人联想到冷冰冰、艰深的数学公式,令许多人望而却步。
这是许多计算机科学课程和教材中教授的方法——这也是我在 90 年代作为计算机科学学生时学到的内容。
从概念上讲,这种方法运作良好——适用于搜索、排序、计算、组织等。为了完成任务,有一系列已经建立的算法类别。
那么,对于建模一个以定性数据为主的环境又该如何处理呢?例如,当建模涉及非正式近似的数据时。
传统上,这就是人在面对多种变量时解决问题的方式。在这里,做出的决策远比许多人想象的更为复杂,并与社会的结构紧密交织。
在攻读博士学位时的一个重大失败让我意识到这一点,这彻底改变了我在算法设计上的方法。
传统的算法思维
我上学时是为了成为一名计算机科学家。这是上世纪 90 年代,当时该领域的大多数人都是纯粹主义者,认为算法……
为什么内部公司聊天机器人会失败,以及如何在企业中有效利用生成性人工智能
从问题出发,而不是从解决方案开始
·发布于 Towards Data Science ·阅读时间:10 分钟·2024 年 11 月 29 日
--

背景许可来源于 elements.envato.com,编辑者:Marcel Müller 2024
许多组织最常见的错觉是这样的:他们对 ChatGPT 或 Microsoft Co-Pilot 等生成性人工智能感到兴奋,阅读了一些关于人工智能如何“以某种方式改善你的业务”的文章,然后尝试找到其他用例,将聊天机器人套用其上,最终当结果不尽如人意时感到失望。然后,便进入了辩解阶段。我经常听到类似“模型不够好”或“我们需要提升员工的技能以编写更好的提示词”的说法。
在 90%的情况下,这些并不是正确的结论,问题出在我们思考聊天机器人时的方式。我已经为从三人小企业到拥有三十万员工的全球企业开发了三十多种生成性人工智能应用,并且在每个地方都看到了这种模式。
有成千上万的公司告诉你,必须有“某种聊天机器人解决方案”,因为大家都在做这个。OpenAI 的 ChatGPT、微软的 Copilot、谷歌的 Gemini 以及所有其他向你推销聊天机器人的公司,都在帮助你打破创建聊天机器人的初步障碍。但让我告诉你:75% 的真正痛点问题,即使使用生成性人工智能,也并不适合做成聊天机器人。
我经常看到经理、项目主管或其他决策者从这样的想法出发:“我们这里有一个带有 AI 的产品,可以帮助我们构建聊天机器人——让我们尽可能多地找出实施的地方。” 根据我的经验,这是错误的方法,因为你从一个解决方案出发,试图将现有的问题强行与其匹配。正确的方法应该是先从问题出发,分析它,然后找到适合的 AI 解决方案。聊天机器人可能是某些应用场景的好接口,但把所有问题都强行塞进聊天机器人里是有问题的。

强行将一个解决方案应用到问题上 vs. 从问题出发并找到解决方案。© Marcel Müller 2024
在本文中,我将分享我通过实际操作经验积累的见解和方法。这些应用程序现在已经上线并服务于成千上万的用户,它们塑造了我在构建有影响力的生成型 AI 解决方案方面的思维——而不是盲目追随趋势,如果它不起作用就感到失望。
首先思考你的流程 — 然后才是聊天机器人(或其他接口)
我告诉你不要从聊天机器人开始思考,那么应该从哪里开始呢?答案很简单:业务流程。
公司内部发生的所有事情都是业务流程。业务流程是由不同活动(“工作单元”)、事件(例如错误)和网关(例如决策)组成,并通过工作流连接起来[1]。有用于建模业务流程的工具[2],这些工具以知名的图表形式展现,还有一门围绕分析和改进业务流程的完整研究学科[3][4][5]。业务流程管理是一个很好的工具,因为它不是理论性的,而是在公司中无处不在——即使他们不知道该如何称呼它。
让我给你举个例子。假设你是一家为银行进行房地产估值的公司。在银行发放贷款之前,他们要求房地产估价师估算该房产的价值,以便知道在贷款无法偿还的情况下,他们能得到的实际价值。
创建房地产估值报告是一个我们可以分解成子流程的大型业务流程。通常,估价师会亲自开车到房屋现场,拍摄照片,然后坐下来写一份 20 到 30 页的报告,描述他们的估值。让我们暂时不要陷入“哦,20 到 30 页的报告,我坐在 ChatGPT 前面可能会更快”这种习惯。记住:流程先行,解决方案后谈。
我们可以将这一过程分解为更小的子过程,如开车到房子,拍照,然后撰写报告的不同部分:房屋位置描述、不同房间的状况和大小。当我们深入分析一个单一过程时,我们会看到其中涉及的任务、决策点和事件。例如,在编写位置描述时,房地产评估师坐在桌前,做一些研究,查看 Google 地图上周围的商店,检查城市的交通图,确定房屋的交通连接情况以及街道的情况。这些都是案件处理人员必须执行的活动(或任务)。如果房子是一座位于荒郊野外的农场,那么公共交通选择可能就不那么重要了,因为购买这种房屋的买家通常本就依赖汽车。这种在流程中选择路径的决策被称为决策点(gateway)。

如同本例中基于 BPMN 2.0 模拟的流程。© Marcel Müller 2024
我们在这里应用的这种流程驱动思维方式从评估当前流程开始,而不是直接投入任何 AI 技术。
协调而非基于聊天的互动
通过对我们流程和目标的分析,我们现在可以开始思考带有 AI 的流程应该是什么样子。考虑到我们需要采取的每个步骤很重要。如果我们只关注生成描述的子流程,它可能是这样的:
-
分析房屋周围的位置和商店
-
描述室内的状况
-
除非地点非常偏远:否则要找到最近的公共交通站点
-
为报告写一篇文本
是的,你可以通过与聊天机器人互动的方式,在与“AI sparring partner”(AI 辩论伙伴)合作直到获得输出。但在公司环境下,这有三个主要问题:
-
可重复性:每个人的提示方式都不同。这会导致根据用户的技能和经验水平,输出结果不同。作为公司,我们希望我们的输出尽可能具有可重复性。
-
质量参差不齐:你可能与 ChatGPT 进行过多次互动,直到你重新措辞提示,才能得到你想要的质量。有时你会得到完全错误的答案。在这个例子中,我们没有找到任何一个大语言模型(LLM)能够高质量地描述周围的商店而不出现幻觉。
-
数据和现有系统集成:每个公司都有可能在这些互动中使用的内部知识。是的,你可以通过聊天机器人进行一些检索增强生成(RAG),但这不是最简单和最普遍的做法,也不是每次都能得到良好结果的方法。
这些问题源自聊天背后的大语言模型(LLMs)的核心基础。

聊天机器人交互(左)与预定义可复现过程的 orchestration(右)。© Marcel Müller 2024
企业应用程序不应该仅仅依赖于“提示-响应”交互周期,而应设计为一系列协调的(部分)AI 驱动的过程步骤,每个步骤都针对特定的目标。例如,用户可以触发一个多步骤的过程,整合多种模型和可能的多模态输入,从而提供更有效的结果,并将这些步骤与小脚本结合,利用这些脚本检索数据而无需使用 AI。通过整合检索增强生成(RAG)并最小化人工干预,可以创建更强大和自动化的工作流。
这种 orchestration 方法相比通过交互界面的手动 orchestration 提供了显著的效率提升。而且,并不是过程中的每一步都应完全依赖于 AI 模型。在上述示例中,我们实际上发现,使用 Google Maps API 来获取附近的站点和交通站点在质量上远远优于询问一个好的 LLM(如 GPT-4o)或甚至是一个基于网络搜索的 RAG 引擎(如 Perplexity)。
通过 orchestration 提升效率
让我们暂时想象一个没有 AI 的时代。手动流程可能需要相当多的时间。假设一个任务手动完成需要一个小时,且该过程需要重复四次,总共需要四个小时。使用由生成 AI 驱动的聊天机器人解决方案可以节省 50%的时间(或其他百分比)。然而,剩余的时间则花费在制定提示、等待响应和通过修正与调整确保输出质量上。这就是真正的最佳状态吗?

比较手动操作和基于聊天机器人的自动化所节省的时间。© Marcel Müller 2024
对于重复性的任务,尽管节省了时间,但在组织中,由多个员工执行相同的过程时,仍然可能存在制定提示、等待和调整输出以确保一致性的问题。为了解决这个问题,利用过程模板变得至关重要。
使用模板,流程得以泛化并参数化,以便可以重复使用。创建高质量的过程模板的工作只需进行一次,而针对个别案例的执行则变得更加高效。用于创建提示、质量保证和输出调整的时间大大减少。这是将基于聊天机器人的解决方案与支持 AI 的过程 orchestration 使用模板进行比较时的核心区别。而这一核心区别对质量和可重复性产生了巨大影响。

过程模板的真实节省。© Marcel Müller 2024
此外,我们现在有一个狭窄的领域可以测试和验证我们的解决方案。在一个用户可以插入任何内容的聊天机器人中,以可量化的方式进行测试并获得信心是困难的。我们定义并限制用户可以插入的可能参数和文件的范围越小,我们就越能以量化方式验证解决方案。
在 AI 支持的流程中使用模板,反映了传统流程管理中业务流程引擎的原理。当出现新案件时,这些引擎利用模板库,并选择相应的模板进行编排。为了进行编排,接下来需要填充输入参数。
在我们房地产评估流程的示例用例中,我们的模板有三个输入:物体类型(单户住宅)、一组室内照片以及地址。
该流程模板如下所示:
-
使用 Google Places API 和给定的地址查找周围的商店。
-
使用 OpenAI 视觉 API 描述室内条件。
-
使用 Google Places API 查找最近的交通选项。
-
获取 1 和 3 中的输出 JSON 对象以及交通选项的描述,并使用 GPT-4o 创建一页文本,结构如下:首先是物体描述、商店和交通,然后是内部描述,最后给出每项的评分并总结。
在我们的示例用例中,我们使用内置的无代码构建器,通过 entAIngine 平台实现了应用程序。
请注意,在此流程中,只有 4 个步骤中的 1 个使用了大型语言模型。这是好事!因为 Google Maps API 从不产生幻觉。是的,它可能有过时的数据,但它永远不会“随便编造一些听起来可能是现实的东西”。其次,我们拥有一个人类验证环节,因为现在我们有了可以分析并签署的真实信息源。
在传统流程管理中,模板减少了流程的变异性,确保了可重复性,并提高了效率和质量(如六西格玛方法学所示)。这正是我们需要在此处采用的思维方式。
生成性 AI 应用程序接口
现在,我们已经开始了一个使用大型语言模型(LLM)的流程,同时也解决了很多难题。那么,用户如何与之交互呢?
这种流程的实现可以通过手动编写代码或使用像 entAIngine [6]这样的无代码 AI 流程引擎来完成。
在使用模板建模业务流程时,交互可以以多种方式发生。根据我过去两年的经验,对于 90%的生成性 AI 用例,以下接口是相关的:
• 知识检索接口:类似于搜索引擎,可以引用和参考来源。
• 文档编辑器接口:将文本处理与访问模板、模型和编排结合在一起。
• 聊天接口:用于迭代和互动式的参与。
• 无需专用接口的嵌入式编排(RPA):通过 API 集成到现有接口中。
最终的问题是,什么是最有效的互动方式?是的,对于一些创意用例或非重复性任务,聊天界面可以是首选工具。但很多时候,情况并非如此。用户的核心目标通常是创建某种文档。此时,在编辑器界面中拥有这些模板是非常高效的互动方式。但有时,如果你有一个现有的应用程序并希望用 AI 增强它,你并不需要创建另一个孤立的接口。这里的挑战仅仅是执行正确的流程,在现有应用程序中获取输入数据,并将输出展示在应用程序界面中的某个地方。
这里提到的接口构成了我迄今为止遇到的大多数生成式 AI 用例的基础,同时也使得在企业环境中的可扩展集成成为可能。
最终结论
通过让企业从“如何在任何地方使用 AI 聊天机器人?”转变为“哪些流程包含哪些步骤,以及如何在这些步骤中利用生成式 AI?”企业为真正的 AI 影响力奠定了基础。将 AI 与现有系统结合,然后只关注所需的用户界面类型。这样,你可以释放出那些无法超越聊天机器人的企业从未敢梦想到的效率。
参考文献
[1] Dumas 等人,《业务流程管理基础》,2018 年
[2] 对象管理集团。“业务流程模型与符号(BPMN)版本 2.0.2。”OMG 规范,2014 年 1 月
[3] van der Aalst,《流程挖掘:数据科学在行动》,2016 年
[4] Luthra, Sunil 等人,《全面质量管理(TQM):原理、方法与应用》,第 1 版,CRC 出版社,2020 年。
[5] Panagacos,《业务流程管理终极指南》,2012 年
[6] www.entaingine.com
为什么找不到数据科学的工作
市场困难的原因及你能做些什么
·发布于 Towards Data Science ·9 分钟阅读·2024 年 7 月 13 日
--

照片来自 Marten Bjork 在 Unsplash
显然,“数据是新的石油”,每年需求不断增长,那么为什么现在感觉找不到数据科学家的工作呢?
好吧,在这篇文章中,我将解释为什么会出现这种情况,并提供建议,告诉你现在可以做些什么来增加被雇佣的机会!
经济

照片来自 Vlad Busuioc 在 Unsplash
现在大家都知道,时局艰难。世界上发生了很多可怕的事情,财务稳定性下降,人们普遍在挣扎。总体而言,经济状况远不如过去,这自然导致企业受到影响。
在英国,2022 年初的利率为 0.25%,现在已升至 5.25%。这是一个陡峭的增长,尤其考虑到利率曾经…
为什么机器学习不适用于因果估计
预测推断与因果推断:一个至关重要的区别
·发布于 Towards Data Science ·13 分钟阅读·2024 年 7 月 18 日
--

图片由作者提供,使用 Dall-E 3 生成。
“如果你手里只有一把锤子,那么一切看起来都像钉子。”
在人工智能/机器学习时代,机器学习常常被当作解决所有问题的万能工具。虽然机器学习在很多方面非常有用且重要,但它并非总是解决问题的最佳方案。
最重要的是,机器学习本质上是为了预测推断而设计的,这与因果推断有着本质的不同。预测模型是非常强大的工具,能够帮助我们发现模式和关联,但它们在解释事件发生的原因时力不从心。这正是因果推断发挥作用的地方,它能提供更为明智的决策依据,从而有效地影响结果,并超越简单的关联。
预测推断利用相关性。因此,如果你知道“相关性不代表因果关系”,你应该明白机器学习不应盲目用于衡量因果效应。
将预测推断误认为因果推断可能会导致代价高昂的错误,正如我们将共同看到的!为了避免犯这样的错误,我们将探讨这两种方法的主要区别,讨论使用机器学习进行因果估计的局限性,探索如何正确选择合适的方法,如何在解决问题的不同部分时,两者如何协同工作,并探讨如何在因果机器学习的框架下有效地整合两者。
本文将回答以下问题:
-
什么是因果推断,什么是预测推断?
-
它们之间的主要区别是什么,为什么相关性不意味着因果关系?
-
为什么使用机器学习来推断因果效应是有问题的?
-
何时使用每种类型的推断?
-
如何将因果推断和预测推断结合使用?
-
什么是因果机器学习,它如何适应这个背景?
什么是预测推断?
机器学习是关于预测的。
预测推断涉及基于其他变量的值(如它们的实际值)来估计某个事物(结果)的值。如果你看向窗外,人们戴着手套和帽子,那很可能是冷的。
示例:
-
垃圾邮件过滤:机器学习算法被用来根据电子邮件的内容、发件人以及其他附加信息来过滤安全邮件和垃圾邮件。
-
肿瘤检测:机器学习(深度学习)可以用于通过 MRI 图像检测脑肿瘤。
-
欺诈检测:在银行业,机器学习被用来根据信用卡活动检测潜在的欺诈行为。
偏差-方差:在预测推断中,你希望有一个能够很好地预测结果的模型,大部分时间能在样本外(使用新数据)进行预测。如果偏差能使预测的方差降低,你可能会接受一些偏差。
什么是因果推断?
因果推断是研究因果关系的学科,它关注的是影响评估。
因果推断旨在衡量当你改变其他事物的值时,结果的值会发生什么变化。在因果推断中,你想知道如果改变某个变量(特征)的值,而其他条件保持不变,会发生什么。这与预测推断完全不同,后者是试图根据特征的不同观测值预测结果的值。
示例:
-
营销活动投资回报率:因果推断帮助衡量营销活动(原因)对营销效果(后果)的影响。
-
政治经济学:因果推断通常被用来衡量政策(原因)对结果(后果)的影响。
-
医学研究:因果推断是衡量药物或行为(原因)对健康结果(后果)影响的关键。
偏差-方差:在因果推断中,你不关注预测的质量,比如 R 方等指标。因果推断旨在衡量一个无偏的系数。即使预测能力较低,只要因果效应能够解释结果方差的一小部分,也有可能构建出一个有效的因果推断模型。
关键概念差异:因果推断的复杂性在于,我们想要衡量的东西是我们永远无法实际观察到的。要衡量因果效应,你需要一个参考点:反事实。反事实是没有治疗或干预的世界。因果效应通过将观察到的情况与这个参考(反事实)进行比较来衡量。
假设你头痛。你服下了一颗药丸,过了一会儿,头痛消失了。但这真的是药丸的作用吗?是因为你喝了茶或大量水吗?还是仅仅因为时间过去了?我们无法知道哪个因素或哪些因素的组合起了作用,因为这些效应是混杂在一起的。唯一能够完美回答这个问题的方法是拥有两个平行世界。在两个世界中,一个你服用了药丸,另一个你没有服用药丸。因为药丸是这两种情况之间唯一的不同,所以它能让你声称它是因果关系的原因。但显然,我们没有平行世界可以进行实验。在因果推断中,我们称之为:因果推断的基本问题。

理想情况下,我们需要平行世界来衡量因果效应。图像来源:作者。
因此,因果推断的整个思想是通过找到一个好的反事实来接近这个不可能的理想平行世界情况。这就是为什么黄金标准是随机实验。如果你在一个有代表性的群体中随机分配治疗(药丸与安慰剂),那么唯一的系统性差异(假设所有操作都是正确的)就是治疗方式,因此,结果的统计显著差异可以归因于治疗。

随机实验的示意图。图像来源:作者。
请注意,随机实验也有其局限性,且通过观察数据也可以衡量因果效应。如果你想了解更多,我在这里深入解释了这些概念和因果推断:
如果我们无法直接测试因果关系,我们该怎么办?
[towardsdatascience.com
为什么相关性不意味着因果关系?
我们都知道“相关性不意味着因果关系”。但是,为什么呢?
有两种主要情况。首先,如下面的案例 1 所示,溺水事故和冰淇淋销售之间的正相关可能仅仅是由于一个共同原因:天气。当阳光明媚时,这两者都会发生,但溺水事故和冰淇淋销售之间并没有直接的因果关系。这就是我们所说的虚假相关。第二种情况如案例 2 所示。教育对表现有直接影响,但认知能力影响两者。所以在这种情况下,教育与工作表现之间的正相关与认知能力的影响是混杂在一起的。

“相关性不意味着因果关系”的主要原因。箭头代表因果图中的因果关系方向。图像来源:作者。
正如我在介绍中提到的,预测推断利用的是相关性。因此,任何知道“相关性不代表因果性”的人应该明白,机器学习本质上并不适合因果推断。冰淇淋销售可能是溺水事故当天风险的良好预测因子,即使它们之间没有因果关系。这种关系只是相关性,并由一个共同原因——天气——驱动。
然而,如果你想研究冰淇淋销售对溺水事故的潜在因果影响,你必须考虑到这个第三变量(天气)。否则,由于著名的遗漏变量偏差,你对因果关系的估计将会有偏差。一旦你将这个第三变量纳入分析,你很可能会发现冰淇淋销售对溺水事故不再有影响。通常,解决这一问题的简单方法是将这个变量包含在模型中,这样它就不再是“遗漏”的。然而,混杂变量通常是不可观察的,因此无法简单地将其纳入模型中。因果推断有多种方法来解决这一问题,但讨论这些超出了本文的范围。如果你想了解更多关于因果推断的内容,你可以参考我的指南,点击这里:
针对所有级别的终极自学指南
towardsdatascience.com
因此,因果推断与预测推断之间的一个核心区别在于如何选择“特征”。
在机器学习中,你通常会包括可能提高预测质量的特征,且你的算法可以根据预测能力来帮助选择最佳特征。然而,在因果推断中,一些特征必须无论如何都要包括在内(如混杂变量/共同原因),即使它们的预测能力较低,且效应在统计上不显著。混杂变量的主要兴趣点不是它的预测能力,而是它如何影响我们研究的因果关系的系数。此外,有些特征不应包括在因果推断模型中,例如中介变量。中介变量代表了间接的因果路径,控制这些变量会妨碍我们测量感兴趣的总因果效应(见下图)。因此,主要区别在于,因果推断中是否包括某个特征取决于变量之间假设的因果关系。

中介变量的示例。这里,动机是训练对生产力影响的中介变量。假设员工培训直接提高了他们的生产力,但也通过他们的动机间接提高生产力。员工现在更有动力,因为他们学到了新技能,并看到雇主在员工技能提升方面投入了努力。如果你想衡量培训的效果,大多数时候,你需要衡量治疗的总效果(直接和间接),而将动机作为控制变量会阻止你这样做。图片来源:作者。
这是一个微妙的话题。有关更多细节,请参考“好与坏控制的速成课程” Cinelli 等(2002)。
为什么使用机器学习推断因果效应是有问题的?
假设你将冰淇淋销售和溺水事故之间的正相关关系解释为因果关系。你可能会想方设法禁止冰淇淋。但当然,这可能对结果几乎没有影响。
一个著名的相关性是巧克力消费与诺贝尔奖得主之间的关系(Messerli (2012))。作者发现,在国家层面,巧克力消费与诺贝尔奖得主之间存在 0.8 的线性相关系数。虽然这听起来是一个吃更多巧克力的好理由,但它不应被解释为因果关系。(请注意,Messerli(2012)中提出的潜在因果关系的论点后来被推翻了(例如,P Maurage 等人(2013))。

诺贝尔奖得主与每百万人口巧克力消费(千克/年/人)之间的正相关关系(见 Messerli(2012))。图片来源:作者。
现在让我分享一个更为严肃的例子。假设你试图优化一个内容创作者的帖子。为此,你构建了一个包含众多特征的机器学习模型。分析结果表明,下午晚些时候或晚上发布的帖子表现最好。因此,你推荐一个精确的时间表,让你仅在下午 5 点到晚上 9 点之间发布帖子。然而,一旦实施后,每个帖子的展示量却急剧下降。发生了什么?机器学习算法基于当前模式进行预测,按数据的表面情况进行解读:晚些时候发布的帖子与更高的展示量相关联。最终,晚上发布的帖子往往是更具自发性、不太有计划的,作者并不是特别为了迎合观众,而是分享一些有价值的内容。所以,问题不在于时间,而在于帖子的性质。这种自发的性质可能很难通过机器学习模型捕捉到(即使你编码了一些特征,如长度、语气等,也可能不容易捕捉到这一点)。
在营销中,预测模型通常用于衡量营销活动的投资回报率(ROI)。
通常,像简单的 营销组合建模(MMM) 这样的模型会受到遗漏变量偏差的影响,ROI 的衡量会产生误导。
通常,竞争对手的行为可能与我们的营销活动相关联,并且也会影响我们的销售。如果没有正确考虑这一点,投资回报率(ROI)可能会被低估或高估,导致次优的商业决策和广告支出。
这个概念对于政策和决策制定也非常重要。在 Covid-19 大流行初期,一位法国“专家”使用一张图表来论证封锁政策是适得其反的(见下图)。图表显示封锁措施的严格程度与 Covid-19 相关死亡人数之间存在正相关关系(更严格的封锁与更多的死亡相关)。然而,这种关系很可能是由相反的因果关系推动的:当情况严重(死亡人数多)时,各国会实施严格措施。这被称为逆向因果关系。实际上,当你正确地研究一个国家在封锁期间病例和死亡人数的变化,并控制潜在的混杂因素时,你会发现一个强烈的负面影响(参见 Bonardi et al. (2023))。

重复使用图表来论证封锁措施无效。绿色表示最低封锁措施,红色表示最严格的封锁措施。图片由作者提供。
每种类型的推论应该在什么情况下使用?
机器学习和因果推论都是非常有用的;它们只是服务于不同的目的。
和数字及统计数据一样,问题通常不在于度量本身,而在于它们的解释。因此,相关性是有价值的信息,但只有在你盲目地将其解释为因果效应时,它才会变得有问题。
何时使用因果推论: 当你想理解因果关系并进行影响评估时。
-
政策评估: 用于确定新政策的影响,例如新教育项目对学生表现的影响。
-
医学研究: 用于评估新药物或治疗对健康结果的有效性。
-
经济学: 用于理解利率变化对通货膨胀或就业等经济指标的影响。
-
营销: 用于评估营销活动对销售的影响。
因果推论中的关键问题:
-
X 对 Y 的影响是什么?
-
改变 X 会导致 Y 的变化吗?
-
如果我们对 X 进行干预,Y 会发生什么变化?
何时使用预测推论: 当你想进行准确的预测(特征与结果之间的关联)并从数据中学习模式时。
-
风险评估: 用于预测信用违约或保险索赔的可能性。
-
推荐系统: 基于用户的过去行为向其推荐产品或内容。
-
诊断: 用于疾病检测的医学图像分类。
预测推断的关键问题:
-
给定 X,Y 的期望值是多少?
-
我们能否基于关于 X 的新数据预测 Y?
-
我们能够多准确地使用当前和历史上的 X 数据来预测 Y?
如何将因果推断和预测推断结合使用?
虽然因果推断和预测推断有不同的目的,但它们有时会一起工作。Dima Goldenberg,Booking.com 的高级机器学习经理,在与 Aleksander Molak(《Python 中的因果推断与发现》一书的作者)的一次podcast中完美地展示了这一点。
Booking.com 显然在推荐系统方面下了很大功夫。“推荐”是一个预测问题:“客户 X 更倾向于看到哪种类型的产品?”因此,这一步通常通过机器学习来解决。然而,还有一个相关的问题:“这个新推荐系统对销售/转化等方面的影响是什么?”在这里,“effect … on” 这个关键词应该让你意识到,第二步必须使用因果推断来解决。此步骤将需要因果推断,准确来说是随机实验(A/B 测试)。
这是一个典型的工作流程,展示了机器学习和因果推断的互补角色。你通过机器学习开发预测模型,然后通过因果推断评估其影响。
那么,因果机器学习是什么,它在这个背景下如何发挥作用?
最近,出现了一个新领域:因果机器学习。虽然这是一个重要的突破,但我认为它增加了混淆。
很多人看到“因果机器学习(Causal ML)”这个术语时,往往误以为可以随便使用机器学习进行因果推断。
因果机器学习是两者世界的优雅结合。然而,因果机器学习并不是盲目地使用机器学习进行因果推断。它实际上是将机器学习与因果推断结合在一起,以提高结果。因果推断的关键区别概念在因果机器学习中依然有效。特征选择依赖于假设的因果关系。
让我展示因果机器学习中的两种主要方法,来说明这种有趣的结合。
A. 处理高维数据和复杂的函数形式
在某些情况下,你的因果推断模型中有大量的控制变量。为了减少遗漏变量偏误的风险,你加入了许多潜在的混杂因素。该如何处理这么多的控制变量呢?也许你在一组控制变量之间有多重共线性,或者也许你应该控制非线性效应或交互效应。这很快就会变得非常复杂,并且使用传统的因果推断方法来解决可能显得非常随意。
由于机器学习在处理高维数据方面特别高效,它可以用来应对这些挑战。机器学习将被用于找到最佳的控制组和正确的函数形式,以确保模型不会遭受多重共线性问题,同时仍能满足衡量因果效应的条件(见:双重机器学习方法)。
B. 异质性处理效应
因果推断历史上专注于衡量平均处理效应(ATE),这是治疗平均效果的一个衡量标准。然而,正如你一定知道的,平均值是有用的,但它也可能具有误导性。治疗的效果可能会根据受试者的不同而导致不同的效果。想象一下,一种新药物平均能显著降低癌症风险,但实际上,整个效果是由男性的结果推动的,而对女性的效果则为零。或者,想象一下一个营销活动,虽然总体上提高了转化率,但实际上在某些特定区域却产生了负面影响。
因果机器学习使我们能够超越平均处理效应,揭示这种异质性,通过识别条件平均处理效应(CATE)。换句话说,它有助于识别不同受试者特征条件下的处理效应。
揭示条件平均处理效应的主要方法叫做因果森林。然而,需要注意的是,尽管这种方法可以帮助我们找到对治疗有不同反应的子群体,但通过机器学习揭示的这些群体的特征不一定是因果的。想象一下,模型揭示广告对智能手机用户和对平板用户的效果完全不同。‘设备’不应被解读为这种差异的原因。可能真正的原因并未被测量,但与这一特征相关,例如,年龄。
结论
如今,区分预测与因果推断是至关重要的,这可以避免在营销、政策制定和医学研究等多个领域出现代价高昂的错误。我们探讨了为何尽管机器学习具有显著的预测能力,但由于它依赖于相关性/关联性而非因果关系,它本质上并不适合因果推断。希望你能够理解这一区分,并为不同类型的问题选择合适的模型。

说明为何将相关性误认为因果效应会导致糟糕的决策。图片来源:War and Peas,Elizabeth Pich 和 Jonathan Kunz。
如果你想了解更多关于因果机器学习的内容,这里有一些有价值且可靠的资源:
因果机器学习:
双重机器学习:
异质性治疗效应:
为什么大多数交叉验证可视化是错误的(以及如何修复它们)
更好的机器学习可视化
停止使用移动的框!
·发表于Towards Data Science ·阅读时长:10 分钟·2024 年 11 月 16 日
--
你知道那些在每个数据科学教程中出现的交叉验证图表吗?那些通过不同颜色的框在四处移动来解释我们如何拆分数据进行训练和测试的图表?像这样:

你见过那个吗?图像由作者提供。
我也见过这些图表——多得数不清。这些图表非常常见——它们已经成为解释交叉验证的首选方式。但在我作为设计师和数据科学家看这些图表时,我注意到了一些有趣的事情。
当我们看到一个黄色框移动到不同的位置时,我们的大脑自动认为它是一个框在四处移动。
这就是我们大脑的工作方式——当我们看到类似的东西移动到新位置时,我们会认为它是同一个东西。(这其实也是卡通和动画能够有效工作的原因!)

你可能认为动画版本更好,但现在你忍不住开始跟着蓝色框移动,并且渐渐忘记了这应该代表交叉验证的工作方式。来源:维基百科
但问题是:在这些图表中,每个新位置的框应该显示不同的数据块。因此,尽管我们的脑袋自然想要跟踪这些框,我们却必须告诉大脑,“不,不,那不是一个框在移动——它们是不同的框!”这就像我们在与大脑的自然工作方式作斗争,仅仅为了理解这个图表的含义。
作为一个同时从事设计和数据工作的人员,我开始思考:也许有更好的方法?如果我们能以一种更符合大脑处理信息方式的方式展示交叉验证呢?

所有可视化:作者使用 Canva Pro 创建。已优化移动设备显示;在桌面上可能显得过大。
交叉验证到底是关于什么的?
交叉验证是确保机器学习模型在现实世界中表现良好的方法。我们不是仅测试一次模型,而是使用数据的不同部分多次测试模型。这有助于我们理解模型在处理新数据时的表现。
事情是这样发生的:
-
我们拿到数据
-
将其分成若干组
-
使用一些组进行训练,其他组进行测试
-
用不同的分组重复这个过程
目标是可靠地理解我们模型的表现。这是核心思想——简单且实用。
(注:我们将在另一篇文章中讨论不同的验证技术及其应用。现在,我们先专注于理解基本概念以及为何当前的可视化方法需要改进。)
当前交叉验证图示有什么问题?
打开任何机器学习教程,你可能会看到这些类型的图示:
-
长方形框被分成不同的部分
-
箭头指示部分数据的移动
-
不同的颜色表示训练数据和测试数据
-
相同图示的多个版本并排显示

当前,如果你搜索“交叉验证”,你看到的第一个图像就类似于这个。(图片由作者提供)
下面是此类图示存在的问题:
并不是每个人都以相同的方式看到颜色
颜色在展示数据分割时会带来实际问题。有些人无法区分某些颜色,而其他人可能根本看不见颜色。当以黑白打印或在不同显示屏上查看时,颜色会发生变化,导致可视化失效。将颜色作为区分数据部分的主要方式意味着一些人由于色觉问题错过了重要信息。

不是每个人看到的颜色都相同。图片由作者提供。
颜色使记忆变得更加困难
关于颜色的另一个问题是,它看起来似乎有助于解释问题,但实际上会给大脑带来额外的负担。当我们为数据的不同部分使用不同的颜色时,我们必须主动记住每种颜色代表什么。这变成了一项记忆任务,而不是帮助我们理解实际概念。颜色与数据分割之间的关联并不是自然而明显的——这是我们需要学习并在试图理解交叉验证时保持跟踪的内容。
我们的大脑本能地不会将颜色与数据分割关联起来。

这些是我们在前面图示中使用的颜色。为什么原始数据集是绿色的?然后分成蓝色和红色?
一次展示过多信息
当前的图表也存在信息过载的问题。它们试图在一个图像中展示整个交叉验证过程,这就带来了不必要的复杂性。多个箭头、大量标签,所有的元素都在争夺注意力。当我们试图同时展示过程的每一个方面时,我们反而让理解每个独立部分变得更加困难。这个方法没有澄清概念,反而增加了我们需要首先解码的复杂层次。

标签太多,颜色太多,箭头太多,导致很难集中注意力。
误导性的移动
这些图表中的运动会对交叉验证的实际运作方式产生根本误解。当我们展示箭头和流动元素时,实际上是在暗示一个并不存在的顺序过程。交叉验证的分割并不需要按特定顺序进行——分割的顺序根本不影响结果。
这些图表还给人错误的印象,认为数据在交叉验证过程中是物理移动的。实际上,我们每次只是从原始数据集中选择不同的行。数据保持在原位,我们只是更改每次分割中用于测试的行。当图表显示数据在分割之间流动时,它们给过程增加了不必要的复杂性。

虽然图表通常是从上到下流动的,但很难跟随操作的顺序。模型训练和计算结果的时间仍然不明确。训练什么时候进行?每个计算的结果是什么?
我们真正需要的是什么
我们需要这样的图表:
-
不要单纯依赖颜色来解释事物
-
清晰地分隔信息块
-
明确不同测试组是独立的
-
不要使用不必要的箭头和移动
让我们来解决这个问题。与其让我们的思维方式变得不同,为什么不创建一些看起来更自然的东西呢?
更好的交叉验证可视化方法
让我们尝试一些不同的方式。首先,这是大多数人看到的数据样子——一行行、列列的数字和索引。

这是我在关于分类算法的文章中使用的常见数据集。
受到这个结构的启发,下面是一个更有意义的图表。

更简单但清晰的交叉验证描绘。
下面是为什么这个设计在逻辑上更合理的原因:
-
真实数据结构: 它与交叉验证中数据的实际工作方式一致。在实践中,我们是在选择数据集的不同部分,而不是移动数据。每一列显示了我们在每次测试中使用的具体分割。
-
独立划分: 每个划分明确显示它是不同的数据。不同于移动的盒子,这可能会让你误以为“它是相同的测试集在移动”,这里显示 Split 2 使用的是与 Split 1 完全不同的数据。这与代码中实际发生的情况相符。
-
数据保护: 通过保持所有折叠的列高度相同,我们展示了交叉验证的一个重要规则:你始终使用整个数据集。一些部分用于测试,剩余部分用于训练。每一条数据都会被使用,没有遗漏。
-
完整覆盖: 从左到右,你可以轻松验证一个重要的交叉验证原则:数据集的每一部分将作为测试数据恰好使用一次。
-
三折简洁性: 我们在这里专门使用三折交叉验证,因为:
a. 它清晰地展示了关键概念,而不会让细节显得过于繁杂。
b. 这个模式很容易理解:三个不同的折叠,三个测试集。足够简单,可以在脑海中跟踪每个折叠中哪些部分用于训练,哪些部分用于测试。
c. 非常适合用于教学目的——添加更多的折叠(比如 5 或 10 个)会使得可视化变得更加杂乱,而不会增加概念上的价值。
(注意:虽然在实践中 5 折或 10 折交叉验证可能更为常见,但三折交叉验证完全可以用来说明这一技术的核心概念。)
为了更清晰地显示索引
虽然上述概念是正确的,但考虑实际的行索引会使其更加清晰:

这是一个增强版的变化,增加了细微的索引,使得更容易看出每个折叠对应的数据集的哪一部分。虚线帮助区分各个索引。
以下是这个视觉改进的一些原因:
-
与其说是“不同的部分”,我们可以看到,Fold 1 测试的是第 1 到第 4 行,Fold 2 测试的是第 5 到第 7 行,而 Fold 3 测试的是第 8 到第 10 行。
-
“完整覆盖”变得更为具体:第 1 到第 10 行每一行都恰好在测试集中出现一次。
-
训练集是明确的:当在第 1 到第 4 行进行测试时,我们在第 5 到第 10 行上进行训练。
-
数据独立性显而易见:测试集使用了不同的行范围(1–3,4–6,7–10)。
这种基于索引的视图并没有改变概念——它只是让它们变得更加具体,且更容易在代码中实现。无论你将其视为部分还是特定的行号,关键原则始终保持不变:独立的折叠、完整覆盖和使用所有数据。
添加一些颜色
如果你觉得黑白版本过于简单,这也是另一种可接受的选项:

这是简单图示的一种变化,给每个折叠的数字添加了颜色。
虽然在这个版本中使用颜色可能因为色盲和记忆负担等问题而看起来有些问题,但它仍然可以作为一个有用的教学工具,辅以更简单的版本。
主要原因是它不仅仅使用颜色来显示信息——行号(1–10)和折叠编号可以告诉你你需要了解的一切,颜色只是一个额外的辅助。
这意味着即使有人无法正确看到颜色或以黑白打印,他们仍然可以通过数字理解一切。虽然必须记住每种颜色的含义可能会让学习变得更加困难,但在这种情况下,你不需要记住颜色——它们只是作为额外的帮助提供给那些觉得它们有用的人,但你完全可以在没有颜色的情况下理解图表。
就像以前的版本一样,行号也通过准确显示数据如何被拆分来提供帮助,无论你是否关注颜色,都更容易理解交叉验证在实践中的运作方式。
即使你完全忽略颜色,视觉化仍然是完全功能性的且易于理解。

尝试上面的挑战。对于有限数量的颜色,它有助于更快地跟踪位置的变化。
为什么这种方式更有效:从设计到数据
让我们从用户体验的角度以及数据科学的角度来看一下,为什么我们的新设计是有道理的。
匹配思维模型: 想一想你是如何向别人解释交叉验证的。你可能会说:“我们先用这些行进行测试,然后是这些行,再然后是这些行。”我们的视觉化现在完全符合我们思考和谈论这个过程的方式。我们不仅仅是为了美观,我们让它与现实匹配。
数据结构的清晰性: 通过将数据以带索引的列形式展示,我们揭示了数据集的实际结构。每行都有一个编号,每个编号仅出现在一个测试集里。这不仅是良好的设计,它还准确反映了我们的数据在代码中的组织方式。

即使是默认的交叉验证方式——洗牌,我们也可以通过更改索引让人们明白数据正在被洗牌。
关注重要的内容: 我们以前展示交叉验证的方式让我们关注移动的部分。但在交叉验证中,真正重要的不是这些,真正重要的是:
-
我们在测试哪些行?
-
我们是否在使用所有的数据?
-
每行是否仅被用作一次测试?
我们的新设计可以一目了然地回答这些问题。
基于索引的理解: 我们不再显示抽象的彩色框,而是显示实际的行索引。当你编写交叉验证代码时,你正在处理这些索引。现在,视觉化与代码相匹配——折叠 1 使用行 1-4,折叠 2 使用行 5-7,以此类推。

使用类似的图示,我们还可以展示如何进行留一法交叉验证。每次测试集只使用一个数据点!拆分编号和选择的测试集索引也很好地匹配。
清晰的数据流: 布局展示了数据从左到右流动:这是你的数据集,这是它是如何被分割的,这是每个分割的样子。它与交叉验证的逻辑步骤匹配,而且也更容易查看。

阐明箭头的作用,表示训练与测试过程,可以更清楚地展示交叉验证中有多少模型以及它们的输出是什么。你可能会注意到,分割之间没有连接元素的箭头。
结论:当可视化与你的代码相匹配时
这是我们关于重新绘制交叉验证图表所学到的内容:
让你的代码与传统匹配: 我们通常遵循传统的展示方式,仅仅因为大家都这样做。但交叉验证实际上是关于选择不同的行数据进行测试,那么为什么不直接展示这一点呢?当你的可视化与你的代码相匹配时,理解也会自然而然地跟随而来。
数据结构很重要: 通过展示索引和实际数据分割,我们揭示了交叉验证的真实工作原理,同时也提供了更清晰的图像。每一行都有其位置,每一个分割都有其目的,你可以精确追踪每个步骤中发生的事情。
简洁有其目的: 事实证明,展示更少的内容实际上可以解释更多。通过专注于核心部分——哪些行用于测试,以及何时使用——我们不仅简化了可视化,还突出了在交叉验证中真正重要的内容。
展望未来,这种思维可以应用于许多数据科学概念。在制作下一个可视化之前,问问自己:
-
这是否展示了代码中实际发生的事情?
-
是否可以追踪数据流?
-
我们是在展示结构,还是仅仅是在遵循传统?
好的可视化不是遵循规则——它是展示真相。有时候,最清晰的真相也是最简单的。
关于插图
除非另有说明,所有图像均由作者创作,并结合了来自 Canva Pro 的授权设计元素。
𝙎𝙚𝙚 𝙢𝙤𝙧𝙚 𝙈𝙤𝙙𝙚𝙡 𝙀𝙫𝙖𝙡𝙪𝙖𝙩𝙞𝙤𝙣 & 𝙊𝙥𝙩𝙞𝙢𝙞𝙯𝙖𝙩𝙞𝙤𝙣 𝙝𝙚𝙧𝙚:

模型评估与优化
查看列表3 个故事


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

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

为什么个性化项目失败
严峻的真相与前行的道路
·发表于Towards Data Science ·阅读时间 6 分钟·2024 年 9 月 3 日
--

图片由Luke Jones提供,来源于Unsplash
TLDR:尽管许多个性化项目有雄心壮志、扎实的技术设计、充足的资金和专注的团队,但仍然停滞不前。本文探讨了常见的原因,并为项目负责人和技术人员提供了前行的路径。
免责声明:这是我个人的看法,不一定代表我的雇主的立场。这些是我真实的想法;没有使用 GPT。
感到卡住
“我们已经做这个个性化项目超过十年了。我觉得我们卡住了。我马上就要做战略审查了。我该怎么办?”这是我与合作超过十年的客户 Alex 说的话。他的头发变灰,变稀。
Alex 并不孤单。许多负责个性化议程的项目负责人有着类似的感受,但各自方式不同。有些人觉得项目没有得到足够的重视;另一些人则在利益方面受到高管的质疑;还有很多人觉得他们在做同样的事情,却没有朝着最初的目标前进。
我们该怎么办?让我们从认识几个硬核真相开始。
硬核真相 #1:它过于复杂
企业因不同原因陷入困境。许多公司让他们的个性化项目变得过于复杂…
为什么在 AI 时代,PoC 逐渐过时?
·发布于 Towards Data Science ·阅读时长 8 分钟·2024 年 11 月 7 日
--

图片由 Canva AI Image Generator 创建
最近,我有机会参加了 OxML 2024 计划,该计划汇集了从事机器学习的人们,共同讨论 AI 领域的最新进展。在活动期间,我聆听了 Reza Khorshidi 发表的鼓舞人心的演讲,他是牛津大学的机器学习研究员,也是ELANDI的首席产品与 AI 官员。他谈到了一个新概念——AI 中的“第一代产品”,这让我思考 AI 如何改变我们工作的方式。
随着 AI 的不断发展,显然传统方法,如概念验证(PoC),正在让位于更具意义的框架,如价值验证(PoV),因为公司不仅寻求技术可行性,还需要在这个快速变化的环境中衡量商业影响。
我将简要总结他关于PoC 与 PoV的分享。
任何熟悉产品构建的人都见过这张图——通常是构建产品的基础。每个公司都希望构建既具吸引力、可行又可持续的产品。

图表展示了构建产品的基础——图片来自作者
在构建产品时,我们关注三个关键问题。以构建自动驾驶汽车为例:
-
我们能否利用机器学习开发自动驾驶汽车?(技术问题)
-
如果我们建立它,人们会购买吗?(设计问题)
-
我们负担得起构建它吗?(战略问题)
即使我们技术上能够造出这辆车,而且市场上有需求,真正的挑战可能在于我们是否能够在不因为高昂的研发成本而破产的情况下做到这一点(战略性问题)。
当今许多商业挑战已经过时。像文本提取、聊天机器人或文本总结这些问题,曾经需要研发团队数月才能解决,现在可以通过使用基础模型迅速解决。这代表了第一代世界中的一次重大转变,在这种转变中,我们现在可以假设这些问题的技术层面已经解决。
基于这个观点,我将探讨为什么在 AI 领域中 PoC 可能不再适用,以及为什么组织应该转向 PoV。
什么是 PoC?
概念验证(PoC)传统上评估一个解决方案在全面实施之前是否可行。它的目的是展示一个想法是否能够有效实施,并满足必要的技术要求。然而,随着 AI 解决方案复杂性的增加——涉及机器学习、预测分析和自然语言处理——PoC 正逐渐被认为不足够。公司现在正在转向价值验证(PoV),它不仅展示技术可行性,还评估 AI 系统的潜在商业价值和影响。
是什么导致了这种转变?
PoC 主要解决技术可行性,侧重于技术或解决方案是否可以实施。然而,它们往往忽视了更广泛的 AI 挑战,例如可扩展性、长期价值和现实世界的影响。尽管 AI 模型在受控环境中可以产生有希望的结果,但这些结果并不总是能在扩展时转化为实际的商业价值。
PoC 通常是时间有限且范围狭窄的,旨在实现短期结果。然而,AI 需要持续学习和适应,因为许多机器学习模型随着时间的推移不断改进。它们的全部利益只有在持续使用和迭代后才能实现。
今天的决策者越来越关注可衡量的商业价值——例如成本节省、收入增长或效率提升——而不仅仅是技术可行性。在许多情况下,PoC 通常在真正的投资回报率(ROI)能够评估之前就结束了。
AI 项目中 PoV 的崛起
从 PoC 到 PoV 的转变至关重要,因为组织需要明确的证据来证明其 AI 投资能够带来有意义的结果。例如,以文档智能解决方案为例。PoV 可以衡量文档处理中的时间节省,以及这些改进带来的运营成本降低——这些度量直接影响公司的盈亏。
根据最近麦肯锡的报告,虽然 50%的企业声称已经“整合了某种形式的 AI”,但只有 21%的企业成功地在多个业务单元中嵌入了 AI。许多这些项目失败,因为 AI 解决方案未能达到业务预期。
同样,智能自动化交流会美国 2023 报告对 50 位行业领导者进行了调查,了解他们在证明 AI 投资价值和将 PoC 转化为实际应用中的挑战。最大的挑战是可视化这些投资的全程端到端影响。
报告中提到的另一个重大挑战是如何量化 AI 的收益以及决定追踪哪些指标。近 41%的受访者将成本节省视为证明 AI 价值的关键指标,而 25%的人强调了时间效率的提升。
公司如何确保 PoV(价值证明)的成功?
在将 AI 应用于现实世界时,我们需要确保它确实为业务提供价值。
数字化转型公司Mindtree进行的一项研究调查了 IT 领导者对 AI 采纳的看法,结果显示,尽管 85%的组织已经实施了数据战略,77%的组织已经投资于与 AI 相关的技术,但只有 31%的公司看到了投资回报。
那么,公司如何确保 PoV(价值证明)的成功呢?
这里是我从OxML 2024项目的各种讲座中汇编出的 8 个简单策略:

图片来自作者
#1 定义应用案例
在制定 AI 战略时,许多公司聚焦于痛点并定义应用案例。思维方式必须发生转变,重点应放在识别成功的定义上——无论是更快的文档处理、更好的欺诈检测,还是更准确的预测。
例子:一家银行识别到需要加速贷款审批,聚焦于诸如手动文档审核和易出错的数据输入等痛点。他们定义了一个特定的 AI 应用案例,用于自动化文档处理,以减少审批时间并提高客户满意度。
#2 定义价值标准
定义利益相关者、客户和用户的价值。价值标准是公司希望通过 AI 解决方案实现的具体、可衡量且可达成的成果。它们可以与收入、成本、质量、客户满意度或任何其他相关指标相关。价值标准应与业务目标和战略一致,并根据其重要性和可行性进行优先排序。
示例:一家电子商务公司为推荐系统 AI 项目定义了价值标准,设定了可衡量的目标,例如将平均订单价值提高 10%并提升点击率 15%。这与推动收入增长和客户参与的商业目标保持一致。
#3 跟踪价值指标
价值指标是衡量 AI 项目进展和影响的关键绩效指标(KPI)。 它们可以是定量的,如准确性、速度或转化率,也可以是定性的,如用户反馈、推荐或评价。这些指标应定期监控,并用于调整项目范围、功能和优先级。
示例:一家医疗保健提供商使用 AI 进行预测患者结果,并跟踪关键指标,如预测准确性、响应时间和患者反馈。定期监控这些 KPI 使公司能够调整模型,以改善临床决策支持和患者护理质量。
#4 展示价值交付
仅仅跟踪价值指标是不够的。公司还必须展示 AI 项目如何为利益相关者、客户和用户创造有意义的价值。价值交付包括清晰、简洁且具有说服力地展示 AI 项目的成果和效益。 这可以通过报告、仪表盘、演示或案例研究等多种方式实现。
示例:一家物流公司通过仪表盘展示 AI 项目的成功,显示交货时间缩短和延误减少。案例研究详细描述了 AI 如何改善路线优化,展示了为利益相关者带来的成本节省和服务改善。
#5 对齐价值期望
价值对齐确保 AI 项目与商业环境和目标保持一致,符合利益相关者、客户和用户的期望。AI 项目并非孤立运行,它们是更广泛商业环境的一部分,并且受到市场趋势、客户行为和竞争者行动等因素的影响,同时也能反作用于这些因素。
示例:一家零售公司实施了 AI 驱动的需求预测工具,但将项目目标与公司的季节性营销策略和客户购买趋势对齐。与利益相关者的定期更新确保项目始终与市场需求和客户期望保持一致。
#6 优化价值创造
价值优化涉及基于数据和反馈对 AI 项目进行增强、扩展或延伸。AI 项目从未真正完成——它们在不断发展、学习和改进。为了最大化其价值,这些项目需要持续优化,这可能包括提升性能、可靠性、可用性和可访问性,或降低成本、风险和局限性。
示例:一家 SaaS 提供商持续收集关于 AI 驱动的支持聊天机器人的用户反馈,优化其准确性和响应速度。这一持续的改进帮助减少了支持成本,同时提升了用户满意度和留存率。
#7 邀请合适的人
不要仅仅将 AI 项目交给技术团队。跨团队协作确保了 PoV 涵盖业务的各个方面,而不仅仅是技术表现。如果需要,可以提升团队的技能,以增加价值。
在Mindtree调查的受访者中,44%报告从外部招聘顶尖人才,30%与学术机构建立了合作关系,22%组织黑客马拉松活动以应对新挑战。
示例:一家保险公司在开发用于欺诈检测的 AI 模型时,邀请了其风险、合规和 IT 团队参与。通过涉及这些部门,他们确保 AI 解决方案符合监管标准,最小化风险,并有效解决技术和业务需求。
#8 放眼未来,但从小做起
一家公司可能对 AI 如何转变其业务流程有明确的愿景,但明智的做法是从一个小而可控的用例开始。小规模启动有助于快速取得成功,同时确保解决方案能够扩展以满足未来需求。
示例:一家全球制造商设想一个 AI 解决方案,用于监控全球所有工厂的设备健康状况。然而,他们首先在一个设施中实施该方案,以展示在减少设备停机时间方面的快速成果。在试点成功后,他们将其推广到整个公司,并根据小规模推广所获得的见解进行扩展。
AI 项目的未来
在 AI 驱动的世界中,技术可行性往往不是最紧迫的问题,PoC(概念验证)已不再足以证明大规模投资的合理性。价值证明(PoV)提供了一个更为强大的框架,专注于实际结果、可衡量的商业价值和战略对齐。从 PoC 到 PoV 的过渡使公司能够降低 AI 项目的风险,确保利益相关者的支持,并实现可扩展的成功。
通过专注于价值而不仅仅是可行性,组织能够更好地定位自己,利用 AI 的真正潜力,确保其投资带来有意义的业务转型。
资源
www.linkedin.com/advice/3/how-do-you-prove-business-value-ai-delivered
www.intelligentautomation.network/
来自 COVID-19 的教训:为什么概率分布如此重要
用极值理解分布:数据科学系列(完结)
·发布于 Towards Data Science ·阅读时长 11 分钟·2024 年 12 月 27 日
--

图片由 Glen Carrie 提供,来源于 Unsplash
如果你不是 Medium 的付费会员,我会将我的文章免费提供给大家:朋友链接
如果你一直在关注我的文章,你可能已经注意到我最近对概率分布的强调。我花了很多时间讲解它们的重要性,这绝不是没有理由的。如果你已经理解了这些分布为何至关重要,这篇文章将为你提供很好的补充。如果还没有,我希望这篇文章能为你提供一些新的见解!
让我问你一个问题
为什么概率分布如此重要? 为什么我们要花这么多时间研究概率密度函数(PDF)和累积分布函数(CDF)?提示:答案取决于你问谁。
然而,你可能是为了更直接的答案而来的。所以……对我来说……我会通过帮助你理解极值像 Xₘᵢₙ 和 Xₘₐₓ 来回答这个问题。(如果你只想要答案,请跳到文章结尾)!
希望我的解释既直观又易于理解,没有多余的行话。
为什么在商业健康中比例比绝对数字更重要
理解比例是洞察企业健康并做出更明智决策的关键
·发布于Towards Data Science ·阅读时间 4 分钟·2024 年 9 月 6 日
--

图片来自 Jill Wellington,来自 Pixabay
假设你是一位农场主,有两个苹果园。一天,你决定数一数每个果园的苹果。A 果园有 1,000 个苹果,而 B 果园只有 500 个。乍一看,似乎 A 果园显然更胜一筹。但如果我告诉你,A 果园有 10,000 棵树,而 B 果园只有 500 棵树呢?突然间,B 果园看起来也不那么糟糕了,是吗?
这就是比例的力量。在商业中,正如在农业中一样,绝对数字可能会误导你。要真正了解你的企业健康状况,你需要关注比例。
你可能每天都在查看绝对数字,比如你本周的收入是 10,000 美元。今天有 250 个客户下单,而昨天是 200 个,上周同一天是 100 个等等。
另一方面,比例就像是小字说明。它们可能不像绝对数字那样立刻引人注目,但它们提供了更准确的企业健康状况图景。
以利润率为例。这是一个比较你利润和收入的比例。较高的利润率表明…
为什么表示微调是目前最有效的方式?
表示微调 LLAMA3 的逐步指南
·发布于Towards Data Science ·阅读时间:11 分钟·2024 年 5 月 26 日
--
你知道吗,实际上只用少量的参数和一个只有 10 个数据点的小数据集,就可以微调一个语言模型?
嗯,这不是魔法。

照片由Mrika Selimi提供,来自Unsplash
我遇到了一种新方法,称为“表示微调”,这是由著名的斯坦福 NLP 团队开发的。
最近的参数高效微调(PEFT)方法,如低秩适应(LoRA),使得你只需更新少量参数即可微调大型语言模型。这节省了许多时间和计算资源,是一大进步。
但还有一种更简洁的方法:表示微调。它涉及更少的参数,并且表现得更好。它不是更新权重,而是操作表示层的激活值。
我们首先向你介绍表示微调,然后将其与 PEFT 方法进行比较。接着我们展示如何在实践中应用这种方法,使用 LLAMA3。这个方法适用于任何来自 HuggingFace 的预训练语言模型。
让我们通过一个例子,展示如何配置 LLAMA3 来过滤掉医学建议——一个仅包含五个数据点的简单数据集……
为什么再训练比训练更难
从神经网络的角度看学习、去学习和再学习
·发布于 Towards Data Science ·12 分钟阅读·2024 年 1 月 23 日
--

图片由 Mary Blackwey 提供,来源 Unsplash
在这个快速变化的世界里,人类需要快速适应新的环境。神经网络展示了为什么这一点说起来容易做起来难。我们的文章通过感知机展示了为什么“去学习”和“再学习”可能比从零开始学习更具成本。
引言
人工智能(AI)的一个积极副作用是,它可以帮助我们更好地理解我们自己的人的智能。具有讽刺意味的是,AI 也是挑战我们认知能力的技术之一。与其他创新技术一起,它以惊人的速度转变现代社会。在他的书《Think Again》中,亚当·格兰特指出,在动荡的环境中,重新思考和去学习可能比思考和学习更为重要[1]。
对于老龄化社会来说,这可能是一个挑战。在德国有一句话:“Was Hänschen nicht lernt, lernt Hans nimmermehr。”英语中类似的说法是:“A tree must be bent while it is young” 或者更不客气地说:“You can’t teach an old dog new tricks。”本质上,这些说法都暗示着年轻人比老年人更容易学习。但这真的正确吗?如果是,背后的原因是什么?
显然,从生理学角度来看,年轻人与年长者的大脑结构是不同的。然而,在个体层面上,这些差异变化很大[2]。根据 Creasy 和 Rapoport 的研究,"即使在年老时,大脑的整体功能也可以保持在高效的水平"[3]。除了生理学因素,动机和情感似乎在学习过程中扮演着至关重要的角色[4][5]。Kim 和 Marriam 在一家退休机构的研究表明,认知兴趣和社会互动是强大的学习动力[6]。
我们的文章从数学和计算机科学的角度讨论了这个问题。受到 Hinton 和 Sejnowski [7]的启发,我们进行了一项关于人工神经网络(ANN)的实验。我们的测试表明,在变化的环境中,重新训练比从头开始训练更具挑战性。原因在于,网络必须先忘记之前学到的概念,才能适应新的训练数据。假设人工智能与人类智能有相似之处,我们可以从这一洞察中得出一些有趣的结论。
人工神经网络
人工神经网络类似于我们大脑神经元的结构和行为。通常,人工神经网络由接收外界信号的输入单元组成。通过处理这些信号,网络能够对接收到的输入做出响应。感知器是人工神经网络的一个简单变体[8]。它由 Rosenblatt 于 1958 年提出[9]。图 1 概述了感知器的基本结构。在最近几十年里,已经开发出了更为先进的人工神经网络类型。然而,针对我们的实验,感知器非常适用,因为它容易解释和理解。

图 1:单层感知器的结构。基于[8,第 284 页]的自定义表示。
图 1 显示了单层感知器的架构。作为输入,网络接收n numbers(i₁..iₙ)。与学习到的权重(w₁..wₙ)一起,这些输入被传递到一个阈值逻辑单元(TLU)。该 TLU 通过将输入(i)与权重(w)相乘来计算加权和(z)。在下一步,激活函数(f)根据加权和(z)来确定输出(o)。最后,输出(o)使得网络能够做出响应接收到的输入的决策。Rosenblatt 已证明,这种简单形式的人工神经网络能够解决多种问题。
感知器可以使用不同的激活函数来决定其输出(o)。常见的函数有二进制阶跃函数和符号函数,如图 2 所示。顾名思义,二进制函数生成一个二进制输出{0,1},可以用来做是/否决策。为此,二进制函数检查给定输入的加权和(z)是否小于或等于零。如果是这种情况,则输出(o)为零,否则为一。相比之下,符号函数区分三个不同的输出值{-1,0,+1}。

图 2:激活函数的示例。根据[8, p. 285]的内容制作。
为了基于给定数据集训练感知器,我们需要提供一个样本,其中包括与期望输出(目标)相关联的输入信号(特征)。在训练过程中,算法反复处理输入数据,以学习最适合的权重,从而生成输出。训练所需的迭代次数是衡量学习努力的标准。对于我们的实验,我们训练一个感知器来判断客户是否会购买某款手机。源代码可在GitHub [10]上找到。我们使用的是 Python v3.10 和 scikit-learn v1.2.2 版本。
学习客户偏好
我们的实验灵感来源于一个著名的(失败的)重新学习案例。假设我们在 2000 年为一家手机制造商工作。我们的目标是训练一个感知器,学习客户是否会购买某款手机型号。在 2000 年,触摸屏仍然是一项不成熟的技术。因此,客户更喜欢带有键盘的设备。此外,客户关注价格,并选择价格较低的型号,而不是更昂贵的手机。这些特征使得诺基亚 3310 成为 2000 年全球销量最高的手机[11]。

图 3:诺基亚 3310,由 LucaLuca 拍摄,CC BY-SA 3.0,维基共享资源
在训练感知器时,我们使用如表 1 所示的假设数据集。每一行代表一个特定的手机型号,“keypad”、“touch”和“low_price”列表示其特征。为了简化,我们使用二进制变量。客户是否会购买设备在“sale”列中定义。如上所述,客户会购买带有键盘且价格低的手机(keypad=1 和 low_price=1)。相反,他们会拒绝高价的型号(low_price=0)和带触摸屏的手机(touch=1)。
+----+--------+-------+-----------+------+
| ID | keypad | touch | low_price | sale |
+----+--------+-------+-----------+------+
| 0 | 1 | 0 | 1 | 1 |
| 1 | 1 | 0 | 0 | 0 |
| 2 | 0 | 1 | 0 | 0 |
| 3 | 0 | 1 | 1 | 0 |
+----+--------+-------+-----------+------+
Table 1: Hypothetical phone sales dataset from 2000
为了训练感知器,我们多次输入上述数据集。从 scikit-learn 的角度来看,我们反复调用函数 partial_fit(源代码见这里)。在每次迭代中,算法会尝试逐渐调整网络的权重,以最小化预测“sale”变量的误差。图 4 展示了前十次迭代中的训练过程。

图 4:使用来自 2000 年的数据训练手机销售感知器
正如上述图示所示,感知器的权重逐渐被优化,以适应数据集。在第六次迭代中,网络学习到了最佳的拟合权重,随后数字保持稳定。图 5 可视化了学习过程后的感知器。

图 5:使用来自 2000 年的数据训练的手机销售感知器
让我们基于训练好的感知器考虑一些例子。一款带有按键的低价手机,其加权和为 z=-11–30+21=1。应用二值步进函数后,生成的输出为 sale=1。因此,网络预测客户会购买该手机。相反,一款带有按键的高价设备,其加权和为 z=-11–30+20=1=-1。这一次,网络预测客户会拒绝购买该设备。对于一款带有触摸屏的手机也是如此。(在我们的实验中,我们忽略了设备既没有按键也没有触摸屏的情况,因为客户必须以某种方式操作设备。)
使用变化的偏好进行重新训练
现在让我们设想,随着时间的推移,客户的偏好发生了变化。到 2007 年,技术进步使触摸屏变得更加易于使用。因此,客户现在更倾向于选择触摸屏而不是按键。随着手机成为身份象征,客户也愿意支付更高的价格。这些新的偏好体现在表 2 所示的假设数据集中。
+----+--------+-------+-----------+------+
| ID | keypad | touch | low_price | sale |
+----+--------+-------+-----------+------+
| 0 | 1 | 0 | 1 | 0 |
| 1 | 1 | 0 | 0 | 0 |
| 2 | 0 | 1 | 0 | 1 |
| 3 | 0 | 1 | 1 | 1 |
+----+--------+-------+-----------+------+
Table 2: Hypothetical phone sales dataset from 2007
根据表 2,客户会购买带有触摸屏的手机(touch=1),并且不太关注价格。相反,他们拒绝购买带有按键的设备。实际上,苹果公司于 2007 年进入了手机市场,推出了其 iPhone。凭借高质量的触摸屏,它挑战了传统品牌。到 2014 年,iPhone 最终成为了全球销量最高的手机,推动诺基亚退出市场[11]。

图 6:iPhone 第一代,Carl Berkeley — CC BY-SA 2.0,维基共享资源
为了将之前训练的感知器调整到新的客户偏好,我们必须使用 2007 年的数据集重新训练它。图 7 展示了在前十次迭代中的重新训练过程。

图 7:使用来自 2007 年的数据重新训练手机销售感知器
如图 7 所示,重新训练需要三次迭代。然后,找到最佳的拟合权重,网络就学习到了 2007 年的新客户偏好。图 8 展示了重新学习后的网络。

图 8:经过 2007 年数据重新训练后的手机销售感知机
让我们考虑一些基于重新训练的感知机的例子。一部具有触摸屏(touch=1)且价格低廉(low_price=1)的手机,现在的加权和为 z=-30+11+11=2。因此,网络预测客户会购买具有这些特征的手机。同样,对于一部具有触摸屏(touch=1)但价格较高(low_price=0*)的设备,网络也做出类似预测。相反,网络现在预测客户会拒绝配有键盘的设备。
从图 7 中我们可以看出,使用 2007 年数据进行重新训练需要三次迭代。但是如果我们从零开始训练一个新的感知机呢?图 9 比较了使用 2007 年数据集重新训练旧网络与完全从零开始训练一个新感知机的情况。

图 9:基于 2007 年数据的重新训练与从零开始训练的对比
在我们的例子中,从零开始训练一个新的感知机比重新训练旧网络更加高效。根据图 9,训练只需要一次迭代,而重新训练需要三倍的步骤。原因在于,旧感知机必须首先“遗忘”2000 年时学习的权重,只有这样,它才能调整到 2007 年新的训练数据。以“触摸”特征的权重为例,旧网络必须将其从 -3 调整到 +1。相比之下,新的感知机可以从零开始,直接将权重从 0 增加到 +1。因此,新的网络学习速度更快,并且最终会达到一个略有不同的设置。
结果讨论
我们的实验从数学角度展示了为什么重新训练一个人工神经网络(ANN)可能比从零开始训练一个新网络更加昂贵。当数据发生变化时,旧的权重必须先被“遗忘”,才能学习新的权重。如果我们假设这同样适用于人类大脑的结构,那么我们可以将这一见解应用于一些实际问题。
在他的著作《创新者的窘境》中,Christensen 研究了曾经在其行业中是创新者的公司为何未能适应新技术 [12]。他通过硬盘和挖掘机市场的例子支撑了他的研究。在几个案例中,市场领导者未能调整以适应激进的变化,反而被市场新进入者超越。根据 Christensen 的观点,进入市场的新公司能够更快且更成功地适应转变后的环境。他认为,经济因素是这种现象的主要原因。我们的实验表明,可能也存在数学上的原因。从人工神经网络的角度来看,市场新进入者有从零开始学习的优势,而现有的供应商必须首先反学习他们的传统观念。特别是在颠覆性创新的情况下,这对现有公司来说可能是一个重大缺点。
激进的变革不仅是企业面临的挑战,也是整个社会面临的挑战。在他们的著作《第二次机器时代》中,Brynjolfsson 和 McAfee 指出,颠覆性技术可以引发痛苦的社会调整过程 [13]。作者将我们这个时代的数字化与 18 世纪和 19 世纪的工业革命作比较。在那个时代,像蒸汽机和电力这样的激进创新导致了社会的深刻变革。像卢德派这样的运动试图通过暴力抗拒这种进化。它们的适应斗争不仅可能是意志的问题,也可能是能力的问题。正如我们上面所见,与从零开始学习相比,反学习和再学习可能需要相当大的努力。
结论
显然,我们的实验建立在简化的现实模型基础上。生物神经网络比感知机更加复杂。移动电话市场中的客户偏好也一样。诺基亚的兴衰有许多原因,除了我们数据集中包含的特征。由于我们只讨论了一个特定的场景,另一个有趣的研究问题是,在哪些情况下,重新训练实际上比训练更难。像 Hinton 和 Sejnowski [7] 以及 Chen 等人 [14] 等作者提供了对这一主题的差异化看法。希望我们的文章能为这些更技术性的出版物提供一个起点。
承认我们工作的局限性,我们可以从中提取一些关键的教训。当人们未能适应变化的环境时,未必是由于缺乏智慧或动机。在涉及数字化转型时,我们应该牢记这一点。与数字原住民不同,老一代人首先必须反学习“模拟”概念。这需要努力和时间。对他们施加过多的压力可能会导致否认的态度,进而转化为阴谋论和要求强有力的领导者阻止进步。相反,我们应该开发成功反学习和再学习的概念。教授技术至少与开发技术一样重要。否则,我们将把我们旨在支持的社会抛在身后。
除非另有说明,所有图片均由作者提供。
关于作者
Christian Koch 是 BWI GmbH 的企业首席架构师,并且是纽伦堡理工学院乔治·西蒙·欧姆学院的讲师。
Markus Stadi 是 Dehn SE 的高级云数据工程师,多年来一直在数据工程、数据科学和数据分析领域工作。
参考文献
-
Grant, A. (2023). 再思考:了解你不知道的事情的力量. Penguin。
-
Reuter-Lorenz, P. A., & Lustig, C. (2005). 脑部衰老:重组关于衰老心智的发现。神经生物学当前意见, 15(2), 245–251.
-
Creasey, H., & Rapoport, S. I. (1985). 衰老的人脑。神经学年鉴:美国神经学会与儿童神经学学会的官方期刊, 17(1), 2–10.
-
Welford AT. Motivation, Capacity, Learning and Age. 国际老龄化与人类发展期刊. 1976;7(3):189–199.
-
Carstensen, L. L., Mikels, J. A., & Mather, M. (2006). 衰老与认知、动机、情感的交集。见 老年心理学手册 (第 343–362 页)。学术出版社。
-
Kim, A., & Merriam, S. B. (2004). 退休学习机构中老年人学习动机。教育老年学, 30(6), 441–455.
-
Hinton, G. E., & Sejnowski, T. J. (1986). 学习与重学在玻尔兹曼机中的应用。并行分布式处理:认知微观结构的探索, 1(282–317), 2.
-
Géron, A. (2022). 动手学习机器学习:使用 Scikit-Learn、Keras 和 TensorFlow. O'Reilly Media, Inc.
-
Rosenblatt, F. (1958). The perceptron: a probabilistic model for information storage and organization in the brain. 心理学评论, 65(6), 386.
-
Koch, C. (2024). Retrain Python 项目。网址:
github.com/c4ristian/retrain。访问时间:2024 年 1 月 11 日。 -
Wikipedia. 热销手机列表。网址:
en.wikipedia.org/wiki/List_of_best-selling_mobile_phones。访问时间:2024 年 1 月 11 日。 -
Christensen, C. M. (2013). 创新者的窘境:当新技术导致大公司失败时. 哈佛商业评论出版社。
-
Brynjolfsson, E., & McAfee, A. (2014). 第二次机器时代:在辉煌技术时代的工作、进步与繁荣. WW Norton & Company。
-
Chen, M., Zhang, Z., Wang, T., Backes, M., Humbert, M., & Zhang, Y. (2022, November). 图形反学习。见 2022 年 ACM SIGSAC 计算机与通信安全大会论文集 (第 499–513 页)。
为什么检索增强生成在长上下文语言模型时代依然重要
在本文中,我们将探讨为什么 128K tokens(甚至更多)模型无法完全取代使用 RAG。
·发表于Towards Data Science ·阅读时间 7 分钟·2024 年 12 月 12 日
--
我们将从简要回顾 RAG 可以解决的问题开始,然后再讨论 LLM 的改进及其对使用RAG 需求的影响。

插图由作者提供。
让我们从一点历史背景开始
RAG 其实并不是什么新鲜事物
向语言模型注入上下文,以让其获取最新数据的想法在 LLM 层面上其实并不“新”。这一概念最早由 Facebook AI/Meta 的研究员在 2020 年的论文《面向知识密集型 NLP 任务的检索增强生成》中提出。相比之下,ChatGPT 的第一个版本直到 2022 年 11 月才发布。
在这篇论文中,他们区分了两种类型的记忆:
-
参数化的记忆,指的是 LLM 固有的记忆,它所学到的内容,在训练期间通过大量文本的喂入获得。
-
非参数化的记忆,指的是你通过向提示提供上下文来实现的记忆。
为什么规模化有效:归纳偏差与痛苦的教训
用一个玩具问题构建深刻的洞见
·发布于 Towards Data Science ·10 分钟阅读·2024 年 10 月 22 日
--

来源:所有图片由作者提供
在过去的十年里,我们见证了深度学习模型规模化的强大力量。大型模型通过在海量数据上训练,持续在语言建模、图像生成、游戏对弈甚至蛋白质折叠等任务中超越了以往的方法。为了理解为什么规模化有效,我们来看看一个玩具问题。
引入一个玩具问题
我们从一个一维流形开始,它穿越二维平面并形成一个螺旋:

现在我们添加一个热图,表示采样特定二维点的概率密度。值得注意的是,这个概率密度是独立于流形的形状的:

假设流形两侧的数据总是完全可分离的(即没有噪声)。流形外部的数据点为蓝色,内部的数据点为橙色。如果我们抽取 N=1000 个数据点,它可能看起来是这样的:

玩具问题:我们如何构建一个基于二维坐标预测点的颜色的模型?
在现实世界中,我们通常不能从特征空间的所有部分均匀地采样。例如,在图像分类中,找到树的图像相对容易,但要找到许多特定树种的例子则较为困难。因此,模型可能会更难学会区分那些没有很多例子的物种。类似地,在我们的玩具问题中,空间的不同部分将变得难以预测,仅仅因为它们更难采样。
解决玩具问题
首先,我们构建一个简单的三层神经网络,训练 1000 个 epoch。神经网络的预测受到样本特性的强烈影响。因此,训练后的模型很难推断出流形的形状,仅仅因为采样的稀疏性:

即使知道这些点是完全可分的,也有无数种方式可以围绕采样点绘制边界。基于样本数据,为什么应该认为某一边界比其他边界更优?
使用正则化技术,我们可以鼓励模型生成更平滑的边界,而不是紧密围绕预测点弯曲。这在一定程度上有所帮助,但在稀疏区域无法解决我们的問題。
既然我们已经知道流形是一个螺旋形,我们能否鼓励模型做出螺旋形的预测?
我们可以添加一个叫做“归纳先验”的东西:我们在模型架构或训练过程中加入的,包含有关问题空间的信息。在这个玩具问题中,我们可以做一些特征工程,调整我们呈现给模型的输入方式。我们将输入从二维(x,y)坐标转换为极坐标(r,θ)。
现在,神经网络可以基于与原点的距离和角度做出预测。这使得模型倾向于产生更加弯曲的决策边界。以下是新训练的模型预测的决策边界:

注意,在输入空间的无样本部分,模型表现得有多好。那些缺失点的特征仍然与观察到的点的特征相似,因此模型可以在没有额外数据的情况下预测有效的边界。
显然,归纳先验是有用的。
大多数架构决策都会引入归纳先验。让我们尝试一些增强方法,并思考它们引入了什么样的归纳先验:
-
Focal Loss — 增加模型在难以预测的数据点上的损失。这可能会提高准确性,但代价是增加这些点周围的模型复杂度(正如我们从偏差-方差权衡中所期望的那样)。为了减少方差增加的影响,我们可以添加一些正则化。
-
Weight Decay — 对权重大小施加 L2 范数,防止模型对任何单一样本学习出过于强烈的特征。
-
Layer Norm — 有许多微妙的效果,其中之一可能是模型更多地关注点与点之间的关系,而不是它们的大小,这可能有助于抵消使用 Focal Loss 所增加的方差。
在做出所有这些改进后,我们预测的流形效果有多好?

一点都不更好。事实上,它在螺旋的中心附近引入了一个伪影。并且它仍然没有预测到螺旋末端(左上象限)没有数据的地方。话虽如此,它成功地捕捉到了接近原点的曲线,这也是一个进步。
苦涩的教训
现在假设另一个研究团队完全不知道单一连续螺旋形状中有一个硬边界。对于他们来说,形状内部可能存在多个带有模糊概率边界的口袋。
然而,这个团队能够收集到 10,000 个样本,而不是 1,000 个。对于他们的模型,他们仅使用 k 近邻(kNN)方法,k=5。
附注:这里 k=5 是一个不好的归纳先验。对于这个问题,通常 k=1 会更好。Challenge: 你能弄明白为什么吗?在文章中留下你的答案。
现在,kNN 与神经网络相比并不是一个特别强大的算法。然而,即使有一个不好的归纳先验,下面是 kNN 在使用 10 倍更多数据时的表现:

使用 10 倍更多数据后,kNN 方法的表现接近于神经网络。特别是在螺旋形状的尾部预测上,它表现得更好,尽管它仍然无法捕捉到那个难以采样的左上象限。它也会犯一些错误,通常会产生一个模糊的边界。
如果我们增加 100 倍或 1000 倍的数据会怎样?让我们看看当我们扩大使用的数据量时,kNN 与神经网络方法的表现如何对比:

当我们增加训练数据的大小时,使用哪种模型基本上不再重要。而且,考虑到足够的数据,低级的 kNN 实际上开始表现得比我们精心设计的、拥有深思熟虑的归纳先验的神经网络更好。
这是一个重要的教训。作为一个领域,我们至今仍没有完全领会这一点,因为我们仍在犯同样的错误。为了看清这一点,并有效抵制它,我们必须理解这些错误的诱惑。我们必须学习到那个苦涩的教训:在长期来看,构建我们认为的思维方式是行不通的。这个苦涩的教训基于历史观察:1) AI 研究人员通常尝试将知识内置到他们的智能体中;2) 这在短期内总是有效,并且会让研究人员感到个人满足;但 3) 从长远来看,这种方法会进入瓶颈,甚至会抑制进一步的进展;4) 突破性进展最终是通过基于计算扩展的搜索和学习的对立方法实现的。最终的成功带有一丝苦涩,且往往未被完全消化,因为它是战胜了一种偏向人类中心的做法。
来自 Rich Sutton 的文章“The Bitter Lesson”
优越的归纳先验无法与单纯使用更多计算来解决问题相提并论。在这种情况下,“更多计算”仅仅涉及在内存中存储更多的数据样本,并使用 kNN 匹配最近邻。我们已经在基于变换器的大型语言模型(LLM)中看到这种情况的发生。它们通过不断训练更大、更强的模型,配备更多的 GPU,并使用越来越多的文本数据,持续超越其他自然语言处理技术。
但是,肯定…?
这个玩具示例有一个微妙的问题,我们在这两种模型中都曾见过:未能预测到左上象限中螺旋的稀疏部分。这与大型语言模型、训练推理能力以及我们追求“人工通用智能”(AGI)息息相关。为了更好地理解我的意思,让我们放大看一下左上角那个形状独特的尾部。

这个区域的采样密度特别低,边界与其余的流形有很大的不同。假设这个区域是我们非常关心的,例如:从大型语言模型(LLM)生成“推理”。这样的数据不仅稀缺(如果是随机抽样的话),而且与其余数据有足够的不同,这意味着来自空间其他部分的特征在此处进行预测时并不有用。此外,注意到边界是多么尖锐和具体——在尖端附近采样的点很容易落在外部。
让我们看看这与基于文本推理训练大型语言模型(LLM)的简化视图有何不同:
-
推理是复杂的,我们可能无法通过拟合一条“平滑”的线来平均几个样本,从而找到解决方案。要解决推理问题,单纯遵循一个显而易见的模式是不够的,而是必须真正理解问题。训练一个能够进行推理的模型可能需要大量的数据。
-
从互联网上随机抽取数据并不能为我们提供很多人类解释复杂思维推理步骤的样本。支付人们显式生成推理数据可能有助于提高数据的密度。但这是一个缓慢的过程,而且所需的数据量实际上相当庞大。
-
我们非常重视正确的推理能力,因为它将为人工智能开辟更多的应用场景。
当然,推理比预测螺旋的尖端要复杂得多。通常有多种方法可以得出正确答案,可能有多个正确答案,而且有时边界可能模糊。然而,在深度学习架构中,我们也并非没有归纳先验,包括使用强化学习的技术。
在我们的玩具问题中,边界形状具有规律性,因此我们使用了归纳先验来鼓励模型学习这种形状。在建模推理时,如果我们能够构建一个更高维空间中的流形来表示概念和思想,那么它的形状会有某种规律性,这可以用作归纳先验。如果《苦涩的教训》继续成立,那么我们将假设寻找这样的归纳先验不是前进的道路。我们只需要扩展计算能力。到目前为止,做到这一点的最佳方法就是收集更多的数据,并将其投入更大的模型中。
但你肯定会说,transformer 之所以如此成功,是因为注意力机制为语言建模引入了强大的归纳先验?论文“我们只需要 RNN 吗”表明,如果规模足够大,简化版的循环神经网络(RNN)也能表现得很好。这不是归纳先验的作用。它是因为论文提高了我们在大数据上训练 RNN 的速度。这就是 transformer 如此有效的原因——并行性使我们能够利用更多的计算资源。这是一种直接来源于《苦涩的教训》的架构。
数据耗尽了吗?
数据永远不够。合成数据或强化学习技术(如自我对弈)可以生成无限的数据。尽管如果与现实世界没有关联,这些数据的有效性可能会变得模糊。这就是为什么像 RLHF 这样的技术以手工制作的数据为基础——以便尽可能准确地建模人类偏好。此外,鉴于推理通常是数学化的,因此可能很容易通过自动化方法生成这些数据。
现在的问题是:考虑到我们目前拥有的归纳先验,需要多少数据才能训练出具有真正推理能力的模型?
如果《苦涩的教训》继续适用,答案是:没关系,找到更好的方法来利用更多的计算资源,所能带来的收益将继续大于试图寻找更优的归纳先验^。这意味着,寻找更强大的 AI 将坚决掌握在预算最大的公司手中。
写完这一切后……我仍然希望这不是真的。
关于我
我是Affinda的首席 AI 工程师。请查看我们的AI 文档自动化案例研究了解更多信息。
一些我的长文:
更多实用阅读:
附录
^ 值得注意的是,文章《苦涩的教训》并非明确讨论归纳偏差与收集更多数据的问题。将更多数据投入更大的模型是利用更多计算资源的一种方式。而在深度学习中,这通常意味着找到更好的方式来提高训练中的并行性。最近,这也涉及到利用更多推理时间的计算(例如 o1-preview)。可能还有其他方法。这个话题比我在这篇短文中呈现的要更为微妙。
为什么集合在编程中如此有用
以及如何利用它们提升代码性能
·发表于Towards Data Science ·8 分钟阅读·2024 年 12 月 19 日
--
一个集合是一个简单的结构,定义为不同元素的集合。集合通常在数学或逻辑领域中出现,但在编程中,它们也非常有用,可以编写高效的代码。在本文中,我详细介绍了集合在某些情况下优于其他数据类型,如列表,以及集合的底层实现,使它们对程序员非常有用。

由Robert Ruggiero拍摄的组成集合的螺栓照片,来源于Unsplash
集合是容器的一种示例;它们用于在一个变量下存储多个元素。在寻找容器数据类型时,列表(通常用方括号 [ ] 定义)是首选,它在几乎所有编程语言中都有广泛应用。集合与列表有很多相似之处,最显著的是它们都是动态的,允许根据需要增长或缩小大小。唯一的区别在于,列表会保留元素的顺序并允许重复,而集合则不保留顺序,也不允许重复,这在某些场景中提供了独特的优势。了解何时选择集合而非列表,可以大大提高程序的性能并改善代码的可读性。
在 Python 中,集合可以使用花括号声明,或者使用集合构造函数来声明。
set_a = {1, 4, 9, 16}
set_b = set([1,2,3]) #coverting list to set
empty_set = set()
集合不包含重复元素
集合的一个定义特征是每个元素都是独特的,因此没有重复的条目。一个简单的应用例子是通过将列表转换为集合来移除所有重复条目。
#LIST Implementation O(n²)
def remove_duplicate_entries(list input_list):
output_list = []
for element in input_list:
if element in output_list:
output_list.append(element)
return output_list
#SET Implementation O(n)
def remove_duplicate_entries(list input_list):
return list(set(input_list))
因为这里使用了集合,列表的顺序没有被保留。如果顺序很重要,可以使用像第一个实现那样的实现。
下一个函数用于识别列表中是否包含重复元素;元素的顺序在这里并不重要,重要的是重复出现的次数。集合的实现更为理想,因为集合上的操作通常比列表要快得多(这将在后面更详细地解释)。
#LIST Implementation
def has_duplicates(list input_list):
unique_elements = []
for element in input_list:
if element in unique_elements:
return True
else:
unique_elements.append(element)
return False
#SET Implementation
def has_duplicates(list input_list):
return len(input_list) != len(set(input_list))
注意:在存在重复元素较早出现的情况下,第一个实现可能会比第二个更高效,因为它能较早地捕捉到重复并返回False,无需检查每个条目,而第二个实现则总是遍历每一个元素。一个更优的解决方案可以采用与第一个实现相似的方法,但使用* 集合 作为唯一元素容器。
def has_duplicates(list input_list) -> bool:
unique_elements = set() #**modified here
for element in input_list:
if element in unique_elements:
return True
else:
unique_elements.add(element)
return False
即使这些应用看起来相当粗糙或简单,但以“去除重复元素”为基础的无数用例需要一些创意才能看出。例如,Leetcode 1832 “检查句子是否是回文”- 你需要检测一个输入句子中是否使用了所有 26 个字母- 就可以优雅地使用集合推导来解决。
def is_pangram(str sentence) -> bool:
present_letters = {letter for letter in sentence}
return len(present_letters) == 26
如果没有集合,这个问题就需要使用嵌套循环来处理,而嵌套循环不仅更难编写,而且效率较低,复杂度为 O(n²)。
识别集合有用场景的关键在于考虑:
-
如果我们只关心出现的次数,比如在本问题中,如果句子中包含单词“eel”,我们只关心字母“e”是否出现,而不在乎它是否出现两次,因此我们可以丢弃第二个及之后的“e”。
-
基于这一原理,在处理非常大的列表时,可能希望去除重复元素,以便继续处理更小的列表。
-
如果我们不关心元素的顺序,那么可以使用集合。在需要保持顺序的场景中,比如优先队列,应该使用列表。
哈希映射和缺乏顺序
倒退一步来看,可能会觉得使用集合的唯一好处就是简化重复元素的去除。我们之前讨论过集合没有顺序;数组有索引元素,可以简单地忽略它们并当作集合处理。看起来数组可以像集合一样完成任务,甚至做得更多。
然而,集合强制的这一简化方式带来了不同的底层实现。在列表中,元素通过索引来分配位置,从而确定每个元素在顺序中的位置。而集合不需要分配索引,因此它们采用了一种不同的引用方法:哈希映射。这些操作通过(伪)随机地为元素分配地址,而不是将元素顺序存储。地址的分配由哈希函数控制,哈希函数以元素为输入,输出一个地址。

哈希表不保留顺序。
H(x) 是确定性的,因此相同的输入总是会给出相同的输出,即 H 函数内部没有随机数生成(RNG),所以在这种情况下 H(4) 总是等于 6。
执行此函数所需的时间与集合的大小无关,即哈希的时间复杂度是 O(1)。这意味着哈希的时间消耗与列表的大小无关,始终保持在一个常数、快速的速度。
集合操作
由于哈希通常非常快速,许多在大数组上通常较慢的操作可以在集合上高效执行。
搜索或成员测试
在数组中搜索元素使用的是一种叫做线性搜索的算法,它通过逐一检查列表中的每个项来完成。在最坏情况下,如果要查找的元素不在列表中,算法将遍历整个列表的每个元素(O(n))。在一个非常大的列表中,这个过程会花费很长时间。

线性搜索平均需要 n/2 次操作,图像由作者提供
然而,由于哈希是 O(1),Python 将要查找的元素哈希处理,然后要么返回该元素在内存中的位置,要么返回该元素不存在——这一过程所需时间非常短。
number_list = range(random.randint(1,000,000))
number_set = set(number_list)
#Line 1
#BEGIN TIMER
print(-1 in number_list)
#END TIMER
#Line 2
#BEGIN TIMER
print(-1 in number_set)
#END TIMER

列表与集合中的搜索时间对比
注意:使用哈希表进行搜索的 摊销 时间复杂度是 O(1)。这意味着在平均情况下,它的运行时间是常数时间,但从技术上讲,在最坏情况下,搜索是 O(n)。然而,这种情况极不可能发生,只有当哈希实现发生碰撞时才会出现碰撞,也就是当哈希表/集合中的多个元素被哈希到同一个地址时。

碰撞是罕见的
删除
从列表中删除一个元素,首先需要搜索以定位该元素,然后通过清除地址来移除对该元素的引用。在数组中,经过 O(n) 时间的搜索后,每个删除元素后面的元素的索引都需要向下移动一个位置。这本身是另一个 O(n) 的过程。

在列表中删除元素大约需要 n 次操作
从集合中删除元素涉及 O(1) 查找,然后是删除内存地址,这也是一个 O(1) 的过程,因此删除操作也是常数时间操作。集合还有更多删除元素的方法,这样可以避免错误发生,或者能够简洁地删除多个元素。
#LIST
numbers = [1, 3, 4, 7, 8, 11]
numbers.remove(4)
numbers.remove(5) #Raises ERROR as 5 is not in list
numbers.pop(0) #Deletes number at index 0, ie. 1
#SET
numbers = {1, 3, 4, 7, 8, 11}
numbers.remove(4)
numbers.remove(5) #Raises ERROR as 5 is not in set
numbers.discard(5) #Does not raise error if 5 is not in the set
numbers -= {1,2,3} #Performs set difference, ie. 1, 3 are discarded
插入
向列表追加元素和向集合添加元素都是常数操作;然而,向列表中的指定索引位置添加元素(.insert)则需要额外的时间来移动元素。
num_list = [1,2,3]
num_set = {1,2,3}
num_list.append(4)
num_set.add(4)
num_list += [5,6,7]
num_set += {5,6,7}
高级集合操作
此外,所有可以在集合上执行的数学操作,在 Python 中也有实现。这些操作对于列表来说执行起来非常耗时,而使用哈希则再次进行了优化。

集合操作
A = {1, 2, 3, 5, 8, 13}
B = {2, 3, 5, 7, 13, 17}
# A n B
AintersectB = A & B
# A U B
AunionB = A | B
# A \ B
AminusB = A - B
# A U B - A n B or A Delta B
AsymmetricdiffB = A ^ B
这还包括比较运算符,即适当的和放宽的子集与超集。这些操作再次比其列表对应操作快得多,运行时间为 O(n),其中 n 是两个集合中的较大者。

子集
A <= B #A is a proper subset of B
A > B #A is a superset of B
Frozen Sets
Python 中的一个最终小特性,但被低估的特性是frozen set,本质上是一个只读或不可变的集合。这些集合提供了更高的内存效率,并且在频繁测试元组中的成员关系时非常有用。
结论
使用集合来提升性能的精髓可以通过通过减少优化的原则来概括。

像列表这样的数据结构功能最强——它们是可索引的且动态的——但代价是效率相对较低:无论是在速度还是内存方面。识别哪些特性是必需的,哪些是未使用的,从而决定使用哪种数据类型,将使代码运行更快,且更易于阅读。
所有技术图表由作者提供。
为什么“统计显著性”是毫无意义的
这是一个更好的数据驱动决策框架
·发表于 Towards Data Science ·9 分钟阅读·2024 年 12 月 1 日
--

[图片来源:作者]
数据科学家从事的是决策工作。我们的工作聚焦于如何在不确定性下做出明智的选择。
然而,在量化这种不确定性时,我们常常依赖“统计显著性”这一概念——这一工具,充其量只能提供一种肤浅的理解。
在本文中,我们将探讨为什么“统计显著性”是有缺陷的:任意的阈值、虚假的确定性感以及未能解决现实世界中的权衡问题。
最重要的是,我们将学习如何超越显著与非显著的二元思维,采用一个以经济影响和风险管理为基础的决策框架。
1. 从一个例子开始
假设我们刚刚进行了一个 A/B 测试,评估一个旨在提高用户在我们网站上停留时间的新功能——从而提高他们的消费。
控制组包含了 5,000 名用户,治疗组包括另外 5,000 名用户。这为我们提供了两个数组,分别命名为treatment和control,每个数组包含 5,000 个值,代表各自组内用户的消费情况。
为什么 STEM 对任何数据科学家都很重要
从我个人经验出发,有 3 个案例可以证明这一点
·发表于 Towards Data Science ·8 分钟阅读·2024 年 11 月 14 日
--

图片来源:Ashley Batz 通过 Unsplash
前言
曾几何时,我曾学习石油工程。说实话,我几乎是偶然进入了这个本科专业。在学校时,我喜欢物理和数学,因此我肯定想在大学学习 STEM 专业。那时,我对石油行业一无所知,像许多人一样,我以为石油是从地下湖泊中提取的。但因为我成功地被该项目录取,我决定尝试一下。
我不能说我后悔自己的选择,尽管我必须承认,除了实习期间,我并未在行业中工作。但我得到的是解决各种任务的科学方法,而毫无疑问,这是一份极大的礼物。
在这篇文章中,我想强调了解科学原理和法则的重要性。在大多数情况下,这些法则是基于积累的经验和长期的观察所制定的,因此它们在各个方面有着广泛的应用。数据科学也不例外,即便没有直接应用这些积累的智慧,拥有与主要科学方法的类比,也帮助我更有效地解决与数据相关的挑战性任务。
案例 # 1: 分解与傅里叶变换
傅里叶变换是一种将复杂的波或信号分解为一组独特的正弦成分的方法。通过这种分解,我们可以检视、放大、衰减或删除每个正弦元素。
这是傅里叶变换的正式定义,从中可以看出,该方法的核心是对波的分解,以便简化它们的分析。因此,傅里叶变换在许多应用中都非常有用。例如,音乐识别服务使用傅里叶变换来识别歌曲。在语音识别中,傅里叶变换和相关的变换用于重建语音。
此外,傅里叶变换在图像处理中的应用也非常广泛。JPEG 压缩算法就是傅里叶变换的一个特例,用于去除图像中的高频成分。
就我个人而言,我已经应用了快速傅里叶变换(或简称 FFT)来创建图像副本,这个方法适用于当我们无法访问微型 CT 扫描仪,但需要一些二进制图像来研究岩石样本的主要特性时。
顺便说一下,最近我写了一篇关于二进制图像的文章:
关于如何使用 AI 和统计方法处理此类图像的简短 Python 指南
pub.towardsai.net](https://pub.towardsai.net/the-brief-history-of-binary-images-c4dacf40ce95?source=post_page-----45b8ec1d445d--------------------------------)
接下来,我将考虑一个稍微简单的情况——去除输入图像中的系统性噪声。
这是我们将要处理的原始照片:

输入图像。由作者拍摄。
让我们使用skimage包中的imread函数来读取图像,然后对其应用傅里叶变换。
Python 代码:
import matplotlib.pyplot as plt
import numpy as np
from skimage.io import imread, imshow
from skimage.color import rgb2gray
# read input image
my_im = imread('photo.jpg')
plt.figure('Input Image')
plt.imshow(my_im)
plt.axis('off') # hide axis
plt.show()
# convert the image to grayscale
gray_im = rgb2gray(my_im)
# applying FFT and center shift
fourier_im = np.fft.fft2(gray_im)
im_shift = np.fft.fftshift(fourier_im)
plt.figure('Applying FFT')
plt.imshow(np.log(abs(im_shift)), cmap='gray')
plt.tight_layout()
plt.show()
输出结果:

应用傅里叶变换。图像由作者提供。
在这里,可以注意到图像中有两处扭曲,表现为交叉的线条——它们与照片中的水平(云层)和垂直(路灯)元素直接相关。
那么,如果我们尝试去除与照片中的云层相关的水平“噪音”呢?
我们可以使用一个掩模,该掩模通过初始化一个与图像大小相同的零矩阵来创建,然后在掩模中设置中心的垂直和水平条带为 1。接着,通过逐元素相乘将掩模应用于平移后的傅里叶变换图像。过滤后,我们对掩模后的频域数据进行逆傅里叶变换(IFFT),以将其转换回空间域。
# create vertical & horizontal mask for noise removal
rows, cols = gray_im.shape
crow, ccol = rows // 2, cols // 2
# create a mask with ones in the vertical and horizontal strip
# let's say width is equal to 100 pixels
mask = np.zeros((rows, cols), dtype=np.float32)
mask[crow - 50:crow + 50, :] = 1 # vertical strip in the center
mask[:, ccol - 50:ccol + 50] = 1 # horizontal strip in the center
# apply the mask to the shifted FFT
filtered_im_shift = im_shift * mask
# inverse FFT to get the filtered image back
filtered_fourier_im = np.fft.ifftshift(filtered_im_shift)
filtered_image = np.fft.ifft2(filtered_fourier_im)
filtered_image = np.abs(filtered_image) # Take absolute value
# display the filtered image
plt.figure('Filtered Image')
plt.imshow(filtered_image, cmap='gray')
plt.axis('off') # hide axis
plt.tight_layout()
plt.show()
结果将如下所示:

经过垂直和水平掩模处理的图像。图像由作者提供。
案例 #2:叠加原理
叠加原理是物理学和工程学中的一个基本概念,特别是在波动力学、光学和信号处理领域。它表明,当两个或多个波在空间中重叠时,任意一点的合成波是该点各个波的和。这个原理适用于线性系统,对于理解干涉和衍射等现象至关重要。
在 STEM(科学、技术、工程和数学)的背景下,叠加原理可以应用于分析各种类型的波,包括声波、电磁波和量子波函数。它使得工程师和科学家能够预测波如何相互作用,这对于设计通信网络、音频设备和光学设备等系统至关重要。
数学表示
对于由以下方程描述的两条正弦波:
y₁(x, t) = A₁ sin(k₁ x - ω₁ t + φ₁)
y₂(x, t) = A₂ sin(k₂ x - ω₂ t + φ₂)
由于这两波的叠加,合成波y(x, t)可以表示为:
y(x, t) = y₁(x, t) + y₂(x, t)
在上述方程中,A₁和A₂是波的振幅;k₁和k₂是波数;ω₁和ω₂是角频率;φ₁和φ₂是相位偏移。
计算两条正弦波叠加的 Python 脚本
以下是一个 Python 脚本,使用numpy和matplotlib计算并可视化两条正弦波的叠加。该脚本生成具有指定参数的两条正弦波并绘制它们的叠加。
import numpy as np
import matplotlib.pyplot as plt
# parameters for the first wave
A1 = 1.0 # amplitude
k1 = 2 * np.pi / 5 # wave number (2*pi/wavelength)
omega1 = 2 * np.pi / 10 # angular frequency (2*pi/period)
phi1 = 0 # phase shift
# parameters for the second wave
A2 = 0.5 # amplitude
k2 = 2 * np.pi / 3 # wave number
omega2 = 2 * np.pi / 15 # angular frequency
phi2 = np.pi / 4 # phase shift
# create an array of x values
x = np.linspace(0, 30, 1000)
t = 0 # time at which we calculate the waves
# calculate the individual waves
y1 = A1 * np.sin(k1 * x - omega1 * t + phi1)
y2 = A2 * np.sin(k2 * x - omega2 * t + phi2)
# calculate the superposition of the two waves
y_superposition = y1 + y2
# plotting
plt.figure(figsize=(12, 8))
plt.plot(x, y1, label='Wave 1', linestyle='--')
plt.plot(x, y2, label='Wave 2', linestyle='--')
plt.plot(x, y_superposition, label='Superposition', linewidth=2)
plt.title('Superposition of Two Sinusoidal Waves')
plt.xlabel('Position (x)')
plt.ylabel('Amplitude')
plt.legend()
plt.show()
输出为:

两条正弦波叠加的结果。图片来自作者。
案例 # 3:物料平衡
应用科学方法的最后一个案例有点“理论化”,所以我完全不会在这里插入复杂的公式。
我决定在关于 STEM 的帖子中提到物料平衡,因为任何数据科学家都或多或少知道一个著名的“垃圾进,垃圾出”(或简称 GIGO)公式,意味着低质量的输入将产生错误的输出,我认为这也是数据科学中物料平衡的一种形式 😃

GIGO 原理中的物料平衡。图片来自作者。
数据科学中的 GIGO 原理指的是输出的质量由输入的质量决定。如果你提供低质量、不准确或无关的数据(垃圾进),那么你的分析、模型或算法的结果也将是有缺陷或具有误导性的(垃圾出)。这强调了数据质量、清洁性和相关性在数据科学项目中的重要性,以及为了确保可靠的结果,数据预处理和验证的必要性。
结论
STEM 背景为数据科学提供了坚实的基础,增强了解释复杂数据集所必需的分析能力。首先,支撑统计学和算法的数学原理使数据科学家能够开发出准确预测趋势和行为的模型。其次,科学方法促进了批判性思维和问题解决能力,使从业者能够系统地提出假设、进行实验并验证结果。最后,工程原理对构建可扩展的数据基础设施和优化性能至关重要,确保数据解决方案不仅有效,而且高效。综合来看,这些 STEM 学科使数据科学家能够以结构化的思维方式应对挑战,推动创新并在日益数据驱动的世界中做出明智的决策。
我尝试通过个人经验提供三个简单的案例,展示 STEM 教育对于那些想进入数据领域的人有多么重要。但当然,现实中还有更多的例子,而 2024 年诺贝尔物理学奖则是 STEM 对数据科学和机器学习发展重要性的另一个亮眼展示。今年的奖项颁发给了“为人工神经网络的机器学习奠定基础的发现和发明”。
感谢阅读!虽然我建议不要仅仅阅读别人的经验,而是尝试在下一个数据科学项目中实现 STEM 原则,亲自感受其背后的深度 😃


浙公网安备 33010602011771号