• [基础知识] MindSpore关于dataset相关总结
    根据过去我们队写过的博客,该文档对Mindspore的dataset处理、Tensor(张量)的定义文件以及应用等进行了梳理,系统地阐述了这些内容,有利于初次接触的新手快速上手Tensor和数据处理,同时也可以辅助资深高手对Mindspore内核文件进行理解。该文档主要涵盖三个方面的内容:Tensor的简单理解对MindSpore内核文件:tensor.cc的深度解析Tensor的实例数据加载Tensor的简单理解为了在深度学习框架中实现更高纬度的矩阵、向量,创建多维数组是必要的。而Tensor(张量)即为一种多维数组,几何代数中定义的张量是基于向量和矩阵的推广,通俗一点理解的话,我们可以将标量视为零阶张量,矢量视为一阶张量,那么矩阵就是二阶张量。当然我们理解为多维数组其实是并不准确的,如果需要正确地定义张量,张量其实是描述向量的变换,n阶张量其实是在基向量的个数为n的情况下,描述一个物体的量,因此扩宽阶数,我们能描述一切物体,这也就是张量当时定义的作用。0阶张量——标量x=np.array(10)1阶张量——矢量x=np.array([1,2,3,4,5,6])2阶张量——矩阵x = np.array([[5,10,15,30,25],[20,30,65,70,90],[7,80,95,20,30]])Tensor是所有深度学习框架中最核心的组件,因为后续的所有运算以及优化算法都是基于Tensor进行的。举例来说,著名深度学习框架Tensorflow就是Tensor和flow组成的。这种多维数组的一种应用就是在于图像处理,能把高维的图像数据用一些Tensor来表示。我们编写程序对这些图像数据进行处理,那么就相当于完成了对这张图片的处理,而深度学习中计算机视觉方向的工作就是对图像进行处理。因此Tensor在整个深度学习中起着地基的作用。该模块详细内容可见我们队伍以前写的博客:Tensor的简单理解(一)Tensor的简单理解(二)对MindSpore底层文件:tensor.cc的深度解析以下内容主要针对tensor.cc进行解读,内容包括:对Android组件的调用对Python组件的调用命名空间的声明和宏的定义读入文件的设置Tensor的创建Tensor的初始化、输入输出及内存分配Tensor的数据处理和分割对Android组件的调用在代码段开头,就include头文件来实现对Android的优化适配://同样地,对ANDROID进行优化适配,#include "minddata/dataset/core/global_context.h"对Android的适配,实现从ByteList类中创建张量://适配安卓,使本文件能被隐式调用,用#ifndef和#end来避免重复定义#ifndef ENABLE_ANDROIDStatus Tensor::CreateFromByteList(const dataengine::BytesList &bytes_list, const TensorShape &shape, TensorPtr *out) {//从字节列表中创建DE_STRING类型张量const TensorAlloc *alloc = GlobalContext::Instance()->tensor_allocator();*out = std::allocate_shared<Tensor>(*alloc, TensorShape({static_cast<dsize_t>(bytes_list.value_size())}),DataType(DataType::DE_STRING));// 所需的总字节数 = 偏移数组 + 字符串// 偏移数组需要为每个元素存储一个偏移变量 + 1 个额外的以获取最后一个字符串的长度。// 字符串将以空字符结尾 --> 每个元素需要 1 个额外字节dsize_t num_bytes = (kOffsetSize) * (*out)->shape_.NumOfElements() + kOffsetSize + bytes_list.ByteSizeLong();(*out)->data_ = (*out)->data_allocator_->allocate(num_bytes);auto offset_arr = reinterpret_cast<offset_t *>((*out)->data_);uchar *buf = (*out)->GetStringsBuffer();offset_t offset = buf - (*out)->data_; // 首字符串起始处uint32_t i = 0;for (; i < bytes_list.value_size(); i++) {const std::string &str = bytes_list.value(i);// 插入字符串的首索引值offset_arr[i] = offset;// 总字节数减少了k倍的offsetsizenum_bytes -= kOffsetSize;// 插入实际字符串int ret_code = memcpy_s((*out)->data_ + offset, num_bytes, common::SafeCStr(str), str.length() + 1);//若溢出(ret_code!=0)输出报错信息"无法复制字符串于张量中"if (ret_code != 0) {MS_LOG(ERROR) << "Cannot copy string into Tensor";}// 下个字符串将紧接着本字符串之后offset = offset + str.length() + 1;// 总字节数减去字符串的长度num_bytes -= str.length() + 1;}// 再存储一个偏移值,这样我们就可以得到最后一个字符串的长度// length[last_element] = offset_arr[last_element + 1] - offset_arr[last_element]offset_arr[i] = offset;(*out)->data_end_ = (*out)->data_ + offset_arr[i];MS_ASSERT(num_bytes == 0);RETURN_IF_NOT_OK((*out)->Reshape(shape));return Status::OK();}#endif对Python组件的调用在代码段开头,引入://对PYTHON进行特殊适配,引入了pybind_support.h,#include "minddata/dataset/core/tensor_shape.h"对Python的适配,实现从NpString类中创建张量://适配python,使本文件隐式调用#ifdef ENABLE_PYTHONStatus Tensor::CreateFromNpString(py::array arr, std::shared_ptr<Tensor> *out) {std::vector<dsize_t> shape;for (dsize_t i = 0; i < arr.ndim(); i++) {shape.push_back(static_cast<dsize_t>(arr.shape()[i]));}arr.resize({arr.size()}); //展开py::array,便于下一次迭代std::vector<std::string> strings;if (arr.dtype().kind() == 'U') {std::for_each(arr.begin(), arr.end(), [&strings](const auto &s) { strings.emplace_back(py::cast<py::str>(s)); });} else {std::for_each(arr.begin(), arr.end(), [&strings](const auto &s) { strings.emplace_back(py::cast<py::bytes>(s)); });}arr.resize(shape); // 重置array于初始shapereturn CreateFromVector(strings, TensorShape{shape}, out);}Status Tensor::CreateFromNpArray(const py::array &arr, std::shared_ptr<Tensor> *out) {if (DataType::FromNpArray(arr) == DataType::DE_STRING) {return CreateFromNpString(arr, out);}std::vector<dsize_t> shape;std::vector<dsize_t> strides;// 判断stride是否连续bool is_strided = false;dsize_t count = arr.size();for (dsize_t i = 0; i < arr.ndim(); i++) {shape.push_back(static_cast<dsize_t>(arr.shape()[i]));strides.push_back(static_cast<dsize_t>(arr.strides()[i]));// 数组为空的情况下,num_items为0if (count != 0) {count /= shape[i];if (strides[i] != arr.itemsize() * count) {is_strided = true;}}}unsigned char *data = static_cast<unsigned char *>(arr.request().ptr);//显式类型转化,将数组请求指针转化为unsigned char *类型if (is_strided) {RETURN_IF_NOT_OK(Tensor::CreateEmpty(TensorShape(shape), DataType::FromNpArray(arr), out));RETURN_IF_NOT_OK(CopyStridedArray((*out)->data_, data, shape, strides, (*out)->type_.SizeInBytes()));} else {RETURN_IF_NOT_OK(Tensor::CreateFromMemory(TensorShape(shape), DataType::FromNpArray(arr), data, out));}return Status::OK();}#endif命名空间的声明和宏的定义其中,也对一些基本的宏的语法做了一些说明:namespace mindspore {namespace dataset {//该命名空间内定义 用于打印tensor元素的辅助宏//对于带参数的宏定义,由于换行符代表结束定义//对此,每行结尾使用`\`来连续上下行,回车要紧接`\`之后#define CASE_PRINT(de_type, native_type) \case de_type: { \native_type o; \rc = GetItemAt<native_type>(&o, index); \out << o; \break; \}//输出宏,定义为16进制的输出和对应的十进制输出#define CASE_PRINT_HEX(de_type, native_type) \case de_type: { \native_type o; \rc = GetItemAt<native_type>(&o, index); \out << std::hex << std::setw(2) << std::setfill('0') << o << std::dec << std::setfill(' '); \break; \}读入文件的设置Status Tensor::CreateFromFile(const std::string &path, std::shared_ptr<Tensor> *out) {//读取文件流,输入至内存std::ifstream fs;fs.open(path, std::ios::binary | std::ios::in);//对于非字符串形式文本读取,使用二进制形式打开文件 CHECK_FAIL_RETURN_UNEXPECTED(!fs.fail(), "Fail to open file: " + path);//将读指针设置为0 + std:ios::end;而且返回读指针给num_typesint64_t num_bytes = fs.seekg(0, std::ios::end).tellg();//确认读文件路径和文件大小的检查报错,并设置正确张量输出CHECK_FAIL_RETURN_UNEXPECTED(num_bytes <= kDeMaxDim, "Invalid file to allocate tensor memory, check path: " + path);CHECK_FAIL_RETURN_UNEXPECTED(fs.seekg(0, std::ios::beg).good(), "Fail to find size of file, check path: " + path);RETURN_IF_NOT_OK(Tensor::CreateEmpty(TensorShape{num_bytes}, DataType(DataType::DE_UINT8), out));//将可变缓冲区的字符强制转化为字符指针类型,再读取该文件并返回长度给written_bytesint64_t written_bytes = fs.read(reinterpret_cast<char *>((*out)->GetMutableBuffer()), num_bytes).gcount();CHECK_FAIL_RETURN_UNEXPECTED(written_bytes == num_bytes && fs.good(),"Error in writing to tensor, check path: " + path);fs.close();return Status::OK();}#ifndef ENABLE_ANDROIDStatus Tensor::CreateFromByteList(const dataengine::BytesList &bytes_list, const TensorShape &shape,const DataType &type, dsize_t pad_size, TensorPtr *out) {RETURN_IF_NOT_OK(Tensor::CreateEmpty(shape, type, out));//将输出可变缓冲区获取当前张量指针,存于curr_tensor_addrunsigned char *current_tensor_addr = (*out)->GetMutableBuffer();int64_t tensor_bytes_remaining = bytes_list.value_size() * pad_size;for (int i = 0; i < bytes_list.value_size(); i++) {// 将字符串数据读入张量const std::string &current_element = bytes_list.value(i);int return_code =memcpy_s(current_tensor_addr, tensor_bytes_remaining, common::SafeCStr(current_element), current_element.size());CHECK_FAIL_RETURN_UNEXPECTED(return_code == 0, "memcpy_s failed when reading bytesList element into Tensor");current_tensor_addr += current_element.size();tensor_bytes_remaining -= current_element.size();// 缓冲设置int64_t chars_to_pad = pad_size - current_element.size();return_code = memset_s(current_tensor_addr, tensor_bytes_remaining, static_cast<int>(' '), chars_to_pad);CHECK_FAIL_RETURN_UNEXPECTED(return_code == 0, "memcpy_s failed when padding Tensor");current_tensor_addr += chars_to_pad;tensor_bytes_remaining -= chars_to_pad;}return Status::OK();}#endifTensor的创建以下代码块的用于张量的创建,其中首先需要对内存空间进行请求并开辟空间。memcpy_ss是进行内存复制的实现函数。CreateEmpty先根据张量的参数(shape和type)进行初始化,并调用析构函数再请求CreateFromMemory完成张量的创建。/*在以下注释前`///`注释,表示此些注释会被加载进入编译,同时生成一个xml文件,便于之后查看此类时生成注释*//// Copy memory with no max limit since memcpy_s will fail when byte_size > 2^31 - 1 (SECUREC_MEM_MAX_LEN)./// 复制内存不会超出限制,因为byte_size超出2^31 - 1(SECUREC_MEM_MAX_LEN)会出现错误/// \param dest Destination buffer./// 参数dest 目标缓存区/// \param destMax Size of the destination buffer./// 参数destMax 目标缓存区的最大空间/// \param src Buffer to copy from./// 参数src 缓存复制来源/// \param count Number of characters to copy/// 参数count 需要复制的字符数/// \return Error number. Returns 0 for succuss copying./// 返回值Error number 成功复制后返回0值errno_t memcpy_ss(uchar *dest, size_t destMax, const uchar *src, size_t count) {uint32_t step = 0;while (count >= SECUREC_MEM_MAX_LEN) {int ret_code = memcpy_s(dest + step * SECUREC_MEM_MAX_LEN, SECUREC_MEM_MAX_LEN, src + step * SECUREC_MEM_MAX_LEN,SECUREC_MEM_MAX_LEN);if (ret_code != 0) {return ret_code;}count -= SECUREC_MEM_MAX_LEN;step++;}if (count > 0) {return memcpy_s(dest + step * SECUREC_MEM_MAX_LEN, count, src + step * SECUREC_MEM_MAX_LEN, count);}return 0;}//利用递归原理进行内存复制,通过step来进行逐步内存复制Tensor::Tensor(const TensorShape &shape, const DataType &type) : shape_(shape), type_(type), data_(nullptr) {// grab the mem pool from global context and create the allocator for char data area//从全局上下文中获取内存池并为字符数据区创建分配器std::shared_ptr<MemoryPool> global_pool = GlobalContext::Instance()->mem_pool();data_allocator_ = std::make_unique<Allocator<unsigned char>>(global_pool);}Tensor::Tensor(Tensor &&other) noexcept//noexcept是函数异常声明,上述相当于noexcept(true),即不会抛出任何异常: shape_(other.shape()),type_(other.type()),data_(other.GetMutableBuffer()),data_end_(other.data_end_),data_allocator_(std::move(other.data_allocator_)) {other.Invalidate();}/*调用Tensor的构造函数,将other的多项参数进行构造。最后调用Invaliate()会使MData设置为空来使此张量无效且无法访问其和其数据,需谨慎使用*/Tensor &Tensor::operator=(Tensor &&other) noexcept {if (&other != this) {shape_ = other.shape();type_ = other.type();data_ = other.GetMutableBuffer();data_end_ = other.data_end_;data_allocator_ = std::move(other.data_allocator_);other.Invalidate();}return *this;}//重载运算符`=`,对输入other进行重载运算//定义CreateEmpty的Status,输入Tensor的shape和type,输出out张量指针Status Tensor::CreateEmpty(const TensorShape &shape, const DataType &type, TensorPtr *out) {CHECK_FAIL_RETURN_UNEXPECTED(shape.known(), "Invalid shape.");CHECK_FAIL_RETURN_UNEXPECTED(type != DataType::DE_UNKNOWN, "Invalid data type.");const TensorAlloc *alloc = GlobalContext::Instance()->tensor_allocator();*out = std::allocate_shared<Tensor>(*alloc, shape, type);// if it's a string tensor and it has no elements, Just initialize the shape and type.if (!type.IsNumeric() && shape.NumOfElements() == 0) {return Status::OK();//如果它是一个字符串张量并且它没有元素,只需初始化形状和类型。}CHECK_FAIL_RETURN_UNEXPECTED(type.IsNumeric(), "Number of elements is not 0. The type should be numeric.");//返回提示信息,当元素不为0时,类型应该为数字int64_t byte_size = (*out)->SizeInBytes();// Don't allocate if we have a tensor with no elements.if (byte_size != 0) {RETURN_IF_NOT_OK((*out)->AllocateBuffer(byte_size));}//对于没有元素的张量,不予分配return Status::OK();}//从内存中创建张量Status Tensor::CreateFromMemory(const TensorShape &shape, const DataType &type, const uchar *src, TensorPtr *out) {RETURN_IF_NOT_OK(CreateEmpty(shape, type, out));if (src != nullptr) {//给定此张量的形状/类型,计算数据大小并复制输入字节int64_t byte_size = (*out)->SizeInBytes();int ret_code = memcpy_ss((*out)->data_, byte_size, src, byte_size);//从内存中复制字节长度,src源以及数据内容,返回输出状态于ret_codeCHECK_FAIL_RETURN_UNEXPECTED(ret_code == 0, "Failed to copy data into tensor.");//如果输出错误,即ret_code为0,输出报错信息}return Status::OK();}Status Tensor::CreateFromMemory(const TensorShape &shape, const DataType &type, const unsigned char *src,const dsize_t &length, TensorPtr *out) {CHECK_FAIL_RETURN_UNEXPECTED(src != nullptr, "Pointer to source data is null.");//如果src为nullptr,则进行空指针报错处理const TensorAlloc *alloc = GlobalContext::Instance()->tensor_allocator();//alloc返回以张量分配器作为的原始指针*out = std::allocate_shared<Tensor>(*alloc, shape, type);if (type.IsNumeric()) {//如果元素类型为数字常量,则计算源数据长度dsize_t calculated_length = (*out)->SizeInBytes();CHECK_FAIL_RETURN_UNEXPECTED(calculated_length == length, "Length of source data does not match the shape.");//判断字节长度是否与对应shape匹配} else {// min_length is the length of a tensor with empty strings// min_length代表空字符串张量的字节长度// min_length = the number of bytes needed to store the offsets + 1 byte for each element//min_length = 存储偏移量所需的字节数 + 每个元素的 1 个字节dsize_t min_length = (shape.NumOfElements() + 1) * kOffsetSize + shape.NumOfElements();CHECK_FAIL_RETURN_UNEXPECTED(min_length <= length, "Length of source data does not match the shape.");//检查字符串张量是否与对应shape匹配}RETURN_IF_NOT_OK((*out)->AllocateBuffer(length));//检查张量分配内存是否成功int ret_code = memcpy_ss((*out)->data_, length, src, length);CHECK_FAIL_RETURN_UNEXPECTED(ret_code == 0, "Failed to copy data into tensor.");//检查复制数据是否成功进入张量return Status::OK();}Tensor的初始化、输入输出及内存分配在此部分代码中,MindSpore对于张量的维度以及输入输出做了详细的设计。Tensor的初始化,输入输出以及内存分配均进行定义。MindSpore与Tensorflow类似,均可进行NumPy和Tensor的互相转化。张量之间有很多运算,包括算术、线性代数、矩阵处理(转置、标引、切片)、采样等。如下代码中也涉及了与NumPy相似的变化维度的功能。在我们使用MindSpore进行深度学习训练时,很多时候都是操作NumPy数据,例如csv或图像数据等。但TensorFlow训练时都是使用Tensor来存储变量的,并且网络输出的结果也是Tensor。 一般情况下我们不会感受到Numpy与Tensor之间的区别,这是因为模型在输入NumPy数据时会自动转换为Tensor来处理。接下来的代码将会体现这一特性,Tensor作为MindSpore的核心数据结构,其操作和转化将影响到模型网络的优化特性。由于代码段过长,详细解析可见:Tensor的初始化、输入输出及内存分配Tensor的数据处理和分割为了更好的进行Tensor的运算,结合之前所提到的Numpy类型转换,MindSpore对一些特定的数据类型进行了Numpy的转化。期间涉及到数组类型与numpy和tensor的转化。由于代码段过长,详细解析可见:Tensor的数据处理和分割该模块详细内容可见我们队伍以前写的博客:对tensor.cc的深度解析(一)对tensor.cc的深度解析(二)对tensor.cc的深度解析(三)对tensor.cc的深度解析(四)Tensor的实例张量属性张量的属性包括形状(shape)和数据类型(dtype)。形状:Tensor的shape,是一个tuple。数据类型:Tensor的dtype,是MindSpore的一个数据类型。张量实例我们队伍对于以下几个方面都写出了一段简短的Python代码方便大家理解,如何使用张量。算数运算(如:加减乘除 Add/minus/multiply/divide)索引和切片操作与NumPy的互相转换如何初始化张量该模块详细内容可见我们队伍以前写的博客:Tensor的实例数据加载在MindSpore框架下运用dataset模块创建了能够对常见数据集完成加载的数据接口。在具体的实践过程中,用户能够通过直接调用此类接口对数据集完成加载。实践代码(以加载MNIST数据集为例):import matplotlib.pyplot as pltimport matplotlibimport numpy as npimport mindspore.dataset as dstrain_data_path = "./datasets/MNIST_Data/train"test_data_path = "./datasets/MNIST_Data/test"mnist_ds = ds.MnistDataset(train_data_path)print('The type of mnist_ds:', type(mnist_ds))print("Number of pictures contained in the mnist_ds:", mnist_ds.get_dataset_size())dic_ds = mnist_ds.create_dict_iterator()item = next(dic_ds)img = item["image"].asnumpy()label = item["label"].asnumpy()print("The item of mnist_ds:", item.keys())print("Tensor of image in item:", img.shape)print("The label of item:", label)plt.imshow(np.squeeze(img))plt.title("number:%s"% item["label"].asnumpy())plt.show()图像显示结果:
  • [问题求助] 【Atlas 200 DK】【CANN】[开发工具链] 【Mindspore】【benchmark功能】
    【功能模块】benchmark执行后生成bin文件【操作步骤&问题现象】1参考 这里 使用benchark对resnet-pytorch1.2模型 测试运行,执行成功,截图如下打开./result/dump0对应文件生成的全是二进制bin文件,而不是同时在“result/dumpOutput_device0”下生成各数据的推理结果文件(与各数据同名的txt文件)。 2、先尝试能不能偷懒,执行vision_metric.py,出现utf8无法解码开始位置的字符,后尝试在open文件,修改读入方式为rb,并在后续加入encoding=‘gbk’,出现gbk也无法编解码的问题。请问老哥们,为什么我的运行没问题,生成的却是bin文件,此外应该怎么处理呢,十分感谢【截图信息】【日志信息】(可选,上传日志内容或者附件)
  • [执行问题] 【MindSpore】【GPU 训练】Faster RCNN 训练 COCO 的时候出现 loss 突然增大导致模型损坏
    【功能模块】MindSpore 版本:MindSpore 1.5.0 官方 PIP 源安装版本Faster RCNN 模型:官方 Model Zoo master 分支:https://gitee.com/mindspore/models/tree/master/official/cv/faster_rcnnMSCOCO 数据集:https://cocodataset.org/#download, 【操作步骤&问题现象】Ubuntu 18.04,NVIDIA Tesla T4 GPU1、安装 Docker,安装 NVIDIA Container Toolkit(可将 GPU 挂载到容器中),下载 MindSpore Runtime Docker 官方镜像,创建 MindSpore 容器;2、在 MindSpore Runtime 容器中安装MindSpore 1.5.0 官方 PIP 源安装版本;3、下载官方 Model Zoo: https://gitee.com/mindspore/models;4、修改配置文件,将训练 batch size 设置为 6,base_lr 保持默认值 0.04;5、从 MSCOCO 训练数据集中,随机抽取 1% 的数据,在配置文件中配置 1%  MSCOCO 训练数据路径;6、启动训练任务。训练进行到第 80 个 epoch 的时候,loss 突然增大,对其梯度经反向传播更新模型后,模型参数变质,模型损坏 ,之前训练获得的推理能力全部丢失。【截图信息】【日志信息】(可选,上传日志内容或者附件)请见附件
  • [基础知识] 浅谈MindSpore的动态Shape
    ## 写在前面   在MindSpore开发过程中,由于动态Shape算子的开发需求,再加上MindSpore的动态Shape也在持续完善,笔者遇到了框架上的一些问题。通过查看源码和相关文档的方式,获得了初步的解决方案和感悟。这篇博客主要是将当时的见闻加以整理,并给出一点点开发建议。由于本人刚入职不久,本博客适合于初学者,高手轻喷。 ## 1 动态Shape的定义   动态Shape,指的是Tensor的Shape依赖于具体的运算,无法提前通过计算得出。具体来说分两种情况:算子输入是动态Shape和算子输出是动态Shape。其中,后者是常见的动态Shape算子,如图1的MaskedSelect算子是动态Shape算子,因为它的输出依赖于输入中Mask的具体数值,我们无法提前知道Shape的大小;而前者一般是被动态Shape算子感染的算子,例如MaskedSelect算子后面接一个Sum算子,那么这个Sum算子被MaskedSelect的输出感染了,所以此时的Sum算子也属于动态Shape算子。 ```python >>> x = Tensor(np.array([1, 2, 3, 4]), mindspore.int64) >>> mask = Tensor(np.array([1, 0, 1, 0]), mindspore.bool_) >>> output = ops.MaskedSelect()(x, mask) >>> print(output) [1 3] ```图1 动态Shape算子MaskedSelect  MindSpore有静态图和动态图模式,可以设置参数一键切换。在推导Shape时,动态图模式对算子逐个推导,不需要考虑整图中Shape的具体大小,所以前后端都很容易处理;而静态图模式需要推导出整图中所有算子的类型和Shape大小,以便于在前后端进行整图优化。下面阐述如何自定义一个支持动态Shape的算子以及简单分析一下动态Shape在MindSpore前后端的具体实现,包括静态图和动态图模式。 ## 2 自定义算子 ### 2.1 自定义静态Shape算子   首先简单回顾下如何在MindSpore自定义一个普通算子(Primitive),即实现一个静态Shape算子。在MindSpore里,每个算子有对应的前端注册和后端实现。其中,前端注册需要明确算子的输入输出个数,相应的参数检查和推导(infer)函数。这里推导函数的作用是告诉框架算子输入输出的Shape大小和类型的计算和推导规则。后端实现是指在不同硬件平台下的具体实现,如CPU,GPU和Ascend等。前后端如此分工的原因是由它们在AI框架中所担负的职责决定的。前端旨在提供硬件无关的中间表达,而后端针对前端的表达执行硬件感知的实现。   **前端注册**。前端注册也称作算子原语注册。首先每个算子是一个单独的类,继承于PrimitiveInfer作为子类,且类的名称即要注册的算子名称。MindSpore已有的算子可以在[官网](https://www.mindspore.cn/docs/api/zh-CN/master/api_python/mindspore.ops.html)查询。PrimitiveInfer定义在*mindspore/ops/primitive.py*。图2是PrimitiveInfer在源码中的注释。 ```python class PrimitiveWithInfer(Primitive): """ PrimitiveWithInfer is the base class of primitives in python and defines functions for tracking inference in python. There are four method can be overridden to define the infer logic of the primitive: __infer__(), infer_shape(), infer_dtype(), and infer_value(). If __infer__() is defined in primitive, the __infer__() has the highest priority to be called. If __infer__() is not defined, infer_shape() and infer_dtype() can be defined to describe the infer logic of the shape and type. The infer_value() is used for constant propagation. """ def __init__(self, name): Primitive.__init__(self, name) self.set_prim_type(prim_type.py_infer_shape) def infer_shape(self, *args): """ Infer output shape based on input shape. """ return None def infer_dtype(self, *args): """ Infer output dtype based on input dtype. """ return None def infer_value(self, *args): """ Infer output value based on input value at compile time. """ return None def __infer__(self, *args): """Infer shape, type, and value at the same time by using dictionary as arguments.""" ```图2 PrimitiveWithInfer的定义\_\_init\_\_函数定义了输入输出的名称和数量,并且有四个可重载的类成员方法:\_\_infer\_\_,infer\_shape,infer\_dtype和infer\_value。其中: - **infer\_shape**:旨在根据输入的Shape推导输出的Shape; - **infer\_dtype**:旨在根据输入的数据类型推导输出的数据类型; - **infer\_value**:要求在编译期根据输入的值直接算出输出的值,一般配合@constexpr装饰符使用,适合于少量算子; - **\_\_infer\_\_**:集成了另外三个infer函数的所有功能,拥有最高优先级,输入和输出是字典的形式。 在撰写算子时一般参考已有的算子,在已有的算子基础上进行修改。   **后端实现**。后端实现依赖于具体的硬件平台,如CPU,GPU和Ascend(自研的昇腾芯片)等。相关代码存放在*mindspore/ccsrc/backend/kernel\_compiler*文件夹下。这里以CPU为目标平台作为例子进行阐述。和算子原语注册类似,首先需要在*mindspore/ccsrc/backend/kernel\_compiler/cpu*文件夹下创建.cc和.h文件,定义一个继承CPUKernel的子类。图3是CPUKernel的主要成员函数。 ```c++ class CPUKernel : public kernel::KernelMod { public: CPUKernel() = default; ~CPUKernel() override = default; virtual void Init(const CNodePtr &kernel_node); virtual void InitKernel(const CNodePtr &kernel_node) = 0; virtual bool Launch(const std::vector<AddressPtr> &inputs, const std::vector<AddressPtr> &workspace, const std::vector<AddressPtr> &outputs) = 0; protected: template <typename T> inline T *GetDeviceAddress(const std::vector<AddressPtr> &addr_list, size_t index); }; ```图3 CPUKernel的定义- **Init**:通常保持默认实现。它会接连调用InitKernel和InitInputOutputSize函数; - **InitKernel**:需要重载。入参是CNode指针的引用,通过调用*mindspore/ccsrc/backend/session/anf\_runtime\_algorithm.cc*中定义的方法,我们可以获取该算子节点的输入输出的Shape以及算子的属性信息,如算子的坐标(axis)属性,这些属性在算子原语的\_\_init\_\_中注册。InitKernel方法的职能包括: 1. 检查参数的合法性,如输入输出的数量,参数的类型,Shape大小和属性等; 2. 指定workspace的大小,这里的workspace表示算子执行过程中需要申请的内存大小,可以由框架统一申请和释放。CPU算子用的较少。常用于GPU,需要框架明确申请显存。 - **Launch**:需要重载。三个入参分别是input,workspace和output的地址。在具体实现中,我们可以采用多线程加速计算,也可以调用第三方库:如CPU可以用MKL实现神经网络算子,Eigen实现线性代数等;GPU可以用CUDA,CUDNN等。 - **GetDeviceAddress**:公共接口,供Launch调用,用于获取输入输出地址。   总的来说,一般我们会重载InitKernel和Launch方法。其中,InitKernel通常做初始化相关的工作,提前计算好内存大小,所以将与不依赖输入值的操作放在此方法执行;而Lanuch执行具体的计算,并且算子之间有依赖关系,当然这是框架应当考虑的事情。后面动态Shape部分也会提及一点。当我们写完算子的Kernel类之后,需要注册该算子的输入输出类型。CPU后端提供了三种算子注册方式: 1. **MS\_REG\_CPU\_KERNEL**: 通常用于非模板类,在Launch控制不同类型的处理; 2. **MS\_REG\_CPU\_KERNEL\_T**: 通常用于输入为泛型的模板类,每注册一次实例化一个模板; 3. **MS\_REG\_CPU\_KERNEL\_T\_S**: 通常用于输入和输出均为泛型的模板类,常用于输入输出不统一的算子,如Cast算子; 三种注册存放在*mindspore/ccsrc/backend/kernel\_compiler/cpu/cpu\_kernel\_factory.h*。本质上都是调用工厂方法进行注册,需要考虑的是如何根据算子的特点选择不同的注册,使得算子的实现更加简洁和易扩展。   综上所述,整个AI框架的运行逻辑如下:前端负责将Python表达翻译成算子组成的图(又叫ANF图)。图的每个节点的输入输出的类型和Shape大小都被正确推导;后端拿到这个图之后有一个算子选择的过程,即根据算子名称,输入输出个数和类型匹配具体的算子实现,最后将执行结果返回到Python侧输出。此外,不同后端的算子选择的过程会有所不同。如在CPU会自动降低精度寻找对应算子,而GPU是严格匹配算子注册类型。然而,动态Shape的出现给这个过程带来了一些变化:在前端,根据动态Shape的定义,我们无法预测算子的输入/输出的准确Shape;在后端,由于内存提前分配的缘故,我们需要在实现的过程注意动态Shape带来的细微变化。另外,算子注册上也有一些需要注意的地方,下一节会详细阐述。 ### 2.2 自定义动态Shape算子   AI网络中用到的算子通常是静态Shape的算子,采用Python编写推理函数简单直接,因此前期MindSpore的推理函数主要在Python侧定义。随着对各类计算场景的需求增多,MindSpore需要支持动态Shape的算子,这类算子不仅要在前端推导输入输出的Shape范围(最大/最小Shape),还需要在后端执行的时候计算算子实际的Shape大小,以便于框架更好地进行内存管理(这部分后面分析代码的时候可以看到)。换句话说,后端也需要调用推理函数。然而,由于Python存在全局解释锁(Global Interpreter Lock, GIL),后端执行的时候需要由C++运行时转到Python运行时,这不仅需要框架在处理流程上进行变动,也不利于框架的并行处理。所以后端需要调用C++侧的推理函数,Python侧的原语定义不能满足动态Shape的需求。再后来,为了框架的统一,MindSpore也逐步推进前后端使用同一个算子推理函数。[图4](https://gitee.com/david-he91/mindspore/wikis/MindSpore%E7%AE%97%E5%AD%90%E4%BC%97%E6%99%BA/%E5%8A%A8%E6%80%81Shape%E7%AE%97%E5%AD%90/%E5%8A%A8%E6%80%81Shape%E7%AE%97%E5%AD%90%E5%BC%80%E5%8F%91%E6%8C%87%E5%8D%97/04%20%E5%BC%80%E5%8F%91%E5%86%85%E5%AE%B9)是目前的算子原语注册的流程。当同时存在Python侧和C++侧推理函数时,以C++侧的推理函数为主。 ![](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202111/25/0940016etj5ehedym5fwpg.png)图4 算子原语的前端定义  **前端注册**。C++侧的算子原语前端注册和Python侧相似:都需要继承基类定义一个算子子类,用于声明算子和输入输出。不同的是:infer函数不是作为类的一部分,需要另外定义,并以宏的方式(与后端注册算子实现类似)注册到prim\_eval\_implement\_map中,供框架统一被调用。具体步骤如下: 1. 在.h文件,继承PrimitiveC声明算子子类、定义推理函数接口; 2. 在mindspore/core/base/core_ops.h声明一个const指针指向新定义的算子原语; 3. 在.cc文件,实现成员函数(若有)和推理函数,并注册推理函数; 为了描述输入输出中shape,value,dtype等信息,C++侧定义了一种AbstractBase的抽象基类,其继承关系如图5所示。其中使用的最广泛的是AbstractTensor和AbstractTuple两个子类,可以认为分别是Python的Tensor和Tuple两种类型的C++实现。此外,通过上述步骤2定义PrimitivePtr类型的const指针可以获取算子的属性值(通过GetAttr方法),这是部分算子确定类型和Shape大小所必须的。前端和后端都会调用prim\_eval\_implement\_map中定义的infer函数(若有),在后面的源码分析部分可以看到。此map的key是上述步骤2定义的const指针,value是步骤3定义的推理函数。综上,C++侧的前端定义表达是足够覆盖Python定义算子原语所需要的信息,包括原语类和数据结构,虽然语法上没有Python侧的易用。 ![](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202111/25/0940119wutlkaq0aujejbn.png)图5 抽象类的继承关系(部分)  对于动态Shape算子,我们需要设置Shape中变化的维度为-1,还有最大和最小的Shape。如果连维度的多少也不确定,那么Shape直接固定为[-2]。以图1的MaskedSelect算子为例,推导的shape,min_shape和max_shape分别为[-1],[size]和[0]。其中size为mask变量中为True的个数。由此可见,小于0是框架判断是否为动态Shape的依据(后续也可以在代码中看到)。此外,编写动态Shape算子的推理函数时,需要特别注意会对最终Shape有确定作用的地方。如Sum算子的属性KeepDims是否为True对Shape有影响。一句话概括就是,具有动态Shape性质的算子需要编写C++侧的前端算子原语注册,并需要准确描述变化的Shape和最大/最小Shape。更详细的示例可以参照这个[开发指南](https://gitee.com/david-he91/mindspore/wikis/MindSpore%E7%AE%97%E5%AD%90%E4%BC%97%E6%99%BA/%E5%8A%A8%E6%80%81Shape%E7%AE%97%E5%AD%90/%E5%8A%A8%E6%80%81Shape%E7%AE%97%E5%AD%90%E5%BC%80%E5%8F%91%E6%8C%87%E5%8D%97/04%20%E5%BC%80%E5%8F%91%E5%86%85%E5%AE%B9)。   **后端实现**。后端下动态Shape算子实现和静态Shape的算子没有什么大的变化,但我们需要注意的是动态Shape算子在后端的执行流程以及实现细节。正如前面2.1节所说,InitKernel会做输入和输出Shape大小的初始化,并且Init部分是统一执行的。然而,动态Shape算子是在执行之后才能知道算子的具体大小。也就是说,InitKernel获取的信息是不准确的(注:截至r1.3的版本,动态Shape算子仍在整改中,听说后面的版本会支持如果有动态Shape算子会重新出发Init)。所以,当一个算子的输入是动态Shape时,我们需要注意InitKernel有没有初始化输入输出大小供后面的Launch使用,这个地方容易导致算子运行错误。Launch部分需要注意的是: - 通过SetOutputInferTypeAndShape的方式设置每个输出的数据类型和数据大小。该方法存放在*mindspore/ccsrc/backend/session/anf_runtime_algorithm.h*。MaskedSelect算子的CPU实现是一个很好的参考; - 在计算之前正式开始之前,要注意输入输出的Shape是否准确,如前面所说的InitKernel是一种情况。此外,还有一些算子(如CPU的Cast算子),通过output->size / sizeof(T),即将分配的output指针的大小作为最后的大小,这个长度有可能会出错。例如CPU后端当存在类型不匹配算子时会自动插入cast算子做自动转换。此时的Cast算子会由于前一个算子是动态Shape算子并且未执行inferShape更新Cast算子输入的真正Shape而导致分配内存大小不正确,这时候Cast算子的输出长度就不正确。避免这种情况的做法是用AnfAlgo::GetOutputInferShape获取正确的大小。 除了算子实现之外,还需注意的是动态Shape算子的注册问题。 ## 3 结语   就到这里结束吧!本来还想多写一章对库上相关代码的分析。不过正如前面所说,MindSpore的代码还在不断完善,相信动态Shape算子会支持得更好,再加上笔者只是前期有动态Shape算子的需求,所以后面就没有持续追踪了。
  • [推理] MindSpore Lite 移动端识别,报错writeToArray dataLength and total should
    package net.pengtu.aihw;import androidx.appcompat.app.AppCompatActivity;import android.content.res.AssetManager;import android.graphics.Bitmap;import android.graphics.BitmapFactory;import android.graphics.Color;import android.os.Bundle;import android.util.Log;import android.widget.Toast;import com.google.gson.Gson;import com.huawei.hmf.tasks.OnFailureListener;import com.huawei.hmf.tasks.OnSuccessListener;import com.huawei.hms.mlsdk.common.MLApplication;import com.huawei.hms.mlsdk.common.MLException;import com.huawei.hms.mlsdk.custom.MLCustomRemoteModel;import com.huawei.hms.mlsdk.custom.MLModelDataType;import com.huawei.hms.mlsdk.custom.MLModelExecutor;import com.huawei.hms.mlsdk.custom.MLModelExecutorSettings;import com.huawei.hms.mlsdk.custom.MLModelInputOutputSettings;import com.huawei.hms.mlsdk.custom.MLModelInputs;import com.huawei.hms.mlsdk.custom.MLModelOutputs;import com.huawei.hms.mlsdk.model.download.MLLocalModelManager;import com.huawei.hms.mlsdk.model.download.MLModelDownloadStrategy;import java.io.IOException;import java.io.InputStream;public class MainActivity extends AppCompatActivity { private static final String TAG = "lhd"; MLModelInputOutputSettings inOutSettings; int batchNum = 0; int max =640; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); try { inOutSettings = new MLModelInputOutputSettings.Factory() .setInputFormat(0, MLModelDataType.FLOAT32, new int[]{1, 3,max, max}) .setOutputFormat(0, MLModelDataType.FLOAT32, new int[]{1, 1001}) .create(); } catch (MLException e) { e.printStackTrace(); Log.e(TAG, "onCreate: " + e.getLocalizedMessage()); return; } MLModelDownloadStrategy strategy = new MLModelDownloadStrategy .Factory()// .needWifi() .setRegion(MLModelDownloadStrategy.REGION_DR_CHINA) // 设置站点区域,目前支持站点区域有:REGION_DR_CHINA,REGION_DR_AFILA,REGION_DR_EUROPE,REGION_DR_RUSSIA(站点区域需要和配置AppGallery Connect时选择的服务接入站点保持一致)。 .create(); String apikey="apikey"; MLApplication.getInstance().setApiKey(apikey); final MLCustomRemoteModel remoteModel = new MLCustomRemoteModel.Factory("gg") .create(); MLLocalModelManager.getInstance().downloadModel(remoteModel, strategy, (l, l1) -> { // 设置监听器。 // 对下载进度进行处理。 Log.e(TAG, "onProcess: " + l + " " + l1); }).onSuccessTask(unused -> { Log.e(TAG, "then:现在成功 "); return null; }).addOnSuccessListener(new OnSuccessListener<Object>() { @Override public void onSuccess(Object aVoid) { // Called when the model package is successfully downloaded. Log.e(TAG, "onSuccess: download" ); } }).addOnFailureListener(new OnFailureListener() { @Override public void onFailure(Exception e) { // Called when the model package fails to be downloaded. Log.e(TAG, "onSuccess: onFailure "+e.getLocalizedMessage() ); } });; MLLocalModelManager.getInstance() .isModelExist(remoteModel) .addOnSuccessListener(new OnSuccessListener<Boolean>() { @Override public void onSuccess(Boolean isDownload) { Log.e(TAG, "onSuccess:" + isDownload); if (isDownload) { MLModelExecutorSettings settings = new MLModelExecutorSettings.Factory(remoteModel).create(); Log.e(TAG, "onSuccess: 3333"); try { MLModelExecutor modelExecutor = MLModelExecutor.getInstance(settings); Log.e(TAG, "onSuccess: 222"); executorImpl(modelExecutor, getBitmap()); } catch (MLException e) { e.printStackTrace(); Log.e(TAG, "识别失败: " + e.getLocalizedMessage()); } } } }) .addOnFailureListener(new OnFailureListener() { @Override public void onFailure(Exception e) { // 异常处理。 } }); } private Bitmap getBitmap() { // 调用模型推理,实现细节见下节模型推理;Bitmap待处理的图片。 AssetManager assetManager = getAssets(); InputStream is = null; try { is = assetManager.open("img.png"); } catch (IOException e) { e.printStackTrace(); } //以下注释掉的代码不靠谱.若采用,会有异常 //InputStream is = assetManager.open("file:///android_asset/Fresh_01.jpg"); return BitmapFactory.decodeStream(is); } private float[][][][] bitmapToSHUZU640(Bitmap srcBitmap){ // 准备输入数据。 final Bitmap inputBitmap = Bitmap.createScaledBitmap(srcBitmap, max, max, true); final float[][][][] input = new float[1][max][max][3]; for (int i = 0; i < max; i++) { for (int j = 0; j < max; j++) { int pixel = inputBitmap.getPixel(i, j); input[batchNum][j][i][0] = (Color.red(pixel) - 127) / 128.0f; input[batchNum][j][i][1] = (Color.green(pixel) - 127) / 128.0f; input[batchNum][j][i][2] = (Color.blue(pixel) - 127) / 128.0f; } } return input; } private void executorImpl(final MLModelExecutor modelExecutor, Bitmap srcBitmap) { Log.e(TAG, "executorImpl: " ); MLModelInputs inputs = null; try { inputs = new MLModelInputs.Factory().add(bitmapToSHUZU640(srcBitmap)).create(); // 若模型需要多路输入,您需要多次调用add()以便图片数据能够一次输入到推理器。 } catch (MLException e) { // 处理输入数据格式化异常。 Log.e(TAG, "executorImpl: " + e.getLocalizedMessage()); return; }// 执行推理。您可以通过“addOnSuccessListener”来监听推理成功,在“onSuccess”回调中处理推理成功。同时,可以通过“addOnFailureListener”来监听推理失败,在“onFailure”中处理推理失败。 modelExecutor.exec(inputs, inOutSettings).addOnSuccessListener(new OnSuccessListener<MLModelOutputs>() { @Override public void onSuccess(MLModelOutputs mlModelOutputs) { float[][] output = mlModelOutputs.getOutput(0); Log.e(TAG, "onSuccess: " + new Gson().toJson(output)); // 这里推理的返回结果在output数组里,可以进一步处理。 } }).addOnFailureListener(new OnFailureListener() { @Override public void onFailure(Exception e) { // 推理异常。 Log.e(TAG, "onFailure:识别失败原因 "+e.getLocalizedMessage() ); } }); }}
  • [调试调优] MindSpore bert模型训练aicore利用率为0
    MindSpore版本:1.2.0cann:5.0.1.spc1.2模型:bert场景:使用两个形同bert模型对两个词语或短句进行相似度匹配问题:模型跑起来以后aicore资源利用率为0,并且训练一个batch需要花60s以上的时间,如下图说明:代码基本都是自己实现,没有参照码云仓bert模型实现,只有在前向梯度累加部分完全复用MindSpore官网代码,链接:https://www.mindspore.cn/tutorial/training/zh-CN/r1.2/advanced_use/apply_gradient_accumulation.html请社区大佬帮忙看一下具体问题...
  • [数据处理] mindspore自定义数据集加载数据时如何实现类似pytorch中collate_fn的功能
    pytorch的collate_fn函数在把 Map-style dataset 取出的数据整合成 batch 时使用,合并样本列表以形成一个 batch,现在我想在mindspore加载数据时实现类似的功能,该怎么实现呢。
  • [API使用] pytorch中有index_fill_接口若使用.ScatterNdUpdate算子如何替代
    pytorch中有index_fill_接口可以实现沿给定轴dim,将输入索引张量index指定位置的值,进行替换。但是,mindspore得API接口没有直接可以和这个函数接口进行替代得。请问,实现pytorch中得index_fill_接口,在mindspore下应该如何操作?在我的代码里面 ,index_fill算子是这样使用的:best_truth_overlap.index_fill_(0, best_prior_idx, 2) # ensure best priorbest_prior_idx.shape: torch.Size([1])best_truth_overlap.shape: torch.Size([32760])之前我自己使用这样的方式替代:best_truth_overlap[best_prior_idx]=2有位华为工程师告诉我使用ops.ScatterNdUpdate算子替代。但是,我没有理解这个算子应该怎么使用?ops.ScatterNdUpdate()这个算子得indices值给出得是替换得具体位置。但是pytroch 里面只给出了替换得维度,比如第0维还是第1维。像这种情况。ops.ScatterNdUpdate()能使用吗?如果能怎么替换呢?如果不能,哪个mindspore算子可以实现index_fill算子的功能啊???????
  • [数据处理] mindspore自定义数据集返回类型
    有一个疑问,mindspore自定义数据集类的时候,getitem只能返回tuple包裹的numpy数组,这样做是有什么特殊的考虑吗,pytorch对于gititem的返回类型是没有限制,后续对数据的处理也灵活一些。
  • [主题讨论] 计算机专业刚刚开始的小白如何学习MindSpore
    有没有大佬知道啊,教程完全看不懂啊
  • [训练管理] mindspore环境无法解析视频
    【功能模块】【操作步骤&问题现象】地区华为云北京四,mindspore环境用opencv解析不了视频,pytorch可以,我实验必须使用minds pore的环境,提供的1.3,1.5我都试了都是没办法用pytorch结果和mindspore结果截图【截图信息】【日志信息】(可选,上传日志内容或者附件)
  • [API使用] torch.Tensor和torch.LongTensor对应mindspore哪一个接口函数?
    【功能模块】torch.Tensor和torch.LongTensor【操作步骤&问题现象】torch.Tensor和torch.LongTensor在代码中得功能如下:loc_t = torch.Tensor(num, num_priors, 4)conf_t = torch.LongTensor(num, num_priors)通过设置得数值可以在pytorch框架下生成指定shape大小得随机数在pytorch代码中,此处功能模块得作用如下:# match priors (default boxes) and ground truth boxesloc_t = torch.Tensor(num, num_priors, 4)conf_t = torch.LongTensor(num, num_priors)for idx in range(num): truths = targets[idx][:,:-1].data labels = targets[idx][:,-1].data defaults = priors.data match(self.threshold,truths,defaults,self.variance,labels,loc_t,conf_t,idx)if GPU: loc_t = loc_t.cuda() conf_t = conf_t.cuda()# wrap targetsloc_t = Variable(loc_t, requires_grad=False)conf_t = Variable(conf_t,requires_grad=False)pos = conf_t > 0# Localization Loss (Smooth L1)# Shape: [batch,num_priors,4]pos_idx = pos.unsqueeze(pos.dim()).expand_as(loc_data)loc_p = loc_data[pos_idx].view(-1,4)loc_t = loc_t[pos_idx].view(-1,4)【求助内容】请问,在mindspore框架下,有没有实现该功能得接口函数?之前试过,        loc_t = Tensor(np.zeros([num, num_priors, 4]),mindspore.float32)        print("loc_t.shape:",loc_t.shape)        conf_t =Tensor(np.zeros([num, num_priors]),mindspore.int32)        print("conf_t.shape:",conf_t.shape)可是如果这样写得话,pos将会一直是0
  • [特性分析] MindSpore Science科学计算行业套件——MindSpore Elec电磁仿真套件
    当前多种算力正在激发跨领域的应用融合,AI已经成为研究科学计算的新范式。因此我们将MindSpore拓展到科学计算领域。通过多尺度混合计算和高阶混合微分两大关键创新,将MindSpore原有的AI计算引擎升级为AI与科学计算的统一引擎,实现融合的统一加速。在此基础上,我们面向8大科学计算行业打造MindSpore Science【1】系列套件。这些行业套件包含业界领先的数据集、基础模型、预置高精度模型和前后处理工具,加速科学行业应用开发。当前,我们推出面向电子信息行业的MindSpore Elec套件和面向生命科学行业的MindSpore Sponge套件,分别实现了电磁仿真性能提升10倍和生物制药化合物模拟效率提升50%。下面将首先介绍MindSpore Elec套件。 01 MindSpore Elec架构图MindSpore Elec电磁仿真套件主要由前后处理工具(数据构建及转换、结果可视化)、AI电磁模型库(物理方程驱动和标签数据驱动)以及优化策略(数据压缩、动态自适应加权等)等组成,同时支持时域和频域的电磁仿真。以下是具体模块及功能:数据构建及转换:支持CSG(Constructive Solid Geometry,CSG)模式的几何构建,如矩形、圆形等结构的交集、并集和差集,以及cst和stp数据(CST等商业软件支持的数据格式)的高效张量转换。未来还会支持智能网格剖分,供传统科学计算使用。AI电磁模型库:提供物理和数据驱动的AI电磁模型:物理驱动是指网络的训练无需额外的标签数据,只需方程和初边界条件即可;数据驱动是指训练需使用仿真或实验等产生的数据。物理驱动相比数据驱动,优势在于可避免数据生成带来的成本和网格独立性等问题,劣势在于需明确方程的具体表达形式并克服点源奇异性、多任务损失函数以及泛化性等技术挑战。优化策略:为提升物理和数据驱动模型的精度、减少训练的成本,提供了一系列优化措施。数据压缩可以有效地减少神经网络的存储和计算量;多尺度滤波、动态自适应加权可以提升模型的精度,克服点源奇异性等问题;小样本学习主要是为了减少训练的数据量,节省训练的成本。结果可视化:仿真的结果如S参数或电磁场等可保存在CSV、VTK文件中。MindSpore Insight可以显示训练过程中的损失函数变化,并以图片的形式在网页上展示结果;Paraview是第三方开源软件,具有动态展示切片、翻转等高级功能。下面将围绕物理驱动和数据驱动的AI电磁仿真展开介绍。 02 物理驱动的AI电磁仿真 目前具有代表性的物理驱动的AI方法是美国布朗大学George Em Karniadakis教授课题组提出的物理信息神经网络(Physics Informed Deep Learning,PINNs)【2】。PINNs方法的核心是将方程求解转化成优化问题,极大简化了方程的建模和求解过程。但PINNs方法也有其不足的地方,如无法有效处理物理场梯度趋于无穷大的场景,无法解决多个损失函数的优化问题,尤其是数量级差异较大的问题;另外,不具备求解一类方程的能力,当方程中的特征参数(如电磁方程中介电系数等)发生变化时需要重新训练,增加了求解时间,端到端性能相比经典方法优势不是很大。 在MindSpore Elec中,通过高斯分布函数平滑、结合sin激活函数的多尺度残差网络结构以及自适应加权的多任务学习策略,可以有效解决奇异性、多任务损失函数优化难等问题;此外,通过将可变参数进行编码,实现神经网络的增量训练,当参数发生变化时,通过微调可以得到新方程的解。下面将以模拟2D TE波为例,介绍MindSpore Elec求解麦克斯韦方程族的具体流程,2D TE波方程如下:其中,E,H分别表示电场和磁场;ϵ, μ分别是介质的绝对介电常数、绝对磁导率;J(x,t)是电磁仿真过程中的激励源,通常表现为端口脉冲的形式。这在数学意义上近似为狄拉克函数形式所表示的点源,可以表示为:其中x0为激励源位置,g(t)为脉冲信号的函数表达形式。初始条件如下:边界条件为二阶Mur吸收边界条件:a) 高斯分布函数平滑:可以采用光滑的概率分布函数代替不连续的狄拉克函数,从而克服点源奇异性的问题,概率分布函数可以选择高斯分布、柯西分布等。b) 多尺度残差网络+sin激活函数:受上交许志钦老师多尺度【3】工作的启发,采用多尺度残差网络结构以及sin激活函数(正余弦函数与电磁波信号传播形式相契合),可以有效提升网络捕捉多频信号的能力。c) 动态自适应加权:AI计算电磁方程时,各项损失函数表现数量级的差异,导致训练收敛难。其中源项附近的损失函数值最大,其次是非源区域方程的损失函数。e) 实验结果:增量训练的AI方法相比原始的PINNs方法,性能提升15倍以上;与Benchmark(传统的数值方法)的相对误差在5%左右。 03 数据驱动的AI电磁仿真MindSpore Elec提供了基于参数化和点云的数据驱动方法。参数化方案实现的是参数到仿真结果的直接映射,例如天线的宽度、角度作为网络输入,网络输出为S参数。参数化方案的优点是直接映射且网络简单。点云方案实现的是从天线/手机的采样点云到仿真结果的映射,该方案先将手机结构文件转化为点云张量数据,压缩后使用卷积神经网络提取结构特征,再通过数层全连接层映射到最终的仿真结果(S参数)。该方案的优点是适用于复杂工况的结构变化,我们重点介绍下点云的数据驱动方案。a) 从CST文件导出几何/材料信息MindSpore Elec提供两种自动化执行脚本,用于将cst格式文件转换为Python可读取的stp文件,使用该脚本可以实现数据批量转换,实现大规模电磁仿真:基于CST的VBA接口自动调用导出json文件和stp文件:打开CST软件的VBA Macros Editor,导入generate_pointcloud目录下的export_stp.bas文件,将json文件和stp文件路径更改为想要存放的位置,然后点击Run即可导出json文件和stp文件。其中,json文件中包含了模型的端口位置以及stp文件对应的材料信息。对于CST2019或更新的版本,支持使用Python直接调用CST:直接调用generate_pointcloud目录下的export_stp.py文件即可。b)点云数据生成stp文件无法直接作为神经网络的输入,需要先转换为规则的张量数据,MindSpore Elec提供将stp文件高效转化为点云张量数据的接口,generate_pointcloud目录下的generate_cloud_point.py文件提供该接口调用示例。调用时,通过配置stp_path和json_path可以指定用来生成点云的stp和json文件的路径;material_dir指定stp对应的材料信息的路径,材料信息直接在cst软件中导出;sample_nums指定x、y、z三个维度分别生成多少个点云数据;bbox_args用来指定生成点云数据的区域,即(x_min, y_min, z_min, x_max, y_max, z_max)。c) 数据压缩如果点云分辨率设置较高,仅单条点云数据的后处理就需巨大的内存和计算量,因此MindSpore Elec提供数据压缩功能。用户可以调用data_compression目录下的脚本,压缩原始点云数据,该压缩过程分两步:首次使用时需要调用train.py训练压缩模型,若已有压缩模型检查点可以跳过该步。模型训练结束后即可调用data_compress.py进行数据压缩。d) 电磁仿真计算点云数据准备完毕后即可调用MindSpore Elec full_em和S_parameter目录下的电磁仿真模型,实现全量电磁场和S参数的仿真计算,每个仿真过程均可以分为如下两步:调用train.py训练仿真模型。模型训练结束后调用eval.py进行全量电磁场或S参数的仿真计算。e) 实验结果:在手机电磁仿真场景中,仿真精度媲美传统科学计算软件,同时性能提升10倍。 04 总结与展望MindSpore Elec套件已构筑基础的AI电磁仿真能力,并在手机电磁仿真等场景取得技术突破。我们也欢迎广大的科学计算爱好者和研究者加入,共同拓展和维护MindSpore Elec套件。感兴趣的同学可以参考我们在arXiv上面的论文【5,6】。参考文献:[1] https://gitee.com/mindspore/mindscience/tree/master.[2] Maziar Raissi, Paris Perdikaris, and George E Karniadakis. Physics-informed neural networks: A deep learning framework for solving forward and inverse problems involving nonlinear partial differential equations. Journal of Computational Physics, 378:686–707, 2019.[3] Zhi-Qin John Xu, Yaoyu Zhang, Tao Luo, Yanyang Xiao, and Zheng Ma. Frequency principle: Fourier analysis sheds light on deep neural networks. arXiv preprint arXiv:1901.06523, 2019.[4] Alex Kendall, Yarin Gal, and Roberto Cipolla. Multi-task learning using uncertainty to weigh losses for scene geometry and semantics. In Proceedings of the IEEE conference on computer vision and pattern recognition, pages 7482–7491, 2018.[5] Huang X, Liu H, Shi B, et al. Solving Partial Differential Equations with Point Source Based on Physics-Informed Neural Networks[J]. arXiv preprint arXiv:2111.01394, 2021.[6] Huang X, Liu H, Shi B, et al. Meta-Auto-Decoder for Solving Parametric Partial Differential Equations[J]. https://arxiv.org/pdf/2111.08823.pdf.欢迎投稿欢迎大家踊跃投稿,有想投稿的同学,可以添加MindSpore官方小助手:小猫子(mindspore0328)的微信,告诉猫哥哦!昇思MindSpore官方交流QQ群 : 486831414MindSpore官方资料GitHub : https://github.com/mindspore-ai/mindsporeGitee : https : //gitee.com/mindspore/mindspore官方QQ群 : 486831414
  • [算子使用] 如何使用昇思MindSpore自定义优化器
    神经网络的参数众多,我们需要选择合适的算法来进行参数的更新和学习,也就是优化器。优化器在神经网络模型训练的过程中有着十分重要的作用。从SGD开始,神经网络模型优化器就一直在迭代和发展之中。如PyTorch就已经开源了包括SGD、Momentum、RMSprop、Adam、AdamW等等丰富的优化器。但是,由于深度学习模型本身的复杂性,深度学习模型框架自带的优化器本身可能并不能很好的适应我们的任务需求,因此我们有时候需要根据自己的任务去定义合适的优化器,让深度学习模型的优化能更加顺利。 01 动机要写这篇关于《如何使用MindSpore优化器》博客的起因是由于在复现DeppMind的论文《High-Performance Large-Scale Image Recognition Without Normalization》(https://arxiv.org/abs/2102.06171),也就是NFNet。关于NFNet的介绍,大家可以参考博客最强ResNet变体!归一化再见!DeepMind提出NFNet,代码已开源!(https://zhuanlan.zhihu.com/p/350766415)。 02 层归一化的缺陷 在计算机视觉领域,神经网络的训练过程中,虽然诸如BatchNorm(主要用在CNN中,以下称为BN)、LayerNorm(主要用在ViT中,以下称为LN)能够为训练带来很稳定的收敛,在一定程度上可以提高模型的指标,但是两者带来的弊端也是不容忽视的。对于BN层来说,模型最后的精度很容易受到BatchSize的影响,当BatchSize很小的时候,模型最后的指标会发生严重的下降。为了缓解这一现象,研究者们提出了很多替换BN层的归一化方法,目前在ViT中广泛使用的LN。尽管LN具有样本之间的独立性,模型的最后指标几乎不会受到BatchSize的影响,但是LN层的存在却会大大的降低模型的单步训练时长,也可以说:LN本身的一个硬件不友好的operation。  03 自适应梯度裁剪 从作者的论文中可以看到,作者使用了一种名为自适应梯度裁剪的技术,相关的代码可以参见nfnets_pytorch(https://github.com/benjs/nfnets_pytorch/blob/master/nfnets/optim.py)。这里我将结合代码和数学公式对论文里面的自适应梯度裁剪技术进行说明。 """ unitwise_norm的功能: 对于Scalars等只有一个维度的张量:求出其二范数 对于全连接层的权重:对于OxI的矩阵,求出每一个I的2-范数 对于卷积层的权重:OIHW的矩阵,求出每一个IHW的F范数(其实也就是flatten的2-范数) """ def unitwise_norm(x): if (len(torch.squeeze(x).shape)) <= 1: # Scalars, vectors # NFNet中的缩放因子,squeeze之后实际上只有一个维度 axis = 0 keepdims = False elif len(x.shape) in [2,3]: # Linear layers # Original code: IO # Pytorch: OI axis = 1 keepdims = True elif len(x.shape) == 4: # Conv kernels # Original code: HWIO # Pytorch: OIHW axis = [1, 2, 3] keepdims = True else: raise ValueError(f'Got a parameter with len(shape) not in [1, 2, 3, 4]! {x}') return torch.sqrt(torch.sum(torch.square(x), axis=axis, keepdim=keepdims))d_p = p.grad # ========================= # Gradient clipping if clipping is not None: """对于训练权重p,按照unitwise_norm函数求出对应权重的2-范数,限定eps为最小阈值""" param_norm = torch.maximum(unitwise_norm(p), torch.tensor(eps).to(p.device)) """对于训练权重p的梯度,按照unitwise_norm函数求出对应梯度的2-范数,这里不加阈值""" grad_norm = unitwise_norm(d_p) """这里给定一个 clipping 缩放系数 """ max_norm = param_norm * group['clipping'] """梯度的对应的范数 和 权重对应的缩放范数""" trigger_mask = grad_norm > max_norm """截断的梯度= 梯度 * 权重对应的缩放函数 / max(梯度范数, 最小阈值)""" clipped_grad = p.grad * (max_norm / torch.maximum(grad_norm, torch.tensor(1e-6).to(p.device))) """ 最后输出梯度: 如果 梯度范数 > 缩放后的权重范数 梯度 = 截断梯度 否则 梯度 = 本身不变 """ d_p = torch.where(trigger_mask, clipped_grad, d_p) """这里猜测,加入最小一直可能只是为了防止最后的输出接近0,导致出现一些特别大或者特别小的数字, 在理解具具体公式的时候,我们可以暂时不管这两个eps"""通过理解公式我们可以简单理解为,当梯度的范数大于一定程度的权重范数时,梯度会进行缩放,反之梯度保持为原来的值,这就是其中自适应三个字的由来。 04 如何用MindSpore自定义优化器并且实现AGC_SGD 要用MindSpore优雅的实现AGC_SGD(主要是想尽量少用for循环,尽量接近MindSpore 的原生程序风格),我们首先要来了解一下MindSpore优化器的整体优化器的优化构建。以下的代码片段有一些跳跃,希望大家紧跟思路,抓住核心的代码片段就好。 mindspore.nn.optim.Momentum MindSpore的优化器整体定义和其他框架类似,都是从一个Optimizer基类继承来的op。在PyTorch中SGD似乎是和Momentum写在一起的,因此这里拿Momentum为例。对于Momentum而言,以下的Momentum优化器的更新准则(暂时忽略weightdecay) 首先是得到 $$当前梯度动量 = 过去动量 \times 动量系数 + (1-动量系数)\times当前的梯度$$ 用公式表示也就是: $$v{t} = v_{t-1} \ast u + (1-u)\times gradients$$ 当use_nesterov=False的时候,就完成单步的更新,同时,相对应的动量也会得到更新。关于use_nesterov参数,感兴趣的读者可以查找牛顿动量(Nesterov)算法 If use_nesterov is True: $$p{t} = p{t-1} - (grad \times lr + v_{t} \times u\times lr) $$ If usenesterov is False: $$p{t} = p{t-1} - lr \times v{t}$$ 可以看到,在更新的过程中,我们既可以等到为下一步准备的当前动量,也可以完成最后参数的更新,以此往复,不断更新参数。 class Momentum(Optimizer): def __init__(self, params, learning_rate, momentum, weight_decay=0.0, loss_scale=1.0, use_nesterov=False): super(Momentum, self).__init__(learning_rate, params, weight_decay, loss_scale) Validator.check_value_type("momentum", momentum, [float], self.cls_name) if isinstance(momentum, float) and momentum < 0.0: raise ValueError("momentum should be at least 0.0, but got momentum {}".format(momentum)) self.momentum = Parameter(Tensor(momentum, mstype.float32), name="momentum") self.params = self.parameters self.use_nesterov = Validator.check_bool(use_nesterov) # 复制一份和self.params一样形状的参数,初始化为0,用来作为保存动量的副本 self.moments = self.params.clone(prefix="moments", init='zeros') self.hyper_map = C.HyperMap() # 动量优化器,实施算法 self.opt = _selected_ops.ApplyMomentum(use_nesterov=self.use_nesterov)在优化器的运行阶段,我们可以看到,大体上就是执行了权重衰减梯度缩放(估计是为了配合缩放器加的参数,假定scale=1.,我们传入优化器前就对混合精度完成缩放,可以看到我之前的如何使用MindSpore进行自定义训练)梯度中心化(这个功能默认是False,op里面不会去执行,因此不用太过于在意)完成单步更新(group_lr是指针对不同的参数使用不同的lr,感兴趣的可以尝试) params = self.params moments = self.moments gradients = self.decay_weight(gradients) gradients = self.scale_grad(gradients) gradients = self.gradients_centralization(gradients) lr = self.get_lr() if self.is_group_lr: success = self.hyper_map(F.partial(_momentum_opt, self.opt, self.momentum), lr, gradients, params, moments, self.ps_parameters, self.cache_enable) else: success = self.hyper_map(F.partial(_momentum_opt, self.opt, self.momentum, lr), gradients, params, moments, self.ps_parameters, self.cache_enable) return success在Momentum中,我们可以看到一个if的逻辑判断,从大体上就可以了解到,估计就是判断是不是优化器中包含过往的动量信息,_ps_pull和_ps_push操作估计就是为了保存下过去的缓存信息。_momentum_opt = C.MultitypeFuncGraph("momentum_opt") @_momentum_opt.register("Function", "Tensor", "Tensor", "Tensor", "Tensor", "Tensor", "Bool", "Bool") def _tensor_run_opt_ext(opt, momentum, learning_rate, gradient, weight, moment, ps_parameter, cache_enable): """Apply momentum optimizer to the weight parameter using Tensor.""" if ps_parameter and not cache_enable: op_shape = P.Shape() _ps_pull = P.Pull() _ps_push = P.Push("ApplyMomentum", []) shapes = (op_shape(learning_rate), op_shape(gradient), op_shape(momentum)) success = F.depend(True, _ps_pull(_ps_push((learning_rate, gradient, momentum), shapes), weight)) else: success = F.depend(True, opt(weight, moment, learning_rate, gradient, momentum)) return success使用MindSpore实现AGC经过上面的介绍,我们可以了解到,实际上在MindSpore实现AGC_SGD,只要在Momentum里面增加一个AGC操作就可以了。以下是代码,这里就不过多赘述了。需要注意的是,在MindSpore中,静态图运行下对于数值的操作都是需要使用C.MultitypeFuncGraph函数构建功能图的。关于这个Op大致就是构建了一个可以用在map函数里面的一个function,然后对变量实行统一操作。目前这个op里面如果包含if的话,最后一定要配上else这种显式的完备逻辑,否则程序会报错的。(就是和PyTorch不咋一样的原因,功能上不影响)_agc_clip = C.MultitypeFuncGraph("agc_clip") @_agc_clip.register("Tensor", "Tensor", "Tensor", "Tensor", "Tensor", "Tensor", "Bool") def _tensor_run_agc_ext(eps, min_grad, clipping, grad_norm, gradient, weight_norm, clip): """Apply sgd optimizer to the weight parameter using Tensor.""" if clip: param_norm = ops.Maximum()(weight_norm, eps) max_norm = param_norm * clipping trigger_mask = ops.Greater()(grad_norm, max_norm) clipped_grad = gradient * (max_norm / ops.Maximum()(grad_norm, min_grad)) gradient = mnp.where(trigger_mask, clipped_grad, gradient) return gradient _unitwise_norm = C.MultitypeFuncGraph("unitwise_norm") @_unitwise_norm.register("Tensor") def unitwise_norm_solve(x): if (len(ops.Squeeze()(x).shape)) <= 1: # Scalars, vectors axis = 0 keepdims = False elif len(x.shape) in [2, 3]: # Linear layers # Original code: IO # Pytorch: OI axis = 1 keepdims = True else: # Conv kernels # Original code: HWIO # Pytorch: OIHW axis = (1, 2, 3) keepdims = True return ops.Sqrt()(ops.ReduceSum(keepdims)(ops.Square()(x), axis))另外,根据作者的意思,最后一层的全连接层的weight权重是不需要使用AGC为好,因此我们可以构建 self.group_clipping_tuple: List[Bool,]对其进行针对name的判断。在优化器中,为了针对不同的权重(weight、bias、gamma、beta),针对权重衰减的weight_decay也会根据参数的顺序转化成一个列表进行权重和weight_decay的一一映射关系。具体的代码可以参见Optimizer源码(https://www.mindspore.cn/docs/api/zh-CN/r1.3/_modules/mindspore/nn/optim/optimizer.html#Optimizer)。 self.group_clipping_tuple = [] for index, params in enumerate(self.group_params): name = params.name if not "attn_last" in name and "fc" in name and 'bias' not in name: print(f"{name} no clipping") self.group_clipping_tuple.append(False) else: self.group_clipping_tuple.append(True) self.group_clipping_tuple = tuple(self.group_clipping_tuple) assert len(self.group_clipping_tuple) == self.param_length至此,我们就依赖于MindSpore本身自带的Momentum函数,完成了MindSpore实现AGC_SGD的功能,代码过段时间我会提交MindSpore官方的model仓库,感兴趣的小伙伴到时候可以自取。 05 昇思MindSpore社区贡献活动 昇思MindSpore社区贡献活动,主要就是使用昇思MindSpore复现顶会论文,既能掌握昇思MindSpore框架的使用,为昇思MindSpore开源社区建设出力,还可以收获超值奖品。活动应该会持续到2021年年底,大家快来参加吧! 活动链接: https://bbs.huaweicloud.com/forum/thread-86967-1-1.html 昇思MindSpore官方交流QQ群 : 486831414 MindSpore官方资料GitHub : https://github.com/mindspore-ai/mindsporeGitee : https : //gitee.com/mindspore/mindspore官方QQ群 : 486831414
  • [应用实践] 昇思MindSpore NLP模型迁移之Bert模型—文本匹配任务(二):训练和评估
    在上一节中,我们已经将Bert模型迁移到MindSpore上。在这节中,我将会介绍如何使用MindSpore的Bert模型来做下游任务:lcqmc的文本匹配任务。主机环境:系统:ubuntu18GPU:3090MindSpore版本:1.3数据集:lcqmclcqmc文本匹配任务的定义:哈工大文本匹配数据集,LCQMC 是哈尔滨工业大学在自然语言处理国际顶会 COLING2018 构建的问题语义匹配数据集,其目标是判断两个问题的语义是否相同。数据集中的字段分别如下:text_a, text_b, label。其中text_a和text_b为两个问题的文本。若两个问题的语义相同则label为1,否则为0。权重迁移PyTorch->MindSpore由于官网已经提供了微调好的权重信息,所以我们尝试直接转换权重进行预测。我们先要知道模型权重名称以及形状等,需要PyTorch与MindSpore模型一一对应。首先,我们将huggingface的bert-chinese-base的torch bin文件下载下来。接下来使用下面的函数将Torch权重参数文件转化为MindSpore权重参数文件def torch_to_ms(model, torch_model,save_path): """ Updates mobilenetv2 model mindspore param's data from torch param's data. Args: model: mindspore model torch_model: torch model """ print("start load") # load torch parameter and mindspore parameter torch_param_dict = torch_model ms_param_dict = model.parameters_dict() count = 0 for ms_key in ms_param_dict.keys(): ms_key_tmp = ms_key.split('.') if ms_key_tmp[0] == 'bert_embedding_lookup': count+=1 update_torch_to_ms(torch_param_dict, ms_param_dict, 'embeddings.word_embeddings.weight', ms_key) elif ms_key_tmp[0] == 'bert_embedding_postprocessor': if ms_key_tmp[1] == "token_type_embedding": count+=1 update_torch_to_ms(torch_param_dict, ms_param_dict, 'embeddings.token_type_embeddings.weight', ms_key) elif ms_key_tmp[1] == "full_position_embedding": count+=1 update_torch_to_ms(torch_param_dict, ms_param_dict, 'embeddings.position_embeddings.weight', ms_key) elif ms_key_tmp[1] =="layernorm": if ms_key_tmp[2]=="gamma": count+=1 update_torch_to_ms(torch_param_dict, ms_param_dict, 'embeddings.LayerNorm.weight', ms_key) else: count+=1 update_torch_to_ms(torch_param_dict, ms_param_dict, 'embeddings.LayerNorm.bias', ms_key) elif ms_key_tmp[0] == "bert_encoder": if ms_key_tmp[3] == 'attention': par = ms_key_tmp[4].split('_')[0] count+=1 update_torch_to_ms(torch_param_dict, ms_param_dict, 'encoder.layer.'+ms_key_tmp[2]+'.'+ms_key_tmp[3]+'.' +'self.'+par+'.'+ms_key_tmp[5], ms_key) elif ms_key_tmp[3] == 'attention_output': if ms_key_tmp[4] == 'dense': print(7) count+=1 update_torch_to_ms(torch_param_dict, ms_param_dict, 'encoder.layer.' + ms_key_tmp[2] + '.attention.output.'+ms_key_tmp[4]+'.'+ms_key_tmp[5], ms_key) elif ms_key_tmp[4]=='layernorm': if ms_key_tmp[5]=='gamma': print(8) count+=1 update_torch_to_ms(torch_param_dict, ms_param_dict, 'encoder.layer.' + ms_key_tmp[2] + '.attention.output.LayerNorm.weight', ms_key) else: count+=1 update_torch_to_ms(torch_param_dict, ms_param_dict, 'encoder.layer.' + ms_key_tmp[2] + '.attention.output.LayerNorm.bias', ms_key) elif ms_key_tmp[3] == 'intermediate': count+=1 update_torch_to_ms(torch_param_dict, ms_param_dict, 'encoder.layer.' + ms_key_tmp[2] + '.intermediate.dense.'+ms_key_tmp[4], ms_key) elif ms_key_tmp[3] == 'output': if ms_key_tmp[4] == 'dense': count+=1 update_torch_to_ms(torch_param_dict, ms_param_dict, 'encoder.layer.' + ms_key_tmp[2] + '.output.dense.'+ms_key_tmp[5], ms_key) else: if ms_key_tmp[5] == 'gamma': count+=1 update_torch_to_ms(torch_param_dict, ms_param_dict, 'encoder.layer.' + ms_key_tmp[2] + '.output.LayerNorm.weight', ms_key) else: count+=1 update_torch_to_ms(torch_param_dict, ms_param_dict, 'encoder.layer.' + ms_key_tmp[2] + '.output.LayerNorm.bias', ms_key) if ms_key_tmp[0] == 'dense': if ms_key_tmp[1] == 'weight': count+=1 update_torch_to_ms(torch_param_dict, ms_param_dict, 'pooler.dense.weight', ms_key) else: count+=1 update_torch_to_ms(torch_param_dict, ms_param_dict, 'pooler.dense.bias', ms_key) save_checkpoint(model, save_path) print("finish load") def update_bn(torch_param_dict, ms_param_dict, ms_key, ms_key_tmp): """Updates mindspore batchnorm param's data from torch batchnorm param's data.""" str_join = '.' if ms_key_tmp[-1] == "moving_mean": ms_key_tmp[-1] = "running_mean" torch_key = str_join.join(ms_key_tmp) update_torch_to_ms(torch_param_dict, ms_param_dict, torch_key, ms_key) elif ms_key_tmp[-1] == "moving_variance": ms_key_tmp[-1] = "running_var" torch_key = str_join.join(ms_key_tmp) update_torch_to_ms(torch_param_dict, ms_param_dict, torch_key, ms_key) elif ms_key_tmp[-1] == "gamma": ms_key_tmp[-1] = "weight" torch_key = str_join.join(ms_key_tmp) update_torch_to_ms(torch_param_dict, ms_param_dict, 'transformer.' + torch_key, ms_key) elif ms_key_tmp[-1] == "beta": ms_key_tmp[-1] = "bias" torch_key = str_join.join(ms_key_tmp) update_torch_to_ms(torch_param_dict, ms_param_dict, 'transformer.' + torch_key, ms_key) def update_torch_to_ms(torch_param_dict, ms_param_dict, torch_key, ms_key): """Updates mindspore param's data from torch param's data.""" value = torch_param_dict[torch_key].cpu().numpy() value = Parameter(Tensor(value), name=ms_key) _update_param(ms_param_dict[ms_key], value) def _update_param(param, new_param): """Updates param's data from new_param's data.""" if isinstance(param.data, Tensor) and isinstance(new_param.data, Tensor): if param.data.dtype != new_param.data.dtype: print("Failed to combine the net and the parameters for param %s.", param.name) msg = ("Net parameters {} type({}) different from parameter_dict's({})" .format(param.name, param.data.dtype, new_param.data.dtype)) raise RuntimeError(msg) if param.data.shape != new_param.data.shape: if not _special_process_par(param, new_param): print("Failed to combine the net and the parameters for param %s.", param.name) msg = ("Net parameters {} shape({}) different from parameter_dict's({})" .format(param.name, param.data.shape, new_param.data.shape)) raise RuntimeError(msg) return param.set_data(new_param.data) return if isinstance(param.data, Tensor) and not isinstance(new_param.data, Tensor): if param.data.shape != (1,) and param.data.shape != (): print("Failed to combine the net and the parameters for param %s.", param.name) msg = ("Net parameters {} shape({}) is not (1,), inconsistent with parameter_dict's(scalar)." .format(param.name, param.data.shape)) raise RuntimeError(msg) param.set_data(initializer(new_param.data, param.data.shape, param.data.dtype)) elif isinstance(new_param.data, Tensor) and not isinstance(param.data, Tensor): print("Failed to combine the net and the parameters for param %s.", param.name) msg = ("Net parameters {} type({}) different from parameter_dict's({})" .format(param.name, type(param.data), type(new_param.data))) raise RuntimeError(msg) else: param.set_data(type(param.data)(new_param.data)) def _special_process_par(par, new_par): """ Processes the special condition. Like (12,2048,1,1)->(12,2048), this case is caused by GE 4 dimensions tensor. """ par_shape_len = len(par.data.shape) new_par_shape_len = len(new_par.data.shape) delta_len = new_par_shape_len - par_shape_len delta_i = 0 for delta_i in range(delta_len): if new_par.data.shape[par_shape_len + delta_i] != 1: break if delta_i == delta_len - 1: new_val = new_par.data.asnumpy() new_val = new_val.reshape(par.data.shape) par.set_data(Tensor(new_val, par.data.dtype)) return True return False实际应用案例如下:import BertConfig import BertModel as ms_bm import BertModel as tc_bm bert_config_file = "./model/test.yaml" bert_config = BertConfig.from_yaml_file(bert_config_file) model = ms_bm(bert_config, False) torch_model = tc_bm.from_pretrained("/content/model/bert_cn") torch_to_ms(model, torch_model.state_dict(),"./model/bert2.ckpt")这里名称一定要一一对应。如果后期改动了模型,也需要在检查一下这个转换函数是否能对应。下游任务:lcqmc文本匹配任务训练1、封装Bert为bert_embeding首先我们先将之前构建好的Bert再进行一步封装为bert_embeding# Copyright 2021 Huawei Technologies Co., Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ============================================================================ """Bert Embedding.""" import logging from typing import Tuple import mindspore.nn as nn from mindspore import Tensor from mindspore.train.serialization import load_checkpoint, load_param_into_net import BertModel, BertConfig logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') logger = logging.getLogger(__name__) class BertEmbedding(nn.Cell): """ This is a class that loads pre-trained weight files into the model. """ def __init__(self, bert_config: BertConfig, is_training: bool = False): super(BertEmbedding, self).__init__() self.bert = BertModel(bert_config, is_training) def init_bertmodel(self, bert): """ Manual initialization BertModel """ self.bert = bert def from_pretrain(self, ckpt_file): """ Load the model parameters from checkpoint """ param_dict = load_checkpoint(ckpt_file) load_param_into_net(self.bert, param_dict) def construct(self, input_ids: Tensor, token_type_ids: Tensor, input_mask: Tensor) -> Tuple[Tensor, Tensor]: """ Returns the result of the model after loading the pre-training weights Args: input_ids (:class:`mindspore.tensor`):A vector containing the transformation of characters into corresponding ids. token_type_ids (:class:`mindspore.tensor`):A vector containing segemnt ids. input_mask (:class:`mindspore.tensor`):the mask for input_ids. Returns: sequence_output:the sequence output . pooled_output:the pooled output of first token:cls.. """ sequence_output, pooled_output, _ = self.bert(input_ids, token_type_ids, input_mask) return sequence_output, pooled_output2、下游任务:BertforSequenceClassification将Bert作为预训练模型,接着在Bert的基础上,取Bert的cls token的embeding作为输入,输入到全连接网络中,这就是BertforSequenceClassification。