- 计算机视觉
- 深度学习
【免费下载链接】ccv
C-based/Cached/Core Computer Vision Library, A Modern Computer Vision Library
导读
CCV(C-based/Cached/Core Computer Vision Library)的 NNC(Neural Network Collection)在ccv_cnnp_dataframe_*系列 API 中提供了一套轻量级 dataframe 抽象,用于管理喂入 NNC 计算图的训练数据:它支持按行列组织数据、随机打乱、迭代与预取、以 map 函数派生新列、以 reduce/sample 函数合并多行为单行,并通过 stream context(流上下文)实现"数据准备与图计算异步重叠"。本文以仓库文档 doc/nnc-df.rst 为主线,结合 lib/nnc/ccv_cnnp_dataframe.c、lib/nnc/ccv_cnnp_dataframe_core.c、lib/nnc/ccv_cnnp_dataframe_addons.c 等源码实现与 test/int/nnc/cifar.tests.c 的实战用例,帮你掌握从原始 dataframe 到 addons 高层 API 的完整数据管道搭建能力。
为什么 NNC 需要 Dataframe 抽象
NNC 的训练通常需要把"原始数据"(图片路径、标签、类别)转换成"可直接喂入计算图的张量"。如果每个训练脚本都自行实现数据过滤、对齐(alignment)、批处理(batching),代码会迅速失控,也难以复用。文档指出,一个 dataframe 抽象对任何机器学习框架都更合适,因为它统一处理了这三类问题:
- 数据过滤:只挑选需要的行/列进入训练流程;
- 对齐:把不同来源的数据(如文件名与标签)按行对齐到同一张表;
- 批处理:把多行记录合并成一批张量,供图计算一次消费。
在 CCV 中,这个职责由ccv_cnnp_dataframe_t(定义见 lib/nnc/ccv_nnc.h)承担,它是 NNC 五级 API 中的 Level-5 Dataframe API。
核心模型:行、列与三种数据操作
加载进 dataframe 的数据以行和列组织。基础能力(对应文档 "Operations on the Data" 一节)如下:
- 迭代:可以用迭代器遍历选定的若干列;
- 打乱:
ccv_cnnp_dataframe_shuffle随机重排行顺序; - 派生新列:依据你指定的 map 函数,从若干已有列推导出一个新列;
- 合并多行:依据你指定的 reduce 函数,把多行归约成一行。
从源码结构看(struct ccv_cnnp_dataframe_s,见 lib/nnc/ccv_cnnp_dataframe.c),一个 dataframe 内部保存了:
row_count/column_size:行数与列数;shuffled_idx:打乱后的行索引表(首次调用 shuffle 时惰性创建);data_ctx:以 stream context 为键的哈希表,用于缓存各列已取出的数据实体,避免重复分配;- 原始列
column_data[]与派生列derived_column_data(记录 map 函数、依赖列、stream_type、析构函数等)。
迭代器与流上下文:数据加载与计算异步重叠
文档 "Iteration" 一节介绍了迭代器的完整用法,这也是 dataframe 与普通数组遍历的本质区别:
ccv_cnnp_dataframe_iter_new(dataframe, column_idxs, column_idx_size)创建迭代器,可指定要遍历的列子集;ccv_cnnp_dataframe_iter_set_cursor(iter, idx)控制访问位置,设为 0 即回到开头;重置游标会使已预取(prefetch)的记录失效——从源码看,ccv_cnnp_dataframe_iter_set_cursor会调用_ccv_cnnp_null_prefetches清空预取队列(见 lib/nnc/ccv_cnnp_dataframe.c),因为游标跳变后旧的预取内容已无意义;ccv_cnnp_dataframe_iter_prefetch(iter, prefetch_count, stream_context)预先取出若干条记录放入缓存,之后ccv_cnnp_dataframe_iter_next直接从缓存返回;ccv_cnnp_dataframe_iter_next(iter, data_ref, column_idx_size, stream_context)返回下一个记录,语义为:0成功、-1无更多行、-2迭代已结束(见 lib/nnc/ccv_nnc.h 与iter_next实现 lib/nnc/ccv_cnnp_dataframe.c)。
双流上下文乒乓预取模式
文档重点描述了一个典型用例:数据准备与图计算使用两个不同的流上下文。如果提供了 stream context,预取和迭代都会在该流上异步执行:
- 在流 A 上取到一条记录,交给图在流 A 上计算;
- 同时在流 B 上立即预取下一条记录;
- 图计算完成后,切到流 B 取回记录,开始下一轮。
这样数据准备(读盘、解码、增广、拷入 GPU)与计算天然重叠,避免了单独开 IO 线程。这正是 test/int/nnc/cifar.tests.c 中训练循环的做法:
ccv_nnc_stream_context_t* stream_contexts[2]; stream_contexts[0] = ccv_nnc_stream_context_new(CCV_STREAM_CONTEXT_GPU); stream_contexts[1] = ccv_nnc_stream_context_new(CCV_STREAM_CONTEXT_GPU); int p = 0, q = 1; ccv_cnnp_dataframe_iter_prefetch(iter, 1, stream_contexts[p]); // 训练循环 ccv_cnnp_dataframe_iter_next(iter, (void**)input_fits, device_count * 2, stream_contexts[p]); ccv_nnc_stream_context_wait(stream_contexts[q]); ccv_cnnp_model_fit(cifar_10, /*...*/, stream_contexts[p]); ccv_cnnp_dataframe_iter_prefetch(iter, 1, stream_contexts[q]); CCV_SWAP(p, q, t);Map:Pull 语义的列派生
文档 "Map" 一节指出,数据准备管道可能很长,因此 dataframe 优先采用pull模型:当某一列被请求(迭代/预取触发)时,它才"拉取"依赖列的数据并执行变换,而不是提前全部算好。map 函数负责具体变换,典型应用包括:
- 把数据从 CPU 搬运到 GPU 设备;
- 对图片做数据增广;
- 把文件名解码成图像矩阵。
核心签名是ccv_cnnp_dataframe_map(dataframe, map, stream_type, data_deinit, column_idxs, column_idx_size, context, context_deinit, name)(见 lib/nnc/ccv_nnc.h),其中:
| 参数 | 含义 |
|---|---|
map | 派生函数,入参是多个依赖列的数据、批量大小,出参是新列数据 |
stream_type | 该派生列兼容的流类型,每列只兼容一种流类型(0表示 CPU/默认) |
data_deinit | 派生数据的析构函数 |
column_idxs | 参与派生的依赖列索引数组 |
context/context_deinit | 随列携带的自定义上下文及其析构函数 |
map 的结果是在现有 dataframe 内新增一列,并返回新列的索引。源码中的递归拉取逻辑见_ccv_cnnp_dataframe_column_data(lib/nnc/ccv_cnnp_dataframe.c):它会先递归取得所有依赖列的数据,再调用map,并在派生列流类型与当前流不一致时通过ccv_nnc_stream_context_emit_signal_new/ccv_nnc_stream_context_wait_signal做流间同步。
基于 map 的常用内置列操作
ccv_cnnp_dataframe_core.c提供了两个基于 map 的通用工具:
ccv_cnnp_dataframe_extract_value:从结构体列中按offsetof提取出某个字段成新列(lib/nnc/ccv_cnnp_dataframe_core.c);ccv_cnnp_dataframe_make_tuple:把若干列打包成一个void*数组形式的元组列,供下游一次取多个值(lib/nnc/ccv_cnnp_dataframe_core.c)。
Reduce / Sample:多行归约为一行
文档 "Reduce" 一节说明:reduce 函数把多行合并成一行,典型用途是把多条记录 batch 成一个张量喂给计算图。与 map 不同,reduce 会产生新的 dataframe(map 只是在现有 dataframe 中加列)。
在源码中,对应实现是ccv_cnnp_dataframe_sample_new(文档中的reduce_new即此概念,当前仓库以 sample 命名,见 lib/nnc/ccv_cnnp_dataframe_core.c)。其内部机制很有意思:
- 创建一个内部迭代器
ccv_cnnp_dataframe_iter_new遍历原 dataframe 的指定列; - 按
batch_size逐段ccv_cnnp_dataframe_iter_prefetch+ccv_cnnp_dataframe_iter_next拉取batch_size行; - 交给
ccv_cnnp_column_data_sample_f采样函数归约为一行; - 新 dataframe 的行数为
(row_count + batch_size - 1) / batch_size。
因为归约后行数发生了变化,所以必须新建 dataframe。同样地,reduce 也是 pull 语义——数据只有被请求时才真正拉取。
为什么应该使用 Addons API 而非原始 API
文档 "Use Dataframe with Addons" 一节明确建议:原始 dataframe API 难用,应当与 addons API 交互。
原始 API(ccv_cnnp_dataframe_new、ccv_cnnp_dataframe_map、ccv_cnnp_dataframe_reduce_new)要求你自己填写ccv_cnnp_column_data_t结构体(见 lib/nnc/ccv_nnc.h),该结构包含stream_type、name、data_enum数据枚举函数、data_deinit析构函数、context与context_deinit六个字段。与ccv_nnc_easy.h里CPU_TENSOR_NCHW这类辅助函数不同,仓库有意不提供填充该结构的辅助函数——因为 addons API 已经用原始 API 实现好了这些细节。
Addons 的能力矩阵
addons API 全部实现在 lib/nnc/ccv_cnnp_dataframe_addons.c,以原始 API 为基础封装:
| API | 功能 |
|---|---|
ccv_cnnp_dataframe_from_array_new | 把一个ccv_array_t包装成 dataframe(未来/现状还支持 CSV 等来源) |
ccv_cnnp_dataframe_read_image | 把存放文件名的列解码为图像(CCV_IO_ANY_FILE \| CCV_IO_RGB_COLOR),支持structof从结构体字段取文件名 |
ccv_cnnp_dataframe_image_random_jitter | 对图像列做随机增广(对比度、饱和度、亮度、PCA 光照抖动、裁剪、翻转、归一化等) |
ccv_cnnp_dataframe_one_hot | 从标签字段生成 one-hot 张量列(支持CCV_32F/CCV_16F) |
ccv_cnnp_dataframe_copy_to_gpu | 把张量列拷到指定 GPU 设备 |
ccv_cnnp_dataframe_add_aux | 为每行附带一个按参数生成的辅助张量(如输出占位) |
ccv_cnnp_dataframe_combine_new | 把多个列各自 batch 成张量,返回新 dataframe(即文档中的batching_new概念,见 lib/nnc/ccv_nnc.h) |
ccv_cnnp_dataframe_from_csv_new | 从 CSV 文件/内存读取数据建 dataframe(见 lib/nnc/ccv_cnnp_dataframe_csv.c) |
文档中"未来从 CSV、SQLite 读取"的能力,在仓库里 CSV 已落地:ccv_cnnp_dataframe_from_csv_new按 RFC 4180 解析,支持自定义分隔符与引号、CRLF/LF/LFCR终止符以及 header 行,采用两遍扫描(先统计引号与奇偶换行,再复制出以\0结尾的连续页并记录各列起始指针),可参考 lib/nnc/ccv_nnc.h 的设计说明。
random jitter 参数详解
ccv_cnnp_random_jitter_t(lib/nnc/ccv_nnc.h)是数据增广的核心配置,字段语义如下:
| 字段 | 取值语义 |
|---|---|
contrast/saturation/brightness | 随机扰动幅度,最终值落在[1/(1+v), 1+v]区间 |
lighting | AlexNet 风格、基于 PCA 的光照抖动 |
aspect_ratio | 长宽比拉伸幅度[1/(1+v), 1+v] |
symmetric | 是否做水平(x 轴)随机翻转 |
seed | 随机种子;为 0 时用ccv_nnc_stream_context_genrand_uint32(0)播种 |
center_crop | 启用中心裁剪(否则随机裁剪) |
resize.min/max/roundup | 缩放目标在min + (max-min)*random_unit之间,且宽高为roundup的倍数 |
size.rows/cols | 最终图像尺寸 |
offset.x/y | x/y 轴额外随机偏移 |
normalize.mean[3]/std[3] | 归一化:pixel = (pixel - mean) / std(源码会把std预转为倒数,见 lib/nnc/ccv_cnnp_dataframe_addons.c) |
注意ccv_cnnp_dataframe_image_random_jitter目前只支持CCV_32F输出(源码中有assert(datatype == CCV_32F))。
端到端实战:CIFAR-10 训练数据管道
test/int/nnc/cifar.tests.c 给出了一个完整的 addons 组合范例,一行一行对应文档描述的能力:
// 1. 把 ccv_array_t(训练集)包成 dataframe ccv_cnnp_dataframe_t* const raw_train_data = ccv_cnnp_dataframe_from_array_new(training_set); // 2. 配置随机增广:resize 到 32x32、水平翻转、±4 随机裁剪偏移、均值归一化 const ccv_cnnp_random_jitter_t random_jitter = { .resize = { .min = 32, .max = 32 }, .size = { .rows = 32, .cols = 32 }, .symmetric = 1, .normalize = { .mean = { mean[0], mean[1], mean[2] } }, .offset = { .x = 4, .y = 4 }, .seed = 1, }; // 3. 从结构体字段抽取图像矩阵列、做随机增广、生成 one-hot 标签列 const int images = ccv_cnnp_dataframe_extract_value(raw_train_data, 0, offsetof(ccv_categorized_t, matrix), 0); const int jitter_images = ccv_cnnp_dataframe_image_random_jitter(raw_train_data, images, CCV_32F, random_jitter, 0); const int one_hot = ccv_cnnp_dataframe_one_hot(raw_train_data, 0, offsetof(ccv_categorized_t, c), 10, 1, 0, CCV_32F, CCV_TENSOR_FORMAT_NCHW, 0); // 4. 把增广图 + one-hot 标签按 batch_size 合批,生成 batch dataframe ccv_cnnp_dataframe_t* const batch_train_data = ccv_cnnp_dataframe_combine_new( raw_train_data, COLUMN_ID_LIST(jitter_images, one_hot), batch_size, device_count, CCV_TENSOR_FORMAT_NCHW); // 5. 拷到 GPU、附上输出占位张量列,创建迭代器 train_device_columns[i] = ccv_cnnp_dataframe_copy_to_gpu(batch_train_data, 0, i * 2, 2, i, 0); train_device_columns[device_count + i] = ccv_cnnp_dataframe_add_aux(batch_train_data, params, 0); ccv_cnnp_dataframe_iter_t* const iter = ccv_cnnp_dataframe_iter_new(batch_train_data, train_device_columns, device_count * 2);每个 epoch 结束时还会ccv_cnnp_dataframe_shuffle(raw_train_data)并ccv_cnnp_dataframe_iter_set_cursor(iter, 0)复位迭代器(test/int/nnc/cifar.tests.c)——这正好对应文档"数据可被打乱"以及"重置游标会使预取失效"的说明。
已知限制与设计取舍
文档 "Others" 一节诚实地列出了当前实现的边界(这一点在源码中同样可以印证):
- 不支持排序;
- 不支持更通用的过滤(可用 reduce 做有限度的过滤,但能力受限);
- 不支持多 dataframe 连接(join)。
作者的解释是:在保证实现不成为瓶颈的前提下,这些功能的高效实现方式尚不清晰。反过来,正因为实现"够 raw",上述基础能力(行/列迭代、打乱、map、reduce)在性能上是足够高效的。
结语:一种表达与执行分离的数据抽象
从架构上看,dataframe + addons 最关键的收益是文档结尾强调的:计算表达与真实执行解耦。你可以在构建阶段自由地组合from_array_new → read_image → image_random_jitter → one_hot → combine_new → copy_to_gpu这条管道,而真实的执行(读盘、解码、增广、拷贝到 GPU)只在iter_prefetch/iter_next触碰数据时才发生;再叠加双流上下文的乒乓预取,NNC 得以在"单线程表达、异步执行"的前提下,实现数据加载与训练的深度重叠。这套抽象正是 CIFAR 训练等高层用例的底座,值得在搭建你自己的训练数据管道时直接复用。
相关文件:核心实现 lib/nnc/ccv_cnnp_dataframe.c、归约与工具列 lib/nnc/ccv_cnnp_dataframe_core.c、addons 层 lib/nnc/ccv_cnnp_dataframe_addons.c、CSV 支持 lib/nnc/ccv_cnnp_dataframe_csv.c、完整 API 文档 lib/nnc/ccv_nnc.h,以及端到端示例 test/int/nnc/cifar.tests.c。
- 计算机视觉
- 深度学习
【免费下载链接】ccv
C-based/Cached/Core Computer Vision Library, A Modern Computer Vision Library
相关推荐
深入理解liuliu/ccv项目中的NNC Dataframe数据处理框架
深入理解liuliu/ccv项目中的NNC Dataframe数据处理框架 痛点:深度学习数据处理的效率瓶颈 在深度学习项目开发中,数据处理往往是性能瓶颈的关键
计算机视觉深度学习如何解锁联想拯救者BIOS隐藏选项:LEGION Y7000系列Insyde高级设置工具的完整解决方案
如何解锁联想拯救者BIOS隐藏选项:LEGION Y7000系列Insyde高级设置工具的完整解决方案 对于联想拯救者Y7000系列用户来说,BIOS高级选项被
固件atomic_write_json 指数退避重试:Webnovel Writer 如何优雅处理文件占用
atomic_write_json 指数退避重试:Webnovel Writer 如何优雅处理文件占用 Webnovel Writer 是一个基于 Claude
机器学习深度学习AutoML大数据后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考