11

I am using np.einsum to multiply probability tables like:

np.einsum('ijk,jklm->ijklm', A, B)

The issue is that I am dealing with more than 26 random variables (axes) overall, so if I assign each random variable a letter I run out of letters. Is there another way I can specify the above operation to avoid this issue, without resorting to a mess of np.sum and np.dot operations?

Alex Riley
  • 169,130
  • 45
  • 262
  • 238
akxlr
  • 1,142
  • 9
  • 23
  • You can stack them probably to form a *fat array*? – Divakar Jun 13 '16 at 15:57
  • @Divakar sorry what do you mean by that? – akxlr Jun 13 '16 at 15:59
  • Nevermind about the previous comment. Do you mean `26` dimensions or variables? If it's just about variables, then it has nothing to do with `einsum`, right? Those `ijk` etc. represent the dimensions, whereas `A` is the variable name. You can always have more variables with names as `AA`, `A2`, etc. – Divakar Jun 13 '16 at 16:03
  • @Divakar by variables I mean random variables in the probability sense. I have a number of joint probability tables, where each table has a variable associated with each axis. I need to multiply the probability tables together to make bigger joint probability tables (i.e. if the tables represent `f(A,B,C)` and `g(B,C,D)` then I want `h(A,B,C,D) = f(A,B,C)g(B,C,D)`) The easiest way to do that I have found is using `einsum`, but it needs letters and I have a lot more than 26 variables. Does that clarify? – akxlr Jun 13 '16 at 16:09
  • There is no connection between the letters in the first argument to einsum and any variables you have -- you could have only one variable in your entire program and 52 letters in that argument if you wanted. It seems like you're confused about what role the letters in the argument are playing. – DSM Jun 13 '16 at 16:14
  • @DSM see above comment, I mean variables in the probability sense because each axis in my tables is associated with a random variable. I am aware that the names have nothing to do with the rest of my code. – akxlr Jun 13 '16 at 16:15
  • @akxlr: it honestly sounds to me like you're using an unusual antipattern-- the only way I can think you'd need 26 separate letters is if you have (roughly) that many separate equations you're entering manually. You can use the two cases -- or numbers -- to get yourself up to 52, but that's it. – DSM Jun 13 '16 at 16:23
  • Worth mentioning that 52 dimensions of size 2 is far larger than any memory storage. – Daniel Jun 13 '16 at 16:26
  • 1
    @DSM I am implementing belief propagation in graphical models which requires sequentially alternating between multiplying probability tables and marginalising. The overall problem can have a lot more than 26 random variables; the hope is that in each step you keep the tables smallish. – akxlr Jun 13 '16 at 16:29
  • @Ophion the issue is my problem has a nice clean mapping from axes to letters, and there are more than 52 axes _overall, in all my tables_ (which is much less storage than `2**52`, because it's not a single table). To avoid doing some sort of weird re-indexing before each call to `einsum` I was wondering if I can just restate the function call in another way. – akxlr Jun 13 '16 at 16:32

3 Answers3

8

The short answer is, you can use any of the 52 letters (upper and lower). That's all the letters in the English language. Any fancier axes names will have to be mapped on those 52, or an equivalent set of numbers. Practically speaking you will want to use a fraction of those 52 in any one einsum call.


@kennytm suggests using the alternative input syntax. A few sample runs suggests that this is not a solution. 26 is still the practical limit (despite the suspicious error messages).

In [258]: np.einsum(np.ones((2,3)),[0,20],np.ones((3,4)),[20,2],[0,2])
Out[258]: 
array([[ 3.,  3.,  3.,  3.],
       [ 3.,  3.,  3.,  3.]])

In [259]: np.einsum(np.ones((2,3)),[0,27],np.ones((3,4)),[27,2],[0,2])
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-259-ea61c9e50d6a> in <module>()
----> 1 np.einsum(np.ones((2,3)),[0,27],np.ones((3,4)),[27,2],[0,2])

ValueError: invalid subscript '|' in einstein sum subscripts string, subscripts must be letters

In [260]: np.einsum(np.ones((2,3)),[0,100],np.ones((3,4)),[100,2],[0,2])
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-260-ebd9b4889388> in <module>()
----> 1 np.einsum(np.ones((2,3)),[0,100],np.ones((3,4)),[100,2],[0,2])

ValueError: subscript is not within the valid range [0, 52]

I'm not entirely sure why you need more than 52 letters (upper and lower case), but I'm sure you need to do some sort of mapping. You don't want to write an einsum string using more than 52 axes all at once. The resulting iterator would be too large (for memory or time).

I'm picturing some sort of mapping function that can be used as:

 astr = foo(A.names, B.names)
 # foo(['i','j','k'],['j','k','l','m'])
 # foo(['a1','a2','a3'],['a2','a3','b4','b5'])
 np.einsum(astr, A, B)

https://github.com/hpaulj/numpy-einsum/blob/master/einsum_py.py

is a Python version of einsum. Crudely speaking einsum parses the subscripts string, creating an op_axes list that can be used in np.nditer to set up the required sum-of-products calculation. With this code I can look at how the translation is done:

From an example in the __name__ block:

    label_str, op_axes = parse_subscripts('ik,kj->ij',    Labels([A.ndim,B.ndim]))
    print op_axes
    # [[0, -1, 1], [-1, 1, 0], [0, 1, -1]]  fine
    # map (4,newaxis,3)(newaxis,3,2)->(4,2,newaxis)
    print sum_of_prod([A,B],op_axes)

Your example, with full diagnostic output is

In [275]:  einsum_py.parse_subscripts('ijk,jklm->ijklm',einsum_py.Labels([3,4])) 
jklm
{'counts': {105: 1, 106: 2, 107: 2, 108: 1, 109: 1}, 
 'strides': [], 
 'num_labels': 5, 
 'min_label': 105, 
 'nop': 2, 
 'ndims': [3, 4], 
 'ndim_broadcast': 0, 
 'shapes': [], 
 'max_label': 109}
[('ijk', [105, 106, 107], 'NONE'), 
 ('jklm', [106, 107, 108, 109], 'NONE')]
 ('ijklm', [105, 106, 107, 108, 109], 'NONE')
iter labels: [105, 106, 107, 108, 109],'ijklm'
op_axes [[0, 1, 2, -1, -1], [-1, 0, 1, 2, 3], [0, 1, 2, 3, 4]]

Out[275]: 
(<einsum_py.Labels at 0xb4f80cac>,
 [[0, 1, 2, -1, -1], [-1, 0, 1, 2, 3], [0, 1, 2, 3, 4]])

Using 'ajk,jkzZ->ajkzZ' changes labels, but results in the same op_axes.


Here is a first draft of a translation function. It should work for any list of lists (of hashable items):

def translate(ll):
    mset=set()
    for i in ll: 
        mset.update(i)
    dd={k:v for v,k in enumerate(mset)}
    x=[''.join([chr(dd[i]+97) for i in l]) for l in ll]
    #  ['cdb', 'dbea', 'cdbea']
    y=','.join(x[:-1])+'->'+x[-1]
    # 'cdb,dbea->cdbea'

In [377]: A=np.ones((3,1,2),int)
In [378]: B=np.ones((1,2,4,3),int)
In [380]: ll=[list(i) for i in ['ijk','jklm','ijklm']]
In [381]: y=translate(ll)
In [382]: y
Out[382]: 'cdb,dbea->cdbea'

In [383]: np.einsum(y,A,B).shape
Out[383]: (3, 1, 2, 4, 3)

The use of set to map index objects means that the final indexing characters are unordered. As long as you specify the RHS that shouldn't be an issue. Also I ignored ellipsis.

=================

The list version of einsum input is converted to the subscript string version in einsum_list_to_subscripts() (in numpy/core/src/multiarray/multiarraymodule.c). It replace ELLIPSIS with '...'. It raised the [0,52] error message if ( s < 0 || s > 2*26) where s is a number in one of those sublists. And converts s to string with

        if (s < 26) {
            subscripts[subindex++] = 'A' + s;
        }
        else {
            subscripts[subindex++] = 'a' + s;

But it looks like the 2nd case is not working; I get errors like for 26.

ValueError: invalid subscript '{' in einstein sum subscripts string, subscripts must be letters

That 'a'+s is wrong if s>26:

In [424]: ''.join([chr(ord('A')+i) for i in range(0,26)])
Out[424]: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'

In [425]: ''.join([chr(ord('a')+i) for i in range(0,26)])
Out[425]: 'abcdefghijklmnopqrstuvwxyz'

In [435]: ''.join([chr(ord('a')+i) for i in range(26,52)])
Out[435]: '{|}~\x7f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94'

That 'a'+s is wrong; is should be:

In [436]: ''.join([chr(ord('a')+i-26) for i in range(26,52)])
Out[436]: 'abcdefghijklmnopqrstuvwxyz'

I submitted https://github.com/numpy/numpy/issues/7741

The existence of this bug after all this time indicates that the sublist format is not common, and that using large numbers in that list is even less frequent.

hpaulj
  • 221,503
  • 14
  • 230
  • 353
4

You could use the einsum(op0, sublist0, op1, sublist1, ..., [sublistout]) form instead of i,j,ik->ijk, which the API is not restricted to 52 axes*. How this verbose form corresponds to the ijk form are shown in the documentation.

OP's

np.einsum('ijk,jklm->ijklm', A, B)

would be written as

np.einsum(A, [0,1,2], B, [1,2,3,4], [0,1,2,3,4])

(* Note: The implementation is still restricted to 26 axes. See @hpaulj's answer and his bug report for explanation)


Equivalences from numpy's examples:

>>> np.einsum('ii', a)
>>> np.einsum(a, [0,0])

>>> np.einsum('ii->i', a)
>>> np.einsum(a, [0,0], [0])

>>> np.einsum('ij,j', a, b)
>>> np.einsum(a, [0,1], b, [1])

>>> np.einsum('ji', c)
>>> np.einsum(c, [1,0])

>>> np.einsum('..., ...', 3, c)
>>> np.einsum(3, [...], c, [...])

>>> np.einsum('i,i', b, b)
>>> np.einsum(b, [0], b, [0])

>>> np.einsum('i,j', np.arange(2)+1, b)
>>> np.einsum(np.arange(2)+1, [0], b, [1])

>>> np.einsum('i...->...', a)
>>> np.einsum(a, [0, ...], [...])

>>> np.einsum('ijk,jil->kl', a, b)
>>> np.einsum(a, [0,1,2], b, [1,0,3], [2,3])
Community
  • 1
  • 1
kennytm
  • 510,854
  • 105
  • 1,084
  • 1,005
  • It raises an error if the index is >52; and due to bug doesn't work right for index >26. – hpaulj Jun 14 '16 at 00:06
  • @hpaulj: Thanks, updated. Frankly speaking einsum should be implemented by transforming the `ijk` form into the more general and structured alternative syntax, not the other way around. – kennytm Jun 14 '16 at 03:58
  • Well it is modeled on the equations that physicists write, and ends up being translated to the `op_axis` list of lists that I show. String arguments were also used in 'indexing_tricks' classes. – hpaulj Jun 14 '16 at 04:36
  • @hpaulj I understand the Einstein notation. I mean numpy should better refactor einsum so that the sublist directly generates the sum-of-product functions, instead of going the indirect "sublist → ijk → parse into sum-of-product" route. – kennytm Jun 14 '16 at 05:56
2

If you are talking about the letters ijk in your example and having more then the available alphabetic characters, then no you can't.

In the einsum numpy code here and here numpy is checking each character one by one with isalpha and there seems to be no way to create names with more than 1 character.

Maybe you can use capital letters, but the main answer to the question is that you cannot have names for the axes with more than 1 character.

Makis Tsantekidis
  • 2,698
  • 23
  • 38