# Bert 本示例主要通过Bert模型说明如何使用MIGraphX C++ API进行自然语言处理模型的推理,包括数据准备、预处理、模型推理以及数据后处理。 ## 模型简介 自然语言处理(Natural Language Processing,NLP )是能够实现人与计算机之间用自然语言进行有效沟通的理论和方法,是计算机科学领域与人工智能领域中的一个重要方向。本次采用经典的Bert模型完成问题回答任务,模型和分词文件下载链接:https://pan.baidu.com/s/1yc30IzM4ocOpTpfFuUMR0w, 提取码:8f1a, 下载bertsquad-10.onnx文件和uncased_L-12_H-768_A-12分词文件保存在Resource/文件夹下。整体模型结构如下图所示,也可以通过netron工具:https://netron.app/ 查看Bert模型结构。 问题回答任务是指输入一段上下文文本的描述和一个问题,模型从给定的文本中预测出答案。例如: ``` 1.描述:My name is Li Ming 2.问题:What's your name? 3.答案:Li Ming ``` ## 数据准备 在自然语言处理领域中,首先需要准备文本数据,如下所示,通常需要提供问题(question)和上下文文本(context),自己可以根据需求准备相应的问题和上下文文本作为输入数据,进行模型推理。 ```json { "context": "ROCm is the first open-source exascale-class platform for accelerated computing that’s also programming-language independent. It brings a philosophy of choice, minimalism and modular software development to GPU computing. You are free to choose or even develop tools and a language run time for your application. ROCm is built for scale, it supports multi-GPU computing and has a rich system run time with the critical features that large-scale application, compiler and language-run-time development requires. Since the ROCm ecosystem is comprised of open technologies: frameworks (Tensorflow / PyTorch), libraries (MIOpen / Blas / RCCL), programming model (HIP), inter-connect (OCD) and up streamed Linux® Kernel support – the platform is continually optimized for performance and extensibility.", "question": "What is ROCm?" } ``` ## 预处理 提供的问题和上下文文本并不能直接输入到模型中执行推理,需要对数据做如下预处理: 1.滑动窗口操作,当问题和上下文文本的字符超过256时,执行该操作,否则不执行。 2.数据拼接,将原始问题和上下文文本拼接成一个序列,作为模型的输入数据。 ### 滑动窗口操作 对于问题回答型任务,关键的是如何构建输入数据。通过对上下文文本和问题的长度做判断,如果上下文文本加问题不超过256个字符时,直接进行后续的数据拼接操作,否则先进行滑动窗口操作构建输入序列,再进行后续的数据拼接。 如下图所示,为滑动窗口的具体操作: 从图中可以看出,通过指定窗口大小为256,进行滑动窗口处理可以将上下文文本分成多个子文本,用于后续的数据拼接。 具体滑动窗口的过程由如下代码实现: ```c++ ErrorCode Bert::Preprocessing(...) { ... // 当上下文文本加问题文本的长度大于规定的最大长度,采用滑动窗口操作 if(tokens_text.size() + tokens_question.size() > max_seq_length - 5) { int windows_len = max_seq_length - 5 -tokens_question.size(); std::vector tokens_text_window(windows_len); std::vector> tokens_text_windows; // 规定起始位置,通过滑动窗口操作将子文本存储在tokens_text_window中 int start_offset = 0; int position = 0; int n; while (start_offset < tokens_text.size()) { n = 0; if(start_offset + windows_len > tokens_text.size()) { for(int i = start_offset; i < tokens_text.size(); ++i) { tokens_text_window[n] = tokens_text[i]; ++n; } } else { for(int i = start_offset; i < start_offset + windows_len; ++i) { tokens_text_window[n] = tokens_text[i]; ++n; } } tokens_text_windows.push_back(tokens_text_window); start_offset += 256; ++position; } } ... } ``` ### 数据拼接 当获得指定的问题和上下文文本时,对问题和上下文文本进行拼接操作,具体过程如下图所示: 从图中可以看出,是将问题和上下文文本拼接成一个序列,问题和上下文文本用[SEP]符号隔开,完成数据拼接后再输入到模型中进行特征提取。其中,“[CLS]”是一个分类标志,表示后面的内容属于问题文本,“[SEP]”字符是一个分割标志,用来将问题和上下文文本分开。 具体过程由如下代码实现: ```c++ ErrorCode Bert::Preprocessing(...) { ... for(int i=0; i < position; ++i) { // 将问题和上下文文本进行拼接处理 input_id[0] = tokenizer.convert_token_to_id("[CLS]"); segment_id[0] = 0; input_id[1] = tokenizer.convert_token_to_id("[CLS]"); segment_id[1] = 0; for (int j = 0; j < tokens_question.size(); ++j) { input_id[j + 2] = tokenizer.convert_token_to_id(tokens_question[j]); segment_id[j + 2] = 0; } input_id[tokens_question.size() + 2] = tokenizer.convert_token_to_id("[SEP]"); segment_id[tokens_question.size() + 2] = 0; input_id[tokens_question.size() + 3] = tokenizer.convert_token_to_id("[SEP]"); segment_id[tokens_question.size() + 3] = 0; for (int j = 0; j < tokens_question.size(); ++j) { input_id[j + tokens_text_windows[i].size() + 4] = tokenizer.convert_token_to_id(tokens_text_windows[i][j]); segment_id[j + tokens_text_windows[i].size() + 4] = 1; } input_id[tokens_question.size() + tokens_text_windows[i].size() + 4] = tokenizer.convert_token_to_id("[SEP]"); segment_id[tokens_question.size() + tokens_text_windows[i].size() + 4] = 1; int len = tokens_text_windows[i].size() + tokens_question.size() + 5; // 掩码为1的表示为真实标记,0表示为填充标记。 std::fill(input_mask.begin(), input_mask.begin() + len, 1); std::fill(input_mask.begin() + len, input_mask.begin() + max_seq_length, 0); std::fill(input_id.begin() + len, input_id.begin() + max_seq_length, 0); std::fill(segment_id.begin() + len, segment_id.begin() + max_seq_length, 0); } ... } ``` 在自然语言处理领域中,不能对文本数据直接处理,需要对文本进行编码后再输入到模型中。因此,Bert模型中的输入数据主要由input_id、segment_id以及input_mask组成,其中input_id主要存储了对文本进行编码后的数值型数据,segment_id主要存储了用于区问题和上下文文本的信息(问题标记为0,上下文文本标记为1),input_mask主要存储了掩码信息(序列长度固定为256,当文本长度不足256时,对应的文本标记为1,无文本的标记为0),标记模型需要关注的地方。 ## 推理 完成数据预处理后,就可以执行推理,得到推理结果。 ```c++ ErrorCode Bert::Inference(...) { ... for(int i=0;i 从图中可以看出,数据后处理主要包含如下操作: 1.获取预测输出,根据模型推理结果,取前K个概率值最大的开始位置和结束位置。 2.筛选与过滤,根据过滤规则筛选开始位置和结束位置。 3.排序并输出结果,对开始位置加结束位置的概率值和进行排序,取概率值最大的组合作为最终的预测结果。 过滤规则: 1.开始位置和结束位置不能大于输入序列的长度。 2.开始位置和结束位置必须位于上下文文本中。 3.开始位置必须位于结束位置前。 本示例数据后处理代码,详见Src/NLP/Bert/Bert.cpp脚本中的Postprocessing()函数。