2026/10/13 0:43:11

从CSV到文本相似度:中文Word2Vec预处理与训练实操

从CSV到文本相似度:中文Word2Vec预处理与训练实操 简介面向自然语言处理初学者和文本挖掘开发者这份资源围绕中文文本预处理、Word2Vec词向量训练及文本相似度计算展开。包内包含两个Python脚本和一个CSV数据集共3个文件压缩包整体约1.19MB轻量但覆盖完整流程一个脚本处理原始中文语料完成数据导入、清洗、分词、停用词过滤等关键环节另一个脚本基于处理后的语料训练Word2Vec模型并利用余弦相似度或欧氏距离衡量文本间语义距离。配套的CSV文件是带标注的中文情感分类语料可直接用于模型训练与效果验证。整份资源适合做入门实践或课程作业参考已有658人学习。通过运行脚本读者可掌握从原始中文文本到词向量表示再到相似度计算的全链路方法理解CBOW与Skip-gram两种架构的差异并学会调节窗口、维度和迭代次数等参数。对于需要快速搭建中文NLP实验环境、理解词向量原理的读者是一份便于直接上手的学习资料。1. 中文文本预处理与 Word2Vec这份资源把「从原始 CSV 到相似度数值」的整条路串通了做中文 NLP 项目的人多半在 word2vec 文本预处理这一环上栽过跟头。直接从开源仓库拉一段语料连 tokenize 都不做就丢进 gensim训练出来的词向量拿去算文本相似度结果两个毫不相关的句子得分比同义句还高。这份资源Word2vec-with-Chinese-character-master解决的正是这个问题它用两个 Python 脚本把「原始 CSV → 清洗 → 分词 → 停用词过滤 → Word2Vec 训练 → 相似度计算」这条链路完整串了起来数据用的是 ChnSentiCorp_htl_all.csv 酒店评论语料。适合刚接触中文词向量的开发者也适合想快速验证预处理流程对结果影响的从业者。下面按我实际跑通的顺序拆解。2. 文本预处理全流程从酒店评论 CSV 到干净分词语料的四个环节文本预处理是整个流程的地基地基没打牢后面训练出来的词向量就是一坨乱码映射。这个环节做的事可以用一句话概括把人类写的、带噪声的原始文本变成一组组干净的、符合模型输入格式的词语列表。下面按数据导入、清洗、分词、停用词过滤四个环节展开每一步我都会给出脚本里的对应写法和可以改的参数。2.1 数据导入编码与缺失值这两个坑必须一开始就处理第一步是用 pandas 把 CSV 读进来。这里有个很容易翻车的细节Windows 环境下生成的 CSV 经常带 BOM 头直接用默认编码读第一列列名会多出一个\ufeff后面所有基于列名的操作全部报 KeyError。资源里用的是utf-8-sig这个编码会自动剥离 BOM属于中文语料读取的默认选项。import pandas as pd df pd.read_csv( datasets/ChnSentiCorp_htl_all.csv, encodingutf-8-sig, dtype{label: int, review: str} ) print(df.shape) print(df.isnull().sum()) df[review] df[review].fillna()这段代码里dtype参数容易被忽略但它很重要。评论列如果混入纯数字内容pandas 可能把整列推断成 float后续做字符串操作直接报错。显式声明成str能避免这种类型推断的玄学问题。fillna()处理的是缺失值因为 CSV 里偶尔会有空行或空字段不填掉的话下一步正则清洗时None会抛异常。提示如果数据集很大读入后先打印df.shape和df.head()确认行数、列名符合预期再做清洗避免清洗了半天发现读错文件。2.2 清洗与标准化正则把噪声挡在模型之前清洗的目标是去掉对语义贡献为零的噪声。酒店评论里最常见的是 HTML 标签、URL、数字、特殊符号以及全角半角混用的问题。脚本里用正则做第一轮过滤再用 Unicode 标准化统一字符宽度。import re import unicodedata def clean_text(text: str) - str: text re.sub(r[^], , text) # 去 HTML 标签 text re.sub(rhttps?://\S|www\.\S, , text) # 去 URL text re.sub(r\d, , text) # 去数字 text re.sub(r[^\u4e00-\u9fa5\uFF01-\uFF5E\w], , text) # 去特殊符号 text unicodedata.normalize(NFKC, text) # 全角转半角 return text逻辑说明前四个re.sub是层层过滤顺序有讲究——先去掉标签和 URL是因为它们内部可能包含数字和特殊符号先删可以避免后面正则误伤第四行是白名单式过滤只保留中文、中文标点、字母数字下划线其余全部剔除。最后的normalize(NFKC, ...)是容易被忽略的一步它把全角字母数字如转成半角good否则同一个词会被分词的字典当成两个完全不同的 token在 Word2Vec 里占两个向量位置白白稀释语料。清洗强度要按场景调整。情感分析任务里数字和 URL 基本没语义可以直接删但如果是电商评论「2024」这种年份可能有意义就别用\d一刀切。我的习惯是先跑一遍清洗打印 20 条样本人工扫一遍再决定要不要放宽正则。2.3 jieba 分词与停用词给 Word2Vec 喂对语料中文分词是把连续字符串切成有意义的词序列。资源用的是 jieba因为它对酒店评论这类口语化文本的切分表现尚可而且支持自定义词典。分词这一步的输出是 Word2Vec 的直接输入脚本里做了两个关键处理过滤空白 token、过滤停用词。import jieba import jieba.analyse def load_stopwords(path: str) - set: with open(path, r, encodingutf-8) as f: return {line.strip() for line in f if line.strip()} def segment(text: str, stopwords: set) - list: words jieba.lcut(text) return [w for w in words if w.strip() and w not in stopwords]jieba.lcut返回的是 list比jieba.cut这种生成器更直观方便后面直接做列表推导。停用词表是外部文件常见的有哈工大停用词表、百度停用词表资源里没有显式列出我用的是自备的一份通用表包含「的」「了」「是」「在」这类高频无实义词。segment函数里先strip()再查集合是因为 jieba 偶尔会切出空格或空串不过滤会让 Word2Vec 训练出无意义的向量。分词环节有个容易犯的错把自定义词典加载写在分词函数内部每调一次segment就load_userdict一次。如果评论有几万条这个加载过程会重复几万次性能直接拉垮。正确做法是在主程序里加载一次然后传给分词函数。词性标注和词形还原在脚本里是选配因为 Word2Vec 只需要词形不需要词性如果要做更深层的语义分析可以在这步输出带词性的元组但那份资源没做我们也不引入额外复杂度。stopwords load_stopwords(stopwords.txt) corpus [segment(clean_text(row), stopwords) for row in df[review]] corpus [s for s in corpus if len(s) 2]最后一行过滤掉分词后不足两个词的样本这类短样本通常是空评论或纯噪声留着只会让模型把低质量数据也学进去。整个预处理链路的产出是corpus一个列表的列表每个内层列表代表一条评论的分词结果这个结构就是后面 Word2Vec 训练的标准输入格式。3. Word2Vec 训练模型选型、关键参数与训练实验记录预处理跑通之后词向量训练就是整个资源的核心环节。这一章讲两个脚本里真正决定模型质量的东西架构怎么选、参数怎么定。很多人训练词向量直接抄别人参数训完也不看效果这属于把黑匣子当真用。下面把选型和参数摸清楚。3.1 CBOW 与 Skip-gram 怎么选先看语料量和生僻词占比Word2Vec 有两种架构资源里词向量训练.py通过sg参数切换。CBOW 用上下文预测中心词训练速度快适合语料量大、词频分布均匀的场景Skip-gram 用中心词预测上下文对低频词更友好训练慢一点但语义细微差别的捕捉能力更强。针对 ChnSentiCorp 这个数据集几十层楼的酒店评论语料词表规模不算大我的选择是 Skip-gram。原因很直接语料几千条很多词出现次数很低CBOW 在这种稀疏语料上会把低频词训成近似零向量而 Skip-gram 即使低频词也能学到有区分度的表示。如果你手头是上亿词的通用语料再切回 CBOW 不迟。对比项CBOWSkip-gram训练速度快慢低频词效果一般好适用语料规模大中小语义精细度较粗较细对应 sg 参数013.2 词向量训练参数窗口、维度、迭代次数怎么定参数设置是影响训练结果最直接的因素也是我见过最多人乱填的地方。脚本里的核心参数可以拆成四组向量维度、上下文窗口、采样策略、训练轮数。每一组都有对应的实际意义理解了才能根据语料调。from gensim.models import Word2Vec model Word2Vec( sentencescorpus, vector_size128, window5, min_count3, sg1, negative5, epochs10, workers4, seed42 )参数说明vector_size是词向量的维度128 是中小语料的折中值——太低语义信息装不下太高训练慢而且容易过拟合window是上下文窗口大小5 意味着左右各看 5 个词窗口太小语义不完整太大引入噪声min_count3表示出现次数少于 3 的词直接丢弃这个值设定的逻辑是把长尾噪声挡在词表外避免模型为只出现一两次的词分配向量空间sg1选的是 Skip-gramnegative5是负采样数量负采样让模型只更新少量负例的权重训练速度能提升好几倍5 是效果和速度的平衡点epochs10是遍历语料的次数语料小就多跑几轮语料大三四轮就够workers4是并行线程数设成 CPU 核心数就行seed42固定随机种子保证每次训练结果可复现这是做实验必须养成的习惯。训练耗时跟语料规模直接相关。这份资源的数据集我实测在普通笔记本上128 维、10 轮迭代跑完大概两三分钟速度不是瓶颈。真正要盯的是训练日志里的 loss 变化——gensim 不开 verbose 的话看不到过程我建议第一次训练加上word2vec日志级别确认语料被正确加载、词表大小合理再关掉日志跑全量。3.3 训练与模型落盘从内存到磁盘的完整动作训练完成后模型交互和落盘是最后一步。脚本里用的是model.save()这里有一个很多人踩过的坑model.wv.save_word2vec_format()和model.save()保存的内容不一样前者只存词向量矩阵后者存的是完整模型对象包括训练状态。如果你之后还要增量训练必须用save()如果只是拿来查相似度save_word2vec_format就够了。model.save(word2vec_htl.model) model.wv.save_word2vec_format(word2vec_htl.vectors, binaryFalse)逻辑说明第一行保存完整模型之后能Word2Vec.load()回来继续训练也能直接查向量。第二行把词向量导出成文本格式每行是一个词和它的向量数值方便用其他工具读取比如可视化或向量数据库导入。binaryFalse表示存成明文文本方便人工查看调试数据量大的话可以设成True存二进制加载更快。注意save_word2vec_format保存的文件没有词频信息加载后无法用model.train()继续训练只能做查询。所以如果你还想调参迭代第二个文件完全依赖save()出来的那份模型。4. 计算文本相似度平均词向量加余弦相似度的实现与调参词向量训练好后最终目标是算文本相似度。这里有个常见的概念误区Word2Vec 输出的向量是词的向量不是句子的向量。要把一段文本变成一个向量常见做法是取这句话里所有词的向量做平均这个被称为平均词向量。脚本里就是按这个思路实现的下面把代码和背后的限制都讲清楚。4.1 从词向量到句向量平均词向量与加权方案平均词向量是最简单的句子向量化方式。实现时要注意两个边界未登录词和全未命中词。未登录词是指训练语料里没出现过模型里查不到向量的词一条句子如果大部分词都未登录平均出来的向量会退化成噪声。脚本里的处理方式是跳过未登录词只在最后兜底一个零向量。import numpy as np VECTOR_SIZE 128 def sentence_vector(words: list, model) - np.ndarray: vecs [model.wv[w] for w in words if w in model.wv] if not vecs: return np.zeros(VECTOR_SIZE) return np.mean(vecs, axis0)逻辑说明model.wv是词向量字典w in model.wv做存在性检查避免 KeyError。np.mean(vecs, axis0)沿行方向取平均得到的是一个 128 维的句向量。这段代码有个隐患如果句子所有词都是未登录词返回零向量这个零向量和任何向量的余弦相似度计算都会出问题。我在应用层会加一层判断句子向量为零向量时直接丢弃或降级处理而不是继续参与相似度计算。平均向量有个明显的短板它把所有词一视同仁但「酒店」「房间」这类主题词和「的」「了」这种高频词的贡献应该不一样。加权方案可以缓解这个问题常见的做法是 TF-IDF 加权——每个词向量乘以它的 TF-IDF 权重再平均。实现时要先拿语料拟合TfidfVectorizer得到每个词的权重再套进平均公式。这属于进阶玩法资源脚本里用的是无权重平均因为它简单且在小语料上效果已经够用。4.2 余弦相似度计算与判定阈值句向量有了相似度计算就变成向量空间的几何问题。余弦相似度衡量两个向量的方向一致性范围在 -1 到 1 之间值越大越相似。欧氏距离也常被提到但它的值受向量长度影响大对词向量这种高维稀疏空间不友好所以主流还是用余弦。def cosine_similarity(v1: np.ndarray, v2: np.ndarray) - float: norm1 np.linalg.norm(v1) norm2 np.linalg.norm(v2) if norm1 0 or norm2 0: return 0.0 return float(np.dot(v1, v2) / (norm1 * norm2)) # 使用示例 sent_a 房间很干净 服务员态度好 sent_b 卫生条件不错 前台很热情 vec_a sentence_vector(jieba.lcut(sent_a), model) vec_b sentence_vector(jieba.lcut(sent_b), model) print(cosine_similarity(vec_a, vec_b))逻辑说明np.linalg.norm算 L2 范数也就是向量的模长。分母先检查是否为零防止除零错误。np.dot(v1, v2) / (norm1 * norm2)就是余弦公式的向量实现。相似度算出来之后阈值怎么定是应用层的核心问题。根据我这批酒店评论语料多次跑的经验阈值大概这么划大于 0.75 可以认为是强相关同话题、同情感倾向0.55 到 0.75 是弱相关小于 0.55 基本不相关。但阈值随语料和向量维度变化很大换数据集必须重新标定不能直接抄。4.3 相似度查询的边界未登录词与短文本边界情况是最容易让线上系统翻车的地方。未登录词导致的零向量问题4.1 已经提到短文本是另一个大坑。比如两个句子都只有两个词「好 吃」和「难 吃」词向量平均之后前者向量偏正向后者向量把「难」的负向信息也平均进去了余弦相似度可能反而很高。因为平均操作会把方向抵消短文本里词的权重太大一句的关键词直接主导整个向量。对应策略有两个一是短文本场景改用加权方案让情感词权重更大二是对相似度得分做后处理文本长度过短的句子单独走规则匹配不参与向量计算。资源脚本里没有这种后处理但我在实际落地时一定会加一层长度过滤。另外同一个词在不同语境下语义不同比如「苹果」指水果还是手机品牌词向量模型训练时没有语境标注这属于平均词向量的先天限制做相似度时心里得有数。5. 避坑指南预处理和训练阶段最常见的四个翻车现场整个流程我从预处理跑到相似度计算遇到过不少问题。有些是参数配错直接报错有些是结果离谱但没报错——后者更可怕因为它不会打断流程只会悄悄污染你的结论。下面按「现象 → 原因 → 解决」写四个我踩过也帮别人排查过的坑。5.1 自定义词典没生效分词语料大批量出错现象往 jieba 里加了自定义词典包括酒店名、菜品名等专有名词但分词结果里这些词还是被切成单字导致 Word2Vec 的词表里出现一堆毫无意义的单字向量。原因最常见的是jieba.load_userdict()调用位置不对。把加载函数写在每个分词函数内部或者写在if __name__ __main__块里但主程序实际没有执行到还有一种可能是词典文件编码不对jieba 要求 UTF-8Windows 下存成 GBK 就会静默失败。解决在启动阶段全局加载一次并且确认词典文件是 UTF-8 编码。加载后做一次冒烟测试拿词典里的词直接jieba.lcut验证切分结果。我一般会写一行断言assert 海底捞 in jieba.lcut(海底捞服务不错)不过这里只是举例词典里有什么词就断言什么词。断言过了再跑全量语料。5.2 gensim 4.x 换了参数名旧代码直接 TypeError现象把网上抄的旧代码粘进来跑到Word2Vec初始化时直接报TypeError: got an unexpected keyword argument iter或者size参数报错。原因gensim 4.0 做了一次大版本 API 清理iter改名为epochssize改名为vector_size。网上大量旧教程和博客用的是 3.x 写法直接照搬必翻车。资源脚本如果按新版本写的没问题但你自己扩展代码时很容易混用旧参数名。解决统一按新 API 写所有旧教程里的size100改成vector_size100iter5改成epochs5。如果你的环境里历史代码太多必须兼容旧版可以包一层函数判断 gensim 版本但新项目没必要这么干。另外注意4.x 里model.wv.vectors代替了旧的model.wv.syn0访问原始向量矩阵时别用旧属性。5.3 全角半角不统一和good被当成两个词现象词表里出现一对长得几乎一样的词一个全角一个半角比如和good查询其中一个另一个完全不相关。如果语料里有大量全角英文字母词表大小会虚增每个词的向量质量反而下降。原因清洗阶段没做 Unicode 标准化。简体中文文本里全角英文字母和全角标点很常见——很多输入法默认输出全角字符用户从 Word 或网页复制的内容尤其明显。分词工具对全角和半角是按不同字符处理的所以切出两个不同 token。解决清洗函数里加一行unicodedata.normalize(NFKC, text)。这一行代码能把全角字母数字转半角全角标点也一并转换。我在 2.2 强调过这一步这里再说一次是因为它太容易被忽略而后果出现得很晚——词表大了一圈但你根本注意不到原因。做完标准化后重建语料再训练词表通常会缩小 5% 到 10%算是有效排查信号。5.4 词向量模型加载后无法增量训练现象加载之前保存的模型想加一批新语料增量训练调用model.train(new_sentences)后报错或者训练完但旧词的向量全被破坏了。还有人把model.wv当成模型本身去加载结果连.train方法都没有。原因model.save()保存的是完整模型加载要对应Word2Vec.load()而model.wv.save_word2vec_format()只存词向量矩阵加载后只能查相似度不能继续训练。另一个坑是增量训练时没设置epochsgensim 对增量训练默认的迭代次数会让旧向量被大幅覆盖导致旧词的语义漂移。解决增量训练必须用Word2Vec.load()加载完整模型然后显式指定epochs而且值要比首次训练小通常是 1 到 3。训练前先把新语料和旧语料合并确保min_count和词表更新逻辑一致。如果只是想用词向量而不需要再训练直接用KeyedVectors.load_word2vec_format()加载第二份文件逻辑更清晰。我现在的习惯是最终交付只导出word2vec_format文件作为只读词向量开发阶段用save()保留训练状态。6. 进阶用 t-SNE 可视化和词类比验证词向量拒绝玄学调参训练完模型先别急着算相似度我强烈建议做一次质量验证——这步做完你才知道参数该往哪个方向调。验证方法有两种直观法和类比法。直观法是用most_similar看一个词的相关词排序比如查「早餐」合理的结果应该是「早餐」附近出现「午餐」「晚餐」「自助餐」这类词。如果返回的全是「房间」「酒店」这类不相关词说明语料太少或窗口太大模型没学到词的语义。print(model.wv.most_similar(早餐, topn10)) print(model.wv.most_similar(干净, topn10))类比法更严格经典测试是国王 - 男 女 ≈ 女王这种词向量加减。中文可以做类似的model.wv.most_similar(positive[服务员, 女], negative[男])看结果是否接近「女服务员」。词向量如果连基本的类比关系都做不出来相似度计算的结果就不值得信。from sklearn.manifold import TSNE import matplotlib.pyplot as plt words [早餐, 午餐, 晚餐, 服务, 前台, 房间, 干净, 卫生] word_vecs np.array([model.wv[w] for w in words if w in model.wv]) words_valid [w for w in words if w in model.wv] tsne TSNE(n_components2, random_state42, perplexity5) coords tsne.fit_transform(word_vecs) plt.figure(figsize(8, 6)) for i, word in enumerate(words_valid): plt.scatter(coords[i, 0], coords[i, 1]) plt.annotate(word, (coords[i, 0], coords[i, 1])) plt.show()参数说明perplexity5在词数量少于 30 时强制调小否则 t-SNE 算法会输出警告甚至结果失真。fit_transform输出的coords是二维坐标数组。可视化时如果「早餐」「午餐」「晚餐」能聚成一簇和「房间」「干净」拉开距离说明词向量空间结构是合理的。调参的逻辑也可以讲讲如果同义词没有聚类先用vector_size从 64 加到 128 看效果如果高频词都挤在一起说明窗口太大从 5 收到 3如果低频词向量质量差把min_count从 3 降到 1同时换sg1让低频词多学几轮。每次只改一个参数跑完做上面三种验证对比结果再动下一个参数。这比我最早一股脑把所有参数调一遍高效得多——那时候调参全凭感觉跑一晚出了结果也不知道哪个参数起作用纯属浪费算力。从那以后我每次训完词向量都强制走一遍「most_similar抽查 词类比验证 t-SNE 聚类」这三个动作确认向量空间结构合理才把模型交给下游算相似度。这个过程也能帮你判断这份资源里的脚本是不是真的跑通了——如果你做完可视化发现语义相关的词确实聚在一起那整条链路就没问题可以放心拿这个模型去做推荐、情感分析或者问答系统的向量检索了。希望帮到你。本文还有配套的精品资源点击获取