Commit 671e5f42 authored by hazrmard's avatar hazrmard
Browse files

working on TorchEstimator

parent bbf9a6db
Loading
Loading
Loading
Loading
+1 −1
Changes for docs/8-models.md: 1 added line, 1 removed line.
Original line number Diff line number Diff line
@@ -123,7 +123,7 @@ The following results are obtained:
| Cluster      	| Coefficient of determination 	|
|--------------	|------------------------------	|
| == 0         	| 0.97                         	|
| < 0 & < 0.95 	| 0.67                         	|
| 0 < & < 0.95 	| 0.67                         	|
| > 0.95       	| 0.87                         	|

Giving a weighed coefficient of determination of 0.86.
+85 −1
Changes for src/Model.ipynb: 85 added lines, 1 removed line.
Original line number Diff line number Diff line
%% Cell type:code id: tags:

``` python
%matplotlib notebook
%reload_ext autoreload
%autoreload 2

import datetime
from os import path, environ
from copy import deepcopy
from multiprocessing import Pool

import numpy as np
import pandas as pd
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.neural_network import MLPRegressor
from sklearn.model_selection import train_test_split
import torch
import torch.nn as nn
import torch.nn.functional as F

from models import fit_composite_model
from models import fit_composite_model, contiguous_sequences, TorchEstimator
# source file, see docs/5-dataset.md for info on field names
chiller_file = path.join(environ['DATADIR'], 'EngineeringScienceBuilding', 'Chillers.csv')
plot_path = path.join('..', 'docs', 'img')
```

%% Cell type:code id: tags:

``` python
# Read pre-processed data
df = pd.read_csv(chiller_file, index_col='Time', parse_dates=True, dtype=float)
df.dropna(inplace=True)
```

%% Cell type:markdown id: tags:

# Evaporative Cooling Model

Modelling the relationship between the cooling done by the cooling tower and the environmental, system, and control inputs.

$$
\begin{align*}
T(t) &= T(0) e^{-\frac{k T_a v_f R}{T_w c_m m} t}
\end{align*}
$$

Where:

* $T(0)$ is the warm entering water temperature `TempCondOut`,
* $T(t)$ is the exiting cool water temperature `TempCondIn`,
* $T_a$ is ambient air temperature `TempAmbient`,
* $v_f$ is avarage fan speed `0.5 * (PerFreqFanA + PerFreqFanB)`,
* $R$ is solar irradiance - constant if assuming shade.
* $T_w$ is wet-bulb temperature `TempWetBulb`,
* $c_m$ is specific mass heat capacity of water,
* $m$ is the mass of water being cooled - constant if assuming steady flow rate.

The model predicts $T(t)$ from all other factors.

## Multi-Layer Perceptron

The motivation for an MLP model is the assumption that inputs manifest instantaneously as outputs with not delay. This is a simplification as water takes some time to cycle through the cooling tower.

The inputs are chosen assuming steady flow rate, constant irradiance.

The control variables are `PerFreqFanA` and `PerFreqFanB` which usually track each other. They are distributed bi-modally around 100% and 0% power with a minority of samples falling in between. A concern is that the model may learn to treat control inputs as constant. Two approaches are chosen:

* A single model is learned over all data,
* Samples are clustered by control=0, control >= 0.95, and 0 < control  < 0.95. A separate model is learned for each cluster.

%% Cell type:code id: tags:

``` python
# Data
feature_cols = ['TempCondOut', 'PerFreqFanA', 'PerFreqFanB', 'TempAmbient', 'TempWetBulb']
X, Y = df.loc[:, feature_cols], df['TempCondIn']
# setting up cluster selectors
c1 = df['PerFreqFanA'] >= 0.95
c0 = df['PerFreqFanA'] == 0
cmid = ~ (c1 | c0)
# generating clusters
X1, Y1 = X[c1], Y[c1]
Xmid, Ymid = X[cmid], Y[cmid]
X0, Y0 = X[c0], Y[c0]
# generating training/testing sets for each cluster
Xtrain, Xtest, Ytrain, Ytest = train_test_split(X, Y, test_size=0.1)
X1train, X1test, Y1train, Y1test = train_test_split(X1, Y1, test_size=0.1)
Xmidtrain, Xmidtest, Ymidtrain, Ymidtest = train_test_split(Xmid, Ymid, test_size=0.1)
X0train, X0test, Y0train, Y0test = train_test_split(X0, Y0, test_size=0.1)
```

%% Cell type:markdown id: tags:

### Single MLP

%% Cell type:code id: tags:

``` python
# Pipeline
scaler = StandardScaler()              # scale to 0 mean and unit variance
regressor = MLPRegressor(hidden_layer_sizes=(20,20),
                         learning_rate_init=1e-3,
                         verbose=True) # regression model
est = Pipeline([('scaler', scaler), ('regressor', regressor)])
```

%% Cell type:code id: tags:

``` python
est.fit(Xtrain, Ytrain)
```

%% Cell type:code id: tags:

``` python
print('Score on all control:\t{:.4f}'.format(est.score(Xtest, Ytest)))
print('Score on >95%-control:\t{:.4f}'.format(est.score(X0test, Y0test)))
print('Score on mid-control:\t{:.4f}'.format(est.score(X1test, Y1test)))
print('Score on 0%-control:\t{:.4f}'.format(est.score(Xmidtest, Ymidtest)))
```

%% Cell type:markdown id: tags:

### Composite MLP

%% Cell type:code id: tags:

``` python
scaler = StandardScaler()              # scale to 0 mean and unit variance
regressor = MLPRegressor(hidden_layer_sizes=(20,20),
                         learning_rate_init=1e-3,
                         verbose=False,
                         max_iter=1000,
                         solver='adam') # regression model
est = Pipeline([('scaler', scaler), ('regressor', regressor)])

est1, estmid, est0 = fit_composite_model(est, zip((X1train, Xmidtrain, X0train),
                                                  (Y1train, Ymidtrain, Y0train)))
print('Loss on >95%-control:\t{:.4f}'.format(est1.named_steps['regressor'].loss_))
print('Loss on mid-control:\t{:.4f}'.format(estmid.named_steps['regressor'].loss_))
print('Loss on 0%-control:\t{:.4f}'.format(est0.named_steps['regressor'].loss_))
```

%% Cell type:code id: tags:

``` python
score1 = est1.score(X1test, Y1test)
scoremid = estmid.score(Xmidtest, Ymidtest)
score0 = est0.score(X0test, Y0test)
scoreall = (len(X1test)*score1 + len(Xmidtest)*scoremid + len(X0test)*score0) / len(Xtest)
print('Score on all control:\t{:.4f}'.format(scoreall))
print('Score on >95%-control:\t{:.4f}'.format(score1))
print('Score on mid-control:\t{:.4f}'.format(scoremid))
print('Score on 0%-control:\t{:.4f}'.format(score0))
```

%% Output

    Score on all control:	0.8641
    Score on >95%-control:	0.8700
    Score on mid-control:	0.6506
    Score on 0%-control:	0.9793

%% Cell type:markdown id: tags:

## LSTM

The MLP model assumes no delay between input variables and output. This is a simplifying assumption. Water takes some time to travel through the cooling tower. So the temperature of the water coming out is a function of a history of the tower's prior states *and* the current inputs.

%% Cell type:code id: tags:

``` python
# indices of contiguous sequences in df
seqIdx = contiguous_sequences(df.index, pd.Timedelta('5T'))
```

%% Cell type:code id: tags:

``` python
# creating an LSTM
class Model(nn.Module):

    def __init__(self, features=5):
        super().__init__()
        self.features = features
        self.fc1 = nn.Linear(self.features, 10)
        self.fc2 = nn.Linear(10, 10)
        self.lstm = nn.LSTM(input_size=10, hidden_size=4, num_layers=2)
        self.fc3 = nn.Linear(4, 1)


    def forward(self, X):
        x = F.relu(self.fc2(F.relu(self.fc1(X))))
        output, (hidden, cell) = self.lstm(x.view(len(X), 1, 10))
        x = self.fc3(output)
#         return x, (hidden, cell)
        return x

m = Model()

# o, (h, c) = m(torch.rand(10,1,5))
# print(o.size(), h.size(), c.size())
```

%% Cell type:code id: tags:

``` python
X = torch.rand(10, 20, 5)      # len, batches, features
y_seq = torch.rand(10, 20, 1)  # len, batches, output
y_sin = torch.rand(1, 20, 1)

reg = TorchEstimator(module=m, optimizer=torch.optim.Adam(m.parameters()),
                    loss=nn.MSELoss(), epochs=10, verbose=True)
reg.fit(X, y_seq)
```

%% Output

    <models.TorchEstimator at 0x2bcc9d04828>

%% Cell type:code id: tags:

``` python
reg.predict(X).size()
```

%% Output

    torch.Size([20, 10, 1, 1])

    torch.Size([20, 10, 1, 1])

%% Cell type:code id: tags:

``` python
r=torch.rand(2,3,4)
print(r.size(), '\n', r.transpose(0,2).size())
```

%% Output

    torch.Size([2, 3, 4])
     torch.Size([4, 3, 2])
+52 −6
Changes for src/models.py: 52 added lines, 6 removed lines.
Original line number Diff line number Diff line
@@ -10,7 +10,9 @@ import numpy as np
import pandas as pd
from sklearn import clone
from sklearn.neural_network import MLPRegressor
import torch
import torch.nn as nn
import torch.optim as optim



@@ -82,13 +84,57 @@ class TorchEstimator:
    Wraps a `torch.nn.Module` instance with a scikit-learn `Estimator` API.
    """

    def __init__(self, module: nn.Module):
    def __init__(self, module: nn.Module, optimizer: optim.Optimizer,
                 loss: nn.modules.loss._Loss, epochs: int=10, verbose=True):
        self.module = module
        self.optimizer = optimizer
        self.loss = loss
        self.epochs = epochs
        self.verbose = verbose


    def fit(self, X, y):
        pass


    def predict(self, X, y):
        pass
 No newline at end of file
        """
        Fit target to features
        """
        for _ in range(self.epochs):
            for instance, target in zip(self._to_batches(X), self._to_batches(y)):
                self.module.zero_grad()
                output = self.module(instance)
                loss = self.loss(output, target)
                loss.backward()
                self.optimizer.step()
        return self


    def predict(self, X):
        shape = X.size()
        results = []
        with torch.no_grad():
            for batch in self._to_batches(X):
                results.append(self.module(batch))
        res_tensor = torch.zeros(len(results), *results[-1].size(),
                                 dtype=results[-1].dtype)
        for i in range(len(res_tensor)):
            res_tensor[i] = results[i]
        return self._from_batches(res_tensor)


    def _to_batches(self, X):
        shape = X.size()
        ndims = len(shape)
        if ndims == 3:
            for i in range(shape[1]):
                yield X[:, i, :]
        else:
            for i in range(shape[0]):
                yield X[i]


    def _from_batches(self, X):
        shape = X.size()
        ndims = len(shape)
        if ndims == 3:
            return X.transpose(0, 1)    # TODO: Transpose b/w seq len & batch not working
        else:
            return X
 No newline at end of file