返回

解决PyTorch LSTM形状错误: shape invalid (含代码)

Ai

PyTorch LSTM 形状错误 RuntimeError: shape '[16, 32, 63]' is invalid for input of size 4032 深度解析

搞机器学习、深度学习的时候,碰到各种报错是家常便饭。这次咱们来聊聊一个在 PyTorch 里使用 LSTM 时常见的形状不匹配错误:RuntimeError: shape '[16, 32, 63]' is invalid for input of size 4032。特别是当你正在处理像手势识别这样的序列数据时,这个问题就更容易冒出来。

问题现象

你在使用 PyTorch 和 MediaPipe 数据训练一个 LSTM 模型来进行手势识别,参考了某个 GitHub 仓库的代码。运行 train.py 脚本时,程序在模型的前向传播(forward 方法)中的 reshape 操作那一行撂挑子了,甩给你一个 RuntimeError,信息是:shape '[16, 32, 63]' is invalid for input of size 4032

报错的代码片段大致长这样 (在 LSTM.py 文件的 forward 方法里):

    def forward(self, x):
        # 问题就出在这行!
        x = x.reshape(self.batch_size, self.seq_len, CFG.num_feats*21) 
        x, (h, c) = self.lstm(x)
        x = x[:, -1, :]
        x = self.hidden2label(x)
        return x

你的配置文件 (config.py) 里可能设置了 CFG.batch_size = 16CFG.sequence_length = 32CFG.num_feats = 3

有趣的是,当你尝试把 batch_size 改成 2 (可能是因为看到某个地方的数据形状是 torch.Size([2, 32, 21, 3])),这个 RuntimeError 消失了,但又跳出来一个新毛病:在调用 torchsummary.summary(model, ...) 时报 AttributeError: 'tuple' object has no attribute 'size'

真是按下葫芦浮起瓢!别慌,咱们一步步来拆解。

刨根问底:错误原因分析

咱们得先把这两个错误分开来看,它们通常是两个独立的问题。

剖析 RuntimeError: 恼人的形状不匹配

这个 RuntimeError 是最核心的问题,直接阻止了你的模型训练。让我们仔细看看这行惹祸的代码:

x = x.reshape(self.batch_size, self.seq_len, CFG.num_feats*21)

这行代码想干嘛?它想把输入张量 x 变成一个特定形状的三维张量,以便送给后面的 nn.LSTM 层。nn.LSTMbatch_first=True 时,期望的输入形状是 (batch_size, seq_len, input_features)

根据你的配置:

  • self.batch_size 是模型初始化时传入的 CFG.batch_size,也就是 16。
  • self.seq_len 是模型初始化时传入的 CFG.sequence_length,也就是 32。
  • CFG.num_feats*21 计算的是每个时间步的特征数量。这里 CFG.num_feats = 3 (通常代表 x, y, z 坐标),21 对应 MediaPipe 手部关键点的数量。所以 3 * 21 = 63

所以,reshape 想要的目标形状是 [16, 32, 63]

一个形状为 [16, 32, 63] 的张量,总共包含多少个元素? 16 * 32 * 63 = 32256 个元素。

现在看错误信息后半段:"invalid for input of size 4032"。这话的意思是,你喂给 reshape 操作的那个输入张量 x,它里面实际上只有 4032 个元素

这就尴尬了!reshape 操作只是改变数据的排列方式,并不能增删元素。你让它把只有 4032 个元素的数据,硬生生变成需要 32256 个元素的三维结构,它当然办不到,只能报错。

那这 4032 个元素是哪来的?

我们猜测一下输入张量 x 在执行 reshape 之前 的实际形状。它很可能来自于你的 DataLoader。结合你提到的尝试将 batch_size 改为 2 后看到 torch.Size([2, 32, 21, 3]) 这个信息,我们可以大胆推测:

  • 你的 DataLoader 可能输出了一个形状为 (batch_size, seq_len, num_landmarks, num_features) 的张量。
  • 当错误发生时,DataLoader 实际输出的批次大小 (batch size) 很可能是 2,而不是你在 CFG 里设置的 16。
  • 那么,此时 x 的形状就是 [2, 32, 21, 3]

算一下这个形状的总元素数量:2 * 32 * 21 * 3 = 4032。 bingo!正好是错误信息里提到的 "input of size 4032"。

为什么实际的 batch size 会是 2 呢?
这可能有几种原因,尽管你的 DataLoader 配置了 batch_size=16drop_last=True (这意味着最后一个不完整的批次会被丢弃):

  1. Dataset/Dataloader 问题: 也许 HandPoseDatasetNumpyDataLoader 的实现在某些情况下并没有严格遵守 batch_sizedrop_last 的设置?(可能性相对较低,但不能完全排除)。
  2. 代码逻辑: 是否有其他地方(比如 eval_func 或者其他调用模型 forward 的地方)传入了不同 batch size 的数据?或者在 train_func 内部 inputs 被意外改变了?
  3. 调试时的干扰: 有时候调试过程中的一些操作可能导致传入的数据批次大小变化。

但无论具体原因是什么,核心矛盾 已经清楚了:forward 方法里的 reshape 操作,写死了要用 self.batch_size (值为 16) 来构建目标形状 [16, 32, 63],而它收到的输入 x 实际上只有 4032 个元素 (对应 batch_size=2 的数据量)。元素总数对不上,RuntimeError 就出现了。

再探 AttributeError: torchsummary 的小插曲

这个 AttributeError: 'tuple' object has no attribute 'size' 是在你把 CFG.batch_size 改成 2 之后,运行 summary(model, ...) 时出现的。

我们来看下 train_eval 函数里调用 summary 的地方:

print(summary(model, (CFG.sequence_length, 21, CFG.num_feats)))

torchsummary 这个库通过在模型的每一层注册钩子 (hook) 来获取输入输出的形状信息。nn.LSTM 层比较特殊,它的 forward 方法执行后返回的是一个元组 (tuple),包含两个部分:

  1. output: 形状为 (batch, seq_len, hidden_dim) 的张量,包含了序列中每个时间步的输出特征。
  2. (hn, cn): 也是一个元组,里面是最后一个时间步的隐藏状态 hn 和细胞状态 cn

报错信息 AttributeError: 'tuple' object has no attribute 'size' 指向 torchsummary 内部试图对某个对象调用 .size() 方法,但这个对象是个元组,元组没有 .size() 方法 (只有 Tensor 有)。

很可能 torchsummary 的钩子在处理 nn.LSTM 的输出时,没能正确地只处理第一个返回项(output 张量),而是错误地把后面那个 (hn, cn) 元组也当成需要获取 .size() 的输出来处理了,于是就报错了。

关键点: 这个 AttributeErrortorchsummary 工具本身的问题,跟你模型的核心逻辑、训练过程关系不大。它只是在你试图打印模型结构摘要时才发生。并且,在你设置 batch_size=2 时发生,并不意味着 batch_size=2 是“正确”的,只是恰好绕过了之前的 RuntimeError,暴露了这个次要问题。

对症下药:解决方案来了

了解了病根,我们就可以开药方了。

方案一:让 Batch Size 动态起来 (推荐)

既然 RuntimeError 的根源在于 reshape 时写死了 batch_size,而实际输入的 batch_size 可能变化,那最直接的解决办法就是——不要写死 batch_size

reshape 操作动态地从输入张量 x 本身获取当前的实际 batch_size

修改 LSTM.py 中的 forward 方法:

import torch
import torch.nn as nn
# ... (其他 imports 和类定义)

class LSTMClassifier(nn.Module):
    # ... (init 方法不变)
    
    def forward(self, x):
        # x 的原始形状可能是 [batch, seq_len, num_landmarks, num_features]
        # 例如 [?, 32, 21, 3]
        
        # 获取当前的实际 batch size
        current_batch_size = x.shape[0]  # 或者 x.size(0)
        
        # 使用实际的 batch size 进行 reshape
        # 目标是把最后两个维度 (num_landmarks, num_features) 合并
        # 目标形状: [current_batch_size, self.seq_len, num_landmarks * num_features]
        # 也就是: [?, 32, 21 * 3] -> [?, 32, 63]
        x = x.reshape(current_batch_size, self.seq_len, -1) 
        # 使用 -1 可以让 PyTorch 自动计算最后一个维度的大小,更保险
        # 等价于 x.reshape(current_batch_size, self.seq_len, CFG.num_feats * 21)
        
        # ===> 原来的代码:
        # x = x.reshape(self.batch_size, self.seq_len, CFG.num_feats*21)
        # ^^^ 把 self.batch_size (固定值, 比如16) 替换成 current_batch_size (动态获取)
        
        # 后续代码不变
        x, (h, c) = self.lstm(x)
        x = x[:, -1, :]
        x = self.hidden2label(x)
        return x

原理与作用:

  • x.shape[0] (或者 x.size(0)) 会返回输入张量 x 的第一个维度的大小,也就是当前批次数据的实际 batch_size
  • 使用这个动态获取的 current_batch_size 来进行 reshape,可以确保无论输入的 batch_size 是 16、2 还是其他值(例如,如果数据集大小不是 batch_size 的整数倍且 drop_last=False 时,最后一个批次的大小会不同),reshape 操作都能正确进行。因为它现在只关心如何根据 当前 输入的元素总数来重新组织形状,而不再强制要求批次大小必须是模型初始化时那个固定的 self.batch_size 值。
  • 我们还用了 reshape(current_batch_size, self.seq_len, -1)。这里的 -1 是个占位符,意思是让 PyTorch 根据总元素数量、已知的 current_batch_sizeself.seq_len,自动推断出最后一个维度的大小。这比手动计算 CFG.num_feats * 21 更健壮,即使将来特征数量或关键点数量改变,只要前两个维度对了,这里也不容易出错。

优点: 这是最推荐的解决方案,因为它直接解决了 RuntimeError 的根本原因——硬编码与实际情况的脱节,使得模型对输入批次大小的变化更具鲁棒性。

进阶使用/注意事项:

  • 务必确认你的 DataLoader 输出的数据,其形状确实是类似 [batch_size, seq_len, num_landmarks, num_features] (即 [?, 32, 21, 3]) 这样的四维张量。reshape(current_batch_size, self.seq_len, -1) 正是设计用来将最后两个维度合并,以匹配 LSTM 输入要求的 (batch, seq, features) 格式。
  • 这个修改不会影响模型的计算逻辑,只是让形状转换更灵活。

方案二:固定输入形状,调整数据加载 (不太推荐)

如果你能保证 DataLoader 输出的数据形状 总是 [batch_size, seq_len, num_feats * 21] (即 [?, 32, 63]),那么 forward 方法里的 reshape 操作就完全是多余的了。

操作步骤:

  1. 修改你的数据预处理部分(比如 HandPoseDatasetNumpy 类或者 df_to_numpy 函数),确保在数据加载阶段就已经将每个样本的数据转换成了 [seq_len, num_feats * 21] (即 [32, 63]) 的形状。
  2. 然后,从 LSTMClassifierforward 方法中 移除 x.reshape(...) 这一行。

原理与作用:
这种方法把形状转换的任务提前到了数据加载阶段。模型接收到的 x 直接就是 LSTM 需要的形状,无需再 reshape

局限性:

  • 你需要修改数据处理逻辑,这可能比修改模型 forward 方法更复杂,也更容易引入新的 bug。
  • 降低了模型代码的自解释性。看 forward 方法时,不清楚输入 x 应该是四维还是三维的。
  • 如果以后数据源或预处理逻辑变了,可能又要调整模型或数据加载代码。

除非你有特别的理由(比如性能优化,且确定输入格式固定),否则方案一通常更优。

方案三:处理 torchsummary 的 AttributeError

这个错误与核心的 RuntimeError 无关,只影响 summary 功能。

方法 1: 忽略它 (最简单)

如果你不是非得看 torchsummary 的输出不可,最简单的办法就是把调用它的那行代码注释掉或删掉:

# print(summary(model, (CFG.sequence_length, 21, CFG.num_feats))) # 注释掉这行

你的模型训练和评估完全不受影响。

方法 2: 尝试更新或寻找替代品 (如果需要 summary)

  • 检查 torchsummary 是否有更新的版本,也许新版本修复了对 LSTM 输出的处理问题。
  • 寻找其他可以打印模型结构和参数信息的库,例如 torchinfo,它可能对 LSTM 的支持更好。
  • (高阶)尝试自己写一个简化的 summary 函数,或者修改 torchsummary 的源码来适应 LSTM 的元组输出。但这通常比较费时费力。

与 RuntimeError 的关联:

记住,当你把 batch_size 改成 2 时,RuntimeError 消失了,但这并不意味着 batch size 为 2 是正确的或解决了根本问题。它只是使得在 forward 中的那个特定 reshape (试图从 4032 个元素变到需要 2 * 32 * 63 = 4032 个元素? 不对,目标仍是 16*32*63=32256) 的条件发生了变化,或者说错误点转移了。最终你遇到的 AttributeError 是一个完全不同的问题。 解决 RuntimeError 的正确方法是方案一(动态 batch size),它与你最初设置的 CFG.batch_size=16 并不矛盾,也适用于 batch_size=2 或任何其他大小。


总的来说,PyTorch 中的形状错误很常见,关键在于仔细阅读错误信息,理解操作(如 reshape)的期望输入和输出,并回溯检查数据的实际形状是如何传递和变化的。对于 RuntimeError: shape [...] is invalid for input of size [...] 这类错误,核心通常是元素总数不匹配。而对于 LSTM 这类可能返回复杂输出(如元组)的层,使用像 torchsummary 这样的工具时,也要留意它们是否能正确处理这些特殊情况。希望这次的分析能帮你解决问题,让你的手势识别模型顺利跑起来!