Redes de densidade de mistura


Olá pessoal!

Vamos falar sobre, como você deve ter adivinhado, redes neurais e aprendizado de máquina. Pelo nome, fica claro o que será dito sobre as redes de densidade de mistura, e então apenas a MDN, não quero traduzir o nome e deixá-lo como está. Sim, sim, sim ... haverá um pouco de matemática chata e teoria das probabilidades, mas sem ela, infelizmente ou felizmente, cabe a você decidir o quão difícil é imaginar o mundo do aprendizado de máquina. Mas me apresso em tranquilizá-lo, será relativamente pequeno e não será muito difícil. De qualquer forma, você pode pular, mas basta olhar para uma pequena quantidade de código no Python e no PyTorch, isso mesmo, escreveremos a rede usando o PyTorch, além de vários gráficos com os resultados. Mas o mais importante é que haverá uma oportunidade de entender um pouco e entender o que são redes MD.

Bem, vamos começar!



Regressão


Para começar, vamos atualizar um pouco nosso conhecimento e relembrar brevemente, o que é regressão linear .

Nós temos um vetor X = \ {x_1, x_2, ..., x_n \}X = \ {x_1, x_2, ..., x_n \} precisamos prever o valor Y , que de alguma forma depende X usando algum modelo linear:

 hatY=XT hat beta

Como função de erro, usaremos o erro ao quadrado:

SE( beta)= sumni=1(yi hatyi)2= sumNi=1(yixTi hat beta)2

Esse problema pode ser resolvido diretamente pegando a derivada de SE e definindo seu valor como zero:

 frac deltaSE( beta) delta beta=2XT( mathbfyX beta)=0

Assim, simplesmente encontramos seu mínimo, e SE é uma função quadrática, o que significa que o mínimo sempre existirá. Depois disso, você já pode encontrar facilmente  beta :

 hat beta=(XTX)1XT mathbfy

Isso é tudo, o problema está resolvido. É aqui que terminamos de lembrar o que é regressão linear.

Obviamente, a dependência inerente à natureza da geração de dados pode ser diferente e, então, alguma não linearidade já deve ser adicionada ao nosso modelo. Resolver o problema de regressão diretamente para dados grandes e reais também é uma má ideia, pois existe uma matriz XTX dimensões n vezesn , e ainda é necessário encontrar sua matriz inversa, e muitas vezes acontece que essa matriz simplesmente não existe. Nesse caso, vários métodos baseados na descida do gradiente vêm em nosso auxílio. A não linearidade dos modelos pode ser implementada de várias maneiras, incluindo o uso de redes neurais.

Mas agora, não vamos falar sobre isso, mas sobre funções de erro. Qual é a diferença entre SE e Probabilidade de log quando os dados podem ter um relacionamento não linear?

Lidamos com o zoológico, a saber: OLS, LS, SE, MSE, RSS
Tudo isso é o mesmo em essência: RSS - soma dos quadrados residuais, OLS - mínimos quadrados ordinários, LS - mínimos quadrados, MSE - erro ao quadrado médio, SE - erro ao quadrado. Em diferentes fontes, você pode encontrar nomes diferentes. A essência disso é apenas uma: desvio quadrático . Você pode se confundir, é claro, mas se acostuma rapidamente.

Vale ressaltar que MSE é o desvio padrão, um certo valor médio do erro para todo o conjunto de dados de treinamento. Na prática, o MSE é geralmente usado. A fórmula não é particularmente diferente:

MSE( beta)= frac1N sumni=1(yi hatyi)2

N - o tamanho do conjunto de dados,  hatyi - previsão de modelo para yi .

Pare com isso! Probabilidade? Isso é algo da teoria das probabilidades. Isso mesmo - é pura teoria das probabilidades. Mas como o desvio quadrático pode estar relacionado à função de probabilidade? E como acontece. Está relacionado à busca da máxima verossimilhança (máxima verossimilhança) e da distribuição normal, para ser mais preciso, com sua média  mu .

Para perceber que é assim, vejamos novamente a função de desvio quadrado:

RSS( beta)= sumni=1(yi hatyi)2 qquad qquad(1)

Agora, suponha que a função de probabilidade tenha uma forma normal, ou seja, uma distribuição gaussiana ou normal:

L(X)=p(X| theta)= prodX mathcalN(xi; mu, sigma2)

Em geral, qual é a função de probabilidade e qual é o significado dela, não vou contar; você pode ler sobre isso em outro lugar; também deve se familiarizar com o conceito de probabilidade condicional, o teorema de Bayes e muito mais, para uma compreensão mais profunda. Tudo isso entra na pura teoria da probabilidade, estudada tanto na escola quanto na universidade.

Agora, lembrando a fórmula de distribuição normal, obtemos:

L(X; mu, sigma2)= prodX frac1 sqrt2 pi sigma2e frac(xi mu)22 sigma2 qquad qquad(2)

E se colocarmos o desvio padrão  sigma2=1 e remova todas as constantes da fórmula (2), apenas remova, não reduza, porque encontrar o mínimo da função não depende delas. Então vamos ver isso:

L(X; mu, sigma2) sim prodXe(xi mu)2

Ainda nada como? Não? Bem, e se pegarmos o logaritmo da função? No logaritmo, existem algumas vantagens: a multiplicação se transformará em uma soma, um grau em multiplicação e  loge=1 - para esta propriedade, vale esclarecer que estamos falando do logaritmo natural e, estritamente falando  lne=1 . E, em geral, o logaritmo de uma função não altera seu máximo, e esse é o recurso mais importante para nós. A conexão com o Log-Likelihood e Likelihood e por que isso será útil será descrita abaixo em uma pequena digressão. E o que fizemos: removemos todas as constantes e assumimos o logaritmo da função de probabilidade. Eles também removeram o sinal de menos, transformando a Probabilidade de log em Probabilidade de log negativo (NLL); a conexão entre eles também será descrita como um bônus. Como resultado, obtivemos a função NLL:

 logL(X; mu,I2) sim sum(X mu)2

Veja novamente a função RSS (1). Sim, eles são iguais! Exatamente! Também é visto que  mu= haty .

Se você usa a função de desvio padrão do MSE, obtemos disso:

 nomedooperadorargminMSE( beta) sim nomedooperadorargmax mathbbEX simPdados logPmodelo(x; beta)

onde  mathbbE - expectativa matemática  beta - parâmetros do modelo, no futuro, iremos denotá-los como:  theta .

Conclusão: Se usarmos a família LS como função de erro na questão da regressão, resolveremos essencialmente o problema de encontrar a função de máxima verossimilhança no caso em que a distribuição é gaussiana. E o valor previsto  haty igual à média na distribuição normal. E agora sabemos como tudo isso está conectado, como a teoria das probabilidades está relacionada (com sua função de probabilidade e distribuição normal) e métodos de desvio padrão ou OLS. Mais detalhes sobre isso podem ser encontrados em [2].

E aqui está o bônus prometido. Como estamos falando das relações entre as várias funções de erro, consideraremos (não necessariamente para ser lido):

A relação entre entropia, verossimilhança, verossimilhança e verossimilhança negativas
Suponha que tenhamos dados X = \ {x_1, x_2, x_3, x_4, ... \}X = \ {x_1, x_2, x_3, x_4, ... \} , cada ponto pertence a uma classe específica, por exemplo \ {x_1 \ rightarrow1, x_2 \ rightarrow2, x_3 \ rightarrow n, ... \}\ {x_1 \ rightarrow1, x_2 \ rightarrow2, x_3 \ rightarrow n, ... \} . Total lá n enquanto a classe 1 ocorre c1 vezes, classe 2 - c2 horários e aula n - cn vezes. Sobre esses dados, treinamos algum modelo  theta . A função de probabilidade (Probabilidade) para ela será parecida com esta:

P(dados| theta)=P(0,1,...,n| theta)=P(0| theta)P(1| theta)...P(n| theta)

P(1| theta)P(2| theta)...P(n| theta)= prodc1 haty1 prodc2 haty2... prodcn hatyn= hatyc11 hatyc22... hatycnn


onde P(n| theta)= hatyn - probabilidade prevista para a classe n .

Pegamos o logaritmo da função de probabilidade e obtemos o Log-Likelihood:

 logP(dados| theta)= log( hatyc11... hatycnn)=c1 log haty1+...+cn log hatyn= sumnici log hatyi

Probabilidade  haty em[0,1] está no intervalo de 0 a 1, com base na definição de probabilidade. Portanto, o logaritmo terá um valor negativo. E se multiplicarmos a probabilidade do log por -1, obtemos a função Probabilidade do log negativo (NLL):

NLL= logP(dados| theta)= sumnici log hatyi

Se dividirmos a NLL pelo número de pontos em X , N=c1+c2+...+cn então temos:

 frac1N logP(dados| theta)= sumni fracciN log hatyi

pode-se notar que a probabilidade real para a classe n é igual a: yn= fraccnN . A partir daqui, obtemos:

NLL= sumniyi log hatyi

Agora, se você olhar para a definição de entropia cruzada H(p,q)= somap logq então temos:

NLL=H(yi, hatyi)

No caso em que temos apenas duas classes n=2 (classificação binária), obtemos a fórmula para entropia cruzada binária (você também pode encontrar o conhecido nome Log-Loss):

H(y, haty)=(y log haty+(1y) log(1 haty))

De tudo isso, pode-se entender que, em alguns casos, minimizar a entropia cruzada é equivalente a minimizar a NLL ou encontrar a função de máxima verossimilhança (probabilidade) ou log-verossimilhança.

Um exemplo Considere uma classificação binária. Temos valores de classe:

y = np.array([0, 1, 1, 1, 1, 0, 1, 1]).astype(np.float32) 

Probabilidade real y para a classe 0 é igual US $ 2/8 = US $ 0,25 , para a classe 1 é igual US $ 6/8 = US $ 0,75 . Suponha que tenhamos um classificador binário que prediz a probabilidade da classe 0  haty para cada exemplo, respectivamente, para a classe 1, a probabilidade é (1 haty) . Vamos traçar os valores da função Log-Loss para diferentes previsões  haty :


No gráfico, você pode ver que o mínimo da função Log-Loss corresponde ao ponto 0,75, ou seja, se nosso modelo "aprendeu" completamente a distribuição dos dados de origem,  haty=y .

Regressão de rede neural


Então chegamos a uma prática mais interessante. Vamos ver como você pode resolver o problema de regressão usando redes neurais (redes neurais). Implementaremos tudo na linguagem de programação Python. Para criar uma rede, usamos a biblioteca de aprendizado profundo do PyTorch.

Geração de dados de origem


Dados de entrada  mathbfX in mathbbRN gerar usando uma distribuição uniforme, leve o intervalo de -15 a 15,  mathbfX emU[15,15] . Pontos  mathbfY obtemos usando a equação:

 mathbfY=0,5 mathbfX+8 sin(0,3 mathbfX)+ruído qquad qquad(3)

onde noise É um vetor de ruído de dimensão N obtido usando a distribuição normal com parâmetros:  mu=0, sigma2=1 .

Geração de dados
 N = 3000 #   IN_DIM = 1 OUT_DIM = IN_DIM x = np.random.uniform(-15., 15., (IN_DIM, N)).T.astype(np.float32) noise = np.random.normal(size=(N, 1)).astype(np.float32) y = 0.5*x+ 8.*np.sin(0.3*x) + noise #  3 x_train, x_test, y_train, y_test = train_test_split(x, y) #      



O gráfico dos dados recebidos.

Construção de rede


Crie uma rede neural de avanço de alimentação regular ou FFNN.

Edifício FFNN
 class Net(nn.Module): def __init__(self, input_dim=IN_DIM, out_dim=OUT_DIM, layer_size=40): super(Net, self).__init__() self.fc = nn.Linear(input_dim, layer_size) self.logit = nn.Linear(layer_size, out_dim) def forward(self, x): x = F.tanh(self.fc(x)) #  4 x = self.logit(x) return x 


Nossa rede consiste em uma camada oculta com uma dimensão de 40 neurônios e com uma função de ativação - tangente hiperbólica:

 tanhx= fracexexex+ex qquad qquad(4)

A camada de saída é uma transformação linear normal sem uma função de ativação.

Aprendendo e obtendo resultados


Como otimizador, usaremos o AdamOptimizer. O número de épocas de estudo = 2000, a taxa de aprendizagem (taxa de aprendizagem ou lr) = 0,1.

Treinamento FFNN
 def train(net, x_train, y_train, x_test, y_test, epoches=2000, lr=0.1): criterion = nn.MSELoss() optimizer = optim.Adam(net.parameters(), lr=lr) N_EPOCHES = epoches BS = 1500 n_batches = int(np.ceil(x_train.shape[0] / BS)) train_losses = [] test_losses = [] for i in range(N_EPOCHES): for bi in range(n_batches): x_batch, y_batch = fetch_batch(x_train, y_train, bi, BS) x_train_var = Variable(torch.from_numpy(x_batch)) y_train_var = Variable(torch.from_numpy(y_batch)) optimizer.zero_grad() outputs = net(x_train_var) loss = criterion(outputs, y_train_var) loss.backward() optimizer.step() with torch.no_grad(): x_test_var = Variable(torch.from_numpy(x_test)) y_test_var = Variable(torch.from_numpy(y_test)) outputs = net(x_test_var) test_loss = criterion(outputs, y_test_var) test_losses.append(test_loss.item()) train_losses.append(loss.item()) if i%100 == 0: sys.stdout.write('\r Iter: %d, test loss: %.5f, train loss: %.5f' %(i, test_loss.item(), loss.item())) sys.stdout.flush() return train_losses, test_losses net = Net() train_losses, test_losses = train(net, x_train, y_train, x_test, y_test) 


Agora vamos ver os resultados da aprendizagem.


Gráfico dos valores da função MSE, dependendo da iteração do treinamento; gráfico de valores para dados de treinamento e dados de teste.


Resultados reais e previstos em dados de teste.

Dados invertidos


Nós complicamos a tarefa e invertemos os dados.

Inversão de dados
 x_train_inv = y_train y_train_inv = x_train x_test_inv = y_train y_test_inv = x_train 



Gráfico de dados invertidos.

Para previsão  mathbf hatY vamos usar a rede de distribuição direta da seção anterior e ver como ela lida com isso.

 inv_train_losses, inv_test_losses = train(net, x_train_inv, y_train_inv, x_test_inv, y_test_inv) 


Gráfico dos valores da função MSE, dependendo da iteração do treinamento; gráfico dos valores dos dados de treinamento e de teste.


Resultados reais e previstos em dados de teste.

Como você pode ver nos gráficos acima, nossa rede não lidou com esses dados, simplesmente não é capaz de prever. E tudo isso aconteceu porque em um problema tão invertido por um ponto x pode corresponder a vários pontos y . Você pergunta, e o barulho? Ele também criou uma situação em que, por um x poderia obter alguns valores y . Sim está certo. Mas o ponto principal é que, apesar do barulho, era tudo uma distribuição definida. E como nosso modelo previu essencialmente p(y|x) , e no caso de MSE, era o valor médio para a distribuição normal (por que é descrito na primeira parte do artigo), depois lidou bem com a tarefa "direta". Caso contrário, obteremos várias distribuições diferentes para um x e, portanto, não podemos obter um bom resultado com apenas uma distribuição normal.

Rede de densidade de mistura


A diversão começa! O que é rede de densidade de mistura (a seguir denominada rede MDN ou MD)? Em geral, este é um determinado modelo capaz de simular várias distribuições ao mesmo tempo:

p( mathbfy| mathbfx; theta)= sumKk pik( mathbfx) mathcalN( mathbfy; muk( mathbfx), sigma2( mathbfx)) qquad qquad(5)

Que fórmula estranha, você diz. Vamos descobrir. Nossa rede MD está aprendendo a modelar a média  mu e variação  sigma2 para várias distribuições. Na fórmula (5)  pik( mathbfx) - os chamados fatores de significância de uma distribuição separada para cada ponto xi in mathbfx , um determinado fator de mistura ou quanto cada uma das distribuições contribui para um determinado ponto. Total lá K distribuições.

Mais algumas palavras sobre  pik( mathbfx) - de fato, isso também é uma distribuição e representa a probabilidade de que, por um ponto xi in mathbfx será uma condição k .

Ah, novamente, essa matemática, já vamos escrever algo. E assim, começaremos a realizar uma rede. Para a nossa rede, tomamos K=30 .

 self.fc = nn.Linear(input_dim, layer_size) self.fc2 = nn.Linear(layer_size, 50) self.pi = nn.Linear(layer_size, coefs) self.mu = nn.Linear(layer_size, out_dim*coefs) # mean self.sigma_sq = nn.Linear(layer_size, coefs) # variance 

Defina as camadas de saída para nossa rede:

 x = F.relu(self.fc(x)) x = F.relu(self.fc2(x)) pi = F.softmax(self.pi(x), dim=1) sigma_sq = torch.exp(self.sigma_sq(x)) mu = self.mu(x) 

Escrevemos a função de erro ou função de perda, fórmula (5):

 def gaussian_pdf(x, mu, sigma_sq): return (1/torch.sqrt(2*np.pi*sigma_sq)) * torch.exp((-1/(2*sigma_sq)) * torch.norm((x-mu), 2, 1)**2) losses = Variable(torch.zeros(y.shape[0])) # p(y|x) for i in range(COEFS): likelihood = gaussian_pdf(y, mu[:, i*OUT_DIM:(i+1)*OUT_DIM], sigma_sq[:, i]) prior = pi[:, i] losses += prior * likelihood loss = torch.mean(-torch.log(losses)) 

Código de compilação MDN completo
 COEFS = 30 class MDN(nn.Module): def __init__(self, input_dim=IN_DIM, out_dim=OUT_DIM, layer_size=50, coefs=COEFS): super(MDN, self).__init__() self.fc = nn.Linear(input_dim, layer_size) self.fc2 = nn.Linear(layer_size, 50) self.pi = nn.Linear(layer_size, coefs) self.mu = nn.Linear(layer_size, out_dim*coefs) # mean self.sigma_sq = nn.Linear(layer_size, coefs) # variance self.out_dim = out_dim self.coefs = coefs def forward(self, x): x = F.relu(self.fc(x)) x = F.relu(self.fc2(x)) pi = F.softmax(self.pi(x), dim=1) sigma_sq = torch.exp(self.sigma_sq(x)) mu = self.mu(x) return pi, mu, sigma_sq #       def gaussian_pdf(x, mu, sigma_sq): return (1/torch.sqrt(2*np.pi*sigma_sq)) * torch.exp((-1/(2*sigma_sq)) * torch.norm((x-mu), 2, 1)**2) #   def loss_fn(y, pi, mu, sigma_sq): losses = Variable(torch.zeros(y.shape[0])) # p(y|x) for i in range(COEFS): likelihood = gaussian_pdf(y, mu[:, i*OUT_DIM:(i+1)*OUT_DIM], sigma_sq[:, i]) prior = pi[:, i] losses += prior * likelihood loss = torch.mean(-torch.log(losses)) return loss 


Nossa rede MD está pronta para funcionar. Quase pronto. Resta treiná-la e analisar os resultados.

Treinamento MDN
 def train_mdn(net, x_train, y_train, x_test, y_test, epoches=1000): optimizer = optim.Adam(net.parameters(), lr=0.01) N_EPOCHES = epoches BS = 1500 n_batches = int(np.ceil(x_train.shape[0] / BS)) train_losses = [] test_losses = [] for i in range(N_EPOCHES): for bi in range(n_batches): x_batch, y_batch = fetch_batch(x_train, y_train, bi, BS) x_train_var = Variable(torch.from_numpy(x_batch)) y_train_var = Variable(torch.from_numpy(y_batch)) optimizer.zero_grad() pi, mu, sigma_sq = net(x_train_var) loss = loss_fn(y_train_var, pi, mu, sigma_sq) loss.backward() optimizer.step() with torch.no_grad(): if i%10 == 0: x_test_var = Variable(torch.from_numpy(x_test)) y_test_var = Variable(torch.from_numpy(y_test)) pi, mu, sigma_sq = net(x_test_var) test_loss = loss_fn(y_test_var, pi, mu, sigma_sq) train_losses.append(loss.item()) test_losses.append(test_loss.item()) sys.stdout.write('\r Iter: %d, test loss: %.5f, train loss: %.5f' %(i, test_loss.item(), loss.item())) sys.stdout.flush() return train_losses, test_losses mdn_net = MDN() mdn_train_losses, mdn_test_losses = train_mdn(mdn_net, x_train_inv, y_train_inv, x_test_inv, y_test_inv) 



O gráfico dos valores da função de perda dependendo da iteração do treinamento; o gráfico de valores para dados de treinamento e dados de teste.

Como nossa rede aprendeu os valores médios para várias distribuições, vejamos o seguinte:

 pi, mu, sigma_sq = mdn_net(Variable(torch.from_numpy(x_test_inv))) 


Gráfico para os dois valores médios mais prováveis ​​para cada ponto (esquerda). Gráfico para os 4 valores médios mais prováveis ​​para cada ponto (à direita).


Gráfico para todos os valores médios para cada ponto.

Para prever dados, selecionaremos aleatoriamente vários valores  mu e  sigma2 com base no valor  pik( mathbfx) . E então, com base neles, para gerar dados de destino  haty usando distribuição normal.

Previsão de resultado
 def rand_n_sample_cumulative(pi, mu, sigmasq, samples=10): n = pi.shape[0] out = Variable(torch.zeros(n, samples, OUT_DIM)) for i in range(n): for j in range(samples): u = np.random.uniform() prob_sum = 0 for k in range(COEFS): prob_sum += pi.data[i, k] if u < prob_sum: for od in range(OUT_DIM): sample = np.random.normal(mu.data[i, k*OUT_DIM+od], np.sqrt(sigmasq.data[i, k])) out[i, j, od] = sample break return out pi, mu, sigma_sq = mdn_net(Variable(torch.from_numpy(x_test_inv))) preds = rand_n_sample_cumulative(pi, mu, sigma_sq, samples=10) 


Dados previstos para 10 valores selecionados aleatoriamente  mu e  sigma2 (esquerda) e para dois (direita).

Pode-se ver pelas figuras que a MDN fez um excelente trabalho com a tarefa "inversa".

Usando dados mais complexos


Vamos ver como nossa rede MD lida com dados mais complexos, como dados em espiral. A equação da espiral hiperbólica nas coordenadas cartesianas:

x= rho cos phi qquad qquad qquad qquad qquad qquad(6)y= rho sin phi

Geração de dados em espiral
 N = 2000 x_train_compl = [] y_train_compl = [] x_test_compl = [] y_test_compl = [] noise_train = np.random.uniform(-1, 1, (N, IN_DIM)).astype(np.float32) noise_test = np.random.uniform(-1, 1, (N, IN_DIM)).astype(np.float32) for i, theta in enumerate(np.linspace(0, 5*np.pi, N).astype(np.float32)): #  6 r = ((theta)) x_train_compl.append(r*np.cos(theta) + noise_train[i]) y_train_compl.append(r*np.sin(theta)) x_test_compl.append(r*np.cos(theta) + noise_test[i]) y_test_compl.append(r*np.sin(theta)) x_train_compl = np.array(x_train_compl).reshape((-1, 1)) y_train_compl = np.array(y_train_compl).reshape((-1, 1)) x_test_compl = np.array(x_test_compl).reshape((-1, 1)) y_test_compl = np.array(y_test_compl).reshape((-1, 1)) 



Gráfico de dados em espiral.

Por diversão, vamos ver como uma rede Feed-Forward regular lidará com essa tarefa.


Como esperado, a rede Feed-Forward não pode resolver o problema de regressão para esses dados.

Utilizamos a rede MD descrita e criada anteriormente para treinamento em dados em espiral.


A Rede de densidade da mistura fez um ótimo trabalho nessa situação.

Conclusão


No início deste artigo, lembramos os princípios básicos da regressão linear. Vimos isso em comum entre encontrar a média para a distribuição normal e MSE. Desmontado como conectado NLL e entropia cruzada. E o mais importante, descobrimos o modelo MDN, que é capaz de aprender com os dados obtidos de uma distribuição mista. Espero que o artigo seja compreensível e interessante, apesar de haver um pouco de matemática.

O código completo pode ser visualizado no GitHub .


Literatura


  1. Redes de densidade de mistura (Christopher M. Bishop, Grupo de Pesquisa em Computação Neural, Departamento de Ciência da Computação e Matemática Aplicada, Universidade de Aston, Birmingham) - o artigo descreve completamente a teoria das redes de MD.
  2. Mínimos quadrados e máxima verossimilhança (MROsborne)

Source: https://habr.com/ru/post/pt433804/


All Articles