Skip to content
Snippets Groups Projects
Commit 6cacf0e6 authored by Alice Brenon's avatar Alice Brenon
Browse files

Restructure the project, separating the core lib from classification-related...

Restructure the project, separating the core lib from classification-related modules, and exposing the executable parts in a separate scripts dir; add a guix package definition
parent ec79f583
No related branches found
No related tags found
No related merge requests found
from EDdA.data import load, domains
File moved
from EDdA.classification.nGramsFrequencies import topNGrams
from EDdA.classification.classSimilarities import confusionMatrix, toPNG, metrics
from EDdA import data
import math
import matplotlib.pyplot as plot
import seaborn
def keysIntersection(d1, d2):
return len(set(d1).intersection(d2))
def scalarProduct(d1, d2):
return sum([d1[k] * d2[k] for k in set(d1.keys()).intersection(d2)])
def norm(d):
return math.sqrt(scalarProduct(d, d))
def colinearity(d1, d2):
return scalarProduct(d1, d2) / (norm(d1) * norm(d2))
metrics = {
'keysIntersection': keysIntersection,
'colinearity': colinearity
}
""" the variable 'domains' allows to restrict the matrices we're computing, but
" for our current needs they're still supposed to be about the classes devised
" for GÉODE so we give it this default value
"""
def confusionMatrix(vectorizer, metric, domains=data.domains):
m = []
matrixSize = len(domains)
for a in range(0, matrixSize):
m.append(matrixSize * [None])
for b in range(0, matrixSize):
m[a][b] = metric(vectorizer(domains[a]), vectorizer(domains[b]))
return m
def toPNG(matrix, filePath, domains=data.domains):
plot.figure(figsize=(16,13))
ax = seaborn.heatmap(
matrix, xticklabels=domains, yticklabels=domains, cmap='Blues'
)
plot.savefig(filePath, dpi=300, bbox_inches='tight')
import pickle
import sklearn
def vectorizerFileName(name, samplingSize):
return "{name}_s{samplingSize}".format(name=name, samplingSize=samplingSize)
......
from cache import Cache
import data
from EDdA.cache import Cache
from EDdA import data
import nltk
import pandas
import results
import sys
def frequenciesLoader(articles, n, domain):
texts = data.domain(articles, domain).contentWithoutClass
......@@ -16,20 +14,20 @@ def frequenciesLoader(articles, n, domain):
def frequenciesPath(inputHash, n):
return lambda domain:\
"frequencies/{inputHash}/{n}grams/{domain}.csv"\
"frequencies/{inputHash}/{n}grams/{domain}.tsv"\
.format(inputHash=inputHash, n=n, domain=domain)
def loadFrequencies(f):
csv = pandas.read_csv(f, sep='\t')
tsv = pandas.read_csv(f, sep='\t', na_filter=False)
return dict(zip(
csv.ngram.map(lambda s: tuple(s.split(','))),
csv.frequency
tsv.ngram.map(lambda s: tuple(s.split(','))),
tsv.frequency
))
def saveFrequencies(data, f):
def saveFrequencies(freqs, f):
pandas.DataFrame(data={
'ngram': map(lambda t: ','.join(t), data.keys()),
'frequency': data.values()
'ngram': map(lambda t: ','.join(t), freqs.keys()),
'frequency': freqs.values()
}).to_csv(f, sep='\t', index=False)
def frequencies(source, n):
......@@ -42,11 +40,11 @@ def frequencies(source, n):
def topLoader(frequencyEvaluator, n, ranks):
return lambda domain:\
dict(nltk.FreqDist(frequencyEvaluator(n, domain)).most_common(ranks))
dict(nltk.FreqDist(frequencyEvaluator(domain)).most_common(ranks))
def topPath(inputHash, n, ranks):
return lambda domain:\
"topNGrams/{inputHash}/{n}grams/top{ranks}/{domain}.csv".format(
"topNGrams/{inputHash}/{n}grams/top{ranks}/{domain}.tsv".format(
inputHash=inputHash,
n=n,
ranks=ranks,
......@@ -61,32 +59,3 @@ def topNGrams(source, n, ranks):
serializer=saveFrequencies,
unserializer=loadFrequencies
)
def __syntax(this):
print(
"Syntax: {this} {required} {optional}".format(
this=this,
required="ARTICLES_DATA(.csv)",
optional="[NGRAM SIZE] [TOP_RANKS_SIZE] [DOMAIN]"
),
file=sys.stderr
)
sys.exit(1)
def __compute(articlesSource, ns, ranksToTry, domains):
for n in ns:
for ranks in ranksToTry:
cached = topNGrams(data.load(articlesSource), n, ranks)
for domain in domains:
cached(domain)
if __name__ == '__main__':
argc = len(sys.argv)
if argc < 2:
__syntax(sys.argv[0])
else:
articlesSource = sys.argv[1]
ns = [int(sys.argv[2])] if argc > 2 else range(1,4)
ranksToTry = [int(sys.argv[3])] if argc > 3 else [10, 100, 50]
domains = [sys.argv[4]] if argc > 4 else data.domains
__compute(articlesSource, ns, ranksToTry, domains)
......@@ -11,7 +11,7 @@ class Source:
def load(name, textColumn="contentWithoutClass",
classColumn="ensemble_domaine_enccre"):
fileName = name if isfile(name) else "datasets/{name}.tsv".format(name=name)
return Source(pandas.read_csv(fileName, sep='\t')\
return Source(pandas.read_csv(fileName, sep='\t', na_filter=False)\
.dropna(subset=[classColumn])\
.reset_index(drop=True)
)
......
# Classification automatique d'articles encyclopédiques
# PyEDdA
Ce dépôt est proposé par **Khaled Chabane**, **Ludovic Moncla** et **Alice Brenon** dans le cadre du [Projet GEODE](https://geode-project.github.io/).
Il contient le code développé pour l'article "*Classification automatique d'articles encyclopédiques*" ([https://hal.archives-ouvertes.fr/hal-03481219v1](https://hal.archives-ouvertes.fr/hal-03481219v1)) présenté lors de la conférence [EGC 2022](https://egc2022.univ-tours.fr/).
Ce dépôt contient le code réalisé dans le cadre du projet
[GEODE](https://geode-project.github.io/) par **Khaled Chabane**, **Ludovic
Moncla** et **Alice Brenon**.
## Présentation
Ce dépôt contient le code développée pour une étude comparative de différentes approches de classification supervisée appliquées à la classification automatique d’articles encyclopédiques. Notre corpus d’apprentissage est constitué des 17 volumes de texte de l’Encyclopédie de Diderot et d’Alembert (1751-1772) représentant un total d’environ 70 000 articles. Nous avons expérimenté différentes approches de vectorisation de textes (sac de mots et plongement de mots) combinées à des méthodes d’apprentissage automatique classiques, d’apprentissage profond et des architectures BERT. En plus de la comparaison de ces différentes approches, notre objectif est d’identifier de manière automatique les domaines des articles non classés de l’Encyclopédie (environ 2 400 articles).
Il contient le code développé à l'origine pour l'article "*Classification
automatique d'articles encyclopédiques*"
([https://hal.archives-ouvertes.fr/hal-03481219v1](https://hal.archives-ouvertes.fr/hal-03481219v1))
présenté lors de la conférence [EGC 2022](https://egc2022.univ-tours.fr/).
## Méthodes testées
## Utilisation
Nos expérimentations concernent l’étude de différentes approches de classification comprenant deux étapes principales : la vectorisation et la classification supervisée. Nous avons testé et comparé les différentes combinaisons suivantes :
Ce dépôt est un paquet python pouvant être installé avec
[`pip`](https://pypi.org/) ainsi qu'avec [`guix`](https://guix.gnu.org/). À
partir d'une copie, depuis ce dossier, il est possible d'obtenir un
environnement dans lequel `pyedda` est installé et utilisable dans un shell à
l'aide des commandes suivantes:
1. vectorisation en sac de mots et apprentissage automatique classique (Naive Bayes, Logistic regression, Random Forest, SVM et SGD) ;
2. vectorisation en plongement de mots statiques (Doc2Vec) et apprentissage automatique classique (Logistic regression, Random Forest, SVM et SGD) ;
3. vectorisation en plongement de mots statiques (FastText) et apprentissage profond (CNN et LSTM) ;
4. approche *end-to-end* utilisant un modèle de langue pré-entraîné (BERT,CamemBERT) et une technique de *fine-tuning* pour adapter le modèle sur notre tâche de classification.
### Pip
```sh
pip install -e .
```
### Guix
## Résultats
```sh
guix shell python -f guix.scm
```
## Présentation
Ce dépôt contient le code développée pour une étude comparative de différentes
approches de classification supervisée appliquées à la classification
automatique d’articles encyclopédiques. Notre corpus d’apprentissage est
constitué des 17 volumes de texte de l’Encyclopédie de Diderot et d’Alembert
(1751-1772) représentant un total d’environ 70 000 articles. Nous avons
expérimenté différentes approches de vectorisation de textes (sac de mots et
plongement de mots) combinées à des méthodes d’apprentissage automatique
classiques, d’apprentissage profond et des architectures BERT. En plus de la
comparaison de ces différentes approches, notre objectif est d’identifier de
manière automatique les domaines des articles non classés de l’Encyclopédie
(environ 2 400 articles).
## Méthodes testées
Nos expérimentations concernent l’étude de différentes approches de
classification comprenant deux étapes principales : la vectorisation et la
classification supervisée. Nous avons testé et comparé les différentes
combinaisons suivantes :
1. vectorisation en sac de mots et apprentissage automatique classique (Naive
Bayes, Logistic regression, Random Forest, SVM et SGD) ;
2. vectorisation en plongement de mots statiques (Doc2Vec) et apprentissage
automatique classique (Logistic regression, Random Forest, SVM et SGD) ;
3. vectorisation en plongement de mots statiques (FastText) et apprentissage
profond (CNN et LSTM) ;
4. approche *end-to-end* utilisant un modèle de langue pré-entraîné
(BERT,CamemBERT) et une technique de *fine-tuning* pour adapter le modèle sur
notre tâche de classification.
## Résultats
### F-mesures moyennes des différents modèles pour les jeux de validation et de test avec un échantillonnage max de 500 (1) et 1 500 (2) articles par classe et sans échantillonnage (3).
......@@ -74,22 +110,30 @@ Nos expérimentations concernent l’étude de différentes approches de clas
| Mathématiques | 164 | 0.88 | 0.00 | 0.89 | Superstition | 26 | 0.81 | 0.00 | 0.73 |
| Musique | 163 | 0.94 | 0.01 | 0.94 | Spectacle | 11 | 0.17 | 0.00 | 0.00 |
### Matrice de confusion obtenue avec l’approche SGD+TF-IDF sur le jeu de test
![image info](./img/sgd_tf_idf_s10000.png)
Cette figure présente la matrice de confusion obtenue avec la méthode SGD+TF-IDF sur le jeu de test. On peut voir qu’un grand nombre d’articles des classes *Arts et métiers* et *Economie domestique* a été classé dans la classe *Métiers*, de la même manière les classes *Mesure*, *Miné- ralogie*, *Pharmacie* et *Politique* sont souvent confondues avec les classes *Commerce*, *Histoire naturelle*, *Médecine - Chirurgie* et *Droit - Jurisprudence*, respectivement. Les proximités sé- mantiques entre ces classes montrent bien la difficulté pour les modèles de choisir entre l’une ou l’autre et les résultats confirment qu’en cas de trop grande proximité les modèles choisissent la classe la plus représentée dans le jeu de données.
Cette figure présente la matrice de confusion obtenue avec la méthode SGD+TF-IDF
sur le jeu de test. On peut voir qu’un grand nombre d’articles des classes *Arts
et métiers* et *Economie domestique* a été classé dans la classe *Métiers*, de
la même manière les classes *Mesure*, *Miné- ralogie*, *Pharmacie* et
*Politique* sont souvent confondues avec les classes *Commerce*, *Histoire
naturelle*, *Médecine - Chirurgie* et *Droit - Jurisprudence*, respectivement.
Les proximités sé- mantiques entre ces classes montrent bien la difficulté pour
les modèles de choisir entre l’une ou l’autre et les résultats confirment qu’en
cas de trop grande proximité les modèles choisissent la classe la plus
représentée dans le jeu de données.
## Citation
Moncla, L., Chabane, K., et Brenon, A. (2022). Classification automatique d’articles encyclopédiques. *Conférence francophone sur l’Extraction et la Gestion des Connaissances (EGC)*. Blois, France.
Moncla, L., Chabane, K., et Brenon, A. (2022). Classification automatique
d’articles encyclopédiques. *Conférence francophone sur l’Extraction et la
Gestion des Connaissances (EGC)*. Blois, France.
## Remerciements
Les auteurs remercient le [LABEX ASLAN](https://aslan.universite-lyon.fr/) (ANR-10-LABX-0081) de l'Université de Lyon pour son soutien financier dans le cadre du programme français "Investissements d'Avenir" géré par l'Agence Nationale de la Recherche (ANR).
Les auteurs remercient le [LABEX ASLAN](https://aslan.universite-lyon.fr/)
(ANR-10-LABX-0081) de l'Université de Lyon pour son soutien financier dans le
cadre du programme français "Investissements d'Avenir" géré par l'Agence
Nationale de la Recherche (ANR).
guix.scm 0 → 100644
(use-modules ((gnu packages python-science) #:select (python-pandas))
((gnu packages python-xyz) #:select (python-matplotlib
python-nltk
python-seaborn))
(guix gexp)
((guix licenses) #:select (lgpl3+))
(guix packages)
(guix build-system python))
(let
((%source-dir (dirname (current-filename))))
(package
(name "python-pyedda")
(version "0.1.0")
(source
(local-file %source-dir
#:recursive? #t
#:select? (lambda (x . _) (not (string=? (basename x) ".git")))))
(build-system python-build-system)
(propagated-inputs
(list python-matplotlib
python-nltk
python-pandas
python-seaborn))
(home-page "https://gitlab.liris.cnrs.fr/geode/pyedda")
(synopsis "A set of tools to explore the EDdA")
(description
"PyEDdA provides a python library to expose the data from the Encyclopédie
by Diderot & d'Alembert, as well as several subpackages for the various
approach tested in the course of project GÉODE.")
(license lgpl3+)))
#!/usr/bin/env python3
from EDdA import data
from EDdA.classification import confusionMatrix, metrics, toPNG, topNGrams
import os
import sys
def preparePath(root, source, n, ranks, metricName):
path = "{root}/confusionMatrix/{inputHash}/{n}grams_top{ranks}_{name}.png".format(
root=root,
inputHash=source.hash,
n=n,
ranks=ranks,
name=metricName
)
os.makedirs(os.path.dirname(path), exist_ok=True)
return path
def __syntax(this):
print(
"Syntax: {this} {required} {optional}".format(
this=this,
required="ARTICLES_DATA(.csv) OUTPUT_DIR",
optional="[NGRAM SIZE] [TOP_RANKS_SIZE] [METRIC_NAME]"
),
file=sys.stderr
)
sys.exit(1)
def __compute(sourcePath, ns, ranksToTry, metricNames, outputDir):
for n in ns:
for ranks in ranksToTry:
source = data.load(sourcePath)
vectorizer = topNGrams(source, n, ranks)
for name in metricNames:
imagePath = preparePath(outputDir, source, n, ranks, name)
toPNG(confusionMatrix(vectorizer, metrics[name]), imagePath)
if __name__ == '__main__':
argc = len(sys.argv)
if argc < 2:
__syntax(sys.argv[0])
else:
sourcePath = sys.argv[1]
outputDir = sys.argv[2]
ns = [int(sys.argv[3])] if argc > 3 else range(1,4)
ranksToTry = [int(sys.argv[4])] if argc > 4 else [10, 100, 50]
metricNames = [sys.argv[5]] if argc > 5 else metrics.keys()
__compute(sourcePath, ns, ranksToTry, metricNames, outputDir)
#!/usr/bin/env python3
from EDdA import data
from EDdA.classification import topNGrams
import sys
def __syntax(this):
print(
"Syntax: {this} {required} {optional}".format(
this=this,
required="ARTICLES_DATA(.tsv)",
optional="[NGRAM SIZE] [TOP_RANKS_SIZE] [DOMAIN]"
),
file=sys.stderr
)
sys.exit(1)
def __populateCache(articlesSource, ns, ranksToTry, domains):
for n in ns:
for ranks in ranksToTry:
cached = topNGrams(data.load(articlesSource), n, ranks)
for domain in domains:
cached(domain)
if __name__ == '__main__':
argc = len(sys.argv)
if argc < 2:
__syntax(sys.argv[0])
else:
articlesSource = sys.argv[1]
ns = [int(sys.argv[2])] if argc > 2 else range(1,4)
ranksToTry = [int(sys.argv[3])] if argc > 3 else [10, 100, 50]
domains = [sys.argv[4]] if argc > 4 else data.domains
__populateCache(articlesSource, ns, ranksToTry, domains)
#!/usr/bin/env python3
from setuptools import setup
setup(name='EDdA',
version='0.1',
packages=['EDdA', 'EDdA.classification'])
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment