抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

《人工智能》课程

实验报告

网络与信息安全学院

级: 20180XX

名: XXX

号: 20009XXXX

提交时间: 2023. 4. 20

基于神经网络的MNIST手写数字识别

一、实验目的

  • 掌握运用神经网络模型解决有监督学习问题

  • 掌握机器学习中常用的模型训练测试方法

  • 了解不同训练方法的选择对测试结果的影响

二、实验内容

MNIST数据集

​ 本实验采用的数据集MNIST是一个手写数字图片数据集,共包含图像和对应的标签。数据集中所有图片都是28x28像素大小,且所有的图像都经过了适当的处理使得数字位于图片的中心位置。MNIST数据集使用二进制方式存储。图片数据中每个图片为一个长度为784(28x28x1,即长宽28像素的单通道灰度图)的一维向量,而标签数据中每个标签均为长度为10的一维向量。

分层采样方法

​ 分层采样(或分层抽样,也叫类型抽样)方法,是将总体样本分成多个类别,再分别在每个类别中进行采样的方法。通过划分类别,采样出的样本的类型分布和总体样本相似,并且更具有代表性。在本实验中,MNIST数据集为手写数字集,有0~9共10种数字,进行分层采样时先将数据集按数字分为10类,再按同样的方式分别进行采样。

神经网络模型评估方法

​ 通常,我们可以通过实验测试来对神经网络模型的误差进行评估。为此,需要使用一个测试集来测试模型对新样本的判别能力,然后以此测试集上的测试误差作为误差的近似值。两种常见的划分训练集和测试集的方法:

​ 留出法(hold-out)直接将数据集按比例划分为两个互斥的集合。划分时为尽可能保持数据分布的一致性,可以采用分层采样(stratified sampling)的方式,使得训练集和测试集中的类别比例尽可能相似。需要注意的是,测试集在整个数据集上的分布如果不够均匀还可能引入额外的偏差,所以单次使用留出法得到的估计结果往往不够稳定可靠。在使用留出法时,一般要采用若干次随机划分、重复进行实验评估后取平均值作为留出法的评估结果。

​ k折交叉验证法(k-fold cross validation)先将数据集划分为k个大小相似的互斥子集,每个子集都尽可能保持数据分布的一致性,即也采用分层采样(stratified sampling)的方法。然后,每次用k-1个子集的并集作为训练集,余下的那个子集作为测试集,这样就可以获得k组训练集和测试集,从而可以进行k次训练和测试。最终返回的是这k个测试结果的均值。显然,k折交叉验证法的评估结果的稳定性和保真性在很大程度上取决于k的取值。k最常用的取值是10,此外常用的取值还有5、20等。

三、实验方法设计

实验环境

1.VSCODE

2.anaconda==4.14.0

3.python==3.7

4.TensorFlow–gpu==1.15.0

5.Keras==2.3.1

6.实验报告编辑器:typora

介绍实验中程序的总体设计方案、关键步骤的编程方法及思路,主要包括:

因为之前用过pytorch进行机器学习的训练和学习,所以本作业使用pytorch进行建模和训练。

标准训练流程如下:导入包->设定初始值->加载数据集(预处理)->建立模型->训练->测试->评估

其中需要对加载数据集进行处理,把留出法的比例进行调整来观察结果。

其次要使用k折交叉验证法进行对比测试。

为了表明k折交叉验证法与留出法的效果对比,我建立了连个模型,一个是按照标准流程建立的优秀的模型。一个是用作对比k折交叉验证法与留出法效果对比的劣质模型。

含有tensorflow部分代码,在四、4中。

  • 设置初始值
1
2
3
4
5
6
>mean = [0.5]
>std = [0.5]
># batch size
>BATCH_SIZE =128
>Iterations = 1 # epoch
>learning_rate = 0.01
  • 优化器与损失函数
1
2
>criterion = torch.nn.CrossEntropyLoss() 
>optimizer = torch.optim.SGD(model.parameters(),learning_rate)
  • 训练代码
1
2
3
4
5
6
7
8
9
10
11
12
13
>def train(model, optimizer,criterion,epoch): 
model.train() # setting up for training
for batch_idx, (data, target) in enumerate(train_loader): # data contains the image and target contains the label = 0/1/2/3/4/5/6/7/8/9
data = data.view(-1, 28*28).requires_grad_()
optimizer.zero_grad() # setting gradient to zero
output = model(data) # forward
loss = criterion(output, target) # loss computation
loss.backward() # back propagation here pytorch will take care of it
optimizer.step() # updating the weight values
if batch_idx % 100 == 0:
print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
epoch, batch_idx * len(data), len(train_loader.dataset),
100. * batch_idx / len(train_loader), loss.item()))
  • 测试代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
>def test(model, criterion, val_loader, epoch,train= False):    
model.eval()
test_loss = 0
correct = 0

with torch.no_grad():
for batch_idx, (data, target) in enumerate(val_loader):
data = data.view(-1, 28*28).requires_grad_()
output = model(data)
test_loss += criterion(output, target).item() # sum up batch loss
pred = output.max(1, keepdim=True)[1] # get the index of the max log-probability
correct += pred.eq(target.view_as(pred)).sum().item() # if pred == target then correct +=1

test_loss /= len(val_loader.dataset) # average test loss
if train == False:
print('\nTest set: Average loss: {:.4f}, Accuracy: {}/{} ({:.4f}%)\n'.format(
test_loss, correct, val_loader.sampler.__len__(),
100. * correct / val_loader.sampler.__len__() ))
if train == True:
print('\nTrain set: Average loss: {:.4f}, Accuracy: {}/{} ({:.4f}%)\n'.format(
test_loss, correct, val_loader.sampler.__len__(),
100. * correct / val_loader.sampler.__len__() ))
return 100. * correct / val_loader.sampler.__len__()

1)模型构建的程序设计(伪代码或源代码截图)及说明解释 (10分)

训练模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.conv = nn.Conv2d(1, 32, 3)
self.dropout = nn.Dropout2d(0.25)
self.fc = nn.Linear(5408, 10)

def forward(self, x):
x = self.conv(x)
x = F.relu(x)
x = F.max_pool2d(x, 2)
x = self.dropout(x)
x = torch.flatten(x, 1)
x = self.fc(x)
output = F.log_softmax(x, dim=1)
return output

我用了一层conv和一层pool来获取cherng图片的特征。之后把这些特征减小为10个层,所以用flatten把特征集中成vector后,再用一个全连接层连接到输出层。

使用留出法原始的训练比例,两个Epoch,得到的结果很好。达到97%。

image-204353234629104253861

为了对比留出法及K折验证法建立的简陋模型

1
model = nn.Sequential(nn.Flatten(), nn.Linear(784, 256), nn.ReLU(),nn.Linear(256, 10))

可以看到这个简陋模型得到的结果很差,准确率只有83%,用于之后的K折校验法的对比组。预处理和上述相同,Epoch只有一组。

6f0c721b1033b4c3223405a077779932787c

2)模型迭代训练的程序设计(伪代码或源代码截图)及说明解释 (10分)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def train(model, optimizer,criterion,epoch): 
model.train() # setting up for training
for batch_idx, (data, target) in enumerate(train_loader):
data = data.view(-1, 28*28).requires_grad_()
optimizer.zero_grad() # setting gradient to zero
output = model(data) # forward
loss = criterion(output, target) # loss computation
loss.backward() # back propagation here pytorch will take care of it
optimizer.step() # updating the weight values
if batch_idx % 100 == 0:
print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
epoch, batch_idx * len(data), len(train_loader.dataset),
100. * batch_idx / len(train_loader), loss.item()))


先注意到因為 training 和 testing 時 model 會有不同行為,所以用 model.train() 把 model 調成 training 模式。

接著 iterate 過 batch_idx,每個 batch_idx會 train 過整個 training set。每個 dataset 會做 batch training。

接下來就是重點了。基本的步驟:zero_grad、model(data)、取 loss、back propagation 算 gradient、最後 update parameter。前面都介紹過了,還不熟的可以往前翻。

3)模型训练过程中周期性测试的程序设计(伪代码或源代码截图)及说明解释(周期性测试指的是每训练n个step就对模型进行一次测试,得到准确率和loss值)(10分)

我选用了每100步进行一个打印的频率,打印训练进度和Loss值,最后打印平均损失值和准确率。

image-20230504133405002

4)分层采样的程序设计(伪代码或源代码截图)及说明解释 (10分)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
train_transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize(mean, std)
])

test_transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize(mean, std)
])

train_loader = torch.utils.data.DataLoader(
datasets.MNIST('./mnist', train=True, download=True,
transform=train_transform),
batch_size=BATCH_SIZE, shuffle=True) # train dataset

test_loader = torch.utils.data.DataLoader(
datasets.MNIST('./mnist', train=False,
transform=test_transform),
batch_size=BATCH_SIZE, shuffle=False) # test dataset
1
2
3
4
5
6
7
8
9
>def hold_out(images, labels, train_percentage):
test_acc = torch.zeros([Iterations])
train_acc = torch.zeros([Iterations])
## training the logistic model
for i in range(Iterations):
train(model, optimizer,criterion,i)
train_acc[i] = test(model, criterion, train_loader, i,train=True) #Testing the the current CNN
test_acc[i] = test(model, criterion, test_loader, i)
torch.save(model,'perceptron.pt')

使用了系统自带的minist数据分类器。

5)k折交叉验证法的程序设计(伪代码或源代码截图)及说明解释 (10分)

  • mnist数据集的训练集和测试集的合并
1
2
3
4
5
6
7
8
9

train_init = datasets.MNIST('./mnist', train=True,
transform=train_transform)

test_init = datasets.MNIST('./mnist', train=False,
transform=test_transform)

# the dataset for k fold cross validation
dataFold = torch.utils.data.ConcatDataset([train_init, test_init])
  • 使用Sklearn中的KFold进行数据集划分,并且转换回pytorch类型的Dataloader
1
2
3
4
5
6
7
8
9
kf = KFold(n_splits=k_split_value,shuffle=True, random_state=0)  # init KFold
for train_index , test_index in kf.split(dataFold): # split
# get train, val 根据索引划分
train_fold = torch.utils.data.dataset.Subset(dataFold, train_index)
test_fold = torch.utils.data.dataset.Subset(dataFold, test_index)

train_loader = torch.utils.data.DataLoader(dataset=train_fold, batch_size=BATCH_SIZE, shuffle=True)
test_loader = torch.utils.data.DataLoader(dataset=test_fold, batch_size=BATCH_SIZE, shuffle=True)

  • 完整的代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

def train_flod_Mnist(k_split_value):
different_k_mse = []
kf = KFold(n_splits=k_split_value,shuffle=True, random_state=0) # init KFold
for train_index , test_index in kf.split(dataFold): # split
# get train, val
train_fold = torch.utils.data.dataset.Subset(dataFold, train_index)
test_fold = torch.utils.data.dataset.Subset(dataFold, test_index)

# package type of DataLoader
train_loader = torch.utils.data.DataLoader(dataset=train_fold, batch_size=BATCH_SIZE, shuffle=True)
test_loader = torch.utils.data.DataLoader(dataset=test_fold, batch_size=BATCH_SIZE, shuffle=True)
# train model
test_acc = torch.zeros([Iterations])
train_acc = torch.zeros([Iterations])

## training the logistic model
for i in range(Iterations):
train(model, optimizer,criterion,i)
train_acc[i] = test(model, criterion, train_loader, i,train=True) #Testing the the current CNN
test_acc[i] = test(model, criterion, test_loader, i)
#torch.save(model,'perceptron.pt')
# one epoch, all acc
different_k_mse.append(np.array(test_acc))
return different_k_mse

  • 按循序打印结果
1
2
3
4
5
6
testAcc_compare_map = {}
for k_split_value in range(2, 10+1):
print('now k_split_value is:', k_split_value)
testAcc_compare_map[k_split_value] = train_flod_Mnist(k_split_value)
for key in testAcc_compare_map:
print(np.mean(testAcc_compare_map[key]))

testAcc_compare_map是将不同k值下训练的结果保存起来,之后我们可以通过这个字典变量,计算出rmse ,比较不同k值下,实验结果的鲁棒性。

四、实验结果展示

展示程序界面设计、运行结果及相关分析等,主要包括:

1)模型在验证集下的准确率(输出结果并截图)(10分)

下面的实验是k值为[2,10]下的结果,训练模型为简陋模型。

  • 对照组:简陋模型,epoch为1,分层抽样(正确率只有83.79%)6f0c721b1033b4c05a077779932787c

  • k折校验:简陋模型,epoch为1,K折交叉验证(K值为2到10)准确率越来越大

b9d3aa67dbc5d072329072416f16959

2)不同模型参数(隐藏层数、隐藏层节点数)对准确率的影响和分析 (10分)

本次实验中只探讨了简陋版模型与卷积模型的对比:

其中简陋版模型如下,先把图片变为一个以为张量,然后由一个全连接层链接,接入到ReLu层中,然后接入全连接层,可以看到,并没有使用卷积层,在epoch=1的情况下只有83%的准确率。

1
>model = nn.Sequential(nn.Flatten(), nn.Linear(784, 256), nn.ReLU(),nn.Linear(256, 10))

卷积模型如下,先定义卷积卷积层,输入通道为1,输出为32,核大小为3,一个Dropout2d层,以0.25的概率将通道输入置零,防止过拟合。然后是一个全连接层,输入为5408,输出为10,映射到10个分类结果。在forward中首先通过卷积层进行卷积,然后通过ReLU进行非线性变换,然后使用最大池化层进行采样,将图签尺寸缩小一半,然后用Dropout2d防止过拟合,接着把输出的张良展平为一维,并传入全连接层。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.conv = nn.Conv2d(1, 32, 3)
self.dropout = nn.Dropout2d(0.25)
self.fc = nn.Linear(5408, 10)

def forward(self, x):
x = self.conv(x)
x = F.relu(x)
x = F.max_pool2d(x, 2)
x = self.dropout(x)
x = torch.flatten(x, 1)
x = self.fc(x)
output = F.log_softmax(x, dim=1)
return output

可以看到在epoch=1的情况下准确率达到97%,拟合效果非常好

image-20230504152837721

3)不同训练参数(batch size、epoch num、学习率)对准确率的影响和分析 (10分)

  • 注:默认值:在讨论某一变化时,其他值不变
1
2
3
>BATCH_SIZE =64
>Iterations = 1 # epoch
>learning_rate = 0.01

原始结果(83%)

image-20230504160123451

  • BATCH_SIZE讨论(可以发现当BATCH_SIZE越大时,准确率直线下降)
1
>{"16":"90.85%","32":"90.1000%","64":"88.2200%","128":"83.7100%","256":"70.7300%","512":"48.2400%"}

image-20230504155956629

  • epoch讨论(可以发现随着epoch的增大,准确率有较大提升,但是随着epoch越来越大,准确率增长越来越慢)
1
2
[1,2,4,6,10,15]
[83.83,88.370,90.290,91.260,92.420,93.36]

image-20230504161433566学习率

可以看到,当学习率增大时,准确率有所增加,但是当学习率大于0.2时,准确率急速下滑到11%左右,也就是说,10个手写体正确率只有1个,趋于随机分布,是一个非常不好的模型,可见,学习率的选择至关重要。

1
2
>[0.01,0.02,0.05,0.1,0.2,0.5,1]
>[83.83,88.230,90.78,91.79,91.78,11.35,11.35]

image-20230504162010713

4)留出法不同比例对结果的影响和分析 (10分)

因为pytorch中的训练集是固定输出的,对其更改较难,所以本小节使用TensorFlow进行实验:

数据集划分:其中a为训练比率,总共有70000个样本,按照比例进行训练和测试,结果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
>np.random.seed(10)

>a = 0.8
>from keras.datasets import mnist
>(x_train_image,y_train_label),(x_test_image,y_test_label)=mnist.load_data()
># x_all = x_train_image + x_test_image

>temp = np.append( x_train_image , x_test_image)
>x_all = temp.reshape(70000,28,28)
>print(len(y_train_label))

>y_lable = np.append(y_train_label,y_test_label)

>train_num = int(60000*a)

>x_train_image = x_all[:train_num]
>y_train_label = y_lable[:train_num]

>x_test_image = x_all[train_num:]
>y_test_label = y_lable[train_num:]

>x_Train=x_train_image.reshape(train_num,784).astype('float32')
>x_Test=x_test_image.reshape(70000-train_num,784).astype('float32')

image-20230504201332595

1
2
>[0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 0.95, 0.9999]
>[0.9213, 0.9381, 0.9528, 0.9620, 0.9666, 0.9673, 0.9709, 0.9752, 0.9758, 0.9784, 0.9780]

可以看到随着训练样本的比率上升,总体的准确率也对应的明显的上升了,但是最后一组0.9999比率的组,较前一组0.95有所下降,这表明过大的训练比率对结果也会产生损害。

5)k折交叉验证法不同k值对结果的影响和分析 (10分)

把k值从2-10进行迭代计算,其他参数不变,结果为:

1
2
3
4
5
6
7
8
>testAcc_compare_map = {}
>for k_split_value in range(2, 10+1):
print('now k_split_value is:', k_split_value)
testAcc_compare_map[k_split_value] = cross_validation(k_split_value)


>for key in testAcc_compare_map:
print(np.mean(testAcc_compare_map[key]))

image-20230504162750415

可见K值对结果影响很大,且在一定范围内,越大越好。

五、实验总结及心得

本次实验熟知了pytorch和TensorFlow的使用,还有机器学习的整体流程和处理概况,解决了出现的诸多问题,尤其是在配置TensorFlow版本时出现的问题,熟知了基本的图像处理模型,以及卷积模型的基本构建。

在参数调配方面,详细了解了分层取样法,k折交叉验证法的使用以及效果还有比例的调试。还有在关键参数如batch size、epoch num、学习率方面有着较好的经验总结。

评论