Aprendizado Ativo: como construir modelos representativos e acurados com ajuda humana.

Emanuel Fontelles
Datarisk.io
Published in
14 min readNov 3, 2021

Uma verdade não conveniente sobre algoritmos de Machine Learning: “Eles não são mais importantes do que os dados necessários para construção de um modelo preditivo”. Apresentaremos o conceito de Aprendizado Ativo, do qual a ideia principal é que um algoritmo de Machine Learning pode alcançar alta acurácia com poucas amostras, caso ele seja capaz de escolher o conjunto de amostras que aprenderá.

1. Introdução

O que fazer quando temos grandes quantidades de dados cujos rótulos não estão anotados? Provavelmente, sua primeira resposta é utilizar algoritmos não supervisionados - como agrupadores - capazes de construir grupos baseados em alguma métrica espacial. O problema? Muitos dos sistemas que queremos agrupar não são distribuídos em forma simétricas onde os algoritmos possam performar facilmente.

Para respondermos a pergunta acima, vamos apresentar o Aprendizado Ativo, técnica que vem ganhando bastante espaço nos problemas recentes de Machine Learning onde rotular amostras é um processo difícil, demorado ou caro de realizar.

O que esperar deste texto:

  • As duas culturas de dados da modelagem estatística e como dados são o ativo mais importantes;
  • Uma introdução geral sobre aprendizado ativo e aplicação de estratégias de consultas baseada em amostragem de incerteza.

2. As duas culturas de dados da modelagem estatística

Essa é uma discussão antiga da comunidade estatística, que durante muito tempo buscou explicar fenômenos naturais com modelos matemáticos que exemplificassem a causalidade do fenômeno. Com o advento de novas tecnologias e metodologias, construir modelos preditivos tornou-se um problema computacional em sua grande maioria (não estou removendo a necessidade da formulação matemática, apenas enfatizando que algoritmos - conhecido como “caixa preta” - garantem maior performance independentemente das causas que levam à explicabilidade do fenômeno).

De acordo com Leo Breiman (Statistical Modeling: The Two Cultures) existem duas culturas no uso de modelagem estatística para alcançar conclusões sobre dados: uma supõe que os dados são gerados por um determinado modelo estocástico (data modeling) e a outra utiliza modelos algorítmicos e trata o mecanismo de dados como desconhecido (algorithm modeling).

Exemplificação da culturas de modelagem estatística. Dado um fenômeno natural, a cultura data modeling busca explicar o fenômeno como consequência das variáveis de entrada.

Para ele, quando estamos analisando a natureza de um fenômeno y gerado por um conjunto de variáveis x , existem dois objetivos principais nas análises:

  • Predição: ser capaz de prever quais serão as respostas às variáveis de entrada futuras;
  • Informação: extrair algumas informações sobre como a natureza está associando a variável de resposta partindo das variáveis de entrada.

2.1 Cultura Data Modeling

Por não encontrar um termo em português mais aderente, vou traduzir como sendo: Cultura da Modelagem do Dado. Na prática, essa cultura estatística pressupõe que os dados são gerados por uma distribuição, seguindo a equação abaixo:

Onde os coeficientes {bₘ} são estimados à partir dos dados e ε é o erro associado derivado de uma distribuição normal {N(0, σ²)}. Supondo que os dados sejam gerados dessa forma, elegantes testes de hipóteses, intervalos de confiança e distribuição do resíduo da soma do erro quadrado podem ser derivados.

O problema dessa abordagem é que se inserirmos combinações não-lineares das variáveis nenhum teste que supõe normalidade dos dados será livre de dúvida. Conclusões enganosas podem seguir os modelos de dados que passam nos testes de adequação (goodness of fit) e verificações residuais.

2.2 Cultura Algorithm Modeling

Em meados da década de 1980, dois novos algoritmos poderosos para modelagem de dados tornaram-se disponíveis: árvores de decisão e redes neurais. Praticantes dessa cultura começaram utilizar esse algoritmos em previsão de problemas complexos, onde modelos de dados não eram aplicáveis: reconhecimento de voz, reconhecimento de imagem, previsão não linear de séries temporais, reconhecimento de caligrafia e previsão em mercados financeiros.

A análise, nesta cultura, considera o interior da caixa como complexo e desconhecido. A natureza gera o dado, e isso é complexo e desconhecido. A abordagem deles é encontrar uma função f(x) - um algoritmo que opera em x para prever as respostas em y. Aqui, o objetivo não é encontrar uma função que descreve como os dados foram gerados, mas sim uma função que consiga generalizar um comportamento de um conjunto de testes.

Exemplificação da culturas de modelagem estatística. Dado um fenômeno natural, a cultura algorithm modeling não está interessada no entendimento do fenômeno gerador dos dados, para ela a natureza gera o dado, e isso é complexo e desconhecido.

2.3 A Cultura fit e predict

Com o objetivo de descontrair, chamo aqui a cultura fit e predict, essa abordagem é a que nos tempos atuais utilizamos diariamente, o famoso fit e predict.

Derivada da cultura anterior, vivemos procurando o algoritmo que possui melhor performance em um conjunto de dados específico, tal abordagem foi facilitada pelo avanço de frameworks de Machine Learning (scikit-learn, Tensorflow, PyTorch, para citar os mais comuns) que possuem uma série de algoritmos implementados, abordando problemas de classificação, regressão e agrupamento.

Até aqui, notamos o quão vantajoso é a Cultura de Modelagem Algoritmica, conseguimos rapidamente entender o comportamento de um conjunto de dados utilizando algoritmos que buscam a relação entre variáveis para explicar um comportamento resultante, nossa variável alvo. Como etapa de validação, analisamos a performance em um conjunto de testes desconhecido do utilizado na etapa de treinamento.

Com o avanço das técnicas de explicabilidade (Como interpretar modelos de machine learning complexos?), essa abordagem de treinamento tornou-se amplamente difundida. A pergunta agora é: “Qual o custo disso tudo?”

Custos:

Até agora, não respondemos nossa pergunta inicial: como construir modelos preditivos onde não temos amostras rotuladas?

Tratamos como a comunidade de Data Science vem favorecendo a divulgação de que modelos / algoritmos são a coisa mais importante para ter sucesso na resolução de problemas preditivos. Você já notou quantos cursos espalhados no mercado que ensinam você a utilizar algoritmos, mas não te ensinam a transformar um problema real, não rotulado, em algo que seja utilizável por uma empresa para resolver problemas reais?

A primeira lição: “Você frequentemente terá bons resultados com bons dados e um algoritmo simples, mas raramente terá bons resultados com o melhor algoritmo e um conjunto de dados ruim”.

A abordagem discutida possui os seguintes custos principais:

  1. Treinamento: uma boa performance vem atrelada de algoritmos que supõe maior tempo de convergência, maior quantidade de hiperparâmetros e processos interativos que nos dão boas acurácias preditivas, mas nos custam processamento e tempo;
  2. Dados precisam estar rotulados. De acordo com Robert Monarch (Human-In-The-Loop Machine Learning: Active Learning and Annotation for Human-Centered AI), nos tempos de hoje, provavelmente 90% dos modelos em produção são supervisionados. Para que nossos modelos funcionem bem precisamos de dados que sejam rotulados de forma correta.

3. Humanos no circuito

The Human Hamster Wheel that powers the tracks and balls

Toda a introdução anterior é para contextualizarmos o seguinte problema:

“Como humanos e máquinas trabalharão juntos para resolver problemas onde não temos dados rotulados, ou ainda não confiáveis, construindo uma tecnologia que efetivamente coopere e colabore com pessoas, aumentando suas habilidades?”

A DAMA conta com algoritmo de IA, em sistema de aprendizado contínuo, no qual todas as descobertas podem ser aprovadas, rejeitadas ou corrigidas pelo profissional de saúde, o que faz com que o algoritmo seja retroalimentado para ficar mais inteligente.

Talvez essa seja a primeira vez que você tenha se perguntado sobre essa abordagem, visto que muitas das aplicações produtizadas atualmente seguem um fluxo de modelos supervisionados, onde os dados já estão rotulados para utilização de um algoritmo estado da arte, sendo que a acurácia seja alta suficiente para previsão das classes em análise. Podemos seguir o mesmo pensamento para modelos regresivos.

Ao contrário dos robôs da Boston Dynamics, muitos do problemas chamados de Inteligência Artificial são incapazes de aprender por si só (você não vai encontrar uma IA de crédito capaz de autorregular a inadimplência dos clientes em uma companhia de empréstimos, ainda não…). Muito do que chamamos AI são aplicações movidas por modelos de Machine Learning supervisionados, por exemplo, sua assistente virtual (Alexa, Ok Google, Siri, …) sabe que “aumentar o volume” realmente significa para você e isso foi graças a milhares de horas humanas dizendo quais comandos pertenceriam àquela determinada ação.

E já falando em treinamento com reforço humano, nossos modelos de Machine Learning estão aprendendo mais dos dados do que regras previamente definidas. Esses dados (antigos e novos) são fruto do trabalho de humanos categorizando as amostras para que possam ser utilizadas como dados de treinamento.

Pelo que falamos acima, modelos por si só são incapazes de aprender - seja por dados rotularizados ou pelo constante feedback em métricas do modelo - forçando um novo retreino periódico para novos conjuntos de dados. Surge, então, um campo misto, talvez o mais importante para os próximos anos, conhecido como: aprendizagem de máquina com humanos no circuito (human-in-the-loop machine learning).

Aprendizagem de máquina com humanos no circuito pode ser definida como um conjunto de estratégias que combinam interação humana e modelos de Machine Learning.

O objetivo é bem simples:

  • Combinar inteligência humana e computacional para alcance de modelos mais acurados;
  • Diminuir o tempo de convergência na etapa de treinamento;
  • Auxiliar tarefas humanas com aprendizado de máquina para aumentar a eficiência.

Para auxiliar na visualização, poderíamos ter algo de acordo com a figura abaixo:

Fonte (Adaptação): Human-In-The-Loop Machine Learning: Active Learning and Annotation for Human-Centered AI. Diagramação de um processo de aprendizado ativo. Dados sem rótulos são selecionados de acordo com a importância deles para revisão humana (anotação). Essa etapa garante que novos modelos treinados sejam mais assertivos dado a contribuição do anotador para o problema proposto.

Para entendermos como a contribuição humana, na etapa de treinamento, entrega modelos mais acurados e específicos para determinado conjunto de dados, vamos introduzir dois temas muito importantes para esse contexto, Aprendizagem Ativa e Rotulação (annotation).

4. Aprendizagem Ativa

Aprendizagem Ativa é o processo de decidir quais dados amostrados deverão ser apresentados para anotação humana.

O processo de rotulação é algo altamente custoso e para isso devemos ter alguma estratégia que minimize esse custo. Antes de explicarmos as principais estratégias de aprendizado ativo, vale lembrar que nenhum algoritmo, arquitetura ou estratégia de aprendizado ativo é mais precisa que outra, cada caso pode ser mais preciso para determinado uso e conjunto de dados.

Vamos lá! Feito nossa retratação acima, apresentaremos duas estratégias de Aprendizado Ativo, com alguns exemplos utilizando a biblioteca modAL — Modular Active Learning framework for Python3.

Amostragem por incerteza é um conjunto de estratégias para identificar dados não rotulados que estão próximo de uma fronteira de decisão que separa duas classes em um modelo preditivo. Para uma classificação binária, por exemplo, são o conjunto de amostras que estão próximos a uma probabilidade de 50% entre duas classes, logo o modelo preditivo treinado não terá confiança sobre a predição, se tornará um modelo confuso em determinado grupo de amostras (imagine que não temos certeza sobre como considerar determinada sentença como positiva ou negativa. “Que ótima ligação, passei 20 minutos em espera…”). Essas amostras têm muito mais chance de serem classificadas erroneamente e daí precisamos corrigi-las, dando uma opinião humana e iniciamos a etapa de retreino.

Amostragem por diversidade contempla um conjunto de estratégias para identificar dados não rotulados que são sub-representados ou desconhecidos para um modelo preditivo em seu estado atual. Essas são amostras que possuem variáveis que são raras no conjunto de treinamento, como por exemplo, o problema de termos modelos faciais que são pouco representados por uma classe de cor negra, ou ainda regiões demográficas não tão comuns. Sacou a diferença? Dados que estão sub-representados, na maioria das vezes, encontram-se distantes da fronteira de decisão, o que oferece ao modelo preditivo uma visão mais geral sobre todo o espaço de amostras.

Em ambos processos de amostragem temos um conjunto de estratégias que podem ser utilizadas, nosso intuito não é abordá-las nesse texto - virá em um texto posterior sobre Estratégias de Amostragem para Aprendizado Ativo. Seu texto base é Active Learning Literature Survey de Burr Settles. Já adianto que coisas como Stream-based active learning podem ser utilizadas para identificação de mudança de comportamento temporal dos dados, ou ainda, detecção de outliers.

4.1 Amostragem ativa baseada na reserva de exemplos

Utilizaremos um framework em Python para aprendizado ativo, modAL (Modular Active Learning framework para Python3). Nossa intenção é, rapidamente, construir uma estratégia de treinamento e para isso vamos utilizar a famoso conjunto de dados Iris, que contém três classes: Iris-Setosa, Iris-Versicolour e Iris-Virginica.

Para exemplificarmos a utilização do aprendizado ativo para treinamento de modelos preditivos, vamos usar o cenário de amostragem ativa baseada na reserva de exemplos (Pool-Based Sampling). A estratégia segue os seguintes passos:

1. Tomemos um conjunto de dados 𝔇 que contém n amostras devidamente rotuladas2. Separamos em dois conjuntos, o conjunto 𝔏 que contém uma porcentagem pequenas do dados rotulados e o conjunto 𝔘 contendo a maior porcentagem de dados, de forma que |𝔏|≪|𝔘|.3. Iniciamos o treinamento utilizando um estimador f e o conjunto 𝔏4. Consultamos amostras seletivamente retiradas do conjunto 𝔘 e retreinamos o modelo com aquela nova amostra, atualizando assim a fronteira de decisão do estimador utilizado.

Na prática, nos apropriamos do conjunto de dados Iris e escolhendo um estimador - no nosso caso um K-vizinho mais próximos (KNN classifier) - treinaremos em um subconjunto de amostras e atualizamos interativamente a fronteira de decisão para encontrarmos um modelo preditivo mais performático para esse conjunto de dados.

import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
from matplotlib.colors import ListedColormap
import seaborn as sns

from sklearn.datasets import load_iris
from sklearn.decomposition import PCA
RANDOM_STATE_SEED = 123
np.random.seed(RANDOM_STATE_SEED)

iris = load_iris()
X_raw = iris.data
# X_raw = iris.data[:, :2] # we only take the first two features if
y_raw = iris['target']


# Define our PCA transformer and fit it onto our raw dataset.
pca = PCA(n_components=2, random_state=RANDOM_STATE_SEED)
transformed_iris = pca.fit_transform(X=X_raw)

x_component, y_component = transformed_iris[:, 0], transformed_iris[:, 1]

# Plot our dimensionality-reduced (via PCA) dataset.
plt.figure(figsize=(8.5, 6), dpi=130)
plt.scatter(x=x_component, y=y_component, c=y_raw, cmap='viridis', s=50, alpha=8/10)
plt.title('Análise de componentes principais para as classes contidas no Iris')
plt.show()
png
Decomposição em componentes principais das variáveis que caracterizam as classes das flores Iris.

Agora, dividimos nosso conjunto de dados de treinamento em 𝔏 e 𝔘. Para o conjunto 𝔏 selecionamos aleatoriamente três amostras para etapa zero do treinamento.

O restante dos dados, 𝔘, serão guardados como dados “não rotulados” (aqui entre aspas, pois temos esse rótulo bem definido e usaremos ele para avaliação de performance) e nos ajudaram na etapa das consultas interativas.

# Isolate our examples for our labeled dataset.
n_labeled_examples = X_raw.shape[0]
training_indices = np.random.randint(
low=0,
high=n_labeled_examples + 1,
size=3
)

X_train = X_raw[training_indices]
y_train = y_raw[training_indices]

# Isolate the non-training examples we'll be querying.
X_pool = np.delete(X_raw, training_indices, axis=0)
y_pool = np.delete(y_raw, training_indices, axis=0)

print(training_indices)
[109 126 66]

Usaremos nosso classificador com as amostras iniciais e calcularemos a acurácia do modelo treinado.

from sklearn.neighbors import KNeighborsClassifier
from modAL.models import ActiveLearner

# Specify our core estimator along with it's active learning model.
knn = KNeighborsClassifier(n_neighbors=3)
learner = ActiveLearner(estimator=knn, X_training=X_train, y_training=y_train)

Para avaliarmos o nosso classificador treinado, calculamos a acurácia do conjunto de treinamento inicial. Logo de cara, percebemos que o modelo tem baixa capacidade.

# Isolate the data we'll need for plotting.
predictions = learner.predict(X_raw)
is_correct = (predictions == y_raw)
# Record our learner's score on the raw data.
unqueried_score = learner.score(X_raw, y_raw)

# Plot our classification results.
fig, ax = plt.subplots(figsize=(8.5, 6), dpi=130)
ax.scatter(
x=x_component[is_correct],
y=y_component[is_correct],
c='g', marker='+',
label='Correct',
alpha=8/10
)
ax.scatter(
x=x_component[~is_correct],
y=y_component[~is_correct],
c='r', marker='x',
label='Incorrect',
alpha=8/10
)
ax.legend(loc='lower right')
ax.set_title("Predição das classes via estimador ActiveLearner (Acurácia: {score:.3f})".format(score=unqueried_score))
plt.show()
png
Avaliação do modelo treinado com apenas três amostras. Transformamos esse problema de multiclasses em uma classificação binária para critérios de avaliação.

Como podemos ver, nosso modelo não é capaz de aprender adequadamente a distribuição dos dados iniciais. Para critério de comparação, utilizaremos o estimador avaliando apenas a terceira classe contra as demais para cálculo da acurácia.

O pulo do gato, para ativamente darmos uma opinião para o estimador utilizado, se dá quando selecionamos de forma interativa uma amostra do conjunto 𝔘, utilizando a estratégia de incerteza, ou seja, teremos uma amostra cuja fronteira de decisão acredita que se a classificarmos, teremos maior chance de melhorar o acurácia do modelo preditivo.

A estratégia abaixo realiza 20 consultas ao observador, que responde com o verdadeiro rótulo da amostra. Os métodos utilizados são o query que retorna a consulta ao observador, e o método teach que utiliza os rótulos verdadeiros para melhora do treinamento. Observe que a quantidade de consultas é um passo que pode levar a minimização do tempo de treinamento.

Utilizada a amostra selecionada, a mesma é removida do conjunto 𝔘 e inteiramos até encontrar um ponto de convergência para a métrica que estamos avaliando.

N_QUERIES = 20
performance_history = [unqueried_score]

# Allow our model to query our unlabeled dataset for the most
# informative points according to our query strategy (uncertainty sampling).
for index in range(N_QUERIES):
query_index, query_instance = learner.query(X_pool)

# Teach our ActiveLearner model the record it has requested.
X = X_pool[query_index].reshape(1, -1)
y = y_pool[query_index].reshape(1, )

learner.teach(X=X, y=y)

# Remove the queried instance from the unlabeled pool.
X_pool = np.delete(X_pool, query_index, axis=0)
y_pool = np.delete(y_pool, query_index)

# Calculate and report our model's accuracy.
model_accuracy = learner.score(X_raw, y_raw)
print('Acurácia depois da consulta {n}: {acc:0.4f}'.format(n=index + 1, acc=model_accuracy))

# Save our model's performance for plotting.
performance_history.append(model_accuracy)
Acurácia depois da consulta 1: 0.6667
Acurácia depois da consulta 2: 0.6667
Acurácia depois da consulta 3: 0.8800
Acurácia depois da consulta 4: 0.8800
Acurácia depois da consulta 5: 0.8733
Acurácia depois da consulta 6: 0.8400
Acurácia depois da consulta 7: 0.7400
Acurácia depois da consulta 8: 0.7267
Acurácia depois da consulta 9: 0.7267
Acurácia depois da consulta 10: 0.7267
Acurácia depois da consulta 11: 0.7267
Acurácia depois da consulta 12: 0.7267
Acurácia depois da consulta 13: 0.7267
Acurácia depois da consulta 14: 0.7267
Acurácia depois da consulta 15: 0.7200
Acurácia depois da consulta 16: 0.8400
Acurácia depois da consulta 17: 0.8800
Acurácia depois da consulta 18: 0.8933
Acurácia depois da consulta 19: 0.9267
Acurácia depois da consulta 20: 0.9267

Aqui, primeiramente, exemplificamos a quantidade de interações por consulta em relação à precisão do modelo. Também exemplificamos as previsões corretas para o modelo em seu estado final no conjunto de dados completo.

# Plot our performance over time.
fig, ax = plt.subplots(figsize=(8.5, 6), dpi=130)

ax.plot(performance_history)
ax.scatter(range(len(performance_history)), performance_history, s=13)

ax.xaxis.set_major_locator(mpl.ticker.MaxNLocator(nbins=5, integer=True))
ax.yaxis.set_major_locator(mpl.ticker.MaxNLocator(nbins=10))
ax.yaxis.set_major_formatter(mpl.ticker.PercentFormatter(xmax=1))

ax.set_ylim(bottom=0, top=1)
ax.grid(True)

ax.set_title('Incremental classification accuracy')
ax.set_xlabel('Query iteration')
ax.set_ylabel('Classification Accuracy')

plt.show()
png
Avaliação de performance com a adição de dados selecionados de acordo com a incerteza do modelo sobre aquela amostra. Ao adicionarmos esses dados com o rótulo correto, a fronteira de decisão é ajustada mais rapidamente pois a amostra é mais significativa para o problema. Para o modelo, se selecionarmos as amostras mais significantes podemos alcançar uma maior acurácia no conjunto de testes de forma mais eficiente.
# Isolate the data we'll need for plotting.
predictions = learner.predict(X_raw)
is_correct = (predictions == y_raw)

# Plot our updated classification results once we've trained our learner.
fig, ax = plt.subplots(figsize=(8.5, 6), dpi=130)

ax.scatter(x=x_component[is_correct], y=y_component[is_correct], c='g', marker='+', label='Correct', alpha=8/10)
ax.scatter(x=x_component[~is_correct], y=y_component[~is_correct], c='r', marker='x', label='Incorrect', alpha=8/10)

ax.set_title('Classification accuracy after {n} queries: {final_acc:.3f}'.format(n=N_QUERIES, final_acc=performance_history[-1]))
ax.legend(loc='lower right')

plt.show()
png
Avaliação da acurácia do modelo após 20 consultas onde foram apresentadas amostras com maior grau de incerteza. Apenas um subgrupo de amostras foram classificadas erroneamente.

Como resultado, temos um modelo que, treinado interativamente, consegue, com ajuda de um conjunto resposta (um humano que seja consultado, ou, no caso do nosso exemplo, o conjunto resposta já performado), alterar a fronteira de decisão utilizando as amostras mais confusas para convergir rapidamente, afim de garantir um modelo mais assertivo.

Abaixo, utilizamos uma fronteira de decisão 2D e, para isso, retreinamos o modelo acima com apenas duas variáveis explicativas para conseguirmos demonstrar o caráter do ajuste - quando adicionamos ao modelo amostras significativas escolhidas de forma a minimizar a incerteza do modelo.

Conclusão

Por maior que seja a hype ou quão bonito seja o céu de hiperparâmetros de uma rede neural, nada se compara a ter dados consistentes.

Apresentamos aqui uma das abordagens de Aprendizado Ativo e como ela pode ser utilizada para, rapidamente, construir modelos acurados com uma quantidade baixa de amostras. O objetivo é representar problemas reais onde dados rotulados são de difícil obtenção ou estão incorretamente rotulados. Nessa última situação, construir modelos com poucas amostras podem ajudar a classificar melhor o nosso conjunto de dados em um processo de aprendizagem constante.

Pensando em um produto, poderíamos construir um sistema que envia formulários diariamente para um conjunto de anotadores, essas pessoas teriam que enviar o seu parecer sobre a informação apresentada. Assim, coletaríamos uma quantidade significante de dados concordantes, para construção de modelos futuros.

Por hoje é só, pessoal! Espero que este texto te ajude a descobrir novas formas de construção de modelos preditivos. Se você quiser falar comigo, escreva um comentário neste post ou entre em contato no LinkedIn.

Para conhecer um pouco mais de Data Science, Machine Learning e como aplicamos isso ao dia a dia da Datarisk, não deixe de conferir nossos outros posts.

Referências

  1. Seleção e controle do viés de aprendizado ativo por Davi Pereira dos Santos
  2. Active Learning Literature Survey de Burr Settles
  3. Pool-based sampling documentação do pacote modAL
  4. Human-in-the-Loop Machine Learning. Active learning and annotation for human-centered AI por Robert (Munro) Monarch
  5. modAL — Modular Active Learning framework for Python3
  6. Statistical Modeling: The Two Cultures por Leo Breiman
  7. Human–computer interaction

--

--