Neste notebook, apresento o uso de Machine Learning com Regressão Logística e Processamento de Linguagem Natural (PLN)
, o nosso objetivo é a previsão de tags com base em perguntas, com um conjunto de dados coletados do site Stack Overflow.
Processamento de Linguagem Natural (PLN) ou Natural Language Preprocessing (NLP na sigla inglesa) é a interação do homem com a máquina através da linguagem natural. A linguagem natural é a forma mais comum de nos comunicarmos, que estamos acostumados desde o nascimento. O computador por outro lado, utiliza um linguagem de Programação, seja python, java, entre outros para processarem informações.
O PLN carrega uma série de desafios em função da linguagem humana (natural), pois carrega uma séries de regras e ambiguidades, sem falar do idioma, que também contribui.
Dessa forma, o principal desafio é conseguir desenvolver sistemas digitais capazes de entender a linguagem humana. Mas isso não vem de hoje e estamos constantementes em contato com essa tecnologia, como por exemplos: Mecanismos de busca, Assistentes virtuais, Análise de sentimento e chatbots.
É uma subárea da ciência da computação, inteligência artificial e da linguística que estuda os problemas da geração e compreensão automática de línguas humanas naturais. Está em constante ascensão e é uma das áreas mais promissoras em inteligência artificial, pois o interesse dessa interação com a máquina está cada vez maior e paralelamente ao big data.
É um site de perguntas e respostas para profissionais e entusiastas na área de programação de computadores. Foi criado em 2008 por Jeff Atwood e Joel Spolsky como alternativa para os sites mais antigos de perguntas e respostas.
O site aceita somente perguntas sobre programação que são fortemente relacionadas com um problema específico, e tanto as perguntas quanto as respostas feitas pelos usuários podem ganhar pontos positivos e negativos, o que recompensa ou pune seus donos com pontos de reputação.
A proposta deste material é utilizar os dados do site Stack Overflow. Esse exercício faz parte de um dos cursos do site Coursera de Processamento de Linguagem Natural, foi utilizado um conjunto de dados com duas colunas, uma com as perguntas de usuários e a outra, que é nossa variável alvo, são as respectivas tags.
O Problema de Negócio que queremos resolver é realizar predições das respectivas tags a partir das perguntas, através do aprendizado de máquina com algoritmo supervisionado
.
Para nosso estudo de caso, vamos fazer uso das seguintes bibliotecas:
# importando as bibliotecas
import nltk
from nltk.corpus import stopwords
import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
from ast import literal_eval
import re
from collections import Counter
from itertools import chain
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.preprocessing import MultiLabelBinarizer
from sklearn.multiclass import OneVsRestClassifier
from sklearn.linear_model import LogisticRegression, RidgeClassifier
from sklearn.metrics import accuracy_score
from sklearn.metrics import f1_score
from sklearn.metrics import roc_auc_score
from sklearn.metrics import average_precision_score
from sklearn.metrics import recall_score
from metrics import roc_auc
nltk.download('stopwords') # caso a lista esteja desatualizada
%matplotlib inline
Neste projeto, contaremos com 2 conjuntos de dados: treino e validação, no qual é composto por títulos
e suas tags
correspondentes (somente essas duas colunas).
Vamos iniciar criando um função para importação dos dados. Nessa importação as tags são importadas entre aspas simples, então utilizaremos a função literal_eval
no qual converte a string em um objeto, para suprimirmos as aspas.
# criando a função
def read_data(filename):
# importando o arquivo
data = pd.read_csv(filename, sep='\t')
# removendo as aspas que são importadas juntas
data['tags'] = data['tags'].apply(literal_eval)
# retornando o arquivo
return data
Agora vamos importar os dois arquivos.
# importando os dados de treino
train = read_data('data/train.tsv')
# importando os dados de validação
validation = read_data('data/validation.tsv')
Vamos dar uma olhada nas primeiras linhas de cada conjunto com o método head()
.
# visualizando as primeiras 5 linhas do conjunto de treino
train.head()
# visualizando as primeiras 5 linhas do conjunto de validação
validation.head()
Podemos notar que temos aspas, dois pontos, traços na coluna title e a coluna tags não tem uma quantidade fixa de palavras, além de cada termo está em formato de lista.
Vamos agora dar uma olhada na dimensão desses dados.
# visualizando as dimensões dos dados de treino
print(f"Os dados de TREINO possui:")
print(f"{train.shape[0]} linhas")
print(f"{train.shape[1]} colunas\n")
# visualizando as dimensões dos dados de treino
print(f"Os dados de VALIDAÇÃO possui:")
print(f"{validation.shape[0]} linhas")
print(f"{validation.shape[1]} colunas")
Observamos que o conjunto de validação corresponde à 30% do conjunto de treino, um número bom para utilizarmos como validação do nosso modelo.
Vamos então, separar nossa variável preditora
(coluna title) da nossa variável alvo
(coluna tags), em ambos os conjuntos, utilizaremos o método values
.
# separando os dados de treino
X_train, y_train = train['title'].values, train['tags'].values
# separando os dados de validação
X_val, y_val = validation['title'].values, validation['tags'].values
Os arquivos viraram uma lista, no qual cada linha se tornou um elemento dessa lista, vamos dar uma olhada para entendermos melhor no X_train, as outras seguem da mesma forma.
# visualizando X_train
X_train
Em geral, quando trabalhamos com Processamento de Linguagem Natural, os dados são desestruturados, como podemos notar também nesse documento. Devemos analisar com cautela e escolher a melhor forma que não irá descaracterizar o sentido ou a palavra do que estamos tratando.
Vamos criar uma função para tratar nossos dados, removendo símbolos e caracteres especiais e também as stopwords, que são palavras que aparecem muitas vezes mas que não agregam valor ao nosso objetivo e se mantivermos podem causar ruidos e não termos uma boa precisão nas previsões.
Nesta etapa vamos começar a "limpar" nosso texto, removendo tudo que não tem valor para nosso objetivo.
Vamos iniciar criando uma função para isso, fazendo uso do pacote re
, Regular Expression Operations e com ela podemos utilizar dos recursos do REGEX.
Faremos uso do método compile (re.compile
), dentro de um objeto, para compilar a expressão regular que queremos, com isso quando a função encontrar essa expressão no texto, ele a removerá. Podemos utilizar para remover caracteres especiais, números, entre outros padrões existentes. Depois usaremos esse objeto como uma função com o método sub
, passando como parâmetros o que irá substituir e o conjunto de dados.
# criando os objetos com as expressões regulares
remove_espec_carac = re.compile('[/(){}\[\]\|@,;]')
remove_symb = re.compile('[^0-9a-z #+_]')
# criando um objeto para remoção de stopwords no idioma ingles
stopwords = stopwords.words('english')
# criando a função
def text_prepare(text):
# normalizando nosso texto em letras minúsculas, assim facilita nossa preparação
text = text.lower()
# substituindo caracteres especiais por espaços em branco
text = remove_espec_carac.sub(' ', text)
# retornando apenas letras e números
text = remove_symb.sub('', text)
# removendo as stopwords
text = ' '.join(word for word in text.split() if word not in stopwords)
# retornando o texto modificado
return text
Para verificarmos se nossa função de preparação está funcionando, vamos criar um pequeno texto com as informações que queremos remover e analisar.
# Criando a função
def test_text_prepare():
# definindo os exemplos
examples = ["SQL Server - any equivalent of Excel's CHOOSE function?",
"How to free c++ memory vector<int> * arr?"]
# inserindo a resposta correta
answers = ["sql server equivalent excels choose function",
"free c++ memory vectorint arr"]
# Aplicando um loop e comparando a limpeza
for ex, ans in zip(examples, answers):
# condições para retornar se o resultado foi aprovado ou reprovado
if text_prepare(ex) != ans:
return "Resposta incorreta para: '%s'" % ex
return 'Testes básicos aprovados.'
# rodando a função de teste
print(test_text_prepare())
Nossa função passou no teste! Agora podemos pré-processar os titles usando a função text_prepare para os conjuntos de dados de treino e validação.
# aplicando a função nos dados de treino
X_train = [text_prepare(x) for x in X_train]
# aplicando a função nos dados de validação
X_val = [text_prepare(x) for x in X_val]
Vamos dar uma olhada nas primeiras linhas, mas que na verdade são elementos porque estão contidos em uma lista e não mais como um dataframe.
# visualizando os 10 primeiros elementos
X_train[:10]
Seguindo aqui com nosso texto "limpo", podemos calcular as quantidades de ocorrência de cada palavra nos conjuntos de treino. Vamos utilizar a função Counter
do pacote Collections
para fazer contagem e a função chain
do pacote itertools
que nos fornece um loop que irá iterar as palavras com suas respectivas frequencias.
# realizando a contagem e inserindo em um dicionário
words_counts = Counter(chain.from_iterable([i.split(" ") for i in X_train]))
# ordenando do maior para o menor
words_freq = sorted(words_counts.items(), key=lambda x: x[1], reverse=True)
Vamos dar uma olhada no TOP10, ou seja, as 10 palavras que mais apareceram.
words_freq[:10]
Vamos visualizar em forma de gráfico, pois ficará mais visível e intuitivo, utilizando um barplot
para visualizar.
Mas teremos que mudar o formato antes, pois como o resultado é uma tupla, vamos converter em um dicionário, separar e deixar as palavras em uma lista, assim como as ocorrências, separar e colocar também em outra lista.
# colocando as palavras em um objeto do tipo lista
words_list = list(dict(words_freq[:10]).keys())
# colocando as ocorrências em um objeto do tipo lista
occur_list = list(dict(words_freq[:10]).values())
# definindo a área de plotagem
plt.figure(figsize=(10,6))
# criando o gráfico
ax = sns.barplot(x = words_list, y = occur_list)
# inserindo o título
ax.set_title('TOP10 palavras que mais ocorreram')
# rotacionando os rótulos do eixo x
plt.xticks(rotation=30);
A palavra using é a que mais aparece, acredito que seja em função da maioria das perguntas serem sobre "como funciona" algo, em seguida entram algumas linguagens de programação, assim como buscas por erros.
Da mesma forma vamos fazer todo esse processo de contagem para as tags.
# realizando a contagem e inserindo em um dicionário
tags_counts = Counter(chain.from_iterable([i for i in y_train]))
# ordenando do maior para o menor
tags_freq = sorted(tags_counts.items(), key=lambda x: x[1], reverse=True)
Vamos também dar uma olhada no TOP 10, as 10 tags que mais apareceram.
tags_freq[:10]
Visualizando graficamente as TOP 10 tags.
# colocando as palavras em um objeto do tipo lista
tags_list = list(dict(tags_freq[:10]).keys())
# colocando as ocorrências em um objeto do tipo lista
occur_list = list(dict(tags_freq[:10]).values())
# definindo a área de plotagem
plt.figure(figsize=(10,6))
# criando o gráfico
ax = sns.barplot(x = tags_list, y = occur_list)
# inserindo o título
ax.set_title('TOP10 tags que mais ocorreram')
# rotacionando os rótulos do eixo x
plt.xticks(rotation=30);
As tags que mais aparecem são linguagens de Programação, no qual são relacionadas com as perguntas, sobre o uso ou erros contidos nas perguntas, por exemplo.
Algoritmos de Machine Learning trabalham com dados numéricos e nós não podemos usar os textos da forma que conhecemos. Entretanto, devemos converter estes textos em vetores numéricos.
É denominado vetorização o processo geral de converter a coleção de textos em vetores numéricos. A estratégia de tokenizar (separar textos ou palavras em "blocos"), contar e normalizar (como já fizemos anteriormente) é chamado de representação Bag of Words (BOW), ou na tradução literal bolsa de palavras, no qual são descritos pelas ocorrências das palavras enquanto são ignoradas suas posições relativas em todos os textos. Segue exemplo abaixo de como fica a estrutura, com a frequencia absoluta de cada palavra:
Um uso comum da Vetorização é a partir do CountVectorizer
do sklearn
, no qual vamos demonstrar em seguida.
Vamos vetorizar nossos dados, limitando à 5000 palavras, que na matriz serão apresentadas como 5000 colunas e daí entramos em um outro conceito que é de esparsidade e quanto mais colunas, mais poder computacional precisamos.
Matriz esparsa ou Sparcity Matrix são matrizes com muitos zeros, podemos visualizar na figura acima (fig.1), os espaços em branco quando a palavra não é encontrada em determinado documento, pois todos os documentos usam um conjunto muito pequenos de palavras e isso resulta nesse tipo de matriz.
# definindo um limite
max_features = 5000
# instanciando o vetorizador
vectorizer = CountVectorizer(max_features = max_features)
# aplicando a transformação
X_train_bow = vectorizer.fit_transform(X_train)
X_val_bow = vectorizer.transform(X_val)
# checando o shape
print('X_train shape:', X_train_bow.shape)
print('X_val shape: ', X_val_bow.shape)
Podemos certificar, como foi dito, que a construção resultou em uma matriz com 5000 colunas, que nós limitamos, em ambos os conjuntos, treino e validação.
Continuando, uma abordagem que se estende à estrutura da Bag of Words é capturar as frequencias relativas das palavras, pois ajuda a penalizar as palavras muito frequentes e otimiza o uso das melhores.
Nos textos, podemos nos deparar com uma quantidade de palavras muito frequentes que acabam carregando informações não muito relevantes e se treinarmos um modelo diretamente dessa forma, essas palavras podem influenciar o modelo e as palavras mais interessantes, porém menos frequentes não apareceriam. Segue exemplo abaixo:
Então podemos fazer uso do TF-IDF que significa:
TF: term frequency ou termos frequentes
IDF: inverse document-frequency ou frequencia de documento inversa
Basicamente, a conta realizada é: os termos frequentes nos documentos, ou seja, o número de vezes que eles ocorrem em um dado documento, é multiplicado com a componente inversa da frequencia tf-idf(t,d) = tf(t,d) x idf(t)
.
Vamos primeiramente instanciar o modelo para treinar o vetorizador a partir de alguns parâmetros, em seguida aplicaremos o modelo instanciado, treinando e transformando com os dados de treino e para os dados de validação somente a transformação, não deverá treinar novamente com esses dados. Em seguida extrairemos o vocabulário e por fim inverteremos a ordem do vocabulário (pois vamos utilizar mais a frente nessa estrutura).
# instanciando o tfidf
tfidf_vec = TfidfVectorizer(token_pattern = '(\S+)', min_df = 5, max_df = .9, ngram_range = (1,2))
# aplicando o modelo com fit_transform aos dados de treino
X_train_tfidf = tfidf_vec.fit_transform(X_train)
# aplicando o modelo com transform aos dados de validação
X_val_tfidf = tfidf_vec.transform(X_val)
# coletando o palavras
tfidf_vocab = tfidf_vec.vocabulary_
# invertendo o dicionário para termos a quantidade como chaves
tfidf_reversed_vocab = {i:word for word,i in tfidf_vocab.items()}
Chegamos o momento de treinarmos nosso modelo para fazer previsões, após deixarmos nossos conjuntos nas formas necessárias.
Para modelar essa predição, vamos converter a nossa variável alvo em binário, ou seja, a previsão será 0 e 1 e para isso vamos utilizar o algoritmo MultiLabelBinarizer
do sklearn
.
O transformador desse classificador converte uma lista de conjuntos ou tuplas em uma matriz binária (amostras x classes) indicando a presença de um rótulo de classe.
# instanciando o modelo
mlb = MultiLabelBinarizer(classes = sorted(tags_counts.keys()))
# convertendo rótulos de treino com fit_transform
y_train = mlb.fit_transform(y_train)
# transformando dados de validação com fit_transform
y_val = mlb.fit_transform(y_val)
Vamos conferir o resultado e a forma resultante nos 3 primeiros elementos.
# checando os 3 primeiros elementos das tags de treino
y_train[:3]
# checando os 3 primeiros elementos das tags de validação
y_val[:3]
Podemos também checar as classes que foram binarizadas.
# olhando as classes
mlb.classes_
Após treinar o classificador, vamos utilizar também utilizar uma abordagem de um contra todos com OneVsRestClassifier
também do sklearn
.
Nesta abordagem o k classificadores (= número de tags) são treinados. Essa estratégia consiste em treinar um classificador por classe, para cada classificador, a classe é treinada contra todas as outras.
A vantagem disso é pela a interpretabilidade, cada classe é representada um e somente um classificador, sendo possível ganhar conhecimento sobre a classe pelo classificador correspondente. Esta é a estratégia mais comumente usada para classificação multiclasse.
Depois usaremos a Regressão Logística ou LogisticRegression
do sklearn
, embora um método mais simples, mas que frequentemente tem um boa performance em tarefas de classificação de textos.
Vamos criar um função para facilitar nosso treinamento, pois queremos fazer tanto para bag-of-words quanto para tf-idf e colocaremos alguns dos parâmetros mais importantes desse modelo, assim facilitará caso queiramos tentar melhora-lo posteriormente.
Agora vamos utilizar a Regressão Logística dentro do OneVsRestClassifier para treinar e depois validar o modelo.
# criando a função
def train_classifier(X_train, y_train, C=2.5, penalty='l2'):
# instanciando a regressão logística
logreg = LogisticRegression(C=C, penalty=penalty, max_iter=2000)
# intanciando o modelo de um contra todos com a regressão logística
one_vs_rest = OneVsRestClassifier(logreg).fit(X_train, y_train)
# retornando o instancia do modelo
return one_vs_rest
Vamos aplicar a função.
# treinando um classificador com bow
classifier_bow = train_classifier(X_train_bow, y_train)
# treinando um classificador com tf-idf
classifier_tfidf = train_classifier(X_train_tfidf, y_train)
Agora podemos criar as previsões e vamos utilizar agora o conjunto de dados de validação. Utilizaremos dois tipos de previsões: labels que são pelas classes e scores pela pontuação.
# realizando as previsões com labels
y_val_predicted_labels_bow = classifier_bow.predict(X_val_bow)
# realizando as previsões com scores
y_val_predicted_scores_bow = classifier_bow.decision_function(X_val_bow)
# realizando as previsões com labels
y_val_predicted_labels_tfidf = classifier_tfidf.predict(X_val_tfidf)
# realizando as previsões com scores
y_val_predicted_scores_tfidf = classifier_tfidf.decision_function(X_val_tfidf)
Agora veja como um classificador que usa o TF-IDF funciona, alguns exemplos:
y_val_pred_inversed = mlb.inverse_transform(y_val_predicted_labels_tfidf)
y_val_inversed = mlb.inverse_transform(y_val)
for i in range(5):
print('Title:\t{}\nTrue labels:\t{}\nPredicted labels:\t{}\n\n'.format(
X_val[i],
','.join(y_val_inversed[i]),
','.join(y_val_pred_inversed[i])
))
Para avaliação do modelo, usaremos algumas métricas de classificação:
Resumo dessa métricas:
Accuracy: é a mais simples das métricas e nos mostra de uma forma geral o quanto o modelo está performando, dividindo o número de acertos (positivos) pelo total dos dados de entrada.
F1-score: é a média harmônica entre precisão e recall.
- precisão: é a taxa de números positivos divididos pela soma do total de positivos e falso positivos. Ele descreve o quão bom o modelo é, em prever a classe positiva.
- recall: é calculada como a taxa de números de verdadeiros positivos divido pela soma dos verdadeiros positivos e falso negativos. Recall é o mesmo que sensibilidade.
ROC-curve: é obtido pela divisão dos positivos verdadeiros pelos positivos totais (RPV) versus a divisão dos falsos positivos pelo negativos totais (RPF), para vários valores do limiar de classificação. O RPV é também conhecido como sensibilidade (ou taxa de verdadeiros positivos), e RPF = 1-especificidade ou taxa de falsos positivos. A especificidade é conhecida como taxa de verdadeiros negativos (RVN).
Vamos implementar a função print_evaluation_scores para calcular e imprimir os resultados da:
Para as métricas de F1-score e precision vamos alternar o parâmetro average entre micro/macro/weighted.
# criando a função para avaliação
def print_evaluation_scores(y_val, predicted):
# calculando a acuracia
print("Accuracy score :",accuracy_score(y_true=y_val,y_pred=predicted))
# calculando f1 score
print("F1 averaged score :",np.mean(np.array(f1_score(y_true=y_val, y_pred=predicted, average = 'micro'))))
# calculando a precision score
print("Precision score :",average_precision_score(y_true=y_val, y_score=predicted, average = 'weighted'))
Os parâmetros acima, foram os que tiveram melhor resultado.
print('Bag-of-words')
print('---------------------------------------------')
print_evaluation_scores(y_val, y_val_predicted_labels_bow)
print('=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=')
print('\nTfidf')
print('---------------------------------------------')
print_evaluation_scores(y_val, y_val_predicted_labels_tfidf)
print('=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=')
Vamos também plotar a Curva ROC para casos de classificação multi-labels, que é o nosso caso. Os parâmetros de entrada são:
# número de classes
n_classes = len(tags_counts)
# plotanco a curva para bow
roc_auc(y_val, y_val_predicted_scores_bow, n_classes)
# número de classes
n_classes = len(tags_counts)
# plotanco a curva para tf-idf
roc_auc(y_val, y_val_predicted_scores_tfidf, n_classes)
O resultado da curva ROC, quanto mais próximo de 1, melhor. Ela nos mostra, de uma forma simplificada, o quanto nosso classificador é bom em acertar as diferentes classes.
Para finalizar, vamos dar uma olhada nas variáveis que mais influenciam nosso classificador (palavras ou n-grams) que são usados com pesos maiores com o classificador da Regressão Logística.
Vamos construir nossa função para exibir essas variáveis. Essa função será criada com os seguinte parâmetros:
def print_words_for_tag(classifier, tag, tags_classes, index_to_words, all_words):
print('Tag:\t{}'.format(tag))
# Extract an estimator from the classifier for the given tag.
# Extract feature coefficients from the estimator.
est = classifier.estimators_[tags_classes.index(tag)]
top_positive_words = [index_to_words[index] for index in est.coef_.argsort().tolist()[0][-5:]]# top-5 words sorted by the coefficiens.
top_negative_words = [index_to_words[index] for index in est.coef_.argsort().tolist()[0][:5]]# bottom-5 words sorted by the coefficients.
print('Top positive words:\t{}'.format(', '.join(top_positive_words)))
print('Top negative words:\t{}\n'.format(', '.join(top_negative_words)))
# all_words_text = list(words_counts.keys())
# print_words_for_tag(classifier_tfidf, 'c', mlb.classes, tfidf_reversed_vocab, all_words_text)
# print_words_for_tag(classifier_tfidf, 'c++', mlb.classes, tfidf_reversed_vocab, all_words_text)
# print_words_for_tag(classifier_tfidf, 'linux', mlb.classes, tfidf_reversed_vocab, all_words_text)
lista_palavras = ['c', 'c++', 'linux']
all_words_text = list(words_counts.keys())
for w in lista_palavras:
print_words_for_tag(classifier_tfidf, w, mlb.classes, tfidf_reversed_vocab, all_words_text)
Podemos observar as top mais, que influenciam tanto positivamente quanto negativamente as nossas classes. Podemos inserir as palavras na lista definida acima antes de rodar a função.
E agora só um exemplo de como podemos tunar a regressão logística de forma mais simplificada com a função que foi criada mais acima.
hypers = np.arange(0.1, 3.0, 0.5)
res = []
for h in hypers:
temp_model = train_classifier(X_train_tfidf, y_train, C=h)
temp_pred = f1_score(y_val, temp_model.predict(X_val_tfidf), average='weighted')
res.append(temp_pred)
plt.figure(figsize=(7,5))
plt.plot(hypers, res, color='blue', marker='o')
plt.grid(True)
plt.xlabel('Parameter $C$')
plt.ylabel('Weighted F1 score')
plt.show()
Neste artigo apresentamos um tutorial passando pelas diversas etapas de como preparar documentos ou textos e aplicar um modelo linear para classificação mult-labels, onde o mais que se encontra são classificações binárias com esse classificador, a proposta era trazer algo diferente relacionado também ao estudo de NLP, e podemos também utilizar outras formas para melhorar ainda mais o resultado do nosso modelo, como tunar o tf-idf e outros parâmetros da Regressão Logística ou até mesmo utilizar outro modelo.
Não foram abordados temas como conceitos de tokenização e n-grams que ficará para um próximo projeto, abaixo deixarei as referências utilizadas para realizar este.
Processamento de linguagem natural
https://bit.ly/31TQkwX
Tectudo: Stack Overflow – Conheça o melhor site de perguntas e respostas de programação
https://glo.bo/2PSfeHA
Multilabel Binarizer
https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.MultiLabelBinarizer.html
OneVsRestClassifier
http://scikit-learn.org/stable/modules/generated/sklearn.multiclass.OneVsRestClassifier.html)
LogisticRegression
http://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LogisticRegression.html
Count Vectorizer
https://scikit-learn.org/stable/modules/generated/sklearn.feature_extraction.text.CountVectorizer.html
Feature Extraction (Bag-of-word, tfidf)
https://scikit-learn.org/stable/modules/feature_extraction.html
ROC Curve
https://scikit-learn.org/stable/modules/generated/sklearn.metrics.roc_curve.html
NLP Tutorial: MultiLabel Classification Problem using Linear Models
https://gdcoder.com/nlp-tutorial-multilabel-claddification-problem-using-linear-models/