Esse dataset está disponível no repositório de machine learning da UCI.
Ele é composto por 5812 artigos coletados desde a abertura da primeira conferência e um conjunto de 11463 palavras que compõem os artigos. Esses dados estão dispostos em uma matriz 11463 x 5812, então o vocabulário se encontra nas linhas e os artigos nas colunas.
Os demais dados é a contagem de quantas vezes cada palavra aparece em cada artigo, sendo assim, temos um Bag Of Words dos artigos. Porém a disposição da matriz não está condizente com o que estamos acostumados a trabalhar, sendo que as features se encontram nas linhas. Para tanto, é necessário criar a transposta dessa matriz, para que se torne mais familiar e possamos utiliza-la em nossos métodos.
Após feito a transposta do dataset, ele toma a forma de uma matriz esparsa com a contagem da frequência de cada palavra, como a abaixo.
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from time import time
%matplotlib notebook
dataset = pd.read_csv('NIPS_1987-2015_transpose.csv')
dataset.head(5)
Agora que temos nossa matriz em formado familiar, podemos estuda-la.
O primeiro passo que pode ser tomado é a visualização do histograma da matriz por completo. Para realizar essa tarefa, foi feita a contagem das frequências máximas de cada token, dessa forma podemos agrupar no histograma tokens com frequências iguais e visualizar se realmente todas as 11464 palavras são úteis para nós.
Então, nós primeiro:
matplotlib
com 40 "baldes".max_feature_counts = dataset.max()
max_feature_counts = max_feature_counts[1:] # Retirando a coluna Xyear_ID
n, bins, patches = plt.hist(max_feature_counts, 40, log=True, ec='black', alpha=0.75, align='mid')
plt.title('Histograma BoW - Repetições', fontsize=20)
plt.xlabel('Maximo encontrado', fontsize=12)
plt.xticks(bins, rotation=90)
plt.ylabel('Frequência', fontsize=12)
ax = plt.gca()
ax.grid(axis='y', linestyle='--', linewidth=1)
plt.show()
print(n, bins)
Podemos visualizar no histograma que cerca de 7782 tokens tem uma frequência máxima de até 24 vezes e outros 4 tokens tem a frequência máxima acima de 164 vezes. Então podemos afirmar, apenas com base nesse histograma, que 7782 tokens podem ser retirados, já que o máximo de vezes que aparecem em um único artigo não é relevante.
A mesma afirmação não é verdadeira para o restante do vocabulário, já que uma alta frequência máxima não nos diz se esse token está presente em apenas poucos artigos ou não. O que nos leva para nossa próxima análise.
Próximo passo é então estudar quais são os termos mais frequentes no nosso vocabulário, para então ter uma ideia de qual é a nossa amostra.
Para isso é feita a ordenação das frequências máximas já coletadas e estudado cada token individualmente na ordem das suas frequências máximas.
sorted_values = max_feature_counts.sort_values(ascending=False)
sorted_columns = sorted_values.axes[0]
print('{} - {}\t{} - {} - {}\t{} - {}'.format('token',
'freq.',
'mediana',
'media',
'Som. freq.',
'qnt. !=0',
'qnt. ==0'))
for index, s in enumerate(sorted_columns):
if index > 69:
break
count_raw = np.array(dataset.loc[:, s])
non_zeros = np.sort([t for t in count_raw if t != 0])
zeros = [t for t in count_raw if t == 0]
print('%s - %0.f\t%0.f - %0.f - %0.f\t%0.f - %0.f' % (s,
sorted_values[index],
np.median(non_zeros),
np.mean(non_zeros),
np.sum(non_zeros),
len(non_zeros),
len(zeros)))
Para visualizar melhor o dataset utilizamos de valores como:
Podemos notar que algumas peculiaridades com esses valores:
Com base nisso podemos retirar os tokens que se encaixam em tais peculiaridades, para tentar reduzir a dimensionalidade do nosso problema.
Logo após o estudo do dataset pela frequência dos tokens, podemos estudar como nosso vocabulário está construido. Faremos isso com o intuito de ver para quais tokens existem outros similares no mesmo vocabulário.
Essa analise é pertinente, já que podemos reduzir vários tokens a apenas um somando as suas frequências, e dessa forma reduzindo drasticamente nossa dimensionalidade.
Para gerar essa análise utilizaremos a biblioteca NLTK
e seu PorterStemmer
para verificar quais tokens tem a mesma raiz gramatical e agrupa-los.
import nltk
from nltk.stem.porter import PorterStemmer
columns = dataset.columns[1:]
stemmer = PorterStemmer()
steammed_columns = [stemmer.stem(word) for word in columns]
repeated_tokens = []
for token in sorted(set(steammed_columns)):
indexes = [i for i, x in enumerate(steammed_columns) if x == token]
if len(indexes) > 1:
repeated_tokens.append((token, indexes))
repeated_tokens[:10]
Com essa análise podemos verificar que existem diversos tokens repetidos em nossa base, onde um token , pode chegar a ter mais de 20 outros similares. ('gener' 21)
import nltk
from nltk.stem.porter import PorterStemmer
def extract_repeated_tokens(tokens):
stemmer = PorterStemmer()
steammed_columns = [stemmer.stem(word) for word in tokens]
repeated_tokens = []
for token in sorted(set(steammed_columns)):
indexes = [i for i, x in enumerate(steammed_columns) if x == token]
if len(indexes) > 1:
repeated_tokens.append((token, indexes))
return repeated_tokens
Agora que as analises foram feitas, já sabemos os passos que podemos seguir para reduzir a dimensionalidade do nosso problema.
Agora para conseguirmos retirar o máximo de tokens sem perder informações valiosas do nosso dataset temos que seguir uma ordem fixa para reduçao de tal dimensionalidade.
y = dataset.iloc[:, 0]
Com a redução de features por steamming conseguimos reduzir o número de features de 11464 para 7168, representando uma redução de 37% das features.
def remove_repeated_tokens(data, r_tokens, verbose=0):
columns_to_drop = []
j = 0
for token, indexes in r_tokens:
for i in range(1, len(indexes)):
data.iloc[:, indexes[0]] += data.iloc[:, indexes[i]]
columns = dataset.columns[1:]
column_to_drop = columns[indexes[i]]
column_to_add = columns[indexes[0]]
columns_to_drop.append(column_to_drop)
if verbose > 1:
print('Droped\t\'{}\'\tand summed with\t\'{}\'\tToken - {} - {}'
.format(column_to_drop, column_to_add, token, indexes))
elif verbose > 0:
if j < 15:
print('Droped\t\'{}\'\tand summed with\t\'{}\'\tToken - {} - {}'
.format(column_to_drop, column_to_add, token, indexes))
j+=1
data = data.drop(columns_to_drop, axis=1)
return data
X = dataset.copy()
X = X.drop('Unnamed: 0', axis=1)
t0 = time()
X = remove_repeated_tokens(X, repeated_tokens, verbose = 1)
print('\n\nNew Dimensions: {}'.format(X.shape))
print('Execution time %.3f' % (time() - t0))
def make_comparative_feature_graph(dataset, X):
features_count = [dataset.shape[1], X.shape[1]]
fig, ax = plt.subplots()
cplt, novo = plt.bar(np.arange(1, 3), features_count)
novo.set_facecolor('r')
plt.ylabel('Features')
plt.xlabel('Datasets')
plt.title('Redução de Features')
ax.set_xticks(np.arange(1, 3))
ax.set_xticklabels(['Original', 'Novo'])
rects = ax.patches
for rect, label in zip(rects, features_count):
height = rect.get_height()
ax.text(rect.get_x() + rect.get_width()/2, height + 5, label, ha='center', va='bottom')
plt.show()
make_comparative_feature_graph(dataset, X)
Agora podemos observar o nosso novo conjunto de dados e como a máxima frequência dos tokens se comportam nele.
Faremos novamente um histograma para as frequências máximas. Com o novo histograma podemos perceber que o número de tokens com a frequência máxima abaixo de 24 vezes, caiu para aproximadamente 4297 tokens.
def count_max_ocurrences(dataset):
max_feature_counts = dataset.max()
sorted_values = max_feature_counts.sort_values(ascending=False)
sorted_columns = sorted_values.axes[0]
return (max_feature_counts, sorted_values, sorted_columns)
max_feature_counts, sorted_values, sorted_columns = count_max_ocurrences(X)
def make_histogram(arr, title, n_partitions=40, verbose=0):
n, bins, patches = plt.hist(arr, n_partitions, log=True, ec='black', alpha=0.75, align='mid')
plt.title(title, fontsize=20)
plt.xlabel('Maximo encontrado', fontsize=12)
plt.xticks(bins, rotation=90)
plt.ylabel('Frequência', fontsize=12)
ax = plt.gca()
ax.grid(axis='y', linestyle='--', linewidth=1)
plt.show()
if verbose > 0:
print(n, bins)
make_histogram(sorted_values, 'Histograma Freq Max - Novo dataset', verbose = 1)
Por último podemos reduzir ainda mais nosso dataset com base na proporção dos tokens nos artigos. Onde tokens que aparecem em poucos artigos ou tokens que aparecem em quase todos os artigos, não são úteis para discriminar nossas amostras das demais.
Para tanto, utilizamos a faixa de features entre 10% e 80% de proporção.
def remove_low_high_proportion(dataset):
tokens = dataset.columns
remove_tokens = []
for token in tokens:
token_proportion = len(dataset[dataset[token]>0]) / len(dataset)
if token_proportion < 0.1 or token_proportion > 0.8:
remove_tokens.append(token)
dataset = dataset.drop(remove_tokens, axis=1)
return (dataset, remove_tokens)
X, tokens_removed = remove_low_high_proportion(X)
tokens_removed[:10]
Aplicando a remoção por proporção podemos retirar 51% do dataset original, ficando ao final com apenas 10% do conjunto de dados inicial.
make_comparative_feature_graph(dataset, X)
Com isso podemos agora remover os tokens com baixa contagem em sua frequência.
Para realizar a redução, tomamos por base a contagem feita no histograma, onde verificamos que existem vários mais de 4 mil tokens com frequencia abaixo de 24 vezes. Porém, não podemos ter certeza de que tokens com frequências próximas a 24 vezes não sejam úteis para classificar algum artigo, então utilizamos de algumas regras baseado na frequência máxima para reduzir nosso problema.
Um token será removido caso:
E um token será enviado para análise de remoção caso a frequência máxima dele for entre 18 e 24 vezes.
def remove_low_counting_tokens(dataset, sorted_columns, sorted_values, bounds=[65,4,18,24]):
drop_columns = []
not_sure_to_drop = []
report = []
for index, s in enumerate(sorted_columns):
count_raw = np.array(dataset.loc[:, s])
non_zeros = [t for t in count_raw if t != 0]
median = np.median(count_raw)
mean = np.mean(count_raw)
# Check if the feature has a high frequency among at least 80% of the samples
if sorted_values[index] < bounds[0] and len(non_zeros) >= (0.8 * dataset.shape[0]):
drop_colum = (s, sorted_values[index], median, len(non_zeros))
report.append(drop_colum)
drop_columns.append(s)
# Check if the column is constant throughout at least 60% of the samples
elif abs(median - mean) <= bounds[1] and len(non_zeros) >= (0.6 * dataset.shape[0]):
drop_colum = (s, sorted_values[index], median, len(non_zeros))
report.append(drop_colum)
drop_columns.append(s)
# Drop if the max frequency is less than 18
elif sorted_values[index] < bounds[2]:
drop_colum = (s, sorted_values[index], median, len(non_zeros))
report.append(drop_colum)
drop_columns.append(s)
# Sent to check if the max frequency is higher than 18 and less than 24
elif sorted_values[index] < bounds[3]:
drop_colum = (s, sorted_values[index], median, len(non_zeros))
not_sure_to_drop.append(s)
else:
continue
dataset = dataset.drop(drop_columns, axis=1)
return (dataset, not_sure_to_drop, report, drop_columns)
max_feature_counts, sorted_values, sorted_columns = count_max_ocurrences(X)
X, columns_to_check, report, columns_droped = remove_low_counting_tokens(X, sorted_columns, sorted_values)
print(len(columns_droped), len(columns_to_check))
print(X.shape)
X.head()
Com isso conseguimos reduzir ainda mais nosso dataset, chegando a um percentual de 7% do dataset inicial.
make_comparative_feature_graph(dataset, X)
Agora devemos remover todo artigo nulo, que não houver contagem das features selecionadas.
def remove_nul_rows(dataset):
remove_rows = []
for i in range(len(dataset)):
total_words = sum(dataset.iloc[i, :])
if total_words == 0:
remove_rows.append(i)
indexes_to_drop = dataset.index[remove_rows]
dataset = dataset.drop(indexes_to_drop, axis=0)
return (dataset, indexes_to_drop)
X, indexes_removed = remove_nul_rows(X)
print('Removidos {} artigos: {}'.format(len(indexes_removed), list(indexes_removed)))
Agora podemos executar todos os passos de uma só vez para facilitar.
dataset = pd.read_csv('NIPS_1987-2015_transpose.csv')
y = dataset.iloc[:, 0]
X = dataset.copy()
X = X.drop('Unnamed: 0', axis=1)
t0 = time()
repeated_tokens = extract_repeated_tokens(X.columns)
X = remove_repeated_tokens(X, repeated_tokens)
max_feature_counts, sorted_values, sorted_columns = count_max_ocurrences(X)
X, columns_to_check, report, columns_droped = remove_low_counting_tokens(X, sorted_columns, sorted_values)
X, outliers = remove_low_high_proportion(X)
X, nul_rows = remove_nul_rows(X)
y = y.drop(nul_rows, axis=0)
print('New Dimensions: {}'.format(X.shape))
print('Execution time %.3fs' % (time() - t0))
Por fim temos este histograma para as features selecionadas.
max_feature_counts, sorted_values, sorted_columns = count_max_ocurrences(X)
make_histogram(sorted_values, 'Histograma Freq Max - Final')
Ao final apenas nos resta salvar o novo conjunto de dados para que possamos então usa-lo futuramente.
new_X = X.assign(y=pd.Series(y).values)
new_X.to_csv('NIPS_1987-2015_remodeled_755.csv', index=False)
Uma variação para teste seria salvar apenas as 300 primeiras palavras com as maiores ocorrências no conjunto por inteiro.
lcolumns = X.sum(0).nlargest(300).index.tolist()
X1 = X[lcolumns]
X1 = X1.assign(y=pd.Series(y).values)
X1.to_csv('NIPS_1987-2015_remodeled_300.csv', index=False)