在本系列的上一篇文章中分享了卷积神经网络的基础知识,在完全了解1+1=2
的基础知识后,本篇文章可以进行2*2=4
进阶的学习。将会通过分析一个实例来进行讲解。
简单的图片识别卷积神经网络
数据集
我们来建立一个简单的图片识别卷积神经网络,使用MNIST数据集进行训练。
MNIST(修改后的美国国家标准与技术研究院)数据集是一个大型手写数字数据库,通常用于训练各种图像处理系统和机器学习模型。该数据集中的图片内容大致如下所示:

下载下来的数据集如下所示:
1 2
| $ ls train-00000-of-00001.parquet test-00000-of-00001.parquet
|
加载数据集:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import numpy as np from datasets import load_dataset
train_datasets = load_dataset("parquet", data_files="train-00000-of-00001.parquet") test_datasets = load_dataset("parquet", data_files="test-00000-of-00001.parquet")
train_datasets = train_datasets["train"] test_datasets = test_datasets["train"]
print(train_datasets[0]) image = train_datasets[0]["image"] print(image.size)
image_np = np.array(image) print(image_np.shape)
{'image': <PIL.PngImagePlugin.PngImageFile image mode=L size=28x28 at 0x137BF87C0>, 'label': 5} (28, 28) (28, 28)
|
从上面的输出中可以看出,这个数据集每个元素中包含一个图片,和该图片对应的真实值。图片是一个灰度图,说明只有两维,28 * 28个像素点。
建立卷积神经网络
根据数据集的信息,我们可以开始建立我们的神经网络了,代码如下所示:
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 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| class numCNN: alpha = 0.0000001 * 100000 def __init__(self, input_shape: tuple, output_sum: int): self.input_x, self.input_y = input_shape self.output_sum = output_sum self.weight = np.random.rand(output_sum, self.input_x * self.input_y)
def forward(self, inputData: np.ndarray) -> np.ndarray: assert inputData.shape == (self.input_x, self.input_y)
inputData = inputData.flatten() result = self.weight.dot(inputData) return result def loss(self, inputD: np.ndarray, outputD: np.ndarray, label: int): assert len(outputD) == self.output_sum real_answer = np.zeros(self.output_sum) real_answer[label] = 1
deltas = outputD - real_answer loss = deltas ** 2 inputD = inputD.flatten() weight_deltas = np.outer(deltas, inputD) return weight_deltas, loss def grad(self, weight_deltas: np.ndarray): self.weight -= self.alpha * weight_deltas
def save(self): with open("imageCNN.pkl", "wb") as f: pickle.dump(self, f)
def eval(self, inputD: np.ndarray): outputD = self.forward(inputD) print(f"[DEBUG] outputD = {outputD}") return np.argmax(outputD)
|
结合上面代码,我们来分析一下我们该如何构建我们的神经网络。
首先,因为图片有28 * 28个像素点,所以相当于有28 * 28个输入。
其次,因为在该数据集中的图片都是手写的0-9这10个数字,所以我们构建的神经网络的作用是根据输入预测这10个数字的概率,那么也就是有10个输出。
接下来我们需要根据输入和输出构建一个权重矩阵,暂时使用随机矩阵来作为初始的权重矩阵。
然后,我们定义forward
函数,用来计算输入和权重卷积计算的结果,由于权重是一个(10, 784)
大小的矩阵,所以在计算卷积前需要把输入的尺寸从(28, 28)
展平成(784, 1)
。
接着,根据上篇文章的方法编写计算误差的loss函数,计算出权重的变化值。最后,在使用梯度下降算法更新权重值。
另外,我还编写了一个save函数,用来把训练的结果保存到文件中。还有一个eval推理函数,推理出来的答案应该是概率最大值的index。
训练
定义好卷积神经网络后,可以开始编写训练代码,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| def train(new=0): if not new: with open("imageCNN.pkl", "rb") as f: imageCNN = pickle.load(f) else: imageCNN = numCNN(image_np.shape, output_sum=10)
for x in range(100): total_loss = np.array([0.0] * 10) for dataset in train_datasets: image, label = dataset["image"], dataset["label"] label = int(label) inputD = np.array(image) / 255 outputD = imageCNN.forward(inputD) weight_deltas, loss = imageCNN.loss(inputD, outputD, label) total_loss += loss imageCNN.grad(weight_deltas) print(f"训练步数:{x}, 损失值:{total_loss}")
imageCNN.save()
|
在训练的过程中,我们可以通过观察total_loss
损失值的情况,调整alpha
,或者调整训练的次数,多次循环训练,直到损失值降低到1以下,差不多就算训练完成了。
另外,通过上面的定义,可以知道weight
是一个(10, 784)
大小的矩阵,也就是说,该矩阵可以转化为十个28 * 28
像素的灰度图。我们可以观察一下权重转化为灰度图以后的情况,相关转化代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| def weightToImage(): with open("imageCNN.pkl", "rb") as f: imageCNN = pickle.load(f) to_pil = transforms.ToPILImage() weight_matrix = torch.tensor(imageCNN.weight) weight_matrix = weight_matrix.view(10, 1, 28, 28)
fig, axes = plt.subplots(2, 5, figsize=(10, 6)) axes = axes.flatten() for axi in range(10): image = to_pil(weight_matrix[axi]) axes[axi].imshow(image, cmap='gray') axes[axi].set_title(f"NUM: {axi}") axes[axi].axis("off") plt.tight_layout() plt.show()
|
生成的图片如下所示:

看到这张图有没有茅塞顿开的感觉?我们可以这么理解卷积神经网络识别图片的本质:通过n个训练图片,提取出图片的特征,并且通过不断的循环训练,最终能形成一个特征综合体图片,而推理的过程就是计算输入的图片和哪个特征的重合度更高。
优化
如果跟着我上面的步骤从0开始训练模型,就会发现训练起来速度特别慢。有以下原因:
- 数据集的大小有6w张图片,我循环100次也就是有600w次计算循环。
- 最开始损失值降的快,但是后期损失值就降的很慢,这就需要更多的训练。
我们该如何加快训练速度呢?
由于我们目前的代码的计算都是通过numpy实现的,全程都是使用cpu进行运算,如果我们使用pytorch库替代numpy,让gpu完成计算过程,计算速度就能加快很多。
我们每次都训练一个数据集,完全可以参考实际的那些开源大模型,一次训练多个数据集(batch相关的参数配置),不过这也需要使用pytorch才能更好的提升训练速度和训练效果。
整合一下上面的代码,其实代码量并不多,我通过gpt-4o把计算方面能优化的都优化好了。其他能修改以下几部分:
alpha
alpha值的设置目前看来只能通过不断的尝试来进行调整
优化输出值
目前forward的输出值如下所示:
1 2
| outputD = [-15.25994374 22.8128506 -65.14659919 180.33356777 -46.43430275 55.35769218 37.90586069 71.81096732 -6.70595218 4.06934123]
|
输出值的我们设定的实际含义是什么呢?输入图片是0-9这十个数字的概率,而上面的输出值明显不是一个概率值,这个时候我们可以增加一个Softmax 激活函数,以下是gpt-4o回复的Softmax 激活函数的具体含义:
1 2 3 4 5 6 7 8 9 10 11 12
| Softmax 激活函数 Softmax 是一种常用于 多分类任务 的激活函数,它的作用是:
1. 把输入转换成概率分布(所有输出值相加等于 1)。 2. 放大较大的值,压缩较小的值,让输出更具可解释性。
Softmax 的特点 1. 输出值总和为 1:结果可以直接作为 分类概率。 2. 放大差异: 最大值对应的类别概率更大,使模型更容易收敛。 3. 适用于多分类问题: 常用于 神经网络最后一层,比如 图像分类(MNIST, CIFAR-10)。
|
更多softmax激活函数的内容请自行研究,该函数的代码如下所示:
1 2 3
| def softmax(x): exp_x = np.exp(x - np.max(x)) return exp_x / np.sum(exp_x)
|
修改后的神经网络代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13
| class numCNN: ...... def forward(self, inputData: np.ndarray) -> np.ndarray: assert inputData.shape == (self.input_x, self.input_y)
inputData = inputData.flatten() result = self.weight.dot(inputData) return softmax(result)
|
最后,我们使用test数据集来判断我们训练后的模型预测效果,评估代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| def eval(): with open("imageCNN.pkl", "rb") as f: imageCNN = pickle.load(f)
success = 0 for i in range(len(test_datasets)): image, label = test_datasets[i]["image"], test_datasets[i]["label"] inputD = np.array(image) predict = imageCNN.eval(inputD) if int(predict) == int(label): success += 1 print(f"评估结果,成功率为:{success}/{i+1}")
|
使用上面描述的代码,训练100个循环后,准确率可以达到80%左右:评估结果,成功率为:8483/10000
。
使用pytorch架构
首先,根据不同的训练设备,还有测试需求,定义如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| if torch.backends.mps.is_available(): if "MPS" in os.environ: print("use mps device") device = torch.device("mps") device_name = "mps" else: print("use cpu device") device = torch.device("cpu") device_name = "cpu" else: if "CUDA" in os.environ: print("use cuda device") device = torch.device("cuda") device_name = "cuda" else: print("use cpu device") device = torch.device("cpu") device_name = "cpu" saveFile = f"imageCNN_{device_name}.pth"
|
在mac m1系列芯片上,使用的是mps设备,在nvidia显卡设备上,使用的cuda。
下一步,定义一个批量加载训练数据集的类,代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| class MNISTDataset(Dataset): def __init__(self, dataset): self.dataset = dataset image = [] label = [] for sample in dataset: image.append(sample['image']) label.append(sample['label']) self.images = torch.tensor(np.array(image, dtype=np.float32) / 255.0).view(-1, 784) self.labels = torch.tensor(label, dtype=torch.long)
def __len__(self): return len(self.dataset)
def __getitem__(self, idx): return self.images[idx], self.labels[idx]
batch_size = 64 train_dataset = MNISTDataset(train_datasets) train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True, pin_memory=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 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| def softmax(x): exp_x = torch.exp(x - torch.max(x, dim=1, keepdim=True)[0]) return exp_x / exp_x.sum(dim=1, keepdim=True)
class numCNN(nn.Module): def __init__(self, input_size=28*28, output_size=10, batch_size=64): super(numCNN, self).__init__() self.weight = nn.Parameter(torch.randn(input_size, output_size) * 0.01) self.batch_size = batch_size self.batch_size_range = range(batch_size) self.real_answer = torch.zeros(self.batch_size, 10, device=device)
def forward(self, x): return torch.matmul(x, self.weight) def save(self): torch.save(self.state_dict(), saveFile) print(f"训练结果已保存到 '{saveFile}'")
def grad(self, inputs, outputs, label): real_answer = self.real_answer.clone() real_answer[self.batch_size_range, label] = 1 deltas = outputs - real_answer weight_deltas = torch.matmul(deltas.t(), inputs) / self.batch_size return weight_deltas
'''loss: 计算误差 inputD的shape = ''' def lossFunction(self, outputs, labels): log_probs = -torch.log(outputs[self.batch_size_range, labels] + 1e-9) return log_probs.mean()
|
第四步,编写训练函数,代码如下所示:
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 27 28 29 30 31 32 33 34 35 36 37
| def train(new=0): batch_size = 60000 // 100 model = numCNN(batch_size=batch_size).to(device) if not new: model.load_state_dict(torch.load(saveFile))
model.weight = model.weight.to(device) train_dataset = MNISTDataset(train_datasets) train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True, pin_memory=True) epochs = 10 for x in range(epochs): start_time = time.time() total_loss = torch.tensor(0.0, device=device) for image, label in train_loader: image, label = image.to(device), label.to(device) conv_output = model(image) probs = softmax(conv_output) loss = model.lossFunction(probs, label) total_loss += loss weight_deltas = model.grad(image, probs, label) with torch.no_grad(): model.weight -= alpha * weight_deltas.t() end_time = time.time() print(f"训练步数:{x}, 损失值:{total_loss.item()},运行时间:{end_time - start_time}") model.save()
|
要使用pytorch架构进行训练,可以选择是使用CPU进行计算训练还是GPU进行计算训练,计算代码不需要做任何修改。因为pytorch判断使用什么设备进行计算是看数据在哪个设备上,如果想使用GPU进行计算,那么只要把数据移动到GPU上,如果数据在CPU上,则使用CPU进行计算。
上面的代码还修改了一下计算损失值的函数,因为损失函数只是让开发者来评估训练效果的变量,我们也没必要深入研究,只要知道可以这么计算来评估训练效果就行。想要了解深入细节的可以询问gpt或者自行研究。
最后一步,还要再编写一个评估函数,代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| def eval(): batch_size = 64 * 1 model = numCNN(batch_size=batch_size).to(device) model.load_state_dict(torch.load(saveFile))
test_dataset = MNISTDataset(test_datasets) test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=True, pin_memory=True)
model.eval() success = 0 for batch_images, batch_labels in test_loader: image, label = batch_images.to(device), batch_labels.to(device) output = model(image) predictions = torch.argmax(output, dim=1) accuracy = predictions == label success += accuracy.sum().item() print(f"评估结果,成功率为:{success}/{len(test_datasets)}")
|
最后来看看训练速度的情况,如下所示:
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 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
| # 使用mac m1 pro设备的CPU进行计算,不使用批量数据集,使用numpy架构,训练10轮 $ /usr/bin/time -p python3 train_image.py train 1 训练步数:0, 损失值:[...],运行时间:5 训练步数:1, 损失值:[...],运行时间:4 训练步数:2, 损失值:[...],运行时间:4 训练步数:3, 损失值:[...],运行时间:4 训练步数:4, 损失值:[...],运行时间:4 训练步数:5, 损失值:[...],运行时间:5 训练步数:6, 损失值:[...],运行时间:4 训练步数:7, 损失值:[...],运行时间:4 训练步数:8, 损失值:[...],运行时间:4 训练步数:9, 损失值:[...],运行时间:4 real 56.31 user 45.23 sys 2.58 # 下面开始都是用pytorch架构 # 使用mac m1 pro设备的CPU进行计算,不使用批量数据集,batch_size = 1,训练10轮 $ python3 train_image_pytorch.py train 1 use cpu device 训练步数:0, 损失值:32250.6171875,运行时间:5.129971027374268 训练步数:1, 损失值:21791.275390625,运行时间:5.347644090652466 训练步数:2, 损失值:20175.958984375,运行时间:5.3410279750823975 训练步数:3, 损失值:19327.568359375,运行时间:5.338259935379028 训练步数:4, 损失值:18796.841796875,运行时间:5.698091983795166 训练步数:5, 损失值:18416.720703125,运行时间:5.464349269866943 训练步数:6, 损失值:18099.1171875,运行时间:5.541553974151611 训练步数:7, 损失值:17852.349609375,运行时间:5.5420732498168945 训练步数:8, 损失值:17676.14453125,运行时间:5.47119402885437 训练步数:9, 损失值:17503.791015625,运行时间:5.431099891662598 训练结束,计算花费时间:54.30526542663574 训练结果已保存到 'imageCNN_cpu.pth' # 使用mac m1 pro设备的GPU进行计算,不使用批量数据集,batch_size = 1,训练10轮 $ MPS=1 python3 train_image_pytorch.py train 1 use mps device 训练步数:0, 损失值:32261.201171875,运行时间:87.1423671245575 训练步数:1, 损失值:21800.0,运行时间:86.88708591461182 训练步数:2, 损失值:20179.2578125,运行时间:87.0305118560791 训练步数:3, 损失值:19334.32421875,运行时间:86.25123476982117 训练步数:4, 损失值:18790.48046875,运行时间:89.27844595909119 训练步数:5, 损失值:18402.587890625,运行时间:86.51565194129944 训练步数:6, 损失值:18101.673828125,运行时间:83.53264117240906 训练步数:7, 损失值:17864.673828125,运行时间:92.22060513496399 训练步数:8, 损失值:17660.087890625,运行时间:88.08050513267517 训练步数:9, 损失值:17501.029296875,运行时间:84.51630687713623 训练结束,计算花费时间:871.4553558826447 训练结果已保存到 'imageCNN_mps.pth' # 使用mac m1 pro设备的CPU进行计算,使用批量数据集,batch_size = 60000 // 100,训练10轮 $ /usr/bin/time -p python3 train_image_pytorch.py train 1 use cpu device 训练步数:0, 损失值:222.49859619140625,运行时间:0.30329084396362305 训练步数:1, 损失值:212.8668975830078,运行时间:0.3047807216644287 训练步数:2, 损失值:203.94381713867188,运行时间:0.2595350742340088 训练步数:3, 损失值:195.66217041015625,运行时间:0.24239015579223633 训练步数:4, 损失值:187.9742889404297,运行时间:0.24459481239318848 训练步数:5, 损失值:180.83810424804688,运行时间:0.32437920570373535 训练步数:6, 损失值:174.21531677246094,运行时间:0.21275115013122559 训练步数:7, 损失值:168.0689239501953,运行时间:0.22297191619873047 训练步数:8, 损失值:162.36447143554688,运行时间:0.2187938690185547 训练步数:9, 损失值:157.06793212890625,运行时间:0.26395273208618164 训练结束,计算花费时间:2.597440481185913 训练结果已保存到 'imageCNN_cpu.pth' real 9.54 user 22.85 sys 4.32 # 使用mac m1 pro设备的GPU进行计算,使用批量数据集,batch_size = 60000 // 10,训练10轮 $ MPS=1 /usr/bin/time -p python3 train_image_pytorch.py train 1 use mps device 训练步数:0, 损失值:225.5301971435547,运行时间:0.5048720836639404 训练步数:1, 损失值:215.48736572265625,运行时间:0.3935577869415283 训练步数:2, 损失值:206.2926025390625,运行时间:0.41111207008361816 训练步数:3, 损失值:197.8079833984375,运行时间:0.4002230167388916 训练步数:4, 损失值:189.952880859375,运行时间:0.43266820907592773 训练步数:5, 损失值:182.66993713378906,运行时间:0.44860124588012695 训练步数:6, 损失值:175.91522216796875,运行时间:0.4099619388580322 训练步数:7, 损失值:169.64776611328125,运行时间:0.4285860061645508 训练步数:8, 损失值:163.83045959472656,运行时间:0.4242072105407715 训练步数:9, 损失值:158.4300994873047,运行时间:0.4490029811859131 训练结束,计算花费时间:4.302792549133301 训练结果已保存到 'imageCNN_mps.pth' real 10.89 user 19.59 sys 4.87
|
通过以上的数据对比可以发现,如果不使用批量数据集,pytorch并不会比numpy快多少,并且如果使用GPU进行计算,速度还会降低的非常严重,因为循环开销还有把数据移动到GPU上的开销会特别大。
但是在使用批量数据集后,速度将会有非常显著的提升,不过这里CPU和GPU的对比不具有参考性,之后另外的文章会分析。
总结
在这篇文章中,我们自行构建了一个简单的神经网络,并且对其开始训练,从numpy架构又转化到pytorch架构,不过由于只是变更架构,算法没有发生变化,因此最终的准确率并没有发生大变化,只是pytorch训练的速度更快了,可以在相同的时间内训练更多的轮数,更快的达到收敛极限。下篇文章将会对算法进行优化,提高识别的准确率。