AI-движки на примере Knowledge Distillation, GAN, Reinforcement learning. ai.. ai. distillation.. ai. distillation. GAN.. ai. distillation. GAN. lake.. ai. distillation. GAN. lake. machine learning.. ai. distillation. GAN. lake. machine learning. machinelearning.. ai. distillation. GAN. lake. machine learning. machinelearning. python.. ai. distillation. GAN. lake. machine learning. machinelearning. python. reinforcement-learning.. ai. distillation. GAN. lake. machine learning. machinelearning. python. reinforcement-learning. tensorflow.

Привет хабр!

Я хочу поделиться своими наблюдениями и размышлениями на тему работы сеток-дуэтов в современных архитектурах нейросетей.

Возьму как пример 3 подхода :

  1. Архитектура GAN, основанная на состязательности нейросетей

  2. Архитектура Knowledge Distillation, основанная на совместном обучении и дистилляции

  3. Архитектура Reinforcement learning, основанная на последовательной или разделенной обработке

1. GAN – Генеративно – состязательные сети.

В данном случае мы рассмотрим "поединок" двух нейросетей - Генератор и Дискриминатор, рассмотрим каждого:

В данном случае мы рассмотрим “поединок” двух нейросетей – Генератор и Дискриминатор, рассмотрим каждого:

Генератор – это сеть, что получает на вход, так называемые, скрытые переменные (latent space) (случайный шум), а на выходе получаются данные ( изображение). Проще говоря постоянно рисует новые картинки, стараясь сделать их максимально похожими на настоящие.

Подгружаем датасет MNIST и смотрим случайную картинку из него, на и нем будем отрабатывать обучение

(X_train, y_train), (_, _) = tf.keras.datasets.mnist.load_data()
i = np.random.randint(0, 60000)
print(y_train[i])
plt.imshow(X_train[i], cmap='gray');

Смотрим предварительно случайную картинку из всего датасета

число 8 из датасета

число 8 из датасета

Cоздаем сеть Генератора

def build_generator():
  network = tf.keras.Sequential()

  network.add(layers.Dense(7*7*256, use_bias=False, input_shape=(100, )))
  network.add(layers.BatchNormalization())
  network.add(layers.LeakyReLU())

  network.add(layers.Reshape((7, 7, 256)))

  # 7x7x128
  network.add(layers.Conv2DTranspose(128, (5,5), padding='same', use_bias=False))
  network.add(layers.BatchNormalization())
  network.add(layers.LeakyReLU())

  # 14x14x64
  network.add(layers.Conv2DTranspose(64, (5,5), strides = (2,2), padding='same', use_bias=False))
  network.add(layers.BatchNormalization())
  network.add(layers.LeakyReLU())

  # 28x28x1
  network.add(layers.Conv2DTranspose(1, (5,5), strides = (2,2), padding='same', use_bias=False, activation='tanh'))

  network.summary()

  return network

Билдим и смотрим таблицу, как сформировались слои Генератора

generator = build_generator()
Model: "sequential_1"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ Layer (type)                    ┃ Output Shape           ┃       Param # ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ dense_1 (Dense)                 │ (None, 12544)          │     1,254,400 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ batch_normalization_3           │ (None, 12544)          │        50,176 │
│ (BatchNormalization)            │                        │               │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ leaky_re_lu_3 (LeakyReLU)       │ (None, 12544)          │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ reshape_1 (Reshape)             │ (None, 7, 7, 256)      │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ conv2d_transpose_3              │ (None, 7, 7, 128)      │       819,200 │
│ (Conv2DTranspose)               │                        │               │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ batch_normalization_4           │ (None, 7, 7, 128)      │           512 │
│ (BatchNormalization)            │                        │               │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ leaky_re_lu_4 (LeakyReLU)       │ (None, 7, 7, 128)      │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ conv2d_transpose_4              │ (None, 14, 14, 64)     │       204,800 │
│ (Conv2DTranspose)               │                        │               │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ batch_normalization_5           │ (None, 14, 14, 64)     │           256 │
│ (BatchNormalization)            │                        │               │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ leaky_re_lu_5 (LeakyReLU)       │ (None, 14, 14, 64)     │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ conv2d_transpose_5              │ (None, 28, 28, 1)      │         1,600 │
│ (Conv2DTranspose)               │                        │               │
└─────────────────────────────────┴────────────────────────┴───────────────┘
 Total params: 2,330,944 (8.89 MB)
 Trainable params: 2,305,472 (8.79 MB)
 Non-trainable params: 25,472 (99.50 KB)

Создаем “случайный шум”, с которого всё начинается для Генератора.

Важный момент: Генератор не формирует шум. Шум создается разработчиком/системой и подается на вход генератору.

noise = tf.random.normal([1, 100])
noise
<tf.Tensor: shape=(1, 100), dtype=float32, numpy=
array([[-1.2752672 , -0.31896377, -1.621226  , -0.07633732, -1.1005756 ,
        -0.8244959 ,  0.32265383, -1.3580662 ,  0.81300926,  1.3841189 ,
         1.1405385 ,  1.3428733 , -0.20784518,  0.2218569 , -0.80084634,
        -0.51266044, -0.5123262 ,  1.2493849 , -0.41784754,  0.11716219,
         0.75289106, -0.04998856, -0.10687224,  0.15446882,  0.23294541,
        -0.45333463,  0.29856005, -2.002146  ,  1.035649  ,  0.00998143,
        -1.4422241 , -0.4550751 ,  0.24101041,  0.3818386 ,  0.8918707 ,
         0.3421659 , -1.0747958 ,  0.07026866,  0.92490923,  0.05733351,
        -1.83129   ,  0.07838591, -1.9661248 ,  0.67199177,  0.52293086,
        -0.7199154 ,  1.1893344 ,  1.3752289 , -0.6383991 ,  0.00620717,
         2.937654  , -0.08155467, -0.04186288,  0.2946215 ,  0.08486137,
         0.40340146,  0.31229848,  0.8557363 , -1.2216685 , -0.8172701 ,
        -0.5734942 ,  1.3174502 ,  1.0580747 , -2.3497086 ,  0.331117  ,
        -0.92475957,  2.0144198 , -0.26455778,  1.0036913 ,  0.08436549,
         0.9257641 , -0.35675535, -0.8078421 , -0.06051978,  2.069581  ,
        -0.24463454, -0.8282004 ,  1.6013093 ,  1.0182651 ,  0.87454027,
        -0.27339602,  1.0042901 ,  0.21967338, -2.185581  , -0.562119  ,
         0.5870542 , -0.5030319 ,  0.8525667 , -0.30234253,  1.629835  ,
         1.3273429 , -0.3319834 , -0.37302145,  0.6386749 , -1.4167624 ,
         1.1601201 , -0.89931196,  0.10231236, -0.5188213 ,  0.645322  ]],
      dtype=float32)>

Открываем “шумную” картинку

generated_image = generator(noise, training = False)
generated_image.shape
plt.imshow(generated_image[0,:,:,0], cmap='gray');

plt.imshow(generated_image[0,:,:,0], cmap=’gray’);

Подготовка генератора завершена!

Дискриминатор – пытается угадать, какая картинка из настоящего набора данных, а какую только что нарисовали.

Строим сеть дискриминатора :

def build_discriminator():
  network = tf.keras.Sequential()

  # 14x14x64
  network.add(layers.Conv2D(64, (5,5), strides=(2,2), padding='same', input_shape=[28,28,1]))
  network.add(layers.LeakyReLU())
  network.add(layers.Dropout(0.3))

  # 7x7x128
  network.add(layers.Conv2D(128, (5,5), strides=(2,2), padding='same'))
  network.add(layers.LeakyReLU())
  network.add(layers.Dropout(0.3))

  network.add(layers.Flatten())
  network.add(layers.Dense(1))

  network.summary()

  return network

Смотрим сеть дискриминатора :

discriminator = build_discriminator()

-
Model: "sequential_1"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 conv2d (Conv2D)             (None, 14, 14, 64)        1664      
                                                                 
 leaky_re_lu_3 (LeakyReLU)   (None, 14, 14, 64)        0         
                                                                 
 dropout (Dropout)           (None, 14, 14, 64)        0         
                                                                 
 conv2d_1 (Conv2D)           (None, 7, 7, 128)         204928    
                                                                 
 leaky_re_lu_4 (LeakyReLU)   (None, 7, 7, 128)         0         
                                                                 
 dropout_1 (Dropout)         (None, 7, 7, 128)         0         
                                                                 
 flatten (Flatten)           (None, 6272)              0         
                                                                 
 dense_1 (Dense)             (None, 1)                 6273      
                                                                 
=================================================================
Total params: 212,865
Trainable params: 212,865
Non-trainable params: 0
_________________________________________________________________

Подготовка дискриминатора завершена!

Let’s train

X_train
epochs = 100
noise_dim = 100
num_images_to_generate = 16

@tf.function
def train_steps(images):
  noise = tf.random.normal([batch_size, noise_dim])
  with tf.GradientTape() as gen_tape, tf.GradientTape() as disc_tape:
    generated_images = generator(noise, training = True)

    expected_output = discriminator(images, training = True)
    fake_output = discriminator(generated_images, training = True)

    gen_loss = generator_loss(fake_output)
    disc_loss = discriminator_loss(expected_output, fake_output)

  gradients_of_generator = gen_tape.gradient(gen_loss, generator.trainable_variables)
  gradients_of_discriminator = disc_tape.gradient(disc_loss, discriminator.trainable_variables)

  generator_optimizer.apply_gradients(zip(gradients_of_generator, generator.trainable_variables))
  discriminator_optimizer.apply_gradients(zip(gradients_of_discriminator, discriminator.trainable_variables))

test_images = tf.random.normal([num_images_to_generate, noise_dim])

test_images.shape

60000 / 256

def train(dataset, epochs, test_images):
  for epoch in range(epochs):
    for image_batch in dataset:
      #print(image_batch.shape)
      train_steps(image_batch)

    print('Epoch: ', epoch + 1)
    generated_images = generator(test_images, training = False)
    fig = plt.figure(figsize=(10,10))
    for i in range(generated_images.shape[0]):
      plt.subplot(4,4,i+1)
      plt.imshow(generated_images[i, :, :, 0] * 127.5 + 127.5, cmap='gray')
      plt.axis('off')
    plt.show()

train(X_train, epochs, test_images)

2 нейросети соревнуются друг с другом, и благодаря этой борьбе “художник” (Генератор) становится просто гениальным подражателем!

Ниже представлены шаги эпох при обучении нейросети

6-я эпоха

6-я эпоха
39-я эпоха

39-я эпоха
91-я эпоха

91-я эпоха

По сути GAN, это AI-движки для творчества. Они изучают, как выглядят настоящие данные, и потом могут создавать свои — бесконечные лица, картинки, звуки и т.д.
Ссылка на Google Colab – https://colab.research.google.com/drive/1yPBi2fxYsfRb1FFLdD475hdM5PjFFfG3#scrollTo=Bo8TdC1JtbSc

2. Knowledge Distillation — Дистилляция знаний

Чтобы дистиллировать знания из одной модели в другую, мы берем предобученную teacher-модель, обученную на определенной задаче (в данном случае — классификация изображений), и инициализируем student-модель со случайными весами для обучения на той же задаче классификации изображений.

(Дистилляция знаний) — это как "учитель" передает опыт "ученику" в мире искусственного интеллекта.

(Дистилляция знаний) — это как “учитель” передает опыт “ученику” в мире искусственного интеллекта.

Перейдем к примеру :

Используем модель merve/beans-vit-224 в качестве teacher-модели. Это модель классификации изображений, основанная на google/vit-base-patch16-224-in21k, дообученная на наборе данных beans (бобовые). Мы будем дистиллировать эту модель в случайно инициализированную MobileNetV2.

Загружаем датасет :

from datasets import load_dataset

dataset = load_dataset("beans")

Подготавливаем данные для работы с teacher-моделью :

from transformers import AutoImageProcessor
teacher_processor = AutoImageProcessor.from_pretrained("merve/beans-vit-224")

def process(examples):
    processed_inputs = teacher_processor(examples["image"])
    return processed_inputs

processed_datasets = dataset.map(process, batched=True)

По сути, мы хотим, чтобы student-модель (случайно инициализированный MobileNet) имитировала teacher-модель (дообученный Vision Transformer). Для достижения этой цели мы сначала получаем данные на выходе как от teacher-модели, так и от student-модели.

from transformers import TrainingArguments, Trainer, infer_device
import torch
import torch.nn as nn
import torch.nn.functional as F

class ImageDistilTrainer(Trainer):
    def __init__(self, teacher_model=None, student_model=None, temperature=None, lambda_param=None,  *args, **kwargs):
        super().__init__(model=student_model, *args, **kwargs)
        self.teacher = teacher_model
        self.student = student_model
        self.loss_function = nn.KLDivLoss(reduction="batchmean")
        device = infer_device()
        self.teacher.to(device)
        self.teacher.eval()
        self.temperature = temperature
        self.lambda_param = lambda_param

    def compute_loss(self, student, inputs, return_outputs=False):
        student_output = self.student(**inputs)

        with torch.no_grad():
          teacher_output = self.teacher(**inputs)


        soft_teacher = F.softmax(teacher_output.logits / self.temperature, dim=-1)
        soft_student = F.log_softmax(student_output.logits / self.temperature, dim=-1)


        distillation_loss = self.loss_function(soft_student, soft_teacher) * (self.temperature ** 2)


        student_target_loss = student_output.loss

      
        loss = (1. - self.lambda_param) * student_target_loss + self.lambda_param * distillation_loss
        return (loss, student_output) if return_outputs else loss

Логинимся в Hugging Face (тут понадобится ваш токен доступа из HF). Через Trainer мы можем выгрузить нашу модель в Hugging Face Hub

from huggingface_hub import notebook_login

notebook_login()

Теперь установим параметры обучения (TrainingArguments), модель-учитель и модель-ученик

from transformers import AutoModelForImageClassification, MobileNetV2Config, MobileNetV2ForImageClassification

repo_name = "ИМЯ ВАШЕГО РЕПОЗИТОРИЯ"

training_args = TrainingArguments(
    output_dir="my-awesome-model",
    num_train_epochs=30,
    fp16=True,
    logging_dir=f"{repo_name}/logs",
    logging_strategy="epoch",
    eval_strategy="epoch",
    save_strategy="epoch",
    load_best_model_at_end=True,
    metric_for_best_model="accuracy",
    report_to="tensorboard",
    push_to_hub=True,
    hub_strategy="every_save",
    hub_model_id=repo_name,
    )

num_labels = len(processed_datasets["train"].features["labels"].names)

# initialize models
teacher_model = AutoModelForImageClassification.from_pretrained(
    "merve/beans-vit-224",
    num_labels=num_labels,
    ignore_mismatched_sizes=True
)

# training MobileNetV2 from scratch
student_config = MobileNetV2Config()
student_config.num_labels = num_labels
student_model = MobileNetV2ForImageClassification(student_config)

После обучения модель будет доступна по ссылке huggingface.co/твой-username/my-model 

Мы можем применить функцию compute_metrics, чтобы оценить нашу модель на тестовой выборке. Данная функция будет задействована во время тренировочного процесса для расчета точности и F1-score модели.

import evaluate
import numpy as np

accuracy = evaluate.load("accuracy")

"""compute_metrics — это кастомная функция,
которая автоматически вызывается Trainer'ом 
во время обучения для мониторинга качества модели."""

def compute_metrics(eval_pred):
    predictions, labels = eval_pred
    acc = accuracy.compute(references=labels, predictions=np.argmax(predictions, axis=1))
    return {"accuracy": acc["accuracy"]}

Перейдем к созданию экземпляра Trainer с определенными нами параметрами обучения

from transformers import Trainer

class CompatibleImageDistilTrainer(ImageDistilTrainer):
    def compute_loss(self, model, inputs, return_outputs=False, **kwargs):
        # Ignore num_items_in_batch and other unexpected kwargs
        return super().compute_loss(model, inputs, return_outputs)

# Use the compatible trainer instead
trainer = CompatibleImageDistilTrainer(
    student_model=student_model,
    teacher_model=teacher_model,
    args=training_args,
    train_dataset=processed_datasets["train"],
    eval_dataset=processed_datasets["validation"],
    data_collator=data_collator,
    processing_class=teacher_processor,
    compute_metrics=compute_metrics,
    temperature=5,
    lambda_param=0.5
)

Следующий шаг после этой настройки — запуск процесса дистилляции, где student будет учиться у teacher

Let’s train)

trainer.train()
Модель обучается "2 раза от эпохи к эпохе"

Модель обучается “2 раза от эпохи к эпохе”

Запустим тест и посмотрим на результат

trainer.evaluate(processed_datasets["test"])

Точность: ~ 68%, это на ~5% лучше, чем MobileNet обученный с нуля (~63%).

{'eval_loss': 0.6835939288139343,
 'eval_accuracy': 0.6875,
 'eval_runtime': 13.9354,
 'eval_samples_per_second': 9.185,
 'eval_steps_per_second': 1.148,
 'epoch': 30.0}

Дистилляция сработала успешно! Student model действительно научилась у teacher model. Модель стала значительно лучше благодаря передаче знаний от teacher model к student model.

Дистилляция знаний — это AI-движок для передачи мудрости. Она берёт знания большой, медленной, но умной модели-учителя и упаковывает их в маленькую, быструю модель-ученика — как будто опытный мастер передаёт свои секреты подмастерью.

Ссылка на Google Colab – https://colab.research.google.com/drive/1s4IrEe6JyXpOhpny7UvKwnRPQypr8RRs

3. Reinforcement learning – обучение с подкреплением

Обучение с подкреплением — это про автономность и адаптацию и в условиях неопределенности. RL лежит в основе беспилотных автомобилей, игровых ИИ и роботов — создавая не просто «умные алгоритмы», а самостоятельных рисерчеров, способных к настоящей стратегии и росту через серию проб и ошибок.

Агент - исследователь и Среда - полигон

Агент – исследователь и Среда – полигон

Возьмем к примеру обучение с подкреплением на Python с использованием библиотекиgymnasium (современная версия OpenAI Gym).

Ставим необходимые библиотеки и импортируем их

pip install gymnasium numpy

import gymnasium as gym
import numpy as np
import random

Создаем среду FrozenLake – упрощенная игра “найди выход из замерзшего озера”

env = gym.make('FrozenLake-v1', desc=None, map_name="4x4", is_slippery=False)

Инициализируем таблицу (состояния × действия)

# 16 состояний (4x4 клетки) × 4 действия (влево, вниз, вправо, вверх)
q_table = np.zeros([env.observation_space.n, env.action_space.n])

# Параметры обучения
learning_rate = 0.1
discount_factor = 0.99  # Насколько ценим будущие награды
epsilon = 1.0  # Вероятность случайного действия (exploration)
epsilon_decay = 0.999
min_epsilon = 0.01
episodes = 1000

Среда (Environment)

FrozenLake-v1: Упрощенная игра где агент должен дойти от старта (S) до цели (G), избегая провалов в воду (H)

S – старт, F – замерзшая поверхность, H – провал, G – цель

Начинаем обучение :

for episode in range(episodes):
    state, info = env.reset()
    done = False
    total_reward = 0
    
    while not done:
        # Epsilon-greedy стратегия: с вероятностью epsilon выбираем случайное действие
        if random.uniform(0, 1) < epsilon:
            action = env.action_space.sample()  # Случайное действие (exploration)
        else:
            action = np.argmax(q_table[state])  # Лучшее действие (exploitation)
        
        # Выполняем действие в среде
        next_state, reward, terminated, truncated, info = env.step(action)
        done = terminated or truncated
        
        old_value = q_table[state, action]
        next_max = np.max(q_table[next_state])
        
        new_value = old_value + learning_rate * (
            reward + discount_factor * next_max - old_value
        )
        q_table[state, action] = new_value
        
        state = next_state
        total_reward += reward
    
    # Уменьшаем epsilon после каждого эпизода
    epsilon = max(min_epsilon, epsilon * epsilon_decay)
    
    if (episode + 1) % 100 == 0:
        print(f"Эпизод {episode + 1}, Epsilon: {epsilon:.3f}, Награда: {total_reward}")

print("nОбучение завершено!")
print("nИтоговая Q-таблица:")
print(q_table)

"""Получаем вывод

Начинаем обучение...
Эпизод 100, Epsilon: 0.905, Награда: 0.0
Эпизод 200, Epsilon: 0.819, Награда: 0.0
Эпизод 300, Epsilon: 0.741, Награда: 0.0
Эпизод 400, Epsilon: 0.670, Награда: 0.0
Эпизод 500, Epsilon: 0.606, Награда: 0.0
Эпизод 600, Epsilon: 0.549, Награда: 1.0
Эпизод 700, Epsilon: 0.496, Награда: 1.0
Эпизод 800, Epsilon: 0.449, Награда: 1.0
Эпизод 900, Epsilon: 0.406, Награда: 1.0
Эпизод 1000, Epsilon: 0.368, Награда: 1.0

Обучение завершено!

Итоговая Q-таблица:
[[0.94147845 0.95099005 0.93191546 0.94146579]
 [0.94144155 0.         0.76517588 0.79404897]
 [0.25227238 0.92225733 0.18540704 0.37700074]
 [0.44469117 0.         0.09322424 0.02430739]
 [0.9509832  0.96059601 0.         0.94146724]
 [0.         0.         0.         0.        ]
 [0.         0.97919907 0.         0.56246919]
 [0.         0.         0.         0.        ]
 [0.96045605 0.         0.970299   0.95097178]
 [0.95927825 0.97900787 0.9801     0.        ]
 [0.96991831 0.99       0.         0.95961833]
 [0.         0.         0.         0.        ]
 [0.         0.         0.         0.        ]
 [0.         0.89988563 0.98996049 0.86611194]
 [0.97278094 0.98831215 1.         0.97813866]
 [0.         0.         0.         0.        ]]

"""

Тестируем обученного агента :

print("nТестируем обученного агента:")
state, info = env.reset()
done = False
steps = 0

while not done and steps < 20:
    action = np.argmax(q_table[state])  # Всегда выбираем лучшее действие
    state, reward, terminated, truncated, info = env.step(action)
    done = terminated or truncated
    steps += 1
    
    env.render()  # Показываем визуализацию
    print(f"Шаг {steps}: Действие {action}, Награда {reward}")
    
    if done:
        if reward > 0:
            print("🎉 Успех! Агент достиг цели!")
        else:
            print("💥 Провал! Агент упал в воду!")

env.close()

"""

Вывод :

Шаг 1: Действие 1, Награда 0.0
 Шаг 2: Действие 1, Награда 0.0
 Шаг 3: Действие 2, Награда 0.0
 Шаг 4: Действие 2, Награда 0.0
 Шаг 5: Действие 1, Награда 0.0
 Шаг 6: Действие 2, Награда 1.0
 🎉 Успех! Агент достиг цели!

"""

После достаточного количества эпизодов агент научится:

  • Находить безопасный путь от старта к цели

  • Избегать провалов в воду

  • Действовать оптимально в каждом состоянии

Этот пример демонстрирует основные принципы RL: агент взаимодействует со средой, получает награды и учится через метод проб и ошибок!

Дополнительная визуализация:

def print_policy(q_table):
    actions = ['←', '↓', '→', '↑']
    policy = []
    
    for state in range(16):
        best_action = np.argmax(q_table[state])
        policy.append(actions[best_action])
    

    for i in range(0, 16, 4):
        print(' '.join(policy[i:i+4]))

print_policy(q_table)

Вывод:
  
  """
↓ ← ↓ ←
↓ ← ↓ ←
→ → ↓ ←
← → → ←
"""

Обучение с подкреплением — это AI-движок для навигации в реальном мире. Это как вырастить цифрового ребенка, который учится методом проб и ошибок — не по учебнику, а на собственном опыте, получая награды за успехи и уроки за ошибки.

Ссылка на Google Colab – https://colab.research.google.com/drive/12Qu0vF6ETfO7s5PiD0u_fePJZ72f8TM5#scrollTo=a2crPdCoDKBy

на этом все) до новых встреч)

Автор: idinahuyvasya

Источник

Rambler's Top100