news 2026/9/30 8:20:50

AI工程从零到部署:手把手构建完整模型服务全链路

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
AI工程从零到部署:手把手构建完整模型服务全链路

AI工程这条路,说难是真难,说容易也真容易。难在信息太杂,今天一个RAG明天一个Agent,后天又冒出个新框架,你永远在追热点;容易在只要你找到一条清晰的路线,按部就班把一个项目从头到尾跑通,很多以前觉得晦涩的概念会自动在脑子里串成线。这个“ai-engineering-from-scratch”的项目,本质上就是干这件事的——不靠碎片化教程拼凑见识,而是靠亲手从零搭一套完整的AI工程体系,把底子打扎实。文章适合两类人:一类是刚入行、被各种名词绕晕的初学者,另一类是已经会调模型但始终觉得“差点工程味道”的开发。我会顺着这条线,讲讲我实际踩过的坑、验证过的路径,以及每一步背后真正的理由。

1. 内容整体设计与思路拆解

1.1 到底什么才算“AI工程”

先说明白一个容易混淆的点:AI工程不等于算法调参,更不等于“会import torch”。真正的AI工程是从需求分析、数据准备、模型训练、评估迭代,到服务化部署、监控运维、持续优化的一整条链路。很多初学者容易陷在“训练准确率从90%提到92%”的局部优化里,但放到真实业务里,真正决定项目成败的往往是数据质量、推理延迟、服务稳定性这些听起来不那么性感的东西。

我当时做这个项目时,给自己定了一个原则:每个环节都不准“跳步”。就算是一个只有几百条数据的分类任务,我也要完整走一遍数据标注、清洗、划分、训练、评估、打包、上线、监控的流程。为什么?因为工程能力不是靠看出来的,是靠一次次重复的肌肉记忆练出来的。你今天能在一个玩具项目里把流程跑通,明天遇到真实业务的大规模数据、复杂模型、高并发需求,才有底气说“我见过这条路上的所有坑”。

1.2 从零开始的路线的核心矛盾

这条路线有一个天然的矛盾:既要广度,又要深度。广度是指你得懂一点数据工程、一点MLOps、一点后端开发;深度是指每一个环节展开来都是一个巨大的领域。解决这个矛盾的关键不是“平均用力”,而是“以终为始”——明确你最终要交付的东西是什么,然后倒推每个环节需要的技能下限。

我的做法是:先把目标定成“做一个能通过API调用、有完整训练和推理链路、并且做了基础监控的模型服务”。这个目标定了之后,每个环节的技能边界就清晰了。比如数据环节,我不需要成为Spark专家,只需要学会用Pandas处理中等规模数据、设计合理的划分策略、做好数据版本管理;比如部署环节,我不需要精通Kubernetes,只需要掌握容器化打包和服务接口设计。这种“够用就好”的原则,能让你在最短时间内建立起全链路视角,而不是在某个环节深挖到丧失全局感。

1.3 为什么强调“手写而非纯调包”

“from-scratch”并不意味着所有东西都要从零造轮子。PyTorch、Transformers、FastAPI这些现成工具该用就用。真正的“from-scratch”是指:每一步你都知道背后发生了什么,而不是黑盒调用。

举个例子,很多教程会让你直接from torchvision import resnet18然后跑训练,但如果你想真正理解迁移学习,就应该先手写一个简单的数据加载器、手写训练循环、手写验证逻辑。你不需要去实现ResNet的结构,但你需要清楚学习率怎么调整、梯度在反向传播时经历了什么、过拟合用什么手段缓解。把这些基本功打扎实之后,再用高级工具就会有一种“我知道它在干什么,只是让它干得更快”的掌控感。

2. 环境搭建与核心工具链解析

2.1 开发环境:别在第一步就被劝退

我见过太多人死在环境配置这一步。Python版本冲突、CUDA版本不匹配、依赖库之间的爱恨情仇,每一个都能耗掉你半天时间。我的建议是:一开始就建立隔离环境,绝不往系统Python里装任何东西。

# 创建独立环境,Python版本指定3.10(目前兼容性最好) python3.10 -m venv .venv # 激活环境 source .venv/bin/activate # 安装核心依赖,建议锁版本 pip install torch==2.2.0 torchvision==0.17.0 pip install transformers==4.38.0 datasets==2.17.0 pip install fastapi==0.110.0 uvicorn[standard]==0.29.0 pip install pandas==2.2.0 scikit-learn==1.4.0

这里有一个很容易被忽视的版本管理技巧:一定要把pip freeze > requirements.txt生成的依赖列表提交到代码仓库。否则两周后你想复现自己的实验,会发现“咦,我当时用的到底是哪个版本的transformers来着?”

关于深度学习框架版本,我建议不要去追求最新。新版本往往会带来Breaking Change,而你的目标是把流程跑通,不是做新特性测试。选一个经过社区充分验证的稳定版本,把精力放在业务流程上,这才是正确的时间分配。

2.2 GPU与CPU的抉择艺术

没有GPU能不能学AI工程?绝对能,而且很多环节根本不需要GPU。数据清洗、特征工程、模型评估、服务化部署,这些用CPU完全可以完成。只有在真正的模型训练环节,GPU才是必需品。

如果你手头只有CPU,有两个应对策略。第一,把模型规模缩小,比如用DistilBERT代替BERT,用ResNet18代替ResNet50,训练时间会从“遥遥无期”变成“可以接受”。第二,把数据集控制在千条级别,保证单轮训练时间不超过十分钟,这样你就能在合理时间内完成迭代调试循环。

# 判断设备,并让代码在两种环境下都能运行 import torch device = torch.device("cuda" if torch.cuda.is_available() else "cpu") # 在MPS环境(Apple Silicon)下也可以加一个判断 # device = torch.device("mps" if torch.backends.mps.is_available() else device)

这段代码看似简单,但它是工程化的起点:你的代码不再假设运行环境,而是动态适配环境。真实的AI系统一定是跑在异构环境里的,训练用GPU集群,推理用CPU或专用加速卡,你提早养成这个习惯,后面会少改很多代码。

2.3 项目结构:让一切井井有条

AI项目和传统软件开发有一个显著区别:不确定性高。你没法一开始就定义清楚“正确的输出是什么”,模型要经过多轮实验才能收敛。这种不确定性要求项目的代码结构必须清晰、模块化,否则很容易陷入“改一处崩三处”的泥潭。

我推荐一个经过多次实战验证的项目结构:

ai-engineering-from-scratch/ ├── config/ # 所有配置项集中管理 │ ├── data_config.yaml # 数据相关配置 │ └── train_config.yaml # 训练相关配置 ├── data/ # 数据存放目录 │ ├── raw/ # 原始数据(只读) │ ├── processed/ # 处理后的数据 │ └── experiments/ # 实验输出 ├── src/ # 核心源代码 │ ├── data/ # 数据加载、清洗逻辑 │ ├── models/ # 模型定义 │ ├── train/ # 训练循环 │ ├── evaluate/ # 评估逻辑 │ └── serve/ # 服务化代码 ├── tests/ # 单元测试 ├── scripts/ # 快速脚本 ├── requirements.txt # 依赖锁定 └── README.md # 项目说明

这个结构的核心思想是关注点分离:数据代码不管模型逻辑,训练代码不管部署细节,配置和代码完全解耦。这样做的好处是当你需要调整数据增强策略时,不会意外破坏推理服务的稳定性。我见过太多项目把训练、评估、推理全塞进一个文件里,到最后改一行代码都要提心吊胆。

2.4 实验管理:你的元记忆系统

实验管理是初学者最容易忽略、但工程化后最重要的基础设施。训练模型就是一个试错过程,你需要知道哪个参数组合效果最好、哪个模型版本部署到了生产环境、复现某个实验结果需要什么条件。没有实验管理,这些信息全靠大脑记忆,百分之百会丢。

# 安装MLflow pip install mlflow # 初始化实验 mlflow.set_experiment("text-classification-experiments") # 在训练代码中记录指标 with mlflow.start_run(): mlflow.log_param("learning_rate", 3e-5) mlflow.log_param("batch_size", 16) mlflow.log_metric("accuracy", 0.92) mlflow.log_artifact("model_checkpoint.pth") mlflow.log_artifact("tokenizer_config.json")

MLflow是我个人用得最顺手的工具,主要原因是它跟现有代码的侵入性极低。你不需要重写训练逻辑,只需要在关键节点加几行log语句,整个实验过程就被自动记录下来。这就像给你的项目装了一个黑匣子,之后任何一次实验回溯都有据可查。

3. 核心实操:从数据到模型的完整链路

3.1 数据篇:AI工程的地基工程

我见过太多数据科学家把90%的时间花在模型调参上,但真正的行业现实是:模型效果的天花板,往往由数据质量决定。与其不断堆叠模型复杂度,不如花力气把数据做到极致。

以文本分类任务为例,完整的数据处理流程包括以下几个步骤:

# 数据清洗示例 import pandas as pd import re def clean_text(text): # 去除HTML标签 text = re.sub(r'<[^>]+>', '', str(text)) # 去除多余空白 text = re.sub(r'\s+', ' ', text).strip() # 去除特殊字符 text = re.sub(r'[^\w\s\u4e00-\u9fff]', '', text) return text df = pd.read_csv("data/raw/raw_data.csv") df["clean_text"] = df["text"].apply(clean_text) # 去重 df = df.drop_duplicates(subset=["clean_text"]) # 去除空值 df = df.dropna(subset=["clean_text", "label"]) print(f"清洗后数据集大小: {len(df)}")

这里有一个坑一定要提醒:数据清洗规则和预处理逻辑不能是“一次性代码”。你训练完模型之后,推理阶段接收的新数据也必须走完全相同的清洗流程。很多项目在训练时精心处理数据,但部署时完全忘了把预处理逻辑封装进推理pipeline,导致线上效果和线下实验差距巨大。正确的做法是把清洗流程封装成可调用的函数或者类,让训练和推理共用同一套逻辑。

3.2 数据划分:建立可信评估的基石

玩过Kaggle的朋友都知道,数据怎么划分,直接决定了你评估指标的可信度。对于AI工程来说,我强烈建议使用分层抽样来保证训练集和测试集的类别分布一致。

另外,实际业务中经常会遇到数据时间分布不均匀的问题。如果你做的是一个新闻分类模型,2020年的数据训练、2023年的数据测试,模型性能很可能出现下降。因为语言在演化、话题在变化,这种情况就要考虑按时间划分训练集和测试集,模拟真实场景。

from sklearn.model_selection import train_test_split # 分层抽样,确保类别分布一致 train_df, temp_df = train_test_split( df, test_size=0.3, stratify=df["label"], random_state=42 ) valid_df, test_df = train_test_split( temp_df, test_size=0.5, stratify=temp_df["label"], random_state=42 ) print(f"训练集: {len(train_df)}, 验证集: {len(valid_df)}, 测试集: {len(test_df)}")

关于random_state,我一直强调一定要固定。这不是玄学,如果你每次的随机种子不同,实验之间的差异就包含了数据划分的随机性,你无法准确判断模型改进是否真的有效。固定随机种子,是AI实验可复现性的起点。

3.3 模型训练:手写训练循环的那些事

与其直接用Trainer写训练,我更建议至少手写一两次完整训练循环。否则你遇到“loss变成NaN”这类问题时,会根本无从排查。

from transformers import AutoTokenizer, AutoModelForSequenceClassification from torch.utils.data import DataLoader, Dataset import torch # 加载模型和分词器 model_name = "bert-base-chinese" tokenizer = AutoTokenizer.from_pretrained(model_name) model = AutoModelForSequenceClassification.from_pretrained( model_name, num_labels=2 ).to(device) # 自定义数据集类 class ClassificationDataset(Dataset): def __init__(self, texts, labels): self.texts = texts self.labels = labels def __len__(self): return len(self.texts) def __getitem__(self, idx): text = str(self.texts[idx]) label = int(self.labels[idx]) return text, label # Tokenize数据 def collate_fn(batch): texts, labels = zip(*batch) encodings = tokenizer( list(texts), truncation=True, max_length=128, padding=True, return_tensors="pt" ) return encodings, torch.tensor(labels, dtype=torch.long) train_dataset = ClassificationDataset( train_df["clean_text"].values, train_df["label"].values ) train_loader = DataLoader( train_dataset, batch_size=16, shuffle=True, collate_fn=collate_fn ) # 优化器和学习率调度 optimizer = torch.optim.AdamW(model.parameters(), lr=3e-5) total_steps = len(train_loader) * 3 # 3个epoch scheduler = torch.optim.lr_scheduler.LinearLR( optimizer, total_iters=total_steps )

这里有好几个细节值得展开。第一个是max_length的设定,128个token比512个token的训练速度快接近4倍,而大多数短文本分类任务128完全够用。第二个是batch_size的选择,我建议先从16开始,然后根据显存使用情况上下调整,显存不足就减半,显存充裕就可以加大,因为在batch size从16变成32时,训练时间并不会翻倍,而梯度估计会更稳定。

3.4 训练循环:关于loss和准确率的那些细节

from tqdm import tqdm from sklearn.metrics import accuracy_score, precision_recall_fscore_support def train_epoch(model, dataloader, optimizer, scheduler, device): model.train() total_loss = 0 all_preds = [] all_labels = [] for batch in tqdm(dataloader, desc="Training"): encodings, labels = batch encodings = {k: v.to(device) for k, v in encodings.items()} labels = labels.to(device) outputs = model(**encodings, labels=labels) loss = outputs.loss logits = outputs.logits optimizer.zero_grad() loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) # 梯度裁剪 optimizer.step() scheduler.step() total_loss += loss.item() preds = torch.argmax(logits, dim=-1).cpu().numpy() all_preds.extend(preds) all_labels.extend(labels.cpu().numpy()) avg_loss = total_loss / len(dataloader) accuracy = accuracy_score(all_labels, all_preds) return avg_loss, accuracy

梯度裁剪这行代码是我强烈建议保留的。它做的事情是:如果梯度的范数超过1.0,就把梯度按比例缩放到最大范数为1.0。为什么要这么做?因为Transformer模型在训练后期容易出现梯度过大导致的loss爆炸,梯度裁剪用一行代码避免了这种情况,属于必须的安全措施。

训练时的进度条tqdm看似只是让输出好看,实际上它传达了一个非常重要的信息:每个batch的训练时间。如果你发现每个batch耗时突然变长或者不稳定,这往往是数据加载出现瓶颈的预警信号。

3.5 评估篇:准确率不是唯一标准

模型评估是我认为工程思维体现得最明显的环节。学术界喜欢用单一指标衡量模型好坏,而工程界必须同时关注多个指标,因为业务场景往往对不同类型的错误容忍度不同。

举例说明,一个垃圾评论过滤器,如果把正常评论误判成垃圾(False Positive),用户的评论被删了,会造成用户体验下降;如果把垃圾评论放过去(False Negative),顶多是社区多了一条垃圾内容。两种错误的代价完全不同,所以我们需要同时看精确率和召回率:

  • 精确率(Precision):预测为正例的样本中,确实是正例的比例
  • 召回率(Recall):真实的正例中,被正确预测为正例的比例
  • F1分数:两者的调和平均,适合不平衡类别的总体评估
from sklearn.metrics import classification_report # 在测试集上做最终评估 report = classification_report(all_labels, all_preds, target_names=["negative", "positive"]) print(report)

这个报告在工程报告中几乎是必填项,它比单一的准确率能提供更丰富的信息。如果你的业务需要高召回率,就在训练时调整损失函数权重或者后处理时调低分类阈值;如果需要高精确率,就往反方向调。

3.6 模型保存与加载:记录全量信息

训练完成后的保存环节,是工程化和非工程化的一个明显分水岭。非工程化的做法是只保存模型权重文件,等到部署时才发现忘记了保存分词器、忘记了记录预处理参数,导致模型根本没法正确加载。

# 保存完整的模型资产 save_path = "models/text_classifier_v1" model.save_pretrained(save_path) tokenizer.save_pretrained(save_path) # 同时保存预处理参数和配置信息 import json with open(f"{save_path}/preprocess_config.json", "w") as f: json.dump({ "max_length": 128, "clean_pattern": "[^\\w\\s\\u4e00-\\u9fff]", "train_data_version": "20250214_v1" }, f, ensure_ascii=False, indent=2)

为什么要这样设计?因为模型推理时,tokenizer的max_length必须和训练时一致,预处理的正则表达式也必须一致,否则输入特征的分布就变了,效果自然就变了。把所有这些信息统一保存在模型目录下,相当于把模型和它的“使用说明书”打包在一起,之后任何人接手部署都一目了然。

4. 工程化落地:从Notebook到生产级服务

4.1 模型服务化:FastAPI搭建推理接口

模型训练好了,如果不通过接口对外提供服务,它本质上只是一个硬盘上的文件。真实场景中,模型需要接受来自前端、后端、定时任务等各种请求,这就需要通过HTTP接口暴露模型能力。

from fastapi import FastAPI, HTTPException from pydantic import BaseModel import torch app = FastAPI(title="文本分类服务") class PredictRequest(BaseModel): text: str class PredictResponse(BaseModel): label: str confidence: float # 启动时加载模型 model_dir = "models/text_classifier_v1" model = AutoModelForSequenceClassification.from_pretrained(model_dir) tokenizer = AutoTokenizer.from_pretrained(model_dir) device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model.to(device).eval() @app.post("/predict", response_model=PredictResponse) def predict(request: PredictRequest): try: encodings = tokenizer( request.text, truncation=True, max_length=128, padding=True, return_tensors="pt" ).to(device) with torch.no_grad(): outputs = model(**encodings) probs = torch.softmax(outputs.logits, dim=-1)[0] predicted_idx = torch.argmax(probs).item() confidence = probs[predicted_idx].item() return PredictResponse( label="positive" if predicted_idx == 1 else "negative", confidence=confidence ) except Exception as e: raise HTTPException(status_code=500, detail=str(e))

这里有很多工程细节值得说明。model.eval()和torch.no_grad()是必须的,前者是为了关闭Dropout和BatchNorm的训练模式,后者是为了停止梯度追踪,避免内存浪费。这两行代码如果不加,你会得到不确定的推理结果和飙升的内存占用。

还有一个细节是置信度的返回。返回confidence不只是为了让输出好看,更是为了后续做人机协作和风险控制。比如confidence低于0.6时,系统可以自动把样本送入人工审核,这样的兜底逻辑在生产环境中非常常见。

4.2 并发与性能:不要让推理服务成为瓶颈

一个基本的FastAPI服务部署好之后,我开始压测,结果在并发请求下发现两个明显问题:一是请求响应变慢,二是CPU/GPU利用率忽高忽低。这两个问题的根源可以从数据缓存、推理批处理、异步处理几个方向来排查。

首先是数据缓存。输入的原始字符串在重复处理时会消耗大量CPU,于是我加入了一个内存缓存层,基于字典进行存储。实际测试下来,当重复请求较多时,缓存命中率可以达到一定高度,CPU占用显著下降。

from functools import lru_cache @lru_cache(maxsize=1024) def preprocess_text(raw_text: str): return clean_text(raw_text) # 使用示例 text_clean = preprocess_text(request.text)

其次是动态批处理。单条请求逐一推理的GPU利用率会很低,因为GPU擅长并行计算。我参考了一些开源实现,为自己封装了一个简单的动态批处理模块,把同时段到达的请求拼成一个batch,统一推理再返回结果。实际使用中,当并发请求达到8条以上时,动态批处理能显著提升吞吐量。

4.3 部署与CI/CD:让更新流程自动运转

很多个人项目停留在“写代码 + 本地跑通”这一步,但这与工程化还有一定距离。工程化意味着:每次代码更新都能自动经过测试、构建、部署,而不是手动复制文件、重启进程。

在这个项目里,我用Docker做镜像打包,用GitHub Actions做自动部署。

FROM python:3.10-slim WORKDIR /app # 先拷贝依赖文件,利用Docker缓存 COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY src/ src/ COPY models/ models/ EXPOSE 8000 CMD ["uvicorn", "src.serve.app:app", "--host", "0.0.0.0", "--port", "8000"]

Dockerfile的书写顺序也有讲究。先拷贝requirements.txt并安装依赖,再拷贝源代码,是为了利用Docker的layer缓存机制。这样当你修改了源代码但依赖没变时,重新构建会直接复用已经构建好的依赖层,构建速度会快一个数量级。而如果你先把全部代码拷贝进去再装依赖,任何一行代码变动都会导致整个依赖层重新构建,极浪费时间。

CI/CD流水线的逻辑很简单:推送代码到main分支后,自动跑测试,通过后构建镜像,然后拉取到服务器并替换容器。以GitHub Actions的main工作流为例:

name: CI/CD Pipeline on: push: branches: [main] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Run tests run: | pip install -r requirements.txt pytest tests/ deploy: needs: test runs-on: ubuntu-latest steps: - name: Deploy to server run: | docker build -t my-ai-service:latest . docker push my-ai-service:latest ssh user@server "docker pull my-ai-service:latest && docker-compose up -d"

写好这个流水线之后,我最大的感受是:工程化带来的不只是效率,更是安全感。我可以在main分支放心提交代码,因为每一次改动都经过了自动化测试的验证。我做AI项目时总有一种隐隐的不安感,担心自己改坏了什么而不自知。CI/CD就像一份保险,把这种不安降到了最低。

4.4 监控与日志:部署不是终点,是起点

我见过太多“模型上线即失联”的情况。上线之后,没有人知道模型的在线表现怎么样,直到业务方反馈“效果变差了”才后知后觉。工程化的最后一个环节——监控,就是为了解决这个问题。

这个项目里,我至少收集三类信息:一是模型指标,比如请求量、平均响应时间、置信度分布、模型预测的类别分布;二是系统资源,比如CPU/GPU使用率、内存占用、磁盘IO;三是业务效果,如果有反馈机制,比如用户是否对预测结果点了“赞/踩”,可以间接评估线上表现。

import time from prometheus_client import Counter, Histogram, Gauge REQUESTS = Counter("http_requests_total", "Total HTTP requests") PREDICT_TIME = Histogram("predict_processing_seconds", "Time spent processing prediction") CONFIDENCE = Histogram("prediction_confidence", "Confidence distribution") @app.post("/predict", response_model=PredictResponse) def predict(request: PredictRequest): REQUESTS.inc() start_time = time.time() # ... 原有推理逻辑 ... PREDICT_TIME.observe(time.time() - start_time) CONFIDENCE.observe(confidence) return prediction

在真实业务里,模型监控的投入不比模型研发低,因为线上系统出了问题是会造成实际损失的。置信度监控尤其重要——如果你发现最近一周的置信度持续走低,往往意味着输入数据分布发生了变化,模型性能正在退化。这比业务方投诉要早得多,能给你留出干预时间。

5. 常见问题与排查技巧实录

5.1 环境类问题的排查思路

问题:CUDA out of memory

这个报错几乎是所有AI工程师的宿命之敌。我的排查顺序是:先确认是不是真的显存不够,因为有时候是被其他进程占用了;然后用nvidia-smi查看显存占用情况;最后才是优化模型方案。优化路径从简单到复杂排序为:减小batch size、降低max_length、使用梯度累积、更换更小的模型。

问题:Could not find a version that satisfies the requirement

这个报错通常发生在新环境安装包的时候。最常见的可能有两种:Python版本太老或太新,某些包还没有对应版本;或者用了不存在的包名。建议先锁定Python版本为3.10,再查官方文档确认包的版本要求。另外,我建议大家养成用conda或venv创建干净环境的习惯,不要为了省事把包装进base环境,因为总有一天会依赖冲突。

5.2 训练过程问题与对策

问题:loss突然变成NaN

训练中浮现NaN的原因很多,如学习率过大、数据中存在NaN值、梯度爆炸等。排查顺序如下:先确认数据没有NaN;再检查模型输出有没有极端值;最后看学习率与梯度范数。

# 开启异常检测,帮助定位NaN torch.autograd.set_detect_anomaly(True)

这个API能在反向传播出现NaN时,准确告诉我们哪一步出了问题。根据经验,文本模型中NaN的原因大多是学习率过大,建议先降低学习率到原本的十分之一再试。

问题:模型只预测一个类别

模型只输出一个类别,通常有两个原因。一个是数据问题,比如positive样本远多于negative样本,模型学会了“全都猜positive”的懒惰策略。另一个是模型初始化不当,或者学习率过大导致模型陷入了错误的局部最优。解决方式可以尝试:使用weighted_cross_entropy按类别比例给loss加权;或尝试更低的初始学习率,让模型更稳定地拟合数据。

from torch.nn import CrossEntropyLoss # 根据类别比例计算权重,给少数类更高的loss权重 class_weights = torch.tensor([1.0, 2.5]).to(device) # 假设negative:positive = 5:2 loss_fn = CrossEntropyLoss(weight=class_weights) inputs = {"input_ids": encodings["input_ids"], "attention_mask": encodings["attention_mask"]} outputs = model(**inputs, labels=labels) loss = loss_fn(outputs.logits, labels)

5.3 生产部署问题的避坑经验

问题:本地能跑,容器里跑不起来

Docker可以解决环境一致性问题,但使用中还是有很多细节会造成“本地能跑、容器里跑不起来”。最常见的原因有:容器内没有正确安装CUDA驱动;时区不同导致的日志时间错乱;文件路径大小写敏感。啊对了,在Dockerfile中设置正确的时区也很重要,否则日志的UTC时间和北京时间对不上,排查问题会非常痛苦。

ENV TZ=Asia/Shanghai RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone

问题:推理速度太慢

推理速度慢是一个综合性问题,通常需要从多个角度优化。首先,把batch size和max_length调整到合理范围;其次,尝试把模型转换为ONNX或TensorRT格式获得2-5倍的加速;再次,如果你用CPU做推理,建议更换为GPU或量化模型(如使用torch.quantization),通常能获得接近数量级的性能提升。

6. 扩展方向与个人体会

6.1 下一步还能怎么走

当你把上面这条链路全部跑通之后,你其实已经具备了一个AI工程师的底层骨架。接下来可以往三个方向深挖,完全取决于你的职业兴趣:

  • 深度学习方向:加入更复杂的模型结构(如多模态、大语言模型微调),做更细粒度的模型优化和推理加速
  • 数据工程方向:学习Spark、Flink等分布式数据处理框架,理解大规模数据管道的设计与实现
  • MLOps平台方向:深入研究模型全生命周期管理(如模型注册中心、特征商店),构建端到端的AI中台

6.2 我的真实体会和最后建议

这个项目从头走到尾,我最深的感受是:AI工程不是一条直线,而是一张充满循环的网。你训练出来的模型部署到线上,线上反馈的数据再回流到训练集,改进后再重新部署。在这个循环里,最不需要担心的恰恰是“记忆力”——模型能不能记住训练数据不重要,重要的是你的系统能不能从数据中持续学习。

最后再分享两个小技巧。第一,习惯写README,不仅要写“这个项目怎么跑”,更要写“为什么要这样设计”。一周之后的你,一定会感谢今天写了详细README的自己,因为你在调试中会翻回这个文档,想起当初那些决策的背景。第二,不要惧怕在公开社区分享你的项目。我发布这个项目后,收到过不止一条“我的数据分布和你的不同,能给点建议吗”的私信。这些交流带来的视角,是闷头写代码永远得不到的。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/30 8:19:15

PostgreSQL vs MySQL:高性能场景下复杂查询与并发控制的实战解析

做数据库选型这些年&#xff0c;被问得最多的一个问题就是&#xff1a;“高性能场景到底用 PostgreSQL 还是 MySQL&#xff1f;”以前我一般会打太极&#xff0c;说“看情况”&#xff0c;但做过的项目越多&#xff0c;我的回答越偏向一个方向&#xff1a;如果是真正的高性能、…

作者头像 李华
网站建设 2026/9/30 8:18:59

Model-Optimizer实战:模型量化与硬件感知优化全流程

1. 项目概述&#xff1a;这不是一个“一键压缩”的玩具&#xff0c;而是一套面向真实推理场景的模型瘦身工作流“Model-Optimizer”这个名称听起来像某个商业软件的商标&#xff0c;但在我过去三年深度参与十几个边缘AI落地项目的实操经验里&#xff0c;它从来不是开箱即用的黑…

作者头像 李华
网站建设 2026/9/30 8:17:25

决策树从直觉到数学:信息熵、信息增益与剪枝实战解析

第一次学决策树的人&#xff0c;多半会有一种“就这”的感觉&#xff1a;训练完一看&#xff0c;无非就是一连串嵌套的 if-else 规则&#xff0c;跟楼下物业大叔用 A4 纸打印的“访客登记流程图”几乎没有区别。强大如机器学习&#xff0c;怎么就折在这种朴素结构上了&#xff…

作者头像 李华
网站建设 2026/9/30 8:16:45

xray服务访问控制改造:匿名、授权与IP白名单三种方式详解

项目是我自己在维护的内网扫描服务。xray用得久了有个绕不开的问题&#xff1a;默认监听端口谁都能连&#xff0c;只要知道地址&#xff0c;随便一个人都能把扫描任务调起来&#xff0c;甚至能看到别人提交的检测目标。公司内部还好&#xff0c;一旦跨部门协作或者需要远程接入…

作者头像 李华
网站建设 2026/9/30 8:16:32

零显卡深度学习环境搭建:Python、PyCharm与PyTorch CPU版

1. 先把路线定下来&#xff1a;这套深度学习环境到底装了什么 搞深度学习环境搭建这件事&#xff0c;说难不难&#xff0c;说简单也确实能把人卡一整天。Python、PyCharm、PyTorch CPU 版这三个东西单独拿出来装&#xff0c;任何一个都不会让你抓狂&#xff0c;但把它们串成一条…

作者头像 李华
网站建设 2026/9/30 8:15:25

ArcGIS属性查询100条公式:SQL表达式、报错与优化

1. 属性查询这件事&#xff0c;90%的人只用到了皮毛干这行十来年&#xff0c;我发现一个挺有意思的现象&#xff1a;身边不少同事能把空间分析、模型构建器、栅格计算器玩得很溜&#xff0c;但一到"按属性选择"那个对话框&#xff0c;敲出来的公式永远是字段 某值这…

作者头像 李华