当前位置: 首页 > news >正文

Transformer图解以及相关的概念

前言

transformer是目前NLP甚至是整个深度学习领域不能不提到的框架,同时大部分LLM也是使用其进行训练生成模型,所以transformer几乎是目前每一个机器人开发者或者人工智能开发者不能越过的一个框架。接下来本文将从顶层往下去一步步掀开transformer的面纱。

transformer概述

Transformer模型来自论文Attention Is All You Need。

在论文中最初是为了提高机器翻译的效率,它使用了Self-Attention机制和Position Encoding去替代RNN。后来大家发现Self-Attention的效果很好,并且在其它的地方也可以使用Transformer模型。并引出后面的BERT和GPT系列。

大家一般看到的transformer框架如下图所示:

transformer模型概览

首先把模型看成一个黑盒,如下图所示,对于机器翻译来说,它的输入是源语言(法语)的句子,输出是目标语言(英语)的句子。

把黑盒子稍微打开一点,Transformer(或者任何的NMT系统)可以分成Encoder和Decoder两个部分,如下图所示。

再展开一点,Encoder由很多结构一样的Encoder堆叠而成,Decoder也是一样。如下图所示。

每一个Encoder的输入是下一层Encoder输出,最底层Encoder的输入是原始的输入(法语句子);Decoder也是类似,但是最后一层Encoder的输出会输入给每一个Decoder层,这是Attention机制的要求。

每一层的Encoder都是相同的结构,它由一个Self-Attention层和一个前馈网络(全连接网络)组成,如下图所示。

每一层的Decoder也是相同的结构,它除了Self-Attention层和全连接层之外还多了一个Attention层,这个Attention层使得Decoder在解码时会考虑最后一层Encoder所有时刻的输出。它的结构如下图所示。

transformer流程串联

transformer的串流需要tensor的加入,输入的句子需要通过Embedding把它变成一个连续稠密的向量,如下图所示。

Embedding之后的序列会输入Encoder,首先经过Self-Attention层然后再经过全连接层

我们在计算𝑧𝑖时需要依赖所有时刻的输入𝑥1,…,𝑥𝑛,这是可以用矩阵运算一下子把所有的𝑧𝑖计算出来的。而全连接网络的计算则完全是独立的,计算i时刻的输出只需要输入𝑧𝑖就足够了,因此很容易并行计算。下图更加明确的表达了这一点。图中Self-Attention层是一个大的方框,表示它的输入是所有的𝑥1,…,𝑥𝑛,输出是𝑧1,…,𝑧𝑛。而全连接层每个时刻是一个方框(但不同时刻的参数是共享的),表示计算𝑟𝑖只需要𝑧𝑖。此外,前一层的输出𝑟1,…,𝑟𝑛直接输入到下一层。

Self-Attention介绍

比如我们要翻译如下句子”The animal didn’t cross the street because it was too tired”(这个动物无法穿越马路,因为它太累了)。这里的it到底指代什么呢,是animal还是street?要知道具体的指代,我们需要在理解it的时候同时关注所有的单词,重点是animal、street和tired,然后根据知识(常识)我们知道只有animal才能tired,而street是不能tired的。Self-Attention用Encoder在编码一个词的时候会考虑句子中所有其它的词,从而确定怎么编码当前词。如果把tired换成narrow,那么it就指代的是street了。

下图是模型的最上一层Encoder的Attention可视化图。这是tensor2tensor这个工具输出的内容。我们可以看到,在编码it的时候有一个Attention Head(后面会讲到)注意到了Animal,因此编码后的it有Animal的语义。

下面我们详细的介绍Self-Attention是怎么计算的,首先介绍向量的形式逐个时刻计算,这便于理解,接下来我们把它写出矩阵的形式一次计算所有时刻的结果。

对于输入的每一个向量(第一层是词的Embedding,其它层是前一层的输出),我们首先需要生成3个新的向量Q、K和V,分别代表查询(Query)向量、Key向量和Value向量。Q表示为了编码当前词,需要去注意(attend to)其它(其实也包括它自己)的词,我们需要有一个查询向量。而Key向量可以认为是这个词的关键的用于被检索的信息,而Value向量是真正的内容。

具体的计算过程如下图所示。比如图中的输入是两个词”thinking”和”machines”,我们对它们进行Embedding(这是第一层,如果是后面的层,直接输入就是向量了),得到向量𝑥1,𝑥2。接着我们用3个矩阵分别对它们进行变换,得到向量𝑞1,𝑘1,𝑣1和𝑞2,𝑘2,𝑣2。比如𝑞1=𝑥1𝑊𝑄,图中𝑥1的shape是1x4,𝑊𝑄是4x3,得到的𝑞1是1x3。其它的计算也是类似的,为了能够使得Key和Query可以内积,我们要求𝑊𝐾𝑊𝑄的shape是一样的,但是并不要求𝑊𝑉和它们一定一样(虽然实际论文实现是一样的)。

每个时刻t都计算出𝑄𝑡,𝐾𝑡,𝑉𝑡之后,我们就可以来计算Self-Attention了。以第一个时刻为例,我们首先计算𝑞1和𝑘1,𝑘2的内积,得到score,过程如下图所示。

接下来使用softmax把得分变成概率,注意这里把得分除以8(𝑑𝑘)之后再计算的softmax,根据论文的说法,这样计算梯度时会更加稳定(stable)。计算过程如下图所示。

接下来用softmax得到的概率对所有时刻的V求加权平均,这样就可以认为得到的向量根据Self-Attention的概率综合考虑了所有时刻的输入信息,计算过程如下图所示。

这里只是演示了计算第一个时刻的过程,计算其它时刻的过程是完全一样的。

softmax示例代码:

import numpy as npdef softmax(x):"""Compute softmax values for each sets of scores in x."""# e_x = np.exp(x)e_x = np.exp(x )return e_x / e_x.sum()if __name__ == '__main__':x = np.array([-3, 2, -1, 0])res = softmax(x )print(res)                        # [0.0056533  0.83902451 0.04177257 0.11354962]

特别注意,以上过程是可以并行计算的

Multi-Head Attention

论文还提出了Multi-Head Attention的概念。其实很简单,前面定义的一组Q、K和V可以让一个词attend to相关的词,我们可以定义多组Q、K和V,它们分别可以关注不同的上下文。计算Q、K和V的过程还是一样,不过现在变换矩阵从一组(𝑊𝑄,𝑊𝐾,𝑊𝑉)变成了多组(𝑊𝑄0,𝑊𝐾0,𝑊𝑉0) ,(𝑊𝑄1,𝑊𝐾1,𝑊𝑉1)。如下图所示。

对于输入矩阵(time_step, num_input),每一组Q、K和V都可以得到一个输出矩阵Z(time_step, num_features)。如下图所示。

但是后面的全连接网络需要的输入是一个矩阵而不是多个矩阵,因此我们可以把多个head输出的Z按照第二个维度拼接起来,但是这样的特征有一些多,因此Transformer又用了一个线性变换(矩阵𝑊𝑂)对它进行了压缩。这个过程如下图所示。

上面的步骤涉及很多步骤和矩阵运算,我们用一张大图把整个过程表示出来,如下图所示。

我们已经学习了Transformer的Self-Attention机制,下面我们通过一个具体的例子来看看不同的Attention Head到底学习到了什么样的语义。

从上面两图的对比也能看出使用多个Head的好处——每个Head(在数据的驱动下)学习到不同的语义。

位置编码(Positional Encoding)

我们的目的是用Self-Attention替代RNN,RNN能够记住过去的信息,这可以通过Self-Attention“实时”的注意相关的任何词来实现等价(甚至更好)的效果。RNN还有一个特定就是能考虑词的顺序(位置)关系,一个句子即使词完全是相同的但是语义可能完全不同,比如”北京到上海的机票”与”上海到北京的机票”,它们的语义就有很大的差别。我们上面的介绍的Self-Attention是不考虑词的顺序的,如果模型参数固定了,上面两个句子的北京都会被编码成相同的向量。但是实际上我们可以期望这两个北京编码的结果不同,前者可能需要编码出发城市的语义,而后者需要包含目的城市的语义。而RNN是可以(至少是可能)学到这一点的。当然RNN为了实现这一点的代价就是顺序处理,很难并行。

为了解决这个问题,我们需要引入位置编码,也就是t时刻的输入,除了Embedding之外(这是与位置无关的),我们还引入一个向量,这个向量是与t有关的,我们把Embedding和位置编码向量加起来作为模型的输入。这样的话如果两个词在不同的位置出现了,虽然它们的Embedding是相同的,但是由于位置编码不同,最终得到的向量也是不同的。

位置编码有很多方法,其中需要考虑的一个重要因素就是需要它编码的是相对位置的关系。比如两个句子:”北京到上海的机票”和”你好,我们要一张北京到上海的机票”。显然加入位置编码之后,两个北京的向量是不同的了,两个上海的向量也是不同的了,但是我们期望Query(北京1)Key(上海1)却是等于Query(北京2)Key(上海2)的。具体的编码算法我们在代码部分再介绍。位置编码加入后的模型如下图所示。

一个具体的位置编码的例子如下图所示。

残差和归一化

每个Self-Attention层都会加一个残差连接,然后是一个LayerNorm层,如下图所示。

下图展示了更多细节:输入𝑥1,𝑥2经self-attention层之后变成𝑧1,𝑧2,然后和残差连接的输入𝑥1,𝑥2加起来,然后经过LayerNorm层输出给全连接层。全连接层也是有一个残差连接和一个LayerNorm层,最后再输出给上一层。

Decoder和Encoder是类似的,如下图所示,区别在于它多了一个Encoder-Decoder Attention层,这个层的输入除了来自Self-Attention之外还有Encoder最后一层的所有时刻的输出。Encoder-Decoder Attention层的Query来自前面一层,而Key和Value则来自Encoder的输出。

此外在解码器的编码器-解码器注意力层中,掩码的使用非常关键,以确保解码器在生成每个目标词时只能使用到源语言句子的信息和它之前已经生成的目标词的信息

pytorch实现transformer

import torch
import torch.nn as nn
import math# 位置编码模块
class PositionalEncoding(nn.Module):def __init__(self, d_model, max_len=5000):super(PositionalEncoding, self).__init__()pe = torch.zeros(max_len, d_model)position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))pe[:, 0::2] = torch.sin(position * div_term)pe[:, 1::2] = torch.cos(position * div_term)pe = pe.unsqueeze(0)self.register_buffer('pe', pe)def forward(self, x):x = x + self.pe[:x.size(0), :]return x# Transformer模型
class TransformerModel(nn.Module):def __init__(self, ntoken, d_model, nhead, d_hid, nlayers, dropout=0.5):super(TransformerModel, self).__init__()self.model_type = 'Transformer'self.pos_encoder = PositionalEncoding(d_model)self.encoder = nn.Embedding(ntoken, d_model)self.transformer = nn.Transformer(d_model, nhead, d_hid, nlayers, dropout)self.decoder = nn.Linear(d_model, ntoken)self.init_weights()self.dropout = nn.Dropout(dropout)def generate_square_subsequent_mask(self, sz):# 生成后续掩码,用于防止位置信息泄露mask = (torch.triu(torch.ones(sz, sz)) == 1).transpose(0, 1)mask = mask.float().masked_fill(mask == 0, float('-inf')).masked_fill(mask == 1, float(0.0))return maskdef init_weights(self):# 初始化权重initrange = 0.1self.encoder.weight.data.uniform_(-initrange, initrange)self.decoder.bias.data.zero_()self.decoder.weight.data.uniform_(-initrange, initrange)def forward(self, src, src_mask):# 前向传播src = self.encoder(src) * math.sqrt(self.d_model)src = self.pos_encoder(src)output = self.transformer(src, src, src_key_padding_mask=src_mask)output = self.decoder(output)return output# 示例使用
ntokens = 1000  # 词汇表大小
d_model = 512  # 嵌入维度
nhead = 8  # 多头注意力中的头数
d_hid = 2048  # 前馈网络模型的维度
nlayers = 6  # 层数
dropout = 0.2  # dropout比率model = TransformerModel(ntokens, d_model, nhead, d_hid, nlayers, dropout)# 示例输入
src = torch.randint(0, ntokens, (10, 32))  # (序列长度, 批量大小)
src_mask = model.generate_square_subsequent_mask(10)  # 创建掩码output = model(src, src_mask)
print(output)

推理过程

在Transformer模型的机器翻译任务中,解码器生成第一个翻译后的词(通常称为第一个目标词)的过程如下:

  1. 起始符号:在解码器的输入序列的开始位置,通常会添加一个特殊的起始符号,如 <sos>(Start Of Sentence)。这个符号告诉模型翻译过程的开始。

  2. 初始化隐藏状态:解码器的隐藏状态通常初始化为零向量或从编码器的最后一层的输出中获得。这个隐藏状态在生成序列的每一步中都会更新。

  3. 第一次迭代:在第一次迭代中,解码器的输入只包含起始符号 <sos>。解码器通过以下步骤生成第一个词:

  • 将起始符号 <sos> 通过嵌入层转换为嵌入向量。

  • 将这个嵌入向量与编码器的输出一起输入到解码器的第一个注意力层。

  • 在自注意力层中,使用因果掩码(Look-ahead Mask)确保解码器只能关注到当前位置和之前的词(在这个例子中只有 <sos>)。

  • 在编码器-解码器注意力层中,解码器可以查看整个编码器的输出,因为这是第一次迭代,解码器需要获取关于整个源语言句子的信息。

  • 经过解码器的前馈网络后,输出层会生成一个概率分布,表示下一个可能的词。

  • 选择概率最高的词作为第一个翻译后的词,或者使用贪婪策略、束搜索(Beam Search)等解码策略来选择词。

  1. 后续迭代:一旦生成了第一个词,它就会被添加到解码器的输入序列中,与 <sos> 一起作为下一步的输入。在后续的迭代中,解码器会继续生成下一个词,直到遇到结束符号 <eos> 或达到最大序列长度。

在训练阶段,目标序列的真实词(包括 <sos> 和 <eos>)会用于计算损失函数,并通过反向传播更新模型的权重。在推理阶段,解码器使用上述过程逐步生成翻译,直到生成完整的句子。

相关文章:

Transformer图解以及相关的概念

前言 transformer是目前NLP甚至是整个深度学习领域不能不提到的框架&#xff0c;同时大部分LLM也是使用其进行训练生成模型&#xff0c;所以transformer几乎是目前每一个机器人开发者或者人工智能开发者不能越过的一个框架。接下来本文将从顶层往下去一步步掀开transformer的面…...

Nginx缓存静态文件

在Python项目中&#xff0c;通过Nginx缓存静态文件&#xff08;如CSS、JS、图片等&#xff09;&#xff0c;可以有效提升网页的加载性能。Nginx可以帮助你缓存静态资源&#xff0c;减少服务器负担&#xff0c;并加速页面加载。 1. 配置Nginx缓存静态文件 首先&#xff0c;你需…...

【隐私计算】隐语HEU同态加密算法解读

HEU: 一个高性能的同态加密算法库&#xff0c;提供了多种 PHE 算法&#xff0c; 包括ZPaillier、FPaillier、IPCL、Damgard Jurik、DGK、OU、EC ElGamal 以及基于FPGA和GPU硬件加速版本的Paillier版本。 本文我们会基于GPU运行HEU Docker容器&#xff0c;编译打包GPaillier并测…...

用C#实现互斥操作

1、传统的lock lock简单易用&#xff0c;适合大多数场景&#xff0c;但在高竞争用情况下可能会导致线程阻塞&#xff1b; Object obj new object(); void method1(){lock (obj){// 进行互斥操作}}2、SpinLock SpinLock在低延迟情况下更有效&#xff0c;因为SpinLock会在忙等…...

【黑马点评优化】之使用Caffeine+Redis实现应用级二层缓存

【黑马点评优化】之使用CaffeineRedis实现应用级二层缓存 1 缓存雪崩定义及解决方案2 为什么要使用多级缓存3 RedisCaffeine实现应用层二级缓存原理4 利用CaffeineRedis解决Redis突然宕机导致的缓存雪崩问题4.1 pom.xml文件引入相关依赖4.2 本地缓存配置类4.3 修改ShopServiceI…...

CEEMDAN +组合预测模型(BiLSTM-Attention + ARIMA)

往期精彩内容&#xff1a; 时序预测&#xff1a;LSTM、ARIMA、Holt-Winters、SARIMA模型的分析与比较 全是干货 | 数据集、学习资料、建模资源分享&#xff01; EMD、EEMD、FEEMD、CEEMD、CEEMDAN的区别、原理和Python实现&#xff08;一&#xff09;EMD-CSDN博客 EMD、EEM…...

2.1.ReactOS系统中断描述符的格式KIDTENTRY结构体

2.&#xff11;.ReactOS系统中断描述符的格式KIDTENTRY结构体 2.&#xff11;.ReactOS系统中断描述符的格式KIDTENTRY结构体 文章目录 2.&#xff11;.ReactOS系统中断描述符的格式KIDTENTRY结构体KIDTENTRY KIDTENTRY 数据结构KIDTENTRY定义了CPU对中断描述符的格式 // // …...

三、ElementPlus下拉搜索加弹窗组件的封装

近期产品提出了一个需求&#xff0c;要求一个form的表单里面的一个组件既可以下拉模糊搜索&#xff0c;又可以弹窗搜索&#xff0c;我就为这个封装了一个组件&#xff0c;下面看效果图。 效果大家看到了&#xff0c;下面就看组件封装和实现方法 第一步&#xff0c;组件封装&…...

androidStudio编译导致的同名.so文件冲突问题解决

files found with path lib/arm64-v8a/libserial_port.so from inputs: ...\build\intermediates\library_jni\debug\jni\arm64-v8a\libserial_port.so C:\Users\...\.gradle\caches\transforms-3\...\jni\arm64-v8a\XXX.so 解决方式如下&#xff1a; 1.将gradle缓存文件删…...

大学新生编程入门指南:如何选择编程语言与制定学习计划

大学新生编程入门指南&#xff1a;如何选择编程语言与制定学习计划 编程已成为当代大学生的必备技能&#xff0c;尤其是在信息技术高速发展的今天&#xff0c;编程能力不仅能帮助你在课堂学习中脱颖而出&#xff0c;更能为未来职业生涯打下坚实的基础。然而&#xff0c;面对如…...

SpringAI快速上手

一、导入依赖 镜像&#xff08;导入maven依赖&#xff09; <repositories><repository><id>spring-snapshots</id><name>Spring Snapshots</name><url>https://repo.spring.io/snapshot</url><releases><enabled>…...

07 django管理系统 - 部门管理 - 搜索部门

在dept_list.html中&#xff0c;添加搜索框 <div class"container-fluid"><div style"margin-bottom: 10px" class"clearfix"><div class"panel panel-default"><!-- Default panel contents --><div clas…...

数据操作学习

1.导入torch。虽然被称为PyTorch&#xff0c;但应导入torch而不是pytorch import torch 2.张量表示一个数值组成的数组&#xff0c;这个数组可能有多个维度 xtorch.arange(12)x 3.通过张量的shape属性来访问张量的形状和张量中元素的总数 x.shape x.numel() 4.要改变张量的形…...

什么是网络代理

了解网络代理 网络代理是一种特殊的网络服务&#xff0c;它允许一个网络终端&#xff08;通常指客户端&#xff09;通过这个服务与另一个网络终端&#xff08;通常指服务器&#xff09;进行非直接的连接。网络代理服务器位于发送主机和接收主机之间&#xff0c;接收网络请求&a…...

安防监控摄像头图传模组,1公里WiFi无线传输方案,监控新科技

在数字化浪潮汹涌的今天&#xff0c;安防监控领域也迎来了技术革新的春风。今天&#xff0c;我们就来聊聊这一领域的产品——摄像头图传模组&#xff0c;以及它如何借助飞睿智能1公里WiFi无线传输技术&#xff0c;为安防监控带来未有的便利与高效。 一、安防监控的新篇章 随着…...

问:JVM中GC类型有哪些?触发条件有哪些?区别是啥?

在Java虚拟机&#xff08;JVM&#xff09;中&#xff0c;垃圾收集&#xff08;GC&#xff09;是自动管理内存的关键机制。GC负责识别并回收那些不再被程序使用的对象&#xff0c;以释放内存空间。根据回收的区域和策略的不同&#xff0c;JVM中的GC可以分为多种类型。 一、GC的…...

【操作系统的使用】Linux 输入输出重定向:掌握控制台的高级用法

文章目录 Linux 输入输出重定向&#xff1a;掌握控制台的高级用法输出重定向将命令输出保存到文件将命令输出追加到文件 输入重定向从文件读取输入 管道操作将多个命令的输出链接起来 错误重定向将错误信息保存到文件同时重定向输出和错误信息 Linux 输入输出重定向&#xff1a…...

无线通信中的四个关键概念:OFDM、多径效应、CSI和信道均衡

无线通信中的四个关键概念&#xff1a;OFDM、多径效应、CSI和信道均衡 无线通信技术在现代通信系统中发挥着至关重要的作用。无论是日常的手机通信&#xff0c;还是复杂的物联网应用&#xff0c;理解无线信道的特性和优化信号传输的技术是关键。在本文中&#xff0c;我们将介绍…...

如何高效规划千人大会?数字化会议管理的实战经验分享!建议收藏!

在当今快节奏的商业环境中&#xff0c;大型会议不仅是企业展示自身实力、促进交流合作的重要平台&#xff0c;更是推动行业发展、分享创新思维的关键活动。然而&#xff0c;随着参会人数的增加&#xff0c;如何高效规划并管理一场千人大会&#xff0c;成为了组织者面临的巨大挑…...

mysql指令笔记(基本)

一、数据库操作 创建数据库&#xff1a;CREATE DATABASE database_name;选择数据库&#xff1a;USE database_name;删除数据库&#xff1a;DROP DATABASE database_name; 二、表操作 创建表&#xff1a;CREATE TABLE table_name (column1 datatype constraint, column2 datat…...

web前端-----html5----用户注册

以改图为例 <!DOCTYPE html> <html lang"en"> <head> <meta charset"UTF-8"> <meta name"viewport" content"widthdevice-width, initial-scale1.0"> <title>用户注册</title> </hea…...

bug的定义和测试

一、软件测试的生命周期 软件测试的⽣命周期是指测试流程&#xff0c;这个流程是按照⼀定顺序执⾏的⼀系列特定的步骤&#xff0c;去保证产品 质量符合需求。在软件测试⽣命周期流程中&#xff0c;每个活动都按照计划的系统的执⾏。每个阶段有不同的 ⽬标和交付产物 需求分析…...

Kamailio-Sngrep 短小精悍的利器

一个sip的抓包小工具&#xff0c;在GitHub上竟然能够积累1K的star&#xff0c;看来还是有点东西&#xff0c;当然官方的友链也是发挥了重要作用 首先送上项目地址&#xff0c;有能力的宝子可以自行查看 经典的网络抓包工具有很多&#xff0c;比如&#xff1a; Wireshark&…...

9.6 Linux_I/O_IO模型

基本概念 I/O执行过程与分类&#xff1a; 用户进程中的一个完整I/O分为 "用户进程空间->内核空间->设备空间(磁盘、网卡)" 这两个阶段。 I/O可以分为内存I/O、网络I/O、磁盘I/O 同步和异步是什么&#xff1a; 1、对于线程的请求调用&#xff0c;同步与异步…...

React 探秘(一):fiber 架构

文章目录 背景React 采用 fiber 主要为了解决哪些问题&#xff1f;性能问题&#xff1a;用户体验问题&#xff1a; 为什么在 React 15 版本中性能会差&#xff1a;浏览器绘制原理&#xff1a;react 15 架构和问题 那么 fiber 怎么解决了这个问题&#xff1f;任务“大”的问题递…...

poi通过在word中写入了表格,通过libreoffice转换成PDF后,word中刚才画的表格宽度无限拉伸问题的解决。

一、复现&#xff1a; poi版本&#xff1a; <poi>3.17</poi><poi-ooxml>3.17</poi-ooxml><poi-ooxml-schemas>3.17</poi-ooxml-schemas><dependency><groupId>org.apache.poi</groupId><artifactId>poi</arti…...

尚硅谷rabbitmq2024 集群篇仲裁队列 第52节 答疑

我们希望创建一个队列&#xff0c;队列分布在各个节点上&#xff0c;仲裁队列很好的解决了这个问题.那么在仲裁队列之前&#xff0c;创建一个队列&#xff0c;队列不是分布在各个节点上的吗&#xff1f; 在RabbitMQ中&#xff0c;默认情况下创建的队列是“普通队列”&#xff0…...

《Spring Cloud 微服务:构建高效、灵活的分布式系统》

《Spring Cloud 微服务&#xff1a;构建高效、灵活的分布式系统》 一、引言 在当今快速发展的数字化时代&#xff0c;软件系统的规模和复杂性不断增加。为了应对这种挑战&#xff0c;微服务架构应运而生。Spring Cloud 作为构建微服务架构的强大工具集&#xff0c;提供了一系…...

OpenFeign 入门与实战:快速搭建 Spring Cloud 微服务客户端

1. 前言 随着微服务架构的流行&#xff0c;服务之间的通信变得越来越重要。Spring Cloud 提供了一系列工具来帮助开发者构建分布式系统&#xff0c;其中 OpenFeign 是一个轻量级的 HTTP 客户端&#xff0c;它简化了 Web 服务客户端的开发。本文将介绍如何在 Spring Cloud 应用…...

上门按摩系统开发方案源码搭建

上门按摩系统开发方案 一、项目概述 上门按摩系统是一个连接按摩技师和客户的平台&#xff0c;旨在提供便捷、高效的上门按摩服务。通过该系统&#xff0c;客户可以轻松预约合适的按摩技师&#xff0c;并享受个性化的按摩服务。 二、系统功能模块 用户管理模块&#xff1a;…...