PyTorch 入门实战:从爬虫数据到模型上线的完整闭环
上周五晚上十点半,我还在百度大厦 B 座调一个搜索排序模型。产品经理突然在钉钉上@我:“能不能用 AI 判断这个网站是不是区块链垃圾站?” 我差点一口老血喷在键盘上——我们是做通用搜索的,不是风控团队啊!但转念一想,这不正好是个练手 PyTorch 的好机会?毕竟在公司三年多,天天和 TF1.x 打交道(别问,问就是历史包袱),最近跳槽面试又总被问 PyTorch,再不学真要被淘汰了。
于是,我决定用周末两天时间,从零搭建一个轻量级深度学习 pipeline:先写个爬虫抓点网页,然后用模型判断是否为“区块链营销号”,最后部署成 API。听起来简单?等你踩完所有坑就知道什么叫“理想很丰满,现实很骨感”了。
起手式:为什么选 PyTorch?
说实话,作为百度老算法狗,我们内部主力框架还是 PaddlePaddle + 自研系统。但外面的世界早已是 PyTorch 的天下。GitHub 上开源项目 80% 都用它,HuggingFace 模型库也优先支持 PyTorch 格式。更重要的是——调试体验真的香!
还记得去年双11前,我 debug 一个 TF2 的动态图模型,光是搞清楚 tf.function 的 tracing 机制就熬了两个通宵。而 PyTorch 的 eager mode 就像 Python 本身一样自然:print 中间变量、打断点、单步执行,毫无障碍。对于我这种喜欢抠底层原理的人,PyTorch 的源码也更“干净”——不像某些框架,一层套一层的 decorator 看得人头晕。
所以,这次项目我毫不犹豫选了 PyTorch。
数据从哪来?自己爬!
既然任务是识别区块链垃圾站,首先得有正负样本。正样本好说,随便搜“区块链投资”、“NFT财富自由”就能找到一堆;负样本就有点 tricky——不能随便拿百度首页当负例,那太 trivial 了。
我的方案是:用 Scrapy 写个微型爬虫,定向抓两类页面:
- 正例:关键词包含 “区块链”、“挖矿”、“代币”、“财富密码” 的站点
- 负例:主流新闻站(如新华网、人民网)的技术/财经频道
# spider.py 片段
class BlockchainSpider(scrapy.Spider):
name = 'blockchain_spider'
def parse(self, response):
# 提取正文文本(简单版,实际需处理 JS 渲染)
text = ' '.join(response.css('p::text').getall())
# 标签由 URL 关键词决定
label = 1 if any(kw in response.url for kw in ['blockchain', 'crypto']) else 0
yield {
'url': response.url,
'text': clean_text(text), # 去广告、去脚本
'label': label
}
💡 真实场景吐槽:你以为爬虫很简单?实际上光是反爬就折腾半天。有些网站检测 headless 浏览器,有些动态加载内容,还有些直接 IP 封禁……最后我不得不加代理池+随机 UA+延时,搞得像在干黑产。测试同学看到代码后一脸惊恐:“你这不会被法务找上门吧?”
最终,我攒了约 5000 条样本(2500 正 + 2500 负),存成 CSV。虽然少,但入门足够了。
模型设计:小而美就够了
既然是快速验证,没必要上 BERT。我选择了最经典的 TextCNN ——结构简单、训练快、效果还不差。核心思想是用不同尺寸的卷积核捕捉 n-gram 特征。
import torch
import torch.nn as nn
class TextCNN(nn.Module):
def __init__(self, vocab_size, embed_dim=128, num_classes=2, dropout=0.5):
super().__init__()
self.embedding = nn.Embedding(vocab_size, embed_dim)
# 多尺度卷积核:2-gram, 3-gram, 4-gram
self.convs = nn.ModuleList([
nn.Conv2d(1, 100, (k, embed_dim)) for k in [2, 3, 4]
])
self.dropout = nn.Dropout(dropout)
self.fc = nn.Linear(300, num_classes) # 100 * 3
def forward(self, x):
# x: [batch, seq_len]
x = self.embedding(x).unsqueeze(1) # [B, 1, L, E]
# 卷积 + ReLU + MaxPool
conv_outs = []
for conv in self.convs:
c = torch.relu(conv(x)) # [B, 100, L-k+1, 1]
c = torch.max_pool1d(c.squeeze(-1), c.size(2)).squeeze(-1) # [B, 100]
conv_outs.append(c)
x = torch.cat(conv_outs, dim=1) # [B, 300]
x = self.dropout(x)
return self.fc(x)
数据预处理:Tokenizer 是关键
PyTorch 不像 TF 有内置的高级 API,所以得自己搞 tokenization。我用了最朴素的方案:
- 构建词汇表(vocab):统计所有单词,保留 top 10000
- 文本转 ID 序列,长度统一 pad 到 200
- 用
DataLoader打包成 batch
from torch.utils.data import Dataset, DataLoader
class TextDataset(Dataset):
def __init__(self, texts, labels, word2idx, max_len=200):
self.texts = texts
self.labels = labels
self.word2idx = word2idx
self.max_len = max_len
def __getitem__(self, idx):
words = self.texts[idx].split()
ids = [self.word2idx.get(w, 0) for w in words[:self.max_len]]
ids += [0] * (self.max_len - len(ids)) # pad
return torch.tensor(ids), torch.tensor(self.labels[idx])
def __len__(self):
return len(self.texts)
🤯 踩坑记录:一开始忘了做
padding,训练时直接报错tensor size mismatch。还有一次把word2idx的 OOV 映射成-1,结果 embedding 层越界崩溃。当时真的想砸电脑——这不比 TF 的tf.keras.preprocessing麻烦多了?
训练与调优:那些年我们追过的 loss
训练脚本写起来倒是很清爽:
model = TextCNN(vocab_size=len(word2idx))
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
for epoch in range(10):
for batch_texts, batch_labels in train_loader:
optimizer.zero_grad()
outputs = model(batch_texts)
loss = criterion(outputs, batch_labels)
loss.backward()
optimizer.step()
但真正折磨人的是 调参。初始版本准确率只有 65%,完全不可用。我尝试了以下优化:
| 策略 | 准确率提升 | 说明 |
|---|---|---|
| 加 Dropout | +5% | 过拟合严重,train loss 很低但 val loss 高 |
| 调整学习率 (1e-3 → 5e-4) | +3% | 原先 learning rate 太大,loss 震荡 |
| 用预训练词向量 (Word2Vec) | +7% | 从头训 embedding 效果差 |
| 增加负样本多样性 | +4% | 原先负样本太“干净”,模型学偏了 |
最终准确率达到 89%,AUC 0.93。虽然比不上大模型,但对一个 weekend project 来说,够用了。
和“区块链”的意外联动
说到这里你可能疑惑:关键词“区块链”到底在哪体现?其实整个项目的业务目标就是识别打着“区块链”旗号的垃圾网站。有趣的是,在分析错误样本时,我发现:
- 很多正例其实是正规区块链媒体(如 CoinDesk),但因为用词浮夸被误判
- 一些负例是科技博客讨论 Web3,也被当成垃圾
这说明:单纯看关键词不行,必须结合上下文语义——而这正是深度学习的优势。传统规则引擎可能写一堆正则匹配“区块链.*财富”,但模型能学到“区块链技术赋能实体经济”和“区块链带你一夜暴富”的语义差异。
🧠 算法思考:在搜索场景中,我们也面临类似问题。比如用户搜“比特币”,到底是想了解技术,还是想找交易平台?我们的排序模型就得区分 intent。这次小实验让我更理解了语义建模的价值。
部署上线:别让模型烂在本地
模型训好了,总不能只在 Jupyter Notebook 里跑吧?我用 Flask 包了个简易 API:
from flask import Flask, request, jsonify
import torch
app = Flask(__name__)
model = torch.load('textcnn_model.pth')
model.eval()
@app.route('/predict', methods=['POST'])
def predict():
data = request.json
text = data['text']
# 预处理 + 推理(省略细节)
with torch.no_grad():
logits = model(input_ids)
prob = torch.softmax(logits, dim=-1)[0][1].item()
return jsonify({'is_blockchain_spam': prob > 0.7, 'score': prob})
启动服务后,用 curl 测试:
curl -X POST http://localhost:5000/predict \
-H "Content-Type: application/json" \
-d '{"text": "加入我们的区块链挖矿群,日赚万元不是梦!"}'
返回:
{"is_blockchain_spam": true, "score": 0.92}
搞定!虽然离生产级还差十万八千里(没做 batching、没加监控、没压测),但至少证明了端到端流程可行。
给 fellow 算法工程师的建议
作为一个在大厂待了三年多、正考虑换环境的老兵,我真心觉得:掌握 PyTorch 已经不是“加分项”,而是“生存技能”。
- 如果你在做 NLP/CV,90% 的 SOTA 模型都首发 PyTorch
- 如果你想读论文复现,PyTorch 社区资源丰富到爆炸
- 如果你准备跳槽,面试官张口就是 “讲讲你用 PyTorch 解决过什么问题”
这次小项目虽然粗糙,但它帮我打通了 数据 → 模型 → 部署 的全链路。更重要的是,我终于敢在简历上写“熟练使用 PyTorch”了(之前一直心虚)。
最后一点碎碎念
写这篇文章时,我又想起那个周五晚上的钉钉消息。其实产品经理后来也没真用这个模型——他们改需求了,现在要识别“AI 生成内容”。但没关系,技术人的快乐,往往就藏在这些“无用但有趣”的折腾里。
如果你也在大厂被 legacy code 折磨,不妨找个周末,用 PyTorch 做点小东西。不用多 fancy,哪怕只是分类垃圾邮件,也能让你找回 coding 的初心。
毕竟,算法工程师的核心竞争力,从来不是会调哪个框架,而是解决问题的思维和动手能力。
共勉。

评论 0