PyMC para la agrupación no paramétrica: el proceso de Dirichlet para estimar los parámetros de la mezcla gaussiana no se agrupa


10

Configuración del problema

Uno de los primeros problemas con los juguetes a los que quería aplicar PyMC es la agrupación no paramétrica: dado algunos datos, modelarlo como una mezcla gaussiana y conocer el número de grupos y la media y covarianza de cada grupo. La mayor parte de lo que sé sobre este método proviene de video conferencias de Michael Jordan y Yee Whye Teh, alrededor del año 2007 (antes de que la escasez se convirtiera en furor), y los últimos días leyendo los tutoriales del Dr. Fonnesbeck y E. Chen [fn1], [ fn2]. Pero el problema está bien estudiado y tiene algunas implementaciones confiables [fn3].

En este problema con el juguete, genero diez sorteos a partir de un Gaussiano unidimensional ( μ = 0 , σ = 1 ) y cuarenta sorteos a partir de N ( μ = 4 , σ = 2 ) . Como puede ver a continuación, no barajé los sorteos, para que sea fácil saber qué muestras provienen de qué componente de la mezcla.N(μ=0,σ=1)N(μ=4,σ=2)

Datos del modelo de mezcla gaussiana

I modelar cada muestra de datos , para i = 1 , . . . , 50 y donde z i indica el clúster para este i ésimo punto de datos: z i[ 1 , . . . , N D P ] . N D P aquí es la duración del proceso de Dirichlet truncado utilizado: para mí, NyiN(μzi,σzi)i=1,...,50ziizi[1,...,NDP]NDP.NDP=50

Expandiendo la infraestructura del proceso de Dirichlet, cada ID de clúster es un sorteo de una variable aleatoria categórica, cuya función de masa de probabilidad viene dada por la construcción de ruptura de palo: z iC a t e g o r i c a l ( p ) con p S t i c k ( α ) para un parámetro de concentración α . La rotura de palos construye el vector N D P - largo pziziCategorical(p)pStick(α)αNDPp, que debe sumar 1, obteniendo primero iid sorteos distribuidos en Beta que dependen de α , ver [fn1]. Y como me gustaría que los datos informaran mi ignorancia de α , sigo [fn1] y asumo α U n i f o r m ( 0.3 , 100 ) .NDPαααUniform(0.3,100)

Esto especifica cómo se genera el ID de clúster de cada muestra de datos. Cada uno de los grupos tiene una desviación estándar y media asociada, μ z i y σ z i . Entonces, μ z iN ( μ = 0 , σ = 50 ) y σ z iU n i f o r m ( 0 , 100 ) .NDPμziσziμziN(μ=0,σ=50)σziUniform(0,100)

μziμziN(μ0,σ0)μ0σ0

En resumen, mi modelo es:

yiN(μzi,σzi)i

ziCategorical(p)NDP1=49pStick(α)NDPαUniform(0.3,100), un escalar. (Ahora me arrepiento un poco de que la cantidad de muestras de datos sea igual a la longitud truncada del Dirichlet antes, pero espero que esté claro).

μziN(μ=0,σ=50)σziUniform(0,100)NDPNDP

Aquí está el modelo gráfico: los nombres son nombres de variables, consulte la sección de códigos a continuación.

Grafico

Planteamiento del problema

A pesar de varios ajustes y correcciones fallidas, los parámetros aprendidos no son en absoluto similares a los valores verdaderos que generaron los datos.

ziα=5

pμziiμzii=1,...,20

Medios con ID de clúster inicializado cero

Recordando que las primeras diez muestras de datos eran de un modo y el resto eran del otro, el resultado anterior claramente no logra capturar eso.

Si permito la inicialización aleatoria de las ID de clúster, entonces obtengo más de un clúster pero el clúster significa que todos deambulan por el mismo nivel 3.5:

Medios con ID de clúster inicializado al azar

zi

α

Apéndice: código

import pymc
import numpy as np

### Data generation

# Means and standard deviations of the Gaussian mixture model. The inference
# engine doesn't know these.
means = [0, 4.0]
stdevs = [1, 2.0]

# Rather than randomizing between the mixands, just specify how many
# to draw from each. This makes it really easy to know which draws
# came from which mixands (the first N1 from the first, the rest from
# the secon). The inference engine doesn't know about N1 and N2, only Ndata
N1 = 10
N2 = 40
Ndata = N1+N2

# Seed both the data generator RNG  as well as the global seed (for PyMC)
RNGseed = 123
np.random.seed(RNGseed)

def generate_data(draws_per_mixand):
    """Draw samples from a two-element Gaussian mixture reproducibly.

    Input sequence indicates the number of draws from each mixand. Resulting
    draws are concantenated together.

    """
    RNG = np.random.RandomState(RNGseed)
    values = np.hstack([RNG.normal(means[i], stdevs[i], ndraws)
                        for (i,ndraws) in enumerate(draws_per_mixand)])
    return values

observed_data = generate_data([N1, N2])


### PyMC model setup, step 1: the Dirichlet process and stick-breaking

# Truncation level of the Dirichlet process
Ndp = 50

# "alpha", or the concentration of the stick-breaking construction. There exists
# some interplay between choice of Ndp and concentration: a high concentration
# value implies many clusters, in turn implying low values for the leading
# elements of the probability mass function built by stick-breaking. Since we
# enforce the resulting PMF to sum to one, the probability of the last cluster
# might be then be set artificially high. This may interfere with the Dirichlet
# process' clustering ability.
#
# An example: if Ndp===4, and concentration high enough, stick-breaking might
# yield p===[.1, .1, .1, .7], which isn't desireable. You want to initialize
# concentration so that the last element of the PMF is less than or not much
# more than the a few of the previous ones. So you'd want to initialize at a
# smaller concentration to get something more like, say, p===[.35, .3, .25, .1].
#
# A thought: maybe we can avoid this interdependency by, rather than setting the
# final value of the PMF vector, scale the entire PMF vector to sum to 1? FIXME,
# TODO.
concinit = 5.0
conclo = 0.3
conchi = 100.0
concentration = pymc.Uniform('concentration', lower=conclo, upper=conchi,
                             value=concinit)

# The stick-breaking construction: requires Ndp beta draws dependent on the
# concentration, before the probability mass function is actually constructed.
betas = pymc.Beta('betas', alpha=1, beta=concentration, size=Ndp)

@pymc.deterministic
def pmf(betas=betas):
    "Construct a probability mass function for the truncated Dirichlet process"
    # prod = lambda x: np.exp(np.sum(np.log(x))) # Slow but more accurate(?)
    prod = np.prod
    value = map(lambda (i,u): u * prod(1.0 - betas[:i]), enumerate(betas))
    value[-1] = 1.0 - sum(value[:-1]) # force value to sum to 1
    return value

# The cluster assignments: each data point's estimated cluster ID.
# Remove idinit to allow clusterid to be randomly initialized:
idinit = np.zeros(Ndata, dtype=np.int64)
clusterid = pymc.Categorical('clusterid', p=pmf, size=Ndata, value=idinit)

### PyMC model setup, step 2: clusters' means and stdevs

# An individual data sample is drawn from a Gaussian, whose mean and stdev is
# what we're seeking.

# Hyperprior on clusters' means
mu0_mean = 0.0
mu0_std = 50.0
mu0_prec = 1.0/mu0_std**2
mu0_init = np.zeros(Ndp)
clustermean = pymc.Normal('clustermean', mu=mu0_mean, tau=mu0_prec,
                          size=Ndp, value=mu0_init)

# The cluster's stdev
clustersig_lo = 0.0
clustersig_hi = 100.0
clustersig_init = 50*np.ones(Ndp) # Again, don't really care?
clustersig = pymc.Uniform('clustersig', lower=clustersig_lo,
                          upper=clustersig_hi, size=Ndp, value=clustersig_init)
clusterprec = clustersig ** -2

### PyMC model setup, step 3: data

# So now we have means and stdevs for each of the Ndp clusters. We also have a
# probability mass function over all clusters, and a cluster ID indicating which
# cluster a particular data sample belongs to.

@pymc.deterministic
def data_cluster_mean(clusterid=clusterid, clustermean=clustermean):
    "Converts Ndata cluster IDs and Ndp cluster means to Ndata means."
    return clustermean[clusterid]

@pymc.deterministic
def data_cluster_prec(clusterid=clusterid, clusterprec=clusterprec):
    "Converts Ndata cluster IDs and Ndp cluster precs to Ndata precs."
    return clusterprec[clusterid]

data = pymc.Normal('data', mu=data_cluster_mean, tau=data_cluster_prec,
                   observed=True, value=observed_data)

Referencias

  1. fn1: http://nbviewer.ipython.org/urls/raw.github.com/fonnesbeck/Bios366/master/notebooks/Section5_2-Dirichlet-Processes.ipynb
  2. fn2: http://blog.echen.me/2012/03/20/infinite-mixture-models-with-nonparametric-bayes-and-the-dirichlet-process/
  3. fn3: http://scikit-learn.org/stable/auto_examples/mixture/plot_gmm.html#example-mixture-plot-gmm-py

Su prioridad sobre las variaciones de componentes es Uniforme (0,100), lo que podría estar causándole problemas importantes. Solo el 2% de la masa de esta versión anterior cubre las variaciones reales de 1 y 2. La variación esperada de sus componentes en esta versión anterior es 50, que es un gaussiano tan amplio que puede explicar fácilmente sus datos con un solo componente.
jerad

¿Has leído este capítulo del libro Probabilistic Programming and Bayesian Statistics for Hackers ? ¡Tiene un ejemplo que puede ayudarte!
Tim

Esto parece un poco breve para una respuesta. Parece ser más un comentario. ¿Podría al menos describir qué información obtendrá el OP al leerla?
Glen_b

@TimRich sí, lo he leído, y tomé la clase en la escuela de posgrado, y trabajé en la industria haciendo estadísticas aplicadas;) esta es una pregunta específica de PyMC.
Ahmed Fasih

1
μZiN(μ0,σ0)μ0,σ0

Respuestas:


4

No estoy seguro de si alguien está analizando esta pregunta más, pero puse tu pregunta en rjags para probar la sugerencia de muestreo de Tom's Gibbs al tiempo que incorporo una idea de Guy sobre el plano anterior para la desviación estándar.

Este problema del juguete puede ser difícil porque 10 e incluso 40 puntos de datos no son suficientes para estimar la varianza sin un previo informativo. El actual σzi∼Uniform anterior (0,100) no es informativo. Esto podría explicar por qué casi todos los sorteos de μzi son la media esperada de las dos distribuciones. Si no altera demasiado su pregunta, usaré 100 y 400 puntos de datos respectivamente.

Tampoco utilicé el proceso de rotura de palo directamente en mi código. La página de wikipedia para el proceso de dirichlet me hizo pensar que p ~ Dir (a / k) estaría bien.

Finalmente, es solo una implementación semiparamétrica, ya que todavía requiere una cantidad de clústeres k. No sé cómo hacer un modelo de mezcla infinita en rjags.

cadena de markov mu cluster 1

cadena de markov mu cluster 2

library("rjags")

set1 <- rnorm(100, 0, 1)
set2 <- rnorm(400, 4, 1)
data <- c(set1, set2)

plot(data, type='l', col='blue', lwd=3,
     main='gaussian mixture model data',
     xlab='data sample #', ylab='data value')
points(data, col='blue')

cpd.model.str <- 'model {
  a ~ dunif(0.3, 100)
  for (i in 1:k){
    alpha[i] <- a/k
    mu[i] ~ dnorm(0.0, 0.001)
    sigma[i] ~ dunif(0, 100)
  }
  p[1:k] ~ ddirich(alpha[1:k])
  for (i in 1:n){
    z[i] ~ dcat(p)
    y[i] ~ dnorm(mu[z[i]], pow(sigma[z[i]], -2))
  }
}' 


cpd.model <- jags.model(textConnection(cpd.model.str),
                        data=list(y=data,
                                  n=length(data),
                                  k=5))
update(cpd.model, 1000)
chain <- coda.samples(model = cpd.model, n.iter = 1000,
                      variable.names = c('p', 'mu', 'sigma'))
rchain <- as.matrix(chain)
apply(rchain, 2, mean)

1
KKαi=a/K500K=25JAGSJAGSp

1

La mala mezcla que está viendo es muy probable debido a la forma en que PyMC toma muestras. Como se explica en la sección 5.8.1 de la documentación de PyMC, todos los elementos de una variable de matriz se actualizan juntos. En su caso, eso significa que intentará actualizar toda la clustermeanmatriz en un solo paso, y de manera similar clusterid. PyMC no hace muestreo de Gibbs; lo hace en Metrópolis, donde la propuesta es elegida por algunas heurísticas simples. Esto hace que sea poco probable que proponga un buen valor para una matriz completa.


Tan pronto como dijiste, "intentará actualizar toda la matriz en un solo paso", entendí los inconvenientes de Metrópolis (en este caso) frente a Gibbs. ¿Hay algo especial en STAN o JAGS que les permita mejorar en esto? En cualquier caso, voy a pasar algún tiempo implementando Gibbs en PyMC. ¡Gracias! (He sido fanático de tu trabajo desde la velocidad de la luz, ¡así que doble gracias!)
Ahmed Fasih

1
STAN no maneja variables discretas, pero vale la pena probar JAGS.
Tom Minka
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.