1. 程式人生 > >.NET粉也可以玩卷積神經網路啦

.NET粉也可以玩卷積神經網路啦

在本文中,我們將實現一個簡單的卷積神經網路模型。 我們將實現此模型以對MNIST資料集進行分類。我們要構建的神經網路的結構如下。 MNIST資料的手寫數字影象有10個類(從0到9)。 網路有2個卷積層,最後是2個全連線層。

開始程式碼實現:

  1. 準備資料

MNIST是手寫數字的資料集,包含55,000個用於訓練的示例,5,000個用於驗證的示例和10,000個用於測試的示例。 這些數字已經過尺寸標準化,並且以固定尺寸的影象(28 x 28畫素)為中心,其值為0和1.每個影象已被展平並轉換為784個特徵的1-D陣列。 它也是深度學習的基準資料集。

先匯入必要的機器學習庫TensorFlow.NETNumSharp.

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 n_classes = 10; // Number of classes, one class per digit
int n_channels = 1;

我們將編寫一個自動載入MNIST資料的函式,並以我們想要的形狀和格式返回它。 有一個MNIST資料助手可以讓這個任務變得列簡單一些。

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

除了載入影象和相應標籤的功能外,我們還需要三個功能:

重新格式化:將資料重新格式化為卷積層可接受的格式。

private (NDArray, NDArray) Reformat(NDArray x, NDArray y)
{
    var (img_size, num_ch, num_class) = (np.sqrt(x.shape[1]), 1, len(np.unique<int>(np.argmax(y, 1))));
    var dataset = x.reshape(x.shape[0], img_size, img_size, num_ch).astype(np.float32);
    //y[0] = np.arange(num_class) == y[0];
    //var labels = (np.arange(num_class) == y.reshape(y.shape[0], 1, y.shape[1])).astype(np.float32);
    return (dataset, y);
}



隨機抽樣:隨機化影象及其標籤的順序。 在每個時期的開始,我們將重新隨機化資料樣本的順序,以確保訓練的模型對資料的順序不敏感。

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]);
}

獲取一批樣本:僅選擇由batch_size變數確定的少量影象(根據Stochastic Gradient Descent方法)。

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);
}

  1. 設定超引數

在訓練集中有大約55,000個影象,使用所有影象計算模型的梯度需要很長時間。 因此,我們通過隨機梯度下降在優化器的每次迭代中使用一小批影象。

epoch:所有訓練樣例的一個前向傳球和一個後傳傳球。

batch:一次前進/後退傳遞中的訓練樣例數。 批量大小越大,您需要的記憶體空間就越大。

iteration:一批前向傳遞和一組後向傳遞一組影象的訓練樣例。

int epochs = 10;
int batch_size = 100;
float learning_rate = 0.001f;
int display_freq = 200; // Frequency of displaying the training results


  1. 網路配置

第一卷積層:

int filter_size1 = 5;  // Convolution filters are 5 x 5 pixels.
int num_filters1 = 16; //  There are 16 of these filters.
int stride1 = 1;  // The stride of the sliding window


第二卷積層:

int filter_size2 = 5; // Convolution filters are 5 x 5 pixels.
int num_filters2 = 32;// There are 32 of these filters.
int stride2 = 1;  // The stride of the sliding window


完全連線層:

h1 = 128  # Number of neurons in fully-connected layer.


  1. 構建神經網路

讓我們做一些函式來幫助構建計算圖。

變數:我們需要定義兩個變數W和b來構造我們的線性模型。 我們使用適當大小和初始化的Tensorflow變數來定義它們。

// Create a weight variable with appropriate initialization
private RefVariable weight_variable(string name, int[] shape)
{
    var initer = tf.truncated_normal_initializer(stddev: 0.01f);
    return tf.get_variable(name,
                           dtype: tf.float32,
                           shape: shape,
                           initializer: initer);
}
// Create a bias variable with appropriate initialization
private RefVariable bias_variable(string name, int[] shape)
{
    var initial = tf.constant(0f, shape: shape, dtype: tf.float32);
    return tf.get_variable(name,
                           dtype: tf.float32,
                           initializer: initial);
}

2D卷積層:此層建立卷積核心,該卷積核心與層輸入卷積以產生輸出張量。

private Tensor conv_layer(Tensor x, int filter_size, int num_filters, int stride, string name)
{
    return with(tf.variable_scope(name), delegate {
var num_in_channel = x.shape[x.NDims - 1];
        var shape = new[] { filter_size, filter_size, num_in_channel, num_filters };
        var W = weight_variable("W", shape);
        // var tf.summary.histogram("weight", W);
        var b = bias_variable("b", new[] { num_filters });
        // tf.summary.histogram("bias", b);
        var layer = tf.nn.conv2d(x, W,
                                 strides: new[] { 1, stride, stride, 1 },
                                 padding: "SAME");
        layer += b;
        return tf.nn.relu(layer);
    });
}

max-pooling layer:池化層。

private Tensor max_pool(Tensor x, int ksize, int stride, string name)
{
    return tf.nn.max_pool(x,
                          ksize: new[] { 1, ksize, ksize, 1 },
                          strides: new[] { 1, stride, stride, 1 },
                          padding: "SAME",
                          name: name);
}

flatten_layer:展開卷積層的輸出以往前饋入完全連線的層。

private Tensor flatten_layer(Tensor layer)
{
    return with(tf.variable_scope("Flatten_layer"), delegate
                {
                    var layer_shape = layer.TensorShape;
                    var num_features = layer_shape[new Slice(1, 4)].Size;
                    var layer_flat = tf.reshape(layer, new[] { -1, num_features });
return layer_flat;
                });
}

完全連線層:神經網路由完全連線(密集)層的堆疊組成。 具有權重(W)和偏差(b)變數,完全連線的層被定義為啟用(W x X + b)。 完整的fc_layer函式如下:

private Tensor fc_layer(Tensor x, int num_units, string name, bool use_relu = true)
{
    return with(tf.variable_scope(name), delegate
                {
                    var in_dim = x.shape[1];
var W = weight_variable("W_" + name, shape: new[] { in_dim, num_units });
                    var b = bias_variable("b_" + name, new[] { num_units });
var layer = tf.matmul(x, W) + b;
                    if (use_relu)
                        layer = tf.nn.relu(layer);
return layer;
                });
}

輸入層:現在我們需要定義適當的張量來輸入我們的模型。 佔位符變數是輸入影象和相應標籤的合適選擇。 這允許我們將輸入(影象和標籤)更改為TensorFlow圖。

with(tf.name_scope("Input"), delegate
     {
         // Placeholders for inputs (x) and outputs(y)
         x = tf.placeholder(tf.float32, shape: (-1, img_h, img_w, n_channels), name: "X");
         y = tf.placeholder(tf.float32, shape: (-1, n_classes), name: "Y");
     });

佔位符y是與佔位符變數x中輸入的影象關聯的真實標籤的變數。 它包含任意數量的標籤,每個標籤是長度為num_classes的向量,為10。

網路層:在建立適當的輸入後,我們必須將它傳遞給我們的模型。 由於我們有神經網路,我們可以使用fc_layer方法堆疊多個完全連線的層。

var conv1 = conv_layer(x, filter_size1, num_filters1, stride1, name: "conv1");
var pool1 = max_pool(conv1, ksize: 2, stride: 2, name: "pool1");
var conv2 = conv_layer(pool1, filter_size2, num_filters2, stride2, name: "conv2");
var pool2 = max_pool(conv2, ksize: 2, stride: 2, name: "pool2");
var layer_flat = flatten_layer(pool2);
var fc1 = fc_layer(layer_flat, h1, "FC1", use_relu: true);
var output_logits = fc_layer(fc1, n_classes, "OUT", use_relu: false);

損失函式,優化器,精度,預測:建立網路後,我們必須計算損失並對其進行優化,我們必須計算預測和準確性。

with(tf.variable_scope("Train"), delegate
     {
with(tf.variable_scope("Optimizer"), delegate
              {
                  optimizer = tf.train.AdamOptimizer(learning_rate: learning_rate, name: "Adam-op").minimize(loss);
              });
with(tf.variable_scope("Accuracy"), delegate
              {
                  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");
              });
with(tf.variable_scope("Prediction"), delegate
              {
                  cls_prediction = tf.argmax(output_logits, axis: 1, name: "predictions");
              });
     });

初始化變數:我們必須呼叫變數初始化程式操作來初始化所有變數。

var init = tf.global_variables_initializer();

  1. 訓練

建立計算圖後,我們可以訓練我們的模型。 為了訓練模型,我們必須建立一個會話並在會話中執行圖。

// Number of training iterations in each epoch
var num_tr_iter = y_train.len / batch_size;
var init = tf.global_variables_initializer();
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 
    (x_train, y_train) = mnist.Randomize(x_train, y_train);
foreach (var iteration in range(num_tr_iter))
    {
        var start = iteration * batch_size;
        var end = (iteration + 1) * batch_size;
        var (x_batch, y_batch) = mnist.GetNextBatch(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, x_valid), new FeedItem(y, y_valid));
    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("---------------------------------------------------------");
}

  1. 測試

訓練完成後,我們必須測試我們的模型,看看它在新資料集上的表現如何。

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

結果非常好,比上一篇完全連線神經網路效果高1個百分點。

如果你想要重複這個過程,請到Github