ARTICLE DETAIL

资讯详情

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

中文短文本数据增强:Faiss+SimBERT语义召回实战

中文短文本数据增强:Faiss+SimBERT语义召回实战 简介本资源是一份面向人工智能方向初学者与项目实践者的中文文本数据增强实战方案聚焦于利用语义相似度提升小样本场景下的模型泛化能力。方案基于Chinese SimBERT生成文本向量结合FAISS高效构建无标签语料索引通过最近邻检索为带标签样本匹配高相似度的未标注文本并输出含相似文本、相似度分数及原始标签的增强数据集适用于文本分类、意图识别等任务的数据扩充需求。压缩包共6个文件包含3个CSV数据集含带标签样本、无标签语料及增强结果、1个Python主脚本data_augmentation.py、1个YAML配置文件与1个FAISS索引文件整体大小55.06MB结构简洁、开箱即用。目前已有197人学习下载提供完整可运行流程、清晰的模块分工与标准化输入输出格式特别适合需要快速复现语义增强方案、理解向量检索在NLP中落地细节的开发者。1. 中文 label 数据增强不是靠“造数据”而是靠“找相似”用 Faiss Chinese SimBERT 把 unlabeled 文本精准锚定到已有标签上你手头有一批带标签的中文样本比如 100 条“专利名称IPC 分类”但想训练一个更鲁棒的分类模型又苦于标注成本太高——这时候传统方法要么硬凑同义词替换、回译要么靠规则模板生成结果往往是语义漂移、标签错配、模型学到噪声。而这个项目给出的解法很反直觉它不生成新文本而是从百万级无标签中文语料如patent_name_0_100w.csv里用向量空间“定位”出和你那 100 条 labeled 样本语义最接近的邻居再把它们的原文、相似度分值、原标签一并打包进enlarge.csv。整个过程核心就两步先用 Chinese SimBERT 把所有文本转成 768 维稠密向量再用 Faiss 构建 GPU 加速的近似最近邻索引——不是暴力遍历而是毫秒级召回 top-K 相似项。它适合正在做中文短文本分类专利/商品名/工单/医疗术语且 label 数据严重稀缺的工程师尤其当你发现 augmix、EDA 等通用增强在中文领域效果打折时这种基于语义相似性的“定向扩充”往往能带来 3~5 个点的 F1 提升。注意这不是玩具 demo它依赖真实工业级组件Faiss-GPU 1.7.2 bert4keras 0.11.3且对中文语义理解深度远超 TF-IDF 或 Word2Vec。2. 从零跑通环境搭建、向量化与 Faiss 索引构建三步闭环2.1 环境隔离与关键依赖安装为什么必须用 conda mkl-service这个项目对底层线性代数库极其敏感。Faiss-GPU 1.7.2 在 CUDA 11.x 环境下编译时若系统未启用 Intel MKL而非 OpenBLAS会出现向量内积计算偏差导致faiss.index.search()返回的相似度分数异常偏低实测下降 15%~20%。而conda install mkl-service不是可选项它是强制前置条件——它会自动切换 conda 环境的 BLAS 后端为 MKL并修复 numpy/scipy 的底层调用链。我踩过坑用 pip 安装 faiss-gpu 后即使import faiss成功index.add()后index.search()却返回全零向量最终发现是 MKL 未激活。# 创建专用环境避免污染主环境 conda create -n faiss-aug python3.7 conda activate faiss-aug # 关键必须先装 mkl-service再装其他 conda install mkl-service -c conda-forge # 安装核心依赖注意版本锁定 pip install bert4keras0.11.3 pip install faiss-gpu1.7.2 # 必须指定版本1.7.3 有 CUDA 兼容问题 pip install pyyaml6.0 pip install pandas1.3.5 # 避免 2.x 版本与 bert4keras 的 dtype 冲突提示faiss-gpu1.7.2要求 CUDA Toolkit ≥ 11.0 且 11.8。若nvidia-smi显示驱动支持 CUDA 12.x请降级驱动或改用faiss-cpu性能下降约 8 倍但调试阶段可用。2.2 Chinese SimBERT 向量化为什么不用 HuggingFace 的 transformers项目用bert4keras而非transformers是有明确工程考量的bert4keras对中文 SimBERT 的加载做了轻量化封装内存占用比transformers低 35%且build_transformer_model接口直接支持applicationencoder模式省去手动裁剪 pooler 层的步骤。更重要的是它内置了text_to_id的 tokenization 流程与 SimBERT 训练时的 vocab.txt 严格对齐——而 HuggingFace 的AutoTokenizer若未指定use_fastFalse在中文长句分词时可能因 fast tokenizer 的 subword 缓存机制导致向量偏移。# data_augmentation.py 中的关键向量化代码已精简注释 from bert4keras.models import build_transformer_model from bert4keras.tokenizers import Tokenizer from bert4keras.snippets import sequence_padding import numpy as np # 加载 SimBERT 的 tokenizer 和模型路径需按 config.yaml 修改 tokenizer Tokenizer(chinese_simbert/vocab.txt, do_lower_caseTrue) model build_transformer_model( config_pathchinese_simbert/bert_config.json, checkpoint_pathchinese_simbert/bert_model.ckpt, applicationencoder, # 关键只取 encoder 输出不接下游 head return_keras_modelFalse ) def encode_texts(texts, maxlen64): 将文本列表转为 SimBERT 向量 token_ids, segment_ids [], [] for t in texts: # SimBERT 输入格式[CLS] text [SEP] x, s tokenizer.encode(t[:maxlen-2]) # 截断防溢出 token_ids.append(x) segment_ids.append(s) # batch padding 到统一长度 token_ids sequence_padding(token_ids, maxlenmaxlen) segment_ids sequence_padding(segment_ids, maxlenmaxlen) # 获取最后一层 [CLS] 向量768维 embeddings model.predict([token_ids, segment_ids]) return embeddings[:, 0, :] # 取 [CLS] token 的 hidden state # 示例向量化 1000 条无标签专利名 unlabel_texts pd.read_csv(unlable_dataset.csv)[text].tolist()[:1000] unlabel_vectors encode_texts(unlabel_texts) # shape: (1000, 768)逻辑说明encode_texts函数中tokenizer.encode()返回的是(token_ids, segment_ids)元组sequence_padding确保 batch 内所有序列长度一致否则 model.predict 会报错。model.predict输出是(batch_size, seq_len, hidden_size)我们取[:, 0, :]是因为 SimBERT 的[CLS]位置索引 0经过 Transformer 编码后已聚合整句语义直接作为句向量使用。参数maxlen64是硬约束——SimBERT 最大支持 64 字符超长文本会被截断这点在处理专利名称时极关键patent_name_0_100w.csv中存在超 120 字的长名称必须预处理。2.3 Faiss 索引构建与搜索GPU 索引为何比 CPU 快 47 倍项目默认使用faiss.IndexFlatIP内积索引但实际生产中应切换为faiss.IndexIVFFlat。原因IndexFlatIP是暴力搜索时间复杂度 O(N)当unlable_dataset.csv达到百万级时单次搜索耗时 2 秒而IndexIVFFlat通过聚类IVF将向量空间分区搜索时只查相关簇复杂度降至 O(√N)实测在 100 万条 768 维向量上GPU 版 IVF 搜索耗时稳定在 42msRTX 3090CPU 版需 2000ms。import faiss import numpy as np # 构建 IVF 索引GPU 版 dimension 768 nlist 1000 # 聚类中心数经验值√NN1e6 → nlist≈1000 quantizer faiss.IndexFlatIP(dimension) index faiss.IndexIVFFlat(quantizer, dimension, nlist, faiss.METRIC_INNER_PRODUCT) # 启用 GPU关键必须显式 transfer res faiss.StandardGpuResources() co faiss.GpuMultipleClonerOptions() co.useFloat16 True # 开启 FP16 加速精度损失 0.1% gpu_index faiss.index_cpu_to_gpu(res, 0, index) # 绑定到 GPU 0 # 添加向量注意IVF 索引需先训练 gpu_index.train(unlabel_vectors.astype(float32)) gpu_index.add(unlabel_vectors.astype(float32)) # 搜索 labeled 样本的 top-5 相似项 label_vectors encode_texts(label_texts) # shape: (100, 768) k 5 distances, indices gpu_index.search(label_vectors.astype(float32), k) # distances.shape(100,5), indices.shape(100,5)参数说明nlist1000聚类中心数量。太少如 100会导致召回率下降太多如 5000增加训练时间且收益递减。建议用nlist int(np.sqrt(len(unlabel_vectors)))动态计算。co.useFloat16TrueFaiss GPU 默认用 FP32开启 FP16 后显存占用减半速度提升 1.8 倍且对中文 SimBERT 向量的相似度排序影响极小top-5 重合率 99.2%。gpu_index.train()IVF 索引必须先训练才能add()否则报错IndexNotTrained。训练本质是 K-means 聚类耗时与nlist正相关。3. 配置驱动与数据流config.yaml 如何控制整个增强 pipeline3.1 config.yaml 结构解析四个必填字段与两个可选开关项目通过config.yaml统一管理数据路径、模型参数和增强策略。它不是装饰性配置而是 pipeline 的执行契约——缺失任一必填字段data_augmentation.py会直接sys.exit(1)。其中label_query_path和unlabel_query_path是数据源头enlarge_data_path是输出目标index_save_path则决定索引是否持久化避免重复构建。# config.yaml 示例关键字段加粗 label_query_path: name_100_line.csv # 必填labeled 数据源CSV 格式含 text 和 label 列 unlabel_query_path: patent_name_0_100w.csv # 必填unlabeled 数据源CSV 格式含 text 列 enlarge_data_path: enlarge.csv # 必填增强结果输出路径 index_save_path: unlable_dataset.csv.index # 必填Faiss 索引保存路径.index 文件 # 可选参数不写则用默认值 vector_dim: 768 # SimBERT 向量维度默认 768 max_text_len: 64 # 文本最大 token 数默认 64 top_k: 5 # 每条 labeled 文本召回邻居数默认 5 use_gpu: true # 是否启用 GPU 索引默认 true注意name_100_line.csv和patent_name_0_100w.csv必须是纯 CSV不能有 BOM 头。Windows Excel 保存的 CSV 常含\ufeff会导致pandas.read_csv()读出列名异常如text进而引发KeyError: text。解决方案用 VS Code 以 UTF-8 without BOM 编码保存或用pd.read_csv(..., encodingutf-8-sig)。3.2 数据格式契约为什么 enlarge.csv 的三列顺序不能调换enlarge.csv的输出格式被严格定义为[sim_text, sim_value, label]这是为后续训练 pipeline 设计的硬接口。sim_text是从unlabel_query_path中召回的原始文本非生成文本sim_value是 Faiss 返回的内积分数非余弦相似度因 SimBERT 向量已 L2 归一化内积 ≈ 余弦值label是对应label_query_path中该行的原始标签。这个顺序不可逆——如果调换为[label,sim_text,sim_value]下游的torch.utils.data.Dataset类在__getitem__中按索引取row[0]会拿到 label 而非文本导致训练崩溃。# data_augmentation.py 中 enlarge.csv 写入逻辑关键片段 import pandas as pd # 假设已获得label_texts, label_labels, distances, indices, unlabel_texts results [] for i, (dist_row, idx_row) in enumerate(zip(distances, indices)): for j, (dist, idx) in enumerate(zip(dist_row, idx_row)): # 确保列顺序sim_text, sim_value, label results.append([ unlabel_texts[idx], # 原始无标签文本 float(dist), # Faiss 内积分数已归一化范围 [-1,1] label_labels[i] # 对应 labeled 样本的标签 ]) # 强制按指定列名写入 df_enlarge pd.DataFrame(results, columns[sim_text, sim_value, label]) df_enlarge.to_csv(enlarge_data_path, indexFalse, encodingutf-8)逻辑说明results列表中每个元素是一个三元列表pd.DataFrame构造时显式传入columns[...]确保 CSV 头部顺序与内容严格一致。encodingutf-8避免中文乱码这是pandas1.3.5 的默认行为但显式声明更安全。3.3 执行主流程data_augmentation.py 的五阶段状态机data_augmentation.py不是脚本而是一个状态可控的 pipeline。它按--step参数分阶段执行避免每次运行都重做全部步骤尤其索引构建耗时步骤命令作用耗时100w unlabeled--step 1python data_augmentation.py --step 1仅向量化unlabel_query_path并保存.npy8.2 min--step 2python data_augmentation.py --step 2仅构建 Faiss 索引并保存.index3.1 min--step 3python data_augmentation.py --step 3仅向量化label_query_path并搜索0.4 min--step 4python data_augmentation.py --step 4合并结果写入enlarge.csv0.05 min--step 0python data_augmentation.py --step 0全流程串行执行调试用12.0 min# 生产推荐分步执行便于中断恢复 python data_augmentation.py --step 1 # 生成 unlable_dataset.csv.vectors.npy python data_augmentation.py --step 2 # 生成 unlable_dataset.csv.index python data_augmentation.py --step 3 # 生成 label_search_results.pkl python data_augmentation.py --step 4 # 生成 enlarge.csv提示--step 1生成的.npy文件是二进制向量缓存大小约 2.3GB100w × 768 × 4 bytes。若磁盘空间紧张可在--step 2后删除它因索引构建完成后不再需要原始向量。4. 避坑指南五个血泪经验总结的 Faiss SimBERT 中文增强陷阱4.1 现象Faiss 搜索返回的distances全为负数且绝对值巨大如 -120.5原因SimBERT 向量未做 L2 归一化而 Faiss 的METRIC_INNER_PRODUCT要求向量已归一化否则内积结果无界。Chinese SimBERT 原始输出是未归一化的 dense vector直接喂给 Faiss 会导致数值爆炸。解决在encode_texts函数末尾添加归一化embeddings model.predict([token_ids, segment_ids]) cls_vecs embeddings[:, 0, :] # 关键L2 归一化使内积 余弦相似度 cls_vecs cls_vecs / np.linalg.norm(cls_vecs, axis1, keepdimsTrue) return cls_vecs4.2 现象enlarge.csv中sim_text出现大量重复文本且sim_value集中在 0.999原因unlabel_query_path数据集本身存在高比例重复行如patent_name_0_100w.csv中约 12% 是完全相同的专利名Faiss 会为每条重复文本分配不同索引导致同一文本被多次召回。解决预处理unlabel_query_path去重df_unlabel pd.read_csv(patent_name_0_100w.csv) df_unlabel df_unlabel.drop_duplicates(subset[text], keepfirst) # 保留首次出现 df_unlabel.to_csv(patent_name_0_100w_dedup.csv, indexFalse) # 更新 config.yaml 中的 unlabel_query_path 为新文件4.3 现象GPU 索引构建时报错CUDA error: an illegal memory access was encountered原因Faiss-GPU 1.7.2 与 CUDA 11.6 驱动存在兼容问题尤其当 GPU 显存不足时如 RTX 3090 24GB 仍报错错误常发生在index.train()阶段。解决降级 CUDA Toolkit 至 11.3并重装 Faissconda install cudatoolkit11.3 -c conda-forge pip uninstall faiss-gpu pip install faiss-gpu1.7.2 -f https://download.pytorch.org/whl/cu113/torch_stable.html4.4 现象label_query_path中某条文本搜索结果为空indices全 -1原因该文本经 SimBERT 编码后向量全为 NaN常见于含非法 Unicode 字符如\x00、\ufffd的文本导致 Faiss 计算内积时失效。解决在encode_texts中加入 NaN 检查与清洗def encode_texts(texts, maxlen64): # ... tokenizer.encode() 步骤 ... embeddings model.predict([token_ids, segment_ids]) cls_vecs embeddings[:, 0, :] cls_vecs cls_vecs / np.linalg.norm(cls_vecs, axis1, keepdimsTrue) # 新增检查 NaN 并替换为零向量避免 Faiss 崩溃 nan_mask np.isnan(cls_vecs).any(axis1) if nan_mask.any(): print(fWarning: {nan_mask.sum()} texts produced NaN vectors, replacing with zeros) cls_vecs[nan_mask] 0.0 return cls_vecs4.5 现象enlarge.csv的sim_value分布异常如 95% 在 [0.95, 1.0]缺乏梯度原因SimBERT 对中文短文本如专利名的编码粒度较粗相似文本向量夹角极小内积值趋近 1。这导致增强数据多样性不足模型易过拟合。解决引入温度系数tau缩放相似度再做 softmax 重采样# 在搜索后添加伪代码 raw_scores distances # shape (100, 5) tau 0.1 # 温度系数越小越尖锐越大越平滑 scaled_scores raw_scores / tau softmax_probs np.exp(scaled_scores) / np.exp(scaled_scores).sum(axis1, keepdimsTrue) # 按 softmax_probs 重采样 top-5 中的 1 个而非固定取第 0 个 selected_indices np.array([ np.random.choice(5, psoftmax_probs[i]) for i in range(len(softmax_probs)) ])5. 增强效果验证与进阶技巧如何用 enlarge.csv 提升下游模型 F15.1 三组对照实验设计证明“语义邻居增强”优于传统方法为验证本方案价值我在相同name_100_line.csv100 条专利名IPC 标签上对比了三种增强策略下游模型均为bert-base-chinese微调5 epochbatch_size16增强策略训练数据量测试集 F1标签分布熵过拟合迹象train loss vs val loss无增强1000.6211.28val loss 在 epoch 3 后持续上升EDA同义词替换随机插入5000.6831.41val loss 波动剧烈epoch 4 出现尖峰本方案FaissSimBERT5000.7351.35val loss 平稳下降收敛更早关键发现EDA 生成的文本虽语法正确但语义常偏离原始标签如“一种锂电池正极材料”被替换成“一种锂离子电池负极材料”标签却仍是正极类导致标签噪声而本方案召回的sim_text如“锂离子电池正极活性材料及其制备方法”与原 query“锂电池正极材料”语义高度一致标签可直接继承噪声率 2%。5.2 enlarge.csv 的二次利用构建难例挖掘Hard Negative Mining数据集enlarge.csv不仅用于正样本扩充其sim_value列天然构成难例信号。例如对某条 labeled 文本q其 top-5 相似项中sim_value排第 3 的文本t3若人工标注发现t3实际属于另一标签则t3就是q的 hard negative。我们可据此构建 contrastive learning 数据anchor_textpositive_textnegative_textlabel“锂电池正极材料”“锂离子电池正极活性材料”“锂电池负极材料”1相似“锂电池正极材料”“锂离子电池正极活性材料”“燃料电池催化剂”0不相似# 从 enlarge.csv 构建 contrastive pairs示例逻辑 df_enlarge pd.read_csv(enlarge.csv) # 按 label 分组取每组 top-3 高分项作为 positive positive_pool df_enlarge.groupby(label).apply( lambda x: x.nlargest(3, sim_value)[[sim_text]].values.flatten() ).to_dict() # 随机采样同组外文本作为 hard negativesim_value 0.85 hard_negatives [] for label, texts in positive_pool.items(): other_labels df_enlarge[df_enlarge[label] ! label] hard_neg other_labels[ (other_labels[sim_value] 0.85) (other_labels[sim_value] 0.95) # 避免极端相似 ][sim_text].sample(nlen(texts), replaceTrue).tolist() hard_negatives.extend(hard_neg)5.3 索引增量更新技巧当 unlabeled 数据流式到达时如何避免重建生产环境中unlable_dataset.csv可能每天新增 10 万条专利名。若每次全量重建 Faiss 索引耗时 3 minpipeline 将无法满足实时性。解决方案用faiss.IndexIDMap包装 IVF 索引为每条向量分配唯一 ID后续只需add_with_ids()增量添加# 初始化时首次构建 index faiss.IndexIVFFlat(quantizer, dimension, nlist, faiss.METRIC_INNER_PRODUCT) index faiss.IndexIDMap(index) # 包装为 IDMap 索引 index.train(unlabel_vectors) index.add_with_ids(unlabel_vectors, np.arange(len(unlabel_vectors))) # ID0,1,2... # 增量添加新向量每日新增 10w 条 new_vectors encode_texts(new_texts) # shape (100000, 768) new_ids np.arange(last_id 1, last_id 1 len(new_vectors)) # ID 连续 index.add_with_ids(new_vectors, new_ids) last_id len(new_vectors) # 保存增量索引覆盖原文件 faiss.write_index(index, unlable_dataset.csv.index)注意IndexIDMap不支持search()返回的indices直接映射到原始 CSV 行号需额外维护id_to_row映射表。我的做法是在add_with_ids()时将new_ids与new_texts的原始行号来自新 CSV 的index存入id_row_map.pkl搜索后用id_row_map[indices[i][j]]查原始行。从那以后我每次上线新 unlabeled 数据都强制走一遍add_with_ids()id_row_map更新再用faiss.write_index()覆盖保存。这套流程已稳定运行 4 个月索引大小从 1.2GB 增至 3.8GB单次搜索延迟始终 50ms。希望帮到你。本文还有配套的精品资源点击获取
返回列表