1、MNIST简介
MNIST是一组带标签的图像集合,专门为监督学习组装而成,是改良的NIST。官网有这样的一段描述性话语:“ It is a subset of a larger set available from NIST. The digits have been size-normalized and centered in a fixed-size image.” MNIST包含了若干手写的数字图像,其数值也是该图像的标签。每个图像的像素是28px*28px的灰度像素。
该数据集目前有 60,000 个示例的训练集和10,000 个示例的测试集,按照从0到9的顺序被整齐地划分为7000组样本,数字是从实际问题中采集的真实手写数字,所以它们具有多样性。MNIST将图像和标签存储在单独的文件中,我们可以编写简单的程序读取它们。
2、训练和测试
我们通过带标签的样本数据训练模型,使用样本计算出的预测值与实际结果进行比较。如果两组数值越接近,则说明预测的效果就越好,但是这种方法可能会存在一些问题,比如最大的问题可能是过拟合(overfitting),其意思就是模型的训练误差远小于它在测试数据集上的误差,当然这个问题,可以采用一些方法去减少其影响。
注意:千万不能使用训练系统时使用过的样本数据来测试系统,这样得到的结果是不理想的。
我们可以使用两个矩阵X,Y去分别表示样本数据和标签值,这也是二元分类的思想。
X中的每行元素代表一个样本数据,每列元素代表一个输入变量,其中有一列是偏置列。为了使MNIST中的图像数据符合这种格式,必须对其进行格式化为一行像素,这样每个像素就成为一个输入变量。MNIST图像有28*28个像素,因此会产生784个元素的行,再加上偏置列,每个图像样本的数据是一个包含785个元素的行。所以,X就是60,000行和785列组成的矩阵。记住,每一行是一幅图像。
对于矩阵Y 依据二元分类器的思想,它应该只包含0和1,而我们的数据集里的标签则是从0-9的,那么,我们采用缩小范围的方式,以某一个数字为界限,比如中间的数字5,所以我们将0-9的标签分成两类——“是5”和“不是5”。
2、加载图像
我们使用python编写代码,如下:
import numpy as np
import struct
import gzip
# 加载图像
def load_images(filename):
# 打开并解压文件
with gzip.open(filename, 'rb') as f:
# 定义变量存储文件里的标题信息,struct.unpack()函数是根据模式字符串从二进制文件中读取数据
_ignored, n_images, columns, rows = struct.unpack('>IIII', f.read(16))
# 往Numpy的字节数组中读取所有的像素
all_pixels = np.frombuffer(f.read(), dtype=np.uint8)
# 将像素重塑为一个矩阵,其每一行都是一个图像,并返回
return all_pixels.reshape(n_images, columns * rows)
def prepend_bias(X):
# 追加一个偏置列,且元素均是1
return np.insert(X, 0, 1, axis=1)
# 将训练图像样本的60000*785矩阵保存到变量中,并初始化原始数据
X_train = prepend_bias(load_images("../data/mnist/train-images-idx3-ubyte.gz"))
# 存储测试图像样本
X_test = prepend_bias(load_images("../data/mnist/t10k-images-idx3-ubyte.gz"))
3、加载标签
我们使用python编写代码,与加载图像的代码放在一起,命名为mnist.py,如下:
# 加载标签
def load_labels(filename):
with gzip.open(filename, 'rb') as f:
# 跳过标题字节
f.read(8)
# 将所有的标签放入一个列表
all_labels = f.read()
# 将标签列表重塑为一列的矩阵
return np.frombuffer(all_labels, dtype=np.uint8).reshape(-1, 1)
def encode_fives(Y):
# 创建一个数组,Y包含5时为真,不包含5时为假,并将该数组转换为整数数据,所有为真的值就变成了1,为假则是0
return (Y == 5).astype(int)
# 训练标签,如果数字是5则为1,否则0
Y_train = encode_fives(load_labels("../data/mnist/train-labels-idx1-ubyte.gz"))
# 测试标签,如果数字是5则为1,否则0
Y_test = encode_fives(load_labels("../data/mnist/t10k-labels-idx1-ubyte.gz"))
4、模型训练与测试
采用分类算法进行训练模型。
import numpy as np
import mnist as mi
# S型函数,平滑误差
def sigmoid(z):
return 1 / (1 + np.exp(-z))
def forward(X, w):
weighted_sum = np.matmul(X, w)
return sigmoid(weighted_sum)
def classify(X, w):
return np.round(forward(X, w))
# log损失函数
def loss(X, Y, w):
y_hat = forward(X, w)
first_term = Y * np.log(y_hat)
second_term = (1 - Y) * np.log(1 - y_hat)
return -np.average(first_term + second_term)
# 梯度下降法
def gradient(X, Y, w):
return np.matmul(X.T, (forward(X, w) - Y)) / X.shape[0]
# iterations为迭代次数
def train(X, Y, iterations, lr):
w = np.zeros((X.shape[1], 1)) # 初始化权重w
for i in range(iterations):
print("Iteration %4d => Loss: %.20f" % (i, loss(X, Y, w)))
w -= gradient(X, Y, w) * lr
return w
def test(X, Y, w):
total_examples = X.shape[0]
correct_results = np.sum(classify(X, w) == Y)
success_percent = correct_results * 100 / total_examples
print("\nSuccess: %d/%d (%.2f%%)" %
(correct_results, total_examples, success_percent))
# 训练模型
w = train(mi.X_train, mi.Y_train, iterations=100, lr=1e-5)
# 模型测试
test(mi.X_test, mi.Y_test, w)
iterations和lr(学习率) 也是此次模型中的超参数(也就是认为设定的值),进行反复的调整,找到一个最优的参数,调参也是机器学习的基本工作之一。
现在,我们将程序运行一下,看看模型预测的效果如何!
我们发现,迭代了99次,有超过96%的样本预测被证明是准确的,有一个直观上的感觉,但这个预测系统是否就一定准确的,答案是否定的。不过这也是一种不错的处理方法!
注意:我们的程序是只识别数字5的。
参考文献:
Programming Machine Learning: Form Coding to Deep Learning.[M],Paolo Perrotta,2021.6.