В этой статье мы узнаем, как построить граф модели нейронной сети на C#. Ключевое преимущество нейронной сети по сравнению с линейным классификатором заключается в том, что она может разделять данные, которые не могут быть разделены линейно. Мы реализуем эту модель для классификации изображений рукописных цифр из набора данных MNIST.

Структура нейронной сети, которую мы собираемся построить, выглядит следующим образом. Изображения рукописных цифр данных MNIST, которые имеют 10 классов (от 0 до 9). Сеть имеет 2 скрытых слоя: первый слой с 200 скрытыми единицами (нейронами) и второй (известный как слой классификатора) с 10 нейронами.

Приступим к реализации шаг за шагом:

1. Подготовьте данные

MNIST — это набор данных рукописных цифр, который содержит 55 000 примеров для обучения, 5 000 примеров для проверки и 10 000 примеров для тестирования. Цифры были нормализованы по размеру и центрированы в изображении фиксированного размера (28 x 28 пикселей) со значениями от 0 до 1. Каждое изображение было сглажено и преобразовано в одномерный массив из 784 функций. Это также своего рода эталон наборов данных для глубокого обучения.

Мы определяем некоторые переменные, чтобы их было легче изменить позже. Важно отметить, что в линейной модели мы должны преобразовать входные изображения в вектор.

using System;
using NumSharp;
using Tensorflow;
using TensorFlowNET.Examples.Utility;
using static Tensorflow.Python;
const int img_h = 28;
const int img_w = 28;
int img_size_flat = img_h * img_w; // 784, the total number of pixels
int n_classes = 10; // Number of classes, one class per digit

Мы напишем функцию, которая автоматически загружает данные MNIST и возвращает их в желаемой форме и формате. Существует помощник по данным MNIST, облегчающий жизнь.

Datasets mnist;
public void PrepareData()
{
    mnist = MnistDataSet.read_data_sets("mnist", one_hot: true);
}

Помимо функции для загрузки изображений и соответствующих меток, нам нужны еще две функции:

randomize: рандомизирует порядок изображений и их меток. В начале каждой эпохи мы повторно рандомизируем порядок выборок данных, чтобы убедиться, что обученная модель не чувствительна к порядку данных.

private (NDArray, NDArray) randomize(NDArray x, NDArray y)
{
    var perm = np.random.permutation(y.shape[0]);
    np.random.shuffle(perm);
    return (mnist.train.images[perm], mnist.train.labels[perm]);
}

get_next_batch: выбирает только несколько изображений, определяемых переменной batch_size (согласно методу стохастического градиентного спуска).

private (NDArray, NDArray) get_next_batch(NDArray x, NDArray y, int start, int end)
{
    var x_batch = x[$"{start}:{end}"];
    var y_batch = y[$"{start}:{end}"];
    return (x_batch, y_batch);
}

2. Задайте гиперпараметры

В обучающем наборе около 55 000 изображений, требуется много времени, чтобы вычислить градиент модели, используя все изображения. Поэтому мы используем небольшую партию изображений в каждой итерации оптимизатора Stochastic Gradient Descent.

  • эпоха: один проход вперед и один проход назад всех обучающих примеров.
  • размер партии: количество обучающих примеров за один проход вперед/назад. Чем больше размер пакета, тем больше памяти вам потребуется.
  • итерация: один проход вперед и один проход назад одной партии изображений обучающих примеров.
int epochs = 10;
int batch_size = 100;
float learning_rate = 0.001f;
int h1 = 200; // number of nodes in the 1st hidden layer

3. Создание нейронной сети

Давайте создадим несколько функций, которые помогут построить граф вычислений.

переменные: нам нужно определить две переменные W и b для построения нашей линейной модели. Мы используем Tensorflow Variables правильного размера и инициализации для их определения.

// weight_variable
var in_dim = x.shape[1];
var initer = tf.truncated_normal_initializer(stddev: 0.01f);
var W = tf.get_variable("W_" + name,
                        dtype: tf.float32,
                        shape: (in_dim, num_units),
                        initializer: initer);
// bias_variable
var initial = tf.constant(0f, num_units);
var b = tf.get_variable("b_" + name,
                        dtype: tf.float32,
                        initializer: initial);

полностью связанный слой: нейронная сеть состоит из стопок полностью связанных (плотных) слоев. Имея переменные веса (W) и смещения (b), полносвязный слой определяется как activation(W x X + b). Полная функция fc_layer выглядит следующим образом:

private Tensor fc_layer(Tensor x, int num_units, string name, bool use_relu = true)
{
    var in_dim = x.shape[1];
var initer = tf.truncated_normal_initializer(stddev: 0.01f);
    var W = tf.get_variable("W_" + name,
                            dtype: tf.float32,
                            shape: (in_dim, num_units),
                            initializer: initer);
var initial = tf.constant(0f, num_units);
    var b = tf.get_variable("b_" + name,
                            dtype: tf.float32,
                            initializer: initial);
var layer = tf.matmul(x, W) + b;
    if (use_relu)
        layer = tf.nn.relu(layer);
return layer;
}

входные данные. Теперь нам нужно определить правильные тензоры для подачи входных данных в нашу модель. Переменная-заполнитель является подходящим выбором для входных изображений и соответствующих меток. Это позволяет нам изменять входные данные (изображения и метки) для графика TensorFlow.

// Placeholders for inputs (x) and outputs(y)
x = tf.placeholder(tf.float32, shape: (-1, img_size_flat), name: "X");
y = tf.placeholder(tf.float32, shape: (-1, n_classes), name: "Y");

Для изображений определен заполнитель x, для формы задано значение [None, img_size_flat], где None означает, что тензор может содержать произвольное количество изображений, причем каждое изображение является вектором длины img_size_flat.

Заполнитель y — это переменная для истинных меток, связанных с изображениями, которые были введены в переменную заполнителя x. Он содержит произвольное количество меток, и каждая метка представляет собой вектор длины num_classes, что равно 10.

слои сети: после создания надлежащих входных данных мы должны передать их в нашу модель. Поскольку у нас есть нейронная сеть, мы можем сложить несколько полносвязных слоев, используя метод fc_layer. Обратите внимание, что мы не будем использовать никакую функцию активации (use_relu = false) в последнем слое. Причина в том, что мы можем использовать tf.nn.softmax_cross_entropy_with_logits для расчета убытка.

// Create a fully-connected layer with h1 nodes as hidden layer
var fc1 = fc_layer(x, h1, "FC1", use_relu: true);
// Create a fully-connected layer with n_classes nodes as output layer
var output_logits = fc_layer(fc1, n_classes, "OUT", use_relu: false);

функция потерь: после создания сети мы должны рассчитать потери и оптимизировать их, мы должны рассчитать correct_prediction и accuracy.

// Define the loss function, optimizer, and accuracy
var logits = tf.nn.softmax_cross_entropy_with_logits(labels: y, logits: output_logits);
loss = tf.reduce_mean(logits, name: "loss");
optimizer = tf.train.AdamOptimizer(learning_rate: learning_rate, name: "Adam-op").minimize(loss);
var correct_prediction = tf.equal(tf.argmax(output_logits, 1), tf.argmax(y, 1), name: "correct_pred");
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32), name: "accuracy");

инициализировать переменные: мы должны вызвать операцию инициализации переменных, чтобы инициализировать все переменные.

var init = tf.global_variables_initializer();

Полный график вычислений выглядит следующим образом:

4. Поезд

После создания графа мы можем обучить нашу модель. Чтобы обучить модель, мы должны создать сеанс и запустить график в сеансе.

// Number of training iterations in each epoch
var num_tr_iter = mnist.train.labels.len / batch_size;
with(tf.Session(), sess =>
{
    sess.run(init);
float loss_val = 100.0f;
    float accuracy_val = 0f;
foreach (var epoch in range(epochs))
    {
        print($"Training epoch: {epoch + 1}");
        // Randomly shuffle the training data at the beginning of each epoch 
        var (x_train, y_train) = randomize(mnist.train.images, mnist.train.labels);
foreach (var iteration in range(num_tr_iter))
        {
            var start = iteration * batch_size;
            var end = (iteration + 1) * batch_size;
            var (x_batch, y_batch) = get_next_batch(x_train, y_train, start, end);
// Run optimization op (backprop)
            sess.run(optimizer, new FeedItem(x, x_batch), new FeedItem(y, y_batch));
if (iteration % display_freq == 0)
            {
                // Calculate and display the batch loss and accuracy
                var result = sess.run(new[] { loss, accuracy }, new FeedItem(x, x_batch), new FeedItem(y, y_batch));
                loss_val = result[0];
                accuracy_val = result[1];
                print($"iter {iteration.ToString("000")}: Loss={loss_val.ToString("0.0000")}, Training Accuracy={accuracy_val.ToString("P")}");
            }
        }
// Run validation after every epoch
        var results1 = sess.run(new[] { loss, accuracy }, new FeedItem(x, mnist.validation.images), new FeedItem(y, mnist.validation.labels));
        loss_val = results1[0];
        accuracy_val = results1[1];
        print("---------------------------------------------------------");
        print($"Epoch: {epoch + 1}, validation loss: {loss_val.ToString("0.0000")}, validation accuracy: {accuracy_val.ToString("P")}");
        print("---------------------------------------------------------");
    }
});

5. Тест

После завершения обучения мы должны протестировать нашу модель, чтобы увидеть, насколько хорошо она работает с новым набором данных.

var result = sess.run(new[] { loss, accuracy }, new FeedItem(x, mnist.test.images),  new FeedItem(y, mnist.test.labels));
loss_test = result[0];
accuracy_test = result[1];
print("---------------------------------------------------------");
print($"Test loss: {loss_test.ToString("0.0000")}, test accuracy: {accuracy_test.ToString("P")}");
print("---------------------------------------------------------");

6. Заговор

СДЕЛАТЬ…

Исходный код предоставлен на Github.