机器学习方法实现水果分类(零基础小白向),手工提取水果图片特征,不一样的特征提取方法

零、前言:

在本篇博客中,我们将探讨如何使用机器学习来实现水果分类,重点介绍如何手工提取水果图片特征,以及不同的特征提取方法。这对于初学者来说,了解这些概念是入门机器学习的基础。

一、什么是特征向量?

特征向量是描述数据(如图片)特征的一组数值。在水果分类中,每张图片可以转换为一个特征向量,这个向量包含了描述图片内容的数值信息。例如,颜色、形状和纹理等都可以被转化为数值,从而形成特征向量。


重要的一点是:特征向量的提取方法有多种,也就是说,一张图片,按照一种方法可以提取出来一个特征向量,按照另一种方法可以提取出来另一个特征向量,他们不冲突,只不过描述特征的角度不一样罢了


二、数据准备

数据集如下: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的核心库,DatasetDataLoader用于处理数据,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,感谢你的点赞与关注,我们共同进步!
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

.别止步春天.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值