PyTorch初体验:三线小厂技术负责人的深度学习实战手记

梁玉★
2025-12-21 12:54
阅读 464

上周五晚上十一点,我正用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.ModuleforwardDataset这些基础概念。

我设计了一个超简单的双塔模型(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倍,但至少能跑。我设置了每晚自动训练,早上起来看结果——典型的“懒人分布式训练”。

为了加速,我还做了几件事:

  1. Batch Size调小:从256降到64,避免OOM
  2. 使用DataLoader的num_workers:设成2,利用多核CPU
  3. 关闭梯度计算时的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入门之旅其实没那么玄乎。核心就三点:

  1. 问题驱动:别为了用AI而用AI,先想清楚业务要什么
  2. 快速验证:用最简模型跑通流程,再迭代优化
  3. 工程落地:模型再好,推不出去等于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

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝