Julia NLP. Wir verarbeiten Texte


Die Analyse und Verarbeitung von Texten in einer natürlichen Sprache ist eine ständig aktuelle Aufgabe, die gelöst wurde, gelöst wird und auf alle verfügbaren Arten gelöst wird. Heute möchte ich über Lösungsinstrumente zur Lösung dieses Problems sprechen, und zwar in der Sprache Julia. Natürlich gibt es aufgrund der Jugend der Sprache keine derart entwickelten Analysewerkzeuge, wie zum Beispiel Stanford CoreNLP, Apache OpenNLP, GATE usw., wie zum Beispiel für die Java-Sprache. Aber auch die bereits entwickelten Bibliotheken können sowohl zur Lösung typischer Probleme als auch als Einstiegspunkt für an der Textverarbeitung interessierte Studierende empfohlen werden. Die syntaktische Einfachheit von Julia und ihre fortschrittlichen mathematischen Werkzeuge erleichtern es Ihnen, in die Aufgaben des Clusterns und Klassifizierens von Texten einzutauchen.


In diesem Artikel werden die Julia-Textverarbeitungstools mit einigen Erläuterungen zu ihrer Verwendung beschrieben. Wir werden zwischen einer kurzen Liste von Möglichkeiten für diejenigen, die sich mit NLP befassen, aber genau die Julia-Tools sehen möchten, und detaillierteren Erklärungen und Anwendungsbeispielen für diejenigen, die sich zum ersten Mal für den NLP-Bereich (Natural Language Processing) entschieden haben, einen Ausgleich schaffen.


Kommen wir nun zur Paketübersicht.


TextAnalysis.jl


Das TextAnalysis.jl- Paket ist eine Basisbibliothek , die einen minimalen Satz typischer Textverarbeitungsfunktionen implementiert. Mit ihr fangen wir an. Beispiele sind teilweise aus der Dokumentation entnommen.


Das Dokument


Die Basisentität ist ein Dokument.


Die folgenden Typen werden unterstützt:


  • FileDocument - Ein Dokument, das durch eine einfache Textdatei auf der Festplatte dargestellt wird

julia> pathname = "/usr/share/dict/words" "/usr/share/dict/words" julia> fd = FileDocument(pathname) A FileDocument * Language: Languages.English() * Title: /usr/share/dict/words * Author: Unknown Author * Timestamp: Unknown Time * Snippet: AA's AMD AMD's AOL AOL's Aachen Aachen's Aaliyah 

  • StringDocument - Ein Dokument, das durch eine UTF-8-Zeichenfolge dargestellt und im RAM gespeichert wird. Die StringDocument-Struktur ermöglicht die Speicherung des gesamten Texts.

 julia> str = "To be or not to be..." "To be or not to be..." julia> sd = StringDocument(str) A StringDocument{String} * Language: Languages.English() * Title: Untitled Document * Author: Unknown Author * Timestamp: Unknown Time * Snippet: To be or not to be... 

  • TokenDocument - Ein Dokument, das eine Folge von UTF-8-Token (hervorgehobene Wörter) ist. Die TokenDocument Struktur speichert eine Reihe von Token. Der vollständige Text kann jedoch nicht ohne Verlust wiederhergestellt werden.

 julia> my_tokens = String["To", "be", "or", "not", "to", "be..."] 6-element Array{String,1}: "To" "be" "or" "not" "to" "be..." julia> td = TokenDocument(my_tokens) A TokenDocument{String} * Language: Languages.English() * Title: Untitled Document * Author: Unknown Author * Timestamp: Unknown Time * Snippet: ***SAMPLE TEXT NOT AVAILABLE*** 

  • NGramDocument - Ein Dokument, das als Satz von n-Gramm in einer UTF8-Darstellung dargestellt wird, d. H. Eine Folge von n UTF-8-Zeichen und ein Zähler für deren Auftreten. Diese Option zum Präsentieren eines Dokuments ist eine der einfachsten Möglichkeiten, um einige Probleme der Morphologie von Sprachen, Tippfehlern und Merkmalen von Sprachstrukturen in den analysierten Texten zu vermeiden. Die Gebühr hierfür ist jedoch eine Abnahme der Qualität der Textanalyse im Vergleich zu Methoden, bei denen Sprachinformationen berücksichtigt werden.

 julia> my_ngrams = Dict{String, Int}("To" => 1, "be" => 2, "or" => 1, "not" => 1, "to" => 1, "be..." => 1) Dict{String,Int64} with 6 entries: "or" => 1 "be..." => 1 "not" => 1 "to" => 1 "To" => 1 "be" => 2 julia> ngd = NGramDocument(my_ngrams) A NGramDocument{AbstractString} * Language: Languages.English() * Title: Untitled Document * Author: Unknown Author * Timestamp: Unknown Time * Snippet: ***SAMPLE TEXT NOT AVAILABLE*** 

Oder eine kurze Option:


 julia> str = "To be or not to be..." "To be or not to be..." julia> ngd = NGramDocument(str, 2) NGramDocument{AbstractString}(Dict{AbstractString,Int64}("To be" => 1,"or not" => 1,"be or" => 1,"or" => 1,"not to" => 1,"not" => 1,"to be" => 1,"to" => 1,"To" => 1,"be" => 2…), 2, TextAnalysis.DocumentMetadata( Languages.English(), "Untitled Document", "Unknown Author", "Unknown Time")) 

Ein Dokument kann auch einfach mit dem generischen Dokumentkonstruktor erstellt werden, und die Bibliothek findet die entsprechende Implementierung des Dokuments.


 julia> Document("To be or not to be...") A StringDocument{String} * Language: Languages.English() * Title: Untitled Document * Author: Unknown Author * Timestamp: Unknown Time * Snippet: To be or not to be... julia> Document("/usr/share/dict/words") A FileDocument * Language: Languages.English() * Title: /usr/share/dict/words * Author: Unknown Author * Timestamp: Unknown Time * Snippet: AA's AMD AMD's AOL AOL's Aachen Aachen's Aaliyah julia> Document(String["To", "be", "or", "not", "to", "be..."]) A TokenDocument{String} * Language: Languages.English() * Title: Untitled Document * Author: Unknown Author * Timestamp: Unknown Time * Snippet: ***SAMPLE TEXT NOT AVAILABLE*** julia> Document(Dict{String, Int}("a" => 1, "b" => 3)) A NGramDocument{AbstractString} * Language: Languages.English() * Title: Untitled Document * Author: Unknown Author * Timestamp: Unknown Time * Snippet: ***SAMPLE TEXT NOT AVAILABLE*** 

Wie Sie sehen, besteht der Hauptteil des Dokuments aus Text / Token und Metadaten. Der Text des Dokuments kann mit der Methode text(...) abgerufen werden:


 julia> td = TokenDocument("To be or not to be...") TokenDocument{String}(["To", "be", "or", "not", "to", "be"], TextAnalysis.DocumentMetadata( Languages.English(), "Untitled Document", "Unknown Author", "Unknown Time")) julia> text(td) ┌ Warning: TokenDocument's can only approximate the original text └ @ TextAnalysis ~/.julia/packages/TextAnalysis/pcFQf/src/document.jl:111 "To be or not to be" julia> tokens(td) 6-element Array{String,1}: "To" "be" "or" "not" "to" "be" 

Das Beispiel zeigt ein Dokument mit automatisch analysierten Token. Wir sehen, dass der Aufruf von text(td) eine Warnung ausgegeben hat, dass der Text nur ungefähr wiederhergestellt wurde, da TokenDocument keine Worttrennzeichen speichert. Der Aufruf von tokens(td) ermöglichte es, genau die hervorgehobenen Wörter zu erhalten.


Sie können Metadaten aus einem Dokument anfordern:


 julia> StringDocument("This document has too foo words") A StringDocument{String} * Language: Languages.English() * Title: Untitled Document * Author: Unknown Author * Timestamp: Unknown Time * Snippet: This document has too foo words julia> language(sd) Languages.English() julia> title(sd) "Untitled Document" julia> author(sd) "Unknown Author" julia> timestamp(sd) "Unknown Time" 

Und alle von ihnen können durch die entsprechenden Funktionen geändert werden. Die Schreibweise zum Ändern von Funktionen in Julia ist dieselbe wie in der Ruby-Sprache. Eine Funktion, die ein Objekt ändert, hat ein Suffix ! :


 julia> using TextAnalysis.Languages julia> language!(sd, Languages.Russian()) Languages.Russian () julia> title!(sd, "") "" julia> author!(sd, " ..") " .." julia> import Dates:now julia> timestamp!(sd, string(now())) "2019-11-09T22:53:38.383" 

Merkmale von Strings mit UTF-8


Julia unterstützt die UTF-8-Codierung bei der Verarbeitung von Zeichenfolgen, sodass es keine Probleme mit der Verwendung von nicht-lateinischen Alphabeten gibt. Beliebige Zeichenverarbeitungsoptionen sind selbstverständlich verfügbar. Beachten Sie jedoch, dass die Zeilenindizes für Julia Bytes und keine Zeichen sind. Und jedes Zeichen kann durch eine andere Anzahl von Bytes dargestellt werden. Für die Arbeit mit UNICODE-Zeichen gibt es separate Methoden. Weitere Informationen finden Sie unter Unicode und UTF-8 . Aber hier ist ein einfaches Beispiel. Setzen wir eine Zeile mit mathematischen UNICODE-Zeichen, die durch Leerzeichen von x und y getrennt sind:


 julia> s = "\u2200 x \u2203 y" "∀ x ∃ y" julia> length(s) # ! 7 julia> ncodeunits(s) # ! 11 

Schauen wir uns nun die Indizes an:


 julia> s[1] '∀': Unicode U+2200 (category Sm: Symbol, math) julia> s[2] ERROR: StringIndexError("∀ x ∃ y", 2) [...] julia> s[3] ERROR: StringIndexError("∀ x ∃ y", 3) Stacktrace: [...] julia> s[4] ' ': ASCII/Unicode U+0020 (category Zs: Separator, space) 

Das Beispiel zeigt deutlich, dass wir mit Index 1 das Symbol . Alle nachfolgenden Indizes bis einschließlich 3 führten jedoch zu einem Fehler. Und nur der 4. Index erzeugte ein Leerzeichen als nächstes Zeichen in der Zeichenkette. Um die Grenzen von Zeichen durch Indizes in einer Zeichenfolge zu bestimmen, gibt es jedoch nützliche Funktionen prevind (vorheriger Index), nextind (nächster Index) und thisind (dieser Index). Für die oben gefundene Lücke fragen wir beispielsweise, wo die Grenze der vorherigen ist:


 julia> prevind(s, 4) 1 

Wir haben den Index 1 als Anfang des Symbols .


 julia> thisind(s, 3) 1 

Überprüfte Index 3 und erhielt die gleiche gültige 1.


Wenn wir alle Zeichen "durchgehen" müssen, kann dies auf mindestens zwei einfache Arten geschehen:
1) unter Verwendung des Designs:


 julia> for c in s print(c) end ∀ x ∃ y 

2) Verwenden eachindex Enumerators:


 julia> collect(eachindex(s)) 7-element Array{Int64,1}: 1 4 5 6 7 10 11 julia> for i in eachindex(s) print(s[i]) end ∀ x ∃ y 

Dokumentenvorverarbeitung


Wenn der Text des Dokuments von einer externen Repräsentation stammt, kann es durchaus zu Codierungsfehlern im Bytestrom kommen. Verwenden Sie die Funktion remove_corrupt_utf8!(sd) um diese zu beseitigen. Das Argument ist das oben diskutierte Dokument.


Die Hauptfunktion für die Verarbeitung von Dokumenten im TextAnalysis-Paket ist die prepare!(...) . Entfernen Sie beispielsweise die Satzzeichen aus dem Text:


 julia> str = StringDocument("here are some punctuations !!!...") julia> prepare!(str, strip_punctuation) julia> text(str) "here are some punctuations " 

Ein nützlicher Schritt bei der Textverarbeitung ist auch die Konvertierung aller Buchstaben in Kleinbuchstaben, da dies den weiteren Vergleich von Wörtern untereinander vereinfacht. In diesem Fall müssen wir im Allgemeinen verstehen, dass wir wichtige Informationen über den Text verlieren können, z. B. die Tatsache, dass das Wort ein Eigenname oder das Wort die Grenze eines Satzes ist. Es kommt aber alles auf das Modell der Weiterverarbeitung an. Die Kleinschreibung erfolgt mit der Funktion remove_case!() .


 julia> sd = StringDocument("Lear is mad") A StringDocument{String} julia> remove_case!(sd) julia> text(sd) "lear is mad" 

Unterwegs können Sie nicht mehr benötigte Wörter löschen, d. H. Solche, die beim Abrufen und Analysieren von Informationen für Übereinstimmungen nicht hilfreich sind. Dies kann explizit mit der Funktion remove_words!(…) und einem Array dieser remove_words!(…) .


 julia> remove_words!(sd, ["lear"]) julia> text(sd) " is mad" 

Unter den zu löschenden Wörtern gibt es auch Artikel, Präpositionen, Pronomen, Zahlen und nur Stoppwörter, die in der Häufigkeit des Auftretens parasitär sind. Diese Wörterbücher sind für jede Sprache individuell. Und sie sind im Paket Languages.jl festgelegt. Die Zahlen stören uns, weil sie in einem zukünftigen Modell eines Wärmedokuments die Dimension der Matrix erheblich vergrößern können, ohne beispielsweise die Zusammenfassung von Texten zu verbessern. Bei Suchproblemen ist es beispielsweise nicht mehr immer möglich, Nummern zu löschen.


Zu den verfügbaren Reinigungsmethoden gehören die folgenden Optionen:


  • prepare!(sd, strip_articles)
  • prepare!(sd, strip_indefinite_articles)
  • prepare!(sd, strip_definite_articles)
  • prepare!(sd, strip_preposition)
  • prepare!(sd, strip_pronouns)
  • prepare!(sd, strip_stopwords)
  • prepare!(sd, strip_numbers)
  • prepare!(sd, strip_non_letters)
  • prepare!(sd, strip_spares_terms)
  • prepare!(sd, strip_frequent_terms)
  • prepare!(sd, strip_html_tags)

Optionen können kombiniert werden. Zum Beispiel in einem Anruf zur prepare! gleichzeitig Artikel, Nummern und HTML-Tags entfernen - prepare!(sd, strip_articles| strip_numbers| strip_html_tags)


Eine andere Art der Verarbeitung ist das Hervorheben der Wortbasis, das Entfernen von Endungen und Suffixen. Auf diese Weise können Sie verschiedene Wortformen kombinieren und die Dimensionalität des Dokumentpräsentationsmodells drastisch reduzieren. Dazu werden Wörterbücher benötigt, daher muss die Sprache der Dokumente klar angegeben werden. Verarbeitungsbeispiel in Russisch:


 julia> sd = StringDocument("   ") StringDocument{String}("   ", TextAnalysis.DocumentMetadata(Languages.English(), "Untitled Document", "Unknown Author", "Unknown Time")) julia> language!(sd, Languages.Russian()) Languages.Russian() julia> stem!(sd) julia> text(sd) "   " 

Dokumentkörper


Unter Korpus versteht man eine Reihe von Dokumenten, die nach den gleichen Regeln bearbeitet werden. Das TextAnalysis-Paket implementiert die Bildung einer Begriffsdokumentmatrix . Und für den Bau benötigen wir sofort einen vollständigen Satz von Dokumenten. In einem einfachen Beispiel für Dokumente:
D1 = "I like databases"
D2 = "I hate databases"


Diese Matrix sieht aus wie:


IchwiehasseDatenbanken
D11101
D21011

Spalten werden durch die Wörter von Dokumenten dargestellt, und Zeilen sind Bezeichner (oder Indizes) von Dokumenten. Dementsprechend ist die Zelle 0, wenn das Wort (der Begriff) nicht im Dokument enthalten ist. Und 1, wenn es mehrmals vorkommt. Komplexere Modelle berücksichtigen sowohl die Häufigkeit des Auftretens (TF-Modell) als auch die Signifikanz in Bezug auf den gesamten Körper (TF-IDF).


Wir können den Körper mit dem Corpus() -Konstruktor erstellen:


 crps = Corpus([StringDocument("Document 1"), StringDocument("Document 2")]) 

Wenn wir sofort eine Liste von Begriffen anfordern, erhalten wir:


 julia> lexicon(crps) Dict{String,Int64} with 0 entries 

Und hier, wo die Bibliothek gezwungen wird, alle Begriffe, die Teil des Falls sind, mit update_lexicon!(crps) , erhalten wir ein anderes Ergebnis:


 julia> update_lexicon!(crps) julia> lexicon(crps) Dict{String,Int64} with 3 entries: "1" => 1 "2" => 1 "Document" => 2 

Das heißt, wir können die ausgewählten Begriffe (Wörter und Zahlen) und deren Anzahl von Einträgen im Dokumenttext sehen.


Gleichzeitig können wir die Häufigkeit des Begriffs klären, zum Beispiel „Dokument“:


 julia> lexical_frequency(crps, "Document") 0.5 

Wir können auch einen Reverse-Index erstellen, dh für jedes Thema die Dokumentennummern in dem Fall ermitteln. Dieser Index wird beim Abrufen von Informationen verwendet, wenn Sie eine Liste von Dokumenten suchen müssen, in der diese aus der Liste der Begriffe hervorgehen:


 julia> update_inverse_index!(crps) julia> inverse_index(crps) Dict{String,Array{Int64,1}} with 3 entries: "1" => [1] "2" => [2] "Document" => [1, 2] 

Für den gesamten Fall können Sie die Vorverarbeitungsfunktionen wie für jedes einzelne Dokument anwenden. Eine andere Methode der prepare! wird verwendet prepare! früher betrachtet. Hier ist das erste Argument der Fall.


 julia> crps = Corpus([StringDocument("Document ..!!"), StringDocument("Document ..!!")]) julia> prepare!(crps, strip_punctuation) julia> text(crps[1]) "Document " julia> text(crps[2]) "Document " 

Sie können sowohl für einzelne Dokumente Metadaten für den gesamten Körper anfordern.


 julia> crps = Corpus([StringDocument("Name Foo"), StringDocument("Name Bar")]) julia> languages(crps) 2-element Array{Languages.English,1}: Languages.English() Languages.English() julia> titles(crps) 2-element Array{String,1}: "Untitled Document" "Untitled Document" julia> authors(crps) 2-element Array{String,1}: "Unknown Author" "Unknown Author" julia> timestamps(crps) 2-element Array{String,1}: "Unknown Time" "Unknown Time" 

Sie können die Werte für den gesamten Text auf einmal oder für bestimmte Dokumente einzeln festlegen, indem Sie ein Array mit elementweisen Werten übergeben.


 julia> languages!(crps, Languages.German()) julia> titles!(crps, "") julia> authors!(crps, "Me") julia> timestamps!(crps, "Now") julia> languages!(crps, [Languages.German(), Languages.English julia> titles!(crps, ["", "Untitled"]) julia> authors!(crps, ["Ich", "You"]) julia> timestamps!(crps, ["Unbekannt", "2018"]) 

Feature-Hervorhebung


Die Merkmalsextraktion ist eine der Grundstufen des maschinellen Lernens. Dies bezieht sich nicht direkt auf das Thema dieses Artikels, aber in der Dokumentation zum TextAnalysis-Paket ist ein ziemlich großer Abschnitt der Identifizierung von Merkmalen in genau dieser Formulierung gewidmet. Dieser Abschnitt enthält sowohl die Erstellung einer Term-Document-Matrix als auch viele andere Methoden. https://juliatext.imtqy.com/TextAnalysis.jl/dev/features/


Wir betrachten kurz die vorgeschlagenen Optionen.


Das Grundmodell für die Präsentation von Dokumenten ist ein Modell, in dem für jedes Dokument eine Reihe von Wörtern gespeichert ist. Darüber hinaus ist ihre Position nicht wichtig. Daher wird diese Option in einem englischsprachigen Schriftsteller "Bag of words" genannt. Für jedes Wort ist nur die Tatsache wichtig, dass es im Dokument vorhanden ist, die Häufigkeit des Auftretens (TF - Term Frequency) oder ein Modell, das die Häufigkeit des Auftretens des Begriffs im gesamten Körper berücksichtigt (TF-IDF - Term Frequency - Inverse Document Frequency).


Nehmen Sie das einfachste Beispiel mit drei Dokumenten, die die Begriffe Document , 1 , 2 , 3 .


 julia> using TextAnalysis julia> crps = Corpus([StringDocument("Document 1"), StringDocument("Document 2"), StringDocument("Document 1 3")]) 

Wir werden keine Vorverarbeitung verwenden. Wir erstellen jedoch das vollständige Lexikon und die Matrix des Begriffsdokuments:


 julia> update_lexicon!(crps) julia> m = DocumentTermMatrix(crps) DocumentTermMatrix( [1, 1] = 1 [3, 1] = 1 [2, 2] = 1 [3, 3] = 1 [1, 4] = 1 [2, 4] = 1 [3, 4] = 1, ["1", "2", "3", "Document"], Dict("1" => 1,"2" => 2,"Document" => 4,"3" => 3)) 

Die Variable m hat einen Wert vom Typ DocumentTermMatrix . Im gedruckten Ergebnis sehen wir, dass die Dimension 3 Dokumente in 4 Begriffen umfasst, die das Wort Document und die Zahlen 1 , 2 , 3 . Für die weitere Verwendung des Modells benötigen wir eine Matrix in der traditionellen Darstellung. Wir können es mit der dtm() -Methode bekommen:


 julia> dtm(m) 3×4 SparseArrays.SparseMatrixCSC{Int64,Int64} with 7 stored entries: [1, 1] = 1 [3, 1] = 1 [2, 2] = 1 [3, 3] = 1 [1, 4] = 1 [2, 4] = 1 [3, 4] = 1 

Diese Option wird durch den SparseMatrixCSC Typ dargestellt, der eine sehr spärliche Matrix wirtschaftlich darstellt, aber nur von einer begrenzten Anzahl von Bibliotheken unterstützt wird. Das Problem der Größe der Begriffsdokumentmatrix ist darauf zurückzuführen, dass die Anzahl der Begriffe mit der Anzahl der verarbeiteten Dokumente sehr schnell zunimmt. Wenn Sie Dokumente nicht vorverarbeiten, fallen absolut alle Wörter mit all ihren Wortformen, Zahlen und Datumsangaben in diese Matrix. Selbst wenn die Anzahl der Wortformen aufgrund der Reduzierung auf die Hauptform verringert wird, liegt die Anzahl der verbleibenden Stämme in der Größenordnung von Tausenden - Zehntausenden. Das heißt, die vollständige Dimension des Ausdrucks Dokumentenmatrix wird durch das Gesamtprodukt dieser Menge durch die Anzahl der verarbeiteten Dokumente bestimmt. In einer vollständigen Matrix müssen nicht nur Einheiten, sondern auch Nullen SparseMatrixCSC . Die Verwendung ist jedoch einfacher als bei SparseMatrixCSC . Sie können es mit einer anderen Methode dtm(..., :dense) oder durch Konvertieren einer dtm(..., :dense) Matrix in eine vollständige mit der Methode collect() :


 julia> dtm(m, :dense) 3×4 Array{Int64,2}: 1 0 0 1 0 1 0 1 1 0 1 1 

Wenn Sie eine Reihe von Begriffen drucken, können Sie in jeder Zeile die ursprüngliche Zusammensetzung der Dokumente leicht erkennen (die ursprüngliche Reihenfolge der Begriffe wird nicht berücksichtigt).


 julia> m.terms 4-element Array{String,1}: "1" "2" "3" "Document" 

Der Begriff Dokumentmatrix für Frequenzmodelle kann mit den Methoden tf() und tf_idf() werden:


 julia> tf(m) |> collect 3×4 Array{Float64,2}: 0.5 0.0 0.0 0.5 0.0 0.5 0.0 0.5 0.333333 0.0 0.333333 0.333333 

Es ist leicht zu erkennen, welche Bedeutung die Begriffe für die einzelnen Dokumente haben. Die ersten beiden Dokumente enthalten zwei Begriffe. Der letzte ist drei. So wird ihr Gewicht reduziert.


Und für TF-IDF und tf_idf() Methode:


 julia> tdm = tf_idf(m) |> collect 3×4 Array{Float64,2}: 0.202733 0.0 0.0 0.0 0.0 0.549306 0.0 0.0 0.135155 0.0 0.366204 0.0 

Und in diesem Modell ist leicht zu erkennen, dass der Begriff Document , der in allen Dokumenten enthalten ist, den Wert 0 hat. Der Begriff 3 im dritten Dokument hat jedoch mehr Gewicht als 1 im selben Dokument gewonnen, da 1 auch im ersten Dokument enthalten ist .


Die resultierenden Matrizen sind sehr einfach zu verwenden, um zum Beispiel das Problem des Clustering von Dokumenten zu lösen. Dazu benötigen Sie das Clustering-Paket . Wir verwenden den einfachsten k-means Clustering-Algorithmus, der die Anzahl der gewünschten Cluster spezifizieren muss. Wir teilen unsere drei Dokumente in zwei Gruppen auf. Die Eingabematrix für kmeans ist eine Feature-Matrix, in der Zeilen Features und Spalten Muster darstellen. Daher müssen die oben erhaltenen Matrizen transponiert werden.


 julia> using Clustering julia> R = kmeans(tdm', 2; maxiter=200, display=:iter) Iters objv objv-change | affected ------------------------------------------------------------- 0 1.386722e-01 1 6.933608e-02 -6.933608e-02 | 0 2 6.933608e-02 0.000000e+00 | 0 K-means converged with 2 iterations (objv = 0.06933608051588186) KmeansResult{Array{Float64,2},Float64,Int64}( [0.0 0.16894379504506848; 0.5493061443340549 0.0; 0.0 0.1831020481113516; 0.0 0.0], [2, 1, 2], [0.03466804025794093, 0.0, 0.03466804025794093], [1, 2], [1, 2], 0.06933608051588186, 2, true) julia> c = counts(R) #    2-element Array{Int64,1}: 1 2 julia> a = assignments(R) #     3-element Array{Int64,1}: 2 1 2 julia> M = R.centers #     4×2 Array{Float64,2}: 0.0 0.168944 0.549306 0.0 0.0 0.183102 0.0 0.0 

Als Ergebnis sehen wir, dass der erste Cluster ein Dokument enthält, Cluster Nummer 2 enthält zwei Dokumente. Darüber hinaus zeigt die Matrix, die die Zentren von R.centers Clustern enthält, deutlich, dass die erste Spalte durch Term 2 „angezogen“ wird. Die zweite Spalte wird durch das Vorhandensein der Terme 1 und 3 .


Das Paket Clustering.jl enthält einen typischen Satz von Clustering-Algorithmen, darunter: K-Mittel, K-Medoide, Affinitätsausbreitung, dichtebasiertes räumliches Clustering von Anwendungen mit Rauschen (DBSCAN), Markov-Clustering-Algorithmus (MCL), Fuzzy-C-Means-Clustering, Hierarchisches Clustering (Single, Average, Complete, Ward's Linkage). Eine Analyse ihrer Anwendbarkeit würde jedoch den Rahmen dieses Artikels sprengen.


Das TextAnalysis.jl Paket befindet sich derzeit in der aktiven Entwicklung, sodass einige Funktionen nur verfügbar sind, wenn das Paket direkt aus dem Git-Repository installiert wird. Dies ist nicht schwierig, kann aber nur denjenigen empfohlen werden, die nicht vorhaben, die Lösung in naher Zukunft in Betrieb zu nehmen:


 julia> ] (v1.2) pkg> add https://github.com/JuliaText/TextAnalysis.jl 

Sie sollten diese Funktionen jedoch in der Überprüfung nicht ignorieren. Deshalb betrachten wir sie auch.


Eine der Verbesserungen ist die Verwendung der Okapi BM25- Ranglistenfunktion. Ähnlich wie bei früheren tf Modellen. tf_idf wir die Methode bm_25(m) . Die Verwendung der resultierenden Matrix ähnelt den vorherigen Fällen.


Eine Analyse der Tonalität von Texten kann mit folgenden Methoden durchgeführt werden:


 model = SentimentAnalyzer(doc) model = SentimentAnalyzer(doc, handle_unknown) 

Darüber hinaus ist doc einer der oben genannten Dokumenttypen. handle_unknown - Funktion zur Verarbeitung unbekannter Wörter. Die Tonalitätsanalyse wird mit dem Paket Flux.jl implementiert, das auf dem IMDB-Paket basiert. Der Rückgabewert liegt im Bereich von 0 bis 1.


Die Verallgemeinerung von Dokumenten kann mit der Methode summarize(d, ns) implementiert werden. Das erste Argument ist das Dokument. Die Sekunde ist ns= Anzahl der Sätze am Ende.


 julia> s = StringDocument("Assume this Short Document as an example. Assume this as an example summarizer. This has too foo sentences.") julia> summarize(s, ns=2) 2-element Array{SubString{String},1}: "Assume this Short Document as an example." "This has too foo sentences." 

Eine sehr wichtige Komponente jeder Textanalysebibliothek ist der syntaktische Parser, der derzeit entwickelt wird und der Teile der Sprache - POS (Teil der Sprache) - unterscheidet. Es gibt verschiedene Möglichkeiten, es zu benutzen. Einzelheiten finden Sie unter Teile der Sprachkennzeichnung.
. Tagging wird aufgerufen, weil für jedes Wort im Quelltext eine Kennzeichnung gebildet wird, die einen Teil der Sprache bedeutet.


Zwei Optionen für die Implementierung sind in der Entwicklung. Der erste ist der durchschnittliche Perceptron-Algorithmus. Die zweite basiert auf der Verwendung der neuronalen Netzwerkarchitektur LSTMs, CNN und der CRF-Methode. Hier ist ein Beispiel für ein einfaches Satz-Markup.


 julia> pos = PoSTagger() julia> sentence = "This package is maintained by John Doe." "This package is maintained by John Doe." julia> tags = pos(sentence) 8-element Array{String,1}: "DT" "NN" "VBZ" "VBN" "IN" "NNP" "NNP" "." 

Die Liste der Abkürzungen, die einen Teil der Rede bedeuten, stammt von Penn Treebank . Insbesondere DT - Determiner, NN - Nomen, Singular oder Masse, VBZ - Verb, 3. Person Singular vorhanden, Verb, Partizip Perfekt, IN - Präposition oder untergeordnete Konjunktion, NNP - Proper Nomen, Singular.


Die Ergebnisse dieses Markups können auch als zusätzliche Funktionen für die Klassifizierung von Dokumenten verwendet werden.


Dimensionsreduktionsmethoden


TextAnalysis bietet zwei Optionen zum Reduzieren der Dimensionalität durch Definieren abhängiger Begriffe. Diese latente semantische Analyse - LSA und latente Dirichlet-Platzierung - LDA.


Die Hauptaufgabe der LSA besteht darin, die Zerlegung der Term-Document-Matrix (unter Verwendung von TF-IDF) in 3 Matrizen zu erreichen, deren Produkt in etwa dem ursprünglichen entspricht.


 julia> crps = Corpus([StringDocument("this is a string document"), TokenDocument("this is a token document")]) julia> update_lexicon!(crps) julia> m = DocumentTermMatrix(crps) julia> tf_idf(m) |> collect 2×6 Array{Float64,2}: 0.0 0.0 0.0 0.138629 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.138629 julia> F2 = lsa(m) SVD{Float64,Float64,Array{Float64,2}}([1.0 0.0; 0.0 1.0], [0.138629, 0.138629], [0.0 0.00.0 0.0; 0.0 0.00.0 1.0]) 

, -, TF-IDF . SVD, , .


LDA . Ein Beispiel:


 julia> crps = Corpus([StringDocument("This is the Foo Bar Document"), StringDocument("This document has too Foo words")]) julia> update_lexicon!(crps) julia> m = DocumentTermMatrix(crps) julia> k = 2 # number of topics julia> iterations = 1000 # number of gibbs sampling iterations julia> α = 0.1 # hyper parameter julia> β = 0.1 # hyper parameter julia> ϕ, θ = lda(m, k, iterations, α, β) ( [2 , 1] = 0.333333 [2 , 2] = 0.333333 [1 , 3] = 0.222222 [1 , 4] = 0.222222 [1 , 5] = 0.111111 [1 , 6] = 0.111111 [1 , 7] = 0.111111 [2 , 8] = 0.333333 [1 , 9] = 0.111111 [1 , 10] = 0.111111, [0.5 1.0; 0.5 0.0]) 

k lda , . ϕ θ , ntopics × nwords , — ntopics × ndocs .



. . , . NaiveBayesClassifier() . — fit!() :


 using TextAnalysis: NaiveBayesClassifier, fit!, predict m = NaiveBayesClassifier([:legal, :financial]) fit!(m, "this is financial doc", :financial) fit!(m, "this is legal doc", :legal) 

predict :


 julia> predict(m, "this should be predicted as a legal document") Dict{Symbol,Float64} with 2 entries: :legal => 0.666667 :financial => 0.333333 

, , :legal .


TextAnalysis.jl . , . MLJ.jl . AdaBoostClassifier, BaggingClassifier, BernoulliNBClassifier, ComplementNBClassifier, ConstantClassifier, XGBoostClassifier, DecisionTreeClassifier. - LSA, . .


TextAnalysis.jl CRF — Conditional Random Fields , Flux.jl, . .



TextAnalysis.jlNER . NERTagger() :


  • PER:
  • LOC:
  • ORG:
  • MISC:
  • O:

:


 julia> sentence = "This package is maintained by John Doe." "This package is maintained by John Doe." julia> tags = ner(sentence) 8-element Array{String,1}: "O" "O" "O" "O" "O" "PER" "PER" "O" 

NERTagger TextAnalysis. .


StringDistances.jl


. , , . . , StringDistances.jl . :


 using StringDistances compare("martha", "martha", Hamming()) #> 1.0 compare("martha", "marhta", Jaro()) #> 0.9444444444444445 compare("martha", "marhta", Winkler(Jaro())) #> 0.9611111111111111 compare("william", "williams", QGram(2)) #> 0.9230769230769231 compare("william", "williams", Winkler(QGram(2))) #> 0.9538461538461539 

compare — . , 1 — . 0 — .


, Jaro-Winkler. , . RatcliffObershelp, , . , . .


  compare("mariners vs angels", "angels vs mariners", RatcliffObershelp()) #> 0.44444 compare("mariners vs angels", "angels vs mariners", TokenSort(RatcliffObershelp()) #> 1.0 compare("mariners vs angels", "los angeles angels at seattle mariners", Jaro()) #> 0.559904 compare("mariners vs angels", "los angeles angels at seattle mariners", TokenSet(Jaro())) #> 0.944444 compare("mariners vs angels", "los angeles angels at seattle mariners", TokenMax(RatcliffObershelp())) #> 0.855 

, , . , TokenSort , . Julia — Julia, .


WordTokenizers.jl


WordTokenizers.jl . , , TextAnalysis.jl.


— . , tokenize(text) .


 julia> using WordTokenizers julia> text = "I cannot stand when they say \"Enough is enough.\""; julia> tokenize(text) |> print # Default tokenizer SubString{String}["I", "can", "not", "stand", "when", "they", "say", "``", "Enough", "is", "enough", ".", "''"] 

WordTokenizers .


 julia> text = "The leatherback sea turtle is the largest, measuring six or seven feet (2 m) in length at maturity, and three to five feet (1 to 1.5 m) in width, weighing up to 2000 pounds (about 900 kg). Most other species are smaller, being two to four feet in length (0.5 to 1 m) and proportionally less wide. The Flatback turtle is found solely on the northerncoast of Australia."; julia> split_sentences(text) 3-element Array{SubString{String},1}: "The leatherback sea turtle is the largest, measuring six or seven feet (2 m) in length at maturity, and three to five feet (1 to 1.5 m) in width, weighing up to 2000 pounds (about900 kg). " "Most other species are smaller, being two to four feet in length (0.5 to 1 m) and proportionally less wide. " "The Flatback turtle is found solely on the northern coast of Australia." julia> tokenize.(split_sentences(text)) 3-element Array{Array{SubString{String},1},1}: SubString{String}["The", "leatherback", "sea", "turtle", "is", "the", "largest", ",", "measuring", "six""up", "to", "2000", "pounds", "(", "about", "900", "kg", ")", "."] SubString{String}["Most", "other", "species", "are", "smaller", ",", "being", "two", "to", "four""0.5", "to", "1", "m", ")", "and", "proportionally", "less", "wide", "."] SubString{String}["The", "Flatback", "turtle", "is", "found", "solely", "on", "the", "northern", "coast", "of", "Australia", "."] 

:


  • Poorman's tokenizer — . , split .
  • Punctuation space tokenize — . , .
  • Penn Tokenizer — , Penn Treebank.
  • Improved Penn Tokenizer — , NLTK.
  • NLTK Word tokenizer — , NLTK, , UNICODE- .
  • Reversible Tokenizer — , .
  • TokTok Tokenizer — , .
  • Tweet Tokenizer — , , , HTML- .

set_tokenizer(nltk_word_tokenize)


Embeddings.jl


Embeddings.jl . , , , , , , . Word2Vec. , : king - man + woman = queen . , , . , , , Wikipedia, . , . «semantic space», , «semantic distance». , , , «» «» . , , , .


, «embedding» , , , . , , , , , , . , -, . , . , . .


Embeddings.jl : Word2Vec, GloVe (English only), FastText. . , , . — , . , , word2vec, 8-16 . , .


, , DataDeps.jl . , (" "). , Embedding.jl , , . , .


 ENV["DATADEPS_ALWAYS_ACCEPT"] = true 

— . ~/.julia/datadeps .


. — :


 using Embeddings const embtable = load_embeddings(Word2Vec) # or load_embeddings(FastText_Text) or ... const get_word_index = Dict(word=>ii for (ii,word) in enumerate(embtable.vocab)) function get_embedding(word) ind = get_word_index[word] emb = embtable.embeddings[:,ind] return emb end 

— :


 julia> get_embedding("blue") 300-element Array{Float32,1}: 0.01540828 0.03409082 0.0882124 0.04680265 -0.03409082 ... 

WordTokenizers TextAnalysis, . , Julia:


 julia> a = rand(5) 5-element Array{Float64,1}: 0.012300397820243392 0.13543646950484067 0.9780602985106086 0.24647179461578816 0.18672770774122105 julia> b = ones(5) 5-element Array{Float64,1}: 1.0 1.0 1.0 1.0 1.0 julia> a+b 5-element Array{Float64,1}: 1.0123003978202434 1.1354364695048407 1.9780602985106086 1.2464717946157882 1.186727707741221 

Clustering.jl. , — . MLJ.jl. , https://github.com/JuliaStats/Distances.jl , :


  • Euclidean distance
  • Squared Euclidean distance
  • Periodic Euclidean distance
  • Cityblock distance
  • Total variation distance
  • Jaccard distance
  • Rogers-Tanimoto distance
  • Chebyshev distance
  • Minkowski distance
  • Hamming distance
  • Cosine distance
  • Correlation distance
  • Chi-square distance
  • Kullback-Leibler divergence
  • Generalized Kullback-Leibler divergence
  • Rényi divergence
  • Jensen-Shannon divergence
  • Mahalanobis distance
  • Squared Mahalanobis distance
  • Bhattacharyya distance
  • Hellinger distance
  • Haversine distance
  • Mean absolute deviation
  • Mean squared deviation
  • Root mean squared deviation
  • Normalized root mean squared deviation
  • Bray-Curtis dissimilarity
  • Bregman divergence

.


Transformers.jl


Transformers.jl — Julia «Transformers», BERT Google. , NER — , .


Transformers.jl Flux.jl , , , Julia- , . Flux.jl CPU GPU, , , , .


BERT , . :


 using Transformers using Transformers.Basic using Transformers.Pretrain using Transformers.Datasets using Transformers.BidirectionalEncoder using Flux using Flux: onehotbatch, gradient import Flux.Optimise: update! using WordTokenizers ENV["DATADEPS_ALWAYS_ACCEPT"] = true const FromScratch = false #use wordpiece and tokenizer from pretrain const wordpiece = pretrain"bert-uncased_L-12_H-768_A-12:wordpiece" const tokenizer = pretrain"bert-uncased_L-12_H-768_A-12:tokenizer" const vocab = Vocabulary(wordpiece) const bert_model = gpu( FromScratch ? create_bert() : pretrain"bert-uncased_L-12_H-768_A-12:bert_model" ) Flux.testmode!(bert_model) function vectorize(str::String) tokens = str |> tokenizer |> wordpiece text = ["[CLS]"; tokens; "[SEP]"] token_indices = vocab(text) segment_indices = [fill(1, length(tokens) + 2);] sample = (tok = token_indices, segment = segment_indices) bert_embedding = sample |> bert_model.embed collect(sum(bert_embedding, dims=2)[:]) end 

vectorize . :


 using Distances x1 = vectorize("Some test about computers") x2 = vectorize("Some test about printers") cosine_dist(x1, x2) 

, wordpiece , tokenizer — . 12 — . 768 — . . https://chengchingwen.imtqy.com/Transformers.jl/dev/pretrain/ . , Transformers.Pretrain.@pretrain_str, pretrain"model-description:item" .


, , Transformers.jl , .


Fazit


, , Julia . . , , . , Julia , . , Julia.


, , , - «», . , , «open source» , , , . , , Julia . , Jupyter Notebook , , — Atom/Juno, VS Code, . , , Julia — 2-3 , ( , , ), C++ .


, , Julia, -, . , , , , . , 2-3 , , . Julia . - , for . — « ». , C, . Julia, , - , , — Julia-. , Julia — , .


, , Julia . , , .


, - Julia — @JuliaLanguage, .


Referenzen


Source: https://habr.com/ru/post/de475922/


All Articles