Tensorflow: Interface Java

Criado em 9 nov. 2015  ·  112Comentários  ·  Fonte: tensorflow/tensorflow

Problema para rastrear o esforço da interface swig para java. Implementação iniciada - será atualizada com o progresso. Se alguém tiver comentários / dicas - sinta-se à vontade para participar da discussão!

Comentários muito úteis

Atualização: consegui fazer com que as coisas funcionassem com sucesso com javacpp (graças a @saudet ) e fazer com que os programas Java leiam / executem modelos do TensorFlow.

https://medium.com/google-cloud/how-to-invoke-a-trained-tensorflow-model-from-java-programs-27ed5f4f502d#.tx8nyds5v

Todos 112 comentários

Agradável!

Movendo este comentário aqui de https://github.com/tensorflow/tensorflow/issues/3 :


Há uma suíte de teste com convergência bastante boa, mas atualmente é principalmente Python com alguns testes C ++. Também há muitas funcionalidades para a construção de gráficos que atualmente são apenas Python, em particular a funcionalidade de diferenciação automática, embora isso não importe para avaliação de gráficos em Java. Existem planos para mover essa funcionalidade para o C ++ subjacente no futuro, ponto no qual as ligações SWIG do Java seriam mais úteis para a criação de gráficos.

Se alguém aceitar o desafio Java SWIG, ficaremos felizes em aceitá-lo com revisão pendente, etc., ponto em que faria parte de nossos testes contínuos. Os detalhes sobre a aceitação de contribuições estão mudando no momento, mas isso vai se estabilizar.

Ola pessoal
Também estamos interessados ​​em adaptar o TensorFlow para Java. @ravwojdyla Por acaso você já começou a trabalhar na interface Swig para Java? Se sim, poderíamos unir nossos esforços e colaborar nisso

Olá,
Estou trabalhando em um pacote SWIG da API C ++ principal. Você pode ver meu progresso até agora na bifurcação, mas o que está lá não está terminado; No momento, estou tendo um problema em que #include "tensorflow/core/lib/core/error_codes.pb.h" não pode ser resolvido e não consigo encontrar o arquivo pretendido em nenhum lugar dos arquivos do projeto. Qualquer tipo de contribuição seria muito apreciada.

Existem predefinições javacpp disponíveis para bibliotecas como Caffe e OpenCV. Veja também https://github.com/bytedeco/javacpp-presets/issues/111. Java-cpp habilita IOS com RoboVM

/ cc @saudet

@pslam - consegui trabalhar um pouco nisto - definitivamente

Olá pessoal, acredito que tenho vínculos bastante funcionais para JavaCPP: https://github.com/bytedeco/javacpp-presets/tree/master/tensorflow. Deixe-me saber se você ver algo que pode ser feito com SWIG, mas não JavaCPP. Eu definitivamente poderia usar o feedback. (Obrigado pelo cc @bhack!)

Muito bem feito @saudet! Quase terminei um wrap SWIG, mas parece que sua implementação funciona tão bem. Não vejo nada que meu envoltório SWIG possa fazer que o seu não possa fazer. JavaCPP parece muito legal, vou ter que pensar em usá-lo para projetos futuros.

Olá @kylevedder , você resolveu o problema relacionado a error_codes.pb.h ?
[Editado]
Todos os arquivos .pb.h são compilados de .proto

@tngan Sim, também descobri isso. Além disso, os arquivos .proto neste projeto requerem o uso de ProtoBuff3. Estou usando o Ubuntu 14.04 e o ProtoBuff3 não estava disponível no meu gerenciador de pacotes, então compilei a partir do código-fonte, obtido na versão beta 3.0.0 .

O obstáculo atual que estou tentando resolver é como fazer com que o ProtoBuff recurse em toda a árvore de arquivos e compile os arquivos .proto em .h e .cc arquivos; fazer cada pasta gradativamente resulta em falhas devido a dependências insatisfeitas de outros arquivos .proto ainda não compilados.

@kylevedder Seus wrappers SWIG estão em um repositório separado ou você está trabalhando no repositório tensorflow? protoc funciona de maneira semelhante a outros compiladores. Se você estiver trabalhando no repositório tensorflow ou usando o Bazel, precisará configurar os destinos de compilação do protobuf e as dependências entre eles.

Se você estiver trabalhando em um repositório separado e usando um sistema de compilação diferente, precisará usar o plug-in protobuf para esse sistema de compilação.

Ficarei feliz em ajudá-lo a configurar a construção, se desejar.

@davidzchen Obrigado pela oferta, toda e qualquer ajuda é muito apreciada.

O que tenho até agora:

Já configurei o Bazel e fiz com que ele compilasse em um arquivo .whl , que entreguei a pip e confirmei que posso executar o primeiro programa TensorFlow .

Eu gerei arquivos de wrapper SWIG em meu repositório bifurcado. Eles estão em uma pasta em core/javaWrapper . [[link] (https://github.com/kylevedder/tensorflow/tree/master/tensorflow/core/javaWrapper)]

O que estou tentando fazer:

Em última análise, meu objetivo é gerar um arquivo .so que pode ser chamado como uma biblioteca nativa em Java. Atualmente, estou tentando usar o g ++ para compilar todo o sistema em um arquivo .so ; no entanto, os arquivos .proto precisam primeiro ser expandidos em .h s e .cc s antes desta compilação, e é isso que estou tentando fazer com protoc .

Você pode ver minha tentativa de um script de wrap aqui para potencialmente ter uma ideia melhor do que estou chegando, embora até agora todas as minhas tentativas de usar protoc tenham sido diretório por diretório e, conseqüentemente, não no script.

Finalmente, qualquer feedback sobre áreas de melhoria seria muito apreciado. Obrigado!

@kylevedder Já tenho uma construção .so como parte das predefinições JavaCPP: https://github.com/bytedeco/javacpp-presets/tree/master/tensorflow. Graças ao Bazel, é muito simples. Basta aplicar um patch como este:

diff -ruN tensorflow/tensorflow/cc/BUILD tensorflow-patch/tensorflow/cc/BUILD
--- tensorflow/tensorflow/cc/BUILD  2015-11-22 00:00:02.441829192 +0900
+++ tensorflow-patch/tensorflow/cc/BUILD    2015-11-14 11:15:12.689330351 +0900
@@ -75,6 +75,17 @@
     ],
 )

+cc_binary(
+    name = "libtensorflow.so",
+    copts = tf_copts(),
+    linkshared = 1,
+    deps = [
+        ":cc_ops",
+        "//tensorflow/core:kernels",
+        "//tensorflow/core:tensorflow",
+    ],
+)
+
 filegroup(
     name = "all_files",
     srcs = glob(

E execute o Bazel assim, por exemplo:

bazel build -c opt //tensorflow/cc:libtensorflow.so

AFAIK, isso deve devorar praticamente qualquer coisa de interesse para a API C ++.

@saudet Existe uma razão pela qual você está usando uma regra cc_binary para construir a biblioteca compartilhada em vez de cc_library ? Você pode apenas ter uma regra cc_library com o nome tensorflow e o destino de compilação construirá uma biblioteca compartilhada chamada libtensorflow.so .

@kylevedder Se seu objetivo é gerar um arquivo .so , então algo semelhante ao que @saudet sugeriu funcionaria.

Se você precisar usar os protos TensorFlow no código Java, precisará adicionar dependências de seus destinos de compilação java_* Bazel aos destinos proto_library que geram as classes Java de .proto arquivos.

Ainda temos um pouco de trabalho a fazer antes de abrir o código das regras nativas proto_library (consulte bazelbuild / bazel # 52), mas enquanto isso, o TensorFlow usa o cc_proto_library e py_proto_library Regras , e para Java, você deve ser capaz de usar a regra Java genproto incluída no Bazel . Vou verificar com a equipe para descobrir qual é o cronograma para proto_library e se valeria a pena unificar as regras fornecidas pelo Protobuf com genproto .

Alguns outros comentários:

  • Acho que seria melhor manter os nomes dos diretórios consistentes e usar java_wrapper vez de javaWrapper
  • Talvez um lugar melhor para o wrapper Java seja //tensorflow/java/wrapper vez de //tensorflow/core/java_wrapper ?
  • Internamente, temos algumas regras de construção que pegam .swig arquivos e geram os fontes. Isso é mais ideal porque evitaríamos o check-in dos arquivos gerados. Posso dar uma olhada para ver como seria difícil para nós adicionar algumas regras de compilação SWIG para o Bazel para tornar esse tipo de coisa mais fácil.

@davidzchen Nenhuma razão em particular. Eu sou novo no Bazel e apenas usando linkshared=1 como já mencionei na lista de discussão funcionou. Então, obrigado pela dica! Eu estarei atualizando isso.

@saudet Obrigado! Eu estava apenas verificando se não era um problema com o Bazel. :) Sinta-se à vontade para me informar ou abrir um bug se você tiver algum problema.

@saudet Obrigado pelas informações sobre como usar o Bazel. Eu também sou novo nisso e não sabia que era capaz de gerar .so dessa maneira.

@davidzchen Obrigado pelo adendo sobre o uso de cc_library , modifiquei o exemplo de @saudet de acordo quando implementei minha compilação de wrapper Bazil . Além disso, obrigado pela entrada sobre a estrutura de diretórios; Eu atualizei minha estrutura de pastas para alinhar com suas sugestões.

Além disso, não fui muito claro em meu comentário anterior sobre a geração de .so arquivos; embora meu objetivo seja gerar um arquivo .so partir da fonte original, também quero incluir o arquivo .cxx que o SWIG gera dentro de .so para facilitar o JNI chamadas. Atualmente, estou tendo um problema em que não consigo compilar o arquivo SWIG gerado .cxx ; ele está tentando fazer referência a JNI.h , um cabeçalho localizado em $JAVA_HOME/include/ , mas não consigo fazer com que o Bazel entenda o caminho de inclusão externo.

@davidzchen Hum, não, cc_library não funciona. Não vejo nenhuma outra maneira de fazer o Bazel passar a opção -shared para o compilador: http://bazel.io/docs/be/c-cpp.html.

@saudet Não acho que você precise passar -shared sozinho. cc_library deve construir um .so por padrão. Isso funciona para você?

@kylevedder Você não poderá adicionar os cabeçalhos JNI dessa forma, pois eles estão fora da área de trabalho. No entanto, o Bazel inclui o JDK local como um repositório local e fornece vários destinos integrados (consulte jdk.WORKSPACE e jdk.BUILD ) que você pode usar para depender do JDK local. Eles estão incluídos em cada área de trabalho do Bazel por padrão.

O próprio Bazel usa JNI e faz interface com o JDK local dessa maneira (consulte src/main/native/BUILD ). Neste arquivo BUILD, há dois genrule s para copiar os cabeçalhos JNI e um destino cc_library para a biblioteca que está construindo que usa JNI que depende dos cabeçalhos e um includes = ["."] para que o código C ++ possa incluir o cabeçalho JNI com #include <jni.h> . No momento, isso não está documentado porque estamos trabalhando em uma série de melhorias no mecanismo do repositório externo, e o nome @local-jdk pode mudar, mas podemos usá-lo para TensorFlow e qualquer outro projeto Bazel que use JNI nesse ínterim .

Aqui está um patch para o seu arquivo BUILD que adiciona os destinos genrule para copiar os cabeçalhos JNI de que você precisa e algumas alterações no destino cc_library para configurar as dependências corretas, a saber:

  1. Adicione jni.h e jni_md.h , que são copiados para o pacote atual por genrule s para srcs
  2. Adicione uma dependência em //tensorflow/core para que você possa incluir os cabeçalhos em tensorflow/core/public . Observe que os cabeçalhos ou qualquer arquivo de origem em um diretório separado estão em um pacote separado do ponto de vista do Bazel e você precisará adicionar uma dependência no destino de compilação que contém esses arquivos.
diff --git a/tensorflow/core/java/wrapper/BUILD b/tensorflow/core/java/wrapper/BUILD
index 72b4076..04a3394 100644
--- a/tensorflow/core/java/wrapper/BUILD
+++ b/tensorflow/core/java/wrapper/BUILD
@@ -7,10 +7,30 @@ exports_files(["LICENSE"])
 load("/tensorflow/tensorflow", "tf_copts")
 load("/tensorflow/tensorflow", "tf_gen_op_wrappers_cc")

+genrule(
+    name = "copy_link_jni_md_header",
+    srcs = ["//external:jni_md_header-linux"],
+    outs = ["jni_md.h"],
+    cmd = "cp -f $< $@",
+)
+
+genrule(
+    name = "copy_link_jni_header",
+    srcs = ["//external:jni_header"],
+    outs = ["jni.h"],
+    cmd = "cp -f $< $@",
+)
+
 cc_library(
     name = "java_wrapper",
-    srcs = glob(["*.cc","*.cxx","*.h"]),
-    copts = ["-I$$JAVA_HOME/include/", "-I$$JAVA_HOME/include/linux/"],
+    srcs = glob(["*.cc", "*.cxx", "*.h"]) + [
+        ":jni.h",
+        ":jni_md.h",
+    ],
+    includes = ["."],
+    deps = [
+        "//tensorflow/core",
+    ],
     visibility = ["//visibility:public"],
 )

Observe que, em geral, as ações de compilação no Bazel são executadas a partir da raiz da árvore de origem e você precisaria alterar as inclusões em seu arquivo SWIG da seguinte forma e, em seguida, gerar novamente os arquivos C ++ para que tenham as inclusões corretas como Nós vamos:

diff --git a/tensorflow/core/java/wrapper/tensor_c_api.i b/tensorflow/core/java/wrapper/tensor_c_api.i
index d08b571..9ab1fa1 100644
--- a/tensorflow/core/java/wrapper/tensor_c_api.i
+++ b/tensorflow/core/java/wrapper/tensor_c_api.i
@@ -1,8 +1,8 @@
 %module tensor_c_api_module
 %{
-#include "../../public/tensor_c_api.h"
+#include "tensorflow/core/public/tensor_c_api.h"
 %}
-%include "../../public/tensor_c_api.h"
+%include "tensorflow/core/public/tensor_c_api.h"
 %include "stddef.h"

Uma vez que isso funcione, você terá a compilação JNI configurada para Linux, uma vez que copy_link_jni_md_header genrule apenas copia o cabeçalho específico do Linux. Para que ele copie o cabeçalho JNI específico da plataforma correto, precisaríamos fazer o seguinte:

  1. Configure cpu config_setting s para outras plataformas. Atualmente, tensorflow tem um config_setting para --cpu=darwin em tensorflow/python/BUILD . Provavelmente deveríamos mover esse pacote mais apropriado, como //tensorflow/core . Basicamente, desejaríamos o mesmo conjunto de config_setting s que o Bazel (consulte src/BUILD ).
  2. Faça com que copy_link_jni_md_header copie o cabeçalho JNI correto com base na configuração definida usando select() , semelhante ao do Bazel . Nosso genrule seria parecido com o seguinte:
genrule(
    name = "copy_link_jni_md_header",
    srcs = select({
        "//tensorflow/core:darwin": ["//external:jni_md_header-darwin"],
        "//tensorflow/core:darwin_x86_64": ["//external:jni_md_header-darwin"],
        "//tensorflow/core:freebsd": ["//external:jni_md_header-freebsd"],
        "//conditions:default": ["//external:jni_md_header-linux"],
    }),
    outs = ["jni_md.h"],
    cmd = "cp -f $< $@",
)

Terei todo o gosto em ajudá-lo se tiver algum problema. Deixe-me saber se isso funciona para você.

@davidzchen cc_library gera vários arquivos .a, mas nenhum arquivo .so. Estou usando o 0.1.0 conforme recomendado anteriormente para o TensorFlow ... Talvez esteja corrigido no 0.1.1? Vou ter que tentar de novo.

@davidzchen Muito obrigado por sua ajuda. Eu segui suas instruções e atualizei o arquivo Java wrapper arquivo SWIG .i como você sugeriu. Além disso, movi o script de wrapper de core/java/wrapper para o diretório raiz e atualizei os links de acordo.

Por enquanto, pulei a generalização de genrule para o arquivo jni_md.h , em vez de me concentrar em tentar construir libtensorflow.so . Infelizmente, parece-me que libtensorflow.so não está sendo gerado; Acabei pesquisando em todo o meu sistema de arquivos por algo com o nome de alguma variante de "libtensorflow" e nada de relevante apareceu. Ele pode ter um nome diferente ou pode ser um simples caso de erro do usuário. Além disso, há uma possibilidade de que ele pode estar relacionada com a questão que @saudet está experimentando com o cc_library regra para .so geração.

Mais uma vez, obrigado por toda a ajuda, agradeço muito.

Desculpe, descobri que estava errado. Para construir um .so que inclui as dependências transitivas, o que @saudet fez usando cc_binary com linkshared = 1 e name = "libtensorflow.so" estava correto. Da documentação cc_binary.linkshared :

Crie uma biblioteca compartilhada. Para habilitar este atributo, inclua linkshared = 1 em sua regra. Por padrão, essa opção está desativada. Se ativá-lo, você deve nomear seu binário libfoo.so (ou qualquer que seja a convenção de nomenclatura de bibliotecas na plataforma de destino) para algum valor razoável de foo.

A principal diferença entre os .so construídos por cc_library targets e .so construídos com cc_binary usando o método descrito acima é que cc_library artefatos contêm apenas o código em srcs . É por isso que construir cc_library destinos sem srcs e apenas deps , como //tensorflow/core , não produz nenhum artefato. Por outro lado, cc_binary alvos irão vincular todas as dependências transitivas.

Eu peço desculpas pela confusão. Talvez devêssemos melhorar nossa documentação e adicionar um exemplo sobre a construção de .so s.

Acho que você deve seguir essas etapas para criar o Tensorflow e todas as suas dependências. Estamos trabalhando para portar o TensorFlow para node.js e implementei um script de shell para compilar e obter apenas fontes essenciais de todo o repo:
https://github.com/node-tensorflow/node-tensorflow/blob/1.0.0/tools/install.sh#L233 -L282

@davidzchen Obrigado pelas informações sobre a criação de um .so . Eu atualizei minha configuração de acordo e criei um tensorflow/core/java/wrapper/example com um testador _extremely_ básico para provar que as chamadas de função JNI para .so funcionam. Observe que createWrapper.sh deve ser executado antes de executar compileAndRun.sh .

Vou tentar melhorar o wrapper SWIG e dar um exemplo melhor, o que tenho agora é simplesmente uma prova mínima de ligações funcionando.

Finalmente, quero agradecer a @davidzchen e @saudet por toda a ajuda; Eu não teria sido capaz de fazer isso sem eles.

Agradável! Obrigado por trabalhar nisso, @kylevedder!

Se você estiver interessado, posso tentar integrar seus scripts createWrapper.sh e compileAndRun.sh na construção do Bazel 1) criando a regra SWIG do Skylark e 2) usando as regras Java do Bazel para construir o código Java.

@davidzchen Isso seria ótimo! Trabalharei para melhorar o wrapper SWIG e o exemplo básico.

Concluí as predefinições para JavaCPP e transferi a amostra example_trainer.cc :
https://github.com/bytedeco/javacpp-presets/tree/master/tensorflow
Ansioso para comparar isso com um invólucro equivalente usando SWIG!

Parece que o link da API está quebrado: http://bytedeco.org/javacpp-presets/tensorflow/apidocs/

@verdiyanto Desculpe, ainda não tenho CI, mas fazer o upload dos documentos da API é fácil, pelo menos fiz isso. Aproveitar!

@saudet Bom trabalho nas predefinições JavaCPP!

Uma atualização sobre meu trabalho: trabalhei mais no wrapper SWIG e você pode ver o trabalho que fiz aqui . No entanto, estou numa encruzilhada e não tenho a certeza da melhor maneira de proceder.

Sou bastante novo em SWIG, dado que este é meu primeiro grande projeto usando-o, então li a documentação SWIG sobre SWIG Basics e em SWIG e Java que mostra como o SWIG funciona e como envolver C / C ++ com wrappers SWIG Java.

A documentação explica como SWIG converte ponteiros em C / C ++ em objetos Java opacos, razão pela qual você obtém classes como SWIGTYPE_p_void geradas por SWIG. O problema é que não há uma maneira fácil de converter POJOs nessas classes SWIG.

Assim, por exemplo, em tensor_c_api.h , o método C TF_CreateTensor() pega um void* que aponta para os dados de entrada e um parâmetro size para especificar o tamanho de os dados de entrada em bytes. Este é um padrão de design perfeitamente razoável para C / C ++, mas completamente sem sentido em Java. O método Java gerado por SWIG TF_CreateTensor() usa um objeto SWIGTYPE_p_void como seus dados, junto com size , mas não há como converter um POJO como String em SWIGTYPE_p_void sem escrever muitos códigos à mão.

E esta é a encruzilhada em que estou atualmente: eu escrevo uma tonelada de métodos de conversão C / C ++ que pegam qualquer tipo definido em TF_DataType e convertem em void* , ou escrevo vários Os mapas de tipo SWIG para fazer a mesma coisa. A documentação SWIG não parece favorecer nenhuma das soluções, já que ambas parecem ser intercambiáveis.

Portanto, a questão é: funções de conversão C / C ++ ou mapas de tipos SWIG?

@kylevedder Vejo que você está começando a entender por que criei o JavaCPP em primeiro lugar. :)

Estou usando presets JavaCPP 's @saudet, extremamente útil, muito obrigado! Estou usando para construir uma interface Clojure para tensorflow.

Alguns comentários:

a) Há oportunidade de simplificação / uma camada de nível superior

Grande parte da API JavaCPP replica a funcionalidade protobuf que pode ser alcançada diretamente na JVM, sem a ponte. Levei um pouco para perceber isso, mas é simplesmente construir um objeto protobuf usando as ligações JavaCPP, produzindo essa representação independente de plataforma usando interoperabilidade e, em seguida, colocando-a na Sessão.

Acabei usando apenas protobufs baseados em jvm para construir o gráfico diretamente, contornando as funções do construtor JavaCPP. Isso tem várias vantagens - uma API mais simples para programar e também um formato .toString agradável que mostra o protobuf legível por humanos.

Particularmente para Clojure, é muito mais fácil descrever o gráfico tensorflow em termos de estruturas de dados e, em seguida, convertê-los diretamente em protobuf, do que procurar e invocar uma função construtora para cada nó em minha estrutura de dados.

b) Melhorias de construção e embalagem

Não sou especialista em construir código nativo ou nas ferramentas de construção usadas nesses projetos. Seria ótimo ter artefatos pervertidos; em particular, se eles também incluíssem as classes de protobuf java geradas. Demorei muito tempo para descobrir como fazer isso.

c) Seria útil ter um pequeno número de casos de teste de gráfico para direcionar.

No momento, minha metodologia é um tanto complicada: usar as funções do construtor JavaCPP para gerar um gráfico, mashall-lo em meus protobufs JVM e ver a forma legível por humanos e descobrir como construir meus próprios construtores para fazer o mesmo formulário.

Seria útil ter uma pequena coleção de gráficos muito simples que exercem as principais funcionalidades do TensorFlow, para que pessoas como eu tenham um conjunto razoável de casos de teste para direcionar para interoperabilidade em diferentes linguagens.

De qualquer forma, obrigado pelos esforços de todos e continuem com o bom trabalho!

@kovasb Obrigado pelo feedback! Obviamente, há muito a ser feito para tornar a interface mais natural para Java, Scala, Clojure, etc.

Se você tiver classes auxiliares para integrar a API C ++ com a API protobuf Java, sinta-se à vontade para colocar tudo isso no pacote a seguir, incluindo as próprias classes protobuf Java geradas, e enviar um PR:
https://github.com/bytedeco/javacpp-presets/tree/master/tensorflow/src/main/java/org/bytedeco/javacpp/helper
É para isso que ele se destina e será automaticamente empacotado no artefato Maven, algo que o Bazel parece não oferecer suporte. Em qualquer caso, obrigado por investigar isso!

@kovasb Uma interface clojure parece muito interessante. Ainda tem algum código para compartilhar?

Obrigado!

Portanto, as pessoas neste tópico também estão cientes, em https://github.com/tensorflow/tensorflow/issues/3 foi levantado: a diferenciação automática não funciona atualmente, a menos que você use o TF da API python. Isso parece um obstáculo enquanto se aguarda a portabilidade da funcionalidade para C ++.

Eu não entendo muito bem o fluxo de dados, mas talvez seja possível iniciar o material auxiliar do python junto com a biblioteca C ++?

Outra solução que estou procurando é apenas usar Jpy ou uma das outras pontes (alguém tem recomendações?) JyNi também parece bastante interessante, mas muito longe do horário nobre (embora seja ótimo ver mais impulso / comunidade por trás dele)

Se JyNi for resolvido, ele + jython daria à JVM uma história realmente incrível sobre a interoperabilidade do ecossistema python. Pode-se sonhar.

1 para uma interface Java!

se pudéssemos usar javaCPP, o SWIG ainda é necessário? devemos colaborar para implementar a interface SWIG?

@maxiwu Gosto de pensar que o JavaCPP faz um trabalho melhor do que o SWIG, mas sou totalmente favorável a compará-los para realmente provar isso :)

@kovasb Eu estaria muito interessado em ajudar / contribuir com a interface do Clojure.

@sorenmacbeth envie um e-mail para meu nome, ponto, sobrenome no gmail, fico feliz em orientar você sobre o que tenho ...

Parece que temos aqui uma predefinição Javacpp bastante completa. É uma solução aceitável para "a equipe"?

@saudet Estou tentando construir uma cópia dos wrappers JavaCPP, mas parece que, devido à rápida taxa de mudança da fonte de tensorflow, eles não são compatíveis com a versão 0.6.0 ou o branch master de hoje. Seria possível atualizá-los com um ponteiro para a confirmação / versão exata do tensorflow com que foram testados?

@nikitakit Acabei de fazer uma atualização para o branch master aqui: https://github.com/bytedeco/javacpp-presets/commit/43bdcdf03beaaddb4bd5badf5d4f79669e9e78dd

Ao contrário do Caffe, porém, o TensorFlow realmente parece obter uma versão a cada mês ou mais, então acho que vou começar a estabilizar as ligações nesses pontos, começando com a próxima versão (0.7.0?)

@martinwicke O que você acha?

Quando há uma ligação Java estável, fico feliz em trabalhar na API Scala.

/ cc @databricks

@kovasb Acho que perdi a primeira vez. Você está dizendo que toda a bela mágica de auto-diferenciação que obtemos com o uso do TensorFlow por meio do python é implementada dentro do python, não dentro das bibliotecas c ++? Portanto, na prática, uma API Java precisaria reimplementar tudo isso ou seria apenas outra biblioteca numérica? Não estou familiarizado o suficiente com os elementos internos do TensorFlow ou a cola python para entender exatamente o que é feito de trabalho pesado e onde.

@drdozer esse é o meu entendimento, com base nos comentários de @girving e depois olhando um pouco a fonte. Reimplementar coisas em Java parece um obstáculo. Eu sugiro olhar para os comentários em # 3

Se alguém estiver realmente interessado, eu apenas recomendaria tentar fazer alguns exemplos de treinamento usando a API Java (até agora eu acabei de ver / fazer o caminho de encaminhamento).

Eu me pergunto o quão longe chegaríamos executando o código Python com Jython ...

Eu acredito que a camada da API Python tem muita lógica que a API da camada C ++ não expõe.
Eu estava tentando seguir o caminho do JavaCpp, mas no final haverá muitos códigos de duplicação e será difícil manter a consistência quando algo mudar na implementação do Python.

Provavelmente, o caminho mais fácil é usar Jython como @saudet mencionou antes ...

Ele foi atribuído em https://github.com/tensorflow/tensorflow/issues/476 a @ josh11b. Se ele está trabalhando nisso, não faz sentido usar Jython.

Se usarmos jython, o código c ++ ainda funcionará? Estou tentando usar isso para um servidor que está em Java, mas estou preso entre tentar uma rota Java diretamente ou apenas enviar os dados por um soquete para um processo Python

Gostaria de mencionar que, embora a API Java não inclua muitos recursos como auto-diferenciação, não achei que isso seja uma barreira para o meu próprio trabalho. Tive grande sucesso ao gerar um modelo em python, serializando-o em um arquivo .proto e abrindo-o por meio do wrapper Java para treinamento. O mesmo pode ser feito para o tempo de teste, pois acredito que a funcionalidade Saver está disponível por meio das APIs C ++ e Java.

+1

@saudet
Obrigado por criar javacpp e as predefinições para tensorflow. Consegui recriar com êxito um gráfico Python em Java, mas estou preso tentando restaurar de um arquivo de modelo salvo. Esta linha não funciona:

Tensor fn = novo Tensor (tensorflow.DT_STRING, novo TensorShape (1));
Buffer CharBuffer = fn.createBuffer ();
buffer.put ("modelfile.tf");
sessão.Run (...);

mas o CharBuffer é NULL. Se eu alterar DT_STRING para DT_FLOAT, obtenho um FloatBuffer, mas DT_STRING não parece funcionar.

@nikitakit, você disse que conseguiu fazer isso funcionar. você poderia compartilhar seu código?

@lakshmanok

EDIT: desculpe, interpretou mal o que você disse aqui. Não posso fornecer nenhuma ajuda para usar protetores externos de Java

Para referência, a parte do meu código que importa gráficos de tensorflow está aqui: https://gist.github.com/nikitakit/d3ec270aee9d930267cec3efa844d5aa

Está no Scala, mas a portabilidade para Java / outra linguagem JVM deve ser direta.

Meu código para realmente executar nós no gráfico está, infelizmente, fortemente vinculado a uma estrutura Scala que estou usando, então você terá que contar com os documentos da API do tensorflow para esta parte.

Alguém chegou a algum lugar com a incorporação do ambiente tensorflow python no jvm? Diga com jython + JyNI? Ou tudo isso é um pouco experimental demais para funcionar de maneira confiável?

Atualmente, estou trabalhando na expansão da API C para adicionar suporte para definição de gráfico. Não tenho certeza de quando isso será feito, mas é um de nossos objetivos antes de 1.0.

Estou trabalhando no uso de fluxo tensor de java. Estou abordando o problema usando jython e modificando a biblioteca cpython de fluxo de tensor para acomodar outro interpretador de python. o cpython deve continuar funcionando perfeitamente e meu código está detectando se o interpretador é Jython e modificando as importações / módulos para permitir que ele funcione. Embaixo dele, usa as ligações javacpp para libtensorflow_cc.so. Isso é algo que a equipe do Google estaria aberta para ver no repositório oficial? @vrv

Parece uma boa prova de conceito, mas acho que uma vinculação oficial provavelmente desejaria vincular mais nativamente do que passar por Python :(

não, em vez de chamar o wrapper c-python, chamamos o wrapper javaccp. Portanto, seria a mesma coisa que o fluxo do tensor cpython, mas avaliado a partir da JVM usando Jython. Reimplementar toda a lógica python em outra linguagem parece demais, você acaba com outra API. Os vínculos do javacpp permitem que você execute a inferência sem problemas, mas o modelo deve ser construído / treinado a partir de um script cpython no momento.

Alguém já viu como fazer o tensorflow funcionar com Kotlin? Parece um ajuste mais natural e ainda é 100% java no final do dia. Acho que a linguagem Kotlin é um meio-termo muito bom entre o python e o Java puro.

Atualização: consegui fazer com que as coisas funcionassem com sucesso com javacpp (graças a @saudet ) e fazer com que os programas Java leiam / executem modelos do TensorFlow.

https://medium.com/google-cloud/how-to-invoke-a-trained-tensorflow-model-from-java-programs-27ed5f4f502d#.tx8nyds5v

Obrigado @lakshmanok e @saudet . O projeto javacpp parece implementar a maioria das APIs do TensorFlow. Estamos tentando executar o tensorflow / servindo em Java.

A API é simples e definida por protobuf . Agora implementamos o servidor e queremos implementar o cliente em Java. Ele só precisa construir TensorProto em Java e invocar a chamada gRPC . O TensorFlow fornece funções auxiliares para converter várias matrizes de dimensão para Python e C ++, mas não para Java.

Você pode dizer como usar javacpp ou implementar por nós mesmos para isso?

O que você está procurando provavelmente já está em https://github.com/bytedeco/javacpp-presets/blob/master/tensorflow/src/main/java/org/bytedeco/javacpp/helper/tensorflow.java, mas me avise se algo está faltando lá. Obrigado!

ainda está sendo trabalhado? existe um repositório oficial do github para este projeto de portabilidade? Eu vejo alguns repositórios aleatórios, mas não posso dizer.

Sim, mas provavelmente em algum momento de outubro / novembro. Estamos usando a API C em vez de mudar para a API C ++. Enquanto isso, você pode usar as ligações que saudet mencionou.

como você chegou à conclusão de usar a API C? estamos trabalhando em um
interface ruby ​​usando swig:
http://github.com/somaticio/tensorflow.rb

Na terça - feira, 13 de setembro de 2016 às 18:22, Jonathan Hseu
escreveu:

Sim, mas provavelmente em algum momento de outubro / novembro. Estamos usando a API C
em vez de mudar para a API C ++. Enquanto isso, você pode usar o
ligações que saudet mencionou.

-
Você está recebendo isso porque comentou.
Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/tensorflow/tensorflow/issues/5#issuecomment -246844192,
ou silenciar o tópico
https://github.com/notifications/unsubscribe-auth/AAA5v3g86Z6D1rz-aTGdMyMWnQZhrZUYks5qpyIJgaJpZM4Getd8
.

No futuro, preferiríamos que todas as ligações de linguagem usassem a API C. Um documento está chegando.

Você pode ver um exemplo de uso aqui:
https://github.com/tensorflow/tensorflow/tree/master/tensorflow/go

Não há urgência, porém, e construir em cima do SWIG está bom por enquanto.

@jhseu Isso significa que a API C será expandida para cobrir tudo o que as ligações Python atualmente têm acesso?

Uau, grande mudança. Gostaria que isso fosse decidido antes. Enfim, para ver a documentação
mais cedo?

Na quarta-feira, 14 de setembro de 2016 às 17:56, Samuel Audet [email protected]
escreveu:

@jhseu https://github.com/jhseu Isso significa que a API C será
expandido para cobrir tudo o que as ligações Python atualmente têm acesso?

-
Você está recebendo isso porque comentou.
Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/tensorflow/tensorflow/issues/5#issuecomment -247167887,
ou silenciar o tópico
https://github.com/notifications/unsubscribe-auth/AAA5vwfBJoZC2s33_7E9Xy6-NYNUjHjnks5qqG2FgaJpZM4Getd8
.

@saudet A maioria das funcionalidades, exceto no curto prazo, algumas coisas estarão faltando (como gradientes, otimizadores).
@jtoy Não há urgência para você migrar. SWIG continuará a funcionar por um tempo.

Os documentos apenas descrevem como fazer isso e as convenções de nomenclatura. Você pode começar a migrar para a API C sem eles, embora:
https://github.com/tensorflow/tensorflow/blob/master/tensorflow/c/c_api.h

Obrigado @saudet . Eu descobri isso no stackoverflow sobre como gerar TensorProto com a API protobuf pura. E aqui está o código de

@ tobegit3hub Legal, se você pode fazer isso funcionar com a API C ++, adicione-o ao pacote auxiliar dos Presets JavaCPP e envie uma solicitação de pull! Esse cara estaria interessado em algo assim: https://github.com/bytedeco/javacpp-presets/issues/240

@girving O javacpp já resolveu o problema?
Eu quero contribuir para tensorflow java api, eu prefiro implementá-lo como python.

Olá pessoal, alguém já começou a trabalhar nas ligações da linguagem Java / Scala usando a API C?
(em vez de construir em cima do SWIG)

Eu tenho uma interface Java / Scala funcional para tensorflow usando apenas a API C via JNR . Infelizmente, ainda não tenho permissão para abri-lo. Vou postar aqui se e quando liberar. Ainda é um trabalho em andamento, mas é muito funcional.

@jdolson A API que você expõe aceita os objetos de buffer de protocolo do TensorFlow? Um dos maiores problemas que tive ao usar os presets javacpp de @saudet é que quando você está manipulando objetos tensores no código do cliente Java, você está lidando com um org.tensorflow.framework.TensorProto que é gerado pelo compilador de buffer de protocolo quando configurado para gerar java. Mas no wrapper da API TensorFlow, você está lidando com um org.bytedeco.javacpp.tensorflow.TensorProto.TensorProto que é gerado por javacpp quando apontado para o código c gerado pelo compilador de buffer de protocolo quando configurado para produzir C. Da mesma forma, você não pode usar diretamente os tensores do seu código Java ao chamar a API TensorFlow empacotada.

@Intropy Sim, eu compilo todas as fontes de tensorflow *.proto para o código-fonte Java com protoc e uso essas classes na API.

@jhseu A interface da API C ainda está

@eaplatanios : A API C é basicamente estável (e será oficialmente assim em 1.0) e utilizável, embora não esteja completa (ainda sem a capacidade de graduar cálculos automaticamente para o gráfico). Um documento que descreve como a API C pode ser usada para construir ligações de linguagem está em https://www.tensorflow.org/how_tos/language_bindings/index.html

A API Go foi implementada usando a API C como um primeiro exemplo de seguir o documento acima.

Esperamos que as ligações Java sejam construídas em cima disso também (usando JNI) e começamos a explorar isso um pouco. Quaisquer comentários / aprendizados pessoas têm baseado no uso de @saudet 's trabalho maravilhoso com a obtenção de JavaCPP trabalho seria bom saber.

Eu tenho algumas sugestões baseadas no uso de ligações JavaCPP.

Primeiro, uma vez que os buffers de protocolo compilam diretamente em java, as versões de java devem ser usadas. De preferência, acho que os buffers de protocolo que fazem parte da API devem estar disponíveis separadamente como um módulo maven e devem vir com as definições de proto para que as pessoas em uma pilha Java tenham uma maneira fácil de obter as definições binárias e também maneira de obter as definições de proto para inclusão em outras definições de proto.

Em segundo lugar, seria útil encontrar a versão mínima da libc de que o TensorFlow precisa e construir com base nela.

Terceiro, é muito mais fácil usar uma API projetada cuidadosamente do que uma gerada automaticamente. Eu sei que isso é óbvio e parece uma tentativa de JavaCPP. Eu não quero que seja. Estou muito feliz que a interface gerada automaticamente exista. É _is_ utilizável. Mas requer circunlóquios estranhos, tem muitas verrugas e é muito difícil ler o código para descobrir como fazer o que você está tentando fazer. Eu gostaria que essa sugestão fosse mais útil do que "você deve torná-la boa", mas acho que o ponto é que veja como a API C ++ e a API python são diferentes. Ambos são diretos porque se ajustam ao ambiente de uma maneira que é improvável que o código convertido automaticamente corresponda.

Teria sido melhor suportar C backend do Swig e gerar TF C API via Swig também: https://github.com/swig/swig/issues/800 para que outras linguagens como Go, Ruby, R possam usar o C api para escrever suas próprias ligações.

Temos uma API C existente para adicionar suporte para qualquer linguagem com um C FFI:
https://github.com/tensorflow/tensorflow/blob/master/tensorflow/c/c_api.h

(E é isso que é usado para construir as ligações Go, Java, Rust etc. para TensorFlow)

A API C pode ser acessada usando JNA ?

@jhseu Eu quis dizer, ele poderia ter sido gerado talvez a partir da API C ++ anteriormente, antes de implementar manualmente a API C.

@ Quantum64 , aqui está uma ligação Scala do tensorflow que usa JNA.

Uma vez que este problema ainda está aberto, como
https://github.com/tensorflow/tensorflow/tree/master/tensorflow/java
sendo implementado e qual foi o PR para o commit?

@hsaputra : Você poderia explicar o que está procurando? Existem vários commits que contribuem para o código em tensorflow/java , a maioria dos quais são referenciados neste problema (como 2b1cd28, d73a266 e muitos outros no meio)

HI @asimshankar , obrigado pela resposta.

Só estou me perguntando qual foi o caminho que tensorflow/java percorreu para implementar a API Java, já que este tíquete não está fechado.
Houve discussões sobre o uso de JavaCPP vs SWIG vs chamada via Jython.

Parece que tensorflow/java foi implementado com JNI direto para chamar APIs C?

Correto.

Ei,

Acabei de colocar essas amarrações Swig funcionando ontem. Tenho um pedido de alteração da API. Atualmente, a fim de gerar Tensores, a reflexão é necessária e o formato das matrizes é um pouco complicado, pois exigem o uso de matrizes Java nativas n-dimensionais. Podemos manter essa interface, mas também adicionar alguns métodos para criar tensores que requerem arrays unidimensionais e especificar a forma usando outro array de long? Eu imagino que poderia ser algo assim:

double[] matrix = {1.414, 2.718, 3.1415, 3.4, 56.7, 89.0};
long[] shape = {2, 3};

// add a method for each primitive type
org.tensorflow.Tensor tensor = org.tensorflow.Tensor.createDouble(matrix, shape);

Isso também levaria à possibilidade de criar tensores int8, int16, uint8, uint16, uint32, o que ajudará na compatibilidade.

Devo tornar isso um problema? Ou está tudo bem aqui?

Além disso, estou mais do que feliz em tentar desenvolver esses métodos.

@hollinwilkins : Espero que o PR # 6577 trate disso, com apenas um pequeno ajuste em seu método de fábrica proposto:

Tensor tensor = Tensor.create(shape, DoubleBuffer.wrap(matrix));

@asimshankar Isso é ótimo! Obrigado pela resposta rápida. Parece que também está muito perto de ser mesclado: +1:

Estou tentando usar a nova API java e descobri algumas coisas que a tornam mais difícil de usar do que acho que deveria ser:

  1. A API java deve aceitar um objeto GraphDef. Atualmente, ele aceita apenas uma matriz de bytes que representa o binário serializado do buffer de protocolo GraphDef. É estranho exigir uma etapa de serialização / desserialização no limite da biblioteca.
  2. Session.Runner.feed deve ser capaz de aceitar org.tensorflow.framework.TensorProto ou deve haver uma boa maneira de criar org.tensorflow.Tensor a partir de org.tensorflow.framework.TensorProto.
  3. Session.Runner.run retorna uma lista de objetos Tensor. Semelhante ao anterior, deve haver uma maneira fácil de obter a saída do TensorProto diretamente ou fornecendo ao org.tensorflow.Tensor uma boa maneira de converter para TensorProto.
  4. Session.Runner.run engole Status. Deve haver uma maneira de obter essas informações sobre as falhas, talvez lançando uma exceção.

Além disso, é possível que eu tenha perdido a maneira de lidar com isso, mas me parece que não consigo obter todos os tipos de tensores suportados na saída do run. Por exemplo, se meu tensor de saída for do tipo d INT16, não há como extrair o valor dele. Não há Tensor.shortValue ou algo semelhante, e Tensor.intValue parece exigir uma correspondência exata. Estou baseando isso na leitura de DEFINE_GET_SCALAR_METHOD em tensor_jni.cc.

@Intropy : Obrigado por seus comentários e eles definitivamente fazem sentido. Por enquanto, posso compartilhar alguns pensamentos rápidos com você:

RE: protobufs: neste ponto, estamos tentando manter a API principal independente de protobufs por uma série de razões (incluindo o uso em sistemas de recursos restritos onde algo como nanproto pode ser mais apropriado). Então, essa é a razão pela qual temos hesitado, mas é algo em que estamos pensando e sugestões são bem-vindas. Uma possibilidade é ter todas as funcionalidades relacionadas ao protobuf em um pacote separado para que haja uma separação clara.

Então, voltando aos seus pontos:

  1. Veja acima. Porém, aposto que há muitos casos em que byte[] faz mais sentido (como ler o gráfico de um arquivo ou canal de rede)

  2. Ponto tomado

  3. Veja acima.

  4. Session.runner.run não deve estar engolindo o status. Se houver um erro, uma exceção será lançada ( session_jni.cc:166 ). Se isso não estiver acontecendo, envie um bug.

Você está certo, nem todos os tipos são suportados ainda, mas deve ser fácil de adicionar. Se você tiver uma necessidade urgente dos tipos ausentes, sinta-se à vontade para registrar um problema e / ou enviar um PR. Contribuições são bem-vindas :)

@asimshankar Obrigado por seus pensamentos.

Em relação ao primeiro ponto, não é realmente grande coisa. Como você disse, há momentos em que um byte [] faz mais sentido. Em meu próprio caso de uso, tenho um InputStream, que é fácil de converter em byte []. A API do buffer de protocolo torna a conversão direta. Eu apenas considero o byte [] uma verruga na API porque você vai ter que desserializar de qualquer maneira (em TF_GraphImportGraphDef) e dessa forma você perde alguma segurança de tipo. Há também a serialização json do proto3 a se considerar.

No estado de engolir, você está certo. Perdi a exceção não verificada.

A maneira mais óbvia de lidar com 2 e 3 é dar ao org.tensorflow.Tensor uma fábrica que converte de um TensorProto em alguns toTensorProto (). Se os casos de uso de recursos limitados são o problema com os buffers de protocolo, então as pessoas nessas circunstâncias simplesmente não podem usar essas funções. O problema é que as pessoas que usam essas funções estariam pagando o custo de uma conversão que provavelmente poderia ser evitada fazendo com que o Tensor armazenasse seus dados diretamente em um protobuff. Eu nunca trabalhei com jni antes, então estou tendo problemas para acompanhar como os dados são armazenados, mas parece que ele está essencialmente tratando nativeHandle como um ponteiro para um TF_Tensor que tem um TensorBuffer que é tratado essencialmente como um void de tamanho *.

Podemos separar esse problema e arquivar problemas separados para cada recurso na interface Java? Isso tornará mais fácil rastrear / analisar, então podemos fechar este problema.

@drpngx : Minha intenção é conseguir mais algumas mudanças no (leitura de tensores de buffers) antes de fechar isso como fizemos para Go e ter recursos / bugs arquivados individualmente. Esperançosamente em breve.

Parece bom, obrigado!

Tudo bem, parece que temos uma base suficiente para construir (por exemplo, o suficiente para construir o exemplo LabelImage e as pessoas estão preenchendo bugs / solicitações de recursos mais específicos.

Vou encerrar esse problema. Ainda há muito a fazer na API Java, mas vamos discutir / rastrear isso em questões separadas. Obrigado!

@asimshankar , estamos no processo de seleção do framework de aprendizado profundo (mxnet / tf) e nosso etl / api é baseado no fluxo spark / akka ... Existe um plano para adicionar suporte a distribuição de tempo de distribuição à API Java para executar treinamento paralelo de modelo usando ps nós? O nó ps é crítico para nós em muitos casos de uso ... presets javacpp podem ser mais fáceis de exportar para o primeiro corte, uma vez que a API C em si não parece ter distribuído_runtime ...

@ debasish83 : Incluir o tempo de execução distribuído por si só é trivial, mas há um monte de construções de nível superior na API Python, como a classe Estimator que cuida de um monte de coisas (pontos de verificação, salvamento de resumo etc. que tornar a visualização por meio do TensorBoard trivial) que pode torná-lo mais adequado para executar os jobs de treinamento em Python.

Tudo isso pode ser construído usando os primitivos existentes na API Java, mas a abordagem certa dependerá de suas necessidades específicas.

Talvez devêssemos sincronizar fora do tópico?

@asimshankar Já existe uma maneira da ligação de tensorflow Java para recuperar informações de um graphDef (construído a partir do arquivo .pb do gráfico no disco) como a lista de nós, formato de entrada e saída ou é um recurso de entrada? Obrigado!

@asimshankar Não tenho certeza de entender o que está faltando para fazer um treinamento com TF Java. É um problema da biblioteca numérica (falta numpy)? Quero dizer, se você não está interessado em TensorBoard dataviz, mas apenas em treinamento, usando uma biblioteca numérica Java nativa, por que usar Python apenas para treinamento (como você sugere sobre a classe Estimator )?

Obrigado.

Qual é o estado dos modelos de treinamento em Java? Tenho pensado em escrever um plugin ImageJ (popular e gratuito conjunto de análise de imagens) para aplicar abordagens como https://arxiv.org/pdf/1505.04597.pdf (recentemente muito popular na segmentação de imagens para rastreamento de células e aplicações biomédicas). Acho que seria útil fornecer uma variedade de modelos pré-treinados e permitir que os usuários os refinem para seu caso de uso específico. Tenho pesquisado o DL4J para esse propósito. Existem planos concretos para permitir o encaixe nas ligações TF Java?

@bergwerf : O treinamento em Java é certamente possível, embora não seja particularmente conveniente.
Você pode encontrar uma amostra em https://github.com/tensorflow/models/tree/master/samples/languages/java/training

(Além disso, tenho certeza que você está ciente, mas veja também https://imagej.net/TensorFlow)

Oh, incrível! Minhas informações devem estar desatualizadas ;-). Eu pensei que tinha lido
em algum lugar, a API Java se destinava apenas a prever com pré-treinados
modelos. Vou olhar para o exemplo.

Na quarta-feira, 28 de março de 2018, 22:01, Asim Shankar [email protected] escreveu:

@bergwerf https://github.com/bergwerf : O treinamento em Java é certamente
possivelmente, se não for particularmente conveniente.
Você pode encontrar uma amostra em
https://github.com/tensorflow/models/tree/master/samples/languages/java/training

-
Você está recebendo isso porque foi mencionado.
Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/tensorflow/tensorflow/issues/5#issuecomment-377015867 ,
ou silenciar o tópico
https://github.com/notifications/unsubscribe-auth/AEQJ1UD9-xACQAII5996ees_UFJ_NzL-ks5ti-wSgaJpZM4Getd8
.

@asimshankar isso é incrível 👍 💯 🥇, vou adicionar ao meu repositório https://github.com/loretoparisi/tensorflow-java

Esta página foi útil?
0 / 5 - 0 avaliações