传感器端计算(in-sensor computing)这两年在我的项目里出现的频率越来越高。之前做低功耗视觉识别时,最折磨人的不是模型选型,而是数据刚出像素阵列就已经把功耗和带宽吃掉大半,后端再强也只能干瞪眼。后来我把一部分卷积和特征提取直接搬进传感器内部,整个系统的功耗、延迟、数据量都被重新洗了一遍牌。这篇文章打算把这轮方案验证的完整过程写下来,包括为什么非算在传感器上不可、算法怎么适配、仿真验证流程怎么搭,以及我在工程里踩过的几个大坑,给正在研究终端感知和边缘AI的朋友做个参考。
1. 传感器端计算的本质:把“第一层智能”塞进像素阵列
1.1 它到底在算什么,又在哪里算
传感器端计算,简单说就是把传统图像传感器“只负责光电转换、然后丢数据”的单一职责,改成“光电转换 + 第一层信息提取”二合一。传统路线上,像素阵列完成曝光、读出后,原始数据要通过MIPI或并行总线一帧一帧搬到SoC,再进ISP、内存,最后才轮到神经网络推理。而in-sensor computing的做法是在感光阵列内部或紧邻阵列的列级电路里,先把一些特定的乘加运算做完,输出的已经不是原始像素,而是特征图、目标框候选区、或“有事件发生”的稀疏标记。
打个比方,传统方案是超市把所有货物都运回总仓,再让拣货员满仓库找你要的那瓶水。传感器端计算等于在货架上就地放了小机器人,它看一眼就知道“这瓶水在第几排第几列”,你只需要去取结果。整个过程省掉了大规模货物搬运。这里的“货物”就是图像数据,“搬运”就是像素读出和总线传输。
1.2 为什么非要在传感器上算,而不是在MCU或云端算
核心原因两个字:能耗,再加两个字:延迟。我在项目里做过一组粗算,一颗720p的CMOS传感器跑30帧,每帧原始数据大概在1MB以上,如果走10bit ADC,一秒的数据量是300MB上下。这些数据先要从像素一行一行扫描读出,再经过模拟前端、数字接口、DMA,最后进DDR。每一次读取和搬运,单位能耗远大于一次数学运算本身。业界有一个经常被引用的数字:一次8bit整数乘加操作能耗大约是0.2到0.5pJ,但一次从DRAM读取8bit数据可能要几十到几百pJ,差了两个数量级。
换句话说,很多边缘AI系统的功耗大头根本不是“算”,而是“挪数据”。传感器端计算的思路就是,既然挪数据这么贵,那就让数据待在产生它的地方,先算完第一层再往外挪。这样数据量被大幅压缩,后级处理器的压力也小很多。延迟也一样,传统链路从曝光到推理结果出来,至少要经过“曝光→读出→传输→ISP→推理”几个环节,而在传感器端,往往在曝光积分的过程中就完成了初步判断,事件从检测到响应的延迟可以做到微秒到亚毫秒级,这对机械臂抓取、无人机避障、AR手势识别这类场景极其关键。
1.3 模拟域计算和数字域计算到底怎么选
刚开始接触这个方向时,我被“模拟域计算”这个概念绕了很久。实际上它没那么玄。传统图像传感器里,像素的光电二极管受光照后会产生光电流,光电流在浮置扩散区或积分电容上电荷累积,最终变成电压。模拟域计算就是利用这个物理过程直接做乘加,把入射光强当成输入,权重用可控电导或可编程电容的物理量表示,照在多个像素上的光电流经过不同大小的“权重通道”后,直接流进同一个汇总节点,电荷一累加,卷积结果就出来了。整个MAC过程是物理发生的,不需要先做ADC,也不需要寄存器参与。
数字域计算就好理解得多。它是在像素阵列旁边或每一列下面放一些数字存储单元,提前存好卷积核权重,等像素值读出后,用数字电路做逐位累加。好处是精度高、抗噪声能力强,支持更复杂的网络结构,坏处是每个像素或每组像素都要塞数字逻辑,面积开销大,单元密度低,传感器分辨率很难做高。选型时主要看场景:如果只需要做目标有无判断、运动感知这种低复杂度任务,模拟域是最优的,功耗可以压到极致;如果要做实时分类、多目标检测这类对精度敏感的算法,宁可让出一些面积做数字域,也得保证精度稳定。
1.4 别再把它和边缘计算搞混
有一个观念我花了不少时间才向团队里的人解释清楚,就是传感器端计算和边缘计算、TinyML不是一回事。TinyML是把模型部署在MCU或低功耗DSP上,数据还是先把原始像素从传感器读出来,再交给MCU内存去处理;边缘计算更是如此,只是把推理位置从云端挪到网关或本地服务器。它们都没有碰“像素读出搬移”这个最贵的环节。
对比一下三种方案:
| 环节 | 云端计算 | 边缘计算/MCU | 传感器端计算 |
|---|---|---|---|
| 数据量传输 | 全量原始数据 | 全量或压缩原始数据 | 特征图/事件/目标框 |
| 延迟 | 几百毫秒到秒级 | 十毫秒级 | 微秒到亚毫秒级 |
| 系统功耗 | 最高 | 较高 | 最低 |
| 隐私风险 | 高 | 中 | 低 |
| 部署灵活性 | 依赖网络 | 中等 | 受传感器硬件约束 |
传感器端最吸引我的地方在于,它在物理层就把数据隐私和带宽问题一起解决了。一些敏感场景下,原始图像根本不需要离开传感器,外部只能拿到算法提取后的结构化信息,这比任何加密方案都天然。
2. 核心技术点拆解与方案选型
2.1 三条主流实现路线,各适合什么场景
我按自己的项目经验把目前看到的传感器端计算实现分成了三条路线。
第一条是像素内卷积路线,也是最容易理解的一种。它在像素阵列里做局部感受野的加权求和,一个输出像素对应周围一片输入像素的加权和。这种方案适合做第一层卷积、边缘检测、图像滤波这类空域操作。工程上常见做法是用3x3或7x7的卷积核扫描全图,把分块特征直接以模拟量形式输出。优点是全图特征提取效率高,缺点是精度受底噪影响大,卷积核一般只能做固定权重,动态调整能力弱。
第二条是事件驱动神经形态路线。这类传感器不是按帧输出图像,而是每个像素独立判断光照变化是否超过阈值,超过就输出一个脉冲事件。因为只有“变化了”的信息才输出,静态背景完全不产生数据,所以数据稀疏性极高。事件相机可以用来做人眼跟踪、高速运动检测、振动监测,在低功耗场景里表现非常惊艳。但它不适合需要纹理细节的任务,因为静止物体根本不产生事件。
第三条是感算一体的光电存储路线,把存储器和感光层做垂直集成,像素本身就具备存内计算能力。这类器件可以用电荷俘获状态或忆阻器电导来编码权重,光输入直接乘上电导值完成MAC。它的密度和能效理论上最高,但目前成熟度较低,商业化器件很少,主要还停留在实验室和原型验证阶段。
我对一般项目团队的建议是:短期落地选第一条,先解决产品能效问题,对数据稀疏性有特殊要求的选第二条高风险高回报,第三条可以持续关注但别过早绑进产品路线。
2.2 算法怎么迁过来:量化、轻量化、稀疏化
传感器端的计算资源比CPU和GPU稀缺得多,模型不可能直接用常规CNN。我自己总结了一套适配流程,顺序很固定,看大家能不能也用上。
优先做权重低比特化。第一层卷积的权重往往可以压到1bit到2bit,因为浅层特征以边缘和纹理为主,对数值精度不敏感。1bit权重意味着乘加操作变成XNOR加popcount,硬件实现非常简单,面积和功耗都省到极致。我把一个手写数字识别网络的第一层从8bit压到1bit后,精度只掉了0.7%,传感器端的面积却能缩小一半。
其次使用深度可分离卷积结构替代标准卷积。标准3x3卷积要做输出通道数乘输入通道数次乘加,深度可分离卷积把空间卷积和通道融合分开做,计算量降低到原来的九分之一左右。这个结构很适合传感器端,因为每一层都很浅,通道数也少,压缩效果非常明显。
最后做事件稀疏化或RoI区域裁剪。不是所有像素都需要进入计算。我做的场景里,绝大多数画面属于背景,真正需要分析的区域只占很小比例。我让传感器先做一个粗粒度的“变化检测”,只把变化区域切成小块做高精度分析,其他区域直接跳过,功耗可以再省一到两个数量级。这一步对系统级收益最大,但需要算法工程师和硬件工程师一起设计,单纯改软件很难把效果做到极致。
2.3 核心指标可以怎么估算、怎么对比
选传感器端方案时,我一般看四个指标,分别是能效、端到端延迟、输出数据带宽、有效精度保持度。能效用TOPS/W表示,就是每瓦每秒能完成多少次万亿次操作。传统AI加速芯片的能效一般在1到10 TOPS/W,而传感器端模拟计算路线可以做到几十甚至上百TOPS/W,前提是只算浅层卷积。延迟上,传统摄像头从曝光到推理结果出来的链路延迟通常在30到100毫秒,换成传感器端后可以压到1毫秒内。带宽更不用说,输出从原始图像变成目标框坐标或事件流之后,数据量能少三个数量级。
有效精度保持度是个容易被忽略的指标。传感器端的计算是在模拟噪声、像素不均匀性、工艺漂移等非理想条件下进行的,模型在普通GPU上达到95%精度,放到传感器端后可能只有80%都不到。我建议在项目预研阶段,就把器件层面的非理想因素建模进训练和验证流程,别等硬件回来了再亡羊补牢。
2.4 传感器与算法协同设计是绕不开的关卡
这是我对整个方向最深的一个感悟。传感器端计算不是一个单纯的算法问题,也不是单纯的器件问题,而是两者联合设计的问题。传统开发流程里,算法工程师拿到图像数据,传感器工程师负责搭光电链路,两端开发是解耦的;到了in-sensor computing,解耦模式直接失效,传感器的噪声、动态范围、量化精度会直接影响模型第一层卷积的数值分布,而模型的结构也反过来决定传感器像素阵列的拓扑和读出策略。
所以我在方案规划阶段就坚持让算法团队加入传感器模型仿真的环节,把传感器输出建模成“理想图像 + 噪声 + 量化 + 非线性 + 运动伪影”,把这个遮蔽后的图像当成训练数据的一部分。这一步做和不做,最终板级精度的差距经常在5到10个百分点以上。下面第三部分,我会把一套完整可复现的仿真验证流程分享出来。
3. 从零搭一套可复现的传感器端计算验证流程
3.1 为什么要先做仿真验证,而不是直接流片
传感器端计算的硬件门槛很高,小团队很难一开始就流片验证。我的做法是先做一个“物理感知仿真”,把传感器端计算的行为模型跑在PyTorch里,先用软件把算法和噪声的关系摸透,再决定要不要往硬件走。这样做的原因很简单:传感器端很多设计失误,比如量化位宽不够、权重溢出、噪声放大,在仿真阶段就能暴露,不用花几十万块流片费用去试错。
我这次验证选的是一个具体任务:在模拟成像传感器上,用第一层卷积直接提取图像局部特征,再判断画面里是否出现了指定目标的区域。这个任务不复杂,但足够把传感器端计算的关键链路跑一遍。平台用的是Python和PyTorch,代码结构大致是这样:先模拟一个带有量子噪声的传感器输出,再定义一组量化卷积核,在像素级做乘加,最后对输出结果特征图做判断。
3.2 模拟传感器像素阵列的光电响应
传感器像素的输出不是理想灰度值,它要经过光电转换、电荷积分、复位噪声、读出噪声、ADC量化等环节。我在仿真里用一个简单的模型来概括这些效应,把一帧理想图像变成带噪的数字化原始图像。
import torch import torch.nn as nn import torch.nn.functional as F gauss = torch.distributions.normal.Normal(torch.tensor([0.0]), torch.tensor([1.0])) def simulate_raw_image(ideal_img, qe=0.5, full_well=30000.0, read_noise=1.5, bit_depth=10): """ 模拟CMOS像素阵列输出: ideal_img: 0~1范围理想灰度图 qe: 量子效率,按比例转换光子数 full_well: 像素满阱容量 read_noise: 读出噪声标准差,单位电子数 bit_depth: ADC量化位宽 """ photon_count = ideal_img * qe * full_well shot_noise = torch.sqrt(photon_count) * gauss.sample(ideal_img.shape).squeeze(-1) electron_count = photon_count + shot_noise electron_count = torch.clamp(electron_count, 0, full_well) noisy_voltage = electron_count + read_noise * gauss.sample(ideal_img.shape).squeeze(-1) quantization_level = 2 ** bit_depth pixel_value = torch.round(noisy_voltage / full_well * (quantization_level - 1)) return pixel_value / (quantization_level - 1)这个函数做了三件事:一是用泊松分布近似光子散粒噪声,光子数越少噪声越大,对应暗光环境下图像变花;二是加入电阻热噪声导致的读出噪声;三是模拟ADC量化,把连续电压转成整数像素值。这一层仿真是我建议所有做传感器端算法的团队都不要省略的,因为第一层卷积是对原始像素值直接操作,噪声和量化的影响会被卷积核的权重放大。
3.3 定义像素内卷积核和权重量化
传感器端第一层卷积的权重需要提前烧录或配置到像素电路的存储单元里。这次我用的是3x3卷积核,分别模拟两个常用的浅层特征算子:边缘检测算子和高斯平滑算子。同时,我模拟了8bit和2bit两种量化情况,用来对比权重量化对特征提取的影响。
def quantize_weight(weight, bits=2): """把权重量化到[-1,1]区间的低比特值""" scale = 2 ** (bits - 1) - 1 quantized = torch.clamp(weight, -1, 1) quantized = torch.round(quantized * scale) / scale return quantized edge_kernel = torch.tensor([[[[-1, -1, -1], [-1, 8, -1], [-1, -1, -1]]]], dtype=torch.float32) smooth_kernel = torch.tensor([[[[1, 2, 1], [2, 4, 2], [1, 2, 1]]]], dtype=torch.float32) / 16.0 edge_8bit = quantize_weight(edge_kernel, bits=8) edge_2bit = quantize_weight(edge_kernel, bits=2)这里有个很关键的点:传感器端的第一层卷积一般是不可学习的固定核,或者只在出厂时做一次离线训练。因为像素内的模拟权重一旦流片后就很难动态更新,算法工程师必须接受“第一层固定、后续层可调”的约束。我通常做法是把后续可学习部分的网络结构设计得足够强大,让第一层固定特征提取后的损失可以靠浅层全连接或1x1卷积来补足。
3.4 在模拟图像上完成像素内卷积
我用的测试图像是一张带有边缘物体的合成灰度图。先经过3.2节的传感器仿真,变成带噪原始图,然后分别用2bit和8bit的卷积核做卷积,比较输出特征图的信噪比。
batch = raw_image.unsqueeze(0).unsqueeze(0) # 增加 batch 和 channel 维 def conv_with_noise(feature_map, kernel): # 使用卷积操作,同时给输出加一点列固定模式噪声 out = F.conv2d(feature_map, kernel, padding=1) col_noise = torch.randn(out.shape[0], out.shape[1], out.shape[2], 1) * 0.02 return out + col_noise out_8bit = conv_with_noise(batch, edge_8bit) out_2bit = conv_with_noise(batch, edge_2bit)这段代码虽然只有几行,但它抓住了传感器端卷积的核心差异。8bit权重下,边缘特征能保持清晰的梯度响应;2bit权重下,卷积核的高频响应会变得粗糙,但依然能勾勒出物体边界。这个实验给我最大的启示是:即便是1bit权重,只要传感器噪声控制得当,浅层特征的质量并不会崩溃,真正伤精度的是后层深度网络对误差的累积。
3.5 关键参数计算:功耗和延迟怎么估
仿真验证不只验证精度,也要在数字上验证能效收益。我按下面的逻辑估算整个系统的功耗。
传统链路中,一帧100x100的灰度图像,10bit ADC输出,每像素读出功耗大约按50pJ算,那么读出10000像素就要0.5微焦。如果后续在MCU上跑一个3x3卷积,按每像素MAC需要8次乘法加8次加法估算,10000像素约160万次操作,每次操作按0.3pJ算,加起来约0.48微焦,加上数据搬移消耗,整帧处理的能耗基本在1微焦以上。
传感器端链路的能耗模型完全不同。像素在积分过程中直接做加权求和,等于把一次3x3卷积的9次乘加压缩成一次电荷累积,每输出一个特征像素,只消耗一次模拟MAC的能耗,大约0.05pJ,还是100x100输出特征图,总MAC能耗只有0.005微焦,相比传统链路低了两个数量级。再加上不需要大量数据搬移,不需要ADC全量量化,整个系统端到端功耗可以压缩到原来的十分之一甚至更低。
延迟方面也很明显。传统链路至少需要一行一行读出整帧之后,才能开始第一层卷积;传感器端在曝光积分阶段就完成了卷积,曝光完成时特征图已经躺在输出节点上。对全局快门传感器,这意味着输出一个特征图的时间约等于曝光时间,省掉的至少是读出行扫描和传输的几十毫秒。
3.6 验证结果怎么看、怎么继续往下走
仿真跑完之后,我一般会看三张图:原始图像、8bit卷积特征图、2bit卷积特征图。如果2bit和8bit特征图的边缘形态趋势一致,说明这个任务可以在低比特权重下继续往下做;如果差异过大,就要检查噪声模型是不是过于严苛,或者任务本身不适合传感器端第一层处理。
下一步是在仿真里加入更真实的光照变化、卷帘快门效应和像素坏点。我建议把坏点模拟也放到仿真早期,因为坏点会直接导致卷积核的响应被污染,后处理阶段再想修复就非常被动。仿真通过后再去做FPGA或SoC原型,可以避免很多低级错误。
4. 工程落地时踩过的坑和排查经验
4.1 固定模式噪声怎么就成了精度杀手
仿真阶段我犯过一个错误:用的噪声模型过于干净,没有加入列固定模式噪声和像素固定模式噪声。真实传感器中,由于工艺偏差,每一列放大器的增益、暗电流、偏置电压都不同,导致满帧图像上出现固定的“条纹”或“斑点”。这个固定模式噪声在视觉观感上可能不明显,但一旦经过3x3卷积核加权,就会变成明显的列状高响应假边缘。
解决方案有两层。第一层是硬件校准,做相关双采样和列级校正可以在一定程度上抑制,但无法完全消除。第二层是算法补偿,把固定模式噪声做成扰动项,在训练模型的第一层之前注入,让网络学会自动忽略这些固定伪影。我实测下来,搭配算法补偿后,传感器端第一层输出的信噪比能提高约3dB。大家在自己仿真时也务必确认,代码里是不是把列噪声当独立变量加进去了,不要顺手用一个全图高斯噪声代替。
4.2 模拟计算的温度漂移和工艺漂移
传感器端模拟计算最让人头疼的问题是漂移。模拟器件对温度极为敏感,同一个卷积核在25度和60度下的有效权重可能差了5%以上。这会导致在实验室调试很好的模型,装进户外设备后精度骤降。工艺漂移则表现为不同批次的传感器之间权重偏差不一致,换一颗传感器就得重新校准,对量产来说非常痛苦。
我踩过温度漂移的坑之后,现在做传感器端设计时会给第一层卷积的权重留出校准通道。具体做法是把部分权重参数做成可微调的小规模数字模拟转换,在设备启动时先运行一段校准图案,根据输出反推出当前温度下的权重偏移量,再做一次补偿。这个思路不增加太多硬件成本,但能让模型适应温度范围扩大好几倍。
4.3 数据分布漂移:同一颗传感器,不同光线下的适配
传感器端计算还有一个隐性风险,就是数据分布漂移。同一颗传感器,在不同光照条件、不同色温环境下,输出的特征图分布差异很大。我在一个物体检测例子里发现,模型在室内灯光下检测率92%,换到户外阴天就降到80%,原因正是第一层卷积的输出范围被光照压缩了。
针对这类问题的建议是:算法侧在特征层加入光照归一化模块,把第一层输出做自适应缩放;或者干脆在训练数据里做光照增强,覆盖不同曝光条件下的数值范围。更激进的做法是在传感器端加一个简单的自动曝光反馈,让像素积分时间随光照动态调整,但这个需要硬件参与,项目周期允许的话可以一起规划。
4.4 工具链割裂和团队协作问题
工程落地中最容易被低估的是工具链问题。传感器端计算同时涉及传感器设计工具、模拟电路仿真、神经网络框架、嵌入式部署工具,生态极不成熟。算法工程师习惯用PyTorch导出模型,但传感器的流片环境用的是Cadence或Synopsys,两边模型格式完全不互通。我现在的做法是强制要求仿真流程中所有数据都从“可复现的数值文件”中流转,把PyTorch的权重导出成标准numpy数组或CSV,再让硬件组自己做转换,避免被单一工具链绑死。
团队协作上,最大的挑战是让算法工程师和模拟工程师互相理解彼此的语言。我的经验是每周同步一次,算法组把最新精度数据反馈给硬件组,硬件组把噪声波形图反馈给算法组,双方对着同一张特征图讨论问题,效率远高于各自闷头调参。
4.5 关于这个方向的一点体会
传感器端计算不是万能的,它最大的优势在于把“数据搬运”这个隐形瓶颈彻底干掉。但如果任务需要高频更新权重、需要处理复杂语义、需要大范围场景理解,传统边缘计算仍然不可替代。我的建议是把它当作系统设计中“最前端的一道加速器”,而不是替代所有计算单元的银弹。
实际项目里,一个好的落地方式是分级处理:传感器端负责极低成本的“有没有、动没动、在哪个区域”判断,一旦判断需要更细的语义分析,才触发后级更高功耗的计算单元。这样既能保留传感器端计算的低功耗优势,又不牺牲复杂任务的处理能力。真正把传感器端计算用好的人,不是把一切算法都塞进像素里,而是知道哪一层计算该留在像素里,哪一层该交给后面的硬件去跑。