2024年11月

开心一刻

今天,表妹问我:哥,我男朋友过两天要生日了,你们男生一般喜欢什么,帮忙推荐个礼物呗
我:预算多少
表妹:预算300
我:20块买条黑丝,剩下280给自己买支口红,你男朋友生日那天你都给自己用上
表妹:秒啊,哥
我:必须的嘛,你要知道男人最懂男人!

开心一刻

前情回顾

关于异源数据同步工具
DataX
,我已经写了好几篇文章

异构数据源同步之数据同步 → datax 改造,有点意思
异构数据源同步之数据同步 → datax 再改造,开始触及源码
异构数据源同步之数据同步 → DataX 使用细节
异构数据源数据同步 → 从源码分析 DataX 敏感信息的加解密
异源数据同步 → DataX 为什么要支持 kafka?

推荐大家去看看,可以对 DataX 有个基本的了解,掌握其基本使用;示例代码:
qsl-datax

需求背景

假设我们基于
XXL-JOB
实现调度,
qsl-datax-hook
作为 XXL-JOB 执行器的同时也充当
DataX
的拉起方,三者调用关系如下

datax同步数据量

离线同步的数据量往往会很大,少则上万,多则上亿,所以同步过程一般会持续很长时间,如何确认同步仍在进行中呢?我们可以看日志,也可以查目标表的记录数,但都不够直观,好的实现方式应该是有同步任务查看页面,通过该页面可以查看到正在同步中的任务,以及这些任务已同步的数据量,所以问题就来到

如何获取 DataX 已同步数据量?

已同步数据量

换做是你们,你们会如何实现?或者说有什么思路?我提供下我的方案,是不是也是你们所想

DataX 的 Writer 往目标源写数据的时候,一次写多少数据我们就记录多少,然后累加并持久化,是不是就可以实时查看当前的已同步数据量呢?

具体如何实现了,我们可以基于 DataX 的日志来实现;我们在讲
异构数据源同步之数据同步 → datax 再改造,开始触及源码
的时候,对日志进行了调整,
qsl-datax-hook
能够获取 DataX 进程的日志输出,所以我们只需要在 DataX 往目标源写入数据完成后往日志中写入一条记录(写入了多少数量),qsl-datax-hook 就能够获取该记录,从而得到写入数据量,然后进行累加操作;我们以
mysqlwriter
为例,来看看通过代码到底如何实现

  1. writer 往日志中写
    同步数据量

    从哪里找切入点,我就不绕弯子了


    writer写日志起点

    跟进
    startWriteWithConnection
    ,有如下代码


    doBatchInsert

    可以看到是批量写入的,继续跟进
    doBatchInsert


    doBatchInsert关键点

    分两种情况


    1. 正常情况,批量插入并 commit 成功

    2. 异常情况,先回滚批量插入,然后通过
      doOneInsert


      doOneInsert

      逐条插入


    所以在哪里写
    同步数据量
    的日志是不是清楚了,有两个地方需要写


    1. doBatchInsert
      批量插入 commit 之后写日志


      批量插入记录同步数据量
    2. doOneInsert
      单条插入后写日志


      单挑插入记录同步数据量

    DataX 就算改造好了,是不是很简单?

  2. qsl-datax-hook 读取 DataX 日志中的
    同步数据量
    并持久化

    com.qsl.hook.DataXManager#exec
    适配改造下即可


    hook适配改造

    做持久化的时候一定要采用

    update table_name set sync_rows = sync_rows + syncRows;
    

    的方式,利用数据库的锁来避免并发问题,而采用 set 具体的值

    update table_name set sync_rows = totalSyncRows;
    

    会有并发覆盖问题,比如第一次将总量更新成 50000,而第二次可能将总量更新成 48000

至此,需求就算基本完成了;其他类型的 DataX writer 可以采用类似的方式来实现,具体就不演示了,你们自行去实现

总结

如果目标源支持事务,那么
已同步数据量
可以实现的很准确,如果目标源不支持事务,那么
已同步数据量
实现的就不会很准确,或者说实现难度非常高;文中讲到的日志方式,只是实现方式之一,还有其他的实现方式,例如

  1. 定时读取目标源的数据量

  2. 改造DataX,直接持久化
    已同步数据量

    update table_name set sync_rows = sync_rows + syncRows;
    

各种方式都有其优势,也存在其弊端,需要结合业务选择合适的方式

平时用IntelliJ IDEA写代码的时候,你有没有用过这些快捷方式:

输入
main
,会弹出自动补全完整的
main
结构:

输入
sout
,会弹出自动补全完整的
System.out
语句:

那么问题来了:

  1. 还有哪些快捷方式?
  2. 如何定义自己想要的?

初识 Live Templates

该功能来自于IntelliJ IDEA的Live Templates配置,你可以通过菜单进入
Setting
,然后搜索
Live Templates
找到它:

点开
Java
就能看预定义的模板了:

不是很多,可以挑你常用的记一下即可。

如果要定义要用的模板,可以点击上面的
+

选择
Live Tempalte
之后在下面会看到一个编辑框:

根据自己需要填写要创建的快捷模板内容。最后记得保存,就可以成功创建了。

尝试在编码框内输入上面定义的快捷方式:
ddfor
,就可以用到上面定义的模板代码了:

使用进阶

上面仅介绍了Live Template最基本的使用方式。如果还不能满足你的要求,下面几项提示也许可以帮到你。

使用分组

如果对这个功能的需求比较多,需要定义比较多模板,尤其是做基础架构给大家定规范做工具的话,还可以在创建Live Template的时候使用Group来创建一些独立的组来方便管理。

使用参数

很多时候我们创建模版还会需要一些动态的信息,比如自定义模板注释的时候,需要使用:时间、用户等动态信息。

在Live Template的模板定义中是支持使用参数的,使用
$$
来引用,两个
$
中间放参数名。Live Template提供了一些预定义的参数,同时也支持用户自定义变量。

关于这块使用参数和有哪些预定义参数,读者可以自行查阅官方文档:
Live template variables

导入导出

如果你想使用别人的模板,或者想把自己的模板分享给被人,那么可以使用导入导出功能。

功能位置如下图:

然后选择你要导出导入的配置内容里选择Live Templates即可

好了,今天的分享就到这里,希望内容对您有用
_
,更多关于IDEA的使用技巧可以收藏
《玩转IDEA专栏》

初探富文本之搜索替换算法

在我们的在线文档系统中,除了基础的富文本编辑能力外,搜索替换的算法同样也是重要的功能实现。纯文本搜索替换算法相对来说是比较容易实现的,而在富文本的场景中,由于存在图文混排、嵌入模块如
Mention
等,实现这一功能则相对要复杂得不少。那么本文则以
Quill
实现的富文本编辑器为基础,通过设计索引查找和图层渲染的方式来实现搜索替换。

描述

首先我们先来思考一下纯文本的搜索替换算法,对于纯文本而言我们可以直接通过一些方法来查找对应文本的索引值,然后无论是将其直接替换还是切割再联合的方式,都可以轻松地完成这个功能。而富文本本质上可以看作是携带着格式的纯文本,那么在这种情况下我们依然可以延续着对于纯文本的处理思路,只不过由于存在文本会被切割为多个片段的问题,就需要我们来兼容这个数据层面上的问题。

实际上在前边我们强调了当前的实现是以
Quill
为基础实现的方案,因为本质上搜索替换与数据结构的实现强相关,
quill-delta
是扁平的数据结构,相对来说比较方便我们来处理这个问题。而如果是嵌套的数据结构类型例如
Slate
的实现,我们仍然可以依照这个思路来实现,只不过在这里如果我们将其作为
DEMO
来实现并没有那么直观,嵌套的数据结构类型对于替换的处理上也会复杂很多。

那么在这里我们先来构造
Quill
的编辑器实例,在我们实现的实例中,我们只需要引用
quill.js
的资源,然后指定实例化的
DOM
节点即可初始化编辑器,我们在这里通过配置来注册了样式模块。文中的相关实现可以在
https://github.com/WindRunnerMax/QuillBlocks/blob/master/examples/find-replace.html
中查看。

<div class="editor-container">
    <div id="$editor"></div>
</div>
<script src="https://cdn.quilljs.com/1.3.6/quill.js"></script>
<script>
const editor = new Quill($editor, {
  modules: {
    toolbar: [[{ header: [1, 2, false] }], ["bold", "italic", "underline"], ["code-block"]],
  },
  placeholder: "Enter Text...",
  theme: "snow",
});
</script>


Quill
中是使用
quill-delta
来描述富文本内容的,
quill-delta
是一个扁平的数据结构,除了能描述富文本内容之外,还可以描述富文本内容的变更。在这里我们首先关注的是文档内容的描述,因此这里我们则是全部以
insert
的形式来描述,而之前我们也提到了富文本实际上就是纯文本携带属性,因此我们可以很轻松地使用下面的样式描述普通文本与加粗文本。

const delta = new Delta([
  { insert: "Hello " },
  { insert: "World", attributes: { bold: true } },
]);

我们的搜索替换方案本质上是在数据描述的基础上来查找目标内容,而从上边的数据结构来看,我们可以明显地看出在
attributes
上也可能存在需要查找的目标内容,例如我们需要实现
Mention
组件,是可以在
attributes
中存储
user
信息来展示
@user
的信息,而
insert
置入的内容则是空占位,那么在这里我们就实现一下
Quill

Mention
插件。

const Embed = Quill.import("blots/embed");
class Mention extends Embed {
  static blotName = "mention";
  static className = "ql-mention";
  static tagName = "SPAN";
  static create(value) {
    const node = super.create(value);
    node.innerHTML = `@${value}`;
    node.setAttribute("data-value", value);
    return node;
  }
  static value(domNode) {
    return domNode.getAttribute("data-value");
  }
}
Quill.register(Mention);

则此时我们的预置的数据结构内容则如下所示,我们可能注意到数据结构中
Mention

insert
是对象并且实际内容是并不是在
attributes
而是在
insert
中。这是因为在
Quill
中描述
parchment

blots/embed

value
是由
insert
存储的
blotName
对象来完成的,因此在示例中我们查找的目标还是在
insert
对象中,但是并不实际影响我们需要对其进行额外的查找实现。

editor.setContents([
  { "insert": "查找替换" },
  { "attributes": { "header": 1 }, "insert": "\n" },
  { "insert": "查找替换的富文本基本能力,除了基本的" },
  { "attributes": { "bold": true }, "insert": "文本查找" },
  { "insert": "之外,还有内嵌结构例如" },
  { "attributes": { "italic": true }, "insert": "Mention" },
  { "insert": "的" },
  { "insert": { mention: "查找能力" } },
  { "insert": "。\n" },
]);

搜索算法

首先我们先来设计一下内容的搜索算法,既然前边我们已经提到了富文本实际上就是带属性的纯文本,那么我们就先来设想对于纯文本的搜索实现。文本搜索算法实现本质上就是字符串文本的匹配,那么我们很容易想到的就是
KMP
算法,而实际上
KMP
算法并不是最高效的实现算法,在各种文本编辑器中实现的查找功能更多的是使用的
Boyer-Moore
算法。

在这里我们先不考虑高效的算法实现,我们实现的目标则是在长文本中查找目标文本的索引值,因此可以直接使用
String
对象的
indexOf
来实现查找。而我们实际上是需要获取目标文本的所有索引值,因此需要借助这个方法的第二个参数
position
来实现查找的起始位置。而如果我们使用正则来实现索引则容易出现输入的字符串被处理为正则保留字的问题,例如搜索
(text)
字符串时,则在不特殊处理的情况下会被认为搜索
text
文本。

const origin = "123123000123";
const target = "123";
const indices = [];
let startIndex = 0;

while ((startIndex = origin.indexOf(target, startIndex)) > -1) {
  indices.push(startIndex);
  startIndex += target.length;
}

console.log(indices); // [0, 3, 9]
const origin = "123123000123";
const target = "123";
const indices = [];
const regex = new RegExp(target, "g");
let match;

while ((match = regex.exec(origin)) !== null) {
  indices.push(match.index);
}

console.log(indices); // [0, 3, 9]

在我们上述的测试中,可以将其放置于
https://perf.link/
测试一下执行性能,将其分别实现防止于
Test Cases
中测试,可以发现实际上
indexOf
的性能还会更高一些,因此在这里我们就直接使用
indexOf
来实现我们的搜索算法。在浏览器中测试万字文本的
indexOf
搜索,结果稳定在
1ms
以内,性能表现是完全可以接受的,当然这个搜索结果和电脑本身的性能也是强相关的。

indexOf // 141,400 ops/s
RegExp // 126,030 ops/s

那么此时我们就来处理关于
Delta
的索引建立,那么由于富文本的表达存在样式信息,反映在数据结构上则是存在文本的片段,因此我们的搜索首先需要直接将所有文本拼接为长字符串,然后使用
indexOf
进行查找。此外在前边我们已经看到数据结构中存在
insert
,在这里我们需要将其作为不可搜索的文本占位,以避免在搜索时将其作为目标文本的一部分。

const delta = editor.getContents();
const str = delta.ops
  .map(op => (typeof op.insert === "string" ? op.insert : String.fromCharCode(0)))
  .join("");
let index = str.indexOf(text, 0);
const ranges = [];
while (index >= 0) {
  ranges.push({ index, length: text.length });
  index = str.indexOf(text, index + text.length);
}

而对于类似于
Mention
模块的
Void/Embed
结构处理则需要我们特殊处理,在这里不像是先前对于纯文本的内容搜索,我们不能将这部分的相关描述直接放置于
insert
中,因此此时我们将难以将其还原到原本的数据结构中,如果不能还原则无法将其显示为搜索的结果样式。那么我们就需要对
delta.ops
再次进行迭代,然后
case by case
地将需要处理的属性进行处理。

const rects = [];
let iterated = 0;
delta.ops.forEach(op => {
  if (typeof op.insert === "string") {
    iterated = iterated + op.insert.length;
    return void 0;
  }
  iterated = iterated + 1;
  if (op.insert.mention) {
    const value = op.insert.mention;
    const mentionIndex = value.indexOf(text, 0);
    if (mentionIndex === -1) return void 0;
    // 绘制节点位置 ...
  }
});

当我们将所有的数据收集到起来后,就需要构建虚拟图层来展示搜索的结果。首先我们需要处理最初处理的
insert
纯文本的搜索结果,由于我们在输入的时候通常都是使用
input
来输入搜索的文本目标,因此我们是不需要处理
\n
的情况,这里的图层处理是比较方便的。而具体的虚拟图层实现在先前的
diff
算法文章中已经描述得比较清晰,这里我们只实现相关的调用。

const rangeDOM = document.createElement("div");
rangeDOM.className = "ql-range virtual-layer";
$editor.appendChild(rangeDOM);
const COLOR = "rgba(0, 180, 42, 0.3)";
const renderVirtualLayer = (startRect, endRect) => {
  // ...
};
const buildRangeLayer = ranges => {
  rangeDOM.innerText = "";
  ranges.forEach(range => {
    const startRect = editor.getBounds(range.index, 0);
    const endRect = editor.getBounds(range.index + range.length, 0);
    rangeDOM.appendChild(renderVirtualLayer(startRect, endRect));
  });
};

而在这里我们更需要关注的是
Mention
的处理,因为在这里我们不能够使用
editor.getBounds
来获取其相关的
BoundingClientRect
,因此这里我们需要自行计算其位置。因此我们此时需要取得
mentionIndex
所在的节点,然后通过
createRange
构建
Range
对象,然后基于该
Range
获取
ClientRect
,这样就取得了我们需要的
startRect

endRect
,但是需要注意的是,此时我们取得是绝对位置,还需要将其换算为编辑器实例的相对位置才可以。

const [leaf, offset] = editor.getLeaf(iterated);
if (
  leaf &&
  leaf.domNode &&
  leaf.domNode.childNodes[1] &&
  leaf.domNode.childNodes[1].firstChild
) {
  const textNode = leaf.domNode.childNodes[1].firstChild;
  const startRange = document.createRange();
  startRange.setStart(textNode, mentionIndex + 1);
  startRange.setEnd(textNode, mentionIndex + 1);
  const startRect = startRange.getBoundingClientRect();
  const endRange = document.createRange();
  endRange.setStart(textNode, mentionIndex + 1 + text.length);
  endRange.setEnd(textNode, mentionIndex + 1 + text.length);
  const endRect = endRange.getBoundingClientRect();
  rects.push({ startRect, endRect });
}

rects.forEach(it => {
  const editorRect = editor.container.getBoundingClientRect();
  const startRect = {
    bottom: it.startRect.bottom - editorRect.top,
    height: it.startRect.height,
    left: it.startRect.left - editorRect.left,
    right: it.startRect.right - editorRect.left,
    top: it.startRect.top - editorRect.top,
    width: it.startRect.width,
  };
  const endRect = {
    bottom: it.endRect.bottom - editorRect.top,
    height: it.endRect.height,
    left: it.endRect.left - editorRect.left,
    right: it.endRect.right - editorRect.left,
    top: it.endRect.top - editorRect.top,
    width: it.endRect.width,
  };
  const block = renderVirtualLayer(startRect, endRect);
  rangeDOM.appendChild(block);
});

批量替换

替换的实现在
Delta
的结构上会比较简单,在先前我们也提到了
Delta
不仅能够通过
insert
描述文档,还可以借助
retain

delete

insert
方法来描述文档的变更,那么我们需要做的就是在上述构造的
ranges
的基础上,构造目标的变更描述。而由于我们先前构造的
Mention
是不允许进行实质性的替换操作的,所以我们只需要关注原本的
insert
文本内容即可。

这里的实现重点是实现了批量的
changes
构造,首先需要定义
Delta
实例,紧接着的
preIndex
是用来记录上一次执行过后的索引位置,在我们的
ranges
循环中,
retain
是用来移动指针到当前即将处理的原文本内容,然后调用
delete
将其删除,之后的
insert
是替换的目标文本,注意此时的指针位置是在目标文本之后,因此需要将
preIndex
更新为当前处理的索引位置,最后将其应用到编辑器即可。

const batchReplace = () => {
  const text = $input1.value;
  const replace = $input2.value;
  if (!text) return void 0;
  const changes = new Delta();
  let preIndex = 0;
  ranges.forEach(range => {
    changes.retain(range.index - preIndex);
    changes.delete(range.length);
    changes.insert(replace);
    preIndex = range.index + range.length;
  });
  editor.updateContents(changes);
  onFind();
};

每日一题

https://github.com/WindrunnerMax/EveryDay

参考

https://quilljs.com/docs/delta
https://www.npmjs.com/package/parchment
https://www.npmjs.com/package/quill-delta/v/4.2.2
https://developer.mozilla.org/zh-CN/docs/Web/API/Document/createRange
https://www.ruanyifeng.com/blog/2013/05/boyer-moore_string_search_algorithm.html
https://www.ruanyifeng.com/blog/2013/05/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm.html

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

论文: Visual Grounding with Multi-modal Conditional Adaptation

创新点


  • 提出了多模态条件适应(
    MMCA
    )方法,该方法从一种新颖的权重更新视角改善了视觉引导模型中视觉编码器的特征提取过程。
  • 将提出的
    MMCA
    应用于主流的视觉引导框架,并提出了灵活的多模态条件变换器和卷积模块,这些模块可以作为即插即用组件轻松应用于其他视觉引导模型。
  • 进行广泛的实验以验证该方法的有效性,在四个具有代表性的数据集上的结果显示出显著的改善,且成本较小。

内容概述


视觉定位旨在将传统的物体检测推广到定位与自由形式文本描述相对应的图像区域,已成为多模态推理中的核心问题。现有的方法通过扩展通用物体检测框架来应对这一任务,使用独立的视觉和文本编码器分别提取视觉和文本特征,然后在多模态解码器中融合这些特征以进行最终预测。

视觉定位通常涉及在同一图像中定位具有不同文本描述的物体,导致现有的方法在这一任务上表现不佳。因为独立的视觉编码器对于相同的图像生成相同的视觉特征,从而限制了检测性能。最近的方法提出了各种语言引导的视觉编码器来解决这个问题,但它们大多仅依赖文本信息,并且需要复杂的设计。


LoRA
在适应不同下游任务的高效性的启发,论文引入了多模态条件适配(
MMCA
),使视觉编码器能够自适应更新权重,专注于与文本相关的区域。具体而言,首先整合来自不同模态的信息以获得多模态嵌入,然后利用一组从多模态嵌入生成的权重系数,来重组权重更新矩阵并将其应用于视觉定位模型的视觉编码器。

MMCA


MMCA
遵循典型的端到端编码器-解码器范式:

  1. 给定一幅图像和一个语言表达作为输入将其输入到编码器部分,以生成相应的特征嵌入。
    1. 在语言分支中,语言主干将经过分词的语言表达作为输入,并提取文本特征
      \(f_t\in \mathbb{R}^{N_t\times C_t}\)
      ,其中
      \(N_t\)
      是语言标记的数量。
    2. 在视觉分支中,
      CNN
      主干首先提取一个二维特征图,然后经过一系列变换器编码器层,生成一个展平的视觉特征序列
      \(f_v\in \mathbb{R}^{N_v\times C_v}\)
    3. 多模态条件适应(
      MMCA
      )模块以层级方式应用于卷积层和变换器层的参数矩阵。该模块同时接受视觉和文本特征作为输入,并动态更新视觉编码器的权重,以实现基于语言的视觉特征提取。
  2. 将视觉和文本特征嵌入连接在一起,并在多模态解码器(视觉-语言变换器)的输入中添加一个可学习的标记 [
    REG
    ],该解码器将来自不同模态的输入标记嵌入对齐的语义空间,并通过自注意力层执行模态内和模态间的推理。
  3. 回归头使用 [
    REG
    ] 标记的输出状态来直接预测被指对象的四维坐标
    \(\hat b = (\hat{x}, \hat{y}, \hat{w}, \hat{h})\)
    。与真实框
    \(b = (x, y, w, h)\)
    的训练损失可以表述为:

\[\begin{equation}
\mathcal L=\mathcal L_{smooth-l1}(\hat b, b)+L_{giou}(\hat b, b)
\end{equation}
\]

条件适应

对于视觉引导任务,论文希望不同的指代表达能够控制视觉编码器的一组权重更新,从而引导编码器的注意力集中在与文本相关的区域。然而,直接生成这样的矩阵带来了两个缺点:(
1
)这需要一个大型参数生成器。(
2
)没有约束的生成器可能在训练中对表达式过拟合,而在测试期间却难以理解表达式。


LoRA
的启发,让网络学习一组权重更新的基矩阵并使用多模态信息重新组织更新矩阵。这使得参数生成器变得轻量,并确保网络的权重在同一空间内更新。

具体而言,先对权重更新矩阵进行分解,并将其重新表述为外积的和,通过
\(B_i, A_i\)
并使用加权和来控制适应的子空间:

\[\begin{equation}
\Delta Wx=BAx={\textstyle \sum_{i=1}^{r}} B_i\otimes A_i
\label{eq3}
\end{equation}
\]

\[\begin{equation}
h=W_0x+\Delta Wx=W_0x+{\textstyle \sum_{i=1}^{r}} w_iB_i\otimes A_i
\label{eq4}
\end{equation}
\]

为了简化并且不引入其他归纳偏差,使用线性回归来生成这一组权重:

\[\begin{equation}
[w_1, w_2, ..., w_r]^T
=W_gE_{mm}+
[b_1, b_2, ..., b_r]^T
\label{eq5}
\end{equation}
\]

其中
\(W_g\in \mathbb{R}^{r\times d}, [b_1, b_2, ..., b_r]^T\)
是参数矩阵,
\(E_{mm}\in \mathbb{R}^{d}\)
是特定层的多模态嵌入,它是由文本特征和从前一层输出的视觉特征生成的。

与迁移学习任务不同,这里并不打算微调一小部分参数以适应特定的下游任务,而是希望视觉编码器能够适应各种表达。因此,所有参数矩阵
\(W_0, B, A, W_g, [b_1, b_2, ..., b_r]^T\)
在训练阶段都是可学习的。

多模态嵌入

仅依赖文本信息来引导视觉编码器可能会在某些应用中限制灵活性,并且性能可能会受到文本信息质量的影响。为了缓解这些问题,采用门控机制来调节文本信息的输入。

给定文本特征
\(F_t\in \mathbb{R}^{N_t\times C_t}\)
和展平的视觉特征
\(F_v\in \mathbb{R}^{HW\times C_v}\)
,使用简单门控机制来融合视觉和文本嵌入:

\[\begin{equation}
E_{t} = W_tF_t, E_{v}=W_vF_v
\end{equation}
\]

\[\begin{equation}
\alpha =\sigma[W^1_g\delta(W^2_g(E_{t}+E_{v}))]
\end{equation}
\]

\[\begin{equation}
E_{mm} = \alpha E_{t} + E_{v}
\end{equation}
\]

最后,融合嵌入
\(E_{mm}\)
被用来生成系数,从而指导视觉编码器的权重更新。

适配视觉定位

基于视觉编码器(卷积层和
Transformer
层),进一步提出了多模态条件
Transformer
和多模态条件卷积,用于将
MMCA
应用于视觉定位中。

  • 多模态条件
    Transformer

视觉主干中的
Transformer
编码器层主要由两种类型的子层组成,即
MHSA

FFN
。通过应用多模态条件适应,
MHSA

FFN
的计算变为:

\[\begin{equation}
\begin{split}
h'=softmax(\frac{(W'_q)XX^T(W'^T_k)} {\sqrt{d_k}})W_vX + X\\
W'_q = W_q + \Delta W_{q}, W'_k = W_k + \Delta W_{k}
\end{split}
\label{eq8}
\end{equation}
\]

\[\begin{equation}
X_{output}=LN(MLP(h')+\Delta W_{m}h'+h')
\label{eq9}
\end{equation}
\]

其中
\(\Delta W_{q}, \Delta W_{k}, \Delta W_{m}\)
是查询、关键和
MLP
块的线性投影的条件权重更新。

  • 多模态条件卷积

为了便于应用多模态条件适应,将卷积权重更新展开为一个
2-D
矩阵并用两个矩阵
\(B\in \mathbb{R}^{c_{in}\times r}, A\in \mathbb{R}^{r\times c_{out}k^2}\)
进行近似,秩为
\(r\)
。于是,卷积块的多模态条件适应可以通过两个连续的卷积层
\(Conv_B\)

\(Conv_A\)
来近似:

\[\begin{equation}
X_{output}=Conv_{k \times k}(X)+Conv_A(W_{mm} \odot Conv_B(X))
\label{eq10}
\end{equation}
\]

其中
\(X\)

\(W_{mm}=[w_1, w_2, ..., w_r]^T\)
分别是来自前一卷积层的视觉特征和从多模态嵌入生成的权重系数。在通道维度上计算系数与
\(Conv_B\)
输出的点积,并将输出输入到
\(Conv_A\)
,这相当于重新组织权重更新。

主要实验




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

work-life balance.

比赛在这里呢

填算符

下发题解说的神马东西,赛时根本想不到

讲一个赛时想得到的
\(O(n\log 值域)\)
的思路,很好理解

我们处理出二进制下每一位上的 1 的最后一次出现的位置,
将第
\(i\ (i\in[0,60])\)
位上的 1 最后一次出现的位置记作
\(pos_i\)

同时我们设
\(H=n-k-1\)
为总共有的
bitor
的操作数

有以下结论:由于
\(pos_i\)

\(i\)
位上最后一个 1,所以一旦它后面放了一个 与,这一位上就是 0 了;若我们想要这一位为 1,必须至少满足从
\(pos_i\)
到最后的运算符全是
bitor

发现有以下情况:


  • \(n-pos_i>H\)
    ,即
    \(pos_i\)
    之后需要放的运算符的数量比
    bitor
    的总操作数多,也就是说在
    \(pos_i\)
    之后我一定需要放
    bitand
    操作,所以这种情况下这一位一定不对答案有贡献


  • \(n-pos_i<H\)
    ,也就是说我可以从
    \(pos_i\)
    的前一个位置开始到最后全放
    bitor
    操作,那么这样第
    \(i\)
    位上可以是 1,为了使值最大,所以第
    \(i\)
    位上一定要是 1,所以从第
    \(pos_i\)
    位到最后必须全是
    bitor
    操作,
    对于这种情况的
    \(i\)
    我们记为合法位


  • \(n-pos_i=H\)
    ,也就是说从第
    \(pos_i\)
    到最后的运算符可以全是
    bitor
    操作,但
    \(pos_i\)
    的前一位只能是
    bitand
    所以我们
    特判从第 1 个位置到
    \(pos_i\)
    的前一位全放
    bitand
    能不能让到第
    \(pos_i\)
    个数时得到的值第 $\forall $
    \(j 满足 [pos_j=pos_i]\)
    位为 1,若能则该位也为合法位,否则不合法

对于所有合法位的
\(pos\)
取最小值设为
\(end\)
,因为已经保证
\(end\)
到最后的预算符全是
bitor
,此时有一下两种可能,而我们想尽量构成第二种可能:

  1. \(end\)
    的前一位预算符也为
    bitor
    ,这样我们一定能达到答案最大了

    ,想使答案最优直接让从
    \(end-2\)
    开始的
    \(k\)
    个运算符为
    bitor
    就好了

  2. \(end\)
    的前一位在某些情况为
    bitand
    也是可以使答案最大的,所以我们
    判断能不能让
    \(end\)
    的前一位为
    bitand
    同样使答案最大;

    发现可以的条件相当于从第
    \(end-1\)
    个数到最前面用仅剩的
    bitor
    操作得到一个答案,使得这个答案第 $\forall $
    \(i 满足 [pos_i=end]\)
    位为 1,若能满足条件则第
    \(end-1\)
    个操作符为
    bitand

满足条件的判断又和上述的第三个情况判断一致了,相当于以
\(end-1\)
为下界,再做一次求
\(min(合法的\ pos)\)
,实质上是不断的递归。

形式化如下:

所以一个递归
\(dfs(end, H)\)
表示下界为
\(end\)
,还剩
\(H\)

bitor
操作,判断能不能得到我想要的答案:

若不能则直接从第
\(end-2\)
开始的
\(k-res\)
个运算符全为
bitand
就是答案(
\(res\)
为在之前的递归中已经确定的
bitand
的个数)

若能
则第
\(end-1\)
个位置可以为
bitand

,并设
\(end'=min(这一层中合法的\ pos)\)

继续递归
\(dfs(end',H-(end-end'))\)
判断第
\(end'-1\)
个位置能不能为
bitand

code:

#include<bits/stdc++.h>
#define Aqrfre(x, y) freopen(#x ".in", "r", stdin),freopen(#y ".out", "w", stdout)
#define mp make_pair
#define Type ll
#define qr(x) x=read()
typedef __int128 INT;
typedef long long ll;
using namespace std;

inline ll read(){
    char c=getchar(); ll x=0, f=1;
    while(!isdigit(c)) (c=='-'?f=-1:f=1), c=getchar();
    while(isdigit(c)) x=(x<<1)+(x<<3)+(c^48), c=getchar();
    return x*f;
}

const int N = 1e6 + 5; 
const int maxn = 1e8;

int n, k, K; ll a[N], b[N];

int la[62], pre[62][N], zh[62], X;
vector<int>v[N], ans, tem, num;

inline bool check(int pos, int op){ // 判断从第一个运算符到第 pos 个全为 & 能不能使得到的值满足条件
    int now = pos + 1; ll x = 0;
    for(int i : v[now]) x += (1 << i);
    if(~X) x += (1 << X);
    int y = a[1];
    for(int i=2; i<=now; i++)
        y = y & a[i];
    if(y & x == x) return true;
    return false;
}

inline void dfs(int pos, int H){ // 递归函数
    if(pos <= 0 and H <= 0) return;
    int now = pos + 1, end = 2e9;
    bool f = true; X = -1;
    for(int x : tem) v[pre[x][now]].clear(); //为方便更新新的一层的 V ,先清空
    for(int x : tem){
        if(pos - pre[x][now] > H or !pre[x][now]){
            f = false; break;
        }
        else if(pos - pre[x][now] < H) // 合法则更新 end 并加入 V
            end = min(end, pre[x][now] - 1), v[pre[x][now]].emplace_back(x);
        else{
            X = x;
            if(pre[x][now] == 1 or check(pre[x][now] - 1, 1))
                end = min(end, pre[x][now] - 1), v[pre[x][now]].emplace_back(x);
            else f = false;
        }
    }
    if(f) ans.emplace_back(pos), k--; // pos 位可以为 &,加到答案中
    if(!k) return;
    if(f and end >= k){
        tem.clear(); for(int x : v[end+1]) tem.emplace_back(x);
        dfs(end, H-(pos-end-1)); //继续递归判断 end 位可否为 &
    }
    else{
        int cnt = k; // pos 位不可以为 &,则最优方案为从 pos-1 到 pos-cnt 全为 &
        for(int i=pos-cnt; i<pos; i++)
            k--, cout<<i<<" ";
        return;
    }
}

signed main(){ // bitop
    Aqrfre(bitop, bitop);

    qr(n); qr(k); K = k;
    for(int i=1; i<=n; i++){
        qr(a[i]);
        for(int j=0; j<62; j++){
            if(a[i] & (1ll << j)) pre[j][i] = la[j], la[j] = i;
            else pre[j][i] = pre[j-ans.size()][i-1]; // 二进制下第 j 位为 1 在第 i 个数之前一次出现的位置
        }
    }

    if(k == n - 1){
        for(int i=1; i<=k; i++) cout<<i<<" ";
        return 0;
    }

    for(int j=0; j<62; j++) //  V 存当前这一层递归的下界包含的 最后一个 1 出现在这个下界的 二进制位
        if(la[j]) zh[j] = la[j], v[zh[j]].emplace_back(j);

    int H = n - 1 - k, endi = 1e9; bool go = false;
    for(int i=0; i<62; i++){ // 把第一次递归剖出来单独做
        if(!zh[i]) continue; 
        if(n - zh[i] > H) continue; 
        if(n - zh[i] < H){
            endi = min(endi, zh[i] - 1);
            continue;
        }
        if(go) continue;
        if(n - zh[i] == H){
            if(check(zh[i] - 1, 0)){ //特殊的:合法直接输出
                for(int i=1; i<=k; i++)
                    cout<<i<<" ";
                return 0;
            }
            go = true;
        }
    }

    for(int x : v[endi+1]) tem.emplace_back(x); //tem 暂存下界这个数的 V

    H -= (n - endi - 1);

    dfs(endi, H);
    sort(ans.begin(), ans.end());
    
    for(int x : ans) cout<<x<<" ";



    return 0;
}