Начало обзора здесь.
Первые необъяснимые результаты
Восьмая глава в каком то смысле переломная для всей истории эволюции глубоких сетей, рассказанной в книге. Здесь тревожные звоночки, которые звенели раньше, превращаются в первые проблемы работы с моделями, которые мы даже объяснить не можем, а можем только высказать какие-то предположения.
Сама глава посвящена оценке качества модели, обученной на учебном наборе данных. Качество понимается как точность предсказаний, сделанных моделью на проверочном, тестовом наборе.
В довольно простой задаче распознавания рукописных цифр, используя хорошо известную базу MNIST (60 тысяч обучающих образцов и десять тысяч тестовых) в упрощенном варианте MNIST-1D (всего 4000 обучающих образцов). Каждый упрощенный образец дискретизируется в 40 точках и на каждый из сорока входов модели подается горизонтальное смещение точки образца (поэтому набор данных и называется MNIST-1D). В модели два слоя по 100 элементов в каждом. Для оптимизации используется стохастический градиентный спуск. По ходу обучения ошибка классификации образцов, подаваемых на вход плавно снижается до нуля. Однако ошибки на тестовом наборе, которые начинают снижаться, дойдя до 40%, остаются на этом уровне. Значение функции потерь при этом даже увеличивается, приближаясь к единице, что означает, что модель делает те же ошибки, но с большей уверенностью.
На простых регрессионных моделях исследуются три источника ошибок (шум, дисперсия и смещение) при генерации тестовых данных и предлагаются методы сокращения дисперсии (за счет увеличения количества тестовых данных) и смещения за счет увеличения мощности модели (с шумом ничего поделать нельзя). При этом еще существует баланс дисперсии и смещения — увеличивая мощность модели, чтобы сократить ошибку смещения, мы получаем увеличение дисперсии как компоненты ошибки. Это значит, что для каждой модели существует некоторая оптимальная мощность для заданного объема обучающих данных.
Когда те же методы сокращения ошибок применяются к примеру с MINST-1D, обнаруживается эффект, которого никто не ожидал. Оказывается, что баланс дисперсии-смещения работает аддитивно только для регрессионных моделей, а для других типов задач их взаимодействие может быть более сложным. При увеличении мощности модели кривая процента ошибок сначала ведет себя как предсказывал баланс ошибок дисперсии-смещения, но при дальнейшем увеличении мощности снова начинает уменьшаться.
Это явление получило название двойного спуска и на некоторых наборах данных оно проявляется уже на исходных данных (как в MNIST), а на некоторых — на зашумленных исходных данных (MNIST-1D, CIFAR 100). Первую часть кривой снижения процента ошибок стали называть режимом недопараметризованности, а второй спуск — режимом перепараметризованности, разделяет их участок критического режима, когда ошибка растет.
Явление двойного спуска открыли недавно, неожиданно, и в какой-то степени к всеобщему удивлению, пишет автор. Оно происходит от взаимодействия двух явлений. Первое, характеристики теста становятся временно хуже, когда мощности модели только-только достаточно, чтобы запомнить данные. Второе, характеристики теста продолжают улучшаться по мере роста мощности даже после того, как качество обучения кажется идеальным. Первое явление в точности предсказывается балансом смещение-дисперсия. Второе явление более озадачивающее; неясно почему характеристики должны быть лучше в режиме пере-параметризации, при том, что нет достаточного количеств точек обучающих данных, чтобы ограничить параметры модели уникальным образом.
Гипотеза (всего лишь гипотеза) заключается в том, что дальнейшее увеличение мощности модели после того, как обучающие данные описаны ею идеально, приводит к тому, что модель начинает интерполировать то, что происходит между точками обучающих данных с увеличивающейся гладкостью. Вообще оказывается, что поведение модели между точками данных критично из-за явления которое стали называть “проклятием размерности”. Еще тридцать лет назад так называли совсем другое явление (нехватку вычислительных ресурсов для решения вычислительных задач большой размерности), а сегодня проклятие размерности описывает проблему разреженности обучающих данных в многомерном пространстве. Эта проблема очень ярко описана в книге Педро Домингеса “Верховный алгоритм [Как машинное обучение изменит наш мир]” (2016), он описывает ее как эффект гиперапельсина, который состоит из одной кожуры. Здесь на Хабре этот эффект упоминается вот в этой публикации 2016 года: https://habr.com/ru/articles/280766/.
Предположительно,
по мере добавления мощности модели она начинает интерполировать между ближайшими точками данных с увеличивающейся гладкостью. В отсутствие информации о том, что происходит между обучающими точками, предполагаемая гладкость кажется разумной и, вероятно, будет приемлемо обобщаться на новых данных.
Опять же, предположительно! Это всего лишь правдоподобный аргумент. То, что более мощные модели создают более гладкие функции мы уже видели на примере кусочно-линейной регрессии. Но почему перепараметризованные модели должны порождать гладкие функции, никто не знает.
Ответ на этот вопрос неясен, но есть две возможных вероятности. Первое, инициализации сети может побудить гладкость, и модель никогда не отдаляется от суб-домена гладкой функции во время процесса обучения. Второе, обучающий алгоритм может каким-то образом «предпочитать» схождение к гладким функциям. Любой фактор, который смещает решение к подмножеству эквивалентных решений, называют регуляризатором, так что есть вероятность, что обучающий алгоритм действует как неявный регуляризатор.
Эта цитата дает нам повод перейти к обзору следующей главы, которая посвящена регуляризации.
Давайте что-нибудь придумаем
Здесь начинается то, что по-английски называют wild goose chase (охота на диких гусей), а в нашей деревне похоже называли “аля-улю гони гусей”. Раз мы не понимаем, почему многопараметрическая модель с большими значениями гиперпараметров (ширина и глубина сети) и вечно недостаточным количеством обучающих данных, надо что-то придумать. И вот этим придумыванием (эвристиками) характеризуется весь дальнейший период развития нейронных сетей вплоть до сегодняшнего дня. Регуляризация, о которой идет речь в этой главе, это набор догадок, основанный на наблюдении за поведением алгоритма стохастического градиентного спуска, который, похоже (!), предпочитает (!) схождение к гладким функциям, как и другие алгоритмы оптимизации.
Все эвристики сводятся к тому, чтобы что-то добавить к функции потерь, некоторый регуляризирующий компонент, который будет уводить минимум функции потерь от оригинального. В вероятностной постановке этой добавкой будет априорное распределение параметров, которое задает наше знание о параметрах до того, как мы начинаем наблюдать данные. Наиболее широко распространенный регуляризационный член функции потерь — L2 метрика — как бы штрафует сумму квадратов значений параметра. Такая регуляризация называется Тихоновской, гребневой или (в случае матриц) регуляризацией нормой Фробениуса.
Для нейронных сетей L2 регуляризация обычно применяется к весам, но не к смещениям и поэтому называется членом снижения весов (weight decay). Эффект заключается в поощрении меньших весов так, чтобы выходная функция была глаже.
Остаток главы посвящен другим эвристикам для улучшения поведения функции потерь при поиске минимума.
Ранняя остановка означает остановку процедуры обучения до того, как она полностью сошлась. Это может сократить переобучение, если модель уже ухватила грубо форму функции, лежащей в основе, но еще не имела времени, чтобы переобучиться шуму.
Ансамблевые методы сводятся к построению нескольких моделей (например, с разными случайными инициализациями) и усреднению их предсказания.
Отсев произвольно обнуляет подмножество скрытых элементов (обычно 50%) на каждой итерации SGD. Преимущество этой техники в том, что можно устранить нежелательные «несуразности» функции, которые далеки от обучающих данных и не влияют на потери.
Наложение шума основано на том наблюдении, что отсев можно интерпретировать как наложение мультипликативного шума Бернулли на сеть активаций. Это ведет к обобщению идеи на другие части сети во время обучения, чтобы в итоге сделать модель более робастной.
Байесовские выводы, трансферное и многозадачное обучение, обучение с самоконтролем, аугментация данных… уверен, что это список стал еще длиннее с момента выхода книги.
За всеми этими методами есть четыре главных принципа. Мы можем (1) поощрять функцию становиться более гладкой (например, L2 регуляризация), (2) увеличивать объем данных (например, аугментацией данных), (3) комбинировать модели (например, ансамблевые методы), или (4) искать более широкий минимум (например, накладывая шум на сетевые веса.
Другой способ улучшить способность модели к обобщению – это выбрать архитектуру модели, подходящую к задаче (тоже что-то придумать, но теперь уже структурно). Например, в сегментации изображений мы можем делиться параметрами внутри модели, так что нам не надо независимо учиться, как выглядит дерево на каждом участке изображения. В главах 10-13 рассматриваются архитектурные вариации, спроектированные для различных задач.
Архитектурные находки
Собственно, здесь начинается парад новейших достижений в области архитектуры моделей, основанных на нейронных сетях, но с множеством эвристических находок.
В десятой главе описываются сверточные сети, которые придумали в первую очередь ради приложений компьютерного зрения. 12 лет назад Саймон Принс — автор этой книги — опубликовал книгу по компьютерному зрению, в которой нейронные сети едва упоминаются, пока только в качестве синонима многослойного перцептрона (MLP). Еще совсем недавно область компьютерного зрения мирно развивалась на базе алгоритмов обработки изображений (фильтрация, преобразование Фурье), марковских процессов принятия решений, динамического программирования и тому подобной уважаемой прикладной математики. И вот результат, автор признается, что многие разделы той книги безнадежно устарели.
Основная идея архитектуры сверточных сетей в том, что добавляются сверточные слои которые “свертывают” несколько соседних элементов в один элемент следующего слоя в виде все той же взвешенной суммы. Это прием отражает тот простой факт, что соседствующие пиксели изображения статистически связаны. Процедуру свертки можно представлять себе как операцию сжатия изображения, не случайно эта локальная операция называется сверточным ядром или фильтром. Ну, и все, сети, которые состоят в основном из сверточных слоев, называют сверточными. Количество параметров, характеризующих сверточный слой невелико, сеть перестает быть полносвязной. Чтобы не терять информацию начали применять множественные свертки с разными весами и смещением, каждую из которых стали называть каналами. Для улучшения результатов применяются операции уменьшения и увеличения размерности изображений.
Вся эта группа методов успешно применяется в типичных задачах компьютерного зрения — классификации изображений, обнаружения объектов и семантической сегментации.
Было показано, что характеристики классификации образов улучшаются с глубиной сети. Однако, последующие эксперименты показали, что увеличение глубины сети в какой-то момент перестает помогать; после определенной глубины система становится труднообучаемой. Это стало мотивом для применения остаточных соединений.
Остаточные сети описываются в следующей одиннадцатой главе.
Ухудшение показателей классификации изображений по мере дальнейшего наращивания глубины сетей до конца не понятно, опять признается автор. Обычный подозреваемый в таких случаях — градиент потерь, будь он неладен. Алгоритм оптимизации использует конечный размер шага, в то время как градиент ведет себя кое-как и может оказаться совсем неподходящим в новой точке. На основании наблюдений за одномерными моделями предполагается, что функция потерь выглядит как огромная цепь крохотных гор, а не как гладкая поверхность, по которой легко спускаться (surprise!). Автокорреляция градиента при большой глубине сети быстро падает до нуля, и это явление стали называть разбитыми градиентами.
Как обычно на помощь в борьбе с трудностями приходит очередная структурная придумка — вводятся остаточные блоки и остаточные слои, состоящие из них. Структурно — это просто прямая связь, когда вход обрабатывающего элемента добавляется к его выходу (в отличие от обратной связи, когда выход добавляется ко входу). Трудно понять, почему эти соединения стали называть остаточными, термин residuals широко используется в других областях математики (статистика, численные методы и др). В численных методах принятый русский термин для residuals — смешное слово “невязка”. В качестве синонима для остаточных соединений используют также термин skip connection, гораздо более понятный. К моменту перевода книги этот термин в русскоязычных публикация вообще никак не переводили, а использовали просто кальку. Я предложил использовать термин “обходные соединения”.
Длинная последовательность обработки от слоя к слою в глубокой сети разбивается на более короткие пути, в которых градиенты ведут себя лучше и остаточные сети меньше страдают от разбитых градиентов. Ура, товарищи! Ну, не то чтобы сразу так ура, пришлось применить еще парочку технических приемов — пакетную нормализацию, например, но в результате архитектура остаточных сетей была успешно применена в целом ряде проектов — ResNet, DenseNet, U-net (и другие остаточные сети типа “песочные часы”).
Ответ на вопрос — почему же остаточные сети работают так хорошо — снова остается открытым. В качестве наблюдений автор приводит два: (1) производительность остаточных сетей лучше, чем глубоких и (2) градиенты во время обучения не так эффективно распространяются по очень длинным путям развертывающейся сети.
Современная точка зрения заключается в том, что остаточные соединения добавляют своей собственной ценности, кроме того, что позволяют обучать более глубокие сети. Эта точка зрения поддерживается тем фактом, что поверхности потерь остаточных сетей склонны к большей гладкости вокруг минимума и более предсказуемы, чем для той же сети, но с удаленными остаточными соединениями. Это может помочь облегчить поиск хорошего решения, которое будет хорошо обобщать.
ИИ в сиянии славы
Ну вот, в двенадцатой главе мы наконец добрались до той области, в которой сосредоточен наибольший хайп, и где, по неслучайному совпадению находится обширная братская могила моих не сбывшихся мечтаний и сокрушенных иллюзий. Здесь похоронены порождающие грамматики Хомского, фреймы для представления знаний Минского, теоретическая семантика и многое другое. Под отдельным обелиском покоится детище Тима Бернеса-Ли Semantic Web.
Да, речь идет о трансформерах, которые изначально были нацелены на задачи обработки естественного языка (Natural Language Processing – NLP). Внешне все обстоит так же, как и в других специализированных архитектурах глубоких нейронных сетей — на вход модели подаются последовательности векторов высокой размерности (embeddings), представляющих слова или фрагменты слов естественного языка. Выход зависит от того, что мы хотим получить от входного текста.
С учетом специфики языковых наборов данных приходится вводить новые понятия, которые являются статистическими аналогами предшествующих семантических конструктов. Важнейшее из них — это понятие внимания и самовнимания. Эта метрика просто определяет силу связи между словами в предложении, например, между существительным и местоимением, которое его заменяет. Сама метрика — это просто композиция очередных линейных (афинных) преобразований, которая завершается вычислением скалярного произведения предварительно вычисленных векторов. Как только придумали внимание, сразу стало понятно, что нужно эту метрику дополнительно укреплять, например позиционным кодированием, чтобы учесть разницу смыслов при изменении слов в предложении, вводить масштабирование больших значений скалярного произведения векторов, а также организовывать параллельные вычисления — так называемое многоголовочное самовнимания (по аналогии с головками многодорожечного магнитофона).
Механизм вычисления самовнимания — это только половина трансформера, после блока многоголовочного самовнимания строится еще одна полносвязная сеть, которая работает по отдельности над каждым словом. Обе сети по типу остаточные (то есть входы подаются на выход, а также после каждой сети вводится слой нормализации, похожей на пакетную.
В задачх NLP процесс начинается с модуля токенизации, который разбивает текст на слова или фрагменты слов. Затем каждый из этих токенов отображается на выученный вектор представления (embedding). Эти вектора передаются через последовательность трансформеров. Трансформеры могут быть кодерами, а могут быть декодерами. Пара кодер-декодер используется для преобразования последовательности слов в последовательность, например в машинном переводе.
В качестве примера кодера приводится модель BERT. В эту модель включены стадии самообучения и тонкой настройки. В ходе самообучения собирается статистика по синтаксису входного языка и формируется поверхностный здравый смысл о мире с ограниченным “пониманием”. Тонкая настройка специализирует сеть на решение конкретных задач. Для этого выполняется, например, классификация текста, классификация слов и предсказание протяженности текста.
В качестве примера декодера приводится всем известный GPT3. Этот декодер строит авторегрессионную модель языка. В GPT3 используется модификация механизма самовнимания — маскированное самовнимание, которое уделяет внимание только текущим и предыдущим токенам, и предотвращает возможность читинга со стороны системы, для которой скрывается текст, следующий за текущим токеном.
Каждое из выходных представлений декодера представляет частичное предложение, и для каждого из них целью является предсказать следующий токен в последовательности. Далее, после трансформеров линейный слой отображает каждое представление слова на размер словаря, за чем следует преобразование этих значений в вероятности. Во время обучения мы стремимся максимизировать сумму логарифмов вероятностей следующего токена в контрольной последовательности в каждой позиции, используя стандартную функцию потерь мультиклассовой перекрестной энтропии.
Авторегрессионная модель языка — это первый пример генеративных моделей, обсуждаемых в этой книге. Ну, и должно быть уже само собой понятным, что для улучшения связности сгенерированного текста применяется множество придумок и примочек, например, лучевой поиск, Top-k выборки и многое другое.
Далее в главе приводятся примеры применения архитектуры кодеров-декодеров для машинного перевода и для обработки изображений (всем известный ImageGPT и менее известные мульти-масштабные трансформеры для видения).
В тринадцатой главе описываются графовые нейронные сети, то есть такие, которые обрабатывают поступающие на вход графы.
Эта глава начинается представлением примеров графов из реального мира. Затем в ней описывается, как кодировать эти графы и как формулировать задачи обучения с учителем для графов. Обсуждаются алгоритмические требования для обработки графов, и это приводит естественным образом к сверточным графовым сетям, особенному типу графовых нейронных сетей.
Довольно специфический материал и наибольший интерес представляет не основной текст главы, а заметки к нему, отсылающие к соответствующим исследованиям и публикациям.
Обучение без учителя
В короткой четырнадцатой главе представлена таксономия моделей обучения без учителя, а затем обсуждаются желаемые свойства моделей и как измерять их производительность. В четырех последующих главах обсуждаются четыре конкретные модели: генеративно-состязательные сети (generative adversarial networks – GAN), вариационные автокодировщики (variational autoencoders – VAE), нормализующие потоки и диффузные модели.
Для понимания этих глав требуется уже более углубленное понимание теории вероятностей.
В моделях обучения без учителя нет никого, кто бы размечал входные данные или ранжировал выходные, поэтому существует насущная необходимость генерировать новые образцы для дальнейшего обучения самой моделью. В моделях GAN (глава 15) главная сеть GAN – генератор – создает образцы, отображая стохастический (белый) шум на пространство выходных данных. Это забавным образом перекликается для меня с концепцией формирующего фильтра в теории стохастических систем. Новые образцы оценивает дискриминатор, и если он не может отличить сгенерированные и реальные образцы, все хорошо.
Если дискриминатор может обнаружить разницу, тогда формируется обучающий сигнал, который можно использовать в качестве обратной связи для улучшения качества образцов. Это простая идея, но само обучение GAN – трудное: алгоритмы обучения могут быть нестабильными, и хотя GAN могут научиться генерировать реалистические образцы, это не означает, что они научились генерировать все возможные образцы.
Наибольшего успеха GAN добились в домене изображений, где они могут порождать образцы, которые почти неотличимы от реальных картин.
В шестнадцатой главе описываются вероятностные генеративные модели, которые назвали нормализующими потоками. Эти модели используют глубокую сеть, что преобразовать исходное простое распределение (чаще всего нормальное) в желаемое более сложное. Они могут как делать выборки из этого нового распределения, так и оценивать правдоподобие новых образцов. В качестве исходного берется распределение некоторой латентной переменной. Для генерации новых образцов выполняется прямое преобразование латентных переменных в образцы данных на входе. Оценка правдоподобия новых образцов требует инверсного преобразования входных переменных в латентные, что накладывает архитектурные ограничения на модель нормализующих потоков — все слои должны быть инвертируемы. Своим названием нормализующие потоки обязаны как раз этому обратному преобразованию от сложного распределения к нормальному.
В семнадцатой главе описывается еще одна группа вероятностных генеративных моделей, которые назвали вариационными автокодировщиками. Она интересна уже тем, что здесь впервые описывается модели латентных переменных с понятными примерами. Благодаря этому становится понятнее, что говорилось о латентных переменных раньше. Здесь, как и в модели нормализующих потоков трудности вызывает прямое вычисление правдоподобия образцов данных, поэтому приходится работать с его оценками, в частности с нижней вариационной границей (ELBO). Я нашел на Хабре прошлогоднюю публикацию Ивана Родькина “Обучение VAE и нижняя вариационная граница”, где понятно описано, зачем нужны ELBO и как с ними работать. Рекомендую ее посмотреть https://habr.com/ru/articles/723674/.
И, наконец, завершается этот парад вероятностных генеративных моделей диффузионными моделями, описанными в восемнадцатой главе. Архитектурно они очень похожи на VAE, состоят из кодера и декодера, но обучается не кодер, как в VAE, а декодер. Оказалось, что диффузионные модели легки в обучении и могут производить образцы очень высокого качества. В целом эта глава тесно примыкает к предыдущей.
Отдельная, девятнадцатая глава, посвящена обучению с подкреплением, которое заметно отличается по набору подходов и методов, от основного содержания книги. Отличие в первую очередь в том, что существует солидная база в виде теории стохастических процессов и набор хорошо изученных формализмов, в первую очередь — марковские процессы принятия решений, а также обширный набор методов динамического программирования и методов Монте-Карло. Все это я еще в институте изучал много лет назад. Новый этап развития этого направления, который автор даже называет прорывом, возник, когда для Q-обучения начали применять глубокие сети. Позже появились и другие методы с использованием глубоких сетей — методы градиента стратегий, методы “актор-критик” и другие. В целом раздел обучения с подкреплением вызывает гораздо меньше ощущений черной магии, по сравнению с предыдущими разделами. Недаром, уже упоминавшийся здесь скептик Ричард Саттон работает именно в этом разделе и второе издание 2020 года его монографии по обучению с подкреплением рекомендуется в качестве фундаментального введения в предмет.
И как все это понимать
В двадцатой главе автор собирает вместе все свои удивленные возгласы, которые он не мог сдержать по мере изложения материала. Удивление тем, что глубокие сети так легко обучаются и так хорошо работают после обучения, хотя совершенно непонятно, почему это так. К сожалению, эту главу нельзя прочесть вместо всей книги, для того, чтобы понимать, что именно удивляет признанного специалиста в этой области, нужно понимать архитектуру и принципы работы глубоких сетей, роль наборов данных, самих моделей и обучающих алгоритмов.
Коротко говоря, удивительно, что мы можем подгонять глубокие сети надежно и эффективно. Сами данные, модели, обучающие алгоритмы или какая-то комбинация всех трех должны обладать какими-то специальными свойствами, чтобы такое было возможно.
Но мы все еще не знаем, какими именно!
Многие вопросы остаются неотвеченными. У нас сейчас нет никакой предписывающей теории, которая позволит нам предсказать обстоятельства, в которых обучение и обобщение будут успешными или нет. Мы не знаем пределов обучения в глубоких сетях или возможны ли гораздо более эффективные модели. Мы не знаем, существуют ли параметры, которые бы обобщали лучше в пределах той же самой модели. Изучение глубокого обучения все еще движется эмпирическими демонстрациями. Они безусловно впечатляют, но пока не соответствуют нашему пониманию механизмов глубокого обучения.
Мораль в этом есть? Как не быть!
Последняя, двадцать первая глава основной части книги посвящена этическим вопросам применения глубоких сетей и вообще искусственного интеллекта. Мне она кажется очень политизированной и полна призывов соблюдать осторожность при выборе темы и методов исследования, чтобы не набежали толпы активистов, которые топят за какую-нибудь группу обиженных, и не устроили гевалт. Я уже давно не ученый, а в роли переводчика мне это не грозит. Возможно, кому-то будет интересна и эта глава, я старался переводить ее честно, как и все предыдущие.
Короткое послесловие
Признаюсь, что когда я начинал писать этот обзор я даже не подозревал, что он превратится в такой лонгрид. Во многом это, конечно объясняется тем, что не я владел материалом при переводе, а материал мной. В этом смысле медленное чтение своего собственного перевода было полезно как для меня, так и для перевода — исправил большое количество просмотров, неизбежных при таком объеме и сложности. Другая польза лично для меня заключалась в том, что я научился пользоваться редактором формул в составе MS Word. В мое время мы все больше TeX-ом пользовались, да LaTeX-ом, когда статью надо было написать с большим количеством нотации. По ходу описания я даже решил несколько задачек, приведенных в книге, чтобы убедиться в том, что я правильно понимаю, что там написано.
Конечно, то обстоятельство, что срок выхода книги на русском до сих пор не известен, и даже непонятно, выйдет ли она вообще, меня очень огорчает. Я даже написал автору письмо с вопросом, нельзя ли выложить перевод рядом с оригиналом на GitHub. К моему удивлению, Саймон Принс очень быстро ответил и сказал, что, к сожалению, это находится за пределами его прав, как автора, и даже издательства MIT Press, которому он продал права на публикацию оригинала.
Остается надежда, что какая-то часть читателей, которые интересуются этой отраслью, решится обратиться к оригиналу этой книги и многочисленным вспомогательным материалам, преодолев боязнь английского языка, который мы все знаем как выученный. Ну, и DeePL вам в помощь тоже. Я даже в техническом переводе не пользуюсь системами автоперевода, но при чтении они могут быть полезны для быстрого понимания текста.
ссылка на оригинал статьи https://habr.com/ru/articles/862370/
Добавить комментарий