基于ConvLSTM的旷场实验小鼠行为自动识别与实现
2026/9/18 23:50:04 网站建设 项目流程

简介:一份聚焦ConvLSTM网络开展小鼠旷场实验行为自动识别的技术文档,面向动物行为学、计算机视觉及深度学习交叉领域的研究者与工程师,解决人工观察耗时、主观偏差大等问题。文档完整呈现了从关键点检测、特征图序列构建到ConvLSTM行为分类模型设计,再到众值滤波修正与时序行为参数统计的整套分析流程,包含鼻尖、左耳、右耳、尾根等关键点标注方式,直走、转身、修饰、静止、直立五类行为的识别分类,以及行为发生次数、持续时间、转变模式的计算规则,便于按步骤复现或借鉴模型优化思路。资源为1个docx文档,压缩包大小约18KB,内容结构紧凑,适合作为方法设计、论文写作或实验框架搭建的参考。已有138人学习,对希望快速了解行为识别技术方案的研究者具有实用价值。

1. 旷场实验的时长问题,ConvLSTM为什么值得试

一段旷场实验视频通常持续 10 到 30 分钟,如果做慢性应激或药物干预实验,样本量动辄几十只。过去靠人工观察秒级标注,工作量大不说,行为边界的判定还高度依赖观察经验——同一段视频,不同标注者的 kappa 一致性可能只有 0.7 左右。更麻烦的是,旷场实验里小鼠的"走动""站立"和"修饰"常常在几秒内交替发生,逐帧分类对时序敏感度要求极高。传统方法里,光流法勉强能抓住运动方向,却忽略了"在哪动";3D CNN 能同时编码时间空间,但需要极大数据量才能训稳。

基于 ConvLSTM 的做法是把视频切成一帧一帧的序列,交给卷积循环网络处理:卷积部分负责提取"小鼠在画面哪里、姿态如何"的空间特征,LSTM 结构负责把连续帧的信息串起来,让模型自己学到"先快走再急停"这类时空模式。这样既不用手工设计光流特征,也不需要把整段视频暴塞进显存。这个思路适合正在做动物行为自动识别、又不想依赖商业软件的研究者,也适合想尝试时空序列建模的工程师。本文会从行为标签定义开始,一直到训练和指标计算,给出一套能落地运行的流程。

2. ConvLSTM网络结构与旷场行为分类的理论基础

要建立一套可复现的旷场行为分类系统,先要把两个问题说清楚:一是你要模型预测什么,二是 ConvLSTM 凭什么能预测出这个东西。这两件事没对齐,后面所有参数都白调。

2.1 旷场实验中的行为学指标与标签体系

旷场实验的核心观测指标包括运动总距离、中央区域停留时间、站立次数和修饰行为频率,但"指标"是算出来的,要先有"行为类别"作为中间层。实践中最常用的标签体系有五类:静止、走动、站立(rear)、修饰(groom)、转圈(circle)。实际标注时,建议不要超过六类,因为行为类别越多,标注一致性和模型收敛难度都会剧烈上升。每类行为都需要明确的起止定义,比如"站立"指小鼠前爪离地、躯干直立,持续超过 0.5 秒。

行为标签标注的粒度直接决定模型输入序列的长度。推荐先把视频按 10 帧每秒抽帧,标注员在序列层标注行为区间,区间再拆成固定长度的样本。下表是一套我常用的标签定义和参考时长,可以作为标注手册的起点:

行为类别行为定义最小持续时长常见误判情况
静止身体位置无位移,仅头部微动0.5s与呼吸导致的躯干起伏混淆
走动四肢交替移动,位移 > 2 cm0.3s与静止后首次迈步混淆
站立前爪离地,躯干直立0.5s与攀爬箱壁动作混淆
修饰舔舐前爪、面部或梳理皮毛1.0s与短促抓挠混淆
转圈原地旋转,角度累计超过 180°0.8s与绕小半径行走混淆

标注完成后再把序列切割成固定长度,比如每个样本 16 帧。这 16 帧对应的是一段时间窗口,窗口里可能包含一个完整行为,也可能跨越两种行为。更合理的做法是取 24 帧,标签取窗口中间帧的行为类别,让模型在训练时同时看到行为前、中、后和尾部变化。这样做的好处是减少边界样本对训练的影响。切割后的样本要做类别统计,如果某类比其他类少 40% 以上,优先考虑过采样而不是换损失函数。

2.2 ConvLSTM为什么能同时看"看哪里"和"怎么动"

ConvLSTM 是卷积LSTM的缩写,它在普通 LSTM 的基础上,把输入到状态、状态到状态的变换从全连接矩阵乘法换成卷积操作。记忆单元、输入门、遗忘门、输出门这些结构依然存在,但每个门控的输入现在是一张特征图而不是一维向量。这让模型在时间维度上传递信息时,空间局部性也被保留下来——上一步学到的小鼠轮廓和这一步的轮廓按空间位置对应着流动。

数学上,ConvLSTM 的核心公式比普通 LSTM 直观得多。以时间步为 t 输入 X_t,上一时刻状态 H_(t-1) 和记忆 C_(t-1),遗忘门 F_t 由当前输入和上一隐藏状态经过卷积后接 sigmoid 得到,输入门 I_t 和候选记忆 C't 类似,更新后的记忆 C_t = F_t * C(t-1) + I_t * C'_t,输出 H_t = O_t * tanh(C_t)。这里的 * 表示卷积,不是逐元素乘法。卷积核大小通常取 3×3,padding 保持特征图尺寸不变。相比单独使用 3D CNN,ConvLSTM 的参数规模更可控,也能处理任意长度的时间序列,测试时不一定非要和训练序列等长。

为什么在旷场行为分析里选 ConvLSTM 而不是普通 LSTM 或 Transformer?普通 LSTM 需要把每一帧展平成一维向量,这对视频而言空间信息被强行打散,小鼠在左上角还是右下角对模型来说只是不同位置的像素组合;Transformer 的注意力能捕捉长程依赖,但在小样本的动物行为数据集上,预训练权重也不好找。ConvLSTM 的归纳偏置正合适——卷积保证平移等变性,循环结构保证时间连续性,这正好符合"小鼠的运动轨迹和姿态是连续时空变化"这一事实。

2.3 用PyTorch定义ConvLSTM单元

实际建模时,我不会直接手写完整的 ConvLSTM 循环,而是先实现一个 ConvLSTMCell 组件,再在序列上循环展开。这里给出一个简洁的 PyTorch 实现,可以放在模型文件底部复用:

import torch import torch.nn as nn class ConvLSTMCell(nn.Module): def __init__(self, in_channels, hidden_channels, kernel_size=3): super().__init__() self.hidden_channels = hidden_channels padding = kernel_size // 2 self.conv = nn.Conv2d(in_channels + hidden_channels, 4 * hidden_channels, kernel_size=kernel_size, padding=padding) def forward(self, x, prev_state): # prev_state 是 (H, C) 的二元组,必须是初始化过的状态 prev_h, prev_c = prev_state combined = torch.cat([x, prev_h], dim=1) gates = self.conv(combined) i, f, o, g = torch.split(gates, self.hidden_channels, dim=1) i = torch.sigmoid(i) f = torch.sigmoid(f) o = torch.sigmoid(o) g = torch.tanh(g) c = f * prev_c + i * g h = o * torch.tanh(c) return h, c

这个ConvLSTMCell的输入是四维张量(batch, channels, height, width)in_channels是输入图像或特征图的通道数,hidden_channels是内部状态的特征图数量。conv层把当前输入和上次隐藏状态拼接后,一次性算出四个门控值,然后按通道维度拆开。要注意的是,kernel_size设为奇数,padding 用kernel_size // 2,目的是让特征图在卷积后保持原尺寸,否则循环几十帧后空间维度会缩得无法对齐。prev_state需要外部传入,初始化时通常用全零张量。这个单元实现了 ConvLSTM 的基本前向过程,训练时梯度会自动沿着时间步反传,不需要额外处理。

3. 数据预处理与序列构建:从原始视频到特征张量

模型的输入决定模型的上限。旷场实验视频不像自然图像那样有丰富的纹理信息,小鼠在画面里通常只占很小一块区域,大量背景像素对分类不提供有效梯度。直接拿整帧训练,除了浪费显存,还容易让模型把笼子角落的污渍学成重要特征。所以在进入网络之前,要完成帧抽取、前景区域裁剪和序列切割这三步。

3.1 帧抽取与运动区域自动裁剪

第一步先把视频转成图片序列。我习惯用 OpenCV 的VideoCapture读取视频,按目标帧率抽帧。旷场实验里 10 fps 已经足够捕捉行为变化,过高的帧率只会让相邻帧高度重复,训练效率反而下降。抽帧完成后,用背景减除或者帧间差分得到激活区域,然后找出所有激活像素的外接矩形,适当外扩像素作为序列输入区域。

下面的代码展示了一个最小实现:抽帧、差分、找轮廓、裁剪并缩放到固定尺寸。

import cv2 import numpy as np def extract_roi_frames(video_path, out_size=128, fps_target=10): cap = cv2.VideoCapture(video_path) src_fps = cap.get(cv2.CAP_PROP_FPS) frame_interval = max(1, int(src_fps // fps_target)) frames = [] idx = 0 prev_gray = None while True: ret, frame = cap.read() if not ret: break if idx % frame_interval != 0: idx += 1 continue gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) gray = cv2.GaussianBlur(gray, (5, 5), 0) if prev_gray is not None: diff = cv2.absdiff(gray, prev_gray) _, thresh = cv2.threshold(diff, 25, 255, cv2.THRESH_BINARY) contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if contours: x, y, w, h = cv2.boundingRect(np.vstack(contours)) # 外扩 20% 保证金鼠四肢和尾巴在框内 pad_x, pad_y = int(w * 0.2), int(h * 0.2) x = max(0, x - pad_x) y = max(0, y - pad_y) w = min(frame.shape[1] - x, w + 2 * pad_x) h = min(frame.shape[0] - y, h + 2 * pad_y) roi = frame[y:y+h, x:x+w] else: roi = frame roi = cv2.resize(roi, (out_size, out_size)) frames.append(roi) prev_gray = gray idx += 1 cap.release() return np.stack(frames, axis=0)

这段代码的关键点是frame_interval的计算,它把原始视频帧率统一到目标帧率。findContours之前用背景差分得到二值图,差分的灵敏度由阈值 25 控制——这个值在光照均匀的旷场箱里基本够用;如果实验环境有反光,建议换成基于均值背景模型的cv2.createBackgroundSubtractorMOG2。ROI 外扩 20% 是保证裁剪框不会卡住小鼠的尾巴和胡须,因为行为分类里尾巴摆动的信息并不是关键,但频繁截断边缘会让模型不稳定。如果检测不到轮廓,代码保留了整帧作为输入,这是一个兜底逻辑,防止空序列导致训练断裂。

3.2 序列滑窗切割与数据增强

裁剪出来的帧序列长度可能上千,需要滑窗切割成固定长度的样本。窗口长度通常取 16 或 24 帧,对应 1.6 秒到 2.4 秒的行为片段。步长设为窗口长度的四分之一,也就是 4 或 6 帧,让相邻样本之间有大面积重叠,相当于做了时序上的数据增强。每个样本的标签取窗口中心帧的标注类别,这样模型被迫从上下文推断中心时刻的行为,而不是偷懒只看第一帧。

数据增强方面,旷场视频的样本量通常只有几千条,需要做一些轻度的空间增强。我常用的组合是随机水平翻转、小角度旋转(±10°)和色彩抖动,这三项都不会改变行为本身的语义——水平翻转对旷场实验成立,因为箱体是对称的;旋转角度控制在 10° 以内,避免把正常的直立甩出感受野。时间维度上不要做随机裁剪,因为前后帧的因果顺序对行为分类很重要,裁剪时间轴会破坏"先走再停"这类时间模式。增强之后的样本会暂时存放在内存列表中,如果数据量超过 2 万条,建议改用一个在__getitem__里实时做增强的 PyTorch Dataset,避免吃光内存。

3.3 归一化与张量维度设计

统一后的输入张量形状是(batch, seq_len, channels, height, width)。PyTorch 里nn.Conv3d接受(N, C, D, H, W),但 ConvLSTM 需要的是(N, D, C, H, W),所以批次维度在循环里要挪到第 0 维。归一化时,按照 ImageNet 的均值方差用(0.5, 0.5, 0.5)简单归一到[-1, 1]即可,不要用 ImageNet 统计量,因为旷场图像的灰度分布差异很大。更好的做法是在训练集上计算每个通道的均值和标准差,存成 JSON 文件,推理时加载同一个统计量。

对于一个批次内长度不一的样本,标准做法是 pad 到相同长度再训练,但行为分类场景下所有样本固定长度,反而省事。我建议在数据生成阶段就把长度固定为seq_len=16,输入高度为 64 或 128 时,通道数取 3。模型结构上,第一层 ConvLSTM 的输入通道数就是 3。如果显存不够,可以把图像缩小到 64×64,再用两层 ConvLSTM 逐层提取特征。

4. 模型实现与训练流程:全流程可直接套用

模型结构这部分,强烈建议在数据集上先跑通一个最小实验:单只小鼠、十段视频、只分两类。千万不要一上来就做五分类和复杂网络,先把数据管线验证好,再加参数量。下面的结构是最常用的三层组合:ConvLSTM + ConvLSTM + 全连接分类头。

4.1 构建ConvLSTM行为分类模型

先实现一个多层 ConvLSTM 封装,再叠加分类头。这里的逻辑是:第一层 ConvLSTM 输出 32 张特征图,第二层输出 64 张,最后把所有时间步的特征图做全局平均池化,得到一个 64 维向量,再过全连接层。全局池化比取最后时间步更稳,因为旷场行为的关键信息可能出现在窗口中间而非末尾。

import torch import torch.nn as nn class ConvLSTMClassifier(nn.Module): def __init__(self, in_channels=3, hidden_channels=32, num_layers=2, num_classes=5): super().__init__() self.num_layers = num_layers self.hidden_channels = hidden_channels cells = [] for i in range(num_layers): input_ch = in_channels if i == 0 else hidden_channels cells.append(ConvLSTMCell(input_ch, hidden_channels)) self.cells = nn.ModuleList(cells) self.avg_pool = nn.AdaptiveAvgPool2d(1) self.fc = nn.Sequential( nn.Dropout(0.4), nn.Linear(hidden_channels, num_classes) ) def forward(self, x): # x 形状: (batch, seq_len, in_channels, H, W) batch, seq_len, _, H, W = x.shape hidden_states = [] cell_states = [] # 初始化每一层的隐藏状态和记忆状态 for _ in range(self.num_layers): hidden_states.append( torch.zeros(batch, self.hidden_channels, H, W, device=x.device) ) cell_states.append( torch.zeros(batch, self.hidden_channels, H, W, device=x.device) ) for t in range(seq_len): x_t = x[:, t, :, :, :] for layer in range(self.num_layers): h, c = self.cells[layer](x_t, (hidden_states[layer], cell_states[layer])) hidden_states[layer] = h cell_states[layer] = c x_t = h # 下一层的输入是当前层的输出 # 取最后一层所有时间步的输出做池化 out = self.avg_pool(hidden_states[-1]).flatten(1) return self.fc(out)

forward里,每一层的隐藏状态都初始化成全零张量,并保持和输入图像相同的高度宽度。循环时,x_tx[:, t]切片得到,然后在多层之间逐层传递。这里有个容易被忽略的细节:AdaptiveAvgPool2d(1)是对最后一层隐藏状态做空间池化,而不是对每次时间步都做池化,否则会丢掉时间上下文。Dropout放在全连接之前,只在训练时生效。如果你希望模型输出行为概率,可以在self.fc后再接nn.Softmax(dim=1),但训练阶段一般用CrossEntropyLoss自带 LogSoftmax,不要手动加。

4.2 训练超参数与收敛检查

旷场数据集规模决定了模型容量和正则化强度。数据量在 5000 到 20000 个样本时,推荐用下面这组参数作为起点:

超参数推荐值调整建议
seq_len16 帧扩大窗口到 24 可提升站立检测精度
hidden_channels32数据量大时调到 64
批次大小16显存小降到 8
初始学习率0.001AdamW 下建议配合 warmup
权重衰减0.01对减少过拟合有帮助
训练轮数30用早停法,patience=5

训练时我用 AdamW 优化器,学习率采用线性 warmup 加余弦退火。具体到旷场数据,类别不平衡会直接影响收敛。如果你统计出"静止"占 60%,"站立"只占 8%,可以给CrossEntropyLoss传入一个逆类别频率的权重向量。这个做法比简单的过采样更平稳,因为过采样会让相邻样本几乎相同,而类别权重保留了数据的原始分布。

训练循环中每 5 步打印一次 loss,等到验证集准确率不再提升时保存最好模型。这里有一个容易踩的坑:把时序样本随机切分成训练集和验证集时,来自同一只小鼠、同一段视频的相邻样本会同时出现在两边,导致验证集虚高。正确做法是按视频维度分组,用GroupKFold或者手动把每只小鼠的视频整体划到验证集。

4.3 显存优化与推理加速

如果输入分辨率是 128 或序列长度超过 24,显存占用会明显上升。常见做法是把输入降采样到 64×64,或者让 ConvLSTM 的 kernel size 从 3 改成 5 不做下采样。另一种更有效的方案是在第一个 ConvLSTM 之前插入一个简单的卷积下采样层,用nn.Conv2d+MaxPool2d把分辨率缩到 32×32,后面循环的计算量骤降。推理端,我一般把模型转成 ONNX 格式,然后用 TensorRT 在 GPU 上跑半精度,单段 30 分钟视频的推理时间能从分钟级压缩到秒级。

训练脚本最后保存两类文件:模型权重和归一化统计量。归一化统计量在预处理脚本里计算,存储为一个字典,推理脚本加载时直接使用。否则训练时归一化到 [-1, 1],推理时却按 [0, 1],准确率会掉得莫名其妙。

5. 行为序列解码与实验指标计算技巧

模型输出的是一帧级概率向量,不是实验指标。要把概率变成可用的行为分析结论,还需要做两件事:把逐帧预测平滑成行为单元,再从行为单元累加出旷场实验的核心指标。

5.1 用滑窗投票消除抖动预测

旷场行为天然具有连续性,模型偶尔会把一个长"站立"预测成"站立-走动-站立",这种单帧抖动在最终指标里会被放大。常见做法是窗口平滑,取前后各 3 帧做一个多数投票,把孤立异常值纠正过来。下面这个函数直接操作 NumPy 数组。

def smooth_predictions(pred_ids, window=7): pad = window // 2 pred_ids = np.pad(pred_ids, (pad, pad), mode='edge') smoothed = [] for i in range(pad, len(pred_ids) - pad): seg = pred_ids[i - pad:i + pad + 1] counts = np.bincount(seg) smoothed.append(np.argmax(counts)) return np.array(smoothed)

平滑窗口取 7 帧,意味着 0.7 秒内的一次闪烁会被吸收。注意窗口不能设太大,否则 0.5 秒的短促站立会被前一个"走动"完全吞掉。如果实验关注短时行为,建议把预测概率存下来而不是只存类别,在概率层面做指数加权平均,这比投票更精细。

5.2 从轨迹计算距离和中央区域时间

对于已经得到逐帧行为的样本,运动总距离和中央区域停留时间必须结合原始坐标计算。坐标可以用预处理阶段得到的外接矩形中心点替代小鼠位置,但更可靠的是用前景轮廓的质心。计算距离时要先做坐标平滑,否则相邻帧的像素抖动会被当成实际位移。

def calc_distance(centers, fps=10): # centers: (N, 2) ndarray,按时间排序 deltas = np.diff(centers, axis=0) step = np.linalg.norm(deltas, axis=1) # 过滤小于 0.5 cm 的抖动位移 step = step[step > 5] return float(step.sum()) / fps

这里的距离单位是像素,实验中需要先用标尺把像素换算成厘米。中央区域时间计算则简单很多,只要知道旷场箱的内边界和中央区域比例,把每一帧坐标落在中央区域的帧数除以总帧数,再乘以视频时长即可。如果你关注焦虑样行为,通常报告中央区域停留时间占比和进入中央区次数,这两者都比总距离更敏感。

最后再提醒一个验证细节:计算完指标后,建议随机抽 10 段 30 秒的预测结果,把每一帧的预测类别按颜色叠加到原视频上,人工回放一遍。这个动作能快速暴露平滑参数是否过度、标签定义是否清晰、模型是否在特定光照下系统性出错。跑一次这个可视化流程,比看十次损失曲线都更有用。

本文还有配套的精品资源,点击获取

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

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

立即咨询