ARTICLE DETAIL

资讯详情

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

NLP子词向量实战:SVD与SGNS的选型与实现解析

NLP子词向量实战:SVD与SGNS的选型与实现解析 简介面向自然语言处理初学者及课程作业人群这份Python源码包提供基于SVD分解与SGNS两种方法构建汉语子词向量并完成相似度评测的完整实现。项目从第一次编程作业的子词词表入手基于训练集与测试集并集构建语料覆盖高维共现矩阵获取K5、SVD降维、SGNS窗口采样K2与向量训练等关键环节并严格按照要求对pku_sim_test.txt中的词对计算余弦相似度缺失词按0处理输出结果便于机器判定与对比。包内共15个文件涵盖数据预处理Notebook、skip-gram与SVD/SGNS训练脚本、模型权重与npy向量、README说明及多份结果文本整体约88.66MB目录结构清晰可直接修改参数运行。目前已有112人学习下载可作为复现经典词向量方法、分析矩阵分解与神经网络模型差异的实验基线也适合课程设计或期末作业的参考实现。1. 一份 NLP 作业里的子词向量工程SVD 与 SGNS 的选型取舍自然语言处理课程里子词向量有两种主流构建路线SVD 分解这类基于全局共现统计的方法以及 SGNS 这类基于局部预测的神经网络方法。这次要拆解的 python 源码包把 BPE 切分后的汉语子词词表当成训练输入分别用窗口 K5 的共现矩阵加 TruncatedSVD 降维和窗口 K2 的 Skip-gram 负采样训练出两套向量再对 pku_sim_test.txt 中的词对计算余弦相似度输出格式满足机器判定。适合已经跑通第一次 BPE 作业、想对比统计向量与预测向量差异的同学。源码包里的 svd.py、sgns.py、result.py 分工清楚照着调整参数就能复现。2. 语料与子词词表BPE 切分结果如何直接喂给向量模型2.1 子词词表不是字符也不是词先确认输入格式这个源码包没有把 corpus.txt 直接丢给模型。它依赖第一次编程作业生成的子词词表和 BPE 切分结果。corpus.txt 是原始汉语语料train_BPE.txt 和 test_BPE.txt 是切分后的子词序列每一行是一个句子句子内部用空格分隔子词。为什么用子词而不是整词汉语词表外问题严重一个新词在测试时可能完全没有向量。BPE 会把低频词拆成更小片段至少让模型能组合出近似表示。第一次作业的子词词表通常来自训练语料中所有 BPE 合并结果所以这里所有子词都保留不再做 min_count 过滤。文件清单对应关系如下输入文件内容用途corpus.txt原始汉语文本备用本次不直接使用train_BPE.txtBPE 切分后的子词序列SVD/SGNS 训练test_BPE.txtBPE 切分后的子词序列补充训练语料扩大覆盖pku_sim_test.txt每行两个子词相似度评测集2.2 加载 BPE 结果并构造训练语料from pathlib import Path def load_subword_lines(*paths): sentences [] for path in paths: with Path(path).open(encodingutf-8) as fh: for line in fh: toks line.strip().split() if toks: sentences.append(toks) return sentences train_sents load_subword_lines(train_BPE.txt) test_sents load_subword_lines(test_BPE.txt) corpus train_sents test_sents print(len(corpus), sum(len(s) for s in corpus))这里把训练集和测试集做了并集然后一起参与向量训练。split()默认按连续空白字符切分能自动处理 BPE 文件中可能存在的多空格。sentences最终是「句子列表 套 子词列表」的结构后续 gensim 和共现矩阵构造都能直接复用。2.3 词表统计与未知词约定from collections import Counter freq Counter(tok for sent in corpus for tok in sent) vocab list(freq.keys()) wid {w: i for i, w in enumerate(vocab)} print(len(vocab), freq.most_common(5))这轮不使用min_count过滤因为 pku_sim_test.txt 里的词可能只在语料中出现一次过滤后就会成为 OOV评测时该行相似度只能写 0。如果计算资源确实紧张可以只取训练集的一部分但评测不区分这部分差异覆盖率下降会直接拉低大量词对的分数。注意corpus.txt 只是原始语料。如果第一次作业生成的 BPE 文件缺失才需要从 corpus.txt 重新跑 BPE。已经有 BPE 文件时不要再对子词做第二次切分否则会把已经稳定的词表打散。3. 基于 SVD 分解构建汉语子词分布向量3.1 为什么 distributional 表示选共现计数而不是 PMI作业里“获取高维 distributional 表示时 K5”本质是统计每个子词在上下文窗口中的共现次数。很多人会顺手换成 PMI 加权但 PMI 会放大低频噪声尤其在 BPE 子词这种切分粒度下低频子词本身就多换成 PPMI 后矩阵更稀疏SVD 分解得到的向量反而不稳定。这里保留原始共现计数直接做矩阵分解属于最稳妥的 baseline 做法。3.2 窗口 K5 的共现矩阵构造import numpy as np from scipy.sparse import lil_matrix window 5 n len(vocab) cooc lil_matrix((n, n), dtypenp.float64) for sent in corpus: ids [wid[t] for t in sent if t in wid] for i, center in enumerate(ids): start max(0, i - window) end min(len(ids), i window 1) for j in range(start, end): if j i: continue cooc[center, ids[j]] 1.0 cooc cooc.tocsr()这段用lil_matrix逐行累加最后转成csr_matrix给 SVD 使用。center是当前中心子词ids[j]是窗口内另一个子词窗口包含左右各 5 个位置。这里没有做距离衰减因为作业要求只提 K5加衰减会改变题目设定的可比性。如果语料规模很大建议改用coo_matrix收集三元组再求和稀疏矩阵的更新速度会比lil_matrix快不少。3.3 TruncatedSVD 降维与向量归一化from sklearn.decomposition import TruncatedSVD dim 300 svd TruncatedSVD(n_componentsdim, random_state42) vec_sta svd.fit_transform(cooc) print(vec_sta.shape, svd.explained_variance_ratio_.sum())TruncatedSVD是随机化 SVD 的封装支持直接输入稀疏 CSR 矩阵不会先把矩阵稠密化。dim是降维后的子词向量维数作业里自定一般取 100 到 300。random_state固定后多次运行结果一致方便对比实验。explained_variance_ratio_.sum()可以看到当前维度保留了共现矩阵多少方差常见范围在 40% 到 60%。norms np.linalg.norm(vec_sta, axis1, keepdimsTrue) vec_sta_norm vec_sta / (norms 1e-12) svd_vec {w: vec_sta_norm[i] for w, i in wid.items()}把向量归一化后后续计算余弦相似度就退化成点积省去每次算范数。1e-12是避免零向量除零。如果某个子词在所有上下文中的共现计数都是 0它的行向量是零向量归一化后依然是 0评测时这类词会被显式置为 0 分。参数作业规定说明K5共现窗口左右各 5 个子词dim自定100-300 常用太大容易过拟合权重无使用原始计数不做 PMI 加权4. 基于 SGNS 训练 Skip-gram 子词向量4.1 SGNS 与 SVD 在目标函数上的差别SGNS 是 Skip-gram 配合负采样Negative Sampling它不构造全量共现矩阵而是从语料中随机采样中心词-上下文词对并用少量负样本做二分类训练。和 SVD 的全局分解相比SGNS 对高频词有天然抑制作用训练过程是增量式的内存占用低但随机性更大需要固定随机种子才能复现。源码包里同时给出了svd.npy和model/SGNS.pth正好对应两套不同的向量产物。4.2 gensim 参数化配置sg1、window2 的含义from gensim.models import Word2Vec sgns Word2Vec( sentencescorpus, sg1, window2, vector_size300, negative5, min_count1, epochs10, sample0, seed42, workers4, )在 gensim 4.x 里sg1才表示 Skip-gramsg0是 CBOW。作业明确要求 SGNS所以这里必须用sg1。window2对应 K2 的上下文窗口默认值是 5不改成 2 就直接跑偏。negative5是每个正样本采样 5 个负样本这是 SGNS 的标准配置。sample0关闭高频子词下采样因为测试词如果只在语料中出现几次被下采样后可能彻底消失。seed42固定 Shuffle 顺序保证训练可复现。注意老教程里常见的size参数在 gensim 4.x 已改成vector_size直接运行旧代码会报TypeError。如果是自己实现的 skip_gram.py需要额外处理负采样分布gensim 内部已经把negative采样表封装好了。4.3 训练后词向量的读取与缺失词处理sgns_dict {w: sgns.wv[w] for w in sgns.wv.index_to_key} print(len(sgns_dict), len(vocab))gensim 4.x 中model.wv.vocab已经被移除必须用wv.index_to_key遍历词表。训练完成后可以判断sgns_dict是否和vocab等长如果少了优先检查min_count是不是被改过或者sample是否被设成了非 0 值。维度SVDSGNS训练方式全局矩阵分解局部随机梯度训练窗口K5K2低频词表现行向量稀疏容易被下采样丢弃结果文件svd_result.txtsgns_result.txt5. 相似度评测与结果格式校验技巧5.1 余弦相似度计算和缺失向量置零from pathlib import Path def read_pairs(path): return [ln.strip() for ln in Path(path).read_text(encodingutf-8).splitlines() if ln.strip()] def cosine_from_dict(pairs, vec): res [] for line in pairs: parts line.split() if len(parts) ! 2: res.append(0.0) continue w1, w2 parts v1, v2 vec.get(w1), vec.get(w2) if v1 is None or v2 is None: res.append(0.0) else: res.append(float(np.dot(v1, v2))) return res pairs read_pairs(pku_sim_test.txt) sim_svd cosine_from_dict(pairs, svd_vec) sim_sgns cosine_from_dict(pairs, sgns_dict)这里直接判断w1或w2是否在向量字典里缺失时整行写 0这是作业的硬性要求。前面已经把向量做了 L2 归一化所以np.dot就是余弦相似度。line.split()比line.split( )更稳能同时处理制表符和连续空格。5.2 输出格式的机器判定要点with open(svd_result.txt, w, encodingutf-8) as f: f.write(\n.join(f{x:.6f} for x in sim_svd)) with open(sgns_result.txt, w, encodingutf-8) as f: f.write(\n.join(f{x:.6f} for x in sim_sgns))机器判定通常按行读 float输出行数必须和pairs行数一致。.6f格式化后是标准 float 文本不会因为有效数字位数被误判。写文件前用断言检查长度和取值范围能省去不少调试时间assert len(sim_svd) len(pairs), svd result line count mismatch assert all(-1e-6 x 1 1e-6 for x in sim_svd), similarity out of range5.3 用相关性分析辅助调整超参数from scipy.stats import spearmanr rho, _ spearmanr(sim_svd, sim_sgns) print(rho)如果rho明显大于 0说明两套方法对相似词排序有共识如果接近 0优先检查窗口 K 是否按作业设置以及两个向量字典的覆盖率是否一致。另一个常用技巧是对 pku_sim_test.txt 里出现的高频子词分别取 SVD 和 SGNS 的 top-10 近邻直接观察“汉语”“处理”这类词的邻居能更直观地发现 SVD 偏向共现主题、SGNS 偏向功能词替换的差异。把sim_svd和sim_sgns的 Spearman 相关性打印出来再对照缺失词数量基本就能定位问题出在语料预处理还是窗口参数配置上。本文还有配套的精品资源点击获取
返回列表