# GPT

本示例主要通过使用MIGraphX Python 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         |

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

<img src="../Images/GPT_01.png" style="zoom:100%;" align=middle>

## 预处理

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

1.加载词汇表，根据提供的路径加载词汇表

2.文本编码，根据词汇表对输入的文本进行编码

在数据预处理的过程中，首先加载词汇表，根据提供的词汇表路径，通过transformers库中的BertTokenizerFast函数实现词汇表的加载。完成词汇表的加载后，就可以正常对输入的文本进行编码处理，从而将文本数据转换为数值型数据。

```python
# 加载词汇表
vocab_file = os.path.join('../../../Resource/Models/NLP/GPT2', 'vocab_shici.txt')
tokenizer = BertTokenizerFast(vocab_file, sep_token="[SEP]", pad_token="[PAD]", cls_token="[CLS]")
```

完成词汇表的加载后，就可以进行文本编码。首先，输入一段文本数据，通过tokenizer.encoder()将文本数据编码为数值型数据。其次，进行数据重构，创建了一个input_ids列表，开头加入[CLS]起始标志符，并将数值型数据拼接到后面。最后，将input_ids列表中的数据都转换为np.int64类型，并将一维数据扩展了二维数据，完成数据的预处理过程。

```python
# 对输入文本进行编码
text = input("user:")
text_ids = tokenizer.encode(text, add_special_tokens=False)   

# 数据重构
input_ids = [tokenizer.cls_token_id]                          
input_ids.extend(text_ids)

input_ids = np.array(input_ids, dtype=np.int64)
input_ids = np.expand_dims(input_ids, axis=0) 
```

## 推理

完成数据预处理后，就可以执行模型推理。推理过程主要做如下处理：

1.设置最大输入shape

2.循环推理

```Python
# 设置最大输入shape
maxInput={"input":[1,1000]}

# 模型推理
for _ in range(max_len):
                
      # 推理
      result = model.run({inputName: migraphx.argument(input_ids)})
      logits = [float(x) for x in result[0].tolist()]
    
      ...
        
      # 当推理得到[SEP]结束标志符，则结束for循环停止生成
      if next_token == tokenizer.convert_tokens_to_ids('[SEP]'):    
           break
        
      # 将推理结果next_token和input_ids进行拼接
      next_token = np.array(next_token, dtype=np.int64)
      input_ids = np.append(input_ids, next_token)
      input_ids = np.expand_dims(input_ids, axis=0)
    
```

1.设置最大输入shape，因为GPT-2属于生成式模型，输入的shape一直在变化，所以在模型推理前设置一个最大的输入shape，maxInput={"input":[1,1000]}，限定模型输入shape的范围。

2.循环推理，GPT-2模型不像其他模型一样只需要执行一次推理，而是需要循环执行多次推理才能完成。首先，模型推理限定在for循环中，将输入数据input_ids，输入到model.run({...})中执行推理，生成一个token的id。其次，将推理结果拼接到输入数据input_ids中，执行下一次循环。最后，当循环结束或者生成的词为[SEP]结束标志符时，完成GPT-2模型的整体推理。如下图所示，为GPT-2模型的一次完整推理过程。

<img src="../Images/GPT_02.png" style="zoom:70%;" align=middle>

## 数据后处理

获得模型推理结果后，并不能直接作为答案输出，还需要进行数据后处理操作，才能得到最终的结果。具体主要做如下后处理：

1.排序

2.解码

```Python
for _ in range(max_len):  
      ...
            
      # 排序
      score = []
      for index in range((input_ids.shape[1]-1)*22557, input_ids.shape[1]*22557):
          score.append(logits[index])
      index_and_score = sorted(enumerate(score), key=lambda x: x[1], reverse=True)
            
      # 取出概率值最大的作为预测结果
      next_token = index_and_score[0][0]
      
      ...
        
      history.append(next_token)
    
# 解码
text = tokenizer.convert_ids_to_tokens(history)                   
print("chatbot:" + "".join(text))
```

1.排序，每次推理都会对每个token生成词汇表中下一个词的概率，例如输入shape为(1,4)，词汇表长度为22557，则一共生成4*22557个概率，但是不需要获得全部的概率，只需要得到最后一个token生成的概率。得到相应的概率后，采用sorted()函数进行排序操作，取概率值最大的作为预测结果。

2.解码，因为history列表中存放的推理结果都是对应token的id，还需要进行解码将对应的id转换文本。解码主要采用BertTokenizerFast中的convert_ids_to_tokens()函数将数值型数据转换为相应的文本数据，从而得到对应的答案。

