Julho de 2018

Volume 33 – Número 7

Machine Learning – Machine Learning com Dispositivos de IoT na Borda

Por James McCaffrey

Imagine que, não muito distante no futuro, você é o designer de uma interseção de tráfego inteligente. A interseção inteligente tem quatro câmeras de vídeo conectadas a uma Internet dispositivo coisas (IoT) com uma CPU pequeno, semelhante a um Raspberry Pi. As câmeras enviam quadros de vídeo para o dispositivo de IoT, onde eles são analisados usando um modelo de machine learning (ML) reconhecimento de imagem e instruções de controle, em seguida, são enviadas para os sinais de tráfego. Um dos pequenos dispositivos IoT está conectado aos serviços de nuvem do Azure, onde as informações são registradas e analisadas offline.

Este é um exemplo do ML em um dispositivo de IoT na borda. Eu uso o termo dispositivo de borda para significar que qualquer coisa conectados à nuvem, onde nuvem refere-se a algo como o Microsoft Azure ou em servidores remotos de uma empresa. Neste artigo, explicarei duas maneiras que você pode projetar ML na borda. Especificamente, descreverei como escrever um modelo personalizado e uma função de e/s para um dispositivo e como usar o conjunto de biblioteca de aprendizado Embedded Microsoft (ELL) de ferramentas para implantar um modelo de ML otimizado em um dispositivo na borda. A abordagem personalizada de e/s está no momento, enquanto escrevo este artigo, a maneira mais comum para implantar um modelo de AM em um dispositivo IoT. A abordagem de ELL é intenções.

Mesmo se você não estiver trabalhando com ML em dispositivos IoT, há pelo menos três motivos por que você talvez queira ler este artigo. Primeiro, os princípios de design envolvidos generalizado para outros cenários de desenvolvimento de software. Em segundo lugar, é bem possível que você estará trabalhando com dispositivos IoT e ML relativamente em breve. Em terceiro lugar, você pode apenas descobrir as técnicas descritas aqui interessantes por si mesmos.

Por ML que precisa ser no IoT edge? Por que não fazemos todo o processamento na nuvem? Dispositivos de IoT na borda podem ser muito baratos, mas eles geralmente têm memória limitada, limitada de processamento de recurso e uma fonte de energia limitado. Em muitos cenários, a tentativa de executar o processamento na nuvem de ML tem várias desvantagens.

Latência costuma ser um grande problema. No exemplo a interseção de tráfego inteligente, um atraso de mais de uma fração de segundo pode ter consequências desastrosas. Problemas adicionais ao tentar realizar ML na nuvem incluem confiabilidade (uma conexão de rede perdida normalmente é impossível prever e difícil de lidar com), disponibilidade de rede (por exemplo, um navio pelo sea pode ter conectividade somente quando um satélite é a sobrecarga) e segurança/privacidade (quando, por exemplo, você está monitorando um paciente em um hospital.)

Este artigo não pressupõe que você tiver qualquer plano de fundo determinado ou habilidade definida mas suponha que você tem alguma experiência de desenvolvimento de software geral. A demonstração programas descritos neste artigo (um programa em Python que usa a biblioteca CNTK para criar um modelo de AM, um programa em C que simula o código de IoT e um programa em Python que usa um modelo ELL) são muito longos para apresentar aqui, mas elas estão disponíveis no arquivo que acompanha este artigo para baixo carregar.

O que é um modelo de aprendizado de máquina?

Para entender os problemas com a implantação de um modelo de ML para um dispositivo de IoT na borda, você deve entender exatamente é um modelo do AM. Muito genericamente falando, um modelo de AM é todas as informações necessárias para aceitar dados de entrada, fazer uma previsão e gerar dados de saída. Em vez de tentar explicar em teoria, vou ilustrar as ideias usando um exemplo concreto.

Dê uma olhada na captura de tela da figura1 e o diagrama na Figura 2. As duas figuras mostram uma rede neural com quatro nós de entrada, cinco nós de processamento de camada oculta e três nós de camada de saída. Os valores de entrada são (6,1, 3.1, 5.1, 1.1) e os valores de saída são (0.0321, 0.6458, 0.3221). Figura 1 mostra como o modelo foi desenvolvido e treinado. Eu usei o Visual Studio Code, mas há diversas alternativas.

Criar e treinar um modelo de rede Neural
Figura 1 criação e treinamento de um modelo de rede Neural

O mecanismo de entrada e saída de rede Neural
Figura 2 o mecanismo de entrada e saída de rede Neural

Esse exemplo específico envolve prevendo a espécie de uma flor de íris usando valores de entrada que representam o comprimento da sépala (uma estrutura semelhante a folha) e largura e comprimento da pétala e largura. Há três espécies possíveis de flor: virginica de setosa, versicolor,. Os valores de saída podem ser interpretados como probabilidades (Observe que somam 1,0) dessa forma, como o segundo valor, 0.6458, é o maior, previsão do modelo é a espécie de segundo, versicolor.

Na Figura 2, cada linha que conecta um par de nós representa um peso. Um peso é apenas uma constante numérica. Se nós tiverem uma base, de cima para baixo, o peso da entrada[0] até oculto[0] é 0.2680 e o peso de oculto[4] à saída [0] é 0.9381.

Cada nó oculto e de saída tem uma pequena seta que aponta para o nó. Eles são chamados de desvios. O desvio para Hidden[0] é 0.1164 e o desvio para output [0] é-0.0466.

Você pode pensar uma rede neural como uma função matemática complicada porque ele simplesmente aceita entrada numérica e produz uma saída numérica. Um modelo de AM em um dispositivo de IoT precisa saber como calcular a saída. Para a rede neural nos Figura 2, a primeira etapa é computar os valores de nós ocultos. O valor de cada nó oculto é a função de tangente hiperbólica (tanh) aplicada à soma dos produtos de entradas e pesos associados mais o desvio. Oculta [0] é o cálculo:

hidden[0] = tanh((6.1 * 0.2680) + (3.1 * 0.3954) +
                 (5.1 * -0.5503) + (1.1 * -0.3220) + 0.1164)
          = tanh(-0.1838)
          = -0.1817

Nós ocultos [1] a [4] são calculados da mesma forma. A função tanh é chamada de função de ativação da camada oculta. Há outras funções de ativação que podem ser usadas, como logística sigmoide e retificadas unidades lineares, que daria valores diferentes de nó oculto.

Depois que os valores de nó oculto terem sido calculados, a próxima etapa é computar valores de nó de saída preliminares. Um valor de nó de saída preliminares é simplesmente a soma dos produtos de nós ocultos e os pesos de oculto-para-saída associados, além da diferença. Em outras palavras, o mesmo cálculo como usado para nós ocultos, mas sem a função de ativação. Para obter o valor preliminar de saída [0] é o cálculo:

o_pre[0] = (-0.1817 * 0.7552) + (-0.0824 * -0.7297) +
           (-0.1190 * -0.6733) + (-0.9287 * 0.9367) +
           (-0.9081 * 0.9381) + (-0.0466)
         = -1.7654

Os valores para nós de saída [1] e [2] são calculados da mesma maneira. Depois de terem sido calculados os valores preliminares de nós de saída, os valores de nó de saída final podem ser convertidos em probabilidades usando a função de ativação softmax. A função softmax é melhor explicada por exemplo. Os cálculos para os valores de saída final são:

sum = exp(o_pre[0]) + exp(o_pre[1]) + exp(o_pre[2])
    = 0.1711 + 3.4391 + 1.7153
    = 5.3255
output[0] = exp(o_pre[0]) / sum
          = 0.1711 / 5.3255 = 0.0321
output[1] = exp(o_pre[1]) / sum
          = 3.4391 / 5.3255 = 0.6458
output[2] = exp(o_pre[2]) / sum
          = 1.7153 / 5.3255 = 0.3221

Assim como acontece com os nós ocultos, há funções de ativação do nó de saída alternativos, como a função identity.

Para resumir, um modelo de AM é todas as informações necessárias para aceitar dados de entrada e gerar uma previsão de saída. No caso de uma rede neural, essas informações consistem em número de nós de entrada, ocultas e de saída, os valores dos pesos e desvios e os tipos de funções de ativação usados em nós de camada ocultos e de saída.

Okey, mas onde não os valores dos pesos e desvios são provenientes? Eles estiverem determinados pelo treinamento do modelo. Treinamento está usando um conjunto de dados que tem valores de entrada conhecidos e conhecidos, corrija valores de saída e aplicando um algoritmo de otimização, como a propagação de retorno para minimizar a diferença entre valores de saída calculados e conhecidos, corrija valores de saída.

Há muitos outros tipos de modelos de ML, como árvores de decisão e naive Bayes, mas os princípios gerais são os mesmos. Ao usar uma biblioteca de códigos de rede neural, como CNTK da Microsoft ou Google Keras/TensorFlow, o programa que treina um modelo de ML salvará o modelo de disco. Por exemplo, o código CNTK e Keras é semelhante:

mp = ".\\Models\\iris_nn.model"
model.save(mp, format=C.ModelFormat.CNTKv2)  # CNTK
model.save(".\\Models\\iris_model.h5")  # Keras

Bibliotecas de ML também têm funções para carregar um modelo salvo. Por exemplo:

mp = ".\\Models\\iris_nn.model"
model = C.ops.functions.Function.load(mp)  # CNTK
model = load_model(".\\Models\\iris_model.h5")  # Keras

Bibliotecas de rede neurais mais tem uma maneira de salvar apenas um modelo pesos e desvios valores em um arquivo (em vez de todo o modelo).

Implantar um modelo de ML padrão em um dispositivo de IoT

A imagem no Figura 1 mostra um exemplo de treinar um modelo de ML semelhante ao seguinte. Usei o Visual Studio Code como o editor e a interface de API de linguagem Python para a biblioteca de v2.4 CNTK. Criando um modelo ML treinado pode levar dias ou semanas de esforço e normalmente requer muita capacidade de processamento e memória. Portanto, o treinamento de modelo normalmente é executado em computadores poderosos, normalmente com um ou mais GPUs. Além disso, conforme aumenta o tamanho e a complexidade de uma rede neural, o número de pesos e desvios aumenta drasticamente e então, o tamanho do arquivo de um modelo salvo também aumenta consideravelmente.

Por exemplo, o 4-5-3 modelo íris descrito na seção anterior tem apenas (4 * 5) + 5 + (5 * 3) + 3 = 43 pesos e desvios. Mas um modelo de classificação de imagem com milhões de valores de pixel de entrada e centenas de nós de processamento ocultos pode ter centenas de milhões ou bilhões até mesmo, de pesos e tendências. Observe que os valores de todos os 43 pesos e desvios do exemplo de íris são mostrados na Figura 1:

[[ 0.2680 -0.3782 -0.3828  0.1143  0.1269]
 [ 0.3954 -0.4367 -0.4332  0.3880  0.3814]
 [-0.5503  0.6453  0.6394 -0.6454 -0.6300]
 [-0.322   0.4035  0.4163 -0.3074 -0.3112]]
 [ 0.1164 -0.1567 -0.1604  0.0810  0.0822]
[[ 0.7552 -0.0001 -0.7706]
 [-0.7297 -0.2048  0.9301]
 [-0.6733 -0.2512  0.9167]
 [ 0.9367 -0.4276 -0.5134]
 [ 0.9381 -0.3728 -0.5667]]
 [-0.0466  0.4528 -0.4062]

Então, suponha que você tem um modelo ML treinado. Você deseja implantar o modelo em um dispositivo de IoT fraco, pequeno. A solução mais simples é instalar no dispositivo IoT o mesmo software de biblioteca de rede neural usado para treinar o modelo. Em seguida, você pode copiar o arquivo de modelo treinado salvo para o dispositivo IoT e escrever código para carregar o modelo e fazer uma previsão. Fácil!

Infelizmente, essa abordagem funciona apenas em relativamente raras situações em que seu dispositivo IoT é muito poderoso — talvez ao longo das linhas de um desktop PC ou laptop. Além disso, as bibliotecas de rede neural, como CNTK e TensorFlow/Keras foram projetadas para treinar modelos de forma rápida e eficiente, mas em geral não foram necessariamente projetadas para otimizar o desempenho ao executar a entrada e saída com um modelo treinado. Em resumo, a solução mais fácil para implantar um modelo ML treinado em um dispositivo de IoT na borda é praticamente inviável.

A solução de código personalizado

A maneira mais comum para implantar um modelo ML treinado em um dispositivo de IoT na borda com base na minha experiência e conversas com colegas, é escrever código personalizado do C/C++ no dispositivo. A ideia é que C/C++ está quase universalmente disponível em dispositivos IoT e C/C++ normalmente é rápido e compacto. O programa de demonstração na Figura 3 ilustra o conceito.

Simulação de código do C/C++ personalizado e/s em um dispositivo IoT
Figura 3 simulação de código do C/C++ personalizado e/s em um dispositivo IoT

Inicia o programa de demonstração usando a ferramenta de C/C++ gcc para compilar Test. c o arquivo em um arquivo executável no dispositivo de destino. Aqui, o dispositivo de destino é apenas meu computador desktop, mas há compiladores do C/C++ para quase todos os tipos de dispositivo de IoT/CPU. Quando executado, o programa de demonstração exibe os valores dos pesos e desvios do exemplo de flor de íris, em seguida, usa valores de entrada (6.1, 3.1, 5.1, 1.1) e calcula e exibe os valores de saída (0.0321, 0.6458, 0.3221). Se você comparar Figura 3 com figuras 1 e 2, você verá as entradas, pesos e desvios e saídas são os mesmos (sujeito a erro de arredondamento).   

Test. c o programa de demonstração implementa somente o processo de entrada e saída de rede neural. O programa começa Configurando uma estrutura de dados do struct para manter o número de nós em cada camada, os valores para ocultos e nós da camada de saída e valores dos pesos e desvios:

#include <stdio.h>
#include <stdlib.h>
#include <math.h>  // Has tanh()
typedef struct {
  int ni, nh, no;
  float *h_nodes, *o_nodes;  // No i_nodes
  float **ih_wts, **ho_wts;
  float *h_biases, *o_biases;
} nn_t;

O programa define as funções a seguir:

construct(): initialize the struct
free(): deallocate memory when done
set_weights(): assign values to weights and biases
softmax(): the softmax function
predict(): implements the NN IO mechanism
show_weights(): a display helper

As principais linhas de código em que a função principal do programa de demonstração se parecer com:

nn_t net;  // Neural net struct
construct(&net, 4, 5, 3);  // Instantiate the NN
float wts[43] = {  // specify the weights and biases
  0.2680, -0.3782, -0.3828, 0.1143, 0.1269,
. . .
 -0.0466, 0.4528, -0.4062 };
set_weights(&net, wts);  // Copy values into NN
float inpts[4] = { 6.1, 3.1, 5.1, 1.1 };  // Inputs
int shownodes = 0;  // Don’t show
float* probs = predict(net, inpts, shownodes);

O ponto é que se você souber exatamente como funciona um modelo de ML de rede neural simples, o processo de e/s não é mágico. Você pode implementar facilmente e/s básica.

A principal vantagem de usar uma função personalizada de e/s do C/C++ é a simplicidade conceitual. Além disso, porque você está codificando em um nível muito baixo (realmente apenas um nível de abstração acima da linguagem de assembly), o código executável gerado normalmente será muito pequena e execução muito rápida. Além disso, porque você tem controle total sobre seu código e/s, você pode usar todos os tipos de técnicas para acelerar o desempenho ou reduzir o volume de memória. Por exemplo, Test. c o programa usa o tipo float, mas, dependendo do cenário de problema, você poderá usar um tipo de dados personalizados de ponto fixo de 16 bits.

A principal desvantagem de usar uma abordagem de e/s do C/C++ personalizada é que a técnica se torna cada vez mais difícil do que a complexidade dos aumentos de modelo ML treinados. Por exemplo, uma função de e/s para uma rede neural de única camada oculta com a ativação tanh e softmax é muito fácil de implementar — levando apenas cerca de um dia para uma semana de esforço de desenvolvimento, dependendo de vários fatores, é claro. Uma rede neural profunda com várias camadas ocultas é um pouco mais fácil lidar com — talvez uma semana ou duas de esforço. Mas a implementação da funcionalidade de e/s de uma rede neural convolucional (CNN) ou um longo período, o curto prazo (LSTM) de memória rede neural recorrente é muito difícil e geralmente exigiria muito mais do que quatro semanas de esforço de desenvolvimento.

Suspeito que como o uso de dispositivos de IoT aumenta, haverá esforços para criar bibliotecas de C/C++ de software livre que implementam a e/s para modelos de ML criados pelas bibliotecas de rede neural diferentes, como CNTK e TensorFlow/Keras. Ou, se houver suficiente por demanda, os desenvolvedores de bibliotecas de rede neural podem criar as APIs de e/s do C/C++ para dispositivos IoT em si. Se você tivesse uma biblioteca, escrever personalizado e/s para um dispositivo IoT seria relativamente simple.

A biblioteca do Microsoft Learning incorporado

A biblioteca de aprendizado Embedded Microsoft (ELL) é um projeto de código-fonte aberto ambicioso se destina a facilitar o esforço de desenvolvimento necessário para implantar um modelo de AM em um dispositivo de IoT na borda (microsoft.github.io/ELL). A ideia básica do ELL é ilustrada no lado esquerdo do Figura 4.

O processo de fluxo de trabalho ELL, Granular e de alto nível
Figura 4 o processo de fluxo de trabalho ELL, Granular e de alto nível

Em palavras, o sistema ELL aceita um modelo de AM criado por uma biblioteca com suporte, como CNTK ou um formato de modelo com suporte, como o exchange de rede neural aberta (ONNX). O sistema ELL usa o modelo de ML entrada e gera um modelo intermediário como um arquivo de .ell. Em seguida, o sistema ELL usa o arquivo de modelo .ell intermediário para gerar o código executável de algum tipo para um dispositivo de destino com suporte. Em outras palavras, você pode pensar ELL como uma espécie de compilador cruzado para modelos de ML.

Obter uma explicação mais granular de como funciona o ELL é mostrada no lado direito da Figura 4, usando o exemplo de modelo de flor de íris. O processo começa com um desenvolvedor de ML escrevendo um programa de Python chamado iris_nn.py para criar e salvar um modelo de previsão chamado iris_cntk.model, que está em um formato binário proprietário. Esse processo é mostrado na Figura 1.

O cntk_import.py de ferramenta de linha de comando ELL, em seguida, é usado para criar um arquivo iris_cntk.ell intermediário, que é armazenado no formato JSON. Em seguida, o wrap.py de ferramenta de linha de comando ELL é usado para gerar um host\build de diretório dos arquivos de código de origem do C/C++. Observe que o "host" significa para levar as configurações do computador atual, portanto, um cenário mais comum seria algo como \pi3\build. Em seguida, a ferramenta de compilação do compilador do C/C++ cmake.exe é usada para gerar um módulo de Python do código executável, que contém a lógica do modelo ML original, denominada iris_cntk. O destino pode ser um executável de C/C++ ou c# executável ou tudo o que é mais adequado para o dispositivo de IoT de destino.

O módulo de Python iris_cntk, em seguida, pode ser importado por um programa em Python (use_iris_ell_model.py) no dispositivo de destino (meu PC desktop), conforme mostrado na Figura 5. Observe que os valores de entrada (6.1, 3.1, 5.1, 1.1) e valores de saída (0.0321, 0.6457, 0.3221) gerados pelo modelo de sistema ELL são o mesmo que os valores gerados durante o desenvolvimento de modelo (Figura 1) e os valores gerados pela função personalizada de e/s do C/C++ (Figura 3).

Simulação de como usar um modelo de CÉLULA em um dispositivo IoTFigura 5 simulação do uso de um modelo de CÉLULA em um dispositivo IoT

A "(py36)" à esquerda antes dos prompts de comando no Figura 5 indicam que estou trabalhando em uma configuração especial do Python chamada um ambiente do Conda onde estou usando o Python versão 3.6, que era necessário no momento, codifiquei minha demonstração ELL.

O código para programa use_iris_ell_model.py é mostrado na Figura 6. O ponto é que ELL gerou um módulo/pacote do Python que pode ser usado assim como qualquer outro pacote/módulo.

Figura 6 usando um modelo de CÉLULA em um programa de Python

# use_iris_ell_model.py
# Python 3.6
import numpy as np
import tutorial_helpers   # used to find package
import iris_cntk as m     # the ELL module/package
print("\nBegin use ELL model demo \n")
unknown = np.array([[6.1, 3.1, 5.1, 1.1]],
  dtype=np.float32)
np.set_printoptions(precision=4, suppress=True)
print("Input to ELL model: ")
print(unknown)
predicted = m.predict(unknown)
print("\nPrediction probabilities: ")
print(predicted)
print("\nEnd ELL demo \n"

O sistema ELL ainda está em estágios iniciais de desenvolvimento, mas com base na minha experiência, o sistema está pronto para experimentar e está estável o suficiente para cenários de desenvolvimento limitadas de produção.

Posso esperar sua reação ao diagrama do processo em ELL Figura 4 e sua explicação é algo como "Uau, que é de várias etapas!" Pelo menos era minha reação. Eventualmente, espero que o sistema ELL para chegar ao ponto em que você pode gerar um modelo para implantação em um dispositivo de IoT ao longo das linhas de:

source_model = ".\\iris_cntk.model"
target_model = ".\\iris_cortex_m4.model"
ell_generate(source_model, target_model)

Mas por enquanto, se você quiser explorar ELL você terá de trabalhar com várias etapas. Felizmente, o tutorial ELL do site da Web de ELL na qual grande parte deste artigo se baseia é muito bom. Devo ressaltar que começar com ELL instale ELL em seu computador desktop e instalação consiste em criar o código-fonte C/C++ — não há nenhum instalador. msi para ELL (ainda).

Um recurso interessante de CÉLULA não é óbvio é que ele executa alguma otimização muito sofisticada em segundo plano. Por exemplo, a equipe ELL explorou maneiras para compactar os grandes modelos de ML, incluindo sparsification e técnicas de remoção e substituição de matemática de ponto flutuante com matemática de 1 bit. A equipe ELL também está observando os algoritmos que podem ser usados no lugar de redes neurais, incluindo árvores de decisão aprimorado e classificadores DNF k.

Os tutoriais no site da Web ELL sejam muito boas, mas porque há muitas etapas envolvidas, eles são um pouco longos. Deixe-me a esboçar brevemente o processo para que você pode ter uma noção do que instalar e usar ELL são semelhante. Observe que os meus comandos não são sintaticamente corretos; eles são altamente simplificados para manter as ideias principais claras.

Instalando o sistema ELL é semelhante a:

x> (install several tools such as cmake and BLAS)
> git clone https://github.com/Microsoft/ELL.git
> cd ELL
> nuget.exe restore external/packages.config -PackagesDirectory external
> md build
> cd build
> cmake -G "Visual Studio 15 2017 Win64" ..
> cmake --build . --config Release
> cmake --build . --target _ELL_python --config Release

Em palavras, você deve ter algumas ferramentas instaladas antes de iniciar, em seguida, obtenha o código-fonte ELL do GitHub e, em seguida, crie as ferramentas de executável ELL e associação de Python usando o cmake.

Criando um modelo de ELL é semelhante a:

> python cntk_import.py iris_cntk.model
> python wrap.py iris_nn_cntk.ell --language python --target host
> cd host
> md build
> cd build
> cmake -G "Visual Studio 15 2017 Win64" .. && cmake --build . --config release

Ou seja, você deve usar ELL cntk_import.py de ferramenta para criar um arquivo de .ell de um arquivo de modelo do CNTK. Você pode usar wrap.py para gerar muito específico para um destino específico do dispositivo IoT para C/C++. E você usar o cmake gerar arquivos executáveis que encapsulam o comportamento original treinado ML do modelo.

Conclusão

Para resumir, um modelo de aprendizado de máquina é todas as informações necessárias para um sistema de software aceitar a entrada e gerar uma previsão. Como dispositivos de IoT na borda geralmente exigem desempenho muito rápido e confiável, às vezes, é necessário calcular previsões de ML diretamente em um dispositivo. No entanto, dispositivos IoT geralmente são pequenos e fraca, portanto, você simplesmente não é possível copiar um modelo que foi desenvolvido em um computador desktop poderosos para o dispositivo. Uma abordagem padrão é escrever código personalizado do C/C++, mas essa abordagem não é dimensionado para modelos de ML complexos. Uma abordagem emergente é o uso de ML compiladores, como a biblioteca de aprendizado incorporado do Microsoft.

Quando totalmente madura e lançadas, o sistema ELL muito provavelmente fará desenvolver modelos de ML complexos para dispositivos de IoT na borda drasticamente mais fácil do que é hoje.


Dr. James McCaffreytrabalha para a Microsoft Research em Redmond, Washington. Ele trabalhou em vários produtos da Microsoft, incluindo Internet Explorer e Bing. Entre em contato com o Dr. McCaffrey pelo email jamccaff@microsoft.com.

Agradecemos aos seguintes especialistas técnicos da Microsoft pela revisão deste artigo: Byron Changuion, Chuck Jacobs, Chris Lee e Ricky Loynd


Discuta esse artigo no fórum do MSDN Magazine