简介:基于Python搭建的多模态虚假新闻检测项目,融合文本与图像特征对新闻真实性进行自动识别,面向计算机、人工智能、通信工程、自动化等专业的高校学生和开发者。资源可在毕设答辩、课程设计、项目初期演示中直接使用,也适合作为NLP与多模态学习的练习素材。压缩包内共39个文件,包含16个Python源码文件、4份Markdown说明文档、4个TXT文本文件、3个Shell脚本、3个TSV数据文件、2个JSON配置、2个tfevents训练日志、1个Jupyter Notebook、1个checkpoint模型检查点及许可证文件,整体约350KB,目录涵盖模型、代码、文档等模块,便于按需查看。已有296人学习下载。源码经测试运行成功,包含基于BERT的多模态识别、LightGBM与CatBoost融合、预测脚本及环境依赖说明,下载后阅读README可快速了解结构并运行,适合在此基础上扩展新闻检测功能。代码结构清晰,关键步骤有注释,方便定位与修改。
1. 用 Python 做虚假新闻多模态检测:BERT 向量 + 树模型为什么比单模型更稳
用 Python 做虚假新闻检测,最容易翻车的地方其实不是模型选型,而是把 BERT 当成一个黑匣子:数据丢进去,准确率出不来,也不知道该调哪。这个资源走的是「多模态识别」路线,但这里的多模态不是图文拼接,而是把文本语义当一个模态、把统计和元信息特征当另一个模态,最后用 BERT 向量 + LightGBM/CatBoost 混合模型去融合判断。工程里包含 bert-final.py、lgb_cat_blend_lb9546.py、predict_test.py 和一套 TensorFlow 格式的 BERT 预训练权重,训练、推理、依赖安装脚本都齐。适合计科、人工智能方向拿来做毕设或课程设计,也适合想复现一整套文本检测管线的入门者照着跑。
2. 项目骨架与数据流:把源码包拆成配置、训练、推理三条线
解压 Multi-domain-fake-news-detection-master.zip 之后不要急着跑训练,先花十分钟把文件按职责分清楚。这个工程的调用关系其实很清晰:先装依赖,再训练 BERT 抽取语义向量,再训练树模型做融合分类,最后用 predict_test.py 出结果。顺序错了,后面每一步都会报让你摸不着头脑的错。
2.1 文件清单与职责划分:从 req.sh 到 predict_test.py,每份文件的角色
拿到源码包后,我习惯先列一遍文件,把「配置类、训练类、推理类」分到三条线上。下面这个表是我对照本项目目录整理的角色划分,你解压后可以对着勾一遍:
| 文件/目录 | 类型 | 在管线里的角色 |
|---|---|---|
req.sh/packages.txt | 环境配置 | 安装 Python 依赖,声明了项目所需的包列表 |
bert-final.py | 训练脚本 | BERT 微调主脚本,负责加载预训练权重、微调并抽取语义向量 |
lgb_cat_blend_lb9546.py | 训练脚本 | 读取 BERT 向量与统计特征,训练 LightGBM 和 CatBoost 并混合 |
predict_test.py | 推理脚本 | 对测试集做预测,输出概率或类别结果 |
train.sh/test.sh | 一键脚本 | 把训练和测试命令串起来,按顺序执行 |
model/tf_bert_model | 模型目录 | 放 TensorFlow 格式的 BERT 预训练权重 |
code | 子模块目录 | 一般放数据加载、特征构造、工具函数等 |
catboost_info | 运行产物 | CatBoost 训练日志和临时文件目录 |
README.md | 说明文档 | 作者写的运行说明、环境要求和参数备注 |
作者的 README 往往会把踩过的坑写在里面,下载后先打开 README.md,比在网上搜别人转述的第二手经验可靠得多。我习惯先看 req.sh 里锁了哪些库,再去结合 README 确认 Python 版本,因为 BERT 这类老工程的网络结构改动大,依赖版本差一个小版本都可能起不来。
接下来在项目根目录执行一次目录确认,把路径和环境看清楚:
cd Multi-domain-fake-news-detection-master tree -L 2 python --version这段命令的作用是确认你当前所在目录就是项目根目录,同时看出一级目录结构是否完整。tree 不存在时可以用find . -maxdepth 2 -type d代替;python --version用于确认解释器版本,BERT 相关代码用 Python 3.6 或 3.7 最常见,如果你本机默认是 3.11,后面依赖安装很容易出现不兼容。
2.2 训练与推理的调用链:train.sh 和 test.sh 到底谁先执行
整个工程不是单文件孤军奋战,主流程走的是「req.sh → bert-final.py → lgb_cat_blend_lb9546.py → predict_test.py」这条链。这里我按这类项目最常见的脚本结构还原一下 train.sh,你解压后看到的行数可能更多,但主线就是这三步:
#!/bin/bash # 1. 先按 packages.txt 安装依赖 bash req.sh # 2. 用 BERT 微调文本编码器,并保存抽取好的向量文件 python bert-final.py \ --data_dir data/fake_news.csv \ --bert_dir model/tf_bert_model \ --max_seq_length 128 \ --batch_size 16 \ --num_epochs 3 \ --output_dir output/bert_ckpt # 3. 训练 LightGBM + CatBoost 混合模型 python lgb_cat_blend_lb9546.py --mode train这里每一条命令都有明确目的:第 1 条是装依赖,第 2 条做语义编码,第 3 条做分类器融合。注意第 2 条的输出目录output/bert_ckpt,它和第 3 条脚本读取的特征向量目录是同一处,这个衔接关系断掉,混合模型就会拿到空文件或者直接报错。
test.sh 相对简单,一般就是把推理脚本指向测试集和模型产物:
#!/bin/bash python predict_test.py \ --test_file data/test.csv \ --model_dir output \ --output result.csv它的输入有两个关键来源:--test_file指向待检测的新闻数据,--model_dir指向第 2 步保存的 BERT 和第 3 步保存的树模型。推理脚本会按特征列名对齐数据,列名顺序错一位,lightgbm 就直接抛特征数不匹配。
2.3 model/tf_bert_model 目录:TensorFlow 预训练权重的组织方式
这个项目用的是 TensorFlow 格式的 BERT,不是 PyTorch 的 bin 文件。目录里通常包含三类文件:bert_config.json定义模型结构,vocab.txt是词表,bert_model.ckpt.data-*、bert_model.ckpt.index、bert_model.ckpt.meta是预训练权重。拿到权重后先确认文件完整性:
ls -lh model/tf_bert_model正常情况下列表里能同时看到bert_config.json、vocab.txt、bert_model.ckpt.index和bert_model.ckpt.data-00000-of-00001。如果只有配置文件没有权重文件,说明权重下载不完整,加载时会在 restore 阶段报 checkpoint 找不到。这套权重在模型加载阶段的作用是提供初始参数,微调之后覆盖的只是分类层和高层语义,底层的通用语言知识是从这份权重继承来的。
文件名里的tf_bert_model直接说明了它是 TensorFlow 1.x checkpoint 格式,这决定了项目里bert-final.py的加载方式用的是tf.train.Checkpoint或tf.train.Saver那一套。后面复现时选择虚拟环境版本,也要跟着这个格式走。
3. 微调 BERT 文本语义编码器:bert-final.py 的加载、训练与特征抽取
BERT 在这个工程里的角色不是最终分类器,而是把文本变成 768 维语义向量的编码器。所以我拿到 bert-final.py 后最关心三件事:预训练权重怎么加载、微调参数怎么设置、向量怎么抽取和保存。这三件事做完,后面的树模型才有像样的输入。
3.1 加载预训练权重:BertConfig、vocab.txt 和 ckpt 的配合方式
加载 TF 版 BERT 的标配步骤是:用BertConfig.from_json_file读结构配置,用FullTokenizer读词表做分词,再用 checkpoint 恢复权重。按项目的目录结构,加载部分的核心逻辑可以整理成下面这样,跑之前对照一下bert-final.py里的函数名,避免接口版本不一致:
# load_bert_encoder.py import tensorflow as tf from bert.tokenization import FullTokenizer from bert.modeling import BertConfig, BertModel BERT_DIR = "model/tf_bert_model" bert_config = BertConfig.from_json_file(BERT_DIR + "/bert_config.json") tokenizer = FullTokenizer(vocab_file=BERT_DIR + "/vocab.txt") init_checkpoint = BERT_DIR + "/bert_model.ckpt"这里三个输入的职责不同:bert_config.json告诉模型有多少层、多少头、隐层维度是多少;vocab.txt是分词用的词表,中英文混合文本的 tokenizer 全靠它切出词片;bert_model.ckpt是初始权重,微调时tf.train.Checkpoint.restore会把预训练参数恢复进来。三者缺任何一个,模型都起不来。
3.2 训练参数实测:max_seq_length、batch_size、learning_rate 的推荐配置
BERT 微调没有太多玄学,参数组合基本固定,但每个参数改错方向,损失值都会给你颜色看。这个项目在本地复现时,我建议先按下面这组参数起步:
| 参数 | 推荐值 | 说明 |
|---|---|---|
max_seq_length | 128 | 新闻标题和短文本 128 足够;长正文案例可以上调到 256 |
batch_size | 16 | 显存 6G 以下降到 8 或 4 |
learning_rate | 2e-5 | BERT 微调标准量级,1e-5 到 5e-5 之间调 |
num_epochs | 3 | 数据集小的情况下 2 ~ 4 轮收敛 |
warmup_proportion | 0.1 | 前 10% 步数线性预热,防止前期震荡 |
这些参数为什么这样配:BERT 微调时学习率如果给到普通网络的 1e-3,第一轮就会 loss 爆炸;max_seq_length也不是越大越好,所有 token 一次性进显存,长度翻倍显存占用接近翻倍,文本本身只有几十个字时设 512 纯属浪费。首次复现先用小参数把流程跑通,再改大值提精度,这是正确的调参顺序。
3.3 把 BERT 输出转成向量:CLS 池化与文本向量化函数
BERT 的骨干网络跑完,每个 token 位置都有一组隐层向量,但树模型需要的是一条文本一个向量,而不是二维矩阵。常见做法有两种:取[CLS]位的输出,或者对所有 token 向量做平均池化。工程里普遍用 CLS 池化,处理起来更直接:
# text_to_vec.py def bert_encode_and_pool(model, tokenizer, texts, max_seq_length=128): input_ids_all, mask_all, segment_ids_all = [], [], [] for text in texts: tokens = tokenizer.tokenize(text)[: max_seq_length - 2] tokens = ["[CLS]"] + tokens + ["[SEP]"] input_ids = tokenizer.convert_tokens_to_ids(tokens) padding_len = max_seq_length - len(input_ids) input_ids += [0] * padding_len mask = [1] * len(tokens) + [0] * padding_len segment_ids = [0] * max_seq_length input_ids_all.append(input_ids) mask_all.append(mask) segment_ids_all.append(segment_ids) input_ids_t = tf.constant(input_ids_all, dtype=tf.int32) mask_t = tf.constant(mask_all, dtype=tf.int32) segment_t = tf.constant(segment_ids_all, dtype=tf.int32) pooled = model.get_pooled_output() with tf.Session() as sess: sess.run(tf.global_variables_initializer()) return sess.run(pooled, feed_dict={ "input_ids:0": input_ids_t, "input_mask:0": mask_t, "token_type_ids:0": segment_t, })这段代码的关键点有三个:一是[CLS]和[SEP]占掉两个位置,所以原始 token 截断长度是max_seq_length - 2;二是mask里真实 token 记 1、padding 记 0,让模型忽略补零位;三是get_pooled_output()拿到的就是 CLS 位的池化向量,形状是(batch_size, 768)。如果发现向量维度不是 768,先检查bert_config.json里的hidden_size。
3.4 训练日志与中断恢复:看什么指标、存哪些 checkpoint
训练时损失值只是参考,更重要的是确认 loss 在 epoch 交界处有没有下降趋势。BERT 微调数据集往往只有几万条,三个 epoch 的损失曲线如果从一开始就乱跳,优先怀疑学习率过大或数据没随机洗牌。启动训练时直接用命令行参数跑:
python bert-final.py \ --data_dir data/fake_news.csv \ --bert_dir model/tf_bert_model \ --max_seq_length 128 \ --batch_size 16 \ --learning_rate 2e-5 \ --num_epochs 3 \ --output_dir output/bert_ckpt训练中断很常见,显存不够、断电、手动 Ctrl+C 都可能让过程停在中间。所以跑之前确保output/bert_ckpt目录是新建的,里面不要残留上一次的旧 checkpoint,否则恢复训练时会把旧权重当成初始化权重,白跑一轮。我的习惯是每 500 步打印一次 loss,同时把模型按 step 号存成ckpt-500、ckpt-1000这种命名,断点续跑时用--init_checkpoint指到最近一个 checkpoint。
4. 特征融合与 LGB + CatBoost 混合:lgb_cat_blend_lb9546.py 的集成细节
模型名lgb_cat_blend_lb9546.py里的 lb 我理解是 leaderboard 的缩写,blend 表示混合,这种命名在有竞赛经验的工程里很常见。它要解决的问题是:BERT 向量擅长语义,但虚假新闻里大量线索藏在标题长度、感叹号数量、URL 出现与否这些统计特征里,树模型恰好擅长这些。两者融合,准确率通常比单一 BERT 微调分类高出两个点以上。
4.1 为什么混合树模型:BERT 向量自带语义,但弱在统计特征
单纯用 BERT 最后一层向量接 softmax 做二分类,能抓住「这段话像不像谣言」,但抓不住「标题全是感叹号」这种强信号。虚假新闻数据集里,标题带多个感叹号、正文带陌生 URL、全是数字百分比,这些特征在统计层面有区分度,BERT 却把它们稀释在语义编码里了。
树模型对小数据集和稀疏特征非常友好,而且 LGB 和 CatBoost 两个模型虽然都是梯度提升树,内部处理类别特征和缺失值的方式不同,预测偏差也不一样。把它们对同一批样本的输出做加权平均,能抵消一部分各自的过拟合。这也是这个工程把 BERT 语义向量维度降到 768 之后,还要拼接统计特征再进树模型的原因。
4.2 特征工程:768 维 BERT 向量之外,还能拼哪些手写特征
特征构造是这套多模态方案里投入产出比最高的一步。我按典型虚假新闻样本的特性,整理了几个百搭的统计特征:
# features.py import re def build_stat_features(title: str) -> dict: if not isinstance(title, str): title = "" features = {} features["title_len"] = len(title) features["num_exclamation"] = title.count("!") features["num_question"] = title.count("?") features["has_url"] = 1 if re.search(r"https?://", title) else 0 features["digit_ratio"] = sum(c.isdigit() for c in title) / (len(title) + 1) features["word_count"] = len(title.split()) return features每个特征的设计理由都对应一类假新闻套路:num_exclamation抓标题党情绪化的感叹号轰炸;has_url抓诱导跳转链接;digit_ratio抓那种「99% 的人不知道」的伪科学数字化表达;title_len是对过短标题和超长标题做区分。这些特征拼接到 BERT 向量的尾部,组成最终输入,维度从 768 变成768 + len(stat_features)。
4.3 五折交叉验证与早停:blend 逻辑与权重选择
混合模型的重点不是分别训练两个模型,而是让两个模型在同一个验证集上打分,再按权重融合。标准做法是五折交叉验证,每一折分别训两个模型,各自对验证折出概率。核心逻辑整理如下:
# blend_core.py import numpy as np from sklearn.model_selection import StratifiedKFold import lightgbm as lgb from catboost import CatBoostClassifier def oof_blend(X, y, lgb_weight=0.6, seed=42): skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=seed) oof_lgb = np.zeros(len(y)) oof_cat = np.zeros(len(y)) for train_idx, valid_idx in skf.split(X, y): X_tr, X_va = X.iloc[train_idx], X.iloc[valid_idx] y_tr, y_va = y.iloc[train_idx], y.iloc[valid_idx] lgb_model = lgb.LGBMClassifier( n_estimators=800, learning_rate=0.05, num_leaves=31 ) lgb_model.fit( X_tr, y_tr, eval_set=(X_va, y_va), early_stopping_rounds=50, verbose=0 ) cat_model = CatBoostClassifier( iterations=800, learning_rate=0.05, depth=7, eval_metric="AUC", verbose=0 ) cat_model.fit( X_tr, y_tr, eval_set=(X_va, y_va), early_stopping_rounds=50 ) oof_lgb[valid_idx] = lgb_model.predict_proba(X_va)[:, 1] oof_cat[valid_idx] = cat_model.predict_proba(X_va)[:, 1] blend = lgb_weight * oof_lgb + (1 - lgb_weight) * oof_cat return blend, oof_lgb, oof_cat这段代码里有三个参数值得单独说。lgb_weight=0.6表示 LGB 结果占 60%、CatBoost 占 40%,这个比例不是拍脑袋定的,而是看两者在验证集上的 AUC 谁高谁低,高的那个给更高权重。early_stopping_rounds=50的作用是验证集指标连续 50 轮不提升就停止迭代,防止训练到后期过拟合。StratifiedKFold保证每折训练集和验证集里真实新闻、虚假新闻比例跟全量数据一致,类别不平衡时这个分层极其重要。
4.4 模型产物与推理输入:把 lgb 和 catboost 模型 save_model 出来
两个模型训练完成后,需要把模型权重和特征列名都存到磁盘,供 predict_test.py 加载。只存模型不存特征列顺序,是后面最容易翻车的地方:
# save_models.py import lightgbm as lgb # lgb_model 是上面训练好的 LGBMClassifier lgb_model.booster_.save_model("output/lgb_model.txt") # cat_model 是 CatBoostClassifier cat_model.save_model("output/cat_model.bin") # 把特征列名的顺序固定下来,推理时按这个顺序拼数据 with open("output/feature_cols.txt", "w", encoding="utf-8") as f: f.write("\n".join(X.columns))feature_cols.txt是推理阶段的对账工具。训练时 X 的列顺序是 「768 维 BERT 向量 + 统计特征」,推理时必须用完全一样的顺序重新拼接。单独把列名存成文件,就是为了避免每次靠记忆排序,一记错,推理阶段就会报特征数不匹配。以后每训练一版模型,这三个文件要同时更新,只更新其中一个,线上推理用的还是旧特征顺序,预测结果会失真。
5. 复现避坑:版本、路径、显存与 catboost_info 的五个翻车现场
这套工程整体不复杂,但复现时最容易出问题的往往不是算法,而是环境。下面的问题我基本都亲手踩过一遍,每条按「现象 → 原因 → 解决」写清楚,你可以直接对照排查。
5.1 TensorFlow 版本过高:AttributeError 和 contrib 找不到
现象:运行bert-final.py还没开始训练,直接报AttributeError: module 'tensorflow' has no attribute 'gfile',或者ModuleNotFoundError: No module named 'tensorflow.contrib'。
原因:源码基于 TensorFlow 1.x 编写,调用的是tf.gfile.GFile、tf.contrib这些 1.x 旧接口。TensorFlow 2.x 把tf.contrib整体移除,tf.gfile挪到了tf.io.gfile,老代码自然起不来。
解决:按照req.sh或packages.txt里的说明安装 TF 1.15 系列,优先考虑 GPU 版:
pip install tensorflow-gpu==1.15.0如果本机没有 GPU,就装 CPU 版tensorflow==1.15.0,BERT 微调慢一些但流程能走通。我建议建独立虚拟环境再装,不要动系统里的 Python,否则影响其他项目。
5.2 项目放在中文路径:vocab.txt 加载失败与乱码
现象:训练脚本能启动,但加载vocab.txt时报NotFoundError,路径打印出来是一串看不懂的转义字符。
原因:tf.gfile在 Windows 下对中文路径和特殊字符支持不稳定,路径里的中文被错误编码,文件定位失败。
解决:把整个项目目录复制到纯英文路径下,比如C:\projects\fake_news_detection,确保从根目录到项目目录中间没有中文和空格。用 PyCharm 的话,右键项目根目录执行 Mark Directory as Sources Root,让相对路径model/tf_bert_model能稳定解析。
5.3 显存溢出:CUDA_OUT_OF_MEMORY 不一定只能换显卡
现象:训练跑到几百步,报ResourceExhaustedError: OOM when allocating tensor,程序直接中断。
原因:BERT 是显存大户,max_seq_length设为 256、batch_size设为 32 时,一张 6G 显存的卡根本扛不住。12 层 Transformer 同时驻留显存,前向传播和反向传播都要保存中间激活值。
解决:优先降batch_size到 8 或 4,配合把max_seq_length降到 128。每条新闻标题通常不超过几十个字,128 的长度限制足够。显存还是不够时,再限制当前进程只看见一张卡:
CUDA_VISIBLE_DEVICES=0 python bert-final.py \ --max_seq_length 128 --batch_size 4CUDA_VISIBLE_DEVICES=0的作用是让程序只使用索引为 0 的显卡,避免 TensorFlow 把显存均匀吃满所有卡,造成其他显存被白白占用。
5.4 catboost_info 目录写不进:训练中断在第 0 步
现象:跑lgb_cat_blend_lb9546.py,CatBoost 刚开始训练就退出,报无法写入catboost_info或learn目录相关错误。
原因:项目压缩包里自带了一个catboost_info目录,解压后如果被设成只读,或者当前工作目录权限受限,CatBoost 默认的日志目录写不进去,模型直接拒绝训练。
解决:在脚本开头显式创建目录,或者指定一个可写目录。常见做法是在训练前加上:
import os os.makedirs("catboost_info", exist_ok=True)exist_ok=True表示目录已存在时不报错,不存在时自动创建。这样就把目录写权限问题挡在训练启动之前,后面 CatBoost 才能正常输出日志和中间模型。
5.5 特征维度对不上:lightgbm 报 feature mismatch
现象:predict_test.py推理时,lightgbm 报错提示数据特征数是 812,但模型期望 813 个特征。
原因:训练阶段特征列是「768 维 BERT 向量 + 统计特征」共 813 列,推理时 BERT 向量正常,但统计特征少拼了一列。最常见的是某条测试数据标题为空,特征构造函数返回的字典缺了title_len或word_count,导致拼接后矩阵少一列。
解决:推理前用训练阶段保存的feature_cols.txt强制对齐列顺序和列数量:
import pandas as pd feature_cols = open("output/feature_cols.txt", encoding="utf-8").read().splitlines() X_test = X_test.reindex(columns=feature_cols, fill_value=0)reindex会把缺失的列补 0,多出来的列自动丢弃,保证进入树模型的特征数量永远和训练时一致。我的习惯是把这行代码贴在推理脚本的数据准备部分,每次训练完模型先跑一遍 test.sh 验证维度,再交付到线上。
6. 把 predict_test.py 改造成单个新闻标题的实时检测入口
predict_test.py 默认是批处理模式,读一个 CSV 输出一个 CSV。但实际使用时,更多场景是拿到一条新闻标题,马上判断真假。把它改造成单条文本的检测函数,是最实用的二次开发。
6.1 predict_test.py 原本的输入输出格式
原脚本做的事是:读取测试集 → 拼接 BERT 向量和统计特征 → 加载output目录里的树模型 → 输出预测概率 CSV。改造方向是把「读整个测试集」换成「接收一条字符串」,内部流程保持不变。
6.2 封装 detect() 函数:从文本清洗到概率输出
改造后的入口函数可以这样组织:
# detect_news.py import pandas as pd import lightgbm as lgb from load_bert_encoder import load_bert_encoder, text_to_vec from features import build_stat_features bert_encoder = load_bert_encoder("model/tf_bert_model") lgb_model = lgb.Booster(model_file="output/lgb_model.txt") feature_cols = open("output/feature_cols.txt", encoding="utf-8").read().splitlines() def detect(news_text: str): clean_text = news_text.strip().replace("\n", " ") bert_vec_df = text_to_vec([clean_text], bert_encoder, max_seq_length=128) stat_feats = pd.DataFrame([build_stat_features(clean_text)]) X = pd.concat([bert_vec_df, stat_feats], axis=1)[feature_cols] prob = lgb_model.predict(X)[0] return ("虚假新闻" if prob >= 0.5 else "真实新闻"), float(prob)这个函数里去掉了 CSV 读取,把手工特征构造和 BERT 向量化封装成两个可复用函数,输出是(类别, 概率)元组。改造完毕后再包装成 FastAPI 或 Flask 接口就能对外服务。Booster直接加载output/lgb_model.txt,比重新训练快得多,适合部署时用。
6.3 部署前的检查清单
上线前至少验证三件事:拿一条真实新闻和一条典型假新闻各跑一次,确认输出类别符合直觉;检查概率值是否落在 0 到 1 之间,超出就说明模型输出没做 sigmoid;再确认feature_cols.txt是最新一版,不要和旧模型交叉使用。我自己就吃过这个亏,训练完新模型忘记同步特征列文件,线上推理用旧列顺序拼数据,静默地出了好几天错结果才被发现。从那以后,我每次训练完都强制走一遍「保存模型 → 保存特征列 → 跑单条检测函数 → 核对输出」,再交付给别人用。希望这条流程能帮到你,也祝你复现顺利。
本文还有配套的精品资源,点击获取