web-dev-qa-db-fra.com

Comment comprendre les foulées engourdies pour le profane?

Je passe actuellement par numpy et il y a un sujet dans numpy appelé "strides". Je comprends ce que c'est. Mais comment ça marche? Je n'ai trouvé aucune information utile en ligne. Quelqu'un peut-il me laisser comprendre en termes simples?

12
Atif

Les données réelles d'un tableau numpy sont stockées dans un bloc de mémoire homogène et contigu appelé tampon de données. Pour plus d'informations, voir NumPy internals . En utilisant l'ordre (par défaut) row-major , un tableau 2D ressemble à ceci:

enter image description here

Pour mapper les indices i, j, k, ... d'un tableau multidimensionnel aux positions dans le tampon de données (l'offset, en octets), NumPy utilise la notion de foulées . Les enjambées sont le nombre d'octets à sauter dans la mémoire pour passer d'un élément à l'élément suivant le long de chaque direction/dimension du tableau . En d'autres termes, c'est la séparation d'octets entre les éléments consécutifs pour chaque dimension.

Par exemple:

>>> a = np.arange(1,10).reshape(3,3)
>>> a
array([[1, 2, 3],
       [4, 5, 6],
       [7, 8, 9]])

Ce tableau 2D a deux directions, axes s'étendant verticalement vers le bas sur les lignes) et axe-1 (s'étendant horizontalement sur les colonnes), chaque élément ayant une taille:

>>> a.itemsize  # in bytes
4  

Donc, pour aller de a[0, 0] -> a[0, 1] (se déplaçant horizontalement le long de la 0e ligne, de la 0e colonne à la 1re colonne), le pas d'octet dans le tampon de données est 4. Idem pour a[0, 1] -> a[0, 2], a[1, 0] -> a[1, 1] etc. Cela signifie que le nombre de pas pour la direction horizontale (axe-1) est de 4 octets.

Cependant, pour passer de a[0, 0] -> a[1, 0] (se déplaçant verticalement le long de la 0ème colonne, de la 0ème ligne à la 1ère ligne), vous devez d'abord parcourir tous les éléments restants sur la 0ème ligne pour accéder à la 1ère ligne, puis parcourir la 1ère ligne pour accéder à l'article a[1, 0], c'est à dire. a[0, 0] -> a[0, 1] -> a[0, 2] -> a[1, 0]. Par conséquent, le nombre de pas pour la direction verticale (axe-0) est de 3 * 4 = 12 octets. Notez que passer de a[0, 2] -> a[1, 0], et en général du dernier élément de la i-ème ligne au premier élément de la (i + 1) -ème ligne, est également de 4 octets car le tableau a est stocké dans la ligne- ordre majeur.

C'est pourquoi

>>> a.strides  # (strides[0], strides[1])
(12, 4)  

Voici un autre exemple montrant que les foulées dans le sens horizontal (axe-1), strides[1], d'un tableau 2D n'est pas nécessairement égal à la taille de l'élément (par exemple, un tableau avec un ordre de colonne majeur):

>>> b = np.array([[1, 4, 7],
                  [2, 5, 8],
                  [3, 6, 9]]).T
>>> b
array([[1, 2, 3],
       [4, 5, 6],
       [7, 8, 9]])

>>> b.strides
(4, 12)

Ici strides[1] est un multiple de la taille de l'élément. Bien que le tableau b soit identique au tableau a, il s'agit d'un tableau différent: en interne b est stocké en tant que |1|4|7|2|5|8|3|6|9| (car la transposition n'affecte pas le tampon de données, mais échange uniquement les foulées et la forme), tandis que a as |1|2|3|4|5|6|7|8|9|. Ce qui les rend semblables, ce sont les différentes étapes. Autrement dit, l'étape d'octet pour b[0, 0] -> b[0, 1] est 3 * 4 = 12 octets et pour b[0, 0] -> b[1, 0] est de 4 octets, tandis que pour a[0, 0] -> a[0, 1] est de 4 octets et pour a[0, 0] -> a[1, 0] est de 12 octets.

Enfin et surtout, NumPy permet de créer des vues de tableaux existants avec la possibilité de modifier les enjambées et la forme, voir --- (astuces de foulées . Par exemple:

>>> np.lib.stride_tricks.as_strided(a, shape=a.shape[::-1], strides=a.strides[::-1])
array([[1, 4, 7],
       [2, 5, 8],
       [3, 6, 9]])

ce qui revient à transposer le tableau a.

Permettez-moi d'ajouter, mais sans entrer dans les détails, que l'on peut même définir des foulées qui ne sont pas des multiples de la taille de l'article. Voici un exemple:

>>> a = np.lib.stride_tricks.as_strided(np.array([1, 512, 0, 3], dtype=np.int16), 
                                        shape=(3,), strides=(3,))
>>> a
array([1, 2, 3], dtype=int16)

>>> a.strides[0]
3

>>> a.itemsize
2
25
AndyK

Juste pour ajouter à la bonne réponse de @AndyK, j'ai appris les foulées numpy de Numpy MedKit . Là, ils montrent l'utilisation avec un problème comme suit:

Entrée donnée :

x = np.arange(20).reshape([4, 5])
>>> x
array([[ 0,  1,  2,  3,  4],
       [ 5,  6,  7,  8,  9],
       [10, 11, 12, 13, 14],
       [15, 16, 17, 18, 19]])

Sortie attendue :

array([[[  0,  1,  2,  3,  4],
        [  5,  6,  7,  8,  9]],

       [[  5,  6,  7,  8,  9],
        [ 10, 11, 12, 13, 14]],

       [[ 10, 11, 12, 13, 14],
        [ 15, 16, 17, 18, 19]]])

Pour ce faire, nous devons connaître les termes suivants:

shape - Les dimensions du tableau le long de chaque axe.

strides - Le nombre d'octets de mémoire qui doivent être ignorés pour passer à l'élément suivant le long d'une certaine dimension.

>>> x.strides
(20, 4)

>>> np.int32().itemsize
4

Maintenant, si nous regardons le Sortie attendue:

array([[[  0,  1,  2,  3,  4],
        [  5,  6,  7,  8,  9]],

       [[  5,  6,  7,  8,  9],
        [ 10, 11, 12, 13, 14]],

       [[ 10, 11, 12, 13, 14],
        [ 15, 16, 17, 18, 19]]])

Nous devons manipuler la forme et les enjambées du tableau. La forme de sortie doit être (3, 2, 5), c'est-à-dire 3 éléments, chacun contenant deux lignes (m == 2) et chaque ligne ayant 5 éléments.

Les pas doivent passer de (20, 4) à (20, 20, 4). Chaque élément du nouveau tableau de sortie commence à une nouvelle ligne, chaque ligne étant composée de 20 octets (5 éléments de 4 octets chacun), et chaque élément occupe 4 octets (int32).

Donc:

>>> from numpy.lib import stride_tricks
>>> stride_tricks.as_strided(x, shape=(3, 2, 5),
                                strides=(20, 20, 4))
...
array([[[  0,  1,  2,  3,  4],
        [  5,  6,  7,  8,  9]],

       [[  5,  6,  7,  8,  9],
        [ 10, 11, 12, 13, 14]],

       [[ 10, 11, 12, 13, 14],
        [ 15, 16, 17, 18, 19]]])

Une alternative serait:

>>> d = dict(x.__array_interface__)
>>> d['shape'] = (3, 2, 5)
>>> s['strides'] = (20, 20, 4)

>>> class Arr:
...     __array_interface__ = d
...     base = x

>>> np.array(Arr())
array([[[  0,  1,  2,  3,  4],
        [  5,  6,  7,  8,  9]],

       [[  5,  6,  7,  8,  9],
        [ 10, 11, 12, 13, 14]],

       [[ 10, 11, 12, 13, 14],
        [ 15, 16, 17, 18, 19]]])

J'utilise cette méthode très souvent au lieu de numpy.hstack ou numpy.vstack et croyez-moi, le calcul est beaucoup plus rapide.

Remarque:

Lorsque vous utilisez de très grands tableaux avec cette astuce, le calcul exact strides n'est pas si simple. Je fais habituellement un numpy.zeroes tableau de la forme souhaitée et obtenir les foulées en utilisant array.strides et utilisez-le dans la fonction stride_tricks.as_strided.

J'espère que cela aide!

6
Rick M.