# 树叶分类
此代码参考simple resnet baseline | Kaggle,我仅仅对代码做了一些注释
labels_dataframe = pd.read_csv('./leave_train.csv') # 读取文件
labels_dataframe.describe() # count:每列的非缺失值数量。unique:每列中不同数值(唯一值)的数量。top:每列中的众数,即在该列中出现次数最多的数值。freq:众数出现的频数,即众数在该列中出现的次数。
def barw(ax): # ax表示表示绘图的坐标轴(axis)对象。
for p in ax.patches: # ax.patches包含了图表中的所有图形元素,每个条形都是其中的一部分。
val = p.get_width() # 获取当前条形的宽度,也就是条形的高度,表示当前类别的频数。
x = p.get_x() + p.get_width() # 获取当前条形的x位置,即条形的右边缘的位置,表示频数值的位置。
y = p.get_y() + p.get_height() / 2 # 获取当前条形的y位置,即条形的垂直中心位置,表示频数值的位置。
fontsize = 7 # 调整字体大小
ax.annotate(round(val, 2), (x, y), fontsize=fontsize) # 使用ax.annotate函数在当前条形的位置(x, y)处添加标签,标签的内容是频数值(通过round(val, 2)四舍五入保留两位小数)。这样就将频数值标签添加到了每个条形上。
plt.figure(figsize = (15,30))
ax0 =sns.countplot(y=labels_dataframe['label'],order=labels_dataframe['label'].value_counts().index) # 按降序排列
barw(ax0)
ax0.set_ylabel('Y-axis', fontsize=7)
plt.show()
leaves_labels = sorted(list(set(labels_dataframe['label']))) # set(...)将标签列中的唯一标签提取出来,使用 set 函数去重,得到一个包含唯一标签的集合(set)。list(...):将唯一标签的集合转换为一个列表,sorted对标签列表进行排序,得到按字母顺序或数值大小升序排列的标签列表。
n_classes = len(leaves_labels) # 计算不同标签的数量,也就是类别的数量。这将得到数据集中不同类别的总数。
print(n_classes)
leaves_labels[:10] # 取前 10 个标签,以便查看数据集中的一部分不同类别
总共有176个类别
class_to_num = dict(zip(leaves_labels, range(n_classes))) # 将标签转成数字
class_to_num
# 再转换回来,方便最后预测的时候使用
num_to_class = {v : k for k, v in class_to_num.items()}
class LeavesData(Dataset):
def __init__(self, csv_path, file_path, mode='train', valid_ratio=0.2, resize_height=256, resize_width=256):
"""
Args:
csv_path: CSV 文件的路径,其中包含图像文件的名称和标签。
file_path: 图像文件的存储路径。
mode: 数据集模式,可以是 'train'(训练集)、'valid'(验证集)或'test'(测试集)。
valid_ratio: 验证集的比例,用于将数据集划分为训练集和验证集。
resize_height 和 resize_width: 需要调整的图像尺寸。
"""
# 需要调整后的照片尺寸,我这里每张图片的大小尺寸不一致#
self.resize_height = resize_height # 存储了需要调整的图像尺寸。
self.resize_width = resize_width
self.file_path = file_path # 存储了图像文件的存储路径。
self.mode = mode # 存储了数据集的模式,可以是 'train'、'valid' 或 'test'。
# 读取 csv 文件
# 利用pandas读取csv文件
self.data_info = pd.read_csv(csv_path, header=None) # header=None是去掉表头部分
# 计算 length
self.data_len = len(self.data_info.index) - 1 # 计算数据集中的总行数。。由于CSV文件的第一行通常包含表头信息而不是样本数据,因此需要减去1,以得到真实的样本数量。
self.train_len = int(self.data_len * (
1 - valid_ratio)) # 计算训练集的长度。valid_ratio 表示验证集的比例,通常在0和1之间。计算方法是将 self.data_len 乘以 (1 - valid_ratio),然后使用 int 函数将结果转换为整数。
if mode == 'train': # 如果 mode 是 'train',则提取训练集的图像文件名和标签。
# 第一列包含图像文件的名称
self.train_image = np.asarray(
self.data_info.iloc[1:self.train_len,
0]) # self.data_info.iloc[1:,0]表示读取第一列,从第二行开始到train_len, np.asarray():将结果转换为 NumPy 数组。
# 第二列是图像的 label
self.train_label = np.asarray(self.data_info.iloc[1:self.train_len, 1])
self.image_arr = self.train_image # 这两行代码将获得的图像文件名和标签信息保存到 self.image_arr 和 self.label_arr 实例变量中。
self.label_arr = self.train_label
elif mode == 'valid': # 如果 mode 是 'valid',则提取验证集的图像文件名和标签。
self.valid_image = np.asarray(self.data_info.iloc[self.train_len:,
0]) # 我们选择了行索引从 self.train_len (训练集)开始到最后一行的所有行(包括self.train_len行),以及第0列。
self.valid_label = np.asarray(
self.data_info.iloc[self.train_len:, 1]) # 选择了从第self.train_len行开始到最后一行的所有行(包括第self.train_len行),以及第1列。
self.image_arr = self.valid_image
self.label_arr = self.valid_label
elif mode == 'test': # 如果 mode 是 'test',则提取测试集的图像文件名。
self.test_image = np.asarray(
self.data_info.iloc[1:, 0]) # 选择 self.data_info 数据框的所有行(从第1行到最后一行,也就是整个 CSV 文件)以及第0列。
self.image_arr = self.test_image
self.real_len = len(self.image_arr)
print('Finished reading the {} set of Leaves Dataset ({} samples found)'
.format(mode, self.real_len))
def __getitem__(self, index):
# 从 image_arr中得到索引对应的文件名
single_image_name = self.image_arr[index]
# 读取图像文件
img_as_img = Image.open(self.file_path + single_image_name)
# 如果需要将RGB三通道的图片转换成灰度图片可参考下面两行
# if img_as_img.mode != 'L':
# img_as_img = img_as_img.convert('L')
# 设置好需要转换的变量,还可以包括一系列的nomarlize等等操作
if self.mode == 'train':
transform = transforms.Compose([
transforms.Resize((224, 224)),
transforms.RandomHorizontalFlip(p=0.5), # 随机水平翻转 选择一个概率
transforms.ToTensor()
])
else:
# valid和test不做数据增强
transform = transforms.Compose([
transforms.Resize((224, 224)),
transforms.ToTensor()
])
img_as_img = transform(img_as_img)
if self.mode == 'test':
return img_as_img
else:
# 得到图像的 string label
label = self.label_arr[index] # self.label_arr存储的是标签
# number label
number_label = class_to_num[label]
return img_as_img, number_label # 返回每一个index对应的图片数据和对应的label
def __len__(self):
return self.real_len
train_path = './input/leave_train.csv'
test_path = './input/leave_test.csv'
img_path = './input/'
train_dataset = LeavesData(train_path, img_path, mode='train')
val_dataset = LeavesData(train_path, img_path, mode='valid')
test_dataset = LeavesData(test_path, img_path, mode='test')
print(train_dataset)
print(val_dataset)
print(test_dataset)
# 定义data loader
train_loader = torch.utils.data.DataLoader( # 加载训练集数据
dataset=train_dataset,
batch_size=8, # 批量大小为8
shuffle=False, # 不随机打乱
num_workers=5 # 工作进程为5 这个只能在liunx下运行,在windows下运行会报多线程错误,如果是winndows注释掉即可
)
val_loader = torch.utils.data.DataLoader( # 验证数据
dataset=val_dataset,
batch_size=8,
shuffle=False,
num_workers=5
)
test_loader = torch.utils.data.DataLoader( # 测试集数据
dataset=test_dataset,
batch_size=8,
shuffle=False,
num_workers=5
)
def im_convert(tensor):
""" 展示数据"""
image = tensor.to("cpu").clone().detach() # 首先将张量复制到 CPU,并去除梯度信息
image = image.numpy().squeeze() # 将张量转换为 NumPy 数组并去掉单一的通道维度
image = image.transpose(1, 2, 0) # 调整通道维度的顺序,将图像数据排列为 (height, width, channels)
image = image.clip(0, 1) # 裁剪像素值,确保在 [0, 1] 范围内
return image
fig = plt.figure(figsize=(20, 12)) # 创建一个 Matplotlib 图形对象,设置图形大小为 20x12
columns = 4 # 定义子图列数
rows = 2 # 定义子图行数
for inputs, classes in val_loader:
for idx in range(columns * rows):
ax = fig.add_subplot(rows, columns, idx + 1, xticks=[], yticks=[])
ax.set_title(num_to_class[int(classes[idx])])
plt.imshow(im_convert(inputs[idx]))
break # 这里加上break是为了只展示一个批次的数据
plt.show()
for idx in range(columns * rows):
ax = fig.add_subplot(rows, columns, idx + 1, xticks=[], yticks=[]) # 添加子图到 Matplotlib 图形中,设置 x 和 y 轴的刻度为空
ax.set_title(num_to_class[int(classes[idx])]) # 设置子图标题为类别标签
plt.imshow(im_convert(inputs[idx])) # 显示图像,使用 im_convert 函数将 PyTorch 张量转换为可视化的图像
plt.show()
def get_device():
return 'cuda' if torch.cuda.is_available() else 'cpu'
device = get_device()
print(device)
def set_parameter_requires_grad(model, feature_extracting):
if feature_extracting:
model = model
for param in model.parameters():
param.requires_grad = False
# resnet34模型
def res_model(num_classes, feature_extract=False, use_pretrained=True): # num_classes:用于指定模型的输出类别数。feature_extract:一个布尔值,如果设置为True,将冻结模型的前面一些层,只训练分类器层。如果设置为False,将对整个模型进行微调。use_pretrained:一个布尔值,如果设置为True,将使用在大型图像数据集上预训练的权重来初始化模型。如果设置为False,将使用随机初始化的权重。
model_ft = models.resnet34(pretrained=use_pretrained) # 首先创建一个ResNet-34模型,如果use_pretrained为True,它将加载在大型图像数据集上预训练的权重。
set_parameter_requires_grad(model_ft, feature_extract) # 然后,根据feature_extract参数的值,它会冻结模型的前面一些层(如果feature_extract为True),或者将整个模型的参数都设置为可训练(如果feature_extract为False)。
num_ftrs = model_ft.fc.in_features # 最后,它会更改模型的分类器层,以适应给定的num_classes,并返回构建好的ResNet-34模型。
model_ft.fc = nn.Sequential(nn.Linear(num_ftrs, num_classes))
return model_ft
# 超参数
learning_rate = 3e-4
weight_decay = 1e-3
num_epoch = 50
model_path = './pre_res_model.ckpt'
# 初始化模型
model = res_model(176) # 创建一个ResNet-34模型,用于176个不同的类别的图像分类任务。
model = model.to(device) # model = model.to(device): 将模型移动到指定的设备(通常是GPU)上,以便在该设备上执行计算。
model.device = device
criterion = nn.CrossEntropyLoss() # 定义损失函数,这里使用交叉熵损失来衡量模型性能。
optimizer = torch.optim.Adam(model.parameters(), lr = learning_rate, weight_decay=weight_decay) # 创建一个Adam优化器来更新模型的参数,可以调整学习率(learning_rate)和权重衰减(weight_decay)等超参数。
n_epochs = num_epoch # 指定训练的总轮数。
best_acc = 0.0 # 用于记录最佳验证集准确率。
for epoch in range(n_epochs):
# ---------- Training ----------
model.train() # model.train(): 将模型设置为训练模式,启用一些训练特定的模块,比如Dropout。
train_loss = [] # 记录损失
train_accs = [] # 记录精度
.
for batch in tqdm(train_loader):
imgs, labels = batch # 获取图像数据和标签。
imgs = imgs.to(device) # 将数据和标签移动到指定的设备。
labels = labels.to(device)
# 要确保你的模型在同一个设备(GPU)上
logits = model(imgs)
# Calculate the cross-entropy loss.
# We don't need to apply softmax before computing cross-entropy as it is done automatically.
loss = criterion(logits, labels)
optimizer.zero_grad() # 清零之前参数的梯度。
loss.backward() # 计算损失的梯度
optimizer.step() # 更新模型参数。
acc = (logits.argmax(dim=-1) == labels).float().mean() # 计算并记录当前批次的准确率、损失等信息。
# 汇总并输出本轮训练的平均损失和准确率。
train_loss.append(loss.item())
train_accs.append(acc)
# 计算平均损失和精度
train_loss = sum(train_loss) / len(train_loss)
train_acc = sum(train_accs) / len(train_accs)
# Print the information.
print(f"[ Train | {epoch + 1:03d}/{n_epochs:03d} ] loss = {train_loss:.5f}, acc = {train_acc:.5f}")
# ---------- Validation ----------
model.eval() # 将模型切换到评估模式,禁用一些训练特定的模块,以便在验证集上进行评估。
# These are used to record information in validation.
valid_loss = []
valid_accs = []
# 逐批次迭代验证集。
for batch in tqdm(val_loader):
imgs, labels = batch
# 在验证过程中我们不需要梯度。
# 使用torch.no_grad()可以加速前向传播过程。这意味着在验证期间不会记录参数的梯度,这样可以节省内存和计算资源。
with torch.no_grad():
logits = model(imgs.to(device))
# 我们仍然可以计算损失(但不会计算梯度)。这表示我们仍然可以评估模型的性能,但不会修改模型参数。
loss = criterion(logits, labels.to(device))
# 对当前epoch计算精度
acc = (logits.argmax(dim=-1) == labels.to(device)).float().mean()
# 记录损失和精度
valid_loss.append(loss.item())
valid_accs.append(acc)
# 整个验证集的平均损失和准确率是记录值的平均值。这表示对验证集中所有样本的损失和准确率进行平均计算,以得出整体性能指标。
valid_loss = sum(valid_loss) / len(valid_loss)
valid_acc = sum(valid_accs) / len(valid_accs)
# Print the information.
print(f"[ Valid | {epoch + 1:03d}/{n_epochs:03d} ] loss = {valid_loss:.5f}, acc = {valid_acc:.5f}")
# 如果本轮验证的准确率比历史最佳准确率更高,就保存模型的权重到文件
if valid_acc > best_acc:
best_acc = valid_acc
torch.save(model.state_dict(), model_path)
print('saving model with acc {:.3f}'.format(best_acc))
## 预测
model = res_model(176)
model = model.to(device)
model.load_state_dict(torch.load(model_path)) # 加载之前保存的最佳权重参数(checkpoint)。
# 确保模型处于评估模式(eval mode)。在评估模式下,模型的某些模块,如Dropout或BatchNorm,会影响模型的行为。
model.eval()
# 初始化一个列表以存储预测结果。
predictions = []
# 迭代测试集,逐批次进行预测。
for batch in tqdm(test_loader):
imgs = batch
with torch.no_grad():
logits = model(imgs.to(device))
# 选择具有最大对数净值的类别作为预测,并将其记录下来。
predictions.extend(logits.argmax(dim=-1).cpu().numpy().tolist())
preds = []
for i in predictions:
preds.append(num_to_class[i]) # 将所有的预测结果(类别索引)转换为类别标签,并将这些预测结果与测试集的图像文件名结合在一起。
test_data = pd.read_csv(test_path)
test_data['label'] = pd.Series(preds)
submission = pd.concat([test_data['image'], test_data['label']], axis=1) # 将预测结果保存到一个CSV文件中(saveFileName)。
submission.to_csv(saveFileName, index=False)
print("Done!!!!!!!!!!!!!!!!!!!!!!!!!!!")
最后的预测结果