```python
import torch
import torch.nn.functional as F

import torchsde
import math
import matplotlib.pyplot as plt

import numpy as np

from tqdm.notebook import tqdm

from torch import _vmap_internals
```

```python
from cfollmer.objectives import log_g, relative_entropy_control_cost, stl_relative_entropy_control_cost
from cfollmer.sampler_utils import FollmerSDE, SimpleForwardNetBN
from cfollmer.trainers import basic_batched_trainer
```

# The Model

\begin{align}
\theta &\sim \mathcal{N}(\theta | 0, \sigma_w^2 \mathbb{I}) \\
y_i | x_i, \theta &\sim  \mathcal{N}(y | \theta^\top x_i + \theta_0, \sigma_y^2 \mathbb{I})
\end{align}

We want samples from $p(\theta | \{(y_i, x_i)\})$.

## Loading the StepFunc dataset

```python
device = "cuda" if torch.cuda.is_available() else "cpu"

func = lambda x: 2* x + 1

# Test inputs
num_test_samples = 30
X_test = np.linspace(-4, 4, num_test_samples).reshape(num_test_samples,1)

# Noise free training inputs
X_train = np.linspace(-3.5, 3.5, 30).reshape(-1,1)
#f_train = np.cos(X_train)
f_train = func(X_train)

# Noise-free training outputs
#f = np.cos(X_test)
f = func(X_test)
y_test  = f

# Noisy training Inputs with additive Gaussian noise (zero-mean, variance sigma_n)
sigma_n = 0.5

mu = np.zeros(X_train.size)
epsilon = np.random.multivariate_normal(mu, sigma_n**2 * np.eye(X_train.size))

# Noisy targets
y_train = f_train + epsilon.reshape(X_train.size,1)

# Noisy observations
fig, ax = plt.subplots(figsize=(6, 4), tight_layout=True)
ax.plot(X_test, f, 'b', label = 'f(x)')
ax.plot(X_train, y_train,".",  label = 'y(x) = f(x) + $\epsilon$')
ax.legend(loc = 'upper left')
ax.set_title('Target function with noisy observations ')

plt.show()
```

```python
X_train.shape, f_train.shape
```

    ((30, 1), (30, 1))

```python
device = "cuda" if torch.cuda.is_available() else "cpu"

X_train, X_test, y_train, y_test = \
    torch.tensor(X_train, dtype=torch.float32, device=device), \
    torch.tensor(X_test, dtype=torch.float32, device=device), \
    torch.tensor(y_train, dtype=torch.float32, device=device), \
    torch.tensor(y_test, dtype=torch.float32, device=device)
```

$$\DeclareMathOperator*{\argmin}{arg\,min}$$
$$\def\E{{\mathbb{E}}}$$
$$\def\rvu{{\mathbf{u}}}$$
$$\def\rvTheta{{\bm{\Theta}}}$$
$$\def\gU{{\mathcal{U}}}$$
$$\def\mX{{\mathbf{X}}}$$

## Controlled Schrodinger Follmer Sampler

The objevtive we are trying to implement is:

\begin{align}
  \mathbf{u}_t^{*}=  \argmin_{\rvu_t \in \mathcal{U}}\mathbb{E}\left[\frac{1}{2\gamma}\int_0^1||\rvu(t, \Theta_t)||^2 dt - \ln\left(\frac{ p(\mX | \Theta_1)p(\Theta_1)}{\mathcal{N}(\Theta_1|\mathbf{0}, \gamma \mathbb{I} )}\right)\right] \
\end{align}

Where:
\begin{align}
d\Theta_t = \rvu(t, \Theta_t)dt + \sqrt{\gamma} dB_t
\end{align}

```python
import torch.nn.functional as F

class OnedRegressionForwardNet(object):

    def __init__(
        self, input_dim=1, output_dim=1, depth=None,
        width=20, width_seq=None, device="cpu", activation=F.relu
    ):

        self.device = device
        self.output_dim = output_dim
        self.input_dim = input_dim
        self.activation = activation
        self.shapes = []

        self.depth = depth
        if not self.depth:
            self.depth = 1
        if not width_seq and self.depth > -1:
            self.width = width
            self.width_seq = [self.width] * (self.depth + 1)
            self.shapes = [(self.width_seq[i-1], self.width_seq[i])  for i in range(1,self.depth)]
            self.shapes += [(self.width_seq[-1], self.output_dim)]
            self.shapes = [(self.input_dim, self.width_seq[0])] + self.shapes

        self.dim = sum([wx * wy + wy for wx, wy in self.shapes]) if self.depth > -1 else self.input_dim + 1

    def forward(self, x, Θ):
        index = 0
        n, d = x.shape

#         Θ = (Θ - Θ.mean()) / Θ.std()

        for wx, wy in self.shapes[:-1]:
            x = F.linear(
                x,
                Θ[index: index + wx * wy].reshape(wy, wx),
                Θ[index + wx * wy: index + wx * wy + wy].reshape(1,wy)
            )
            x = self.activation(x)
            index += wx * wy  + wy
#         import pdb; pdb.set_trace()
        wx, wy = self.shapes[-1] if self.shapes else (self.input_dim, self.output_dim)
#         import pdb; pdb.set_trace()
        x = F.linear(
            x,
            Θ[index: index + wx * wy].reshape(wy, wx),
            Θ[index + wx * wy: index + wx * wy + wy].reshape(1,wy)
        )
        return x.to(self.device)

    def map_forward(self, x, Θ):
        preds_func = lambda θ: self.forward(x, θ)
        batched_preds = torch._vmap_internals.vmap(preds_func)
        preds = torch.hstack(list(map(preds_func, Θ)))
        return preds

```

```python
net = OnedRegressionForwardNet(
    1,1, device=device, depth=-1, width=1, activation=F.relu
)

def ln_prior(Θ, σ_w=1.5):
    """
    Logistic regresion bayesian prior
    """
    return -0.5 * (Θ**2).sum(axis=1) / σ_w**2

def log_likelihood_vmap(Θ, X, y, net=net, σ_y=0.5):
    """
    Hoping this implementation is less buggy / faster

    still feels a bit slow.
    """
    preds_func = lambda θ: net.forward(X, θ)
#     batched_preds = torch._vmap_internals.vmap(preds_func)
#     import pdb; pdb.set_trace()
    preds = torch.hstack(list(map(preds_func, Θ)))

    diff = preds - y
#     import pdb; pdb.set_trace()
    diff_sq_fld = -0.5 * (diff**2).sum(dim=0) / σ_y**2

    return diff_sq_fld
```

```python

γ = 0.5
Δt = 0.01

sde, losses = basic_batched_trainer(
    γ, Δt, ln_prior, log_likelihood_vmap, net.dim, X_train, y_train=y_train,
    method="euler", stl=True, adjoint=False, optimizer=None,
    num_steps=400, batch_size_data=None, batch_size_Θ=50,
    batchnorm=True, device=device
)
```

      0%|          | 0/400 [00:00<?, ?it/s]

    c:\users\vargf\onedrive\documents\projects\controlledfollmerdrift\cfollmer\objectives.py:97: UserWarning: torch.vmap is an experimental prototype that is subject to change and/or deletion. Please use at your own risk. There may be unexpected performance cliffs due to certain operators not being implemented. To see detailed performance warnings please use `torch._C._debug_only_display_vmap_fallback_warnings(True) before the call to `vmap`.
      f = _vmap_internals.vmap(f_)
    c:\users\vargf\onedrive\documents\projects\controlledfollmerdrift\cfollmer\objectives.py:98: UserWarning: torch.vmap is an experimental prototype that is subject to change and/or deletion. Please use at your own risk. There may be unexpected performance cliffs due to certain operators not being implemented. To see detailed performance warnings please use `torch._C._debug_only_display_vmap_fallback_warnings(True) before the call to `vmap`.
      f_detached = _vmap_internals.vmap(sde.f_detached)

```python
torch.cuda.empty_cache()
```

```python
losses
```

    [26977.826171875,
     28747.9765625,
     25292.6328125,
     26358.103515625,
     25706.7890625,
     29131.22265625,
     27866.8515625,
     23903.33984375,
     25851.34375,
     26609.990234375,
     26499.349609375,
     26602.583984375,
     24546.19921875,
     24899.919921875,
     26768.8046875,
     25674.90625,
     26300.779296875,
     23568.888671875,
     24364.5546875,
     21115.91796875,
     25042.984375,
     24783.46875,
     24833.314453125,
     26055.09375,
     21933.96484375,
     21109.638671875,
     23920.775390625,
     21265.763671875,
     23479.490234375,
     19066.154296875,
     22337.60546875,
     19909.98828125,
     19296.720703125,
     20933.111328125,
     21305.20703125,
     20715.04296875,
     21529.94140625,
     18224.271484375,
     19750.53125,
     18432.69921875,
     18344.9140625,
     18693.10546875,
     17119.744140625,
     15886.603515625,
     12858.5283203125,
     16038.0673828125,
     15609.3994140625,
     14181.380859375,
     12114.1669921875,
     12311.4580078125,
     11952.357421875,
     9585.7490234375,
     7573.1005859375,
     8726.7568359375,
     8618.8232421875,
     8608.998046875,
     4774.63671875,
     6174.1923828125,
     3442.7373046875,
     3510.60595703125,
     4390.537109375,
     3235.971923828125,
     3711.7109375,
     4494.095703125,
     4260.98974609375,
     4325.41552734375,
     3904.11669921875,
     3530.556884765625,
     3676.64501953125,
     3914.88671875,
     3110.45751953125,
     3283.476806640625,
     4192.1767578125,
     2648.19189453125,
     2331.8486328125,
     3747.77294921875,
     3503.773681640625,
     2971.973388671875,
     3316.806884765625,
     2810.22998046875,
     2427.58251953125,
     2840.340576171875,
     2572.528564453125,
     2689.287841796875,
     3479.699951171875,
     3704.225830078125,
     2560.509765625,
     2222.35302734375,
     2365.885498046875,
     2361.185302734375,
     2941.826171875,
     3316.24267578125,
     2632.624267578125,
     3170.92236328125,
     2334.969970703125,
     2890.87158203125,
     3375.533447265625,
     2984.73388671875,
     3023.364990234375,
     2405.326904296875,
     2323.139404296875,
     2044.7506103515625,
     2726.79345703125,
     2857.191162109375,
     2737.27001953125,
     2651.8662109375,
     3054.19189453125,
     2536.81494140625,
     2578.516357421875,
     2160.623779296875,
     3310.46875,
     2319.47216796875,
     2252.244873046875,
     2558.380126953125,
     2900.706787109375,
     2724.62646484375,
     2789.752685546875,
     2544.89404296875,
     2411.3056640625,
     2227.51123046875,
     2951.471923828125,
     3406.85498046875,
     2568.15576171875,
     2076.73681640625,
     2655.491455078125,
     2582.919921875,
     2862.63427734375,
     2249.003173828125,
     2938.385498046875,
     2566.7109375,
     3175.347412109375,
     2386.175537109375,
     2201.866943359375,
     2564.864990234375,
     2347.088134765625,
     2198.7421875,
     2058.072509765625,
     2402.64404296875,
     2390.858642578125,
     2511.545654296875,
     2911.834228515625,
     2700.68798828125,
     2398.760009765625,
     2389.26953125,
     2687.4208984375,
     2630.919921875,
     2328.016845703125,
     2098.810546875,
     2850.27001953125,
     1930.1871337890625,
     2140.322509765625,
     2479.464599609375,
     2164.98583984375,
     2224.00732421875,
     1833.8712158203125,
     1771.837158203125,
     1808.024169921875,
     2333.448974609375,
     2263.822265625,
     2485.64306640625,
     2358.2529296875,
     2379.352294921875,
     2070.4296875,
     1737.9124755859375,
     2402.322021484375,
     1982.1082763671875,
     2020.0902099609375,
     1941.494384765625,
     1945.718994140625,
     2356.6328125,
     2098.34814453125,
     2222.951904296875,
     2428.724365234375,
     1912.1302490234375,
     2078.6689453125,
     2178.607177734375,
     2192.151123046875,
     1817.3468017578125,
     1705.7188720703125,
     1891.7840576171875,
     2161.5302734375,
     1965.086181640625,
     1911.3780517578125,
     2129.10302734375,
     2110.760498046875,
     1974.498291015625,
     2305.29248046875,
     1785.5140380859375,
     2145.65380859375,
     1428.1224365234375,
     1524.560791015625,
     2200.529052734375,
     1823.461181640625,
     2351.3974609375,
     1928.69140625,
     1944.416259765625,
     1861.3035888671875,
     2067.82470703125,
     1858.96484375,
     1954.00146484375,
     1522.60498046875,
     1845.7904052734375,
     1766.0552978515625,
     1978.4183349609375,
     1990.6995849609375,
     2099.74658203125,
     1725.360595703125,
     1782.80615234375,
     1694.114990234375,
     2395.12890625,
     1821.86962890625,
     1736.7178955078125,
     1496.164306640625,
     2041.068603515625,
     2008.9774169921875,
     2001.5343017578125,
     2132.092041015625,
     2160.679443359375,
     1886.13818359375,
     1799.8189697265625,
     1600.45751953125,
     1777.85888671875,
     1672.822265625,
     1965.416015625,
     1609.656005859375,
     1599.5235595703125,
     1707.9571533203125,
     1649.767822265625,
     1662.023193359375,
     1595.2642822265625,
     1805.9932861328125,
     1802.714599609375,
     1696.525634765625,
     1933.399658203125,
     1677.35791015625,
     1969.7376708984375,
     1768.844482421875,
     1544.6024169921875,
     1470.3529052734375,
     1726.70263671875,
     1748.755859375,
     1641.494384765625,
     1489.30517578125,
     1593.19970703125,
     1386.566650390625,
     1686.5859375,
     1610.1954345703125,
     1586.2796630859375,
     1521.0618896484375,
     1859.201171875,
     1671.637451171875,
     1638.4521484375,
     1743.2032470703125,
     1730.935302734375,
     1304.9652099609375,
     1309.87744140625,
     1833.89794921875,
     1794.0008544921875,
     1579.9854736328125,
     1870.8846435546875,
     1743.4627685546875,
     1446.546630859375,
     1646.8233642578125,
     1865.982177734375,
     1769.7373046875,
     1653.820068359375,
     1453.0068359375,
     1379.567138671875,
     1519.8968505859375,
     1463.542724609375,
     1276.0538330078125,
     1664.130615234375,
     1426.2493896484375,
     1778.5423583984375,
     1388.7550048828125,
     1733.539306640625,
     1536.18359375,
     1328.771240234375,
     1624.2242431640625,
     1580.9215087890625,
     1410.364013671875,
     1550.8538818359375,
     1495.097900390625,
     1524.126953125,
     1634.805908203125,
     1422.2076416015625,
     1345.6669921875,
     1575.484619140625,
     1403.0440673828125,
     1314.1121826171875,
     1441.83935546875,
     1743.02099609375,
     1375.7109375,
     1570.281982421875,
     1751.015625,
     1380.415283203125,
     1461.580322265625,
     1537.216552734375,
     1527.7255859375,
     1738.9354248046875,
     1402.299072265625,
     1332.389404296875,
     1280.5570068359375,
     1435.019287109375,
     1537.4205322265625,
     1345.4974365234375,
     1461.005859375,
     1138.1373291015625,
     1205.567138671875,
     1502.8218994140625,
     1386.03857421875,
     1283.20947265625,
     1328.894775390625,
     1274.91650390625,
     1483.5257568359375,
     1478.06201171875,
     1456.6810302734375,
     1337.9539794921875,
     1325.649658203125,
     1313.4482421875,
     1313.498779296875,
     1274.8970947265625,
     1714.591552734375,
     1222.166748046875,
     1450.0250244140625,
     1462.1849365234375,
     1595.242431640625,
     1249.55126953125,
     1354.523193359375,
     1283.425537109375,
     1150.4522705078125,
     1306.136474609375,
     1425.7293701171875,
     1212.1590576171875,
     1235.0921630859375,
     1434.0574951171875,
     1302.18408203125,
     1373.0531005859375,
     1225.2628173828125,
     1383.176025390625,
     1456.9940185546875,
     1401.9937744140625,
     1231.313720703125,
     1441.515625,
     1336.4168701171875,
     1387.8680419921875,
     1305.716796875,
     1124.9761962890625,
     1199.1644287109375,
     1083.48779296875,
     1207.8065185546875,
     1411.1490478515625,
     1295.046142578125,
     1268.705322265625,
     1146.2957763671875,
     1328.201171875,
     1408.42919921875,
     1419.74951171875,
     1315.9527587890625,
     1212.076904296875,
     1308.101806640625,
     1362.331787109375,
     1247.5111083984375,
     1238.8043212890625,
     1176.4063720703125,
     1427.9505615234375,
     1331.823974609375,
     1156.779541015625,
     1179.093994140625,
     1220.0242919921875,
     1286.0389404296875,
     1353.1328125,
     1172.0545654296875,
     1124.7501220703125,
     1291.6351318359375,
     1215.6654052734375,
     1089.4420166015625,
     1137.1925048828125,
     1198.5430908203125,
     1268.9305419921875,
     1163.6546630859375,
     1114.0206298828125,
     1011.6346435546875,
     1217.064453125,
     1158.7435302734375,
     1033.87890625,
     1293.097900390625,
     1116.50390625,
     1246.880615234375,
     1373.1490478515625,
     1129.9473876953125,
     1185.578369140625,
     1230.1729736328125,
     1196.252685546875,
     1392.620849609375,
     1172.822509765625,
     1410.0308837890625,
     1180.6290283203125,
     1136.2861328125,
     1168.1768798828125]

```python
plt.plot(losses[:])
```

```python
Θ_0 = torch.zeros((100, net.dim)).to(device)
t_size = int(math.ceil(1.0/Δt))
ts = torch.linspace(0, 1, t_size).to(device)

torch.cuda.empty_cache()

Θ_1 = torchsde.sdeint(sde, Θ_0, ts, dt=Δt)[-1,...]
```

```python
torch.cuda.empty_cache()
fig, (ax1,ax2,ax3) = plt.subplots(1,3)

ax1.hist(Θ_1[:,0].cpu().detach().numpy())
ax2.hist(Θ_1[:,1].cpu().detach().numpy())
# ax3.hist(Θ_1[:,-23].cpu().detach().numpy())
```

```python
# pred = net.map_forward(X_train, Θ_1.mean(dim=0).reshape(1,-1))#.mean(axis=1)
end = 6.5
X_train_2 = torch.linspace(-end, end, 100).reshape(-1,1).to(device)

pred_ = net.map_forward(X_train_2, Θ_1)
pred = pred_.mean(axis=1)

σ  = pred_.std(axis=1)
```

    C:\Users\vargf\AppData\Local\Temp/ipykernel_43580/4055719056.py:57: UserWarning: torch.vmap is an experimental prototype that is subject to change and/or deletion. Please use at your own risk. There may be unexpected performance cliffs due to certain operators not being implemented. To see detailed performance warnings please use `torch._C._debug_only_display_vmap_fallback_warnings(True) before the call to `vmap`.
      batched_preds = torch._vmap_internals.vmap(preds_func)

```python
σ.shape, pred.shape
```

    (torch.Size([100]), torch.Size([100]))

```python
fig, ax = plt.subplots(figsize=(10, 6), tight_layout=True)
ax.plot(X_test.detach().cpu(), f, 'b', label = 'f(x)')
ax.plot(X_train.detach().cpu(), y_train.detach().cpu(), 'kP', ms = 9, label = 'y(x) = f(x) + $\epsilon$')
ax.plot(X_train_2.detach().cpu(), pred.detach().cpu(), color="red", label="pred")

σ_p , σ_m = (pred-2*σ).detach().cpu().numpy(), (pred+2*σ).detach().cpu().numpy()
ax.fill_between(X_train_2.detach().cpu().flatten(), σ_p.flatten() , σ_m.flatten(), alpha=.2)
ax.legend(loc = 'upper left')
ax.set_title('Target function with noisy observations ')

plt.show()
```

```python

fig, (ax, ax1) = plt.subplots(1,2, figsize=(15, 5), tight_layout=True)
# ax.plot(X_test.detach().cpu(), f, 'b', label = 'f(x)')
ax.plot(X_train.detach().cpu(), y_train.detach().cpu(), 'kP', ms = 9, label = 'y(x) = f(x) + $\epsilon$')
# ax.plot(X_train.detach().cpu(), pred.detach().cpu(), color="red", label="pred")

zero_ind = torch.abs(X_train).argmin()

thresh = 10
label_2, label_1 = True, True

end=7.6
X_train_2 = torch.linspace(-end, end, 100).reshape(-1,1).to(device)

for i in range(50):
#     print(Θ_1[i,:3])
    θ = Θ_1[i,:,]
    pred = net.forward(X_train_2, θ).mean(axis=1)
    pred_min, pred_max = pred[zero_ind-thresh], pred[zero_ind+thresh]
    label = True if i==0 else False
    if torch.abs(pred_min - pred_max) < 0.25:
        ax.plot(
            X_train_2.detach().cpu(), pred.detach().cpu(),
            alpha=0.3, color="red", label="Posterior function Samples Flat" if label_1 else None
        )
        ax1.plot(θ.detach().cpu(),  alpha=0.5, color="red", label="Posterior weights Samples Flat"  if label_1 else None)
        label_1 = False
    else:
        ax.plot(
            X_train_2.detach().cpu(), pred.detach().cpu(),
            alpha=0.3, color="green", label="Posterior fuctions Samples Good" if label_2 else None
        )
        ax1.plot(θ.detach().cpu(),  alpha=0.5, color="green", label="Posterior weights Samples Good" if label_2 else None )
        label_2 = False

ax.legend(loc = 'upper left')
ax.set_title('Target function with noisy observations ')

ax1.legend(loc = 'upper left')
ax1.set_title('Sampled Posterior Weights')
# plt.plot(X_train.detach().cpu(), y_train.detach().cpu())

```

```python

```


# Feature Engineering & Importance Testing
Goal of this notebook is to get an overview of all the features found around different kernels in this competition, as well as add a few new ones, and test their importance for predicting is_attributed using xgBoost.

## Current best score with these features
Current I've got a public LB score of **0.9769** using V2 of these features, and the xgBoost parameters found by [Bayesian Optimization in this notebook](https://www.kaggle.com/nanomathias/bayesian-tuning-of-xgboost-lightgbm-lb-0-9769), training on the entire dataset.

## Inspirational Notebooks
Inspiration for features from (let me know if I have forgotten to give credit to anyone)
* https://www.kaggle.com/nuhsikander/lgbm-new-features-corrected
* https://www.kaggle.com/rteja1113/lightgbm-with-count-features
* https://www.kaggle.com/aharless/swetha-s-xgboost-revised
* https://www.kaggle.com/bk0000/non-blending-lightgbm-model-lb-0-977

# 1. Loading data
I'll just load a small subset of the data for more speedy testing

```python
import gc
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

# Load subset of the training data
X_train = pd.read_csv('../input/train.csv', nrows=1000000, parse_dates=['click_time'])

# Show the head of the table
X_train.head()
```

# 2. Creating Features
## 2.1 Extracting time information
First extract day, minute, hour, second from the click_time.

```python
X_train['day'] = X_train['click_time'].dt.day.astype('uint8')
X_train['hour'] = X_train['click_time'].dt.hour.astype('uint8')
X_train['minute'] = X_train['click_time'].dt.minute.astype('uint8')
X_train['second'] = X_train['click_time'].dt.second.astype('uint8')
X_train.head()
```

## 2.2. Confidence Rates for is_attributed
My thought is that some ips, apps, devices, etc. might have higher frequencies of is_attributed, and I wish to add that information, i.e. I'm calculating the following "attributed rates":

\begin{equation}
\text{P}\,\text{(is_attributed}\,\,|\,\,\text{category)}
\end{equation}

or in some cases two- or multiple-paired combinations:

\begin{equation}
\text{P}\,\text{(is_attributed}\,\,|\,\,\text{category_1, category_2)}
\end{equation}

The danger of this is that if a given category-combination has very few clicks, then the statistical significance of above equations cannot be trusted. Therefore I'll be weighing the rates by the following confidence rates:

\begin{equation}
    \text{conf}_{\text{is_attributed}} = \frac{\log(\text{views}_{\text{category_1}})}{\log(100000)}
\end{equation}

where the value 100000 has been chosen arbitrarily to such that if a given category has 1000 views, then it gets a confidence weight of 60%, if it has 100 views then onfly a confidence weight of 40% etc.

```python
ATTRIBUTION_CATEGORIES = [
    # V1 Features #
    ###############
    ['ip'], ['app'], ['device'], ['os'], ['channel'],

    # V2 Features #
    ###############
    ['app', 'channel'],
    ['app', 'os'],
    ['app', 'device'],

    # V3 Features #
    ###############
    ['channel', 'os'],
    ['channel', 'device'],
    ['os', 'device']
]

# Find frequency of is_attributed for each unique value in column
freqs = {}
for cols in ATTRIBUTION_CATEGORIES:

    # New feature name
    new_feature = '_'.join(cols)+'_confRate'

    # Perform the groupby
    group_object = X_train.groupby(cols)

    # Group sizes
    group_sizes = group_object.size()
    log_group = np.log(100000) # 1000 views -> 60% confidence, 100 views -> 40% confidence
    print(">> Calculating confidence-weighted rate for: {}.\n   Saving to: {}. Group Max /Mean / Median / Min: {} / {} / {} / {}".format(
        cols, new_feature,
        group_sizes.max(),
        np.round(group_sizes.mean(), 2),
        np.round(group_sizes.median(), 2),
        group_sizes.min()
    ))

    # Aggregation function
    def rate_calculation(x):
        """Calculate the attributed rate. Scale by confidence"""
        rate = x.sum() / float(x.count())
        conf = np.min([1, np.log(x.count()) / log_group])
        return rate * conf

    # Perform the merge
    X_train = X_train.merge(
        group_object['is_attributed']. \
            apply(rate_calculation). \
            reset_index(). \
            rename(
                index=str,
                columns={'is_attributed': new_feature}
            )[cols + [new_feature]],
        on=cols, how='left'
    )

X_train.head()
```

Locally I computed 10-fold cross-validation scores with 10million samples using xgBoost to see how each feature improved the model or not. The result was as follows:

On the public LB the score was improved from **0.955 -> 0.9624** by just including these features

## 2.3. Group-By-Aggregation
There are a lot of groupby -> count()/var()/mean() etc. feature engineering in the kernels I've checked out, so of course those have to be added as well :)

```python
# Define all the groupby transformations
GROUPBY_AGGREGATIONS = [

    # V1 - GroupBy Features #
    #########################
    # Variance in day, for ip-app-channel
    {'groupby': ['ip','app','channel'], 'select': 'day', 'agg': 'var'},
    # Variance in hour, for ip-app-os
    {'groupby': ['ip','app','os'], 'select': 'hour', 'agg': 'var'},
    # Variance in hour, for ip-day-channel
    {'groupby': ['ip','day','channel'], 'select': 'hour', 'agg': 'var'},
    # Count, for ip-day-hour
    {'groupby': ['ip','day','hour'], 'select': 'channel', 'agg': 'count'},
    # Count, for ip-app
    {'groupby': ['ip', 'app'], 'select': 'channel', 'agg': 'count'},
    # Count, for ip-app-os
    {'groupby': ['ip', 'app', 'os'], 'select': 'channel', 'agg': 'count'},
    # Count, for ip-app-day-hour
    {'groupby': ['ip','app','day','hour'], 'select': 'channel', 'agg': 'count'},
    # Mean hour, for ip-app-channel
    {'groupby': ['ip','app','channel'], 'select': 'hour', 'agg': 'mean'},

    # V2 - GroupBy Features #
    #########################
    # Average clicks on app by distinct users; is it an app they return to?
    {'groupby': ['app'],
     'select': 'ip',
     'agg': lambda x: float(len(x)) / len(x.unique()),
     'agg_name': 'AvgViewPerDistinct'
    },
    # How popular is the app or channel?
    {'groupby': ['app'], 'select': 'channel', 'agg': 'count'},
    {'groupby': ['channel'], 'select': 'app', 'agg': 'count'},

    # V3 - GroupBy Features                                              #
    # https://www.kaggle.com/bk0000/non-blending-lightgbm-model-lb-0-977 #
    ######################################################################
    {'groupby': ['ip'], 'select': 'channel', 'agg': 'nunique'},
    {'groupby': ['ip'], 'select': 'app', 'agg': 'nunique'},
    {'groupby': ['ip','day'], 'select': 'hour', 'agg': 'nunique'},
    {'groupby': ['ip','app'], 'select': 'os', 'agg': 'nunique'},
    {'groupby': ['ip'], 'select': 'device', 'agg': 'nunique'},
    {'groupby': ['app'], 'select': 'channel', 'agg': 'nunique'},
    {'groupby': ['ip', 'device', 'os'], 'select': 'app', 'agg': 'nunique'},
    {'groupby': ['ip','device','os'], 'select': 'app', 'agg': 'cumcount'},
    {'groupby': ['ip'], 'select': 'app', 'agg': 'cumcount'},
    {'groupby': ['ip'], 'select': 'os', 'agg': 'cumcount'},
    {'groupby': ['ip','day','channel'], 'select': 'hour', 'agg': 'var'}
]

# Apply all the groupby transformations
for spec in GROUPBY_AGGREGATIONS:

    # Name of the aggregation we're applying
    agg_name = spec['agg_name'] if 'agg_name' in spec else spec['agg']

    # Name of new feature
    new_feature = '{}_{}_{}'.format('_'.join(spec['groupby']), agg_name, spec['select'])

    # Info
    print("Grouping by {}, and aggregating {} with {}".format(
        spec['groupby'], spec['select'], agg_name
    ))

    # Unique list of features to select
    all_features = list(set(spec['groupby'] + [spec['select']]))

    # Perform the groupby
    gp = X_train[all_features]. \
        groupby(spec['groupby'])[spec['select']]. \
        agg(spec['agg']). \
        reset_index(). \
        rename(index=str, columns={spec['select']: new_feature})

    # Merge back to X_total
    if 'cumcount' == spec['agg']:
        X_train[new_feature] = gp[0].values
    else:
        X_train = X_train.merge(gp, on=spec['groupby'], how='left')

     # Clear memory
    del gp
    gc.collect()

X_train.head()
```

1. Locally I computed 10-fold cross-validation scores with 10million samples using xgBoost to see how each feature improved the model or not. The result was as follows:

On the public leaderboard the score was improved from **0.955 -> 0.9584** by just including these features

# 2.4. Time till next click
It might be interesting to know e.g. how long it takes for a given ip-app-channel before they perform the next click. So I'll create some features for these as well. ****

```python
GROUP_BY_NEXT_CLICKS = [

    # V1
    {'groupby': ['ip']},
    {'groupby': ['ip', 'app']},
    {'groupby': ['ip', 'channel']},
    {'groupby': ['ip', 'os']},

    # V3
    {'groupby': ['ip', 'app', 'device', 'os', 'channel']},
    {'groupby': ['ip', 'os', 'device']},
    {'groupby': ['ip', 'os', 'device', 'app']}
]

# Calculate the time to next click for each group
for spec in GROUP_BY_NEXT_CLICKS:

    # Name of new feature
    new_feature = '{}_nextClick'.format('_'.join(spec['groupby']))

    # Unique list of features to select
    all_features = spec['groupby'] + ['click_time']

    # Run calculation
    print(f">> Grouping by {spec['groupby']}, and saving time to next click in: {new_feature}")
    X_train[new_feature] = X_train[all_features].groupby(spec['groupby']).click_time.transform(lambda x: x.diff().shift(-1)).dt.seconds

X_train.head()
```

Locally I computed 10-fold cross-validation scores with 10million samples using xgBoost to see how each feature improved the model or not. The result was as follows:

On the public leaderboard the score was improved from **0.955 -> 0.9608** by just including these features

## 2.5. Clicks on app ad before & after
Has the user previously or subsequently clicked the exact same app-device-os-channel? I thought that might be an interesting feature to test out as well.

```python
HISTORY_CLICKS = {
    'identical_clicks': ['ip', 'app', 'device', 'os', 'channel'],
    'app_clicks': ['ip', 'app']
}

# Go through different group-by combinations
for fname, fset in HISTORY_CLICKS.items():

    # Clicks in the past
    X_train['prev_'+fname] = X_train. \
        groupby(fset). \
        cumcount(). \
        rename('prev_'+fname)

    # Clicks in the future
    X_train['future_'+fname] = X_train.iloc[::-1]. \
        groupby(fset). \
        cumcount(). \
        rename('future_'+fname).iloc[::-1]

# Count cumulative subsequent clicks
X_train.head()
```

Locally I computed 10-fold cross-validation scores with 10million samples using xgBoost to see how each feature improved the model or not. The result was as follows:

Possibly a very small improvement. This small improvement was also seen on public LB, where the score went from **0.955 -> 0.9568** by just including these features

# 3. Evaluating Feature Importance
Having created heaps of features, I'll fit xgBoost to the data, and evaluate the feature importances. First split into X and y

```python
import xgboost as xgb

# Split into X and y
y = X_train['is_attributed']
X = X_train.drop('is_attributed', axis=1).select_dtypes(include=[np.number])

# Create a model
# Params from: https://www.kaggle.com/aharless/swetha-s-xgboost-revised
clf_xgBoost = xgb.XGBClassifier(
    max_depth = 4,
    subsample = 0.8,
    colsample_bytree = 0.7,
    colsample_bylevel = 0.7,
    scale_pos_weight = 9,
    min_child_weight = 0,
    reg_alpha = 4,
    n_jobs = 4,
    objective = 'binary:logistic'
)
# Fit the models
clf_xgBoost.fit(X, y)
```

The feature importances are MinMax scaled, put into a DataFrame, and finally plotted ordered by the mean feature importance.

```python
from sklearn import preprocessing

# Get xgBoost importances
importance_dict = {}
for import_type in ['weight', 'gain', 'cover']:
    importance_dict['xgBoost-'+import_type] = clf_xgBoost.get_booster().get_score(importance_type=import_type)

# MinMax scale all importances
importance_df = pd.DataFrame(importance_dict).fillna(0)
importance_df = pd.DataFrame(
    preprocessing.MinMaxScaler().fit_transform(importance_df),
    columns=importance_df.columns,
    index=importance_df.index
)

# Create mean column
importance_df['mean'] = importance_df.mean(axis=1)

# Plot the feature importances
importance_df.sort_values('mean').plot(kind='bar', figsize=(20, 7))
```

# Evaluation on Public Leaderboard
I've evaluated the impact of all the above features on the public leaderboard - the results are as follows for the simple xgBoost model used in this notebook applyed to the entire training set, without any additional tuning etc.

```python

```

reference_kernels/TalkingData Feature Engineering & Importance Testing.ipynb


# **[HW6] DCGAN**
1. DataLoader
2. Model
3. Inception Score
4. Trainer
5. Train

이번 실습에서는 Convolution기반의 Generative Adversarial Network를 구현해서 이미지를 직접 생성해보는 실습을 진행해보겠습니다.

- dataset: CIFAR-10 (https://www.cs.toronto.edu/~kriz/cifar.html)
- model: DCGAN (https://arxiv.org/abs/1511.06434)
- evaluation: Inception Score (https://arxiv.org/abs/1801.01973)

## Import packages

런타임의 유형을 변경해줍니다.

상단 메뉴에서 [런타임]->[런타임유형변경]->[하드웨어가속기]->[GPU]

변경 이후 아래의 cell을 실행 시켰을 때, torch.cuda.is_avialable()이 True가 나와야 합니다.

```python
import torch
import torch.nn as nn
import torch.nn.functional as F
import torchvision
import torch.optim as optim
print(torch.__version__)
print(torch.cuda.is_available())
```

```python
import matplotlib.pyplot as plt
import numpy as np
import scipy as sp
import tqdm
import os
import random
import time
import datetime
from sklearn.datasets import fetch_openml
from sklearn.model_selection import train_test_split

# for reproducibility
np.set_printoptions(precision=3)
np.set_printoptions(suppress=True)
random.seed(1234)
torch.manual_seed(1234)
np.random.seed(1234)
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False
```

# 1. DataLoader

이전의 실습들에서 사용한것과 마찬가지로, pre-defined된 CIFAR-10 dataset을 활용해서 dataloader를 만들어 두겠습니다.

```python
from PIL import Image
from torch.utils import data
import torchvision
import torchvision.transforms as transforms

def create_dataloader(batch_size=64, num_workers=1):
    transform = transforms.Compose([transforms.ToTensor(),
                                    transforms.Normalize(mean=(0.5, 0.5, 0.5), std=(0.5, 0.5, 0.5))])

    trainset = torchvision.datasets.CIFAR10(root='./data/', train=True, transform=transform, download=True)
    testset = torchvision.datasets.CIFAR10(root='./data/', train=False, transform=transform, download=True)

    trainloader = data.DataLoader(dataset=trainset, batch_size=batch_size, shuffle=True, num_workers=num_workers)
    testloader = data.DataLoader(dataset=testset, batch_size=batch_size, shuffle=False, num_workers=num_workers)

    return trainloader, testloader
```

# 2. Model

이번 section에서는 DCGAN의 모델구조를 직접 구현해보도록 하겠습니다.

우선 본격적인 모델 구현에 앞서 GAN의 전체적인 구조에 대해 살펴보겠습니다.

GAN은 Generator와 Discriminator로 구성되어, Generator는 random latent vector를 받아 Discriminator를 속일 수 있는 fake image를 만들고, Discriminator는 real image와 fake image를 구분하는 형태로 학습이 진행되게 됩니다.

DCGAN은 image 데이터 처리에 효과적인 convolution layer를 활용하여 Generator와 Discriminator의 구조를 변형한 모델입니다.

DCGAN의 Generator와 Discriminator의 구조는 아래와 같습니다.

이 때, Generator는 output의 width와 height를 키우는 convolution을 진행해주어야 하기 때문에, standard한 convolution operation이 아닌 deconvolution 혹은 transpose convolution이라고 불리는 연산을 통해 output의 size를 키워주는 연산을 진행하게 됩니다.

반대로, Discriminator는 Generator와 대칭되는 구조를 통해 standard한 convolution을 사용하여 classification을 진행해주게 됩니다.

Transpose Convolution:(https://pytorch.org/docs/stable/generated/torch.nn.ConvTranspose2d.html)

## Convolution Block

우선, 모델을 쉽게 구현할 수 있도록, Generator와 Discriminator에서 반복적으로 사용할 convolution block고 deconvolution block을 정의해두도록 하겠습니다.

```python
def conv(c_in, c_out, k_size, stride=2, pad=1, bias=False, norm='bn', activation=None):
    layers = []

    # Conv.
    layers.append(nn.Conv2d(c_in, c_out, k_size, stride, pad, bias=bias))

    # Normalization
    if norm == 'bn':
        layers.append(nn.BatchNorm2d(c_out))
    elif norm == None:
        pass

    # Activation
    if activation == 'lrelu':
        layers.append(nn.LeakyReLU(0.2))
    elif activation == 'relu':
        layers.append(nn.ReLU())
    elif activation == 'tanh':
        layers.append(nn.Tanh())
    elif activation == 'sigmoid':
        layers.append(nn.Sigmoid())
    elif activation == None:
        pass

    return nn.Sequential(*layers)

def deconv(c_in, c_out, k_size, stride=2, pad=1, output_padding=0, bias=False, norm='bn', activation=None):
    layers = []

    # Deconv.
    layers.append(nn.ConvTranspose2d(c_in, c_out, k_size, stride, pad, output_padding, bias=bias))

    # Normalization
    if norm == 'bn':
        layers.append(nn.BatchNorm2d(c_out))
    elif norm == None:
        pass

    # Activation
    if activation == 'lrelu':
        layers.append(nn.LeakyReLU(0.2))
    elif activation == 'relu':
        layers.append(nn.ReLU())
    elif activation == 'tanh':
        layers.append(nn.Tanh())
    elif activation == 'sigmoid':
        layers.append(nn.Sigmoid())
    elif activation == None:
        pass

    return nn.Sequential(*layers)
```

## Generator

이제, 위에서 정의한 deconv block을 활용해서 DCGAN의 Generator를 구현해보도록 하겠습니다.

```python
class Generator(nn.Module):
    def __init__(self):
        super(Generator, self).__init__()
        model = []

        ### DCGAN Generator
        # You have to implement 4-layers generator.
        # Note: Recommend to use 'deconv' function
        ### YOUR CODE HERE (~ 4 lines)

        ### END YOUR CODE
        self.model = nn.Sequential(*model)

    def forward(self, z):
        # Input (z) size : [Batch, 256, 1, 1]
        # Output (Image) size : [Batch, 3, 32, 32]
        z = z.view(z.size(0), z.size(1), 1, 1)
        output = self.model(z)

        return output
```

## Discriminator

이제, 위에서 정의한 conv block을 활용해서 DCGAN의 Discriminator를 구현해보도록 하겠습니다.

```python
class Discriminator(nn.Module):
    def __init__(self):
        super(Discriminator, self).__init__()
        model = []

        ### DCGAN Discriminator
        # You have to implement 4-layers discriminator.
        # Note: Recommend to use 'conv' function
        ### YOUR CODE HERE (~ 4 lines)

        ### END YOUR CODE
        self.model = nn.Sequential(*model)

    def forward(self, x: torch.Tensor):
        # Input (z) size : [Batch, 3, 32, 32]
        # Output (probability) size : [Batch, 1]
        output = self.model(x).squeeze()

        return output
```

## Implementation Test

이제 Generator와 Discriminator를 맞게 구현했는지 test해보도록 하겠습니다.

체크를 위해서 코드와 함께 주어졌던 두개의 파일
- sanity_check_dcgan_netG.pth
- sanity_check_dcgan_netD.pth

를 왼쪽 상단에 [파일]->[세션 저장소에 업로드]를 눌러 업로드 하고, \\
아래의 코드를 실행시켜 코드가 통과되면 성공입니다.

```python
def test_model():
    print("=====Model Initializer Test Case======")
    netG = Generator()
    # the first test
    try:
        netG.load_state_dict(torch.load("sanity_check_dcgan_netG.pth", map_location='cpu'))
    except Exception as e:
        print("Your DCGAN generator initializer is wrong. Check the comments in details and implement the model precisely.")
        raise e
    print("The first test passed!")

    # the second test
    netD = Discriminator()
    try:
        netD.load_state_dict(torch.load("sanity_check_dcgan_netD.pth", map_location='cpu'))
    except Exception as e:
        print("Your DCGAN discriminator initializer is wrong. Check the comments in details and implement the model precisely.")
        raise e
    print("The second test passed!")
    print("All 2 tests passed!")

test_model()
```

# 3. Inception Score

비록 이제 dataloader와 model을 구현하였지만, 본격적으로 학습을 진행하기전 지도학습과 다르게 한가지 추가적으로 필요한 것이 있습니다.

기존의 지도학습 세팅에서는 loss나 validation accuracy를 통해서 학습이 원활히 진행되고 있는지 모니터링이 가능했지만, GAN에서는 generator가 비록 discriminator를 잘 속이고 있을지라도 (i.e., 낮은 loss) discriminator가 학습이 충분히 되지 못했다면 낮은 퀄리티의 이미지가 생성되게 됩니다.

이미지의 퀄리티를 측정하는 방법은 크게 2가지 입니다.
1. Fidelity(충실도): 얼마나 고품질의 이미지를 생성하는가?.
2. Diversity(다양성): 생성된 이미지들이 얼마나 다양한가? (e.g., 고양이만 생성하지 않음)

보통 Fidelity를 측정하기 위해서는 **Frechet Inception Distance**라는 metric이, Diversity를 측정하기 위해서는 **Inception Score**라는 evaluation metric이 사용되곤 합니다.

이번 실습에서는 이미지의 다양성을 측정하는 Inception Score를 통해 학습이 원활히 진행되고 있는지 모니터링 하도록 하겠습니다.

Inception score를 측정하는 방법은 아래와 같습니다.
1. Generator를 통해 이미지를 N개 생성한다.
2. 생성된 이미지들을 pre-trained된 inception network (=googleNet)에 통과시킨다.
3. inception network가 예측한 생성된 image의 label별 probability의 평균이 얼마나 diverse한지 측정한다.

Inception score에 대한 자세한 내용이 궁금하신 분은 아래를 참조해주세요
- https://arxiv.org/abs/1801.01973
- https://cyc1am3n.github.io/2020/03/01/is_fid.html

```python
from torchvision.models.inception import inception_v3
from scipy.stats import entropy

class Inception_Score():
    def __init__(self, dataset):
        self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

        # Dataset & DataLoader
        self.N = len(dataset)
        self.batch_size = 64

        self.dataset = dataset
        self.dataloader = data.DataLoader(dataset=dataset, batch_size=self.batch_size, num_workers=1)
        self.transform = nn.Upsample(size=(299, 299), mode='bilinear').to(self.device)

        # Inception Model
        self.inception_model = inception_v3(pretrained=True, transform_input=False).to(self.device)
        self.inception_model.eval()

    def get_pred(self, x):
        with torch.no_grad():
            x = self.transform(x)
            x = self.inception_model(x)
            return F.softmax(x, dim=1).data.cpu().numpy()

    def compute_score(self, splits=1):
        preds = np.zeros((self.N, 1000))

        for i, batch in tqdm.tqdm(enumerate(self.dataloader)):
            batch = batch.to(self.device)
            batch_size_i = batch.size(0)
            preds[i * self.batch_size : i * self.batch_size + batch_size_i] = self.get_pred(batch)

        # Compute the mean KL-divergence
        # You have to calculate the inception score.
        # The logit values from inception model are already stored in 'preds'.
        inception_score = 0.0
        split_scores = []
        for k in tqdm.tqdm(range(splits)):
            part = preds[k * (self.N // splits): (k + 1) * (self.N // splits), :]
            py = np.mean(part, axis=0)
            scores = []
            for i in range(part.shape[0]):
                pyx = part[i, :]
                scores.append(entropy(pyx, py))
            split_scores.append(np.exp(np.mean(scores)))
        inception_score = np.mean(split_scores)

        return inception_score
```

```python
def test_inception_score():
    print("======Inception Score Test Case======")

    # CIFAR10 Datset without Label
    class CIFAR10woLabel():
        def __init__(self):
            transform = transforms.Compose([transforms.ToTensor(),
                                            transforms.Normalize(mean=(0.5, 0.5, 0.5), std=(0.5, 0.5, 0.5))])
            self.dataset = torchvision.datasets.CIFAR10(root='./data/', download=True, transform=transform)

        def __getitem__(self, index):
            return self.dataset[index][0]

        def __len__(self):
            return len(self.dataset)

    print("Calculating Inception Score...")

    Inception = Inception_Score(CIFAR10woLabel())
    score = Inception.compute_score(splits=1)

    assert np.allclose(score, 9.719672, atol=1e-3), \
        "Your inception score does not match expected result."

    print("All test passed!")

test_inception_score()
```

# 4. Trainer

이제 앞서 선언한 dataloader, model, evaluator를 모두 활용해서 GAN을 학습시키는 Trainer를 구현해보도록 하겠습니다.

## Preliminary

\begin{equation}
D_{\theta}: \text{Discriminator network}\\
G_{\phi}: \text{Generator network}\\
x: \text{real_image} \\
z: \text{latent_vector} \\
\end{equation}

## Discriminator Loss

\begin{equation}
\mathcal{L}_{D_{\theta}} = -E_{x \sim p_{data}}[logD_{\theta}(x) + E_{z}[\log(1 - D_{\theta}(G_{\phi}(z)))]]
\end{equation}

Discriminator loss는 위와 같이 real_image는 1으로, generated_image는 0으로 판별하는 방식으로 학습을 진행하게 됩니다.

## Generator Loss

Generator network는 이론적으로는 discriminator의 loss에서 generator가 해당되는 부분에 -1을 곱해서 표현할 수 있습니다.

\begin{equation}
\mathcal{L}_{G_{\phi}} = E_{z}[\log(1-D_{\theta}(G_{\phi}(z))]  \tag{1}
\end{equation}

하지만, 위의 식으로 학습을 진행할 경우 Generator의 학습이 원활히 이루어지지 않게되는 문제점이 있습니다.

```python
plt.title('log(1-D(G(z))')
x = np.arange(0, 1.0, 0.01)
y = np.log(1-x)
plt.xlabel('D(G(z))')
plt.plot(x,y)
```

위의 loss plot에서 볼 수 있듯이, Generator는 Discriminator를 속이는 것에 성공할 수록 $D_{\theta}(G_{\phi}(z)) \approx 1$ 낮은 loss를 갖게 됩니다.

하지만, 이미지 생성의 난이도를 생각하면, 학습 초반에 Discriminator에 비해 Generator가 못하는 일은 자명한 일입니다. 이 때, D(G(z))가 0에 가까운 지점 $D_{\theta}(G_{\phi}(z)) \approx 0$ 에서의 함수의 기울기가 너무 작기 때문에 학습 초반에 Generator가 충분한 양의 학습 시그널을 받지 못하게 되는 문제점이 발생하게 됩니다.

따라서, 위의 식과 직관적으로 유사한 의미를 가지는 다른 loss function을 정의해보도록 하겠습니다.

\begin{equation}
\mathcal{L}_{G_{\phi}} = -E_{z}[\log(D_{\theta}(G_{\phi}(z))]  \tag{2}
\end{equation}

```python
plt.title('-log(D(G(z))')
x = np.arange(0, 1.0, 0.01)
y = -np.log(x)
plt.xlabel('D(G(z))')
plt.plot(x,y)
```

위의 loss plot에서 볼 수 있듯이, Generator는 여전히 Discriminator를 속이는 것에 성공할 수록 $D_{\theta}(G_{\phi}(z)) \approx 1$ 낮은 loss를 갖게 됩니다.

하지만, 이전과는 달리 $D_{\theta}(G_{\phi}(z)) \approx 0$에서의 gradient가 크기 때문에 학습 초반에 이미지를 생성하지 못할 때 오히려 충분한 양의 학습 시그널을 받을 수 있게 됩니다.

따라서, 이번 과제에서는 Generator의 Loss로 두번째 식을 사용하도록 하겠습니다.

```python
# Utility Functions
def denorm(x):
    out = (x + 1) / 2
    return out.clamp(0, 1)

def save_checkpoint(model, save_path, device):
    if not os.path.exists(os.path.dirname(save_path)):
        os.makedirs(os.path.dirname(save_path))
    torch.save(model.cpu().state_dict(), save_path)
    model.to(device)

def load_checkpoint(model, checkpoint_path, device):
    if not os.path.exists(checkpoint_path):
        print("Invalid path!")
        return
    model.load_state_dict(torch.load(checkpoint_path))
    model.to(device)

class FolderDataset(data.Dataset):
    def __init__(self, folder):
        self.folder = folder
        self.image_list = os.listdir(folder)
        self.transform = transforms.Compose([transforms.ToTensor(),
                                             transforms.Normalize(mean=(0.5, 0.5, 0.5), std=(0.5, 0.5, 0.5))])

    def __getitem__(self, index):
        image = Image.open(os.path.join(self.folder, self.image_list[index]))
        return self.transform(image)

    def __len__(self):
        return len(self.image_list)

# Trainer
class Trainer():
    def __init__(self,
                 trainloader,
                 testloader,
                 generator,
                 discriminator,
                 criterion,
                 g_optimizer,
                 d_optimizer,
                 device):
        """
        trainloader: train data's loader
        testloader: test data's loader
        generator: generator
        discriminator: discriminator
        criterion: loss function to evaluate the model (e.g., BCE Loss)
        g_optimizer: optimizer for generator
        d_optimizer: optimizer for discriminator
        """
        self.trainloader = trainloader
        self.testloader = testloader
        self.G = generator
        self.D = discriminator
        self.criterion = criterion
        self.g_optimizer = g_optimizer
        self.d_optimizer = d_optimizer
        self.device = device

        # Make directory to save the images & models for a specific checkpoint
        os.makedirs(os.path.join('./results/', 'images'), exist_ok=True)
        os.makedirs(os.path.join('./results/', 'checkpoints'), exist_ok=True)
        os.makedirs(os.path.join('./results/', 'evaluation'), exist_ok=True)

    def train(self, epochs = 1):
        self.G.to(self.device)
        self.D.to(self.device)

        start_time = time.time()
        for epoch in range(epochs):
            for iter, (real_img, _) in enumerate(self.trainloader):
                self.G.train()
                self.D.train()

                batch_size = real_img.size(0)
                real_label = torch.ones(batch_size).to(self.device)
                fake_label = torch.zeros(batch_size).to(self.device)
                # get real CIFAR-10 image
                real_img = real_img.to(self.device)
                # initialize latent_vector to feed into the Generator
                z = torch.randn(real_img.size(0), 256).to(self.device)

                ##########################################################################################
                # Discriminator Loss 구현                                                                #
                # Note : Discriminator Loss는 Generator network의 parameter에 영향을 주지 않아야 합니다. #
                #        detach() function을 참고하세요.                                                 #
                #        https://pytorch.org/docs/stable/generated/torch.Tensor.detach.html              #
                ##########################################################################################
                D_loss: torch.Tensor = None
                ### YOUR CODE HERE (~ 4 lines)

                ### END YOUR CODE

                # TEST CODE
                # (TEST의 통과가 맞는 구현을 보장하지는 못합니다. 일반적으로는 loss가 1.38~1.45 사이의 값이 나와야 합니다.)
                if epoch == 0 and iter == 0:
                    assert D_loss.detach().allclose(torch.tensor(1.4000), atol=2e-1), \
                    f"Discriminator Loss of the model does not match expected result."
                    print("==Discriminator loss function test passed!==")

                self.D.zero_grad()
                D_loss.backward()
                self.d_optimizer.step()

                #######################################################
                # Generator Loss 구현                                 #
                # Note : 위의 정의된 두번 째 식을 사용해서 구현하세요 #
                #######################################################
                G_loss: torch.Tensor = None
                ### YOUR CODE HERE (~ 3 lines)

                ### END YOUR CODE

                # Test code
                # (TEST의 통과가 맞는 구현을 보장하지는 못합니다. 일반적으로는 loss가 1.35~1.52 사이의 값이 나와야 합니다.)
                if epoch == 0 and iter == 0:
                    assert G_loss.detach().allclose(torch.tensor(1.5), atol=2e-1), \
                    f"Generator Loss of the model does not match expected result."
                    print("==Generator loss function test passed!==")

                self.G.zero_grad()
                G_loss.backward()
                self.g_optimizer.step()

            # verbose
            end_time = time.time() - start_time
            end_time = str(datetime.timedelta(seconds=end_time))[:-7]
            print('Time [%s], Epoch [%d/%d], lossD: %.4f, lossG: %.4f'
                  % (end_time, epoch+1, epochs, D_loss.item(), G_loss.item()))

            # Save Images
            fake_img = fake_img.reshape(fake_img.size(0), 3, 32, 32)
            torchvision.utils.save_image(denorm(fake_img), os.path.join('./results/', 'images', 'fake_image-{:03d}.png'.format(epoch+1)))
            if epoch % 10 == 0:
                self.test()

        # Save Checkpoints
        save_checkpoint(self.G, os.path.join('./results', 'checkpoints', 'G_final.pth'), self.device)
        save_checkpoint(self.D, os.path.join('./results', 'checkpoints', 'D_final.pth'), self.device)

    def test(self):
        print('Start computing Inception Score')
        self.G.eval()
        with torch.no_grad():
            for iter in tqdm.tqdm(range(5000)):
                z = torch.randn(1, 256).to(self.device)
                fake_img = self.G(z)
                torchvision.utils.save_image(denorm(fake_img), os.path.join('./results/', 'evaluation', 'fake_image-{:03d}.png'.format(iter)))

        # Compute the Inception score
        dataset = FolderDataset(folder = os.path.join('./results/', 'evaluation'))
        Inception = Inception_Score(dataset)
        score = Inception.compute_score(splits=1)
        print('Inception Score : ', score)
```

### Train

자, 이제 학습을 진행해 보겠습니다.
학습이 진행됨에 따라 generator가 생성하는 image는 \\
[파일]->[results]->[images]에서 각 epoch별로 확인해보실 수 있습니다.

```python
lr = 2e-4

trainloader, testloader = create_dataloader()
G = Generator()
D = Discriminator()
criterion = nn.BCELoss()
g_optimizer = optim.Adam(G.parameters(), lr=lr, betas=(0.5, 0.999))
d_optimizer = optim.Adam(D.parameters(), lr=lr, betas=(0.5, 0.999))
device = torch.device('cuda')

trainer = Trainer(trainloader=trainloader,
                  testloader=testloader,
                  generator=G,
                  discriminator=D,
                  criterion=criterion,
                  g_optimizer=g_optimizer,
                  d_optimizer=d_optimizer,
                  device=device)

trainer.train(epochs=50)
```

```python

```


```python
try:
    import openmdao.api as om
    import dymos as dm
except ImportError:
    !python -m pip install openmdao[notebooks]
    !python -m pip install dymos[docs]
    import openmdao.api as om
    import dymos as dm
```

(examples:minimium_time_climb)=
# Supersonic Interceptor Minimum Time Climb

This example is based on the _A/C Min Time to Climb_ example given in
chapter 4 of Bryson {cite}`bryson1999dynamic`. It finds the
angle-of-attack history required to accelerate a supersonic interceptor
from near ground level, Mach 0.4 to an altitude of 20 km and Mach 1.0.

The vehicle dynamics are given by

\begin{align}
  \frac{dv}{dt} &= \frac{T}{m} \cos \alpha - \frac{D}{m} - g \sin \gamma \\
  \frac{d\gamma}{dt} &= \frac{T}{m v} \sin \alpha + \frac{L}{m v} - \frac{g \cos \gamma}{v} \\
  \frac{dh}{dt} &= v \sin \gamma \\
  \frac{dr}{dt} &= v \cos \gamma \\
  \frac{dm}{dt} &= - \frac{T}{g I_{sp}}
\end{align}

The initial conditions are

\begin{align}
  r_0 &= 0 \rm{\,m} \\
  h_0 &= 100 \rm{\,m} \\
  v_0 &= 135.964 \rm{\,m/s} \\
  \gamma_0 &= 0 \rm{\,deg} \\
  m_0 &= 19030.468 \rm{\,kg}
\end{align}

and the final conditions are

\begin{align}
  h_f &= 20000 \rm{\,m} \\
  M_f &= 1.0 \\
  \gamma_0 &= 0 \rm{\,deg}
\end{align}

## The ODE System: min_time_climb_ode.py

The top level ODE definition is a _Group_ that connects several subsystems.

```python
om.display_source("dymos.examples.min_time_climb.doc.aero_partial_coloring")
```

```python
import openmdao.api as om
from dymos.models.eom import FlightPathEOM2D
from dymos.examples.min_time_climb.prop import PropGroup
from dymos.models.atmosphere import USatm1976Comp
from dymos.examples.min_time_climb.doc.aero_partial_coloring import AeroGroup

class MinTimeClimbODE(om.Group):

    def initialize(self):
        self.options.declare('num_nodes', types=int)
        self.options.declare('fd', types=bool, default=False, desc='If True, use fd for partials')
        self.options.declare('partial_coloring', types=bool, default=False,
                             desc='If True and fd is True, color the approximated partials')

    def setup(self):
        nn = self.options['num_nodes']

        self.add_subsystem(name='atmos',
                           subsys=USatm1976Comp(num_nodes=nn),
                           promotes_inputs=['h'])

        self.add_subsystem(name='aero',
                           subsys=AeroGroup(num_nodes=nn,
                                            fd=self.options['fd'],
                                            partial_coloring=self.options['partial_coloring']),
                           promotes_inputs=['v', 'alpha', 'S'])

        self.connect('atmos.sos', 'aero.sos')
        self.connect('atmos.rho', 'aero.rho')

        self.add_subsystem(name='prop',
                           subsys=PropGroup(num_nodes=nn),
                           promotes_inputs=['h', 'Isp', 'throttle'])

        self.connect('aero.mach', 'prop.mach')

        self.add_subsystem(name='flight_dynamics',
                           subsys=FlightPathEOM2D(num_nodes=nn),
                           promotes_inputs=['m', 'v', 'gam', 'alpha'])

        self.connect('aero.f_drag', 'flight_dynamics.D')
        self.connect('aero.f_lift', 'flight_dynamics.L')
        self.connect('prop.thrust', 'flight_dynamics.T')

```

## Building and running the problem

In the following code we follow the following process to solve the
problem:

```python
import matplotlib.pyplot as plt

import openmdao.api as om

import dymos as dm
from dymos.examples.plotting import plot_results

#
# Instantiate the problem and configure the optimization driver
#
p = om.Problem(model=om.Group())

p.driver = om.pyOptSparseDriver()
p.driver.options['optimizer'] = 'SLSQP'
p.driver.declare_coloring()

#
# Instantiate the trajectory and phase
#
traj = dm.Trajectory()

phase = dm.Phase(ode_class=MinTimeClimbODE,
                 transcription=dm.GaussLobatto(num_segments=15, compressed=False))

traj.add_phase('phase0', phase)

p.model.add_subsystem('traj', traj)

#
# Set the options on the optimization variables
# Note the use of explicit state units here since much of the ODE uses imperial units
# and we prefer to solve this problem using metric units.
#
phase.set_time_options(fix_initial=True, duration_bounds=(50, 400),
                       duration_ref=100.0)

phase.add_state('r', fix_initial=True, lower=0, upper=1.0E6, units='m',
                ref=1.0E3, defect_ref=1.0E3,
                rate_source='flight_dynamics.r_dot')

phase.add_state('h', fix_initial=True, lower=0, upper=20000.0, units='m',
                ref=1.0E2, defect_ref=1.0E2,
                rate_source='flight_dynamics.h_dot')

phase.add_state('v', fix_initial=True, lower=10.0, units='m/s',
                ref=1.0E2, defect_ref=1.0E2,
                rate_source='flight_dynamics.v_dot')

phase.add_state('gam', fix_initial=True, lower=-1.5, upper=1.5, units='rad',
                ref=1.0, defect_ref=1.0,
                rate_source='flight_dynamics.gam_dot')

phase.add_state('m', fix_initial=True, lower=10.0, upper=1.0E5, units='kg',
                ref=1.0E3, defect_ref=1.0E3,
                rate_source='prop.m_dot')

phase.add_control('alpha', units='deg', lower=-8.0, upper=8.0, scaler=1.0,
                  rate_continuity=True, rate_continuity_scaler=100.0,
                  rate2_continuity=False)

phase.add_parameter('S', val=49.2386, units='m**2', opt=False, targets=['S'])
phase.add_parameter('Isp', val=1600.0, units='s', opt=False, targets=['Isp'])
phase.add_parameter('throttle', val=1.0, opt=False, targets=['throttle'])

#
# Setup the boundary and path constraints
#
phase.add_boundary_constraint('h', loc='final', equals=20000, scaler=1.0E-3)
phase.add_boundary_constraint('aero.mach', loc='final', equals=1.0)
phase.add_boundary_constraint('gam', loc='final', equals=0.0)

phase.add_path_constraint(name='h', lower=100.0, upper=20000, ref=20000)
phase.add_path_constraint(name='aero.mach', lower=0.1, upper=1.8)

# Minimize time at the end of the phase
phase.add_objective('time', loc='final', ref=1.0)

p.model.linear_solver = om.DirectSolver()

#
# Setup the problem and set the initial guess
#
p.setup(check=True)

p['traj.phase0.t_initial'] = 0.0
p['traj.phase0.t_duration'] = 500

p.set_val('traj.phase0.states:r', phase.interp('r', [0.0, 50000.0]))
p.set_val('traj.phase0.states:h', phase.interp('h', [100.0, 20000.0]))
p.set_val('traj.phase0.states:v', phase.interp('v', [135.964, 283.159]))
p.set_val('traj.phase0.states:gam', phase.interp('gam', [0.0, 0.0]))
p.set_val('traj.phase0.states:m', phase.interp('m', [19030.468, 10000.]))
p.set_val('traj.phase0.controls:alpha', phase.interp('alpha', [0.0, 0.0]))

#
# Solve for the optimal trajectory
#
dm.run_problem(p)

#
# Get the explicitly simulated solution and plot the results
#
exp_out = traj.simulate()

plot_results([('traj.phase0.timeseries.time', 'traj.phase0.timeseries.states:h',
               'time (s)', 'altitude (m)'),
              ('traj.phase0.timeseries.time', 'traj.phase0.timeseries.controls:alpha',
               'time (s)', 'alpha (deg)')],
             title='Supersonic Minimum Time-to-Climb Solution',
             p_sol=p, p_sim=exp_out)

plt.show()
```

```python
from openmdao.utils.assert_utils import assert_near_equal

assert_near_equal(p.get_val('traj.phase0.t_duration'), 321.0, tolerance=1.0E-1)
```

## References

```{bibliography}
:filter: docname in docnames
```