« EventFormatBot » : différence entre les versions
Aucun résumé des modifications |
Aucun résumé des modifications |
||
(12 versions intermédiaires par 2 utilisateurs non affichées) | |||
Ligne 1 : | Ligne 1 : | ||
EventFormatBot a pour but d'étendre les fonctionnalités de [[FormatBot]]. Il vérifie que la syntaxe d'écriture des événements est bien respectée selon le dictionnaire des typologies d'événements | EventFormatBot a pour but d'étendre les fonctionnalités de [[FormatBot]]. Il vérifie que la syntaxe d'écriture des événements est bien respectée selon le dictionnaire des typologies d'événements. | ||
== Implémentation | == Implémentation technique == | ||
EventFormatBot prend un article Wikipast comme argument. Il distingue les entrées et vérifie la syntaxe indépendament de chacune d'elles. Cette particularité permet de traiter chacune des entrées de manière parallèle. | EventFormatBot prend un article Wikipast comme argument. Il distingue les entrées et vérifie la syntaxe indépendament de chacune d'elles. Cette particularité permet de traiter chacune des entrées de manière parallèle. | ||
Ligne 43 : | Ligne 43 : | ||
#Il est possible d'ajouter des hypermots optionnels avec des parenthèses. | #Il est possible d'ajouter des hypermots optionnels avec des parenthèses. | ||
Nous avons également simplifié le nombre de natures possibles de mot, il y a | Nous avons également simplifié le nombre de natures possibles de mot, il y a 7 natures possibles : | ||
*Date | *Date | ||
*Lieu | *Lieu | ||
Ligne 49 : | Ligne 49 : | ||
*Oeuvre d'Art | *Oeuvre d'Art | ||
*Institution | *Institution | ||
*Objet - qui regroupe toutes les catégories, inclue tous les mots | *Evenement | ||
*Objet - qui regroupe toutes les catégories, i.e inclue tous les mots | |||
Un hypermot peut être de natures différentes, précisés par un séparateur '/' entre chaque nature possible, voir exemple ci-dessous. | Un hypermot peut être de natures différentes, précisés par un séparateur '/' entre chaque nature possible, voir exemple ci-dessous. | ||
Ligne 57 : | Ligne 58 : | ||
Syntaxe: <code><nowiki> [[Date]] / [[Lieu]]. [[Publication]] de [[Oeuvre d'Art]] par [[Personnage/Institution]] (dans [[Journal]]) </nowiki></code> | Syntaxe: <code><nowiki> [[Date]] / [[Lieu]]. [[Publication]] de [[Oeuvre d'Art]] par [[Personnage/Institution]] (dans [[Journal]]) </nowiki></code> | ||
=== Vérification de la syntaxe === | === Vérification/Correction de la syntaxe === | ||
Pour une entrée donnée, | Pour une entrée donnée, l'algorithme se décompose de la manière suivante : | ||
#Il extrait l'événement de l'entrée. | |||
#Il détermine la syntaxe correspondante avec le dictionnaire des typologies d'événements. | |||
#Il associe un label 'nature' à chaque hypermot de l'entrée avec l'aide de spaCy | |||
#Il compare chacun des labels avec ceux attendus par la syntaxe de l'événement. Trois cas se présentent : | |||
##L'ordre et la nature des labels sont respectés, la syntaxe est alors respectée. L'algorithme retourne VRAIE. | |||
##La nature des labels est respectée mais l'ordre ne l'est pas. L'algorithme indique sur l'entrée que l'ordre n'est pas respecté. L'algorithme retourne FAUX. | |||
##La nature des labels n'est pas respectée. Cette erreur peut provenir de l'utilisateur comme de la détermination de la nature du mot. L'algorithme indique qu'il manque des informations et retourne FAUX. | |||
=== | == Performance == | ||
La partie cruciale d'EventFormatBot est la détermination de la nature d'un mot. Cette partie est très dépendante du modèle de reconnaissance utilisé (ici le modèle fr_core_news_md de spaCy). Il serait possible d'entraîner un modèle spécialement pour Wikipast, ce qui améliorait nettement l'intervalle de confiance lors de la reconnaissance de nature d'un mot. | |||
Lors de la phase de test, nous avons atteint le taux de précision de 98%, ce qui est satisfaisant pour nos objectifs. Concernant le temps d'exécution, il est d'environ 30s pour une page à 20 entrées, et le bottleneck principal est le temps d'exécution du modèle de NER. | |||
== Example == | |||
Nous prenons l'example de la page sur Albert Cohen [http://wikipast.epfl.ch/wikipast/index.php/Albert_Cohen]. | |||
=== Avant === | |||
[[Fichier:Albert_Cohen_Avant_EventFormatBot.png|900px]] | |||
=== Après === | |||
[[Fichier:Albert_Cohen_Après_EventFormatBot.png|900px]] | |||
== Code == | |||
<pre> | |||
import requests | |||
from bs4 import BeautifulSoup | |||
from urllib.request import Request, urlopen | |||
import spacy | |||
import re | |||
from collections import Counter | |||
from pprint import pprint | |||
def main(name): | |||
page_name = name | |||
nlp = spacy.load('fr_core_news_md') | |||
def most_frequent(List): | |||
occurence_count = Counter(List) | |||
return occurence_count.most_common(1)[0][0] | |||
event_page = "http://wikipast.epfl.ch/wikipast/index.php/%C3%89v%C3%A8nements" | |||
req = Request(event_page, headers={'User-Agent': 'Mozilla/5.0'}) | |||
webpage = urlopen(req).read() | |||
soup = BeautifulSoup(webpage, 'html.parser') | |||
entries = soup.findAll("li") | |||
events = [] | |||
events_links = [] | |||
for entry in entries: | |||
if entry.find("span") == None and entry.get("id") == None: | |||
events.append(entry.text.lower().strip()) | |||
events_links.append(entry.find("a").get("href")) | |||
target_page = "http://wikipast.epfl.ch/wikipast/index.php/" + page_name.strip().replace(" ", "_") | |||
req = Request(target_page, headers={'User-Agent': 'Mozilla/5.0'}) | |||
webpage = urlopen(req).read() | |||
soup = BeautifulSoup(webpage, 'html.parser') | |||
body = soup.find("div", {"class": "mw-content-ltr"}) | |||
entries = body.findAll("li") | |||
base_url = "http://wikipast.epfl.ch" | |||
event2spacy = { | |||
"OBJET": ["MISC"], | |||
"EVENEMENT": ["MISC"], | |||
"DATE": ["DATE"], | |||
"LIEU": ["LOC"], | |||
"PERSONNAGE": ["PER"], | |||
"OEUVRE D'ART": ["WORK_OF_ART", "MISC"], | |||
"INSTITUTION" : ["ORG"], | |||
"PERSONNAGE/INSTITUTION" : ["ORG" , "PER"], | |||
"NOM" : ["PER", "ORG"], | |||
"SUJET" : ["MISC"], | |||
"PUBLICATION" : ["MISC"], | |||
"ŒUVRE D'ART" : ["WORK_OF_ART", "MISC"] | |||
} | |||
modifs = [] | |||
for entry in entries: | |||
event = None | |||
#get syntax | |||
hyperlinks = [] | |||
doc = nlp(entry.text) | |||
links = entry.findAll("a") | |||
subject = entry.text.find(page_name) | |||
if subject!=-1: | |||
i = 0 | |||
while entry.text.find(links[i].text)<subject: | |||
i+=1 | |||
links.insert(i, page_name) | |||
for word in links: | |||
if word != page_name: | |||
word = word.text | |||
word_link = [] | |||
if word.lower() in events: | |||
event = word | |||
for x in doc: | |||
if str(x) in word.split(): | |||
word_link.append([x, x.ent_iob_, x.ent_type_]) | |||
tags = [elem[2] for elem in word_link] | |||
txt = "" | |||
for elem in word_link: | |||
txt += (str(elem[0])) | |||
event2 = event.strip().lower() if event != None else None | |||
if tags!=[]: | |||
if txt.strip().lower()==event2: | |||
hyperlinks.append([txt, event.upper()]) | |||
else: | |||
hyperlinks.append([txt, most_frequent(tags) ]) | |||
if event == None: | |||
continue | |||
for elem in hyperlinks: | |||
link = str(elem[0]) | |||
if re.search("([12]\d{3}\.(0[1-9]|1[0-2])\.(0[1-9]|[12]\d|3[01]))", link): | |||
elem[1] = "DATE" | |||
if re.search("([12]\d{3}\.(0[1-9]|1[0-2]))", link): | |||
elem[1] = "DATE" | |||
if re.search("([12]\d{3})", link): | |||
elem[1] = "DATE" | |||
print(hyperlinks) | |||
idx = events.index(event.lower()) | |||
event_url = events_links[idx] | |||
url = base_url + event_url | |||
req = Request(url, headers={'User-Agent': 'Mozilla/5.0'}) | |||
webpage = urlopen(req).read() | |||
soup = BeautifulSoup(webpage, 'html.parser') | |||
syntax = soup.find("code") | |||
optionnal = re.findall("\(([^\)]+)\)", syntax.text) | |||
optionnal = [] if optionnal==None else optionnal | |||
args = re.findall("\[\[.*?\]\]", syntax.text) | |||
relevant_args = [] | |||
for arg in args: | |||
optionnal_ = False | |||
for elem in optionnal: | |||
if arg in elem: | |||
optionnal_=True | |||
if not optionnal_: | |||
relevant_args.append(arg) | |||
arg_syntax = [] | |||
for arg in relevant_args: | |||
item = arg[2:-2] | |||
if item.strip().lower() == event.lower(): | |||
arg_syntax.append([item.upper()]) | |||
else: | |||
arg_syntax.append(event2spacy[arg[2:-2].upper()]) | |||
print(arg_syntax) | |||
if len(hyperlinks)<len(arg_syntax): | |||
modifs.append([entries.index(entry), " <span style=\"color: red;\">[EventFormatBot] --> Wrong formatting : missing parameters</span> \n \n"]) | |||
else: | |||
good_elem = True | |||
ordered = True | |||
arg_syntax_test = arg_syntax.copy() | |||
for elem in hyperlinks: | |||
elem_here = False | |||
for arg in arg_syntax_test: | |||
if elem[1] in arg: | |||
arg_syntax_test.remove(arg) | |||
elem_here = True | |||
if elem_here == False: | |||
good_elem = False | |||
if good_elem: | |||
for i in range(len(hyperlinks)): | |||
if hyperlinks[i][1] not in arg_syntax[i]: | |||
ordered = False | |||
if not ordered: | |||
modifs.append([entries.index(entry), | |||
" <span style=\"color: red;\">[EventFormatBot] --> Wrong order of parameters</span> \n \n"]) | |||
print(good_elem) | |||
print(ordered) | |||
user='EventFormatBot' | |||
passw='dhbot2019' | |||
baseurl='http://wikipast.epfl.ch/wikipast/' | |||
summary='Wikipastbot update' | |||
name = page_name | |||
# Login request | |||
payload={'action':'query','format':'json','utf8':'','meta':'tokens','type':'login'} | |||
r1=requests.post(baseurl + 'api.php', data=payload) | |||
#login confirm | |||
login_token=r1.json()['query']['tokens']['logintoken'] | |||
payload={'action':'login','format':'json','utf8':'','lgname':user,'lgpassword':passw,'lgtoken':login_token} | |||
r2=requests.post(baseurl + 'api.php', data=payload, cookies=r1.cookies) | |||
#get edit token2 | |||
params3='?format=json&action=query&meta=tokens&continue=' | |||
r3=requests.get(baseurl + 'api.php' + params3, cookies=r2.cookies) | |||
edit_token=r3.json()['query']['tokens']['csrftoken'] | |||
edit_cookie=r2.cookies.copy() | |||
edit_cookie.update(r3.cookies) | |||
result=requests.post(baseurl+'api.php?action=query&titles='+name+'&export&exportnowrap') | |||
soup = BeautifulSoup(result.text, "lxml") | |||
soup = soup.find("text").text | |||
indices = [] | |||
for match in re.finditer("\*", soup): | |||
indices.append(match.start()) | |||
indices.append(len(soup)) | |||
for i in range(len(modifs)): | |||
line = modifs[i][0] | |||
to_replace = soup[indices[line]:indices[line+1]] | |||
print(to_replace) | |||
soup = soup.replace(to_replace, to_replace.replace("\n", "") + modifs[i][1]) | |||
indices = [] | |||
for match in re.finditer("\*", soup): | |||
indices.append(match.start()) | |||
indices.append(len(soup)) | |||
print(soup) | |||
payload={'action':'edit','assert':'user','format':'json','utf8':'','text':soup,'summary':summary,'title':name,'token':edit_token} | |||
r4=requests.post(baseurl+'api.php',data=payload,cookies=edit_cookie) | |||
print(r4.text) | |||
</pre> |
Dernière version du 21 mai 2019 à 10:24
EventFormatBot a pour but d'étendre les fonctionnalités de FormatBot. Il vérifie que la syntaxe d'écriture des événements est bien respectée selon le dictionnaire des typologies d'événements.
Implémentation technique
EventFormatBot prend un article Wikipast comme argument. Il distingue les entrées et vérifie la syntaxe indépendament de chacune d'elles. Cette particularité permet de traiter chacune des entrées de manière parallèle.
Interprétation de la nature d'un mot
La syntaxe d'un événement est une succession ordonnée de mots de nature spécifique. Dans le cadre de cet algorithme, il est donc nécessaire de déterminer la nature d'un mot (Personnage, Lieu, Objet, autre...) : principe d'un NER (Named Entity Recognition)[1].
Example
- NAISSANCE : DATE / LIEU. Naissance de PERSONNAGE. [2]
Ici trois natures différentes sont utilisées : Date, Lieu et Personnage.
L'une des étapes du EventFormatBot est donc de vérifier que la nature du mot correspond à celle attendue par la syntaxe de l'événement.
Dans le cadre du cours, nous n'avons pas le temps de développer nous-même le NER, ainsi nous utiliserons une librairie externe Python open-source : spaCy [3]. spaCy regroupe les mots selon les catégories suivantes [4]
Nature d'un mot
- PERSON - Personnages, fictionnels inclus
- NORP - Nationalités ou groupes politique ou religieux
- FAC - Bâtiments, aéroports, autoroutes, ponts...
- ORG - Entreprises, institutions, agences...
- GPE - Pays et villes
- LOC - Lieu autre que pays ou villes
- PRODUCT - Objets, véhicules, nourritures...
- EVENT - Ouragans, batailles historique, événements sportif...
- WORK_OF_ART - Titres de livre, morceaux de musique...
- LAW - Nom d'article de loi
- LANGUAGE - Nom de language
- DATE - Dates absolue ou relative, période historique...
- TIME - Périodes plus courte qu'une journée
- PERCENT - Pourcentage, incluant '%'
- MONEY - valeurs monétaire
- QUANTITY - Mesures de poids ou distance.
- ORDINAL
- CARDINAL - Nombres hors-catégorie
Normalisation de la syntaxe typologique du dictionnaire
Pour chaque événement, il est nécessaire d'extraire automatiquement la syntaxe typologique de l'article correspondant. Nous avons mis en place une syntaxe standardisée située au début de l'article, inspirée de celle de Parution et mis à jour tous les autres événements afin qu'il soit conforme à la nouvelle syntaxe :
- Chaque entrée commence par une date.
- La date peut être suivie d'un lieu, séparé d'un séparateur.
- Chaque mot avec une nature imposée se traduit par un hypermot.
- Il est possible d'ajouter des hypermots optionnels avec des parenthèses.
Nous avons également simplifié le nombre de natures possibles de mot, il y a 7 natures possibles :
- Date
- Lieu
- Personnage
- Oeuvre d'Art
- Institution
- Evenement
- Objet - qui regroupe toutes les catégories, i.e inclue tous les mots
Un hypermot peut être de natures différentes, précisés par un séparateur '/' entre chaque nature possible, voir exemple ci-dessous.
Example - Parution
Syntaxe: [[Date]] / [[Lieu]]. [[Publication]] de [[Oeuvre d'Art]] par [[Personnage/Institution]] (dans [[Journal]])
Vérification/Correction de la syntaxe
Pour une entrée donnée, l'algorithme se décompose de la manière suivante :
- Il extrait l'événement de l'entrée.
- Il détermine la syntaxe correspondante avec le dictionnaire des typologies d'événements.
- Il associe un label 'nature' à chaque hypermot de l'entrée avec l'aide de spaCy
- Il compare chacun des labels avec ceux attendus par la syntaxe de l'événement. Trois cas se présentent :
- L'ordre et la nature des labels sont respectés, la syntaxe est alors respectée. L'algorithme retourne VRAIE.
- La nature des labels est respectée mais l'ordre ne l'est pas. L'algorithme indique sur l'entrée que l'ordre n'est pas respecté. L'algorithme retourne FAUX.
- La nature des labels n'est pas respectée. Cette erreur peut provenir de l'utilisateur comme de la détermination de la nature du mot. L'algorithme indique qu'il manque des informations et retourne FAUX.
Performance
La partie cruciale d'EventFormatBot est la détermination de la nature d'un mot. Cette partie est très dépendante du modèle de reconnaissance utilisé (ici le modèle fr_core_news_md de spaCy). Il serait possible d'entraîner un modèle spécialement pour Wikipast, ce qui améliorait nettement l'intervalle de confiance lors de la reconnaissance de nature d'un mot.
Lors de la phase de test, nous avons atteint le taux de précision de 98%, ce qui est satisfaisant pour nos objectifs. Concernant le temps d'exécution, il est d'environ 30s pour une page à 20 entrées, et le bottleneck principal est le temps d'exécution du modèle de NER.
Example
Nous prenons l'example de la page sur Albert Cohen [5].
Avant
Après
Code
import requests from bs4 import BeautifulSoup from urllib.request import Request, urlopen import spacy import re from collections import Counter from pprint import pprint def main(name): page_name = name nlp = spacy.load('fr_core_news_md') def most_frequent(List): occurence_count = Counter(List) return occurence_count.most_common(1)[0][0] event_page = "http://wikipast.epfl.ch/wikipast/index.php/%C3%89v%C3%A8nements" req = Request(event_page, headers={'User-Agent': 'Mozilla/5.0'}) webpage = urlopen(req).read() soup = BeautifulSoup(webpage, 'html.parser') entries = soup.findAll("li") events = [] events_links = [] for entry in entries: if entry.find("span") == None and entry.get("id") == None: events.append(entry.text.lower().strip()) events_links.append(entry.find("a").get("href")) target_page = "http://wikipast.epfl.ch/wikipast/index.php/" + page_name.strip().replace(" ", "_") req = Request(target_page, headers={'User-Agent': 'Mozilla/5.0'}) webpage = urlopen(req).read() soup = BeautifulSoup(webpage, 'html.parser') body = soup.find("div", {"class": "mw-content-ltr"}) entries = body.findAll("li") base_url = "http://wikipast.epfl.ch" event2spacy = { "OBJET": ["MISC"], "EVENEMENT": ["MISC"], "DATE": ["DATE"], "LIEU": ["LOC"], "PERSONNAGE": ["PER"], "OEUVRE D'ART": ["WORK_OF_ART", "MISC"], "INSTITUTION" : ["ORG"], "PERSONNAGE/INSTITUTION" : ["ORG" , "PER"], "NOM" : ["PER", "ORG"], "SUJET" : ["MISC"], "PUBLICATION" : ["MISC"], "ŒUVRE D'ART" : ["WORK_OF_ART", "MISC"] } modifs = [] for entry in entries: event = None #get syntax hyperlinks = [] doc = nlp(entry.text) links = entry.findAll("a") subject = entry.text.find(page_name) if subject!=-1: i = 0 while entry.text.find(links[i].text)<subject: i+=1 links.insert(i, page_name) for word in links: if word != page_name: word = word.text word_link = [] if word.lower() in events: event = word for x in doc: if str(x) in word.split(): word_link.append([x, x.ent_iob_, x.ent_type_]) tags = [elem[2] for elem in word_link] txt = "" for elem in word_link: txt += (str(elem[0])) event2 = event.strip().lower() if event != None else None if tags!=[]: if txt.strip().lower()==event2: hyperlinks.append([txt, event.upper()]) else: hyperlinks.append([txt, most_frequent(tags) ]) if event == None: continue for elem in hyperlinks: link = str(elem[0]) if re.search("([12]\d{3}\.(0[1-9]|1[0-2])\.(0[1-9]|[12]\d|3[01]))", link): elem[1] = "DATE" if re.search("([12]\d{3}\.(0[1-9]|1[0-2]))", link): elem[1] = "DATE" if re.search("([12]\d{3})", link): elem[1] = "DATE" print(hyperlinks) idx = events.index(event.lower()) event_url = events_links[idx] url = base_url + event_url req = Request(url, headers={'User-Agent': 'Mozilla/5.0'}) webpage = urlopen(req).read() soup = BeautifulSoup(webpage, 'html.parser') syntax = soup.find("code") optionnal = re.findall("\(([^\)]+)\)", syntax.text) optionnal = [] if optionnal==None else optionnal args = re.findall("\[\[.*?\]\]", syntax.text) relevant_args = [] for arg in args: optionnal_ = False for elem in optionnal: if arg in elem: optionnal_=True if not optionnal_: relevant_args.append(arg) arg_syntax = [] for arg in relevant_args: item = arg[2:-2] if item.strip().lower() == event.lower(): arg_syntax.append([item.upper()]) else: arg_syntax.append(event2spacy[arg[2:-2].upper()]) print(arg_syntax) if len(hyperlinks)<len(arg_syntax): modifs.append([entries.index(entry), " <span style=\"color: red;\">[EventFormatBot] --> Wrong formatting : missing parameters</span> \n \n"]) else: good_elem = True ordered = True arg_syntax_test = arg_syntax.copy() for elem in hyperlinks: elem_here = False for arg in arg_syntax_test: if elem[1] in arg: arg_syntax_test.remove(arg) elem_here = True if elem_here == False: good_elem = False if good_elem: for i in range(len(hyperlinks)): if hyperlinks[i][1] not in arg_syntax[i]: ordered = False if not ordered: modifs.append([entries.index(entry), " <span style=\"color: red;\">[EventFormatBot] --> Wrong order of parameters</span> \n \n"]) print(good_elem) print(ordered) user='EventFormatBot' passw='dhbot2019' baseurl='http://wikipast.epfl.ch/wikipast/' summary='Wikipastbot update' name = page_name # Login request payload={'action':'query','format':'json','utf8':'','meta':'tokens','type':'login'} r1=requests.post(baseurl + 'api.php', data=payload) #login confirm login_token=r1.json()['query']['tokens']['logintoken'] payload={'action':'login','format':'json','utf8':'','lgname':user,'lgpassword':passw,'lgtoken':login_token} r2=requests.post(baseurl + 'api.php', data=payload, cookies=r1.cookies) #get edit token2 params3='?format=json&action=query&meta=tokens&continue=' r3=requests.get(baseurl + 'api.php' + params3, cookies=r2.cookies) edit_token=r3.json()['query']['tokens']['csrftoken'] edit_cookie=r2.cookies.copy() edit_cookie.update(r3.cookies) result=requests.post(baseurl+'api.php?action=query&titles='+name+'&export&exportnowrap') soup = BeautifulSoup(result.text, "lxml") soup = soup.find("text").text indices = [] for match in re.finditer("\*", soup): indices.append(match.start()) indices.append(len(soup)) for i in range(len(modifs)): line = modifs[i][0] to_replace = soup[indices[line]:indices[line+1]] print(to_replace) soup = soup.replace(to_replace, to_replace.replace("\n", "") + modifs[i][1]) indices = [] for match in re.finditer("\*", soup): indices.append(match.start()) indices.append(len(soup)) print(soup) payload={'action':'edit','assert':'user','format':'json','utf8':'','text':soup,'summary':summary,'title':name,'token':edit_token} r4=requests.post(baseurl+'api.php',data=payload,cookies=edit_cookie) print(r4.text)