Biblioteca CPython "VKF" para aprendizado de máquina

Em uma postagem anterior do autor , um servidor da Web foi descrito para a realização de experimentos com o método VKF de aprendizado de máquina baseado na teoria da rede. Como alternativa ao uso de um servidor Web, este artigo tenta especificar o caminho para usar a biblioteca CPython diretamente. Nós reproduziremos sessões de trabalho de experimentos com matrizes Mushroom e Wine Quality do repositório de dados da UCI para testar algoritmos de aprendizado de máquina. Depois, haverá explicações sobre os formatos dos dados de entrada.







Introdução



Este artigo descreve uma nova versão de um programa de aprendizado de máquina com base na teoria da rede. A principal vantagem desta versão é uma interface para programadores Python para algoritmos eficientes programados em C ++.



Atualmente, os procedimentos de aprendizado de máquina (como redes neurais convolucionais, florestas aleatórias e máquinas de vetores de suporte) atingiram um nível muito alto, superando os seres humanos no reconhecimento de fala, vídeo e imagem. Eles, no entanto, não podem apresentar argumentos para provar a exatidão de suas conclusões.



Por outro lado, abordagens simbólicas ao aprendizado de máquina (programação lógica indutiva, cobertura de aprendizado usando programação inteira) têm uma complexidade computacional comprovadamente muito alta e são praticamente inaplicáveis ​​a amostras de tamanho relativamente pequeno.



A abordagem descrita aqui usa algoritmos probabilísticos para evitar esses problemas. O método ICF de aprendizado de máquina usa as técnicas da moderna teoria das redes algébricas (Análise de conceitos formais) e da teoria das probabilidades (especialmente cadeias de Markov). Mas agora você não precisa de conhecimentos de matemática avançada para usar e criar programas usando o sistema da CIF. O autor criou uma biblioteca (vkf.cp38-win32.pyd no Windows ou vkf.cpython-38-x86_64-linux-gnu.so no Linux) para fornecer acesso ao programa através de uma interface que os programadores do Python possam entender.



Existe uma abordagem parental em relação à abordagem descrita - inventada no início dos anos 80 do século XX, Doutor em Ciências Técnicas. prof. VK. Método JSM finlandês para geração automática de hipóteses. Agora, ele evoluiu, como diz seu criador, para um método de suporte automatizado à pesquisa científica. Ele permite que você use métodos de lógica de argumentação, verifique a estabilidade das hipóteses encontradas ao expandir a amostra de treinamento e combine os resultados de previsões usando várias estratégias de raciocínio JSM.



Infelizmente, o autor desta nota e seus colegas descobriram e investigaram algumas deficiências teóricas do método JSM:



  1. Na pior das hipóteses, o número de semelhanças geradas pode ser exponencialmente grande em comparação com o tamanho dos dados de entrada (amostra de treinamento).
  2. (NP-).
  3. .
  4. «» , .
  5. .


A pesquisa do autor está resumida no capítulo 2 de sua dissertação . O último ponto foi descoberto recentemente pelo autor, mas, na sua opinião, põe fim à abordagem da amostra em expansão.



Por fim, a comunidade JSM não oferece acesso ao código fonte de seus sistemas. Além disso, as linguagens de programação utilizadas (Fort e C #) não permitirão seu uso pelo público em geral. A única versão gratuita em C ++ do solucionador JSM conhecida pelo autor (por T.A. Volkovarobofreak) contém um erro irritante que às vezes leva ao término anormal dos cálculos.



Inicialmente, o autor planejava compartilhar o codificador desenvolvido para o sistema VKF-method com a comunidade JSM. Portanto, ele colocou todos os algoritmos simultaneamente aplicáveis ​​aos sistemas JSM e VKF em uma biblioteca separada (vkfencoder.cp38-win32.pyd no Windows ou vkfencoder.cpython-38-x86_64-linux-gnu.so no Linux) ... Infelizmente, esses algoritmos não foram reivindicados pela comunidade JSM. A biblioteca VKF implementa esses algoritmos (por exemplo, a classe vkf.FCA), mas depende de preencher tabelas não de arquivos, mas diretamente através da interface da web. Aqui vamos usar a biblioteca vkfencoder.



1 Procedimento para trabalhar com a biblioteca para recursos discretos





Vamos supor que o leitor saiba como instalar o servidor MariaDB + o MariaDB Connector / C (por padrão, usamos o endereço IP 127.0.0.1:3306 e o ​​usuário 'root' com senha 'toor'). Vamos começar instalando as bibliotecas vkfencoder e vkf no ambiente virtual de demonstração e criando um banco de dados MariaDB vazio chamado 'mushroom'.



krrguest@amd2700vii:~/src$ python3 -m venv demo 
krrguest@amd2700vii:~/src$ source demo/bin/activate 
(demo) krrguest@amd2700vii:~/src$ cd vkfencoder
(demo) krrguest@amd2700vii:~/src/vkfencoder$ python3 ./setup.py build
(demo) krrguest@amd2700vii:~/src/vkfencoder$ python3 ./setup.py install
(demo) krrguest@amd2700vii:~/src/vkfencoder$ cd ../vkf
(demo) krrguest@amd2700vii:~/src/vkf$ python3 ./setup.py build
(demo) krrguest@amd2700vii:~/src/vkf$ python3 ./setup.py install
(demo) krrguest@amd2700vii:~/src/vkf$ cd ../demo
(demo) krrguest@amd2700vii:~/src/demo$ mysql -u root -p
MariaDB [(none)]> CREATE DATABASE IF NOT EXISTS mushroom;
MariaDB [(none)]> exit;


Como resultado do trabalho, o banco de dados 'mushroom'

aparecerá , na pasta ~ / src / demo / lib / python3.8 / site-packages / vkfencoder-1.0.3-py3.8-linux-x86_64.egg / o arquivo vkfencoder.cpython-38 será exibido -x86_64-linux-gnu.so e o

arquivo vkf.cpython aparecerá na pasta ~ / src / demo / lib / python3.8 / site-packages / vkf-2.0.1-py3.8-linux-x86_64.egg / 38-x86_64-linux-gnu.so



Inicie o Python3 e execute um experimento CCF no array Cogumelos. Supõe-se que existam 3 arquivos na pasta ~ / src / demo / files / (mushrooms.xml, MUSHROOMS.train, MUSHROOMS.rest). A estrutura desses arquivos será descrita na próxima seção. O primeiro arquivo 'mushrooms.xml' define a estrutura da meia-rede inferior nos valores de cada atributo que descreve os cogumelos. O segundo e o terceiro arquivos são o arquivo 'agaricus-lepiota.data' dividido por um gerador de números aleatórios aproximadamente pela metade - o livro digitalizado "Identificador de cogumelos da América do Norte" publicado em Nova York em 1981. Os nomes a seguir são 'encoder', 'lattices', ' trens 'e' testes 'são os nomes das tabelas no banco de dados' cogumelo 'para, respectivamente, codificações de valores de recursos por substrings de bits, relações de cobertura para diagramas de semilátices nesses valores,exemplos de treinamento e teste.



(demo) krrguest@amd2700vii:~/src/demo$ Python3
>>> import vkfencoder
>>> xml = vkfencoder.XMLImport('./files/mushrooms.xml', 'encoder', 'lattices', 'mushroom', '127.0.0.1', 'root', 'toor')
>>> trn = vkfencoder.DataImport('./files/MUSHROOMS.train', 'e', 'encoder', 'trains', 'mushroom', '127.0.0.1', 'root', 'toor') 
>>> tst = vkfencoder.DataImport('./files/MUSHROOMS.rest', 'e', 'encoder', 'tests', 'mushroom', '127.0.0.1', 'root', 'toor') 


O 'e' nas duas últimas linhas corresponde à comestibilidade do cogumelo (é assim que a característica de destino é codificada nessa matriz).



É importante observar que existe uma classe vkfencoder.XMLExport que permite salvar informações de duas tabelas 'encoder' e 'lattices' em um arquivo xml que, após fazer alterações, pode ser processado novamente pela classe vkfencoder.XMLImport.



Agora, voltamos ao experimento VKF real: conectamos os codificadores, carregamos as hipóteses calculadas anteriormente (se houver), calculamos o número adicional (100) de hipóteses nos fluxos de número especificado (4) e os salvamos na tabela 'vkfhyps'.



>>> enc = vkf.Encoder('encoder', 'mushroom', '127.0.0.1', 'root', 'toor')
>>> ind = vkf.Induction()
>>> ind.load_discrete_hypotheses(enc, 'trains', 'vkfhyps', 'mushroom', '127.0.0.1', 'root', 'toor') 
>>> ind.add_hypotheses(100, 4) 
>>> ind.save_discrete_hypotheses(enc, 'vkfhyps', 'mushroom', '127.0.0.1', 'root', 'toor')


Você pode obter uma lista Python de todos os pares não triviais (feature_name, feature_value) para a hipótese do CCF numerada ndx



>>> ind.show_discrete_hypothesis(enc, ndx)


Uma das hipóteses para tomar uma decisão sobre a comestibilidade do cogumelo tem a forma



[('gill_attachment', 'free'), ('gill_spacing', 'close'), ('gill_size', 'broad'), ('stalk_shape', 'enlarging'), ('stalk_surface_below_ring', 'scaly'), ('veil_type', 'partial'), ('veil_color', 'white'), ('ring_number', 'one'), ('ring_type', 'pendant')]


Devido à natureza probabilística dos algoritmos do método CCF, ele pode não ser gerado, mas o autor provou que, com um número suficientemente grande de hipóteses de CCF geradas, uma hipótese muito semelhante a ela surgirá, o que quase sempre prevê a propriedade de destino para casos de teste importantes. Os detalhes estão no capítulo 4 da dissertação do autor.



Finalmente, nos voltamos para a previsão. Criamos uma amostra de teste para avaliar a qualidade das hipóteses geradas e ao mesmo tempo prever a propriedade alvo de seus elementos



>>> tes = vkf.TestSample(enc, ind, 'tests', 'mushroom', '127.0.0.1', 'root', 'toor')
>>> tes.correct_positive_cases()
>>> tes.correct_negative_cases()
>>> exit()


2 Descrição da estrutura de dados



2.1 Estruturas de treliça em recursos discretos



A classe vkfencoder.XMLImport analisa um arquivo XML que descreve a ordem entre os valores dos recursos, cria e preenche duas tabelas 'encoder' (convertendo cada valor em uma cadeia de bits) e 'treliças' (armazenando relacionamentos entre os valores de um recurso).

A estrutura do arquivo de entrada deve ser como



<?xml version="1.0"?>
<document name="mushrooms_db">
  <attribute name="cap_color">
    <vertices>
      <node string="null" char='_'></node>
      <node string="brown" char='n'></node>
      <node string="buff" char='b'></node>
      <node string="cinnamon" char='c'></node>
      <node string="gray" char='g'></node>
      <node string="green" char='r'></node>
      <node string="pink" char='p'></node>
      <node string="purple" char='u'></node>
      <node string="red" char='e'></node>
      <node string="white" char='w'></node>
      <node string="yellow" char='y'></node>
    </vertices>
    <edges>
      <arc source="brown" target="null"></arc>
      <arc source="buff" target="brown"></arc>
      <arc source="buff" target="yellow"></arc>
      <arc source="cinnamon" target="brown"></arc>
      <arc source="cinnamon" target="red"></arc>
      <arc source="gray" target="null"></arc>
      <arc source="green" target="null"></arc>
      <arc source="pink" target="red"></arc>
      <arc source="pink" target="white"></arc>
      <arc source="purple" target="red"></arc>
      <arc source="red" target="null"></arc>
      <arc source="white" target="null"></arc>
      <arc source="yellow" target="null"></arc>
    </edges>
  </attribute>
</document>


O exemplo acima representa a ordem entre os valores da terceira característica 'cap_color' (cor do chapéu) para a matriz Mushrooms do Repositório de Dados do Machine Learning (Universidade da Califórnia em Irvine). Cada campo 'atributo' representa uma estrutura de treliça nos valores do atributo correspondente. No nosso exemplo, o grupo corresponde ao atributo discreto 'cap_color'. A lista de todos os valores das características forma um subgrupo. Adicionamos um valor para indicar semelhanças triviais (ausentes). O restante dos valores corresponde à descrição no arquivo anexo 'agaricus-lepiota.names'.

A ordem entre eles é representada pelo conteúdo do subgrupo. Cada item descreve uma relação mais específica / mais geral entre pares de valores de características. Por exemplo, significa que uma tampa de cogumelo rosa é mais específica que uma tampa vermelha.



O autor provou o teorema da correção da representação gerada pelo construtor da classe vkfencoder.XMLImport e armazenada na tabela 'encoder'. Sua prova do algoritmo e do teorema da correção usa um ramo moderno da teoria da rede conhecida como Análise Formal de Conceito. Para detalhes, o leitor é novamente encaminhado ao trabalho de dissertação do autor (capítulo 1).



2.2 Estrutura da amostra para recursos discretos



Antes de tudo, observe que o leitor pode implementar seu próprio treinamento e carregador de casos de teste no banco de dados ou usar a classe vkfencoder.DataImport disponível na biblioteca. No segundo caso, o leitor deve levar em consideração que o recurso de destino deve estar na primeira posição e consistir em um caractere (por exemplo, '+' / '-', '1' / '0' ou 'e' / 'p').



A amostra de treinamento deve ser um arquivo CSV (com valores separados por vírgula) descrevendo exemplos de treinamento e exemplos de contador (exemplos que não possuem a propriedade de destino).



A estrutura do arquivo de entrada deve ser como



e,f,f,g,t,n,f,c,b,w,t,b,s,s,p,w,p,w,o,p,k,v,d
p,x,s,p,f,c,f,c,n,u,e,b,s,s,w,w,p,w,o,p,n,s,d
p,f,y,g,f,f,f,c,b,p,e,b,k,k,b,n,p,w,o,l,h,v,g
p,x,y,y,f,f,f,c,b,p,e,b,k,k,n,n,p,w,o,l,h,v,p
e,x,y,b,t,n,f,c,b,e,e,?,s,s,e,w,p,w,t,e,w,c,w
p,f,f,g,f,f,f,c,b,g,e,b,k,k,n,p,p,w,o,l,h,y,g
p,x,f,g,f,f,f,c,b,p,e,b,k,k,p,n,p,w,o,l,h,y,g
p,x,f,y,f,f,f,c,b,h,e,b,k,k,n,b,p,w,o,l,h,y,g
p,f,f,y,f,f,f,c,b,h,e,b,k,k,p,p,p,w,o,l,h,y,g
p,x,y,g,f,f,f,c,b,h,e,b,k,k,p,p,p,w,o,l,h,v,d
p,x,f,g,f,c,f,c,n,u,e,b,s,s,w,w,p,w,o,p,n,v,d
p,x,y,g,f,f,f,c,b,h,e,b,k,k,p,b,p,w,o,l,h,v,g
e,f,y,g,t,n,f,c,b,n,t,b,s,s,p,g,p,w,o,p,k,y,d
e,f,f,e,t,n,f,c,b,p,t,b,s,s,p,p,p,w,o,p,k,v,d
p,f,y,g,f,f,f,c,b,p,e,b,k,k,p,b,p,w,o,l,h,y,p


Cada linha aqui descreve um exemplo de treinamento. A primeira posição contém 'e' ou 'p', dependendo da comestibilidade / toxicidade do cogumelo descrito. O restante das posições (separadas por vírgulas) contém letras (nomes abreviados) ou cadeias (nomes completos dos valores do atributo correspondente). Por exemplo, a quarta posição corresponde ao atributo 'cup_color', onde 'g', 'p', 'y', 'b' e 'e' representam "cinza", "rosa", "amarelo", "areia" e "vermelho" , respectivamente. O ponto de interrogação no meio da tabela acima indica um valor ausente. No entanto, os valores das características restantes (não-alvo) podem ser cadeias de caracteres. É importante observar que, ao salvar no banco de dados, os espaços em seus nomes serão substituídos por caracteres '_'.



O arquivo com casos de teste tem a mesma forma, apenas o sistema não é informado sobre o sinal real do exemplo e sua previsão é comparada a ele para calcular a qualidade das hipóteses geradas e seu poder preditivo.



2.3 Estrutura de amostra para recursos contínuos



Nesse caso, as opções são novamente possíveis: o leitor pode implementar seu próprio treinador e carregador de casos de teste no banco de dados ou usar a classe vkfencoder.DataLoad disponível na biblioteca. No segundo caso, o leitor deve levar em consideração que o recurso de destino deve estar na primeira posição e consistir em um número natural (por exemplo, 0, 7, 15).



A amostra de treinamento deve ser um arquivo CSV (com valores separados por algum tipo de delimitador) descrevendo exemplos de treinamento e contra-exemplos (exemplos que não possuem a propriedade de destino).



A estrutura do arquivo de entrada deve ser como



"quality";"fixed acidity";"volatile acidity";"citric acid";"residual sugar";"chlorides";"free sulfur dioxide";"total sulfur dioxide";"density";"pH";"sulphates";"alcohol"
5;7.4;0.7;0;1.9;0.076;11;34;0.9978;3.51;0.56;9.4
5;7.8;0.88;0;2.6;0.098;25;67;0.9968;3.2;0.68;9.8
5;7.8;0.76;0.04;2.3;0.092;15;54;0.997;3.26;0.65;9.8
6;11.2;0.28;0.56;1.9;0.075;17;60;0.998;3.16;0.58;9.8
5;7.4;0.7;0;1.9;0.076;11;34;0.9978;3.51;0.56;9.4
5;7.4;0.66;0;1.8;0.075;13;40;0.9978;3.51;0.56;9.4
5;7.9;0.6;0.06;1.6;0.069;15;59;0.9964;3.3;0.46;9.4
7;7.3;0.65;0;1.2;0.065;15;21;0.9946;3.39;0.47;10
7;7.8;0.58;0.02;2;0.073;9;18;0.9968;3.36;0.57;9.5


No nosso caso, estas são as primeiras linhas do arquivo 'vv_red.csv' gerado a partir do arquivo 'winequality-red.csv' dos vinhos tintos portugueses da matriz Wine Quality do repositório de dados UCI para testar os procedimentos de aprendizado de máquina, reorganizando a última coluna desde o início. É importante observar que, ao salvar no banco de dados, os espaços em seus nomes serão substituídos por caracteres '_'.



3 experimento CCF em exemplos com recursos contínuos



Como foi escrito anteriormente, demonstraremos o trabalho na matriz Wine Quality do repositório UCI. Vamos começar criando um banco de dados vazio no MariaDB chamado 'red_wine'.



(demo) krrguest@amd2700vii:~/src/demo$ mysql -u root -p
MariaDB [(none)]> CREATE DATABASE IF NOT EXISTS red_wine;
MariaDB [(none)]> exit;


Como resultado do trabalho, o banco de dados 'red_wine' aparecerá.



Lançamos o Python3 e realizamos um experimento VKF no array Wine Quality. Supõe-se que exista um arquivo vv_red.csv na pasta ~ / src / demo / files /. A estrutura desses arquivos foi descrita no parágrafo anterior. Os seguintes nomes 'verges', 'complex', 'train' e 'tests' são os nomes de tabelas no banco de dados 'red_wine' para, respectivamente, limites (tanto para o inicial quanto para os recursos calculados por regressão), coeficientes de regressões logísticas significativas entre pares de recursos, treinamentos e casos de teste.



(demo) krrguest@amd2700vii:~/src/demo$ Python3
>>> import vkfencoder
>>> nam = vkfencoder.DataLoad('./files/vv_red.csv', 7, 'verges', 'trains', 'red_wine', ';', '127.0.0.1', 'root', 'toor')


O segundo argumento define o limite (7 no nosso caso), acima do qual o exemplo é declarado positivo. Argumento ';' corresponde ao delimitador (o padrão é ',').



Conforme indicado na nota anterior pelo autor , o procedimento é diferente do caso de recursos discretos. Primeiro, calculamos as regressões logísticas usando a classe vkf.Join, cujos coeficientes são armazenados na tabela 'complexa'.



>>> join = vkf.Join('trains', 'red_wine', '127.0.0.1', 'root', 'toor')
>>> join.compute_save('complex', 'red_wine', '127.0.0.1', 'root', 'toor') 


Agora, usando a teoria da informação, calculamos os limites usando a classe vkf.Generator, que, juntamente com o máximo e o mínimo, são armazenados na tabela 'verges'.



>>> gen = vkf.Generator('complex', 'trains', 'verges', 'red_wine', 7, '127.0.0.1', 'root', 'toor')


O quinto argumento define o número de limites nos recursos originais. Por padrão (e com um valor de 0), é calculado como o logaritmo do número de recursos. As regressões procuram um único limite.



Agora voltamos ao experimento VKF real: conectamos os codificadores, carregamos as hipóteses calculadas anteriormente (se houver), calculamos o número adicional (300) de hipóteses nos fluxos de número especificado (8) e as salvamos na tabela 'vkfhyps'.



>>> qual = vkf.Qualifier('verges', 'red_wine', '127.0.0.1', 'root', 'toor')
>>> beg = vkf.Beget('complex', 'red_wine', '127.0.0.1', 'root', 'toor')
>>> ind = vkf.Induction()
>>> ind.load_continuous_hypotheses(qual, beg, 'trains', 'vkfhyps', 'red_wine', '127.0.0.1', 'root', 'toor') 
>>> ind.add_hypotheses(300, 8) 
>>> ind.save_continuous_hypotheses(qual, 'vkfhyps', 'red_wine', '127.0.0.1', 'root', 'toor')


Você pode obter uma lista Python de triplos (feature_name, (lower_bound, upper_bound)) para a hipótese do CCF numerada ndx



>>> ind.show_continuous_hypothesis(enc, ndx)


Finalmente, nos voltamos para a previsão. Criamos uma amostra de teste para avaliar a qualidade das hipóteses geradas e ao mesmo tempo prever a propriedade alvo de seus elementos



>>> tes = vkf.TestSample(qual, ind, beg, 'trains', 'red_wine', '127.0.0.1', 'root', 'toor')
>>> tes.correct_positive_cases()
>>> tes.correct_negative_cases()
>>> exit()


Como temos apenas um arquivo, aqui usamos o conjunto de treinamento como casos de teste.



4. Nota sobre o upload de arquivos



Usei a biblioteca aiofiles para fazer upload de arquivos (como 'mushrooms.xml') para um servidor web. Aqui está um pedaço de código que você pode achar útil



import aiofiles
import os
import vkfencoder

async def write_file(path, body):
    async with aiofiles.open(path, 'wb') as file_write:
        await file_write.write(body)
    file_write.close()

async def xml_upload(request):
    #determine names of tables
    encoder_table_name = request.form.get('encoder_table_name')
    lattices_table_name = request.form.get('lattices_table_name')
    #Create upload folder if doesn't exist
    if not os.path.exists(Settings.UPLOAD_DIR):
        os.makedirs(Settings.UPLOAD_DIR)
    #Ensure a file was sent
    upload_file = request.files.get('file_name')
    if not upload_file:
        return response.redirect("/?error=no_file")
    #write the file to disk and redirect back to main
    short_name = upload_file.name.split('/')[-1]
    full_path = f"{Settings.UPLOAD_DIR}/{short_name}"
    try:
        await write_file(full_path, upload_file.body)
    except Exception as exc:
        return response.redirect('/?error='+ str(exc))
    return response.redirect('/ask_data')


Conclusão



A biblioteca vkf.cpython-38-x86_64-linux-gnu.so contém muitas classes e algoritmos ocultos. Eles usam conceitos matemáticos avançados como operações fechadas por uma, avaliação lenta, cadeias de Markov emparelhadas, estados transitórios da cadeia de Markov, métrica de variação total etc.



Na prática, experimentos com matrizes do Repositório de Dados para Aprendizado de Máquina (Universidade da Califórnia em Irvine ) mostrou a real aplicabilidade do programa C ++ 'Método VKF' a dados de tamanho médio (a matriz Adulto contém 32560 treinamentos e 16280 casos de teste).



O sistema 'Método VKF' superou (em termos de precisão preditiva) o sistema CLIP3 (Integer Programming Coverage Learning v.3) em uma matriz SPECT, em que CLIP3 é um programa criado pelos autores da matriz SPECT. Na matriz Cogumelos (dividida aleatoriamente em amostras de treinamento e teste), o sistema VKF Method mostrou 100% de precisão (tanto no que diz respeito ao critério de toxicidade do cogumelo quanto ao critério de comestibilidade). O programa também foi aplicado ao array Adulto para gerar mais de um milhão de hipóteses (sem falhas).



Os códigos-fonte da biblioteca vkf CPython são pré-moderados em savannah.nongnu.org. Os códigos para a biblioteca auxiliar vkfencoder podem ser obtidos no Bitbucket .



O autor gostaria de agradecer a seus colegas e alunos (D. A. Anokhin, E. D. Baranova, I. V. Nikulin, E. Yu. Sidorova, L. A. Yakimova) por seu apoio, discussões úteis e pesquisa conjunta. No entanto, como sempre, o autor é o único responsável por todas as deficiências existentes.



All Articles