В этом посте мы увидим, как реализовать нейронную сеть прямого распространения с нуля на Python. Это продолжение моей предыдущей публикации о нейронных сетях прямого распространения.
Примечание для цитирования: содержание и структура этой статьи основаны на лекциях по глубокому обучению от One-Fourth Labs - Padhai.
Нейронные сети с прямой связью
Нейронные сети с прямой связью также известны как Многослойная сеть нейронов (MLN). Эта сеть моделей называется прямой связью, потому что информация передается только вперед в нейронной сети, через входные узлы, затем через скрытые слои (один или несколько слоев) и, наконец, через выходные узлы.
Возможности традиционных моделей, таких как модели Мак-Каллока-Питтса, персептрона и сигмовидного нейрона, ограничиваются линейными функциями. Чтобы справиться со сложной нелинейной границей решений между входом и выходом, мы используем многослойную сеть нейронов.
Чтобы понять алгоритм обучения нейронной сети с прямой связью и вычисления, присутствующие в сети, любезно обратитесь к моему предыдущему сообщению о нейронных сетях с прямой связью.
Кодирование Часть
В разделе кодирования мы рассмотрим следующие темы.
- Создавайте данные, которые нельзя разделить линейно
- Тренируйтесь с сигмовидным нейроном и оценивайте эффективность
- Напишите с нуля нашу первую сеть прямого распространения
- Обучите сеть FF на данных и сравните с сигмовидным нейроном
- Напишите общий класс для сети FF
- Обучение универсального класса бинарной классификации
- Обучите сеть FF для мультиклассовых данных с помощью функции кросс-энтропийных потерь
Если вы хотите пропустить теоретическую часть и сразу перейти к коду,
PS: Если вы хотите преобразовать код в R, отправьте мне сообщение, когда это будет сделано. Я буду размещать ваши работы здесь, а также на странице GitHub.
Прежде чем мы начнем строить нашу сеть, сначала нам нужно импортировать необходимые библиотеки. Мы импортируем numpy
для оценки умножения матриц и скалярного произведения между двумя векторами, matplotlib
для визуализации данных, а из пакетаsklearn
мы импортируем функции для генерации данных и оценки производительности сети.
Создать фиктивные данные
Помните, что мы используем нейронные сети с прямой связью, потому что мы хотели иметь дело с нелинейно разделяемыми данными. В этом разделе мы увидим, как случайным образом генерировать нелинейно разделимые данные.
Чтобы генерировать данные случайным образом, мы будем использовать make_blobs
, чтобы генерировать капли точек с распределением Гаусса. Я сгенерировал 1000 точек данных в 2D-пространстве с четырьмя каплями centers=4
в качестве задачи прогнозирования классификации нескольких классов. Каждая точка данных имеет два входа и метки классов 0, 1, 2 или 3. Код, представленный в Строках 9, 10, помогает визуализировать данные с помощью диаграммы рассеяния. Мы можем видеть, что они представляют собой 4 центра, и данные линейно разделимы (почти).
На приведенном выше графике я смог представить 3 измерения - 2 входа и метки классов в виде цветов, используя простую диаграмму рассеяния. Обратите внимание, что функция make_blobs()
будет генерировать линейно разделяемые данные, но нам нужны нелинейно разделяемые данные для двоичной классификации.
labels_orig = labels labels = np.mod(labels_orig, 2)
Один из способов преобразовать 4 класса в двоичную классификацию - взять оставшуюся часть этих 4 классов, когда они разделены на 2, чтобы я мог получить новые метки как 0 и 1.
Из графика мы можем видеть, что центры капель сливаются, так что теперь у нас есть проблема двоичной классификации, где граница решения не является линейной. Когда наши данные готовы, я использовал функцию train_test_split
, чтобы разделить данные для training
и validation
в соотношении 90:10.
Тренировка с сигмовидным нейроном
Прежде чем мы начнем обучение данных сигмовидного нейрона, мы построим нашу модель внутри класса под названием SigmoidNeuron.
В классе SigmoidNeuron
у нас есть 9 функций, я проведу вас по этим функциям одну за другой и объясню, что они делают.
def __init__(self): self.w = None self.b = None
Функция __init__
(функция-конструктор) помогает инициализировать параметры w весов и смещений b сигмовидного нейрона на None.
#forward pass def perceptron(self, x): return np.dot(x, self.w.T) + self.b def sigmoid(self, x): return 1.0/(1.0 + np.exp(-x))
Затем мы определим две функции perceptron
и sigmoid
, которые характеризуют прямой проход. В случае прямого прохождения сигмовидного нейрона происходит два этапа.
perceptron
- вычисляет скалярное произведение между входными x и весами w и добавляет смещение bsigmoid
- принимает выходные данные перцептрона и применяет к ним сигмовидную (логистическую) функцию.
#updating the gradients using mean squared error loss def grad_w_mse(self, x, y): ..... def grad_b_mse(self, x, y): ..... #updating the gradients using cross entropy loss def grad_w_ce(self, x, y): ..... def grad_b_ce(self, x, y): .....
Следующие четыре функции характеризуют вычисление градиента. Я написал две отдельные функции для обновления весов w и смещений b с использованием среднеквадратичных потерь ошибок и потерь перекрестной энтропии.
def fit(self, X, Y, epochs=1, learning_rate=1, initialise=True, loss_fn="mse", display_loss=False): ..... return
Затем мы определяем метод «подгонки», который принимает несколько параметров,
X
- Входы
Y
- Ярлыки
epochs
- Количество эпох, в течение которых наш алгоритм будет выполнять итерацию данных, значение по умолчанию равно 1
learning_rate
- Величина изменения наших весов на каждом этапе наших обучающих данных, значение по умолчанию установлено на 1.
intialise
- Произвольно инициализировать параметры модели или нет. Если он установлен на True
, веса будут инициализированы, вы можете установить его на False
, если хотите повторно обучить обученную модель.
loss_fn
- для выбора функции потерь для алгоритма обновления параметров. Это может быть «mse» или «ce».
display_loss
- логическая переменная, указывающая, показывать ли уменьшение потерь для каждой эпохи
В методе fit
мы просматриваем данные, переданные через параметры X и Y, и вычисляем обновленные значения для параметров либо с использованием среднеквадратичных потерь, либо перекрестных потерь энтропии. Как только мы обновим значение, мы перейдем к обновлению весовых коэффициентов и смещений (Строка 49–62).
def predict(self, X):
Теперь мы определяем, что наша функция прогнозирования принимает входные данные X
в качестве аргумента, который, как ожидается, будет массивом numpy
. В функции прогнозирования мы вычислим прямой проход каждого ввода с помощью обученной модели и отправим обратно numpy array
, которое содержит прогнозируемое значение каждого ввода.
Теперь мы обучим наши данные на сигмовидном нейроне, который мы создали. Сначала мы создаем экземпляр класса сигмовидного нейрона, а затем вызываем метод fit
для обучающих данных с 1000 эпох и скоростью обучения, установленной на 1 (эти значения являются произвольными, а не оптимальными значениями для этих данных, вы можете поиграть с этими значениями и найти лучшие количество эпох и скорость обучения). По умолчанию функция потерь установлена на среднеквадратическую потерю ошибки, но вы также можете изменить ее на перекрестную потерю энтропии.
Как видите, потеря сигмовидного нейрона уменьшается, но из-за большой скорости обучения может быть много колебаний. Вы можете уменьшить скорость обучения и проверить вариацию потерь. После обучения модели мы можем делать прогнозы на основе данных тестирования и преобразовывать эти прогнозы в двоичную форму, взяв 0,5 в качестве порога. Мы можем вычислить точность обучения и проверки модели, чтобы оценить производительность модели и проверить возможность улучшения, изменив количество эпох или скорость обучения.
#visualizing the results plt.scatter(X_train[:,0], X_train[:,1], c=Y_pred_binarised_train, cmap=my_cmap, s=15*(np.abs(Y_pred_binarised_train-Y_train)+.2)) plt.show()
Чтобы узнать, какие из точек данных, которые модель предсказывает правильно или нет для каждой точки в обучающем наборе. мы будем использовать функцию точечной диаграммы из matplotlib.pyplot
. Функция принимает два входа в качестве первой и второй функций для цвета, который я использовал Y_pred_binarised_train
и определил настраиваемый cmap для визуализации. Как вы можете видеть, размер каждой точки на графике ниже отличается.
Размер каждой точки на графике определяется формулой
s=15*(np.abs(Y_pred_binarised_train-Y_train)+.2)
Формула принимает абсолютную разницу между прогнозируемым значением и фактическим значением.
- Если истинное значение равно предсказанному значению, то size = 3
- Если истинное значение не равно предсказанному значению, размер = 18
Все маленькие точки на графике указывают на то, что модель правильно предсказывает эти наблюдения, а большие точки указывают на то, что эти наблюдения неправильно классифицированы.
На этом графике мы можем представить 4 измерения - две входные функции, цвет для обозначения разных меток и размер точки указывает, правильно ли она спрогнозирована или нет. Важное замечание из графика заключается в том, что сигмовидный нейрон не может обрабатывать нелинейно разделяемые данные.
Если вы хотите подробно изучить алгоритм обучения сигмовидного нейрона с помощью математики, ознакомьтесь с моим предыдущим постом.
Написать первую нейронную сеть с прямой связью
В этом разделе мы возьмем очень простую нейронную сеть прямого распространения и построим ее с нуля на Python. Всего в сети три нейрона - два в первом скрытом слое и один в выходном слое. Для каждого из этих нейронов предварительная активация представлена буквой «а», а пост-активация - буквой «h». В сети всего 9 параметров - 6 весовых параметров и 3 смещения.
Подобно реализации сигмовидного нейрона, мы напишем нашу нейронную сеть в классе с именем FirstFFNetwork.
В классе FirstFFNetwork
у нас 6 функций, мы рассмотрим эти функции одну за другой.
def __init__(self): .....
Функция __init__
инициализирует все параметры сети, включая веса и смещения. В отличие от сигмовидного нейрона, где у нас есть только два параметра в нейронной сети, у нас есть 9 параметров, которые нужно инициализировать. Все 6 весов инициализируются случайным образом, а 3 смещения обнуляются.
def sigmoid(self, x): return 1.0/(1.0 + np.exp(-x))
Затем мы определяем сигмовидную функцию, используемую для постактивации для каждого из нейронов в сети.
def forward_pass(self, x): #forward pass - preactivation and activation self.x1, self.x2 = x self.a1 = self.w1*self.x1 + self.w2*self.x2 + self.b1 self.h1 = self.sigmoid(self.a1) self.a2 = self.w3*self.x1 + self.w4*self.x2 + self.b2 self.h2 = self.sigmoid(self.a2) self.a3 = self.w5*self.h1 + self.w6*self.h2 + self.b3 self.h3 = self.sigmoid(self.a3) return self.h3
Теперь у нас есть функция прямого прохода, которая принимает входной x и вычисляет результат. Во-первых, я инициализировал две локальные переменные и приравнял их к входу x, который имеет 2 функции.
Для каждого из этих 3 нейронов произойдут две вещи:
Предварительная активация, обозначенная буквой «а»: это взвешенная сумма входов плюс смещение.
Активация обозначается буквой «h»: функция активации является сигмовидной функцией.
Предварительная активация для первого нейрона определяется выражением
a₁ = w₁ * x₁ + w₂ * x₂ + b₁
Чтобы получить значение постактивации для первого нейрона, мы просто применяем логистическую функцию к выходу предварительной активации a₁.
h₁ = sigmoid(a₁)
Повторите тот же процесс для второго нейрона, чтобы получить a₂ и h₂.
Выходы двух нейронов, присутствующих в первом скрытом слое, будут действовать как входные данные для третьего нейрона. Предварительная активация третьего нейрона определяется выражением
a₃ = w₅ * h₁ + w₆ * h₂ + b₃
и применение сигмоида к a₃ даст окончательный прогнозируемый результат.
def grad(self, x, y): #back propagation ......
Затем у нас есть функция grad
, которая принимает входные данные x и y в качестве аргументов и вычисляет прямой проход. На основе прямого прохода он вычисляет частные производные этих весов по функции потерь, которая в данном случае представляет собой среднеквадратичную потерю ошибки.
Примечание: в этом посте я не объясняю, как мы достигаем этих частных производных для параметров. Пока просто рассматривайте эту функцию как черный ящик, в своей следующей статье я объясню, как вычислить эти частные производные при обратном распространении.
def fit(self, X, Y, epochs=1, learning_rate=1, initialise=True, display_loss=False): ......
Затем у нас есть функция fit
, аналогичная сигмовидному нейрону. В этой функции мы перебираем каждую точку данных, вычисляем частные производные, вызывая функцию grad
, и сохраняем эти значения в новой переменной для каждого параметра (Строки 63–75). Затем мы продолжаем и обновляем значения всех параметров (Строки 77–87). У нас также есть условие display_loss
, если оно установлено в True
, оно будет отображать график изменения сетевых потерь для всех эпох.
def predict(self, X): #predicting the results on unseen data .....
Наконец, у нас есть функция прогнозирования, которая принимает большой набор значений в качестве входных данных и вычисляет прогнозируемое значение для каждого входа, вызывая функцию forward_pass
для каждого входа.
Обучите сеть FF на данных
Теперь мы обучим наши данные в созданной нами сети Feedforward. Сначала мы создаем экземпляр класса FirstFFNetwork
, а затем вызываем метод fit
для данных обучения с 2000 эпох и скоростью обучения, установленной на 0,01.
#visualize the predictions plt.scatter(X_train[:,0], X_train[:,1], c=Y_pred_binarised_train, cmap=my_cmap, s=15*(np.abs(Y_pred_binarised_train-Y_train)+.2)) plt.show()
Чтобы получить лучшее представление о производительности нейронной сети, мы будем использовать тот же график 4D визуализации, который мы использовали в сигмовидном нейроне, и сравним его с моделью сигмовидного нейрона.
Как видите, большинство точек правильно классифицируются нейронной сетью. Ключевой вывод заключается в том, что просто объединив три сигмовидных нейрона, мы можем решить проблему нелинейно разделяемых данных.
Общий класс для нейронной сети с прямой связью
В этом разделе мы напишем общий класс, в котором он может генерировать нейронную сеть, взяв количество скрытых слоев и количество нейронов в каждом скрытом слое в качестве входных параметров. Общий класс также принимает количество входов в качестве параметра, раньше у нас было только два входа, но теперь у нас могут быть и n-мерные входы.
Примечание. В данном случае я рассматриваю сеть только для двоичной классификации.
Прежде чем мы начнем писать код для общей нейронной сети, давайте разберемся в формате индексов для представления весов и смещений, связанных с конкретным нейроном.
W (номер слоя) (номер нейрона в слое) (вводимый номер)
b (номер слоя) (номер смещения, связанный с этим вводом)
a (номер слоя) (вводимый номер)
W₁₁₁ - Вес, связанный с первым нейроном, присутствующим в первом скрытом слое, подключенном к первому входу.
W₁₁₂ - Вес, связанный с первым нейроном, присутствующим в первом скрытом слое, подключенном ко второму входу.
b₁₁ - смещение, связанное с первым нейроном, присутствующим в первом скрытом слое.
b₁₂ - Смещение, связанное со вторым нейроном, присутствующим в первом скрытом слое.
Код:
Функция за функцией объяснение,
def __init__(self, n_inputs, hidden_sizes=[2]): #intialize the inputs self.nx = n_inputs self.ny = 1 #one final neuron for binary classification. self.nh = len(hidden_sizes) self.sizes = [self.nx] + hidden_sizes + [self.ny] .....
Функция __init__
принимает несколько аргументов,
n_inputs
- Количество входов, идущих в сеть.
hidden_sizes
- ожидает список целых чисел, представляет количество нейронов, присутствующих в скрытом слое.
- [2] - Один скрытый слой с 2 нейронами
- [2,3] - Два скрытых слоя с 2 нейронами в первом слое и 3 нейронами во втором слое.
В этой функции мы инициализируем два словаря W и B для хранения случайно инициализированных весов и смещений для каждого скрытого слоя в сети.
def forward_pass(self, x): self.A = {} self.H = {} self.H[0] = x.reshape(1, -1) ....
В функции forward_pass
мы инициализировали два словаря A и H, и вместо представления входных данных как X я представляю их как H₀, так что мы можем сохранить это в словаре после активации H. Затем мы пройдемся по всем слоям и вычислим значения до и после активации и сохраним их в соответствующих словарях. Результат предварительной активации последнего слоя такой же, как и прогнозируемое значение нашей сети. Функция вернет это значение снаружи. Таким образом, мы можем использовать это значение для расчета потери нейрона.
Помните, что в предыдущем классе FirstFFNetwork мы жестко запрограммировали вычисление предварительной активации и постактивации для каждого нейрона отдельно, но это не относится к нашему универсальному классу.
def grad_sigmoid(self, x): return x*(1-x) def grad(self, x, y): self.forward_pass(x) .....
Затем мы определяем две функции, которые помогают вычислить частные производные параметров по функции потерь.
def fit(self, X, Y, epochs=1, learning_rate=1, initialise=True, display_loss=False): # initialise w, b if initialise: for i in range(self.nh+1): self.W[i+1] = np.random.randn(self.sizes[i], self.sizes[i+1]) self.B[i+1] = np.zeros((1, self.sizes[i+1]))
Затем мы определяем нашу fit
функцию, которая, по сути, такая же, но здесь мы перебираем каждый из входных данных и обновляем веса и смещения в обобщенном виде, а не обновляем отдельные параметры.
def predict(self, X): #predicting the results on unseen data .....
Наконец, у нас есть функция прогнозирования, которая принимает большой набор значений в качестве входных данных и вычисляет прогнозируемое значение для каждого входа, вызывая функцию forward_pass
для каждого входа.
Обучить универсальный класс для нейронной сети с прямой связью
Теперь мы обучим наши данные в созданной нами общей сети прямого распространения. Сначала мы создаем экземпляр класса FFSNetwork
, а затем вызываем метод fit
для данных обучения с 2000 эпохами и скоростью обучения, установленной на 0,01.
Вариация потерь нейронной сети для обучающих данных приведена ниже.
Из графика мы видим, что функция потерь падает немного медленнее, чем в предыдущей сети, потому что в этом случае у нас есть два скрытых слоя с 2 и 3 нейронами соответственно. Поскольку это большая сеть с большим количеством параметров, алгоритму обучения требуется больше времени, чтобы изучить все параметры и распространить потери по сети.
#visualize the predictions plt.scatter(X_train[:,0], X_train[:,1], c=Y_pred_binarised_train, cmap=my_cmap, s=15*(np.abs(Y_pred_binarised_train-Y_train)+.2)) plt.show()
Мы снова будем использовать тот же четырехмерный график для визуализации прогнозов нашей общей сети. Помните, что маленькие точки указывают на то, что эти наблюдения правильно классифицированы, а большие точки указывают на то, что эти наблюдения не классифицированы.
Вы можете поиграть с количеством эпох и скоростью обучения и посмотреть, можно ли уменьшить ошибку ниже текущего значения. Кроме того, вы можете создать гораздо более глубокую сеть с множеством нейронов на каждом уровне и посмотреть, как эта сеть работает.
Общий класс FF для мультиклассовой классификации
В этом разделе мы расширим нашу универсальную функцию, написанную в предыдущем разделе, для поддержки мультиклассовой классификации. Прежде чем мы приступим к созданию нашего универсального класса, нам нужно выполнить некоторую предварительную обработку данных.
Помните, что изначально мы сгенерировали данные с 4 классами, а затем преобразовали эти данные нескольких классов в данные двоичного класса. В этом разделе мы будем использовать эти исходные данные для обучения нашей многоклассовой нейронной сети.
Здесь у нас есть 4 разных класса, поэтому мы кодируем каждую метку, чтобы машина могла понимать и выполнять вычисления поверх нее. Для кодирования этикеток мы будем использовать sklearn.OneHotEncoder
на этикетках для обучения и проверки.
Мы напишем нашу общую сеть прямой связи для мультиклассовой классификации в классе с именем FFSN_MultiClass
.
Я объясню изменения, какие изменения были внесены в наш предыдущий класс FFSNetwork
, чтобы он работал для многоклассовой классификации.
Во-первых, у нас есть функция forward_pass
,
def forward_pass(self, x): self.A = {} self.H = {} self.H[0] = x.reshape(1, -1) for i in range(self.nh): self.A[i+1] = np.matmul(self.H[i], self.W[i+1]) + self.B[i+1] self.H[i+1] = self.sigmoid(self.A[i+1]) self.A[self.nh+1] = np.matmul(self.H[self.nh], self.W[self.nh+1]) + self.B[self.nh+1] self.H[self.nh+1] = self.softmax(self.A[self.nh+1]) return self.H[self.nh+1]
Поскольку у нас есть мультиклассовый выход из сети, мы используем активацию softmax вместо сигмоидной активации на выходном слое. В Строках 29–30 мы используем слой softmax для вычисления прямого прохода на выходном слое.
def cross_entropy(self,label,pred): yl=np.multiply(pred,label) yl=yl[yl!=0] yl=-np.log(yl) yl=np.mean(yl) return yl
Далее у нас есть функция потерь. В этом случае вместо среднеквадратичной ошибки мы используем функцию потерь кросс-энтропии. Используя потерю кросс-энтропии, мы можем найти разницу между предсказанным распределением вероятностей и фактическим распределением вероятностей для вычисления потерь в сети.
Обучить общий класс для мультиклассовой классификации
Теперь мы обучим наши данные в созданной нами общей мультиклассовой сети прямого распространения. Сначала мы создаем экземпляр класса FFSN_MultiClass
, а затем вызываем метод fit
для данных обучения с 2000 эпох и скоростью обучения, установленной на 0,005. Помните, что наши данные имеют два входа и 4 закодированных метки.
Вариация потерь нейронной сети для обучающих данных приведена ниже.
Мы снова будем использовать тот же четырехмерный график для визуализации прогнозов нашей общей сети. Чтобы построить график, нам нужно получить последнюю предсказанную метку из сети, чтобы получить это предсказанное значение, я применил функцию argmax
, чтобы получить метку с наибольшей вероятностью. Используя эту метку, мы можем построить наш четырехмерный график и сравнить его с фактическим графиком разброса входных данных.
Итак, мы успешно построили нашу универсальную нейронную сеть для мультиклассовой классификации с нуля.
Что дальше?
УЧИТЬСЯ ПО КОДИРОВКЕ
В этой статье мы использовали функцию make_blobs
для генерации игрушечных данных и увидели, что make_blobs
генерирует линейно разделяемые данные. Если вы хотите сгенерировать некоторые сложные нелинейно разделяемые данные для обучения нейронной сети прямого распространения, вы можете использовать функцию make_moons
из пакета sklearn
.
Функция make_moons
генерирует два чередующихся полукруглых данных, по сути, дает вам нелинейно разделимые данные. Кроме того, вы можете добавить в данные некоторый гауссов шум, чтобы нейронной сети было сложнее прийти к нелинейно разделимой границе решения.
Используя наш общий класс нейронной сети, вы можете создать гораздо более глубокую сеть с большим количеством нейронов в каждом слое (также разным количеством нейронов в каждом слое) и поиграться со скоростью обучения и количеством эпох, чтобы проверить, при каких параметрах нейронная сеть способна. чтобы достичь наилучшей возможной границы решения.
Весь код, обсуждаемый в статье, присутствует в этом репозитории GitHub. Не стесняйтесь разветвлять или скачивать.
PS: Если вы хотите преобразовать код в R, отправьте мне сообщение, когда это будет сделано. Я буду размещать ваши работы здесь, а также на странице GitHub.
Если вы хотите узнать больше об искусственной нейронной сети, посмотрите Искусственные нейронные сети Абхишека и Пухраджа из Starttechacademy. Также этот курс будет проходить в последней версии Tensorflow 2.0 (бэкэнд Keras). У них также есть очень хороший комплект по машинному обучению (базовый + продвинутый) на языках Python и R.
Заключение
В этом посте мы построили простую нейронную сеть с нуля и увидели, что она работает хорошо, в то время как наш сигмовидный нейрон не может обрабатывать нелинейно разделяемые данные. Затем мы увидели, как написать общий класс, который может принимать количество входов 'n' и количество скрытых слоев 'L (с множеством нейронов для каждого слоя) для двоичная классификация с использованием среднеквадратичной ошибки как функции потерь. После этого мы расширили наш общий класс, чтобы обрабатывать мультиклассовую классификацию, используя softmax и кросс-энтропию в качестве функции потерь, и увидели, что он работает достаточно хорошо.
Рекомендуемая литература
В моем следующем посте я подробно объясню обратное распространение ошибки вместе с некоторой математикой. Поэтому убедитесь, что вы подписаны на меня в среде, чтобы получать уведомления, как только они упадут.
А пока мир :)
NK.
Ниранджан Кумар - стажер аналитического подразделения HSBC. Он увлечен глубоким обучением и искусственным интеллектом. Он является одним из лучших авторов Medium по теме Искусственный интеллект. Свяжитесь со мной в LinkedIn или подпишитесь на меня в Twitter, чтобы получать новости о предстоящих статьях по глубокому обучению и искусственному интеллекту.
Заявление об ограничении ответственности. В этом сообщении могут быть партнерские ссылки на соответствующие ресурсы. Вы можете приобрести комплект по самой низкой цене. Я получу небольшую комиссию, если вы купите курс.