2024年10月

您是否曾经希望能够在调试时快速测试集合或数据集上的不同查询?您希望节省时间并避免仅仅为了检查数据而编写代码吗?如果您的回答是肯定的,那么您一定会喜欢 Visual Studio 调试器中新的可编辑表达式特性。

这个特性允许您用您想要的 LINQ 表达式修改 IEnumerable 可视化对话框顶部的表达式文本框。可视化工具实时更新,反映您的查询所导致的数据更改。您可以根据需要轻松地对集合应用不同的筛选器或排序顺序。

在这篇博文中,我们将向您展示如何使用这个强大的特性,以及它如何帮助您更高效地进行调试

开始

在调试会话中,通过将鼠标悬停在调试器中的集合或数据集变量上并单击放大镜图标来启动 IEnumerable 可视化工具。或者,您可以右键单击变量并从上下文菜单中选择 View Visualizer。

这将打开 IEnumerable 可视化对话框,您将在顶部看到表达式文本框。您可以在此文本框中键入任何有效的 LINQ 表达式,然后按<ENTER>将其应用于您的集合。可视化工具将使用您的查询结果更新下面的数据网格。

可编辑表达式的用例

可编辑表达式特性对于调试密集数据集和复杂的集合操作非常有用。您可以直接在 Visual Studio 调试器中试验不同的数据转换和过滤器,而无需编写任何代码或切换到其他工具。

例如,假设您有一个想要检查的产品集合。您可以使用可编辑表达式特性过滤出缺货的产品,按价格对它们进行排序,并仅选择名称和价格属性。您可以这样做:

如您所见,可编辑表达式特性允许您快速、轻松地操作数据并在可视化工具中查看结果。如果要在应用程序逻辑中使用表达式,还可以从文本框中复制表达式并将其粘贴到代码中。

给我们您的反馈

我们希望您喜欢使用 Visual Studio 调试器中的可编辑表达式特性。我们很乐意听到您的反馈和建议,我们可以如何进一步改进它。请在下面留下评论或使用 Visual Studio 中的报告问题工具让我们知道您的想法。

我们还要感谢您的持续反馈和支持,这有助于我们为您更好地开发 Visual Studio。敬请期待更多令人兴奋的功能和更新即将到来!

Happy debugging!

原文链接:https://devblogs.microsoft.com/visualstudio/improve-your-debugger-game-with-editable-expressions/

今天我们将学习新的数据结构-堆。

01
、定义

堆是一种特殊的二叉树,并且满足以下两个特性:

(1)

是一棵
完全二叉树

(2)

中任意一个节点元素值都
小于等于
(或
大于等于
)左右
子树
中所有节点元素值;

小根堆,根节点元素永远是最小值,即堆中每个节点元素值都小于等于左右子树中所有节点元素值;

大根堆,根节点元素永远是最大值,即堆中每个节点元素值都大于等于左右子树中所有节点元素值;

根据堆的定义我们不难发现,堆特别适合用来求集合最值,以及求最值引申的问题比如:排序、优先队列、动态排名等等

02
、结构

我们指定堆是一种特殊完全二叉树,因此堆的逻辑结构是树。

我们指定树的存储结构有两种,顺序存储(数组)和链式存储(链表),那么堆的存储结构是什么呢?

既然堆是完全二叉树,那么树的存储结构当然也适用于堆。但是堆一般都选用顺序存储(数组)实现。其原因有三:

(1)
位置计算简单
:数组实现堆可以使用完全二叉树特性用简单的数学公式即可表示父子节点的索引关系,从而避免了链表实现使用额外的指针,即减少内存开销和实现复杂度;

(2)
性能好
:数组的连续内存特性使得其有高效的访问速度,而链表因为节点不一定连续访问速度相对较差;

(3)
操作简单
:数组实现在逻辑实现上更加简单高效,通过交换数组中的元素即可快速实现堆的性质,链表实现在插入和删除操作中需要遍历链表效率远不如数组实现;

总结一句话数组简单、内存连续、性能更好,所以一般选用数组实现堆,当然不一般的情况也可以使用链表实现。

03
、实现(最小堆)

下面我们就用数组来实现一个最小堆结构,最大堆只是比较大小不同这里就不做过多赘述。

1、初始化 Init

首先我们需要定义两个私有变量,一个变量用来存放堆元素的数组,一个变量用来存放数组尾元素索引,主要用来跟踪当前堆元素个数情况。

而初始化方法就是初始化两个变量,创建指定容量数组,以及标记数组尾元素索引为-1,表示堆中还没有元素。

//存放堆元素
private int[] _array;
//数组尾元素索引
private int _tailIndex;
//初始化堆为指定容量
public MyselfMinHeap Init(int capacity)
{
    //初始化指定长度数组用于存放堆元素
    _array = new int[capacity];
    //初始化数组尾元素索引为-1,表示空数组
    _tailIndex = -1;
    //返回堆
    return this;
}

2、获取堆容量 Capacity

堆容量指的是数组的固定空间,即数组最多能容纳多少个元素,直接返回数组长度即可。

//堆容量
public int Capacity
{
    get
    {
        return _array.Length;
    }
}

3、获取堆元素数量 Count

堆元素数量指当前堆中一共有多少个元素,我们可以通过私有字段数组尾元素索引值加1获得。

//堆元素数量
public int Count
{
    get
    {
        //数组尾元素索引加1即为堆元素数量
        return _tailIndex + 1;
    }
}

4、获取堆顶元素 Top

堆顶元素指树节点中的根节点,也就是数组中的第一个元素。同时要注意判断数组是否为空,为空报异常。

//获取堆顶元素,即最小元素
public int Top
{
    get
    {
        if (IsEmpty())
        {
            //空堆,不可以进行获取最小堆元素操作
            throw new InvalidOperationException("空堆");
        }
        return _array[0];
    }
}

5、是否为空堆 IsEmpty

空堆即堆中还没有任何元素,当数组尾元素索引为-1时表示空堆。

//检查堆是否为空
public bool IsEmpty()
{
    return _tailIndex == -1;
}

6、是否为满堆 IsFull

满堆即堆空间已满不能再添加新元素,当数组尾元素索引等于数组容量减1时表示满堆。

//检查堆是否为满堆
public bool IsFull()
{
    //数组末尾元素索引等于数组容量减1表示满堆
    return _tailIndex == _array.Length - 1;
}

7、入堆 Push

入堆即向堆中新添加一个元素。

而入堆也涉及到一个问题,就是如何保存每次添加完新元素后,还保持着堆的特性。很显然我们也没办法做到直接把新元素直接插入到其正确的位置上,因此我们可以梳理新添加一个元素的流程,可能大致有以下几个步骤:

(1)首先把新元素添加到堆的末尾;

(2)然后调整堆使其满足堆的性质;

(3)更新堆尾元素索引;

而难点显然就在第二步,如何调整数组使其满足堆的性质?

我们先直接模拟把7654按顺序推入堆中,看看如何实现。

(1)首先7入堆,此时只有一个元素,无需做任何操作,而7就作为根节点;

(2)然后6入堆,此时已有两个元素,因此需要保持堆的两个特性:根节点永远是最小元素和堆是完全二叉树。由完全二叉树特性可得,根节点左子节点索引为2
0+1=1,而右子节点索引为2
0+2=2,而此时6的索引为1,所以6为左子节点;又因6比7小,所以根节点变为6, 7变为根节点的左子节点;

(3)然后5入堆,此时已有3个元素,所以5的索引为2,而根节点右子节点索引为2*0+2=2,所以5添加为根节点的右子节点。5比6小,所以5和6交互位置;

(4)然后4入堆,此时已有4个元素,所以4的索引为3,而7节点左子节点索引为2*1+1=3,所以4添加为7节点的左子节点。4比7小,所以4和7交互位置; 再次比较4和5,4比5小,所以4和5交互位置;

相信到这里大家已经看出一点规律了,调整整个数组元素使其满足堆的性质的整个过程大致分为以下几个步骤:

(1)从新元素开始向上比较其与父节点元素的值大小;

(2)如果新元素值小于其父节点元素值,则交互两者位置,否则结束调整;

(3)重复以上步骤直至处理到根节点。

代码实现如下:

//入堆 向堆中添加一个元素
public void Push(int data)
{
    if (IsFull())
    {
        //满堆,不可以进行入添加元素操作
        throw new InvalidOperationException("满堆");
    }
    //新元素索引
    var index = _tailIndex + 1;
    //在数组末尾添加新元素
    _array[index] = data;
    //向上调整堆,以保持堆的性质
    SiftUp(index);
    //尾元素索引向后前进1位
    _tailIndex++;
}
//向上调整堆,以保持堆的性质
private void SiftUp(int index)
{
    //一直调整到堆顶为止
    while (index > 0)
    {
        //计算父节点元素索引
        var parentIndex = (index - 1) / 2;
        //如果当前元素大于等于其父节点元素,则结束调整
        if (_array[index] >= _array[parentIndex])
        {
            break;
        }
        //否则,交换两个元素
        (_array[parentIndex], _array[index]) = (_array[index], _array[parentIndex]);
        //更新当前索引为父节点元素索引继续调整
        index = parentIndex;
    }
}

8、出堆 Pop

出堆即删除并返回堆顶元素(堆中最小元素)。

而出堆同样也涉及到和入堆同样的问题,就是如何保存每次删除元素后,还保持着堆的特性。

显然删除和添加元素情况还不一样。添加新元素后还是一棵完全二叉树,只不过可能没有满足堆的性质,所以需要调整。而删除就不一样了,想象一下当我们把堆顶元素删除后,回怎样?根节点空了,此时还能较完全二叉树吗?显然不能。

如何处理呢?直观的想法就是根节点空了就用其子节点补充上呗,就这样从上到下一直填补,直至最后的空落在了叶子节点那一层,如果这个空位落到了叶子节点左侧,而右侧还有值,此时就表明堆不满足完全二叉树这一特性,因此还需要把这个空位移到堆的末尾,想象都头大。这显然不是一个好办法。

既然我们最后要把根节点删除后的这个空位移到堆的末尾,何不直接把这个空位和堆的尾元素直接调换个位置呢,然后再参考出堆中调整整个数组使其满足堆的性质。

我们梳理整个流程,可能大致有以下几个步骤:

(1)首先获取堆顶元素并暂存;

(2)将堆尾元素赋值给堆顶元素,并将堆尾元素置空;

(3)然后调整堆使其满足堆性质;

(4)更新数组尾元素索引,并返回暂存的堆顶元素;

而难点同样在第二步,如何调整数组使其满足堆的性质?入堆是新元素在堆尾所以是从下往上进行调整,而出堆是堆顶元素需要调整是否可以从上往下进行调整呢?

我们把87654按顺序推入堆中后把4推出堆中,看看如何实现。

首先删除根节点4,并把堆尾元素8放到根节点;

为了保持堆特性,找出根节点及其子节点中最小元素并与当前根节点8交互位置,因为8>6>5,所以5与8交互位置;

然后8节点继续与其子节点进行比较,因为8>7,所以7与8交互位置。

这一出堆过程我们可以总结为以下步骤:

(1)从当前节点开始,找到其子节点元素值;

(2)比较当前节点元素值与其子节点元素值大小,如果当前节点值最小则结束调整,否则取值最小的与其交互;

(3)重复以上步骤直至处理到叶子节点。

代码实现如下:

//出堆 删除并返回堆中最小元素
public int Pop()
{
    if (IsEmpty())
    {
        //空堆,不可以进行删除并返回堆中最小元素操作
        throw new InvalidOperationException("空堆");
    }
    //取出数组第一个元素即最小元素
    var min = _array[0];
    //将数组末尾元素赋值给第一个元素
    _array[0] = _array[_tailIndex];
    //将数组末尾元素设为默认值
    _array[_tailIndex] = 0;
    //将数组末尾元素索引向前移动1位
    _tailIndex--;
    //向下调整堆,以保持堆的性质
    SiftDown(0);
    //返回最小元素
    return min;
}
//向下调整堆,以保持堆的性质
private void SiftDown(int index)
{
    while (index <= _tailIndex)
    {
        //定义较小值索引变量,用于存放比较当前元素及其左右子节点元素中最小元素
        var minIndex = index;
        //计算右子节点索引
        var rightChildIndex = 2 * index + 2;
        //如果存在右子节点,则比较其与当前元素,保留值较小的索引
        if (rightChildIndex <= _tailIndex && _array[rightChildIndex] < _array[minIndex])
        {
            minIndex = rightChildIndex;
        }
        //计算左子节点索引
        var leftChildIndex = 2 * index + 1;
        //如果存在左子节点,则比较其与较小值元素,保留值较小的索引
        if (leftChildIndex <= _tailIndex && _array[leftChildIndex] < _array[minIndex])
        {
            minIndex = leftChildIndex;
        }
        //如果当前元素就是最小的,则停止调整
        if (minIndex == index)
        {
            break;
        }
        //否则,交换当前元素和较小元素
        (_array[minIndex], _array[index]) = (_array[index], _array[minIndex]);
        //更新索引为较小值索引,继续调整
        index = minIndex;
    }
}

9、堆化 Heapify

堆化即把一个无序数组堆化成小根堆。

可以通过调用出堆是用到的调整方法来完成。大家可以思考一下为什么不是堆尾元素开始调整?为什么是从下往上调用向下调整方法调整?

//堆化,即把一个无序数组堆化成小根堆
public void Heapify(int[] array)
{
    if (array == null || _array.Length < array.Length)
    {
        throw new InvalidOperationException("无效数组");
    }
    //将数组复制到堆中
    Array.Copy(array, _array, array.Length);
    //更新尾元素索引
    _tailIndex = array.Length - 1;
    //从最后一个非叶子节点开始向下调整堆
    for (int i = (array.Length / 2) - 1; i >= 0; i--)
    {
        SiftDown(i);
    }
}


:测试方法代码以及示例源码都已经上传至代码库,有兴趣的可以看看。
https://gitee.com/hugogoos/Planner

视觉变换器(
ViT
)已成为众多工业级视觉解决方案的事实标准选择。但由于每一层都计算自注意力,这导致其推理成本对许多场景而言是不可接受的,因为自注意力在标记数量上具有平方的计算复杂度。另一方面,图像中的空间信息和视频中的时空信息通常是稀疏和冗余的。

LookupViT
旨在利用这种信息稀疏性来降低
ViT
的推理成本,提供了一种新颖的通用视觉变换器块,通过将来自高分辨率标记的信息压缩到固定数量的标记来操作。这些压缩的标记进行细致的处理,而高分辨率标记则通过计算成本较低的层。通过双向交叉注意力机制,使得这两个标记集之间的信息共享成为可能。

该方法具有多个优点(a)通过标准高层操作,在标准的机器学习加速器(
GPU
/
TPU
)上易于实现。(b)适用于标准的
ViT
及其变体,因此能够推广到各种任务。(c)可以处理不同的标记化和注意力方法。
LookupViT
还为压缩标记提供了灵活性,使得在单个训练模型中进行性能与计算的权衡成为可能。

论文在多个领域展示了
LookupViT
的有效性。
LookupViT
提供了
2X

FLOPs
减少,同时在这些领域保持或提高了准确性。此外,
LookupViT
在图像分类(
ImageNet-C,R,A,O
)上也表现出了开箱即用的鲁棒性和泛化能力,较
ViT
提高了多达
4%

来源:晓飞的算法工程笔记 公众号,转载请注明出处

论文: LookupViT: Compressing visual information to a limited number of tokens

Introduction


图像和视频作为现代视觉通信的基石,具有一个固有特性:它们的信息内容通常是稀疏的,并且存在显著的冗余。然而,尽管视觉变换器(
ViTs
)在多个视觉任务中占主导地位,但它们并没有利用这种冗余,而是以同质化的方式处理每个标记。这导致了与图像大小相关的平方计算复杂度,妨碍了其在实时场景中的应用。为了弥补这一差距,迫切需要有效地将视觉信息压缩为一个更小、更易于计算处理的标记集。这种表示将解锁
ViTs
在资源受限场景中的潜力,同时保持其灵活性和性能优势,这些优势使其在计算机视觉领域得到了广泛采用。

若干架构旨在通过有针对性地减少标记的数量来解决
ViTs
的计算负担。标记剪枝方法保留一部分标记,而标记池化技术则将相似的标记组合以获得更紧凑的表示。这些机制依赖基于注意力分数或特征相似性的启发式方法,特定任务可能需要额外的调整。尽管这些技术提供了有价值的好处,但它们可能需要根据应用进行进一步的微调。相较之下,论文提出了一种新颖的
LookupViT
模块,以替代传统的
ViT
模块,该模块本质上充当压缩模块。该设计消除了后处理或广泛微调的需要。此外,论文的方法保留了
ViT
架构的整体结构,从而允许使用现有方法(如标记修剪或合并)进行进一步优化和适应。

其它压缩模块,如
TokenLearner

Perceiver
,论文也进行了对比。
TokenLearner
在网络深度的很大一部分中利用传统的
ViT
模块,在后期将大量标记压缩到一个较小的集合(例如,
8
个或
16
个)。这种对
ViT
模块的依赖会产生相当大的计算开销,并严重限制了压缩模块在网络中的完全利用。另一方面,
Perceiver
通过在整个网络中以迭代方式直接从图像像素生成一小组潜在表示,设计了一种不对称的信息流。对于这些网络架构,从相同参数中提取多个模型并非易事,需要在提取的模型之间进行计算性能权衡。而
LookupViT
以提供可扩展的、计算上高效的模块而脱颖而出,这些模块可以像标准
ViT
模块一样无缝重复。其双向交叉注意力机制促进了压缩标记和原始标记之间更丰富的信息交换,增强了表达能力。

对于天生冗余的模态,如视觉,从原始标记中将相关的空间(和时间)信息压缩到一个更小的集合仍然可以维持性能,同时显著降低计算需求,前提是保持两个标记集合之间有效的信息交换。图
1b
表明,与传统的
ViT
模块相比,
LookupViT
能够有效地扩展到较大图像尺寸,因为它仅处理相关信息,而前者在原始图像标记的数量上呈平方级扩展。将较小的压缩标记集合称为压缩标记,这些标记“查看”较大的原始标记集合,将其称之为查找标记。

这些标记之间的信息交换发生在每个
LookupViT
模块中,其过程包括三个关键步骤,如图
2
所示。 (
i
) 交叉注意力将相关信息从查找标记转移到压缩标记(见图
1a
),(
ii
) 压缩标记之间的自注意力,以及(
iii
) 使用第一步中计算的共享注意力权重,将信息从压缩标记转移到查找标记。当压缩标记通过自注意力进行交流时,查找标记仅通过压缩标记进行相互通信。这种技术避免了平方级扩展,同时确保查找潜在表示在各层之间变得更加丰富。

LookupViT
的内部设计自然支持在标记压缩和可变图像或标记大小方面的灵活性。通过调整压缩标记和查找标记之间的下采样比例,成本与性能的权衡可以根据具体应用要求进行定制。这种多分辨率特性允许在推理阶段提取计算高效的高性能模型,同时保持相同的参数空间。为了验证
LookupViT
的有效性,论文在多个基准上展示了结果,如图像和视频分类,以及图像标题生成。值得注意的是,由于信息流的瓶颈设计,
LookupViT
还表现出对图像损坏的开箱即用的鲁棒性。

本工作的主要贡献如下:

  1. 高效的标记压缩:
    LookupViT
    引入了一种新颖的多头双向交叉注意力(
    MHBC
    )模块,能够实现有效的信息流动,同时显著节省计算成本。
  2. 通用框架:
    LookupViT
    提供了一个适用于视觉模态的灵活框架。它还通过压缩标记的多分辨率能力提供了计算与性能之间的权衡,同时保持相同的模型参数。
  3. 增强的性能:
    LookupViT
    在图像/视频模态的应用中表现出良好的泛化能力,并且具有开箱即用的抗损坏鲁棒性。

LookupViT Methodology


Overall Architecture

LookupViT
架构如图
3
所示。与
ViT
架构类似,它由一系列
LookupViT
模块组成。首先,输入的
RGB
图像(或视频)被分割成非重叠的图像块,随后通过卷积层生成特征嵌入,添加位置嵌入以构建输入标记,这一过程与标准
ViT
架构相同。与普通
ViT
不同的是,这里的核心思想是将视觉信息压缩为较少数量的标记,重点将大部分计算专注于这些标记。

固定数量的标记
\(M\)
\((\ll N)\)
,将其称为压缩标记,使用双线性插值从输入标记中抽取出来。对压缩标记进行的计算密集型处理类似于标准
ViT
模块,同时通过异步多头双向交叉注意力(
MHBC
)与原始标记交换信息。该过程分为以下几个步骤(
1
)信息聚集:压缩标记利用交叉注意力“查看”原始标记(称为查找标记)并收集相关信息。(
2
)表示精炼:压缩标记之间相互交换信息,更新其表示。(
3
)全局上下文注入:查找标记利用经过处理的信息丰富的压缩标记,更新自身表示,重用在信息聚集过程中计算的注意力权重以提高效率。

在整个过程中,查找标记只能通过与压缩标记的交互来收集信息,从而减少了计算复杂度。此外,查找标记经过一个具有较小投影维度 (
\(D/q\)
) 的多层感知机(
MLP
)模块,相较于普通模型的投影 (
\(pD\)
),该投影应用于压缩标记,其中
\(D\)
代表变换器的嵌入维度 (
\((p,q) = (4,2)\)
)。这种优化进一步减少了计算量。尽管存在这个显著的
MLP
瓶颈,
LookupViT
模块仍能实现与基线相当的性能,这证明了压缩标记与查找标记之间信息交换的有效性。

Input Tokenization

查找标记嵌入的构建类似于标准
ViT
的标记化策略。给定输入图像
\(\boldsymbol{\mathrm{X}} \in \mathbb{R}^{h \times w \times c}\)
,通过卷积层获得查找特征
\(\boldsymbol{\mathrm{F}}_l \in \mathbb{R}^{h_l \times w_l \times D}\)
。一个可学习的查找位置嵌入
\(\boldsymbol{\mathrm{F}}_{l,pos} \in \mathbb{R}^{h_l \times w_l \times D}\)
被添加到此特征图中。然后,这些标记被显著下采样到固定形状
\((h_p, w_p)\)
,构成压缩标记。

总结如下:

\[\begin{align}
\boldsymbol{\mathrm{F}}_p &\leftarrow \boldsymbol{\mathcal{T}}\big(\boldsymbol{\mathrm{F}}_l, (h_p, w_p)\big) & \boldsymbol{\mathrm{F}}&_{p, pos} \leftarrow \boldsymbol{\mathcal{T}}\big(\boldsymbol{\mathrm{F}}_{l, pos}, (h_p, w_p)\big)\\
\boldsymbol{\mathrm{F}}_{p} &\leftarrow \boldsymbol{\mathrm{F}}_{p} + \boldsymbol{\mathrm{F}}_{p, pos} & \boldsymbol{\mathrm{F}}&_{l} \leftarrow \boldsymbol{\mathrm{F}}_{l} + \boldsymbol{\mathrm{F}}_{l, pos}
\end{align}
\]

算子
\(\boldsymbol{\mathcal{T}}(\mathbf{x},s)\)
以双线性方式将
\(\mathbf{x}\)
调整为形状
\(s\)
。查找标记和压缩标记网格的大小分别为
\((h_l, w_l)\)

\((h_p, w_p)\)

\(D\)
是嵌入维度。这些特征图
\(\boldsymbol{\mathrm{F}}_{p}\)

\(\boldsymbol{\mathrm{F}}_{l}\)
然后被空间展平为
\(\boldsymbol{\mathrm{z}}^0_p\)

\(\boldsymbol{\mathrm{z}}^0_l\)

\[\begin{align}
& \boldsymbol{\mathrm{z}}^0_p = [\boldsymbol{\mathrm{F}}_{p(0,0)}, \dots, \boldsymbol{\mathrm{F}}_{p(h_p-1,w_p-1)}] & \boldsymbol{\mathrm{z}}^0_p &\in \mathbb{R}^{h_p.w_p \times D}\\
& \boldsymbol{\mathrm{z}}^0_l = [\boldsymbol{\mathrm{F}}_{l(0,0)}, \dots, \boldsymbol{\mathrm{F}}_{l(h_l-1,w_l-1)}] & \boldsymbol{\mathrm{z}}^0_l &\in \mathbb{R}^{h_l.w_l \times D}
\end{align}
\]

这些展平的特征图
\(\boldsymbol{\mathrm{z}}^0_p\)

\(\boldsymbol{\mathrm{z}}^0_l\)
(分别是压缩标记和查找标记)被作为输入传递给
LookupViT
块,该块通过信息交换有效地优化这些表示。调整比例
\(C = h_l.w_l/h_p.w_p\)
是一个灵活性参数,表示信息压缩的程度。这能够灵活地使用不同的调整比例训练模型,从而在特定的
\(C\)
下实现计算感知模型提取。更小的
\(C\)
值表示更多的压缩标记,从而具有更好的表示能力。实际上,
\(C=1\)
代表的是普通的
ViT
,但由于跨注意力的原因会有额外的计算。将查找标记和压缩标记的数量分别表示为
\(N = h_l.w_l\)

\(M = h_p.w_p\)
,这种标记化形式可以很容易扩展到视频,其中会引入一个表示时间的第三维度,压缩比例将变为
\(C = h_l.w_l.t_l/h_p.w_p.t_p\)
,其中
\(t_{.}\)
表示时间维度。

LookupViT Block


\(k^{th}\)

LookupViT
块接收来自前一个块的压缩标记
\(\boldsymbol{\mathrm{z}}^{k-1}_p\)
和查找标记
\(\boldsymbol{\mathrm{z}}^{k-1}_l\)
,促进这两个标记集之间的信息交换,并将更新后的表示传递给下一个块,这里的新架构设计是异步多头双向跨注意力(
MHBC
)。直观地说,在第一层中,查找标记比压缩标记保留了更丰富的图像表示。然而,在通过
LookupViT
块多次传递后,压缩标记积累了相关的压缩图像信息,从而使它们适合于下游任务。这是通过在每个
LookupViT
块中查找标记和压缩标记之间的迭代通信实现的(算法
4
)。

这一过程可以总结为三个关键步骤:

  • Information Gathering

在这一步骤中,通过 MHBC
\(_{l\rightarrow p}\)
可以实现从查找到压缩标记的单向信息流。压缩标记用作查询(
\(\boldsymbol{\mathrm{Q}}\)
),标记标记用作键值(
\(\boldsymbol{\mathrm{K}},\boldsymbol{\mathrm{V}}\)
),如算法
1
所示。此外,还存储了在此步骤中计算出的注意力权重
\(\mathcal{A}\)
,以便在反向共享信息时重复使用。

  • Representation Refinement

在信息提取步骤之后,压缩标记经过一个普通的
ViT
块(自注意力后跟
MLP
),如算法
3
所示。
MLP
维度上升因子
\(p\)
保持为
4
,和普通的
ViT
一样。但这个计算是在较小的压缩标记集上进行的。该步骤允许压缩标记之间进行内部信息共享,以更新它们的表示。

  • Global Context Infusion

信息收集与基于
ViT
的处理丰富了压缩标记特征,因为它们包含图像的全局压缩表示。虽然查找标记之间并不直接共享信息,但它们通过反向信息交换(从压缩标记到查找标记)了解全局信息,如算法
2
所示。这里不是重新计算注意力矩阵,而是重用之前在算法
1
中保存的注意力矩阵。这种关系进一步施加了两个特征图之间的隐式相似性约束,并增强了信息交换。最后,为了细化查找特征,应用一个低维的
MLP
块,维度为 (
\(D/q\)
),是普通
ViT MLP
维度的
\(pq\)
倍小(在所有的实验中设置
\((p, q) = (4, 2)\)
)。这为压缩标记在下一个
LookupViT
块中的信息提取丰富了查找标记。

  • Multi-Resolution Tokens

压缩标记通过简单地在非可学习的方式下调整查找标记的大小来构建。因此,可以在具有多种压缩标记分辨率的情况下共享相同的参数空间和查找标记。为此,在训练期间随机选择压缩标记的大小,借鉴了
FlexiViT
的灵感。一旦以这种方式进行训练,就可以从一个单一训练的模型中提取出多个具有不同计算需求的高性能模型。这种灵活性使得该方法可以在各种环境中使用,具体取决于资源的可用性。

Training and Token Utilization for Downstream Applications


LookupViT
中,整个网络中维护两组标记,
\(N\)
个查找标记和
\(M\)
个压缩标记。对于分类,可以将分类器应用于任一或两个标记集。通过实验证明,对两个头施加分类损失可以获得最佳性能。对各自的标记集使用全局平均池化,然后使用两个独立的分类器。接着,联合损失函数以相同的权重进行优化。

尽管训练损失独立应用于两个标记集,但在推理过程中使用压缩标记上的分类器就足够了,添加来自查找标记的分类器输出确实也可以略微提高性能。由于分类没有额外的计算成本,将压缩头和查找头的输出以相同的权重进行平均。对于分类以外的下游应用(例如,图像-语言模型任务,如字幕生成),在
LookupViT
编码器上使用解码器。这种情况下,使用有限的压缩标记集在计算上对交叉注意力块有好处。因此,实验只使用压缩标记。

Computational Complexity


\(\mathcal{C}_x\)
表示过程
\(x\)
的计算。然后,给定特征维度
\(D\)
、查找标记的数量
\(N\)
、压缩标记的数量
\(M (<<N)\)

MLP
升维因子
\(p=4\)
(针对压缩标记)和降维因子
\(q=2\)
(针对查找标记),普通
ViT

LookupViT
块的计算复杂度可以表示如下(忽略较小的项)。

\[\begin{align}
{{\mathcal{C}}}_{\mathrm{ViT}} &= 2N^2D + 12ND^2\\
{{\mathcal{C}}}_{\mathrm{LookupViT}} &= \left(3NM + 2M^2\right)D + \left(4N + 15M\right)D^2
\end{align}
\]

LookupViT
消除了对查找标记数量
\(N\)
的平方依赖,并分别减少了注意力和线性投影的计算。由于压缩标记的数量
\(M (<< N)\)
保持在用户指定的值,因此注意力减少因子迅速增长,能够在更高分辨率下实现可扩展性。通常,对于
384
的图像分辨率,使用
\(N=576\)

\(M=25\)
,这显示出比普通模型更优越的性能,同时将
FLOPs
降低了超过
\(3\)
倍。

Results




如果本文对你有帮助,麻烦点个赞或在看呗~
更多内容请关注 微信公众号【晓飞的算法工程笔记】

work-life balance.

一、环境安装

书接上文,
浅学Redis
之后,服务器已经
安装Redis
了,用 nodejs 链接 redis 之前,先安装 nodejs 环境。

1、安装fnm

(1)压缩包
fnm-linux.zip
搞到服务器上,我放在root里。

(2)解压、设置权限

unzip fnm-linux.zip
chmod 777 fnm

(3)设置环境变量,添加到/etc/profile文件末尾,配置生效

export PATH=$PATH:/root  
source /etc/profile

(4)添加到~/.bashrc文件末尾

eval "$(fnm env --use-on-cd --shell bash)"
source ~/.bashrc

查看fnm版本命令验证是否安装成功

fnm --version
fnm 1.37.2

2、下载并安装 Node.js

fnm use --install-if-missing 20

3、验证 Node.js 版本

node -v

node: /lib64/libm.so.6: version `GLIBC_2.27' not found (required by node)
node: /lib64/libstdc++.so.6: version `GLIBCXX_3.4.20' not found (required by node)
node: /lib64/libstdc++.so.6: version `CXXABI_1.3.9' not found (required by node)
node: /lib64/libstdc++.so.6: version `GLIBCXX_3.4.21' not found (required by node)
node: /lib64/libc.so.6: version `GLIBC_2.28' not found (required by node)
node: /lib64/libc.so.6: version `GLIBC_2.25' not found (required by node)

4、安装依赖前,先换阿里云yum源(换过的请跳过)

(1)更换
阿里云yum源

mv /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.backup
wget -O /etc/yum.repos.d/CentOS-Base.repo https://mirrors.aliyun.com/repo/Centos-7.repo

(2)查看你安装的scl

yum list installed|grep "scl"

(3)删除scl重新安装

yum remove centos-release-scl.noarch
yum remove centos-release-scl-rh.noarch

yum install -y centos-release-scl centos-release-scl-rh

(4)配置scl国内源阿里云
文件 CentOS-SCLo-scl.repo

[centos-sclo-sclo]
name=CentOS-7 - SCLo sclo
baseurl=https://mirrors.aliyun.com/centos/7/sclo/x86_64/sclo/
gpgcheck=0
enabled=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-SIG-SCLo

文件 CentOS-SCLo-scl-rh.repo

[centos-sclo-rh]
name=CentOS-7 - SCLo rh
baseurl=https://mirrors.aliyun.com/centos/7/sclo/x86_64/rh/
gpgcheck=0
enabled=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-SIG-SCLo

(5)清理缓存

yum clean all && yum makecache

坑我踩过了,5、6、7、8 按这个顺序安装

5、升级 gcc(默认为4 升级为8)

yum install -y centos-release-scl
yum install -y devtoolset-8-gcc*
mv /usr/bin/gcc /usr/bin/gcc-4.8.5
ln -s /opt/rh/devtoolset-8/root/bin/gcc /usr/bin/gcc
mv /usr/bin/g++ /usr/bin/g++-4.8.5
ln -s /opt/rh/devtoolset-8/root/bin/g++ /usr/bin/g++

6、升级 make(默认为3 升级为4)

wget http://ftp.gnu.org/gnu/make/make-4.3.tar.gz
tar -xzvf make-4.3.tar.gz && cd make-4.3/
./configure --prefix=/usr/local/make --disable-dependency-tracking
make && make install
cd /usr/bin/ && mv make make.bak
ln -sv /usr/local/make/bin/make /usr/bin/make

7、升级 glibc

wget http://ftp.gnu.org/gnu/glibc/glibc-2.28.tar.gz
tar xf glibc-2.28.tar.gz 
cd glibc-2.28/ && mkdir build  && cd build
../configure --prefix=/usr --disable-profile --enable-add-ons --with-headers=/usr/include --with-binutils=/usr/bin
make && make install

8、升级 libstdc++

wget http://www.vuln.cn/wp-content/uploads/2019/08/libstdc.so_.6.0.26.zip
unzip libstdc.so_.6.0.26.zip
cp libstdc++.so.6.0.26 /lib64/
cd /lib64
cp libstdc++.so.6 libstdc++.so.6.bak
rm -f libstdc++.so.6
ln -s libstdc++.so.6.0.26 libstdc++.so.6

“降龙8掌”打完了,收工

node -v
v20.17.0

二、服务端

1、初始化文件夹,装包:

  • express Express.js简洁而灵活的 Node.js 框架
  • cors 跨域中间件
  • redis 客户端
  • pm2 一个守护进程管理工具

2、修改配置

package.json -> scripts -> 添加:"start": "node ./index.js"
pm2安装后在项目目录下创建启动配置文件 ecosystem.config.js,代码如下:

module.exports = {
  apps: [
    {
      name: 'first-api',
      script: './index.js',
    },
  ],
}

3、新建 index.js

const express = require('express');
const app = express();
const cors = require('cors');
const redis = require("redis");

const redisClient = redis.createClient({
    url: 'redis://default:密码@IP:Port'
});
redisClient.on('error', err => console.log('Redis Client Error', err));
redisClient.connect();

// 使用cors中间件
app.use(cors());

app.get("/", (req, res) => {
    res.send("Hello World");
})

app.get("/redis/keys", async (req, res) => {
    var value = await redisClient.get("test")
    res.send(value);
})

app.listen(3001, () => {
    console.log("Server is running on port 3001");
})

4、pm2 start --watch,水灵灵启动

三、前端

1、一个请求的事

axios.get('http://localhost:3001/redis/keys').then(function (response) {
    alert(response.data);
})

前言

在编程领域,数据结构与算法是构建高效、可靠和可扩展软件系统的基石。它们对于提升程序性能、优化资源利用以及解决复杂问题具有至关重要的作用。今天大姚给大家分享四种C#中常见的经典查找算法。

C#二分查找算法

简介

二分查找算法是一种在有序数组中查找特定元素的搜索算法。

代码实现

public class 二分查找算法
    {
        /// <summary>
        /// 二分查找算法
        /// </summary>
        /// <param name="arr">arr是已排序的数组</param>
        /// <param name="target">target是要查找的目标值</param>
        /// <returns>目标值在数组中的索引,如果未找到则返回-1</returns>
        public static int BinarySearch(int[] arr, int target)
        {
            int left = 0; // 定义左指针
            int right = arr.Length - 1; // 定义右指针

            while (left <= right)
            {
                // 计算中间元素的索引
                int mid = left + (right - left) / 2;

                if (arr[mid] == target)
                {
                    // 如果中间元素等于目标值
                    return mid; // 查找成功,返回索引
                }
                else if (arr[mid] < target)
                {
                    // 如果目标值小于中间元素,则在左半部分查找
                    left = mid + 1;
                }
                else
                {
                    // 如果目标值大于中间元素,则在右半部分查找
                    right = mid - 1;
                }
            }

            // 未找到 target,返回-1
            return -1;
        }

        public static void BinarySearchRun()
        {
            int[] arr = { 1, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59 }; //注意:这里的数组是已排序的数组
            int target = 31; //需要要查找的目标值

            int result = BinarySearch(arr, target); //调用二分查找方法

            if (result == -1)
            {
                Console.WriteLine("元素未找到");
            }
            else
            {
                Console.WriteLine($"元素找到,索引为:{result},值为:{arr[result]}");
            }
        }
    }

C#线性查找算法

简介

线性查找算法是一种简单的查找算法,用于在一个数组或列表中查找一个特定的元素。它从数组的第一个元素开始,逐个检查每个元素,直到找到所需的元素或搜索完整个数组。线性查找的时间复杂度为O(n),其中n是数组中的元素数量。

代码实现

  public static void LinearSearchRun()
        {
            int[] arr = { 2, 3, 4, 10, 40, 50, 100, 77, 88, 99 };
            int target = 100;

            int result = LinearSearch(arr, target);

            // 输出结果
            if (result == -1)
            {
                Console.WriteLine("元素未找到");
            }
            else
            {
                Console.WriteLine($"元素在索引 {result} 处找到,index = {result}");
            }
        }

        /// <summary>
        /// 线性查找函数
        /// </summary>
        /// <param name="arr">arr</param>
        /// <param name="target">target</param>
        /// <returns></returns>
        public static int LinearSearch(int[] arr, int target)
        {
            // 遍历数组
            for (int i = 0; i < arr.Length; i++)
            {
                // 如果找到目标值,返回其索引
                if (arr[i] == target)
                {
                    return i;
                }
            }
            // 如果没有找到,则返回-1
            return -1;
        }

C#二叉搜索树算法

简介

二叉搜索树(Binary Search Tree,简称BST)是一种节点有序排列的二叉树数据结构。

代码实现

namespace HelloDotNetGuide.常见算法
{
    public class 二叉搜索树算法
    {
        public static void BinarySearchTreeRun()
        {
            var bst = new BinarySearchTree();

            // 插入一些值到树中
            bst.Insert(50);
            bst.Insert(30);
            bst.Insert(20);
            bst.Insert(40);
            bst.Insert(70);
            bst.Insert(60);
            bst.Insert(80);
            bst.Insert(750);

            Console.WriteLine("中序遍历(打印有序数组):");
            bst.InorderTraversal();

            Console.WriteLine("\n");

            // 查找某些值
            Console.WriteLine("Search for 40: " + bst.Search(40)); // 输出: True
            Console.WriteLine("Search for 25: " + bst.Search(25)); // 输出: False

            Console.WriteLine("\n");

            // 删除某个值
            bst.Delete(50);
            Console.WriteLine("删除50后:");
            bst.InorderTraversal();
        }
    }

    /// <summary>
    /// 定义二叉搜索树的节点结构
    /// </summary>
    public class TreeNode
    {
        public int Value;
        public TreeNode Left;
        public TreeNode Right;

        public TreeNode(int value)
        {
            Value = value;
            Left = null;
            Right = null;
        }
    }

    /// <summary>
    /// 定义二叉搜索树类
    /// </summary>
    public class BinarySearchTree
    {
        private TreeNode root;

        public BinarySearchTree()
        {
            root = null;
        }

        #region 插入节点

        /// <summary>
        /// 插入新值到二叉搜索树中
        /// </summary>
        /// <param name="value">value</param>
        public void Insert(int value)
        {
            if (root == null)
            {
                root = new TreeNode(value);
            }
            else
            {
                InsertRec(root, value);
            }
        }

        private void InsertRec(TreeNode node, int value)
        {
            if (value < node.Value)
            {
                if (node.Left == null)
                {
                    node.Left = new TreeNode(value);
                }
                else
                {
                    InsertRec(node.Left, value);
                }
            }
            else if (value > node.Value)
            {
                if (node.Right == null)
                {
                    node.Right = new TreeNode(value);
                }
                else
                {
                    InsertRec(node.Right, value);
                }
            }
            else
            {
                //值已经存在于树中,不再插入
                return;
            }
        }

        #endregion

        #region 查找节点

        /// <summary>
        /// 查找某个值是否存在于二叉搜索树中
        /// </summary>
        /// <param name="value">value</param>
        /// <returns></returns>
        public bool Search(int value)
        {
            return SearchRec(root, value);
        }

        private bool SearchRec(TreeNode node, int value)
        {
            // 如果当前节点为空,表示未找到目标值
            if (node == null)
            {
                return false;
            }

            // 如果找到目标值,返回true
            if (node.Value == value)
            {
                return true;
            }

            // 递归查找左子树或右子树
            if (value < node.Value)
            {
                return SearchRec(node.Left, value);
            }
            else
            {
                return SearchRec(node.Right, value);
            }
        }

        #endregion

        #region 中序遍历

        /// <summary>
        /// 中序遍历(打印有序数组)
        /// </summary>
        public void InorderTraversal()
        {
            InorderTraversalRec(root);
        }

        private void InorderTraversalRec(TreeNode root)
        {
            if (root != null)
            {
                InorderTraversalRec(root.Left);
                Console.WriteLine(root.Value);
                InorderTraversalRec(root.Right);
            }
        }

        #endregion

        #region 删除节点

        /// <summary>
        /// 删除某个值
        /// </summary>
        /// <param name="val">val</param>
        public void Delete(int val)
        {
            root = DeleteNode(root, val);
        }

        private TreeNode DeleteNode(TreeNode node, int val)
        {
            if (node == null)
            {
                return null;
            }

            if (val < node.Value)
            {
                node.Left = DeleteNode(node.Left, val);
            }
            else if (val > node.Value)
            {
                node.Right = DeleteNode(node.Right, val);
            }
            else
            {
                // 节点有两个子节点
                if (node.Left != null && node.Right != null)
                {
                    // 使用右子树中的最小节点替换当前节点
                    TreeNode minNode = FindMin(node.Right);
                    node.Value = minNode.Value;
                    node.Right = DeleteNode(node.Right, minNode.Value);
                }
                // 节点有一个子节点或没有子节点
                else
                {
                    TreeNode? temp = node.Left != null ? node.Left : node.Right;
                    node = temp;
                }
            }

            return node;
        }

        /// <summary>
        /// 找到树中的最小节点
        /// </summary>
        /// <param name="node"></param>
        /// <returns></returns>
        private TreeNode FindMin(TreeNode node)
        {
            while (node.Left != null)
            {
                node = node.Left;
            }
            return node;
        }

        #endregion
    }
}

C#哈希查找算法

简介

哈希查找算法是一种高效的查找算法,通过将键值映射到哈希表中的位置来实现快速访问。在C#中,哈希查找通常通过哈希表(Hashtable)或字典(Dictionary)来实现。

代码实现

 public class 哈希查找算法
    {
        /// <summary>
        /// 哈希查找函数
        /// </summary>
        /// <param name="target">target</param>
        public static void HashSearchFunctionRun(int target)
        {
            //创建一个字典来存储键值对
            var dic = new Dictionary<int, string>();
            dic.Add(1, "one");
            dic.Add(2, "two");
            dic.Add(3, "three");

            //查找目标值是否在Dictionary中存在
            //TryGetValue方法可以返回一个bool值和值,如果找到了目标值,则返回true和对应的值,否则返回false和默认值
            string value;
            if (dic.TryGetValue(target, out value))
            {
                Console.WriteLine("Found Data: " + value);
            }
            else
            {
                Console.WriteLine("Not Found Data.");
            }
        }
    }