☰
PyTorch机器学习基础:从数据流程到模型训练全解析
2026/9/30 8:52:24 网站建设 项目流程

1. 机器学习的基本流程:先看全貌,再动手编码

很多人一上来就装 PyTorch、跑 MNIST,结果代码写得飞起,但对“机器学习到底在干嘛”却是一笔糊涂账。我在带新手时最常说的一句话:先把流程刻在脑子里,再谈框架和代码。这一章的主题虽然是 PyTorch 里的机器学习基础,但核心并不是某个 API 怎么调,而是帮你把“数据 -> 模型 -> 损失 -> 优化 -> 评估”这条主线焊死,后面所有实战都不会偏。

1.1 机器学习到底解决什么问题

机器学习的本质,是从数据中自动总结规律,然后用规律去预测新样本。拿我几年前做过的房价预测举例:给出一堆历史房屋数据(面积、楼层、位置、房龄),模型学到“面积越大价格越高、市中心溢价严重”这类关系后,再给一套新房子信息,它就能估出价格。这个过程没有人为写死规则,全是数据驱动的。

所以你会看到,机器学习解决的问题大致分三类:回归(预测连续数值,比如房价)、分类(预测离散类别,比如邮件是否垃圾邮件)、聚类(把相似样本自动分组,比如用户分群)。前两类属于监督学习,得有带标签的数据;聚类属于无监督学习,只有特征没有标签。除此之外还有强化学习,那个是智能体与环境交互的范式,在第三章通常只做概念了解,重点还是监督学习。

1.2 常见任务类型:监督、无监督、强化学习

做个表直观点:

类型是否有标签典型任务常见算法
监督学习有分类、回归线性回归、逻辑回归、决策树、SVM、神经网络
无监督学习无聚类、降维K-Means、DBSCAN、PCA
强化学习有延迟奖励决策、控制Q-Learning、DQN、PPO

这里有个新手容易懵的地方:逻辑回归明明做分类,为什么名字叫回归?因为它的历史渊源来自统计学,本质是对线性回归的输出做了个 Sigmoid 变换,把结果压到 0~1 之间,再当概率用。理解这点后,你再看 PyTorch 里的nn.Linear、nn.Sigmoid组合,就不会觉得乱。

1.3 机器学习项目的一般流程

一个标准项目基本走这六步:

  1. 明确问题:回归还是分类?业务指标是什么?
  2. 采集与清洗数据:缺值、异常值、重复值处理。
  3. 特征工程与预处理:标准化、编码、特征选择。
  4. 训练模型:选算法、调超参。
  5. 评估模型:在没见过的数据上看效果。
  6. 部署与监控:上线后持续跟踪表现。

很多教程只讲第 4 步,导致新手以为机器学习就是“写个模型然后 fit”。实际上,真实项目中 70% 的时间都花在数据清洗和特征工程上,建模反而是最机械的一步。记住这句话:数据决定上限,模型只是逼近上限。这一章后面给的所有代码示例,其实都嵌在这个大流程里。

2. 数据与特征:机器学习的地基

2.1 数据集的划分:训练/验证/测试

先讲一个所有入门者都容易踩的坑:把用于训练的数据拿去评估模型,得到的结果一定虚高。就好比你考试前把答案背完了,模拟考当然满分,但一到高考就露馅。机器学习里把数据集的划分想成“练习册”、“模拟卷”、“高考卷”就很好懂了。

通常会把数据分成三份:

  • 训练集(约 60%~80%):核心学习材料,模型看着它调整参数。
  • 验证集(约 10%~20%):用来调超参数,比如学习率、网络层数,相当于模拟卷,可以多做几套。
  • 测试集(约 10%~20%):最终评估用,整个训练期间绝不碰它。

在 PyTorch 里,手动划分可以这样写:

import torch from torch.utils.data import random_split, DataLoader, TensorDataset # 假设 X 是特征张量,y 是标签张量 dataset = TensorDataset(X, y) train_len = int(0.8 * len(dataset)) val_len = (len(dataset) - train_len) // 2 test_len = len(dataset) - train_len - val_len train_ds, val_ds, test_ds = random_split( dataset, [train_len, val_len, test_len] ) train_loader = DataLoader(train_ds, batch_size=32, shuffle=True) val_loader = DataLoader(val_ds, batch_size=32) test_loader = DataLoader(test_ds, batch_size=32)

注意shuffle=True只用在训练集,验证和测试不要打乱顺序,保证批次之间没有关联。

2.2 数据处理:标准化、归一化、缺失值处理

机器学习里有个词叫“数据处理”,听起来基础,但直接决定训练能不能收敛。先看特征量纲问题:假设房价预测里,“面积”是几十平方米级别,“房龄”是几年级别,如果放任不管,模型会自动把数值大的特征当成重点,小数值特征容易被忽略。更麻烦的是,很多优化器对梯度的量级很敏感,量纲差异会导致损失震荡。

解决办法就两个:

  • 归一化(Min-Max Scaling):把所有值缩放到 [0,1] 区间。
  • 标准化(Standardization):减去均值,除以标准差,让数据变成均值为 0、方差为 1 的分布。

实际用哪个?我的经验是:如果特征分布没有强异常值,标准化优先,因为 Min-Max 受最大最小值影响太大,万一将来某条新数据的值超出原范围,归一化结果会超出 [0,1],处理起来反而麻烦。但在图像处理里,像素值本来就是 0~255,直接除以 255 归一化是最常见的。

PyTorch 里最灵活的做法是自定义一个预处理层:

import torch import torch.nn as nn class StandardScaler(nn.Module): def __init__(self, mean, std): super().__init__() self.register_buffer("mean", mean) self.register_buffer("std", std) def forward(self, x): return (x - self.mean) / self.std

register_buffer是个关键点:它会让张量跟着模型一起去.to(device),并且不会参与梯度计算。如果你直接用普通属性存均值方差,换 GPU 时会踩坑。均值方差一定要在训练集上计算,然后再应用到验证/测试集,不能全体数据一起算,否则会引入“未来信息”,评估结果失真。

缺失值处理更要小心:如果缺失比例低于 5%,可以直接删掉对应样本;如果较高,建议用均值、中位数或模型预测填充。千万不要用 0 随便填,尤其当特征本质上不可能是 0 时(比如年龄),那会让模型学到错误分布。

2.3 特征工程入门:从原始数据到可用特征

特征工程听起来高大上,其实就是把原始数据转换成模型更容易学习的形态。举个现实例子:拿到一个“注册时间”字段,模型本来不知道“2020-05-01”比“2019-05-01”意味着什么,但你把它拆成年、月、日、星期几,这就是特征加工。

常见的操作有三类:

  • 数值特征:分箱(比如把年龄分成年龄段)、取对数(处理长尾分布)。
  • 类别特征:独热编码(One-Hot)或 Embedding。独热编码在 PyTorch 里可以借助torch.nn.functional.one_hot。
  • 组合特征:比如面积和楼层单独给模型,不如再来一个“面积/总价”比例。

有一个反直觉的真相:特征工程做得好,有时比换一个复杂模型更有效。我刚入行时迷信神经网络万能,后来做工业数据发现,简单模型加靠谱特征,往往比复杂模型加脏特征效果更好。这一章不要求你把特征工程学透,但要建立这个意识——后续在 PyTorch 中写Dataset类时,预处理逻辑最好都放在__getitem__里,配合在线增强很顺手。

3. PyTorch 承载机器学习基础:张量与自动求导

3.1 张量:机器学习的通用语言

PyTorch 里最基础的对象就是张量(Tensor),你可以把它理解成支持 GPU 计算的“多维数组”。标量是 0 维张量,向量是 1 维,矩阵是 2 维,图片可以看作 3 维(通道、高、宽),视频是 4 维,Transformer 里的 batch 更是高达 5 维。

新手常问:NumPy 的 ndarray 也有多维数组,为什么要用张量?答案就两句话:第一,张量能放到 GPU 上并行计算;第二,张量能自动记录运算轨迹,用于反向传播。拿面点做类比,NumPy 是案板上的面团,你得自己揉(手动求导);PyTorch 张量是面团加了一台揉面机,你只需把配方给它,它自己会揉完送进烤箱。

创建张量最简单的方式:

import torch a = torch.tensor([1, 2, 3]) # 普通张量 b = torch.zeros(2, 3) # 全 0 c = torch.randn(2, 3) # 标准正态分布 d = torch.arange(0, 10, 2) # [0, 2, 4, 6, 8]

注意torch.tensor和torch.Tensor的细微差别:小写tensor()会复制数据并推断 dtype;大写Tensor()实际是torch.FloatTensor的别名,在新版里直接有一个torch.tensor(..., dtype=torch.float32)的推荐写法。另一点是默认整数张量是int64,做除法时容易得 0,我见过太多人栽在这个坑里:两个整数张量相除得到的结果不是浮点,要养成指定 dtype 的习惯:

x = torch.tensor([1, 2, 3], dtype=torch.float32)

3.2 自动求导:反向传播的引擎

机器学习里绕不开求导,因为梯度下降要算损失对每个参数的导数。手推一个 10 层的网络,公式能写满一页纸,还容易错。PyTorch 的autograd机制就是来解决这件事的:你只管定义前向计算,反向传播自动完成。

工作机制可以这样理解:

  1. 创建张量时把requires_grad=True打开,相当于在简历上标了“我需要被求导”。
  2. 在这个张量基础上做运算,PyTorch 会构建一个计算图,每个节点记录“从哪来、到哪里去”。
  3. 调用backward()时,梯度沿计算图反向流动,每个叶子节点(需要求导的模型参数)的.grad被自动填上。

一个完整的最小示例:

import torch x = torch.tensor(2.0, requires_grad=True) y = x ** 2 + 3 * x + 1 # 前向计算 y.backward() # 反向传播 print(x.grad) # 结果是 2x + 3 = 7

求导结果当然没问题。但这里有个需要提醒的:backward()之后,计算图默认会被释放,如果还想用同一批数据再做一次反向传播,需要把retain_graph=True传进去。模型训练时一般不这么干,只会在某些特殊结构(比如对抗网络里同一个网络多次更新)才需要。

再提醒一个细节:.grad是累加的。如果你在一个循环里连续对同一参数调backward(),梯度会把之前的累在一起。PyTorch 每步更新完参数后都要用optimizer.zero_grad()把梯度清零,否则梯度会越滚越大,损失直接飞掉。这个顺序问题后面专门说。

3.3 用 PyTorch 实现一个极简线性回归

理论知识讲再多,不如跑一次最能理解。线性回归是最简单的监督学习模型,正好用来把前面的张量、自动求导串起来。假设我们要模拟一条直线y = 2x + 1加一点噪声,然后让模型学出来。

import torch import torch.nn as nn import torch.optim as optim # 1. 生成数据 torch.manual_seed(42) x = torch.randn(100, 1) * 5 y = 2 * x + 1 + torch.randn(100, 1) * 1.5 # 2. 定义模型 model = nn.Linear(1, 1) # 一个输入特征,一个输出 # 3. 损失和优化器 criterion = nn.MSELoss() optimizer = optim.SGD(model.parameters(), lr=0.01) # 4. 训练 epochs = 500 for epoch in range(epochs): optimizer.zero_grad() pred = model(x) loss = criterion(pred, y) loss.backward() optimizer.step() if (epoch + 1) % 100 == 0: print(f"Epoch {epoch+1}, Loss={loss.item():.4f}") print("模型权重:", model.weight.item(), "偏置:", model.bias.item())

你会发现训练 500 轮后,weight非常接近 2,bias接近 1。这个过程里最核心的顺序是固定的:zero_grad -> forward -> loss -> backward -> step。这个顺序一个都不能乱。很多人容易把zero_grad放在backward后面,或者干脆忘了,最后模型不稳定,还以为是模型结构问题。

4. 损失函数与优化器:模型怎么学会

4.1 损失函数怎么选:MSE、交叉熵等

损失函数是“模型做得有多差”的量化指标。不同任务用不同损失,最常用的几个:

任务类型损失函数说明
回归Mean Squared Error预测值与真实值差的平方均值,对大误差惩罚重
回归Mean Absolute Error绝对误差均值,对异常值更鲁棒
二分类Binary Cross Entropy用于输出为概率的二分类,比如垃圾邮件判断
多分类Cross Entropy通常配合nn.LogSoftmax使用

PyTorch 里有个让人困惑的点:nn.CrossEntropyLoss()本身已经包含了LogSoftmax,所以模型最后一层不用再加Softmax。如果你自己加了,等于是做了两次 softmax,概率分布会被压得更尖,训练反而出问题。我确实见过有人因为这个原因调了好几天的损失曲线,最后发现是结构写重复了。

回归里还有个选择细节:MSE 对异常值特别敏感,因为误差被平方放大。如果你的数据里有几个离谱的噪声点,它们会把模型往自己那边拽。这种情况建议试试 MAE,或者用 Huber Loss(平滑平均绝对误差),PyTorch 里对应nn.SmoothL1Loss,它结合了 MAE 和 MSE 的优点,离得远时表现像 MAE,离得近时像 MSE。

4.2 优化器:梯度下降与它的变体

有了损失函数,就得想办法让损失变小。最朴素的思路是沿着梯度的反方向更新参数,这就是梯度下降。但原始梯度下降一次要用全量数据计算梯度,太慢;随机梯度下降(SGD)虽然一次只用一个样本,又快又省,但太噪声。实际中我们用 mini-batch 梯度下降:每次拿一小撮样本(batch)算平均梯度。

PyTorch 里优化器都在torch.optim下,最常用的有三种:

optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9) optimizer = optim.Adam(model.parameters(), lr=1e-3) optimizer = optim.AdamW(model.parameters(), lr=1e-3, weight_decay=0.01)
  • SGD + momentum:momentum 相当于惯性,沿之前方向继续冲,能抑制震荡,适合比较规则的凸问题。
  • Adam:结合了动量思想和自适应学习率,每个参数有自己的步长,上手快,是目前默认选择。
  • AdamW:修正了 Adam 的权重衰减实现方式,在 Transformer 和现代大模型里更常用。

我的建议是:最先跑通模型用 Adam,追求最终精度时可以换 AdamW 或 SGD+momentum 加学习率调度。Adam 不等于万能,它可能收敛到尖锐极小值,泛化性能有时不如 SGD,但那是进阶话题,入门不用太纠结。

4.3 训练循环的标准写法

无论模型多复杂,训练循环的基本骨架都不会变。下面是一个封装过的训练函数,注释里写清楚了每一步的职责:

def train_one_epoch(model, dataloader, criterion, optimizer, device): model.train() # 启用训练模式,影响 Dropout 和 BatchNorm total_loss = 0.0 for inputs, labels in dataloader: inputs = inputs.to(device) labels = labels.to(device) optimizer.zero_grad() outputs = model(inputs) loss = criterion(outputs, labels) loss.backward() optimizer.step() total_loss += loss.item() * inputs.size(0) return total_loss / len(dataloader.dataset)

这里的model.train()很多人不写,但非常关键。如果你的模型里有nn.Dropout或nn.BatchNorm,训练模式和数据评估模式的行为完全不同。验证和测试时必须调用model.eval(),并且用torch.no_grad()包裹推理过程,否则会白占显存,还会因为 Dropout 导致预测结果随机波动。

我自己在实际项目中,还会额外加一个步骤——给每个 epoch 输出训练损失和验证损失,方便实时观察:

for epoch in range(epochs): train_loss = train_one_epoch(...) val_loss = evaluate(model, val_loader, criterion, device) print(f"Epoch {epoch+1:03d} | Train Loss {train_loss:.4f} | Val Loss {val_loss:.4f}")

损失曲线是训练过程的“心电图”,比起只看准确率,我强烈建议新手先学会看损失曲线的形状。正常情况是训练和验证损失都缓慢下降;如果训练损失下降但验证损失反弹,说明已经开始过拟合了,得赶紧停。

5. 模型评估与过拟合问题

5.1 评估指标:准确率、精确率、召回率、F1

分类任务不能只看准确率,尤其是类别不平衡的时候。举一个经典例子:某举报系统里,97% 的评论是正常的,3% 是垃圾评论。如果模型全部预测“正常”,准确率也有 97%,看起来挺牛,但实际上垃圾评论一条都没找出来,系统彻底失效。

所以分类评估指标要看这几位:

  • 准确率(Accuracy):预测正确的样本占比。
  • 精确率(Precision):预测为垃圾的里面,真是垃圾的比例。它关心“杀错不杀错”。
  • 召回率(Recall):真实垃圾里面,被找出来的比例。它关心“有没有漏网”。
  • F1 Score:精确率和召回率的调和平均,平衡两者。

这三者关系用一个渔网来类比:精确率是指“捞上来的都是鱼”的比例,召回率是指“整个池塘里的鱼被捞上来多少”。撒网撒得太密(精确率低但召回率高)会捞出垃圾,撒得太稀则会漏鱼。F1 就是帮你找一个平衡点。

PyTorch 本身不直接提供这些指标,可以配合 scikit-learn 来计算:

from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score # 先用模型预测,得到概率,再取 argmax 得到类别 preds = torch.argmax(model(test_x), dim=1).cpu().numpy() print("Accuracy:", accuracy_score(test_y, preds)) print("Precision:", precision_score(test_y, preds, average="binary")) print("Recall:", recall_score(test_y, preds, average="binary")) print("F1:", f1_score(test_y, preds, average="binary"))

average="binary"用于二分类,多分类时要换成"macro"或"weighted"。Macro 是各类别指标的简单平均,更关注小类;weighted 按样本量加权,更符合实际场景。这块在机器学习期末复习里经常被考,建议做几道计算题上手。

5.2 过拟合与欠拟合:现象与应对

模型学得太好,把训练集的噪声一并背了下来,就是过拟合;连训练集都学不透,损失居高不下,就是欠拟合。

用记忆来类比:过拟合是背书只记原题答案,题目换个数字就懵;欠拟合是课本压根没看懂,原题也不会做。怎么判断是哪种?画训练/验证损失曲线是最直观的办法:

  • 训练损失高,验证损失高:欠拟合。换更强的模型、加特征、减小正则化。
  • 训练损失低,验证损失高:过拟合。增加数据、加正则化、提前停止、做数据增强。

在 PyTorch 里,有个非常简单有效的处理过拟合的手段:Dropout。在nn.Linear之间插入nn.Dropout(p=0.5),训练时随机让一半神经元失活,迫使模型学到更鲁棒的特征。不过要注意:model.eval()时 Dropout 自动关闭,不用你手动操作,前提是你正确调用了eval()。

5.3 正则化与早停:小技巧

正则化的核心思路是限制模型的复杂度,让它不要“太自由”。最常用的有 L1 和 L2 正则。L2 正则也叫权重衰减(Weight Decay),它给损失函数加上权重平方和的一项,逼着参数往小里走。PyTorch 里直接在优化器中加weight_decay参数就行:

optimizer = optim.AdamW(model.parameters(), lr=1e-3, weight_decay=1e-2)

weight_decay通常设 1e-4 到 1e-2,具体可以网格搜索。不要设得太大,否则模型会欠拟合。

早停更简单:训练过程中实时监控验证集损失,如果连续 N 个 epoch 没有下降(可以设 N=10),就停止训练,并恢复历史上验证损失最低时的模型参数。PyTorch 保存最佳模型的写法:

best_val_loss = float("inf") best_state = None for epoch in range(epochs): train_loss = train_one_epoch(...) val_loss = evaluate(...) if val_loss < best_val_loss: best_val_loss = val_loss best_state = {k: v.clone() for k, v in model.state_dict().items()} # 训练结束后加载最优参数 model.load_state_dict(best_state)

这里clone()很重要,因为state_dict()里的张量是引用,如果不克隆,后面训练继续更新,之前“最佳模型”的备份也被改了,早停就名存实亡。这个细节是我被坑过一次才记住的。

6. 常见坑与调试经验

6.1 环境安装的坑:PyTorch 版本与 CUDA

网上关于 PyTorch 安装的教程一搜一大把,但真正动手时最坑的是版本对应关系。核心原则是:PyTorch 版本、CUDA 工具包版本、显卡驱动版本三者要匹配。不是你装个最新版就万事大吉,也不是有 N 卡就代表能用 GPU。

简单说下判断方法:

# 查看 CUDA 是否可用 python -c "import torch; print(torch.cuda.is_available())" # 查看 PyTorch 对应的 CUDA 版本 python -c "print(torch.version.cuda)"

如果is_available()是 False,先别急着重装系统,检查一下驱动是不是太旧。在 Windows 或 Linux 上,建议直接用 conda 创建独立环境,避免把系统 Python 搞乱:

conda create -n pytorch python=3.10 conda activate pytorch pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118

--index-url选cu118、cu121这些具体 CUDA 版本标识,比默认pip install torch更容易匹配。另外,如果你的显卡比较新,比如最新的 RTX 40 系或 AMD RX 7000 系,直接无脑装带 CUDA 的版本反而可能得到 CPU 版本。装完记得跑一个实际操作:

a = torch.randn(10000, 10000, device="cuda") print(a.sum().item())

能跑出结果说明 GPU 环境没问题。我见过不少人在这一步卡住,问题往往不是代码,而是没有让 PyTorch 使用 GPU:模型和输入数据都必须执行.to("cuda"),否则 CPU/GPU 数据不在同一设备会直接报错。

6.2 数据处理相关坑

第一个坑:DataLoader返回的数据类型和你想的不一样。比如TensorDataset里的标签是long类型,但nn.CrossEntropyLoss要求标签必须是torch.long,如果你用 float 就会报错。这个初期很容易遇到,解决办法就是统一在预处理时指定 dtype。

第二个坑:batch 大小和数据维度的关系。很多新人对batch_size=32怎么影响张量形状没概念。假设一个样本是(3, 224, 224)的图片(通道 3、高 224、宽 224),一个 batch 就是(32, 3, 224, 224),通常记为(N, C, H, W)。一旦你的输入形状写错了,常见报错是矩阵乘法维度不匹配。调试时第一时间打印inputs.shape和labels.shape,别猜。

第三个坑:做标准化时统计量用错了。前面讲过必须在训练集上算均值方差,但新手常常把整个数据一起算。这会带来一个隐蔽的问题:验证集的一部分信息(均值/方差)已经泄漏进训练过程中,导致评估结果偏高。我自己做竞赛时吃过这个亏,后来给团队定了个规矩:凡是数据处理相关的统计量,都必须只在训练部分计算。

6.3 调试技巧:从损失曲线到梯度检查

调试深度学习模型,有一套固定顺序,按这个顺序做能省下大量时间:

  1. 检查损失是否下降:如果损失不降,先看数据预处理、模型结构、优化器设置。
  2. 打印梯度范数:多个参数梯度的平方和开根号,也就是grad_norm。如果梯度为 0,说明网络可能死了,比如 ReLU 把大量神经元置零;如果梯度爆炸,大概率是学习率太大或损失函数不稳定。
  3. 试试先拟合一个小数据:比如只用 5 个样本训练,看模型能不能记住。如果损失能降到接近 0,说明模型有足够表达能力,问题出在数据或训练设置上;如果连小样本都拟合不动,那肯定是 bug。

梯度范数打印可以这样加:

total_norm = 0.0 for p in model.parameters(): if p.grad is not None: param_norm = p.grad.data.norm(2) total_norm += param_norm.item() ** 2 total_norm = total_norm ** 0.5 print(f"Grad norm: {total_norm:.4f}")

如果发现梯度爆炸,直接调小学习率,或者对梯度做裁剪torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0),这个操作在训练 RNN 时尤其常用。

另外还有个很实用的调试技巧:在训练循环里设一个断点,拿一个固定 batch 反复跑,观察 loss 是否稳定下降。不同的 batch 抽到不同数据,loss 有波动正常,但趋势应该是下降的。如果固定 batch 都不下降,那就是代码本身有问题。这个方法能帮你把“数据问题”和“代码问题”快速分开。

最后再分享一个经验:机器学习基础阶段,不要急着上大模型、调超参,而要先能稳住一个最简单的模型。哪怕是线性模型,只要能跑通完整流程,理解了每一行代码在干什么,后面换成卷积、Transformer 都是水到渠成的事。我在带新人时,第一条要求就是“把线性回归的每一步讲到别人能听懂”,达到这个标准,基础才算真正打牢。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询