# **Modelos de Classificação**

#### Este laboratório irá cobrir os passos para tratar a base de dados de taxa de cliques (click-through rate - CTR) e criar um modelo de classificação para tentar determinar se um usuário irá ou não clicar em um banner.

#### Para isso utilizaremos a base de dados [Criteo Labs](http://labs.criteo.com/) que foi utilizado em uma competição do [Kaggle](https://www.kaggle.com/c/criteo-display-ad-challenge).

#### ** Nesse notebook: **
+  ####*Parte 1:* Utilização do one-hot-encoding (OHE) para transformar atributos categóricos em numéricos
+  ####*Parte 2:* Construindo um dicionário OHE
+  ####*Parte 3:* Geração de atributos OHE na base de dados CTR
 + #### *Visualização 1:* Frequência de atributos
+  ####*Parte 4:* Predição de CTR e avaliação da perda logarítimica (logloss)
 + #### *Visualização 2:* Curva ROC
+  ####*Parte 5:* Reduzindo a dimensão dos atributos através de hashing (feature hashing)

#### Referências de métodos: [Spark's Python API](https://spark.apache.org/docs/latest/api/python/pyspark.html#pyspark.RDD)e [NumPy Reference](http://docs.scipy.org/doc/numpy/reference/index.html)

### ** Part 1: Utilização do one-hot-encoding (OHE) para transformar atributos categóricos em numéricos **

#### ** (1a) One-hot-encoding **

#### Para um melhor entendimento do processo da codificação OHE vamos trabalhar com uma base de dados pequena e sem rótulos. Cada objeto dessa base pode conter três atributos, o primeiro indicando o animal, o segundo a cor e o terceiro qual animal que ele come.

#### No esquema OHE, queremos representar cada tupla `(IDatributo, categoria)` através de um atributo binário. Nós podemos fazer isso no Python criando um dicionário que mapeia cada possível tupla em um inteiro que corresponde a sua posição no vetor de atributos binário.

#### Para iniciar crie um dicionário correspondente aos atributos categóricos da base construída logo abaixo. Faça isso manualmente.

```python
# Data for manual OHE
# Note: the first data point does not include any value for the optional third feature
sampleOne = [(0, 'mouse'), (1, 'black')]
sampleTwo = [(0, 'cat'), (1, 'tabby'), (2, 'mouse')]
sampleThree =  [(0, 'bear'), (1, 'black'), (2, 'salmon')]
sampleDataRDD = sc.parallelize([sampleOne, sampleTwo, sampleThree])
```

```python
# EXERCICIO
sampleOHEDictManual = {}
<COMPLETAR>
```

```python
# TEST One-hot-encoding (1a)
from test_helper import Test

Test.assertEqualsHashed(sampleOHEDictManual[(0,'bear')],
                        'b6589fc6ab0dc82cf12099d1c2d40ab994e8410c',
                        "incorrect value for sampleOHEDictManual[(0,'bear')]")
Test.assertEqualsHashed(sampleOHEDictManual[(0,'cat')],
                        '356a192b7913b04c54574d18c28d46e6395428ab',
                        "incorrect value for sampleOHEDictManual[(0,'cat')]")
Test.assertEqualsHashed(sampleOHEDictManual[(0,'mouse')],
                        'da4b9237bacccdf19c0760cab7aec4a8359010b0',
                        "incorrect value for sampleOHEDictManual[(0,'mouse')]")
Test.assertEqualsHashed(sampleOHEDictManual[(1,'black')],
                        '77de68daecd823babbb58edb1c8e14d7106e83bb',
                        "incorrect value for sampleOHEDictManual[(1,'black')]")
Test.assertEqualsHashed(sampleOHEDictManual[(1,'tabby')],
                        '1b6453892473a467d07372d45eb05abc2031647a',
                        "incorrect value for sampleOHEDictManual[(1,'tabby')]")
Test.assertEqualsHashed(sampleOHEDictManual[(2,'mouse')],
                        'ac3478d69a3c81fa62e60f5c3696165a4e5e6ac4',
                        "incorrect value for sampleOHEDictManual[(2,'mouse')]")
Test.assertEqualsHashed(sampleOHEDictManual[(2,'salmon')],
                        'c1dfd96eea8cc2b62785275bca38ac261256e278',
                        "incorrect value for sampleOHEDictManual[(2,'salmon')]")
Test.assertEquals(len(sampleOHEDictManual.keys()), 7,
                  'incorrect number of keys in sampleOHEDictManual')
```

#### ** (1b) Vetores Esparsos **

#### Pontos de dados categóricos geralmente apresentam um pequeno conjunto de OHE não-nulos relativo ao total de possíveis atributos. Tirando proveito dessa propriedade podemos representar nossos dados como vetores esparsos, economizando espaço de armazenamento e cálculos computacionais.

#### No próximo exercício transforme os vetores com nome precedidos de `Dense` para vetores esparsos. Utilize a classe [SparseVector](https://spark.apache.org/docs/latest/api/python/pyspark.mllib.html#pyspark.mllib.linalg.SparseVector) para representá-los e verifique que ambas as representações retornam o mesmo resultado nos cálculos dos produtos interno.

#### Use `SparseVector(tamanho, *args)` para criar um novo vetor esparso onde `tamanho` é o tamanho do vetor e `args` pode ser um dicionário, uma lista de tuplas (índice, valor) ou duas arrays separadas de índices e valores ordenados por índice.

```python
import numpy as np
from pyspark.mllib.linalg import SparseVector
```

```python
# EXERCICIO
aDense = np.array([0., 3., 0., 4.])
aSparse = SparseVector(<COMPLETAR>)

bDense = np.array([0., 0., 0., 1.])
bSparse = SparseVector(<COMPLETAR>)

w = np.array([0.4, 3.1, -1.4, -.5])
print aDense.dot(w)
print aSparse.dot(w)
print bDense.dot(w)
print bSparse.dot(w)
```

```python
# TEST Sparse Vectors (1b)
Test.assertTrue(isinstance(aSparse, SparseVector), 'aSparse needs to be an instance of SparseVector')
Test.assertTrue(isinstance(bSparse, SparseVector), 'aSparse needs to be an instance of SparseVector')
Test.assertTrue(aDense.dot(w) == aSparse.dot(w),
                'dot product of aDense and w should equal dot product of aSparse and w')
Test.assertTrue(bDense.dot(w) == bSparse.dot(w),
                'dot product of bDense and w should equal dot product of bSparse and w')
```

#### **(1c) Atributos OHE como vetores esparsos **

#### Agora vamos representar nossos atributos OHE como vetores esparsos. Utilizando o dicionário `sampleOHEDictManual`, crie um vetor esparso para cada amostra de nossa base de dados. Todo atributo que ocorre em uma amostra deve ter valor 1.0. Por exemplo, um vetor para um ponto com os atributos 2 e 4 devem ser `[0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0]`.

```python
# Reminder of the sample features
# sampleOne = [(0, 'mouse'), (1, 'black')]
# sampleTwo = [(0, 'cat'), (1, 'tabby'), (2, 'mouse')]
# sampleThree =  [(0, 'bear'), (1, 'black'), (2, 'salmon')]
```

```python
# EXERCICIO
sampleOneOHEFeatManual = SparseVector(<COMPLETAR>)
sampleTwoOHEFeatManual = SparseVector(<COMPLETAR>)
sampleThreeOHEFeatManual = SparseVector(<COMPLETAR>)
```

```python
# TEST OHE Features as sparse vectors (1c)
Test.assertTrue(isinstance(sampleOneOHEFeatManual, SparseVector),
                'sampleOneOHEFeatManual needs to be a SparseVector')
Test.assertTrue(isinstance(sampleTwoOHEFeatManual, SparseVector),
                'sampleTwoOHEFeatManual needs to be a SparseVector')
Test.assertTrue(isinstance(sampleThreeOHEFeatManual, SparseVector),
                'sampleThreeOHEFeatManual needs to be a SparseVector')
Test.assertEqualsHashed(sampleOneOHEFeatManual,
                        'ecc00223d141b7bd0913d52377cee2cf5783abd6',
                        'incorrect value for sampleOneOHEFeatManual')
Test.assertEqualsHashed(sampleTwoOHEFeatManual,
                        '26b023f4109e3b8ab32241938e2e9b9e9d62720a',
                        'incorrect value for sampleTwoOHEFeatManual')
Test.assertEqualsHashed(sampleThreeOHEFeatManual,
                        'c04134fd603ae115395b29dcabe9d0c66fbdc8a7',
                        'incorrect value for sampleThreeOHEFeatManual')
```

#### **(1d) Função de codificação OHE **

#### Vamos criar uma função que gera um vetor esparso codificado por um dicionário de OHE. Ele deve fazer o procedimento similar ao exercício anterior.

```python
# EXERCICIO
def oneHotEncoding(rawFeats, OHEDict, numOHEFeats):
    """Produce a one-hot-encoding from a list of features and an OHE dictionary.

    Note:
        You should ensure that the indices used to create a SparseVector are sorted.

    Args:
        rawFeats (list of (int, str)): The features corresponding to a single observation.  Each
            feature consists of a tuple of featureID and the feature's value. (e.g. sampleOne)
        OHEDict (dict): A mapping of (featureID, value) to unique integer.
        numOHEFeats (int): The total number of unique OHE features (combinations of featureID and
            value).

    Returns:
        SparseVector: A SparseVector of length numOHEFeats with indicies equal to the unique
            identifiers for the (featureID, value) combinations that occur in the observation and
            with values equal to 1.0.
    """
    return <COMPLETAR>

# Calculate the number of features in sampleOHEDictManual
numSampleOHEFeats = len(sampleOHEDictManual)

# Run oneHotEnoding on sampleOne
sampleOneOHEFeat = oneHotEncoding(sampleOne, sampleOHEDictManual, numSampleOHEFeats)

print sampleOneOHEFeat
```

```python
# TEST Define an OHE Function (1d)
Test.assertTrue(sampleOneOHEFeat == sampleOneOHEFeatManual,
                'sampleOneOHEFeat should equal sampleOneOHEFeatManual')
Test.assertEquals(sampleOneOHEFeat, SparseVector(7, [2,3], [1.0,1.0]),
                  'incorrect value for sampleOneOHEFeat')
Test.assertEquals(oneHotEncoding([(1, 'black'), (0, 'mouse')], sampleOHEDictManual,
                                 numSampleOHEFeats), SparseVector(7, [2,3], [1.0,1.0]),
                  'incorrect definition for oneHotEncoding')
```

#### **(1e) Aplicar OHE em uma base de dados **

#### Finalmente, use a função da parte (1d) para criar atributos OHE para todos os 3 objetos da base de dados artificial.

```python
# EXERCICIO
sampleOHEData = sampleDataRDD.<COMPLETAR>
print sampleOHEData.collect()
```

```python
# TEST Apply OHE to a dataset (1e)
sampleOHEDataValues = sampleOHEData.collect()
Test.assertTrue(len(sampleOHEDataValues) == 3, 'sampleOHEData should have three elements')
Test.assertEquals(sampleOHEDataValues[0], SparseVector(7, {2: 1.0, 3: 1.0}),
                  'incorrect OHE for first sample')
Test.assertEquals(sampleOHEDataValues[1], SparseVector(7, {1: 1.0, 4: 1.0, 5: 1.0}),
                  'incorrect OHE for second sample')
Test.assertEquals(sampleOHEDataValues[2], SparseVector(7, {0: 1.0, 3: 1.0, 6: 1.0}),
                  'incorrect OHE for third sample')
```

### ** Part 2: Construindo um dicionário OHE **

#### **(2a) Tupla RDD de `(IDatributo, categoria)` **

#### Crie um RDD de pares distintos de `(IDatributo,  categoria)`. Em nossa base de dados você deve gerar `(0, 'bear')`, `(0, 'cat')`, `(0, 'mouse')`, `(1, 'black')`, `(1, 'tabby')`, `(2, 'mouse')`, `(2, 'salmon')`. Repare que `'black'` aparece duas vezes em nossa base de dados mas contribui apenas para um item do RDD: `(1, 'black')`, por outro lado `'mouse'` aparece duas vezes e contribui para dois itens: `(0, 'mouse')` and `(2, 'mouse')`.

#### Dica: use [flatMap](https://spark.apache.org/docs/latest/api/python/pyspark.html#pyspark.RDD.flatMap) e [distinct](https://spark.apache.org/docs/latest/api/python/pyspark.html#pyspark.RDD.distinct).

```python
# EXERCICIO
sampleDistinctFeats = (sampleDataRDD
                       .<COMPLETAR>
                       .<COMPLETAR>
                      )

```

```python
# TEST Pair RDD of (featureID, category) (2a)
Test.assertEquals(sorted(sampleDistinctFeats.collect()),
                  [(0, 'bear'), (0, 'cat'), (0, 'mouse'), (1, 'black'),
                   (1, 'tabby'), (2, 'mouse'), (2, 'salmon')],
                  'incorrect value for sampleDistinctFeats')
```

#### ** (2b) Dicionário OHE de atributos únicos **

#### Agora, vamos criar um RDD de tuplas para cada `(IDatributo, categoria)` em `sampleDistinctFeats`. A chave da tupla é a própria tupla original, e o valor será um inteiro variando de 0 até número de tuplas - 1.

#### Em seguida, converta essa `RDD` em um dicionário, utilizando o comando `collectAsMap`.

#### Use o comando  [zipWithIndex](https://spark.apache.org/docs/latest/api/python/pyspark.html#pyspark.RDD.zipWithIndex) seguido de [collectAsMap](https://spark.apache.org/docs/latest/api/python/pyspark.html#pyspark.RDD.collectAsMap).

```python
# EXERCICIO
sampleOHEDict = (sampleDistinctFeats
                           .<COMPLETAR>
                           .<COMPLETAR>)
print sampleOHEDict
```

```python
# TEST OHE Dictionary from distinct features (2b)
Test.assertEquals(sorted(sampleOHEDict.keys()),
                  [(0, 'bear'), (0, 'cat'), (0, 'mouse'), (1, 'black'),
                   (1, 'tabby'), (2, 'mouse'), (2, 'salmon')],
                  'sampleOHEDict has unexpected keys')
Test.assertEquals(sorted(sampleOHEDict.values()), range(7), 'sampleOHEDict has unexpected values')
```

#### **(2c) Criação automática do dicionário OHE **

#### Agora use os códigos dos exercícios anteriores para criar uma função que retorna um dicionário OHE a partir dos atributos categóricos de uma base de dados.

```python
# EXERCICIO
def createOneHotDict(inputData):
    """Creates a one-hot-encoder dictionary based on the input data.

    Args:
        inputData (RDD of lists of (int, str)): An RDD of observations where each observation is
            made up of a list of (featureID, value) tuples.

    Returns:
        dict: A dictionary where the keys are (featureID, value) tuples and map to values that are
            unique integers.
    """
    return (inputData
             .<COMPLETAR>
             .<COMPLETAR>
             .<COMPLETAR>
             .<COMPLETAR>
             )

sampleOHEDictAuto = createOneHotDict(sampleDataRDD)
print sampleOHEDictAuto
```

```python
# TEST Automated creation of an OHE dictionary (2c)
Test.assertEquals(sorted(sampleOHEDictAuto.keys()),
                  [(0, 'bear'), (0, 'cat'), (0, 'mouse'), (1, 'black'),
                   (1, 'tabby'), (2, 'mouse'), (2, 'salmon')],
                  'sampleOHEDictAuto has unexpected keys')
Test.assertEquals(sorted(sampleOHEDictAuto.values()), range(7),
                  'sampleOHEDictAuto has unexpected values')
```

### **Part 3: Parse CTR data and generate OHE features**

#### Antes de começar essa parte, vamos carregar a base de dados e verificar o formato dela.

#### Repare que o primeiro campo é o rótulo de cada objeto, sendo 0 se o usuário não clicou no banner e 1 caso tenha clicado. O restante dos atributos ou são numéricos ou são strings representando categorias anônimas. Vamos tratar todos os atributos como categóricos.

```python
import os.path
baseDir = os.path.join('Data')
inputPath = os.path.join('Aula04', 'dac_sample.txt')
fileName = os.path.join(baseDir, inputPath)

if os.path.isfile(fileName):
    rawData = (sc
               .textFile(fileName, 2)
               .map(lambda x: x.replace('\t', ',')))  # work with either ',' or '\t' separated data
    print rawData.take(1)
```

#### **(3a) Carregando e dividindo os dados **

#### Da mesma forma que no notebook anterior, vamos dividir os dados entre treinamento, validação e teste. Use o método [randomSplit](https://spark.apache.org/docs/latest/api/python/pyspark.html#pyspark.RDD.randomSplit) com os pesos (weights) e semente aleatória (seed) especificados para criar os conjuntos, então faça o [cache](https://spark.apache.org/docs/latest/api/python/pyspark.html#pyspark.RDD.cache) de cada RDD, pois utilizaremos cada uma delas com frequência durante esse exercício.

```python
# EXERCICIO
weights = [.8, .1, .1]
seed = 42
# Use randomSplit with weights and seed
rawTrainData, rawValidationData, rawTestData = rawData.randomSplit(weights, seed)
# Cache the data
rawTrainData.<COMPLETAR>
rawValidationData.<COMPLETAR>
rawTestData.<COMPLETAR>

nTrain = rawTrainData.count()
nVal = rawValidationData.count()
nTest = rawTestData.count()
print nTrain, nVal, nTest, nTrain + nVal + nTest
print rawData.take(1)
```

```python
# TEST Loading and splitting the data (3a)
Test.assertTrue(all([rawTrainData.is_cached, rawValidationData.is_cached, rawTestData.is_cached]),
                'you must cache the split data')
Test.assertEquals(nTrain, 79911, 'incorrect value for nTrain')
Test.assertEquals(nVal, 10075, 'incorrect value for nVal')
Test.assertEquals(nTest, 10014, 'incorrect value for nTest')
```

#### ** (3b) Extração de atributos **

#### Como próximo passo, crie uma função para ser aplicada em cada objeto do RDD para gerar uma RDD de tuplas (IDatributo, categoria). Ignore o primeiro campo, que é o rótulo e gere uma lista de tuplas para os atributos seguintes. Utilize o comando [enumerate](https://docs.python.org/2/library/functions.html#enumerate) para criar essas tuplas.

```python
# EXERCICIO
def parsePoint(point):
    """Converts a comma separated string into a list of (featureID, value) tuples.

    Note:
        featureIDs should start at 0 and increase to the number of features - 1.

    Args:
        point (str): A comma separated string where the first value is the label and the rest
            are features.

    Returns:
        list: A list of (featureID, value) tuples.
    """
    <COMPLETAR>

parsedTrainFeat = rawTrainData.map(parsePoint)

numCategories = (parsedTrainFeat
                 .<COMPLETAR>
                 .<COMPLETAR>
                 .<COMPLETAR>
                 .<COMPLETAR>
                 .<COMPLETAR>
                 .collect()
                )

print numCategories[2][1]
```

```python
# TEST Extract features (3b)
Test.assertEquals(numCategories[2][1], 855, 'incorrect implementation of parsePoint')
Test.assertEquals(numCategories[32][1], 4, 'incorrect implementation of parsePoint')
```

#### **(3c) Crie o dicionário de OHE dessa base de dados **

#### Note que a função parsePoint retorna um objeto em forma de lista `(IDatributo, categoria)`, que é o mesmo formato utilizado pela função `createOneHotDict`. Utilize o RDD `parsedTrainFeat` para criar um dicionário OHE.

```python
# EXERCICIO
ctrOHEDict = <COMPLETAR>
numCtrOHEFeats = len(ctrOHEDict.keys())
print numCtrOHEFeats
print ctrOHEDict[(0, '')]
```

```python
# TEST Create an OHE dictionary from the dataset (3c)
Test.assertEquals(numCtrOHEFeats, 233286, 'incorrect number of features in ctrOHEDict')
Test.assertTrue((0, '') in ctrOHEDict, 'incorrect features in ctrOHEDict')
```

#### ** (3d) Aplicando OHE à base de dados **

#### Agora vamos usar o dicionário OHE para criar um RDD de objetos [LabeledPoint](http://spark.apache.org/docs/1.3.1/api/python/pyspark.mllib.html#pyspark.mllib.regression.LabeledPoint) usando atributos OHE. Complete a função `parseOHEPoint`. Dica: essa função é uma extensão da função `parsePoint` criada anteriormente e que usa a função `oneHotEncoding`.

```python
from pyspark.mllib.regression import LabeledPoint
```

```python
# EXERCICIO
def parseOHEPoint(point, OHEDict, numOHEFeats):
    """Obtain the label and feature vector for this raw observation.

    Note:
        You must use the function `oneHotEncoding` in this implementation or later portions
        of this lab may not function as expected.

    Args:
        point (str): A comma separated string where the first value is the label and the rest
            are features.
        OHEDict (dict of (int, str) to int): Mapping of (featureID, value) to unique integer.
        numOHEFeats (int): The number of unique features in the training dataset.

    Returns:
        LabeledPoint: Contains the label for the observation and the one-hot-encoding of the
            raw features based on the provided OHE dictionary.
    """
    <COMPLETAR>

OHETrainData = rawTrainData.map(lambda point: parseOHEPoint(point, ctrOHEDict, numCtrOHEFeats))
OHETrainData.cache()
print OHETrainData.take(1)

# Check that oneHotEncoding function was used in parseOHEPoint
backupOneHot = oneHotEncoding
oneHotEncoding = None
withOneHot = False
try: parseOHEPoint(rawTrainData.take(1)[0], ctrOHEDict, numCtrOHEFeats)
except TypeError: withOneHot = True
oneHotEncoding = backupOneHot
```

```python
# TEST Apply OHE to the dataset (3d)
numNZ = sum(parsedTrainFeat.map(lambda x: len(x)).take(5))
numNZAlt = sum(OHETrainData.map(lambda lp: len(lp.features.indices)).take(5))
Test.assertEquals(numNZ, numNZAlt, 'incorrect implementation of parseOHEPoint')
Test.assertTrue(withOneHot, 'oneHotEncoding not present in parseOHEPoint')
```

#### **Visualização 1: Frequência dos Atributos **

#### Vamos agora visualizar o número de vezes que cada um dos 233.286 atributos OHE aparecem na base de treino.  Para isso primeiro contabilizamos quantas vezes cada atributo aparece na base, então alocamos cada atributo em um balde de histograma. Os baldes tem tamanhos de potência de 2, então o primeiro balde conta os atributos que aparecem exatamente uma vez ( $ \scriptsize 2^0 $ ), o segundo atributos que aparecem duas vezes ( $ \scriptsize 2^1 $ ), o terceiro os atributos que aparecem de 3 a 4 vezes ( $ \scriptsize 2^2 $ ), o quinto balde é para atributos que ocorrem de cinco a oito vezes ( $ \scriptsize 2^3 $ ) e assim por diante. O gráfico de dispersão abaixo mostra o logarítmo do tamanho dos baldes versus o logarítmo da frequência de atributos que caíram nesse balde.

```python
def bucketFeatByCount(featCount):
    """Bucket the counts by powers of two."""
    for i in range(11):
        size = 2 ** i
        if featCount <= size:
            return size
    return -1

featCounts = (OHETrainData
              .flatMap(lambda lp: lp.features.indices)
              .map(lambda x: (x, 1))
              .reduceByKey(lambda x, y: x + y))
featCountsBuckets = (featCounts
                     .map(lambda x: (bucketFeatByCount(x[1]), 1))
                     .filter(lambda (k, v): k != -1)
                     .reduceByKey(lambda x, y: x + y)
                     .collect())
print featCountsBuckets
```

```python
import matplotlib.pyplot as plt

x, y = zip(*featCountsBuckets)
x, y = np.log(x), np.log(y)

def preparePlot(xticks, yticks, figsize=(10.5, 6), hideLabels=False, gridColor='#999999',
                gridWidth=1.0):
    """Template for generating the plot layout."""
    plt.close()
    fig, ax = plt.subplots(figsize=figsize, facecolor='white', edgecolor='white')
    ax.axes.tick_params(labelcolor='#999999', labelsize='10')
    for axis, ticks in [(ax.get_xaxis(), xticks), (ax.get_yaxis(), yticks)]:
        axis.set_ticks_position('none')
        axis.set_ticks(ticks)
        axis.label.set_color('#999999')
        if hideLabels: axis.set_ticklabels([])
    plt.grid(color=gridColor, linewidth=gridWidth, linestyle='-')
    map(lambda position: ax.spines[position].set_visible(False), ['bottom', 'top', 'left', 'right'])
    return fig, ax

# generate layout and plot data
fig, ax = preparePlot(np.arange(0, 10, 1), np.arange(4, 14, 2))
ax.set_xlabel(r'$\log_e(bucketSize)$'), ax.set_ylabel(r'$\log_e(countInBucket)$')
plt.scatter(x, y, s=14**2, c='#d6ebf2', edgecolors='#8cbfd0', alpha=0.75)
pass
```

#### **(3e) Atributos não observados **

#### Naturalmente precisaremos aplicar esse mesmo procedimento para as outras bases (validação e teste), porém nessas bases podem existir atributos não observados na base de treino.

#### Precisamos adaptar a função `oneHotEncoding` para ignorar os atributos que não existem no dicionário.

```python
# EXERCICIO
def oneHotEncoding(rawFeats, OHEDict, numOHEFeats):
    """Produce a one-hot-encoding from a list of features and an OHE dictionary.

    Note:
        If a (featureID, value) tuple doesn't have a corresponding key in OHEDict it should be
        ignored.

    Args:
        rawFeats (list of (int, str)): The features corresponding to a single observation.  Each
            feature consists of a tuple of featureID and the feature's value. (e.g. sampleOne)
        OHEDict (dict): A mapping of (featureID, value) to unique integer.
        numOHEFeats (int): The total number of unique OHE features (combinations of featureID and
            value).

    Returns:
        SparseVector: A SparseVector of length numOHEFeats with indicies equal to the unique
            identifiers for the (featureID, value) combinations that occur in the observation and
            with values equal to 1.0.
    """
    <COMPLETAR>

OHEValidationData = rawValidationData.map(lambda point: parseOHEPoint(point, ctrOHEDict, numCtrOHEFeats))
OHEValidationData.cache()
print OHEValidationData.take(1)
```

```python
# TEST Handling unseen features (3e)
numNZVal = (OHEValidationData
            .map(lambda lp: len(lp.features.indices))
            .sum())
Test.assertEquals(numNZVal, 372080, 'incorrect number of features')
```

### ** Part 4: Predição do CTR e avaliação da perda-log (logloss) **

#### ** (4a) Regressão Logística **

#### Um classificador que podemos utilizar nessa base de dados é a regressão logística, que nos dá a probabilidade de um evento de clique em banner ocorrer. Vamos utilizar a função  [LogisticRegressionWithSGD](https://spark.apache.org/docs/latest/api/python/pyspark.mllib.html#pyspark.mllib.classification.LogisticRegressionWithSGD) para treinar um modelo usando  `OHETrainData` com a configuração de parâmetros dada.  `LogisticRegressionWithSGD` retorna um [LogisticRegressionModel](https://spark.apache.org/docs/latest/api/python/pyspark.mllib.html#pyspark.mllib.regression.LogisticRegressionModel).

#### Em seguida, imprima  `LogisticRegressionModel.weights` e `LogisticRegressionModel.intercept` para verificar o modelo gerado.

```python
from pyspark.mllib.classification import LogisticRegressionWithSGD

# fixed hyperparameters
numIters = 50
stepSize = 10.
regParam = 1e-6
regType = 'l2'
includeIntercept = True
```

```python
# EXERCICIO
model0 = <COMPLETAR>
sortedWeights = sorted(model0.weights)
print sortedWeights[:5], model0.intercept
```

```python
# TEST Logistic regression (4a)
Test.assertTrue(np.allclose(model0.intercept,  0.56455084025), 'incorrect value for model0.intercept')
Test.assertTrue(np.allclose(sortedWeights[0:5],
                [-0.45899236853575609, -0.37973707648623956, -0.36996558266753304,
                 -0.36934962879928263, -0.32697945415010637]), 'incorrect value for model0.weights')
```

#### ** (4b) Log loss **

#### Uma forma de avaliar um classificador binário é através do log-loss, definido como: $$  \begin{align} \scriptsize \ell_{log}(p, y) = \begin{cases} -\log (p) & \text{if } y = 1 \\\ -\log(1-p) & \text{if } y = 0 \end{cases} \end{align} $$ onde $ \scriptsize p$ é uma probabilidade entre 0 e 1 e  $ \scriptsize y$ é o rótulo binário (0 ou 1). Log loss é um critério de avaliação muito utilizado quando deseja-se predizer eventos raros. Escreva uma função para calcular o log-loss, e avalie algumas entradas de amostra.

```python
# EXERCICIO
from math import log

def computeLogLoss(p, y):
    """Calculates the value of log loss for a given probabilty and label.

    Note:
        log(0) is undefined, so when p is 0 we need to add a small value (epsilon) to it
        and when p is 1 we need to subtract a small value (epsilon) from it.

    Args:
        p (float): A probabilty between 0 and 1.
        y (int): A label.  Takes on the values 0 and 1.

    Returns:
        float: The log loss value.
    """
    <COMPLETAR>

print computeLogLoss(.5, 1)
print computeLogLoss(.5, 0)
print computeLogLoss(.99, 1)
print computeLogLoss(.99, 0)
print computeLogLoss(.01, 1)
print computeLogLoss(.01, 0)
print computeLogLoss(0, 1)
print computeLogLoss(1, 1)
print computeLogLoss(1, 0)
```

```python
# TEST Log loss (4b)
Test.assertTrue(np.allclose([computeLogLoss(.5, 1), computeLogLoss(.01, 0), computeLogLoss(.01, 1)],
                            [0.69314718056, 0.0100503358535, 4.60517018599]),
                'computeLogLoss is not correct')
Test.assertTrue(np.allclose([computeLogLoss(0, 1), computeLogLoss(1, 1), computeLogLoss(1, 0)],
                            [25.3284360229, 1.00000008275e-11, 25.3284360229]),
                'computeLogLoss needs to bound p away from 0 and 1 by epsilon')
```

#### ** (4c)  Baseline log loss **

#### Agora, vamos utilizar a função da Parte (4b) para calcular um baseline da métrica de log-loss na nossa base de treino. Uma forma de calcular um baseline é predizer sempre a média dos rótulos observados. Primeiro calcule a média dos rótulos da base e, em seguida, calcule o log-loss médio para a base de treino.

```python
# EXERCICIO
# Note that our dataset has a very high click-through rate by design
# In practice click-through rate can be one to two orders of magnitude lower
classOneFracTrain = OHETrainData.<COMPLETAR>
print classOneFracTrain

logLossTrBase = OHETrainData.<COMPLETAR>
print 'Baseline Train Logloss = {0:.3f}\n'.format(logLossTrBase)
```

```python
# TEST Baseline log loss (4c)
Test.assertTrue(np.allclose(classOneFracTrain, 0.22717773523), 'incorrect value for classOneFracTrain')
Test.assertTrue(np.allclose(logLossTrBase, 0.535844), 'incorrect value for logLossTrBase')
```

#### ** (4d) Probabilidade da Predição **

#### O modelo gerado na Parte (4a) possui um método chamado `predict`, porém esse método retorna apenas 0's e 1's. Para calcular a probabilidade de um evento, vamos criar uma função `getP` que recebe como parâmetro o ponto x, o conjunto de pesos `w` e o `intercept`.

#### Calcule o modelo de regressão linear nesse ponto x e aplique a  [função sigmoidal](http://en.wikipedia.org/wiki/Sigmoid_function) $ \scriptsize \sigma(t) = (1+ e^{-t})^{-1} $ para retornar a probabilidade da predição do objeto x.

```python
# EXERCICIO
from math import exp #  exp(-t) = e^-t

def getP(x, w, intercept):
    """Calculate the probability for an observation given a set of weights and intercept.

    Note:
        We'll bound our raw prediction between 20 and -20 for numerical purposes.

    Args:
        x (SparseVector): A vector with values of 1.0 for features that exist in this
            observation and 0.0 otherwise.
        w (DenseVector): A vector of weights (betas) for the model.
        intercept (float): The model's intercept.

    Returns:
        float: A probability between 0 and 1.
    """
    # calculate rawPrediction = w.x + intercept
    rawPrediction = <COMPLETAR>

    # Bound the raw prediction value
    rawPrediction = min(rawPrediction, 20)
    rawPrediction = max(rawPrediction, -20)

    # calculate (1+e^-rawPrediction)^-1
    return <COMPLETAR>

trainingPredictions = OHETrainData.<COMPLETAR>

print trainingPredictions.take(5)
```

```python
# TEST Predicted probability (4d)
Test.assertTrue(np.allclose(trainingPredictions.sum(), 18135.4834348),
                'incorrect value for trainingPredictions')
```

#### ** (4e) Avalie o modelo **

#### Finalmente, crie uma função `evaluateResults` que calcula o log-loss médio do modelo em uma base de dados. Em seguida, execute essa função na nossa base de treino.

```python
# EXERCICIO
def evaluateResults(model, data):
    """Calculates the log loss for the data given the model.

    Args:
        model (LogisticRegressionModel): A trained logistic regression model.
        data (RDD of LabeledPoint): Labels and features for each observation.

    Returns:
        float: Log loss for the data.
    """
    return (data
            .<COMPLETAR>
            .<COMPLETAR>
            .<COMPLETAR>
            )

logLossTrLR0 = evaluateResults(model0, OHETrainData)
print ('OHE Features Train Logloss:\n\tBaseline = {0:.3f}\n\tLogReg = {1:.3f}'
       .format(logLossTrBase, logLossTrLR0))
```

```python
# TEST Evaluate the model (4e)
Test.assertTrue(np.allclose(logLossTrLR0, 0.456903), 'incorrect value for logLossTrLR0')
```

#### ** (4f) log-loss da validação **

#### Agora aplique o modelo na nossa base de validação e calcule o log-loss médio, compare com o nosso baseline.

```python
# EXERCICIO
logLossValBase = OHEValidationData.<COMPLETAR>

logLossValLR0 = evaluateResults(model0, OHEValidationData)
print ('OHE Features Validation Logloss:\n\tBaseline = {0:.3f}\n\tLogReg = {1:.3f}'
       .format(logLossValBase, logLossValLR0))
```

```python
# TEST Validation log loss (4f)
Test.assertTrue(np.allclose(logLossValBase, 0.527603), 'incorrect value for logLossValBase')
Test.assertTrue(np.allclose(logLossValLR0, 0.456957), 'incorrect value for logLossValLR0')
```

#### **Visualização 2: Curva ROC  **

#### A curva ROC nos mostra o custo-benefício entre a taxa de falso positivo e a taxa de verdadeiro positivo, conforme diminuimos o limiar de predição. Um modelo aleatório é representado por uma linha pontilhada. Idealmente nosso modelo deve formar uma curva acima dessa linha.

```python
labelsAndScores = OHEValidationData.map(lambda lp:
                                            (lp.label, getP(lp.features, model0.weights, model0.intercept)))
labelsAndWeights = labelsAndScores.collect()
labelsAndWeights.sort(key=lambda (k, v): v, reverse=True)
labelsByWeight = np.array([k for (k, v) in labelsAndWeights])

length = labelsByWeight.size
truePositives = labelsByWeight.cumsum()
numPositive = truePositives[-1]
falsePositives = np.arange(1.0, length + 1, 1.) - truePositives

truePositiveRate = truePositives / numPositive
falsePositiveRate = falsePositives / (length - numPositive)

# Generate layout and plot data
fig, ax = preparePlot(np.arange(0., 1.1, 0.1), np.arange(0., 1.1, 0.1))
ax.set_xlim(-.05, 1.05), ax.set_ylim(-.05, 1.05)
ax.set_ylabel('True Positive Rate (Sensitivity)')
ax.set_xlabel('False Positive Rate (1 - Specificity)')
plt.plot(falsePositiveRate, truePositiveRate, color='#8cbfd0', linestyle='-', linewidth=3.)
plt.plot((0., 1.), (0., 1.), linestyle='--', color='#d6ebf2', linewidth=2.)  # Baseline model
pass
```

### **Parte 5: Reduzindo a dimensão dos atributos via feature hashing**

#### ** (5a) Função Hash **

#### Nosso modelo OHE consegue criar uma representação numérica boa o suficiente para ser aplicável em algoritmos de classificação que não conseguem tratar dados categóricos. Porém, para nossa base de dados isso gerou um número enorme de atributos (233 mil) que pode tornar o problema intratável. Para reduzir o espaço de atributos vamos utilizar um truque através de funções hash chamado de feature hashing.

#### Logo abaixo, já está implementada a função de hash que usaremos nessa parte do notebook. Vamos aplicá-la na nossa base artificial criada na Parte (1a) para termos uma intuição do que está acontecendo. Execute essa função para valores diferentes de `numBuckets` e observe o resultado.

```python
from collections import defaultdict
import hashlib

def hashFunction(numBuckets, rawFeats, printMapping=False):
    """Calculate a feature dictionary for an observation's features based on hashing.

    Note:
        Use printMapping=True for debug purposes and to better understand how the hashing works.

    Args:
        numBuckets (int): Number of buckets to use as features.
        rawFeats (list of (int, str)): A list of features for an observation.  Represented as
            (featureID, value) tuples.
        printMapping (bool, optional): If true, the mappings of featureString to index will be
            printed.

    Returns:
        dict of int to float:  The keys will be integers which represent the buckets that the
            features have been hashed to.  The value for a given key will contain the count of the
            (featureID, value) tuples that have hashed to that key.
    """
    mapping = {}
    for ind, category in rawFeats:
        featureString = category + str(ind)
        mapping[featureString] = int(int(hashlib.md5(featureString).hexdigest(), 16) % numBuckets)
    if(printMapping): print mapping
    sparseFeatures = defaultdict(float)
    for bucket in mapping.values():
        sparseFeatures[bucket] += 1.0
    return dict(sparseFeatures)

# Reminder of the sample values:
# sampleOne = [(0, 'mouse'), (1, 'black')]
# sampleTwo = [(0, 'cat'), (1, 'tabby'), (2, 'mouse')]
# sampleThree =  [(0, 'bear'), (1, 'black'), (2, 'salmon')]
```

```python
# EXERCICIO
# Use four buckets
sampOneFourBuckets = <COMPLETAR>
sampTwoFourBuckets = <COMPLETAR>
sampThreeFourBuckets = <COMPLETAR>

# Use one hundred buckets
sampOneHundredBuckets = <COMPLETAR>
sampTwoHundredBuckets = <COMPLETAR>
sampThreeHundredBuckets = <COMPLETAR>

print '\t\t 4 Buckets \t\t\t 100 Buckets'
print 'SampleOne:\t {0}\t\t {1}'.format(sampOneFourBuckets, sampOneHundredBuckets)
print 'SampleTwo:\t {0}\t\t {1}'.format(sampTwoFourBuckets, sampTwoHundredBuckets)
print 'SampleThree:\t {0}\t {1}'.format(sampThreeFourBuckets, sampThreeHundredBuckets)
```

```python
# TEST Hash function (5a)
Test.assertEquals(sampOneFourBuckets, {2: 1.0, 3: 1.0}, 'incorrect value for sampOneFourBuckets')
Test.assertEquals(sampThreeHundredBuckets, {72: 1.0, 5: 1.0, 14: 1.0},
                  'incorrect value for sampThreeHundredBuckets')
```

#### ** (5b) Criando hashed features **

#### Agora vamos usar essa função hash para criar hashed features para nossa base CTR. Primeiro escreva uma função que usa a função hash da Parte (5a) com numBuckets = $ \scriptsize 2^{15} \approx 33K $ para criar um `LabeledPoint` com os  hashed features armazenados como um `SparseVector`.  Então use esta função para criar uma nova base de treino, validação e teste com  hashed features. Dica: `parsedHashPoint` é similar a `parseOHEPoint` da Parte (3d).

```python
# EXERCICIO
def parseHashPoint(point, numBuckets):
    """Create a LabeledPoint for this observation using hashing.

    Args:
        point (str): A comma separated string where the first value is the label and the rest are
            features.
        numBuckets: The number of buckets to hash to.

    Returns:
        LabeledPoint: A LabeledPoint with a label (0.0 or 1.0) and a SparseVector of hashed
            features.
    """
    <COMPLETAR>

numBucketsCTR = 2 ** 15
hashTrainData = rawTrainData.map(lambda x: parseHashPoint(x,numBucketsCTR))
hashTrainData.cache()
hashValidationData = rawValidationData.map(lambda x: parseHashPoint(x,numBucketsCTR))
hashValidationData.cache()
hashTestData = rawTestData.map(lambda x: parseHashPoint(x,numBucketsCTR))
hashTestData.cache()

print hashTrainData.take(1)
```

```python
# TEST Creating hashed features (5b)
hashTrainDataFeatureSum = sum(hashTrainData
                           .map(lambda lp: len(lp.features.indices))
                           .take(20))
hashTrainDataLabelSum = sum(hashTrainData
                         .map(lambda lp: lp.label)
                         .take(100))
hashValidationDataFeatureSum = sum(hashValidationData
                                .map(lambda lp: len(lp.features.indices))
                                .take(20))
hashValidationDataLabelSum = sum(hashValidationData
                              .map(lambda lp: lp.label)
                              .take(100))
hashTestDataFeatureSum = sum(hashTestData
                          .map(lambda lp: len(lp.features.indices))
                          .take(20))
hashTestDataLabelSum = sum(hashTestData
                        .map(lambda lp: lp.label)
                        .take(100))

Test.assertEquals(hashTrainDataFeatureSum, 772, 'incorrect number of features in hashTrainData')
Test.assertEquals(hashTrainDataLabelSum, 24.0, 'incorrect labels in hashTrainData')
Test.assertEquals(hashValidationDataFeatureSum, 776,
                  'incorrect number of features in hashValidationData')
Test.assertEquals(hashValidationDataLabelSum, 16.0, 'incorrect labels in hashValidationData')
Test.assertEquals(hashTestDataFeatureSum, 774, 'incorrect number of features in hashTestData')
Test.assertEquals(hashTestDataLabelSum, 23.0, 'incorrect labels in hashTestData')
```

#### ** (5c) Esparsidade **

#### Uma vez que temos  33 mil hashed features contra 233 mil OHE, devemos esperar que os atributos OHE sejam mais esparsos. Verifique essa hipótese computando a esparsidade média do OHE e do hashed features.

#### Note que se você tem um `SparseVector` chamado `sparse`, chamar `len(sparse)` retornará o total de atributos, e não o número de valores não nulos. `SparseVector` tem atributos `indices` e `values` que contém informações sobre quais atributos são não nulos.

```python
# EXERCICIO
def computeSparsity(data, d, n):
    """Calculates the average sparsity for the features in an RDD of LabeledPoints.

    Args:
        data (RDD of LabeledPoint): The LabeledPoints to use in the sparsity calculation.
        d (int): The total number of features.
        n (int): The number of observations in the RDD.

    Returns:
        float: The average of the ratio of features in a point to total features.
    """
    return (data
            .<COMPLETAR>
            .<COMPLETAR>
           )/(d*n*1.)

averageSparsityHash = computeSparsity(hashTrainData, numBucketsCTR, nTrain)
averageSparsityOHE = computeSparsity(OHETrainData, numCtrOHEFeats, nTrain)

print 'Average OHE Sparsity: {0:.7e}'.format(averageSparsityOHE)
print 'Average Hash Sparsity: {0:.7e}'.format(averageSparsityHash)
```

```python
# TEST Sparsity (5c)
Test.assertTrue(np.allclose(averageSparsityOHE, 1.6717677e-04),
                'incorrect value for averageSparsityOHE')
Test.assertTrue(np.allclose(averageSparsityHash, 1.1805561e-03),
                'incorrect value for averageSparsityHash')
```

#### ** (5d) Modelo logístico com hashed features **

#### Agora treine um modelo de regressão logística para os hashed features. Execute um grid search para encontrar parâmetros adequados para essa base, avaliando o log-loss no conjunto de validação. Nota: isso pode demorar alguns minutos para terminar. Use `stepSizes` de 1 e 10 e `regParams`  de 1e-6 e 1e-3.

```python
numIters = 500
regType = 'l2'
includeIntercept = True

# Initialize variables using values from initial model training
bestModel = None
bestLogLoss = 1e10
```

```python
# EXERCICIO
stepSizes = [1, 10]
regParams = [1e-6, 1e-3]
for stepSize in stepSizes:
    for regParam in regParams:
        model = (<COMPLETAR>)
        logLossVa = <COMPLETAR>
        print ('\tstepSize = {0:.1f}, regParam = {1:.0e}: logloss = {2:.3f}'
               .format(stepSize, regParam, logLossVa))
        if (logLossVa < bestLogLoss):
            bestModel = model
            bestLogLoss = logLossVa

print ('Hashed Features Validation Logloss:\n\tBaseline = {0:.3f}\n\tLogReg = {1:.3f}'
       .format(logLossValBase, bestLogLoss))
```

```python
# TEST Logistic model with hashed features (5d)
Test.assertTrue(np.allclose(bestLogLoss, 0.4481683608), 'incorrect value for bestLogLoss')
```

#### ** (5e) Avaliando a base de testes **

#### Finalmente, avalie o melhor modelo da Parte (5d) na base de testes. Compare o log-loss do resultado com o log-loss do nosso baseline no conjunto de testes, calculando da mesma forma que na Parte (4f).

```python
# EXERCICIO
# Log loss for the best model from (5d)
logLossValLR0 = <COMPLETAR>

logLossTest = <COMPLETAR>

# Log loss for the baseline model
logLossTestBaseline = hashTestData.map(lambda lp: computeLogLoss(classOneFracTrain,lp.label)).mean()

print ('Hashed Features Test Log Loss:\n\tBaseline = {0:.3f}\n\tLogReg = {1:.3f}'
       .format(logLossTestBaseline, logLossTest))
```

```python
# TEST Evaluate on the test set (5e)
Test.assertTrue(np.allclose(logLossTestBaseline, 0.537438),
                'incorrect value for logLossTestBaseline')
Test.assertTrue(np.allclose(logLossTest, 0.455616931), 'incorrect value for logLossTest')
```

```python

```


+ This notebook is part of lecture 5 *Transposes, permutations, and vector spaces* in the OCW MIT course 18.06 by Prof Gilbert Strang [1]
+ Created by me, Dr Juan H Klopper
    + Head of Acute Care Surgery
    + Groote Schuur Hospital
    + University Cape Town
    + Email me with your thoughts, comments, suggestions and corrections

Linear Algebra OCW MIT18.06 IPython notebook [2] study notes by Dr Juan H Klopper is licensed under a Creative Commons Attribution-NonCommercial 4.0 International License.

+ [1] OCW MIT 18.06
+ [2] Fernando Pérez, Brian E. Granger, IPython: A System for Interactive Scientific Computing, Computing in Science and Engineering, vol. 9, no. 3, pp. 21-29, May/June 2007, doi:10.1109/MCSE.2007.53. URL: http://ipython.org

```python
from IPython.core.display import HTML, Image
css_file = 'style.css'
HTML(open(css_file, 'r').read())
```

<link href='http://fonts.googleapis.com/css?family=Alegreya+Sans:100,300,400,500,700,800,900,100italic,300italic,400italic,500italic,700italic,800italic,900italic' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Arvo:400,700,400italic' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=PT+Mono' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Shadows+Into+Light' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Philosopher:400,700,400italic,700italic' rel='stylesheet' type='text/css'>

```python
#import numpy as np
from sympy import init_printing, Matrix, symbols
#import matplotlib.pyplot as plt
#import seaborn as sns
#from IPython.display import Image
from warnings import filterwarnings

init_printing(use_latex = 'mathjax')
%matplotlib inline
filterwarnings('ignore')
```

# Transposes, permutations and vector spaces

## The permutation matrices

* Remember that the permutation matrices allow for row exchanges
* They are used to manage zero's in pivot positions
* The have the following property
$$ {P}^{-1} = {P}^{T} $$

```python
P = Matrix([[0, 1, 0], [1, 0, 0], [0, 0, 1]])
P # Exchanging rows 1 and 2
```

$$\left[\begin{matrix}0 & 1 & 0\\1 & 0 & 0\\0 & 0 & 1\end{matrix}\right]$$

```python
P.inv(), P.transpose()
```

$$\begin{pmatrix}\left[\begin{matrix}0 & 1 & 0\\1 & 0 & 0\\0 & 0 & 1\end{matrix}\right], & \left[\begin{matrix}0 & 1 & 0\\1 & 0 & 0\\0 & 0 & 1\end{matrix}\right]\end{pmatrix}$$

```python
P.inv() == P.transpose()
```

    True

* If a matrix is of size *n* × *n* then there are *n*! number of permutations

## The transpose of a matrix

* We have mentioned transposes of a matrix, but what are they?
* The simply make row of the column elements and columns of the row elements as in the example below

```python
a11, a12, a13, a14, a21, a22, a23, a24, a31, a32, a33, a34 = symbols('a11, a12, a13, a14, a21, a22, a23, a24, a31, a32, a33, a34')
# Creating mathematical scalar constants
```

```python
A = Matrix([[a11, a12, a13], [a21, a22, a23], [a31, a32, a33]])
A
```

$$\left[\begin{matrix}a_{11} & a_{12} & a_{13}\\a_{21} & a_{22} & a_{23}\\a_{31} & a_{32} & a_{33}\end{matrix}\right]$$

```python
A.transpose()
```

$$\left[\begin{matrix}a_{11} & a_{21} & a_{31}\\a_{12} & a_{22} & a_{32}\\a_{13} & a_{23} & a_{33}\end{matrix}\right]$$

* This applies to any size matrix

```python
A = Matrix([[a11, a12, a13, a14], [a21, a22, a23, a24]])
A
```

$$\left[\begin{matrix}a_{11} & a_{12} & a_{13} & a_{14}\\a_{21} & a_{22} & a_{23} & a_{24}\end{matrix}\right]$$

```python
A.transpose()
```

$$\left[\begin{matrix}a_{11} & a_{21}\\a_{12} & a_{22}\\a_{13} & a_{23}\\a_{14} & a_{24}\end{matrix}\right]$$

* Multiplying a matrix by its transpose results in a symmetric matrix

```python
A * A.transpose()
```

$$\left[\begin{matrix}a_{11}^{2} + a_{12}^{2} + a_{13}^{2} + a_{14}^{2} & a_{11} a_{21} + a_{12} a_{22} + a_{13} a_{23} + a_{14} a_{24}\\a_{11} a_{21} + a_{12} a_{22} + a_{13} a_{23} + a_{14} a_{24} & a_{21}^{2} + a_{22}^{2} + a_{23}^{2} + a_{24}^{2}\end{matrix}\right]$$

## Symmetric matrices

* A symmetric matrix is a square matrix with elements opposite the main diagonal all equal
* Example

```python
S = Matrix([[1, 3, 2], [3, 2, 4], [2 , 4, 2]])
S
```

$$\left[\begin{matrix}1 & 3 & 2\\3 & 2 & 4\\2 & 4 & 2\end{matrix}\right]$$

* On the main diagonal we have 1, 2, 2
* Opposite this main diagonal we have a 3 and a 3 and a 2 and a 2 and a 4 and a 4
* The transpose of a symmetric matrix is equal to the matrix

```python
S == S.transpose()
```

    True

## Vector spaces

* A vector space is a bunch of vectors (a set of vectors) With certain properties that allow us to do stuff with
* The space ℝ2 is all vectors of two components that reaches every coordinate point in ℝ2
* It always includes the zero vector **0**
* We usually call this vector space *V*, such that *V* = ℝ2 or *V* = ℝn
* A linear combination of a certain number of these can also fill all of ℝ2
* A good example is the two unit vectors along the two axes
* Such a set of vectors form a basis for *V*
* The two of them also span ℝ2, i.e. a linear combination of them fills *V* = ℝ2
* Linear independence means the vectors in ℝ2 don't fall on the same line
    * If they do, we can't get to all coordinate points in ℝ2
* The **important** point about a vector space *V* is that it allows for vector addition and scalar multiplication
    * Taking any of the set of vectors in *V* and adding them results in a new vector which is still a component of *V*
    * Multiplying a scalar by any of the vectors in *V* results in a vector still in *V*

### A subspace

* For a subspace the rules of vector addition and scalar multiplication must apply
* I.e. a quadrant of ℝ2 is not a vector subspace
    * Addition or scalar multiplication of any vector in this quadrant can lead to a vector outside of this quadrant
* The zero vector **0** is a subspace (every subspace must contain the zero vector)
* The whole space *V* = ℝn (here we use *n* = 2) is a subspace of itself
* Continuing with our example of *n* = 2, any line **through the origin** is a subspace of ℝ2
    * Adding a vector on this line to itself of a scalar multiple of itself will eventually fill the whole line
* For *n* = 3 we have the whole space *V* = ℝ3, a plane through the origin, a line through the origin and the zero vectors are all subspace of *V* = ℝ3
* The point is that vector addition and scalar multiplication of vectors in the subspace must result in a new vector that remains in the subspace
* Every subspace must include the zero vector **0**
* All the properties of vectors must apply to the vectors in a subspace (and a space)

## Column spaces of matrices

* Here we see the columns of a matrix as a vector
* If there are two columns and three rows we will have the following as an example
$$ \begin{bmatrix} 2 & 1 \\ 1 & 3 \\ 2 & 2 \end{bmatrix}=\begin{bmatrix} 2 \\ 1 \\ 2 \end{bmatrix}+\begin{bmatrix} 1 \\ 3 \\ 2 \end{bmatrix} $$
* If they are not linear combinations of each other addition and scalar multiplication of the two of them will fill a plane in ℝ3

```python

```


```python
import numpy as np
import pandas as pd
import linearsolve as ls
import matplotlib.pyplot as plt
plt.style.use('classic')
%matplotlib inline

pd.plotting.register_matplotlib_converters()
```

# Class 16: Introduction to New-Keynesian Business Cycle Modeling

In this notebook, we will briefly explore US macroeconomic data suggesting that, contrary to the assumptions of most RBC models, there is in fact a relationship between real and nominal quantities over the business cycle. Then we will use `linearsolve` to compute impulse responses of output, inflation, and the nominal interest rate to a monetary policy shock in the New-Keynesian model.

## Data

The file `business_cycle_data_actual_trend_cycle.csv`, available at https://github.com/letsgoexploring/econ126/raw/master/Data/Csv/business_cycle_data_actual_trend_cycle.csv, contains actual and trend data for real GDP per capita, real consumption per capita, real investment per capita, real physical capital per capita, TFP, hours per capita, the rea money supply (M2), (nominal) interest rate on 3-month T-bills, the PCE inflation rate, and the unemployment rate; each at quarterly frequency. The GDP, consumption, investment, capital, and money supply data are in terms of 2012 dollars. Hours is measured as an index with the value in October 2012 set to 100.

```python
# Read business_cycle_data_actual_trend.csv into a Pandas DataFrame with the first column set as the index and parse_dates=True
data = pd.read_csv('https://github.com/letsgoexploring/econ126/raw/master/Data/Csv/business_cycle_data_actual_trend_cycle.csv',index_col=0,parse_dates=True)

# Print the last five rows of the data
data.tail()
```

      gdp
      gdp_trend
      gdp_cycle
      consumption
      consumption_trend
      consumption_cycle
      investment
      investment_trend
      investment_cycle
      hours
      ...
      real_m2_cycle
      t_bill_3mo
      t_bill_3mo_trend
      t_bill_3mo_cycle
      pce_inflation
      pce_inflation_trend
      pce_inflation_cycle
      unemployment
      unemployment_trend
      unemployment_cycle

      2018-07-01
      72.589059
      72.376813
      0.002928
      49.377099
      49.228728
      0.003009
      12.887706
      12.752714
      0.010530
      105.399877
      ...
      -0.010409
      0.020400
      0.016665
      0.003735
      0.057662
      0.045360
      0.012303
      0.037667
      0.038054
      -0.000388

      2018-10-01
      72.606447
      72.672948
      -0.000915
      49.377294
      49.441990
      -0.001309
      12.942796
      12.825832
      0.009078
      105.435736
      ...
      -0.015843
      0.023167
      0.018115
      0.005051
      0.046050
      0.045643
      0.000408
      0.038000
      0.036738
      0.001262

      2019-01-01
      73.251081
      72.970978
      0.003831
      49.529684
      49.656456
      -0.002556
      13.135114
      12.899000
      0.018139
      105.677466
      ...
      -0.010384
      0.023867
      0.019566
      0.004300
      0.039181
      0.045903
      -0.006722
      0.038667
      0.035432
      0.003234

      2019-04-01
      73.482233
      73.270527
      0.002885
      49.967211
      49.872074
      0.001906
      12.910780
      12.972253
      -0.004750
      105.340443
      ...
      -0.012074
      0.023000
      0.021015
      0.001985
      0.040987
      0.046152
      -0.005165
      0.036333
      0.034133
      0.002201

      2019-07-01
      73.703609
      73.571388
      0.001796
      50.218595
      50.088709
      0.002590
      12.811444
      13.045774
      -0.018125
      105.768114
      ...
      -0.004615
      0.019800
      0.022462
      -0.002662
      0.039933
      0.046396
      -0.006463
      0.036333
      0.032835
      0.003498

5 rows × 30 columns

### Exercise: GDP and Inflation

Construct a plot of the cyclical components of GDP and inflation.

```python
# Construct plot
plt.plot(data['pce_inflation_cycle']*100,alpha=0.75,lw=3,label='Inflation')
plt.plot(data['gdp_cycle']*100,c='r',alpha=0.5,label='GDP')
plt.grid()
plt.ylabel='Percent'
plt.title('GDP and Inflation')

# Place legend to right of figure. PROVIDED
plt.legend(loc='center left', bbox_to_anchor=(1, 0.5))
```

### Exercise: GDP and the 3-Month T-Bill Rate

Construct a plot of the cyclical components of GDP and the 3-month T-bill rate.

```python
# Construct plot
plt.plot(data['t_bill_3mo_cycle']*100,alpha=0.75,lw=3,label='Inflation')
plt.plot(data['gdp_cycle']*100,c='r',alpha=0.5,label='GDP')
plt.grid()
plt.ylabel='Percent'
plt.title('GDP and 3-Month T-Bill Rate')

# Place legend to right of figure. PROVIDED
plt.legend(loc='center left', bbox_to_anchor=(1, 0.5))
```

### Correlations Between GDP, Inflation, and 3-Month T-Bill Rate

Compute the coefficients of corrrelation between GDP, inflation, and the 3-month T-bill rate.

```python
data[['gdp_cycle','pce_inflation_cycle','t_bill_3mo_cycle']].corr()
```

      gdp_cycle
      pce_inflation_cycle
      t_bill_3mo_cycle

      gdp_cycle
      1.000000
      0.553497
      0.407666

      pce_inflation_cycle
      0.553497
      1.000000
      0.241685

      t_bill_3mo_cycle
      0.407666
      0.241685
      1.000000

Strong (but not perfect!) correlations between GDP and inflation and GDP and the T-bill rate suggest link between nominal and real quantities over the business cycle that should be exaplined by business cycle theory.

## The New-Keynesian Model

The most basic version of the New-Keynesian Model can be expressed as:

\begin{align}
y_t & = E_t y_{t+1}  - \left( r_{t} - \bar{r}\right) + g_t\\
i_{t} & = r_{t} + E_t \pi_{t+1}\\
i_{t} & = \bar{r} + \pi^T + \phi_{\pi}\big(\pi_t - \pi^T\big) + \phi_{y}\big(y_t - \bar{y}\big) + v_t\\
\pi_t -\pi^T & = \beta \left( E_t\pi_{t+1} - \pi^T\right)  + \kappa (y_t -\bar{y})+ u_t,
\end{align}

where: $y_t$ is (log) output, $r_t$ is the real interest rate, $i_t$ is the nominal interest rate, $\pi_t$ is the rate of inflation between periods $t-1$ and $t$, $\bar{r}$ is the long-run average real interest rate or the *natural rate of interest*, $\beta$ is the household's subjective discount factor, and $\pi^T$ is the central bank's inflation target. The coeffieints $\phi_{\pi}$ and $\phi_{y}$ reflect the degree of intensity to which the central bank *endogenously* adjusts the nominal interest rate in response to movements in inflation and output.

The variables $g_t$, $u_t$, and $v_t$ represent exogenous shocks to aggregate demand, inflation, and monetary policy. They follow AR(1) processes:

\begin{align}
g_{t+1} & = \rho_g g_{t} + \epsilon^g_{t+1}\\
u_{t+1} & = \rho_u u_{t} + \epsilon^u_{t+1}\\
v_{t+1} & = \rho_v v_{t} + \epsilon^v_{t+1}.
\end{align}

The goal is to compute impulse responses in the model to a one percent exogenous increase in the nominal interest rate. We will use the following parameterization:

| $\bar{y}$ | $\beta$ | $\bar{r}$    | $\kappa$ | $\pi^T$ | $\phi_{\pi}$ | $\phi_y$ | $\rho_g$ | $\rho_u$ | $\rho_v$ |
|-----------|---------|--------------|----------|---------|--------------|----------|----------|----------|---------|
| 0         | 0.995    | $-\log\beta$ | 0.1      | 0.02/4    | 1.5          | 0.5/4    | 0.5      | 0.5      | 0.5     |

```python
# Create a variable called 'parameters' that stores the model parameter values in a Pandas Series
parameters = pd.Series()
parameters['y_bar'] = 0
parameters['beta'] = 0.995
parameters['r_bar'] = -np.log(parameters.beta)
parameters['kappa'] = 0.1
parameters['pi_T'] = 0.02/4
parameters['phi_pi'] = 1.5
parameters['phi_y'] = 0.5/4
parameters['rho_g'] = 0.5
parameters['rho_u'] = 0.5
parameters['rho_v'] = 0.5

# Print the model's parameters
print(parameters)
```

    y_bar     0.000000
    beta      0.995000
    r_bar     0.005013
    kappa     0.100000
    pi_T      0.005000
    phi_pi    1.500000
    phi_y     0.125000
    rho_g     0.500000
    rho_u     0.500000
    rho_v     0.500000
    dtype: float64

```python
# Create variable called 'var_names' that stores the variable names in a list with state variables ordered first
var_names = ['g','u','v','y','pi','i','r']

# Create variable called 'shock_names' that stores an exogenous shock name for each state variable.
shock_names = ['e_g','e_u','e_v']

# Define a function that evaluates the equilibrium conditions of the model solved for zero. PROVIDED
def equilibrium_equations(variables_forward,variables_current,parameters):

    # Parameters. PROVIDED
    p = parameters

    # Current variables. PROVIDED
    cur = variables_current

    # Forward variables. PROVIDED
    fwd = variables_forward

    # IS equation
    is_equation = fwd.y - (cur.r -p.r_bar) + cur.g - cur.y

    # Fisher_equation
    fisher_equation = cur.r + fwd.pi - cur.i

    # Monetary policy
    monetary_policy = p.r_bar + p.pi_T + p.phi_pi*(cur.pi - p.pi_T) + p.phi_y*cur.y + cur.v - cur.i

    # Phillips curve
    phillips_curve = p.beta*(fwd.pi- p.pi_T) + p.kappa*cur.y + cur.u - (cur.pi-p.pi_T)

    # Demand process
    demand_process = p.rho_g*cur.g - fwd.g

    # Monetary policy process
    monetary_policy_process = p.rho_v*cur.v - fwd.v

    # Inflation process
    inflation_process = p.rho_u*cur.u - fwd.u

    # Stack equilibrium conditions into a numpy array
    return np.array([
            is_equation,
            fisher_equation,
            monetary_policy,
            phillips_curve,
            demand_process,
            monetary_policy_process,
            inflation_process
        ])

# Initialize the model into a variable named 'nk_model'
nk_model = ls.model(equations = equilibrium_equations,
                 n_states=3,
                 var_names=var_names,
                 shock_names=shock_names,
                 parameters = parameters)
```

```python
# Compute the steady state numerically using .compute_ss() method of nk_model
guess = [0,0,0,0,0.01,0.01,0.01]
nk_model.compute_ss(guess)

# Print the computed steady state
print(nk_model.ss)
```

    g     1.832559e-36
    u     3.518101e-35
    v     4.196417e-36
    y    -3.184980e-20
    pi    5.000000e-03
    i     1.001254e-02
    r     5.012542e-03
    dtype: float64

```python
# Find the log-linear approximation around the non-stochastic steady state and solve using .approximate_and_solve() method of nk_model
# set argumement 'log_linear' to False because the model is already log-linear.
nk_model.approximate_and_solve(log_linear=False)

print(nk_model.approximated())
```

    Linear equilibrium conditions:

           y[t+1|t] = -g[t]+y[t]+r[t]

          pi[t+1|t] = i[t]-r[t]

                  0 = -v[t]-0.125·y[t]-1.5·pi[t]+i[t]

    0.995·pi[t+1|t] = -u[t]-0.1·y[t]+pi[t]

            -g[t+1] = -0.5·g[t]

            -v[t+1] = -0.5·v[t]

            -u[t+1] = -0.5·u[t]

### Impulse Responses

Compute a 21 period impulse response of the model's variables to a 0.01/4 unit shock to the exogenous component of  monetary policy ($v_t$) in period 5.

```python
# Compute impulse responses
nk_model.impulse(T=21,t0=5,shocks=[0,0,0.01/4])

# Print the first 10 rows of the computed impulse responses to the monetary policy shock
print(nk_model.irs['e_v'].head(10))
```

          e_v             g    u         v         y        pi         i         r
    0  0.0000  0.000000e+00  0.0  0.000000  0.000000  0.000000  0.000000  0.000000
    1  0.0000  0.000000e+00  0.0  0.000000  0.000000  0.000000  0.000000  0.000000
    2  0.0000  0.000000e+00  0.0  0.000000  0.000000  0.000000  0.000000  0.000000
    3  0.0000  0.000000e+00  0.0  0.000000  0.000000  0.000000  0.000000  0.000000
    4  0.0000  0.000000e+00  0.0  0.000000  0.000000  0.000000  0.000000  0.000000
    5  0.0025  0.000000e+00  0.0  0.002500 -0.003034 -0.000604  0.001215  0.001517
    6  0.0000 -3.469447e-19  0.0  0.001250 -0.001517 -0.000302  0.000608  0.000758
    7  0.0000 -3.469447e-19  0.0  0.000625 -0.000758 -0.000151  0.000304  0.000379
    8  0.0000 -2.602085e-19  0.0  0.000313 -0.000379 -0.000075  0.000152  0.000190
    9  0.0000 -1.734723e-19  0.0  0.000156 -0.000190 -0.000038  0.000076  0.000095

Plot the computed impulses responses of the nominal interest rate, the real interest rate, output, and inflation. Express inflation and interest rates in *annualized* (e.g., multiplied by 4) terms.

```python
# Create figure. PROVIDED
fig = plt.figure(figsize=(12,8))

# Create upper-left axis. PROVIDED
ax1 = fig.add_subplot(2,2,1)
# Create upper-right axis. PROVIDED
ax2 = fig.add_subplot(2,2,2)
# Create lower-left axis. PROVIDED
ax3 = fig.add_subplot(2,2,3)
# Create lower-right axis. PROVIDED
ax4 = fig.add_subplot(2,2,4)

# Set axis 1 ylabel
ax1.set_ylabel('% dev from steady state')
# Set axis 2 ylabel
ax2.set_ylabel('% dev from steady state')
# Set axis 3 ylabel
ax3.set_ylabel('% dev from steady state')
# Set axis 4 ylabel
ax4.set_ylabel('% dev from steady state')

# Set axis 1 limits
ax1.set_ylim([-0.2,0.8])
# Set axis 2 limits
ax2.set_ylim([-0.2,0.8])
# Set axis 3 limits
ax3.set_ylim([-0.4,0.1])
# Set axis 4 limits
ax4.set_ylim([-0.4,0.1])

# Plot the nominal interest rate, real interest rate, output, and inflation
(nk_model.irs['e_v']['i']*400).plot(ax=ax1,lw=4,alpha=0.75,title='Nominal Interest',grid=True)
(nk_model.irs['e_v']['r']*400).plot(ax=ax2,lw=4,alpha=0.75,title='Real Interest',grid=True)
(nk_model.irs['e_v']['y']*100).plot(ax=ax3,lw=4,alpha=0.75,title='Output',grid=True)
(nk_model.irs['e_v']['pi']*400).plot(ax=ax4,lw=4,alpha=0.75,title='Inflation',grid=True)
```


之前介绍了几种不同的监督学习算法以及测试集和验证集的概念：

- [线性回归](https://github.com/wolverinn/Machine-Learning-notes/blob/master/Machine%20Learning%20Week1%20--%20Linear%20Regression%2C%20Gradient%20Descent.ipynb)
- [Logistic 回归](https://github.com/wolverinn/Machine-Learning-notes/blob/master/Machine%20Learning%20Week2%20--%20Logistic%20Regression.ipynb)
- [神经网络模型](https://github.com/wolverinn/Machine-Learning-notes/blob/master/Machine%20Learning%20Week3%20--%20Neural%20Networks%2C%20Back%20Propagation.ipynb)
- [训练模型的优化](https://github.com/wolverinn/Machine-Learning-notes/blob/master/Machine%20Learning%20Week4%20--%20Test%20set%20and%20Validation%20set.ipynb)

这篇文章介绍监督学习中一个很强大，在学术界和工业界都很流行的算法——**支持向量机（Support Vector Machine，SVM）**，首先大致了解一下支持向量机的思想

### 支持向量机简介

支持向量机是用于分类问题的算法，支持向量机的基本思想是对于N维的数据，找到一个N-1维的超平面（hyperplane），作为数据分类的决策边界，确定这个超平面的规则是找到离分隔超平面最近的那些点，使它们离分隔超平面的距离尽可能远，因为这样可以使得训练出来的分类器更加健壮。离超平面最近的那些点就被称为支持向量（support vector），举个例子，对于二维的数据，支持向量机就要找到一个一维的向量来作为边界，如图所示：

### 线性支持向量机（Linear SVM）

在 Logistic 回归中，我们根据$\theta^Tx$是否大于0来进行分类，并且将两个类别分别记作1和0，这里我们则将两个类别分别记为+1和-1，即$y^n=1$或$y^n=-1$。当$f(x)=\theta^Tx$大于0的时候，预测为+1，并且$f(x)$越大越好；当小于0的时候，预测为-1，并且$f(x)$越小越好

所以其实线性支持向量机的假设函数和 Logistic回归的假设函数是相同的，不同的只是损失函数。下面我们来看一下怎么确定线性支持向量机的损失函数。

首先我们画出用不同函数作为损失函数时候的图像，将横轴记为$y^nf(x)$，这样的话，如果$y^n=1$，这时候我们希望$f(x)$越正越好，所以$y^nf(x)$越大，损失函数越小，$f(x)$越小，损失函数越大；而如果$y^n=-1$，这时候希望$f(x)$越负越好，所以$y^nf(x)$越大，说明$f(x)$越负，即损失函数就越小。所以，将横轴记为$y^nf(x)$更加方便，无论是$y^n=1$还是$y^n=-1$的情况，损失函数都是随着横轴递减的

之前我们曾经使用过 Square Error Cost 作为损失函数，这是一个二次函数，当$y^n=1$的时候，是$(f(x)-1)^2$，当$y^n=-1$的时候，是$(f(x-(-1)))^2$，因此总的可以写成$(y^nf(x)-1)^2$

在 Logistic 回归中使用的损失函数则是交叉熵（Cross-Entropy），统一了$y^n=1$和$y^n=-1$的情况后，可以写成$\ln(1+e^{-y^nf(x)})/\ln2$，其中除以了一个常数是不影响的。从下图看一下：

红色的曲线就是 Square Error Cost，显然违背了随着横轴递减的规律，因此不能采用；蓝线是 Sigmoid 函数加上 Square Error Cost，虽然满足递减的规律，但是变化趋势太小，损失函数的下降并不明显，而我们知道随着横轴增大，损失函数应该会下降很多；而绿色的曲线就是 Logistic 回归中使用的交叉熵，和蓝线对比起来，损失函数的下降趋势就很明显

那么在支持向量机中，使用的是叫做 **Hinge Loss** 的损失函数，它的图像就是下面的紫色的线：

具体的损失函数是：

$$l(f(x^n),y^n)=max(0,1-y^nf(x^n))$$

当$y^n=1$时，只要$1-f(x)<0$即$f(x)>1$，损失函数就为0了；

当$y^n=-1$时，只要$1+f(x)<0$即$f(x)<-1$，损失函数为0

也就是说，当$y^nf(x)>1$时，损失函数就是0，就算继续让$y^nf(x)$增大，也不会得到损失的下降；而当$y^nf(x)$在0到1之间时，虽然已经可以预测出正确的结果，但是对于 Hinge Loss 来说还不够好，还是有一定的损失，要比正确的结果好过一段距离之后，损失函数才会降为0。相比于交叉熵，Hinge Loss 更加健壮

所以概括一下，线性支持向量机使用的假设函数就是一个线性函数（$f(x)=\theta^Tx$），损失函数是 Hinge Loss 再加上一个正则化的部分，这是一个凸函数，因此我们可以使用梯度下降法来进行优化

### 松弛变量（Slack Variable）

先直观地理解一下松弛变量。支持向量机的目标是找到一个边界以最大化边界与支持向量的距离，但是有时有些样本点可能不满足间隔条件，这时引入一个松弛变量$\epsilon^n\ge0$，使间隔加上松弛变量满足间隔条件

我们来看一下 Hinge Loss 的部分，记：

$$\epsilon^n=max(0,1-y^nf(x))$$

由于我们的目标是最小化 Hinge Loss，因此上式等价于：

$$\begin{align} \epsilon\ge1-y^nf(x) \newline\ \epsilon^n\ge0\end{align}$$

变化之后：$y^nf(x)\ge1-\epsilon^n$，就是说**间隔（margin）**要大于等于1，但是这个间隔是软间隔（soft margin），其中有一个松弛变量，就是$\epsilon^n$

这是一个**二次规划（Quadratic Programming，QP）**问题，因此可以用QP问题的算法来解。当然，不用QP算法的话用梯度下降法也是可以的

### 对 Hinge Loss 使用梯度下降法

忽略掉正则化的部分，线性支持向量机的损失函数就是：

$$L(f)=\sum_nl(f(x^n,y^n))$$

$$l(f(x^n),y^n)=max(0,1-y^nf(x^n))$$

如果我们的参数是$\theta$，我们首先需要求出损失函数对$\theta$的偏导数：

$$\frac{\partial l(f(x^n,y^n))}{\partial \theta_i}=\frac{\partial l(f(x^n,y^n))}{\partial f(x^n)}\frac{\partial f(x^n)}{\partial \theta_i}$$

由于$f(x^n)=\theta^Tx^n$，因此$\frac{\partial f(x^n)}{\partial \theta_i}=x_i^n$

接下来计算$\frac{\partial l(f(x^n,y^n))}{\partial f(x^n)}$：

$$\frac{\partial max(0,1-y^nf(x^n))}{\partial f(x^n)}=
\begin{cases}
-y^n, & if \ \ \ y^nf(x^n)<1 \\
0, & otherwise
\end{cases}$$

因此，偏导数为：

$$\frac{\partial L(f)}{\partial \theta_i}=\sum_n-\delta(y^nf(x^n)<1)y^nx_i^n$$

我们把$-\delta(y^nf(x^n)<1)y^n$记作$c^n(\theta)$，其中$\delta(x)$表示当满足$x$时，$\delta(x)$为1，否则为0

于是，我们就得到了在 Hinge Loss 中，梯度下降法的更新公式：

$$\theta_i=\theta_i-\alpha\sum_n c^n(\theta)x_i^n$$

### 核函数（Kernel Function）

将我们最终使用梯度下降法得到的那组最优化的参数记作向量$\theta^*$，实际上，$\theta^*$就是$x^n$的线性组合：

$$\theta^*=\sum_n a_n^*x^n$$

将梯度下降法的更新公式写成向量的形式：

$$\theta=\theta-\alpha\sum_n c^n(\theta)x^n$$

而前面说过$c^n(\theta)$很多时候都会是等于0的，因此对应最后的参数$\theta^*=\sum_n a_n^*x^n$，就会有很多数据点$x^n$前面对应的$a_n^*$是等于0的，也叫做$a_n^*$是**稀疏的（sparse）**，而那些$a_n^*$不等于0的$x^n$就叫做**支持向量（Support Vectors）**

意思就是如果一个数据点对应的$a_n^*$等于0，那么这个数据点对模型一点作用都没有，只有当它对应的$a_n^*$不等于0，它才会对决定最后的模型有用。只有少数的点会被选作支持向量，如果出现了异常值（outlier），那么只需要保证不把它选作支持向量就行了。比如如果是用的交叉熵作为损失函数，那么损失函数在每个地方的微分都是不等于0的，所以最终的$a_n^*$也不会是稀疏的，那么每一个数据点对最后的模型都会有影响，所以相比于其他模型，支持向量机是比较稳健的（robust）。

将式子$\theta^*=\sum_n a_n^*x^n$矩阵化得到：$\theta=Xa$，则：

$$f(x)=\theta^Tx=a^TX^Tx=\sum_n a_n(x^n*x)$$

具体的矩阵变换如图（图中的$w$就是指$\theta$）：

接下来，我们将假设函数写成带核函数的形式：

$$f(x)=\sum_n a_nK(x^n,x)$$

其中的$K(x,z)$就是**核函数（Kernel Function）**，如果不作任何映射直接作内积，就称为线性核，得到的就是之前的线性支持向量机。

之前构造的线性支持向量机没有作数据的映射，因此只能用来分隔线性的数据。如果数据线性不可分，就需要将$x$映射到$\phi(x)$，映射到高维空间之后得到线性可分的数据：

这样带来的问题是如果维度较高会导致计算量爆炸，因此使用核技巧（Kernel trick）可以帮助我们在高维映射下计算SVM，举个高维映射的例子：

这样，计算$\phi(x)*\phi(z)$就可以通过计算核函数$K(x,z)=(x*z)^2$来计算，避免了显式地写出映射后的结果，直接使计算量小了很多

在实际应用中，不需要去考虑如何映射，而是可以直接选择核函数，通常我们会从一些核函数中选择，例如：

- 线性核函数：$K(x,z)=<x,z>$（向量内积）
- 多项式核函数：$K(x,z)=(<x,z>+R)^d$
- 高斯核函数，高斯核函数是最常用的，可以将数据映射到无穷维，也叫做径向基函数（Radial Basis Function，RBF）：
$$K(x,z)=exp(-\frac{||x-z||^2}{2\sigma^2})$$

最后，放张图总结一下支持向量机：

参考：
- [李弘毅机器学习--支持向量机](https://www.bilibili.com/video/av10590361/?p=31)
- [支持向量机系列--Freemind](http://blog.pluskid.org/?page_id=683)

Machine Learning Week5 -- Support Vector Machine.ipynb