测试运行

使用神经网络进行分类和预测

James McCaffrey

下载代码示例

James McCaffrey

在本月的专栏中,我将说明如何使用神经网络解决分类和预测问题。说明分类目标的最好的方法还是通过示例。假定您具有与鸢尾花有关的历史数据,如下所示:

5.1 3.5 1.4 0.2 Setosa
7.0 3.2 4.7 1.4 Versicolor
6.3 3.3 6.0 2.5 Virginica
6.4 3.2 4.5 1.5 Versicolor
...

每一行数据都具有五个字段,这五个字段用空格分隔。 前四个数字字段是花萼(绿色芽状覆盖物)长度、花萼宽度、花瓣(花朵的带颜色部分)长度和花瓣宽度。 第五个字段是种类: Setosa(山鸢尾)、Versicolor(变色鸢尾)或 Virginica(维吉尼亚鸢尾)。 该分类目标是要确定预测某一鸢尾花属于哪一种类的一个方程式或一组规则。 然后,可以使用该规则组基于萼片和花瓣的长度和宽度值预测新鸢尾花的种类。 此鸢尾花植物数据由 R. A. Fisher 在 1936 年首次作为典型示例使用。 分类可能不会令您兴奋,但非常重要。 有关的示例包括:基于收入和每月支出之类的变量对申请人的信用评级进行分类(或者,也可以说是预测申请人的信用可靠性),基于验血的值对医院患者是否患有癌症进行分类。

有许多对数据进行分类的方法,包括使用神经网络。 可以通过这样一种方法来考虑神经网络:神经网络就是虚拟的输入-输出设备,接受任何数目的数字输入并且生成任何数目的数字输出。 要想了解我在本专栏中所讲述的内容,最好是查看图 1 所示的屏幕快照以及图 2 中的图像。 图 1 显示神经网络分类实操。 为了让使用神经网络进行分类的概念更清晰明了,我没有使用真实的数据。 我而是使用了虚构的数据,其中,输入 x 值是没有特定意义的四个任意数值。 用于分类的输出 y 变量是颜色,可取以下三个分类值之一: red、green 或 blue。 图 1 中所示的程序通过生成具有 100 行虚构数据(例如“8.0 5.0 9.0 5.0 green”)的文本开始,然后显示这些数据的前四行。 然后,该程序将此原始数据文件读入内存中,作为具有 80 行数据的定型矩阵以及具有 20 行数据的测试矩阵。 请注意,两个变化形式应用于原始数据。 原始数字输入数据将被规范化,以便所有值都介于 -1.00 和 +1.00 之间,并且原始输出数据(例如“red”)将被编码为具有三个值的向量(“1.0 0.0 0.0”)。


图 1 神经网络分类实操

Neural Network Structure
图 2 神经网络结构

在创建定型矩阵和测试矩阵后,该演示程序将创建一个完全连接的前向式神经网络,它具有三个输入神经元、五个用于计算的隐藏的神经元以及三个输出神经元。 这表明 4-5-3 完全连接的神经网络要求 43 个权重和偏置。 接下来,分类程序将对定型数据进行分析,以便找到最佳的 43 个权重和偏置(它们将分类总误差降至最低)。 该程序使用粒子群优化以及互熵误差来估计权重和偏置的最佳值。

该分类程序然后将这些最佳权重和偏置值加载到神经网络中,并且对测试矩阵中的 20 行数据评估模型的预测准确性。 请注意,已对神经网络的输出进行了设计,以便三个输出值的总和为 1.0。 在这个示例中,该模型正确预测了 20 个测试向量中的 17 个。 图 2 中的图像阐释一个神经网络,它接受 (-1.00, 1.00, 0.25, -0.50) 的输入并且生成 (0.9, 0.1, 0.0) 的预测输出(对应于红色)。

该示例程序表明,在使用神经网络进行分类(其中,输入数据是数字,输出数据是分类)时要作出以下五个主要决策:

  1. 如何规范化数字输入数据
  2. 如何对分类输出数据进行编码
  3. 如何在 [0.0, 1.0] 范围中生成神经输出
  4. 如何在定型时测量误差
  5. 如何在测试时测量精确性

在随后的部分中,我将说明在该示例程序中作出的如下选择:

  1. 对数字输入数据执行线性转换
  2. 将 1-of-N 编码用于分类输出数据
  3. 将 Softmax 激活函数用于输出层
  4. 使用互熵误差来确定最佳权重
  5. 使用 winner-takes-all(赢家通吃)方法来确定精确性

生成了图 1 中的屏幕快照的程序代码有点过长,无法在本文中提供,因此,我只是强调了所使用的算法。 完整程序源在 MSDN 代码下载网站(网址为 archive.msdn.microsoft.com/mag201207TestRun)上提供。 本文假定您具有高级编程技能,并大体上了解神经网络。 我在《MSDN 杂志》2012 年 5 月刊 (msdn.microsoft.com/magazine/hh975375) 中介绍了神经网络基础知识。

程序的整体结构

图 3 列出了在图 1 中运行的示例的程序结构。 我使用 Visual Studio 2010 创建了一个名为 NeuralClassification 的 C# 控制台应用程序。 在解决方案资源管理器窗口中,我将文件 Program.cs 重命名为更具描述性的 NeuralClassificationProgram.cs,它将自动重命名包含 Main 的类。 我删除了 Visual Studio 模板生成的不必要的 using 语句,并且添加了对 System.IO 命名空间的引用。

图 3 神经分类程序结构

using System;
using System.IO;
namespace NeuralClassification
{
  class NeuralClassificationProgram
  {
    static Random rnd = null;
    static void Main(string[] args)
    {
      try
      {
        Console.WriteLine("\nBegin Neural network classification\n");
        rnd = new Random(159); // 159 makes a nice example
        string dataFile = "..
\\..
\\colors.txt";
        MakeData(dataFile, 100);
        double[][] trainMatrix = null;
        double[][] testMatrix = null;
        MakeTrainAndTest(dataFile, out trainMatrix, out testMatrix);
        NeuralNetwork nn = new NeuralNetwork(4, 5, 3);
        double[] bestWeights = nn.Train(trainMatrix);
        nn.SetWeights(bestWeights);
        double accuracy = nn.Test(testMatrix);
        Console.WriteLine("\nEnd neural network classification\n");
      }
      catch (Exception ex)
      {
        Console.WriteLine("Fatal: " + ex.Message);
      }
    } // Main()
    static void MakeData(string dataFile, int numLines) { ...
}
    static void MakeTrainAndTest(string file, out double[][] trainMatrix,
      out double[][] testMatrix) { ...
}
  }
  class NeuralNetwork
  {
    // Class member fields here
    public NeuralNetwork(int numInput, int numHidden,
      int numOutput) { ...
}
    public void SetWeights(double[] weights) { ...
}
    public double[] ComputeOutputs(double[] currInputs) { ...
}
    private static double SigmoidFunction(double x) { ...
}
    private static double[] Softmax(double[] hoSums) { ...
}
    public double[] Train(double[][] trainMatrix) { ...
}
    private double CrossEntropy(double[][] trainData,
      double[] weights) { ...
}
    public double Test(double[][] testMatrix) { ...
}
  }
  public class Helpers
  {
    static Random rnd = new Random(0);
    public static double[][] MakeMatrix(int rows, int cols) { ...
}
    public static void ShuffleRows(double[][] matrix) { ...
}
    public static int IndexOfLargest(double[] vector) { ...
}
    public static void ShowVector(double[] vector, int decimals,
      bool newLine) { ...
}
    public static void ShowMatrix(double[][] matrix, int numRows) { ...
}
    public static void ShowTextFile(string textFile, int numLines) { ...
}
  }
  public class Particle
  {
    // Class member fields here
    public Particle(double[] position, double fitness,
      double[] velocity, double[] bestPosition,
     double bestFitness) { ...
}
    public override string ToString() { ...
}
  }
} // ns

除了包含 Main 方法的类之外,该程序具有其他三个类。 类 NeuralNetwork 封装完全连接的前向式神经网络。 所有核心程序逻辑都包含在此类中。 类 Helpers 包含六个实用工具例程。 类 Particle 定义粒子群优化算法在 NeuralNetwork 类的 Train 方法中使用的粒子对象。 分类程序的一个特性是存在许多可能的程序结构;此处展示的组织仅仅是其中一个可能性。

生成原始数据文件

在大多数分类方案中,您已具有一组原始数据,但对于本文,我使用方法 MakeData 创建了虚拟原始数据。 下面是伪代码中的进程:

create 43 arbitrary weights between -2.0 and +2.0
create a 4-5-3 neural network
load weights into the neural network
open a result file for writing
loop 100 times
  generate four random inputs x0, x1, x2, x3 between 1.0 and 9.0
  compute the y0, y1, y2 neural outputs for the input values
  determine largest of y0, y1, y2
  if y0 is largest write x0, x1, x2, x3, red
  else if y1 is largest write x0, x1, x2, x3, green
  else if y2 is largest write x0, x1, x2, x3, blue
end loop
close result file

这里的目标是获取具有百分之百精确性的可明确分类的一些数据,而非不清楚分类方法有效性如何的随机数据。 换言之,我使用某个神经网络创建原始数据,然后重新开始使用并使用神经网络尝试对这些数据进行分类。

创建定型矩阵和测试矩阵

在使用一组现有数据执行分类分析时,一个常见的方法(称作似然度验证)是将数据拆分成两部分,其中一个较大的数据集(通常是 80%)用于对神经网络进行定型,另一个较小的数据集 (20%) 用于对模型进行测试。 定型意味着找到将某个误差值降至最低的神经网络权重和偏置。 测试意味着使用一些精确性度量值通过在定型过程中发现的最佳权重对神经网络进行评估。 在这里,方法 MakeTrainAndTest 创建定型矩阵和测试矩阵,并且还规范化数字输入数据以及对分类输出数据进行编码。 在伪代码中,该方法的工作方式如下:

determine how many rows to split data into
create a matrix to hold all data
loop
  read a line of data
  parse each field
  foreach numeric input
    normalize input to between -1.0 and +1.0
  end foreach
  encode categorical output using 1-of-N
  place normalized and encoded data in matrix
end loop
shuffle matrix
create train and test matrices
transfer first 80% rows of matrix to train, remaining rows to test

方法签名为:

static void MakeTrainAndTest(string file, out double[][] trainMatrix,
  out double[][] testMatrix)

称作 file 的参数是要创建的原始数据文件的名称。 参数 trainMatrix 和 testMatrix 是用于放置结果的输出参数。 该方法的开头如下所示:

int numLines = 0;
FileStream ifs = new FileStream(file, FileMode.Open);
StreamReader sr = new StreamReader(ifs);
while (sr.ReadLine() != null)
  ++numLines;
sr.Close(); ifs.Close();
int numTrain = (int)(0.80 * numLines);
int numTest = numLines - numTrain;

此代码计算原始数据文件中的行数,然后计算多少行构成 80% 的数据,多少行构成 20% 的数据。 这里的这些百分比是硬编码的;您可能要对它们执行参数化。 接下来,将分配存放所有数据的矩阵:

double[][] allData = new double[numLines][];
  for (int i = 0; i < allData.Length; ++i)
    allData[i] = new double[7];

共有七列数据: 四个数字输入各有一列,分类的颜色变量的 1-of-N 编码值有三列。 请注意,对于此示例,目标是预测颜色,颜色可以是以下三个分类值之一: red、green 或 blue。 在此情况下使用 1-of-N 技术进行编码意味着将红色编码为 (1.0, 0.0, 0.0)、将绿色编码为 (0.0, 1.0, 0.0) 以及将蓝色编码为 (0.0, 0.0, 1.0)。 分类数据必须以数字形式进行编码,因为神经网络仅直接处理数值。 事实证明,使用简单方法(例如 1 表示红色、2 表示绿色以及 3 表示蓝色)对颜色进行编码是错误的思路。 要说明这种想法为什么错误的篇幅稍有点长,因此不在本文的论述范围内。

针对分类输出数据的 1-of-N 编码指导原则有一个例外,就是在只有两个可能值(例如“male”或“female”)时,可以使用具有单个数字输出值的 1-of-(N-1) 编码,这样,就上面的例子而言,0.0 表示男性,1.0 表示女性。

该编码由下面的代码执行:

tokens = line.Split(' ');
allData[row][0] = double.Parse(tokens[0]);
allData[row][1] = double.Parse(tokens[1]);
allData[row][2] = double.Parse(tokens[2]);
allData[row][3] = double.Parse(tokens[3]);
for (int i = 0; i < 4; ++i)
  allData[row][i] = 0.25 * allData[row][i] - 1.25;
if (tokens[4] == "red") {
  allData[row][4] = 1.0; 
  allData[row][5] = 0.0; 
  allData[row][6] = 0.0; }
else if (tokens[4] == "green") {
  allData[row][4] = 0.0; 
  allData[row][5] = 1.0; 
  allData[row][6] = 0.0; }
else if (tokens[4] == "blue") {
  allData[row][4] = 0.0; 
  allData[row][5] = 0.0; 
  allData[row][6] = 1.0; }

请注意,一行原始数据如下:

8.0 5.0 9.0 5.0 green

使用 String.Split 对这五个字段进行分析。 经验证明,在大多数情况下,在数字输入是 -1.0 和 +1.0 范围内的值时,可以获得更好的结果。 通过对前四个数字输入中的每个输入都先乘以 0.25 再减去 1.25,转换这些输入。 请注意,这个虚拟的数据文件中的数字输入都介于 1.0 和 9.0 之间。 在真实的分类问题中,您需要对原始数据进行扫描,并且确定最小值和最大值。 我们希望 -1.0 对应于 1.0,+1.0 对应于 9.0。 执行线性转换意味着查找斜率(此处为 0.25)和截距 (-1.25)。 这些值可计算为:

slope = 2 / (max value - min value) = 2 / (9.0 - 1.0) = 0.25
intercept = 1.0 - (slope * max value) = 1 - (0.25 * 9.0) = -1.25

有许多可用于对数字输入值执行线性转换的方法,但本文中介绍的方法十分简单,并且在大多数情况下都是很好的起点。

在已转换了四个数字输入值后,将使用 1-of-N 编码对来自原始数据文件的颜色值进行编码。 在来自原始数据文件的所有值都已计算并放置于 allData 矩阵中之后,该矩阵将使用 Helpers 类中的 ShuffleRows 实用工具方法随机重新排列其行。 在 allData 中行的顺序已打乱后,将分配针对矩阵 trainMatrix 和 testMatrix 的空间,然后将 allData 的前 numTrain 行复制到 trainMatrix,将 allData 的其余 numTest 行复制到 testMatrix。

针对此定型-测试方法的一个重要的设计方案就是将原始数据划分为三组: 定型、验证和测试。 具体的思路是使用定型数据确定一组最佳的神经网络权重以及验证数据(用于知道何时停止定型)。 也有其他方法,统称为交叉验证技术。

Softmax 激活函数

在使用输出变量是分类的神经网络执行分类时,在神经网络输出激活函数中有一个相当棘手的关系,在定型期间计算误差以及计算神经网络的可预测精确性。 在使用 1-of-N 编码对分类输出数据(例如具有值 red、green 或 blue 的颜色)进行编码时,例如,(1.0 0.0 0.0) 表示红色,您希望神经网络生成三个数值,以便您可以在对网络进行定型时确定误差。 不过,您不希望生成三个任意数值,因为这样不会完全清楚如何计算误差项。 但是,假定神经网络生成三个数值,它们全都介于 0.0 和 1.0 之间并且总和是 1.0。 然后,可以将生成的值解释为概率,事实证明,这样可以很容易地在定型时计算误差项以及在测试时计算精确度。 Softmax 激活函数以此形式生成输出值。 Softmax 函数接受对输出隐藏的神经总和值,并且返回最终的神经输出值;可按如下所示实现该函数:

private static double[] Softmax(double[] hoSums)
{
  double max = hoSums[0];
  for (int i = 0; i < hoSums.Length; ++i)
    if (hoSums[i] > max) max = hoSums[i];
  double scale = 0.0;
    for (int i = 0; i < hoSums.Length; ++i)
      scale += Math.Exp(hoSums[i] - max);
  double[] result = new double[hoSums.Length];
    for (int i = 0; i < hoSums.Length; ++i)
      result[i] = Math.Exp(hoSums[i] - max) / scale;
  return result;
}

从原理上讲,Softmax 函数按以下方式计算比例因子:对每个对输出隐藏的总和执行 Exp 计算,对它们求和,然后将每个值的 Exp 除以比例因子。 例如,假定三个对输出隐藏的总和为 (2.0, -1.0, 4.0)。 比例因子将是 Exp(2.0) + Exp(-1.0) + Exp(4.0) = 7.39 + 0.37 + 54.60 = 62.36。 然后,Softmax 输出值将是 Exp(2.0)/62.36, Exp(-1.0)/62.36, Exp(4.0)/62.36) = (0.12, 0.01, 0.87)。 请注意,最终的输出全都介于 0.0 和 1.0 之间并且实际求和结果为 1.0,进一步说,最大的对输出隐藏的总和 (4.0) 具有最大的输出/概率 (0.87),并且对于第二最大值和第三最大值而言该关系是类似的。

但遗憾的是,Softmax 函数的简单实现常常会失败,因为 Exp 函数很快就会变得非常大并且会生成算术溢出。 前面的实现使用 Exp(a - b) = Exp(a) / Exp(b),以便用避免溢出的方式计算输出。 如果您将 (2.0, -1.0, 4.0) 用作输入来跟踪实现的执行,将会得到与在之前部分中说明的相同的 (0.12, 0.01, 0.87) 输出。

互熵误差

对神经网络进行定型的本质是找到一组权重,对于对定型集中的数据,这组权重产生最小的误差。 例如,假定一行规范化的、已编码的定型数据为 (0.75 -0.25 0.25 -0.50 0.00 1.00 0.00)。 请记住,前四个值是规范化输入,最后三个值在 1-of-N 编码中表示绿色。 现在假定输入通过神经网络馈送,该神经网络已加载有某组权重,并且 Softmax 输出为 (0.20 0.70 0.10)。 为此测试向量计算误差的传统方法是使用方差和,在此例子中是 (0.00 - 0.20)^2 + (1.00 - 0.70)^2 + (0.00 - 0.10)^2 = 0.14。 但假定 Softmax 输出为 (0.30 0.70 0.00)。 此向量以及之前的向量都预测输出是绿色,因为绿色的概率是 0.70。 但是,第二个向量的方差和是 0.18,这与第一个误差项不同。 尽管方差和可用于计算定型误差,但某些研究结果表明,使用称作互熵误差的其他测量方法更可取。

从原理上讲,某一给定神经网络输出向量 v 和测试输出向量 t 的互熵误差是通过每个 v 向量分量和相应的 t 向量分量的乘积的负求和来确定的。 最好地解释这个方法的依然是示例。 如果某个测试向量为 (0.75 -0.25 0.25 -0.50 0.00 1.00 0.00) 并且相应的 Softmax 神经网络输出为 (0.20 0.70 0.10),则相应的互熵误差是 -1 * (0.00 * Log(0.20)) + (1.00 * Log(0.70)) + (0.00 * Log(0.10)) = -1 * (0 + -0.15 + 0) = 0.15。 请注意,在使用 1-of-N 编码时,除了一个之外,总和中的所有项都将是零。 整个定型集的互熵误差可计算为所有测试向量的互熵的总和,或者每个测试向量的平均互熵。 互熵误差的实现在图 4 中列出。

图 4 互熵误差

private double CrossEntropy(double[][] trainData, 
  double[] weights)
{
  this.SetWeights(weights);
  double sce = 0.0; // sum of cross entropies
  for (int i = 0; i < trainData.Length; ++i)
  {
    double[] currInputs = new double[4];
    currInputs[0] = trainData[i][0];
    currInputs[1] = trainData[i][1];
    currInputs[2] = trainData[i][2];
    currInputs[3] = trainData[i][3];
    double[] currExpected = new double[3];
    currExpected[0] = trainData[i][4];
    currExpected[1] = trainData[i][5];
    currExpected[2] = trainData[i][6];
    double[] currOutputs = this.ComputeOutputs(currInputs);
    double currSum = 0.0;
    for (int j = 0; j < currOutputs.Length; ++j)
    {
      if (currExpected[j] != 0.0)
        currSum += currExpected[j] * Math.Log(currOutputs[j]);
    }
    sce += currSum; // accumulate
  }
  return -sce;
}

对神经网络进行定型

有许多方法可以对神经网络分类器进行定型,以便找到最好地匹配定型数据(或者,也可以说生成最小的互熵误差)的一组权重值。 在高抽象级别,对神经网络进行定型的方式如下:

create an empty neural network
loop
  generate a candidate set of weights
  load weights into neural network
  foreach training vector
    compute the neural output
    compute cross-entropy error
    accumulate total error
  end foreach
  if current weights are best found so far
    save current weights
  end if
until some stopping condition
return best weights found

迄今为止,用于对神经网络进行定型的最常见技术称作反向传播。 有大量研究文章都在论述这个技术 — 数目是如此之多,实际上,如果您是神经网络分类领域中的新手,可能很容易就会因此误认为反向传播是用于定型的唯一技术。 评估某一神经网络的权重的最佳集合是一个数字最小化问题。 对使用反向传播的两个常见的替代方法是使用真实值遗传算法(也称作进化优化算法)和使用粒子群优化。 每种评估技术都各有优缺点。 图 1 中显示的程序使用粒子群优化。 我在《MSDN 杂志》2012 年 6 月刊 (msdn.microsoft.com/magazine/jj133825) 中介绍了进化优化算法,在 2011 年 8 月刊 (msdn.microsoft.com/magazine/hh335067) 中介绍了粒子群优化。

有许多技术可用于确定何时停止对神经网络的定型。 尽管在互熵误差非常接近于零(指示接近完美拟合)之前只需让定型算法运行就行了,但这样做的危险在于,最终生成的权重将过度拟合定型数据,并且这些权重所创建的神经网络对不在定型集中的数据进行分类的效果会很差。 此外,定型直到互熵误差中没有变化可能很容易就会导致模型过度拟合。 由示例程序使用的简单方法会将定型限制为固定数目的迭代。 在大多数情况下,避免过度拟合的一个好得多的方法是将源数据集拆分为定型-验证-测试集。 通常,这三个数据集分别使用源数据的 60%、20% 和 20%。 该技术如前所述对定型集计算误差,但在主循环中的每次迭代后,该技术都将对验证数据集计算互熵误差。 在针对验证集的互熵误差开始显示误差在稳步增加时,很可能定型算法已开始过度拟合数据并且定型应该中止。 还有其他许多可能的停止技术。

对神经网络分类器进行评估

在神经网络分类器已定型并且生成了一组最佳权重和偏置后,下一步是确定对于测试数据而言,最终生成的模型的精确度(其中,模型意味着具有一组最佳权重的神经网络)。 尽管可以使用方差和或互熵误差之类的度量值,但精确度的合理度量值只不过是该模型生成的正确预测的百分比。 再次重申,有若干方法可用于测量精确度,但一个简单的方法是使用称作 winner-takes-all 的方法。 还是要用示例来很好地解释这个技术。 假定某个测试向量为 (-1.00 1.00 0.25 -0.50 1.00 0.00 0.00),如图 1 中第一组预测数据所示。 神经网络使用一组最佳权重,生成预测的 Softmax 输出 (0.9 0.1 0.0)。 如果每个输出都解释为概率,则最高概率为 0.9,并且预测的输出可视作 (1 0 0),这样,该模型将作出正确的预测。 换句话说,winner-takes-all 技术确定具有最大值的神经网络输出分量,使该分量为 1,所有其他分量均为 0,并且将该结果与定型向量中的实际数据进行比较。 在图 1 的第三组精确性分析数据中,测试向量为 (-0.50 0.75 0.25 -0.75 0.0 0.0 1.0)。 实际输出数据为 (0.0 0.0 1.0),这对应于蓝色。 预测的输出为 (0.3 0.6 0.1)。 最大的分量是 0.6,因此,模型预测是 (0 1 0),这对应于绿色。 该模型生成了错误的预测。

总结

使用神经网络进行分类是一个很有吸引力的主题,它不但重要,而且复杂。 本文中展示的示例应该为您试用神经网络分类打下牢固基础。 但是,本文仅介绍了一个非常具体的神经网络分类情形(具有分类输出变量的数字输入变量)并且只是一个起点。 其他情形会要求稍有不同的技术。 例如,如果输入数据包含某一分类变量,则您可能会希望使用 1-of-N 编码对其进行编码,就像对待分类输出变量一样。 但在此情况下,应该使用 1-of-(N-1) 技术对输入数据进行编码。 如果您想要了解使用神经网络进行分类的详细信息,我建议您看一下在 faqs.org 上提供的有关神经网络的系列,这个系列包含七个常见问题。 指向这些常见问题的链接往往会移动位置,但您应该能够使用 Internet 搜索轻松地找到它们。

James McCaffrey博士 供职于 Volt Information Sciences, Inc.,在该公司他负责管理对华盛顿州雷蒙德市沃什湾 Microsoft 总部园区的软件工程师进行的技术培训。 他参与过多项 Microsoft 产品的研发工作,其中包括 Internet Explorer 和 MSN Search。 McCaffrey 是《.NET 软件测试自动化之道》(Apress,2006)的作者。 可通过 jmccaffrey@volt.comjammc@microsoft.com 与他联系。

衷心感谢以下 Microsoft 技术专家对本文的审阅: Matthew Richardson