Давайте еще раз глянем отрывок нашего последнего кода!
re>class SimpleConvNet(Module): def __init__(self): . def forward(self, x): x = self.conv1(x) x = self.relu(x) x = self.maxpool1(x) x = self.conv2(x) x = self.relu(x) x = self.maxpool2(x) x = self.flatten(x) x = self.linear1(x) x = self.relu(x) x = self.linear2(x) return x
Подумайте, какие потенциальные случаи мы можем упустить из рассмотрения?
Я предложу несколько идей:
- Что если мы производим вычисления вне определенных нами слоёв? Например:
re>class SimpleConvNet(Module): def __init__(self): . def forward(self, x): x = self.conv1(x) x = self.relu(x) x = self.maxpool1(x) x = self.conv2(x) x = self.relu(x) x = x * 2 x = self.maxpool2(x) x = x ** 2 x = self.flatten(x) x = self.linear1(x) x = self.relu(x) x = x + 1 x = self.linear2(x) return x
Если вы вглядитесь в метод .backward() класса CrossEntropyLoss , то поймете, что мы не умеем обрабатывать случаи когда наши значения изменяются вне классов Linear , Conv2d , BatchNorm2d и т.д. Если запустим градиентный спуск, то он будет работать так, как будто этих промежуточных вычислений не было, что очевидно не приведёт ни к чему хорошему. А если мы еще каким-то образом меняли размер наших матриц, то программа вообще упадёт с ошибкой.

- Вспомните, как мы производили алгоритм вычисления градиентов? Мы их считали на бумаге, а потом переписывали в коде. Так было с линейными слоями, свёрточными слоями, слоями нормализации. Но как только вы захотим что-то более сложное реализовать, мы просто утоним в бесконечных вычислениях градиентов! Вот, например, я считал градиенты для RNN . И это только самая простая реализация рекуррентных слоев, для LSTM и GRU — у которых ещё более сложные зависимости, я сомневаюсь что-то вообще реально выписать формулы. А использовать их очень хочется! Значит надо что-то придумать!
- Нет гибкости! Для каждой операции нам нужно продумать градиент! Нам хочется, чтобы программа сама считала градиенты! Точно также, как мы возложили вычисление градиентов на метод .backward() в слоях, теперь на .backward() — мы хотим возложить вообще все вычисления!
Все наши проблемы поможет решить граф вычислений!
Computational Graph
Давайте представлять все наши вычисления в виде графа, например!

В узлах будут храниться значения при инициализации либо результат действия операции. Узел в синем квадратике будет хранить в себе информацию, что была произведена операция "сложение", двух чисел a и b , и полученный результат — число c .Узел в красном квадратике будет хранить в себе информацию, что была произведена операция "умножение", двух чисел b и c , и полученный результат — число f .Посчитаем производные такой функции.

На самом деле мы просто получили красивую визуализацию chain rule!
Ключевая идея заключается в том, чтобы после каждой операции получать не только значение, но и сразу считать производные.

Например, c = a + b . Тогда в c мы будем хранить само значение c , а также производные от a и b , то есть ( c , 1 , 1 )Для c = a * b будем хранить ( c , b , a ), для c = a / b , будем хранить ( c , 1 / b , -a / b^2 ) — логика я надеюсь стала понятна.Как только мы определим все базовые операции и их производные, мы сможем производить любые вычисления и брать любые производные от них, потому что мы на каждом этапе считаем свой локальный градиент — а это задача сильно проще, чем брать градиент от итогового выражения. Пример из жизни. Вы читаете много книг разных жанров. Но вы также хотите, чтобы каждый жанр лежал в своей полке. Что проще — после прочтения каждой книги класть в нужную полку или перебирать целую стопку накопившихся книг? Также например и здесь

Проще посчитать производную всей функции или сначала от x1/x2 , потом от sin(x) , потом exp(x2) и в конце просто перемножить их по правилу chain rule?
Переменная (или узел) содержит две части данных:
- value — значение переменной.
- local_gradients — дочерние переменные и соответствующие «локальные производные».
Функция get_gradients использует данные из local_gradients переменных для рекурсивного обхода графа, вычисляя градиенты. (Т. е. local_gradients содержит ссылки на дочерние переменные, у которых есть свои local_gradients , которые содержат ссылки на дочерние переменные, у которых есть свои local_gradients , и так далее.)
Градиент переменной относительно дочерней переменной вычисляется с использованием следующих правил:
- Для каждого пути от переменной к дочерней переменной умножьте значения рёбер пути (что даёт path_value ).
- Сложите все path_value для каждого пути.
… Это даёт частную производную первого порядка переменной относительно дочерней переменной.
Давайте реализуем скелет будущего класса!
re>class Tensor: def __init__(self, value, local_gradients=None): self.value = value self.local_gradients = local_gradients
Попробуем перезагрузить операцию сложения!
re>class Tensor: def __init__(self, value, local_gradients=None): self.value = value self.local_gradients = local_gradients def __add__(self, other): value = self.value + other.value local_gradients = ((self, 1), (other, 1)) return Tensor(value, local_gradients)
Посмотрим на работу
re>a = Tensor(5) b = Tensor(10) c = a + b c, c.value, c.local_gradients >>> (, 15, ((, 1), (, 1)))
Попробуем теперь посчитать производные! Добавим метод .backward()
re>from collections import defaultdict class Tensor: def backward(self): # словарь в котором будем хранить градиенты для всех переменных gradients = defaultdict(lambda: 0) # рекурсивно вызываемая функция для вычисления градиентов у детей, потом у их детей и т.д. def compute_gradients(obj, path_value): if obj.local_gradients: # проверяем не является ли узел листом (leaf) # получаем ссылку на ребенка и его предпосчитанный градиент for child, local_grad_value in obj.local_gradients: # используем chain rule и умножаем накопленный градиент на градиент child path_value_to_child = path_value * local_grad_value # добавляем градиенты от разных листьев gradients[child] += path_value_to_child # считаем градиенты для детей текущего child compute_gradients(child, path_value_to_child) compute_gradients(self, path_value=1) return gradients
re>a = Tensor(5) b = Tensor(10) c = a + b gradients = c.backward() gradients[a], gradients[b] >>> (1, 1)
Теперь перегрузим операцию умножения
re>def __mul__(self, other): value = self.value * other.value local_gradients = ((self, other.value), (other, self.value)) return Tensor(value, local_gradients)
Обратите внимание, в качестве производной по первому объекту, будет значение второго и наоборот!
re>a = Tensor(4) b = Tensor(3) c = a + b # = 4 + 3 = 7 d = a * c # = 4 * 7 = 28 gradients = d.backward() print('d.value =', d.value) print("The partial derivative of d with respect to a python">print('gradients[b] =', gradients[b]) print('gradients[c] =', gradients[c]) >>> gradients[b] = 4 gradients[c] = 4
Посмотрим на промежуточные градиенты
re>print('dict(d.local_gradients)[a] =', dict(d.local_gradients)[a]) print('dict(d.local_gradients)[c] =', dict(d.local_gradients)[c]) print('dict(c.local_gradients)[a] =', dict(c.local_gradients)[a]) print('dict(c.local_gradients)[b] =', dict(c.local_gradients)[b]) >>> dict(d.local_gradients)[a] = 7 dict(d.local_gradients)[c] = 4 dict(c.local_gradients)[a] = 1 dict(c.local_gradients)[b] = 1
Всё верно, можете перепроверить на бумаге!
Добавим еще несколько базовых операций!
re># вычитание def __sub__(self, other): value = self.value - other.value local_gradients = ((self, 1), (other, -1)) return Tensor(value, local_gradients) # унарный минус def __neg__(self): value = -self.value local_gradients = ((self, -1),) return Tensor(value, local_gradients) # деление def __truediv__(self, other): value = self.value / other.value local_gradients = ((self, 1 / other.value), (other, - self.value / (other.value**2))) return Tensor(value, local_gradients)
Посчитаем производную по аргументам более сложной функции
И ещё раз проверим!
re>def f(a, b): return ((a ** 2) / Tensor.sin(b) - a) * (b / a + Tensor.cos(a) + b) * (a - Tensor.exp(b)) a = Tensor(230.3) b = Tensor(33.2) y = f(a, b) gradients = y.backward() print("The partial derivative of y with respect to a =", gradients[a]) print("The partial derivative of y with respect to b =", gradients[b]) delta = Tensor(1e-10) numerical_grad_a = (f(a + delta, b) - f(a, b)) / delta numerical_grad_b = (f(a, b + delta) - f(a, b)) / delta print("The numerical estimate for a =", numerical_grad_a.value) print("The numerical estimate for b formula inline" source="C = AB" alt="C = AB" src="https://habrastorage.org/getpro/habr/formulas/6/66/668/668262ec6fc9765062d39f653a74efe3.svg" width="auto" height="auto"/>,
Для вычисления производной по матрице (где ):
Таким образом:
Аналогично для производной по матрице :
Но есть одна загвоздочка! Посмотрим ещё раз на реализацию метода
.backward()
re>for child, local_grad_value in obj.local_gradients: path_value_to_child = path_value * local_grad_value gradients[child] += path_value_to_child compute_gradients(child, path_value_to_child)
re>def __add__(self, other): value = self.value + other.value local_gradients = ((self, lambda x: x), (other, lambda x: x)) return Tensor(value, local_gradients=local_gradients)
Давайте хранить в local_gradients — не само значение производной, а функцию, которая будет получать значение с предыдущего шага и преобразовывать его для следующего шага. В данном случае функция получает x и возвращает также x , так как производная равна 1 . Для вычитания
re>def __sub__(self, other): value = self.value + other.value local_gradients = ((self, lambda x: x), (other, lambda x: -x)) return Tensor(value, local_gradients=local_gradients)
Для первого слагаемого она получает x и возвращает также x , а для второго она получает x а возвращает -x , так как производная равна -1 . Также предлагаю хранить название операции, это будет очень полезно для отладки!
re>def __add__(self, other): value = self.value + other.value local_gradients = (('add', self, lambda x: x), ('add', other, lambda x: x)) return Tensor(value, local_gradients=local_gradients)
Итак, матричное умножение будет выглядеть в конечном итоге так!
И метод .backward() тоже немного преобразится с учётом наших изменений.
re>for operation, child, child_gradient_func in obj.local_gradients: # child_gradient_func как раз та самая lambda функция path_value_to_child = child_gradient_func(path_value) gradients[child] += path_value_to_child compute_gradients(child, path_value_to_child)
Теперь мы готовы переходить к нейронным слоям. Нужно будет немного перестроить логику! Вернемся к самому первому примеру с "изображением" собаки. Теперь это будет не объект numpy.ndarray , а объект нашего нового класса Tensor
re>input_x = np.array([[ 0.99197708, -0.77980023, -0.8391331 , -0.41970686, 0.72636492], [ 0.85901409, -0.22374584, -1.95850625, -0.81685145, 0.96359871], [-0.42707937, -0.50053309, 0.34049477, 0.62106931, -0.76039365], [ 0.34206742, 2.15131285, 0.80851759, 0.28673013, 0.84706839], [-1.70231094, 0.36473216, 0.33631525, -0.92515589, -2.57602677]]) target_x = [1.0, 0.0] input_tensor = Tensor(input_x) target_tensor = Tensor(target_x)
Класс Module и Linear оставим такими же, только теперь веса модели это также объекты класса Tensor
Давайте при инициализации объекта класса Tensor , проверять является ли он объектом этого же класса или является объектом другого класса (число или numpy.ndarray ) и переводить его в объект numpy.ndarray .Также добавим информацию о форме объекта в атрибут shape
re>class Tensor: def __init__(self, value, local_gradients=None): if isinstance(value, Tensor): self.value = value.value self.local_gradients = value.local_gradients else: self.value = np.array(value) self.local_gradients = local_gradients self.shape = self.value.shape
re>class SimpleNet(Module): def __init__(self): super().__init__() self.linear1 = Linear(input_channels=5, output_channels=10, bias=True) def forward(self, x): return self.linear1(x) model = SimpleNet() model(input_tensor).shape >>> (5, 10)
Отлично, модель выдаёт значения! Добавим ещё несколько полезных команд!
Метод reshape , он нам пригодится, так как мы часто будем менять размеры наших тензоров:
re>def reshape(self, *args): local_gradients = (('reshape', self, lambda x: x.reshape(self.shape)),) return Tensor(self.value.reshape(*args), local_gradients=local_gradients)
Отображение: сейчас вывод нашего тензоры выглядит так, не очень красиво и информативно: Давайте поправим
re>def __repr__(self): return np.array_repr(self.value)
Теперь: >>> array(4) Добавим ещё несколько полезных команд:
Попробуем более сложную модель, немного поменяв наши слои
re>class Flatten: def __init__(self): Parameter([self, []]) def __call__(self, x): self.init_shape = x.shape return x.reshape(self.init_shape[0], -1) class ReLU: def __init__(self): pass def __call__(self, x): return x * (Tensor.sign(x) + 1) / 2
re>class SimpleNet(Module): def __init__(self): super().__init__() self.linear1 = Linear(input_channels=25, output_channels=10, bias=True) self.linear2 = Linear(input_channels=10, output_channels=2, bias=True) self.flatten = Flatten() self.relu = ReLU() def forward(self, x): x_1 = self.flatten(x) x_2 = self.linear1(x_1) x_3 = self.relu(x_2) x_4 = self.linear2(x_3) return x_4 model = SimpleNet() model(input_tensor.reshape(1, -1)) >>> array([[ 0.2440679 , -1.75806267]])
Круто! Всё работает, осталось обучить! Дальше считаем значение loss -функции.
re>class CrossEntropyLoss: def __init__(self): self.predicted = None self.true = None def __call__(self, logits, true): predicted = Tensor.exp(logits) / Tensor.sum(Tensor.exp(logits), axis=1).reshape(-1, 1) # softmax self.true = true # вычисляем значение лосс-функции прямо по формуле self.loss = Tensor.sum(self.true * Tensor.log(predicted + 1e-5), axis=1) * -1 return self
Заметьте, эта та же самая реализация, что и в прошлых статьях, но мы поменяли np на Tensor .
re>loss = loss_fn(model(input_tensor.reshape(1, -1)), target_tensor) loss.loss >>> array([0.2581563])
Что ж, мы получили значение и мы уже знаем, что можем вызвать .backward() прямо с тензора loss.loss , чтобы посчитать все градиенты (спойлер: не сможем, у нас вылетит ошибка)! Но открою для вас небольшой секретик. Градиент для кросс-энтропии + softmax можно считать не через граф, а через формулу. Вот так вот!
Мы убегали от формульных вычислений, а сами же вернулись к ним. Но здесь это оправдано, ведь вспомните какая там простая производная получается, а значит мы может сделать небольшой трюк

Для него нам потребуется добавить метод detach — он вытаскивает тензор из графа. То есть это просто матрица значений.
re>def detach(self): return Tensor(self.value)
re>class CrossEntropyLoss: def __call__(self, predicted, true): ### сохраним значения выхода модели self.logits = Tensor(predicted, local_gradients=predicted.local_gradients) ### self.predicted = Tensor.softmax(predicted) # softmax #number_of_classes = predicted.shape[1] #self.true = Tensor.int_(Tensor.arange(0, number_of_classes) == true) self.true = true # вычисляем значение лосс-функции прямо по формуле self.loss = Tensor.sum(self.true * Tensor.log(self.predicted + 1e-5), axis=1) * -1 return self def backward(self): # Посчитаем градиент по формуле self.analytics = (self.predicted - self.true) # Вытащим из графа, то есть по факту просто получим значения и домножим на self.logits, который всё еще находится в графе. self.analytics = self.analytics.detach() * self.logits self.gradients = self.analytics.backward()
То есть мы с помощью self.analytics подменили вычисление производной внутри графа. А домножив self.analytics на self.logits , мы вернулись в граф, который был еще до применения softmax и кросс-энтропии, и уже отсюда можем честно считать градиенты внутри графа!
Ещё раз: self.logits.backward() — посчитает градиенты для графа, в котором нет softmax + кросс-энтропии, а ((self.predicted — self.true).detach() * self.logits).backward() — также посчитает градиенты для графа, в котором нет softmax + кросс-энтропии, но при этом неявно учтёт их существование за счет множителя (self.predicted — self.true).detach()
re>loss.backward() loss.gradients[model.linear1.weight].shape, loss.gradients[model.linear1.bias].shape >>> ((25, 10), (1, 10))
Теперь давайте снова ручками сделаем градиентный спуск и обучим модель!
re>model = SimpleNet() loss_fn = CrossEntropyLoss() lr = 0.01 for i in range(100): output = model(input_tensor.reshape(1, -1)) loss = loss_fn(output, target_tensor) loss.backward() gradients = loss.gradients for layer in [model.linear1, model.linear2]: layer.weight.value = layer.weight.value - lr * gradients[layer.weight] layer.bias.value = layer.bias.value - lr * gradients[layer.bias] if i % 10 == 0: print(loss.loss) >>> array([1.29812516]) array([0.46082039]) array([0.21713806]) array([0.13151886]) array([0.0906402]) array([0.06659202]) array([0.05139489]) array([0.04118782]) array([0.03398361]) array([0.0286905])
Ура. Наша модель обучается! Идем дальше и запихнём градиентный спуск в уже знакомый нам SGD
re>class Tensor: def __init__(self, value, local_gradients=None): self.shape = self.value.shape self.grad = 0 class CrossEntropyLoss def backward(self): self.analytics = (self.predicted - self.true) self.analytics = self.analytics.detach() * self.logits self.gradients = self.analytics.backward() global Parameter for index, layer in enumerate(Parameter.layers[::-1]): if type(layer).__name__ == 'Linear': layer.weight.grad += self.gradients[layer.weight] / self.loss.shape[0] layer.bias.grad += self.gradients[layer.bias] / self.loss.shape[0] class SGD: def __init__(self, model, lr=2e-4): self.model = model self.lr = lr def step(self): for index, layer in enumerate(self.model._constructor_Parameter.layers[::-1]): if type(layer).__name__ == 'Linear': layer.weight.value -= self.lr * layer.weight.grad layer.bias.value -= self.lr * layer.b.grad.mean(axis=0)
Но посмотрите сюда
re>layer.weight.grad += self.gradients[layer.weight] / self.loss.shape[0] layer.bias.grad += self.gradients[layer.bias] / self.loss.shape[0]
Это же накопление градиентов! А разве оно нам нужно? Нет! Значит нам нужно обнулять накопленные градиенты в каждой операции, для этого введём новый метод .zero_grad()
re>class Module: def zero_grad(self): for index, layer in enumerate(self._constructor_Parameter.layers): if type(layer).__name__ == 'Linear': layer.weight.grad = 0 layer.bias.grad = 0
re>model = SimpleNet() loss_fn = CrossEntropyLoss() optim = SGD(model.parameters(), lr=1e-3) lr = 0.001 for i in range(100): output = model(input_tensor.reshape(1, -1)) loss = loss_fn(output, target_tensor) model.zero_grad() loss.backward() optim.step() if i % 10 == 0: print(loss.loss) >>> array([0.51065697]) array([0.15970178]) array([0.01386941]) array([0.00090227]) array([4.67924761e-05]) array([-6.95378636e-06]) array([-9.88413122e-06]) array([-9.99684238e-06]) array([-9.99989122e-06]) array([-9.99994927e-06])
Круто! Мы обучили нашу первую нейронку на графе вычислений!
NOTE!
В следующем блоке я буду рассказывать реализацию свёрточной нейронки на графе вычислений. По итогу она работает, но не обучается. Ошибку я не успел найти, но очень постараюсь отладить код и дополнить статью. Я решил оставить эту часть, так как хотел донести именно идейную составляющую моего рассказа. И пусть обучить модель не получится, я надеюсь понимание происходящего у читателя останется!
Conv2d
Круто! Усложним задачу
И заключительная проверка с нашей реализацией из предыдущей статьи
Отлично! Мы научились проводить свёртку с помощью матричного перемножения, теперь добавим эту операцию в наш класс и определим для неё производную!
- Используется метод np.add.at , который позволяет эффективно добавлять значения в массив temp на основе path_value .
- Для работы с "окнами" в массиве используется ещё одно представление sliding_window_view с параметром writeable=True , что позволяет модифицировать данные.
Как вы также могли увидеть, что мы несколько раз использовали операцию .transpose() , но не определили её в классе, исправим!
re>def transpose(self, *args): local_gradients = (('transpose', self, lambda x: x.transpose(*args)),) return Tensor(self.value.transpose(*args), local_gradients=local_gradients)
Наконец переопределим класс Conv2d с учётом новых знаний
Собираем модель для обучения на MNIST . Код для подготовки данных возьмём из прошлой статьи.
А вот так примерно будет выглядеть вычисление градиентов!
re>mul child shape: (64, 10) obj shape: (64, 10) mul child shape: (64, 10) obj shape: (64, 10) add child shape: (64, 10) obj shape: (64, 10) matmul child shape: (64, 50) obj shape: (64, 10) div child shape: (64, 50) obj shape: (64, 50) mul child shape: (64, 50) obj shape: (64, 50) add child shape: (64, 50) obj shape: (64, 50) matmul child shape: (64, 1296) obj shape: (64, 50) reshape child shape: (64, 4, 18, 18) obj shape: (64, 1296) div child shape: (64, 4, 18, 18) obj shape: (64, 4, 18, 18) mul child shape: (64, 4, 18, 18) obj shape: (64, 4, 18, 18) add child shape: (64, 4, 18, 18) obj shape: (64, 4, 18, 18) reshape child shape: (64, 4, 324) obj shape: (64, 4, 18, 18) transpose child shape: (64, 324, 4) obj shape: (64, 4, 324) matmul child shape: (64, 324, 36) obj shape: (64, 324, 4) reshape child shape: (64, 1, 18, 18, 1, 4, 3, 3) obj shape: (64, 324, 36) slide child shape: (64, 4, 20, 20) obj shape: (64, 1, 18, 18, 1, 4, 3, 3) div child shape: (64, 4, 20, 20) obj shape: (64, 4, 20, 20) mul child shape: (64, 4, 20, 20) obj shape: (64, 4, 20, 20) add child shape: (64, 4, 20, 20) obj shape: (64, 4, 20, 20) reshape child shape: (64, 4, 400) obj shape: (64, 4, 20, 20) transpose child shape: (64, 400, 4) obj shape: (64, 4, 400) matmul child shape: (64, 400, 36) obj shape: (64, 400, 4) reshape child shape: (64, 1, 20, 20, 1, 4, 3, 3) obj shape: (64, 400, 36) slide child shape: (64, 4, 22, 22) obj shape: (64, 1, 20, 20, 1, 4, 3, 3)
re>class SimpleConvNet(Module): def __init__(self): super().__init__() self.conv1 = Conv2d(input_channels = 1, output_channels = 5, kernel_size=5) #28 -> 24 self.conv2 = Conv2d(input_channels = 5, output_channels = 10, kernel_size=5) #24 -> 20 self.conv3 = Conv2d(input_channels = 10, output_channels = 20, kernel_size=5) #20 -> 16 self.conv4 = Conv2d(input_channels = 20, output_channels = 20, kernel_size=5) #16 -> 12 self.conv5 = Conv2d(input_channels = 20, output_channels = 20, kernel_size=5) #12 -> 8 self.conv6 = Conv2d(input_channels = 20, output_channels = 10, kernel_size=5) #8 -> 4 self.flatten = Flatten() self.linear1 = Linear(input_channels= 4 * 4 * 10, output_channels=20, bias=True) self.linear2 = Linear(input_channels= 20, output_channels=10, bias=True) self.relu = ReLU() def forward(self, x): x = self.conv1(x) x = self.relu(x) x = self.conv2(x) x = self.relu(x) x = self.conv3(x) x = self.relu(x) x = self.conv4(x) x = self.relu(x) x = self.conv5(x) x = self.relu(x) x = self.conv6(x) x = self.relu(x) x = self.flatten(x) x = self.linear1(x) x = self.relu(x) x = self.linear2(x) return x model = SimpleConvNet() loss_fn = CrossEntropyLoss() optim = SGD(model.parameters(), lr=1e-3) for i in range(5): y_pred_list = [] y_true_list = [] for index, batch in enumerate(data_loader): input_x, target = batch input_x = input_x / 255 input_x = np.expand_dims(input_x, axis=1) # (64, 28, 28) -> (64, 1, 28, 28) input_tensor = Tensor(input_x) target_tensor = Tensor(target) output = model(input_tensor) loss = loss_fn(output, target_tensor) model.zero_grad() loss.backward() optim.step() print(loss.loss.value.mean()) >>> 2.3434739196082752 2.3261346480555405 2.3450367034537822 2.328755621690293 2.290884864380055 2.3062695760361183 2.312287414927344 2.3049557593729144 2.2829010337160796
Не обучается

Но надеюсь вы хотя бы поняли идею!
Я опустил очень много моментов, например добавление __hash__ , __eq__ , работу с градиентами внутри оптимизатора, проверка совпадения размерностей тензоров, обработку broadcasting для всех операций. Все они не несут большой идейной составляющей, но безусловно необходимы для корректной работы всех алгоритмов. Я не стал зацикливать на этом внимание и надеюсь вы поймете меня!
КУЛЬМИНАЦИЯ
Итак, вспоминаем самый первый блок кода из первой статьи!
re># Создаем простой набор данных X = torch.randn(100, 3) # 100 примеров с 3 признаками y = torch.randint(0, 2, (100,)) # 100 меток классов (0 или 1) # Определим простую нейронную сеть class SimpleNN(nn.Module): def __init__(self): super(SimpleNN, self).__init__() self.fc1 = nn.Linear(3, 5) # Первый слой: 3 входа, 5 выходов self.fc2 = nn.Linear(5, 2) # Второй слой: 5 входов, 2 выхода (классы) self.softmax = nn.Softmax(dim=1) # Для получения вероятностей классов def forward(self, x): x = torch.relu(self.fc1(x)) # Применяем активацию ReLU x = self.fc2(x) # Второй слой x = self.softmax(x) # Преобразуем в вероятности return x # Создаем модель model = SimpleNN() # Определяем функцию потерь и оптимизатор criterion = nn.CrossEntropyLoss() # Кросс-энтропия для многоклассовой классификации optimizer = optim.SGD(model.parameters(), lr=0.01) # Стохастический градиентный спуск # Обучаем модель num_epochs = 100 for epoch in range(num_epochs): # Прямой проход outputs = model(X) # Вычисление потерь loss = criterion(outputs, y) # Обратный проход optimizer.zero_grad() # Обнуляем градиенты loss.backward() # Вычисляем градиенты optimizer.step() # Обновляем параметры модели
Смотрим и понимаем: Мы с вами разобрались в каждой строчке этого кода. Как я и обещал, собрав знания я одну библиотеку, мы заменим наконец
re>import torch import torch.nn as nn
re>import import .nn as nn
А так я могу сделать например со своей библиотекой
re>import candle import candle.nn as nn

Основная часть моего рассказа подошла к концу. Я надеюсь, что смог достаточно понятно пояснить за работу алгоритмов глубокого обучения и библиотеки PyTorch! Спасибо за внимание!
В следующей финальной статье, я хочу уже воспользовавшись собственной написанной библиотекой реализовать и запустить обученный GPT2, тем самым показав, что мы в достаточной степени овладели мастерством машинного обучения!
Пользовательский ввод (input) в Python
Обычно программа работает по такой схеме: получает входные данные → обрабатывает их → выдает результат. Ввод может поступать как непосредственно от пользователя через клавиатуру, так и через внешний источник (файл, база данных).
В стандартной библиотеке Python 3 есть встроенная функция input() (в Python 2 это raw_input() ), которая отвечает за прием пользовательского ввода. Разберемся, как она работает.
Чтение ввода с клавиатуры
Функция input([prompt]) отвечает за ввод данных из потока ввода:
s = input() print(f"Привет, !") > мир # тут мы с клавиатуры ввели слово "мир" > Привет, мир!
- При вызове функции input() выполнение программы приостанавливается до тех пор, пока пользователь не введет текст на клавиатуре (приложение может ждать бесконечно долго).
- После нажатия на Enter , функция input() считывает данные и передает их приложению (символ завершения новой строки не учитывается).
- Полученные данные присваиваются переменной и используются дальше в программе.
input() всегда возвращает строку:
s = input() print(type(s)) > 2 >
Также у input есть необязательный параметр prompt – это подсказка пользователю перед вводом:
name = input("Введите имя: ") print(f"Привет, !") > Введите имя: Вася > Привет, Вася!
Более подробное описание функции из документации:
def input([prompt]): """ Read a string from standard input. The trailing newline is stripped. The prompt string, if given, is printed to standard output without a trailing newline before reading input. If the user hits EOF (*nix: Ctrl-D, Windows: Ctrl-Z+Return), raise EOFError. On *nix systems, readline is used if available. """ pass
Преобразование вводимых данных
Данные, введенные пользователем, попадают в программу в виде строки, поэтому и работать с ними можно так же, как и со строкой. Если требуется организовать ввод цифр, то строку можно преобразовать в нужный формат с помощью функций явного преобразования типов.
☝️ Важно : если вы решили преобразовать строку в число, но при этом ввели строку (например: test), возникнет ошибка:
ValueError: invalid literal for int() with base 10: ‘test’
На практике такие ошибки нужно обрабатывать через try except . В примере ниже пользователь будет вводить данные до тех пор, пока они успешно не преобразуются в число.
def get_room_number(): while True: try: num = int(input("Введите номер комнаты: ")) return num except ValueError: print("Вы ввели не число. Повторите ввод") room_number = get_room_number() print(f"Комната успешно забронирована!") > Введите номер комнаты: test > Вы ввели не число. Повторите ввод > Введите номер комнаты: 13 > Комната 13 успешно забронирована!
Input() → int
Для преобразования в целое число используйте функцию int() . В качестве аргумента передаются данные которые нужно преобразовать, а на выходе получаем целое число:
age_str = input("Введите ваш возраст: ") age = int(age_str) print(age) print(type(age)) > Введите ваш возраст: 21 > 21 >
То же самое можно сделать в одну строку: age = int(input("Введите ваш возраст: ")) .
Input() → float
Если нужно получить число с плавающей точкой (не целое), то его можно получить с помощью функции float() .
weight = float(input("Укажите вес (кг): ")) print(weight) print(type(weight)) > Укажите вес (кг): 10.33 > 10.33 >
Input() → list (список)
Если в программу вводится информация, которая разделяется пробелами, например, "1 word meow", то ее легко преобразовать в список с помощью метода split() . Он разбивает введенные строки по пробелам и создает список:
list = input().split() print(list) print(type(list)) > 1 word meow > [‘1’, ‘word’, ‘meow’] >
Обратите внимание, что каждый элемент списка является строкой. Для преобразования в число, можно использовать int() и цикл for. Например, так:
int_list = [] for element in input().split(): int_list.append(int(element)) print([type(num) for num in int_list]) > 1 2 3 > [, , ]
Ввод в несколько переменных
Если необходимо заполнить одним вводом с клавиатуры сразу несколько переменных, воспользуйтесь распаковкой:
В этом примере строка из input() разбивается по пробелу функцией split() . Далее применяется синтаксис распаковки – каждый элемент списка попадает в соответствующую переменную.
Все переменные после распаковки будут строкового типа. Преобразовать их (например в int) можно так:
a, b = [int(s) for s in input().split()] print(f"type a: , type b: ") > 13 100 > type a: , type b:
☝️ Важно : не забывайте обрабатывать ошибки:
- если введенных значений больше чем переменных, получите ошибку – ValueError: too many values to unpack (expected 3) ;
- если введенных значений меньше чем переменных, получите ошибку – ValueError: not enough values to unpack (expected 3, got 2) ;
- если преобразовываете в int, а вводите строку – ValueError: invalid literal for int() with base 10: ‘test’ .
В этом руководстве вы узнали, как принимать данные от пользователя, введенные с клавиатуры, научились преобразовывать данные из input и обрабатывать исключения.




