【深度学习实战:kaggle自然场景的图像分类-----使用keras框架实现vgg16的迁移学习】
项目简介
本次数据集来自kaggle,该数据集包括自然场景的图像。模型应该预测每个图像的正确标签。
您的目标是实现分类问题的高精度。
数据集
train.csv - 训练集
test.csv - 测试集
SceneImages - 图像文件夹
训练集的数据格式如下:
image_name | label |
---|---|
0.jpg | 0 |
1.jpg | 4 |
2.jpg | 5 |
7.jpg | 4 |
8.jpg | 1 |
9.jpg | 5 |
一共有7000多张风景图。
迁移学习
迁移学习的原理
迁移学习(Transfer Learning)是一种机器学习方法,它的核心思想是将已经学到的知识应用到新的任务上。这就像是从一个领域的经验中借鉴来帮助解决另一个相关的任务。
生活中的类比
想象你已经学会了骑自行车。现在你需要学骑摩托车,尽管这两者有所不同,但你已经掌握了保持平衡、控制方向等技能。因此,你学骑摩托车时会比从零开始快很多。
在机器学习中,迁移学习的过程也类似:它利用在一个任务上训练得到的模型,来加速另一个相关任务的学习。
为什么使用迁移学习?
- 数据有限:
有时候,收集和标注大量数据是非常困难的。迁移学习可以帮助你在数据不充足的情况下,仍然训练出一个高性能的模型。 - 节省计算资源:
训练一个深度学习模型需要大量的计算资源和时间。迁移学习通过使用已有的预训练模型,避免了从头开始训练的巨大开销。 - 提高模型表现:
预训练模型已经学到了一些基础特征(例如图像的边缘、颜色、形状等),这些特征可以被迁移到新任务中,从而提升模型的准确度。
迁移学习的流程
-
选择预训练模型:
首先,选择一个在大型数据集(如 ImageNet)上训练好的模型。这个模型已经学会了很多通用的特征,如图像中的边缘、颜色等。
微调模型: -
然后,将这个预训练模型应用到你的任务中。你可以对模型进行“微调”——即保留大部分已经学到的知识,只调整最后几层,或者仅训练最后一层来适应你的任务。
优化和调整: -
根据具体任务,你可能还需要调整模型的参数或网络结构,以进一步提升性能。
迁移学习的实际应用
图像分类:许多人使用在大规模数据集(如 ImageNet)上训练的预训练模型,解决特定的图像分类任务(如植物识别、动物分类等)。这些模型已学到的图像特征在新任务中同样有效。
自然语言处理(NLP):类似地,像 BERT、GPT 等预训练的语言模型已经从大量文本数据中学习了语言的结构和模式,这些知识可以快速应用到具体任务(如情感分析、文本生成等)。
总结
迁移学习是一种通过借用在一个任务中获得的知识,来帮助解决另一个相关任务的方法。它能显著加速模型的训练过程,特别适用于数据稀缺或计算资源有限的情况。就像骑自行车的经验可以帮助你更快学会骑摩托车,迁移学习同样能让你在新的任务上事半功倍。
Vgg16
VGG16 是一种非常流行的卷积神经网络(CNN)架构,由 Visual Geometry Group (VGG) 提出,通常用于图像分类任务。它以其简单、易理解且效果优异的网络结构闻名,尤其在大规模图像数据集(如 ImageNet)上取得了显著的成功。
- VGG16 的架构
VGG16 网络包含 16 个层,具体包括:
13 个卷积层:每个卷积层使用大小为 3x3 的卷积核,步幅(stride)为 1,并且有 padding 保证图像尺寸不变。
3 个全连接层:这三个全连接层用于分类,每个全连接层后面跟一个 ReLU 激活函数。
最大池化层(MaxPooling):每两个卷积层后面接一个最大池化层,池化窗口大小为 2x2,步幅为 2。
Softmax 层:用于最终的分类输出。
本次迁移学习使用vgg16为例,再其架构上进行部分微调以适用于本次任务。
源码及解释
首先导入需要的库
import os
import pandas as pd
import numpy as np
from tensorflow.keras.preprocessing.image import ImageDataGenerator, load_img, img_to_array
from tensorflow.keras.applications import VGG16
from tensorflow.keras.models import Model, Sequential
from tensorflow.keras.layers import Dense, Flatten, Dropout, GlobalAveragePooling2D
from tensorflow.keras.optimizers import Adam
from sklearn.model_selection import train_test_split
from tensorflow.keras.utils import to_categorical
from tensorflow.keras.callbacks import ModelCheckpoint, EarlyStopping
# 路径和文件
data_file = '/kaggle/input/skku-2024-1-machine-learning-third-project/train.csv'
image_dir = '/kaggle/input/skku-2024-1-machine-learning-third-project/SceneImages/'
test_data_file = '/kaggle/input/skku-2024-1-machine-learning-third-project/test.csv'# 加载标签数据
df = pd.read_csv(data_file)
df['image_path'] = df['image_name'].apply(lambda x: os.path.join(image_dir, x))# 加载测试集数据
test_df = pd.read_csv(test_data_file)
test_df['image_path'] = test_df['image_name'].apply(lambda x: os.path.join(image_dir, x))# 打印数据集信息
print("Dataframe head:")
print(df.head())
print(test_df.head())print("Number of unique labels:", df['label'].nunique())
print("Number of samples:", len(df))
print("Number of test samples:", len(test_df))
接下来,加载图像数据并进行预处理(如调整大小、归一化),同时提取对应的标签,最终将图像和标签作为 NumPy 数组返回。这样可以为深度学习模型的训练提供准备好的数据集。
# 图像大小
img_size = (150, 150)
num_classes = df['label'].nunique()# 加载和预处理图像数据
def load_and_preprocess_images(df):images = []labels = []for _, row in df.iterrows():img_path = row['image_path']label = row['label']try:img = load_img(img_path, target_size=img_size)img_array = img_to_array(img) / 255.0 # 归一化images.append(img_array)labels.append(label)except Exception as e:print(f"Error loading image {img_path}: {e}")return np.array(images), np.array(labels)images, labels = load_and_preprocess_images(df)# 验证加载的图像和标签
print("Loaded images shape:", images.shape)
print("Loaded labels shape:", labels.shape)
可以看到原始图像是1501503的。
# 图像大小
img_size = (150, 150)# 加载和预处理图像数据(没有标签)
def load_and_preprocess_images_no_labels(df):images = []for _, row in df.iterrows():img_path = row['image_path']try:img = load_img(img_path, target_size=img_size)img_array = img_to_array(img) / 255.0 # 归一化images.append(img_array)except Exception as e:print(f"Error loading image {img_path}: {e}")return np.array(images)# 加载test数据的图像
test_images = load_and_preprocess_images_no_labels(test_df)# 验证加载的图像形状
print("Loaded images shape:", test_images.shape)
这里就是对test的图像进行预处理。加载和预处理图像数据,并将它们转化为适合输入到深度学习模型的格式。具体来说,就是从每个图像路径加载图像,进行归一化,并将所有图像存储为一个 NumPy 数组。
# 将标签转为独热编码
labels = to_categorical(labels, num_classes=num_classes)# 数据集划分
X_train, X_val, y_train, y_val = train_test_split(images, labels, test_size=0.2, random_state=42)print("Training data shape:", X_train.shape, y_train.shape)
print("Validation data shape:", X_val.shape, y_val.shape)
to_categorical 函数:
to_categorical 是 Keras 库中提供的一个函数,用于将整数标签(通常是分类任务中的标签)转换为独热编码形式。函数签名如下:
to_categorical(y, num_classes=None, dtype=‘float32’)
y:输入的整数标签数组。
num_classes:类别总数。如果未指定,默认为 y 中标签的最大值 + 1。
dtype:输出的数组数据类型,默认是 float32。
为什么要对数值的标签进行独热编码?
独热编码在深度学习中的使用非常普遍,尤其是对于分类问题,原因包括:
-
模型输出格式要求:
在多类别分类任务中,通常希望模型的输出是一个与类别数相同长度的向量,每个元素表示该类别的预测概率。独热编码正好符合这个需求。例如,模型输出 [0.7, 0.1, 0.2] 可能表示模型认为第一个类别的概率为 0.7,第二个类别为 0.1,第三个类别为 0.2。与之对应的标签 [1, 0, 0] 表示真实标签是类别 0。 -
交叉熵损失函数:
在多分类任务中,常使用 交叉熵损失函数(Categorical Crossentropy),这个损失函数要求标签以独热编码形式提供。如果标签不是独热编码形式,使用交叉熵计算损失时会出现错误。 -
避免顺序关系的假设:
在将类别标签转化为数字时(例如:0, 1, 2),模型可能会错误地假设这些数字有某种顺序关系(例如 0 < 1 < 2)。独热编码可以避免这种假设,因为每个类别都是独立的,并且没有显式的顺序。 -
提高模型的训练效果:
独热编码能够帮助模型清晰地学习每个类别的特征,而不是让模型将所有类别当作连续的数字进行处理,进而避免了数字之间的任何潜在顺序关系。
# 数据增强
train_datagen = ImageDataGenerator(rotation_range=20, # 随机旋转图像范围(-20 到 +20 度)width_shift_range=0.2, # 水平平移范围,相对于图像宽度的比例height_shift_range=0.2, # 垂直平移范围,相对于图像高度的比例shear_range=0.2, # 剪切变换的角度范围zoom_range=0.2, # 随机缩放范围horizontal_flip=True, # 随机水平翻转fill_mode='nearest' # 填充新生成区域的方式
)val_datagen = ImageDataGenerator()# 构建数据生成器
train_generator = train_datagen.flow(X_train, y_train, batch_size=32)
val_generator = val_datagen.flow(X_val, y_val, batch_size=32)# 验证生成器输出
batch_images, batch_labels = next(train_generator)
print("Batch images shape:", batch_images.shape)
print("Batch labels shape:", batch_labels.shape)
这里进行了数据增强,并使用 Keras 的 ImageDataGenerator 来动态生成训练和验证数据。具体来说,代码对训练数据应用了多种数据增强技术,以增加数据集的多样性,并创建数据生成器,便于模型在训练过程中批量加载图像。目的是防止模型过拟合,提高模型的泛化能力。
from tensorflow.keras.applications import VGG16
from tensorflow.keras.models import Model
from tensorflow.keras.layers import GlobalAveragePooling2D, Dense, Dropout, Inputnum_classes = 6# 创建输入层
input_tensor = Input(shape=(150, 150, 3))# 加载VGG16模型,不包含顶层(即不包含全连接层),并设置输入形状
base_model = VGG16(weights='imagenet', include_top=False, input_tensor=input_tensor)# 冻结VGG16的卷积层
for layer in base_model.layers:layer.trainable = False# 添加全连接层和输出层
x = GlobalAveragePooling2D()(base_model.output) # 池化层,转化为一个向量
x = Dense(256, activation='relu')(x) # 全连接层
x = Dropout(0.3)(x) # Dropout层,防止过拟合
x = Dense(num_classes, activation='softmax')(x) # 输出层,用于多分类# 构建模型
model = Model(inputs=input_tensor, outputs=x)# 打印模型摘要
model.summary()
首先,我们加载了一个已经在 ImageNet 数据集上训练好的 VGG16 模型,只保留它的卷积层部分,这部分能提取图像中的特征(比如边缘、形状、颜色等)。然后,我们在这些卷积层的基础上添加了一些自定义的层,帮助模型进行特定的分类任务。具体来说,我们加入了一个全连接层、一个 Dropout 层(用于防止过拟合)以及最终的输出层,用来预测图像属于 6 个类别中的哪一个。
Total params: 14,847,558 (56.64 MB)
Trainable params: 132,870 (519.02 KB)
Non-trainable params: 14,714,688 (56.13 MB)
可以看到参数量较大,这也是vgg的一个缺点吧,ResNet 引入了残差连接(skip connections)来避免梯度消失问题,减少了计算量和参数数量。想联系的伙伴可以用ResNet 试试。
from tensorflow.keras.callbacks import ModelCheckpoint, EarlyStopping
# 编译模型
model.compile(optimizer=Adam(learning_rate=0.0001),loss='categorical_crossentropy',metrics=['accuracy'])
# 检查点和早停
checkpoint = ModelCheckpoint('best_model.keras', monitor='val_accuracy', save_best_only=True, mode='max')
early_stopping = EarlyStopping(monitor='val_loss', patience=10, restore_best_weights=True)print("Callbacks set up successfully.")
这里使用交叉熵损失函数和Adam优化器,学习率设置了一个较小的值,这里大家可以将学习率进行动态调整,还加入的早停,防止过拟合。
# 训练模型
history = model.fit(train_generator,validation_data=val_generator,epochs=60,callbacks=[checkpoint, early_stopping]
)
# 保存最终模型
model.save('final_model.keras')print("Final model saved as 'final_model.keras'.")
训练模型并进行保存。
import matplotlib.pyplot as plt
# 在验证集上评估模型
val_loss, val_acc = model.evaluate(X_val, y_val)
print(f"Validation Loss: {val_loss:.4f}, Validation Accuracy: {val_acc:.4f}")# 绘制训练曲线
plt.figure(figsize=(12, 5))# Loss 曲线
plt.subplot(1, 2, 1)
plt.plot(history.history['loss'], label='Training Loss')
plt.plot(history.history['val_loss'], label='Validation Loss')
plt.title('Loss Curve')
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.legend()# Accuracy 曲线
plt.subplot(1, 2, 2)
plt.plot(history.history['accuracy'], label='Training Accuracy')
plt.plot(history.history['val_accuracy'], label='Validation Accuracy')
plt.title('Accuracy Curve')
plt.xlabel('Epochs')
plt.ylabel('Accuracy')
plt.legend()plt.show()
通过损失图和准确率的图可以看到:
- 训练准确率和验证准确率相近,说明模型在训练集和验证集上表现一致,没有明显的过拟合问题。过拟合的表现通常是训练准确率高而验证准确率低。
- 训练损失和验证损失也相近,表明模型的学习在训练集和验证集上都有较好的效果。
from tensorflow.keras.models import load_model
# 加载训练好的模型
model = load_model('final_model.keras')# 对 test_images 进行预测
predictions = model.predict(test_images) # 获取预测的标签(通过 np.argmax 获取每个样本的类别)
predicted_labels = np.argmax(predictions, axis=1)# 将预测结果与 image_name 合并
test_df['label'] = predicted_labels# 将 'image_name' 列设置为索引
output_df = test_df[['image_name', 'label']].set_index('image_name')# 打印输出
print(output_df)# 保存到 CSV 文件,包含索引
output_df.to_csv('predicted.csv')
通过最后一段,加载模型并预测,输出预测值,到这里本期的分享就结束了。
感谢大家阅读我的博客!非常高兴能够与大家分享一些有价值的知识和经验,希望这些内容对你们有所帮助。如果你觉得我的文章对你有启发,请不要忘记点赞和关注,这对我来说是巨大的支持和鼓励。同时,我也非常欢迎大家在评论区进行交流和讨论,分享你们的观点和经验,让我们一起进步,共同学习!再次感谢大家的关注,期待与大家的互动!