
Для целей этой публикации мы сосредоточимся на стандартных сверточных слоях.
Результаты сверточных нейронных сетей
В 2012 году Alex Krizhevsky, Ilya Sutskever, и Geoff Hinton достигли существенного улучшения качества распознавания в сравнении с известными на тот момент решениями (Krizehvsky et al. (2012)).
Прогресс был результатом объединения нескольких подходов. Использовались графические процессоры для обучения большой (по меркам 2012 года), глубокой нейронной сети. Использовался новый тип нейронов (ReLU) и новая техника для уменьшения проблемы, называемой «overfitting» (DropOut). Использовали большой набор данных с большим количеством категорий изображений (ImageNet). И конечно же, это была сверточная нейронная сеть.
Архитектура, показанная ниже, была глубокой. Она имеет 5 сверточных слоев, 3 pooling с чередованием и три полносвязных слоя.
.

Сеть была обучена классификации фотографий в тысячи разных категорий.
Модель Крижевского и др. была способна дать правильный ответ в 63% случаев. Кроме того, правильный ответ из 5 лучших ответов, присутствует 85% прогнозов!

Проиллюстрируем, что узнает первый уровень сети.
Напомним, что сверточные слои были разделены между двумя графическими процессорами. Информация не идет назад и вперед по каждому слою. Оказывается, каждый раз, когда модель запускается, обе стороны специализируются.

Фильтры, полученные первым сверточным слоем. Верхняя половина соответствует слою на одном графическом процессоре, нижняя — на другом. From Krizehvsky et al. (2012)
Нейроны с одной сторону, фокусируются на черно-белом цвете, учась обнаруживать края разных ориентаций и размеров. Нейроны с другой стороны, специализируются на цвете и текстуре, обнаруживают цветовые контрасты и узоры. Помните, что нейроны случайным образом инициализируются. Ни один человек не пошел и не поставил их пограничными детекторами, или разделил таким образом. Это произошло при обучении сети классификации изображений.
Эти замечательные результаты (и другие захватывающие результаты примерно в то время) были только началом. За ними быстро последовало множество других работ, которые тестировали измененные подходы и постепенно улучшали результаты или применяли их в других областях.
Сверточные нейронные сети являются важным инструментом в компьютерном видении и современном распознавании образов.
Формализация сверточных нейронных сетей
Рассмотрим одномерный сверточный слой с входами
![]()
Свёрточная нейронная сеть (англ. convolutional neural network , CNN) — специальная архитектура искусственных нейронных сетей, предложенная Яном Лекуном в 1988 году [1] и нацеленная на эффективное распознавание образов [2] , входит в состав технологий глубокого обучения (англ. deep learning ). Использует некоторые особенности зрительной коры [3] , в которой были открыты так называемые простые клетки, реагирующие на прямые линии под разными углами, и сложные клетки, реакция которых связана с активацией определённого набора простых клеток. Таким образом, идея свёрточных нейронных сетей заключается в чередовании свёрточных слоёв (англ. convolution layers ) и субдискретизирующих слоёв (англ. subsampling layers или англ. pooling layers , слоёв подвыборки). Структура сети — однонаправленная (без обратных связей), принципиально многослойная. Для обучения используются стандартные методы, чаще всего метод обратного распространения ошибки. Функция активации нейронов (передаточная функция) — любая, по выбору исследователя.
Название архитектура сети получила из-за наличия операции свёртки, суть которой в том, что каждый фрагмент изображения умножается на матрицу (ядро) свёртки поэлементно, а результат суммируется и записывается в аналогичную позицию выходного изображения.
Содержание
Интерпретация [ править | править код ]
Работа свёрточной нейронной сети обычно интерпретируется как переход от конкретных особенностей изображения к более абстрактным деталям, и далее к ещё более абстрактным деталям вплоть до выделения понятий высокого уровня. При этом сеть самонастраивается и вырабатывает сама необходимую иерархию абстрактных признаков (последовательности карт признаков), фильтруя маловажные детали и выделяя существенное.
Подобная интерпретация носит скорее метафорический или иллюстративный характер. Фактически «признаки», вырабатываемые сложной сетью, малопонятны и трудны для интерпретации настолько, что в практических системах не особенно рекомендуется пытаться понять содержания этих признаков или пытаться их «подправить», вместо этого рекомендуется усовершенствовать саму структуру и архитектуру сети, чтобы получить лучшие результаты. Так, игнорирование системой каких-то существенных явлений может говорить о том, что либо не хватает данных для обучения, либо структура сети обладает недостатками и система не может выработать эффективных признаков для данных явлений.
Архитектура и принцип работы [ править | править код ]
![]()
![]()
В обычном перцептроне, который представляет собой полносвязную нейронную сеть, каждый нейрон связан со всеми нейронами предыдущего слоя, причём каждая связь имеет свой персональный весовой коэффициент. В свёрточной нейронной сети в операции свёртки используется лишь ограниченная матрица весов небольшого размера, которую «двигают» по всему обрабатываемому слою (в самом начале — непосредственно по входному изображению), формируя после каждого сдвига сигнал активации для нейрона следующего слоя с аналогичной позицией. То есть для различных нейронов выходного слоя используются одна и та же матрица весов, которую также называют ядром свёртки. Её интерпретируют как графическое кодирование какого-либо признака, например, наличие наклонной линии под определённым углом. Тогда следующий слой, получившийся в результате операции свёртки такой матрицей весов, показывает наличие данного признака в обрабатываемом слое и её координаты, формируя так называемую карту признаков (англ. feature map ). Естественно, в свёрточной нейронной сети набор весов не один, а целая гамма, кодирующая элементы изображения (например линии и дуги под разными углами). При этом такие ядра свёртки не закладываются исследователем заранее, а формируются самостоятельно путём обучения сети классическим методом обратного распространения ошибки. Проход каждым набором весов формирует свой собственный экземпляр карты признаков, делая нейронную сеть многоканальной (много независимых карт признаков на одном слое). Также следует отметить, что при переборе слоя матрицей весов её передвигают обычно не на полный шаг (размер этой матрицы), а на небольшое расстояние. Так, например, при размерности матрицы весов 5×5 её сдвигают на один или два нейрона (пикселя) вместо пяти, чтобы не «перешагнуть» искомый признак.
Операция субдискретизации (англ. subsampling , англ. pooling , также переводимая как «операция подвыборки» или операция объединения), выполняет уменьшение размерности сформированных карт признаков. В данной архитектуре сети считается, что информация о факте наличия искомого признака важнее точного знания его координат, поэтому из нескольких соседних нейронов карты признаков выбирается максимальный и принимается за один нейрон уплотнённой карты признаков меньшей размерности. За счёт данной операции, помимо ускорения дальнейших вычислений, сеть становится более инвариантной к масштабу входного изображения.
Рассмотрим типовую структуру свёрточной нейронной сети более подробно. Сеть состоит из большого количества слоёв. После начального слоя (входного изображения) сигнал проходит серию свёрточных слоёв, в которых чередуется собственно свёртка и субдискретизация (пулинг). Чередование слоёв позволяет составлять «карты признаков» из карт признаков, на каждом следующем слое карта уменьшается в размере, но увеличивается количество каналов. На практике это означает способность распознавания сложных иерархий признаков. Обычно после прохождения нескольких слоёв карта признаков вырождается в вектор или даже скаляр, но таких карт признаков становятся сотни. На выходе свёрточных слоёв сети дополнительно устанавливают несколько слоёв полносвязной нейронной сети (перцептрон), на вход которому подаются оконечные карты признаков.
Слой свёртки [ править | править код ]
![]()
Слой свёртки (англ. convolutional layer ) — это основной блок свёрточной нейронной сети. Слой свёртки включает в себя для каждого канала свой фильтр, ядро свёртки которого обрабатывает предыдущий слой по фрагментам (суммируя результаты поэлементного произведения для каждого фрагмента). Весовые коэффициенты ядра свёртки (небольшой матрицы) неизвестны и устанавливаются в процессе обучения.
Особенностью свёрточного слоя является сравнительно небольшое количество параметров, устанавливаемое при обучении. Так например, если исходное изображение имеет размерность 100×100 пикселей по трём каналам (это значит 30000 входных нейронов), а свёрточный слой использует фильтры c ядром 3×3 пикселя с выходом на 6 каналов, тогда в процессе обучения определяется только 9 весов ядра, однако по всем сочетаниям каналов, то есть 9×3×6 =162, в таком случае данный слой требует нахождения только 162 параметров, что существенно меньше количества искомых параметров полносвязной нейронной сети.
Слой активации [ править | править код ]
Скалярный результат каждой свёртки попадает на функцию активации, которая представляет собой некую нелинейную функцию. Слой активации обычно логически объединяют со слоем свёртки (считают, что функция активации встроена в слой свёртки). Функция нелинейности может быть любой по выбору исследователя, традиционно для этого использовали функции типа гиперболического тангенса ( f ( x ) = tanh ( x ) <displaystyle f(x)= anh(x)> , f ( x ) = | tanh ( x ) | <displaystyle f(x)=| anh(x)|>
) или сигмоиды ( f ( x ) = ( 1 + e − x ) − 1 <displaystyle f(x)=(1+e^<-x>)^<-1>>
). Однако в 2000х годах была предложена [4] и исследована [5] новая функция активации — ReLU (сокращение от англ. rectified linear unit ), которая позволила существенно ускорить процесс обучения и одновременно упростить вычисления (за счёт простоты самой функции) [6] , что означает блок линейной ректификации, вычисляющий функцию f ( x ) = max ( 0 , x ) <displaystyle f(x)=max(0,x)>
. То есть по сути это операция отсечения отрицательной части скалярной величины. По состоянию на 2017 год эта функция и её модификации (Noisy ReLU, Leaky ReLU и другие) являются наиболее часто используемыми функциями активации в глубоких нейросетях, в частности, в свёрточных. Существует методика определения оптимального числа блоков линейной ректификации [7] .
Пулинг или слой субдискретизации [ править | править код ]
![]()
Слой пулинга (иначе подвыборки, субдискретизации) представляет собой нелинейное уплотнение карты признаков, при этом группа пикселей (обычно размера 2×2) уплотняется до одного пикселя, проходя нелинейное преобразование. Наиболее употребительна при этом функция максимума. Преобразования затрагивают непересекающиеся прямоугольники или квадраты, каждый из которых ужимается в один пиксель, при этом выбирается пиксель, имеющий максимальное значение. Операция пулинга позволяет существенно уменьшить пространственный объём изображения. Пулинг интерпретируется так: если на предыдущей операции свёртки уже были выявлены некоторые признаки, то для дальнейшей обработки настолько подробное изображение уже не нужно, и оно уплотняется до менее подробного. К тому же фильтрация уже ненужных деталей помогает не переобучаться. Слой пулинга, как правило, вставляется после слоя свёртки перед слоем следующей свёртки.
Кроме пулинга с функцией максимума можно использовать и другие функции — например, среднего значения или L2-нормирования. Однако практика показала преимущества именно пулинга с функцией максимума, который включается в типовые системы.
В целях более агрессивного уменьшения размера получаемых представлений, всё чаще находят распространение идеи использования меньших фильтров [8] или полный отказ от слоёв пулинга. [9]
Полносвязная нейронная сеть [ править | править код ]
После нескольких прохождений свёртки изображения и уплотнения с помощью пулинга система перестраивается от конкретной сетки пикселей с высоким разрешением к более абстрактным картам признаков, как правило на каждом следующем слое увеличивается число каналов и уменьшается размерность изображения в каждом канале. В конце концов остаётся большой набор каналов, хранящих небольшое число данных (даже один параметр), которые интерпретируются как самые абстрактные понятия, выявленные из исходного изображения.
Эти данные объединяются и передаются на обычную полносвязную нейронную сеть, которая тоже может состоять из нескольких слоёв. При этом полносвязные слои уже утрачивают пространственную структуру пикселей и обладают сравнительно небольшой размерностью (по отношению к количеству пикселей исходного изображения).
Обучение [ править | править код ]
Наиболее простым и популярным способом обучения является метод обучения с учителем (на маркированных данных) — метод обратного распространения ошибки и его модификации. Но существует также ряд техник обучения свёрточной сети без учителя. Например, фильтры операции свёртки можно обучить отдельно и автономно, подавая на них вырезанные случайным образом кусочки исходных изображений обучающей выборки и применяя для них любой известный алгоритм обучения без учителя (например, автоассоциатор или даже метод k-средних) — такая техника известна под названием patch-based training . Соответственно, следующий слой свёртки сети будет обучаться на кусочках от уже обученного первого слоя сети. Также можно скомбинировать сверточную нейросеть с другими технологиями глубинного обучения. Например, сделать свёрточный авто-ассоциатор [10] , свёрточную версию каскадных ограниченных машин Больцмана, обучающихся за счёт вероятностного математического аппарата [11] , свёрточную версию разреженного кодирования (англ. sparse coding ), названную deconvolutional networks («развертывающими» сетями) [12] .
Для улучшения работы сети, повышения её устойчивости и предотвращения переобучения применяется также исключение(дропаут)— метод тренировки подсети с выбрасыванием случайных одиночных нейронов.
Полный курс на русском языке можно найти по этой ссылке.
Оригинальный курс на английском доступен по этой ссылке.
Выход новых лекций запланирован каждые 2-3 дня.
Интервью с Себастьяном
— Итак, мы снова с Себастьяном в третьей части этого курса. Себастьян, я знаю, что вы проводили много разработок с использованием свёрточных нейронных сетей. Можете нам рассказать чуточку больше об этих сетях и что они собой представляют? Я уверена, что студенты нашего курса будут слушать с не меньшим интересом, потому что в этой части им предстоит разрабатывать свёрточную нейронную сеть самим.
— Отлично! Итак, свёрточные нейронные сети это великолепный способ структурирования в сети, построения так называемой инвариантности (выделения неизменяемых признаков). Например, возьмём идею распознавания образов на сцене или фотографии, вы хотите понять изображён ли Себастьян на ней или нет. Не важно в какой части фотографии я располагаюсь, где располагается моя голова — по центру фотографии или в углу. Распознавание моей головы, моего лица должно происходить вне зависимости от того, где они расположены на изображении. Это и есть инвариантность, вариативность местоположения, которая реализуется свёрточными нейронными сетями.
— Очень интересно! Можете назвать нам основные задачи в которых применяются свёрточные нейронные сети?
— Свёрточные нейронные сети достаточно плотно используются при работе с аудио и видео, включая медицинские изображения. Так же они используются в языковых технологиях, где специалисты используют глубокое обучение для понимания и воспроизведения языковых конструкций. На самом деле применений у этой технологии очень много, я бы даже сказал, что они безграничны! Её, технологию, можно использовать в финансах и любых других областях.
— Я использовала свёрточные нейронные сети для анализа снимков со спутников.
— Здорово! Стандартная задача!
— Как вы думаете, можем ли мы считать свёрточные нейронные сети чем-то последним и самым продвинутым инструментом в развитии глубокого обучения?
— Ха! Я уже научился никогда не говорить «никогда». Всегда будет находиться нечто новое и потрясающее!
— Значит нам ещё предстоит поработать? 🙂
— Работы будет достаточно!
— Превосходно! В этом курсе мы как раз обучаем будущих пионеров машинного обучения. Есть ли у вас пожелания нашим студентам, перед тем как они начнут строить свою первую свёрточную нейронную сеть?
— Вот вам интересный факт. Свёрточные нейронные сети были изобретены в 1989 году, а это очень давно! Большинство из вас даже не родились ещё в то время, а это означает, что не гениальность алгоритма имеет огромное значение, а данные, которыми оперирует этот алгоритм. Мы живём в мире, где предостаточно данных для анализа и поиска закономерностей. У нас есть возможность эмулировать функции человеческого разума используя это огромное количество данных. Когда вы будете работать над свёрточными нейронными сетями, попробуйте сфокусироваться на поиске правильных данных и их применении — посмотрите, что из этого получится и, порой, это может оказаться настоящей магией, как было в нашем случае, когда мы решали задачу определения рака кожи.
— Великолепно! Ну, что, давайте наконец-то займёмся магией!
Введение
На прошлом занятии мы научились разрабатывать глубокие нейронные сети, которые способны классифицировать изображения элементов одежды из набора данных Fashion MNIST.

Результаты, которых мы добились в процессе работы над нейронной сетью, были впечатляющими — 88% точность классификации. И это в несколько строк кода (не учитывая код построения графиков и изображений)!
Точность: 0.8782
Мы так же экспериментировали с влиянием количества нейронов в скрытых слоях и количества обучающих итераций на точность модели. Но как нам сделать эту модель ещё лучше и точнее? Один из способов этого добиться — использовать свёрточные нейронные сети, сокращённо СНС. СНС показывают большую точность при решении задач классификации изображений, нежели стандартные полносвязные нейронные сети с которыми мы сталкивались на предыдущих занятиях. Именно по этой причине СНС стали такими популярными и именно благодаря им стал возможен технологический прорыв в области машинного зрения.
На этом занятии мы узнаем, насколько легко разработать СНС-классификатор с нуля используя TensorFlow и Keras. Мы будем использовать тот же набор данных Fashion MNIST, который использовали на предыдущем занятии. В конце этого занятия мы сравним точность классификации элементов одежды предыдущей нейронной сети со свёрточной нейронной сетью из этого занятия.
Перед тем как погружаться в разработку стоит немного углубиться в принцип работы свёрточных нейронных сетей.
Две основные концепции в свёрточных нейронных сетях:
- свёртка
- операция подвыборки (pooling, max pooling)
Давайте рассмотрим их подробнее.
Свёртка
В этой части нашего занятия мы изучим технику, которая называется свёрткой. Давайте посмотрим, как же она работает.
Возьмём изображение в оттенках серого и, для примера, представим, что его размеры 6 px в высоту и 6 px в ширину.

Наш компьютер интерпретирует изображение как двумерный массив пикселей. Так как наше изображение в оттенках серого, то значение каждого пикселя будет находиться в интервале от 0 до 255. 0 — черный, 255 — белый.
На изображении ниже мы видим представление изображения 6px x 6px и соответствующие значения пикселей:

Как вы уже знаете, перед работой с изображениями значения пикселей необходимо нормализовать — привести значения к интервалу от 0 до 1. Однако в данном примере, для удобства пояснений, сохраним значения пикселей изображения и не будет их нормализовать.
Суть свёртки заключается в создании другого набора значений, который называется ядром или фильтром. Пример можно увидеть на изображении ниже — матрица размером 3 х 3:

Затем мы можем просканировать наше изображение с использованием ядра. Размеры нашего изображения 6х6px, а ядра — 3x3px. Свёрточный слой применяется к ядру и каждому участку входного изображения.
Давайте представим себе, что мы хотим выполнить свёртку над пикселем со значением 25 (3 строка, 3 столбец) и первым делом, что необходимо сделать — центрировать ядро над этим пикселем:

На изображении размещение ядра выделено жёлтым цветом. Теперь мы будем смотреть только на те значения пикселей, которые находятся в нашем жёлтом прямоугольнике, размеры которого соответствуют размерам нашего ядра свёртки.
Теперь берём значения пикселей изображения и ядра, умножаем каждый пиксель изображения с соответствующим пикселем ядра и складываем все значения произведений, а результатирующее значение пикселя присваиваем новому изображению.



Подобную операцию мы производим со всеми пикселями нашего изображения. Но что должно происходить с пикселями на границах?

Есть несколько решений. Во-первых, мы можем просто проигнорировать эти пиксели, но в таком случае мы потеряем информацию об изображении, которая может оказаться значимой, а свёрнутое изображение станет по размерам меньше оригинального. Во-вторых, мы можем просто «забить» нулевыми значениями те пиксели, значения ядра для которых выходят за рамки изображения. Процесс называется выравниванием.

Теперь, когда произведено выравнивание нулевыми значениями пикселей, мы можем вычислить значение итогового пикселя в свёрнутом изображении как и прежде.
Свёртка — это процесс применения ядра (фильтра) к каждому участку входного изображения, по аналогии с полносвязным слоем (dense-слоем) мы увидим, что и свёртка представляет собой такой же слой в Keras.
Теперь давайте рассмотрим вторую концепцию свёрточных нейронных сетей — операцию подвыборки (pooling, max-pooling).
Операция подвыборки (pooling, max-pooling)
Сейчас мы рассмотрим вторую фундаментальную концепцию лежащую в основе свёрточных нейронных сетей — операцию подвыборки (pooling, max-pooling). Простыми словами, операция подвыборки это процесс сжатия (уменьшения размеров) изображения путём сложения значений блоков пикселей. Давайте рассмотрим как это работает на конкретном примере.

Чтобы выполнить операцию подвыборку нам необходимо определиться с двумя составляющими этого процесса — размером выборки (размером прямоугольной сетки) и величиной шага. В этом примере мы будем использовать прямоугольную сетку размером 3х3 и шагом 3. Шаг определяет количество пикселей на которые необходимо сдвигать прямоугольную сетку при выполнении операции подвыборки.
После того как мы определились с размером сетки и размером шага нам необходимо найти максимальное значение пикселя попадающее в выделенную сетку. В пример выше в сетку попадают значения 1, 0, 4, 8, 2, 5, 20, 13, 25. Максимальное значение — 25. Это значение «переносится» в новое изображение. Сетка сдвигается на 3 пикселя вправо и процесс выборки максимального значения и его переноса на новое изображение повторяется.

В результате будет получено изображение меньшего размера по сравнению с оригинальным входным изображением. В нашем примере было получено изображение, которое в два раза меньше нашего исходного изображения. Размер итогового изображения будет варьироваться в зависимости от выбора размера прямоугольной сетки и размера шага.
Давайте посмотрим как это будет работать в Python!
Резюме
Мы познакомились с такими понятиями как свёртка и операция подвыборки (max-pooling).
Свёртка — процесс применения фильтра («ядра») к изображению. Операция подвыборки по максимальному значению — процесс уменьшения размеров изображения через объединение группы пикселей в единое максимальное значение из этой группы.
Как мы увидим в практической части, свёрточный слой может быть добавлен к нейронной сети с использованием Conv2D -слоя в Keras. Этот слой подобен Dense-слою, и содержит веса и смещения, которые подвергаются оптимизации (подбору). Conv2D -слой так же содержит фильтры («ядра»), значения которых тоже оптимизируются. Итак, в Conv2D -слое значения внутри матрицы фильтра и есть переменные, которые подвергаются оптимизации.
Некоторые термины с которыми мы успели столкнуться:
- СНС — свёрточные нейронные сети. Нейронная сеть, которая содержит хотя бы один свёрточный слой. Типичная СНС содержит и другие слои, такие как слои выборки и полносвязные слои.
- Свёртка — процесс применения фильтра («ядра») к изображению.
- Фильтр (ядро) — матрица, по размерам меньше, чем входные данные, предназначенная для преобразования входных данных блоками.
- Выравнивание — процесс добавления, чаще всего нулевых значений, по краям изображения.
- Операция подвыборки — процесс уменьшения размера изображения через сэмплирование. Существует несколько типов слоёв подвыборки, например, слой усредненной подвыборки (выборка среднего значения), однако подвыборка по максимальному значению используется чаще всего.
- Подвыборка по максимальному значению — процесс подвыборки, в ходе которого множество значений преобразовываются в единое значение — максимальное среди выборки.
- Шаг — количество пикселей смещения фильтром (ядром) на изображении.
- Сэмплирование (downsampling) — процесс уменьшения размера изображения.
CoLab: классификация элементов одежды Fashion MNIST с использованием свёрточной нейронной сети
Нас обвели вокруг пальца! Данную практическую часть есть смысл выполнять только после выполнения предыдущей части — весь код, кроме одного блока, остаётся прежним. Меняется структура нашей нейронной сети, а это — четыре дополнительные строки для свёрточных нейронных слоёв и слоёв подвыборки по максимальному значению (max-pooling).
Все детальные пояснения, как это работать, нам обещают дать в следующей части — 4 части.
Ах, да. Точность модели на этапе обучения стала равна 97% (модель «переобучилась» при epochs=10 ), а при прогонке по набору данных для тестов показала точно 91%. Заметный прирост точности относительно предыдущей архитектуры, где у нас были использованы только полносвязаные слои — 88%.
Итоги
В этой части занятия мы изучили новый тип нейронных сетей — свёрточные нейронные сети. Познакомились с такими терминами как «свёртка» и «операция подвыборки по максимальному значению» (max-pooling), разработали и обучили свёрточную нейронную сеть с нуля. В результе мы увидели, что наша свёрточная нейронная сеть выдаёт точность больше, чем нейронная сеть, которую мы разрабатывали на прошлом занятии.
P.S. Примечание от автора перевод.
Курс называется «Введение в глубокое обучение с использованием TensorFlow», поэтому сетовать на то, что нет детализированных пояснений по принципу работы свёрточных нейронных сетей (слоёв) мы не будем — следующие две статьи будут как раз о принципе работы свёрточной нейронной сети и их внутренней структуре (статьи не касаются курса, а были рекомендованы участниками StackOverflow для лучшего понимания происходящего).
… и стандартные call-to-action — подписывайся, ставь плюс и делай share 🙂
YouTube
Telegram
ВКонтакте






