PyTorch 基础与 NLP 适配:构建第一个文本分类器

📂 所属阶段:第二阶段 — 深度学习与序列模型(进阶篇)
🔗 相关章节:词向量空间 · 循环神经网络 (RNN)


你好呀,我是道满!🤖 上一篇我们聊了词向量的概念,但光有理论可不够——今天就直接把 PyTorch 这个深度学习界的“瑞士军刀”拿上手,从 Tensor 基础、自动求导,一步步走到能用的中文/英文文本分类器

(悄悄说:2026 年生产环境确实直接用 Hugging Face 但学基础是为了“造轮子的底气”,这点必须补!)


1. PyTorch 核心:把 NumPy 升级成“深度学习专用库”

如果你会 NumPy,那 PyTorch 上手简直是光速——Tensor 就是 PyTorch 版的 ndarray,但它多了两个超能力:

  1. 放在 GPU/TPU 上跑,速度提升几十上百倍;
  2. 自带「自动求导」机制,不用手动算梯度了!

1.1 一分钟学会 Tensor 创建

先来看看最常用的几种创建方式,全是代码实战👇

import torch
import numpy as np

# 1️⃣ 从 Python 原生列表转(最常用)
# 注意:深度学习多用 float32/float64,分类标签用 long
text_emb_sample = torch.tensor([[0.1, 0.2, 0.3], [0.4, 0.5, 0.6]], dtype=torch.float32)
print(f"列表转Tensor:\n{text_emb_sample}\n形状:{text_emb_sample.shape}\n")

# 2️⃣ 从 NumPy 数组无缝转换
# 注意:两者共享内存(修改其中一个另一个变),用 clone() 可以独立
np_emb = np.array([[1, 2], [3, 4]])
torch_emb = torch.from_numpy(np_emb).float()  # 转float32
print(f"NumPy转Tensor:\n{torch_emb}\n")

# 3️⃣ 预定义占位Tensor(初始化模型权重常用)
zero_emb = torch.zeros(2, 3)  # 全0
randn_emb = torch.randn(2, 3) # 标准正态分布(均值0方差1)
range_ids = torch.arange(0, 10, 2) # 分词ID常用:[0,2,4,6,8]

1.2 Tensor 操作:和 NumPy 90% 一致,加了 NLP 友好的功能

NLP 里最常用的操作是「展平」「转置」「平均池化」「维度交换」,先看核心👇

# 假设我们有一个 NLP 常用的张量:(batch_size=32, seq_len=10, embed_dim=768)
x = torch.randn(32, 10, 768)

# 1️⃣ 形状变换(view/reshape 差不多,view 要求内存连续)
flattened = x.view(32, -1)  # -1 自动计算剩下的维度 → (32, 7680)
print(f"展平后形状:{flattened.shape}\n")

# 2️⃣ 转置/维度交换(NLP 常用 Transpose 和 Permute)
batch_seq_swapped = x.transpose(0, 1)  # 交换前两个维度 → (10, 32, 768)
permuted = x.permute(2, 0, 1)           # 完全重排 → (768, 32, 10)
print(f"交换维度后形状:{batch_seq_swapped.shape}\n")

# 3️⃣ NLP 友好的聚合操作(把句子的多个词向量聚合成一个)
sentence_emb = x.mean(dim=1)  # 沿着 seq_len 维度取平均 → (32, 768)
print(f"句子聚合后形状:{sentence_emb.shape}")

2. PyTorch 杀手锏:自动求导机制(autograd)

以前写神经网络要手动推链式法则、算每个权重的梯度——想想 CNN 或者 Transformer,那代码得写多少页?🤯 现在 PyTorch 的 autograd 帮我们全包了!

2.1 10行代码看明白 autograd

我们来举个简单的例子:求 y = x² + 2x + 1x=[2,3] 处的导数👇

# 1️⃣ 创建需要跟踪梯度的张量(requires_grad=True)
x = torch.tensor([2.0, 3.0], requires_grad=True)

# 2️⃣ 前向传播:计算 y(PyTorch 会自动记录计算图)
y = x ** 2 + 2 * x + 1  # 对每个元素操作
loss = y.sum()  # 必须转成标量(单个数值)才能反向传播

# 3️⃣ 反向传播:自动计算每个 requires_grad=True 的张量的梯度
loss.backward()

# 4️⃣ 查看 x 的梯度(dy/dx = 2x + 2,代入 x=2→6,x=3→8)
print(f"x 的梯度:{x.grad}")  # tensor([6., 8.])

2.2 用 nn.Module 封装一个最简单的分类器

有了 autograd,我们可以用 PyTorch 官方的 torch.nn 模块直接搭网络,不用自己写矩阵乘法、激活函数这些底层代码了!

import torch.nn as nn

class SimpleMLP(nn.Module):
    def __init__(self, input_size=768, hidden_size=128, num_classes=2):
        super().__init__()  # 必须调用父类的 __init__
        # 定义层:全连接层(Linear)→ ReLU激活 → 全连接层
        self.layers = nn.Sequential(
            nn.Linear(input_size, hidden_size),
            nn.ReLU(),
            nn.Linear(hidden_size, num_classes)
        )

    def forward(self, x):
        # 定义前向传播(autograd 会自动生成反向传播!)
        return self.layers(x)

# 测试模型
model = SimpleMLP()
test_input = torch.randn(32, 768)  # 32个样本,每个768维
test_output = model(test_input)
print(f"模型输出形状:{test_output.shape}")  # (32, 2) → 32个样本,2个分类的 logits

3. 实战开始!从零构建文本分类器

好了,基础打牢了——现在我们来做一个二分类文本分类器(比如判断评论是好评还是差评),分成「数据准备」「模型搭建」「训练循环」三步。

3.1 数据准备:用 Dataset + DataLoader 处理文本

PyTorch 处理数据的标准流程是:

  1. 定义 Dataset 子类:负责读单个样本、分词、转 ID、补全长度
  2. DataLoader 打包成 batch:自动打乱、加速数据加载
import torch
from torch.utils.data import Dataset, DataLoader
from sklearn.model_selection import train_test_split

# --------------------------
# 假设我们有自己的文本数据(这里先模拟)
# --------------------------
texts = [
    "这部电影太好看了!演员演技在线", "剧情太拖沓,完全看不下去",
    "推荐给所有喜欢科幻片的朋友", "导演拍的什么东西,浪费时间",
    # ... 多补点数据,这里简化写
] * 25  # 凑100条
labels = [1, 0, 1, 0] * 25  # 1好评 0差评

# --------------------------
# 1️⃣ 定义一个简单的“词表+分词器”(生产环境用jieba+预训练词表或BERT tokenizer)
# --------------------------
class SimpleTokenizer:
    def __init__(self, vocab_size=10000):
        self.vocab = {"<PAD>": 0, "<UNK>": 1}  # 0补全 1未知词
        self.vocab_size = vocab_size

    def fit(self, texts):
        # 统计词频、构建词表(简化:按空格/单字分,这里用中文单字吧)
        word_counts = {}
        for text in texts:
            for char in text:
                word_counts[char] = word_counts.get(char, 0) + 1
        # 取前 vocab_size-2 个高频词
        sorted_words = sorted(word_counts.items(), key=lambda x: -x[1])[:self.vocab_size-2]
        for word, _ in sorted_words:
            self.vocab[word] = len(self.vocab)

    def tokenize(self, text):
        return [char for char in text]

    def convert_tokens_to_ids(self, tokens):
        return [self.vocab.get(token, 1) for token in tokens]

# --------------------------
# 2️⃣ 定义 Dataset
# --------------------------
class ReviewDataset(Dataset):
    def __init__(self, texts, labels, tokenizer, max_len=32):
        self.texts = texts
        self.labels = labels
        self.tokenizer = tokenizer
        self.max_len = max_len

    def __len__(self):
        return len(self.texts)

    def __getitem__(self, idx):
        # 读单个样本
        text = self.texts[idx]
        label = self.labels[idx]
        # 分词、转ID、补全
        tokens = self.tokenizer.tokenize(text)[:self.max_len]
        token_ids = self.tokenizer.convert_tokens_to_ids(tokens)
        padding_len = self.max_len - len(token_ids)
        token_ids += [0] * padding_len
        # 返回字典(方便后续处理)
        return {
            "input_ids": torch.tensor(token_ids, dtype=torch.long),
            "label": torch.tensor(label, dtype=torch.long)
        }

# --------------------------
# 3️⃣ 准备数据
# --------------------------
# 训练集/验证集划分
X_train, X_val, y_train, y_val = train_test_split(texts, labels, test_size=0.2, random_state=42)
# 训练分词器
tokenizer = SimpleTokenizer(vocab_size=1000)
tokenizer.fit(X_train)
# 打包 Dataset + DataLoader
train_dataset = ReviewDataset(X_train, y_train, tokenizer, max_len=32)
train_loader = DataLoader(train_dataset, batch_size=16, shuffle=True)
val_dataset = ReviewDataset(X_val, y_val, tokenizer, max_len=32)
val_loader = DataLoader(val_dataset, batch_size=16, shuffle=False)

3.2 模型搭建:加 Embedding 层的文本分类器

刚才的 SimpleMLP 没有处理“词→向量”的步骤——NLP 里必须加 nn.Embedding 层!

class ReviewClassifier(nn.Module):
    def __init__(self, vocab_size, embed_dim=64, hidden_dim=32, num_classes=2, padding_idx=0):
        super().__init__()
        # 词嵌入层:把 vocab_size 个词转成 embed_dim 维的向量
        self.embedding = nn.Embedding(vocab_size, embed_dim, padding_idx=padding_idx)
        # Dropout 层:防止过拟合(随机丢弃50%的神经元)
        self.dropout = nn.Dropout(0.5)
        # 全连接层
        self.fc = nn.Sequential(
            nn.Linear(embed_dim, hidden_dim),
            nn.ReLU(),
            self.dropout,
            nn.Linear(hidden_dim, num_classes)
        )

    def forward(self, input_ids):
        # input_ids: (batch_size, max_len)
        embedded = self.embedding(input_ids)  # (batch_size, max_len, embed_dim)
        # 平均池化:把句子的所有词向量聚合成一个(最最简单的聚合方式)
        pooled = embedded.mean(dim=1)  # (batch_size, embed_dim)
        # 全连接层分类
        logits = self.fc(pooled)  # (batch_size, num_classes)
        return logits

3.3 训练循环:核心的“五步走”

不管是简单的文本分类器还是复杂的 Transformer,训练循环的结构永远是这五步👇

import torch.optim as optim

# --------------------------
# 初始化模型、损失函数、优化器
# --------------------------
model = ReviewClassifier(vocab_size=len(tokenizer.vocab), embed_dim=64, hidden_dim=32)
criterion = nn.CrossEntropyLoss()  # 分类任务用交叉熵损失
optimizer = optim.Adam(model.parameters(), lr=1e-3)  # Adam 优化器最常用

# --------------------------
# 训练循环
# --------------------------
num_epochs = 5
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.to(device)  # 把模型放到 GPU/CPU 上

print(f"开始训练,使用设备:{device}")

for epoch in range(num_epochs):
    # --------------------------
    # 训练阶段
    # --------------------------
    model.train()  # 开启训练模式(启用 Dropout/BatchNorm)
    train_loss = 0.0
    for batch in train_loader:
        # 1️⃣ 把数据放到 GPU/CPU 上
        input_ids = batch["input_ids"].to(device)
        labels = batch["label"].to(device)
        # 2️⃣ 梯度清零(上一个 batch 的梯度不能留)
        optimizer.zero_grad()
        # 3️⃣ 前向传播
        logits = model(input_ids)
        # 4️⃣ 计算损失
        loss = criterion(logits, labels)
        # 5️⃣ 反向传播 + 更新权重
        loss.backward()
        optimizer.step()
        # 统计损失
        train_loss += loss.item() * input_ids.size(0)

    # --------------------------
    # 验证阶段
    # --------------------------
    model.eval()  # 开启评估模式(关闭 Dropout/BatchNorm)
    val_loss = 0.0
    correct = 0
    total = 0
    with torch.no_grad():  # 验证时不需要计算梯度,节省内存和时间
        for batch in val_loader:
            input_ids = batch["input_ids"].to(device)
            labels = batch["label"].to(device)
            logits = model(input_ids)
            loss = criterion(logits, labels)
            # 统计损失和准确率
            val_loss += loss.item() * input_ids.size(0)
            _, predicted = torch.max(logits.data, 1)  # 取概率最大的类别
            total += labels.size(0)
            correct += (predicted == labels).sum().item()

    # --------------------------
    # 打印结果
    # --------------------------
    avg_train_loss = train_loss / len(train_dataset)
    avg_val_loss = val_loss / len(val_dataset)
    val_acc = correct / total
    print(f"Epoch {epoch+1}/{num_epochs} | 训练损失:{avg_train_loss:.4f} | 验证损失:{avg_val_loss:.4f} | 验证准确率:{val_acc:.4f}")

4. 2026 年的实用小结

今天我们把 PyTorch 从「Tensor 基础」到「自动求导」再到「文本分类器」全走了一遍——但还是要提醒大家:

💡 2026 年生产环境最佳实践:除非你是在做底层研究或者非常小的私有数据场景,否则 直接用 Hugging Face Transformers 库!它提供了成千上万的预训练模型(比如 BERT、GPT 系列),只需要微调几行代码,效果就比今天从头训练的好10倍以上!

最后给大家整理了一份 PyTorch NLP 速查表,保存起来随时用👇

# PyTorch NLP 速查表(核心部分)
import torch
import torch.nn as nn
import torch.optim as optim

# 1️⃣ Tensor 创建(NLP 常用)
token_ids = torch.tensor([[1,2,3],[4,5,6]], dtype=torch.long)
zero_pad = torch.zeros(2, 3, dtype=torch.long)
randn_emb = torch.randn(2, 3, 64)

# 2️⃣ 模型定义(标准模板)
class MyNLPModel(nn.Module):
    def __init__(self):
        super().__init__()
        # 层定义
        self.embedding = nn.Embedding(10000, 64, padding_idx=0)
    def forward(self, x):
        # 前向传播
        return self.embedding(x).mean(dim=1)

# 3️⃣ 训练循环(五步走,永远不变)
optimizer.zero_grad()  # 梯度清零
output = model(input)   # 前向传播
loss = criterion(output, target)  # 计算损失
loss.backward()         # 反向传播
optimizer.step()        # 更新权重

🔗 扩展阅读