2019 年 6 月

第 34 卷,第 6 期

此文章由机器翻译。

[测试运行]

使用 C# 简化朴素贝叶斯分类

通过James McCaffrey |2019 年 6 月 |获取代码

James McCaffrey朴素贝叶斯分类问题的目标是预测离散值。例如,你可能想要预测基于其颜色、 大小和形状 gemstone 的真实性 (0 = 虚设,1 = 可信)。在本文中,我将介绍如何实现简化的朴素贝叶斯分类算法使用C#语言。

了解本文所述观点的最佳方法是看看该演示中运行图 1。演示程序设置了 40 虚拟数据项目。每个项都有三个预测因子值: (水绿色、 蓝色、 蓝绿色,沙丘色) 的颜色、 大小 (小、 大) 和一个二进制的类来预测 (0 或 1) 后跟的形状 (Pointed、 圆形、 Twisted)。

简化的 Naive Bayes 分类演示运行
图 1 简化 Naive Bayes 分类演示运行

此演示程序设置了某个项来预测:(青色,较小,扭曲)。Naive Bayes 分类基于概率,又基于数据中的计数。此演示程序扫描 40 项数据集和计算并显示六个结点计数: 具有的同时青色和 0 (3 项),蓝绿色和 1 (5),小型和 0 (18) 小到 1 (15) Twisted 和 0 (5),Twisted 和 1 (3)。此演示程序还计算类 0 项 (24) 和类 1 项 (16) 的数目。

使用计数信息,此演示程序计算中间值称为证据条款 (0.0082,0.0131) 并使用证据条款来计算伪概率 (0.3855,0.6145) 对应于类 0 和类 1 的预测。第二个伪概率较大,因为结论是,(青色,较小,扭曲) 是类 1。

本文假定您有中等或更好地编程技能与C#或 C 系列语言,如 Python 或 Java 中,但并不假定您知道有关朴素贝叶斯分类的任何信息。在本文中显示完整的演示代码和关联的数据。源代码和数据也是在随附下载中提供的。为了尽可能地让主要思想清晰明确,已删除所有常见错误检查。

了解 Naive Bayes 分类

Naive Bayes 分类是简单和复杂。实现相对简单,但基础数学创意是非常复杂。有许多不同的数学等式,用于定义朴素贝叶斯分类。三个示例所示图 2。字母 P 意味着概率;Ck 是 k 类之一 (0、 1、 2、...。);"给定的"; 读取管道符号并且 X 为如 (青色、 小、 Twisted) 输入值的向量。遗憾的是,这些等式不时实现直到朴素贝叶斯分类后,您了解该技术提供非常有帮助。

三种形式的 Naive Bayes 分类数学
图 2 三个窗体的 Naive Bayes 分类的数学计算

在我看来,朴素贝叶斯分类很好地解释使用具体的示例。第一步是通过将源数据扫描并计算结点计数。如果有 nx 预测器变量 (在演示中的三个) 和 nc 类 (演示中两个),则有 nx * nc 联合进行计数以计算。请注意盘点过程意味着预测值的数据必须是离散而不是数字。

计算结点计数之后, 1 添加到每个计数。这称为 Laplacian 平滑处理,是为了防止任何结点计数 0,这将最终结果清零。对于演示数据平滑结点计数为:

cyan and 0:     2 + 1 = 3
cyan and 1:     4 + 1 = 5
small and 0:   17 + 1 = 18
small and 1:   14 + 1 = 15
twisted and 0:  4 + 1 = 5
twisted and 1:  2 + 1 = 3

接下来,计算类 0 项和类 1 项的原始计数。由于这些计数将始终为大于零,没有平滑处理,因此需要身份。对于演示数据的类计数是:

0:  24
1:  16

接下来,计算的证据术语,每个类。对于类 0,证据项是:

Z(0) = (3 / 24+3) * (18 / 24+3) * (5 / 24+3) * (24 / 40)
        = 3/27 * 18/27 * 5/27 * 24/40
        = 0.1111 * 0.6667 * 0.1852 * 0.6
        = 0.0082

计算 Z(0) 的前三个条款以及数目的预测器变量使用除以 0 (24) 的类计数平滑结点计数为 0,类 (nx = 3) 来弥补由于 Laplacian 平滑处理 1 的三个新增功能。第四个术语为 P (类 0)。类 1 的证据项的计算如下所示相同的模式:

Z(1) = (5 / 16+3) * (15 / 16+3) * (3 / 16+3) * (16 / 40)
        = 5/19 * 15/19 * 3/19 * 16/40
        = 0.2632 * 0.7895 * 0.1579 * 0.4
        = 0.0131

最后一步是计算伪概率:

P(class 0) = Z(0) / (Z(0) + Z(1))
                 = 0.0082 / (0.0082 + 0.0131)
                 = 0.3855
P(class 1) = Z(1) / (Z(0) + Z(1))
                 = 0.0131 / (0.0082 + 0.0131)
                 = 0.6145

称为证据和用于规范化的证据条款,以便它们总和为 1.0,可以笼统地称之为概率分母之和。请注意,如果你只想在预测中,您只需使用最大的证据术语并跳过证据规范化步骤。

演示程序

图 3 展示了完整的演示程序(为节省空间,进行了少量小幅改动)。若要创建节目,我启动了 Visual Studio 并创建名为 NaiveBayes 的新控制台应用程序。我使用 Visual Studio 2017,但演示对并不非常依赖.NET Framework,因此任何版本的 Visual Studio 可以正常工作。

图 3 Naive Bayes 分类演示计划

using System;
using System.IO;
namespace CSharp
{
  class Program
  {
    static void Main(string[] args)
    {
      Console.WriteLine("\nBegin simple naive Bayes demo\n");
      string fn = "C:\\NaiveBayes\\data.txt";
      int nx = 3;  // Number predictor variables
      int nc = 2;  // Number classes
      int N = 40;  // Number data items
      string[][] data = LoadData(fn, N, nx+1, ',');
      Console.WriteLine("Training data:");
      for (int i = 0; i < 5; ++i) {
        Console.Write("[" + i + "] ");
        for (int j = 0; j < nx+1; ++j) {
          Console.Write(data[i][j] + " ");
        }
        Console.WriteLine("");
      }
      Console.WriteLine(". . . \n");
      int[][] jointCts = MatrixInt(nx, nc);
      int[] yCts = new int[nc];
      string[] X = new string[] { "Cyan", "Small", "Twisted" };
      Console.WriteLine("Item to classify: ");
      for (int i = 0; i < nx; ++i)
        Console.Write(X[i] + " ");
      Console.WriteLine("\n");
      // Compute joint counts and y counts
      for (int i = 0; i < N; ++i) {
        int y = int.Parse(data[i][nx]);
        ++yCts[y];
        for (int j = 0; j < nx; ++j) {
          if (data[i][j] == X[j])
            ++jointCts[j][y];
        }
      }
      // Laplacian smoothing
      for (int i = 0; i < nx; ++i)
        for (int j = 0; j < nc; ++j)
          ++jointCts[i][j];
      Console.WriteLine("Joint counts: ");
      for (int i = 0; i < nx; ++i) {
        for (int j = 0; j < nc; ++j) {
          Console.Write(jointCts[i][j] + " ");
        }
        Console.WriteLine("");
      }
      Console.WriteLine("\nClass counts: ");
      for (int k = 0; k < nc; ++k)
        Console.Write(yCts[k] + " ");
      Console.WriteLine("\n");
      // Compute evidence terms
      double[] eTerms = new double[nc];
      for (int k = 0; k < nc; ++k) {
        double v = 1.0;
        for (int j = 0; j < nx; ++j) {
          v *= (double)(jointCts[j][k]) / (yCts[k] + nx);
        }
        v *= (double)(yCts[k]) / N;
        eTerms[k] = v;
      }
      Console.WriteLine("Evidence terms:");
      for (int k = 0; k < nc; ++k)
        Console.Write(eTerms[k].ToString("F4") + " ");
      Console.WriteLine("\n");
      double evidence = 0.0;
      for (int k = 0; k < nc; ++k)
        evidence += eTerms[k];
      double[] probs = new double[nc];
      for (int k = 0; k < nc; ++k)
         probs[k] = eTerms[k] / evidence;
      Console.WriteLine("Probabilities: ");
      for (int k = 0; k < nc; ++k)
        Console.Write(probs[k].ToString("F4") + " ");
      Console.WriteLine("\n");
      int pc = ArgMax(probs);
      Console.WriteLine("Predicted class: ");
      Console.WriteLine(pc);
      Console.WriteLine("\nEnd naive Bayes ");
      Console.ReadLine();
    } // Main
    static string[][] MatrixString(int rows, int cols)
    {
      string[][] result = new string[rows][];
      for (int i = 0; i < rows; ++i)
        result[i] = new string[cols];
      return result;
    }
    static int[][] MatrixInt(int rows, int cols)
    {
      int[][] result = new int[rows][];
      for (int i = 0; i < rows; ++i)
        result[i] = new int[cols];
      return result;
    }
    static string[][] LoadData(string fn, int rows,
      int cols, char delimit)
    {
      string[][] result = MatrixString(rows, cols);
      FileStream ifs = new FileStream(fn, FileMode.Open);
      StreamReader sr = new StreamReader(ifs);
      string[] tokens = null;
      string line = null;
      int i = 0;
      while ((line = sr.ReadLine()) != null)  {
        tokens = line.Split(delimit);
        for (int j = 0; j < cols; ++j)
          result[i][j] = tokens[j];
        ++i;
      }
      sr.Close(); ifs.Close();
      return result;
    }
    static int ArgMax(double[] vector)
    {
      int result = 0;
      double maxV = vector[0];
      for (int i = 0; i < vector.Length; ++i) {
        if (vector[i] > maxV) {
          maxV = vector[i];
          result = i;
        }
      }
      return result;
    }
  } // Program
} // ns

加载模板代码后,在编辑器窗口中我删除不需要命名空间的所有引用并添加对 System.IO 命名空间的引用。在解决方案资源管理器窗口中,我右键单击文件 Program.cs,它重命名为更具描述性的 NaiveBayesProgram.cs,并允许 Visual Studio 会自动重命名 Program 类。

后生成项目时,我使用记事本创建 40 项虚拟数据文件中所示的内容图 4,并将其保存为 BayesData.txt 项目根目录中。

图 4 文件 BayesData.txt 内容

Aqua,Small,Twisted,1
Blue,Small,Pointed,0
Dune,Large,Rounded,0
Dune,Small,Rounded,1
Cyan,Large,Rounded,0
Aqua,Small,Rounded,1
Aqua,Small,Rounded,0
Cyan,Small,Pointed,1
Cyan,Small,Pointed,1
Dune,Small,Rounded,1
Dune,Small,Rounded,0
Dune,Small,Rounded,1
Dune,Small,Rounded,1
Cyan,Small,Pointed,1
Dune,Small,Rounded,1
Dune,Large,Rounded,0
Cyan,Small,Twisted,1
Blue,Small,Rounded,0
Aqua,Small,Pointed,1
Aqua,Small,Pointed,1
Dune,Small,Twisted,0
Blue,Small,Rounded,0
Dune,Small,Rounded,0
Blue,Small,Twisted,0
Dune,Small,Rounded,0
Aqua,Large,Pointed,1
Dune,Large,Rounded,0
Dune,Small,Rounded,0
Dune,Small,Rounded,0
Cyan,Large,Rounded,0
Dune,Small,Twisted,0
Dune,Large,Twisted,0
Dune,Small,Rounded,0
Dune,Small,Rounded,0
Dune,Large,Rounded,0
Aqua,Large,Rounded,1
Aqua,Small,Rounded,0
Aqua,Small,Rounded,1
Dune,Small,Rounded,0
Blue,Small,Rounded,0

将数据加载到内存

该演示使用程序定义的方法名为 LoadData 作为字符串类型的数组的数组样式矩阵将数据文件读入内存。方法 LoadData 假定类值的数据每一行上的最后一个值。备用设计是预测因子值读入字符串矩阵,读取 0-1 的整数类型数组类值。

LoadData 方法调用一个名为 MatrixString,创建指定数量的行 (40) 和列 (4) 的数组的字符串矩阵的帮助器方法。备用设计是以编程方式计算的行数和列,但在我看来硬编码的方法是更简单、 更好。

程序逻辑

所有程序控制逻辑都包含在 Main 方法。结点计数存储在名为 jointCts,使用一个名为 MatrixInt 的帮助器方法创建一个整数数组的矩阵。JointCts 预测因子值 (青色,较小,Twisted) 指向的行索引和列索引指类值。例如,jointCts [2] [1] 包含同时具有 Twisted 和类 1 的数据的项的计数。使用长度为 2 名为 yCts 整数数组存储具有类 0 和类 1 的数据项的数目。

使用结点计数和类计数来计算两个证据术语的代码是:

double[] eTerms = new double[nc];
for (int k = 0; k < nc; ++k) {
  double v = 1.0;
  for (int j = 0; j < nx; ++j) {
    v *= (double)(jointCts[j][k]) / (yCts[k] + nx);
  }
  v *= (double)(yCts[k]) / N;
  eTerms[k] = v;
}

请注意该 v 是 nx + 1 是所有小于 1.0 的小数部分的产品。这不是演示程序的问题,因为仅 nx = 3 预测因子值和 none 的部分条款曾经可以小于 1/27 = 0.0370。但在某些情况下,可能遇到算术乘以许多很小的值。

避免算术错误时计算证据术语的方法是使用利用的事实数据的日志技巧 log(A * B) = log(A) + log(B) 和 log(A / B) = log(A)-log(B)。通过计算每个证据期限内的日志,并随后执行该结果的 exp 函数,可以使用加法和减法的许多小的值而不是乘法和除法。一个可能使用重构日志技巧就是:

double[] eTerms = new double[nc];
for (int k = 0; k < nc; ++k) {
  double v = 0.0;
  for (int j = 0; j < nx; ++j) {
    v += Math.Log(jointCts[j][k]) - Math.Log(yCts[k] + nx);
  }
  v += Math.Log(yCts[k]) - Math.Log(N);
  eTerms[k] = Math.Exp(v);
}

生成预测的类

计算出的证据条款后,演示程序将对它们求和,并使用它们来计算伪概率。因为,尽管它们总和为 1.0,但它们并不真正表示结果的可能性很多重复的采样试验,我调用这些值伪概率。但是,可以谨慎将伪概率解释为轻度窗体的置信度。例如,伪概率的 (0.97,0.03) 建议比类更多的强度与 0 (0.56,0.44)。

通过调用程序定义 ArgMax 函数,它将数字数组中返回的最大值的索引来生成预测的类。例如,如果数组包含值 (0.20,0.50,介于 0.90,0.10) ArgMax 返回 2。

总结

在本文中介绍的演示程序执行二元分类,因为只有两个类值。程序逻辑的多类分类也无需修改即可使用。演示程序中的预测因子值是所有分类。如果您的数据具有数值数据,如 3.78 和 9.21,等的值的权重变量可以应用的数值数据装箱到类别,如 light、 中和繁重这篇文章中介绍的技术。

有几个其他形式的朴素贝叶斯分类,除了本文中介绍的类型。一种形式使用由演示程序,但可以处理数据所有预测因子值是数字的基本数学原则。但是,您必须做出假设数学的属性的数据,如数据是与某个特定平均值和标准偏差正态 (高斯) 分布。


Dr.James McCaffrey 供职于华盛顿地区雷蒙德市沃什湾的 Microsoft Research。他一直从事多个关键 Microsoft 产品,包括 Azure 和必应。Dr.可通过 jamccaff@microsoft.com 与 McCaffrey 取得联系。

衷心感谢以下 Microsoft 技术专家对本文的审阅:Chris Lee、 Ricky Loynd Kirk Olynyk


在 MSDN 杂志论坛讨论这篇文章