En aquest mòdul estudiarem la tasca anomenada etiquetatge morfosintàctic (en anglès part-of-speech tagging o POS-tagging). Aquesta tasca consisteix a assignar a cada paraula d'un text una categoria gramatical i altra informació addicional (com poden ser diverses subcategories, el lema associat, etc.) Aquesta és una tasca fonamental en el processament del llenguatge natural, encara que no està exempta de problemes que no estan encara totalment resolts. El llenguatge natural és ambigu des de molts punts de vista, i també ho és en el morfosintàctic. Una determinada forma (com ara casa) pot tenir diverses interpretacions morfosintàctiques, pot ser un substantiu comú femení singular (amb lema casa) i també una forma de present o d'imperatiu del verb casar. Els etiquetadors morfosintàctics hauran d'intentar donar la interpretació adequada segons el context d'utilització; per tant hauran de desambiguar les diferents possibilitats.
Abans d'abordar l'etiquetatge morfosintàctic veurem també l'anàlisi morfològica, és a dir, l'anàlisi que ens permet determinar el lema i la categoria gramatical (i altres subcategorizaciones) d'una determinada forma. Un cop siguem capaços de fer l'anàlisi morfològica, passarem a estudiar diverses tècniques que ens permetran etiquetar textos des del punt de vista morfosintàctic i que intentaran desambiguar (amb més o menys èxit) les diferents possibilitats.
Hi ha tot un seguit de formalismes per descriure la morfologia d'una llengua. En aquest capítol només presentarem un, que és senzill d'implementar i ens servirà per comprendre els mecanismes fonamentals: el formalisme de descomposició morfològica (Alshawi, 1992). La idea bàsica d'aquest formalisme és senzilla i es basa en dos tipus de coneixement:
En el programa-5-1.py podem veure una primera implementació molt senzilla d'aquesta idea:
t1="o"t2="es"t3="a"t4="em"t5="eu"t6="en"print(arrel+t1)print(arrel+t2)print(arrel+t3)print(arrel+t4)print(arrel+t5)print(arrel+t6)Si executem aquest programa, obtenim la següent sortida:
cantocantescantacantemcanteucantenEn aquest programa definim una sèrie de variables: una que conté el lema d’un verb i altres que contenen les terminacions de present d’indicatiu. Mitjançant l’operador + que concatena cadenes obtenim les formes corresponents al present d'indicatiu.
Seguint aquesta idea bàsica idearem un formalisme per expressar les regles (que a més de les formes volem que ens donin una etiqueta de categoria gramatical i subcategoritzacions). També idearem un formalisme per expressar el diccionari de lemes, que a més del propi lema ens indicarà el tipus de flexió que segueix (és a dir, el paradigma flexiu).
Com a formalisme per a les regles proposem una serie de valors separats per dos punts (:):
terminació_forma:terminació_lema:etiqueta:paradigmaPer exemple:
o:ar:VMIP1S:V1es:ar:VMIP2S:V1a:ar:VMIP3S:V1em:ar:VMIP1P:V1eu:ar:VMIP2P:V1en:ar:VMIP3P:V1Per les etiquetes fem servir les etiquetes EAGLES per al català. Al fitxer adjunt regles.txt podem observar un conjunt més extens de regles morfològiques.
Com a formalisme per al diccionari de lemes seguirem una idea similar:
lema:paradigmaQue en l'exemple seria:
cantar:V1Al fitxer adjunt diccionari.txt podem observar un diccionari més complet.
Ara necessitem un programa que ens permeti generar totes les formes a partir de les regles i el diccionari (programa-5-2.py)
fregles=open("regles.txt","r")regles=[]while True: linia=fregles.readline().rstrip() if not linia:break regles.append(linia)fregles.close()fdiccionari=open("diccionari.txt","r")while True: linia=fdiccionari.readline().rstrip() if not linia:break (lema,tipus)=linia.split(":") for regla in regles: (tf,tl,etiqueta,tipus2)=regla.split(":") if ((tipus2 == tipus)&(lema.endswith(tl))): print(lema[0:(len(lema)-len(tl))]+tf,lema,etiqueta)fdiccionari.close()que ens proporciona com a sortida:
canto cantar VMIP1Scantes cantar VMIP2Scanta cantar VMIP3Scantem cantar VMIP1Pcanteu cantar VMIP2Pcanten cantar VMIP3Pllegeixo llegir VMIP1Sllegeixes llegir VMIP2S.....Estem parlant d'anàliis morfològica, però en realitat el que hem fet és un programa que genera formes amb el seu lema i una etiqueta morfosintàctica, el que anomenem diccionari morfològic. Els programes d'anàlisi morfològica sovint funcionen amb un diccionari morfològic.
Al següent programa (programa-5-3.py) carreguem aquest diccionari i du a terme l'anàlisi de les parules que indica l'usuari (ef programa finalitza quan l'usuari introdueix un espai en blanc). El diccionari que hem creat en el programa anterior el podeu descarregar en el arxiu adjunt diccionari-curt-cat.txt:
import codecsdiccionari={}arxiu_diccionari=codecs.open("diccionari-curt-cat.txt","r",encoding="utf-8")for entrada in arxiu_diccionari: entrada=entrada.rstrip() camps=entrada.split(" ") forma=camps[0] lema=camps[1] etiqueta=camps[2] diccionari[forma]=diccionari.get(forma,"")+":"+lema+"\t"+etiqueta while 1: paraula=input() if paraula==" ": break if paraula in diccionari: print("ANALISI:",paraula,diccionari[paraula]) else: print("PARAULA DESCONEGUDA")que ofereix, per exemple, la següent sortida:
cantoANALISI: canto :cantar VMIP1ScasaPARAULA DESCONEGUDASi en comptes d'un diccionari tant petit fem servir un de més gran, el programa serà capaç d'analitzar moltes més paraules. Descarregueu l'arxiu diccionari-cat.txt, que és un gran diccionari morfològic extret de l'analitzador Freeling.
En aquest apartat construirem un analitzador morfològic, és a dir, un programa que és capaç de donar-nos les anàlisis morfològiques de totes les paraules d'un text. Per a les paraules ambigües des del punt de vista morfosintàctic, ens retornarà totes les possibles interpretacions. Amb el que hem fet fins ara tenim tots els components:
Al programa-5-4.py podem observar una primera versió del programa (que analitzarà l'arxiu noticia.txt, que conté un fragment de notícia publicada al diari Ara). Utilitza també el catalan-mod.pickle que hem creat al capítol anterior).
import codecsimport nltkfrom nltk.corpus.reader.plaintext import PlaintextCorpusReaderfrom nltk.tokenize import RegexpTokenizerdiccionari={}arxiu_diccionari=codecs.open("diccionari-cat.txt","r",encoding="utf-8")for entrada in arxiu_diccionari: entrada=entrada.rstrip() camps=entrada.split(" ") forma=camps[0] lema=camps[1] etiqueta=camps[2] if forma in diccionari: diccionari[forma]=diccionari.get(forma,"")+" "+lema+" "+etiqueta else: diccionari[forma]=lema+" "+etiquetasegmentador= nltk.data.load("catalan-mod.pickle")tokenitzador=RegexpTokenizer('[ldsmLDSM]\'|\w+|[^\w\s]+')corpus = PlaintextCorpusReader(".", 'noticia.txt',word_tokenizer=tokenitzador,sent_tokenizer=segmentador)for forma in corpus.words(): if forma in diccionari: info=diccionari[forma] else: info="DESCONEGUDA" print(forma+" "+info)que ens proporciona la sortida (mostrem únicament un fragment):
Després DESCONEGUDAd' de SPun un DI0MS0 un PI0MS00debat debat NCMS000 debatre VMIP3S0 debatre VMM02S0llampec llampec NCMS000lluny lluny AQ0MS00 lluny RGde de NCFS000 de SPl' el DA0CS0 el PP3CSA0esperpèntic esperpèntic AQ0MS00comitè comitè NCMS000federal federal AQ0CS00que que CS que PR0CN00 que RGva anar VAIP3S0 anar VMIP3S0 va AQ0MS00derrocar derrocar VMN0000Pedro DESCONEGUDASánchez DESCONEGUDA, DESCONEGUDACom veiem, hi ha alguns problemes, com per exemple:
" " Fe' ' Fe. . Fp, , Fc; ; Fx: : Fd( ( Fpa) ) Fpt[ [ Fca] ] FctVeiem la nova implementació al programa-5-5.py.
import codecsimport nltkfrom nltk.corpus.reader.plaintext import PlaintextCorpusReaderfrom nltk.tokenize import RegexpTokenizerdiccionari={}arxiu_diccionari=codecs.open("diccionari2-cat.txt","r",encoding="utf-8")for entrada in arxiu_diccionari: entrada=entrada.rstrip() camps=entrada.split(" ") forma=camps[0] lema=camps[1] etiqueta=camps[2] if forma in diccionari: diccionari[forma]=diccionari.get(forma,"")+" "+lema+" "+etiqueta else: diccionari[forma]=lema+" "+etiquetasegmentador= nltk.data.load("catalan-mod.pickle")tokenitzador=RegexpTokenizer('[ldsmLDSM]\'|\w+|[^\w\s]+')corpus = PlaintextCorpusReader(".", 'noticia.txt',word_tokenizer=tokenitzador,sent_tokenizer=segmentador)for forma in corpus.words(): if forma in diccionari: info=diccionari[forma] elif forma.lower() in diccionari: info=diccionari[forma.lower()] else: info="DESCONEGUDA" print(forma+" "+info)que dóna com a sortida:
Després després RGd' de SPun un DI0MS0 un PI0MS00debat debat NCMS000 debatre VMIP3S0 debatre VMM02S0llampec llampec NCMS000lluny lluny AQ0MS00 lluny RGde de NCFS000 de SPl' el DA0CS0 el PP3CSA0esperpèntic esperpèntic AQ0MS00comitè comitè NCMS000federal federal AQ0CS00que que CS que PR0CN00 que RGva anar VAIP3S0 anar VMIP3S0 va AQ0MS00derrocar derrocar VMN0000Pedro DESCONEGUDASánchez DESCONEGUDAEncara queden problemes per resoldre, com els noms propis, algun aspecte malament tokenitzat, etc. També poden aparèixer paraules desconegudes que siguin correctes, però que no estiguin recollides al diccionari morfològic, etc. En propers apartats d'aquest mateix capítol anirem presentant solucions a aquestes qüestions.
En aquest mòdul estudiarem la tasca anomenada etiquetatge morfosintàctic (enanglès part-of-speech tagging o POS-tagging). Aquesta tasca consisteix a assignar a cada paraula d’un text una categoria gramatical i altra informació addicional (com poden ser diverses subcategories, el lema associat, etc.) Aquesta és una tasca fonamental en el processament del llenguatge natural, tot i que no està exempta de problemes que no estan encara totalment resolts.
El llenguatge natural és ambigu des de molts punts de vista, i també ho és en el morfosintàctic. Una determinada forma (com per exemple casa) pot tenir diverses interpretacions morfosintàctiques, pot ser un substantiu comú femení singular (amb lema casa) i també una forma de present o d’imperatiu del verb casar. Els etiquetadors morfosintàctics hauran d’intentar donar la interpretació adequada segons el context on apareix una determinada paraula; per tant hauran de desambiguar les diferents possibilitats.
En el mòdul veurem diverses tècniques que ens permetran etiquetar textos des del punt de vista morfosintàctic i que intentaran desambiguar (amb major o menor èxit) les diferents possibilitats.
5.3.2. Etiquetatge morfosintàctic vs. anàlisi morfològica
A l'apartat anterior van estudiar la tasca anomenada anàlisi morfològica, que consisteix a assignar a cada paraula d'un text totes les possibles anàlisis morfològiques. A continuació podem veure l'anàlisi morfològica, duta a terme amb l'analitzador Freeling, de l'oració: Avui fa sol però demà plourà.
Veiem que diverses paraules presenten ambigüitat: fa pot ser tant una forma del verb fer, com un substantiu, la nota musical fa; sol, pot ser tant un adjectiu, com un nom, com diverses formes del verb soler; però pot ser tant una conjunció com un substantiu (Dificultat, objecció. Trobar peròs en tot. [Font: DIEC]); i demà pot ser tant un adverbi com un substantiu.
Un analitzador morfosintàctic ofereix la mateixa sortida, però desambiguada, és a dir, tria una de les possibilitats de cada paraula. Veiem ara l'anàlisi morfosintàctica feta per Freeling de la mateixa oració:
Com podem observar, en general la tria la fa correcta, excepte en el cas de sol que l'etiqueta com a adjectiu.
L’etiquetatge morfosintàctic és una tasca bàsica per a moltes tasques de processament del llenguatge natural. Si volem fer una anàlisi sintàctica d’una oració, un pas previ és conèixer la categoria gramatical de cada paraula. Disposar de textos etiquetats a escala morfosintàctica és interessant per a molts estudis. Podem saber quins són els substantius més utilitzats en un corpus, veure totes les aparicions d’un verb independentment de la forma concreta, etc. L’etiquetatge morfosintàctic també es fa servir per a extreure els termes més rellevants d’un determinat document o conjunt de documents. L’etiquetatge també es fa servir per a la classificació de documents i recuperació d’informació.
NLTK proporciona un etiquetador per a l'anglès que funciona prou bé i que es pot fer servir fàcilment de manera directa, com al programa-5-6.py.
import nltkoracio="They refuse to permit us to obtain the refuse permit"paraules = nltk.tokenize.word_tokenize(oracio)analisi=nltk.pos_tag(paraules)print(analisi)que dóna la següent sortida:
[('They', 'PRP'), ('refuse', 'VBP'), ('to', 'TO'), ('permit', 'VB'), ('us', 'PRP'), ('to', 'TO'), ('obtain', 'VB'), ('the', 'DT'), ('refuse', 'NN'), ('permit', 'NN')]Fixeu-vos que la paraula permit l'ha etiquetat correctament com a verb i com a substantiu. Fixeu-vos que les etiquetes les dona amb un etiquetari (tagset), o conjunt d'etiquetes, determinat. En aquest cas concret es fan servir el tagset WSJ (Wall Street Journal), que és el següent:
Tag FunctionCC coordinating conjunctionCD cardinal numberDT determinerEX existential ``there''FW foreign wordIN prepositionJJ adjectiveJJR adjective, comparativeJJS adjective, superlativeMD modalNN non-plural common nounNNP non-plural proper nounNNPS plural proper nounNNS plural common nounof the word ``of''PDT pre-determinerPOS posessivePRP pronounpuncf final punctuation (period, question mark and exclamation mark)punc other punctionRB adverbRBR adverb, comparativeRBS adverb, superlativeRP particleTO the word ``to''UH interjectionVB verb, base formVBD verb, past tenseVBG verb, gerund or present participleVBN verb, past participleVBP verb, non-3rd personVBZ verb, 3rd personWDT wh-determinerWP wh-pronounWRB wh-adverbsym symbol2 ambiguously labelledCom veurem quan comencem a construir etiquetadors per a altres llengües, com el català, les etiquetes que farem servir seran diferents. Hi ha una proposta d'etiquetari universal (universal tagset). A continuació podem observar aquest etiquetari.
Tag Meaning English ExamplesADJ adjective new, good, high, special, big, localADP adposition on, of, at, with, by, into, underADV adverb really, already, still, early, nowCONJ conjunction and, or, but, if, while, althoughDET determiner, article the, a, some, most, every, no, whichNOUN noun year, home, costs, time, AfricaNUM numeral twenty-four, fourth, 1991, 14:24PRT particle at, on, out, over per, that, up, withPRON pronoun he, their, her, its, my, I, usVERB verb is, say, told, given, playing, would. punctuation marks . , ; !X other ersatz, esprit, dunno, gr8, univeristyNLTK ens permet convertir les etiquetes d'un determinat etiquetari al l'etiquetari universal. Ho podem veure al programa-5-6b.py.
import nltkoracio="They refuse to permit us to obtain the refuse permit"paraules = nltk.tokenize.word_tokenize(oracio)analisi=nltk.pos_tag(paraules)for ana in analisi: forma=ana[0] etiqueta=ana[1] universal=nltk.tag.mapping.map_tag('en-ptb', 'universal', etiqueta) print(forma,etiqueta,universal)que ens dóna la següent sortida:
They PRP PRONrefuse VBP VERBto TO PRTpermit VB VERBus PRP PRONto TO PRTobtain VB VERBthe DT DETrefuse NN NOUNpermit NN NOUNEn aquest apartat presentem un etiquetador molt simple, que l’únic que fa és etiquetar totes les paraules amb una etiqueta determinada, és a dir, etiqueta totes les paraules amb la mateixa etiqueta. Per determinar quina etiqueta triarem el que farem primer és calcular quina és l'etiqueta més freqüent. Per a aconseguir això farem ús de corpus ja etiquetats: per a l'anglès farem servir el Brown Corpus i per al català el CESS_CAT.
Per a l'anglès:
En un intèrpret interactiu fem, per calcular l'etiqueta més freqüent:
>>> import nltk>>> from nltk.corpus import brown>>> tags=[tag for (word,tag) in brown.tagged_words()]>>> nltk.FreqDist(tags).max()'NN'I per definir un etiquetador per defecte fem:
>>> from nltk.tokenize import word_tokenize>>> tokens=word_tokenize(oracio)>>> from nltk.tokenize import word_tokenize>>> oracio="they refuse to permit us to obtain the refuse permit">>> tokens=word_tokenize(oracio)>>> default_tagger=nltk.DefaultTagger('NN')>>> default_tagger.tag(tokens)[('they', 'NN'), ('refuse', 'NN'), ('to', 'NN'), ('permit', 'NN'), ('us', 'NN'), ('to', 'NN'), ('obtain', 'NN'), ('the', 'NN'), ('refuse', 'NN'), ('permit', 'NN')]Com podeu observar, etiqueta totes les paraules amb 'NN'.
Per al català:
>>> from nltk.corpus import cess_cat>>> import nltk>>> from nltk.corpus import cess_cat>>> tags=[tag for (word,tag) in cess_cat.tagged_words()]>>> nltk.FreqDist(tags).max()'sps00'Ens pot sorprendre que l'etiqueta més freqüent en català sigui la corresponent a la preposició ja que esperaríem que fos també la corresponent a substantiu. El que passa és que en l'etiquetari català tenin posicions per la categoria i diverses subcategoritzacions (gènere i nombre). Si modifiquem les dues darreres línies per fer que miri només la primera posició de l'etiqueta, obtindrem que la categoria més freqüent és 'n' (substantiu).
>>> tags=[tag[0] for (word,tag) in cess_cat.tagged_words()]>>> nltk.FreqDist(tags).max()'n'Podríem fer també un etiquetador per defecte del català indicant que l'etiqueta per defecte fons 'n'. Però realment preferiríem indicar l'etiqueta completa més freqüent per als substantius.
>>> tags=[tag for (word,tag) in cess_cat.tagged_words()]>>> nltk.FreqDist(tags).most_common(10)[('sps00', 64225), ('ncfs000', 30917), ('ncms000', 29527), ('Fc', 26964), ('da0fs0', 17322), ('np00000', 17237), ('Fp', 16875), ('vmn0000', 15715), ('ncmp000', 14797), ('cc', 14403)]Que ens dona que els noms comuns femenins singulars són lleugerament més freqüents que els noms comuns masculins singulars. Ara podem definir un etiquetador per defecte per al català:
>>> oracio="Avui fa sol però demà plourà">>> tokens=word_tokenize(oracio)>>> default_tagger=nltk.DefaultTagger('ncfs000')>>> default_tagger.tag(tokens)[('Avui', 'ncfs000'), ('fa', 'ncfs000'), ('sol', 'ncfs000'), ('però', 'ncfs000'), ('demà', 'ncfs000'), ('plourà', 'ncfs000')]Ja ens podem imaginar que aquest etiquetador no funcionarà gaire bé. En el següent apartat aprendrem a avaluar etiquetadors i podrem veure la precisió d'aquest etiquetador. També pot servir pels casos que tinguem una paraula desconeguda ja que li podrem assignar l'etiqueta més freqüent (sense comptar la de preposició, ja que sent una categoria tancada és pràcticament impossible que sigui desconeguda).
L’etiquetador per defecte estudiat en l'apartat anterior etiqueta totes les paraules amb l’etiqueta més freqüent en tot el corpus. En aquest apartat i els propers estudiarem una sèrie d’etiquetadors anomenats genèricament etiquetadors per
n-grames. Un n-grama és una combinació d'n elements. En general aquests etiquetadors aprenen a partir del corpus tenint
en compte un context de n paraules. En el cas de l’etiquetador per unigrames l’únic que tenim en compte és la paraula per etiquetar mateixa, sense cap context. L'etiquetador per unigrames etiquetarà cada paraula amb l'etiqueta més frequent per a aquella paraula.
Amb l’NLTK crear un etiquetador per unigrames és molt senzill. Primer ho farem per a l’anglès i després per al català.
Per a l'anglès
>>> import nltk>>> from nltk.corpus import brown>>> tagged_sents=brown.tagged_sents()>>> unigram_tagger=nltk.UnigramTagger(tagged_sents)>>> oracio="they refuse to permit us to obtain the refuse permit">>> tokens=nltk.word_tokenize(oracio)>>> unigram_tagger.tag(tokens)[('they', 'PPSS'), ('refuse', 'VB'), ('to', 'TO'), ('permit', 'VB'), ('us', 'PPO'), ('to', 'TO'), ('obtain', 'VB'), ('the', 'AT'), ('refuse', 'VB'), ('permit', 'VB')]Si ens fixem en el resultat, veiem que refuse l'etiqueta les dues vegades que apareix com a verb, ja que aquesta és l'etiqueta més freqüent per a aquesta paraula.
Per al català
Farem el mateix, però aquesta vegada en un programa (programa-5-7.py):
import nltkfrom nltk.corpus import cess_catfrom nltk.tokenize import word_tokenizetagged_sents=cess_cat.tagged_sents()unigram_tagger=nltk.UnigramTagger(tagged_sents)oracio="avui fa sol però demà manifestació"tokens=word_tokenize(oracio)analisi=unigram_tagger.tag(tokens)print(analisi)que ens dona la sortida:
[('avui', 'rg'), ('fa', 'vmip3s0'), ('sol', 'aq0ms0'), ('però', None), ('demà', None), ('plourà', 'None')]que veiem que sistemàticament dona problemes amb les paraules accentuades. Malauradament, en el moment d'escriure aquest apartat el corpus cess_cat de NLTK presenta problemes de codificació de caràcters. La codificació en els diferents arxius no és sistemàtica de manera que no podem carregar el corpus per entrenar etiquetadors d'una manera fiable.
Per solucionar això farem servir un altre corpus per al català, el Wikicorpus, que és un corpus format per textos de la Vikipèdia i etiquetats amb Freeling. Com que aquest corpus ja té uns anys, descarregarem els textos i el tornarem a etiquetar amb una versió més nova de Freeling. Podeu descarregar aquest corpus dels fitxers d'aquest capítol (recordeu descomprimir-lo un cop descarregat). Observem el format del corpus:
S' es P0300000 0.999814hi hi PP3CN000 1poden poder VMIP3P0 0.470339representar representar VMN0000 1nombres nombre NCMP000 0.454513enters enter AQ0MP0 0.366457o o CC 0.999266decimals decimal AQ0CP0 0.566499. . Fp 1És a dir, forma, lema, etiqueta i probabilitat de l'etiqueta. Haurem de crear un codi capaç de llegir aquest corpus i crear les tagged_sents com a llistes de tagged_words on cada tagged_word és una tupla forma, etiqueta. Ho podem fer amb aquest fragment de codi:
import codecsimport nltkentrada=codecs.open("catalanTagged_0_5000-utf-8.txt","r",encoding="utf-8")tagged_words=[]tagged_sents=[]for linia in entrada: linia=linia.rstrip() if linia.startswith("<") or len(linia)==0: #nova linia if len(tagged_words)>0: tagged_sents.append(tagged_words) tagged_words=[] else: camps=linia.split(" ") forma=camps[0] lema=camps[1] etiqueta=camps[2] tupla=(forma,etiqueta) tagged_words.append(tupla)que integrarem al programa-5-8.py que pot entrenar un etiquetador per unigrames:
import codecsimport nltkentrada=codecs.open("catalanTagged_0_5000-utf-8.txt","r",encoding="utf-8")tagged_words=[]tagged_sents=[]for linia in entrada: linia=linia.rstrip() if linia.startswith("<") or len(linia)==0: #nova linia if len(tagged_words)>0: tagged_sents.append(tagged_words) tagged_words=[] else: camps=linia.split(" ") forma=camps[0] lema=camps[1] etiqueta=camps[2] tupla=(forma,etiqueta) tagged_words.append(tupla)unigram_tagger=nltk.UnigramTagger(tagged_sents)oracio="avui fa sol però demà plourà"tokens=nltk.tokenize.word_tokenize(oracio)analisi=unigram_tagger.tag(tokens)print(analisi)i proporciona la següent sortida:
[('avui', 'RG'), ('fa', 'VMIP3S0'), ('sol', 'NCMS000'), ('però', 'CC'), ('demà', 'RG'), ('plourà', 'VMIF3S0')]que ja no presenta problemes de codificació i pot etiquetar tant demà com plourà.
5.4.3. L'etiquetador per bigrames
L'etiquetador per unigrames no té en compte el context d'aparició de les paraules, i les etiqueta sempre amb l'etiqueta més freqüent per a aquesta paraula. En aquest apartat construirem un etiquetador per bigrames que té en compte la pròpia paraula a etiquetar i la paraula anterior:
Per a l'anglès
>>> import nltk>>> from nltk.corpus import brown>>> tagged_sents=brown.tagged_sents()>>> bigram_tagger=nltk.BigramTagger(tagged_sents)>>> oracio="they refuse to permit us to obtain the refuse permit">>> tokens=word_tokenize(oracio)>>> tokens=nltk.tokenize.word_tokenize(oracio)>>> bigram_tagger.tag(tokens)>>> bigram_tagger.tag(tokens)[('they', 'PPSS'), ('refuse', 'VB'), ('to', 'TO'), ('permit', 'VB'), ('us', 'PPO'), ('to', 'TO'), ('obtain', 'VB'), ('the', 'AT'), ('refuse', 'NN'), ('permit', 'NN')]Fixem-nos que ara pot etiquetar el segon refuse com a nom, ja que té en compte el context immediat.
Per al català
Modifiquem el programa-5-8.py per obtenir el programa-5-9.py:
import codecsimport nltkentrada=codecs.open("catalanTagged_0_5000-utf-8.txt","r",encoding="utf-8")tagged_words=[]tagged_sents=[]for linia in entrada: linia=linia.rstrip() if linia.startswith("<") or len(linia)==0: #nova linia if len(tagged_words)>0: tagged_sents.append(tagged_words) tagged_words=[] else: camps=linia.split(" ") forma=camps[0] lema=camps[1] etiqueta=camps[2] tupla=(forma,etiqueta) tagged_words.append(tupla)bigram_tagger=nltk.BigramTagger(tagged_sents)oracio="avui fa sol però demà plourà"tokens=nltk.tokenize.word_tokenize(oracio)analisi=bigram_tagger.tag(tokens)print(analisi)Que ofereix ara la següent sortida:
[('avui', None), ('fa', None), ('sol', None), ('però', None), ('demà', None), ('plourà', None)]És a dir, que no ha pogut etiquetar cap paraula. Això és degut a un fenòmen conegut com a dispersió de dades. Hi ha molts més unigrames que bigrames en un corpus. Si entrenem un etiquetador amb bigrames, necessitarem un corpus més gran per poder trobar suficients evidències de cada bigrama de l'oracio a analitzar, com que no sempre és possible disposar de grans corpus, es pot recórrer a la tècnica coneguda com a backoff. A aquesta tècnica també se la coneix com a combinació d'etiquetadors. Al programa-5-10.py combinem un etiquetador de bigrames amb un d'unigrames.
import codecsimport nltkentrada=codecs.open("catalanTagged_0_5000-utf-8.txt","r",encoding="utf-8")tagged_words=[]tagged_sents=[]for linia in entrada: linia=linia.rstrip() if linia.startswith("<") or len(linia)==0: #nova linia if len(tagged_words)>0: tagged_sents.append(tagged_words) tagged_words=[] else: camps=linia.split(" ") forma=camps[0] lema=camps[1] etiqueta=camps[2] tupla=(forma,etiqueta) tagged_words.append(tupla)unigram_tagger=nltk.UnigramTagger(tagged_sents)bigram_tagger=nltk.BigramTagger(tagged_sents,backoff=unigram_tagger)oracio="avui fa sol però demà plourà"tokens=nltk.tokenize.word_tokenize(oracio)analisi=bigram_tagger.tag(tokens)print(analisi)i ens ofereix ara la següent sortida:
[('avui', 'RG'), ('fa', 'VMIP3S0'), ('sol', 'AQ0MS0'), ('però', 'CC'), ('demà', 'RG'), ('plourà', 'VMIF3S0')]i com veiem, ha pogut etiquetar totes les paraules, ja que amb l'etiquetador de bigrames no ha pogut, però sí fent servir el d'unigrames.
L'etiquetador per trigrames etiqueta una paraula tenint en compte el context format per les dues paraules anteriors. En aquest cas, el problema de la dispersió de dades serà encara més pronunciat. En el programa-5-11.py implementem un etiquetador per trigrames que es combina amb un de bigrames i a la seva vegada per un de trigrames. Fixeu-vos que en aquest programa hem canviat la frase a analitzar i hem fet servir un tokenitzador per expressions regular que ens permet tractar satisfactòriament els apòstrofs:
import codecsimport nltkentrada=codecs.open("catalanTagged_0_5000-utf-8.txt","r",encoding="utf-8")tagged_words=[]tagged_sents=[]for linia in entrada: linia=linia.rstrip() if linia.startswith("<") or len(linia)==0: if len(tagged_words)>0: tagged_sents.append(tagged_words) tagged_words=[] else: camps=linia.split(" ") forma=camps[0] lema=camps[1] etiqueta=camps[2] tupla=(forma,etiqueta) tagged_words.append(tupla)unigram_tagger=nltk.UnigramTagger(tagged_sents)bigram_tagger=nltk.BigramTagger(tagged_sents,backoff=unigram_tagger)trigram_tagger=nltk.TrigramTagger(tagged_sents,backoff=bigram_tagger)oracio="l'àcid desoxiribonucleic (ADN o DNA) és un àcid nucleic que conté les instruccions genètiques utilitzades en el desenvolupament i funcionament de tots els éssers vius coneguts, així com en alguns virus, des d'un punt de vista químic, l'ADN es compon de dos llargs polímers d'unitats simples anomenades nucleòtids, amb un tronc compost de sucres i grups fosfats units per enllaços èster"tokenitzador=nltk.tokenize.RegexpTokenizer('[ldsmLDSM]\'|\w+|[^\w\s]+')tokens=tokenitzador.tokenize(oracio)analisi=trigram_tagger.tag(tokens)print(analisi)que proporciona la següent anàlisi:
[("l'", 'DA0CS0'), ('àcid', 'NCMS000'), ('desoxiribonucleic', 'AQ0MS0'), ('(', 'Fpa'), ('ADN', 'NP00000'), ('o', 'CC'), ('DNA', 'NP00000'), (')', 'Fpt'), ('és', 'VSIP3S0'), ('un', 'DI0MS0'), ('àcid', 'NCMS000'), ('nucleic', 'AQ0MS0'), ('que', 'PR0CN000'), ('conté', 'VMIP3S0'), ('les', 'DA0FP0'), ('instruccions', 'NCFP000'), ('genètiques', 'AQ0FP0'), ('utilitzades', 'VMP00PF'), ('en', 'SPS00'), ('el', 'DA0MS0'), ('desenvolupament', 'NCMS000'), ('i', 'CC'), ('funcionament', 'NCMS000'), ('de', 'SPS00'), ('tots', 'DI0MP0'), ('els', 'DA0MP0'), ('éssers', 'NCMP000'), ('vius', 'AQ0MP0'), ('coneguts', 'NCMP000'), (',', 'Fc'), ('així', 'RG'), ('com', 'CS'), ('en', 'SPS00'), ('alguns', 'DI0MP0'), ('virus', 'NCMN000'), (',', 'Fc'), ('des', 'SPS00'), ("d'", 'SPS00'), ('un', 'DI0MS0'), ('punt', 'NCMS000'), ('de', 'SPS00'), ('vista', 'NCCS000'), ('químic', 'AQ0MS0'), (',', 'Fc'), ("l'", 'DA0CS0'), ('ADN', 'NP00000'), ('es', 'P0300000'), ('compon', 'VMIP3S0'), ('de', 'SPS00'), ('dos', 'Z'), ('llargs', 'AQ0MP0'), ('polímers', 'NCMP000'), ("d'", 'SPS00'), ('unitats', 'NCFP000'), ('simples', 'AQ0CP0'), ('anomenades', 'NCFP000'), ('nucleòtids', 'NCMP000'), (',', 'Fc'), ('amb', 'SPS00'), ('un', 'DI0MS0'), ('tronc', 'NCMS000'), ('compost', 'NCMS000'), ('de', 'SPS00'), ('sucres', 'NCMP000'), ('i', 'CC'), ('grups', 'NCMP000'), ('fosfats', 'NCMP000'), ('units', 'VMP00PM'), ('per', 'SPS00'), ('enllaços', 'NCMP000'), ('èster', None)]On si ens fixem, hi ha algunes paraules, com èster que no les pot etiqueta, tot i que hi ha paraules força especialitzades que sí que és capaç d'etiquetar.
5.4.5. Tractament de paraules desconegudes: diccionari morfològic, etiquetador per afixos i etiquetador per defecte
En el programa anterior hem vist que un etiquetador entrenat amb un corpus no serà capaç d'etiquetar paraules que no apareguin en el corpus. Per solucionar això farem servir 3 estratègies:
Veiem primer aquests tres entrenaments per separat i després ho combinarem tot en un únic etiquetador.
Etiquetador per unigrames a partir d'un diccionari morfològic
Farem servir aquesta part de codi (programa-5-12.py):
import codecsimport nltkentrada=codecs.open("diccionari-cat.txt","r",encoding="utf-8")tagged_words=[]tagged_sents=[]cont=0for linia in entrada: cont+=1 if cont==10000: break linia=linia.rstrip() camps=linia.split(" ") forma=camps[0] lema=camps[1] etiqueta=camps[2] tupla=(forma,etiqueta) tagged_words.append(tupla)tagged_sents.append(tagged_words)unigram_tagger_diccionari=nltk.UnigramTagger(tagged_sents)Fixeu-vos que contem les línies i quan arriba a la 10000 parem l'entrenament per a que no trigui massa. Per fer les proves serà suficient i en el moment de fer l'entrenament definitiu eliminarem (comentarem) aquestes línies de codi de manera que faci l'entrenament amb tot el diccionari. Aquest programa no ofereix cap sortida.
Entrenament d'un etiquetador per afixos utilitzant el diccionari morfològic
Podem trobar la implementació en el programa-5-13.py que és exactament igual que l'anterior però canvia la darrera línia i ara és:
affix_tagger=nltk.AffixTagger(tagged_sents, affix_length=-3, min_stem_length=2)que permet entrenar l'etiquetador per afixos tenint en compte els afixos amb tres caràcters sempre i quan l'arrel que quedi tingui 2 o més caràcters.
Determinació de l'etiqueta més freqüent i entrenament de l'etiquetador per defecte
El programa-5-14.py ens retorna les 10 etiquetes més freqüents:
import codecsimport nltkentrada=codecs.open("catalanTagged_0_5000-utf-8.txt","r",encoding="utf-8")tagged_words=[]tagged_sents=[]for linia in entrada: linia=linia.rstrip() if linia.startswith("<") or len(linia)==0: #nova linia if len(tagged_words)>0: tagged_sents.append(tagged_words) tagged_words=[] else: camps=linia.split(" ") forma=camps[0] lema=camps[1] etiqueta=camps[2] tupla=(forma,etiqueta) tagged_words.append(tupla)tags=[]for ts in tagged_sents: for wt in ts: tags.append(wt[1])mft=nltk.FreqDist(tags).most_common(10)print("Etiqueta més freqüent: ",mft)default_tagger=nltk.DefaultTagger("NP00000")Que ens dona a la sortida:
Etiquetes més freqüent: [('SPS00', 284406), ('NP00000', 177296), ('NCMS000', 140239), ('NCFS000', 130773), ('Fc', 113652), ('Fp', 96173), ('DA0MS0', 87666), ('DA0FS0', 69641), ('CC', 64777), ('NCMP000', 62556)]I tornem a tenir que la més freqüent (de les categories obertes) és el substantiu, concretament el nom propi. I llavors creem l'etiquetador per defecte fent servir aquesta etiqueta
Posarem tot això junt en el següent apartat, o a més, aprendrem a emmagatzemar etiquetadors .
En l'apartat anterior hem après a entrenar i combinar etiquetadors. Cada cop que volíem etiquetar una oració, entrenàvem un etiquetador i després etiquetàvem. Com que l'entrenament és lent, ens interessa entrenar una vegada i poder guardar l'etiquetador ja entrenat de manera que el puguem fer servir tantes vegades com vulguem.
En el programa-5-15.py entrenen i emmagatzem un etiquetador. Fixeu-vos en tots els etiquetadors diferents que entrenem, com els combinem, i com finalment emmagatzem el darrer, que de fet està combinat amb tota la resta. Fixeu-vos també com fem servir el mòdul pickle. I també tingueu en compte que hem comentat les línies que limiten el nombre d'entrades del diccionari morfològic que fa servir per entrenar. Si veieu que triga molt a entrenar, torneu a descomentar aquestes línies:
import codecsimport nltkimport pickleentrada=codecs.open("catalanTagged_0_5000-utf-8.txt","r",encoding="utf-8")tagged_words=[]tagged_sents=[]tagged_sents_per_unigrams=[]cont=0for linia in entrada: #cont+=1 #if cont==10000: # break linia=linia.rstrip() if linia.startswith("<") or len(linia)==0: #nova linia if len(tagged_words)>0: tagged_sents.append(tagged_words) tagged_sents_per_unigrams.append(tagged_words) tagged_words=[] else: camps=linia.split(" ") forma=camps[0] lema=camps[1] etiqueta=camps[2] tupla=(forma,etiqueta) tagged_words.append(tupla)if len(tagged_words)>0: tagged_sents.append(tagged_words) tagged_sents_per_unigrams.append(tagged_words) tagged_words=[] diccionari=codecs.open("diccionari-cat.txt","r",encoding="utf-8")cont=0for linia in diccionari: #cont+=1 #if cont==10000: # break linia=linia.rstrip() camps=linia.split(" ") forma=camps[0] lema=camps[1] etiqueta=camps[2] tupla=(forma,etiqueta) tagged_words.append(tupla)tagged_sents_per_unigrams.append(tagged_words)default_tagger=nltk.DefaultTagger("NP00000")affix_tagger=nltk.AffixTagger(tagged_sents_per_unigrams, affix_length=-3, min_stem_length=2,backoff=default_tagger)unigram_tagger_diccionari=nltk.UnigramTagger(tagged_sents_per_unigrams,backoff=affix_tagger)unigram_tagger=nltk.UnigramTagger(tagged_sents,backoff=unigram_tagger_diccionari)bigram_tagger=nltk.BigramTagger(tagged_sents,backoff=unigram_tagger)trigram_tagger=nltk.TrigramTagger(tagged_sents,backoff=bigram_tagger)sortida=open('etiquetador-cat.pkl', 'wb')pickle.dump(trigram_tagger, sortida, -1)sortida.close()El que és important tenir en compte en el programa anterior és que estem proporcionant un corpus i un diccionari. El corpus ens proporciona informació de forma i etiqueta dins del context de l'oració. En canvi, el diccionari només ens dona informació sobre formes i les seves etiquetes d'una manera totalment fora de context, ja que la paraula que apareix al diccionari abans d'una altra només guarda una relació alfabètica. Per aquest motiu, en tots els entrenaments que suposin un context (bigrams i trigrams) només podem fer servir la informació que prové del corpus. En canvi, per als entrenaments que no es tingui en compte el context (afixos i unigrames) podem fer servir tant la informació que apareix en el corpus com la que apareix al diccionari. Fixeu-vos que al programa fem servir dues llistes per emmagatzemar la informació que fem servir per entrenar:
Les línies:
cont=0for linia in diccionari: #cont+=1 #if cont==10000: # breakserveixen per limitar la informació que es carrega o bé del corpus o bé del diccionari, o de tots dos. Com que el programa triga molt a executar-se, podeu descomentar (treure el símbol "#") del davant de les línies.
Adjunto el etiquetador-cat.pkl resultant per a que el pugeu fer servir en el següent programa sense esperar que es completi l'entrenament.
Ara a etiquetador-cat.pkl tenim un etiquetador que podem carregar sempre que vulguem de manera molt ràpida. Fixeu-vos en el programa-5-16:
import nltk
import pickleimport nltkentrada=open('etiquetador-cat.pkl','rb')etiquetador=pickle.load(entrada)entrada.close()oracio="l'àcid desoxiribonucleic (ADN o DNA) és un àcid nucleic que conté les instruccions genètiques utilitzades en el desenvolupament i funcionament de tots els éssers vius coneguts, així com en alguns virus, des d'un punt de vista químic, l'ADN es compon de dos llargs polímers d'unitats simples anomenades nucleòtids, amb un tronc compost de sucres i grups fosfats units per enllaços èster"tokenitzador=nltk.tokenize.RegexpTokenizer('[ldsmLDSM]\'|\w+|[^\w\s]+')tokens=tokenitzador.tokenize(oracio)analisi=etiquetador.tag(tokens)print(analisi)Que ofereix l'anàlisi:
[("l'", 'DA0CS0'), ('àcid', 'NCMS000'), ('desoxiribonucleic', 'AQ0MS0'), ('(', 'Fpa'), ('ADN', 'NP00000'), ('o', 'CC'), ('DNA', 'NP00000'), (')', 'Fpt'), ('és', 'VSIP3S0'), ('un', 'DI0MS0'), ('àcid', 'NCMS000'), ('nucleic', 'AQ0MS0'), ('que', 'PR0CN000'), ('conté', 'VMIP3S0'), ('les', 'DA0FP0'), ('instruccions', 'NCFP000'), ('genètiques', 'AQ0FP0'), ('utilitzades', 'VMP00PF'), ('en', 'SPS00'), ('el', 'DA0MS0'), ('desenvolupament', 'NCMS000'), ('i', 'CC'), ('funcionament', 'NCMS000'), ('de', 'SPS00'), ('tots', 'DI0MP0'), ('els', 'DA0MP0'), ('éssers', 'NCMP000'), ('vius', 'AQ0MP0'), ('coneguts', 'NCMP000'), (',', 'Fc'), ('així', 'RG'), ('com', 'CS'), ('en', 'SPS00'), ('alguns', 'DI0MP0'), ('virus', 'NCMN000'), (',', 'Fc'), ('des', 'SPS00'), ("d'", 'SPS00'), ('un', 'DI0MS0'), ('punt', 'NCMS000'), ('de', 'SPS00'), ('vista', 'NCCS000'), ('químic', 'AQ0MS0'), (',', 'Fc'), ("l'", 'DA0CS0'), ('ADN', 'NP00000'), ('es', 'P0300000'), ('compon', 'VMIP3S0'), ('de', 'SPS00'), ('dos', 'Z'), ('llargs', 'AQ0MP0'), ('polímers', 'NCMP000'), ("d'", 'SPS00'), ('unitats', 'NCFP000'), ('simples', 'AQ0CP0'), ('anomenades', 'NCFP000'), ('nucleòtids', 'NCMP000'), (',', 'Fc'), ('amb', 'SPS00'), ('un', 'DI0MS0'), ('tronc', 'NCMS000'), ('compost', 'NCMS000'), ('de', 'SPS00'), ('sucres', 'NCMP000'), ('i', 'CC'), ('grups', 'NCMP000'), ('fosfats', 'NCMP000'), ('units', 'VMP00PM'), ('per', 'SPS00'), ('enllaços', 'NCMP000'), ('èster', 'NCMS000')]En els apartats anteriors he après a crear etiquetadors estadístics. També hem après a fer-los servir per etiquetar textos i intuïtivament hem vist que funcionen força bé, tot i que poden etiquetar malament algunes paraules. Quan desenvolupem un etiquetador, ens interessaria saber quina precisió assolim. NLTK ens proporciona una manera molt senzilla d'avaluar etiquetadors. Comencem avaluant un etiquetador per a l'anglès (programa-5-17.py)
import nltkfrom nltk.corpus import brownbrown_tagged_sents=brown.tagged_sents()print("TOTAL ORACIONS:", len(brown_tagged_sents))train_sents=brown_tagged_sents[:10000]test_sents=brown_tagged_sents[56001:]default_tagger=nltk.DefaultTagger("NN")affix_tagger=nltk.AffixTagger(train_sents, affix_length=-3, min_stem_length=2)unigram_tagger=nltk.UnigramTagger(train_sents, backoff=affix_tagger)bigram_tagger=nltk.BigramTagger(train_sents, backoff=unigram_tagger)trigram_tagger=nltk.TrigramTagger(train_sents, backoff=bigram_tagger)precisio=trigram_tagger.evaluate(test_sents)print("PRECISIO: ",precisio)El programa primer ens indicarà el nombre total d'oracions del corpus. Fixeu-vos que fa un conjunt d'oracions d'entrenament (train_sents) amb les primeres 10000 oracions del corpus; i un de test amb les oracions finals, de la 56001 fins la final. Després de crear l'etiquetador amb el mètode evaluate avalua la precisió fent servir les oracions de test. El que fa el programa és etiquetar aquestes oracions i comparar-les amb les etiquetes reals. Si executeu el programa, obtindreu la següent sortida:
TOTAL ORACIONS: 57340PRECISIO: 0.8937947951400866Canvieu ara el nombre d'oracions per entrenar l'etiquetador i poseu el màxim (sense agafar oracions de test), és a dir, 56000. Si executeu ara el programa, la precisió passa a ser de:
PRECISIO: 0.9220420834770611Veiem que quan més gran sigui el corpus d'entrenament, obtindrem una millor precisió.
Anem a avaluar l'etiquetador del català que hem entrenat i emmagatzemat en l'apartat anterior. Ho fem en el programa-5-18.py.
import nltkimport pickleimport codecs#carreguem l'etiquetadorentrada=open('etiquetador-cat.pkl','rb')etiquetador=pickle.load(entrada)entrada.close()#carreguem les oracions del corpus de testentrada=codecs.open("wikicorpus-tagged-test.txt","r",encoding="utf-8")tagged_words=[]test_tagged_sents=[]for linia in entrada: linia=linia.rstrip() if linia.startswith("<") or len(linia)==0: #nova linia if len(tagged_words)>0: test_tagged_sents.append(tagged_words) tagged_words=[] else: camps=linia.split(" ") forma=camps[0] lema=camps[1] etiqueta=camps[2] tupla=(forma,etiqueta) tagged_words.append(tupla)precisio=etiquetador.evaluate(test_tagged_sents)print("PRECISIO: ",precisio)Si ens fixem, fem servir un fragment nou del wikicorpus ja etiquetat (wikicorpus-tagged-test.txt). Primer carreguem l'etiquetador emmagatzemat i després carreguem el nou corpus afegint-lo al test_tagged_sents, que són les que farem servir per avaluar al mètode evaluate.
Aquest programa ens proporciona la següent sortida:
PRECISIO: 0.9585872792623847
Recordem que el corpus que estem fent servir és un corpus etiquetat automàticament amb Freeling, i per tant, el 95.8% de precisió que obtenim no és real. Hauríem de fer servir corpus etiquetats manualment, o bé etiquetats automàticament i revisats.