机器学习与深度学习

把工程中"用数据拟合规律"的经验翻译成机器学习的语言:线性回归就是加了自动微分的最小二乘法,神经网络就是多层可学习的函数复合。

4-6 周
5 个章节
8 个代码示例
5 个验收实验

本阶段目录

  1. 线性回归:从最小二乘到梯度下降
  2. 逻辑回归与分类
  3. 神经网络:从线性到非线性
  4. CNN:让网络"看见"图像
  5. 训练实践:过拟合与调参

1. 线性回归:从最小二乘到梯度下降

你已经知道的

实验数据处理中你用最小二乘法拟合过直线。线性回归就是它的多维推广——只不过把手工求解换成了自动微分。

$\hat{y} = \mathbf{w}^T\mathbf{x} + b \qquad \mathcal{L} = \frac{1}{N}\sum (\hat{y}_i - y_i)^2$

闭式解 $\mathbf{w}^* = (X^TX)^{-1}X^T\mathbf{y}$,等价于你熟悉的法方程。当特征维度超过几千时,改用梯度下降。

import numpy as np
from sklearn.linear_model import LinearRegression

# 任务:从 (角度, 速度, 负载) → 预测关节力矩
angles = np.random.uniform(-np.pi/2, np.pi/2, 200)
vels   = np.random.uniform(-1, 1, 200)
loads  = np.random.uniform(0, 5, 200)
torque = 15*angles + 2.5*vels + 8*loads*np.cos(angles) + 0.5*np.random.randn(200)

X = np.column_stack([angles, vels, loads, loads*np.cos(angles)])
model = LinearRegression().fit(X, torque)
print(f"R² = {model.score(X, torque):.4f}")
🔧 工程连接:关节摩擦补偿、传感器标定、力控前馈——都可以用线性回归从实验数据学一个比理论模型更准的映射。

2. 逻辑回归与分类

$P(y=1|\mathbf{x}) = \sigma(\mathbf{w}^T\mathbf{x}+b) = \frac{1}{1+e^{-(\mathbf{w}^T\mathbf{x}+b)}}$

把线性输出映射到 [0,1] 的概率区间。决策边界 $\mathbf{w}^T\mathbf{x}+b=0$ 是一条超平面。

from sklearn.linear_model import LogisticRegression
# 6维力传感器数据 → 接触检测 (接触=1, 悬空=0)
X_force = np.random.randn(500, 6)
y = (np.linalg.norm(X_force, axis=1) > 2).astype(int)
model = LogisticRegression().fit(X_force, y)
print(f"准确率: {model.score(X_force, y):.2%}")

3. 神经网络:从线性到非线性

为什么需要

线性回归只能学平面,逻辑回归只能画直线。真实机器人系统是非线性的——关节摩擦(Stribeck)、末端力的三角函数依赖、图像特征——都需要非线性表达能力。

import torch; import torch.nn as nn
class DynamicsNet(nn.Module):
    """从关节状态预测力矩的MLP"""
    def __init__(self):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(14, 128), nn.ReLU(),  # 7角度+7速度→128
            nn.Linear(128, 256), nn.ReLU(),
            nn.Linear(256, 128), nn.ReLU(),
            nn.Linear(128, 7)   # → 预测7个关节力矩
        )
    def forward(self, x): return self.net(x)
激活函数公式特点
ReLU$\max(0,x)$最常用,计算简单,梯度不消失
Sigmoid$\frac{1}{1+e^{-x}}$输出[0,1],适合概率输出层
Tanh$\frac{e^x-e^{-x}}{e^x+e^{-x}}$输出[-1,1],适合归一化输入

4. CNN:让网络"看见"图像

全连接处理 224×224×3=150528 维输入,参数量爆炸。CNN 用共享参数的核(kernel)在图像上滑动检测特征——就像用一个特征模板扫描整张图。

class SimpleCNN(nn.Module):
    def __init__(self, n_classes=10):
        super().__init__()
        self.features = nn.Sequential(
            nn.Conv2d(3,32,3,padding=1), nn.ReLU(), nn.MaxPool2d(2),
            nn.Conv2d(32,64,3,padding=1), nn.ReLU(), nn.MaxPool2d(2),
            nn.Conv2d(64,128,3,padding=1), nn.ReLU(), nn.MaxPool2d(2),
        )
        self.head = nn.Sequential(nn.AdaptiveAvgPool2d((1,1)), nn.Flatten(), nn.Linear(128,n_classes))

核心操作:卷积(特征提取)、池化(降采样、平移不变)、步长(控制滑动密度)。

5. 训练实践:过拟合与调参

过拟合诊断

信号含义对策
训练loss↓ 验证loss↑过拟合Dropout、Weight Decay、数据增强
训练loss和验证loss都高欠拟合增大模型、减小正则化
# 训练循环模板
model.train()
for epoch in range(epochs):
    for x, y in train_loader:
        opt.zero_grad(); loss = criterion(model(x), y)
        loss.backward(); opt.step()
    # 每个epoch验证一次
    with torch.no_grad():
        val_loss = sum(criterion(model(x),y) for x,y in val_loader)
    print(f"Epoch {epoch}: train={loss:.4f} val={val_loss:.4f}")

验收实验

  • 用线性回归从关节数据预测力矩,RMSE < 5% 满量程
  • 用逻辑回归做6维力传感器接触检测,F1 > 0.95
  • 训练 CNN 在 MNIST 上达到 > 98% 准确率
  • 用 MLP 学习动力学模型,对比 RBDL 的刚体动力学计算结果
  • 写一份调参报告:对比不同优化器、学习率、正则化对训练的影响

上一阶段:← 线性代数基础  |  下一阶段:计算机视觉 →

拓展资源:机器学习

GitHub 仓库

视频课程

3. 神经网络深度解析

从感知机到多层网络

单层感知机只能解决线性可分问题。多层网络+非线性激活函数解决了这个问题。你理解"装配工序越多→能制造的零件越复杂",同理:层数越多→能逼近的函数越复杂。

$h^{(l)} = \sigma(W^{(l)}h^{(l-1)} + b^{(l)})$

激活函数对比

函数特点场景
Sigmoid输出(0,1),可解释为概率二分类输出层
ReLU$f(x)=\max(0,x)$,梯度不衰减隐藏层首选
Leaky ReLU$f(x)=\max(0.01x,x)$避免"神经元死亡"

反向传播:链式法则

你计算过装配误差传递链——每个工序的公差累加到最终产品。反向传播就是计算"损失对每个参数的梯度"的链式法则。

import torch.nn as nn

class RobotClassifier(nn.Module):
    """3层MLP: 从传感器读数分类机器人状态"""
    def __init__(self, input_dim=12, hidden=64, n_classes=5):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(input_dim, hidden),
            nn.ReLU(),
            nn.Dropout(0.2),
            nn.Linear(hidden, hidden // 2),
            nn.ReLU(),
            nn.Linear(hidden // 2, n_classes),
        )
    def forward(self, x): return self.net(x)

model = RobotClassifier()
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
for epoch in range(50):
    optimizer.zero_grad()
    outputs = model(X_train)
    loss = criterion(outputs, y_train)
    loss.backward()
    optimizer.step()

验收实验

  • 用PyTorch实现3层MLP在MNIST上达到 > 97% 准确率
  • 对比ReLU/Sigmoid/Tanh的收敛速度差异
  • 通过Dropout将过拟合gap从15%降到 < 3%

4. CNN卷积神经网络——机器人视觉基座

卷积的物理直觉

你做零件表面缺陷检测时,用一个"小窗口"在零件图上滑动检查每个局部区域——这"滑动窗口"就是卷积核。CNN先看局部再逐层抽象。

class GraspCNN(nn.Module):
    def __init__(self):
        super().__init__()
        self.features = nn.Sequential(
            nn.Conv2d(3, 32, 3, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(2),
            nn.Conv2d(32, 64, 3, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(2),
            nn.AdaptiveAvgPool2d((1, 1)),
        )
        self.classifier = nn.Linear(64, 2)
    def forward(self, x):
        x = self.features(x); x = x.flatten(1)
        return self.classifier(x)
🔧 工程连接:CNN的池化层(Pooling)本质上是降采样——类似CAD中高分辨率→低分辨率预览。池化保持重要特征同时大幅减少计算量。

迁移学习

不需要从头训练。ImageNet预训练的ResNet已学会"边缘→纹理→部件→物体"的层级特征。你只需替换最后的分类层,用少量机器人数据微调——就像你不需要重新发明轴承,只需根据载荷选型。

验收实验

  • PyTorch实现CNN在MNIST上 > 98%
  • 使用预训练ResNet18做迁移学习微调
  • 对比从头训练 vs 迁移学习在100样本下的准确率差距

5. 反向传播完整推导与代码实现

为什么必须理解反向传播

PyTorch的loss.backward()帮你自动计算了梯度。但如果你不理解它内部在做什么,当梯度消失/爆炸/NaN时你完全无法排查。就像你不需要手算有限元刚度矩阵,但你必须理解节点位移和单元应力的关系。

标量对矩阵的链式法则

$\frac{\partial L}{\partial \mathbf{W}^{(l)}} = \frac{\partial L}{\partial \mathbf{z}^{(l)}} \cdot \frac{\partial \mathbf{z}^{(l)}}{\partial \mathbf{W}^{(l)}}$

其中 $\mathbf{z}^{(l)} = \mathbf{W}^{(l)}\mathbf{a}^{(l-1)} + \mathbf{b}^{(l)}$,$\mathbf{a}^{(l)} = \sigma(\mathbf{z}^{(l)})$。loss L对权重W的梯度 = 损失对激活的梯度 × 激活对权重的梯度(就是前一层的激活值!)。

手写反向传播(不含自动微分)

import numpy as np

class ManualMLP:
    """2层MLP + 手写反向传播 — 彻底理解梯度流动"""
    def __init__(self, in_dim, hidden, out_dim, lr=0.01):
        self.W1 = np.random.randn(in_dim, hidden) * 0.01
        self.b1 = np.zeros(hidden)
        self.W2 = np.random.randn(hidden, out_dim) * 0.01
        self.b2 = np.zeros(out_dim)
        self.lr = lr

    def relu(self, x): return np.maximum(0, x)
    def relu_grad(self, x): return (x > 0).astype(float)
    def softmax(self, x):
        e = np.exp(x - np.max(x, axis=1, keepdims=True))
        return e / e.sum(axis=1, keepdims=True)

    def forward(self, X):
        self.z1 = X @ self.W1 + self.b1
        self.a1 = self.relu(self.z1)           # 隐藏层激活
        self.z2 = self.a1 @ self.W2 + self.b2
        self.probs = self.softmax(self.z2)    # 输出概率
        return self.probs

    def backward(self, X, y):
        N = X.shape[0]
        # dL/dz2 = probs - y_onehot (交叉熵+softmax的简洁梯度!)
        dz2 = self.probs - np.eye(self.W2.shape[1])[y]

        # dL/dW2 = a1.T @ dz2 / N    (线性层的梯度 = 输入转置 × 上游梯度)
        dW2 = self.a1.T @ dz2 / N
        db2 = dz2.sum(axis=0) / N

        # 梯度流过ReLU: da1 = dz2 @ W2.T, 然后乘以ReLU的导数
        da1 = dz2 @ self.W2.T
        dz1 = da1 * self.relu_grad(self.z1)

        dW1 = X.T @ dz1 / N
        db1 = dz1.sum(axis=0) / N

        # 梯度下降更新
        self.W2 -= self.lr * dW2; self.b2 -= self.lr * db2
        self.W1 -= self.lr * dW1; self.b1 -= self.lr * db1
        return -np.mean(np.log(self.probs[range(N), y] + 1e-8))

# 测试: XOR问题(线性不可分,至少需要1个隐藏层)
X = np.array([[0,0],[0,1],[1,0],[1,1]])
y = np.array([0,1,1,0])
mlp = ManualMLP(2, 8, 2, lr=0.5)
for epoch in range(1000):
    mlp.forward(X); loss = mlp.backward(X, y)
    if epoch % 100 == 0: print(f"Epoch {epoch:4d}: loss={loss:.4f}")
preds = mlp.forward(X).argmax(1)
print(f"XOR预测: {preds} (期望: [0 1 1 0])")

梯度消失与梯度爆炸

梯度消失:Sigmoid在输入绝对值大时梯度趋近于0 → 深层参数几乎不更新。解决方案:ReLU激活+He初始化+BatchNorm。梯度爆炸:RNN中梯度指数级增长 → 参数更新步长过大 → Loss NaN。解决方案:梯度裁剪(gradient clipping)。

验收实验

  • 手写2层MLP解决XOR问题(不借助PyTorch/autograd)
  • 对比ReLU vs Sigmoid:记录每层梯度的L2范数,观察深层梯度衰减
  • 实现梯度裁剪并验证能防止loss NaN

6. PyTorch训练管线完整实战

工业级训练脚本结构

不是"塞数据→调API→等结果"。一个可复现的训练脚本必须包含:数据加载器、模型、损失函数、优化器、学习率调度、验证循环、Checkpoint保存、TensorBoard记录。

import torch, torch.nn as nn, torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset
from torch.utils.tensorboard import SummaryWriter
import numpy as np

# ===== 1. 数据准备 =====
# 模拟: 1000个6轴机械臂关节角样本, 标签是末端是否到达目标区域
N = 1000; D = 6
X = np.random.randn(N, D).astype(np.float32)
y = (np.linalg.norm(X, axis=1) < 2.0).astype(np.int64)

dataset = TensorDataset(torch.from_numpy(X), torch.from_numpy(y))
train_loader = DataLoader(dataset, batch_size=32, shuffle=True)
val_loader = DataLoader(dataset, batch_size=64, shuffle=False)

# ===== 2. 模型 =====
model = nn.Sequential(
    nn.Linear(6, 64), nn.ReLU(), nn.BatchNorm1d(64),
    nn.Linear(64, 32), nn.ReLU(), nn.Dropout(0.2),
    nn.Linear(32, 2)
)
criterion = nn.CrossEntropyLoss()
optimizer = optim.AdamW(model.parameters(), lr=1e-3, weight_decay=1e-4)
scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=50)
writer = SummaryWriter('runs/robot_classifier')

# ===== 3. 训练循环 =====
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model.to(device)
best_acc = 0

for epoch in range(50):
    # 训练
    model.train()
    train_loss = 0
    for xb, yb in train_loader:
        xb, yb = xb.to(device), yb.to(device)
        optimizer.zero_grad(set_to_none=True)  # 比zero_grad()快
        loss = criterion(model(xb), yb)
        loss.backward()
        torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
        optimizer.step()
        train_loss += loss.item()
    scheduler.step()

    # 验证
    model.eval()
    correct = 0
    with torch.no_grad():
        for xb, yb in val_loader:
            xb, yb = xb.to(device), yb.to(device)
            correct += (model(xb).argmax(1) == yb).sum().item()
    acc = correct / len(dataset)

    # 记录
    writer.add_scalar('Loss/train', train_loss/len(train_loader), epoch)
    writer.add_scalar('Acc/val', acc, epoch)
    if epoch % 10 == 0:
        print(f"Epoch {epoch:2d} | LR={scheduler.get_last_lr()[0]:.2e} | Acc={acc:.3f}")

    # 保存最佳模型
    if acc > best_acc:
        best_acc = acc
        torch.save(model.state_dict(), 'best_model.pt')

writer.close()

常见训练问题排查

现象可能原因排查方法
Loss不下降学习率太小/太大lr从1e-4到1e-1对数扫描
Loss突然NaN梯度爆炸/除零加梯度裁剪+检查数据有无NaN
训练acc高验证acc低过拟合加Dropout/weight_decay/数据增强
验证acc剧烈波动batch太小增大batch_size或加BatchNorm
GPU利用率低数据加载成瓶颈增加num_workers+pin_memory

验收实验

  • 复现上述训练脚本,验证train/val曲线正常
  • 对比 Adam/AdamW/SGD+Momentum 三种优化器收敛速度
  • 添加TensorBoard监控,观察梯度范数和权重分布