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

PyTorch与TensorFlow 1.x GRU层输出不一致问题及解决问询

TensorFlow 1.x GRU模型迁移PyTorch的数值差异问题及解决方法

问题背景

我正尝试将TensorFlow 1.x中训练好的多单元GRU模型迁移到PyTorch,以便将编码器与其他PyTorch高级模块结合。已提取各层权重到NumPy数组,在PyTorch中重建模型并手动设置权重后,测试输出的张量维度正确,但数值差异极大。

原始TensorFlow 1.x模型代码

encoder_cell = [tf.nn.rnn_cell.GRUCell(size) for size in self.cell_size]
encoder_cell = tf.contrib.rnn.MultiRNNCell(encoder_cell)
encoder_outputs, encoder_state = tf.nn.dynamic_rnn(encoder_cell,
                                                   encoder_emb_inp,
                                                   sequence_length=self.input_len,
                                                   dtype=tf.float32,
                                                   time_major=False)
emb = tf.layers.dense(tf.concat(encoder_state, axis=1),
                              self.embedding_size
                             )
emb = self.emb_activation(emb)

return emb

重建的PyTorch模型代码

class GRU_Encoder(nn.Module):
    def __init__(self, input_size=40, embedding_size=32, hidden_size=512):
        super(GRU_Encoder, self).__init__()
        
        self.weights_dir = '/media/drives/drive1/robin/cddd/default_model/weights'
        self.embedding_sizes = [32, 512, 1024, 2048]
        
        # Initial embedding layer
        char_weights = np.load(f'{self.weights_dir}/char_embedding_0.npy')
        self.char_projection = nn.Linear(input_size, embedding_size, bias=False)
        self.char_projection.weight.data = torch.FloatTensor(char_weights.T)
        
        # Create 3 GRU layers
        self.gru_layers = nn.ModuleList([
            nn.GRU(
                input_size=self.embedding_sizes[i],
                hidden_size=self.embedding_sizes[i+1],
                batch_first=True
            ) for i in range(3)
        ])
        
        # Final dense layer
        dense_kernel = np.load(f'{self.weights_dir}/Encoder_dense_kernel_0.npy')
        self.dense = nn.Linear(sum(self.embedding_sizes[1:]), dense_kernel.shape[1])
        self.tanh = nn.Tanh()
        
        self._load_gru_weights()
        self._load_dense_weights()
    
    def _load_gru_weights(self):
        for i in range(3):
            # Load weights
            gates_kernel = np.load(f'{self.weights_dir}/Encoder_rnn_multi_rnn_cell_cell_{i}_gru_cell_gates_kernel_0.npy')
            gates_bias = np.load(f'{self.weights_dir}/Encoder_rnn_multi_rnn_cell_cell_{i}_gru_cell_gates_bias_0.npy')
            candidate_kernel = np.load(f'{self.weights_dir}/Encoder_rnn_multi_rnn_cell_cell_{i}_gru_cell_candidate_kernel_0.npy')
            candidate_bias = np.load(f'{self.weights_dir}/Encoder_rnn_multi_rnn_cell_cell_{i}_gru_cell_candidate_bias_0.npy')
            
            input_size = self.embedding_sizes[i]
            hidden_size = self.embedding_sizes[i+1]
            
            # Properly reshape weights for PyTorch GRU format
            # PyTorch expects (3 * hidden_size, input_size) for ih weights
            # and (3 * hidden_size, hidden_size) for hh weights
            
            # Split input and hidden weights
            gates_kernel_i = gates_kernel[:input_size, :]  # Input weights
            print(gates_kernel_i.shape)
            gates_kernel_h = gates_kernel[input_size:, :]  # Hidden weights
            print(gates_kernel_h.shape)
            candidate_kernel_i = candidate_kernel[:input_size, :]
            candidate_kernel_h = candidate_kernel[input_size:, :]
            
            # Combine weights in PyTorch's expected format
            w_ih = np.concatenate([
                gates_kernel_i[:, :hidden_size],     # reset gate
                gates_kernel_i[:, hidden_size:],     # update gate
                candidate_kernel_i
            ], axis=1)
            
            w_hh = np.concatenate([
                gates_kernel_h[:, :hidden_size],     # reset gate
                gates_kernel_h[:, hidden_size:],     # update gate
                candidate_kernel_h
            ], axis=1)
            
            # Combine biases
            b_ih = np.concatenate([
                gates_bias[:hidden_size],            # reset gate
                gates_bias[hidden_size:],            # update gate
                candidate_bias
            ])
            
            print('You are here')
            print(self.gru_layers[i].bias_ih_l0.shape)
            print(self.gru_layers[i].bias_hh_l0.shape)
            # Set weights and biases
            self.gru_layers[i].weight_ih_l0.data = torch.FloatTensor(w_ih.T)
            self.gru_layers[i].weight_hh_l0.data = torch.FloatTensor(w_hh.T)
            self.gru_layers[i].bias_ih_l0.data = torch.FloatTensor(b_ih)
            self.gru_layers[i].bias_hh_l0.data = torch.zeros_like(self.gru_layers[i].bias_hh_l0)
    
    def _load_dense_weights(self):
        dense_kernel = np.load(f'{self.weights_dir}/Encoder_dense_kernel_0.npy')
        dense_bias = np.load(f'{self.weights_dir}/Encoder_dense_bias_0.npy')
        self.dense.weight.data = torch.FloatTensor(dense_kernel.T)
        self.dense.bias.data = torch.FloatTensor(dense_bias)
    
    def forward(self, x):
        # Initial projection
        x = self.char_projection(x)
        # Process through GRU layers
        hidden_states = []
        current_input = x
        
        for gru in self.gru_layers:
            output, hidden = gru(current_input)
            print(f' Output: {output.shape}')
            print(f' Hidden: {hidden.shape}')
            output_array = np.array(output)
            print(np.array(hidden[-1]))
            hidden_states.append(hidden[-1])
            current_input = output
        
        # Combine hidden states and apply final transformation
        combined_hidden = torch.cat(hidden_states, dim=1)
        x = self.dense(combined_hidden)
        x = self.tanh(x)
        return x

问题细节

经过self.char_projection后的x与原始TensorFlow模型的encoder_emb_inp完全一致,但后续输出差异明显。单GRU层测试发现两者输出相似但数值差异较大,最终定位到候选隐藏状态$\hat{h}$计算步骤中的矩阵乘法顺序不同:TensorFlow 1.x采用原始实现,而PyTorch不支持切换该逻辑。


差异根源

TensorFlow 1.x的GRUCell与PyTorch的GRU在候选隐藏状态$\hat{h}$的计算逻辑上存在矩阵乘法顺序的核心差异:

  • TensorFlow 1.x中,$\hat{h} = \tanh(W_{xh} \cdot x_t + b_x + W_{hh} \cdot (r_t \odot h_{t-1}) + b_h)$,权重矩阵为[input_size, hidden_size]格式,计算时是输入向量左乘权重矩阵。
  • PyTorch的GRU中,$\hat{h} = \tanh(W_{ih} x_t + b_{ih} + W_{hh} (r_t \odot h_{t-1}) + b_{hh})$,其中W_ih为[3*hidden_size, input_size]格式,计算时是权重矩阵左乘输入向量(本质是输入向量转置后右乘权重矩阵转置)。

这种矩阵乘法顺序的差异,若权重转置逻辑处理不当,会直接引发数值偏差。

解决方法

要让PyTorch输出与TensorFlow一致,需手动实现GRU的前向逻辑,完全对齐TensorFlow的计算步骤,而非使用PyTorch内置的nn.GRU。

1. 自定义GRU层类

实现单个GRU单元的前向计算,严格遵循TensorFlow 1.x的GRU公式:

class CustomGRULayer(nn.Module):
    def __init__(self, input_size, hidden_size):
        super().__init__()
        self.input_size = input_size
        self.hidden_size = hidden_size
        
        # 定义TensorFlow风格的权重(input_size -> hidden_size)
        self.W_gates = nn.Parameter(torch.empty(input_size + hidden_size, 2 * hidden_size))
        self.b_gates = nn.Parameter(torch.empty(2 * hidden_size))
        self.W_candidate = nn.Parameter(torch.empty(input_size + hidden_size, hidden_size))
        self.b_candidate = nn.Parameter(torch.empty(hidden_size))
        
    def forward(self, x, h_prev):
        # x shape: [batch_size, seq_len, input_size]
        # h_prev shape: [batch_size, hidden_size]
        
        seq_len = x.shape[1]
        outputs = []
        
        for t in range(seq_len):
            x_t = x[:, t, :]  # [batch_size, input_size]
            # 拼接输入与上一时刻隐藏状态
            concat = torch.cat([x_t, h_prev], dim=1)  # [batch_size, input_size + hidden_size]
            
            # 计算重置门r_t和更新门z_t
            gates = torch.matmul(concat, self.W_gates) + self.b_gates
            r_t = torch.sigmoid(gates[:, :self.hidden_size])
            z_t = torch.sigmoid(gates[:, self.hidden_size:])
            
            # 计算候选隐藏状态hat_h
            concat_candidate = torch.cat([x_t, r_t * h_prev], dim=1)
            hat_h = torch.tanh(torch.matmul(concat_candidate, self.W_candidate) + self.b_candidate)
            
            # 更新当前隐藏状态h_t
            h_t = (1 - z_t) * h_prev + z_t * hat_h
            outputs.append(h_t.unsqueeze(1))
            h_prev = h_t
        
        outputs = torch.cat(outputs, dim=1)  # [batch_size, seq_len, hidden_size]
        return outputs, h_t.unsqueeze(0)  # 输出格式对齐PyTorch的nn.GRU(hidden是[num_layers, batch_size, hidden_size])

2. 修改编码器模型的权重加载逻辑

在GRU_Encoder中替换nn.GRU为自定义的CustomGRULayer,直接加载TensorFlow导出的权重,无需转置:

class GRU_Encoder(nn.Module):
    def __init__(self, input_size=40, embedding_size=32, hidden_size=512):
        super(GRU_Encoder, self).__init__()
        
        self.weights_dir = '/media/drives/drive1/robin/cddd/default_model/weights'
        self.embedding_sizes = [32, 512, 1024, 2048]
        
        # Initial embedding layer
        char_weights = np.load(f'{self.weights_dir}/char_embedding_0.npy')
        self.char_projection = nn.Linear(input_size, embedding_size, bias=False)
        self.char_projection.weight.data = torch.FloatTensor(char_weights.T)
        
        # 创建3个自定义GRU层
        self.gru_layers = nn.ModuleList([
            CustomGRULayer(
                input_size=self.embedding_sizes[i],
                hidden_size=self.embedding_sizes[i+1]
            ) for i in range(3)
        ])
        
        # Final dense layer
        dense_kernel = np.load(f'{self.weights_dir}/Encoder_dense_kernel_0.npy')
        self.dense = nn.Linear(sum(self.embedding_sizes[1:]), dense_kernel.shape[1])
        self.tanh = nn.Tanh()
        
        self._load_gru_weights()
        self._load_dense_weights()
    
    def _load_gru_weights(self):
        for i in range(3):
            # 加载TensorFlow导出的权重
            gates_kernel = np.load(f'{self.weights_dir}/Encoder_rnn_multi_rnn_cell_cell_{i}_gru_cell_gates_kernel_0.npy')
            gates_bias = np.load(f'{self.weights_dir}/Encoder_rnn_multi_rnn_cell_cell_{i}_gru_cell_gates_bias_0.npy')
            candidate_kernel = np.load(f'{self.weights_dir}/Encoder_rnn_multi_rnn_cell_cell_{i}_gru_cell_candidate_kernel_0.npy')
            candidate_bias = np.load(f'{self.weights_dir}/Encoder_rnn_multi_rnn_cell_cell_{i}_gru_cell_candidate_bias_0.npy')
            
            # 直接赋值,无需转置,完全对齐TensorFlow的权重格式
            self.gru_layers[i].W_gates.data = torch.FloatTensor(gates_kernel)
            self.gru_layers[i].b_gates.data = torch.FloatTensor(gates_bias)
            self.gru_layers[i].W_candidate.data = torch.FloatTensor(candidate_kernel)
            self.gru_layers[i].b_candidate.data = torch.FloatTensor(candidate_bias)
    
    def _load_dense_weights(self):
        dense_kernel = np.load(f'{self.weights_dir}/Encoder_dense_kernel_0.npy')
        dense_bias = np.load(f'{self.weights_dir}/Encoder_dense_bias_0.npy')
        self.dense.weight.data = torch.FloatTensor(dense_kernel.T)
        self.dense.bias.data = torch.FloatTensor(dense_bias)
    
    def forward(self, x):
        # Initial projection
        x = self.char_projection(x)
        # Process through GRU layers
        hidden_states = []
        current_input = x
        h_prev = None
        
        for gru in self.gru_layers:
            if h_prev is None:
                # 初始隐藏状态设为全0,对齐TensorFlow的dynamic_rnn默认行为
                h_prev = torch.zeros(x.shape[0], gru.hidden_size, device=x.device)
            output, h_t = gru(current_input, h_prev)
            hidden_states.append(h_t.squeeze(0))
            current_input = output
            h_prev = h_t.squeeze(0)
        
        # Combine hidden states and apply final transformation
        combined_hidden = torch.cat(hidden_states, dim=1)
        x = self.dense(combined_hidden)
        x = self.tanh(x)
        return x

3. 额外注意事项

  • 对齐sequence_length处理逻辑:TensorFlow的dynamic_rnn会根据sequence_length截断后续计算并复制最后有效状态,自定义GRU层也需实现这一逻辑(若原始模型使用了该参数)。
  • 验证数值精度:使用相同输入张量,对比PyTorch自定义模型与TensorFlow模型的输出,确保误差在浮点精度范围内(如1e-6以下)。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 09:28:09