Modelos Discriminativos - Parte 1 - Sistemas Lineares

Os modelos geradores utilizam a Regra de Bayes para criar procedimentos de reconhecimento de padrões. O procedimento de treino desses modelos leva em consideração apenas dados que testam positivo para a classe que se deseja modelar. Os modelos discriminativos, em adição, são treinados para aceitar dados que testam positivo e, ao mesmo tempo, rejeitar dados que testam negativo para cada classe. Neste caderno, utilizaremos modelos discriminativos lineares no problema de classificação, utilizando a regra de minimização da norma Euclidiana do erro.

Objetivos

Ao fim desta iteração, o aluno será capaz de:

  • Entender o conceito de modelo discriminativo
  • Entender o conceito de norma Euclidiana do erro
  • Aplicar métodos lineares para problemas de classificação
  • Aplicar a técnica do enviesamento (biasing) para melhorar o desempenho de classificadores

In [1]:
# Inicializacao
%matplotlib inline

import numpy as np
from matplotlib import pyplot as plt

# Abrindo conjunto de dados
import csv
with open("biometria.csv", 'rb') as f:
    dados = list(csv.reader(f))
    
rotulos_volei = [d[0] for d in dados[1:-1] if d[0] is 'V']
rotulos_futebol = [d[0] for d in dados[1:-1] if d[0] is 'F']
altura_volei = [[float(d[1])] for d in dados[1:-1] if d[0] is 'V']
altura_futebol = [[float(d[1])] for d in dados[1:-1] if d[0] is 'F']
peso_volei = [[float(d[2])] for d in dados[1:-1] if d[0] is 'V']
peso_futebol = [[float(d[2])] for d in dados[1:-1] if d[0] is 'F']

Sistemas lineares

Um sistema linear é aquele cujas saídas são combinações lineares das entradas. Assim, se um sistema linear tem $N$ entradas $x_n$ e $M$ saídas $y_m$, então temos que: $$y_m = \sum_{n=1}^N a_{n,m} x_n, \forall m \in [1, M], $$ Na forma matricial, essa expressão é equivalente a: $$\boldsymbol Y = \boldsymbol A \boldsymbol X$$

Se $\boldsymbol A$ for uma matriz quadrada e inversível, então podemos multiplicar os dois lados da expressão por $\boldsymbol A^{-1}$ e, assim, temos que $\boldsymbol A^{-1} \boldsymbol Y = \boldsymbol A^{-1} \boldsymbol A \boldsymbol X = \boldsymbol X$.

Caso a matriz $\boldsymbol A$ não seja quadrada e inversível, haverá inevitavelmente uma diferença entre os lados da expressão matricial, ou seja, a norma Euclidiana do erro $e = ||\boldsymbol Y - \boldsymbol A \boldsymbol X||$ será diferente de zero. Neste caso, podemos calcular sua pseudo-inversa $\boldsymbol A^\dagger = (\boldsymbol A^T \boldsymbol A)^{-1} \boldsymbol A^T$, que será a solução que minimiza a norma Euclidiana do erro para o caso visto acima.

Classificadores lineares

Classificadores, como sabemos, são sistemas que recebem como entrada um conjunto de características de um elemento, executam alguma operação sobre elas e retornam, na saída, uma estimativa da classe à qual o elemento em questão deve fazer parte. Em nosso modelo linear, poderíamos então assumir que:

  • $\boldsymbol Y$ representa classes de elementos,
  • $\boldsymbol A$ representa características de elementos, e
  • $\boldsymbol X$ representa procedimentos de classificação.

Assim, podemos definir nosso procedimento de treino e teste supervisionados:

  1. Definir elementos $\boldsymbol Y$ e $\boldsymbol A$ que serão usados para treino,
  2. Estimar $\boldsymbol X$ utilizando a regra: $\boldsymbol X = \boldsymbol A^\dagger \boldsymbol Y$,
  3. Na etapa de teste, estimar $\boldsymbol Y = \boldsymbol A \boldsymbol X$.

Agora, temos um novo problema: como representar as classes na matriz $\boldsymbol Y$?

Representações

Podemos optar por representar cada classe como um valor diferente, de forma que $\boldsymbol Y$ é uma matrix $1 \times 1$. Em vetores de características com $D$ dimensões, $\boldsymbol A$ tem dimensão $1 \times D$ e, portanto, $\boldsymbol X$ tem dimensão $D \times 1$.

Outra opção é representar $\boldsymbol Y$ como uma matriz com tantas dimensões quantas forem as $M$ (ou seja, é uma matrix $M times 1$) classes envolvidas no problema. Nesse caso, $\boldsymbol X$ tem dimensão $D \times M$.

Veja que a segunda possibilidade, na verdade, envolve a construção de um classificador diferente para cada classe. A seguir, verificaremos, na prática, as consequências de usar cada um desses esquemas de classificação.


In [2]:
from sklearn.cross_validation import train_test_split
def treinamento_linear_1d(train_size=0.3):
    # Separar dados adequadamente
    dados_treino, dados_teste, rotulos_treino, rotulos_teste =\
        train_test_split(altura_volei + altura_futebol, rotulos_volei + rotulos_futebol, train_size=train_size)

    # Definir representacoes de classes
    y = []
    for i in rotulos_treino:
        if i == 'V':
            y.append(1)
        else:
            y.append(-1)
    y = np.array(y)

    # Treinar modelo linear
    a = np.array(dados_treino)
    x = np.dot(np.linalg.pinv(a), y)
    
    # Executar modelos sobre conjunto de teste
    y_est = np.dot(np.array(dados_teste), x)

    # Verificar qual modelo mais provavelmente gerou os dados de teste
    res = []
    for i in xrange(len(dados_teste)):
        if y_est[i] < 0:
            res.append('F')
        else:
            res.append('V')

            
    # Verificar quantidade de acertos
    acertos = 0.0
    for i in xrange(len(res)):
        if res[i] == rotulos_teste[i]:
            acertos += 1
    acertos *= 100.0/float(len(res))
    
    return acertos

print "Acertos:", treinamento_linear_1d(), "%"


Acertos: 45.1612903226 %

In [7]:
def treinamento_linear_2d(train_size=0.3):
    # Separar dados adequadamente
    dados_treino, dados_teste, rotulos_treino, rotulos_teste =\
        train_test_split(altura_volei + altura_futebol, rotulos_volei + rotulos_futebol, train_size=train_size)

    # Definir representacoes de classes
    y = []
    for i in rotulos_treino:
        if i == 'V':
            y.append([1, 0])
        else:
            y.append([0, 1])
    y = np.array(y)

    # Treinar modelo linear
    a = np.array(dados_treino)
    x = np.dot(np.linalg.pinv(a), y)
    
    # Executar modelos sobre conjunto de teste
    y_est = np.dot(np.array(dados_teste), x)

    # Verificar qual modelo mais provavelmente gerou os dados de teste
    res = []
    for i in xrange(len(dados_teste)):
        if y_est[i][0] < y_est[i][1]:
            res.append('F')
        else:
            res.append('V')

            
    # Verificar quantidade de acertos
    acertos = 0.0
    for i in xrange(len(res)):
        if res[i] == rotulos_teste[i]:
            acertos += 1
    acertos *= 100.0/float(len(res))
    
    return acertos

print "Acertos:", treinamento_linear_2d(), "%"


Acertos: 45.1612903226 %

Enviesamento

Ao executar algumas vezes o procedimento de testes acima, é possível verificar que ele está significativamente instável. Isso acontece quando o modelo proposto é inadequado ao problema. Em nosso caso, o que acontece é que o modelo força uma fronteira de decisão que passa pela origem do espaço Euclidiano, o que claramente é inadequado.

Em verdade, fronteiras de decisão que passam pela origem são frequentemente incorretas, de forma que é possível usar soluções para esse problema. Uma solução bastante simples é adicionar um componente constante ao nosso sistema, de forma que passamos a calcular: $$\boldsymbol Y = \boldsymbol A \boldsymbol X + \boldsymbol B.$$

Veja que isso significa que $\boldsymbol B$ também deverá ser estimado no processo de treino. Uma solução de programação comumente encontrada para tal é implementar o sistema na forma:

$$\boldsymbol Y = [\boldsymbol A \boldsymbol 1] [\boldsymbol X \boldsymbol B],$$

onde $\boldsymbol 1$ representa uma matriz-coluna composta somente de elementos unitários e o operador $[u v]$ representa a justaposição de duas matrizes.

Desta forma, podemos mudar nossas rotinas de treino para:


In [8]:
def treinamento_linear_1d_bias(train_size=0.3):
    # Separar dados adequadamente
    dados_treino, dados_teste, rotulos_treino, rotulos_teste =\
        train_test_split(altura_volei + altura_futebol, rotulos_volei + rotulos_futebol, train_size=train_size)

    # Definir representacoes de classes
    y = []
    for i in rotulos_treino:
        if i == 'V':
            y.append(1)
        else:
            y.append(-1)
    y = np.array(y)

    # Treinar modelo linear
    a = np.array(dados_treino)
    a = np.hstack( (a, np.ones((len(dados_treino),1))))
    x = np.dot(np.linalg.pinv(a), y)
    
    # Executar modelos sobre conjunto de teste
    y_est = np.dot(np.hstack ( (np.array(dados_teste), np.ones((len(dados_teste),1)))), x)

    # Verificar qual modelo mais provavelmente gerou os dados de teste
    res = []
    for i in xrange(len(dados_teste)):
        if y_est[i] < 0:
            res.append('F')
        else:
            res.append('V')

            
    # Verificar quantidade de acertos
    acertos = 0.0
    for i in xrange(len(res)):
        if res[i] == rotulos_teste[i]:
            acertos += 1
    acertos *= 100.0/float(len(res))

    return acertos

print "Acertos:", treinamento_linear_1d_bias(), "%"


Acertos: 93.5483870968 %

In [9]:
def treinamento_linear_2d_bias(train_size=0.3):
    # Separar dados adequadamente
    dados_treino, dados_teste, rotulos_treino, rotulos_teste =\
        train_test_split(altura_volei + altura_futebol, rotulos_volei + rotulos_futebol, train_size=train_size)

    # Definir representacoes de classes
    y = []
    for i in rotulos_treino:
        if i == 'V':
            y.append([1, 0])
        else:
            y.append([0, 1])
    y = np.array(y)

    # Treinar modelo linear
    a = np.array(dados_treino)
    a = np.hstack( (a, np.ones((len(dados_treino),1))))
    x = np.dot(np.linalg.pinv(a), y)
    
    # Executar modelos sobre conjunto de teste
    y_est = np.dot(np.hstack ( (np.array(dados_teste), np.ones((len(dados_teste),1)))), x)

    # Verificar qual modelo mais provavelmente gerou os dados de teste
    res = []
    for i in xrange(len(dados_teste)):
        if y_est[i][0] < y_est[i][1]:
            res.append('F')
        else:
            res.append('V')


            
    # Verificar quantidade de acertos
    acertos = 0.0
    for i in xrange(len(res)):
        if res[i] == rotulos_teste[i]:
            acertos += 1
    acertos *= 100.0/float(len(res))

    return acertos

print "Acertos:", treinamento_linear_2d_bias(), "%"


Acertos: 83.8709677419 %

Sistemas geradores e sistemas discriminativos

Podemos, como sempre, comparar os sistemas que propusemos, através dos procedimentos Monte Carlo que já temos executado. Essa comparação será deixada como exercício. Neste ponto da discussão, é importante apenas que possamos entender que o enviesamento aumentou significativamente o desempenho de nosso sistema de classificação.

Vamos, porém, fazer algumas considerações sobre os sistemas discriminativos e os sistemas geradores.

No treinamento de sistemas geradores, o modelo de cada classe precisa de um certo número $N$ de amostras para ser treinado adequadamente. Isso acontece porque cada modelo é independente dos outros. O número $N$ é uma estimativa, e dificilmente pode ser estimado com precisão.

Nos sistemas discriminativos, cada modelo precisa de dados mostrando cada uma das $M$ classes, o que significa que precisará de $M \times N$ pontos de dados. Apesar dessa necessidade, sistemas discriminativos podem conseguir resultados melhores que os geradores se forem treinados adequadamente.

Esse cenário se torna ainda mais relevante se for permitido que um elemento pertença a mais de uma classe simultaneamente. Suponha, por exemplo, pessoas que sabem jogar futebol e pessoas que sabem jogar vôlei: uma pessoa pode saber jogar um dos esportes, os dois, ou nenhum. O treinamento de um sistema gerador para este caso envolveria estimar um modelo para saber jogar vôlei, um modelo para saber jogar futebol e, possivelmente, um limiar de verossimilhança acima do qual o sistema estima que a pessoa sabe jogar aquele esporte. No caso de um sistema discriminativo, precisamos estimar modelos para todas as 4 combinações de habilidades esportivas.

Numa situação em que cada elemento pode pertencer a mais de uma das $M$ classes, existirão $2^M$ possíveis combinações de pertinência e não-pertinência. Assim, no caso de termos 20 classes (por exemplo, um sistema que indica as possíveis posições que um jogador de futebol poderia ocupar), teríamos que treinar nossos sistemas discriminativos para $2^{20}$ situações diferentes.

Esse número exponencial explode rapidamente. Devemos lembrar que $10^9 \approx 2^{30}$ e que o Número de Avogadro está em torno de $2^{80}$, o que significa que um modelo discriminativo rapidamente se torna inviável para problemas de classes múltiplas.

Exercícios

  1. Esboce, num desenho, a diferença entre fronteiras de decisão que usam enviesamento e fronteiras que não usam enviesamento.

  2. Escreva um pequeno texto que justifica o seu desenho.

  3. Mostre seu texto e o desenho para um colega, e veja o texto e o desenho desse colega. Compare os raciocínios de vocês dois. Como você sugeriria que o colega melhorasse seu texto e o desenho? Há equívocos? Ou ainda: ao ver o material produzido pelo colega, você tem pontos a adicionar em sua própria exposição? Tente entender as linhas de raciocínio que levaram aos diferentes resultados.

  4. Ao usar duas dimensões sem enviesamento, o desempenho do sistema caiu. Ao usar duas dimensões com enviesamento, o desempenho do sistema melhorou. Justifique esse comportamento.

  5. Crie um programa que permita comparar o desempenho dos sistemas discriminativos mostrados nesta iteração e os sistemas geradores discutidos anteriormente. Houve diferenças no desempenho? Crie hipóteses sobre esse resultado e então discuta com os colegas sobre quais hipóteses poderiam estar certas ou não.


In [ ]:


In [ ]:


In [ ]:


In [ ]: