Questo è il primo articolo di quella che diventerà una serie di tutorial relativi alla classificazione dei documenti in linguaggio naturale, al fine di realizzare l’analisi del sentiment e, in definitiva, un filtro per il trading automatico o per la generazione dei segnali. Questo specifico articolo descrive l’uso delle di Support Vector Machines (SVM) per classificare i documenti di testo in gruppi che si escludono a vicenda.

Classificazione dei Documenti per il Trading Quantitativo

Esiste un numero significativo di passi da eseguire tra la visualizzazione di un documento di testo su un sito Web, ad esempio, e l’utilizzo del suo contenuto come input per una strategia di trading automatica per generare filtri o segnali di trading. In particolare, devono essere eseguite le seguenti operazioni:

  • Automatizzare il download di più articoli generati continuamente da fonti esterne tramite una elevata velocità di esecuzione.
  • Analizzare le sezioni rilevanti di testo / informazioni di questi documenti che richiedono analisi, anche nel caso di formati diversi tra i documenti.
  • Convertire paragrafi di testo arbitrariamente lunghi (attraverso molte lingue possibili) in una struttura dati coerente che può essere compresa da un sistema di classificazione.
  • Determinare un insieme di gruppi (o etichette) dove poter inserire ogni documento. Ad esempio, possono essere “positivo” e “negativo” o “rialzista” e “ribassista”.
  • Creare un “training corpus” di documenti a cui sono associate etichette note. Ad esempio, un migliaio di articoli finanziari potrebbe dover essere etichettato con le etichette “rialzista” o “ribassista”
  • Addestrare i classificatori su questo corpus mediante una libreria software come scikit-learn di Python (che useremo di seguito)
  • Utilizzare il classificatore per etichettare nuovi documenti, in modo automatico e continuo.
  • Valutare il “tasso di classificazione” e altre metriche di rendimento associate al classificatore
  • Integrare il classificatore in un sistema di trading automatico, filtrando altri segnali di trading o generandone di nuovi.
  • Monitorare continuamente il sistema e regolarlo secondo necessità, se le sue prestazioni iniziano a peggiorare

In questo articolo eviteremo di  descrivere come scaricare articoli da diverse fonti esterne e faremo uso direttamente di un dataset di dati già fornito con le proprie etichette. Questo ci permetterà di concentrarci sull’attuazione della “pipeline di classificazione”, piuttosto che dedicare una notevole quantità di tempo all’ottenimento e all’etichettatura dei documenti.

Negli articoli successivi di questa serie faremo uso delle librerie Python, come ScraPy e BeautifulSoup per ottenere automaticamente molti articoli basati sul web ed estrarre efficacemente i loro dati basati sul testo dall’HTML.

Inoltre non considereremo, all’interno di questo specifico articolo, come integrare un tale classificatore in un sistema di trading algoritmico pronto per andare live. Tuttavia, questo aspetto sarà oggetto di articoli successivi.
È estremamente importante non solo creare esempi “studio”, come in questo articolo, ma anche discutere su come integrare completamente un classificatore in un sistema che potrebbe essere utilizzato in produzione. Quindi gli articoli successivi considereranno l’implementazione in un sistema reale.

Supponiamo quindi di avere un corpus di documenti pre-etichettato (da delineare di seguito!), iniziamo prendendo il training corpus e lo includiamo in una struttura dati Python adatta alla pre-elaborazione e l’utilizzo tramite il classificatore.

Tuttavia, prima di essere in grado di entrare nei dettagli di questo processo, dobbiamo discutere brevemente i concetti di classificazione supervisionata e macchine vettoriali di supporto.

Classificazione supervisionata e Macchine Vettoriali di Supporto

Per una panoramica più approfondita su concetti base dell’apprendimento automatico statistico, puoi consultare questo articolo.

Classificatori Supervisionati

I classificatori supervisionati sono un gruppo di tecniche di apprendimento automatico statistico che tentano di associare una “classe”, o “etichetta”, a un particolare insieme di funzionalità, sulla base di etichette note in precedenza collegate ad altri set di features simili.

Questa è chiaramente una definizione abbastanza astratta, quindi può essere utile avere un esempio. Consideriamo una serie di documenti di testo. Ogni documento è associato ad insieme di parole, che chiameremo “features” o caratteristiche. Ciascuno di questi documenti potrebbe essere associato a un’etichetta che descrive l’argomento dell’articolo.

Ad esempio, una serie di articoli da un sito web che parlano di animali domestici potrebbe contenere articoli che riguardano principalmente cani, gatti o criceti. Alcune parole, come “gabbia” (criceto), “guinzaglio” (cane) o “latte” (gatto) potrebbero essere più rappresentative di alcuni animali domestici rispetto ad altri. I classificatori supervisionati sono in grado di isolare alcune parole rappresentative di determinate etichette (animali) “apprendendo” da una serie di articoli di “addestramento”, che sono già pre-etichettati, spesso in modo manuale, da un essere umano.

Matematicamente, ciascuno degli \(j\) articoli sugli animali domestici all’interno di un corpus di addestramento è associato ad un vettore \(j\) di features, le cui componenti rappresentano la “forza” delle parole (in seguito definiremo il concetto di “forza”). Ogni articolo è associata anche un’etichetta di classe, \(j\), che in questo caso sarebbe il nome dell’animale più associato all’articolo.

La “supervisione” della procedura di addestramento si verifica quando un modello viene addestrato o si adatta a questi dati particolari. Nell’esempio seguente useremo la Support Vector Machine come nostro modello e la “addestreremo” su un corpus (una raccolta di documenti) generato in precedenza.

Support Vector Machines

Per una panoramica matematica più approfondita e completa di come funzionano le Support Vector Machines, si può consultare questo articolo.

Le Support Vector Machine sono una sottoclasse di classificatori supervisionati che tentano di suddividere uno spazio di elementi in due o più gruppi, cioè nel nostro caso significa separare una raccolta di articoli in due o più etichette di classe.

Gli SVM ottengono questo risultato trovando un mezzo ottimale per separare tali gruppi in base alle loro etichette di classe già note. Nei casi più semplici, il “confine” di separazione è lineare e quindi si ottiene due o più gruppi che sono divisi da linee (o piani) in spazi multi-dimensionali.

Nei casi più complicati (dove i gruppi non sono ben separati da linee o piani), gli SVM sono in grado di eseguire partizioni non lineari. Ciò si ottiene mediante un metodo kernel. In definitiva, questo li rende classificatori molto sofisticati e capaci, ma con il solito svantaggio di poter essere soggetti a overfitting. Maggiori dettagli possono essere trovati qui.

Nella figura seguente ci sono due esempi di limiti decisionali non lineari (rispettivamente kernel polinomiale e kernel radiale) per due etichette di classe (arancione e blu), attraverso due features [lavel]X_1[/label] e [label]X_2[/label].

Gli SVM sono potenti classificatori se usati correttamente e possono fornire risultati molto promettenti. Utilizzeremo SVM per il resto di questo articolo.

trading-machine-learning-svm-0010
Confini decisionali di Support Vector Machine per due diversi kernel

Preparare un Dataset per la Classificazione

Un famoso set di dati utilizzato nella progettazione della classificazione dell’apprendimento automatico è il set Reuters 21578. È uno dei set di dati di test più utilizzati per la classificazione del testo, ma oggigiorno è un po ‘obsoleto. Tuttavia, per gli scopi di questo articolo sarà più che sufficiente.

Il set è costituito da una raccolta di articoli di notizie (un “corpus”) contrassegnati da una selezione di argomenti e posizioni geografiche.E’ quindi “ready made” per essere utilizzato nei test di classificazione, poiché è già pre-etichettato.

Ora vediamo come scaricare, estrarre e preparare il set di dati. Sto effettuando questo tutorial su una macchina Ubuntu 18.04, quindi ho accesso al terminal di riga di comando. Se usi Linux o Mac OSX potrai tranquillamente seguire questi comandi. Se usi Windows, dovrei scaricare uno strumento di estrazione Tar / GZIP per ottenere i dati.

Il set di dati Reuters 21578 può essere scaricato da questo link, in formato tar GZIP compresso. La prima operazione da fare è creare una nuova directory di lavoro e scaricare il file al suo interno. Puoi modificare il nome della directory come meglio credi:

cd ~
mkdir -p datatrading/classification/data
cd datatrading/classification/data
wget http://kdd.ics.uci.edu/databases/reuters21578/reuters21578.tar.gz
Possiamo quindi decomprimere il file:
tar -zxvf reuters21578.tar.gz
Se elenchiamo il contenuto della directory (ls -l) possiamo vedere quanto segue (ho rimosso i permessi e i dettagli di proprietà per brevità):
...     186 Dec  4  1996 all-exchanges-strings.lc.txt
...     316 Dec  4  1996 all-orgs-strings.lc.txt
...    2474 Dec  4  1996 all-people-strings.lc.txt
...    1721 Dec  4  1996 all-places-strings.lc.txt
...    1005 Dec  4  1996 all-topics-strings.lc.txt
...   28194 Dec  4  1996 cat-descriptions_120396.txt
...  273802 Dec 10  1996 feldman-cia-worldfactbook-data.txt
...    1485 Jan 23  1997 lewis.dtd
...   36388 Sep 26  1997 README.txt
... 1324350 Dec  4  1996 reut2-000.sgm
... 1254440 Dec  4  1996 reut2-001.sgm
... 1217495 Dec  4  1996 reut2-002.sgm
... 1298721 Dec  4  1996 reut2-003.sgm
... 1321623 Dec  4  1996 reut2-004.sgm
... 1388644 Dec  4  1996 reut2-005.sgm
... 1254765 Dec  4  1996 reut2-006.sgm
... 1256772 Dec  4  1996 reut2-007.sgm
... 1410117 Dec  4  1996 reut2-008.sgm
... 1338903 Dec  4  1996 reut2-009.sgm
... 1371071 Dec  4  1996 reut2-010.sgm
... 1304117 Dec  4  1996 reut2-011.sgm
... 1323584 Dec  4  1996 reut2-012.sgm
... 1129687 Dec  4  1996 reut2-013.sgm
... 1128671 Dec  4  1996 reut2-014.sgm
... 1258665 Dec  4  1996 reut2-015.sgm
... 1316417 Dec  4  1996 reut2-016.sgm
... 1546911 Dec  4  1996 reut2-017.sgm
... 1258819 Dec  4  1996 reut2-018.sgm
... 1261780 Dec  4  1996 reut2-019.sgm
... 1049566 Dec  4  1996 reut2-020.sgm
...  621648 Dec  4  1996 reut2-021.sgm
... 8150596 Mar 12  1999 reuters21578.tar.gz
Vedrai che tutti i file che iniziano con reut2- sono .sgm, il che significa che sono file SGML. Sfortunatamente, Python ha deprecato sgmllib a partire da Python 2.6 e lo ha completamente rimosso in Python 3. Tuttavia, non tutto è perduto perché possiamo creare la nostra classe SGML Parser che sovrascrive quella HTMLParser incorporata in Python. Ecco un singolo elemento presente in uno dei file:
..
..
<REUTERS TOPICS="YES" LEWISSPLIT="TRAIN" CGISPLIT="TRAINING-SET" OLDID="5544" NEWID="1">
<DATE>26-FEB-1987 15:01:01.79</DATE>
<TOPICS><D>cocoa</D></TOPICS>
<PLACES><D>el-salvador</D><D>usa</D><D>uruguay</D></PLACES>
<PEOPLE></PEOPLE>
<ORGS></ORGS>
<EXCHANGES></EXCHANGES>
<COMPANIES></COMPANIES>
<UNKNOWN> 
C T
f0704reute
u f BC-BAHIA-COCOA-REVIEW   02-26 0105</UNKNOWN>
<TEXT>
<TITLE>BAHIA COCOA REVIEW</TITLE>
<DATELINE>    SALVADOR, Feb 26 - </DATELINE>
<BODY>Showers continued throughout the week in
the Bahia cocoa zone, alleviating the drought since early
January and improving prospects for the coming temporao,
although normal humidity levels have not been restored,
Comissaria Smith said in its weekly review.
    The dry period means the temporao will be late this year.
    Arrivals for the week ended February 22 were 155,221 bags
of 60 kilos making a cumulative total for the season of 5.93
mln against 5.81 at the same stage last year. Again it seems
that cocoa delivered earlier on consignment was included in the
arrivals figures.
    Comissaria Smith said there is still some doubt as to how
much old crop cocoa is still available as harvesting has
practically come to an end. With total Bahia crop estimates
around 6.4 mln bags and sales standing at almost 6.2 mln there
are a few hundred thousand bags still in the hands of farmers,
middlemen, exporters and processors.
    There are doubts as to how much of this cocoa would be fit
for export as shippers are now experiencing dificulties in
obtaining +Bahia superior+ certificates.
    In view of the lower quality over recent weeks farmers have
sold a good part of their cocoa held on consignment.
    Comissaria Smith said spot bean prices rose to 340 to 350
cruzados per arroba of 15 kilos.
    Bean shippers were reluctant to offer nearby shipment and
only limited sales were booked for March shipment at 1,750 to
1,780 dlrs per tonne to ports to be named.
    New crop sales were also light and all to open ports with
June/July going at 1,850 and 1,880 dlrs and at 35 and 45 dlrs
under New York july, Aug/Sept at 1,870, 1,875 and 1,880 dlrs
per tonne FOB.
    Routine sales of butter were made. March/April sold at
4,340, 4,345 and 4,350 dlrs.
    April/May butter went at 2.27 times New York May, June/July
at 4,400 and 4,415 dlrs, Aug/Sept at 4,351 to 4,450 dlrs and at
2.27 and 2.28 times New York Sept and Oct/Dec at 4,480 dlrs and
2.27 times New York Dec, Comissaria Smith said.
    Destinations were the U.S., Covertible currency areas,
Uruguay and open ports.
    Cake sales were registered at 785 to 995 dlrs for
March/April, 785 dlrs for May, 753 dlrs for Aug and 0.39 times
New York Dec for Oct/Dec.
    Buyers were the U.S., Argentina, Uruguay and convertible
currency areas.
    Liquor sales were limited with March/April selling at 2,325
and 2,380 dlrs, June/July at 2,375 dlrs and at 1.25 times New
York July, Aug/Sept at 2,400 dlrs and at 1.25 times New York
Sept and Oct/Dec at 1.25 times New York Dec, Comissaria Smith
said.
    Total Bahia sales are currently estimated at 6.13 mln bags
against the 1986/87 crop and 1.06 mln bags against the 1987/88
crop.
    Final figures for the period to February 28 are expected to
be published by the Brazilian Cocoa Trade Commission after
carnival which ends midday on February 27.
 Reuter
</BODY></TEXT>
</REUTERS>
..
..

Sebbene possa essere alquanto laborioso analizzare i dati in questo modo, specialmente se confrontati con l’effettivo apprendimento automatico, posso assicurarti che gran parte della giornata di un data scientist o di un ricercatore quantistico consiste nell’ottenere effettivamente i dati in un formato utilizzabile dai software di analisi! Questa particolare attività viene spesso chiamata scherzosamente “data wrangling”. Quindi è opportuno fare un po ‘di pratica!

Se diamo un’occhiata al file topics, all-topics-strings.lc.txt, digitando less all-topics-strings.lc.tx possiamo vedere quanto segue (per sintesi ne ho rimosso la maggior parte):

acq
alum
austdlr
austral
barley
bfr
bop
can
carcass
castor-meal
castor-oil
castorseed
citruspulp
cocoa
coconut
coconut-oil
coffee
copper
copra-cake
corn
...
...
silver
singdlr
skr
sorghum
soy-meal
soy-oil
soybean
stg
strategic-metal
sugar
sun-meal
sun-oil
sunseed
tapioca
tea
tin
trade
tung
tung-oil
veg-oil
wheat
wool
wpi
yen
zinc
Eseguendo il comando cat all-topics-strings.lc.txt | wc -l possiamo vedere che ci sono 135 argomenti separati tra gli articoli. Ciò rappresenterà la vera sfida della classificazione! In questa fase dobbiamo creare quello che è noto come un elenco di coppie predittore-risposta. Questo è un elenco di due tuple che contengono l’etichetta di classe più appropriata e il testo del documento non elaborato, come due componenti separati. Ad esempio, l’obbiettivo dell’analisi è ottenere una struttura dati simile alla seguente:
[
    ("cat", "It is best not to give them too much milk"),
    ("dog", "Last night we took him for a walk, but he had to remain on the leash"),
    ..
    ..
    ("hamster", "Today we cleaned out the cage and prepared the sawdust"),
    ("cat", "Kittens require a lot of attention in the first few months")
]

Per creare questa struttura è necessario analizzare individualmente tutti i file Reuters e aggiungerli a un grande elenco di “corpus”. Poiché la dimensione del file del corpus è piuttosto bassa, si adatterà facilmente alla RAM disponibile sulla maggior parte dei laptop / desktop moderni.

Tuttavia, nelle applicazioni in produzione è solitamente necessario trasmettere i dati di addestramento in un sistema di apprendimento automatico ed eseguire un “adattamento parziale” su ciascun lotto, in modo iterativo. Negli articoli successivi descriveremo questo scenario quando studieremo set di dati estremamente grandi (in particolare i dati tick).

Come affermato in precedenza, il nostro primo obiettivo è creare l’SGML Parser che raggiunga effettivamente questo obiettivo. Per fare ciò, ereditiamo la classe HTMLParser di Python per gestire i specifici tag nel set di dati Reuters.

Quando si eridita la classe HTMLParser, dobbiamo sovrascrivere tre metodi, handle_starttag, handle_endtag e handle_data, che dicono al parser cosa fare all’inizio dei tag SGML, cosa fare alla chiusura dei tag SGML e come gestire i dati intermedi.

Creiamo anche due metodi aggiuntivi, _reset e parse, che vengono utilizzati per monitorare lo stato interno della classe e per analizzare i dati effettivi in ​​modo frammentato, in modo da non utilizzare troppa memoria.

Infine, implementiamo una elementare funzione __main__ di per testare il parser sul primo set di dati all’interno del corpus Reuters.

Come per la maggior parte, se non tutti, dei codici presenti su DataTrading, ho inserito commenti parlanti in modo che si possa capire cosa si sta implementando ad ogni passaggio:

import html
import pprint
import re
from html.parser import HTMLParser


class ReutersParser(HTMLParser):
    """
    ReutersParser è una sottoclasse HTMLParser e viene utilizzato per aprire file SGML
    associati al dataset di Reuters-21578.

    Il parser è un generatore e produrrà un singolo documento alla volta.
    Poiché i dati verranno suddivisi in blocchi durante l'analisi, è necessario mantenere
    alcuni stati interni di quando i tag sono stati "inseriti" e "eliminati".
    Da qui le variabili booleani in_body, in_topics e in_topic_d.
    """

    def __init__(self, encoding='latin-1'):
        """
        Inizializzo la superclasse (HTMLParser) e imposto il parser.
        Imposto la decodifica dei file SGML con latin-1 come default.
        """
        html.parser.HTMLParser.__init__(self)
        self._reset()
        self.encoding = encoding

    def _reset(self):
        """
        Viene chiamata solo durante l'inizializzazione della classe parser
        e quando è stata generata una nuova tupla topic-body. Si
        resetta tutto lo stato in modo che una nuova tupla possa essere
        successivamente generato.
        """
        self.in_body = False
        self.in_topics = False
        self.in_topic_d = False
        self.body = ""
        self.topics = []
        self.topic_d = ""

    def parse(self, fd):
        """
        parse accetta un descrittore di file e carica i dati in blocchi
        per ridurre al minimo l'utilizzo della memoria.
        Quindi produce nuovi documenti man mano che vengono analizzati.
        """
        self.docs = []
        for chunk in fd:
            self.feed(chunk.decode(self.encoding))
            for doc in self.docs:
                yield doc
            self.docs = []
        self.close()

    def handle_starttag(self, tag, attrs):
        """
        Questo metodo viene utilizzato per determinare cosa fare quando
        il parser incontra un particolare tag di tipo "tag".
        In questo caso, impostiamo semplicemente i valori booleani
        interni su True se è stato trovato quel particolare tag.
        """
        if tag == "reuters":
            pass
        elif tag == "body":
            self.in_body = True
        elif tag == "topics":
            self.in_topics = True
        elif tag == "d":
            self.in_topic_d = True

    def handle_endtag(self, tag):
        """
        Questo metodo viene utilizzato per determinare cosa fare
        quando il parser termina con un particolare tag di tipo "tag".

        Se il tag è un tag <REUTERS>, rimuoviamo tutti gli spazi bianchi
        con un'espressione regolare e quindi aggiungiamo la tupla topic-body.

        Se il tag è un tag <BODY> o <TOPICS>, impostiamo semplicemente lo
        stato interno su False per questi valori booleani, rispettivamente.

        Se il tag è un tag <D> (che si trova all'interno di un tag <TOPICS>),
        aggiungiamo l'argomento specifico all'elenco "topics" e infine lo resettiamo.
        """
        if tag == "reuters":
            self.body = re.sub(r'\s+', r' ', self.body)
            self.docs.append((self.topics, self.body))
            self._reset()
        elif tag == "body":
            self.in_body = False
        elif tag == "topics":
            self.in_topics = False
        elif tag == "d":
            self.in_topic_d = False
            self.topics.append(self.topic_d)
            self.topic_d = ""

    def handle_data(self, data):
        """
        I dati vengono semplicemente aggiunti allo stato appropriato
        per quel particolare tag, fino a quando non viene visualizzato
        il tag di chiusura finale.
        """
        if self.in_body:
            self.body += data
        elif self.in_topic_d:
            self.topic_d += data


if __name__ == "__main__":
    # Apre il primo set di dati Reuters e crea il parser
    filename = "data/reut2-000.sgm"
    parser = ReutersParser()

    # Analizza il documento e forza tutti i documenti generati
    # in un elenco in modo che possano essere stampati sulla console
    doc = parser.parse(open(filename, 'rb'))
    pprint.pprint(list(doc))
In questa fase vedremo una quantità significativa di output simile a questa:
..
..
(['grain', 'rice', 'thailand'],
  'Thailand exported 84,960 tonnes of rice in the week ended February 24, '
  'up from 80,498 the previous week, the Commerce Ministry said. It said '
  'government and private exporters shipped 27,510 and 57,450 tonnes '
  'respectively. Private exporters concluded advance weekly sales for '
  '79,448 tonnes against 79,014 the previous week. Thailand exported '
  '689,038 tonnes of rice between the beginning of January and February 24, '
  'up from 556,874 tonnes during the same period last year. It has '
  'commitments to export another 658,999 tonnes this year. REUTER '),
 (['soybean', 'red-bean', 'oilseed', 'japan'],
  'The Tokyo Grain Exchange said it will raise the margin requirement on '
  'the spot and nearby month for U.S. And Chinese soybeans and red beans, '
  'effective March 2. Spot April U.S. Soybean contracts will increase to '
  '90,000 yen per 15 tonne lot from 70,000 now. Other months will stay '
  'unchanged at 70,000, except the new distant February requirement, which '
  'will be set at 70,000 from March 2. Chinese spot March will be set at '
  '110,000 yen per 15 tonne lot from 90,000. The exchange said it raised '
  'spot March requirement to 130,000 yen on contracts outstanding at March '
  '13. Chinese nearby April rises to 90,000 yen from 70,000. Other months '
  'will remain unchanged at 70,000 yen except new distant August, which '
  'will be set at 70,000 from March 2. The new margin for red bean spot '
  'March rises to 150,000 yen per 2.4 tonne lot from 120,000 and to 190,000 '
  'for outstanding contracts as of March 13. The nearby April requirement '
  'for red beans will rise to 100,000 yen from 60,000, effective March 2. '
  'The margin money for other red bean months will remain unchanged at '
  '60,000 yen, except new distant August, for which the requirement will '
  'also be set at 60,000 from March 2. REUTER '),
..
..

In particolare, si tenga presente che invece di avere una singola etichetta di argomento associata a un documento, abbiamo più argomenti. Per aumentare l’efficacia del classificatore, è necessario assegnare una sola etichetta di classe a ciascun documento. Tuttavia, noterai anche che alcune delle etichette sono in realtà tag di posizione geografica, come “giappone” o “thailandia”. Poiché ci occupiamo esclusivamente di argomenti e non di paesi, desideriamo rimuoverli prima di selezionare il nostro argomento.

Lo specifico metodo che useremo per eseguire questa operazione è piuttosto semplice. Elimineremo i nomi dei paesi e quindi selezioneremo il primo argomento rimanente nell’elenco. Se non ci sono argomenti associati, elimineremo l’articolo dal nostro corpus. Nell’output sopra, questo si ridurrà a una struttura di dati che assomiglia a:

..
..
 ('grain',
  'Thailand exported 84,960 tonnes of rice in the week ended February 24, '
  'up from 80,498 the previous week, the Commerce Ministry said. It said '
  'government and private exporters shipped 27,510 and 57,450 tonnes '
  'respectively. Private exporters concluded advance weekly sales for '
  '79,448 tonnes against 79,014 the previous week. Thailand exported '
  '689,038 tonnes of rice between the beginning of January and February 24, '
  'up from 556,874 tonnes during the same period last year. It has '
  'commitments to export another 658,999 tonnes this year. REUTER '),
 ('soybean',
  'The Tokyo Grain Exchange said it will raise the margin requirement on '
  'the spot and nearby month for U.S. And Chinese soybeans and red beans, '
  'effective March 2. Spot April U.S. Soybean contracts will increase to '
  '90,000 yen per 15 tonne lot from 70,000 now. Other months will stay '
  'unchanged at 70,000, except the new distant February requirement, which '
  'will be set at 70,000 from March 2. Chinese spot March will be set at '
  '110,000 yen per 15 tonne lot from 90,000. The exchange said it raised '
  'spot March requirement to 130,000 yen on contracts outstanding at March '
  '13. Chinese nearby April rises to 90,000 yen from 70,000. Other months '
  'will remain unchanged at 70,000 yen except new distant August, which '
  'will be set at 70,000 from March 2. The new margin for red bean spot '
  'March rises to 150,000 yen per 2.4 tonne lot from 120,000 and to 190,000 '
  'for outstanding contracts as of March 13. The nearby April requirement '
  'for red beans will rise to 100,000 yen from 60,000, effective March 2. '
  'The margin money for other red bean months will remain unchanged at '
  '60,000 yen, except new distant August, for which the requirement will '
  'also be set at 60,000 from March 2. REUTER '),
..
..
Per rimuovere i tag geografici e selezionare il principale tag dell’argomento possiamo aggiungere il seguente codice:
...
...

def obtain_topic_tags():
    """
    Apre il file dell'elenco degli argomenti e importa tutti i nomi 
    degli argomenti facendo attenzione a rimuovere il finale "\ n" da ogni parola.
    """
    topics = open(
        "data/all-topics-strings.lc.txt", "r"
    ).readlines()
    topics = [t.strip() for t in topics]
    return topics


def filter_doc_list_through_topics(topics, docs):
    """
    Legge tutti i documenti e crea un nuovo elenco di due tuple che
    contengono una singola voce di funzionalità e il corpo del testo,
    invece di un elenco di argomenti. Rimuove tutte le caratteristiche
    geografiche e conserva solo quei documenti che hanno almeno un
    argomento non geografico.
    """
    ref_docs = []
    for d in docs:
        if d[0] == [] or d[0] == "":
            continue
        for t in d[0]:
            if t in topics:
                d_tup = (t, d[1])
                ref_docs.append(d_tup)
                break
    return ref_docs


if __name__ == "__main__":
    # Apre il primo set di dati Reuters e crea il parser
    filename = "data/reut2-000.sgm"
    parser = ReutersParser()

    # Analizza il documento e forza tutti i documenti generati
    # in un elenco in modo che possano essere stampati sulla console
    doc = parser.parse(open(filename, 'rb'))

    # Ottenere i tags e filtrare il documento con essi
    topics = obtain_topic_tags()
    ref_docs = filter_doc_list_through_topics(topics, docs)
    pprint.pprint(list(doc))

L’output è il seguente:

..
..
('acq',
  'Security Pacific Corp said it completed its planned merger with Diablo '
  'Bank following the approval of the comptroller of the currency. Security '
  'Pacific announced its intention to merge with Diablo Bank, headquartered '
  'in Danville, Calif., in September 1986 as part of its plan to expand its '
  'retail network in Northern California. Diablo has a bank offices in '
  'Danville, San Ramon and Alamo, Calif., Security Pacific also said. '
  'Reuter '),
 ('earn',
  'Shr six cts vs five cts Net 188,000 vs 130,000 Revs 12.2 mln vs 10.1 mln '
  'Avg shrs 3,029,930 vs 2,764,544 12 mths Shr 81 cts vs 1.45 dlrs Net '
  '2,463,000 vs 3,718,000 Revs 52.4 mln vs 47.5 mln Avg shrs 3,029,930 vs '
  '2,566,680 NOTE: net for 1985 includes 500,000, or 20 cts per share, for '
  'proceeds of a life insurance policy. includes tax benefit for prior qtr '
  'of approximately 150,000 of which 140,000 relates to a lower effective '
  'tax rate based on operating results for the year as a whole. Reuter '),
..
..
Siamo ora in grado di pre-elaborare i dati per l’input nel classificatore.

Vettorizzazione

In questa fase abbiamo una vasta raccolta di coppie di tuple, ciascuna coppia contenente un’etichetta di classe e un corpo di testo grezzo dagli articoli. La ovvia domanda da porsi è come poter convertire il corpo del testo grezzo in una rappresentazione di dati che può essere utilizzata da un classificatore (numerico)?

La risposta sta in un processo noto come vettorizzazione. La vettorizzazione consente la conversione di lunghezze molto variabili di testo grezzo in un formato numerico che può essere elaborato dal classificatore.

Si ottiene questo risultato creando alcuni token da una stringa. Un token è una singola parola (o gruppo di parole) estratta da un documento, utilizzando spazi bianchi o punteggiatura come separatori. Questo può, ovviamente, includere i numeri presenti all’interno della stringa come “parole” aggiuntive. Una volta creato questo elenco di token, è possibile assegnargli un identificatore intero, che consente loro di essere elencati.

Una volta che l’elenco dei token è stato generato, viene conteggiato il numero di token all’interno di un documento. Infine, questi token sono normalizzati per de-enfatizzare i token che appaiono frequentemente all’interno di un documento (come “a”, “the”). Questo processo è noto come Bag Of Words.

La rappresentazione Bag Of Words consente di associare un vettore a ciascun documento, ogni componente del quale è a valore reale e rappresenta l’importanza dei token (cioè “parole”) che compaiono all’interno di quel documento.

Inoltre, significa che dopo aver iterato un intero corpus di documenti (e quindi sono stati valutati tutti i possibili token), il numero totale di token separati è noto e quindi anche la lunghezza del vettore token, ed è anche fisso e identico per qualsiasi documento di qualsiasi lunghezza.

Ciò significa che il classificatore ha una serie di features tramite la frequenza di occorrenza del token. Inoltre il token-vector del documento rappresenta un campione per il classificatore.

In sostanza, l’intero corpus può essere rappresentato come una grande matrice, ogni riga della quale rappresenta uno dei documenti e ogni colonna rappresenta l’occorrenza del token all’interno di quel documento. Questo è il processo di vettorizzazione.

Si noti che la vettorizzazione non tiene conto del posizionamento relativo delle parole all’interno del documento, ma solo della frequenza di occorrenza. Tuttavia, tecniche di apprendimento automatico più sofisticate utilizzano questa informazioni per migliorare la classificazione.

Term-Frequency Inverse Document-Frequency

Uno dei problemi principali con la vettorizzazione, tramite la rappresentazione Bag Of Words, è che c’è molto “rumore” sotto forma di parole di arresto, come “un”, “il”, “lui”, “lei” ecc. Queste parole forniscono poco contesto al documento, ma la loro alta frequenza significa che possono mascherare parole che forniscono contesto al documento.

Ciò motiva un processo di trasformazione, noto come Term-Frequency Inverse Document-Frequency (TF-IDF). Il valore TF-IDF per un token aumenta proporzionalmente alla frequenza della parola nel documento ma è normalizzato dalla frequenza della parola nel corpus. Ciò riduce essenzialmente l’importanza per le parole che appaiono molto in generale, invece di apparire molto all’interno di un particolare documento.

Questo è esattamente ciò di cui abbiamo bisogno in quanto parole come “un”, “il” avranno occorrenze estremamente elevate all’interno dell’intero corpus, ma la parola “gatto” può apparire spesso solo in un particolare documento. Ciò significherebbe che stiamo dando a “gatto” una forza relativa maggiore di “un” o “lui”, per quel documento.

Non mi soffermerò sul calcolo del TF-IDF, ma se sei interessato leggi l’articolo di Wikipedia sull’argomento, che entra più in dettaglio.

Desideriamo quindi combinare il processo di vettorizzazione con quello di TF-IDF per produrre una matrice normalizzata di occorrenze documento-token. Questo verrà quindi utilizzato per fornire un elenco “features” al classificatore su cui allenarsi.

Per fortuna, gli sviluppatori di scikit-learn si sono resi conto che vettorializzare e trasformare i file di testo in questo modo sarebbe stata un’operazione estremamente utile e comune, quindi hanno incluso la classe TfidfVectorizer nella libreria.

Possiamo usare questa classe per prendere il nostro elenco di coppie di tuple che rappresentano le etichette di classe e il testo del documento grezzo, per produrre sia un vettore di etichette di classe che una matrice sparsa, che rappresentano rispettivamente la vettorizzazione applicata ai dati di testo non elaborati e la procedura TF-IDF.

Poiché i classificatori di scikit-learn prendono due strutture di dati separate per l’addestramento, vale a dire, \(y\), il vettore delle etichette di classe o “risposte” associate a un insieme ordinato di documenti, e, \(X\), la matrice sparsa TF-IDF del testo del documento, modifichiamo la nostra lista di coppie di tuple per creare \(y\) e \(X\). Il codice per creare questi oggetti è il seguente:

..
from sklearn.feature_extraction.text import TfidfVectorizer
..
..

def create_tfidf_training_data(docs):
    """
    Crea un elenco di corpus di documenti (rimuovendo le etichette della classe),
    quindi applica la trasformazione TF-IDF a questo elenco.

   La funzione restituisce sia il vettore etichetta di classe (y) che
   la matrice token / feature corpus (X).
    """
    # Crea le classi di etichette per i dati di addestramento
    y = [d[0] for d in docs]

    # Crea la lista dei corpus del documenti
    corpus = [d[1] for d in docs]

    # Create la vettorizzazione TF-IDF e trasforma il corpus
    vectorizer = TfidfVectorizer(min_df=1)
    X = vectorizer.fit_transform(corpus)
    return X, y


if __name__ == "__main__":
    # Apre il primo set di dati Reuters e crea il parser
    filename = "data/reut2-000.sgm"
    parser = ReutersParser()

    # Analizza il documento e forza tutti i documenti generati
    # in un elenco in modo che possano essere stampati sulla console
    docs = list(parser.parse(open(filename, 'rb')))

    # Ottenere i tags e filtrare il documento con essi
    topics = obtain_topic_tags()
    ref_docs = filter_doc_list_through_topics(topics, docs)

    # Vettorizzazione e TF-IDF
    X, y = create_tfidf_training_data(ref_docs)
A questo punto abbiamo due componenti per i nostri dati di addestramento. Il primo,[label]X[/label], è una matrice di occorrenze di token di documento. Il secondo, [label]y[/label], è un vettore (che corrisponde all’ordine della matrice) che contiene le corrette etichette di classe per ciascuno dei documenti. Questo è tutto ciò di cui abbiamo bisogno per iniziare l’addestramento e il test della Support Vector Machine.

Addestrare un Support Vector Machine

Per addestrare la Support Vector Machine è necessario fornirle sia un insieme di features (la matrice [label]X[/label]) sia un insieme di etichette di addestramento “supervisionato”, in questo caso le classi [label]$[/label]. Tuttavia, abbiamo anche bisogno di un mezzo per valutare le prestazioni del classificatore dopo la sua fase di addestramento.

Un approccio consiste nel provare semplicemente a classificare alcuni dei documenti che formano il corpus utilizzato per addestrarlo. Tale procedura di valutazione è nota come test in-sample. Tuttavia, questo non è un meccanismo particolarmente efficace per valutare le prestazioni del sistema.

In poche parole, il classificatore ha già “visto” questi dati e gli è stato detto come agire su di essi, quindi è molto probabile che classifichi correttamente il documento. Questo quasi certamente sovrastimerà le reali prestazioni di test out-of-sample. Quindi dobbiamo fornire al classificatore i dati che non ha utilizzato per l’addestramento, come mezzo di test più realistico.

Tuttavia, non è chiaro da dove ottenere questi nuovi dati. Un approccio potrebbe essere quello di creare un nuovo corpus con alcuni nuovi dati. Tuttavia, in realtà è probabile che ciò sia costoso in termini di tempo e / o processi aziendali. Un approccio alternativo consiste nel suddividere l’insieme di addestramento in due sottoinsiemi distinti, uno dei quali viene utilizzato per l’addestramento e l’altro per i test. Questo è noto come training-test split.

Tale partizione ci consente di addestrare il classificatore esclusivamente sulla prima partizione e quindi di classificare le sue prestazioni con la seconda partizione. Questo permette di avere una visione migliore circa le possibili prestazioni future con dati reali “out-of-sample”.

A questo punto ci si può domandare quale percentuale dei dati utilizzare per l’addestramento e quale per i  test. Chiaramente quanto più viene utilizzato per l’addestramento, tanto “migliore” sarà il classificatore perché avrà visto più dati. Tuttavia, più dati di addestramento significano meno dati di test e di conseguenza una stima più scarsa della sua reale capacità di classificazione. In pratica, è comune prevedere circa il 70-80% dei dati per l’addestramento e utilizzare il resto per i test.

Dato che il training-test split è un’operazione così comune nell’apprendimento automatico, gli sviluppatori di scikit-learn hanno fornito il metodo train_test_split per creare automaticamente la divisione da un dataset di input. Ecco il codice che fornisce la suddivisione:

from sklearn.model_selection import train_test_split
..
..
X_train, X_test, y_train, y_test = train_test_split(
  X, y, test_size=0.2, random_state=42
)

L’argomento della parola chiave test_size controlla la dimensione del set di test, in questo caso il 20%. L’argomento della parola chiave random_state controlla il fonte casuale per la selezione casuale della partizione.

Il passaggio successivo consiste nel creare effettivamente la Support Vector Machine e addestrarla. In questo caso useremo la classe SVC (Support Vector Classifier) ​​di scikit-learn. Gli diamo i parametri [label]C = 1000000.0[/label], [label]\gamma = 0.0[/label] e scegliamo un kernel radiale. Per capire da dove provengono questi parametri, consultare l’articolo su Support Vector Machines.

Il codice seguente importa la classe SVC e quindi la adatta ai dati di addestramento:

from sklearn.svm import SVC
..
..

def train_svm(X, y):
    """
    Crea e addestra la Support Vector Machine.
    """
    svm = SVC(C=1000000.0, gamma=0.0, kernel='rbf')
    svm.fit(X, y)
    return svm


if __name__ == "__main__":
    # Apre il primo set di dati Reuters e crea il parser
    filename = "data/reut2-000.sgm"
    parser = ReutersParser()

    # Analizza il documento e forza tutti i documenti generati
    # in un elenco in modo che possano essere stampati sulla console
    docs = list(parser.parse(open(filename, 'rb')))

    # Ottenere i tags e filtrare il documento con essi
    topics = obtain_topic_tags()
    ref_docs = filter_doc_list_through_topics(topics, docs)

    # Vettorizzazione e TF-IDF
    X, y = create_tfidf_training_data(ref_docs)

    # Crea il training-test split dei dati
    X_train, X_test, y_train, y_test = train_test_split(
        X, y, test_size=0.2, random_state=42
    )

    # Crea ed addestra la Support Vector Machine
    svm = train_svm(X_train, y_train)

Ora che l’SVM è stata addestrata, dobbiamo valutarne le prestazioni sui dati di test.

Term-Frequency Inverse Document-Frequency

Le due principali metriche delle prestazioni che prenderemo in considerazione per questo classificatore supervisionato sono il tasso di successo(hit-rate) e la confusion-matrix. Il primo è semplicemente il rapporto tra le associazioni corrette e le associazioni totali ed è solitamente espresso in percentuale.

La matrice di confusione entra più in dettaglio e fornisce statistiche sui veri positivi, veri negativi, falsi positivi e falsi negativi. In un sistema di classificazione binario, con un’etichettatura di classe “vero” o “falso”, questi caratterizzano la velocità con cui il classificatore classifica correttamente qualcosa come vero o falso quando è, rispettivamente, vero o falso, e classifica anche erroneamente qualcosa come vero o falso quando è, rispettivamente, falso o vero.

Una matrice di confusione non deve essere limitata a una situazione di classificatore binario. Per più gruppi di classi (come nel nostro esempio con il dataset di Reuters) avremo una matrice \(N\times N\), dove \(N\) è il numero di etichette di classe (o argomenti del documento).

Scikit-learn ha funzioni sia per il calcolo dell hit-rate sia per la matrice di confusione di un classificatore supervisionato. Il primo è un metodo dello stesso classificatore chiamato score. Quest’ultimo deve essere importato dalla libreria metrics.

La prima attività è creare un array di previsioni dal set di test X_test. Questo conterrà semplicemente le etichette di classe previste dall’SVM tramite il set di dati previsto per il test (20%). Questo array di previsione viene utilizzata per creare la matrice di confusione. Da notare che la funzione confusion_matrix accetta sia l’array di previsione pred sia le etichette della classe corretta y_test per produrre la matrice. Inoltre, si crea l’hit-rate tramite lo score di entrambi i sottoinsiemi X_test e y_test del set di dati:

..
..
from sklearn.metrics import confusion_matrix
..
..


if __name__ == "__main__":

    ..
    ..

    # Crea ed addestra la Support Vector Machine
    svm = train_svm(X_train, y_train)

    # Crea un array delle predizioni con i dati di test
    pred = svm.predict(X_test)

    # Calcolo del hit-rate e della confusion matrix per ogni modello
    print(svm.score(X_test, y_test))
    print(confusion_matrix(pred, y_test))
L’output dello script è il seguente:
0.660194174757
[[21  0  0  0  2  3  0  0  0  1  0  0  0  0  1  1  1  0  0]
 [ 0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0]
 [ 0  0  1  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0]
 [ 0  0  0  1  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0]
 [ 0  0  0  0  4  0  0  0  0  0  0  0  0  0  0  0  0  0  0]
 [ 0  1  0  0  1 26  0  0  0  1  0  1  0  1  0  0  0  0  0]
 [ 0  0  0  0  0  0  2  0  0  0  0  0  0  0  0  0  0  0  1]
 [ 0  0  0  0  0  0  0  1  0  0  0  0  0  0  0  0  0  0  0]
 [ 0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0]
 [ 0  0  0  0  1  0  0  0  0  3  0  0  0  0  0  0  0  0  0]
 [ 3  0  0  1  2  2  3  0  1  1  6  0  1  0  0  0  2  3  0]
 [ 0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0]
 [ 0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0]
 [ 0  0  0  0  0  0  0  0  0  0  0  0  0  1  0  0  0  0  0]
 [ 0  0  0  0  0  0  0  0  0  0  0  0  0  0  1  0  0  0  0]
 [ 0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  1  0  0  0]
 [ 0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0]
 [ 0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0]
 [ 0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0]]

Quindi abbiamo un tasso di successo della classificazione del 66%, con una matrice di confusione che ha voci principalmente sulla diagonale (cioè la corretta assegnazione dell’etichetta di classe). Si noti che poiché stiamo utilizzando solo un singolo file dal set Reuters (numero 000), non vedremo l’intero set di etichette di classe e quindi la nostra matrice di confusione è di dimensioni inferiori rispetto a quella in cui avessimo usato l’intero set di dati.

Per utilizzare il set di dati completo, possiamo modificare la funzione __main__ per caricare tutti i 21 file Reuters e addestrare SVM sul set di dati completo. Possiamo quindi calcolare la completa performance dell’hit-rate. Ho trascurato di includere l’output della matrice di confusione poiché è di grandi dimensioni a causa del numero totale di etichette di classe all’interno di tutti i documenti. Da notare che ci vorrà del tempo! Sul mio sistema sono necessari ci vogliono circa 30-45 per completare l’esecuzione.

if __name__ == "__main__":
    # Apre il primo set di dati Reuters e crea il parser
    files = ["data/reut2-%03d.sgm" % r for r in range(0, 22)]
    parser = ReutersParser()

    # Analizza il documento e forza tutti i documenti generati
    # in un elenco in modo che possano essere stampati sulla console
    docs = []
    for fn in files:
        for d in parser.parse(open(fn, 'rb')):
            docs.append(d)

    ..
    ..

    print(svm.score(X_test, y_test))

Per tutti i corpus, l’hit-rate del sistema è: 

0.835971855761

Ci sono molti modi per migliorare questo valore. In particolare, possiamo eseguire una Grid Search Cross-Validation, che è un metodo per determinare i parametri ottimali per il classificatore in modo da raggiungere il miglior tasso di successo (o altra metrica di scelta).

Negli articoli successivi discuteremo tali procedure di ottimizzazione e spiegheremo come un classificatore come questo può essere aggiunto a un sistema di produzione in un contesto di data-science o di finanza quantitativa.

Codice completo dell'implementazione in Python

Di seguito il codice completo per reuters_svm.py, scritto in Python 3.7.x:

import html
import pprint
import re
from html.parser import HTMLParser
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.model_selection import train_test_split
from sklearn.svm import SVC
from sklearn.metrics import confusion_matrix


class ReutersParser(HTMLParser):
    """
    ReutersParser è una sottoclasse HTMLParser e viene utilizzato per aprire file SGML
    associati al dataset di Reuters-21578.

    Il parser è un generatore e produrrà un singolo documento alla volta.
    Poiché i dati verranno suddivisi in blocchi durante l'analisi, è necessario 
    mantenere alcuni stati interni di quando i tag sono stati "inseriti" e 
    "eliminati".
    Da qui le variabili booleani in_body, in_topics e in_topic_d.
    """

    def __init__(self, encoding='latin-1'):
        """
        Inizializzo la superclasse (HTMLParser) e imposto il parser.
        Imposto la decodifica dei file SGML con latin-1 come default.
        """
        html.parser.HTMLParser.__init__(self)
        self._reset()
        self.encoding = encoding

    def _reset(self):
        """
        Viene chiamata solo durante l'inizializzazione della classe parser
        e quando è stata generata una nuova tupla topic-body. Si
        resetta tutto lo stato in modo che una nuova tupla possa essere
        successivamente generato.
        """
        self.in_body = False
        self.in_topics = False
        self.in_topic_d = False
        self.body = ""
        self.topics = []
        self.topic_d = ""

    def parse(self, fd):
        """
        parse accetta un descrittore di file e carica i dati in blocchi
        per ridurre al minimo l'utilizzo della memoria.
        Quindi produce nuovi documenti man mano che vengono analizzati.
        """
        self.docs = []
        for chunk in fd:
            self.feed(chunk.decode(self.encoding))
            for doc in self.docs:
                yield doc
            self.docs = []
        self.close()

    def handle_starttag(self, tag, attrs):
        """
        Questo metodo viene utilizzato per determinare cosa fare quando
        il parser incontra un particolare tag di tipo "tag".
        In questo caso, impostiamo semplicemente i valori booleani
        interni su True se è stato trovato quel particolare tag.
        """
        if tag == "reuters":
            pass
        elif tag == "body":
            self.in_body = True
        elif tag == "topics":
            self.in_topics = True
        elif tag == "d":
            self.in_topic_d = True

    def handle_endtag(self, tag):
        """
        Questo metodo viene utilizzato per determinare cosa fare
        quando il parser termina con un particolare tag di tipo "tag".

        Se il tag è un tag <REUTERS>, rimuoviamo tutti gli spazi bianchi
        con un'espressione regolare e quindi aggiungiamo la tupla topic-body.

        Se il tag è un tag <BODY> o <TOPICS>, impostiamo semplicemente lo
        stato interno su False per questi valori booleani, rispettivamente.

        Se il tag è un tag <D> (che si trova all'interno di un tag <TOPICS>),
        aggiungiamo l'argomento specifico all'elenco "topics" e infine lo resettiamo.
        """
        if tag == "reuters":
            self.body = re.sub(r'\s+', r' ', self.body)
            self.docs.append((self.topics, self.body))
            self._reset()
        elif tag == "body":
            self.in_body = False
        elif tag == "topics":
            self.in_topics = False
        elif tag == "d":
            self.in_topic_d = False
            self.topics.append(self.topic_d)
            self.topic_d = ""

    def handle_data(self, data):
        """
        I dati vengono semplicemente aggiunti allo stato appropriato
        per quel particolare tag, fino a quando non viene visualizzato
        il tag di chiusura finale.
        """
        if self.in_body:
            self.body += data
        elif self.in_topic_d:
            self.topic_d += data


def obtain_topic_tags():
    """
    Apre il file dell'elenco degli argomenti e importa tutti i nomi
    degli argomenti facendo attenzione a rimuovere il finale "\ n" da ogni parola.
    """
    topics = open(
        "data/all-topics-strings.lc.txt", "r"
    ).readlines()
    topics = [t.strip() for t in topics]
    return topics


def filter_doc_list_through_topics(topics, docs):
    """
    Legge tutti i documenti e crea un nuovo elenco di due tuple che
    contengono una singola voce di funzionalità e il corpo del testo,
    invece di un elenco di argomenti. Rimuove tutte le caratteristiche
    geografiche e conserva solo quei documenti che hanno almeno un
    argomento non geografico.
    """
    ref_docs = []
    for d in docs:
        if d[0] == [] or d[0] == "":
            continue
        for t in d[0]:
            if t in topics:
                d_tup = (t, d[1])
                ref_docs.append(d_tup)
                break
    return ref_docs


def create_tfidf_training_data(docs):
    """
    Crea un elenco di corpus di documenti (rimuovendo le etichette della classe),
    quindi applica la trasformazione TF-IDF a questo elenco.

   La funzione restituisce sia il vettore etichetta di classe (y) che
   la matrice token / feature corpus (X).
    """
    # Crea le classi di etichette per i dati di addestramento
    y = [d[0] for d in docs]

    # Crea la lista dei corpus del documenti
    corpus = [d[1] for d in docs]

    # Create la vettorizzazione TF-IDF e trasforma il corpus
    vectorizer = TfidfVectorizer(min_df=1)
    X = vectorizer.fit_transform(corpus)
    return X, y



def train_svm(X, y):
    """
    Crea e addestra la Support Vector Machine.
    """
    svm = SVC(C=1000000.0, gamma=0.0, kernel='rbf')
    svm.fit(X, y)
    return svm


if __name__ == "__main__":
    # Apre il primo set di dati Reuters e crea il parser
    filename = "data/reut2-000.sgm"
    parser = ReutersParser()

    # Analizza il documento e forza tutti i documenti generati
    # in un elenco in modo che possano essere stampati sulla console
    docs = list(parser.parse(open(filename, 'rb')))




if __name__ == "__main__":
    # Apre il primo set di dati Reuters e crea il parser
    files = ["data/reut2-%03d.sgm" % r for r in range(0, 22)]
    parser = ReutersParser()

    # Analizza il documento e forza tutti i documenti generati
    # in un elenco in modo che possano essere stampati sulla console
    docs = []
    for fn in files:
        for d in parser.parse(open(fn, 'rb')):
            docs.append(d)
    # Ottenere i tags e filtrare il documento con essi
    topics = obtain_topic_tags()
    ref_docs = filter_doc_list_through_topics(topics, docs)

    # Vettorizzazione e TF-IDF
    X, y = create_tfidf_training_data(ref_docs)

    # Crea il training-test split dei dati
    X_train, X_test, y_train, y_test = train_test_split(
        X, y, test_size=0.2, random_state=42
    )


    # Crea ed addestra la Support Vector Machine
    svm = train_svm(X_train, y_train)

    # Crea un array delle predizioni con i dati di test
    pred = svm.predict(X_test)

    # Calcolo del hit-rate e della confusion matrix per ogni modello
    print(svm.score(X_test, y_test))
    print(confusion_matrix(pred, y_test))

Recommended Posts