transformer代码注解
其中代码均来自李沐老师的动手学pytorch中。
class PositionWiseFFN(nn.Module):'''ffn_num_inputs 4ffn_num_hiddens 4ffn_num_outputs 8'''def __init__(self,ffn_num_inputs,ffn_num_hiddens,ffn_num_outputs):super(PositionWiseFFN,self).__init__()self.dense1 = nn.Linear(ffn_num_inputs,ffn_num_hiddens)#4*4self.relu = nn.ReLU()self.dense2 = nn.Linear(ffn_num_hiddens,ffn_num_outputs)#4*8def forward(self,X):return self.dense2(self.relu(self.dense1(X)))
positionWiseFFN = PositionWiseFFN(4,4,8)
positionWiseFFN.eval()
positionWiseFFN(torch.ones(size=(2,3,4)))[0]
上面的代码为前馈神经网络结构,其实也就是一个全连接层。
class AddNorm(nn.Module):def __init__(self,normalized_shape,dropout):super(AddNorm, self).__init__()self.dropout=nn.Dropout(dropout)self.layer_norm=nn.LayerNorm(normalized_shape=normalized_shape)def forward(self,x,y):return self.layer_norm(self.dropout(y)+x)
#比如[3, 4]或torch.Size([3, 4]),则会对网络最后的两维进行归一化,且要求输入数据的最后两维尺寸也是[3, 4]
add_norm = AddNorm(normalized_shape=[3,4],dropout=0.5)
add_norm.eval()
add_norm(torch.ones(size=(2,3,4)),torch.ones(size=(2,3,4)))
这里实现的是残差化和规范化。nn.LayerNorm(normalized_shape=normalized_shape)
为layer规范化,其中normalized_shape为[3, 4],对网络最后的两维进行归一化。
class MultiHeadAttention(nn.Module):def __init__(self,query_size,key_size,value_size,num_hiddens,num_heads,dropout,bias=False):super(MultiHeadAttention, self).__init__()self.num_heads=num_heads#用独立学习得到的 ℎ 组不同的线性投影(linear projections)来变换查询、键和值self.attention=d2l.torch.DotProductAttention(dropout)self.W_q=nn.Linear(query_size,num_hiddens,bias=bias)self.W_k=nn.Linear(key_size,num_hiddens,bias=bias)self.W_v = nn.Linear(value_size, num_hiddens, bias=bias)self.W_o = nn.Linear(num_hiddens, num_hiddens, bias=bias)
#总之就是:我们的Q,K,V的embedding,怎么拆分成k个头的数据,然后放到一个大头中,一遍算出multi_head的值
#这里是一组QKV乘一组W 直接生成特征大小的结果,在切分成8份,放到batch里等价于并行计算def forward(self,queries,keys,values,valid_lens):# print('----')# print(queries)queries = transpose_qkv(self.W_q(queries),self.num_heads)# print(queries)# print('----')keys = transpose_qkv(self.W_k(keys),self.num_heads)values = transpose_qkv(self.W_v(values),self.num_heads)if valid_lens is not None:# 在轴0,将第一项(标量或者矢量)复制num_heads次,# 然后如此复制第二项,然后诸如此类。valid_lens = torch.repeat_interleave(valid_lens,repeats=self.num_heads,dim=0)#valid_lens tensor([3, 3, 3, 3, 3, 2, 2, 2, 2, 2])# output的形状:(batch_size*num_heads,查询的个数,# num_hiddens/num_heads)# print(queries.shape)# print(keys.shape)'''queries-->torch.Size([10, 4, 20])keys----->torch.Size([10, 6, 20])两个批次,每次五个多头注意力,就一共会有十个注意力需要做。得出的矩阵为10*4*6,表示为10次注意力每个注意力query和key的矩阵为4*6keys keys keys keys keys keysQueryQueryQueryQuery在经过mask时 需要将10*4*6的矩阵,转为二维矩阵,就是40*6。valid_lens首先会在上面的代码中,扩展至num_heads,然后会在masked_softmax中扩至40大小。'''output = self.attention(queries,keys,values,valid_lens)# print('-----')# print(output)# print('-----')# output_concat的形状:(batch_size,查询的个数,num_hiddens)output_concat = transpose_output(output,self.num_heads)# print(output_concat.shape)torch.Size([2, 4, 100])return self.W_o(output_concat)
def transpose_qkv(X,num_heads):# 2,6,100 2,4,100X = X.reshape(X.shape[0],X.shape[1],num_heads,-1)# 2,5,6,20 2,5,4,20# 输出X的形状: (batch_size,num_heads,查询或者“键-值”对的个数, num_hiddens/num_heads)X = X.permute(0, 2, 1, 3)#最终输出的形状: (batch_size * num_heads,查询或者“键-值”对的个数,num_hiddens/num_heads)#10,6,20 10,6,20return X.reshape(-1, X.shape[2], X.shape[3])
def transpose_output(X,num_heads):"""逆转transpose_qkv函数的操作"""X = X.reshape(-1,num_heads,X.shape[1],X.shape[2])X = X.permute(0,2,1,3)return X.reshape(X.shape[0],X.shape[1],-1)
#在这里,我们设置head为5,也就是一共有5次self-attention。
num_hiddens,num_heads = 100,5
multiHeadAttention = MultiHeadAttention(num_hiddens,num_hiddens,num_hiddens,num_hiddens,5,0.5)
multiHeadAttention.eval()batch_size,num_queries = 2,4
num_kvpairs,valid_lens = 6,torch.tensor([3,2])
#2,6,100 批次 句子长度 embedsize
Y = torch.ones(size=(batch_size,num_kvpairs,num_hiddens))
#2,4,100
X = torch.ones(size=(batch_size,num_queries,num_hiddens))
print(multiHeadAttention(X,Y,Y,valid_lens).shape)
首先我们设置head为5。num_hiddens可以理解为query或者key的大小,num_kvpairs表示每次注意力中key的数量,num_queries表示每次注意力中query的数量。value的数量与key的数量一样。另外一种理解就是,将X理解为批次*句子长度(单词的数量)*embedding size。每个单词对应一次查询。随后就是__init__
,创建几个全连接层,对query、key、value进行变换,不同注意力的query、key和value,均不一样。
主要是实现图中红色部分。然后会调用forward函数,transpose_qkv
函数进行切分,假定原本的输入为2 * 6 * 100,因为大小为两个批次,每个批次需要做五个注意力机制,每个注意力机制的key的数量为6,所以将输入为2 * 6 * 100,转换为10 * 6 * 20。意思就是10次注意力,每个注意力中的key为6个,每个key由20维度的向量表示。query同理。因为我们要并行计算,这样使用torch.bmm
可以直接进行计算,计算得出Query和key矩阵。在上面的例子中,计算得出的为10 * 4 * 6大小的矩阵。
在训练时刻的mask中,首先会将结果转变为二维矩阵40 * 6,其中的每一行代表了query与不同key计算的结果,有时候query只能和部分key进行计算,比如:第二个词的query只能计算第一个词与第二个词的key,而之后key需要进行mask。我们会给定一个valid_lens
代表需要保留的计算结果。其中mask部分会调用以下代码:
mask = torch.arange((maxlen), dtype=torch.float32,device=X.device)[None, :] < valid_len[:, None]X[~mask] = value#value为极小值。
torch.arange((maxlen)
会生成从0到5的矩阵,valid_len在之前会经过扩展为140大小的矩阵,然后转换为40 * 1的矩阵。最终的mask会变成40 * 6大小矩阵就像以下形式:
[True,True,True,False,False]
而最后两个False是需要进行mask的,X[~mask] = value
将最后两个Fasle,变为负极小值,再经过softmax之后,结果将趋近于0,从而将其mask。然后与value相乘,得出结果为10 * 4 20矩阵大小的结果,在经过变换,变为2 * 4* 100矩阵,最后再经过最后一次全连接层,然后输出结果。
class PositionalEncoding(nn.Module):"""位置编码"""def __init__(self,num_hiddens,dropout,max_len=1000):super(PositionalEncoding,self).__init__()self.dropout = nn.Dropout(dropout)# 创建一个足够长的Pself.P = torch.zeros(size=(1,max_len,num_hiddens))X = torch.arange(max_len,dtype=torch.float32).reshape(-1,1)/torch.pow(1000,torch.arange(0,num_hiddens,2,dtype=torch.float32)/num_hiddens)self.P[:,:,0::2] = torch.sin(X)self.P[:,:,1::2] = torch.cos(X)def forward(self,X):X = X+self.P[:,:X.shape[1],:].to(X.device)return self.dropout(X)
主要实现位置编码,将基于正弦函数和余弦函数的固定位置编码公式进行实现,X就是将公式进行实现,P的大小为批次 * 输入模型单词可能最多的数量 * 每个单词的embedding size。我们可以假设P的大小为1 * 1000 * 32的矩阵,其中1000代表网络一次最多输入1000个词,每个词使用32维度向量表示。生成的X,是一个1000 * 16大小的矩阵,其中每一行的数值均不相同。P中每一行的偶数位置数据是由torch.sin(X)
来生成的,奇数位置数据由torch.cos(X)
生成。这样位置编码已经提前生成好了,在需要进行位置编码的时候,直接拿取前多少行,就行了。
class EncoderBlock(nn.Module):"""transformer编码器块"""
#EncoderBlock(query_size=24, key_size=24, value_size=24, num_hiddens=24, normalized_shape=[100, 24],ffn_num_inputs=24, ffn_num_hiddens=48, num_heads=8, dropout=0.5, use_bias=False)def __init__(self,query_size,key_size,value_size,num_hiddens,normalized_shape,ffn_num_inputs,ffn_num_hiddens,num_heads,dropout,use_bias=False):super(EncoderBlock,self).__init__()self.multihead_attention = MultiHeadAttention(key_size,query_size,value_size,num_hiddens,num_heads,dropout,use_bias)self.addnorm1 = AddNorm(normalized_shape,dropout)self.ffn = PositionWiseFFN(ffn_num_inputs,ffn_num_hiddens,num_hiddens)self.addnorm2 = AddNorm(normalized_shape,dropout)def forward(self,X,valid_lens):Y = self.addnorm1(X,self.multihead_attention(X,X,X,valid_lens))return self.addnorm2(Y,self.ffn(Y))
EncoderBlock对一个encoderBlock进行实现。先后经过,多头注意力机制,残差和规范化,前馈神经网络,残差和规范化,最后将结果输出。
class TransformerEncoder(d2l.torch.Encoder):"""transformer编码器"""def __init__(self,vocab_size,query_size,key_size,value_size,num_hiddens,normalized_shape,ffn_num_inputs,ffn_num_hiddens,num_heads,num_layers,dropout,use_bias=False):super(TransformerEncoder,self).__init__()self.num_hiddens = num_hiddensself.embedding = nn.Embedding(vocab_size,num_hiddens)self.positionalEncoding = d2l.torch.PositionalEncoding(num_hiddens,dropout)self.encoder_blocks = nn.Sequential()for i in range(num_layers):self.encoder_blocks.add_module(f'encoder_block{i}',EncoderBlock(query_size,key_size,value_size,num_hiddens,normalized_shape,ffn_num_inputs,ffn_num_hiddens,num_heads,dropout,use_bias=use_bias))def forward(self, X,valid_lens, *args):。X = self.positionalEncoding(self.embedding(X)*math.sqrt(self.num_hiddens))self.attention_weights = [None]*len(self.encoder_blocks)for i,encoder_block in enumerate(self.encoder_blocks):X = encoder_block(X,valid_lens)self.attention_weights[i] = encoder_block.multihead_attention.attention.attention_weightsreturn X
transformer编码器,对encoder进行堆叠,self.embedding(X)*math.sqrt(self.num_hiddens
主要因为embedding的值相对于位置编码比较小,乘以math.sqrt(self.num_hiddens,使得值与位置编码的值,差不多大小。
class DecoderBlock(nn.Module):"""解码器中第i个块"""#decoder_block = DecoderBlock(24,24,24,24,[100,24],24,48,8,0.5,0,use_bias=False)def __init__(self, query_size, key_size, value_size, num_hiddens, normalized_shape, ffn_num_inputs, ffn_num_hiddens,num_heads, dropout, i, use_bias=False):super(DecoderBlock, self).__init__()self.i = i # i表示这是第i个DecoderBlock块self.mask_multihead_attention1 = MultiHeadAttention(key_size, query_size, value_size, num_hiddens,num_heads, dropout, bias=use_bias)self.addnorm1 = AddNorm(normalized_shape, dropout)self.mutilhead_attention2 = MultiHeadAttention(key_size, query_size, value_size, num_hiddens,num_heads, dropout, bias=use_bias)self.addnorm2 = AddNorm(normalized_shape, dropout)self.ffn = PositionWiseFFN(ffn_num_inputs, ffn_num_hiddens, num_hiddens)self.addnorm3 = AddNorm(normalized_shape, dropout)def forward(self, X, state):enc_outputs, enc_valid_lens = state[0], state[1]# 训练阶段,输出序列的所有词元都在同一时间处理,# 因此state[2][self.i]初始化为None。# 预测阶段,输出序列是通过词元一个接着一个解码的,# 因此state[2][self.i]包含着直到当前时间步第i个块解码的输出表示# 训练时,由于每次都需要调用init_state函数,因此重新训练一个batch时,state[2]始终是一个None列表,# 当测试时,由于每次根据当前时间步的词元预测下一个词元时都不会重新调用init_state()函数,# 不会重新初始化state,因此state[2]里面保存的是之前时间步预测出来的词元信息(存的是decoder每层第一个掩码多头注意力state信息)if state[2][self.i] is None:keys_values = Xelse:keys_values = torch.cat([state[2][self.i], X], dim=1)state[2][self.i] = keys_valuesif self.training:#[2, 100, 24]batch_size, num_step, _ = X.shape# 训练时执行当前时间步的query时只看它前面的keys,values,不看它后面的keys,values。# 因为预测时是从左往右预测的,右边还没有预测出来,因此右侧的keys是没有的,看不到右侧的keys;# 训练时预测当前时间步词元能看到后面的目标词元,因此需要dec_valid_lens# dec_valid_lens的开头:(batch_size,num_steps),# 其中每一行是[1,2,...,num_steps]dec_valid_lens = torch.arange(1, num_step + 1, device=X.device).repeat(batch_size, 1)print(dec_valid_lens)else:# 测试时预测当前时间步的词元只能看到之前预测出来的词元,后面还没预测的词元还看不到,因此dec_valid_lens可以不需要dec_valid_lens = None# 自注意力X2 = self.mask_multihead_attention1(X, keys_values, keys_values, dec_valid_lens)Y = self.addnorm1(X, X2)# 编码器-解码器注意力。# enc_outputs的开头:(batch_size,num_steps,num_hiddens)Y2 = self.mutilhead_attention2(Y, enc_outputs, enc_outputs, enc_valid_lens)Z = self.addnorm2(Y, Y2)return self.addnorm3(Z, self.ffn(Z)), state
区分主要在两点,就是训练的时候,会执行if state[2][self.i] is None:
,所以第一次多头注意力,输入的key和value,均为本身。而在预测阶段,第一次多头注意力输入的为之前生成
相关文章:

transformer代码注解
其中代码均来自李沐老师的动手学pytorch中。 class PositionWiseFFN(nn.Module):ffn_num_inputs 4ffn_num_hiddens 4ffn_num_outputs 8def __init__(self,ffn_num_inputs,ffn_num_hiddens,ffn_num_outputs):super(PositionWiseFFN,self).__init__()self.dense1 nn.Linear(ffn…...

【产品经理】高阶产品如何处理需求?(3方法论+2案例+1清单)
不管你是萌新小白,还是工作了几年的“老油条”,需求一直是产品经理工作的重点。只不过,不同年限的产品经理需要面对的需求大有不同,对能力的要求更高。 不知你是否遇过以下问题? 你接手一个项目后,不知从何…...

Neo4j数据库中导入CSV示例数据
本文简要介绍Neo4j数据库以及如何从CSV文件中导入示例数据,方便我们快速学习测试图数据库。首先介绍简单数据模型以及基本图查询概念,然后通过LOAD CSV命令导入数据,生成节点和关系。 环境准备 读者可以快速安装Neo4j Desktop,启…...

第四章 No.1树状数组的原理与使用
文章目录 应用问题原理树状数组练习题241. 楼兰图腾242. 一个简单的整数问题243. 一个简单的整数问题2244. 谜一样的牛 线段树的反面:树状数组原理复杂,实现简单 应用问题 支持两个操作:快速求前缀和任意地修改某个数,时间复杂度…...

mysql(五)主从配置
目录 前言 一、MySQL Replication概述 二、MySQL复制类型 三、部署MySQL主从异步复制 总结 前言 为了实现MySQL的读写分离,可以使用MySQL官方提供的工具和技术,如MySQL Replication(复制)、MySQL Group Replication(组…...

扫地机语音提示芯片,智能家居语音交互首选方案,WT588F02B-8S
智能家居已经成为现代家庭不可或缺的一部分,而语音交互技术正是智能家居的核心。在智能家居设备中,扫地机无疑是最受欢迎的产品之一。然而,要实现一个更智能的扫地机,需要一颗语音提示芯片,以提供高质量的语音交互体验…...

ChatGPT | 分割Word文字及表格,优化文本分析
知识库读取Word内容时,由于embedding切片操作,可能会出现表格被分割成多个切片的情况。这种切片方式可能导致“列名栏”和“内容栏”之间的Y轴关系链断裂,从而无法准确地确定每一列的数据对应关系,从而使得无法准确知道每一列的数…...

基于JavaSE的手机库存管理系统
1、项目背景 基于JavaSE完成如下需求: 功能需求: 1、查询库存量 2、可以修改库存中不同品牌手机的个数 3、退出系统 实现步骤: 1、把List当做库房 2、把手机存放在库房中 3、使用封装的方法区操作仓库中的手机 2、项目知识点 面向对象 集合…...
驱动开发 day4 (led灯组分块驱动)
//编译驱动(注意Makefile的编译到移植到开发板的内核) make archarm //清除编译生成文件 make clean //安装驱动 insmod mycdev.ko //卸载驱动 rmmod mycdev //编译fun.c 函数(用到交叉工具编译) arm-linux-gnueabihf-gcc fun.c head.h #ifndef __HEAD_H__ #define __HEAD_H__…...

electron dialog.showMessageBox使用案例
electron 版本:25.3.1 index.html <!DOCTYPE html> <html> <head><meta charset"UTF-8"><title>Hello World!</title><meta http-equiv"Content-Security-Policy" content"script-src self unsa…...

代码随想录算法训练营第二十二天 | 读PDF复习环节2
读PDF复习环节2 本博客的内容只是做一个大概的记录,整个PDF看下来,内容上是不如代码随想录网站上的文章全面的,并且PDF中有些地方的描述,是很让我疑惑的,在困扰我很久后,无意间发现,其网站上的讲…...
TimescaleDB时序数据库初识
注:本文翻译自https://legacy-docs.timescale.com/v1.7/introduction TimescaleDB是一个开源时间序列数据库,针对快速摄取和复杂查询进行了优化。它说的是“完整的SQL”,因此像传统的关系数据库一样易于使用,并且以以前为NoSQL数…...
Numpy-聚合函数
NumPy 提供了很多统计函数,用于从数组中查找最小元素,最大元素,百分位标准差和方差等。 函数名说明np.sum()求和np.prod()所有元素相乘np.mean()平均值np.std()标准差np.var()方差np.median()中位数np.power()幂运算np.sqrt()开方np.min()最小…...

企业博客资讯如何高效运营起来?
运营一个高效的企业博客资讯需要综合考虑多个因素,包括内容策划、发布频率、优化推广、互动反馈等。下面将从这些方面介绍如何高效运营企业博客资讯。 如何高效运营企业博客资讯 内容策划 首先,需要制定一个明确的内容策略。确定博客的定位和目标受众…...
跟我学c++中级篇——模板的继承
一、继承 面向对象编程有三个特点:封装、继承和多态。其中继承在其中起着承上启下的作用。一般来说,继承现在和组合的应用比较难区分,出于各种场景和目的,往往各有千秋。但目前主流的观点,一般是如果没有特殊情况&…...

需求分析案例:消息配置中心
本文介绍了一个很常见的消息推送需求,在系统需要短信、微信、邮件之类的消息推送时,边界如何划分和如何设计技术方案。 1、需求 一个系统,一般会区分多个业务模块,并拆分成不同的业务系统,例如一个商城的架构如下&am…...
自动化测试——环境
一、搭建环境 1、安装Slenium pip install selenium 2、安装浏览器驱动-》查询浏览器版本-》下载对应版本驱动-》在path路径中配置(浏览器更新需要重新下载) pip install webdriver -helper(自动化)python3.9以上 pip install 安…...

短视频矩阵营销系统技术开发者开发笔记分享
一、开发短视频seo抖音矩阵系统需要遵循以下步骤: 1. 确定系统需求:根据客户的需求,确定系统的功能和特点,例如用户注册登录、视频上传、视频浏览、评论点赞等。 2. 设计系统架构:根据系统需求,设计系统的…...
vue2和vue3引用ueditor的区别
官方文档入口 UEditor Docs vue2使用方式 UE.vue组件 <template><div><script id"editor" type"text/plain"></script><Upload v-if"isupload" :config"{total:9}" :isupload"isupload" ret…...

【每日运维】RockyLinux8非容器化安装Mysql、Redis、RabitMQ单机环境
系统版本:RockyLinux 8.6 安装方式:非容器化单机部署 安装版本:mysql 8.0.32 redis 6.2.11 rabbitmq 3.11.11 elasticsearch 6.7.1 前置条件:时间同步、关闭selinux、主机名、主机解析host 环境说明:PC电脑VMware Work…...
Linux链表操作全解析
Linux C语言链表深度解析与实战技巧 一、链表基础概念与内核链表优势1.1 为什么使用链表?1.2 Linux 内核链表与用户态链表的区别 二、内核链表结构与宏解析常用宏/函数 三、内核链表的优点四、用户态链表示例五、双向循环链表在内核中的实现优势5.1 插入效率5.2 安全…...

label-studio的使用教程(导入本地路径)
文章目录 1. 准备环境2. 脚本启动2.1 Windows2.2 Linux 3. 安装label-studio机器学习后端3.1 pip安装(推荐)3.2 GitHub仓库安装 4. 后端配置4.1 yolo环境4.2 引入后端模型4.3 修改脚本4.4 启动后端 5. 标注工程5.1 创建工程5.2 配置图片路径5.3 配置工程类型标签5.4 配置模型5.…...
解锁数据库简洁之道:FastAPI与SQLModel实战指南
在构建现代Web应用程序时,与数据库的交互无疑是核心环节。虽然传统的数据库操作方式(如直接编写SQL语句与psycopg2交互)赋予了我们精细的控制权,但在面对日益复杂的业务逻辑和快速迭代的需求时,这种方式的开发效率和可…...

linux arm系统烧录
1、打开瑞芯微程序 2、按住linux arm 的 recover按键 插入电源 3、当瑞芯微检测到有设备 4、松开recover按键 5、选择升级固件 6、点击固件选择本地刷机的linux arm 镜像 7、点击升级 (忘了有没有这步了 估计有) 刷机程序 和 镜像 就不提供了。要刷的时…...
【ROS】Nav2源码之nav2_behavior_tree-行为树节点列表
1、行为树节点分类 在 Nav2(Navigation2)的行为树框架中,行为树节点插件按照功能分为 Action(动作节点)、Condition(条件节点)、Control(控制节点) 和 Decorator(装饰节点) 四类。 1.1 动作节点 Action 执行具体的机器人操作或任务,直接与硬件、传感器或外部系统…...
【服务器压力测试】本地PC电脑作为服务器运行时出现卡顿和资源紧张(Windows/Linux)
要让本地PC电脑作为服务器运行时出现卡顿和资源紧张的情况,可以通过以下几种方式模拟或触发: 1. 增加CPU负载 运行大量计算密集型任务,例如: 使用多线程循环执行复杂计算(如数学运算、加密解密等)。运行图…...

HashMap中的put方法执行流程(流程图)
1 put操作整体流程 HashMap 的 put 操作是其最核心的功能之一。在 JDK 1.8 及以后版本中,其主要逻辑封装在 putVal 这个内部方法中。整个过程大致如下: 初始判断与哈希计算: 首先,putVal 方法会检查当前的 table(也就…...

Mysql中select查询语句的执行过程
目录 1、介绍 1.1、组件介绍 1.2、Sql执行顺序 2、执行流程 2.1. 连接与认证 2.2. 查询缓存 2.3. 语法解析(Parser) 2.4、执行sql 1. 预处理(Preprocessor) 2. 查询优化器(Optimizer) 3. 执行器…...
现有的 Redis 分布式锁库(如 Redisson)提供了哪些便利?
现有的 Redis 分布式锁库(如 Redisson)相比于开发者自己基于 Redis 命令(如 SETNX, EXPIRE, DEL)手动实现分布式锁,提供了巨大的便利性和健壮性。主要体现在以下几个方面: 原子性保证 (Atomicity)ÿ…...
MinIO Docker 部署:仅开放一个端口
MinIO Docker 部署:仅开放一个端口 在实际的服务器部署中,出于安全和管理的考虑,我们可能只能开放一个端口。MinIO 是一个高性能的对象存储服务,支持 Docker 部署,但默认情况下它需要两个端口:一个是 API 端口(用于存储和访问数据),另一个是控制台端口(用于管理界面…...