文章目录
- 零、前言:
- 一、什么是特征向量?
- 二、数据准备
- 三、构建数据集
- 1、什么是数据集?
- 2、图片对应的标签?
- 3、特征提取方法
- (1)生成固定正方形
- (2)特征提取实现
- 四、建立模型
- 五、自定义数据集类
- 六、训练模型
- 七、输入图片进行测试
- 八、结语
零、前言:
在本篇博客中,我们将探讨如何使用机器学习来实现水果分类,重点介绍如何手工提取水果图片特征,以及不同的特征提取方法。这对于初学者来说,了解这些概念是入门机器学习的基础。
一、什么是特征向量?
特征向量是描述数据(如图片)特征的一组数值。在水果分类中,每张图片可以转换为一个特征向量,这个向量包含了描述图片内容的数值信息。例如,颜色、形状和纹理等都可以被转化为数值,从而形成特征向量。
重要的一点是:特征向量的提取方法有多种,也就是说,一张图片,按照一种方法可以提取出来一个特征向量,按照另一种方法可以提取出来另一个特征向量,他们不冲突,只不过描述特征的角度不一样罢了
二、数据准备
数据集如下:kaggle水果分类数据集
这个数据集有100多类水果
方便起见,我只选择其中14中进行分类。
目录结构如下:dataset文件夹包含train,test两个文件夹
点开test或train,里面包含我选出来的14类水果
随便点开一个水果文件夹看看样子:
三、构建数据集
1、什么是数据集?
数据集是机器学习中用来训练和测试模型的一组数据。可以把它想象成一本书,书中包含了许多章节,每个章节都提供了不同的信息。在水果分类的例子中,数据集可能包含大量水果的图片,每张图片都带有标签(比如“苹果”、“香蕉”等)。这些图片和标签一起构成了模型学习的基础,让模型能够识别和分类不同类型的水果。数据集通常分为两个部分:训练集(用于训练模型)和测试集(用于评估模型的效果)。
2、图片对应的标签?
是的,训练模型的时候,数据集的格式为:一张图片对应一个标签,进一步说:一个特征向量对应一个标签,这就叫监督学习。
可以这么理解:训练的时候模型收到一张图片,提取它的特征向量,发现:他的特征向量长这样,模型学习了成百上千张图片之后,知道了:原来有这种特征向量的图片就是苹果,那种特征向量就是香蕉啊。
于是,一个训练好的模型诞生了,说白了,一个训练好的模型,就是一个权重文件,pytorch框架下,权重文件后缀为.pth
首先,我们需要准备我们的水果图片数据集。我们将通过以下函数获取图片路径和对应的标签:
import os
import json
def get_image_paths(root_dir):
image_paths = []
labels = []
for fruit_dir in os.listdir(root_dir):
fruit_path = os.path.join(root_dir, fruit_dir)
if os.path.isdir(fruit_path):
label = fruit_dir # 使用文件夹名作为标签
for img_file in os.listdir(fruit_path):
if img_file.endswith(('png', 'jpg', 'jpeg')):
image_paths.append((os.path.join(fruit_path, img_file), label))
return image_paths
train_dir = "dataset/train"
test_dir = "dataset/test"
train_image_paths = get_image_paths(train_dir)
test_image_paths = get_image_paths(test_dir)
3、特征提取方法
我的特征提取方法有点特殊,先来一个总体介绍:
首先随机在图片上选择两个点,以这2个点为中心画2个随机大小的正方形,分别计算正方形1和正方形2区域内的像素之和,将两者相减,若大于0,则记录为1,小于零则记录为0,选取n对这样的正方形进行计算,得到一个包含n个数字(0/1)的特征向量。也就是一个图片对应一个特征向量。
最重要的是:要记录随机选取正方形的顺序和大小,然后用同样的选取取样方法对所有图片样本进行特征提取。因为一定要用同样的选择的流程进行特征提取,图片之间才有可比性!
开始提取
我们采用固定正方形区域进行特征提取,这意味着我们将在图片上随机选择固定区域,计算这些区域的平均值,来判断它们之间的差异。
(1)生成固定正方形
import random
def generate_fixed_squares(num_pairs, image_size):
width, height = image_size
squares = []
for _ in range(num_pairs):
x1, y1 = random.randint(0, width-1), random.randint(0, height-1)
x2, y2 = random.randint(0, width-1), random.randint(0, height-1)
size1 = random.randint(1, min(width, height) // 2)
size2 = random.randint(1, min(width, height) // 2)
squares.append(((x1, y1, size1), (x2, y2, size2)))
return squares
# 假设图像大小为100×100,生成100对正方形的选择方案
num_pairs =300
image_size = (100, 100) # 假设图片大小固定为 100×100
fixed_squares = generate_fixed_squares(num_pairs, image_size)
# 保存方案到JSON文件
with open('fixed_squares.json', 'w') as f:
json.dump(fixed_squares, f, indent=4)
(2)特征提取实现
我们将利用这些固定区域来提取特征:
import numpy as np
from PIL import Image
# 使用固定正方形提取图片的特征
def extract_features_with_fixed_squares(image, squares):
width, height = image.size
image = np.array(image)
features = []
for square1, square2 in squares:
x1, y1, size1 = square1
x2, y2, size2 = square2
# 限制正方形在图片内
square1_region = image[max(0, y1-size1//2):min(height, y1+size1//2), max(0, x1-size1//2):min(width, x1+size1//2)]
square2_region = image[max(0, y2-size2//2):min(height, y2+size2//2), max(0, x2-size2//2):min(width, x2+size2//2)]
# 计算区域的平均值并记录差异为 0 或 1
mean1 = np.mean(square1_region)
mean2 = np.mean(square2_region)
features.append(1 if mean1 > mean2 else 0)
return features
# 处理整个数据集,使用固定的正方形选择方案
def process_images_with_fixed_squares(image_paths, squares):
all_features = []
all_labels = []
for image_path, label in image_paths:
# image = Image.open(image_path).convert("L") # 转为灰度图
image = Image.open(image_path) # 保留RGB
features = extract_features_with_fixed_squares(image, squares)
all_features.append(features)
all_labels.append(label)
return np.array(all_features), np.array(all_labels)
# 使用固定的正方形选择方案提取训练集和测试集特征
train_features, train_labels = process_images_with_fixed_squares(labeled_train_image_paths, fixed_squares)
test_features, test_labels = process_images_with_fixed_squares(labeled_test_image_paths, fixed_squares)
四、建立模型
接下来,我们将使用多层感知机(MLP)作为分类模型。MLP是一种基础的神经网络,适合处理分类问题。
model.py代码如下:
import torch.nn as nn
import torch
# 定义 MLP 模型
class MLP(nn.Module):
def __init__(self, input_size, hidden_size, num_classes):
super(MLP, self).__init__()
self.fc1 = nn.Linear(input_size, hidden_size)
self.relu = nn.ReLU()
self.fc2 = nn.Linear(hidden_size, num_classes)
def forward(self, x):
x = self.fc1(x)
x = self.relu(x)
x = self.fc2(x)
return x
在这里,我仍然希望对上述模型代码进行逐行解释,我认为对于小白来说是有必要的,有助于更好地理解什么是机器学习,什么是神经网络。
这是一个简单的多层感知机(MLP)模型,通常用于分类任务。以下是逐行解释:
import torch
from torch.utils.data import Dataset, DataLoader
import torch.nn as nn
import torch.optim as optim
- 这些是导入需要的库。
torch
是PyTorch的核心库,Dataset
和DataLoader
用于处理数据,nn
用于构建神经网络,optim
用于优化算法。
class MLP(nn.Module):
- 定义一个名为
MLP
的类,继承自nn.Module
,表示这是一个神经网络模型。
def __init__(self, input_size, hidden_size, num_classes):
- 初始化方法,接受三个参数:
input_size
(输入特征的维度)、hidden_size
(隐藏层的神经元数量)、num_classes
(输出类别的数量)。
super(MLP, self).__init__()
- 调用父类的初始化方法,以确保所有父类的属性被正确初始化。
self.fc1 = nn.Linear(input_size, hidden_size)
- 定义第一个全连接层(线性层),从输入层到隐藏层,输入特征数量为
input_size
,输出特征数量为hidden_size
。
self.relu = nn.ReLU()
- 使用ReLU激活函数,这是一种常用的非线性激活函数,用于增加模型的表达能力。
self.fc2 = nn.Linear(hidden_size, num_classes)
- 定义第二个全连接层,从隐藏层到输出层,输入特征数量为
hidden_size
,输出特征数量为num_classes
,即最终的分类结果。
def forward(self, x):
- 定义前向传播方法,接收输入数据
x
。
out = self.fc1(x)
- 将输入数据通过第一个全连接层,得到隐藏层的输出。
out = self.relu(out)
- 对隐藏层的输出应用ReLU激活函数,增加非线性特性。
out = self.fc2(out)
- 将经过激活的输出传递到第二个全连接层,得到最终的输出。
return out
- 返回模型的输出,通常是对每个类别的得分或概率,用于进行分类决策。
这个模型通过简单的三层结构,能够学习输入特征与分类标签之间的关系,适用于简单的分类任务。之后可能会有更多层的网络。不过这个任务比较简单,三层就够了。
五、自定义数据集类
# 自定义数据集类
class FruitFeatureDataset(Dataset):
def __init__(self, features, labels):
self.features = features
self.labels = labels
def __len__(self):
return len(self.features)
def __getitem__(self, idx):
return torch.tensor(self.features[idx], dtype=torch.float32), torch.tensor(self.labels[idx], dtype=torch.long)
同样逐行解释:
这是一个自定义数据集类,用于处理水果特征和标签。以下是逐行解释:
# 自定义数据集类
class FruitFeatureDataset(Dataset):
- 定义一个名为
FruitFeatureDataset
的类,继承自Dataset
,表示这是一个自定义数据集。
def __init__(self, features, labels):
- 初始化方法,接受两个参数:
features
(特征数据)和labels
(对应的标签)。
self.features = features
self.labels = labels
- 将传入的特征和标签保存为类的属性,以便在其他方法中使用。
def __len__(self):
- 定义一个方法,用于返回数据集的大小。
return len(self.features)
- 返回特征数组的长度,即数据集中样本的数量。
def __getitem__(self, idx):
- 定义一个方法,通过索引
idx
获取特定样本的特征和标签。
return torch.tensor(self.features[idx], dtype=torch.float32), torch.tensor(self.labels[idx], dtype=torch.long)
- 将特征和标签转换为PyTorch张量(tensor),特征类型为
float32
(浮点数),标签类型为long
(整数),并返回这对特征和标签。这使得数据可以被模型直接使用。
这个自定义数据集类的主要目的是方便地将特征和标签组织在一起,并提供索引访问功能,以便在训练过程中使用DataLoader
进行批处理。
六、训练模型
内有每行注释
import torch
from torch.utils.data import Dataset, DataLoader
import torch.nn as nn
import torch.optim as optim
from model import MLP
# 训练模型
def train_model(model, train_loader, criterion, optimizer, num_epochs=30):
# 遍历每个训练轮次
for epoch in range(num_epochs):
model.train() # 设置模型为训练模式
running_loss = 0.0 # 初始化当前轮次的损失为0
# 遍历训练数据加载器
for inputs, labels in train_loader:
# 前向传播,获取模型输出
outputs = model(inputs)
# 计算损失值
loss = criterion(outputs, labels)
# 反向传播和优化
optimizer.zero_grad() # 清空梯度
loss.backward() # 计算梯度
optimizer.step() # 更新模型参数
running_loss += loss.item() # 累加当前批次的损失
# 输出当前轮次的平均损失
print(f"Epoch [{epoch+1}/{num_epochs}], Loss: {running_loss/len(train_loader):.4f}")
# 测试模型
def test_model(model, test_loader):
model.eval() # 设置模型为评估模式
correct = 0 # 初始化正确预测的数量
total = 0 # 初始化总预测的数量
with torch.no_grad(): # 不计算梯度
# 遍历测试数据加载器
for inputs, labels in test_loader:
outputs = model(inputs) # 前向传播,获取输出
_, predicted = torch.max(outputs.data, 1) # 获取预测的类别
total += labels.size(0) # 更新总样本数
correct += (predicted == labels).sum().item() # 统计正确预测的数量
# 输出测试集的准确率
print(f'Accuracy: {100 * correct / total:.2f}%')
# 创建 DataLoader
batch_size = 32 # 定义批次大小
train_dataset = FruitFeatureDataset(train_features, train_labels) # 创建训练数据集
test_dataset = FruitFeatureDataset(test_features, test_labels) # 创建测试数据集
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True) # 创建训练数据加载器
test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False) # 创建测试数据加载器
# 初始化模型参数
input_size = train_features.shape[1] # 获取特征向量的长度
hidden_size = 256 # 定义隐藏层的神经元数量
num_classes = len(unique_labels) # 获取输出类别的数量
print(input_size) # 输出特征向量的长度
model = MLP(input_size, hidden_size, num_classes) # 初始化模型
# 定义损失函数和优化器
criterion = nn.CrossEntropyLoss() # 使用交叉熵损失函数
optimizer = optim.Adam(model.parameters(), lr=0.001) # 使用Adam优化器
# 训练和测试模型
train_model(model, train_loader, criterion, optimizer, num_epochs=40) # 训练模型
test_model(model, test_loader) # 测试模型
# 在训练完成后,保存模型
torch.save(model.state_dict(), "fruit_classifier_mlp.pth") # 保存模型参数
print("哈哈哈模型已保存为 fruit_classifier_mlp.pth") # 输出保存成功的信息
七、输入图片进行测试
test.py内容如下:
import torch
from PIL import Image
import numpy as np
import json
from model import MLP # 导入模型定义
import os
# 定义模型参数
input_size = 100 # 示例输入尺寸
hidden_size = 256 # 示例隐藏层尺寸
num_classes = 14 # 示例类别数
# 加载模型
model = MLP(input_size, hidden_size, num_classes)
model.load_state_dict(torch.load("fruit_classifier_mlp.pth"))
model.eval()
# 读取 JSON 文件
with open('fixed_squares.json', 'r') as f:
fixed_squares_list = json.load(f)
# 将列表转换为元组
fixed_squares = [tuple(map(tuple, pair)) for pair in fixed_squares_list]
# 使用固定正方形提取图片的特征
def extract_features_with_fixed_squares(image, squares):
width, height = image.size
image = np.array(image)
features = []
for square1, square2 in squares:
x1, y1, size1 = square1
x2, y2, size2 = square2
# 限制正方形在图片内
square1_region = image[max(0, y1-size1//2):min(height, y1+size1//2), max(0, x1-size1//2):min(width, x1+size1//2)]
square2_region = image[max(0, y2-size2//2):min(height, y2+size2//2), max(0, x2-size2//2):min(width, x2+size2//2)]
# 计算区域的平均值并记录差异为 0 或 1
mean1 = np.mean(square1_region)
mean2 = np.mean(square2_region)
features.append(1 if mean1 > mean2 else 0)
return features
# 定义图像预处理函数
def preprocess_image(image_path, squares):
image = Image.open(image_path) # 转为灰度图
features = extract_features_with_fixed_squares(image, squares)
return np.array(features)
# 输入一张图片进行测试
def predict_image(image_path, squares, label_map):
features = preprocess_image(image_path, squares)
features_tensor = torch.tensor(features, dtype=torch.float32).unsqueeze(0) # 增加一个维度
with torch.no_grad():
outputs = model(features_tensor)
_, predicted = torch.max(outputs, 1)
# 获取预测的标签
predicted_label = [label for label, idx in label_map.items() if idx == predicted.item()]
return predicted_label[0] if predicted_label else None
# 批量预测函数
def predict_images(folder_path, squares, label_map):
predictions = {}
# 获取文件夹中的所有图片
image_paths = [os.path.join(folder_path, filename) for filename in os.listdir(folder_path) if filename.endswith(".jpg")]
# 预处理并预测每张图片
for image_path in image_paths:
features = preprocess_image(image_path, squares)
features_tensor = torch.tensor(features, dtype=torch.float32).unsqueeze(0) # 增加一个维度
with torch.no_grad():
outputs = model(features_tensor)
_, predicted = torch.max(outputs, 1)
# 获取预测的标签
predicted_label = [label for label, idx in label_map.items() if idx == predicted.item()][0]
predictions[image_path] = predicted_label
return predictions
label_map={'Apple Braeburn 1': 0, 'Apricot 1': 1, 'Avocado 1': 2, 'Avocado ripe 1': 3, 'Banana 1': 4, 'Beetroot 1': 5, 'Blueberry 1': 6, 'Cabbage white 1': 7, 'Cactus fruit 1': 8, 'Strawberry Wedge 1': 9, 'Tamarillo 1': 10, 'Tangelo 1': 11, 'Tomato 1': 12, 'Walnut 1': 13}
# 测试
# 单个预测
# test_image_path="dataset\\test\\Walnut 1\\12_100.jpg"
# predicted_fruit = predict_image(test_image_path, fixed_squares, label_map)
# print(f"预测的水果类型: {predicted_fruit}")
#批量预测
# 测试文件夹路径
folder_path = "dataset/test/Walnut 1"
# 批量预测
predictions = predict_images(folder_path, fixed_squares, label_map)
# 输出预测结果
for image_path, predicted_fruit in predictions.items():
print(f"文件 {image_path} 预测的水果类型: {predicted_fruit}")
八、结语
通过本文的介绍,您应该对使用机器学习进行水果分类的过程有了初步了解。从数据准备到特征提取,再到模型训练,每一步都至关重要。希望这篇博客能为您的机器学习之旅提供帮助,激励您深入探索这一领域的无限可能!
我的代码并不完美,有一些不自动化的地方,有点懒不想改了(bushi)
anyway,感谢你的点赞与关注,我们共同进步!