是华为的昇腾众智计划没错了,首先直接搜CharNgram是搜不到几乎任何东西的,因为这个语言模型叫Ngram模型, char是字符的意思,这是字符级的Ngram语言模型。
补充: CharNgram是用到了Ngram里对字符的处理思想,至于对每个字母的预训练词向量是如何训练出的,应该在文献中。
什么是Ngram语言模型(2021.7.11)
- 基本思想:将文本里面的内容按照字节(char)进行大小为N的滑动窗口操作,形成了长度是N的字节片段序列。
输入:唧唧复唧唧
切分长度:3
列表输出:['唧唧复', '唧复唧', '复唧唧']
集合输出:{('唧', '唧', '复'), ('复', '唧', '唧'), ('唧', '复', '唧')}
- 每一个字节片段成为gram(克),对所有gram的出现频度进行统计,并且按照事先设定好的阈值进行过滤,形成关键gram列表,也就是这个文本的向量特征空间。列表中每一种gram就是一个特征向量维度。
- 该模型基于这样的假设,第N个词的出现只与前面N-1个词相关,而与其他任何词多度不相关,整句的概率就是各个词出现概率的乘积。这些概率可以通过直接从语料中统计N个词同时出现的穿刺术得到。常用的是二元的Bi-Gram和三元的Tri-Gram。
以上部分是Ngram语言模型的说明,和要实现的CharNgram貌似还不是很一样。。。
CharNgram和GloVe以及FastText都类似,是一个词向量模型。
以GloVe为例,GloVe通过算法进行语句分词、搭建模型等等操作,基于语料库训练出模型,如
图中这些都是训练好的模型,glove.6B.50d
是GloVe模型的,有6B个词的,50维的词向量模型,通过调用该模型,可以将一条语句用1*50
的tensor表示在向量空间。
最新GLove词向量预训练文件国内服务器下载
以charngram(100维)为例:
from torchtext.vocab import CharNGram
from torchtext.vocab import GloVe
from torchtext.vocab import FastText
from torch import blackman_window
Vectors = CharNGram()
print(Vectors['My name is Wang Yanjing.'])
# 输出
tensor([[-0.2658, -0.2718, -0.2288, 0.0344, -0.1136, -0.2309, 0.4125, 0.1443,
-0.3710, 0.5686, 0.1151, -0.4536, -0.2334, -0.2856, 0.1084, 0.6384,
-0.0823, -0.3413, 0.0104, 0.0701, -0.1546, 0.2360, -0.3585, 0.3601,
0.0722, -0.1139, -0.1138, -0.4176, 0.4759, 0.1183, 0.0982, 0.1348,
-0.0175, 0.5187, 0.3676, -0.2539, 0.2249, 0.2301, -0.1833, 0.4381,
-0.0426, -0.1642, -0.2591, 0.3303, -0.2448, -0.0107, 0.4560, -0.1828,
0.0871, -0.1294, 0.3380, 0.2650, 0.2327, -0.0858, -0.1031, 0.3131,
0.5225, 0.0023, 0.1901, -0.3117, 0.1022, -0.0141, -0.0613, 0.0876,
0.2386, -0.0949, -0.2063, 0.0091, 0.0067, -0.2477, 0.4505, -0.4122,
0.2108, -0.2395, 0.0539, 0.6798, -0.2289, 0.0048, -0.1066, -0.0020,
0.0778, 0.2532, -0.2099, 0.2040, 0.0598, 0.1349, 0.0604, -0.0370,
-0.1130, -0.2258, 0.1604, 0.2693, -0.2702, 0.3247, 0.1394, 0.2917,
-0.0868, 0.3125, -0.0340, 0.4312]])
但问题就是,GloVe和FastText模型怎么训练,原理是什么,训练的源代码,加载的源代码都有,看起来还行,我这个CharNgram就像个孤儿,唯一从torch那看到的链接论文里,还是纯英文,全文只有一处github链接的注释提到了charngram这个词。。。
如果不要求自己训练模型的话(应该是不需要,毕竟是数据增强算子,加载词向量),只是将训练好的模型加载进来,用该模型对我们输入的文本进行一个映射,得到一个100维的tensor,就可以了。
那么我们做的工作就是对文本进行初步预处理,分词等,然后根据模型进行一个映射,得到一个tensor即可。
华为串讲要提问的问题(旧)
- 我们要做的只是对用户输入的文本进行一个预处理,进行分词等,然后加载训练好的模型,得到一个张量输出是吗?不需要我们自己训练模型是吗?
- 我们对分词的话,比如中文可以调用jieba分词库吗?或者英文别的分词函数吗?
- 我的这个charngram的分析文档有点难找,在torchtext中也只有一个预训练词向量文件,请问这个是比较小众的加载方法吗?
重大进展(2021.7.12)
- GloVe和FastText都只能得到单词的向量,CharNgram可以得到任意长度的文本的向量。因为映射方式不同。
- GloVe和FastText貌似都是直接加载了预训练的词向量表之后,直接用输入的单词去词向量表里面查找对应的那一串词向量。例如:
from torchtext.vocab import CharNGram
from torchtext.vocab import GloVe
from torchtext.vocab import FastText
examples = ['chip', 'baby', 'Beautiful']
vec = GloVe(name='6B', dim=50)
ret = vec.get_vecs_by_tokens(examples, lower_case_backup=True)
print(ret)
# 输出
tensor([[-0.7710, -1.1697, 1.5195, 0.8371, 0.7419, -0.2185, -0.7212, -0.9400,
-0.0113, 0.5485, 0.4040, -0.1846, -0.4630, 0.2620, -0.6464, 0.3599,
-0.8610, -0.3869, -0.0271, -1.0254, 0.3280, -0.7500, -0.6859, -0.6912,
0.3429, -0.6660, -0.2910, -0.6104, 0.3322, -0.4252, 2.4573, -0.8748,
0.4891, 1.2888, 0.5780, -0.5509, -0.2263, 0.8127, 0.7048, -0.5498,
0.3620, -0.2171, -0.2991, 0.2917, 1.2260, 0.2446, 1.2133, -0.0967,
0.0474, 0.1971],
[ 0.5494, 0.2299, -0.0357, -0.9143, 0.7044, 1.3736, -0.9937, -0.5034,
0.5793, 0.3481, 0.2385, 0.5444, 0.3432, 0.5741, 1.3732, 0.4636,
-0.7288, 0.2887, 0.1001, -0.2302, -0.1289, 0.7033, 0.3961, 0.2605,
0.2697, -1.3036, -0.9377, 0.2705, 0.6070, -0.6689, 1.9709, 0.6796,
-0.6944, 1.0380, 0.5136, 0.2302, 0.3646, -0.3090, 1.1395, -1.1466,
-0.7889, 0.0544, -0.0691, -0.2439, 1.4049, 0.0919, 0.2313, -1.3028,
0.3246, 0.1074],
[ 0.5462, 1.2042, -1.1288, -0.1325, 0.9553, 0.0405, -0.4786, -0.3397,
-0.2806, 0.7176, -0.5369, -0.0046, 0.7322, 0.1210, 0.2809, -0.0881,
0.5973, 0.5526, 0.0566, -0.5025, -0.6320, 1.1439, -0.3105, 0.1263,
1.3155, -0.5244, -1.5041, 1.1580, 0.6880, -0.8505, 2.3236, -0.4179,
0.4452, -0.0192, 0.2897, 0.5326, -0.0230, 0.5896, -0.7240, -0.8522,
-0.1776, 0.1443, 0.4066, -0.5200, 0.0908, 0.0830, -0.0220, -1.6214,
0.3458, -0.0109]])
去查找glove.6B.50d.txt
中的chip、baby、Beautiful
:
chip -0.77103 -1.1697 1.5195 0.83709 0.74189 -0.21854 -0.72117 -0.93998 -0.011318 0.54849 0.40399 -0.1846 -0.46297 0.26203 -0.64637 0.35986 -0.86101 -0.38694 -0.027086 -1.0254 0.32798 -0.75004 -0.68591 -0.69124 0.34293 -0.66602 -0.29097 -0.61035 0.33217 -0.42516 2.4573 -0.87484 0.48914 1.2888 0.57795 -0.5509 -0.22631 0.81266 0.70484 -0.54982 0.36196 -0.21708 -0.29905 0.2917 1.226 0.24455 1.2133 -0.096686 0.047387 0.19711
baby 0.54936 0.22994 -0.035731 -0.91432 0.70442 1.3736 -0.99369 -0.50342 0.5793 0.34814 0.23851 0.54439 0.34322 0.57407 1.3732 0.46358 -0.72877 0.28868 0.10006 -0.2302 -0.12893 0.7033 0.39612 0.26045 0.26971 -1.3036 -0.93774 0.27053 0.60701 -0.66894 1.9709 0.6796 -0.69439 1.038 0.51364 0.23022 0.36456 -0.30902 1.1395 -1.1466 -0.78887 0.054432 -0.069112 -0.24386 1.4049 0.091876 0.23131 -1.3028 0.3246 0.10741
beautiful 0.54623 1.2042 -1.1288 -0.1325 0.95529 0.040524 -0.47863 -0.3397 -0.28056 0.71761 -0.53691 -0.0045698 0.73217 0.12101 0.28093 -0.088097 0.59733 0.55264 0.056646 -0.50247 -0.63204 1.1439 -0.31053 0.1263 1.3155 -0.52444 -1.5041 1.158 0.68795 -0.85051 2.3236 -0.41789 0.44519 -0.019216 0.28969 0.53258 -0.023008 0.58958 -0.72397 -0.85216 -0.17761 0.14432 0.40658 -0.52003 0.09081 0.082961 -0.021975 -1.6214 0.34579 -0.010919
确实是直接去查表,然后返回的,FastText中也是一样。所以对应的torchtext中这两者的类定义也非常简单,只是继承Vectors类并重写了一下__init__
初始化函数:
class GloVe(Vectors):
url = {
'42B': 'http://nlp.stanford.edu/data/glove.42B.300d.zip',
'840B': 'http://nlp.stanford.edu/data/glove.840B.300d.zip',
'twitter.27B': 'http://nlp.stanford.edu/data/glove.twitter.27B.zip',
'6B': 'http://nlp.stanford.edu/data/glove.6B.zip',
}
def __init__(self, name='840B', dim=300, **kwargs):
url = self.url[name]
name = 'glove.{}.{}d.txt'.format(name, str(dim))
super(GloVe, self).__init__(name, url=url, **kwargs)
class FastText(Vectors):
url_base = 'https://dl.fbaipublicfiles.com/fasttext/vectors-wiki/wiki.{}.vec'
def __init__(self, language="en", **kwargs):
url = self.url_base.format(language)
name = os.path.basename(url)
super(FastText, self).__init__(name, url=url, **kwargs)
- 与之不同的是,我所要做的CharNGram是通过Ngram的原理来实现词向量的对应,源码是这样的,还重写了一个
__getitem__
函数:
class CharNGram(Vectors):
name = 'charNgram.txt'
url = ('http://www.logos.t.u-tokyo.ac.jp/~hassy/publications/arxiv2016jmt/'
'jmt_pre-trained_embeddings.tar.gz')
def __init__(self, **kwargs):
super(CharNGram, self).__init__(self.name, url=self.url, **kwargs)
def __getitem__(self, token):
vector = torch.Tensor(1, self.dim).zero_()
if token == "<unk>":
return self.unk_init(vector)
chars = ['#BEGIN#'] + list(token) + ['#END#']
num_vectors = 0
for n in [2, 3, 4]:
end = len(chars) - n + 1
grams = [chars[i:(i + n)] for i in range(end)]
for gram in grams:
gram_key = '{}gram-{}'.format(n, ''.join(gram))
if gram_key in self.stoi:
vector += self.vectors[self.stoi[gram_key]]
num_vectors += 1
if num_vectors > 0:
vector /= num_vectors
else:
vector = self.unk_init(vector)
return vector
- 上述
__getitem__
函数中,用到了ngram向量的累加平均法,即用一串字符进行ngram拆分:
chars = 'MyNameIs!'
for n in [2, 3, 4]:
end = len(chars) - n + 1
grams = [chars[i:(i + n)] for i in range(end)]
print('n=', n, ':',grams)
# 输出
n= 2 : ['My', 'yN', 'Na', 'am', 'me', 'eI', 'Is', 's!']
n= 3 : ['MyN', 'yNa', 'Nam', 'ame', 'meI', 'eIs', 'Is!']
n= 4 : ['MyNa', 'yNam', 'Name', 'ameI', 'meIs', 'eIs!']
- 然后在每个n遍历grams列表,寻找对应的向量,如果在
charNgram.txt
中(这里表现为在self.stoi
),就累加对应的向量值得到vector
,每加一个,就num_vectors+1
:
for gram in grams:
# 可能self.stoi中是以字典的形式存储向量的,所以这里的gram_key是每个gram,可以用这个键去查找对应的向量
gram_key = '{}gram-{}'.format(n, ''.join(gram))
if gram_key in self.stoi:
vector += self.vectors[self.stoi[gram_key]]
num_vectors += 1
- 若是
num_vectors
大于0,即有累加到向量,那么就用vector
除以num_vectors
得到一个平均值,返回该平均vector
。如果没有加到,代表该词在词向量表中啥也没有,返回全0的张量。
if num_vectors > 0:
vector /= num_vectors
else:
vector = self.unk_init(vector)
return vector
华为串讲要提问的问题(2021.7.13更新)
- 在第四批算子分析文档(图1)中,我们的python层接口要在utils里写,但是数据算子开发指南(图2)这个文档中,python层接口说是在transform中写
- 我们需要编写的算子Op实现就是分析文档中所说的:直接继承 Vectors 基类,只需对 CharNGram 预训练文件名补充一些校验即可。是吗?
- 在charngram里,首先要对输入的文本进行一个切分处理,就是把单词切分成固定长度的字符,通过查找字符对应的向量,来组成这个词向量,那么这一块我应该在算子op、还是算子IR层还是算子接口层实现呢?
数据算子开发中有三层
-
算子Op实现,核心Computer函数,实现底层的计算逻辑;Name函数,返回算子标识(c++)【Tensor相关检查】
-
算子IR层,核心Build、ValidateParams、Name 方法,与算子 Op实现 产生关联(c++)【参数数值检查】
-
算子接口定义,核心是c++和python的接口定义文件,以及pybind交互文件【参数类型检查】
- c++ 接口层:有了中间表示后,我们可以定义 C++接口类令其返回中间表示 ExampleOperation,即可接入到 MindData 的 Pipeline 中执行。(c++)
- python 接口层:有了中间表示后,我们可以定义 Python 接口令其返回 C++的 ExampleOperation。(python)
- c++和python的交互:使用 Pybind11 来实现,即利用 Pybind11 将 ExampleOperation 导出 到 Python,再定义 Example 的 Python API 并使其返回中间表示 ExampleOperation,即可接入到 MindData 的 Pipeline 中执行。(c++)
torchtext中Vectors的python源码的分析
- 首先分析.pt文件,构造前50个向量的.pt文件
Vectors = CharNGram(max_vectors = 50)
2. 得到charNgram.txt_50.pt
的文件,为了知道.pt这是什么文件,对该文件进行加载:
a = torch.load('F:\code\HuaWei\.vector_cache\charNgram.txt_50.pt')
print('type(a):', type(a))
print('a:', a)
# 输出
type(a): <class 'tuple'>
a: (['1gram-e', '1gram-a', '1gram-t', '1gram-i', '1gram-n', '1gram-o', '1gram-r', '1gram-s', '1gram-h', '1gram-l', '1gram-d', '2gram-e#END#', '1gram-c', '2gram-s#END#', '1gram-u', '2gram-#BEGIN#t', '1gram-m', '2gram-he', '2gram-th', '2gram-n#END#', '2gram-#BEGIN#a', '2gram-d#END#', '1gram-f', '2gram-in', '1gram-p', '1gram-g', '2gram-er', '3gram-#BEGIN#th', '2gram-an', '3gram-he#END#', '3gram-the', '1gram-y', '1gram-w', '2gram-t#END#', '4gram-#BEGIN#the', '2gram-on', '1gram-b', '4gram-the#END#', '2gram-re', '2gram-#BEGIN#o', '1gram-,', '2gram-r#END#', '1gram-.', '2gram-,#END#', '2gram-#BEGIN#,', '3gram-#BEGIN#,#END#', '2gram-#BEGIN#i', '2gram-y#END#', '2gram-.#END#', '2gram-#BEGIN#.'], {'1gram-e': 0, '1gram-a': 1, '1gram-t': 2, '1gram-i': 3, '1gram-n': 4, '1gram-o': 5, '1gram-r': 6, '1gram-s': 7, '1gram-h': 8, '1gram-l': 9, '1gram-d': 10, '2gram-e#END#': 11, '1gram-c': 12, '2gram-s#END#': 13, '1gram-u': 14, '2gram-#BEGIN#t': 15, '1gram-m': 16, '2gram-he': 17, '2gram-th': 18, '2gram-n#END#': 19, '2gram-#BEGIN#a': 20, '2gram-d#END#': 21, '1gram-f': 22, '2gram-in': 23, '1gram-p': 24, '1gram-g': 25, '2gram-er': 26, '3gram-#BEGIN#th': 27, '2gram-an': 28, '3gram-he#END#': 29, '3gram-the': 30, '1gram-y': 31, '1gram-w': 32, '2gram-t#END#': 33, '4gram-#BEGIN#the': 34, '2gram-on': 35, '1gram-b': 36, '4gram-the#END#':
37, '2gram-re': 38, '2gram-#BEGIN#o': 39, '1gram-,': 40, '2gram-r#END#': 41, '1gram-.': 42, '2gram-,#END#': 43, '2gram-#BEGIN#,': 44, '3gram-#BEGIN#,#END#': 45, '2gram-#BEGIN#i': 46, '2gram-y#END#': 47, '2gram-.#END#': 48, '2gram-#BEGIN#.': 49}, tensor([[-0.6554, 0.5743, -0.7140, ..., 0.2847, -0.2165, 0.7952],
[-0.2890, -0.2256, 0.3239, ..., 0.2294, 0.8031, -0.4031],
[ 0.4084, 0.1759, -0.2969, ..., 1.0544, -0.1056, 0.4516],
...,
[-0.5275, 0.5633, -0.0967, ..., -0.4472, 0.6202, 1.2261],
[-1.1693, -4.5730, -0.2427, ..., -0.9647, 1.5259, 1.0756],
[ 0.2617, -1.5953, 0.3065, ..., -0.2694, -0.7443, -2.4474]]), 100)
发现输出是个大小为4的元组,分别打印看看:
print('type(a[0]):', type(a[0]))
print('a[0]', a[0])
print('type(a[1]):', type(a[1]))
print('a[1]:', a[1])
print('type(a[2]):', type(a[2]))
print('a[2]:', a[2])
print('type(a[3]):', type(a[3]))
print('a[3]:', a[2])
# 输出
type(a[0]): <class 'list'> # 第一个itos存储字符(词)列表
a[0] ['1gram-e', '1gram-a', '1gram-t', '1gram-i', '1gram-n', '1gram-o', '1gram-r', '1gram-s', '1gram-h', '1gram-l', '1gram-d', '2gram-e#END#', '1gram-c', '2gram-s#END#', '1gram-u', '2gram-#BEGIN#t', '1gram-m', '2gram-he', '2gram-th', '2gram-n#END#', '2gram-#BEGIN#a', '2gram-d#END#', '1gram-f', '2gram-in', '1gram-p', '1gram-g', '2gram-er', '3gram-#BEGIN#th', '2gram-an', '3gram-he#END#', '3gram-the', '1gram-y', '1gram-w', '2gram-t#END#', '4gram-#BEGIN#the', '2gram-on', '1gram-b', '4gram-the#END#', '2gram-re', '2gram-#BEGIN#o', '1gram-,', '2gram-r#END#', '1gram-.', '2gram-,#END#', '2gram-#BEGIN#,', '3gram-#BEGIN#,#END#', '2gram-#BEGIN#i', '2gram-y#END#', '2gram-.#END#', '2gram-#BEGIN#.']
type(a[1]): <class 'dict'># 第二个stoi存储字典,键为字符(词),值为字符在预训练文件中的位置
a[1]: {'1gram-e': 0, '1gram-a': 1, '1gram-t': 2, '1gram-i': 3, '1gram-n': 4, '1gram-o': 5, '1gram-r': 6, '1gram-s': 7, '1gram-h': 8, '1gram-l': 9, '1gram-d': 10, '2gram-e#END#': 11, '1gram-c': 12, '2gram-s#END#': 13, '1gram-u': 14, '2gram-#BEGIN#t': 15, '1gram-m': 16, '2gram-he': 17, '2gram-th': 18, '2gram-n#END#': 19, '2gram-#BEGIN#a': 20, '2gram-d#END#': 21, '1gram-f': 22, '2gram-in': 23, '1gram-p': 24, '1gram-g': 25, '2gram-er': 26, '3gram-#BEGIN#th': 27, '2gram-an': 28, '3gram-he#END#': 29, '3gram-the': 30, '1gram-y': 31, '1gram-w': 32, '2gram-t#END#': 33, '4gram-#BEGIN#the': 34, '2gram-on': 35, '1gram-b': 36, '4gram-the#END#': 37, '2gram-re': 38, '2gram-#BEGIN#o': 39, '1gram-,': 40, '2gram-r#END#': 41, '1gram-.': 42, '2gram-,#END#': 43, '2gram-#BEGIN#,': 44, '3gram-#BEGIN#,#END#': 45, '2gram-#BEGIN#i': 46, '2gram-y#END#': 47, '2gram-.#END#': 48, '2gram-#BEGIN#.': 49}
type(a[2]): <class 'torch.Tensor'># 第三个vectors为50*100的张量,对应前50个字符(词)的张量
a[2]: tensor([[-0.6554, 0.5743, -0.7140, ..., 0.2847, -0.2165, 0.7952],
[-0.2890, -0.2256, 0.3239, ..., 0.2294, 0.8031, -0.4031],
[ 0.4084, 0.1759, -0.2969, ..., 1.0544, -0.1056, 0.4516],
...,
[-0.5275, 0.5633, -0.0967, ..., -0.4472, 0.6202, 1.2261],
[-1.1693, -4.5730, -0.2427, ..., -0.9647, 1.5259, 1.0756],
[ 0.2617, -1.5953, 0.3065, ..., -0.2694, -0.7443, -2.4474]])
type(a[3]): <class 'int'># 第四个dim为整数,代表字符(词)向量的纬度
a[3]: 100
- 分析Vectors的方法
def __getitem__(self, token):
if token in self.stoi:
return self.vectors[self.stoi[token]]
else:
return self.unk_init(torch.Tensor(self.dim))
- 这个相当于从stoi这个字典中查找值,token为输入的字符,作为键;值为该字符所在的位置,返回位置作为vectors的索引,查找到对应的向量并返回。
def cache(self, name, cache, url=None, max_vectors=None):
……
self.itos, self.stoi, self.vectors, self.dim = torch.load(path_pt)
- cache是加载预训练文件。如果.pt文件不在,那么先构建.pt文件;如果在,那就将其读取,如上。
def get_vecs_by_tokens(self, tokens, lower_case_backup=False):
- 这个传入参数,tokens是字符(词),通过该方法去查找向量;lower_case_backup为是否查找小写词,默认False为否。
华为串讲要提问的问题(2021.7.13晚更新)
-
在第四批算子分析文档(图1)中,我们的python层接口要在utils里写,但是数据算子开发指南(图2)这个文档中,python层接口说是在transform中写。应该遵循哪个文档呢?
图1
图2 -
python接口同样是调用底层的c++算子Op实现来运行的吗?还是说python在接口层也实现了一遍算法?torch的底层是c/c++
看错类型了
我们的应该是数据增强算子里的数据集加载算子!
。。。也不是数据集加载算子。。。
啥也不是,不包括在增强算子内,不按照开发指南上写。
更新流程
- 对于Vectors基类,应该是需要编写算子Op实现、算子IR层和接口层;因为其他的都要继承vectors基类,调用它里面的方法进行实现。
- 对于我的CharNgram,应该只需要编写c++接口、python接口和pybind绑定(不一定);而编写接口需要继承vectors类。
7.22更新流程
vectors应该只需要这样写:
- python接口
- c++接口+Op实现
- pybind11的实现将python和c++连接
7.27更新
我们把问题搞复杂了,原本参考的pytorch的实现,有cache、有多线程等等,但是我们都不用,我们用mindspore这一套,参考vocab就可以。那么只要能单线程buildFromFile,能Lookup即可。
然后ut测试这块,我们只需要测试接口即可,不用测试内部功能。
-
测试能否正确初始化并查找
-
测试能否正确查找词向量、查找小写、查找词列表
- 测试空的预训练词向量文件
- 自定义几行的token–>vectors的test_vectors.txt文件
- 默认值不变,初始化test_vectors.txt文件,查词与词列表。词列表中有不在文件中的词
- 自定义unk,max_vectors不变,查词列表,词列表中有不在文件中的词
- 自定义unk与max_vectors,查词列表,词列表中有不在max_vectors与不在文件中的词
- 采用自定义预训练词向量文件,查找词的小写形式
8.2更新
那么现在基本写完了,更新一下文件。
vectorsop是底层的实现,包括
class VectorsOp{
private:
TensorPtr unknown_init;
int rows = 0, dim = 0;
public:
StrMapTensorPtr stovec;
explicit VectorsOp(const std::string &file_path, std::shared_ptr<mindspore::MSTensor> unknown_init = nullptr,
int max_vectors = INT_MAX);
~VectorsOp() = default;
TensorPtr Lookup(const std::vector<std::string> tokens);
static Status BuildFromFile(const std::string &file_path, int max_vectors, StrMapTensorPtr &stovec, const std::string &delimiter_str, int *dim_for_zero);
};
构造函数VectorsOp
、BuildFromFile
和Lookup
三个主要函数
VectorsOp
构造函数:- 首先调用BuildFromFile来加载预训练文件。
- 其次处理unknown_init,unknown_init是一个MSTensor,如果没有传入自定义的unknown_init,那么就默认构造一个全零的Tensor; 如果传入了自定义的unknown_init,那么就用这个来构造Tensor。
mindspore在c++中暴露在外部的是MSTensor类型,在内部是Tensor类型。所以外部传参只能是MSTensor。
VectorsOp::VectorsOp(const std::string &file_path, std::shared_ptr<mindspore::MSTensor> unknown_init, int max_vectors) {
VectorsOp::BuildFromFile(file_path, max_vectors, this->stovec, "\n", &(this->dim));
if (unknown_init != nullptr){
TensorPtr init_temp;
Tensor::CreateFromMSTensor(*unknown_init, &init_temp);
this->unknown_init = init_temp;
}
else {
std::vector<double> vec_temp(0, this->dim);
Tensor::CreateFromVector(vec_temp, &(this->unknown_init));
}
}
BuildFromFile
函数:
Status VectorsOp::BuildFromFile(const std::string &file_path, int max_vectors, StrMapTensorPtr &stovec,
const std::string &delimiter_str, int *dim_for_zero) {
入参:
- file_path:预训练文件路径
- max_vectors:读取的最大预训练词向量
- stovec:string—>vector的映射(这里其实是string—>Tensor指针的映射)
- delimiter_str:分隔符,用来处理预训练文件中的
'\n'
和' '
,来读取token和对应的vector - dim_for_zero:预训练文件词向量的维度
首先是推断一些信息:
std::tie(real_lines_counts, header_lines_counts, vec_dim) = InferShape(file_path, max_vectors, delimiter);
- real_lines_counts:用户自定义的max_vectors数量
- header_lines_counts:预训练文件中有多少行
- vec_dim:每个Tensor的维度
得到每个信息之后
Lookup
函数:
TensorPtr VectorsOp::Lookup(const std::vector<std::string> tokens)
因为判断是否查找大小写是在接口中处理,这里只需要一个入参即要查找的token的vector,即tokens。返回一个TensorPtr。
左边是c++的接口部分实现
- 先实现Vectors接口,继承VectorsOp,然后调用VectorsOp的构造函数和Lookup的进行处理即可。
- 然后实现其他三个子算子,三个算子继承Vectors,然后调用Vectors的构造函数和Lookup进行处理即可。
- 最后写一个测试文件,测试接口的正确性。
右边是python的接口部分实现 【有误】
- 首先用pybind将c++的底层和python接口做一个连接。
PYBIND_REGISTER(VectorsOp, 0, ([](const py::module *m) {
(void)py::class_<VectorsOp, std::shared_ptr<VectorsOp>>(*m, "Vectors")
.def(py::init<const std::string, std::shared_ptr<mindspore::MSTensor>, int>())
.def("lookup", &VectorsOp::Lookup);
}));
- 然后和c++接口步骤类似,即实现Vectors,继承即可。之后进行测试
右边是python的接口部分实现
- 首先用pybind将c++的底层和python接口做一个连接。
PYBIND_REGISTER(VectorsOp, 0, ([](const py::module *m) {
(void)py::class_<VectorsOp, std::shared_ptr<VectorsOp>>(*m, "VectorsOp")
.def(py::init<>())
.def_static("make_vector",
[](const std::string &file_path, int max_v) {
int max_vectors = (max_v == -1)? INT_MAX : max_v;
return VectorsOp::BuildFromFilePy(file_path, max_vectors, " ");
})
.def_static("lookup",
[](std::unordered_map<std::string, std::vector<double>> dict,
std::vector<std::string> tokens, py::array_t<double> ar, int dim) {
std::vector<double> unknown_init;
if (ar.size() == 0) {
std::vector<double> temp(dim, 0);
unknown_init = temp;
} else {
py::buffer_info bf = ar.request();
double* ptr = (double*)bf.ptr;
for (int i=0;i<bf.shape[0];++i) unknown_init.push_back(ptr[i]);
}
return VectorsOp::LookupPy(dict, tokens, unknown_init);
});
}));
因为init函数直接传参貌似不容易,所以仿照vocab的方式,重新写了init的方式,就是用make_vector
的方式间接去调用一个BuildFromFile函数。同时在c++层也需要编写对应的BuildFromFilePy函数用来对应c++和python的类型。
- 下面给出Vectors基类的python接口实现:
class Vectors(cde.VectorsOp):
"""
Vectors object that is used to lookup tokens.
It contains a map that maps each token(str)/tokens(list(str)) to a tensor(Tensor).
Args:
【?缺参数说明】
"""
def __init__(self, file_path, unknown_init=None, max_vectors=None):
unknown_ = mindspore.Tensor([]) if unknown_init is None else unknown_init
self.unknown_init = unknown_() if hasattr(unknown_, '__call__') else unknown_
self.max_vectors = -1 if max_vectors is None else max_vectors
self.tensor_dict = super().make_vector(file_path, self.max_vectors)
self.dim = len(list(self.tensor_dict.values())[0])
def lookup(self, tokens, lower_case_backup=False):
if (not isinstance(tokens, list)) and (not isinstance(tokens, str)):
raise TypeError('tokens should be a str or a list(str)')
elif tokens is str:
tokens = tokens.split()
else:
for i in tokens:
if type(i) is not str:
raise TypeError('tokens should be a str or a list(str)')
if lower_case_backup:
tokens = [token.lower() for token in tokens]
array = super().lookup(self.tensor_dict, tokens, self.unknown_init.asnumpy(), self.dim)
tensor = mindspore.Tensor(array).reshape(len(tokens), self.dim)
return tensor
- 其他GloVe和FastText算子的实现简单,就是继承直接去查找。但我想象中没那么简单。下面给出CharNGram的c++接口实现,与python接口实现对比:
c++实现
CharNGram::CharNGram(const std::string &file_path, std::shared_ptr<mindspore::MSTensor> unknown_init,
int max_vectors)
:Vectors(file_path, unknown_init, max_vectors){}
mindspore::MSTensor CharNGram::Lookup(std::vector<std::string>&tokens, bool lower_case_backup){
std::shared_ptr<VectorsOp> vectorsop = this->GetOp();
// 首先就转换大小写
if(lower_case_backup) {
for (auto iter = tokens.begin(); iter != tokens.end(); iter++) {
transform(iter->begin(), iter->end(), iter->begin(), ::tolower);
}
}
std::vector<double> res_vec; // 最终用来构造二维MSTensor的vector
std::shared_ptr<mindspore::MSTensor> mst = vectorsop->GetUnknownInit(); // 获取unknown_init的MSTensor
// 遍历每一个token
for (auto t : tokens) {
std::vector<std::string> chars;
chars.push_back("#BEGIN#");
for (int i = 0; i < t.length(); i++) {
std::string s;
s.push_back(t[i]); // 将char类型的字母转换为string类型
chars.push_back(s);
}
chars.push_back("#END#");
// 经过上述处理,chars中存储着单个字母 如the---> vector<string> chars={#BEGIN#, t, h, e, #END#}
int len = chars.size();
int num_vectors = 0; // 记录能查到的字符片,后续求平均值
std::vector<double> sum_value(vectorsop->getDim(), 0); // 初始化一个全0的vector方便做加法
std::vector<double> res_value; // 以vector存储每个字符片的lookup结果
// 依次切片,切2、3、4长度的字符片
int slice_len[3] = { 2, 3, 4 };
for (int i = 0; i < 3; i++) {
int end = len - slice_len[i] + 1;
// 该层for循环会将一个字符进行切片并查找。如the,当切2长度的字符时,得到#BEGIN#t, th, he, e#END#
for (int pos = 0; pos < end; pos++) {
std::vector<std::string>::const_iterator first = chars.begin() + pos;
std::vector<std::string>::const_iterator second = first + slice_len[i];
std::vector<std::string> gram_vec;
gram_vec.assign(first, second); // 得到字符片
std::string gram = "";
for (auto c : gram_vec)
gram += c;
std::string gram_key = std::to_string(slice_len[i]) + "gram-" + gram; // 进行字符的拼接,如th ---> 2gram-th
mindspore::MSTensor sum_mstensor = Vectors::Lookup(gram_key); // 调用lookup
std::shared_ptr<mindspore::MSTensor> sum_mstensor_ptr = std::make_shared<mindspore::MSTensor>(sum_mstensor);
if(!compare(mst, sum_mstensor_ptr)){ // 判断和mst不相等的时候,才进行累加
sum_value = this->add(sum_value, sum_mstensor_ptr);
num_vectors++; // 累加的同时记录累加了几次
}
}
}
if (num_vectors > 0) {
res_value = mean(sum_value, num_vectors); // 查到了就求平均值
} else {
res_value = this->transfrom(mst); // 没查到返回mst
}
res_vec.insert(res_vec.end(), res_value.begin(), res_value.end()); // 将查到的每个vector进行扩充
}
TensorPtr res_ptr;
std::vector<dsize_t> shape_ = {(dsize_t)tokens.size(), vectorsop->getDim()};
TensorShape shape(shape_);
Tensor::CreateFromVector(res_vec, shape, &res_ptr); // 根据res_vec构造二维mstensor
auto res = mindspore::MSTensor(std::make_shared<mindspore::dataset::DETensor>(res_ptr));
return res;
}
mindspore::MSTensor CharNGram::Lookup(std::string &token, bool lower_case_backup){
std::vector<std::string> tokens = {token};
return CharNGram::Lookup(tokens, lower_case_backup);
}
// 辅助函数
std::vector<double> CharNGram::add(std::vector<double> &a, std::shared_ptr<mindspore::MSTensor> b){
TensorPtr b_tensor;
Tensor::CreateFromMSTensor(*b, &b_tensor);
auto a_iter = a.begin();
auto b_iter = b_tensor->begin<double>();
std::vector<double> res;
double element = 0;
for(; a_iter != a.end(); a_iter++, b_iter++){
element = *a_iter + *b_iter;
res.push_back(element);
}
return res;
}
std::vector<double> CharNGram::mean(std::vector<double> &sum, int &num_vectors){
auto sum_iter = sum.begin();
std::vector<double> res;
double element = 0;
for(; sum_iter != sum.end(); sum_iter++){
element = *sum_iter / num_vectors;
res.push_back(element);
}
return res;
}
std::vector<double> CharNGram::transfrom(std::shared_ptr<mindspore::MSTensor> x) {
TensorPtr x_tensor;
Tensor::CreateFromMSTensor(*x, &x_tensor);
auto x_iter = x_tensor->begin<double>();
std::vector<double> res;
for(; x_iter != x_tensor->end<double>(); x_iter++){
res.push_back(*x_iter);
}
return res;
}
//Equal returns true
bool CharNGram::compare(std::shared_ptr<mindspore::MSTensor> a, std::shared_ptr<mindspore::MSTensor> b){
TensorPtr a_tensor;
TensorPtr b_tensor;
Tensor::CreateFromMSTensor(*a, &a_tensor);
Tensor::CreateFromMSTensor(*b, &b_tensor);
auto a_iter = a_tensor->begin<double>();
auto b_iter = b_tensor->begin<double>();
bool flag = true;
int count = 0, false_count = 0;
for(; a_iter != a_tensor->end<double>(); a_iter++, b_iter++){
count++;
if(*a_iter != *b_iter){
false_count++;
}
}
if(count == false_count){
flag = false;
}
return flag;
}
python实现:
class CharNGram(Vectors, cde.VectorsOp):
def __init__(self, file_path, unknown_init=None, max_vectors=None):
super(CharNGram, self).__init__(file_path, unknown_init=unknown_init,
max_vectors=max_vectors)
# 重写lookup
def lookup(self, tokens, lower_case_backup=False):
if (not isinstance(tokens, list)) and (not isinstance(tokens, str)):
raise TypeError('tokens should be a str or a list(str)')
elif tokens is str:
tokens = tokens.split()
else:
for i in tokens:
if type(i) is not str:
raise TypeError('tokens should be a str or a list(str)')
if lower_case_backup:
tokens = [token.lower() for token in tokens]
# 将Tensor类型的unknown_init转换为numpy的ndarray类型
unknown_array = (self.unknown_init).asnumpy()
vectors_list = [] # 存储每个token的张量
for token in tokens:
vector = np.zeros(self.dim, dtype = float)
chars = ['#BEGIN#'] + list(token) + ['#END#']
num_vectors = 0
for n in [2, 3, 4]:
end = len(chars) - n + 1
grams = [chars[i:(i + n)] for i in range(end)] # 得到字符切片列表
for gram in grams:
gram_key_str = '{}gram-{}'.format(n, ''.join(gram))
gram_key = gram_key_str.split() # 转换为列表
# 调用VectorsOp的lookup,得到一个列表
temp_list = super(Vectors, self).lookup(self.tensor_dict, gram_key, self.unknown_init.asnumpy(), self.dim)
temp_array = np.array(temp_list) # 转换为ndarray类型
if not np.array_equal(unknown_array, temp_array): # 如果和默认值不同,就进行加和
vector = vector + temp_array
num_vectors += 1
if num_vectors > 0:
vector = vector / num_vectors # 求平均值
else: # 没有查到该token
vector = unknown_array
vectors_list.append(vector)
vectors = Tensor(np.stack(vectors_list,axis=0)).reshape(len(tokens), 1 ,self.dim) # 转换为Tensor
return vectors
至于VectorsOp的内部逻辑,如果有空,或者需要进行写注释了,再补充。到此告一段落。
git push自己代码时用到git—全局设置用户名、密码、邮箱
git创建分支,提交代码详细流程(保姆级)