• 使用 C# 进行 Naive Bayes 分类


    Naive Bayes 分类的工作原理

    图 1 为例,程序目标是预测职业为教育,习惯用右手且身高较高(不低于 71.0 英寸)的人员的性别(男性或女性)。 为此,我们可以计算具有这一指定信息的人员是男性的概率,以及具有这一指定信息的人员是女性的概率,然后依据其中较大的概率来预测性别。 若用符号表示,我们希望得出 P(male | X)(含义是“给定自变量值 X 的条件下是男性的概率”)和 P(female | X),其中 X 是 (education, right, tall)。 Naive Bayes 中“naive”一词指所有 X 属性都假定为数学上的自变量,这可以极大地简化分类。 您可以找到许多在线参考资料,其中介绍了 Naive Bayes 分类背后很有趣的数学计算,但结果都是相对简单的。 符号表示如下:

    
              P(male | X) =
      [ P(education | male) * P(right | male) * P(tall | male) * P(male) ] /
        [ PP(male | X) + PP(female | X) ]
            

    可以看到,该等式是一个分数。 分子有时不严格地称为部分概率,是四个项的乘积。 本文中,我用非标准符号 PP 表示部分概率项。 分母是两项之和,其中一项是分子。 首先要计算 P(education | male),即在某人是男性的前提下其职业为教育的概率。 此概率最终可通过以下公式估计得出,即将职业为教育且性别为男性的训练用例的计数除以性别为男性(任意职业)的用例数:

    
              P(education | male ) = count(education & male) / count(male) = 2/24 = 0.0833
            

    同理可得:

    
              P(right | male) = count(right & male) / count(male) = 17/24 = 0.7083
    P(tall | male) = count(tall & male) / count(male) = 4/24 = 0.1667
            

    其次要计算 P(male)。 在 Naive Bayes 术语中,它称为先验概率。 对于计算先验概率的最佳方法,存在着一些争议。 一方面,我们可以假定没有任何理由认为存在的男性比存在的女性多,因此为 P(male) 赋值 0.5。 另一方面,我们可以利用训练数据有 24 位男性和 16 位女性这一事实,估计出 P(male) 概率为 24/40 = 0.6000。 我更喜欢后一种方法,即使用训练数据估计先验概率。

    现在,如果选择前面的 P(male | X) 等式,会发现该等式中包含 PP(female | X)。 下面的总和 PP(male | X) + PP(female | X) 有时称为证据。 PP(female | X) 各部分计算如下:

    
              P(education | female) = count(education & female) / count(female) = 4/16 = 0.2500
    P(right | female) = count(right & female) / count(female) = 14/16 = 0.8750
    P(tall | female) = count(tall & female) / count(female) = 2/16 = 0.1250
    P(female) = 16/40 = 0.4000
            

    因此,P(male | X) 的部分概率分子为:

    
              PP(male | X) = 0.0833 * 0.7083 * 0.1667 * 0.6000 = 0.005903
            

    同理可得,在 X = (education, right, tall) 的前提下女性的部分概率为:

    
              PP(female | X) = 0.2500 * 0.8750 * 0.1250 * 0.4000 = 0.010938
            

    最后,得出男性和女性的总体概率分别为:

    
              P(male | X) = 0.005903 / (0.005903 + 0.010938) = 0.3505
    P(female | X) = 0.010938 / (0.005903 + 0.010938) = 0.6495
            

    这些总体概率有时称为后验概率。 由于 P(female | X) 大于 P(male | X),因此系统判定未知人员的性别为女性。 但是,请等一等。 这两个概率 0.3505 和 0.6495 与图 1 所示的两个概率 0.3855 和 0.6145 虽然很接近,却截然不同。 产生这一差异的原因在于,演示程序使用了一项对基本 Naive Bayes 的重大可选修改,这项修改称为 Laplacian 平滑处理。

    Laplacian 平滑处理

    参考图 1 可以发现,人员职业为建筑且性别为女性的训练用例计数为 0。 在演示中,X 值为 (education, right, tall),其中并不包含建筑。 但假设 X 为 (construction, right, tall), 那么,在计算 PP(female | X) 的过程中,必须计算 P(construction | female) = count(construction & female) / count(female),而该概率为 0,这又会使整个部分概率为零。 简而言之,当结点计数为 0 时很糟糕。 避免此情况的最常用方法就是给所有结点计数加 1。 此方法看似粗暴,实际却有可靠的数学基础。 该方法称为加一平滑处理,是 Laplacian 平滑处理的一种具体形式。

    利用 Laplacian 平滑处理,如果如前所述 X = (education, right, tall),则 P(male | X) 和 P(female | X) 计算如下:

    
              P(education | male ) =
    count(education & male) + 1 / count(male) + 3 = 3/27 = 0.1111
    P(right | male) =
    count(right & male) + 1 / count(male) + 3 = 18/27 = 0.6667
    P(tall | male) =
    count(tall & male) + 1 / count(male) + 3 = 5/27 = 0.1852
    P(male) = 24/40 = 0.6000
    P(education | female) =
    count(education & female) + 1 / count(female) + 3 = 5/19 = 0.2632
    P(right | female) =
    count(right & female) + 1 / count(female) + 3 = 15/19 = 0.7895
    P(tall | female) =
    count(tall & female) + 1 / count(female) + 3 = 3/19 = 0.1579
    P(female) = 16/40 = 0.4000
            

    部分概率为:

    
              PP(male | X) = 0.1111 * 0.6667 * 0.1852 * 0.6000 = 0.008230
    PP(female | X) = 0.2632 * 0.7895 * 0.1579 * 0.4000 = 0.013121
            

    因此,两个最终概率为:

    
              P(male | X) = 0.008230 / (0.008230 + 0.013121) = 0.3855
    P(female | X) = 0.013121 / (0.008230 + 0.013121) = 0.6145
            

    这些便是图 1 所示屏幕快照中的值。 可以看到,虽然每个结点计数加了 1,但给分母 count(male) 和 count(female) 加了 3。 在某种程度上来说,3 是任意值,因为 Laplacian 平滑处理并不指定要使用的任何具体值。 在本例中,它是 X 属性 (occupation, dominance, height) 的数目。 在 Laplacian 平滑处理中,这是加到部分概率分母的最常见的值,但您也可以试用其他值。 在 Naive Bayes 的数学文字中,要加到分母的值通常被赋予符号 k。 您还会看到,在 Naive Bayes Laplacian 平滑处理中,通常不会修改先验概率 P(male) 和 P(female)。

    程序的整体结构

    图 1 所示运行中的演示程序是单个 C# 控制台应用程序。 Main 方法如图 2 所示(其中删除了一些 WriteLine 语句)。

    图 2 Naive Bayes 程序结构

    1.           using System;
    2. namespace NaiveBayes
    3. {
    4.   class Program
    5.   {
    6.     static Random ran = new Random(25); // Arbitrary
    7.     static void Main(string[] args)
    8.     {
    9.       try
    10.       {
    11.         string[] attributes = new string[] { "occupation""dominance",
    12.           "height""sex"};
    13.         string[][] attributeValues = new string[attributes.Length][];
    14.         attributeValues[0] = new string[] { "administrative",
    15.           "construction""education""technology" };
    16.         attributeValues[1] = new string[] { "left""right" };
    17.         attributeValues[2] = new string[] { "short""medium""tall" };
    18.         attributeValues[3] = new string[] { "male""female" };
    19.         double[][] numericAttributeBorders = new double[1][];
    20.         numericAttributeBorders[0] = new double[] { 64.071.0 };
    21.         string[] data = MakeData(40);
    22.         for (int i = 0; i < 4; ++i)
    23.           Console.WriteLine(data[i]);
    24.         string[] binnedData = BinData(data, attributeValues,
    25.           numericAttributeBorders);
    26.         for (int i = 0; i < 4; ++i)
    27.           Console.WriteLine(binnedData[i]);
    28.         int[][][] jointCounts = MakeJointCounts(binnedData, attributes,
    29.           attributeValues);
    30.         int[] dependentCounts = MakeDependentCounts(jointCounts, 2);
    31.         Console.WriteLine("Total male = " + dependentCounts[0]);
    32.         Console.WriteLine("Total female = " + dependentCounts[1]);
    33.         ShowJointCounts(jointCounts, attributeValues);
    34.         string occupation = "education";
    35.         string dominance = "right";
    36.         string height = "tall";
    37.         bool withLaplacian = true;
    38.         Console.WriteLine(" occupation = " + occupation);
    39.         Console.WriteLine(" dominance = " + dominance);
    40.         Console.WriteLine(" height = " + height);
    41.         int c = Classify(occupation, dominance, height, jointCounts,
    42.           dependentCounts, withLaplacian, 3);
    43.         if (c == 0)
    44.           Console.WriteLine("\nData case is most likely male");
    45.         else if (c == 1)
    46.           Console.WriteLine("\nData case is most likely female");
    47.         Console.WriteLine("\nEnd demo\n");
    48.       }
    49.       catch (Exception ex)
    50.       {
    51.         Console.WriteLine(ex.Message);
    52.       }
    53.     } // End Main
    54.     // Methods to create data
    55.     // Method to bin data
    56.     // Method to compute joint counts
    57.     // Helper method to compute partial probabilities
    58.     // Method to classify a data case
    59.   } // End class Program
    60. }
    61.         

    程序首先设置硬编码的 X 属性 occupation、dominance 和 height 以及因变量属性 sex。 在某些情况下,您可能更愿意通过扫描现有数据源确定这些属性,尤其在数据源为包含标题的数据文件或是包含列名的 SQL 表时。 演示程序还指定九个分类 X 属性值: occupation 的 (administrative, construction, education, technology);dominance 的 (left, right) 和 height 的 (short, medium, tall)。 在本例中,sex 有两个因变量属性值: (male, female)。 同样,您也可以通过扫描数据以编程方式确定属性值。

    演示程序通过设置硬编码的边界值 64.0 和 71.0 将 height 数值装箱,从而将小于等于 64.0 的 height 值分类为 short;将介于 64.0 与 71.0 之间的 height 值分类为 medium;将大于等于 71.0 的 height 值分类为 tall。 将 Naive Bayes 数值数据装箱时,边界值的数目比类别数少一个。 在本例中,确定 64.0 和 71.0 的方式如下:先扫描训练数据找出最小和最大 height 值(57.0 和 78.0),计算这两者之差 21.0,然后通过将该差值除以 height 类别数目 3 计算出间隔大小(即 7.0)。 多数情况下,您都会以编程而不是手动方式确定数值 X 属性的边界值。

    演示程序通过调用 Helper 方法 MakeData 生成有些随机的训练数据。 MakeData 再调用 Helper 方法 MakeSex、MakeOccupation、MakeDominance 和 MakeHeight。 例如,这些 Helper 方法会生成数据,使男性职业更可能为建筑和技术,男性习惯用手更可能为右手,而男性身高更可能介于 66.0 和 72.0 英寸之间。

    Main 中调用的主要方法及其用途如下:BinData 用于分类身高数据;MakeJointCounts 用于扫描装箱数据并计算结点计数;MakeDependentCounts 用于计算男性和女性的总人数;Classify 使用结点计数和因变量计数执行 Naive Bayes 分类。

    数据装箱

    方法 BinData 如图 3 所示。 该方法接受一个由逗号分隔字符串组成的数组,其中每个字符串类似于“education,left,67.5,male”。许多情况下,您将从每行均为字符串的文本文件读取训练数据。 该方法使用 String.Split 将每个字符串解析为标记。 Token[2] 表示 height。 它通过 double.Parse 方法从字符串转换为双精度类型。 height 数值与边界值进行比较,直到找到 height 的间隔,然后确定字符串形式的相应 height 类别。 然后,使用旧标记、逗号分隔符和新计算出的 height 类别字符串将所得的字符串连在一起。

    图 3 用于对身高进行分类的方法 BinData

    1.           static string[] BinData(string[] data, string[][] attributeValues,
    2.   double[][] numericAttributeBorders)
    3. {
    4.   string[] result = new string[data.Length];
    5.   string[] tokens;
    6.   double heightAsDouble;
    7.   string heightAsBinnedString;
    8.   for (int i = 0; i < data.Length; ++i)
    9.   {
    10.     tokens = data[i].Split(',');
    11.     heightAsDouble = double.Parse(tokens[2]);
    12.     if (heightAsDouble <= numericAttributeBorders[0][0]) // Short
    13.       heightAsBinnedString = attributeValues[2][0];
    14.     else if (heightAsDouble >= numericAttributeBorders[0][1]) // Tall
    15.       heightAsBinnedString = attributeValues[2][2];
    16.     else
    17.       heightAsBinnedString = attributeValues[2][1]; // Medium
    18.     string s = tokens[0] + "," + tokens[1] + "," + heightAsBinnedString +
    19.       "," + tokens[3];
    20.     result[i] = s;
    21.   }
    22.   return result;
    23. }
    24.         

    将数值数据装箱并非执行 Naive Bayes 分类的硬性要求。 Naive Bayes 可直接处理数值数据,但这些方法超出了本文的讨论范围。 数据装箱的优点是十分简单,而且无需对数据的数学分布(如高斯或泊松分布)明确作出任何具体假定。 不过,数据装箱实际上会丢失信息,而且需要确定并指定将数据划分为多少个类别。

    确定结点计数

    Naive Bayes 分类的关键在于计算结点计数。 在演示示例中,共有九个自变量 X 属性值 (administrative, construction, … tall) 和两个因变量属性值 (male, female),因此总共必须计算并存储 9 * 2 = 18 个结点计数。 我的首选方法是将结点计数存储在一个三维数组 int[][][] jointCounts 中。 第一个索引表示自变量 X 属性;第二个索引表示自变量 X 属性值;第三个索引表示因变量属性值。 例如,jointCounts[0][3][1] 表示属性 0 (occupation)、属性值 3 (technology) 和 sex 1 (female),换句话说,jointCounts[0][3][1] 中的值是职业为技术且性别为女性的训练用例的计数。 方法 MakeJointCounts 如图 4 所示。

    图 4 方法 MakeJointCounts

    1.           static int[][][] MakeJointCounts(string[] binnedData, string[] attributes,
    2.   string[][] attributeValues)
    3. {
    4.   int[][][] jointCounts = new int[attributes.Length - 1][][]; // -1 (no sex)
    5.   jointCounts[0] = new int[4][]; // 4 occupations
    6.   jointCounts[1] = new int[2][]; // 2 dominances
    7.   jointCounts[2] = new int[3][]; // 3 heights
    8.   jointCounts[0][0] = new int[2]; // 2 sexes for administrative
    9.   jointCounts[0][1] = new int[2]; // construction
    10.   jointCounts[0][2] = new int[2]; // education
    11.   jointCounts[0][3] = new int[2]; // technology
    12.   jointCounts[1][0] = new int[2]; // left
    13.   jointCounts[1][1] = new int[2]; // right
    14.   jointCounts[2][0] = new int[2]; // short
    15.   jointCounts[2][1] = new int[2]; // medium
    16.   jointCounts[2][2] = new int[2]; // tall
    17.   for (int i = 0; i < binnedData.Length; ++i)
    18.   {
    19.     string[] tokens = binnedData[i].Split(',');
    20.     int occupationIndex = AttributeValueToIndex(0, tokens[0]);
    21.     int dominanceIndex = AttributeValueToIndex(1, tokens[1]);
    22.     int heightIndex = AttributeValueToIndex(2, tokens[2]);
    23.     int sexIndex = AttributeValueToIndex(3, tokens[3]);
    24.     ++jointCounts[0][occupationIndex][sexIndex];
    25.     ++jointCounts[1][dominanceIndex][sexIndex];
    26.     ++jointCounts[2][heightIndex][sexIndex];
    27.   }
    28.   return jointCounts;
    29. }
    30.         

    该实现包含许多硬编码值,这样更容易理解。 例如,下面三条语句可换为一个 for 循环,该循环通过在数组 attributeValues 中使用 Length 属性来分配空间:

    1.           jointCounts[0] = new int[4][]; // 4 occupations
    2. jointCounts[1] = new int[2][]; // 2 dominances
    3. jointCounts[2] = new int[3][]; // 3 heights
    4.         

    Helper 函数 AttributeValueToIndex 接受属性索引和属性值字符串并返回相应的索引。 例如,AttributeValueToIndex(2, “medium”) 返回 height 属性中“medium”的索引,也就是 1。

    演示程序使用方法 MakeDependentCounts 确定男性和女性数据用例的数目。 有多种方法可以做到这一点。 参考图 1 可以发现,有一种方法是添加三个属性中任何一个的结点计数。 例如,男性人数是 count(administrative & male)、count(construction & male)、count(education & male) 和 count(technology & male) 的总和:

    1.           static int[] MakeDependentCounts(int[][][] jointCounts,
    2.   int numDependents)
    3. {
    4.   int[] result = new int[numDependents];
    5.   for (int k = 0; k < numDependents; ++k) 
    6.   // Male then female
    7.     for (int j = 0; j < jointCounts[0].Length; ++j)
    8.     // Scanning attribute 0
    9.       result[k] += jointCounts[0][j][k];
    10.   return result;
    11. }
    12.         

    对数据用例进行分类

    方法 Classify 如图 5 所示,该方法很短,因为它依赖于 Helper 方法。

    图 5 方法 Classify

    1.           static int Classify(string occupation, string dominance, string height,
    2.   int[][][] jointCounts, int[] dependentCounts, bool withSmoothing,
    3.   int xClasses)
    4. {
    5.   double partProbMale = PartialProbability("male", occupation, dominance,
    6.     height, jointCounts, dependentCounts, withSmoothing, xClasses);
    7.   double partProbFemale = PartialProbability("female", occupation, dominance,
    8.     height, jointCounts, dependentCounts, withSmoothing, xClasses);
    9.   double evidence = partProbMale + partProbFemale;
    10.   double probMale = partProbMale / evidence;
    11.   double probFemale = partProbFemale / evidence;
    12.   if (probMale > probFemale) return 0;
    13.   else return 1;
    14. }
    15.         

    方法 Classify 接受以下参数:jointCounts 和 dependentCounts 数组;一个布尔型字段,用于指示是否使用 Laplacian 平滑处理;参数 xClasses,在本示例中为 3,因为有三个自变量 (occupation, dominance, height)。 此参数也可从 jointCounts 参数推断得到。

    方法 Classify 返回 int 值,用于表示预测因变量的索引。 实际上,您可能想要返回每个因变量的概率的数组。 请注意,分类基于 probMale 和 probFemale,它们均是通过部分概率与证据值相除得出的。 您可能希望直接忽略证据项,只比较部分概率值本身。

    方法 Classify 返回具有最大概率的因变量的索引。 替代方法是提供一个阈值。 例如,假设 probMale 为 0.5001,probFemale 为 0.4999。 您可能认为这两个值过于接近,因而返回一个表示“未定”的分类值。

    方法 PartialProbability 代 Classify 执行了大部分操作,如图 6 所示。

    图 6 方法 PartialProbability

    1.           static double PartialProbability(string sex, string occupation, string dominance,
    2.   string height, int[][][] jointCounts, int[] dependentCounts,
    3.   bool withSmoothing, int xClasses)
    4. {
    5.   int sexIndex = AttributeValueToIndex(3, sex);
    6.   int occupationIndex = AttributeValueToIndex(0, occupation);
    7.   int dominanceIndex = AttributeValueToIndex(1, dominance);
    8.   int heightIndex = AttributeValueToIndex(2, height);
    9.   int totalMale = dependentCounts[0];
    10.   int totalFemale = dependentCounts[1];
    11.   int totalCases = totalMale + totalFemale;
    12.   int totalToUse = 0;
    13.   if (sex == "male") totalToUse = totalMale;
    14.   else if (sex == "female") totalToUse = totalFemale;
    15.   double p0 = (totalToUse * 1.0) / (totalCases); // Prob male or female
    16.   double p1 = 0.0;
    17.   double p2 = 0.0;
    18.   double p3 = 0.0;
    19.   if (withSmoothing == false)
    20.   {
    21.     p1 = (jointCounts[0][occupationIndex][sexIndex] * 1.0) / totalToUse
    22.     p2 = (jointCounts[1][dominanceIndex][sexIndex] * 1.0) / totalToUse;  
    23.     p3 = (jointCounts[2][heightIndex][sexIndex] * 1.0) / totalToUse;     
    24.   }
    25.   else if (withSmoothing == true)
    26.   {
    27.     p1 = (jointCounts[0][occupationIndex][sexIndex] + 1) /
    28.      ((totalToUse + xClasses) * 1.0); 
    29.     p2 = (jointCounts[1][dominanceIndex][sexIndex] + 1) /
    30.      ((totalToUse + xClasses) * 1.0 ;
    31.     p3 = (jointCounts[2][heightIndex][sexIndex] + 1) /
    32.      ((totalToUse + xClasses) * 1.0);
    33.   }
    34.   //return p0 * p1 * p2 * p3; // Risky if any very small values
    35.   return Math.Exp(Math.Log(p0) + Math.Log(p1) + Math.Log(p2) + Math.Log(p3));
    36. }
    37.         

    为清楚起见,方法 PartialProbability 几乎全部采用了硬编码。 例如,有四个概率组分 p0、p1、p2 和 p3。 您可以使用概率数组使 PartialProbability 更通用,该数组大小由 jointCounts 数组确定。

    请注意,该方法并不是返回四个概率组分的乘积,而是返回每个组分的对数和的对等指数。 使用对数概率是计算机学习算法中的一种标准方法,可用于避免非常小的实数数值可能出现的数值错误。

    总结

    本文展示的示例应该能为您向 .NET 应用程序添加 Naive Bayes 分类功能打下良好的基础。 Naive Bayes 分类是一种相对粗糙的方法,但相较神经网络分类、逻辑回归分类和支持向量机分类等比较精深的其他方法,确有多方面优势。 Naive Bayes 十分简单,相对易于实现,并且能够适应超大型数据集。 此外,Naive Bayes 还可轻松应对多项分类问题,即包含三个或更多因变量的问题。

    源码下载

  • 相关阅读:
    Java设计模式-----装饰模式
    Java并发包中Lock的实现原理
    ThreadLocal,静态变量,实例变量,局部变量的线程安全
    ThreadLocal类详解
    SQL之LEFT JOIN,EIGHT JOIN,INSERT JOIN的区别
    Wireshark 、HTTPWatch、Fiddler的介绍
    TCP/IP、HTTP、Socket的区别
    我希望你并不幸福
    Autoregressive Convolutional Neural Networks for Asynchronous Time Series
    DRL Lecture1:Policy Gradient
  • 原文地址:https://www.cnblogs.com/webyu/p/2937170.html
Copyright © 2020-2023  润新知