# 识别手写数字(CNN 卷积神经网络)

# 为什么要使用卷积神经网络?

  • 需要处理的数据量太大,特征太多,普通神经网络效率太低

  • 卷积神经网络模拟人类对的视觉处理流程,高效提取特征,大幅度减少运算量

# 卷积神经网络的层

  • 卷积层(通过卷积运算提取特征)

卷积神经网络提取特征示例网站

卷积层包含了多个卷积核对图像进行卷积操作,根据不同的卷积核(矩阵)计算出特征

一个卷积核可能只会提取某一种特征,比如 outline,多个卷积核通过逐轮提取并组合使得结果更加准确

卷积层是有权重需要训练的,卷积核就是权重

  • 池化层(属于优化的一个层,没有它也行)

作用是用于提取最强的特征

池化层提取最强的特征演示

扩大感受野,减少计算量

池化层是没有权重需要训练的

  • 全连接层

作为输出层

作为分类器

全连接层是有权重需要训练的

# 代码展示

import * as tf from '@tensorflow/tfjs';
import * as tfvis from '@tensorflow/tfjs-vis';
import { MnistData } from './data';
import { accuracy } from '@tensorflow/tfjs-vis/dist/util/math';

window.onload = async () => {
  /*加载变量*/
  const data = new MnistData();
  await data.load(); // 加载图片和二进制文件的过程-load之后就可以拿到图片的数据了

  const TEST_COUNT = 20;
  const example = data.nextTestBatch(TEST_COUNT); // 加载一些(20个)验证集tensor数据

  const surface = tfvis.visor().surface({
    // 船舰一个tfvis的绘画面板
    name: '输入示例',
  });
  console.log(example);

  /*将tensors数据转换成图片展示*/
  for (let i = 0; i < TEST_COUNT; i++) {
    const imagetensor = tf.tidy(() => {
      // 释放内存
      /*切割出每一张图并且转换成三维的tensor*/
      return example.xs.slice([i, 0], [1, 784]).reshape([28, 28, 1]); // 784是图片为28*28*1的黑白图
    });

    /*船舰canvas对象*/
    const canvas = document.createElement('canvas');
    canvas.width = 28;
    canvas.height = 28;
    canvas.style = 'margin : 4px';

    /*渲染图片*/
    await tf.browser.toPixels(imagetensor, canvas); // 将tensor转化为像素,将数据渲染到canvas上面去
    surface.drawArea.append(canvas); // 在visor的绘画区域插入canvas
  }

  /*定义模型*/
  const model = tf.sequential();

  /*添加一个卷积层*/
  model.add(
    tf.layers.conv2d({
      inputShape: [28, 28, 1], // 输入数据shape
      kernelSize: 5, // 设置卷积核大小(奇数可以更好地提取中心点)
      filters: 8, // 使用的卷积核个数(类型)(超参数)
      strides: 1, // 每次扫描步长
      activation: 'relu',
      kernelInitializer: 'varianceScaling', // 设置初始化方法(选填)可以提升收敛速度
    })
  );

  /*添加(最大)池化层*/
  model.add(
    tf.layers.maxPool2d({
      poolSize: [2, 2], // 池化尺寸
      strides: [2, 2], // 步数
    })
  );

  /*重复上诉步骤提取特征组合--比如第一轮提取横竖,第二轮提取撇捺等*/
  model.add(
    tf.layers.conv2d({
      kernelSize: 5,
      filters: 16, // 需要更加多的卷积核,因为要提取更加复杂的组合特征
      strides: 1,
      activation: 'relu',
      kernelInitializer: 'varianceScaling', // 设置初始化方法(选填)可以提升收敛速度
    })
  );

  model.add(
    tf.layers.maxPool2d({
      poolSize: [2, 2], // 池化尺寸
      strides: [2, 2], // 步数
    })
  );

  /*将高维数据转换成一维*/
  model.add(tf.layers.flatten());

  /*添加输出层(一维)*/
  model.add(
    tf.layers.dense({
      // 全连接层
      units: 10, // 输出0-9共10个结果
      activation: 'softmax', // 多分类的激活函数
      kernelInitializer: 'varianceScaling',
    })
  );

  /*设置损失函数和优化器*/
  model.compile({
    loss: 'categoricalCrossentropy', // 交叉熵
    optimizer: tf.train.adam(),
    metrics: 'accuracy', // 度量单位(准确度)
  });

  /*准备训练集和验证集*/
  const [tranXs, trainYs] = tf.tidy(() => {
    const d = data.nextTestBatch(2000);
    return [d.xs.reshape([2000, 28, 28, 1]), d.labels]; // 2000个数据,每个是28*28*1
  });

  const [testXs, testYs] = tf.tidy(() => {
    const d = data.nextTestBatch(400);
    // console.log('d', d);
    // console.log('d', d.xs.reshape([400, 28, 28, 1]).print());
    return [d.xs.reshape([400, 28, 28, 1]), d.labels];
  });

  /*进行训练*/
  model.fit(tranXs, trainYs, {
    validationData: [testXs, testYs], // 验证集数据
    epochs: 50,
    batchSize: 1000,
    callbacks: tfvis.show.fitCallbacks(
      { name: '训练效果' },
      ['loss', 'val_loss', 'acc', 'val_acc'],
      { callbacks: ['onEpochEnd'] }
    ),
  });

  /*画布操作*/
  const canvas = document.querySelector('canvas');

  /*手写*/
  canvas.addEventListener('mousemove', (e) => {
    if (e.buttons === 1) {
      // 左键画东西
      const ctx = canvas.getContext('2d');
      ctx.fillStyle = 'rgb(255,255,255)';
      ctx.fillRect(e.offsetX, e.offsetY, 20, 20);
    }
  });

  /*清除事件*/
  window.clear = () => {
    const ctx = canvas.getContext('2d');
    ctx.fillStyle = 'rgb(0,0,0)';
    ctx.fillRect(0, 0, 300, 300);
  };

  clear();

  /*预测*/
  window.predict = () => {
    const input = tf.tidy(() => {
      return tf.image
        .resizeBilinear(
          // canvas转tensor并转大小
          tf.browser.fromPixels(canvas),
          [28, 28], // 尺寸
          true // 边角
        )
        .slice([0, 0, 0], [28, 28, 1]) // 切成黑白图片
        .toFloat() // 归一化
        .div(255) // 归一化(转化成0-1之间)
        .reshape([1, 28, 28, 1]); // 保持与训练数据形状一致
    });
    const pred = model.predict(input).argMax(1);
    console.log('预测结果', model.predict(input));
    console.log('预测结果', model.predict(input).dataSync());
    alert(`预测结果为 ${pred.dataSync()[0]}`);
  };
};

# 重点笔记

  • 需要使用 http-server 在本地建立一个静态服务器以加载/data/mnist 下的图片和标签, http-server(hs) 目录 --cors(防止跨域--端口不一样也算跨域)

  • 加载的数据是一个 tensor,里面有 lables 标签---一个 shape 是[20,10]的数据,具体是[[0,1,0,0,0,0,0,0,0,0],...,[]]。 另外一个是 xs,是一个 shape 为[20,784]的数据(784 是因为这个图片是 28 像素28 像素的,所以为 28281)1 为黑白照片,如果是彩色照片 rgb 三个通道的花就要3

  • tf.tidy()的使用,可以清除内存(中间张量),使用此方法有助于避免内存泄漏。 tf.tidy

    其实涉及到 tensor 操作都应该放在 tidy 中

  • tf.slice()的使用 tf.slice

  • tf.Tensor.reshape()的使用 tf.Tensor.reshape

  • tf.browser.toPixels()的使用,将 tensor 转化成像素。第一个参数接收一个图片的 tensor(必须是二维或者三维的) tf.browser.toPixels

  • 使用 tfvis.visor().surface()加载图片数据。tfvis.visor().surface()会返回一个 drawArea 绘图区域,将这个 drawArea 给 append 到 canvas 即可显示 tfvis.visor().surface()

  • 卷积神经网络如何提取特征? 卷积神经网络提取特征示例网站

  • 添加层的顺序

卷积层 - 最大池化层 -

  • tf.layers.flatten 层的使用--把高纬数据转换成一维数据

把高纬数据放在最后一层 dense 层去做分类的非常普遍的一个操作