Tutorial_Cpp.md 7.11 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# GPT

本示例主要通过使用MIGraphX C++ API对GPT2模型进行推理,包括预处理、模型推理以及数据后处理。

## 模型简介

GPT(Generative Pre-trained Transformer)系列模型以不断堆叠transformer中的decoder模块为特征提取器,提升训练语料的规模和质量、模型的参数量进行迭代更新。GPT-1主要通过在无标签的数据上学习一个通用的语言模型,再根据特定的任务进行微调处理有监督任务;GPT-2在GPT-1的模型结构上使用更多的模型参数和数据集,训练一个泛化能力更强的词向量模型。GPT-3更是采用海量的模型参数和数据集,训练了一个更加强大的语言模型。

| 模型  | 发布时间     | 参数量   | 预训练数据量 |
| ----- | ------------ | -------- | ------------ |
| GPT-1 | 2018 年 6 月 | 1.17 亿  | 约 5GB       |
| GPT-2 | 2019 年 2 月 | 15 亿    | 40GB         |
| GPT-3 | 2020 年 5 月 | 1,750 亿 | 45TB         |

liucong's avatar
liucong committed
15
本次采用GPT-2模型进行诗词生成任务,模型文件下载链接:https://pan.baidu.com/s/1KWeoUuakCZ5dualK69qCcw, 提取码:4pmh。将GPT2_shici.onnx模型文件保存在Resource/文件夹下。整体模型结构如下图所示,也可以通过netron工具:https://netron.app/ 查看GPT-2的模型结构。
16

liucong's avatar
liucong committed
17
<img src="./Images/GPT_01.png" style="zoom:100%;" align=middle>
18
19
20
21
22
23
24
25
26
27
28
29

## 预处理

在将文本输入到模型之前,需要做如下预处理:

1.加载词汇表

2.文本编码

首先,根据提供的词汇表路径,通过cuBERT::FullTokenizer()函数加载词汇表,用于后续对输入文本的编码操作。其次,将词汇表中的内容依次保存到vector容器output中,用于数据后处理中的解码操作。

```c++
liucong's avatar
liucong committed
30
cuBERT::FullTokenizer tokenizer = cuBERT::FullTokenizer("../Resource/vocab_shici.txt");
31
32
33
std::ifstream infile;
std::string buf;
std::vector<std::string> output;
liucong's avatar
liucong committed
34
infile.open("../Resource/vocab_shici.txt");
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
while (std::getline(infile,buf))
{
    output.push_back(buf);
}
```

文本编码的实现方法主要封装在GPT2::Preprocessin()函数中,通过输入问题question,进行数据重构,在输入序列中的起始位置处加入起始标志符[CLS],之后拼接问题question的编码信息,从而完成数据预处理过程。

```c++
ErrorCode GPT2::Preprocessing(cuBERT::FullTokenizer tokenizer,
                             char *question,
                             std::vector<long unsigned int> &input_id)
{
    // 对问题进行分词操作
    int max_seq_length =1024;
    std::vector<std::string> tokens_question;
    tokens_question.reserve(max_seq_length);
    tokenizer.tokenize(question, &tokens_question, max_seq_length);

    // 将文本数据转换为数值型数据
    input_id.push_back(tokenizer.convert_token_to_id("[CLS]"));
    for (int i=0;i<tokens_question.size();++i) 
    {
        input_id.push_back(tokenizer.convert_token_to_id(tokens_question[i]));
    }
    return SUCCESS;
}
```

## 推理

对于GPT-2这种生成式语言模型来说,模型不是仅执行一次推理就结束,而是需要执行多次推理,才能得到最终的答案。如下图所示,GPT-2模型每次推理仅生成一个词,通过将生成的词与输入数据拼接,输入到模型中继续下一次的推理,直到循环结果或者生成[SEP]结束标识符才结束推理。

liucong's avatar
liucong committed
68
<img src="./Images/GPT_02.png" style="zoom:70%;" align=middle>
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86

具体GPT-2模型的推理,如下代码所示。首先,通过gpt2.Inference()函数实现模型的具体推理细节,推理结果保存在outputs中。其次,对每次推理结果进行判断,当判断为[SEP]结束标志符时,结束循环完成推理,否则就将推理结果outputs加入到输入数据input_id中,继续下一次的模型推理。

```c++
// 推理
for(int i=0;i<50;++i)
{
   long unsigned int outputs = gpt2.Inference(input_id);
   if(outputs == 102)      // 当outputs等于102时,即[SEP]结束标志符,退出循环。
   {
       break;
   }
   input_id.push_back(outputs);
}
```

在GPT2::Inference()函数具体实现了GPT-2模型的推理过程,主要做如下处理:

liucong's avatar
liucong committed
87
1.设置输入shape并执行推理
88
89
90
91
92
93
94
95
96
97
98
99

```c++
long unsigned int GPT2::Inference(const std::vector<long unsigned int> &input_id)
{

    // 保存预处理后的数据
    long unsigned int input[1][input_id.size()];
    for (int j=0;j<input_id.size();++j)
    {
        input[0][j] = input_id[j];
    }

liucong's avatar
liucong committed
100
    // 输入shape
101
102
103
104
    std::vector<std::vector<std::size_t>> inputShapes;
    inputShapes.push_back({1,input_id.size()});

    // 输入数据
liucong's avatar
liucong committed
105
    std::unordered_map<std::string, migraphx::argument> inputData;
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
    inputData[inputName]=migraphx::argument{migraphx::shape(inputShape.type(),inputShapes[0]),(long unsigned int*)input};

    // 推理
    std::vector<migraphx::argument> results = net.eval(inputData);

    // 获取输出节点的属性
    migraphx::argument result = results[0];
    migraphx::shape outputShape = result.get_shape();       // 输出节点的shape
    int numberOfOutput=outputShape.elements();              // 输出节点元素的个数
    float *data = (float *)result.data();                   // 输出节点数据指针

    ...
}
```

liucong's avatar
liucong committed
121
1.执行推理,GPT-2模型的推理结果results是一个std::vector< migraphx::argument >类型,包含一个输出,所以result = results[0]。result中一共包含了input_id.size() * 22557个概率值,其中,input_id.size()代表输入数据的长度,22557代表了词汇表中词的数量。
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162

## 数据后处理

得到模型推理结果后,还需要对数据做如下后处理:

1.排序

2.解码

```c++
long unsigned int GPT2::Inference(const std::vector<long unsigned int> &input)
{
    ...

    // 保存模型推理出的概率值
    long unsigned int n = 0;
    std::vector<Predictions> resultsOfPredictions(22557);
    for(int i=(input.size()-1)*22557; i<input.size()*22557; ++i)
    {
        resultsOfPredictions[n].index = n;
        resultsOfPredictions[n].predictionvalue = data[i];
        ++n;
    }

    // 对概率值进行排序操作
    std::sort(resultsOfPredictions.begin(), resultsOfPredictions.end(), CompareM);

    return resultsOfPredictions[0].index;
}
```

1.排序。首先,保存数据,因为每次推理都会对每个token生成词汇表中下一个词的概率,例如输入shape为(1,4),词汇表长度为22557,则一共生成4*22557个概率,但是不需要获得全部的概率,只需要保存最后一个token生成的概率。通过设计了一个结构体Predictions,其中包含了两个成员变量,索引index和概率值predictionvalue,将推理结果保存在结构体中,其次,采用std::sort()函数进行排序操作,取概率值最大的作为预测结果,最后,返回概率值最大的索引值。

```C++
for(int i=0;i<score.size();++i)
    {
        result.push_back(output[score[i]]);
    }
```

2.解码。score是一个vector容器,存储了推理结果的索引值,另外,在数据预处理的过程中,已经将词汇表中的词依次存储在了vector容器output中。因此,通过给output提供下标score[i],便可以取出对应的词,即将数值型数据解码为文本数据,得到最终的答案。