
A análise e o processamento de textos em um idioma natural é uma tarefa constantemente atual que foi resolvida, está sendo resolvida e será resolvida de todas as formas disponíveis. Hoje eu gostaria de falar sobre ferramentas de solução para resolver esse problema, a saber, na linguagem Julia. Obviamente, devido à juventude da linguagem, não existem ferramentas de análise desenvolvidas, como, por exemplo, Stanford CoreNLP, Apache OpenNLP, GATE etc., como, por exemplo, para a linguagem Java. No entanto, mesmo as bibliotecas que já foram desenvolvidas podem ser usadas para solucionar problemas típicos e podem ser recomendadas como ponto de entrada para estudantes interessados no campo do processamento de texto. E a simplicidade sintática de Julia e suas ferramentas matemáticas avançadas facilitam a imersão nas tarefas de agrupar e classificar textos.
O objetivo deste artigo é revisar as ferramentas de processamento de texto de Julia com algumas explicações sobre seu uso. Equilibraremos entre uma breve lista de oportunidades para aqueles que estão no tópico da PNL, mas gostaríamos de ver exatamente as ferramentas de Julia e explicações e exemplos de aplicações mais detalhados para aqueles que decidiram mergulhar na área da PNL (Natural Language Processing) pela primeira vez.
Bem, agora, vamos à visão geral do pacote.
TextAnalysis.jl
O pacote TextAnalysis.jl é uma biblioteca básica que implementa um conjunto mínimo de funções típicas de processamento de texto. É com ela que começamos. Exemplos são parcialmente retirados da documentação .
O documento
A entidade básica é um documento.
Os seguintes tipos são suportados:
- FileDocument - um documento representado por um simples arquivo de texto em disco
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 - um documento representado por uma string UTF-8 e armazenado na RAM. A estrutura StringDocument fornece o armazenamento de texto como um todo.
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 - um documento que é uma sequência de tokens UTF-8 (palavras destacadas). A estrutura
TokenDocument
armazena um conjunto de tokens; no entanto, o texto completo não pode ser restaurado sem perda.
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 - um documento apresentado como um conjunto de n-gramas em uma representação UTF8, ou seja, uma sequência de
n
caracteres UTF-8 e um contador para sua ocorrência. Essa opção de apresentar um documento é uma das maneiras mais simples de evitar alguns problemas de morfologia das línguas, erros de digitação e características das estruturas da linguagem nos textos analisados. No entanto, a taxa para isso é uma diminuição na qualidade da análise de texto em comparação com os métodos em que as informações do idioma são levadas em consideração.
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 uma opção curta:
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"))
Um documento também pode ser criado simplesmente usando o construtor genérico de documentos, e a biblioteca encontrará a implementação apropriada do documento.
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***
Como você pode ver, o corpo do documento consiste em texto / tokens e metadados. O texto do documento pode ser obtido usando o método 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"
O exemplo demonstra um documento com tokens analisados automaticamente. Vimos que a chamada para text(td)
emitiu um aviso de que o texto foi restaurado apenas aproximadamente, pois o TokenDocument
não armazena delimitadores de palavras. A chamada de tokens(td)
tornou possível obter exatamente as palavras destacadas.
Você pode solicitar metadados de um documento:
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"
E todos eles podem ser alterados pelas funções correspondentes. A notação de funções de modificação em Julia é a mesma que na linguagem Ruby. Uma função que modifica um objeto possui um sufixo !
:
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"
Recursos de strings com UTF-8
Julia suporta codificação UTF-8 ao processar seqüências de caracteres, portanto, não há problemas com o uso de alfabetos não latinos. Quaisquer opções de processamento de caracteres estão naturalmente disponíveis. No entanto, lembre-se de que os índices de linha para Julia são bytes, não caracteres. E cada caractere pode ser representado por um número diferente de bytes. E existem métodos separados para trabalhar com caracteres UNICODE. Consulte Unicode-e-UTF-8 para obter detalhes. Mas aqui está um exemplo simples. Vamos definir uma linha com caracteres UNICODE matemáticos separados de x e y por espaços:
julia> s = "\u2200 x \u2203 y" "∀ x ∃ y" julia> length(s)
Agora vamos ver os índices:
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)
O exemplo mostra claramente que o índice 1
nos permitiu obter o símbolo ∀
. Mas todos os índices subseqüentes, com até 3 inclusive, levaram a um erro. E apenas o quarto índice produziu um espaço, como o próximo caractere na string. No entanto, para determinar os limites dos caracteres pelos índices em uma string, existem funções úteis prevind
(índice anterior), nextind
(próximo índice) e thisind
(este índice). Por exemplo, para a lacuna encontrada acima, perguntamos onde está a borda da anterior:
julia> prevind(s, 4) 1
Temos o índice 1 como o início do símbolo ∀
.
julia> thisind(s, 3) 1
Verifiquei o índice 3 e obtive o mesmo 1 válido.
Se precisarmos "revisar" todos os personagens, isso pode ser feito de pelo menos duas maneiras simples:
1) usando o design:
julia> for c in s print(c) end ∀ x ∃ y
2) usando eachindex
enumerador de 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é-processamento de documentos
Se o texto do documento foi obtido de alguma representação externa, é bem possível que possa haver erros de codificação no fluxo de bytes. Para eliminá-los, use a função remove_corrupt_utf8!(sd)
. O argumento é o documento discutido acima.
A principal função para processar documentos no pacote TextAnalysis é prepare!(...)
. Por exemplo, remova os sinais de pontuação do texto:
julia> str = StringDocument("here are some punctuations !!!...") julia> prepare!(str, strip_punctuation) julia> text(str) "here are some punctuations "
Além disso, uma etapa útil no processamento de texto é a conversão de todas as letras para minúsculas, pois isso simplifica a comparação posterior de palavras. Nesse caso, no caso geral, deve-se entender que podemos perder informações importantes sobre o texto, por exemplo, o fato de a palavra ser um nome próprio ou a palavra ser o limite de uma frase. Mas tudo depende do modelo de processamento adicional. A minúscula é feita pela função remove_case!()
.
julia> sd = StringDocument("Lear is mad") A StringDocument{String} julia> remove_case!(sd) julia> text(sd) "lear is mad"
Ao longo do caminho, podemos excluir palavras ilegíveis, ou seja, aquelas que não são úteis na recuperação e análise de informações para correspondências. Isso pode ser feito explicitamente usando a função remove_words!(…)
e uma matriz dessas palavras de parada.
julia> remove_words!(sd, ["lear"]) julia> text(sd) " is mad"
Entre as palavras a serem excluídas, há também artigos, preposições, pronomes, números e apenas palavras de parada, que são parasitárias na frequência de ocorrência. Para cada idioma específico, esses dicionários são individuais. E eles são definidos no pacote Languages.jl Os números nos incomodam, porque no futuro modelam um documento térmico, eles podem aumentar bastante a dimensão da matriz sem melhorar, por exemplo, o agrupamento de textos. No entanto, em problemas de pesquisa, por exemplo, nem sempre é possível descartar números.
Entre os métodos de limpeza disponíveis estão as seguintes opções:
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)
As opções podem ser combinadas. Por exemplo, em uma chamada para se prepare!
remova simultaneamente artigos, números e tags html - prepare!(sd, strip_articles| strip_numbers| strip_html_tags)
Outro tipo de processamento é destacar a base das palavras, removendo finais e sufixos. Isso permite combinar diferentes formas de palavras e reduzir drasticamente a dimensionalidade do modelo de apresentação do documento. Dicionários são necessários para isso, portanto o idioma dos documentos deve ser claramente indicado. Exemplo de processamento em russo:
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) " "
Corpo do documento
O corpus é entendido como um conjunto de documentos que serão processados de acordo com as mesmas regras. O pacote TextAnalysis implementa a formação de um termo matriz de documentos . E para sua construção, precisamos ter imediatamente um conjunto completo de documentos. Em um exemplo simples para documentos:
D1 = "I like databases"
D2 = "I hate databases"
essa matriz se parece com:
As colunas são representadas pelas palavras dos documentos e as linhas são identificadores (ou índices) de documentos. Assim, a célula será 0 se a palavra (termo) não aparecer no documento. E 1 se ocorrer inúmeras vezes. Modelos mais complexos levam em consideração a frequência de ocorrência (modelo TF) e a significância em relação a todo o corpo (TF-IDF).
Podemos construir o corpo usando o construtor Corpus()
:
crps = Corpus([StringDocument("Document 1"), StringDocument("Document 2")])
Se solicitarmos uma lista de termos imediatamente, obteremos:
julia> lexicon(crps) Dict{String,Int64} with 0 entries
E, aqui, forçando a biblioteca a recontar todos os termos que fazem parte do caso usando update_lexicon!(crps)
, obtemos um resultado diferente:
julia> update_lexicon!(crps) julia> lexicon(crps) Dict{String,Int64} with 3 entries: "1" => 1 "2" => 1 "Document" => 2
Ou seja, podemos ver os termos selecionados (palavras e números) e o número de entradas no corpo do documento.
Ao mesmo tempo, podemos esclarecer a frequência do termo, por exemplo, "Documento":
julia> lexical_frequency(crps, "Document") 0.5
Além disso, podemos criar um índice reverso, ou seja, para cada tópico, obter os números dos documentos no caso. Esse índice é usado na recuperação de informações, quando você precisa encontrar uma lista de documentos em que eles aparecem na lista de termos:
julia> update_inverse_index!(crps) julia> inverse_index(crps) Dict{String,Array{Int64,1}} with 3 entries: "1" => [1] "2" => [2] "Document" => [1, 2]
Para o caso como um todo, você pode aplicar as funções de pré-processamento, as mesmas de cada documento individual. Outro método da função de prepare!
é usado prepare!
considerado anteriormente. Aqui, o primeiro argumento é o caso.
julia> crps = Corpus([StringDocument("Document ..!!"), StringDocument("Document ..!!")]) julia> prepare!(crps, strip_punctuation) julia> text(crps[1]) "Document " julia> text(crps[2]) "Document "
Além de documentos individuais, você pode solicitar metadados para todo o corpo.
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"
Você pode definir os valores iguais para todo o corpo de uma só vez ou individualmente para documentos específicos, passando uma matriz com valores elemento a elemento para eles.
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"])
Destaque de Recursos
A extração de recursos é um dos estágios básicos do aprendizado de máquina. Isso não está diretamente relacionado ao tópico deste artigo, mas na documentação do pacote TextAnalysis, uma seção bastante grande é dedicada à identificação de recursos nessa formulação. Esta seção inclui, de fato, a construção de uma matriz termo-documento e muitos outros métodos. https://juliatext.imtqy.com/TextAnalysis.jl/dev/features/
Consideramos brevemente as opções propostas.
O modelo básico para apresentação de documentos é um modelo em que um conjunto de palavras é armazenado para cada documento. Além disso, sua posição não é importante. Portanto, em um escritor no idioma inglês, essa opção é chamada Saco de palavras. Para cada palavra, apenas o fato de sua presença no documento, a frequência de ocorrência (TF - Frequency Term) ou um modelo que leva em consideração a frequência de ocorrência do termo no corpo como um todo (TF-IDF - Term Frequency - Inverse Document Frequency) é importante.
Tome o exemplo mais simples com três documentos contendo os termos Document
, 1
, 2
, 3
.
julia> using TextAnalysis julia> crps = Corpus([StringDocument("Document 1"), StringDocument("Document 2"), StringDocument("Document 1 3")])
Não usaremos pré-processamento. Mas criaremos o léxico e a matriz completos do termo documento:
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))
A variável m
tem um valor do tipo DocumentTermMatrix
. No resultado impresso, vemos que a dimensão é de 3 documentos em 4 termos, que incluem a palavra Document
e os números 1
, 2
, 3
. Para uso posterior do modelo, precisamos de uma matriz na representação tradicional. Podemos obtê-lo usando o método 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
Essa opção é representada pelo tipo SparseMatrixCSC
, que é econômico em representar uma matriz muito esparsa, mas há apenas um número limitado de bibliotecas que a suportam. O problema do tamanho do termo matriz de documentos se deve ao fato de o número de termos crescer muito rapidamente com o número de documentos processados. Se você não pré-processar documentos, absolutamente todas as palavras com todas as suas formas de palavras, números e datas cairão nessa matriz. Mesmo que o número de formas de palavras seja reduzido devido à redução da forma principal, o número de hastes restantes será da ordem de milhares - dezenas de milhares. Ou seja, a dimensão completa do termo matriz de documentos é determinada pelo produto total dessa quantidade pelo número de documentos processados. Uma matriz completa requer o armazenamento não apenas de unidades, mas também de zeros, no entanto, é mais fácil de usar que o SparseMatrixCSC
. Você pode obtê-lo por outro método dtm(..., :dense)
ou convertendo uma matriz esparsa em uma matriz completa usando o método collect()
:
julia> dtm(m, :dense) 3×4 Array{Int64,2}: 1 0 0 1 0 1 0 1 1 0 1 1
Se você imprimir uma matriz de termos, será fácil ver em cada linha a composição original dos documentos (a ordem original dos termos não é levada em consideração).
julia> m.terms 4-element Array{String,1}: "1" "2" "3" "Document"
O termo matriz de documentos para modelos de frequência pode ser obtido usando os métodos tf()
e 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
É fácil ver o significado dos termos para cada um dos documentos. Os dois primeiros documentos contêm dois termos. O último é três. Então, o peso deles é reduzido.
E para o método TF-IDF e 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
E, nesse modelo, é fácil ver que o termo Document
, encontrado em todos os documentos, tem um valor de 0. Mas o termo 3
no terceiro documento ganhou mais peso que 1
no mesmo documento, pois 1
também é encontrado no primeiro documento. .
As matrizes resultantes são muito fáceis de usar, por exemplo, para resolver o problema de agrupar documentos. Para fazer isso, você precisará do pacote Clustering . Utilizamos o algoritmo de clustering k-means mais simples, que precisa especificar o número de clusters desejados. Dividimos nossos três documentos em dois grupos. A matriz de entrada para kmeans
é uma matriz de recursos, em que linhas representam recursos e colunas representam padrões. Portanto, as matrizes obtidas acima devem ser transpostas.
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)
Como resultado, vemos que o primeiro cluster contém um documento, o número do cluster 2 contém dois documentos. Além disso, a matriz que contém os centros dos agrupamentos de R.centers
mostra claramente que a primeira coluna é "atraída" pelo termo 2
. A segunda coluna é determinada pela presença dos termos 1
e 3
.
O pacote Clustering.jl
contém um conjunto típico de algoritmos de clustering, entre eles: K-means, K-medoids, Affinity Propagation, Cluster espacial baseado em densidade de aplicativos com ruído (DBSCAN), Markov Clustering Algorithm (MCL), Fuzzy C-Means Clustering, Clustering hierárquico (único, médio, completo, ligação de Ward). Mas uma análise de sua aplicabilidade está além do escopo deste artigo.
O pacote TextAnalysis.jl
está atualmente em desenvolvimento ativo, portanto, algumas funções estarão disponíveis apenas ao instalar o pacote diretamente do repositório git. Não é difícil fazer isso, mas pode ser aconselhável apenas para aqueles que não planejam colocar a solução em operação em um futuro próximo:
julia> ] (v1.2) pkg> add https://github.com/JuliaText/TextAnalysis.jl
No entanto, você não deve ignorar essas funções na revisão. Portanto, nós os consideramos também.
Uma das melhorias é o uso da função de classificação Okapi BM25 . Semelhante aos modelos tf
anteriores. tf_idf
, usamos o bm_25(m)
. O uso da matriz resultante é semelhante aos casos anteriores.
Uma análise da tonalidade dos textos pode ser feita usando os métodos:
model = SentimentAnalyzer(doc) model = SentimentAnalyzer(doc, handle_unknown)
Além disso, doc
é um dos tipos de documento acima. handle_unknown
- função para processar palavras desconhecidas. A análise de tonalidade é implementada usando o pacote Flux.jl, com base no pacote IMDB. O valor de retorno está no intervalo de 0 a 1.
A generalização de documentos pode ser implementada usando o método summarize(d, ns)
. O primeiro argumento é o documento. O segundo é ns=
número de frases no final.
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."
Um componente muito importante de qualquer biblioteca de análise de texto é o analisador sintático atualmente em desenvolvimento, que distingue partes do discurso - POS (parte do discurso). Existem várias opções para usá-lo. Para detalhes, consulte Partes da marcação de fala.
. Tagging
é chamada porque, para cada palavra no texto de origem, é formada uma marcação que significa parte do discurso.
Duas opções para implementação estão em desenvolvimento. O primeiro é o algoritmo médio de Perceptron. O segundo é baseado no uso da arquitetura de rede neural LSTMs, CNN e o método CRF. Aqui está um exemplo de uma marcação de sentença simples.
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" "."
A lista de abreviações que significa parte do discurso é retirada do Penn Treebank . DT - Determinador, NN - Substantivo ou singular, VBZ - Verbo, terceira pessoa do singular do presente, Verbo, particípio passado, IN - Preposição ou conjunção subordinada, PNN - Nome próprio, singular.
Os resultados dessa marcação também podem ser usados como recursos adicionais para a classificação de documentos.
Métodos de redução de dimensão
O TextAnalysis fornece duas opções para reduzir a dimensionalidade, definindo termos dependentes. Essa análise semântica latente - LSA e colocação latente de Dirichlet - LDA.
A principal tarefa do LSA é obter a decomposição da matriz termo-documento (usando TF-IDF) em 3 matrizes, cujo produto corresponde aproximadamente ao 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.0 … 0.0 0.0; 0.0 0.0 … 0.0 1.0])
, -, TF-IDF . SVD, , .
LDA . Um exemplo:
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
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.jl — NER . NERTagger()
:
:
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())
compare
— . , 1 — . 0 — .
, Jaro-Winkler. , . RatcliffObershelp, , . , . .
compare("mariners vs angels", "angels vs mariners", RatcliffObershelp())
, , . , 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
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)
— :
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 — 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
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 , .
Conclusão
, , 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, .
Referências