zoukankan      html  css  js  c++  java
  • 使用 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 还可轻松应对多项分类问题,即包含三个或更多因变量的问题。

    源码下载

  • 相关阅读:
    Spring Boot 使用 Dom4j XStream 操作 Xml
    Spring Boot 使用 JAX-WS 调用 WebService 服务
    Spring Boot 使用 CXF 调用 WebService 服务
    Spring Boot 开发 WebService 服务
    Spring Boot 中使用 HttpClient 进行 POST GET PUT DELETE
    Spring Boot Ftp Client 客户端示例支持断点续传
    Spring Boot 发送邮件
    Spring Boot 定时任务 Quartz 使用教程
    Spring Boot 缓存应用 Memcached 入门教程
    ThreadLocal,Java中特殊的线程绑定机制
  • 原文地址:https://www.cnblogs.com/webyu/p/2937170.html
Copyright © 2011-2022 走看看