
Compartilhar:
Steve é um ex-membro da equipe da Vonage. Ele atuou como Developer Advocate .NET na Vonage, engenheiro de software full-stack poliglota, especializado em IA/ML
“Ei, Facebook, que raça de cachorro é essa?” Adicionando aprendizado de máquina ao Messenger
Tempo de leitura: 9 minutos
As redes neurais convolucionais (CNNs) oferecem um mecanismo poderoso e escalável para realizar a classificação de imagens. Elas podem ser relativamente difíceis de construir, treinar e ajustar do zero, e é isso que torna ferramentas como o TensorFlow e os modelos Inception tão indispensáveis para aprimorar nossos fluxos de trabalho de aprendizado de máquina.
Dito isso, para nós, profissionais do .NET, executar scripts em Python a partir de um shell dentro do aplicativo não é exatamente a solução ideal, e é isso que torna o lançamento da biblioteca ML.NET para TensorFlow tão empolgante.
E se eu lhe dissesse que, com apenas algumas centenas de linhas de código em C# e um pouco de configuração, você poderia criar um aplicativo ASP.NET Core que abrigaria uma poderosa rede neural (CNN) com a qual você poderia interagir de forma tão simples quanto enviar uma foto para uma página do Facebook?
Com um treinamento tão simples quanto:
Training Image
E uma solicitação de classificação tão simples quanto:
Classification Request
Bem, é exatamente isso que vamos fazer — usando o ML.NET, vamos criar um classificador avançado e, em seguida, com a Messages API e o Messenger da Nexmo, vamos criar uma ferramenta poderosa e fácil de usar para treinamento e classificação.
Objetivos de aprendizagem
Neste tutorial, vamos:
Criar uma rede neural do TensorFlow no ML.NET
Treine essa rede neural para reconhecer diferentes tipos de cães
Crie um vetor de mensagens para solicitar que a rede neural classifique cães que ela nunca viu antes
Crie um vetor de aprendizado para permitir que a rede neural aprenda novos tipos de cães de forma dinâmica.
Pré-requisitos
Visual Studio 2019, versão 16.3 ou superior
Uma página do Facebook vinculada à sua conta Nexmo Veja aqui como fazer a configuração
Opcional: Ngrok para implantação de teste
Configuração do projeto
Primeiro, vamos abrir o Visual Studio, criar um novo aplicativo API ASP.NET Core 3.0 e chamá-lo de MessagesTensorFlow. Agora, vamos adicionar os seguintes pacotes NuGet à solução:
Castelo Inflável
jose-jwt
Microsoft.ML
Microsoft.ML.ImageAnalytics
Microsoft.ML.TensorFlow
Newtonsoft.Json
Vamos começar nossa rede neural com o modelo Inception V1 e, em seguida, alimentá-la com imagens e rótulos armazenados no disco. Crie uma pasta no diretório MessagesTensorFlow chamada assets.
Na pasta “assets”, baixe e descompacte o Modelo Inception V1
Além disso, na pasta “assets”, crie uma pasta chamada “train” e outra chamada “predict”. Em cada uma dessas pastas, adicione um arquivo chamado “tags.tsv”. Sua estrutura de pastas deve ficar mais ou menos assim agora:
Directory structure
Agora, acesse cada arquivo e, na seção de propriedades avançadas, defina a opção “Copiar para o diretório de saída” como “Copiar se for mais recente”
Criando o aluno
Vamos agora criar a classe que irá, de fato, abrigar nossa rede neural. Crie um arquivo chamado TFEngine.cs.
Importações
Adicione as seguintes importações no início do arquivo:
using Microsoft.ML;
using Microsoft.ML.Data;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net; Organização da aula
Então, dentro da classe TFEngine, vamos adicionar alguns caminhos para que possamos acessar todos os arquivos que vamos incorporar ao nosso modelo. Além disso, vamos definir algumas configurações para gerenciar os dados iniciais.
static readonly string _assetsPath = Path.Combine(Environment.CurrentDirectory, "assets");
static readonly string _imagesFolder = Path.Combine(_assetsPath, "train");
static readonly string _savePath = Path.Combine(_assetsPath, "predict");
static readonly string _trainTagsTsv = Path.Combine(_imagesFolder, "tags.tsv");
static readonly string _inceptionTensorFlowModel = Path.Combine(_assetsPath, "inception5h", "tensorflow_inception_graph.pb");
const int ImageHeight = 224;
const int ImageWidth = 224;
const float Mean = 117;
const bool ChannelsLast = true;Vamos também definir essa classe como um singleton e permitir apenas um acesso a ela por vez. Além disso, vamos adicionar um webClient para baixar as URLs das imagens.
static readonly object _lock = new object();
private static WebClient _client = new WebClient();
private static TFEngine _instance;
public static TFEngine Instance
{
get
{
lock (_lock)
{
if (_instance == null)
{
_instance = new TFEngine();
}
return _instance;
}
}
}
private TFEngine()
{
_mlContext = new MLContext();
GenerateModel();
}Também vamos criar alguns campos para armazenar nosso pipeline, que será usado para criar nosso modelo — o modelo que será utilizado para realizar a previsão — e o MLContext.
private IEstimator<ITransformer> _pipeline;
private ITransformer _model;
private MLContext _mlContext;Em seguida, adicione uma classe chamada ImageData, que armazenará os dados da imagem à medida que ela passa pelo modelo
public class ImageData
{
[LoadColumn(0)]
public string ImagePath;
[LoadColumn(1)]
public string Label;
}Em seguida, crie uma estrutura para armazenar os dados de previsão à medida que eles são gerados pelo modelo:
public class ImagePrediction : ImageData
{
public float[] Score;
public string PredictedLabelValue;
}A pontuação será um array contendo as probabilidades que a rede neural atribui a cada rótulo possível, e o `PredictedLabelValue` será, obviamente, a previsão da rede (o item com a pontuação mais alta)
Treinamento de modelos
Agora é hora de treinar nosso modelo!
Adicione um método chamado GenerateModel
public string ClassifySingleImage(string imageUrl)
{
try
{
var filename = Path.Combine(_savePath, $"{Guid.NewGuid()}.jpg");
_client.DownloadFile(imageUrl, filename);
var imageData = new ImageData()
{
ImagePath = filename
};
var predictor = _mlContext.Model.CreatePredictionEngine<ImageData, ImagePrediction>(_model);
var prediction = predictor.Predict(imageData);
var response = $"I'm about {prediction.Score.Max() * 100}% sure that the image you sent me is a {prediction.PredictedLabelValue}";
Console.WriteLine($"Image: {Path.GetFileName(imageData.ImagePath)} predicted as: {prediction.PredictedLabelValue} with score: {prediction.Score.Max() * 100} ");
return response;
}
catch (Exception)
{
return "Something went wrong when trying to classify image";
}
}Essa é, na verdade, a essência do que fará nosso preditor funcionar. A seção “_pipeline =” é uma sequência de comandos que irá:
Carregar as imagens do disco
Redimensione as imagens para importação
Extrair e vetorizar os pixels das imagens
Carregar o modelo inicial do TensorFlow (que é, basicamente, nossa rede neural pré-criada)
Crie um modelo de treinamento e processe os dados de treinamento nele para criar um modelo de previsão que possamos utilizar
Classificação de uma única imagem
Com nosso modelo treinado, agora podemos criar um método que receba o nome de um arquivo e retorne uma string contendo uma previsão e o nível de confiança da rede nessa previsão. Essa função recebe um imageUrl, salva o arquivo no disco, classifica a imagem e retorna uma string contendo a previsão do classificador com seu nível de confiança.
public string ClassifySingleImage(string imageUrl)
{
try
{
var filename = Path.Combine(_savePath, $"{Guid.NewGuid()}.jpg");
_client.DownloadFile(imageUrl, filename);
var imageData = new ImageData()
{
ImagePath = filename
};
var predictor = _mlContext.Model.CreatePredictionEngine<ImageData, ImagePrediction>(_model);
var prediction = predictor.Predict(imageData);
var response = $"I'm about {prediction.Score.Max() * 100}% sure that the image you sent me is a {prediction.PredictedLabelValue}";
Console.WriteLine($"Image: {Path.GetFileName(imageData.ImagePath)} predicted as: {prediction.PredictedLabelValue} with score: {prediction.Score.Max() * 100} ");
return response;
}
catch (Exception)
{
return "Something went wrong when trying to classify image";
}
} Adicionando dados de treinamento
A operação final que vamos solicitar ao TensorFlow Engine é, essencialmente, o inverso da previsão: vamos pedir que ele aceite uma URL de imagem e um rótulo, e que se atualize para reconhecer melhor as imagens com esse rótulo. O método `AddTrainingImage` salva a imagem fornecida no disco, acrescenta informações sobre essa imagem ao arquivo `tags.tsv` e regenera o modelo.
public string AddTrainingImage(string imageUrl, string label)
{
try
{
var id = Guid.NewGuid();
var fileName = Path.Combine(_imagesFolder, $"{id}.jpg");
_client.DownloadFile(imageUrl, fileName);
File.AppendAllText(_trainTagsTsv, $"{id}.jpg\t{label}" + Environment.NewLine);
IDataView trainingData = _mlContext.Data.LoadFromTextFile<ImageData>(path: _trainTagsTsv, hasHeader: false);
_model = _pipeline.Fit(trainingData);
return $"I have trained myself to recognize the image you sent me as a {label}. Your teaching is appreciated";
}
catch (Exception)
{
return "something went wrong when trying to train on image";
}
} Utilizando a Messages API para impulsionar a classificação e o treinamento
Objetos de mensagens
A seguir, vamos adicionar alguns POCOs para armazenar nossos dados de mensagens à medida que eles chegam e são enviados para a Messages API — esses objetos são bastante detalhados e não fazem nada de particularmente interessante, além de permitir a serialização/desserialização de JSON; portanto, para sermos mais concisos, fique à vontade para simplesmente usar as seguintes estruturas:
Interagindo com a API
A criação dessas estruturas nos permite gerenciar os dados que recebemos e enviamos para a Messages API. No entanto, precisamos de mais uma etapa para podermos realmente usar a API — precisaremos gerar um JWT para autenticar nosso aplicativo na Messages API. Para isso, vamos criar os seguintes arquivos.
TokenGenerator.cs
MessageSender.cs
Gerar JWT
O `TokenGenerator` terá um método estático, `GenerateToken`, que aceitará uma lista de Claims e a chave privada do seu aplicativo
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.OpenSsl;
using Org.BouncyCastle.Security;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Claims;
using System.Security.Cryptography;
namespace MessagesTensorFlow
{
public class TokenGenerator
{
public static string GenerateToken(List<Claim> claims, string privateKey)
{
RSAParameters rsaParams;
using (var tr = new StringReader(privateKey))
{
var pemReader = new PemReader(tr);
var kp = pemReader.ReadObject();
var privateRsaParams = kp as RsaPrivateCrtKeyParameters;
rsaParams = DotNetUtilities.ToRSAParameters(privateRsaParams);
}
using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider())
{
rsa.ImportParameters(rsaParams);
Dictionary<string, object> payload = claims.ToDictionary(k => k.Type, v => (object)v.Value);
return Jose.JWT.Encode(payload, rsa, Jose.JwsAlgorithm.RS256);
}
}
}
}Isso irá gerar um JWT para você usar com a Messages API.
Gerar uma lista de reivindicações para JWT
No arquivo MessageSender.cs, teremos um método para gerar as reivindicações para o JWT a partir do seu appId:
private static List<Claim> GetClaimsList(string appId)
{
const int SECONDS_EXPIRY = 3600;
var t = DateTime.UtcNow - new DateTime(1970, 1, 1);
var iat = new Claim("iat", ((Int32)t.TotalSeconds).ToString(), ClaimValueTypes.Integer32); // Unix Timestamp for right now
var application_id = new Claim("application_id", appId); // Current app ID
var exp = new Claim("exp", ((Int32)(t.TotalSeconds + SECONDS_EXPIRY)).ToString(), ClaimValueTypes.Integer32); // Unix timestamp for when the token expires
var jti = new Claim("jti", Guid.NewGuid().ToString()); // Unique Token ID
var claims = new List<Claim>() { iat, application_id, exp, jti };
return claims;
} Leia as configurações do aplicativo e crie um JWT
Em seguida, teremos outro método para ler os itens relevantes da sua configuração — que o controlador nos fornecerá por meio da injeção de dependências —, recuperar a lista de claims e gerar o JWT
private static string BuildJwt(IConfiguration config)
{
var appId = config["Authentication:appId"];
var priavteKeyPath = config["Authentication:privateKey"];
string privateKey = "";
using (var reader = File.OpenText(priavteKeyPath)) // file containing RSA PKCS1 private key
privateKey = reader.ReadToEnd();
var jwt = TokenGenerator.GenerateToken(GetClaimsList(appId), privateKey);
return jwt;
}É claro que isso exigirá algumas configurações no seu arquivo appsettings.json. Adicione o objeto a seguir ao seu arquivo appsettings.json e preencha-o com os valores adequados:
"Authentication": {
"appId": "app_id",
"privateKey": "path_to_key_file"
} Enviar uma mensagem
Agora vamos integrar tudo isso com nosso método `SendMessage`, que receberá nossa mensagem, `toId`, `fromId` e `config`. Esse método irá gerar um JWT e enviar uma solicitação à Messages API para que seja enviada uma mensagem contendo o feedback do nosso classificador ao nosso usuário final.
public static void SendMessage(string message, string fromId, string toId, IConfiguration config)
{
const string MESSAGING_URL = @"https://api.nexmo.com/v0.1/messages";
try
{
var jwt = BuildJwt(config);
var requestObject = new MessageRequest()
{
to = new MessageRequest.To()
{
id = toId,
type = "messenger"
},
from = new MessageRequest.From()
{
id = fromId,
type = "messenger"
},
message = new MessageRequest.Message()
{
content = new MessageRequest.Message.Content()
{
type = "text",
text = message
},
messenger = new MessageRequest.Message.Messenger()
{
category = "RESPONSE"
}
}
};
var requestPayload = JsonConvert.SerializeObject(requestObject, new JsonSerializerSettings() { NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.Ignore });
var httpWebRequest = (HttpWebRequest)WebRequest.Create(MESSAGING_URL);
httpWebRequest.ContentType = "application/json";
httpWebRequest.Accept = "application/json";
httpWebRequest.Method = "POST";
httpWebRequest.PreAuthenticate = true;
httpWebRequest.Headers.Add("Authorization", "Bearer " + jwt);
using (var streamWriter = new StreamWriter(httpWebRequest.GetRequestStream()))
{
streamWriter.Write(requestPayload);
}
using (var httpResponse = (HttpWebResponse)httpWebRequest.GetResponse())
{
using (var streamReader = new StreamReader(httpResponse.GetResponseStream()))
{
var result = streamReader.ReadToEnd();
Console.WriteLine(result);
Console.WriteLine("Message Sent");
}
}
}
catch (Exception e)
{
Debug.WriteLine(e.ToString());
}
} Manipulador de classificação
Queremos que o tratamento dos webhooks recebidos seja assíncrono e responda imediatamente; por isso, vamos criar um arquivo chamado ClassificationHandler.cs para lidar efetivamente com as operações de classificação e resposta. Esse arquivo conterá algumas pequenas estruturas que nos permitirão descarregar, classificar ou treinar e responder às mensagens recebidas.
No arquivo ClassificationHandler.cs, adicione o seguinte código:
public static void ClassifyAndRespond(object state)
{
var request = state as ClassifyRequest;
var response = TFEngine.Instance.ClassifySingleImage(request.imageUrl);
MessageSender.SendMessage(response, request.toId, request.fromid, request.Configuration);
}
public static void AddTrainingData(object state)
{
var request = state as TrainRequest;
var response = TFEngine.Instance.AddTrainingImage(request.imageUrl, request.Label);
MessageSender.SendMessage(response, request.toId, request.fromid, request.Configuration);
}
public class TrainRequest : Request
{
public string Label { get; set; }
}
public class ClassifyRequest : Request{}
public abstract class Request
{
public string imageUrl { get; set; }
public string toId { get; set; }
public string fromid { get; set; }
public IConfiguration Configuration { get; set; }
} Tratar webhooks de mensagens recebidas
Do ponto de vista do nosso código, a última coisa que precisaremos fazer é criar alguns controladores para lidar com as mensagens recebidas e o status da Messages API.
Na pasta Controllers, adicione dois “controladores API - Vazio” chamados InboundController e StatusController.
Controlador de status
O controlador Status irá fornecer o status das mensagens do nosso aplicativo à medida que elas passam pela API; para acompanhar o que está acontecendo, vamos adicionar um método POST ao controlador Status para exibir o conteúdo do status no console de depuração:
[HttpPost]
public HttpStatusCode Post([FromBody]StatusMessage message)
{
Debug.WriteLine(JsonConvert.SerializeObject(message));
return HttpStatusCode.NoContent;
} Controlador de entrada
O Controlador de Entrada ficará encarregado de gerenciar as mensagens recebidas do nosso webhook.
Organização da aula
Vamos, primeiro, preparar tudo criando um dicionário para os rótulos de treinamento pendentes, um objeto `Configuration` para que o controlador acesse a configuração e, por meio da injeção de dependências, inserindo o objeto `Configuration` no construtor do controlador `Inbound`:
public static Dictionary<string, string> _pendingTrainLabels = new Dictionary<string, string>();
public IConfiguration Configuration { get; set; }
public InboundController(IConfiguration configuration)
{
Configuration = configuration;
} Tratamento de mensagens recebidas
A seguir, vamos escrever o manipulador InboundMessage propriamente dito. Esse manipulador será uma solicitação POST. Ele verificará se há algum texto na mensagem. Se houver, ele verificará se a primeira palavra da mensagem é “train”. Se for o caso, ele salvará o restante da mensagem como um rótulo de treinamento e, na próxima vez que esse usuário enviar uma mensagem com uma imagem, o classificador será treinado com essa imagem e esse rótulo.
Em qualquer outra mensagem com imagem, ele simplesmente classificará a imagem e enviará o resultado da classificação de volta ao remetente da mensagem.
Em ambos os casos, ele inicia um WorkItem no ThreadPool, passando um desses práticos objetos de solicitação do ClassificationHandler que geramos anteriormente — isso libera o controlador para enviar um status de volta à Messages API (neste caso, um 204 para informar que a mensagem foi recebida)
[HttpPost]
public HttpStatusCode Post([FromBody]InboundMessage message)
{
const string TRAIN = "train";
try
{
Debug.WriteLine(JsonConvert.SerializeObject(message));
if (!string.IsNullOrEmpty(message.message.content.text))
{
var split = message.message.content.text.Split(new[] { ' ' }, 2);
if (split.Length > 1)
{
if (split[0].ToLower() == TRAIN)
{
var label = split[1];
var requestor = message.from.id;
if (!_pendingTrainLabels.ContainsKey(requestor))
{
_pendingTrainLabels.Add(requestor, label);
}
else
{
_pendingTrainLabels[requestor] = label;
}
}
}
}
if (_pendingTrainLabels.ContainsKey(message.from.id) && message.message.content?.image?.url != null)
{
ThreadPool.QueueUserWorkItem(ClassificationHandler.AddTrainingData, new ClassificationHandler.TrainRequest()
{
toId = message.to.id,
fromid = message.from.id,
imageUrl = message.message.content.image.url,
Label = _pendingTrainLabels[message.from.id],
Configuration = Configuration
});
_pendingTrainLabels.Remove(message.from.id);
}
else
{
ThreadPool.QueueUserWorkItem(ClassificationHandler.ClassifyAndRespond,
new ClassificationHandler.ClassifyRequest()
{
toId = message.to.id,
fromid = message.from.id,
imageUrl = message.message.content.image.url,
Configuration = Configuration
});
}
return HttpStatusCode.NoContent;
}
catch (Exception ex)
{
return HttpStatusCode.NoContent;
}
} Inicialização com poucos dados.
Você pode adicionar as imagens e tags que quiser para começar. Para simplificar, vou começar com apenas uma imagem — uma foto do meu cachorro (que, apropriadamente, se chama Zero).
Training data
Vou colocar essa imagem no diretório assets/train.
Como o Zero é um whippet, vou, no arquivo tags.tsv, na pasta assets/train, adicionar o nome do arquivo “zero.jpg”, seguido de uma tabulação, seguido do rótulo “whippet” e, em seguida, uma nova linha
zero.jpg whippet Testes
Feito isso, só falta ligar o sistema, conectá-lo à internet e testá-lo. Eu uso o ngrok e o IIS Express para testá-lo.
Configuração do IIS Express
Primeiro, acesse a guia “Depuração” nas propriedades do projeto e procure o campo “URL do aplicativo” — especificamente, qual porta ele vai usar —; eu desmarco a caixa de seleção “Ativar SSL” para fins de teste.
Debug
Em seguida, execute o site a partir do Visual Studio usando o IIS Express — você verá a porta na barra de endereços do navegador que é exibido — no meu exemplo, removi todo o conteúdo do controlador meteorológico que vem por padrão, então recebo um erro 404 ao iniciá-lo — o que está tudo bem, já que ele está, na verdade, atuando apenas como um serviço web para ouvir e responder a webhooks. Não há nenhuma solicitação GET para enviar uma página de volta ao seu navegador.
Usando o Ngrok para expor a porta à Internet
Para que a Messages API encaminhe as mensagens, precisaremos expor o site à internet — para fins de teste, usaremos o ngrok para expor nossa porta do IIS Express. Abra a linha de comando e use este comando, substituindo por seu número de porta.
ngrok http --host-header="localhost:" http://localhost:
Este comando gera uma saída como esta:
Command Output
Configurando os webhooks
Usando o link http que acabamos de obter do ngrok, você pode criar a URL para a qual o webhook fará a chamada de retorno — você pode ver na seção “Rota” dos controladores que acabamos de criar como será essa rota:
Route
Isso vai resultar em http://dc0feb1d.ngrok.io/api/Status para mensagens de status e http://dc0feb1d.ngrok.io/api/Inbound para mensagens recebidas
NOTA: A primeira parte da URL (dc0feb1d) mudará sempre que você reiniciar o ngrok no plano gratuito.
Usaremos essas URLs de retorno de chamada para registrar nossos webhooks no Nexmo.
Acesse ${CUSTOMER_DASHBOARD_URL} e faça login na sua conta da Nexmo
Acesse “Mensagens e despacho” → “Seus pedidos” e selecione o botão “Editar” correspondente ao seu pedido
Na tela de edição, altere os campos “URL de status” e “URL de entrada” para os valores indicados acima e clique no botão azul “Salvar”, no canto inferior direito.
E é isso. Agora você tem um classificador/modelo de aprendizado ao qual pode enviar imagens pelo Messenger.
Links úteis
Documentação do ML.NET TensorFlow para obter mais informações sobre o ML.NET TensorFlow — na verdade, a função GenerateModel do TFEngine é derivada deste tutorial.