Как быстро выполнить наименьшие квадраты для нескольких наборов данных? - PullRequest
11 голосов
/ 09 января 2012

Я пытаюсь подогнать гауссово по многим точкам данных.Например, у меня есть массив данных размером 256 x 262144.Там, где 256 точек должны быть согласованы с гауссовым распределением, а мне нужно 262144 из них.

Иногда пик гауссовского распределения находится вне диапазона данных, поэтому для получения точного усреднения кривой результата среднего значенияэто лучший подход.Даже если пик находится внутри диапазона, аппроксимация кривой дает лучшую сигма, потому что другие данные не находятся в диапазоне.

У меня это работает для одной точки данных, используя код из http://www.scipy.org/Cookbook/FittingData.

Я пытался просто повторить этот алгоритм, но похоже, что для решения этой проблемы потребуется что-то порядка 43 минут.Есть ли уже написанный быстрый способ сделать это параллельно или более эффективно?

from scipy import optimize                                                                                                                                          
from numpy import *                                                                                                                                                 
import numpy                                                                                                                                                        
# Fitting code taken from: http://www.scipy.org/Cookbook/FittingData                                                                                                

class Parameter:                                                                                                                                                    
    def __init__(self, value):                                                                                                                                  
            self.value = value                                                                                                                                  

    def set(self, value):                                                                                                                                       
            self.value = value                                                                                                                                  

    def __call__(self):                                                                                                                                         
            return self.value                                                                                                                                   


def fit(function, parameters, y, x = None):                                                                                                                         
    def f(params):                                                                                                                                              
            i = 0                                                                                                                                               
            for p in parameters:                                                                                                                                
                    p.set(params[i])                                                                                                                            
                    i += 1                                                                                                                                      
            return y - function(x)                                                                                                                              

    if x is None: x = arange(y.shape[0])                                                                                                                        
    p = [param() for param in parameters]                                                                                                                       
    optimize.leastsq(f, p)                                                                                                                                      


def nd_fit(function, parameters, y, x = None, axis=0):                                                                                                              
    """                                                                                                                                                         
    Tries to an n-dimensional array to the data as though each point is a new dataset valid across the appropriate axis.                                        
    """                                                                                                                                                         
    y = y.swapaxes(0, axis)                                                                                                                                     
    shape = y.shape                                                                                                                                             
    axis_of_interest_len = shape[0]                                                                                                                             
    prod = numpy.array(shape[1:]).prod()                                                                                                                        
    y = y.reshape(axis_of_interest_len, prod)                                                                                                                   

    params = numpy.zeros([len(parameters), prod])                                                                                                               

    for i in range(prod):                                                                                                                                       
            print "at %d of %d"%(i, prod)                                                                                                                       
            fit(function, parameters, y[:,i], x)                                                                                                                
            for p in range(len(parameters)):                                                                                                                    
                    params[p, i] = parameters[p]()                                                                                                              

    shape[0] = len(parameters)                                                                                                                                  
    params = params.reshape(shape)                                                                                                                              
    return params                                                                                                                                               

Обратите внимание, что данные не обязательно 256x262144, и я попытался поработать над этим в nd_fit.

Код, который я использую, чтобы заставить это работать, является

from curve_fitting import *
import numpy
frames = numpy.load("data.npy")
y = frames[:,0,0,20,40]
x = range(0, 512, 2)
mu = Parameter(x[argmax(y)])
height = Parameter(max(y))
sigma = Parameter(50)
def f(x): return height()  * exp (-((x - mu()) / sigma()) ** 2)

ls_data = nd_fit(f, [mu, sigma, height], frames, x, 0)

Примечание: Решение, опубликованное ниже @JoeKington, великолепно и решает очень быстро.Однако он не работает, если значительная часть гауссиана не находится внутри соответствующей области.Я должен буду проверить, является ли среднее значение все еще точным, поскольку это - главное, для чего я использую это.Analysis of gaussian distribution estimations

1 Ответ

18 голосов
/ 09 января 2012

Самое простое, что нужно сделать - это линеаризовать проблему. Вы используете нелинейный итеративный метод, который будет медленнее, чем линейное решение наименьших квадратов.

В основном у вас есть:

y = height * exp(-(x - mu)^2 / (2 * sigma ^ 2))

Чтобы сделать это линейным уравнением, возьмите (натуральное) логарифм обеих сторон:

ln(y) = ln(height) - (x - mu)^2 / (2 * sigma^2)

Затем это упрощается до полинома:

ln(y) = -x^2 / (2 * sigma^2) + x * mu / sigma^2 - mu^2 / sigma^2 + ln(height)

Мы можем изменить это в несколько более простой форме:

ln(y) = A * x^2 + B * x + C

где:

A = 1 / (2 * sigma^2)
B = mu / (2 * sigma^2)
C = mu^2 / sigma^2 + ln(height)

Однако есть одна загвоздка. Это станет нестабильным при наличии шума в «хвостах» распределения.

Поэтому нам нужно использовать только данные около «пиков» распределения. Достаточно просто включить в пример только те данные, которые превышают некоторый порог. В этом примере я включаю только данные, которые превышают 20% от максимального наблюдаемого значения для данной гауссовой кривой, которую мы подгоняем.

Как только мы это сделаем, это будет довольно быстро. Решение для 262144 различных гауссовых кривых занимает всего ~ 1 минуту (Обязательно удалите часть кода для построения графика, если вы запускаете его на чем-то таком большом ...). Также очень легко распараллелить, если вы хотите ...

import numpy as np
import matplotlib.pyplot as plt
import matplotlib as mpl
import itertools

def main():
    x, data = generate_data(256, 6)
    model = [invert(x, y) for y in data.T]
    sigma, mu, height = [np.array(item) for item in zip(*model)]
    prediction = gaussian(x, sigma, mu, height)

    plot(x, data, linestyle='none', marker='o')
    plot(x, prediction, linestyle='-')
    plt.show()

def invert(x, y):
    # Use only data within the "peak" (20% of the max value...)
    key_points = y > (0.2 * y.max())
    x = x[key_points]
    y = y[key_points]

    # Fit a 2nd order polynomial to the log of the observed values
    A, B, C = np.polyfit(x, np.log(y), 2)

    # Solve for the desired parameters...
    sigma = np.sqrt(-1 / (2.0 * A))
    mu = B * sigma**2
    height = np.exp(C + 0.5 * mu**2 / sigma**2)
    return sigma, mu, height

def generate_data(numpoints, numcurves):
    np.random.seed(3)
    x = np.linspace(0, 500, numpoints)

    height = 100 * np.random.random(numcurves)
    mu = 200 * np.random.random(numcurves) + 200
    sigma = 100 * np.random.random(numcurves) + 0.1
    data = gaussian(x, sigma, mu, height)

    noise = 5 * (np.random.random(data.shape) - 0.5)
    return x, data + noise

def gaussian(x, sigma, mu, height):
    data = -np.subtract.outer(x, mu)**2 / (2 * sigma**2)
    return height * np.exp(data)

def plot(x, ydata, ax=None, **kwargs):
    if ax is None:
        ax = plt.gca()
    colorcycle = itertools.cycle(mpl.rcParams['axes.color_cycle'])
    for y, color in zip(ydata.T, colorcycle):
        ax.plot(x, y, color=color, **kwargs)

main()

enter image description here

Единственное, что нам нужно изменить для параллельной версии, - это основная функция. (Нам также нужна фиктивная функция, потому что multiprocessing.Pool.imap не может предоставить дополнительные аргументы для своей функции ...) Это будет выглядеть примерно так:

def parallel_main():
    import multiprocessing
    p = multiprocessing.Pool()
    x, data = generate_data(256, 262144)
    args = itertools.izip(itertools.repeat(x), data.T)
    model = p.imap(parallel_func, args, chunksize=500)
    sigma, mu, height = [np.array(item) for item in zip(*model)]
    prediction = gaussian(x, sigma, mu, height)

def parallel_func(args):
    return invert(*args)

Редактировать: В тех случаях, когда простая полиномиальная подгонка не работает должным образом, попробуйте взвешивать проблему по значениям y, , как упомянуто в ссылке / документе , что @tslisten shared (и Стефан ван дер Уолт реализовал, хотя моя реализация немного отличается).

import numpy as np
import matplotlib.pyplot as plt
import matplotlib as mpl
import itertools

def main():
    def run(x, data, func, threshold=0):
        model = [func(x, y, threshold=threshold) for y in data.T]
        sigma, mu, height = [np.array(item) for item in zip(*model)]
        prediction = gaussian(x, sigma, mu, height)

        plt.figure()
        plot(x, data, linestyle='none', marker='o', markersize=4)
        plot(x, prediction, linestyle='-', lw=2)

    x, data = generate_data(256, 6, noise=100)
    threshold = 50

    run(x, data, weighted_invert, threshold=threshold)
    plt.title('Weighted by Y-Value')

    run(x, data, invert, threshold=threshold)
    plt.title('Un-weighted Linear Inverse'

    plt.show()

def invert(x, y, threshold=0):
    mask = y > threshold
    x, y = x[mask], y[mask]

    # Fit a 2nd order polynomial to the log of the observed values
    A, B, C = np.polyfit(x, np.log(y), 2)

    # Solve for the desired parameters...
    sigma, mu, height = poly_to_gauss(A,B,C)
    return sigma, mu, height

def poly_to_gauss(A,B,C):
    sigma = np.sqrt(-1 / (2.0 * A))
    mu = B * sigma**2
    height = np.exp(C + 0.5 * mu**2 / sigma**2)
    return sigma, mu, height

def weighted_invert(x, y, weights=None, threshold=0):
    mask = y > threshold
    x,y = x[mask], y[mask]
    if weights is None:
        weights = y
    else:
        weights = weights[mask]

    d = np.log(y)
    G = np.ones((x.size, 3), dtype=np.float)
    G[:,0] = x**2
    G[:,1] = x

    model,_,_,_ = np.linalg.lstsq((G.T*weights**2).T, d*weights**2)
    return poly_to_gauss(*model)

def generate_data(numpoints, numcurves, noise=None):
    np.random.seed(3)
    x = np.linspace(0, 500, numpoints)

    height = 7000 * np.random.random(numcurves)
    mu = 1100 * np.random.random(numcurves) 
    sigma = 100 * np.random.random(numcurves) + 0.1
    data = gaussian(x, sigma, mu, height)

    if noise is None:
        noise = 0.1 * height.max()
    noise = noise * (np.random.random(data.shape) - 0.5)
    return x, data + noise

def gaussian(x, sigma, mu, height):
    data = -np.subtract.outer(x, mu)**2 / (2 * sigma**2)
    return height * np.exp(data)

def plot(x, ydata, ax=None, **kwargs):
    if ax is None:
        ax = plt.gca()
    colorcycle = itertools.cycle(mpl.rcParams['axes.color_cycle'])
    for y, color in zip(ydata.T, colorcycle):
        #kwargs['color'] = kwargs.get('color', color)
        ax.plot(x, y, color=color, **kwargs)

main()

enter image description here enter image description here

Если это все еще доставляет вам проблемы, попробуйте итеративно перевесить задачу наименьших квадратов (последний «лучший» рекомендуемый метод в ссылке @tslisten упоминается). Имейте в виду, что это будет значительно медленнее.

def iterative_weighted_invert(x, y, threshold=None, numiter=5):
    last_y = y
    for _ in range(numiter):
        model = weighted_invert(x, y, weights=last_y, threshold=threshold)
        last_y = gaussian(x, *model)
    return model
...