resume points

多任务模型:
1. 平行依赖-软共享: MMOE\PLE,顺序依赖-硬共享: ESMM\ESM2\AITM
2. MMOE就是embedding+多个专家网络,每个tower都有个gate控制专家网络的输出。
3. PLE多层特征抽取层,每层特征抽取层又分为共享专家网络和任务专家网络。
4. ESMM硬共享embedding后配合概率联乘构建tower(ctr和cvr), loss=ctr+ctr*cvr(ctcvr)。
5. ESM2引入中间复杂的行为概率分支路径,如购买可能来自收藏、心愿单、购物车等。
6. AITM是tower出来过AIT(self-attention),然后经过MLP info层,控制上层信息流入下游环节。

抗噪损失:
1. 抗噪损失的定义是:Loss能让模型在干净数据和混杂噪声标签的数据上分别取得相同的预测效果;
2. 对称损失函数能抗噪。而对称损失函数是指样本在所有类别下的损失之和固定为一个常数。而MAE是,CCE和MSE不是。
3. 广义交叉熵GCE改进了CE,加入q,(1-f(x)^q)/q,q为0时,即CE,q=1时MAE, 通过q能控制抗噪程度。
4. 对称CE是考虑现有分布并不一定代表真实的类分布,所以,对称ce=假定类分布真实的ce+假定类分布不真实的ce,同时用alpha和beta控制拟合和鲁棒的偏好。
5. L_dmi是基于行列式的互信息求损失,它是希望分类结果和标签的互信息大。互信息是联合分布p(x,y)与边缘分布p(x)p(y)的相对熵,sum_x sum_y p(x,y)log(p(x,y)/p(x)p(y))。
6. 主动被动损失 (Active Passive Loss, APL):主动损失(只关注目标类的损失,非目标类为0,如CE)+被动损失(非目标类的损失也关注,如MAE)。

 

多任务调权:

1. Magnitude(Loss量级):Loss值有大有小,出现取值大的Loss主导的现象,怎么办?
2. Velocity (Loss学习速度):任务有难有易,Loss学习速度有快有慢,怎么办?
3. Direction(Loss梯度冲突:多个Loss的反向梯度,更新方向冲突,出现翘翘板、负迁移现象,怎么办?

- GradNorm: 是基于不同任务的学习速度调整任务权重,学习速度越快,权重越小。计算任务i,在t轮下相较第1轮损失的差异,若第t轮损失变得很小,则Li/L0越小,代表该任务在历史训练的很快。该任务相比于其他任务来说,在历史训练中学习得更快。

- DWA: 受到GradNorm的启发,它也通过考虑每个任务的损失改变,去学习平均不同训练轮数下各任务的权重。GradNorm需要接触网络内部梯度,而DWA提出只要任务的损失数值,所以实施起来更简单,计算损失相对衰减率w_k=L_{t-1}/L_{t-2},然后喂入softmax获取各任务的权重,这样w_k越小,权重越小,即近期训练过程中学习速度快的任务,要给低的重要度。

 

样本选择:
- K折投票过滤法:k折,每折训练k个分类器,每个样本k个预测结果,若多数预测错误,认为是噪声标签;
- MentorNet: 老师MentorNet学习教学策略,给学生样本权重,学生StudentNet提供特征给老师(loss、epoch percentage、label和loss_diff;
- Curriculum: 算各样本的特征表征之间的欧式距离,然后基于样本之间的密度(统计样本i之间距离阈值范围内的点数量,作为样本i的密度),用Kmeans聚类,分为干净、难和噪声样本,然后课程学习;
- coteaching: 在每个mini-batch内采样较小训练损失的样本(认作干净样本)给它们peer network;
- coteaching+: coteaching会收敛成一个共识,为此引入分歧样本(预测不一致),再选择小损失数据;
- AUM: 正确样本的正确类logit更大,错标样本的错标类的logit比其他类小(因为模型没信心), margin=当前logit-其他类的logit,margin负为错标样本;
- JoCoR: 通过对称KL散度鼓励两个网络相近,主张把干净样本参与训练;
- colearning: 相比于CoJoR只在label上实现agreement,CoLearning在feature上也实现了agreement(通过加入特征维度的cosine_distance(组织正负pair数据,做数据增强)作为loss,还有特征和label之间的KL散度作为loss)。
心得:K折投票过滤法、CurriculumNet和AUM三种方法,在一定程度上是把样本选择和模型正式训练解耦开,而Mentornet、Co-teaching、Co-teaching+、JoCoR和Co-learning却是把样本选择的操作嵌入到模型训练流程当中。前者好处在于样本选择完成后,可以直接接入不同的模型结构做实验,但不是端对端的方案。后者是端对端,但依赖模型和内部参数的调试,因为都在担心训练初期模型过于复杂,或者训练后期,采样过多小损失样本会给后续mini batch流入过多的噪声样本,所以在模型结构上设计的相对不敢那么复杂,后期采样比例也不敢太大。在挑选样本的参照基准上,大多是在标签维度上花功夫,例如logits,预测的label,训练时的loss等。

 

模型融合:
- (b+alpha*x)^beta,其中alpha为线性变化、beta为非线性变化;
- 粒子群算法:鸟第d步的速度=上一步自身的速度惯性+自我认知部分(到第d次迭代为止,第i个粒子经过的最好的位置)+社会认知部分(到第d次迭代为止,所有粒子经过的最好的位置)。鸟第d+步所在的位置=第d步所在的位置+第d步的速度*t*v,t一般为1。

 

时间序列预测:
- ACF是自相关系数:不同lag下子序列之间的协方差/两条子序列的标准差乘积,COV(X,Y)=E(XY)-E(X)E(Y)刻画X与Y之间的相关程度。
- AR: xt=wxt-1,wxt-2等有关(看PACF定p,偏自相关系数剔除了x_t与x_t-k之间变量的干扰)。MA: xt =mu+et - wet-1-wet-2 (看ACF定q,简单看x_t与x_{t-k}之间单纯的相关关系)。截尾阶数做定阶,拖尾用0。
- 平稳性:ACF衰减到0的速度很快,并且十分靠近0,并控制在2倍标准差内。
- ARMA的前提是时序平稳,若不平稳用差分。或者时序分解后对残差用ARMA。有时即使平稳化后预测效果也不好,是因为ARMA模型有方差齐性假定,一般看残差平方图(对1阶差分后的时序求平方)就能看出方差变化,因为按理说残差序列为零均值白噪声序列。若非方差齐性,可以log对数转换。
- DeepAR: LSTM+递归预测目标值概率分布(即mu和std),每时刻的对数似然连乘作为loss。用模型输出的mu和std,求每个点目标真实值y下P(y;mu,std)概率,多个概率连乘作为似然,即可能性,然后作为loss,希望模型输出的mu和std形成的分布和真实目标值y的连乘概率最大化。对数是为了方便求导。
- prophet: y=trend(非线性sigmoid-like模型/分段线性模型)+seasonality(傅里叶级数)+holiday(假期预测时的变化量服从正态分布)+error。
- NBeats有2个stacks, 分为预测趋势项和季节项,原始序列给到趋势stack后,将原序列-预测的趋势序列=残差项即季节项,给到季节项stack拟合,每个stack有3个block,每个block含FC,分别输出针对当前历史窗口和未来窗口的预测开启了2个独立分支,由浅入深,层次分解,下游block拟合残差序列;
- autoformer的两个创新点在设计序列分解模块和将原自注意力机制替换成自相关机制。Encoder-Decoder结构,Encoder经过auto correlation和series decompose后获取季节项编码结果,作为KV给到Decoder的auto correlation部分,Q为decoder上一个输出。Decoder输入是分解后的趋势项和周期项。auto correlation是挑选topk个滞后序列和原序列相关性最大的序列,基于相关性做softmax得到attention score。为了提高计算效率,用快速傅里叶变换(FFT)将信号转为频域 ,再通过频域算出自相关性。
- informer: 先验是:Transformer中attention分数是很稀疏的,呈长尾分布,只有少数是对模型有帮助的。经过推导,若q和k之间分布一致,即KL散度越小,说明attention score为均匀分布,不可能存在关注重点。所以采取随机采样QK点积对减少计算复杂度,另外用max替换点积对求和避免数值溢出。Encoder使用了stride=2的max pooling下采样一半,减少内存占用。
- TFT: 提出了分位数预测(分位数作为权重加权y真实值和y预测值的差值,若预测值小于真实值,分位数加权后,模型预测偏小,loss会增加的更多)和加入了未来已知变量的信息输入(作为decoder的输入)。还有就是配合静态变量喂入门控网络控制变量信息的流入。


项目:
- kdd: 对于前排顶端的团队,基本用了2种度量近邻风机的方法:空间距离和功率相关性。例如求功率相关性/地理位置后Kmeans聚类/人工划分获取风机cluster,把近邻风机的特征们求均值加入特征,或者分cluster建模预测,比如华为诺亚和阿里达摩院使用近邻风机预测值,去做模型预测值的融合。
- M6: rps=rank的mse,information ratio是累计收益/收益日标准差。投资组合权重用了马科维姿组合,找到资产组合收益和风险下有效前沿下的权重(风险最小,收益最大)。差分进化算法:初始化、变异、交叉和选择。种群内每个x就是资产的组合权重。随机选择两个向量做差分,得到差分向量,乘上缩放系数后,加上基向量得到变异向量。随机交叉变异向量和目标向量,得到试验向量,比较试验向量和目标向量,目标函数小的向量可留到下一代。

 

常见问题:

Q:Clickhouse为什么这么快?

A:1. 列式存储. 2. 不是逐行执行,而是按块执行. 3. 采用更好编码方式,压缩数据,读取更快,4. 有多索引,提高查询效率。

 

Q:spark的数据倾斜怎么带来的?

A:spark的数据倾斜主要是shuffle下,不同key对应数据量不同,导致不同task处理时间不同,从而任务要等待要处理最多数据的那个task才能结束。
解决方案:
1. 避免用shuffle算子,如reduceByKey, countByKey, groupByKey, join等。
2. key粒度粗一点,减少数据倾斜可能,增大每个task数据量。
3. 过滤导致倾斜的key。
4. 增加shuffle操作中的reduce并行度,即增加task数量,让每个task分配数量少点。

 

Q:spark支持shuffle的算子?

A:尽量避免使用Shuffle算子 Spark作业最消耗性能的部分就是Shuffle过程,应尽量避免使用Shuffle算子。Shuffle过程就是将分布在集群中多个节点上的同一个 key,拉取到同一个节点上,进行聚合或者join操作,在操作过程中可能会因为一个节点上处理的key过多导致数据溢出到磁盘。由此可见,Shuffle过程可能会发生大量的磁盘文件读写的 IO 操作,以及数据的网络传输操作,Shuffle过程如下图 所示。

Shuffle类算子有:distinct、groupByKey、reduceByKey、aggregateByKey、join、cogroup、repartition等,编写Spark作业程序时,应该尽量使用map类算子替代Shuffle 算子。

 

Q:大小表关联顺序?

A:Hive假定查询中最后一个表是最大的那个表,在对每行记录进行连接操作时,它会尝试将其它表缓存起来,然后扫描最后那个表进行计算。因此,用户需要保证连接操作中的表的大小从左到右时依次增加的。简单来说,join的顺序应该时从小到大的表连接。

 

Q:LSTM的原理?

A:cell state ct-1先point-wise的乘法,忘记一些不再有用的记忆,再经过一个point-wise加法,把xt中有用的信息加到记忆中。

  • 忘记旧记忆 靠的是 遗忘门:ft=sigmoid(ht-1和xt);
  • 新增新记忆 靠的是 输入门(也称记忆门):it=sigmoid(ht-1和xt),Ct=tanh(ht-1和xt)), ct=ft*Ct-1+it*Ct。
  • 输出预测值ot和隐藏ht 靠的是 输出门:ot=sigmoid(ht-1和xt),ht=ot*tanh(Ct)。

 

Q:(顺丰科技第3面)找到旋转数组中特定元素

A:

class Solution:
    def search(self, nums: List[int], target: int) -> int:
        # 在旋转后的序列中,找target的位置。旋转后的序列,肯定左边存在有序数组,而右边不一定有序。
        # 对于有序数组找元素问题,我们可以想到 二分法(两重判断:判断mid左右两边那个有序,判断target是否在有序一侧)

        # 整体思路参考HaominYuan:将数组一分为二,其中一定有一个是有序的,另一个可能是有序,也能是部分有序。
        # 此时有序部分用二分法查找。无序部分再一分为二,其中一个一定有序,另一个可能有序,可能无序。就这样循环. 

        # 判断特例
        if not nums:
            return -1

        # 定义左右指针
        l, r = 0, len(nums) - 1

        while l <= r:
            mid = (l + r) // 2 

            # 若mid上的元素为target,则直接返回mid
            if nums[mid] == target:
                return mid
            
            # 先判断mid左右那边肯定是有序数组
            # 若nums[0] <= nums[mid],左边肯定为有序数组
            if nums[0] <= nums[mid]:
                # target在左边,继续在左边搜索, 否则在右边搜索
                if nums[0] <= target < nums[mid]:
                    r = mid - 1
                else:
                    l = mid + 1

            # 若nums[0] > nums[mid],则右边肯定为有序数组
            else:
                # target在右边,继续在右边搜索, 否则在左边搜索
                if nums[mid] < target <= nums[len(nums) - 1]:
                    l = mid + 1
                else:
                    r = mid - 1
        return -1

 

Q:为什么Dlinear说比Transformer好?

A:自注意力机制本身是无序的(permutation-invariant)。虽然使用各种类型的位置编码技术可以保留一些排序信息,但在time series data上面应用自我注意后,时间信息仍然不可避免地丢失(自注意机制还是很难学习time order信息,因为本质上position embedding也是靠数据学出来的)。对于NLP等语义应用来说,这通常不是一个严重的问题,例如,即使我们对句子中的一些单词重新排序,句子的语义也基本上保持不变。然而,在分析时间序列数据时,数值数据本身通常缺乏语义,我们主要感兴趣的是建模连续点集之间的时间变化。也就是说,order本身起着最关键的作用。然而transformer的attention并没有考虑连续点集的变化,对时间信息的抽取能力弱。而且attention容易过拟合时序中的噪声,影响泛化性,所以想Dlinear通过时序分解,提前显示提取出时序信息来建模,模型表现会更好些。

 
 
Q: 特征数据监控

A: 特征数据监控主要从离线+在线两个角度。

离线天级可以通过抽样全量特征,主要分析来源数据是否异常。

  1. 特征异常值
  2. 特征覆盖率
  3. 特征max min avg 中位数等数据分布

在线监控主要是从 feature server 端,抽样上报,主要分析特征调用方,调用数据源,调用频率等等。除了用来统计流量来源,也可以及时发现一些过期不用的特征,通过下线存储来达到节约成本的目的。

 

Q: (平安产险)Spark的存储数据格式有哪些?

A:ORC>Parquet>RC>SequenceFile>TextFile。

  • TextFile: 默认格式,按行存储,可以压缩但压缩文件不支持分片,反序列化开销是SequenceFile的几十倍(需要判断分隔符和换行符)
  • SequenceFile: hadoop原生支持,将kv以二进制方式按行存储,压缩后的文件支持压缩。默认以record压缩,可以改为block性能更好。压缩率很低,查询速度一般。
  • RCFile: 按行分块、按列存储的存储方式,反序列化速度较慢,但压缩率和查询速度最快
  • ORC file:RC的改良版,每个Task输出单文件、存储索引、支付复杂类型、支持块压缩、可以直接读取,ORC比RC高效很多。
  • Parquet:列式存储,是spark的默认存储格式,压缩和查询性能比ORC稍差,但是支持的编码更多,而且对嵌套式结构支持的更好(json)。

因此对结构化数仓来说ORC file格式更好,对灵活的spark作业来说Parquet格式更好。

 

Q: (平安产险)Adam优化器和SGD的区别?

A:SGD->SGD+Momentum->SGD+Nesterov Momentum-> Adagrad->RMSprop->Adam。

SGD: 没考虑全局,容易受高方差参数影响,呈现z型下降走位,易陷入局部最优;

 SGD+Momentum: 加入动量,即加入历史更新梯度分量,有利于帮助摆脱局部最优。像我们从山上推下一个球,球在滚下来的过程中累积动量,变得越来越快;

 SGD+Nesterov Momentum: 基于历史梯度分量,修正当前梯度后,再加入历史分量,这样,球从山上滚下的时候,盲目地沿着斜率方向,往往并不能令人满意。我们希望有一个智能的球,这个球能够知道它将要去哪,以至于在重新遇到斜率上升时能够知道减速;

 Adagrad: 在原梯度项上,除以所有历史梯度的平方和的开方。若大梯度,除大的大的平方和开方,波动幅度会变小,有效缓解z型走位。但累加平方和开方会持续增长,导致学习率变小,进而出现梯度消失问题;

 RMSprop: 为了Adagrad的极速递减的学习率,将历史square_grad做了衰减平均,避免累加后变得很大导致零梯度;

 Adam: Adam结合了Momentum(避免陷入局部最优)和RMSprop(避免z型走势),既保存一个历史梯度的指数衰减均值m_t(动量), 又存储一个指数衰减的历史平方梯度的平均v_t(square_grad)

 

Q:(平安产险)二阶梯度相较一阶梯度的优势?

A:在处理非凸优化问题时,二阶梯度方法往往比一阶梯度方法更有优势。非凸问题中存在多个局部最小值,而使用二阶梯度可以更好地区分这些局部最小值,并找到全局最优解。

 

Q:(平安产险)GBDT、XGB、LGB、 Catboost的区别?

A:

  • GBDT:是先产生一个弱学习器 (也叫基学习器),训练后得到样本的 "残差" (严格来说是负梯度),然后再产生一个弱学习器并基于上一轮学习器得到的 "残差" 进行训练,不断迭代,最后结合所有弱学习器得到强学习器。用的是CART决策树(gini值,=样本纯度(相当于权重)*特征值出现概率),适用于大样本。是个回归树,最后一层加入sigmoid、softmax就是分类树。

  • XGBoost: 为了避免GBDT过拟合,XGBoost提出加入三种正则化手段:在损失函数内加入正则化(叶子节点数量叶子节点权重 )、缩减树权重和列采样。此外,XGBoost在树分裂点划分上,采用近似算法 (即以特征值分位数作为候选分裂点)。同时,针对近似算法还额外补充三种策略去优化模型训练速度:局部分裂策略 (即在新分裂结点下的样本集中重新提出分位数点)、加权分位数策略 (即以二阶导数加权赋值,使得不同样本不同权重) 和稀疏值策略 (即对缺失值和稀疏值数据自动划分方向)。最后,XGBoost还进行了工程上的优化:列块并行学习(将排序好的特征值和对应样本的位置指针保存至块中 (Block),方便分裂)、缓存访问(为每个线程分配一个内部缓存区,将导数数据放在里面)和块式核外计算。分裂增益 = 分裂前损失 - (左分裂损失 + 右分裂损失)

 

  •  LightGBM: LGBM比XGBoost更快,且很好保持住了准确性。它主要从三方面着手:

(1) 在树方面,提出了直方图算法(对gain分箱)寻找最佳分裂点,而且还采用Leaf-wise树生长策略(从分裂增益大的叶子节点不断向下生长)。

(2) 在样本数上,使用GOSS单边梯度采样保留所有大梯度样本但随机采样小梯度样本,减少训练样本量。

(3) 在特征数上,使用EFB捆绑互斥特征(即它们同时出现非零值,信息丰富),将特征变稠密,加偏移量调整捆绑的特征量纲。

此外,作者还采用GS (梯度统计Gradient Statistics,(一阶偏导数之和/二阶偏导数之和) )编码,在GBDT一类模型中,这是第一次能直接支持类别型特征,不需要提前独热编码后再输入至模型中。

最后,同样地,LightGBM也跟XGBoost一样进行了工程优化,使得训练能高效并行且增加Cache命中率。

Catboost:  CatBoost解决了由训练集和测试集分布不一致带来的预测偏移问题,主要是从两方面入手:(1) 使用Ordered TS对类别型特征编码(GS每轮都要为每个类别值做计算,挺耗时。TS),成功避免目标泄露。

 (2) 使用Ordered Boosting提升方法,使模型获取无偏残差,保证模型不偏于训练集。CatBoost基于排序提升原则去计算残差,例如样本4的残差计算是用前3个样本训练得到的模型预测值与样本真实值做差,这样的话,样本4的残差计算没有让样本4自身参与进去。

此外,模型还对类别型特征进行特征组合,Catboost使用贪婪策略构建特征组合,即,对一个树的每个分裂点,Catboost会组合所有现有树中已被历史分裂使用过的类别特征 (和它们的组合)。

 

Q:(富途)前缀树

A:前缀树又称 Trie 树,是一种树形结构,用于处理字符串匹配问题。它的优点在于能够快速进行字符串的查找、插入和删除操作。

class TrieNode:
    def __init__(self):
        self.children = {}
        self.is_word = False # 判断字符串是否到底

class Trie:
    def __init__(self):
        self.root = TrieNode()

    def insert(self, word):
       # 我们遍历要插入的字符串中的每个字符,判断其是否在当前节点的 children 中。如果不在,
# 则在 children 中新建一个 TrieNode;如果已存在,则继续遍历下一个字符。最后,将最后一位的 is_word 置为 True。 node = self.root for c in word: if c not in node.children: node.children[c] = TrieNode() node = node.children[c] node.is_word = True def search(self, word): # 我们同样遍历要搜索的字符串中的每个字符,判断其是否在节点的 children 中。
# 如果存在,则将节点指向该字符所在的子节点;如果不存在,则说明该字符串不在前缀树中,返回 False。 node = self.root for c in word: if c not in node.children: return False node = node.children[c] return node.is_word t = Trie() t.insert("pidancode") t.insert("皮蛋编程") print(t.search("pidancode")) # True

  

Q:(富途)有向无环图,带权重怎么办?

A:

  • 有向无环:不需要visited辅助
  • 有向有环:如果发现这幅有向图中存在环,类比贪吃蛇游戏,visited 记录蛇经过的格子,而 onPath 仅仅记录蛇身。onPath 用于判断是否成环,类比当贪吃蛇自己咬到自己(成环)的场景。
  • 有向无环+权重:迪杰斯特拉算法(Dijkstra)解决的是有权图中最短路径问题。 可以理解成一个带 dp table(或者说备忘录)的 BFS 算法(树的层次遍历)。主要作用是输入是一幅图graph和一个起点start,返回是一个记录最短路径权重的数组。

 如,最小体力消耗路径:

class Solution:
    # 返回坐标 (x, y) 的上下左右相邻坐标
    def adj(self, matrix, x, y):
        m, n = len(matrix), len(matrix[0])
        dirs = [[0,1],[1,0],[-1,0],[0,-1]]
        neighbors = []
        for row_dir, col_dir in dirs:
            nx = x + row_dir
            ny = y + col_dir
            # 边界溢出,跳过
            if (nx < 0) | (nx >= m) | (ny < 0) | (ny >= n):
                continue
            neighbors.append([nx,ny])

        return neighbors

    def minimumEffortPath(self, heights: List[List[int]]) -> int:
        # 从 (0, 0) 到 (i, j) 的最小体力消耗是 distTo[i][j]
        m, n = len(heights), len(heights[0])
        distTo = []
        for i in range(m):
            distTo.append([float('inf')]*n)

        # base case,起点到起点的最小消耗就是 0
        distTo[0][0] = 0

        # 起点 (0, 0) 开始进行 BFS
        # (体力消耗值,节点行索引, 节点列索引)
        heap = [(0, 0, 0)]

        # BFS
        # 遍历该层每一个节点
        while heap:
            # 获取当前节点
            curState = heapq.heappop(heap)
            # 体力消耗值
            curDistFromStart = curState[0]
            # 图节点x,y
            curNode_x = curState[1]
            curNode_y = curState[2]

            # 到达终点提前结束
            if (curNode_x == m-1) and (curNode_y == n-1):
                return curDistFromStart

            # 若体力消耗值大于该节点历史记录的最小体力消耗值,就跳过
            if curDistFromStart > distTo[curNode_x][curNode_y]:
                continue

            # 将 curNode 的相邻节点装入队列
            for neighbor in self.adj(heights, curNode_x, curNode_y):
                # 获取下一个节点的位置
                nextNode_x = neighbor[0]
                nextNode_y = neighbor[1]
                # 下一个节点的体力消耗值=max(当前节点最大值,该节点与相邻格子之间高度差绝对值)
                distToNextNode = max(
                    distTo[curNode_x][curNode_y],
                    abs(heights[curNode_x][curNode_y]-heights[nextNode_x][nextNode_y])
                    )

                # 更新dp table
                # 若下一个节点的路径比历史记录的还短,替换掉
                if distTo[nextNode_x][nextNode_y] > distToNextNode:
                    distTo[nextNode_x][nextNode_y] = distToNextNode
                    heapq.heappush(heap, (distToNextNode, nextNode_x, nextNode_y))
        # print(distTo)
        # 正常情况不会达到这个 return
        return -1

  

 

posted @ 2023-07-11 22:27  Alvin_Ai  阅读(54)  评论(0编辑  收藏  举报