Множество приложений градиентного спуска в TensorFlow
Опубликовано: 2022-03-11Google TensorFlow — один из ведущих инструментов для обучения и развертывания моделей глубокого обучения. Он способен оптимизировать чрезвычайно сложные архитектуры нейронных сетей с сотнями миллионов параметров и поставляется с широким набором инструментов для аппаратного ускорения, распределенного обучения и производственных рабочих процессов. Эти мощные функции могут показаться пугающими и ненужными за пределами области глубокого обучения.
Но TensorFlow может быть как доступным, так и пригодным для решения более простых задач, не связанных напрямую с обучением моделей глубокого обучения. По своей сути TensorFlow — это просто оптимизированная библиотека для тензорных операций (векторов, матриц и т. д.) и операций исчисления, используемых для выполнения градиентного спуска в произвольных последовательностях вычислений. Опытные специалисты по данным признают «градиентный спуск» фундаментальным инструментом вычислительной математики, но обычно он требует реализации кода и уравнений для конкретного приложения. Как мы увидим, именно здесь вступает в дело современная архитектура «автоматического дифференцирования» TensorFlow.
Варианты использования TensorFlow
- Пример 1: линейная регрессия с градиентным спуском в TensorFlow 2.0
- Что такое градиентный спуск?
- Пример 2. Максимально разбросанные единичные векторы
- Пример 3: Генерация состязательных входных данных ИИ
- Заключительные мысли: оптимизация градиентного спуска
- Градиентный спуск в TensorFlow: от поиска минимумов до атак на системы ИИ
Пример 1: линейная регрессия с градиентным спуском в TensorFlow 2.0
Пример 1 ноутбук
Прежде чем перейти к коду TensorFlow, важно ознакомиться с градиентным спуском и линейной регрессией.
Что такое градиентный спуск?
Проще говоря, это численный метод поиска входных данных для системы уравнений, которые минимизируют ее выходные данные. В контексте машинного обучения эта система уравнений является нашей моделью , входными данными являются неизвестные параметры модели, а выходными данными является минимизируемая функция потерь , которая показывает, насколько велика ошибка между моделью и нашими данными. Для некоторых задач (таких как линейная регрессия) существуют уравнения для прямого вычисления параметров, которые минимизируют нашу ошибку, но для большинства практических приложений нам требуются численные методы, такие как градиентный спуск, чтобы получить удовлетворительное решение.
Самый важный момент этой статьи заключается в том, что градиентный спуск обычно требует составления наших уравнений и использования исчисления для получения взаимосвязи между нашей функцией потерь и нашими параметрами. С TensorFlow (и любым современным инструментом автоматического дифференцирования) вычисления выполняются за нас, поэтому мы можем сосредоточиться на разработке решения и не тратить время на его реализацию.
Вот как это выглядит на простой задаче линейной регрессии. У нас есть выборка роста (h) и веса (w) 150 взрослых мужчин, и мы начинаем с неточной оценки наклона и стандартного отклонения этой линии. Примерно после 15 итераций градиентного спуска мы приходим к почти оптимальному решению.
Давайте посмотрим, как мы создали вышеуказанное решение, используя TensorFlow 2.0.
Для линейной регрессии мы говорим, что веса можно предсказать с помощью линейного уравнения роста.
Мы хотим найти параметры α и β (наклон и точка пересечения), которые минимизируют среднеквадратичную ошибку (потери) между прогнозами и истинными значениями. Итак, наша функция потерь (в данном случае «среднеквадратичная ошибка» или MSE) выглядит так:
Мы можем видеть, как выглядит среднеквадратическая ошибка для пары несовершенных линий, а затем и для точного решения (α=6,04, β=-230,5).
Давайте воплотим эту идею в жизнь с помощью TensorFlow. Первое, что нужно сделать, это запрограммировать функцию потерь, используя тензоры и функции tf.*
.
def calc_mean_sq_error(heights, weights, slope, intercept): predicted_wgts = slope * heights + intercept errors = predicted_wgts - weights mse = tf.reduce_mean(errors**2) return mse
Это выглядит довольно просто. Все стандартные алгебраические операторы перегружены для тензоров, поэтому нам нужно только убедиться, что оптимизируемые переменные являются тензорами, а для всего остального мы используем методы tf.*
.
Затем все, что нам нужно сделать, это поместить это в цикл градиентного спуска:
def run_gradient_descent(heights, weights, init_slope, init_icept, learning_rate): # Any values to be part of gradient calcs need to be vars/tensors tf_slope = tf.Variable(init_slope, dtype='float32') tf_icept = tf.Variable(init_icept, dtype='float32') # Hardcoding 25 iterations of gradient descent for i in range(25): # Do all calculations under a "GradientTape" which tracks all gradients with tf.GradientTape() as tape: tape.watch((tf_slope, tf_icept)) # This is the same mean-squared-error calculation as before predictions = tf_slope * heights + tf_icept errors = predictions - weights loss = tf.reduce_mean(errors**2) # Auto-diff magic! Calcs gradients between loss calc and params dloss_dparams = tape.gradient(loss, [tf_slope, tf_icept]) # Gradients point towards +loss, so subtract to "descend" tf_slope = tf_slope - learning_rate * dloss_dparams[0] tf_icept = tf_icept - learning_rate * dloss_dparams[1]
Давайте на минутку оценим, насколько это аккуратно. Градиентный спуск требует вычисления производных функции потерь по всем переменным, которые мы пытаемся оптимизировать. Предполагается, что здесь будет задействован расчет, но на самом деле мы ничего из этого не делали. Магия в том, что:
- TensorFlow строит график вычислений для каждого вычисления, выполненного в
tf.GradientTape()
. - TensorFlow умеет вычислять производные (градиенты) каждой операции, чтобы определить, как любая переменная в графе вычислений влияет на любую другую переменную.
Как выглядит процесс с разных точек отсчета?
Градиентный спуск заметно приближается к оптимальной MSE, но на самом деле сходится к существенно другому наклону и точке пересечения, чем оптимум в обоих примерах. В некоторых случаях это просто градиентный спуск, сходящийся к локальному минимуму, что является неотъемлемой проблемой алгоритмов градиентного спуска. Но доказуемо, что линейная регрессия имеет только один глобальный минимум. Так как же мы оказались на неправильном склоне и перехвате?
В данном случае проблема в том, что мы упростили код ради демонстрации. Мы не нормализовали наши данные, и параметр наклона имеет другую характеристику, чем параметр пересечения. Крошечные изменения наклона могут привести к значительным изменениям потерь, в то время как крошечные изменения пересечения имеют очень небольшой эффект. Эта огромная разница в масштабе обучаемых параметров приводит к тому, что в расчетах градиента преобладает наклон, а параметр пересечения почти игнорируется.
Таким образом, градиентный спуск эффективно находит лучший наклон очень близко к исходному предположению перехвата. А поскольку ошибка так близка к оптимальной, градиенты вокруг нее крошечные, поэтому каждая последующая итерация смещается лишь на крошечный бит. Предварительная нормализация наших данных значительно улучшила бы это явление, но не устранила бы его.
Это был относительно простой пример, но в следующих разделах мы увидим, что эта возможность «автодифференцирования» может обрабатывать довольно сложные вещи.
Пример 2. Максимально разбросанные единичные векторы
Пример 2. Ноутбук
Следующий пример основан на забавном упражнении по глубокому обучению на курсе глубокого обучения, который я прошел в прошлом году.
Суть проблемы в том, что у нас есть «вариационный автокодер» (VAE), который может создавать реалистичные лица из набора из 32 нормально распределенных чисел. Для идентификации подозреваемых мы хотим использовать VAE для создания разнообразного набора (теоретических) лиц для выбора свидетеля, а затем сузить поиск, создав больше лиц, похожих на те, которые были выбраны. Для этого упражнения было предложено рандомизировать начальный набор векторов, но я хотел найти оптимальное начальное состояние.
Мы можем сформулировать задачу так: в 32-мерном пространстве найдите набор из X единичных векторов, максимально разнесенных друг от друга. В двух измерениях это легко вычислить точно. Но для трех измерений (или 32 измерений!), однозначного ответа нет. Однако, если мы сможем определить правильную функцию потерь, которая будет минимальной, когда мы достигнем целевого состояния, возможно, градиентный спуск поможет нам достичь этого.
Мы начнем с рандомизированного набора из 20 векторов, как показано выше, и поэкспериментируем с тремя различными функциями потерь, каждая из которых имеет возрастающую сложность, чтобы продемонстрировать возможности TensorFlow.
Давайте сначала определим наш тренировочный цикл. Мы поместим всю логику TensorFlow в метод self.calc_loss()
, а затем сможем просто переопределить этот метод для каждой техники, повторяя этот цикл.
# Define the framework for trying different loss functions # Base class implements loop, sub classes override self.calc_loss() class VectorSpreadAlgorithm: # ... def calc_loss(self, tensor2d): raise NotImplementedError("Define this in your derived class") def one_iter(self, i, learning_rate): # self.vecs is an 20x2 tensor, representing twenty 2D vectors tfvecs = tf.convert_to_tensor(self.vecs, dtype=tf.float32) with tf.GradientTape() as tape: tape.watch(tfvecs) loss = self.calc_loss(tfvecs) # Here's the magic again. Derivative of spread with respect to # input vectors gradients = tape.gradient(loss, tfvecs) self.vecs = self.vecs - learning_rate * gradients
Первая техника, которую стоит попробовать, самая простая. Мы определяем метрику спреда, которая представляет собой угол векторов, которые находятся ближе всего друг к другу. Мы хотим максимизировать спред, но принято делать из этого проблему минимизации. Поэтому мы просто берем отрицательное значение показателя спреда:
class VectorSpread_Maximize_Min_Angle(VectorSpreadAlgorithm): def calc_loss(self, tensor2d): angle_pairs = tf.acos(tensor2d @ tf.transpose(tensor2d)) disable_diag = tf.eye(tensor2d.numpy().shape[0]) * 2 * np.pi spread_metric = tf.reduce_min(angle_pairs + disable_diag) # Convention is to return a quantity to be minimized, but we want # to maximize spread. So return negative spread return -spread_metric
Некоторая магия Matplotlib даст визуализацию.
Это неуклюже (в буквальном смысле!), но это работает. Только два из 20 векторов обновляются одновременно, увеличивая расстояние между ними до тех пор, пока они не перестанут быть ближайшими, а затем переключаясь на увеличение угла между новыми двумя ближайшими векторами. Важно отметить, что это работает . Мы видим, что TensorFlow смог передать градиенты через метод tf.reduce_min()
и метод tf.acos()
, чтобы сделать все правильно.
Давайте попробуем что-нибудь более сложное. Мы знаем, что при оптимальном решении все векторы должны иметь одинаковый угол с ближайшими соседями. Итак, добавим к функции потерь «дисперсию минимальных углов».
class VectorSpread_MaxMinAngle_w_Variance(VectorSpreadAlgorithm): def spread_metric(self, tensor2d): """ Assumes all rows already normalized """ angle_pairs = tf.acos(tensor2d @ tf.transpose(tensor2d)) disable_diag = tf.eye(tensor2d.numpy().shape[0]) * 2 * np.pi all_mins = tf.reduce_min(angle_pairs + disable_diag, axis=1) # Same calculation as before: find the min-min angle min_min = tf.reduce_min(all_mins) # But now also calculate the variance of the min angles vector avg_min = tf.reduce_mean(all_mins) var_min = tf.reduce_sum(tf.square(all_mins - avg_min)) # Our spread metric now includes a term to minimize variance spread_metric = min_min - 0.4 * var_min # As before, want negative spread to keep it a minimization problem return -spread_metric
Этот одинокий вектор, направленный на север, теперь быстро присоединяется к своим равным, потому что угол к его ближайшему соседу огромен и резко увеличивает член дисперсии, который теперь минимизируется. Но в конечном итоге это по-прежнему обусловлено глобально-минимальным углом, который по-прежнему медленно увеличивается. Идеи, которые я должен улучшить, обычно работают в этом случае 2D, но не в каких-либо более высоких измерениях.
Но слишком много внимания уделять качеству этой математической попытки — значит упустить суть. Посмотрите, сколько тензорных операций задействовано в вычислениях среднего и дисперсии, и как TensorFlow успешно отслеживает и различает каждое вычисление для каждого компонента во входной матрице. И нам не нужно было делать никаких ручных вычислений. Мы просто объединили несколько простых математических вычислений, а TensorFlow сделал вычисления за нас.
Наконец, давайте попробуем еще одну вещь: решение, основанное на силе. Представьте, что каждый вектор — это маленькая планета, привязанная к центральной точке. Каждая планета излучает силу, которая отталкивает ее от других планет. Если бы мы запустили физическое моделирование этой модели, мы бы пришли к желаемому решению.
Моя гипотеза состоит в том, что градиентный спуск тоже должен работать. При оптимальном решении касательная сила, действующая на каждую планету со стороны любой другой планеты, должна уравновешиваться чистой нулевой силой (если бы она была не нулевой, планеты двигались бы). Итак, давайте вычислим величину силы, действующую на каждый вектор, и воспользуемся градиентным спуском, чтобы приблизить ее к нулю.
Во-первых, нам нужно определить метод, который вычисляет силу, используя методы tf.*
:
class VectorSpread_Force(VectorSpreadAlgorithm): def force_a_onto_b(self, vec_a, vec_b): # Calc force assuming vec_b is constrained to the unit sphere diff = vec_b - vec_a norm = tf.sqrt(tf.reduce_sum(diff**2)) unit_force_dir = diff / norm force_magnitude = 1 / norm**2 force_vec = unit_force_dir * force_magnitude # Project force onto this vec, calculate how much is radial b_dot_f = tf.tensordot(vec_b, force_vec, axes=1) b_dot_b = tf.tensordot(vec_b, vec_b, axes=1) radial_component = (b_dot_f / b_dot_b) * vec_b # Subtract radial component and return result return force_vec - radial_component
Затем мы определяем нашу функцию потерь, используя приведенную выше силовую функцию. Мы накапливаем результирующую силу на каждом векторе и вычисляем ее величину. В нашем оптимальном решении все силы должны уравновешиваться, и мы должны иметь нулевую силу.

def calc_loss(self, tensor2d): n_vec = tensor2d.numpy().shape[0] all_force_list = [] for this_idx in range(n_vec): # Accumulate force of all other vecs onto this one this_force_list = [] for other_idx in range(n_vec): if this_idx == other_idx: continue this_vec = tensor2d[this_idx, :] other_vec = tensor2d[other_idx, :] tangent_force_vec = self.force_a_onto_b(other_vec, this_vec) this_force_list.append(tangent_force_vec) # Use list of all N-dimensional force vecs. Stack and sum. sum_tangent_forces = tf.reduce_sum(tf.stack(this_force_list)) this_force_mag = tf.sqrt(tf.reduce_sum(sum_tangent_forces**2)) # Accumulate all magnitudes, should all be zero at optimal solution all_force_list.append(this_force_mag) # We want to minimize total force sum, so simply stack, sum, return return tf.reduce_sum(tf.stack(all_force_list))
Мало того, что решение прекрасно работает (не считая некоторого хаоса в первых нескольких кадрах), реальная заслуга принадлежит TensorFlow. Это решение включало несколько циклов for
, оператор if
и огромную паутину вычислений, и TensorFlow успешно отследил для нас градиенты во всем этом.
Пример 3: Генерация состязательных входных данных ИИ
Пример 3 Блокнот
В этот момент читатели могут подумать: «Эй! Этот пост не должен был быть о глубоком обучении!» Но технически введение относится к выходу за рамки « обучения моделей глубокого обучения». В этом случае мы не обучаем , а вместо этого используем некоторые математические свойства предварительно обученной глубокой нейронной сети, чтобы обмануть ее и получить неправильные результаты. Это оказалось намного проще и эффективнее, чем предполагалось. И все, что для этого потребовалось, — это еще один короткий фрагмент кода TensorFlow 2.0.
Начнем с поиска классификатора изображений для атаки. Мы будем использовать одно из лучших решений для конкурса Dogs vs. Cats Kaggle; в частности, решение, представленное Kaggler, «уйсимты». Вся заслуга в том, что они предоставили эффективную модель «кошка против собаки» и предоставили отличную документацию. Это мощная модель, состоящая из 13 миллионов параметров на 18 слоях нейронной сети. (Читатели могут прочитать об этом подробнее в соответствующем блокноте.)
Обратите внимание, что цель здесь не в том, чтобы выделить какой-либо недостаток в этой конкретной сети, а в том, чтобы показать, насколько уязвима любая стандартная нейронная сеть с большим количеством входных данных.
Немного повозившись, я смог понять, как загрузить модель и предварительно обработать изображения, которые будут классифицироваться ею.
Это похоже на действительно надежный классификатор! Все классификации образцов верны и имеют достоверность выше 95%. Давайте атаковать!
Мы хотим создать изображение, которое, очевидно, является кошкой, но чтобы классификатор решил, что это собака с высокой степенью достоверности. Как мы можем сделать это?
Давайте начнем с изображения кошки, которое он правильно классифицирует, а затем выясним, как крошечные изменения в каждом цветовом канале (значения 0-255) данного входного пикселя влияют на конечный результат классификатора. Изменение одного пикселя, вероятно, мало что даст, но, возможно, кумулятивные настройки всех значений 128x128x3 = 49 152 пикселя достигнут нашей цели.
Откуда мы знаем, в какую сторону нажимать каждый пиксель? При обычном обучении нейронной сети мы пытаемся минимизировать потери между целевой меткой и предсказанной меткой, используя градиентный спуск в TensorFlow для одновременного обновления всех 13 миллионов свободных параметров. В этом случае мы вместо этого оставим 13 миллионов параметров фиксированными и настроим значения пикселей самого ввода.
Какова наша функция потерь? Ну, насколько образ похож на кота! Если мы вычислим производную от значения кота по отношению к каждому входному пикселю, мы узнаем, в какую сторону нажимать каждый из них, чтобы минимизировать вероятность классификации кота.
def adversarial_modify(victim_img, to_dog=False, to_cat=False): # We only need four gradient descent steps for i in range(4): tf_victim_img = tf.convert_to_tensor(victim_img, dtype='float32') with tf.GradientTape() as tape: tape.watch(tf_victim_img) # Run the image through the model model_output = model(tf_victim_img) # Minimize cat confidence and maximize dog confidence loss = (model_output[0] - model_output[1]) dloss_dimg = tape.gradient(loss, tf_victim_img) # Ignore gradient magnitudes, only care about sign, +1/255 or -1/255 pixels_w_pos_grad = tf.cast(dloss_dimg > 0.0, 'float32') / 255. pixels_w_neg_grad = tf.cast(dloss_dimg < 0.0, 'float32') / 255. victim_img = victim_img - pixels_w_pos_grad + pixels_w_neg_grad
Магия Matplotlib снова помогает визуализировать результаты.
Ух ты! Для человеческого глаза каждая из этих картинок идентична. Тем не менее, после четырех итераций мы убедили классификатор, что это собака, с достоверностью 99,4%!
Давайте удостоверимся, что это не случайность, и это работает и в другом направлении.
Успех! Первоначально классификатор правильно предсказал это как собаку с достоверностью 98,4%, а теперь считает, что это кошка с достоверностью 99,8%.
Наконец, давайте посмотрим на образец патча изображения и посмотрим, как он изменился.
Как и ожидалось, окончательный патч очень похож на оригинал, каждый пиксель смещается только от -4 до +4 в значении интенсивности красного канала. Этого сдвига недостаточно для того, чтобы человек различил разницу, но он полностью меняет вывод классификатора.
Заключительные мысли: оптимизация градиентного спуска
На протяжении всей этой статьи мы рассмотрели ручное применение градиентов к нашим обучаемым параметрам ради простоты и прозрачности. Однако в реальном мире специалисты по данным должны сразу же приступить к использованию оптимизаторов , потому что они, как правило, гораздо более эффективны, не добавляя никакого раздувания кода.
Существует множество популярных оптимизаторов, в том числе RMSprop, Adagrad и Adadelta, но наиболее распространенным, вероятно, является Adam . Иногда их называют «методами адаптивной скорости обучения», потому что они динамически поддерживают разную скорость обучения для каждого параметра. Многие из них используют термины импульса и аппроксимируют производные более высокого порядка с целью избежать локальных минимумов и добиться более быстрой сходимости.
На анимации, заимствованной у Себастьяна Рудера, мы можем видеть, как различные оптимизаторы спускаются по поверхности потерь. Мануальные методы, которые мы продемонстрировали, наиболее сопоставимы с «SGD». Самый эффективный оптимизатор не будет одним и тем же для каждой поверхности потерь; однако более продвинутые оптимизаторы обычно работают лучше, чем более простые.
Однако редко бывает полезно быть экспертом по оптимизаторам — даже тем, кто заинтересован в предоставлении услуг по разработке искусственного интеллекта. Лучше использовать время разработчиков, чтобы ознакомиться с парой, просто чтобы понять, как они улучшают градиентный спуск в TensorFlow. После этого они могут просто использовать Адама по умолчанию и пробовать другие, только если их модели не сходятся.
Для читателей, которые действительно заинтересованы в том, как и почему работают эти оптимизаторы, обзор Рудера, в котором появляется анимация, является одним из лучших и наиболее исчерпывающих ресурсов по этой теме.
Давайте обновим наше решение линейной регрессии из первого раздела, чтобы использовать оптимизаторы. Ниже приведен исходный код градиентного спуска с использованием ручных градиентов.
# Manual gradient descent operations def run_gradient_descent(heights, weights, init_slope, init_icept, learning_rate): tf_slope = tf.Variable(init_slope, dtype='float32') tf_icept = tf.Variable(init_icept, dtype='float32') for i in range(25): with tf.GradientTape() as tape: tape.watch((tf_slope, tf_icept)) predictions = tf_slope * heights + tf_icept errors = predictions - weights loss = tf.reduce_mean(errors**2) gradients = tape.gradient(loss, [tf_slope, tf_icept]) tf_slope = tf_slope - learning_rate * gradients[0] tf_icept = tf_icept - learning_rate * gradients[1]
А вот тот же код, но с использованием оптимизатора. Вы увидите, что лишнего кода практически нет (измененные строки выделены синим цветом):
# Gradient descent with Optimizer (RMSprop) def run_gradient_descent (heights, weights, init_slope, init_icept, learning_rate) : tf_slope = tf.Variable(init_slope, dtype= 'float32' ) tf_icept = tf.Variable(init_icept, dtype= 'float32' ) # Group trainable parameters into a list trainable_params = [tf_slope, tf_icept] # Define your optimizer (RMSprop) outside of the training loop optimizer = keras.optimizers.RMSprop(learning_rate) for i in range( 25 ): # GradientTape loop is the same with tf.GradientTape() as tape: tape.watch( trainable_params ) predictions = tf_slope * heights + tf_icept errors = predictions - weights loss = tf.reduce_mean(errors** 2 ) # We can use the trainable parameters list directly in gradient calcs gradients = tape.gradient(loss, trainable_params ) # Optimizers always aim to *minimize* the loss function optimizer.apply_gradients(zip(gradients, trainable_params))
Вот и все! Мы определили оптимизатор RMSprop
вне цикла градиентного спуска, а затем использовали метод optimizer.apply_gradients()
после каждого вычисления градиента для обновления обучаемых параметров. Оптимизатор определяется вне цикла, поскольку он будет отслеживать исторические градиенты для вычисления дополнительных условий, таких как импульс и производные более высокого порядка.
Посмотрим, как это выглядит с оптимизатором RMSprop .
Выглядит отлично! Теперь давайте попробуем это с оптимизатором Adam .
Вау, что здесь произошло? Похоже, что механика импульса в Адаме заставляет его промахиваться мимо оптимального решения и несколько раз менять курс. Обычно эта механика импульса помогает со сложными поверхностями убытков, но в этом простом случае она вредит нам. Это подчеркивает совет сделать выбор оптимизатора одним из гиперпараметров для настройки при обучении вашей модели.
Любой, кто хочет изучить глубокое обучение, захочет ознакомиться с этим шаблоном, поскольку он широко используется в пользовательских архитектурах TensorFlow, где необходимо иметь сложную механику потерь, которую нелегко включить в стандартный рабочий процесс. В этом простом примере градиентного спуска TensorFlow было всего два обучаемых параметра, но это необходимо при работе с архитектурами, содержащими сотни миллионов параметров для оптимизации.
Градиентный спуск в TensorFlow: от поиска минимумов до атак на системы ИИ
Все фрагменты кода и изображения были созданы из блокнотов в соответствующем репозитории GitHub. Он также содержит сводку всех разделов со ссылками на отдельные записные книжки для читателей, которые хотят увидеть полный код. Ради упрощения сообщения было опущено множество деталей, которые можно найти в обширной встроенной документации.
Я надеюсь, что эта статья была полезной и заставила вас задуматься о способах использования градиентного спуска в TensorFlow. Даже если вы не используете его сами, мы надеемся, что он прояснит, как работают все современные архитектуры нейронных сетей — создайте модель, определите функцию потерь и используйте градиентный спуск, чтобы подогнать модель к вашему набору данных.
Как партнер Google Cloud, специалисты Toptal, сертифицированные Google, доступны для компаний по запросу для их наиболее важных проектов.