深入解析 PaddleSpeech T2S 多注意力头模块:paddlespeech.t2s.modules.transformer.attention 源码详解
2026/9/24 19:58:10 网站建设 项目流程

深入解析 PaddleSpeech T2S 多注意力头模块:paddlespeech.t2s.modules.transformer.attention 源码详解

【免费下载链接】PaddleSpeechEasy-to-use Speech Toolkit including Self-Supervised Learning model, SOTA/Streaming ASR with punctuation, Streaming TTS with text frontend, Speaker Verification System, End-to-End Speech Translation and Keyword Spotting. Won NAACL2022 Best Demo Award.项目地址: https://gitcode.com/paddlepaddle/PaddleSpeech

导读

本文围绕 PaddleSpeech 语音合成(T2S)子系统中 Transformer/Conformer 架构的核心组件——paddlespeech.t2s.modules.transformer.attention模块展开。该模块实现了三类多头注意力层:标准的多头自注意力MultiHeadedAttention、支持相对位置编码的新版RelPositionMultiHeadedAttention与旧版LegacyRelPositionMultiHeadedAttention。读完本文,你将掌握这些注意力层的构造参数、前向计算流程、掩码处理机制,以及它们在 PaddleSpeech 的 Transformer/Conformer 编码器、解码器中的实际装配位置与配置文件对应关系。

一、模块定位:T2S Transformer/Conformer 的注意力核心

在 PaddleSpeech 的 TTS 技术栈中,paddlespeech/t2s/modules/transformer/目录下存放着完整的 Transformer 相关实现,包括编码器(encoder.py)、解码器(decoder.py)、位置编码(embedding.py)等。而 attention.py 正是其中定义多头注意力层的核心文件,从源码头部注释可以看到,该实现源自 ESPnet 并针对 PaddlePaddle 框架进行了移植改写("Modified from espnet")。

该模块对外暴露三个可实例化的注意力层类,构成完整的注意力家族:

类名特点适用场景
MultiHeadedAttention标准 scaled dot-product 多头注意力Transformer 编码器selfattn、解码器自注意力与源注意力
RelPositionMultiHeadedAttention新版相对位置编码注意力(shift trick)Conformer 编码器rel_selfattn
LegacyRelPositionMultiHeadedAttention旧版相对位置编码注意力兼容旧式实现

二、MultiHeadedAttention:标准缩放点积多头注意力

MultiHeadedAttention是模块中最基础的类,继承自paddle.nn.Layer,实现标准的多头注意力计算。

2.1 构造参数与内部结构

def __init__(self, n_head, n_feat, dropout_rate): super().__init__() assert n_feat % n_head == 0 self.d_k = n_feat // n_head self.h = n_head self.linear_q = nn.Linear(n_feat, n_feat, bias_attr=True) self.linear_k = nn.Linear(n_feat, n_feat, bias_attr=True) self.linear_v = nn.Linear(n_feat, n_feat, bias_attr=True) self.linear_out = nn.Linear(n_feat, n_feat, bias_attr=True) self.attn = None self.dropout = nn.Dropout(p=dropout_rate)

三个核心构造参数的含义如下:

  • n_head(int):注意力头数,即论文中的 h。
  • n_feat(int):特征维度(d_model),必须能被n_head整除(源码通过assert n_feat % n_head == 0强制校验),每个头的维度d_k = n_feat // n_head。源码注释特别说明 "We assume d_v always equals d_k",即键与值的维度保持一致。
  • dropout_rate(float):注意力权重矩阵上的 dropout 比例。

类内部定义了 4 个线性变换层:linear_qlinear_klinear_v分别将 query、key、value 从n_feat维投影到n_feat维,linear_out负责将注意力输出拼回并投影回n_feat维。self.attn属性会保存最近一次计算的注意力权重矩阵(形状为#batch, n_head, time1, time2),便于调试与可视化。self.dropout则对归一化后的注意力权重施加 dropout。

2.2 forward_qkv:Q/K/V 的投影与分头

forward_qkv将输入的三元组(query, key, value)完成线性投影、reshape 分头和维度转置:

def forward_qkv(self, query, key, value): n_batch = paddle.shape(query)[0] q = paddle.reshape(self.linear_q(query), [n_batch, -1, self.h, self.d_k]) k = paddle.reshape(self.linear_k(key), [n_batch, -1, self.h, self.d_k]) v = paddle.reshape(self.linear_v(value), [n_batch, -1, self.h, self.d_k]) q = q.transpose((0, 2, 1, 3)) # (batch, head, time1, d_k) k = k.transpose((0, 2, 1, 3)) # (batch, head, time2, d_k) v = v.transpose((0, 2, 1, 3)) # (batch, head, time2, d_k) return q, k, v

注意各张量的形状变化:输入 query 为(#batch, time1, size),key/value 为(#batch, time2, size)(time1 与 time2 可不等长,例如编码器输出序列与解码器输入序列长度不同)。经线性投影后 reshape 为(batch, time, head, d_k),再转置为(batch, head, time, d_k),从而将不同的头放到独立的维度上进行并行批量计算。

2.3 forward_attention:掩码、Softmax 与加权求和

forward_attention完成注意力得分的掩码处理、归一化和加权聚合:

def forward_attention(self, value, scores, mask=None): n_batch = paddle.shape(value)[0] softmax = paddle.nn.Softmax(axis=-1) if mask is not None: mask = mask.unsqueeze(1) mask = paddle.logical_not(mask) dtype = str(scores.dtype).split(".")[-1] min_value = float(numpy.finfo(dtype).min) scores = masked_fill(scores, mask, min_value) self.attn = softmax(scores) self.attn = masked_fill(self.attn, mask, 0.0) else: self.attn = softmax(scores) p_attn = self.dropout(self.attn) x = paddle.matmul(p_attn, value) x = paddle.reshape(x.transpose((0, 2, 1, 3)), (n_batch, -1, self.h * self.d_k)) return self.linear_out(x)

这里有几个关键实现细节:

  1. 掩码处理:传入的 mask 形状为(#batch, 1, time2)(#batch, time1, time2),经过unsqueeze(1)扩展维度后与 scores 广播。掩码中为True的位置表示"有效",因此先通过paddle.logical_not取反得到"需要屏蔽"的位置,再调用masked_fill将这些位置的得分填充为对应 dtype 的最小浮点值(numpy.finfo(dtype).min),使得这些位置在 softmax 后权重趋近于 0。
  2. 二次掩码清零:对 softmax 后的注意力权重再次masked_fill(..., 0.0),将被屏蔽位置的概率精确置 0,避免数值误差残留。
  3. 加权求和p_attn(施加 dropout 后的注意力权重)与valuepaddle.matmul,形状变化为(batch, head, time1, time2) × (batch, head, time2, d_k) → (batch, head, time1, d_k),随后转置并 reshape 回(batch, time1, d_model),最后经linear_out投影输出。

其中使用的masked_fill工具定义在 paddlespeech/t2s/modules/masked_fill.py:它通过broadcast_shape计算广播后的目标形状,将 mask 广播后经paddle.where完成条件填充,并显式设置mask.stop_gradient = True防止梯度流经掩码。注释中特别提到该写法是为了兼容 Paddle 动态图转静态图("comment following line for converting dygraph to static graph")。

2.4 forward:完整的缩放点积注意力

forward方法将上述子步骤串接为完整的计算链路:

def forward(self, query, key, value, mask=None): q, k, v = self.forward_qkv(query, key, value) scores = paddle.matmul(q, k.transpose((0, 1, 3, 2))) / math.sqrt(self.d_k) return self.forward_attention(v, scores, mask)

即:投影分头 →q与转置后的k做矩阵乘法 → 除以sqrt(d_k)完成缩放(防止点积过大导致 softmax 梯度消失)→ 进入forward_attention。这正是经典论文 "Attention Is All You Need" 中 scaled dot-product attention 的 Paddle 实现。

三、RelPositionMultiHeadedAttention:带相对位置编码的新版注意力

RelPositionMultiHeadedAttention继承自MultiHeadedAttention,在其基础上引入相对位置编码(论文 Self-Attention with Relative Position Representations,即 Transformer-XL 使用的相对位置编码方案),主要服务于 PaddleSpeech 的 Conformer 编码器。

3.1 新增参数与可学习偏置

def __init__(self, n_head, n_feat, dropout_rate, zero_triu=False): super().__init__(n_head, n_feat, dropout_rate) self.zero_triu = zero_triu self.linear_pos = nn.Linear(n_feat, n_feat, bias_attr=False) self.pos_bias_u = paddle.create_parameter( shape=(self.h, self.d_k), dtype='float32', default_initializer=paddle.nn.initializer.XavierUniform()) self.pos_bias_v = paddle.create_parameter( shape=(self.h, self.d_k), dtype='float32', default_initializer=paddle.nn.initializer.XavierUniform())

相比基类多出的部分:

  • zero_triu(bool):是否将注意力矩阵的上三角部分置零,用于因果(causal)场景。
  • linear_pos:对位置编码做的线性投影,bias_attr=False(不加偏置)。
  • pos_bias_u/pos_bias_v:两个形状为(n_head, d_k)的可学习偏置向量,分别用于论文 Section 3.3 中的矩阵 C(与 key 相关的偏置)和矩阵 D(与位置编码相关的偏置),使用 Xavier 均匀分布初始化。

3.2 forward:四矩阵分解的相对位置注意力

forward方法是该类的核心,输入多了一个pos_emb(相对位置编码张量,形状(#batch, 2*time1-1, size)),且 mask 为必填参数:

def forward(self, query, key, value, pos_emb, mask): q, k, v = self.forward_qkv(query, key, value) q = q.transpose([0, 2, 1, 3]) n_batch_pos = paddle.shape(pos_emb)[0] p = self.linear_pos(pos_emb).reshape([n_batch_pos, -1, self.h, self.d_k]) p = p.transpose([0, 2, 1, 3]) q_with_bias_u = (q + self.pos_bias_u).transpose([0, 2, 1, 3]) q_with_bias_v = (q + self.pos_bias_v).transpose([0, 2, 1, 3]) matrix_ac = paddle.matmul(q_with_bias_u, k.transpose([0, 1, 3, 2])) matrix_bd = paddle.matmul(q_with_bias_v, p.transpose([0, 1, 3, 2])) matrix_bd = self.rel_shift(matrix_bd) scores = (matrix_ac + matrix_bd) / math.sqrt(self.d_k) return self.forward_attention(v, scores, mask)

按照论文 Section 3.3 的分解方式,注意力得分被拆成四部分:

  • 矩阵 Aqk的点积(内容到内容的注意力);
  • 矩阵 Cpos_bias_uk的点积(内容到位置的偏置项);
  • 两者合并为matrix_ac = matmul(q + pos_bias_u, k^T)
  • 矩阵 Bq与位置投影p的点积(位置到内容的注意力);
  • 矩阵 Dpos_bias_v与位置投影p的点积(位置到位置的偏置项);
  • 两者合并为matrix_bd = matmul(q + pos_bias_v, p^T),再经rel_shift位移后与matrix_ac相加并除以sqrt(d_k)

3.3 rel_shift:相对位置的移位技巧

rel_shift实现了相对位置编码特有的"移位"操作,将(batch, head, time1, 2*time1-1)形状的得分矩阵转换为(batch, head, time1, time2)

def rel_shift(self, x): b, h, t1, t2 = paddle.shape(x) zero_pad = paddle.zeros((b, h, t1, 1)) x_padded = paddle.concat([zero_pad, x], axis=-1) x_padded = x_padded.reshape([b, h, t2 + 1, t1]) new_t = paddle.cast(paddle.floor(t2 / 2) + 1, dtype='int32') x = x_padded[:, :, 1:].reshape([b, h, t1, t2])[:, :, :, :new_t] if self.zero_triu: ones = paddle.ones((t1, t2)) x = x * paddle.tril(ones, t2 - t1)[None, None, :, :] return x

实现思路是:在最后一维前补一个零列,reshape 后裁剪首行,再截取前floor(t2/2)+1列,从而将相对位置索引对齐到绝对位置上。当zero_triu=True时,通过paddle.tril(ones, t2 - t1)构造下三角掩码相乘,屏蔽未来位置的信息(因果注意力)。

四、LegacyRelPositionMultiHeadedAttention:旧版实现的差异

LegacyRelPositionMultiHeadedAttention与新版的核心区别在于位置编码的输入形状与矩阵拼接方式:

  • 新版的pos_emb形状为(#batch, 2*time1-1, size)(正负位置拼接后经 shift trick 对齐);
  • 旧版的pos_emb形状为(#batch, time1, size),其rel_shift不执行:new_t的截取,而是直接 reshape 完成位移。
def rel_shift(self, x): b, h, t1, t2 = paddle.shape(x) zero_pad = paddle.zeros((b, h, t1, 1)) x_padded = paddle.concat([zero_pad, x], axis=-1) x_padded = paddle.reshape(x_padded, [b, h, t2 + 1, t1]) x = paddle.reshape(x_padded[:, :, 1:], [b, h, t1, t2]) if self.zero_triu: ones = paddle.ones((t1, t2)) x = x * paddle.tril(ones, t2 - t1)[None, None, :, :] return x

两者都依赖RelPositionalEncoding/LegacyRelPositionalEncoding生成位置编码(定义于 embedding.py)。其中新版RelPositionalEncoding会同时生成正向与负向位置编码(pe_positivepe_negative),拼接成2*len-1的长度以配合 shift trick;旧版则直接使用reverse=True的标准正弦位置编码。从代码注释可以看出两个版本的实现差异对应 ESPnet PR #2816 的讨论。

五、在编码器与解码器中的装配方式

5.1 编码器:Transformer 与 Conformer 的注意力选择

在 encoder.py 中,BaseEncoder.get_encoder_selfattn_layer方法根据selfattention_layer_type参数完成注意力层的选择:

if selfattention_layer_type == "selfattn": encoder_selfattn_layer = MultiHeadedAttention encoder_selfattn_layer_args = (attention_heads, attention_dim, attention_dropout_rate, ) elif selfattention_layer_type == "rel_selfattn": assert pos_enc_layer_type == "rel_pos" encoder_selfattn_layer = RelPositionMultiHeadedAttention encoder_selfattn_layer_args = (attention_heads, attention_dim, attention_dropout_rate, zero_triu, )

对应的get_pos_enc_class方法中,pos_enc_layer_type的可选值与位置编码类一一对应:

pos_enc_layer_type位置编码类配套selfattention_layer_type
abs_posPositionalEncodingselfattn
scaled_abs_posScaledPositionalEncodingselfattn
rel_posRelPositionalEncodingrel_selfattn(强制断言)

由此可以清晰看出两条链路:

  • Transformer 编码器TransformerEncoder):默认selfattention_layer_type="selfattn"pos_enc_layer_type="abs_pos",使用MultiHeadedAttention+ 绝对位置编码;
  • Conformer 编码器ConformerEncoder):默认selfattention_layer_type="rel_selfattn"pos_enc_layer_type="rel_pos",使用RelPositionMultiHeadedAttention+ 相对位置编码。

BaseEncoder.__init__中,注意力层会被传入EncoderLayer(定义于 encoder_layer.py),并由repeat工具按num_blocks堆叠。EncoderLayer.forward中的残差连接形态由concat_after控制:为False时执行x = residual + dropout(self_attn(x_q, x, x, mask));为True时先拼接输入与注意力输出再经concat_linear投影。此外EncoderLayer支持cache参数,仅对最后一帧计算 query,用于流式/增量推理。

5.2 解码器:自注意力与源注意力

在 decoder.py 的Decoder类中,MultiHeadedAttention承担两种角色:

  1. 解码器自注意力:当selfattention_layer_type="selfattn"时,每个DecoderLayer的自注意力由MultiHeadedAttention(attention_heads, attention_dim, self_attention_dropout_rate)构造,配合 mask.py 中的subsequent_mask(下三角掩码)防止解码器"看到未来";
  2. 编码器-解码器交叉注意力(源注意力):固定使用MultiHeadedAttention(attention_heads, attention_dim, src_attention_dropout_rate),以编码器输出memory作为 key/value、解码器当前状态作为 query。

DecoderLayer(定义于 decoder_layer.py)依次执行自注意力 → 交叉注意力 → 前馈网络三阶段,各自带有独立的 LayerNorm(norm1/norm2/norm3)与残差连接,同样支持concat_aftercacheDecoder.forward_one_stepscorebatch_score则为自回归解码(含 beam search)提供逐帧打分与缓存复用能力。

另外需要说明的是,解码器的目标序列掩码由target_mask生成:它把subsequent_mask与"非 padding 位置"掩码做按位与,从而同时屏蔽未来位置与 padding 位置。

六、配置文件中的真实参数对照

PaddleSpeech 的 TTS 示例(如 aishell3 的多说话人语音合成)中,注意力相关参数直接暴露在模型配置中。以 examples/aishell3/tts3/conf/conformer.yaml 为例:

model: adim: 384 # attention dimension aheads: 2 # number of attention heads elayers: 4 # number of encoder layers eunits: 1536 # number of encoder ff units dlayers: 4 # number of decoder layers dunits: 1536 # number of decoder ff units positionwise_layer_type: conv1d # type of position-wise layer positionwise_conv_kernel_size: 3 # kernel size of position wise conv layer encoder_normalize_before: True # whether to perform layer normalization before the input decoder_normalize_before: True # whether to perform layer normalization before the input reduction_factor: 1 # reduction factor encoder_type: conformer # encoder type decoder_type: conformer # decoder type conformer_pos_enc_layer_type: rel_pos # conformer positional encoding type conformer_self_attn_layer_type: rel_selfattn # conformer self-attention type

参数与 attention 模块的对应关系如下:

  • adim: 384即注意力层的n_featattention_dim),每个注意力头维度d_k = 384 / 2 = 192
  • aheads: 2n_head,满足n_feat % n_head == 0的整除约束;
  • conformer_self_attn_layer_type: rel_selfattn对应RelPositionMultiHeadedAttention
  • conformer_pos_enc_layer_type: rel_pos对应RelPositionalEncoding,二者必须成对出现(源码中有断言强制);
  • encoder_normalize_before / decoder_normalize_before对应normalize_before,决定 LayerNorm 在残差块前还是后执行;
  • positionwise_layer_type: conv1d对应前馈层选择MultiLayeredConv1d(在 positionwise_feed_forward.py 中定义),attention 输出经它做非线性变换。

对比 examples/aishell3/tts3/conf/default.yaml 可见相同结构的默认配置同样采用adim: 384, aheads: 2,而encoder_type: conformer时默认启用rel_pos + rel_selfattn组合。

七、实现要点总结与工程启示

从源码层面归纳本模块的几个工程要点,供二次开发与调试参考:

  1. 整除约束n_feat必须能被n_head整除,配置adim/aheads时需保持一致(384/2、512/4、256/4 等均为合法组合)。
  2. 掩码语义:模块内部约定 mask 中True表示"有效位置",在forward_attention中会取反后填充finfo.min,并在 softmax 后二次置零,保证屏蔽位置严格不贡献信息。
  3. 相对位置的双实现:新版(RelPositionMultiHeadedAttention+RelPositionalEncoding)与旧版(Legacy...)在位置编码形状与rel_shift截取逻辑上存在差异,移植模型时需按对应版本配套使用,避免混用。
  4. 流式推理支持EncoderLayer/DecoderLayercache参数配合forward_one_step,使注意力层可仅对最新一帧计算,为流式 TTS 场景(如 demos/streaming_tts_server)的增量解码提供基础。
  5. 配置驱动:注意力类型、头数、维度、dropout 均通过配置文件暴露,研究者无需修改源码即可在selfattnrel_selfattnabs_posrel_pos之间切换,快速开展对比实验。

总体而言,paddlespeech.t2s.modules.transformer.attention是连接 PaddleSpeech TTS 中 Transformer 与 Conformer 两大编码器家族的枢纽模块,其实现忠实复现了经典多头注意力与相对位置编码注意力,并通过 Paddle 动态图 API 完成了从 ESPnet 的移植。对语音合成模型进行定制或研究注意力机制时,本文件是首选的阅读与修改入口。

【免费下载链接】PaddleSpeechEasy-to-use Speech Toolkit including Self-Supervised Learning model, SOTA/Streaming ASR with punctuation, Streaming TTS with text frontend, Speaker Verification System, End-to-End Speech Translation and Keyword Spotting. Won NAACL2022 Best Demo Award.项目地址: https://gitcode.com/paddlepaddle/PaddleSpeech

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询