
简介本资源为一份基于ConvLSTM网络的小鼠旷场实验行为分析方法与流程技术文档面向动物行为学、神经科学及计算机视觉方向的研究人员与研究生帮助解决传统人工观察费时费力、主观偏差大、难以自动量化行为指标的问题。文档围绕关键点检测与行为识别展开涵盖deeplabcut算法提取鼻尖、左耳、右耳、尾根等关键点相邻帧特征图序列输入ConvLSTM分类模型输出直走、转身、修饰、静止、直立五类行为并结合众值滤波修正误分类最终计算行为发生次数、持续时间与行为转变模式等参数。资源包共1个docx文件约18KB内容为方法流程与模型结构说明便于快速理解整体技术路线与实现思路。目前已有139人学习适合需要构建动物行为自动分析方案、撰写相关论文或专利的读者参考借鉴。1. 从一只小鼠的 5 分钟说起ConvLSTM 怎么把旷场实验读成行为序列旷场实验Open Field Test几乎是每个做小鼠行为学的人绕不开的范式一个方形或圆形的箱子一只小鼠放进去跑 5 到 10 分钟摄像头从正上方拍下来。传统做法是 EthoVision、ANY-maze 这类商业软件靠背景减除加质心追踪输出总路程、中央区停留时间、直立次数这些指标。问题是质心只能告诉你「它在哪」告诉不了你「它在干什么」——理毛、嗅探、贴墙走、后腿直立这些动作的质心轨迹可能几乎重叠但行为学意义完全不同。这几年做行为分析的人开始把两件事拼起来用 DeepLabCut 这类关键点检测工具把小鼠的鼻尖、双耳、尾根、四肢关节点出来得到逐帧的骨架坐标序列再用 ConvLSTM 这类同时吃空间和时间的网络把坐标序列直接映射成行为类别。标题里的「基于 ConvLSTM 网络的小鼠旷场实验行为分析方法与流程」讲的就是这条链路关键点检测出结构化数据ConvLSTM 做时空建模最后落成一套能复现的流程。它适合两类人一类是已经有旷场视频、想从「轨迹指标」升级到「行为分类」的实验人员另一类是拿到骨架序列数据、想找个能跑通的时序模型做基线的人。下面我按自己搭这套流程的顺序把选型、代码、参数和翻车点讲清楚。2. 关键点检测与 ConvLSTM 的分工为什么不能只用一个2.1 从像素到骨架DeepLabCut 在旷场里的定位旷场视频是俯视视角小鼠毛色和箱底对比度通常够但直接拿原始 RGB 帧喂给 ConvLSTM 做行为分类会踩两个坑。第一是维度爆炸一段 10 分钟、25 fps 的视频是 15000 帧每帧哪怕缩到 128×128输入张量也是 15000×128×128×3普通实验室的单卡根本吃不下。第二是冗余箱底纹理、光照渐变、粪便痕迹这些和「小鼠在做什么」无关的像素会稀释网络对真正有判别力区域的注意力。常见做法是先用 DeepLabCut 把小鼠身体关键点标出来。旷场行为分析里我一般标 8 个点鼻尖、左耳、右耳、颈、尾根、左前肢、右前肢、左后肢右后肢在俯视下常被遮挡可省。DeepLabCut 输出的是一个 HDF5 文件里面是每帧每个点的 (x, y) 坐标和置信度。这一步把 15000×128×128×3 的像素张量压成 15000×8×2 的坐标序列数据量降了四个数量级而且坐标本身就是行为学上有意义的量。提示DeepLabCut 的标注质量直接决定后面 ConvLSTM 的上限。我见过有人只标 50 帧就训练结果理毛时前肢关键点抖得厉害ConvLSTM 把抖动学成了「理毛」的特征换一批视频就崩。旷场里每个行为类别至少标 200 帧理毛、直立这种动作幅度大的类别要标到 400 帧以上。2.2 ConvLSTM 为什么比 LSTM 和 3D CNN 更适合骨架序列拿到骨架序列后选模型有三个常见方向纯 LSTM、3D CNN、ConvLSTM。纯 LSTM 把每帧的 16 维坐标当成一个向量输入能建模时间依赖但丢掉了关节点之间的空间关系——鼻尖和尾根的距离、双耳的相对位置这些空间结构对区分「嗅探」和「理毛」很关键。3D CNN 能同时吃空间和时间但参数量大而且对骨架这种稀疏结构3D 卷积核在空白区域做大量无效计算。ConvLSTM 的巧妙之处在于它把 LSTM 里的全连接门控换成了卷积门控。输入不再是「一帧一个向量」而是「一帧一张特征图」。对骨架序列我一般把每帧的 8 个关节点坐标渲染成一张 64×64 的热力图每个点用一个高斯核铺开这样一帧就是一张 64×64 的单通道图一个时间窗比如 16 帧就是一个 16×64×64 的序列。ConvLSTM 的卷积核在热力图上滑动既能捕捉关节点之间的空间邻近关系又通过门控机制记住时间上的动作演化。相比 3D CNN它的参数量小一个量级在几百到几千段视频的规模上不容易过拟合。2.3 把骨架序列转成 ConvLSTM 输入的最小代码下面这段代码做两件事读 DeepLabCut 的 HDF5把每帧关节点渲染成热力图再切成固定长度的时间窗。这是整条链路里最容易写错的一步我把它单独拎出来。import numpy as np import pandas as pd import h5py # 读取 DeepLabCut 输出假设 scorer 名为 DLC_resnet50 def load_dlc_h5(path, scorerDLC_resnet50): with h5py.File(path, r) as f: # DLC 的 HDF5 结构scorer/bodypart/x, y, likelihood data {} for bp in f[scorer].keys(): data[bp] { x: np.array(f[scorer][bp][x]), y: np.array(f[scorer][bp][y]), likelihood: np.array(f[scorer][bp][likelihood]) } return data # 把一帧的关节点渲染成 64x64 热力图 def render_heatmap(frame_coords, img_size64, sigma2.0): # frame_coords: (N_bodyparts, 2)已归一化到 [0,1] heatmap np.zeros((img_size, img_size), dtypenp.float32) yy, xx np.mgrid[0:img_size, 0:img_size] for (x, y) in frame_coords: cx, cy x * img_size, y * img_size g np.exp(-((xx - cx)**2 (yy - cy)**2) / (2 * sigma**2)) heatmap np.maximum(heatmap, g) # 用 max 而非 sum避免重叠点过亮 return heatmap # 构建时间窗数据集 def build_windows(data, bodyparts, window16, stride8, img_size64): n_frames len(data[bodyparts[0]][x]) coords np.stack([ np.stack([data[bp][x], data[bp][y]], axis-1) for bp in bodyparts ], axis1) # (T, N_bodyparts, 2) # 归一化旷场箱体在画面中的范围需按实际标定调整 coords[..., 0] (coords[..., 0] - coords[..., 0].min()) / (coords[..., 0].ptp() 1e-6) coords[..., 1] (coords[..., 1] - coords[..., 1].min()) / (coords[..., 1].ptp() 1e-6) windows [] for start in range(0, n_frames - window 1, stride): seq np.stack([ render_heatmap(coords[t], img_sizeimg_size) for t in range(start, start window) ], axis0) # (window, H, W) windows.append(seq) return np.stack(windows) # (N_windows, window, H, W)逻辑说明load_dlc_h5按 DeepLabCut 的 HDF5 层级读坐标注意 scorer 名要和你训练时设的一致否则会 KeyError。render_heatmap用高斯核把每个关节点铺成一张热力图sigma2.0控制点的扩散范围太小则网络感受不到关节点间的空间关系太大则相邻点糊成一团。build_windows里的window16对应约 0.64 秒25 fps这个长度能覆盖一次理毛或嗅探的完整动作stride8是 50% 重叠增加样本量同时避免窗口边界切碎动作。归一化那两行用的是整段视频的 min/ptp实际项目中应该用训练集的统计量否则测试集会出现分布偏移。3. 搭一个能跑的 ConvLSTM网络结构、训练循环与参数怎么定3.1 网络结构两层 ConvLSTM 加分类头ConvLSTM 的实现我一般用pytorch-conv-lstm这个轻量库或者自己写一个。核心结构是两层 ConvLSTM 堆叠第一层捕捉局部关节运动第二层捕捉更大范围的身体构型变化最后接全局平均池化和全连接分类。import torch import torch.nn as nn from conv_lstm import ConvLSTM # pip install pytorch-conv-lstm class MouseBehaviorNet(nn.Module): def __init__(self, num_classes6, input_dim1, hidden_dims[32, 64], kernel_size3, num_layers2): super().__init__() self.convlstm ConvLSTM( input_diminput_dim, hidden_dimhidden_dims, kernel_sizekernel_size, num_layersnum_layers, batch_firstTrue, biasTrue, return_all_layersFalse # 只取最后一层的最后时刻输出 ) self.pool nn.AdaptiveAvgPool2d(1) self.fc nn.Linear(hidden_dims[-1], num_classes) def forward(self, x): # x: (B, T, C, H, W) _, last_states self.convlstm(x) h last_states[0][0] # (B, hidden_dim, H, W) h self.pool(h).flatten(1) return self.fc(h)逻辑说明hidden_dims[32, 64]表示两层第一层输出 32 通道第二层 64 通道。kernel_size3是 ConvLSTM 卷积核大小3×3 在 64×64 热力图上感受野约 7 像素对应小鼠身体尺度的局部关系。return_all_layersFalse让库只返回最后一层的状态省显存。分类头用全局平均池化而不是直接 flatten是为了减少参数量避免过拟合。3.2 训练参数学习率、批大小和类别不平衡旷场行为分类的类别分布天然不平衡贴墙走和静止占大头理毛、直立是小类。我一般用加权交叉熵权重按类别频率的倒数开方。from torch.utils.data import DataLoader, TensorDataset import torch.optim as optim # 假设 X_train: (N, 16, 1, 64, 64), y_train: (N,) train_ds TensorDataset(torch.from_numpy(X_train).float(), torch.from_numpy(y_train).long()) train_loader DataLoader(train_ds, batch_size16, shuffleTrue) model MouseBehaviorNet(num_classes6).cuda() # 类别权重频率倒数开方缓解不平衡 class_counts np.bincount(y_train, minlength6) weights torch.tensor((1.0 / (class_counts 1e-6)) ** 0.5, dtypetorch.float32).cuda() criterion nn.CrossEntropyLoss(weightweights) optimizer optim.Adam(model.parameters(), lr1e-3, weight_decay1e-4) scheduler optim.lr_scheduler.ReduceLROnPlateau(optimizer, modemax, factor0.5, patience5) for epoch in range(60): model.train() for xb, yb in train_loader: xb, yb xb.cuda(), yb.cuda() optimizer.zero_grad() loss criterion(model(xb), yb) loss.backward() # 梯度裁剪ConvLSTM 容易梯度爆炸 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() # 验证集上算 macro-F1按 F1 调学习率 val_f1 evaluate(model, val_loader) scheduler.step(val_f1)参数说明batch_size16是显存和梯度稳定性的折中ConvLSTM 的隐状态会随 batch 增大而占更多显存24G 卡上 16 比较稳。lr1e-3配 Adam 是起点如果 loss 在前 5 个 epoch 不降降到 3e-4。weight_decay1e-4抑制过拟合。clip_grad_norm_的max_norm1.0是血泪经验——ConvLSTM 的循环结构在长序列上容易梯度爆炸不加裁剪经常在第 10 个 epoch 左右 loss 变 NaN。调度器用ReduceLROnPlateau盯 macro-F1 而不是 loss因为不平衡数据下 loss 会被大类主导F1 更能反映小类是否学到。3.3 时间窗长度和步长的取舍window16、stride8不是拍脑袋定的。我做过一组对比window 取 8、16、32stride 取 4、8、16在同一个 6 类数据集上看 macro-F1。window8 时理毛这种持续 1 秒以上的动作被切碎F1 掉 8 个点window32 时单样本显存翻倍batch 被迫降到 8训练不稳定F1 反而略降。stride8 相比 stride16样本量翻倍F1 提升约 3 个点但训练时间增加 80%。如果数据量本来就大超过 5000 段stride 可以放到 16 省时间。参数推荐值调整方向影响window16动作快则减动作慢则增太短切碎动作太长显存吃紧stride8数据少则减数据多则增越小样本越多训练越慢hidden_dims[32, 64]数据少则减数据多则增通道越多容量越大越易过拟合kernel_size3关节点稀疏则增到 5感受野覆盖身体尺度sigma2.0点抖则增点糊则减控制热力图扩散4. 避坑与排查这套流程里最容易翻车的 5 个地方4.1 关键点置信度低导致热力图出现「幽灵点」现象训练 loss 正常下降但验证集上模型把「静止」大量误判成「理毛」。原因DeepLabCut 在遮挡帧输出的低置信度坐标likelihood 0.3被直接渲染成热力图这些「幽灵点」在空间上随机跳动ConvLSTM 把它们学成了理毛时前肢快速摆动的特征。解决在render_heatmap前加一道过滤likelihood 低于阈值的点用上一帧的坐标填充或者直接置零不渲染。def filter_low_conf(coords, likelihood, thresh0.3): # coords: (T, N, 2), likelihood: (T, N) for t in range(1, len(coords)): mask likelihood[t] thresh coords[t][mask] coords[t-1][mask] # 用上一帧填充 return coords4.2 归一化用了整段视频的统计量现象同一只小鼠的训练集 F1 有 0.85换一只小鼠测试掉到 0.6。原因build_windows里用整段视频的 min/ptp 做归一化不同视频里小鼠在画面中的位置和箱体范围不同归一化后的坐标尺度不一致。解决用旷场箱体的物理边界做标定把像素坐标映射到箱体的厘米坐标这样跨视频的尺度就统一了。标定方法是在箱底四角贴标记点检测后算单应矩阵。4.3 ConvLSTM 隐状态没 detach 导致显存泄漏现象训练到第 20 个 batch 左右显存爆掉报 CUDA out of memory。原因如果自己实现 ConvLSTM 并在时间维度上循环每次前向都把隐状态挂在计算图上一个 batch 内 16 个时间步的图全保留。解决用pytorch-conv-lstm这类库时它内部已经处理了自己写的话在每个时间步后对隐状态做detach()或者用torch.no_grad()包住不需要反传的中间状态。4.4 类别标签在时间窗上没对齐现象模型在验证集上表现很好但实际推理时行为切换点总是延迟 0.5 秒左右。原因标注时是按帧标的行为类别但build_windows切窗口时一个窗口的标签取的是窗口最后一帧的类别而窗口覆盖的前 15 帧可能属于上一个行为。解决标签取窗口中间帧的类别或者用多数投票。更稳妥的做法是标注时按行为段标切窗口时只保留完全落在同一行为段内的窗口。4.5 用 accuracy 选模型而不是 macro-F1现象模型 accuracy 0.92但理毛和直立的 F1 只有 0.4。原因贴墙走和静止占 70% 以上模型只要全预测这两类就能拿到高 accuracy。解决训练时监控 macro-F1模型选择、学习率调度、早停都用 macro-F1。如果小类 F1 持续低于 0.5考虑过采样小类窗口或者把理毛细分成「前肢理毛」和「全身理毛」分别建模。5. 从能跑到好用推理加速、跨个体泛化和一个验证技巧模型训完之后真正落地还有两件事推理速度和跨个体泛化。推理这块ConvLSTM 是循环结构逐窗口推理在 15000 帧的视频上要跑几分钟。我的做法是把视频按 stride16 切成不重叠窗口用torch.no_grad()加半精度推理batch 开到 32一张 3090 上 10 分钟视频约 40 秒跑完。如果还要快可以把 ConvLSTM 换成因果卷积TCN并行度高一个量级但会损失一点长时依赖的建模能力看你对延迟的容忍度。跨个体泛化是这套流程最容易被低估的问题。同一批实验里不同小鼠的体型、毛色、活动量差异很大用 A 鼠数据训的模型直接测 B 鼠F1 掉 15 到 20 个点是常事。我一般做三件事一是训练集里至少包含 5 只以上不同小鼠的数据每只的窗口数尽量均衡二是用 leave-one-mouse-out 交叉验证而不是随机划分窗口——随机划分会让同一只鼠的窗口同时出现在训练和验证集指标虚高三是在热力图渲染时对坐标做随机缩放0.9 到 1.1和随机旋转±10 度做数据增强模拟个体姿态差异。验证模型是否真的学到了行为而不是数据集偏差我有个习惯把测试集里模型判为「理毛」的窗口对应的原始视频片段抽出来逐段看。如果模型判对的片段里小鼠确实在做理毛动作那说明它学到了如果有些片段小鼠明明在静止模型却判成理毛那大概率是热力图里有关键点抖动被误读。这个人工回看的过程很笨但比任何指标都可靠。我自己的教训是早期太信验证集 F1直接拿模型去跑新一批视频结果理毛的误报率高得离谱回看才发现是 DeepLabCut 在新视频上对耳部关键点的检测置信度整体偏低热力图里耳朵位置一直在飘。后来我在推理前加了一道关键点置信度的整体检查低于 0.5 的视频先重新标定再跑误报就压下去了。这套流程没有一劳永逸的参数每换一批设备或品系关键点检测和归一化这两步都要重新过一遍。希望帮到你。本文还有配套的精品资源点击获取