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…...

第一次后端复习整理(JVM、Redis、反射)
1. JVM 文章仅为自身笔记 详情查看一篇文章掌握整个JVM,JVM超详细解析!!! 1.1 什么是JVM jvm是Java虚拟机 1.2 Java文件的编译过程 程序员编写代码形成.java文件经过javac编译成.class文件再通过JVM的类加载器进入运行时数据…...

python的web学习(一)-初识django
文章目录 软件创建项目默认项目文件说明App的概念(应用)apps.py编写URL和视图函数对应关系【urls.py】编写视图函数【views.py】启动服务 软件 python下载 django下载 创建项目 django-admin startproject 文件名默认项目文件说明 项目名 manage.py(项目管理,启…...

JavaWeb+jsp+Tomcat的叮当书城项目
点击以下链接获取源码: https://download.csdn.net/download/qq_64505944/88123111?spm1001.2014.3001.5503 技术:ssm jsp JDK1.8 MySQL5.7 Tomcat8.3 源码数据库课程设计 功能:管理员与普通用户和超级管理员三个角色,管理员可…...

【嵌入式Linux系统开发】——系统移植概述
目录 🍉🍉一、什么是嵌入式系统 🍉🍉二、嵌入式系统操作 🍉🍉三、嵌入式Linux的特点 🍉🍉四、嵌入式系统的组成 1、硬件和软件 2、硬件层 3、中间层 4、软件层 5、 功能层与执…...

升讯威在线客服系统是如何实现对 IE8 完全完美支持的(怎样从 WebSocket 降级到 Http)【干货】
简介 升讯威在线客服与营销系统是基于 .net core / WPF 开发的一款在线客服软件,宗旨是: 开放、开源、共享。努力打造 .net 社区的一款优秀开源产品。 完整私有化包下载地址 💾 https://kf.shengxunwei.com/freesite.zip 当前版本信息 发布…...

用VMware给运行在VMware上的CentOS7生成一个以SSH方式连接VMware上的CentOS7的运行在Windows上的命令行窗口
2023年7月27日,周四早上 目录 一个发现生成方法如果上面的方法连接失败,就采取这个方法 一个发现 今天早上无意间发现VMware可以生成一个以SSH方式连接着CentOS7的Windows命令行窗口, 这样做可以带来一定的便利性 : 方便复制、…...

C语言基础-3
1、函数 函数是C语言代码的基本组成部分,它是一个小的模块,整个程序由很多个功能独立的模块(函数)组成。这就是程序设计的基本分化方法。 main:C语言中所谓的主函数,主函数就是一种特别的函数。特别之处在于…...

Python 编程规范进阶(1) | 命名规范
养成良好的开发、编程习惯 跟着google开源项目走 https://github.com/google/styleguide 近期Target: 命名规范; Pythonic 积累 按照需求写需要的API; 写前先动脑子,比如画流程图,测试接口; Google 推荐的P…...

算法----二叉搜索树中第K小的元素
题目 二叉搜索树中第K小的元素 给定一个二叉搜索树的根节点 root ,和一个整数 k ,请你设计一个算法查找其中第 k 个最小元素(从 1 开始计数)。 示例 1: 输入:root [3,1,4,null,2], k 1 输出ÿ…...

阿里Java开发手册~安全规约
1. 【强制】隶属于用户个人的页面或者功能必须进行权限控制校验。 说明: 防止没有做水平权限校验就可随意访问、修改、删除别人的数据,比如查看他人的私信 内容、修改他人的订单。 2. 【强制】用户敏感数据禁止直接展示,必须对展示数据进…...