Para etiquetar una secuencia reducimos este problema a un problema de clasificación. Una historia es una instancia de clasificación, que es una tupla con los siguientes elementos:
sent
: La oración entera.prev_tags
: Una tupla con las n
etiquetas anterioresi
: El índice de la palabra a etiquetar.from collections import namedtuple
History = namedtuple('History', 'sent prev_tags i')
Por ejemplo, para la oración "El gato come pescado ." con n = 2
tenemos las siguientes historias válidas:
sent = 'El gato come pescado .'.split()
histories = [
History(sent, ('<s>', '<s>'), 0),
History(sent, ('<s>', 'D'), 1),
History(sent, ('D', 'N'), 2),
History(sent, ('N', 'V'), 3),
History(sent, ('V', 'N'), 4)
]
histories
[History(sent=['El', 'gato', 'come', 'pescado', '.'], prev_tags=('<s>', '<s>'), i=0), History(sent=['El', 'gato', 'come', 'pescado', '.'], prev_tags=('<s>', 'D'), i=1), History(sent=['El', 'gato', 'come', 'pescado', '.'], prev_tags=('D', 'N'), i=2), History(sent=['El', 'gato', 'come', 'pescado', '.'], prev_tags=('N', 'V'), i=3), History(sent=['El', 'gato', 'come', 'pescado', '.'], prev_tags=('V', 'N'), i=4)]
Para cada instancia de clasificación (historias) debemos calcular un conjunto de características relevantes (features) para la clasificación.
Por ejemplo, podemos usar como feature la palabra a etiquetar en minúsculas (un feature de tipo string):
def word_lower(h):
"""Feature: current lowercased word.
h -- a history.
"""
sent, i = h.sent, h.i
return sent[i].lower()
[word_lower(h) for h in histories]
['el', 'gato', 'come', 'pescado', '.']
También podemos usar como feature si la palabra comienza con mayúsculas o no (un feature de tipo booleano):
def word_istitle(h):
"""Feature: is the current word titlecased?
h -- a history.
"""
sent, i = h.sent, h.i
return sent[i].istitle()
[word_istitle(h) for h in histories]
[True, False, False, False, False]
Para usar los features que definimos debemos construir un vectorizador, que convierta los valores de los features en entradas en una matriz:
from featureforge.vectorizer import Vectorizer
features = [word_lower, word_istitle]
vect = Vectorizer(features)
Como cualquier componente de scikit-learn, el vectorizador debe entrenarse para definir los features concretos y el mapeo a columnas de una matriz:
vect.fit(histories)
<featureforge.vectorizer.Vectorizer at 0x7f7d546b5048>
Luego podemos vectorizar cualquier historia, como por ejemplo la que corresponde a etiquetar la primer palabra de la oración "Come salmón el mormón .".
h = History('Come salmón el mormón .'.split(), ('<s>', '<s>'), 0)
m = vect.transform([h])
m.toarray()
array([[1., 0., 0., 1., 0., 0.]])
Los features activos son el de la columna 0 y el de la columna 3. Podemos ver qué significa cada uno de ellos:
print(vect.column_to_feature(0))
print(vect.column_to_feature(0)[0]._name)
print(vect.column_to_feature(3))
print(vect.column_to_feature(3)[0]._name, vect.column_to_feature(3)[1])
(<featureforge.feature.Feature object at 0x7f7d2579bcf8>, None) word_istitle (<featureforge.feature.Feature object at 0x7f7d54718978>, 'come') word_lower come
La columna 0 indica que la palabra comienza en mayúsculas, mientras que la columna 3 indica que la palabra en minúsculas es "come".
Algunos features pueden tener parámetros que permitan diferentes instanciaciones. Estos features se pueden definir como subclases de la clase Feature
. Por ejemplo, podemos definir un feature booleano que indica si una palabra es más larga que un número dado:
from featureforge.feature import Feature
class WordLongerThan(Feature):
def __init__(self, n):
self.n = n
self._name = 'word_longer_than_{}'.format(n)
def _evaluate(self, h):
"""Feature: is the current word longer than n?
h -- a history.
"""
sent, i = h.sent, h.i
return len(sent[i]) > self.n
Una vez definido, podemos instanciar el feature con un valor particular:
word_longer_than_two = WordLongerThan(2)
Luego, podemos usarlo como un feature normal, por ejemplo evaluándolo sobre las historias o incluyéndolo en un vectorizador. Por ejemplo, lo evaluamos para las historias correspondientes a la oración "El gato come pescado.":
list(zip('El gato come pescado .'.split(), [word_longer_than_two(h) for h in histories]))
[('El', False), ('gato', True), ('come', True), ('pescado', True), ('.', False)]
También podemos agregar el feature al vectorizador:
features = [word_lower, word_istitle, word_longer_than_two]
vect = Vectorizer(features)
vect.fit(histories)
h = History('Come salmón el mormón .'.split(), ('<s>', '<s>'), 0)
m = vect.transform([h])
m.toarray()
array([[1., 1., 0., 0., 1., 0., 0.]])
En este caso la segunda columna corresponde al nuevo feature:
vect.column_to_feature(1)[0]._name
'word_longer_than_2'
Construimos un pipeline que se conforma del vectorizador y un clasificador:
from sklearn.pipeline import Pipeline
from sklearn.naive_bayes import MultinomialNB
clf = MultinomialNB() # probar acá otros clasificadores!
pipeline = Pipeline([
('vect', vect),
('clf', clf)
])
Entrenamos el pipeline con datos supervisados:
sent1 = 'El gato come pescado .'.split()
sent2 = 'La gata come salmón .'.split()
histories = [
History(sent1, ('<s>', '<s>'), 0),
History(sent1, ('<s>', 'D'), 1),
History(sent1, ('D', 'N'), 2),
History(sent1, ('N', 'V'), 3),
History(sent1, ('V', 'N'), 4),
History(sent2, ('<s>', '<s>'), 0),
History(sent2, ('<s>', 'D'), 1),
History(sent2, ('D', 'N'), 2),
History(sent2, ('N', 'V'), 3),
History(sent2, ('V', 'N'), 4)
]
labels = 'D N V N P'.split() + 'D N V N P'.split()
pipeline.fit(histories, labels)
Pipeline(memory=None, steps=[('vect', <featureforge.vectorizer.Vectorizer object at 0x7f7d257442e8>), ('clf', MultinomialNB(alpha=1.0, class_prior=None, fit_prior=True))])
Ahora ya podemos etiquetar nuevas oraciones usando la función predict:
new_sent = 'El gato come salmón .'.split()
prev = ('<s>', '<s>')
for i, w in enumerate(new_sent):
h = History(new_sent, prev, i)
tag = pipeline.predict([h])[0]
print(w, tag)
prev = (prev + (tag,))[1:]
El D gato N come V salmón N . P