Довгий час я шукав хороший приклад, щоб пояснити своїм студентам і колегам, як відбувається магія навчання нейронів. Теоретично все виглядає красиво: є ваги, є зв’язки, є активація. Але розібратися, як саме встановлюються ваги, і тим більше відчути цей процес, ніяк не вдавалося. Мені потрібно було знайти приклад, який був би логічно вирішуваним, але водночас демонстрував сенс застосування навчання. І я такий приклад знайшов.
Кожен із нас у сучасному світі постійно стикається із семисегментним індикатором. Якщо у вас є калькулятор, то, швидше за все, відображення цифр на ньому — це саме такий підхід. У нас є 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 цифр, виконуючи наступні дії для кожної цифри:
- Обчислює суму сигналів від нейронів через функцію
calcSum
. - Використовує сигмоїдну функцію активації
calcSigmoid
для отримання прогнозованого результату. - Визначає очікуване значення, яке дорівнює 1, якщо поточна цифра відповідає цільовій (цільова цифра передається через
props.number
), інакше 0. - Обчислює помилку як різницю між прогнозованим результатом і очікуваним значенням.
- Виправляє ваги на основі обчисленої помилки, застосовуючи формулу коригування, яка враховує похідну сигмоїдної функції активації (це здійснюється через вираз
error * sigmoid * (1 - sigmoid)
). - Оновлює ваги для кожного з 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/