【NLP 19、词的向量化和文本向量化】
祝我们都能在各自的选择中渐入佳境
—— 25.1.8
一、向量化
向量对于机器学习非常重要
大量的算法都需要基于向量来完成
1.文本向量化
对于机器来说,字符是没有含义的,只是有区别
只使用字符无法去刻画字与字、词与词、文本与文本之间的关系
文本转化为向量可以更好地刻画文本之间的关系
向量化后,可以启用大量的机器学习算法,具有很高的价值
文本是由词和字组成的,想将文本转化为向量,首先要能够把词和字转化为向量
所有向量应该有同一维度n,我们可以称这个n维空间是一个语义空间,为比较词向量之间的关系,字数不同、语言不同的词映射到的向量也都应该在同一维度的n维空间
二、one-hot编码
1.one-hot编码方式
首先统计一个字表或词表,选出n个字或词(标点符号和数字也可以看作词),one-hot编码的维度取决于词表的大小(one-hot编码的向量长度是词表中词数个数),然后通过向量的相似度来衡量文本的相似度
例:
考虑词频
在对文本向量化时,也可以考虑词频
例:
临时构建词表
有时也可以不事先准备词表,临时构建
如做文本比对任务,成对输入,此时维度可随时变化
例:
2.one-hot编码-缺点
① 如果有很多词,编码向量维度会很高,而且向量十分稀疏(大部分位置都是零),计算负担很大(维度灾难)
② 编码向量不能反映字词之间的 语义相似性,只能做到区分
③ 忽略了组成词的字的语序信息,忽略了顺序,例如:“你打我” 和 “我打你” 虽然意思不一样,但是由于组成词的字相同,他们的one-hot编码还是一样的
三、词向量 — Word2vec
我们希望得到一种词向量,使得向量关系能反映语义关系
比如: cos(你好, 您好) > cos(你好,天气)
注:cos值越大,则两向量越接近
即词义的相似性反映在向量的相似性
国王 - 男人 = 皇后 - 女人
即向量可以通过数值运算反映词之间的关系
同时,不管有多少词,向量维度应当是固定的
Word2vec 是一种用于自然语言处理(NLP)的深度学习模型,它的主要目的是将单词表示为向量形式。这些向量能够捕获单词的语义和语法信息,使得计算机可以更好地理解文本。
四、Word embedding 和 Word vector
二者本质上是一样的,都是以向量代表字符
一般说Word Embedding是指随机初始化的词向量或字向量本身
Word vector一般指训练词向量Word Embedding的一种方法,使得其向量具有一定的性质(向量相似度反映语义相似度)
五、One-hot编码 -> word vectors
① 将整个embedding矩阵看作一个线性层
② One-hot 编码作为输入
word embedding 权重矩阵
例:
五个字符,每个字符转化为三维的向量
word embedding的值是随机初始化得来
六、词向量训练方式① Word2Vec —— 基于语言模型
语言模型:用前n个字预测下一个字
公式:y = b + Wx + Utanh(d + Hx)
d + Hx:线性层 U:另一个线性层 tanh:激活函数 Utanh(d + Hx):双层线性层
b + Wx:单层线性层
x:若干个词向量的拼接
再过一层softmax归一化层,预测下一个字的类别
·
1.基于假设
做出假设:每段文本中的某一个词,由它前面n个词(上文)决定
例如:
2.步骤
① 转化为词向量
② 用序列中这些词的特征向量来表示词序列的联合概率函数
③ 同时学习单词特征向量和该概率函数的参数
预测下一个字,本质上可以看作是一个分类任务,内部用到交叉熵损失函数,将目前字词预测到哪一类,则下一个字就应该是哪一类
3.代码示例
Ⅰ 模型定义
① 继承父类:调用 super 方法继承 nn.Module 的属性和方法。
② 词向量层:创建一个词嵌入层 word_vectors,将词汇表中的每个词映射到一个固定维度的向量。
③ 投影层:定义多个线性变换层 Linear,用于将输入数据从一个维度转换到另一个维度。具体包括:
inner_projection_layer:将嵌入层输出展平后映射到隐藏层大小。
outter_projection_layer:将隐藏层映射到隐藏层。
x_projection_layer:将嵌入层输出展平后映射到隐藏层大小。
projection_layer:将隐藏层映射到词汇表大小。
class LanguageModel(nn.Module):
# 输入是词表大小、每个字的文本长度、嵌入维度、隐藏层维度
# 准备四个线性层,分别是词向量层、内部投影层、外部投影层、输出投影层
def __init__(self, vocab_size, max_len, embedding_size, hidden_size):
super(LanguageModel, self).__init__()
# 词向量层 word Ebedding
self.word_vectors = nn.Embedding(vocab_size, embedding_size)
self.inner_projection_layer = nn.Linear(embedding_size * max_len, hidden_size)
self.outter_projection_layer = nn.Linear(hidden_size, hidden_size)
self.x_projection_layer = nn.Linear(embedding_size * max_len, hidden_size)
self.projection_layer = nn.Linear(hidden_size, vocab_size)
Ⅱ 前向计算
① self.word_vectors(context):将输入的context文本转换为对应的词向量
② context_embedding.view():使用 view
方法对 context_embedding
的维度进行调整。view
函数可以重塑张量(Tensor)的形状,在不改变数据本身的情况下改变其维度排列
③ self.inner_projection_layer(x):调用线性层,将拼接后的特征向量 x
作为输入,经过该线性层的线性变换
④ torch.tanh(inner_projection) :应用 torch.tanh
双曲正切激活函数
⑤ self.outter_projection_layer():调用另一个 nn.Linear
全连接层,对经过 tanh
激活后的 inner_projection
进行进一步的线性变换
⑥ self.x_projection_layer(x):再次调用全连接层,将最初的拼接特征向量 x
作为输入,进行线性变换后输出 x_projection
⑦ x_projection + outter_project:将 x_projection
和 outter_project
两个张量对应元素相加,得到 y
⑧ torch.softmax(y, dim=-1):对融合后的特征 y
使用 torch.softmax
函数。softmax
函数常用于多分类任务中,它将输入的数值向量转换为一个概率分布向量,使得向量中各元素的值在 0 到 1 之间,且所有元素之和等于 1。
def forward(self, context):
#context shape = batch_size, max_length
context_embedding = self.word_vectors(context) #output shape = batch_size, max_length, embedding_size
#总体计算 y = b+Wx+Utanh(d+Hx), 其中x为每个词向量的拼接
#词向量的拼接
x = context_embedding.view(context_embedding.shape[0], -1) #shape = batch_size, max_length*embedding_size
#hx + d
inner_projection = self.inner_projection_layer(x) #shape = batch_size, hidden_size
#tanh(hx+d)
inner_projection = torch.tanh(inner_projection) #shape = batch_size, hidden_size
#U * tanh(hx+d) + b
outter_project = self.outter_projection_layer(inner_projection) # shape = batch_size, hidden_size
#Wx
x_projection = self.x_projection_layer(x) #shape = batch_size, hidden_size
#y = Wx + Utanh(hx+d) + b
y = x_projection + outter_project #shape = batch_size, hidden_size
#softmax后输出预测概率, 训练的目标是让y_pred对应到字表中某个字
y_pred = torch.softmax(y, dim=-1) #shape = batch_size, hidden_size
return y_pred
Ⅲ 设置参数
① vocab_size:词表大小,词的个数
② embedding_size:转换后人为指定的向量维度
③ max_len:输入词的最大长度
④ hiddensize:隐含层输入向量的维度
⑤ LanguageModel:传入参数,构建模型
vocab_size = 8 #词表大小
embedding_size = 5 #人为指定的向量维度
max_len = 4 #输入长度
hidden_size = vocab_size #由于最终的输出维度应当是字表大小的,所以这里hidden_size = vocab_size
model = LanguageModel(vocab_size, max_len, embedding_size, hidden_size)
Ⅳ 进行预测
#coding:utf8
import torch
import torch.nn as nn
import numpy as np
"""
基于pytorch的语言模型
与基于窗口的词向量训练本质上非常接近
只是输入输出的预期不同
不使用向量的加和平均,而是直接拼接起来
"""
class LanguageModel(nn.Module):
# 输入是词表大小、每个字的文本长度、嵌入维度、隐藏层维度
# 准备四个线性层,分别是词向量层、内部投影层、外部投影层、输出投影层
def __init__(self, vocab_size, max_len, embedding_size, hidden_size):
super(LanguageModel, self).__init__()
# 词向量层 word Ebedding
self.word_vectors = nn.Embedding(vocab_size, embedding_size)
self.inner_projection_layer = nn.Linear(embedding_size * max_len, hidden_size)
self.outter_projection_layer = nn.Linear(hidden_size, hidden_size)
self.x_projection_layer = nn.Linear(embedding_size * max_len, hidden_size)
self.projection_layer = nn.Linear(hidden_size, vocab_size)
def forward(self, context):
#context shape = batch_size, max_length
context_embedding = self.word_vectors(context) #output shape = batch_size, max_length, embedding_size
#总体计算 y = b+Wx+Utanh(d+Hx), 其中x为每个词向量的拼接
#词向量的拼接
x = context_embedding.view(context_embedding.shape[0], -1) #shape = batch_size, max_length*embedding_size
#hx + d
inner_projection = self.inner_projection_layer(x) #shape = batch_size, hidden_size
#tanh(hx+d)
inner_projection = torch.tanh(inner_projection) #shape = batch_size, hidden_size
#U * tanh(hx+d) + b
outter_project = self.outter_projection_layer(inner_projection) # shape = batch_size, hidden_size
#Wx
x_projection = self.x_projection_layer(x) #shape = batch_size, hidden_size
#y = Wx + Utanh(hx+d) + b
y = x_projection + outter_project #shape = batch_size, hidden_size
#softmax后输出预测概率, 训练的目标是让y_pred对应到字表中某个字
y_pred = torch.softmax(y, dim=-1) #shape = batch_size, hidden_size
return y_pred
vocab_size = 8 #词表大小
embedding_size = 5 #人为指定的向量维度
max_len = 4 #输入长度
hidden_size = vocab_size #由于最终的输出维度应当是字表大小的,所以这里hidden_size = vocab_size
model = LanguageModel(vocab_size, max_len, embedding_size, hidden_size)
#假如选取一个文本窗口“天王盖地虎”
#输入:“天王盖地” —> 输出:"虎"
#假设词表embedding中, 天王盖地虎 对应位置 12345
context = torch.LongTensor([[1,2,3,4]]) #shape = 1, 4 batch_size = 1, max_length = 4
pred = model(context)
print("预测值:", pred)
print("loss可以使用交叉熵计算:", nn.functional.cross_entropy(pred, torch.LongTensor([5])))
print("词向量矩阵")
matrix = model.state_dict()["word_vectors.weight"]
print(matrix.shape) #vocab_size, embedding_size
print(matrix)
七、词向量训练方式② Word2Vec —— 基于窗口
1.基于假设
做假设:如果两个词在文本中出现时,它的前后出现的词相似,则这两个词语义相似
换句话说,相似的词有相似的上下文
基于窗口的词向量训练方式会忽略语序信息,而只要样本数量足够多,语序信息并不会影响词向量的预测结果
词向量的本质是训练目标的选择
例如:
2.CBOW模型:周围词预测中间词
基于前述思想,我们尝试用窗口中的词(或者说周围词)来表示(预测)中间词
窗口:你 想 明白 了 吗
输入:你 想 了 吗
输出:明白
pooling池化层池化后,接一个线性层做分类
训练方式
①
②
③
④
⑤
代码示例
Ⅰ 模型构建
① 继承父类:调用super(CBOW, self).__init__(),确保父类nn.Module的初始化方法被执行
② 词嵌入层:创建一个词嵌入层self.word_vectors,将词汇表中的每个词映射到一个固定维度的向量空间
③ 池化层:创建一个平均池化层self.pooling,用于对上下文窗口内的词向量进行平均处理
④ 线性层:创建一个线性层self.projection_layer,用于将平均后的词向量映射回词汇表大小的输出空间
def __init__(self, vocab_size, embedding_size, window_length):
super(CBOW, self).__init__()
# 词嵌入层word_vectors
self.word_vectors = nn.Embedding(vocab_size, embedding_size)
# 平均池化层pooling
self.pooling = nn.AvgPool1d(window_length)
# 投影层projection_layer
self.projection_layer = nn.Linear(embedding_size, vocab_size)
Ⅱ 前向传播
获取词向量:输入上下文单词的索引,通过词嵌入层获取对应的词向量。
平均池化:将词向量矩阵转置并进行平均池化,得到上下文表示。
投影到词汇表维度:将上下文表示通过线性层映射到词汇表大小的维度,得到预测的目标词。
def forward(self, context):
# 输入上下文单词的索引context,通过词嵌入层获取词向量
context_embedding = self.word_vectors(context) #batch_size * max_length * embedding size 1*4*4
# 将词向量矩阵转置并进行平均池化,得到上下文表示
#transpose: batch_size * embedding size * max_length -> pool: batch_size * embedding_size * 1 -> squeeze:batch_size * embeddig_size
context_embedding = self.pooling(context_embedding.transpose(1, 2)).squeeze()
# 上下文表示通过投影层映射到词汇表大小的维度,得到预测的目标词
#batch_size * embeddig_size -> batch_size * vocab_size
pred = self.projection_layer(context_embedding)
return pred
Ⅲ 设置参数
设置词表大小、输入的向量维度、窗口的长度
vocab_size = 8 #词表大小
embedding_size = 4 #人为指定的向量维度
window_length = 4 #窗口长度
Ⅳ 进行预测
#coding:utf8
import torch
import torch.nn as nn
import numpy as np
"""
基于pytorch的词向量CBOW
模型部分
"""
class CBOW(nn.Module):
def __init__(self, vocab_size, embedding_size, window_length):
super(CBOW, self).__init__()
# 词嵌入层word_vectors
self.word_vectors = nn.Embedding(vocab_size, embedding_size)
# 平均池化层pooling
self.pooling = nn.AvgPool1d(window_length)
# 投影层projection_layer
self.projection_layer = nn.Linear(embedding_size, vocab_size)
# 前向传播:输入context,输出预测值
def forward(self, context):
# 输入上下文单词的索引context,通过词嵌入层获取词向量
context_embedding = self.word_vectors(context) #batch_size * max_length * embedding size 1*4*4
# 将词向量矩阵转置并进行平均池化,得到上下文表示
#transpose: batch_size * embedding size * max_length -> pool: batch_size * embedding_size * 1 -> squeeze:batch_size * embeddig_size
context_embedding = self.pooling(context_embedding.transpose(1, 2)).squeeze()
# 上下文表示通过投影层映射到词汇表大小的维度,得到预测的目标词
#batch_size * embeddig_size -> batch_size * vocab_size
pred = self.projection_layer(context_embedding)
return pred
vocab_size = 8 #词表大小
embedding_size = 4 #人为指定的向量维度
window_length = 4 #窗口长度
model = CBOW(vocab_size, embedding_size, window_length)
#假如选取一个词窗口【1,2,3,4,5】·
context = torch.LongTensor([[1,2,4,5]]) #输入1,2,4,5, 预期输出3, 两边预测中间
pred = model(context)
print("预测值:", pred)
# print("词向量矩阵")
# print(model.state_dict()["word_vectors.weight"])
3.Skip-Gram模型:中间词预测周围词
用中间词来表示(预测)周围词
例:
训练方式
① 首先需要一个大规模的文本语料库,例如维基百科文章、新闻文章、小说等。将文本语料库中的单词转换为唯一的整数索引,构建词汇表。
② 对于语料库中的每个中心词,我们需要找到其上下文词。通常使用一个窗口大小 window_size 来确定上下文范围。
③ 定义一个词嵌入层embedding,将词映射到指定维度大小的向量
④ 将过embedding后的词向量投影到规定大小窗口中上下文的词向量上,通过映射得到的词向量进行预测
代码示例
Ⅰ 模型构建
self.word_vectors = nn.Embedding(vocab_size, embedding_size)
:创建一个词嵌入层,将词汇表中的每个词映射到embedding_size
维的向量空间self.projection_layer = nn.Linear(embedding_size, vocab_size)
:创建一个线性投影层,将embedding_size
维的中心词向量映射到词汇表大小的维度,得到预测的上下文词的概率分布
class SkipGram(nn.Module):
def __init__(self, vocab_size, embedding_size):
super(SkipGram, self).__init__()
# 词嵌入层 word_vectors
self.word_vectors = nn.Embedding(vocab_size, embedding_size)
# 投影层 projection_layer
self.projection_layer = nn.Linear(embedding_size, vocab_size)
Ⅱ 前向传播
center_embedding = self.word_vectors(center_word)
:将输入的中心词的索引通过词嵌入层转换为其对应的词向量pred = self.projection_layer(center_embedding)
:将中心词向量通过线性投影层得到预测的上下文词的概率分布- 返回预测的上下文词的概率
# 前向传播:输入中心词,输出预测的上下文词的概率分布
def forward(self, center_word):
# 输入中心词的索引 center_word,通过词嵌入层获取词向量
center_embedding = self.word_vectors(center_word) # batch_size * embedding_size
# 中心词向量通过投影层映射到词汇表大小的维度,得到预测的上下文词的概率分布
pred = self.projection_layer(center_embedding) # batch_size * vocab_size
return pred
Ⅲ 设置参数
- 设定
vocab_size词表大小
和embedding_size指定的向量维度
- 创建
SkipGram
模型的实例model
- 定义
center_word
,这里使用torch.LongTensor([[3]])
表示一个中心词的索引 - 调用
model(center_word)
进行前向传播,得到预测结果pred
。
vocab_size = 8 # 词表大小
embedding_size = 4 # 人为指定的向量维度
model = SkipGram(vocab_size, embedding_size)
# 假如选取一个中心词 3,预期输出其上下文词 [1, 2, 4, 5]
center_word = torch.LongTensor([[3]]) # 输入中心词 3
pred = model(center_word)
print("预测值:", pred)
Ⅳ 进行预测
#coding:utf8
import torch
import torch.nn as nn
import numpy as np
"""
基于pytorch的词向量SkipGram
模型部分
"""
class SkipGram(nn.Module):
def __init__(self, vocab_size, embedding_size):
super(SkipGram, self).__init__()
# 词嵌入层 word_vectors
self.word_vectors = nn.Embedding(vocab_size, embedding_size)
# 投影层 projection_layer
self.projection_layer = nn.Linear(embedding_size, vocab_size)
# 前向传播:输入中心词,输出预测的上下文词的概率分布
def forward(self, center_word):
# 输入中心词的索引 center_word,通过词嵌入层获取词向量
center_embedding = self.word_vectors(center_word) # batch_size * embedding_size
# 中心词向量通过投影层映射到词汇表大小的维度,得到预测的上下文词的概率分布
pred = self.projection_layer(center_embedding) # batch_size * vocab_size
return pred
vocab_size = 8 # 词表大小
embedding_size = 4 # 人为指定的向量维度
model = SkipGram(vocab_size, embedding_size)
# 假如选取一个中心词 3,预期输出其上下文词 [1, 2, 4, 5]
center_word = torch.LongTensor([[3]]) # 输入中心词 3
pred = model(center_word)
print("预测值:", pred)
# print("词向量矩阵")
# print(model.state_dict()["word_vectors.weight"])
八、词向量训练方式③ Word2Vec —— 基于共现矩阵 Glove
1.基于共现矩阵
n × n 的矩阵:每一个词旁边出现其他词的次数,选取窗口长度为1
例:
2.共现概率
公式:
词j 出现在词i 周围的概率,被称为词i 和词j 的共现概率
P(天气 | 今天) = 2 / 2 = 1;P(不错 | 天气) = 2 / 6 = 1/3
3.共现概率比
共现概率比从语料中进行统计
两个共现概率的比值:
如果词A与词B的相关性大于词A与词C的相关性,则共现概率比:P(A|B) / P(A|C) 会较高,反之亦然,在大量的语料统计下,共现概率比会呈现出这三个词之间相关性的信息,共现概率体现了两个词之间的相关性
问题转化:
给定三个词的词向量,Va, Vb, Vc三者的通过某个函数映射后,其比值应接近ABC的共现概率比
即目标为找到向量使得 f(Va, Vb, Vc) = P(A|B) / P(A|C)
预测数值,属于回归问题, 损失函数使用均方差
f的设计,论文中给出的是f(Va, Vb, Vc) = (Va - Vb ) · Vc
九、词向量训练总结
1.词向量训练的过程
1.根据词与词之间关系的某种假设,制定训练目标
2.设计模型,以词向量为输入
3.随机初始化词向量,开始训练
4.训练过程中词向量作为参数不断调整,获取一定的语义信息
5.使用训练好的词向量做下游任务
2.词向量训练的问题
1.输出层使用one-hot向量会面临维度灾难,因为词表可能很大。
2.收敛速度缓慢
3.四种词向量训练方式:语言模型 / Skip-Gram / CBOW / GloVe
① 语言模型 以前n个字预测下一个字
原理:利用前n个字预测下一个字,预测下一个字,本质上可以看作是一个分类任务,内部用到交叉熵损失函数,将目前字词预测到哪一类,则下一个字就应该是哪一类
应用场景:文本分类、机器翻译、情感分析、问答系统、命名实体识别
优势:
语义理解更深入:语言模型能够学习到单词在不同上下文语境中的含义变化,从而生成更具语义丰富性和准确性的词向量。相比传统的词向量模型,如 Word2Vec 和 GloVe 等,基于语言模型的词向量可以更好地捕捉多义词、同义词、反义词等语义关系;
上下文适应性强:可以根据具体的上下文动态地调整词向量的表示。在不同的句子中,同一个单词可能具有不同的语义和语法角色,基于语言模型的词向量能够更好地适应这种变化,为每个单词在特定上下文中提供更合适的向量表示,从而提高自然语言处理任务的性能;
模型泛化能力好:通过在大规模语料上进行预训练,语言模型可以学习到通用的语言知识和规律,这些知识可以被迁移到各种不同的自然语言处理任务中。基于预训练的语言模型生成的词向量具有较好的泛化能力,能够在不同的领域和任务中发挥作用,减少了针对特定任务重新训练词向量的需求,节省了时间和计算资源;
利用大规模语料:语言模型通常需要大量的文本数据进行训练,这使得词向量能够充分吸收丰富的语言信息,更好地覆盖各种语言现象和词汇用法,从而提高词向量的质量和代表性。对于低频词和罕见词,也能通过大规模语料中的上下文信息学习到相对合理的向量表示。
② Skip-Gram 基于中间词预测两边词
原理:Skip - Gram 是一种基于神经网络的词向量训练方法,属于 Word2Vec 模型的一种架构。它的核心思想是通过中心词来预测其上下文单词。例如,对于句子 “我爱自然语言处理”,如果以 “爱” 为中心词,Skip - Gram 会尝试预测 “我” 和 “自然语言处理” 这些在一定窗口范围内的上下文词。它通过构建一个神经网络,将单词的索引作为输入,经过词嵌入层(将单词索引转换为低维向量),然后通过投影层(线性层)等进行计算,最后输出预测的上下文单词的概率分布。训练过程是基于给定的语料库,通过最大化中心词对其上下文词的预测概率来学习词向量
应用场景:在自然语言处理任务中广泛应用,如信息检索、文本生成、机器翻译等。例如,在机器翻译中,词向量可以帮助模型更好地理解源语言和目标语言中的单词语义,提高翻译质量。
优势:能够有效地捕捉单词之间的语义和句法关系,特别是对于低频词也能较好地学习其语义表示。而且,Skip - Gram 能够很好地处理一词多义的情况,因为它是基于中心词来学习上下文关系的
③ CBOW 基于两边词预测中间词
原理:CBOW 与 Skip - Gram 相反,它是通过上下文单词来预测中心词。同样以 “我爱自然语言处理” 为例,CBOW 会利用 “我” 和 “自然语言处理” 来预测 “爱”。它假设上下文单词的顺序对预测中心词的影响较小,将上下文单词的词向量进行某种组合(如平均),然后通过神经网络映射到词汇表维度,输出预测的中心词的概率分布。
应用场景:在文本分类、情感分析等任务中表现良好。例如,在文本分类中,CBOW 生成的词向量可以作为文本特征输入到分类模型中,帮助模型理解文本的语义,提高分类准确率。
优势:对于文本数据中常见词的语义学习效果较好,并且在训练过程中,由于是基于上下文单词的组合来预测中心词,计算效率相对较高,尤其在处理大规模文本数据时,训练速度可能比 Skip - Gram 更快
④ GloVe 基于词的共现矩阵
原理:GloVe 基于词的共现矩阵来学习词向量。它的基本思想是通过统计大规模语料库中词与词的共现次数,构建词 - 词共现矩阵。例如,如果在一个语料库中,单词 “苹果” 和 “水果” 经常同时出现,它们在共现矩阵中的相应元素值就会较高。然后,利用这些共现信息,通过一种基于矩阵分解的方法来学习词向量,使得词向量之间的点积能够近似反映词共现概率的对数。
应用场景:在词相似度计算、文本聚类、命名实体识别等多种自然语言处理任务中都有广泛应用。例如,在命名实体识别中,GloVe 词向量可以帮助模型区分不同类型的实体名称,提高识别准确率。
优势:能够有效地利用语料库中的全局统计信息,结合了全局的单词共现统计和局部的上下文信息,相比单纯基于局部上下文的方法(如 Skip - Gram 和 CBOW),在一些任务上能够更好地捕捉词之间的语义相似性和长期依赖关系。同时,GloVe 词向量的训练过程相对简单,计算效率较高,适合处理大规模文本数据。
十、词向量训练的加速技巧
1.技巧① Huffman哈夫曼树
对所有词进行二进制编码,使其符合以下特点:
1)不同词编码不同
2)每个词的编码不会成为另一个词编码的前缀
即:如果某个词编码为011,则不能有词的编码是0111 或0110 或011001等
3)构造出的词编码总体长度最小,且词出现频率越高编码越短
4)哈夫曼树编码方法可以把一个多分类问题 拆分成:多个二分类问题
Ⅰ 训练过程
① 对词按照词频进行排序
② 选取词频最低的两个节点,词频更低的放在左侧,较低的放在右侧,形成一个二叉树,顶点的值记录为二者频率之和
③ 选择剩余词中词频最小的,将其与前两个词之和对比,仍然是小的放在左边,较大的放在右边(词频相同放在哪边都可以),形成树
④ 将所有节点进行遍历判断加入树中,生成最终的哈夫曼树,编码从根节点开始,左子树为0,右子树为1(也可左1右0),由此进行编码,且所有词都在哈夫曼树的叶子节点上
Ⅱ 代码实现
① 定义哈夫曼树节点类
word_id:存储叶子节点对应的词ID或中间节点的ID
frequency:存储单词的频次
left_child 和 right_child:分别存储左子节点和右子节点,默认为 None
father:存储父节点,默认为 None
Huffman_code:存储哈夫曼编码(左0右1),初始为空列表
path:存储从根到叶子节点的路径,默认为空列表
class HuffmanNode:
def __init__(self, word_id, frequency):
self.word_id = word_id # 叶子结点存词对应的id, 中间节点存中间节点id
self.frequency = frequency # 存单词频次
self.left_child = None
self.right_child = None
self.father = None
self.Huffman_code = [] # 哈夫曼码(左0右1)
self.path = [] # 根到叶子节点的中间节点id
② 定义哈夫曼树类
1.初始化属性:
计算单词数量并存储在self.word_count。
初始化词id和哈夫曼码、路径的映射字典self.wordid_code和self.wordid_path。
dict() 是 Python 内置的函数,用于创建一个新的字典(dictionary)
初始化Huffman树的根节点self.root为None。
2.创建未合并节点列表:
根据输入的wordid_frequency_dict创建一个包含所有叶子节点的列表unmerge_node_list。
3.存储所有节点:
将所有叶子节点存储在self.huffman中,后续会添加中间节点。
4.构建Huffman树:
调用build_tree方法,使用unmerge_node_list构建Huffman树,并更新self.root。
5.生成哈夫曼编码和路径:
调用generate_huffman_code_and_path方法,生成每个词id对应的哈夫曼编码和路径。
class HuffmanTree:
def __init__(self, wordid_frequency_dict):
# 单词数量
self.word_count = len(wordid_frequency_dict) # 单词数量
# 词id和霍夫曼码的映射
self.wordid_code = dict()
# 词id和路径的映射
self.wordid_path = dict()
# Huffman树的根节点
self.root = None
# 创建未合并节点列表
unmerge_node_list = [HuffmanNode(wordid, frequency) for wordid, frequency in
wordid_frequency_dict.items()] # 未合并节点list
# 将所有叶子节点和中间节点存储在self.huffman中
self.huffman = [HuffmanNode(wordid, frequency) for wordid, frequency in
wordid_frequency_dict.items()] # 存储所有的叶子节点和中间节点
# 构建huffman tree
self.build_tree(unmerge_node_list)
# 生成huffman code
self.generate_huffman_code_and_path()
③ 合并两个节点并创建一个新的父节点
1.计算频率和:将两个节点的频率相加,得到新节点的频率。
2.生成中间节点ID:使用当前 self.huffman 列表的长度作为新节点的ID。
3.创建父节点:根据频率大小决定左右子节点,并创建新的父节点。
4.添加到列表:将新创建的父节点添加到 self.huffman 列表中。
5.返回父节点:返回新创建的父节点
# 合并两个节点并创建一个新的父节点
def merge_node(self, node1, node2):
sum_frequency = node1.frequency + node2.frequency
mid_node_id = len(self.huffman) # 中间节点的value存中间节点id
father_node = HuffmanNode(mid_node_id, sum_frequency)
if node1.frequency >= node2.frequency:
father_node.left_child = node1
father_node.right_child = node2
else:
father_node.left_child = node2
father_node.right_child = node1
self.huffman.append(father_node)
return father_node
④ Huffman编码树的构建过程
1.检查node_list的长度是否大于1,如果是则继续,否则跳转到设置根节点。
2.初始化最小和次小节点的索引。
3.遍历node_list,找到频率最小和次小的两个节点。
4.合并这两个节点,生成新的父节点。
5.判断i1是否小于i2,如果是则删除i2和i1节点;否则进入下一步判断。
6.判断i1是否大于i2,如果是则删除i1和i2节点;否则抛出异常。
7.将新生成的父节点插入到node_list的开头。
8.当node_list长度为1时,设置根节点
# Huffman编码树的构建过程
def build_tree(self, node_list):
while len(node_list) > 1:
i1 = 0 # 概率最小的节点
i2 = 1 # 概率第二小的节点
if node_list[i2].frequency < node_list[i1].frequency:
[i1, i2] = [i2, i1]
for i in range(2, len(node_list)):
if node_list[i].frequency < node_list[i2].frequency:
i2 = i
if node_list[i2].frequency < node_list[i1].frequency:
[i1, i2] = [i2, i1]
father_node = self.merge_node(node_list[i1], node_list[i2]) # 合并最小的两个节点
if i1 < i2:
node_list.pop(i2)
node_list.pop(i1)
elif i1 > i2:
node_list.pop(i1)
node_list.pop(i2)
else:
raise RuntimeError('i1 should not be equal to i2')
node_list.insert(0, father_node) # 插入新节点
self.root = node_list[0]
⑤ 生成哈夫曼编码及其路径
1.初始化栈:将根节点加入栈。
2.遍历树:从栈中弹出一个节点,沿着左子树遍历。更新左右子树的哈夫曼编码和路径,并将右子树节点压入栈。
3.记录叶节点信息:当遍历到叶子节点时,记录其哈夫曼编码和路径,并更新全局字典。
# 生成哈夫曼编码及其路径
def generate_huffman_code_and_path(self):
stack = [self.root]
while len(stack) > 0:
node = stack.pop()
# 顺着左子树走
while node.left_child or node.right_child:
code = node.Huffman_code
path = node.path
node.left_child.Huffman_code = code + [1]
node.right_child.Huffman_code = code + [0]
node.left_child.path = path + [node.word_id]
node.right_child.path = path + [node.word_id]
# 把没走过的右子树加入栈
stack.append(node.right_child)
node = node.left_child
word_id = node.word_id
word_code = node.Huffman_code
word_path = node.path
self.huffman[word_id].Huffman_code = word_code
self.huffman[word_id].path = word_path
# 把节点计算得到的霍夫曼码、路径 写入词典的数值中
self.wordid_code[word_id] = word_code
self.wordid_path[word_id] = word_path
⑥ 获取所有词的正向节点id和负向节点id数组
1.初始化两个空列表 positive 和 negative,分别用于存储所有词的正向和负向路径
2.遍历每个词(通过 word_id),为每个词初始化两个空列表 pos_id 和 neg_id,分别用于存储该词的正向和负向节点ID
3.遍历该词的Huffman编码,根据编码值(0或1)将对应的路径节点ID添加到 pos_id 或 neg_id
4.将每个词的 pos_id 和 neg_id 分别添加到 positive 和 negative 列表中。
5.返回 positive 和 negative。
# 获取所有词的正向节点id和负向节点id数组
def get_all_pos_and_neg_path(self):
positive = [] # 所有词的正向路径数组
negative = [] # 所有词的负向路径数组
for word_id in range(self.word_count):
pos_id = [] # 存放一个词 路径中的正向节点id
neg_id = [] # 存放一个词 路径中的负向节点id
for i, code in enumerate(self.huffman[word_id].Huffman_code):
if code == 1:
pos_id.append(self.huffman[word_id].path[i])
else:
neg_id.append(self.huffman[word_id].path[i])
positive.append(pos_id)
negative.append(neg_id)
⑦ 哈夫曼树的实现
"""
构建哈夫曼树
"""
class HuffmanNode:
def __init__(self, word_id, frequency):
self.word_id = word_id # 叶子结点存词对应的id, 中间节点存中间节点id
self.frequency = frequency # 存单词频次
self.left_child = None
self.right_child = None
self.father = None
self.Huffman_code = [] # 霍夫曼码(左1右0)
self.path = [] # 根到叶子节点的中间节点id
class HuffmanTree:
def __init__(self, wordid_frequency_dict):
self.word_count = len(wordid_frequency_dict) # 单词数量
self.wordid_code = dict()
self.wordid_path = dict()
self.root = None
unmerge_node_list = [HuffmanNode(wordid, frequency) for wordid, frequency in
wordid_frequency_dict.items()] # 未合并节点list
self.huffman = [HuffmanNode(wordid, frequency) for wordid, frequency in
wordid_frequency_dict.items()] # 存储所有的叶子节点和中间节点
# 构建huffman tree
self.build_tree(unmerge_node_list)
# 生成huffman code
self.generate_huffman_code_and_path()
def merge_node(self, node1, node2):
sum_frequency = node1.frequency + node2.frequency
mid_node_id = len(self.huffman) # 中间节点的value存中间节点id
father_node = HuffmanNode(mid_node_id, sum_frequency)
if node1.frequency >= node2.frequency:
father_node.left_child = node1
father_node.right_child = node2
else:
father_node.left_child = node2
father_node.right_child = node1
self.huffman.append(father_node)
return father_node
def build_tree(self, node_list):
while len(node_list) > 1:
i1 = 0 # 概率最小的节点
i2 = 1 # 概率第二小的节点
if node_list[i2].frequency < node_list[i1].frequency:
[i1, i2] = [i2, i1]
for i in range(2, len(node_list)):
if node_list[i].frequency < node_list[i2].frequency:
i2 = i
if node_list[i2].frequency < node_list[i1].frequency:
[i1, i2] = [i2, i1]
father_node = self.merge_node(node_list[i1], node_list[i2]) # 合并最小的两个节点
if i1 < i2:
node_list.pop(i2)
node_list.pop(i1)
elif i1 > i2:
node_list.pop(i1)
node_list.pop(i2)
else:
raise RuntimeError('i1 should not be equal to i2')
node_list.insert(0, father_node) # 插入新节点
self.root = node_list[0]
def generate_huffman_code_and_path(self):
stack = [self.root]
while len(stack) > 0:
node = stack.pop()
# 顺着左子树走
while node.left_child or node.right_child:
code = node.Huffman_code
path = node.path
node.left_child.Huffman_code = code + [1]
node.right_child.Huffman_code = code + [0]
node.left_child.path = path + [node.word_id]
node.right_child.path = path + [node.word_id]
# 把没走过的右子树加入栈
stack.append(node.right_child)
node = node.left_child
word_id = node.word_id
word_code = node.Huffman_code
word_path = node.path
self.huffman[word_id].Huffman_code = word_code
self.huffman[word_id].path = word_path
# 把节点计算得到的霍夫曼码、路径 写入词典的数值中
self.wordid_code[word_id] = word_code
self.wordid_path[word_id] = word_path
# 获取所有词的正向节点id和负向节点id数组
def get_all_pos_and_neg_path(self):
positive = [] # 所有词的正向路径数组
negative = [] # 所有词的负向路径数组
for word_id in range(self.word_count):
pos_id = [] # 存放一个词 路径中的正向节点id
neg_id = [] # 存放一个词 路径中的负向节点id
for i, code in enumerate(self.huffman[word_id].Huffman_code):
if code == 1:
pos_id.append(self.huffman[word_id].path[i])
else:
neg_id.append(self.huffman[word_id].path[i])
positive.append(pos_id)
negative.append(neg_id)
return positive, negative
def main():
words = "你 我 他 你们 我们 他们 它们"
freqs = "50 10 8 7 6 3 2"
word_to_id = dict((word, i) for i, word in enumerate(words.split()))
print(word_to_id)
word_frequency = dict((word_to_id[x], int(y)) for x, y in zip(words.split(), freqs.split()))
tree = HuffmanTree(word_frequency)
word_code = dict((word, tree.wordid_code[word_to_id[word]]) for word in words.split())
print(word_code)
if __name__ == '__main__':
main()
2.技巧② 负采样 - negative sampling
以大化小,把正样本随机分为n个小轮次,每轮放入一个正确样本,在词表中随机挑选其余的错误样本,然后在每个小轮次进行判断,可以理解成一种抽样检测
词向量训练最终采取softmax作为激活函数,得到预测词的分布
只要词表中数据足够多,随机采取负样本,轮数够多,就与全部采样效果差不多
负采样:负样本通常容易获取,正样本不易获取,可以达到大致的效果
softmax函数:
对于一个数组V:
如果V中元素很多,则该计算非常耗时
反向传播时,所有权重一起更新非常耗时
代码示例:
⭐ gensim 词向量表示库
gensim
是一个强大的 Python 库,主要用于主题建模、文档相似性分析和词向量表示等自然语言处理任务,在处理大规模文本数据时表现出色。
① 词向量表示
Word2Vec:gensim 可以训练 word2vec 模型,将单词映射到低维向量空间。
Word2vec 有两种训练算法:
1.CBOW(Continuous Bag of Words):根据上下文预测当前词,使用周围词的词向量的平均值作为输0
2.Skip-gram:根据当前词预测上下文词,将当前词的词向量作为输入。
from gensim.models import Word2Vec
sentences = [["我", "爱", "自然", "语言", "处理"], ["这", "是", "一个", "好", "工具"]]
model = Word2Vec(sentences, min_count=1, vector_size=100, window=5, sg=1)
'''
sentences 是训练数据,每个元素是一个句子,以单词列表的形式表示。
min_count=1 表示只考虑出现至少一次的词。
vector_size=100 表示词向量的维度为 100。
window=5 表示窗口大小为 5,即考虑当前词前后 5 个词的上下文。
sg=1 表示使用 Skip-gram 算法,sg=0 表示使用 CBOW 算法。
'''
② FastText:
word2vec的扩展,能够处理未登录词(OOV),它将单词拆分为字符 n-grams 来训练词向量。
from gensim.models import FastText
model = FastText(sentences, min_count=1, vector_size=100, window=5)
③ 文档相似性:
TF-IDF:计算词的 TF-IDF 权重
from gensim import corpora, models
dictionary = corpora.Dictionary(sentences)
corpus = [dictionary.doc2bow(text) for text in sentences]
tfidf = models.TfidfModel(corpus)
'''
dictionary = corpora.Dictionary(sentences):创建一个词典,将每个词映射到一个唯一的 id。
corpus = [dictionary.doc2bow(text) for text in sentences]:将每个句子转换为词袋表示,doc2bow 方法将句子转换为 (词 id, 词频) 的元组列表。
tfidf = models.TfidfModel(corpus):创建一个 TF-IDF 模型并对词袋进行转换。
'''
④ 主题建模:
LDA (Latent Dirichlet Allocation):进行主题建模,将文档表示为主题的分布
lda_model = models.LdaModel(corpus=corpus, id2word=dictionary, num_topics=5)
# 使用 corpus 和 dictionary 训练一个 LDA 模型,num_topics=5 表示设置 5 个主题。
⑤ 词向量的使用:
查找相似词:可以使用 most_similar
方法查找相似词
similar_words = model.wv.most_similar("语言", topn=5) # 查找与 “语言” 最相似的 5 个词
词向量运算:可以对词向量进行加、减运算
result = model.wv.most_similar(positive=["语言", "处理"], negative=["工具"]) # 查找与 v(语言) + v(处理) - v(工具) 最相似的词
存储和加载词向量:可以将词向量保存到文件并加载
model.save("word2vec.model") # 保存
loaded_model = Word2Vec.load("word2vec.model") # 加载
Ⅰ 模型定义
① 初始化 Word2Vec 模型: 使用给定的语料库和词向量维度初始化 Word2Vec 模型,并设置为 Skip-gram 模式
参数:corpus:语料、corpus_file:语料文件路径、vector_size:训练的词向量维度、window:窗口大小(当前词与预测词之间的最大距离)、min_count:词频最小阈值、workers:线程数、sg:训练算法,默认为0,sg为0:cbow方式,sg为1:skipgram方式、negative:负采样,默认为5,大于0则使用负采样、hs:哈夫曼树处理方式,hs=1:打开、hs=0:关闭
② 设置参数: 设置模型的参数,如词向量维度和训练算法。
③ 训练模型: 使用语料库训练模型。
④ 保存模型到文件: 将训练好的模型保存到指定文件中。
⑤ 返回模型: 返回训练好的模型对象。
注:Word2Vec模型要求输入的语料就是直接切分好的
def train_word2vec_model(corpus, dim):
# corpus:语料 corpus_file:语料文件路径, vector_size:训练的词向量维度,window:窗口大小(当前词与预测词之间的最大距离),min_count:词频最小阈值,workers:线程数,sg:训练算法,默认为0:cbow方式,sg为1:skipgram方式,negative:负采样,默认为5,大于0则使用负采样
model = Word2Vec(corpus, vector_size=dim, sg=1)
model.save("model.w2v")
return model
Ⅱ 加载模型
① Word2Vec.load(path):接受一个路径参数 path,使用 Word2Vec.load(path) 加载指定路径的 Word2Vec 模型
② return model:返回加载的模型
#输入模型文件路径
#加载训练好的模型
def load_word2vec_model(path):
model = Word2Vec.load(path)
return model
Ⅲ 模型训练
① 初始化句子列表:创建一个空列表 sentences
② 读取语料文件:打开并逐行读取 corpus.txt 文件,每行使用 jieba.lcut 进行分词,将分词结果添加到 sentences 列表中
③ 训练Word2Vec模型:调用 train_word2vec_model 函数,传入分词后的句子列表和词向量维度(512),训练并保存模型
④ 返回模型:返回训练好的Word2Vec模型
def main():
sentences = []
with open("corpus.txt", encoding="utf8") as f:
for line in f:
sentences.append(jieba.lcut(line))
model = train_word2vec_model(sentences, 512)
return model
Ⅳ 模型预测
① 训练模型:调用 main() 函数进行模型训练
② 加载模型:加载预训练的 Word2Vec 模型
③ 类比计算:计算“男人”和“母亲”的相似词,并减去“女人”的影响,输出最相似的词语,通过余弦相似度比较两词语向量的相似性
model.wv
表示词向量模型的KeyedVectors
部分,其中存储了词向量的信息。most_similar
是KeyedVectors
类中的一个方法,用于寻找与给定词向量最相似的词。positive=["男人", "母亲"]
:表示正相关列表。most_similar
方法会将这些词的词向量相加。negative=["女人"]
:表示负相关列表。most_similar
方法会将这些词的词向量相
④ 交互查询:进入一个无限循环,用户可以输入词语,程序会输出与该词语最相似的词语。如果输入的词语不在模型中,则提示“输入词不存在”。
if __name__ == "__main__":
model = main() #训练
model = load_word2vec_model("model.w2v") #加载
# 计算“男人”和“母亲”的相似词,并减去“女人”的影响
print(model.wv.most_similar(positive=["男人", "母亲"], negative=["女人"])) #类比
while True: #找输入词语相似词语
string = input("input:")
try:
print(model.wv.most_similar(string))
except KeyError:
print("输入词不存在")
词向量尽可能倾向正确结果即可
Ⅴ 词向量模型的简单实现
import json
import jieba
import numpy as np
import gensim
from gensim.models import Word2Vec
from collections import defaultdict
'''
词向量模型的简单实现
'''
#训练模型
#corpus: [["cat", "say", "meow"], ["dog", "say", "woof"]]
#corpus: [["今天", "天气", "不错"], ["你", "好", "吗"]]
#dim指定词向量的维度,如100
def train_word2vec_model(corpus, dim):
# corpus:语料 corpus_file:语料文件路径, vector_size:训练的词向量维度,window:窗口大小(当前词与预测词之间的最大距离),min_count:词频最小阈值,workers:线程数,sg:训练算法,默认为0:cbow方式,sg为1:skipgram方式,negative:负采样,默认为5,大于0则使用负采样
model = Word2Vec(corpus, vector_size=dim, sg=1)
model.save("model.w2v")
return model
#输入模型文件路径
#加载训练好的模型
def load_word2vec_model(path):
model = Word2Vec.load(path)
return model
def main():
sentences = []
with open("corpus.txt", encoding="utf8") as f:
for line in f:
sentences.append(jieba.lcut(line))
model = train_word2vec_model(sentences, 512)
return model
if __name__ == "__main__":
model = main() #训练
model = load_word2vec_model("model.w2v") #加载
# 计算“男人”和“母亲”的相似词,并减去“女人”的影响
print(model.wv.most_similar(positive=["男人", "母亲"], negative=["女人"])) #类比
while True: #找输入词语相似词语
string = input("input:")
try:
print(model.wv.most_similar(string))
except KeyError:
print("输入词不存在")
神经网络最重要的就是泛化性
十一、词向量的应用
1.寻找近义词
注意:
① 依赖分词正确
② 没有对称性:与A最接近的词是B,不代表与B最接近的词是A
③ 有时也会有反义词因为词性相同训练出的结果很相似
④ 总会有很多badcase
2.句向量或文本向量
步骤:
① 将一句话或一段文本分成若干个词
② 找到每个词对应的词向量
③ 所有词向量加和求平均或通过各种网络模型(rnn、pooling),得到文本向量
④ 使用文本向量计算相似度或进行聚类(K-Means)等无监督学习工作
十二、聚类算法 —— KMeans
1.应用场景
Ⅰ 客户细分
企业常常拥有大量客户的数据,例如客户的年龄、性别、消费金额、购买频率、购买产品类型等信息。通过应用 K-Means 算法,可以依据这些特征将客户划分成不同的群体(聚类)
比如,将客户聚类成高消费频繁购买群体、中等消费偶尔购买群体、低消费低频购买群体等。这样,企业就能针对不同的客户群体制定个性化的营销策略,对高价值客户群体提供更优质的专属服务、推送高端产品;对价格敏感型的低消费群体,侧重于推送优惠活动、性价比高的产品等,从而提高营销的精准性和效果,提升客户满意度与企业的销售额。
Ⅱ 推荐系统中的初始聚类
虽然现在的推荐系统大多基于协同过滤、深度学习等复杂技术,但在初始阶段,K-Means 算法可以发挥定作用。例如,将用户基于历史行为数据(如购买的商品、观看的视频、收藏的音乐等)进行聚类,把具有相似行为模式的用户聚到一起。
后续再结合其他算法,针对每个聚类内的用户特点推荐他们可能感兴趣的商品、视频、音乐等,有助于提高推荐的效率和精准度,尤其在冷启动阶段(新用户或新物品没有足够交互数据时),这种基于聚类的粗粒度推荐有一定的实用价值。
Ⅲ 文档聚类
对于大量的文档(如新闻文章、学术论文、博客等),可以提取文档的特征,像词频、TF-IDF 值等,将每篇文档看作是高维空间中的一个数据点(特征向量代表文档)。
运用 K-Means 算法,根据文档特征的相似性,把文档聚类成不同的主题类别。例如,将众多新闻报道聚类成体育类、财经类、娱乐类、科技类等不同的类别,方便用户快速浏览和査找感兴趣的内容,也有助于后续进一步分析各主题类别文档的特点、趋势等。
Ⅳ 图像压缩
一幅图像可以看作是由众多像素点组成,每个像素点有其对应的颜色值(比如在 RGB 色彩模式下,有红、绿、蓝三个通道的数值)。利用 K-Means 算法,可将图像中的像素点基于颜色特征进行聚类。
例如,设定聚类的类别数量K为一个相对较小的值(如 16、32 等),把众多不同颜色的像素点聚合成K个类别,然后用每个类别对应的中心点颜色值来代表该类别内所有像素点的颜色,以此实现图像颜色的简化,达到图像压缩的目的。
这种方式适用对图像质量要求不是极高的场景下(如网页缩略图展示等),既能有效减少图像数据量,又能保留图像的大致视觉特征。
⭐2.算法流程
Ⅰ 初始化
随机选择k(类别数)个点作为初始质心
Ⅱ repeat
将每个点指派到最近的质心,形成k个簇
重新计算每个簇的质心
Ⅲ until
质心不发生变化
3.KMeans的优缺点
Ⅰ 优点:
速度很快,可以支持很大量的数据
样本均匀特征明显的情况下,效果不错
Ⅱ 缺点:
人为设定聚类数量
初始化中心影响效果,导致结果不稳定
对于个别特殊样本敏感,会大幅影响聚类中心位置
不适合多分类或样本较为离散的数据
4.K-Means的使用技巧
先设定较多的聚类类别(以样本数的开方作为一个基准设置类别簇数)
聚类结束后计算类内平均距离
排序后,舍弃类内平均距离较长的类别
计算距离时可以尝试欧式距离、余弦距离或其他距离
短文本的聚类记得先去重,以及其他预处理
5.K-Means代码实现
import numpy as np
import random
import sys
'''
Kmeans算法实现
原文链接:https://blog.csdn.net/qingchedeyongqi/article/details/116806277
'''
class KMeansClusterer: # k均值聚类
def __init__(self, ndarray, cluster_num):
self.ndarray = ndarray
self.cluster_num = cluster_num
self.points = self.__pick_start_point(ndarray, cluster_num)
def cluster(self):
result = []
for i in range(self.cluster_num):
result.append([])
for item in self.ndarray:
distance_min = sys.maxsize
index = -1
for i in range(len(self.points)):
distance = self.__distance(item, self.points[i])
if distance < distance_min:
distance_min = distance
index = i
result[index] = result[index] + [item.tolist()]
new_center = []
for item in result:
new_center.append(self.__center(item).tolist())
# 中心点未改变,说明达到稳态,结束递归
if (self.points == new_center).all():
sum = self.__sumdis(result)
return result, self.points, sum
self.points = np.array(new_center)
return self.cluster()
def __sumdis(self,result):
#计算总距离和
sum=0
for i in range(len(self.points)):
for j in range(len(result[i])):
sum+=self.__distance(result[i][j],self.points[i])
return sum
def __center(self, list):
# 计算每一列的平均值
return np.array(list).mean(axis=0)
def __distance(self, p1, p2):
#计算两点间距
tmp = 0
for i in range(len(p1)):
tmp += pow(p1[i] - p2[i], 2)
return pow(tmp, 0.5)
def __pick_start_point(self, ndarray, cluster_num):
if cluster_num < 0 or cluster_num > ndarray.shape[0]:
raise Exception("簇数设置有误")
# 取点的下标
indexes = random.sample(np.arange(0, ndarray.shape[0], step=1).tolist(), cluster_num)
points = []
for index in indexes:
points.append(ndarray[index].tolist())
return np.array(points)
x = np.random.rand(100, 8)
kmeans = KMeansClusterer(x, 10)
result, centers, distances = kmeans.cluster()
print(result)
print(centers)
print(distances)
6.K-Means应用示例
将新闻根据标题相似的放在一起,采用K-Means算法,并根据类内距离排序
Ⅰ 加载训练模型
① 接受一个参数 path,表示模型文件的路径
② 使用 Word2Vec.load(path) 方法加载模型
③ 返回加载后的模型对象
# 输入模型文件路径
# 加载训练好的模型
def load_word2vec_model(path):
model = Word2Vec.load(path)
return model
Ⅱ 加载所有句子
① 初始化一个空集合 sentences
② 打开指定路径的文件,按行读取内容
③ 对每一行去除首尾空白字符后进行分词,并将分词结果以空格连接成字符串加入集合
④ 打印获取到的不同句子的数量
⑤ 返回包含所有不同句子的集合
# 加载所有句子
def load_sentence(path):
sentences = set()
with open(path, encoding="utf8") as f:
for line in f:
sentence = line.strip()
sentences.add(" ".join(jieba.cut(sentence)))
print("获取句子数量:", len(sentences))
return sentences
Ⅲ 文本向量化
① 初始化一个空列表 vectors,用于存储每个句子的向量。
② 遍历输入的 sentences 列表,对每个句子进行处理:
1.将句子按空格分割成单词列表 words
2.初始化一个全零向量 vector,长度为模型的向量维度
3.对每个单词,尝试从模型中获取其词向量并累加到 vector 中;如果单词不在模型中,则用全零向量代替
4.将累加后的向量除以单词数量,得到句子的平均向量,并添加到 vectors 列表中
③ 返回包含所有句子向量的 numpy 数组
# 将文本向量化
def sentences_to_vectors(sentences, model):
vectors = []
for sentence in sentences:
words = sentence.split() #sentence是分好词的,空格分开
vector = np.zeros(model.vector_size)
#所有词的向量相加求平均,作为句子向量
for word in words:
try:
vector += model.wv[word]
except KeyError:
#部分词在训练中未出现,用全0向量代替
vector += np.zeros(model.vector_size)
vectors.append(vector / len(words))
return np.array(vectors)
Ⅳ 计算向量余弦距离
① 归一化向量:将输入的两个向量 vec1 和 vec2 分别除以它们各自的模长(即向量的欧几里得范数),得到单位向量。
② 计算点积:返回归一化后的两个向量的点积,作为余弦相似度的结果。
注:由于余弦相似度的取值范围在 -1 到 1 之间,该函数实际上返回的是余弦相似度的正值部分。
# 向量余弦距离
def cosine_distance(vec1, vec2):
vec1 = vec1 / np.sqrt(np.sum(np.square(vec1))) #A/|A|
vec2 = vec2 / np.sqrt(np.sum(np.square(vec2))) #B/|B|
return np.sum(vec1 * vec2)
Ⅴ 计算向量欧氏距离
① 计算两个向量的差值
② 对差值进行平方操作
③ 对平方后的结果求和
④ 对求和结果开平方根
# 计算向量欧式距离
def eculid_distance(vec1, vec2):
return np.sqrt((np.sum(np.square(vec1 - vec2))))
Ⅵ 文本聚类
① 加载模型和数据:
加载预训练的词向量模型
加载并处理文本标题,使用结巴分词将每个标题分词
② 向量化:
将所有标题转换为词向量表示,每个标题的向量是其包含的所有词向量的平均值
③ 聚类:
根据标题数量确定聚类数量(平方根)
使用KMeans算法对标题向量进行聚类
④ 计算类内距离:
计算每个标题向量与其所属聚类中心的距离。
对每个聚类内的所有距离取平均值,得到该聚类的密度。
⑤ 输出结果:
按照聚类密度从大到小排序,输出每个聚类的前10个标题。
def main():
model = load_word2vec_model("model.w2v") #加载词向量模型
sentences = load_sentence("titles.txt") #加载所有标题
vectors = sentences_to_vectors(sentences, model) #将所有标题向量化
n_clusters = int(math.sqrt(len(sentences))) #指定聚类数量
print("指定聚类数量:", n_clusters)
kmeans = KMeans(n_clusters) #定义一个kmeans计算类
kmeans.fit(vectors) #进行聚类计算
sentence_label_dict = defaultdict(list)
for sentence, label in zip(sentences, kmeans.labels_): #取出句子和标签
sentence_label_dict[label].append(sentence) #同标签的放到一起
# 计算类内距离 使用向量余弦距离
density_dict = defaultdict(list)
for vector_index, label in enumerate(kmeans.labels_):
vector = vectors[vector_index] #某句话的向量
center = kmeans.cluster_centers_[label] #对应的类别中心向量
distance = cosine_distance(vector, center) #计算距离
density_dict[label].append(distance) #保存下来
for label, distance_list in density_dict.items():
density_dict[label] = np.mean(distance_list) #对于每一类,将类内所有文本到中心的向量余弦值取平均
density_order = sorted(density_dict.items(), key=lambda x:x[1], reverse=True) #按照平均距离排序,向量夹角余弦值越大,越接近1,距离越小
'''
# 计算类内距离 使用欧式距离
density_dict = defaultdict(list)
for vector_index, label in enumerate(kmeans.labels_):
vector = vectors[vector_index] #某句话的向量
center = kmeans.cluster_centers_[label] #对应的类别中心向量
distance = eculid_distance(vector, center) #计算距离
density_dict[label].append(distance) #保存下来
for label, distance_list in density_dict.items():
density_dict[label] = np.mean(distance_list) #对于每一类,将类内所有文本到中心的欧氏距离取平均
density_order = sorted(density_dict.items(), key=lambda x:x[1], reverse=False) #按照平均距离逆序排序,欧式距离值越小,距离越小
'''
#按照类内距离顺序输出
for label, distance_avg in density_order:
print("cluster %s , cluster density %f: " % (label, distance_avg))
sentences = sentence_label_dict[label]
for i in range(min(10, len(sentences))): #随便打印几个,太多了看不过来
print(sentences[i].replace(" ", ""))
print("---------")
Ⅶ 按照向量距离进行K-Means聚类进行分类
#coding: utf-8
#基于训练好的词向量模型进行聚类
#聚类采用Kmeans算法
import math
import re
import json
import jieba
import numpy as np
from gensim.models import Word2Vec
from sklearn.cluster import KMeans
from collections import defaultdict
#输入模型文件路径
#加载训练好的模型
def load_word2vec_model(path):
model = Word2Vec.load(path)
return model
# 加载所有句子
def load_sentence(path):
sentences = set()
with open(path, encoding="utf8") as f:
for line in f:
sentence = line.strip()
sentences.add(" ".join(jieba.cut(sentence)))
print("获取句子数量:", len(sentences))
return sentences
# 将文本向量化
def sentences_to_vectors(sentences, model):
vectors = []
for sentence in sentences:
words = sentence.split() #sentence是分好词的,空格分开
vector = np.zeros(model.vector_size)
#所有词的向量相加求平均,作为句子向量
for word in words:
try:
vector += model.wv[word]
except KeyError:
#部分词在训练中未出现,用全0向量代替
vector += np.zeros(model.vector_size)
vectors.append(vector / len(words))
return np.array(vectors)
# 向量余弦距离
def cosine_distance(vec1, vec2):
vec1 = vec1 / np.sqrt(np.sum(np.square(vec1))) #A/|A|
vec2 = vec2 / np.sqrt(np.sum(np.square(vec2))) #B/|B|
return np.sum(vec1 * vec2)
# 计算向量欧式距离
def eculid_distance(vec1, vec2):
return np.sqrt((np.sum(np.square(vec1 - vec2))))
def main():
model = load_word2vec_model("model.w2v") #加载词向量模型
sentences = load_sentence("titles.txt") #加载所有标题
vectors = sentences_to_vectors(sentences, model) #将所有标题向量化
n_clusters = int(math.sqrt(len(sentences))) #指定聚类数量
print("指定聚类数量:", n_clusters)
kmeans = KMeans(n_clusters) #定义一个kmeans计算类
kmeans.fit(vectors) #进行聚类计算
sentence_label_dict = defaultdict(list)
for sentence, label in zip(sentences, kmeans.labels_): #取出句子和标签
sentence_label_dict[label].append(sentence) #同标签的放到一起
# 计算类内距离 使用向量余弦距离
density_dict = defaultdict(list)
for vector_index, label in enumerate(kmeans.labels_):
vector = vectors[vector_index] #某句话的向量
center = kmeans.cluster_centers_[label] #对应的类别中心向量
distance = cosine_distance(vector, center) #计算距离
density_dict[label].append(distance) #保存下来
for label, distance_list in density_dict.items():
density_dict[label] = np.mean(distance_list) #对于每一类,将类内所有文本到中心的向量余弦值取平均
density_order = sorted(density_dict.items(), key=lambda x:x[1], reverse=True) #按照平均距离排序,向量夹角余弦值越大,越接近1,距离越小
'''
# 计算类内距离 使用欧式距离
density_dict = defaultdict(list)
for vector_index, label in enumerate(kmeans.labels_):
vector = vectors[vector_index] #某句话的向量
center = kmeans.cluster_centers_[label] #对应的类别中心向量
distance = eculid_distance(vector, center) #计算距离
density_dict[label].append(distance) #保存下来
for label, distance_list in density_dict.items():
density_dict[label] = np.mean(distance_list) #对于每一类,将类内所有文本到中心的欧氏距离取平均
density_order = sorted(density_dict.items(), key=lambda x:x[1], reverse=False) #按照平均距离逆序排序,欧式距离值越小,距离越小
'''
#按照类内距离顺序输出
for label, distance_avg in density_order:
print("cluster %s , cluster density %f: " % (label, distance_avg))
sentences = sentence_label_dict[label]
for i in range(min(10, len(sentences))): #随便打印几个,太多了看不过来
print(sentences[i].replace(" ", ""))
print("---------")
if __name__ == "__main__":
# Cluster density:按照平均距离排序,向量夹角余弦值越接近1,距离越小
# Cluster:指各个类的标号
main()
十三、词向量总结
1.词向量的作用
1.质变:将离散的字符转化为连续的数值
2.通过向量的相似度代表语义的相似度
3.虽然词向量的训练基于很多不完全正确的假设,但是据此训练的词向量是有意义的
4.使用无标注的文本的一种好方法
5.特定词向量适合在特定领域使用
2.词向量存在的问题
Ⅰ 词向量是“静态”的。每个词使用固定向量,没有考虑前后文
Ⅱ 一词多义的情况。例:西瓜 - 苹果 - 华为
Ⅲ 影响效果的因素非常多
如:
① 维度选择
② 随机初始化
③ 训练方式:skip-gram/ cbow/ Glove
④ 分词质量
⑤ 词频截断
⑥ 未登录词
⑦ 窗口大小
⑧ 迭代轮数
⑨ 停止条件
⑩ 语料质量等
Ⅳ 没有好的直接评价指标。常需要用下游任务来评价
3.词向量与TF·IDF的关系:
① 概念
词向量(Word Embedding):是一种将单词映射到低维实数向量空间的技术。例如,通过 Word2Vec、GloVe 等模型,词向量能够把每个单词表示为一个固定长度的向量,向量中的数值代表了单词在语义空间中的某种特征。如 “国王” 和 “王后” 的词向量在语义空间中距离较近,因为它们语义相关。
TF - IDF(词频 - 逆文档频率):它是一种用于信息检索和文本挖掘的统计方法。TF(词频)表示一个词在文档中出现的频率,IDF(逆文档频率)用于衡量一个词的普遍重要性。TF - IDF 的值越高,说明这个词在当前文档中比较重要,同时在整个文档集合中相对比较独特。
② 联系
特征表示层面:两者都是用于文本的特征表示。TF - IDF 可以将文本表示为一个向量,向量的每个维度代表一个单词的 TF - IDF 值,用于衡量单词对文本的重要性。词向量则是从语义角度将单词表示为向量,这些向量也可以用于构建文本的向量表示。例如,对于一个文档,可以将文档中的单词词向量进行某种组合(如求和、求平均等)来得到文档的向量表示,就像通过 TF - IDF 值构建文档向量一样。
在一些应用场景中,比如文本分类或聚类,它们都可以作为输入特征。例如,在文本分类任务中,可以将 TF - IDF 向量或者文本的词向量表示输入到分类模型(如支持向量机、神经网络等)中,帮助模型区分不同类别的文本。
语义信息补充:TF - IDF 主要关注单词在文档中的出现频率和文档间的分布情况,相对缺乏语义信息。而词向量蕴含了丰富的语义信息,如语义相似性、词的类比关系等。在实际应用中,可以将词向量的语义信息与 TF - IDF 的统计信息相结合。例如,在构建文本相似度计算模型时,同时考虑词向量的语义相似度和 TF - IDF 权重来更全面地衡量文本之间的相似性。
③ 区别
语义表达深度:词向量能够捕捉到词汇之间深层次的语义关系,如 “苹果” 和 “香蕉” 在词向量空间中的位置关系能够体现它们都是水果这一语义关联。而 TF - IDF 主要基于单词的出现频率,很难直接体现这种语义关联,它更侧重于单词对特定文档的重要性区分。
向量性质和计算方式:TF - IDF 向量的每个维度是根据单词在文档中的出现频率和整个文档集合计算得到的权重值,其计算依赖于文档集合和具体的文档。词向量是通过大规模的语料训练得到的,其向量维度是通过模型训练确定的固定值,如 Word2Vec 模型训练出的词向量维度通常是固定的(如 100 维、300 维等),并且词向量的计算基于神经网络模型对语料中单词的上下文关系的学习。
应用场景侧重:TF - IDF 在信息检索任务中应用广泛,如搜索引擎中对文档与查询词的相关性排名。词向量在自然语言处理的更多语义相关任务中表现出色,如机器翻译、问答系统等,用于理解词汇之间的语义关系以生成更合理的文本输出。