NLP基础学习(NLP鱼书)
NLP基础学习(NLP鱼书)
基于同义词词典、计数的方法
- 最基础的方法即同义词词典,建立词间上下级关系,构造WordNet,需要大量人工标记。
- 优于同义词词典的方法即基于计数,即用共现矩阵统计语料库corpus中单词的单位距离(窗口大小)内共现次数;共现矩阵为对称矩阵,对应的行则为词的向量,可用余弦相似度判断词向量之间的相似度,越接近1表示越相似。
- 分布式假设是指某个单词的含义由它周围的单词形成,是向量表示单词的想法基础。
- 原始次数并不是一个好的信息指标,例如常用词(the...)会与部分名词达到很强的相关性,可以用PPMI(正的点互信息)表示其相关性,PPMI矩阵可以很好的表示词向量,但是维度与语料库中词数成正比,即词汇量大时单词向量维数也大;可以用奇异值分解降维PPMI矩阵,将稀疏向量转换为密集向量。
- PMI点互信息是用来衡量两个事件发生的概率与它们分别发生的概率的乘积的差异,即\(PMI(x,y)=P(x,y)-P(x)P(y)\),其中\(P(x,y)\)是事件\(x\)和\(y\)同时发生的概率,\(P(x)\)和\(P(y)\)分别是事件\(x\)和\(y\)发生的概率,可以很好的减少常用词对其他词的影响。
- PPMI(正的点互信息)是PMI的一种变形,即\(PPMI(x,y)=max(PMI(x,y),0)\),它将所有负值都设为0,使得PPMI矩阵中的元素都为非负。
- 因为由PPMI得出的矩阵比较稀疏,稳健性差,所以需要用奇异值分解(SVD)降维,将稀疏向量转换为密集向量,可是其复杂度较高,可以使用Truncated SVD截去奇异值较小的部分实现高速化。
基于推理的方法(Word2vec)
- 基于计数的方法是一次性处理整个语料库的统计数据获得单词分布式表示,而基于推理的方法通常是在mini-batch上进行,并且神经网络可以使用多GPU执行加速,具有优势。
- 每个词都用一个多维向量表示,向量之间的绝对距离即词与词义的相似度。
- word2vec常用CBOW和Skip-gram两种模型,CBOW是用上下文预测中心词,Skip-gram是用中心词预测上下文。
- CBOW的输入首先要同时经过一个嵌入层(原始为输入层),将输入的上下文词转换为对应的词向量表示(因为由one-hot向量表示,所以输入层的作用是用one-hot向量矩阵乘积从词向量矩阵中获取输入的词向量表示),然后将这些向量进行平均,得到一个表示上下文的向量,最后将这个向量输入到一个全连接层(输出层),输出一个表示中心词的向量。
- 无论是输入侧还是输出侧的权重都可以被视为单词的分布式表示,最受欢迎的是将输入侧的权重作为单词的分布式表示。
- Word2Vec实际就是由输入词用网络(Encode+Decode+softmax)计算共现词(上下文出现词)的得分,优化目标是在输入\(\omega_I\)时输出\(\omega_j\)的概率最高。
- 词袋模型(BOW)即是将文本视为词的无序集合,忽略语法词序,通过统计构建特征,由这构成的词向量的负向量接近的词并非反义词而是几乎完全无关的词,但是却擅长处理除语义相似以外的类比语义(\(\text{king}-\text{man}+\text{woman}=\text{queen}\)或\(\text{king}-\text{man}\approx\text{queen}-\text{woman}\))。
- Word2Vec中我们通常会建立一个外部词向量表和一个中心词向量表,但实际使用中通常只将词视为一个向量,实践中最常见是平均。
- skip-gram模型与CBOW模型的区别在于,skip-gram模型是用中心词预测上下文,而CBOW模型是用上下文预测中心词;我们应该用skip-gram模型,因为它的性能要优于CBOW模型在处理低频词、类推语义等方面,因为其训练的问题更难相较于CBOW。
- word2vec相较于基于计数的方法,编码除了单词的相似性以外,还能理解更复杂的单词之间的模式,但是就单词相似性的定量评价而言,基于推理的方法和基于计数的方法难分上下,因为两个方法论在某些条件下作用是相通的,及对整个语料库的共现矩阵进行特殊矩阵分解。
word2vec的高速化
-
首先是输入层的改进,将输入层换成嵌入层(embedding),即用直接通过单词 ID 索引权重矩阵的对应行替代one-hot向量的生成与乘积,由one-hot查表变为embedding查向量,减少输入的维度与计算量,计算量由\(O(V*N)\)减少为\(O(K)\)(K个上下文单词)。
-
其次是输出层的改进,将输出层的全量softmax替换为负采样(negative sampling),,从而达到轻量化的计算,计算量由\(O(V)\)减少为\(O(K)\)(K个负样本),K远小于V。
-
负采样的核心思想即不计算所有单词的概率,只计算正样本和少量负样本(随机单词)的概率,用二分类代替多分类,随机单词的采样来源于概率分布,即常用词被采样的概率要大于不常用词。
-
在选择解决问题的模型时,需要关注任务类型、语料库大小、单词向量维度等因素,根据不同的场景选择不同的模型,否则会导致表现差。
-
改进版word2vec的CBOW示例:
import numpy as np from collections import defaultdict # -------------------------- # 1. 数据预处理(构建词汇表和训练数据) # -------------------------- def preprocess(text, window_size=2): # 简单分词(示例用空格分割) words = text.split() # 构建词汇表(词→ID映射) vocab = defaultdict(int) for word in words: vocab[word] += 1 # 统计词频 vocab = {word: i for i, word in enumerate(vocab.keys())} vocab_size = len(vocab) # 生成训练数据:(上下文ID列表, 中心词ID) data = [] for i in range(window_size, len(words) - window_size): center_word = words[i] context_words = [words[i - window_size + j] for j in range(2 * window_size)] # 转换为ID center_id = vocab[center_word] context_ids = [vocab[w] for w in context_words] data.append((context_ids, center_id)) return data, vocab, vocab_size # -------------------------- # 2. 负采样工具(生成负样本) # -------------------------- def generate_negative_samples(positive_id, vocab_size, num_neg=5, power=0.75): """根据词频分布采样负样本(简化版:这里用均匀分布替代真实词频分布)""" negatives = [] while len(negatives) < num_neg: # 随机生成一个不等于正样本的ID neg_id = np.random.randint(vocab_size) if neg_id != positive_id and neg_id not in negatives: negatives.append(neg_id) return negatives # -------------------------- # 3. CBOW模型(含Embedding和负采样) # -------------------------- class CBOW: def __init__(self, vocab_size, embedding_dim=100): self.vocab_size = vocab_size # 词汇表大小 self.embedding_dim = embedding_dim # 词向量维度 # 初始化参数(Embedding矩阵和输出权重) # 输入Embedding矩阵 W_in: [vocab_size, embedding_dim] self.W_in = np.random.randn(vocab_size, embedding_dim) * 0.01 # 输出权重矩阵 W_out: [vocab_size, embedding_dim](每一行对应一个词的输出权重) self.W_out = np.random.randn(vocab_size, embedding_dim) * 0.01 def sigmoid(self, x): """Sigmoid激活函数(替代Softmax)""" return 1.0 / (1.0 + np.exp(-x)) def forward(self, context_ids): """前向传播:通过Embedding层获取上下文向量并平均""" # 1. Embedding层:直接索引提取上下文词向量(高速化改进1) context_vectors = self.W_in[context_ids] # [window_size*2, embedding_dim] # 2. 中间层:上下文向量求平均 hidden = np.mean(context_vectors, axis=0) # [embedding_dim] return hidden def loss(self, hidden, positive_id, negative_ids): """计算负采样损失(高速化改进2)""" # 正样本损失:log(sigmoid(正样本得分)) pos_score = np.dot(self.W_out[positive_id], hidden) # 内积计算得分 pos_loss = -np.log(self.sigmoid(pos_score) + 1e-8) # 防止log(0) # 负样本损失:sum(log(sigmoid(-负样本得分))) neg_loss = 0.0 for neg_id in negative_ids: neg_score = np.dot(self.W_out[neg_id], hidden) neg_loss -= np.log(self.sigmoid(-neg_score) + 1e-8) return pos_loss + neg_loss def backward(self, hidden, context_ids, positive_id, negative_ids, lr=0.01): """反向传播更新参数""" # 1. 计算输出侧梯度(正样本+负样本) pos_score = np.dot(self.W_out[positive_id], hidden) pos_delta = self.sigmoid(pos_score) - 1 # 正样本梯度 neg_deltas = [] for neg_id in negative_ids: neg_score = np.dot(self.W_out[neg_id], hidden) neg_deltas.append(self.sigmoid(neg_score)) # 负样本梯度 # 2. 更新输出权重 W_out self.W_out[positive_id] -= lr * pos_delta * hidden for i, neg_id in enumerate(negative_ids): self.W_out[neg_id] -= lr * neg_deltas[i] * hidden # 3. 计算隐藏层梯度,反向更新输入Embedding W_in hidden_delta = pos_delta * self.W_out[positive_id] for i, neg_id in enumerate(negative_ids): hidden_delta += neg_deltas[i] * self.W_out[neg_id] # 4. 更新输入Embedding(上下文每个词的向量都要更新) avg_delta = hidden_delta / len(context_ids) # 平均到每个上下文词 for ctx_id in context_ids: self.W_in[ctx_id] -= lr * avg_delta # -------------------------- # 4. 训练示例 # -------------------------- if __name__ == "__main__": # 示例文本(可替换为更大语料) text = "i like natural language processing and i like deep learning" # 预处理数据 data, vocab, vocab_size = preprocess(text, window_size=2) print("词汇表大小:", vocab_size) print("训练样本数:", len(data)) # 初始化模型 model = CBOW(vocab_size, embedding_dim=100) # 训练参数 epochs = 100 lr = 0.01 num_neg = 5 # 每个正样本配5个负样本 # 开始训练 for epoch in range(epochs): total_loss = 0.0 for context_ids, center_id in data: # 前向传播:获取上下文平均向量 hidden = model.forward(context_ids) # 生成负样本 negative_ids = generate_negative_samples(center_id, vocab_size, num_neg) # 计算损失 loss = model.loss(hidden, center_id, negative_ids) total_loss += loss # 反向传播更新 model.backward(hidden, context_ids, center_id, negative_ids, lr) # 每10轮打印一次损失 if (epoch + 1) % 10 == 0: print(f"Epoch {epoch+1}, Loss: {total_loss / len(data):.4f}") # 训练完成后,W_in即为词向量(Embedding) word = "like" if word in vocab: word_id = vocab[word] print(f"\n单词 '{word}' 的词向量(前5维):{model.W_in[word_id][:5]}")

浙公网安备 33010602011771号