Чтобы установить Pytorch, следуйте инструкциям на официальном веб -сайте:
pip install torch torchvision
Мы стремимся постепенно расширить эту серию, добавив новые статьи и поддерживать контент с последними выпусками Pytorch API. Если у вас есть предложения о том, как улучшить эту серию или найти объяснения неоднозначными, не стесняйтесь создавать проблему, отправлять патчи или обратиться по электронной почте.
Pytorch является одной из самых популярных библиотек для численных вычислений и в настоящее время является одним из наиболее широко используемых библиотек для проведения исследований машинного обучения. Во многих отношениях Pytorch похож на Numpy, с дополнительным преимуществом, которое Pytorch позволяет вам выполнять ваши вычисления на процессорах, графических процессорах и TPU без каких -либо изменений в вашем коде. Pytorch также позволяет легко распространять ваши вычисления на несколько устройств или машин. Одной из наиболее важных особенностей Pytorch является автоматическая дифференциация. Это позволяет аналитически аналитически вычислять градиенты ваших функций, что имеет решающее значение для моделей обучения машинного обучения с использованием метода градиентного спуска. Наша цель здесь состоит в том, чтобы обеспечить мягкое введение в Pytorch и обсудить лучшие практики для использования Pytorch.
Первое, что можно узнать о Pytorch, - это концепция тензоров. Тенсоры - это просто многомерные массивы. Тензор питорха очень похож на массив Numpy с некоторыми магический Дополнительная функциональность.
Тензор может хранить скалярное значение:
import torch
a = torch . tensor ( 3 )
print ( a ) # tensor(3)или массив:
b = torch . tensor ([ 1 , 2 ])
print ( b ) # tensor([1, 2])Матрица:
c = torch . zeros ([ 2 , 2 ])
print ( c ) # tensor([[0., 0.], [0., 0.]])или любой произвольный тензор измерения:
d = torch . rand ([ 2 , 2 , 2 ])Тензоры могут быть использованы для эффективного выполнения алгебраических операций. Одной из наиболее часто используемых операций в приложениях машинного обучения является умножение матрицы. Скажем, вы хотите умножить две случайные матрицы размера 3x5 и 5x4, это можно сделать с помощью операции умножения матрицы (@):
import torch
x = torch . randn ([ 3 , 5 ])
y = torch . randn ([ 5 , 4 ])
z = x @ y
print ( z )Точно так же, чтобы добавить два вектора, вы можете сделать:
z = x + yЧтобы преобразовать тензор в массив Numpy, вы можете назвать метод Numpy () Tensor ():
print ( z . numpy ())И вы всегда можете преобразовать массив Numpy в тензор:
x = torch . tensor ( np . random . normal ([ 3 , 5 ]))Наиболее важным преимуществом Pytorch над Numpy является его автоматическая функция дифференциации, которая очень полезна в приложениях оптимизации, таких как оптимизация параметров нейронной сети. Давайте попробуем понять это с примером.
Скажем, у вас есть составная функция, которая представляет собой цепь двух функций: g(u(x)) . Чтобы вычислить производную g в отношении x мы можем использовать правило цепи, которое указывает, что: dg/dx = dg/du * du/dx . Pytorch может аналитически вычислять производные для нас.
Чтобы вычислить производные в Pytorch, сначала мы создаем тензор и устанавливаем его requires_grad в true. Мы можем использовать тензорные операции для определения наших функций. Мы предполагаем, что u - квадратичная функция, а g - простая линейная функция:
x = torch . tensor ( 1.0 , requires_grad = True )
def u ( x ):
return x * x
def g ( u ):
return - u В этом случае наша составная функция g(u(x)) = -x*x . Таким образом, его производная относительно x составляет -2x . В точке x=1 это равно -2 .
Давайте подтвердим это. Это можно сделать с помощью функции GRAD в Pytorch:
dgdx = torch . autograd . grad ( g ( u ( x )), x )[ 0 ]
print ( dgdx ) # tensor(-2.) Чтобы понять, насколько мощной автоматической дифференциацией давайте посмотрим на другой пример. Предположим, что у нас есть образцы из кривой (скажем, f(x) = 5x^2 + 3 ), и мы хотим оценить f(x) на основе этих образцов. Мы определяем параметрическую функцию g(x, w) = w0 x^2 + w1 x + w2 , которая является функцией ввода x и латентных параметров w , наша цель состоит в том, чтобы найти скрытые параметры, так что g(x, w) ≈ f(x) . Это можно сделать, минимизируя следующую функцию потери: L(w) = Σ (f(x) - g(x, w))^2 . Хотя для этой простой проблемы есть решение закрытой формы, мы предпочитаем использовать более общий подход, который можно применить к любой произвольной дифференцируемой функции, которая использует стохастический градиент. Мы просто вычисляем средний градиент L(w) по отношению к w над набором точек образца и перемещаемся в противоположном направлении.
Вот как это можно сделать в Pytorch:
import numpy as np
import torch
# Assuming we know that the desired function is a polynomial of 2nd degree, we
# allocate a vector of size 3 to hold the coefficients and initialize it with
# random noise.
w = torch . tensor ( torch . randn ([ 3 , 1 ]), requires_grad = True )
# We use the Adam optimizer with learning rate set to 0.1 to minimize the loss.
opt = torch . optim . Adam ([ w ], 0.1 )
def model ( x ):
# We define yhat to be our estimate of y.
f = torch . stack ([ x * x , x , torch . ones_like ( x )], 1 )
yhat = torch . squeeze ( f @ w , 1 )
return yhat
def compute_loss ( y , yhat ):
# The loss is defined to be the mean squared error distance between our
# estimate of y and its true value.
loss = torch . nn . functional . mse_loss ( yhat , y )
return loss
def generate_data ():
# Generate some training data based on the true function
x = torch . rand ( 100 ) * 20 - 10
y = 5 * x * x + 3
return x , y
def train_step ():
x , y = generate_data ()
yhat = model ( x )
loss = compute_loss ( y , yhat )
opt . zero_grad ()
loss . backward ()
opt . step ()
for _ in range ( 1000 ):
train_step ()
print ( w . detach (). numpy ())Запустив этот кусок кода, вы должны увидеть результат, близкий к этому:
[ 4.9924135 , 0.00040895029 , 3.4504161 ]Что является относительно близким приближением к нашим параметрам.
Это просто кончик айсберга для того, что может сделать Pytorch. Многие проблемы, такие как оптимизация крупных нейронных сетей с миллионами параметров, могут быть эффективно реализованы в Pytorch всего за несколько строк кода. Pytorch заботится о масштабировании по нескольким устройствам и потокам и поддерживает различные платформы.
В предыдущем примере мы использовали тензоры Bare Bone и тензоры для построения нашей модели. Чтобы сделать ваш код немного более организованным, рекомендуется использовать модули Pytorch. Модуль - это просто контейнер для ваших параметров и инкапсулирует операции модели. Например, скажем, вы хотите представить линейную модель y = ax + b . Эта модель может быть представлена со следующим кодом:
import torch
class Net ( torch . nn . Module ):
def __init__ ( self ):
super (). __init__ ()
self . a = torch . nn . Parameter ( torch . rand ( 1 ))
self . b = torch . nn . Parameter ( torch . rand ( 1 ))
def forward ( self , x ):
yhat = self . a * x + self . b
return yhatЧтобы использовать эту модель на практике, вы создаете экземпляр модуля и просто называете ее функцией:
x = torch . arange ( 100 , dtype = torch . float32 )
net = Net ()
y = net ( x ) Параметры, по сути, являются тензорами с requires_grad , установленной в true. Удобно использовать параметры, потому что вы можете просто получить их все с помощью метода parameters() :
for p in net . parameters ():
print ( p ) Теперь, скажем, у вас есть неизвестная функция y = 5x + 3 + some noise , и вы хотите оптимизировать параметры вашей модели в соответствии с этой функцией. Вы можете начать с выборки некоторых моментов из вашей функции:
x = torch . arange ( 100 , dtype = torch . float32 ) / 100
y = 5 * x + 3 + torch . rand ( 100 ) * 0.3Аналогично предыдущему примеру, вы можете определить функцию потери и оптимизировать параметры вашей модели следующим образом:
criterion = torch . nn . MSELoss ()
optimizer = torch . optim . SGD ( net . parameters (), lr = 0.01 )
for i in range ( 10000 ):
net . zero_grad ()
yhat = net ( x )
loss = criterion ( yhat , y )
loss . backward ()
optimizer . step ()
print ( net . a , net . b ) # Should be close to 5 and 3 Pytorch поставляется с рядом предопределенных модулей. Одним из таких модулей является torch.nn.Linear , который является более общей формой линейной функции, чем то, что мы определили выше. Мы можем переписать наш модуль выше, используя torch.nn.Linear
class Net ( torch . nn . Module ):
def __init__ ( self ):
super (). __init__ ()
self . linear = torch . nn . Linear ( 1 , 1 )
def forward ( self , x ):
yhat = self . linear ( x . unsqueeze ( 1 )). squeeze ( 1 )
return yhat Обратите внимание, что мы использовали сжатие и Unsqueeze с тех пор, как torch.nn.Linear работает на партии векторов, а не скаляры.
По умолчанию параметры вызова () в модуле вернет параметры всех его подмодулей:
net = Net ()
for p in net . parameters ():
print ( p ) Есть несколько предопределенных модулей, которые действуют как контейнер для других модулей. Наиболее часто используемым контейнерным модулем является torch.nn.Sequential . Как подразумевает его название, он используется для складывания нескольких модулей (или слоев) друг на друга. Например, чтобы сложить два линейных слоя с нелинейностью ReLU , вы можете сделать:
model = torch . nn . Sequential (
torch . nn . Linear ( 64 , 32 ),
torch . nn . ReLU (),
torch . nn . Linear ( 32 , 10 ),
) Pytorch поддерживает трансляцию ElementWise Operations. Обычно, когда вы хотите выполнить такие операции, как сложение и умножение, вам необходимо убедиться, что формы операндов соответствуют, например, вы не можете добавить тензор формы [3, 2] в тензор формы [3, 4] . Но есть особый случай, и тогда у вас есть единственное измерение. Pytorch неявно пенис тензор по своим единственным размерам, чтобы соответствовать форме другого операнда. Таким образом, действительно добавить тензор формы [3, 2] к тензору формы [3, 1] .
import torch
a = torch . tensor ([[ 1. , 2. ], [ 3. , 4. ]])
b = torch . tensor ([[ 1. ], [ 2. ]])
# c = a + b.repeat([1, 2])
c = a + b
print ( c )Вещание позволяет нам выполнять неявную плитку, что делает код более коротким и более эффективным памятью, поскольку нам не нужно хранить результат операции плитки. Одно изящное место, которое это можно использовать, - это объединение функций различной длины. Чтобы объединить особенности различной длины, мы обычно плим входные тензоры, объединяем результат и применяем некоторую нелинейность. Это общая схема для различных архитектур нейронной сети:
a = torch . rand ([ 5 , 3 , 5 ])
b = torch . rand ([ 5 , 1 , 6 ])
linear = torch . nn . Linear ( 11 , 10 )
# concat a and b and apply nonlinearity
tiled_b = b . repeat ([ 1 , 3 , 1 ])
c = torch . cat ([ a , tiled_b ], 2 )
d = torch . nn . functional . relu ( linear ( c ))
print ( d . shape ) # torch.Size([5, 3, 10]) Но это может быть сделано более эффективно с вещанием. Мы используем тот факт, что f(m(x + y)) равен f(mx + my) . Таким образом, мы можем выполнять линейные операции отдельно и использовать вещание, чтобы сделать неявную конкатенацию:
a = torch . rand ([ 5 , 3 , 5 ])
b = torch . rand ([ 5 , 1 , 6 ])
linear1 = torch . nn . Linear ( 5 , 10 )
linear2 = torch . nn . Linear ( 6 , 10 )
pa = linear1 ( a )
pb = linear2 ( b )
d = torch . nn . functional . relu ( pa + pb )
print ( d . shape ) # torch.Size([5, 3, 10])Фактически, этот кусок кода довольно общий и может применяться к тензорам произвольной формы, пока возможна вещание между тензорами:
class Merge ( torch . nn . Module ):
def __init__ ( self , in_features1 , in_features2 , out_features , activation = None ):
super (). __init__ ()
self . linear1 = torch . nn . Linear ( in_features1 , out_features )
self . linear2 = torch . nn . Linear ( in_features2 , out_features )
self . activation = activation
def forward ( self , a , b ):
pa = self . linear1 ( a )
pb = self . linear2 ( b )
c = pa + pb
if self . activation is not None :
c = self . activation ( c )
return cДо сих пор мы обсуждали хорошую часть вещания. Но какая уродливая часть вы можете спросить? Неявные предположения почти всегда усложняют отладку. Рассмотрим следующий пример:
a = torch . tensor ([[ 1. ], [ 2. ]])
b = torch . tensor ([ 1. , 2. ])
c = torch . sum ( a + b )
print ( c ) Как вы думаете, каким будет значение c после оценки? Если вы догадались 6, это неправильно. Это будет 12. Это потому, что когда ранжирование двух тензоров не совпадает, Pytorch автоматически расширяет первое измерение тензора с более низким рангом перед работой элемента, поэтому результат добавления будет [[2, 3], [3, 4]] , а уменьшение всех параметров даст нам 12.
Способ избежать этой проблемы - быть максимально явным. Если бы мы указали, какое измерение мы хотели бы уменьшить, поймать эту ошибку, было бы намного проще:
a = torch . tensor ([[ 1. ], [ 2. ]])
b = torch . tensor ([ 1. , 2. ])
c = torch . sum ( a + b , 0 )
print ( c ) Здесь ценность c будет [5, 7] , и мы сразу же догадываемся, основываясь на форме результата, что что -то не так. Общее правило - всегда указывать размеры в операциях сокращения и при использовании torch.squeeze .
Как и Numpy, Pytorch перегружает ряд операторов Python, чтобы сделать код Pytorch короче и читаемым.
Нарезка OP - один из перегруженных операторов, который может сделать тензоры индексации очень простыми:
z = x [ begin : end ] # z = torch.narrow(0, begin, end-begin)Будьте очень осторожны при использовании этого OP, хотя. Нарезанный OP, как и любой другой OP, имеет несколько накладных расходов. Поскольку это обычный OP и невинный взгляд, это может многое использоваться, что может привести к неэффективности. Чтобы понять, насколько неэффективен этот OP, давайте посмотрим на пример. Мы хотим вручную выполнить сокращение рядов матрицы:
import torch
import time
x = torch . rand ([ 500 , 10 ])
z = torch . zeros ([ 10 ])
start = time . time ()
for i in range ( 500 ):
z += x [ i ]
print ( "Took %f seconds." % ( time . time () - start )) Это работает довольно медленно, и причина в том, что мы называем Slice OP 500 раз, что добавляет много накладных расходов. Лучшим выбором было бы использовать torch.unbind OP, чтобы нарезать матрицу на список векторов одновременно:
z = torch . zeros ([ 10 ])
for x_i in torch . unbind ( x ):
z += x_iЭто значительно (~ 30% на моей машине) быстрее.
Конечно, правильный способ сделать это простое сокращение - использовать torch.sum op к этому в одном OP:
z = torch . sum ( x , dim = 0 )что очень быстро (~ 100 раз быстрее на моей машине).
Pytorch также перегружает ряд арифметических и логических операторов:
z = - x # z = torch.neg(x)
z = x + y # z = torch.add(x, y)
z = x - y
z = x * y # z = torch.mul(x, y)
z = x / y # z = torch.div(x, y)
z = x // y
z = x % y
z = x ** y # z = torch.pow(x, y)
z = x @ y # z = torch.matmul(x, y)
z = x > y
z = x >= y
z = x < y
z = x <= y
z = abs ( x ) # z = torch.abs(x)
z = x & y
z = x | y
z = x ^ y # z = torch.logical_xor(x, y)
z = ~ x # z = torch.logical_not(x)
z = x == y # z = torch.eq(x, y)
z = x != y # z = torch.ne(x, y) Вы также можете использовать дополненную версию этих OPS. Например, x += y и x **= 2 также действительны.
Обратите внимание, что Python не разрешает перегрузку and , or , и not ключевые слова.
Pytorch оптимизирован для выполнения операций на больших тензорах. Делать много операций на небольших тензорах довольно неэффективно в Pytorch. Таким образом, когда это возможно, вы должны переписать свои вычисления в пакетной форме, чтобы уменьшить накладные расходы и повысить производительность. Если вы не можете вручную выровнять свои операции, использование TorchScript может улучшить производительность вашего кода. TorchScript - это просто подмножество функций Python, которая распознается Pytorch. Pytorch может автоматически оптимизировать ваш код TorchScript, используя его компилятор (JIT) и уменьшить некоторые накладные расходы.
Давайте посмотрим на пример. Очень распространенная операция в приложениях ML - «Сбор пакетов». Эта операция может просто записать как output[i] = input[i, index[i]] . Это может быть просто реализовано в Pytorch следующим образом:
import torch
def batch_gather ( tensor , indices ):
output = []
for i in range ( tensor . size ( 0 )):
output += [ tensor [ i ][ indices [ i ]]]
return torch . stack ( output ) Для реализации той же функции с использованием TorchScript просто используйте torch.jit.script Decorator:
@ torch . jit . script
def batch_gather_jit ( tensor , indices ):
output = []
for i in range ( tensor . size ( 0 )):
output += [ tensor [ i ][ indices [ i ]]]
return torch . stack ( output )На моих тестах это примерно на 10% быстрее.
Но ничто не сравнится вручную, чтобы получить вашу деятельность. Векторизованная реализация в моих тестах в 100 раз быстрее:
def batch_gather_vec ( tensor , indices ):
shape = list ( tensor . shape )
flat_first = torch . reshape (
tensor , [ shape [ 0 ] * shape [ 1 ]] + shape [ 2 :])
offset = torch . reshape (
torch . arange ( shape [ 0 ]). cuda () * shape [ 1 ],
[ shape [ 0 ]] + [ 1 ] * ( len ( indices . shape ) - 1 ))
output = flat_first [ indices + offset ]
return output На последнем уроке мы говорили о написании эффективного кода Pytorch. Но чтобы ваш код работал с максимальной эффективностью, вам также необходимо эффективно загружать данные в память вашего устройства. К счастью, Pytorch предлагает инструмент для облегчения загрузки данных. Это называется DataLoader . DataLoader использует несколько работников для одновременной загрузки данных из Dataset и, опционально, использует Sampler для выборки данных и сформировать партию.
Если вы можете случайным образом получить доступ к вашим данным, использование DataLoader очень просто: вам просто нужно реализовать класс Dataset , который реализует __getitem__ (для чтения каждого элемента данных) и __len__ (чтобы вернуть количество элементов в наборе набора). Например, вот как загружать изображения из данного каталога:
import glob
import os
import random
import cv2
import torch
class ImageDirectoryDataset ( torch . utils . data . Dataset ):
def __init__ ( path , pattern ):
self . paths = list ( glob . glob ( os . path . join ( path , pattern )))
def __len__ ( self ):
return len ( self . paths )
def __item__ ( self ):
path = random . choice ( paths )
return cv2 . imread ( path , 1 )Чтобы загрузить все изображения JPEG из данного каталога, вы можете сделать следующее:
dataloader = torch . utils . data . DataLoader ( ImageDirectoryDataset ( "/data/imagenet/*.jpg" ), num_workers = 8 )
for data in dataloader :
# do something with dataЗдесь мы используем 8 работников, чтобы одновременно читать наши данные с диска. Вы можете настроить количество работников на вашей машине для оптимальных результатов.
Использование DataLoader для чтения данных со случайным доступом может быть в порядке, если у вас быстрое хранение или если ваши элементы данных большие. Но представьте, что у вас есть сетевая файловая система с медленным соединением. Запрашивать отдельные файлы таким образом может быть чрезвычайно медленным и, вероятно, в конечном итоге станет узким местом вашего тренировочного конвейера.
Лучшим подходом является хранение ваших данных в смежном формате файла, который можно прочитать последовательно. Например, если у вас есть большая коллекция изображений, вы можете использовать TAR для создания одного архива и извлечения файлов из архива последовательно в Python. Для этого вы можете использовать IterableDataset от Pytorch. Чтобы создать класс IterableDataset , вам нужно только реализовать метод __iter__ , который последовательно считывает и дает элементы данных из набора данных.
Наивная реализация хотела бы это:
import tarfile
import torch
def tar_image_iterator ( path ):
tar = tarfile . open ( self . path , "r" )
for tar_info in tar :
file = tar . extractfile ( tar_info )
content = file . read ()
yield cv2 . imdecode ( content , 1 )
file . close ()
tar . members = []
tar . close ()
class TarImageDataset ( torch . utils . data . IterableDataset ):
def __init__ ( self , path ):
super (). __init__ ()
self . path = path
def __iter__ ( self ):
yield from tar_image_iterator ( self . path )Но есть серьезная проблема с этой реализацией. Если вы попытаетесь использовать DataLoader для прочтения из этого набора данных с более чем одним работником, вы будете соблюдать много дублированных изображений:
dataloader = torch . utils . data . DataLoader ( TarImageDataset ( "/data/imagenet.tar" ), num_workers = 8 )
for data in dataloader :
# data contains duplicated items Проблема в том, что каждый работник создает отдельный экземпляр набора данных, и каждый из них начинает с начала набора данных. Один из способов избежать этого - вместо того, чтобы иметь один файл TAR, разделите ваши данные на num_workers Разделите файлы TAR и загрузите каждый с отдельным работником:
class TarImageDataset ( torch . utils . data . IterableDataset ):
def __init__ ( self , paths ):
super (). __init__ ()
self . paths = paths
def __iter__ ( self ):
worker_info = torch . utils . data . get_worker_info ()
# For simplicity we assume num_workers is equal to number of tar files
if worker_info is None or worker_info . num_workers != len ( self . paths ):
raise ValueError ( "Number of workers doesn't match number of files." )
yield from tar_image_iterator ( self . paths [ worker_info . worker_id ])Так можно использовать наш класс наборов данных:
dataloader = torch . utils . data . DataLoader (
TarImageDataset ([ "/data/imagenet_part1.tar" , "/data/imagenet_part2.tar" ]), num_workers = 2 )
for data in dataloader :
# do something with dataМы обсудили простую стратегию, чтобы избежать дублированных проблем с записями. Пакет Tfrecord использует немного более сложные стратегии, чтобы нарушить ваши данные на лету.
При использовании любой численной библиотеки вычислений, такой как Numpy или Pytorch, важно отметить, что написание математического правильного кода не обязательно приводит к правильным результатам. Вы также должны убедиться, что вычисления стабильны.
Начнем с простого примера. Математически, легко увидеть, что x * y / y = x для любого не нулевого значения x . Но давайте посмотрим, всегда ли это верно на практике:
import numpy as np
x = np . float32 ( 1 )
y = np . float32 ( 1e-50 ) # y would be stored as zero
z = x * y / y
print ( z ) # prints nan Причина неверного результата заключается в том, что y просто слишком мал для типа Float32. Подобная проблема возникает, когда y слишком велика:
y = np . float32 ( 1e39 ) # y would be stored as inf
z = x * y / y
print ( z ) # prints nanНаименьшее положительное значение, которое может представлять тип Float32, составляет 1.4013E-45, и все ниже, которое будет сохранено как ноль. Кроме того, любое число за пределами 3.40282E+38, будет храниться как инф.
print ( np . nextafter ( np . float32 ( 0 ), np . float32 ( 1 ))) # prints 1.4013e-45
print ( np . finfo ( np . float32 ). max ) # print 3.40282e+38Чтобы убедиться, что ваши вычисления стабильны, вы хотите избежать значений с небольшим или очень большим абсолютным значением. Это может показаться очень очевидным, но такие проблемы могут стать чрезвычайно трудно отлаживать, особенно при выполнении градиентного происхождения в Pytorch. Это связано с тем, что вам нужно не только убедиться, что все значения в прямом проходе находятся в пределах достоверного диапазона ваших типов данных, но и должны убедиться в том же для обратного прохода (во время расчета градиента).
Давайте посмотрим на настоящий пример. Мы хотим вычислить Softmax над вектором логитов. Наивная реализация будет выглядеть примерно так:
import torch
def unstable_softmax ( logits ):
exp = torch . exp ( logits )
return exp / torch . sum ( exp )
print ( unstable_softmax ( torch . tensor ([ 1000. , 0. ])). numpy ()) # prints [ nan, 0.] Обратите внимание, что вычисление экспоненты логитов для относительно небольших чисел результаты для гигантских результатов, которые находятся вне диапазона float32. Самой большой допустимым логитом для нашей наивной реализации SoftMax является ln(3.40282e+38) = 88.7 , все, что за пределами этого приводит к исходу NAN.
Но как мы можем сделать это более стабильным? Решение довольно простое. Легко увидеть, что exp(x - c) Σ exp(x - c) = exp(x) / Σ exp(x) . Поэтому мы можем вычесть любую константу из логитов, и результат останется прежним. Мы выбираем эту константу, чтобы быть максимумом логитов. Таким образом, область экспоненциальной функции будет ограничена [-inf, 0] , и, следовательно, его диапазон будет [0.0, 1.0] , что желательно:
import torch
def softmax ( logits ):
exp = torch . exp ( logits - torch . reduce_max ( logits ))
return exp / torch . sum ( exp )
print ( softmax ( torch . tensor ([ 1000. , 0. ])). numpy ()) # prints [ 1., 0.] Давайте посмотрим на более сложный случай. Считайте, что у нас есть проблема с классификацией. Мы используем функцию SoftMax для создания вероятностей из наших логитов. Затем мы определяем нашу функцию потерь как поперечная энтропия между нашими прогнозами и метками. Напомним, что поперечная энтропия для категориального распределения может быть просто определен как xe(p, q) = -Σ p_i log(q_i) . Таким образом, наивная реализация поперечной энтропии была бы выглядела так:
def unstable_softmax_cross_entropy ( labels , logits ):
logits = torch . log ( softmax ( logits ))
return - torch . sum ( labels * logits )
labels = torch . tensor ([ 0.5 , 0.5 ])
logits = torch . tensor ([ 1000. , 0. ])
xe = unstable_softmax_cross_entropy ( labels , logits )
print ( xe . numpy ()) # prints infОбратите внимание, что в этой реализации, когда вывод SoftMax приближается к нулю, выходной выходной сигнал подходит к бесконечности, что вызывает нестабильность в нашем вычислении. Мы можем переписать это, расширив Softmax и сделав некоторые упрощения:
def softmax_cross_entropy ( labels , logits , dim = - 1 ):
scaled_logits = logits - torch . max ( logits )
normalized_logits = scaled_logits - torch . logsumexp ( scaled_logits , dim )
return - torch . sum ( labels * normalized_logits )
labels = torch . tensor ([ 0.5 , 0.5 ])
logits = torch . tensor ([ 1000. , 0. ])
xe = softmax_cross_entropy ( labels , logits )
print ( xe . numpy ()) # prints 500.0Мы также можем убедиться, что градиенты также вычисляются правильно:
logits . requires_grad_ ( True )
xe = softmax_cross_entropy ( labels , logits )
g = torch . autograd . grad ( xe , logits )[ 0 ]
print ( g . numpy ()) # prints [0.5, -0.5]Позвольте мне еще раз напомнить, что при выполнении градиента спускается дополнительная помощь, чтобы убедиться, что диапазон ваших функций, а также градиенты для каждого уровня находятся в достоверном диапазоне. Экспоненциальные и логарифмические функции при использовании наивенно особенно проблематичны, потому что они могут отображать небольшие числа с огромными, а наоборот.
По умолчанию тензоры и параметры модели в Pytorch хранятся в 32-разрядной точке плавающей точки. Обучение нейронных сетей с использованием 32-разрядных поплавок обычно стабильно и не вызывает серьезных численных проблем, однако было показано, что нейронные сети довольно хорошо работают в 16-битных и даже более низких характеристиках. Вычисление в более низких точках может быть значительно быстрее для современных графических процессоров. Он также имеет дополнительное преимущество в использовании меньшего количества памяти, позволяющих обучать более крупные модели и/или с большими размерами партии, которые могут повысить производительность. Однако проблема заключается в том, что тренировки в 16 битах часто становится очень нестабильной, потому что точность обычно недостаточно для выполнения некоторых операций, таких как накопление.
Чтобы помочь с этой проблемой Pytorch поддерживает обучение в смешанной точке. Короче говоря, обучение смешанного определения проводится путем выполнения некоторых дорогих операций (таких как свертывание и мультирезы матрицы) в 16-битных путем отложения входов при выполнении других численных операций, таких как накопления в 32-битных. Таким образом, мы получаем все преимущества 16-битных вычислений без его недостатков. Затем мы говорим об использовании AutoCast и GradScaler для автоматического обучения смешанном назначении.
autocast помогает повысить производительность выполнения путем автоматического составления данных до 16-битных для некоторых вычислений. Чтобы понять, как это работает, давайте посмотрим на пример:
import torch
x = torch . rand ([ 32 , 32 ]). cuda ()
y = torch . rand ([ 32 , 32 ]). cuda ()
with torch . cuda . amp . autocast ():
a = x + y
b = x @ y
print ( a . dtype ) # prints torch.float32
print ( b . dtype ) # prints torch.float16 Обратите внимание, что x и y -32-битные тензоры, но autocast выполняет умножение матрицы в 16-битном при этом операции добавления в 32-битной. Что если один из операндов находится в 16-битном?
import torch
x = torch . rand ([ 32 , 32 ]). cuda ()
y = torch . rand ([ 32 , 32 ]). cuda (). half ()
with torch . cuda . amp . autocast ():
a = x + y
b = x @ y
print ( a . dtype ) # prints torch.float32
print ( b . dtype ) # prints torch.float16 Опять же, autocast и спускает 32-битный операнд на 16-битный, чтобы выполнить умножение матрицы, но не изменяет операцию добавления. По умолчанию добавление двух тензоров в Pytorch приводит к актерскому составу до более высокой точности.
На практике вы можете доверять autocast для выполнения правильного кастинга, чтобы повысить эффективность выполнения. Важно сохранить все ваши вычисления вперед в контексте autocast :
model = ...
loss_fn = ...
with torch . cuda . amp . autocast ():
outputs = model ( inputs )
loss = loss_fn ( outputs , targets )Возможно, это все, что вам нужно, если у вас есть относительно стабильная проблема оптимизации, и если вы используете относительно низкую скорость обучения. Добавление этой линии дополнительного кода может сократить ваше обучение до половины на современном оборудовании.
Как мы упоминали в начале этого раздела, 16-битная точность не всегда может быть достаточно для некоторых вычислений. Одним из конкретных интересов является представляющий градиентные значения, большая часть которых обычно представляют собой небольшие значения. Представление их с 16-битными поплавками часто приводит к буферным нижним потокам (то есть они будут представлены как нули). Это делает тренировочные нейронные сети очень нестабильными. GradScalar предназначен для решения этой проблемы. Он принимает в качестве входного значения потерь и умножает его на большие скалярные, раздувающие значения градиента и, следовательно, создавая их представляют собой 16-битную точность. Затем он масштабирует их во время обновления градиента, чтобы убедиться, что параметры обновляются правильно. Как правило, это то, что делает GradScalar . Но под капотом GradScalar немного умнее, чем это. Раздучение градиентов может на самом деле привести к переполнению, что одинаково плохое. Таким образом, GradScalar фактически контролирует значения градиента и, если он обнаруживает переполнение, он пропускает обновления, уменьшая скалярную коэффициент в соответствии с настраиваемым графиком. (График по умолчанию обычно работает, но вам может потребоваться настроить это для вашего варианта использования.)
Использовать GradScalar очень просто на практике:
scaler = torch . cuda . amp . GradScaler ()
loss = ...
optimizer = ... # an instance torch.optim.Optimizer
scaler . scale ( loss ). backward ()
scaler . step ( optimizer )
scaler . update () Обратите внимание, что сначала мы создаем экземпляр GradScalar . В учебном цикле мы называем GradScalar.scale , чтобы масштабировать потерю перед вызовом назад, чтобы произвести надутые градиенты, мы затем используем GradScalar.step , который (может) обновить параметры модели. Затем мы называем GradScalar.update , который при необходимости выполняет скалярное обновление. Вот и все!
Ниже приведен пример кода, который показывает случаи смешанной точной подготовки по синтетической проблеме обучения для генерации шахматной доски из координат изображений. Вы можете вставить его в Google Colab, установить бэкэнд в графический процессор и сравнить одно и смешанную производительность. Обратите внимание, что это маленький пример игрушек, на практике с большими сетями вы можете увидеть более крупные повышения производительности, используя смешанную точность.
import torch
import matplotlib . pyplot as plt
import time
def grid ( width , height ):
hrange = torch . arange ( width ). unsqueeze ( 0 ). repeat ([ height , 1 ]). div ( width )
vrange = torch . arange ( height ). unsqueeze ( 1 ). repeat ([ 1 , width ]). div ( height )
output = torch . stack ([ hrange , vrange ], 0 )
return output
def checker ( width , height , freq ):
hrange = torch . arange ( width ). reshape ([ 1 , width ]). mul ( freq / width / 2.0 ). fmod ( 1.0 ). gt ( 0.5 )
vrange = torch . arange ( height ). reshape ([ height , 1 ]). mul ( freq / height / 2.0 ). fmod ( 1.0 ). gt ( 0.5 )
output = hrange . logical_xor ( vrange ). float ()
return output
# Note the inputs are grid coordinates and the target is a checkerboard
inputs = grid ( 512 , 512 ). unsqueeze ( 0 ). cuda ()
targets = checker ( 512 , 512 , 8 ). unsqueeze ( 0 ). unsqueeze ( 1 ). cuda () class Net ( torch . jit . ScriptModule ):
def __init__ ( self ):
super (). __init__ ()
self . net = torch . nn . Sequential (
torch . nn . Conv2d ( 2 , 256 , 1 ),
torch . nn . BatchNorm2d ( 256 ),
torch . nn . ReLU (),
torch . nn . Conv2d ( 256 , 256 , 1 ),
torch . nn . BatchNorm2d ( 256 ),
torch . nn . ReLU (),
torch . nn . Conv2d ( 256 , 256 , 1 ),
torch . nn . BatchNorm2d ( 256 ),
torch . nn . ReLU (),
torch . nn . Conv2d ( 256 , 1 , 1 ))
@ torch . jit . script_method
def forward ( self , x ):
return self . net ( x ) net = Net (). cuda ()
loss_fn = torch . nn . MSELoss ()
opt = torch . optim . Adam ( net . parameters (), 0.001 )
start_time = time . time ()
for i in range ( 500 ):
opt . zero_grad ()
outputs = net ( inputs )
loss = loss_fn ( outputs , targets )
loss . backward ()
opt . step ()
print ( loss )
print ( time . time () - start_time )
plt . subplot ( 1 , 2 , 1 ); plt . imshow ( outputs . squeeze (). detach (). cpu ());
plt . subplot ( 1 , 2 , 2 ); plt . imshow ( targets . squeeze (). cpu ()); plt . show () net = Net (). cuda ()
loss_fn = torch . nn . MSELoss ()
opt = torch . optim . Adam ( net . parameters (), 0.001 )
scaler = torch . cuda . amp . GradScaler ()
start_time = time . time ()
for i in range ( 500 ):
opt . zero_grad ()
with torch . cuda . amp . autocast ():
outputs = net ( inputs )
loss = loss_fn ( outputs , targets )
scaler . scale ( loss ). backward ()
scaler . step ( opt )
scaler . update ()
print ( loss )
print ( time . time () - start_time )
plt . subplot ( 1 , 2 , 1 ); plt . imshow ( outputs . squeeze (). detach (). cpu (). float ());
plt . subplot ( 1 , 2 , 2 ); plt . imshow ( targets . squeeze (). cpu (). float ()); plt . show ()