Commit 816b3d52 authored by liucong's avatar liucong
Browse files

提交GPT2模型的python和C++代码示例

parent 6f5d13e8
Pipeline #296 canceled with stages
#! /bin/sh
############### Ubuntu ###############
# 参考:https://docs.opencv.org/3.4.11/d7/d9f/tutorial_linux_install.html
# apt-get install build-essential -y
# apt-get install cmake git libgtk2.0-dev pkg-config libavcodec-dev libavformat-dev libswscale-dev -y
# apt-get install python-dev python-numpy libtbb2 libtbb-dev libjpeg-dev libpng-dev libtiff-dev libjasper-dev libdc1394-22-dev -y # 处理图像所需的包,可选
############### CentOS ###############
yum install gcc gcc-c++ gtk2-devel gimp-devel gimp-devel-tools gimp-help-browser zlib-devel libtiff-devel libjpeg-devel libpng-devel gstreamer-devel libavc1394-devel libraw1394-devel libdc1394-devel jasper-devel jasper-utils swig python libtool nasm -y
\ No newline at end of file
############################ 在线安装依赖 ###############################
#cd ./3rdParty
#pip install rbuild-master.tar.gz
############################ 离线安装依赖 ###############################
# 安装依赖
cd ./3rdParty/rbuild_depend
pip install click-6.6-py2.py3-none-any.whl
pip install six-1.15.0-py2.py3-none-any.whl
pip install subprocess32-3.5.4.tar.gz
pip install cget-0.1.9.tar.gz
# 安装rbuild
cd ../
pip install rbuild-master.tar.gz
# 设置cmake的最低版本
cmake_minimum_required(VERSION 3.5)
# 设置项目名
project(MIGraphX_Samples)
# 设置编译器
set(CMAKE_CXX_COMPILER g++)
set(CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS} -std=c++17) # 2.2版本以上需要c++17
set(CMAKE_BUILD_TYPE release)
# 添加头文件路径
set(INCLUDE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/Src/
${CMAKE_CURRENT_SOURCE_DIR}/Src/Utility/
${CMAKE_CURRENT_SOURCE_DIR}/Src/NLP/GPT2/
$ENV{DTKROOT}/include/
${CMAKE_CURRENT_SOURCE_DIR}/depend/include/)
include_directories(${INCLUDE_PATH})
# 添加依赖库路径
set(LIBRARY_PATH ${CMAKE_CURRENT_SOURCE_DIR}/depend/lib64/
$ENV{DTKROOT}/lib/)
link_directories(${LIBRARY_PATH})
# 添加依赖库
set(LIBRARY opencv_core
opencv_imgproc
opencv_imgcodecs
opencv_dnn
migraphx_ref
migraphx
migraphx_c
migraphx_device
migraphx_gpu
migraphx_onnx)
link_libraries(${LIBRARY})
# 添加源文件
set(SOURCE_FILES ${CMAKE_CURRENT_SOURCE_DIR}/Src/main.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Src/Sample.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Src/NLP/GPT2/tokenization.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Src/NLP/GPT2/utf8proc.c
${CMAKE_CURRENT_SOURCE_DIR}/Src/NLP/GPT2/GPT2.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Src/Utility/CommonUtility.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Src/Utility/Filesystem.cpp)
# 添加可执行目标
add_executable(MIGraphX_Samples ${SOURCE_FILES})
# 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 |
本次采用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.文本编码
首先,根据提供的词汇表路径,通过cuBERT::FullTokenizer()函数加载词汇表,用于后续对输入文本的编码操作。其次,将词汇表中的内容依次保存到vector容器output中,用于数据后处理中的解码操作。
```c++
cuBERT::FullTokenizer tokenizer = cuBERT::FullTokenizer("../Resource/Models/NLP/GPT2/vocab_shici.txt");
std::ifstream infile;
std::string buf;
std::vector<std::string> output;
infile.open("../Resource/Models/NLP/GPT2/vocab_shici.txt");
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]结束标识符才结束推理。
<img src="../Images/GPT_02.png" style="zoom:70%;" align=middle>
具体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模型的推理过程,主要做如下处理:
1.reshape操作
2.执行推理
```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];
}
// 设置输入shape并执行reshape
std::vector<std::vector<std::size_t>> inputShapes;
inputShapes.push_back({1,input_id.size()});
std::unordered_map<std::string, std::vector<std::size_t>> inputShapeMap;
inputShapeMap[inputName] = inputShapes[0];
migraphx::reshape2(net,inputShapeMap);
// 输入数据
migraphx::parameter_map inputData;
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(); // 输出节点数据指针
...
}
```
1.reshape操作,因为GPT-2属于生成式语言模型,输入的shape一直在变化,所以在模型推理前需要reshape的操作。shape的大小根据输入数据的shape决定,之后执行reshape操作,从而使模型可以根据不同的shape进行推理。
2.执行推理,GPT-2模型的推理结果results是一个std::vector< migraphx::argument >类型,包含一个输出,所以result = results[0]。result中一共包含了input_id.size() * 22557个概率值,其中,input_id.size()代表输入数据的长度,22557代表了词汇表中词的数量。
## 数据后处理
得到模型推理结果后,还需要对数据做如下后处理:
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],便可以取出对应的词,即将数值型数据解码为文本数据,得到最终的答案。
\ No newline at end of file
# 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.reshape操作
2.循环推理
```Python
# 设置最大输入shape
maxInput={"input":[1,1024]}
# 模型推理
for _ in range(max_len):
# 执行reshape
inputShapes = [input_ids.shape[0], input_ids.shape[1]]
inputShapeMap={inputName:inputShapes}
migraphx.reshape2(model, inputShapeMap)
# 推理
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.reshape操作,因为GPT-2属于生成式模型,输入的shape一直在变化,所以在模型推理前需要进行reshape操作。首先,设置一个最大的输入shape,maxInput={"input":[1,1024]},限定模型输入shape的范围。其次,根据每次输入shape的具体大小,执行migraphx.reshape2()操作,从而使模型可以根据不同的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()函数将数值型数据转换为相应的文本数据,从而得到对应的答案。
import os
import numpy as np
from transformers import BertTokenizerFast
import migraphx
# 加载词汇表
print("INFO: Complete loading the vocabulary")
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]")
# 设置最大输入shape
maxInput={"input":[1,1024]}
# 加载模型
print("INFO: Parsing and compiling the model")
model = migraphx.parse_onnx("../../../Resource/Models/NLP/GPT2/GPT2_shici.onnx", map_input_dims=maxInput)
inputName=model.get_parameter_names()[0]
inputShape=model.get_parameter_shapes()[inputName].lens()
print("inputName:{0} \ninputShape:{1}".format(inputName,inputShape))
# 编译
model.compile(t=migraphx.get_target("gpu"), device_id=0)
print('开始和GPT2对诗,输入CTRL + Z以退出')
while True:
try:
history = []
text = input("user:")
text_ids = tokenizer.encode(text, add_special_tokens=False)
history.extend(text_ids)
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)
max_len = 50
for _ in range(max_len):
# 推理
result = model.run({inputName: migraphx.argument(input_ids)})
logits = [float(x) for x in result[0].tolist()]
# 对于[UNK]的概率设为无穷小,模型的预测结果不可能是[UNK]
logits[tokenizer.convert_tokens_to_ids('[UNK]')] = -float('Inf')
# 排序
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]
if next_token == tokenizer.convert_tokens_to_ids('[SEP]'): # 遇到[SEP]结束标志符,结束循环
break
history.append(next_token) # 结果存放在response列表中
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)
text = tokenizer.convert_ids_to_tokens(history)
print("chatbot:" + "".join(text))
except KeyboardInterrupt:
break
......@@ -6,43 +6,117 @@ GPT2模型:第二代生成式预训练模型(Generative Pre-Training2)。
## 模型结构
GPT2主要使用Transformer的Decoder模块为特征提取器,并对Transformer Decoder进行了一些改动,原本的Decoder包含了两个Multi-Head Attention结构,而GPT2只保留了Mask Multi-Head Attention。
## 推理
### 环境配置
## 构建安装
在光源可拉取推理的docker镜像,GPT2模型推理的镜像如下:
```python
docker pull image.sourcefind.cn:5000/dcu/admin/base/custom:ort_dcu_1.14.0_migraphx2.5.2_dtk22.10.1
docker pull image.sourcefind.cn:5000/dcu/admin/base/custom:ort1.14.0_migraphx3.0.0-dtk22.10.1
```
在光合开发者社区可下载MIGraphX安装包,python依赖安装:
### 安装Opencv依赖
```python
pip install -r requirement.txt
cd <path_to_migraphx_samples>
sh ./3rdParty/InstallOpenCVDependences.sh
```
### 修改CMakeLists.txt
- 如果使用ubuntu系统,需要修改CMakeLists.txt中依赖库路径:
将"${CMAKE_CURRENT_SOURCE_DIR}/depend/lib64/"修改为"${CMAKE_CURRENT_SOURCE_DIR}/depend/lib/"
- **MIGraphX2.3.0及以上版本需要c++17**
### 安装OpenCV并构建工程
```
rbuild build -d depend
```
### 设置环境变量
将依赖库依赖加入环境变量LD_LIBRARY_PATH,在~/.bashrc中添加如下语句:
**Centos**:
```
export LD_LIBRARY_PATH=<path_to_migraphx_samples>/depend/lib64/:$LD_LIBRARY_PATH
```
**Ubuntu**:
```
export LD_LIBRARY_PATH=<path_to_migraphx_samples>/depend/lib/:$LD_LIBRARY_PATH
```
然后执行:
```
source ~/.bashrc
```
本次采用GPT-2模型进行诗词生成任务,模型文件下载链接:https://pan.baidu.com/s/1KWeoUuakCZ5dualK69qCcw , 提取码:4pmh ,并将GPT2_shici.onnx模型文件保存在model文件夹下。
## 推理
本次采用GPT-2模型进行诗词生成任务,模型文件下载链接:https://pan.baidu.com/s/1KWeoUuakCZ5dualK69qCcw , 提取码:4pmh ,并将GPT2_shici.onnx模型文件保存在model文件夹下。下面介绍如何运行python代码和C++代码示例,具体推理代码解析,在Doc目录中有详细说明。
### python版本推理
### 运行示例
1.参考《MIGraphX教程》中的安装方法安装MIGraphX并设置好PYTHONPATH
2.安装依赖:
```python
# 进入migraphx samples工程根目录
cd <path_to_migraphx_samples>
我们提供了基于MIGraphX的推理脚本,版本依赖:
# 进入示例程序目录
cd Python/NLP/GPT2
- Migraphx(DCU版本) >= 2.5.2
# 安装依赖
pip install -r requirements.txt
```
gpt2.py是基于Migraphx的推理脚本,使用需安装好MIGraphX,由于GPT2模型是动态推理的,需要设置动态shape模式,再执行推理。使用方法
3.设置环境变量
```python
# 设置动态shape模式
export MIGRAPHX_DYNAMIC_SHAPE=1
```
4.在Python/NLP/GPT2目录下执行如下命令运行该示例程序:
# 执行推理
```python
python gpt2.py
```
推理结果为:
如下所示,采用交互式界面,通过输入开头诗词,GPT2模型可以生成后续的诗句。
<img src="./Doc/Images/GPT_03.png" style="zoom:80%;" align=middle>
### C++版本推理
切换到build目录中,执行如下命令:
```python
cd ./build/
./MIGraphX_Samples
```
根据提示选择运行GPT2模型的示例程序
```python
# 设置动态shape模式
export MIGRAPHX_DYNAMIC_SHAPE=1
# 运行示例
./MIGraphX_Samples 0
```
如下所示,采用交互式界面,通过输入开头诗词,GPT2模型可以推理出后续的诗句。
<img src="./Sample_picture.png" style="zoom:90%;" align=middle>
<img src="./Doc/Images/GPT_04.png" style="zoom:90%;" align=middle>
## 历史版本
......
<?xml version="1.0" encoding="GB2312"?>
<opencv_storage>
<!--GPT2-->
<GPT2>
<ModelPath>"../Resource/Models/NLP/GPT2/GPT2_shici.onnx"</ModelPath>
</GPT2>
</opencv_storage>
This diff is collapsed.
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment