Довгий час я шукав хороший приклад, щоб пояснити своїм студентам і колегам, як відбувається магія навчання нейронів. Теоретично все виглядає красиво: є ваги, є зв’язки, є активація. Але розібратися, як саме встановлюються ваги, і тим більше відчути цей процес, ніяк не вдавалося. Мені потрібно було знайти приклад, який був би логічно вирішуваним, але водночас демонстрував сенс застосування навчання. І я такий приклад знайшов.

Кожен із нас у сучасному світі постійно стикається із семисегментним індикатором. Якщо у вас є калькулятор, то, швидше за все, відображення цифр на ньому — це саме такий підхід. У нас є 7 елементів (сегментів), які визначають, що це за число. Наприклад, якщо горять тільки сегменти ‘b’ і ‘c’, а решта вимкнені, то це цифра 1. А якщо горить ще й сегмент ‘a’, то це вже 7. Таким чином, написавши 10 умов if і реалізувавши алгоритм розгалуження, ми зможемо відповісти, яка цифра відображена на екрані.

А що якщо ми підійдемо до завдання не у звичній для розробників парадигмі, а за допомогою нейронної мережі? Тоді я створюю клас “нейрон”, який буде з’єднаний з усіма сімома вхідними сигналами. На кожен сигнал у нейрона буде свій ваг.

Для візуалізації рішення я скористався фреймворком Vue та його можливостями. Давайте подивимося, що з цього вийшло.

Вхідний шар

Щоб моя нейронна мережа запрацювала, мені потрібно було реалізувати “вхідний шар” — набір нейронів, кожен з яких відповідає за конкретний сегмент семисегментного індикатора. Цей шар міститиме сім нейронів, відповідних семи сегментам, і один нейрон зміщення (bias). Нейрон зміщення допомагає покращити навчання мережі, додаючи додатковий параметр для налаштування. Я також передбачив можливість перемикання стану сегментів на цьому шарі, щоб можна було легко тестувати різні комбінації увімкнених та вимкнених сегментів.

Доступ до цього шару мені знадобиться з усього мого застосунку, тому логічно буде використовувати можливості Pinia

export const useSevenSegmentState = defineStore('states', {
  state: () => ({
    matrix: Array.from({ length: 10 }, () => Array.from({ length: 7 }, () => false)),
    current: 0
  }),
  getters: {
// ...
  },
  actions: {
// ...
    fillMatrix(): void {
      // a     b      c      d      e      f      g
      this.matrix[0] = [true, true, true, true, true, true, false, true]
      this.matrix[1] = [false, true, true, false, false, false, false, true]
      this.matrix[2] = [true, true, false, true, true, false, true, true]
      this.matrix[3] = [true, true, true, true, false, false, true, true]
      this.matrix[4] = [false, true, true, false, false, true, true, true]
      this.matrix[5] = [true, false, true, true, false, true, true, true]
      this.matrix[6] = [true, false, true, true, true, true, true, true]
      this.matrix[7] = [true, true, true, false, false, false, false, true]
      this.matrix[8] = [true, true, true, true, true, true, true, true]
      this.matrix[9] = [true, true, true, true, false, true, true, true]
    }
  }
})

Тепер створимо компонент, який дозволить користувачеві перемикати значення індикатора, а також візуалізувати значення на ньому.

<script setup lang="ts">
import { useSevenSegmentState } from '@/stores/sevenSegmentState'
import { computed } from 'vue'
import Neuron from '@/components/Neuron.vue'

const segments = useSevenSegmentState()
segments.fillMatrix()

const currentValue = computed({
  get() {
    return segments.getCurrent
  },
  set(val) {
    segments.setCurrent(val)
  }
})
</script>

<template>
// ...
      <div class="mb-4">
        <input type="number" min="0" max="9" v-model="currentValue" class="w-full p-2" />
      </div>
      <div id="sevenSegmentDisplay">
        <div id="a" :class="[segments.getCurrentSegments[0] ? 'on' : 'off']"></div>
        <div id="b" :class="[segments.getCurrentSegments[1] ? 'on' : 'off']"></div>
        <div id="c" :class="[segments.getCurrentSegments[2] ? 'on' : 'off']"></div>
        <div id="d" :class="[segments.getCurrentSegments[3] ? 'on' : 'off']"></div>
        <div id="e" :class="[segments.getCurrentSegments[4] ? 'on' : 'off']"></div>
        <div id="f" :class="[segments.getCurrentSegments[5] ? 'on' : 'off']"></div>
        <div id="g" :class="[segments.getCurrentSegments[6] ? 'on' : 'off']"></div>
      </div>

// ...
</template>

Візуальна частина готова – перейдемо до суті проєкту.

Нейрон

Кожен нейрон буде відповідати лише за одну цифру. Відповідно, окрім того, що вибрав користувач для аналізу, він має містити ще й вагу кожного елемента. Підключимо наш вхідний шар до нейрона, а цифру передамо параметром.

<script setup lang="ts">

import { onMounted, type Ref, ref, watch } from 'vue'
import { useSevenSegmentState } from '@/stores/sevenSegmentState'
import { initAccordions } from 'flowbite'

const props = defineProps({
  number: Number
})


const segments = useSevenSegmentState()
const weights = Array.from({ length: 8 }, () => Math.random())

// ...
</script> 

Тоді функція активації нейрона виглядатиме так:

<script setup lang="ts">
//  ...

const calcNeuronActivation = (): number => {
  if (weights.length !== segments.getCurrentSegments.length) {
    throw new Error('The length of weights array must be equal to the length of segments array')
  }

  let activation = 0
  for (let i = 0; i < weights.length; i++) {
    activation += weights[i] * (segments.getCurrentSegments[i] ? 1 : 0)
  }

  neuronActivation.value = activation

  return activation
}

// ...
</script> 

Залишилося відобразити результати

<template>
// ...

    <div class="p-4 border border-b-0 border-gray-200 dark:border-gray-700 dark:bg-gray-900">
      <div class="flex">
        <div class="w-1/2">
          <ol type="a" class="text-gray-500 list-outside list-lower-alpha">
            <li v-for="(weight, index) in weights" :key="index">
              {{ weight.toFixed(4) }}
            </li>
          </ol>
        </div>
        <div class="w-1/2 pl-4">
          <p class="text-sky-400/100">{{ props.number }} ==> {{ neuronActivation }}</p>
          <button class="bg-sky-500 text-white px-4 py-2 mt-2" @click="educationFor">Edu</button>
        </div>
      </div>
    </div><template>
    <div class="p-4 border border-b-0 border-gray-200 dark:border-gray-700 dark:bg-gray-900">
      <div class="flex">
        <div class="w-1/2">
          <ol type="a" class="text-gray-500 list-outside list-lower-alpha">
            <li v-for="(weight, index) in weights" :key="index">
              {{ weight.toFixed(4) }}
            </li>
          </ol>
        </div>
        <div class="w-1/2 pl-4">
          <p class="text-sky-400/100">{{ props.number }} ==> {{ neuronActivation }}</p>
          <button class="bg-sky-500 text-white px-4 py-2 mt-2" @click="educationFor">Edu</button>
        </div>
      </div>
    </div>

// ...
<template>

Процес навчання

Тепер можемо описати крок навчання нашого нейрона. Для цього ми покажемо йому всі 10 цифр, при цьому знаючи, яка цифра є правильною.

<script setup lang="ts">
//  ...

const educationStep = () => {
  for (let i = 0; i < 10; i++) {
    let sum = calcSum(i)
    let sigmoid = calcSigmoid(sum)

    let expected = 0
    if (i == props.number) expected = 1

    let error = sigmoid - expected
    let correct = error * sigmoid * (1 - sigmoid)

    let rate = 0.1

    for (let j = 0; j < 8; j++) {
      weights[j] = weights[j] - (segments.getMatrix[i][j] ? 1 : 0) * correct * rate
    }
  }
}

//  ...
</script>

Цей метод educationStep використовується для навчання нейронної мережі з використанням методу навчання з учителем. Він ітеративно проходить через всі 10 цифр, виконуючи наступні дії для кожної цифри:

  1. Обчислює суму сигналів від нейронів через функцію calcSum.
  2. Використовує сигмоїдну функцію активації calcSigmoid для отримання прогнозованого результату.
  3. Визначає очікуване значення, яке дорівнює 1, якщо поточна цифра відповідає цільовій (цільова цифра передається через props.number), інакше 0.
  4. Обчислює помилку як різницю між прогнозованим результатом і очікуваним значенням.
  5. Виправляє ваги на основі обчисленої помилки, застосовуючи формулу коригування, яка враховує похідну сигмоїдної функції активації (це здійснюється через вираз error * sigmoid * (1 - sigmoid)).
  6. Оновлює ваги для кожного з 8 входів (припускаючи, що існує 7 сегментів плюс один нейрон зсуву) з використанням матриці сегментів segments.getMatrix[i][j], зменшуючи кожну вагу на величину, що пропорційна помилці, коефіцієнту навчання rate та стану сегменту.

Такий підхід дозволяє моделі вчитися розпізнавати цифри, поступово коригуючи ваги на основі помилок у передбаченнях, щоб краще відповідати даним.

Підсумок

Самі по собі нейронні мережі — це математика. Це досягнення цивілізації, де теорія підкріплена масивами даних та обчислювальними потужностями.

Звісно, мережа з 10 нейронів не має особливого практичного значення, але завдяки цьому ми можемо побачити, як саме відбувається корекція ваг, та скільки ресурсів потрібно для більш складних моделей.

Ознайомитися з кодом можна в github проекту: https://github.com/ninydev-com/seven-segment-ai/
Також там доступна демо-версія: https://ninydev-com.github.io/seven-segment-ai/

Схожі записи