这个系列写到第三篇,还能继续读下去的,大概率已经不是被“资料太少”困住的人,而是被“资料太多、不知道先动手做哪一步”卡住的人。前两篇我们把 Python 环境、项目基本流程和几个通用工具链聊得比较透了,这一篇终于要进入正题:机器学、深度学习、自然语言处理和强化学习,这四个方向到底应该怎么上手,每一步做什么、为什么这么做、踩了坑怎么爬出来。文章定位很纯粹——实践基础导航,不搞理论推导,不追求模型刷分,只解决“从零到第一个能跑通的项目”这段路。
适合谁来读?一类是刚啃完 Python 语法,想往 AI 方向走但面对一堆术语和框架不知道从哪下手的初学者;另一类是已经能跑通某个教程,但换个数据集就懵、遇到报错只会复制粘贴的实践者。我会把四个方向各自的核心链路拆开讲清楚,并且把它们放在同一套思维方式下面:先跑通最小闭环,再谈性能和调优。
1. 导航思路:先搞清四个方向的依赖关系,再定学习节奏
很多人一上来就把机器学习、深度学习、NLP、强化学习当成四门并列的课,按顺序各学一遍,结果学完机器学习觉得没什么用,学深度学习时又把前面的知识忘了,到 NLP 和强化学习时已经筋疲力尽。这个路径最大的问题不在于内容多,而在于没有理清它们之间的依赖关系。
1.1 四个方向不是并列关系,而是层层递进
我习惯把这四个方向理解成一套“地基-主体-应用-进阶”的结构。机器学习是地基,它教会你跟数据打交道的基本功:数据怎么划分、特征怎么处理、模型怎么验证、指标怎么选。深度学习是主体,它把机器学习里的“特征工程”一部分自动化了,很多机器学习的经验和教训在这里依然适用,但多了训练过程的控制问题。自然语言处理是深度学习的典型应用场景之一,它要解决的是“文本怎么变成向量、序列怎么建模”这类特殊问题。强化学习则是另一条分支,它解决的问题从“给定数据学规律”变成了“智能体在环境里试错学策略”,思维模式有很大转变。
所以学习节奏应该是:先把机器学习里那套“数据—模型—验证”的闭环跑熟,再进入深度学习掌握训练过程的控制,然后根据兴趣选择 NLP 或强化学习深入。我见过不少直接跳进深度学习、完全不懂交叉验证和过拟合的人,最后做出来的模型在训练集上表现惊艳,一到新数据就崩,而且自己完全不知道问题出在哪,就是因为跳过了地基。
1.2 以“最小闭环”倒推学习目标
不要一上来就定“我要精通 NLP”这种目标,太虚了,支撑不了行动。我建议每个方向都定一个“最小闭环”目标,这个目标必须包含三要素:能拿到数据、能跑通训练、能看到指标变化。比如机器学习的闭环是“拿一份表格数据,训练一个分类模型,算出准确率”;深度学习的闭环是“用框架搭一个三层网络,在图像数据上把 loss 降到明显低于随机水平”;NLP 的闭环是“做一次文本分类或情感判断,能看到 F1 值”;强化学习的闭环是“在经典环境里让 agent 的累计奖励曲线明显上升”。
这套目标的好处是:它逼着你在第一周就去接触真实代码、真实数据和真实报错,而不是在书本概念里打转。等你把闭环跑通,后面再怎么深入都只是往这个闭环里加细节。
1.3 节奏建议:每周一个小项目,比啃书更有效
我自己的经验是,AI 方向的学习必须项目驱动,看书和看视频只能作为辅助。更具体一点:每 7 到 10 天尽量完成一个小项目,项目之间要有明确的递进关系。比如机器学习先做“基于表格数据的分类”,再做“回归问题”,然后尝试“多分类”;深度学习先做“图像二分类”,再换一个数据集做“多分类”;NLP 先做“短文本情感判断”,再做“多标签分类”,后面可以尝试“序列标注”;强化学习先让 agent 在经典控制环境里学会保持平衡,再做需要更多步决策的任务。
当学习变成“每隔几天交付一个小模型”的节奏,你会发现自己的状态完全不一样:不会陷入理论焦虑,因为每一周都有看得见的产出;也不会盲目堆砌知识,因为每个新概念都立刻有地方安放。
2. 环境准备:虚拟环境、数据规范与第一条数据流水线
在进入四个方向之前,我还是要再啰嗦一遍环境准备。不是不信任读者的动手能力,而是我见过太多项目死在环境问题上了——装包装到一半、版本冲突、重启后环境全乱。这里的核心原则就一句话:每个项目一个独立环境,所有依赖版本固定。
2.1 环境隔离是第一优先级
强烈建议用虚拟环境工具管理 Python 依赖。轻量方案可以直接用 Python 自带的venv,做得更舒服一点可以用conda管理环境和 Python 版本。创建完环境后,第一件事是把项目依赖导出成文件,锁定版本,这能避免“在我电脑上是好的”这种尴尬局面。
python -m venv .venv source .venv/bin/activate pip install numpy pandas scikit-learn pip freeze > requirements.txt一个容易被忽略的细节:不要把requirements.txt放在随便哪个位置,建议放在项目根目录下,并且配合固定版本号。我自己的习惯是直接把pip freeze的输出原样保存,不带任何筛选,因为 AI 项目的依赖交叉复杂,少一个隐式依赖后续都可能出问题。
2.2 数据集目录与版本管理
数据集的管理比想象中重要。我在做第一个项目时把所有文件都放在同一个目录下,数据、代码、输出混在一起,到后来想复现结果,发现已经分不清哪个 CSV 是处理过的、哪个版本是最终的。后来我固定了一套目录规范:
project/ ├─ data/ │ ├─ raw/ # 原始数据,只读不改 │ ├─ processed/ # 清洗后的数据 ├─ src/ # 代码文件 ├─ models/ # 训练好的模型和 checkpoint ├─ output/ # 图表、预测结果、日志 ├─ requirements.txt这套规范最重要的价值是形成“输入—输出”的边界。raw 目录里的原始数据永远不被修改,任何清洗逻辑都生成新的文件放到 processed,这样就算清洗脚本写错了,重新跑一遍就行,不会污染原始数据。
2.3 先写一条读取数据的流水线
不管哪个方向,数据读取都是第一步。我的建议是先不要急着建模,花半天时间把你手上数据的读入、划分、抽样、简单统计全部跑通。下面是一段非常基础的模板,先让数据能稳定流入:
import pandas as pd from sklearn.model_selection import train_test_split df = pd.read_csv("data/raw/sample.csv") print(df.info()) print(df.head()) train, test = train_test_split(df, test_size=0.2, random_state=42) train.to_csv("data/processed/train.csv", index=False) test.to_csv("data/processed/test.csv", index=False)这段代码看起来简单,但里面有个非常重要的习惯:random_state固定随机种子。深度学习、机器学习项目里,你是否固定随机种子直接决定了结果能不能复现。要是每次跑出来的准确率都不一样,你根本分不清是模型问题还是随机性带来的波动。所以我建议从第一个项目开始,就养成“固定种子”这个习惯。
3. 机器学习实践:先把数据链路跑通,再讨论调参
机器学习的入门项目最大误区是:拿到数据就急着调模型参数。实际上,机器学习项目里最花时间、也最影响结果的环节,是数据清洗和特征工程,模型本身的差距很多时候反而没有想象中大。
3.1 数据清洗与特征工程为什么要花大部分时间
我做过一个模拟项目,拿到的表格数据里有缺失值、异常值、时间戳、类别文本,表面上看字段不多,实际上没有一列是能直接喂给模型的。缺失值怎么补、类别特征怎么编码、数值特征要不要标准化,这些决策直接决定了模型效果。比如某个模型的训练分数从 0.76 涨到 0.81,往往不是换了个模型,而是把特征处理的细节修好了。
特征工程的常见操作包括:连续值做标准化或归一化,类别值做独热编码或标签编码,长尾类别进行合并,时间戳拆成年月日,文本列做长度统计等。每一步操作都需要回头用验证集验证,不要想当然觉得“这个特征肯定有用”。
3.2 从 Baseline 开始:先完成,再优化
不要一上来就上集成模型或深度模型。我的建议是先用最朴素的方法跑通全流程,比如逻辑回归或者决策树,得到一个 Baseline 分数,然后在此基础上迭代。Baseline 的意义不只在于分数,更在于它帮你确认了整个代码链路是通的:数据能读入、模型能训练、预测能输出、指标能计算。
Baseline 之后,迭代顺序可以是这样:先补数据清洗(缺失值、异常值),再试简单的特征组合,然后用交叉验证观察稳定性,最后再考虑换更强的模型或做调参。如果一开始就上随机森林配网格搜索,发现分数不错,你很难知道提升到底来自哪里。
3.3 验证与评估:三个容易翻车的点
关于评估,有三个坑我必须提醒。第一是数据泄漏,清洗或特征工程时用了全局统计量,比如用全量数据的均值填充缺失值,再划分训练集和测试集,这会让测试集信息提前混进训练过程,分数虚高。解决办法是先划分、后处理。第二是类别不平衡,如果两类样本比例悬殊,准确率会骗人,这时候应该关注精确率、召回率和 F1。第三是指标选择根据任务而定,回归任务看误差,分类任务看准确率和 F1,排序任务看排序类指标,不要一把准确率用到底。
4. 深度学习实践:框架选型与训练过程掌控
进入深度学习以后,最大的变化是代码结构变得统一了:数据集类、模型类、训练循环、评估循环。只要你建过一次训练流程,后面换模型、换数据都只是往里填内容。但控制训练过程的能力,是初学者最欠缺的。
4.1 框架选型:围绕 PyTorch 搭建最小训练循环
专业上我建议从 PyTorch 入手。它的调试体验比静态图时代好太多,能用 Python 原生逻辑控制流程,非常适合学习研究和小型项目。即使未来需要部署,很多推理优化工具也支持从 PyTorch 模型转换。深度学习的核心代码结构大致如下:
import torch import torch.nn as nn import torch.optim as optim class SimpleNet(nn.Module): def __init__(self): super().__init__() self.fc = nn.Sequential( nn.Linear(128, 64), nn.ReLU(), nn.Linear(64, 10) ) def forward(self, x): return self.fc(x) model = SimpleNet() criterion = nn.CrossEntropyLoss() optimizer = optim.Adam(model.parameters(), lr=1e-3)这段代码很短,但它背后的结构几乎适用于所有监督学习任务。你要做的就是替换模型层、替换损失函数、替换数据输入。初学者最容易犯的错误是,把太多精力花在追求花哨的网络结构上,却忽略了训练过程本身。
4.2 训练循环里的五个控制点
训练循环是深度学习的“车间流水线”,要关注五个关键环节,缺一不可。
第一是损失函数的选择,分类任务用交叉熵,回归任务用均方误差,二分类问题尤其要注意输出层激活函数和损失函数的匹配。第二是优化器的选择,入门阶段 Adam 比较省心,适合大多数任务。第三是学习率,这是最有影响也最容易失控的参数,学习率太大 loss 会震荡甚至爆炸,太小则收敛极慢。第四是批量大小,它影响梯度估计的稳定性也影响显存占用。第五是模型保存策略,每个 epoch 结束应该记录 checkpoint,一般保留验证指标最好的那一个,同时存一份最近一次的,避免训练中断前功尽弃。
结合上面的五个控制点,最小训练循环一般是这样:
for epoch in range(n_epochs): model.train() for x_batch, y_batch in train_loader: optimizer.zero_grad() output = model(x_batch) loss = criterion(output, y_batch) loss.backward() optimizer.step() model.eval() with torch.no_grad(): val_loss = compute_loss(model, val_loader) print(epoch, loss.item(), val_loss.item())4.3 过拟合处理:从 Early Stopping 到数据增强
训练深度学习模型时最常遇到的现象是训练集 loss 持续下降,验证集 loss 却开始回升,这就是过拟合。解决思路有几个层级:最简单的是 Early Stopping,验证集指标连续多个 epoch 不涨就停止训练;然后是增强数据,图像任务做随机裁剪翻转,文本任务做随机 dropout,表格任务可以考虑增加样本量;再就是结构层面的手段,Dropout、正则化、BatchNorm 等。
我个人的体会是,初学者先做 Early Stopping 和更轻量的模型,比一上来就堆数据增强和正则化更能建立直觉。你首先要能观察到过拟合现象,才能理解这些手段为什么有效。
4.4 显存不足的排查顺序
显存不够是入门阶段最常见的报错之一,尤其是拿笔记本做图像训练。排查顺序应该由简到繁:先减小批量大小,确认是显存原因;再检查输入图像的尺寸和数据类型;然后考虑用梯度累积来模拟更大批量;最后才是换更小模型或混合精度训练。不要一看到报错就换服务器,很多项目用较小的 batch 完全可以完成。
5. 自然语言处理实践:从文本清洗到序列建模
NLP 在实践上跟表格数据有个很不一样的地方:文本是非结构化的,它的长度不固定、表达多样,需要先转换成模型能处理的数值形式。整个 NLP 入门链路可以浓缩成四步:清洗、分词、数值化、建模。
5.1 文本预处理:清洗、分词、截断与 Padding
文本清洗最基础的是去空行、去特殊符号、统一大小写,但要注意不要做得太过,很多信息藏在标点和大小写里。接着是分词,中文需要专门的分词工具,英文按空格拆就可以。分词之后要决定词表大小,太小的词表会丢掉大量信息,太大的词表会引入大量低频词形成噪音。
对序列模型来说,文本长度不一是个必须解决的问题,通常用截断和 Padding 把一批样本统一到相同长度。截断策略不能随便定,一般先统计文本长度分布,选取覆盖 90% 以上样本的长度作为最大长度。Padding 用占位符补到该长度,同时用注意力掩码告诉模型哪些位置是真实内容。
from transformers import AutoTokenizer checkpoint = "bert-base-chinese" tokenizer = AutoTokenizer.from_pretrained(checkpoint) encoded = tokenizer(["这个产品我很喜欢", "完全没有感觉"], padding=True, truncation=True, max_length=64, return_tensors="pt")这段代码里的两个参数很关键:padding=True把短句补到一样长,truncation=True把超长文本截断。如果你看过一些老教程,会发现它们还在用手工方式处理文本序列,新一点的库已经把这些封装好了,但底层逻辑是一样的,建议还是理解一下它的原理,否则换一个库就会手足无措。
5.2 从 Embedding 到预训练模型的路线
NLP 建模有两条路线。传统路线是把词映射成词向量,然后输入 LSTM、GRU 等循环网络,适合资源受限和快速验证的场景。更强的路线是使用预训练语言模型,比如在中文语料上预训练的 BERT 系列模型,通过迁移学习获得更强的语义表示能力,适合效果优先的场景。
不要一上手就追求预训练模型,我建议先用简单的词向量加循环网络做一个文本分类任务,跑通整个流程,体会文本数据是怎么流经模型的。当你明白了 embedding 层只是查表、循环网络是如何按时间步读取文本的,再去使用预训练模型,就能理解它内部为什么要分段、为什么要返回注意掩码。
5.3 情感分类最小项目:一次完整的流程
用一个包含若干条好评差评的中文购物评论数据集,做二分类情感判断,是最经典也最适合入门的 NLP 项目。流程不复杂:读取数据,清洗分词,构建词表,切分训练验证集,构建数据加载器,搭建一个简单的词向量加平均池化或 LSTM 模型,训练并评估 F1 值。
我自己做这个项目时踩过一个很典型的坑:分词器没有注册到数据处理流程里,训练时用了一个词表,推理时用了另一个词表,结果词索引对不上,模型输出完全失控。现在我会把分词器对象和应用逻辑做成同一个模块,并导出配置文件,确保训练和推理用的是同一套预处理流程。
6. 强化学习实践:环境交互与训练稳定性的平衡
强化学习是目前四个方向里思维模式最不一样的一个。前面三种学习方式都有现成的数据集,强化学习没有现成的“正确答案”,它靠的是智能体与环境交互,通过奖励信号调整策略。理解这一点,你学起来会顺手很多。
6.1 强化学习四件套:状态、动作、奖励、策略
入门强化学习,先记住四个概念。状态是智能体观察到的环境信息,动作是智能体可选的行为,奖励是环境给出的反馈信号,策略则是智能体根据状态选择动作的规则。目标是通过不断与环境交互,学习一个能够最大化累计奖励的策略。
这里最容易混淆的是“奖励”和“目标”。奖励是环境给的单步反馈,目标是长期累计收益的最大化。很多初学者把智能体的目标设定成“每步都拿最高奖励”,结果学到的是短视策略,在需要长远规划的任务里完全失效。理解这种单步与长期的权衡,是入门强化学习的关键一步。
6.2 环境库的版本坑与 API 差异
入门阶段常用的环境库近年 API 变化比较大,不同版本之间的接口差异让人头疼。老版本里env.step()返回四项,新版本返回五项,多了一个终止原因;环境创建的写法也调整了不少。我的建议是:锁定环境库版本,严格按照你所用版本的官方示例来写,不要混看新旧教程的代码。看到报错先检查是不是版本不匹配,再检查代码逻辑。
6.3 DQN 入门:固定经验回放与目标网络
从算法层面入门,DQN 是理解深度强化学习的一个好起点。它把深度神经网络引入到价值函数拟合中,核心创新有两点:固定经验回放和目标网络。固定经验回放是把智能体的历史交互数据存进缓冲区,每次训练随机抽取一小批,打破样本间的相关性;目标网络是单独维护一套延迟更新的网络参数,用来稳定训练目标。
# 伪代码:DQN 的关键逻辑 for step in range(total_steps): action = select_action(state, epsilon) next_state, reward, done, _ = env.step(action) replay_buffer.push(state, action, reward, next_state, done) if len(replay_buffer) > batch_size: batch = replay_buffer.sample(batch_size) target_q = reward + gamma * target_net(next_state).max(1) loss = mse_loss(q_net(state), target_q) optimizer.zero_grad() loss.backward() optimizer.step()两套网络的更新节奏需要仔细处理:训练网络每步都更新,目标网络每隔若干步从训练网络复制权重,或者用软更新缓慢逼近。初学者最常见的问题是省掉了目标网络,训练过程中 loss 曲线剧烈震荡甚至发散,还以为是学习率的问题。先加目标网络,再调整其他超参,能少走很多弯路。
6.4 训练不稳定:从奖励缩放和固定随机种子入手
强化学习训练不稳定的原因比监督学习更复杂。环境本身的随机性、策略的探索行为、奖励尺度的差异,都会导致训练曲线忽高忽低。排查时我一般按这个顺序来:先固定随机种子,确认问题不是随机波动;然后看奖励量级,奖励数值过大或过小都会让网络难以收敛,必要时对奖励做缩放;再检查探索参数,探索率过高会让好的策略被不断干扰;最后才怀疑网络结构和学习率。
实践中我发现,奖励设计的影响被很多人低估了。稀疏奖励环境训练慢,稠密奖励设计得好则学得快。如果你的任务长期没有正向奖励,可以考虑给中间步骤设计一些引导性奖励,但要注意不能改变任务的最终目标。
7. 常见问题与排查实录:我踩过的坑
这一路走来,踩过的坑比学到的算法还多。下面这些是初学者问得最多、也最容易浪费大半天时间的问题,我按自己的排查顺序整理出来。
7.1 Loss 变成 NaN
Loss 变成 NaN 的原因中,最常见的是学习率太大导致梯度爆炸,其次是数据里有异常值或缺失值没处理就进入模型,再次是损失函数与输出层不匹配。排查步骤是:先把学习率调到很小的值试跑几十步,看看 NaN 是否消失;然后检查输入数据是否有非常大的值或 NaN;最后检查模型输出层有没有除以零或者取对数取到零。如果用了混合精度训练,先关闭它再排查。
7.2 显存爆掉
显存不足报错的优先排查顺序之前说过:先调小批量,再检查数据尺寸和类型,最后尝试梯度累积。这里有一个很实际的建议:如果你的项目跑在个人电脑上,把图像尺寸、文本长度、批量大小都设得保守一些,训练速度慢一点没关系,先确认整个流程能跑通,再逐步增大规模。
7.3 版本依赖冲突
AI 领域版本更新太快,依赖冲突几乎无法避免。我现在的做法是每个项目都建独立环境,并且在项目 README 里直接写明环境创建命令和依赖安装命令。当你发现一个库升级后另一个库报错,最快的方法是回滚成之前能运行的版本组合,而不是升级所有库。
7.4 常用问题速查表
| 现象 | 优先排查项 | 再排查项 | 最后排查项 |
|---|---|---|---|
| Loss 为 NaN | 学习率过大 | 输入数据含 NaN 或异常值 | 损失函数与输出层不匹配 |
| 显存不足 | 批量大小过大 | 输入尺寸过大 | 数据加载器缓存过多 |
| 训练集分数高、验证集分数低 | 过拟合 | 数据泄漏 | 验证集划分不合理 |
| 训练 loss 不下降 | 学习率太小 | 数据预处理错误 | 梯度计算未开启 |
| 强化学习曲线发散 | 目标网络缺失 | 奖励值未缩放 | 随机种子未固定 |
| 验证集结果不可复现 | 随机种子未固定 | 数据增强顺序随机 | GPU 并行策略不一致 |
再额外提醒一个容易忽略的点:数据加载器里的shuffle参数。训练阶段需要打乱数据顺序,验证和测试阶段必须关闭 shuffle,否则每次评估结果都会不一样。这个问题非常隐蔽,因为代码不会报错,只是指标波动。
如果你对照速查表还是找不到问题,那就用最原始的办法:在关键节点打印中间结果,把数据形状、数值范围、张量是否有 NaN 一行行检查。调试深度学习模型的过程,很大程度上就是“看中间量”的过程,不要指望一步到位。
这个系列走到这里,其实已经覆盖了从环境到四个方向的完整入门链路。我个人做到现在的体会是:项目经验的增长不是线性累积,而是一关一关突破瓶颈的过程。每一个跑通的项目、每一次报错后的排查,都会沉淀成你在下一个项目里的手感。如果你现在正卡在某个报错上,不要急着搜答案,先把打印中间变量、固定随机种子、检查数据格式这三件事做完,绝大多数问题就自己浮出水面了。这一篇之后,该挑一个具体项目,让手脏起来跑了。