Cython memoryview transpose: Typeerror

2019-08-11 07:39发布

I'm trying to develop a small Convolutional Neural Network framework with python. The code for the convolutional node already works (slowly) and I would like to speed it up. The hotspots are the loops where the convolutional filter is moved across the image. I chose to use cython to speed up those loops.

The obvious small annotations, cdef for all local variables and removing boundscheck, shaved hardly 10% off of my runtime. That seemed strange to me, based on what I read online, cython should already be able to do its magic.

Unfortunately the code is inside a class and relies heavily on the properties of that class. I decided to convert it into a cdef class. This means that all class attributes have to be declared with cdef. Apparently cython doesn't support numpy arrays, so I declared all numpy arrays as double[:,:,...]

So far the code worked fine, all unittests passing. Now the compilation to .pyd (I'm working under windows) still works. But running the code creates a Typeerror:

TypeError: only length-1 arrays can be converted to Python scalars

Here is some code. This is the entire forward method of my convolutional node, which might be too much and not easily readable. You probably only need the very last line. That's were the error happens:

    @cython.boundscheck(False)
    @cython.nonecheck(False)
    def forward(self):

        # im2col: x -> in_cols
        # padding
        cdef np.ndarray[DTYPE_t, ndim=4] x_padded = np.zeros((self.batch_size, self.in_colors, self.in_width + self.padding*2, self.in_height + self.padding*2))
        if self.padding>0:
            x_padded[:, :, self.padding:self.in_width+self.padding, self.padding:self.in_height+self.padding] = self.x
        else:
            x_padded[:]=self.x

        # allocating new field
        cdef np.ndarray[DTYPE_t, ndim=4] rec_fields = np.empty((self.filter_size**2* self.in_colors, self.batch_size, self.out_width, self.out_height))

        # copying receptive fields
        cdef int w,h
        for w, h in np.ndindex((self.out_width, self.out_height)):
            rec_fields[:, :, w, h] = x_padded[:, :, w*self.stride:w*self.stride + self.filter_size, h*self.stride:h*self.stride + self.filter_size] \
                .reshape((self.batch_size, self.filter_size**2* self.in_colors)) \
                .T

        self.in_cols = rec_fields.reshape((self.filter_size**2 * self.in_colors, self.batch_size * self.out_width * self.out_height))

        # linear node: in_cols -> out_cols
        cdef np.ndarray[DTYPE_t, ndim=2] out_cols=np.dot(self.W,self.in_cols)+self.b

        # col2im: out_cols -> out_image -> y
        cdef np.ndarray[DTYPE_t, ndim=4] out_image = out_cols.reshape((self.out_colors, self.batch_size, self.out_width, self.out_height))
        self.y[:] = out_image.transpose(1, 0, 2, 3)

This last call to transpose is marked in the exception. I can't explain this. Do memoryviews behave differently when transposed ?

UPDATE:

I'm sure that the dimensions are defined correctly. If there is a dimension mismatch, it produces a different runtime error. Can't check right now, but it was something like "got 4-dim, expected 2-dim". I've got to say that I'm extremely impressed by the type system of cython. This kind of runtime type information in a python exception is rather useful. Sadly it doesn't explain why the transpose above fails.

UPDATE:

There's some complication with the arrays: They must not be overwritten, only be used as references.

A little difficult to explain: At the core of the neural network is a loop which calls the method forward() on all nodes in the network consecutively.

for node in self.nodes:
    node.forward()

In this method the node looks at its input data, makes some computations and writes to its output. It relies on the fact that the input already contains the correct data.

For the setup of my network I store the nodes in the right order. And I connect them manually.

node2.x=node1.y

Now if I write

self.y[:]= data

in the forward method of node1, node2 automatically has the correct input. This requires careful programming: the forward methods must be called in the right order and the output must never be overwritten, only written to.

The alternative would be a huge structure where I store the output of each node and pass this data around. That would create lots of boilerplate code and mess up the forward and backward pass.

UPDATE:

the last few lines in forward now look like this:

cdef np.ndarray[DTYPE_t, ndim=4] out_image = out_cols.reshape((self.out_colors, self.batch_size, self.out_width, self.out_height))
        cdef double[:,:,:,:] temp
        temp=out_image.transpose(1,0,2,3)
        self.y[...] = temp

The assignment to temp fails with the same TypeError message.

1条回答
闹够了就滚
2楼-- · 2019-08-11 08:16
self.y[...] = some_array
# or equivalently self.y[:,:,:,:] = some_array

does a copy of some_array into self.y, which must already be initialised to the right size. It also only seems to work if some_array is already a memoryview (which doesn't hugely make sense to me, but this seems to be the case).

(self.y[:] = some_array only works for 1D arrays)

If you just want make self.y "look at" a numpy array you just want to do

self.y = some_array
# in your case:
# self.y = out_image.transpose(1, 0, 2, 3) 

The chances are that the this is fine for your purposes!


If you're particularly keen on making a copy (possibly if you've taken a C pointer to self.y or something like that) then you have to force some_array to be a memoryview. You'd do something like

cdef double[:,:,:,:] temporary_view_of_transpose

# temporary_view_of_transpose now "looks at" the memory allocated by transpose
# no square brackets!
temporary_view_of_transpose = out_image.transpose(1, 0, 2, 3)

# data is copied from temporary_view_of_transpose to self.y
self.y[...] = temporary_view_of_transpose # (remembering that self.y must be the correct shape before this assignment).

I agree the error message seen is unhelpful!


Edit: The following is minimum complete example that works for me (Cython 0.24, Python 3.5.1, Linux - I can't easily test on Anaconda) At this stage I'm not clear what's different in your code.

# memview.pyx
cimport numpy as np
import numpy as np

cdef class MemviewClass:
    cdef double[:,:,:,:] y

    def __init__(self):
        self.y = np.zeros((2,3,4,5))

    def do_something(self):
        cdef np.ndarray[np.float64_t,ndim=4] out_image = np.ones((3,2,4,5))
        cdef double[:,:,:,:] temp
        temp = out_image.transpose(1,0,2,3)
        self.y[...] = temp

    def print_y(self):
        # just to check it gets changed
        print(np.asarray(self.y))

and test_script.py to show it works:

# use pyximport for ease of testing
import numpy
import pyximport; pyximport.install(setup_args=dict(include_dirs=numpy.get_include()))

import memview

a = memview.MemviewClass()
a.print_y() # prints a big array of 0s
a.do_something()
a.print_y() # prints a big array of 1s
查看更多
登录 后发表回答