TensorFlow.js - CNN을 활용한 필기 입력 숫자 인식

이 튜토리얼에서는 컨볼루셔널 신경망(CNN)을 활용해 필기 입력 숫자를 인식하는 TensorFlow.js 모델을 빌드합니다. 먼저 수천 개의 필기 입력 숫자 이미지와 해당 라벨을 '인식'하도록 분류기를 학습시킵니다. 그런 다음 모델에서 인식한 적 없는 테스트 데이터를 사용해 분류기의 정확성을 평가합니다.

이 작업은 입력 이미지에 카테고리(이미지에 표시되는 숫자)를 할당하도록 모델을 학습시키므로 분류 작업으로 간주됩니다. 많은 입력 예시와 올바른 출력을 인식시켜 모델을 학습시키는데, 이러한 학습을 지도 학습이라고 합니다.

빌드 대상

TensorFlow.js를 사용하여 브라우저에서 모델을 학습시키는 웹페이지를 만듭니다. 특정 크기의 흑백 이미지를 제공하여 이미지에 표시된 숫자를 분류합니다. 구체적인 단계는 다음과 같습니다.

  • 데이터를 로드합니다.
  • 모델의 아키텍처를 정의합니다.
  • 모델을 학습시키고 학습하는 동안 성능을 모니터링합니다.
  • 몇 가지 예측을 수행하여 학습된 모델을 평가합니다.

학습 내용

  • TensorFlow.js Layers API를 사용해 컨볼루셔널 모델을 만드는 TensorFlow.js 구문
  • TensorFlow.js에서 분류 작업 작성
  • tfjs-vis 라이브러리를 사용해 브라우저 내 학습을 모니터링하는 방법

필요한 사항

또한 첫 번째 학습 튜토리얼의 내용도 숙지해야 합니다.

HTML 페이지 작성 및 자바스크립트 포함

96914ff65fc3b74c.png아래 이름의 html 파일에 다음 코드를 복사합니다.

index.html

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>TensorFlow.js Tutorial</title>

  <!-- Import TensorFlow.js -->
  <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@1.0.0/dist/tf.min.js"></script>
  <!-- Import tfjs-vis -->
  <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-vis@1.0.2/dist/tfjs-vis.umd.min.js"></script>

  <!-- Import the data file -->
  <script src="data.js" type="module"></script>

  <!-- Import the main script file -->
  <script src="script.js" type="module"></script>

</head>

<body>
</body>
</html>

데이터 및 코드의 자바스크립트 파일 만들기

  1. 위 HTML 파일과 같은 폴더에 data.js 파일을 만들고 이 링크의 콘텐츠를 해당 파일로 복사합니다.
  2. 1단계와 동일한 폴더에 script.js라는 파일을 만든 후 이 파일에 다음 코드를 입력합니다.
console.log('Hello TensorFlow');

테스트

HTML 및 자바스크립트 파일을 만들었으므로 이제 테스트해 보겠습니다. 브라우저에서 index.html 파일을 열고 DevTools 콘솔을 엽니다.

모든 항목이 제대로 작동한다면 2개의 전역 변수가 생성됩니다. tf는 TensorFlow.js 라이브러리의 참조이고 tfvistfjs-vis 라이브러리의 참조입니다.

Hello TensorFlow*,*라고 표시된 메시지가 나타나면 다음 단계를 진행할 준비가 된 것입니다.

이 튜토리얼에서는 아래와 같이 이미지에서 숫자를 인식하도록 모델을 학습시킵니다. 이러한 이미지는 MNIST라는 이름의 데이터 세트에 포함된 28x28픽셀의 그레이 스케일 이미지입니다.

mnist 4 mnist 3 mnist 8

학습 부분에 집중할 수 있도록 미리 만들어 둔 특별한 스프라이트 파일(최대 10MB)에서 이러한 이미지를 로드할 수 있도록 코드가 제공됩니다.

원한다면 data.js 파일을 살펴보고 데이터가 로드되는 방식을 알아보세요. 이 튜토리얼을 완료했으면 직접 데이터를 로드하는 방식을 만들어 보세요.

제공된 코드에는 다음과 같은 2개의 public 메서드가 포함된 MnistData 클래스가 있습니다.

  • nextTrainBatch(batchSize): 학습 세트에서 이미지와 해당 라벨의 무작위 배치를 반환합니다.
  • nextTestBatch(batchSize): 테스트 세트에서 이미지와 해당 라벨의 배치를 반환합니다.

MnistData 클래스는 중요 단계인 데이터의 셔플링정규화도 수행합니다.

총 65,000개의 이미지가 있는데 그 중 최대 55,000개의 이미지를 사용해 모델을 학습시키고 나머지 이미지 10,000개는 학습 완료 후에 모델의 성능을 테스트할 때 사용합니다. 그리고 모든 작업은 브라우저에서 진행합니다.

데이터를 로드하여 올바르게 로드되는지 테스트해 보겠습니다.

96914ff65fc3b74c.png scripts.js 파일에 다음 코드를 추가합니다.

import {MnistData} from './data.js';

async function showExamples(data) {
  // Create a container in the visor
  const surface =
    tfvis.visor().surface({ name: 'Input Data Examples', tab: 'Input Data'});

  // Get the examples
  const examples = data.nextTestBatch(20);
  const numExamples = examples.xs.shape[0];

  // Create a canvas element to render each example
  for (let i = 0; i < numExamples; i++) {
    const imageTensor = tf.tidy(() => {
      // Reshape the image to 28x28 px
      return examples.xs
        .slice([i, 0], [1, examples.xs.shape[1]])
        .reshape([28, 28, 1]);
    });

    const canvas = document.createElement('canvas');
    canvas.width = 28;
    canvas.height = 28;
    canvas.style = 'margin: 4px;';
    await tf.browser.toPixels(imageTensor, canvas);
    surface.drawArea.appendChild(canvas);

    imageTensor.dispose();
  }
}

async function run() {
  const data = new MnistData();
  await data.load();
  await showExamples(data);
}

document.addEventListener('DOMContentLoaded', run);

페이지를 새로고침하면 몇 초 후 왼쪽에 여러 이미지가 포함된 패널이 표시됩니다.

6dff857738b54eed.png

입력 데이터는 다음과 같습니다.

6dff857738b54eed.png

목표는 이미지 1개를 가져와 이미지가 속할 수 있는 클래스 10개 각각의 점수(숫자 0~9)를 예측하도록 모델을 학습시키는 것입니다.

각 이미지는 너비 28픽셀, 높이 28픽셀이며 그레이 스케일 이미지이므로 색상 채널이 1개입니다. 따라서 각 이미지의 모양은 [28, 28, 1]입니다.

각 입력 예시의 모양뿐만 아니라 1-10 매핑에 대해 작업하고 있다는 것을 잊지 마세요. 이 내용은 다음 섹션에서 중요합니다.

이 섹션에서는 모델 아키텍처를 설명하는 코드를 작성합니다. 모델 아키텍처란 '모델이 실행될 때 모델이 실행하는 함수' 또는 '응답을 계산하기 위해 모델에서 사용하는 알고리즘'으로 표현할 수 있습니다.

머신러닝에서는 아키텍처(또는 알고리즘)를 정의하고 학습 프로세스에서 해당 알고리즘의 매개변수를 학습하도록 합니다.

96914ff65fc3b74c.png 다음 함수를

script.js 파일에 추가해 모델 아키텍처를 정의합니다.

function getModel() {
  const model = tf.sequential();

  const IMAGE_WIDTH = 28;
  const IMAGE_HEIGHT = 28;
  const IMAGE_CHANNELS = 1;

  // In the first layer of our convolutional neural network we have
  // to specify the input shape. Then we specify some parameters for
  // the convolution operation that takes place in this layer.
  model.add(tf.layers.conv2d({
    inputShape: [IMAGE_WIDTH, IMAGE_HEIGHT, IMAGE_CHANNELS],
    kernelSize: 5,
    filters: 8,
    strides: 1,
    activation: 'relu',
    kernelInitializer: 'varianceScaling'
  }));

  // The MaxPooling layer acts as a sort of downsampling using max values
  // in a region instead of averaging.
  model.add(tf.layers.maxPooling2d({poolSize: [2, 2], strides: [2, 2]}));

  // Repeat another conv2d + maxPooling stack.
  // Note that we have more filters in the convolution.
  model.add(tf.layers.conv2d({
    kernelSize: 5,
    filters: 16,
    strides: 1,
    activation: 'relu',
    kernelInitializer: 'varianceScaling'
  }));
  model.add(tf.layers.maxPooling2d({poolSize: [2, 2], strides: [2, 2]}));

  // Now we flatten the output from the 2D filters into a 1D vector to prepare
  // it for input into our last layer. This is common practice when feeding
  // higher dimensional data to a final classification output layer.
  model.add(tf.layers.flatten());

  // Our last layer is a dense layer which has 10 output units, one for each
  // output class (i.e. 0, 1, 2, 3, 4, 5, 6, 7, 8, 9).
  const NUM_OUTPUT_CLASSES = 10;
  model.add(tf.layers.dense({
    units: NUM_OUTPUT_CLASSES,
    kernelInitializer: 'varianceScaling',
    activation: 'softmax'
  }));

  // Choose an optimizer, loss function and accuracy metric,
  // then compile and return the model
  const optimizer = tf.train.adam();
  model.compile({
    optimizer: optimizer,
    loss: 'categoricalCrossentropy',
    metrics: ['accuracy'],
  });

  return model;
}

좀 더 자세히 살펴보겠습니다.

컨볼루션

model.add(tf.layers.conv2d({
  inputShape: [IMAGE_WIDTH, IMAGE_HEIGHT, IMAGE_CHANNELS],
  kernelSize: 5,
  filters: 8,
  strides: 1,
  activation: 'relu',
  kernelInitializer: 'varianceScaling'
}));

여기서는 순차적 모델을 사용합니다.

밀집 레이어 대신 conv2d 레이어를 사용합니다. 여기서는 컨볼루션 작동 방식을 자세히 살펴보지는 않지만 기본 작업을 설명하는 몇 가지 리소스를 소개합니다.

conv2d에 대한 구성 객체의 각 인수를 자세히 살펴보겠습니다.

  • inputShape: 모델의 첫 번째 레이어로 전달될 데이터의 모양입니다. 이 경우의 MNIST 예시는 28x28픽셀 흑백 이미지입니다. 이미지 데이터의 표준 형식은 [row, column, depth]이므로 여기서는 [28, 28, 1]의 모양을 구성합니다. 각 차원의 픽셀 수로 28개의 행과 열이 있고 이미지에 색상 채널이 1개만 있으므로 깊이는 1입니다. 입력 모양에 배치 크기를 지정하지 않는다는 점에 유의하세요. 레이어는 배치 크기의 제약이 없도록 설계되므로 추론 중에 모든 배치 크기의 텐서를 전달할 수 있습니다.
  • kernelSize: 입력 데이터에 적용되는 슬라이딩 컨볼루셔널 필터 창의 크기입니다. 여기서는 kernelSize5로 설정하며 정사각형의 5x5 컨볼루셔널 창을 나타냅니다.
  • filters: 입력 데이터에 적용할 kernelSize 크기의 필터 창 수입니다. 여기서는 데이터에 8개의 필터를 적용합니다.
  • strides: 슬라이딩 창의 '보폭'(즉, 이미지에서 이동할 때마다 필터가 변경하는 픽셀 수)입니다. 여기서는 스트라이드를 1로 지정합니다. 필터가 1픽셀 보폭으로 이미지에서 슬라이딩한다는 의미입니다.
  • activation: 컨볼루션이 완료된 후 데이터에 적용할 활성화 함수입니다. 이 경우에는 ML 모델에서 일반적인 활성화 함수인 정류 선형 유닛(ReLU) 함수를 적용합니다.
  • kernelInitializer: 모델 가중치를 무작위로 초기화하는 데 사용하는 메서드로서 동적인 학습에 매우 중요합니다. 여기서는 초기화에 대해 자세히 다루지 않지만 여기서 사용되는 VarianceScaling은 일반적으로 유용한 초기화 옵션입니다.

데이터 표현 평면화

model.add(tf.layers.flatten());

이미지는 고차원 데이터이며 컨볼루션 연산에서는 전달된 데이터의 크기를 늘리는 경향이 있습니다. 최종 분류 레이어로 전달하기 전에 데이터를 하나의 긴 배열로 평면화해야 합니다. 최종 레이어로 사용하는 밀집 레이어에서 tensor1d만 사용하므로 이 단계는 대부분의 분류 작업에서 일반적으로 사용됩니다.

최종 확률 분포 계산

const NUM_OUTPUT_CLASSES = 10;
model.add(tf.layers.dense({
  units: NUM_OUTPUT_CLASSES,
  kernelInitializer: 'varianceScaling',
  activation: 'softmax'
}));

밀집 레이어와 소프트맥스 활성화를 함께 사용하여 가능한 10개의 클래스에 대한 확률 분포를 계산합니다. 점수가 가장 높은 클래스가 예측 숫자가 됩니다.

옵티마이저 및 손실 함수 선택

const optimizer = tf.train.adam();
model.compile({
  optimizer: optimizer,
  loss: 'categoricalCrossentropy',
  metrics: ['accuracy'],
});

옵티마이저, 손실 함수, 추적할 측정항목을 지정하는 모델을 컴파일합니다.

첫 번째 튜토리얼과 달리 여기서는 categoricalCrossentropy를 손실 함수로 사용합니다. 이름에서 알 수 있듯이 이 함수는 모델의 출력이 확률 분포일 때 사용됩니다. categoricalCrossentropy는 모델의 마지막 레이어에서 생성된 확률 분포와 True 라벨에서 지정한 확률 분포 간의 오차를 측정합니다.

예를 들어 숫자가 실제로 7을 나타낼 경우 다음과 같은 결과가 표시될 수 있습니다.

색인

0

1

2

3

4

5

6

7

8

9

True 라벨

0

0

0

0

0

0

0

1

0

0

예측

0.1

0.01

0.01

0.01

0.20

0.01

0.01

0.60

0.03

0.02

범주형 교차 엔트로피는 예측 벡터가 True 라벨 벡터와 얼마나 유사한지를 나타내는 하나의 숫자를 생성합니다.

여기서 라벨에 사용되는 데이터 표현을 원-핫 인코딩이라고 하며 분류 문제에서 널리 사용됩니다. 각 예시의 클래스마다 연결된 확률이 있습니다. 어떻게 설정해야 할지 정확히 안다면 이 확률을 1로 설정하고 다른 확률은 0으로 설정하면 됩니다. 원-핫 인코딩에 대한 자세한 내용은 이 페이지를 참조하세요.

모니터링할 다른 측정항목은 accuracy이며 분류 문제에 대한 모든 예측 중 올바른 예측의 비율입니다.

96914ff65fc3b74c.png다음 함수를 script.js 파일에 복사합니다.

async function train(model, data) {
  const metrics = ['loss', 'val_loss', 'acc', 'val_acc'];
  const container = {
    name: 'Model Training', tab: 'Model', styles: { height: '1000px' }
  };
  const fitCallbacks = tfvis.show.fitCallbacks(container, metrics);

  const BATCH_SIZE = 512;
  const TRAIN_DATA_SIZE = 5500;
  const TEST_DATA_SIZE = 1000;

  const [trainXs, trainYs] = tf.tidy(() => {
    const d = data.nextTrainBatch(TRAIN_DATA_SIZE);
    return [
      d.xs.reshape([TRAIN_DATA_SIZE, 28, 28, 1]),
      d.labels
    ];
  });

  const [testXs, testYs] = tf.tidy(() => {
    const d = data.nextTestBatch(TEST_DATA_SIZE);
    return [
      d.xs.reshape([TEST_DATA_SIZE, 28, 28, 1]),
      d.labels
    ];
  });

  return model.fit(trainXs, trainYs, {
    batchSize: BATCH_SIZE,
    validationData: [testXs, testYs],
    epochs: 10,
    shuffle: true,
    callbacks: fitCallbacks
  });
}

96914ff65fc3b74c.png 다음 코드를

run 함수에 추가합니다.

const model = getModel();
tfvis.show.modelSummary({name: 'Model Architecture', tab: 'Model'}, model);

await train(model, data);

페이지를 새로고침하면 몇 초 후에 학습 진행 상황을 보고하는 그래프가 표시됩니다.

a2c7628dc47d465.png

좀 더 자세히 살펴보겠습니다.

측정항목 모니터링

const metrics = ['loss', 'val_loss', 'acc', 'val_acc'];

여기서는 모니터링할 측정항목을 결정합니다. 학습 세트에서의 손실과 정확성을 모니터링하고 검증 세트에서의 손실과 정확성(각각 val_loss 및 val_acc)도 모니터링합니다. 검증 세트에 대한 자세한 내용은 아래에서 다룹니다.

데이터를 텐서로 준비

const BATCH_SIZE = 512;
const TRAIN_DATA_SIZE = 5500;
const TEST_DATA_SIZE = 1000;

const [trainXs, trainYs] = tf.tidy(() => {
  const d = data.nextTrainBatch(TRAIN_DATA_SIZE);
  return [
    d.xs.reshape([TRAIN_DATA_SIZE, 28, 28, 1]),
    d.labels
  ];
});

const [testXs, testYs] = tf.tidy(() => {
  const d = data.nextTestBatch(TEST_DATA_SIZE);
  return [
    d.xs.reshape([TEST_DATA_SIZE, 28, 28, 1]),
    d.labels
  ];
});

여기서는 2개의 데이터 세트, 즉 모델을 학습시킬 학습 세트와 각 세대가 끝날 때 모델을 테스트할 검증 세트를 만듭니다. 단, 검증 세트의 데이터는 학습 중에 모델에 표시되지 않습니다.

제공된 데이터 클래스를 사용하면 이미지 데이터에서 텐서를 쉽게 가져올 수 있습니다. 하지만 모델에 전달하기 전에 모델에서 예상하는 모양([num_examples, image_width, image_height, channels])으로 텐서를 바꿔야 합니다. 각 데이터 세트에는 입력(X)과 라벨(Y)이 모두 있습니다.

return model.fit(trainXs, trainYs, {
  batchSize: BATCH_SIZE,
  validationData: [testXs, testYs],
  epochs: 10,
  shuffle: true,
  callbacks: fitCallbacks
});

model.fit을 호출해 학습 루프를 시작합니다. 또한 각 세대가 끝난 후 모델이 테스트에 사용할 데이터(학습에는 사용하지 않음)를 나타내기 위해 validationData 속성도 전달합니다.

학습 데이터는 결과가 좋지만 검증 데이터는 결과가 좋지 않다면 모델이 학습 데이터에 과적합했을 가능성이 크며 이전에 인식되지 않은 입력에 효과적으로 일반화되지 않는다는 의미입니다.

검증 정확성은 이전에 인식되지 않은 데이터(어떤 식으로든 검증 세트와 유사한 데이터)에서 모델이 어떻게 작동할지를 예측하는 데 도움이 됩니다. 하지만 다양한 클래스에서 성능을 상세 분석하고 싶을 수도 있습니다.

이 경우 도움이 될 수 있는 tfjs-vis의 메서드가 몇 가지 있습니다.

96914ff65fc3b74c.png scripts.js 파일 하단에 다음 코드를 추가합니다.

const classNames = ['Zero', 'One', 'Two', 'Three', 'Four', 'Five', 'Six', 'Seven', 'Eight', 'Nine'];

function doPrediction(model, data, testDataSize = 500) {
  const IMAGE_WIDTH = 28;
  const IMAGE_HEIGHT = 28;
  const testData = data.nextTestBatch(testDataSize);
  const testxs = testData.xs.reshape([testDataSize, IMAGE_WIDTH, IMAGE_HEIGHT, 1]);
  const labels = testData.labels.argMax(-1);
  const preds = model.predict(testxs).argMax(-1);

  testxs.dispose();
  return [preds, labels];
}

async function showAccuracy(model, data) {
  const [preds, labels] = doPrediction(model, data);
  const classAccuracy = await tfvis.metrics.perClassAccuracy(labels, preds);
  const container = {name: 'Accuracy', tab: 'Evaluation'};
  tfvis.show.perClassAccuracy(container, classAccuracy, classNames);

  labels.dispose();
}

async function showConfusion(model, data) {
  const [preds, labels] = doPrediction(model, data);
  const confusionMatrix = await tfvis.metrics.confusionMatrix(labels, preds);
  const container = {name: 'Confusion Matrix', tab: 'Evaluation'};
  tfvis.render.confusionMatrix(container, {values: confusionMatrix, tickLabels: classNames});

  labels.dispose();
}

이 코드의 역할

  • 예측 수행
  • 정확성 측정항목 계산
  • 측정항목 표시

각 단계를 자세히 살펴보겠습니다.

예측 수행

function doPrediction(model, data, testDataSize = 500) {
  const IMAGE_WIDTH = 28;
  const IMAGE_HEIGHT = 28;
  const testData = data.nextTestBatch(testDataSize);
  const testxs = testData.xs.reshape([testDataSize, IMAGE_WIDTH, IMAGE_HEIGHT, 1]);
  const labels = testData.labels.argMax(-1);
  const preds = model.predict(testxs).argMax(-1);

  testxs.dispose();
  return [preds, labels];
}

먼저 예측을 수행해야 합니다. 여기서는 이미지 500개를 가져와 이미지에 포함된 숫자를 예측합니다. 나중에 수를 늘려 더 많은 이미지 모음에서 테스트해 볼 수 있습니다.

특히 argmax 함수는 확률이 가장 높은 클래스 색인을 제공합니다. 모델은 각 클래스의 확률을 출력한다는 점을 유의하세요. 여기서는 가장 높은 확률을 찾아 이를 예측으로 할당합니다.

또한 한 번에 500개의 예시 모두에 대한 예측을 수행할 수도 있습니다. 이 작업은 TensorFlow.js에서 제공하는 벡터화 기능 덕분에 가능합니다.

클래스당 정확성 표시

async function showAccuracy() {
  const [preds, labels] = doPrediction();
  const classAccuracy = await tfvis.metrics.perClassAccuracy(labels, preds);
  const container = { name: 'Accuracy', tab: 'Evaluation' };
  tfvis.show.perClassAccuracy(container, classAccuracy, classNames);

  labels.dispose();
}

예측 집합과 라벨을 사용해 각 클래스의 정확성을 계산할 수 있습니다.

혼동 행렬 표시

async function showConfusion() {
  const [preds, labels] = doPrediction();
  const confusionMatrix = await tfvis.metrics.confusionMatrix(labels, preds);
  const container = { name: 'Confusion Matrix', tab: 'Evaluation' };
  tfvis.render.confusionMatrix(container, {values: confusionMatrix, tickLabels: classNames});

  labels.dispose();
}

혼동 행렬은 클래스당 정확성과 비슷하지만 상세 분석을 통해 잘못된 분류의 패턴을 보여줍니다. 이를 통해 모델에 특정 클래스 쌍에 대한 혼동이 있는지 확인할 수 있습니다.

평가 표시

96914ff65fc3b74c.png 실행 함수의 하단에 다음 코드를 추가하여 평가를 표시합니다.

await showAccuracy(model, data);
await showConfusion(model, data);

다음과 같이 표시됩니다.

82458197bd5e7f52.png

수고하셨습니다. 지금까지 컨볼루셔널 신경망을 학습시켰습니다.

입력 데이터의 카테고리 예측을 분류 작업이라고 합니다.

분류 작업을 사용하려면 라벨에 대한 적절한 데이터 표현이 필요합니다.

  • 라벨의 일반적인 표현에는 카테고리의 원-핫 인코딩이 포함됩니다.

데이터 준비

  • 모델에 평가하는 데 사용할 수 있도록 학습 중에 모델에서 인식한 적이 없는 일부 데이터를 따로 보관하는 것이 좋습니다. 이를 검증 세트라고 합니다.

모델 빌드 및 실행

  • 컨볼루셔널 모델은 이미지 작업에 효과적인 것으로 나타났습니다.
  • 분류 문제에서는 일반적으로 손실 함수에 범주형 교차 엔트로피를 사용합니다.
  • 학습을 모니터링하여 손실이 감소하고 정확성이 개선되는지 확인합니다.

모델 평가

  • 학습이 끝난 후 모델을 평가할 방법을 결정하여 해결하고자 하는 초기 문제에서 잘 작동하는지 확인합니다.
  • 클래스당 정확성과 혼동 행렬은 전체적인 정확성보다는 모델 성능을 상세 분석하는 데 유용합니다.