连续词袋模型(CBOW)计算句子相似度(余弦相似度和欧氏距离)

相关了解可以参考下面的博客:

https://blog.csdn.net/weixin_40771521/article/details/103893982

提出问题:如何计算中文句子的相似度

本文使用的是CBOW模型,通过负采样减少计算量

1.先给出框架

在这里插入图片描述

2.对数据做预处理(数据末尾有链接data)

运行pre_process.py文件

##pre_process.py##
#1.生成样本数据:每一句有效词w2v_words.pkl    2.词表(词:序号)w2v_vocab.pkl

import jieba
import pickle as pkl

def pre_process():
    # 加载数据
    with open('../data/w2v_msr_training.utf8', 'r', encoding='utf-8') as f:
        #8864行含有空格 引号 回车的句子
        tmp = f.readlines()
    text_pure = []
    for i in tmp:
        #处理前:'接连  亮相  的  全  都是  票友  ,  生  旦  净  丑  ,  行当  俱全  ,  流派  纷呈  。'
        #去掉引号,回车,空格
        t1 = i.replace('“  ', '')
        t2 = t1.replace('\n', '')
        t3 = t2.replace('  ', '')
        #处理后:'接连亮相的全都是票友,生旦净丑,行当俱全,流派纷呈。'
        text_pure.append(t3)
        #text_pure有8844行纯文本的句子
    with open('../data/w2v_stopwords.txt', 'r', encoding='utf-8') as f:
        stopwords = [line.strip() for line in f.readlines()]
    #stopwords有1893个停用词

    # 去除停用词 构词表
    words_ = []
    vocab = set()
    for sen in text_pure:#'接连亮相的全都是票友,生旦净丑,行当俱全,流派纷呈。'
        words = jieba.lcut(sen) #['接连', '亮相', '的', '全都', '是', '票友', ',', '生旦净', '丑', ',', '行当', '俱全', ',', '流派', '纷呈', '。']
        temp = []
        for wd in words:
            if wd not in stopwords:
                temp.append(wd)
                vocab.add(wd)
                #只要不是停用词,就加入词的集合中,{'流派', '丑', '票友', '生旦净', '俱全', '接连', '行当', '纷呈', '亮相'}
        words_.append(temp)
        #[['接连', '亮相', '票友', '生旦净', '丑', '行当', '俱全', '流派', '纷呈'],
        # ['没想到', '车', '碰上', '一位', '作家', '耽误', '几分钟', '进场', '一看', '座无虚席', '二楼', '东侧', '好不容易', '找到', '角度', '最次', '空座', '时', '主持人', '李世英', '报', '第三个', '登台演唱', '名字']]
        #words_有8844行,每一行为每一句的有效词的列表
        #vocab有28938个有效词,集合具有无序性

    vocab_dist = {}
    for i, wd in enumerate(list(vocab)):
        vocab_dist[wd] = i
    #生成词和序号的键值对{'菲利普': 0, '第十五届': 1, '元': 2, '但求无过': 3, '献上': 4, '坐': 5, '今春': 6, '咖啡馆': 7, '带出': 8,
    # '宗介华': 9, '辞去': 10, '腰椎': 11, '责令': 12, '支部': 13, '分类': 14, '泊车': 15, '动物': 16, '沈阳铁路局': 17,共28938对
    return words_, vocab_dist

if __name__ == '__main__':
    words_, vocab_ = pre_process()
    #将words_(有8844行,每一行为每一句的有效词的列表)写入/data/w2v_words.pkl文件中
    with open('../data/w2v_words.pkl', 'wb') as f:
        pkl.dump(words_, f)
    #将vocab_dist(由28938个有效词及其序号构成的键值对)写入/data/w2v_words.pkl文件中
    with open('../data/w2v_vocab.pkl', 'wb') as f:
        pkl.dump(vocab_, f)

得到data文件夹变化如下:
在这里插入图片描述

3.CBOW模型实现

# coding=utf8
#model of generating CBOW

import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np

class CBOW(nn.Module):
    def __init__(self, vocab_size, embedding_dim, hidden_dim):
        super(CBOW, self).__init__()
        #将vocab_size个字,每一个字都表示为embedding_dim维张量
        self.embedding = nn.Embedding(vocab_size, embedding_dim)
        self.vocab_size = vocab_size
        #通过第一个隐藏层将每个字仿射为hidden_dim维张量
        self.layer1 = nn.Linear(embedding_dim, hidden_dim)
        #通过输出层将hidden_dim维张量仿射为vocab_size维张量
        self.layer2 = nn.Linear(hidden_dim, vocab_size)

    def forward(self, inputs, target):
        #每一个字都是64维的表示,输入的是待预测值的前两个字和后两个字,一共就是4 * 64 ,sum(0)就是列向相加,得到1*64
        #再/4,即计算平均值
        embed = self.embedding(inputs).sum(0) / 4
        hidden_o = F.relu(self.layer1(embed))  # 非线性变换
        output_ = self.layer2(hidden_o)  # 仿射变换
        #包含标签的21个序号
        Nec_list = self.__random__(target, 20)
        #设置一个词表大小(28938)的全True张量
        temp = torch.ones_like(output_, dtype=torch.bool)
        for i in Nec_list:#将上面包含标签的21个序号设置为False
            temp[i] = False
        #将词表大小的输出张量output_里面下标对应temp中的序号为True的全设置为负无穷-inf
        #负采样操作,减少计算压力
        masked_out = output_.masked_fill(temp, -np.inf)

        out = F.softmax(masked_out, dim=0).view(1, -1)
        # out = F.softmax(output_, dim=0).view(1, -1)
        return out

    def __random__(self, target, num_random):
    #随机选出小于词表大小的20个序号,并且这20个序号不含目标序号
        rand_list = np.random.randint(self.vocab_size, size=num_random)
        if target in rand_list:#如果标签序号在其中,就再随机取一个,直到集齐20个非标签的序号
            while True:
                temp = np.random.randint(self.vocab_size, size=1)
                if temp != target:
                    rand_list = np.append(rand_list, temp)
                    break
        else:
            rand_list = np.append(rand_list, target)
        return rand_list

4.生成词袋模型 .bin文件,方便后面词嵌入时调用

##generate_cbow_bin.py##
#Through the training sample set, the continuous word bag model is generated
#The role of the word bag model is to predict the key words from the context

import torch
import torch.nn as nn
import torch.optim as opt
from tqdm import tqdm
from models.CBOW import CBOW
import pickle as pkl

def train(words, vocab):
    torch.manual_seed(1)
    vocab_size = len(vocab)
    train_cbow = []
    for line in words:#['接连', '亮相', '票友', '生旦净', '丑', '行当', '俱全', '流派', '纷呈']
        for i in range(2, len(line) - 2):
            #([['接连', '亮相'], ['生旦净', '丑']], '票友')-->([['亮相', '票友'], ['丑', '行当']], '生旦净')
            #先以下标为2的词为中心,窗口大小为5  中心右移直到倒数第三个词,每一个窗口都得到一个元组类型
            temp = ([[line[i - 2], line[i - 1]], [line[i + 1], line[i + 2]]], line[i])
            train_cbow.append(temp)
        #每一句都这样滑动,得到由79792个元组构成的的列表
    hidden_dim = 32
    embedding_dim = 64
    losses = []
    #初始化模型
    model = CBOW(vocab_size, embedding_dim, hidden_dim)
    #定义损失函数为交叉熵损失
    loss_fun = nn.CrossEntropyLoss()
    #使用梯度下降法更新参数
    optimizer = opt.SGD(model.parameters(), lr=3e-2)
    for i in tqdm(range(10)):
        total_loss = 0
        #目标是用上下文预测中心词
        for context, target in tqdm(train_cbow):
        #context——[['接连', '亮相'], ['生旦净', '丑']]  target——'票友'
            context_indexes = []
            for t in context:
                context_indexes.extend([vocab[t[0]], vocab[t[1]]])
            #将列表转变为张量
            context_indexes = torch.tensor(context_indexes, dtype=torch.long)
            ## 将模型的参数梯度初始化为0
            model.zero_grad()
            #prob--tensor([[0., 0., 0.,  ..., 0., 0., 0.]],out[0][8662] -- tensor(0.0386)
            prob = model(context_indexes, vocab[target])
            #true_label -- tensor([8662]) --> tensor([26834])
            true_label = torch.tensor([vocab[target]], dtype=torch.long)
            loss = loss_fun(prob, true_label)  # 预测值 真实值
            #反向传播计算梯度
            #tensor(10.2344, grad_fn=<NllLossBackward0>) --> tensor(10.2322, grad_fn=<NllLossBackward0>)
            loss.backward()
            optimizer.step()
            #每一轮的总损失,可以用来评价这一轮的效果
            total_loss += loss.item()
        #最后我们保存了运行了10轮的连续词袋模型,
        torch.save(model, '../data/w2v_CBOW.bin')

if __name__ == '__main__':
    with open('../data/w2v_words.pkl', 'rb') as f:
        _words = pkl.load(f)
    with open('../data/w2v_vocab.pkl', 'rb') as f:
        _vocab = pkl.load(f)
    train(_words, _vocab)

运行这个文件大概要2~3个小时:(这里给出.bin文件的链接)
在这里插入图片描述
运行完毕后,data文件夹变化如下:
在这里插入图片描述

5.计算句子的相似度

##CbowSenTest.py##
#Calculate the cosine similarity between sentences when the word bag model is used to represent sentences

import torch
import pickle as pkl
import jieba

#输入一组词的序号张量,使得句子中的每一个词得到embedding_dim维的张量表示
def get_word_embed(sentence):
    #先下载训练好的模型的参数
    model = torch.load('../data/w2v_CBOW.bin')
    embed = model.embedding(sentence)
    wd_tensors = model.layer1(embed)
    #输出32维的张量
    return wd_tensors

def test():
    sentences1 = '海啸发生在当地时间17日晚8时许。'
    sentences2 = '这次海啸是由在该国北海岸发生的一次里氏7级海底地震引发的。'
    sentences3 = '韩日元贬值还使东南亚国家的出口严重受挫。'
    sens = [sentences1, sentences2, sentences3]
    word1 = '贬值'
    word2 = '总裁'
    word3 = '总统'
    words_sim = [word1, word2, word3]
    with open('../data/w2v_stopwords.txt', 'r', encoding='utf-8') as f:
        stopwords = [line.strip() for line in f.readlines()]
    with open('../data/w2v_vocab.pkl', 'rb') as f:
        #词表大小为28938
        _vocab = pkl.load(f)
    words_ = []
    for sen in sens:
        words = jieba.lcut(sen)
        temp = []
        for wd in words:
            if wd not in stopwords:
                temp.append(wd)
        words_.append(temp)
        #words_ = [ ['海啸', '发生', '时间', '日晚', '时许'],
        # ['海啸', '该国', '北海岸', '发生', '里氏', '级', '海底', '地震', '引发'],
        # ['韩', '日元', '贬值', '东南亚', '国家', '出口', '受挫'] ]
    sens_id = []
    for sen in words_:
        sen_id = []
        for wd in sen:
            sen_id.append(_vocab[wd])
        #sens_id记录了sens中的有效词的序号
        sens_id.append(sen_id)
    words_tensor = torch.tensor([_vocab[wd] for wd in words_sim], dtype=torch.long)
    sens_tensor = [torch.tensor(idx, dtype=torch.long) for idx in sens_id]
    #得到每一个词的张量
    word_embed = get_word_embed(words_tensor)
    sen_embed = []
    for s in sens_tensor:
        len_s = len(s)
        #一个句子有len_s个词,即有len_s*32的张量
        s_1 = get_word_embed(s)
        #将句子变为1*32维的张量,每一列取平均值,得到一个句子的32维张量表示
        s_2 = s_1.sum(0)/len_s
        sen_embed.append(s_2)

    sim_sen1 = torch.cosine_similarity(sen_embed[0].view(1, -1), sen_embed[1].view(1, -1))
    sim_sen2 = torch.cosine_similarity(sen_embed[1].view(1, -1), sen_embed[2].view(1, -1))
    sim_wd1 = torch.cosine_similarity(word_embed[0].view(1, -1), word_embed[2].view(1, -1))
    sim_wd2 = torch.cosine_similarity(word_embed[1].view(1, -1), word_embed[2].view(1, -1))
    print('句子1和句子2的相似度是:{}'.format(sim_sen1.item()))
    print('句子2和句子3的相似度是:{}'.format(sim_sen2.item()))
    print('词语1和词语2的相似度是:{}'.format(sim_wd1.item()))
    print('词语2和词语3的相似度是:{}'.format(sim_wd2.item()))

    odis = torch.nn.PairwiseDistance(p=2)
    odis_sen1 = odis(sen_embed[0].view(1, -1), sen_embed[1].view(1, -1))
    odis_sen2 = odis(sen_embed[0].view(1, -1), sen_embed[2].view(1, -1))
    odis_wd1 = odis(word_embed[0].view(1, -1), word_embed[2].view(1, -1))
    odis_wd2 = odis(word_embed[1].view(1, -1), word_embed[2].view(1, -1))
    print('句子1和句子2的欧式距离是:{}'.format(odis_sen1.item()))
    print('句子2和句子3的欧氏距离是:{}'.format(odis_sen2.item()))
    print('词语1和词语2的欧氏距离是:{}'.format(odis_wd1.item()))
    print('词语2和词语3的欧氏距离是:{}'.format(odis_wd2.item()))

if __name__ == '__main__':
    test()

输出结果如下:
在这里插入图片描述

资源

w2v_CBOW.bin:

链接:https://pan.baidu.com/s/12hhablJ2q-34ZySjrYiPTQ
提取码:2933

data:

链接:https://pan.baidu.com/s/1i0H8ULcqDH4c8OyTmbJgzQ
提取码:2933

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

榆钱不知秋

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值