2024年10月

本研究解决了领域-类别增量学习问题,这是一个现实但富有挑战性的持续学习场景,其中领域分布和目标类别在不同任务中变化。为应对这些多样化的任务,引入了预训练的视觉-语言模型(
VLMs
),因为它们具有很强的泛化能力。然而,这也引发了一个新问题:在适应新任务时,预训练
VLMs
中编码的知识可能会受到干扰,从而损害它们固有的零样本能力。现有方法通过在额外数据集上对
VLMs
进行知识蒸馏来解决此问题,但这需要较大的计算开销。为了高效地解决此问题,论文提出了分布感知无干扰知识集成(
DIKI
)框架,从避免信息干扰的角度保留
VLMs
的预训练知识。具体而言,设计了一个完全残差机制,将新学习的知识注入到一个冻结的主干网络中,同时对预训练知识产生最小的不利影响。此外,这种残差特性使分布感知集成校准方案成为可能,明确控制来自未知分布的测试数据的信息植入过程。实验表明,
DIKI
超过了当前最先进的方法,仅使用
0.86%
的训练参数,并且所需的训练时间大幅减少。

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

论文: Mind the Interference: Retaining Pre-trained Knowledge in Parameter Efficient Continual Learning of Vision-Language Models

Introduction


监督学习技术在对所有数据完全访问的情况下训练网络,这可能导致在扩展网络以获取新任务知识时缺乏灵活性。持续学习(
CL
)作为一种解决方案应运而生,使得模型能够在陆续到达的数据上进行持续训练,同时保留所学的信息。传统的
CL
设置一般考虑的只新引入的类别或领域分布的变化,这称为类别增量学习和领域增量学习。然而,只考虑一种增量的现有工作限制了它们在复杂现实场景中的适用性。

考虑一个更具挑战性的领域-类别增量学习(
DCIL
)设置,在该设置中,领域数据分布和待分类的类别在所有任务中可能不断变化,如图
1(a)
所示。在这种情况下,基于传统图像编码器的技术由于其不可扩展的分类头设计而无法实现。最近,对比训练的视觉-语言模型(
VLMs
)如
CLIP
的出现,使得解决这一要求高但实际的问题成为可能。
VLMs
是在大规模的图像-文本对上训练的,具有强大的零样本泛化能力,可以识别几乎无限的类别,应对这种严重的任务变化场景。

然而,使用视觉-语言模型引入了增量训练的新挑战。传统的持续学习方案旨在防止模型遗忘先前学习的知识,这被称为向后遗忘(忘记微调的知识)。现有的研究探讨了正则化机制、复习缓冲区和架构设计在减轻向后遗忘方面的潜力,并取得了令人鼓舞的成果。然而,当这些方法应用于视觉-语言模型时,出现了一种不同形式的灾难性遗忘:模型往往会遗忘在预训练阶段所学的知识,从而妨碍其强大的零样本泛化能力。这个问题被称为向前遗忘(忘记预训练的知识),因为它发生在
VLMs
对未知分布数据进行“向前”预测时。图
1(a)
展示了这两种遗忘类型。

最近的工作
ZSCL
尝试解决
CLIP
上的向前遗忘问题,引入了一个大规模的参考数据集来进行知识蒸馏,并结合了权重集成方案。然而,这种方法需要大量的计算和外部数据,在实际场景中可能不可行。同时,现有的基于
VLM
的参数高效持续学习方法主要利用提示调整机制,未能保留预训练知识,并导致零样本能力下降,如图
1
(b)所示。论文将这个问题归因于信息干扰:新引入的任务特定参数可能会干扰预训练知识。这些方法的示意图如图
1(c)
所示。

为了以计算和参数高效的方式缓解
VLMs
的向前遗忘问题,论文引入了分布感知无干扰知识融合(
DIKI
)框架。具体而言,将任务特定信息注入到冻结的
VLM
中,以便为每个任务高效地存储已学习的知识。

论文的贡献总结为三点:

  1. 引入了参数高效的
    DIKI
    ,以在
    DCIL
    设置下保留
    VLM
    中的预训练知识。它解决了信息干扰问题,降低了对大量计算和外部数据的需求。
  2. 为了缓解向前遗忘,
    DIKI
    以完全残差的方式植入新知识,保持预训练知识不受干扰。凭借这种残差特性,进一步集成了分布感知融合校准,以提高在未见任务上的性能。
  3. 综合实验表明,与以前的方法相比,
    DIKI
    以仅
    0.86%
    的训练参数和显著更少的训练时间实现了最先进的性能。

Preliminaries


  • Continual learning protocol

持续学习旨在以顺序方式学习不同的任务,同时不忘记之前学到的知识。考虑到
\(N\)
个顺序到达的任务
\(\left[ \mathcal{T}^1, \mathcal{T}^2, \cdots, \mathcal{T}^N \right]\)
,每个任务
\(\mathcal{T}^i\)
包含一个数据集
\(D^i=\{x^i_j, y^i_j\}_{j=1}^{N^i}\)
,其中
\(x^i_j\)
是一幅图像,
\(y^i_j\)
是当前数据集中对应的独热标签,
\(N^i\)
是图像样本的数量。此外,还包括一个类名集合
\(C^i=\{c^i_j\}_{j=1}^{N_{c}^i}\)
,将标签索引连接到
VLMs
使用的类别名称。

与之前的类别和领域增量学习设置不同,本研究强调了一种更实际的持续学习设置:领域-类别增量学习(
DCIL
)。在这个设置中,领域分布和需要识别的类别在不同任务之间不断变化,即
\(C^i \neq C^j\)

\(\mathbb{P}(D^i) \neq \mathbb{P}(D^j)\)
,对于
\(i \neq j\)
,其中
\(\mathbb{P}\)
表示任务数据集的数据分布。

  • Vision-language models

在具有挑战性的领域-类别增量学习(
DCIL
)设置中,训练基于普通图像编码器的模型,如
ResNets

ViTs
,对于增量学习强烈变化的领域和类别并不实用。因此,引入了预训练的视觉-语言模型,因为它们具有强大的零样本迁移能力。
CLIP
包含一个图像编码器
\(f\)
和一个文本编码器
\(g\)
,它们被训练用于生成成对图像-文本样本的紧密对齐特征。在推理时,
\(f\)
首先将输入图像
\(x\)
编码为特征向量
\(f(x)\)
。与此同时,潜在的类名被嵌入到一个模板中,例如“一个{
\(c\)
}的照片”,然后由
\(g\)
编码以形成文本嵌入
\(\{t_j\}_{j=1}^{N_c}\)
。模型的预测通过图像嵌入与所有文本嵌入之间的最大相似性得分来确定
\(s_j = \Braket{f(x), t_j}\)
,其中
\(\Braket{\cdot, \cdot}\)
表示余弦相似度。

  • Task-specific prompt learning

一系列研究开始探索在持续学习中参数高效微调的潜力,常见的做法是为每个任务学习和存储一组轻量级提示,在持续学习阶段形成一个“提示池”,表示为:

\[\begin{equation}
\mathbf{P}=\{P_1, P_2, \cdots, P_N\},\ \ \text{where}\ P_i\in \mathbb{R}^{l\times d},
\end{equation}
\]

其中
\(N\)
是任务编号,
\(l\)

\(d\)
分别是提示的长度和特征嵌入的维度。

在推理时,选择经过良好训练的提示并将附加到预训练的冻结模型上,以恢复学习到的知识。假设
\(\mathbf{x_e}\in \mathbb{R}^{L\times d}\)

Transformer

\(h\)
的特征嵌入,那么可以将提示添加到
\(\mathbf{x_e}\)
前面,以生成提示输入:

\[\begin{equation}
\mathbf{x_p} = \left[P_s^1; P_s^2; \cdots; P_s^l; \mathbf{x_e}\right] \in \mathbb{R}^{(l+L)\times d},
\end{equation}
\]

其中
\(\{P_s^i\in \mathbb{R}^{d}\}_{i=1}^l\)
是选定提示
\(P_s\)
的嵌入向量,
\(;\)
表示沿着
token
长度维度的连接操作。通过这种植入的知识,生成了更好的图像和文本特征嵌入,并且最终的分类准确率得到了提高。

上述提到的提示选择过程是通过查询-键匹配来实现的。在持续训练阶段,通过最大化余弦相似度或应用聚类算法来学习每个任务的平均特征表示
\(\mathbf{I}=\{I^i\}_{i=1}^N\)
。当测试样本
\(\mathbf{x}\)
到来时,进行键查找操作:

\[\begin{equation}
\label{eq_matching}
I_s = {\arg \max}_{I^i\sim \mathbf{I}}\Braket{f(\mathbf{x}), I^i}.
\end{equation}
\]

通过最相关的键
\(I_s\)
,选择相应的提示
\(P_s\)
并将其附加到冻结模型上,执行推理过程。

Methodology


Interference-free Knowledge Integration

  • Is prepending the best choice?

尽管将提示预先添加到输入
tokens
的方法因其实现简单而被广泛使用,但论文发现它们面临两个方面的问题。

  1. 将提示与输入
    tokens
    进行连接会导致它们在注意力过程中相互作用,从而影响预训练知识的提取。当测试样本来自模型学习提示时的分布时,适应后的模型可以保持相对令人满意的结果。然而,一旦遇到分布发生改变的样本,这种干扰可能导致模型性能下降,并损失其重要的零样本泛化能力,造成前向遗忘问题。
  2. 简单地预先添加提示不可避免地增加了所有
    Transformer
    块的
    token
    长度,这在许多有
    token
    长度限制的场景中并不理想。另外,它的可扩展性有限:较长的提示上下文可能会使文本编码器忽视重要的类别名称,从而导致文本嵌入表示不佳。

上述问题的存在表明,基于提示调优的方法并不满足“残差属性”:期望学习到的参数应该是与冻结主干并行的残差路径,补充新的知识而不影响关键的预训练知识。因此,论文提出了一种无干扰知识整合(
Interference-free Knowledge Integration

IKI
)方案,以最小化噪声的方式将新学习的知识注入到预训练的
VLM
中。

  • IKI mechanism

论文不再为每个任务训练一系列预先添加的提示向量,而是关注自注意力机制的修改,这遵循了自然语言处理领域中广泛使用的参数高效微调方法。回想一下,在
Transformer

\(h\)
中,对输入
tokens
\(\mathbf{x_e}\in \mathbb{R}^{L\times d}\)
进行的多头自注意力机制。为了简化,省略了多头设计,仅考虑单头情况,这可以自然扩展到多头场景。输入
tokens
首先通过线性投影转换为查询
\(Q\)
、键
\(K\)
和价值
\(V\)
矩阵:

\[\begin{equation}
Q_e = \mathbf{x_e}W^Q + b^Q; K_e = \mathbf{x_e}W^K + b^K; V_e = \mathbf{x_e}W^V + b^V,
\end{equation}
\]

其中
\(W\in \mathbb{R}^{d\times d}\)

\(b\in \mathbb{R}^{d}\)
是预训练参数。然后,执行自注意力计算,通过以下方式生成输出矩阵:

\[\begin{equation}
O_L = \text{Attn}(Q_e, K_e)V_e = \text{softmax}(\frac{Q_eK_e^T}{\sqrt{d}})V_e\ \ \in \mathbb{R}^{L\times d},
\end{equation}
\]

其中
\(\text{softmax}(\mathbf{z})_i = \frac{\exp{(\mathbf{z_i})}}{\sum_j\exp{(\mathbf{z_j})}}\)
可以约束注意力结果中的元素
\(\text{Attn}(Q_e, K_e)\in \mathbb{R}^{L\times L}\)
的总和为一。

普通的提示调优方法将可训练的提示添加到输入
tokens
中,将
\(\mathbf{x_e}\in \mathbb{R}^{L\times d}\)
扩展为
\(\mathbf{x_p}\in \mathbb{R}^{(l+L)\times d}\)
。然后,将计算
\(Q_{p}K_{p}^T\in \mathbb{R}^{(l+L)\times (l+L)}\)
并传递给
softmax
函数。在
softmax
计算内部,输入
tokens
和提示的注意力分数相互作用并相互影响,导致预训练知识的不可避免损失,如图
2(a)
所示。

为了解决这个问题,论文分别计算输入
tokens
内的自注意力和提示与输入
tokens
之间的交叉注意力,如图
2(b)
所示。换句话说,只训练一个残差注意力分支,保持现有的注意力分数不变。通过新引入的键
\(K_r\)
和值
\(V_r\)
,残差注意力分支的输出可以表示为:

\[\begin{equation}
\label{eq:res_attn}
O_r = \text{softmax}(\frac{Q_eK_r^T}{\sqrt{d}})V_r, \text{where}\ K_r,V_r\in \mathbb{R}^{l\times d}.
\end{equation}
\]

这里,残差输出
\(O_r\in \mathbb{R}^{L\times d}\)
通过与原始输出
\(O_L\)
的正交路径得出,对原始注意力过程没有影响。最后,通过加法将存储在
\(O_r\)
中的学习知识植入输出中。在持续训练阶段,更新可学习的键
\(K_r\)
和值
\(V_r\)
,而不是常用的提示
\(P\)
。请注意,为了保持序列长度不变,没有引入任何查询参数。

理想情况下,一个理想的残差块在未在下游数据集上进行训练之前,应该不会影响原始分支,比如在初始化时。广泛使用的方式用均匀或正态分布初始化提示,这会在没有学习到任何知识的情况下向预训练的
VLMs
中注入随机噪声。具体而言,通过将参数
\(V_r\)
初始化为零,强制残差注意力加法成为一个恒等函数:

\[\begin{equation}
O = O_L+O_r^{\text{init}} = O_L+\text{softmax}(\frac{Q_eK_r^T}{\sqrt{d}})\mathbf{[0]}^{l\times d} = O_L.
\end{equation}
\]

注意,论文仅在开始时将值
\(V_r^{\text{init}}\)
限制为零,同时保持
\(K_r\)
随机初始化。这是因为将
\(K_r\)

\(V_r\)
都初始化为零矩阵会阻止
\(K_r\)
通过梯度更新,从而使
\(V_r\)
陷入到具有相同值的向量中。

由于零初始化更像是一种选择而非技术,一些研究在各种任务中采用了它。然而,这些工作利用零初始化来确保稳定和渐进的训练机制,而在
DCIL
场景中并不存在这一顾虑。论文认为,零初始化对于残差注意力设计是至关重要的,它可以以最小的噪声将新知识注入到预训练的
VLMs
中。

Distribution-aware Integration Calibration

  • Observations

在推理时,会执行公式
3
中描述的查询-键匹配机制,以检索适合当前测试样本的学习提示。这种方法是针对传统的持续学习设置而设计的,仅考虑了向后遗忘。然而,当面对来自未见领域的数据时,这种简单的匹配设计被强制执行,从而为测试样本分配一个相对相似的任务,尽管它们之间存在显著的分布差距。

得益于
IKI
的残差设计,与之前的方法相比,现在可以在这种不匹配的场景中引入更少的噪声。然而,当训练和测试分布之间的差异增加时,模型在某种程度上的性能下降是不可避免的,这会损害
VLMs
在预训练阶段所学到的零样本能力。

ZSCL
通过蒸馏来解决这个问题。他们构建了一个包含来自
ImageNet

100,000
张图像的参考数据集,以在每个训练步骤中将原始
CLIP
的预训练知识蒸馏到当前模型中,明确进行复习以避免遗忘。这种方法可能有效,但它依赖于大规模存储和高计算资源,从而在实际环境中显得不切实际。

一个直观的解决方案是控制知识植入模型的程度。然而,之前基于前置的提示调整技术只有两个选择:要么追加学习到的提示,要么不对原始
CLIP
模型进行任何修改。得益于
IKI
的优雅残差特性,现在可以控制这一并行分支的能力。

  • DIKI: calibrate the integration with distribution

为了确定测试样本属于已学习任务的可能性,为每个任务维护一个特征分布,而不是一个单一的关键向量。在这里,论文简单地应用多元高斯分布,并发现效果良好。形式上,在训练阶段为任务
\(i\)
构建一个
\(\mathcal{N}^i(\mathbf{\mu}^i, \mathbf{\Sigma}^i)\)

\[\begin{equation}
\begin{gathered}
\mathbf{\mu}^i = \mathbb{E}_{\mathbf{x}^i_j \sim D^i}[f(\mathbf{x}^i_j)], \ \ \ \mathbf{\Sigma}^i = \mathbb{E}_{\mathbf{x}^i_j \sim D^i}[(f(\mathbf{x}^i_j)-\mathbf{\mu}^i)^T(f(\mathbf{x}^i_j)-\mathbf{\mu}^i)],
\end{gathered}
\end{equation}
\]

其中
\(f(\mathbf{x}^i_j)\)
是由冻结编码器提取的图像特征。通过这些估计的分布,可以计算每个
\(\mathcal{N}^i\)
中测试样本被抽取的可能性。在这里,计算概率密度的对数作为输入
\(\mathbf{x}\)
在每个学习任务上的评分函数:

\[\begin{equation}
\begin{split}
S^i &= \log \varphi(f(\mathbf{x}); \mathbf{\mu}^i, \mathbf{\Sigma}^i) \\
&= - \frac{1}{2}[ (f(\mathbf{x})-\mathbf{\mu}^i)^T(\mathbf{\Sigma}^i)^{-1}(f(\mathbf{x})-\mathbf{\mu}^i) + d\log 2\pi + \log |\mathbf{\Sigma}^i|) ],
\end{split}
\end{equation}
\]

其中
\(\varphi\)
是概率密度函数。

直观上,得分较高的样本
\(S^i\)
更可能是从任务
\(i\)
中抽取的,并且应该引入参数
\(K_r^i, V_r^i\)
以进行模型预测。此外,还应该考虑到输入样本
\(\mathbf{x}\)
可能来自某些新的分布,如果所有
\(S^i\)
都很低,这一点就得到了暗示。因此,利用最大得分
\(\hat{S}=\max_{i\in [1,N]}S^{i}\)
来加权残余注意力输出:

\[\begin{equation}
\label{eq:final_output}
O = O_L+\mathcal{M}(\hat{S})O_r,
\end{equation}
\]

其中
\(\mathcal{M}\)
是一个映射函数,将得分
\(\hat{S}\)
缩放到范围
\([0,1]\)
。在这里,论文发现简单的
Sigmoid
函数
\(\sigma(x)=\frac{1}{1+e^{-x}}\)
在此效果很好。得益于这种基于分布感知的集成校准机制,
VLMs
的预训练零样本能力可以更好地保留,通过对不熟悉的图像分配较低的权重,进一步解决了前向遗忘的问题。

Experiments




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

work-life balance.

前言

快速将创意变为现实!无需实体建库建表即可完成增删改查操作,支持15种条件查询、分页、列表及无限级树形列表等多种功能的API部署。

提供完善的接口文档、Auth授权、接口限流、客户端真实IP获取、先进服务器缓存组件及动态API等特性。让大家的工作效率倍增,远离加班和额外的知识付费。

项目介绍

  • 无需实体数据库,即可进行数据的增删改查

  • 支持15种条件查询

  • 提供分页、列表、无限级树形列表等功能

  • 提供详细的接口文档

  • 包含Auth授权机制

  • 支持接口限流和获取客户端真实IP

  • 拥有先进的服务器缓存组件

  • 支持动态API

  • 快速部署API

项目特点

为了让非技术人员也能轻松使用,我们特别发布了适用于 Linux、Mac 和 Windows 平台的 x64 和 x32 版本的应用程序,以及各平台的二进制文件。只需下载并直接启动即可运行。

启动项目后,在浏览器中输入
http://你的IP:3000/index.html
即可访问管理系统。

本系统无需安装任何额外环境即可启动运行,但数据库等外部软件需自行安装。

可以通过修改软件配置文件夹
Configuration
中的设置来调整系统行为:

  • Database.config
    文件用于配置数据库,默认使用 SQLite;

  • App.json
    文件包含软件的相关配置,其中
    urls
    字段允许您自定义软件的启动端口。”

项目依赖

  • 动态 API 解决方案:Panda.DynamicWebApi
  • 高性能 ORM 框架:SqlSugar
  • 自动生成 Swagger 接口文档:Swashbuckle.AspNetCore
  • 支持跨平台(Linux、macOS、Windows),无需安装额外环境,直接运行
  • SoybeanAdmin:基于最新前端技术栈(Vue3、Vite5、TypeScript、Pinia 和 UnoCSS)
  • FastCrud(简称 fs):面向配置的 CRUD 开发框架,基于 Vue3,助力快速开发 CRUD 功能,适合作为低代码平台的基础框架

项目环境

1、服务端启动

  • 使用 Visual Studio 2022 或 JetBrains Rider 打开
    SuperApi.sln
  • 确保已安装 .NET 8 SDK。

  • SuperApi
    设置为启动项目并运行,即可启动服务端。

2、前端项目启动

  • 使用 VSCode 打开
    admin-ui
    目录。
  • 在命令行中执行
    pnpm install
    以安装依赖。
  • 运行
    pnpm run dev
    启动前端项目。
  • 这样组织后,每个步骤的重点更加突出,用户可以更容易地跟随指导进行操作。

项目使用

后台配置

1、打开
SuperApi/SuperApi.sln
解决方案,进入
Configuration
目录,配置数据库及其他设置。

2、将
SuperApi
设为启动项目后直接运行。

前端页面

1、打开
admin-ui
文件夹,在命令行中执行
pnpm install
来安装依赖(如未安装 pnpm,请先执行
npm install -g pnpm
)。

2、安装完成后,执行
pnpm run dev
启动开发服务器。

3、启动后,浏览器将自动打开接口文档页面,您可以开始使用了。

登录信息

账号:admin/sp123456

项目效果

1、登录页

2、系统首页

3、订单管理

4、接口文档

项目地址

Gitee:
https://gitee.com/tmm-top/SuperApi

总结

本文只展示了部分功能和内容,如有需求访问项目地址获取详细信息。希望本文能在.NET开发方面为各位提供有益的帮助。期待大家在评论区留言交流,分享您的宝贵经验和建议。

最后

如果你觉得这篇文章对你有帮助,不妨点个赞支持一下!你的支持是我继续分享知识的动力。如果有任何疑问或需要进一步的帮助,欢迎随时留言。

也可以加入微信公众号
[DotNet技术匠]
社区,与其他热爱技术的同行一起交流心得,共同成长!
优秀是一种习惯,欢迎大家留言学习!

使用doccano标注NER数据详细教程

说明:

部署doccano

https://github.com/doccano/doccano
有说明如何部署。比如使用Docker部署:

docker run --name doccano \
  -d --restart always \
  -e "ADMIN_USERNAME=admin" \
  -e "ADMIN_EMAIL=admin@example.com" \
  -e "ADMIN_PASSWORD=password" \
  -v doccano-db:/data \
  -p 8001:8000 doccano/doccano

创建用户

默认只有一个用户,我们需要打开ADMIN管理页面添加新的用户。

在主网址后加
/admin/
然后打开ADMIN管理页面(注意后边的斜杠是必须的),点击
Add

添加用户名和密码信息后,点击
SAVE
以保存:

如何进行NER标注

创建项目

默认的界面是英语的,不习惯英语的话,可以切换为中文:

然后点击登录,输入用户名和密码登录,登录之后:

点击
创建
,会跳转到以下页面:

点击以选择
序列标注
(Sequence Labeling),然后输入名称等必要信息,根据需要配置其他属性:

点击
创建
,跳转到以下页面:

导入数据集

单击左侧的
数据集
按钮:

移动鼠标到
操作
按钮:

点击导入数据集:

doccano
支持多种格式的文本,它们的区别如下:

  • Textfile
    :上传的文件为
    txt
    格式,打标时一整个
    txt
    文件显示为一页内容;
  • Textline
    :上传的文件为
    txt
    格式,打标时
    txt
    文件的一行文字显示为一页内容;
  • JSONL

    JSON Lines
    的简写,每行是一个有效的
    JSON
    值;
  • CoNLL

    CoNLL
    格式的文件,每行均带有一系列制表符分隔的单词;

上传一个TXT文件:

点击导入后:

定义标签

点击左侧的
标签
,然后移动鼠标到
操作
菜单后点击
创建标签

创建3个常见的标签,
PER
,
LOC
,
ORG
,实际应用中需要根据需求确定有哪些标签。以下以创建
PER
标签为例:

创建完后:

添加成员

点击左侧的
成员
按钮,然后点击
增加

选择需要添加到项目的用户和角色,其中有3种角色(项目管理员 ,标注员,审查员)。选择好后保存:

保存后可以看到:

分配标注任务

首先,选中需要分配的数据:

然后,点击操作菜单下的
Assign to member

选择分配方案,然后点击右侧的
Assign
按钮

以上分配方案将15%的任务分配给
admin
用户,85%的任务分配给
user1
用户。

查看分配结果:

标注

点击左侧
数据集
,然后选择一条数据,点击最右边的
标注
按钮开始标注。

举例来说,点击右侧的
PER
标签,然后鼠标分别选择文本中的对应文字:

标注完成后,点击文本左上角的X按钮表示已标注完成:

导出数据

点击左侧
数据集
按钮,移动鼠标到
操作
菜单,点击
导出数据集

选择
JSONL
格式,勾选
Export only approved documents
(仅导出已审核过的数据),然后点击导出:

前言

在mongo中数据类型有很多种,常见的包括:

数据类型 例子 描述
String { "x" : "foot" } 字符串。存储数据常用的数据类型。在 MongoDB 中,UTF-8 编码的字符串才是合法的。
Integer { "x" : 1 } 整型数值。用于存储数值。根据你所采用的服务器,可分为 32 位或 64 位。
Object { "x" : { "y" : "foot" } } 用于内嵌文档
Array { "x" : [ "a" , "b" ] } 用于将数组或列表或多个值存储为一个键。

有一种很常见的查询,就是过滤数组中的一些数据,只返回符合要求的数据。数据如下,将下面travel中的vehicle=train的记录保留,过滤掉其他的元素,并返回整个文档。

{
    "name": "tom", 
    "travel": [
        {
            "vehicle" : "train",
            "city" : "北京"
        },
        {
            "vehicle" : "plane",
            "city" : "上海"
        }, 
        {
            "vehicle" : "train",
            "city" : "深圳"
        }
    ]
}

想要实现数组的过滤有三种方法,包括:

  1. 聚合查询 使用
    $unwind

    travel
    数组打散,获取结果集后用
    $match
    筛选符合条件的数据,最后使用
    $group
    进行聚合获取最终结果集
  2. 聚合查询 使用
    $match
    过滤符合条件的根文档结果集,然后使用
    $projec
    t返回对应字段的同时,在
    travel
    数组中使用
    $filter
    进行内部过滤,返回最终结果集
  3. 普通查询 先筛选记录,然后通过投影查询过滤数组

下面来分析这三种方法能否实现需求。

添加数据

假设有两条记录,每条记录是一个人的信息,包括姓名、职业、旅游过的城市。旅游过的城市是一个数组,包含城市的名字以及交通工具。

db.test.insertOne({
    "uid" : "1000001",
    "name" : "zhangsan",
    "job": "coder",
    "travel" : [ 
        {
            "vehicle" : "train",
            "city" : "北京"
        }, 
        {
            "vehicle" : "plane",
            "city" : "上海"
        }, 
        {
            "vehicle" : "train",
            "city" : "深圳"
        }
    ]
})
db.test.insertOne({

    "uid" : "1000002",
    "name" : "lisi",
    "job": "coder",
    "travel" : [ 
        {
            "vehicle" : "plane",
            "city" : "北京"
        }, 
        {
            "vehicle" : "car",
            "city" : "上海"
        }, 
        {
            "vehicle" : "train",
            "city" : "深圳"
        }
    ]
})
db.test.find()
{ _id: ObjectId("6708d3e646d2075ca11e88ce"),
  uid: '1000001',
  name: 'zhangsan',
  job: 'coder',
  travel: 
   [ { vehicle: 'train', city: '北京' },
     { vehicle: 'plane', city: '上海' },
     { vehicle: 'train', city: '深圳' } ] }
{ _id: ObjectId("6708d3f646d2075ca11e88cf"),
  uid: '1000002',
  name: 'lisi',
  job: 'coder',
  travel: 
   [ { vehicle: 'plane', city: '北京' },
     { vehicle: 'car', city: '上海' },
     { vehicle: 'train', city: '深圳' } ] }

验证三种方法

需求说明

现在的目标是:筛选的出所有记录中通过火车去旅游的城市,也就是travel数组中vehicle=train的记录,过滤掉非目标记录。

方法一

方法一:使用
$unwind

travel
数组打散,获取结果集后用
match
筛选符合条件的数据,最后使用
$group
进行聚合获取最终结果集。

db.getCollection('test').aggregate(
    [
        {   
            $unwind: "$travel" 
        },
        { 
            $match : {
                "job":"coder", 
                "travel.vehicle": "train" 
            } 
        },
        { 
            $group : { 
                "_id" : "$uid", 
                "travel": { $push: "$travel" } 
            } 
        } 
    ]
)

结果:

{ _id: '1000002', travel: [ { vehicle: 'train', city: '深圳' } ] }
{ _id: '1000001', travel: [ { vehicle: 'train', city: '北京' }, { vehicle: 'train', city: '深圳' } ] }

分析:

unwind 可以将一个数组拆分,例如unwind的效果如下:

{ _id: ObjectId("6708d3e646d2075ca11e88ce"),
  uid: '1000001',
  name: 'zhangsan',
  job: 'coder',
  travel: { vehicle: 'train', city: '北京' } }
{ _id: ObjectId("6708d3e646d2075ca11e88ce"),
  uid: '1000001',
  name: 'zhangsan',
  job: 'coder',
  travel: { vehicle: 'plane', city: '上海' } }
{ _id: ObjectId("6708d3e646d2075ca11e88ce"),
  uid: '1000001',
  name: 'zhangsan',
  job: 'coder',
  travel: { vehicle: 'train', city: '深圳' } }
{ _id: ObjectId("6708d3f646d2075ca11e88cf"),
  uid: '1000002',
  name: 'lisi',
  job: 'coder',
  travel: { vehicle: 'plane', city: '北京' } }
{ _id: ObjectId("6708d3f646d2075ca11e88cf"),
  uid: '1000002',
  name: 'lisi',
  job: 'coder',
  travel: { vehicle: 'car', city: '上海' } }
{ _id: ObjectId("6708d3f646d2075ca11e88cf"),
  uid: '1000002',
  name: 'lisi',
  job: 'coder',
  travel: { vehicle: 'train', city: '深圳' } }

然后通过match筛选出符合条件的数据

{ _id: ObjectId("6708d3e646d2075ca11e88ce"),
  uid: '1000001',
  name: 'zhangsan',
  job: 'coder',
  travel: { vehicle: 'train', city: '北京' } }
{ _id: ObjectId("6708d3e646d2075ca11e88ce"),
  uid: '1000001',
  name: 'zhangsan',
  job: 'coder',
  travel: { vehicle: 'train', city: '深圳' } }
{ _id: ObjectId("6708d3f646d2075ca11e88cf"),
  uid: '1000002',
  name: 'lisi',
  job: 'coder',
  travel: { vehicle: 'train', city: '深圳' } }

最后通过group进行聚合,以_id为聚合依赖,合并相同_id的数据。

总结:

这种方法是能够达到过滤数组的要求,但是有一个问题,拆分数组比较简单,想要再合并起来就不容易了。group只能以某一个变量为基准聚合,其他变量都会丢失。比如最后的结果只保留了_id和travel,其他变量都丢失了。

方法二

方法二:使用
$match
过滤符合条件的根文档结果集,然后使用
$project
返回对应字段的同时,在
travel
数组中使用
$filter
进行内部过滤,返回最终结果集

db.getCollection('test').aggregate(
    [
        { 
            $match : { "job": "coder" } 
        },
        {
            $project: {
                "uid": 1,
                "name": 1,
                "travel": {
                    $filter: {
                        input: "$travel",
                        as: "item",
                        cond: { $eq : ["$$item.vehicle","train"] }
                    }
                }
            }
        }
    ]
)

结果分析:

{ _id: ObjectId("6708d3e646d2075ca11e88ce"),
  uid: '1000001',
  name: 'zhangsan',
  travel: [ { vehicle: 'train', city: '北京' },{ vehicle: 'train', city: '深圳' } ] }
{ _id: ObjectId("6708d3f646d2075ca11e88cf"),
  uid: '1000002',
  name: 'lisi',
  travel: [ { vehicle: 'train', city: '深圳' } ] }

分析:

mongo中查询分为两种:普通查询和高级查询。高级查询包括聚合查询,用aggregate关键字实现。

MongoDB的聚合管道将MongoDB文档在一个管道处理完毕后将结果传递给下一个管道处理。管道操作是可以重复的。

这里我们介绍一下聚合框架中常用的几个操作:

  • $project
    :修改输入文档的结构。可以用来重命名、增加或删除域,也可以用于创建计算结果以及嵌套文档。
  • $match
    :用于过滤数据,只输出符合条件的文档。$match使用MongoDB的标准查询操作。
  • $limit
    :用来限制MongoDB聚合管道返回的文档数。
  • $skip
    :在聚合管道中跳过指定数量的文档,并返回余下的文档。
  • $unwind
    :将文档中的某一个数组类型字段拆分成多条,每条包含数组中的一个值。
  • $group
    :将集合中的文档分组,可用于统计结果。
  • $sort
    :将输入文档排序后输出。
  • $geoNear
    :输出接近某一地理位置的有序文档。

这里首先使用match过滤所有job=coder,然后使用project修改输出的结构。在project中使用了filter来过滤数组中的元素。

filter的定义如下:

根据指定条件选择要返回的数组的子集。返回仅包含与条件匹配的那些元素的数组。返回的元素按原始顺序。

$filter
具有以下语法:

{ $filter: { input: <array>, as: <string>, cond: <expression> } }
领域 规格
input 解析为数组的
表达式
as 可选的。代表数组中每个单独元素的
变量
名称
<u><font style="color:rgb(199, 37, 78);background-color:rgb(249, 242, 244);">input</font></u>
。如果未指定名称,则变量名称默认为
<u><font style="color:rgb(199, 37, 78);background-color:rgb(249, 242, 244);">this</font></u>
cond
表达式
可解析为布尔值,该布尔值用于确定输出数组中是否应包含元素。该表达式
<u><font style="color:rgb(199, 37, 78);background-color:rgb(249, 242, 244);">input</font></u>
使用在中指定的变量名称分别引用数组的每个元素
<u><font style="color:rgb(199, 37, 78);background-color:rgb(249, 242, 244);">as</font></u>

https://mongodb.net.cn/manual/reference/operator/aggregation/filter/

在cond将vehicle=train的元素留下,排除其他元素。

总结:

这种方法可以完成查询目标,既可以过滤掉数组中的元素,也可以返回完整的文档。

方法三

方法三:

通过投影查询,先选择符合条件的记录,在通过使用投影操作符,需要返回的字段,以及排除特定的字段。

db.test.find(
      {
         job: "coder"
      }, 
      {  
          uid: 1, 
          name: 1, 
          travel: {
             $filter: {
                input: "$travel",
                as: "item",
                cond: { $eq : ["$$item.vehicle","train"] }
             } 
          } 
      }
)

结果:

{ _id: ObjectId("6708d3e646d2075ca11e88ce"),
  uid: '1000001',
  name: 'zhangsan',
  travel: 
   [ { vehicle: 'train', city: '北京' },
     { vehicle: 'train', city: '深圳' } ] }
{ _id: ObjectId("6708d3f646d2075ca11e88cf"),
  uid: '1000002',
  name: 'lisi',
  travel: [ { vehicle: 'train', city: '深圳' } ] }

分析:

什么是投影查询?

在MongoDB中,投影查询是一种查询操作,用于选择性地返回文档中的字段。通过使用投影操作符,我们可以指定需要返回的字段,以及是否要排除特定的字段。

投影查询语法如下所示:

db.collection.find({ <query> }, { <projection> })

其中,
是一个查询表达式,用于筛选满足条件的文档。 是一个可选参数,用于指定要返回的字段。

在projection中保留字段、排除字段、
选择或排除数组中的特定元素。利用选择或排除数组中的特定元素的特性也可以达到目的。

例如:

如果我们只想返回每个文档中的第一个标签,我们可以这样做:

db.products.find({}, { tags: { $slice: 1 } })

在本篇中通过filter方法来过滤数组,保留符合条件的元素。

总结:

该方法能够完成查询目标,并且是一种简洁的实现,普通查询复杂度低,而且没有太多关键字的使用。

参考文档

https://geek-docs.com/mongodb/mongodb-questions/393_mongodb_mongo_query_with_projection.html

https://segmentfault.com/a/1190000016629733

https://mongodb.net.cn/manual/reference/operator/aggregation/filter/

https://blog.csdn.net/weixin_44009447/article/details/115479348

近年来,随着前端技术的飞速发展,各类后台管理系统框架层出不穷。
Vue
作为热门的前端框架,也有许多优秀的后台模板涌现。而
Vue-Vben-Admin
,凭借其高效、灵活的架构设计和完善的功能体系,成为了许多前端开发者的不二选择。其
Github Star达
到了
24K
之多,可见其受欢迎程度。本文将详细介绍 Vue-Vben-Admin 的特点、使用方法以及适用场景,帮助大家深入了解这个强大的工具。

简要介绍

Vue-Vben-Admin
是基于
Vue3

Vite

TypeScript
构建的后台管理系统模板。它集成了
Vue3
生态中的多种先进技术,如
Pinia
作为状态管理工具、
Vue Router
作为路由管理工具,并通过
Shadcn UI
提供了一套现代化的 UI 组件,具有强大的扩展性和模块化特性。无论是开发中小型项目,还是大型企业级应用,
Vue-Vben-Admin
都能够提供一个强大、灵活、可扩展的管理后台框架。

显著特性

  • 最新技术栈
    :使用 Vue3/vite 等前端前沿技术开发
  • TypeScript
    : 应用程序级 JavaScript 的语言
  • 主题
    :提供多套主题色彩,可配置自定义主题
  • 国际化
    :内置完善的国际化方案
  • 权限
    :内置完善的动态路由权限生成方案

使用方式

  1. 安装依赖
git clone https://github.com/vbenjs/vue-vben-admin.git
cd vue-vben-admin
pnpm install
  1. 运行
pnpm serve

启动成功后,你可以在浏览器中打开
http://localhost:3000
,访问后台管理系统。
3. 自定义配置
项目配置文件位于
src/config
目录中,开发者可以根据项目需求自定义
API 地址

权限设置
以及
主题风格

适用场景

Vue-Vben-Admin
的高扩展性和模块化设计,使得它能够适应多种场景的后台管理系统开发:

  1. 企业级后台管理系统
    Vue-Vben-Admin
    内置的
    权限管理

    模块化架构
    非常适合用于开发大型企业级管理系统,支持复杂的
    用户角色

    权限控制
    ,满足企业的各种业务需求。

  2. 中小型项目管理平台
    对于中小型项目,
    Vue-Vben-Admin
    提供了快速构建的能力,开箱即用的功能和高效的开发工具,使开发者能够快速搭建功能完善的管理平台。

  3. SaaS 平台
    Vue-Vben-Admin
    具有高度的灵活性,能够根据不同的
    SaaS
    平台需求进行功能扩展和定制,同时支持多用户权限体系的灵活配置,是开发
    SaaS
    平台管理系统的理想选择。

  4. 技术学习与练手项目
    作为一个集成了
    Vue3
    生态中各种先进技术的项目,
    Vue-Vben-Admin
    非常适合用于学习和实践前端技术的开发者,通过它可以深入学习
    Vue3

    Vite

    TypeScript

    Pinia
    等技术的使用与应用。

结语

Vue-Vben-Admin
作为一个基于
Vue3
的后台管理模板,凭借其高效的构建工具、完善的功能体系、灵活的模块设计,为前端开发者提供了强大的支持。不论是大型企业的管理系统,还是个人项目的后台框架,它都能帮助开发者快速搭建出一个高性能、高扩展性的后台系统。如果你正在寻找一个功能强大、易于扩展的
Vue3
管理系统模板,不妨试试
Vue-Vben-Admin
,它将是你开发项目的好帮手。


该模版已经收录到我的全栈前端一站式开发平台
“前端视界”
中(浏览器搜
前端视界
第一个),感兴趣的欢迎查看!