Bakeoff Часть 1 Python vs Cython vs Cython Типизированные представления памяти: LDA by Gibbs Sampling

Обновление: Memory Views побеждает. Cython использует типизированные memoryviews: 0,0253449

Особая благодарность lothario, который указал на несколько критических изменений.

Смешной. Конечно, теперь проблема в том , что с ними не так много арифметических действий (суммы и умножения). Исходный пост Вдохновлен Реализация тематической модели с помощью Python (numpy) , что невероятно медленно. Я подумал, что было бы хорошей идеей cythonize это. Однако я мог только понять, как вдвое сократить время с cython. Здесь явно есть операции с массивами, которые не оптимизируются - некоторые мысли и предложения будут приветствоваться. Я всегда хотел поиграть с cython, и это, кажется, хорошая возможность!

для 15 документов по 300 слов в каждом, python: 39.6903322834 cython: 19.2733114806 Cython с использованием типизированных просмотров памяти: 0.547822975

Я специально хочу использовать nogil, так что это может еще больше ускориться: 1) с представлениями памяти, помогает ли добавление nogil в цикл? 2) У меня есть список документов, каждый документ представлен массивом чисел. Какой объект C мне лучше всего использовать? nogil не работает с объектами Python. В настоящее время у меня есть это как список массивов.

Я не фанат C, но буду рад любым дальнейшим предложениям по оптимизации.

Реализация Java от друга, на 1000 документов по 300 слов, 3 секунды.

код lda_pyx Cython

import numpy as np
cimport numpy as np
cimport cython
DTYPE = np.int
ctypedef np.int_t DTYPE_t




cdef class LDA:
    cdef int iteration, M
    cdef int[:] docSizes
    cdef double[:, ::1] n_k_w ,n_m_k
    #cdef
    cdef double[:] n_k
    cdef list k_m_n
    cdef list numbered_docs


    #def __init__(self,int iteration,int M,  np.ndarray[np.double_t, ndim=2] n_k_w ,np.ndarray[np.double_t, ndim=2]  n_m_k, np.ndarray[np.double_t, ndim=1] n_k,np.ndarray[np.int_t, ndim=1] docSizes, list numbered_docs, list k_m_n):
    def __init__(self,int iteration,int M,  double[:, ::1] n_k_w ,double[:, ::1] n_m_k, double[:] n_k, int[:] docSizes, list numbered_docs, list k_m_n):
        self.iteration = iteration
        self.M = M
        self.n_k_w = n_k_w
        self.n_m_k = n_m_k
        self.n_k = n_k
        self.k_m_n = k_m_n
        self.numbered_docs = numbered_docs
        self.docSizes = docSizes


    @cython.boundscheck(False)
    @cython.wraparound(False)
    cdef int _sample(self) :
        #cdef np.ndarray[np.double_t, ndim=2, mode="c"] n_k_w = self.n_k_w
        #cdef np.ndarray[np.double_t, ndim=2, mode="c"]  n_m_k = self.n_m_k
        #cdef np.ndarray[np.double_t, ndim=1, mode="c"] n_k = self.n_k
        cdef double[:, ::1] n_k_w = self.n_k_w
        cdef double[:] n_k = self.n_k
        cdef double[:, ::1]  n_m_k = self.n_m_k
        #cdef np.ndarray[np.int_t, ndim=1, mode="c"] docSizes = self.docSizes
        cdef int[:] docSizes = self.docSizes
        cdef  int m , n, t , k ,new_k
        #cdef np.ndarray[np.int_t, ndim=1, mode="c"] doc
        cdef int[:] doc

        for m in xrange(self.M):
            doc = self.numbered_docs[m]
            for n in xrange(docSizes[m]):

                t = doc[n]

                # discount for n-th word t with topic z
                k = self.k_m_n[m][n]
                #print k

                n_m_k[m,k] -= 1
                n_k_w[k,t] -= 1
                n_k[k] -= 1
                #print "ok"
                # sampling topic new_z for t
                #p_k = n_k_w[:, t] * n_m_k[m][k] / n_k
                new_k = 1
                #np.random.multinomial(1, p_z / p_z.sum()).argmax()

                # set z the new topic and increment counters
                self.k_m_n[m][n] = new_k
                #print n_m_k[m, new_k] ,"after"
                n_m_k[m, new_k] += 1
                #print n_m_k[m, new_k] ,"after"
                n_k_w[new_k][t] += 1
                n_k[new_k] += 1
        #print self.n_k_w ,"before"
        self.n_k_w = n_k_w
        #print self.n_k_w ,"after"
        self.n_m_k = n_m_k
        self.n_k = n_k
        #self.k_m_n = k_m_n

        return 1

    @cython.boundscheck(False)
    @cython.wraparound(False)
    cdef int _iterate(self) :

        while self.iteration >0 :
            self._sample()
            self.iteration -= 1
        return 1

def iterate(iteration, M,  n_k_w , n_m_k, n_k, docSizes, numbered_docs, k_m_n ):
    cdef LDA lda
    lda= LDA(iteration, M,  n_k_w , n_m_k, n_k, docSizes, numbered_docs, k_m_n)
    lda._iterate()
    return lda.n_k_w , lda.n_m_k, lda.n_k , lda.k_m_n

чистая версия Python

def gibbs_sample():
    for i in xrange(iteration):
    #print i
        for m in xrange(M):
            doc = numbered_docs[m]
            for n in xrange(docSizes[m]):
                #print t
                t = doc[n]

                # discount for n-th word t with topic z
                k = k_m_n[m][n]
                n_m_k[m][k] -= 1
                n_k_w[k][t] -= 1
                n_k[k] -= 1

                # sampling topic new_z for t
                #p_k = n_k_w[:, t] * n_m_k[m][k] / n_k
                new_k = 1
                #np.random.multinomial(1, p_z / p_z.sum()).argmax()

                # set z the new topic and increment counters
                k_m_n[m][n] = new_k
                n_m_k[m][new_k] += 1
                n_k_w[new_k][t] += 1
                n_k[new_k] += 1

cProfile

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000    0.419    0.419 <string>:1(<module>)
        1    0.419    0.419    0.419    0.419 {lda_pyx.iterate}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}

person pythOnometrist    schedule 16.04.2013    source источник
comment
Вы смотрели на вывод html cython -a?   -  person JoshAdel    schedule 17.04.2013
comment
Я сделал. В основном выглядит белым. есть ли способ загрузить html с цветами?   -  person pythOnometrist    schedule 17.04.2013
comment
Профиль кода C — groups.google.com/forum/# !msg/cython-users/t61fOeEwltQ/   -  person pythOnometrist    schedule 17.04.2013


Ответы (1)


Относительно ногила:

Использование with nogil просто позволяет другим потокам запускать этот блок кода без глобальной блокировки — вы по-прежнему можете запускать многопоточный код в этом блоке и следить за тем, чтобы вы не при этом не касайтесь каких-либо объектов Python. Типизированные представления памяти не являются объектами Python, поэтому вы можете использовать/манипулировать ими в блоке nogil с несколькими потоками. Cython имеет функцию prange(), которая автоматически генерирует директивы OpenMP внутри блока with nogil. Вы можете легко получить хорошее ускорение с помощью prange, если ваши итерации цикла независимы друг от друга. Здесь есть много деталей - пожалуйста, смотрите связанную документацию.

Что касается вашего кода:

Сосредоточьтесь на оптимизации кода во внутреннем цикле.

Использование cython -a в вашем коде показывает, что несколько строк, вероятно, снижают вашу производительность.

  • Вы можете напрямую индексировать n_k_w[new_k,t], а не то, что у вас есть.

  • Вы получите улучшение, преобразовав список k_m_n в двумерный массив numpy и используя для этого типизированное представление памяти.

  • То же самое для numbered_docs.

  • Вам также необходимо использовать типизированные arr[::1] объявления представления памяти всякий раз, когда вы знаете, что у вас есть непрерывные данные, иначе Cython обрабатывает представление памяти как последовательное, что замедлит доступ.

См. приведенный ниже код cython для некоторых предложений — возможно, вам придется подправить его, чтобы заставить его работать для ваших вещей.

lda.pyx

import numpy as np
cimport numpy as np
cimport cython
DTYPE = np.int
ctypedef np.int_t DTYPE_t


cdef class LDA:

    cdef:
        int iteration, M
        int[::1] docSizes
        double[:, ::1] n_k_w ,n_m_k
        double[::1] n_k
        list k_m_n, numbered_docs

    def __init__(self, iteration, M, n_k_w , n_m_k, n_k, docSizes, numbered_docs, k_m_n):
        self.iteration = iteration
        self.M = M
        self.n_k_w = n_k_w
        self.n_m_k = n_m_k
        self.n_k = n_k
        self.k_m_n = k_m_n
        self.numbered_docs = numbered_docs
        self.docSizes = docSizes

    @cython.boundscheck(False)
    @cython.wraparound(False)
    cdef int _sample(self) :

        cdef:
            int[::1] docSizes = self.docSizes
            double[:, ::1] n_k_w = self.n_k_w, n_m_k = self.n_m_k
            double[::1] n_k = self.n_k
            int[::1] k_n, doc
            int m, n, t, k, new_k

        for m in range(self.M):
            k_n = self.k_m_n[m]
            doc = self.numbered_docs[m]
            for n in range(docSizes[m]):

                t = doc[n]
                k = k_n[n]

                n_m_k[m,k] -= 1
                n_k_w[k,t] -= 1
                n_k[k] -= 1
                new_k = 1

                # set z the new topic and increment counters
                k_n[n] = new_k
                n_m_k[m, new_k] += 1
                n_k_w[new_k, t] += 1
                n_k[new_k] += 1

        return 1

    @cython.boundscheck(False)
    @cython.wraparound(False)
    cdef int _iterate(self) :

        while self.iteration >0 :
            self._sample()
            self.iteration -= 1
        return 1

def iterate(iteration, M,  n_k_w , n_m_k, n_k, docSizes, numbered_docs, k_m_n ):

    # pass array / list arguments through np.ascontiguousarray(), will make
    # copy only if not contiguous buffer already.
    ascontig = np.ascontiguousarray
    n_k_w = ascontig(n_k_w, dtype=np.double)
    n_m_k = ascontig(n_m_k, dtype=np.double)
    n_k = ascontig(n_k, dtype=np.double)
    docSizes = ascontig(docSizes, dtype=np.int32)
    k_m_n = [ascontig(k_n, dtype=np.int32) for k_n in k_m_n]
    numbered_docs = [ascontig(n_d, dtype=np.int32) for n_d in numbered_docs]

    cdef LDA lda
    lda= LDA(iteration, M,  n_k_w , n_m_k, n_k, docSizes, numbered_docs, k_m_n)
    lda._iterate()
    # since the lda object just grabs views of the n_k_w, n_m_k etc. arrays,
    # these will be modified, so return them directly.
    return n_k_w, n_m_k, n_k, k_m_n

setup.py

import numpy as np
from distutils.core import setup
from distutils.extension import Extension
from Cython.Distutils import build_ext

exts = [Extension("lda", ["lda.pyx"],
                  include_dirs=[np.get_include()])
        ]

setup(
    cmdclass = {'build_ext': build_ext},
    ext_modules = exts,
)

test.py:

import numpy as np
from speedup import iterate

iteration = 10
M = 10
n_k_w = np.random.rand(10, 10)
n_m_k = np.random.rand(10, 10)
n_k = np.random.rand(10)
docSizes = np.zeros((10,), dtype=np.int32) + 10
numbered_docs = np.zeros((10, 10), dtype=np.int32) + 3
k_m_n = np.zeros((10, 10), dtype=np.int32) + 7
k_m_n_orig = k_m_n.copy()

iterate(iteration, M,  n_k_w, n_m_k, n_k, docSizes, numbered_docs, k_m_n)

print k_m_n_orig[1]
print k_m_n[1]
person lothario    schedule 17.04.2013
comment
Привет, Лотарио, спасибо за предложения, результаты будут обновлены сегодня днем. Одна проблема заключается в том, что numbered_docs представляет собой список массивов. Каждый массив отличается по длине, знаете ли вы, могут ли типизированные массивы meview или даже массивы numpy справиться с этим? Я могу сделать массив массивов, но это не то же самое, что массив 2 d. - person pythOnometrist; 17.04.2013
comment
То же самое и с кмн. Потенциально я могу просто объединить и получить один массив, но тогда мне нужно быть более осторожным с циклом, чтобы контролировать начало и остановку каждого документа. - person pythOnometrist; 17.04.2013
comment
# поскольку объект lda просто захватывает представления массивов n_k_w, n_m_k и т. д., # они будут изменены, поэтому возвращайте их напрямую. Я использовал себя. переменные для отслеживания изменений по мере выполнения алгоритма. Я вижу, вы удалили эти шаги назначения. Создает ли использование ascontig глобальные переменные? - person pythOnometrist; 17.04.2013
comment
Ясно, если numbered_docs и kmn будут списками массивов разной длины, то это изменит ситуацию. Я исправлю опубликованный код. Было бы очень полезно, если бы вы могли создать полный рабочий пример с поддельными данными и небольшими размерами массива. - person lothario; 17.04.2013
comment
Представления памяти — это представления — они совместно используют базовые данные с тем, что просматривают. При назначении памяти представление завершится ошибкой, если по какой-либо причине оно не может совместно использовать базовые данные. Поэтому, если вы измените содержимое представления, то, что оно просматривает, тоже изменится. ascontig создаст новые непрерывные массивы, если это необходимо. Вызов lda._iterate() изменит эти массивы на месте, поэтому копирование/назначение не требуется. - person lothario; 17.04.2013
comment
Как и обещал. bitbucket.org/pythonOmetrist/cython-lda/overview обратите внимание, что ускорение невероятное , но в первую очередь из-за преобразования списка массивов в один массив и небольшой перезаписи цикла. Теперь возникает настоящая проблема: как я могу выполнять арифметические действия с памятью? Я открою отдельный вопрос SO. - person pythOnometrist; 17.04.2013