目录
第一次打卡
实践任务:农民身份识别挑战赛
https://challenge.xfyun.cn/topic/info?type=peasant-identity&ch=ymfk4uU
时间规划
三环节
赛题解读(What)
赛题本质:这是一个图像分类问题,需要根据图像序列中的农民特征,对农民进行身份识别。
身份识别优势(Why)
任务(How)
baseline代码解读
图像特征统计(传统方法)
1.导入需要的库
2.数据收集与准备
3.特征提取
np.where函数
4.模型训练、评估与优化
5.生成测试集提交结果submit.csv
进阶实践CNN(深度学习方法)
1.导入模块——Pytorch版本
2.数据收集与准备
3.特征提取
4.模型训练、评估与优化
5.结果输出
8月2日 开营
8月3日 - 6日
理解赛题,跑通代码
提交任务一打卡,查看个人成绩排行榜
8月8日 直播 竞赛进阶上分思路与技巧
8月7日-9日
优化Baseline,尝试高阶技巧
提交任务二打卡,查看个人成绩排行榜
8月12日 直播 结营直播&优秀学习者颁奖
- 可以真实客观地记录农民的状态,为农场管理和农产品追溯提供真实的客观数据;
- 较之直接存储视频,可以有效地降低存储空间;
- 相比于比人工监管,能大幅度提高效率,减少人工成本。
图像分类是计算机视觉领域的一个重要任务,旨在将输入的图像分为不同的预定义类别,即让计算机能够理解和识别图像的内容,并将其归类到已知的类别中。流程图为(直接照搬了)
任务输入:图像序列,每个序列包含多张图像,每张图像中有一名农民。
任务输出:农民身份,每个身份对应一个类别编号,从0到24。
这是我第一次参加CV方向的比赛,在参加比赛之前,我对相关代码了解很少,好在有机器学习的一点基础,但优化更是遥远。
在图像分类中,常用的特征提取方法是图像特征统计 或 使用卷积神经网络(CNN)。
图像特征统计是指对图像中的像素或图像区域进行统计分析,以提取和描述图像的特征信息
CNN是一种专门设计用于处理图像数据的神经网络架构,能够有效地从原始像素中学习和提取特征。
通过一系列的卷积层、池化层和全连接层,CNN能够自动学习具有区分性的图像特征。
所以本次实践给了2份代码分别采用了图像特征统计(传统方法 K近邻)和CNN(深度学习方法-随机森林)下面来分别进行解读,使用云环境-百度飞桨平台进行。
import os, sys, glob, argparse
from PIL import Image #用于处理图像文件
import cv2 #常用的计算机视觉库,用于读取,显示,处理图像和视频
import pandas as pd #数据分析库,操作数据表格,读取和加载csv文件
import numpy as np #科学计算库,用于操作多维数组和矩阵,数值运算
from tqdm import tqdm #进度条库,用于显示循环的进度和时间
#sklearn:机器学习库,提供包括以下一些分类器和评估函数。
from sklearn.neighbors import KNeighborsClassifier #K近邻算法
from sklearn.ensemble import RandomForestClassifier #随机森林算法
from sklearn.model_selection import cross_val_predict #交叉验证,并返回每个样本的预测标签
from sklearn.metrics import classification_report #包括准确率,召回率,F1分数等常用指标
# 读取数据集
train_path = glob.glob('./农民身份识别挑战赛公开数据/train/*') #路径获取
test_path = glob.glob('./农民身份识别挑战赛公开数据/test/*') #路径获取
#排序为了确保读取正确
train_path.sort()
test_path.sort()
#pandas读取csv文件,存入DataFrame对象中
train_df = pd.read_csv('农民身份识别挑战赛公开数据/train.csv')
train_df = train_df.sort_values(by='name') #排序为了确保读取正确
train_label = train_df['label'].values #获取训练label标签
构建占比特征、边缘特征、纹理特征
# 读取图像特征
def image_feat(path): #封装接受图像文件的路径即参数path
img = cv2.imread(path, 0) #灰度模式读取图像文件
img = img.astype(np.float32) #数据类型转换为np.float32
feat = [ #feat的列表,统计以下8个元素
(img != 0).sum(), # 非零像素的数量
(img == 0).sum(), # 零像素的数量
img.mean(), # 平均值
img.std(), # 标准差
len(np.where(img.mean(0))[0]), # 在列方向上平均值不为零的数量
len(np.where(img.mean(1))[0]), # 在行方向上平均值不为零的数量
img.mean(0).max(), # 列方向上的最大平均值
img.mean(1).max() # 行方向上的最大平均值
]
return feat
两个较为复杂的解释一下
len(np.where(img.mean(0))[0])
首先调用img.mean(0)函数,返回img在列方向上(即第0轴)的平均值,得到一个一维数组。然后使用np.where函数对该数组进行判断,它返回一个元组,其中第0个元素是一个数组,表示满足条件(默认为不为零)的元素在原数组中的索引。然后您使用len函数对该数组求长度,得到在列方向上平均值不为零的数量。
len(np.where(img.mean(1))[0])
调用img.mean(1)函数,它返回img在行方向上(即第1轴)的平均值,得到一个一维数组。然后您使用np.where函数对该数组进行判断,它返回一个元组,其中第0个元素是一个数组,表示满足条件(默认为不为零)的元素在原数组中的索引。然后您使用len函数对该数组求长度,得到在行方向上平均值不为零的数量。
np.where函数是一个numpy模块中的函数,可以返回数组中的元素或元素的索引。两种用法:
• 当有三个参数时,np.where(condition, x, y),表示根据条件condition,从x或y中选择元素返回。当condition为True时,返回x中的元素,否则返回y中的元素。x, y和condition需要可以广播到同一形状。
• 当只有一个参数时,np.where(condition),表示返回数组中满足条件的元素的索引。它相当于np.asarray(condition).nonzero(),返回一个元组,其中每个元素是一个一维数组,表示满足条件的元素在对应轴上的索引。
采取K近邻的算法构建模型
# 训练集特征
train_feat = [] #存储训练集的图像特征
for path in tqdm(train_path): #进度条显示循环的进度和时间
train_feat += [image_feat(path)]
# 测试集特征
test_feat = []
for path in tqdm(test_path): #存储测试集的图像特征
test_feat += [image_feat(path)] #进度条显示循环的进度和时间
# 训练集交叉验证
train_pred = cross_val_predict( #交叉验证 样本的预测标签
KNeighborsClassifier(), #k近邻算法
np.array(train_feat), #输入
train_label #输入
)
print(classification_report(train_label, train_pred)) #打印准确率,召回率,F1分数
# 模型训练与预测
model = KNeighborsClassifier()
model.fit( #fit对模型进行训练
np.array(train_feat), #输入
train_label #输入
)
test_pred = model.predict(np.array(test_feat)) #测试集的预测标签
# 生成测试集提交结果
submit = pd.DataFrame(
{
'name': [x.split('/')[-1] for x in test_path],
'label': test_pred
})
submit = submit.sort_values(by='name')
submit.to_csv('submit.csv', index=None)
一些重复的部分就不再赘述了
卷积神经网络(Convolutional Neural Network,CNN)是一种深度学习模型,广泛用于图像识别、计算机视觉和模式识别任务中。CNN 在处理具有网格结构数据(如图像)时表现出色,它能够自动学习和提取图像中的特征,并在分类、定位和分割等任务表现优秀的性能。
import os, sys, glob, argparse
import pandas as pd
import numpy as np
from tqdm import tqdm
import cv2, time #获取和格式化当前时间、延迟执行
from PIL import Image
from sklearn.model_selection import train_test_split, StratifiedKFold, KFold
import torch
torch.manual_seed(0)
#设置随机数种子为0,为了保证每次运行代码时得到相同的结果。
torch.backends.cudnn.deterministic = False
#提高运行速度,但可能会导致结果不可复现
torch.backends.cudnn.benchmark = True
#让程序自动寻找最适合当前配置的高效算法,以加速运行。
#torchchvision是一个视觉相关的库,提供了一些预训练的模型,图像变换和数据集的功能
import torchvision.models as models #预训练的深度学习模型-ResNet, VGG, AlexNet..
import torchvision.transforms as transforms #图像变换,对图像进行预处理或数据增强
import torchvision.datasets as datasets #图像数据集,加载数据
import torch.nn as nn #常用的层和激活函数
import torch.nn.functional as F #常用的函数和损失函数
import torch.optim as optim #优化相关的库,提供常用的优化器
from torch.autograd import Variable #自动求导相关的库,提供包装张量并记录梯度
from torch.utils.data.dataset import Dataset #数据集相关的库,自定义数据集的功能
# Check if GPU is available
if torch.cuda.is_available():
device = torch.device("cuda")
else:
device = torch.device("cpu")
from sklearn.model_selection import train_test_split, StratifiedKFold, KFold
• train_test_split函数是一个数据划分函数,它可以将数据集按照一定的比例随机划分为训练集和测试集,或者训练集,验证集和测试集。它有很多参数,可以指定划分的比例,随机数种子,是否保持类别平衡等。
• StratifiedKFold函数是一个交叉验证函数,它可以将数据集按照一定的份数(称为折数)划分为不同的子集,每次使用一个子集作为测试集,其他子集作为训练集,进行多轮训练和测试,得到平均的评估结果。它可以保持每个子集中类别的比例与原始数据集相同,适用于类别不平衡的情况。
• KFold函数是一个交叉验证函数,它与StratifiedKFold函数类似,但不保证每个子集中类别的比例与原始数据集相同,适用于类别平衡的情况。
# 读取数据集
train_path = glob.glob('./农民身份识别挑战赛公开数据/train/*')
test_path = glob.glob('./农民身份识别挑战赛公开数据/test/*')
train_path.sort()
test_path.sort()
train_df = pd.read_csv('农民身份识别挑战赛公开数据/train.csv')
train_df = train_df.sort_values(by='name')
train_label = train_df['label'].values
# 自定义数据集
# 带有图片缓存的逻辑
DATA_CACHE = {}
class XunFeiDataset(Dataset): #继承自torch.utils.data.dataset模块中的Dataset类
def __init__(self, img_path, img_label, transform=None):
self.img_path = img_path #包含图像文件路径
self.img_label = img_label #包含图像标签
if transform is not None: #可选的图像变换函数
self.transform = transform
else:
self.transform = None
def __getitem__(self, index): #获取数据的索引
if self.img_path[index] in DATA_CACHE:
img = DATA_CACHE[self.img_path[index]]
else:
img = cv2.imread(self.img_path[index])
DATA_CACHE[self.img_path[index]] = img
if self.transform is not None:
img = self.transform(image = img)['image']
#首先判断img_path[index]是否在DATA_CACHE字典中,这是一个用于缓存已经读取过的图像的字典。如果在字典中,则从字典中获取图像,并赋值给img变量;否则,使用cv2模块的imread函数读取图像文件,并将其添加到字典中,并赋值给img变量。这样做可以避免重复读取相同的图像文件,提高运行效率。
img = img.transpose([2,0,1])
return img, torch.from_numpy(np.array(self.img_label[index]))
def __len__(self):
return len(self.img_path)
#接着使用transpose方法对img变量进行转置操作,将其形状从(高度, 宽度, 通道数)变为(通道数, 高度, 宽度),以适应PyTorch框架对图像数据的要求。
import albumentations as A #图像增强库
# 训练集
# 三个数据加载器对象 train_loader val_loader test_loader
train_loader = torch.utils.data.DataLoader( #使用除了最后1000个以外的所有训练样本
XunFeiDataset(train_path[:-1000], train_label[:-1000],
A.Compose([
A.RandomRotate90(), #随机旋转90度
A.Resize(256, 256), #缩放到256x256
A.RandomCrop(224, 224), #随机裁剪到224x224
A.HorizontalFlip(p=0.5), #水平翻转
A.RandomContrast(p=0.5), #随机对比度
A.RandomBrightnessContrast(p=0.5), #随机亮度对比度
A.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)) #归一化
])
), batch_size=30, #每次加载30个样本作为一个批次
shuffle=True, #每次加载前打乱样本的顺序
num_workers=1, #使用一个线程进行数据加载
pin_memory=False #不将数据预先加载到内存中
)
# 验证集
val_loader = torch.utils.data.DataLoader(
XunFeiDataset(train_path[-1000:], train_label[-1000:],
A.Compose([
A.Resize(256, 256),
A.RandomCrop(224, 224),
A.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225))
])
), batch_size=30, shuffle=False, num_workers=1, pin_memory=False
)
# 测试集
test_loader = torch.utils.data.DataLoader(
XunFeiDataset(test_path, [0] * len(test_path),
A.Compose([
A.Resize(256, 256),
A.RandomCrop(224, 224),
A.HorizontalFlip(p=0.5),
A.RandomContrast(p=0.5),
A.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225))
])
), batch_size=2, shuffle=False, num_workers=1, pin_memory=False
)
自定义CNN模型,本次采取ResNet18来提取图像特征,下次可以试试VGG, AlexNet
#创建自定义的神经网络模型
class XunFeiNet(nn.Module): #继承自torch.nn模块中的Module类
def __init__(self):
super(XunFeiNet, self).__init__() #super函数调用父类的构造函数
model = models.resnet18(True) #下载并加载预训练的权重
#ResNet-18是一个深度残差网络,用于图像分类任务,具有18层卷积层和全连接
model.avgpool = nn.AdaptiveAvgPool2d(1)
#使用自适应平均池化层,将输出的特征图大小调整为1x1
model.fc = nn.Linear(512, 25)
#使用一个全连接层,将输入的512维特征向量映射为25维的输出向量
#25是分类的类别数
self.resnet = model
def forward(self, img): #定义模型的前向传播逻辑的
out = self.resnet(img)
return out
model = XunFeiNet()
model = model.to(device)
#有GPU设备,使用GPU加速计算;否则使用CPU进行计算
criterion = nn.CrossEntropyLoss().cuda()
#创建了一个交叉熵损失函数对象,计算模型输出和真实标签之间的损失值,并进行反向传播和优化
optimizer = torch.optim.AdamW(model.parameters(), 0.001)
#创建了一个AdamW优化器,model.parameters()是对象优化器要更新的参数;还传入0.001作为参数,表示优化器的学习率
AdamW是一种改进的Adam算法,可以更好地处理权重衰减和学习率衰减等问题
生成测试集提交结果submit.csv
# 对模型进行一轮训练,并返回训练集的平均损失值
def train(train_loader, model, criterion, optimizer):
#train_loader是一个数据加载器对象,用于批量加载训练集的数据和标签;
#model是一个神经网络模型对象,用于进行前向和反向传播;
#criterion是一个损失函数对象,用于计算损失值;
#optimizer是一个优化器对象,用于更新模型的参数
model.train() #模型设置为训练模式
train_loss = 0.0 #用于累计训练集的总损失值
for i, (input, target) in enumerate(train_loader): #得到批次的数据和标签
input = input.to(device)
target = target.to(device)
output = model(input) #模型的输出结果
loss = criterion(output, target) #该批次的损失值
optimizer.zero_grad() #参数梯度清零,避免梯度累加
loss.backward() #对损失值进行反向传播,计算模型中所有参数的梯度
optimizer.step() #根据梯度和学习率更新模型中所有参数的值
if i % 100 == 0:
print('Train loss', loss.item())
train_loss += loss.item() #计算训练集的总损失值
return train_loss/len(train_loader) #训练集的平均损失值
# 模型一轮验证
def validate(val_loader, model, criterion):
#val_loader是一个数据加载器对象,用于批量加载验证集的数据和标签;
model.eval() ##模型设置为评估模式
val_acc = 0.0
with torch.no_grad():
end = time.time() #获取当前时间,表示循环开始时刻
for i, (input, target) in enumerate(val_loader):
input = input.to(device)
target = target.to(device)
output = model(input)
loss = criterion(output, target)
#使用torch.argmax函数对output变量沿着第1个维度(即每个样本的输出向量)求最大值的索引,得到一个一维张量,表示每个样本的预测标签。然后使用torch.eq函数对预测标签和真实标签进行比较,得到一个布尔型张量,表示每个样本的预测是否正确。然后使用torch.sum函数对布尔型张量求和,得到一个标量,表示该批次的正确预测数。然后使用item方法将标量转换为Python数值,并累加到val_acc变量中,以便计算验证集或训练集的总正确预测数。
val_acc += (output.argmax(1) == target).sum().item()
return val_acc / len(val_loader.dataset)
# 模型预测
def predict(test_loader, model, criterion):
model.eval()
val_acc = 0.0
test_pred = []
#使用torch.no_grad()上下文管理器包裹一段代码块,在这个代码块中不会记录梯度信息,以节省内存和提高运行速度
with torch.no_grad():
end = time.time()
for i, (input, target) in enumerate(test_loader):
input = input.to(device)
target = target.to(device)
output = model(input)
test_pred.append(output.data.cpu().numpy())
#使用data属性获取output变量中的数据部分,并使用cpu方法将其移动到CPU设备上,然后使用numpy模块中的vstack函数将其垂直堆叠起来,并添加到test_pred列表中,以便存储测试集的预测结果。
return np.vstack(test_pred)
for _ in range(1):
train_loss = train(train_loader, model, criterion, optimizer)
val_acc = validate(val_loader, model, criterion)
train_acc = validate(train_loader, model, criterion)
print(train_loss, train_acc, val_acc)
# 对测试集多次预测
pred = None
for _ in range(3):
if pred is None:
pred = predict(test_loader, model, criterion)
else:
pred += predict(test_loader, model, criterion)
submit = pd.DataFrame(
{
'name': [x.split('/')[-1] for x in test_path],
'label': pred.argmax(1)
})
# 生成提交结果
submit = submit.sort_values(by='name')
submit.to_csv('submit.csv', index=None)