深入 ‘Video-based Memory Augmentation’:构建一个能记住‘刚才视频中发生了什么’的长时记忆图结构

在人工智能领域,我们正努力让机器不仅仅是处理信息,更能理解信息,并在此基础上建立长期记忆。特别是对于视频这种富含时序和语义信息的媒体,如何让机器“记住”视频中“刚才发生了什么”,并能基于这些记忆进行推理,是构建更智能AI系统的关键一步。今天,我们将深入探讨“基于视频的记忆增强”这一主题,重点关注如何构建一个能够捕捉和存储近期视频事件的长时记忆图结构。

一、 记忆之于AI:为何我们需要视频长时记忆?

人类的记忆系统是其智能的核心。我们通过记忆过去,理解现在,并预测未来。对于AI而言,尤其是处理动态、连续的视频流时,拥有一个强大的记忆机制同样至关重要。

1. 挑战的源头:视频的特性
视频数据具有以下几个显著特点,也正是这些特点构成了记忆增强的挑战:

  • 连续性与冗余性: 视频是连续的帧序列,相邻帧之间通常高度相似,包含大量冗余信息。
  • 时序性: 事件的发生有严格的时间顺序,理解事件间的因果、并列、包含关系需要强大的时序推理能力。
  • 语义复杂性: 视频中的事件往往涉及多个主体、客体、动作、场景以及它们之间的复杂交互。
  • 信息量巨大: 高分辨率、长时间的视频流会产生海量数据,直接存储所有原始信息是不切实际的。
  • 遗忘与更新: 随着时间的推移,旧的、不重要的信息需要被淡化或遗忘,新的、重要的信息则需要被及时整合。

2. 记忆增强的目标
我们的目标是构建一个AI系统,它能够:

  • 实时或近实时处理: 在视频流播放时,同步地提取和理解关键信息。
  • 事件抽象与结构化: 将原始的像素数据抽象为有意义的事件、实体和关系,并以结构化的形式存储。
  • 长期记忆: 存储这些结构化信息,使其可以在未来被检索和利用。这里的“长时”并非指无限期,而是指超越当前帧或短时片段的上下文。
  • 高效检索与推理: 能够根据用户的查询快速检索相关记忆,并进行简单的逻辑推理。

3. 类比人类记忆
我们可以将这种视频记忆系统类比为人类的“情景记忆”和“语义记忆”的结合。情景记忆让我们记住特定时间地点发生的具体事件,而语义记忆则存储关于世界的一般知识和概念。我们的长时记忆图结构将尝试融合这两种记忆形式,用图的节点表示实体和事件,用边表示它们之间的关系,从而构建一个丰富的、可查询的知识库。

二、 核心架构:从像素到记忆图

要实现视频的长时记忆,我们需要一个端到端的处理流水线,将原始视频数据逐步转化为结构化的知识图谱。其核心架构通常包含以下几个关键模块:

  1. 视频流预处理与特征提取 (Video Stream Processing & Feature Extraction): 将原始视频帧转换为机器可理解的数值特征。
  2. 事件检测与语义解析 (Event Detection & Semantic Parsing): 从特征序列中识别出有意义的事件、实体及其属性。
  3. 知识表示:长时记忆图构建 (Knowledge Representation: Long-Term Memory Graph Construction): 将检测到的事件和实体以图结构的形式存储起来。
  4. 记忆管理与整合 (Memory Management & Consolidation): 维护图的动态性,包括新信息的添加、旧信息的衰减、冗余信息的合并。
  5. 记忆检索与推理 (Memory Retrieval & Reasoning): 根据查询从记忆图中提取信息或进行逻辑推理。

接下来,我们将逐一深入探讨这些模块,并重点关注长时记忆图的构建。

三、 视频流预处理与特征提取

这是记忆系统的感知层,负责将原始视频数据“翻译”成AI可以理解的语言。

1. 挑战:维度灾难与语义鸿沟
原始视频数据是高维像素矩阵,直接处理计算量巨大且难以捕捉高级语义。我们需要通过特征提取,将这些高维数据降维到更具代表性的特征向量,同时尽可能地保留语义信息。

2. 常用特征类型与模型

  • 视觉特征 (Visual Features):

    • 帧级特征: 对每一帧图片独立提取特征,通常使用预训练的CNN模型(如ResNet, VGG, EfficientNet)。这些模型在ImageNet等大规模图像数据集上训练,能有效捕捉图像中的物体、场景等静态视觉信息。
    • 时序特征: 考虑到视频的连续性,仅仅依赖帧级特征不足以捕捉动作和变化。3D CNN(如C3D, I3D)、Two-Stream Networks、SlowFast等模型能够同时处理空间和时间维度,更好地捕捉视频中的运动信息。Transformer架构(如VideoMAE, MViT)也开始在视频领域展现出强大能力,通过自注意力机制捕捉长距离时空依赖。
  • 语义特征 (Semantic Features):

    • 物体检测 (Object Detection): 使用YOLO、Faster R-CNN、DETR等模型识别视频中出现的物体,并获取其类别、位置(边界框)和置信度。
    • 动作识别 (Action Recognition): 使用TSM、ActionFormer、Temporal Action Localization (TAL) 模型识别视频中发生的具体动作,如“奔跑”、“跳跃”、“握手”。
    • 场景理解 (Scene Understanding): 识别视频发生的场景类型,如“客厅”、“街道”、“办公室”。
    • 人物姿态估计 (Pose Estimation): 识别视频中人物的关键骨骼点,用于理解人物动作的细节。

3. 特征融合与表示
通常,我们会融合多种特征来获得更全面的视频理解。例如,将帧级视觉特征与检测到的物体、动作特征结合起来。这些特征通常表示为高维向量,作为后续事件检测模块的输入。

4. 代码示例:基于预训练ResNet的帧级特征提取
这里我们使用torchvision库中预训练的ResNet模型来提取视频帧的特征。

import cv2
import torch
import torchvision.models as models
import torchvision.transforms as transforms
import numpy as np
from PIL import Image

class FeatureExtractor:
    def __init__(self, model_name='resnet50', device='cuda'):
        """
        初始化特征提取器。
        Args:
            model_name (str): 使用的预训练模型名称,例如 'resnet50'。
            device (str): 运行设备,'cuda' 或 'cpu'。
        """
        self.device = torch.device(device if torch.cuda.is_available() else 'cpu')

        # 加载预训练模型,并移除最后一层(分类层),只保留特征提取部分
        if model_name == 'resnet50':
            model = models.resnet50(pretrained=True)
            self.model = torch.nn.Sequential(*(list(model.children())[:-1])) # 移除avgpool和fc层
        elif model_name == 'efficientnet_b0':
            model = models.efficientnet_b0(pretrained=True)
            self.model = torch.nn.Sequential(*(list(model.children())[:-1])) 
        else:
            raise ValueError(f"Unsupported model: {model_name}")

        self.model.eval() # 设置为评估模式
        self.model.to(self.device)

        # 定义图像预处理转换
        self.preprocess = transforms.Compose([
            transforms.Resize(256),
            transforms.CenterCrop(224),
            transforms.ToTensor(),
            transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
        ])

    def extract(self, frame: np.ndarray) -> np.ndarray:
        """
        从单帧图像中提取特征向量。
        Args:
            frame (np.ndarray): OpenCV格式的BGC图像帧。
        Returns:
            np.ndarray: 提取到的特征向量。
        """
        # 将OpenCV BGR图像转换为PIL RGB图像
        frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
        pil_image = Image.fromarray(frame_rgb)

        # 应用预处理
        input_tensor = self.preprocess(pil_image)
        input_batch = input_tensor.unsqueeze(0).to(self.device) # 添加batch维度并移动到设备

        with torch.no_grad(): # 在推理模式下不计算梯度
            features = self.model(input_batch)

        # 展平特征向量
        return features.squeeze().cpu().numpy()

# 示例使用
if __name__ == "__main__":
    video_path = "path/to/your/video.mp4" # 替换为你的视频路径
    cap = cv2.VideoCapture(video_path)

    if not cap.isOpened():
        print("Error: Could not open video.")
        exit()

    feature_extractor = FeatureExtractor(model_name='resnet50', device='cuda')
    frame_features = []
    frame_timestamps = [] # 记录帧的时间戳
    frame_idx = 0

    while True:
        ret, frame = cap.read()
        if not ret:
            break

        # 每隔N帧提取一次特征,以降低计算量
        if frame_idx % 10 == 0: # 例如,每10帧提取一次
            timestamp_ms = cap.get(cv2.CAP_PROP_POS_MSEC) # 获取当前帧的时间戳(毫秒)
            features = feature_extractor.extract(frame)
            frame_features.append(features)
            frame_timestamps.append(timestamp_ms)
            # print(f"Frame {frame_idx}, Timestamp: {timestamp_ms:.2f}ms, Feature shape: {features.shape}")

        frame_idx += 1

        # 可视化,按'q'退出
        # cv2.imshow('Video Frame', frame)
        # if cv2.waitKey(1) & 0xFF == ord('q'):
        #     break

    cap.release()
    cv2.destroyAllWindows()

    print(f"Total {len(frame_features)} feature vectors extracted.")
    # frame_features 现在是一个列表,每个元素是一个numpy数组,代表一帧的特征
    # frame_timestamps 对应每帧特征的时间戳

四、 事件检测与语义解析

有了视频的特征表示,下一步就是从这些连续的、低级的特征中识别出高层语义事件和实体。

1. 事件的定义与粒度
“事件”可以有不同的粒度:

  • 微观事件 (Micro-events): 单个对象的细微动作或状态变化,如“人走了一步”、“球弹了一下”。
  • 宏观事件 (Macro-events): 包含多个主体、客体和动作的复杂场景,如“两个人正在讨论”、“汽车在十字路口转弯”。
    我们的记忆系统需要能够捕捉不同粒度的事件,并能将它们关联起来。

2. 事件检测技术

  • 基于特征聚类与变化点检测: 当视频特征在短时间内发生显著变化时,可能预示着一个新事件的开始。可以使用聚类算法(如K-Means, DBSCAN)或变化点检测算法(如PELT, E-Divisive)来识别这些变化。
  • 基于预训练模型: 直接利用物体检测、动作识别、场景分类等模型来识别视频中的特定元素。
    • 物体检测: 识别“人”、“车”、“椅子”等实体,并获取它们的边界框和置信度。
    • 动作识别: 识别“跑步”、“挥手”、“坐下”等动作。
    • 人物姿态估计: 识别关键骨骼点,用于更精细的动作分析。
  • 语义角色标注 (Semantic Role Labeling, SRL) / 视频场景图生成 (Video Scene Graph Generation): 这是更高级的语义解析技术,旨在从视频中提取出“谁做了什么”、“在什么地方”、“和谁一起”这样的三元组信息 (subject-verb-object)。这直接为知识图谱的构建提供了天然的节点和边。

3. 事件的表示
一个事件不仅仅是“发生了什么”,还包括“谁”、“何时”、“何地”、“如何”等信息。因此,一个事件的表示可能包括:

  • 事件类型: 如“行走”、“交互”、“状态改变”。
  • 主体 (Subject): 执行动作的实体,如“Person A”。
  • 客体 (Object): 动作作用的实体,如“Door”。
  • 地点 (Location): 事件发生的区域,如“Kitchen”。
  • 时间 (Timestamp/Duration): 事件开始和结束的时间。
  • 属性 (Attributes): 额外的描述信息,如“快速”、“高兴地”。
  • 置信度 (Confidence Score): 模型对该事件识别的确定程度。

4. 代码示例:简化版事件检测与实体提取
这里我们模拟一个简单的事件检测过程,假设我们已经有了一个物体检测器和一个动作识别器。

import numpy as np
import uuid # 用于生成唯一ID

class Event:
    """
    事件的抽象表示。
    """
    def __init__(self, event_type: str, timestamp_start: float, timestamp_end: float, 
                 subject_id: str = None, object_id: str = None, location: str = None,
                 confidence: float = 1.0, attributes: dict = None, event_id: str = None):
        self.id = event_id if event_id else str(uuid.uuid4())
        self.event_type = event_type
        self.timestamp_start = timestamp_start
        self.timestamp_end = timestamp_end
        self.subject_id = subject_id
        self.object_id = object_id
        self.location = location
        self.confidence = confidence
        self.attributes = attributes if attributes is not None else {}

    def __repr__(self):
        return (f"Event(ID={self.id[:8]}, Type='{self.event_type}', "
                f"Time=[{self.timestamp_start:.2f}-{self.timestamp_end:.2f}]ms, "
                f"Subject={self.subject_id[:8] if self.subject_id else 'None'}, "
                f"Object={self.object_id[:8] if self.object_id else 'None'})")

class Entity:
    """
    实体的抽象表示。
    """
    def __init__(self, entity_type: str, name: str = None, bounding_box: list = None, 
                 timestamp_seen: float = None, entity_id: str = None, attributes: dict = None):
        self.id = entity_id if entity_id else str(uuid.uuid4())
        self.entity_type = entity_type
        self.name = name if name else f"{entity_type}_{self.id[:4]}"
        self.bounding_box = bounding_box # [x1, y1, x2, y2]
        self.timestamp_seen = timestamp_seen
        self.attributes = attributes if attributes is not None else {}

    def __repr__(self):
        return (f"Entity(ID={self.id[:8]}, Type='{self.entity_type}', Name='{self.name}', "
                f"Seen={self.timestamp_seen:.2f}ms)")

class EventDetector:
    def __init__(self, object_detector_model=None, action_recognizer_model=None):
        """
        初始化事件检测器。
        Args:
            object_detector_model: 模拟的物体检测模型。
            action_recognizer_model: 模拟的动作识别模型。
        """
        self.obj_detector = object_detector_model
        self.action_rec = action_recognizer_model
        self.known_entities = {} # 存储已知实体,用于ID关联

    def detect_and_extract(self, frame_features: np.ndarray, timestamp: float) -> tuple:
        """
        模拟从一帧特征中检测实体和事件。
        在真实场景中,这会调用实际的物体检测和动作识别模型。
        Args:
            frame_features (np.ndarray): 当前帧的特征向量。
            timestamp (float): 当前帧的时间戳。
        Returns:
            tuple: (entities_in_frame, events_in_frame)
        """
        entities_in_frame = []
        events_in_frame = []

        # --- 模拟物体检测 ---
        # 假设这里调用了self.obj_detector.predict(frame_features)
        # 并返回了一些检测结果
        simulated_objects = []
        if timestamp < 2000: # 视频前2秒出现一个人
            simulated_objects.append({'type': 'Person', 'name': 'Person A', 'bbox': [100, 100, 200, 300]})
        elif timestamp >= 2000 and timestamp < 5000: # 2-5秒出现一个人和一张桌子
            simulated_objects.append({'type': 'Person', 'name': 'Person A', 'bbox': [120, 110, 210, 310]})
            simulated_objects.append({'type': 'Table', 'name': 'Table 1', 'bbox': [300, 400, 500, 600]})
        elif timestamp >= 5000 and timestamp < 8000: # 5-8秒出现一个人、一张桌子和一杯咖啡
            simulated_objects.append({'type': 'Person', 'name': 'Person A', 'bbox': [150, 130, 230, 330]})
            simulated_objects.append({'type': 'Table', 'name': 'Table 1', 'bbox': [310, 410, 510, 610]})
            simulated_objects.append({'type': 'CoffeeCup', 'name': 'Cup 1', 'bbox': [350, 450, 380, 480]})

        for obj_data in simulated_objects:
            # 检查是否是已知实体(例如通过物体跟踪或位置相似性)
            entity_id = None
            for eid, ent in self.known_entities.items():
                if ent.entity_type == obj_data['type'] and ent.name == obj_data['name']:
                    # 简化:如果类型和名字相同,则认为是同一个实体
                    entity_id = eid
                    break

            entity = Entity(
                entity_type=obj_data['type'], 
                name=obj_data['name'], 
                bounding_box=obj_data['bbox'], 
                timestamp_seen=timestamp,
                entity_id=entity_id
            )
            if entity_id is None: # 新实体,加入已知实体列表
                self.known_entities[entity.id] = entity
            else: # 更新已知实体的最新状态
                self.known_entities[entity.id].bounding_box = obj_data['bbox']
                self.known_entities[entity.id].timestamp_seen = timestamp

            entities_in_frame.append(self.known_entities[entity.id])

        # --- 模拟动作识别 ---
        # 假设这里调用了self.action_rec.predict(frame_features)
        # 并返回了一些检测结果
        simulated_actions = []
        person_a_id = next((e.id for e in entities_in_frame if e.name == 'Person A'), None)
        table_1_id = next((e.id for e in entities_in_frame if e.name == 'Table 1'), None)
        cup_1_id = next((e.id for e in entities_in_frame if e.name == 'Cup 1'), None)

        if person_a_id and timestamp >= 2500 and timestamp < 4000:
            simulated_actions.append({'type': 'Standing', 'subject_id': person_a_id, 'confidence': 0.9})
        if person_a_id and table_1_id and timestamp >= 5500 and timestamp < 7000:
            simulated_actions.append({'type': 'Approaching', 'subject_id': person_a_id, 'object_id': table_1_id, 'confidence': 0.85})
        if person_a_id and cup_1_id and timestamp >= 6000 and timestamp < 7500:
            simulated_actions.append({'type': 'Holding', 'subject_id': person_a_id, 'object_id': cup_1_id, 'confidence': 0.92})

        for action_data in simulated_actions:
            event = Event(
                event_type=action_data['type'],
                timestamp_start=timestamp, # 简化:事件持续一帧
                timestamp_end=timestamp,
                subject_id=action_data.get('subject_id'),
                object_id=action_data.get('object_id'),
                confidence=action_data['confidence']
            )
            events_in_frame.append(event)

        return entities_in_frame, events_in_frame

# 示例使用
if __name__ == "__main__":
    event_detector = EventDetector()

    # 模拟视频帧和时间戳
    simulated_timestamps = np.arange(0, 10000, 500) # 每500ms一帧

    all_detected_entities = []
    all_detected_events = []

    for ts in simulated_timestamps:
        # 模拟帧特征(这里只是一个占位符)
        dummy_frame_features = np.random.rand(2048) 

        entities, events = event_detector.detect_and_extract(dummy_frame_features, ts)

        if entities:
            # print(f"n--- Timestamp {ts:.2f}ms ---")
            for ent in entities:
                # print(f"  Detected Entity: {ent}")
                all_detected_entities.append(ent)
            for evt in events:
                # print(f"  Detected Event: {evt}")
                all_detected_events.append(evt)

    print("n--- All Detected Entities (Unique IDs) ---")
    unique_entities = {e.id: e for e in all_detected_entities}.values()
    for ent in unique_entities:
        print(ent)

    print("n--- All Detected Events ---")
    for evt in all_detected_events:
        print(evt)

五、 知识表示:长时记忆图构建

这是整个系统的核心,我们将把检测到的实体和事件结构化地存储在一个图数据库中,作为AI的长期记忆。

1. 为什么选择图结构?

  • 自然表示关系: 现实世界中的实体和事件之间存在复杂的关联,图结构(节点和边)能够非常直观且强大地表示这些关系。
  • 灵活扩展: 随着新的信息涌入,可以方便地添加新的节点和边,而无需修改整个数据模式。
  • 高效查询与推理: 图数据库针对关系查询进行了优化,能够高效地进行路径查找、模式匹配和多跳推理。
  • 语义丰富: 节点和边都可以附加属性,存储更丰富的上下文信息。

2. 图的基本组成
一个记忆图由以下元素构成:

  • 节点 (Nodes): 代表视频中的实体或事件。
    • 实体节点 (Entity Nodes): 人、物体、地点、抽象概念。
    • 事件节点 (Event Nodes): 动作、交互、状态变化。
  • 边 (Edges/Relationships): 连接节点,表示它们之间的关系。
    • 结构关系: “属于”、“包含”。
    • 时序关系: “发生在前”、“同时发生”、“导致”。
    • 语义关系: “执行”、“拥有”、“位于”、“影响”。
  • 属性 (Properties): 附加在节点和边上的键值对,提供更详细的信息。

3. 记忆图的本体设计 (Ontology Design)
一个良好的本体设计是构建有效记忆图的基础。它定义了可以存在哪些类型的节点、哪些类型的边以及它们可以拥有哪些属性。

表1:记忆图节点类型示例

节点类型 描述 核心属性 示例
Person 视频中的人物 name, gender, age_range, bbox Person A, Person B
Object 视频中的可识别物体 category, sub_category, bbox, color Table, CoffeeCup, Laptop
Location 视频中的场景或特定区域 name, type, spatial_coords LivingRoom, Kitchen, Doorway
Action 具体的动作 verb, duration, confidence Walking, Sitting, PickingUp
Interaction 实体间的交互 type, duration, confidence Talking, Handshake, PassingObject
StateChange 实体状态的变化 entity_id, old_state, new_state, timestamp Door_Opened, Light_On
Frame 视频的特定帧(作为时间锚点) timestamp_ms, frame_idx Frame_123, Frame_456
Segment 视频的连续片段(包含多个事件) start_ms, end_ms, summary Segment_0-10s

表2:记忆图边类型示例

边类型 描述 源节点类型 目标节点类型 核心属性 示例
PERFORMS 某人执行某个动作 Person Action confidence, start_ms Person A -PERFORMS-> Walking
INTERACTS_WITH 某实体与另一实体交互 Person/Object Person/Object type, confidence, start_ms Person A -INTERACTS_WITH-> Table
LOCATED_AT 某实体或事件发生在某地点 Person/Object/Action Location confidence, start_ms Person A -LOCATED_AT-> LivingRoom
HAS_OBJECT 某场景或人拥有某物体 Location/Person Object confidence, start_ms LivingRoom -HAS_OBJECT-> Sofa
PRECEDES 事件A发生在事件B之前 Event Event gap_ms Action A -PRECEDES-> Action B
CONTAINS 视频片段包含特定事件 Segment Event start_offset_ms Segment_0-10s -CONTAINS-> Action A
AT_FRAME 实体或事件出现在特定帧 Person/Object/Action Frame bbox (for entity) Person A -AT_FRAME-> Frame_123
CAUSES 事件A导致事件B发生 Event Event confidence DroppingCup -CAUSES-> CupBreaking
HAS_STATE_CHANGE 实体经历了状态变化 Object StateChange timestamp Door -HAS_STATE_CHANGE-> Door_Opened

4. 记忆图的实现
在实际系统中,我们会使用图数据库(如Neo4j, ArangoDB, Amazon Neptune)来存储和管理这个图。这些数据库提供了高效的图查询语言(如Cypher for Neo4j),以及扩展性强、支持事务的存储能力。

对于讲座中的代码示例,我们使用Python的networkx库来模拟图的构建和操作,因为它轻量且易于理解。

5. 代码示例:使用networkx构建记忆图

import networkx as nx
import matplotlib.pyplot as plt
import uuid
import json # 用于序列化属性

class MemoryGraph:
    def __init__(self):
        self.graph = nx.DiGraph() # 使用有向图
        self.entity_map = {} # 存储实体ID到图节点的映射
        self.event_map = {}  # 存储事件ID到图节点的映射
        self.node_counter = 0 # 用于生成唯一的内部节点ID

    def add_entity(self, entity: Entity):
        """
        向记忆图添加一个实体节点。
        如果实体已存在(通过entity.id判断),则更新其属性。
        """
        if entity.id in self.entity_map:
            node_id = self.entity_map[entity.id]
            # 更新节点属性
            self.graph.nodes[node_id].update({
                'entity_type': entity.entity_type,
                'name': entity.name,
                'latest_bbox': entity.bounding_box, # 存储最新的bbox
                'latest_timestamp_seen': entity.timestamp_seen,
                'attributes': entity.attributes # 可以合并或更新
            })
            # print(f"Updated Entity Node: {entity.name} (ID: {entity.id[:8]})")
        else:
            node_id = f"N{self.node_counter:05d}"
            self.node_counter += 1
            self.graph.add_node(node_id, 
                                 type='ENTITY', 
                                 entity_id=entity.id,
                                 entity_type=entity.entity_type,
                                 name=entity.name,
                                 latest_bbox=entity.bounding_box,
                                 latest_timestamp_seen=entity.timestamp_seen,
                                 attributes=entity.attributes)
            self.entity_map[entity.id] = node_id
            # print(f"Added Entity Node: {entity.name} (ID: {entity.id[:8]}) as {node_id}")
        return self.entity_map[entity.id]

    def add_event(self, event: Event):
        """
        向记忆图添加一个事件节点。
        """
        if event.id in self.event_map:
            node_id = self.event_map[event.id]
            # 更新事件属性(如果需要,例如持续时间、置信度)
            self.graph.nodes[node_id].update({
                'event_type': event.event_type,
                'timestamp_start': event.timestamp_start,
                'timestamp_end': event.timestamp_end,
                'confidence': event.confidence,
                'attributes': event.attributes
            })
            # print(f"Updated Event Node: {event.event_type} (ID: {event.id[:8]})")
        else:
            node_id = f"N{self.node_counter:05d}"
            self.node_counter += 1
            self.graph.add_node(node_id, 
                                 type='EVENT', 
                                 event_id=event.id,
                                 event_type=event.event_type,
                                 timestamp_start=event.timestamp_start,
                                 timestamp_end=event.timestamp_end,
                                 confidence=event.confidence,
                                 attributes=event.attributes)
            self.event_map[event.id] = node_id
            # print(f"Added Event Node: {event.event_type} (ID: {event.id[:8]}) as {node_id}")
        return self.event_map[event.id]

    def add_relationship(self, source_entity_id: str, target_entity_id: str, 
                         relation_type: str, timestamp: float, attributes: dict = None):
        """
        在两个实体/事件节点之间添加关系。
        Args:
            source_entity_id (str): 源节点的内部ID或外部实体/事件ID。
            target_entity_id (str): 目标节点的内部ID或外部实体/事件ID。
            relation_type (str): 关系类型,如 'PERFORMS', 'LOCATED_AT'。
            timestamp (float): 关系发生的时间戳。
            attributes (dict): 关系附加属性。
        """
        source_node_id = None
        target_node_id = None

        # 尝试通过外部ID查找内部ID
        if source_entity_id in self.entity_map:
            source_node_id = self.entity_map[source_entity_id]
        elif source_entity_id in self.event_map:
            source_node_id = self.event_map[source_entity_id]
        else: # 假设source_entity_id已经是内部node_id
            source_node_id = source_entity_id 

        if target_entity_id in self.entity_map:
            target_node_id = self.entity_map[target_entity_id]
        elif target_entity_id in self.event_map:
            target_node_id = self.event_map[target_entity_id]
        else: # 假设target_entity_id已经是内部node_id
            target_node_id = target_entity_id

        if source_node_id not in self.graph.nodes:
            print(f"Warning: Source node {source_entity_id} not found in graph.")
            return
        if target_node_id not in self.graph.nodes:
            print(f"Warning: Target node {target_entity_id} not found in graph.")
            return

        edge_attrs = {'relation_type': relation_type, 'timestamp': timestamp}
        if attributes:
            edge_attrs.update(attributes)

        # 检查是否已存在相同类型的边,如果存在则更新
        if self.graph.has_edge(source_node_id, target_node_id):
            existing_edge_attrs = self.graph.get_edge_data(source_node_id, target_node_id)
            if existing_edge_attrs.get('relation_type') == relation_type:
                # 简单更新时间戳和属性
                self.graph[source_node_id][target_node_id].update(edge_attrs)
                # print(f"Updated Relationship: {source_node_id} -[{relation_type}]-> {target_node_id}")
                return

        self.graph.add_edge(source_node_id, target_node_id, **edge_attrs)
        # print(f"Added Relationship: {source_node_id} -[{relation_type}]-> {target_node_id}")

    def visualize(self, filename="memory_graph.png"):
        """
        可视化记忆图。
        """
        pos = nx.spring_layout(self.graph, k=0.3, iterations=50) # 布局算法

        node_colors = []
        node_labels = {}
        for node_id, data in self.graph.nodes(data=True):
            if data['type'] == 'ENTITY':
                node_colors.append('skyblue')
                node_labels[node_id] = f"{data.get('name', data['entity_type'])} ({data['entity_id'][:4]})"
            elif data['type'] == 'EVENT':
                node_colors.append('lightcoral')
                node_labels[node_id] = f"{data['event_type']} ({data['event_id'][:4]})"
            else:
                node_colors.append('lightgrey')
                node_labels[node_id] = node_id

        edge_labels = {
            (u, v): d['relation_type'] 
            for u, v, d in self.graph.edges(data=True)
        }

        plt.figure(figsize=(15, 12))
        nx.draw_networkx_nodes(self.graph, pos, node_color=node_colors, node_size=3000)
        nx.draw_networkx_edges(self.graph, pos, edge_color='gray', arrowsize=20)
        nx.draw_networkx_labels(self.graph, pos, labels=node_labels, font_size=10, font_weight='bold')
        nx.draw_networkx_edge_labels(self.graph, pos, edge_labels=edge_labels, font_color='darkgreen', font_size=9)
        plt.title("Long-Term Memory Graph")
        plt.axis('off')
        plt.savefig(filename)
        plt.close()
        print(f"Memory graph visualized and saved to {filename}")

    def get_node_by_external_id(self, external_id: str):
        """根据外部ID(entity_id或event_id)获取内部节点ID"""
        if external_id in self.entity_map:
            return self.entity_map[external_id]
        if external_id in self.event_map:
            return self.event_map[external_id]
        return None

# 整合之前的部分,构建完整的记忆图
if __name__ == "__main__":
    event_detector = EventDetector()
    memory_graph = MemoryGraph()

    simulated_timestamps = np.arange(0, 10000, 500) # 每500ms一帧

    # 存储所有唯一的实体对象,以便在事件发生时引用它们
    all_unique_entities_obj = {} 

    for ts in simulated_timestamps:
        dummy_frame_features = np.random.rand(2048) 

        current_frame_entities, current_frame_events = event_detector.detect_and_extract(dummy_frame_features, ts)

        # 1. 将当前帧检测到的实体添加到图或更新其信息
        for entity_obj in current_frame_entities:
            # 确保使用唯一的实体ID作为key
            memory_graph.add_entity(entity_obj)
            all_unique_entities_obj[entity_obj.id] = entity_obj # 更新或添加实体对象

        # 2. 将当前帧检测到的事件添加到图
        for event_obj in current_frame_events:
            event_node_id = memory_graph.add_event(event_obj)

            # 3. 添加事件与相关实体之间的关系
            if event_obj.subject_id:
                subject_node_id = memory_graph.get_node_by_external_id(event_obj.subject_id)
                if subject_node_id:
                    memory_graph.add_relationship(subject_node_id, event_node_id, 'PERFORMS', ts, {'confidence': event_obj.confidence})

            if event_obj.object_id:
                object_node_id = memory_graph.get_node_by_external_id(event_obj.object_id)
                if object_node_id:
                    memory_graph.add_relationship(event_node_id, object_node_id, 'AFFECTS', ts, {'confidence': event_obj.confidence})
                    # 也可以添加实体之间的交互关系
                    if event_obj.subject_id:
                        subject_node_id = memory_graph.get_node_by_external_id(event_obj.subject_id)
                        if subject_node_id:
                             memory_graph.add_relationship(subject_node_id, object_node_id, 'INTERACTS_WITH', ts, {'event_type': event_obj.event_type})

        # 4. 添加实体间的时序和空间关系(简化,在实际中需要更复杂的逻辑)
        # 例如,如果多个实体同时出现在一帧,可以添加CO_OCCURS_WITH关系
        for i in range(len(current_frame_entities)):
            for j in range(i + 1, len(current_frame_entities)):
                ent1 = current_frame_entities[i]
                ent2 = current_frame_entities[j]
                # 假设如果bbox有重叠,则认为它们在同一区域
                # (更精确的判断需要计算IoU)
                if ent1.bounding_box and ent2.bounding_box:
                    # 简单判断中心点距离
                    center1 = np.array([(ent1.bounding_box[0] + ent1.bounding_box[2])/2, (ent1.bounding_box[1] + ent1.bounding_box[3])/2])
                    center2 = np.array([(ent2.bounding_box[0] + ent2.bounding_box[2])/2, (ent2.bounding_box[1] + ent2.bounding_box[3])/2])
                    distance = np.linalg.norm(center1 - center2)
                    if distance < 200: # 假设距离小于200像素则认为接近
                         memory_graph.add_relationship(
                            memory_graph.get_node_by_external_id(ent1.id), 
                            memory_graph.get_node_by_external_id(ent2.id), 
                            'CO_LOCATED_WITH', ts, {'distance': distance}
                         )

    print(f"nTotal Nodes in Graph: {memory_graph.graph.number_of_nodes()}")
    print(f"Total Edges in Graph: {memory_graph.graph.number_of_edges()}")

    # 可视化记忆图
    memory_graph.visualize()

这个代码示例展示了如何将检测到的实体和事件添加到networkx图中,并建立它们之间的关系。在实际的图数据库中,节点和边的创建以及属性的存储会更加高效和健壮。

六、 记忆管理与整合

记忆图并非静态的,它需要动态更新以反映视频流的实时变化,同时也要防止无限增长和信息冗余。

1. 记忆的动态更新

  • 新信息添加: 当检测到新的实体、事件或关系时,直接添加到图中。
  • 信息更新: 如果检测到已存在实体的状态变化(如位置、属性),则更新对应节点的属性。例如,一个人的latest_bboxlatest_timestamp_seen会随着视频的进行而更新。

2. 记忆的整合与合并 (Consolidation)
这是防止图爆炸的关键。很多时候,连续的帧会检测到高度相似的事件或实体。

  • 实体合并: 例如,通过物体跟踪算法,我们可以识别出在不同帧中出现的同一个“人”,并将其合并为图中的一个Person节点。如果检测到“Person A 走了两步”,然后又检测到“Person A 走了三步”,我们可以将这些微观的“行走”事件合并为一个更长的“Person A 持续行走”的宏观事件。
  • 事件合并:
    • 时序合并: 将在短时间内连续发生的同类型事件合并。例如,连续10帧都检测到“Person A Standing”,可以合并为一个长时段的“Person A is Standing from T1 to T2”。
    • 语义合并: 识别语义上等价的事件,即使它们的表述略有不同。例如,“PickingUp Cup”和“Grabbing Cup”可能被合并。
  • 层次化抽象: 将一系列微观事件抽象为宏观事件。例如,“打开冰箱”、“拿出牛奶”、“关上冰箱”可以抽象为“准备早餐”。

3. 记忆的衰减与遗忘 (Decay & Forgetting)
为了保持记忆图的效率和相关性,不重要的或过时的信息需要被淡化或移除。

  • 时间衰减: 离当前时间越远的事件,其“重要性”或“活跃度”分数越低。当分数低于某个阈值时,可以考虑将其存档或移除。
  • 重要性评估: 基于事件的置信度、与其他事件的连接程度、被查询的频率等,为其分配重要性分数。高重要性事件被保留更久。
  • 上下文相关性: 在特定任务中,只有与当前任务相关的记忆才被激活和保留。

4. 代码示例:简化版事件合并
这里我们演示一个简单的时序事件合并策略:将短时间内连续发生的相同事件进行合并。

def consolidate_events(events_list: list[Event], time_threshold: float = 500.0) -> list[Event]:
    """
    合并列表中的连续相同类型事件。
    Args:
        events_list (list[Event]): 待合并的事件列表。
        time_threshold (float): 时间阈值(毫秒),如果两个相同类型事件的间隔小于此阈值,则合并。
    Returns:
        list[Event]: 合并后的事件列表。
    """
    if not events_list:
        return []

    # 按开始时间排序
    events_list.sort(key=lambda x: x.timestamp_start)

    consolidated = []
    current_event = None

    for event in events_list:
        if current_event is None:
            current_event = Event(event.event_type, event.timestamp_start, event.timestamp_end,
                                  event.subject_id, event.object_id, event.location, 
                                  event.confidence, event.attributes, event_id=str(uuid.uuid4()))
        else:
            # 如果事件类型相同,且主体和客体也相同(如果存在),且时间间隔在阈值内
            # 并且属性相似(这里简化为不比较属性或只比较关键属性)
            if (event.event_type == current_event.event_type and
                event.subject_id == current_event.subject_id and
                event.object_id == current_event.object_id and
                event.timestamp_start - current_event.timestamp_end <= time_threshold):

                # 合并:更新结束时间,并可以更新置信度(例如取平均或最大值)
                current_event.timestamp_end = event.timestamp_end
                current_event.confidence = max(current_event.confidence, event.confidence) # 取最大置信度
            else:
                # 否则,当前事件结束,开始一个新的事件
                consolidated.append(current_event)
                current_event = Event(event.event_type, event.timestamp_start, event.timestamp_end,
                                      event.subject_id, event.object_id, event.location, 
                                      event.confidence, event.attributes, event_id=str(uuid.uuid4()))

    if current_event is not None:
        consolidated.append(current_event)

    return consolidated

# 示例使用
if __name__ == "__main__":
    # 模拟一些事件
    e1 = Event('Walking', 100, 200, subject_id='person_A', confidence=0.9)
    e2 = Event('Walking', 250, 350, subject_id='person_A', confidence=0.8)
    e3 = Event('Standing', 400, 500, subject_id='person_A', confidence=0.95)
    e4 = Event('Walking', 600, 700, subject_id='person_B', confidence=0.7)
    e5 = Event('Walking', 750, 850, subject_id='person_B', confidence=0.75)
    e6 = Event('Walking', 880, 980, subject_id='person_B', confidence=0.78) # 间隔小
    e7 = Event('Walking', 1500, 1600, subject_id='person_A', confidence=0.85) # 间隔大

    raw_events = [e1, e2, e3, e4, e5, e6, e7]
    print("--- Raw Events ---")
    for e in raw_events:
        print(e)

    consolidated_events = consolidate_events(raw_events, time_threshold=200)
    print("n--- Consolidated Events (Threshold 200ms) ---")
    for e in consolidated_events:
        print(e)

    # 结果分析:
    # e1, e2 会合并成一个更长的 Walking 事件
    # e3 是 Standing,不会和 Walking 合并
    # e4, e5, e6 会合并成一个更长的 Walking 事件
    # e7 会是独立的 Walking 事件

七、 记忆检索与推理

构建记忆图的最终目的是为了能够利用它。一旦记忆图建立起来,我们就可以进行复杂的查询和推理。

1. 记忆检索

  • 基于模式匹配: 查找图中符合特定模式的子图。例如,“找出所有Person A执行的动作”。
  • 基于关键词或自然语言: 将用户输入的文本查询转换为图查询语言(如Cypher),然后执行查询。例如,用户问“Person A 在做什么?”,系统可以查询与“Person A”相关的PERFORMS关系。
  • 时序查询: “在Person A拿起咖啡杯之后发生了什么?”
  • 空间查询: “在客厅里有哪些物体?”

2. 记忆推理

  • 因果推理: 发现事件之间的因果链条。“灯泡坏了”可能是由于“Person B不小心打翻了椅子”。
  • 预测: 基于已发生的事件序列,预测未来可能发生的事件。“如果Person A走向门口,那么他可能会离开房间。”
  • 异常检测: 识别与记忆中常见模式不符的事件。
  • 问答系统: 回答更复杂的开放式问题。

在实际应用中,记忆检索和推理模块会与自然语言处理(NLP)技术结合,将用户的自然语言问题转化为图查询语句,并将图查询结果转化为自然语言回答。

八、 系统架构概览

将上述模块整合起来,一个基于视频的长时记忆增强系统大致如下:

+----------------+       +-------------------------+       +---------------------+       +-------------------+       +--------------------+
|  Video Stream  | ----> |  Feature Extraction     | ----> |  Event Detection    | ----> |  Memory Management| ----> |  Long-Term Memory  |
| (Raw Pixels)   |       | (CNNs, Transformers,    |       | (Object Detection,  |       | (Consolidation,   |       |  Graph (Neo4j/    |
|                |       |  Object/Action Features)|       |  Action Recognition,|       |  Decay, Update)   |       |  networkx)         |
+----------------+       +-------------------------+       |  Video Scene Graph) |       +---------^---------+       +---------^----------+
                                                           +----------->----------+                 |                           |
                                                                                                    |                           |
                                                                                                    |                           |
                                                                                                    |                           |
                                                                                                    +---------------------------+
                                                                                                    |  Memory Retrieval &       |
                                                                                                    |  Reasoning (NLP, Graph    |
                                                                                                    |  Queries, Inference)      |
                                                                                                    +---------------------------+
                                                                                                                ^
                                                                                                                |
                                                                                                    +---------------------------+
                                                                                                    |    User Query (Text/Voice)|
                                                                                                    +---------------------------+

视频流经过特征提取模块,将原始帧转换为语义丰富的特征向量。这些特征被送入事件检测模块,识别出视频中的实体、事件及其属性。这些结构化信息随后被传递给记忆管理模块,进行整合、更新和潜在的合并操作,最终存储在长时记忆图结构中。当用户提出查询时,记忆检索与推理模块会与记忆图交互,提取相关信息并进行逻辑推理,最终将结果返回给用户。

九、 挑战与未来展望

构建一个鲁棒、高效的视频长时记忆系统仍面临诸多挑战:

  1. 大规模与实时性: 处理TB级别的视频数据并实时更新记忆图,对计算资源和算法效率是巨大考验。
  2. 语义理解深度: 从像素到高级语义的转化仍然存在鸿沟。如何捕捉更抽象的意图、情感和复杂交互,是未来的研究方向。
  3. 图的动态演化与维护: 随着时间的推移,记忆图会变得庞大而复杂。如何高效地进行更新、合并、删除、摘要,并保持其一致性和准确性,是一个持续的挑战。
  4. 不确定性与模糊性: 视觉识别结果往往带有不确定性。如何将这些不确定性融入记忆图的表示和推理中,是提高系统鲁棒性的关键。
  5. 多模态融合: 除了视频,结合音频、文本(如字幕、语音识别)等信息可以极大地丰富记忆内容,但多模态信息的有效融合仍需深入研究。
  6. 可解释性与透明度: AI系统如何解释其记忆和推理过程,增强用户的信任和理解,是伦理和技术层面都需要关注的问题。

尽管挑战重重,视频长时记忆增强技术无疑是构建真正智能AI的关键一步。它不仅能让机器更好地理解复杂的动态世界,更将赋能广泛的应用,从智能监控、机器人导航、人机交互到个性化内容推荐,都将因此迈向新的高度。

总结

我们探讨了如何构建一个能够理解并记住视频中近期发生事件的长时记忆图结构。从视频特征提取、事件检测,到核心的记忆图本体设计与实现,再到记忆的动态管理与整合,最后展望了其检索与推理能力以及面临的挑战。通过将视频内容转化为结构化的知识图谱,我们为AI系统提供了一种强大的、可查询的长期记忆能力,这对于实现更深层次的视频理解和智能行为至关重要。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注