-
Notifications
You must be signed in to change notification settings - Fork 2
/
Диплом.tex
1753 lines (1539 loc) · 119 KB
/
Диплом.tex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
\documentclass[utf8,a5paper,portrait,10pt]{eskdtext}
\usepackage{eskdplain}
\usepackage[T2A]{fontenc}
\usepackage{pscyr}
\usepackage{pstricks}
\usepackage{tikz}
\renewcommand{\ESKDtitleFontI}{\ESKDfontIIsize}
\renewcommand{\ESKDtitleFontIII}{\fontsize{8pt}{8pt}}
\renewcommand{\ESKDtitleFontIV}{\fontsize{14pt}{14pt}}
\renewcommand{\ESKDtitleFontVIII}{\fontsize{12pt}{12pt}}
\renewcommand{\ESKDtitleFontX}{\ESKDfontII\upshape}
\setcounter{tocdepth}{2}
\begin{document}
\ESKDdepartment{Санкт-Петербургский Государственный Политехнический
Университет}
\ESKDcompany{Факультет Технической Кибернетики}
\renewcommand{\ESKDapprovingName}{Работа допущена к защите}
\ESKDtitleApprovedBy{Зав. кафедрой}{И. Г. Черноруцкий}
\ESKDtitle{Дипломная работа}
\ESKDdocName{Тема «Использование генетических алгоритмов
для автоматизированного написания программ»}
\renewcommand{\ESKDtheTitleFieldVIII}{
\begin{flushleft}
Направление: 230100 — Информатика и вычислительная техника.\\
Специальность: 230105 — Программное обеспечение вычислительной техники
и автоматизированных систем.
\vspace{10mm}
Исполнитель: \hfill Ситник Андрей Андреевич \\
Научный руководитель: \hfill Амосов Владимир Владимирович \\
Рецензент: \hfill \\
\end{flushleft}}
\renewcommand{\ESKDtheTitleFieldX}{Санкт-Петербург 2001}
\maketitle
\tableofcontents
\newpage
\section{Актуальность темы}
\subsection{Авто-написание программы}
Для того, чтобы программа была автоматически написана, разработчик должен
описать требования к ней в виде оценочной функции. К сожалению, для большинства
бытовых задач, написание подходящей оценочной функции сложнее, чем
разработка самой программы.
Но есть области информационных технологий, где создание программы невероятно
сложно, а описание требований к ней — относительно простое:
\begin{itemize}
\item Программирование сложных систем, состоящих из автономных объектов
взаимодействующих друг с другом. Например, группировка простых
спутников. Такие сложные системы очень надёжны (выход одного элемента
лишь немного уменьшает мощность множества) и эффективны (из-за
параллельной работы).
\item Поиск оптимальных стратегий для теории игр, макроэкономики и
эволюционной биологии.\cite{communication}
\end{itemize}
Поиск решений для данных задач довольно трудоёмок и сейчас слишком похож на
метод проб и ошибок. Например, чтобы найти оптимальную стратегию в Повторяющейся
дилемме заключённого, её автор, Роберт Аксельрод, просто пригласил знакомых
коллег со всего мира предложить своё решение и выбрал лучшее.\cite{dilemma}
Автоматическая разработка программ способна заменить человека в этом сложном,
долгом и не интересном поиске.
Поскольку сложные системы, теория игр, макроэкономика и эволюционная биология
являются очень востребованными областями науки, то и инструмент для них будет
очень актуален.
\subsection{Саморазвитие программы}
В данный момент изменения в алгоритм программы вносит её разработчик. Тогда,
когда это экономически неоправданно или невозможно, алгоритм делают зависимым
от констант и конечные пользователи или автоматика меняют эти настройки.
Однако, параметры статически заданного алгоритма не могут дать полную гибкость,
что бывает нередко нужно. Например, спамеры постоянно экспериментируют и нередко
обходят защиту спам-фильтров, в итоге программистам снова приходится
анализировать новые методы массовой рекламы и переписывать алгоритмы защиты.
Саморазвитие программ будет очень актуально для:
\begin{itemize}
\item Защиты от спама, чтобы автоматически подстраиваться под новые уловки,
когда пользователь отмечает письмо как рекламное.
\item Алгоритмов противника в компьютерных играх, чтобы подстраиваться под
способности и навыки игрока.
\item Автоматических роботов, чтобы изменять программу управления при
поломках и других непредвиденных ситуациях.\cite{damage} Это особенно
полезно в областях, где человек не может прийти ему на помощь, а связь
затрудненна, например, в космосе.
\end{itemize}
\newpage
\section{Постановка задачи}
Разработчик формулирует требование к алгоритму $a$ в виде оценочной
функции $T$, так чтобы более подходящий для задачи алгоритм давал больший
результат $T(a)$. Разработчик также предоставляет функцию останова $F$, такую,
что $F(T(a))$ вернёт 1, если алгоритм $a$ полностью подходит и цель разработки
достигнута.
Цель данной работы, предоставить инструментарий для разработчика, реализующий
операцию $G(T, F) = a$, которая генерирует алгоритм $a$, получающий максимальное
значение в оценочной функции $T$ и удовлетворяющий критерию останова $F$.
Инструментарий будет использоваться для изучения генетических алгоритмов, так
что важно, чтобы итоговый алгоритм был понятен человеку и весь процесс его
генерации можно было наглядно представить и проанализировать.
\newpage
\section{Генетические алгоритмы}
\begin{figure}
\centering
\input{./images/evolution.tex}
\caption{Генетический алгоритм}
\end{figure}
Как мы видим, задача сводится к оптимизации функции $T(a)$. Однако характер этой
функции неизвестен. Для подобных задач оптимизации весьма эффективны
генетические алгоритмы.\cite{reinforcement}
Генетический алгоритм $G_{ga}$ работает сразу с множеством кандидатов на решение
(популяцией) $A = [a_1, a_2, …, a_n]$. Это итерационный процесс и каждый шаг
$A_i = G_{ga}(A_{i-1})$ немного улучшает оценочную функцию (за значение
оценочной функции популяции, принимается оценочная функция лучшего члена
популяции).
Каждая итерация $G_{ga}(A) = S(Mi(Mu(R(A))))$ состоит из\\
операций:\cite{reinforcement}
\begin{enumerate}
\item Размножения $R(A)$ — увеличения популяции простым копированием.
Поскольку последующий отбор уменьшит популяцию, размножение поддерживает
её размер на каждом шаге.
\item Мутации $Mu(A)$ — случайного изменения элементов.
\item Смешивания $Mi(A)$ — объединения двух элементов со случайным выбором
частей из каждой половины.
\item Отбора $S(A)$ — удаление наименее подходящих кандидатов: для каждого
элемента популяции вычисляется оценочная функция и элементы с
наименьшим её значением изымаются из множества.
\end{enumerate}
В конце каждой итерации с помощью функции останова $F(A)$, которую задаёт
разработчик, определяется нужно ли остановить цикл.
\section{Анализ проблемы}
В данным момент, при создании программы, разработчики представляют алгоритм в
виде иерархического дерева команд на одном из множества языков программирования.
Компилятор транслирует исходный текст в машинный код — список команд с условными
переходами.
К сожалению, необходимые для генетического алгоритма мутация и смешивание очень
трудоёмко реализовать для типа данных в виде дерева или списка с переходами.
Поскольку современные языки программирования и машинных код — это не
единственный способ представить алгоритм, то выгоднее представить его в другом,
более эффективном для задачи виде.
\newpage
\section{Обзор имеющихся решений}
\subsection{Нейронная сеть}
В исследованиях эволюции\cite{communication} довольно часто поведение объекта
кодируют с помощью нейронной сети:\cite{neural}
\begin{itemize}
\item Обработка происходит в множестве простых элементов, нейронов.
\item Сигналы передаются между нейронами через связи.
\item Каждая связь характеризуется весом, в зависимости от знака которой
передаваемый сигнал увеличивается или уменьшается.
\item Каждому нейрону соответствует активационная функция, которая определяет
зависимость выходного сигнала от комбинации входящих сигналов.
\end{itemize}
Несмотря на то, что элементы нейронной сети имеют очень ограниченные
вычислительные способности, вся сеть в целом, объединяя большое число таких
элементов, оказывается способной выполнять весьма сложные задачи.
Нейронная сеть представляется в виде матрицы весов $X$, так что элемент
$x_{ij}$ соответствует весу связи от нейрона $i$ к нейрону $j$. Мутация
вносит случайные изменения в веса и, с небольшой вероятностью, добавляет или
удаляет новый нейрон. Смешивание либо усредняет веса между двумя матрицами, либо
случайным образом выбирает ячейки из матриц-«родителей».
К сожалению, человек не может понять алгоритм нейронной сети при беглом взгляде
на матрицу или граф, что требуется в поставленной задаче.
\subsection{Конечные автоматы}
В рамках развития идей А. А. Шалыто о автоматном программировании
рассматривается генерация конечного автомата с помощью генетических
алгоритмов.\cite{shalyto}
Конечный автомат характеризуется множеством состояний и матрицей переходов $X$,
где $x_{ji}$ содержит дискретный код условия при котором автомат перейдёт из
состояний $i$ в состояние $j$.
Матрица состояний преобразуется в битовую строку, с которой генетический
алгоритм и работает: мутация случайно изменяет биты, смешивание объединяет две
строки в одну, случайно выбрав символы из каждой.
К сожалению, для поставленной задачи автоматы имеют ряд недостатков:
\begin{itemize}
\item Они не полны по Тьюрингу, что несколько ограничивает их гибкость.
\item Чтобы быть понятными разработчику их надо представить в виде графа, а
работа с графикой сложнее, чем с обычным текстом.
\end{itemize}
\newpage
\section{Обзор языка $D^2NA$}
\subsection{Анализ}
Поскольку целью работы является создания понятного и удобного представления
алгоритма для человека, то за основу возьмём современный высокоуровневый язык
программирования.
Первая проблема, которая усложняет случайные изменения в алгоритме —
иерархичность. Уберём её, представив программу в виде набора
условных операторов (\texttt{if}) внутри одного большого постоянного цикла
(\texttt{while}):
\begin{verbatim}
while (true) {
if (…) {
…
}
if (…) {
…
}
}
\end{verbatim}
Будем создавать \texttt{if}-оператор для каждого возможного условия. Чтобы
условных операторов было слишком много, ограничим возможные условия проверки:
\begin{itemize}
\item поступление внешнего сигнала;
\item переменная имеет значение больше нуля;
\end{itemize}
\newpage
\begin{verbatim}
while (true) {
if (on_siganl('Input')) {
…
}
if (on_siganl('Input') && a > 0) {
…
}
if (a > 0) {
…
}
}
\end{verbatim}
Чтобы упростить внесение случайных изменений, уменьшим количество возможных
команд:
\begin{itemize}
\item увеличение переменой на единицу;
\item уменьшение переменой на единицу;
\item отправка исходящего сигнала;
\end{itemize}
\begin{verbatim}
while (true) {
if (on_siganl('Input')) {
a += 1;
}
if (on_siganl('Input') && a > 0) {
send('Output');
a -= 1;
a -= 1;
}
if (a > 0) { }
}
\end{verbatim}
\subsection{Описания языка}
В рамках данной работы был создан особый язык программирования $D^2NA$.
Программа на $D^2NA$ с внешним миром общается с помощью входящих и исходящих
сигналов. Они характеризуются только именем и не имеют параметров и
продолжительности. Количество и имена сигналов напрямую зависят от задачи и
задаются разработчиком. Имя сигнала должно начинаться с двоеточия и заглавной
буквы и может состоять из букв, цифр и знака подчёркивания (например,
\texttt{:Input}).
Существует особый сигнал \texttt{:Init}, которые посылается в программу при её
запуске. Он является своего рода конструктором.
Программа может хранить внутреннее состояние в знаковых целочисленных
переменных. Генератор может автоматически добавлять или удалять их. Имя
переменной должно начинаться с двоеточия и прописной буквы и может состоять из
букв, цифр и знака подчёркивания (например, \texttt{:a}).
Программа состоит из списка правил. Правило характеризуется условием выполнения
и списком команд (правило можно представить в виде оператора if). Условие может
быть двух видов: поступление определённого сигнала и значение определённой
переменной больше 0. Поскольку имена сигналов и переменных отличаются и для
них возможна только одна проверка, то условие записывается как \texttt{on} с
последующими именами сигналов или переменных, разделёнными запятыми (например,
\texttt{on~:Input, :a}).
Команды в правиле будут выполнены только если все проверки условия будут
истинными. Команды записываются между \texttt{do} и \texttt{end} и могут быть
трёх видов: послать исходящий сигнал (\texttt{send X}), увеличить
(\texttt{up~x}) или уменьшить (\texttt{down x}) значение переменной.
\subsection{Пример программы на $D^2NA$}
\begin{verbatim}
on :Init do
up :ping
end
on :Print, :ping do
send :Ping
down :ping
up :pong
end
on :Print, :pong do
send :Pong
down :pong
up :ping
end
\end{verbatim}
Данная простая программа посылает поочерёдно \texttt{:Ping} или \texttt{:Pong}
при каждом входящем сигнале \texttt{:Print}. Чтобы помнить, какой сигнал нужно
послать следующим используются две переменные \texttt{:ping} и \texttt{:pong}.
При запуске программы (при системном сигнале \texttt{:Init}) переменная
\texttt{:ping} увеличивается до значения 1. Далее, когда пришёл сигнал
\texttt{:Print} и значение \texttt{:ping} больше нуля, посылается сигнал
\texttt{:Ping}, \texttt{:ping} уменьшается до 0, а \texttt{:pong}, наоборот,
увеличивается до 1.
При сигнале \texttt{:Print} и положительном значении \texttt{:pong}, посылается
сигнал \texttt{:Pong} и происходит обратная процедура.
\subsection{Генерация программы на $D^2NA$}
Программа на $D^2NA$ представляет из себя просто список списков, которые
генетическому алгоритму удобно смешивать и переставлять.
Мутация добавляет или удаляет случайную команду в случайном списке, а также с
небольшой вероятностью добавляет или удаляет переменную. Количество возможных
команд из-за их простоты небольшое. Правила генерируются для всех возможных
условий, которых не так много из-за небольшого количества проверок.
Смешивание просто объединяет переменные и случайно выбирая половину правил из
«родителей», формирует новую программу.
Код программы $D^2NA$ представляется из себя обычный текст и очень похож на
современные языки программирования. Его легко понять понять разработчику,
и к нему легко применять множество инструментов для анализа текста (например,
смотреть различия с помощью UNIX-программы \texttt{diff}).
\newpage
\section{Архитектура генератора}
\begin{figure}
\centering
\input{./images/architecture.tex}
\caption{Архитектура генератора}
\end{figure}
Инструментарий для $D^2NA$ состоит из двух пакетов:
\begin{itemize}
\item виртуальной машины \texttt{d2na/vm} для запуска уже готовых программ на
$D^2NA$;
\item генератора \texttt{d2na/evolution}, который с помощью генетических
алгоритмов создаёт новую программу на $D^2NA$;
\end{itemize}
Такой ход позволит упростить разделение разработки программы и её использования.
Например, на сервер, где программа на $D^2NA$ будет выполняться можно поставить
только виртуальную машину, а на компьютер разработчика, который будет задавать
оценочную функцию и следить за ходом генерации, нужно поставить и генератор и
виртуальную машину.
\subsection{Виртуальная машина}
Виртуальная машина \texttt{d2na-vm} выполняет код на языке $D^2NA$ и позволяет
ему общаться с внешним миром с помощью входящих/исходящих сигналов.
Код на языке $D^2NA$ можно выполнять двумя способами:
\begin{itemize}
\item независимой программой, передав ей файл с $D^2NA$-кодом;
\item библиотекой для запуска $D^2NA$-кода внутри другого приложения на языке
программирования Ruby;
\end{itemize}
\subsubsection{Программа интерпретатора}
Программа интерпретатора предназначена для работы с $D^2NA$-кодом человеком или
другой программой, написанной не на языке программирования Ruby.
Управлением программой \texttt{d2na-vm} сделано согласно рекомендация для
UNIX:
\begin{itemize}
\item Программа предназначена для запуска их текстового терминала и общается
с внешним миром простым текстом.
\item При запуске можно указать аргументы. Они могут быть либо однобуквенные,
и перед ними должен быть дефис (например, \texttt{-h}), или полным
названием команды с двумя дефиса вначале (например, \texttt{-help}).
\end{itemize}
\newpage
Чтобы вызвать интерпретатор, нужно в текстовом термина набрать команду вида:
\texttt{d2na-vm} опции файл
Опции могут быть:
\begin{itemize}
\item \texttt{-p} или \texttt{--prompt} — показывать перед исходящими
сигналами \texttt{>}, а перед входящими \texttt{<}. Это упрощает работу
с программой оператору человеку.
\item \texttt{-c} или \texttt{--color} — показывать перед исходящими
сигналами красную стрелку \texttt{>}, а перед входящими зелёную стрелку
\texttt{<}. Это упрощает работу с программой оператору человеку, но
требует поддержки цветного вывода у текстового терминала.
\item \texttt{-h} или \texttt{--help} — вывести информацию о опциях и способе
работы с интерпретатором.
\end{itemize}
После запуска интерпретатор загружает переданный ему файл с $D^2NA$-кодом и
сам посылает ему стартовый входящих сигнал \texttt{:Init}. Если загруженная
$D^2NA$-программа посылает исходящий сигнал, он выводит его в стандартный выход
(\texttt{STDOUT}). Исходящие сигналы разделяются переводом строки.
После загрузки интерпретатор ожидает входящих сигналов со стандартного входа
(\texttt{STDIN}), которые должны оканчиваться переводом строки.
Если входящий сигнал начинается с прописной буквы, то интерпретатор сам
делает первую букву заглавной. При поступлении новых входящих сигналов,
\texttt{d2na-vm} отправляет их загруженной программе на $D^2NA$ и выводи её
ответные исходящие сигналы.
Программа перестаёт работать получив со стандартного входа (\texttt{STDIN}) код
\texttt{ETX} об окончании текста. Его можно ввести нажав \texttt{Ctrl+C}.
\newpage
Рассмотрим пример работы с виртуальной машиной человеком:
\begin{verbatim}
~$ d2na-vm -p ping-pong.d2na
< Print
> Ping
< Print
> Pong
< Print
> Ping
< ^C
~$
\end{verbatim}
С помощью виртуальной машины код на $D^2NA$ может вызывать приложение на любом
языке и платформе. Её надо лишь запустить программу \texttt{d2na-vm}, передав ей
путь к файлу с $D^2NA$-кодом, и передавать/принимать сигналы через текстовый
канал (\texttt{pipe}):
\begin{verbatim}
IO.popen('d2na-vm ping-pong.d2na') do |io|
io << 'Print'
io.read #=> "Ping\n"
io << 'Print'
io.read #=> "Pong\n"
io << 'Print'
io.read #=> "Ping\n"
end
\end{verbatim}
\newpage
\subsubsection{Библиотека интерпретации}
Хоть программа-интерпретатор и позволяет работать с $D^2NA$-кодом и человеку,
и любой другой программе, но запуск отдельного процесса и взаимодействие с ним
через канал (\texttt{pipe}) — не самый эффективный способ. Тем более, если
$D^2NA$-код должен быть запущен из приложения на языке программирования Ruby,
то интерпретатор загрузит ещё одну виртуальную машину Ruby, что не имеет смысла.
Поскольку при генератор будет очень часто запускать $D^2NA$-код, то было решено
сделать более быстрый способ запуска $D^2NA$-программ из приложений на языке
программирования Ruby.
Для этого был сделан Ruby-класс \texttt{D2NA::Code} с методами:
\begin{itemize}
\item \texttt{new(\&block)} — загружает $D2NA$-код переданный в переменной
\texttt{block}.
\item \texttt{{<}< singal} — послать входящий сигнал из переменной
\texttt{signal}.
\item \texttt{listen(*singals, \&block)} — код в переменной \texttt{block} будет
вызван, когда загруженный код пошлёт исходящий сигнал, содержащийся в
массиве \texttt{singals}. Если массив \texttt{singals} пустой, то код из
\texttt{block} будет вызываться при любом исходящем сигнале.
\item \texttt{delete\_listeners!} — удаляет все коды, добавленные через метод
\texttt{listen}.
\item \texttt{reset!} — обнуляет все переменные загруженного кода
(переводит его в начальное состояние).
\end{itemize}
При получении первого входящего сигнала библиотека сама отправит загруженному
коду стартовый сигнал \texttt{:Init}. После вызова метода \texttt{reset!} и
обнуления переменных стартовый сигнал \texttt{:Init} будет снова послан
автоматически.
Пример работы с библиотекой:
\begin{verbatim}
require 'd2na/vm'
code = D2NA::Code.new do
on :Init do
up :ping
end
on :Print, :ping do
send :Ping
down :ping
up :pong
end
on :Print, :pong do
send :Pong
down :pong
up :ping
end
end
code.listen { |signal| puts signal }
code << :Print # Выведет Ping
code << :Print # Выведет Pong
code << :Print # Выведет Ping
code.reset!
code.listen { |signal| puts signal }
code << :Print # Выведет Ping
\end{verbatim}
Очень часто программа работающая с $D^2NA$-кодом должна просто складывать
исходящие сигналы в массив. Поэтому в библиотеке есть специальный класс
\texttt{D2NA::Recorder} для этого. Он расширяет класс массива \texttt{Array},
но позволяет в конструкторе передать код, сигналы с которого он будет
записывать:
\begin{verbatim}
require 'd2na/vm'
code = D2NA::Code.new do
on :Input do
up :a
end
on :Inpit, :a do
send :Output
end
end
out = D2NA::Recorder.new(code)
code << :Input
code << :Input
code << :Input
out #=> [:Output, :Output]
\end{verbatim}
\newpage
\subsection{Генератор}
Разработчик указывает требования к новой программе с помощью оценочной функции и
функции останова, с помощью которых генератор создаёт новый $D^2NA$-код
множеством итерация из трёх шагов:
\begin{enumerate}
\item Простое копирование $D2NA$-кода, чтобы получить множество нужного
размера.
\item Внесение случайных изменений в каждый экземпляр $D2NA$-кода.
\item Проверка результата оценочной функцией и удаление программ, набравших
минимум баллов.
\end{enumerate}
Пакет генератора требует установки виртуальной машины для $D2NA$ и работает с
ней через её библиотеку для языка Ruby.
Генератор можно разделить на четыре независимые части:
\begin{itemize}
\item \textbf{Evolution} — установка настроек генератора и управление
процессом создания новой программы.
\item \textbf{MutableCode} — изменение $D2NA$-кода, в том числе внесение
случайных изменений.
\item \textbf{Tests} — макоязык (DSL, Domain-Specific Language) для записи
оценочной функции и тестирование промежуточного $D2NA$-кода.
\item \textbf{Population} — упорядоченное хранение промежуточных программ и
удаление на каждом шаге $D2NA$-кода получившего минимальную оценку в
оценочной функции.
\end{itemize}
\subsubsection{Настройка генератора}
Настройка генератора осуществляется с помощью специального макроязыка
(DSL, Domain-Specific Language) — библиотеки с набором методов для языка
программирования Ruby.
Основа генератора — класс \texttt{D2NA::Evolution}. Чтобы описать задание для
генератора нужно написать программу на языке Ruby, подключить библиотеку
\texttt{d2na/evolution} и создать в ней экземпляр этого класса:
\begin{verbatim}
require 'd2na/evolution'
e = D2NA::Evolution.new do
# Настройки генератора и оценочная функция
end
\end{verbatim}
Поскольку Ruby — интерпретируемый язык программирования, то инструкции
генератору можно писать с помощью простого текстового редактора и запуска
текстовый файл в программе \texttt{ruby} версии 1.9.1:
\begin{verbatim}
~$ ruby evolution.rb
\end{verbatim}
Класс \texttt{D2NA::Evolution} имеет набор методов для настройки эволюции:
\begin{itemize}
\item \texttt{protocode(code)} — задаёт \texttt{code} как первое поколение
программы для начала итераций. В основном разработчик в первом поколении
задаёт только имена входящих и исходящий сигналов.
Вызванный без параметров возвращает текущее значение.
\item \texttt{min\_population(count)} — задаёт \texttt{size}, как минимальное
количество программ-кандидатов после каждой итерации. На первый итерации
$D^2NA$-код, заданный в \texttt{protocode}, клонируется до размера
минимальной популяции.
Вызванный без параметров возвращает текущее значение.
\item \texttt{worker\_count(count)} — задаёт \texttt{size}, как количество
потоков, которые будут генерировать новую программу. Чтобы полностью
использовать потенциал современных многоядерных процессоров при
мутация и тестировании кода в многопоточном режиме.
Вызванный без параметров возвращает текущее значение.
\item \texttt{selection(description, options, \&block)} — добавляет
\texttt{block} как оценочную функцию, которой конечный $D^2NA$-код
должен соответствовать. Аргумент \texttt{description} может содержать
описание оценочной функции, а \texttt{options} — хеш с параметром
\texttt{priority}, в котором содержится цифровой приоритет этой
оценочной над другими.
\item \texttt{end\_if(\&block)} — задаёт \texttt{block}, как функцию
остановка. Переданный код будет вызываться в конце каждой итерации, и,
если он вернёт истинное значение, процесс генерации будет остановлен,
так как цель разработчика достигнута. Для разработчик имеется набор
функций, упрощающих задание функции останова:
\begin{itemize}
\item \texttt{success} — возвращает истинное значение, если все
требований оценочной функции выполнены.
\item \texttt{stagnation > }\textit{count} — возвращает истинное
значение, если значение оценочной функции не было улучшено за
последнее \textit{count} шагов.
\end{itemize}
Разработчик может объединять функции, например:
\texttt{success and stagnation > 10}.
\item \texttt{stagnation\_limit(maximum)} — задаёт \texttt{maximum}, как
максимальное количество шагов простоя. Если значение оценочной функции
не было улучшено за максимальное количество шагов простоя, то генерация
останавливается, поскольку генератор не способен создать требуемый код.
Вызванный без параметров возвращает текущее значение.
\end{itemize}
\newpage
Так же класс \texttt{D2NA::Evolution} содержит методы для управления ходом
генерации:
\begin{itemize}
\item \texttt{step!} — делает следующую итерацию написания программы.
\item \texttt{next\_step?} — возвращает истинное значение, если функция
останова показала, что цель генерации ещё не достигнута и требуются ещё
итерации.
\item \texttt{step} — возвращает количество уже выполненных итераций.
\item \texttt{stagnation} — возвращает количество последних шагов, за которые
не удалось улучшить результат оценочной функции.
\item \texttt{population} — возвращает популяцию программ, сгенерированных на
текущем шаге.
\item \texttt{old\_population} — возвращает популяцию программ, полученных на
предыдущем шаге.
\end{itemize}
\newpage
В итоге типичная настройка генератора будет выглядеть как:
\begin{verbatim}
require 'd2na/evolution'
e = D2NA::Evolution.new do
worker_count 4
min_population 10
protocode do
# Входящие/исходящие сигналы согласно
# условиям задачи
input :Print
output :Ping, :Pong
end
selection "First" do
# Первая оценочная функция
end
selection "Second", priority: 2 do
# Вторая оценочная функция,
# более приоритетная
end
end_if { success and stagnation > 10 }
stagnation_limit 20
end
while e.next_step?
e.step!
# Выводим «!» при улучшении результата
# оценочной функции, иначе количество
# выполненных шагов.
0 == e.stagnation ? putc('!') : e.step
end
\end{verbatim}
\subsubsection{Мутатор}
Чтобы вносить в уже созданный код изменения, в пакете генератора есть класс
\texttt{D2NA::MutableCode}, который расширяется класс \texttt{D2NA::Code} из
виртуальной машины, добавляя в него методы:
\begin{itemize}
\item \texttt{modify(\&block)} — вносит набор изменений в код, указанные в
коде \texttt{block}, после чего восстанавливает кеши и компилирует
$D2NA$-код.
\item \texttt{clone} — возвращает полную копию объекта с данным $D^2NA$-кода.
\item \texttt{mutate!} — вносит случайные изменения в текущий $D^2NA$-код.
Параметры случайных изменений можно задать аргументов как хеш или
задать глобальной для всех вызовов \texttt{mutate!} с помощью свойства
объекта \texttt{mutation\_params}.
\item \texttt{to\_ruby} — возвращает текстовое представление загруженного
$D^2NA$-кода для сохранения в файл.
\end{itemize}
Поскольку, каждое изменение $D^2NA$-кода требует ресурсов на компилияцию и
пересоздание кешей, то несколько изменений можно объединить в один блок, передав
их методу \texttt{modify}. Возможные команды изменения программы:
\begin{itemize}
\item \texttt{add\_command(rule, command, param)} — вставляет в
правило с номером \texttt{rule} команду с именем
\texttt{command} для переменной или исходящего сигнала
\texttt{param}.
\item \texttt{remove\_command(position, rule)} — удаляет команду. Либо по
порядковому номеру \texttt{position} во всём коде (если \texttt{rule}
не задан), либо в правиле с указанным в \texttt{rule} номером.
\item \texttt{add\_states(*states)} — добавляет в $D^2NA$-код переменные с
указанными в массиве \texttt{states} именами.
\item \texttt{remove\_state(name)} — удаляет из кода переменную с именем,
указанным в \texttt{name}, и все команды связанные с ней.
\end{itemize}
В итоге, измение загруженного $D^2NA$-код будет выглядеть как:
\begin{verbatim}
code = D2NA::Code.new do
on :Input do
up :a
send :Wrong
end
end
code.modify do
remove_command 2
add_command 1, :send, :Right
end
\end{verbatim}
Параметры доступные в свойстве объекта \texttt{mutation\_params} для управления
случайными изменениями в программе:
\begin{itemize}
\item \texttt{max\_actions} — максимальное количество изменений при мутации.
Количество действий мутатора выбирается с помощью случайной величины
равномерно распределения.
\item \texttt{min\_actions} — минимальной количество изменений при мутации.
\item \texttt{max\_state\_actions} — максимальное количество внесённых
случайных команд с новой переменной после её добавления. Количество
выбирается с помощью случайной величины равномерно распределения.
\item \texttt{min\_state\_actions} — минимальное количество внесённых
случайных команд с новой переменной после её добавления.
\item \texttt{add} — вероятность добавления новой команды.
\item \texttt{remove} — вероятность удаления существующей команды.
\item \texttt{add\_state} — вероятность добавления новой переменной. После
добавления новой переменной будет добавлено случайное количество
команд её использующих.
\item \texttt{remove\_state} — вероятность удаления существующей переменной и
всех команд её использующих.
\end{itemize}
Разработчик может задать параметры мутации, с помощью метода \texttt{protocode}
объекта \texttt{D2NA::Evolution}:
\begin{verbatim}
e = D2NA::Evolution.new do
protocode do
mutation_params[:max_actions] = 6
mutation_params[:min_actions] = 4
…
end
…
end
\end{verbatim}
\subsubsection{Оценочная функция}
Задание оценочной функции — наиболее сложная задание для разработчика при
использовании автоматической генерации программ с помощью генетических
алгоритмов.
К сожалению, нельзя использовать имеющиеся инструменты для автоматического
тестирования, потому что они возвращают только два возможных значения: программа
соответствует тестам или нет. Для генетического алгоритма, оценочная функция
должна возвращать число, чтобы понять, что программа стала чуть больше
соответствовать тесту, даже если полностью его не проходит.
Например, если нам нужно чтобы программа возвращала $1$, но одна программа в
ответе вернула $0$, а другая — $-1$, то тесты должны показать, что первая
программа больше соответствует описанными нами условиям.
Исходя из описанных требований, в пакете генератора для $D^2NA$-кода есть набор
удобных инструментов для задания оценочной функции в виде макроязыка (DSL,
Domain-Specific Language).
Для добавления оценочной функции в классе \texttt{D2NA::Evolution} есть метод
\texttt{selection(description, options, \&block)}, который принимает описание
теста, его настройки и код тестирования.
При автоматическом написании программы, генератор может использовать несколько
оценочных функция сразу. Разработчик может задать им разное описание, чтобы
отличать их, и разный приоритет в настройках теста с помощью ключа
\texttt{priority}, например:
\begin{verbatim}
e = D2NA::Evolution.new do
…
selection "Первая" do
# Код первой оценочной функции
…
end
selection "Первая", priority: 2 do
# Код второй оценочной функции,
# более приоритетной.
…
end
end
\end{verbatim}
Код оценочной функции — это обычная программа на языке программирования Ruby.
Проверки и требования для генерируемого кода, задаются с помощью набора методов:
\begin{itemize}
\item \texttt{send(signals)} — посылает в тестируемый код все входящие сигналы
из массива \texttt{signals}.
\item \texttt{out} — возвращает массив исходящих сигналов, посланных текущим
тестируемым кодом.
\item \texttt{clear\_out!} — очищает буфер исходящих сигналов, возвращаемый
методов \texttt{out}.
\item \texttt{should(value)} — проверяет, чтобы значение \texttt{value} было
истинным, например, вызов \texttt{should out.length == 2} проверяет,
что тестируемая программа послала 2 исходящих сигнала (после начала
теста или последнего вызова \texttt{clear\_out!}).
\item \texttt{max(value)} — требует, чтобы тестируемая программа имела
максимальное значение \texttt{value}. То есть, программа с максимальное
значение \texttt{value} получить максимальную оценочную функцию, чтобы
остатся в следующущуй итерации.
\item \texttt{min(value)} — требует, чтобы тестируемая программа имела
минимальное значение \texttt{value}. Например, вызов\\
\texttt{min(out.length)} требует, чтобы $D^2NA$-код посылал минимум
исходящих сигналов.
\end{itemize}
Для упрощения работы разработчика, библиотека тестов имеет ряд методов,
объединиящих вызов \texttt{should}, \texttt{min} и \texttt{max} для решения
распространённых задач:
\begin{itemize}
\item \texttt{out\_should\_be\_empty} — требует, чтобы тестируемая программа
не посылала ни один исходящий сигнал в начала теста или с последнего
вызова \texttt{clear\_out!}.
\item \texttt{out\_should(signals)} — принимает хеш имеющий в ключах имена
исходящих сигналов и в значения — их количество и проверяет, чтобы
в буфере исходящих сигналов \texttt{out} содержалось только указанные в
\texttt{signals} исходящие сигналы и только в указанном количестве.
\item \texttt{out\_should\_has(signals)} — нестрогий аналог\\
\texttt{out\_should}. Он принимает хеш имеющий в ключах имена
исходящих сигналов и в значения — их количество и проверяет, чтобы
в буфере исходящих сигналов \texttt{out} содержалось указанные в
\texttt{signals} исходящие сигналы и только в указанном количестве.
То есть, если тестируемая программа посылала исходящие сигналы не
указанные в \texttt{signals}, то это не будет считаться ошибкой.
\item \texttt{out\_should\_hasnt(signals)} — проверяет, чтобы в буфере
исходящих сигналов \texttt{out} не содержались сигналы указанные в
массиве \texttt{signals}.
\end{itemize}
Пример, оценочной функции для программы-сумматора, которая посылает исходящих
сигналов \texttt{:C} столько, сколько в сумме пришло входящих сигналов
\texttt{:A} и \texttt{:B}:
\begin{verbatim}
selection "Сумматор" do
out_should_be_empty
send :A, :A
out_should :C => 2
send :B, :B, :B
out_should :C => 5
end
\end{verbatim}
Каждый метод проверки (\texttt{should}, \texttt{min}, \texttt{max}, \\
\texttt{out\_should}, \texttt{out\_should\_has}, \texttt{out\_should\_hasnt},\\
\texttt{out\_should\_be\_empty}) может принимать приоритет проверки с помощью
ключа \texttt{priority}.
Например, в данном тесте, код посылающий лишний сигнал \texttt{:B} будет иметь
меньшее значение оценочной функции, чем код, не посылающий нужный сигнал
\texttt{:A}.
\begin{verbatim}
out_should_has :A => 1
out_should_hasnt :B, priority: 2
\end{verbatim}
Тест считается пройденным, только если выполнены все условиях в проверках
\texttt{should}. Проверки \texttt{min} и \texttt{max} не учитываются в вызове
метода \texttt{success} класс \texttt{D2NA::Evolution}, но по их результата
тест может считаться пройденным лучше или хуже.
\subsubsection{Популяция}
На каждой итерации, после клонирования и случайного изменения, программы на
$D^2NA$ должны складывать в специальный массив-популяцию вместе со своими
результатами оценочной функции.
Хранится они должны по слоям — программы с одинаковыми результатом оценочной
функции должны иметь один слой.
\begin{figure}
\centering
\input{./images/population.tex}
\caption{Пример обрезки популяции}
\end{figure}
После размещения, программы набравшие минимальное значение оценочной функции
должны быть удалены по следующему алгоритму:
\begin{enumerate}
\item Максимальный размер первого слоя (с наилучшему результатами оценочной
функции) вычисляется по формуле:\\
$L_0 = min\_population + (stagnation * stagnation\_grow)$.\\
Максимальный размер каждого последующего слоя равен половине от
предыдущего: $L_{x} = L_{x-1} / 2$
\item На каждом слое программы размещаются по уменьшению длины кода — самые
короткие в начале.
\item Программы не помещающиеся в максимальный размер слоя удаляются с конца
списка.
\end{enumerate}
В начале каждый итерации старая популяция копируется в переменную
\texttt{old\_population}, в в переменной \texttt{population} создаётся новая.
Чтобы в результате случайных изменений не ухудщить результат, перед итерацией
$D^2NA$-код набравший максимальное значение оценочной функции и имеющий
минимальный размер исходного кода без изменений копируется в новую популяцию.
Текущая популяция находится в свойстве \texttt{population} класса
\texttt{D2NA::Evolution} и имеет набор методов:
\begin{itemize}
\item \texttt{best} — возвращает лучший код с максимальным значение оценочной
функции и минимальный размером исходного кода (первый элемент первого
слоя).
\item \texttt{best\_result} — возвращает лучшее значение оценочной функции
(полученное для всех элементов первого слоя).
\end{itemize}
\newpage
\section{Программная реализация}
Программная реализация виртуальной машины и генератора для $D^2NA$-программ
полностью открыта и вместе с историей изменений опубликована в Интернете по
адресу \texttt{http://github.com/ai/d2na} на условиях свободной лицензии
GNU GPL.
\subsection{Средства разработки}
Разработка велась на операционной системе Linux, поскольку являясь UNIX-подобной
операционной системой она обладает целым набором полезным инструментов для
разработки и позволяет программировать быстро и эффективно. Поэтому $D^2NA$
разрабатывалась в духе идеологии UNIX.
Для разработки $D^2NA$ требовался язык удовлетворяющий следующим критериям: