LeNet5是卷积神经网络的开山之作,在手写数字识别上达到了出色的效果。LeNet5具有5层神经网络,其中2层卷积神经网络,3层全连接。两层卷积神经网络之后分别接了一个激活层和一个平均池化层。这里通过对lenet进行解析和修改,实现对自定义图像数据的分类。
首先来解析lenet5手写数字识别模型:
第一个卷积层:输入图像为32*32大小的灰度图,图像通道为1,宽高为32*32,卷积层包含6个卷积核,每个卷积核大小为5*5,经过第一个卷积层后,生成通道为6的宽高为28*28的特征图,之后通过sigmoid激活函数和池化层,池化层为均值池化,采用2*2卷积核,步长为2进行池化卷积,对特征图进行下采样,经过池化以后,特征图变为6*14*14;
第二个卷积层:这一层有16个卷积核,卷积核大小5*5,经过该层卷积后特征图大小变为16*10*10,池化层同样采用2*2卷积核,步长为2,经过池化后,特征图变为16*5*5;
在全连接层之前需要对特征图进行打平,16*5*5=400,通过第一个全连接层,降维到120,第二个全连接层后降维到84,第三个全连接层降维到10,也就是手写数字识别的分类数。
lenet5的pytorch实现:
net = torch.nn.Sequential(
nn.Conv2d(1,6,kernel_size=5), #[1,1,28,28]*[6,5*5]-->[1,6,28,28],图像与6个5*5的卷积核计算,得到6个28*28特征图
nn.Sigmoid(), #激活函数
nn.AvgPool2d(2,stride=2), #[1,6,28,28]-->[1,6,14,14],平均池化,卷积核2*2,步长2(每四个像素合并为一个)
nn.Conv2d(6,16,kernel_size=5), #[1,6,14,14]*[16,5*5]-->[1,16,10,10],图像与16个5*5卷积核计算,得到16个10*10特征图
nn.Sigmoid(),
nn.AvgPool2d(kernel_size=2, stride=2), #[1,16,10,10]-->[1,16,5,5]
nn.Flatten(), #[1,16,5,5]-->[1,400] 打平
nn.Linear(400,120), #[1,400]-->[1,120]
nn.Sigmoid(),
nn.Linear(120,84), #[1,120]-->[1,84]
nn.Sigmoid(),
nn.Linear(84,10) #[1,84]-->[1,10]
)
使用lenet5训练自定义图像分类模型,根据数据和分类对模型进行修改。
数据集路径类似如下结构:
dataset/dogs/train/(husky,labrado)
dataset/dogs/val/(husky,labrado)
由于这里使用的彩色图像,所以将第一个卷积层输入通道改为3,使用的数据集只包含两个分类,那么需要最后一个全连接层输出改为分类数,也就是2。
构建网络:
net = torch.nn.Sequential(
nn.Conv2d(3,6,kernel_size=5),
nn.Sigmoid(),
nn.AvgPool2d(2,stride=2),
nn.Conv2d(6,16,kernel_size=5),
nn.Sigmoid(),
nn.AvgPool2d(kernel_size=2, stride=2),
nn.Flatten(),
nn.Linear(400,120),
nn.Sigmoid(),
nn.Linear(120,84),
nn.Sigmoid(),
nn.Linear(84,2)
)
定义训练函数,由于是图像分类,将不同类别图像放到不同文件夹下,使用ImageFolder加数据集并自动分配标签。
def train():
# 如有GPU,默认使用第一块GPU
device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
print('using device {}'.format(device))
#数据预处理
data_transform = {
"train": transforms.Compose([
transforms.RandomResizedCrop(32), #随机缩放裁剪
transforms.RandomHorizontalFlip(), #随机水平翻转
transforms.ToTensor(), #转换为Tensor
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)) #归一化
]),
"val": transforms.Compose([
transforms.RandomResizedCrop(32),
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
])
}
#加载数据集
batch_size = 32
data_path = 'dataset/dogs'
assert os.path.exists(data_path), "{} does not exist".format(data_path)
train_dataset = datasets.ImageFolder(root=os.path.join(data_path, 'train'), transform=data_transform['train'])
val_dataset = datasets.ImageFolder(root=os.path.join(data_path, 'val'), transform=data_transform['val'])
train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=batch_size, shuffle=True, num_workers=8)
val_loader = torch.utils.data.DataLoader(val_dataset, batch_size=5, shuffle=False, num_workers=8)
net.load_state_dict(torch.load('lenet.pt'))
net.to(device)
loss_function = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(net.parameters(), lr=0.01)
epochs = 10
save_path = 'lenet.pt'
best_acc = 0.0
steps = len(train_loader)
for epoch in range(epochs):
net.train()
running_loss = 0.0
for step, data in enumerate(train_loader):
images, labels = data
optimizer.zero_grad()
outputs = net(images.to(device))
loss = loss_function(outputs, labels.to(device))
loss.backward()
optimizer.step()
running_loss += loss.item()
print('step:{}/{},loss:{}'.format(step + 1, steps, loss))
net.eval()
acc = 0.0
with torch.no_grad():
for val_data in val_loader:
images, labels = val_data
outputs = net(images.to(device))
predict = torch.max(outputs, dim=1)[1]
acc += torch.eq(predict, labels.to(device)).sum().item()
val_acc = acc / len(val_dataset)
print('epoch:{}, acc:{}'.format(epoch + 1, val_acc))
if val_acc > best_acc:
best_acc = val_acc
torch.save(net.state_dict(), save_path)
print("finish train")
开始训练,经过10轮训练后,分类精度达到82%,最高精度85%。
这里只是简单修改了lenet模型的输入输出,如果对模型其他地方进行修改,将激活函数替换为Relu又会如何呢?
修改以后,经过第一轮训练后,模型精度就达到了71%,训练过程最高精度达到85%,看来更换激活函数还是有一定效果。可以操作的地方还有很多,比如将平均池化改为最大池化,增加卷积层数,修改卷积核数量,调整卷积核大小,使用padding自定义卷积层输出大小,或者修改全连接层等,直到调出一个性能不错的模型,炼丹不就成了。