资讯详情

SimBERT中文文本增强实战:基于RoFormer的相似句生成与句向量检索

📅 2026/9/13 15:43:45 | 华诺云谱 👁 阅读
SimBERT中文文本增强实战:基于RoFormer的相似句生成与句向量检索
简介面向自然语言处理开发者的SimBERT中文相似语句生成代码复现包专注解决文本数据增强场景下的语义相似句构造问题适用于文本分类、情感分析、问答匹配等任务的数据扩充。压缩包共20个文件约338MB以Python脚本、XML配置、模型权重与说明文档为主其中py文件覆盖训练、生成、检索等完整流程checkpoint与data文件为预训练模型参数md与txt提供使用说明与配置指引。已有1860人学习下载。借助这份代码可快速运行SimBERT模型输入任意中文句子即可生成语义相近的多样表述同时可结合stage1、stage2等训练脚本理解模型的训练与微调过程并借助retrieval实现相似句检索。含bert_config.json、vocab.txt等必要配置目录结构清晰便于直接调试二次开发能显著提升中文NLP任务中的数据多样性与模型鲁棒性。1. 为什么中文NLP需要 SimBERT 这类文本数据增强工具文本分类、意图识别这类任务模型效果往往不是被网络结构卡住而是被训练数据卡住。一个二分类任务给你五百条标注数据BERT 微调之后在验证集上可能不错一上线上回流数据就明显抖动。原因不复杂标注样本里同一个意图的表达方式太单一模型学到的是“词面模式”而不是“语义模式”。中文尤其明显同一个意思可以有十几种说法句式稍微一变模型就不认了。SimBERT 解决的就是这个问题。它不是让你去多标数据而是用预训练模型把已有句子改写成语义相近但表达不同的新句子变相扩大训练集。这个仓库里的代码基于 RoFormer 架构权重是 chinese_roformer-sim-char_L-12_H-768_A-12和我之前用过的 SimBERT 原始版本同源但位置编码换成了旋转位置编码对长文本和句子对任务的适用性更好。适合谁用做文本分类、相似问匹配、问答意图扩充的算法工程师和在校学生。不需要自己预训练加载权重就能跑核心工作量在数据组织和参数调整上。2. RoFormer-Sim 模型结构与双任务训练原理2.1 从 BERT 到 RoFormer旋转位置编码解决了什么问题原始 BERT 用的是绝对位置编码把位置信息直接加到 token 向量上。这种方式在预训练阶段见过的长度范围内表现尚可一旦推理时遇到更长的序列位置编码的分布外问题会直接影响注意力权重的计算。RoFormer 的思路是把位置编码从“加法”改成“旋转”每一步向量都乘上一个由绝对位置决定、相对位置生效的旋转矩阵。核心性质是两个 token 的注意力分数只依赖于它们的相对位置差而不是各自所在的绝对位置这让外推长度变得更自然。这个仓库的权重名 roformer-sim-char 里的 char 表示字级别分词对中文来说比词级别更稳因为不需要额外维护分词词表OOV 问题天然规避掉了。L-12_H-768_A-12 对应的就是 BERT-base 规模12 层 Transformer、隐藏层 768 维、12 个注意力头。2.2 SimBERT 的双任务句向量相似度与 UNILM 生成如何共用一套参数SimBERT 在设计上把两个任务压进同一个模型一个是句向量相似度任务一个是文本生成任务。训练时每个 batch 包含若干对相似句子句子 A 和句子 B 分别经过模型得到 CLS 位置的句向量计算两个向量之间的内积相似度然后构造一个 batch 内的相似度矩阵让对应句对的分数高、非对应句对的分数低。这个矩阵的 label 是单位矩阵第 i 行第 j 列表示句子 A_i 与句子 B_j 是否互为改写关系。生成任务走的是 UNILM 的方式把句子对按[CLS] A [SEP] B [SEP]拼接通过修改 attention mask 让模型只能看到当前位置左侧的信息然后以自回归方式预测 B 的每个 token。这样训练得到的模型既能取句向量做召回也能用前缀文本生成后续内容。两个任务在训练时 loss 直接相加前向传播和反向传播共用一套权重不需要额外增加参数量。2.3 权重文件与配置文件的对应关系仓库里bert_model.ckpt.data-00000-of-00001、.index、.meta三个文件分别是参数数据、索引和图定义这是老版 TensorFlow 1.x checkpoint 的标准格式用当前的 PyTorch 生态直接没法加载需要走转换流程或者直接用 bert4keras 等支持 TF checkpoint 的库。bert_config.json描述的是 Transformer 结构超参而不是 RoFormer 专属配置加载时最关键的是要和权重文件保持在同一个目录下路径写错会在build_transformer_model阶段直接抛出找不到 checkpoint 的异常。3. stage1.py 无监督预训练复现数据准备与 loss 计算3.1 无监督相似句对从哪里来stage1.py 做的事情是让模型先学会“改写”这件事但它并不需要人工标注的相似句对。常见做法是把每一条语料自身作为正样本的来源训练时从 batch 内部构造相似度矩阵假设一个 batch 里有 N 条文本把它们复制成两份一份作为 A 句一份作为 B 句然后打乱顺序重新配对。这样构造出的监督信号是“第 i 行的 A 句和打乱后的 B 句配对”虽然不保证语义完全一致但作为弱监督信号足够让模型学到句向量的分布结构。生成部分则直接把原始句子作为生成目标让模型学会完形填空式的重构。3.2 模型加载与 batch 构造的代码实现# stage1.py 核心加载与数据拼装逻辑 from bert4keras.models import build_transformer_model from bert4keras.tokenizers import Tokenizer import numpy as np config_path chinese_roformer-sim-char_L-12_H-768_A-12/bert_config.json checkpoint_path chinese_roformer-sim-char_L-12_H-768_A-12/bert_model.ckpt dict_path chinese_roformer-sim-char_L-12_H-768_A-12/vocab.txt tokenizer Tokenizer(dict_path, do_lower_caseTrue) model build_transformer_model( config_path, checkpoint_path, modelroformer, with_poolTrue, with_mlmTrue ) def get_batch(texts, max_len128): # texts 是一个字符串列表每个元素是一条原始句子 token_ids_a, segment_ids_a [], [] token_ids_b, segment_ids_b [], [] for text in texts: # 对每条文本分别编码模拟两路输入 tokens_a, segs_a tokenizer.encode(text, max_lenmax_len) tokens_b, segs_b tokenizer.encode(text, max_lenmax_len) token_ids_a.append(tokens_a) segment_ids_a.append(segs_a) token_ids_b.append(tokens_b) segment_ids_b.append(segs_b) # 返回两个序列矩阵一个用于相似度计算一个用于生成损失 return token_ids_a, segment_ids_a, token_ids_b, segment_ids_b这里的核心逻辑是把同一条文本编码成两份输入一份作为相似度任务的 anchor另一份作为正样本来源。tokenizer.encode返回 token 序列和 segment 序列max_len控制截断长度。do_lower_caseTrue会把英文统一转小写对中文无影响但能减少英文单词的词表膨胀。3.3 相似度 loss 与生成 loss# 相似度 lossbatch 内句子向量两两内积标签为单位矩阵 def similarity_loss(pooled_a, pooled_b): # pooled_a: [batch_size, hidden_dim] # pooled_b: [batch_size, hidden_dim] scores pooled_a pooled_b.T # 得到 batch_size x batch_size 的相似度矩阵 labels np.eye(len(pooled_a)) # 单位矩阵作为监督信号 # 将 scores 展平后用交叉熵计算 scores tf.reshape(scores, [-1]) labels tf.reshape(labels, [-1]) loss tf.keras.losses.binary_crossentropy(labels, tf.sigmoid(scores)) return tf.reduce_mean(loss)pooled_a是句子 A 的 CLS 向量pooled_b是句子 B 的 CLS 向量两者做矩阵乘法就得到相似度矩阵。单位矩阵作为 label 的含义是第 i 个 anchor 只和它自己对应的那条正样本相似和其他样本视为负样本。实际实现时我一般不会用全部 batch 的样本做负样本而是限制负样本数量在 128 以内否则显存占用会随 batch_size 平方增长。生成部分用 UNILM attention mask本质上是把[CLS] A [SEP] B [SEP]拼接后让注意力只能看到左侧 token。训练时用 B 部分的标准文本作为预测目标计算交叉熵。3.4 训练超参与显存占用的权衡超参数推荐值说明batch_size32文本平均长度 50 字以内时显存约 11GBmax_len128超过这个长度会被截断生成结果可能不完整learning_rate5e-5加载预训练权重后不宜过大negative_samples128相似度矩阵的负样本数量上限复现 stage1 的最常见问题是显存不足解决方式是把 batch_size 减半而不是降低 max_len因为 max_len 直接决定生成文本的上限长度。4. stage2.py 监督微调与 supervised.py 分类增强4.1 stage2.py 的监督微调流程stage1 训练完得到的模型已经具备基本的改写能力但如果你的任务领域比较专一比如法律文书或医疗问答还是需要用领域内的相似句对做一轮监督微调。stage2.py 的输入格式是文本对加标签的 TSV 文件每行一列是原始句、一列是改写句。标签可以省略省略时默认全部为正样本对。# stage2.py 监督微调核心代码 import csv from bert4keras.models import build_transformer_model from bert4keras.optimizers import Adam import tensorflow as tf # 读取监督数据 def load_supervised_data(path): pairs [] with open(path, r, encodingutf-8) as f: reader csv.reader(f, delimiter\t) for row in reader: if len(row) 2: pairs.append((row[0], row[1])) return pairs # 微调时保留生成 loss同时替换相似度 loss 为有监督交叉熵 model.compile( optimizerAdam(2e-5), loss[lambda y_true, y_pred: y_pred], # 自定义多任务 loss )监督微调的数据量不需要很大几百对高质量改写句就能把输出风格拉向目标领域。要点是学习率要比 stage1 更低因为此时模型已经在相似语句生成任务上收敛过大的学习率会把已学到的分布破坏掉。4.2 supervised.py用增强数据训练文本分类器supervised.py 的作用是把 stage1 或 stage2 生成的相似句应用到一个实际的文本分类任务上。流程是先用原始训练集里的每一条文本调用生成模型得到若干个改写版本然后把原始文本和改写文本一起作为训练数据喂给分类模型。这样做的好处是分类器能看到同一语义的不同表达方式决策边界不再被词面重合度带偏。# supervised.py 增强分类训练示意 from sklearn.linear_model import LogisticRegression from sklearn.feature_extraction.text import TfidfVectorizer # 原始训练数据 texts [这个电影很好看, 这家餐厅服务态度差] labels [1, 0] # 调用生成模型为每条文本生成 2 个相似句 def augment(text, num2): results [] for _ in range(num): results.append(generate_similar(text)) # 封装 generation.py 的生成函数 return results augmented_texts, augmented_labels [], [] for text, label in zip(texts, labels): augmented_texts.append(text) augmented_labels.append(label) for new_text in augment(text): augmented_texts.append(new_text) augmented_labels.append(label) vectorizer TfidfVectorizer() X vectorizer.fit_transform(augmented_texts) clf LogisticRegression().fit(X, augmented_labels)这里用 TF-IDF 加逻辑回归只是为了快速验证增强效果实际项目中可以把增强后的数据直接用于 BERT 微调。判断增强是否有用的是对比实验同一套超参数下只用原始数据训练和用增强数据训练的验证集准确率对比。5. generation.py 与 retrieval.py 的调用生成相似句与句向量检索5.1 generation.py 自回归生成中文相似句generation.py 是日常最常用的脚本。原理是输入一个句子模型以自回归方式逐个预测后续 token直到输出[SEP]或达到最大长度。实际生成时直接贪心解码会导致大量重复词特别是“我们我们我们”这类循环所以需要引入采样策略。# generation.py 核心生成逻辑 import numpy as np def generate_similar(text, topk5, temperature0.95, max_len32): # 编码输入文本 token_ids, segment_ids tokenizer.encode(text, max_len64) token_ids np.array([token_ids]) segment_ids np.array([segment_ids]) for _ in range(max_len): # 预测下一个 token 的 logits logits model.predict([token_ids, segment_ids])[0][-1] # temperature 控制分布平滑程度值越低越保守 logits logits / temperature # 屏蔽 [UNK] 和 [PAD]避免生成无意义字符 logits[1] -np.inf # [UNK] 的 id 通常是 1 logits[0] -np.inf # [PAD] 的 id 通常是 0 # topk 采样从概率最高的 k 个 token 中随机选一个 topk_indices np.argsort(logits)[-topk:] probs np.exp(logits[topk_indices] - np.max(logits[topk_indices])) probs probs / probs.sum() next_token np.random.choice(topk_indices, pprobs) # 遇到 [SEP] 停止生成 if next_token 3: # [SEP] 的 id 在 vocab.txt 中通常为 3 break token_ids np.concatenate([token_ids, [[next_token]]], axis1) segment_ids np.concatenate([segment_ids, [[0]]], axis1) return tokenizer.decode(list(token_ids[0]))temperature是最值得调的参数。调低到 0.8 左右生成结果更接近原句适合做语义保持型增强调到 1.0 以上句子变化更大但可能跑偏语义。topk我一般设在 5 到 10 之间太小容易重复太大会出现语法错误。生成文本中如果出现连续两个[UNK]说明输入里有人名或生僻词建议在预处理阶段做一下替换。5.2 retrieval.py 句向量召回与相似度排序retrieval.py 的作用正好和 generation 互补。它不生成新句子而是从一个候选池里把与输入句语义最接近的句子找出来。实现方式是把输入句和候选句都过一遍模型取 CLS 向量然后计算余弦相似度排序返回 TopK。# retrieval.py 语义检索核心逻辑 import faiss def build_index(sentences, batch_size64): vectors [] for i in range(0, len(sentences), batch_size): batch sentences[i: i batch_size] # 批量编码得到句向量矩阵 vec encode_batch(batch) # 内部调用模型 predict vectors.append(vec) vectors np.concatenate(vectors, axis0) # 归一化后使用 faiss 的 IndexFlatIP 做内积检索 vectors vectors / np.linalg.norm(vectors, axis1, keepdimsTrue) index faiss.IndexFlatIP(vectors.shape[1]) index.add(vectors) return index def search(index, query, topk10): vec encode_batch([query]) vec vec / np.linalg.norm(vec, axis1, keepdimsTrue) scores, indices index.search(vec, topk) return indices[0], scores[0]这里的encode_batch是把文本批量过模型取 CLS 向量。faiss 的IndexFlatIP是暴力内积检索数据量在十万级以内时性能完全够用。注意两个细节一是向量必须做 L2 归一化否则内积分数会被向量模长干扰二是每次加入新句子时需要重新建索引或者用IndexIDMap增量添加。5.3 生成加检索组合成双通道增强generation 负责“无中生有”retrieval 负责“从已有数据里挑相近表达”。实际做数据增强时我一般两者结合先用 generation 生成候选句子跳过相似度低于阈值的低质量结果再把通过阈值的句子加入训练集。这样既保证数量又控制质量。6. 文本数据增强实战批量生成、质量过滤与常见报错排查6.1 三个最容易翻车的点第一个坑是 checkpoint 路径问题。build_transformer_model加载的是老版 TF 权重如果你在 Windows 下跑路径里的反斜杠和转义字符容易出问题建议统一用/分隔的绝对路径。第二个坑是 vocab.txt 与权重不匹配报错特征是在 tokenizer 编码阶段正常、但模型 predict 时输出全是[UNK]检查 vocab 文件是否被替换过。第三个坑是 batch 维度的坑generate 函数里token_ids的 shape 必须是[1, seq_len]丢维度后 predict 阶段会报维度不匹配。6.2 质量过滤器与批量增强管线给生成结果打分是控制增强质量的关键。我的做法是用 retrieval 模块把生成句和原始句做一次相似度比对低于 0.7 的直接丢弃高于 0.95 的也丢弃——前者语义跑偏后者和原句几乎一样没什么增强价值只有中间区间的句子才值得加入训练集。批量生成时建议按文本长度分批长度相近的放同一个 batch避免 padding 浪费过多算力。用 GPU 推理时把 batch_size 设置为 32 左右生成 10000 条候选句子大概几分钟就能跑完瓶颈主要在 CPU 的 tokenizer 编码上可以把编码和生成拆成两个阶段异步执行。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。