
简介这份资源面向自然语言处理入门者与中文文本分析实践者聚焦中文文本预处理与Word2Vec词向量训练帮助解决原始语料噪声多、分词与停用词处理繁琐、文本相似度难以量化等问题。压缩包共5个文件约1.19MB包含2个Python脚本分别负责文本预处理与词向量训练1个CSV数据集用于模型训练与相似度验证另有说明文档及备份文件结构清晰便于直接运行与二次修改。内容覆盖数据导入与编码处理、HTML标签与特殊符号清洗、jieba分词、停用词过滤以及CBOW与Skip-gram模型选择、窗口大小与嵌入维度等参数设置最终通过余弦相似度或欧氏距离衡量文本相似度。已有58人学习适合希望快速跑通中文词向量与相似度计算全流程的读者参考。1. 中文文本处理与Word2Vec相似度计算从分词到向量召回的一条龙落地电商搜索里搜「苹果手机」结果把「苹果」水果的新闻推到了首位客服工单里「无法开机」和「开不了机」被当成两个完全不同的意图——这类问题我每年至少遇到三四次。根因往往不在模型而在中文文本处理与Word2Vec相似度计算这条链路没打通。中文没有天然空格分词粒度、停用词表、训练语料规模、向量维度、相似度度量方式任何一环选错召回结果都会「玄学」起来。这篇笔记面向需要快速搭一套中文语义相似度服务的工程师从零训练一个Word2Vec模型到用向量算相似度做召回把每一步的参数、坑和验证方法讲清楚。读完你能在本地跑通一条最小可用链路也能判断什么时候该换方案。2. 中文文本处理的前置工序分词、清洗与语料准备Word2Vec本身不认字它只认「词」这个单位。中文文本处理的第一步就是把连续汉字切成词序列。这一步做不好后面训练出来的向量全是噪声。我见过最典型的翻车是有人直接把整句当成一个token丢进去训练完发现所有句子的相似度都接近1因为词表里只有几百个句子ID。2.1 分词工具选型与粒度控制常见的中文分词方案有jieba、HanLP、LAC、THULAC等。做Word2Vec训练语料预处理我一般用jieba原因是它轻量、可自定义词典、支持并行分词对百万级语料足够用。HanLP精度更高但依赖重LAC对短文本友好但词表偏新闻语料。选型时看两点你的语料领域电商、医疗、法律术语差异极大和是否需要增量更新词典。分词粒度直接影响词表大小和向量质量。比如「自然语言处理」切成「自然语言/处理」还是「自然/语言/处理」前者词表小、语义集中后者泛化好但容易把复合概念拆散。我的经验是领域术语用自定义词典强制合并通用词按默认粒度切。下面是一段可复现的预处理代码。import jieba import re # 加载领域自定义词典每行格式词语 词频 词性词频词性可省略 jieba.load_userdict(domain_dict.txt) def clean_text(text): # 去除URL、邮箱、连续空白和特殊符号保留中文、英文、数字 text re.sub(rhttps?://\S|www\.\S, , text) text re.sub(r[^\u4e00-\u9fa5a-zA-Z0-9], , text) text re.sub(r\s, , text).strip() return text def tokenize(text): text clean_text(text) # 精确模式HMM新词发现开启适合训练语料 words jieba.lcut(text, cut_allFalse, HMMTrue) # 过滤单字和纯数字单字噪声大数字对语义贡献低 words [w for w in words if len(w) 1 and not w.isdigit()] return words if __name__ __main__: corpus [苹果手机无法开机怎么办, iPhone开不了机了求助] for line in corpus: print(tokenize(line))这段代码做了三件事清洗掉对语义无贡献的符号、用jieba精确模式切词、过滤单字和纯数字。load_userdict加载的自定义词典是提升领域效果最划算的操作比如把「无法开机」「开不了机」都加进去后续相似度计算会直接受益。HMMTrue开启新词发现对未登录词友好但会带来少量不可控切分语料规整时可以关掉换速度。2.2 停用词表与语料格式规范停用词过滤是第二个容易翻车的点。通用停用词表哈工大、百度、四川大学版本能去掉「的、了、是」这类高频虚词但领域停用词必须自己补。比如做客服场景「请问」「你好」「谢谢」出现频率极高却不携带意图信息不删掉它们会在向量空间里形成一个大簇把所有短文本的相似度都拉高。语料格式上Word2Vec训练输入是「一行一句、词之间空格分隔」的纯文本。我一般把预处理结果写成corpus_seg.txt每行一个文档或一句话。注意两个细节一是句子不要过长超过200词的句子建议按标点切分否则窗口内上下文跨度过大二是语料去重重复文档会让高频词向量过度偏移。下面这段代码把原始语料批量转成训练格式。from collections import Counter def build_corpus(raw_lines, stopwords_path, out_path, min_freq3): with open(stopwords_path, encodingutf-8) as f: stopwords set(line.strip() for line in f if line.strip()) tokenized [] for line in raw_lines: words [w for w in tokenize(line) if w not in stopwords] if words: tokenized.append(words) # 统计词频过滤低频词降低词表噪声 freq Counter(w for sent in tokenized for w in sent) with open(out_path, w, encodingutf-8) as f: for sent in tokenized: kept [w for w in sent if freq[w] min_freq] if kept: f.write( .join(kept) \n) print(f词表规模: {len(freq)}, 保留词: {sum(1 for w in freq if freq[w] min_freq)})min_freq3是经验值语料小于10万句时设2大于百万句时设5。低频词训练不充分向量随机性大留着反而干扰相似度计算。停用词表建议通用表加领域表合并领域表从你语料的高频词里人工筛一遍半小时的活能省后面几天的调参。3. Word2Vec训练过程参数怎么设、模型怎么存语料准备好之后进入Word2Vec训练过程。这一步的核心是理解SGSkip-gram和CBGCBOW的区别以及几个关键参数对向量质量的影响。很多人训练完发现相似词召回不准八成是窗口大小或负采样设错了。3.1 SG与CBOW的选型理由CBOW用上下文预测中心词训练快对高频词效果好适合语料大、追求速度的场景。SG用中心词预测上下文对低频词和语义细节更敏感语料偏小或需要精细语义时选它。我的默认选择是SG因为相似度计算场景里低频领域词往往是关键CBOW容易把它们训成「平均脸」。向量维度vector_size一般设100到300。低于100表达能力不足高于300在中小语料上容易过拟合且内存翻倍。窗口window设5是通用起点太小捕捉不到远距离搭配太大把无关词拉进上下文。负采样negative设5到10小语料取5大语料取10。min_count和前面词频过滤保持一致避免训练时再丢词造成词表对不上。from gensim.models import Word2Vec def train_w2v(corpus_path, model_path, vector_size200, window5, sg1, negative5, epochs10, min_count3): # 按行读取分词后的语料 sentences [] with open(corpus_path, encodingutf-8) as f: for line in f: words line.strip().split() if words: sentences.append(words) model Word2Vec( sentencessentences, vector_sizevector_size, # 向量维度 windowwindow, # 上下文窗口 sgsg, # 1Skip-gram, 0CBOW negativenegative, # 负采样个数 min_countmin_count, # 忽略低频词 workers4, # 并行线程 epochsepochs, # 训练轮数 seed42 # 固定随机种子保证可复现 ) model.save(model_path) # 单独存词表后续相似度计算只加载向量省内存 model.wv.save_word2vec_format(model_path .vec, binaryFalse) print(f词表大小: {len(model.wv)}) return model if __name__ __main__: train_w2v(corpus_seg.txt, w2v.model)epochs10是起点语料小于5万句可以加到20大于百万句5轮就够。seed42固定后每次训练结果一致方便排查问题。训练完用model.wv.save_word2vec_format导出纯文本向量线上服务只加载这个文件比加载完整模型快得多。验证训练质量最直接的方法是看相似词model.wv.most_similar(开机)如果返回「开机、启动、开机键」这类词说明窗口和维度设得合理如果返回一堆虚词回去检查停用词和分词。3.2 训练结果的快速验证方法训练完不验证就上线是血泪教训。我一般做三个检查相似词、类比任务、向量范数分布。相似词看语义聚类是否合理类比任务用「A对B等于C对D」的形式比如「北京-中国日本≈东京」能跑对说明向量空间结构健康向量范数分布看是否有个别词范数异常大那通常是高频噪声词没过滤干净。def validate_model(model_path): model Word2Vec.load(model_path) # 检查1相似词 for word in [开机, 手机, 故障]: if word in model.wv: print(word, -, model.wv.most_similar(word, topn5)) # 检查2类比任务 try: print(model.wv.most_similar(positive[中国, 东京], negative[北京], topn3)) except KeyError as e: print(类比词缺失:, e) # 检查3向量范数分布 import numpy as np norms np.linalg.norm(model.wv.vectors, axis1) print(f范数均值: {norms.mean():.3f}, 最大值: {norms.max():.3f})如果相似词结果里混入大量「的、了、是」说明停用词没清干净如果类比任务全错检查语料规模是否太小低于1万句SG很难学好如果范数最大值超过均值5倍以上找出那个词大概率是需要过滤的高频噪声。4. 相似度计算落地从词向量到句子向量的三种做法有了训练好的词向量下一步是算相似度。这里有个常见误解Word2Vec给的是词向量不能直接算句子相似度。句子向量怎么来直接决定召回效果。我按落地频率从高到低讲三种做法。4.1 词向量平均与TF-IDF加权平均最简单的是把句子里所有词的向量取平均。优点是快、无额外训练缺点是所有词等权「的、了」这类词会稀释语义。改进版是TF-IDF加权平均每个词的权重用它的TF-IDF值这样领域关键词权重高虚词权重低。TF-IDF的IDF从训练语料统计和Word2Vec语料保持一致。import numpy as np from gensim.models import Word2Vec from sklearn.feature_extraction.text import TfidfVectorizer class TfidfWeightedEmbedding: def __init__(self, w2v_path, corpus_path): self.model Word2Vec.load(w2v_path) # 用同一份语料拟合TF-IDF保证IDF分布一致 corpus [line.strip().split() for line in open(corpus_path, encodingutf-8)] self.vectorizer TfidfVectorizer(analyzerlambda x: x, min_df3) self.vectorizer.fit(corpus) self.idf dict(zip(self.vectorizer.get_feature_names_out(), self.vectorizer.idf_)) def sentence_vector(self, words): vecs, weights [], [] for w in words: if w in self.model.wv: vecs.append(self.model.wv[w]) # 未登录词给默认IDF中位数避免权重为0 weights.append(self.idf.get(w, np.median(list(self.idf.values())))) if not vecs: return np.zeros(self.model.vector_size) weights np.array(weights).reshape(-1, 1) return np.sum(np.array(vecs) * weights, axis0) / weights.sum() def similarity(self, words1, words2): v1, v2 self.sentence_vector(words1), self.sentence_vector(words2) # 余弦相似度分母加极小值防除零 return float(np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2) 1e-8))analyzerlambda x: x让TF-IDF直接接受分词后的列表避免二次分词。min_df3和训练时词频过滤对齐。这个方案在短文本10词以内上效果不错长文本会因为平均操作丢失语序信息相似度区分度下降。4.2 基于词移距离的相似度词移距离Word Movers Distance, WMD把两个句子看成词向量的分布计算把一个句子的词「搬」到另一个句子词的最小代价。它保留了词与词的对应关系比平均向量更能区分「苹果手机」和「手机苹果」这种语序差异。缺点是计算复杂度高句子长时慢适合离线评估或短文本。from gensim.models import Word2Vec from gensim.similarities import WmdSimilarity def wmd_demo(model_path, corpus_path): model Word2Vec.load(model_path) corpus [line.strip().split() for line in open(corpus_path, encodingutf-8)][:5000] # 只用前5000句建索引WMD计算量大全量会拖垮内存 index WmdSimilarity(corpus, model.wv, num_best5) query 手机开不了机.split() for score, idx in index[query]: print(f{score:.4f}, .join(corpus[idx]))num_best5返回最相似的5条。WMD的分数是距离越小越相似和余弦相似度方向相反线上混用时要注意统一。语料超过1万句建议先做粗排平均向量召回Top100再用WMD精排否则响应时间不可接受。4.3 相似度阈值怎么定阈值不是拍脑袋定的。我的做法是构造一批正负样本对正样本是人工标注的相似句负样本是随机组合算完相似度画PR曲线选F1最高的点作为阈值。没有标注数据时用业务日志里的「点击同一结果的query对」当正样本随机query对当负样本效果也能接受。余弦相似度在中文短文本上0.7以上通常算高度相似0.5到0.7是弱相关0.5以下基本无关但具体值必须用你的数据校准。5. 避坑与排查相似度计算最常见的五个翻车现场这一章是我踩过的坑里挑出来最有代表性的五条每条按现象、原因、解决写。新手照着排查能省大量时间。5.1 所有句子相似度都偏高现象任意两个句子算出来的余弦相似度都在0.8以上区分度极低。原因通常是停用词没过滤高频虚词在所有句子里都出现把向量拉向同一个方向或者语料太小词向量本身没训开所有词挤在一起。解决先检查停用词表把语料里Top50高频词人工过一遍再看语料规模低于1万句建议换预训练词向量或增大语料。5.2 领域词相似词召回全是通用词现象查「开机」的相似词返回「开始、开放、机会」这类字面相近但语义无关的词。原因是分词把「开机」切成了「开」和「机」或者语料里「开机」出现次数太少没训好。解决把领域词加进自定义词典强制合并同时检查min_count是否把低频领域词过滤掉了必要时对领域词单独提高采样权重。5.3 训练结果每次不一样现象同样的语料和参数两次训练相似词结果差异明显。原因是没固定随机种子且workers多线程下更新顺序不确定。解决设seed42把workers设为1做调试确认结果稳定后再开多线程。注意gensim的workers1时即使固定seed也可能有微小差异生产环境要接受这点或改用单线程。5.4 句子向量为零向量现象某些句子算相似度时报除零错误或结果恒为0。原因是句子里所有词都不在词表里未登录词sentence_vector返回了零向量。解决在函数里加判断零向量时回退到字符级n-gram或返回一个默认低相似度同时检查分词词典是否覆盖了业务高频词。5.5 线上响应慢现象相似度接口P99超过500ms。原因通常是每次请求都重新加载模型或重新计算TF-IDF。解决模型和IDF表在服务启动时加载一次常驻内存句子向量做缓存相同query直接命中WMD这类重计算放到离线或异步。我一般还会把词向量转成numpy数组存比gensim的KeyedVectors查询快一截。6. 进阶技巧用负采样词对微调提升领域相似度基础链路跑通后如果领域相似度还不够最划算的进阶操作是用业务里的「相似/不相似」词对做微调。Word2Vec本身不支持在线微调但可以用gensim的train方法在已有模型上继续训练或者用Word2Vec的build_vocab加新语料。我的做法是构造一批领域正负样本对用对比学习的思想调整向量正样本对拉近负样本对推远。import numpy as np from gensim.models import Word2Vec def finetune_pairs(model_path, pairs, lr0.01, epochs5): pairs: [(word_a, word_b, label)], label1相似, 0不相似 model Word2Vec.load(model_path) for _ in range(epochs): for a, b, label in pairs: if a not in model.wv or b not in model.wv: continue va, vb model.wv[a], model.wv[b] # 余弦相似度对向量的梯度方向 cos np.dot(va, vb) / (np.linalg.norm(va) * np.linalg.norm(vb) 1e-8) grad (cos - label) # 简单梯度更新正样本拉近负样本推远 model.wv[a] va - lr * grad * vb model.wv[b] vb - lr * grad * va model.save(model_path .finetuned) return modellr0.01是保守值太大容易把向量空间搞乱。pairs建议至少几百对覆盖主要业务词。微调后一定要用第3章的验证方法重新检查相似词和类比任务确认没有退化。这个技巧在客服意图匹配场景里能把「开不了机」和「无法开机」的相似度从0.6拉到0.85以上代价只是几百对标注。另一个实用技巧是向量归一化后存成矩阵用矩阵乘法批量算相似度比逐个调most_similar快一个数量级。线上召回Top-K时把候选集向量拼成矩阵query向量和矩阵做点积取Top-K即可。这套组合拳下来一个中等规模的中文相似度服务单机QPS做到几百不难。我自己的习惯是每次调整分词词典或停用词表后都重新跑一遍第3章的验证脚本把相似词结果存档对比。这个习惯帮我抓到过好几次「改A坏B」的回归问题。希望帮到你。本文还有配套的精品资源点击获取