2024年8月

大家好,我是码农先森。

在 PHP 的编程实践中多进程通常都是在 cli 脚本的模式下使用,我依稀还记得在多年以前为了实现从数据库导出千万级别的数据,第一次在 PHP 脚本中采用了多进程编程。在此之前我从未接触过多进程,只知道 PHP-FPM 进程管理器是多进程模型,但从未在编程中进行实践。多进程虽然能带来效率上的提升,但依然会带来不少的问题,如果初学者使用多进程,那注定会遇到各种奇奇怪怪的 Bug 比如并发操作数据库引起死锁、共用内存变量资源造成串数据、忘记回收进程资源导致产生孤儿进程、僵尸进程等。反正如果我们长期都是 PHP-FPM 模式下编程的话,在使用多进程编程时需要慎之又慎,避免出现意想不到的问题。不过这次我想分享的内容是多进程模式下的孤儿进程和僵尸进程,通过示例代码来看看这两者进程是如何产生的,又应该如何解决,内容不难但是在实际的编程中是可能比较容易忽视的点。

按照惯例我们先看看孤儿进程和僵尸进程的基础概念。

  • 孤儿进程:是指一个进程的父进程已经终止,但该子进程仍然在运行。当父进程结束时,操作系统会将其所有的子进程重新分配给 init 进程。init 进程会负责这些孤儿进程,并确保它们能够正确结束。孤儿进程不会造成资源泄漏,因为最终它们会被 init 进程管理并正确清理。
  • 僵尸进程:是指一个已经完成执行的进程,但仍在进程表中保留了一些信息。这通常发生在父进程未调用 wait() 或相关函数来获取子进程的退出状态时。僵尸进程处于 Z 状态,是一种占用系统资源但不占用 CPU 的进程。僵尸进程会继续占用系统的进程 ID,如果大量产生将导致进程 ID 耗尽,可能会影响系统的正常运行。

这两者进程的基础概念应该还比较好理解,孤儿进程的产生就是缘于父进程的不负责,自己先跑路了,导致自己的子进程变成了孤儿,最后孤儿进程被系统给回收了,可以理解为被政府的福利院收养了。僵尸进程的产生就是儿子进程执行完了没有退出,但是父进程又不知情,无法及时回收儿子进程的资源,导致自己的儿子进程变成了僵尸进程,僵尸进程往往比孤儿进程对系统的危害更大,接下来我们来看看具体的代码示例。

首先看看
孤儿进程
示例,使用 pcntl_fork 函数创建了一个子进程,子进程会每间隔 1 秒钟获取一次自己进程的 ID 和父进程的 ID,而父进程在 2 秒钟之后就退出跑路了,自此子进程就变成了孤儿进程,被系统进程收养了。

<?php

// 孤儿进程示例

$pid = pcntl_fork();
if ($pid < 0) {
   exit('fork error');
} else if($pid > 0) {
   // 父进程执行空间 ...
   // getmypid 函数获取当前父进程ID
   echo "父进程ID: " . getmypid() . PHP_EOL;

   // 2 秒之后退出当前的父进程
   // 父进程先行跑路了
   sleep(2);
   exit();
}

// 子进程执行空间 ...
// getmypid 函数获取当前子进程ID
$cid = getmypid();
echo "当前子进程: {$cid}" . PHP_EOL;

// 每隔 1 秒获取一下进程ID
for($i = 1; $i <= 10; $i++){
    // posix_getppid 函数获取当前子进程的父进程ID
    sleep(1);
    echo "当前子进程ID: " . $cid. ", 父进程ID: " . posix_getppid() . PHP_EOL;
}

// 由于父进程跑路了,子进程变成了孤儿进程 ...

执行
php index.php
观察输出结果,可以看出间隔一段时间之后父进程的 ID 就变成 1 了,即为系统进程。

## 执行程序
[manongsen@root php_test]$ php index.php 
父进程ID: 3484
当前子进程: 3485
当前子进程ID: 3485, 父进程ID: 3484
当前子进程ID: 3485, 父进程ID: 3484
当前子进程ID: 3485, 父进程ID: 1
当前子进程ID: 3485, 父进程ID: 1
当前子进程ID: 3485, 父进程ID: 1
当前子进程ID: 3485, 父进程ID: 1
当前子进程ID: 3485, 父进程ID: 1
当前子进程ID: 3485, 父进程ID: 1
当前子进程ID: 3485, 父进程ID: 1
当前子进程ID: 3485, 父进程ID: 1

然后再看看
僵尸进程
示例,同样也使用 pcntl_fork 创建了一个子进程,然后子进程先行执行完了,父进程还未执行完,这时子进程变成为了僵尸进程。当然僵尸进程也不会一直存在,如果父进程退出了其也会结束自身进程,反之就会一直存在占用着系统资源。

<?php

// 僵尸进程示例

$pid = pcntl_fork();
if ($pid < 0) {
   exit('fork error');
} else if($pid > 0) {
   // 父进程执行空间 ...
   // getmypid 函数获取当前父进程ID
   echo "父进程ID: " . getmypid() . PHP_EOL;

   // 120 秒之后退出当前的父进程
   sleep(120);
   exit();
}

// 子进程执行空间 ...
// getmypid 函数获取当前子进程ID
$cid = getmypid();
echo "当前子进程: {$cid}" . PHP_EOL;

// 10 秒之后退出子进程
sleep(10);

执行
php index.php
观察输出结果,通过查看子进程信息中有一个
Z+
标识,则表示该进程已经成为了僵尸进程。

## 执行程序
[manongsen@root php_test]$ php index.php 
父进程ID: 85804
当前子进程: 85805

## 查看进程信息
[manongsen@root php_test]$ ps aux | grep 85805
root             90776   0.0  0.0 408169072   1408 s060  U+    22:06下午   0:00.00 grep 85805
root             85805   0.0  0.0         0      0 s062  Z+    22:06下午   0:00.00 (php)

最后来看看正常进程的示例,也先使用 pcntl_fork 创建了一个子进程,但与上面两个例子不同的是在其父进程中会调用 pcntl_wait 函数一直等待子进程结束。在子进程 10 秒钟过后,父进程会接受到子进程执行完毕的通知,然后回收子进程的资源。

<?php

// 正常进程示例

$pid = pcntl_fork();
if ($pid < 0) {
   exit('fork error');
} else if($pid > 0) {
    // 父进程执行空间 ...
    // getmypid 函数获取当前父进程ID
    echo "父进程ID: " . getmypid() . PHP_EOL;

    // 一直等待到子进程结束后回收资源
    $cid = pcntl_wait($status);
    echo "父进程ID: " . getmypid() . ", 接收到子进程ID: {$cid} 退出" . PHP_EOL;
    exit();
}

// 子进程执行空间 ...
// getmypid 函数获取当前子进程ID
$cid = getmypid();
echo "当前子进程: {$cid}" . PHP_EOL;

// 睡眠 10 秒
sleep(10);

执行
php index.php
观察输出结果,可以看出子进程执行完毕之后,父进程接收到了子进程的通知。

## 执行程序
[manongsen@root php_test]$ php index.php 
父进程ID: 49954
当前子进程: 49955
父进程ID: 49954, 接收到子进程ID: 49955 退出

## 查看进程 49955
[manongsen@root php_test]$ ps aux | grep 49955
root             19516   0.0  0.0 407972944   1216 s062  R+    22:23下午   0:00.00 grep 49955
root             49955   0.0  0.0 437931336    372 s060  S+    22:23下午   0:00.00 php index.php

## 再次查看进程 49955
[manongsen@root php_test]$ ps aux | grep 49955
root             26599   0.0  0.0 407963440    480 s062  R+    22:24下午   0:00.00 grep 49955

通过这上面的例子可以看出,多进程中正确的使用方式是要在父进程中使用 pcntl_wait 函数等待子进程的结束,而不是只管 pcntl_fork 生产完子进程,然后就对子进程不闻不问了。从生活化的例子来说就是,你不能只管生娃,生完之后就不管养育了,这种操作肯定是不行的,道德和法律层面这一关你都过不去。利用 pcntl_wait 这个函数可以很优雅的解决了孤儿进程和僵尸进程,但在实际的编程中很容易忽视这一点,因此这一点值得注意。本次分享的内容就到这里了,希望对大家能有所帮助。

感谢阅读,个人观点仅供参考,欢迎在评论区发表不同观点。


欢迎关注、分享、点赞、收藏、在看,我是微信公众号「码农先森」作者。

前言

在上一篇文章
《GcExcel 模板系列教程四-分组与扩展》
中,小编为大家分享了如何使用 GcExcel 实现模板的分组与扩展,本文小编将为大家主要介绍如何在模板中进行全局设置。

GcExcel 模板中的全局设置是针对整个模板定义的设置。当需要在多个字段上应用相同属性时,全局设置能够极大地简化工作量。这些设置可以应用于所有模板布局,甚至可以涵盖工作簿中的多个工作表。

GcExcel

GcExcel 模板提供的全局设置说明如下:

  • KeepLineSize(保持行高与列宽)
  • InsertMode(插入整行或整列)
  • DebugMode(调试模式)
  • PaginationMode(分页模式)
  • EmbedFontForFormFields(嵌入字体文件)

本节小编将主要介绍前三个设置,对于其他的模式您可以参考 GcExcel 的官方文档了解更多全局设置的细节。

配置方法

全局设置,是通过 Excel 的公式名称管理器来配置的。需要注意的是,全局设置需要在模板填充之前配置,才可生效,可以使用如下的代码进行全局配置:

Workbook wb = new Workbook();
wb.open("template.xlsx");
//配置全局设置
wb.getNames().add("TemplateOptions.KeepLineSize", "true");
//配置数据源, ds 对象需要额外配置
wb.addDataSource("ds", ds);
//模板填充
wb.processTemplate();
//保存报表
wb.save("report.pdf");

除了使用代码设置之外,也可以通过 Excel 或者
SpreadJS
,在模板中直接配置全局配置,这样就无需再在代码中显示设置了。

Excel:

SpreadJS:

全局设置

1.
保持行高与列宽(KeepLineSize)

通常情况下,GcExcel 在扩展单元格时不会改变单元格的行高和列宽,而是直接沿用已有单元格的行高和列宽,如下图所示:

导出后,可以看到,只有第一行的高度比较大,其余的新行高度并未修改,与模板中原有行高保持一致。

但往往为了布局整齐,美观,我们期望的结果应该是下面的这个样子:

这个时候我们就可以使用 KeepLineSize 属性,修改新增行列时,保持行高列宽一致。

使用如下代码实现:

Workbook workbook = new Workbook();
workbook.open("template.xlsx");
workbook.getNames().add("TemplateOptions.KeepLineSize","True");
workbook.addDataSource("ds",CreateData2());
workbook.processTemplate();

除了使用代码之外,还可以使用名称管理器,在模板中预先配置好:

2.
插入整行或整列(InsertMode)

GcExcel 在扩展单元格时,为了尽量避免对模板布局的影响,默认是以插入单元格的方式进行扩展,如下图所示:

然而,有时在实际情景中,这种操作可能会破坏布局,就像上图中绿色行单元格被切割一样。这种情况下,我们可以通过使用 InsertMode 属性来修改插入行为,让 GcExcel 在填充模板时按照行的方式进行插入:

Workbook workbook = new Workbook();
workbook.open("template.xlsx");
workbook.getNames().add("TemplateOptions.InsertMode","EntireRowColumn");
workbook.addDataSource("ds",CreateData());
workbook.processTemplate();

除了使用代码之外,也可以使用名称管理器,添加设置:

3.
调试模式(DebugMode)

调试模式,是为了方便对比模板与结果而设计的一种模式,当开启 DebugMode 后,模板填充时会保留模板工作表,并将名字改为 {sheetname}_template,例如原本工作表名字为 sales,则模板工作表为 sales_template。

如下图所示:

模板

报表

与前面的设置相同,您可以通过代码或者名称管理器来开启调试模式。

Workbook workbook = new Workbook();
workbook.open("template.xlsx");
workbook.getNames().add("TemplateOptions.DebugMode","True");
workbook.addDataSource("ds",CreateData());
workbook.processTemplate();

总结

GcExcel 的全局设置旨在有效解决不同工作表或模板中共享的配置问题。不同的配置方式各有其优势。通过代码配置,可以避免关注模板本身是否设置了全局属性,而使用名称管理器配置则更好地实现代码与模板的解耦,提高代码的可维护性和灵活性。这种灵活的配置方法为处理全局设置提供了多种选择,使得在不同需求下能够更好地应用和管理全局设置。

扩展链接:

轻松构建低代码工作流程:简化繁琐任务的利器

数据驱动创新:以Java编程方式定制数据透视表

Java批量操作Excel文件实践

前言

本文主要讲神经网络的下半部分。
其实就是结合之前学习的全部内容,进行一次神经网络的训练。

神经网络

下面是使用MNIST数据集进行的手写数字识别的神经网络训练和使用。
MNIST 数据集,是一个常用的手写数字识别数据集。MNIST 数据集包含 60,000 张 28x28 像素的灰度训练图像和 10,000 张测试图像,每张图像都表示一个手写的数字(0-9)。

import torch
import torch.nn as nn
import torchvision
import torchvision.transforms as transforms
import matplotlib.pyplot as plt
# device config
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# hyper parameters
input_size = 784  # 28x28
hidden_size = 100
num_classes = 10 
batch_size = 100
learning_rate = 0.001


# MNIST
# torchvision.datasets.MNIST: 这是一个用于加载MNIST数据集的类。 MNIST 数据集,它包含灰度的手写数字图像。每张图像的尺寸是 28x28 像素,灰度图像只有一个通道(channels=1)
# root='./data': root 参数指定了数据集的存储位置 './data' 表示一个相对路径,表示数据集将存储在当前工作目录下的 data 文件夹中。如果这个文件夹不存在,PyTorch 会自动创建它。
# train=True: 表示加载的是训练集数据。
# transform=transforms.ToTensor(): 将图像转换为PyTorch张量,并归一化为[0, 1]的范围。
# download=True: 如果指定的 root 路径下没有找到数据集,会自动从互联网下载MNIST数据集。
train_dataset = torchvision.datasets.MNIST(
    root='./data', train=True, transform=transforms.ToTensor(), download=True)
test_dataset = torchvision.datasets.MNIST(
    root='./data', train=False, transform=transforms.ToTensor())
# torchvision.datasets.MNIST 是内置的数据集,所以不用去像之前内容中,要搞一个csv文件
# 这里直接把MNIST导入进DataLoader
# batch_size 指定了一次输入模型的数据量。指定batch_size为100,那就是一批次读取100个,利用数据集的索引就可以读取,因为下面还有个参数shuffle=True,所以读取的时候,数据是被打乱的。
train_loader = torch.utils. data.DataLoader(
    dataset=train_dataset, batch_size=batch_size, shuffle=True)
test_loader = torch.utils.data.DataLoader(
    dataset=test_dataset, batch_size=batch_size, shuffle=False)
print('每份100个,被分成多了份',len(train_loader))

examples = iter(train_loader)  # 转换为迭代器,这样可以调用next,一行一行的取数据,只不过他这一行,是一组数据
samples, labels = examples.__next__()  # 这里取出 x和y
print(samples.shape, labels.shape) # samples即x,是一个批次,即100个图像
# 这里输出的是torch.Size([100, 1, 28, 28]) torch.Size([100])
# 其中x是的数据维度是下面这样的。
# 第一个维度 (64): 表示批次中包含的样本数量,即 batch_size。在这个例子中,一次输入模型的有 100 张图像。
# 第二个维度 (1): 表示图像的通道数。对于灰度图像,通道数是 1,彩色图像则通常有 3 个通道(对应 RGB)。
# 第三个维度 (28): 表示图像的高度。MNIST 图像的高度为 28 像素。
# 第四个维度 (28): 表示图像的宽度。MNIST 图像的宽度也是 28 像素。
# y只有一个维度,就是100张图像

# x里都数据都是手写的数字,这里可以用图像把他们展示出来看一看
for i in range(6):
    plt.subplot(2, 3, i+1)  # 在图像窗口中创建一个 2 行 3 列的子图布局,并选择第 i+1 个子图位置。
    plt.imshow(samples[i][0], cmap='gray')
    # plt.show()


class NeuralNet(nn.Module):
    def __init__(self, input_size, hidden_size, num_classes):
        super(NeuralNet, self).__init__()
        self.linear1 = nn.Linear(input_size, hidden_size)
        self.relu = nn.ReLU()
        self.linear2 = nn.Linear(hidden_size, num_classes)

    def forward(self, x):
        out = self.linear1(x)
        out = self.relu(out)
        out = self.linear2(out)
        # no softmax at the end
        return out


model = NeuralNet(input_size=input_size,
                  hidden_size=hidden_size, num_classes=num_classes)
criterion = nn.CrossEntropyLoss()  # (applies Softmax) 这里会调用激活函数,所以上面不调用激活函数了

optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
# training loop
n_total_steps = len(train_loader)
num_epochs = 2 
#下面这个循环就走2次,意思是在训练完集合里的全部数据后,在重新来一遍
for epoch in range(num_epochs):  #for——range模式=其他语言的for
    #下面这个循环是训练集合里的全部数据
    for i, (images, labels) in enumerate(train_loader): #for——enumerate模式=其他语言的foreach
        # 这里的images是100个图像,也就是一个批次
       
        # 将100,1,28,28  这个四维数组 转换成2维数组,转换结果应该是 100,784 
        # to(device) 是指将数据转移到这个设备上计算,如果有GPU,这个计算会被加速
        images = images.reshape(-1, 28*28).to(device) 
        labels = labels.to(device)
        # forward
        outputs = model(images)
        loss = criterion(outputs, labels)
        # backwards
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        if (i+1) % 100 == 0:
            print(
                f'epoch {epoch+1} / {num_epochs}, step {i+1}/{n_total_steps}, loss = {loss.item}')
# test
with torch.no_grad():
    n_correct = 0
    n_samples = 0
    for images, labels in test_loader:
        images = images.reshape(-1, 28*28).to(device) #转二维数组
        labels = labels.to(device)
        outputs = model(images) # 通过我们训练的模型,我们得到了y_predicted

        # value,index
        _, predictions = torch.max(outputs, 1) #torch.max(outputs, 1) 会在 outputs 的每一行(对应每个样本)中找到最大值及其索引。由于模型输出的是每个类别的概率分布,所以最大值的索引代表模型对该图像的预测类别。
        n_samples += labels.shape[0] #labels.shape[0]会返回y的行数,就是100,因为一个批次100个图像
        print("y行数",labels.shape[0])
        #predictions == labels 会生成一个布尔张量(True 表示预测正确,False 表示预测错误)
        #sum() 计算正确预测的数量并加到 n_correct 上
        n_correct += (predictions == labels).sum().item()

acc = 100.0*n_correct/n_samples #计算正确率
print(f'accuracy ={acc}')

图形

现在我们学会了使用神经网络开发,我们在来看一些图形,就能看懂了。
比如这个M-P神经元模型。

在比如这个神经网络结构图。
下面粉色是输入层,绿色是隐藏层,蓝色是输出层。虽然下面画的隐藏层节点比输入层多,但实际情况并不一定,这只是个示意图,比如我们上面,输入的x是784列,隐藏层计算后,就剩100列。
image

结语

本质上我并不是python程序员,其实看我的注释就应该能感觉到吧,比如我对python的for循环都会加注释。
我之所以写这个系列,就是因为我不是python开发,这个系列是为了当我间隔超长时间重新使用python时,唤起死去的记忆用的。
不过,我感觉我写的顺序还不错,如果大家反复的仔细的阅读,应该也能掌握神经网络开发。
而且,正因为我不是python开发,反正更好的证明了,python的学习和人工智能的学习,并没有想象中那么难,相信大家只有认真研究,一定都能学会。


传送门:
零基础学习人工智能—Python—Pytorch学习(一)
零基础学习人工智能—Python—Pytorch学习(二)
零基础学习人工智能—Python—Pytorch学习(三)
零基础学习人工智能—Python—Pytorch学习(四)
零基础学习人工智能—Python—Pytorch学习(五)
零基础学习人工智能—Python—Pytorch学习(六)
零基础学习人工智能—Python—Pytorch学习(七)


注:此文章为原创,任何形式的转载都请联系作者获得授权并注明出处!



若您觉得这篇文章还不错,请点击下方的【推荐】,非常感谢!

https://www.cnblogs.com/kiba/p/18372411

论文提出了包括渐进重参数化批归一化和简化线性注意力在内的新策略,以获取高效的
Transformer
架构。在训练过程中逐步将
LayerNorm
替换为重参数化批归一化,以实现无损准确率,同时在推理阶段利用
BatchNorm
的高效优势。此外,论文设计了一种简化的线性注意力机制,其在计算成本较低的情况下达到了与其他线性注意力方法可比的性能。

来源:晓飞的算法工程笔记 公众号

论文: SLAB: Efficient Transformers with Simplified Linear Attention and Progressive Re-parameterized Batch Normalization

Introduction


transformer
架构最初引入用于自然语言处理任务,迅速成为语言模型领域的杰出模型。随着
Vision Transformer
(
ViT
)的引入,其影响力显著扩展,展示了基于
transformer
的架构的有效性和多样性。这些架构在与卷积神经网络(
CNNs
)相比,在各种视觉任务中表现出了竞争力的性能基准。由于其强大的性能,
transformer
已成为深度学习中的主流架构。然而,
transformer
架构的计算需求构成了一个重大挑战,这主要是由于其注意力机制的二次计算复杂性和
LayerNorm
组件在线统计计算的必要性。

许多工作致力于提升
transformer
架构的效率。有的方法试图通过限制自注意机制中
token
交互的范围来减少计算复杂度,例如降采样键和值矩阵、采用稀疏全局注意模式以及在较小的窗口内计算自注意力。与此同时,线性注意力作为一种替代策略出现,通过将注意力机制分解为线性计算成本来增强计算效率,然而,在效率和准确性之间取得良好平衡仍然是一个具有挑战性的任务。此外,由于
LayerNorm
在推理过程中额外的计算开销,一些探索尝试将
BatchNorm

BN
)替代
transformer
中的
LayerNorm

LN
),比如在前向网络的两个线性层之间添加一个
BatchNorm
层来稳定训练。然而,
LayerNorm

BatchNorm

transformer
之间仍存在性能差距。

论文的重点是通过深入研究计算效率低下的模块,即归一化层和注意力模块,来获取高效的
transformer
架构。首先,论文探索了用
BatchNorm
替换
LayerNorm
以加速
transformer
的推理过程。
BatchNorm
可以降低推理延迟,但可能导致训练崩溃和性能下降,而
LayerNorm
可以稳定训练,但在推理过程中会增加额外的计算成本。因此,论文提出了一种渐进策略,通过使用超参数控制两种归一化层的比例,逐步将
LayerNorm
替换为
BatchNorm
。最初,
transformer
架构由
LayerNorm
主导,随着训练的进行逐渐过渡到纯
BatchNorm
。这种策略有效地减轻了训练崩溃的风险,并且在推理过程中不再需要计算统计信息。除了渐进策略外,论文还提出了一种新的
BatchNorm
重新参数化公式(
RepBN
),以增强训练稳定性和整体性能。

此外,注意力机制的计算成本对于高效的
transformer
架构至关重要,之前的方法在效率和准确性之间难以取得良好的平衡。因此,论文提出了一种简化的线性注意力(
SLA
)模块,该模块利用
ReLU
作为核函数,结合深度可分卷积来进行局部特征增强。这种注意力机制比之前的线性注意力更高效,而且能达到可比较的性能水平。

论文在各种架构和多个基准测试上广泛评估了提出的方法。渐进重新参数化的
BatchNorm
在图像分类和物体检测任务中表现出强大的性能,以更低的推理延迟获得类似的准确性。此外,结合渐进
RepBN
和简化线性注意力模块的
SLAB transformer
在提高计算效率的同时,与
Flatten transformer
相比达到了竞争性的准确性。例如,
SLAB-Swin-S

ImageNet-1K
上达到了
83.6%

Top-1
准确率,推理延迟为
16.2
毫秒,比
Flatten-Swin-S
的准确率高出
0.1%
,延迟则减少了
2.4
毫秒。论文还对提出的方法在语言建模任务上进行了评估,获得了可比较的性能和更低的推理延迟。

Preliminaries


给定输入为
\(N\)
个令牌的特征
\(X \in \mathbb{R}^{N \times C}\)
,其中
\(C\)
是特征维度,
Transformer
块的一般架构可以写成:

\[\begin{equation}
\begin{split}
X = X + \mathrm{Attn}(\mathrm{Norm}(X)), \\
X = X + \mathrm{MLP}(\mathrm{Norm}(X)),
\end{split}
\end{equation}
\]

其中,
\(\mathrm{Attn}(\cdot)\)
计算注意力分数,
\(\mathrm{MLP}(\cdot)\)
表示多层感知机,
\(\mathrm{Norm}(\cdot)\)
是归一化函数。在
Transformer
块的默认配置中,
\(\mathrm{Norm}(\cdot)\)
通常是一个
LayerNorm
操作,
\(\mathrm{Attn}(\cdot)\)
是基于
softmax
的注意力机制

注意力在
Transformer
中扮演着重要角色。将查询、键和值矩阵表示为
\(Q, K, V \in \mathbb{R}^{N \times C}\)

softmax
注意力首先计算查询和键之间的成对相似性。成对相似性计算导致与查询和键的数量
\(N\)
相关的二次计算复杂度
\(O(N^2C)\)
,使得
Transformer
在处理具有长序列输入的任务时计算成本昂贵。线性注意力旨在解耦
softmax
函数,通过适当的近似方法或者用其他核函数先计算
\(K^T V\)
,计算复杂度变为
\(O(NC^2)\)
,与查询和键的数量
\(N\)
线性相关。

然而,
LayerNorm
在推理过程中需要统计计算,因此占据了不可忽视的延迟部分。因此,论文探索利用
BatchNorm
来构建高效的
Transformer
模型,
BatchNorm
仅在训练过程中存在,并且可以与前置或顺序线性层合并。此外,注意力模块对于
Transformer
至关重要,而基于
softmax
的注意力机制由于其二次计算复杂度而在计算效率上存在问题。因此,论文提出了一种简单而高效的注意力形式,极大地减少了延迟,同时在各种视觉任务上保持了良好的性能。

Methods


论文专注于构建高效的
Transformer
模型,并提出了一系列策略,包括逐步替换
LayerNorm

LN
)为重新参数化的
BatchNorm

BN
)以及简化的线性注意力(
SLA
)模块。所提出的
SLAB Transformer
模型在与先前方法相比表现出了强大的性能,同时具备更高的计算效率。

Progressive Re-parameterized BatchNorm

LayerNorm
在训练和推理过程中都需要进行统计量计算,因此显著影响了
Transformer
的运行速度。相比之下,
BatchNorm
在推理过程中可以简单地与线性层合并,更适合于高效的架构设计。然而,直接在
Transformer
中使用
BatchNorm
会导致性能表现不佳。为此,论文提出在训练过程中逐步替换
LayerNorm

BatchNorm
,并且还提出了一种受
Repvgg
启发的新的
BatchNorm
重新参数化公式,以进一步提高性能,如图
2
所示。

  • Re-parameterized BatchNorm

RepBN
公式如下:

\[\begin{equation}
\mathrm{RepBN}(X) = \mathrm{BN}(X) + \eta X,
\end{equation}
\]

其中,
\(\eta\)
是一个可学习的参数,以端到端的方式联合训练。一旦训练完成,
RepBN
可以重新参数化为
BatchNorm
的一种规范形式。

根据引理
4.1

RepBN
输出的分布由
\(\alpha+\eta\sigma\)

\(\beta+\eta\mu\)
控制,分别对应于方差和均值。
RepBN
可以借助
\(\sigma\)

\(\mu\)
来恢复分布。

同时,当
\(\alpha=0, \beta=0\)
时,相当于跳过了
BatchNorm
。当
\(\eta=0\)
时,
RepBN
则退化为纯粹的
BatchNorm

  • Progressive LN
    \(\rightarrow\)
    RepBN

为了促进基于纯粹
BN

Transformer
模型的训练,论文建议在训练过程中逐步过渡从
LN

RepBN
,即

\[\begin{equation}
\mathrm{PRepBN}(X) = \gamma\mathrm{LN}(X) + (1 - \gamma)\mathrm{RepBN}(X),
\end{equation}
\]

其中,
\(\gamma\)
是一个超参数,用于控制不同归一化层的输出。通常,在训练初期
LN
主导架构时,
\(\gamma=1\)
;在训练结束时,为了确保过渡到基于纯粹
BN

Transformer

\(\gamma=0\)
。我们采用了一个简单而有效的衰减策略来调整
\(\gamma\)
的值:

\[\begin{equation}
\gamma = \dfrac{T - T_{cur}}{T}, \gamma \in [0, 1],
\end{equation}
\]

其中,
\(T\)
表示使用
LayerNorm
进行训练的总步数,
\(T_{cur}\)
表示当前的训练步数。这种渐进策略有助于减轻训练纯粹基于
BN

Transformer
的难度,从而在各种任务上实现强大的性能表现。

还有一些其他衰减策略可以逐渐减小
\(\gamma\)
的值,例如余弦衰减和阶梯衰减。从实验来看,线性策略是比较有效且简单的一种方法。

Simplified Linear Attention

注意力模块是
Transformer
网络中最重要的部分,通常表述为:

\[\begin{equation}
\begin{split}
&Q=XW_{Q}, K=XW_{K}, V=XW_{V},\\
&O_{i} = \sum_{j=1}^{N}\dfrac{\mathrm{Sim}(Q_{i}, K_{j})}{\sum_{j}\mathrm{Sim}(Q_{i}, K_{j})}V_{j},
\end{split}
\end{equation}
\]

其中,
\(W_Q, W_K, W_V \in \mathbb{R}^{C \times C}\)
将输入的标记投影到查询(
query
)、键(
key
)和值(
value
)张量。
\(\mathrm{Sim}(\cdot, \cdot)\)
表示相似性函数。对于注意力的原始形式,相似性函数是

\[\begin{equation}
\mathrm{Sim_{softmax}}(Q_i , K_j) = \exp (\frac{Q_iK_j^{T}}{\sqrt{C}}),
\end{equation}
\]

这种基于
softmax
的注意力导致了较高的计算复杂度。近年来,有几种方法研究了使用线性注意力来避免
softmax
计算,从而提高
Transformer
的效率。然而,这些方法仍然存在相当复杂的设计,并且计算效率不够高。因此,论文提出了一种简化的线性注意力(
SLA
):

\[\begin{equation}
\begin{split}
&{\rm Sim}_{SLA}\left(Q_{i},K_{j}\right)=\mathrm{ReLU}\left(Q_{i}\right){\mathrm{ReLU}\left(K_{j}\right)}^T,\\
&\tilde {\rm O}_{i} = \sum_{j=1}^{N}\dfrac{\mathrm{Sim}_{SLA}(Q_{i}, K_{j})}{\sum_{j}\mathrm{Sim}_{SLA}(Q_{i}, K_{j})}V_{j},\\
&\!{\rm O}_{SLA}\!=\tilde {\rm O}+\!{\rm DWC}(V),
\end{split}
\end{equation}
\]

其中,
\(DWC(\cdot)\)
表示深度可分离卷积(
depth-wise convolution
)。这是一种简单而高效的线性注意力方法,因为它通过先计算
\(K^T V\)
,享受了解耦的计算顺序,从而显著减少了复杂度。此外,该方法只使用了
ReLU
函数和深度可分离卷积,这两种操作在大多数硬件上都具有良好的计算效率。

这里的整体逻辑跟
FLatten Transformer
基本一样,只是将其提出聚焦函数替换为
ReLU
函数。这里的效率提升通过摘除
softmax
计算从而达到先计算
\(K^T V\)
实现的(公式7做下乘法结合律),
ReLU
(也有保证内积为正数的作用)和
DWC
是补充计算顺序改变带来的性能损失。

为了展示该方法仍然保持特征多样性,论文通过可视化注意力图表明了应用了渐进重新参数化批归一化和简化线性注意力(
SLAB
)策略的
DeiT-T
的效果,如图
3
所示。可以看出,论文提出的方法仍然保持了较高的排名,表明其在捕捉注意力信息方面具有良好的能力。

Experiments




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

work-life balance.

@

创建和插入代码片段

VS Code扩展提供了数据存储,其中globalState是使用全局存储的Key-Value方式来保存用户状态,支持在不同计算机上保留某些用户状态,详情请参考
官方文档

若在编辑器区域有选中的文本,点击右键菜单中点击创建Snippet,则调用
extension.snippetCraft.createSnipp
命令,执行创建代码片段。

在这里插入图片描述

创建服务类
SnippService.ts
,代码如下

export async function AddSnipp(context: ExtensionContext, state: Partial<ISnipp>) {
  const content = await getSnippText();
  const trimmedName = content?.text?.trim().substring(0, 20) || '';
  await _addOrUpdateSnipp(context, { ...state, name: trimmedName }, content)
}


_addOrUpdateSnipp
方法中对
snipps
进行更新操作

async function _addOrUpdateSnipp(context: ExtensionContext, state: Partial<ISnipp>, content?: {
  text: string | undefined;
  type: string | undefined;
}, snippIndex?: number) {
   
  ...
  context.globalState.update("snipps", updatedSnipps);

若在编辑器区域右键菜单中点击插入Snippet,或在代码片段视图中点击条目,则调用
extension.snippetCraft.insertSnipps
命令,它会调用
InsertSnipp
方法执行插入代码片段操作。

在服务类
SnippService.ts
,插入如下代码

export async function InsertSnipp(context: ExtensionContext, snipp: ISnipp) {
  const editor = window.activeTextEditor;
  if (editor && SnippDataProvider.isSnipp(snipp)) {
    const position = editor?.selection.active;
    editor.edit(async (edit) => {

      edit.insert(position, snipp.content || '');
    });
  } 
}

代码片段列表

代码片段显示为一个树形结构,根据创建时的文件内容类型,分组显示代码片段条目

在这里插入图片描述

创建代码片段和分组条目的接口类型

import * as vscode from "vscode";

export interface ISnipp {
  name: string;
  content: string;
  contentType: string;
  created: Date;
  lastUsed: Date;
}

export interface IGroup {
  name: string;
  contentType: string | undefined;
}

在SnippItem中创建获取所有分组类型的get访问器,和获取分组下的条目getChildren方法


export class SnippItem {
  constructor(
    readonly view: string,
    private context: vscode.ExtensionContext
  ) { }

  public get roots(): Thenable<IGroup[]> {
    const snipps = this.context?.globalState?.get("snipps", []);
    const types = snipps
      .map((snipp: ISnipp) => snipp.contentType)
      .filter((value, index, self) => self.indexOf(value) === index)
      .map((type) => ({ name: type, contentType: undefined }));
    return Promise.resolve(types);
  }

  public getChildren(node: IGroup): Thenable<ISnipp[]> {
    const snipps = this.context?.globalState
      ?.get("snipps", [])
      .filter((snipp: ISnipp) => {
        return snipp.contentType === node.name;
      })
      .sort((a: ISnipp, b: ISnipp) => a.name.localeCompare(b.name));

    return Promise.resolve(snipps);
  }



export class GroupItem { }

VS Code扩展的侧边栏中显示内容需为树形结构,通过实现
TreeDataProvider
为内容提供数据,请参考
官方说明

实现getChildren方法

export class SnippDataProvider
  implements
    vscode.TreeDataProvider<ISnipp | IGroup>
{
  
  public getChildren(
    element?: ISnipp | IGroup
  ): ISnipp[] | Thenable<ISnipp[]> | IGroup[] | Thenable<IGroup[]> {
    return element ? this.model.getChildren(element) : this.model.roots;
  }

}

代码片段预览

实现getTreeItem方法,显示预览

点击时调用
extension.snippetCraft.insertEntry
命令实现插入代码片段,command部分在上一章节有介绍。

鼠标移动到代码片段条目上时,显示tooltip预览

在这里插入图片描述

代码如下:

public getTreeItem(element: ISnipp | IGroup): vscode.TreeItem {
    const t = element.name;
    const isSnip = SnippDataProvider.isSnipp(element);
    const snippcomm = {
      command: "extension.snippetCraft.insertEntry",
      title: '',
      arguments: [element],
    };

    let snippetInfo: string = `[${element.contentType}] ${element.name}`;

    return {
      // @ts-ignore
      label: isSnip ? element.name : element.name,
      command: isSnip ? snippcomm : undefined,
      iconPath:isSnip ? new ThemeIcon("code"):new ThemeIcon("folder"),
      tooltip: isSnip
        ? new vscode.MarkdownString(
            // @ts-ignore
            `**标题:**${snippetInfo}\n\n**修改时间:**${element.created}\n\n**最近使用:**${element.lastUsed}\n\n**预览:**\n\`\`\`${element.contentType}\n${element.content}\n\`\`\``
          )
        : undefined,
      collapsibleState: !isSnip
        ? vscode.TreeItemCollapsibleState.Collapsed
        : undefined,
    };
  }

代码片段编辑

编辑器是一个输入框,由于VS Code的输入框不支持多行输入,所以需要使用webview实现多行输入。同时需要提交按钮与取消按钮

在这里插入图片描述

首先创建一个多行文本框的WebView,
在服务类
SnippService.ts
,创建一个函数
getWebviewContent
,返回一个HTML字符串,用于创建一个多行输入框。

function getWebviewContent(placeholder: string, initialValue: string): string {
  return `
      <!DOCTYPE html>
      <html lang="en">
      <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Multiline Input</title>
      </head>
      <body>
        <textarea id="inputBox" rows="10" cols="50" placeholder="${placeholder}">${initialValue}</textarea>
        <br>
        <button onclick="submitText()">提交</button>
        <button onclick="cancel()">取消</button>
        <script>
          const vscode = acquireVsCodeApi();
          function submitText() {
            const text = document.getElementById('inputBox').value;
            vscode.postMessage({ command: 'submit', text: text });
          }
          function cancel() {
            vscode.postMessage({ command: 'cancel' });
          }
        </script>
      </body>
      </html>
    `;
}

添加处理函数,当用户点击“提交“时,将文本输入框中的内容返回,同时关闭输入框窗口。


async function showInputBoxWithMultiline(context: ExtensionContext, placeholder: string, initialValue: string): Promise<string | undefined> {
  const panel = window.createWebviewPanel(
    'multilineInput',
    'Multiline Input',
    ViewColumn.One,
    {
      enableScripts: true
    }
  );

  panel.webview.html = getWebviewContent(placeholder, initialValue);

  return new Promise<string | undefined>((resolve) => {
    panel.webview.onDidReceiveMessage(
      message => {
        switch (message.command) {
          case 'submit':
            resolve(message.text);
            panel.dispose();
            return;
          case 'cancel':
            resolve(undefined);
            panel.dispose();
            return;
        }
      },
      undefined,
      context.subscriptions
    );
  });
}

在添加代码片段和编辑代码片段时触发函数


export async function AddSnippFromEditor(context: ExtensionContext, state: Partial<ISnipp>) {
  const content = await showInputBoxWithMultiline(context, '请输入Snippet内容', '');
  if (content) {
    _addOrUpdateSnipp(context, state, { text: content, type: "TEXT" })

  }
}

export async function EditSnipp(context: ExtensionContext, state: Partial<ISnipp>, snippIndex: number) {
  const content = await showInputBoxWithMultiline(context, '请输入Snippet内容', state.content ?? '');
  if (content) {
    _addOrUpdateSnipp(context, state, { text: content, type: state.contentType ?? "TEXT" }, snippIndex)

  }
}

自定义映射

映射是插入代码片段时,自动替换的变量,他们通过Key-Value形式存储于globalState中。

代码片段中通过设置占位符(如
${AUTHOR}
),在插入代码片段时,将自动替换为全局变量中的值。

当自定义映射值未设置或者不可用时,将直接显示变量占位符

扩展初始化时,插入了三个常用的自定义映射,你可以自由更改或添加自定义映射。

  • ${AUTHOR}
    : 作者姓名
  • ${COMPANY}
    : 公司名称
  • ${MAIL}
    : 邮箱地址

扩展中所有的自定义映射,呈现于“映射表”树视图中。

在这里插入图片描述

示例:

代码片段内容

value of 'AUTHOR' is: ${AUTHOR}
value of 'COMPANY' is: ${COMPANY}
value of 'MAIL' is: ${MAIL}
value of 'FOOBAR' (non-exist) is: ${FOOBAR}

插入代码片段后,显示如下:

value of 'AUTHOR' is: 林晓lx
value of 'COMPANY' is: my-company
value of 'MAIL' is: jevonsflash@qq.com
value of 'FOOBAR' (non-exist) is: ${FOOBAR}

首先定义KVItem类:

export class KVItem extends vscode.TreeItem {
    constructor(
      public readonly key: string,
      public readonly value: string | undefined
    ) {
      super(key, vscode.TreeItemCollapsibleState.None);
      this.tooltip = `${this.key}: ${this.value}`;
      this.description = this.value;
      this.contextValue = 'kvItem';
    }
  }

“映射表”树视图中显示内容需为树形结构,同样需要定义
KVTreeDataProvider
,在此实现刷新、添加、删除、获取子节点等方法。

export class KVTreeDataProvider implements vscode.TreeDataProvider<KVItem> {
  private _onDidChangeTreeData: vscode.EventEmitter<KVItem | undefined> = new vscode.EventEmitter<KVItem | undefined>();
  readonly onDidChangeTreeData: vscode.Event<KVItem | undefined> = this._onDidChangeTreeData.event;

  constructor(private globalState: vscode.Memento) {}

  getTreeItem(element: KVItem): vscode.TreeItem {
    return element;
  }

  getChildren(element?: KVItem): Thenable<KVItem[]> {
    if (element) {
      return Promise.resolve([]);
    } else {
      const kvObject = this.globalState.get<{ [key: string]: string }>('key-value', {});
      const keys = Object.keys(kvObject);
      return Promise.resolve(keys.map(key => new KVItem(key, kvObject[key])));
    }
  }

  refresh(): void {
    this._onDidChangeTreeData.fire(undefined);
  }

  addOrUpdateKey(key: string, value: string): void {
    const kvObject = this.globalState.get<{ [key: string]: string }>('key-value', {});
    kvObject[key] = value;
    this.globalState.update('key-value', kvObject);
    this.refresh();
  }

  deleteKey(key: string): void {
    const kvObject = this.globalState.get<{ [key: string]: string }>('key-value', {});
    delete kvObject[key];
    this.globalState.update('key-value', kvObject);
    this.refresh();
  }
}

默认映射

默认映射是扩展内置的映射功能,可用的映射如下

文件和编辑器相关:

  • TM_SELECTED_TEXT: 当前选定的文本或空字符串
  • TM_CURRENT_LINE: 当前行的内容
  • TM_CURRENT_WORD: 光标下的单词或空字符串的内容
  • TM_LINE_INDEX: 基于零索引的行号
  • TM_LINE_NUMBER: 基于一个索引的行号
  • TM_FILENAME: 当前文档的文件名
  • TM_FILENAME_BASE: 当前文档的文件名(不含扩展名)
  • TM_DIRECTORY: 当前文档的目录
  • TM_FILEPATH: 当前文档的完整文件路径
  • RELATIVE_FILEPATH: 当前文档的相对文件路径(相对于打开的工作区或文件夹)
  • CLIPBOARD: 剪贴板的内容
  • WORKSPACE_NAME: 打开的工作区或文件夹的名称
  • WORKSPACE_FOLDER: 打开的工作区或文件夹的路径
  • CURSOR_INDEX: 基于零索引的游标编号
  • CURSOR_NUMBER: 基于单索引的游标编号

时间相关:

  • CURRENT_YEAR: 本年度
  • CURRENT_YEAR_SHORT: 当年的最后两位数字
  • CURRENT_MONTH: 两位数字的月份(例如“02”)
  • CURRENT_MONTH_NAME: 月份的全名(例如“July”)
  • CURRENT_MONTH_NAME_SHORT: 月份的简短名称(例如“Jul”)
  • CURRENT_DATE: 以两位数字表示的月份中的某一天(例如“08”)
  • CURRENT_DAY_NAME: 日期的名称(例如“星期一”)
  • CURRENT_DAY_NAME_SHORT: 当天的简短名称(例如“Mon”)
  • CURRENT_HOUR24: 小时制格式的当前小时
  • CURRENT_MINUTE: 两位数的当前分钟数
  • CURRENT_SECOND: 当前秒数为两位数
  • CURRENT_SECONDS_UNIX: 自 Unix 纪元以来的秒数
  • CURRENT_TIMEZONE_OFFSET当前 UTC 时区偏移量为 +HH:MM 或者 -HH:MM (例如“-07:00”)。

其他:

  • RANDOM6: 个随机 Base-10 数字
  • RANDOM_HEX6: 个随机 Base-16 数字
  • UUID: 第四版UUID

这些项目参考至VS Code 代码片段变量,请查看
VSCode官方文档

与自定义映射一样,当默认映射值未设置或者不可用时,将直接显示变量占位符

实现方法如下:


export async function ReplacePlaceholders(text: string, context: ExtensionContext): Promise<string> {
  const editor = window.activeTextEditor;
  const clipboard = await env.clipboard.readText();
  const workspaceFolders = workspace.workspaceFolders;
  const currentDate = new Date();
  const kvObject = context.globalState.get<{ [key: string]: string }>('key-value', {});

  const replacements: { [key: string]: string } = {
    '${TM_SELECTED_TEXT}': editor?.document.getText(editor.selection) || '',
    '${TM_CURRENT_LINE}': editor?.document.lineAt(editor.selection.active.line).text || '',
    '${TM_CURRENT_WORD}': editor?.document.getText(editor.document.getWordRangeAtPosition(editor.selection.active)) || '',
    '${TM_LINE_INDEX}': (editor?.selection.active.line ?? 0).toString(),
    '${TM_LINE_NUMBER}': ((editor?.selection.active.line ?? 0) + 1).toString(),
    '${TM_FILENAME}': editor ? path.basename(editor.document.fileName) : '',
    '${TM_FILENAME_BASE}': editor ? path.basename(editor.document.fileName, path.extname(editor.document.fileName)) : '',
    '${TM_DIRECTORY}': editor ? path.dirname(editor.document.fileName) : '',
    '${TM_FILEPATH}': editor?.document.fileName || '',
    '${RELATIVE_FILEPATH}': editor && workspaceFolders ? path.relative(workspaceFolders[0].uri.fsPath, editor.document.fileName) : '',
    '${CLIPBOARD}': clipboard,
    '${WORKSPACE_NAME}': workspaceFolders ? workspaceFolders[0].name : '',
    '${WORKSPACE_FOLDER}': workspaceFolders ? workspaceFolders[0].uri.fsPath : '',
    '${CURSOR_INDEX}': (editor?.selections.indexOf(editor.selection) ?? 0).toString(),
    '${CURSOR_NUMBER}': ((editor?.selections.indexOf(editor.selection) ?? 0) + 1).toString(),
    '${CURRENT_YEAR}': currentDate.getFullYear().toString(),
    '${CURRENT_YEAR_SHORT}': currentDate.getFullYear().toString().slice(-2),
    '${CURRENT_MONTH}': (currentDate.getMonth() + 1).toString().padStart(2, '0'),
    '${CURRENT_MONTH_NAME}': currentDate.toLocaleString('default', { month: 'long' }),
    '${CURRENT_MONTH_NAME_SHORT}': currentDate.toLocaleString('default', { month: 'short' }),
    '${CURRENT_DATE}': currentDate.getDate().toString().padStart(2, '0'),
    '${CURRENT_DAY_NAME}': currentDate.toLocaleString('default', { weekday: 'long' }),
    '${CURRENT_DAY_NAME_SHORT}': currentDate.toLocaleString('default', { weekday: 'short' }),
    '${CURRENT_HOUR}': currentDate.getHours().toString().padStart(2, '0'),
    '${CURRENT_MINUTE}': currentDate.getMinutes().toString().padStart(2, '0'),
    '${CURRENT_SECOND}': currentDate.getSeconds().toString().padStart(2, '0'),
    '${CURRENT_SECONDS_UNIX}': Math.floor(currentDate.getTime() / 1000).toString(),
    '${CURRENT_TIMEZONE_OFFSET}': formatTimezoneOffset(currentDate.getTimezoneOffset()),
    '${RANDOM}': Math.random().toString().slice(2, 8),
    '${RANDOM_HEX}': Math.floor(Math.random() * 0xffffff).toString(16).padStart(6, '0'),
    '${UUID}': generateUUID()
  };

  Object.keys(kvObject).forEach(key => {
    replacements[`$\{${key}\}`] = kvObject[key];
  });

  return text.replace(/\$\{(\w+)\}/g, (match, key) => {
    return replacements[match] || match;
  });
}

自动完成

自动完成是VS Code编辑器提供的一个功能,用于在编辑器中显示自动提示和补全内容。扩展提供了基于代码片段的自动完成功能。

在这里插入图片描述

CompletionItemProvider
用于注册自动完成的规则,提供者约定了在指定的文档类型下,当输入的字符匹配时,将出现自动完成上下文菜单。

上下文菜单中列出所有可用的自动完成条目,每个条目由
CompletionItem
定义,点击对应条目后,将处理后的字符串返回,填写到编辑器当前光标处。

languages.registerCompletionItemProvider用于注册自动完成的规则提供者。


extension.ts
中注册初始化时,所有的自动完成条目

const providers = contentTypes
  .filter((value, index, self) => self.indexOf(value) === index)
  .map(type =>
    languages.registerCompletionItemProvider(type, {
      provideCompletionItems(
        document: TextDocument,
        position: Position,
        token: CancellationToken,
        context: CompletionContext
      ) {
        return new Promise<CompletionItem[]>((resolve, reject) => {

          var result = snipps
            .filter((snipp: ISnipp) => {
              return snipp.contentType === type;
            })
            .map(async (snipp: ISnipp) => {
              const replacedContentText = await ReplacePlaceholders(snipp.content, extensionContext);

              const commandCompletion = new CompletionItem(snipp.name);
              commandCompletion.insertText = replacedContentText || '';
              return commandCompletion;
            });

          Promise.all(result).then(resolve);
        });
      }
    })
  );

context.subscriptions.push(...providers);

SnippService.ts
_addOrUpdateSnipp方法中配置修改或新增的自动完成条目


  if (content?.type && state.name) {
    languages.registerCompletionItemProvider(content.type, {
      provideCompletionItems(
        document: TextDocument,
        position: Position,
        token: CancellationToken,
        context: CompletionContext
      ) {
        return new Promise<CompletionItem[]>((resolve, reject) => {
          ReplacePlaceholders(state.content || '', extensionContext).then(res => {
            const replacedContentText = res;
            const commandCompletion = new CompletionItem(state.name || '');
            commandCompletion.insertText = replacedContentText || '';
            resolve([commandCompletion]);
          });


        });
      }
    });
  }

项目地址

Github:snippet-craft