这是「机器学习」系列的第一篇。我们不假设你已熟悉神经网络,而是把最小的问题讲透:一个只会输出 0 和 1 的空白系统,如何一步步学会识别手写数字,并最终走向自主智能体。
必要的背景知识
主要涉及的数学学科:
分析学(Analysis)
线性代数(Linear Algebra)
近世代数(Modern Algebra)
常用的知识点:
- 多元离散微积分(Multivariable Discrete Calculus)
- 张量(Tensor)代数
- 流形假设(Manifold Hypothesis)
机器学习的本质
一般架构
一般地,我们认为机器学习遵循一个一般性的路线:
观察 → 表示 → 预测 → 决策 → 行动 → 反馈 → 更新 → 更好的观察与决策 这也是经典的“神经连接主义学派”的理论方法;与此对应的还有“神经符号主义学派”,他们认为智能来源于大规模集群化的逻辑推导,但是由于Transformer的崛起,这个流派目前已经发展停滞了。本文讨论的机器学习也是以神经连接主义为基本的讨论对象。
流形假设
流形假设(manifold hypothesis)是机器学习中一个重要的经验性假设:
真实世界的高维数据虽然看起来维度很高,但它们并不是均匀填满整个高维空间,而是集中在一个低维流形上,或分布在该流形附近。
用数学语言说,若数据位于D维环境空间中,则假设存在一个内在维度 d \ll D 的流形M,数据点x大致满足
其中f是从低维潜空间到高维数据空间的映射,ε是噪声。d称为数据的内在维度(潜在维度)。
直观理解
例如 MNIST 手写数字图像是 28\times28=784 维向量,但真正控制数字变化的因素可能只有笔画粗细、倾斜角度、位置、书写风格等少数几个变量。因此 784 维数据实际上可能集中在一个几十维甚至更低维的流形附近。人脸图像、语音、文本嵌入等也类似。
流形的一个重要性质是“局部欧氏”:虽然整体可能弯曲复杂,但每个点附近看起来像低维欧氏空间。因此可以用局部线性、切空间、图结构等工具来建模。
局限与争议
流形假设是经验假设,不是普遍定理。现实数据可能:
- 由多个流形组成,而不是单一流形;
- 内在维度并不低,或随尺度变化;
- 存在强噪声、离群点和分形结构;
- 流形高度弯曲,局部邻域选择困难;
- 深度模型未必显式恢复真实流形。
因此,流形假设很有解释力,但使用时要注意其适用范围。它更多是理解高维数据几何结构、指导降维和表示学习的一个有用框架。
流形假设认为高维数据背后存在低维结构;机器学习的目标之一,就是发现并利用这种低维流形结构。
如何构造神经网络
数学模型
在正式开始之前请确保你熟悉相关的数学背景知识——虽然用到的不多,但是不明不白地“形而上学”主义是不可取的。
一般地我们对所有AI模型有一个标准的数学模型:
抽象成一个拥有N个参数,输入一个张量x,输出张量y的函数,通过反向传播(BP)更新这个函数的N个参数以达到使其趋于满足我们的约束条件最小化的数学模型。
约束条件及参数更新
一般而言,我们称其为“损失函数(Loss Function)”,即让目标的误差(损失)最小化的函数。
通过损失函数可以计算模型每个参数的梯度,再通过梯度下降法更新参数,使参数朝让Loss缩小的方向调整——这便是反向传播算法的核心思想。
反向传播算法是机器学习的基石。
有用的视频:【官方双语】3Blue1Brown-深度学习之反向传播算法 合集
训练
假设我们有一个数据集,MNIST(美国国家标准与技术研究院改良数据集)是一个图像分类基准,包含 70,000 张 28x28 的手写数字灰度图像,分为 10 个类别,即数字 0 到 9。该数据集提供预先划分的数据集,其中包含 60,000 张训练图像和 10,000 张测试图像,长期以来一直是评估机器学习和计算机视觉算法的标准基准。训练一个手写数字识别的网络:
import torchimport torch.nn as nnimport torch.optim as optimfrom torch.utils.data import DataLoaderfrom torchvision import datasets, transformsimport matplotlib.pyplot as pltimport numpy as np# ========== 1. 配置参数 ==========BATCH_SIZE = 64EPOCHS = 5LR = 0.001DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")print(f"使用设备: {DEVICE}")# ========== 2. 数据加载与预处理 ==========transform = transforms.Compose([ transforms.ToTensor(), # 转为 [0,1] 的张量 transforms.Normalize((0.1307,), (0.3081,)) # MNIST 均值/标准差])train_dataset = datasets.MNIST( root='./data', train=True, download=True, transform=transform)test_dataset = datasets.MNIST( root='./data', train=False, download=True, transform=transform)train_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True)test_loader = DataLoader(test_dataset, batch_size=BATCH_SIZE, shuffle=False)print(f"训练集大小: {len(train_dataset)}, 测试集大小: {len(test_dataset)}")# ========== 3. 定义 CNN 网络 ==========class Net(nn.Module): def __init__(self): super(Net, self).__init__() # 卷积层 self.conv1 = nn.Conv2d(1, 32, kernel_size=3, padding=1) # 1x28x28 -> 32x28x28 self.conv2 = nn.Conv2d(32, 64, kernel_size=3, padding=1) # 32x28x28 -> 64x28x28 self.pool = nn.MaxPool2d(2, 2) # 尺寸减半 # 全连接层 self.fc1 = nn.Linear(64 * 7 * 7, 128) self.fc2 = nn.Linear(128, 10) self.dropout = nn.Dropout(0.25) def forward(self, x): x = self.pool(torch.relu(self.conv1(x))) # 32x14x14 x = self.pool(torch.relu(self.conv2(x))) # 64x7x7 x = x.view(-1, 64 * 7 * 7) # 展平 x = self.dropout(torch.relu(self.fc1(x))) x = self.fc2(x) return xmodel = Net().to(DEVICE)print(model)# ========== 4. 损失函数与优化器 ==========criterion = nn.CrossEntropyLoss()optimizer = optim.Adam(model.parameters(), lr=LR)# ========== 5. 训练函数 ==========def train(epoch): model.train() running_loss = 0.0 correct = 0 total = 0 for batch_idx, (data, target) in enumerate(train_loader): data, target = data.to(DEVICE), target.to(DEVICE) optimizer.zero_grad() output = model(data) loss = criterion(output, target) loss.backward() optimizer.step() running_loss += loss.item() pred = output.argmax(dim=1) correct += pred.eq(target).sum().item() total += target.size(0) if (batch_idx + 1) % 200 == 0: print(f"Epoch {epoch} [{batch_idx+1}/{len(train_loader)}] " f"Loss: {running_loss/200:.4f} " f"Acc: {100.*correct/total:.2f}%") running_loss = 0.0 return 100. * correct / total# ========== 6. 测试函数 ==========def test(): model.eval() test_loss = 0 correct = 0 with torch.no_grad(): for data, target in test_loader: data, target = data.to(DEVICE), target.to(DEVICE) output = model(data) test_loss += criterion(output, target).item() pred = output.argmax(dim=1) correct += pred.eq(target).sum().item() test_loss /= len(test_loader) acc = 100. * correct / len(test_dataset) print(f"\n测试集: 平均Loss: {test_loss:.4f}, 准确率: {acc:.2f}%\n") return acc# ========== 7. 开始训练 ==========train_accs, test_accs = [], []for epoch in range(1, EPOCHS + 1): train_acc = train(epoch) test_acc = test() train_accs.append(train_acc) test_accs.append(test_acc)# 保存模型torch.save(model.state_dict(), "mnist_cnn.pth")print("模型已保存到 mnist_cnn.pth")# ========== 8. 可视化 ==========def visualize_results(): fig, axes = plt.subplots(1, 2, figsize=(12, 4)) # 8.1 训练曲线 axes[0].plot(range(1, EPOCHS+1), train_accs, 'o-', label='Train') axes[0].plot(range(1, EPOCHS+1), test_accs, 's-', label='Test') axes[0].set_xlabel('Epoch') axes[0].set_ylabel('Accuracy (%)') axes[0].set_title('Training / Test Accuracy') axes[0].legend() axes[0].grid(True) plt.savefig("accuracy_curve.png", dpi=120) plt.show()visualize_results()# ========== 9. 展示预测结果 ==========def show_predictions(): model.eval() # 取一批测试数据 data, target = next(iter(test_loader)) data, target = data.to(DEVICE), target.to(DEVICE) with torch.no_grad(): output = model(data) preds = output.argmax(dim=1) # 显示前 12 张 fig, axes = plt.subplots(3, 4, figsize=(10, 8)) for i, ax in enumerate(axes.flatten()): img = data[i].cpu().squeeze() * 0.3081 + 0.1307 # 反归一化 ax.imshow(img, cmap='gray') color = 'green' if preds[i] == target[i] else 'red' ax.set_title(f"预测: {preds[i].item()} / 真值: {target[i].item()}", color=color) ax.axis('off') plt.tight_layout() plt.savefig("predictions.png", dpi=120) plt.show()show_predictions()下载好数据集放置在./data/目录下即可完成全套训练并可视化展示,这是一切人工智能学习的开端,类似于“Hello,World!”
如何构造数据集
核心,好的数据往往事半功倍,差的数据模型再好也无济于事
设计数据集的结构
根据任务的不同,数据集一般情况下有三种结构:
- 监督学习:标签-数据(模式识别)
- 自监督学习:数据-掩膜/对抗样本/其他自监督信号(自回归生成)
- 无监督学习:纯数据(一般用来数学建模)
处理数据集
划分数据集: 训练/验证/测试通常按 70%:15%:15%的比例构造,策略上:独立同分布用随机划分,类别不均衡用分层抽样,时序任务用时间划分,同一用户或设备必须整体归入一侧(分组划分)。测试集独立保存,只用于最终评估。
处理不平衡与增强: 重采样、加权损失;图像用翻转/裁剪/Mixup,文本用回译/同义替换,音频用加噪/变速,也可借助合成数据补长尾。注意增强不能改变标签语义,验证与测试集不做增强。
评估与迭代: 检查覆盖度、代表性、标签准确性、群体偏差和泄漏;若验证集分数异常高,先查泄漏而非庆祝。
小规模高质量数据跑基线 → 错误分析 → 针对性补长尾与困难样本 → 更新数据版本 → 重训 存档: 用 DVC、Git LFS 管理版本,并写一份数据卡(Datasheet),固定随机种子保证可复现。
常见坑
- 用测试集调参,评估虚高。
- 同一用户/患者/设备跨训练和测试。
- 时序任务随机划分,用未来预测过去。
- 标签定义模糊,标注者各标各的。
- 只收容易样本,忽略长尾。
- 归一化等预处理用了全量统计量,造成泄漏。
- 忽略隐私、版权和偏见。
结语
千里之行,始于足下。到这里,你已经亲手训练出了第一个真正意义上的神经网络——但它处理的图像彼此独立。当数据换成语言、代码与时间序列,「让模型学会关注重点」就成了下一步:《机器学习·注意力的时代》。
评论