PyTorch初体验:三线小厂技术负责人的深度学习实战手记
上周五晚上十一点,我正用Vim改一个线上Bug——对,就是那种产品经理在群里@全体成员说“明天上线前必须修好”的紧急问题。刚搞定,钉钉又弹出一条消息:“老板想搞个AI推荐功能,下个月MVP要能跑。” 我盯着屏幕,叹了口气,默默关掉Vim,打开了PyTorch文档。
说实话,在我们这种三线城市的互联网公司,平时主要做的是前端展示、后端API和数据库CRUD。AI?那不是大厂才玩得起的玩具吗?但老板说了:“现在不搞点‘智能’,投资人看了简历都摇头。” 于是,作为团队里唯一看过《动手学深度学习》前两章的人,这活儿自然落到了我头上。
更惨的是,我还得远程办公。家里猫总在我训练模型的时候跳上键盘,上周它一屁股坐下去,直接把loss.backward()删了,我还以为是梯度消失……(别问,问就是真实经历)
为啥选PyTorch?而不是TensorFlow?
其实一开始我也纠结。毕竟TensorFlow名声在外,Google背书,工业级部署成熟。但翻了几篇社区讨论,加上自己试了两天,果断选了PyTorch。原因很简单:
- Pythonic:写起来像普通Python代码,调试方便
- 动态图:不用先定义计算图再跑,边写边试,适合我这种边学边干的野路子
- 生态活跃:HuggingFace、TorchVision这些库开箱即用,省事
而且,我们这次的需求其实不复杂:给用户推荐他们可能感兴趣的“综合服务”——比如本地家政、维修、课程等。数据量不大,也就几十万条用户行为日志,模型也不需要特别深。所以,快速验证想法比追求极致性能更重要。
从零开始:一个极简推荐模型
我们的目标很明确:输入用户历史点击过的服务(比如“空调清洗”、“儿童编程课”),输出Top5可能感兴趣的新服务。听起来像分类问题,但标签空间太大(上千种服务),直接分类效果差。于是,我决定用向量化召回 + 排序的两阶段架构。
但第一阶段,先搞个端到端的小模型跑通流程。
数据准备:别被“前端”骗了
很多人以为深度学习就是调模型,其实80%时间花在数据上。我们的原始数据来自前端埋点——用户在APP里点“综合服务”页面的每一个卡片,都会上报user_id, service_id, timestamp。
但前端传的数据……怎么说呢,有时候service_id是字符串,有时候是数字;有些字段缺失;甚至有一次测试版本把user_id全传成"test_user"……运维大哥看到日志差点报警。
所以我先写了个清洗脚本(用Pandas,别笑,Vim也能写Python):
import pandas as pd
df = pd.read_csv("user_clicks.csv")
# 过滤测试账号
df = df[~df["user_id"].str.contains("test")]
# 统一service_id为整数
df["service_id"] = pd.to_numeric(df["service_id"], errors="coerce")
df = df.dropna(subset=["service_id"])
df["service_id"] = df["service_id"].astype(int)
# 构建用户-服务交互矩阵(稀疏)
from scipy.sparse import csr_matrix
user_ids = df["user_id"].astype("category")
service_ids = df["service_id"].astype("category")
interactions = csr_matrix(
(np.ones(len(df)), (user_ids.cat.codes, service_ids.cat.codes)),
shape=(len(user_ids.cat.categories), len(service_ids.cat.categories))
)
注:这里用了稀疏矩阵,因为用户-服务矩阵99.9%都是0。别傻乎乎用dense array,内存炸了别找我。
模型搭建:三行代码能搞定?
很多人吹PyTorch简单,但真上手才发现:简单≠无脑。你得理解nn.Module、forward、Dataset这些基础概念。
我设计了一个超简单的双塔模型(User Tower + Item Tower):
import torch
import torch.nn as nn
class SimpleRecModel(nn.Module):
def __init__(self, num_users, num_items, embedding_dim=64):
super().__init__()
self.user_emb = nn.Embedding(num_users, embedding_dim)
self.item_emb = nn.Embedding(num_items, embedding_dim)
self.dropout = nn.Dropout(0.3)
# 加个小MLP提升非线性能力
self.mlp = nn.Sequential(
nn.Linear(embedding_dim * 2, 128),
nn.ReLU(),
nn.Dropout(0.3),
nn.Linear(128, 1)
)
def forward(self, user_ids, item_ids):
user_vec = self.user_emb(user_ids)
item_vec = self.item_emb(item_ids)
# 拼接 + MLP
combined = torch.cat([user_vec, item_vec], dim=1)
output = self.mlp(self.dropout(combined))
return torch.sigmoid(output) # 输出0~1概率
看起来挺清爽?但第一次跑的时候,loss根本不下降。我以为是学习率太高,调低到1e-5还是不行。最后发现:embedding没初始化好!PyTorch默认是均匀分布,但对于推荐场景,用nn.init.xavier_uniform_效果更好:
nn.init.xavier_uniform_(self.user_emb.weight)
nn.init.xavier_uniform_(self.item_emb.weight)
加了这一行,loss终于开始往下走了。那一刻,我激动得差点把猫踢下椅子(当然没真踢)。
训练:GPU不够?CPU硬扛!
我们公司穷,没买云GPU。本地笔记本只有MX350,显存2G。跑个ResNet都卡,别说训练了。
但PyTorch有个好处:CPU/GPU无缝切换。只要加一行:
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.to(device)
然后所有tensor .to(device) 就行。虽然训练慢了10倍,但至少能跑。我设置了每晚自动训练,早上起来看结果——典型的“懒人分布式训练”。
为了加速,我还做了几件事:
- Batch Size调小:从256降到64,避免OOM
- 使用DataLoader的num_workers:设成2,利用多核CPU
- 关闭梯度计算时的autograd:推理阶段用
torch.no_grad()
训练代码片段:
from torch.utils.data import DataLoader, TensorDataset
# 准备数据
train_dataset = TensorDataset(torch.tensor(train_user_ids),
torch.tensor(train_item_ids),
torch.tensor(train_labels).float())
train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True, num_workers=2)
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
criterion = nn.BCELoss()
for epoch in range(10):
model.train()
total_loss = 0
for user_ids, item_ids, labels in train_loader:
user_ids, item_ids, labels = user_ids.to(device), item_ids.to(device), labels.to(device)
optimizer.zero_grad()
outputs = model(user_ids, item_ids).squeeze()
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
total_loss += loss.item()
print(f"Epoch {epoch+1}, Avg Loss: {total_loss/len(train_loader):.4f}")
跑了10个epoch,AUC达到0.82。虽然比不上SOTA,但比我们之前的规则推荐(基于热门榜)高了15个百分点。老板看了直呼“高科技”!
踩坑实录:那些让我想砸电脑的瞬间
坑1:DataLoader死锁
在Mac上用num_workers > 0时,程序直接卡死。查了半天,发现是fork模式的问题。解决方案:在main函数开头加
if __name__ == "__main__":
torch.multiprocessing.set_start_method("spawn")
或者干脆num_workers=0。血泪教训。
坑2:模型保存与加载不一致
我用torch.save(model.state_dict(), "model.pth")保存,但加载时忘了实例化模型结构,直接model = torch.load(...),结果报错。正确姿势:
# 保存
torch.save({
'model_state_dict': model.state_dict(),
'optimizer_state_dict': optimizer.state_dict(),
}, "checkpoint.pth")
# 加载
checkpoint = torch.load("checkpoint.pth", map_location=device)
model.load_state_dict(checkpoint['model_state_dict'])
坑3:线上推理慢如蜗牛
模型训练完,部署到后端Flask服务,QPS只有2。一查发现每次请求都重新加载模型!赶紧改成全局加载一次:
# app.py
model = SimpleRecModel(...).to(device)
model.load_state_dict(torch.load("best_model.pth", map_location=device))
model.eval() # 别忘了!
@app.route("/recommend")
def recommend():
with torch.no_grad():
# 推理逻辑
QPS立马提到50+,勉强够用。
效果评估:别光看准确率
很多人只看accuracy或loss,但在推荐系统里,业务指标才是王道。我们重点看:
| 指标 | 定义 | 目标 |
|---|---|---|
| CTR(点击率) | 推荐结果被点击的比例 | +10% |
| Diversity | 推荐结果的品类覆盖度 | >5类/用户 |
| Freshness | 新服务曝光占比 | >30% |
通过AB测试,新模型上线后CTR提升了12%,而且长尾服务(比如“宠物殡葬”、“老人陪诊”)曝光明显增加——这正是我们做“综合服务”平台想要的效果。
总结:小厂也能玩转AI
回过头看,这次PyTorch入门之旅其实没那么玄乎。核心就三点:
- 问题驱动:别为了用AI而用AI,先想清楚业务要什么
- 快速验证:用最简模型跑通流程,再迭代优化
- 工程落地:模型再好,推不出去等于0
作为三线城市的技术负责人,我深知资源有限。但我们有优势:决策链短、试错成本低。老板一句话,我周末就能把原型跑出来。不像大厂,光立项就要三个月。
现在,这个推荐模块已经集成到我们的前端“综合服务”首页。用户打开APP,看到的不再是千篇一律的“热门榜”,而是“猜你喜欢”。虽然模型简单,但确实带来了业务增长。
至于下一步?我打算试试用图神经网络(GNN)建模用户-服务-商家的关系。不过在这之前,得先给家里猫买个猫爬架——让它别老坐我键盘上了。
附:环境配置清单(避坑指南)
Python 3.9 PyTorch 2.0.1 (CPU版) pandas 1.5.3 scikit-learn 1.2.2 Flask 2.2.3别用最新版PyTorch!我试过2.1,和某些旧CUDA驱动冲突,折腾半天。稳定压倒一切。
最后,如果你也在小厂搞AI,欢迎交流。Vim党、远程办公、被产品经理折磨的同志,咱们抱团取暖。

评论 0