• [技术干货] es6 数组去重
    一行代码实现数组去重(ES6) ES6中新增了Set数据结构,类似于数组,但是 它的成员都是唯一的 ,其构造函数可以接受一个数组作为参数,如: let array = [1, 1, 1, 1, 2, 3, 4, 4, 5, 3];  let set = new Set(array);  console.log(set);  // => Set {1, 2, 3, 4, 5} ES6中Array新增了一个静态方法Array.from,可以把类似数组的对象转换为数组,如通过querySelectAll方法得到HTML DOM Node List,以及ES6中新增的Set和Map等可遍历对象,如: let set = new Set();  set.add(1).add(2).add(3);  let array = Array.from(set);  console.log(array);  // => [1, 2, 3] 于是,现在我们可以用一行代码实现数组去重了:let array = Array.from(new Set([1, 1, 1, 2, 3, 2, 4])); console.log(array); // => [1, 2, 3, 4] 附:ES5实现数组去重var array = [1, '1', 1, 2, 3, 2, 4]; var tmpObj = {}; var result = []; array.forEach(function(a) {   var key = (typeof a) + a;   if (!tmpObj[key]) {     tmpObj[key] = true;     result.push(a);   } }); console.log(result); // => [1, "1", 2, 3, 4]
  • [技术干货] 【CANN训练营学习笔记】:再看resnet50推理应用
    尝试解读resnet50推理应用的开源样例的代码逻辑,结合文档了解每个函数到底做了什么事情。开源样例的代码仓链接:https://gitee.com/ascend/samples/tree/master/cplusplus/level2_simple_inference/1_classification/resnet50_imagenet_classification 文档内有对应用开发流程的介绍,样例代码也会遵守这个流程,该流程适用于基于CANN的推理应用的开发。这个过程中,调用AscendCL提供的接口,间接控制计算硬件完成数据搬运、计算等操作,最终完成整个推理过程。文档中也给出了main函数的代码逻辑,具体的样例代码可能会有不同,但需要做的事情是相似的 main.cpp第10-12行,包含了C++标准库的iostream,包含了样例内的头文件sample_process.h和utils.h,相关函数可以在inc目录下这两个文件中找到描述。main第18行,创建SampleProcess类的一个实例,名为sampleProcessmain第19行,调用对象sampleProcess的方法InitResource(),进行“AscendCL初始化”和“运行管理资源申请”,返回Result类型的变量retmain第25行,调用对象sampleProcess的方法Process(),进行模型加载、模型推理及数据后处理、模型卸载,返回ret在main函数结束时,注销对象sampleProcess过程中,会进行“运行管理资源释放”和“AscendCL去初始化” 一、首先看sampleProcess对象被创建时有哪些初始值在头文件sample_process.h中对SampleProcess类的声明信息为可知SampleProcess类具有构造函数SampleProcess()、析构函数~SampleProcess();公有函数InitResource(),用于初始化资源;公有函数Process();私有函数DestroyResource();私有属性deviceId_、context_、stream_,这里的下划线一般表明该属性为私有变量在src目录下的sample_process.cpp内可以看到SampleProcess类具体的定义构造函数SampleProcess(),表明在实例化SampleProcess类的对象时,对私有属性的初始化:deviceId_初始化为0,context_初始化为空指针,stream_初始化为空指针 二、再看InitResource()函数是怎么进行AscendCL初始化和运行管理资源申请的InitResource函数第30行,定义了指针变量aclConfigPath,指向字符串,字符串为acl.json的路径InitResource函数第31行,调用函数aclInit(),传入刚才定义的aclConfigPath作为参数,该函数声明在acl/acl.h中,可在CANN文档中查找相关说明。应用开发(C&C++)\AscendCL API参考\系统配置\aclInitInitResource函数第39行,调用aclrtSetDevice()函数,传入变量deviceId_作为参数,由前面的构造函数可知,deviceId_被初始化为0;该函数同样声明在acl/acl.h中,可在CANN文档中查找相关说明。应用开发(C&C++)\AscendCL API参考\Device管理\aclrtSetDevice InitResource函数第47行,调用aclrtCreateContext()函数,传入&context_和deviceId_作为参数,其中context_在实例化对象时被初始化为空指针;该函数同样声明在acl/acl.h中,可在CANN文档中查找相关说明。应用开发(C&C++)\AscendCL API参考\Context管理\aclrtCreateContext(就不贴文档截图了) InitResource函数第56行,调用aclrtCreateStream()函数,传入&stream_作为参数,其中stream_在实例化对象时被初始化为空指针;该函数同样声明在acl/acl.h中,可在CANN文档中查找相关说明。应用开发(C&C++)\AscendCL API参考\Stream管理\aclrtCreateStream InitResource函数第67行,创建了aclrtRunMode类型的变量runMode,该数据类型对应文档:应用开发(C&C++)\AscendCL API参考\数据类型及其操作接口\aclrtRunMode可知,该数据类型为枚举类型,其取值可以是ACL_DEVICE或ACL_HOST InitResource函数第68行,调用aclrtGetRunMode()函数,传入&runMode作为参数,其中runMode就是刚才创建的枚举类型的变量;该函数自动获取当前昇腾AI软件栈运行在HOST侧还是DEVICE侧,并将结果存储在枚举变量。对应文档:应用开发(C&C++)\AscendCL API参考\Device管理\aclrtGetRunMode InitResource函数第73行,根据获取到的runMode,创建一个布尔类型的g_isDevice供后续调用方便。 三、main函数中InitResource()函数之后就是Process()函数了,这里面涵盖的过程就多一些了,模型加载、模型推理及数据后处理、模型卸载。Process()函数第81行,创建了ModelProcess类的一个实例,名为modelProcess。在inc/model_process.h文件内有对ModelProcess类的声明。包含各种后面会用到的公有成员函数,以及私有成员变量。创建实例时自动执行构造函数,在src/model_process.cpp文件描述,为私有成员变量赋初始值。 3.1 模型加载Process()函数第82-83行,调用LoadModel()函数,传入模型路径信息,加载模型LoadModel()第36-39行,先检查私有成员变量loadFlag_,看是否已经加载过模型,构造实例时赋值为false表明没有加载过模型;LoadModel()第72行,在完成加载过程后,会将该变量赋值为true,示意模型已加载。 LoadModel()第40行,调用aclmdlQuerySize()函数,传入模型路径modelPath,传入私有成员变量modelWorkSize_以及modelWeightSize_;获取模型对内存的需求并存入私有成员变量中,具体内容可参考文档:应用开发(C&C++)\AscendCL API参考\模型加载与执行\aclmdlQuerySize获取模型对应权重所需内存的大小,存入变量modelWeightSize_中;获取模型所需工作内存的大小,存入变量modelWorkSize_中;注意文档中“函数原型”对参数数据类型的要求。 LoadModel()第48行,调用aclrtMalloc()函数,在Device侧申请内存。参考文档:应用开发(C&C++)\AscendCL API参考\内存管理\aclrtMalloc传入的第二个参数是刚才获取的执行模型所需的工作内存的大小modelWorkSize_,作为本次内存申请的大小;传入的第三个参数是内存分配的规则,通过文档可知,本次执行的内存分配规则ACL_MEM_MALLOC_HUGE_FIRST,有限申请大页内存;传入的第一个参数,私有成员变量modelWorkPtr_,用于存放申请下来的工作内存的地址。除了要注意文档“函数原型”中对数据类型的要求之外,可以通过“参数说明”中的“输入/输出”列,理解函数的功能,由于C++只能return一个变量,想要输出更多变量的话,就可以使用这种“改变参数”的做法,标为“输出”的参数一般在函数执行之前都只是一个初始值,在函数执行之后就具有了各自的意义。 LoadModel()第57行,再次调用aclrtMalloc()函数,也是申请Device侧的内存,但是这次是为模型权重申请内存,请参考本帖上一段。 LoadModel()第64-65行,调用aclmdlLoadFromFileWithMem()函数,虽然函数名很长,但是从函数名以及参数名大概可以猜测这个函数的功能,猜测为“将模型文件加载到内存中”,具体还是要参考文档:应用开发(C&C++)\AscendCL API参考\模型加载与执行\aclmdlLoadFromFileWithMem从“函数功能”来看,将模型文件加载到自行申请的内存上,加载完成后返回模型ID。该函数共有5个标为“输入”的参数,1个标为“输出”的参数,这个“输出”参数就是模型ID,对应私有成员变量modelId_;其他的“输入”参数依次是om文件的路径信息、申请下来的工作内存的地址、工作内存的大小、申请下来的权重内存的地址、权重内存的大小。这里我猜测,该函数就是在进行内存搬运,将模型文件从HOST侧搬运到DEVICE侧,具体细节仍需参考该函数的实现细节。 3.2 创建模型描述符Process()函数第89行,调用CreateModelDesc()函数,创建模型描述符CreateModelDesc()第79行,调用aclmdlCreateDesc()函数,创建模型描述符,参考文档:(文档内也有检索框)应用开发(C&C++)\AscendCL API参考\数据类型及其操作接口\aclmdlDesc\aclmdlCreateDesc该函数创建了一个aclmdlDesc类型的数据,并将其赋值给私有成员变量modelDesc_,这类最后加下划线“_”的变量一般都是私有成员变量,可以到“类”的构造函数里查看初始值为空指针。这里只是创建了一个空的模型描述符,但是还没有赋予具体的信息。 CreateModelDesc()第85行,调用aclmdlGetDesc()函数,为模型描述符填充具体的信息,参考文档:应用开发(C&C++)\AscendCL API参考\数据类型及其操作接口\aclmdlDesc\aclmdlGetDesc该函数传入模型ID,即之前加载模型是得到的modelId_,输出模型的具体信息,并赋值给上一步创建的模型描述符modelDesc_。文档中“函数原型”还可以看到模型描述符的数据类型是aclmdlDesc,点开之后可以看到一些列函数,名称基本为aclmdlGetXX,可用于从模型描述符中读取具体的信息,比如模型输入输出的个数、维度等,具体内容请参考文档。 3.3 读取待分类图片,并传到Device侧Process()函数第95-98行,指定了待分类图片的路径,如果想分类自己上传的图片,在这里需要做相应的调整。 Process()函数第102行,调用GetInputSizeByIndex()函数,该函数定义在src/moedel_process.cpp下该函数主要调用aclmdlGetInputSizeByIndex函数,从模型描述信息中获取“模型输入”所需内存大小,单位为Byte,参考文档:应用开发(C&C++)\AscendCL API参考\数据类型及其操作接口\aclmdlDesc\aclmdlGetInputSizeByIndex该函数传入的参数包括模型描述符modelDesc_、要查询的是第几个输入index=0,resnet50只有1个输入;函数的返回值是获取的输入所需内存大小,单位是Byte,保存在变量devBufferSize中,供后续调用。 Process()函数第108行,调用aclrtMalloc函数,为待分类图片申请Device侧的内存。申请的内存大小为上一步获取的devBufferSize,申请到的内存地址保存在变量picDevBuffer上,内存分配规则ACL_MEM_MALLOC_NORMAL_ONLY,具体内容请参考文档。 Process()函数第115行,调用CreateInput函数,该函数传入了模型输入所需的内存大小,以及Device侧申请到的内存的地址,该函数定义在src/ moedel_process.cpp下CreateInput第119行,检查模型描述符是否为空指针,即检查是否已经加载模型CreateInput第123-127行,获取模型描述符modelDesc_的第0个输入所需的内存大小,并与传入参数进行比较,即检查“申请下来的内存大小”是否符合“模型输入所需内存”的要求。 CreateInput第129行,调用aclmdlCreateDataset()函数,创建了aclmdlDataset类型的变量,赋值给私有变量input_,用于描述模型推理时的输入数据,这里只是创建一个变量,后续会赋予这个变量具体的意义,参考文档:应用开发(C&C++)\AscendCL API参考\数据类型及其操作接口\aclmdlDataset\aclmdlCreateDataset CreateInput第135行,调用aclCreateDataBuffer()函数,创建了aclDataBuffer类型的变量,赋值给变量inputData,该函数传入“模型输入在Device侧的内存地址”以及“模型输入在Device侧所占内存的大小”,该函数返回的变量用于描述内存地址和内存大小,可以理解为将内存信息转换为aclDataBuffer类型。参考文档:应用开发(C&C++)\AscendCL API参考\数据类型及其操作接口\aclDataBuffer\aclCreateDataBuffer CreateInput第141行,调用aclmdlAddDatasetBuffer()函数,向前面创建的aclmdlDataset类型的变量中添加aclDataBuffer信息,传入描述内存信息的变量inputData,传入描述模型输入的变量input_,将内存信息添加到input_中。 至此,CreateInput函数将待分类图片的内存信息转换成模型推理所需的aclmdlDataset类型的input_变量 Process()函数第122行,调用CreateOutput函数,该函数定义在src/ moedel_process.cpp下CreateOutput函数第170-173行,检查模型是否已加载CreateOutput函数第175行,调用aclmdlCreateDataset()函数,和CreateInput函数类似,也创建了一个aclmdlDataset类型的变量output_,用于描述模型的输出,这里暂时也只是创建一个空变量。 CreateOutput函数第181行,调用aclmdlGetNumOutputs()函数,从模型描述符modelDesc_中读取该模型的输出个数,并赋值给变量outputSize,resnet50模型具有1个输出。后面的for循环是对模型的每个输出,依次添加信息到output_变量内。 CreateOutput函数第183行,调用aclmdlGetOutputSizeByIndex()函数,获取模型第i个输出的大小,单位为Byte,赋值给变量modelOutputSize,参考文档:应用开发(C&C++)\AscendCL API参考\数据类型及其操作接口\aclmdlDesc\aclGetOutputSizeByIndex CreateOutput函数第186行,调用aclrtMalloc函数,为第i个模型输出申请Device侧的内存,将申请到的内存的地址信息赋值给变量outputBuffer。申请内存的函数已多次出现,请参考本帖之前的内容以及文档。 CreateOutput函数第193行,调用aclCreateDataBuffer函数,与CreateInput函数中类似,这里也是传入内存大小和内存地址,创建一个aclDataBuffer类型的变量,用于描述模型输出的内存信息,赋值给变量outputData CreateOutput函数第200行,调用aclmdlAddDatasetBuffer函数,与CreateInput函数中类似,这里也是将模型输出的内存信息outputData送入aclmdlDataset类型的变量output_中。 至此,随着for循环的进行,如果模型包括多个输出,则会依次传入到output_中。 Process()函数第129行,开始for循环,这里将遍历testFile中的每个待分类图片,对每个图片执行依次模型推理 Process()函数第132行,调用Utils::MemcpyFileToDeviceBuffer()函数,传入参数包括待分类图片的文件路径、之前在Device侧申请到的内存的地址、申请到的内存的大小,并将数据搬运到Device侧申请下来的内存上。该函数定义在src/utils.cpp文件内MemcpyFileToDeviceBuffer()第73行,调用Utils::ReadBinFile函数,读取二进制文件,该函数同样定义在src/utils.cpp文件内ReadBinFile第30行,创建std::ifstream类的一个实例,名为binFile,并以二进制格式打开该文件ReadBinFile第36行,调用seekg函数,将输入文件流的指针指向binFile.end的位置,或者说是从该位置开始偏移0个单位,即指向文件流结束的位置。ReadBinFile第37行,调用tellg函数,获取输入文件流指针当前位置,并赋值给binFileBufferLen,由于当前指针指向文件末尾,所以该信息可以描述文件的大小。随后又调用seekg函数将指针重新指向文件开头。ReadBinFile第46-62行,根据昇腾AI软件栈的运行模式,申请内存。如果运行在Host侧,则调用aclrtMallocHost函数在Host侧申请内存。ReadBinFile第63-65行,调用read函数,将数据读取到Host侧,Host侧内存首地址为inputBuff,共读取binFileBufferLen个单位。 ReadBinFile()函数在Host侧申请一段内存,并将图片以二进制的方式读取到该内存,供后续模型推理使用。 MemcpyFileToDeviceBuffer()第78-86行,比较“读到Host侧的内存大小”fileSize和“模型推理所支持的输入图片的大小”inputBufferSize,检查二者是否相等,如果不相等,则表明读取到Host侧的待分类图片的大小不满足模型推理对输入图片大小的要求。 MemcpyFileToDeviceBuffer()第87-104行,根据昇腾AI软件栈的运行模式,用恰当的方式执行数据搬运。如果运行在Device上,则调用aclrtMemcpy函数,将数据从Host侧搬运到Device侧。参考文档:应用开发(C&C++)\AscendCL API参考\内存管理\aclrtMemcpy该函数传入的最后一个参数指定了数据搬运的类型ACL_MEMCPY_HOST_TO_DEVICE,即将数据从Host侧复制到Device侧,要完成这项复制,需要指定源内存地址inputBuff、源内存大小inputBuffSize、目标内存地址picDevBuffer、目标内存大小inputBuffSize。 至此,Utils:MemcpyFileToDeviceBuffer函数实现:将待分类图片读取到Host内存,从Host内存搬运到Devce内存。 3.4 定义推理函数,执行推理Process()函数第139行,调用Execute()函数,执行推理,该函数定义在modelProcess.cpp文件内Execute函数第352行,调用aclmdlExecute()函数,传入参数包括:模型ID-modelId_、模型输入input_、模型输出output_。可参考文档:应用开发(C&C++)\AscendCL API参考\模型加载与执行\aclmdlExecute 3.5 定义推理结果数据处理函数,打印top5识别结果Process()函数第148行,调用OutputModelResult()函数,打印识别结果,该函数定义在modelProcess.cpp文件内OutputProcess函数内包含一个for循环,与前面为output_添加信息时的for循环类似,这里遍历output_的每一个输出,对每个输出都执行相同的数据处理。resnet50只有1个输出。 OutputProcess函数第277行,调用aclmdlGetDatasetBuffer函数,获取output_中的第i个aclDataBuffer,并赋值给dataBuffer,参考文档:应用开发(C&C++)\AscendCL API参考\数据类型及其操作接口\aclmdlGetDatasetBuffer OutputProcess函数第278行,调用aclGetDataBufferAddr函数,获取aclDataBuffer类型的变量dataBuffer中的内存地址,并赋值给data。这个过程与前面创建output_的过程刚好互补。参考文档:应用开发(C&C++)\AscendCL API参考\数据类型及其操作接口\aclDataBuffer\aclGetDataBuffer OutputProcess函数第279行,调用aclGetDataSizeV2函数,获取aclDataBuffer类型的变量dataBuffer中的内存大小,并赋值给len。参考文档:应用开发(C&C++)\AscendCL API参考\数据类型及其操作接口\aclDataBuffer\aclGetDataBufferSizeV2 OutputProcess函数第284-303行,根据昇腾AI软件栈的运行模式,执行恰当的数据搬运。如果是Host模式,需要先调用aclrtMallocHost函数,在Host侧申请内存,用于存放推理结果,将申请到的内存地址信息交给outHostData,要申请的内存大小为上一步获取的内存大小len。然后调用aclrtMemcpy函数,将模型输出数据从Device侧搬运到Host侧。最后调用reinterpret_cast函数,将Host侧的内存outHostData上的数据重新解释成float数据类型,并赋值给outData。 OutputProcess函数第304行,创建一个容器resultMap,其中将容纳键值对Key-Value,其中Key是float类型的,Value是unsigned int类型,容器默认按升序排列,这里指定按照greater<float>的方式排序,即降序排列。 OutputProcess函数第305-308行,这个for循环将推理结果依次插入到容器resultMap中。resnet50的推理结果是一系列连续排列的float数据,对应类别0-999的置信度,这里用推理结果的内存大小和单个float的内存大小确定类别数量,再将每个float类型的置信度作为Key送入容器resultMap中,其对应的Value为类别的索引。 OuputProcess函数第310-318行,这个for循环按顺序遍历容器resultMap中的Key-Value,并将Key最大的前5个结果打印成日志。 OutputProcess函数第319-325行,调用aclrtFreeHost函数,将之前用于存放推理结果的Host侧的内存释放掉。参考文档:应用开发(C&C++)\AscendCL API参考\内存管理\aclrtFreeHost 至此,OutputModelResult函数实现了对推理结果的处理,打印了Top5识别结果。 3.6 卸载模型、释放内存、去初始化Process()函数第152-155行,调用了相关函数,释放资源,分析方法与之前类似,可参考文档自行解读。 四、总结以上内容,尝试对开源样例 resnet50推理应用 进行分析,了解推理应用的代码逻辑有助于在遇到报错时分析报错原因与解决方法,对创建自定义推理应用也有一定的参考意义。
  • [技术干货] 一行代码实现数组去重(ES6)
     ES6中新增了Set数据结构,类似于数组,但是 它的成员都是唯一的 ,其构造函数可以接受一个数组作为参数,如: let array = [1, 1, 1, 1, 2, 3, 4, 4, 5, 3];  let set = new Set(array);  console.log(set);  // => Set {1, 2, 3, 4, 5} ES6中Array新增了一个静态方法Array.from,可以把类似数组的对象转换为数组,如通过querySelectAll方法得到HTML DOM Node List,以及ES6中新增的Set和Map等可遍历对象,如: let set = new Set();  set.add(1).add(2).add(3);  let array = Array.from(set);  console.log(array);  // => [1, 2, 3] 于是,现在我们可以用一行代码实现数组去重了:let array = Array.from(new Set([1, 1, 1, 2, 3, 2, 4])); console.log(array); // => [1, 2, 3, 4] 附:ES5实现数组去重var array = [1, '1', 1, 2, 3, 2, 4]; var tmpObj = {}; var result = []; array.forEach(function(a) {   var key = (typeof a) + a;   if (!tmpObj[key]) {     tmpObj[key] = true;     result.push(a);   } }); console.log(result); // => [1, "1", 2, 3, 4]
  • [技术干货] 循环
     sum (...m) {  //...m根据函数调用时的具体参数个数      let total = 0      for (var i in m) {        total += i      }      console.log(total)    }JavaScript 原有的for...in循环,只能获得对象的键名,不能直接获取键值。ES6 提供for...of循环,允许遍历获得键值。var arr = ['a', 'b', 'c', 'd']; for (let a in arr) {   console.log(a); // 0 1 2 3 } for (let a of arr) {   console.log(a); // a b c d } 上面代码表明,for...in循环读取键名,for...of循环读取键值for...in循环有几个缺点。数组的键名是数字,但是for...in循环是以字符串作为键名“0”、“1”、“2”等等。for...in循环不仅遍历数字键名,还会遍历手动添加的其他键,甚至包括原型链上的键。某些情况下,for...in循环会以任意顺序遍历键名。总之,for...in循环主要是为遍历对象而设计的,不适用于遍历数组。for...of循环相比上面几种做法,有一些显著的优点。for (let value of myArray) {   console.log(value); } 有着同for...in一样的简洁语法,但是没有for...in那些缺点。不同于forEach方法,它可以与break、continue和return配合使用。提供了遍历所有数据结构的统一操作接口。
  • [新手课堂] 结构体的作用
    结构体和其他类型基础数据类型一样,例如 int 类型,char 类型 只不过结构体可以做成你想要的数据类型。以方便日后的使用。 在实际项目中,结构体是大量存在的。研发人员常使用结构体来封装一些属性来组成新的类型。  结构体在函数中的作用不是简便,其最主要的作用就是封装。封装的好处就是可以再次利用。让使用者不必关心这个是什么,只要根据定义使用就可以了。
  • [问题求助] 【appcube产品】【对象字段】对象新增数组字段
    【功能模块】【操作步骤&问题现象】1、对象新增时需要新增数组字段2、【截图信息】【日志信息】(可选,上传日志内容或者附件)
  • [技术干货] 选择排序(c语言)[转载]
    一、什么是选择排序? 选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理是:第一次从待排序的中数据元素选出最小(或最大)的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小(大)元素,然后放到已排序的序列的末尾。以此类推,直到全部待排序的数据元素的个数为零。选择排序是不稳定的排序方法。二、选择排序思路首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。三、排序过程例:定义一个数组 int a[8] = {9,3,7,2,6,1,5,8},要求利用选择排序的方法将数组从小到大排序。排序的次数:因为每排好一个元素,那么所需要排的元素个数减一,直到排到倒数第二个元素停止,将倒数第二个元素也排好后,整体数组排序就完成了。所以排序的次数 = 元素个数 - 1。(冒泡排序的排序次数与该排序的排序次数计算方法相同)9,3,7,2,6,1,5,8第一次排序:假设首元素作为整体元素数据最小值,然后从该元素的后一个元素开始每个元素都与该最小值进行比较,假如有比该元素小的值,就用一个变量去记住下标值,最后比较完成后,把两个元素互换位置即可。第一次排序结果:{1,3,7,2,6,9,5,8}1,3,7,2,6,9,5,8第二次排序:因为第一次排序选择的是将首元素作为最小值,最终经过互换位置,首元素排序完成,第二次排序就不需要排序首元素,只需要排序除首元素以外的元素,然后在依照第一次排序的原理进行排序。第二次排序结果:{1,2,7,3,6,9,5,8}然后根据第一次排序和第二次排序的原理,最终的排序结果为:{1,2,3,5,6,7,8,9}四、代码的实现#include <stdio.h> void arr_out(int a[8])//输出函数{    int i = 0;    for(i = 0;i < 8;i++)    {        printf("%d ",a[i]);    }    printf("\n");} void arr_sort(int *p,int n){    int i,j;    int min = 0;    for(i = 0;i < n - 1;i++)//排序次数    {        min = i;        for(j = i + 1;j < n;j++)        {            if(p[j] < p[min])            {                min = j;//记录交换的元素下标值            }        }        if(i != min)        {            int temp = p[i];            p[i] = p[min];            p[min] = temp;        }      }} int main(){    int a[8] = {0};    int i = 0;    for(i = 0;i < 8;i++)    {        scanf("%d",&a[i]);    }     arr_sort(a,8);//排序函数    arr_out(a);//输出函数     return 0;}原文链接:https://blog.csdn.net/m0_59083833/article/details/123971321
  • [技术干货] STM32+OLED显示屏制作指针式电子钟
    ## 一、硬件环境介绍 **单片机:** STM32F103C8T6 **RTC时钟来源:** 使用STM32内部RTC定时器得到时间。 **显示屏:** 中景园0.96寸 SPI接口的OLED显示屏 **编程软件:** keil5 ![image.png](https://bbs-img.huaweicloud.com/blogs/img/20220706/1657111232899138627.png) ## 二、实现效果图与代码技术部分介绍 ### 2.1 实现的效果图 ![image.png](https://bbs-img.huaweicloud.com/blogs/img/20220706/1657111248242826743.png) ![image.png](https://bbs-img.huaweicloud.com/blogs/img/20220706/1657111261817786638.png) ### 2.2 **代码技术部分介绍** 核心代码内容里分为以下几个部分: (1) RTC时钟代码部分:该电子钟使用的是STM32内部RTC实时时钟,需要编写RTC初始化代码。 (2) 电子钟界面逻辑代码部分:电子钟的界面仪表盘画面更新是在RTC秒中断里调用,实现时间指针的更新。 (3) OLED驱动代码部分:编写OLED驱动代码,编写常用的OLED接口函数,比如:字符串显示,画点,划线等。 OLED在程序的驱动方式采用显存的方式驱动,定义一个显存数组,程序里的所有逻辑代码先绘制在显存数组里,然后再刷新到OLED显示屏上。 ## 三、核心代码 ### 3.1 OLED显示屏驱动代码 **(1). oled.h** ```cpp #ifndef _OLED_H #define _OLED_H #include "stm32f10x.h" #include "delay.h" #include "sys.h" #include extern const u8 ChineseFont_16_16[][32]; extern const u8 ChineseFont_24_24[][24*24/8]; extern const u8 ASCII_8_16[][16]; extern const u8 bmp_foot_18_29_1[]; extern const u8 bmp_foot_17_32_2[]; extern const u8 ASCII_12_24[][12*3]; /* 初始化OLED显示屏硬件 硬件连接: D0--PB14--时钟线 D1--PB13--数据线 RES-PB12-复位脚 DC--PB1--命令数据选择脚 CS--PA7--片选 */ #define OLED_SCK PBout(14) #define OLED_MOSI PBout(13) #define OLED_RES PBout(12) #define OLED_DC PBout(1) #define OLED_CS PAout(7) //定义命令 #define OLED_WRITE_CMD 0 #define OLED_WRITE_DAT 1 //函数声明 void OLED_SPI_WriteOneByte(u8 data,u8 flag); void OLED_Init(void); void OLED_Clear(u8 data); void OLED_SetPos(u8 x,u8 y); void OLED_DisplayPoint(u8 x,u8 y,u8 c); void OLED_DisplayData(u8 x,u8 y,u8 w,u8 h,u8 *p); void OLED_WriteGRAM(void); void OLED_DrawLine(u16 x1, u16 y1, u16 x2, u16 y2); //两点画线 void OLED_DrawAngleLine(u32 x,u32 y,float du,u32 len,u32 w,u8 c);//角度画线 void OLED_DrawRectangle(u16 x1, u16 y1, u16 x2, u16 y2); //矩形 void OLED_Circle(u16 x0,u16 y0,u8 r); //圆 void OLED_DrawAngleLine2(u32 x,u32 y,int du,u32 len,u8 c); void OLED_ShowString(u8 x,u8 y,u8 h,char *str); void OLED_ShowChineseFont(u8 x,u8 y,u8 size,u8 number); extern u8 OLED_GRAM[8][128]; //8行128列---->8页128列 #endif ``` ### 3.2 rtc驱动代码 **(1) rtc.c** ```cpp #include "rtc.h" //定义RTC标准结构体 struct RTC_CLOCK rtc_clock; /* 函数功能: RTC初始化函数 */ void RTC_Init(void) { if(BKP->DR1!=0xAB) //表示RTC第一次初始化 { //1. 备份寄存器时钟 RCC->APB1ENR|=127; //备份时钟接口 RCC->APB1ENR|=128; //电源时钟接口 PWR->CR|=18; //允许写入RTC和后备寄存器 //2. 配置RTC时钟源 RCC->BDCR|=10; //开启外部32.768K时钟 while(!(RCC->BDCR&11)){} //等待时钟就绪 RCC->BDCR&=~(0x38); //清空时钟配置 RCC->BDCR|=0x18; //选择外部32.768K时钟 //3. 配置RTC核心寄存器 RCC->BDCR|=115; //开启RTC时钟 while(!(RTC->CRL&15)){} //判断上一次寄存器是否写完成 RTC->CRL|=14; //进入配置模式 RTC->PRLH=0; //预分频高位 RTC->PRLL=0x7FFF; //32767 预分频低位 RTC->CNTH=0; //计数器高位 RTC->CNTL=0; //计数器低位 RTC->ALRH=0; //闹钟寄存器高位 RTC->ALRL=60; //闹钟寄存器低位 RTC->CRL&=~(14);//退出配置模式 while(!(RTC->CRL&15)){} //判断上一次寄存器是否写完成 BKP->DR1=0xAB; //表示配置成功了 } RTC->CRH|=10; //秒中断 RTC->CRH|=11; //闹钟中断 STM32_SetPriority(RTC_IRQn,2,2); //优先级 } extern void Update_FrameShow(void); /* 函数功能: RTC闹钟中断服务函数 */ void RTC_IRQHandler(void) { u32 SecCnt; if(RTC->CRL&10) { SecCnt=RTC->CNTH16;//获取高位 SecCnt|=RTC->CNTL; //获取低位 RTC_GetTime(SecCnt); //转换标准时间 RTC_GetWeek(SecCnt); // printf("%d-%d-%d %d:%d:%d week:%d\n",rtc_clock.year,rtc_clock.mon,rtc_clock.day,rtc_clock.hour,rtc_clock.min,rtc_clock.sec,rtc_clock.week); Update_FrameShow(); //更新显示 RTC->CRL&=~(10); //清除秒中断标志位 } if(RTC->CRL&11) { // printf("闹钟时间到达!....\n"); // BEEP=1; // DelayMs(500); // BEEP=0; RTC->CRL&=~(11); //清除闹钟中断标志位 } } //闰年的月份 static int mon_r[12]={31,29,31,30,31,30,31,31,30,31,30,31}; //平年的月份 static int mon_p[12]={31,28,31,30,31,30,31,31,30,31,30,31}; /* 函数功能: 设置RTC时间 函数形参: u32 year; 2018 u32 mon; 8 u32 day; u32 hour; u32 min; u32 sec; */ void RTC_SetTime(u32 year,u32 mon,u32 day,u32 hour,u32 min,u32 sec) { u32 i; u32 SecCnt=0; //总秒数 /*1. 累加已经过去的年份*/ for(i=2017;i/基准年份:20170101000000 { if(RTC_GetYearState(i)) { SecCnt+=366*24*60*60; //闰年一年的秒数 } else { SecCnt+=365*24*60*60; //平年一年的秒数 } } /*2. 累加过去的月份*/ for(i=0;i/闰年一月的秒数 } else { SecCnt+=mon_p[i]*24*60*60; //平年一月的秒数 } } /*3. 累加过去的天数*/ SecCnt+=(day-1)*24*60*60; /*4. 累加过去小时*/ SecCnt+=hour*60*60; /*5. 累加过去的分钟*/ SecCnt+=min*60; /*6. 累加过去的秒*/ SecCnt+=sec; /*7. 设置RTC时间*/ RCC->APB1ENR|=127; //备份时钟接口 RCC->APB1ENR|=128; //电源时钟接口 PWR->CR|=18; //允许写入RTC和后备寄存器 while(!(RTC->CRL&15)){} //判断上一次寄存器是否写完成 RTC->CRL|=14; //进入配置模式 RTC->CNTH=SecCnt>>16; //计数器高位 RTC->CNTL=SecCnt&0xFFFF; //计数器低位 RTC->CRL&=~(14);//退出配置模式 while(!(RTC->CRL&15)){} //判断上一次寄存器是否写完成 } /* 函数功能: 获取RTC时间 函数参数: u32 sec 秒单位时间 */ void RTC_GetTime(u32 sec) { u32 i; rtc_clock.year=2017; //基准年份 /*1. 计算当前的年份*/ while(1) { if(RTC_GetYearState(rtc_clock.year)) { if(sec>=366*24*60*60) //够一年 { sec-=366*24*60*60; rtc_clock.year++; } else break; } else { if(sec>=365*24*60*60) //够一年 { sec-=365*24*60*60; rtc_clock.year++; } else break; } } /*2. 计算当前的月份*/ rtc_clock.mon=1; for(i=0;i12;i++) { if(RTC_GetYearState(rtc_clock.year)) { if(sec>=mon_r[i]*24*60*60) { sec-=mon_r[i]*24*60*60; rtc_clock.mon++; } else break; } else { if(sec>=mon_p[i]*24*60*60) { sec-=mon_p[i]*24*60*60; rtc_clock.mon++; } else break; } } /*3. 计算当前的天数*/ rtc_clock.day=1; while(1) { if(sec>=24*60*60) { sec-=24*60*60; rtc_clock.day++; } else break; } /*4. 计算当前的小时*/ rtc_clock.hour=0; while(1) { if(sec>=60*60) { sec-=60*60; rtc_clock.hour++; } else break; } /*5. 计算当前的分钟*/ rtc_clock.min=0; while(1) { if(sec>=60) { sec-=60; rtc_clock.min++; } else break; } /*6. 计算当前的秒*/ rtc_clock.sec=sec; } /* 函数功能: 判断年份是否是平年、闰年 返回值 : 0表示平年 1表示闰年 */ u8 RTC_GetYearState(u32 year) { if((year%4==0&&year%100!=0)||year%400==0) { return 1; } return 0; } /* 函数功能: 获取星期 */ void RTC_GetWeek(u32 sec) { u32 day1=sec/(60*60*24); //将秒单位时间转为天数 switch(day1%7) { case 0: rtc_clock.week=0; break; case 1: rtc_clock.week=1; break; case 2: rtc_clock.week=2; break; case 3: rtc_clock.week=3; break; case 4: rtc_clock.week=4; break; case 5: rtc_clock.week=5; break; case 6: rtc_clock.week=6; break; } } ``` **(2) rtc.h** ```cpp #ifndef RTC_H #define RTC_H #include "stm32f10x.h" #include "sys.h" #include "usart.h" #include "delay.h" //定时RTC时钟结构体 struct RTC_CLOCK { u32 year; u32 mon; u32 day; u32 hour; u32 min; u32 sec; u32 week; }; extern struct RTC_CLOCK rtc_clock; //函数声明 void RTC_Init(void); u8 RTC_GetYearState(u32 year); void RTC_GetTime(u32 sec); void RTC_GetWeek(u32 sec); void RTC_SetTime(u32 year,u32 mon,u32 day,u32 hour,u32 min,u32 sec); #endif ``` **3.3 界面绘制部分代码-主函数** ```cpp #include "stm32f10x.h" #include "led.h" #include "delay.h" #include "key.h" #include "usart.h" #include #include "timer.h" #include "oled.h" #include "rtc.h" /* 函数功能: 绘制时钟表盘框架 */ void DrawTimeFrame(void) { u8 i; OLED_Circle(32,32,31);//画外圆 OLED_Circle(32,32,1); //画中心圆 //画刻度 for(i=0;i60;i++) { if(i%5==0)OLED_DrawAngleLine(32,32,6*i,31,3,1); } OLED_WriteGRAM(); //刷新数据到OLED屏幕 } /* 函数功能: 更新时间框架显示,在RTC中断里调用 */ char TimeBuff[20]; void Update_FrameShow(void) { /*1. 绘制秒针、分针、时针*/ OLED_DrawAngleLine2(32,32,rtc_clock.sec*6-6-90,27,0);//清除之前的秒针 OLED_DrawAngleLine2(32,32,rtc_clock.sec*6-90,27,1); //画秒针 OLED_DrawAngleLine2(32,32,rtc_clock.min*6-6-90,24,0); OLED_DrawAngleLine2(32,32,rtc_clock.min*6-90,24,1); OLED_DrawAngleLine2(32,32,rtc_clock.hour*30-6-90,21,0); OLED_DrawAngleLine2(32,32,rtc_clock.hour*30-90,21,1); //绘制电子钟时间 sprintf(TimeBuff,"%d",rtc_clock.year); OLED_ShowString(65,16*0,16,TimeBuff); //年份字符串 OLED_ShowChineseFont(66+32,16*0,16,4); //显示年 sprintf(TimeBuff,"%d/%d",rtc_clock.mon,rtc_clock.day); OLED_ShowString(75,16*1,16,TimeBuff); //月 if(rtc_clock.sec==0)OLED_ShowString(65,16*2,16," "); //清除多余的数据 sprintf(TimeBuff,"%d:%d:%d",rtc_clock.hour,rtc_clock.min,rtc_clock.sec); OLED_ShowString(65,16*2,16,TimeBuff); //秒 //显示星期 OLED_ShowChineseFont(70,16*3,16,5); //星 OLED_ShowChineseFont(70+16,16*3,16,6); //期 OLED_ShowChineseFont(70+32,16*3,16,rtc_clock.week+7); //具体的值 } int main() { LED_Init(); BEEP_Init(); KEY_Init(); USART1_Init(115200); TIMER1_Init(72,20000); //超时时间20ms USART2_Init(9600);//串口-蓝牙 TIMER2_Init(72,20000); //超时时间20ms USART3_Init(115200);//串口-WIFI TIMER3_Init(72,20000); //超时时间20ms USART1_Printf("正在初始化WIFI请稍等.\n"); RTC_Init(); //RTC初始化 OLED_Init(); OLED_Clear(0x00); //清屏 RTC_SetTime(2020,7,15,10,52,10); DrawTimeFrame(); while(1) { } } ```
  • [技术干货] memset的用法详解[转载]
    memset是一个初始化函数,作用是将某一块内存中的全部设置为指定的值。void *memset(void *s, int c, size_t n); 1s指向要填充的内存块。c是要被设置的值。n是要被设置该值的字符数。返回类型是一个指向存储区s的指针。需要说明的几个地方一、不能任意赋值memset函数是按照字节对内存块进行初始化,所以不能用它将int数组出初始化为0和-1之外的其他值(除非该值高字节和低字节相同)。其实c的实际范围应该在0~255,因为memset函数只能取c的后八位给所输入范围的每个字节。也就是说无论c多大只有后八位二进制是有效的。=================================================================================================对于int a[4];memset(a, -1, sizeof(a)) 与 memset(a, 511, sizeof(a)) 所赋值的结果一样都为-1:因为 -1 的二进制码为(11111111 11111111 11111111 11111111);511 的二进制码为(00000000 00000000 00000001 11111111);后八位均为(11111111),所以数组中的每个字节都被赋值为(11111111)。注意int占四个字节,例如a[0]的四个字节都被赋值为(11111111),那么a[0](11111111 11111111 11111111 11111111),即a[0] = -1。二、注意所要赋值的数组的元素类型先来看两个例子:例一:对char类型的数组a初始化,设置元素全为’1’int main(){    char a[4];    memset(a,'1',4);    for(int i=0; i<4; i++){        cout<<a[i]<<" ";    }    return 0;}例二:对int类型的数组a初始化,设置元素值全为1int main(){    int a[4];    memset(a,1,sizeof(a));    for(int i=0; i<4; i++){        cout<<a[i]<<" ";    }    return 0;}1、首先要说明的第一点 对于第二个程序,数组a是整型的,一般int所占内存空间为4个字节,所以在使用memset赋值时,下面的语句是错误的:int a[4];memset(a,1,4);由于memset函数是以字节为单位进行赋值的,所以上述代码是为数组a的前4个字节进行赋值,那么所得到的执行结果就只能是:正确的memset语句应为:memset(a,1,16); //int所占内存为4字节的情况memset(a,1,sizeof(a));至于为什么不是预期得到的1,将在下面的第二点进行说明。当然,不同的机器上int的大小可能不同,所以最好用sizeof()函数。2、为什么第一个程序可以正确赋值1而第二个不可以?这就又回到了刚刚说的第一个问题,memset函数中只能取c的后八位赋给每个字节。第一个程序中,数组a是字符型的,字符型占据的内存大小就是1Byte,而memset函数也是以字节为单位进行赋值的,所以输出正确。第二个程序中,数组a是整型的,整型占据的内存大小为4Byte,而memset函数还是按照字节为单位进行赋值,将1(00000001)赋给每一个字节。那么对于a[0]来说,其值为(00000001 00000001 00000001 00000001),即十进制的16843009。关于所要赋值的字符数的写法先来看一个示例:#include<bits/stdc++.h>using namespace std;void fun1(int a[]){    memset(a,-1,sizeof(a)); }int main(){    int a[6];    fun1(a);    for(int i=0; i<6; i++){        cout<<a[i]<<" ";    }    return 0;}当数组作为参数传递时,其传递的实际上是一个指针,这个指针指向数组的首地址,如果用sizeof(a)函数得到的只是指针的长度,而不是数组的长度。解决方案:在函数中加入数组长度参数,在传递前先获取数组长度,然后将数组长度作为参数传递进去。#include<bits/stdc++.h>using namespace std;void fun1(int a[], int len){    memset(a,-1,len); }int main(){    int a[6];    int len = sizeof(a);    fun1(a,len);    for(int i=0; i<6; i++){        cout<<a[i]<<" ";    }    return 0;}具体用法实例初始化数组char str[100];memset(str,0,100);清空结构体类型的变量typedef struct Stu{    char name[20];    int cno;}Stu;Stu stu1; memset(&stu1, 0 ,sizeof(Stu));Stu stu2[10]; //数组memset(stu2, 0, sizeof(Stu)*10);此外,如果结构体中有数组的话还是需要对数组单独进行初始化处理的。————————————————版权声明:本文为CSDN博主「薛定谔的猫ovo」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。原文链接:https://blog.csdn.net/weixin_44162361/article/details/115790452
  • [技术专区] 11111111111111
    MySQL
  • [技术干货] 《王道》数据结构笔记整理2022[转载]
    第一章:绪论1.1数据结构的基本概念1.数据:数据是信息的载体,是描述客观事物属性的数、字符以及所有能输入到计算机中并被程序识别和处理的符号的集合。2.数据元素:数据元素是数据的基本单位,通常作为一个整体进行考虑和处理。一个数据元素可由若干数据项组成,数据项是构成数据元素的不可分割的最小单位。例如,学生记录就是一个数据元素,它由学号、姓名、性别等数据项组成。3.数据对象:数据对象是具有相同性值的数据元素的集合,是数据的一个子集。4.数据类型:数据类型是一个值的集合和定义再此集合上的一组操作的总称。1)原子类型。其值不可再分的数据类型。如bool 和int 类型。2)结构类型。其值可以再分解为若干成分(分量)的数据类型。3)抽象数据类型。抽象数据组织及与之相关的操作。5.数据结构:数据结构是相互之间存在一种或多种特定关系的数据元素的集合。1.2数据结构的三要素1.数据的逻辑结构:逻辑结构是指数据元素之间的逻辑关系,即从逻辑关系上描述数据。逻辑结构包括:集合结构:结构中的数据元素之间除“同属一个集合”外,别无其它关系。线性结构:结构中的数据元素之间只存在一对一的关系,除了第一个元素,所有元素都有唯一前驱;除了最后一个元素,所有元素都有唯一后继。树形结构:结构中数据元素之间存在一对多的关系。图状结构:数据元素之间是多对多的关系。2.数据的存储结构(物理结构):存储结构是指数据结构在计算机中的表示(又称映像),也称物理结构。存储结构包括:顺序存储:把逻辑上相邻的元素存储在物理位置也相邻的存储单元中,元素之间的关系由存储单元的邻接关系来体现。链式存储:逻辑上相邻的元素在物理位置上可以不相邻,借助指示元素存储地址的指针来表示元素之间的逻辑关系。索引存储:在存储元素信息的同时,还建立附加的索引表,索引表中的每项称为索引项,索引项的一般形式是(关键字,地址)散列存储:根据元素的关键字直接计算出该元素的存储地址,又称哈希(Hash)存储。3.数据的运算:施加在数据上的运算包括运算的定义何实现。运算的定义是针对逻辑结构的,指出运算的功能;运算的实现是针对存储结构的,指出运算的具体操作步骤。1.3算法的基本概念程序=数据结构+算法算法(algorithm)是对特定问题求解步骤的一种描述,它是指令的有限序列,其中的每条指令表示一个或多个操作。算法的特性:1.有穷性:一个算法必须总在执行有穷步之后结束,且每一步都可在有穷时间内完成。2.确定性:算法中每条指令必须有确定的含义,对于相同的输入只能得到相同的输出。3.可行性:算法中描述的操作都可以通过已经实现的基本运算执行有限次来实现。4.输入:一个算法有零个或多个输入,这些输入取自于某个特定的对象的集合。5.输出:一个算法有一个多个输出,这些输出是与输入有着某种特定关系的量。好的算法达到的目标:正确性:算法应能够正确的求接问题。可读性:算法应具有良好的可读性,以帮助人们理解。健壮性:输入非法数据时,算法能适当地做出反应或进行处理,而不会产生莫名奇妙地输出结果。效率与低存储量需求:效率是指算法执行的时间,存储量需求是指算法执行过程中所需要的最大存储空间,这两者都与问题的规模有关。1.4算法的时间复杂度一般情况下,算法中基本操作重复执行的次数是问题规模n的某个函数f(n),算法的时间量度记作T(n)=O(n),它表示随问题规模n的增大而增大,算法执行时间的增长率和f(n)的增长率相同,称作算法的渐近时间复杂度,简称时间复杂度。1.5算法的空间复杂度算法的空间复杂度S(n)定义为该算法所耗费的存储空间,它是问题规模n的函数。记为S(n)=O(g(n))。第二章:线性表2.1线性表的定义线性表是具有相同数据类型的n(n>0)个数据元素的有限序列,其中n为表长,当n=0时线性表是一个空表。2.2顺序表的定义2.2.1静态分配://顺序表的实现--静态分配#include<stdio.h>#define MaxSize 10   //定义表的最大长度 typedef struct{    int data[MaxSize];//用静态的"数组"存放数据元素    int length; //顺序表的当前长度  }SqList;        //顺序表的类型定义(静态分配方式) void InitList(SqList &L){     for(int i=0;i<MaxSize;i++){         L.data[i]=0;  //将所有数据元素设置为默认初始值               }     L.length=0;}int main(){    SqList L;//声明一个顺序表    InitList(L);//初始化一个顺序表    for(int i=0;i<MaxSize;i++){        printf("data[%d]=%d\n",i,L.data[i]);    }    return 0; }2.2.2动态分配//顺序表的实现——动态分配#include<stdio.h>#include<stdlib.h>//malloc、free函数的头文件 #define InitSize 10 //默认的最大长度typedef struct{    int  *data;//指示动态分配数组的指针    int MaxSize; //顺序表的最大容量    int length;  //顺序表的当前长度 }SeqList; //初始化void InitList(SeqList &L){    //用malloc 函数申请一片连续的存储空间    L.data =(int*)malloc(InitSize*sizeof(int)) ;    L.length=0;    L.MaxSize=InitSize;} //增加动态数组的长度void IncreaseSize(SeqList &L,int len){    int *p=L.data;    L.data=(int*)malloc((L.MaxSize+len)*sizeof(int));    for(int i=0;i<L.length;i++){        L.data[i]=p[i];   //将数据复制到新区域     }    L.MaxSize=L.MaxSize+len; //顺序表最大长度增加len    free(p);  //释放原来的内存空间     } int main(void){    SeqList L; //声明一个顺序表    InitList(L);//初始化顺序表    IncreaseSize(L,5);    return 0; }顺序表的特点:随机访问 ,可以在O(1)时间内找到第i个元素。存储密度高,每个节点只存储数据元素拓展容量不方便插入、删除操作不方便,需要移动大量元素2.2顺序表的基本操作1.插入操作 :平均时间复杂度O(n)bool ListInsert(SqList &L, int i, int e){     //判断i的范围是否有效    if(i<1||i>L.length+1)         return false;    if(L.length>MaxSize) //当前存储空间已满,不能插入          return false;    for(int j=L.length; j>i; j--){    //将第i个元素及其之后的元素后移        L.data[j]=L.data[j-1];    }    L.data[i-1]=e;  //在位置i处放入e    L.length++;      //长度加1    return true;}2.删除操作:平均时间复杂度O(n)bool LisDelete(SqList &L, int i, int &e){ // e用引用型参数     //判断i的范围是否有效    if(i<1||i>L.length)         return false;    e = L.data[i-1]    //将被删除的元素赋值给e    for(int j=L.length; j>i; j--){    //将第i个后的元素前移        L.data[j-1]=L.data[j];    }    L.length--;      //长度减1    return true;}3.按位查找(获取L表中第i个位置的值):平均时间复杂度O(1)#define MaxSize 10            //定义最大长度 typedef struct{    ElemType data[MaxSize];  //用静态的“数组”存放数据元素     int Length;              //顺序表的当前长度}SqList;                     //顺序表的类型定义ElemType GetElem(SqList L, int i){    // ...判断i的值是否合法    return L.data[i-1];      //注意是i-1}4.按值查找:平均时间复杂度O(n)#define InitSize 10            //定义最大长度 typedef struct{    ElemTyp *data;  //用静态的“数组”存放数据元素     int Length;              //顺序表的当前长度}SqList;   //在顺序表L中查找第一个元素值等于e的元素,并返回其位序int LocateElem(SqList L, ElemType e){    for(int i=0; i<L.lengthl i++)        if(L.data[i] == e)              return i+1;     //数组下标为i的元素值等于e,返回其位序i+1    return 0;               //推出循环,说明查找失败}2.3线性表的链式表示2.3.1 单链表的定义定义: 线性表的链式存储又称单链表,它是指通过一组任意的存储单元来存储线性表中的数据元素。typedef struct LNode{//定义单链表结点类型    ElemType data; //数据域    struct LNode *next;//指针域}LNode, *LinkList;1234可以利用typedef关键字——数据类型重命名:type<数据类型><别名>单链表的两种实现方式:不带头结点的单链表```bashtypedef struct LNode{    ElemType data;    struct LNode *next;}LNode, *LinkList;//初始化一个空的单链表bool InitList(LinkList &L){  //注意用引用 &    L = NULL; //空表,暂时还没有任何结点;    return true;}void test(){    LinkList L;  //声明一个指向单链表的指针: 头指针    //初始化一个空表    InitList(L);    //...}//判断单链表是否为空bool Empty(LinkList L){    if (L == NULL)        return true;    else        return false;}头结点:代表链表上头指针指向的第一个结点,不带有任何数据。带头结点的单链表typedef struct LNode{    ElemType data;    struct LNode *next;}LNode, *LinkList;//初始化一个单链表(带头结点)bool InitList(LinkList &L){      L = (LNode*) malloc(sizeof(LNode));  //头指针指向的结点——分配一个头结点(不存储数据)    if (L == NULL)          //内存不足,分配失败        return false;    L -> next = NULL;       //头结点之后暂时还没有结点    return true;}void test(){    LinkList L;  //声明一个指向单链表的指针: 头指针    //初始化一个空表    InitList(L);    //...}//判断单链表是否为空(带头结点)bool Empty(LinkList L){    if (L->next == NULL)        return true;    else        return false;}带头结点和不带头结点的比较:不带头结点:写代码麻烦!对第一个数据节点和后续数据节点的处理需要用不同的代码逻辑,对空表和非空表的处理也需要用不同的代码逻辑; 头指针指向的结点用于存放实际数据;带头结点:头指针指向的头结点不存放实际数据,头结点指向的下一个结点才存放实际数据;2.3.2单链表上基本操作的实现1.按位序插入(带头结点):==ListInsert(&L, i, e): ==在表L中的第i个位置上插入指定元素e = 找到第i-1个结点(前驱结点),将新结点插入其后;其中头结点可以看作第0个结点,故i=1时也适用。typedef struct LNode{    ElemType data;    struct LNode *next;}LNode, *LinkList;//在第i个位置插入元素e(带头结点)bool ListInsert(LinkList &L, int i, ElemType e){      //判断i的合法性, i是位序号(从1开始)    if(i<1)        return False;        LNode *p;       //指针p指向当前扫描到的结点     int j=0;        //当前p指向的是第几个结点    p = L;          //L指向头结点,头结点是第0个结点(不存数据)    //循环找到第i-1个结点    while(p!=NULL && j<i-1){     //如果i>lengh, p最后会等于NULL        p = p->next;             //p指向下一个结点        j++;    }    if (p==NULL)                 //i值不合法        return false;        //在第i-1个结点后插入新结点    LNode *s = (LNode *)malloc(sizeof(LNode)); //申请一个结点    s->data = e;    s->next = p->next;    p->next = s;                 //将结点s连到p后,后两步千万不能颠倒qwq    return true;}平均时间复杂度:O(n)2.按位序插入(不带头结点)==ListInsert(&L, i, e): ==在表L中的第i个位置上插入指定元素e = 找到第i-1个结点(前驱结点),将新结点插入其后; 因为不带头结点,所以不存在“第0个”结点,因此!i=1 时,需要特殊处理——插入(删除)第1个元素时,需要更改头指针L;typedef struct LNode{    ElemType data;    struct LNode *next;}LNode, *LinkList;bool ListInsert(LinkList &L, int i, ElemType e){    if(i<1)        return false;        //插入到第1个位置时的操作有所不同!    if(i==1){        LNode *s = (LNode *)malloc(size of(LNode));        s->data =e;        s->next =L;        L=s;          //头指针指向新结点        return true;    }    //i>1的情况与带头结点一样!唯一区别是j的初始值为1    LNode *p;       //指针p指向当前扫描到的结点     int j=1;        //当前p指向的是第几个结点    p = L;          //L指向头结点,头结点是第0个结点(不存数据)    //循环找到第i-1个结点    while(p!=NULL && j<i-1){     //如果i>lengh, p最后会等于NULL        p = p->next;             //p指向下一个结点        j++;    }    if (p==NULL)                 //i值不合法        return false;        //在第i-1个结点后插入新结点    LNode *s = (LNode *)malloc(sizeof(LNode)); //申请一个结点    s->data = e;    s->next = p->next;    p->next = s;              return true;}3.指定结点的后插操作:InsertNextNode(LNode *p, ElemType e): 给定一个结点p,在其之后插入元素e; 根据单链表的链接指针只能往后查找,故给定一个结点p,那么p之后的结点我们都可知,但是p结点之前的结点无法得知;typedef struct LNode{    ElemType data;    struct LNode *next;}LNode, *LinkList;bool InsertNextNode(LNode *p, ElemType e){    if(p==NULL){        return false;    }    LNode *s = (LNode *)malloc(sizeof(LNode));    //某些情况下分配失败,比如内存不足    if(s==NULL)        return false;    s->data = e;          //用结点s保存数据元素e     s->next = p->next;    p->next = s;          //将结点s连到p之后    return true;}                         //平均时间复杂度 = O(1)//有了后插操作,那么在第i个位置上插入指定元素e的代码可以改成:bool ListInsert(LinkList &L, int i, ElemType e){      if(i<1)        return False;        LNode *p;       //指针p指向当前扫描到的结点     int j=0;        //当前p指向的是第几个结点    p = L;          //L指向头结点,头结点是第0个结点(不存数据)    //循环找到第i-1个结点    while(p!=NULL && j<i-1){     //如果i>lengh, p最后4鸟会等于NULL        p = p->next;             //p指向下一个结点        j++;    }    return InsertNextNode(p, e)}王道书代码:bool InsertPriorNode(LNode *p, LNode *s){    if(p==NULL || S==NULL)        return false;        s->next = p->next;    p->next = s;  ///s连接到p    ELemType temp = p->data;  //交换数据域部分    p->data = s->data;    s->data = temp;    return true;} 5.按位序删除节点(带头结点)ListDelete(&L, i, &e): 删除操作,删除表L中第i个位置的元素,并用e返回删除元素的值;头结点视为“第0个”结点;思路:找到第i-1个结点,将其指针指向第i+1个结点,并释放第i个结点;typedef struct LNode{    ElemType data;    struct LNode *next;}LNode, *LinkList;bool ListDelete(LinkList &L, int i, ElenType &e){    if(i<1) return false;    LNode *p;       //指针p指向当前扫描到的结点     int j=0;        //当前p指向的是第几个结点    p = L;          //L指向头结点,头结点是第0个结点(不存数据)    //循环找到第i-1个结点    while(p!=NULL && j<i-1){     //如果i>lengh, p最后会等于NULL        p = p->next;             //p指向下一个结点        j++;    }    if(p==NULL)         return false;    if(p->next == NULL) //第i-1个结点之后已无其他结点        return false;    LNode *q = p->next;         //令q指向被删除的结点    e = q->data;                //用e返回被删除元素的值    p->next = q->next;          //将*q结点从链中“断开”    free(q)                     //释放结点的存储空间    return true;}时间复杂度分析:最坏,平均时间复杂度:O(n)最好时间复杂度:删除第一个结点 O(1)6.指定结点的删除bool DeleteNode(LNode *p){    if(p==NULL)        return false;        LNode *q = p->next;      //令q指向*p的后继结点    p->data = p->next->data; //让p和后继结点交换数据域    p->next = q->next;       //将*q结点从链中“断开”    free(q);    return true;} //时间复杂度 = O(1)2.3.3单链表的查找按位查找==GetElem(L, i): ==按位查找操作,获取表L中第i个位置的元素的值;LNode * GetElem(LinkList L, int i){    if(i<0) return NULL;        LNode *p;               //指针p指向当前扫描到的结点    int j=0;                //当前p指向的是第几个结点    p = L;                  //L指向头结点,头结点是第0个结点(不存数据)    while(p!=NULL && j<i){  //循环找到第i个结点        p = p->next;        j++;    }    return p;               //返回p指针指向的值}平均时间复杂度O(n)按值查找LocateElem(L, e):按值查找操作,在表L中查找具有给定关键字值的元素;LNode * LocateElem(LinkList L, ElemType e){    LNode *P = L->next;    //p指向第一个结点    //从第一个结点开始查找数据域为e的结点    while(p!=NULL && p->data != e){        p = p->next;    }    return p;           //找到后返回该结点指针,否则返回NULL}2.3.4求单链表的长度== Length(LinkList L)==:计算单链表中数据结点(不含头结点)的个数,需要从第一个结点看是顺序依次访问表中的每个结点。算法的时间复杂度为O(n)。int Length(LinkList L){    int len=0;       //统计表长    LNode *p = L;    while(p->next != NULL){        p = p->next;        len++;    }    return len;}2.3.5单链表的创建操作1.头插法建立单链表(平均时间复杂度O(n))思路:每次都将生成的结点插入到链表的表头。LinkList List_HeadInsert(LinkList &L){       //逆向建立单链表    LNode *s;    int x;    L = (LinkList)malloc(sizeof(LNode));     //建立头结点    L->next = NULL;                          //初始为空链表,这步不能少!    scanf("%d", &x);                         //输入要插入的结点的值    while(x!=9999){                          //输入9999表结束        s = (LNode *)malloc(sizeof(LNode));  //创建新结点        s->data = x;        r->next = L->next;        L->next = s;                         //将新结点插入表中,L为头指针        scanf("%d", &x);       }    return L;   }2.尾插法建立单链表(时间复杂度O(n))思路:每次将新节点插入到当前链表的表尾,所以必须增加一个尾指针r,使其始终指向当前链表的尾结点。好处:生成的链表中结点的次序和输入数据的顺序会一致。LinkList List_TailInsert(LinkList &L){       //正向建立单链表    int x;                                   //设ElemType为整型int    L = (LinkList)malloc(sizeof(LNode));     //建立头结点(初始化空表)    LNode *s, *r = L;                        //r为表尾指针    scanf("%d", &x);                         //输入要插入的结点的值    while(x!=9999){                          //输入9999表结束        s = (LNode *)malloc(sizeof(LNode));        s->data = x;        r->next = s;        r = s                                //r指针指向新的表尾结点        scanf("%d", &x);       }    r->next = NULL;                          //尾结点指针置空    return L;}链表的逆置:算法思想:逆置链表初始为空,原表中结点从原链表中依次“删除”,再逐个插入逆置链表的表头(即“头插”到逆置链表中),使它成为逆置链表的“新”的第一个结点,如此循环,直至原链表为空;LNode *Inverse(LNode *L){    LNode *p, *q;    p = L->next;     //p指针指向第一个结点    L->next = NULL;  //头结点指向NULL    while (p != NULL){        q = p;        p = p->next;        q->next = L->next;          L->next = q;    }    return L;2.3.6双链表双链表中节点类型的描述:`typedef struct DNode{            //定义双链表结点类型    ElemType data;               //数据域    struct DNode *prior, *next;  //前驱和后继指针}DNode, *DLinklist;双链表的初始化(带头结点)typedef struct DNode{            //定义双链表结点类型    ElemType data;               //数据域    struct DNode *prior, *next;  //前驱和后继指针}DNode, *DLinklist;//初始化双链表bool InitDLinkList(Dlinklist &L){    L = (DNode *)malloc(sizeof(DNode));      //分配一个头结点    if(L==NULL)                              //内存不足,分配失败        return false;        L->prior = NULL;   //头结点的prior指针永远指向NULL    L->next = NULL;    //头结点之后暂时还没有结点    return true;}void testDLinkList(){    //初始化双链表    DLinklist L;         // 定义指向头结点的指针L    InitDLinkList(L);    //申请一片空间用于存放头结点,指针L指向这个头结点    //...}//判断双链表是否为空bool Empty(DLinklist L){    if(L->next == NULL)    //判断头结点的next指针是否为空        return true;    else        return false;}双链表的插入操作后插操作InsertNextDNode(p, s): 在p结点后插入s结点bool InsertNextDNode(DNode *p, DNode *s){ //将结点 *s 插入到结点 *p之后    if(p==NULL || s==NULL) //非法参数        return false;        s->next = p->next;    if (p->next != NULL)   //p不是最后一个结点=p有后继结点          p->next->prior = s;    s->prior = p;    p->next = s;        return true;}按位序插入操作:思路:从头结点开始,找到某个位序的前驱结点,对该前驱结点执行后插操作;前插操作:思路:找到给定结点的前驱结点,再对该前驱结点执行后插操作;双链表的删除操作删除p节点的后继节点//删除p结点的后继结点bool DeletNextDNode(DNode *p){    if(p==NULL) return false;    DNode *q =p->next;            //找到p的后继结点q    if(q==NULL) return false;     //p没有后继结点;    p->next = q->next;    if(q->next != NULL)           //q结点不是最后一个结点        q->next->prior=p;    free(q);    return true;}//销毁一个双链表bool DestoryList(DLinklist &L){    //循环释放各个数据结点    while(L->next != NULL){        DeletNextDNode(L);  //删除头结点的后继结点    free(L); //释放头结点    L=NULL;  //头指针指向NULL    }}双链表的遍历操作前向遍历while(p!=NULL){    //对结点p做相应处理,eg打印    p = p->prior;}后向遍历while(p!=NULL){    //对结点p做相应处理,eg打印    p = p->next;}注意:双链表不可随机存取,按位查找和按值查找操作都只能用遍历的方式实现,时间复杂度为O(n)2.3.7循环链表1.循环单链表最后一个结点的指针不是NULL,而是指向头结点typedef struct LNode{                ElemType data;                   struct LNode *next;  }DNode, *Linklist;/初始化一个循环单链表bool InitList(LinkList &L){    L = (LNode *)malloc(sizeof(LNode)); //分配一个头结点    if(L==NULL)             //内存不足,分配失败        return false;    L->next = L;            //头结点next指针指向头结点    return true;}//判断循环单链表是否为空(终止条件为p或p->next是否等于头指针)bool Empty(LinkList L){    if(L->next == L)        return true;    //为空    else        return false;}//判断结点p是否为循环单链表的表尾结点bool isTail(LinkList L, LNode *p){    if(p->next == L)        return true;    else        return false;}单链表和循环单链表的比较:**单链表:**从一个结点出发只能找到该结点后续的各个结点;对链表的操作大多都在头部或者尾部;设立头指针,从头结点找到尾部的时间复杂度=O(n),即对表尾进行操作需要O(n)的时间复杂度;**循环单链表:**从一个结点出发,可以找到其他任何一个结点;设立尾指针,从尾部找到头部的时间复杂度为O(1),即对表头和表尾进行操作都只需要O(1)的时间复杂度;==优点:==从表中任一节点出发均可找到表中其他结点。2.循环双链表表头结点的prior指向表尾结点,表尾结点的next指向头结点typedef struct DNode{              ElemType data;                   struct DNode *prior, *next;  }DNode, *DLinklist;//初始化空的循环双链表bool InitDLinkList(DLinklist &L){    L = (DNode *) malloc(sizeof(DNode));    //分配一个头结点    if(L==NULL)            //内存不足,分配失败        return false;      L->prior = L;          //头结点的prior指向头结点    L->next = L;           //头结点的next指向头结点}void testDLinkList(){    //初始化循环单链表    DLinklist L;    InitDLinkList(L);    //...}//判断循环双链表是否为空bool Empty(DLinklist L){    if(L->next == L)        return true;    else        return false;}//判断结点p是否为循环双链表的表尾结点bool isTail(DLinklist L, DNode *p){    if(p->next == L)        return true;    else        return false;}双链表的插入(循环双链表):bool InsertNextDNode(DNode *p, DNode *s){     s->next = p->next;    p->next->prior = s;    s->prior = p;    p->next = s;双链表的删除//删除p的后继结点qp->next = q->next;q->next->prior = p;free(q);双向循环链表:和单链的循环表类似,双向链表也可以有循环表,让头结点的前驱指针指向链表的最后一个结点,让最后一个结点的后继指针指向头结点。————————————————版权声明:本文为CSDN博主「胖胖的懒羊羊」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。原文链接:https://blog.csdn.net/qq_44867340/article/details/119455799
  • [经验分享] 使用MindStudio 进行Deit 模型推理迁移
    1. 介绍1.1 软件简介本文旨在帮助用户使用 CANN 架构和 MindStudio 平台进行 AI CPU 离线模型推理指导。其中 CANN(Compute Architecture for Neural Networks)是华为公司针对 AI 场景推出的异构计算架构,通过提供多层次的编程接口,支持用户快速构建基于昇腾平台的 AI 应用和业务。CANN 框架下有众多软件包供用户使用,比如 Profiling 性能分析工具,AtuoMl 工具, Atc 工具以及 Benchmark 工具等。在模型推理工作中,涉及到 Atc 和 Bechmark 工具。AI 模型推理步骤涉及模型转换和推理以及精度测试过程。Mindstudio 提供您在 AICPU 上模型转换功能,由于 Mindstudio 版本的升级,Mindstudio 移除了模型推理所需的 benchmark 菜单,所以我整体流程无法全部在 Mindstudio 可视化界面完成。本文我将使用 Mindstudio 可视化界面进行模型转换并且在 Mindstudio 的 Remote 终端进行推理步骤。1.2 模型简介Deit 是一个全 Transformer 的架构,没有使用任何的卷积操作。其核心是将蒸馏方法引入 VIT 的训练,引入了一种教师-学生的训练策略,提出了 token-based distillation。有趣的是, 这种训练策略使用卷积网络作为教师网络进行蒸馏,能够比使用 transformer 架构的网络作为教师取得更好的效果。2. 安装教程2.1 CANN 软件包CANN 下载链接:https://www.hiascend.com/software/cann/community CANN 安装指南:https://www.hiascend.com/document/detail/zh/CANNCommunityEdition/51RC1alpha005/softwareinstall/instg/atlasdeploy_03_0002.html2.2 Mindstudio 平台Mindstudio 下载地址:https://www.hiascend.com/software/mindstudio/download 2.2.1 环境依赖配置由于我电脑是 Windows11,所以接下来的安装过程都是在 win11 系统下进行的。(1) Python 官方网站下载安装安装 Python3.7.5 到 Windows 本地。在开始菜单搜索“环境变量”,在“环境变量 > 用户变量(U)”弹框中选中 Path 变量后单击“编辑”,如图 1 所示:这里我们要将 Python 安装环境下的 Scipts 路径添加到 Path 里,最后点击确认,完成环境变量设置。图 1Python 安装完成后,需要安装 Python 依赖环境:1. pip install xlrd==1.2.02. pip install absl-py3. pip install numpy4. pip install requests(2) 安装 MinGW 依赖首先根据电脑配置,下载适合的(下载地址 ),例如 64 位可以选择 x86_64-posix-seh, 解压 MinGW 安装包到自定义路径。接着跟上述配置环境变量方式相同,将安装 MinGW 路径下的bin 文件夹添加到Path 中。最后我们打开电脑 CMD 终端查看是否安装成功,如图 2 所示:图 2(3) 安装 Java 依赖要求 Java 版本为 11,下载地址 。我们将 java 安装包路径下的 bin 添加到 Path 里,接下来验证是否安装成功,如图 3 所示:图 3(4) 安装 Cmake 依赖Cmake 可以到官网 下载 msi 文件进行安装,打开安装包后选择自己要安装的路径,但是要将安装路径添加到环系统境里,如图 4 所示:图 4(5) 安装 Mindstudio我们选择 exe 安装方式,下完成安装包后,我们选择安装路径后,进入图 5 界面,这里我们选择 64bit launcher 和 add launcher dir to Path。图 5(6) 下载 jbr下载 jbr ,解压到 MindStudio 安装根目录,与 bin 目录同级。3. 推理实践3.1 ONNX 简介ONNX 是一种针对机器学习所设计的开放式的文件格式,于存储训练好的模型。它使得不同的深度学习框架(如 Pytorch, MXNet)可以采用相同格式存储模型数据。简而言之,ONNX 是一种便于 在各个主流深度学习框架中迁移模型的中间表达格式。开放式神经网络交换(ONNX)是迈向开放式生态系统的第一步,它使 AI 开发人员能够随着项目的发展选择合适的工具。ONNX 为 AI 模型提供开源格式。它定义了可扩展的计算图模型,以及内置运算符和标准数据类型的定义。最初的 ONNX 专注于推理(评估)所需的功能。ONNX 解释计算图的可移植,它使用 graph 的序列化格式。它不一定是框架选择在内部使用和操作计算的形式。例如,如果在优化过程中操作更有效,则实现可以在存储器中以不同方 式表示模型。3.2 ONNX 推理分析这里我们要明确推理的模型推理的输出输出,如表 1 所示:输入数据大小数据类型数据排布格式inputbatchsize x 3 x 224 x 224FLOAT32NVHW输入数据大小数据类型数据排布格式Output1batchsize x 1000FLOAT32ND表 13.3 ONNX 推理工程创建打开 MindStudio, 选择 File -> New -> Project,如图 6 所示:图 6在左边选择 Ascend Operator,若是远程配置 CANN,点击 install,这里我已经安装好了, 所以是 change 按钮,如图 7 所示:图 7第一次安装 CANN 会弹出连接服务器的窗口,如图 8 所示:图 8我们在这里配置我们的服务器,输入服务器的用户名和密码点击测试,成功连接则如图9 所示:图 9接下来我们点击 ok,进行下一步,选择 AICPU,选择 ONNX,如图 10 所示:图 10这样我们的项目工程就创建好了。3.4 ONNX 数据处理3.4.1 配置 mindstudio 的python_SDK这里我们使用 mindstudio 的 python_sdk 运行数据处理的脚本代码,另外我们要先下载推理脚本开源仓库,请点此链接下载 ,然后再我们先配置代码环境,如图 11 所示:图 11进入 Projects Structure,选择 Project Setting 中的 Project SDK,选择 Add Project SDK,如图 12 所示:图 12进入 Add Python SDK,选择 SSH Interpreter,选择自己 conda 环境下的 Python 路径,如图 13 所示:图 13接下来进入 File->setting->Tools->Deployment,连接自己的服务器后选择 Mappings,选择我们工程目录以服务器上工程路径,如图 14 所示:图 14接下来我们从服务器上拉取代码,如图 15 所示:图 15拉取代码成功如图 16 所示:图 163.4.2 数据处理 python 代码我们的deit 离线模型推理,用到ILSVRC2012 数据集的验证集以及文件夹中的val_label.txt数据标签。将原始数据进行重新规整大小并裁剪,如图 17 和图 18:图 17图 18将原始数据(.jpeg)转化为二进制文件(.bin),代码如图 19 所示:图 19接下来使用 benchmark 推理需要输入二进制数据集的 info 文件,用于获取数据集。利用如图 20 和图 21 的代码,输入已经得到的二进制文件,输出生成二进制数据集的 info 文件。图 20图 21具体代码可以在此下载 ,上述代码运行成功后,运行成功后,在当前目录中生成deit_prep_bin.info。3.4.3 数据处理的代码执行了解了数据处理的代码,接下来我们执行代码选择 Run->Debug,进入图 22 的界面:图 22选择 Ascend App,进入界面之后,选择我们要运行的代码,并在 Command Arguments中输入代码所需要的参数,点击应用,如图 23 所示:图 23这样我们的代码就配置好了,接下来想要运行的话,点击 Run->Run,选择要运行的代码,如图 24 所示:图 243.5 ONNX 模型转换使用 PyTorch 将模型权重文件.pth 转换为.onnx 文件。获取权重文件,点击 Link 或 ModelZoo 的源码包中获取权重文 deit 模型的权重文件。执行图 25 代码进行 pth 文件到 ONNX 的转换。图 25利用 pip install onnx-simplifier == 0.3.6,安装 onnxsim,利用 onnxsim 对模型进行优化, 执行图 26 的命令生成静态 ONNX 模型(固定 Batchsize)。图 26得到静态 ONNX 模型后,使用 ATC 工具将.onnx 文件转换为.om 文件,导出.onnx 模型文件时需设置算子版本为 11。昇腾张量编译器(Ascend Tensor Compiler,简称 ATC)是昇腾 CANN 架构体系下的模型转换工具, 它可以将开源框架的网络模型或 Ascend IR 定义的单算子描述文件(json 格式) 转换为昇腾 AI 处理器支持的.om 格式离线模型。其功能架构如图 27 所示。模型转换过程中,ATC 会进行算子调度优化、权重数据重排、内存使用优化等具体操作, 对原始的深度学习模型进行进一步的调优,从而满足部署场景下的高性能需求,使其能够高效执行在昇腾 AI 处理器上。图 27由于 Mindstudio 已经嵌入 ATC 工具,所以我将在 Mindstudio 上进行模型转换。在菜单栏选择“Ascend > Model Converter”,如图 28 所示。注意:进行模型转换前,使用 MindStudio 安装用户,将所转换模型的模型文件以及权重文件上传到 Ascend-cann-toolkit 开发套件包所在的开发环境。图 28打开模型转换页面,在“Model Information”页签中上传模型文件和权重文件,界面参考如图 29:图 29Model Information 界面参数配置如表 2 所示:参数说明CANN Machine( 仅 Windows 系统支持此参数)自动填充。 远程连接 ADK 所在环境的 SSH 地址, 表现格式为<username>@localhost:端口号。Model File模型文件。必填。该模型文件需要取消其他用户写的权限。 有两种选择方式:1. 单击右侧的文件夹图标,在后台服务器路径选择需要转化的模型文件并上传。2. 在参数后面的输入框中自行输入模型文件在后台服务器的路径,包括模型文件名称Weight File权重文件。当原始框架是 Caffe 时,该参数存在且必填:1. 如果模型文件和权重文件存在于后台服务器同一目录下,且名称和模型文件名称相同,则选择模型文件后,权重文件会自动填充。2. 如果模型文件和权重文件存在于后台服务器不同目录下,或者在同一目录下,但名称和模型文件名称不相同。Model Name模型文件名称,必填。选择模型文件后,该参数会自动填充,用户可以根 据需要自行修改名称,要求如下:1. 只支持 a-z、A-Z、0-9、下划线以及短划线的组合,最多支持 64 个字符。2. 如果模型转换的输出路径已经存在相同名称模型文件,单击“Next” 后会提示覆盖原有文件或重命名当前 Model Name 的信息,用户根据实际情况选择。Target SoC Version模型转换时指定芯片型号。请根据板端环境具体芯片形态进行选择。Input Format输入数据格式。1. 当原始框架是 Caffe 时,取值为 NCHW、ND(表示支持任意格式,N<=4),默认为 NCHW。2. 当原始框架是 MindSpore、ONNX 时,取值为 NCHW。3. 当原始框架是 TensorFlow 时,取值为 NCHW、NHWC、ND、NCDHW、NDHWC,默认为 NHWC。Input Nodes模型输入节点信息。1. 如果选择模型文件并且解析成功,则该参数下方会展示模型输入节点的 shape 信息以及 Type 信息。2. 如果选择模型文件后,无法解析“Input Nodes”,该场景下,需要用户根据模型文件中的相关信息手动输入:单击该参数右侧的,在弹出界面中 输入模型输入节点的 Name、Shape 信息(只支持英文逗号,数字(-1 或大于 0 的整数),不能以英文逗号开头,只能以数字结尾)和输入节点的数据类型 Type。单击删除节点信息。3. 如果模型有多个输入,解析成功后,“Input Nodes”参数下方会展示每一个输入节点的 Shape 信息和 Type 信息。Shape模型输入的 shape 信息,例如图 20 中的数值分别代表输入数据的 N(模型一次处理的图片个数),C(Channel,例如彩色 RGB 图像的 Channel 数为 3),H(Height),W(Width)。若开启 AIPP 功能,则此处的 H,W 取值即为 AIPP 输出数据的高和宽。TypeType:指定输入节点的数据类型。1. 若原始框架类型为 Caffe、ONNX,支持的数据类型为 FP32、FP16、UINT8。2. 若原始框架类型为 MindSpore,支持的数据类型为 FP32、UINT8。3. 若原始框架类型为 TensorFlow,支持的输入数据类型为 FP32、FP16、UINT8、Int32、Int64、Bool。Output Nodes指定输出节点信息。单击“Select”在弹出的网络拓扑结构中,选中某层节点,右击选择“Select”,该层变成蓝色,单击“OK”后,在“Output Nodes”参数下面会看到标记层的算子,右击选择“Deselect”取消选中。1. Op Name:标记层的算子名称。2. Data Type:算子输出的数据类型,包括 FP32、UINT8、FP16,通过该参数用户可以设置单个算子的输出数据类型。“Output Nodes”参数下方“Select”层的算子,默认为全部选中,用户可以自行选择将不需要输出的算子去勾选,只有选中的算子才会作为模型的输 出。本章节以选中所有算子为例进行说明。某些情况下,用户想要查看某层算子参数是否合适,则需要将该层算子的 参数输出,即可以通过单击“Select”按钮,在弹出网络拓扑结构中将所需层的算子标记为“Select”,然后在“Output Nodes”参数下方选中想要输出的算子,模型转换后,在相应.om 模型文件可以看到该算子的输出直接作为模型的输出。详细信息请参见模型可视化 。Load Configuration导入上次模型转换的配置文件。如果用户之前转换过模型,无论成功与否,在$HOME/modelzoo/${Model Name}/device/路径都会生成${Model Name}_config.json 配置文件,该文件记录用户模型转换时所选择的配置信息,包括所用模型路径、模型名称、输入输出配置,数据预处理配置等,下次重新转换模型时,通过单击“LoadConfiguration”选择相应路径下的配置文件,则相应的配置信息会自动填充,用户自行决定是否沿用上次配置还是修改配置后重新进行模型转换。表 2在“Model Information” 页签中上传模型文件和权重文件后点击 Next 按钮, Data Processing 页面直接点击 Next,进入到图 30 界面。图 30Command Preview 里的命令应该与我们在命令行运行 atc 的命令几乎相同,点击 Finsh开始转换,运行成功后如图 31 所示:图 313.6 Benchmark 推理由于 CANN 5.1.RC1 和 MindStudio 5.0.RC1 版本不发布 benchmark 工具, 若需要在MindStudio 环境下使用该工具,请安装 CANN 5.0.3 版本下的 benchmark 工具以及 3.0.3 版本的 MindStudio。具体说明请见此 Link 。图 32所以我们使用Mindstudio 客户端的远程终端Remote Terminal 进行 benchmark 工具的使用。在菜单栏选择“Tools> Start SSH Session”,选择我们配置好的远程连接,如图 32 所示。接下来从获取软件包 章节中获取 benchmark 工具软件包,并进行解压。进入解压后的文件夹,获取 benchmark 工具benchmark.{arch}。{arch}为 CPU 架构,取值为 aarch64 或x86_64。将 benchmark 工具 benchmark.{arch}和准备相关数据文件章节准备的模型 OM 文件上传到服务器的任意路径下。这些文件可以上传到同一路径,也可以上传到不同路径,以我们这种用户实际情况为准。 设置环境变量。CANN 软件提供进程级环境变量设置脚本,供用户在进程中引用,以自动完成环境变量设置。用户进程结束后自动失效。设置方法如下:# 以 root 用户安装 toolkit 包. /usr/local/Ascend/ascend-toolkit/set_env.sh # 以非 root 用户安装 toolkit 包. ${HOME}/Ascend/ascend-toolkit/set_env.sh进入 benchmark 工具所在路径,执行如下命令增加对工具的可执行权限:chmod +x benchmark.{arch}执行如下命令运行 benchmark 工具,命令如图 33 所示:图 33命    令    :    benchmark.x86_64    -model_type=vision    -device_id=0    -batch_size=16-om_path=deit_small_bs16.om    -input_text_path=./deit_prep_bin.info    -input_width=224-input_height=224 -output_binary=False -useDvpp=False Benchmark 工具支持的运行参数及其说明请参见表 3。和 nmt 模型类型,该参数必填。[-output_binary, -ob]输出结果格式是否为二进制文件(即bin 文件)。取值为:1. true:输出结果格式为 bin 文件。2. false:输出结果格式为 txt 文件。3. 缺省值为 true。否[-useDvpp, -u]模型前处理是否使用 DVPP 编解码模块。取值为:1. true    2. false缺省值为 false。若使用 DVPP 编解码或图像缩放,则该参数置为 true。其他情况置为 true。仅vision 和yolocaffe 模型类型支持该参数,且为必选。表 3 Benchmark 工具运行之后,结果如图 34 所示:图 34运行结果参数说明如表 4 所示:参数说明[e2e]throughputRatelatency端到端总吞吐率。公式为 sample 个数/时间。 端到端时延,即从处理第一个 sample 到最后一个sample 的完成时间。[data read][preprocess]throughputRate当前模块的吞吐率。[post]moduleLatency执行一次当前模块的时延。[infer]throughputRatemoduleLatencyInterface throughputRate推理模块整体吞吐率,包含线程启动、数据等待、实际推理等时间。推理模块的平均时延。公式为执行一次推理的时间/batch size,其中执行一次推理的时间包含了内存申请、内存拷贝以及推理等时间。aclmdlExecute 接口的吞吐率。公式为 sample 个数/aclmdlExecute 接口的平均执行时间。表 43.7 精度验证调用 imagenet_acc_eval.py 脚本与数据集标签 val_label.txt 比对,可以获得 Accuracy Top5数据,结果保存在 result.json 中。我们选择 Run->Deubug->Edit_Configurations,进入图 35 界面:图 35我们在 Conmand Arguments 输入我们需要的运行参数,第一个为 benchmark 输出目录, 第二个为数据集配套标签,第三个是生成文件的保存目录,第四个是生成的文件名。如果在命令行执行,则命令如下:python3.7 imagenet_acc_eval.py result/dumpOutput_device0/ /home/common/datasets/imagenet/val_label.txt ./ result.json输出结果如表 5 所示:Om ModelAcc@1Acc@5BS179.6994.97BS1679.6994.97BS3279.6994.97Official79.995.0表 5得到的 om 离线模型推理 TopN 精度与该模型 github 代码仓上公布的精度对比,精度下降在 1%范围之内,故精度达标。4. FAQQ:到遇到无法使用 pip 或者 conda 安装 python 运行环境时Q:当使用 benchmark 时,碰到图 36 的 bug:图 36A:这是由于我们环境配置错误,运行如下命令即可: source /usr/local/Ascend/ascend-toolkit/set_env.shQ:在 Mindstudio 上转换模型成功却无法得到模型,bug 如图 37 所示:图 37A:这是由于我们的项目出了问题,点击 File-New,新建一个项目即可。Q:出现如图 38 的输入数据大小和模型输入大小不一致的的情况:图 38A:这是由于模型转换时,数据类型应选择 FP32 而不是默认的 Fp16。5. 从昇腾社区获得更多帮助开发者在使用 MindStudio 或进行算子开发过程中遇到任何问题,都可以来昇腾社区获得更多的帮助。昇腾官网:https://www.hiascend.com/ 昇腾社区:https://bbs.huaweicloud.com/ 昇腾论坛:https://bbs.huaweicloud.com/forum/forum-726-1.html
  • [技术干货] Python面试基础篇 - 50道经典面试题(附答案及多种解答)[转载]
    题目001: 在Python中如何实现单例模式。点评:单例模式是指让一个类只能创建出唯一的实例,这个题目在面试中出现的频率极高,因为它考察的不仅仅是单例模式,更是对Python语言到底掌握到何种程度,建议大家用装饰器和元类这两种方式来实现单例模式,因为这两种方式的通用性最强,而且也可以顺便展示自己对装饰器和元类中两个关键知识点的理解。方法一: 使用装饰器实现单例模式。from functools import wrapsdef singleton(cls):    """单例类装饰器"""    instances = {}    @wraps(cls)    def wrapper(*args, **kwargs):        if cls not in instances:            instances[cls] = cls(*args, **kwargs)        return instances[cls]    return wrapper@singletonclass President:    pass扩展:装饰器是Python中非常有特色的语法,用一个函数去装饰另一个函数或类,为其添加额外的能力。通常通过装饰来实现的功能都属横切关注功能,也就是跟正常的业务逻辑没有必然联系,可以动态添加或移除的功能。装饰器可以为代码提供缓存、代理、上下文环境等服务,它是对设计模式中代理模式的践行。在写装饰器的时候,带装饰功能的函数(上面代码中的wrapper函数)通常都会用functools模块中的wraps再加以装饰,这个装饰器最重要的作用是给被装饰的类或函数动态添加一个__wrapped__属性,这个属性会将被装饰之前的类或函数保留下来,这样在我们不需要装饰功能的时候,可以通过它来取消装饰器,例如可以使用President = President.__wrapped__来取消对President类做的单例处理。需要提醒大家的是:上面的单例并不是线程安全的,如果要做到线程安全,需要对创建对象的代码进行加锁的处理。在Python中可以使用threading模块的RLock对象来提供锁,可以使用锁对象的acquire和release方法来实现加锁和解锁的操作。当然,更为简便的做法是使用锁对象的with上下文语法来进行隐式的加锁和解锁操作。方法二: 使用元类实现单例模式。class SingletonMeta(type):    """自定义单例元类"""    def __init__(cls, *args, **kwargs):        cls.__instance = None        super().__init__(*args, **kwargs)    def __call__(cls, *args, **kwargs):        if cls.__instance is None:            cls.__instance = super().__call__(*args, **kwargs)        return cls.__instanceclass President(metaclass=SingletonMeta):    pass扩展:Python是面向对象的编程语言,在面向对象的世界中,一切皆为对象。对象是通过类来创建的,而类本身也是对象,类这样的对象是通过元类来创建的。我们在定义类时,如果没有给一个类指定父类,那么默认的父类是object,如果没有给一个类指定元类,那么默认的元类是type。通过自定义的元类,我们可以改变一个类默认的行为,就如同上面的代码中,我们通过元类的__call__魔术方法,改变了President类的构造器那样。补充:关于单例模式,在面试中还有可能被问到它的应用场景。通常一个对象的状态是被其他对象共享的,就可以将其设计为单例,例如项目中使用的数据库连接池对象和配置对象通常都是单例,这样才能保证所有地方获取到的数据库连接和配置信息是完全一致的;而且由于对象只有唯一的实例,因此从根本上避免了重复创建对象造成的时间和空间上的开销,也避免了对资源的多重占用。再举个例子,项目中的日志操作通常也会使用单例模式,这是因为共享的日志文件一直处于打开状态,只能有一个实例去操作它,否则在写入日志的时候会产生混乱。题目002:不使用中间变量,交换两个变量a和b的值。点评:典型的送人头的题目,通常交换两个变量需要借助一个中间变量,如果不允许使用中间变量,在其他编程语言中可以使用异或运算的方式来实现交换两个变量的值,但是Python中有更为简单明了的做法。方法一:a = a ^ bb = a ^ ba = a ^ b方法二:a, b = b, a扩展:需要注意,a, b = b, a这种做法其实并不是元组解包,虽然很多人都这样认为。Python字节码指令中有ROT_TWO指令来支持这个操作,类似的还有ROT_THREE,对于3个以上的元素,如a, b, c, d = b, c, d, a,才会用到创建元组和元组解包。想知道你的代码对应的字节码指令,可以使用Python标准库中dis模块的dis函数来反汇编你的Python代码。题目003:写一个删除列表中重复元素的函数,要求去重后元素相对位置保持不变。点评:这个题目在初中级Python岗位面试的时候经常出现,题目源于《Python Cookbook》这本书第一章的第10个问题,有很多面试题其实都是这本书上的原题,所以建议大家有时间好好研读一下这本书。def dedup(items):    no_dup_items = []    seen = set()    for item in items:        if item not in seen:            no_dup_items.append(item)            seen.add(item)    return no_dup_items如果愿意也可以把上面的函数改造成一个生成器,代码如下所示。def dedup(items):    seen = set()    for item in items:        if item not in seen:            yield item            seen.add(item)扩展:由于Python中的集合底层使用哈希存储,所以集合的in和not in成员运算在性能上远远优于列表,所以上面的代码我们使用了集合来保存已经出现过的元素。集合中的元素必须是hashable对象,因此上面的代码在列表元素不是hashable对象时会失效,要解决这个问题可以给函数增加一个参数,该参数可以设计为返回哈希码或hashable对象的函数。题目004:假设你使用的是官方的CPython,说出下面代码的运行结果。点评:下面的程序对实际开发并没有什么意义,但却是CPython中的一个大坑,这道题旨在考察面试者对官方的Python解释器到底了解到什么程度。a, b, c, d = 1, 1, 1000, 1000print(a is b, c is d)def foo():    e = 1000    f = 1000    print(e is f, e is d)    g = 1    print(g is a)foo()运行结果:True FalseTrue FalseTrue上面代码中a is b的结果是True但c is d的结果是False,这一点的确让人费解。CPython解释器出于性能优化的考虑,把频繁使用的整数对象用一个叫small_ints的对象池缓存起来造成的。small_ints缓存的整数值被设定为[-5, 256]这个区间,也就是说,在任何引用这些整数的地方,都不需要重新创建int对象,而是直接引用缓存池中的对象。如果整数不在该范围内,那么即便两个整数的值相同,它们也是不同的对象。CPython底层为了进一步提升性能还做了另一个设定,对于同一个代码块中值不在small_ints缓存范围内的整数,如果同一个代码块中已经存在一个值与其相同的整数对象,那么就直接引用该对象,否则创建新的int对象。需要大家注意的是,这条规则对数值型适用,但对字符串则需要考虑字符串的长度,这一点大家可以自行证明。扩展:如果你用PyPy(另一种Python解释器实现,支持JIT,对CPython的缺点进行了改良,在性能上优于CPython,但对三方库的支持略差)来运行上面的代码,你会发现所有的输出都是True。题目005:Lambda函数是什么,举例说明的它的应用场景。点评:这个题目主要想考察的是Lambda函数的应用场景,潜台词是问你在项目中有没有使用过Lambda函数,具体在什么场景下会用到Lambda函数,借此来判断你写代码的能力。因为Lambda函数通常用在高阶函数中,主要的作用是通过向函数传入函数或让函数返回函数最终实现代码的解耦合。Lambda函数也叫匿名函数,它是功能简单用一行代码就能实现的小型函数。Python中的Lambda函数只能写一个表达式,这个表达式的执行结果就是函数的返回值,不用写return关键字。Lambda函数因为没有名字,所以也不会跟其他函数发生命名冲突的问题。扩展:面试的时候有可能还会考你用Lambda函数来实现一些功能,也就是用一行代码来实现题目要求的功能,例如:用一行代码实现求阶乘的函数,用一行代码实现求最大公约数的函数等。fac = lambda x: __import__('functools').reduce(int.__mul__, range(1, x + 1), 1)gcd = lambda x, y: y % x and gcd(y % x, x) or xLambda函数其实最为主要的用途是把一个函数传入另一个高阶函数(如Python内置的filter、map等)中来为函数做解耦合,增强函数的灵活性和通用性。下面的例子通过使用filter和map函数,实现了从列表中筛选出奇数并求平方构成新列表的操作,因为用到了高阶函数,过滤和映射数据的规则都是函数的调用者通过另外一个函数传入的,因此这filter和map函数没有跟特定的过滤和映射数据的规则耦合在一起。items = [12, 5, 7, 10, 8, 19]items = list(map(lambda x: x ** 2, filter(lambda x: x % 2, items)))print(items)    # [25, 49, 361]扩展:用列表的生成式来实现上面的代码会更加简单明了,代码如下所示。items = [12, 5, 7, 10, 8, 19]items = [x ** 2 for x in items if x % 2]print(items)    # [25, 49, 361]题目006:说说Python中的浅拷贝和深拷贝。点评:这个题目本身出现的频率非常高,但是就题论题而言没有什么技术含量。对于这种面试题,在回答的时候一定要让你的答案能够超出面试官的预期,这样才能获得更好的印象分。所以回答这个题目的要点不仅仅是能够说出浅拷贝和深拷贝的区别,深拷贝的时候可能遇到的两大问题,还要说出Python标准库对浅拷贝和深拷贝的支持,然后可以说说列表、字典如何实现拷贝操作以及如何通过序列化和反序列的方式实现深拷贝,最后还可以提到设计模式中的原型模式以及它在项目中的应用。浅拷贝通常只复制对象本身,而深拷贝不仅会复制对象,还会递归的复制对象所关联的对象。深拷贝可能会遇到两个问题:一是一个对象如果直接或间接的引用了自身,会导致无休止的递归拷贝;二是深拷贝可能对原本设计为多个对象共享的数据也进行拷贝。Python通过copy模块中的copy和deepcopy函数来实现浅拷贝和深拷贝操作,其中deepcopy可以通过memo字典来保存已经拷贝过的对象,从而避免刚才所说的自引用递归问题;此外,可以通过copyreg模块的pickle函数来定制指定类型对象的拷贝行为。deepcopy函数的本质其实就是对象的一次序列化和一次返回序列化,面试题中还考过用自定义函数实现对象的深拷贝操作,显然我们可以使用pickle模块的dumps和loads来做到,代码如下所示。import picklemy_deep_copy = lambda obj: pickle.loads(pickle.dumps(obj))列表的切片操作[:]相当于实现了列表对象的浅拷贝,而字典的copy方法可以实现字典对象的浅拷贝。对象拷贝其实是更为快捷的创建对象的方式。在Python中,通过构造器创建对象属于两阶段构造,首先是分配内存空间,然后是初始化。在创建对象时,我们也可以基于“原型”对象来创建新对象,通过对原型对象的拷贝(复制内存)就完成了对象的创建和初始化,这种做法更加高效,这也就是设计模式中的原型模式。在Python中,我们可以通过元类的方式来实现原型模式,代码如下所示。import copyclass PrototypeMeta(type):    """实现原型模式的元类"""    def __init__(cls, *args, **kwargs):        super().__init__(*args, **kwargs)        # 为对象绑定clone方法来实现对象拷贝        cls.clone = lambda self, is_deep=True: \            copy.deepcopy(self) if is_deep else copy.copy(self)class Person(metaclass=PrototypeMeta):    passp1 = Person()p2 = p1.clone()                 # 深拷贝p3 = p1.clone(is_deep=False)    # 浅拷贝题目007:Python是如何实现内存管理的?点评:当面试官问到这个问题的时候,一个展示自己的机会就摆在面前了。你要先反问面试官:“你说的是官方的CPython解释器吗?”。这个反问可以展示出你了解过Python解释器的不同的实现版本,而且你也知道面试官想问的是CPython。当然,很多面试官对不同的Python解释器底层实现到底有什么差别也没有概念。所以,千万不要觉得面试官一定比你强,怀揣着这份自信可以让你更好的完成面试。Python提供了自动化的内存管理,也就是说内存空间的分配与释放都是由Python解释器在运行时自动进行的,自动管理内存功能极大的减轻程序员的工作负担,也能够帮助程序员在一定程度上解决内存泄露的问题。以CPython解释器为例,它的内存管理有三个关键点:引用计数、标记清理、分代收集。引用计数:对于CPython解释器来说,Python中的每一个对象其实就是PyObject结构体,它的内部有一个名为ob_refcnt 的引用计数器成员变量。程序在运行的过程中ob_refcnt的值会被更新并藉此来反映引用有多少个变量引用到该对象。当对象的引用计数值为0时,它的内存就会被释放掉。typedef struct _object {    _PyObject_HEAD_EXTRA    Py_ssize_t ob_refcnt;    struct _typeobject *ob_type;} PyObject;以下情况会导致引用计数加1:对象被创建对象被引用对象作为参数传入到一个函数中对象作为元素存储到一个容器中以下情况会导致引用计数减1:用del语句显示删除对象引用对象引用被重新赋值其他对象一个对象离开它所在的作用域持有该对象的容器自身被销毁持有该对象的容器删除该对象可以通过sys模块的getrefcount函数来获得对象的引用计数。引用计数的内存管理方式在遇到循环引用的时候就会出现致命伤,因此需要其他的垃圾回收算法对其进行补充。标记清理:CPython使用了“标记-清理”(Mark and Sweep)算法解决容器类型可能产生的循环引用问题。该算法在垃圾回收时分为两个阶段:标记阶段,遍历所有的对象,如果对象是可达的(被其他对象引用),那么就标记该对象为可达;清除阶段,再次遍历对象,如果发现某个对象没有标记为可达,则就将其回收。CPython底层维护了两个双端链表,一个链表存放着需要被扫描的容器对象(姑且称之为链表A),另一个链表存放着临时不可达对象(姑且称之为链表B)。为了实现“标记-清理”算法,链表中的每个节点除了有记录当前引用计数的ref_count变量外,还有一个gc_ref变量,这个gc_ref是ref_count的一个副本,所以初始值为ref_count的大小。执行垃圾回收时,首先遍历链表A中的节点,并且将当前对象所引用的所有对象的gc_ref减1,这一步主要作用是解除循环引用对引用计数的影响。再次遍历链表A中的节点,如果节点的gc_ref值为0,那么这个对象就被标记为“暂时不可达”(GC_TENTATIVELY_UNREACHABLE)并被移动到链表B中;如果节点的gc_ref不为0,那么这个对象就会被标记为“可达“(GC_REACHABLE),对于”可达“对象,还要递归的将该节点可以到达的节点标记为”可达“;链表B中被标记为”可达“的节点要重新放回到链表A中。在两次遍历之后,链表B中的节点就是需要释放内存的节点。分代回收:在循环引用对象的回收中,整个应用程序会被暂停,为了减少应用程序暂停的时间,Python 通过分代回收(空间换时间)的方法提高垃圾回收效率。分代回收的基本思想是:对象存在的时间越长,是垃圾的可能性就越小,应该尽量不对这样的对象进行垃圾回收。CPython将对象分为三种世代分别记为0、1、2,每一个新生对象都在第0代中,如果该对象在一轮垃圾回收扫描中存活下来,那么它将被移到第1代中,存在于第1代的对象将较少的被垃圾回收扫描到;如果在对第1代进行垃圾回收扫描时,这个对象又存活下来,那么它将被移至第2代中,在那里它被垃圾回收扫描的次数将会更少。分代回收扫描的门限值可以通过gc模块的get_threshold函数来获得,该函数返回一个三元组,分别表示多少次内存分配操作后会执行0代垃圾回收,多少次0代垃圾回收后会执行1代垃圾回收,多少次1代垃圾回收后会执行2代垃圾回收。需要说明的是,如果执行一次2代垃圾回收,那么比它年轻的代都要执行垃圾回收。如果想修改这几个门限值,可以通过gc模块的set_threshold函数来做到。题目008:说一下你对Python中迭代器和生成器的理解。点评:很多人面试者都会写迭代器和生成器,但是却无法准确的解释什么是迭代器和生成器。如果你也有同样的困惑,可以参考下面的回答。迭代器是实现了迭代器协议的对象。跟其他编程语言不通,Python中没有用于定义协议或表示约定的关键字,像interface、protocol这些单词并不在Python语言的关键字列表中。Python语言通过魔法方法来表示约定,也就是我们所说的协议,而__next__和__iter__这两个魔法方法就代表了迭代器协议。可以通过for-in循环从迭代器对象中取出值,也可以使用next函数取出迭代器对象中的下一个值。生成器是迭代器的语法升级版本,可以用更为简单的代码来实现一个迭代器。扩展:面试中经常让写生成斐波那契数列的迭代器,大家可以参考下面的代码。class Fib(object):        def __init__(self, num):        self.num = num        self.a, self.b = 0, 1        self.idx = 0       def __iter__(self):        return self    def __next__(self):        if self.idx < self.num:            self.a, self.b = self.b, self.a + self.b            self.idx += 1            return self.a        raise StopIteration()如果用生成器的语法来改写上面的代码,代码会简单优雅很多。def fib(num):    a, b = 0, 1    for _ in range(num):        a, b = b, a + b        yield a题目009:正则表达式的match方法和search方法有什么区别?点评:正则表达式是字符串处理的重要工具,所以也是面试中经常考察的知识点。在Python中,使用正则表达式有两种方式,一种是直接调用re模块中的函数,传入正则表达式和需要处理的字符串;一种是先通过re模块的compile函数创建正则表达式对象,然后再通过对象调用方法并传入需要处理的字符串。如果一个正则表达式被频繁的使用,我们推荐用re.compile函数创建正则表达式对象,这样会减少频繁编译同一个正则表达式所造成的开销。match方法是从字符串的起始位置进行正则表达式匹配,返回Match对象或None。search方法会扫描整个字符串来找寻匹配的模式,同样也是返回Match对象或None。题目010:下面这段代码的执行结果是什么。def multiply():    return [lambda x: i * x for i in range(4)]print([m(100) for m in multiply()])运行结果:[300, 300, 300, 300]1上面代码的运行结果很容易被误判为[0, 100, 200, 300]。首先需要注意的是multiply函数用生成式语法返回了一个列表,列表中保存了4个Lambda函数,这4个Lambda函数会返回传入的参数乘以i的结果。需要注意的是这里有闭包(closure)现象,multiply函数中的局部变量i的生命周期被延展了,由于i最终的值是3,所以通过m(100)调列表中的Lambda函数时会返回300,而且4个调用都是如此。如果想得到[0, 100, 200, 300]这个结果,可以按照下面几种方式来修改multiply函数。方法一:使用生成器,让函数获得i的当前值。def multiply():    return (lambda x: i * x for i in range(4))print([m(100) for m in multiply()])或者def multiply():    for i in range(4):        yield lambda x: x * iprint([m(100) for m in multiply()])方法二:使用偏函数,彻底避开闭包。from functools import partialfrom operator import __mul__def multiply():    return [partial(__mul__, i) for i in range(4)]print([m(100) for m in multiply()])题目011:Python中为什么没有函数重载?点评:C++、Java、C#等诸多编程语言都支持函数重载,所谓函数重载指的是在同一个作用域中有多个同名函数,它们拥有不同的参数列表(参数个数不同或参数类型不同或二者皆不同),可以相互区分。重载也是一种多态性,因为通常是在编译时通过参数的个数和类型来确定到底调用哪个重载函数,所以也被称为编译时多态性或者叫前绑定。这个问题的潜台词其实是问面试者是否有其他编程语言的经验,是否理解Python是动态类型语言,是否知道Python中函数的可变参数、关键字参数这些概念。首先Python是解释型语言,函数重载现象通常出现在编译型语言中。其次Python是动态类型语言,函数的参数没有类型约束,也就无法根据参数类型来区分重载。再者Python中函数的参数可以有默认值,可以使用可变参数和关键字参数,因此即便没有函数重载,也要可以让一个函数根据调用者传入的参数产生不同的行为。题目012:用Python代码实现Python内置函数max。点评:这个题目看似简单,但实际上还是比较考察面试者的功底。因为Python内置的max函数既可以传入可迭代对象找出最大,又可以传入两个或多个参数找出最大;最为关键的是还可以通过命名关键字参数key来指定一个用于元素比较的函数,还可以通过default命名关键字参数来指定当可迭代对象为空时返回的默认值。下面的代码仅供参考:def my_max(*args, key=None, default=None):    """    获取可迭代对象中最大的元素或两个及以上实参中最大的元素    :param args: 一个可迭代对象或多个元素    :param key: 提取用于元素比较的特征值的函数,默认为None    :param default: 如果可迭代对象为空则返回该默认值,如果没有给默认值则引发ValueError异常    :return: 返回可迭代对象或多个元素中的最大元素    """    if len(args) == 1 and len(args[0]) == 0:        if default:            return default        else:            raise ValueError('max() arg is an empty sequence')    items = args[0] if len(args) == 1 else args    max_elem, max_value = items[0], items[0]    if key:        max_value = key(max_value)    for item in items:        value = item        if key:            value = key(item)        if value > max_value:            max_elem, max_value = item, value题目013:写一个函数统计传入的列表中每个数字出现的次数并返回对应的字典。点评:送人头的题目,不解释。def count_letters(items):    result = {}    for item in items:        if isinstance(item, (int, float)):            result[item] = result.get(item, 0) + 1    return result也可以直接使用Python标准库中collections模块的Counter类来解决这个问题,Counter是dict的子类,它会将传入的序列中的每个元素作为键,元素出现的次数作为值来构造字典。from collections import Counterdef count_letters(items):    counter = Counter(items)    return {key: value for key, value in counter.items() \            if isinstance(key, (int, float))}题目014:使用Python代码实现遍历一个文件夹的操作。点评:基本也是送人头的题目,只要用过os模块就应该知道怎么做。Python标准库os模块的walk函数提供了遍历一个文件夹的功能,它返回一个生成器。import osg = os.walk('/Users/Hao/Downloads/')for path, dir_list, file_list in g:    for dir_name in dir_list:        print(os.path.join(path, dir_name))    for file_name in file_list:        print(os.path.join(path, file_name))说明:os.path模块提供了很多进行路径操作的工具函数,在项目开发中也是经常会用到的。如果题目明确要求不能使用os.walk函数,那么可以使用os.listdir函数来获取指定目录下的文件和文件夹,然后再通过循环遍历用os.isdir函数判断哪些是文件夹,对于文件夹可以通过递归调用进行遍历,这样也可以实现遍历一个文件夹的操作。题目015:现有2元、3元、5元共三种面额的货币,如果需要找零99元,一共有多少种找零的方式?点评:还有一个非常类似的题目:“一个小朋友走楼梯,一次可以走1个台阶、2个台阶或3个台阶,问走完10个台阶一共有多少种走法?”,这两个题目的思路是一样,如果用递归函数来写的话非常简单。from functools import lru_cache@lru_cache()def change_money(total):    if total == 0:        return 1    if total < 0:        return 0    return change_money(total - 2) + change_money(total - 3) + \        change_money(total - 5)说明:在上面的代码中,我们用lru_cache装饰器装饰了递归函数change_money,如果不做这个优化,上面代码的渐近时间复杂度将会是O ( 3 N ) O(3^N)O(3 N ),而如果参数total的值是99,这个运算量是非常巨大的。lru_cache装饰器会缓存函数的执行结果,这样就可以减少重复运算所造成的开销,这是空间换时间的策略,也是动态规划的编程思想。题目016:写一个函数,给定矩阵的阶数n,输出一个螺旋式数字矩阵。例如:n = 2,返回:1 24 312例如:n = 3,返回:1 2 38 9 47 6 5这个题目本身并不复杂,下面的代码仅供参考。def show_spiral_matrix(n):    matrix = [[0] * n for _ in range(n)]    row, col = 0, 0    num, direction = 1, 0    while num <= n ** 2:        if matrix[row][col] == 0:            matrix[row][col] = num            num += 1        if direction == 0:            if col < n - 1 and matrix[row][col + 1] == 0:                col += 1            else:                direction += 1        elif direction == 1:            if row < n - 1 and matrix[row + 1][col] == 0:                row += 1            else:                direction += 1        elif direction == 2:            if col > 0 and matrix[row][col - 1] == 0:                col -= 1            else:                direction += 1        else:            if row > 0 and matrix[row - 1][col] == 0:                row -= 1            else:                direction += 1        direction %= 4    for x in matrix:        for y in x:            print(y, end='\t')        print()题目017:阅读下面的代码,写出程序的运行结果。items = [1, 2, 3, 4] print([i for i in items if i > 2])print([i for i in items if i % 2])print([(x, y) for x, y in zip('abcd', (1, 2, 3, 4, 5))])print({x: f'item{x ** 2}' for x in (2, 4, 6)})print(len({x for x in 'hello world' if x not in 'abcdefg'}))点评:生成式(推导式)属于Python的特色语法之一,几乎是面试必考内容。Python中通过生成式字面量语法,可以创建出列表、集合、字典。[3, 4][1, 3][('a', 1), ('b', 2), ('c', 3), ('d', 4)]{2: 'item4', 4: 'item16', 6: 'item36'}6题目018:说出下面代码的运行结果。class Parent:    x = 1class Child1(Parent):    passclass Child2(Parent):    passprint(Parent.x, Child1.x, Child2.x)Child1.x = 2print(Parent.x, Child1.x, Child2.x)Parent.x = 3print(Parent.x, Child1.x, Child2.x)点评:运行上面的代码首先输出1 1 1,这一点大家应该没有什么疑问。接下来,通过Child1.x = 2给类Child1重新绑定了属性x并赋值为2,所以Child1.x会输出2,而Parent和Child2并不受影响。执行Parent.x = 3会重新给Parent类的x属性赋值为3,由于Child2的x属性继承自Parent,所以Child2.x的值也是3;而之前我们为Child1重新绑定了x属性,那么它的x属性值不会受到Parent.x = 3的影响,还是之前的值2。1 1 11 2 13 2 3123题目19:说说你用过Python标准库中的哪些模块。点评:Python标准库中的模块非常多,建议大家根据自己过往的项目经历来介绍你用过的标准库和三方库,因为这些是你最为熟悉的,经得起面试官深挖的。模块名 介绍sys 跟Python解释器相关的变量和函数,例如:sys.version、sys.exit()os 和操作系统相关的功能,例如:os.listdir()、os.remove()re 和正则表达式相关的功能,例如:re.compile()、re.search()math 和数学运算相关的功能,例如:math.pi、math.e、math.coslogging 和日志系统相关的类和函数,例如:logging.Logger、logging.Handlerjson / pickle 实现对象序列化和反序列的模块,例如:json.loads、json.dumpshashlib 封装了多种哈希摘要算法的模块,例如:hashlib.md5、hashlib.sha1urllib 包含了和URL相关的子模块,例如:urllib.request、urllib.parseitertools 提供各种迭代器的模块,例如:itertools.cycle、itertools.productfunctools 函数相关工具模块,例如:functools.partial、functools.lru_cachecollections / heapq 封装了常用数据结构和算法的模块,例如:collections.dequethreading / multiprocessing 多线程/多进程相关类和函数的模块,例如:threading.Threadconcurrent.futures / asyncio 并发编程/异步编程相关的类和函数的模块,例如:ThreadPoolExecutorbase64 提供BASE-64编码相关函数的模块,例如:bas64.encodecsv 和读写CSV文件相关的模块,例如:csv.reader、csv.writerprofile / cProfile / pstats 和代码性能剖析相关的模块,例如:cProfile.run、pstats.Statsunittest 和单元测试相关的模块,例如:unittest.TestCase题目20:__init__和__new__方法有什么区别?Python中调用构造器创建对象属于两阶段构造过程,首先执行__new__方法获得保存对象所需的内存空间,再通过__init__执行对内存空间数据的填充(对象属性的初始化)。__new__方法的返回值是创建好的Python对象(的引用),而__init__方法的第一个参数就是这个对象(的引用),所以在__init__中可以完成对对象的初始化操作。__new__是类方法,它的第一个参数是类,__init__是对象方法,它的第一个参数是对象。题目21:输入年月日,判断这个日期是这一年的第几天。方法一:不使用标准库中的模块和函数。def is_leap_year(year):    """判断指定的年份是不是闰年,平年返回False,闰年返回True"""    return year % 4 == 0 and year % 100 != 0 or year % 400 == 0def which_day(year, month, date):    """计算传入的日期是这一年的第几天"""    # 用嵌套的列表保存平年和闰年每个月的天数    days_of_month = [        [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],        [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]    ]    days = days_of_month[is_leap_year(year)][:month - 1]    return sum(days) + date方法二:使用标准库中的datetime模块。import datetimedef which_day(year, month, date):    end = datetime.date(year, month, date)    start = datetime.date(year, 1, 1)    return (end - start).days + 1题目22:平常工作中用什么工具进行静态代码分析。点评:静态代码分析工具可以从代码中提炼出各种静态属性,这使得开发者可以对代码的复杂性、可维护性和可读性有更好的了解,这里所说的静态属性包括:代码是否符合编码规范,例如:PEP-8。代码中潜在的问题,包括:语法错误、缩进问题、导入缺失、变量覆盖等。代码中的坏味道。代码的复杂度。代码的逻辑问题。工作中静态代码分析主要用到的是Pylint和Flake8。Pylint可以检查出代码错误、坏味道、不规范的代码等问题,较新的版本中还提供了代码复杂度统计数据,可以生成检查报告。Flake8封装了Pyflakes(检查代码逻辑错误)、McCabe(检查代码复杂性)和Pycodestyle(检查代码是否符合PEP-8规范)工具,它可以执行这三个工具提供的检查。题目23:说一下你知道的Python中的魔术方法。点评:魔术方法也称为魔法方法,是Python中的特色语法,也是面试中的高频问题。魔术方法 作用__new__、__init__、__del__ 创建和销毁对象相关__add__、__sub__、__mul__、__div__、__floordiv__、__mod__ 算术运算符相关__eq__、__ne__、__lt__、__gt__、__le__、__ge__ 关系运算符相关__pos__、__neg__、__invert__ 一元运算符相关__lshift__、__rshift__、__and__、__or__、__xor__ 位运算相关__enter__、__exit__ 上下文管理器协议__iter__、__next__、__reversed__ 迭代器协议__int__、__long__、__float__、__oct__、__hex__ 类型/进制转换相关__str__、__repr__、__hash__、__dir__ 对象表述相关__len__、__getitem__、__setitem__、__contains__、__missing__ 序列相关__copy__、__deepcopy__ 对象拷贝相关__call__、__setattr__、__getattr__、__delattr__ 其他魔术方法题目24:函数参数*arg和**kwargs分别代表什么?Python中,函数的参数分为位置参数、可变参数、关键字参数、命名关键字参数。*args代表可变参数,可以接收0个或任意多个参数,当不确定调用者会传入多少个位置参数时,就可以使用可变参数,它会将传入的参数打包成一个元组。**kwargs代表关键字参数,可以接收用参数名=参数值的方式传入的参数,传入的参数的会打包成一个字典。定义函数时如果同时使用*args和**kwargs,那么函数可以接收任意参数。题目25:写一个记录函数执行时间的装饰器。点评:高频面试题,也是最简单的装饰器,面试者必须要掌握的内容。方法一:用函数实现装饰器。from functools import wrapsfrom time import timedef record_time(func):        @wraps(func)    def wrapper(*args, **kwargs):        start = time()        result = func(*args, **kwargs)        print(f'{func.__name__}执行时间: {time() - start}秒')        return result            return wrapper方法二:用类实现装饰器。类有__call__魔术方法,该类对象就是可调用对象,可以当做装饰器来使用。from functools import wrapsfrom time import timeclass Record:        def __call__(self, func):                @wraps(func)        def wrapper(*args, **kwargs):            start = time()            result = func(*args, **kwargs)            print(f'{func.__name__}执行时间: {time() - start}秒')            return result                return wrapper说明:装饰器可以用来装饰类或函数,为其提供额外的能力,属于设计模式中的代理模式。扩展:装饰器本身也可以参数化,例如上面的例子中,如果不希望在终端中显示函数的执行时间而是希望由调用者来决定如何输出函数的执行时间,可以通过参数化装饰器的方式来做到,代码如下所示。from functools import wrapsfrom time import timedef record_time(output):    """可以参数化的装饰器"""def decorate(func):@wraps(func)def wrapper(*args, **kwargs):start = time()result = func(*args, **kwargs)output(func.__name__, time() - start)return result            return wrapperreturn decorate题目26:什么是鸭子类型(duck typing)?鸭子类型是动态类型语言判断一个对象是不是某种类型时使用的方法,也叫做鸭子判定法。简单的说,鸭子类型是指判断一只鸟是不是鸭子,我们只关心它游泳像不像鸭子、叫起来像不像鸭子、走路像不像鸭子就足够了。换言之,如果对象的行为跟我们的预期是一致的(能够接受某些消息),我们就认定它是某种类型的对象。在Python语言中,有很多bytes-like对象(如:bytes、bytearray、array.array、memoryview)、file-like对象(如:StringIO、BytesIO、GzipFile、socket)、path-like对象(如:str、bytes),其中file-like对象都能支持read和write操作,可以像文件一样读写,这就是所谓的对象有鸭子的行为就可以判定为鸭子的判定方法。再比如Python中列表的extend方法,它需要的参数并不一定要是列表,只要是可迭代对象就没有问题。说明:动态语言的鸭子类型使得设计模式的应用被大大简化。————————————————版权声明:本文为CSDN博主「五包辣条!」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。原文链接:https://blog.csdn.net/AI19970205/article/details/125182872
  • [经验分享] 基于MindStudio的AdvancedEAST模型离线推理
    一、 概述MindStudio提供了基于华为自研昇腾AI处理器开发所需的一站式开发环境,能够在此工具上高效便捷地完成AI应用开发。本文使用MindStudio的模型转换工具将开源框架Pytorch下的AdvancedEAST网络模型转换成昇腾AI处理器支持的离线模型并进行离线推理。AdvancedEAST是一种用于场景图像文本检测的算法,它主要基于EAST: An Efficient and Accurate Scene Text Detector,并进行了重大改进,使长文本预测更加准确。1.参考论文:Xinyu Zhou, Cong Yao, He Wen, Yuzhi Wang, Shuchang Zhou, Weiran He, Jiajun Liang. EAST: An Efficient and Accurate Scene Text Detector. (2017)2.参考实现:url=https://github.com/BaoWentz/AdvancedEAST-PyTorchbranch=master commit_id=a835c8cedce4ada1bc9580754245183d9f4aaa173.适配昇腾 AI 处理器的实现:url=https://gitee.com/ascend/ModelZoo-PyTorchtag=v.0.4.0code_path=ACL_PyTorch/contrib/cv/detection4.输入输出数据输入数据大小数据类型数据排布格式inputbatchsize x 3 x 736 x 736RGB_FP32NCHW 输出数据大小数据类型数据排布格式output_1batchsize x 7 x 184 x 184FP32ND二、 推理环境安装1.运行环境:系统:Ubuntu 18.04.5 LTS 处理器:Ascend 710 2.开发环境:MindStudio:5.0.RC1Ascend-cann-toolkit:5.1.RC1官方安装指南: https://www.hiascend.com/document/detail/zh/mindstudio/50RC1/instg 3.Python环境在Anaconda下创建Python虚拟环境,激活环境并安装必要的依赖:conda create -n zdf python=3.7conda activate zdfpip3 install xxx 依赖名称版本onnx1.7.0Torch1.8.0TorchVision0.9.0numpy1.20.3Pillow8.2.0opencv-python4.5.2.54shapely1.7.1三、 源码和数据集准备1.获取推理源码从以下链接获取推理所需源码,单击“立即下载”,下载源码包。https://www.hiascend.com/zh/software/modelzoo/detail/1/18c7bf3cafac447e849e53e88e2044f9。上传源码包到服务器任意目录并解压(如:/home/zdf)。解压后推理源码目录为:/home/zdf/AdvancedEAST2.获取开源模型源码git clone https://github.com/BaoWentz/AdvancedEAST-PyTorch -b master cd AdvancedEAST-PyTorch git reset a835c8cedce4ada1bc9580754245183d9f4aaa17 --hard cd ..把*.py文件拷贝至推理源码目录辅助推理。 3.获取原始数据集[天池ICPR数据集](https://pan.baidu.com/s/1NSyc-cHKV3IwDo6qojIrKA ),提取码: ye9y本模型使用天池ICPR数据集中的1000张图片作为验证集。下载ICPR_text_train_part2_20180313.zip和ICPR_text_train_part1_20180316.zip两个压缩包,在推理源码目录新建目录icpr和子目录icpr/image_10000、icpr/txt_10000,将压缩包中image_9000、image_1000中的图片文件解压至image_10000中,将压缩包中txt_9000、txt_1000中的标签文件解压至txt_10000中。四、模型推理1.打开 MindStudio, 选择 File -> New -> Project,新建Ascend App项目2.数据预处理执行AdvancedEAST_preprocess.py脚本对数据集进行预处理,包括图片缩放、标签转换为npy文件和图片转换为bin文件。Command Arguments中:第一个参数为数据集的路径,第二个参数为生成bin文件的路径。3.生成数据集info文件使用benchmark推理需要输入图片数据集的info文件,用于获取数据集。执行gen_dataset_info脚本,输入已经获得的图片文件,输出生成图片数据集的info文件。Command Arguments中:第一个参数为生成的数据集文件格式,第二个参数为预处理后的bin文件的路径,第三个参数为生成的数据集文件保存的路径,第四第五个参数为图片的宽和高。运行成功后,在当前目录中生成prep_bin.info。4.模型转换使用PyTorch将模型权重文件.pth转换为.onnx文件,再使用ATC工具将.onnx文件转为离线推理模型文件.om文件。        a.获取权重文件。从源码包中获取权重文件3T736_best_mF1_score.pth。或 (https://pan.baidu.com/s/1NSyc-cHKV3IwDo6qojIrKA ),提取码: ye9y。解压后使用3T736_best_mF1_score.pth,文件sha1: 9D0C603C4AA4E955FEA04925F3E01E793FEF4045        b.导出onnx文件。              执行AdvancedEAST_pth2onnx.py脚本,获得AdvancedEAST_dybs.onnx文件。              Command Arguments中:第一个参数为PyTorch模型的.pth权重文件的路径,第二个参数为生成.onnx文件的路径。        c. 使用MindStudio中的ATC工具将ONNX模型转为 OM模型。如下图所示,点击菜单栏Ascend->Model Converter 调用出模型转换界面。        在“Model Information” 页签中上传模型文件和权重文件后点击 Next 按钮,Data Processing 页面直接点击 Next,Model Converter中Command Preview 里的命令与在命令行运行atc 的命令几乎相同,点击 Finish开始转换。Model Information 界面参数配置如下表所示:参数说明CANN Machine(仅Windows系统支持此参数)自动填充。远程连接ADK所在环境的 SSH 地址,格式为<username>@localhost:端口号。-Model File模型文件。必填。该模型文件需要取消其他用户写的权限。有两种选择方式:1. 单击右侧的文件夹图标,在后台服务器路径选择需要转化的模型文件并上传。2. 在参数后面的输入框中自行输入模型文件在后台服务器的路径,包括模型文件名称-Weight File权重文件。当原始框架是 Caffe 时,该参数存在且必填:1. 如果模型文件和权重文件存在于后台服务器同一目录下,且名称和模型文件名称相同,则选择模型文件后,权重文件会自动填充。2. 如果模型文件和权重文件存在于后台服务器不同目录下,或者在同一目录下,但名称和模型文件名称不相同。-Model Name模型文件名称,必填。选择模型文件后,该参数会自动填充,用户可以根据需要自行修改名称,要求如下:1. 只支持 a-z、A-Z、0-9、下划线以及短划线的组合,最多支持 64 个字符。2. 如果模型转换的输出路径已经存在相同名称模型文件,单击“Next”后会提示覆盖原有文件或重命名当前 Model Name 的信息,用户根据实际情况选择。-Target SoC Version模型转换时指定芯片型号。请根据板端环境具体芯片形态进行选择。-Input Format输入数据格式。1. 当原始框架是 Caffe 时,取值为 NCHW、ND(表示支持任意格式,N<=4),默认为 NCHW。2. 当原始框架是 MindSpore、ONNX 时,取值为 NCHW。3. 当原始框架是 TensorFlow 时,取值为 NCHW、NHWC、ND、NCDHW、NDHWC,默认为 NHWC。-Input Nodes模型输入节点信息。1. 如果选择模型文件并且解析成功,则该参数下方会展示模型输入节点的 shape 信息以及 Type 信息。2. 如果选择模型文件后,无法解析“Input Nodes”,该场景下,需要用户根据模型文件中的相关信息手动输入:单击该参数右侧的,在弹出界面中输入模型输入节点的 Name、Shape 信息(只支持英文逗号,数字(-1 或大于 0 的整数),不能以英文逗号开头,只能以数字结尾)和输入节点的数据类型 Type。单击删除节点信息。3. 如果模型有多个输入,解析成功后,“Input Nodes”参数下方会展示每一个输入节点的 Shape 信息和 Type 信息。-Shape模型输入的 shape 信息,N(模型一次处理的图片个数),C(Channel,例如彩色 RGB 图像的 Channel数为 3),H(Height),W(Width)。若开启 AIPP 功能,则此处的 H,W取值即为 AIPP 输出数据的高和宽。-TypeType:指定输入节点的数据类型。1. 若原始框架类型为 Caffe、ONNX,支持的数据类型为 FP32、FP16、UINT8。2. 若原始框架类型为 MindSpore,支持的数据类型为 FP32、UINT8。3. 若原始框架类型为 TensorFlow,支持的输入数据类型为 FP32、FP16、UINT8、Int32、Int64、Bool。-Output Nodes指定输出节点信息。单击“Select”在弹出的网络拓扑结构中,选中某层节点,右击选择“Select”,该层变成蓝色,单击“OK”后,在“Output Nodes”参数下面会看到标记层的算子,右击选择“Deselect”取消选中。1. Op Name:标记层的算子名称。2. Data Type:算子输出的数据类型,包括 FP32、UINT8、FP16,通过该参数用户可以设置单个算子的输出数据类型。“Output Nodes”参数下方“Select”层的算子,默认为全部选中,用户可以自行选择将不需要输出的算子去勾选,只有选中的算子才会作为模型的输出。某些情况下,用户想要查看某层算子参数是否合适,则需要将该层算子的参数输出,即可以通过单击“Select”按钮,在弹出网络拓扑结构中将所需层的算子标记为“Select”,然后在“Output Nodes”参数下方选中想要输出的算子,模型转换后,在相应.om 模型文件可以看到该算子的输出直接作为模型的输出。-Load Configuration导入上次模型转换的配置文件。如果用户之前转换过模型,无论成功与否,在$HOME/modelzoo/${Model Name}/device/路径都会生成${Model Name}_config.json 配置文件,该文件记录用户模型转换时所选择的配置信息,包括所用模型路径、模型名称、输入输出配置,数据预处理配置等,下次重新转换模型时,通过单击“Load Configuration”选择相应路径下的配置文件,则相应的配置信息会自动填充,用户自行决定是否沿用上次配置还是修改配置后重新进行模型转换。        模型转换成功后会提示:Model converted successfully.如上图所示。 5.开始推理        a.获取Benchmark工具。                  从源码包中获取Benchmark工具。或(https://support.huawei.com/enterprise/zh/ascend-computing/cann-pid-251168373/software/) 将benchmark.x86_64或benchmark.aarch64放到当前目录。         b.启动ssh session,切换python环境、当前工作目录、增加benchmark.{arch}可执行权限        c.推理Benchmark工具运行参数说明:参数说明-model_type模型的类型。-batch_size执行一次模型推理所处理的数据量。-device_id运行的Device编号。-om_path经过ATC转换后的模型OM文件所在的路径。-input_text_path模型对应的数据集所在的路径。-input_width输入模型的宽度。-input_height输入模型的高度。-useDvpp模型前处理是否使用DVPP编解码模块。-output_binary输出结果格式是否为二进制文件(即bin文件)。更多参数具体说明见:https://support.huawei.com/enterprise/zh/doc/EDOC1100191895/4de572a8 推理后的输出默认在当前目录result下。运行结果参数说明:参数说明[e2e]-throughputRate端到端总吞吐率。公式为 sample 个数/时间。-latency端到端时延,即从处理第一个 sample 到最后一个sample 的完成时间。[data read][preprocess][post]-throughputRate当前模块的吞吐率。-moduleLatency执行一次当前模块的时延。[infer]-throughputRate推理模块整体吞吐率,包含线程启动、数据等待、实际推理等时间。-moduleLatency推理模块的平均时延。公式为执行一次推理的时间/batch size,其中执行一次推理的时间包含了内存申请、内存拷贝以及推理等时间。-Interface throughputRateaclmdlExecute 接口的吞吐率。公式为 sample 个数/aclmdlExecute 接口的平均执行时间。        d.精度验证                     执行AdvancedEAST_postprocess.py脚本对推理结果进行后处理,获得精度数据。                  Command Arguments中:第一个参数为数据集路径,第二个参数为推理结果所在路径。五、推理主要目录结构:├── benchmark.aarch64                //离线推理工具(适用ARM架构)├── benchmark.x86_64                //离线推理工具(适用x86架构)├── AdvancedEAST_dybs.om           //batchsize=1的om模型文件├── AdvancedEAST_dybs.onnx        //onnx模型文件├── 3T736_best_mF1_score.pth        //训练后的权重文件├── AdvancedEAST_postprocess.py    // 后处理文件,用于计算精度 ├── AdvancedEAST_preprocess.py    // 前处理文件,用于处理数据集和生成bin文件├── AdvancedEAST_pth2onnx.py      // 用于转换pth文件到onnx文件├── LICENSE                       // LICENSE文件├── README.md                     // 模型离线推理说明README ├── gen_dataset_info.py              // 生成数据集info文件├── requirements.txt                 // 模型离线推理用到的所有且必要的依赖库├── cfg.py                                 //配置文件 控制参数├── preprocess.py                    //预处理数据 调整图像大小├── imgs2LMDB.py                  //生成LMDB数据集├── label.py                              //生成标签信息├── model_VGG.py                  //定义网络├── losses.py                           //定义损失函数├── train.py                              //执行训练├── dataset.py                          //读LMDB数据集├── predict.py                           //预测├── nms.py                               //预测├── utils.py                               //评估模型├── icpr                                    //数据集目录├── prep_dataset                    //bin文件目录├── result                                //推理结果目录└── test     ├── pth2om.sh                   // pth文件转换到onnx文件和om文件的脚本    ├── eval_acc_perf.sh             // 测评推理精度和性能的脚本    └── parse.py                    // 解析结果的脚本 六、FAQQ:执行AdvancedEAST_preprocess.py脚本对数据集进行预处理时提示找不到tqdm模块。A:执行pip3 install tqdm,安装完成后再运行以上脚本。 Q:模型推理时出现输入数据大小和模型输入大小不一致的错误。A:模型转换时,在“Model Information” 页签上数据类型默认是FP16,应选择FP32。
  • [技术干货] GO语言类型系统之值语义和引用语义笔记分享
    值语义和引用语义的差别在于赋值,比如: b = a b.Modify() 如果b的修改不会影响a的值,那么此类型属于值类型。如果会影响a的值,那么此类型是引用类型。 Go语言中的大多数类型都基于值语义,包括: 基本类型,如byte、int、bool、float32、float64和string等; 复合类型,如数组(array)、结构体(struct)和指针(pointer)等。 Go语言中类型的值语义表现得非常彻底。我们之所以这么说,是因为数组。 Go语言中的数组和基本类型没有区别,是很纯粹的值类型,例如: var a = [3]int{1, 2, 3} var b = a b[1]++ fmt.Println(a, b) 该程序的运行结果如下: [1 2 3] [1 3 3]。 这表明b=a赋值语句是数组内容的完整复制。要想表达引用,需要用指针: var a = [3]int{1, 2, 3} var b = &a b[1]++ fmt.Println(a, *b) 该程序的运行结果如下: [1 3 3] [1 3 3] 这表明b=&a赋值语句是数组内容的引用。变量b的类型不是[3]int,而是*[3]int类型。 Go语言中有4个类型比较特别,看起来像引用类型,如下所示。 数组切片:指向数组(array)的一个区间。 map:极其常见的数据结构,提供键值查询能力。 channel:执行体(goroutine)间的通信设施。 q 接口(interface):对一组满足某个契约的类型的抽象。 但是这并不影响我们将Go语言类型看做值语义。下面我们来看看这4个类型。 数组切片本质上是一个区间,你可以大致将[]T表示为: type slice struct {     first *T 2     len int     cap int } 因为数组切片内部是指向数组的指针,所以可以改变所指向的数组元素并不奇怪。数组切片类型本身的赋值仍然是值语义。            map本质上是一个字典指针,你可以大致将map[K]V表示为:                                            4 type Map_K_V struct {  // ... }                type map[K]V struct {                                                          5     impl *Map_K_V } 基于指针,我们完全可以自定义一个引用类型,如:               type IntegerRef struct {                                                       6    impl *int } channel和map类似,本质上是一个指针。将它们设计为引用类型而不是统一的值类型的原因是,完整复制一个channel或map并不是常规需求。 7 同样,接口具备引用语义,是因为内部维持了两个指针,示意为: type interface struct {     data *void     itab *Itab               }                                                                              8 接口在Go语言中的地位非常重要。