代码
https://github.com/s055523/MNISTTensorFlowSharp
数据的获得
数据可以由http://yann.lecun.com/exdb/mnist/下载。之后,储存在trainDir中,下次就不需要下载了。
/// <summary>
/// 如果文件不存在就去下载
/// </summary>
/// <param name="urlBase">下载地址</param>
/// <param name="trainDir">文件目录地址</param>
/// <param name="file">文件名</param>
/// <returns></returns>
public static Stream MaybeDownload(string urlBase, string trainDir, string file)
{
if (!Directory.Exists(trainDir))
{
Directory.CreateDirectory(trainDir);
} var target = Path.Combine(trainDir, file);
if (!File.Exists(target))
{
var wc = new WebClient();
wc.DownloadFile(urlBase + file, target);
}
return File.OpenRead(target);
}
数据格式处理
下载下来的文件共有四个,都是扩展名为gz的压缩包。
train-images-idx3-ubyte.gz 55000张训练图片和5000张验证图片
train-labels-idx1-ubyte.gz 训练图片对应的数字标签(即答案)
t10k-images-idx3-ubyte.gz 10000张测试图片
t10k-labels-idx1-ubyte.gz 测试图片对应的数字标签(即答案)
处理图片数据压缩包
每个压缩包的格式为:
偏移量 |
类型 |
值 |
意义 |
0 |
Int32 |
2051或2049 |
一个定死的魔术数。用来验证该压缩包是训练集(2051)或测试集(2049) |
4 |
Int32 |
60000或10000 |
压缩包的图片数 |
8 |
Int32 |
28 |
每个图片的行数 |
12 |
Int32 |
28 |
每个图片的列数 |
16 |
Unsigned byte |
0 - 255 |
第一张图片的第一个像素 |
17 |
Unsigned byte |
0 - 255 |
第一张图片的第二个像素 |
… |
… |
… |
… |
因此,我们可以使用一个统一的方式将数据处理。我们只需要那些图片像素。
/// <summary>
/// 从数据流中读取下一个int32
/// </summary>
/// <param name="s"></param>
/// <returns></returns>
int Read32(Stream s)
{
var x = new byte[];
s.Read(x, , );
return DataConverter.BigEndian.GetInt32(x, );
} /// <summary>
/// 处理图片数据
/// </summary>
/// <param name="input"></param>
/// <param name="file"></param>
/// <returns></returns>
MnistImage[] ExtractImages(Stream input, string file)
{
//文件是gz格式的
using (var gz = new GZipStream(input, CompressionMode.Decompress))
{
//不是2051说明下载的文件不对
if (Read32(gz) != )
{
throw new Exception("不是2051说明下载的文件不对: " + file);
}
//图片数
var count = Read32(gz);
//行数
var rows = Read32(gz);
//列数
var cols = Read32(gz); Console.WriteLine($"准备读取{count}张图片。"); var result = new MnistImage[count];
for (int i = ; i < count; i++)
{
//图片的大小(每个像素占一个bit)
var size = rows * cols;
var data = new byte[size]; //从数据流中读取这么大的一块内容
gz.Read(data, , size); //将读取到的内容转换为MnistImage类型
result[i] = new MnistImage(cols, rows, data);
}
return result;
}
}
准备一个MnistImage类型:
/// <summary>
/// 图片类型
/// </summary>
public struct MnistImage
{
public int Cols, Rows;
public byte[] Data;
public float[] DataFloat; public MnistImage(int cols, int rows, byte[] data)
{
Cols = cols;
Rows = rows;
Data = data;
DataFloat = new float[data.Length];
for (int i = ; i < data.Length; i++)
{
//数据归一化(这里将0-255除255变成了0-1之间的小数)
//也可以归一为-0.5到0.5之间
DataFloat[i] = Data[i] / 255f;
}
}
}
这样一来,图片数据就处理完成了。
处理数字标签数据压缩包
数字标签数据压缩包和图片数据压缩包的格式类似。
偏移量 |
类型 |
值 |
意义 |
0 |
Int32 |
2051或2049 |
一个定死的魔术数。用来验证该压缩包是训练集(2051)或测试集(2049) |
4 |
Int32 |
60000或10000 |
压缩包的数字标签数 |
5 |
Unsigned byte |
0 - 9 |
第一张图片对应的数字 |
6 |
Unsigned byte |
0 - 9 |
第二张图片对应的数字 |
… |
… |
… |
… |
它的处理更加简单。
/// <summary>
/// 处理标签数据
/// </summary>
/// <param name="input"></param>
/// <param name="file"></param>
/// <returns></returns>
byte[] ExtractLabels(Stream input, string file)
{
using (var gz = new GZipStream(input, CompressionMode.Decompress))
{
//不是2049说明下载的文件不对
if (Read32(gz) != )
{
throw new Exception("不是2049说明下载的文件不对:" + file);
}
var count = Read32(gz);
var labels = new byte[count]; gz.Read(labels, , count); return labels;
}
}
将数字标签转化为二维数组:one-hot编码
由于我们的数字为0-9,所以,可以视为有十个class。此时,为了后续的处理方便,我们将数字标签转化为数组。因此,一组标签就转换为了一个二维数组。
例如,标签0变成[1,0,0,0,0,0,0,0,0,0]
标签1变成[0,1,0,0,0,0,0,0,0,0]
以此类推。
/// <summary>
/// 将数字标签一维数组转为一个二维数组
/// </summary>
/// <param name="labels"></param>
/// <param name="numClasses">多少个类别,这里是10(0到9)</param>
/// <returns></returns>
byte[,] OneHot(byte[] labels, int numClasses)
{
var oneHot = new byte[labels.Length, numClasses];
for (int i = ; i < labels.Length; i++)
{
oneHot[i, labels[i]] = ;
}
return oneHot;
}
到此为止,数据格式处理就全部结束了。下面的代码展示了数据处理的全过程。
/// <summary>
/// 处理数据集
/// </summary>
/// <param name="trainDir">数据集所在文件夹</param>
/// <param name="numClasses"></param>
/// <param name="validationSize">拿出多少做验证?</param>
public void ReadDataSets(string trainDir, int numClasses = , int validationSize = )
{
const string SourceUrl = "http://yann.lecun.com/exdb/mnist/";
const string TrainImagesName = "train-images-idx3-ubyte.gz";
const string TrainLabelsName = "train-labels-idx1-ubyte.gz";
const string TestImagesName = "t10k-images-idx3-ubyte.gz";
const string TestLabelsName = "t10k-labels-idx1-ubyte.gz"; //获得训练数据,然后处理训练数据和测试数据
TrainImages = ExtractImages(Helper.MaybeDownload(SourceUrl, trainDir, TrainImagesName), TrainImagesName);
TestImages = ExtractImages(Helper.MaybeDownload(SourceUrl, trainDir, TestImagesName), TestImagesName);
TrainLabels = ExtractLabels(Helper.MaybeDownload(SourceUrl, trainDir, TrainLabelsName), TrainLabelsName);
TestLabels = ExtractLabels(Helper.MaybeDownload(SourceUrl, trainDir, TestLabelsName), TestLabelsName); //拿出前面的一部分做验证
ValidationImages = Pick(TrainImages, , validationSize);
ValidationLabels = Pick(TrainLabels, , validationSize); //拿出剩下的做训练(输入0意味着拿剩下所有的)
TrainImages = Pick(TrainImages, validationSize, );
TrainLabels = Pick(TrainLabels, validationSize, ); //将数字标签转换为二维数组
//例如,标签3 =》 [0,0,0,1,0,0,0,0,0,0]
//标签0 =》 [1,0,0,0,0,0,0,0,0,0]
if (numClasses != -)
{
OneHotTrainLabels = OneHot(TrainLabels, numClasses);
OneHotValidationLabels = OneHot(ValidationLabels, numClasses);
OneHotTestLabels = OneHot(TestLabels, numClasses);
}
} /// <summary>
/// 获得source集合中的一部分,从first开始,到last结束
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="source"></param>
/// <param name="first"></param>
/// <param name="last"></param>
/// <returns></returns>
T[] Pick<T>(T[] source, int first, int last)
{
if (last == )
{
last = source.Length;
} var count = last - first;
var ret = source.Skip(first).Take(count).ToArray();
return ret;
} public static Mnist Load()
{
var x = new Mnist();
x.ReadDataSets(@"D:\人工智能\C#代码\MNISTTensorFlowSharp\MNISTTensorFlowSharp\data");
return x;
}
在这里,数据共有下面几部分:
- 训练图片数据55000 TrainImages及对应标签TrainLabels
- 验证图片数据5000 ValidationImages及对应标签ValidationLabels
- 测试图片数据10000 TestImages及对应标签TestLabels
KNN算法的实现
现在,我们已经有了所有的数据在手。需要实现的是:
- 拿出数据中的一部分(例如,5000张图片)作为KNN的训练数据,然后,再从数据中的另一部分拿一张图片A
- 对这张图片A,求它和5000张训练图片的距离,并找出一张训练图片B,它是所有训练图片中,和A距离最小的那张(这意味着K=1)
- 此时,就认为A所代表的数字等同于B所代表的数字b
- 重复1-3,N次
首先进行数据的收集:
//三个Reader分别从总的数据库中获得数据
public BatchReader GetTrainReader() => new BatchReader(TrainImages, TrainLabels, OneHotTrainLabels);
public BatchReader GetTestReader() => new BatchReader(TestImages, TestLabels, OneHotTestLabels);
public BatchReader GetValidationReader() => new BatchReader(ValidationImages, ValidationLabels, OneHotValidationLabels); /// <summary>
/// 数据的一部分,包括了所有的有用信息
/// </summary>
public class BatchReader
{
int start = ;
//图片库
MnistImage[] source;
//数字标签
byte[] labels;
//oneHot之后的数字标签
byte[,] oneHotLabels; internal BatchReader(MnistImage[] source, byte[] labels, byte[,] oneHotLabels)
{
this.source = source;
this.labels = labels;
this.oneHotLabels = oneHotLabels;
} /// <summary>
/// 返回两个浮点二维数组(C# 7的新语法)
/// </summary>
/// <param name="batchSize"></param>
/// <returns></returns>
public (float[,], float[,]) NextBatch(int batchSize)
{
//一张图
var imageData = new float[batchSize, ];
//标签
var labelData = new float[batchSize, ]; int p = ;
for (int item = ; item < batchSize; item++)
{
Buffer.BlockCopy(source[start + item].DataFloat, , imageData, p, * sizeof(float));
p += * sizeof(float);
for (var j = ; j < ; j++)
labelData[item, j] = oneHotLabels[item + start, j];
} start += batchSize;
return (imageData, labelData);
}
}
然后,在算法中,获取数据:
static void KNN()
{
//取得数据
var mnist = Mnist.Load(); //拿5000个训练数据,200个测试数据
const int trainCount = ;
const int testCount = ; //获得的数据有两个
//一个是图片,它们都是28*28的
//一个是one-hot的标签,它们都是1*10的
(var trainingImages, var trainingLabels) = mnist.GetTrainReader().NextBatch(trainCount);
(var testImages, var testLabels) = mnist.GetTestReader().NextBatch(testCount); Console.WriteLine($"MNIST 1NN");
下面进行计算。这里使用了K=1的L1距离。这是最简单的情况。
//建立一个图表示计算任务
using (var graph = new TFGraph())
{
var session = new TFSession(graph); //用来feed数据的占位符。trainingInput表示N张用来进行训练的图片,N是一个变量,所以这里使用-1
TFOutput trainingInput = graph.Placeholder(TFDataType.Float, new TFShape(-, )); //xte表示一张用来测试的图片
TFOutput xte = graph.Placeholder(TFDataType.Float, new TFShape()); //计算这两张图片的L1距离。这很简单,实际上就是把784个数字逐对相减,然后取绝对值,最后加起来变成一个总和
var distance = graph.ReduceSum(graph.Abs(graph.Sub(trainingInput, xte)), axis: graph.Const()); //这里只是用了最近的那个数据
//也就是说,最近的那个数据是什么,那pred(预测值)就是什么
TFOutput pred = graph.ArgMin(distance, graph.Const());
最后是开启Session计算的过程:
var accuracy = 0f; //开始循环进行计算,循环trainCount次
for (int i = ; i < testCount; i++)
{
var runner = session.GetRunner(); //每次,对一张新的测试图,计算它和trainCount张训练图的距离,并获得最近的那张
var result = runner.Fetch(pred).Fetch(distance)
//trainCount张训练图(数据是trainingImages)
.AddInput(trainingInput, trainingImages)
//testCount张测试图(数据是从testImages中拿出来的)
.AddInput(xte, Extract(testImages, i))
.Run(); //最近的点的序号
var nn_index = (int)(long)result[].GetValue(); //从trainingLabels中找到答案(这是预测值)
var prediction = ArgMax(trainingLabels, nn_index); //正确答案位于testLabels[i]中
var real = ArgMax(testLabels, i); //PrintImage(testImages, i); Console.WriteLine($"测试 {i}: " +
$"预测: {prediction} " +
$"正确答案: {real} (最近的点的序号={nn_index})");
//Console.WriteLine(testImages); if (prediction == real)
{
accuracy += 1f / testCount;
}
}
Console.WriteLine("准确率: " + accuracy);
对KNN的改进
本文只是对KNN识别MNIST数据集进行了一个非常简单的介绍。在实现了最简单的K=1的L1距离计算之后,正确率约为91%。大家可以试着将算法进行改进,例如取K=2或者其他数,或者计算L2距离等。L2距离的结果比L1好一些,可以达到93-94%的正确率。