# ActivityNet数据集实战:如何快速提取200+行为分类标签(附Python代码)
刚接触行为识别这个领域时,面对ActivityNet这样的大型数据集,很多开发者都会感到无从下手。官方提供的JSON文件动辄几十MB,结构嵌套复杂,一眼望去全是密密麻麻的键值对。对于只想快速了解数据集包含哪些行为类别,或者需要提取一个清晰标签列表用于模型训练的研究者和工程师来说,直接阅读原始数据几乎是不可能的任务。这篇文章就是为你准备的。我们不谈空洞的理论,直接从实战出发,手把手教你用Python解析ActivityNet的标注文件,精准、高效地提取出那271个行为分类标签,并在这个过程中避开几个常见的“坑”。无论你是计算机视觉的入门新手,还是需要快速验证数据可行性的项目工程师,这套方法都能让你在十分钟内,从一团乱麻的数据中理出头绪。
## 1. 理解ActivityNet数据集的结构与挑战
ActivityNet作为行为识别领域的基准数据集之一,其规模和复杂性既是其价值所在,也是初学者面临的第一道门槛。它包含了超过600小时、涵盖200多种日常人类行为的视频片段。官方通过一个庞大的JSON文件(通常是 `activity_net.v1-3.min.json`)来组织所有的元数据,包括视频信息、时间片段标注,以及最重要的——行为分类的层级结构。
这个JSON文件并非一个简单的标签列表。它是一个**树形结构**的嵌套字典,描述了行为类别之间的层级关系。例如,“Playing soccer”(踢足球)是“Participating in Sports, Exercise, or Recreation”(参与体育、锻炼或娱乐活动)的子类,而后者又是“Sports, Exercise, and Recreation”(体育、锻炼和娱乐)的子类,最终追溯到根节点“Root”。这种结构对于构建细粒度的分类模型很有帮助,但当我们只需要一个扁平的、唯一的叶子节点标签列表时,就需要进行提取和去重。
直接打开JSON文件,你会看到类似下面的结构片段(经过简化):
```json
{
"version": "VERSION 1.3",
"taxonomy": [
{
"nodeId": 0,
"nodeName": "Root",
"parentId": null,
"parentName": null
},
{
"nodeId": 1,
"nodeName": "Household Activities",
"parentId": 0,
"parentName": "Root"
},
{
"nodeId": 17,
"nodeName": "Participating in Sports, Exercise, or Recreation",
"parentId": 5,
"parentName": "Sports, Exercise, and Recreation"
},
{
"nodeId": 64,
"nodeName": "Playing sports",
"parentId": 17,
"parentName": "Participating in Sports, Exercise, or Recreation"
},
{
"nodeId": 250,
"nodeName": "Archery",
"parentId": 64,
"parentName": "Playing sports"
}
],
"database": {
"--0edUL8JcA": {
"duration": 45.3,
"subset": "training",
"annotations": [
{
"segment": [3.2, 15.7],
"label": "Archery"
}
]
}
}
}
```
> **注意**:实际文件中的 `taxonomy` 部分包含所有271个节点,而 `database` 部分则记录了每个视频ID对应的片段和标签。我们的目标是从 `taxonomy` 中提取出所有**叶子节点**(即没有子节点的行为类别)。
面临的挑战主要有三个:
1. **数据量大**:文件体积大,直接加载和查看不便。
2. **结构嵌套**:标签以树形结构存储,需要算法识别叶子节点。
3. **信息冗余**:同一个行为可能在树的不同分支以不同名称出现(尽管ActivityNet做了去重,但父类节点名也会混在其中)。
接下来,我们将用Python代码逐一攻克这些挑战。
## 2. 环境准备与数据获取
工欲善其事,必先利其器。我们首先需要一个简洁的Python环境。
### 2.1 安装必要的库
你只需要Python的标准库 `json`,无需安装任何额外包。但为了更好的数据分析和后续处理,我强烈建议安装 `pandas`。在终端或命令提示符中执行:
```bash
pip install pandas
```
`pandas` 能让我们将提取的标签列表轻松转换为结构清晰的DataFrame,方便查看、统计和导出。
### 2.2 下载ActivityNet标注文件
访问ActivityNet官方网站的下载页面,找到标注文件(Annotation File)的链接。通常文件名是 `activity_net.v1-3.min.json`。下载后,将其放在你的项目目录下。为了演示,我们假设文件路径为 `./activity_net.v1-3.min.json`。
如果你只是想快速测试代码,也可以直接使用Python的 `urllib` 从官方URL读取(注意网络连接)。但更稳妥的做法是本地持有文件。
```python
import json
import pandas as pd
# 定义文件路径
json_file_path = './activity_net.v1-3.min.json'
# 加载JSON数据
with open(json_file_path, 'r', encoding='utf-8') as f:
data = json.load(f)
print(f"JSON文件加载成功!")
print(f"数据顶级键: {list(data.keys())}")
```
运行这段代码,你应该能看到输出类似 `数据顶级键: ['version', 'taxonomy', 'database']`,这证明文件已正确加载。
## 3. 核心算法:从树形结构中提取叶子节点标签
这是整个任务最核心的部分。我们需要遍历 `taxonomy` 列表,找出所有没有子节点的行为类别。在树形结构中,一个节点如果没有其他节点的 `parentId` 等于它的 `nodeId`,那么它就是叶子节点。
### 3.1 方法一:基于集合的快速查找法
这种方法逻辑直观,效率较高。我们先收集所有节点的ID,再收集所有作为子节点出现的ID(即所有非空的 `parentId`),两者之差就是叶子节点的ID集合。
```python
def extract_leaf_nodes_optimized(taxonomy_list):
"""
使用集合运算高效提取叶子节点。
参数:
taxonomy_list: 从JSON中加载的taxonomy列表。
返回:
一个列表,包含所有叶子节点的字典信息。
"""
# 将所有节点ID放入一个集合
all_node_ids = {node['nodeId'] for node in taxonomy_list}
# 将所有出现在parentId字段中的ID放入另一个集合(排除None)
child_node_ids = {node['parentId'] for node in taxonomy_list if node['parentId'] is not None}
# 叶子节点ID = 所有节点ID - 子节点ID集合
leaf_node_ids = all_node_ids - child_node_ids
# 根据叶子节点ID,从原始列表中提取完整的节点信息
leaf_nodes = [node for node in taxonomy_list if node['nodeId'] in leaf_node_ids]
return leaf_nodes
# 应用函数
taxonomy = data['taxonomy']
leaf_nodes = extract_leaf_nodes_optimized(taxonomy)
print(f"共找到 {len(leaf_nodes)} 个叶子节点(行为类别)。")
```
### 3.2 方法二:构建父子关系字典的遍历法
另一种更稳健的方法是显式地构建节点的父子关系图,然后进行遍历。这种方法虽然代码稍长,但在处理更复杂的树操作(如查找路径)时更具扩展性。
```python
def extract_leaf_nodes_by_graph(taxonomy_list):
"""
通过构建邻接表(父子关系图)来识别叶子节点。
参数:
taxonomy_list: 从JSON中加载的taxonomy列表。
返回:
一个列表,包含所有叶子节点的字典信息。
"""
# 创建两个字典:节点信息字典 和 父节点到子节点列表的字典
node_info = {node['nodeId']: node for node in taxonomy_list}
parent_to_children = {}
for node in taxonomy_list:
pid = node['parentId']
if pid not in parent_to_children:
parent_to_children[pid] = []
parent_to_children[pid].append(node['nodeId'])
# 叶子节点:在parent_to_children字典中,没有任何节点以其作为父节点
leaf_node_ids = []
for nid in node_info.keys():
# 如果该nodeId不在parent_to_children的键中,或者对应的子节点列表为空
# 注意:根节点(parentId为None)可能被记录,但我们需要排除非叶子根节点
children = parent_to_children.get(nid, [])
if not children:
leaf_node_ids.append(nid)
leaf_nodes = [node_info[nid] for nid in leaf_node_ids]
return leaf_nodes
# 验证两种方法结果是否一致
leaf_nodes_graph = extract_leaf_nodes_by_graph(taxonomy)
print(f"图遍历法找到 {len(leaf_nodes_graph)} 个叶子节点。")
print(f"两种方法结果一致吗?{set([n['nodeId'] for n in leaf_nodes]) == set([n['nodeId'] for n in leaf_nodes_graph])}")
```
通常,两种方法会得到完全相同的结果。你可以选择任意一种。**方法一**更简洁,**方法二**更利于理解树结构和进行后续扩展。
## 4. 结果整理、分析与导出
提取出叶子节点对象后,我们通常需要将其转换为更易读和使用的格式。
### 4.1 整理为清晰的标签列表并去重
首先,我们提取每个叶子节点的 `nodeName`,并确保没有重复(尽管在规范的ActivityNet中应该没有)。
```python
# 从叶子节点列表中提取行为名称
action_labels = [node['nodeName'] for node in leaf_nodes]
# 去重(以防万一)
unique_action_labels = list(set(action_labels))
unique_action_labels.sort() # 按字母顺序排序,方便查阅
print(f"去重后,共有 {len(unique_action_labels)} 个唯一的行为标签。")
print("\n前20个行为标签示例:")
for i, label in enumerate(unique_action_labels[:20]):
print(f"{i+1:3d}. {label}")
```
### 4.2 使用Pandas进行高级分析和导出
`pandas` 能让我们的分析工作变得异常轻松。我们可以创建一个包含ID、标签名、父类名等完整信息的表格。
```python
# 创建叶子节点的DataFrame
df_leaves = pd.DataFrame(leaf_nodes)
# 按节点ID排序
df_leaves = df_leaves.sort_values('nodeId').reset_index(drop=True)
# 查看DataFrame的前几行和基本信息
print("\n叶子节点信息表(前10行):")
print(df_leaves[['nodeId', 'nodeName', 'parentName']].head(10))
print(f"\nDataFrame形状: {df_leaves.shape}") # 应为 (叶子节点数, 4)
# 统计每个顶级父类下的叶子节点数量(了解数据分布)
if 'parentName' in df_leaves.columns:
top_level_stats = df_leaves['parentName'].value_counts().head(10)
print("\n叶子节点数量最多的前10个父类别:")
print(top_level_stats)
```
为了更直观地对比不同大类下的行为数量,我们可以创建一个简单的汇总表:
| 父类别(parentName) | 叶子节点数量 | 示例行为 |
| :--- | :--- | :--- |
| Playing sports | 约40个 | Archery, Dodgeball, Paintball |
| Participating in water sports | 约10个 | Kayaking, Scuba diving, Surfing |
| Food and drink preparation | 约10个 | Baking cookies, Making a sandwich, Peeling potatoes |
| Washing, dressing and grooming oneself | 约15个 | Brushing teeth, Shaving, Washing face |
| Playing musical instruments | 约15个 | Playing piano, Playing guitarra, Playing drums |
> **提示**:这个分布表能帮助你快速了解数据集的侧重点。例如,如果您的应用场景集中在室内日常活动,那么“Household Activities”大类下的子类可能更需要关注。
### 4.3 导出结果
分析完成后,将结果保存到文件,方便后续在模型训练中直接加载使用。
```python
# 1. 将唯一的标签列表保存为文本文件(每行一个标签)
with open('activitynet_leaf_labels.txt', 'w', encoding='utf-8') as f:
for label in unique_action_labels:
f.write(label + '\n')
print("已导出标签列表至 'activitynet_leaf_labels.txt'")
# 2. 将完整的叶子节点信息保存为CSV文件
df_leaves.to_csv('activitynet_leaf_nodes_detail.csv', index=False, encoding='utf-8-sig')
print("已导出详细信息至 'activitynet_leaf_nodes_detail.csv'")
# 3. 也可以保存为JSON格式,保留结构
output_data = {
'leaf_action_labels': unique_action_labels,
'leaf_nodes': leaf_nodes
}
with open('activitynet_extracted.json', 'w', encoding='utf-8') as f:
json.dump(output_data, f, indent=2, ensure_ascii=False)
print("已导出结构化数据至 'activitynet_extracted.json'")
```
## 5. 常见问题与避坑指南
在实际操作中,你可能会遇到一些预料之外的问题。这里我总结了几点,都是我在初次解析时踩过的“坑”。
### 5.1 JSON解码错误
**问题**:使用 `json.load()` 时可能遇到 `JSONDecodeError`,提示格式错误。
**原因**:ActivityNet的原始JSON文件是有效的,但有时从网页复制或下载不完整会导致文件损坏。另一个常见原因是文件中包含了不标准的字符或编码问题。
**解决**:
- 确保文件完整下载。可以检查文件大小,完整版v1-3的min.json文件大约几十MB。
- 指定正确的编码打开文件,如 `encoding='utf-8'`。
- 如果仍有问题,尝试使用 `json.loads()` 并传入 `strict=False` 参数(谨慎使用),或者用文本编辑器检查文件首尾是否完整。
```python
# 更健壮的加载方式
try:
with open(json_file_path, 'r', encoding='utf-8') as f:
data = json.load(f)
except json.JSONDecodeError as e:
print(f"JSON解析错误: {e}")
# 可以尝试读取文件内容,手动检查问题位置
with open(json_file_path, 'r', encoding='utf-8') as f:
content = f.read()
print("检查文件开头100字符:", content[:100])
print("检查文件结尾100字符:", content[-100:])
```
### 5.2 提取的标签数量不对
**问题**:运行代码后,提取的叶子节点数量不是预期的200多个(例如271个)。
**原因**:
1. 可能错误地包含了根节点(Root)或中间节点。确保你的算法正确识别了叶子节点(没有子节点的节点)。
2. `taxonomy` 列表可能包含重复的 `nodeId`(虽然不常见)。
3. 使用的数据集版本不同(如v1.2, v1.3),标签总数会有差异。
**解决**:
- 手动验证几个节点。找一个你认为肯定是叶子节点的ID(如“Archery”, nodeId 250),检查是否有其他节点的 `parentId` 等于250。
- 打印出 `nodeId` 和 `nodeName` 的对应关系,检查是否有明显的非行为类条目,如“Root”、“Household Activities”等。
- 确认你使用的JSON文件版本。本文代码基于ActivityNet v1-3。
### 5.3 内存不足
**问题**:处理极大的JSON文件时(虽然ActivityNet的标注文件不算极大),可能遇到内存错误。
**解决**:对于真正巨大的JSON,可以考虑使用 `ijson` 库进行流式解析。但对于ActivityNet,标准 `json` 库在普通个人电脑上处理绰绰有余。如果遇到问题,可以尝试增加Python可用内存,或检查是否有其他程序占用过多资源。
### 5.4 中文字符或特殊字符显示问题
**问题**:在控制台或导出文件中,某些标签名称显示为乱码。
**原因**:编码不一致。原始JSON文件是UTF-8编码,但你的控制台或文本编辑器可能使用了其他编码(如GBK)。
**解决**:
- 在Python中,始终使用 `encoding='utf-8'` 读写文件。
- 在Windows命令行中,可以尝试执行 `chcp 65001` 将控制台代码页改为UTF-8。
- 导出CSV时,使用 `encoding='utf-8-sig'` 可以添加BOM头,使Excel等软件正确识别UTF-8编码。
最后,分享一个我自己的小技巧:在开始任何基于ActivityNet的项目前,我都会先运行一遍上述提取脚本,生成一个 `label_stats.txt` 的简易报告,里面包含标签总数、按字母排序的列表以及按父类统计的数量。这份报告能让我在五分钟内对数据集有一个全局的、量化的认识,远比直接扎进原始JSON里高效得多。这套流程已经成了我处理类似结构化标注数据的标准起手式。