赞
踩
本来是不打算做这个CBOW代码案例的,想快马加鞭看看前馈神经网络
毕竟书都买好了
可是…可是…我看书的时候,感觉有点儿困难,哭的很大声…
感觉自己脑细胞可能无法这么快接受
要不,还是退而求个稍微难度没那么大的事,想想自己还有什么是没实现的呢
哦!CBOW的案例还没做呢~
在一个巨人面前,我无耻地选择了暂避其锋芒
就好像,我本应该英勇地迈过刀山火海,可是我却怂了
而且,怂的有理有据:CBOW还没实现呢
只希望,CBOW案例,不要太难,不然我根本寸步难行,只能天天打游戏聊以自慰
首先,我计划根据一个客服语料,然后用CBOW去预测某个中间关键词
目标明确后,问题接踵而至
问题1-能否用中文语料
:不行,因为中文分词很麻烦,我懒得去搞分词,重点是CBOW
英文语料很简单,因为英文是一个词跟另一个词,用空格分开,获取十分简单
问题2-头尾单词怎么解决
:毕竟CBOW是获取关键词的前c个和后c个单词来训练的,但开头单词没有前c个单词,末尾单词没有后c个单词
解决办法
:用额外的单词替代,比如None,应该问题不大的吧
问题3-按句还是按所有句取上下文
:如果是按句取上下文,那么一个句子开头和末尾单词的下文就是None,语料文件有很多个句子,就会有很多个None。
如果按所有句取上下文,那整个语料文件就只有开头有None,末尾有None,None数量很少
解决办法
:还是按句吧,毕竟两句之间的上下文,是毫无关系的。
按照【基于HierarchicalSoftmax的CBOW】正反向传播流程,来设计程序吧!
【基于HierarchicalSoftmax的CBOW】正向传播过程
- 输入层:
- 转换独热编码:将词典D转换为one-hot独热编码,
- 获取上下文:按规定上下文的长度k,来截取语库C里的上下文单词 x x x和预测单词 y ∗ y* y∗
- 获取独热编码:获取上下文单词x的独热编码向量 x 1 x1 x1, x 2 x2 x2, x 3 x3 x3, x 4 x4 x4,作为初始输入矩阵X=[ x 1 x1 x1, x 2 x2 x2, x 3 x3 x3, x 4 x4 x4]
- 投影层:
- 计算中间向量:
- 将初始矩阵X乘以一个权重矩阵W,提取出各个初始向量 x 1 x1 x1, x 2 x2 x2, x 3 x3 x3, x 4 x4 x4的权重系数 w 1 w1 w1、 w 2 w2 w2、 w 3 w3 w3、 w 4 w4 w4
- 将这些权重系数加和,作为中间向量h=[ w 1 w1 w1+ w 2 w2 w2+ w 3 w3 w3+ w 4 w4 w4],注意,这里的加和是按列加和
- 输出层:
- 构建huffman树,
- 计算路径概率
- 计算预测模型
其次,要先进行数据结构设计,总不能用向量吧,因为每个单词,都有对应的独热编码
有对应关系的,可以用字典、也可以用pandas的dataframe的二维结构数据
考虑到dataframe有较多的计算操作语法,或许会更加简洁,不需要自己另外设计各种计算的操作方式。
所以,能用dataframe的就尽量多用dataframe吧
输入层:
- dataframe:
- 所有单词的独热编码
- 选取的上下文独热编码
投影层:
- dataframe:
- 每个单词潜在特征的向量组成的矩阵W(词向量)
- 选取的上下文词向量
- 中间向量h(上下文的词向量之和)
输出层:
- 二叉树节点:
- 矩阵:每个节点上的θ值
至于构建huffman树,打算是构建一个二叉树对象,然后每个节点上的θ用矩阵表示
暂定这样吧。
输入层,需要给投影层输入一个上下文里每个单词的独热编码
但首先,要先有语料C,才能依次获取上下文进行训练
我在想,到底是每次获取一个上下文进行训练,还是一次性把所有上下文都获取了再训练。
。。。。每次训练一个上下文,会不会要训练很久
。。。。可是,要获取所有的上下文,那就相当于语料C如果总共有1000个单词,那上下文就会有1000个向量构成的矩阵了,量会不会太大了呢?
不如每次获取一个,然后每次都训练一个
- 问题来了:每次训练一个上下文,那这个上下文要训练到底,还是说要等所有上下文都训练一次后,再统一反向传播去迭代模型参数呢?
- 很显然,肯定是要所有上下文都训练一次后,再反向传播
- 所以上下文的训练是按批次来进行的
- 第一批:
- 正向传播:所有上下文依次训练第一次
- 反向传播:模型参数迭代第一次
- 第二批:
- 正向传播:所有上下文依次训练第二次
- 反向传播:模型参数迭代第二次
…
- 从语料中得到一个不含重复单词的词典D
- 将词典D中所有单词转换成独热编码矩阵
现在要从语料C种,获取一个上下文,但考虑到依次获取所有上下文
所以需要循环获取上下文
另外,先规定一下,上下文的n为4,也就是一个上下文的上文为2,下文为2,另外要预测上下文中间的单词,所以总共要获取的上下文和待预测单词,就是5
上文单词(2个)+待遇测中间单词(1个)+下文单词(2个)
获取4个上下文的独热编码
- dataframe:
- 每个单词潜在特征的向量组成的矩阵W(词向量)
- 选取的上下文词向量
- 中间向量h(上下文的词向量之和)
每个单词的特征维度数,由我们自己决定。
我决定,假设每个单词由3个维度来决定(具体是什么维度,其实我也不知道,这个要交给神经网络去自动计算迭代里边的数值)
那么所有单词,都有自己的词向量
假设D词典里的单词数为N,词向量维度为3,所有单词的词向量矩阵就是一个Nx3的矩阵
我们将这个矩阵里的每个值,都初始化为1
- 然后获取单个上下文的词向量矩阵如下
不理解,为什么一个单词里的词向量,三个值都一样呢?
值一样那还有什么意义?
但思考了一下原本W迭代思路,确实也没看出它的三个值会有区别的计算
但。。。该怎么办呢?值都一样,是没意义的,难道是我对于CBOW的原理思考有点儿问题?
唉。。。回去再看看。。。
实操困惑解决:W和θ的初始化,都用随机数(我后来用了随机正太分布值)
我以为只要参数进入训练,就可以自动调参并往好的方向发展
但现在看来,不太像。
现在让参数随机初始化,就像是给了随机化的数值,然后随机数值与随机数值之间进行关系的重组计算。
这就好像是,如果一群男的和一群男的,无论经过多少次迭代计算,都一定生不出小孩
但如果我随机设置性别,那一群男女和一群男女,经过多次迭代计算,应该能计算出生小孩的最大概率组合
如果是这么个意思,那我之前对于W词向量的特征理解就有误了。
我原以为词向量是对一个单词的潜在语义的数值表示,比如褒贬义、情感色彩、动名词等描述一个单词的特征数值,但现在看来并不是。
现在在CBOW训练下的词向量,只是纯粹的随机值,经过迭代后能够结合其他单词的词向量一起,更好地预测中心单词,仅此而已。
并且不同的初始随机值,最终训练出来的词向量W都未必是一样的。
百度了很久,有up主提到scikit-learn还是什么官方包,在初始化时用的是正态分布随机数来初始化的。
但没人能告诉我,为什么参数W要用正态分布的随机数来进行初始化。
没办法,只能自己进行一下合理的猜测。
首先,中间向量h是根据上下文的词向量W相加而成。
如果W词向量的每一项数值都是服从同一个(0,1)的正态分布,那么h向量里的每一个数值也都是服从同一个(0,1)正态分布的。
也就是,h向量里的每一项都是围绕0上下浮动的,且总和加起来有可能大于0,也有可能小于0
那么,当中间向量h,进入huffman里,与每个节点的θ进行sigmoid的概率计算。
P_右子树 = sigmoid(h*θ)
假设单个节点的θ向量里的每一项都是1,而h向量里的每一项都服从同一个(0,1)正态分布
那么h与θ的点乘,就会围绕在0上下,且有可能大于0,也有可能小于0。
那这样的话,根据sigmoid函数特点,当前节点计算出来的sigmoid值,也就围绕在0.5上下。
所以,如果我们把词向量W用服从正态分布的随机数来初始化,那么在进入huffman树进行每一层的节点选择时,左子树和右子树的概率就相对比较均衡(即,既有可能进入左子树,也有可能进入右子树,避免我们自己人为设定固定值造成模型一开始有较大的偏差)
这是我自己猜测下,目前认为比较合理的
但也有可能后期打脸,到时候脸疼了再说吧
眼泪哗哗哗哗地流,之前一直出问题
周六下午闲暇无聊,排了一下bug!
终于发现问题了,呜呜呜呜呜呜tmd,列表不能直接赋值,我早该想到的。。。py的列表唉
排完bug,做了一系列检查
首先看看,每个待预测单词路径下,每个经过的节点的一些属性值,尤其是θ值,可以看到是经过迭代的
第二个检查是W的检查,W也发生了迭代,但因为迭代次数少,所以可能不那么明显
import math import numpy as np from docx import Document import re import pandas as pd import random pd.options.display.max_rows = None """ 需要提前设置的参数:doc_path语料的word文档、η学习率、词向量的维度W_columns、iterate_times迭代次数、context_word_num单个上下文的词数量 +++++++ 【W词向量的所有初始值、θ霍夫曼树非叶子节点上的权重参数】:这两个关键参数在后续用正态分布随机树来进行初始化 ++++ """ doc_path = r"trainingword.docx" η = 0.1 W_columns = 20 iterate_times = 100 context_word_num = 4 # 获取语料库C+统计无重复单词的词典D doc = Document(doc_path) C_list = [] all_text = '' for i in doc.paragraphs: if len(i.text) != 0: para = [x for x in re.split(' |!|\?|\.|。|,|,|\(|\)', i.text) if x] C_list.append(para) all_text = all_text + i.text words_org = [x for x in re.split(' |!|\?|\.|。|,|,|\(|\)', all_text) if x] """缺失单词设为aaa,以便上下文开头结尾处缺失""" # replace_word = 'aaa' # words_org.append(replace_word) """词典D转独热编码""" D_list = set(words_org) N = len(D_list) D_onehot = {} for index, value in enumerate(D_list): temp = [0] * N temp[index] = 1 D_onehot[value] = temp del temp # 最终词典D的独热编码为D_df D_df = pd.DataFrame(D_onehot) """初始化词向量矩阵W:W_columns个维度特征""" # 这意味着后续的中间向量h和huffman树上的θ向量,分别都是用3个值表示,如 h=[1,10,20] W_dict = {} for index, word in enumerate(D_list): W_dict[word] = [10*random.gauss(0,1) for i in range(W_columns)] # 最终词向量用W_df表示 W_df = pd.DataFrame(W_dict) print(f"初始的词向量W为") print(W_df) # 统计每个单词的词频,word_count是series数据类型 word_count = pd.value_counts(words_org) # word_count.pop(item='aaa') """第2阶段huffman树:设置huffman树-节点对象的属性""" class Node: # 一棵huffman树,有几个特征: # 根节点:没有d值,没有path值,没有word值 # 非叶子节点:没有word值 # 叶子节点:没有θ值,没有left左子树、right右子树 def __init__(self, value, word): self.value = value # !!! self.word = word # 如果是叶子节点,则存储按权重划分好的最终的某个单词 self.θ = None # 待迭代的参数θ self.d = None # 当前节点的编码,是0或1,左节点为0,右节点为1 self.path = [] # 记录从根节点到当前节点的编码路径,如[0,1,0,1] self.left = None # 左子树!!! self.right = None # 右子树!!! """定义输出的魔法函数""" def __str__(self): return (self.value, self.word) """第2阶段huffman树:插入排序堆""" def insert_sort_node(nodes, node): # 如果nodes列表里,没有任何节点了,则表示所有node节点都已经划分完毕, # 把最后的一个node作为根节点插入nodes列表后,返回只包含一个根节点的nodes列表。 if len(nodes) == 0: nodes.append(node) return nodes # 要根据Node对象里的value值(权重),插入nodes列表,并排序 for index in range(len(nodes) - 1, -1, -1): if node.value < nodes[index].value: nodes.insert(index + 1, node) break else: nodes.insert(0, node) # 查看每次nodes堆变化 # list1 = [(node.value, node.word) for node in nodes] # print(list1) return nodes """第2阶段huffman树:建huffman树,完成每个node的word单词、d值、value权重值、left左子树、right右子树的属性存储""" def build_huffman_tree(nodes): # huffman树的建立过程:不断地从频次最低(即权重最低)的两个节点开始,合并成新节点 # ① 初始:将所有单词都按频次,由高到低存储在列表nodes里 # ② pop取出频次最低的单词及频次值,作为右节点,设置当前右节点的编码值d为0 # ③ pop再去除频次第二低的单词及频次值,作为左节点,设置当前左节点的编码值d为1 # ④ 将左右节点的频次值加和,作为一个新节点,插入到nodes列表里,重新排序 # ⑤ 重复①②③④,但是要注意,第5步合并成新节点node之后,node就不是单词了,而是一个有左右子节点的node,只有权重值(权重值=它的左子节点权重+右子节点权重) while len(nodes) > 1: node_right = nodes.pop() node_right.d = 0 node_left = nodes.pop() node_left.d = 1 node = Node(node_left.value + node_right.value, None) node.left = node_left node.right = node_right nodes = insert_sort_node(nodes, node) else: huffman_tree = nodes[0] return huffman_tree """第2阶段huffman树:遍历huffman树,并依次初始化θ,记录每个单词的huffman树的路径path""" word_path = {} def search_huff_tree(node_now, last_path=None): # 从根节点,到每一个叶子节点,都遍历一次,顺路存储每个节点(除了根节点外)的θ初始参数和path路径 node = node_now # 当一个node节点的word为空,说明它是非叶子节点,且θ参数为空时,说明它是θ还没有初始化的非叶子节点,这时候才可以对当前node初始化θ if node.word == None and node.θ == None: node.θ = [random.randint(-5,5) for i in range(W_columns)] # 当一个node节点上一个节点last_path不为空时,说明上个节点是有path值的,当前节点node先把上个节点的path继承下来 if last_path != None: node.path = [p for p in last_path] if node.word == None: # 只要当前节点不是叶子节点,就增加新节点的编码值作为node路径path, # 并且还要向下递归,遍历下一个节点,从而开始下一个节点搜索,完成下一个节点的θ值初始化和记录下一个节点path路径,直到所有节点都遍历完 node.path.append(node.d) path = [p for p in node.path] search_huff_tree(node.right, path) node.path.append(node.d) search_huff_tree(node.left,path) else: # 如果当前节点是叶子节点,那就在当前节点的path里,添加完当前的编码值,形成当前node的path值,然后结束本次搜索 node.path.append(node.d) """所有单词的路径,都存储在列表word_path里""" word_path[node.word] = [p for p in node.path] return """第2阶段huffman树:建立huffman树,并初始化非叶子节点上的参数θ,设置每个非根节点上的d编码值(左1右0)""" nodes = [] for word_value in word_count.items(): nodes.append(Node(word_value[1], word_value[0])) huffman_tree = build_huffman_tree(nodes) search_huff_tree(huffman_tree) """第3阶段反向传播:需要迭代θ和W""" """第3阶段反向传播:【根据上个节点的d值,判断下个节点是选左,还是选右(左1右0)】""" def get_node(next_d, node_now=None): if next_d == 0: return node_now.right elif next_d == 1: return node_now.left def sigmoid(h, θ): x = np.matmul(h, θ) try: result = 1 / (1.0 + math.exp(-x)) except OverflowError: if -x>0: result = 1 / (1.0 + math.exp(700)) elif -x<0: result = 1 / (1.0 + math.exp(-700)) return result """第3阶段反向传播:计算单个上下文到预测单词之间的概率总值""" def single_context_iterate(h, y_word, huffman_tree): global η, W_step_value path = word_path[y_word] # 获取当前单词的路径path if path[0] == None: path.pop(0) node = huffman_tree # node为根节点 for next_d in path: # 注意:根节点是没有d值,所以path里的所有不为0的d,都是当前节点的下一个节点的d值,所以就代号为next_d,表示当前node的下一个应选择的node的d值 """【下一个节点的d值,表示当前节点的选择方向】""" if node.word != None: # 当前节点的word值不是空值,说明它是叶子节点,则结束循环 break sig = sigmoid(h, node.θ) """迭代当前节点的θ值:十分重要!!!十分关键!!!!""" W_single_step_value = [η * (1 - next_d - sig) * θ_value for θ_value in node.θ] W_step_value = [x + y for x, y in zip(W_step_value, W_single_step_value)] θ_single_step_value = η * (1 - next_d - sig)*h node.θ = [x + y for x, y in zip(node.θ, θ_single_step_value)] node = get_node(next_d, node) return W_step_value """第3阶段反向传播:把所有上下文存进列表里""" # 由于我们要迭代很多次,所以每次都重新选取上下文,会非常耗时 # 因此,不如一次性,把所有的上下文都选取后,放进一个列表里边 # 这样,以后无论迭代多少次,上下文都是一样的,不需要重复选取 c = context_word_num//2 y_and_context_list = [] for sentence in C_list: for index, word in enumerate(sentence): """获取单个上下文""" y = word context = [] if index-2>=0: context.append(sentence[index-1]) if index-2>=0: context.append(sentence[index-2]) if index+1<len(sentence): context.append(sentence[index+1]) if index+2<len(sentence): context.append(sentence[index+2]) """计算单个上下文的中间向量h""" y_and_context = (y, context) y_and_context_list.append(y_and_context) """第3阶段反向传播:根据每个上下文和对应的待预测单词y,去迭代θ""" for i in range(iterate_times): print(f"第{i + 1}次迭代") for y_and_context in y_and_context_list: y, context = y_and_context if y == 'aaa': print('aaa也可以吗!') raise Exception """计算单个上下文的中间向量h""" h = W_df[context].sum(axis=1) W_step_value = [0.0]*W_columns # W的迭代公式,是需要对同一条路径下每个节点的迭代值加和后,再进行迭代的,所以需要这个中间值,表示迭代值加和 """计算每个上下文,到达待预测单词之间的每一个节点上的概率总值,并迭代每个θ值""" W_step_value = single_context_iterate(h, y, huffman_tree) """迭代当前的上下文里,每个单词的词向量W""" for word in context: W_df[word] += [step_value/W_columns for step_value in W_step_value] print(f"迭代后的词向量W为") print(W_df) """第3阶段的检查:根据每个待预测单词的path路径-依次查看每个路径的每个节点上的属性值""" def check_path_para(node_now): next_d = next(g) print(f"next_d值为{next_d},word值为{node_now.word},权重{node_now.value},θ为{node_now.θ}") if next_d == 1: node_now = node_now.left else: node_now = node_now.right # 如果下一个节点是叶子节点,就返回,否则继续往下递归 if node_now.word != None: return check_path_para(node_now) # 建立一个迭代器,辅助获取每个单词的路径 def get_next_d(y_path): for p in y_path: yield p for y_word in D_list: y_path = word_path[y_word] print(f"当前单词为{y_word},路径为{y_path}") g = get_next_d(y_path) check_path_para(huffman_tree) """第4阶段:检测迭代一定次数后,模型预测正确率""" # 已知训练好的词向量矩阵为W_df字典、所有待预测单词的正确路径word_path字典、已经训练好的huffman树(含θ) # 根据单个上下文的词向量来获取中间向量h,然后h进入huffman树,在每个节点上进行左右分岔路的概率计算 # 进入左子树的概率值为h*θ的1-sigmoid值、进入右子树的概率值为h*θ的sigmoid值 # 其实只需要判断一个概率值,即可确定下一个节点是左子树or右子树。 # 例如,如果进入右子树的概率值大于0.5,则让h进入右子树,否则进入左子树 # 依次循环进行,直到达到叶子节点,获取到依据概率判断得到的预测单词y*为止 # 若依概率获取到的单词y*与实际待预测单词y不同,则判定为预测错误,预测结果记为0;若y*与y相同,则预测正确,预测结果记为1。最终统计正确率 def get_y_predict(h,node_now): node = node_now """如果当前节点的word属性不为None的时候,说明已经到达叶子节点,返回叶子节点上的预测单词即可结束迭代""" if node.word != None: return node.word print(f"_______节点+1______") """如果未到达叶子节点,则进入迭代,依据右子树的概率sig来判断进入下一节点""" θ = node.θ sig = sigmoid(h,θ) # print(f"θ值为{node.θ}") # print(f"h*θ值{np.matmul(h,node.θ)},sig值为{sig}") if sig<0.5: node_next = node.right else: node_next = node.left return get_y_predict(h,node_next) result = [] # 记录每个预测结果 y_pre_lst = [] y_real_lst = [] for y_and_context in y_and_context_list: node_root = huffman_tree y, context = y_and_context """获取单个上下文的中间向量h""" print("++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++") # print(f"中间向量h{h},真实单词为{y}") print() h = W_df[context].sum(axis=1) y_pre = get_y_predict(h,node_root) if y_pre == y: result.append(1) else: result.append(0) y_pre_lst.append(y_pre) y_real_lst.append(y) data_y = pd.DataFrame([y_pre_lst,y_real_lst]) print(data_y) print(f"最后预测准确率为{sum(result)/len(result)}")
#程序验证的预测准确率,接近于0,不知道为什么,完全不知道该怎么办,找不到任何思路,除了怀疑hierarchical softmax对于参数的迭代公式本身有问题,但那都是论文出来的,怎么可能有问题呢。
头疼,非常头疼,国内的理论一大堆,实践真的寥寥无几
问了GPT-4,唉,句句都有道理,但就是没有落到实处的问题。。。查无所获,
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。