You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Keras Transformer形状不兼容报错及num_heads与batch_size疑问

问题:Transformer翻译模型形状不兼容错误及num_heads与batch_size关系疑惑

参考Keras英西文本翻译示例实现代码时,出现以下形状不兼容错误:

Incompatible shapes: [64,8,20,20] vs. [64,64,20,20] [[{{node gradient_tape/transformer/transformer_encoder/multi_head_attention/softmax/add/BroadcastGradientArgs}}]]
[Op:__inference_train_function_16963]

将num_heads设置为与batch_size相同后报错消失,但疑惑二者是否必须保持一致(官方示例中二者数值不同)。

完整实现代码:

import random
import string
import re
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers

with open('spa.txt', encoding="utf8") as f:
    lines = f.read().split("\n")[:-1]
text_pairs = []
for line in lines:
    eng, spa = line.split("\t")
    spa = "[start] " + spa + " [end]"
    text_pairs.append((eng, spa))
random.shuffle(text_pairs)
num_val_samples = int(0.15 * len(text_pairs))
num_train_samples = len(text_pairs) - 2 * num_val_samples
train_pairs = text_pairs[:num_train_samples]
val_pairs = text_pairs[num_train_samples : num_train_samples + num_val_samples]
test_pairs = text_pairs[num_train_samples + num_val_samples :]

strip_chars = string.punctuation + "¿"
strip_chars = strip_chars.replace("[", "")
strip_chars = strip_chars.replace("]", "")

vocab_size = 15000
sequence_length = 20
batch_size = 64


def custom_standardization(input_string):
    lowercase = tf.strings.lower(input_string)
    return tf.strings.regex_replace(lowercase, "[%s]" % re.escape(strip_chars), "")


eng_vectorization = layers.TextVectorization(
    max_tokens=vocab_size, output_mode="int", output_sequence_length=sequence_length,
)
spa_vectorization = layers.TextVectorization(
    max_tokens=vocab_size,
    output_mode="int",
    output_sequence_length=sequence_length + 1,
    standardize=custom_standardization,
)
train_eng_texts = [pair[0] for pair in train_pairs]
train_spa_texts = [pair[1] for pair in train_pairs]
eng_vectorization.adapt(train_eng_texts)
spa_vectorization.adapt(train_spa_texts)

def format_dataset(eng, spa):
    eng = eng_vectorization(eng)
    spa = spa_vectorization(spa)
    return ({"encoder_inputs": eng, "decoder_inputs": spa[:, :-1],}, spa[:, 1:])


def make_dataset(pairs):
    eng_texts, spa_texts = zip(*pairs)
    eng_texts = list(eng_texts)
    spa_texts = list(spa_texts)
    dataset = tf.data.Dataset.from_tensor_slices((eng_texts, spa_texts))
    dataset = dataset.batch(batch_size)
    dataset = dataset.map(format_dataset)
    return dataset.shuffle(2048).prefetch(16).cache()


train_ds = make_dataset(train_pairs)
val_ds = make_dataset(val_pairs)

class TransformerEncoder(layers.Layer):
    def __init__(self, embed_dim, dense_dim, num_heads, **kwargs):
        super(TransformerEncoder, self).__init__(**kwargs)
        self.embed_dim = embed_dim
        self.dense_dim = dense_dim
        self.num_heads = num_heads
        self.attention = layers.MultiHeadAttention(
            num_heads=num_heads, key_dim=embed_dim
        )
        self.dense_proj = keras.Sequential(
            [layers.Dense(dense_dim, activation="relu"), layers.Dense(embed_dim),]
        )
        self.layernorm_1 = layers.LayerNormalization()
        self.layernorm_2 = layers.LayerNormalization()
        self.supports_masking = True

    def call(self, inputs, mask=None):
        if mask is not None:
            padding_mask = tf.cast(mask[:, tf.newaxis, tf.newaxis, :], dtype="int32")
        attention_output = self.attention(
            query=inputs, value=inputs, key=inputs, attention_mask=padding_mask
        )
        proj_input = self.layernorm_1(inputs + attention_output)
        proj_output = self.dense_proj(proj_input)
        return self.layernorm_2(proj_input + proj_output)


class PositionalEmbedding(layers.Layer):
    def __init__(self, sequence_length, vocab_size, embed_dim, **kwargs):
        super(PositionalEmbedding, self).__init__(**kwargs)
        self.token_embeddings = layers.Embedding(
            input_dim=vocab_size, output_dim=embed_dim
        )
        self.position_embeddings = layers.Embedding(
            input_dim=sequence_length, output_dim=embed_dim
        )
        self.sequence_length = sequence_length
        self.vocab_size = vocab_size
        self.embed_dim = embed_dim

    def call(self, inputs):
        length = tf.shape(inputs)[-1]
        positions = tf.range(start=0, limit=length, delta=1)
        embedded_tokens = self.token_embeddings(inputs)
        embedded_positions = self.position_embeddings(positions)
        return embedded_tokens + embedded_positions

    def compute_mask(self, inputs, mask=None):
        return tf.math.not_equal(inputs, 0)


class TransformerDecoder(layers.Layer):
    def __init__(self, embed_dim, latent_dim, num_heads, **kwargs):
        super(TransformerDecoder, self).__init__(**kwargs)
        self.embed_dim = embed_dim
        self.latent_dim = latent_dim
        self.num_heads = num_heads
        self.attention_1 = layers.MultiHeadAttention(
            num_heads=num_heads, key_dim=embed_dim
        )
        self.attention_2 = layers.MultiHeadAttention(
            num_heads=num_heads, key_dim=embed_dim
        )
        self.dense_proj = keras.Sequential(
            [layers.Dense(latent_dim, activation="relu"), layers.Dense(embed_dim),]
        )
        self.layernorm_1 = layers.LayerNormalization()
        self.layernorm_2 = layers.LayerNormalization()
        self.layernorm_3 = layers.LayerNormalization()
        self.supports_masking = True

    def call(self, inputs, encoder_outputs, mask=None):
        causal_mask = self.get_causal_attention_mask(inputs)
        if mask is not None:
            padding_mask = tf.cast(mask[:, tf.newaxis, :], dtype="int32")
            padding_mask = tf.minimum(padding_mask, causal_mask)

        attention_output_1 = self.attention_1(
            query=inputs, value=inputs, key=inputs, attention_mask=causal_mask
        )
        out_1 = self.layernorm_1(inputs + attention_output_1)

        attention_output_2 = self.attention_2(
            query=out_1,
            value=encoder_outputs,
            key=encoder_outputs,
            attention_mask=padding_mask,
        )
        out_2 = self.layernorm_2(out_1 + attention_output_2)

        proj_output = self.dense_proj(out_2)
        return self.layernorm_3(out_2 + proj_output)

    def get_causal_attention_mask(self, inputs):
        input_shape = tf.shape(inputs)
        batch_size, sequence_length = input_shape[0], input_shape[1]
        i = tf.range(sequence_length)[:, tf.newaxis]
        j = tf.range(sequence_length)
        mask = tf.cast(i >= j, dtype="int32")
        mask = tf.reshape(mask, (1, input_shape[1], input_shape[1]))
        mult = tf.concat(
            [tf.expand_dims(batch_size, -1), tf.constant([1, 1], dtype=tf.int32)],
            axis=0,
        )
        return tf.tile(mask, mult)

embed_dim = 256
latent_dim = 2048
num_heads = 8

encoder_inputs = keras.Input(shape=(None,), dtype="int64", name="encoder_inputs")
x = PositionalEmbedding(sequence_length, vocab_size, embed_dim)(encoder_inputs)
encoder_outputs = TransformerEncoder(embed_dim, latent_dim, num_heads)(x)
encoder = keras.Model(encoder_inputs, encoder_outputs)

decoder_inputs = keras.Input(shape=(None,), dtype="int64", name="decoder_inputs")
encoded_seq_inputs = keras.Input(shape=(None, embed_dim), name="decoder_state_inputs")
x = PositionalEmbedding(sequence_length, vocab_size, embed_dim)(decoder_inputs)
x = TransformerDecoder(embed_dim, latent_dim, num_heads)(x, encoded_seq_inputs)
x = layers.Dropout(0.5)(x)
decoder_outputs = layers.Dense(vocab_size, activation="softmax")(x)
decoder = keras.Model([decoder_inputs, encoded_seq_inputs], decoder_outputs)

decoder_outputs = decoder([decoder_inputs, encoder_outputs])
transformer = keras.Model(
    [encoder_inputs, decoder_inputs], decoder_outputs, name="transformer"
)

epochs = 1  # This should be at least 30 for convergence

transformer.summary()
transformer.compile(
    "rmsprop", loss="sparse_categorical_crossentropy", metrics=["accuracy"]
)
transformer.fit(train_ds, epochs=epochs, validation_data=val_ds)

解答

num_heads和batch_size完全不需要保持一致,你调整后解决问题只是巧合,真正的错误根源在注意力掩码的维度处理上:

  1. 错误原因分析
    错误中的形状[64,8,20,20]是MultiHeadAttention输出的梯度形状(batch_size=64,num_heads=8,seq_len=20),而[64,64,20,20]是掩码的形状,说明掩码维度和注意力层预期不匹配。
    在原代码的TransformerEncoder.call方法中,生成的padding_mask形状为[batch_size,1,1,seq_len],但MultiHeadAttention要求4D注意力掩码的形状为[batch_size, num_heads, seq_len_q, seq_len_k]。当你把num_heads改成64时,掩码的第二维度1会自动广播到64,刚好匹配,所以错误消失,但这是错误的解决方式。

  2. 正确修复方式

    • 修改TransformerEncoder.call中的padding_mask生成逻辑,适配num_heads维度:
      def call(self, inputs, mask=None):
          if mask is not None:
              padding_mask = tf.cast(mask[:, tf.newaxis, tf.newaxis, :], dtype="int32")
              # 在head维度复制掩码,匹配MultiHeadAttention要求
              padding_mask = tf.tile(padding_mask, [1, self.num_heads, 1, 1])
          attention_output = self.attention(
              query=inputs, value=inputs, key=inputs, attention_mask=padding_mask
          )
          proj_input = self.layernorm_1(inputs + attention_output)
          proj_output = self.dense_proj(proj_input)
          return self.layernorm_2(proj_input + proj_output)
      
    • 同步修复TransformerDecoder.call中的掩码维度问题:
      def call(self, inputs, encoder_outputs, mask=None):
          causal_mask = self.get_causal_attention_mask(inputs)
          # 为因果掩码添加head维度
          causal_mask = tf.expand_dims(causal_mask, axis=1)
          if mask is not None:
              padding_mask = tf.cast(mask[:, tf.newaxis, tf.newaxis, :], dtype="int32")
              padding_mask = tf.tile(padding_mask, [1, self.num_heads, 1, 1])
              padding_mask = tf.minimum(padding_mask, causal_mask)
      
          attention_output_1 = self.attention_1(
              query=inputs, value=inputs, key=inputs, attention_mask=causal_mask
          )
          out_1 = self.layernorm_1(inputs + attention_output_1)
      
          attention_output_2 = self.attention_2(
              query=out_1,
              value=encoder_outputs,
              key=encoder_outputs,
              attention_mask=padding_mask,
          )
          out_2 = self.layernorm_2(out_1 + attention_output_2)
      
          proj_output = self.dense_proj(out_2)
          return self.layernorm_3(out_2 + proj_output)
      
  3. 总结

    • num_heads的取值只需满足embed_dim能被其整除(每个head的维度为embed_dim/num_heads),和batch_size无任何关联。
    • 你的错误本质是注意力掩码维度未适配MultiHeadAttention的要求,调整num_heads只是偶然触发广播机制生效,并非正确解决方案。

内容的提问来源于stack exchange,提问作者webber

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.16 00:55:15