ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

直接用BERT预训练模型计算句子相似度,无需微调

直接用BERT预训练模型计算句子相似度,无需微调 简介面向自然语言处理初学者与开发者这份下载包提供了一整套基于 PyTorch Transformers 加载 BERT 预训练模型计算句子相似度的可直接运行方案。资源以文本相似度计算为主线涵盖问答匹配、文档检索、语义分析等常见场景通过调用中文 BERTchinese-bert-wwm-ext模型演示了从分词编码、模型推理到余弦相似度计算的核心流程。压缩包共 4 个文件包括 Python 脚本、文本说明、config.json 配置和 pytorch_model.bin 预训练权重文件整体约 365.84 MB其中 py 脚本为可直接运行的示例代码txt 说明梳理实现思路config.json 与 bin 分别对应模型配置和预训练权重。运行脚本时BertTokenizer 完成句子分词与特殊标记添加BertModel 获取 last_hidden_state 中的句子向量再通过 cosine_similarity 计算相似度输出数值越接近 1 代表语义越相近。目前已有 1641 人学习下载对希望快速上手 BERT 语义匹配任务的读者具有直接参考价值。1. 算相似度先别急着训练直接加载 BERT 预训练模型就够了句子相似度这个需求几乎每个做文本的团队都会遇到搜索召回、重复问题合并、客服问答匹配、论文去重。很多人第一反应是“得先标注数据、微调模型”但如果你只是先验证效果、跑通基线直接用 torch 和 transformers 加载一个 BERT 预训练模型不做任何训练也能算出可用的语义相似度。这个方案的核心就三步加载 tokenizer 和模型把句子过一遍得到向量再算余弦相似度。写起来不过三十行代码CPU 也能跑。适合想快速搭起语义匹配基线、或者需要离线批量算相似度的从业者。这里有一个反直觉的点BERT 明明是个语言模型为什么不做任何微调直接拿它输出的向量算相似度效果就比字符重叠、TF-IDF 好一大截因为它经过预训练后每一层 Transformer 编码出的向量已经包含了上下文语义信息句子向量之间的空间距离本身就是一种相似度度量。本文沿着这条路径从环境安装、模型加载、向量计算到踩坑排查完整走一遍。2. 装环境与加载 bert-base-chinese先让模型在本地吐出一个向量2.1 安装 torch 与 transformers先解决本地跑起来的问题这个方案的技术栈只有三个torch 负责张量计算transformers 提供模型和分词器BERT 预训练模型作为权重。先装软件包。pip install torch transformers如果你有 NVIDIA GPU 并且想用 GPU 加速一般做法是先去官网选对应的 CUDA 版本再装 torch然后把 transformers 一起装掉。我通常直接用默认源写这行命令它能自动拉取当前 Python 版本匹配的 torch 和 transformers。但如果你装过 PyTorch 的 CPU 版后面想切换 GPU 版最简单的办法是建一个新的虚拟环境不要原地反复覆盖省得出现torch.cuda.is_available()永远为 False 的玄学问题。关于版本不用刻意追求最新。torch 2.x 系列配 transformers 4.x 系列是目前最常见组合transformers 的from_pretrained接口从 3.0 以后就非常稳定。如果你看到error: could not find a version that satisfies the requirement torch别急这个错误大概率不是版本号的问题而是你的 pip 源里没有对应平台和 Python 版本的 wheel具体排查我放到第 5 章的“坑”里面讲。装完后先跑一个最基本的检查import torch import transformers print(torch:, torch.__version__) print(transformers:, transformers.__version__) print(cuda available:, torch.cuda.is_available())这里cuda available输出 False 不影响跑通本文的代码只是速度会慢一些。如果你有 GPU 但输出 False先查驱动和 CUDA 工具包再决定要不要重装 torch。2.2 用 from_pretrained 加载 bert-base-chinese第一次下载与本地缓存这里我们用 BERT 的中文预训练权重bert-base-chinese。它是 Google 发布的 12 层 Transformer隐藏层维度 768参数量大约 1.1 亿对中文句子相似度来说是一个非常稳妥的基线模型。加载代码极短from transformers import AutoTokenizer, AutoModel model_name bert-base-chinese tokenizer AutoTokenizer.from_pretrained(model_name) model AutoModel.from_pretrained(model_name) print(tokenizer 类型:, type(tokenizer)) print(model 类型:, type(model))第一次执行这代码时transformers 会去 Hugging Face 下载模型权重和词典文件保存到本地缓存目录。第二次再跑就直接从缓存读取了不会重复下载。AutoTokenizer会根据你传的模型名自动选择对应的分词器类AutoModel同理这也是相比直接写BertTokenizer和BertModel更省心的原因换 RoBERTa、换多语言模型代码不用改。这里需要理解一个关键概念分词器负责把句子变成 token id模型负责把 token id 变成向量。两者必须来自同一个预训练模型不能拿bert-base-chinese的 tokenizer 去配其他模型的权重否则词典对不上出来的向量就是乱套的。2.3 确认模型在 CPU/GPU 上运行device 迁移与推理开关模型加载进来后默认在 CPU 上。如果你的机器有 GPU我建议显式把模型搬过去device torch.device(cuda if torch.cuda.is_available() else cpu) model.to(device) model.eval()这三行是后面所有代码的地基。model.to(device)把权重搬到 GPU 显存里model.eval()把模型切到推理模式关掉 Dropout 和 BatchNorm 的训练行为。我见过不少人忘了调model.eval()结果同一个句子对每次算出来的相似度都不太一样其实就是 Dropout 在搞鬼。这一点在 BERT 上尤其隐蔽因为from_pretrained加载出来的模型默认处于训练模式和 PyTorch 里新建一个网络默认状态一样。除非你显示调用model.eval()否则 Dropout 层会在前向传播时随机丢弃一部分神经元输出向量每次都有细微差别。到这一步模型已经可以向前计算了。接下来是判断一次前向传播的输出长什么样方便理解后续代码sent 今天天气怎么样 inputs tokenizer(sent, return_tensorspt) with torch.no_grad(): outputs model(**inputs) # outputs 是一个 ModelOutput 对象 last_hidden outputs.last_hidden_state print(inputs 的形状:, inputs[input_ids].shape) print(last_hidden_state 形状:, last_hidden.shape)last_hidden_state的形状是[batch_size, sequence_length, hidden_size]对上面的单条短句来说就是[1, 11, 768]。sequence_length是 token 数量包含[CLS]和[SEP]768是每个 token 的向量维度。inputs是一个 dict里面通常包含input_ids、token_type_ids和attention_mask这三种输入的含义分别是token 在词表中的编号、句子标识、哪些位置是真实文本哪些是 padding。这三个字段直接作为model(**inputs)的关键字参数传入就能得到输出。3. 用 transformers 把句子变成向量CLS 池化与 mean pooling 的代码实现3.1 从词向量到句向量CLS 池化与 mean pooling 的实现模型输出的是每个 token 的向量但相似度计算需要的是一个句子一个向量因此要做“池化”。最常见的两种做法是取[CLS]位置的向量以及对所有 token 向量做平均。import torch def get_cls_pooling(model_output): 取 [CLS] 位置的向量BERT 输出第 0 个 token 对应 [CLS] return model_output.last_hidden_state[:, 0, :] def get_mean_pooling(model_output, attention_mask): 对所有 token 向量做加权平均权重用 attention_mask 这样 padding 位置不会把均值拉低 last_hidden model_output.last_hidden_state # [batch, seq_len, hidden] mask attention_mask.unsqueeze(-1).float() # [batch, seq_len, 1] masked_hidden last_hidden * mask # padding 位置被乘 0 summed masked_hidden.sum(dim1) # 按序列方向求和 counts mask.sum(dim1) # 每个句子真实 token 数 return summed / counts # 求平均这里要理解attention_mask的作用句子被 padding 后后面补的[PAD]token 本身也有向量但它们不表示任何语义。如果直接对所有 token 平均那些无意义的[PAD]向量会把句向量拉偏。所以用 mask 把 padding 位置乘 0再把真实位置加起来除以真实 token 数得到一个不含 padding 干扰的均值向量。那[CLS]池化呢BERT 预训练时[CLS]位置的输出被当作整个序列的聚合表示用于分类任务所以它天然带有句子级的语义。不过在相似度场景里[CLS]的表现并不总是比 mean pooling 好因为预训练任务对它优化的方向和句向量相似度不完全一致。我一般会把两种都跑一遍拿一个小验证集看一下效果再定。3.2 完整的句子相似度最小可运行代码把前两节的内容拼起来就是一个完整的句子相似度计算函数import torch from transformers import AutoTokenizer, AutoModel device torch.device(cuda if torch.cuda.is_available() else cpu) model_name bert-base-chinese tokenizer AutoTokenizer.from_pretrained(model_name) model AutoModel.from_pretrained(model_name).to(device).eval() def encode_sentence(sentence, max_length128, poolmean): 把一条句子编码成 768 维向量 pool: mean 用均值池化cls 用 [CLS] 向量 inputs tokenizer( sentence, max_lengthmax_length, paddingmax_length, truncationTrue, return_tensorspt, ) inputs {k: v.to(device) for k, v in inputs.items()} with torch.no_grad(): outputs model(**inputs) if pool cls: return outputs.last_hidden_state[:, 0, :].squeeze().cpu() else: mask inputs[attention_mask].unsqueeze(-1).float() last_hidden outputs.last_hidden_state masked last_hidden * mask vec masked.sum(dim1) / mask.sum(dim1) return vec.squeeze().cpu() # 计算两条句子的相似度 sent_a 今天天气怎么样 sent_b 明天会下雨吗 vec_a encode_sentence(sent_a) vec_b encode_sentence(sent_b) cos_sim torch.nn.functional.cosine_similarity(vec_a, vec_b, dim0) print(句子A, sent_a) print(句子B, sent_b) print(余弦相似度, cos_sim.item())这段代码的流程是tokenizer 把句子变成模型输入 dict模型输出最后一层 hidden state池化函数得到句向量最后用cosine_similarity计算相似度。注意我把整个编码过程包在with torch.no_grad()里这是推理代码的基本素养不计算梯度内存占用小很多速度也快。encode_sentence返回的是.cpu()的张量原因是在 GPU 上算完的向量如果后续要存盘或者用 numpy 处理搬到 CPU 更顺手。如果只是临时比较两个向量留在 GPU 上也能算但这会让后续批量处理时很难统一管理显存。3.3 max_length、padding 与 truncation 三个参数必须显式配置tokenizer 有一堆参数但实际项目中必须关注的就这么几个不配置全的话代码能跑但结果不可控inputs tokenizer( sentence, max_length128, paddingmax_length, truncationTrue, return_tensorspt, )第一个是max_length。BERT 的位置编码上限是 512超过就没法处理。实际场景里句子相似度任务很少有超过 128 个 token 的句子我的经验是设成 128 能覆盖绝大多数中文使用场景。第二个是truncationTrue对超过max_length的文本从尾部截断。第三个是paddingmax_length把所有样本补到统一长度这样 batch 训练和推理时张量形状才对齐。这里有一个很多人踩过的细节两个长度不同的句子如果各自用paddingmax_length编码得到的向量长度是一致的但 padding 位置会对 mean pooling 有影响如果不用 mask 加权平均而是裸算last_hidden.mean(dim1)那句子越短[PAD]占比越高向量被稀释得越狠。我见过有人拿同样两个句子换一台机器跑结果就不一样最后发现是 tokenizer 配置不同导致 padding 长度变化均值池化结果跟着变了。所以在第 2 章的池化代码里我刻意传了attention_mask进去就是为了消除这个影响。4. 相似度分数怎么算才靠谱余弦、内积、温度与阈值的选型参数4.1 余弦相似度 vs 内积 vs 欧氏距离不同业务下的选择逻辑向量算出来了接下来选相似度指标。最常用的是余弦相似度因为它只关心方向不关心向量长度。这在句子相似度场景下非常合适两句语义相近的话即使一句是短句一句是长句向量长度有明显差异余弦相似度依然能给出接近的值。import torch.nn.functional as F def cosine_sim(a, b): 对两个向量做 L2 归一化后再点积等价于余弦相似度 a F.normalize(a, p2, dim-1) b F.normalize(b, p2, dim-1) return (a * b).sum(dim-1)这段代码的工程意义在于先归一化再点积和直接调cosine_similarity结果一致但归一化后的向量可以预先算好缓存后面算相似度就是一次点积。如果你要做批量比对比如一万条文本两两计算相似度把每个向量先归一化再走矩阵乘法比循环里反复调cosine_similarity快一个数量级。什么时候用内积如果你希望向量本身的“长度”也有信息量比如用max_length截断后长文本和短文本的差别在向量长度上有体现内积会放大这种差别。但在 BERT 直接输出向量的场景里向量长度受 padding 影响较大不适合作为业务判断依据。所以我建议统一用余弦相似度。欧氏距离也能用但它的值域和余弦正好相反——距离越小越相似很多团队为了逻辑统一最后还是回到余弦。4.2 温度系数与阈值选择相似度分数怎么变成业务判断余弦相似度的输出范围是 -1 到 1但 BERT 直接输出的句向量算出来的余弦值通常集中在 0.7 到 0.95 之间分布非常窄。这意味着你很难直接拿 0.8 作为阈值去判断“相似还是不相似”可能两个字面完全不相关的句子算出来也有 0.7 多的相似度。处理这个问题的常见做法是引入温度系数把分数拉开temperature 0.05 # 小于 1 的温度能放大分数差异 def scaled_similarity(a, b, temperaturetemperature): a F.normalize(a, p2, dim-1) b F.normalize(b, p2, dim-1) return (a * b).sum(dim-1) / temperature温度系数的作用是把向量间的微小差异放大。当temperature1.0时就是原始余弦值当temperature取 0.05 时原本 0.85 和 0.86 的两对句子分数差距能明显拉开。这个技巧最早流行于对比学习但在相似度阈值筛选时同样好用。阈值怎么定我建议不要拍脑袋跑一批真实数据把相似度分数画直方图或者按配对样本看分布重叠区。如果你有少量标注数据可以做一个简单的网格搜索import numpy as np # sims 是 N 个句子对的相似度分数labels 是 0/1 人工标注 def best_threshold(sims, labels): best_acc 0 best_t 0.5 for t in np.arange(0.5, 0.99, 0.005): preds (sims t).astype(int) acc (preds labels).mean() if acc best_acc: best_acc acc best_t t return best_t, best_acc这里labels表示句子对是否语义相似人工标几十对就够初步定阈值。注意这个搜索是在验证集上做的真正上线前最好再拿一批没参与调参的数据复核一遍否则容易过拟合到标注集上。4.3 batch 输入与梯度开关批量计算时别把显存打满实际工作中很少只算两三条句子更多时候是对一个列表做两两比较。批量编码时如果不注意显存很容易爆掉。下面的代码展示怎么批量跑并且输出一个相似度矩阵import torch import torch.nn.functional as F def batch_encode(sentences, batch_size8): 批量编码句子返回归一化后的句向量矩阵 [n, 768] all_vecs [] for i in range(0, len(sentences), batch_size): batch sentences[i: i batch_size] inputs tokenizer( batch, max_length128, paddingmax_length, truncationTrue, return_tensorspt, ) inputs {k: v.to(device) for k, v in inputs.items()} with torch.no_grad(): outputs model(**inputs) last_hidden outputs.last_hidden_state mask inputs[attention_mask].unsqueeze(-1).float() vec (last_hidden * mask).sum(dim1) / mask.sum(dim1) all_vecs.append(F.normalize(vec, p2, dim-1).cpu()) return torch.cat(all_vecs, dim0) sentences [ 今天天气怎么样, 明天会下雨吗, 现在几点钟了, 北京的天气如何, ] vecs batch_encode(sentences, batch_size2) sim_matrix vecs vecs.T # 归一化后点积即余弦相似度矩阵 print(sim_matrix.numpy().round(3))我解释一下这个代码的逻辑batch_encode按batch_size切片处理每批过一遍模型出来的向量做 L2 归一化后拼到一起。最后一步vecs vecs.T是矩阵乘法等价于把所有句子两两之间的余弦相似度一次性算完。vecs的形状是[n, 768]乘上它的转置[768, n]得到[n, n]的相似度矩阵[i, j]位置就是第 i 句和第 j 句的相似度。注意batch_size对显存的影响BERT base 模型在 128 token 长度下单个样本大约占 1GB 显存包括中间激活8 是一个相对安全的初始值。如果你的 GPU 显存较小比如 4GB就把batch_size降到 4 或 2。如果显存足够了但还是想要更快一点可以把max_length从 128 降到 64很多短文本场景在这个长度下精度损失很小但速度能提升一截。这也是为什么我不建议在推理时叠加 flash-attention 之类的加速库对句子相似度这种短文本、小 batch 的场景显存和速度瓶颈主要在数据搬移和前向传播本身优化注意力实现带来的收益有限反而引入额外的安装兼容性问题。这个方向真正值得投入的是第 6 章要讲的向量缓存和批量召回。5. BERT 加载与相似度计算的 5 个常见坑从安装 torch 失败到结果全为 15.1 安装 torch 报 “could not find a version that satisfies the requirement torch”这是新环境里最常见的报错完整报错是ERROR: Could not find a version that satisfies the requirement torch后面通常还跟着一行说找不到匹配版本。现象是 pip install torch 直接失败怀疑是不是版本名写错了。原因一般有两个一是 pip 默认源里没有当前平台和 Python 版本的 torch wheel。比如 Python 3.12 刚发布时PyTorch 官方只支持到 3.11你拿 3.12 装旧版 torch 就会找不到匹配版本。二是网络原因默认源连接失败导致拉取列表不完整。解决思路是先确认自己的 Python 版本然后用官方源安装或在命令里指定平台。最常见做法是去 PyTorch 官网复制一条带 CUDA 参数的安装命令它生成的链接指向官方准确版本。如果你在公司内网配置镜像源的优先级是先换 pip 源再考虑版本问题。注意安装失败时不要反复重试同一命令先跑python --version和pip --version确认环境再决定是换源还是换版本。5.2 模型下载卡在进度条或 SSL 报错transformers 一直加载不了现象是首次执行from_pretrained时看到下载进度条一直在 0% 或者几 KB 就停住最后抛一个 SSL 证书错误。这是因为bert-base-chinese的权重文件总大小大约 400MB下载连接不稳定就会中断。我一般在公司服务器上第一次加载模型时都会先设置一个临时的镜像环境变量把模型文件的下载指向国内镜像地址。如果镜像也不好使就先把权重包手动下载好放到本地路径然后用本地路径加载# 手动下载并解压后模型目录里包含 pytorch_model.bin / config.json / vocab.txt model AutoModel.from_pretrained(/data/models/bert-base-chinese) tokenizer AutoTokenizer.from_pretrained(/data/models/bert-base-chinese)这是最不依赖网络稳定性的一种做法。权重文件下载一次之后整个目录可以打成压缩包分发到其他机器后续全部走本地路径既不需要缓存目录也避免了每台机器重复下载。from_pretrained对本地路径和模型名的处理是一样的你传给它一个目录路径它就直接从目录读文件不会再触发下载逻辑。另外注意一点手动下载时文件名必须是pytorch_model.bin不要下载成 TensorFlow 格式的bert_model.ckpt否则加载时会报缺少键名或者 key 不匹配。拿不准的话直接看目录里有没有config.json这是 transformers 识别一个模型目录是否合法的硬指标。5.3 相似度结果全部接近 1或者同一对句子每次结果都不一样这两个现象放在一起说因为它们经常同时出现。第一批跑出来的结果任意两个句子的相似度都在 0.98 以上看起来“什么都是相似的”如果重新跑一遍数值又有变化。第一个现象的原因是忘了做attention_mask加权平均。前面代码里我提到了这一点如果你简化成last_hidden.mean(dim1)并且句子长度差异大padding 占大部分位置均值池化结果就会被[PAD]向量主导所有句子都趋向同一个方向相似度自然接近 1。第二个现象的原因是模型还在训练模式Dropout 没有关闭每次前向传播输出的向量有随机扰动。解决办法也很直接池化时带上attention_mask推理前显式调用model.eval()再把前向传播包进torch.no_grad()。这三个动作缺一个结果都有隐患。我建议把它们写进一个初始化函数不要每次写代码时临场决定。5.4 中文分词结果和预期不符未登录词与繁体字现象是“今天天气怎么样”分词变成一堆[UNK]或者繁体句子和简体句子算出来的相似度异常低。bert-base-chinese用的是字级别的分词理论上不会出现[UNK]但如果文本里有罕见 Unicode 字符、emoji 或者生僻字词表覆盖不到时也会映射到[UNK]。另一个容易忽略的是繁简体。bert-base-chinese的训练数据以简体为主对繁体支持一般。如果你做的是台湾用户或香港用户的文本相似度直接用这个模型可能效果偏差。常见做法有两种一是代码层先繁转简再进模型用opencc转一下二是换用对繁体覆盖更好的中文预训练模型比如一些社区开源的全词掩码版本。遇到相似度明显不合理的情况先把两句的 tokenizer 结果打出来看确认没有异常分词再怀疑模型本身tokens tokenizer.tokenize(今天天气怎么样) print(tokens) # [今, 天, 天, 气, 怎, 么, 样]这一步能帮你快速定位是词表问题还是模型计算问题。5.5 GPU 显存溢出明明只传了两句话现象是计算批量相似度时代码跑到一半报CUDA out of memory但你以为自己只输出了两条短句。原因通常有两个一是在batch_encode中batch_size设置太大BERT 的中间激活值占了大量显存二是代码里没有释放历史张量循环里累积了太多旧的计算图。如果你忘了with torch.no_grad()每个 batch 都会保留一份计算图显存占用会随着循环线性增长直到溢出。解决方式是三层递进先确保推理代码用no_grad然后调小batch_size短文本场景从 8 降到 4 基本能绕开多数显存不足最后如果你的句子真的非常长把max_length从 128 改成 64。这三个参数调整对相似度结果的影响很小但对显存占用是决定性的。建议在任何团队内部测试环境里先跑一次用 20 条真实句子的批量编码记录显存占用峰值再推断全量数据的 batch 大小。6. 从两句话到批量文本向量缓存、语义检索与效果验证的进阶玩法当你的数据量从几条变成几万条逐次调用encode_sentence就不现实了。这里的核心转变是把算力花一次、结果存下来反复用。句子向量是一个 768 维的浮点数列表一条句子大约 3KB一万条也就 30MB完全可以直接落盘。import torch # 全量句子编码 all_sentences [...] # 你的语料 vecs batch_encode(all_sentences, batch_size8) # 落盘保存后续直接加载不再重新过模型 torch.save(vecs, sentence_vecs.pt) with open(sentences.txt, w, encodingutf-8) as f: f.write(\n.join(all_sentences))这里我把句向量和原始文本分开存向量文件用来算相似度文本文件用来回查内容。后续跑服务时加载一次向量文件新来的查询句子只需要算一次向量和库里所有句子做一次矩阵乘法就能拿到相似度排序。这就是一个最简的语义检索系统不依赖任何外部向量数据库单机就能扛住十万级别的数据量。如果你想在几万条向量里找“和某条最相似的前 10 条”完全手写矩阵乘也能跑但每次查询都是 O(n) 复杂度。当数据量到百万级别就需要引入向量检索工具了。最常见的做法是直接把归一化后的向量写入一个索引文件用 FAISS 建索引它内置了 IVF、HNSW 等近似最近邻算法检索耗时能降两个数量级。我这里提供思路不贴完整代码因为接入 FAISS 后你的向量存储和更新策略会跟着变它适合数据量确实大到算不过来的阶段。最后一个必做的步骤是效果验证。很多人跑通了两条句子的相似度就认为“BERT 直接加载做相似度可行”但真实业务里的相似和不相似边界比想象中模糊。我一般会取 200 对样本人工标 0/1然后算模型分数和人工标签的 Spearman 相关系数用这个相关系数衡量当前池化方式和阈值好不好用from scipy.stats import spearmanr # 人工标注1 表示语义相似0 表示不相似 human_labels [1, 0, 1, 0, 1] model_scores [0.91, 0.62, 0.88, 0.58, 0.93] corr, p_value spearmanr(human_labels, model_scores) print(fSpearman 相关系数: {corr:.3f})如果相关系数低于 0.6说明当前配置下模型对相似和不相识的区分能力不够优先尝试调池化方式mean 换 cls、换更合适的中文预训练模型。相关系数只是衡量排序一致性最终业务判断还是要回到第 4 章说的阈值选择。我个人的习惯是相关系数过 0.8 之后就不再纠结模型选型专心跳阈值和业务指标。这个方向我从最早的 TF-IDF 一路做到 BERT 向量化最大的一个教训是别过早引入向量数据库、微调、蒸馏这些重武器。先用bert-base-chinese把基线跑通向量存好拿真实的业务数据验证一轮效果再决定下一步投入。你在这一步踩过的那些从安装到阈值调整的坑后面换任何模型都要重踩一遍。希望这篇笔记能帮你绕开其中大部分直接到业务验证的阶段。本文还有配套的精品资源点击获取
返回列表