这次我们来看一门课,而不是一个“一键部署的工具”。哥伦比亚大学的公开课“计算机视觉第一原理”(First Principles of Computer Vision)第十二讲,主题是神经网络。这一讲在整门课里的位置很关键:前几讲还在讲成像、特征提取、传统几何视觉,到这一讲开始进入神经网络,后续的卷积网络、视觉识别任务都会建立在它的基础上。如果你学过图像处理,也看过不少深度学习实战,但总感觉“特征提取、分类器、神经网络”这些概念之间是断开的,那这一讲正好可以把链路补上。
很多同学学计算机视觉,会直接跳到“怎么调 API”“怎么微调大模型”,这本身没问题。但一旦遇到模型效果不好、需要改进网络结构或设计损失函数,缺少原理支撑会非常被动。这门课的价值就在于它从“第一原理”出发,把神经网络是怎么被设计出来、为什么能工作、梯度是怎么传下去的讲清楚。第十二讲不是让你从零手写一个深度学习框架,而是帮你把前馈神经网络、激活函数、损失函数、反向传播这些基础扎牢,再往卷积神经网络和更复杂的视觉模型走就会顺很多。
这篇文章会围绕第十二讲做两件事:第一,整理这一讲的关键知识脉络,给出明确的学习路线;第二,提供一套直接能跑的 PyTorch 实验方案,从多层感知机到 MNIST 手写数字识别,再到一个简单 CNN 对照,让概念落到代码上。内容偏学习笔记和实验指南,适合准备系统学习计算机视觉的本科生、研究生,以及想补齐深度学习基础的工程师。
1. 核心能力速览
| 项目 | 说明 |
|---|---|
| 课程名称 | 计算机视觉第一原理(First Principles of Computer Vision)第十二讲 |
| 课程来源 | 哥伦比亚大学公开课 |
| 本讲主题 | 神经网络基础:前馈网络、激活函数、损失函数、反向传播、梯度下降 |
| 内容范围 | 从传统特征+分类器过渡到神经网络,为卷积神经网络打基础 |
| 适合人群 | 计算机视觉入门者、准备系统学 CV 基础的研究生/工程师 |
| 先修知识 | 线性代数、微积分、概率基础,Python 基础 |
| 实验环境要求 | 不强制 GPU;CPU 即可跑 MNIST 小模型;Windows/macOS/Linux 均可 |
| 建议工具 | Python、NumPy、PyTorch、Jupyter Notebook |
| 学习方式 | 视频 + 讲义 + 代码复现结合 |
| 版权边界 | 课程资源版权归哥伦比亚大学,学习用途需合法获取,公开分享笔记需注明来源 |
从表格可以看到,这一讲对硬件几乎没有什么门槛。学习神经网络原理和跑最基础的手写数字识别实验,用普通笔记本的 CPU 就能完成。如果已经有一块支持 CUDA 的显卡,训练速度会更快,但并不影响理解核心概念。
2. 适用场景与使用边界
先明确这个课程适合谁。最直接的一类读者是“学过图像处理和机器学习基础,但还没系统学过深度学习”的计算机视觉学习者。传统视觉流程通常是 SIFT/HOG 等特征 + SVM/随机森林分类器,而神经网络会把“特征提取”这件事也交给网络自己完成。如果你在做毕业设计、课题入门,或者在准备面试时被问到“卷积神经网络为什么比全连接网络更适合图像”,这一讲的帮助会非常大。
另一类适合的是“做过不少深度学习项目,但基本都是调用现成模型”的工程师。这类人通常知道怎么用 ResNet、怎么训练 YOLO,但对反向传播、梯度消失、激活函数选择背后的原因并不完全清楚。第十二讲的价值是把这些基础概念重新梳理一遍,查漏补缺。需要提醒的是,这不是一门“零基础 Python 入门课”。如果你完全没写过代码,也没有线性代数和基础微积分基础,建议先补一下相关基础知识再看,否则看到矩阵运算、求导和梯度更新时容易卡住。
使用边界也要说清楚。这门课是大学公开课,视频、讲义、作业等资源版权归哥伦比亚大学所有,学习时要从官方或授权渠道获取。后续做实验时,MNIST、CIFAR-10 这类公开数据集有各自的数据许可,通常可以用于学术研究和学习,但如果在商业项目中直接使用,需要确认数据集的许可条款。另外,如果后续把网络用于人脸识别、声音克隆、图像生成等方向,涉及真实人物肖像、隐私或版权素材时,必须确认已获得合法授权,不能拿来随意训练或发布。
3. 学习环境准备与前置条件
这一讲的环境准备非常简单。学习过程中需要三样东西:Python 环境、PyTorch、一个 Notebook 或脚本运行环境。
如果本机已经装过 Anaconda,可以直接创建一个虚拟环境。没有的话,用 venv 也可以。下面是一个通用安装过程:
# 创建并激活虚拟环境(Windows 用 activate) python -m venv cv_env # Linux/macOS source cv_env/bin/activate # Windows cv_env\Scripts\activate # 安装基础依赖 pip install numpy matplotlib torch torchvision jupyterTorch 和 TorchVision 的版本不需要追求最新,优先选择当前稳定版本即可。如果本机有 NVIDIA 显卡,想用 GPU 训练,需要确认显卡驱动和 CUDA 版本匹配。PyTorch 官网会根据 CUDA 版本生成对应的安装命令,建议直接去官网选择合适版本,不要随便复制老教程里的命令。如果不想配置本机环境,也可以用 Google Colab 等在线 Notebook 服务,MNIST 小实验在免费额度内能跑完。
CUDA 是否可用,可以用下面的 Python 代码验证:
import torch print("PyTorch:", torch.__version__) print("CUDA available:", torch.cuda.is_available()) print("Device count:", torch.cuda.device_count())如果输出CUDA available: False,说明当前环境无法使用 GPU,但 CPU 跑 MNIST 多层感知机也完全可行,不要因此卡住。磁盘空间方面,MNIST 数据集本身只有几 MB,学习阶段不需要担心占用。需要担心的是同时安装多个 PyTorch 版本或下载大型预训练模型,那些才会占较多空间。
4. 课程内容结构与学习路线
第十二讲的内容如果拆开看,实际上是从“传统视觉的痛点”一路推到“神经网络的基本训练方式”。不建议直接跳着看,建议按下面这条线走。
4.1 从传统特征到神经网络
传统计算机视觉识别一张图像里的物体,通常需要两步:第一步是设计特征,比如颜色直方图、纹理特征、SIFT 特征点;第二步是把特征送到分类器里做分类。这里的核心问题是:特征需要人来设计,而“到底什么特征最好”很难回答。神经网络改变的不是分类器这一步,而是让网络在训练过程中自己从数据里学习特征。第十二讲的第一层铺垫,就是把这种“手工设计特征”和“数据驱动特征学习”的区别讲明白。理解这一点,后面再看卷积网络里的卷积核,就不会觉得它们是随机初始化的黑盒,而是从数据里学出来的特征模板。
4.2 神经元、感知机与激活函数
神经网络的基本单元是“神经元”。一个神经元做的事情很朴素:把输入做加权求和,加上偏置,再通过一个非线性激活函数输出。写成数学形式就是:
output = activation(W * x + b)其中W是权重,b是偏置,activation是激活函数。单个神经元能解决线性可分问题,但现实里的视觉任务几乎没有线性可分的。要让网络具备非线性表达能力,就需要在每层之间插入激活函数。
第十二讲会重点讲几个常见激活函数:Sigmoid、Tanh、ReLU。Sigmoid 输出范围是 0 到 1,适合作为二分类输出,但容易出现梯度饱和;Tanh 输出范围是 -1 到 1,均值为 0,比 Sigmoid 更容易优化;ReLU 是目前最常用的隐藏层激活函数,计算简单且能缓解梯度消失问题。学习时要关注的不只是函数长什么样,还包括它们的导数。因为反向传播本质上是链式求导,激活函数的导数决定了梯度能不能顺利传到前面的层。
4.3 前馈神经网络与层结构
把很多个神经元按层组织起来,就是前馈神经网络,也叫多层感知机。最典型的结构是输入层、若干隐藏层、输出层。输入层接收数据,隐藏层负责非线性变换,输出层给出预测结果。第十二讲里的神经网络部分,主线就是多层感知机。
理解前馈网络时有两个关键问题需要想清楚。第一个是“为什么需要隐藏层”。没有隐藏层的单层感知机只能画一条直线,解决不了 XOR 这类线性不可分问题;加入一个隐藏层,配合非线性激活函数,就能逼近很多复杂函数。第二个是“加多少层、每层多少个神经元”。这个问题没有理论上的万能答案,更多靠实验和任务约束。课程会给一些直观解释,但真正的感觉要通过代码实验来建立。
4.4 损失函数与梯度下降
网络输出之后,要判断输出和真实标签差多少,就需要损失函数。第十二讲会讨论两种常见损失函数:均方误差(MSE)和交叉熵(Cross-Entropy)。对分类任务来说,交叉熵通常比均方误差更合适,因为它在概率分布上衡量差异,梯度变化也更符合分类场景。对回归任务,均方误差更直观。
有了损失函数,训练目标就是找到一组参数,让损失值尽量小。梯度下降是最核心的优化思想:计算损失对每个参数的梯度,然后沿着梯度的反方向更新参数。学习率决定更新步长,太大容易震荡,太小收敛太慢。第十二讲不会花大量篇幅讲优化器,但会把梯度下降的基本形式讲清楚。后续要学 Adam、SGD 带动量等优化器,也都是在这个思想上做改进。
4.5 反向传播
反向传播是神经网络训练的“发动机”。计算图从输入到输出是前向传播,而反向传播是从损失函数出发,利用链式法则,把梯度从输出层逐层传回输入层。每一层都算出损失对当前层权重的梯度,然后交给优化器做更新。
第十二讲里会逐步推导反向传播的公式。学习时不要只背公式,建议自己拿一个只有两三层的小网络,从手写数字识别任务出发,先手动算一遍中间变量的梯度,再用 PyTorch 的loss.backward()验证结果。这样能把“反向传播只是链式法则”这句话真正变成自己的直觉。
4.6 向卷积神经网络过渡
多层感知机处理图像时,最常见的做法是把图片展平成一维向量,再把向量送入全连接层。这种方式有两个明显问题:一是参数量巨大,一张 28×28 的 MNIST 图片变成 784 维向量,第一层全连接层如果有 256 个神经元,就接近 20 万个参数;二是丢失了图像的空间结构,相邻像素的关系、边缘和纹理信息在展平过程中会变得混乱。
卷积神经网络解决这两个问题的思路是:保留图像的二维结构,用局部连接和权值共享降低参数数量。第十二讲作为神经网络模块的一环,会指出这条通路,但不会一下子展开太多卷积细节。学习到这里,最好已经清楚全连接网络和卷积网络的区别,再进入下一讲就会轻松很多。
5. 动手验证:从公式到代码
只看公式容易飘,动手跑一个最小实验才能真正理解。这里给出三个由浅入深的实验:多层感知机结构、MNIST 手写数字识别、简单 CNN 对照。代码基于 PyTorch,不需要额外模型文件,数据集由 TorchVision 自动下载。
5.1 先理解输入输出结构
先写一个最简单的多层感知机。这个模型接收 28×28 的灰度图,输入维度 784,隐藏层 128 个神经元,输出维度 10。代码里的nn.Flatten()会把图像从[batch, 1, 28, 28]变成[batch, 784]。
import torch import torch.nn as nn class MLP(nn.Module): def __init__(self, input_dim=784, hidden_dim=128, num_classes=10): super().__init__() self.net = nn.Sequential( nn.Flatten(), nn.Linear(input_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, num_classes) ) def forward(self, x): return self.net(x) model = MLP() print(model)输出应该能看到四个组件:Flatten、Linear、ReLU、Linear。运行后如果报错,优先检查 PyTorch 是否安装成功。
5.2 MNIST 手写数字识别完整训练脚本
把模型定义和训练逻辑放在一起,能直接跑通一个完整实验。
import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import DataLoader from torchvision import datasets, transforms device = "cuda" if torch.cuda.is_available() else "cpu" transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ]) train_set = datasets.MNIST("./data", train=True, download=True, transform=transform) test_set = datasets.MNIST("./data", train=False, download=True, transform=transform) train_loader = DataLoader(train_set, batch_size=64, shuffle=True) test_loader = DataLoader(test_set, batch_size=256) class MLP(nn.Module): def __init__(self, input_dim=784, hidden_dim=128, num_classes=10): super().__init__() self.net = nn.Sequential( nn.Flatten(), nn.Linear(input_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, num_classes) ) def forward(self, x): return self.net(x) model = MLP().to(device) optimizer = optim.Adam(model.parameters(), lr=1e-3) criterion = nn.CrossEntropyLoss() for epoch in range(3): model.train() for images, labels in train_loader: images, labels = images.to(device), labels.to(device) outputs = model(images) loss = criterion(outputs, labels) optimizer.zero_grad() loss.backward() optimizer.step() model.eval() correct = 0 total = 0 with torch.no_grad(): for images, labels in test_loader: images, labels = images.to(device), labels.to(device) preds = model(images).argmax(dim=1) correct += (preds == labels).sum().item() total += labels.size(0) print(f"epoch {epoch + 1}, test accuracy: {correct / total:.4f}")如果按默认超参数完整训练 3 轮,MNIST 测试集准确率通常可以达到 97% 左右。实际数值会受随机种子、数据下载状态和硬件影响,不要因为一次结果偏高或偏低就怀疑代码。训练时注意观察 loss 变化,如果 loss 一直不下降,优先检查学习率是不是设置得过大或过小。
5.3 换成简单 CNN 对照
把 MLP 替换成一个小型 CNN,观察卷积结构对图像任务的影响。
class CNN(nn.Module): def __init__(self): super().__init__() self.features = nn.Sequential( nn.Conv2d(1, 32, kernel_size=3, padding=1), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(32, 64, kernel_size=3, padding=1), nn.ReLU(), nn.MaxPool2d(2) ) self.classifier = nn.Sequential( nn.Flatten(), nn.Linear(64 * 7 * 7, 10) ) def forward(self, x): return self.classifier(self.features(x)) model = CNN().to(device)训练时只需把上一节脚本里的MLP()替换成CNN()。CNN 参数量和计算量更大,训练速度会比 MLP 慢一些,但通常准确率会更高。这一组对照实验非常值得做:它用同一个数据集、同一套训练代码,直接展示了“网络结构对效果的影响”。
6. 模型接口与批量推理
第十二讲本身不涉及部署 API,但很多读者学完神经网络后会想把训练好的模型接到自己的工具里。这里给一个通用的模型导出和推理服务示例,方便理解“训练”和“推理”之间的衔接。学习阶段不需要立刻做,但提前知道如何把模型变成可调用接口会有帮助。
训练完成后,可以用下面的方式保存模型权重:
torch.save(model.state_dict(), "mnist_mlp.pth")后面写一个 FastAPI 推理服务,对外暴露 HTTP 接口。注意这是一个通用模板,实际部署时要把模型类放在同一文件,并确保输入数据格式正确。
from fastapi import FastAPI from pydantic import BaseModel import torch import torch.nn as nn import uvicorn class MLP(nn.Module): def __init__(self, input_dim=784, hidden_dim=128, num_classes=10): super().__init__() self.net = nn.Sequential( nn.Flatten(), nn.Linear(input_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, num_classes) ) def forward(self, x): return self.net(x) app = FastAPI() class Item(BaseModel): data: list # 28x28 像素值列表,长度 784 model = MLP() model.load_state_dict(torch.load("mnist_mlp.pth", map_location="cpu")) model.eval() @app.post("/predict") def predict(item: Item): x = torch.tensor(item.data, dtype=torch.float32).reshape(1, 784) with torch.no_grad(): pred = model(x).argmax(dim=1).item() return {"pred": pred} if __name__ == "__main__": uvicorn.run(app, host="127.0.0.1", port=8000)启动服务后,可以用 curl 测试:
curl -X POST http://127.0.0.1:8000/predict \ -H "Content-Type: application/json" \ -d '{"data": [0.0, 0.1, ...]}'批量推理更简单的做法是用DataLoader读取一个目录下的图片,循环调用模型。实际工程里建议加日志、异常捕获和失败重试机制,防止某一张图片格式异常导致整个批量任务中断。如果要在生产环境部署,还建议用 ONNX Runtime 或 TensorRT 做加速,但学习阶段不需要过早投入这些。
7. 训练资源占用与性能观察
跑神经网络训练时,多观察资源占用能帮你判断模型是否适合当前硬件。对于 MNIST 这种小任务,CPU 训练已经足够,但如果你想在更大数据集上做实验,就需要关注 GPU 显存和内存占用。
观察显存占用可以用命令:
nvidia-smi -l 1如果 PyTorch 在 GPU 上运行,还可以在代码里查看当前显存占用:
if torch.cuda.is_available(): print(torch.cuda.memory_allocated() / 1024**2, "MB")资源占用和训练速度受几个因素影响。batch_size越大,单次前向和反向传播处理的样本越多,显存占用越高;图片分辨率越高,特征图尺寸越大,显存开销越大;模型网络层越深、通道数越多,参数量和计算量越大。训练轮数epoch本身不影响单步显存占用,但影响总训练时间。
如果显存不足,最简单的办法是调小batch_size,比如从 64 改成 32 或 16。还可以降低输入分辨率,但这可能会影响准确率。更进阶的做法是使用混合精度训练,PyTorch 的torch.amp可以在支持相关硬件时降低显存占用并加速训练,但学习阶段不必急着用。
CPU 训练时,瓶颈通常在计算而不是显存。观察 CPU 占用可以用任务管理器或top命令。如果分析后发现训练太慢,优先考虑减小网络规模或换用更小的数据集做验证,而不是直接把学习率调大。
8. 常见问题与排查方法
学习这一讲时,最容易遇到的问题集中在环境、数据和训练效果三方面。下面是一张常见问题排查表。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 安装 PyTorch 失败 | 网络原因或源不稳定 | 查看 pip 报错信息 | 使用国内镜像源或离线安装包 |
| CUDA 不可用 | 驱动和 PyTorch 版本不匹配 | 运行torch.cuda.is_available() | 到 PyTorch 官网选择匹配 CUDA 的安装命令 |
| MNIST 数据集下载失败 | 网络无法访问数据集源 | 查看下载日志 | 手动下载数据集并放到./data对应目录 |
| loss 不下降 | 学习率不合适、数据未归一化 | 打印前几个 batch 的 loss | 调整学习率,使用 ToTensor 和 Normalize |
| 显存不足 | batch_size 过大 | 查看nvidia-smi显存占用 | 减小 batch_size 或使用 CPU 训练 |
| 测试准确率低 | 模型结构简单或训练轮数不足 | 观察训练集准确率 | 增加隐藏层宽度、训练轮数或改用 CNN |
| 模型推理时端口被占用 | 8080 或 8000 端口已有服务 | 查看端口占用进程 | 改用其他端口或关闭占用进程 |
这里想特别强调“数据归一化”这个问题。很多初学者在 MNIST 上直接输入 0 到 255 的原始像素,导致 loss 下降很慢。正确做法是先ToTensor()把像素缩放到 0 到 1,再Normalize用数据集的均值和标准差做标准化。这一步对训练稳定性的影响通常比网络结构更大。
9. 最佳实践与学习建议
学完第十二讲,不建议立刻去追最新的大模型或复杂网络结构,先把基础动作练扎实。下面几条是这套学习路线里最值得注意的实践建议。
第一,先手动推导一遍反向传播。找一个人工神经网络,输入层 2 个节点、隐藏层 3 个节点、输出层 1 个节点,自己用求导公式算一遍梯度,然后和 PyTorch 的backward()结果对比。做一次这样的练习,比反复看十遍课件都更有用。
第二,写实验时固定随机种子。神经网络初始化是随机的,如果不固定随机种子,每次训练出来的准确率会有波动,难以判断某个改动是否真的有效。在代码开头加一行:
torch.manual_seed(42)如果有 GPU,还需要加:
torch.cuda.manual_seed_all(42)第三,建立清晰的实验目录结构。建议把数据、模型、Notebook、日志分开,避免所有东西堆在一个文件夹里。下面是推荐结构:
cv_learning/ ├── data/ # 数据集 ├── models/ # 模型权重 ├── notebooks/ # Jupyter Notebook ├── scripts/ # 训练脚本 ├── logs/ # 训练日志 └── outputs/ # 输出结果第四,不要只看准确率这一个指标。训练时记录 loss 的变化,观察训练集和测试集准确率的差距。如果训练集准确率高、测试集准确率低,说明模型过拟合;如果两个都低,说明模型容量或训练方式有问题。第十二讲学完你可以先不做复杂实验,但一定要养成记录训练过程的习惯。
第五,注意版权和数据合规。课程资源、公开数据集、开源代码都有各自的许可协议。学习用途一般没有问题,但如果有一天你要把实验成果发布成博客、论文或商业产品,务必备注来源并确认是否满足许可要求。涉及人脸、声音、私人图片的数据,必须先获得明确授权再使用。
10. 总结与下一步
这一讲最值得掌握的是“手工设计特征到数据驱动特征学习”的转变,以及前馈神经网络的基本训练框架。具体来说,就是神经元和感知机的形式、激活函数的作用、损失函数和梯度下降的关系、反向传播如何把梯度逐层传回去。这套框架不只在第十二讲有用,后面学卷积神经网络、循环神经网络、Transformer,本质上都还在这个框架里。
最容易踩的坑有两个。一个是只跑通代码但没推导公式,导致后续调参时没有方向感;另一个是没有控制超参数和随机种子,实验结果不可复现,很难判断某个改动到底有没有效果。建议学完这一讲后,先把 MLP 和 CNN 在 MNIST 上的对照实验做完整,再进入卷积神经网络的系统学习。
下一步可以看两类内容:一类是卷积神经网络的详细原理,包括卷积层、池化层、感受野和经典结构;另一类是更实用的训练技巧,比如数据增强、正则化、学习率调度、优化器选择。手边可以准备 CIFAR-10 数据集,它的图像尺寸比 MNIST 大,类别也更多,用来验证“模型结构对视觉任务的影响”会更明显。基础打牢之后,再去看目标检测、图像分割、视觉生成等方向,就不容易觉得全是黑盒了。