CharNgram从0到1实现

是华为的昇腾众智计划没错了,首先直接搜CharNgram是搜不到几乎任何东西的,因为这个语言模型叫Ngram模型, char是字符的意思,这是字符级的Ngram语言模型。
补充: CharNgram是用到了Ngram里对字符的处理思想,至于对每个字母的预训练词向量是如何训练出的,应该在文献中。

什么是Ngram语言模型(2021.7.11)

自然语言处理中N-Gram模型介绍

  • 基本思想:将文本里面的内容按照字节(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即可。

华为串讲要提问的问题(旧)
  1. 我们要做的只是对用户输入的文本进行一个预处理,进行分词等,然后加载训练好的模型,得到一个张量输出是吗?不需要我们自己训练模型是吗?
  2. 我们对分词的话,比如中文可以调用jieba分词库吗?或者英文别的分词函数吗?
  3. 我的这个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
  1. 上述__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!']
  1. 然后在每个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
  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. 在第四批算子分析文档(图1)中,我们的python层接口要在utils里写,但是数据算子开发指南(图2)这个文档中,python层接口说是在transform中写第四批算子分析文档
    在这里插入图片描述
  2. 我们需要编写的算子Op实现就是分析文档中所说的:直接继承 Vectors 基类,只需对 CharNGram 预训练文件名补充一些校验即可。是吗?
  3. 在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源码的分析
  1. 首先分析.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
  1. 分析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. 在第四批算子分析文档(图1)中,我们的python层接口要在utils里写,但是数据算子开发指南(图2)这个文档中,python层接口说是在transform中写。应该遵循哪个文档呢?第四批算子分析文档
    图1
    在这里插入图片描述
    图2

  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测试这块,我们只需要测试接口即可,不用测试内部功能。

  • 测试能否正确初始化并查找

  • 测试能否正确查找词向量、查找小写、查找词列表

    1. 测试空的预训练词向量文件
    2. 自定义几行的token–>vectors的test_vectors.txt文件
    3. 默认值不变,初始化test_vectors.txt文件,查词与词列表。词列表中有不在文件中的词
    4. 自定义unk,max_vectors不变,查词列表,词列表中有不在文件中的词
    5. 自定义unk与max_vectors,查词列表,词列表中有不在max_vectors与不在文件中的词
    6. 采用自定义预训练词向量文件,查找词的小写形式
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);
};

构造函数VectorsOpBuildFromFileLookup三个主要函数

  • 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创建分支,提交代码详细流程(保姆级)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值