初学者友好的 MLOps 介绍
原文:
towardsdatascience.com/a-beginner-friendly-introduction-to-mlops-95282f25325c
MLOps 的第一步
· 发布于 Towards Data Science · 9 分钟阅读 · 2023 年 1 月 4 日
–
由 Sarah Dorweiler 拍摄于 Unsplash
我对 MLOps 已经有一段时间的兴趣了。我最初是从机器学习工程师那里了解到它的,当时我还是博士生,不知道它的存在。然而,我的好奇心被激发了,我开始学习它。回顾过去,我后悔没有早点了解它,以便优化我的机器学习工作流。
在这篇文章中,我将尝试提供一个初学者友好的 MLOps 介绍,并用简单的方式解释关键概念。作为一个起初也觉得难以理解的人,我明白需要一个更简单的入门介绍。我的希望是,阅读完这篇文章后,初学者能够更自如地阅读更高级的 MLOps 文档。
目录:
· 1. 对 MLOps 的动机
· 2. 定义
· 3. MLOps 生命周期
· 4. MLOps 工作流
∘ 4.1. 商业问题
∘ 4.2. 数据工程
∘ 4.3. 机器学习模型工程
∘ 4.4. 代码工程
· 5. 结论
我的 MLOps 教程:
-
教程 1:MLOps 的关键起点:探索其基本组成部分
-
教程 2:初学者友好的 MLOps 工作流介绍
-
教程 6: 实践中的测试:代码、数据和机器学习模型
-
教程 7: 实践中的追踪:代码、数据和机器学习模型
[我将在发布相关文章时更新此列表]
1. 对 MLOps 的动机
由于机器学习技术在各种研究领域的成功,许多公司寻求将其纳入他们的软件系统,以提高效率并解决实际问题。然而,在生产环境中实施机器学习对许多公司来说可能是一个具有挑战性且耗时的过程。此外,一旦部署,模型必须被管理和维护,并且其性能必须被监控以确保其正常运作。这些任务在大型软件系统中尤为困难。
另一方面,软件工程师使用 DevOps(开发与运维)范式,这是一套促进开发与运维团队之间协作和沟通的实践和工具,以开发和管理他们的系统。这有助于保持开发速度和质量。MLOps 旨在将这些 DevOps 原则调整为机器学习系统。考虑到这一背景,MLOps 到底是什么呢?
2. 定义
为了定义 MLOps,我们从检查各种定义开始:
定义 1:
“MLOps(机器学习操作)是一个范式,包括最佳实践、概念集合以及在机器学习产品的端到端概念化、实施、监控、部署和可扩展性方面的开发文化。” [1]
定义 2:
“将 DevOps 方法学扩展到包括机器学习和数据科学资产,作为 DevOps 生态系统中的第一类公民” [2]
定义 3:
我们可以使用机器学习工程(MLE)的定义,其中 MLE 是使用科学原则、工具和机器学习及传统软件工程技术来设计和构建复杂计算系统。MLE 包括从数据收集、模型构建到使模型可供产品或消费者使用的所有阶段。”(由 A.Burkov 提供) [3].
基于之前的定义,我们可以理解 MLOps 作为一套技术和实践,用于以高效、优化和有序的方式设计、构建和部署机器学习模型。这些技术和实践通常在 MLOps 生命周期的背景下进行讨论。
3. MLOps 生命周期
MLOps 生命周期 (source) CC BY 4.0
MLOps 生命周期包括 MLOps 范式中的步骤和技术,从设计和开发机器学习模型到在生产环境中部署,并随时间进行监控和维护。它通常分为三个主要阶段:
-
第一个阶段是设计过程,涉及定义业务问题、模型的要求及其预期用途。这通常涉及创建 AI/ML 画布。
-
第二个阶段是模型开发过程,包括数据和模型工程。
-
第三个阶段是运维过程,涵盖了模型的部署和维护。
在模型部署后,维持其性能是很重要的,因此这些阶段通常以循环的方式进行。这确保了模型的良好表现并且仍然满足在第一阶段定义的需求。现在我们已经讨论了 MLOps 生命周期的各个阶段,让我们深入了解 MLOps 工作流程,它概述了在每个阶段所执行的具体任务和活动。
4. MLOps 工作流程
MLOps 工作流程
MLOps 工作流程概述了开发、部署和维护机器学习模型的步骤。在理想的情况下,遵循工作流程就足够了:首先理解业务问题,然后选择、训练和部署模型。然而,现实世界中情况并非总是如此。在任何时候,都可能需要返回到之前的步骤。此外,模型部署后必须进行维护和监控,这就是为什么理解 MLOps 生命周期和 MLOps 工作流程都很重要。
4.1. 业务问题
业务问题(工作流程图、AI 画布和 ML 画布来自来源 CC BY 4.0)
MLOps 工作流程的第一步是理解业务问题,这涉及定义模型的输入和输出,以及过程和其各种子任务。为了构建这一过程,你可以使用 AI(人工智能)画布或 ML(机器学习)画布,这可以看作是组织 MLOps 工作流程的模板。AI 画布通常提供 ML/AI 实现的高层次结构,而 ML 画布提供系统的高层描述和具体细节。你可以在这里阅读更多关于这些画布的信息。
举个例子! 假设为了改善其产品,一家乳制品公司希望收集消费者对其产品的反馈。为此,需要对消费者在社交媒体平台上对产品的评论进行情感分析。可以使用机器学习技术训练模型,将这些评论的情感分类为积极、消极或中性。这将使公司更好地了解客户对其产品的体验,并识别改进的领域。这个业务问题描述转化为 AI 画布和/或 ML 画布,以更清晰地呈现:
-
预测/预测任务:AI 系统将分析文本输入并预测文本的情感(积极、消极或中性)。
-
判断:系统将使用自然语言处理技术来理解文本的含义和情感。
-
行动/决策:根据预测的情感,系统可能会采取不同的措施,例如标记负面评论以便进一步审查,或优先推广积极的社交媒体帖子。
-
结果:期望的结果是系统能够准确分类文本输入的情感,从而提高客户满意度、改善社交媒体互动或根据具体用例带来其他好处。
-
训练:系统将使用标注文本数据集进行训练,该数据集包含输入文本及其相应的情感标签。
-
输入/数据来源:系统将接受来自各种来源的文本输入,例如社交媒体帖子或客户评论。
-
输出/预测:系统将分析文本输入并预测文本的情感(积极、消极或中性)。
-
反馈:系统可能会结合用户或利益相关者的反馈,以改善其性能,例如通过调整自然语言处理算法的参数或向训练数据集中添加新数据。
-
离线评估:系统将使用标准评估指标,如精确度、召回率和 F1 得分进行评估,以确保其准确分类文本输入的情感。
-
实时监控:系统将会持续监控并根据需要进行更新,以确保其在时间的推移中继续准确地执行。
4.2. 数据工程
数据工程
在了解了当前的业务问题后,MLOps 工作流中的下一步是数据工程过程。这包括数据采集、探索和验证、数据清洗、数据标注和数据拆分。
-
数据采集涉及使用一系列技术来收集数据、创建备份、保护私人信息、创建元数据目录,并抽样测试集以避免数据窥探偏差。
-
为了探索和验证数据集,使用了一组统计和可视化技术。
-
收集到的数据通常存在噪声、包含异常值和缺失值。这些问题可能影响下一步,因此应用数据清洗步骤来解决它们。
-
数据标注是当选择的模型基于监督学习时所必需的。这一步可以手动、自动或半自动完成。
-
数据拆分是此过程的最后一步,涉及将数据划分为训练集、验证集和测试集。
4.3. ML 模型工程
ML 模型工程
MLOps 工作流程的第三步是机器学习工程,其中包括模型训练、模型评估、模型测试和模型打包。
-
训练模型涉及特征工程、代码审查和版本控制,以及超参数调优。你可能会想知道为什么特征工程包含在这一步而不是前一步。原因在于,这一步测试了多种类型和架构的模型,因此特征工程通常并不相同。值得注意的是,在选择最合适的模型之前,会训练和测试多个模型。
-
模型评估涉及验证模型,以确保它符合在业务问题步骤中描述的业务目标。
-
在模型测试步骤中,使用初始测试集进行模型验收测试。
-
一旦模型被验证和测试,最后一步是导出模型为特定格式,以便可以提供给业务应用程序。
4.4. 代码工程
代码工程
在这一步,模型已准备好部署到生产环境。模型部署包括三个步骤:模型服务、性能监控和性能日志记录。
-
为了服务一个模型,必须考虑服务模式和部署策略。服务模式指的是模型如何集成到软件中,例如将其集成作为服务、依赖项、使用预计算服务、按需服务或混合服务。部署策略指的是将模型打包的方式,例如将其部署为 Docker 容器或无服务器函数。
-
模型监控涉及观察模型的整体行为,例如模型预测与之前模型表现的偏差。
-
性能日志记录涉及将模型预测的结果保存到日志记录中。
5. 结论
在这篇文章中,我们简要介绍了 MLOps。我们讨论了 MLOps 的必要性,提出了各种定义,解释了 MLOps 生命周期,并描述了 MLOps 工作流程。如果你想了解更多关于 MLOps 的信息,我推荐访问 ml-ops.org。
这是关于 MLOps 的第一篇文章,但肯定不是最后一篇!我将继续撰写关于 MLOps 及其各种技术的教程,并附带示例,敬请关注。如果您有任何问题或建议,请随时在下方留言。
参考文献
[1] Kreuzberger, D., Kühl, N., & Hirschl, S. 机器学习操作(mlops):概述、定义和架构,2022. doi: 10.48550. arXiv 预印本 arXiv.2205.02302。
[3] ml-ops.org/content/motivation#mlops-definition
图片来源
本文中所有未在标题中提及来源的图片和图形均由作者提供。
从零开始构建检索增强生成(RAG)应用程序的初学者指南
用简单易懂的语言学习构建 AI 应用程序的关键知识
·
关注 发表在 Towards Data Science ·10 分钟阅读·2023 年 11 月 2 日
–
从零开始构建检索增强生成(RAG)应用程序的初学者指南
检索增强生成(RAG)目前非常流行,因为它为像 OpenAI 的 GPT-4 这样的语言模型引入了一些强大的功能——即利用和利用自身数据的能力。
本文将教你 RAG 的基本直觉,并提供一个简单的教程帮助你入门。
在快速变化的领域中学习的问题
AI 领域有太多噪音,特别是关于 RAG 的噪音。供应商们试图使其过于复杂。他们试图注入他们的工具、生态系统和愿景。
这使得 RAG 变得比实际需要的复杂得多。这个教程旨在帮助初学者从零开始学习如何构建 RAG 应用程序。不包含冗余内容,不使用术语(好吧,尽量少用),不依赖库,只是一个简单的逐步 RAG 应用程序。
LlamaIndex 的 Jerry 提倡从零开始构建事物以真正理解各个部分。一旦你做到这一点,使用像 LlamaIndex 这样的库就更有意义了。
从零开始学习,然后用库进行扩展。
让我们开始吧!
介绍我们的概念:检索增强生成
你可能听说过检索增强生成(RAG),也可能没有。
这是Facebook 介绍这个概念的博客文章中的定义:
构建一个能够研究和上下文化的模型更具挑战性,但这是未来发展的关键。我们最近在这一领域取得了重大进展,推出了我们的检索增强生成(RAG)架构,这是一种端到端的可微模型,将信息检索组件(Facebook AI 的密集段落检索系统)与 seq2seq 生成器(我们的双向自回归变换器[BART]模型)相结合。RAG 可以在知识密集型的下游任务上进行微调,与即使是最大的预训练 seq2seq 语言模型相比,能够达到最先进的结果。而且,与这些预训练模型不同,RAG 的内部知识可以随时轻松更改或补充,使研究人员和工程师能够控制 RAG 知道和不知道的内容,而无需浪费时间或计算能力重新训练整个模型。
哇,真是太复杂了。
为了简化技术对初学者,我们可以说 RAG 的本质是将你自己的数据(通过检索工具)添加到你传递给大型语言模型的提示中。这样,你将获得一个输出。这带来了几个好处:
-
你可以在提示中包含事实,以帮助 LLM 避免产生幻觉。
-
你可以(手动)参考权威来源来回应用户查询,帮助检查潜在问题。
-
你可以利用 LLM 可能未曾训练过的数据。
我们 RAG 系统的高级组件
-
一组文档(正式称为语料库)
-
用户输入
-
文档集合与用户输入之间的相似性测量
是的,就是这么简单。
要开始学习和理解基于 RAG 的系统,你不需要一个向量存储,甚至不需要LLM(至少在概念学习和理解方面)。
虽然它经常被描绘得很复杂,但其实不一定如此。
查询 RAG 系统的有序步骤
我们将按顺序执行以下步骤。
-
接收用户输入
-
执行我们的相似度度量
-
对用户输入和获取的文档进行后处理。
后处理是通过 LLM 完成的。
论文本身的备注
实际的 RAG 论文显然是最重要的资源。问题在于它假设了大量的上下文。这比我们需要的要复杂得多。
例如,下面是论文中提出的 RAG 系统概述。
来自 RAG 论文的 RAG 概述 由 Lewis 等人提供
这很密集。
对于研究人员来说,这很棒,但对于我们其他人来说,通过自己构建系统逐步学习将会容易得多。
通过一个示例进行工作——最简单的 RAG 系统
让我们回到从头开始构建 RAG,逐步进行。这里是我们将要经历的简化步骤。虽然这不严格是“RAG”,但它是一个好的简化模型,有助于学习并使我们能够进步到更复杂的变体。
获取文档集合
下面你可以看到我们有一个简单的‘文档’语料库(请宽容一点😉)。
corpus_of_documents = [
"Take a leisurely walk in the park and enjoy the fresh air.",
"Visit a local museum and discover something new.",
"Attend a live music concert and feel the rhythm.",
"Go for a hike and admire the natural scenery.",
"Have a picnic with friends and share some laughs.",
"Explore a new cuisine by dining at an ethnic restaurant.",
"Take a yoga class and stretch your body and mind.",
"Join a local sports league and enjoy some friendly competition.",
"Attend a workshop or lecture on a topic you're interested in.",
"Visit an amusement park and ride the roller coasters."
]
定义和执行相似度度量
现在我们需要一种方法来衡量我们将要接收的用户输入和我们组织的文档集合之间的相似度。可以说,最简单的相似度度量是杰卡德相似度。我在过去写过这方面的内容(参见这篇文章),但简短的回答是杰卡德相似度是“词集”的交集除以并集。
这使我们能够将用户输入与源文档进行比较。
附注:预处理
一个挑战是,如果我们有一个简单的字符串像"Take a leisurely walk in the park and enjoy the fresh air."
,我们需要将其预处理为一个集合,以便进行这些比较。我们将以最简单的方式进行处理,转为小写并按" "
分割。
def jaccard_similarity(query, document):
query = query.lower().split(" ")
document = document.lower().split(" ")
intersection = set(query).intersection(set(document))
union = set(query).union(set(document))
return len(intersection)/len(union)
现在我们需要定义一个函数,该函数接收精确的查询和我们的语料库,并选择‘最佳’文档返回给用户。
def return_response(query, corpus):
similarities = []
for doc in corpus:
similarity = jaccard_similarity(query, doc)
similarities.append(similarity)
return corpus_of_documents[similarities.index(max(similarities))]
现在我们可以运行它了,我们将从一个简单的提示开始。
user_prompt = "What is a leisure activity that you like?"
还有一个简单的用户输入…
user_input = "I like to hike"
现在我们可以返回响应。
return_response(user_input, corpus_of_documents)
'Go for a hike and admire the natural scenery.'
恭喜,你已经构建了一个基本的 RAG 应用程序。
我有 99 个问题,糟糕的相似度是其中之一
现在我们选择了一个简单的相似性度量进行学习。但这会有问题,因为它过于简单。它没有语义的概念。它只是查看两个文档中包含了哪些词。这意味着如果我们提供一个负面例子,我们将得到相同的“结果”,因为这是最接近的文档。
user_input = "I don't like to hike"
return_response(user_input, corpus_of_documents)
'Go for a hike and admire the natural scenery.'
这是一个在“RAG”中经常出现的话题,但目前请放心,我们会在后面解决这个问题。
目前为止,我们还没有对响应的“文档”进行任何后处理。到现在为止,我们只实现了“检索增强生成”的“检索”部分。下一步是通过引入大型语言模型(LLM)来增强生成。
添加 LLM
为此,我们将使用ollama来启动并运行本地机器上的开源 LLM。我们同样可以使用 OpenAI 的 gpt-4 或 Anthropic 的 Claude,但现在我们将从Meta AI的开源 llama2 开始。
本文假设你对大型语言模型有一些基本了解,所以我们直接开始查询这个模型吧。
import requests
import json
首先,我们将定义输入。为了使用这个模型,我们将采取
-
用户输入,
-
获取最相似的文档(按我们的相似性度量来衡量),
-
将其传递给语言模型的提示,
-
然后 将结果返回给用户
这引入了一个新术语,即提示。简而言之,它就是你给 LLM 的指令。
当你运行这段代码时,你会看到流式结果。流式处理对用户体验很重要。
user_input = "I like to hike"
relevant_document = return_response(user_input, corpus_of_documents)
full_response = []
prompt = """
You are a bot that makes recommendations for activities. You answer in very short sentences and do not include extra information.
This is the recommended activity: {relevant_document}
The user input is: {user_input}
Compile a recommendation to the user based on the recommended activity and the user input.
"""
确定了这一点之后,现在让我们调用 ollama(和 llama2)的 API。一个重要的步骤是确保 ollama 已经在你的本地机器上运行,通过运行ollama serve
。
注意:这在你的机器上可能很慢,在我的机器上肯定很慢。请耐心点,小草 hopper。
url = 'http://localhost:11434/api/generate'
data = {
"model": "llama2",
"prompt": prompt.format(user_input=user_input, relevant_document=relevant_document)
}
headers = {'Content-Type': 'application/json'}
response = requests.post(url, data=json.dumps(data), headers=headers, stream=True)
try:
count = 0
for line in response.iter_lines():
# filter out keep-alive new lines
# count += 1
# if count % 5== 0:
# print(decoded_line['response']) # print every fifth token
if line:
decoded_line = json.loads(line.decode('utf-8'))
full_response.append(decoded_line['response'])
finally:
response.close()
print(''.join(full_response))
Great! Based on your interest in hiking, I recommend trying out the nearby trails for a challenging and rewarding experience with breathtaking views Great! Based on your interest in hiking, I recommend checking out the nearby trails for a fun and challenging adventure.
这给了我们一个完整的 RAG 应用,从零开始,没有提供者,没有服务。你了解了检索增强生成应用中的所有组件。视觉上,这是我们构建的内容。
如果你幸运的话,LLM 将处理与推荐文档不符的用户输入。我们可以在下面看到。
user_input = "I don't like to hike"
relevant_document = return_response(user_input, corpus_of_documents)
# https://github.com/jmorganca/ollama/blob/main/docs/api.md
full_response = []
prompt = """
You are a bot that makes recommendations for activities. You answer in very short sentences and do not include extra information.
This is the recommended activity: {relevant_document}
The user input is: {user_input}
Compile a recommendation to the user based on the recommended activity and the user input.
"""
url = 'http://localhost:11434/api/generate'
data = {
"model": "llama2",
"prompt": prompt.format(user_input=user_input, relevant_document=relevant_document)
}
headers = {'Content-Type': 'application/json'}
response = requests.post(url, data=json.dumps(data), headers=headers, stream=True)
try:
for line in response.iter_lines():
# filter out keep-alive new lines
if line:
decoded_line = json.loads(line.decode('utf-8'))
# print(decoded_line['response']) # uncomment to results, token by token
full_response.append(decoded_line['response'])
finally:
response.close()
print(''.join(full_response))
Sure, here is my response:
Try kayaking instead! It's a great way to enjoy nature without having to hike.
改进领域
如果我们回到 RAG 应用的图示,考虑一下我们刚刚构建的内容,我们会看到各种改进的机会。这些机会是工具如向量存储、嵌入和提示“工程”介入的地方。
这里有十个潜在的改进领域:
-
文档的数量 👉 更多文档可能意味着更多的推荐。
-
文档的深度/大小 👉 更高质量的内容和信息更多的长文档可能更好。
-
我们提供给 LLM 的文档数量 👉 目前,我们只给 LLM 一个文档。我们可以将几个文档作为“上下文”输入,并允许模型根据用户输入提供更个性化的推荐。
-
我们提供给 LLM 的文档部分 👉 如果我们有较大或更全面的文档,我们可能只想添加这些文档的部分、各个文档的部分,或其某种变体。在词汇中,这被称为分块(chunking)。
-
我们的文档存储工具 👉 我们可能以不同的方式或不同的数据库存储文档。特别是,如果我们有大量文档,我们可能会探索将它们存储在数据湖或向量存储中。
-
相似度度量 👉 我们如何度量相似度是重要的,我们可能需要在性能和全面性之间进行权衡(例如,查看每一个单独的文档)。
-
文档及用户输入的预处理 👉 我们可能在将用户输入传递到相似度度量之前,进行一些额外的预处理或增强。例如,我们可能会使用嵌入将输入转换为向量。
-
相似度度量 👉 我们可以更改相似度度量,以获取更好或更相关的文档。
-
模型 👉 我们可以更改我们使用的最终模型。我们上面使用的是 llama2,但我们也可以轻松使用 Anthropic 或 Claude 模型。
-
提示 👉 我们可以使用不同的提示输入 LLM/模型,并根据我们希望得到的输出进行调整。
-
如果你担心有害或有毒的输出 👉 我们可以实现一种“断路器”,运行用户输入以检查是否存在有毒、有害或危险的讨论。例如,在医疗保健背景中,你可以检查信息是否包含不安全的语言,并相应地做出回应——超出典型流程之外。
改进的范围不仅限于这些点;可能性广泛,我们将在未来的教程中深入探讨。在此之前,如果你有任何问题,请随时 在 Twitter 上联系我。祝你 RAGING 愉快 😃。
参考文献
这篇文章最初发布在 learnbybuilding.ai。我将在未来几个月举办一个关于如何为产品经理构建生成式 AI 产品的课程, 点击这里报名。
构建高质量机器学习数据集的初学者指南
数据清洗、可视化、增强和合成数据生成的工具和技术
·
关注 发表在Towards Data Science ·9 分钟阅读·2023 年 11 月 11 日
–
找到数据复杂性的真实来源可能就像在扮演“数据侦探”,直到你找到解锁真正有用洞察的“黄金钥匙”。照片由Michael Dziedzic提供,刊登在Unsplash
智能数据胜过大数据。这是“数据中心人工智能”范式的基本假设。
数据科学家不仅仅是“简单地预处理”数据,还应该建立一种持续且系统的理解和改进数据集的实践。
这将最终将我们的重点从盲目追求通过使用越来越复杂的算法来提升分类结果,转向对分类结果本身的深刻理解,问题的复杂性源自何处,以及如何调整数据以便分类器能够更好地学习问题,从而提高其性能。
如果你是机器学习新手,这可能会让你感到有些畏惧:“构建高质量数据集的最佳实践是什么,如何实施?”
在本教程中,我们将通过一个简单的案例来运用数据中心人工智能范式,以实现高质量数据并改善我们的机器学习分类结果。
根据数据中心人工智能的原则——一切都围绕数据——我们不会深入到模型本身(说实话,它会是一个简单的决策树)。
我们将使用皮马印第安人糖尿病数据集,该数据集在 Kaggle 上免费提供(许可证:CC0: 公开领域)。你还可以在数据中心人工智能社区 GitHub找到所有代码和额外的材料。
我们开始吧?
步骤 1:进行数据概况分析以理解数据
在开始整理我们的数据集之前,我们需要理解我们要解决的问题以及我们处理的数据的特殊性。彻底理解数据特征、问题复杂性和使用案例领域是数据中心人工智能的基本原则之一。
这将帮助我们确定下一步行动,以推进你的机器学习流程。
关于数据概况分析,有几个有趣的开源工具可以探索:我自己做了一些评测,包括[ydata-profiling](https://github.com/ydataai/ydata-profiling)
、[dataprep](https://github.com/sfu-db/dataprep)
、[sweetviz](https://github.com/fbdesignpro/sweetviz)
、[autoviz](https://github.com/AutoViML/AutoViz)
和[lux](https://github.com/lux-org/lux)
。
我目前主要使用ydata-profiling:我发现它是一个顶尖的工具,可以让数据从业者在几行代码中完成对数据特征和可视化的全面分析,而无需费劲地使用 pandas。
首先,你需要安装 ydata-profiling(最好使用虚拟环境——如果你不知道怎么做,你可以 查看这个 2 分钟的视频,或者如果你从未在 conda 环境中工作过,可以查看这个 完整教程):
然后,我们可以通过保存一个.html
报告来获得数据的完整概述,其中包含所有必要的特征和可视化:
数据报告立即让我们了解数据的整体特征,并突出一些我们可能需要考虑的警告:
YData Profiling 报告:查找数据集的基本统计信息、可视化和质量警告。
数据集包含 768 个观察值和 9 个变量/特征。虽然 8 个是数值型的,1 个被识别为分类变量(Outcome
似乎是我们的目标)。没有重复的行,并且显然没有缺失值。最后,特征中发现了一些高相关性
警告。此外,几个特征有大量的零值
。
现在是时候充当数据侦探了。高相关性
在生物特征中是比较常见的,但这些零
值怎么样呢?
查看一些突出的特征(例如BMI
),我们可以看到这些值与整体分布相差甚远。根据领域知识,这些“0”值实际上是没有意义的:怀孕次数
的 0 值是可以接受的,但 BMI、葡萄糖、胰岛素、血压或皮肤厚度的 0 值则是无效的。
YData Profiling 报告:BMI 特征,表明零值与分布有很大差距。
我们很快意识到这些零值编码了什么:缺失数据。
现在我们将解决这个问题,但一个彻底的 EDA 过程可以涵盖更多内容。查看这个 探索性数据分析的基本指南 以了解你可以从数据中发现什么其他信息。
第 2 步:调查数据质量问题
既然我们发现某些列有无效的零值,我们可以开始处理数据集中的缺失数据问题。
许多机器学习模型和 scikit-learn 估算器本身不支持缺失值,因此在将数据集提供给估算器之前,我们需要以某种方式处理这些 NaNs。
首先,我们将这些 0 值标记为 NaN 值:
现在,我们可以使用数据填充来用合理的替代值替换 NaN 观察值。
“没有免费午餐”定理告诉我们,没有一种最佳的解决方案适用于所有情况——我们应该调查不同解决方案对训练数据复杂性的影响,并确定什么能最好地提升我们的机器学习模型。这实际上是数据中心 AI 的另一个原则:不断迭代和改进。
目前我们将使用一个非常简单的方法——SimpleImputer
——将零值替换为每个特征的均值。这是一个非常初级的方法,可能会在我们的分布中产生一些不希望有的“尖峰”,但目标只是展示如何突出和填补缺失数据,我们可以稍后尝试更好的方法:
现在,我们可以尝试一个非常简单的决策树分类器,看看我们的分类结果的基线是什么。作为旁注,决策树可以扩展以自然支持缺失值,通过替代拆分或其他方法。确实,在 scikit-learn 的文档 中似乎决策树在当前版本 (1.3.2
) 中确实支持缺失值。然而,当我使用版本 1.2.2
时,我遇到了这个错误:
即使 NaN 值在内部处理了,用缺失数据训练你的模型也不是一个好习惯,因为这会危及模型从混乱和有限的信息中学习到的概念。
这是混淆矩阵:
分类结果不是很好。请记住,我们使用的是简单决策树,但仍然……我们目标类别的预测存在显著差异。为什么分类器在类别“0”上的表现优于类别“1”?
第 3 步:增强不足代表的类别
如果我们在第 1 步中有留意到(也许你已经发现了),我们的目标类别Outcome
是不平衡的。虽然可能不足以在默认设置中触发警告(默认阈值是0.5
),但足以使分类器对多数类别产生偏向,忽视少数类别。从 Profiling Report 中提供的数据可视化中可以清楚地看出这一点:
YData Profiling Report: 结果类别“0”和“1”并不均等地代表。分类器自然会对代表性较强的类别“0”更有偏向,忽视类别“1”。
请注意,虽然缺失数据可能是由于数据收集、传输或存储过程中出现的多个错误造成的,类不平衡可能反映了领域的自然特征:例如,这个医疗中心诊断出的糖尿病患者较少。
然而,对训练数据进行处理仍然很重要,以确保模型不会忽视少数类别:事实上,这就是我们试图更准确预测的内容。
假阳性是糟糕的,因为它会给健康患者错误的信息,告诉她她有糖尿病。 但当进行额外的测试时,这将只是一个“惊吓”。
然而,在这种情况下,假阴性更糟。 我们将告诉一位患有糖尿病的患者一切正常,她未被诊断出来,疾病继续发展。
增加这些数字的一种方法是使用数据过采样技术。 数据过采样是数据从业者中流行的一种技术,用于调整数据集中现有类别或分类之间的比例,从而缓解数据不平衡问题。
这只是合成数据的许多有趣和有用应用之一。
虽然合成数据可能有多种解释 — 例如,“假数据”,“虚拟数据”,“模拟数据” — 但在这里我们指的是“数据驱动的”合成数据生成。
从这个意义上说,合成数据是人工生成的,保留了真实数据的特征 — 结构、统计属性、依赖关系和相关性。
有大量的方法和开源工具可用于生成合成数据 — [ydata-synthetic](https://github.com/ydataai/ydata-synthetic)
, [sdv](https://github.com/sdv-dev/SDV)
, [gretel-synthetics](https://github.com/gretelai/gretel-synthetics)
, [nbsynthetic](https://github.com/NextBrain-ai/nbsynthetic)
, 和 [synthcity](https://github.com/vanderschaarlab/synthcity)
只是我过去尝试过的一些。
同样地…… 这里没有“免费午餐”:选择最合适的方法将始终取决于需要合成数据的目标。
为了快速掌握合成数据如何用于增强,我们将利用[ydata-synthetic](https://github.com/ydataai/ydata-synthetic)
包,并尝试他们的高斯混合模型。
首先,我们需要安装该包:
一旦完成这些步骤,创建合成数据就非常简单了:
获得我们的合成数据之后,我们可以简单地从合成数据中抽取新生成的少数类样本的子集,并将其添加到训练数据中,以创建一个平衡(即 50%-50%)的分布:
看看这如何影响我们决策树的学习及其随后的结果:
以及混淆矩阵:
注意到我们对训练集进行如此简单的修改,导致我们的 F-score 提升了 10%,少数类敏感性结果显著改善(从 53%提高到 73%)。
这就是数据中心化 AI 范式的美妙之处:在不触及模型参数化的情况下,我们用非常简单的启发式方法和标准技术显著改善了我们的训练集的质量 — 想象一下如果我们采用更先进的策略和专门的数据准备流水线能做些什么!
当然,类别 0 的召回率有些下降,但最终,我们需要这个模型比特异性更敏感(即更好地检测正类而非负类),这是由于我们所面临的特定约束:疾病诊断——再一次,数据中心人工智能的另一个原则:方法和结果需要根据领域的需求和约束进行评估。
最终思考和进一步方向
在本文中,我们用一个非常实际的案例实验了数据中心人工智能范式。
我们总是从理解数据开始。我们发现、调查并解决了特定的数据质量问题,如缺失数据,**以及通过合成数据来改善我们的训练数据,以克服领域的不平衡性。**当然,对于这样一个快速而简单的案例研究,我们专注于简单的启发式方法来完成工作,但数据科学家的工作从未止步于此。
如果我们考虑不同的插补方法,结果会如何变化?我们如何在合成数据生成中获得更好的拟合?我们是否应该平衡两个类别,还是提高少数类别的表示?某些特征转换或降维是否有助于分类结果?我们是否应该去除一些混淆特征?
所有这些问题在任何机器学习项目开始时似乎都是不可知的。**但是随着我们开始测量和揭示每个数据集中的复杂源,我们可以获得更好的见解,了解哪些方法可以改进分类结果(一种“元学习”方法)。**当然,数据需要根据数据特征和项目的最终目标进行处理和改进。
生产一个预定义的流程并将数据准备视为一刀切的解决方案,就像是盲目飞行一样。**相反,一个熟练的数据科学家会不断扮演数据侦探的角色,并尝试根据数据留给我们的线索找到最佳的技术。**通常这确实有效。我们只需要保持警觉!
我希望你喜欢这个教程,像往常一样,反馈、问题和建议非常受欢迎。请在评论中告诉我你希望我写关于哪些其他主题!
关于我
博士,机器学习研究员,教育者,数据倡导者,以及全能型人才。在 Medium 上,我撰写关于数据中心人工智能和数据质量的文章,教育数据科学和机器学习社区如何将不完美的数据转变为智能数据。
数据中心人工智能社区 | GitHub | Google Scholar | LinkedIn
《线性规划及单纯形算法初学者指南》
玉米农场。Dall-E 2 的静物画。
解决各种优化问题
·发布于 Towards Data Science ·8 分钟阅读·2023 年 1 月 9 日
–
线性规划是一种强大的工具。它在工程、金融和运筹学等广泛领域中都有应用。线性规划已被用来解决从航空公司航班调度到制造过程设计等各种问题。在这篇博客文章中,我们将探讨线性规划的基础知识以及它如何用来解决实际问题。
线性规划(LP)是一种数学优化技术。它用于寻找涉及多个变量和约束条件问题的最佳解决方案。通过将问题的约束条件和目标函数表示为线性方程和不等式的系统,线性规划算法可以系统地搜索解空间,找到最大化或最小化给定目标的解决方案。
首先,我会给出一个简单的例子。然后我们将深入探讨单纯形算法,它在后台用于快速找到 LP 问题的最佳解决方案。
线性规划问题的示例
假设一个农民拥有 120 英亩的土地用来种植小麦和玉米。小麦每吨需要 2 英亩土地,玉米每吨需要 1 英亩土地。农民希望最大化作物的利润,小麦每吨€100,玉米每吨€150。农民还有限制,每种小麦的产量不超过 50 吨,每种玉米的产量不超过 40 吨。
要使用线性规划解决这个问题,我们首先需要确定决策变量,即农民将种植的小麦和玉米的数量。我们将使用变量 w 和 c 分别表示小麦和玉米的吨数。
接下来,我们需要编写目标函数,它表示农民想要最大化的利润。在这种情况下,目标函数是:
然后,我们需要编写约束条件,这些条件代表了可以生产的小麦和玉米的数量限制。约束条件如下:
前两个约束条件表示可以生产的小麦和玉米总吨数的限制。第三个约束条件表示可用土地总面积的限制。
为了解决这个问题,我们可以使用简单形算法或其他线性规划方法来寻找最大化目标函数的* w* 和 * c* 的值,同时满足约束条件。在这种情况下,最优解是种植 40 吨小麦和 40 吨玉米,这样可以获得€10,000 的利润。
这只是一个简单的例子,用来说明解决线性规划问题的基本步骤。在实际应用中,线性规划用于解决更多变量和约束条件的复杂问题。
下面是问题的可视化:
线性规划可视化。图片由作者提供。
灰色区域称为可行区域。区域中的每一个点都包含一个可行解。该区域受约束条件的限制。
线性规划基本定理 说明了最大值出现在区域的角落。这是简单形算法使用的重要信息。
简单形算法
简单形算法是一种广泛使用的解决线性规划问题的方法。它由乔治·丹茨格于 1947 年开发。该算法的直观思路是以系统的方式从可行区域的角落“行走”。当找到最优解时,过程结束。
简单形算法是一个迭代过程,依靠数学计算和逻辑推理来找到线性规划问题的最优解。它高效可靠,也用于混合整数规划(在约束条件放松后)。简单形算法的基本步骤如下:
-
将线性规划问题的约束条件转化为线性方程组的标准形式。
-
通过将一些变量设置为零,并求解其余变量,找到一个基本可行解。
-
测试基本可行解是否最优。如果是,算法结束。如果不是,算法选择一个变量进入基准,并相应更新基本可行解。
-
重复步骤 3,直到找到最优解。
让我们逐步走过小麦和玉米的例子。
步骤 1. 将线性规划问题的约束转换为线性方程组,即标准形式。 第一步是将约束和目标重写为标准形式:
重写为标准形式。图片由作者提供。
在标准形式中,“小于”符号被替换为等号,并且每个约束都有一个松弛变量添加(s1、s2、s3)。这样,小于仍然得以保持,因为松弛变量只能取得正值。
通常,表格用于使问题更具可读性。表格是一个我们写下系数的表格。列代表一个变量,行代表一个约束。在这种情况下,底部行包含目标函数的系数。最后一列包含方程右侧的值。我们使用标准形式来创建表格:
玉米和小麦问题的表格。每个约束和目标都在行中表示,而列表示变量。值是系数。图片由作者提供。
步骤 2. 通过将某些变量设置为零并求解其余变量来找到一个基本可行解。 现在我们有了表格,我们可以使用它来找到一个基本可行解。基本可行解是一个满足所有约束并位于一个顶点上的解。我们可以很容易地从表格中检测到一个。
在表格中,有两种类型的变量:基本变量 和 非基本变量。基本变量只有一个非零项,而非基本变量有多个非零项。基变量 包含所有基本变量。如果我们查看当前的表格状态,松弛变量 s₁、s₂ 和 s₃ 在基变量中。变量 w 和 c 是非基本的,并被设置为零。当前基本可行解的值为:s₁ = 50,s₂ = 40,s₃ = 120,Z = 0,w = 0 和 c = 0。
非基本变量和基本变量。图片由作者提供。
该解对应于以下角点:
单纯形算法的起点。图片由作者提供。
步骤 3. 测试基本可行解是否最优。如果是,则算法结束。如果不是,算法选择一个变量进入基变量,并相应地更新基本可行解。 通过查看表格中的目标行,可以测试基本可行解是否最优。由于这是一个最大化问题,我们可以通过去除负系数来改进它。如果所有负值都消失了,我们就无法再改进了。底部行中存在负值,因此可以改进,我们尚未找到最优解。
根据目标行中的负值选择进入变量。图像由作者提供。
现在,我们需要选择一个变量进入基。选择进入变量时,取目标行中最低的系数。在我们的例子中,它是 -150,对应于 c。这就是进入变量。接着,我们需要选择要进行高斯消元的行。这是通过将右侧值除以 c 的系数来完成的。因此,在我们的例子中,第二行得到 40/1=40,第三行得到 120/1=120。最低值是 40,这就是为什么使用第二行。现在我们可以用第 2 行清除表格,以将 c 包括在基中:
执行高斯消元以使变量c
进入基:从第 3 行中减去第 2 行,将第 2 行乘以 150 后加到第 4 行。图像由作者提供。
新的目标值是 6000,基中的变量是 c、s₁ 和 s₃,分别为 40、50 和 80。我们移动到一个新的角点,即下一个基本可行解:
现在我们转到下一个可行解(右下角)。图像由作者提供。
第 4 步。重复第 3 步,直到找到最优解。 由于表格的目标行中有负值,因此未找到最优解。所以让我们重复第 3 步:进入变量是 w,因为它在目标行中有最低的系数。我们应该使用第 3 行进行高斯消元,因为 80/2 < 50/1。经过行简化,表格如下:
让 w 进入基,将第 3 行除以 2,从第 1 行中减去这一新行 3,将第 3 行乘以 100 后加到第 4 行。图像由作者提供。
基中的变量是 w、c 和 s₁,对应的值分别为 40、40 和 10。Z(目标)的值等于 10000。这正是我们一开始得到的最优解。底行中没有负值,所以这是最优解。我们采取的步骤:
在使用单纯形算法解决问题时所采取的步骤。从左下角开始,走两步到达最优解。图像由作者提供。
这是一个简单的单纯形算法工作原理的例子。它用于解决比我们讨论的更复杂的问题。
结论
线性规划是一种工具,可以帮助个人和组织充分利用资源,实现目标。无论你是试图最大化利润的企业主、寻求优化复杂过程的研究员,还是学习优化技术的学生,线性规划都提供了广泛的应用和成长机会。通过理解线性规划的原理和单纯形算法,你可以迈出解决各种优化问题的第一步,并做出能够产生持久影响的明智决策。
在实践中应用这些技术时,你不需要自己实现算法,因为优化软件会为你完成这项工作。
单纯形算法也应用于混合整数规划,其中约束的放松使得算法得以应用。如果你想了解更多,可以阅读下面的文章。
简单示例及其解决方案和代码。
[towardsdatascience.com ## 数据科学家应该知道的数学优化启发式方法
局部搜索、遗传算法等。
[towardsdatascience.com
LLM 微调的初学者指南
原文:
towardsdatascience.com/a-beginners-guide-to-llm-fine-tuning-4bae7d4da672
如何使用一个工具微调 Llama 及其他 LLM
·发表于Towards Data Science ·8 分钟阅读·2023 年 8 月 30 日
–
图片由作者提供
对大型语言模型(LLMs)的日益关注导致了旨在简化其训练过程的工具和封装的激增。
流行的选项包括 LMSYS 的FastChat(用于训练Vicuna)和 Hugging Face 的transformers/trl库(用于我之前的文章)。此外,每个大型 LLM 项目,如WizardLM,通常都有自己独特的训练脚本,灵感来自于最初的Alpaca实现。
在这篇文章中,我们将使用由 OpenAccess AI Collective 创建的Axolotl工具。我们将使用它在一个包含 1,000 个 Python 代码样本的 evol-instruct 数据集上微调一个Code Llama 7b模型。
🤔 为什么选择 Axolotl?
Axolotl 的主要吸引力在于它提供了一站式解决方案,其中包括众多功能、模型架构和一个活跃的社区。以下是我最喜欢的几点:
-
配置:用于训练 LLM 的所有参数都整齐地存储在 yaml 配置文件中。这使得模型的共享和再现变得方便。你可以在这里查看 Llama 2 的示例。
-
数据集灵活性:Axolotl 允许指定多个数据集,支持各种提示格式,如 alpaca(
{"instruction": "...", "input": "...", "output": "..."}
),sharegpt:chat({"conversations": [{"from": "...", "value": "..."}]}
)和原始完成({"text": "..."}
)。组合数据集非常顺畅,并且避免了统一提示格式的麻烦。 -
功能:Axolotl 配备了 SOTA 技术,如 FSDP、deepspeed、LoRA、QLoRA、ReLoRA、样本打包、GPTQ、FlashAttention、xformers 和 rope scaling。
-
工具:集成了众多用户友好的工具,包括添加或更改特殊 token,或自定义 wandb 配置。
一些使用此工具训练的知名模型包括 OpenAccess AI Collective 的Manticore-13b和 Eric Hartford 的Samantha-1.11–70b。像其他包装器一样,它建立在 transformers 库之上,并使用了许多其功能。
⚙️ 创建你自己的配置文件
在开始之前,我们需要一个配置文件。你可以重用[examples](https://github.com/OpenAccess-AI-Collective/axolotl/tree/main/examples)
文件夹中的现有配置。我们将调整QLoRA 配置以创建我们自己的Code Llama模型。该模型将在[nickrosh/Evol-Instruct-Code-80k-v1](https://huggingface.co/datasets/nickrosh/Evol-Instruct-Code-80k-v1)
数据集中的 1,000 个 Python 样本子集上进行训练。
首先,我们必须将base_model
和base_model_config
字段更改为"codellama/CodeLlama-7b-hf"。为了将我们训练的适配器推送到 Hugging Face Hub,我们需要添加一个新的字段hub_model_id
,对应我们的模型名称"EvolCodeLlama-7b"。现在,我们必须将数据集更新为[mlabonne/Evol-Instruct-Python-1k](https://huggingface.co/datasets/mlabonne/Evol-Instruct-Python-1k)
并将type
设置为"alpaca"。
数据集中没有大于 2048 个 token 的样本,因此我们可以将sequence_len
减少到"2048"以节省一些 VRAM。说到 VRAM,我们将使用micro_batch_size
为 10 和gradient_accumulation_steps
为 1,以最大化其使用。在实际操作中,你可以尝试不同的值,直到使用超过 95%的可用 VRAM。
为了方便,我将"axolotl"添加到wandb_project
字段中,以便在我的账户上更容易追踪。我还将warmup_steps
设置为"100"(个人偏好),将eval_steps
设置为 0.01,以便进行 100 次评估。
最终的配置文件应该是这样的:
base_model: codellama/CodeLlama-7b-hf
base_model_config: codellama/CodeLlama-7b-hf
model_type: LlamaForCausalLM
tokenizer_type: LlamaTokenizer
is_llama_derived_model: true
hub_model_id: EvolCodeLlama-7b
load_in_8bit: false
load_in_4bit: true
strict: false
datasets:
- path: mlabonne/Evol-Instruct-Python-1k
type: alpaca
dataset_prepared_path: last_run_prepared
val_set_size: 0.02
output_dir: ./qlora-out
adapter: qlora
lora_model_dir:
sequence_len: 2048
sample_packing: true
lora_r: 32
lora_alpha: 16
lora_dropout: 0.05
lora_target_modules:
lora_target_linear: true
lora_fan_in_fan_out:
wandb_project: axolotl
wandb_entity:
wandb_watch:
wandb_run_id:
wandb_log_model:
gradient_accumulation_steps: 1
micro_batch_size: 10
num_epochs: 3
optimizer: paged_adamw_32bit
lr_scheduler: cosine
learning_rate: 0.0002
train_on_inputs: false
group_by_length: false
bf16: true
fp16: false
tf32: false
gradient_checkpointing: true
early_stopping_patience:
resume_from_checkpoint:
local_rank:
logging_steps: 1
xformers_attention:
flash_attention: true
warmup_steps: 100
eval_steps: 0.01
save_strategy: epoch
save_steps:
debug:
deepspeed:
weight_decay: 0.0
fsdp:
fsdp_config:
special_tokens:
bos_token: "<s>"
eos_token: "</s>"
unk_token: "<unk>"
你也可以在这里找到这个配置文件,作为一个 GitHub gist。
在开始训练模型之前,我想介绍几个重要的参数:
-
QLoRA:我们使用 QLoRA 进行微调,这就是为什么我们以 4 位精度(NF4 格式)加载基础模型。你可以查看这篇文章来了解更多关于 QLoRA 的内容,作者是本杰明·玛丽。
-
梯度检查点:通过删除在反向传播过程中按需重新计算的一些激活来降低 VRAM 需求。根据 Hugging Face 的文档,它还会使训练速度降低约 20%。
-
FlashAttention:这实现了FlashAttention机制,通过巧妙融合 GPU 操作来提高模型的速度和内存效率(了解更多内容,请参阅这篇文章,作者是Aleksa Gordić)。
-
样本打包:一种通过重新组织样本顺序来创建尽可能少填充的批次的智能方法(装箱问题)。因此,我们需要更少的批次来训练相同的数据集。它的灵感来自于Multipack Sampler(见我的笔记)和Krell 等人。
你可以在其他一些工具中找到 FlashAttention,但样本打包相对较新。据我所知,OpenChat是第一个在微调过程中使用样本打包的项目。感谢 Axolotl,我们可以免费使用这些技术。
🦙 微调 Code Llama
配置文件准备好之后,就该开始实际的微调工作了。你可以考虑在 Colab 笔记本上运行训练。然而,对于那些没有高性能 GPU 的人来说,一个更具成本效益的解决方案是租用基于云的 GPU 服务,如 AWS、Lambda Labs、Vast.ai、Banana或RunPod。
就我个人而言,我使用 RunPod,这是微调社区中一个受欢迎的选项。它不是最便宜的服务,但在界面清晰度和成本之间达到了一个良好的折中。你可以使用你喜欢的服务轻松复制以下步骤。
当你的 RunPod 账户设置好后,转到“管理”>“模板”并点击“新建模板”。以下是一个简单的模板:
作者提供的图像
让我们回顾一下不同的字段及其对应的值:
-
模板名称:Axolotl(你可以选择任何你想要的名称)
-
容器镜像: winglian/axolotl-runpod:main-py3.10-cu118–2.0.1
-
容器磁盘: 100 GB
-
存储卷: 0 GB
-
卷挂载路径: /workspace
此外,还有两个有用的环境变量可以包含:
另外,你可以稍后在终端登录(使用 huggingface-cli login 和 wandb login)。设置完成后,前往 Community Cloud 并部署 RTX 3090。你可以在这里搜索你的模板名称并选择它,如下所示:
图片由作者提供
你可以点击“继续”,RunPod 将部署你的模板。你可以在你的 pod 的日志中查看安装情况(管理 > Pods)。当选项可用时,点击“连接”。在这里,点击“启动 Web 终端”,然后“连接到 Web 终端”。你现在已连接到你的 pod!
以下步骤是无论你选择哪个服务都相同的:
- 我们按如下方式安装 Axolotl 和 PEFT 库:
git clone https://github.com/OpenAccess-AI-Collective/axolotl
cd axolotl
pip3 install -e .[flash-attn]
pip3 install -U git+https://github.com/huggingface/peft.git
2. 下载我们创建的配置文件:
wget https://gist.githubusercontent.com/mlabonne/8055f6335e2b85f082c8c75561321a66/raw/93915a9563fcfff8df9a81fc0cdbf63894465922/EvolCodeLlama-7b.yaml
3. 你现在可以使用以下命令 开始微调模型:
accelerate launch scripts/finetune.py EvolCodeLlama-7b.yaml
如果一切配置正确,你应该能够在 一个多小时 内训练模型(我花了 1 小时 11 分钟 44 秒)。如果你检查使用的 GPU 内存,你会发现几乎达到 100%,这意味着我们正在很好地优化它。如果你使用具有更多 VRAM 的 GPU(如 A100),你可以增加微批量大小以确保充分利用它。
与此同时,可以关闭 Web 终端,并在 Weights & Biases 上检查你的损失。我们使用 tmux,所以即使你关闭终端,训练也不会停止。以下是我的损失曲线:
图片由作者提供
我们看到评估损失稳步改善,这是一个好兆头。然而,你也可以发现评估损失的下降并未与输出质量的降低相关联… 评估你的模型的最佳方式是直接使用它:你可以在终端中运行命令 accelerate launch scripts/finetune.py EvolCodeLlama-7b.yaml --inference --lora_model_dir="./qlora-out"
。
QLoRA 适配器应该已经上传到 Hugging Face Hub。然而,你也可以通过以下步骤将基础 Code Llama 模型与此适配器合并并将合并后的模型推送到那里:
- 下载 这个脚本:
wget https://gist.githubusercontent.com/mlabonne/a3542b0519708b8871d0703c938bba9f/raw/60abc5afc07f9d843bc23d56f4e0b7ab072c4a62/merge_peft.py
2. 使用以下命令执行:
python merge_peft.py --base_model=codellama/CodeLlama-7b-hf --peft_model=./qlora-out --hub_id=EvolCodeLlama-7b
恭喜,你现在应该在 Hugging Face Hub 上拥有你自己的 EvolCodeLlama-7b!作为参考,你可以在这里访问我用这个过程训练的模型: [mlabonne/EvolCodeLlama-7b](https://huggingface.co/mlabonne/EvolCodeLlama-7b)
考虑到我们的 EvolCodeLlama-7b 是一个代码 LLM,比较它在标准基准上的表现与其他模型会很有趣,例如HumanEval和MBPP。作为参考,你可以在以下地址找到排行榜:多语言代码评估。
如果你对这个模型满意,你可以使用 GGML 进行量化以便本地推理,使用这个免费的 Google Colab 笔记本。你还可以利用deepspeed对更大的模型(例如,70b 参数)进行微调,只需额外的配置文件即可。
结论
在本文中,我们覆盖了如何高效微调 LLMs的基本要点。我们自定义了参数,在一个小的 Python 数据集上训练了我们的 Code Llama 模型。最后,我们合并了权重,并将结果上传到 Hugging Face。
我希望你觉得这份指南有用。我推荐使用 Axolotl 与基于云的 GPU 服务来获取一些经验,并在 Hugging Face 上上传几个模型。构建自己的数据集,调整参数,过程中可能会遇到一些问题。就像使用任何包装器一样,不要犹豫去查看源代码,以便对其实际操作有一个很好的直观了解。这样在长期内会大大受益。
感谢 OpenAccess AI Collective 和所有贡献者!
如果你对 LLMs 的更多技术内容感兴趣,在 Medium 上关注我。
相关文章
LLM 微调的实用介绍
使用 AutoGPTQ 对自己的 LLMs 进行量化
了解更多关于机器学习的内容,并通过一键支持我的工作——在这里成为 Medium 会员:
加入 Medium,使用我的推荐链接 - Maxime Labonne
作为 Medium 会员,你的会员费用的一部分会给你阅读的作者,同时你可以全面访问每个故事…
初学者指南:通过蒙特卡罗模拟理解 A/B 测试性能
·发布于 Towards Data Science ·16 分钟阅读·2023 年 8 月 5 日
–
本教程探讨了协变量如何在随机实验中影响 A/B 测试的精度。一个适当随机化的 A/B 测试通过比较治疗组和对照组的平均结果来计算提升。然而,除了治疗以外的特征对结果的影响决定了 A/B 测试的统计特性。例如,在测试提升计算中遗漏重要特征可能导致提升估计的不精确,即使随着样本量的增加,它也会收敛到真实值。
你将学习 RMSE、偏差和测试规模是什么,并通过生成模拟数据和运行蒙特卡罗实验来理解 A/B 测试的性能。这种工作有助于了解数据生成过程(DGP)的特性如何影响 A/B 测试的性能,并将帮助你在实际数据上进行 A/B 测试。首先,我们讨论估计量的一些基本统计特性。
估计量的统计特性
均方根误差(RMSE)
RMSE(均方根误差):RMSE 是一个经常使用的衡量模型或估计量预测值与观测值之间差异的指标。它是预测值与实际观测值之间平均平方差异的平方根。RMSE 的公式是:
RMSE = sqrt[(1/n) * Σ(actual - prediction)²]
RMSE 对大误差给予相对较高的权重,因为在平均之前这些误差会被平方,这意味着当大误差不受欢迎时,RMSE 更为有用。
偏差
在统计学中,估计量的偏差是该估计量的期望值与被估计参数的真实值之间的差异。一个具有零偏差的估计量被称为无偏的;否则,该估计量被称为有偏的。换句话说,当算法因未能看到准确的潜在关系而始终学习到相同的错误内容时,就会发生偏差。
例如,如果你尝试根据房子的特征预测房价,而你的预测始终比实际价格低$100,000,那么你的模型是有偏的。
规模
在统计学中的假设检验中,“检验规模”指的是检验的显著性水平,通常用希腊字母α(alpha)表示。显著性水平或检验规模是检验统计量必须超过的阈值,以拒绝假设。
它表示在零假设实际上为真的情况下,拒绝零假设的概率,这是一种被称为 I 型错误或假阳性的错误。
例如,如果一个检验的显著性水平设为 5%(α = 0.05),这意味着在零假设为真的情况下,有 5%的风险会拒绝零假设。这个水平 0.05 是α的常见选择,尽管其他水平,如 0.01 或 0.10,也可以根据背景和研究领域使用。
检验规模越小,拒绝零假设所需的证据就越强,从而降低了 I 型错误的可能性,但可能会增加 II 型错误的机会(当零假设为假时未能拒绝它)。I 型错误和 II 型错误之间的平衡是设计任何统计检验时的重要考虑因素。
实证规模
在通过蒙特卡洛模拟进行的假设检验中,实证规模指的是在零假设为真的情况下,零假设在模拟中被错误拒绝的比例。这本质上是 I 型错误率的模拟版本。
下面是一般的操作流程:
1. 设置你的零假设并选择一个显著性水平(例如,α = 0.05)。
2. 在假设零假设为真的情况下生成大量样本。样本的数量通常非常大,如 10,000 或 100,000,以确保结果的稳定性。
对于每个样本,进行假设检验并记录零假设是否被拒绝(你可以将拒绝记录为 1,未拒绝记录为 0)。
4. 计算实证规模,即在零假设被拒绝的模拟比例。这估计了在给定检验程序下,零假设为真的情况下拒绝零假设的概率。
下面的代码显示了如何实现这一点。
import numpy as np
from scipy.stats import ttest_1samp
import random
random.seed(10)
def calculate_empirical_size(num_simulations: int, sample_size: int, true_mean: float, significance_level: float) -> float:
"""
Simulates a set of samples and conducts a hypothesis test on each, then calculates the empirical size.
Parameters:
num_simulations (int): The number of simulations to run.
sample_size (int): The size of each simulated sample.
true_mean (float): The true mean under the null hypothesis.
significance_level (float): The significance level for the hypothesis tests.
Returns:
float: The empirical size, or the proportion of tests where the null hypothesis was rejected.
"""
import numpy as np
from scipy.stats import ttest_1samp
# Initialize counter for null hypothesis rejections
rejections = 0
# Run simulations
np.random.seed(0) # for reproducibility
for _ in range(num_simulations):
sample = np.random.normal(loc=true_mean, scale=1, size=sample_size)
t_stat, p_value = ttest_1samp(sample, popmean=true_mean)
if p_value < significance_level:
rejections += 1
# Calculate empirical size
empirical_size = rejections / num_simulations
return empirical_size
calculate_empirical_size(1000, 1000, 0, 0.05)
对于每个 1000 次模拟,从均值为 0 和标准差为 1 的正态分布中抽取一个大小为 1000 的随机样本。进行一次样本 t 检验,以测试样本均值是否显著不同于真实均值(在这种情况下为 0)。如果测试的 p 值小于显著性水平(0.05),则拒绝原假设。
经验规模计算为原假设被拒绝的次数(假阳性次数)除以模拟的总次数。这个值应该在一个良好校准的测试中接近名义显著性水平。在这种情况下,该函数返回经验规模,给出在实际应用中在假设与模拟条件相同的情况下,你可能错误拒绝原假设的频率。
由于随机变异,经验规模可能无法完全匹配名义显著性水平,但如果样本量足够大且测试假设得到满足,它们应该接近。这种经验规模与名义规模之间的差异就是为什么进行这样的模拟研究,以了解名义规模与实际情况的匹配程度。
A/B 测试估计的统计性质
在本节中,你将学习 DGP 的属性以及协变量在 A/B 测试结果估计中的加入如何影响测试的表现。通过下面的代码,你将模拟有或没有协变量的 DGP 的 A/B 测试,并观察测试的表现如何随着是否在估计中包含协变量而变化。
import numpy as np
import random
from matplotlib import pyplot as plt
from tqdm import tqdm
import statsmodels.api as sm
def fn_variance(data: list, ddof: int = 0) -> float:
"""
Calculate the variance of a given list of data.
Parameters:
data (list): The list of data to calculate the variance for.
ddof (int): Delta Degrees of Freedom. The divisor used in calculations is N - ddof. Default is 0.
Returns:
float: The variance of the data.
"""
n = len(data)
mean = sum(data) / n
return sum((x - mean) ** 2 for x in data) / (n - ddof)
def fn_generate_cov(dim: int, corr: float) -> np.ndarray:
"""
Generate a covariance matrix of a given dimension with a specified correlation.
Parameters:
dim (int): The dimension of the covariance matrix.
corr (float): The correlation value for the off-diagonal entries.
Returns:
np.ndarray: The generated covariance matrix.
"""
acc = []
for i in range(dim):
row = np.ones((1, dim)) * corr
row[0][i] = 1
acc.append(row)
return np.concatenate(acc, axis=0)
def fn_generate_multnorm(nobs: int, corr: float, nvar: int) -> np.ndarray:
"""
Generate a multivariate normal distribution.
Parameters:
nobs (int): The number of observations in the distribution.
corr (float): The correlation coefficient.
nvar (int): The number of variables in the distribution.
Returns:
np.ndarray: The generated multivariate normal distribution.
"""
mu = np.zeros(nvar)
std = (np.abs(np.random.normal(loc = 1, scale = .5,size = (nvar,1))))**(1/2)
# generate random normal distribution
acc = []
for i in range(nvar):
acc.append(np.reshape(np.random.normal(mu[i],std[i],nobs),(nobs,-1)))
normvars = np.concatenate(acc,axis=1)
cov = fn_generate_cov(nvar,corr)
C = np.linalg.cholesky(cov)
Y = np.transpose(np.dot(C,np.transpose(normvars)))
return Y
def fn_randomize_treatment(N: int, p: float = 0.5) -> np.ndarray:
"""
Randomize the treatment assignment for a population.
Parameters:
N (int): The total population size.
p (float): The proportion of the population to be treated. Defaults to 0.5.
Returns:
np.ndarray: A binary array where 1 indicates treatment and 0 indicates control.
"""
treated = random.sample(range(N), round(N*p))
return np.array([(1 if i in treated else 0) for i in range(N)]).reshape([N,1])
def split_columns(X: np.ndarray, a: float, b: float, p0: int) -> np.ndarray:
"""
Splits the input array into two sections based on given percentages.
Parameters:
X (np.ndarray): The input array of size (n, p).
a (float): The percentage of the first p0 columns to keep (between 0 and 1).
b (float): The percentage of the remaining columns to keep (between 0 and 1).
p0 (int): The index up to which to apply the first percentage.
Returns:
np.ndarray: The output array containing a% of the first p0 columns and b% of the remaining columns.
"""
if not (0 <= a <= 1 and 0 <= b <= 1):
raise ValueError("a and b must be between 0 and 1.")
if not (0 <= p0 <= X.shape[1]):
raise ValueError("p0 must be between 0 and number of columns in X.")
first_part = X[:, :p0]
second_part = X[:, p0:]
first_indices = np.random.choice(first_part.shape[1], int(a * first_part.shape[1]), replace=False)
second_indices = np.random.choice(second_part.shape[1], int(b * second_part.shape[1]), replace=False)
return np.concatenate((first_part[:, first_indices], second_part[:, second_indices]), axis=1)
def fn_generate_data(tau: float, N: int, p: int, p0: int, corr: float, flagX: bool = False):
"""
Generate synthetic data for experimentation.
Parameters:
tau (float): Treatment effect.
N (int): Number of observations.
p (int): Number of covariates.
p0 (int): Number of covariates with nonzero coefficients.
corr (float): Correlation for multivariate normal.
flagX (bool): If True, return covariates. Defaults to False.
Returns:
tuple: Depending on flagX, either returns (Yab,T) or (Yab,T,X).
"""
X = fn_generate_multnorm(N,corr,p)
T = fn_randomize_treatment(N) # choose treated units
err = np.random.normal(0,1,[N,1])
beta0 = np.random.normal(5,5,[p,1])
beta0[p0:p] = 0 #set the coefficient of all covariates after p0 to 0
Yab = tau*T+X@beta0+err
if flagX==False:
return (Yab,T)
else:
return (Yab,T,X)
def fn_tauhat_means(Yt: np.ndarray, Yc: np.ndarray) -> tuple:
"""
Calculate the treatment effect estimate and its standard error.
Parameters:
Yt (np.ndarray): Outcome for treated units.
Yc (np.ndarray): Outcome for control units.
Returns:
tuple: The treatment effect estimate and its standard error.
"""
nt = len(Yt)
nc = len(Yc)
tauhat = np.mean(Yt)-np.mean(Yc)
se_tauhat = (np.var(Yt,ddof=1)/nt+np.var(Yc,ddof=1)/nc)**(1/2)
return (tauhat,se_tauhat)
def fn_bias_rmse_size(theta0: float, thetahat: float, se_thetahat: float, cval: float = 1.96) -> tuple:
"""
Calculate bias, RMSE, and test size for the parameter estimate.
Parameters:
theta0 (float): The true parameter value.
thetahat (float): The estimated parameter value.
se_thetahat (float): The standard error of the estimated parameter value.
cval (float): The critical value for the hypothesis test. Defaults to 1.96.
Returns:
tuple: The bias, RMSE, and test size for the parameter estimate.
"""
b = thetahat - theta0
bias = np.mean(b)
rmse = np.sqrt(np.mean(b**2))
tval = b/se_thetahat
size = np.mean(1*(np.abs(tval)>cval))
# note size calculated at true parameter value
return (bias,rmse,size)
def fn_run_experiments(tau: float, Nrange: list, p: int, p0: int, corr: float, flagX: bool = False, a: float = None, b: float = None) -> tuple:
"""
Run experiments by generating synthetic data and estimate treatment effect.
Parameters:
tau (float): Treatment effect.
Nrange (list): Range of number of observations.
p (int): Total number of covariates.
p0 (int): Number of covariates with nonzero coefficients.
a (float, optional): Percentage of the first p0 columns to keep (between 0 and 1). Only used if flagX is True.
b (float, optional): Percentage of the remaining columns to keep (between 0 and 1). Only used if flagX is True.
corr (float): Correlation for multivariate normal.
flagX (bool): If True, return covariates. Defaults to False.
Returns:
tuple: The treatment effect estimates and their standard errors, and 95% confidence interval.
Note:
In the flagX == 2 case, the function uses the split_columns function to select a% of the first p0 columns
and b% of the remaining columns from the X data, before performing the regression and estimating the treatment effect.
"""
n_values = []
tauhats = []
sehats = []
lb = []
ub = []
for N in tqdm(Nrange):
n_values = n_values + [N]
if flagX==False:
Yexp,T = fn_generate_data(tau,N,p,p0,corr,flagX)
Yt = Yexp[np.where(T==1)[0],:]
Yc = Yexp[np.where(T==0)[0],:]
tauhat,se_tauhat = fn_tauhat_means(Yt,Yc)
elif flagX==1:
# use the correct covariates in regression
Yexp,T,X = fn_generate_data(tau,N,p,p0,corr,flagX)
covars = np.concatenate([T,X],axis = 1)
mod = sm.OLS(Yexp,covars)
res = mod.fit()
tauhat = res.params[0]
se_tauhat = res.HC1_se[0]
elif flagX==2:
# use fraction a of the correct covariates and fraction b of the remaining covariates
assert a is not None and b is not None, "Please provide valid 'a' and 'b' when flagX is 2"
Yexp,T,X = fn_generate_data(tau,N,p,p0,corr,flagX)
Xreg = split_columns(X,a,b,p0)
covars = np.concatenate([T,Xreg],axis = 1)
mod = sm.OLS(Yexp,covars)
res = mod.fit()
tauhat = res.params[0]
se_tauhat = res.HC1_se[0]
tauhats = tauhats + [tauhat]
sehats = sehats + [se_tauhat]
lb = lb + [tauhat-1.96*se_tauhat]
ub = ub + [tauhat+1.96*se_tauhat]
return (n_values,tauhats,sehats,lb,ub)
def fn_plot_with_ci(n_values: list, tauhats: list, tau: float, lb: list, ub: list, caption: str):
"""
Plot the treatment effect estimates and their 95% confidence intervals.
Parameters:
n_values (list): List of number of observations.
tauhats (list): List of treatment effect estimates.
tau (float): True treatment effect.
lb (list): List of lower bounds of the confidence intervals.
ub (list): List of upper bounds of the confidence intervals.
caption (str): Title for the plot.
"""
fig = plt.figure(figsize = (10,6))
plt.plot(n_values,tauhats,label = '$\hat{\\tau}$')
plt.xlabel('N')
plt.ylabel('$\hat{\\tau}$')
plt.axhline(y=tau, color='r', linestyle='-',linewidth=1,
label='True $\\tau$={}'.format(tau))
plt.title('{}'.format(caption))
plt.fill_between(n_values, lb, ub,
alpha=0.5, edgecolor='#FF9848', facecolor='#FF9848',label = '95% CI')
plt.legend()
无协变量的 DGP 实验
以下我们模拟数据,这些数据遵循一个 DGP,其中结果仅受处理和随机误差的影响。
y_i = tau*T_i+e_i
如果结果仅受到处理的影响,即使对于相对较小的样本量,处理效应参数的估计也是准确的,并且随着样本量的增加很快就会收敛到真实的参数值。在下面的代码中,参数 tau
的值设置为 2。
tau = 2
corr = .5
p = 10
p0 = 0 # number of covariates used in the DGP
Nrange = range(10,1000,2) # loop over N values
(nvalues,tauhats,sehats,lb,ub) = fn_run_experiments(tau,Nrange,p,p0,corr)
caption = """Estimates of the treatment effect parameter
for a randomized experiment without covariates"""
fn_plot_with_ci(nvalues,tauhats,tau,lb,ub,caption)
对于选定的样本量,验证这与进行包含截距的回归分析是一样的。
你可以通过对结果进行一个截距和处理的 OLS 回归来验证你获得了相同的结果。
N = 100
Yexp,T = fn_generate_data(tau,N,10,0,corr)
Yt = Yexp[np.where(T==1)[0],:]
Yc = Yexp[np.where(T==0)[0],:]
tauhat,se_tauhat = fn_tauhat_means(Yt,Yc)
# n_values = n_values + [N]
# tauhats = tauhats + [tauhat]
lb = lb + [tauhat-1.96*se_tauhat]
ub = ub + [tauhat+1.96*se_tauhat]
print(f"Parameter estimate and stadard error obtained by calculating the difference in means:{tauhat:.5f},{se_tauhat:.5f}")
const = np.ones([N,1])
model = sm.OLS(Yexp,np.concatenate([T,const],axis = 1))
res = model.fit()
print(f"Parameter estimate and stadard error obtained by running an OLS regression with an intercept:{res.params[0]:.5f},{ res.HC1_se[0]:.5f}")
Parameter estimate and stadard error obtained by calculating the difference in means:1.91756,0.21187
Parameter estimate and stadard error obtained by running an OLS regression with an intercept:1.91756,0.21187
运行 R 的蒙特卡洛迭代,计算偏差、均方根误差(RMSE)和规模
现在你将进行蒙特卡洛模拟,通过循环遍历 N
参数的值列表来增加样本量。你将计算每次迭代的测试 RMSE、偏差和经验规模。
这个 Python 脚本进行了一次实验模拟,研究样本量(N
)在不考虑协变量的情况下如何影响 A/B 测试性能的偏差、RMSE 和规模。我们逐步解析如下:
1. estDict = {}
初始化一个空字典以存储实验结果。
2. R=2000
设置实验的重复次数为 2000。
3. for N in [10,50,100,500,1000]
遍历不同的样本大小。
4. 在这个循环中,tauhats=[], sehats=[]
被初始化为空列表,用于存储每次实验的估计处理效应 tauhat
及其对应的标准误差 se_tauhat
。
5. for r in tqdm(range(R)):
遍历 R 次实验,tqdm
提供进度条。
6. Yexp,T = fn_generate_data(tau,N,10,0,corr)
为每次实验生成合成数据,具有预定义的处理效应 tau
、观测数 N
、10 个协变量、没有非零系数的协变量,以及预定义的相关性。
7. Yt=Yexp[np.where(T==1)[0],:]
和 Yc=Yexp[np.where(T==0)[0],;]
将合成数据分成处理组和对照组。
8. tauhat,se_tauhat=fn_tauhat_means(Yt,Yc)
计算处理效应估计值及其标准误差。
9. tauhats=tauhats+[tauhat]
和 sehats=sehats+[se_tauhat]
将处理效应估计值及其标准误差附加到对应的列表中。
10. estDict[N]={‘tauhat':np.array(tauhats).reshape([len(tauahts),1]),’sehat':np.array(sehats).reshape([len(sehats),1])}
将估计值存储在字典中,样本大小作为键。
11. tau0 = tau*np.ones([R,1])
创建一个大小为 R 的数组,所有元素等于真实的处理效应。
12. 对于 estDict
中的每个样本大小,脚本计算并打印使用 fn_bias_rmse_size()
函数的偏差、RMSE 和处理效应估计的大小。
正如预期的那样,随着样本大小的增加,偏差和 RMSE 降低,大小接近真实大小 0.05。
estDict = {}
R = 2000
for N in [10,50,100,500,1000]:
tauhats = []
sehats = []
for r in tqdm(range(R)):
Yexp,T = fn_generate_data(tau,N,10,0,corr)
Yt = Yexp[np.where(T==1)[0],:]
Yc = Yexp[np.where(T==0)[0],:]
tauhat,se_tauhat = fn_tauhat_means(Yt,Yc)
tauhats = tauhats + [tauhat]
sehats = sehats + [se_tauhat]
estDict[N] = {
'tauhat':np.array(tauhats).reshape([len(tauhats),1]),
'sehat':np.array(sehats).reshape([len(sehats),1])
}
tau0 = tau*np.ones([R,1])
for N, results in estDict.items():
(bias,rmse,size) = fn_bias_rmse_size(tau0,results['tauhat'],
results['sehat'])
print(f'N={N}: bias={bias}, RMSE={rmse}, size={size}')
100%|██████████| 2000/2000 [00:00<00:00, 3182.81it/s]
100%|██████████| 2000/2000 [00:00<00:00, 2729.99it/s]
100%|██████████| 2000/2000 [00:00<00:00, 2238.62it/s]
100%|██████████| 2000/2000 [00:04<00:00, 479.67it/s]
100%|██████████| 2000/2000 [02:16<00:00, 14.67it/s]
N=10: bias=0.038139125088721144, RMSE=0.6593256331782233, size=0.084
N=50: bias=0.002694446014687934, RMSE=0.29664599979723183, size=0.0635
N=100: bias=-0.0006785229668018156, RMSE=0.20246779253127453, size=0.0615
N=500: bias=-0.0009696751953095926, RMSE=0.08985542730497854, size=0.062
N=1000: bias=-0.0011137216061364087, RMSE=0.06156258265280801, size=0.047
在 DGP 中进行的协变量实验
接下来,你将向 DGP 添加协变量。现在感兴趣的结果不仅依赖于处理,还依赖于一些其他变量 X
。下面的代码模拟了包含 50 个协变量的 DGP。使用与之前无协变量模拟相同的样本大小和处理效应参数,可以看到这次估计值噪声更大,但仍然趋向于正确的解。
y_i = tau*T_i + beta*x_i + e_i
下图比较了两个 DGP 的估计值——你可以看到,当在 DGP 中引入协变量时,估计值的噪声增加了。
tau = 2
corr = .5
p = 100
p0 = 50 # number of covariates used in the DGP
Nrange = range(10,1000,2) # loop over N values
(nvalues_x,tauhats_x,sehats_x,lb_x,ub_x) = fn_run_experiments(tau,Nrange,p,p0,corr)
caption = """Estimates of the treatment effect parameter
for a randomized experiment with X's in the DGP but no X's included in the estimator"""
fn_plot_with_ci(nvalues_x,tauhats_x,tau,lb_x,ub_x,caption)
# rerun experiment with no covariates
p0 = 0 # number of covariates used in the DGP
Nrange = range(10,1000,2) # loop over N values
(nvalues_x0,tauhats_x0,sehats_x0,lb_x0,ub_x0) = fn_run_experiments(tau,Nrange,p,p0,corr)
fig = plt.figure(figsize = (10,6))
plt.title("""Estimates of the treatment effect parameter
for a randomized experiment with X's in the DGP but no X's included in the estimator, zoom in for large N""")
plt.plot(nvalues_x[400:],tauhats_x[400:],label = '$\hat{\\tau}^{(x)}$')
plt.plot(nvalues_x[400:],tauhats_x0[400:],label = '$\hat{\\tau}$',color = 'green')
plt.legend()
fig = plt.figure(figsize = (10,6))
plt.title("""
Treatment effect estimates from DGP with and without covariates, zoom in for large N
""")
plt.plot(nvalues_x[400:],tauhats_x[400:],label = '$\hat{\\tau}^{(x)}$')
plt.plot(nvalues_x[400:],tauhats_x0[400:],label = '$\hat{\\tau}$',color = 'green')
plt.legend()
100%|██████████| 495/495 [00:41<00:00, 12.06it/s]
100%|██████████| 495/495 [00:42<00:00, 11.70it/s]
重复实验时使用更大的样本大小是否能缓解问题?不一定。尽管样本大小增加,估计值仍然相当嘈杂。
tau = 2
corr = .5
p = 100
p0 = 50 # number of covariates used in the DGP
Nrange = range(1000,50000,10000) # loop over N values
(nvalues_x2,tauhats_x2,sehats_x2,lb_x2,ub_x2) = fn_run_experiments(tau,Nrange,p,p0,corr)
fn_plot_with_ci(nvalues_x2,tauhats_x2,tau,lb_x2,ub_x2,caption)
DGP with X — 在回归中添加协变量
在这一部分,你将使用与之前相同的 DGP:
y_i = tau*T_i + beta*x_i + e_i
现在,你将把这些协变量X
包含在回归模型中。你会发现这显著提高了估计的精度。然而,请记住,这有点像是“作弊”——在这种情况下,你从一开始就包含了正确的协变量。
在现实世界的情境中,你可能不知道哪些协变量是“正确”的,可能需要尝试不同的模型和协变量。
tau = 2
corr = .5
p = 100
p0 = 50 # number of covariates used in the DGP
Nrange = range(100,1000,2) # loop over N values
# we need to start with more observations than p
flagX = 1
(nvalues2,tauhats2,sehats2,lb2,ub2) = fn_run_experiments(tau,Nrange,p,p0,corr,flagX)
caption = """Estimates of the treatment effect parameter
for a randomized experiment with X's in the DGP,
estimates obtained using regression with the right Xs"""
fn_plot_with_ci(nvalues2,tauhats2,tau,lb2,ub2,caption)
如果你使用一些会影响结果的 X 和一些不会影响结果的 X,会发生什么?
本节探讨了在回归模型中包含一些相关的和一些不相关的协变量。这模拟了一个现实世界的情境,在这种情境下,可能不清楚哪些协变量会影响结果。
尽管包含了一些无影响的变量,但可以观察到,与没有包含任何协变量的情况相比,整体估计趋向于改善。然而,包含不相关的变量可能会引入一些噪声和不确定性,使得估计可能不如仅包含相关协变量时那样精确。
总之,理解数据中协变量的影响对于提高 A/B 测试结果的精度和可靠性至关重要。本教程探讨了像 RMSE、偏差和规模这样的估计量的统计特性,并演示了如何通过蒙特卡洛模拟来估计和理解它们。它还强调了在 DGP 和回归模型中包含协变量的影响,突出了在实践中仔细选择模型和假设检验的重要性。
# Use same DGP as before
tau = 2
corr = .5
p = 100
p0 = 50 # number of covariates used in the DGP
a = 0.9
b = 0.1
Nrange = range(100,1000,2) # loop over N values
# we need to start with more observations than p
flagX = 2
(nvalues3,tauhats3,sehats3,lb3,ub3) = fn_run_experiments(tau,Nrange,p,p0,corr,flagX,a,b)
caption = f"""Estimates of the treatment effect parameter
for a randomized experiment with X's in the DGP,
estimates obtained using regression with the {100*a:.1f}% of the correct Xs and {100*b:.1f}% of the irrelevant Xs"""
fn_plot_with_ci(nvalues3,tauhats3,tau,lb3,ub3,caption)
除非另有说明,所有图像均由作者提供。
一种更好的符号回归方法,通过明确考虑单位
通过提供根植于基本概念的更具可解释性的模型,有助于将机器学习与经典科学和工程方法相结合
LucianoSphere (Luciano Abriata, PhD)
·发表于 Towards Data Science ·7 分钟阅读·2023 年 3 月 16 日
–
图由作者通过结合不同的 Dall-E-2 生成图像创建。
符号回归是一种技术,帮助我们理解不同数据之间的关系,通过找到描述这些关系的数学方程。我对符号回归方法充满期待,并且大力支持它们,因为通过提供明确的方程,它们原则上是高度可解释的,以直接的方式 - 这与大多数现代 AI 模型形成对比,后者像黑箱一样运作,我们无法理解其工作原理和原因,从而难以知道它们如何以及为什么有效。
由 Tenachi 等人提出的新工作,现已在 arXiv 上以预印本形式发布,提出了一种新方法,该方法利用深度强化学习来寻找数据集变量的方程,同时考虑数据相关的单位。这种方法有助于消除物理上不可能的解,并通过限制方程生成器的自由度来提高性能。
索引
- 介绍 - 关于符号回归及介绍新方法 - 新符号回归方法的工作原理 - 总结说明、预印本和代码
介绍
自然科学和工程学中的一个基本问题是发现给定系统的自变量和因变量之间的定量关系。虽然可以通过类似黑箱模型的现代常规人工智能方法完美地建模这些关系,但更理想的方式是通过符号方程量化这些关系,比如说 a=F/m 或 I=I0exp(-kt)。
为什么?好吧,有很多原因:
-
方程是可解释的,因此对人类思维来说更易理解,可能甚至通过已经知道的科学或工程概念来连接变量之间的关系。例如,在 a=F/m 中,我们可以直接理解施加更强的力会使物体加速更快,以线性方式表现;而在 I=Ioexp(-kt) 中,我们知道因变量的衰减是随着时间的指数变化,从中我们可以利用数学得到半衰期时间的方程、线性化形式(通过对数)等。
-
如果可以用简单的术语解释,连接变量的方程有很大可能直接与被建模的科学或工程问题的基础概念、想法和公理相关。例如,考虑应用于放射性衰变的 I=I0exp(-kt),从其导数 (dI/dt = -kt) 我们可以理解同位素衰变的速率与样本中剩余的放射性核数成正比,因此这是一个一级过程。
-
通过方程传播自变量(输入)以建模结果变量(因变量)是一种极其快速的计算,几乎是即时的,相比之下,将输入传播通过神经网络的所有单元则更为复杂。这对个别预测可能没有影响,但在需要进行大量预测时可能很重要。此外,通过符号方式关联变量的方程可以通过简单地插入拟合方程来无缝集成到其他程序中。例如,查看一个很好的用例 这里。
-
重要的是,分析地关联变量的方程可能在输入数据采样的领域之外更安全地进行外推。
对于常规的机器学习模型,这些点通常不适用,至少不是以如此直接的方式,因为这些模型通过大量嵌套函数和组合来混合和卷积信号,直到它们能够正确建模因变量,虽然这些模型可能表现得非常好,但解释性往往很差——如果有的话。
上述解释说明了当一个关系可以通过方程建模时,你最好选择这种方法。但当你找不到数学表达式来关联变量时,会发生什么呢?这时你可以尝试常规神经网络,或者尝试符号回归。
关于符号回归,并介绍新方法
符号回归的目标是找到适合给定数据集变量的自由形式符号解析函数,这比仅仅在线性或非线性函数中拟合系数的方法要更为通用。简单来说,符号回归不仅是拟合方程,更是找到它们的表达式(然后是拟合其中的系数)。从介绍中总结但用不同术语表述,符号回归的优势包括紧凑性、泛化能力(这意味着如果正确,解析表达式在训练范围外的外推能力会更强)以及可理解性和可解释性。
为了明确这些优势,并理解符号回归在建模数据集中的作用,请查看我从近期文献中涉及的符号回归在科学应用中的最新进展和示例:
比起常规神经网络,这种方法较少像黑箱,提供的模型不仅预测数据,还能进行合理化…
该新方法以符号形式推导准确的泛函(量子力学计算的元素),因此…
不幸的是,实现符号回归以发现新的物理定律是极具挑战性的。更难的是获得紧凑且简单的方程,这些方程可以真正用基础科学或工程概念进行解释。我强调这一点是因为许多符号回归模型提出的方程非常复杂,以至于很难解释,并且与标准机器学习模型相比几乎没有或根本没有贡献。而在最成功的案例中,找到足够简单的方程通常需要很长的执行时间,因为程序需要探索可能的数学操作的巨大树的不同分支,这些操作必须组合并测试以构建可能的方程。
最后这两点,即对简化方程的需求和可能快速收敛到这些方程,是 Tenachi 等人新工作的主要动机。以下是他们的主要贡献:作者意识到,通过符号回归程序连接的变量的单位对方程的形状施加了强有力的约束。他们探索了如何利用这一事实来优化方程搜索,从而提出了一种将物理单位信息纳入符号回归程序的具体框架。
作者意识到,通过符号回归程序连接的变量的单位对他们寻找的方程的形状施加了强有力的约束。他们利用这一点来优化方程搜索,并提出了一种将物理单位信息纳入符号回归程序的框架。
通过在方程搜索过程中将单位作为约束,新框架有效地解决了试验表达式的巨大搜索空间的组合挑战。搜索空间显著缩小,结果是方程搜索速度更快。此外,作者发现该程序生成的表达式更简单,因此比其他符号回归方法获得的表达式更易解释且更准确。
新的符号回归方法如何工作
其核心是框架包括一种新颖的符号嵌入,这里针对物理学进行了调整,但原则上可以更广泛地扩展,这使得能够控制在部分组合的数学表达式中生成的每个符号的单位。这使得程序能够自动将搜索空间引导到单位保持一致的路径上。当程序运行时,它使用递归神经网络生成解析表达式,并在单位约束下通过强化学习的步骤进行循环,从而得到物理上有意义的输入变量组合。
更详细地说,该过程首先通过使用二叉树生成符号表达式,其中每个节点表示库中可用符号的一个符号。表达式被视为类别向量的序列,并使用递归神经网络生成标记序列。(顺便说一下,这些类别向量可以被人为调整以纳入先验知识;在这一阶段采用了一些先验,例如限制分析表达式的最大可能大小,允许不超过两级的嵌套三角函数操作,并且不允许指数和对数运算符的自我嵌套,这在科学中是不寻常的,等等。)
生成的表达式受到物理单位约束,这些约束通过一种程序进行计算,该程序在可能的情况下计算所需的单位,否则将其留作自由。然后,在强化学习部分,生成一组试验符号函数,并通过与数据对比计算每个函数的奖励。网络随后需要生成一批新的试验函数,通过强化与高奖励值相关的行为来鼓励生成更好的结果。这种方法强化基于不仅是强化网络的输出,还基于从先验分布得出的局部单位约束的候选项,这确保了标记选择的物理正确性。
值得注意的是,该方法允许候选函数包含具有固定单位的常数,但数值自由,从而可以模拟问题中存在一些未知物理尺度的情况。最后,常数的最佳值通过使用标准优化程序的梯度下降找到。
结论、预印本和代码
在展示了新方法在一系列天体物理学示例中的有效性后,这项新工作的作者旨在为其他物理科学构建一个强大的通用符号回归算法。
你可以在 arXiv 上阅读完整的预印本:
符号回归是研究自动化搜索与数据拟合的解析表达式的算法。虽然…
arxiv.org](https://arxiv.org/abs/2303.03192?source=post_page-----35b3630165b--------------------------------)
你可以在这里尝试该程序:
[## GitHub - WassimTenachi/PhySO: 物理符号优化
物理符号回归( Φ \Phi Φ-SO)包 physo 是一个完全利用…
你还可能会觉得预印本的首席作者的演讲很有用,以及他在推特上发布的讨论:
正如 Tenachi 在他的推特讨论中总结的那样,
虽然神经网络是建模物理系统的出色工具,但它们缺乏可解释性和泛化能力。[新方法] 提供了打开这些黑箱并恢复基本方程的机会,作为物理学家,我们都知道并喜爱这些方程。
www.lucianoabriata.com 我写作和拍摄的内容涵盖了我广泛兴趣领域中的一切:自然、科学、技术、编程等。 成为 Medium 会员 以访问所有故事(平台的关联链接,为我带来少量收入,但对你没有费用),并且 订阅获取我的新故事 通过电子邮件*。要* 咨询关于小工作, 请查看我的 服务页面在这里。你也可以 在这里联系我。
更好的分析功能发布影响的方法
或者——为什么简单的“前后”比较可能导致糟糕的产品决策
·
关注 发表在 Towards Data Science ·5 分钟阅读·2023 年 5 月 23 日
–
作者提供的照片 — 使用 DALL-E 2
A/B 测试是估计产品分析中因果效应的金标准。但在许多情况下,它们并不可行。最常见的情况之一是功能发布。
在这篇文章中,我将讨论使用简单的“前后”比较来衡量功能发布影响的常见做法,以及这些分析中常见的偏差。我还会提供一些建议,说明如何减轻这些偏差。
一些背景信息
很多时候,公司在发布新产品功能或应用版本时,未进行 A/B 测试来评估其对主要 KPI 的影响。这可能由于各种原因,如流量较低或技术复杂性较高。
在特定日期将功能部署给所有用户后,产品经理通常会通过简单的“前后”分析来评估功能发布的影响:比较发布后短时间内的 KPI 与发布前相同时间段的 KPI。
尽管直观,但这种幼稚的比较可能忽略重要的偏差来源。
以下我将讨论简单的前后分析中最常见的两个偏差来源及其如何导致错误结论。
偏差 1:时间效应
一个常见的情况是产品经理进行“前后”分析并获得积极的结果。
然而,查看 KPI 随时间变化的图表时,他们可能会遇到令人失望的结论:
作者提供的照片
无论发布如何,KPI 在整个时期内都呈上升趋势,而发布本身似乎有负面影响。简单的“前后”比较假设没有时间动态,这可能是非常错误的,就像上面所示的情况一样。
偏差 2:业务组合的变化
尽管时间效应引入的偏差可能非常明显,但其他的可能更为微妙。
在另一种情况下,产品经理可能会测得负面的“前后”发布影响。绘制 KPI 随时间变化的图表似乎没有提供替代结论:
作者提供的照片
许多公司会在这里停下,假设发布效果不好,需要回滚。
然而,在许多情况下,发布前后期间的差异可能是由于用户组合的变化。这可能是偶然发生的,但通常与伴随功能发布的营销活动有关。
为了使例子具体化,可能是在发布后的某一时期,Android 用户的比例显著上升,相比于发布前的时期。
作者提供的照片
在这个具体的例子中,那些 Android 用户的转化率往往低于 iOS 用户,但这些组内的发布效果实际上是积极的:
作者提供的照片
因此,考虑到设备因素,发布的影响实际上是积极的。总体差异与组内差异相反的情况是辛普森悖论的经典例子。
这是否意味着我们不能没有 A/B 测试?
上述情况相对简单。时间效应可能包括复杂的趋势和每日季节性变化,细分比例变化可能更微妙,并且分布在许多子集中。
可能会给人一种分析功能发布数据毫无意义的印象。然而,我认为情况不一定是这样。
引入发布影响算法
在Loops工作时,我设计了一个算法来自动和透明地处理上述偏差。由于业务和知识产权原因,我无法分享完整的实现细节,但以下是一个总体概述:
-
使用 ML 算法找到在发布前后期间中比例变化最大的细分市场。
-
在每个细分市场中,分别建模时间趋势和季节性以及发布影响。
-
对所有细分市场内估计的发布影响进行加权平均,以得出最终的影响估计。
测试算法的有效性
你无法确定任何方法是否适用于特定的数据集。然而,你可以通过使用过去的 A/B 测试来获得粗略估计。
例如,执行了一个包含控制组和处理组的 A/B 测试。比较这两个组之间的平均 KPI 可以得到对处理影响的无偏估计。这作为我们的“金标准”。
我们将测试前的用户段命名为“前控制”。将前控制人群与处理人群进行比较类似于我们在前后分析中所做的比较。
使用多种不同的测试,我们可以将“金标准”估计与“前后”估计进行比较,看看它们的接近程度。
在Loops工作时,我可以访问来自数十个客户使用我们系统的数百个 A/B 测试。使用上述基准方法,我们发现该算法的准确性远远优于简单的“前后”比较。
总结
我希望到此时读者已经意识到使用简单的“前后”比较方法的风险,并且上述算法将为任何希望更好地评估功能发布影响的人提供基础。
这篇文章的修改版已于 https://getloops.ai 在 2023 年 5 月 11 日发布。
一种更好的在没有数据的情况下获得结果的方法
原文:
towardsdatascience.com/a-better-way-to-get-results-without-data-3d75cd93c424
想象一下有一个数据集,并希望看到零而不是空单元格。这听起来简单?实际上,这个案例有一些陷阱。让我们来深入研究一下。
·发表于Towards Data Science ·5 分钟阅读·2023 年 2 月 27 日
–
图片由Ramón Salinero拍摄,来源于Unsplash
介绍
几周前,我已经写过一篇关于这个主题的文章:
## 如何在 Power BI 中在没有数据的情况下显示结果
这个标题似乎不直观。为什么在没有数据的情况下我还要要求结果?让我们看看请求…
towardsdatascience.com
在那篇文章中,我使用 Power Query 生成了额外的数据行来填充数据表并获得所需的结果。
现在我找到了一种更简单的方法来解决这个挑战。
首先,我将解释这个挑战。如果你知道我上面链接的文章,以下部分描述了我如何找到新的方法。然后我将向你展示新的解决方案。
挑战
想象一个报告,其中包含显示多个任务及其相关成本和每个任务状态的表格:
图 1 — 起始点的表格(图由作者提供)
现在,想象一个类似的表格。但现在在“失败”状态中没有任务:
图 2 — 缺失状态的表格(图由作者提供)
现在,我的客户希望看到零而不是空单元格,并且他希望看到所有可能的状态,即使没有任务具有特定状态,如上图所示。
问题是,如果数据表中没有值,则不会执行任何计算,这意味着不可能显示零而不是空单元格。
新方法的灵感
几天前,我的一位客户问我关于他的 Power BI 报告的问题。
请理解,我只能在这里展示一些细节。
总而言之:他计算了一个百分比,并从 1 中扣除了结果金额。
这个度量看起来是这样的:
Measure Name = 1 - ( CALCULATE(SUM(column1)
,[Column2] = "Filter Text"
)
/ 12345 )
结果是,他总是得到 100 %,而期望得到一个空结果。
经过一些测试,我意识到“1 –”部分是罪魁祸首。
当 CALCULATE() 函数返回一个值时,结果会被正确计算。
如果没有 CALCULATE() 的结果,引擎计算 1–0 = 1,即 100 %
Power BI 将值 1 视为常量,并且没有应用任何过滤器。
结果是,当表中没有数据时,Power BI 返回了 100 %。
我必须添加一个 IF() 来解决这个问题,检查 CALCULATE() 函数是否返回了一个值:
Measure Name =
VAR Result = CALCULATE(SUM(column1)
,[Column2] = "Text"
)
RETURN
IF (NOT ISBLANK(Result)
, 1 - ( Result / 12345 )
)
现在,只有当 CALCULATE() 返回一个值时,度量才返回一个值。
反转想法
从中产生了一个想法:将这个“问题”反转并转化为解决方案。
如果我能向我的解决方案中添加一个度量,并计算 0 + SUM(‘table’[column]) 呢?
在这种情况下,如果 SUM 没有返回任何结果,我将得到 0。而当 SUM() 返回一个值时,添加 0 不会改变结果。
但等等,这里有个问题:当没有状态为“失败”的行时,我怎么能显示任何结果?
为了解决这个问题,我必须稍微改变我的数据模型。
最初,我的数据模型只包含包含值的表。
为了强制计算,我需要一个包含所有状态的表,并将其用作维度,以便为表中的每一行强制设置过滤上下文:
图 3 — 状态表(作者绘制)
然后,我必须向我的演示数据表中添加一个关系:
图 4 — 包含两个表的数据模型(作者绘制)
现在我更改了我的报告,从状态表中获取状态列,并添加了一个新的度量:
Costs = 0 + SUM('Demo Data 2'[Cost])
下面,你可以看到原始表的数据(包括所有状态的数据),以及使用新解决方案的没有状态失败的表的数据:
图 5 — 比较两个结果(作者绘制)
这是我客户需要的结果。
结论
数据集是虚构的,像之前的文章一样,在 Excel 中从头开始创建。
虽然我的第一个解决方案是建立在数据表是我所拥有的一切的假设上,但现在我必须创建一个新的状态表,并确保这个表中包含所有可能的状态。
你可能可以从数据源中提取这些信息。
如果没有,你可以使用我基于 Power Query 的旧解决方案,或者使用 Power BI 的“输入数据”功能创建一个表。
无论你如何添加额外的表格,你都需要生成一个过滤上下文,以便度量值可以始终返回一个值。
照片由 Bill Jelen 提供,来源于 Unsplash
如果你对 Power Query 不熟悉,可以阅读我之前的文章以了解更多关于这个强大工具的信息。
在某个时刻,这可能会节省你一天的时间。
[## 通过我的推荐链接加入 Medium - Salvatore Cagliari
阅读 Salvatore Cagliari 的每一篇故事(以及 Medium 上成千上万其他作者的故事)。你的会员费用将直接……
medium.com](https://medium.com/@salvatorecagliari/membership?source=post_page-----3d75cd93c424--------------------------------)
线性代数的全景视角:方程组、线性回归和神经网络
谦逊的矩阵乘法及其逆几乎是许多简单机器学习模型中的核心内容
·发表于 Towards Data Science ·18 分钟阅读·2023 年 12 月 28 日
–
图片由 midjourney 提供
这是进行中的线性代数书籍《线性代数的全景视角》的第四章。到目前为止的目录:
-
第二章:映射的度量——行列式
-
第三章:为什么矩阵乘法是这样?
-
第四章(当前):方程组、线性回归和神经网络
-
第五章:秩和零化以及为什么行秩等于列秩
本博客中的所有图片,除非另有说明,均由作者提供。
I) 引言
现代人工智能模型利用高维向量空间来编码信息。并且我们 用于推理高维空间及其映射的工具是线性代数。
在这一领域内,矩阵乘法(以及它的逆)几乎是构建许多简单机器学习模型所需的全部内容。这也是为什么花时间深入理解它是一个很好的投资。这就是我们在第三章中所做的。
这些简单模型,虽然在自身之内有用,却构成了更复杂的机器学习和人工智能模型的基础,这些复杂模型具备最先进的性能。
在这一章中,我们将介绍一些这些应用(从线性回归到初级神经网络)。
但首先,我们需要转到最简单的情况和最简单的模型——当数据点的数量等于模型参数的数量时,即求解线性方程组的情况。
II) 线性方程组
我们终于来到了线性代数的核心(在本书的背景下)。求解线性方程组是我们最初发现线性代数的方式,这个领域的大多数概念的动机在这个应用中有着深远的根基。
让我们从简单的一维情况开始。除法的概念根植于一维线性方程中。
ax = b
这个方程的意思是,“哪个数字在乘以a后得到b”。解决方案定义了标量除法:
x = b/a _(1)
这是在一维的情况下,即x。当进入多维时,最有趣的事情发生了。所以我们不仅有一个变量x,还有n个变量(x_1, x_2, x_3, …, x_n)。
一旦进入多维空间,线性代数就会出现。
当问题变得多维时,线性代数就会出现。图像来自 midjourney。
现在,我们的方程应包括n个变量,即x_1, x_2, x_3, …, x_n。就像x之前有系数a一样,这次每个x_i都有自己的系数a_i。
但与一维情况不同,这个方程不足以求解x,因为它不能唯一地确定x。例如,我们可以随意选择x_2, x_3,…, x_n(例如:全部为 0),只有这样我们才能得到唯一的x_1值。如果我们已经知道了x_1, x_2, …, x_n的值,并且想以方程组的形式传达它们,它们会看起来像这样:
因此,我们需要n个方程(等于变量的数量)。现在可以通过“混合”上述方程来创建任何一般系统,即取它们的线性组合。这允许添加任意两个方程,并将任意方程乘以任意标量。这些操作显然不会改变系统的解(或其存在性问题)。
例如,我们可以将第二个方程乘以三加到第一个方程中,得到:
然后我们可以用这个方程替换第一个方程(x_1=4.6),得到的方程组仍然有相同的解(因为我们可以通过从新的第一个方程中减去三倍的第二个方程来撤销我们所做的更改)。如果我们以这种方式非常彻底地“混合”,随机替换其中一个方程,并重复多次,我们将得到n个方程,每个方程涉及所有n个变量的倍数。
系数 a_{ij} 看起来非常像一个方阵的元素。确实,根据我们在第三章中学到的矩阵向量乘法(见 III-A 节中的动画-1),我们可以将方程组表示为:
方程(1)线性方程组的矩阵形式。
就像在方程(1)中我们取了乘法逆来得到标量变量 x (x=b/a) 一样,这里我们取矩阵乘法的逆来得到向量 x。
我们已经达到了一切开始的标志性方程。线性代数的起点。线性方程组。计算逆矩阵及其相关操作有一个完整的科学背景,我们将在后续章节中讨论。
注意,A 矩阵的行数是系统中的方程数,列数是变量数。
从几何上讲,每个方程在空间中都是一个维度降低的超平面。考虑以下方程组:
这个方程组的解是*(x=0, y=0, z=1)*。由于这个方程组中有三个变量(x, y 和 z),因此矢量空间是三维的,如下图-1 所示。请参阅该图以获取后续讨论。
x=0 方程对应于这个三维空间中的黄色超平面。由于该方程表示增加一个约束,满足该方程的空间的维度降低一个(3–1=2)。类似地,y=0 方程对应于蓝色超平面,也二维。现在我们有两个方程,因此两个约束。两个方程都满足的子空间的维度将降低两个,变成3–2=1维。一维子空间就是一条直线,实际上我们得到的是绿色直线图-1。最后,当我们加入第三个方程 x+y+z=1,它由粉色平面表示。现在我们有三个方程/约束。它们将三维空间的维度限制为 3。因此,我们剩下的维度是3–3=0(即一个点),确实,我们得到红色点,它是同时满足系统中所有三个方程的唯一矢量空间元素。
图-1:三变量的方程组。空间是三维的。每个方程切割出一个低一维的超平面。两个方程的组合去除两个维度,依此类推。图像由作者提供。
在上面的例子中,我们有 n 个方程和 n 个变量。一般来说,它们不一定相同。假设有 n 个方程和 m 个变量。从上面的图可以清楚地看出:
-
当n<m时,我们有更多的变量而不是方程,系统具有无限解。可以把方程看作是要求。要求指定得不够多,可能的解就会很多。
-
当n>m时,我们将有更多的方程而不是变量。现在有了太多的约束条件,无法在向量空间中找到满足所有约束的点。
-
所以当n=m时,我们应该总是有一个唯一解?不完全是,还有一些情况可能会出错(所有三种情况)。
II-A) 问题发生的原因
一致性
使任何系统没有解的一个因素是矛盾。例如,第一个方程是2x+y=3,第二个方程是 2x+y = 5。现在,无论我们再添加多少个方程,都无法同时满足这两个方程(它们相互矛盾)。任何包含这两个方程的系统都是不一致的。
依赖性
接下来,系统可能会欺骗我们,看起来好像有比实际更多的方程式。最明显的情况是如果我们简单地复制其中一个方程。这将使系统增加一个方程,使得技术上我们现在有了m+1个方程。然而,很明显,我们并没有真正增加任何新信息。我们新增的方程是多余的。这被称为依赖系统,因为我们的一些方程没有带来任何新的信息,而是“依赖”于其他方程。如果我们直接抄袭一个方程,那将很容易被发现。但可以以更隐蔽的方式做到这一点。我们可以通过对几个方程进行线性组合来创建一个新的方程。线性组合可以做到几乎不被发现混入了一个“冒牌方程”。当然,一旦这样的依赖方程被加入到系统中,就很难分辨哪个是“冒牌”方程,就像下面的三只蜘蛛侠一样。
两个鲍勃。但只能有一个。另一个是冒牌的。很难判断哪一个。就像通过对其他方程进行线性组合引入的依赖方程一样。图像来源:midjourney。
一致性和依赖性这两个问题在A矩阵(来自方程(1))方面表现相同。向量b然后决定系统是否不一致(没有解)或依赖(无限多个解)。发生的情况是,A的某些行变成了其他行的线性组合。
II-B) 数据分析的情况
对我们最感兴趣的情况是建立在数据之上的模型。如果你有独立收集的数据点,并且数据收集过程是随机的(就像数据收集应该是的那样),几乎可以肯定你的数据矩阵的行不会线性相关(在概率术语中,“几乎肯定”)。所以,我们不必担心得到一个不一致或相关的系统。
此外,我们通常有比列数(变量/特征)多得多的行(数据点)。所以,矩阵将是“瘦长的”,其中 n>m。
一个高而瘦的矩形数据矩阵,行数多于列数。图片由 midjourney 提供。
我们正处于无解方程组的领域。如果我们选择任何 m 个 n 个方程并删除/忽略其余的方程,那么我们将回到 m=m 的情况,这时方程数和变量数相等,现在将会有一个唯一的解。
因此,超平面对应于系统中方程的总数是 (n 选择 m) 个点。这些点中没有一个点会同时位于所有超平面上。
尽管没有一个点同时满足所有方程,我们仍然可以问:“在整个向量空间中哪个点最接近满足所有方程”。这就是线性回归的作用所在。
III) 线性回归
线性回归绘制一个最接近你数据点的线性超平面。就像忍者尝试以一种尽可能接近最多叶子的方式挥动他的剑一样。图片由 midjourney 提供。
首先,在线性回归的背景下,矩阵和向量的命名与方程组有所不同。现在,系数矩阵 A 变成了包含数据的矩阵,我们称之为 X。这个矩阵的每一行,即行向量 x_i,是一个数据条目。向量 x 包含了方程 (1) 中的未知变量。现在,未知变量是线性回归系数,我们用 𝛽 表示。最后,线性系统的右边向量 b 变成了包含因变量的向量,我们称之为 y。因此,在线性回归的背景下,方程 (1) 变成了:
Eq (2):线性回归的基本方程。图片由作者提供。
展开后,这个方程看起来如下:
注意第一列的1。这对应于常数项。例如,在单变量的情况下,如果没有那一列,我们的模型将会是:y = m.x. 这将排除像 y=x+1 这样的直线。如果要考虑像 y = mx+c (我们在大多数情况下确实需要)的直线,我们需要那一列1。
就像线性方程组一样,我们希望对两边进行X的乘法逆运算,找到𝛽。如图所示:
这个方程没有任何意义。矩阵 X 是矩形的,所以它不能被逆转。
不幸的是,这没有意义。只有方阵才可以逆转。矩形矩阵则不行。我们的数据矩阵 X 是一个矩形矩阵,行数(n)大于列数(m)。
一种理解这一点的方法是,它表示一个方程数量多于变量数量的线性系统,因此第二部分的论点适用。就矩阵背后的线性映射而言,对于一个“瘦长”的矩阵(如 X 所示),行数多于列数,它不会是一个一对一的映射(同一空间中的多个点会映射到第二个空间中的同一点,这种情况是不允许的)。
是否有一种“技巧”可以让乘以 𝛽 的矩阵变成方阵?矩阵 X 目前是 n⨉m。如果我们乘以另一个 m⨉n 的矩阵 U,那么得到的矩阵 V 将是方阵 m⨉m。
通过对方程(2)进行 U 的左乘,我们可以逆转结果矩阵并得到 𝛽。
现在,问题是,我们从哪里得到这个 U 矩阵(m⨉n)?我们拥有的是 X 矩阵,n⨉m(即数据本身)。我们可以做的一件事是将 X 矩阵翻转,使其行变成列,列变成行。这种对矩阵的操作称为转置,记作 X^T,如下图所示。
矩阵的转置。图片来源:维基百科 关于转置的文章。
因此,我们可以用 X 的转置来替代 U。这将得到:
方程(3):线性回归系数。
我们现在已经找到了回归模型的系数。如果我们得到一个新的数据点,x_new(以行向量的形式),我们可以将其与𝛽进行点积,从而获得相应的y。
其中 𝛽 由上述方程(3)给出。现在,我们提供了用X^T替代U的动机,因为这是一个显而易见的选择,符合我们所需的维度。然而,相同的公式还有更强的数学动机。
III-A) 数学动机
我们需要为𝛽(方程(3)中的那个)提供一个具体的值。因此,让我们思考不同𝛽值的情况。如动画-1、第三章 III-A 节(矩阵乘法)中所述,我们可以将 X𝛽 理解为将 X 的列向量拆开并通过线性组合将它们组合成一个单一的列向量。更具体地说,我们将 X 的第一个列向量乘以𝛽向量的第一个元素,将第二个列向量乘以第二个元素,以此类推,然后将所有结果相加。因此,当我们改变𝛽向量时,我们就在探索矩阵 X 的“列空间”,即通过 X 的列向量的线性组合可以得到的所有向量的集合。然后,我们还有向量 y,这是我们方程(2)的右侧。就像 X 的列向量一样,这个向量的维度也是 n(数据点的数量/矩阵中的行数)。
这个向量空间(包含 y 和 X 的列向量)的维度是 n。另一方面,X 的列向量的数量是 m。请记住,我们的情况是 n>>m。所以,X 的列空间(由这些 m 个向量张成的空间)的维度是 m。这比 n 要低得多,而 n 是那些列向量所在的更大空间。
向量 y 生活在与 n 维度相同的更大空间中。由于列向量的数量 m 比 n 小得多,因此向量 y 几乎肯定不会在由 X 的列向量所张成的列空间中。
如果是这样,我们会有一个𝛽可以完全满足所有方程。这是不可能做到的(如前所述)。但是,我们仍然希望找到一个𝛽,使其尽可能接近向量 y。为此,我们需要最小化 y 和 X𝛽之间的距离。
让我们来看一个具体的例子。假设我们有一个数据集,其中 n=3 个数据点。在我们的回归模型中,我们选择 m=2 个特征。方程 X𝛽=y 看起来是这样的:
例如,矩阵 X 有两个列向量,[1,1,1] 和 [2,5,7]。它们存在于一个三维空间中(n=3)。这两个列向量在下面的图 2 中用蓝色绘制。由这些向量(列空间)张成的空间是二维的(m=2),其中一部分被粉色阴影覆盖。
图 2:展示线性回归作为数据矩阵 X 的列空间探索。图片由作者提供。
现在,看看黑色向量、灰色向量和红色向量形成的三角形。黑色向量是 X𝛽,红色向量是 y,灰色向量是 d。这三者形成一个三角形,因此满足:
我们希望找到这个 d 向量在 X 的列空间(由 𝛽 控制)中最接近 y 的点。
我们通过将列向量的转置与其自身进行矩阵乘法来获得列向量的平方长度。
最后,让我们对 𝛽 取导数并设置为 0。这将给出最小化向量 d 的平方长度的 𝛽。
在这里,我们使用来自矩阵宝典的方程(78),[2]。这导致:
这与方程(3)是相同的。
我们在这里用 X 的列空间来激励(如[1]中更详细地解释)。相同的方程也可以被激励为最小化预测和实际值向量 y 中的平方误差项。这种方法在[2]中涵盖。
III-B) 在线线性回归
在线线性回归。数据以恒定的流入方式每天进入。我们需要在这个过程中保持我们的线性回归模型的最新状态。图像来源于 midjourney。
到目前为止,我们把线性回归看作一个静态模型,其中数据矩阵 X 和对应的响应向量 y 已经给定。很常见的是,数据会随着时间作为一个恒定的流入。一些数据点今天可能会丢失,明天可能会更多,以此类推。我们希望我们的模型使用一个滚动窗口的数据(比如 30 天),并且参数每天更新一次,针对过去 30 天的数据。显而易见的方法是每天使用过去 30 天的数据应用方程(3),并每天刷新参数。但如果数据点的数量 n(矩阵 X 的行数)非常大呢?这会使方程(3)的计算变得非常昂贵。如果你考虑昨天的模型与今天的模型。由于滚动窗口,大部分数据都是相同的。第 31 天,我们使用了第 1 天到第 30 天的数据,而第 32 天,我们使用了第 2 天到第 31 天的数据。第 2 天到第 30 天的数据是共同的。第 32 天的模型与第 31 天的模型不同之处在于它不考虑第 1 天丢失的所有数据点,但考虑了第 31 天丢失的数据点。除此之外,绝大多数数据(第 2 天到第 29 天)是共同的。因此,忽略这一点并每天从头开始训练整个模型似乎是浪费的。如果我们可以将方程(3)重新表述为对 X 的行 x_i 的某些函数的求和,我们可以在新数据到来时不断增加贡献,同时减去掉出滚动窗口的旧数据的贡献。
在第三章中我们讨论的矩阵乘法的众多解释之一可以帮助我们做到这一点。在第三章 的 III-B 节(大约动画 5)中,介绍了将矩阵乘法解释为两个矩阵行的外积之和。利用这一点,我们可以将方程 (3) 左侧的矩阵平方为:
方程 (4) 图片由作者提供
在这里,向量 x_i 是矩阵 X 的第 i 行。它有一行和 m 列 (1⨉m)。
同样,那个方程左侧的向量可以写成:
方程 (5) 图片由作者提供
由于这两个项已经以 n 个数据点的总和表示,我们可以随意地添加或删除来自单个数据点的贡献。我们可以基于过去 7 天内的数据点,保持方程 (4) 中的矩阵和方程 (5) 中的向量的不断更新。如果某个数据点超出了 30 天的窗口,我们可以从两个方程中减去它对应的项,如果有新的数据点进入,我们可以将其项添加到两个方程中。而且由于 m 很小,我们可以高效地计算 𝛽,每次更新这些项时保持其最新。这种方法也可以用来为数据点添加权重。如果(例如)你从多个来源获取数据并希望对其中一些进行加权,这将非常有用。你只需将第 i 个数据点的权重 w_i 乘到方程 (4) 和 (5) 的每一项中。
IV) 神经网络
神经网络架构由向量层组成。第一层是输入层,最后一层是输出层,中间的所有层都是隐藏层。图片由 Midjourney 提供。
神经网络是受生物大脑神经连接启发的机器学习模型。它们是我们追求人工通用智能的当前选择武器。所有近期的进展,从文本到图像再到对话机器人,都使用了以神经网络为核心的模型。
线性回归可以被看作是最简单的神经网络。现在我们关注推断,即在给定模型输入实例的情况下获得输出向量的过程。对于线性回归,我们将获得一个行向量 x 作为输入和一个单一的标量值 y 作为输出。参数向量 𝛽 将输入转换为输出。
在线性回归中,我们得到一个对应于新数据点 x_j 的向量。这与参数向量 beta 相乘以产生响应 y_j,它是一个单一的标量。图片由作者提供。
要将其扩展到神经网络,我们以两种方式进行概括。
首先,为了使模型能够输出各种有趣的内容,如图像、句子、视频和宇宙飞船,我们需要它输出的不仅仅是一个标量(如线性回归),而是一个向量。如果这个向量的维度足够高,我们期望的任何复杂响应都可以有效地嵌入其相应的向量空间中。
因此,我们需要将线性回归的向量->标量情况改为向量->向量。标量输出只是一个特例,因为它是一个一维向量。这将上述图像更改为:
一般来说,我们希望响应 y
是一个向量而不是一个标量。这样,我们可以在那个向量空间中嵌入关于现实世界的复杂信息。图片由作者提供。
现在,之前的参数向量 𝛽 将需要变成参数矩阵。这是一个线性映射,将输入向量映射到输出向量。
但是,现实世界中大多数有趣的关系都是非线性的。为了适应这一点,我们首先插入了一堆中间的“隐藏层”,如下图中的蓝色部分所示。
深度神经网络的结构。图中的蓝色隐藏层是模型的中间层。图片由作者提供。
以这种方式添加层本身不会改变任何东西。它仍然等同于没有任何隐藏层的原始模型。要看到这一点,请注意,下面图中的参数矩阵 B_1, B_2, B_3, … 只是相乘并变成另一个参数矩阵。
B = (B_1. B_2. B_3.B_4)
但这个简单的技巧使得这种方法从只能任意逼近线性映射变成可以任意逼近任何映射。为了使隐藏层值得我们费心,我们在每一层添加一个简单的逐元素非线性函数 f。这是一个简单的一维函数 f,它接受一个标量作为输入并返回一个标量作为输出。我们只需在乘以下一个参数矩阵 B_j 之前,将 f 应用于向量的每个元素。这个 f 的一个流行选择是 Sigmoid 函数。
通用逼近定理 [4] 说,这种架构可以任意逼近两个向量空间之间的任何映射(线性或非线性)。
V) 结论
谦逊的矩阵乘法是一种极其强大的工具。你会在最简单的模型以及最复杂、最前沿的模型中找到它。在本章中,我们回顾了一些以矩阵乘法为核心引擎的简单模型。在本书的后续章节中,我们将探索更多线性代数概念,突出它们在现代 AI 模型中的作用。
如果你喜欢这篇文章,给我买杯咖啡吧 😃 www.buymeacoffee.com/w045tn0iqw
参考文献
[1] 看待线性代数的美妙方式: medium.com/towards-data-science/a-beautiful-way-of-looking-at-linear-regressions-a4df174cdce
[2] 从最小二乘法推导线性回归: stats.stackexchange.com/questions/46151/how-to-derive-the-least-square-estimator-for-multiple-linear-regression
[3] 矩阵宝典: www.math.uwaterloo.ca/~hwolkowi/matrixcookbook.pdf
[4] 通用逼近定理: en.wikipedia.org/wiki/Universal_approximation_theorem
线性代数的鸟瞰图:基础知识
原文:
towardsdatascience.com/a-birds-eye-view-of-linear-algebra-the-basics-29ad2122d98f
我们在思想上摆脱基础,但当真正需要时,我们会关上办公室的门,疯狂地计算矩阵。
·发布于 Towards Data Science ·12 分钟阅读·2023 年 8 月 27 日
–
线性代数的鸟瞰图。图像由 Midjourney 创建
这是正在进行中的线性代数书籍《线性代数的鸟瞰图》的第一章。目前的目录如下:
-
第一章:(当前)基础知识
-
第二章: 映射的度量——行列式
-
第三章: 为什么矩阵乘法是这样的?
-
第四章: 方程组、线性回归和神经网络
-
第五章: 秩与空秩及为什么行秩 == 列秩
线性代数是一门基础学科,它支撑了数学中可以做的一切。从物理学到机器学习、概率论(例如:马尔可夫链),无论你在做什么,线性代数总是潜伏在背后,一旦问题变得多维,它就会立刻出现。在我的经历中(我也从其他人那里听到过),这是高中和大学之间一个巨大的冲击源。在高中(印度),我接触了一些非常基础的线性代数(主要是行列式和矩阵乘法)。然后在大学的工程教育中,每个学科突然都假定你对特征值、雅可比矩阵等概念非常熟悉,好像你应该天生就具备这些知识一样。
这个博客旨在提供对这一学科中存在并且重要的概念及其明显应用的高层次概述。这样你至少知道自己不知道什么(如果有的话)。这也是一个收集资源和链接的借口,以便人们可以更深入地探索这个领域。
I) 向量空间
如前一节所述,当事物变成多维时,线性代数不可避免地出现。我们从一个标量开始,它只是某种数字。对于本文,我们将考虑这些标量的实数和复数。一般来说,标量可以是任何定义了加法、减法、乘法和除法基本操作的对象(抽象为“域”)。现在,我们需要一个框架来描述这样的数字集合(添加维度)。这些集合被称为“向量空间”。我们将考虑向量空间的元素是实数或复数的情况(前者是后者的特例)。得到的向量空间分别称为“实向量空间”和“复向量空间”。
线性代数中的概念适用于这些“向量空间”。最常见的例子是你的地板、桌子或你正在阅读本文的计算机屏幕。这些都是二维向量空间,因为你桌子上的每一个点都可以用两个数字来指定(如下面所示的 x 和 y 坐标)。这个空间被表示为 R²,因为两个实数可以指定它。
我们可以以不同的方式推广 R²。首先,我们可以添加维度。我们生活的空间是三维的(R³)。或者,我们可以对它进行曲线变换。例如,地球的表面(记作 S²)仍然是二维的,但与 R²(平坦的)不同,它是弯曲的。到目前为止,这些空间基本上都是数字的数组。但是向量空间的概念更为一般。它是一个对象集合,其中以下概念应该被定义得很明确:
-
任意两个对象的加法。
-
对象与标量(实数)的乘法。
不仅如此,这些对象还应该在这些操作下是“封闭”的。这意味着,如果你对向量空间的对象应用这两个操作,你应该得到相同类型的对象(你不应该离开向量空间)。例如,整数集合不是一个向量空间,因为通过标量(实数)乘法可能得到不是整数的结果(3*2.5 = 7.5,不是整数)。
表达向量空间中的对象的一种方式是使用向量。向量需要一个任意的“基”。基的一个例子是带方向的罗盘系统——北、南、东和西。任何方向(如“西南”)都可以用这些方向表示。这些是“方向向量”,但我们也可以有“位置向量”,其中我们需要一个原点和一个在该原点相交的坐标系统。地球表面的纬度和经度系统就是一个例子。纬度和经度对是一种标识你家位置的方法。但还有无限多种其他方式。另一种文化可能会将纬度和经度线绘制成与标准略有不同的角度,因此,他们会为你的房子得出不同的数字。但这并不会改变房子的实际位置。房子作为向量空间中的一个对象存在,这些表达位置的不同方式被称为“基”。选择一个基允许你为房子分配一对数字,而选择另一个基则允许你分配一组不同但同样有效的数字。
一个向量空间,其中每个位置都被组织并整齐地映射到一组数字。图像由 MidJourney 创建。
向量空间也可以是无限维的。例如,在[2]中的第 12 页,整个实数集被视为一个无限维的向量空间。
II) 线性映射
现在我们知道了什么是向量空间,让我们更进一步,讨论两个向量空间。由于向量空间只是对象的集合,我们可以考虑一种映射,将一个空间中的对象映射到另一个空间中的对象。一个例子是最近的 AI 程序,如 Midjourney,你输入一个文本提示,它们会返回一个匹配的图像。你输入的文本首先被转换为一个向量。然后,这个向量通过这样的“映射”转换为图像空间中的另一个向量。
设 V 和 W 为向量空间(可以是实向量空间或复向量空间)。一个函数 f: V -> W 被称为“线性映射”,如果对于任何两个向量 u, v ∈ V 和任何标量 c(根据我们处理的是实向量空间还是复向量空间,c 可以是实数或复数),以下两个条件都满足:
f(u+v) = f(u) + f(v) __(1)
f(c.v) = c.f(v) __(2)
结合上述两个属性,我们可以得到关于n个向量的线性组合的以下结果。
f(c1.u1+ c2.u2+ … cn.un) = c1.f(u1)+c2.f(u2)+…+cn.f(un)
现在我们可以看到“线性映射”这个名称的来源。如果我们将一个 n 个向量的 线性组合 传递给线性映射 f(上面方程的左侧),这等同于将相同的线性映射应用于单个向量的函数 (f)。我们可以先应用线性映射,然后进行线性组合,或者先进行线性组合,然后应用线性映射。这两者是等价的。
在高中,我们学习线性方程。在二维空间中,这样的方程由 f(x)=m.x+c 表示。这里,m 和 c 是方程的参数。请注意,这个函数不是线性映射。尽管它满足上面的方程 (1),但它未能满足方程 (2)。如果我们将 f(x)=m.x 代入,那么这是一个线性映射,因为它满足这两个方程。
线性映射将一个向量空间中的对象映射到另一个向量空间中的对象。就像是世界之间的一个传送门。当然,可能有许多这样的“映射”或“传送门”。线性映射必须满足其他属性。如果你将第一个空间中的向量的线性组合传递给它,先应用线性映射还是先应用线性组合都不重要。图像由 Midjourney 创建
III) 矩阵
在第一节中,我们介绍了向量空间的基的概念。给定第一个向量空间 (V) 的基和第二个向量空间 (U) 的维度,每个线性映射都可以表示为一个矩阵(详细信息见 这里)。矩阵只是向量的集合。这些向量可以按列排列,从而得到一个如下所示的二维数字网格。
矩阵作为按列排列的向量集合。图像由作者提供。
矩阵是人们在谈论线性代数时首先想到的对象,这有充分的理由。大多数时间花在练习线性代数上都是处理矩阵。但重要的是要记住,根据我们为第一个空间 V 选择的基,实际上有无数个矩阵可以表示一个线性映射。因此,线性映射的概念比用于表示它的矩阵要更加一般。
矩阵如何帮助我们执行它们表示的线性映射(从一个向量到另一个向量)?通过矩阵与第一个向量相乘。结果是第二个向量,映射完成(从第一个到第二个)。
具体来说,我们取第一个向量 v_1 与矩阵的第一行的点积(和积),这将得到结果向量 v_2 的第一个条目,然后是 v_1 与矩阵的第二行的点积以获得 v_2 的第二个条目,依此类推。下图展示了这一过程,对于一个具有 2 行和 3 列的矩阵。第一个向量 v_1 是三维的,第二个向量 v_2 是二维的。
矩阵与向量的乘法如何运作。图片由作者提供。
注意,具有这种维度 (2x3) 的矩阵背后的线性映射总是将一个三维向量 v_1 映射到二维空间 v_2。
一种将三维空间中的向量映射到二维空间的线性变换。图片由 MidJourney 创建。
一般来说,一个 (nxm) 矩阵将一个 m 维向量映射到一个 n 维向量。
III-A) 矩阵的属性
让我们介绍一些矩阵的属性,这些属性将帮助我们识别它们所表示的线性映射的性质。
秩
矩阵及其对应的线性映射的一个重要属性是秩。我们可以用一组向量来谈论这个问题,因为矩阵本质上就是一组向量。假设我们有一个向量,v1=[1,0,0]。这个向量的第一个元素是沿 x 轴的坐标,第二个元素是沿 y 轴的坐标,第三个元素是沿 z 轴的坐标。这三个轴是三维空间的基(有很多种基),R³,意味着这个空间中的任何向量都可以表示为这三个向量的线性组合。
一个三维空间中的单一向量。图片由作者提供。
我们可以将这个向量乘以一个标量,s。这会得到 s.[1,0,0] = [s,0,0]。当我们改变 s 的值时,我们可以得到沿 x 轴的任何一点。仅此而已。假设我们在集合中添加另一个向量,v2=[3.5,0,0]。那么,我们可以用这两个向量的线性组合得到哪些向量呢?我们将第一个向量乘以任何标量,s_1,第二个向量乘以任何标量,s_2。这给我们:
s_1.[1,0,0] + s_2[3.5,0,0] = [s_1+3.5 s_2, 0,0] = [s’,0,0]
这里,s’ 只是另一个标量。因此,即使对这两个向量进行线性组合,我们仍然只能到达 x 轴上的点。第二个向量并没有“扩展我们的到达范围”。用这两个向量的线性组合能到达的点数与用第一个向量能到达的点数完全相同。所以,即使我们有两个向量,这些向量的集合的秩仍然是1,因为它们跨越的空间是一维的。如果另一方面,第二个向量是 v2=[0,1,0],那么你可以用这两个向量在 x-y 平面上得到任何点。因此,所跨越的空间将是二维的,这个集合的秩将是2。如果第二个向量是 v2=[2.1,1.5,0.8],我们仍然可以用 v1 和 v2 跨越一个二维空间(尽管这个空间与 x-y 平面不同,它将是其他的 2 维平面)。这两个向量的秩仍然是2。如果一个向量集合的秩与向量的数量相同(意味着它们可以一起跨越一个与向量数量相同的维度的空间),那么它们被称为“线性无关”。
如果构成矩阵的向量能够跨越一个m维空间,那么矩阵的秩就是m。但是,矩阵可以通过两种方式被看作是向量的集合。由于它是一个简单的二维数字网格,我们可以考虑所有的列作为向量组,或者考虑所有的行作为向量组,如下所示。这里,我们有一个(3x4)的矩阵(三行四列)。它可以被看作是 4 个列向量的集合(每个向量是 3 维的)或者 3 个行向量的集合(每个向量是 4 维的)。
矩阵可以被看作是行向量的集合或列向量的集合。图像由作者提供。
满行秩意味着所有的行向量是线性无关的。满列秩意味着所有的列向量是线性无关的。
当矩阵是方阵时,行秩和列秩将始终相同。这一点并不明显,相关证明可以在 mathexchange 帖子中找到,[3]。这意味着对于方阵,我们只需要讨论秩,而不必去指明“行秩”或“列秩”。
对于一个(3 x 3)矩阵,若其秩为 2,则对应的线性变换会将 3 维空间中的所有内容映射到一个较低的 2 维空间,就像我们在上一节中遇到的(3 x 2)矩阵一样。
一个光源将 3 维空间中点的阴影投射到 2 维地板或墙面上,这是一个将 3 维向量映射到 2 维向量的线性变换。图像由 MidJourney 创建。
与方阵的秩密切相关的概念是行列式和可逆性。
行列式
方阵的行列式在某种意义上是它的“测量”。让我通过回到将矩阵视为向量集合来解释。我们从一个向量开始。对它进行“测量”的方法是显而易见的——它的长度。由于我们只处理方阵,拥有一个向量的唯一方法是使它一维。实际上这就是一个标量。当我们从一维转到二维时,事情变得有趣了。现在,我们处于二维空间。因此,“测量”的概念不再是长度,而是面积。在这个二维空间中,两个向量形成的平行四边形的面积就是它们的行列式。如果这两个向量彼此平行(例如:都在 x 轴上)。换句话说,它们不是线性独立的,那么它们之间的平行四边形的面积将变为零。由它们形成的矩阵的行列式将为零,该矩阵的秩也将为零。
两个向量形成一个平行四边形。平行四边形的面积是由这两个向量构成的矩阵的行列式。图片由作者提供。
将维度提升一维,我们得到三维空间。要构造一个方阵(3x3),现在需要三个向量。由于三维空间中的“测量”概念是体积,因此一个(3x3)矩阵的行列式变成了构成它的向量之间的体积。
在三维空间中,需要三个向量来创建一个 3x3 矩阵。该矩阵的行列式是这些向量之间的体积。图片由 MidJourney 提供。
这可以扩展到任意维度的空间。
请注意,我们提到了向量之间的面积或体积。我们没有指定这些向量是构成方阵的行还是列。令人有些惊讶的是,我们不需要指定这一点,因为无论哪种方式都没有关系。无论我们取向量形成的行来测量它们之间的体积,还是取形成列的向量,我们都得到相同的答案。这在 mathexchange 的帖子 [4] 中得到了证明。
线性映射及对应矩阵的其他许多属性在理解它们并从中提取价值时非常宝贵。我们将在接下来的文章中深入探讨可逆性、特征值、对角化以及可以进行的各种变换(请回来查看链接)。
如果你喜欢这个故事,请请我喝杯咖啡 😃 www.buymeacoffee.com/w045tn0iqw
参考文献
[1] 线性映射: en.wikipedia.org/wiki/Linear_map
[2] Matousek 的小册子: kam.mff.cuni.cz/~matousek/stml-53-matousek-1.pdf
[3] 证明行秩与列秩相同的 Mathexchange 帖子:math.stackexchange.com/questions/332908/looking-for-an-intuitive-explanation-why-the-row-rank-is-equal-to-the-column-ran
[4] 证明矩阵及其转置的行列式相同的 Mathexchange 帖子:math.stackexchange.com/a/636198/155881