С.Короткий Нейронные сети: основные положения В статье

advertisement
С.Короткий
Нейронные сети: основные положения
В статье рассмотрены основы теории нейронных сетей, позволяющие в
дальнейшем обратиться к конкретным структурам, алгоритмам и идеологии
практического применения сетей в компьютерных приложениях.
В последние десятилетия в мире бурно развивается новая прикладная область
математики, специализирующаяся на искусственных нейронных сетях (НС).
Актуальность исследований в этом направлении подтверждается массой различных
применений НС. Это автоматизация процессов распознавания образов, адаптивное
управление, аппроксимация функционалов, прогнозирование, создание экспертных
систем, организация ассоциативной памяти и многие другие приложения. С помощью
НС можно, например, предсказывать показатели биржевого рынка, выполнять
распознавание оптических или звуковых сигналов, создавать самообучающиеся
системы, способные управлять автомашиной при парковке или синтезировать речь по
тексту. В то время как на западе применение НС уже достаточно обширно, у нас это
еще в некоторой степени экзотика – российские фирмы, использующие НС в
практических целях, наперечет [1].
Широкий круг задач, решаемый НС, не позволяет в настоящее время создавать
универсальные, мощные сети, вынуждая разрабатывать специализированные НС,
функционирующие по различным алгоритмам.
Модели НС могут быть программного и аппаратного исполнения. В дальнейшем
речь пойдет в основном о первом типе.
Несмотря на существенные различия, отдельные типы НС обладают несколькими
общими чертами.
Во-первых, основу каждой НС составляют относительно простые, в большинстве
случаев – однотипные, элементы (ячейки), имитирующие работу нейронов мозга. Далее
под нейроном будет подразумеваться искусственный
нейрон, то есть ячейка НС. Каждый нейрон
характеризуется своим текущим состоянием по
аналогии с нервными клетками головного мозга,
которые могут быть возбуждены или заторможены. Он
обладает группой синапсов – однонаправленных
входных связей, соединенных с выходами других
нейронов, а также имеет аксон – выходную связь
Рис.1 Искусственный
данного нейрона, с которой сигнал (возбуждения или
нейрон
торможения) поступает на синапсы следующих нейронов. Общий вид нейрона
приведен на рисунке 1. Каждый синапс характеризуется величиной синаптической
связи или ее весом wi, который по физическому смыслу эквивалентен электрической
проводимости.
Текущее состояние нейрона определяется, как взвешенная сумма его входов:
n
s = ∑ x i ⋅ wi
(1)
Выход нейрона есть функция его состояния:
y = f(s)
(2)
i =1
1
Нелинейная функция f называется активационной
и может иметь различный вид, как показано на рисунке
2. Одной из наиболее распространеных является
нелинейная функция с насыщением, так называемая
логистическая функция или сигмоид (т.е. функция Sобразного вида)[2]:
1
(3)
f ( x) =
1 + e − αx
Рис.2
а)
функция
При уменьшении α сигмоид становится более
единичного
скачка;
б) пологим, в пределе при α=0 вырождаясь в
линейный порог (гистере- горизонтальную линию на уровне 0.5, при увеличении α
зис); в) сигмоид – гипербо- сигмоид приближается по внешнему виду к функции
лический
тангенс;
г) единичного скачка с порогом T в точке x=0. Из
сигмоид – формула (3)
выражения для сигмоида очевидно, что выходное
значение нейрона лежит в диапазоне [0,1]. Одно из ценных свойств сигмоидной
функции – простое выражение для ее производной, применение которого будет
рассмотрено в дальнейшем.
(4)
f '( x ) = α ⋅ f ( x ) ⋅ (1 − f ( x ))
Следует отметить, что сигмоидная функция дифференцируема на всей оси
абсцисс, что используется в некоторых алгоритмах обучения. Кроме того она обладает
свойством усиливать слабые сигналы лучше, чем большие, и предотвращает насыщение
от больших сигналов, так как они соответствуют областям аргументов, где сигмоид
имеет пологий наклон.
Возвращаясь к общим чертам, присущим всем НС, отметим, во-вторых, принцип
параллельной обработки сигналов, который достигается путем объединения большого
числа нейронов в так называемые слои и соединения определенным образом нейронов
различных слоев, а также, в некоторых конфигурациях, и нейронов одного слоя между
собой, причем обработка взаимодействия всех нейронов ведется послойно.
В качестве примера простейшей НС рассмотрим
трехнейронный перцептрон (рис.3), то есть такую сеть,
нейроны которой имеют активационную функцию в виде
единичного скачка* . На n входов поступают некие
сигналы, проходящие по синапсам на 3 нейрона,
образующие единственный слой этой НС и выдающие
три выходных сигнала:
 n

(5)
y j = f ∑ xi ⋅ wij  , j=1...3
Рис.3 Однослойный
 i =1

перцептрон
Очевидно, что все весовые коэффициенты синапсов
одного слоя нейронов можно свести в матрицу W, в которой каждый элемент wij задает
величину i-ой синаптической связи j-ого нейрона. Таким образом, процесс,
происходящий в НС, может быть записан в матричной форме:
(6)
Y=F(XW)
где X и Y – соответственно входной и выходной сигнальные векторы, F( V) –
активационная функция, применяемая поэлементно к компонентам вектора V.
*
Иногда перцептроном называют любую НС слоистой структуры, однако здесь и далее под
перцептроном понимается только сеть, состоящая из нейронов с активационными функциями единичного
скачка (бинарная сеть).
2
Теоретически число слоев и число нейронов в каждом слое может быть
произвольным, однако фактически оно ограничено ресурсами компьютера или
специализированной микросхемы, на которых обычно реализуется НС. Чем сложнее
НС, тем масштабнее задачи, подвластные ей.
Выбор структуры НС осуществляется в соответствии с особенностями и
сложностью задачи. Для решения некоторых отдельных типов задач уже существуют
оптимальные, на сегодняшний день, конфигурации, описанные, например, в [2],[3],[4]
и других изданиях, перечисленных в конце статьи. Если же задача не может быть
сведена ни к одному из известных типов, разработчику приходится решать сложную
проблему синтеза новой конфигурации. При этом он руководствуется несколькими
основополагающими принципами: возможности сети возрастают с увеличением числа
ячеек сети, плотности связей между ними и числом выделенных слоев (влияние числа
слоев на способность сети выполнять классификацию плоских образов показано на
рис.4 из [5]); введение обратных связей наряду с увеличением возможностей сети
поднимает вопрос о динамической устойчивости сети; сложность алгоритмов
функционирования сети (в том числе, например, введение нескольких типов синапсов –
возбуждающих, тромозящих и др.) также способствует усилению мощи НС. Вопрос о
необходимых и достаточных свойствах сети для решения того или иного рода задач
представляет собой целое направление нейрокомпьютерной науки. Так как проблема
синтеза НС сильно зависит от решаемой задачи, дать общие подробные рекомендации
затруднительно. В большинстве случаев оптимальный вариант получается на основе
интуитивного подбора.
Очевидно, что процесс функционирования НС, то есть сущность действий,
которые она способна выполнять, зависит от величин синаптических связей, поэтому,
задавшись определенной структурой НС, отвечающей какой-либо задаче, разработчик
сети должен найти оптимальные значения всех переменных весовых коэффициентов
(некоторые синаптические связи могут быть постоянными).
Этот этап называется обучением НС, и от того, насколько качественно он будет
выполнен, зависит способность сети решать поставленные перед ней проблемы во
время эксплуатации. На этапе обучения кроме параметра качества подбора весов
важную роль играет время обучения. Как правило, эти два параметра связаны обратной
зависимостью и их приходится выбирать на основе компромисса.
Обучение НС может вестись с учителем или без него. В первом случае сети
предъявляются значения как входных, так и желательных выходных сигналов, и она по
некоторому внутреннему алгоритму подстраивает веса своих синаптических связей. Во
втором случае выходы НС формируются самостоятельно, а веса изменяются по
алгоритму, учитывающему только входные и производные от них сигналы.
Существует великое множество различных алгоритмов обучения, которые однако
делятся на два больших класса: детерминистские и стохастические. В первом из них
подстройка весов представляет собой жесткую последовательность действий, во втором
– она производится на основе действий, подчиняющихся некоторому случайному
процессу.
Развивая дальше вопрос о возможной классификации НС, важно отметить
существование бинарных и аналоговых сетей. Первые из них оперируют с двоичными
сигналами, и выход каждого нейрона может принимать только два значения:
логический ноль ("заторможенное" состояние) и логическая единица ("возбужденное"
состояние). К этому классу сетей относится и рассмотренный выше перцептрон, так как
выходы его нейронов, формируемые функцией единичного скачка, равны либо 0, либо
1. В аналоговых сетях выходные значения нейронов способны принимать непрерывные
3
значения, что могло бы иметь место после замены активационной функции нейронов
перцептрона на сигмоид.
Еще одна классификация делит НС на синхронные и асинхронные[3]. В первом
случае в каждый момент времени свое состояние меняет лишь один нейрон. Во втором
– состояние меняется сразу у целой группы нейронов, как правило, у всего слоя.
Алгоритмически ход времени в НС задается итерационным выполнением однотипных
действий над нейронами. Далее будут рассматриваться только синхронные НС.
Сети также можно классифицировать по числу
слоев. На рисунке 4 представлен двухслойный
перцептрон, полученный из перцептрона с рисунка 3
путем добавления второго слоя, состоящего из двух
нейронов. Здесь уместно отметить важную роль
нелинейности активационной функции, так как, если бы
она не обладала данным свойством или не входила в
алгоритм работы каждого нейрона,
результат
Рис.4 Двухслойный
функционирования любой p-слойной НС с весовыми
перцептрон
матрицами W(i), i=1,2,...p для каждого слоя i сводился бы
к перемножению входного вектора сигналов X на матрицу
(7)
W(Σ)=W(1)⋅W(2) ⋅...⋅W(p)
то есть фактически такая p-слойная НС эквивалентна однослойной НС с весовой
матрицей единственного слоя W(Σ):
(8)
Y=XW(Σ)
Продолжая разговор о нелинейности, можно отметить, что она иногда вводится и
в синаптические связи. Большинство известных на сегодняшний день НС используют
для нахождения взвешенной суммы входов нейрона формулу (1), однако в некоторых
приложениях НС полезно ввести другую запись, например:
n
s = ∑ xi2 ⋅ wi
(9)
i =1
или даже
n
s = ∑ xi ⋅ x (( i +1) mod n ) ⋅ wi
(10)
i =1
Вопрос в том, чтобы разработчик НС четко понимал, для чего он это делает,
какими ценными свойствами он тем самым дополнительно наделяет нейрон, и каких
лишает. Введение такого рода нелинейности, вообще говоря, увеличивает
вычислительную мощь сети, то есть позволяет из меньшего числа нейронов с
"нелинейными" синапсами сконструировать НС, выполняющую работу обычной НС с
большим числом стандартных нейронов и более сложной конфигурации[4].
Многообразие существующих структур НС позволяет отыскать и другие критерии
для их классификации, но они выходят за рамки данной статьи.
Теперь рассмотрим один нюанс, преднамеренно опущенный ранее. Из рисунка
функции единичного скачка видно, что пороговое значение T, в общем случае, может
принимать произвольное значение. Более того, оно должно принимать некое
произвольное, неизвестное заранее значение, которое подбирается на стадии обучения
вместе с весовыми коэффициентами. То же самое относится и к центральной точке
сигмоидной зависимости, которая может сдвигаться вправо или влево по оси X, а также
и ко всем другим активационным функциям. Это, однако, не отражено в формуле (1),
которая должна была бы выглядеть так:
4
n
s = ∑ xi ⋅ wi − T
(11)
i =1
Дело в том, что такое смещение обычно вводится путем добавления к слою
нейронов еще одного входа, возбуждающего дополнительный синапс каждого из
нейронов, значение которого всегда равняется 1. Присвоим этому входу номер 0. Тогда
n
s = ∑ xi ⋅ wi
(12)
i =0
где w0 = –T, x0 = 1.
Очевидно, что различие формул (1) и (12) состоит лишь в способе нумерации
входов.
Из всех активационных функций, изображенных на рисунке 2, одна выделяется
особо. Это гиперболический тангенс, зависимость которого симметрична относительно
оси X и лежит в диапазоне [-1,1]. Забегая вперед, скажем, что выбор области
возможных значений выходов нейронов во многом зависит от конкретного типа НС и
является вопросом реализации, так как манипуляции с ней влияют на различные
показатели эффективности сети, зачастую не изменяя общую логику ее работы.
Пример, иллюстрирующий данный аспект, будет представлен после перехода от
общего описания к конкретным типам НС.
Какие задачи может решать НС? Грубо говоря,
работа всех сетей сводится к классификации (обобщению)
входных сигналов, принадлежащих n-мерному гиперпространству, по некоторому числу классов. С математической точки зрения это происходит путем разбиения Рис.5 Однонейронный
гиперпространства гиперплоскостями (запись для случая
перцептрон
однослойного перцептрона)
n
∑x
i
⋅ wik = Tk , k=1...m
(13)
i =1
Каждая полученная область является областью определения отдельного класса.
Число таких классов для одной НС перцептронного типа не
Таблица
превышает 2m, где m – число выходов сети. Однако не все из них
1
могут быть разделимы данной НС.
0 1
Например, однослойный перцептрон, состоящий из одного x1
нейрона с двумя входами, представленный на рисунке 5, не способен x2
разделить плоскость (двумерное гиперпространоство) на две 0
A B
полуплоскости так, чтобы осуществить классификацию входных 1
B A
сигналов по классам A и B (см. таблицу 1).
Уравнение сети для этого случая
(14)
x1 ⋅ w1 + x 2 ⋅ w2 = T
является уравнением прямой (одномерной гиперплоскости), которая ни при каких
условиях не может разделить плоскость так, чтобы точки из множества входных
сигналов, принадлежащие разным классам, оказались по разные стороны от прямой
(см. рисунок 6).
Если присмотреться к таблице 1, можно заметить, что данное разбиение на
классы реализует логическую функцию исключающего ИЛИ для входных сигналов.
Невозможность реализации однослойным перцептроном этой функции получила
название проблемы исключающего ИЛИ.
5
Функции, которые не реализуются однослойной сетью,
называются линейно неразделимыми[2]. Решение задач,
подпадающих под это ограничение, заключается в применении
2-х и более слойных сетей или сетей с нелинейными
синапсами, однако и тогда существует вероятность, что
корректное разделение некоторых входных сигналов на классы
невозможно.
Наконец, мы можем более подробно рассмотреть вопрос
обучения НС, для начала – на примере перцептрона с рисунка Рис.6 Визуальное
3.
представление
Рассмотрим алгоритм обучения с учителем[2][4].
работы НС с
1. Проинициализировать элементы весовой матрицы
рисунка 5
(обычно небольшими случайными значениями).
2. Подать на входы один из входных векторов, которые сеть должна научиться
различать, и вычислить ее выход.
3. Если выход правильный, перейти на шаг 4.
Иначе вычислить разницу между идеальным и полученным значениями выхода:
δ = YI − Y
Модифицировать веса в соответствии с формулой:
wij (t + 1) = wij (t ) + ν ⋅ δ ⋅ xi
где t и t+1 – номера соответственно текущей и следующей итераций; ν –
коэффициент скорости обучения, 0<νЈ1; i – номер входа; j – номер нейрона в слое.
Очевидно, что если YI > Y весовые коэффициенты будут увеличены и тем самым
уменьшат ошибку. В противном случае они будут уменьшены, и Y тоже уменьшится,
приближаясь к YI.
4. Цикл с шага 2, пока сеть не перестанет ошибаться.
На втором шаге на разных итерациях поочередно в случайном порядке
предъявляются все возможные входные вектора. К сожалению, нельзя заранее
определить число итераций, которые потребуется выполнить, а в некоторых случаях и
гарантировать полный успех. Этот вопрос будет косвенно затронут в дальнейшем.
В завершении данной вводной статьи хотелось бы отметить, что дальнейшее
рассмотрение НС будет в основном тяготеть к таким применениям, как распознавание
образов, их классификация и, в незначительной степени, сжатие информации. Более
подробно об этих и других применениях и реализующих их структурах НС можно
прочитать в журналах Neural Computation, Neural Computing and Applications, Neural
Networks, IEEE Transactions on Neural Networks, IEEE Transactions on System, Man, and
Cybernetics и других.
Литература
1. Е. Монахова, "Нейрохирурги" с Ордынки, PC Week/RE, №9, 1995.
2. Ф.Уоссермен, Нейрокомпьютерная техника, М.,Мир, 1992.
3. Итоги науки и техники: физические и математические модели нейронных сетей, том
1, М., изд. ВИНИТИ, 1990.
4. Artificial Neural Networks: Concepts and Theory, IEEE Computer Society Press, 1992.
5. Richard P. Lippmann, An Introduction to Computing withNeural Nets, IEEE Acoustics,
Speech, and Signal ProcessingMagazine, April 1987.
6
С.Короткий
Нейронные сети: алгоритм обратного распространения
В статье рассмотрен алгоритм обучения нейронной сети с помощью процедуры
обратного распространения, описана библиотека классов для С++.
Среди различных структур нейронных сетей (НС) одной из наиболее известных
является многослойная структура, в которой каждый нейрон произвольного слоя связан
со всеми аксонами нейронов предыдущего слоя или, в случае первого слоя, со всеми
входами НС. Такие НС называются полносвязными. Когда в сети только один слой,
алгоритм ее обучения с учителем довольно очевиден, так как правильные выходные
состояния нейронов единственного слоя заведомо известны, и подстройка
синаптических связей идет в направлении, минимизирующем ошибку на выходе сети.
По этому принципу строится, например, алгоритм обучения однослойного
перцептрона[1]. В многослойных же сетях оптимальные выходные значения нейронов
всех слоев, кроме последнего, как правило, не известны, и двух или более слойный
перцептрон уже невозможно обучить, руководствуясь только величинами ошибок на
выходах НС. Один из вариантов решения этой проблемы – разработка наборов
выходных сигналов, соответствующих входным, для каждого слоя НС, что, конечно,
является очень трудоемкой операцией и не всегда осуществимо. Второй вариант –
динамическая подстройка весовых коэффициентов синапсов, в ходе которой
выбираются, как правило, наиболее слабые связи и изменяются на малую величину в ту
или иную сторону, а сохраняются только те изменения, которые повлекли уменьшение
ошибки на выходе всей сети. Очевидно, что данный метод "тыка", несмотря на свою
кажущуюся простоту, требует громоздких рутинных вычислений. И, наконец, третий,
более приемлемый вариант – распространение сигналов ошибки от выходов НС к ее
входам, в направлении, обратном прямому распространению сигналов в обычном
режиме работы. Этот алгоритм обучения НС получил название процедуры обратного
распространения. Именно он будет рассмотрен в дальнейшем.
Согласно методу наименьших квадратов, минимизируемой целевой функцией
ошибки НС является величина:
1
(1)
E ( w) = ∑ ( y (j ,Np) − d j , p ) 2
2 j,p
где y (j ,Np) – реальное выходное состояние нейрона j выходного слоя N нейронной
сети при подаче на ее входы p-го образа; djp – идеальное (желаемое) выходное
состояние этого нейрона.
Суммирование ведется по всем нейронам выходного слоя и по всем
обрабатываемым сетью образам. Минимизация ведется методом градиентного спуска,
что означает подстройку весовых коэффициентов следующим образом:
∂E
∆wij( n ) = −η ⋅
(2)
∂wij
Здесь wij – весовой коэффициент синаптической связи, соединяющей i-ый нейрон
слоя n-1 с j-ым нейроном слоя n, η – коэффициент скорости обучения, 0<η<1.
Как показано в [2],
∂E
∂E dy j ∂s j
(3)
=
⋅
⋅
∂wij ∂y j ds j ∂wij
Здесь под yj, как и раньше, подразумевается выход нейрона j, а под sj –
взвешенная сумма его входных сигналов, то есть аргумент активационной функции.
7
Так как множитель dyj/dsj является производной этой функции по ее аргументу, из
этого следует, что производная активационной функция должна быть определена на
всей оси абсцисс. В связи с этим функция единичного скачка и прочие активационные
функции с неоднородностями не подходят для рассматриваемых НС. В них
применяются такие гладкие функции, как гиперболический тангенс или классический
сигмоид с экспонентой. В случае гиперболического тангенса
dy
(4)
= 1 − s2
ds
Третий множитель ∂sj/∂wij, очевидно, равен выходу нейрона предыдущего слоя
(n-1)
yi .
Что касается первого множителя в (3), он легко раскладывается следующим
образом[2]:
∂E
∂E dy k ∂sk
∂E dy k ( n+1)
(5)
=∑
⋅
⋅
=∑
⋅
⋅w jk
∂y j
dsk ∂y j k ∂y k dsk
k ∂y k
Здесь суммирование по k выполняется среди нейронов слоя n+1.
Введя новую переменную
∂E dy j
δ (jn ) =
⋅
∂y j ds j
(6)
мы получим рекурсивную формулу для расчетов величин δj(n) слоя n из величин
δk(n+1) более старшего слоя n+1.

 dy
δ (jn ) = ∑ δ (kn+1) ⋅ w (jkn+1)  ⋅ j
(7)
k
 ds j
Для выходного же слоя
dy
δ (l N ) = ( yl( N ) − d l ) ⋅ l
(8)
dsl
Теперь мы можем записать (2) в раскрытом виде:
∆wij( n ) = −η ⋅ δ (jn) ⋅ yi( n −1)
(9)
Иногда для придания процессу коррекции весов некоторой инерционности,
сглаживающей резкие скачки при перемещении по поверхности целевой функции, (9)
дополняется значением изменения веса на предыдущей итерации
(10)
∆wij( n ) ( t ) = −η ⋅ ( µ ⋅ ∆wij( n ) ( t − 1) + (1 − µ ) ⋅ δ (jn) ⋅ yi( n −1) )
где µ – коэффициент инерционности, t – номер текущей итерации.
Таким образом, полный алгоритм обучения НС с помощью процедуры обратного
распространения строится так:
1. Подать на входы сети один из возможных образов и в режиме обычного
функционирования НС, когда сигналы распространяются от входов к выходам,
рассчитать значения последних. Напомним, что
M
s (jn ) = ∑ yi( n −1) ⋅ wij( n )
(11)
i=0
где M – число нейронов в слое n-1 с учетом нейрона с постоянным выходным
состоянием +1, задающего смещение; yi(n-1)=xij(n) – i-ый вход нейрона j слоя n.
(12)
yj(n) = f(sj(n)), где f() – сигмоид
(0)
(13)
yq =Iq,
где Iq – q-ая компонента вектора входного образа.
2. Рассчитать δ(N) для выходного слоя по формуле (8).
8
Рассчитать по формуле (9) или (10) изменения весов ∆w(N) слоя N.
3. Рассчитать по формулам (7) и (9) (или (7) и (10)) соответственно δ(n) и ∆w(n) для
всех остальных слоев, n=N-1,...1.
4. Скорректировать все веса в НС
(14)
wij( n ) (t ) = wij( n ) (t − 1) + ∆wij( n ) ( t )
5. Если ошибка сети существенна, перейти на шаг 1. В противном случае – конец.
Сети на шаге 1 попеременно в случайном порядке
предъявляются все тренировочные образы, чтобы сеть,
образно говоря, не забывала одни по мере запоминания
других. Алгоритм иллюстрируется рисунком 1.
Из выражения (9) следует, что когда выходное
значение yi(n-1) стремится к нулю, эффективность
обучения заметно снижается. При двоичных входных
векторах в среднем половина весовых коэффициентов
не будет корректироваться[3], поэтому область
возможных значений выходов нейронов [0,1]
желательно сдвинуть в пределы [-0.5,+0.5], что
Рис.1 Диаграмма сигналов достигается простыми модификациями логистических
функций.
Например,
сигмоид
с
экспонентой
в сети при обучении по
преобразуется
к
виду
алгоритму обратного
1
распространения
(15)
f ( x ) = −0.5 +
1 + e − α ⋅x
Теперь коснемся вопроса емкости НС, то есть числа образов, предъявляемых на ее
входы, которые она способна научиться распознавать. Для сетей с числом слоев больше
двух, он остается открытым. Как показано в [4], для НС с двумя слоями, то есть
выходным и одним скрытым слоем, детерминистская емкость сети Cd оценивается так:
Nw/Ny<Cd<Nw/Ny⋅log(Nw/Ny)
(16)
где Nw – число подстраиваемых весов, Ny – число нейронов в выходном слое.
Следует отметить, что данное выражение получено с учетом некоторых
ограничений. Во-первых, число входов Nx и нейронов в скрытом слое Nh должно
Во-вторых,
Nw/Ny>1000.
Однако
удовлетворять
неравенству
Nx+Nh>Ny.
вышеприведенная оценка выполнялась для сетей с активационными функциями
нейронов в виде порога, а емкость сетей с гладкими активационными функциями,
например – (15), обычно больше[4]. Кроме того, фигурирующее в названии емкости
прилагательное "детерминистский" означает, что полученная оценка емкости подходит
абсолютно для всех возможных входных образов, которые могут быть представлены Nx
входами. В действительности распределение входных образов, как правило, обладает
некоторой регулярностью, что позволяет НС проводить обобщение и, таким образом,
увеличивать реальную емкость. Так как распределение образов, в общем случае,
заранее не известно, мы можем говорить о такой емкости только предположительно, но
обычно она раза в два превышает емкость детерминистскую.
В продолжение разговора о емкости НС логично затронуть вопрос о требуемой
мощности выходного слоя сети, выполняющего окончательную классификацию
образов. Дело в том, что для разделения множества входных образов, например, по
двум классам достаточно всего одного выхода. При этом каждый логический уровень –
"1" и "0" – будет обозначать отдельный класс. На двух выходах можно закодировать
уже 4 класса и так далее. Однако результаты работы сети, организованной таким
образом, можно сказать – "под завязку", – не очень надежны. Для повышения
9
достоверности классификации желательно ввести избыточность путем выделения
каждому классу одного нейрона в выходном слое или, что еще лучше, нескольких,
каждый из которых обучается определять принадлежность образа к классу со своей
степенью достоверности, например: высокой, средней и низкой. Такие НС позволяют
проводить классификацию входных образов, объединенных в нечеткие (размытые или
пересекающиеся) множества. Это свойство приближает подобные НС к условиям
реальной жизни.
Рассматриваемая НС имеет несколько "узких мест". Во-первых, в процессе
обучения может возникнуть ситуация, когда большие положительные или
отрицательные значения весовых коэффициентов сместят рабочую точку на сигмоидах
многих нейронов в область насыщения. Малые величины производной от
логистической функции приведут в соответствие с (7) и (8) к остановке обучения, что
парализует НС. Во-вторых, применение метода градиентного спуска не гарантирует,
что будет найден глобальный, а не локальный минимум целевой функции. Эта
проблема связана еще с одной, а именно – с выбором величины скорости обучения.
Доказательство сходимости обучения в процессе обратного распространения основано
на производных, то есть приращения весов и, следовательно, скорость обучения
должны быть бесконечно малыми, однако в этом случае обучение будет происходить
неприемлемо медленно. С другой стороны, слишком большие коррекции весов могут
привести к постоянной неустойчивости процесса обучения. Поэтому в качестве η
обычно выбирается число меньше 1, но не очень маленькое, например, 0.1, и оно,
вообще говоря, может постепенно уменьшаться в процессе обучения. Кроме того, для
исключения случайных попаданий в локальные минимумы иногда, после того как
значения весовых коэффициентов застабилизируются, η кратковременно сильно
увеличивают, чтобы начать градиентный спуск из новой точки. Если повторение этой
процедуры несколько раз приведет алгоритм в одно и то же состояние НС, можно более
или менее уверенно сказать, что найден глобальный максимум, а не какой-то другой.
Существует и иной метод исключения локальных минимумов, а заодно и
паралича НС, заключающийся в применении стохастических НС, но о них лучше
поговорить отдельно.
Теперь мы можем обратиться непосредственно к программированию НС. Следует
отметить, что число зарубежных публикаций, рассматривающих программную
реализацию сетей, ничтожно мало по сравнению с общим числом работ на тему
нейронных сетей, и это при том, что многие авторы опробывают свои теоретические
выкладки именно программным способом, а не с помощью нейрокомпьютеров и
нейроплат, в первую очередь из-за их дороговизны. Возможно, это вызвано тем, что к
программированию на западе относятся как к ремеслу, а не науке. Однако в результате
такой дискриминации остаются неразобранными довольно важные вопросы.
Как видно из формул, описывающих алгоритм функционирования и обучения НС,
весь этот процесс может быть записан и затем запрограммирован в терминах и с
применением операций матричной алгебры, что сделано, например, в [5]. Судя по
всему, такой подход обеспечит более быструю и компактную реализацию НС, нежели
ее
воплощение
на
базе
концепций
объектно-ориентированного
(ОО)
программирования. Однако в последнее время преобладает именно ОО подход, причем
зачастую разрабатываются специальные ОО языки для программирования НС[6], хотя,
с моей точки зрения, универсальные ОО языки, например C++ и Pascal, были созданы
как раз для того, чтобы исключить необходимость разработки каких-либо других ОО
языков, в какой бы области их не собирались применять.
10
И все же программирование НС с применением ОО подхода имеет свои плюсы.
Во-первых, оно позволяет создать гибкую, легко перестраиваемую иерархию моделей
НС. Во-вторых, такая реализация наиболее прозрачна для программиста, и позволяет
конструировать НС даже непрограммистам. В-третьих, уровень абстрактности
программирования, присущий ОО языкам, в будущем будет, по-видимому, расти, и
реализация НС с ОО подходом позволит расширить их возможности. Исходя из
вышеизложенных соображений, приведенная в листингах библиотека классов и
программ, реализующая полносвязные НС с обучением по алгоритму обратного
распространения, использует ОО подход. Вот основные моменты, требующие
пояснений.
Прежде всего необходимо отметить, что библиотека была составлена и
использовалась в целях распознавания изображений, однако применима и в других
приложениях. В файле neuro.h в листинге 1 приведены описания двух базовых и пяти
производных (рабочих) классов: Neuron, SomeNet и NeuronFF, NeuronBP, LayerFF,
LayerBP, NetBP, а также описания нескольких общих функций вспомогательного
назначения, содержащихся в файле subfun.cpp (см.листинг 4). Методы пяти
вышеупомянутых рабочих классов внесены в файлы neuro_ff.cpp и neuro_bp.cpp,
представленные в листингах 2 и 3. Такое, на первый взгляд искусственное, разбиение
объясняется тем, что классы с суффиксом _ff, описывающие прямопоточные
нейронные сети (feedforward), входят в состав не только сетей с обратным
распространением – _bp (backpropagation), но и других, например таких, как с
обучением без учителя, которые будут рассмотрены в дальнейшем. Иерархия классов
приведенной библиотеки приведена на рисунке 2.
11
┌──────────┐
┌─────────┐
│ Neuron │
│
SomeNet │
└────┬─────┘
└────┬────┘
│
│
┌────┴─────┐
│
│ NeuronFF │
│
└────┬╥────┘
│
│╚════════════╗
│
┌────┴─────┐ ┌────╨────┐
│
│ NeuronBP │ │ LayerFF │
│
└────╥─────┘ └────┬────┘
│
╚════════════╗│
│
┌───╨┴────┐
│
│ LayerBP │
│
└───┬─────┘
│
В
ущерб
принципам
ОО
программирования,
шесть
основных
параметров, характеризующих работу сети,
вынесены на глобальный уровень, что
облегчает операции с ними. Параметр
SigmoidType определяет вид активационной
функции.
В
методе
NeuronFF::Sigmoid
перечислены
некоторые
его
значения,
макроопределения
которых
сделаны
в
заголовочном файле. Пункты HARDLIMIT и
THRESHOLD даны для общности, но не могут
быть использованы в алгоритме обратного
распространения, так как соответствующие им
активационные функции имеют производные с
особыми точками. Это отражено в методе
расчета производной NeuronFF::D_Sigmoid, из
которого эти два случая исключены.
Переменная SigmoidAlfa задает крутизну α
сигмоида ORIGINAL из (15). MiuParm и
NiuParm – соответственно значения параметров
µ и η из формулы (10). Величина Limit
используется в методах IsConverged для
определения момента, когда сеть обучится или
попадет в паралич. В этих случаях изменения
весов становятся меньше малой величины
Limit. Параметр dSigma эмулирует плотность
шума, добавляемого к образам во время
обучения НС. Это позволяет из конечного
│┌───────┘
набора
"чистых"
входных
образов
┌──┴┴───┐
генерировать практически неограниченное
│ NetBP │
число "зашумленных" образов. Дело в том, что
└───────┘
для нахождения оптимальных значений
Рис.2 Иерархия классов библиотеки весовых коэффициентов число степеней
для сетей обратного распространения свободы НС – N должно быть намного
w
(одинарная линия – наследование,
меньше числа накладываемых ограничений –
двойная – вхождение)
Ny⋅Np, где Np – число образов, предъявляемых
НС во время обучения. Фактически, параметр dSigma равен числу входов, которые
будут инвертированы в случае двоичного образа. Если dSigma = 0, помеха не вводится.
Методы Randomize позволяют перед началом обучения установить весовые
коэффициенты в случайные значения в диапазоне [-range,+range]. Методы Propagate
выполняют вычисления по формулам (11) и (12). Метод NetBP::CalculateError на основе
передаваемого в качестве аргумента массива верных (желаемых) выходных значений
НС вычисляет величины δ. Метод NetBP::Learn рассчитывает изменения весов по
формуле (10), методы Update обновляют весовые коэффициенты. Метод NetBP::Cycle
объединяет в себе все процедуры одного цикла обучения, включая установку входных
сигналов NetBP::SetNetInputs. Различные методы PrintXXX и LayerBP::Show позволяют
контролировать течение процессов в НС, но их реализация не имеет принципиального
значения, и простые процедуры из приведенной библиотеки могут быть при желании
переписаны, например, для графического режима. Это оправдано и тем, что в
12
алфавитно-цифровом режиме уместить на экране информацию о сравнительно
большой НС уже не удается.
Сети могут конструироваться посредством NetBP(unsigned), после чего их нужно
заполнять сконструированными ранее слоями с помощью метода NetBP::SetLayer, либо
посредством NetBP(unsigned, unsigned,...). В последнем случае конструкторы слоев
вызываются автоматически. Для установления синаптических связей между слоями
вызывается метод NetBP::FullConnect.
После того как сеть обучится, ее текущее состояние можно записать в файл
(метод
NetBP::SaveToFile),
а затем восстановить
с помощью
метода
NetBP::LoadFromFile, который применим лишь к только что сконструированной по
NetBP(void) сети.
Для ввода в сеть входных образов, а на стадии обучения – и для задания
выходных, написаны три метода: SomeNet::OpenPatternFile, SomeNet::ClosePatternFile и
NetBP::LoadNextPattern. Если у файлов образов произвольное расширение, то входные
и выходные вектора записываются чередуясь: строка с входным вектором, строка с
соответствующим ему выходным вектором и т.д. Каждый вектор есть
последовательность действительных чисел в диапазоне [-0.5,+0.5], разделенных
произвольным числом пробелов (см. листинг 7). Если файл имеет расширение IMG,
входной вектор представляется в виде матрицы символов размером dy*dx (величины dx
и dy должны быть заблаговременно установлены с помощью LayerFF::SetShowDim для
нулевого слоя), причем символ 'x' соответствует уровню 0.5, а точка – уровню -0.5, то
есть файлы IMG, по крайней мере – в приведенной версии библиотеки, бинарны (см.
листинг 8). Когда сеть работает в нормальном режиме, а не обучается, строки с
выходными векторами могут быть пустыми. Метод SomeNet::SetLearnCycle задает
число проходов по файлу образов, что в сочетании с добавлением шума позволяет
получить набор из нескольких десятков и даже сотен тысяч различных образов.
В листингах 5 и 6 приведены программы, конструирующие и обучающие НС, а
также использующие ее в рабочем режиме распознавания изображений.
Особо следует отметить тот нюанс, что в рассматриваемой библиотеке классов
НС отсутствует реализация подстраиваемого порога для каждого нейрона. Сеть,
вообще говоря, может работать и без него, однако процесс обучения от этого
замедляется[3]. Простой, хотя и не самый эффективный способ ввести для нейронов
каждого слоя регулируемое смещение заключается в добавлении в класс NeuronFF
метода Saturate, который принудительно устанавливал бы выход нейрона в состояние
насыщения axon=0.5, с вызовом этого метода для какого-нибудь одного, например,
последнего нейрона слоя в конце функции LayerFF::Propagate. Очевидно, что при этом
на стадии конструирования в каждый слой НС, кроме выходного необходимо добавить
один дополнительный нейрон. Он, в принципе, может не иметь ни синапсов, ни
массива изменений их весов и не вызывать метод Propagate внутри LayerFF::Propagate.
Рассмотренный выше и реализованный в программе алгоритм является, можно
сказать, классическим вариантом процедуры обратного распространения, однако
известны многие его модификации. Изменения касаются как методов расчетов [7][8],
так и конфигурации сети [9][5]. В частности в [5] послойная организация сети заменена
на магистральную, когда все нейроны имеют сквозной номер и каждый связан со всеми
предыдущими.
Сеть, сконструированная в качестве примера в программе, приведенной на
листинге 5, была обучена распознавать десять букв, схематично заданных матрицами
6*5 точек за несколько сотен циклов обучения, которые выполнились на компьютере
13
386DX40 за время меньше минуты. Обученная сеть успешно распознавала
изображения, зашумленные более сильно, чем образы, на которых она обучалась.
Программа компилировалась с помощью Borland C++ 3.1 в моделях Large и Small.
Предложенная библиотека классов позволит создавать сети, способные решать
широкий спектр задач, таких как построение экспертных систем, сжатие информации и
многих других, исходные условия которых могут быть приведены к множеству парных,
входных и выходных, наборов данных.
Литература
1. С.Короткий, Нейронные сети: основные положения.
2. Sankar K. Pal, Sushmita Mitra, Multilayer Perceptron, Fuzzy Sets, and Classification
//IEEE Transactions on Neural Networks, Vol.3, N5,1992, pp.683-696.
3. Ф.Уоссермен, Нейрокомпьютерная техника, М., Мир, 1992.
4. Bernard Widrow, Michael A. Lehr, 30 Years of Adaptive NeuralNetworks: Perceptron,
Madaline, and Backpropagation //Artificial Neural Networks: Concepts and Theory, IEEE
Computer Society Press, 1992, pp.327-354.
5. Paul J. Werbos, Backpropagation Through Time: What It Does and How to Do It
//Artificial Neural Networks: Concepts and Theory, IEEE Computer Society Press, 1992,
pp.309-319.
6. Gael de La Croix Vaubois, Catherine Moulinoux, Benolt Derot, The N Programming
Language //Neurocomputing, NATO ASI series, vol.F68, pp.89-92.
7. H.A.Malki, A.Moghaddamjoo, Using the Karhunen-Loe`ve Transformation in the BackPropagation Training Algorithm //IEEE Transactions on Neural Networks, Vol.2, N1,
1991, pp.162-165.
8. Harris Drucker, Yann Le Cun, Improving Generalization Performance Using
Backpropagation //IEEE Transactions on Neural Networks, Vol.3, N5, 1992, pp.991-997.
9. Alain Petrowski, Gerard Dreyfus, Claude Girault, Performance Analysis of a Pipelined
Backpropagation Parallel Algorithm //IEEE Transactions on Neural Networks, Vol.4, N6,
1993, pp.970-981.
Листинг 1
// FILE neuro.h
#include <stdio.h>
#define OK
0
#define ERROR 1
#define
#define
#define
#define
ORIGINAL
HYPERTAN
HARDLIMIT
THRESHOLD
#define INNER
#define EXTERN
// состояния объектов
// типы активационных функций
0
1
2
3
0
1
// тип распределения памяти
#define HORIZONTAL 1
#define VERTICAL 0
#ifndef max
#define max(a,b)
#define min(a,b)
(((a) > (b)) ? (a) : (b))
(((a) < (b)) ? (a) : (b))
14
#endif
// базовый класс нейронов для большинства сетей
class Neuron
{
protected:
float state;
// состояние
float axon;
// выход
int status;
// признак ошибки
public:
Neuron(void){ state=0.; axon=0.; status=OK; };
virtual float Sigmoid(void)=0;
int GetStatus(void){return status;};
};
class SomeNet
{
protected:
FILE *pf;
int imgfile; // 0 - числа; 1 - 2D; 2 - эмуляция
unsigned rang;
int status;
unsigned learncycle;
int (*emuf)(int n, float _FAR *in, float _FAR *ou);
public:
SomeNet(void)
{pf=NULL;imgfile=0;rang=0;status=OK;learncycle=0;};
unsigned GetRang(void){return rang;};
void SetLearnCycle(unsigned l){learncycle=l;};
int OpenPatternFile(unsigned char *file);
int ClosePatternFile(void);
void EmulatePatternFile(int (*p)(int n,
float _FAR *, float _FAR *))
{emuf=p;imgfile=2;};
int GetStatus(void){return status;};
};
class LayerBP;
class NetBP;
// нейрон для полносвязной сети прямого распространения
class NeuronFF: public Neuron
{
protected:
unsigned rang;
// число весов
float _FAR *synapses; // веса
float _FAR * _FAR *inputs;
// массив указателей на выходы нейронов предыд. слоя
void _allocateNeuron(unsigned);
void _deallocate(void);
public:
NeuronFF(unsigned num_inputs);
NeuronFF(void){rang=0; synapses=NULL;
inputs=NULL; status=OK;};
15
~NeuronFF();
virtual void Propagate(void);
void SetInputs(float *massive);
void InitNeuron(unsigned numsynapses);
virtual void RandomizeAxon(void);
virtual void Randomize(float);
virtual float Sigmoid(void);
virtual float D_Sigmoid(void);
virtual void PrintSynapses(int,int);
virtual void PrintAxons(int, int);
};
class NeuronBP: public NeuronFF
{
friend LayerBP;
friend NetBP;
float error;
float _FAR *deltas;
// изменения весов
void _allocateNeuron(unsigned);
void _deallocate(void);
public:
NeuronBP(unsigned num_inputs);
NeuronBP(void){deltas=NULL; error=0.;};
~NeuronBP();
void InitNeuron(unsigned numsynapses);
int IsConverged(void);
};
class LayerFF
{
protected:
unsigned rang;
int status;
int x,y,dx,dy;
unsigned char *name;
// имя слоя
public:
LayerFF(void) { rang=0; name=NULL; status=OK; };
unsigned GetRang(void){return rang;};
void SetShowDim(int _x, int _y, int _dx, int _dy)
{x=_x; y=_y; dx=_dx; dy=_dy;};
void SetName(unsigned char *s) {name=s;};
unsigned char *GetName(void)
{if(name) return name;
else return (unsigned char *)&("NoName");};
int GetStatus(void){return status;};
int GetX(void){return x;};
int GetY(void){return y;};
int GetDX(void){return dx;};
int GetDY(void){return dy;};
};
class LayerBP: public LayerFF
{
friend NetBP;
protected:
unsigned neuronrang; // число синапсов в нейронах
16
int allocation;
NeuronBP _FAR *neurons;
public:
LayerBP(unsigned nRang, unsigned nSinapses);
LayerBP(NeuronBP _FAR *Neu, unsigned nRang,
unsigned nSinapses);
LayerBP(void)
{neurons=NULL; neuronrang=0; allocation=EXTERN;};
~LayerBP();
void Propagate(void);
void Randomize(float);
void RandomizeAxons(void);
void Normalize(void);
void Update(void);
int IsConverged(void);
virtual void Show(void);
virtual void PrintSynapses(int,int);
virtual void PrintAxons(int x, int y, int direction);
};
class NetBP: public SomeNet
{
LayerBP _FAR * _FAR *layers;
// нулевой слой нейронов без синапсов реализует входы
public:
NetBP(void) { layers=NULL; };
NetBP(unsigned nLayers);
NetBP(unsigned n, unsigned n1, ...);
~NetBP();
int SetLayer(unsigned n, LayerBP _FAR *pl);
LayerBP *GetLayer(unsigned n)
{if(n<rang) return layers[n]; else return NULL; }
void Propagate(void);
int FullConnect(void);
void SetNetInputs(float _FAR *mvalue);
void CalculateError(float _FAR * Target);
void Learn(void);
void Update(void);
void Randomize(float);
void Cycle(float _FAR *Inp, float _FAR *Out);
int SaveToFile(unsigned char *file);
int LoadFromFile(unsigned char *file);
int LoadNextPattern(float _FAR *IN, float _FAR *OU);
int IsConverged(void);
void AddNoise(void);
virtual void PrintSynapses(int x=0,...){};
virtual float Change(float In);
};
// Сервисные функции
void out_char(int x,int y,int c,int at);
void out_str(int x,int y,unsigned char *s,unsigned col);
void ClearScreen(void);
17
// Глобальные параметры для обратного распространения
int SetSigmoidType(int st);
float SetSigmoidAlfa(float Al);
float SetMiuParm(float Mi);
float SetNiuParm(float Ni);
float SetLimit(float Li);
unsigned SetDSigma(unsigned d);
// Псевдографика
#define GRAFCHAR_UPPERLEFTCORNER 218
#define GRAFCHAR_UPPERRIGHTCORNER 191
#define GRAFCHAR_HORIZONTALLINE 196
#define GRAFCHAR_VERTICALLINE 179
#define GRAFCHAR_BOTTOMLEFTCORNER 192
#define GRAFCHAR_BOTTOMRIGHTCORNER 217
#define
#define
#define
#define
#define
GRAFCHAR_EMPTYBLACK 32
GRAFCHAR_DARKGRAY 176
GRAFCHAR_MIDDLEGRAY 177
GRAFCHAR_LIGHTGRAY 178
GRAFCHAR_SOLIDWHITE 219
Листинг 2
//FILE neuro_ff.cpp FOR neuro1.prj & neuro2.prj
#include <stdlib.h>
#include <math.h>
#include "neuro.h"
static int SigmoidType=ORIGINAL;
static float SigmoidAlfa=2.; // > 4 == HARDLIMIT
int SetSigmoidType(int st)
{
int i;
i=SigmoidType;
SigmoidType=st;
return i;
}
float SetSigmoidAlfa(float Al)
{
float a;
a=SigmoidAlfa;
SigmoidAlfa=Al;
return a;
}
void NeuronFF::Randomize(float range)
{
for(unsigned i=0;i<rang;i++)
synapses[i]=range*((float)rand()/RAND_MAX-0.5);
}
18
void NeuronFF::RandomizeAxon(void)
{
axon=(float)rand()/RAND_MAX-0.5;
}
float NeuronFF::D_Sigmoid(void)
{
switch(SigmoidType)
{
case HYPERTAN: return (1.-axon*axon);
case ORIGINAL: return SigmoidAlfa*(axon+0.5)*
(1.5-axon);
default:
return 1.;
}
}
float NeuronFF::Sigmoid(void)
{
switch(SigmoidType)
{
case HYPERTAN: return 0.5*tanh(state);
case ORIGINAL: return -0.5+1./
(1+exp(-SigmoidAlfa*state));
case HARDLIMIT:if(state>0) return 0.5;
else if(state<0) return -0.5;
else return state;
case THRESHOLD:if(state>0.5) return 0.5;
else if(state<-0.5) return -0.5;
else return state;
default:
return 0.;
}
}
void NeuronFF::_allocateNeuron(unsigned num_inputs)
{
synapses=NULL;inputs=NULL;status=OK;rang=0;
if(num_inputs==0) return;
synapses= new float[num_inputs];
if(synapses==NULL) status=ERROR;
else
{
inputs=new float _FAR * [num_inputs];
if(inputs==NULL) status=ERROR;
else
{
rang=num_inputs;
for(unsigned i=0;i<rang;i++)
{ synapses[i]=0.; inputs[i]=NULL; }
}
}
}
NeuronFF::NeuronFF(unsigned num_inputs)
19
{
_allocateNeuron(num_inputs);
}
void NeuronFF::_deallocate(void)
{
if(rang && (status==OK))
{delete [] synapses;delete [] inputs;
synapses=NULL; inputs=NULL;}
}
NeuronFF::~NeuronFF()
{
_deallocate();
}
void NeuronFF::Propagate(void)
{
state=0.;
for(unsigned i=0;i<rang;i++)
state+=(*inputs[i]*2)*(synapses[i]*2);
state/=2;
axon=Sigmoid();
}
void NeuronFF::SetInputs(float *vm)
{
for(unsigned i=0;i<rang;i++) inputs[i]=&vm[i];
}
void NeuronFF::InitNeuron(unsigned num_inputs)
{
if(rang && (status==OK))
{delete [] synapses;delete [] inputs;}
_allocateNeuron(num_inputs);
}
void NeuronFF::PrintSynapses(int x=0, int y=0)
{
unsigned char buf[20];
for(unsigned i=0;i<rang;i++)
{
sprintf(buf,"%+7.2f",synapses[i]);
out_str(x+8*i,y,buf,11);
}
}
void NeuronFF::PrintAxons(int x=0, int y=0)
{
unsigned char buf[20];
sprintf(buf,"%+7.2f",axon);
out_str(x,y,buf,11);
}
20
Листинг 3
// FILE neuro_bp.cpp FOR neuro1.prj & neuro2.prj
#include <stdlib.h>
#include <alloc.h>
#include <math.h>
#include <string.h>
#include <stdarg.h>
#include <values.h>
#include "neuro.h"
static
static
static
static
float MiuParm=0.0;
float NiuParm=0.1;
float Limit=0.000001;
unsigned dSigma=0;
float SetMiuParm(float Mi)
{
float a;
a=MiuParm;
MiuParm=Mi;
return a;
}
float SetNiuParm(float Ni)
{
float a;
a=NiuParm;
NiuParm=Ni;
return a;
}
float SetLimit(float Li)
{
float a;
a=Limit;
Limit=Li;
return a;
}
unsigned SetDSigma(unsigned d)
{
unsigned u;
u=dSigma;
dSigma=d;
return u;
}
void NeuronBP::_allocateNeuron(unsigned num_inputs)
{
deltas=NULL;
if(num_inputs==0) return;
deltas=new float[num_inputs];
21
if(deltas==NULL) status=ERROR;
else for(unsigned i=0;i<rang;i++) deltas[i]=0.;
}
NeuronBP::NeuronBP(unsigned num_inputs)
:NeuronFF(num_inputs)
{
_allocateNeuron(num_inputs);
}
void NeuronBP::_deallocate(void)
{
if(deltas && (status==OK))
{delete [] deltas; deltas=NULL;}
}
NeuronBP::~NeuronBP()
{
_deallocate();
}
void NeuronBP::InitNeuron(unsigned num_inputs)
{
NeuronFF::InitNeuron(num_inputs);
if(deltas && (status==OK)) delete [] deltas;
_allocateNeuron(num_inputs);
}
int NeuronBP::IsConverged(void)
{
for(unsigned i=0;i<rang;i++)
if(fabs(deltas[i])>Limit) return 0;
return 1;
}
//
LayerBP::LayerBP(unsigned nRang, unsigned nSynapses)
{
allocation=EXTERN; status=ERROR; neuronrang=0;
if(nRang==0) return;
neurons=new NeuronBP[nRang];
if(neurons==NULL) return;
for(unsigned i=0;i<nRang;i++)
neurons[i].InitNeuron(nSynapses);
rang=nRang;
neuronrang=nSynapses;
allocation=INNER;
name=NULL; status=OK;
}
LayerBP::LayerBP(NeuronBP _FAR *Neu, unsigned nRang,
unsigned nSynapses)
{
22
neurons=NULL; neuronrang=0; allocation=EXTERN;
for(unsigned i=0;i<nRang;i++)
if(Neu[i].rang!=nSynapses) status=ERROR;
if(status==OK)
{
neurons=Neu;
rang=nRang;
neuronrang=nSynapses;
}
}
LayerBP::~LayerBP(void)
{
if(allocation==INNER)
{
for(unsigned i=0;i<rang;i++)
neurons[i]._deallocate();
delete [] neurons; neurons=NULL;
}
}
void LayerBP::Propagate(void)
{
for(unsigned i=0;i<rang;i++)
neurons[i].Propagate();
}
void LayerBP::Update(void)
{
for(unsigned i=0;i<rang;i++)
{
for(unsigned j=0;j<neuronrang;j++)
neurons[i].synapses[j]-=neurons[i].deltas[j];
}
}
void LayerBP::Randomize(float range)
{
for(unsigned i=0;i<rang;i++)
neurons[i].Randomize(range);
}
void LayerBP::RandomizeAxons(void)
{
for(unsigned i=0;i<rang;i++)
neurons[i].RandomizeAxon();
}
void LayerBP::Normalize(void)
{
float sum;
unsigned i;
for(i=0;i<rang;i++)
sum+=neurons[i].axon*neurons[i].axon;
23
sum=sqrt(sum);
for(i=0;i<rang;i++) neurons[i].axon/=sum;
}
void LayerBP::Show(void)
{
unsigned char sym[5]={ GRAFCHAR_EMPTYBLACK, GRAFCHAR_DARKGRAY,
GRAFCHAR_MIDDLEGRAY, GRAFCHAR_LIGHTGRAY, GRAFCHAR_SOLIDWHITE
};
int i,j;
if(y && name) for(i=0;i<strlen(name);i++)
out_char(x+i,y-1,name[i],3);
out_char(x,y,GRAFCHAR_UPPERLEFTCORNER,15);
for(i=0;i<2*dx;i++)
out_char(x+1+i,y,GRAFCHAR_HORIZONTALLINE,15);
out_char(x+1+i,y,GRAFCHAR_UPPERRIGHTCORNER,15);
for(j=0;j<dy;j++)
{
out_char(x,y+1+j,GRAFCHAR_VERTICALLINE,15);
for(i=0;i<2*dx;i++) out_char(x+1+i, y+1+j,
sym[(int) ((neurons[j*dx+i/2].axon+0.4999)*5)], 15);
out_char(x+1+i, y+1+j,GRAFCHAR_VERTICALLINE,15);
}
out_char(x,y+j+1,GRAFCHAR_BOTTOMLEFTCORNER,15);
for(i=0;i<2*dx;i++)
out_char(x+i+1,y+j+1,GRAFCHAR_HORIZONTALLINE,15);
out_char(x+1+i,y+j+1, GRAFCHAR_BOTTOMRIGHTCORNER,15);
}
void LayerBP::PrintSynapses(int x, int y)
{
for(unsigned i=0;i<rang;i++)
neurons[i].PrintSynapses(x,y+i);
}
void LayerBP::PrintAxons(int x, int y)
{
for(unsigned i=0;i<rang;i++)
neurons[i].PrintAxons(x,y+i);
}
int LayerBP::IsConverged(void)
{
for(unsigned i=0;i<rang;i++)
if(neurons[i].IsConverged()==0) return 0;
return 1;
}
//
NetBP::NetBP(unsigned nLayers)
{
24
layers=NULL;
if(nLayers==0) { status=ERROR; return; }
layers=new LayerBP _FAR *[nLayers];
if(layers==NULL) status=ERROR;
else
{
rang=nLayers;
for(unsigned i=0;i<rang;i++) layers[i]=NULL;
}
}
NetBP::~NetBP()
{
if(rang)
{
for(unsigned i=0;i<rang;i++) layers[i]->~LayerBP();
delete [] layers; layers=NULL;
}
}
int NetBP::SetLayer(unsigned n, LayerBP _FAR * pl)
{
unsigned i,p;
if(n>=rang) return 1;
p=pl->rang;
if(p==0) return 2;
if(n)
// если не первый слой
{
if(layers[n-1]!=NULL)
// если предыдущий слой уже подключен, про{
// веряем, равно ли число синапсов каждого
// его нейрона числу нейронов предыд. слоя
for(i=0;i<p;i++)
if((*pl).neurons[i].rang!=layers[n-1]->rang)
return 3;
}
}
if(n<rang-1) // если не последний слой
{
if(layers[n+1])
for(i=0;i<layers[n+1]->rang;i++)
if(p!=layers[n+1]->neurons[i].rang) return 4;
}
layers[n]=pl;
return 0;
}
void NetBP::Propagate(void)
{
for(unsigned i=1;i<rang;i++)
layers[i]->Propagate();
25
}
int NetBP::FullConnect(void)
{
LayerBP *l;
unsigned i,j,k,n;
for(i=1;i<rang;i++)
// кроме входного слоя
{
// по всем слоям
l=layers[i];
if(l->rang==0) return 1;
n=(*layers[i-1]).rang;
if(n==0) return 2;
for(j=0;j<l->rang;j++) // по нейронам слоя
{
for(k=0;k<n;k++)
// по синапсам нейрона
{
l->neurons[j].inputs[k]=
&(layers[i-1]->neurons[k].axon);
}
}
}
return 0;
}
void NetBP::SetNetInputs(float _FAR *mv)
{
for(unsigned i=0;i<layers[0]->rang;i++)
layers[0]->neurons[i].axon=mv[i];
}
void NetBP::CalculateError(float _FAR * Target)
{
NeuronBP *n;
float sum;
unsigned i;
int j;
for(i=0;i<layers[rang-1]->rang;i++)
{
n=&(layers[rang-1]->neurons[i]);
n->error=(n->axon-Target[i])*n->D_Sigmoid();
}
for(j=rang-2;j>0;j--)
// по скрытым слоям
{
for(i=0;i<layers[j]->rang;i++)
// по нейронам
{
sum=0.;
for(unsigned k=0;k<layers[j+1]->rang;k++)
sum+=layers[j+1]->neurons[k].error
*layers[j+1]->neurons[k].synapses[i];
layers[j]->neurons[i].error=
sum*layers[j]->neurons[i].D_Sigmoid();
}
}
}
26
void NetBP::Learn(void)
{
for(int j=rang-1;j>0;j--)
{
for(unsigned i=0;i<layers[j]->rang;i++)
{
// по нейронам
for(unsigned k=0;k<layers[j]->neuronrang;k++)
// по синапсам
layers[j]->neurons[i].deltas[k]=NiuParm*
(MiuParm*layers[j]->neurons[i].deltas[k]+
(1.-MiuParm)*layers[j]->neurons[i].error
*layers[j-1]->neurons[k].axon);
}
}
}
void NetBP::Update(void)
{
for(unsigned i=0;i<rang;i++) layers[i]->Update();
}
void NetBP::Randomize(float range)
{
for(unsigned i=0;i<rang;i++)
layers[i]->Randomize(range);
}
void NetBP::Cycle(float _FAR *Inp, float _FAR *Out)
{
SetNetInputs(Inp);
if(dSigma) AddNoise();
Propagate();
CalculateError(Out);
Learn();
Update();
}
int NetBP::SaveToFile(unsigned char *file)
{
FILE *fp;
fp=fopen(file,"wt");
if(fp==NULL) return 1;
fprintf(fp,"%u",rang);
for(unsigned i=0;i<rang;i++)
{
fprintf(fp,"\n+%u",layers[i]->rang);
fprintf(fp,"\n¦%u",layers[i]->neuronrang);
for(unsigned j=0;j<layers[i]->rang;j++)
{
fprintf(fp,"\n¦+%f",layers[i]->neurons[j].state);
fprintf(fp,"\n¦¦%f",layers[i]->neurons[j].axon);
fprintf(fp,"\n¦¦%f",layers[i]->neurons[j].error);
for(unsigned k=0;k<layers[i]->neuronrang;k++)
27
{
fprintf(fp,"\n¦¦%f",
layers[i]->neurons[j].synapses[k]);
}
fprintf(fp,"\n¦+");
}
fprintf(fp,"\n+");
}
fclose(fp);
return 0;
}
int NetBP::LoadFromFile(unsigned char *file)
{
FILE *fp;
unsigned i,r,nr;
unsigned char bf[12];
if(layers) return 1; // возможно использование только
// экземпляров класса, сконструированных по умолчанию
// с помощью NetBP(void).
fp=fopen(file,"rt");
if(fp==NULL) return 1;
fscanf(fp,"%u\n",&r);
if(r==0) goto allerr;
layers=new LayerBP _FAR *[r];
if(layers==NULL)
{ allerr: status=ERROR; fclose(fp); return 2; }
else
{
rang=r;
for(i=0;i<rang;i++) layers[i]=NULL;
}
for(i=0;i<rang;i++)
{
fgets(bf,10,fp);
r=atoi(bf+1);
fgets(bf,10,fp);
nr=atoi(bf+1);
layers[i] = new LayerBP(r,nr);
for(unsigned j=0;j<layers[i]->rang;j++)
{
fscanf(fp,"¦+%f\n",&(layers[i]->neurons[j].state));
fscanf(fp,"¦¦%f\n",&(layers[i]->neurons[j].axon));
fscanf(fp,"¦¦%f\n",&(layers[i]->neurons[j].error));
for(unsigned k=0;k<layers[i]->neuronrang;k++)
{
fscanf(fp,"¦¦%f\n",
&(layers[i]->neurons[j].synapses[k]));
}
fgets(bf,10,fp);
28
}
fgets(bf,10,fp);
}
fclose(fp);
return 0;
}
NetBP::NetBP(unsigned n, unsigned n1, ...)
{
unsigned i, num, prenum;
va_list varlist;
status=OK; rang=0; pf=NULL; learncycle=0; layers=NULL;
layers=new LayerBP _FAR *[n];
if(layers==NULL) { allerr: status=ERROR; }
else
{
rang=n;
for(i=0;i<rang;i++) layers[i]=NULL;
num=n1;
layers[0] = new LayerBP(num,0);
va_start(varlist,n1);
for(i=1;i<rang;i++)
{
prenum=num;
num=va_arg(varlist,unsigned);
layers[i] = new LayerBP(num,prenum);
}
va_end(varlist);
}
}
int NetBP::LoadNextPattern(float _FAR *IN,
float _FAR *OU)
{
unsigned char buf[256];
unsigned char *s, *ps;
int i;
if(pf==NULL) return 1;
if(imgfile)
{
restart:
for(i=0;i<layers[0]->dy;i++)
{
if(fgets(buf,256,pf)==NULL)
{
if(learncycle)
{
rewind(pf);
learncycle--;
goto restart;
}
else return 2;
29
}
for(int j=0;j<layers[0]->dx;j++)
{
if(buf[j]=='x') IN[i*layers[0]->dx+j]=0.5;
else if(buf[j]=='.') IN[i*layers[0]->dx+j]=-0.5;
}
}
if(fgets(buf,256,pf)==NULL) return 3;
for(i=0;i<layers[rang-1]->rang;i++)
{
if(buf[i]!='.') OU[i]=0.5;
else
OU[i]=-0.5;
}
return 0;
}
// "scanf often leads to unexpected results
// if you diverge from an expected pattern." (!)
//
Borland C On-line Help
start:
if(fgets(buf,250,pf)==NULL)
{
if(learncycle)
{
rewind(pf);
learncycle--;
goto start;
}
else return 2;
}
s=buf;
for(;*s==' ';s++);
for(i=0;i<layers[0]->rang;i++)
{
ps=strchr(s,' ');
if(ps) *ps=0;
IN[i]=atof(s);
s=ps+1; for(;*s==' ';s++);
}
if(fgets(buf,250,pf)==NULL) return 4;
s=buf;
for(;*s==' ';s++);
for(i=0;i<layers[rang-1]->rang;i++)
{
ps=strchr(s,' ');
if(ps) *ps=0;
OU[i]=atof(s);
s=ps+1; for(;*s==' ';s++);
}
return 0;
}
30
int NetBP::IsConverged(void)
{
for(unsigned i=1;i<rang;i++)
if(layers[i]->IsConverged()==0) return 0;
return 1;
}
float NetBP::Change(float In)
{
// для бинарного случая
if(In==0.5) return -0.5;
else
return 0.5;
}
void NetBP::AddNoise(void)
{
unsigned i,k;
for(i=0;i<dSigma;i++)
{
k=random(layers[0]->rang);
layers[0]->neurons[k].axon=
Change(layers[0]->neurons[k].axon);
}
}
Листинг 4
// FILE subfun.cpp FOR neuro1.prj & neuro2.prj
#include <string.h>
#include <math.h>
#include <values.h>
#include "neuro.h"
#define vad(x,y) ((y)*160+(x)*2)
void out_char(int x,int y,int c,int at)
{
unsigned far *p;
p=(unsigned far *)(0xB8000000L+
(unsigned long)vad(x,y));
*p=(c & 255) | (at<<8);
}
void out_str(int x,int y,unsigned char *s,unsigned col)
{
for(int i=0;i<strlen(s);i++) out_char(x+i,y,s[i],col);
}
void ClearScreen(void)
{
for(int i=0;i<80;i++) for(int j=0;j<25;j++)
out_char(i,j,' ',7);
}
31
int matherr(struct exception *pe)
{
if(strcmp(pe->name,"exp")==0)
{
if(pe->type==OVERFLOW) pe->retval=MAXDOUBLE;
if(pe->type==UNDERFLOW) pe->retval=MINDOUBLE;
return 10;
}
else
{
if(pe->type==UNDERFLOW || pe->type==TLOSS) return 1;
else return 0;
}
}
int SomeNet::OpenPatternFile(unsigned char *file)
{
pf=fopen(file,"rt");
if(strstr(file,".img")) imgfile=1;
else imgfile=0;
return !((int)pf);
}
int SomeNet::ClosePatternFile(void)
{
int i;
if(pf)
{
i=fclose(pf);
pf=NULL;
return i;
}
return 0;
}
Листинг 5
// FILE neuman1.cpp FOR neuro1.prj
#include <string.h>
#include <conio.h>
#include "neuro.h"
#define N0 30
#define N1 10
#define N2 10
void main()
{
float Inp[N0], Out[N2];
unsigned count;
unsigned char buf[256];
int i;
NetBP N(3,N0,N1,N2);
/* первый способ конструирования сети */
32
/*** второй способ конструирования сети
NeuronBP _FAR *H0, _FAR *H1, _FAR *H2;
H0= new NeuronBP [N0];
H1= new NeuronBP [N1];
H2= new NeuronBP [N2];
for(i=0;i<N1;i++) H1[i].InitNeuron(N0);
for(i=0;i<N2;i++) H2[i].InitNeuron(N1);
LayerBP L0(H0,N0,0);
LayerBP L1(H1,N1,N0);
LayerBP L2(H2,N2,N1);
NetBP N(3);
i=N.SetLayer(0,&L0);
i=N.SetLayer(1,&L1);
i=N.SetLayer(2,&L2); // здесь можно проверить i
***/
/* третий способ создания сети см. в листинге 6 */
ClearScreen();
N.FullConnect();
N.GetLayer(0)->SetName("Input");
N.GetLayer(0)->SetShowDim(1,1,5,6);
N.GetLayer(1)->SetName("Hidden");
N.GetLayer(1)->SetShowDim(15,1,2,5);
N.GetLayer(2)->SetName("Out");
N.GetLayer(2)->SetShowDim(23,1,10,1);
// srand(1);
// меняем особенность случайной структуры сети
SetSigmoidType(HYPERTAN);
SetNiuParm(0.1);
SetLimit(0.001);
SetDSigma(1);
N.Randomize(1);
N.SetLearnCycle(64000U);
N.OpenPatternFile("char1.img");
for(count=0;;count++)
{
sprintf(buf,"Cycle %u",count);
out_str(1,23,buf,10 | (1<<4));
out_str(1,24,"ESC breaks
",11 | (1<<4));
if(kbhit() || i==13) i=getch();
if(i==27) break;
if(i=='s' || i=='S') goto save;
if(N.LoadNextPattern(Inp,Out)) break;
N.Cycle(Inp,Out);
// N.Propagate(); // "сквозной канал"
N.GetLayer(0)->Show();
N.GetLayer(1)->Show();
33
N.GetLayer(2)->Show();
N.GetLayer(2)->PrintAxons(47,0);
if(count && N.IsConverged())
{
save:
out_str(40,24,"FileConf:",15 | (1<<4));
gotoxy(50,25);
gets(buf);
if(strlen(buf)) N.SaveToFile(buf);
break;
}
}
N.ClosePatternFile();
}
Листинг 6
// FILE neuman2.cpp FOR neuro2.prj
#include <string.h>
#include <conio.h>
#include "neuro.h"
#define N0 30
#define N1 10
#define N2 10
main(int argc, char *argv[])
{
NetBP N;
static float Inp[N0], Out[N2];
if(argc!=2) return 1;
ClearScreen();
if(N.LoadFromFile(argv[1])) return 1;
if(N.FullConnect()) return 1;
N.GetLayer(0)->SetName("Input");
N.GetLayer(0)->SetShowDim(1,1,5,6);
N.GetLayer(1)->SetName("Hidden");
N.GetLayer(1)->SetShowDim(15,1,2,5);
N.GetLayer(2)->SetName("Out");
N.GetLayer(2)->SetShowDim(23,1,10,1);
SetSigmoidType(HYPERTAN);
if(N.OpenPatternFile("charnois.img")) return 1;
for(;;)
{
if(N.LoadNextPattern(Inp,Out)) break;
// если все образы кончились, выходим
N.SetNetInputs(Inp);
N.Propagate();
N.GetLayer(0)->Show();
N.GetLayer(1)->Show();
N.GetLayer(2)->Show();
N.GetLayer(2)->PrintAxons(47,0);
34
getch();
}
N.ClosePatternFile();
return 0;
}
Листинг 7
Файл char1.pat (перенос длинных строк по '\'
сделан при верстке)
-0.5
0.5
0.5
0.5
0.5
0.5
0.5
-0.5
0.5
0.5
-0.5
-0.5
0.5
-0.5
-0.5
-0.5
0.5
0.5
-0.5
-0.5
-0.5
0.5
-0.5
-0.5
-0.5
-0.5
-0.5
-0.5
0.5
-0.5
0.5
-0.5
0.5
-0.5
-0.5
-0.5
0.5
0.5
-0.5
-0.5
-0.5
-0.5
0.5
-0.5
-0.5
-0.5
-0.5
0.5
-0.5
0.5
-0.5
-0.5
-0.5
0.5
-0.5
-0.5
-0.5
-0.5
0.5
-0.5
-0.5
-0.5
-0.5
-0.5
0.5
-0.5
-0.5
-0.5
0.5
0.5
-0.5
-0.5
0.5
-0.5
-0.5
-0.5
0.5
0.5
-0.5
-0.5
0.5
0.5
0.5
-0.5
-0.5
0.5
0.5
-0.5
-0.5
0.5
0.5
0.5
-0.5
-0.5
0.5
-0.5
-0.5
-0.5
-0.5
-0.5
-0.5
-0.5
0.5
-0.5
0.5
-0.5
0.5
-0.5
0.5
-0.5
-0.5
-0.5
0.5
-0.5
0.5
-0.5
0.5
0.5
0.5
-0.5
-0.5
-0.5
0.5
-0.5
-0.5
0.5
0.5
-0.5
-0.5
0.5
0.5
-0.5
-0.5
0.5
0.5
0.5
0.5
-0.5
0.5
-0.5
0.5
0.5
0.5
-0.5
0.5
-0.5
-0.5
-0.5
0.5
-0.5
-0.5
-0.5
0.5
0.5
0.5
-0.5
0.5
-0.5
0.5
-0.5
-0.5
0.5
-0.5
-0.5
0.5
0.5
-0.5
-0.5
0.5
0.5
-0.5
-0.5
0.5
0.5
-0.5
-0.5
0.5
0.5
-0.5
0.5
-0.5
0.5
-0.5
-0.5
-0.5
0.5
0.5
-0.5
0.5
-0.5
-0.5
-0.5
0.5
0.5
-0.5
-0.5
-0.5
0.5
0.5
-0.5
-0.5
-0.5
-0.5
-0.5
0.5
-0.5
-0.5
-0.5
0.5
-0.5
-0.5
-0.5
0.5
-0.5
-0.5
-0.5
0.5
-0.5
-0.5
-0.5
-0.5
-0.5
-0.5
0.5
0.5
-0.5
0.5
-0.5
-0.5
-0.5
0.5
-0.5
0.5
-0.5
-0.5
-0.5
0.5
-0.5
0.5
-0.5
0.5
-0.5
-0.5
-0.5
0.5
0.5
-0.5
-0.5
-0.5
-0.5
-0.5
-0.5
-0.5
0.5
-0.5
-0.5
-0.5
0.5
-0.5
-0.5
-0.5
-0.5
-0.5
-0.5
-0.5
-0.5
0.5
0.5
-0.5
0.5
-0.5
-0.5
-0.5
-0.5
-0.5
-0.5
-0.5
-0.5
0.5
-0.5
-0.5
-0.5
0.5
-0.5
-0.5
-0.5
0.5
-0.5
-0.5
-0.5
0.5
-0.5
-0.5
-0.5
0.5
-0.5
0.5
-0.5
0.5
-0.5
0.5
-0.5
0.5
-0.5
-0.5
-0.5
-0.5
-0.5
0.5
-0.5
-0.5
0.5
-0.5
-0.5
0.5
-0.5
-0.5
-0.5
-0.5
-0.5
0.5 -0.5 -0.5 \
0.5 0.5 0.5 \
-0.5 -0.5
0.5 0.5 0.5 \
0.5 0.5 -0.5 \
-0.5 -0.5
-0.5 0.5 0.5 \
0.5 0.5 -0.5 \
-0.5 -0.5
0.5 0.5
0.5 0.5
0.5 \
0.5 \
-0.5 -0.5
-0.5 -0.5 0.5 \
-0.5 0.5 -0.5 \
-0.5 -0.5
-0.5 0.5 -0.5 \
0.5 0.5 -0.5 \
-0.5 -0.5
-0.5 0.5 0.5 \
-0.5 0.5 -0.5 \
-0.5 -0.5
-0.5 -0.5 -0.5 \
-0.5 -0.5 -0.5 \
-0.5 -0.5
-0.5 0.5 0.5 \
0.5 0.5 -0.5 \
0.5 -0.5
-0.5 0.5 0.5 \
0.5 0.5 -0.5 \
-0.5
0.5
Листинг 8
Файл char1.img
35
..x..
.x.x.
.x.x.
x...x
xxxxx
x...x
A.........
x...x
xx.xx
xx.xx
x.x.x
x.x.x
x...x
.M........
x...x
x...x
xxxxx
x...x
x...x
x...x
..H.......
x...x
x..xx
x.x.x
x.x.x
xx..x
x...x
...N......
x..xx
x.x..
xx...
x.x..
x..x.
x...x
....K.....
...x.
..x.x
.x..x
x...x
x...x
x...x
.....L....
.xxx.
x...x
x....
x....
x...x
.xxx
......C...
xxxxx
..x..
..x..
..x..
..x..
36
..x..
.......T..
xxxxx
x...x
x...x
x...x
x...x
x...x
........P.
xxxx.
x...x
xxxx
x..xx
x...x
xxxx.
.........B
С.Короткий
Нейронные сети: обучение без учителя
В статье рассмотрены алгоритмы обучения искусственных нейронных сетей без
учителя. Приведена библиотека классов на C++ и тестовый пример.
Рассмотренный в [1] алгоритм обучения нейронной сети с помощью процедуры
обратного распространения подразумевает наличие некоего внешнего звена,
предоставляющего сети кроме входных так же и целевые выходные образы.
Алгоритмы, пользующиеся подобной концепцией, называются алгоритмами обучения с
учителем. Для их успешного функционирования необходимо наличие экспертов,
создающих на предварительном этапе для каждого входного образа эталонный
выходной. Так как создание искусственного интеллекта движется по пути копирования
природных прообразов, ученые не прекращают спор на тему, можно ли считать
алгоритмы обучения с учителем натуральными или же они полностью искусственны.
Например, обучение человеческого мозга, на первый взгляд, происходит без учителя:
на зрительные, слуховые, тактильные и прочие рецепторы поступает информация
извне, и внутри нервной системы происходит некая самоорганизация. Однако, нельзя
отрицать и того, что в жизни человека не мало учителей – и в буквальном, и в
переносном смысле, – которые координируют внешние воздействия. Вместе в тем, чем
37
бы ни закончился спор приверженцев этих двух концепций обучения, они обе имеют
право на существование.
Главная черта, делающая обучение без учителя привлекательным, – это его
"самостоятельность". Процесс обучения, как и в случае обучения с учителем,
заключается в подстраивании весов синапсов. Некоторые алгоритмы, правда, изменяют
и структуру сети, то есть количество нейронов и их взаимосвязи, но такие
преобразования правильнее назвать более широким термином – самоорганизацией, и в
рамках данной статьи они рассматриваться не будут. Очевидно, что подстройка
синапсов может проводиться только на основании информации, доступной в нейроне,
то есть его состояния и уже имеющихся весовых коэффициентов. Исходя из этого
соображения и, что более важно, по аналогии с известными принципами
самоорганизации нервных клеток[2], построены алгоритмы обучения Хебба.
Сигнальный метод обучения Хебба заключается в изменении весов по
следующему правилу:
(1)
wij (t ) = wij (t − 1) + α ⋅ yi( n−1) ⋅ y (j n )
где yi(n-1) – выходное значение нейрона i слоя (n-1), yj(n) – выходное значение
нейрона j слоя n; wij(t) и wij(t-1) – весовой коэффициент синапса, соединяющего эти
нейроны, на итерациях t и t-1 соответственно; α – коэффициент скорости обучения.
Здесь и далее, для общности, под n подразумевается произвольный слой сети. При
обучении по данному методу усиливаются связи между возбужденными нейронами.
Существует также и дифференциальный метод обучения Хебба.
(2)
wij (t ) = wij (t − 1) + α ⋅ yi( n−1) (t ) − yi( n−1) (t − 1) ⋅ y (j n ) (t ) − y (j n ) (t − 1)
(n-1)
(n-1)
[
][
]
Здесь yi (t) и yi (t-1) – выходное значение нейрона i слоя n-1 соответственно
на итерациях t и t-1; yj(n)(t) и yj(n)(t-1) – то же самое для нейрона j слоя n. Как видно из
формулы (2), сильнее всего обучаются синапсы, соединяющие те нейроны, выходы
которых наиболее динамично изменились в сторону увеличения.
Полный алгоритм обучения с применением вышеприведенных формул будет
выглядеть так:
1. На стадии инициализации всем весовым коэффициентам присваиваются
небольшие случайные значения.
2. На входы сети подается входной образ, и сигналы возбуждения
распространяются по всем слоям согласно принципам классических прямопоточных
(feedforward) сетей[1], то есть для каждого нейрона рассчитывается взвешенная сумма
его входов, к которой затем применяется активационная (передаточная) функция
нейрона, в результате чего получается его выходное значение yi(n), i=0...Mi-1, где Mi –
число нейронов в слое i; n=0...N-1, а N – число слоев в сети.
3. На основании полученных выходных значений нейронов по формуле (1) или (2)
производится изменение весовых коэффициентов.
4. Цикл с шага 2, пока выходные значения сети не застабилизируются с заданной
точностью. Применение этого нового способа определения завершения обучения,
отличного от использовавшегося для сети обратного распространения, обусловлено
тем, что подстраиваемые значения синапсов фактически не ограничены.
На втором шаге цикла попеременно предъявляются все образы из входного
набора.
Следует отметить, что вид откликов на каждый класс входных образов не
известен заранее и будет представлять собой произвольное сочетание состояний
нейронов выходного слоя, обусловленное случайным распределением весов на стадии
инициализации. Вместе с тем, сеть способна обобщать схожие образы, относя их к
38
одному классу. Тестирование обученной сети позволяет определить топологию классов
в выходном слое. Для приведения откликов обученной сети к удобному представлению
можно дополнить сеть одним слоем, который, например, по алгоритму обучения
однослойного перцептрона необходимо заставить отображать выходные реакции сети в
требуемые образы.
Другой алгоритм обучения без учителя – алгоритм Кохонена – предусматривает
подстройку синапсов на основании их значений от предыдущей итерации.
(3)
wij (t ) = wij (t − 1) + α ⋅ yi( n−1) − wij (t − 1)
[
]
Из вышеприведенной формулы видно, что обучение сводится к минимизации
разницы между входными сигналами нейрона, поступающими с выходов нейронов
предыдущего слоя yi(n-1), и весовыми коэффициентами его синапсов.
Полный алгоритм обучения имеет примерно такую же структуру, как в методах
Хебба, но на шаге 3 из всего слоя выбирается нейрон, значения синапсов которого
максимально походят на входной образ, и подстройка весов по формуле (3) проводится
только для него. Эта, так называемая, аккредитация может сопровождаться
затормаживанием всех остальных нейронов слоя и введением выбранного нейрона в
насыщение. Выбор такого нейрона может осуществляться, например, расчетом
скалярного произведения вектора весовых коэффициентов с вектором входных значений. Максимальное произведение дает выигравший нейрон.
Другой вариант – расчет расстояния между этими векторами в p-мерном
пространстве, где p – размер векторов.
Dj =
p −1
∑ (y
( n −1)
i
− wij ) 2 ,
(4)
i=0
где j – индекс нейрона в слое n, i – индекс суммирования по нейронам слоя (n-1),
wij – вес синапса, соединяющего нейроны; выходы нейронов слоя (n-1) являются
входными значениями для слоя n. Корень в формуле (4) брать не обязательно, так как
важна лишь относительная оценка различных Dj.
В данном случае, "побеждает" нейрон с наименьшим расстоянием. Иногда
слишком часто получающие аккредитацию нейроны принудительно исключаются из
рассмотрения, чтобы "уравнять права" всех нейронов слоя. Простейший вариант такого
алгоритма заключается в торможении только что выигравшего нейрона.
При использовании обучения по алгоритму Кохонена существует практика
нормализации входных образов, а так же – на стадии инициализации – и нормализации
начальных значений весовых коэффициентов.
xi = xi /
n −1
∑x
2
j
,
(5)
j=0
где xi – i-ая компонента вектора входного образа или вектора весовых
коэффициентов, а n – его размерность. Это позволяет сократить длительность процесса
обучения.
Инициализация весовых коэффициентов случайными значениями может привести
к тому, что различные классы, которым соответствуют плотно распределенные
входные образы, сольются или, наоборот, раздробятся на дополнительные подклассы в
случае близких образов одного и того же класса. Для избежания такой ситуации
используется метод выпуклой комбинации[3]. Суть его сводится к тому, что входные
нормализованные образы подвергаются преобразованию:
39
1
,
(6)
n
где xi – i-ая компонента входного образа, n – общее число его компонент, α(t) –
коэффициент, изменяющийся в процессе обучения от нуля до единицы, в результате
чего вначале на входы сети подаются практически одинаковые образы, а с течением
времени они все больше сходятся к исходным. Весовые коэффициенты
устанавливаются на шаге инициализации равными величине
1
,
(7)
wo =
n
где n – размерность вектора весов для нейронов инициализируемого слоя.
На основе рассмотренного выше метода строятся нейронные сети особого типа –
так называемые самоорганизующиеся структуры – self-organizing feature maps (этот
устоявшийся перевод с английского, на мой взгляд, не очень удачен, так как, речь идет
не об изменении структуры сети, а только о подстройке синапсов). Для них после
выбора из слоя n нейрона j с минимальным расстоянием Dj (4) обучается по формуле
(3) не только этот нейрон, но и его соседи, расположенные в окрестности R. Величина
R на первых итерациях очень большая, так что обучаются все нейроны, но с течением
времени она уменьшается до нуля. Таким образом, чем ближе конец обучения, тем
точнее определяется группа нейронов, отвечающих каждому классу образов. В
приведенной ниже программе используется именно этот метод обучения.
Развивая объектно-ориентированный подход в моделировании нейронных сетей,
рассмотренный в [1], для программной реализации сетей, использующих алгоритм
обучения без учителя, были разработаны отдельные классы объектов типа нейрон, слой
и сеть, названия которых снабжены суффиксом UL. Они наследуют основные свойства
от соответствующих объектов прямопоточной сети, описанной в [1]. Фрагмент
заголовочного файла с описаниями классов и функций для таких сетей представлен на
листинге 1.
Как видно из него, в классе NeuronUL в отличие от NeuronBP отсутствуют
обратные связи и инструменты их поддержания, а по сравнению с NeuronFF здесь
появилось лишь две новых переменных – delta и inhibitory. Первая из них хранит
расстояние, рассчитываемое по формуле (4), а вторая – величину заторможенности
нейрона. В классе NeuronUL существует два конструктора – один, используемый по
умолчанию, – не имеет параметров, и к созданным с помощью него нейронам
необходимо затем применять метод _allocateNeuron класса NeuronFF. Другой сам
вызывает эту функцию через соответствующий конструктор NeuronFF. Метод Propagate
является почти полным аналогом одноименного метода из NeuronFF, за исключением
вычисления величин delta и inhibitory. Методы Normalize и Equalize выполняют
соответственно нормализацию значений весовых коэффициентов по формуле (5) и их
установку согласно (7). Метод CountDistance вычисляет расстояние (4). Следует особо
отметить, что в классе отсутствует метод IsConverged, что, объясняется, как говорилось
выше, различными способами определения факта завершения обучения. Хотя в
принципе написать такую функцию не сложно, в данной программной реализации
завершение обучения определяется по "телеметрической" информации, выводимой на
экран, самим пользователем. В представленном же вместе со статьей тесте число
итераций, выполняющих обучение, вообще говоря, эмпирически задано равным 3000.
В состав класса LayerUL входит массив нейронов neurons и переменная с
размерностью массивов синапсов – neuronrang. Метод распределения нейронов –
внешний или внутренний – определяется тем, как создавался слой. Этот признак
xi = α (t ) ⋅ xi + (1 − α (t )) ⋅
40
хранится в переменной allocation. Конструктор LayerUL(unsigned, unsigned) сам
распределяет память под нейроны, что соответствует внутренней инициализации;
конструктор LayerUL(NeuronUL _FAR *, unsigned, unsigned) создает слой из уже
готового, внешнего массива нейронов. Все методы этого класса аналогичны
соответствующим методам класса LayerFF и, в большинстве своем, используют
одноименные методы класса NeuronUL.
В классе NetUL также особое внимание необходимо уделить конструкторам. Один
из них – NetUL(unsigned n) создает сеть из n пустых слоев, которые затем необходимо
заполнить с помощью метода SetLayer. Конструктор NetUL(unsigned n, unsigned n1, ...)
не только создает сеть из n слоев, но и распределяет для них соответствующее число
нейронов с синапсами, обеспечивающими полносвязность сети. После создания сети
необходимо связать все нейроны с помощью метода FullConnect. Как и в случае сети
обратного распространения, сеть можно сохранять и загружать в/из файла с помощью
методов SaveToFile, LoadFromFile. Из всех остальных методов класса новыми по сути
являются лишь NormalizeNetInputs и ConvexCombination. Первый из них нормализует
входные вектора, а второй реализует преобразование выпуклой комбинации (6).
В конце заголовочного файла описаны глобальные функции. SetSigmoidTypeUL,
SetSigmoidAlfaUL и SetDSigmaUL аналогичны одноименным функциям для сети
обратного распространения. Функция SetAccreditationUL устанавливает режим, при
котором эффективность обучения нейронов, попавших в окружение наиболее
возбужденного на данной итерации нейрона, пропорциональна функции Гаусса от
расстояния до центра области обучения. Если этот режим не включен, то все нейроны
попавшие в область с текущим радиусом обучения одинаково быстро подстраивают
свои синапсы, причем область является квадратом со стороной, равной радиусу
обучения. Функция SetLearnRateUL устанавливает коэффициент скорости обучения, а
SetMaxDistanceUL – радиус обучения. Когда он равен 0 – обучается только один
нейрон. Функции SetInhibitionUL и SetInhibitionFresholdUL устанавливают
соответственно длительность торможения и величину возбуждения, его вызывающего.
Тексты функций помещены в файле neuro_mm.cpp, представленном в листинге 2.
Кроме него в проект тестовой программы входят также модули neuron_ff.cpp и
subfun.cpp, описанные в [1]. Главный модуль, neuman7.cpp приведен в листинге 3.
Программа компилировалась с помощью компилятора Borland C++ 3.1 в модели
LARGE.
Тестовая программа демонстрирует христоматийный пример обучения самонастраивающейся сети следующей конфигурации. Входной слой состоит из двух
нейронов, значения аксонов которых генерируются вспомогательной функцией на
основе генератора случайных чисел. Выходной слой имеет размер 10 на 10 нейронов. В
процессе обучения он приобретает свойства упорядоченной структуры, в которой
величины синапсов нейронов плавно меняются вдоль двух измерений, имитируя
двумерную сетку координат. Благодаря новой функции DigiShow и выводу индексов X
и Y выигравшего нейрона, пользователь имеет возможность убедиться, что значения на
входе сети достаточно точно определяют позицию точки максимального возбуждения
на ее выходе.
Необходимо отметить, что обучение без учителя гораздо более чувствительно к
выбору оптимальных параметров, нежели обучение с учителем. Во-первых, его
качество сильно зависит от начальных величин синапсов. Во-вторых, обучение
критично к выбору радиуса обучения и скорости его изменения. И наконец, разумеется,
очень важен характер изменения собственно коэффициента обучения. В связи с этим
41
пользователю, скорее всего, потребуется провести предварительную работу по подбору
оптимальных параметров обучения сети.
Несмотря на некоторые сложности реализации, алгоритмы обучения без учителя
находят обширное и успешное применение. Например, в [4] описана многослойная
нейронная сеть, которая по алгоритму самоорганизующейся структуры обучается
распознавать рукописные символы. Возникающее после обучения разбиение на классы
может в случае необходимости уточняться с помощью обучения с учителем. По сути
дела, по алгоритму обучения без учителя функционируют и наиболее сложные из
известных на сегодняшний день искусственные нейронные сети – когнитрон и
неокогнитрон, – максимально приблизившиеся в своем воплощении к структуре мозга.
Однако они, конечно, существенно отличаются от рассмотренных выше сетей и
намного более сложны. Тем не менее, на основе вышеизложенного материала можно
создать реально действующие системы для распознавания образов, сжатия
информации, автоматизированного управления, экспертных оценок и много другого.
Литература
1. С.Короткий, Нейронные сети: алгоритм обратного распространения.
2. Ф.Блум, А.Лейзерсон, Л.Хофстедтер, Мозг, разум и поведение, М., Мир, 1988.
3. Ф.Уоссермен, Нейрокомпьютерная техника, М., Мир, 1992.
4. Keun-Rong Hsieh and Wen-Tsuen Chen, A Neural Network Model which Combines
Unsupervised and Supervised Learning, IEEE Trans. on Neural Networks, vol.4, No.2,
march 1993.
42
Листинг 1
// FILE neuro_mm.h
#include "neuro.h" //
описание базовых классов
class LayerUL;
class NetUL;
class NeuronUL: public
NeuronFF
{
friend LayerUL;
friend NetUL;
float delta;
unsigned inhibitory;
public:
NeuronUL(unsigned
num_inputs);
NeuronUL(void){delta=0.;};
~NeuronUL();
// int IsConverged(void); //
можно реализовать
virtual void
Propagate(void);
void Equalize(void);
void Normalize(void);
float CountDistance(void);
void SetInhibitory(unsigned
in){inhibitory=in;};
unsigned
GetInhibitory(void){return
inhibitory;};
};
class LayerUL: public LayerFF
{
friend NetUL;
NeuronUL _FAR *neurons;
unsigned neuronrang;
int allocation;
int imax, imaxprevious1;
public:
LayerUL(unsigned nRang,
unsigned nSinapses);
LayerUL(NeuronUL _FAR *Neu,
unsigned nRang,
unsigned nSinapses);
LayerUL(void)
{
neurons=NULL; neuronrang=0;
allocation=EXTERN;
imax=imaxprevious1=-1;
};
~LayerUL();
void Propagate(void);
void Randomize(float);
void Normalize(void);
void
NormalizeSynapses(void);
void Equalize(void);
virtual void Show(void);
virtual void DigiShow(void);
virtual void
PrintSynapses(int,int);
virtual void PrintAxons(int
x, int y, int direction);
void TranslateAxons(void);
NeuronUL *GetNeuron(unsigned
n)
{
if(neurons && (n<rang))
return &neurons[n];
else return NULL;
};
};
class NetUL: public SomeNet
{
LayerUL _FAR * _FAR *layers;
// 1-й слой - входной, без
синапсов
public:
NetUL(void) {layers=NULL;};
NetUL(unsigned nLayers);
NetUL(unsigned n, unsigned
n1, ...);
~NetUL();
int SetLayer(unsigned n,
LayerUL _FAR *pl);
LayerUL *GetLayer(unsigned
n)
{if(n<rang) return
layers[n]; else return NULL;
}
int FullConnect(void);
void Propagate(void);
void SetNetInputs(float _FAR
*mvalue);
void Learn(void);
void Randomize(float);
void Normalize(void);
void
NormalizeSynapses(void);
void Equalize(void);
int SaveToFile(unsigned
char *file);
43
int LoadFromFile(unsigned
char *file);
int LoadNextPattern(float
_FAR *IN);
void SetLearnCycle(unsigned
l){learncycle=l;};
void virtual
PrintSynapses(int x=0,...){};
float virtual Change(float
In);
void AddNoise(void);
void
NormalizeNetInputs(float _FAR
*mv);
void ConvexCombination(float
*In, float step);
int Reverse(NetUL **Net);
};
static float
SigmoidAlfa=1.;
static float
LearnRate=0.25;
static unsigned dSigma=0;
static float
MaxDistance=MAXDISTANCE;
static int
Inhibition=0;
static float
InFreshold=0.0;
int SetSigmoidTypeUL(int st);
float SetSigmoidAlfaUL(float
Al);
float SetLearnRateUL(float
lr);
unsigned SetDSigmaUL(unsigned
d);
void SetAccreditationUL(int
ac);
float SetMaxDistanceUL(float
md);
void SetInhibitionUL(int in);
float
SetInhibitionFreshold(float
f);
void SetAccreditationUL(int
ac)
{
Accreditation=ac;
}
Листинг 2
// FILE neuro_mm.cpp FOR
neuro_mm.prj
#include <stdlib.h>
#include <alloc.h>
#include <math.h>
#include <string.h>
#include <stdarg.h>
#include <values.h>
#include "neuro_mm.h"
#include "colour.h"
#define MAXDISTANCE MAXFLOAT
static int
SigmoidType=ORIGINAL;
static int
Accreditation=0;
int SetSigmoidTypeUL(int st)
{
int i;
i=SigmoidType;
SigmoidType=st;
return i;
}
// число циклов на которые
нейрон тормозится
// после возбуждения
void SetInhibitionUL(int in)
{
Inhibition=in;
}
// порог возбуждения,
инициирующий торможение
float
SetInhibitionFreshold(float
f)
{
float a;
a=InFreshold;
InFreshold=f;
return a;
}
float SetSigmoidAlfaUL(float
Al)
{
float a;
a=SigmoidAlfa;
SigmoidAlfa=Al;
return a;
}
44
float SetMaxDistanceUL(float
md)
{
float a;
a=MaxDistance;
if(md<1.0) Accreditation=0;
MaxDistance=md;
return a;
}
if(axon>InFreshold) //
возбуждение
{
if(inhibitory<=0) // пока
не заторможен
float SetLearnRateUL(float
lr)
{
float a;
a=LearnRate;
LearnRate=lr;
return a;
}
// постепенное
восстановление возможности
возбуждаться
if(inhibitory>0) inhibitory-;
if(Inhibition==0) return;
inhibitory=axon*Inhibition/0.
5; // тормозим
}
}
unsigned SetDSigmaUL(unsigned
d)
{
unsigned u;
u=dSigma;
dSigma=d;
return u;
}
NeuronUL::~NeuronUL()
{
// dummy
}
NeuronUL::NeuronUL(unsigned
num_inputs)
:NeuronFF(num_inputs)
{
delta=0.; inhibitory=0;
}
void
NeuronUL::Propagate(void)
{
state=0.;
for(unsigned i=0;i<rang;i++)
state+=(*inputs[i]*2)*(synaps
es[i]*2);
// поправка на использование
логики ±0.5
state/=2;
axon=Sigmoid();
void NeuronUL::Equalize(void)
{
float sq=1./sqrt(rang);
for(int i=0;i<rang;i++)
synapses[i]=sq-0.5;
}
void
NeuronUL::Normalize(void)
{
float s=0;
for(int i=0;i<rang;i++)
s+=synapses[i]*synapses[i];
s=sqrt(s);
if(s) for(int
i=0;i<rang;i++)
synapses[i]/=s;
}
LayerUL::LayerUL(unsigned
nRang, unsigned nSynapses)
{
allocation=EXTERN;
status=ERROR; neuronrang=0;
if(nRang==0) return;
neurons=new NeuronUL[nRang];
if(neurons==NULL) return;
for(unsigned
i=0;i<nRang;i++)
{
neurons[i].InitNeuron(nSynaps
es);
45
neurons[i].SetInhibitory(0.0)
;
if(neurons[i].GetStatus()==ER
ROR)
{
status=ERROR;
return;
}
}
rang=nRang;
neuronrang=nSynapses;
allocation=INNER;
name=NULL; status=OK;
imax=/*-imaxprevious1=*/-1;
}
LayerUL::LayerUL(NeuronUL
_FAR *Neu, unsigned nRang,
unsigned
nSynapses)
{
neurons=NULL; neuronrang=0;
allocation=EXTERN;
for(unsigned
i=0;i<nRang;i++)
if(Neu[i].rang!=nSynapses)
status=ERROR;
if(status==OK)
{
neurons=Neu;
rang=nRang;
neuronrang=nSynapses;
imax=/*-imaxprevious1=*/-1;
}
}
LayerUL::~LayerUL(void)
{
if(allocation==INNER)
{
if(neurons!=NULL)
{
for(unsigned
i=0;i<rang;i++)
neurons[i]._deallocate();
delete [] neurons;
neurons=NULL;
}
}
}
void LayerUL::Randomize(float
range)
{
for(unsigned i=0;i<rang;i++)
neurons[i].Randomize(range);
}
void LayerUL::Equalize(void)
{
for(unsigned i=0;i<rang;i++)
neurons[i].Equalize();
}
void
LayerUL::NormalizeSynapses(vo
id)
{
for(unsigned i=0;i<rang;i++)
neurons[i].Normalize();
}
void LayerUL::Normalize(void)
{
float s=0.;
for(unsigned i=0;i<rang;i++)
s+=(neurons[i].axon+0.5)*(neu
rons[i].axon+0.5);
s=sqrt(s);
if(s) for(i=0;i<rang;i++)
neurons[i].axon=(neurons[i].a
xon+0.5)/s-0.5;
}
void LayerUL::Show(void)
{
unsigned char
sym[5]={GRAFCHAR_EMPTYBLACK,
GRAFCHAR_DARKGRAY,
GRAFCHAR_MIDDLEGRAY,
GRAFCHAR_LIGHTGRAY,
GRAFCHAR_SOLIDWHITE };
int i,j;
if(y && name)
for(i=0;i<strlen(name);i++)
out_char(x+i,y1,name[i],3);
out_char(x,y,GRAFCHAR_UPPERLE
FTCORNER,15);
for(i=0;i<2*dx;i++)
46
out_char(x+1+i,y,GRAFCHAR_HOR
IZONTALLINE,15);
out_char(x+1+i,y,GRAFCHAR_UPP
ERRIGHTCORNER,15);
for(j=0;j<dy;j++)
{
out_char(x,y+1+j,GRAFCHAR_VER
TICALLINE,15);
for(i=0;i<2*dx;i++)
{
int n=(int)
((neurons[j*dx+i/2].axon+0.49
99)*5);
if(n<0) n=0;
if(n>=5) n=4;
if(j*dx+i/2<rang)
out_char(x+1+i, y+1+j,
sym[n], 15);
}
out_char(x+1+i,
y+1+j,GRAFCHAR_VERTICALLINE,1
5);
}
out_char(x,y+j+1,GRAFCHAR_BOT
TOMLEFTCORNER,15);
for(i=0;i<2*dx;i++)
out_char(x+i+1,y+j+1,GRAFCHAR
_HORIZONTALLINE,15);
out_char(x+1+i,y+j+1,
GRAFCHAR_BOTTOMRIGHTCORNER,15
);
}
// вывод уровня возбуждения
цветом и цифрами
// одновременно
void LayerUL::DigiShow(void)
{
int i,j;
char cc[3];
if(y && name)
for(i=0;i<strlen(name);i++)
out_char(x+i,y1,name[i],3);
out_char(x,y,GRAFCHAR_UPPERLE
FTCORNER,15);
for(i=0;i<2*dx;i++)
out_char(x+1+i,y,GRAFCHAR_HOR
IZONTALLINE,15);
out_char(x+1+i,y,GRAFCHAR_UPP
ERRIGHTCORNER,15);
for(j=0;j<dy;j++)
{
out_char(x,y+1+j,GRAFCHAR_VER
TICALLINE,15);
for(i=0;i<2*dx;i++)
{
int n=(int)
((neurons[j*dx+i/2].axon+0.49
99)*100);
if(n<0) n=0;
if(n>=100) n=99;
sprintf(cc,"%02d",n);
if(j*dx+i/2<rang)
{
int a;
if(n>=70) a=CGRAY;
else if(n>=50) a=CCYAN;
else if(n>=30) a=CBLUE;
else a=0;
if(i%2==0)
out_char(x+1+i, y+1+j,
cc[0], 15 | a);
else
out_char(x+1+i, y+1+j,
cc[1], 15 | a);
}
}
out_char(x+1+i,
y+1+j,GRAFCHAR_VERTICALLINE,1
5);
}
out_char(x,y+j+1,GRAFCHAR_BOT
TOMLEFTCORNER,15);
for(i=0;i<2*dx;i++)
out_char(x+i+1,y+j+1,GRAFCHAR
_HORIZONTALLINE,15);
47
out_char(x+1+i,y+j+1,
GRAFCHAR_BOTTOMRIGHTCORNER,15
);
}
{
fmax=f;
imax=i;
}
}
void
LayerUL::PrintSynapses(int x,
int y)
{
for(unsigned i=0;i<rang;i++)
neurons[i].PrintSynapses(x,y+
i);
}
void LayerUL::PrintAxons(int
x, int y, int direction)
{
for(unsigned i=0;i<rang;i++)
if(imax==-1)
{
out_str(0,13,"minD=???",10);
return;
}
ny=imax/dx;
// вычисление
координат X & Y
nx=imax%dx;
char buf[40];
neurons[i].PrintAxons(x+8*i*d
irection,
sprintf(buf,"minD=%d(%d,%d)",
imax,nx,ny);
out_str(0,13,buf,10);
y+i*(!direction));
}
for(i=0;i<rang;i++)
neurons[i].delta = 0;
float
NeuronUL::CountDistance(void)
{
int i;
float s=0.0;
for(i=0;i<rang;i++)
s+=fabs(*inputs[i]synapses[i]);
delta=s;
return delta;
}
void LayerUL::Propagate(void)
{
unsigned i,j;
float fmax, f;
int cx, cy, nx, ny;
for(i=0;i<rang;i++)
neurons[i].Propagate();
fmax=MAXDISTANCE;
imax=-1;
for(i=0;i<rang;i++)
{
f=neurons[i].CountDistance();
if(f<fmax)
if(0==Accreditation)
//neurons[imax].delta = 1;
{
for(cx=max(nx(int)MaxDistance,0);
cx<min(nx+(int)MaxDistance+1,
dx);cx++)
{
if(dy > 0)
{
for(cy=max(ny(int)MaxDistance,0);
cy<min(ny+(int)MaxDistance+1,
dy);cy++)
{
// нейрон в зоне
обучения
neurons[cy*dx+cx].delta
= 1;
}
}
else
{
neurons[cx].delta = 1;
48
}
}
}
}
}
}
else //if(Accreditation)
{
for(i=0;i<rang;i++)
{
int y=i/dx;
int x=i%dx;
int NetUL::SetLayer(unsigned
n, LayerUL _FAR * pl)
{
unsigned i,p;
if(fabs(MaxDistance)>=1.0)
neurons[i].delta=
exp(-sqrt((nx-x)*(nxx)+(ny-y)*(ny-y))
/MaxDistance);
else Accreditation=0;
}
}
}
NetUL::NetUL(unsigned
nLayers)
{
layers=NULL;
if(nLayers==0)
{
status=ERROR; return;
}
layers=new LayerUL _FAR
*[nLayers];
if(layers==NULL)
status=ERROR;
else
{
rang=nLayers;
for(unsigned
i=0;i<rang;i++)
layers[i]=NULL;
}
}
if(n>=rang) return 1;
p=pl->rang;
if(p==0) return 2;
if(n)
// если не первый
слой
{
if(layers[n-1]!=NULL)
// если предыдущий
слой уже установлен,
{
// проверяем
соответствие числа нейронов
// в нем и синапсов в
добавляемом слое
for(i=0;i<p;i++)
if((*pl).neurons[i].rang!=lay
ers[n-1]->rang)
return 3;
}
}
if(n<rang-1) // если не
последний слой
{
if(layers[n+1])
for(i=0;i<layers[n+1]>rang;i++)
if(p!=layers[n+1]>neurons[i].rang) return 4;
}
layers[n]=pl;
return 0;
}
NetUL::~NetUL()
{
if(rang)
{
if(layers!=NULL)
{
for(unsigned
i=0;i<rang;i++) layers[i]>~LayerUL();
delete [] layers;
layers=NULL;
int NetUL::FullConnect(void)
{
LayerUL *l;
unsigned i,j,k,n;
for(i=1;i<rang;i++) //
кроме входного слоя
{
l=layers[i];
// по слоям
if(l->rang==0) return 1;
49
n=(*layers[i-1]).rang;
if(n==0) return 2;
for(j=0;j<l->rang;j++)
// по нейронам
{
for(k=0;k<n;k++)
// по синапсам
{
l->neurons[j].inputs[k]=
&(layers[i-1]>neurons[k].axon);
}
}
}
return 0;
}
void NetUL::Propagate(void)
{
for(unsigned i=1;i<rang;i++)
{
layers[i]->Propagate();
}
}
void
NetUL::SetNetInputs(float
_FAR *mv)
{
for(unsigned
i=0;i<layers[0]->rang;i++)
layers[0]>neurons[i].axon=mv[i];
}
void
NetUL::NormalizeNetInputs(flo
at _FAR *mv)
{
float s=0.;
for(unsigned
i=0;i<layers[0]->rang;i++)
s+=(mv[i]+0.5)*(mv[i]+0.5);
s=sqrt(s);
if(s) for(i=0;i<layers[0]>rang;i++)
mv[i]=(mv[i]+0.5)/s-0.5;
}
int Signum(float a, float b)
{
if(a<0 && b<0) return -1;
if(a>0 && b>0) return 1;
return 0;
}
void
LayerUL::TranslateAxons(void)
{
if(0==Accreditation) return;
for(int i=0;i<rang;i++)
{
neurons[i].axon=neurons[i].de
lta-0.5;
}
}
void NetUL::Learn(void)
{
int j;
unsigned i,k;
for(j=1;j<rang;j++)
// по слоям
{
if(Accreditation==0)
{
for(i=0;i<layers[j]>rang;i++)
{
// по нейронам
if(layers[j]>neurons[i].delta == 0)
continue;
for(k=0;k<layers[j]>neuronrang;k++)
// по
синапсам
{
layers[j]>neurons[i].synapses[k]+=Lear
nRate*
(layers[j-1]>neurons[k].axon
- layers[j]>neurons[i].synapses[k]);
}
}
}
else
{
for(i=0;i<layers[j]>rang;i++)
{
// по нейронам
50
if(Inhibition
//
заторможенные пропускаем
&& layers[j]>neurons[i].inhibitory>0)
continue;
for(k=0;k<layers[j]>neuronrang;k++)
// по
синапсам
{
layers[j]>neurons[i].synapses[k]+=Lear
nRate
*layers[j]>neurons[i].delta
*(layers[j-1]>neurons[k].axon
- layers[j]>neurons[i].synapses[k]);
}
}
}
}
}
void NetUL::Randomize(float
range)
{
for(unsigned i=0;i<rang;i++)
layers[i]>Randomize(range);
}
void NetUL::Equalize(void)
{
for(unsigned i=1;i<rang;i++)
layers[i]->Equalize();
}
void NetUL::Normalize(void)
{
for(unsigned i=1;i<rang;i++)
layers[i]->Normalize();
}
int
NetUL::SaveToFile(unsigned
char *file)
{
FILE *fp;
fp=fopen(file,"wt");
if(fp==NULL) return 1;
fprintf(fp,"%u",rang);
for(unsigned i=0;i<rang;i++)
{
fprintf(fp,"\n+%u",layers[i]>rang);
fprintf(fp,"\n¦%u",layers[i]>neuronrang);
for(unsigned
j=0;j<layers[i]->rang;j++)
{
fprintf(fp,"\n¦+%f",layers[i]
->neurons[j].state);
fprintf(fp,"\n¦¦%f",layers[i]
->neurons[j].axon);
fprintf(fp,"\n¦¦%f",layers[i]
->neurons[j].delta);
for(unsigned
k=0;k<layers[i]>neuronrang;k++)
{
fprintf(fp,"\n¦¦%f",
layers[i]>neurons[j].synapses[k]);
}
fprintf(fp,"\n¦+");
}
fprintf(fp,"\n+");
}
fclose(fp);
return 0;
}
int
NetUL::LoadFromFile(unsigned
char *file)
{
FILE *fp;
unsigned i,r,nr;
unsigned char bf[12];
fp=fopen(file,"rt");
if(fp==NULL) return 1;
fscanf(fp,"%u\n",&r);
if(r==0) goto allerr;
layers=new LayerUL _FAR
*[r];
if(layers==NULL)
{ allerr: status=ERROR;
fclose(fp); return 2; }
else
51
{
rang=r;
for(i=0;i<rang;i++)
layers[i]=NULL;
}
for(i=0;i<rang;i++)
{
fgets(bf,10,fp);
r=atoi(bf+1);
fgets(bf,10,fp);
nr=atoi(bf+1);
layers[i] = new
LayerUL(r,nr);
for(unsigned
j=0;j<layers[i]->rang;j++)
{
fscanf(fp,"¦+%f\n",&(layers[i
]->neurons[j].state));
fscanf(fp,"¦¦%f\n",&(layers[i
]->neurons[j].axon));
fscanf(fp,"¦¦%f\n",&(layers[i
]->neurons[j].delta));
for(unsigned
k=0;k<layers[i]>neuronrang;k++)
{
fscanf(fp,"¦¦%f\n",
&(layers[i]>neurons[j].synapses[k]));
}
fgets(bf,10,fp);
}
fgets(bf,10,fp);
}
fclose(fp);
return 0;
}
NetUL::NetUL(unsigned n,
unsigned n1, ...)
{
unsigned i, num, prenum;
va_list varlist;
status=OK; rang=0; pf=NULL;
learncycle=0;layers=NULL;
layers=new LayerUL _FAR
*[n];
if(layers==NULL) { allerr:
status=ERROR; }
else
{
rang=n;
for(i=0;i<rang;i++)
layers[i]=NULL;
num=n1;
layers[0] = new
LayerUL(num,0);
if(layers[0]>GetStatus()==ERROR)
status=ERROR;
va_start(varlist,n1);
for(i=1;i<rang;i++)
{
prenum=num;
num=va_arg(varlist,unsigned);
layers[i] = new
LayerUL(num,prenum);
if(layers[i]>GetStatus()==ERROR)
status=ERROR;
}
va_end(varlist);
}
}
int
NetUL::LoadNextPattern(float
_FAR *IN)
{
unsigned char buf[256];
unsigned char *s, *ps;
int i;
if(imgfile==1)
{
restart:
for(i=0;i<layers[0]>dy;i++)
{
if(fgets(buf,256,pf)==NULL)
{
if(learncycle)
{
rewind(pf);
learncycle--;
goto restart;
}
else return 2;
}
52
for(int j=0;j<layers[0]>dx;j++)
{
if(buf[j]=='x')
IN[i*layers[0]->dx+j]=0.5;
else if(buf[j]=='.')
IN[i*layers[0]->dx+j]=-0.5;
}
}
fgets(buf,256,pf);
return 0;
}
else if(imgfile==2 && emuf
!= NULL)
return (*emuf)(layers[0]>rang,IN,NULL);
else if(pf==NULL) return 1;
// разбор строки доверять
функции scanf нельзя
start:
if(fgets(buf,250,pf)==NULL)
{
if(learncycle)
{
rewind(pf);
learncycle--;
goto start;
}
else return 2;
}
s=buf;
for(;*s==' ';s++);
for(i=0;i<layers[0]>rang;i++)
{
ps=strchr(s,' ');
if(ps) *ps=0;
IN[i]=atof(s);
s=ps+1; for(;*s==' ';s++);
}
fgets(buf,256,pf);
return 0;
{
unsigned i,k;
for(i=0;i<dSigma;i++)
{
k=random(layers[0]->rang);
layers[0]->neurons[k].axon=
Change(layers[0]>neurons[k].axon);
}
}
void
NetUL::ConvexCombination(floa
t *In, float step)
{
float sq=1./sqrt(layers[0]>rang)-0.5;
if(step<0.) step=0.;
if(step>1.) step=1.;
for(int i=0;i<layers[0]>rang;i++)
In[i]=In[i]*step+sq*(1step);
}
void
NetUL::NormalizeSynapses(void
)
{
for(unsigned i=0;i<rang;i++)
layers[i]>NormalizeSynapses();
}
Листинг 3
// FILE neuman7.cpp for
neuro_mm.prj
#include <string.h>
#include <conio.h>
#include <stdlib.h>
#include <math.h>
#include <bios.h>
#include "neuro_mm.h"
//#define INHIBITION 2
}
// функция внесения помех
float NetUL::Change(float In)
{
return -In;
}
#pragma argsused
int GenFunction(int n, float
_FAR *in, float _FAR *ou)
{
static unsigned loop=0;
static int repeat=0;
int i;
void NetUL::AddNoise(void)
53
for(i=0;i<n;i++)
in[i]=(float)rand()/RAND_MAX0.5;
repeat++;
if(repeat==232)
{
repeat=0;
loop++;
srand(loop);
}
return 0;
}
main()
{
float Inp[30];
int count;
unsigned char buf[256];
float md=0.0;
int i;
NetUL N(2,2,100);
if(N.GetStatus()==ERROR)
{
printf("\nERROR: Net Can
not Be Constructed!");
return 1;
}
ClearScreen();
N.GetLayer(0)>SetName("Input");
N.GetLayer(0)>SetShowDim(1,1,2,1);
N.GetLayer(1)>SetName("Out");
N.GetLayer(1)>SetShowDim(17,1,10,10);
srand(2); // задаем
начальное условие для ГСЧ
SetSigmoidTypeUL(HYPERTAN);
SetDSigmaUL(2);
N.FullConnect();
N.Randomize(5);
N.NormalizeSynapses();
// N.Equalize(); //
использовать с
ConvexCombination
N.SetLearnCycle(64000U);
// используем гауссиан для
определения формы
// области обучения и
эффективности воздействия
SetAccreditationUL(1);
//
SetInhibitionUL(INHIBITION);
N.EmulatePatternFile(GenFunct
ion);
i=13;
for(count=0;;count++)
{
sprintf(buf," Cycle %u
",count);
out_str(1,23,buf,10 |
(1<<4));
sprintf(buf,"MD=%.1f ",md);
out_str(14,23,buf,10);
out_str(1,24," ESC breaks
",11 | (1<<4));
if(kbhit() || i==13)
i=getch();
if(i==27) break;
if(i=='s' || i=='S')
{
out_str(40,24,"FileConf:",7);
gotoxy(50,25);
gets(buf);
if(strlen(buf))
N.SaveToFile(buf);
break;
}
if(N.LoadNextPattern(Inp))
break;
// использовать вместе
NormalizeSynapses
// для сложных образов
//
N.NormalizeNetInputs(Inp);
if(count<3000)
md=SetMaxDistanceUL(7.0*(3000
-count)/3000+1);
else
SetMaxDistanceUL(0);
SetLearnRateUL(1);
if(count<3000)
54
SetLearnRateUL(0.1*(3000count)/3000+0.05);
else
SetLearnRateUL(0.1);
#define CBLACK 0
#define CGRAY
(CGREEN|CBLUE|CRED)
//
N.ConvexCombination(Inp,(floa
t)count/1000);
N.SetNetInputs(Inp);
// в случае ограниченного
тренировочного набора
// варьируем выборку данных
// N.AddNoise();
N.Propagate();
// если нажат Shift, ничего
не выводим
// для ускорения процесса
if(!(bioskey(2) & 0x03))
{
N.GetLayer(0)->DigiShow();
N.GetLayer(1)->DigiShow();
// состояние
N.GetLayer(1)>SetShowDim(50,1,10,10);
N.GetLayer(1)>TranslateAxons();
N.GetLayer(1)->Show(); //
текущая область обучения
N.GetLayer(1)>SetShowDim(17,1,10,10);
}
// N.NormalizeSynapses();
N.Learn();
}
N.ClosePatternFile();
return 0;
}
Листинг 4
// FILE colour.h
// background colours
#define CBLUE (1<<4)
#define CGREEN (1<<5)
#define CRED (1<<6)
#define CCYAN (CGREEN|CBLUE)
#define CYELLOW (CGREEN|CRED)
#define CMAGENTA (CBLUE|CRED)
55
С.Короткий
Нейронные сети Хопфилда и Хэмминга
Среди различных конфигураций искуственных нейронных сетей (НС)
встречаются такие, при классификации которых по принципу обучения, строго говоря,
не подходят ни обучение с учителем [1], ни обучение без учителя [2]. В таких сетях
весовые коэффициенты синапсов рассчитываются только однажды перед началом
функционирования сети на основе информации об обрабатываемых данных, и все
обучение сети сводится именно к этому расчету. С одной стороны, предъявление
априорной информации можно расценивать, как помощь учителя, но с другой – сеть
фактически просто запоминает образцы до того, как на ее вход поступают реальные
данные, и не может изменять свое поведение, поэтому говорить о звене обратной связи
с "миром" (учителем) не приходится. Из сетей с подобной логикой работы наиболее
известны сеть Хопфилда и сеть Хэмминга, которые обычно используются для
организации ассоциативной памяти. Далее речь пойдет именно о них.
Структурная схема сети Хопфилда приведена на рис.1. Она состоит из
единственного слоя нейронов, число которых является одновременно числом входов и
выходов сети. Каждый нейрон связан синапсами со всеми остальными нейронами, а
также имеет один входной синапс, через который осуществляется ввод сигнала.
Выходные сигналы, как обычно, образуются на аксонах.
Рис.1 Структурная схема сети Хопфилда
Задача, решаемая данной сетью в качестве ассоциативной памяти, как правило,
формулируется следующим образом. Известен некоторый набор двоичных сигналов
(изображений, звуковых оцифровок, прочих данных, описывающих некие объекты или
характеристики процессов), которые считаются образцовыми. Сеть должна уметь из
произвольного неидеального сигнала, поданного на ее вход, выделить ("вспомнить" по
частичной информации) соответствующий образец (если такой есть) или "дать
заключение" о том, что входные данные не соответствуют ни одному из образцов. В
общем случае, любой сигнал может быть описан вектором X = { xi: i=0...n-1}, n – число
56
нейронов в сети и размерность входных и выходных векторов. Каждый элемент xi
равен либо +1, либо -1. Обозначим вектор, описывающий k-ый образец, через Xk, а его
компоненты, соответственно, – xik, k=0...m-1, m – число образцов. Когда сеть
распознáет (или "вспомнит") какой-либо образец на основе предъявленных ей данных,
ее выходы будут содержать именно его, то есть Y = Xk, где Y – вектор выходных
значений сети: Y = { yi: i=0,...n-1}. В противном случае, выходной вектор не совпадет
ни с одним образцовым.
Если, например, сигналы представляют собой некие изображения, то, отобразив в
графическом виде данные с выхода сети, можно будет увидеть картинку, полностью
совпадающую с одной из образцовых (в случае успеха) или же "вольную
импровизацию" сети (в случае неудачи).
На стадии инициализации сети весовые коэффициенты синапсов устанавливаются
следующим образом [3][4]:
 m −1 k k
 xi x j , i ≠ j
(1)
wij = ∑
k =0
 0,
i= j
Здесь i и j – индексы, соответственно, предсинаптического и постсинаптического
нейронов; xik, xjk – i-ый и j-ый элементы вектора k-ого образца.
Алгоритм функционирования сети следующий (p – номер итерации):
1. На входы сети подается неизвестный сигнал. Фактически его ввод
осуществляется непосредственной установкой значений аксонов:
(2)
yi(0) = xi , i = 0...n-1,
поэтому обозначение на схеме сети входных синапсов в явном виде носит чисто
условный характер. Ноль в скобке справа от yi означает нулевую итерацию в цикле
работы сети.
2. Рассчитывается новое состояние нейронов
n −1
s j ( p + 1) = ∑ wij y i ( p) , j=0...n-1
(3)
и новые значения аксонов
y j ( p + 1) = f s j ( p + 1)
(4)
i=0
[
]
где f – активационная функция в виде скачка,
приведенная на рис.2а.
3. Проверка, изменились ли выходные
значения аксонов за последнюю итерацию. Если да –
переход к пункту 2, иначе (если выходы
застабилизировались) – конец. При этом выходной
вектор представляет собой образец, наилучшим
образом сочетающийся с входными данными.
Как говорилось выше, иногда сеть не может
Рис.2 Активационные
провести распознавание и выдает на выходе
функции
несуществующий образ. Это связано с проблемой
ограниченности возможностей сети. Для сети Хопфилда число запоминаемых образов
m не должно превышать величины, примерно равной 0.15•n. Кроме того, если два
образа А и Б сильно похожи, они, возможно, будут вызывать у сети перекрестные
ассоциации, то есть предъявление на входы сети вектора А приведет к появлению на ее
выходах вектора Б и наоборот.
57
Рис.3 Структурная схема сети Хэмминга
Когда нет необходимости, чтобы сеть в явном виде выдавала образец, то есть
достаточно, скажем, получать номер образца, ассоциативную память успешно
реализует сеть Хэмминга. Данная сеть характеризуется, по сравнению с сетью
Хопфилда, меньшими затратами на память и объемом вычислений, что становится
очевидным из ее структуры (рис. 3).
Сеть состоит из двух слоев. Первый и второй слои имеют по m нейронов, где m –
число образцов. Нейроны первого слоя имеют по n синапсов, соединенных со входами
сети (образующими фиктивный нулевой слой). Нейроны второго слоя связаны между
собой ингибиторными (отрицательными обратными) синаптическими связями.
Единственный синапс с положительной обратной связью для каждого нейрона
соединен с его же аксоном.
Идея работы сети состоит в нахождении расстояния Хэмминга от тестируемого
образа до всех образцов. Расстоянием Хэмминга называется число отличающихся
битов в двух бинарных векторах. Сеть должна выбрать образец с минимальным
расстоянием Хэмминга до неизвестного входного сигнала, в результате чего будет
активизирован только один выход сети, соответствующий этому образцу.
На стадии инициализации весовым коэффициентам первого слоя и порогу
активационной функции присваиваются следующие значения:
xk
(5)
wik = i , i=0...n-1, k=0...m-1
2
(6)
Tk = n / 2, k = 0...m-1
Здесь xik – i-ый элемент k-ого образца.
Весовые коэффициенты тормозящих синапсов во втором слое берут равными
некоторой величине 0 < ε < 1/m. Синапс нейрона, связанный с его же аксоном имеет вес
+1.
Алгоритм функционирования сети Хэмминга следующий:
58
1. На входы сети подается неизвестный вектор X = {xi:i=0...n-1}, исходя из
которого рассчитываются состояния нейронов первого слоя (верхний индекс в скобках
указывает номер слоя):
n −1
y (j1) = s (j1) = ∑ wij xi + T j , j=0...m-1
(7)
i=0
После этого полученными значениями инициализируются значения аксонов
второго слоя:
(8)
yj(2) = yj(1), j = 0...m-1
2. Вычислить новые состояния нейронов второго слоя:
m−1
s (j 2 ) ( p + 1) = y j ( p) − ε ∑ y k( 2 ) ( p), k ≠ j , j = 0... m − 1
(9)
и значения их аксонов:
y (j 2 ) ( p + 1) = f s (j2 ) ( p + 1) , j = 0... m − 1
(10)
k =0
[
]
Активационная функция f имеет вид порога (рис. 2б), причем величина F должна
быть достаточно большой, чтобы любые возможные значения аргумента не приводили
к насыщению.
3. Проверить, изменились ли выходы нейронов второго слоя за последнюю
итерацию. Если да – перейди к шагу 2. Иначе – конец.
Из оценки алгоритма видно, что роль первого слоя весьма условна:
воспользовавшись один раз на шаге 1 значениями его весовых коэффициентов, сеть
больше не обращается к нему, поэтому первый слой может быть вообще исключен из
сети (заменен на матрицу весовых коэффициентов), что и было сделано в ее
конкретной реализации, описанной ниже.
Программная модель сети Хэмминга строится на основе набора специальных
классов NeuronHN, LayerHN и NetHN – производных от классов, рассмотренных в
предыдущих статьях цикла [1][2]. Описания классов приведены в листинге 1.
Релизации всех функций находятся в файле NEURO_HN (листинг 2). Классы NeuronHN
и LayerHN наследуют большинство методов от базовых классов.
В классе NetHN определены следующие элементы:
Nin и Nout – соответственно размерность входного вектора с данными и число
образцов;
dx и dy – размеры входного образа по двум координатам (для случая трехмерных
образов необходимо добавить переменную dz), dx*dy должно быть равно Nin, эти
переменные используются функцией загрузки данных из файла LoadNextPattern;
DX и DY – размеры выходного слоя (влияют только на отображение выходого
слоя с помощью функции Show); обе пары размеров устанавливаются функцией
SetDxDy;
Class – массив с данными об образцах, заполняется функцией SetClasses, эта
функция выполняет общую инициализацию сети, сводящуюся к запоминанию
образцовых данных.
Метод Initialize проводит дополнительную инициализацию на уровне
тестируемых данных (шаг 1 алгоритма). Метод Cycle реализует шаг 2, а метод
IsConverged проверят, застабилизировались ли состояния нейронов (шаг 3).
Из глобальных функций – SetSigmoidAlfaHN позволяет установить параметр F
активационной функции, а SetLimitHN задает коэффициент, лежащий в пределах от
нуля до единицы и определяющий долю величины 1/m, образующую ε.
59
На листинге 3 приведена тестовая программа для проверки сети. Здесь
конструируется сеть со вторым слоем из пяти нейронов, выполняющая распознавание
пяти входных образов, которые представляют собой схематичные изображения букв
размером 5 на 6 точек (см.рис.4а). Обучение сети фактически сводится к загрузке и
запоминанию идеальных изображений, записанных в файле "charh.img", приведенном
на листинге 4. Затем на ее вход поочередно подаются зашумленные на 8/30 образы
(см.рис.4б) из файла "charhh.img" с листинга 5, которые она успешно различает.
Рис. 4 Образцовые и тестовые образы
Рис.5 Структурная схема ДАП
В проект кроме файлов NEURO_HN и NEUROHAM входят также SUBFUN и
NEURO_FF, описанные в [1]. Программа тестировалась в среде Borland C++ 3.1.
Предложенные классы позволяют моделировать и более крупные сети Хэмминга.
Увеличение числа и сложности распознаваемых образов ограничивается фактически
только объемом ОЗУ. Следует отметить, что обучение сети Хэмминга представляет
60
самый простой алгоритм из всех рассмотренных до настоящего времени алгоритмов в
этом цикле статей.
Обсуждение сетей, реализующих ассоциативную память, было бы неполным без
хотя бы краткого упоминания о двунаправленной ассоциативной памяти (ДАП). Она
является логичным развитием парадигмы сети Хопфилда, к которой для этого
достаточно добавить второй слой. Структура ДАП представлена на рис.5. Сеть
способна запоминать пары ассоциированных друг с другом образов. Пусть пары
образов записываются в виде векторов Xk = {xik:i=0...n-1} и Yk = {yjk: j=0...m-1}, k=0...r1, где r – число пар. Подача на вход первого слоя некоторого вектора P = {pi:i=0...n-1}
вызывает образование на входе второго слоя некоего другого вектора Q = {qj:j=0...m-1},
который затем снова поступает на вход первого слоя. При каждом таком цикле вектора
на выходах обоих слоев приближаются к паре образцовых векторов, первый из
которых – X – наиболее походит на P, который был подан на вход сети в самом начале,
а второй – Y – ассоциирован с ним. Ассоциации между векторами кодируются в
весовой матрице W(1) первого слоя. Весовая матрица второго слоя W(2) равна
транспонированной первой (W(1))T. Процесс обучения, также как и в случае сети
Хопфилда, заключается в предварительном расчете элементов матрицы W (и
соответственно WT) по формуле:
(11)
wij = ∑ xi y j , i = 0... n − 1, j = 0... m − 1
k
Эта формула является развернутой записью матричного уравнения
W = ∑ XTY
(12)
k
для частного случая, когда образы записаны в виде векторов, при этом
произведение двух матриц размером соответственно [n*1] и [1*m] приводит к (11).
В заключении можно сделать следующее обобщение. Сети Хопфилда, Хэмминга
и ДАП позволяют просто и эффективно разрешить задачу воссоздания образов по
неполной и искаженной информации. Невысокая емкость сетей (число запоминаемых
образов) объясняется тем, что, сети не просто запоминают образы, а позволяют
проводить их обощение, например, с помощью сети Хэмминга возможна
классификация по критерию максимального правдоподобия [3]. Вместе с тем, легкость
построения программных и аппаратных моделей делают эти сети привлекательными
для многих применений.
Литература
1. С. Короткий, Нейронные сети: алгоритм обратного распространения.
2. С. Короткий, Нейронные сети: обучение без учителя.
3. Artificial Neural Networks: Concepts and Theory, IEEE Computer Society Press, 1992.
4. Ф.Уоссермен, Нейрокомпьютерная техника, М.,Мир, 1992.
61
Листинг 1
// FILE neuro_hn.h
#include "neuro.h"
// Hamming Net
class LayerHN;
class NetHN;
class NeuronHN: public Neuron
{
friend LayerHN;
friend NetHN;
public:
virtual float Sigmoid(void);
};
class LayerHN: public LayerFF
{
friend NetHN;
NeuronHN _FAR *neurons;
public:
LayerHN(unsigned nRang);
~LayerHN();
void PrintSynapses(int,int){};
void PrintAxons(int x, int y){};
};
class NetHN: public SomeNet
{
LayerHN _FAR *layers;
int Nin, Nout;
int dx, dy, DX, DY;
float _FAR * Class; // [Nout]x[Nin] {+1;-1}
unsigned char *name; // сети можно дать имя
public:
NetHN(int N, int M)
{
layers = new LayerHN(M); Nin=N; Nout=M; name=NULL;
};
~NetHN()
{
if(layers) delete layers; Nin=0; Nout=0; layers=NULL;
};
LayerHN _FAR *GetLayer(void){return layers;};
void SetClasses(float _FAR * ps) {Class=ps;};
void Initialize(float _FAR *In);
void Cycle(void);
int IsConverged(void);
int LoadNextPattern(float _FAR *In);
void SetDxDy(int x, int y, int _dx, int _dy)
{if(x*y==Nin) {dx=x; dy=y;} DX=_dx; DY=_dy;};
void SetName(unsigned char *s) {name=s;};
void Show(void);
62
void PrintAxons(int x, int y, int direction);
};
float SetSigmoidAlfaHN(float Al);
float SetLimitHN(float Al);
Листинг 2
// FILE neuro_hn.cpp FOR neuro_hn.prj
#include <math.h>
#include <string.h>
#include <stdlib.h>
#include "neuro_hn.h"
static int SigmoidType=THRESHOLD;
static float SigmoidAlfa=1.; // величина порога
static float Limit=0.9;
// eps=Limit*(1/Nout)
float SetSigmoidAlfaHN(float Al)
{
float a;
a=SigmoidAlfa;
SigmoidAlfa=fabs(Al);
if(SigmoidAlfa<0.01) SigmoidAlfa=0.01;
return a;
}
float SetLimitHN(float Al)
{
float a;
a=Limit;
Limit=fabs(Al);
if(Limit>=1.) Limit=0.98;
return a;
}
float NeuronHN::Sigmoid(void)
{
switch(SigmoidType)
{
case THRESHOLD:
if(state>SigmoidAlfa) return SigmoidAlfa;
else if(state<0) return 0;
else return state;
default:
return state;
}
}
LayerHN::LayerHN(unsigned nRang)
{
status=ERROR;
if(nRang==0) return;
neurons=new NeuronHN[nRang];
if(neurons==NULL) return;
63
rang=nRang;
status=OK;
}
LayerHN::~LayerHN(void)
{
if(neurons) delete [] neurons;
neurons=NULL;
}
void NetHN::Initialize(float _FAR *In)
{
float sum;
for(unsigned i=0;i<Nout;i++) // по классам
{
sum=0.;
// расчет (7) с подстановкой (5) и (6)
for(unsigned j=0;j<Nin;j++)
sum+=Class[i*Nin+j] * In[j]; // число совпадений...
// минус число ошибок
// C1=(Nin-sum)/2
- число ошибок
// C2=Nin-C1;
- число совпадений
sum=(Nin+sum)/2; // sum = C2(C1) - число совпадений
layers->neurons[i].state=sum;
layers->neurons[i].axon=
layers->neurons[i].Sigmoid();
}
}
void NetHN::Cycle(void)
{
float sum;
for(unsigned i=0;i<Nout;i++)
{
sum=0.;
for(unsigned j=0;j<Nout;j++)
if(i!=j) sum+=layers->neurons[j].axon;
sum*=((1./Nout)*Limit);
layers->neurons[i].state=
layers->neurons[i].axon-sum;
layers->neurons[i].state= // рассчитываем значения
layers->neurons[i].Sigmoid(); // аксонов, но...
}
for(i=0;i<Nout;i++)
layers->neurons[i].axon=
layers->neurons[i].state; // ...обновляем их здесь
}
int NetHN::IsConverged(void)
{
int sum=0;
for(unsigned i=0;i<Nout;i++)
{
if(layers->neurons[i].axon>0.) sum++;
64
}
if(sum==1) return 1;
else return 0;
}
int NetHN::LoadNextPattern(float _FAR *IN)
{
unsigned char buf[256];
unsigned char *s, *ps;
int i;
if(pf==NULL) return 1;
if(imgfile) // данные расположены двумерно
{
for(i=0;i<dy;i++)
{
if(fgets(buf,256,pf)==NULL) return 2;
for(int j=0;j<dx;j++)
{
if(buf[j]=='x'||buf[j]=='1') IN[i*dx+j]=1.;
else IN[i*dx+j]=-1.;
}
}
if(fgets(buf,256,pf)==NULL) return 2;
return 0;
}
// данные в виде строки: 1 символ - 1 элемент
if(fgets(buf,250,pf)==NULL) return 2;
for(i=0;i<Nin;i++)
{
if(buf[i]=='0') IN[i]=-1.;
else
IN[i]=1.;
}
return 0;
}
void NetHN::Show(void)
{
unsigned char sym[5]={ GRAFCHAR_EMPTYBLACK, GRAFCHAR_DARKGRAY,
GRAFCHAR_MIDDLEGRAY, GRAFCHAR_LIGHTGRAY, GRAFCHAR_SOLIDWHITE
};
int i,j,k;
float fmax=0.0;
if(name) out_str(0,0,name,3);
out_char(0,1,GRAFCHAR_UPPERLEFTCORNER,15);
for(i=0;i<2*DX;i++)
out_char(1+i,1,GRAFCHAR_HORIZONTALLINE,15);
out_char(1+i,1,GRAFCHAR_UPPERRIGHTCORNER,15);
for(j=0;j<DY;j++)
for(i=0;i<DX;i++)
if(layers->neurons[j*DX+i].axon>fmax)
fmax=layers->neurons[j*DX+i].axon;
65
for(j=0;j<DY;j++)
{
out_char(0,2+j,GRAFCHAR_VERTICALLINE,15);
for(i=0;i<2*DX;i++)
{
if(fmax)
{
k=(int)(((layers->neurons[j*DX+i/2].axon)
/fmax)*5.);
}
else k=0;
if(k<0) k=0;
if(k>=5) k=4;
out_char(1+i, 2+j, sym[k], 15);
}
out_char(1+i, 2+j,GRAFCHAR_VERTICALLINE,15);
}
out_char(0,j+2,GRAFCHAR_BOTTOMLEFTCORNER,15);
for(i=0;i<2*DX;i++)
out_char(i+1,j+2,GRAFCHAR_HORIZONTALLINE,15);
out_char(1+i,j+2,GRAFCHAR_BOTTOMRIGHTCORNER,15);
}
void NetHN::PrintAxons(int x, int y, int direction)
{
unsigned char buf[20];
for(unsigned i=0;i<Nout;i++)
{
sprintf(buf,"%+7.2f ",layers->neurons[i].axon);
out_str(x+8*i*direction,y+i*(!direction),buf,11);
}
}
Листинг 3
// FILE neuroham.cpp FOR neuro_hn.prj
#include <conio.h>
#include <bios.h>
#include "neuro_hn.h"
#define INS 30
#define OUTS 5
#define TEST 5
// число элементов во входных данных
// число выходов (образцов)
// число тестовых (зашумленных) образов
main()
{
int i,j,k=13;
unsigned char buf[20];
float _FAR *In;
float _FAR * cl;
In = new float [INS]; // массив для ввода данных
cl = new float [OUTS*INS]; // хранилище образцов
66
NetHN Hn(INS,OUTS);
// создание сети
SetLimitHN(0.5);
SetSigmoidAlfaHN(INS); // установка размера порога
Hn.SetDxDy(5,6,OUTS,1); // входные вектора - [5*6]
Hn.OpenPatternFile("charh.img");
for(i=0;i<OUTS;i++)
// загрузка образцов
{
Hn.LoadNextPattern(&cl[i*INS]);
}
Hn.SetClasses(cl);
// инициализация весов
Hn.ClosePatternFile();
ClearScreen();
Hn.SetName("Hamming");
Hn.OpenPatternFile("charhh.img");
for(i=0;i<TEST;i++)
// цикл по тестируемым образам
{
sprintf(buf,"pattern %d
",i);
out_str(0,10,buf,15);
Hn.LoadNextPattern(In); // загрузка
Hn.Initialize(In);
// инициализация входов сети
for(j=0;;j++)
{
sprintf(buf,"cycle %d
",j);
out_str(0,11,buf,15);
Hn.Cycle();
Hn.Show();
Hn.PrintAxons(30,0,VERTICAL);
if(kbhit() || k==13) k=getch();
if(k==27) break; // ESC - безусловный выход
// нажатие ENTER приведет к пошаговому просмотру,
// любая другая клавиша задает непрерывное
// выполнение итераций цикла вплоть до момента...
if(Hn.IsConverged())
{
// ...когда сеть застабилизируется
out_str(0,24,"Converged",15);
k=getch();
out_str(0,24,"
",0);
break;
}
}
}
end:
Hn.ClosePatternFile();
delete cl;
delete In;
return 0;
}
Листинг 4
Файл charh.img
67
..x..
.x.x.
.x.x.
x...x
xxxxx
x...x
A.........
x...x
xx.xx
xx.xx
x.x.x
x.x.x
x...x
.M........
x...x
x...x
xxxxx
x...x
x...x
x...x
..H.......
.xxx.
x...x
x....
x....
x...x
.xxx.
......C...
xxxxx
..x..
..x..
..x..
..x..
..x..
.......T..
Листинг 5
Файл charhh.img
x.x..
..xx.
xx.x.
....x
x.xx.
x.x.x
A.........
x.x..
.x.xx
x..xx
x.x.x
xxx.x
xx.x.
.M........
68
x.x.x
x..x.
xxx..
xx..x
..x.x
x...x
..H.......
.x.x.
x.x..
.x...
x....
x.xxx
.xx..
......C...
xx..x
...x.
..x.x
..x..
x....
..x.x
.......T..
69
Download