恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
深度学习文本分类与聚类工具实战:从向量表示到半监督闭环
首页
资讯中心
/
深度学习文本分类与聚类工具实战:从向量表示到半监督闭环
深度学习文本分类与聚类工具实战:从向量表示到半监督闭环
发布时间:2026/10/9 3:08:03
简介针对深度学习文本分类与聚类任务这份可运行的实验工具包面向具备Python基础与机器学习概念的算法工程师、在校学生及NLP入门者帮助快速上手文本向量化、模型训练与无监督聚类。压缩包内共24个文件其中17个Python脚本构成主体覆盖数据清洗与预处理、Word2Vec词嵌入、CNN/LSTM分类器、K-means及自编码器聚类等关键环节另有2个pkl模型文件可直接加载使用2个txt说明与2个readme文档辅助理解1个Go转换脚本用于生成句向量整体仅61KB结构紧凑且易于二次开发。目前已有153人学习下载适用于课程设计、算法对比或工程原型验证。借助这套工具使用者可复现从词向量训练到分类/聚类的完整流程直观理解深度模型在短文本与长文本处理中的表现差异。各模块按功能拆分支持替换数据源、调整网络结构与聚类参数便于结合实际数据开展对比实验与针对性扩展。1. 拿到「基于深度学习的文本分类聚类工具.zip」先搞清楚它到底替你做了什么做 NLP 落地的人大概率都遇到过这种场景甲方丢过来一批历史工单、舆情评论或者法律文书开口就是“帮我分个类顺便看看都有哪些热点”然后补一句“最好能自动跑”。你心里清楚这事拆开就是两个步骤——先做文本分类把每条文本归到预设的标签里再做聚类在没有标签的情况下让模型自己找出文本的群组结构。市面上单做分类的工具很多单做聚类的脚本也不少但能把两条线揉进一个工程里、互相借力的才是这个 zip 里真正值钱的东西。这篇笔记不聊论文复现就聊怎么把这个工具包解压之后跑起来、参数往哪调、哪些地方会翻车。这个工具面对的是有明确诉求的从业者手里有几千到几十万条中文文本想快速得到一组可解释的类别划分同时希望在没有标注数据时先用聚类探一探底。适合的人群包括做舆情分析的运营、做工单分派的研发以及刚入门深度学习、想用一套完整代码练手的同学。它的核心思路并不神秘——分类模块通常用预训练语言模型或者 TextCNN 这类轻量网络聚类模块则落在 KMeans 或层次聚类上二者通过向量表示这一层打通。说白了你不需要从零造轮子只需要理解它怎么把文本变成向量、再把向量变成类别就能上手改。需要提前打个预防针这类 zip 包解压出来往往不是“双击就能用”的成品更像一套半成品框架。你要做的不是抱怨它不完善而是顺着它的目录结构把数据入口、模型权重、训练脚本三个关键点找到然后替换成你自己的语料。下面章节会按照“架构拆解 → 分类跑通 → 聚类调参 → 避坑 → 进阶”的顺序展开每一步都有可以抄的命令和代码。2. 拆解工具包分类和聚类两条线是怎么在一个工程里协同的2.1 先看目录结构别急着跑 train.py解压 zip 之后第一件事不是看 README 里写了什么高大上的算法名称而是用tree命令把目录结构打出来。一个设计合理的深度学习文本工具包通常会有data/、models/、utils/、configs/、scripts/这几个核心目录。data/里一般放着原始语料和预处理脚本models/里是网络结构定义configs/里是参数配置文件scripts/里是训练和推理的入口。如果某份代码把模型定义、数据处理、训练循环全堆在同一个文件里也不是不能跑但后面要改 loss 比例或者换数据集时你会想骂人。我一般会先看configs/目录下的 YAML 或者 JSON 文件因为那里藏着 90% 的调参入口。比如embedding_dim决定向量维度num_classes决定分类头输出大小kmeans_k决定聚类个数。如果工具包里的配置没有写明这些字段说明作者可能把参数硬编码在代码里了那你就得逐个脚本去翻。检查完目录下一个动作是验证 Python 环境——用python -m venv venv建一个干净环境然后pip install -r requirements.txt。遇到依赖冲突是常态常见的坑是pytorch版本和CUDA版本不匹配后面避坑章会专门说。2.2 两条线的数据流文本 → 向量 → 类别共享同一个表示层这个工具包的精髓在于“分类”和“聚类”不是两个孤立的模块而是共享同一个文本向量表示层。分类任务是有监督的它需要带标签的数据去训练一个分类器聚类任务是纯无监督的它只需要文本的向量表示就能跑。作者通常会把文本编码器单独抽出来比如用 BERT 的[CLS]向量或者 TextCNN 的池化输出作为文本向量然后分类头在这个向量上接一个全连接层做 softmax聚类模块则直接拿这个向量去算距离。这里有个设计上的讲究为什么要共享表示层而不是各训各的因为分类数据往往有限而聚类数据量大且无标注。先用有标签数据把编码器训出来再去给无标签数据生成向量做聚类聚出来的簇往往比直接用 TF-IDF 聚更有语义区分度。反过来聚类结果也能辅助分类——给那些置信度低的样本打上簇标签作为伪标签扩充训练集。这就是这个 zip 里最值得你花时间理解的部分。理解了这条数据流你后面调参时就明白改了编码器的输出维度分类和聚类的结果会同时变化因为它们在同一个向量空间里。2.3 工具包自带的 demo 脚本其实是你的第一个测试用例不要急着替换成自己的数据先用工具包自带的 demo 数据跑一遍完整流程。这能验证环境是否配通、代码路径是否完整、模型能否正常加载。通常scripts/下会有run_demo.sh或者demo.py这样的入口。跑 demo 的目的不是看准确率多高而是确认三件事数据加载是否正常、训练循环能否跑完一个 epoch、聚类模块能不能输出结果文件。如果 demo 都跑不通那大概率是环境问题而不是代码逻辑问题。跑通 demo 之后把输出结果打开看看——分类的预测结果应该是一个 CSV 文件里面有文本、真实标签、预测标签和置信度聚类的输出应该是一个带簇标签的 CSV。有的工具包还会在outputs/目录生成 TSNE 降维图这个图虽然只是可视化但对判断聚类效果好不好的作用远超你的预期——如果散点图里不同簇混成一团说明向量表示没学好这时候去调损失函数或者换预训练模型比调聚类参数有效得多。3. 用预训练模型跑通文本分类最小命令与三个必调参数3.1 替换自己的数据格式不对一切白搭绝大多数此类工具包期望的输入格式是 CSV 两列text和label。如果你手里的数据是 Excel 或者 JSON先花五分钟用 Python 转一下而不是手工改。这里给一个通用的转换脚本import pandas as pd import json # 读取原始 JSON 数据每条是 {content: ..., tag: ...} with open(raw_data.json, r, encodingutf-8) as f: raw_data [json.loads(line) for line in f] # 提取文本和标签这里假设标签字段是 tag文本是 content df pd.DataFrame([ {text: item[content], label: item[tag]} for item in raw_data if item.get(content) and item.get(tag) ]) # 去掉空文本和空标签 df df.dropna(subset[text, label]) # 标签统一转成字符串避免数字类别被当成数值 df[label] df[label].astype(str) # 按 8:1:1 切分训练集、验证集、测试集 train_df df.sample(frac0.8, random_state42) val_df df.drop(train_df.index).sample(frac0.5, random_state42) test_df df.drop(train_df.index).drop(val_df.index) # 分别保存编码统一用 utf-8-sig 避免 Excel 打开乱码 train_df.to_csv(data/train.csv, indexFalse, encodingutf-8-sig) val_df.to_csv(data/val.csv, indexFalse, encodingutf-8-sig) test_df.to_csv(data/test.csv, indexFalse, encodingutf-8-sig) print(f训练集 {len(train_df)} 条, 验证集 {len(val_df)} 条, 测试集 {len(test_df)} 条)逻辑说明这段代码做的是数据形态的标准化核心是dropna清洗和astype(str)的类型统一。random_state42固定随机种子保证每次切分结果一致否则你调参时没法对比。切分比例 8:1:1 是文本分类的常见做法如果你的数据量低于一万条建议改成 7:2:1 或者做 K 折交叉验证否则验证集的置信度不可靠。输出用utf-8-sig是为了让 Windows 上的 Excel 打开不产生乱码这在交付给非技术同事时特别重要。如果你的数据量超过十万条这一步建议用 Dask 或者分块写入避免一次性读入内存导致崩溃。参数说明frac0.8是采样比例random_state是随机种子subset指定去重列。如果你手里的数据标签分布极度不均衡比如 99% 都是“无关”类那这里的随机切分会把少数类几乎全切到验证集里。解决办法是改成按标签分层采样用train_test_split的stratify参数后面避坑章会展开。3.2 训练命令里那些看着就烦的参数到底哪个最影响结果数据准备好之后进入训练阶段。常见的训练入口是scripts/train.py命令行参数一般长这样python scripts/train.py \ --train_data data/train.csv \ --val_data data/val.csv \ --model_name bert-base-chinese \ --num_classes 10 \ --batch_size 16 \ --epochs 5 \ --lr 2e-5 \ --max_length 128 \ --device cuda:0逻辑说明--model_name指定预训练模型权重bert-base-chinese是中文场景的默认选择如果你的文本是领域性极强的法律或医疗文本换成hfl/chinese-roberta-wwm-ext效果通常会更好。--max_length控制句子截断长度128 对短文本足够但如果你的工单平均长度超过 200 字需要调到 256 甚至 512代价是显存占用翻倍。--lr 2e-5是 BERT 微调的标准学习率如果你用的是 TextCNN 这类从零训练的模型学习率可以放大到 1e-3 量级。参数说明这里最关键的两个参数是--batch_size和--lr。batch size 太大容易显存溢出OOM太小则模型收敛慢且容易震荡。我个人的经验是压缩到 BERT 级别的模型batch size 在 16 到 32 之间表现最稳。学习率这一项BERT 用 2e-5 是经验值但如果你发现训练 loss 不下降先别急着调学习率看看是不是数据预处理出了问题。--device cuda:0指定 GPU如果你没有 GPU 而被迫用 CPU 跑 BERT建议把--model_name换成tinybert或者直接换 TextCNN否则一个 epoch 可能要跑几个小时。训练过程中一定要盯着验证集的 loss 而不是准确率。准确率在类别不均衡时会有极大欺骗性——如果你的数据里 90% 是 A 类模型全部预测成 A 类也能有 90% 准确率但 loss 会暴露出它其实什么都没学会。如果验证 loss 在第二个 epoch 就开始回升而训练 loss 还在下降那就是过拟合了这时候调低 epoch 数或者加早停比调学习率更有效。3.3 推理脚本把你的模型从“会做题”变成“能干活”训练完模型需要用它去预测没有标签的新数据。推理脚本的输出要保留原始文本、预测标签和置信度方便后续人工审核import torch from transformers import AutoModelForSequenceClassification, AutoTokenizer # 加载训练好的模型和分词器 model_path outputs/checkpoint-best tokenizer AutoTokenizer.from_pretrained(model_path) model AutoModelForSequenceClassification.from_pretrained(model_path) model.eval() # 读取待预测数据这里假设是一列没有标签的文本 import pandas as pd new_data pd.read_csv(data/unlabeled.csv, encodingutf-8-sig) texts new_data[text].tolist() # 逐条预测并记录置信度 results [] with torch.no_grad(): for text in texts: inputs tokenizer(text, truncationTrue, max_length128, return_tensorspt) outputs model(**inputs) probs torch.softmax(outputs.logits, dim-1) pred_class torch.argmax(probs, dim-1).item() confidence probs[0][pred_class].item() results.append({text: text, label: pred_class, confidence: confidence}) # 导出结果 pd.DataFrame(results).to_csv(outputs/pred_results.csv, indexFalse, encodingutf-8-sig)逻辑说明推理阶段必须用torch.no_grad()包裹否则会构建计算图导致内存泄漏。truncationTrue和max_length128要和你训练时的配置保持一致否则文本截断长度不一致会降低预测质量。softmax把 logits 转成概率分布confidence就是最大概率值这个值后面会被聚类和半监督流程用到。参数说明AutoModelForSequenceClassification会自动加载分类头from_pretrained会读取保存的权重和配置文件。如果你训练时用的是自定义模型结构这里需要改成加载对应模型的函数整个环节最容易出的问题就是训练和推理的模型类不一致。建议输出的 CSV 增加一列confidence的阈值标记比如低于 0.6 的样本打上“需人工复核”这是文本分类落地保命的习惯。4. 聚类模块实操从 KMeans 到层次聚类的参数收敛4.1 用分类模型的编码器生成向量这一步决定了聚类的上限聚类模块最常被人忽略的是向量表示的质量。很多人直接拿 TF-IDF 做聚类效果差强人意——因为 TF-IDF 是词袋模型无法捕捉“苹果”和“iPhone”这种语义等价关系。这个工具包既然已经有训练好的分类编码器就应该用它来提取文本向量import torch import numpy as np from transformers import AutoModel, AutoTokenizer # 加载分类模块的编码器部分不要加载分类头 encoder_path outputs/checkpoint-best tokenizer AutoTokenizer.from_pretrained(encoder_path) encoder AutoModel.from_pretrained(encoder_path) encoder.eval() # 假设你已经有了所有待聚类的文本 import pandas as pd texts pd.read_csv(data/unlabeled.csv, encodingutf-8-sig)[text].tolist() # 批量编码注意这里用 [CLS] 向量 vectors [] with torch.no_grad(): for text in texts: inputs tokenizer(text, truncationTrue, max_length128, return_tensorspt) outputs encoder(**inputs) # [CLS] 向量是最后一层隐藏状态的第一个 token cls_vec outputs.last_hidden_state[:, 0, :].numpy() vectors.append(cls_vec) vectors np.vstack(vectors) np.save(outputs/text_vectors.npy, vectors) print(f向量矩阵形状: {vectors.shape})逻辑说明AutoModel加载的是不带分类头的预训练模型输出是last_hidden_state形状为[batch_size, seq_len, hidden_size]。取[:, 0, :]就是 [CLS] token 的向量在 BERT 类模型里这个向量经过预训练已经聚合了整个句子的语义信息。注意这里做了.numpy()转换因为后续用 sklearn 做聚类不需要保留梯度。参数说明如果你发现聚类效果不理想最先应该尝试的不是调聚类算法参数而是换一种向量池化策略。last_hidden_state的第一维不是唯一选择你也可以对整个序列做 mean pooling平均所有 token 的向量或者用outputs.pooler_output。实践里 [CLS] 向量适合短文本mean pooling 对长文本更稳。另外向量维度一般是 768这个数字会在后面的 KMeans 距离计算中直接影响性能——维度越高距离越稀疏聚类越不稳定。4.2 选择聚类算法KMeans 适合快速探索层次聚类适合小样本向量生成之后就到了算法选型环节。这个工具包通常同时提供 KMeans 和层次聚类两种实现。KMeans 快、省内存适合几万条以上的数据层次聚类慢但不需要预设类别数适合几千条以内的数据。来看具体代码from sklearn.cluster import KMeans from sklearn.cluster import AgglomerativeClustering import numpy as np # 加载上一步生成的向量 vectors np.load(outputs/text_vectors.npy) # KMeans 聚类先定一个 K 值 kmeans KMeans(n_clusters8, random_state42, n_init10) kmeans_labels kmeans.fit_predict(vectors) # 层次聚类不预设簇数量用距离阈值控制粒度 hierarchical AgglomerativeClustering( n_clustersNone, distance_threshold0.8, linkageward ) hier_labels hierarchical.fit_predict(vectors) # 保存聚类结果 import pandas as pd texts pd.read_csv(data/unlabeled.csv, encodingutf-8-sig)[text].tolist() output_df pd.DataFrame({text: texts, cluster_kmeans: kmeans_labels, cluster_hier: hier_labels}) output_df.to_csv(outputs/cluster_results.csv, indexFalse, encodingutf-8-sig)逻辑说明KMeans 的n_clusters8是我们预设的簇数量这个值怎么定下面一小节专门讲。n_init10表示用 10 次不同的初始化取最优结果这是避免 KMeans 陷入局部最优的关键参数。层次聚类里的distance_threshold0.8意味着当两个簇的距离大于 0.8 时停止合并这样簇的数量会由数据自动决定linkageward是合并策略ward 方法最小化合并后的方差增量在文本向量上通常效果最好。参数说明两个算法对比着看是有意义的——如果 KMeans 分出的簇和层次聚类分出的簇高度一致说明你数据里的簇结构非常明显如果两者差异很大说明向量表示还不够好或者簇数量选取有问题。random_state42还是要固定否则每次跑结果都不一样下游分析没法做。层次聚类这里有个明显的坑它是基于距离矩阵计算的复杂度是 O(n^2)所以当你的数据超过 1 万条时这个代码会跑得极其痛苦。到时候要么降采样要么回到 KMeans。4.3 簇数 K 怎么定手肘法只是一个起点别把它当真理KMeans 最麻烦的就是设n_clusters这个值。网上教你用的“手肘法”——看 SSE簇内平方和的拐点——是一个参考但实际数据往往没有明显的拐点图是一条平滑的下降曲线怎么看怎么像“手肘”。跳出来看K 值选取本质是一个业务问题而不是数学问题。你得问自己我分出来的簇要拿去哪里用如果是给运营做分群5 到 8 个簇是刚好能人工理解的上限如果是给下游任务提供伪标签那么 20 到 30 个簇可能让每一簇更纯净。更务实的做法是跑一次轮廓系数来验证sklearn.metrics.silhouette_score计算每个样本到自身簇内和最近邻簇的距离差数值越接近 1 表示簇越紧凑。但它同样不是万能——考虑到你用的是 768 维向量高维空间里距离度量本身就不可靠。我的个人经验是把 K 从 2 到 20 扫一遍画出轮廓系数曲线然后选曲线最高点对应的 K 值同时人工抽看每个簇的 top 关键词和代表性文本。聚类这种东西算法只是半成品业务判断才是收尾。层次聚类的distance_threshold就更像一个“玄学”参数了——它直接决定树的切分位置。一种比较可靠的做法是把阈值从大到小扫一遍观察簇数量变化找到“数量开始暴涨”的那个点。比如阈值 0.9 时簇数是 60.8 时是 90.7 时突然跳到 25说明 0.8 附近是天然的簇边界。这时候用 0.8 作为阈值簇的数量是数据自己告诉你的比起你拍脑袋定的 K说服力强得多。5. 避坑清单标注不均衡、K 值玄学与编码翻车5.1 数据类别极度不均衡准确率让人误判loss 曲线才是照妖镜现象训练完模型测试准确率 92%你正打算交给业务方却在抽样检查时发现 A 类文本被大量错分成 “其他”。再看混淆矩阵B 类样本总量极少模型干脆把 B 类全体预测成了 A 类。原因经典的类别不均衡问题。分类器倾向于把样本推向数量占优的类别因为这样能让整体 loss 最小。准确率这个指标在失衡数据下天然失效——“全都预测成大类”就能获得高准确率但没有任何业务价值。解决在训练代码里给 loss 加类别权重。做法是在 loss 函数里传入weight参数或者在数据加载时用WeightedRandomSampler对少数类过采样。如果工具包里没有这个接口最省事的方案是复制一份少数类样本到训练集但不要复制太多以免过拟合。验证时不要只看准确率把精确率、召回率和 F1 打印出来。如果你的业务是工单分类那种“漏掉一个关键类别比错分十个普通类别更严重”的场景还要把少数类的召回率作为首要优化目标。5.2 聚类 K 值永远有争议数学指标和业务预期对着干现象你用轮廓系数选了 K12业务方打开聚类结果说“怎么这么多簇我要的五个维度呢”你解释这是数学上的最优业务方不认双方僵住。原因数学上的“最优 K”和业务上的“可解释 K”从来不是一回事。轮廓系数衡量的是向量空间里的几何紧密度但业务方要的是“新旧客群”“高价值/低价值”“投诉/建议”这种有业务意义的区分度。几何最优往往会劈开某个业务类别或者把两个业务类别合并成一簇。解决把 K 值决策变成一个对齐过程而不是一个计算过程。我通常是先跑 K5、K8、K12 三组结果每组都输出簇内 top 关键词和代表性样本然后让业务方看哪组的簇语义最清楚。这里有件事值得尝试把簇的标签交给 LLM 生成——让模型读簇里 20 条代表性文本提炼出一个不超过四个字的标签比人工慢慢翻文本高效得多。最终选哪个 K 不取决于算法取决于哪一版标签业务方拍板说“对这就是我们要的”。5.3 预训练模型下载失败或加载报错多半是网络和版本问题现象from_pretrained报OSError: Cant load config file或者卡在下载页面不动。更常见的是第一次能加载换台机器就加载不了。原因默认情况下transformers 会从 Hugging Face 远程仓库下载模型权重。国内网络环境下这个下载极不稳定时好时坏而且如果本地没有缓存每次加载都要重新下载一个几百 MB 的文件非常痛苦。解决提前把模型权重下载到本地修改代码里的模型加载路径指向本地目录。做法是先用huggingface-cli download或modelscope的下载工具把模型拉下来然后在AutoModel.from_pretrained(本地路径)里传绝对路径。如果你用的是轻量网络而不需要预训练权重那这个问题不存在。另外transformers 版本不一致也会导致加载报错——训练时用 4.x推理时用 3.x权重文件可能读不出来。建议在 requirements.txt 里锁死版本号别用pip install transformers这种不指定版本的安装方式。5.4 CSV 编码和 Excel 乱码你的中文文本被 GBK 悄悄替换了现象训练时 loss 正常下降但打开预测结果 CSV中文全部变成“锟斤拷”或者一个个问号。更隐蔽的是数据读取时 pandas 用默认编码读入导致文本变成乱码后再进模型语义信息全丢了准确率却看起来还行。原因Windows 环境下 Excel 默认用 GBK 打开 CSV而 Python 默认写入 UTF-8。另外 pandas 读取 CSV 时如果文件实际是 GBK 编码而没指定encodingutf-8就会产生乱码或抛 UnicodeDecodeError。解决写入文件时统一用encodingutf-8-sig带 BOM 头Excel 就能正确识别。读取数据时先用file命令或者在 Python 里试几种编码来探测文件真实编码不要假定一定是 UTF-8。保险起见在所有to_csv和read_csv的地方显式声明编码参数用utf-8-sig做写入、用utf-8做读取。如果遇到 GBK 文件读取时可以指定encodinggbk但需要额外处理因 GBK 解码失败导致的报错加一个errorsignore参数能避免崩溃。5.5 显存溢出和训练中断batch size 与梯度累积的取舍现象训练跑到第三个 epoch突然蹦出CUDA out of memory。你调小 batch size 到 4不 OOM 了但训练速度肉眼可见地变慢而且 loss 曲线开始剧烈震荡。原因batch size 太小会引入过多的梯度噪声导致收敛不稳定太大又超过显存容量。这是深度学习的一个经典矛盾尤其在 BERT 这类大模型上表现突出。解决不要只调 batch size用梯度累积来补偿。具体做法是把 batch size 设成 4然后每 8 步做一次优化器更新等效于 batch size32 的效果。代码上就是optimizer.step()之前判断step % accumulation_steps 0。这个技巧在不少分类工具包里已经内置了参数名通常叫gradient_accumulation_steps。如果工具包没实现你可以自己在训练循环里加。此外用torch.cuda.empty_cache()在验证前清一下缓存也能稍微缓解显存压力但这不是解决问题的根本办法。6. 进阶半监督迭代把分类器当聚类结果的校验器跑通了基础流程之后这个工具包真正能发挥超出预期的价值是在“分类-聚类”之间形成闭环的地方。具体做法是利用聚类结果生成伪标签扩充训练集再反馈给分类器迭代训练。这个技巧非常适合标注数据少于一万条、但无标注文本堆积如山的场景。操作流程是有章法的。第一步用已有标注数据训练一个初始分类器准确率不需要高60% 就够。第二步用这个分类器给无标注数据预测标签同时记录置信度。第三步把置信度高于 0.7 的无标注数据和它们的预测标签作为伪标签样本加入训练集。第四步用扩充后的训练集重新训练分类器。注意这里有个安全阀——置信度阈值不要设太低否则错误的伪标签会污染模型。0.6 到 0.7 是经验值具体数字要观察加入伪标签后验证集的 F1 变化如果 F1 提升说明伪标签质量合格如果 F1 下降阈值要提高或暂停加入。聚类在这个闭环里也有角色。很多无标注文本的预测置信度都低于 0.5说明分类器对它们毫无把握。这时候把这些低置信度文本送去做聚类每一簇里取距离中心最近的几条文本人工审核后给整簇打上一个标签就变成了一批“半手工”标注数据。这个流程比逐条标注高效得多——簇里的文本在语义上接近一条标注能代表一簇的标注这就是层次聚类在这个环节里存在感最强的地方。簇数量不宜太大8 到 15 个为宜因为簇越多人工审核的次数就越多收益会被成本吃掉。我个人的习惯是不会一次把所有伪标签都灌进训练集而是分两批灌每批灌完重新训练并评估。这相当于给模型的自我纠错留了缓冲期。如果第二批灌进去之后验证集 F1 反而下降了那就说明前面某批伪标签里的噪声占比过高需要回滚到上一轮的模型权重。这就是我常说的“后悔药”机制——训练时每跑完一个验证集表现最好的 epoch保存一次 checkpoint这样伪标签迭代失败时还能救得回来。关于阈值选择还有一个小技巧把置信度低于 0.5 的样本直接剔除出聚类只保留 0.5 到 0.7 之间的样本。因为这些中段置信度的样本是分类器犹豫不定的区域反而是聚类最能帮上忙的地方——分类器高置信度判对的样本聚类也会把它们聚到正确簇里并没有增量价值。把握住“分类器犹豫的地方交给聚类打底、聚类信心不足的地方拉回分类器投票”这个分工工具包里的两个模块就真的活起来了。这套方案做下来从解压 zip 到产出第一轮可交付的分类结果熟练的话一个下午能完成。重要的是你心里有数分类和聚类的边界不在于哪个模型更高级而在于你有一批标签还是没标签。作为从业者这个工具最值得你投入的不是它现成的效果而是吃透它在分类与聚类之间的这条向量通路。希望上面的参数和踩坑记录能帮你少走一段弯路。本文还有配套的精品资源点击获取