Programação Probabilística
=====
e Métodos Bayesianos para Hackers
========

##### Versão 0.1

`Conteúdo original criado por Cam Davidson-Pilon`

`Transferido para Python 3 e PyMC3 por Max Margenot (@clean_utensils) e Thomas Wiecki (@twiecki) em Quantopian (@quantopian)`
__

Bem-vindo a *Métodos Bayesianos para Hackers*. O repositório Github completo está disponível em [github/Probabilistic-Programming-and-Bayesian-Methods-for-Hackers](https://github.com/CamDavidsonPilon/Probabilistic-Programming-and-Bayesian-Methods-for-Hackers). Os outros capítulos podem ser encontrados em [homepage](https://camdavidsonpilon.github.io/Probabilistic-Programming-and-Bayesian-Methods-for-Hackers/). Esperamos que você goste do livro e incentivamos qualquer contribuição!
___

Versão pt_BR

Tradução por Rodolpho Macedo dos Santos disponibilizado em [Probabilistic-Programming-and-Bayesian-Methods-for-Hackers](http://github.com/rodolphomacedo/Probabilistic-Programming-and-Bayesian-Methods-for-Hackers)

Capítulo 1
======
***

A filosofia da inferência bayesiana
------

> Você é um programador habilidoso, mas ainda assim existem bugs em seu código. Após uma implementação particularmente difícil de um algoritmo, você decide testar seu código em um exemplo trivial. Ele roda. Você testa o código em um problema mais difícil. Isso roda mais uma vez. E roda no próximo, *ainda mais difícil*, roda também! Você está começando a acreditar que pode não ter bugs no código ...

Se você pensa assim, então parabéns, você já está pensando bayesiano! A inferência bayesiana é simplesmente atualizar suas crenças após considerar novas evidências. Um bayesiano raramente pode ter certeza sobre o resultado, mas pode estar muito confiante. Assim como no exemplo acima, nunca podemos ter 100% de certeza de que nosso código está livre de bugs, a menos que o testemos em todos os problemas possíveis; algo raramente possível na prática. Em vez disso, podemos testá-lo em um grande número de problemas e, se for bem-sucedido, podemos nos sentir mais *confiantes* sobre nosso código, mas ainda não temos certeza. A inferência bayesiana funciona de forma idêntica: atualizamos nossas crenças sobre um resultado; raramente podemos ter certeza absoluta, a menos que excluamos todas as outras alternativas.

### O estado de espírito bayesiano

A inferência bayesiana difere da inferência estatística mais tradicional por preservar a *incerteza*. A princípio, isso soa como uma técnica estatística ruim. As estatísticas não são apenas derivar *certeza* da aleatoriedade? Para reconciliar isso, precisamos começar a pensar como bayesianos.

A visão de mundo bayesiana interpreta a probabilidade como uma medida de *credibilidade em um evento*, ou seja, o quão confiantes estamos na ocorrência de um evento. Na verdade, veremos em um momento que esta é a interpretação natural da probabilidade.

Para que isso fique mais claro, consideramos uma interpretação alternativa da probabilidade: *Frequentista* (ou *Frequencista*), conhecida como a versão mais *clássica* da estatística, assume que a probabilidade é a frequência relativa de eventos no longo prazo (daí o título concedido). Por exemplo, a *probabilidade de acidentes de avião* sob uma filosofia frequentista é interpretada como a *frequência relativa no longo prazo de acidentes de avião*. Isso faz sentido lógico para muitas probabilidades de eventos, mas se torna mais difícil de entender quando os eventos não têm a frequência de ocorrência de longo prazo. Considere: muitas vezes atribuímos probabilidades aos resultados das eleições presidenciais, mas a eleição em si só acontece uma vez! Os frequentistas contornam isso invocando realidades alternativas e dizendo que, em todas essas realidades, a freqüência das ocorrências define a probabilidade.

Os bayesianos, por outro lado, têm uma abordagem mais intuitiva. Os bayesianos interpretam uma probabilidade como uma medida de *crença*, ou a confiança, da ocorrência de um evento. Simplesmente, a probabilidade é um resumo de uma opinião. Um indivíduo que atribui uma crença de $0$ a um evento não tem confiança de que o evento ocorrerá; inversamente, atribuir uma crença de $1$ implica que o indivíduo está absolutamente certo da ocorrência de um evento. Crenças entre $0$ e $1$ permitem ponderações para outros resultados. Esta definição está de acordo com a probabilidade do exemplo do acidente de avião, por termos observado a frequência dos acidentes de avião, a crença de um indivíduo deve ser igual a essa frequência, excluindo qualquer informação externa. Da mesma forma, sob esta definição de probabilidade igualmente a crenças, é significativo dizer que a probabilidade (crença) do resultado das eleições presidenciais: o quão confiante você está, o candidato *A* irá vencer?

Observe no parágrafo acima, atribuímos a medida de crença (probabilidade) a um *indivíduo*, não à Natureza. Isso é muito interessante, pois essa definição abre espaço para crenças conflitantes entre os indivíduos. Novamente, isso é apropriado para o que ocorre naturalmente: diferentes indivíduos têm diferentes crenças sobre os eventos que ocorrem, porque possuem diferentes *informações* sobre o mundo. A existência de diferentes crenças não significa que alguém esteja errado. Considere os seguintes exemplos que demonstram a relação entre crenças e probabilidades individuais:

- Lanço uma moeda e ambos adivinhamos o resultado. Ambos concordaríamos, supondo que a moeda seja justa, que a probabilidade de cara é $\frac{1}{2}$. Suponha, então, que eu espie a moeda. Agora eu sei com certeza qual é o resultado: eu atribuo probabilidade $1.0$ a cara ou coroa (seja qual for). Agora, qual é a *sua* crença de que a moeda é cara? Meu conhecimento do resultado não mudou os resultados da moeda. Assim, atribuímos diferentes probabilidades ao resultado.

- Seu código tem um bug ou não, mas não sabemos com certeza qual afirmação é verdadeira, embora tenhamos uma crença sobre a presença ou ausência de um bug.

- Um paciente médico está exibindo os sintomas $x$, $y$ e $z$. E existem várias doenças que poderiam estar causando todos esses sintomas, mas apenas uma doença está presente. Um médico tem crenças sobre qual doença, mas um segundo médico pode ter crenças ligeiramente diferentes.

Essa filosofia da abordagem de crenças como probabilidade é natural para os humanos. Nós a utilizamos constantemente à medida que interagimos com o mundo e vemos apenas verdades parciais, mas reunimos evidências para formar as crenças. Alternativamente, você deve ser *treinado* para pensar como um frequentista.

Para nos alinhar com a notação de probabilidade tradicional, denotamos nossa crença sobre o evento $A$ como $P(A)$. Chamamos essa quantidade de *probabilidade a priori*.

John Maynard Keynes, um grande economista e pensador, disse: "Quando os fatos mudam, eu mudo de ideia. O que você faz, senhor?" Esta citação reflete a maneira como um bayesiano atualiza suas crenças depois de ver as evidências. Mesmo — especialmente — se a evidência for contrária ao que se acreditava inicialmente, a evidência não pode ser ignorada. Denotamos nossa crença atualizada como $P(A | X)$, interpretada como a probabilidade de $A$ dada a evidência $X$. Chamamos de atualização da crença  da *probabilidade posteriori* para contrastá-la com a probabilidade a priori. Por exemplo, considere as probabilidades a posteriori (leia-se: crenças a posteriori) dos exemplos acima, após observar algumas evidências $X$:

1\. $P(A): \;\;$ a moeda tem 50 por cento de chance de ser cara. $P(A | X):\;\;$ Voce olha para a moeda,
observa que caiu Cara, denote essa informação $X$ e atribua trivialmente a probabilidade $1.0$ para Cara e $0.0$ para Coroa.

2\.   $P(A): \;\;$ Este é um código grande e complexo, provavelmente contém um bug. $P(A | X): \;\;$ O código passou em todos os testes $X$; ainda pode haver um bug, mas sua presença é menos provável agora.

3\.  $P(A):\;\;$ O paciente pode ter muitas doenças. $P(A | X):\;\;$  Realização de um teste de sangue gerou evidências $X$, descartando algumas das possíveis doenças de consideração.

É claro que em cada exemplo não descartamos completamente a crença a priori depois de ver a nova evidência $X$, mas *reponderamos a priori* para incorporar a nova evidência (ou seja, colocamos mais peso ou confiança em algumas crenças versus outras).

Ao introduzir a incerteza a priori sobre os eventos, já estamos admitindo que qualquer suposição que fizermos é potencialmente muito errada. Depois de observar dados, evidências ou outras informações, atualizamos nossas crenças e nosso palpite de tal modo a se tornar *menos errada*. Este é o lado alternativo da previsão de uma moeda, onde normalmente tentamos estar *mais certos*.

### Inferência Bayesiana na Prática

Se a inferência frequentista e bayesiana fossem funções de programação, com as entradas sendo problemas estatísticos, então as duas seriam diferentes no que retornam ao usuário. A função de inferência frequentista retornaria um número, representando uma estimativa (normalmente uma estatística de resumo como a média da amostra etc.), enquanto a função Bayesiana retornaria as *probabilidades*.

Por exemplo, em nosso problema de debugging acima, chamamos a função frequentista com o argumento "Meu código passou em todos os testes $X$; meu código está livre de erros?" retornaria um *SIM*. Por outro lado, perguntando à nossa função Bayesiana "Muitas vezes meu código tem bugs. Meu código passou em todos os testes $X$; meu código está livre de bugs?" retornaria algo muito diferente: probabilidades de *SIM* e de *NÃO*. A função poderia retornar:

>    *SIM*, com probabilidade de 0,8; *NÃO*, com probabilidade de 0,2

Isso é muito diferente da resposta que a função frequentista retornou. Observe que a função Bayesiana aceita um argumento adicional: *"Freqüentemente meu código tem bugs" *. Este parâmetro é o *a priori*. Ao incluir o parâmetro a priori, estamos dizendo à função bayesiana para incluir nossa crença sobre a situação. Tecnicamente, este parâmetro na função bayesiana é opcional, mas veremos que excluí-lo terá suas consequências.

#### Incorporando evidências

À medida que adquirimos mais e mais exemplos de evidências, nossa crença a priori é *apagada* pelas novas evidências. Isto é o esperado. Por exemplo, se sua crença a priori é algo ridículo, como "Espero que o sol exploda hoje", e a cada dia que você estiver errado, você esperaria que qualquer inferência a corrigisse, ou pelo menos alinhasse melhor suas crenças. A inferência bayesiana corrigirá essa crença.

Denote $N$ como o número de evidências que possuímos. À medida que reunimos uma quantidade *infinita* de evidência, digamos como $N \rightarrow \infty$, nossos resultados bayesianos (frequentemente) se alinham aos resultados frequentistas. Portanto, para $N$ grandes, a inferência estatística é mais ou menos objetiva. Por outro lado, para $N$ pequenos, a inferência é muito mais *instável*: as estimativas frequentistas têm mais variância e intervalos de confiança maiores. É aqui que a análise bayesiana se destaca. Ao introduzir uma probabilidade a priori e retornar (em vez de uma estimativa escalar), *preservamos a incerteza* que reflete a instabilidade da inferência estatística de um pequeno conjunto de dados $N$.

Pode-se pensar que para $N$ grandes, pode-se ficar indiferente entre as duas técnicas, visto que elas oferecem inferência semelhante, e pode-se inclinar para os métodos frequentistas mais simples computacionalmente. Um indivíduo nesta posição deve considerar a seguinte citação de Andrew Gelman (2005)[1], antes de tomar tal decisão:

> Os tamanhos das amostras nunca são grandes. Se $N$ for muito pequeno para obter uma estimativa suficientemente precisa, você precisará obter mais dados (ou fazer mais suposições). Mas uma vez que $N$ é "grande o suficiente", você pode começar a subdividir os dados para aprender mais (por exemplo, em uma pesquisa de opinião pública, depois de ter uma boa estimativa para todo o país, você pode estimar entre homens e mulheres, pessoas do norte e pessoas do sul, diferentes faixas etárias, etc.). $N$ nunca será suficiente porque se fosse "suficiente" você já estaria próximo do problema para o qual precisa de mais dados.

### Os métodos frequentistas estão incorretos então?

**Não.**

Os métodos freqüentistas ainda são úteis ou de última geração em muitas áreas. Ferramentas como regressão linear de mínimos quadrados, regressão LASSO e algoritmos de maximização de expectativa são poderosos e rápidos. Os métodos bayesianos complementam essas técnicas resolvendo problemas que essas abordagens não conseguem ou iluminando o sistema subjacente com uma modelagem mais flexível.

#### Uma nota sobre *Big Data*
Paradoxalmente, os problemas analíticos preditivos de big data são resolvidos por algoritmos relativamente simples [2][4]. Assim, podemos argumentar que a dificuldade de previsão de big data não está no algoritmo usado, mas sim nas dificuldades computacionais de armazenamento e execução em grande conjuntos de dados. (Também se deve considerar a citação de Gelman acima e perguntar "Eu realmente tenho cenário de big data?")

Os problemas analíticos muito mais difíceis envolvem *dados médios* e, especialmente problemáticos, *dados realmente pequenos*. Usando um argumento semelhante ao do Gelman citado acima, se os problemas de big data forem *grandes o suficiente* para serem prontamente resolvidos, então, deveriamos estar muito mais interessados nos conjuntos de dados *não muito grandes*.

### Nossa Estrutura Bayesiana

Estamos interessados em crenças, que podem ser interpretadas como probabilidades pelo pensamento bayesiano. Temos uma crença *a priori* do evento $A$, essas crenças foram formadas por informações anteriores, por exemplo, nossa crença a priori sobre os bugs encontrados em nosso código antes de realizar os testes.

Em segundo lugar, observamos nossas evidências. Para continuar em nosso exemplo do código com bugs: se nosso código passar nos testes $X$, então queremos atualizar nossa crença com essa nova informação. Chamamos essa nova crença de probabilidade *a posteriori*. A atualização de nossa crença é feita por meio da seguinte equação, conhecida como Teorema de Bayes, em homenagem a seu descobridor Thomas Bayes:

\begin{align}
 P( A | X )\;\ =\;\ & \frac{ P(X | A) P(A) } {P(X) } \\\\[5pt]
& \propto P(X | A) P(A)\;\; (\propto \text{é proporcional à })
\end{align}

A fórmula acima não é exclusiva da inferência bayesiana: é um fato matemático com usos fora da inferência bayesiana. A inferência bayesiana apenas a utiliza para conectar as probabilidades a priori $P(A)$ com as probabilidades a posteriori atualizadas $P(A | X)$.

##### Exemplo:  obrigatório exemplo de cara ou coroa

Todo texto de estatísticas deve conter um exemplo de cara ou coroa, vou usá-lo aqui para tirá-lo do caminho. Suponha que, ingenuamente, você não tem certeza sobre a probabilidade de cara em um cara ou coroa (alerta de spoiler: é 50%). Você acredita que existe algum índice subjacente verdadeiro, chame-o de $p$, mas não tem opinião prévia sobre o que $p$ pode ser.

Começamos a jogar uma moeda e registramos as observações: $H$ ou $T$. Estes são os nossos dados observados. Uma pergunta interessante a se fazer é como nossa inferência muda à medida que observamos mais e mais dados? Mais especificamente, como são nossas probabilidades a posteriori quando temos poucos dados, em comparação com quando temos muitos dados.

Abaixo, traçamos uma seqüência de atualização das probabilidades a posteriori à medida que observamos quantidades crescentes de dados (cara ou coroa).

```python
"""
O livro usa um arquivo matplotlibrc personalizado, que fornece estilos exclusivos para
gráficos de matplotlib. Se estiver executando este livro, e você deseja usar o
estilo do livro, são fornecidas duas opções:
     1. Substitua seu próprio arquivo matplotlibrc com o arquivo rc fornecido no
        styles/dir do livro. Veja http://matplotlib.org/users/customizing.html
     2. Também nos estilos está o arquivo bmh_matplotlibrc.json. Isso pode ser usado para
        atualizar os estilos apenas neste bloco de notas. Tente executar o seguinte código:

        import json
        s = json.load(open("../styles/bmh_matplotlibrc.json"))
        matplotlib.rcParams.update(s)

"""
# O código abaixo pode ser ignorado, pois atualmente não é importante, mais ele
# usa tópicos avançados que ainda não cobrimos. OLHE A FOTO, MICHAEL!

%matplotlib inline
from IPython.core.pylabtools import figsize
import numpy as np
from matplotlib import pyplot as plt
figsize(11, 9)

import scipy.stats as stats

dist = stats.beta
n_trials = [0, 1, 2, 3, 4, 5, 8, 15, 50, 500]
data = stats.bernoulli.rvs(0.5, size=n_trials[-1])
x = np.linspace(0, 1, 100)

# Para os já preparados, estou usando o conjugação a priori da Binomial.
for k, N in enumerate(n_trials):
    sx = plt.subplot(int(len(n_trials)/2), 2, k+1)
    plt.xlabel("$p$, probabilidade de cara") \
        if k in [0, len(n_trials)-1] else None
    plt.setp(sx.get_yticklabels(), visible=False)
    heads = data[:N].sum()
    y = dist.pdf(x, 1 + heads, 1 + N - heads)
    if heads != 1:
        plt.plot(x, y, label="observando %d lançamentos,\n %d caras" % (N, heads))
    else:
        plt.plot(x, y, label="observando %d lançamentos,\n %d cara" % (N, heads))
    plt.fill_between(x, 0, y, color="#348ABD", alpha=0.4)
    plt.vlines(0.5, 0, 4, color="k", linestyles="--", lw=1)

    leg = plt.legend()
    leg.get_frame().set_alpha(0.4)
    plt.autoscale(tight=True)

plt.suptitle("Atualização bayesiana das probabilidades a posteriori",
             y=1.02,
             fontsize=14)

plt.tight_layout()
```

As probabilidades a posteriori são representadas pelas curvas, e nossa incerteza é proporcional à largura da curva. Como mostra o gráfico acima, conforme começamos a observar os dados, nossas probabilidades a posteriori começam a se deslocar e se mover. Eventualmente, conforme observamos mais e mais dados (caras ou coroas), nossas probabilidades ficarão cada vez mais estreitas em torno do valor real de $p = 0.5$ (marcado por uma linha tracejada).

Observe que os gráficos nem sempre têm *picos* em 0,5. Não há motivo para isso: lembre-se de que presumimos que não tínhamos uma opinião a priori sobre qual é o valor de $p$. Na verdade, se observarmos dados bastante extremos, digamos 8 lançamentos e apenas 1 cara observada, nossa distribuição pareceria muito tendenciosa *longe* de agrupar em torno de $0.5$ (sem a opinião a priori, quão confiante você se sentiria ao apostar em uma moeda justa após observar 8 coroas e 1 cara?). À medida que mais dados se acumulam, veríamos mais e mais a probabilidade sendo atribuída a $p = 0.5$, embora nunca temos toda ela.

O próximo exemplo é uma demonstração simples da matemática da inferência bayesiana.

##### Exemplo: Bug, ou apenas um recurso agradável e não intencional?

Seja $A$ o evento em que nosso código **não contém  bugs**. Seja $X$ o evento em que o código passa em todos os testes de depuração. Por enquanto, deixaremos a probabilidade a priori de nenhum bug como variável, ou seja, $P(A) = p$.

Estamos interessados em $P(A|X)$, ou seja, a probabilidade de nenhum bug, dados nossos testes de debugging $X$. Para usar a fórmula acima, precisamos calcular algumas quantidades.

O que é $P(X|A)$, ou seja, a probabilidade de que o código passe nos testes $X$ *dado* que não haja bugs? Bem, é igual a 1, pois um código sem bugs passará em todos os testes.

$P(X)$ é um pouco mais complicado: o evento $X$ pode ser dividido em duas possibilidades, o evento $X$ ocorrendo mesmo que nosso código *realmente tenha* bugs (denotado $\sim A\;$, dito *não $A$*), ou evento $X$ sem bugs ($A$).

Assim, $P(X)$ pode ser representado como:

\begin{align}
P(X ) & = P(X \text{ e } A) + P(X \text{ e } \sim A) \\\\[5pt]
 & = P(X|A)P(A) + P(X | \sim A)P(\sim A)\\\\[5pt]
& = P(X|A)p + P(X | \sim A)(1-p)
\end{align}

Já calculamos $P(X|A)$ acima ( $=1$). Por outro lado, $ P(X|\sim A)$ é subjetivo: nosso código pode passar nos testes, mas ainda tem um bug nele, embora a probabilidade de haver um bug seja reduzida. Observe que isso depende do número de testes realizados, do grau de complicação nos testes, etc. Vamos ser conservadores e atribuir $P(X|\sim A) = 0,5$. Então:

\begin{align}
P(A | X) & = \frac{1\cdot p}{ 1\cdot p +0.5 (1-p) } \\\\
& = \frac{p}{ p + 0.5-0.5  p } \\\\
& = \frac{p}{ 0.5 + 0.5 p } \\\\
& = \frac{p}{ 0.5 \cdot ( 1 + p )} \\\\
& = \frac{ 2 p}{1+p}
\end{align}

Esta é a probabilidade a posteriori. A qual se parece como a nossa função a priori, $p \in [0,1]$?

```python
figsize(12.5, 4)
p = np.linspace(0, 1, 50)
plt.plot(p, 2*p/(1+p), color="#348ABD", lw=3)
#plt.fill_between(p, 2*p/(1+p), alpha=.5, facecolor=["#A60628"])
plt.scatter(0.2, 2*(0.2)/1.2, s=140, c="#348ABD")
plt.xlim(0, 1)
plt.ylim(0, 1)
plt.xlabel("Priori, $P(A) = p$")
plt.ylabel("Posteriori, $P(A|X)$, com $P(A) = p$")
plt.title("Existem bugs no meu código?");
```

Podemos ver os maiores ganhos se observarmos os testes $X$ que passaram quando a probabilidade a priori, $p$, é baixa. Vamos definir um valor específico para a priori. Sou um programador bom (eu acho), então vou me dar uma ideia realista a priori de 0.20, ou seja, há 20% de chance de escrever um código sem bugs. Para ser mais realista, essa prioridade deve ser uma função de quão complicado e grande é o código, mas vamos fixá-lo em 0.20. Então, minha crença atualizada de que meu código está livre de bugs é de 0,33.

Lembre-se de que a priori é uma probabilidade: $p$ é a probabilidade a priori de *não haver bugs*, então $1-p$ é a probabilidade a priori de *ter bugs* no código.

Da mesma forma, nossa posteriori também é uma probabilidade, com $P(A|X)$ a probabilidade de não haver bug *dado que vimos que todos os testes passaram*, portanto $1 - P(A|X)$ é a probabilidade de haver um bug *considerando que todos os testes passaram*. Qual é a nossa probabilidade a posteriori? Abaixo está um gráfico das probabilidades a priori e a posteriori.

```python
figsize(12.5, 4)
colours = ["#348ABD", "#A60628"]

prior = [0.20, 0.80]
posterior = [1./3, 2./3]
# posterior = [(2*0.2)/(1+0.2),1-(2*0.2)/(1+0.2)]

plt.bar([0, .7], prior, alpha=0.70, width=0.25,
        color=colours[0], label="Distribuição a Priori",
        lw="3", edgecolor=colours[0])

plt.bar([0+0.25, .7+0.25], posterior, alpha=0.7,
        width=0.25, color=colours[1],
        label="Distribuição a Posteriori",
        lw="3", edgecolor=colours[1])

plt.xticks([0.20, .95], ["Bugs Ausentes", "Bugs Presentes"])
plt.title("Probabilidade de bugs presentes  - Priori e Posteriori")
plt.ylabel("Probabildade")
plt.legend(loc="upper left");
```

Observe que, depois que observamos a ocorrência de $X$, a probabilidade dos bugs estarem ausentes aumentou. Aumentando o número de testes, podemos nos aproximar da confiança (probabilidade 1) de que não há bugs presentes.

Este foi um exemplo muito simples de inferência bayesiana e da regra de Bayes. Infelizmente, a matemática necessária para realizar inferências bayesianas mais complicadas apenas se torna mais difícil, exceto para casos construídos artificialmente. Veremos mais tarde que esse tipo de análise matemática é realmente desnecessária. Primeiro, devemos ampliar nossas ferramentas de modelagem. A próxima seção trata das *distribuições de probabilidades*. Se você já estiver familiarizado, sinta-se à vontade para pular (ou pelo menos dar uma olhada), mas para os menos familiarizados, a próxima seção é essencial.

_______

##Distribuições de Probabilidades

**Vamos lembrar rapidamente o que é uma distribuição de probabilidade:** Seja $Z$ alguma variável aleatória. Então, associada a $Z$ está uma *função de distribuição de probabilidade* que atribui probabilidades aos diferentes resultados que $Z$ pode obter. Graficamente, uma distribuição de probabilidade é uma curva em que a probabilidade de um resultado é proporcional à altura da curva. Você pode ver exemplos na primeira figura deste capítulo.

Podemos dividir as variáveis aleatórias em três classificações:

- **$Z$ é discreto**: Variáveis aleatórias discretas só podem assumir valores em uma lista especificada. Coisas como populações, classificações de filmes e número de votos são variáveis aleatórias discretas. Variáveis aleatórias discretas tornam-se mais claras quando as contrastamos com ...

- **$Z$ é contínuo**: a variável aleatória contínua pode assumir valores exatos arbitrariamente. Por exemplo, temperatura, velocidade, tempo, cor são modelados como variáveis contínuas porque você pode tornar os valores cada vez mais precisos.

- **$Z$ é misto**: as variáveis aleatórias mistas atribuem probabilidades a variáveis aleatórias discretas e contínuas, ou seja, é uma combinação das duas categorias acima.

### Caso Discreto

Se $Z$ for discreto, então sua distribuição é chamada de *função de massa de probabilidade*, que mede a probabilidade de $Z$ assumir o valor $k$, denotado $P(Z = k)$. Observe que a função de massa de probabilidade descreve completamente a variável aleatória $Z$, ou seja, se conhecemos a função de massa, sabemos como $Z$ deve se comportar. Existem funções de massa de probabilidade populares que aparecem de forma consistente: iremos apresentá-las conforme necessário, mas vamos introduzir a primeira função de massa de probabilidade muito útil.

Dizemos que a variável $Z$ é *Poisson*-distribuída se:

$$P(Z = k) =\frac{ \lambda^k e^{-\lambda} }{k!}, \; \; k=0,1,2, \dots $$

$\lambda$ é chamado de parâmetro da distribuição e controla a forma da distribuição. Para a distribuição de Poisson, $\lambda$ pode ser qualquer número positivo. Aumentando $\lambda$, estamos adicionamos mais probabilidade a valores maiores e, inversamente, diminuindo $\lambda$, estamos adicionamos mais probabilidade a valores menores. Pode-se descrever $\lambda$ como a *intensidade* da distribuição de Poisson.

Ao contrário de $\lambda$, que pode ser qualquer número positivo, o valor $k$ na fórmula acima deve ser um número inteiro não negativo, ou seja, $k$ deve assumir os valores 0,1,2 e assim por diante. Isso é muito importante, porque se você quisesse modelar uma população, não conseguiria entender as populações com 4,25 ou 5,612 indivíduos.

Se a variável aleatória $z$ tem a distribuição de massa de uma Poisson, nós denotamos isso escrevendo:

$$Z \sim \text{Poi}(\lambda) $$

Uma propriedade útil da distribuição de Poisson é que seu valor esperado é igual ao seu parâmetro, ou seja:

$$E\large[ \;Z\; | \; \lambda \;\large] = \lambda $$

Usaremos essa propriedade com frequência, por isso é útil lembrar. Abaixo, traçamos a distribuição de massa de probabilidade para diferentes valores de $\lambda$. A primeira coisa a notar é que ao aumentar $\lambda$, adicionamos mais probabilidade de ocorrência de valores maiores. Em segundo lugar, observe que embora o gráfico termine em 15, as distribuições não. Eles atribuem probabilidade positiva a cada número inteiro não negativo.

```python
figsize(12.5, 4)

import scipy.stats as stats
a = np.arange(16)
poi = stats.poisson
lambda_ = [1.5, 4.25]
colours = ["#348ABD", "#A60628"]

plt.bar(a, poi.pmf(a, lambda_[0]), color=colours[0],
        label="$\lambda = %.1f$" % lambda_[0], alpha=0.60,
        edgecolor=colours[0], lw="3")

plt.bar(a, poi.pmf(a, lambda_[1]), color=colours[1],
        label="$\lambda = %.1f$" % lambda_[1], alpha=0.60,
        edgecolor=colours[1], lw="3")

plt.xticks(a + 0.4, a)
plt.legend()
plt.ylabel("Probabilidade de $k$")
plt.xlabel("$k$")
plt.title("""Função de massa de probabilidade de uma variável aleatória de Poisson;
\n Com diferentes valores para $\lambda$!""");
```

### Caso Contínuo

Em vez de uma função de massa de probabilidade, agora temos uma variável aleatória contínua que tem uma *função de densidade de probabilidade*. Isso pode parecer uma nomenclatura desnecessária, mas a função de densidade e a função de massa são criaturas muito diferentes. Um exemplo de variável aleatória contínua é uma variável aleatória com *densidade exponencial*. A função de densidade para uma variável aleatória exponencial se parece com isto:

$$f_Z(z | \lambda) = \lambda e^{-\lambda z }, \;\; z\ge 0$$

Como uma variável aleatória de Poisson, uma variável aleatória Exponencial pode assumir apenas valores não negativos. Mas, ao contrário de uma variável de Poisson, a Exponencial pode assumir *quaisquer* valores não negativos, incluindo valores não integrais, como 4,25 ou 5,612401. Esta propriedade a torna uma escolha ruim para dados de contagem, que devem ser um número inteiro, mas uma ótima escolha para dados de tempo, dados de temperatura (medidos em Kelvins, é claro) ou qualquer outra variável precisa *e positiva*. O gráfico abaixo mostra duas funções de densidade de probabilidade com diferentes valores $\lambda$.

Quando uma variável aleatória $Z$ tem uma distribuição Exponencial com o parâmetro $\lambda$, dizemos *$Z$ é (distribuído conforme) Exponencial* e escrevemos:

$$Z \sim \text{Exp}(\lambda)$$

Dado um $\lambda$ específico, o valor esperado de uma variável aleatória exponencial é igual ao inverso de $\lambda$, ou seja:

$$E[\; Z \;|\; \lambda \;] = \frac{1}{\lambda}$$

```python
a = np.linspace(0, 4, 100)
expo = stats.expon
lambda_ = [0.5, 1]

for l, c in zip(lambda_, colours):
    plt.plot(a, expo.pdf(a, scale=1./l), lw=3,
             color=c, label="$\lambda = %.1f$" % l)
    plt.fill_between(a, expo.pdf(a, scale=1./l), color=c, alpha=.33)

plt.legend()
plt.ylabel("FDP de $z^{(1)}$")
plt.xlabel("$z$")
plt.ylim(0,1.2)
plt.title("""Função de densidade de probabilidade de uma variável aleatória exponencial;
diferentes $\lambda$""");
```

----
$^{(1)}$ *Nota do Tradutor*:
A nomenclatura **FDP** é o acrônimo de **F**unção **D**ensidade de **P**robabilidade. Textos em inglês são rotulados como **PDF**, ou seja, **P**robability **D**ensity **F**unction.

### Mas o que é $\lambda \;$?

**Esta questão é o que motiva as estatísticas**. No mundo real, $\lambda$ está oculto para nós. Vemos apenas $Z$ e devemos retroceder para tentar determinar $\lambda$. O problema é difícil porque não há mapeamento um-para-um de $Z$ para $\lambda$. Muitos métodos diferentes foram criados para resolver o problema de estimar $\lambda$, mas como $\lambda$ nunca é realmente observado, ninguém pode dizer com certeza qual método é o melhor!

A inferência bayesiana está preocupada com *crenças* sobre o que $\lambda$ pode ser. Em vez de tentar adivinhar $\lambda$ exatamente, só podemos falar sobre o que $\lambda$ **provavelmente será** atribuindo, assim, uma distribuição de probabilidade à $\lambda$.

Isso pode parecer estranho à primeira vista. Afinal, $\lambda$ é fixo; não é (necessariamente) aleatório! Como podemos atribuir probabilidades a valores de uma variável não aleatória? Ah, caímos na nossa velha maneira de pensar: frequentista. Lembre-se de que, sob a filosofia bayesiana, nós *podemos* atribuir probabilidades se as interpretarmos como crenças. E é totalmente aceitável ter *crenças* sobre o parâmetro $\lambda$.

##### Exemplo: inferir comportamento dos dados nas mensagens de texto recebidas

Vamos tentar modelar um exemplo mais interessante, que diz respeito à taxa na qual um usuário envia e recebe mensagens de texto:

> Você recebe uma série de mensagens de texto diárias *(whatsapp, messenger, email, etc)* de  de um usuário do seu sistema. Os dados, plotados ao longo do tempo, aparecem no gráfico abaixo. Você está curioso para saber se os hábitos de envio de mensagens de texto do usuário mudaram com o tempo, gradual ou repentinamente. Como você pode modelar isso? (Estes são, na verdade, meus próprios dados de mensagens de texto. Avalie minha popularidade como desejar.)

```python
figsize(12.5, 3.5)
count_data = np.loadtxt("data/txtdata.csv")
n_count_data = len(count_data)
plt.bar(np.arange(n_count_data), count_data, color="#348ABD")
plt.xlabel("Tempo (dias)")
plt.ylabel("Contagem de mensagens de texto recebidas")
plt.title("Os hábitos de envio de mensagens de texto desse usuário mudaram com o tempo?")
plt.xlim(0, n_count_data);
```

Antes de começarmos a modelar, veja o que você pode descobrir olhando para o gráfico acima. Você diria que houve uma mudança de comportamento durante esse período?

Como podemos começar a modelar isso? Bem, como já vimos convenientemente, uma variável aleatória de Poisson é um modelo muito apropriado para este tipo de dados de *contagem*. Denotando a **$C$**ontagem de mensagens de texto do dia $i$ por $C_i$,

$$ C_i \sim \text{Poisson}(\lambda)  $$

Não temos certeza de qual é realmente o valor do parâmetro $\lambda$. Olhando para o gráfico acima, parece que a taxa pode ficar mais alta no final do período de observação, o que equivale a dizer que $\lambda$ aumenta em algum ponto durante as observações. (Lembre-se de que um valor mais alto de $\lambda$ atribui mais probabilidade a resultados maiores. Ou seja, há uma probabilidade maior de muitas mensagens de texto terem sido enviadas em um determinado dia.)

Como podemos representar esta observação matematicamente? Vamos supor que em algum *dia* durante o período de observação (chamaremos esse dia de $\tau$), o parâmetro $\lambda$ repentinamente salte para um valor mais alto. Portanto, temos realmente dois parâmetros $\lambda$: um ($\lambda_1$) para o período anterior a $\tau$ e outro ($\lambda_2$) para o resto do período de observação. Na literatura, uma transição repentina como esta seria chamada de *switchpoint* $^{(1)}$:

$$
\lambda =
\begin{cases}
\lambda_1  & \text{if } t \lt \tau \cr
\lambda_2 & \text{if } t \ge \tau
\end{cases}
$$

Se, na realidade, nenhuma mudança repentina ocorreu e de fato $\lambda_1 = \lambda_2$, então as distribuições posterioris de $\lambda$s devem parecer iguais.

Estamos interessados em inferir os desconhecidos $\lambda$s. Para usar a inferência Bayesiana, precisamos atribuir probabilidades prioris aos diferentes valores possíveis de $\lambda$. Quais seriam boas distribuições de probabilidade a priori para $\lambda_1$ e $\lambda_2$? Lembre-se de que $\lambda$ pode ser qualquer número positivo. Como vimos anteriormente, a distribuição *exponencial* fornece uma função de densidade contínua para números positivos, portanto, pode ser uma boa escolha para modelar $\lambda_i$. Mas lembre-se de que a distribuição exponencial tem um parâmetro próprio, portanto, precisaremos incluir esse parâmetro em nosso modelo. Vamos chamar esse parâmetro de $\alpha$.

\begin{align}
&\lambda_1 \sim \text{Exp}( \alpha ) \\\
&\lambda_2 \sim \text{Exp}( \alpha )
\end{align}

$\alpha$ é chamado de *hiperparâmetro* ou *variável pai*. Em termos literais, é um parâmetro que influencia outros parâmetros. Nossa estimativa inicial em $\alpha$ não influencia o modelo muito fortemente, portanto, temos alguma flexibilidade em nossa escolha. Uma boa regra prática é definir o parâmetro exponencial igual ao inverso da média dos dados de contagem. Como estamos modelando $\lambda$ usando uma distribuição exponencial, podemos usar a identidade de valor esperada mostrada anteriormente para obter:

$$\frac{1}{N}\sum_{i=0}^N \;C_i \approx E[\; \lambda \; |\; \alpha ] = \frac{1}{\alpha}$$

Uma alternativa, e algo que encorajo o leitor a tentar, seria ter duas prioris: um para cada $\lambda_i$. A criação de duas distribuições exponenciais com valores $\alpha$ diferentes reflete nossa crença à priori de que a taxa mudou em algum ponto durante as observações.

E quanto a $\tau$? Por causa do ruído dos dados, é difícil definir a priori quando $\tau$ pode ter ocorrido. Em vez disso, podemos atribuir uma *crença à priori uniforme* a todos os dias possíveis. Isso é equivalente a dizer:

\begin{align}
& \tau \sim \text{DiscreteUniform(1,70) }\\\\
& \Rightarrow P( \tau = k ) = \frac{1}{70}
\end{align}

Depois de tudo isso, como nossas distribuições à priori em geral ficam para as variáveis desconhecidas? Francamente, *não importa*. O que devemos entender é que é uma bagunça feia e complicada envolvendo símbolos que apenas um matemático poderia amar. E as coisas só vão ficar mais feias quanto mais complicados nossos modelos se tornarem. Independentemente disso, tudo o que realmente importa é a distribuição *à posteriori*.

Em seguida, voltamos para PyMC3, uma biblioteca Python para realizar análises bayesianas que não se intimida com o monstro matemático que criamos.

Apresentando nosso primeiro martelo: PyMC3
-----

PyMC3 é uma biblioteca Python para programação de análises Bayesianas[3]. É uma biblioteca rápida e bem mantida. A única parte lamentável é que sua documentação está faltando em certas áreas, especialmente aquelas que preenchem a lacuna entre iniciante e o hacker. Um dos principais objetivos deste livro é resolver esse problema e também demonstrar por que o PyMC3 é tão legal.

Vamos modelar o problema acima usando PyMC3. Esse tipo de programação é chamado de *programação probabilística*, um nome impróprio infeliz que invoca idéias de código gerado aleatoriamente e provavelmente confundiu e assustou os usuários para longe desse campo. O código não é aleatório; é probabilístico no sentido de que criamos modelos de probabilidade usando variáveis de programação como componentes do modelo. Os componentes do modelo são primitivos de primeira classe na estrutura PyMC3.

B. Cronin [5] tem uma descrição muito motivadora de programação probabilística:

> Outra maneira de pensar sobre isso: ao contrário de um programa tradicional, que só roda nas direções para frente, um programa probabilístico é executado tanto na direção para frente quanto para trás. Ele avança para calcular as consequências das suposições que contém sobre o mundo (ou seja, o espaço do modelo que representa), mas também avança para trás a partir dos dados para restringir as possíveis explicações. Na prática, muitos sistemas de programação probabilística habilmente intercalam essas operações de avanço e retrocesso para localizar com eficiência as melhores explicações.

Por causa da confusão gerada pelo termo *programação probabilística*, vou abster-me de usá-lo. Em vez disso, direi simplesmente *programação*, pois é isso que realmente é.

O código PyMC3 é fácil de ler. A única coisa nova deve ser a sintaxe. Simplesmente lembre-se de que estamos representando os componentes do modelo ($\tau, \lambda_1, \lambda_2 $) como variáveis.

---

$^{(1)}$(*Nota do Tradutor*): *switchpoint* significa ponto de troca, nesse exemplo, indica o momento no qual o comportamento da quantidade de mensagens recebidas se alterou.

```python
import pymc3 as pm
import theano.tensor as tt

with pm.Model() as model:
    alpha = 1.0/count_data.mean()  # Lembre-se que count_data é a
                                   # variável que mantém nossas contagens de txt
    lambda_1 = pm.Exponential("lambda_1", alpha)
    lambda_2 = pm.Exponential("lambda_2", alpha)

    tau = pm.DiscreteUniform("tau", lower=0, upper=n_count_data - 1)
```

    WARNING (theano.tensor.blas): Using NumPy C-API based implementation for BLAS functions.

No código acima, criamos as variáveis PyMC3 correspondentes a $\lambda_1$ e $\lambda_2$. Nós os atribuímos às *variáveis estocásticas* de PyMC3, e são chamadas assim porque são tratadas pelo backend como geradores de números aleatórios.

```python
with model:
    idx = np.arange(n_count_data) # Index
    lambda_ = pm.math.switch(tau > idx, lambda_1, lambda_2)
```

Este código cria uma nova função `lambda_`, mas realmente podemos pensar nela como uma variável aleatória: a variável aleatória $\lambda$ de cima. A função `switch ()` atribui `lambda_1` ou` lambda_2` como o valor de `lambda_`, dependendo do lado de` tau` em que estamos. Os valores de `lambda_` até` tau` são `lambda_1` e os valores posteriores são` lambda_2`.

Observe que, como `lambda_1`,` lambda_2` e `tau` são aleatórios,` lambda_` será aleatório. **Não** estamos corrigindo nenhuma variável ainda.

```python
with model:
    observation = pm.Poisson("obs", lambda_, observed=count_data)
```

A variável `observation` combina nossos dados,` count_data`, com nosso esquema de geração de dados proposto, dado pela variável `lambda_`, através da palavra-chave `observation`.

O código abaixo será explicado no Capítulo 3, mas eu o mostro aqui para que você possa ver de onde vêm nossos resultados. Pode-se pensar nisso como um passo de *aprendizagem*. O maquinário sendo empregado é chamado *Markov Chain Monte Carlo* (MCMC), que também retardo a explicação até o Capítulo 3. Essa técnica retorna milhares de variáveis aleatórias das distribuições posteriores de $\lambda_1, \lambda_2$ e $\tau$. Podemos traçar um histograma das variáveis aleatórias para ver como as distribuições à posterioris se parecem. Abaixo, coletamos algumas amostras (chamadas *traces*$^{(1)}$ na literatura MCMC) em histogramas.

----
$^{(1)}$ Nota do Tradutor: *traces* podem ser entendidos como o traço, ou melhor, rastros. Significando o caminhos que o algoritmo do MCMC faz no espaço das variáveis no processo de construção da posteriori.

```python
### Mysterious code to be explained in Chapter 3.
with model:
    step = pm.Metropolis()
    trace = pm.sample(10000, tune=5000,step=step)
```

    Multiprocess sampling (4 chains in 4 jobs)
    CompoundStep
    >Metropolis: [tau]
    >Metropolis: [lambda_2]
    >Metropolis: [lambda_1]
    Sampling 4 chains, 0 divergences: 100%|██████████| 60000/60000 [00:07<00:00, 7817.55draws/s]
    The number of effective samples is smaller than 25% for some parameters.

```python
lambda_1_samples = trace['lambda_1']
lambda_2_samples = trace['lambda_2']
tau_samples = trace['tau']
```

```python
figsize(12.5, 10)
#histogram of the samples:

ax = plt.subplot(311)
ax.set_autoscaley_on(False)

plt.hist(lambda_1_samples, histtype='stepfilled', bins=30, alpha=0.85,
         label="posterior of $\lambda_1$", color="#A60628", density=True)
plt.legend(loc="upper left")
plt.title(r"""Distribuição à posteriori das variáveis $\lambda_1,\;\lambda_2,\;\tau$""")
plt.xlim([15, 30])
plt.xlabel("Valor de $\lambda_1$")

ax = plt.subplot(312)
ax.set_autoscaley_on(False)
plt.hist(lambda_2_samples, histtype='stepfilled', bins=30, alpha=0.85,
         label="Posteriori de $\lambda_2$", color="#7A68A6", density=True)
plt.legend(loc="upper left")
plt.xlim([15, 30])
plt.xlabel("Valor de $\lambda_2$")

plt.subplot(313)
w = 1.0 / tau_samples.shape[0] * np.ones_like(tau_samples)
plt.hist(tau_samples, bins=n_count_data, alpha=1,
         label=r"Posteriori de $\tau$",
         color="#467821", weights=w, rwidth=2.)
plt.xticks(np.arange(n_count_data))

plt.legend(loc="upper left")
plt.ylim([0, .75])
plt.xlim([35, len(count_data)-20])
plt.xlabel(r"$\tau$ (em dias)")
plt.ylabel("probabilidade");
```

### Interpretação

Lembre-se de que na metodologia bayesiana é retornado uma *distribuição*. Portanto, agora temos distribuições para descrever os desconhecidos $\lambda$s e $\tau$. O que ganhamos? Imediatamente, podemos ver a incerteza em nossas estimativas: quanto mais ampla a distribuição, menos certa deve ser nossa crença posterior. Também podemos ver quais são os valores plausíveis para os parâmetros: $\lambda_1$ é cerca de 18 e $\lambda_2$ é cerca de 23. As distribuições posteriores dos dois $\lambda$s são claramente distintas, indicando que é de fato provável que houve uma mudança no comportamento da mensagem de texto do usuário.

Que outras observações você pode fazer? Se você olhar os dados originais novamente, esses resultados parecem razoáveis?

Observe também que as distribuições à posterioris para os $\lambda$s não se parecem com distribuições exponenciais, embora nossas prioris para essas variáveis fossem exponenciais. Na verdade, as distribuições à posterioris não são realmente de qualquer forma que reconhecemos do modelo original. Mas tudo bem! Este é um dos benefícios de se adotar um ponto de vista computacional. Se, em vez disso, tivéssemos feito essa análise usando abordagens matemáticas, teríamos ficado presos a uma distribuição analiticamente intratável (e confusa). No uso de uma abordagem computacional torna-se indiferentes à tratabilidade matemática.

Nossa análise também retornou uma distribuição de $\tau$. Sua distribuição à posteriori parece um pouco diferente das outras duas porque é uma variável aleatória discreta, então não atribui probabilidades a intervalos. Podemos ver que próximo ao dia 45, havia 50% de chance de que o comportamento do usuário mudasse. Se nenhuma mudança tivesse ocorrido, ou se a mudança tivesse sido gradual ao longo do tempo, a distribuição posterior de $\tau$ teria sido mais espalhada, refletindo que muitos dias eram candidatos plausíveis para $\tau$. Em contraste, nos resultados reais, vemos que apenas três ou quatro dias fazem algum sentido como pontos de transição potenciais.

### Enfim, por que eu iria querer amostras da parte da posteriori?

Trataremos dessa questão no restante do livro, e é um eufemismo dizer que nos levará a alguns resultados surpreendentes. Por enquanto, vamos encerrar este capítulo com mais um exemplo.

Usaremos as amostras à posterioris para responder à seguinte pergunta: qual é o número esperado de textos no dia $t,\; 0 \leq t \leq 70$? Lembre-se de que o valor esperado de uma variável Poisson é igual ao seu parâmetro $\lambda$. Portanto, a questão é equivalente a *qual é o valor esperado de $\lambda$ no tempo $t$*?

No código abaixo, o $i$ indexa as amostras das distribuições à posteriori. Dado um dia $t$, fazemos a média de todos os $\lambda_i$ possíveis para esse dia $t$, usando $\lambda_i = \lambda_{1, i}$ se $t \lt \tau_i$ (ou seja, se o a mudança de comportamento ainda não ocorreu), senão usamos $\lambda_i = \lambda_{2, i}$.

```python
figsize(12.5, 5)
# tau_samples, lambda_1_samples, lambda_2_samples contém
# N amostras a partir da distribuição à posteriori correspondente

N = tau_samples.shape[0]
expected_texts_per_day = np.zeros(n_count_data)

for day in range(0, n_count_data):
    # ix é um índice booleano de todas as amostras de tau correspondentes ao
    # switchpoint (ponto de mudança) ocorrendo antes do valor do 'dia'
    ix = day < tau_samples
    # Cada amostra à posteriori corresponde a um valor para tau.
    # para cada dia, esse valor de tau indica se estamos "antes"
    # (regido pelo lambda1) ou
    # "depois" (regido pelo lambda2), sob o ponto de switchpoint.
    # tomando a amostra à posteriori de lambda_{1/2} adequadamente,
    # podemos calcular a média
    # em todas as amostras para obter um valor esperado para lambda naquele dia.
    # Conforme explicado, a variável aleatória "contagem de mensagens"
    # é distribuída por Poisson,
    # e, portanto, lambda (o parâmetro da Poisson) é o valor esperado de
    # "contagem das mensagens".
    expected_texts_per_day[day] = (lambda_1_samples[ix].sum()
                                   + lambda_2_samples[~ix].sum()) / N

plt.plot(range(n_count_data), expected_texts_per_day, lw=4, color="#E24A33",
         label=" número esperado do número de mensagens de texto recebídas")
plt.xlim(0, n_count_data)
plt.xlabel("Dia")
plt.ylabel("Esperado # mensagens de texto")
plt.title("Esperado número de mensagens de texto recebídas")
plt.ylim(0, 60)
plt.bar(np.arange(len(count_data)), count_data, color="#348ABD", alpha=0.65,
        label="Observação de texto por dia")

plt.legend(loc="upper left");
```

Nossa análise mostra um forte suporte que nos leva a acreditar que o comportamento do usuário mudou ($\lambda_1$ teria valor próximo a $\lambda_2$ se isso não fosse verdade), e que a mudança foi repentina em vez de gradual (como demonstrado por $\tau$ na posteriori fortemente pontiaguda). Podemos especular o que pode ter causado isso: uma taxa de mensagem de texto mais barata, uma assinatura recente do clima para texto ou talvez um novo relacionamento. (Na verdade, o 45º dia corresponde ao Natal, e me mudei para Toronto no mês seguinte, deixando uma namorada para trás.)

##### Exercícios

1\.  Usando `lambda_1_samples` e `lambda_2_samples`, qual é a média das distribuições à posterioris de $\lambda_1$ e $\lambda_2$?

```python
# Digite seu código aqui.
```

2\.  Qual é o aumento percentual esperado de aumento nas taxas de mensagens de texto? `dica:` calcule a média de `lambda_1_samples / lambda_2_samples`. Observe que esta quantidade é muito diferente de `lambda_1_samples.mean () / lambda_2_samples.mean ()`.

```python
# Digite seu código aqui.
```

3\. Qual é a média de $\lambda_1$ **dado** que sabemos que $\tau$ é menor que 45. Ou seja, suponha que recebemos novas informações de que a mudança de comportamento ocorreu antes do dia 45. Qual é o valor esperado de $\lambda_1$ agora? (Você não precisa refazer a parte PyMC3. Considere todas as instâncias em que `tau_samples < 45`.)

```python
# Digite seu código aqui.
```

### Referências

-  [1] Gelman, Andrew. N.p.. Web. 22 Jan 2013. [N is never large enough](http://andrewgelman.com/2005/07/31/n_is_never_larg).
-  [2] Norvig, Peter. 2009. [The Unreasonable Effectiveness of Data](http://static.googleusercontent.com/media/research.google.com/en//pubs/archive/35179.pdf).
- [3] Salvatier, J, Wiecki TV, and Fonnesbeck C. (2016) Probabilistic programming in Python using PyMC3. *PeerJ Computer Science* 2:e55 <https://doi.org/10.7717/peerj-cs.55>
- [4] Jimmy Lin and Alek Kolcz. Large-Scale Machine Learning at Twitter. Proceedings of the 2012 ACM SIGMOD International Conference on Management of Data (SIGMOD 2012), pages 793-804, May 2012, Scottsdale, Arizona.
- [5] Cronin, Beau. "Why Probabilistic Programming Matters." 24 Mar 2013. Google, Online Posting to Google . Web. 24 Mar. 2013. <https://plus.google.com/u/0/107971134877020469960/posts/KpeRdJKR6Z1>.

```python
from IPython.core.display import HTML
def css_styling():
    styles = open("../styles/custom.css", "r").read()
    return HTML(styles)
css_styling()
```

<style>
    @font-face {
        font-family: "Computer Modern";
        src: url('http://9dbb143991406a7c655e-aa5fcb0a5a4ec34cff238a2d56ca4144.r56.cf5.rackcdn.com/cmunss.otf');
    }
    @font-face {
        font-family: "Computer Modern";
        font-weight: bold;
        src: url('http://9dbb143991406a7c655e-aa5fcb0a5a4ec34cff238a2d56ca4144.r56.cf5.rackcdn.com/cmunsx.otf');
    }
    @font-face {
        font-family: "Computer Modern";
        font-style: oblique;
        src: url('http://9dbb143991406a7c655e-aa5fcb0a5a4ec34cff238a2d56ca4144.r56.cf5.rackcdn.com/cmunsi.otf');
    }
    @font-face {
        font-family: "Computer Modern";
        font-weight: bold;
        font-style: oblique;
        src: url('http://9dbb143991406a7c655e-aa5fcb0a5a4ec34cff238a2d56ca4144.r56.cf5.rackcdn.com/cmunso.otf');
    }
    div.cell{
        width:800px;
        margin-left:16% !important;
        margin-right:auto;
    }
    h1 {
        font-family: Helvetica, serif;
    }
    h4{
        margin-top:12px;
        margin-bottom: 3px;
       }
    div.text_cell_render{
        font-family: Computer Modern, "Helvetica Neue", Arial, Helvetica, Geneva, sans-serif;
        line-height: 145%;
        font-size: 130%;
        width:800px;
        margin-left:auto;
        margin-right:auto;
    }
    .CodeMirror{
            font-family: "Source Code Pro", source-code-pro,Consolas, monospace;
    }
    .prompt{
        display: None;
    }
    .text_cell_render h5 {
        font-weight: 300;
        font-size: 22pt;
        color: #4057A1;
        font-style: italic;
        margin-bottom: .5em;
        margin-top: 0.5em;
        display: block;
    }

    .warning{
        color: rgb( 240, 20, 20 )
        }
</style>

```python

```


```python
# This mounts your Google Drive to the Colab VM.
from google.colab import drive
drive.mount('/content/drive')

# TODO: Enter the foldername in your Drive where you have saved the unzipped
# assignment folder, e.g. 'cs231n/assignments/assignment1/'
FOLDERNAME = '/Colab Notebooks/Stanford_CS231n/assignment2'
assert FOLDERNAME is not None, "[!] Enter the foldername."

# Now that we've mounted your Drive, this ensures that
# the Python interpreter of the Colab VM can load
# python files from within it.
import sys
sys.path.append('/content/drive/My Drive/{}'.format(FOLDERNAME))

# This downloads the CIFAR-10 dataset to your Drive
# if it doesn't already exist.
%cd /content/drive/My\ Drive/$FOLDERNAME/cs231n/datasets/
!bash get_datasets.sh
%cd /content/drive/My\ Drive/$FOLDERNAME
```

    Mounted at /content/drive
    /content/drive/My Drive/Colab Notebooks/Stanford_CS231n/assignment2/cs231n/datasets
    /content/drive/My Drive/Colab Notebooks/Stanford_CS231n/assignment2

# Batch Normalization
One way to make deep networks easier to train is to use more sophisticated optimization procedures such as SGD+momentum, RMSProp, or Adam. Another strategy is to change the architecture of the network to make it easier to train. One idea along these lines is batch normalization, proposed by [1] in 2015.

To understand the goal of batch normalization, it is important to first recognize that machine learning methods tend to perform better with input data consisting of uncorrelated features with zero mean and unit variance. When training a neural network, we can preprocess the data before feeding it to the network to explicitly decorrelate its features. This will ensure that the first layer of the network sees data that follows a nice distribution. However, even if we preprocess the input data, the activations at deeper layers of the network will likely no longer be decorrelated and will no longer have zero mean or unit variance, since they are output from earlier layers in the network. Even worse, during the training process the distribution of features at each layer of the network will shift as the weights of each layer are updated.

The authors of [1] hypothesize that the shifting distribution of features inside deep neural networks may make training deep networks more difficult. To overcome this problem, they propose to insert into the network layers that normalize batches. At training time, such a layer uses a minibatch of data to estimate the mean and standard deviation of each feature. These estimated means and standard deviations are then used to center and normalize the features of the minibatch. A running average of these means and standard deviations is kept during training, and at test time these running averages are used to center and normalize features.

It is possible that this normalization strategy could reduce the representational power of the network, since it may sometimes be optimal for certain layers to have features that are not zero-mean or unit variance. To this end, the batch normalization layer includes learnable shift and scale parameters for each feature dimension.

[1] [Sergey Ioffe and Christian Szegedy, "Batch Normalization: Accelerating Deep Network Training by Reducing
Internal Covariate Shift", ICML 2015.](https://arxiv.org/abs/1502.03167)

```python
# Setup cell.
import time
import numpy as np
import matplotlib.pyplot as plt
from cs231n.classifiers.fc_net import *
from cs231n.data_utils import get_CIFAR10_data
from cs231n.gradient_check import eval_numerical_gradient, eval_numerical_gradient_array
from cs231n.solver import Solver

%matplotlib inline
plt.rcParams["figure.figsize"] = (10.0, 8.0)  # Set default size of plots.
plt.rcParams["image.interpolation"] = "nearest"
plt.rcParams["image.cmap"] = "gray"

%load_ext autoreload
%autoreload 2

def rel_error(x, y):
    """Returns relative error."""
    return np.max(np.abs(x - y) / (np.maximum(1e-8, np.abs(x) + np.abs(y))))

def print_mean_std(x,axis=0):
    print(f"  means: {x.mean(axis=axis)}")
    print(f"  stds:  {x.std(axis=axis)}\n")
```

    =========== You can safely ignore the message below if you are NOT working on ConvolutionalNetworks.ipynb ===========
    	You will need to compile a Cython extension for a portion of this assignment.
    	The instructions to do this will be given in a section of the notebook below.

```python
# Load the (preprocessed) CIFAR-10 data.
data = get_CIFAR10_data()
for k, v in list(data.items()):
    print(f"{k}: {v.shape}")
```

    X_train: (49000, 3, 32, 32)
    y_train: (49000,)
    X_val: (1000, 3, 32, 32)
    y_val: (1000,)
    X_test: (1000, 3, 32, 32)
    y_test: (1000,)

# Batch Normalization: Forward Pass
In the file `cs231n/layers.py`, implement the batch normalization forward pass in the function `batchnorm_forward`. Once you have done so, run the following to test your implementation.

Referencing the paper linked to above in [1] may be helpful!

```python
# Check the training-time forward pass by checking means and variances
# of features both before and after batch normalization

# Simulate the forward pass for a two-layer network.
np.random.seed(231)
N, D1, D2, D3 = 200, 50, 60, 3
X = np.random.randn(N, D1)
W1 = np.random.randn(D1, D2)
W2 = np.random.randn(D2, D3)
a = np.maximum(0, X.dot(W1)).dot(W2)

print('Before batch normalization:')
print_mean_std(a,axis=0)

gamma = np.ones((D3,))
beta = np.zeros((D3,))

# Means should be close to zero and stds close to one.
print('After batch normalization (gamma=1, beta=0)')
a_norm, _ = batchnorm_forward(a, gamma, beta, {'mode': 'train'})
print_mean_std(a_norm,axis=0)

gamma = np.asarray([1.0, 2.0, 3.0])
beta = np.asarray([11.0, 12.0, 13.0])

# Now means should be close to beta and stds close to gamma.
print('After batch normalization (gamma=', gamma, ', beta=', beta, ')')
a_norm, _ = batchnorm_forward(a, gamma, beta, {'mode': 'train'})
print_mean_std(a_norm,axis=0)
```

    Before batch normalization:
      means: [ -2.3814598  -13.18038246   1.91780462]
      stds:  [27.18502186 34.21455511 37.68611762]

    After batch normalization (gamma=1, beta=0)
      means: [5.32907052e-17 7.04991621e-17 1.85962357e-17]
      stds:  [0.99999999 1.         1.        ]

    After batch normalization (gamma= [1. 2. 3.] , beta= [11. 12. 13.] )
      means: [11. 12. 13.]
      stds:  [0.99999999 1.99999999 2.99999999]

```python
# Check the test-time forward pass by running the training-time
# forward pass many times to warm up the running averages, and then
# checking the means and variances of activations after a test-time
# forward pass.

np.random.seed(231)
N, D1, D2, D3 = 200, 50, 60, 3
W1 = np.random.randn(D1, D2)
W2 = np.random.randn(D2, D3)

bn_param = {'mode': 'train'}
gamma = np.ones(D3)
beta = np.zeros(D3)

for t in range(50):
  X = np.random.randn(N, D1)
  a = np.maximum(0, X.dot(W1)).dot(W2)
  batchnorm_forward(a, gamma, beta, bn_param)

bn_param['mode'] = 'test'
X = np.random.randn(N, D1)
a = np.maximum(0, X.dot(W1)).dot(W2)
a_norm, _ = batchnorm_forward(a, gamma, beta, bn_param)

# Means should be close to zero and stds close to one, but will be
# noisier than training-time forward passes.
print('After batch normalization (test-time):')
print_mean_std(a_norm,axis=0)
```

    After batch normalization (test-time):
      means: [-0.03927354 -0.04349152 -0.10452688]
      stds:  [1.01531428 1.01238373 0.97819988]

# Batch Normalization: Backward Pass
Now implement the backward pass for batch normalization in the function `batchnorm_backward`.

To derive the backward pass you should write out the computation graph for batch normalization and backprop through each of the intermediate nodes. Some intermediates may have multiple outgoing branches; make sure to sum gradients across these branches in the backward pass.

Once you have finished, run the following to numerically check your backward pass.

```python
# Gradient check batchnorm backward pass.
np.random.seed(231)
N, D = 4, 5
x = 5 * np.random.randn(N, D) + 12
gamma = np.random.randn(D)
beta = np.random.randn(D)
dout = np.random.randn(N, D)

bn_param = {'mode': 'train'}
fx = lambda x: batchnorm_forward(x, gamma, beta, bn_param)[0]
fg = lambda a: batchnorm_forward(x, a, beta, bn_param)[0]
fb = lambda b: batchnorm_forward(x, gamma, b, bn_param)[0]

dx_num = eval_numerical_gradient_array(fx, x, dout)
da_num = eval_numerical_gradient_array(fg, gamma.copy(), dout)
db_num = eval_numerical_gradient_array(fb, beta.copy(), dout)

_, cache = batchnorm_forward(x, gamma, beta, bn_param)
dx, dgamma, dbeta = batchnorm_backward(dout, cache)

# You should expect to see relative errors between 1e-13 and 1e-8.
print('dx error: ', rel_error(dx_num, dx))
print('dgamma error: ', rel_error(da_num, dgamma))
print('dbeta error: ', rel_error(db_num, dbeta))
```

    dx error:  1.7029261167605239e-09
    dgamma error:  7.420414216247087e-13
    dbeta error:  2.8795057655839487e-12

# Batch Normalization: Alternative Backward Pass
In class we talked about two different implementations for the sigmoid backward pass. One strategy is to write out a computation graph composed of simple operations and backprop through all intermediate values. Another strategy is to work out the derivatives on paper. For example, you can derive a very simple formula for the sigmoid function's backward pass by simplifying gradients on paper.

Surprisingly, it turns out that you can do a similar simplification for the batch normalization backward pass too!

In the forward pass, given a set of inputs $X=\begin{bmatrix}x_1\\x_2\\...\\x_N\end{bmatrix}$,

we first calculate the mean $\mu$ and variance $v$.
With $\mu$ and $v$ calculated, we can calculate the standard deviation $\sigma$  and normalized data $Y$.
The equations and graph illustration below describe the computation ($y_i$ is the i-th element of the vector $Y$).

\begin{align}
& \mu=\frac{1}{N}\sum_{k=1}^N x_k  &  v=\frac{1}{N}\sum_{k=1}^N (x_k-\mu)^2 \\
& \sigma=\sqrt{v+\epsilon}         &  y_i=\frac{x_i-\mu}{\sigma}
\end{align}

The meat of our problem during backpropagation is to compute $\frac{\partial L}{\partial X}$, given the upstream gradient we receive, $\frac{\partial L}{\partial Y}.$ To do this, recall the chain rule in calculus gives us $\frac{\partial L}{\partial X} = \frac{\partial L}{\partial Y} \cdot \frac{\partial Y}{\partial X}$.

The unknown/hard part is $\frac{\partial Y}{\partial X}$. We can find this by first deriving step-by-step our local gradients at
$\frac{\partial v}{\partial X}$, $\frac{\partial \mu}{\partial X}$,
$\frac{\partial \sigma}{\partial v}$,
$\frac{\partial Y}{\partial \sigma}$, and $\frac{\partial Y}{\partial \mu}$,
and then use the chain rule to compose these gradients (which appear in the form of vectors!) appropriately to compute $\frac{\partial Y}{\partial X}$.

If it's challenging to directly reason about the gradients over $X$ and $Y$ which require matrix multiplication, try reasoning about the gradients in terms of individual elements $x_i$ and $y_i$ first: in that case, you will need to come up with the derivations for $\frac{\partial L}{\partial x_i}$, by relying on the Chain Rule to first calculate the intermediate $\frac{\partial \mu}{\partial x_i}, \frac{\partial v}{\partial x_i}, \frac{\partial \sigma}{\partial x_i},$ then assemble these pieces to calculate $\frac{\partial y_i}{\partial x_i}$.

You should make sure each of the intermediary gradient derivations are all as simplified as possible, for ease of implementation.

After doing so, implement the simplified batch normalization backward pass in the function `batchnorm_backward_alt` and compare the two implementations by running the following. Your two implementations should compute nearly identical results, but the alternative implementation should be a bit faster.

```python
np.random.seed(231)
N, D = 100, 500
x = 5 * np.random.randn(N, D) + 12
gamma = np.random.randn(D)
beta = np.random.randn(D)
dout = np.random.randn(N, D)

bn_param = {'mode': 'train'}
out, cache = batchnorm_forward(x, gamma, beta, bn_param)

t1 = time.time()
dx1, dgamma1, dbeta1 = batchnorm_backward(dout, cache)
t2 = time.time()
dx2, dgamma2, dbeta2 = batchnorm_backward_alt(dout, cache)
t3 = time.time()

print('dx difference: ', rel_error(dx1, dx2))
print('dgamma difference: ', rel_error(dgamma1, dgamma2))
print('dbeta difference: ', rel_error(dbeta1, dbeta2))
print('speedup: %.2fx' % ((t2 - t1) / (t3 - t2)))
```

    dx difference:  7.49749656041581e-13
    dgamma difference:  0.0
    dbeta difference:  0.0
    speedup: 2.04x

# Fully Connected Networks with Batch Normalization
Now that you have a working implementation for batch normalization, go back to your `FullyConnectedNet` in the file `cs231n/classifiers/fc_net.py`. Modify your implementation to add batch normalization.

Concretely, when the `normalization` flag is set to `"batchnorm"` in the constructor, you should insert a batch normalization layer before each ReLU nonlinearity. The outputs from the last layer of the network should not be normalized. Once you are done, run the following to gradient-check your implementation.

**Hint:** You might find it useful to define an additional helper layer similar to those in the file `cs231n/layer_utils.py`.

```python
np.random.seed(231)
N, D, H1, H2, C = 2, 15, 20, 30, 10
X = np.random.randn(N, D)
y = np.random.randint(C, size=(N,))

# You should expect losses between 1e-4~1e-10 for W,
# losses between 1e-08~1e-10 for b,
# and losses between 1e-08~1e-09 for beta and gammas.
for reg in [0, 3.14]:
  print('Running check with reg = ', reg)
  model = FullyConnectedNet([H1, H2], input_dim=D, num_classes=C,
                            reg=reg, weight_scale=5e-2, dtype=np.float64,
                            normalization='batchnorm')

  loss, grads = model.loss(X, y)
  print('Initial loss: ', loss)

  for name in sorted(grads):
    f = lambda _: model.loss(X, y)[0]
    grad_num = eval_numerical_gradient(f, model.params[name], verbose=False, h=1e-5)
    print('%s relative error: %.2e' % (name, rel_error(grad_num, grads[name])))
  if reg == 0: print()
```

    Running check with reg =  0
    Initial loss:  8.447989963075447
    W1 relative error: 1.10e-04
    W2 relative error: 7.56e-07
    W3 relative error: 8.76e-11
    b1 relative error: 2.49e-06
    b2 relative error: 1.78e-07
    b3 relative error: 2.55e-11
    beta1 relative error: 7.17e-09
    beta2 relative error: 7.31e-10
    gamma1 relative error: 7.52e-09
    gamma2 relative error: 9.68e-10

    Running check with reg =  3.14
    Initial loss:  11.926373672569037
    W1 relative error: 1.25e-06
    W2 relative error: 6.14e-07
    W3 relative error: 6.26e-01
    b1 relative error: 8.88e-03
    b2 relative error: 8.88e-03
    b3 relative error: 6.99e-11
    beta1 relative error: 6.25e-09
    beta2 relative error: 2.53e-09
    gamma1 relative error: 6.09e-09
    gamma2 relative error: 1.03e-09

# Batch Normalization for Deep Networks
Run the following to train a six-layer network on a subset of 1000 training examples both with and without batch normalization.

```python
np.random.seed(231)

# Try training a very deep net with batchnorm.
hidden_dims = [100, 100, 100, 100, 100]

num_train = 1000
small_data = {
  'X_train': data['X_train'][:num_train],
  'y_train': data['y_train'][:num_train],
  'X_val': data['X_val'],
  'y_val': data['y_val'],
}

weight_scale = 2e-2
bn_model = FullyConnectedNet(hidden_dims, weight_scale=weight_scale, normalization='batchnorm')
model = FullyConnectedNet(hidden_dims, weight_scale=weight_scale, normalization=None)

print('Solver with batch norm:')
bn_solver = Solver(bn_model, small_data,
                num_epochs=10, batch_size=50,
                update_rule='adam',
                optim_config={
                  'learning_rate': 1e-3,
                },
                verbose=True,print_every=20)
bn_solver.train()

print('\nSolver without batch norm:')
solver = Solver(model, small_data,
                num_epochs=10, batch_size=50,
                update_rule='adam',
                optim_config={
                  'learning_rate': 1e-3,
                },
                verbose=True, print_every=20)
solver.train()
```

    Solver with batch norm:
    (Iteration 1 / 200) loss: 9.307661
    (Epoch 0 / 10) train acc: 0.106000; val_acc: 0.119000
    (Epoch 1 / 10) train acc: 0.331000; val_acc: 0.271000
    (Iteration 21 / 200) loss: 5.947877
    (Epoch 2 / 10) train acc: 0.407000; val_acc: 0.271000
    (Iteration 41 / 200) loss: 4.991967
    (Epoch 3 / 10) train acc: 0.473000; val_acc: 0.308000
    (Iteration 61 / 200) loss: 4.969000
    (Epoch 4 / 10) train acc: 0.537000; val_acc: 0.323000
    (Iteration 81 / 200) loss: 2.854577
    (Epoch 5 / 10) train acc: 0.580000; val_acc: 0.320000
    (Iteration 101 / 200) loss: 2.697992
    (Epoch 6 / 10) train acc: 0.630000; val_acc: 0.323000
    (Iteration 121 / 200) loss: 1.656204
    (Epoch 7 / 10) train acc: 0.692000; val_acc: 0.338000
    (Iteration 141 / 200) loss: 1.791152
    (Epoch 8 / 10) train acc: 0.722000; val_acc: 0.347000
    (Iteration 161 / 200) loss: 0.848149
    (Epoch 9 / 10) train acc: 0.750000; val_acc: 0.324000
    (Iteration 181 / 200) loss: 1.716791
    (Epoch 10 / 10) train acc: 0.787000; val_acc: 0.332000

    Solver without batch norm:
    (Iteration 1 / 200) loss: 8.997418
    (Epoch 0 / 10) train acc: 0.130000; val_acc: 0.132000
    (Epoch 1 / 10) train acc: 0.259000; val_acc: 0.209000
    (Iteration 21 / 200) loss: 6.253194
    (Epoch 2 / 10) train acc: 0.270000; val_acc: 0.222000
    (Iteration 41 / 200) loss: 5.843208
    (Epoch 3 / 10) train acc: 0.338000; val_acc: 0.286000
    (Iteration 61 / 200) loss: 4.396365
    (Epoch 4 / 10) train acc: 0.379000; val_acc: 0.310000
    (Iteration 81 / 200) loss: 4.258562
    (Epoch 5 / 10) train acc: 0.453000; val_acc: 0.314000
    (Iteration 101 / 200) loss: 4.274043
    (Epoch 6 / 10) train acc: 0.487000; val_acc: 0.314000
    (Iteration 121 / 200) loss: 2.996210
    (Epoch 7 / 10) train acc: 0.559000; val_acc: 0.343000
    (Iteration 141 / 200) loss: 2.036799
    (Epoch 8 / 10) train acc: 0.565000; val_acc: 0.311000
    (Iteration 161 / 200) loss: 1.926624
    (Epoch 9 / 10) train acc: 0.628000; val_acc: 0.318000
    (Iteration 181 / 200) loss: 1.558469
    (Epoch 10 / 10) train acc: 0.682000; val_acc: 0.339000

Run the following to visualize the results from two networks trained above. You should find that using batch normalization helps the network to converge much faster.

```python
def plot_training_history(title, label, baseline, bn_solvers, plot_fn, bl_marker='.', bn_marker='.', labels=None):
    """utility function for plotting training history"""
    plt.title(title)
    plt.xlabel(label)
    bn_plots = [plot_fn(bn_solver) for bn_solver in bn_solvers]
    bl_plot = plot_fn(baseline)
    num_bn = len(bn_plots)
    for i in range(num_bn):
        label='with_norm'
        if labels is not None:
            label += str(labels[i])
        plt.plot(bn_plots[i], bn_marker, label=label)
    label='baseline'
    if labels is not None:
        label += str(labels[0])
    plt.plot(bl_plot, bl_marker, label=label)
    plt.legend(loc='lower center', ncol=num_bn+1)

plt.subplot(3, 1, 1)
plot_training_history('Training loss','Iteration', solver, [bn_solver], \
                      lambda x: x.loss_history, bl_marker='o', bn_marker='o')
plt.subplot(3, 1, 2)
plot_training_history('Training accuracy','Epoch', solver, [bn_solver], \
                      lambda x: x.train_acc_history, bl_marker='-o', bn_marker='-o')
plt.subplot(3, 1, 3)
plot_training_history('Validation accuracy','Epoch', solver, [bn_solver], \
                      lambda x: x.val_acc_history, bl_marker='-o', bn_marker='-o')

plt.gcf().set_size_inches(15, 15)
plt.show()
```

# Batch Normalization and Initialization
We will now run a small experiment to study the interaction of batch normalization and weight initialization.

The first cell will train eight-layer networks both with and without batch normalization using different scales for weight initialization. The second layer will plot training accuracy, validation set accuracy, and training loss as a function of the weight initialization scale.

```python
np.random.seed(231)

# Try training a very deep net with batchnorm.
hidden_dims = [50, 50, 50, 50, 50, 50, 50]
num_train = 1000
small_data = {
  'X_train': data['X_train'][:num_train],
  'y_train': data['y_train'][:num_train],
  'X_val': data['X_val'],
  'y_val': data['y_val'],
}

bn_solvers_ws = {}
solvers_ws = {}
weight_scales = np.logspace(-4, 0, num=20)
for i, weight_scale in enumerate(weight_scales):
    print('Running weight scale %d / %d' % (i + 1, len(weight_scales)))
    bn_model = FullyConnectedNet(hidden_dims, weight_scale=weight_scale, normalization='batchnorm')
    model = FullyConnectedNet(hidden_dims, weight_scale=weight_scale, normalization=None)

    bn_solver = Solver(bn_model, small_data,
                  num_epochs=10, batch_size=50,
                  update_rule='adam',
                  optim_config={
                    'learning_rate': 1e-3,
                  },
                  verbose=False, print_every=200)
    bn_solver.train()
    bn_solvers_ws[weight_scale] = bn_solver

    solver = Solver(model, small_data,
                  num_epochs=10, batch_size=50,
                  update_rule='adam',
                  optim_config={
                    'learning_rate': 1e-3,
                  },
                  verbose=False, print_every=200)
    solver.train()
    solvers_ws[weight_scale] = solver
```

    Running weight scale 1 / 20
    Running weight scale 2 / 20
    Running weight scale 3 / 20
    Running weight scale 4 / 20
    Running weight scale 5 / 20
    Running weight scale 6 / 20
    Running weight scale 7 / 20
    Running weight scale 8 / 20
    Running weight scale 9 / 20
    Running weight scale 10 / 20
    Running weight scale 11 / 20
    Running weight scale 12 / 20
    Running weight scale 13 / 20
    Running weight scale 14 / 20
    Running weight scale 15 / 20
    Running weight scale 16 / 20
    Running weight scale 17 / 20
    Running weight scale 18 / 20
    Running weight scale 19 / 20
    Running weight scale 20 / 20

```python
# Plot results of weight scale experiment.
best_train_accs, bn_best_train_accs = [], []
best_val_accs, bn_best_val_accs = [], []
final_train_loss, bn_final_train_loss = [], []

for ws in weight_scales:
  best_train_accs.append(max(solvers_ws[ws].train_acc_history))
  bn_best_train_accs.append(max(bn_solvers_ws[ws].train_acc_history))

  best_val_accs.append(max(solvers_ws[ws].val_acc_history))
  bn_best_val_accs.append(max(bn_solvers_ws[ws].val_acc_history))

  final_train_loss.append(np.mean(solvers_ws[ws].loss_history[-100:]))
  bn_final_train_loss.append(np.mean(bn_solvers_ws[ws].loss_history[-100:]))

plt.subplot(3, 1, 1)
plt.title('Best val accuracy vs. weight initialization scale')
plt.xlabel('Weight initialization scale')
plt.ylabel('Best val accuracy')
plt.semilogx(weight_scales, best_val_accs, '-o', label='baseline')
plt.semilogx(weight_scales, bn_best_val_accs, '-o', label='batchnorm')
plt.legend(ncol=2, loc='lower right')

plt.subplot(3, 1, 2)
plt.title('Best train accuracy vs. weight initialization scale')
plt.xlabel('Weight initialization scale')
plt.ylabel('Best training accuracy')
plt.semilogx(weight_scales, best_train_accs, '-o', label='baseline')
plt.semilogx(weight_scales, bn_best_train_accs, '-o', label='batchnorm')
plt.legend()

plt.subplot(3, 1, 3)
plt.title('Final training loss vs. weight initialization scale')
plt.xlabel('Weight initialization scale')
plt.ylabel('Final training loss')
plt.semilogx(weight_scales, final_train_loss, '-o', label='baseline')
plt.semilogx(weight_scales, bn_final_train_loss, '-o', label='batchnorm')
plt.legend()
plt.gca().set_ylim(1.0, 3.5)

plt.gcf().set_size_inches(15, 15)
plt.show()
```

## Inline Question 1:
Describe the results of this experiment. How does the weight initialization scale affect models with/without batch normalization differently, and why?

## Answer:
[FILL THIS IN]

# Batch Normalization and Batch Size
We will now run a small experiment to study the interaction of batch normalization and batch size.

The first cell will train 6-layer networks both with and without batch normalization using different batch sizes. The second layer will plot training accuracy and validation set accuracy over time.

```python
def run_batchsize_experiments(normalization_mode):
    np.random.seed(231)

    # Try training a very deep net with batchnorm.
    hidden_dims = [100, 100, 100, 100, 100]
    num_train = 1000
    small_data = {
      'X_train': data['X_train'][:num_train],
      'y_train': data['y_train'][:num_train],
      'X_val': data['X_val'],
      'y_val': data['y_val'],
    }
    n_epochs=10
    weight_scale = 2e-2
    batch_sizes = [5,10,50]
    lr = 10**(-3.5)
    solver_bsize = batch_sizes[0]

    print('No normalization: batch size = ',solver_bsize)
    model = FullyConnectedNet(hidden_dims, weight_scale=weight_scale, normalization=None)
    solver = Solver(model, small_data,
                    num_epochs=n_epochs, batch_size=solver_bsize,
                    update_rule='adam',
                    optim_config={
                      'learning_rate': lr,
                    },
                    verbose=False)
    solver.train()

    bn_solvers = []
    for i in range(len(batch_sizes)):
        b_size=batch_sizes[i]
        print('Normalization: batch size = ',b_size)
        bn_model = FullyConnectedNet(hidden_dims, weight_scale=weight_scale, normalization=normalization_mode)
        bn_solver = Solver(bn_model, small_data,
                        num_epochs=n_epochs, batch_size=b_size,
                        update_rule='adam',
                        optim_config={
                          'learning_rate': lr,
                        },
                        verbose=False)
        bn_solver.train()
        bn_solvers.append(bn_solver)

    return bn_solvers, solver, batch_sizes

batch_sizes = [5,10,50]
bn_solvers_bsize, solver_bsize, batch_sizes = run_batchsize_experiments('batchnorm')
```

    No normalization: batch size =  5
    Normalization: batch size =  5
    Normalization: batch size =  10
    Normalization: batch size =  50

```python
plt.subplot(2, 1, 1)
plot_training_history('Training accuracy (Batch Normalization)','Epoch', solver_bsize, bn_solvers_bsize, \
                      lambda x: x.train_acc_history, bl_marker='-^', bn_marker='-o', labels=batch_sizes)
plt.subplot(2, 1, 2)
plot_training_history('Validation accuracy (Batch Normalization)','Epoch', solver_bsize, bn_solvers_bsize, \
                      lambda x: x.val_acc_history, bl_marker='-^', bn_marker='-o', labels=batch_sizes)

plt.gcf().set_size_inches(15, 10)
plt.show()
```

## Inline Question 2:
Describe the results of this experiment. What does this imply about the relationship between batch normalization and batch size? Why is this relationship observed?

## Answer:
[FILL THIS IN]

# Layer Normalization
Batch normalization has proved to be effective in making networks easier to train, but the dependency on batch size makes it less useful in complex networks which have a cap on the input batch size due to hardware limitations.

Several alternatives to batch normalization have been proposed to mitigate this problem; one such technique is Layer Normalization [2]. Instead of normalizing over the batch, we normalize over the features. In other words, when using Layer Normalization, each feature vector corresponding to a single datapoint is normalized based on the sum of all terms within that feature vector.

[2] [Ba, Jimmy Lei, Jamie Ryan Kiros, and Geoffrey E. Hinton. "Layer Normalization." stat 1050 (2016): 21.](https://arxiv.org/pdf/1607.06450.pdf)

## Inline Question 3:
Which of these data preprocessing steps is analogous to batch normalization, and which is analogous to layer normalization?

1. Scaling each image in the dataset, so that the RGB channels for each row of pixels within an image sums up to 1.
2. Scaling each image in the dataset, so that the RGB channels for all pixels within an image sums up to 1.
3. Subtracting the mean image of the dataset from each image in the dataset.
4. Setting all RGB values to either 0 or 1 depending on a given threshold.

## Answer:
[FILL THIS IN]

# Layer Normalization: Implementation

Now you'll implement layer normalization. This step should be relatively straightforward, as conceptually the implementation is almost identical to that of batch normalization. One significant difference though is that for layer normalization, we do not keep track of the moving moments, and the testing phase is identical to the training phase, where the mean and variance are directly calculated per datapoint.

Here's what you need to do:

* In `cs231n/layers.py`, implement the forward pass for layer normalization in the function `layernorm_forward`.

Run the cell below to check your results.
* In `cs231n/layers.py`, implement the backward pass for layer normalization in the function `layernorm_backward`.

Run the second cell below to check your results.
* Modify `cs231n/classifiers/fc_net.py` to add layer normalization to the `FullyConnectedNet`. When the `normalization` flag is set to `"layernorm"` in the constructor, you should insert a layer normalization layer before each ReLU nonlinearity.

Run the third cell below to run the batch size experiment on layer normalization.

```python
# Check the training-time forward pass by checking means and variances
# of features both before and after layer normalization.

# Simulate the forward pass for a two-layer network.
np.random.seed(231)
N, D1, D2, D3 =4, 50, 60, 3
X = np.random.randn(N, D1)
W1 = np.random.randn(D1, D2)
W2 = np.random.randn(D2, D3)
a = np.maximum(0, X.dot(W1)).dot(W2)

print('Before layer normalization:')
print_mean_std(a,axis=1)

gamma = np.ones(D3)
beta = np.zeros(D3)

# Means should be close to zero and stds close to one.
print('After layer normalization (gamma=1, beta=0)')
a_norm, _ = layernorm_forward(a, gamma, beta, {'mode': 'train'})
print_mean_std(a_norm,axis=1)

gamma = np.asarray([3.0,3.0,3.0])
beta = np.asarray([5.0,5.0,5.0])

# Now means should be close to beta and stds close to gamma.
print('After layer normalization (gamma=', gamma, ', beta=', beta, ')')
a_norm, _ = layernorm_forward(a, gamma, beta, {'mode': 'train'})
print_mean_std(a_norm,axis=1)
```

    Before layer normalization:
      means: [-59.06673243 -47.60782686 -43.31137368 -26.40991744]
      stds:  [10.07429373 28.39478981 35.28360729  4.01831507]

    After layer normalization (gamma=1, beta=0)
      means: [ 4.81096644e-16 -7.40148683e-17  2.22044605e-16 -5.92118946e-16]
      stds:  [0.99999995 0.99999999 1.         0.99999969]

    After layer normalization (gamma= [3. 3. 3.] , beta= [5. 5. 5.] )
      means: [5. 5. 5. 5.]
      stds:  [2.99999985 2.99999998 2.99999999 2.99999907]

```python
# Gradient check batchnorm backward pass.
np.random.seed(231)
N, D = 4, 5
x = 5 * np.random.randn(N, D) + 12
gamma = np.random.randn(D)
beta = np.random.randn(D)
dout = np.random.randn(N, D)

ln_param = {}
fx = lambda x: layernorm_forward(x, gamma, beta, ln_param)[0]
fg = lambda a: layernorm_forward(x, a, beta, ln_param)[0]
fb = lambda b: layernorm_forward(x, gamma, b, ln_param)[0]

dx_num = eval_numerical_gradient_array(fx, x, dout)
da_num = eval_numerical_gradient_array(fg, gamma.copy(), dout)
db_num = eval_numerical_gradient_array(fb, beta.copy(), dout)

_, cache = layernorm_forward(x, gamma, beta, ln_param)
dx, dgamma, dbeta = layernorm_backward(dout, cache)

# You should expect to see relative errors between 1e-12 and 1e-8.
print('dx error: ', rel_error(dx_num, dx))
print('dgamma error: ', rel_error(da_num, dgamma))
print('dbeta error: ', rel_error(db_num, dbeta))
```

    dx error:  0.5013824514038396
    dgamma error:  4.519489546032799e-12
    dbeta error:  2.276445013433725e-12

# Layer Normalization and Batch Size

We will now run the previous batch size experiment with layer normalization instead of batch normalization. Compared to the previous experiment, you should see a markedly smaller influence of batch size on the training history!

```python
ln_solvers_bsize, solver_bsize, batch_sizes = run_batchsize_experiments('layernorm')

plt.subplot(2, 1, 1)
plot_training_history('Training accuracy (Layer Normalization)','Epoch', solver_bsize, ln_solvers_bsize, \
                      lambda x: x.train_acc_history, bl_marker='-^', bn_marker='-o', labels=batch_sizes)
plt.subplot(2, 1, 2)
plot_training_history('Validation accuracy (Layer Normalization)','Epoch', solver_bsize, ln_solvers_bsize, \
                      lambda x: x.val_acc_history, bl_marker='-^', bn_marker='-o', labels=batch_sizes)

plt.gcf().set_size_inches(15, 10)
plt.show()
```

## Inline Question 4:
When is layer normalization likely to not work well, and why?

1. Using it in a very deep network
2. Having a very small dimension of features
3. Having a high regularization term

## Answer:
[FILL THIS IN]