代码
经验分享|昇思MindSpore周旭林:首先学会像成功者那样自信

经验分享|昇思MindSpore周旭林:首先学会像成功者那样自信

经验分享|昇思MindSpore周旭林:首先学会像成功者那样自信

01、关于昇思MindSpore项目介绍****项目名称

昇思MindSpore CPU正向算子开发:SparseApplyProximalAdagrad(注:项目内容在开放申请阶段经过了社区调整,后续实际开发的算子为EditDistance,其它描述不变)

项目链接

https://summer-ospp.ac.cn/#/org/prodetail/221cb0310

项目描述算子开发流程:我的项目关于算子开发,交付目标是完成一个CPU正向算子。先来解释这些概念。

算子是什么

算子是什么?算子是深度学习框架的基本组件。一般AI模型由若干个计算模块(即网络中的block)组合而成,比如全连接层Dense、卷积层Conv、激活函数Relu就是计算模块,模块的实现是通过调用封装好的算子来完成的。 算子怎么分类?由于计算模块的数据处理对象一般为Tensor,而不同的后端硬件平台对Tensor的存储、处理和计算方式有所不同(如何基于GPU和Ascend等AI加速器的硬件特点对算子实现做特殊处理以使其执行速度更快,这是另一个有趣的话题)。MindSpore对 CPU、GPU 和 Ascend 等硬件平台都提供支持,因此根据算子所支持的不同设备的底层逻辑,可分为 CPU 算子、GPU 算子和 Ascend 算子。因为AI模型需要进行反向传播,所以有时需要定义一个新算子来履行原先算子的反向实现功能,因此就有正向算子和反向算子之分。

算子如何开发

算子开发本质上是打通算子在MindSpore中被调用的各个环节,这些环节可分为Python前端和C++后端两个部分。算子通过Python前端的接口被调用,并在具体硬件平台的后端实际执行。 算子如何被调用?MindSpore支持静态图模式,在该模式下AI模型的工作的会经过构图、图执行这些步骤:首先在前端构图环节,会将编写模型的 Python 语言中用到的算子接口API(比如mindspore.ops.Conv表示卷积模块)表达翻译为计算图(由计算模块共同组成的有向无环图);接下来在图执行环节,每个计算模块所对应算子输入的 type 和 shape 才会被推导(即构图完成后才能拿到,这样处理是为了能在不考虑Tensor具体信息的情况下先对计算图做编译优化);最后后端会根据图以及算子名称、输入输出个数和类型匹配具体平台的算子实现,并将执行结果返回到 Python 侧进行输出。

那么,算子如何开发?遵照算子被调用的逻辑,主要需要开发 Python 算子原语、C++侧输出推导文件和基于硬件平台的底层实现文件,具体来说:

**1.Python 算子原语开发:**算子原语就是算子的前端接口 API,在 MindSpore 中用 Python 实现,是组成网络模型的基本单元,可以被运行在不同硬件平台上的算子共用。算子原语主要包括算子的名称、属性、输入输出名称、输出 shape 推理方法、输出 data type 推理方法等信息。在存在 C++侧输出推导文件时,Python 算子原语的开发意义主要在于定义名称和属性;

**2.C++侧输出推导文件开发:**C++侧输出推导文件本质上是算子原语的 C++形式。由于算子属性的初始化已在 Python 算子原语中完成,因此 C++算子原语的主要功能是对输入数据的规范性进行校验,并推导算子输出的 data type 和 shape。这里其实可以将C++侧输出推导文件理解为算子前端,单独抽取出来并写成C++文件的意义主要是为了保证一般算子在推导时的高效。目前只有当遇到动态shape等特殊情况时,为了方便处理才会直接在Python算子原语中推导;

**3.CPU 底层实现文件开发:**利用框架提供的 C++ API,结合算子具体特性实现算子内部计算逻辑,并在保证结果和精度达标的基础上尽量保证性能达到最优。这里的C++ API 除了经典的STL库以外,还包括MindSpore提供的辅助类和方法。 **总的来说,**算子这个组件在MindSpore框架中,分为了算子前端和算子后端:其中前端是算子接口 API,用于从抽象上指代某类算子并供用户调用;后端是对用户不可见的算子底层实现,随实际运行平台而不同。算子开发既要实现算子接口API(重点是实现其中的data type和shape的推导流程),也要完成后端底层实现。我当时修改并交付的代码包括以下文件,最终在Gitee中以PR的形式合入。

文件名

功能含义

修改描述

edit_distance_cpu_kernel.cc

EditDistance算子CPU底层实现的源文件

实现算子的底层逻辑,包括具体算法实现和新增数据类型支持

edit_distance_cpu_kernel.h

EditDistance算子CPU底层实现的头文件

定义算子的所用数据结构,实现对编辑距离的求解

edit_distance.cc

EditDistance算子C++侧推导文件

从type和shape等角度验证算子输入的规范性

array_ops.py

EditDistance算子Python前端接口

更新算子基本信息描述

算子如何验收

算子开发除了交付代码以外,为了方便验证和合入代码,还需要提交开发者自测交付件。交付件需要涵盖算子设计文档、算子自测用例、测试报告与执行结果等。这些交付件需要给华为的测试负责人验收,一般他们关心以下方面:

**1.算子文档是否规范?**交付件需要证明该算子的文档齐全且为最新(比如,若打通了CPU平台上该算子的支持,就要更新官网上的适用平台说明);

**2.代码交付是否完全合格?**这里的合格除了接口规格要与友商保持一致以外,还需要保证结果在各种情况下都正确。为了证明这点,交付件中需要包括根据“算子自验报告”中的逐个细节(比如Tensor维度取从0维到7维的情况、输入数据类型不匹配的报错情况等)一一编写。算子自验报告需要面面俱到,当时的报告长得像是以下这样子。

图片.png

因此,交付件需要清楚地涵盖到以上的内容。在我的交付件中,一级目录的文件和文件夹描述如下。

名称

类型

描述

doctest自验执行结果

截图

算子的文档规范验证结果

算子自验报告

Excel表格

自测数据的构造逻辑和覆盖内容

算子自验测试用例

由自测数据组成的代码库

包括20个自测数据和必要的测试脚本

EditDistance测试用例执行结果.docx

截图

算子通过自测数据的截图证明

EditDistance接入设计文档.doc

Word文档

算子开发文档

其文件树结构如下所示。

图片.png

02、项目开发方案描述

模型开发流程

下面介绍我负责的项目内容和开发细节,内容组织遵循项目的开发思路。

开发思路

在了解了算子开发流程后,我遵循了以下流程进行开发前调研、开发中划分模块和开发后自测检验。

**1.算子分析:**了解算子的作用、适用范围和特性,关注算子的属性、输入和输出参数的shape和dtype等规格;

**2.接口实现:**基于算子的功能设计面向用户的Python API接口,并按照指定文档格式添加算子的基本信息。由于MindSpore中已有EditDistance算子的Python接口,因此本次开发不涉及该部分;

**3.输入规范性检验:**即实现C++侧推导文件。需要考虑包括算子名称、属性、输出shape、dtype推理函数等信息,并对不合理的输入抛出异常;

**4.实现基于特定平台的底层逻辑:**在本项目中为基于CPU平台实现算子内部逻辑,主要包括“序列生成”、“序列比较”和“编辑距离求解”三个模块。其中,“序列生成”模块负责从所输入的稀疏矩阵中提取出需要计算编辑距离的序列;“序列比较”模块负责对所提取出的序列进行枚举和比较;比较后需要输出的结果通过调用“编辑距离求解”模块得到。

**5.测试用例构建:**测试用例包括UT测试用例和ST测试用例。前者需要附在Pull Request上面,在本项目中不涉及;后者无需跟开发代码一同上库,但是需要根据测试提供的模板编写用例,与标杆对比结果,并全量测试算子功能和异常场景。

算子分析与示例演示

我的项目开发的算子对象是EditDistance算子,需要对标TensorFlow已有实现来完成交付。该算子的功能是计算两个序列的编辑距离。序列将作为输入以稀疏矩阵的形式给出。提一下这里涉及到的两个概念:

编辑距离是是衡量两个字符串(序列)差异程度的量化标准。具体来说,给定当前字符串s和期望字符串t,并定义如下操作均为合法操作:(1)在s插入一个新字母;(2)在s中删除一个原有字母;(3)替换s中的任意一个字母为其它字母。则s和t的编辑距离,就是将s变成t的最小合法操作数。

稀疏矩阵是内部元素大部分都为无意义值的矩阵,它是相对于稠密矩阵而言的。在深度学习框架中,稠密矩阵一般用 Tensor 存储,因为 Tensor 是存储多维数组的理想数据结构;但是稀疏矩阵一般会采用特定的表示方法,即通过只存储在给定索引上有意义的元素值及其下标来表示一个矩阵。其中一种表示方法需要三个参数:indices、values 和 shape,其中:

  1. indices 表示稀疏矩阵中所有有意义值各自所在的位置。它是一个 shape 为 [N, R] 的 Tensor,其中N表示稀疏矩阵中有意义值的个数,R表示该稀疏矩阵的维度;
  2. values 表示稀疏矩阵中所有的有意义值。它是一个 shape 为[N]的 Tensor,用来给 indices 中的每个位置提供具体数值;
  3. shape:表示该稀疏矩阵的维度。它是一个包含R个数的元组或 shape 为[R]的 Tensor。

现在,了解了算子作用后,**我遇到的第一个挑战是这个算子的输入太多了,且序列之间比较的逻辑看起来不容易理解。**贴一个该算子对参数和输入的具体规范感受下。

normalize:bool类型参数,在EditDistance算子类初始化时传入。若为true,表示输出得到的编辑距离会被目标序列的长度normalize;
hypothesis_indices :Tensor类型输入,要求shape为[N,R],其中N表示hypothesis中有意义的值的个数,R表示hypothesis的矩阵维度;
hypothesis_values:Tensor类型输入,要求shape为[N];
hypothesis_shape:Tensor类型输入,要求shape为[R];
truth_indices:Tensor类型输入,要求shape为[M,R],其中M表示truth中有意义的值的个数,R表示truth的矩阵维度,且该维度应与hypothesis的矩阵维度保持一致;
truth_values:Tensor类型输入,要求shape为[M];
truth_shape :Tensor类型输入,要求shape为[R]。
output:以Tensor形式返回的编辑距离,该Tensor的shape为R-1,为表示当前序列的稀疏矩阵和表示目标序列的稀疏矩阵在对应序列位置上的编辑距离。

让我们通过手工计算求解基于这些输入的编辑距离。

hypothesis的稀疏矩阵维度为(2,1,1),它实际上表示的是一个维度为(2,1)的序列矩阵,且序列中的元素个数不超过1。即位置在(0,0)的序列内容为[1],位置在(1,0)的序列内容为[2],为方便指代,可简记为:

(0,0) = [1]

(1,0) = [2]

truth的稀疏矩阵维度为(2,2,2),它实际上表示的是一个维度为(2,2)的序列矩阵,且序列中的元素个数不超过2。可简记为:

(0,0) = []

(0,1) = [1]

(1,0) = [2, 3]

(1,1) = [1]

由于truth的序列矩阵相关维度包括了hypothesis的序列矩阵相关维度,因此在计算编辑距离之前,会自动将hypothesis的序列矩阵维度转化为与truth的一致,并将额外增加的位置对应的序列内容置为空。随后,比较序列矩阵中的每一个序列,具体来说:

  1. 对于(0,0)位置:hypothesis的序列为 [1],truth的序列为 [],因此编辑距离为1;由于truch的序列长度为0,因此normalize后的编辑距离为inf;
  2. 对于(0,1)位置:hypothesis的序列为 [],truth的序列为 [1],因此编辑距离为1;由于truch的序列长度为1,因此 normalize后的编辑距离为1;
  3. 对于(1,0)位置:hypothesis的序列为 [2],truth的序列为 [2,3],因此编辑距离为1;由于truch的序列长度为2,因此normalize后的编辑距离为0.5;
  4. 对于(1,1)位置:hypothesis的序列为 [],truth的序列为 [1],因此编辑距离为1;由于truch的序列长度为1,因此normalize后的编辑距离为1。

采用edit_distance算子所得的结果如下。可以看出实际输出结果与上述推导的结果一致。

图片.png

输入规范性检验

在规范性检验里,**我遇到了开发中的第二个挑战,即由于 EditDistance 算子的参数较多,对它们的规范性校验较为复杂,需要细心处理。**这个过程既需要单独考虑每个参数的特定规格,又需要考虑不同参数之间的约束和联系(比如代表同一个稀疏矩阵的三个参数必须遵循稀疏矩阵的构造规范)。在Editdistance的开发中,依次通过如下检查步骤来完成算子输入的规范性检验,若其中一个不通过则抛出具体的报错异常:

  1. 确认输入为六个参数;
  2. 确认六个参数的 dtype 都为 Tensor;
  3. 获取六个参数的 shape 并做检查。对于分别表示两个稀疏矩阵索引的两个参数,确认它们的秩为 2;对于其他四个参数,确认它们的秩为 1;
  4. 确认表示稀疏矩阵索引和表示稀疏矩阵索引范围的两对参数,它们的维度数量一致;
  5. 确认表示稀疏矩阵索引范围的两个参数的值保持一致或满足自动广播条件;
  6. 确认六个参数的元素数据类型合法,其中表示稀疏矩阵索引的参数和表示稀疏矩阵索引范围的参数元素数据类型必须为 int64,表示稀疏矩阵值的一对参数元素数据类 型必须一致且在所支持的参数列表内。

序列生成

序列生成负责基于输入的稀疏矩阵得到需要计算编辑距离的序列。**该部分开发的难点在于无法直接参照TensorFlow源码的算子实现逻辑,这是我在开发中的遇到的第三个挑战,**也是本项目的难点所在。

一般算子开发任务是对标友商中的已有实现的,这会给开发工作带来便利:因为已经有了现成的源码,只需要参考逻辑实现“本土化”移植到mindspore中就可以了。但是本次开发却行不通,这是因为在TensorFlow中,EditDistance算子的内部实现涉及将输入的三个Tensor转化为一个与其对应的稀疏矩阵,并使用稀疏矩阵的遍历方法来对矩阵进行处理,因此需要动用专门的SparseTensor数据结构完成稀疏矩阵的通用操作。但是MindSpore中没有此类数据结构(更具体地来说,是没有实现稀疏矩阵的顺序遍历方法),因此只好自行构造方案进行处理。

本次开发采用的方案基于朴素的枚举思路,方案伪代码如下。

图片.png

其中,indices、values和shape三个输入参数共同表示一个稀疏矩阵,该稀疏矩阵可能存在部分有实际值的下标是落在输出矩阵的某一个特定下标output_index对应的位置里面的,算法的目标就是对于特定的output_index,将稀疏矩阵中所有落在该位置的有意义的值收集起来生成一个序列,以便后续进行比较。

当然在实际开发中,还需要考虑代码书写的规范性。比如两个不同数据类型变量的计算需要调用工具函数做显性类型转换,并判断生成序列的所在下标是否超出输出矩阵范围等,这里就不做详细讨论了。

序列比较

在基于两个稀疏矩阵hypothesis和truth都生成了对应的序列后,需要通过序列比较模块,对位于同样输出下标的序列进行比较以计算编辑距离。这里遇到了第四个挑战:该模块尽管可以通过简单循环和嵌套判断直接实现,但是函数内环形复杂度就会过大导致无法通过门禁。这里的环形复杂度是一种程序逻辑复杂性的度量,程序中所包含的判断和循环语句越多,环形复杂度越大。

为了在保证功能的前提下精简算法,首先需要分类讨论序列比较的八种不同情况如下。

图片.png

若采用直接的判断方法,则每种对应情况需要进行三次分支判断才能检索到。为了降低环形复杂度,修改思路是将部分判断“隐藏”在另一个函数里,通过两个函数合理分摊分支判断,这样就能避免某个函数的环形复杂度过大的情况。

注意到当且仅当 truth 序列为空且 normalize 参数为 true 时,需要根据 hypothesis 序列是否为空来判断是返回 inf 还是 0,而除了这种情况以外的情况都可以直接调用编辑距离求解函数求解。通过这种方式就可以将对序列是否为空的判断转移到求解函数中(求解函数会在下一节探讨),从而降低当前函数的环形复杂度。这个部分的码风长如下这样子。

// calculate distance
    if (normalize_ && truth_seq_size == 0) {
      output_addr[output_index] =
        (hypothesis_seq_size != 0 ? std::numeric_limits::infinity() : static_cast(0));
      continue;
    }
    auto cmp = std::equal_to();
    size_t dis = LevenshteinDistance(truth_seq, hypothesis_seq, cmp);
    output_addr[output_index] = (normalize_ ? SizeToFloat(dis) / SizeToFloat(truth_seq_size) : SizeToFloat(dis));
  }

编辑距离求解

在完成序列生成和序列比较两个预处理步骤后,我们就把算子开发任务转换成了:给定两个已经从稀疏矩阵中单独抽离出来的纯粹的序列,求解它们的编辑距离。**这是第五个挑战,难度等同于这道力扣题:**力扣_[1]_。

可以用动态规划解决,动态转移方程为如下。

图片.png

其中,dis(i, j)表示 s 的前缀子串 s[0i]与 t 的前缀子串 t[0j]的编辑距离,[s[i − 1] ≠ t[j − 1]] 表示若 s 的第 i 位字母与 t 的第 j 位字母不同,则该值为 1;否则为 0。最终 s 和 t 的编辑距离为dis(s. length, t. length)。

下面是这道力扣题的求解代码。

class Solution {
public:
    int minDistance(string word1, string word2) {
        vector> dis(word1.size()+1, vector(word2.size()+1,0));
        int len1 = word1.size();
        int len2 = word2.size();
        for (int i=0;i<=len2;i++) dis[0][i]=i;
        for (int i=0;i<=len1;i++) dis[i][0]=i;
        for (int i=1;i<=len1;i++)
        for (int j=1;j<=len2;j++)
        {
            dis[i][j]=min({dis[i-1][j]+1,//删除word1的一个字母 
                        dis[i][j-1]+1,//删除word2的一个字母,等价于增加 word1的一个字母 
                        dis[i-1][j-1]+(word1[i-1]!=word2[j-1])});
                        //修改word1的一个字母。若该字母其实无需修改,则不增加合法操作数 
        }
        return dis[len1][len2];
    }    
};

下面是本次项目开发关于编辑距离求解部分的函数。

template 
inline size_t LevenshteinDistance(const std::vector &s, const std::vector &t, const Cmp &cmp) {
  const size_t s_size = s.size();
  const size_t t_size = t.size();
  if (s_size == 0 || t_size == 0) {
    return std::max(s_size, t_size);
  }
  std::vector> dis(s_size + 1, std::vector(t_size + 1, 0));
  for (size_t i = 0; i <= t_size; i++) dis[0][i] = i;
  for (size_t i = 0; i <= s_size; i++) dis[i][0] = i;
  for (size_t i = 1; i <= s_size; i++)
    for (size_t j = 1; j <= t_size; j++) {
      dis[i][j] =
        std::min({dis[i - 1][j] + 1, dis[i][j - 1] + 1, dis[i - 1][j - 1] + (cmp(s[i - 1], t[j - 1]) ? 0 : 1)});
    }
  return dis[s_size][t_size];
}

从上面两个代码片段可以看出:同样是封装成函数,实际开发场景与力扣模拟场景还是存在值得注意的区别。举例来说,需要用 C++ 的 template 机制实现以支持不同种类的数据类型,并且传入自定义比较类型 Cmp 实现 所传入的序列元素的对比;另外还要考虑到在 EditDistance 算子的使用场景中,大部分比较的两个序列中至少有一方的长度为 0,因此增加相应的特殊判断显得更有必要,同时也可以均摊上一节提到的环形复杂度。

项目开发完成后,就需要一一编写针对性测试用例进行验证。若多次测试无误,就可以将代码和交付件转交给测试人员验收,同时预告项目开发基本完成了。最后放上Editdistance的MindSpore官网链接_[2]_。 03、随访

参与开源之夏

**ospp:**请简单介绍一下自己和你的开源经历吧

**xlinsist:**我是来自暨南大学19级智能科学与技术专业的周旭林,**我最开始投身开源的动机是希望能以更有效的方式接触和揭秘AI“黑盒”背后的技术。**在日常专业课的学习中,老师会给我们讲授神经网络和深度学习模型的概念和原理,并提供机会让我们基于TensorFlow搭建模型进行实践。由于我时常好奇所用到的这些库的内部实现方式,因此经常会通过查阅技术博客采撷对这些细节的解读。这个习惯为我能“随机搜索”到开源项目提供了基础。

“梦开始的地方”是在今年初关注到的华为开源书籍《机器学习系统:设计与实现》。这本书讲述了 AI 模型以计算图的形式经过框架和编译器并最终得到部署的完整流程。由于彼时鲜有全面涉及相关内容的教材,我饶有兴趣地参与了这本书的内测阅读,提出了一些关于排版的建议和修订并最终在书籍检视活动中获了奖。这在给我带来成就感的同时也让我了解到了这样一种社区玩法,即通过积极参与自己感兴趣的方向中正在开展的活动,能够让你保持对前沿资讯的关注并投入到有实际价值的事情中。

**ospp:**你是从什么渠道了解开源之夏的,为什么选择参与开源之夏?

****xlinsist:****由于书籍检视活动让我意犹未尽,于是我开始有意识地在网上寻找其它机会,就是在这个过程中我认识了开源之夏。事实上,好的开源平台自然会吸引开发者关注、参与并留下产出。开源之夏就是这样一个优秀的平台,它的开发流程、导师制度和奖励机制能够很好地激发我们参与其中的热情,因此我关注到了它。在翻看了前两届学生的活动记录和结项成果后,我当即认为这就是一个我可以争取的机会,因为通过主动了解前辈们的开发心得和项目经验,我实际上是为自己确定了在项目开发中想成为的榜样,而榜样的树立可以为我带来无形的激励力量。

参与开源社区

**ospp:**介绍一下你在开源之夏活动的项目吧,你是怎么从这么多项目中选中它的呢?

****xlinsist:****我选择的项目关于算子开发,交付目标是对标友商开发完成一个CPU算子并验证通过。这里的算子是深度学习框架的基本组件,深度学习模型中一个网络层的实现就是通过调用算子来完成的。挑中该项目主要出于两个考虑:首先,算子开发本质上是打通算子在MindSpore中被调用的各个环节,这些环节包括Python前端和C++后端两个部分,我认为理清算子开发流程对我接触MindSpore这个“黑盒”背后的技术会有很大帮助;再者,算子开发是一个通用的开发任务,它不要求开发者预先对框架的某一组件部分有较深的理解基础,但是需要有一定的源码理解和编程实现能力,可以参考友商框架的实现思路在MindSpore中完成逻辑开发。我相信当开发思路大致清楚后,这就不会是一个特别难上手的任务,因此我把它视为我在昇思MindSpore社区的“good-first-issue”,它是我上手MindSpore的一个很好的切入点。

图片.png

**ospp:**在开展项目的过程中,涉及陌生的知识领域你是如何应对的?

****xlinsist:****开展项目对我来说是一个从0到1的过程。除了理清算子开发的完整流程,我还需要熟记目标算子的功能和参数,熟悉MindSpore的安装和使用,才可以开始编码开发。编码期间需要了解库函数的使用并遵循一定的编程规范,编码完成后也需要遵循验收标准提供交付件。这些过程看似繁琐,但都是项目开发的必经之路。因此我不但需要适应它,还需要刻意将这些细节培养成自己的开发习惯。

在这样的意识指导下,我会用一种“终局思维”去应对陌生的知识领域,即为了推进这个项目,我首先要走出当下因知识盲区而面临的困惑;为了解答这个困惑,我首先要弄清楚某个具体知识点的含义。这样就将当下遇到的大困惑转换成小困惑,大困难拆解成小困难,最后通过研读源码和求助社区和导师不重复也不遗漏地解决一个个小困难,最终就能解决问题。

**ospp:**项目进行中社区和导师给你带来了怎样的帮助?

****xlinsist:****在项目开发前后我幸运地得到了社区前辈们的支持,他们为我在开源社区中快速成长带来了很多帮助,对此我由衷表示感谢。

首先要感谢我的导师,他对我在开发过程中遇到的困惑和瓶颈都有问必答。每周我都会将当前工作进展汇总后发给他交流确认,在重要的开发阶段也会建立线上会议与他直接探讨。跟导师的及时交流让我在项目开展时逐渐建立起一种自信,即眼前项目最终一定能完成,因为我所遇到的流程性问题都能在他那寻求解答,而对于相对复杂的细节和程序性问题也能够找他磋商,这对项目进展起到了非常积极的推进作用。

然后也要感谢社区的算子测试负责人,他帮助我仔细检查了自测交付件的规范性,并在处理转测遇到的突发情况时提供了关键的设备资源和材料。在开发代码刚开始转测时,曾出现测试环境与本地环境不一致导致无法验证自测结果的问题。为了提高沟通效率,我在导师的帮助下直接单方面联系到他,他为我提供了众智服务器以保证源码编译验证的高效性,并及时地将测试结果发与我交流。这些验收反馈对保证交付质量具有重要意义。

最后还要感谢在社区和交流群中为我答疑解惑的前辈们。在最开始准备项目申请书的日子里,我曾参加华为SIG的内部交流会向前辈提出自己的疑问,也曾在交流群里向老师们寻求参考资料和学习建议,令我感激的是我每次都能得到他们热心且详细的答复,这对我在申请书的撰写和项目的学习上都带来了很大帮助。

**ospp:**通过参与这次开源之夏的活动,你对开源社区有什么新的理解和看法?你会怎么跟在校学生介绍或推荐开源社区?

****xlinsist:**开源本身就是一个很棒的想法。从宣传的角度来看,它为社区和外围开发者搭建起了交流的桥梁,这一方面可以提高社区的知名度,另一方面可以满足开源爱好者对关注前沿和参与其中的需求,是一个“双赢”**的策略;从运作的角度来看,吸引外部的开发者可以为社区输送有潜力的人才,改善人员结构,并带动社区的活跃氛围。

开源社区是一个宝贵的资源库。对于像我这样的在校学生来说,最初踏入开源社区仿佛打开新世界的大门,那里有大量深度和广度兼具的成功项目,它们会帮助你建立起你跟其它技术大佬的联系。**在有这样丰富机会的环境下,你会自觉地去思考自己想要的是什么,而不是受限于自己在学校只接触过什么;你会开始跟随自己的兴趣去探索,而不是盲目跟随大家都在讨论的规划。**你不需要顾虑自己以前没参与过类似项目,因为社区中有很多适合你的“good-first-issue”可以作为切入点;你不需要担心自己的水平有限,因为社区里总会有热心高手为你答疑解惑。你要做的是“Fake it till you make it”,即先从“假装”自己就是以后也想成为的大佬中获得自信,像他们那样子参与其中,迈出第一步,直到真的能把这个事情办成。

在校学生参与开源项目有许多好处。首先,它是项目导向的,即需要在规定时间内完成项目开发,但是整体节奏可以自行安排,这就为那些在学校尚有学习任务但是想体验**“兼职实习”的学生提供了机会。其次,由于项目的ddl和任务指标较为明确,这反而有利于推动自己完成一些看似不可能的挑战。再者,这样的一次项目经历会成为大学阶段的一个代表性成就,是一个极佳的“面向简历学习”范例**,即所学的内容和所做的事情刚好能成为自己简历上有价值的一笔,后续去面试时,就能有东西可以说。尽管对方不一定会关心你的项目细节,但是从你的申请书撰写、项目开发推进过程、项目难点解决方案中,也可以了解你的思考历程和行动方式,这会成为一个亮点。

收获与寄语

**ospp:**这次的开源之夏你顺利完成并结项,有什么珍贵的经验可以分享给大家呢?

****xlinsist:****首先,在开发前再怎么强调调研的重要性都不为过。开源之夏会根据大家的项目申请书决定中选学生,因此我会建议参与开源之夏的学弟学妹们提早决定和准备自己的项目,做足充分的调研并将自己对项目的理解尽可能详细地呈现在申请书上。然后,在调研的过程中会涉猎到很多技术博客,有些博客的观点和细节可能让人“眼花缭乱”,这时切记不要迷信和盲从,而是回归自己的项目内容并诉诸实践。因为当你决定承担起这个项目时,你就是这个项目的“专家”,你应该敢于让自己成为这方面的“权威”,并通过理性输出自己的见解来打动他人。

在正式开发时,注意要**跟导师主动沟通,做好及时反馈。**这是因为一旦项目开始,没有人会清楚你目前的项目进展和细节,一般也不会直接打扰和过问,直到你能将自己的进展反馈出来,并主动讲述自己的情况。我在开始开发项目时,就跟导师约好我会于每周五晚上用几句话概括本周的工作和下周的安排,并将自己目前遇到的问题或觉得会有挑战性的地方备注在后面。这样的反馈沟通既是对导师的尊重,也表明自己正在把握着对项目的掌控感。

在任何时候,**若需要资源和信息都可以跟社区多多求助。**我在前面也提到过社区甚至为我提供了众智服务器验证本地环境,这意味着我不再需要在自己的轻薄本上编译一次就要挂一个晚上等待了:) 这极大提高了我的生产效率。另外在最后的转测阶段,为了合力定位问题,社区还提供了原有测试仓的部分关键用例供我进行本地检验,这就给任务的交付质量多带来了一层保障。

**ospp:**最后,有什么话想对准备参与开源之夏的学弟学妹们说的?

****xlinsist:****我曾经在项目申请书的结尾这样写道:“希望如有机会,我也能够成长为后续开源之夏活动的导师,以实际行动点亮开源计划中的一盏指明灯,为后辈们指引光明的道途。”我始终相信开源社区是一个注重传承的社区,既能承载过来人的经验,也能包容后浪们的智慧。我会继续以实际行动投入到开源社区的建设中,尽我所能给大家解答困惑。也希望大家在开展项目时,能够主动参与到开源社区中来,积极参与到大家的讨论中来。当我们的力量汇聚在了一起时,我们的光明就是黑夜中璀璨的星空。

[1]https://leetcode.cn/problems/edit-distance/

[2]https://www.mindspore.cn/docs/zh-CN/master/api\_python/ops/mindspore.ops.EditDistance.html