1. 三个改变世界的瞬间为什么偏偏挤在同一天11 月 8 日这个日子翻技术史的时候总会让人愣一下。计算机领域的奠基者之一在这一天出生一个后来撑起整个深度学习时代的网络结构在这一天被正式发表而更早的时候一项让人类第一次实现隔空操控的发明也在这一天递交了专利申请。三件事横跨了将近一个半世纪却像约好了一样落在同一个日期上。我平时有翻老论文和老专利的习惯越看越觉得这些节点不是孤立的巧合而是技术演进里那种该来的总会来的必然。计算机先驱的诞生解决的是机器能不能按人的意志做通用计算这个根本问题LeNet-5 的问世回答的是机器能不能自己从数据里学会识别模式而遥控器的发明则把人机之间隔着距离还能不能交互这件事变成了现实。三条线看似不搭界其实都在处理同一个母题——人如何把自己的意图可靠地传递给机器并让机器给出正确的反馈。这篇东西我打算把这三个节点拆开揉碎讲一遍。不是那种百科式的年份罗列而是从从业者的角度聊聊每个节点背后到底解决了什么工程难题、当时的人是怎么想的、这些思路今天还在哪些地方被沿用。如果你是对计算机历史感兴趣的开发者或者正在学深度学习想搞清楚 LeNet-5 到底牛在哪又或者只是单纯好奇遥控器这么简单的东西有什么好讲的那这篇应该都能给你一些不一样的角度。我会尽量把原理讲透也会补上一些自己踩过的坑和实操层面的细节让这些历史上的今天真正能落到你手上的代码和项目里。2. 计算机先驱的诞生通用计算这件事是怎么被想明白的2.1 那个年代计算意味着什么要理解这位先驱的意义得先回到他出生的那个年代看看当时计算是个什么状态。十九世纪中后期到二十世纪初计算基本等同于人肉算盘——大量受过训练的运算员拿着纸笔和机械计算器按照固定流程一步步算。天文台算星表、保险公司算费率、军方算弹道表靠的都是这种人力流水线。这种模式的问题非常致命慢、贵、而且极易出错一个环节抄错一个数字后面全盘皆输。当时也有机械计算设备比如各种差分机、分析机的构想但大多停留在专用层面——一台机器只能干一件事。真正稀缺的是一种能根据指令序列自动完成任意计算任务的通用机器。这个想法今天听起来理所当然但在当时是极其超前的。因为通用意味着机器必须能存储指令、能根据中间结果做判断、能循环执行这在纯机械时代几乎是工程噩梦。这位先驱的核心贡献恰恰在于把通用计算从一个哲学构想推进到了可实现的逻辑模型层面。他提出的关键洞察是计算过程可以被分解为一组基本操作的序列而机器只需要一个能读写、能按状态转移的执行单元就能模拟任何可计算的过程。这个思路后来被形式化为我们熟悉的计算模型成了整个计算机科学的理论地基。2.2 从专用机器到通用模型的关键一跃我特别想强调这一跃的难度。今天你写代码默认有 CPU、有内存、有循环和条件判断觉得天经地义。但在那个连程序存储概念都没成型的年代要论证一台机器可以模拟另一台机器需要极强的抽象能力。这位先驱做的事情本质上是把计算从物理设备里剥离出来变成一个纯粹的数学对象。这个抽象带来的直接后果是人们第一次意识到计算能力不依赖于具体的硬件形态。你可以用齿轮实现可以用继电器实现可以用电子管实现甚至理论上可以用任何能表达状态转移的物理系统实现。只要逻辑模型成立剩下的就是工程问题。这种逻辑与实现分离的思想是后来所有计算机体系结构、编程语言、编译器的思想源头。举个具体的例子帮助理解。假设你要算一个复杂的函数专用机器可能直接把公式焊死在电路里换个函数就得重新设计硬件。而通用模型下你把函数拆成读入、比较、加减、跳转、输出这些基本动作机器按顺序执行就行。换函数只需要换指令序列硬件不动。这就是为什么后来的计算机能跑游戏、能处理文档、能训练神经网络——同一套硬件靠不同的指令序列完成完全不同的任务。2.3 这套思想今天还在哪里发光别以为这是老古董。你今天写的每一行代码背后都站着这套通用计算的思想。更具体地说有几个地方能直接看到它的影子。第一是虚拟机与解释器。当你用 Python 跑脚本解释器本质上就是在模拟一台能执行 Python 指令的抽象机器。这台机器不存在于硅片上而是用 C 语言描述出来的。这正是一台机器模拟另一台机器思想的现代版本。第二是图灵完备性判断。我们评估一门新语言、一个新计算模型比如某些区块链脚本、某些配置语言时第一件事就是问它图灵完备吗。这个判断标准源头就是通用计算模型。第三是可计算性边界。有些问题理论上就无解比如停机问题。理解这个边界能帮你在工程里少走弯路——当你发现某个需求怎么设计都别扭时有可能不是方案问题而是它本身就撞到了可计算性的墙。提示学计算机的人容易把通用计算当成理所当然。但真正理解它的边界和代价能让你在设计系统时更清醒。比如为什么有些嵌入式场景宁愿用状态机而不是通用 CPU就是因为通用性是有成本的。2.4 实操层面用最小模型理解通用计算想真正吃透这套思想我建议你动手实现一个极简的计算模型。不用复杂一个能读写纸带、能根据当前符号和状态决定写什么、往哪移动、切换到哪个状态的小程序就行。下面是一个用 Python 模拟的骨架帮你建立直觉# 一个极简的状态转移执行器用来体会通用计算的核心机制 # 规则格式: (当前状态, 读到的符号) - (写入符号, 移动方向, 新状态) rules { (q0, 1): (1, R, q0), (q0, _): (_, L, q1), # 读到空白就停下并回退 } def run(tape, state, head, steps100): tape list(tape) for _ in range(steps): symbol tape[head] if (state, symbol) not in rules: break write, move, state rules[(state, symbol)] tape[head] write head 1 if move R else -1 if head 0: tape.insert(0, _) head 0 if head len(tape): tape.append(_) return .join(tape).strip(_), state print(run(111_, q0, 0))这段代码跑起来很简单但它体现的正是通用计算的精髓一套固定的规则表配合可读写的存储就能表达任意计算过程。你可以试着往 rules 里加规则让它做加法、做比较甚至模拟另一个执行器。亲手写过一遍你对通用二字的理解会完全不一样。3. LeNet-5 问世卷积神经网络第一次真正跑通3.1 在 LeNet-5 之前图像识别卡在哪聊 LeNet-5 之前得先说说它出现之前图像识别有多难。上世纪八九十年代让机器认数字、认字母主流做法是手工设计特征。什么意思呢就是人先想好这个数字有哪些特征比如笔画端点数量、闭合区域个数、笔画方向直方图然后把这些特征提取出来再喂给一个分类器比如 SVM 或简单的全连接网络。这套流程的问题在于特征是人拍脑袋定的换个任务就得重新设计。手写数字和印刷体数字特征不一样认数字和认人脸更是天差地别。而且手工特征对平移、缩放、轻微旋转极其敏感——同一个数字往左挪两个像素特征值可能就全变了分类器直接懵掉。当时的研究者花了大量精力在特征工程上效果却始终有限。LeNet-5 的突破点在于它不再依赖人工设计特征而是让网络自己从原始像素里学。这在当时是相当激进的想法。因为那个年代算力稀缺、数据也少让网络自己学听起来像是不靠谱的浪漫主义。但 LeNet-5 用一套精巧的结构设计证明了这条路走得通。3.2 卷积、池化、权值共享三个关键设计到底解决了什么LeNet-5 的结构今天看已经很经典了但每个设计背后的动机值得掰开讲。卷积操作解决的是局部模式提取问题。图像里很多有意义的模式边缘、角点、笔画都是局部的一个像素和它周围几个像素的关系最重要离得远的像素关系不大。卷积核就是一个小窗口在图像上滑动每个位置做加权求和。这样网络就能专门捕捉局部特征而不是像全连接层那样把整张图拉平、丢失空间结构。权值共享解决的是参数量爆炸和平移不变性问题。如果每个位置都用独立的权重一张 32x32 的图接一个全连接层参数量轻松上万。而卷积核在整张图上共享同一组权重参数量骤降同时带来一个美妙副作用同一个模式无论出现在图像哪个位置都会被同一个卷积核检测到。这就是平移不变性的来源。池化操作解决的是降维和轻微形变容忍问题。池化把特征图分成小块每块取最大值或平均值尺寸直接减半。这样做一方面减少计算量另一方面让网络对输入的微小位移不那么敏感——只要特征还在这个小块里池化后结果就一样。下面这张表把 LeNet-5 各层的作用和参数规模理一下方便你建立整体印象层类型作用关键参数输出尺寸以 32x32 输入为例卷积 C1提取初级边缘特征6 个 5x5 卷积核6 x 28 x 28池化 S2降采样增强鲁棒性2x2 平均池化6 x 14 x 14卷积 C3组合初级特征成更复杂模式16 个 5x5 卷积核16 x 10 x 10池化 S4再次降采样2x2 平均池化16 x 5 x 5卷积 C5全连接式卷积120 个 5x5 卷积核120 x 1 x 1全连接 F6特征整合84 个神经元84输出层分类10 个神经元103.3 为什么 LeNet-5 是深度学习的活化石LeNet-5 最了不起的地方不在于它在当时多准而在于它奠定了后来几乎所有卷积网络的骨架。你今天用的 ResNet、VGG、甚至各种视觉 Transformer 之前的 CNN 变体基本结构都是卷积 池化 全连接这套组合的演化。LeNet-5 就是那个原型。更关键的是它验证了一个核心假设端到端学习是可行的。也就是说你不需要手工设计特征只要给足数据和算力网络能自己学到有用的表示。这个假设在 LeNet-5 时代还只是在小数据集上成立但到了 2012 年 AlexNet 用同样的思路在大规模数据上碾压传统方法时整个领域就彻底转向了。可以说LeNet-5 是那场革命的种子。我个人的看法是学深度学习的人如果跳过 LeNet-5 直接上 ResNet会缺失对卷积网络为什么这样设计的直觉。很多后来看起来很花哨的技巧本质上都是在解决 LeNet-5 时代就暴露出来的问题——梯度消失、过拟合、训练慢。理解了原型后面的演化才有脉络。3.4 动手复现用现代框架搭一个 LeNet-5理论讲再多不如跑一遍。下面用 PyTorch 复现一个 LeNet-5 的变体输入改成 28x28方便直接吃 MNIST 数据。注意我在关键地方加了注释说明每个参数为什么这么选。import torch import torch.nn as nn class LeNet5(nn.Module): def __init__(self, num_classes10): super().__init__() # C1: 输入 1 通道输出 6 通道5x5 卷积核 # 选 5x5 是因为手写数字的笔画宽度和这个尺度匹配 self.conv1 nn.Conv2d(1, 6, kernel_size5, padding2) # S2: 2x2 平均池化步长 2尺寸减半 self.pool1 nn.AvgPool2d(kernel_size2, stride2) # C3: 6 通道进16 通道出 self.conv2 nn.Conv2d(6, 16, kernel_size5) self.pool2 nn.AvgPool2d(kernel_size2, stride2) # C5: 卷积核大小刚好等于特征图尺寸等价于全连接 self.conv3 nn.Conv2d(16, 120, kernel_size5) self.fc1 nn.Linear(120, 84) self.fc2 nn.Linear(84, num_classes) def forward(self, x): x torch.relu(self.conv1(x)) x self.pool1(x) x torch.relu(self.conv2(x)) x self.pool2(x) x torch.relu(self.conv3(x)) x x.view(x.size(0), -1) # 拉平准备进全连接 x torch.relu(self.fc1(x)) return self.fc2(x) model LeNet5() print(sum(p.numel() for p in model.parameters()), 个参数)跑一下你会发现整个模型参数量只有几万级别放在今天简直小得可怜。但正是这个小模型证明了卷积结构的有效性。你可以试着把AvgPool2d换成MaxPool2d或者把激活函数从 ReLU 换回原始的 sigmoid/tanh观察训练速度和准确率的变化——这个对比实验能让你直观感受到后来那些改进的价值。注意原始 LeNet-5 用的是 tanh 激活和平均池化现代复现通常换成 ReLU 和最大池化收敛更快。做历史复现时别纠结必须一模一样理解设计意图比复刻细节更重要。4. 遥控器发明隔空操控背后的信号与编码4.1 从有线到无线的那一步遥控器这东西今天便宜到几块钱一个以至于没人觉得它有什么技术含量。但回到它被发明的那一刻意义完全不同。在此之前要控制一台设备你得用线连着或者人站在设备旁边操作。隔空操控第一次把人和机器在物理上解耦了——你可以站在房间另一头甚至另一个房间让机器执行你的意图。这项发明最初的应用场景是控制移动载具。它的核心思路是发送端把控制意图编码成某种信号早期是无线电波接收端接收信号、解码、驱动执行机构。听起来简单但当时要解决几个硬问题信号怎么编码才能区分不同指令怎么避免误触发接收端怎么在嘈杂环境里可靠识别4.2 信号编码遥控器真正的技术内核很多人以为遥控器的关键是无线传输其实真正的难点在编码与解码。无线传输只是载体编码才决定了这个系统好不好用。早期遥控器用的是简单的频率或脉冲编码一个按键对应一种信号。问题是通道数有限按键一多就冲突而且容易受干扰。后来发展出脉冲编码调制思路把指令编码成一串有特定时序的脉冲接收端按约定解码。这样一条信号线上就能传多个指令抗干扰能力也强得多。这套思路今天还在用只是形式变了。你家里的红外遥控器用的是 38kHz 载波加脉冲位置调制无线遥控车用的可能是 2.4GHz 跳频加数据包编码。本质都是把意图变成可区分、可纠错的信号序列。下面这张表对比几种常见遥控编码方式帮你建立整体认知编码方式典型应用优点缺点简单频率编码早期遥控载具电路极简通道少易干扰脉冲位置调制红外遥控通道多成本低需视线传播脉冲编码调制通用遥控抗干扰强电路稍复杂数据包编码2.4G 遥控通道极多可双向成本较高4.3 从遥控器看现代交互设计遥控器发明带来的影响远不止能隔空控制这么简单。它开启了一个思路人和机器之间的交互可以抽象成意图编码—传输—解码—执行的链路。这个链路模型今天几乎无处不在。你想想手机触屏手指点击是意图触摸屏控制器把它编码成坐标数据系统解码后执行对应操作。你想想语音助手你说的话是意图被编码成音频数据云端解码成文本再解析成指令。甚至你想想 API 调用客户端请求是意图编码成 HTTP 报文服务端解码后执行。全都是同一套链路模型。理解了这一点你在做交互系统设计时就会多一层思考编码方式是否足够区分不同意图传输环节是否可靠解码端是否能处理异常输入这些问题在遥控器时代就被反复验证过今天只是换了载体。4.4 自己动手用红外收发模块做一个最小遥控系统想真正理解遥控原理最好的办法是自己搭一套。用常见的红外发射管和接收头配合一块开发板就能做出一个能发能收的最小系统。核心是理解载波和编码。# 用脉冲位置调制发送一个简单指令的示意逻辑 # 实际硬件上需要配合定时器产生 38kHz 载波 import time def send_bit(pin, bit, carrier_period26): # carrier_period 单位微秒约对应 38kHz if bit 1: # 发一个较长的高电平脉冲表示 1 pin.high() time.sleep_us(560) pin.low() time.sleep_us(560) else: # 短脉冲表示 0 pin.high() time.sleep_us(560) pin.low() time.sleep_us(1690) def send_command(pin, command_bits): # 先发引导码让接收端同步 pin.high(); time.sleep_us(9000); pin.low(); time.sleep_us(4500) for bit in command_bits: send_bit(pin, bit)这段代码是逻辑示意实际跑需要处理微秒级定时和载波调制。但写一遍你就会明白遥控器的可靠性全靠收发双方对时序的严格约定。发送端脉冲宽度稍有偏差接收端就可能解错。这也是为什么早期遥控器经常按了没反应——不是信号没到而是时序对不上。提示做红外遥控实验时接收头对电源噪声很敏感记得在电源脚旁边加滤波电容。我踩过这个坑信号明明发对了接收端就是乱码折腾半天才发现是电源纹波导致的。5. 三个节点串起来看技术演进的共同规律5.1 抽象层级每上升一次能力就跃迁一次把这三个节点放一起看会发现一个共同规律每一次重大突破本质都是抽象层级的上升。计算机先驱把计算从具体机械里抽象出来变成逻辑模型于是通用计算成为可能。LeNet-5 把特征提取从人工设计里抽象出来交给网络自己学于是图像识别能力跃迁。遥控器把控制从物理连线里抽象出来变成信号编码于是交互距离被打破。这个规律对做技术的人极有启发。当你觉得某个问题怎么都解决不好时不妨问自己我是不是被困在太低的抽象层级里了是不是该往上抽一层换个视角重新定义问题很多看似无解的工程难题换个抽象层级就豁然开朗。5.2 工程可行性往往滞后于思想几十年另一个有意思的现象是这三个节点的思想都超前于它们的工程实现。通用计算的思想在理论层面早就成立但真正造出能用的通用计算机等了几十年。LeNet-5 在 1998 年就发表了但要等到算力和数据都跟上它的价值才被真正释放中间隔了十几年。遥控器的原理不复杂但早期受限于电子元件水平可靠性和成本都很难看。这说明什么一个技术方向能不能成不只看思想对不对还要看配套条件到没到。做技术选型时这一点特别重要。有些方向思想很先进但配套的算力、数据、工具链还没成熟硬上就是给自己找罪受。反过来有些方向思想平平但配套条件刚好成熟反而能快速落地。5.3 给当下做技术的人的三点实在建议基于这三个节点的观察我总结几条自己一直在用的判断原则。第一重视基础模型和原型。LeNet-5 这种原型参数量小、结构清晰是理解整个技术族谱的钥匙。别一上来就啃最复杂的模型先把原型吃透后面的演化你才能看懂每一步在解决什么问题。第二关注抽象层级而非具体实现。具体框架、具体硬件、具体语言都会过时但抽象层级的思想会一直有用。学一个东西时多问它把什么抽象掉了它暴露了什么接口比死记 API 有价值得多。第三判断技术方向时看配套条件。一个想法再好如果算力、数据、工具链、人才储备都不到位那它可能还要再等几年。反过来如果配套条件刚好齐了那这个方向就值得重点投入。注意历史节点的价值不在于记住日期而在于理解当时的人面对什么问题、做了什么取舍。把这种问题—取舍—结果的思维迁移到你自己手上的项目才是读技术史真正的收益。6. 把这些历史落到你手上的项目里6.1 用 LeNet-5 做一次完整的训练实验光看结构不够我建议你完整跑一次训练把数据加载、训练循环、评估都走一遍。下面是一个精简但完整的训练脚本可以直接跑在 MNIST 上。import torch import torch.nn as nn from torch.utils.data import DataLoader from torchvision import datasets, transforms # 数据准备标准化到均值 0.1307、标准差 0.3081这是 MNIST 的统计值 transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ]) train_set datasets.MNIST(./data, trainTrue, downloadTrue, transformtransform) train_loader DataLoader(train_set, batch_size64, shuffleTrue) model LeNet5() optimizer torch.optim.Adam(model.parameters(), lr1e-3) criterion nn.CrossEntropyLoss() for epoch in range(5): model.train() total_loss 0 for images, labels in train_loader: optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() total_loss loss.item() print(fEpoch {epoch1}, Loss: {total_loss/len(train_loader):.4f})跑完五轮准确率通常能到 98% 以上。这个数字放在今天不算惊艳但你要知道这是用一个参数量只有几万的模型、在几分钟内训练出来的。对比一下手工特征时代动辄几周的调参就能体会到端到端学习的威力。6.2 训练中容易踩的坑我在复现 LeNet-5 时踩过几个坑分享出来帮你省时间。第一个坑是输入尺寸没对齐。原始 LeNet-5 输入是 32x32MNIST 是 28x28。如果你直接按原始结构搭卷积后尺寸会算错全连接层输入维度对不上。解决办法要么给输入加 padding 补到 32x32要么调整第一层卷积的 padding。我一般选后者更省事。第二个坑是池化层类型选择。原始论文用平均池化但现代实践里最大池化收敛更快、效果通常更好。如果你追求复现历史用平均池化如果追求效果用最大池化。别在这上面纠结太久。第三个坑是学习率设太大。LeNet-5 参数量小对学习率比较敏感。用 Adam 的话 1e-3 通常合适用 SGD 的话可能要降到 1e-2 甚至更低。学习率太大loss 会震荡不下降太小收敛慢得让人怀疑人生。6.3 从历史节点反推自己的学习路径最后聊聊怎么用这些历史节点规划自己的学习。我的建议是按抽象层级递进而不是按时间顺序。先搞懂通用计算模型理解计算是什么、边界在哪这是地基。然后学 LeNet-5 这类经典网络理解表示学习是怎么回事这是核心方法。再研究遥控器这类交互系统理解意图如何可靠传递这是应用层。三层打通你看任何新技术都能快速定位它在哪一层、解决什么问题。这套路径我自己走过也带过一些朋友走反馈是比从最新论文倒着学要扎实得多。因为最新论文往往默认你已经理解了底层直接讲最上层的技巧基础不牢的人看了只会一头雾水。反过来从底层往上走每一步都有落脚点学起来踏实。提示技术史不是用来背的是用来建立直觉的。当你对某个新框架感到困惑时回头看看它的思想源头往往能一下子想通它为什么这么设计。