[连载]Transformer架构详解

news2025/4/16 10:30:32

Transformer: Attention Is All You Need

Paper 地址:https://arxiv.org/abs/1706.03762
Paper 代码:https://github.com/tensorflow/tensor2tensor
Paper 作者:Ashish Vaswani,Noam Shazeer,Niki Parmar,Jakob Uszkoreit,Llion Jones,Aidan N. Gomez,Lukasz Kaiser,Illia Polosukhin
Paper 信息:NeurlPS 2017

在这里插入图片描述


1.1 Transformer的诞生与发展史

2017年,Google发出一篇论文《Attention is All You Need》,提出了transformer模型。它彻底改变了自然语言处理 (NLP) 领域,并在机器翻译、文本生成、文本分类等任务中取得了显著的成果。

2018年10月,Google发出一篇论文《BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding》, BERT模型横空出世, 并横扫NLP领域11项任务的最佳成绩!

而 Bert中发挥重要作用的结构就是Transformer,之后各种模型相继出现,如roBert,GPT等,它们围绕Transformer进行了各自改造,但本质上其实还是沿用的Transformer的核心架构,可以说Transformer是目前大模型发展的奠基石!


1.2 Abstract

Why Transformer?

Transformer的成功依赖于其架构中的多头-自注意力机制层(Mulit-Head Self-Attention),它是一种基于自注意力机制(Self-Attention)序列到序列 (sequence-to-sequence) 的深度学习模型,最早由Vaswani等人在2017年的论文**《Attention is All You Need》**中提出。旨在解决自然语言处理(NLP)中的序列到序列(Seq2Seq)问题,如机器翻译等任务。

Transformer完全摒弃了递归(RNN)和卷积(CNN),从而避免了大量的冗余计算,在这一全新架构中,Transformer完全依赖于自注意力机制,并摒弃了序列化计算过程,允许模型并行处理整个输入序列,因此具有更高的效率和更强的性能。

注意力机制是Transformer模型的核心。它可以让模型在处理序列中的每个位置时,关注序列中其他位置的信息。 这意味着模型可以根据当前任务动态地调整每个位置的重要性,从而更好地捕捉序列中的长距离依赖关系。

1.3 Transformer的模型架构

在这里插入图片描述

1.3.0 模型架构粗略解析

对图中的网络进行拆解,大体上可以分成下面几个部分

  • 输入部分
    • 输入嵌入:将输入的词或子词转换为固定维度的向量表示。
    • 位置编码:由于Transformer不使用循环或卷积结构,无法直接捕捉序列的顺序信息,因此通过位置编码为输入添加位置信息。位置编码通常使用正弦和余弦函数来生成,并与输入的嵌入向量相加。

在这里插入图片描述

  • 输出部分
    • 线性变换:将解码器的输出映射到词汇表大小的维度。
    • Softmax激活函数:生成每个词的概率分布。

在这里插入图片描述

  • 编码器部分

    编码器部分由N个相同的编码器层堆叠而成,每个编码器层都包含以下几个部分

    • 多头自注意力机制:
      • 这是Transformer的核心组件。自注意力机制允许模型在处理一个token时,同时关注输入序列中的所有其他 token,并计算它们之间的相关性权重。
      • 多头机制将输入映射到多个不同的子空间,并行计算注意力,从而增强模型的表达能力。
    • 前馈神经网络:
      • 每个注意力层的输出都会经过一个前馈神经网络 (通常是两层全连接层) 进行非线性变换,进一步提取特征。
    • 残差连接:
      • 为了缓解深层网络中的梯度消失问题,每个子层(注意力层和前馈层)的输入会与该层的输出相加,形成残差连接。
    • 规范化层(层归一化):
      • 在每个子层的输入和输出之间进行层归一化,可以加速训练并提高模型稳定性。

在这里插入图片描述

  • 解码器部分

    解码器部分同样由N个相同的编码器层堆叠而成,每个解码器层都包含以下几个部分

    • 掩码多头自注意力机制:

      • 与编码器中的自注意力类似,但加入了掩码机制,确保模型在生成每个token时,只能关注到该token之前的 token。
      • 这种掩码机制保证了解码过程的自回归性,即模型按照顺序逐步生成输出序列。
    • 多头编码器-解码器注意力机制:

      • 解码器还需要关注编码器的输出,从而将编码器的语义信息融入到解码过程。
      • 该注意力机制的查询 (Query) 来自解码器的上一层输出,键 (Key) 和值 (Value) 来自编码器的输出。
    • 前馈神经网络、残差连接和规范化层(层归一化): 与编码器类似。

在这里插入图片描述


1.3.1 输入部分实现

输入部分包含:

  • 编码器源文本嵌入层及其位置编码器
  • 解码器目标文本嵌入层及其位置编码器

在这里插入图片描述

除了使用 Token Embedding对序列中的元素进行嵌入,Transformer还引入了了位置编码(Positional Encoding),使得模型总体的输入满足:
在这里插入图片描述

这里的Inputs并非指编码器层的输入,而是代表编码器和解码器的输入
Token_Embedding就是普通的词嵌入操作

“we add “positional encodings” to the input embeddings at the bottoms of the encoder and decoder stacks.
The positional encodings have the same dimension dmodel as the embeddings, so that the two can be summed. There are many choices of positional encodings, learned and fixed”

Tips:在实际工业场景中,将Token_Embedding和Positional_Embedding拼接起来的效果会更好


Token_Embeding(文本嵌入层)

Token_Embedding的实现与nn.Embedding无异,只不过在返回值和权重初始化上有所区别

nn.Embedding的前向计算过程

embedding module 的前向过程其实是一个索引(查表)的过程
表的形式是一个 matrix(embedding.weight, learnable parameters)

matrix.shape: (v, h)
v:vocabulary size=num_embedding
h:hidden dimension=embedding_dim

仅从数学的角度来说(方便推导模型),具体索引的过程,可以通过 one hot + 矩阵乘法的形式实现的

input.shape: (b, s)
> b:batch size
> s:seq len

当执行下行代码时,会进行如下计算 
 embed = embedding(input) 

> input.shape(b,s) 	e.g [[0, 2, 2,1]]
> 最终的维度变化情况:(b, s) ==> (b, s, h)

1.(b, s) 经过 one hot => (b, s, v)
inputs: [[0, 2, 2, 1 , 1]] 
inputs One-Hot: 数值分类(0-4 => 五分类) 0:[1,0,0,0,0]
    [[[1,0,0,0,0],
    [0,0,1,0,0],
    [0,0,1,0,0],
    [0,1,0,0,0],
    [0,1,0,0,0]]]
matrix(embedding.weight):
    [[ 1.0934,  1.7521, -1.9529, -1.0145,  0.5770],
    [-0.4371, -0.4270, -0.4908, -0.3988,  0.9695],
    [ 0.0000,  0.0000,  0.0000,  0.0000,  0.0000],
    [ 0.7268, -0.4491, -0.8089,  0.7516,  1.2716],
    [ 0.7785, -0.4336, -0.7542, -0.1953,  0.9711]]

2.(b, s, v) @ (v, h) ==> (b, s, h)

x(b, s, h):
    [[[ 1.0934,  1.7521, -1.9529, -1.0145,  0.5770],
    [ 0.0000,  0.0000,  0.0000,  0.0000,  0.0000],
    [ 0.0000,  0.0000,  0.0000,  0.0000,  0.0000],
    [-0.4371, -0.4270, -0.4908, -0.3988,  0.9695],
    [-0.4371, -0.4270, -0.4908, -0.3988,  0.9695]]]

本质上,embedding(input)是一个内存寻址的过程:

假设 inputs 是一个包含词索引的张量,i是数值化文本 inputs上的Token,weight 是嵌入矩阵。
	对于每个索引 i,嵌入向量 v_i 对应的计算过程是:
		v_i=embedding.weight[i]
nn.Embedding的反向传播过程

只有前向传播中用到的索引会接收梯度。

假如反向传播过来的梯度是 [0.1,0.1,0.3] ,原始的embedding矩阵= [[1. ,1. ,1.],[1. ,1. ,1.]] , lr=0.1

那么 反向传播以后embedding的参数就为 [[1. ,1. ,1.],[1. ,1. ,1.]] - 1 * [[0.1,0.1,0.3],[0.,0.,0.]]

即 [[0.99. ,0.99 ,0.97],[1. ,1. ,1.]]

Token Embedding的代码实现
class Embedding(nn.Module):
    def __init__(self, vocab_size, d_model):
        super(Embedding, self).__init__()
        
        self.vocab_size = vocab_size
        self.d_model = d_model
		
        self.embedding = nn.Embedding(num_embeddings=self.vocab_size, embedding_dim=self.d_model, padding_idx=0)
        nn.init.uniform_(self.embedding.weight,1/-math.sqrt(self.d_model), 1/math.sqrt(self.d_model))

    def forward(self, x):
        """
        将x传给self.embed并与根号下self.d_model相乘作为结果返回  x.shape-> (batch_size,seq_len)
        \n原因:
        词嵌入层的权重通常初始化较小(如均匀分布在[-0.1, 0.1]), 导致嵌入后的向量幅度较小。\n
        x经过词嵌入后乘以sqrt(d_model)来增大x的值, 与位置编码信息值量纲[-1,1]差不多, 确保两者相加时信息平衡。
        :param x: 词嵌入的样本
        :return:self.embedding(x) + math.sqrt(self.d_model)
        """
        # return self.embedding(x)
        return self.embedding(x) * math.sqrt(self.d_model)

Positional Embedding的实现

[前言] 为什么需要Positional Embedding(位置编码)?

因为在Transformer的编码器结构中, 并没有RNN那样针对词汇位置信息的处理能力,它无法直接感知输入数据中的顺序关系。

“Since our model contains no recurrence and no convolution, in order for the model to make use of the order of the sequence, we must inject some information about the relative or absolute position of the tokens in the sequence.

正弦位置编码(Sinusoidal)

在Transformer中,位置编码的公式是:
P E ( p o s , 2 i ) = s i n ( p o s 1000 0 2 i d m o d e l ) \Large \bold {PE_{(pos,2i)} = sin(\frac{pos}{10000^{\frac{2i}{d_{model}}}})} PE(pos,2i)=sin(10000dmodel2ipos)
P E ( p o s , 2 i + 1 ) = c o s ( p o s 1000 0 2 i d m o d e l ) \Large \bold {PE_{(pos,2i+1)} = cos(\frac{pos}{10000^{\frac{2i}{d_{model}}}})} PE(pos,2i+1)=cos(10000dmodel2ipos)

其中:

  • pos 是词在序列中的实际位置(例如第1个词为0,第2个词为1…)

  • i 是维度索引,每个词向量维度下标值

  • d m o d e l d_{model} dmodel 是模型的维度,也是词向量维度

三角函数与旋转变换矩阵

这个公式的神奇之处在于在序列中Token与Token之间的位置编码向量都可以通过使用旋转变换来相互转换,并且使用从小到大的波长可以模拟出类似于二进制位置编码的效果。

“ That is, each dimension of the positional encoding corresponds to a sinusoid.

The wavelengths form a geometric progression from 2π to 10000 · 2π.

We chose this function because we hypothesized it would allow the model to easily learn to attend byrelative positions,

since for any fixed offset k,$ PE_{pos+k}$ can be represented as a linear function of P E p o s PE_{pos} PEpos.

首先我们先介绍三角函数的变换公式

在这里插入图片描述

基于这个公式,我们可以推演出向量空间中一种常用的线性变换:对一个二维向量 [ s i n ( t + Δ t ) , c o s ( t + Δ t ) ] T [sin(t+Δt),cos(t+Δt)]^T [sin(t+Δt),cos(t+Δt)]T,可以由 [ s i n ( t ) , c o s ( t ) ] T [sin(t),cos(t)]^T [sin(t),cos(t)]T经过如下线性变换得到
在这里插入图片描述
其几何意义为一个二维单位向量 e ⋅ t a n ( t ) e \cdot tan(t) etan(t)在平面坐标轴上旋转了 Δ t ° Δt^° Δt°,因为任意一个模长为1的向量的直角坐标都可以由 [ s i n ( t ) , c o s ( t ) ] [sin(t),cos(t)] [sin(t),cos(t)]表示

同理此性质可以推广到N维空间中


接着我们来介绍不同位置上的位置编码向量是如何相互表示的

把三角函数的频率 1 1000 0 2 i d m o d e l \large {\frac{1}{10000^{\frac{2i}{d_{model}}}}} 10000dmodel2i1记为 w i , i = 0 , 1... , d m o d e l 2 ; w i ∈ [ 1 10000 , 1 ] w_{i} ,i=0,1...,\frac{d_{model}}{2} ;w_{i}∈[\frac{1}{10000},1] wi,i=0,1...,2dmodel;wi[100001,1] ,则
P E ( p o s = t ) = [ s i n ( w 0 ⋅ t ) , c o s ( w 0 ⋅ t ) . . . , s i n ( w d m o d e l 2 ⋅ t ) , c o s ( w d m o d e l 2 ⋅ t ) ] PE(pos=t) = [sin(w_0·t),cos(w_0·t)...,sin(w_{\frac{d_{model}}{2}}·t),cos(w_{\frac{d_{model}}{2}}·t)] PE(pos=t)=[sin(w0t),cos(w0t)...,sin(w2dmodelt),cos(w2dmodelt)]

P E ( p o s = t + Δ t ) = [ s i n ( w 0 ⋅ ( t + Δ t ) ) , c o s ( w 0 ⋅ ( ( t + Δ t ) ) ) . . . , s i n ( w d m o d e l 2 ⋅ ( t + Δ t ) ) , c o s ( w d m o d e l 2 ⋅ ( t + Δ t ) ) ] \small PE(pos=t + Δt) = [sin(w_0·(t + Δt)),cos(w_0·((t + Δt)))...,sin(w_{\frac{d_{model}}{2}}·(t + Δt)),cos(w_{\frac{d_{model}}{2}}·(t + Δt))] PE(pos=t+Δt)=[sin(w0(t+Δt)),cos(w0((t+Δt)))...,sin(w2dmodel(t+Δt)),cos(w2dmodel(t+Δt))]

于是,我们可以利用旋转变换来表示 第t个Token与其他Token之间的位置关系,如下所示

在这里插入图片描述

位置编码的性质

  1. 三角编码的点积大小可以反应Token之间的距离
  2. 位置编码的点积是无向的

位置编码存在的问题: 进入attention层之后,内积的**距离意识(distance-aware)**的模式也遭到了破坏。

本部分参考:

Transformer学习笔记一:Positional Encoding(位置编码)

Transformer 位置编码汇总

漫谈 Transformer 中的绝对位置编码、相对位置编码和融合位置编码(旋转位置编码 RoPE)


同时,论文中也提到了作者曾经尝试使用其他形式的位置编码,比如可学习的位置编码。但后续选择使用正弦位置编码是因为它能够外拓到更长的序列长度中(对未登录的长序列样本具备更好的表现)

“We also experimented with using learned positional embeddings [8] instead, and found that the two versions produced nearly identical results (see Table 3 row (E)). We chose the sinusoidal version because it may allow the model to extrapolate to sequence lengths longer than the ones encountered during training

Positional Embedding的实现代码实现
# todo:定义位置编码类 `PositionalEncoding`
class PositionalEncoding(nn.Module):
    __doc__ = """
        位置编码器(Positional Encoding)是Transformer模型中的一个重要组成部分,用于在模型中引入序列中各个元素的位置或顺序信息。
        因为在Transformer的编码器结构中, 并没有RNN那样针对词汇位置信息的处理能力,它无法直接感知输入数据中的顺序关系。
        因此需要在Embedding层后加入位置编码器,将词汇位置不同可能会产生不同语义的信息加入到词嵌入张量中, 以弥补位置信息的缺失。
        位置编码器将序列位置(例如,单词在句子中的位置)转换为一组向量,这些向量会与输入的词嵌入(word embeddings)相加,使模型能够在处理每个词时,理解其在序列中的位置。
        位置编码通常使用正弦和余弦函数生成,使得模型可以区分不同位置的token。
            # 手动实现`transformer`中的位置编码层
                ## 实现思路
                    1.生成一个全0张量矩阵 PE(max_len, d_model)
                    2.计算位置编码矩阵
                        torch.sin(position / (10000 ** (_2i / self.d_model)))
                        torch.cos(position / (10000 ** (_2i / self.d_model)))
                    3. 将位置编码信息与传入的样本相加并返回
                        x = x + self.PE[:, :x.shape[1], :]
            """

    def __init__(self, d_model, dropout_p, max_len=5000):
        """
        :param d_model:词向量维度
        :param dropout_p:dropout正则化概率
        :param max_len:句子的最大长度
        """
        super(PositionalEncoding, self).__init__()
        # todo:0- 初始化参数
        self.d_model = d_model
        self.max_len = max_len
        self.dropout_p = dropout_p

        # todo:1- 定义`dropout`层
        self.dropout = nn.Dropout(p=self.dropout_p)

        """
        # 位置编码公式
            PE_{(pos,2i)} = sin(\frac{pos}{10000 ** (2i/d_model)}
            PE_{(pos,2i+1)} = cos(\frac{pos}{10000 ** (2i/d_model)}
        思路:位置编码矩阵 + 特征矩阵 相当于给特征增加了位置信息
        定义位置编码矩阵PE eg pe[60, 512], 位置编码矩阵和特征矩阵形状是一样的
        """
        # todo:2- 定义位置编码矩阵
        PE = torch.zeros(max_len, d_model)

        # todo:3- 定义位置列矩阵`position`
        # 数据形状[max_len,1] eg: [0,1,2,3,4...60]^T
        position = torch.arange(0, self.max_len).unsqueeze(1)

        # todo:4- 计算 `2i` => 词向量的位置索引
        _2i = torch.arange(0, self.d_model, step=2)
        # print('_2i->',_2i)

        # todo:5- 计算位置编码矩阵:方式一
        PE[:, 0::2] = torch.sin(position / (10000 ** (_2i / self.d_model)))
        PE[:, 1::2] = torch.cos(position / (10000 ** (_2i / self.d_model)))

        # # todo:5- 计算位置编码矩阵:方式二
        # div_term = torch.exp(-torch.arange(0, d_model, 2) / d_model * math.log(10000.0))
        # PE[:, 0::2] = torch.sin(position * div_term)
        # PE[:, 1::2] = torch.cos(position * div_term)

        # todo:6- 将二维的位置编码矩阵`PE`在0轴升维, 词向量是三维 =>(1,max_len, d_model)

        PE = PE.unsqueeze(0)
        # todo:7- 将结果保存缓存区, 后续使用直接按模型参数加载
        self.register_buffer('PE', PE)

        # print('PE->', self.PE.shape, self.PE)

    # todo: 定义`forward`前向传播函数
    def forward(self, x):
        """
        x.shape -> (batch_size,seq_len,d_model)
        :param x: x->词嵌入层后的词向量
        :return: x + self.PE[:, :x.shape[1], :] => 位置编码后的结果
        """
        # 第1个 `:` -> 句子数  eg:我爱你
        # `:x.shape[1]` -> 句子长度  :3
        # 第2个 `:` -> 位置编码信息
        x = x + self.PE[:, :x.shape[1], :]
        return self.dropout(x)

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

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

相关文章

LVGL Video控件和Radiobtn控件详解

LVGL Video控件和Radiobtn控件详解 一、 Video控件详解1. 概述2. 创建和初始化3. 基本属性设置4. 视频控制5. 回调函数6. 高级功能7. 注意事项 二、Radiobtn控件详解1. 概述2. 创建和初始化3. 属性设置4. 状态控制5. 组管理6. 事件处理7. 样式设置8. 注意事项 三、效果展示四、…

组合数哭唧唧

前言&#xff1a;手写一个简单的组合数&#xff0c;但是由于长期没写&#xff0c;导致一些细节没处理好 题目链接 #include<bits/stdc.h> using namespace std; #define endl "\n"#define int long longconst int N (int)2e510; const int Mod (int)1e97;int…

NLP高频面试题(四十二)——RAG系统评估:方法、指标与实践指南

1. 引言:RAG系统概述与评估挑战 检索增强生成(Retrieval-Augmented Generation,简称 RAG)是近年来自然语言处理领域的一个重要进展。RAG系统在大型语言模型生成文本的过程中引入了外部检索模块,从外部知识库获取相关信息,以缓解纯生成模型可能出现的幻觉和知识盲点。通过…

Linux路漫漫

目录 Vim模式 基本操作 文本编辑 更多功能 1. 直接启动 Vim 2. 打开一个已存在的文件 3. 打开多个文件 4. 以只读模式打开文件 5. 从指定行号开始编辑 6. 快速打开并执行命令 7. 检查是否安装了 Vim 8. 退出 Vim 前提条件 SCP 命令格式 具体操作 1. Windows 命…

游戏引擎学习第227天

今天的计划 今天的工作重点是进行吸引模式&#xff08;attract mode&#xff09;的开发&#xff0c;主要是处理游戏的进出和其他一些小的细节问题&#xff0c;这些是之前想要整理和清理的部分。我做了一些工作&#xff0c;将游戏代码中的不同部分分离到逻辑上独立的区域&#…

一键直达:用n8n打造谷歌邮箱到Telegram的实时通知流

欢迎来到我的博客&#xff0c;代码的世界里&#xff0c;每一行都是一个故事 &#x1f38f;&#xff1a;你只管努力&#xff0c;剩下的交给时间 &#x1f3e0; &#xff1a;小破站 一键直达&#xff1a;用n8n打造谷歌邮箱到Telegram的实时通知流 前言n8n的强大之处实现简便性实战…

【QT】 QT定时器的使用

QT定时器的使用 1. QTimer介绍&#xff08;1&#xff09;QTimer的使用方法步骤示例代码1&#xff1a;定时器的启动和关闭现象&#xff1a;示例代码2&#xff1a;定时器每隔1s在标签上切换图片现象&#xff1a; (2)实际开发的作用 2.日期 QDate(1)主要方法 3.时间 QTime(1)主要方…

【自动化测试】如何获取cookie,跳过登录的简单操作

前言 &#x1f31f;&#x1f31f;本期讲解关于自动化测试函数相关知识介绍~~~ &#x1f308;感兴趣的小伙伴看一看小编主页&#xff1a;GGBondlctrl-CSDN博客 &#x1f525; 你的点赞就是小编不断更新的最大动力 &#x1f386;那么废话…

每天五分钟深度学习PyTorch:RNN CELL模型原理以及搭建

本文重点 RNN Cell(循环神经网络单元)是循环神经网络(RNN)的核心组成部分,用于处理序列数据中的每个时间步,并维护隐藏状态以捕获序列中的时间依赖关系。 RNN CELL的结构 RNN是一个循环结构,它可以看作是RNN CELL的循环,RNN CELL的结构如下图所示,RNN CELL不断进行…

【基于开源insightface的人脸检测,人脸识别初步测试】

简介 InsightFace是一个基于深度学习的开源人脸识别项目,由蚂蚁金服的深度学习团队开发。该项目提供了人脸检测、人脸特征提取、人脸识别等功能,支持多种操作系统和深度学习框架。本文将详细介绍如何在Ubuntu系统上安装和实战InsightFace项目。 目前github有非常多的人脸识…

进程(完)

今天我们就补充一个小的知识点,查看进程树命令,来结束我们对linux进程的学习,那么话不多说,来看. 查看进程树 pstree 基本语法&#xff1a; pstree [选项] 优点&#xff1a;可以更加直观的来查看进程信息 常用选项&#xff1a; -p&#xff1a;显示进程的pid -u&#xff…

【控制学】控制学分类

【控制学】控制学分类 文章目录 [TOC](文章目录) 前言一、工程控制论1. 经典控制理论2. 现代控制理论 二、生物控制论三、经济控制论总结 前言 控制学是物理、数学与工程的桥梁 提示&#xff1a;以下是本篇文章正文内容&#xff0c;下面案例可供参考 一、工程控制论 1. 经典…

软考中级-软件设计师 2022年上半年下午题真题解析:通关秘籍+避坑指南

&#x1f4da; 目录&#xff08;快速跳转&#xff09; 大题&#xff08;下午题&#xff09;&#xff08;每题15分&#xff0c;共75分&#xff09;一、结构化分析与设计&#x1f354; 试题一&#xff1a;外卖订餐系统 二、数据库应用分析与设计&#x1f697; 试题二&#xff1a;…

波束形成(BF)从算法仿真到工程源码实现-第十节-非线性波束形成

一、概述 本节我们基于webrtc的非线性波束形成进行代码仿真&#xff0c;并对仿真结果进行展示和分析总结。更多资料和代码可以进入https://t.zsxq.com/qgmoN &#xff0c;同时欢迎大家提出宝贵的建议&#xff0c;以共同探讨学习。 二、仿真代码 2.1 常量参数 % *author : a…

《忘尘谷》音阶与调性解析

一、音高与音名的对应关系 根据搜索结果及音乐理论&#xff0c;结合《忘尘谷》的曲谱信息&#xff0c;其音阶与调性分析如下&#xff1a; 调性判定 原曲调性为 D调&#xff08;原曲标注为D调&#xff09;&#xff0c;但曲谱编配时采用 C调指法&#xff0c;通过变调夹夹2品&…

App测试小工具

前言 最近app测试比较多&#xff0c;每次都得手动输入日志tag&#xff0c;手动安装&#xff0c;测完又去卸载&#xff0c;太麻烦。就搞了小工具使用。 效果预览 每次测试完成&#xff0c;点击退出本次测试&#xff0c;就直接卸载了&#xff0c;usb插下一个手机又可以继续测了…

数智读书笔记系列029 《代数大脑:揭秘智能背后的逻辑》

《代数大脑:揭秘智能背后的逻辑》书籍简介 作者简介 加里F. 马库斯(Gary F. Marcus)是纽约大学心理学荣休教授、人工智能企业家,曾创立Geometric Intelligence(后被Uber收购)和Robust.AI公司。他在神经科学、语言学和人工智能领域发表了大量论文,并著有《重启AI》等多部…

Apache Kafka UI :一款功能丰富且美观的 Kafka 开源管理平台!!

Apache Kafka UI 是一个免费的开源 Web UI&#xff0c;用于监控和管理 Apache Kafka 集群&#xff0c;可方便地查看 Kafka Brokers、Topics、消息、Consumer 等情况&#xff0c;支持多集群管理、性能监控、访问控制等功能。 1 特征 多集群管理&#xff1a; 在一个地方监控和管理…

临床协调简历模板

模板信息 简历范文名称&#xff1a;临床协调简历模板&#xff0c;所属行业&#xff1a;其他 | 职位&#xff0c;模板编号&#xff1a;C1S3WO 专业的个人简历模板&#xff0c;逻辑清晰&#xff0c;排版简洁美观&#xff0c;让你的个人简历显得更专业&#xff0c;找到好工作。希…

【第45节】windows程序的其他反调试手段上篇

目录 引言 一、通过窗口类名和窗口名判断 二、检测调试器进程 三、父进程是否是Explorer 四、RDTSC/GetTickCount时间敏感程序段 五、StartupInfo结构的使用 六、使用BeingDebugged字段 七、 PEB.NtGlobalFlag,Heap.HeapFlags,Heap.ForceFlags 八、DebugPort:CheckRem…