Julia NLP. Nous traitons les textes


L'analyse et le traitement de textes dans une langue naturelle est une tâche constamment d'actualité qui a été résolue, est résolue et sera résolue de toutes les manières disponibles. Aujourd'hui, je voudrais parler des outils de solution pour résoudre ce problème, à savoir dans la langue Julia. Bien sûr, en raison de la jeunesse du langage, il n'y a pas d'outils d'analyse développés tels que, par exemple, Stanford CoreNLP, Apache OpenNLP, GATE, etc., comme, par exemple, pour le langage Java. Cependant, même les bibliothèques qui ont déjà été développées peuvent être utilisées à la fois pour résoudre des problèmes typiques et peuvent être recommandées comme point d'entrée pour les étudiants intéressés par le domaine du traitement de texte. Et la simplicité syntaxique de Julia et ses outils mathématiques avancés vous permettent de vous immerger facilement dans les tâches de regroupement et de classification des textes.


Le but de cet article est de passer en revue les outils de traitement de texte Julia avec quelques explications sur leur utilisation. Nous équilibrerons entre une brève liste d'opportunités pour ceux qui sont dans le sujet de la PNL, mais aimerions voir exactement les outils Julia, et des explications plus détaillées et des exemples d'application pour ceux qui ont décidé de plonger dans la zone NLP (Natural Language Processing) en tant que telle pour la première fois.


Eh bien, maintenant, passons à l'aperçu du package.


TextAnalysis.jl


Le package TextAnalysis.jl est une bibliothèque de base qui implémente un ensemble minimal de fonctions de traitement de texte typiques. C'est avec elle que nous commençons. Des exemples sont partiellement tirés de la documentation .


Le document


L'entité de base est un document.


Les types suivants sont pris en charge:


  • FileDocument - un document représenté par un simple fichier texte sur le disque

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 - un document représenté par une chaîne UTF-8 et stocké dans la RAM. La structure StringDocument prévoit le stockage du texte dans son ensemble.

 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 - un document qui est une séquence de jetons UTF-8 (mots en surbrillance). La structure TokenDocument stocke un ensemble de jetons, cependant, le texte intégral ne peut pas être restauré sans perte.

 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 - un document présenté comme un ensemble de n-grammes dans une représentation UTF8, c'est-à-dire une séquence de n caractères UTF-8, et un compteur pour leur occurrence. Cette option de présentation d'un document est l'un des moyens les plus simples d'éviter certains problèmes de morphologie des langues, de fautes de frappe et de caractéristiques des structures linguistiques dans les textes analysés. Cependant, les frais pour cela sont une diminution de la qualité de l'analyse de texte par rapport aux méthodes où les informations linguistiques sont prises en compte.

 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*** 

Ou une courte 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")) 

Un document peut également être créé simplement à l'aide du constructeur générique Document, et la bibliothèque trouvera l'implémentation appropriée du document.


 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*** 

Comme vous pouvez le voir, le corps du document se compose de texte / jetons et de métadonnées. Le texte du document peut être obtenu à l'aide de la méthode text(...) :


 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" 

L'exemple montre un document avec des jetons analysés automatiquement. Nous voyons que l'appel au text(td) émis un avertissement que le texte n'a été qu'approximativement restauré, car TokenDocument ne stocke pas de délimiteurs de mots. L'appel de tokens(td) a permis d'obtenir exactement les mots en surbrillance.


Vous pouvez demander des métadonnées à partir d'un document:


 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" 

Et tous peuvent être modifiés par les fonctions correspondantes. La notation des fonctions de modification dans Julia est la même que dans le langage Ruby. Une fonction qui modifie un objet a un suffixe ! :


 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" 

Caractéristiques des cordes avec UTF-8


Julia prend en charge le codage UTF-8 lors du traitement des chaînes, elle n'a donc aucun problème à utiliser des alphabets non latins. Toutes les options de traitement des caractères sont naturellement disponibles. Cependant, gardez à l'esprit que les index de ligne pour Julia sont des octets, pas des caractères. Et chaque caractère peut être représenté par un nombre différent d'octets. Et il existe des méthodes distinctes pour travailler avec des caractères UNICODE. Voir Unicode-et-UTF-8 pour plus de détails. Mais voici un exemple simple. Fixons une ligne avec des caractères UNICODE mathématiques séparés de x et y par des espaces:


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

Voyons maintenant les indices:


 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) 

L'exemple montre clairement que l'indice 1 nous a permis d'obtenir le symbole . Mais tous les index suivants jusqu'à 3 inclus, ont conduit à une erreur. Et seul le 4ème index a produit un espace, comme le caractère suivant de la chaîne. Cependant, pour déterminer les limites des caractères par index dans une chaîne, il existe des fonctions utiles prevind (index précédent), nextind (index suivant) et thisind (cet index). Par exemple, pour l'écart trouvé ci-dessus, nous demandons où est la frontière de la précédente:


 julia> prevind(s, 4) 1 

Nous avons obtenu l'index 1 comme début du symbole .


 julia> thisind(s, 3) 1 

Vérifié l'index 3 et obtenu le même valide 1.


Si nous devons "parcourir" tous les personnages, cela peut se faire de deux manières simples au moins:
1) en utilisant la conception:


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

2) en utilisant eachindex énumérateur d' eachindex :


 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 

Prétraitement des documents


Si le texte du document a été obtenu à partir d'une représentation externe, il est fort possible qu'il puisse y avoir des erreurs de codage dans le flux d'octets. Pour les éliminer, utilisez la fonction remove_corrupt_utf8!(sd) . L'argument est le document discuté ci-dessus.


La fonction principale pour le traitement des documents dans le package TextAnalysis est de prepare!(...) . Par exemple, supprimez les signes de ponctuation du texte:


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

En outre, une étape utile du traitement de texte est la conversion de toutes les lettres en minuscules, car cela simplifie la comparaison des mots entre eux. Dans ce cas, dans le cas général, il faut comprendre que nous pouvons perdre des informations importantes sur le texte, par exemple, le fait que le mot est un nom propre ou le mot est la limite d'une phrase. Mais tout dépend du modèle de traitement ultérieur. Les minuscules sont effectuées par la fonction remove_case!() .


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

En cours de route, nous pouvons supprimer les mots inutiles, c'est-à-dire ceux qui ne sont d'aucune utilité dans la recherche et l'analyse des informations pour les correspondances. Cela peut être fait explicitement en utilisant la fonction remove_words!(…) et un tableau de ces mots remove_words!(…) .


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

Parmi les mots à supprimer, il y a aussi des articles, des prépositions, des pronoms, des nombres et juste des mots d'arrêt, qui sont parasites en fréquence d'occurrence. Pour chaque langue spécifique, ces dictionnaires sont individuels. Et ils sont définis dans le package Languages.jl. Les nombres nous dérangent car dans le futur modèle d'un document thermique, ils peuvent augmenter considérablement la dimension de la matrice sans améliorer, par exemple, le regroupement des textes. Cependant, dans les problèmes de recherche, par exemple, il n'est plus toujours possible de supprimer des numéros.


Parmi les méthodes de nettoyage disponibles figurent les options suivantes:


  • 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)

Les options peuvent être combinées. Par exemple, en un seul appel pour vous prepare! supprimer simultanément des articles, des nombres et des balises html - prepare!(sd, strip_articles| strip_numbers| strip_html_tags)


Un autre type de traitement consiste à mettre en évidence la base des mots, à supprimer les terminaisons et les suffixes. Cela vous permet de combiner différentes formes de mots et de réduire considérablement la dimensionnalité du modèle de présentation de document. Des dictionnaires sont nécessaires pour cela, la langue des documents doit donc être clairement indiquée. Exemple de traitement en russe:


 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) "   " 

Corps du document


Le corpus est compris comme un ensemble de documents qui seront traités selon les mêmes règles. Le package TextAnalysis implémente la formation d'une matrice de document de terme . Et pour sa construction, nous devons immédiatement disposer d'un ensemble complet de documents. Dans un exemple simple pour les documents:
D1 = "I like databases"
D2 = "I hate databases"


cette matrice ressemble à:


Jecommedétestebases de données
D11101
D21011

Les colonnes sont représentées par les mots des documents et les lignes sont des identifiants (ou index) des documents. Par conséquent, la cellule sera 0 si le mot (terme) n'apparaît pas dans le document. Et 1 si cela se produit un certain nombre de fois. Des modèles plus complexes prennent en compte à la fois la fréquence d'occurrence (modèle TF) et la signification par rapport à l'ensemble du corps (TF-IDF).


Nous pouvons construire le corps en utilisant le constructeur Corpus() :


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

Si nous demandons une liste de conditions immédiatement, nous obtenons:


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

Et, ici, en forçant la bibliothèque à recompter tous les termes qui font partie du cas en utilisant update_lexicon!(crps) , nous obtenons un résultat différent:


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

Autrement dit, nous pouvons voir les termes sélectionnés (mots et nombres) et leur nombre d'entrées dans le corps du document.


Dans le même temps, nous pouvons clarifier la fréquence du terme, par exemple, «Document»:


 julia> lexical_frequency(crps, "Document") 0.5 

De plus, nous pouvons construire un index inversé, c'est-à-dire, pour chaque sujet, obtenir les numéros de document dans le cas. Cet index est utilisé dans la recherche d'informations, lorsque vous avez besoin de trouver une liste de documents où ils apparaissent dans la liste des termes:


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

Pour le cas dans son ensemble, vous pouvez appliquer les fonctions de prétraitement, les mêmes que pour chaque document individuel. Une autre méthode de la fonction de prepare! est utilisée prepare! examiné plus tôt. Ici, le premier argument est le cas.


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

Comme pour les documents individuels, vous pouvez demander des métadonnées pour l'ensemble du corps.


 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" 

Vous pouvez définir les valeurs de la même manière pour le corps entier à la fois ou individuellement pour des documents spécifiques en leur passant un tableau avec des valeurs élément par élément.


 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"]) 

Mise en évidence des fonctionnalités


L'extraction de fonctionnalités est l'une des étapes de base de l'apprentissage automatique. Cela ne se rapporte pas directement au sujet de cet article, mais dans la documentation du package TextAnalysis, une section assez importante est consacrée à l'identification des fonctionnalités dans cette formulation même. Cette section comprend à la fois, en fait, la construction d'une matrice terme-document, et de nombreuses autres méthodes. https://juliatext.imtqy.com/TextAnalysis.jl/dev/features/


Nous examinons brièvement les options proposées.


Le modèle de base pour la présentation de documents est un modèle où un ensemble de mots est stocké pour chaque document. De plus, leur position n'est pas importante. Par conséquent, dans un écrivain de langue anglaise, cette option est appelée Sac de mots. Pour chaque mot, seul le fait de sa présence dans le document, la fréquence d'occurrence (TF - Term Frequency) ou un modèle qui prend en compte la fréquence d'occurrence du terme dans le corps dans son ensemble (TF-IDF - Term Frequency - Inverse Document Frequency) est important.


Prenons l'exemple le plus simple avec trois documents contenant les termes Document , 1 , 2 , 3 .


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

Nous n'utiliserons pas de prétraitement. Mais nous allons construire le lexique complet et la matrice du terme document:


 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)) 

La variable m a une valeur de type DocumentTermMatrix . Dans le résultat imprimé, nous voyons que la dimension est de 3 documents en 4 termes, qui incluent le mot Document et les nombres 1 , 2 , 3 . Pour une utilisation ultérieure du modèle, nous avons besoin d'une matrice dans la représentation traditionnelle. Nous pouvons l'obtenir en utilisant la méthode dtm() :


 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 

Cette option est représentée par le type SparseMatrixCSC , qui est économique pour représenter une matrice très clairsemée, mais il n'y a qu'un nombre limité de bibliothèques qui la prennent en charge. Le problème de la taille de la matrice de documents à terme est dû au fait que le nombre de termes croît très rapidement avec le nombre de documents traités. Si vous ne pré-traitez pas les documents, alors absolument tous les mots avec toutes leurs formes de mots, nombres, dates tomberont dans cette matrice. Même si le nombre de formes de mots est réduit en raison de la réduction à la forme principale, le nombre de tiges restantes sera de l'ordre de milliers - dizaines de milliers. C'est-à-dire que la dimension complète du terme matrice de documents est déterminée par le produit total de cette quantité par le nombre de documents traités. Une matrice complète nécessite de stocker non seulement des unités mais aussi des zéros, mais elle est plus facile à utiliser que SparseMatrixCSC . Vous pouvez l'obtenir par une autre méthode dtm(..., :dense) ou en convertissant une matrice clairsemée en une matrice complète en utilisant la méthode collect() :


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

Si vous imprimez un tableau de termes, il est facile de voir dans chaque ligne la composition originale des documents (l'ordre d'origine des termes n'est pas pris en compte).


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

Le terme matrice de documents pour les modèles de fréquence peut être obtenu en utilisant les méthodes tf() et tf_idf() :


 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 

Il est facile de voir la signification des termes pour chacun des documents. Les deux premiers documents contiennent deux termes. Le dernier est trois. Leur poids est donc réduit.


Et pour la méthode TF-IDF et tf_idf() :


 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 

Et dans ce modèle, il est facile de voir que le terme Document , qui se trouve dans tous les documents, a une valeur de 0. Mais le terme 3 dans le troisième document a gagné plus de poids que 1 dans le même document, car 1 se trouve également dans le premier document .


Les matrices résultantes sont très faciles à utiliser, par exemple, pour résoudre le problème du regroupement de documents. Pour ce faire, vous aurez besoin du package de clustering . Nous utilisons l'algorithme de clustering k-means le plus simple, qui doit spécifier le nombre de clusters souhaités. Nous divisons nos trois documents en deux groupes. La matrice d'entrée pour kmeans est une matrice d' kmeans , où les lignes représentent les entités et les colonnes représentent les modèles. Par conséquent, les matrices obtenues ci-dessus doivent être transposées.


 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 

En conséquence, nous voyons que le premier cluster contient un document, le cluster numéro 2 contient deux documents. De plus, la matrice contenant les centres des clusters R.centers montre clairement que la première colonne est «attirée» par le terme 2 . La deuxième colonne est déterminée par la présence des termes 1 et 3 .


Le package Clustering.jl contient un ensemble typique d'algorithmes de clustering, parmi lesquels: K-means, K-medoids, Affinity Propagation, Densité-based spatialing clustering of applications with noise (DBSCAN), Markov Clustering Algorithm (MCL), Fuzzy C-Means Clustering, Regroupement hiérarchique (simple, moyen, complet, liaison de Ward). Mais une analyse de leur applicabilité dépasse le cadre de cet article.


Le package TextAnalysis.jl est actuellement en cours de développement actif, donc certaines fonctions ne seront disponibles que lors de l'installation du package directement à partir du référentiel git. Ce n'est pas difficile à faire, mais cela ne peut être conseillé qu'à ceux qui ne prévoient pas de mettre la solution en service dans un avenir proche:


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

Cependant, vous ne devez pas ignorer ces fonctions dans la révision. Par conséquent, nous les considérons également.


L'une des améliorations est l'utilisation de la fonction de classement Okapi BM25 . Similaire aux modèles tf précédents. tf_idf , nous utilisons la bm_25(m) . L'utilisation de la matrice résultante est similaire aux cas précédents.


Une analyse de la tonalité des textes peut être effectuée à l'aide de méthodes:


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

De plus, doc est l'un des types de documents ci-dessus. handle_unknown - fonction de traitement des mots inconnus. L'analyse de la tonalité est implémentée à l'aide du package Flux.jl basé sur le package IMDB. La valeur de retour est comprise entre 0 et 1.


La généralisation des documents peut être implémentée à l'aide de la méthode summarize(d, ns) . Le premier argument est le document. Le second est ns= nombre de phrases à la fin.


 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." 

Un composant très important de toute bibliothèque d'analyse de texte est l'analyseur syntaxique actuellement en cours de développement, qui distingue les parties du discours - POS (partie du discours). Il existe plusieurs options pour l'utiliser. Pour plus de détails, voir Parties du balisage vocal.
. Tagging est appelé parce que pour chaque mot du texte source, un tag est formé qui signifie une partie du discours.


Deux options de mise en œuvre sont en cours d'élaboration. Le premier est l'algorithme Perceptron moyen. Le second est basé sur l'utilisation de l'architecture des réseaux de neurones LSTM, CNN et la méthode CRF. Voici un exemple de balisage de phrase simple.


 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" "." 

La liste des abréviations signifiant une partie du discours est tirée de Penn Treebank . En particulier, DT - Déterminateur, NN - Nom, singulier ou masse, VBZ - Verbe, 3e personne du singulier présent, Verbe, participe passé, IN - Préposition ou conjonction subordonnée, NNP - Nom propre, singulier.


Les résultats de ce balisage peuvent également être utilisés comme fonctionnalités supplémentaires pour la classification des documents.


Méthodes de réduction des dimensions


TextAnalysis propose deux options pour réduire la dimensionnalité en définissant des termes dépendants. Cette analyse sémantique latente - LSA et placement de Dirichlet latent - LDA.


La tâche principale du LSA est d'obtenir la décomposition de la matrice terme-document (à l'aide de TF-IDF) en 3 matrices, dont le produit correspond grosso modo à l'original.


 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 . Un exemple:


 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 , .


Conclusion


, , 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, .


Les références


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


All Articles