Importando bibliotecas necessárias:
import random
import numpy as np
import pandas as pd
import pickle
import copy
import re
import json
import time
import seaborn as sns
import matplotlib.pyplot as plt
import nltk
import nltk, re, pprint
from nltk import word_tokenize
from nltk.stem import *
from nltk.corpus import stopwords
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics import confusion_matrix
from sklearn.linear_model import LogisticRegression
from sklearn.feature_selection import SelectFromModel
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import MaxAbsScaler
import keras
from keras.layers import Input, Dense
from keras.models import Model
from keras.models import load_model
from keras import optimizers
from keras.regularizers import l1
from keras.regularizers import l2
nltk.download('stopwords')
nltk.download('punkt')
nltk.download('rslp')
Conectando ao Google Drive:
from google.colab import drive
drive.mount('/content/drive')
Definindo caminho dos arquivos (base de dados etc):
path = "/content/drive/My Drive/Workshop2/"
Lendo base de dados classificação de notícias (a base de dados pode ser baixada em https://medium.com/@robert.salgado/multiclass-text-classification-from-start-to-finish-f616a8642538):
#Carregando JSON
data = []
for line in open(path+'News Classification DataSet.json', 'r'):
data.append(json.loads(line))
content, label = [], []
for each in data:
content.append(each['content'])
label.append(each['annotation']['label'][0])
df = pd.DataFrame([content, label]).T
df.columns= ['content', 'label']
df.head()
Vamos ver o shape da base:
np.shape(df)
Checando frequências relativas de labels:
pd.crosstab(index=df['label'], columns="freq_rel", normalize=True)
Vamos transformar os 4 labels em 4 variáveis binárias:
df=pd.concat([df,pd.get_dummies(df['label'])], axis=1)
df.head()
Vamos transformar nossa base de dados em duas listas (uma para os textos e outra para a variável de interesse):
X=df['content'].to_list()
y=np.array(df[['Business', 'SciTech', 'Sports', 'World']])
Checando um texto da lista:
X[0]
Checando sua classe:
y[0]
Definindo uma função para a limpeza dos textos:
def clean(resulta):
result = copy.deepcopy(resulta)
result=result.lower()
result=result.replace(",", "")
result=re.sub(r"\@\w+", ' citation### ', result) #subtituindo @* por 'citation###'
result=re.sub(r"http\S+", ' weblink### ', result) #subtituindo urls por 'weblink###'
result=re.sub('\d', ' ### ', result) #subtituindo números por '###'
result=re.sub('([.,!?()])', r' \1 ', result) #colocando espaço entre pontuações de palavras
result=re.sub('\s{2,}', ' ', result)
result=result.replace("<br />","")
result=result.replace("\n", " ")
result=result.replace("/", "")
result=result.replace("|", "")
result=result.replace("+", "")
#result=result.replace(".", "") vamos MANTER
#result=result.replace(":", "") vamos MANTER
#result=result.replace(";", "") vamos MANTER
#result=result.replace("!", "") vamos MANTER
#result=result.replace("?", "") vamos MANTER
result=result.replace(">", "")
result=result.replace("=", "")
result=result.replace("§", "")
result=result.replace(" - ", " ")
result=result.replace(" _ ", " ")
result=result.replace("&", "")
result=result.replace("*", "")
#result=result.replace("(", "") vamos MANTER
#result=result.replace(")", "") vamos MANTER
result=result.replace("ª", "")
result=result.replace("º", "")
result=result.replace("%", "")
result=result.replace("[", "")
result=result.replace("]", "")
result=result.replace("{", "")
result=result.replace("}", "")
result=result.replace("'", "")
result=result.replace('"', "")
result=result.replace("“", "")
result=result.replace("”", "")
result=re.sub(' +', ' ', result)
return(result)
Limpando cada um dos textos:
for i in range(len(X)):
X[i]=clean(X[i])
Checando o mesmo texto de forma limpa:
X[0]
Abrindo conjunto de Stopwords do pacote NLTK:
stop_words = set(stopwords.words('english'))
Definindo função para a tokenização dos textos. A função já faz o processo de stemming e utiliza a base de stopwords para fazer a filtragem:
def tokenize(text):
stemmer = PorterStemmer() # para o port. >>> RSLPStemmer()
tokens = nltk.word_tokenize(text)
stems = []
for item in tokens:
if item not in stop_words:
stems.append(stemmer.stem(item))
return stems
Vamos dividir nossa base em bases de treino teste e validação:
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.20, random_state=42)
X_test, X_val, y_test, y_val = train_test_split(X_test, y_test, test_size=0.5, random_state=42)
np.shape(X_train)
Treinaremos o modelo TFIDF (https://en.wikipedia.org/wiki/Tf%E2%80%93idf) para a extração de atributos dos textos. Na implementação abaixo, você pode ver que escolhemos trabalhar com unigramas e bigramas e isso quer dizer que se um texto da nossa base de dados fosse "Eu como bolo" trabalharíamos com os seguintes termos: "Eu", "como", "bolo", "Eu como", "como bolo". A partir de uma análise da nossa base de treino, o algoritmo dá um escore chamado IDF (Inverse Document Frequency) a cada um dos termos, sendo que termos que aparecem em menos textos têm maior escore - esses escores dados aos termos são ponderadores de quão importantes os termos são. No processo de construção de atributos de cada um dos textos, para cada um dos termos presentes nos textos calculamos a frequência relativa dos termos dentro de cada um dos textos (Term Frequency) e multiplicamos pelo escores 'IDF' de cada um dos termos (aquele calculado anteriormente). Dessa maneira, para cada um dos termos dentro de um texto, teremos potenciais variáveis com valores diferentes de zero (se a palavra não aparece naquele texto, então é automaticamente nula). Como pode-se perceber, teremos um número gigantesco de variáveis e para controlá-lo vamos impor um teto (max_features) de k variáveis. O pacote automaticamente seleciona os k termos com maior frequência em todo o corpus da base de treino. Vamos treinar o modelo TFIDF na base de treino:
#Treinando bag of N-grams (TF-IDF) para a extração
n_feat=10000
tfidf = TfidfVectorizer(tokenizer=tokenize,ngram_range=(1,2), max_features=n_feat)
tfidf.fit(X_train)
#Salvando modelo
#pickle.dump(tfidf, open(path+"tfidf.sav", 'wb'))
Vamos transformar nossas bases de treino, validação e test, que estão em formato de textos, em dados estruturados:
#Aplicando a transformação aprendida
X_train = tfidf.transform(X_train)
X_val = tfidf.transform(X_val)
X_test = tfidf.transform(X_test)
Afim de aumentar o poder preditivo dos nossos modelos, vamos normalizar (para tudo ficar entre 0 e 1) nossos atributos em nossas bases de dados:
scaler = MaxAbsScaler()
scaler.fit(X_train)
X_train = scaler.transform(X_train)
X_val = scaler.transform(X_val)
X_test = scaler.transform(X_test)
Checando formato de cada um dos arrays:
np.shape(X_train), np.shape(X_test), np.shape(X_val)
np.shape(y_train), np.shape(y_test), np.shape(y_val)
Para utilizar o modelo de regressão logística com o pacote Scikit-Learn, temos que transformar as nossas variáveis respostas em uma só da seguinte maneira:
y_train2=np.argmax(y_train,axis=1)
y_val2=np.argmax(y_val,axis=1)
y_test2=np.argmax(y_test,axis=1)
Em 'y_train2', 'y_test2' e 'y_val2' temos o seguinte encoding 'Business' -->0, 'SciTech' -->1, 'Sports' -->2 e 'World' -->3. Abaixo treinaremos um modelo de regressão logística multinomial, avaliando sua acurácia na base de validação:
model = LogisticRegression(multi_class='multinomial', solver='lbfgs', max_iter=100).fit(X_train, y_train2)
logregScore = model.score(X_val, y_val2)
print("Acurácia na base de validação=",logregScore)
Como temos um grande número de atributos, utilizaremos a penalidade do tipo 'L1' para fazer a seleção dos atributos mais importantes para a predição. Tendo que encontrar um bom parâmetro 'C' (hiperparameter tuning), que é o inverso do parâmetro de regularização, vamos aplicar o método de validação com grid-search, comparando diferentes valores de 'C', o número de atributos selecionados e a acurácia do modelo com os atributos selecionados na etapa anterior (ver https://towardsdatascience.com/l1-and-l2-regularization-methods-ce25e7fc831c):
start_time = time.time()
#
cs=[.01,.1,1,10,100]
#
summary=[]
for c in cs:
#seleção de atributos
logreg = LogisticRegression(multi_class='multinomial', solver='saga', penalty='l1',C=c, max_iter=100).fit(X_train, y_train2)
select_features = SelectFromModel(logreg, prefit=True)
X_train_sel=select_features.transform(X_train)
X_test_sel=select_features.transform(X_test)
X_val_sel=select_features.transform(X_val)
#fittando o modelo
model = LogisticRegression(multi_class='multinomial', solver='lbfgs', max_iter=100).fit(X_train_sel, y_train2)
#avaliando acurácia
logregScore = model.score(X_val_sel, y_val2)
#resumo da validação
summary.append((c,np.shape(X_train_sel)[1],logregScore))
print(round((time.time() - start_time)/60,2),"minutos \n")
Mesmo tendo alguns problemas de convergência, os resultados foram interessantes:
for i in summary:
print("C=%8.2f --- # Features=%5d --- Acurácia=%3.2f" % i)
Vamos escolher C=10 para fazer a seleção de nossos atributos daqui para frente. Fazendo a seleção e treinando modelo:
#Seleção
logreg = LogisticRegression(multi_class='multinomial', solver='saga', penalty='l1',C=10).fit(X_train, y_train2)
select_features = SelectFromModel(logreg, prefit=True)
X_train_sel=select_features.transform(X_train)
X_test_sel=select_features.transform(X_test)
X_val_sel=select_features.transform(X_val)
#Treinando
model = LogisticRegression(multi_class='multinomial', solver='lbfgs').fit(X_train_sel, y_train2)
Avaliando modelo na base de teste. Na matriz de confusão, nas linhas temos as classes previstas (0,1,2,3) enquanto nas colunas temos as verdadeiras classes (0,1,2,3) :
print("Acurácia na base de teste=%3.2f \n" % model.score(X_test_sel, y_test2))
#
y_pred = model.predict(X_test_sel)
print(confusion_matrix(y_pred,y_test2))