-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathmain.tex
1574 lines (1222 loc) · 227 KB
/
main.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[oneside,final,12pt]{extreport}
%% my command
%%%%%%%%%%%%%%
% Путь к файлу с изображениями
\newcommand{\picPath}{images}
% Величина отступа
\newcommand{\indentSpace}{1.25cm}
% Сокращения
\newcommand{\urlTitle}{ $-$ URL: }
%%%%%%%%%%%%%%%
% Изменяем шрифт
\usepackage{fontspec}
\setmainfont{Times New Roman}
\listfiles
% Полуторный интервал
\linespread{1.6}
% Отступ
\setlength\parindent{\indentSpace}
% Математика
\usepackage{mathtools}
% Картинки
\usepackage{graphicx}
\usepackage{subcaption}
% Языковой пакет
\usepackage[russianb]{babel}
% Таблицы
\usepackage{tabularx}
% Настройка подписей к фигурам
% Меняем заголовки картинок
\usepackage[ labelsep= endash]{caption}
\captionsetup{%
figurename= Рисунок,
tablename= Таблица,
justification= centering, singlelinecheck=false
}
\captionsetup[table]
{
justification= raggedright, singlelinecheck=false
}
% Кирилица в подфигурах
\renewcommand{\thesubfigure}{\asbuk{subfigure}}
% разделитель в подфигурах - правая скобка
\DeclareCaptionLabelSeparator{r_paranthesis}{)\quad }
\captionsetup[subfigure]{labelformat=simple, labelsep=r_paranthesis}
% Добавляем итератор \asbuk,
% чтобы использовать кирилицу
% как маркеры
\usepackage{enumitem}
\makeatletter
\AddEnumerateCounter{\asbuk}{\russian@alph}{щ}
\makeatother
% Меняем маркеры в перечислениях
% Списки уровня 1
\setlist[enumerate,1]{label=\arabic*),ref=\arabic*}
% Списки уровня 2
\setlist[enumerate,2]{label=\asbuk*),ref=\asbuk*}
% Перечисления
\setlist[itemize,1]{label=$-$}
% Удаляем отступы перед и после
% списка
\setlist[itemize]{noitemsep, topsep=0pt}
\setlist[enumerate]{noitemsep, topsep=0pt}
% Красная строка в начале главы
\usepackage{indentfirst}
% Убиваем перенос
\usepackage[none]{hyphenat}
% Перенос длинных ссылок
\usepackage[hyphens]{url}
\urlstyle{same}
% Выравнивание по ширине
\usepackage{microtype}
%\usepackage[fontfamily=courier]{fancyvrb}
%\usepackage{verbatim}% configurable verbatim
% \makeatletter
% \def\verbatim@font{\normalfont\sffamily% select the font
% \let\do\do@noligs
% \verbatim@nolig@list}
%\makeatother
% Границы
\usepackage{vmargin}
\setpapersize{A4}
% отступы
%\setmarginsrb
%{3cm} % левый
%{2cm} % верхний
%{1cm} % Правый
%{2cm} % Нижний
%{0pt}{0mm} % Высота - отступ верхнего колонтитула
%{0pt}{0mm} % Высота - отступ нижнего колонтитула
\setlength\hoffset{0cm}
\setlength\voffset{0cm}
\usepackage[top=2cm, bottom=2cm, left=3cm, right=2cm,
]{geometry}
% Настройка заглавиий
\addto\captionsrussian{% Replace "english" with the language you use
\renewcommand{\contentsname}% содержания
{\hfill\bfseries
СОДЕРЖАНИЕ
\hfill
}%
\renewcommand{\bibname}% списка источников
{\hfill\bfseries
СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ
\hfill
}%
}%\
%\renewcommand{\contentsname}{\hfill\bfseries СОДЕРЖАНИЕ \hfill}
% Настройка заглавий в главах
\usepackage{titlesec}
%\titleformat
%{\chapter} % command
%[display]
%{
%\bfseries
%} % format
%{
%\thechapter.
%} % label
%{
% 0 pt
%} % sep
%{
%\centering
%} % before-code
\titleformat{\chapter}
[block]
{\bfseries }
{\hspace{\indentSpace}\thechapter}
{1em}
{\vspace{0mm} }
[\vspace{14pt}]% Отступ после
% Начальный сдвиг заголовка 50 pt = 1.763888888cm.
% Второй параметр- сдвиг до = 2cm - 50pt
\titlespacing{\chapter}{0pt}
{-0.2361cm}{0pt}
\titleformat{\section}[block]
{\bfseries}{\hspace{\indentSpace}\thesection}{1em}{}
%\titlespacing{\section}{0pt}{0pt}{0pt}
\titleformat{\subsection}[block]
{\bfseries}{\hspace{\indentSpace}\thesubsection}{1em}{}
%\titlespacing{\subsection}{0pt}{0pt}{0pt}
%\titleformat{\section}
% {\bfseries}
% {\thechapter.\hspace{1em}}
% {0pt}
% {\centering
% \vspace{0mm} }
% [\vspace{14pt}]% Отступ после
%\titlespacing{\section}{0pt}{-50pt}{0pt}
% Конец настройка заглавий
% Форматирование списка источников
% Bibl label
\makeatletter
\renewcommand*{\@biblabel}[1]{\indent#1}
\makeatother
% Убрать отсупы в списке источников
\usepackage{lipsum}
% ADD THE FOLLOWING COUPLE LINES INTO YOUR PREAMBLE
%\let\OLDthebibliography\thebibliography
%\renewcommand\thebibliography[1]{
% \OLDthebibliography{#1}
% \setlength{\parskip}{0pt}
% \setlength{\itemsep}{0pt plus 0.3ex}
%}
% Change indent
\usepackage{etoolbox}
\patchcmd{\thebibliography}
{\advance\leftmargin\labelsep}
{\leftmargin=0pt\itemindent=\labelwidth\advance\itemindent\labelsep}
{}{}
% Define separations
\patchcmd{\thebibliography}{\sloppy}{\itemsep -0.28cm \parsep 0pt \sloppy}{}{}
% Добавить точки в оглавление
\usepackage{tocstyle}
\newcommand{\autodot}{}
% Чтобы картинки вставляись
% куда надо
\usepackage{float}
% Для вычисления кол-ва страниц
\usepackage{lastpage}
% Для вычисления кол-ва рисунков и таблиц
%%%
\usepackage{etoolbox}
\newcounter{totfigures}
\newcounter{tottables}
\providecommand\totfig{}
\providecommand\tottab{}
\makeatletter
\AtEndDocument{%
\addtocounter{totfigures}{\value{figure}}%
\addtocounter{tottables}{\value{table}}%
\immediate\write\@mainaux{%
\string\gdef\string\totfig{\number\value{totfigures}}%
\string\gdef\string\tottab{\number\value{tottables}}%
}%
}
\makeatother
\pretocmd{\chapter}{\addtocounter{totfigures}{\value{figure}}\setcounter{figure}{0}}{}{}
\pretocmd{\chapter}{\addtocounter{tottables}{\value{table}}\setcounter{table}{0}}{}{}
%%%
% Режим релиза
\sloppy
\usepackage{layout}
%\renewcommand{\arraystretch}{1.6}
\newcommand{\cmmnt}[1]{\ignorespaces}
\newcommand{\bs}{\boldsymbol}
\usepackage{breqn}
% Change intemize intend
\usepackage{calc}
\usepackage{listings}
\newlength{\mylength}
\settowidth{\mylength}{$-$\textvisiblespace}
\setlist{itemindent= \mylength + \indentSpace ,leftmargin=0pt}
\begin{document}
\tableofcontents
\newpage
\textbf{Введение}
\addcontentsline{toc}{chapter}{Введение}
Машинное обучение в современном мире ставит перед собой довольно амбициозные задачи: мы пытаемся научить машину понимать человеческую речь, видеть картинки и ориентироваться в пространстве, понимать какие-то сложные человеческие концепции и так далее. Все это требует извлечения очень сложных зависимостей из оригинальных данных, и соответственно, чтобы извлекать эти сложные зависимости, нам требуются сложные модели.
Наиболее сложной и продвинутой моделью машинного обучения на текущий момент является нейронная сеть — про нее и будет идти речь в данной курсовой работе. На текущий момент существуют очень большие нейронные сети, которые могут делать потрясающие вещи. Например, существует сеть GPT-3, и в ней ученые смогли обучить 175 миллиардов параметров. Просто ради интереса можно привести числа о том, сколько нейронов находится в голове человека. Ученые говорят, что где-то от 20 до 80 миллиардов, то есть формально говоря, в нейронной сети GPT-3 нейронов больше, чем в голове человека, и при этом она действительно может делать довольно интересные вещи.
Однако у такого подхода есть и обратная сторона медали — эти модели получаются очень трудно используемыми: они получаются тяжело обучаемыми, они занимают очень много места, а также они очень долго считают свой ответ.
В целом, если мы говорим про какую-то исследовательскую работу, тогда это небольшая проблема: там действительно мы ставим перед собой задачу просто получить самую лучшую модель по качеству. Однако, если мы с вами возвращаемся в реальный мир, то ситуация немного меняется.
Реальные примеры использования машинного обучения требуют более жестких требований к самим алгоритмам. Хочется одновременно получать и хорошее качество результатов, и высокую скорость обработки; при этом было бы неплохо, если бы наша модель была автономной, а также ресурсоэффективной.
Примеров таких задач огромное количество. Например, это улучшение фотографий в наших телефонах — сделали какую-то фотографию, и она сразу стала более лучшего качества. Это возможно, потому что она была сразу обработана алгоритмом. Также можно привести автоматический перевод текстов с картинки, голосовые ассистенты, которые работают в каждом телефоне, и вплоть до беспилотных автомобилей. Все эти алгоритмы должны работать быстро, качественно и, желательно, не требовать дополнительного взаимодействия с сетью Интернет.
Отметим, что такое вообще мобильные девайсы или мобильные устройства. Самые очевидные представители мобильных устройств — это, конечно же, телефоны и смартфоны. По состоянию на 2021 год насчитывают более 14 миллиардов телефонов и смартфонов по всему миру, то есть немного больше, чем количество людей на планете.
Однако стоит отметить, что телефоны и смартфоны — это не единственные представители мобильных устройств. Также существует гигантское количество различных приборов, аудиосистем, роутеров, умных вещей, часов и так далее. То есть много каких-то маленьких устройств, которые при этом имеют процессор и умеют вычислять и решать какие-то несложные в плане ресурсов задачи.
Все эти приборы можно было бы каким-то образом улучшить, если бы мы могли добавить к ним элементы алгоритмов искусственного интеллекта, чтобы они могли лучше выполнять поставленную перед ними задачу. Однако, возникает проблема с тем, что у них очень жесткие требования к алгоритмам, которые мы можем на них запускать.
К таким требованиям относится то, что они должны работать в рамках очень ограниченной памяти, они должны использовать ограниченное число тактов процессора, потому что процессор на самих устройствах довольно слабый, желательно, чтобы они по минимуму использовали или не использовали вовсе интернет-соединение, так как на некоторых девайсах его просто нет, а на некоторых это очень дорого, и, что немаловажно, у всех таких девайсов чаще всего очень ограничено количество батареи, которую они могут использовать для своей работы, и поэтому никто бы не хотел разряжать устройство алгоритмом слишком быстро.
Можно пытаться решить эту задачу таким классическим образом: мы берем большие модели с огромным количеством параметров и запускаем их на гигантских кластерах. В итоге все приложения пользователей будут просто общаться с этими серверами и получать нужную информацию от них. Однако, модели все еще остались достаточно большими, и даже на таких больших мощностях они могут работать медленно, мы можем тратить на их поддержание огромное количество денег, и при этом не стоит забывать, что нам все еще требуется интернет-соединение для каждой работы нашего алгоритма.
Если же идти другим путем и пытаться запускать эти модели на самих устройствах, возникает другая проблема: устройства очень слабые, и им просто может не хватить ресурсов, чтобы запустить эти модели; или даже если мы все-таки смогли их запустить, они могут работать слишком медленно или моментально расходовать всю батарею, которая есть на устройстве. Таким образом, если мы говорим про то, чтобы запускать реальные модели машинного обучения в каких-то практических случаях, тогда мы должны подумать о том, как бы нам оптимизировать размер этой модели и скорость ее выполнения.
В этой курсовой работе будут разобраны эффективные методы вычислений корневых операций в нейронных сетях, какие модели машинного обучения можно оптимизировать и каким образом, какие различные архитектуры подходят для запуска на мобильных устройствах. Особое внимание будет уделено таким процессам оптимизации, как квантизация и прунинг. Будут рассмотрены существующие фреймворки для этих инструментов, в частности NNCF, почему именно этот фреймворк подходит для промышленого машинного обучения и зачем нам в целом иметь какой-либо фреймворк для алгоритмов сжатия моделей.
В практической части представлена реализация алгоритмов дистилляции знаний между нейронными сетями, статический прунинг после тренировки, итеративный прунинг, методы квантизации после тренировки и динамический вид квантизации во время тренировки. Для проверки этих алгоритмов были взяты сверточные и полносвязанные нейронные сети, в частности для алгоритмов сжатия была выбрана наиболее популярная архитектура ResNet и академический датасет FashionMNIST. В заключительной части мы посмотрим и применим фреймворк NNCF к реальной задаче антиспуфинга лиц, где в качестве основы нейронной сети взята MobileNetV3.
\chapter{Структурная оптимизация нейронных сетей}
Структурная оптимизация может включать в себя три части: оптимизация алгоритмов производимых вычислений, поиск архитектурных решений и дистилляция знаний.
Сегодня, занимаясь разработкой исскуственных нейронных сетей все исследователи в той или иной мере используют уже готовые фреймовроки, такие как Tensorflow, Pytorch, Theano. Они позволяют не задумываться о тонкостях реализации таких операций как матричные вычисления, операции свертки, взятие градиента и так далее, а сосредоточиться на основном алгоритме для решения поставленной задачи. Однако, важно понимать, что эффективная реализация и оптимизация подобных низкоуровневых компонентов очень важна и правильное использование подобных инструментов позволит ускорить вычисления в разы.
Например, давайте рассмотрим умножение матриц. Это является ключевой операцией в любом алгоритме исскуственного интелекта, связанного с нейронными сетями. На рисунке 1.1 представлен наивный алгоритм реализации матричного умножения на языке программирования С
\begin{figure}
\begin{center}
\includegraphics{\picPath/structure_1.png}
\caption{Код реализации матричного умножения на языке С}
\label{fig:structure_1}
\end{center}
\end{figure}
Что не так с этим алгоритмом? Очевидно, что временая сложность такого алгоритма будет порядка $O(n^3)$ для квадратных матриц. Поэтому нужно попытаться как-то оптимизировать данный алгоритм.
Существует такая программа для вычислений матриц, как GEMM (General matrix multiplication). Она предоставляет интрефейс для вычислений матриц подобного вида: $a*AB + b*C$. Это одна из трех программ коплекса вычислений линейной алгебры BLAS (Basic Linear Algebra Subprograms). BLAS включает в себя стандартизированный интерфейс для работы с объектами линейной алгебры, а именно включает в себя 3 уровня подпрограмм:
\begin{itemize}
\item векторные вычисления
\item матрица-вектор
\item матрица-матрица
\end{itemize}
Множество современных библиотек исаользуют реализацию GEMM с множеством дополнительных оптимизаций под разные архитектуры железа.
Рассмотрим как представлена x86,64 архитектура центрального процессора. На рисунке 1.2 представлена пирамида иерархии памяти для процессора, справа изображен пример как распределена кэш-память для процессора Intel core i7.
Что же ограничивает производительность нашего алгоритма? Здесь может быть две причины. Первая это то, что у нашего процессора ограничено количество операций в секунду, которое мы не можем превзойти. Но есть другой вариант, когда наш алгоритм сильно завязан на перессылке данных из оперативной паяти в процессор и это тоже может очень сильно ограничивать его скорость. В уравнении (1.1) представлена оценка производительности алгоритма в термине FLOPS (FLoating-point Operations Per Second)
\begin{figure}[H]
\includegraphics[width=\linewidth]{\picPath/structure_2.png}
\caption{Иерархия памяти на примере процессора Intel Core I7. Помимо основной памяти, можно выделить три уровная кэша, наверху расположен регистер процессора, образующий сверхбыструю оперативную память}
\label{fig:structure_2}
\end{figure}
\begin{equation}
FLOPS \leq min( peak\ FLOPS,\ \frac{FLOP}{bytes} * peak\ bindwidth),
\end{equation}
где $peak\ bandwidth$ – пропускная способность памяти,
$\frac{FLOP}{bytes}$ – интенсивность вычислений
Получается так называемая Roofline model, на рисунке 1.3 представлено изображение, показывающее зависимость интенсивности вычислений от производительности реализации алгоритма. Эффективной реализацией алгоритма (эффективной оптимизацией под аппаратуру) считается та реализация, у которой производительность близко находится к прямым ограничения по памяти и по количеству операций в секунду конкретного процессора. Ключевой фактор любого алгоритма – это его интенсивность вычисления.
\begin{figure}[H]
\begin{center}
\includegraphics{\picPath/structure_4.png}
\caption{Roofline model}
\label{fig:structure_4}
\end{center}
\end{figure}
При наивной реализации матричного произведения не эффективно используется память и происходит многократная перессылка данных в оперативной памяти и данный алгоритм будет иметь низкую интенсивность вычислений, примерно $\frac{1}{2}$.
Первый подход к оптимизации использования кэш памяти в данном алгоритме это так называемый блокинг. На рисунке 1.4 представлена визуализация данного процесса.
\begin{figure}[H]
\begin{center}
\includegraphics{\picPath/structure_5.png}
\caption{Визуализация операции блокинга}
\label{fig:structure_5}
\end{center}
\end{figure}
Мы разбиваем матрицы на небольшие блоки размером bxb и мы можем применить матричное произведение для этих блоков, как показано в уравнении (1.2):
\begin{equation}
C_{pq}=\sum_{r=1}^{k/b}A_{pr}B_{rq}\ \ \ \ \ \
p=1,...,\frac{m}{b};\ \
q=1,...,\frac{n}{b}
\end{equation}
Мы также будем использовать тройной цикл, но интенсивность вычислений будет выше. У нас будет всего $\frac{mnk}{b^3}$ внутренних итераций (для матриц размером $m \times n$ и $n \times k$). Теперь на каждой итерации считывается $2b^2$ элементов, а количество операций в секунду для вычисления блока матрицы $С – 2b^3$, поэтому итоговая интенсивность вычислений будет следующая, как показано в уравнении (1.3)
\begin{equation}
\frac{2mnk}{(\frac{mnk}{b^3} \cdot 2b^2 \cdot sizeof(float))} = \frac{b}{4}
\end{equation}
Параметр b выбирается настолько большим, насколько нам позволяет вместить кэш. В данном примере мы считали, что у нас всего один кэш, однако как было уже сказано выше, в процессорах несколько кэшей разного уровня. Поэтому для дальнейшего ускорения мы можем сделать блокинг на нескольких уровнях кэша и даже на уровне регистров. Есть также подходы по переупорядочению элементов, чтобы элементы блоков лежали рядом, при этом выборка будет из соседних участков памяти и это еще сильнее должно ускорить алгоритм. Далее у многопоточных процессоров есть векторные инструкции для векторизации вычислений в циклах.
Все это способствуют ускорению и оптимизации вычислений в наших алгоритмах во много раз.
\section{Эффективная реализация свертки}
Операция свертки – это ключевая операция для сверточных нейронных сетей. Поэтому ее оптимизация очень важна для производительной работы алгоритма. Наивная реализация этой операции может быть представлена в виде скользящего окна размером $k \times k$ и для каждого положения этого окна мы будем производить операции поэлементного умножения и последующей суммы. Однако это опять неэффективное использование памяти и многократная перессылка данных из оперативной памяти и обратно, что ведет к низкой интенсивности вычислений. Чтобы ускорить этот алгоритм мы можем выразить операцию свертки через матричные операции. Такое преобразование называется image to column.
Рассмотрим свертку одного одноканального изображения размером $4 \times 4$ пикселя (значения пикселей обозначены через $X$). Сворачивать будем с ядром из одного фильтра размером $3 \times 3$, веса обозначены через $W$. Для простоты примем $stride = 1$. Тогда выход $Y$ будет иметь размерность $1 \times 1 \times 2 \times 2$ (в данном случае на входе одно изображение - это первая единица в размерности, в ядре один фильтр - это вторая единица в размерности выхода). На рисунке 1.5 представлена визуализация операции свертки в простейшем виде.
\begin{figure}[H]
\begin{center}
\includegraphics[width=\linewidth]{\picPath/svertra_1.png}
\caption{ визуализация операции свертки}
\label{fig:svertra_1}
\end{center}
\end{figure}
Оказывается выход свертки можно получить умножением матриц, как показано на рисунке 1.6.
\begin{figure}[H]
\begin{center}
\includegraphics[width=\linewidth]{\picPath/svertra_2.png}
\caption{Image2column.Представление операции свертки матричным произведением}
\label{fig:svertra_2}
\end{center}
\end{figure}
Давайте перейдем от простого случая к общему:
\begin{itemize}
\item Если фильтров в ядре больше одного. Заметим, что для каждого фильтра, матрица $W’$ будет умножаться на один и тот же вектор изображения. Значит, можно сконкатенировать матрицы фильтров ядра по вертикали и за одно умножение получить ответ для всех фильтров.
\item Если на входе более одного изображения: заметим, что матрица $W’$ одинакова для всех изображений батча, то есть, можно каждое изображение вначале вытянуть в столбец, а затем эти столбцы для всех изображений батча сконкатенировать по горизонтали.
\item Если в изображении больше одного слоя, вначале выполним преобразования входа и ядра для каждого слоя, а затем сконкатенируем: вектора разных слоев входа в один большой вектор, а матрицы ядра соответственно в одну длинную матрицу. И мы получим сложение от выходов по слоям в процессе перемножения матриц.
\end{itemize}
На рисунке 1.7 представлена визуализация подхода Image2Column для много-канального изображения. То есть даже в самом общем случае мы за одно умножение матриц можем получить ответ.
В реализации данной операции есть один недостаток. Полученная матрица ядер является разреженной и содержит много нулей, это снижает эффективность метода.
Давайте получим тоже самое, при этом уберем этот недостаток. Пусть в этот раз на входе батч из одного трехслойного (RGB) изображения размером $3 \times 3$. Пусть ядро имеет 2 фильтра шириной и высотой 2 пикселя. Тогда выход должен иметь размерность $1 \times 2 \times 2 \times 2$. Пусть $W$ - веса ядра, $X$ - значения входной матрицы, $Y$ - значения на выходе. Для простоты слои изображения и слои фильтров ядра покрашены в цвета. На рисунке 1.8 представлена визуализация операции свертки для двух фильтров и RGB изображения
\begin{figure}[H]
\begin{center}
\includegraphics[width=\linewidth]{\picPath/svertra_3.png}
\caption{многоканальный Image2Column}
\label{fig:svertra_3}
\end{center}
\end{figure}
\begin{figure}[H]
\begin{center}
\includegraphics[width=\linewidth]{\picPath/svertra_4.png}
\caption{Визуализация операции свертки для двух фильтров и трех каналов изображения}
\label{fig:svertra_4}
\end{center}
\end{figure}
Если в первом матричном способе мы вытягивали изображения в столбцы, то теперь будем вытягивать фильтры кернела в строки, как показано на изображении 1.9.
Если изображений в батче больше одного: преобразования ядра от этого не меняется, а преобразованные матрицы входных изображений конкатенируются по горизонтали. Теперь, используя GEMM для матричного произведения мы можем добиться высокой оптимизации операции свертки.
Но с таким подходом есть все же свои проблемы. Когда мы конструируем подобную матрицу в общем случае у нас происходит дублирование данных. Почти каждый элемант дублируется $k^2$ раз, где $k$ – это размер ядра. Конструируем матрицу в главной, глобальной памяти и занимаем достаточно много ее количества. На практике напрямую такую матрицу в память не сохраняют, а подгружают ее частями. Например, при реализации GEMM для GPU используется разделяемая память, которая достаточно быстрая и используют указатели при хранении объектов для более эффективного использования памяти.
Следующий подход к реализации свертки основан на быстром преобразовании Фурье. Дискретное преобразование Фурье (Discrete Fourier Transform) — это одно из преобразований Фурье, широко применяемых в алгоритмах цифровой обработки сигналов, а также в других областях, связанных с анализом частот в дискретном (к примеру, оцифрованном аналоговом) сигнале. Дискретное преобразование Фурье требует в качестве входа дискретную функцию. Такие функции часто создаются путём дискретизации (выборки значений из непрерывных функций). Дискретные преобразования Фурье помогают решать дифференциальные уравнения в частных производных, его модификации применяются в сжатии звука в MP3, сжатии изображений в JPEG, выполнять такие операции, как свёртки и др. Определение представлено в уравнении (1.4).
\begin{figure}[H]
\begin{center}
\includegraphics[width=\linewidth]{\picPath/svertra_5.png}
\caption{Представление операции свертки через матричное произведение более эффективным путем. Избавляемся от разреженных матриц}
\label{fig:svertra_5}
\end{center}
\end{figure}
\begin{equation}
X_k = \sum_{n=0}^{N-1}x_n\cdot e^{\frac{-2\pi i}{N}kn}=\sum_{n=0}^{N-1}x_n\cdot[cos(\frac{2\pi}{N}kn)-i\cdot sin(\frac{2\pi}{N}kn))]
\end{equation}
Это преобразование обратимо и обратная функция имеет вид, как показона в уравнении (1.5):
\begin{equation}
X_k = \frac{1}{N}\sum_{n=0}^{N-1}x_n\cdot e^{\frac{2\pi i}{N}kn}
\end{equation}
Попробуем оценить вычислительную сложноть DFT.
Наивная оценка данной операции порядка $ О (N^2) $. Однако, если мы разобьем функцию на сумму четных и нечетных индексов (уравнение (1.6))
\begin{equation}
\begin{split}
X_k = \sum_{m=0}^{N/2-1}x_{2m}\cdot e^{\frac{-2\pi i}{N}(2m)k} + \sum_{m=0}^{N/2-1}x_{2m+1}\cdot e^{\frac{-2\pi i}{N}(2m+1)k} = \\ =\sum_{m=0}^{N/2-1}x_{2m}\cdot e^{\frac{-2\pi i}{N/2}(m)k} + e^{\frac{-2\pi i}{N}k}\sum_{m=0}^{N/2-1}x_{2m+1}\cdot e^{\frac{-2\pi i}{N/2}mk}
\end{split}
\end{equation}
Если $k < N/2-1$, то финальная сумма для $k$-ых элементов DFT четных и нечетных сигналов входа будет соответственно:
\begin{equation}
X_k = E_k + e^{\frac{-2\pi i}{N}}O_k
\end{equation}
Для $k’ = k + N/2$:
\begin{multline}
X_{k+\frac{N}{2}} = \sum_{m=0}^{N/2-1}x_{2m}\cdot e^{\frac{-2\pi i}{N/2}m(k+\frac{N}{2})} + e^{\frac{-2\pi i}{N}(k+N/2)}\sum_{m=0}^{N/2-1}x_{2m+1}\cdot e^{\frac{-2\pi i}{N/2}m(k+\frac{N}{2})} = ...\\ = E_k - e^{\frac{-2\pi i}{N}k}O_k
\end{multline}
Вторая половина выходов тоже выражается через компоненты четных и нечетных индексов. То есть получили рекурсивную процедуру и если предположить, что N это степень двойки, то мы получим сложность порядка $О(NlogN)$.
Полезное свойство DFT заключается в том, что DFT реального входа (нулевая мнимая часть) это палидром вида:
$$[X_0\ \ X_1\ \ X_2\ \ X_3\ \ X_4\ ... \ X_3\ \ X_2\ \ X_1]$$
Благодаря нулевой фазе и периодичности синусоидальных функций мы можем вычислять и хранить примерно половину всех данных. Доказана теорема свертки (уравнение (1.9)):
\begin{equation}
\begin{split}
{(f*g)(x)}
\Leftrightarrow
{(F \cdot G )(\xi)}\\
{(f \cdot g)(x)}
\Leftrightarrow
{(1/MN)(F*G )(\xi)}
\end{split}
\end{equation}
То есть мы применяем прямое преобразование Фурье для входа и фильтра, покомпонентно перемножаем и затем делаем обратное преобразование. Свертка двух функций в пространственном базисе эквивалентна по элементному произведению в частотном. Тогда как наивный метод посредством скользящего окна требует $MNmn$ операций для свертки изображения размера $M \times N$ с ядром размерности $m \times n$. Свертка же посредством преобразования Фурье, вместе со всеми преобразованиями требует $2MNlog2(MN)$ операций, что в случае большой размерности ядра значительно уменьшает число вычислений.
Еще один способ вычислить эффективно свертку – применить так называемую свертку Винограда. Стандартный подход к вычислению линейной свертки требует 6 операций умножений. Данный алгоритм сокращает количество умножений до 4, при этом увеличив количество сложений вещественных чисел. Но так как умножение это более дорогая операция, то мы можем получить выйгрыш в скорости. в уравнении (1.10) представлена формула для вычисления одномерной свертки таким способом:
\begin{equation}
F(2,3) = \begin{bmatrix}
d_0& d_1 & d_2\\
d_1& d_2& d_3
\end{bmatrix} \cdot \begin{bmatrix}
g_0\\
g_1\\
g_2
\end{bmatrix} = \begin{bmatrix}
m_1+m_2+m_3\\
m_2-m_3-m_4
\end{bmatrix}
\end{equation}
где\ \ $m_1=(d_0-d_2)g_0$\\
$m_2=(d_1+d_2)\frac{g_0+g_1+g_2}{2}$\\
$m_3=(d_2-d_1)\frac{g_0-g_1+g_2}{2}$\\
$m_4=(d_1-d_3)g_2$
В целом можно подвести итог по текущему состоянию в области оптимизации сверточных операций (согласно статьям и исследованиям):
\begin{itemize}
\item DFT эффективен только для больших ядер, что редко встречается в современных моделях глубокого обучения. Поэтому данный метод используется редко.
\item Winograd - это самый прорывной алгоритм для сверток $3 \times 3$, однако он требует очень тщательной оптимизации под конкретное железо из-за низкой вычислительной интенсивности.
\item GEMM по-прежнему является выбором по умолчанию в средах глубокого обучения (и, конечно, он используется на различных несверточных уровнях, таких как self-attention модулях)
\end{itemize}
Подведем итог:
\begin{itemize}
\item Операции умножения и свертки матриц лежат в основе нейронных сетей и требуют тщательной оптимизации. Существует несколько алгоритмов свертки с разными преимуществами и недостатками.
\item Roofline позволяет оценить максимальную производительность, достижимую алгоритмом и четко определить насколько удачно оптимизирован тот или иной алгоритм под конкретный вид аппартного обеспечения.
\end{itemize}
\section{Архитектурные решения}
\subsection{MobileNet}
Когда речь идет про запуск какого-то алгоритма на мобильных устройствах, то всегда говорят про лекговесные архитектуры, которые позволяют запускать сложные алгоритмы на слабых в плане ресурсов системах. В целом, мы могли бы взять методы оптимизации, такие как прунинг или квантизация, о которых пойдет речь в дальнейших главах и применить их к довольно крупной нейронной сети. Это довольно неплохой подход, и скорее всего он будет работать. Однако стоит отметить, что для каких-то конкретных задач мы могли бы взять какую-то конкретную архитектуру, то есть более оптимальную для решения конкретной задачи архитектуру нейронной сети, которая могла бы запускаться на мобильном устройстве.
В качестве такой архитектуры рассмотрим MobileNet — она была выпущена компанией Google, и основная идея этой нейронной сети в том, чтобы использовать облегченные, так называемые "разделяемые" сверточные слои. Чтобы понять, чем облегченный сверточный слой отличается от обычного сверточного слоя, давайте сначала рассмотрим свертки в двумерном пространстве.
У нас есть какая-то небольшая матрица с числами размера $K \times K$ — это, собственно, ядро свертки, и у нас есть изображение $N \times M$. Посчитаем, сколько умножений нам потребуется применяя свертку размера $K \times K$ к изображению $N \times M$. Чтобы применить один раз свертку, нам нужно $K \times K$ перемножений. Нам ее нужно применить для каждого элемента нашего изображения, то есть $N \times M$ раз. Итого умножений будет: $K \times K \times N \times M$.
Нужно заметить, что иногда большую свертку можно разбить на две свертки меньшего размера. Как в примере ниже можно разделить большую свертку на две поменьше: одна размера $K \times 1$, и другая размера $1 \times K$. Теперь, чтобы применить нашу уже разделенную свертку, нам нужно действовать в два этапа. Первым этапом мы применим первую свертку размера к нашим данным, и после этого к полученному результату мы применим уже вторую сверху размера $1 \times K$. В итоге получим исходный результат.
Давайте теперь посмотрим, сколько перемножений нам потребуется для того, чтобы это реализовать. На первом этапе мы потратим $N \times M \times K$ перемножений. На втором этапе мы же потратим $K \times 1 \times N \times M$ операций. Итого выходит, что на применение таких разделенных сверток нам потребуется в итоге $2 \times K \times N \times M$ операций. Стоит заметить, что если К больше двух, то мы уже сильно уменьшаем количество перемножений, необходимых для проведения данной операции. Если бы $K$ равен был трем, то вместо $9 \times N \times M$ операций сделали бы всего $6 \times N \times M$ операций.
Такие свертки получили название Separable Convolutions (разделяемые свертки). Стоит отметить, что не любую свертку мы можем разделить на несколько поменьше. Однако, когда мы пытаемся выучить эти свертки, то этот пример показывает, что если мы будем пытаться обучить две свертки вместо одной большой, то результат в целом может не уступать по качеству оригинальной свертке.
Теперь посмотрим на более практическое применение, на те свертки, которые используются конкретно в MobileNet. Там используются уже трехмерные свертки, третья размерность отвечает за количество каналов. На рисунке 1.10 изображена операция трехмерной свертки. Будем считать, что в нашем изображении $D$ каналов. Если изображение — это матрица размера $N \times M \times D$, то и свертка — это теперь матрица размера $K \times K \times D$, то есть на то число каналов, которое в нашем изображении.
Если мы хотим получить на выходе больше чем один канал, например, то нужно просто сделать большее количество фильтров, обозначим их количество за $S$. Давайте теперь посчитаем, сколько умножений требуется для применения вот такой трехмерной свертки. Очевидно, каждое ядро свертки применяется за $D \times K \times K$ элементов, далее, это нужно применить для каждого элемента нашего изображения. Изображение размера $N \times M$, поэтому это домножаем еще на $N$ и на $M$. Как мы уже выяснили, если мы хотим $S$ каналов в итоге мы, должны сделать это $S$ раз, то есть умножаем на $S$. В итоге $D \times K \times K \times N \times M \times S$ — это количество перемножений в таких тяжелых свертках.
\begin{figure}[H]
\begin{center}
\includegraphics[width=\linewidth]{\picPath/arc_1.png}
\caption{Визуализация трехмерной операции свертки}
\label{fig:arc_1}
\end{center}
\end{figure}
Теперь давайте попробуем адаптировать ту идею разделения сверток, которая была представлена выше в двумерном случае. На первом этапе у нас вместо одной свертки размера $D \times K \times K$ будет $D$ сверток двумерных размера всего лишь $K \times K \times 1$. Каждую конкретную свертку мы применим независимо к своему каналу в изображении. То есть если в изображении было три канала, у нас будет три свертки двумерные размера $K \times K$.
Мы параллельно и независимо их применяем и получаем по итогу $D$ каналов, но уже с примененными двумерными свертками. После этого осталось этот результат размера $D \times N \times M$ свернуть в один слой, и для этого мы используем отделенную от нашей оригинальной свертки свертку размера $D \times 1 \times 1$. Она будет схлопывать обратно наши независимые слои, которые мы посчитали на предыдущем шаге.
Если же мы теперь хотим сделать $S$ каналов по итогу, то нам вместо всей операции нужно будет $S$ раз применить последнюю свертку размера $D \times 1 \times 1$. Такие свертки получили название "Depthwise Separable Convolutions", то есть сверки как бы разделяемые по глубине. На рисунке 1.11 изображен алгоритм выполнения такой свертки.
\begin{figure}[H]
\begin{center}
\includegraphics[width=\linewidth]{\picPath/arc_2.png}
\caption{Визуализация depth-wise свертки}
\label{fig:arc_2}
\end{center}
\end{figure}
Давайте посчитаем количество перемножений. В первом случае у нас есть $D$ сверток размера $K \times K$, итого $D \times K \times K$ операции, умножить на $N \times M$, то есть на количество пикселей в нашем изображении. Это мы сделали на первом этапе. На втором этапе нам нужно применить свертку размера $D$ к изображению $N \times M$, то есть сделать $D \times N \times M$ операций, и после этого сделать это $S$ раз. Складывая все эти числа, мы получаем итоговую формулу (1.11):
\begin{equation}
K\cdot K\cdot N\cdot M\cdot D + D\cdot N\cdot M\cdot S = N\cdot M\cdot D\cdot (K\cdot K + S)
\end{equation}
Если взять какие-то конкретные числа:
$N=256, M=256, S=64, K=3, D=10$, тогда получим:\\
Обычные свертки: $103\cdot 3\cdot 256\cdot 256\cdot 64 = 337$млн\\
разделяемые: $256\cdot 256\cdot 10\cdot (3\cdot 3 + 64) = 47$млн
Мы смогли уменьшить количество перемножений почти в 8 раз, при этом количество параметров стало меньше. На основе таких сверток строилась архитектура MobileNet и это основной принцип который помог MobileNet стать легковесной моделью.
\subsection{Inception}
\textbf{Receptive field} - это размер карты характеристик, вычисляемой с помощью сверточного ядра, что в действительности и является размером ядра. Если необходимо извлечь данные с большим воспринимающим полем с высокой точностью, следует применять каскадные уровни, как показано на изображении 1.12.
Однако можно пойти другим путем и вычислить паралельно разные свертки с разным размером ядра. Архитектурный блок Inception сети расширяет ширину сети с помощью четырех разных фильтров. Это позволяет увеличить поле воспримчивости сети и при этом значительно сократить количество вычислений. В дополнении конкатенированный результат работает лучше, чем один сверточный слой при одинаковых вычислительных нагрузках. На рисунке 1.13 изображен Inception блок
\begin{figure}[H]
\begin{center}
\includegraphics[width=\linewidth]{\picPath/arc_3.png}
\caption{Каскадные свертки для увеличения воспринимающего поля нейронной сети.}
\label{fig:arc_3}
\end{center}
\end{figure}
\begin{figure}[H]
\begin{center}
\includegraphics[width=\linewidth]{\picPath/arc_4.png}
\caption{Inception блок}
\label{fig:arc_4}
\end{center}
\end{figure}
\subsection{EfficientNet}
Авторы EfficientNet предлагают использовать поиск нейронной архитектуры посредством NAS алгоритма для построения эффективной базовой архитектуры сети EfficientNet-b0. Она достигает 77.3\% точности на датасете ImageNet, при этом используя в 5 раз меньше параметров, чем ResNet50, который достигает точности в 76\%. Далее авторы предлагают масштабирование сети, посредством трех коэффициентов, отвечающих за ширину, глубину и пространственное разрешение сети.
Основным строительным блоком этой сети является MBConv, к которому добавлена операция, так называемого сжатия и возбуждения. Эта операция помогает в какой-то степени отфильтровать карты характеристик по значимости, перевзвешивая их.
MBConv аналогичен инвертированным остаточным блокам (Inverted Residuals), используемых в MobileNet V2 и V3. Карты активации входных данных сначала расширяются с использованием сверток $1 \times 1$ для увеличения глубины. За этим следуют разделяемые свертки $3 \times 3$ и точечные свертки $1 \times 1$, которые уменьшают количество каналов в выходной карте функций. Таким образом, получается инвертированный bottleneck. Эта структура помогает уменьшить общее количество требуемых операций, а также размер модели. На изображении 1.14 представлен Inverted Residual блок.
Сверточную нейронную сеть можно масштабировать в трех измерениях: глубина, ширина, разрешение. Глубина сети соответствует количеству слоев в сети. Ширина связана с количеством нейронов в слое или, что более уместно, с количеством фильтров в сверточном слое. Разрешение - это просто высота и ширина входного изображения.
\begin{figure}[H]
\begin{center}
\includegraphics[width=\linewidth]{\picPath/arc_5.png}
\caption{Inverted Residual блок}
\label{fig:arc_5}
\end{center}
\end{figure}
Увеличение глубины за счет применения большего количества сверточных слоев позволяет сети изучать более сложные функции. Однако более глубокие сети, как правило, страдают от исчезающих градиентов и их трудно обучать. Хотя новые методы, такие как пакетная нормализация и пропуск соединений (residual connection), эффективны для решения этой проблемы, эмпирические исследования показывают, что фактический выигрыш в точности достигаемый только за счет увеличения глубины сети быстро исчезает. Например, Resnet-1000 обеспечивает ту же точность, что и Resnet-100, несмотря на все дополнительные слои.
Масштабирование ширины сетей позволяет слоям изучать более мелкие объекты. Эта концепция широко использовалась во многих работах, таких как Wide ResNet и Mobile Net. Однако, как и в случае увеличения глубины, одно лишь увеличение ширины не позволяет сети изучать сложные функции, что приводит к снижению прироста точности.
Более высокое входное разрешение обеспечивает более подробную информацию об изображении и, следовательно, увеличивает способность модели рассуждать о более мелких объектах и извлекать более детальные узоры. Но, как и другие масштабные изменения, это тоже само по себе имеет ограниченный прирост точности.
Авторы предлагают простой, хотя и эффективный метод масштабирования, который использует составной коэффициент $ \phi $ для единообразного масштабирования ширины, глубины и разрешения сети как представлено в уравнении 1.11.
$\phi$ - определяется пользователем, это глобальный коэффициент масштабирования (целое число), который контролирует количество доступных ресурсов, тогда как $\alpha$, $\beta$ и $\gamma$ определяют, как “распределить” эти ресурсы на глубину, ширину и на пространственное разрешение сети соответственно. FLOPS (количество операций с плавующей точкой в секунду) сверточной сети пропорциональны $d$, $w^2$, $r^2$, поскольку удвоение глубины удвоит FLOPS, в то время как удвоение ширины или разрешения увеличивает FLOPS почти в четыре раза. Таким образом, масштабирование сети с использованием уравнения 1.11 увеличит общее количество FLOPS на $(\alpha \cdot \beta^2 \cdot \gamma^2) ^ \phi$. Следовательно, чтобы гарантировать, что общее количество FLOPS не превышает $2 ^ \phi$, применяется ограничение $(\alpha \cdot \beta^2 \cdot \gamma^2) \approx 2$. Это означает, что если у нас вдвое больше доступных ресурсов, мы можем просто использовать составной коэффициент 1 для масштабирования количества операций в секунду в 2 раза.
\begin{equation}
\begin{split}
\text{глубина:} d = \alpha^\phi\\
\text{ширина}: w = \beta^\phi\\
\text{разрешение}: r = \gamma^\phi\\
\text{при ограничениях}: \alpha \cdot \beta^2 \cdot \gamma^2 \approx 2\\
\alpha \geq 1, \beta \geq 1, \gamma \geq 1
\end{split}
\end{equation}
Параметры - $\alpha$,$\beta$ и $\gamma$ - можно определить с помощью жадного поиска, установив $\phi$ = 1 и найдя параметры, которые приводят к наилучшей точности. После того, как эти параметры найдены, их можно зафиксировать, а составной коэффициент $\phi$ можно увеличить, чтобы получить более крупные, но более точные модели. Так построены EfficientNet-B1 - EfficientNet-B7, с целым числом в конце названия, указывающим значение составного коэффициента.
\section{дистилляция знаний}
\subsection{Пример дистилляции знаний}
В этой секции мы рассмотрим на примере задачи классификации такой метод, как дистилляция. Когда мы обучаем большую нейронную сеть, мы, по сути, аккумулируем гигантское количество информации про ту или иную предметную область. Вся эта информация зашита где-то в глубинных слоях этой нейронной сети и позволяет ей принимать какие-то решения. Однако, сами эти знания, которые находятся внутри нейросети, мы практически никогда не используем. Нас чаще всего интересует конкретный ответ данной сети на конкретный вопрос, то, как она “думала” и как она пришла к этому ответу нас скорее всего не интересует. Если же взглянуть на маленькие нейронные сети, их количество параметров может просто не позволить этим моделям хорошо обучиться и понять структуру предметной области, поэтому они обучаются хуже и дают хуже качество с точки зрения целевой метрики. Возникает логичная мысль: а можем ли мы как-то использовать все эти скрытые знания, которые зашиты внутри большой модели (внутри модели учителя), и передать их в маленькую модель (в модель ученика)?
Давайте посмотрим на последний слой какой-либо нейронной сети. Чаще всего мы можем увидеть там какой-то полносвязный слой, который дальше передается в функицию SoftMax, которая превращает эти выходы в вероятности ответов. На рисунке 1.15 представлена визуализация этого процесса.
\begin{figure}[H]
\begin{center}
\includegraphics[width=\linewidth]{\picPath/distil_1.png}
\caption{Визуализация последнего слоя нейронной сети и операции SoftMax}
\label{fig:distil_1}
\end{center}
\end{figure}
В данном конкретном случае у нас есть какая-то нейронная сеть и три класса, и нейронная сеть выдает три числа, которые являются вероятностями принадлежности к тому или иному классу. Видно, что так как для класса А — вероятность составляет 0.95, то есть на 95\% процентов нейронная сеть уверена, что это класс А.
Однако, стоит заметить, что и про класс B и C — она тоже что-то “сказала”. А именно, что входные данные похожи так же на класс Б, но всего лишь на четыре процента, и на класс C — на один процент. Именно это и можно назвать так называемой интуицией модели, то есть то, как она вообще смотрит на текущий пример данных и как она думает, с какой вероятностью представлен тот или иной класс.
Эту информацию можно попробовать передавать маленькой нейронной сети вместе с правильными ответами. Таким образом, мы можем надеяться, что добавление подобной информации, выходного распределения для всех классов от большой модели, которая хорошо понимает данные, в маленькую — позволит ей быстрее сориентироваться в том пространстве данных, которое мы ей передаем, и лучше и быстрее обучиться.
Остается последний вопрос: как технически это реализовать? Мы должны сделать более сложную функцию ошибки, которую нам потребуется минимизировать в процессе обучения. У нас есть размеченные данные, на которых мы посчитаем стандартную ошибку классификации посредством кросс-энтропии и просто к этой ошибке (через оператор плюс) мы добавляем другую ошибку — ошибку дистилляции. В качестве ошибки дистилляции берут расстояние (расхождение, дивергенция) Кульбака — Лейблера. Для дискретных вероятностных распределений $P$ и $Q$ с числом элементарных событий n данное расстояние можно представить как (1.13).
\begin{equation}
D_{KL}(P||Q)=\sum_{i=1}^{n}p_ilog\frac{p_i}{q_i}
\end{equation}
где\ \ $P$ – постулируемое априорное распределение\\
$Q$ – предполагаемое, проверяемое распределение
Значение функционала можно понимать как количество неучтённой информации распределения $P$, если $Q$ было использовано для приближения $P$. Данная мера расстояния в теории информации также интерпретируется как величина потерь информации при замене истинного распределения $P$ на распределение $Q$.
Нужно конечно еще добавить, что есть некоторые нюансы, которые также нужно учитывать. Если у нас очень мощная модель и мы ее очень хорошо натренировали — скорее всего она будет слишком уверена в своих ответах, такая проблема в литературе называется overconfidence, она является прямой причиной переобучения. Другими словами, если мы дадим какое-то изображение сети и она выдает, что с вероятностью 99\% или даже с вероятностью 100\% — это картинка класса А, по сути она выдает практически те же результаты, что и просто правильный ответ из размеченных данных. Какой-то дополнительной информации здесь можно не получить, потому что она не будет отличаться от того, что мы и так подаем на вход нашей маленькой сети.
Для предотвращения данной проблемы есть такой подход, как нагретый SoftMax. В целом это самый обычный SoftMax с одним исключением: мы все входные данные дополнительно делим на параметр $T$ (на параметр температуры), как показано в уравнении (1.14).
\begin{equation}
\frac{exp(\frac{z_i}{T})}{\sum_{j}^{}exp(\frac{z_j}{T})}
\end{equation}
где\ \ $z_i$ – это логитсы (выходы) модели\\
$T$ – температура
Данный прием позволяет сгладить выходное распределение моделей и из почти one-hot распредления получить более мягкое. Например, у нас есть нейронная сеть, которая выдает результаты 98\% для класса А и всего 2\% — для класса В, и это можно считать слишком уверенным ответом. Теперь разделим все выходы до SoftMax на параметр t (в данном примере t равен 5) и видим, что распределение ответов немножко смягчилось, то есть вероятностное распределение распределилось по другим классам. На рисунке 1.16 представлена визуализация действия нагретого SoftMax.
Ошибку для правильных ответов, на размеченных данных, мы оставляем, как есть, а ошибку дистилляции, то есть когда мы будем сравнивать распределение сети-студента и сети-учителя, мы будем прогонять дополнительно через нагретый SoftMax, для более глубокого обмена скрытой в более ресурсоемкой модели информации. Важно отметить, что нагретый SoftMax нужно использовать и для сети-учителя и для сети-ученика для вычисления функции потерь дистилляции.
В этом и заключается базовая идея дистилляции, однако ее можно дополнительно модифицировать, чтобы достигать какого-то лучшего качества и, может быть, экспериментировать с различными другими подходами. Например, мы можем дистиллировать не только последний слой, а еще какой-то более глубокий.
Пусть есть какая-то одна нейросеть и другая нейросеть поменьше, они имеют согласованную структуру, то есть обе представители одной архитектуры, просто вторая физически меньше, то мы можем пытаться сравнивать точно таким же образом через нагретый SoftMax какие-то более глубокие слои в первой и во второй нейросети. На рисунке 1.17 можно видеть схематичную визуализацию этого процесса.
\begin{figure}[H]
\begin{center}
\includegraphics[width=0.8\linewidth]{\picPath/distil_2.png}
\caption{Визуализация действия нагретого SoftMax}
\label{fig:distil_2}
\end{center}
\end{figure}
\begin{figure}[H]
\begin{center}
\includegraphics[width=0.8\linewidth]{\picPath/distil_3.png}
\caption{Визуализация дистиллирования знаний с более глубоких слоев}
\label{fig:distil_3}
\end{center}
\end{figure}
Также можно применить этот подход к автоматической разметки данных следующим образом: пусть у нас есть уже какие-то размеченные данные и на них мы классически обучаем нашу модель. Однако могут быть еще и какие-то неразмеченные данные, они просто лежат и для того, чтобы их как-то задействовать, в цикле разработки продукта в компании мы бы попросили какой-то отдел разметки, чтобы они разметили эти данные, передали нам и мы их добавили в обучающую выборку. Мы могли бы попытаться использовать “знания” большой ресурсоемкой модели для того, чтобы добавить новые данные в обучающую выборку. Тут нужно тоже отдельно упомянуть, что так как наша модель все-таки не идеальна и она не может прямо со стопроцентной точностью разметить данные, то не нужно бинализировать ее ответы, стоит использовать мягкое распределение через нагретый SoftMax. На рисунке 1.18 схематично представлен алгоритм обучения на таких полуразмеченных данных.
\begin{figure}[H]
\begin{center}
\includegraphics[width=\linewidth]{\picPath/distil_4.png}
\caption{Визуализация обучения на полуразмеченных данных}
\label{fig:distil_4}
\end{center}
\end{figure}
Таким образом, используя дистилляцию, мы можем улучшить качество и скорость обучения маленькой сети, чтобы она могла быть, с одной стороны, компактной, но при этом “понимала” про данные так же много, как и более большая модель. К тому же подход дистилляции знаний может помочь при дополнительной мягкой разметки данных.
Рассмотрим, как применить процесс дистилляции знаний на практике. Наша задача взять более большую, уже предобученную модель на данных и дистиллировать знания в маленькую с точки зрения параметров сеть.
\subsection{Программная реализация дистилляции знаний}
Для примера программной реализации возьмем предоубученную нейронную сеть с более чем 2 миллионами параметров (2 669 248 параметров) для распознавания изображений. Эта модель была обучена на датасете Fashion MNIST и мы можем использовать полученные веса для дистилляции. Fashion-MNIST - это набор изображений статей Zalando, состоящий из обучающего набора из 60 000 примеров и тестового набора из 10 000 примеров. Каждый пример представляет собой черно-белое изображение с разрешением 28x28 пикселей. Всего представленно 10 классов. Мы будем использовать Fashion-MNIST для обучения нейронных сетей в данной курсовой работе, так как это небольшой по своим размерам датасет, на котором можно быстро получить результаты, а также, считается, что это более сложная задача, чем классический MNIST. Пример изображения одного из классов, а также весь код представлен в приложении А к курсовой работе.
В качестве сети-ученика возьмем сеть с двумя полносвязанными слоями с 20 нейронами: 139-20, 20-10. Посчитав количество параметров сети-ученика видим, что модель гораздо меньше первой и имеет 3.5 тысячи параметров. Также время инференса для нее значительно меньше, примерно в 3 раза. Далее посмотрим, какое качество модель выдает сама по себе. Точность составляет 82.7\%, в то время как крупная модель показывает метрику 91.2\%. Основная задача - дистрилировать знания из этой модели в более компактную модель.
Напишем функции тренировки и валидации, стандартные для фреймворка Pytorch. Мы объявим функцию \texttt{error\_and\_output} которая задает особую функцию Дивергенции Кульбака-Лейблера. Дивергенция Кульбака-Лейблера нужна, чтобы подсчитать кросс-энтропию между двумя распределениями. А именно между распределениями ответов модели-учителя и модели-ученика. После подсчета логитсов моделей учителя и ученика рассчитываем распределение вероятностей ответов сетей с помощью softmax с параметром T (нагретый софтмакс) для сети-ученика и для сети-учителя. В конце складываем ошибку ученика и взаимную кросс энтропию с соответсвующим коэффициентом альфа. Следует отметить, что этот коэффициент, а также температура получены эмпирическим путем посредством экспериментов.
Обучим нашу сеть-ученика, пользуясь знаниями сети-учителя. После тренировки видим, что что применяя дистилляцию знаний, нам удалось повысить качество маленькой модели на несколько процентов, при этом сохранив ее компактность. В таблице 1.1 представлена сводная информация по полученным метрикам на данных FashionMNIST
\begin{table}[H]
% Подпись таблицы
\caption{Результаты дистилляции знаний}
% Ссылка на таблицу
\label{table_1.1}
\begin{tabularx}{\textwidth}{|X|X|} % Столько X, сколько столбцов
\hline
Модель & Точность, \% \\ \hline
BigNet & 91.2 \\
ScNet baseline & 82.7 \\
ScNet distilled & 84.1 \\ \hline
\end{tabularx}
\end{table}
\chapter{Прунинг и прореживание нейронных сетей}
\textbf{Отсечение сети} - важный метод как для уменьшения объема памяти, так и для оптимизации ее пропускной способности. В начале 1990-х годов были разработаны методы отсечения, чтобы сжать обученную большую сеть в меньшую сеть без необходимости переобучения. Это позволило развертывать нейронные сети на мобильных и ограниченных платформах. При прунинге удаляются избыточные параметры или нейроны, которые не вносят существенного вклада в точность результатов. Различные методы прунинга предлагают как именно выбрать это условие по которому веса классифицируются как ненужные и избыточные.
Так как мы убираем некоторую часть весов это снижает вычислительную сложность алгоритма. Многие исследования подтверждают, что если сокращенные сети повторно обучить, то это может позволить избежать предыдущих локальных минимумов, возникших при тренировке, которые не позволяют сети достичь максимальной точности. Исследования по сокращению сети начались в начале 1990-х годов и были разделены на методы расчета чувствительности и так называемые иметоды штрафных санкций (penalty-term methods).
Сейчас наблюдается значительные интерес к данной теме и последние работы в этой области показывают улучшения как для отдельных категорий сокращения сети, так и для их дальнейшей комбинации. Современные методы отсечения можно классифицировать по различным аспектам, включая:
\begin{itemize}
\item структурированное и неструктурированное отсечение в зависимости от того, является ли отсекаемая сеть симметричной или нет
\item отсечение целых нейронов или связей между нейроными
\item статическое и динамическое отсечение.
\end{itemize}
На рисунке 2.1 показаны различия в обработке статической и динамической обрезки. При статическом прунинге все этапы выполняются в автономном режиме до инференса сети, в то время как динамическое сокращение выполняется во время обучения. Несмотря на то, что категории частично совпадают, в этой главе мы будем использовать статическое и динамическое отсечение для классификации методов прунинга сети.
\begin{figure}[H]
\begin{center}
\includegraphics[width=\linewidth]{\picPath/pruning_1.png}
\caption{Схемы статического и динамического отсечения}
\label{fig:pruning_1}
\end{center}
\end{figure}
\begin{figure}[H]
\begin{center}
\includegraphics[width=0.8\linewidth]{\picPath/pruning_2.png}
\caption{Различные виды отсечения}
\label{fig:pruning_2}
\end{center}
\end{figure}
Отсечение может происходить по элементам, строкам, по столбцам, по фильтрам или по слоям. На рисунке 2.2 показаны возможные варианты отсечения. Обычно применение element-wise подхода оказывает наименьшее влияние на разреженность сети. На рисунке разреженность сети уменьшается слева направо.
Независимо от типа прунинга, данную операцию можно описать математически как показано в уравнении 2.1:
\begin{equation}
\arg \min_{p} L = N(x;W) - N_p(x;W_p)
\end{equation}
где $N_p(x;W_p) = P(N(x;W))$\\
$ P(\cdot) $ - функция прунинга\\
$L$ представляет собой некоторую функцию меры точности нейронной сети \\
$N$ - исходная нейронная сеть\\
$W$ - исходные веса нейронной сети\\
$N_p$ - нейронная сеть после прунинга
Задача любого подхода к отсечению сетей это поиск такой функции прунинга $P(.)$, чтобы минимизировать потерю в качестве после отсечения и при этом как можно лучше проредить сеть.
\section{Статический прунинг}
\textbf{Статический прунинг} – это метод оптимизации сети, при котором удаляются ее параметры после обучения и до развертывания этой сети у клиента. Во время инференса никакого дополнительного прунинга не производится.
Статический прунинг, как правило, имеет 3 части:
\begin{itemize}
\item Выбор параметров для отсечения по какому-то критерию
\item Выбор метода отсечения
\item Опционально, дотренировка сети после этой операции
\end{itemize}
Дотренировка сети может увеличить получаемое качество сети по сравнению с оригинальной версией, но и может потребовать значительных ресурсов и времени для последующей тренировки.
\subsection{magnitude-based pruning}
Один из первых методов обрезки сетей – это метод перебора. В этом методе выполняется поэлементный обход всей сети и удаление тех весов, которые не влияют на точность.
Недостатком такого подхода, естесственно, является большая временная сложность данного алгоритма и большое пространство решений. Типичная метрика для определения какие параметры можно обрезать является $l_p$ норма. $l_p$ норма вектора, который содержит $n$ элементов можно выразить математически как показано в уравнении (2.2):
\begin{equation}
\left \|X_p \right \| = (\sum_{i=1}^{n}\left | x_i \right |^p)^{\frac{1}{p}}
\end{equation}
Такой подход называется magnitude-based. Было предложено и широко признано, что натренированные веса с большими значениями более важны, чем натренированные веса с меньшими значениями. Это наблюдение является ключом к данному методу.
Методы отсечения, основанные на величине, стремятся идентифицировать ненужные веса или функции, чтобы удалить их при применении сети для задачи. Ненужные значения могут быть удалены либо в ядре свертки, либо на карте активаций. Самый интуитивно понятный метод отсечения, основанный на величине - это отсечение всех весов с нулевым значением или всех весов в пределах некоторого порога по абсолютному значению.
Ян Лекун еще в 1990 году предложил метод оптимального повреждение мозга (OBD) для удаления отдельных несущественных весов. Используя вторую производную (матрицу Гессе) функции потерь, этот метод статической обрезки снизил параметры сети на четверть. Для упрощенного вычисления производной, функция OBD рассматривалась при трех предположениях:
\begin{itemize}
\item квадратичная - функция стоимости почти квадратичная
\item экстремальная – обрезка делается после схождения сети
\item диагональная – суммирует отдельные производные весов как результат их совместного следствия
\end{itemize}
Это исследование также показало, что разряжение нейронной сети может предоставить возможности для повышения ее производительности. Позднее был предложен метод Optimal Brain Surgeon (OBS), расширенная OBD с аналогичным методом вычисления производной второго порядка, но без диагонального допущения в OBD. OBS рассматривает матрицу Гессе как недиагональную для большинства случаев. OBS повысил точность прунинга с 90\% сокращением, особенно для сетей XOR.
Эти ранние методы уменьшили количество связей между нейронная на основе второй производной функции потерь. Они также предложили методы, основанные на методе матрицы Гессе, при которых такая обрезка сети будет показывать более высокую точность, чем magnitude based прунинг. Но современные сети имеют намного больше праметров, чем сети в те времена. Например, GPT-3 содержит 175 миллиардов параметров, тогда как VGG-16 содержит 133 миллиона. Вычисление матрицы Гессе во время тренировки потребует вычислительной сложности порядка $О(W^2)$ и для GPT-3 такая сложность недосягаема на текущий момент. Именно поэтому сейчас используют в основном различные алгоритмы основанные на величиние значений нейронной сети.
Давайте посмотрим на конкретный пример: пусть у нас есть нейросеть и нам нужно понять, какие здесь веса лишние, а какие нужно оставить и они будут отвечать за корректную работу алгоритма. Для того, чтобы думать о том, какой из весов является важным, а какой нет, можно смотреть на очень простой критерий: можно смотреть на модуль коэффициентов, привязанных к конкретной связи.
\begin{figure}[H]
\begin{center}
\includegraphics[width=0.7\linewidth]{\picPath/pruning_3.png}
\caption{Пример полносвязанной нейронной сети}
\label{fig:pruning_3}
\end{center}
\end{figure}
На схематичном изображении 2.3 приведена простая полносвязанная сеть, у этой сети есть три связи с коэффициентами: 25, - 13 и - 12. Это довольно большие значения относительно всех представленных весов в этой сети и поэтому ожидается, что если мы их удалим,то точность сильно просядет и сеть перестанет хорошо выполнять поставленную перед ней задачу.
Однако в этой же сети есть и другие связи, например, с коэффициентом -0.2 и 0.3.
Эти значения относительно не очень большие, поэтому можно ожидать, что если мы удалим их сети, то есть заменим на 0, то не очень много что поменяется в работе сети и она все также будет хорошо предсказывать изначальный ответ, однако уже без лишних параметров.
Итак, будем удалять из нашей нейросети веса с наименьшим значением по модулю; в данном конкретном примере возьмем вес с коэффициентом - 0.2 и так, как он самый маленький по модулю, удалим его. Однако стоит отметить, что такое удаление весов не пройдет бесследно и мы можем ожидать небольшую потерю в качестве. Поэтому, чтобы восстановить предсказательную способность нашей сети, применяют процесс дотренировки на оставшихся параметрах. Мы ожидаем, что сеть все-таки уже хорошо обучена, и мы не будем очень долго ее дообучать и проведем несколько эпох для того, чтобы оставшиеся связи как бы взяли на себя ту роль, которая лежала на удаленных коэффициентах.
После того как мы дообучили оставшуюся нейросеть, мы можем продолжать наш алгоритм, то есть продолжать удалять веса с наименьшим коэффициентом. Удаляем снова и после этого проводим еще один этап дообучения нейросети. Таким вот образом мы будем удалять веса и восстанавливать качество дообучением до тех пор, пока качество не будет падать драматически.
Такой алгоритм называется итеративным прунингом. Однако если мы будем удалять только по одному весу — это будет очень долгий процесс. Вместо этого можем использовать более грубый подход: можем удалять, например , по 10 процентов сети. Тогда за девять шагов мы бы могли теоретически удалить 90 процентов сети, не потеряв сильно в качестве. Весь алгоритм представлен на изображении 2.4.
Мы рассмотрели возможный алгоритм с element-wise подходом. Как уже было сказано выше у этого алгоритма также есть вариации. Мы можем прореживать веса целиком в сети, а можем делать это конкретно по слоям, то есть смотреть не на самый маленький коэффициент по абсолютному значению внутри всей сети, а на самый маленький в конкретном слое. Можно удалять не какие-то отдельные элементы, а целые нейроны, слои или фильтры (filter pruning). При таких подходах в основном используется $L_p$ норма (в частности $L_1$) для удаления фильтров, которые не влияют на точность классификации, а также удаляются связанные с этим фильтром карты характеристик. Такая работа приводит к снижению затрат на инференс на 34\% для VGG-16 и на 38\% для ResNet-110 при этом увеличивая точность на 0.75\% и 0.02\% соответственно при экспериментах на CIFAR-10. Алгоритм же выбора значимых или не значимых нейронов можно схематично изобразить как показано на рисунке 2.5.
Большинство методов отсечения сетей предпочитают измерение веса, а не активаций для критерия прунинга. Однако активации также могут быть индикатором для сокращения соответствующих весов. Средний процент нулей (APoZ) был введен, чтобы судить, влияет ли та или иная выходная карта активации на конечный результат. Некоторые функции активации, в частности функции Rectified linear utit (ReLU) могут приводить к высокому проценту нулей в активациях и, таким образом, поддаваться к прунингу.
\begin{figure}[H]
\begin{center}
\includegraphics[width=\linewidth]{\picPath/pruning_4.png}
\caption{Схемы статического и динамического отсечения}
\label{fig:pruning_4}
\end{center}
\end{figure}
\begin{figure}[H]
\begin{center}
\includegraphics[width=\linewidth]{\picPath/pruning_5.png}
\caption{Схемы статического и динамического отсечения}
\label{fig:pruning_5}
\end{center}
\end{figure}
Уравнение 2.3 демонстрирует определение APoZ для $c$-го нейрона на $i$-ом слое.
\begin{equation}
APoZ_c^{(i)} = APoZ(O_c^{(i)}) = \frac{\sum_{k=0}^{N}\sum_{j=0}^{M}f(O_{c,j}^{(i)}(k)=0)}{N \times M}
\end{equation}
где $O_c^{(i)}$ - обозначает активацию\\
$N$ - количество валидационных изображений\\
$M$ - пространственное разрешение карты активации\\
$f(true) = 1;\ f(false) = 0$
\subsection{Regularize-based sparsity}
Выполнение отсечения по фильтрам с порогом из суммы абсолютных значений будет напрямую использовать преимущество структурной сети. Таким образом, коэффициент сокращения сети положительно коррелирует с процентом нулей в ядре свертки, который может быть повышен с помощью наложенных ограничений на веса в процессе обучения, эта техника известна в литературе как регуляризация. Исходя из этого строится прунинг на основе штрафа. При прунинге на основе штрафов цель состоит в том, чтобы изменить функцию ошибок или добавить дополнительные ограничения, известные как условия смещения (bias terms), в процесс обучения.
Штрафное значение используется для обновления некоторых весов до нулевых или близких к нулю значений. Затем эти значения удаляются. В частности существует метод, исследованный в конце 80х, который использует уменьшение веса при обратном распространении ошибки, чтобы определить, нужно ли обрезать нейрон. Веса с наименьшим значением заменяются нулями. Остаточные нулевые веса после обучения затем используются для отсечения ненужных нейронов.
Позднее в качестве штрафа было введено LASSO. LASSO уменьшает соответствующие веса наименее значимых признаков, это также ведет к увеличению разреженности. Поэлементное сокращение может привести к неструктурированной сетевой организации. Это приводит к разреженным матрицам весов, которые неэффективно выполняются на процессорах. Кроме того, их обычно трудно сжать или ускорить без специализированной аппаратной поддержки.
Group LASSO смягчает эту неэффективность, используя метод структурированной отсечения, который удаляет целые группы нейронов, сохраняя структуру организации нейронной сети. Групповой LASSO предназначен для того, чтобы все переменные, отсортированные в одну группу, можно было либо включить, либо исключить как единое целое. Уравнение (2.4) дает критерий для отсечения весов посредством этого метода
\begin{equation}
\arg \min_{\beta\in R^p}\left \{ \left \| y - \sum_{j=1}^{J}X_j\beta_j \right \|^{2}_2 + \lambda \sum_{j=1}^{J}\left \| \beta_j \right \|_{K_j} \right \}
\end{equation}
Веса разделены на несколько групп. Ненужные группы весов удаляются с помощью выбора функций LASSO. Группы могут определяться на основе геометрии, вычислительной сложности, разреженности групп и т.д. Можно найти примеры исследований, в котором разреженность групп в направлении строк или столбцов (структурное разрежение) может использоваться для сокращения времени выполнения GEMM. SSL (structured sparsity learning) показало улучшенное время инференса на AlexNet как на CPU, так и на GPU в 5,1 раз и в 3,1 раза соответственно.
\subsection{Variational dropout}
Dropout хоть и не является специальным методом сокращения сетей, снижает количество параметров. Первоначально он был разработан как стохастический регуляризатор, чтобы избежать чрезмерного переобучения на данных. Метод случайным образом прореживает процент нейронов, обычно до 50\%. Эта операция dropout разрывает часть связей между нейронами, чтобы избежать коадаптации. Dropout также можно рассматривать как операцию, которая отдельно обучает множество подсетей и берет их среднее значение на этапе вывода. Dropout увеличивает расходы на обучение, но не влияет на время инференса. На изображении 2.6 представлено действие dropout на нейронную сеть.
\begin{figure}[H]
\begin{center}
\includegraphics[width=\linewidth]{\picPath/pruning_6_1.png}
\caption{Действие dropout}
\label{fig:pruning_6}
\end{center}
\end{figure}
Variational dropout добавил гиперпараметр, называемый процентом выпадения, чтобы уменьшить вес сетей, подобных VGG, в 68 раз. Во время тренировки этот гиперпараметр можно использовать для определения отдельных весов для прунинга. Это также может быть применено с другими подходами к сжатию для дальнейшего снижения веса.
\section{Динамический прунинг}
Отсеченные веса обычно невозможно восстановить. Это может привести к снижению пропускной способности сети. Восстановление утраченных возможностей сети требует значительного повторного обучения. Глубокое сжатие потребовало бы миллионов итераций для обучения сети заново. Чтобы избежать этого недостатка, во многих подходах используются восстанавливаемые алгоритмы прунинга. Отсеченные элементы также могут быть задействованы в последующем процессе обучения и подстраиваться под сокращенную сеть.
Существует восстанавливаемый метод прунинга с использованием матриц двоичных масок, чтобы идентифицировать, сокращается ли текущее значение веса или нет. Отсеченные веса с нормой L1 могут быть стохастически возвращены обратно в сеть. Используя этот подход, AlexNet удалось уменьшить в 17,7 раза без потери точности. Итерации повторного обучения были значительно сокращены до 14,58\% от глубокого сжатия. Однако этот тип отсечения по-прежнему приводит к асимметричности сети, усложняющей аппаратную реализацию.
Хотя метод восстановления был в какой-то мере исследован, статический прунинг навсегда разрушает исходную структуру сети, что может привести к снижению возможностей модели. После удаления и повторного обучения подход статического удаления элементов сети не может восстановить уничтоженную информацию. Кроме того, наблюдения показывают, что важность связывания нейронов не зависит от входных данных.
Динамическое отсечение выполняется во время обучения, принимается решение какие слои, каналы или нейроны не будут участвовать в дальнейшей деятельности нейронной сети. Динамический прунинг может преодолеть ограничения статического прунинга за счет изменения входных данных, потенциально сокращая вычисления и пропускную способность. Динаический прунинг обычно не требует дотренировки. На рисунке 2.7 можно видеть обзор систем динамического отсечения. Самая важная часть - это система принятия решений, определяющая, какую часть обрезать. Связанные с этим вопросы:
\begin{enumerate}
\item Тип компонентов решения:
\begin{itemize}
\item Дополнительные соединения, прикрепленные к исходной сети, используемые на этапе вывода и / или фазы обучения
\item Характеристики соединений, которые могут быть изучены стандартными алгоритмами обратного распространения ошибки
\item Сеть побочных решений, которая имеет тенденцию работать хорошо, но ее часто трудно обучить
\end{itemize}
\item Уровень (форма) отсечения. Выбранный уровень обрезки влияет на тонкости разработки под конкретное железо:
\begin{itemize}
\item По каналам
\item По слоям
\item По блокам
\item По сети.
\end{itemize}
\item Входные данные:
\begin{itemize}
\item Однократная подача информации: подает весь датасет в систему принятия решений
\item Послойная подача информации, где окно данных итеративно подается в систему принятия решений паралельно с форвардом сети.
\end{itemize}
\item Вычисление критерия решения:
\begin{itemize}
\item $L_p$-норма
\item Другие подходы
\end{itemize}
\item Сравнение оценок:
\begin{itemize}
\item Человеческий опыт / результаты экспериментов
\item Автоматический порог
\item Динамические механизмы
\end{itemize}
\item Критерии остановки:
\begin{itemize}
\item В случае послойного и отсечения по всей сети, некоторые алгоритмы отсечения намеренно оставляют слой / сеть неотсеченным
\item Некоторые алгоритмы динамически выбирают по какому пути пропускать данные
\item Другие же алгоритмы завершают вычисление и выводят результат прогноза. В этом случае оставшиеся слои считаются обрезанными.
\end{itemize}
\item Обучение компонента принятия решения:
\begin{itemize}
\item Присоединенные соединения могут быть обучены вместе с исходной сетью
\item Вспомогательные сети обычно обучаются с использованием алгоритмов обучения с подкреплением (RL).
\end{itemize}
\end{enumerate}
\begin{figure}[H]
\begin{center}
\includegraphics[width=\linewidth]{\picPath/pruning_6.png}
\caption{Обзор систем динамического отсечения}
\label{fig:pruning_7}
\end{center}
\end{figure}
Для процессоров карты функций или количество фильтров, используемых для идентификации объектов, составляют большую часть использования вычислительной эффективности и полосы пропускания памяти - особенно для depth-wise или point-wise сверток. Динамическая настройка также может применяться к статически отсеченным сетям, что потенциально дополнительно снижает требования к вычислениям и уменьшает перессылку из памяти в память.
Недостатком динамического сокращения является то, что критерии для определения элементов, подлежащих сокращению, должны вычисляться во время выполнения. Это увеличивает накладные расходы на систему, требуя дополнительных вычислений,памяти и мощности. Следует учитывать компромисс между динамическими затратами на прунинг, сокращением вычислений в сети и потерей точности. Один из методов для устранения этих проблем запрещает вычисления с нулевыми параметрами в обрабатывающем элементе (PE).
Современные подходы также предлагают более продвинутый способ понимания того, какие элементы нужно удалить, а какие нет. Одна из недавних работ позволяет делать это с помощью других нейросетей и подхода обучения с подкреплением, то есть авторы предлагают умно оценивать какие-то значимости внутри одной большой нейронной сети с помощью какой-то другой модели машинного обучения, которая может выучивать это из тех данных, которые поставляет оригинальная нейросеть.
Таким образом, мы хотим построить какую-то одну модель, которая будет смотреть на другую и удалять уже в автоматическом режиме из нее лишние элементы. Один из таких подходов называется AutoML for Model Compression (AMC). На рисунке 2.8 изображена схема этого алгоритма.
\begin{figure}[H]
\begin{center}
\includegraphics[width=\linewidth]{\picPath/pruning_7.png}
\caption{AutoML for Model Compression}
\label{fig:pruning_7}
\end{center}
\end{figure}
Идея состоит в том, что дополнительная модель смотрит на рассматриваемую нейронную сеть и пытается удалить какие-то элементы. После этого мы запускаем оригинальную нейронную сеть и смотрим, какое качество получилось. То какое качество получилось, некоторую ее меру, мы возвращаем обратно в функцию потерь для обучения нашей вспомогательной модели. То есть простыми словами эта модель смотрит на то, хорошо у нее получилось удалить элементы или плохо, и на основе этого обучается понимать, какие элементы нужно удалять из рассматриваемой сети, а какие элементы удалять не нужно.
В оригинальной статье авторы также приводят результаты того, как работает их подход: на некоторых популярных архитектурах сжатие достигает 70\%, при этом без потери какой-либо точности. В таблице 2.1 можно видеть полученные результаты применения этого алгоритма к архитектуре MobileNet
\begin{table}[H]
% Подпись таблицы
\caption{Результаты прунинга методом обучения с подкреплением}
% Ссылка на таблицу
\label{table_1}
\begin{tabularx}{\textwidth}{|X|X|X|} % Столько X, сколько столбцов
\hline
Модель & Точность (\%) & Время работы (мс) \\ \hline
MobileNetV1 & 70.9 & 123 \\
MobileNetV1 - 50\% & 70.5 & 68.9 \\
MobileNetV2 - 70\% & 70.9 & - \\ \hline
\end{tabularx}
\end{table}
Чтобы соответствовать вычислительным ограничениям, многомасштабные плотные сети (MSDNets) были спроектированы как адаптивная сеть, включающая два метода, один из которых – возможность генерации результатов прогнозирования во многих узлах, что позволяет ранний выход из сети; другой – ограниченный бюджет вычислений для обеспечения выхода для более простого инференса (на более простых примерах) на раннем этапе, таким образом сохраняя вычислительные ресурсы для сложного инференса более сложных примеров. Инновационная технология MSDNets объединяет многомасштабные карты характеристик и плотную связь для обеспечения раннего выхода при сохранении более высокой точности. Классификаторы дифференцируемы, поэтому MSDNets можно обучать с использованием стохастического градиентного спуска. MSDNets обеспечивает ускорение в 2,2 раза при той же точности, что и ResNet-50, в наборе данных ImageNet.
По итогу можно сказать, что не все параметры, которые существуют в нейронной сети,
одинаково важны для ее работы. Некоторые из этих параметров не являются необходимыми и важными для принятия решения. В целом, некоторые исследования показывают, что используя итеративное прореживание на основе прунинга фильтров и нейронов, мы можем удалять мало значимые элементы по некоторому критерию и делать нашу модель более компактной, быстрой и более осознанно решать поставленную задачу, при этом особо не уступая в производительности и эффективности сложным алгоритмам динамического прунинга.
\section{Программная реализация прореживания нейронной сети}
Для программной реализации прореживания была выбрана сеть ResNet18, адаптированная под низкое разрешение и решающая задачу классификации 10 классов. Данная сеть разработана компанией Microsoft. Сеть имеет 11.16 млн параметров и представляет собой последовательный набор Res блоков c cоединениями быстрого доступа (shortcut connections). На рисунке 2.9 схематически изображен блок такой архитектуры. Весь код был реализован с помощью низкоуровнего фреймворка для глубокого обучения PyTorch и представлен в приложении Б.
\begin{figure}[H]
\begin{center}
\includegraphics[width=0.8\linewidth]{\picPath/pruning_8.png}
\caption{Res блок}
\label{fig:pruning_8}
\end{center}
\end{figure}
\subsection{Глобальное прореживание нейронной сети (спарсификация)}
Для того, чтобы начать оптимизировать размер сети, нам нужен инструментарий для удаления связей внутри нашей модели. В приложении Б представлена возможная реализация оберток на полносвязанный линейный и сверточный 2d слои. Идея прореживания в том, чтобы сгенирировать определенным образом бинарную маску, регулирующую какие веса мы оставляем, а какие будем отключать. Далее мы перемножим маску с весами слоя,оставляя самые важные, исходя из определенного правила при генерации этой маски.
Функция $weight\_sparse$ реализует алгоритм генерации такой маски исходя из абсолютного значения - задавая пороговое значение, вычисляем персентиль и далее зануляем только те веса, которые меньше этого значения.
Алгоритмы прунинга достаточно востребованы и поэтому в современных фреймворках уже представлен набор инструментов позволяющий проводить операцию прунинга. В методе класса нейронной сети $prune\_unstructured$ мы пробегаем по всем слоям нашей сети и применяем функцию прунинга. Метод $calc\_weights$ позволяет посчитать количество весов с учетом сгенерированных для прунинга масок. Далее реализуем простую функцию тренировки сети на тренировочных данных и функцию для тестирования на валидационных данных. Реализация этих функций поддерживает обучение и тестирование как на ЦПУ, так и на ГПУ. Далее подготовим данные для тренировки и валидации. Для наших целей будем использовать тот же набор данных, что и при реализации алгоритма дистиляции, датасет FashionMNIST, содержащий примрено 70 тысяч черно-белых изображений с разрешением 32х32.
Первоначально обучим нашу нейронную сеть на данных перед применением алгоритма прунинга и получим метрику 93.2\%. Попробуем применить спарсификацию на полученной модели напрямую, уберем 50\% и посмотрим на просадку в качестве. Точность модели просела на 2.3\%. Результат довльно слабый. Попробуем применить другой подход, сжать сеть еще больше и при этом не допустить сильной потери в качестве
Идин из способов сжатия нейронных сетей - итеративное прореживание (Incremental Magnitude Pruning). Он достаточно ресурсоемкий, однако позволяет достаточно несложными методами добиться неплохого результата.
Будем идти с шагом, каждый раз будем отключать внутри сети несколько десятков процентов связей. После отключения, оставшиеся веса дообучим на всех данных используя одну эпоху. Ожидается, что так как мы выкинули за один раз не очень много, то оставшиеся связи "перехватят" ответственность тех слабых, которые мы только что отключили.
Таким образом за P таких итераций мы выкинем желаемое количество сети и не должны при этом потерять сильно в качестве.
Мы составим расписание для сети в виде списка и напишем более умную функцию тренировки, как показано в приложении Б. В таблице 2.2 представлены результаты спарсификации нейронной сети этим и предыдущим методами.