PyMC pour le regroupement non paramétrique: le processus de Dirichlet pour estimer les paramètres du mélange gaussien ne parvient pas à se regrouper

10

Configuration du problème

L'un des premiers problèmes de jouets auquel j'ai voulu appliquer PyMC est le clustering non paramétrique: étant donné certaines données, modélisez-le comme un mélange gaussien et apprenez le nombre de clusters et la moyenne et la covariance de chaque cluster. La plupart de ce que je sais de cette méthode provient de conférences vidéo de Michael Jordan et Yee Whye Teh, vers 2007 (avant que la rareté ne devienne la rage), et des deux derniers jours de lecture des tutoriels du Dr Fonnesbeck et d'E. Chen [fn1], [ fn2]. Mais le problème est bien étudié et a quelques implémentations fiables [fn3].

Dans ce problème de jouet, je génère dix tirages à partir d'un gaussien unidimensionnel ( μ = 0 , σ = 1 ) et quarante tirages à partir de N ( μ = 4 , σ = 2 ) . Comme vous pouvez le voir ci-dessous, je n'ai pas mélangé les tirages, pour qu'il soit facile de dire quels échantillons provenaient de quel composant du mélange.N(μ=0,σ=1)N(μ=4,σ=2)

Données du modèle de mélange gaussien

Je modèle chaque échantillon de données , pour i = 1 , . . . , 50 et où z i indique la grappe pour cette i ème point de données: z i[ 1 , . . . , N D P ] . N D P ici est la longueur du processus de Dirichlet tronqué utilisé: pour moi, NyiN(μzi,σzi)i=1,...,50ziizi[1,...,NDP]NDP.NDP=50

En développant l'infrastructure du processus de Dirichlet, chaque ID de cluster est un tirage d'une variable aléatoire catégorielle, dont la fonction de masse de probabilité est donnée par la construction de rupture de bâton: z iC a t e g o r i c a l ( p ) avec p S t i c k ( α ) pour un paramètre de concentration α . Le bris de bâtons construit le vecteur N D P- long pziziCategorical(p)pStick(α)αNDPp, qui doit être égal à 1, en obtenant d'abord iid tirages distribués bêta qui dépendent de α , voir [fn1]. Et comme je voudrais que les données informent mon ignorance de α , je suis [fn1] et je suppose que α U n i f o r m ( 0,3 , 100 ) .NDPαααUniform(0.3,100)

Cela spécifie comment l'ID de cluster de chaque échantillon de données est généré. Chacun des groupes a une moyenne et un écart-type associés, μ z i et σ z i . Ensuite, μ z iN ( μ = 0 , σ = 50 ) et σ 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 résumé, mon modèle est:

yiN(μzi,σzi)i

ziCategorical(p)NDP1=49pStick(α)NDPαUniform(0.3,100), un scalaire. (Je regrette maintenant d'avoir légèrement égalé le nombre d'échantillons de données à la longueur tronquée du Dirichlet, mais j'espère que c'est clair.)

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

Voici le modèle graphique: les noms sont des noms de variables, voir la section code ci-dessous.

Graphique

Énoncé du problème

Malgré plusieurs ajustements et correctifs échoués, les paramètres appris ne sont pas du tout similaires aux vraies valeurs qui ont généré les données.

ziα=5

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

Moyens avec un ID de cluster initialisé à zéro

Rappelant que les dix premiers échantillons de données provenaient d'un mode et les autres de l'autre, le résultat ci-dessus ne parvient clairement pas à saisir cela.

Si j'autorise l'initialisation aléatoire des ID de cluster, alors j'obtiens plus d'un cluster mais le cluster signifie que tous errent autour du même niveau 3.5:

Moyens avec un ID de cluster initialisé au hasard

zi

α

Annexe: code

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)

Références

  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
Ahmed Fasih
la source
Votre priorité sur les écarts de composants est uniforme (0,100), ce qui pourrait vous causer des problèmes majeurs. Seulement 2% de la masse de cet a priori couvre les vraies variances de 1 et 2. La variance attendue de vos composants sous cet a priori est de 50, ce qui est un gaussien si large qu'il peut facilement rendre compte de vos données avec un seul composant.
2013
Avez-vous lu ce chapitre du livre Programmation probabiliste et statistiques bayésiennes pour les pirates ? Il a un exemple qui peut vous aider!
Tim
Cela semble un peu bref pour une réponse. Il semble s'agir davantage d'un commentaire. Pourriez-vous au moins indiquer quelles informations le PO obtiendra en le lisant?
Glen_b -Reinstate Monica
@TimRich oui, je l'ai lu, et j'ai pris des cours dans des études supérieures et travaillé dans l'industrie en faisant des statistiques appliquées;) c'est une question spécifique à PyMC.
Ahmed Fasih
1
μZiN(μ0,σ0)μ0,σ0

Réponses:

4

Je ne sais pas si quelqu'un se penche plus sur cette question, mais j'ai posé votre question à rjags pour tester la suggestion d'échantillonnage Gibbs de Tom tout en incorporant les informations de Guy sur l'appartement avant l'écart type.

Ce problème de jouet pourrait être difficile car 10 et même 40 points de données ne sont pas suffisants pour estimer la variance sans un préalable informatif. L'actuel σzi∼Uniform (0,100) n'est pas informatif. Cela pourrait expliquer pourquoi presque tous les tirages de μzi sont la moyenne attendue des deux distributions. Si cela ne modifie pas trop votre question, j'utiliserai respectivement 100 et 400 points de données.

Je n'ai pas non plus utilisé le processus de bris de bâton directement dans mon code. La page wikipedia du processus dirichlet m'a fait penser que p ~ Dir (a / k) serait ok.

Enfin, il ne s'agit que d'une implémentation semi-paramétrique car il prend encore un certain nombre de clusters k. Je ne sais pas comment faire un modèle de mélange infini dans rjags.

chaîne de Markov mu cluster 1

chaîne 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)
user1068430
la source
1
KKαi=a/K500K=25JAGSJAGSp
1

Le mauvais mélange que vous voyez est probablement dû à la façon dont PyMC tire les échantillons. Comme expliqué dans la section 5.8.1 de la documentation PyMC, tous les éléments d'une variable de tableau sont mis à jour ensemble. Dans votre cas, cela signifie qu'il essaiera de mettre à jour l'intégralité du clustermeantableau en une seule étape, et de même pour clusterid. PyMC ne fait pas d'échantillonnage Gibbs; il fait Metropolis où la proposition est choisie par quelques heuristiques simples. Il est donc peu probable de proposer une bonne valeur pour un tableau entier.

Tom Minka
la source
Dès que vous avez dit: "il essaiera de mettre à jour l'ensemble du tableau en une seule étape", j'ai compris les inconvénients de Metropolis (dans ce cas) par rapport à Gibbs. Y a-t-il quelque chose de spécial à propos de STAN ou JAGS qui pourrait leur permettre de faire mieux à ce sujet? Dans les deux cas, je vais passer du temps à implémenter Gibbs dans PyMC. Je vous remercie! (Je suis un fan de votre travail depuis la vitesse de l'éclair, donc double merci!)
Ahmed Fasih
1
STAN ne gère pas les variables discrètes, mais JAGS vaut la peine d'être essayé.
Tom Minka