Ускорьте Python / Numpy Code для модели Изинг / Поттс Монте-Карло - PullRequest
0 голосов
/ 16 февраля 2019

Для небольшого побочного проекта я написал двумерное моделирование Монте-Карло модели Изинга / Поттса в Python / Numpy с (упрощенным) кодом ниже.

По сути, код выполняет следующие действия:

  • Устанавливает массив NXM, заполненный случайными целыми числами ( ориентации ) в [1, ориентации]
  • для каждого временного шага ( MCS * 1011)*) каждый пиксель массива посещается один раз в псевдослучайном порядке (поэтому наибольшее простое число при () и index = (a*i + rand) % (N**2) x = index % N y = index // N)
  • проверяются 8 соседних элементов массива (периодические граничные условия) и записьизменяется на одно из соседних значений
  • , если энергия новой конфигурации становится меньше, изменение принимается, в противном случае оно отклоняется, если не выполняется условие

Я пытался ускоритьэто так много, как я мог подумать, но для больших массивов (N, M> 500) код не очень быстрый.Поскольку мне нужно около 10e5 MCS для массива, чтобы увидеть четкую тенденцию, достигнутого

1 loop, best of 3: 276 ms per loop

для 1 MCS в массиве 100x100 на самом деле недостаточно.К сожалению, я не знаю, как повысить производительность, учитывая мой недостаток опыта.

Я предполагаю, что функции Neighbors () и calc_dE () являются узкими местами и особенно вложенными циклами, но я не могу найти способ ускорить его.Попытки моего Cython не были действительно успешными, так как я ничего не делал с Cython ранее, поэтому я открыт для любых предложений.

CODE:

(Команды pyplot предназначены только для визуализации иобычно комментируются.)

import math
import numpy as np
import matplotlib.pyplot as plt

def largest_primes_under(N):
    n = N - 1
    while n >= 2:
        if all(n % d for d in range(2, int(n ** 0.5 + 1))):
            return n
        n -= 1

def Neighbors(Lattice,i,j,n=1):
    ''' Returns an flat array of all neighboring sites in the n-th coordination sphere including the center'''
    N, M = Lattice.shape
    rows = [(i-1) % N, i, (i+1) % N]
    cols = [(j-1) % N, j, (j+1) % M]
    return Lattice[rows][:, cols].flatten()

def calc_dE(Lattice, x, y, z):
    N, M = Lattice.shape
    old_energy = 0
    new_energy = 0
    for i in [0,1,-1]:
        for j in [0,1,-1]:
            if i == 0 and j == 0: 
                continue
            if Lattice[x%N,y%M] == Lattice[(x+i)%N,(y+j)%M]:
                old_energy += 1
            elif z == Lattice[(x+i)%N,(y+j)%M]: 
                new_energy += 1 
    return old_energy-new_energy

N, M = 100,100
orientations = N*M
MCS = int(100)

a = largest_primes_under(N*M)
L = np.random.randint(1,orientations+1,size=(N,M))

mat = plt.matshow(L,cmap = plt.get_cmap('plasma', orientations+1), vmin = -0.5, vmax = orientations+0.5, interpolation='kaiser')
plt.axis('off')

for t in range(1,MCS+1):
    rand = np.random.random_integers(N*M)
    for i in range(0,N**2):
        index = (a*i + rand) % (N**2)
        x = index % N
        y = index // N
        n = Neighbors(L,x,y)
        if len(n)-1 == 0: 
            continue
        else: 
            z = np.random.choice(n)
        dE = calc_dE(L,x,y,z)
        if  (dE < 0): 
            L[x%N,y%N] = z      
        elif np.random.sample() < math.exp(-dE*2.5): 
            L[x%N,y%N] = z

    mat.set_data(L)
    plt.draw()
    plt.pause(0.0001)

1 Ответ

0 голосов
/ 16 февраля 2019

Не уверен, есть ли у вас какие-либо ограничения с точки зрения зависимостей, но я бы определенно рассмотрел Numba .Он предоставляет набор декораторов (в частности, njit), которые скомпилируют ваш код в машинный код и сделают его потенциально намного, гораздо быстрее, если вы используете совместимые типы (например, массивы numpy, но не pandas DataFrames).

Кроме того, не уверен, в каком масштабе вы смотрите, но я вполне могу найти примеры гораздо более оптимизированных алгоритмов простого поиска, чем ручной цикл for.

В противном случае вы всегда можете упасть-вернемся на Cython, но это требует переписывания вашего кода.


EDIT : хорошо, я попробовал numba.

Несколько замечаний:

  1. переместил весь цикл for внутри функции, так что вы можете украсить его с помощью njit
  2. в Neighbors, мне пришлось изменить rows и colsот list с до np.array с, потому что numba не принимает индексирование через списки
  3. Я заменил np.random.random_integers на np.random.randint, так как первый устарел
  4. Я заменил math.exp наnp.exp, что должно дать небольшое повышение производительности (лучшеИдес экономит вам импорт)
import numpy as np
from numba import njit

def largest_primes_under(N):
    n = N - 1
    while n >= 2:
        if all(n % d for d in range(2, int(n ** 0.5 + 1))):
            return n
        n -= 1

@njit
def Neighbors(Lattice,i,j,n=1):
    ''' Returns an flat array of all neighboring sites in the n-th coordination sphere including the center'''
    N, M = Lattice.shape
    rows = np.array([(i-1) % N, i, (i+1) % N])
    cols = np.array([(j-1) % N, j, (j+1) % M])
    return Lattice[rows,:][:,cols].flatten()

@njit
def calc_dE(Lattice, x, y, z):
    N, M = Lattice.shape
    old_energy = 0
    new_energy = 0
    for i in [0,1,-1]:
        for j in [0,1,-1]:
            if i == 0 and j == 0: 
                continue
            if Lattice[x%N,y%M] == Lattice[(x+i)%N,(y+j)%M]:
                old_energy += 1
            elif z == Lattice[(x+i)%N,(y+j)%M]: 
                new_energy += 1 
    return old_energy-new_energy

@njit
def fun(L, MCS, a):
    N, M = L.shape

    for t in range(1,MCS+1):
        rand = np.random.randint(N*M)
        for i in range(0,N**2):
            index = (a*i + rand) % (N**2)
            x = index % N
            y = index // N
            n = Neighbors(L,x,y)
            if len(n)-1 == 0: continue
            else: z = np.random.choice(n)
            dE = calc_dE(L,x,y,z)
            if  (dE < 0): L[x%N,y%N] = z      
            elif np.random.sample() < np.exp(-dE*2.5): L[x%N,y%N] = z    
    return L

Выполнение того же примера

N, M = 100,100
orientations = N*M
MCS = 1

L = np.random.randint(1,orientations+1,size=(N,M))
a = largest_primes_under(N*M)

через %timeit fun(L, MCS, a) (в Jupyter) дает мне

16.9 ms ± 853 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

что в ~ 15 раз быстрее, чем у вас сейчас.Вероятно, вы можете сделать больше оптимизаций, но в numba приятно то, что я получил ускорение в 15 раз, не углубляясь и не меняя при этом, как реализован ваш код.

Пара общих замечаний:

  1. в Neighbors, аргумент / параметр n не используется, поэтому вы должны удалить его для ясности (или обновить код)
  2. в Python, вы обычно хотите использовать нижний регистр дляимена функций и переменных.Прописные буквы обычно зарезервированы для классов (не объектов) и «глобальных» переменных
  3. Ваш комментарий о том, что largest_primes_under вызывается только один раз, определенно уместен, мне следовало бы лучше посмотреть код.

преждевременная оптимизация - корень всего зла

...