20、基于卷积神经网络的大规模图像分类实战

基于卷积神经网络的大规模图像分类实战

1. 项目背景与问题描述

如今,美食自拍和以照片为中心的社交分享成为社交趋势。美食爱好者会在社交媒体和相关网站上上传大量美食和餐厅照片,并附上文字评论,这能显著提升餐厅的知名度。以 Yelp 为例,数百万独立访客访问该平台,撰写了超过 1.35 亿条评论,同时上传了大量照片。Yelp 通过向当地商家出售广告盈利,这些照片蕴含了丰富的本地商业信息。

本项目的挑战在于如何将这些图片转化为文字,即构建一个模型,能够自动为餐厅用户提交的照片添加多个标签,从而预测商业属性。

2. 图像数据集描述

为完成这一挑战,我们需要真实的数据集。Kaggle 是一个提供此类数据集的平台,可在 https://www.kaggle.com/c/yelp-restaurant-photo-classification 找到 Yelp 数据集及其描述。

餐厅的标签由 Yelp 用户在提交评论时手动选择,数据集中有 Yelp 社区标注的九种不同标签:
| 标签编号 | 标签含义 |
| ---- | ---- |
| 0 | good_for_lunch |
| 1 | good_for_dinner |
| 2 | takes_reservations |
| 3 | outdoor_seating |
| 4 | restaurant_is_expensive |
| 5 | has_alcohol |
| 6 | has_table_service |
| 7 | ambience_is_classy |
| 8 | good_for_kids |

数据集包含六个文件:
- train_photos.tgz:用作训练集的照片(234,545 张图像)
- test_photos.tgz:用作测试集的照片(500 张图像)
- train_photo_to_biz_ids.csv:提供照片 ID 到商业 ID 的映射(234,545 行)
- test_photo_to_biz_ids.csv:提供照片 ID 到商业 ID 的映射(500 行)
- train.csv:主要训练数据集,包括商业 ID 及其相应标签(1996 行)
- sample_submission.csv:示例提交文件,为预测提供正确格式,包括 business_id 和相应的预测标签

3. 整体项目工作流程

3.1 图像预处理

  • 读取图像 :将 .jpg 格式的图像读取为 Scala 中的矩阵表示。
  • 图像操作 :对图像进行一系列操作,如将所有图像变为正方形、调整大小为相同维度,最后应用灰度滤镜。
graph LR
    A[读取图像] --> B[图像变正方形]
    B --> C[调整图像大小]
    C --> D[应用灰度滤镜]

3.2 模型训练

  • 训练 CNN :为每个类别在训练数据上训练九个 CNN。
  • 保存模型 :训练完成后,保存训练好的模型、CNN 配置和参数。

3.3 模型评估

  • 聚合分类 :应用简单的聚合函数为每个餐厅分配类别。
  • 测试评分 :对测试数据进行评分,并使用测试图像评估模型。

4. 实现 CNN 进行图像分类

4.1 工作流程步骤

  1. 从 train.csv 文件中读取所有商业标签。
  2. 读取并创建从图像 ID 到商业 ID 的映射(imageID → busID)。
  3. 从 photoDir 目录获取要加载和处理的图像列表,并获取 10,000 张图像的 ID(可自定义范围)。
  4. 将图像读取并处理为 photoID → vector 映射。
  5. 链接步骤 3 和步骤 4 的输出,对齐商业特征、图像 ID 和标签 ID,为 CNN 提取特征。
  6. 构建九个 CNN。
  7. 训练所有 CNN 并指定模型保存位置。
  8. 重复步骤 2 到步骤 6,从测试集中提取特征。
  9. 评估模型并将预测结果保存到 CSV 文件。

4.2 代码实现

val labelMap = readBusinessLabels("data/labels/train.csv")
val businessMap =
  readBusinessToImageLabels("data/labels/train_photo_to_biz_ids.csv")
val imgs = getImageIds("data/images/train/", businessMap,
  businessMap.map(_._2).toSet.toList).slice(0,100) // 20000 images
println("Image ID retreival done!")
val dataMap = processImages(imgs, resizeImgDim = 128)
println("Image processing done!")
val alignedData = new featureAndDataAligner(dataMap, businessMap,
  Option(labelMap))()
println("Feature extraction done!")
val cnn0 = trainModelEpochs(alignedData, businessClass = 0, saveNN =
  "models/model0")
val cnn1 = trainModelEpochs(alignedData, businessClass = 1, saveNN =
  "models/model1")
val cnn2 = trainModelEpochs(alignedData, businessClass = 2, saveNN =
  "models/model2")
val cnn3 = trainModelEpochs(alignedData, businessClass = 3, saveNN =
  "models/model3")
val cnn4 = trainModelEpochs(alignedData, businessClass = 4, saveNN =
  "models/model4")
val cnn5 = trainModelEpochs(alignedData, businessClass = 5, saveNN =
  "models/model5")
val cnn6 = trainModelEpochs(alignedData, businessClass = 6, saveNN =
  "models/model6")
val cnn7 = trainModelEpochs(alignedData, businessClass = 7, saveNN =
  "models/model7")
val cnn8 = trainModelEpochs(alignedData, businessClass = 8, saveNN =
  "models/model8")
val businessMapTE =
  readBusinessToImageLabels("data/labels/test_photo_to_biz.csv")
val imgsTE = getImageIds("data/images/test//", businessMapTE,
  businessMapTE.map(_._2).toSet.toList)
val dataMapTE = processImages(imgsTE, resizeImgDim = 128) // make them 128*128
val alignedDataTE = new featureAndDataAligner(dataMapTE, businessMapTE,
  None)()
val Results = SubmitObj(alignedDataTE, "results/ModelsV0/")
val SubmitResults = writeSubmissionFile("kaggleSubmitFile.csv", Results,
  thresh = 0.9)

5. 图像预处理

5.1 图像形状处理

由于 CNN 无法处理大小和形状各异的图像,我们需要对图像进行预处理。首先将不规则形状的图像变为正方形,代码如下:

def makeSquare(img: java.awt.image.BufferedImage):
  java.awt.image.BufferedImage = {
  val w = img.getWidth
  val h = img.getHeight
  val dim = List(w, h).min
  img match {
    case x if w == h => img // do nothing and returns the original one
    case x if w > h => Scalr.crop(img, (w - h) / 2, 0, dim, dim)
    case x if w < h => Scalr.crop(img, 0, (h - w) / 2, dim, dim)
  }
}

5.2 图像大小调整

将所有图像调整为 128 x 128 大小,代码如下:

def resizeImg(img: java.awt.image.BufferedImage, width: Int, height: Int) =
{
  Scalr.resize(img, Scalr.Method.BALANCED, width, height)
}

5.3 灰度转换

为简化计算,将图像转换为灰度图像,代码如下:

def pixels2Gray(R: Int, G: Int, B: Int): Int = (R + G + B) / 3
def makeGray(testImage: java.awt.image.BufferedImage):
  java.awt.image.BufferedImage = {
  val w = testImage.getWidth
  val h = testImage.getHeight
  for {
    w1 <- (0 until w).toVector
    h1 <- (0 until h).toVector
  }
  yield
  {
    val col = testImage.getRGB(w1, h1)
    val R = (col & 0xff0000) / 65536
    val G = (col & 0xff00) / 256
    val B = (col & 0xff)
    val graycol = pixels2Gray(R, G, B)
    testImage.setRGB(w1, h1, new Color(graycol, graycol, graycol).getRGB)
  }
  testImage
}

5.4 综合处理

将上述三个步骤链在一起,代码如下:

import scala.Vector
import org.imgscalr._
object imageUtils {
  implicit class imageProcessingPipeline(img:
    java.awt.image.BufferedImage) {
    // image 2 vector processing
    def pixels2gray(R: Int, G:Int, B: Int): Int = (R + G + B) / 3
    def pixels2color(R: Int, G:Int, B: Int): Vector[Int] = Vector(R, G, B)
    private def image2vec[A](f: (Int, Int, Int) => A ): Vector[A] = {
      val w = img.getWidth
      val h = img.getHeight
      for {
        w1 <- (0 until w).toVector
        h1 <- (0 until h).toVector
      }
      yield {
        val col = img.getRGB(w1, h1)
        val R = (col & 0xff0000) / 65536
        val G = (col & 0xff00) / 256
        val B = (col & 0xff)
        f(R, G, B)
      }
    }
    def image2gray: Vector[Int] = image2vec(pixels2gray)
    def image2color: Vector[Int] = image2vec(pixels2color).flatten
    // make image square
    def makeSquare = {
      val w = img.getWidth
      val h = img.getHeight
      val dim = List(w, h).min
      img match {
        case x if w == h => img
        case x if w > h => Scalr.crop(img, (w-h)/2, 0, dim, dim)
        case x if w < h => Scalr.crop(img, 0, (h-w)/2, dim, dim)
      }
    }
    // resize pixels
    def resizeImg(width: Int, height: Int) = {
      Scalr.resize(img, Scalr.Method.BALANCED, width, height)
    }
  }
}

6. 提取图像元数据

使用 readMetadata() 方法读取 CSV 格式的图像元数据,该方法在 CSVImageMetadataReader.scala 脚本中定义:

def readMetadata(csv: String, rows: List[Int]=List(-1)): List[List[String]]
= {
  val src = Source.fromFile(csv)
  def reading(csv: String): List[List[String]]= {
    src.getLines.map(x => x.split(",").toList)
      .toList
  }
  try {
    if(rows==List(-1)) reading(csv)
    else rows.map(reading(csv))
  }
  finally {
    src.close
  }
}

6.1 商业标签读取

使用 readBusinessLabels() 方法将商业 ID 映射到标签集合,代码如下:

def readBusinessLabels(csv: String, rows: List[Int]=List(-1)): Map[String,
  Set[Int]] = {
  val reader = readMetadata(csv)
  reader.drop(1)
    .map(x => x match {
      case x :: Nil => (x(0).toString, Set[Int]())
      case _ => (x(0).toString, x(1).split(" ").map(y => y.toInt).toSet)
    }).toMap
}

6.2 图像到商业 ID 映射读取

使用 readBusinessToImageLabels() 方法将图像 ID 映射到商业 ID,代码如下:

def readBusinessToImageLabels(csv: String, rows: List[Int] = List(-1)):
  Map[Int, String] = {
  val reader = readMetadata(csv)
  reader.drop(1)
    .map(x => x match {
      case x :: Nil => (x(0).toInt, "-1")
      case _ => (x(0).toInt, x(1).split(" ").head)
    }).toMap
}

7. 图像特征提取

7.1 定义正则表达式

val patt_get_jpg_name = new Regex("[0-9]")

7.2 提取图像 ID

def getImgIdsFromBusinessId(bizMap: Map[Int, String], businessIds:
  List[String]): List[Int] = {
  bizMap.filter(x => businessIds.exists(y => y == x._2)).map(_._1).toList
}

7.3 获取图像路径

def getImageIds(photoDir: String, businessMap: Map[Int, String] = Map(-1 ->
  "-1"), businessIds:
  List[String] = List("-1")): List[String] = {
  val d = new File(photoDir)
  val imgsPath = d.listFiles().map(x => x.toString).toList
  if (businessMap == Map(-1 -> "-1") || businessIds == List(-1)) {
    imgsPath
  }
  else {
    val imgsMap = imgsPath.map(x =>
      patt_get_jpg_name.findAllIn(x).mkString.toInt -> x).toMap
    val imgsPathSub = getImgIdsFromBusinessId(businessMap, businessIds)
    imgsPathSub.filter(x => imgsMap.contains(x)).map(x => imgsMap(x))
  }
}

7.4 处理图像

def processImages(imgs: List[String], resizeImgDim: Int = 128, nPixels: Int
  = -1): Map[Int,Vector[Int]]= {
  imgs.map(x => patt_get_jpg_name.findAllIn(x).mkString.toInt -> {
    val img0 = ImageIO.read(new File(x))
     .makeSquare
     .resizeImg(resizeImgDim, resizeImgDim) // (128, 128)
     .image2gray
    if(nPixels != -1) img0.slice(0, nPixels)
    else img0
  }).filter( x => x._2 != ())
    .toMap
}

7.5 数据对齐

val alignedData = new featureAndDataAligner(dataMap, businessMap,
  Option(labelMap))()
class featureAndDataAligner(dataMap: Map[Int, Vector[Int]], bizMap:
  Map[Int, String], labMap: Option[Map[String, Set[Int]]])(rowindices:
  List[Int] = dataMap.keySet.toList) {
  def this(dataMap: Map[Int, Vector[Int]], bizMap: Map[Int,
    String])(rowindices: List[Int]) = this(dataMap, bizMap,
    None)(rowindices)

  def alignBusinessImgageIds(dataMap: Map[Int, Vector[Int]], bizMap:
    Map[Int, String])
    (rowindices: List[Int] = dataMap.keySet.toList): List[(Int, String,
    Vector[Int])] = {
    for {
      pid <- rowindices
      val imgHasBiz = bizMap.get(pid)
      // returns None if img doe not have a bizID
      val bid = if(imgHasBiz != None) imgHasBiz.get
      else "-1"
      if (dataMap.keys.toSet.contains(pid) && imgHasBiz != None)
    }
    yield {
      (pid, bid, dataMap(pid))
    }
  }

  def alignLabels(dataMap: Map[Int, Vector[Int]], bizMap: Map[Int, String],
    labMap: Option[Map[String, Set[Int]]])(rowindices: List[Int] =
    dataMap.keySet.toList): List[(Int, String, Vector[Int], Set[Int])] = {
    def flatten1[A, B, C, D](t: ((A, B, C), D)): (A, B, C, D) = (t._1._1,
      t._1._2, t._1._3, t._2)
    val al = alignBusinessImgageIds(dataMap, bizMap)(rowindices)
    for { p <- al
    }
    yield {
      val bid = p._2
      val labs = labMap match {
        case None => Set[Int]()
        case x => (if(x.get.keySet.contains(bid)) x.get(bid)
        else Set[Int]())
      }
      flatten1(p, labs)
    }
  }

  lazy val data = alignLabels(dataMap, bizMap, labMap)(rowindices)
  // getter functions
  def getImgIds = data.map(_._1)
  def getBusinessIds = data.map(_._2)
  def getImgVectors = data.map(_._3)
  def getBusinessLabels = data.map(_._4)
  def getImgCntsPerBusiness =
    getBusinessIds.groupBy(identity).mapValues(x => x.size)
}

8. 准备 ND4j 数据集

8.1 创建数据集

def makeDataSet(alignedData: featureAndDataAligner, bizClass: Int): DataSet
= {
  val alignedXData = alignedData.getImgVectors.toNDArray
  val alignedLabs = alignedData.getBusinessLabels.map(x =>
    if (x.contains(bizClass)) Vector(1, 0)
    else Vector(0, 1)).toNDArray
  new DataSet(alignedXData, alignedLabs)
}

8.2 转换为 INDArray

def makeDataSetTE(alignedData: featureAndDataAligner): INDArray = {
  alignedData.getImgVectors.toNDArray
}

9. 训练 CNN 并保存模型

9.1 训练模型

def trainModelEpochs(alignedData: featureAndDataAligner, businessClass: Int
  = 1, saveNN: String = "") = {
  val ds = makeDataSet(alignedData, businessClass)
  val nfeatures = ds.getFeatures.getRow(0).length // Hyperparameter
  val numRows = Math.sqrt(nfeatures).toInt //numRows*numColumns == data*channels
  val numColumns = Math.sqrt(nfeatures).toInt //numRows*numColumns == data*channels
  val nChannels = 1 // would be 3 if color image w R,G,B
  val outputNum = 9 // # of classes (# of columns in output)
  val iterations = 1
  val splitTrainNum = math.ceil(ds.numExamples * 0.8).toInt // 80/20 training/test split
  val seed = 12345
  val listenerFreq = 1
  val nepochs = 20
  val nbatch = 128 // recommended between 16 and 128

  ds.normalizeZeroMeanZeroUnitVariance()
  Nd4j.shuffle(ds.getFeatureMatrix, new Random(seed), 1) // shuffles rows in the ds.
  Nd4j.shuffle(ds.getLabels, new Random(seed), 1) // shuffles labels accordingly
  val trainTest: SplitTestAndTrain = ds.splitTestAndTrain(splitTrainNum,
    new Random(seed))
  // creating epoch dataset iterator
  val dsiterTr = new ListDataSetIterator(trainTest.getTrain.asList(),
    nbatch)
  val dsiterTe = new ListDataSetIterator(trainTest.getTest.asList(),
    nbatch)
  val epochitTr: MultipleEpochsIterator = new
    MultipleEpochsIterator(nepochs, dsiterTr)
  val epochitTe: MultipleEpochsIterator = new
    MultipleEpochsIterator(nepochs, dsiterTe)
  //First convolution layer with ReLU as activation function
  val layer_0 = new ConvolutionLayer.Builder(6, 6)
    .nIn(nChannels)
    .stride(2, 2) // default stride(2,2)
    .nOut(20) // # of feature maps
    .dropOut(0.5)
    .activation("relu") // rectified linear units
    .weightInit(WeightInit.RELU)
    .build()
  //First subsampling layer
  val layer_1 = new
    SubsamplingLayer.Builder(SubsamplingLayer.PoolingType.MAX)
    .kernelSize(2, 2)
    .stride(2, 2)
    .build()
  //Second convolution layer with ReLU as activation function
  val layer_2 = new ConvolutionLayer.Builder(6, 6)
    .nIn(nChannels)
    .stride(2, 2)
    .nOut(50)
    .activation("relu")
    .build()
  //Second subsampling layer
  val layer_3 = new
    SubsamplingLayer.Builder(SubsamplingLayer.PoolingType.MAX)
    .kernelSize(2, 2)
    .stride(2, 2)
    .build()
  //Dense layer
  val layer_4 = new DenseLayer.Builder()
    .activation("relu")
    .nOut(500)
    .build()
  // Final and fully connected layer with Softmax as activation function
  val layer_5 = new
    OutputLayer.Builder(LossFunctions.LossFunction.MCXENT)
    .nOut(outputNum)
    .weightInit(WeightInit.XAVIER)
    .activation("softmax")
    .build()
  val builder: MultiLayerConfiguration.Builder = new
    NeuralNetConfiguration.Builder()
    .seed(seed)
    .iterations(iterations)
    .miniBatch(true)
    .optimizationAlgo(OptimizationAlgorithm.STOCHASTIC_GRADIENT_DESCENT)
    .regularization(true).l2(0.0005)
    .learningRate(0.01)
    .list(6)
    .layer(0, layer_0)
    .layer(1, layer_1)
    .layer(2, layer_2)
    .layer(3, layer_3)
    .layer(4, layer_4)
    .layer(5, layer_5)
    .backprop(true).pretrain(false)
  new ConvolutionLayerSetup(builder, numRows, numColumns, nChannels)
  val conf: MultiLayerConfiguration = builder.build()
  val model: MultiLayerNetwork = new MultiLayerNetwork(conf)
  model.init()
  model.setListeners(Seq[IterationListener](new
    ScoreIterationListener(listenerFreq)).asJava)
  model.fit(epochitTr)
  val eval = new Evaluation(outputNum)
  while (epochitTe.hasNext) {
    val testDS = epochitTe.next(nbatch)
    val output: INDArray = model.output(testDS.getFeatureMatrix)
    eval.eval(testDS.getLabels(), output)
  }
  if (!saveNN.isEmpty) {
    // model config
    FileUtils.write(new File(saveNN + ".json"),
      model.getLayerWiseConfigurations().toJson())
    // model parameters
    val dos: DataOutputStream = new
      DataOutputStream(Files.newOutputStream(Paths.get(saveNN + ".bin")))
    Nd4j.write(model.params(), dos)
  }
}

9.2 保存模型

def saveNN(model: MultiLayerNetwork, NNconfig: String, NNparams: String) =
{
  // save neural network config
  FileUtils.write(new File(NNconfig),
    model.getLayerWiseConfigurations().toJson())
  // save neural network parms
  val dos: DataOutputStream = new
    DataOutputStream(Files.newOutputStream(Paths.get(NNparams)))
  Nd4j.write(model.params(), dos)
}

9.3 加载模型

def loadNN(NNconfig: String, NNparams: String) = {
  // get neural network config
  val confFromJson: MultiLayerConfiguration =
    MultiLayerConfiguration.fromJson(FileUtils.readFileToString(new
      File(NNconfig)))
  // get neural network parameters
  val dis: DataInputStream = new DataInputStream(new
    FileInputStream(NNparams))
  val newParams = Nd4j.read(dis)
  // creating network object
  val savedNetwork: MultiLayerNetwork = new
    MultiLayerNetwork(confFromJson)
  savedNetwork.init()
  savedNetwork.setParameters(newParams)
  savedNetwork
}

10. 模型评估

10.1 模型评分

def scoreModel(model: MultiLayerNetwork, ds: INDArray) = {
  model.output(ds)
}

10.2 聚合评分

def aggImgScores2Business(scores: INDArray, alignedData:
  featureAndDataAligner ) = {
  assert(scores.size(0) == alignedData.data.length, "alignedData and
    scores length are different. They must be equal")
  def getRowIndices4Business(mylist: List[String], mybiz: String): List[Int]
    = mylist.zipWithIndex.filter(x => x._1 == mybiz).map(_._2)
  def mean(xs: List[Double]) = xs.sum / xs.size
  alignedData.getBusinessIds.distinct.map(x => (x, {
    val irows = getRowIndices4Business(alignedData.getBusinessIds, x)
    val ret =
      for(row <- irows)
        yield scores.getRow(row).getColumn(1).toString.toDouble
    mean(ret)
  }))
}

11. 执行主方法

package Yelp.Classifier
import Yelp.Preprocessor.CSVImageMetadataReader._
import Yelp.Preprocessor.featureAndDataAligner
import Yelp.Preprocessor.imageFeatureExtractor._
import Yelp.Evaluator.ResultFileGenerator._
import Yelp.Preprocessor.makeND4jDataSets._
import Yelp.Evaluator.ModelEvaluation._
import Yelp.Trainer.CNNEpochs._
import Yelp.Trainer.NeuralNetwork._
object YelpImageClassifier {
  def main(args: Array[String]): Unit = {
    // image processing on training data
    val labelMap = readBusinessLabels("data/labels/train.csv")
    val businessMap =
      readBusinessToImageLabels("data/labels/train_photo_to_biz_ids.csv")
    val imgs = getImageIds("data/images/train/", businessMap,
      businessMap.map(_._2).toSet.toList).slice(0,20000) // 20000 images
    println("Image ID retreival done!")
    val dataMap = processImages(imgs, resizeImgDim = 256)
    println("Image processing done!")
    val alignedData =
      new featureAndDataAligner(dataMap, businessMap,
        Option(labelMap))()
    println("Feature extraction done!")
    // training one model for one class at a time. Many hyperparamters
    // hardcoded within
    val cnn0 = trainModelEpochs(alignedData, businessClass = 0, saveNN
      = "models/model0")
    val cnn1 = trainModelEpochs(alignedData, businessClass = 1, saveNN
      = "models/model1")
    val cnn2 = trainModelEpochs(alignedData, businessClass = 2, saveNN
      = "models/model2")
    val cnn3 = trainModelEpochs(alignedData, businessClass = 3, saveNN
      = "models/model3")
    val cnn4 = trainModelEpochs(alignedData, businessClass = 4, saveNN
      = "models/model4")
    val cnn5 = trainModelEpochs(alignedData, businessClass = 5, saveNN
      = "models/model5")
    val cnn6 = trainModelEpochs(alignedData, businessClass = 6, saveNN
      = "models/model6")
    val cnn7 = trainModelEpochs(alignedData, businessClass = 7, saveNN
      = "models/model7")
    val cnn8 = trainModelEpochs(alignedData, businessClass = 8, saveNN
      = "models/model8")
    // processing test data for scoring
    val businessMapTE =
      readBusinessToImageLabels("data/labels/test_photo_to_biz.csv")
    val imgsTE = getImageIds("data/images/test//", businessMapTE,
      businessMapTE.map(_._2).toSet.toList)
    val dataMapTE = processImages(imgsTE, resizeImgDim = 128) // make
    // them 256x256
    val alignedDataTE = new featureAndDataAligner(dataMapTE,
      businessMapTE, None)()
    // creating csv file to submit to kaggle (scores all models)
    val Results = SubmitObj(alignedDataTE, "results/ModelsV0/")
    val SubmitResults = writeSubmissionFile("kaggleSubmitFile.csv",
      Results, thresh = 0.9)
  }
}

12. 模型评估结果

==========================Scores======================================
 Accuracy: 0.6833
 Precision: 0.53
 Recall: 0.5222
 F1 Score: 0.5261
======================================================================

本项目通过一系列步骤,从图像预处理、特征提取、模型训练到最终评估,实现了基于卷积神经网络的大规模图像分类。通过合理设置超参数和优化模型结构,我们可以进一步提高模型的性能。

13. 模型评估方法总结

模型评估是整个图像分类项目中至关重要的环节,它能够直观地反映模型的性能优劣。在本项目中,我们采用了多种评估指标,如准确率(Accuracy)、精确率(Precision)、召回率(Recall)和 F1 分数(F1 Score)。这些指标从不同角度衡量了模型的分类效果,具体解释如下:
- 准确率(Accuracy) :表示模型正确分类的样本数占总样本数的比例。在本项目的评估结果中,准确率为 0.6833,这意味着模型大约有 68.33% 的样本分类是正确的。
- 精确率(Precision) :是指模型预测为正类的样本中,实际为正类的比例。本项目的精确率为 0.53,说明在模型预测为正类的样本中,只有 53% 是真正的正类。
- 召回率(Recall) :也称为灵敏度,它衡量的是实际为正类的样本中,被模型正确预测为正类的比例。本项目的召回率为 0.5222,即模型能够召回大约 52.22% 的正类样本。
- F1 分数(F1 Score) :是精确率和召回率的调和平均数,它综合考虑了精确率和召回率两个指标。F1 分数越接近 1,说明模型的性能越好。本项目的 F1 分数为 0.5261,表明模型在精确率和召回率之间取得了一定的平衡,但仍有提升的空间。

评估指标表格

评估指标 数值
准确率(Accuracy) 0.6833
精确率(Precision) 0.53
召回率(Recall) 0.5222
F1 分数(F1 Score) 0.5261

14. 项目整体流程回顾

整个项目的流程可以概括为以下几个主要步骤,每个步骤都紧密相连,共同构成了一个完整的图像分类系统:
1. 数据准备 :从 Kaggle 平台获取 Yelp 数据集,包括训练集和测试集的图像以及相关的元数据文件。对数据进行整理和分析,了解数据的结构和特点。
2. 图像预处理 :由于原始图像的大小和形状各异,不适合直接输入到 CNN 中进行训练,因此需要对图像进行预处理。具体操作包括将图像变为正方形、调整图像大小为 128x128 像素,并将图像转换为灰度图像。这些操作可以使图像数据更加规整,便于后续的特征提取和模型训练。
3. 特征提取 :通过读取图像元数据文件,建立图像 ID 到商业 ID 的映射以及商业 ID 到标签的映射。然后,从预处理后的图像中提取特征,并将其转换为适合 CNN 输入的格式。
4. 模型训练 :为每个类别在训练数据上训练九个 CNN 模型。在训练过程中,需要设置合适的超参数,如学习率、批量大小、迭代次数等,并对数据集进行归一化和洗牌操作,以提高模型的训练效果。训练完成后,保存模型的配置和参数,以便后续的使用和评估。
5. 模型评估 :使用测试集对训练好的模型进行评估,计算准确率、精确率、召回率和 F1 分数等评估指标。通过对评估结果的分析,了解模型的性能表现,并根据评估结果对模型进行调整和优化。

项目流程 mermaid 流程图

graph LR
    A[数据准备] --> B[图像预处理]
    B --> C[特征提取]
    C --> D[模型训练]
    D --> E[模型评估]

15. 项目优化建议

尽管本项目已经取得了一定的成果,但仍有许多可以优化的地方,以下是一些具体的优化建议:
- 数据方面
- 数据增强 :通过对训练图像进行旋转、翻转、缩放等操作,增加训练数据的多样性,从而提高模型的泛化能力。
- 数据清洗 :进一步清理数据集中的噪声和重复数据,提高数据的质量。例如,去除模糊不清、内容无关的图像,以及重复上传的图像。
- 模型方面
- 调整超参数 :通过网格搜索、随机搜索等方法,寻找最优的超参数组合,如学习率、批量大小、迭代次数等。不同的超参数设置可能会对模型的性能产生显著影响,因此需要进行细致的调优。
- 改进模型结构 :尝试使用更复杂的 CNN 架构,如 ResNet、Inception 等,或者增加卷积层和全连接层的数量,以提高模型的表达能力。
- 集成学习 :将多个不同的 CNN 模型进行集成,通过投票、平均等方式综合各个模型的预测结果,从而提高模型的准确性和稳定性。
- 评估方面
- 使用更多评估指标 :除了准确率、精确率、召回率和 F1 分数外,还可以考虑使用其他评估指标,如 ROC 曲线、AUC 值等,以更全面地评估模型的性能。
- 交叉验证 :采用交叉验证的方法,将数据集划分为多个子集,轮流使用不同的子集进行训练和测试,从而更准确地评估模型的泛化能力。

16. 总结

本项目通过使用卷积神经网络实现了大规模图像分类任务,从项目背景的分析、数据集的准备、图像预处理、特征提取、模型训练到最终的模型评估,每个步骤都进行了详细的阐述和实现。通过对模型评估结果的分析,我们了解了模型的性能表现,并提出了一些优化建议。在实际应用中,我们可以根据具体的需求和数据特点,对项目进行进一步的优化和改进,以提高模型的性能和实用性。同时,本项目也为其他图像分类任务提供了一个可参考的范例,希望能够对相关领域的研究和实践有所帮助。

在未来的工作中,我们可以继续探索更先进的技术和方法,不断提升图像分类的准确率和效率,为更多的实际应用场景提供支持。例如,将图像分类技术应用于智能安防、医疗影像诊断、自动驾驶等领域,为这些领域的发展带来新的机遇和挑战。

【CNN-GRU-Attention】基于卷积神经网络和门控循环单元网络结合注意力机制的多变量回归预测研究(Matlab代码实现)内容概要:本文介绍了基于卷积神经网络(CNN)、门控循环单元网络(GRU)与注意力机制(Attention)相结合的多变量回归预测模型研究,重点利用Matlab实现该深度学习模型的构建与仿真。该模型通过CNN提取输入数据的局部特征,利用GRU捕捉时间序列的长期依赖关系,并引入注意力机制增强关键时间步的权重,从而提升多变量时间序列回归预测的精度与鲁棒性。文中涵盖了模型架构设计、训练流程、参数调优及实际案例验证,适用于复杂非线性系统的预测任务。; 适合人群:具备一定机器学习与深度学习基础,熟悉Matlab编程环境,从事科研或工程应用的研究生、科研人员及算法工程师,尤其适合关注时间序列预测、能源预测、智能优化等方向的技术人员。; 使用场景及目标:①应用于风电功率预测、负荷预测、交通流量预测等多变量时间序列回归任务;②帮助读者掌握CNN-GRU-Attention混合模型的设计思路与Matlab实现方法;③为学术研究、毕业论文或项目开发提供可复现的代码参考和技术支持。; 阅读建议:建议读者结合Matlab代码逐模块理解模型实现细节,重点关注数据预处理、网络结构搭建与注意力机制的嵌入方式,并通过调整超参数和更换数据集进行实验验证,以深化对模型性能影响因素的理解。
下载前必看:https://pan.quark.cn/s/da7147b0e738 《商品采购管理系统详解》商品采购管理系统是一款依托数据库技术,为中小企业量身定制的高效且易于操作的应用软件。 该系统借助VC++编程语言完成开发,致力于改进采购流程,增强企业管理效能,尤其适合初学者开展学习与实践活动。 在此之后,我们将详细剖析该系统的各项核心功能及其实现机制。 1. **VC++ 开发环境**: VC++是微软公司推出的集成开发平台,支持C++编程,具备卓越的Windows应用程序开发性能。 在该系统中,VC++作为核心编程语言,负责实现用户界面、业务逻辑以及数据处理等关键功能。 2. **数据库基础**: 商品采购管理系统的核心在于数据库管理,常用的如SQL Server或MySQL等数据库系统。 数据库用于保存商品信息、供应商资料、采购订单等核心数据。 借助SQL(结构化查询语言)进行数据的增加、删除、修改和查询操作,确保信息的精确性和即时性。 3. **商品管理**: 系统内含商品信息管理模块,涵盖商品名称、规格、价格、库存等关键字段。 借助界面,用户能够便捷地录入、调整和查询商品信息,实现库存的动态调控。 4. **供应商管理**: 供应商信息在采购环节中占据重要地位,系统提供供应商注册、联系方式记录、信用评价等功能,助力企业构建稳固的供应链体系。 5. **采购订单管理**: 采购订单是采购流程的关键环节,系统支持订单的生成、审批、执行和追踪。 通过自动化处理,减少人为失误,提升工作效率。 6. **报表与分析**: 系统具备数据分析能力,能够生成采购报表、库存报表等,帮助企业掌握采购成本、库存周转率等关键数据,为决策提供支持。 7. **用户界面设计**: 依托VC++的MF...
【DC-AC】使用了H桥MOSFET进行开关,电感器作为滤波器,R和C作为负载目标是产生150V的双极输出和4安培(双极)的电流(Simulink仿真实现)内容概要:本文档围绕一个基于Simulink的电力电子系统仿真项目展开,重点介绍了一种采用H桥MOSFET进行开关操作的DC-AC逆变电路设计,结合电感器作为滤波元件,R和C构成负载,旨在实现150V双极性输出电压和4A双极性电流的仿真目标。文中详细描述了系统结构、关键器件选型及控制策略,展示了通过Simulink平台完成建模与仿真的全过程,并强调了参数调整与波形分析的重要性,以确保输出符合设计要求。此外,文档还提及该仿真模型在电力变换、新能源并网等领域的应用潜力。; 适合人群:具备电力电子基础知识和Simulink仿真经验的高校学生、科研人员及从事电力系统、新能源技术等相关领域的工程技术人员;熟悉电路拓扑与基本控制理论的初级至中级研究人员。; 使用场景及目标:①用于教学演示H桥逆变器的工作原理与滤波设计;②支撑科研项目中对双极性电源系统的性能验证;③为实际工程中DC-AC转换器的设计与优化提供仿真依据和技术参考;④帮助理解MOSFET开关行为、LC滤波机制及负载响应特性。; 阅读建议:建议读者结合Simulink模型文件同步操作,重点关注H桥驱动信号生成、电感电容参数选取及输出波形的傅里叶分析,建议在仿真过程中逐步调试开关频率与占空比,观察其对输出电压电流的影响,以深化对逆变系统动态特性的理解。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值