我应该如何有选择地对数组的多个轴求和?

How should I selectively sum multiple axes of an array?

在 J 中有选择地对数组的多个轴求和的首选方法是什么?

例如,假设 a 是以下第 3 阶数组:

   ]a =: i. 2 3 4
 0  1  2  3
 4  5  6  7
 8  9 10 11

12 13 14 15
16 17 18 19
20 21 22 23

我的目标是定义一个 dyad "sumAxes" 来对我选择的多个轴求和:

   0 1 sumAxes a      NB. 0+4+8+12+16+20 ...
60 66 72 78

   0 2 sumAxes a      NB. 0+1+2+3+12+13+14+15 ...
60 92 124

   1 2 sumAxes a      NB. 0+1+2+3+4+5+6+7+8+9+10+11 ...
66 210

我目前尝试实现这个动词的方法是使用二元组 |: 首先排列 a 的轴,然后使用 [= 拼凑必要等级的项目17=](其中 n 是我要求和的数字轴)在对结果项求和之前:

   sumAxes =: dyad : '(+/ @ ,"(#x)) x |: y'

这似乎如我所愿,但作为 J 的初学者,我不确定我是否忽略了等级或特定动词的某些方面,这些方面可以实现更清晰的定义。更一般地说,我想知道排列轴、拼凑和求和在这种语言中是惯用的还是有效的。

就上下文而言,我以前数组编程的大部分经验都是使用 Python 的 NumPy 库。

NumPy 没有 J 的等级概念,而是希望用户明确标记数组的轴以减少超过:

>>> import numpy
>>> a = numpy.arange(2*3*4).reshape(2, 3, 4) # a =: i. 2 3 4
>>> a.sum(axis=(0, 2))                       # sum over specified axes
array([ 60,  92, 124])

作为脚注,我当前的 sumAxes 实现与仅指定单个轴的 NumPy 相比有工作 "incorrectly" 的缺点(因为等级与 "axis" 不可互换) ).

动机

J 在处理任意排序的数组方面拥有令人难以置信的便利。但是语言的一个方面几乎同时普遍有用和合理,但也与这种与维度无关的性质有些对立。

主轴(实际上是一般的引导轴)具有隐式特权。这是基础的概念,例如# 项目数 (即第一个轴的维度),+/ 的低调优雅和通用性,无需进一步修改,以及许多其他语言的美丽部分。

但这也是您在尝试解决此问题时遇到障碍的原因。

标准方法

所以解决问题的一般方法就像您拥有的那样:转置或以其他方式重新排列数据因此您感兴趣的轴成为引导轴。您的方法是经典且无懈可击的。你可以凭良心使用它。

替代方法

但是,像你一样,让我有点烦恼的是我们在类似的情况下被迫跳过这样的箍。一个线索表明我们正在违背语言的原则,即连词 "(#x) 的动态参数;通常连词的参数是固定的,并且在 运行 时计算它们通常会迫使我们使用显式代码(如您的示例)或 。当语言使某事变得困难时,这通常是您违背本意的迹象。

另一种是拼音(,)。这不仅仅是我们要转置一些轴;这是我们想要关注 一个特定的 轴,然后 运行 将它尾随成一个平面向量的所有元素。尽管我实际上认为这更多地反映了我们如何构建问题所施加的约束,而不是符号中的约束。更多内容在本 post.

的最后部分

有了这个,我们可能会觉得 直接处理非引导轴 的愿望是合理的。而且,J 在这里和那里提供了一些原语,使我们能够做到这一点,这可能暗示该语言的设计者也觉得有必要在引导轴的首要地位上包含某些例外情况。

介绍性示例

例如,二元运算 |. (rotate) 具有秩 1 _,即它在左侧取一个向量。

对于使用它多年的人来说,这有时会让他们感到惊讶,他们从来没有在左边传递超过一个标量。这与未绑定的右秩一起,是 J 的引导轴偏差的另一个微妙结果:我们将右参数视为 项目向量 ,而左参数是一个简单的,该向量的标量旋转值

因此:

   3 |. 1 2 3 4 5 6
4 5 6 1 2 3

   1 |. 1 2 , 3 4 ,: 5 6
3 4
5 6
1 2

但在后一种情况下,如果我们不想将 table 视为 行向量 ,而是 列向量?

当然,经典的方法是使用等级,显式表示我们感兴趣的轴(因为离开它隐式 始终选择引导轴):

   1 |."1 ] 1 2 , 3 4 ,: 5 6
2 1
4 3
6 5

现在,这完全是惯用的、标准的,并且在 J 代码中无处不在:J 鼓励我们从等级的角度思考。没有人会在阅读此代码时眨眼。

但是,正如一开始所描述的,从另一种意义上说,它感觉像是逃避或手动调整。特别是当我们要在运行时动态选择rank。在符号上,我们现在 不再将数组作为一个整体进行寻址,而是对每一行进行寻址 .

这就是 |. 的左秩出现的地方:它是 can address non-leading axes directly.

的少数基元之一
   0 1 |. 1 2 , 3 4 ,: 5 6
2 1
4 3
6 5

看马,没有排名!当然,我们现在必须为每个轴单独指定一个旋转值,但这不仅可以,而且有用,因为现在那个留下了争论闻起来更像是可以根据输入计算的东西,本着真正的 J 精神。

直接求和非引导轴

所以,既然我们知道 J 可以让我们在某些情况下处理非引导轴,我们只需 to survey those cases 并确定一个似乎适合我们这里目的的轴。

我发现最常用于非引导轴工作的原语是 ;.boxed 左手参数。所以我的直觉是先达到那个。

让我们从您的示例开始,稍微修改一下以了解我们总结的内容。

    ]a =: i. 2 3 4
    sumAxes =: dyad : '(< @ ,"(#x)) x |: y'

     0 1 sumAxes a
+--------------+--------------+---------------+---------------+
|0 4 8 12 16 20|1 5 9 13 17 21|2 6 10 14 18 22|3 7 11 15 19 23|
+--------------+--------------+---------------+---------------+ 
     0 2 sumAxes a
+-------------------+-------------------+---------------------+
|0 1 2 3 12 13 14 15|4 5 6 7 16 17 18 19|8 9 10 11 20 21 22 23|
+-------------------+-------------------+---------------------+
    1 2 sumAxes a
+-------------------------+-----------------------------------+
|0 1 2 3 4 5 6 7 8 9 10 11|12 13 14 15 16 17 18 19 20 21 22 23|
+-------------------------+-----------------------------------+

the definition of for dyads derived from ;.1和朋友的相关部分是:

The frets in the dyadic cases 1, _1, 2 , and _2 are determined by the 1s in boolean vector x; an empty vector x and non-zero #y indicates the entire of y. If x is the atom 0 or 1 it is treated as (#y)#x. In general, boolean vector >j{x specifies how axis j is to be cut, with an atom treated as (j{$y)#>j{x.

这意味着:如果我们只是试图在没有内部分割的情况下沿其维度对数组进行切片,我们可以简单地使用 dyad cut 和仅由 [=28= 组成的左参数]s 和 a:s。向量中 1 的数量(即总和)决定了结果数组的排名。

因此,要重现上面的示例:

     ('';'';1) <@:,;.1 a
+--------------+--------------+---------------+---------------+
|0 4 8 12 16 20|1 5 9 13 17 21|2 6 10 14 18 22|3 7 11 15 19 23|
+--------------+--------------+---------------+---------------+
     ('';1;'') <@:,;.1 a
+-------------------+-------------------+---------------------+
|0 1 2 3 12 13 14 15|4 5 6 7 16 17 18 19|8 9 10 11 20 21 22 23|
+-------------------+-------------------+---------------------+
     (1;'';'') <@:,;.1 a
+-------------------------+-----------------------------------+
|0 1 2 3 4 5 6 7 8 9 10 11|12 13 14 15 16 17 18 19 20 21 22 23|
+-------------------------+-----------------------------------+

瞧瞧。另外,注意到左手参数中的模式了吗?这两个 A 正好位于您最初调用 sumAxe 的索引处。明白我的意思吗,为每个维度提供一个值,这听起来像是一件好事,本着 J 精神?

因此,要使用此方法提供具有相同界面的 sumAxe 模拟:

   sax =: dyad : 'y +/@:,;.1~ (1;a:#~r-1) |.~ - {. x -.~ i. r=.#$y'     NB. Explicit
   sax =: ]  +/@:,;.1~  ( (] (-@{.@] |. 1 ; a: #~ <:@[) (-.~ i.) ) #@$) NB. Tacit

为简洁起见省略了结果,但它们与您的 sumAxe.

相同

最后的考虑

我还想指出一件事。您的 sumAxe 调用的界面,从 Python 计算,命名您想要的两个轴 "run together"。这绝对是一种看待它的方式。

另一种看待它的方法是命名你想要求和的轴,它借鉴了我在这里提到的 J 哲学。事实上,这是我们真正关注的焦点,这一事实得到了我们对每个 "slice" 的理解这一事实的证实,因为我们 不关心它的形状 ,只关心它的值。

这种改变视角来谈论您感兴趣的事情的好处是它始终是一个单一事物,并且这种独特性允许我们的代码进行某些简化(同样,尤其是在 J 中,我们通常谈论 [新的,即 post-transpose] 引导轴)¹.

让我们再看看 ;. 的 ones-and-aces 向量参数,以说明我的意思:

     ('';'';1) <@:,;.1 a
     ('';1;'') <@:,;.1 a
     (1;'';'') <@:,;.1 a

现在将三个括号内的参数视为一个三行矩阵。什么对你来说很突出?对我来说,它是沿着反对角线的那些。它们数量较少,并且具有价值;相比之下,ace 形成矩阵的 "background"(零)。那些都是真实的内容。

这与我们的 sumAxe 界面现在的情况截然不同:它要求我们指定 A(零)。我们指定 1 怎么样,即 我们真正感兴趣的轴?

如果我们这样做,我们可以这样重写我们的函数:

  xas  =: dyad : 'y +/@:,;.1~ (-x) |. 1 ; a: #~ _1 + #$y'  NB. Explicit
  xas  =: ]  +/@:,;.1~  -@[ |. 1 ; a: #~ <:@#@$@]          NB. Tacit

而不是调用 0 1 sax a,你会调用 2 xas a,而不是 0 2 sax a,你会调用 1 xas a,等等

这两个动词相对简单表明 J 同意这种焦点倒转。


¹ 在这段代码中,我假设你总是想折叠除 1 以外的所有轴。这个假设是在我用来生成 ones-and-aces 向量的方法中编码的,使用 |..

然而,你的脚注 sumAxes 与仅指定单个轴的 NumPy 相比工作 "incorrectly" 的缺点 建议有时您只想折叠一个轴。

这是完全可能的,;. 方法可以采用任意(原位)切片;我们只需要改变我们指示它的方法(生成 1s 和 aces 向量)。如果您提供几个您喜欢的概括示例,我将在此处更新 post。可能只是使用 (<1) x} a: #~ #$y((1;'') {~ (e.~ i.@#@$)) 而不是 (-x) |. 1 ; a:#~<:#$y.

的问题