以上下文词汇预测当前词』架构被称为连续词袋(CBOW),还有另一种架构,刚好反过来,根据当前词推测当前单词可能的前后单词,这种架构就是所谓的Skipgram架构
负采样:
自然语言处理领域中,判断两个单词是不是一对上下文词(context)与目标词(target),如果是一对,则是正样本,如果不是一对,则是负样本。采样得到一个上下文词和一个目标词,生成一个正样本(positive example),生成一个负样本(negative example),则是用与正样本相同的上下文词,再在字典中随机选择一个单词,这就是负采样(negative sampling)。
比如给定一句话“这是去上学的班车”,则对这句话进行正采样,得到上下文“上”和目标词“学”,则这两个字就是正样本。负样本的采样需要选定同样的“上”,然后在训练的字典中任意取另一个字,“梦”、“目”,这一对就构成负样本。
训练需要正样本和负样本同时存在。
有字向量和词向量,区别在于字数量较小,汉语不同的字就那么多个,但词的数量很多很多,比如天,可以是天空,天气,今天,蓝天等,所以词向量需要先分词且数量庞大,字多用于古诗词等,词多用于小说。
表示方式,举个例子:
今:[1,0,0,......,0]1*50,用一个大小为1*50的矩阵来表示今这个字
今天:[1,0,0,......,0]1*50
训练过程参数有:统计文本里面出现的不重复的词,并分配id
- word_2_index--------一般为字典形式,就是词对应的索引,例如:今天:0
- index_2_word--------字典,或者list,索引对应的词,例如:0:今天
- word_2_onehot------表示的是词到向量的映射,例如:今天:[1,0,0,......,0]
训练过程,以skip-gram为例:
今天天气真好,假设word_size=5000(这个词库的大小,也就是词的数量),embedding_num=50(表示这个词用维度的大小,嗯。。。。就是1*50中的这个50,应该是这样,错的话哈哈哈我就笑笑哈哈哈)
接下来,比如说,天气
- 查表会映射word_2_onehot,得到[0,1,0,0,.........,0]1*5000(5000就是word_size)
- [0,1,0,0,.........,0]@w1, w1一开始就是一个随机的生成的,大小为word_size*embedding_num的矩阵,之后会得到一个1*embedding_num的向量,不懂为啥的可以去学习一下线性代数,嘿~
- 然后再用1*50的向量@w2,w2一开始是一个随机的生成的,大小为embedding_num*word_size的矩阵,与w1的形状是转置的,但内容不是哟,只是说w1的行的大小是w2列的大小,w1的列的大小是w2的行的大小。结果得到一个1*word_size的向量,将这个向量送到sofmax函数中,然后将结果与词库里的真好对应的word_2_onehot做一个loss得到损失值,然后修改w1,继续重复以上步骤。
代码:
import numpy as np
import pandas as pd
import pickle
import os
import jieba
from tqdm import tqdm
def load_stop_words(file="stopwords.txt"):
"""
从文件中读取停用词表
参数:
file:str,停用词表文件路径
返回值:
stop_words:list,停用词表
"""
with open(file, 'r', encoding="utf-8") as f:
return f.read().split("\n")
def cut_word(file="数学原始数据.csv"):
"""
对原始数据进行分词处理,并去除停用词
参数:
file:str,原始数据文件路径
返回值:
result:list,分词后的数据列表
"""
stop_words = load_stop_words()
result = []
all_data = pd.read_csv(file, encoding='gbk', names=["data"])["data"]
for words in all_data:
cut_words = jieba.lcut(words)
result.append([word for word in cut_words if word not in stop_words])
return result
def get_dict(data):
"""
构建词汇表,并将词汇表中的每个单词表示成 one-hot 向量
参数:
data:list,分词后的数据列表
返回值:
word_2_index:dict,单词到索引的映射表
index_2_word:list,索引到单词的映射表
word_2_onehot:dict,单词到 one-hot 向量的映射表
"""
index_2_word = [] # 索引到单词的映射表
for words in data:
for word in words:
if word not in index_2_word:
index_2_word.append(word)
word_2_index = {word: index for index, word in enumerate(index_2_word)} # 单词到索引的映射表
word_size = len(word_2_index)
word_2_onehot = {}
for word, index in word_2_index.items():
one_hot = np.zeros((1, word_size))
one_hot[0, index] = 1
word_2_onehot[word] = one_hot # 单词到 one-hot 向量的映射表
return word_2_index, index_2_word, word_2_onehot
def softmax(x):
"""
softmax 函数的实现
参数:
x:numpy.ndarray,需要进行 softmax 处理的向量
返回值:
result:numpy.ndarray,softmax 处理后的向量
"""
ex = np.exp(x)
return ex / np.sum(ex, axis=1, keepdims=True)
if __name__ =="__main__":
data = cut_word()
word_2_index,index_2_word,word_2_onehot = get_dict(data)
word_size = len(word_2_index)
embedding_num = 107
lr = 0.01
epoch = 10
n_gram = 3
num_neg_samples = 5
neg_sampling_power = 0.75
w1=np.random.normal(-1,1,size=(word_size,embedding_num))
w2=np.random.normal(-1,1,size=(embedding_num,word_size))
word_counts = {}
for words in data:
for word in words:
if word not in word_counts:
word_counts[word] = 0
word_counts[word] += 1
total_word_count = sum(word_counts.values())
word_freqs = {word: count/total_word_count for word, count in word_counts.items()}
word_sampling_probs = {word: word_freqs[word]**neg_sampling_power for word in word_counts}
word_sampling_probs = {word: prob/sum(word_sampling_probs.values()) for word, prob in word_sampling_probs.items()}
for e in range(epoch):
for words in tqdm(data):
for n_index,now_word in enumerate(words):
now_word_onehot = word_2_onehot[now_word]
#negative sampling
neg_words = np.random.choice(list(word_2_index.keys()), size=num_neg_samples, replace=False, p=list(word_sampling_probs.values()))
neg_word_indices = [word_2_index[word] for word in neg_words]
context_words = words[max(n_index-n_gram,0):n_index] + words[n_index+1 : n_index+1+n_gram]
for context_word in context_words:
context_word_onehot = word_2_onehot[context_word]
hidden = now_word_onehot @ w1
p = hidden @ w2
pre = softmax(p)
#loss = -np.sum(other_word_onehot * np.log(pre))
#A@B=C
#delta_C=G
#delta_A=G@B.T
#delta_B=A.T@G
G2 = pre - context_word_onehot
delta_w2 = hidden.T @ G2
G1 = G2@ w2.T
delta_w1 = now_word_onehot.T @ G1
w1 -= lr * delta_w1
w2 -= lr * delta_w2
#negative samples
for neg_word_index in neg_word_indices:
neg_word_onehot = word_2_onehot[index_2_word[neg_word_index]]
hidden = now_word_onehot @ w1
p = hidden @ w2
pre = softmax(p)
G2 = pre - neg_word_onehot
delta_w2 = hidden.T @ G2
G1 = G2@ w2.T
delta_w1 = now_word_onehot.T @ G1
w1 -= lr * delta_w1
w2 -= lr * delta_w2
with open("word2vec.pkl","wb") as f:
pickle.dump([w1,word_2_index,index_2_word],f)
这里面已经加了负采样了。
友情提示:最好部署到服务器上跑,哈哈哈。
我是菜小硕,知识的搬运工,里面的文件,这里就不提供了,自己换成别的,也可以去B站找一下,未加负采样之前的代码来源:http://【本课程详细讲解了word2vec的训练过程和方法, 不涉及数学公式, 简单易懂, 并且全程手写代码加强理解。】 https://www.bilibili.com/video/BV11U4y1E7sK/?p=9&share_source=copy_web&vd_source=b4a26e247f3c69e1c20d1b01eb804f7a主要是为了自己梳理和记录一下学习的内容,可以用于回顾,如果对你有帮助,那我这个搬运工也算是有所贡献,哈哈哈。