跳转至

TransformerLM

1 概述

1.1 背景介绍

TransformerLM 通常作为ASR中声学模型的评分器, 对预测结果进行重打分, 该模型来源于FunASR开源仓库。

具体可参考:

https://github.com/modelscope/FunASR/blob/v0.3.0/funasr/bin/lm_inference.py

模型下载地址为:

https://www.modelscope.cn/models/iic/speech_transformer_lm_zh-cn-common-vocab8404-pytorch/files

1.2 使用说明

Linux SDK-alkaid中默认带了已经预先转换好的离线模型及板端示例, 相关文件路径如下:

  • 板端示例程序路径

    Linux_SDK/sdk/verify/opendla/source/llm/transformerlm
    
  • 板端离线模型路径

    Linux_SDK/project/board/${chip}/dla_file/ipu_open_models/llm/lm_100sim.img
    
  • 板端测试字典路径

    Linux_SDK/sdk/verify/opendla/source/resource/units_asr_punc_lm.txt
    

如果用户不需要转换模型可直接跳转至第3章节。

2 模型转换

2.1 onnx模型转换

  • python环境搭建

    $conda create -n funasr python==3.10
    $conda activate funasr
    $pip install "modelscope[audio_asr]" --upgrade -f https://modelscope.oss-cn-beijing.aliyuncs.com/releases/repo.html
    $git clone https://github.com/alibaba/FunASR.git && cd FunASR
    $pip install --editable ./ -i https://mirrors.aliyun.com/pypi/simple/
    

    注意:这里提供的python环境搭建, 仅作为参考示例, 具体搭建过程请参考官方源码运行教程:

    https://github.com/modelscope/FunASR/tree/v0.3.0?tab=readme-ov-file#installation
    
  • 模型测试

    • 编写模型测试脚本funasr/bin/predict.py

      from modelscope.pipelines import pipeline
      from modelscope.utils.constant import Tasks
      
      inference_pipline = pipeline(
          task=Tasks.language_score_prediction,
          model='./speech_transformer_lm_zh-cn',
          output_dir='./tmp/')
      
      rec_result = inference_pipline(text_in='hello 大 家 好 呀')
      print(rec_result)
      
    • 运行模型测试脚本, 确保funasr环境配置正确

      $将模型文件speech_transformer_lm_zh-cn-common-vocab8404-pytorch放在当前目录下
      $mv speech_transformer_lm_zh-cn-common-vocab8404-pytorch speech_transformer_lm_zh-cn
      $python ./funasr/bin/predict.py
      
  • 模型导出

    • 编写模型转换脚本funasr/bin/lm_export.py:

      import os
      import sys
      sys.path.append(os.getcwd())
      import logging
      import argparse
      import torch
      import onnx
      import onnxsim
      import onnxruntime as ort
      
      from funasr.torch_utils.initialize import initialize
      from funasr.train.class_choices import ClassChoices
      
      from funasr.lm.espnet_model import ESPnetLanguageModel
      from funasr.lm.seq_rnn_lm import SequentialRNNLM
      from funasr.lm.transformer_lm import TransformerLM
      from funasr.lm.abs_model import AbsLM
      from funasr.modules.mask import subsequent_mask
      from funasr.modules.nets_utils import make_pad_mask
      import torch.nn.functional as F
      
      import numpy as np
      from funasr.tasks.lm import LMTask
      from funasr.torch_utils.forward_adaptor import ForwardAdaptor
      
      def load_vocab(vocab_path, extra_word_list=[]):
          n = len(extra_word_list)
          with open(vocab_path, encoding='utf-8') as vf:
              vocab = {word.strip(): i + n for i, word in enumerate(vf)}
      
          for i, word in enumerate(extra_word_list):
              vocab[word] = i
      
          return vocab
      
      def softmaxcrossentropy_c(  # type: ignore
          x, target, weight=None, reduction="None"
      ):
          input_shape = x.shape
          max_x = np.max(x, axis=1, keepdims=True).astype(np.float64)
          exp_x = np.exp(x - max_x)
          p = exp_x / np.sum(exp_x, axis=1, keepdims=True)
          inp = np.log(p)
      
          N = input_shape[0]
          neg_gather_element_input = np.zeros((N), dtype=x.dtype)
      
          inp = np.squeeze(inp)
          target = np.squeeze(target)
          for i in range(N):
              index = target[i]
              neg_gather_element_input[i] = -inp[i, index]
      
          return neg_gather_element_input
      
      if __name__ == '__main__':
          # 1. Build Model
          model, train_args = LMTask.build_model_from_file(
              './speech_transformer_lm_zh-cn/lm.yaml', './speech_transformer_lm_zh-cn/lm.pb', 'cpu')
          wrapped_model = ForwardAdaptor(model, "export")
      
          tmp_input = "梁家人出烟的电视剧有什么"
          token_dict = load_vocab('./speech_transformer_lm_zh-cn/tokens.txt')
          x_tmp = []
          for char in tmp_input:
              if char in token_dict.keys():
                  x_tmp.append(token_dict[char])
              else:
                  x_tmp.append(token_dict["<unk>"])
          x_tmp = torch.tensor([x_tmp], dtype=torch.int64)
          x_tmp = torch.cat([x_tmp,torch.zeros((1, 100 - x_tmp.shape[1]), dtype=int)], dim=1)
      
          wrapped_model.eval()
      
          onnx_path = './speech_transformer_lm_zh-cn/lm_1x100.onnx'
          torch.onnx.export(
                  wrapped_model,
                  (x_tmp),
                  onnx_path,
                  export_params=True,
                  opset_version=14,
                  input_names=["token"],
                  output_names=["probs", 'x_lengths'],
                  verbose=False,
              )
          model_onnx = onnx.load(onnx_path)  # load onnx model
          onnx.checker.check_model(model_onnx)  # check onnx model
          model_onnx, check = onnxsim.simplify(model_onnx)
          onnx.save(model_onnx, onnx_path.replace('lm_1x100','lm_1x100_sim'))
          ort_sess = ort.InferenceSession(onnx_path)
      
          input_length = 100
          input_list = [
                  "梁家人出烟的电视剧有什么"
              ]
          token_dict = load_vocab('./speech_transformer_lm_zh-cn/tokens.txt')
          count = 0
      
          for sentence in input_list:
              x = []
              for char in sentence:
                  if char in token_dict.keys():
                      x.append(token_dict[char])
                  else:
                      x.append(token_dict["<unk>"])
              src_len = len(x)
              x = torch.tensor([x], dtype=torch.int64)
              x = torch.cat([x,torch.zeros((1, 100 - x.shape[1]), dtype=int)], dim=1)
              np.save("./speech_transformer_lm_zh-cn/lmx100/lm_input_%d.npy"%(count), x)
              count+=1
              ort_inputs = {
                      ort_sess.get_inputs()[0].name: x.numpy(),
                  }
              ort_outs = ort_sess.run(None, ort_inputs)
              logits = ort_outs[0]
      
              print("logits: ", np.array(logits[0]).shape)
              print("logits: ", logits[0][0][:10])
      
              onnx_nll = softmaxcrossentropy_c(logits[0], ort_outs[1].flatten())
              print("onnx nll", onnx_nll)
      
              x_lens = torch.full((1,), fill_value=src_len, dtype=torch.int64)
              mask_ = np.zeros(input_length+1)
              mask_[np.arange(x_lens + 1)] = 1
      
              onnx_nll = np.array(onnx_nll * mask_)
              onnx_nll = np.sum(onnx_nll)
              onnx_nll = onnx_nll / (x_lens + 1)
              print("onnx_nll ", onnx_nll)
      
    • 运行模型转换脚本, 会在./speech_transformer_lm_zh-cn目录下生成lm_1x100.onnx模型

      python ./funasr/bin/lm_export.py
      

2.2 离线模型转换

2.2.1 预&后处理说明

  • 预处理

    language model 的输入为声学模型的输出, 即对应字典里的索引值。转换成功的lm_sim.onnx模型输入信息如下图所示:

  • 后处理

    language model模型没有后处理, 通常language model的输出的分数会再乘上一个系数, 再和声学模型的输出的分数进行加权, 然后再通过gready search的方式进行解码。输出信息如下所示:

2.2.2 offline模型转换流程

注意:1)OpenDLAModel对应的是压缩包image-dev_model_convert.tar解压之后的smodel文件。2)转换命令需要在docker环境下运行, 请先根据Docker开发环境教程, 加载SGS Docker环境。

  • 拷贝onnx模型到转换代码目录

    $cp speech_transformer_lm_zh-cn/lm_1x100.onnx OpenDLAModel/llm/transformerlm/onnx
    
  • 转换命令

    $cd IPU_SDK_Release/docker
    $bash run_docker.sh
    #进入到docker环境下的OpenDLAModel目录
    $cd /work/SGS_XXX/OpenDLAModel
    $bash convert.sh -a llm/transformerlm -c config/llm_transformerlm.cfg -p SGS_IPU_Toolchain(绝对路径) -s false
    
  • 最终生成的模型地址

    output/${chip}_${时间}/lm_100sim.img
    output/${chip}_${时间}/lm_100sim_fixed.sim
    output/${chip}_${时间}/lm_100sim_float.sim
    

2.2.2 关键脚本参数解析

-   input_config.ini

        [INPUT_CONFIG]
        inputs=token;                       #onnx 输入节点名称, 如果有多个需以“,”隔开;
        input_formats=RAWDATA_S16_NHWC;     #板端输入格式, 可以根据onnx的输入格式选择, 例如float:RAWDATA_F32_NHWC, int32:RAWDATA_S16_NHWC;
        quantizations=TRUE;                 #打开输入量化, 不需要修改;

        [OUTPUT_CONFIG]
        outputs=probs;                      #onnx 输出节点名称, 如果有多个需以“,”隔开;
        dequantizations=TRUE;               #是否开启反量化, 根据实际需求填写, 建议为TRUE。设为False, 输出为int16; 设为True, 输出为float32

-   llm_transformerlm.cfg

        [TRANSFORMERLM]
        CHIP_LIST=pcupid                    #平台名称, 必须和板端平台一致, 否则模型无法运行
        Model_LIST=lm_1x100                 #输入onnx模型名称
        INPUT_SIZE_LIST=0                   #模型输入分辨率,这里填0即可
        INPUT_INI_LIST=input_config.ini     #配置文件
        CLASS_NUM_LIST=0                    #填0即可
        SAVE_NAME_LIST=lm_100sim.img        #输出模型名称
        QUANT_DATA_PATH=image_list.txt      #量化数据路径

2.3 模型仿真

  • 获取float/fixed/offline模型输出

    $bash convert.sh -a llm/transformerlm -c config/llm_transformerlm.cfg -p SGS_IPU_Toolchain(绝对路径)  -s true
    

    执行上述命令后, 会默认将float模型的输出tensor保存到llm/transformerlm/log/output路径下的txt文件中。此外, 在llm/transformerlm/convert.sh脚本中也提供了fixedoffline的仿真示例, 用户在运行时可以通过打开注释代码块, 分别获取fixedoffline模型输出。

  • 模型精度对比

    在保证输入和上述模型相同的情况下, 进入2.1章节搭建好的环境, 在FunASR/funasr/bin/lm_inference.py脚本中第175行处添加打印:

    print(nll)
    

    即可获取pytorch模型对应节点的输出tensor, 进而和float、fixed、offline模型进行对比。此外需要特别注意的是, 原始模型的输出格式是NCHW, 而float/fixed/offline模型输出的格式是NHWC

3 板端部署

3.1 程序编译

示例程序编译之前需要先根据板子(nand/nor/emmc, ddr型号等)选择deconfig进行sdk整包编译, 具体可以参考alkaid sdk sigdoc《开发环境搭建》文档。

  • 编译板端transformerlm示例。

    $cd sdk/verify/opendla
    $make clean && make source/llm/transformerlm -j8
    
  • 最终生成的可执行文件地址

    sdk/verify/opendla/out/${AARCH}/app/prog_llm_transformerlm
    

3.2 运行文件

运行程序时, 需要先将以下几个文件拷贝到板端

  • prog_llm_transformerlm
  • lm_100sim.img

3.3 运行说明

  • Usage: ./prog_llm_transformerlm model dict(执行文件使用命令)

  • Required Input:

    • model: offline模型路径
    • dict: 字典
  • Typical Output:

    ./prog_llm_transformerlm models/lm_100sim.img resource/units_asr_punc_lm.txt
    
        client [801] connected, module:ipu
        load dict...
        invoke start ...
        model invoke time: 60.463000 ms
        lm score: -3.692400
    
        ------shutdown IPU0------
        client [801] disconnected, module:ipu