上一篇文章我們介紹了通過神經網絡來處理一個非線性回歸的問題,這次我們將采用神經網絡來處理一個多元分類的問題。
這次我們解決這樣一個問題:輸入一個人的身高和體重的數據,程序判斷出這個人的身材狀況,一共三個類別:偏瘦、正常、偏胖。
處理流程如下:
1、收集數據
2、構建神經網絡
3、訓練網絡
4、保存和消費模型
詳細步驟如下:
1、收集數據
對於一個復雜的業務數據,在實際應用時應該是通過收集取得數據,本文的重點不在數據收集,所以我們將制造一批標准數據來進行學習。
關於人體的胖瘦問題,有一個BMI算法,即:BMI=weight / (height * height),當BMI小於18時,認為偏瘦,當BMI大於28時,認為偏胖,18到28之間,認為正常。
首先隨機生成身高和體重的數據,然后計算BMI值,並對結果進行標記,其中,偏瘦標記為0,正常標記為1,偏胖標記為2 。代碼如下:
/// <summary> /// 加載訓練數據 /// </summary> /// <param name="total_size"></param> private (NDArray, NDArray) PrepareData(int total_size) { float[,] arrx = new float[total_size, num_features]; int[] arry = new int[total_size]; for (int i = 0; i < total_size; i++) { float weight = (float)random.Next(30, 100) / 100; float height = (float)random.Next(140, 190) / 100; float bmi = (weight * 100) / (height * height); arrx[i, 0] = weight; arrx[i, 1] = height; switch (bmi) { case var x when x < 18.0f: arry[i] = 0; break; case var x when x >= 18.0f && x <= 28.0f: arry[i] = 1; break; case var x when x > 28.0f: arry[i] = 2; break; } }
2、構建神經網絡
相對於簡單的非線性模型,本次的網絡結構會稍微復雜一些:
// 網絡參數
int num_features = 2; // data features
int num_classes = 3; // total output
/// <summary> /// 構建網絡模型 /// </summary> private Model BuildModel() { // 網絡參數 int n_hidden_1 = 64; // 1st layer number of neurons. int n_hidden_2 = 64; // 2nd layer number of neurons. var model = keras.Sequential(new List<ILayer> { keras.layers.InputLayer(num_features), keras.layers.Dense(n_hidden_1, activation:keras.activations.Relu), keras.layers.Dense(n_hidden_2, activation:keras.activations.Relu), keras.layers.Dense(num_classes, activation:keras.activations.Softmax) }); return model; }
首先,本次包含兩層神經網絡,激活函數均采用RELU,輸出層激活函數采用Softmax函數。
和上一篇文章中的網絡結構相比看上去復雜很多,但其本質實際上差別不大,只是多了一個Softmax函數。
請注意觀察3個Output節點,如果只是看其中一個節點的話,它實際上就是一個普通的非線性模型。
由於1、2、3三個節點的數據之和不一定等於1,Softmax函數的目的就是要使得最終輸出的三個數字之和為1,這樣數字本身就可以表示概率了。其計算方法也非常簡單:
最后我們看一下這個網絡的摘要信息:
第一層網絡的訓練參數數量:(2+1)*64=192
第二層網絡的訓練參數數量:(64+1)*64=4160
輸出層網絡的訓練參數數量:(64+1)*3=195
3、訓練網絡
(NDArray train_x, NDArray train_y) = PrepareData(1000); model.compile(optimizer: keras.optimizers.Adam(0.001f), loss: keras.losses.SparseCategoricalCrossentropy(), metrics: new[] { "accuracy" }); model.fit(train_x, train_y, batch_size: 128, epochs: 300);
這里注意一點:損失函數采用稀疏分類交叉熵(SparseCategoricalCrossentropy)方法,對於分類任務,大部分時候都是采用分類交叉熵方法作為損失函數。
下面為二值交叉熵的實現公式:
可以不用看公式,簡單理解交叉熵的含義:就是如果標記值為1時預測值接近1 或 標記值為0時預測值接近0 則損失函數的值就會比較小。
比如標記值為[1,0,0],預測值為[0.99,0.01,0],則損失比較小,反之,如果預測值為[0.1,0.1,0.8],則損失比較大。
下面時一個二值交叉熵的實現方法:
private Tensor BinaryCrossentropy(Tensor x, Tensor y) { var shape = tf.reduce_prod(tf.shape(x)); var count = tf.cast(shape, TF_DataType.TF_FLOAT); x = tf.clip_by_value(x, 1e-6f, 1.0f - 1e-6f); var z = y * tf.log(x) + (1 - y) * tf.log(1 - x); var result = -1.0f / count * tf.reduce_sum(z); return result; }
稀疏分類交叉熵和二值交叉熵的區別在於:二值交叉熵需要對標記結果進行獨熱編碼(one-hot),而稀疏分類交叉熵則不需要。
前面提到,我們對分類結果進行標記,其中,偏瘦標記為0,正常標記為1,偏胖標記為2;而采用二值交叉熵進行計算時,偏瘦標記為[1,0,0],正常標記為[0,1,0],偏胖標記為[0,0,1] 。
4、保存和消費模型
訓練完成后,我們通過消費這個模型來檢查模型的准確性。
/// <summary> /// 消費模型 /// </summary> private void test(Model model) { int test_size = 20; for (int i = 0; i < test_size; i++) { float weight = (float)random.Next(40, 90) / 100; float height = (float)random.Next(145, 185) / 100; float bmi = (weight * 100) / (height * height); var test_x = np.array(new float[1, 2] { { weight, height } }); var pred_y = model.Apply(test_x); Console.WriteLine($"{i}:weight={(float)weight} \theight={height} \tBMI={bmi:0.0} \tPred:{pred_y[0].numpy()}"); } }
下面為測試結果:
隨便看兩條數據:當BMI為30.5時,預測結果為[0,0.0016,0.9983];當BMI為12.5時,預測結果為:[1,0,0],可見結果還是准確的。
全部代碼如下:

/// <summary> /// 通過神經網絡來實現多元分類 /// </summary> public class NN_MultipleClassification_BMI { private readonly Random random = new Random(1); // 網絡參數 int num_features = 2; // data features int num_classes = 3; // total output . public void Run() { var model = BuildModel(); model.summary(); Console.WriteLine("Press any key to continue..."); Console.ReadKey(); (NDArray train_x, NDArray train_y) = PrepareData(1000); model.compile(optimizer: keras.optimizers.Adam(0.001f), loss: keras.losses.SparseCategoricalCrossentropy(), metrics: new[] { "accuracy" }); model.fit(train_x, train_y, batch_size: 128, epochs: 300); test(model); } /// <summary> /// 構建網絡模型 /// </summary> private Model BuildModel() { // 網絡參數 int n_hidden_1 = 64; // 1st layer number of neurons. int n_hidden_2 = 64; // 2nd layer number of neurons. var model = keras.Sequential(new List<ILayer> { keras.layers.InputLayer(num_features), keras.layers.Dense(n_hidden_1, activation:keras.activations.Relu), keras.layers.Dense(n_hidden_2, activation:keras.activations.Relu), keras.layers.Dense(num_classes, activation:keras.activations.Softmax) }); return model; } /// <summary> /// 加載訓練數據 /// </summary> /// <param name="total_size"></param> private (NDArray, NDArray) PrepareData(int total_size) { float[,] arrx = new float[total_size, num_features]; int[] arry = new int[total_size]; for (int i = 0; i < total_size; i++) { float weight = (float)random.Next(30, 100) / 100; float height = (float)random.Next(140, 190) / 100; float bmi = (weight * 100) / (height * height); arrx[i, 0] = weight; arrx[i, 1] = height; switch (bmi) { case var x when x < 18.0f: arry[i] = 0; break; case var x when x >= 18.0f && x <= 28.0f: arry[i] = 1; break; case var x when x > 28.0f: arry[i] = 2; break; } } return (np.array(arrx), np.array(arry)); } /// <summary> /// 消費模型 /// </summary> private void test(Model model) { int test_size = 20; for (int i = 0; i < test_size; i++) { float weight = (float)random.Next(40, 90) / 100; float height = (float)random.Next(145, 185) / 100; float bmi = (weight * 100) / (height * height); var test_x = np.array(new float[1, 2] { { weight, height } }); var pred_y = model.Apply(test_x); Console.WriteLine($"{i}:weight={(float)weight} \theight={height} \tBMI={bmi:0.0} \tPred:{pred_y[0].numpy()}"); } } }
【相關資源】
源碼:Git: https://gitee.com/seabluescn/tf_not.git
項目名稱:NN_MultipleClassification_BMI