Word2Vec的pytorch实现(Skip-gram)

本文档详细介绍了如何使用PyTorch实现Word2Vec的Skip-gram模型,包括数据预处理、构建DataSet、Loader、训练模型以及词向量的测试。通过负采样方法提升训练效率,最后展示了如何查找相似词。
摘要由CSDN通过智能技术生成

写在前面:

本篇文章是我个人的学习记录,仅包含代码实现和一些个人理解,参考的一些文章我会给出链接。
深度学习word2vec笔记之算法篇.
Word2Vec
PyTorch 实现 Word2Vec

Word2Vec的数学原理详解:链接:https://pan.baidu.com/s/1_xlOvItHKYh4ndu4UaMF8Q
提取码:gt2f

相关代码:https://github.com/deborausujono/word2vecpy
如果文章出现错误或者有可优化的部分,都欢迎大家指出,一起学习进步~

正文

需要用的数据,文件中的内容是英文文本,去除了标点符号,每个单词之间用空格隔开
语料库下载地址:https://pan.baidu.com/s/10Bd3JxCCFTjBPNt0YROvZA
提取码:81fo
语料预览

使用Skip-gram模型和负采样训练法。

代码部分

读取数据

首先定义一个类,读取原始语料,创建单词和id的映射关系,并计算词频。
这里词频使用了3/4 率.为 word2vec 论文里面推荐这么做


class Process_source_data():
    # 该类的作用是读取原始数据,构建词典映射关系和单词词频
    def __init__(self, data_path, vocab_size=10000):
        with open(data_path, 'r', encoding='utf8') as f:
            text = f.read()
        # 将单词切分
        self.text = text.lower().split()
        
        # 选出最多的vocab_size个词
        # 得到单词字典表,key是单词,value是次数
        vocob_dict = dict(Counter(self.text).most_common(vocab_size-1))
        # 把不常用的单词都编码为"<UNK>"
        vocob_dict['<UNK>'] = len(text) - np.sum(list(vocob_dict.values()))       
        
        #构建映射关系
        self.word2id = {word:i for i, word in enumerate(vocob_dict)}
        self.id2word = {i:word for i, word in enumerate(vocob_dict)}     
        
        # 根据3/4率调整词频
        word_counts = np.asarray(list(vocob_dict.values()))
        self.word_freq = (word_counts / np.sum(word_counts))** (3./4.)

构建DataSet

这里按照Skip-gram的方式构建训练数据,同时构建负采样样本。
继承的父类是torch.utils.data.Dateset,因此需要重新两个函数:def _len_ 和 def _getitem_

class Skip_gram_Dataset(Data.Dataset):
    # 该类的作用是将文字转换为对应ID, 并返回给定idx时对应的训练数据
    def __init__(self, text, word2id, word_freq, C=3, K=15, batch_size=32):
        super().__init__()
        self.C = C     # 上下文窗口
        self.K = K     # 负采样比例
        
        self.text_encoded = [word2id.get(word, word2id['<UNK>']) for word in text]
        self.text_encoded = torch.tensor(self.text_encoded, dtype=torch.long)
        
        self.word_freq = torch.tensor(word_freq)
        
    def __len__(self):
        return len(self.text_encoded)
    
    def __getitem__(self, idx):
        '''
        对于给定的idx,返回对应的训练数据
        - 中心词
        - 这个单词附近的positive word
        - 随机采样的K个单词作为negative word
        '''
        center_word = self.text_encoded[idx]
        pos_idx = list(range(idx - self.C, idx)) + list(range(idx + 1, idx + self.C + 1))
        pos_idx = [i % len(self.text_encoded) for i in pos_idx]
        pos_words = self.text_encoded[pos_idx]
        
        # torch.multinomial(input, num_samples, replacement=False, *, generator=None, out=None)
        # 可以根据input的权重随机选出num_samples个input的下标, 当input中的值为0时,则不会返回该值的下标
        # replacement表示是否是有放回的抽取
        select_weight = copy.deepcopy(self.word_freq)
        select_weight[pos_words] = 0    # 去除背景词
        select_weight[center_word] = 0   # 去除中心词
        # 每取一个背景词,需要取K倍的负采样
        neg_words = torch.multinomial(select_weight, self.K * pos_words.shape[0], True)

        return center_word, pos_words, neg_words  

构建Loader

利用torch.utils.data.DataLoader将dataset的数据按照batch_size输出。
这里将前两个类添加进来,实现从读取文本到构建数据迭代器的一条龙

class My_data_loader():
    def __init__(self, data_path, batch_size, shuffle=True):
        process_data = Process_source_data(data_path)
        self.dataset = Skip_gram_Dataset(process_data.text, process_data.word2id, process_data.word_freq)
        self.loader = Data.DataLoader(self.dataset, batch_size, shuffle)

构建训练模型

由于一个词可能充当中心词,也可能充当背景词,因此需要构建两个Embedding表分别进行词嵌入。
模型中的具体计算方法可查看文章开头给的两篇文章。
按照 Word2Vec 论文所写,推荐使用中心词向量作为最终的Embedding结果返回。

class Embedding_Model(nn.Module):
    def __init__(self, vocab_size, embed_size):
        super().__init__()
        self.in_embed = nn.Embedding(vocab_size, embed_size)  #中心词的词向量矩阵
        self.out_embed = nn.Embedding(vocab_size, embed_size) #背景词的词向量矩阵
        
    def forward(self, input_labels, pos_labels, neg_labels):
        input_embedding = self.in_embed(input_labels)  # [bs, embed_size]
        input_embedding = input_embedding.unsqueeze(2) # [bs, embed_size, 1]
        pos_embedding = self.out_embed(pos_labels)    # [bs, windows * 2 , embed_size]
        neg_embedding = self.out_embed(neg_labels)    # [bs, windows * 2 * K, embed_size]
        
        # 中心词与背景词应该同时出现,因此pos_dot的sigmoid结果应该趋于1
        pos_dot = torch.bmm(pos_embedding, input_embedding) # [batch_size, (window * 2), 1]
        pos_dot = pos_dot.squeeze(2) # [batch_size, (window * 2)]
        
        # 中心词与噪声词(负采样)不应该同时出现,因此pos_dot的sigmoid结果应该趋于0,
        # 但由于sigmoid函数的输出越接近1, logsigmoid的输出越接近0 
        # 因此多一个负号, 使得igmoid结果应该趋于1
        neg_dot = torch.bmm(neg_embedding, -input_embedding) # [batch_size, (window * 2 * K), 1]
        neg_dot = neg_dot.squeeze(2) # batch_size, (window * 2 * K)]
        
        # sigmoid函数的输出在为0-1之间, 则logsigmoid的输出全都小于0
        # 当sigmoid函数的输出越接近1, 则logsigmoid的输出越接近0
        # 可以理解为输出为1的损失小,为0的损失大
        log_pos = F.logsigmoid(pos_dot).sum(1) 
        log_neg = F.logsigmoid(neg_dot).sum(1)
        
        loss = log_pos + log_neg
        
        # logsigmoid的输出全都小于0, 如果要最小化loss,需要取负号
        return -loss

模型训练

data_path = 'text8/text8.train.txt'
batch_size = 32
lr = 0.2
epochs = 2
MAX_VOCAB_SIZE = 10000
EMBEDDING_SIZE = 100

my_loader = My_data_loader(data_path, batch_size)
model = Embedding_Model(MAX_VOCAB_SIZE, EMBEDDING_SIZE)
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
for e in range(epochs):
    for i, (input_labels, pos_labels, neg_labels) in enumerate(my_loader.loader):
        input_labels = input_labels.long()
        pos_labels = pos_labels.long()
        neg_labels = neg_labels.long()

        optimizer.zero_grad()
        loss = model(input_labels, pos_labels, neg_labels).mean()
        loss.backward()

        optimizer.step()

        if i % 100 == 0:
            print('epoch', e, 'iteration', i, loss.item())

测试词向量

写个函数,找出与某个词相近的一些词,比方说输入 good,他能帮我找出 nice,better,best 之类的

word2idx = my_loader.process_data.word2id
idx2word= my_loader.process_data.idx2word
embedding_weights = model.in_embed
def find_nearest(word):
    index = word2idx[word]
    embedding = embedding_weights(index)
    cos_dis = np.array([scipy.spatial.distance.cosine(e, embedding) for e in embedding_weights])
    return [idx2word[i] for i in cos_dis.argsort()[:10]]

for word in ["two", "america", "computer"]:
    print(word, find_nearest(word))
# 输出
two ['two', 'zero', 'four', 'one', 'six', 'five', 'three', 'nine', 'eight', 'seven']
america ['america', 'states', 'japan', 'china', 'usa', 'west', 'africa', 'italy', 'united', 'kingdom']
computer ['computer', 'machine', 'earth', 'pc', 'game', 'writing', 'board', 'result', 'code', 'website']
  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值