Cette réponse a été considérablement modifiée par rapport à sa forme d'origine. Les défauts de ma réponse d'origine seront discutés ci-dessous, mais si vous souhaitez voir à peu près à quoi ressemblait cette réponse avant de faire le gros montage, jetez un œil au cahier suivant: https://nbviewer.jupyter.org/github /dmarx/data_generation_demo/blob/54be78fb5b68218971d2568f1680b4f783c0a79a/demo.ipynb
TL; DR: Utilisez un KDE (ou la procédure de votre choix) pour approximer , puis utilisez MCMC pour tirer des échantillons de , où est donné par votre modèle. À partir de ces échantillons, vous pouvez estimer le «optimal» en ajustant un deuxième KDE aux échantillons que vous avez générés et en sélectionnant l'observation qui maximise le KDE comme estimation maximale a posteriori (MAP).P(X)P(X|Y)∝P(Y|X)P(X)P(Y|X)X
Estimation de vraisemblance maximale
... et pourquoi cela ne fonctionne pas ici
Dans ma réponse d'origine, la technique que j'ai suggérée était d'utiliser MCMC pour effectuer une estimation du maximum de vraisemblance. Généralement, le MLE est une bonne approche pour trouver les solutions "optimales" aux probabilités conditionnelles, mais nous avons un problème ici: parce que nous utilisons un modèle discriminant (une forêt aléatoire dans ce cas), nos probabilités sont calculées par rapport aux limites de décision . En fait, cela n'a pas de sens de parler d'une solution "optimale" à un modèle comme celui-ci, car une fois que nous nous éloignerons suffisamment de la frontière de classe, le modèle ne fera que prédire celles pour tout. Si nous avons suffisamment de classes, certaines d'entre elles pourraient être complètement «entourées», auquel cas ce ne sera pas un problème, mais les classes à la frontière de nos données seront «maximisées» par des valeurs qui ne sont pas nécessairement réalisables.
Pour démontrer, je vais tirer parti du code de commodité que vous pouvez trouver ici , qui fournit la GenerativeSampler
classe qui encapsule le code de ma réponse d'origine, du code supplémentaire pour cette meilleure solution et des fonctionnalités supplémentaires avec lesquelles je jouais (certaines fonctionnent , certains qui ne le font pas) que je n'entrerai probablement pas ici.
np.random.seed(123)
sampler = GenerativeSampler(model=RFC, X=X, y=y,
target_class=2,
prior=None,
class_err_prob=0.05, # <-- the score we use for candidates that aren't predicted as the target class
rw_std=.05, # <-- controls the step size of the random walk proposal
verbose=True,
use_empirical=False)
samples, _ = sampler.run_chain(n=5000)
burn = 1000
thin = 20
X_s = pca.transform(samples[burn::thin,:])
# Plot the iris data
col=['r','b','g']
for i in range(3):
plt.scatter(*X_r[y==i,:].T, c=col[i], marker='x')
plt.plot(*X_s.T, 'k')
plt.scatter(*X_s.T, c=np.arange(X_s.shape[0]))
plt.colorbar()
plt.show()
Dans cette visualisation, les x sont les vraies données et la classe qui nous intéresse est verte. Les points connectés aux lignes sont les échantillons que nous avons dessinés, et leur couleur correspond à l'ordre dans lequel ils ont été échantillonnés, avec leur position de séquence "amincie" donnée par l'étiquette de la barre de couleur à droite.
Comme vous pouvez le voir, l'échantillonneur a divergé assez rapidement des données, puis se bloque essentiellement assez loin des valeurs de l'espace des fonctionnalités qui correspondent à toutes les observations réelles. C'est clairement un problème.
Une façon de tricher consiste à modifier notre fonction de proposition pour autoriser uniquement les entités à prendre des valeurs que nous avons réellement observées dans les données. Essayons cela et voyons comment cela change le comportement de notre résultat.
np.random.seed(123)
sampler = GenerativeSampler(model=RFC, X=X, y=y,
target_class=2,
prior=None,
class_err_prob=0.05,
verbose=True,
use_empirical=True) # <-- magic happening under the hood
samples, _ = sampler.run_chain(n=5000)
X_s = pca.transform(samples[burn::thin,:])
# Constrain attention to just the target class this time
i=2
plt.scatter(*X_r[y==i,:].T, c='k', marker='x')
plt.scatter(*X_s.T, c='g', alpha=0.3)
#plt.colorbar()
plt.show()
sns.kdeplot(X_s, cmap=sns.dark_palette('green', as_cmap=True))
plt.scatter(*X_r[y==i,:].T, c='k', marker='x')
plt.show()
C'est certainement une amélioration significative et le mode de notre distribution correspond à peu près à ce que nous recherchons, mais il est clair que nous générons encore beaucoup d'observations qui ne correspondent pas aux valeurs réalisables de donc nous ne devrions pas vraiment faites confiance à cette distribution non plus.X
La solution évidente ici est d'incorporer manière ou d'une autre pour ancrer notre processus d'échantillonnage aux régions de l'espace caractéristique que les données sont susceptibles de prendre. Prenons donc un échantillon de la probabilité conjointe de la probabilité donnée par le modèle, , et une estimation numérique de donnée par un ajustement KDE sur l'ensemble de données entier. Alors maintenant, nous ... échantillonnage à partir de ... ....P(X)P(Y|X)P(X)P(Y|X)P(X)
Entrez la règle de Bayes
Après que vous m'ayez harcelé pour être moins agité avec les maths ici, j'ai beaucoup joué avec cela (donc je construis la GenerativeSampler
chose), et j'ai rencontré les problèmes que j'ai exposés ci-dessus. Je me sentais vraiment, vraiment stupide quand j'ai fait cette prise de conscience, mais évidemment ce que vous demandez appelle une application de la règle de Bayes et je m'excuse d'être dédaigneux plus tôt.
Si vous n'êtes pas familier avec la règle bayes, cela ressemble à ceci:
P(B|A)=P(A|B)P(B)P(A)
Dans de nombreuses applications, le dénominateur est une constante qui agit comme un terme d'échelle pour garantir que le numérateur s'intègre à 1, de sorte que la règle est souvent reformulée ainsi:
P(B|A)∝P(A|B)P(B)
Ou en langage simple: "le postérieur est proportionnel aux temps antérieurs à la probabilité".
Semble familier? Et maintenant:
P(X|Y)∝P(Y|X)P(X)
Oui, c'est exactement ce que nous avons travaillé plus tôt en construisant une estimation pour le MLE qui est ancrée à la distribution observée des données. Je n'ai jamais pensé à la règle Bayes de cette façon, mais cela a du sens, alors merci de me donner l'opportunité de découvrir cette nouvelle perspective.
Pour revenir en arrière un peu, MCMC est l'une de ces applications de la règle bayésienne où nous pouvons ignorer le dénominateur. Lorsque nous calculons le ratio d'acceptation, prendra la même valeur à la fois au numérateur et au dénominateur, annulant et nous permettant de tirer des échantillons de distributions de probabilité non normalisées.P(Y)
Donc, après avoir fait cette idée que nous devons incorporer un prior pour les données, faisons-le en ajustant un KDE standard et voyons comment cela change notre résultat.
np.random.seed(123)
sampler = GenerativeSampler(model=RFC, X=X, y=y,
target_class=2,
prior='kde', # <-- the new hotness
class_err_prob=0.05,
rw_std=.05, # <-- back to the random walk proposal
verbose=True,
use_empirical=False)
samples, _ = sampler.run_chain(n=5000)
burn = 1000
thin = 20
X_s = pca.transform(samples[burn::thin,:])
# Plot the iris data
col=['r','b','g']
for i in range(3):
plt.scatter(*X_r[y==i,:].T, c=col[i], marker='x')
plt.plot(*X_s.T, 'k--')
plt.scatter(*X_s.T, c=np.arange(X_s.shape[0]), alpha=0.2)
plt.colorbar()
plt.show()
Bien mieux! Maintenant, nous pouvons estimer votre valeur "optimale" en utilisant ce qu'on appelle l'estimation "maximum a posteriori", ce qui est une façon élégante de dire que nous ajustons un deuxième KDE - mais à nos échantillons cette fois - et de trouver la valeur qui maximise le KDE, c'est-à-dire la valeur correspondant au mode de .P ( X | Y )XP(X|Y)
# MAP estimation
from sklearn.neighbors import KernelDensity
from sklearn.model_selection import GridSearchCV
from scipy.optimize import minimize
grid = GridSearchCV(KernelDensity(), {'bandwidth': np.linspace(0.1, 1.0, 30)}, cv=10, refit=True)
kde = grid.fit(samples[burn::thin,:]).best_estimator_
def map_objective(x):
try:
score = kde.score_samples(x)
except ValueError:
score = kde.score_samples(x.reshape(1,-1))
return -score
x_map = minimize(map_objective, samples[-1,:].reshape(1,-1)).x
print(x_map)
x_map_r = pca.transform(x_map.reshape(1,-1))[0]
col=['r','b','g']
for i in range(3):
plt.scatter(*X_r[y==i,:].T, c=col[i], marker='x')
sns.kdeplot(*X_s.T, cmap=sns.dark_palette('green', as_cmap=True))
plt.scatter(x_map_r[0], x_map_r[1], c='k', marker='x', s=150)
plt.show()
Et voilà: le grand «X» noir est notre estimation MAP (ces contours sont le KDE du postérieur).