机器学习与深度学习
把工程中"用数据拟合规律"的经验翻译成机器学习的语言:线性回归就是加了自动微分的最小二乘法,神经网络就是多层可学习的函数复合。
1. 线性回归:从最小二乘到梯度下降
你已经知道的
实验数据处理中你用最小二乘法拟合过直线。线性回归就是它的多维推广——只不过把手工求解换成了自动微分。
闭式解 $\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. 逻辑回归与分类
把线性输出映射到 [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 仓库
- pytorch/pytorch — 深度学习框架,从官方教程开始
- fastai/fastbook — Practical Deep Learning,含 Jupyter Notebook
- scikit-learn/scikit-learn — 经典 ML 算法库,适合实验
视频课程
- Stanford CS229: Machine Learning — Andrew Ng 经典课程
- fast.ai Practical Deep Learning — 代码先行,适合工程师
3. 神经网络深度解析
从感知机到多层网络
单层感知机只能解决线性可分问题。多层网络+非线性激活函数解决了这个问题。你理解"装配工序越多→能制造的零件越复杂",同理:层数越多→能逼近的函数越复杂。
激活函数对比
| 函数 | 特点 | 场景 |
|---|---|---|
| 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)
迁移学习
不需要从头训练。ImageNet预训练的ResNet已学会"边缘→纹理→部件→物体"的层级特征。你只需替换最后的分类层,用少量机器人数据微调——就像你不需要重新发明轴承,只需根据载荷选型。
验收实验
- PyTorch实现CNN在MNIST上 > 98%
- 使用预训练ResNet18做迁移学习微调
- 对比从头训练 vs 迁移学习在100样本下的准确率差距
5. 反向传播完整推导与代码实现
为什么必须理解反向传播
PyTorch的loss.backward()帮你自动计算了梯度。但如果你不理解它内部在做什么,当梯度消失/爆炸/NaN时你完全无法排查。就像你不需要手算有限元刚度矩阵,但你必须理解节点位移和单元应力的关系。
标量对矩阵的链式法则
其中 $\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监控,观察梯度范数和权重分布