解码器 | 基于 Transformers 的编码器-解码器模型

news2024/10/5 21:21:20

基于 transformer 的编码器-解码器模型是 表征学习模型架构 这两个领域多年研究成果的结晶。本文简要介绍了神经编码器-解码器模型的历史,更多背景知识,建议读者阅读由 Sebastion Ruder 撰写的这篇精彩 博文。此外,建议读者对 自注意力 (self-attention) 架构 有一个基本了解,可以阅读 Jay Alammar 的 这篇博文 复习一下原始 transformer 模型。

本文分 4 个部分:

  • 背景 - 简要回顾了神经编码器-解码器模型的历史,重点关注基于 RNN 的模型。

  • 编码器-解码器 - 阐述基于 transformer 的编码器-解码器模型,并阐述如何使用该模型进行推理。

  • 编码器 - 阐述模型的编码器部分。

  • 解码器 - 阐述模型的解码器部分。

每个部分都建立在前一部分的基础上,但也可以单独阅读。这篇分享是最后一部分 解码器

解码器

编码器-解码器 部分所述, 基于 transformer 的解码器定义了给定上下文编码序列条件下目标序列的条件概率分布:

根据贝叶斯法则,在给定上下文编码序列和每个目标变量的所有前驱目标向量的条件下,可将上述分布分解为每个目标向量的条件分布的乘积:

我们首先了解一下基于 transformer 的解码器如何定义概率分布。基于 transformer 的解码器由很多 解码器模块 堆叠而成,最后再加一个线性层 (即 “LM 头”)。这些解码器模块的堆叠将上下文相关的编码序列 和每个目标向量的前驱输入 (这里 为 BOS) 映射为目标向量的编码序列 。然后,“LM 头”将目标向量的编码序列 映射到 logit 向量序列 , 而每个 logit 向量 的维度即为词表的词汇量。这样,对于每个 ,其在整个词汇表上的概率分布可以通过对 取 softmax 获得。公式如下:

“LM 头” 即为词嵌入矩阵的转置, 。直观上来讲,这意味着对于所有 “LM 头” 层会将 与词汇表 中的所有词嵌入一一比较,输出的 logit 向量 即表示 与每个词嵌入之间的相似度。Softmax 操作只是将相似度转换为概率分布。对于每个 ,以下等式成立:

总结一下,为了对目标向量序列 的条件分布建模,先在目标向量 前面加上特殊的 向量 ( ),并将其与上下文相关的编码序列 一起映射到 logit 向量序列 。然后,使用 softmax 操作将每个 logit 目标向量 转换为目标向量 的条件概率分布。最后,将所有目标向量的条件概率 相乘得到完整目标向量序列的条件概率:

与基于 transformer 的编码器不同,在基于 transformer 的解码器中,其输出向量 应该能很好地表征 下一个 目标向量 (即 ),而不是输入向量本身 (即 )。此外,输出向量 应基于编码器的整个输出序列 。为了满足这些要求,每个解码器块都包含一个 单向自注意层,紧接着是一个 交叉注意层,最后是两个前馈层。单向自注意层将其每个输入向量 仅与其前驱输入向量 (其中 ,且 ) 相关联,来模拟下一个目标向量的概率分布。交叉注意层将其每个输入向量 与编码器输出的所有向量 相关联,来根据编码器输入预测下一个目标向量的概率分布。

好,我们仍以英语到德语翻译为例可视化一下 基于 transformer 的解码器。

ad7594c980fc3391625a3da3e0f398e7.png

我们可以看到解码器将 : “BOS”、“Ich”、“will”、“ein”、“Auto”、“kaufen” (图中以浅红色显示) 和 “I”、“want”、“to”、“buy”、“a”、“car”、“EOS” ( (图中以深绿色显示)) 映射到 logit 向量 (图中以深红色显示)。

因此,对每个 、、、 使用 softmax 操作可以定义下列条件概率分布:

总条件概率如下:

其可表示为以下乘积形式:

图右侧的红框显示了前三个目标向量 、、 在一个解码器模块中的行为。下半部分说明了单向自注意机制,中间说明了交叉注意机制。我们首先关注单向自注意力。

与双向自注意一样,在单向自注意中, query 向量 (如下图紫色所示), key 向量 (如下图橙色所示),和 value 向量 (如下图蓝色所示) 均由输入向量 (如下图浅红色所示) 映射而来。然而,在单向自注意力中,每个 query 向量 与当前及之前的 key 向量进行比较 (即 ) 并生成各自的 注意力权重 。这可以防止输出向量 (如下图深红色所示) 包含未来向量 (,其中 且  ) 的任何信息 。与双向自注意力的情况一样,得到的注意力权重会乘以它们各自的 value 向量并加权求和。

我们将单向自注意力总结如下:

请注意, keyvalue 向量的索引范围都是 而不是 , 是双向自注意力中 key 向量的索引范围。

下图显示了上例中输入向量 的单向自注意力。

99aea9deb2584d3ef6db3f400be91045.png

可以看出 只依赖于 和  。因此,单词 “Ich” 的向量表征 ( ) 仅与其自身及 “BOS” 目标向量 ( ) 相关联,而 与 “will” 的向量表征 ( ) 相关联。

那么,为什么解码器使用单向自注意力而不是双向自注意力这件事很重要呢?如前所述,基于 transformer 的解码器定义了从输入向量序列 到其 下一个 解码器输入的 logit 向量的映射,即 。举个例子,输入向量 = “Ich” 会映射到 logit 向量 ,并用于预测下一个输入向量 。因此,如果 可以获取后续输入向量 的信息,解码器将会简单地复制向量 “will” 的向量表征 ( ) 作为其输出 ,并就这样一直传播到最后一层,所以最终的输出向量 基本上就只对应于 的向量表征,并没有起到预测的作用。

这显然是不对的,因为这样的话,基于 transformer 的解码器永远不会学到在给定所有前驱词的情况下预测下一个词,而只是对所有 ,通过网络将目标向量 复制到 。以下一个目标变量本身为条件去定义下一个目标向量,即从 中预测 , 显然是不对的。因此,单向自注意力架构允许我们定义一个 因果的 概率分布,这对有效建模下一个目标向量的条件分布而言是必要的。

太棒了!现在我们可以转到连接编码器和解码器的层 - 交叉注意力 机制!

交叉注意层将两个向量序列作为输入: 单向自注意层的输出 和编码器的输出 。与自注意力层一样, query 向量 是上一层输出向量 的投影。而 keyvalue 向量 、 是编码器输出向量 的投影。定义完 keyvaluequery 向量后,将 query 向量 与 所有 key 向量进行比较,并用各自的得分对相应的 value 向量进行加权求和。这个过程与 双向 自注意力对所有 求 是一样的。交叉注意力可以概括如下:

注意,keyvalue 向量的索引范围是 ,对应于编码器输入向量的数目。

我们用上例中输入向量 来图解一下交叉注意力机制。

5816eed6c0a707a08164cc2a0432ccef.png

我们可以看到 query 向量 (紫色)源自 (红色),因此其依赖于单词 "Ich" 的向量表征。然后将 query 向量 与对应的 key 向量 (黄色)进行比较,这里的 key 向量对应于编码器对其输入 = "I want to buy a car EOS" 的上下文相关向量表征。这将 "Ich" 的向量表征与所有编码器输入向量直接关联起来。最后,将注意力权重乘以 value 向量 (青绿色)并加上输入向量 最终得到输出向量 (深红色)。

所以,直观而言,到底发生了什么?每个输出向量 是由所有从编码器来的 value 向量( )的加权和与输入向量本身 相加而得(参见上图所示的公式)。其关键思想是: 来自解码器的query 投影与 来自编码器的 越相关,其对应的 对输出的影响越大。

酷!现在我们可以看到这种架构的每个输出向量 取决于其来自编码器的输入向量 及其自身的输入向量 。这里有一个重要的点,在该架构中,虽然输出向量 依赖来自编码器的输入向量 ,但其完全独立于该向量的数量 。所有生成 key 向量 和 value 向量 的投影矩阵 和 都是与 无关的,所有 共享同一个投影矩阵。且对每个 ,所有 value 向量 被加权求和至一个向量。至此,关于为什么基于 transformer 的解码器没有远程依赖问题而基于 RNN 的解码器有这一问题的答案已经很显然了。因为每个解码器 logit 向量 直接 依赖于每个编码后的输出向量,因此比较第一个编码输出向量和最后一个解码器 logit 向量只需一次操作,而不像 RNN 需要很多次。

总而言之,单向自注意力层负责基于当前及之前的所有解码器输入向量建模每个输出向量,而交叉注意力层则负责进一步基于编码器的所有输入向量建模每个输出向量。

为了验证我们对该理论的理解,我们继续上面编码器部分的代码,完成解码器部分。


词嵌入矩阵 为每个输入词提供唯一的 上下文无关 向量表示。这个矩阵通常也被用作 “LM 头”,此时 “LM 头”可以很好地完成“编码向量到 logit” 的映射。

与编码器部分一样,本文不会详细解释前馈层在基于 transformer 的模型中的作用。Yun 等 (2017)  的工作认为前馈层对于将每个上下文相关向量 映射到所需的输出空间至关重要,仅靠自注意力层无法完成。这里应该注意,每个输出词元 对应的前馈层是相同的。有关更多详细信息,建议读者阅读论文。

from transformers import MarianMTModel, MarianTokenizer
import torch

tokenizer = MarianTokenizer.from_pretrained("Helsinki-NLP/opus-mt-en-de")
model = MarianMTModel.from_pretrained("Helsinki-NLP/opus-mt-en-de")
embeddings = model.get_input_embeddings()

# create token ids for encoder input
input_ids = tokenizer("I want to buy a car", return_tensors="pt").input_ids

# pass input token ids to encoder
encoder_output_vectors = model.base_model.encoder(input_ids, return_dict=True).last_hidden_state

# create token ids for decoder input
decoder_input_ids = tokenizer("<pad> Ich will ein", return_tensors="pt", add_special_tokens=False).input_ids

# pass decoder input ids and encoded input vectors to decoder
decoder_output_vectors = model.base_model.decoder(decoder_input_ids, encoder_hidden_states=encoder_output_vectors).last_hidden_state

# derive embeddings by multiplying decoder outputs with embedding weights
lm_logits = torch.nn.functional.linear(decoder_output_vectors, embeddings.weight, bias=model.final_logits_bias)

# change the decoder input slightly
decoder_input_ids_perturbed = tokenizer("<pad> Ich will das", return_tensors="pt", add_special_tokens=False).input_ids
decoder_output_vectors_perturbed = model.base_model.decoder(decoder_input_ids_perturbed, encoder_hidden_states=encoder_output_vectors).last_hidden_state
lm_logits_perturbed = torch.nn.functional.linear(decoder_output_vectors_perturbed, embeddings.weight, bias=model.final_logits_bias)

# compare shape and encoding of first vector
print(f"Shape of decoder input vectors {embeddings(decoder_input_ids).shape}. Shape of decoder logits {lm_logits.shape}")

# compare values of word embedding of "I" for input_ids and perturbed input_ids
print("Is encoding for `Ich` equal to its perturbed version?: ", torch.allclose(lm_logits[0, 0], lm_logits_perturbed[0, 0], atol=1e-3))

输出:

Shape of decoder input vectors torch.Size([1, 5, 512]). Shape of decoder logits torch.Size([1, 5, 58101])
    Is encoding for `Ich` equal to its perturbed version?: True

我们首先比较解码器词嵌入层的输出维度 embeddings(decoder_input_ids) (对应于 ,这里 <pad> 对应于 BOS 且  "Ich will das" 被分为 4 个词) 和 lm_logits (对应于 ) 的维度。此外,我们还通过解码器将单词序列 “<pad> Ich will ein” 和其轻微改编版 “<pad> Ich will das” 与 encoder_output_vectors 一起传递给解码器,以检查对应于 “Ich” 的第二个 lm_logit 在仅改变输入序列中的最后一个单词 (“ein” -> “das”) 时是否会有所不同。

正如预期的那样,解码器输入词嵌入和 lm_logits 的输出, 和   的最后一个维度不同。虽然序列长度相同 (=5),但解码器输入词嵌入的维度对应于 model.config.hidden_size,而 lm_logit 的维数对应于词汇表大小 model.config.vocab_size。其次,可以注意到,当将最后一个单词从 “ein” 变为 “das”, 的输出向量的值不变。鉴于我们已经理解了单向自注意力,这就不足为奇了。

最后一点, 自回归 模型,如 GPT2,与删除了交叉注意力层的 基于 transformer 的解码器模型架构是相同的,因为纯自回归模型不依赖任何编码器的输出。因此,自回归模型本质上与 自编码 模型相同,只是用单向注意力代替了双向注意力。这些模型还可以在大量开放域文本数据上进行预训练,以在自然语言生成 (NLG) 任务中表现出令人印象深刻的性能。在 Radford 等 (2019)  的工作中,作者表明预训练的 GPT2 模型无需太多微调即可在多种 NLG 任务上取得达到 SOTA 或接近 SOTA 的结果。你可以在 此处 获取所有 🤗 transformers 支持的 自回归 模型的信息。

好了!至此,你应该已经很好地理解了 基于 transforemr 的编码器-解码器模型以及如何在 🤗 transformers 库中使用它们。

非常感谢 Victor Sanh、Sasha Rush、Sam Shleifer、Oliver Åstrand、Ted Moskovitz 和 Kristian Kyvik 提供的宝贵反馈。

附录

如上所述,以下代码片段展示了如何为 基于 transformer 的编码器-解码器模型编写一个简单的生成方法。在这里,我们使用 torch.argmax 实现了一个简单的 贪心 解码法来对目标向量进行采样。

from transformers import MarianMTModel, MarianTokenizer
import torch

tokenizer = MarianTokenizer.from_pretrained("Helsinki-NLP/opus-mt-en-de")
model = MarianMTModel.from_pretrained("Helsinki-NLP/opus-mt-en-de")

# create ids of encoded input vectors
input_ids = tokenizer("I want to buy a car", return_tensors="pt").input_ids

# create BOS token
decoder_input_ids = tokenizer("<pad>", add_special_tokens=False, return_tensors="pt").input_ids

assert decoder_input_ids[0, 0].item() == model.config.decoder_start_token_id, "`decoder_input_ids` should correspond to `model.config.decoder_start_token_id`"

# STEP 1

# pass input_ids to encoder and to decoder and pass BOS token to decoder to retrieve first logit
outputs = model(input_ids, decoder_input_ids=decoder_input_ids, return_dict=True)

# get encoded sequence
encoded_sequence = (outputs.encoder_last_hidden_state,)
# get logits
lm_logits = outputs.logits

# sample last token with highest prob
next_decoder_input_ids = torch.argmax(lm_logits[:, -1:], axis=-1)

# concat
decoder_input_ids = torch.cat([decoder_input_ids, next_decoder_input_ids], axis=-1)

# STEP 2

# reuse encoded_inputs and pass BOS + "Ich" to decoder to second logit
lm_logits = model(None, encoder_outputs=encoded_sequence, decoder_input_ids=decoder_input_ids, return_dict=True).logits

# sample last token with highest prob again
next_decoder_input_ids = torch.argmax(lm_logits[:, -1:], axis=-1)

# concat again
decoder_input_ids = torch.cat([decoder_input_ids, next_decoder_input_ids], axis=-1)

# STEP 3
lm_logits = model(None, encoder_outputs=encoded_sequence, decoder_input_ids=decoder_input_ids, return_dict=True).logits
next_decoder_input_ids = torch.argmax(lm_logits[:, -1:], axis=-1)
decoder_input_ids = torch.cat([decoder_input_ids, next_decoder_input_ids], axis=-1)

# let's see what we have generated so far!
print(f"Generated so far: {tokenizer.decode(decoder_input_ids[0], skip_special_tokens=True)}")

# This can be written in a loop as well.

输出:

Generated so far: Ich will ein

在这个示例代码中,我们准确地展示了正文中描述的内容。我们在输入 “I want to buy a car” 前面加上 ,然后一起传给编码器-解码器模型,并对第一个 logit (对应代码中第一次出现 lm_logits 的部分) 进行采样。这里,我们的采样策略很简单: 贪心地选择概率最高的词作为下一个解码器输入向量。然后,我们以自回归方式将采样得的解码器输入向量与先前的输入一起传递给编码器-解码器模型并再次采样。重复 3 次后,该模型生成了 “Ich will ein”。结果没问题,开了个好头。

在实践中,我们会使用更复杂的解码方法来采样 lm_logits。你可以参考 这篇博文 了解更多的解码方法。

至此,《基于 Transformers 的编码器-解码器模型》的四个部分就全部分享完啦,欢迎大家阅读其他分享 🤗!


英文原文: https://hf.co/blog/encoder-decoder

原文作者: Patrick von Platen

译者: Matrix Yao (姚伟峰),英特尔深度学习工程师,工作方向为 transformer-family 模型在各模态数据上的应用及大规模模型的训练推理。

审校/排版: zhongdongy (阿东)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/618237.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Mocha AE:AdjustTrack 模块

跟踪时由于缺乏细节或有障碍物阻挡&#xff0c;跟踪点会发生漂移&#xff0c;此时可考虑使用 AdjustTrack &#xff08;调整跟踪&#xff09;模块手动设置表面区域 Planar Surface关键帧来获得更可靠的表面跟踪数据。 但是&#xff0c;如果需要设置较多的关键帧时&#xff0c;建…

Linux计划任务

常见的计划任务&#xff1a;进行日志的轮替&#xff08;log rotate&#xff09;&#xff1b;日志文件分析&#xff08;logwatch&#xff09;任务&#xff1b;建立locate数据库&#xff1b;man page查询数据库的建立&#xff1b;RPM软件登录文件的建立&#xff1b;移除暂存档&am…

尺度悖论解析费米悖论:从夜郎自大到揭秘宇宙中智慧生命的谜团

费米悖论是一个引人入胜的问题&#xff0c;它引发了人们对宇宙中是否存在其他智慧生命体的思考。然而&#xff0c;尺度悖论提供了一个可能的解释角度&#xff0c;即我们对宇宙的观测和推断尺度可能太小&#xff0c;无法涵盖整个宇宙范围。下面深入探讨尺度悖论以及费米悖论的具…

Linux系统一般用来干嘛

Linux系统是一种开源的操作系统&#xff0c;广泛应用于服务器、嵌入式设备、超级计算机等领域。它具有高度的稳定性、安全性和灵活性&#xff0c;可以用来进行各种各样的任务&#xff0c;例如&#xff1a; 1、服务器操作系统 Linux系统在服务器领域应用广泛&#xff0c;可以用…

Maven继承

Maven 在设计时&#xff0c;借鉴了 Java 面向对象中的继承思想&#xff0c;提出了 POM 继承思想。 当一个项目包含多个模块时&#xff0c;可以在该项目中再创建一个父模块&#xff0c;并在其 POM 中声明依赖&#xff0c;其他模块的 POM 可通过继承父模块的 POM 来获得对相关依赖…

微信小程序自定义导航栏

微信小程序自定义导航栏 业务需求&#xff1a; 点击小房子进行跳转指定的页面 、更改小房子的样式、或者是自定义导航栏 首先我们需要找到pages.json这个文件 如果是原生的微信小程序文件名字是 app.json其实就是找到配置路由的文件在代码里面添加属性"navigationStyle&qu…

java设计模式(十四)模板方法

目录 定义模式结构角色职责代码实现适用场景优缺点 定义 模板方法模式(Template Method Pattern)&#xff0c;又叫模板模式(Template Pattern)&#xff0c; 指在一个抽象类公开定义了执行它的方法的模板。它的子类可以按需要重写方法实现&#xff0c;但调用将以抽象类中定义的…

1. Hadoop 入门

1. Hadoop 入门 1. 大数据概述 1. 大数据相关说明 大数据由来&#xff1a; 传统数据处理应用软件不足以处理&#xff08;存储和计算&#xff09;它们大而复杂的数据集 大数据面临的两大问题&#xff1a; 针对海量数据的 存储、计算 大数据的特性&#xff1a;容量大、种类多…

VFP使用BLOB字段存取图片到SQL2000,显示出来也EASY

首先来看一下BLOB这个数据类型的介绍&#xff1a; 大二进制对象(Blob)数据类型&#xff0c;若要存储一个任何种类的二进制数据&#xff0c;如 ASCII 码文本、一个可执行文件(.exe) 或一个带有不确定长度的字节字符串&#xff0c;可使用大二进制对象数据类型。对于从 SQL Serve…

c++11 标准模板(STL)(std::bitset)(六)

定义于头文件 <bitset> template< std::size_t N > class bitset; 类模板 bitset 表示一个 N 位的固定大小序列。可以用标准逻辑运算符操作位集&#xff0c;并将它与字符串和整数相互转换。 bitset 满足可复制构造 (CopyConstructible) 及可复制赋值 (CopyAssign…

港科夜闻|海南省教育厅党委书记曹献坤到访香港科大(广州)开展实地调研

关注并星标 每周阅读港科夜闻 建立新视野 开启新思维 1、海南省教育厅党委书记曹献坤到访香港科大(广州)开展实地调研。香港科大(广州)临时党委书记屈哨兵从政治建设、思想建设、组织建设、制度建设及工作机制等方面&#xff0c;为曹献坤书记详细介绍了学校的党建工作体系构建&…

C盘爆了怎么办

一、删除大文件 关闭hiberfil.sys功能 关闭hiberfil.sys功能&#xff08;系统休眠时才会用到&#xff09; 管理员身份运行cmd 输入如下命令 powercfg.exe -h off移动pagefile.sys 这是虚拟内存文件,不建议删除&#xff0c;可以移动 右击此电脑->属性->高级系统设置 …

被吐槽,苹果挤牙膏式发布会,跟微信产品迭代如出一辙

大家好&#xff0c;我是校长。 今天一大早醒来&#xff0c;苹果发布会&#xff0c;毫无意外&#xff0c;在 iOS 系统更新迭代方面&#xff0c;可谓是乏善可陈&#xff0c;毫无新意。 当然了&#xff0c;被吐槽也就在情理之中了。 很多人说 iOS17 的最大变化&#xff0c;就是没有…

ISO21434 产品开发网络安全(七)

目录 一、概述 二、目标 三、输入 3.1 先决条件 3.2 进一步支持信息 四、要求和建议 4.1 设计 4.2 集成和验证 五、输出 一、概述 本条款描述了网络安全要求和架构设计的规范&#xff08;章节10.4.1&#xff09;。 此外&#xff0c;本子句还描述了集成和验证活动&…

Yakit: 集成化单兵安全能力平台使用教程·反连管理篇

Yakit: 集成化单兵安全能力平台使用教程反连管理篇 1.端口监听器2.DNSlog3.反连服务器4.ICMP-Sizelog5.TCP-Portlog6.Yso-Java Hack1.端口监听器 反弹 Shell 的接收工具,利用端口监听器可以在服务器上开启一个端口,进行监听,并进行交互 输入想要监听的端口,点击监听该端口…

直播抽奖功能(互动功能接收端JS-SDK)

功能概述 本模块主要用于处理抽奖相关的逻辑&#xff0c;可以对抽奖开始和抽奖结束等事件进行监听以及提交观众中奖信息&#xff0c;接入方可以根据这些事件流程定制自己的UI界面。 抽奖--效果截图 抽奖--效果截图 初始化及销毁 在实例化该模块并进行使用之前&#xff0c;需要…

Shell脚本:for循环

shell脚本-for循环 一、for循环&#xff1a;1.格式&#xff1a;2.实操&#xff1a;3.类c语言&#xff1a; 一、for循环&#xff1a; 1.格式&#xff1a; &#xff08;1&#xff09;for 已知的循环次数场景 for 变量名 in 【 名称范围 】 do 命令序列&#xff08;执行内容&…

Vue+springboot美发美容化妆品产品商城系统

不同的游客可以注册成为用户&#xff0c;然后可以查看&#xff0c;搜索自己想买的美发产品&#xff0c;加入购物车&#xff0c;下订单&#xff0c;收货&#xff0c;确认付款等购物流程。前台和后台的分析使得该系统结构清晰&#xff0c;即包含管理员和用户两个最基本的实体&…

C++递推基本概念和基础知识

目录 一、递推的概念 什么是递推算法&#xff1f; 解决递推问题的一般形式 二、递推和递归的区别 三、递推的实例 一、递推的概念 什么是递推算法&#xff1f; “递推”是计算机解题的一种常用方法。利用“递推法”解题首先要分析归纳出“递推关系”。如经典的斐波那契数…

MIT 6.824 lab distributed system 分布式系统(1)----lab1 MapReduce

https://youtu.be/cQP8WApzIQQ 概念 为什么需要分布式系统&#xff1f; high performanceparallelism&#xff1a;分布式系统可以实现CPU、内存、硬盘的并行运行fault tolerancephysical&#xff1a;security / isolated 分布式系统的困难 concurrency&#xff1a;各个并行的…