Классификация изображений в CIFAR 10: полное руководство
Глава II: Мелкая нейронная сеть
Это часть 2/3 мини-сериала, в котором используется классификация изображений в CIFAR-10. Посмотрите последнюю главу, в которой мы использовали логистическую регрессию, более простую модель.
Для понимания softmax, кросс-энтропии, мини-пакетного градиентного спуска, подготовки данных и других вещей, которые также играют большую роль в нейронных сетях, прочтите предыдущую запись в этой мини-серии.
На этой модели мы достигли наивысшей общей точности тестового набора около 40%. Хотя это значительно превышает случайные предположения (что составляет около 10%), но все же не приближается к чему-либо полезному и далек от точности, которая нам нужна в реальных приложениях, таких как беспилотные автомобили.
Я не буду объяснять предварительную обработку данных или какие-либо предварительные концепции, поскольку их можно увидеть в первых нескольких частях руководства по логистической регрессии. На самом деле, прочтите это - большая часть содержания здесь основана на понимании предыдущего, тем более что мы по-прежнему будем использовать комбинацию softmax и кросс-энтропии.
содержание
Это довольно длинная статья, так что вот ее содержание. Сегменты, предшествующие [кратко], являются обзорами, поскольку я рассмотрел их все более подробно в прошлой статье.
- [Краткое] Подготовка данных
- Модель однослойной нейронной сети
- Вычисление нейронной сети
- Нейронная сеть как расширение логистической регрессии
- PyTorch для нейронных сетей
- Что такое графический процессор?
- Графические процессоры для машинного обучения
- Загрузка на графический процессор в PyTorch
- Обучение модели
- Высокоуровневое математическое объяснение обучения нейронной сети
- [краткое] Замечание о мини-пакетном градиентном спуске
- [краткое] Вычисление всех 98666 производных за один пакет
- [краткое описание] Оценка стоимости и точности набора для валидации
- [краткое] Примерка модели
- [кратко] Определение нашей метрики точности
- Перенос параметров в GPU
- Воспроизводимость
- Почему до сих пор так плохо предсказывает?
- "Код"
- Математика обратного распространения ошибки
несколько заявлений об отказе от ответственности (ctrl c → ctrl v’ed из предыдущего)
Я буду просматривать [большую часть] кода, который использовал, но я буду уделять больше внимания математике и внутренней работе, которую представляет код. Большая часть кода была позаимствована из фантастического учебника Aakash по PyTorch с сайта freeCodeCamp.org, но немного изменена, чтобы приспособить к немного более сложному набору данных (CIFAR вместо MNIST). Я добавлю свой полностью адаптированный код в виде документа Google Colab в конце этой статьи.
Я новичок в программировании. Я ужасно в этом. Я использую эту статью, чтобы глубже изучить код, который я пишу, так что терпите меня. Следуя своему усмотрению зрители советуют.
В этом руководстве предполагается базовое, но неполное понимание нейронных сетей, функций стоимости, градиентного спуска и других основных концепций машинного обучения, а также математических концепций, таких как умножение матриц.
Я не буду подробно останавливаться на выводах для softmax и кросс-энтропии, поскольку я сделал их в отдельной статье.
обозначение (также ctrl c → ctrl v’ed)
Я обычно использую здесь стандартные обозначения.
- м - длина обучающего набора.
- функции - это различные входные данные в алгоритм.
- j как переменная обычно используется в качестве индекса для обозначения цикла.
- Я обычно говорю thetas как синонимы «веса и смещения», поскольку тета часто используется вместо W или b в весах или смещениях.
- На протяжении всего руководства я иногда переключаюсь между использованием векторов столбцов (например, 10, 1) и строк (например, 1, 10), но они означают одно и то же.
- Всякий раз, когда вы видите log (), воспринимайте его как ln ().
- В нейронных сетях значения, применимые ко всем слоям, представлены с помощью [x], где x - номер слоя.
подготовка данных
Подготовка данных идентична логистической регрессии. Прочтите первые семь глав предыдущей статьи, чтобы подвести итоги. Краткое резюме:
- Загрузить данные из наборов данных torchvision.datasets
- Преобразование данных в матрицы (тензоры)
- Перемешайте и разделите на обучение и проверку в соотношении 80/20
- Загружать данные партиями по 100
Весь код будет включен в окончательный документ Colab, поэтому я не буду повторять его здесь снова.
однослойная модель нейронной сети
Терминология «однослойная нейронная сеть» технически неверна. Я должен сказать «двухслойная нейронная сеть», поскольку, говоря о нейронных сетях, мы учитываем как скрытый, так и выходной слой, что имеет смысл, потому что у нас есть другой набор весов и смещений для входа и выхода. С этого момента я буду использовать эту терминологию.
Мы будем создавать нейронную сеть с 32 узлами в скрытом слое. Поскольку наша модель по-прежнему имеет 3072 входа и 10 выходов, наша модель будет выглядеть так.
Давайте посмотрим, как мы можем думать о вычислении нейронной сети как о серии умножений и сложений матриц.
Давайте посмотрим на единственный узел в первом слое нашей сети a [1].
Мы можем думать о каждой связи между узлом и другим узлом как о значении в первом узле, умноженном на некоторый вес.
Следовательно, мы можем представить [1] вычисляемым как взвешенную сумму всех узлов, которые в него подаются (3072 входа), плюс смещение, назначенное этому слою.
Каждый узел может быть разделен на две части - первая вычисляет что-то вроде приведенного выше - взвешенную сумму всех узлов и весов, которые в нее входят, плюс смещение. Затем идет вторая часть.
Вторая часть берет взвешенную сумму и сохраняет ее в некоторой переменной z (где z указывает взвешенную сумму всех входов в рассматриваемый узел).
Затем мы передаем это в нелинейную функцию активации, такую как ReLU, tanh или, исторически, сигмоид. Функции активации обычно переводят значение взвешенной суммы в некоторый меньший диапазон (для сигмоида это от 1 до 0, для tanh это -1 и 1).
ReLU не обязательно является хорошим примером этого, поскольку он принимает значение z, (просто линейная активация) или 0, если z отрицательно. Несмотря на принятие значения z, того факта, что ReLU (z) = 0, когда z отрицательно, достаточно, чтобы сделать его нелинейным.
Можно довольно легко показать, что нейронная сеть без функций активации на каждом уровне становится моделью линейной / логистической регрессии и теряет всю свою вычислительную ценность.
Теперь мы можем обобщить это не только на первый узел скрытого слоя, но и на весь скрытый слой. Теперь мы должны обновить имена наших параметров, чтобы они были более понятными - θ1, θ2, θ3…. θ3072 больше не режет, так как у нас разные ноды идут на разные активации.
Поначалу метод правильного индексирования этих весов может немного сбивать с толку, поэтому давайте взглянем.
Надеюсь, это проясняет ситуацию. Мы увидим, насколько это станет важным, когда мы векторизуем все эти длинные взвешенные суммы.
Обратите внимание, что в приложении для краткости, в ущерб ясности, пробел между a и x в указателе часто отсутствует, что делает указатель похожим на единственное число. . Вам просто нужно использовать контекстные подсказки (сколько узлов в слое l? И т. Д.…), Чтобы увидеть, что они означают.
Вычисление нейронной сети
Напомним, что вычисление значения одной активации в слое a [1] было выполнено следующим образом (помните, подумайте о разделении узла на две части - одну, которая вычисляет взвешенную сумму всех входов в этот узел, и одну, которая придерживается эту взвешенную сумму в функцию активации, подобную сигмовидной).
Посмотрим, сможем ли мы записать в этой форме все активации для первого слоя. Для экономии места я сделаю это в одной строке вычислений, вместо этого покажу что-то вроде этого:
Мы можем думать о каждой активации как о взвешенной сумме всех весов и входов (плюс смещение, которое назначается всему слою) для этой активации внутри какой-то функции активации.
Если вы проследите за расчетами, для всего активационного слоя вы получите примерно следующее:
Для тех, кто знаком с матричным умножением, это может показаться подозрительно векторизуемым. Давайте просто выделим вычисления для всего слоя z’s (z [1])
Мы можем представить это как произведение некоторой матрицы весов на некоторую матрицу входных данных, добавленную к некоторому вектору смещения.
Представим себе один тренировочный пример. Это означает, что наш ввод будет вектором-строкой (1, 3072), содержащим все функции для этого одного обучающего примера.
Мы хотим превратить это в вектор-столбец активаций размером (1, 32), где мы умножаем каждый элемент в нашем векторе-строке на некоторый вес. Давайте сначала проигнорируем функцию активации и посмотрим, как мы можем получить вектор-столбец z.
Мы знаем, что при умножении матриц столбцы в первой матрице должны соответствовать строкам во второй матрице. Мы также знаем, что произведение двух матриц занимает строки первой и столбцы второй. Итак, мы знаем, что строки матрицы должны быть 3072, чтобы соответствовать x.
Затем, по нашему второму правилу, мы можем знать, что нам нужно 32 столбца в нашей матрице, чтобы придать форму продукта (32, 1).
Итак, наша матрица весов имеет форму (3072, 32), где обозначают наши размеры (входные узлы, выходные узлы).
Матрицы весов имеют естественную форму (выходные узлы, входные узлы), где каждая строка обозначает веса выходного узла, поэтому нашу матрицу (3072, 32) можно рассматривать как транспонированную матрицу весов.
Таким образом, мы можем выразить вычисления первого уровня на одном обучающем примере как серию матричных операций.
Затем мы можем поэлементно протолкнуть z через функцию активации, чтобы получить a.
Этот единственный расчет + какая-то функция потерь в конце, по сути, является логистической регрессией.
Все наши расчеты по слоям можно подытожить в элегантной векторизованной формуле:
Нейронная сеть как расширение логистической регрессии
По сути, нейронная сеть может быть описана как два последовательно выполняемых алгоритма логистической регрессии. Мы используем одну логистическую регрессию, чтобы описать, как x переходит в a1, а затем другую, которая принимает a и выплевывает a2.
Однослойная нейронная сеть полностью идентична запуску логистической регрессии на «а» - активациях предыдущей регрессии. Математически это работает.
В нашем сценарии с 3072 входами и 32 скрытыми слоями и размером пакета 100 расчеты для одного пакета будут выглядеть примерно так:
Теперь, когда мы можем понять, что нейронная сеть просто воспроизводит логистическую регрессию (а также выполняет некоторые нелинейные функции на каждом уровне), код становится намного проще.
PyTorch для нейронных сетей
Опять же, как и в коде логистической регрессии, мы модифицируем класс nn.Module в соответствии с нашей моделью.
В PyTorch nn.Module
содержит nn.Linear
command, которая создает однослойную модель нейронной сети (= логистическая регрессия), передавая функции и размер вывода (в логарифмической регрессии, размер вывода == количество классов) . nn.Linear(features, outputs)
создаст матрицу весов размера (характеристики, выходы) и смещения размера (выходы).
Передача чего-либо через nn.Linear
будет эквивалентна умножению ввода на [случайно инициализированные] веса и добавлению смещения.
>> model = nn.linear(features, numClasses) # data must have input size of 'features' - be size (x, features) - x is batch size. >> model(data)
nn.Linear
имеет один метод, forward (),, который подгоняет (умножает матрицу) данные на модель. Поскольку существует только один метод, нам не нужно указывать nn.Linear.forward()
— nn.Linear()
будет достаточно.
Теперь мы можем отредактировать определение forward()
, чтобы запустить линейную регрессию дважды и автоматически создать и случайным образом инициализировать наши матрицы веса и смещения для W1, b1 и W2, b2. .
>> import torch >> import torch.nn.functional as F >> import torch.nn as nn
В качестве функции активации мы будем использовать ReLU. Я настоятельно рекомендую вам просмотреть диаграммы ранее и следить за умножением матриц, читая следующие несколько абзацев.
>> class CIFAR10model(nn.Module): """ feedforward neural network with single hidden layer""" >> def __init__(self, inSize, hiddenSize, outSize): super().__init__() # creates the weights and biases for the first layer. self.linear1 = nn.Linear(inSize, hiddenSize) # creates the weights and biases for the output layer. self.linear2 = nn.Linear(hiddenSize, outSize) >> def forward(self, xb): # flatten the image tensors (xb.size[0] gets the batch size, -1 adapts to whatever fits all units) xb = xb.reshape(xb.size(0), -1) # get intermediate outputs from hidden layer out = self.linear1(xb) # apply activation function element wise on the outputs from self.linear out = F.relu(out) # get predictions using the output layer. out = self.linear2(out) return out
Давайте медленно рассмотрим первую часть - создание экземпляра.
>> class CIFAR10model(nn.Module): """ feedforward neural network with single hidden layer""" >> def __init__(self, inSize, hiddenSize, outSize): super().__init__() self.linear1 = nn.Linear(inSize, hiddenSize) self.linear2 = nn.Linear(hiddenSize, outSize)
Наш новый класс CIFAR10model основан на родительском модуле (nn.module
)
Во-первых, мы используем __init__
для создания экземпляра нашего нового класса CIFAR10model. Для успешного создания модели CIFAR10 требуются входной размер (inSize
), размер скрытого слоя (hiddenSize
) и выходной размер (outSize
). self является обязательным и представляет собой экземпляр самого объекта.
super().__init__
помогает наследовать методы родительского класса nn.module.
Затем мы определяем self.linear1
и self.linear2
, которые определены с помощью метода nn.Linear
, который мы получили через родительский класс.
Напомним, что nn.Linear
потребовались inputSize
и outputSize
для создания модели логистической регрессии (матрица веса и смещения подходящего размера). Точно так же, как математическая интуиция, мы можем практически дважды запустить логистическую регрессию.
self.linear1
создает W [1] (размер: 3072, 32) и b [1] (размер: 32, используя два входа - функции (3072) и скрытый слой (32). Помните, что наш скрытый слой , если рассматривать это просто как логистическую регрессию, это наш результат.self.linear2
создает W [2] (размер: 32, 10) и b [2] (размер: 10), используя размер скрытого слоя в качестве входных данных (32) и количество классов в качестве выходных данных (10).
Теперь давайте перейдем ко второй части, переопределив метод forward (xb) или то, как ваша модель соотносит данные с весами и смещениями, созданными self.linear1 и self.linear2.
... >> def forward(self, xb): xb = xb.reshape(xb.size(0), -1) out = self.linear1(xb) out = F.relu(out) out = self.linear2(out) >> return out
forward требует передачи только пакета обучающих примеров (size: batchSize, features).
Во-первых, xb.reshape
(взаимозаменяемый с xb.view
), аналогично логистической регрессии, изменяет форму входных данных в соответствии с моделью. Наши нетронутые xb будут иметь размер (100, 3, 32, 32), который нельзя умножить ни на один из наших весов.
Здесь reshape заставляет количество строк в новом xb равняться batchSize (принимая длину наших пакетов xb.size (0)).
Затем мы используем -1, чтобы адаптироваться к этому - в основном, -1 решает, какое количество столбцов будет содержать то же количество элементов, что и наша предыдущая матрица размера (100, 3, 32, 32). Поскольку мы должны иметь строки batchSize (100), сколько столбцов потребуется, чтобы матрица поместилась в элементы (3 x 32 x 32 x 100)? Это эквивалентно решению для x за 100 x = 307200.
out - наша текущая переменная выходов. Наши шаги:
out = self.linear1(xb)
- получить значения 32 промежуточных узлов в нейронной сети путем умножения наших 3072 входных данных на W [1] и добавления смещения b [1].out = F.relu(out)
запускает функцию активации ReLU поэлементно для всех значений на промежуточном уровне - если мы имеем дело с batchSize 100, это будет матрица размера (100, 32).
out = self.linear2(out)
получает значения 10 выходных узлов в нейронной сети, умножая наши промежуточные значения на W [2] и добавляя смещение b [2]. Это будет матрица логитов, для которой нам нужно будет использовать softmax, где строки - это обучающий пример (100), а столбцы содержат logit вероятности для каждого класса (10).
- Нам не нужно запускать активацию
F.relu
, поскольку мы будем использовать softmax для преобразования наших логитов в конкретные вероятности. В двоичной классификации (где на выходе просто 0 или 1) вместо этого мы использовали бы сигмоид. - затем мы возвращаемся, что является [2], матричным произведением наших весов и смещений во втором слое. a [2] - это матрица логитов, где каждая строка является обучающим примером, а каждый столбец - прогнозом для соответствующего класса.
Теперь мы можем создать экземпляр модели обучения, который мы определили в нескольких предыдущих блоках кода. Нам просто нужно передать три аргумента: функции или входы (inSize
), узлы скрытого слоя (hiddenSize
) и количество классов или размер вывода (outSize
).
>> inSize = 3072 >> hiddenSize = 32 >> outSize = 10 >> model = CIFAR10model(inSize, hiddenSize, outSize)
Теперь мы можем проверить размер матриц веса и смещения linear1()
и linear2()
внутри нашего экземпляра модели CIFAR10model.
>> for t in model.parameters(): print(t.shape) torch.Size([32, 3072]) # W[1] torch.Size([32]) # b[1] torch.Size([10, 32]) # W[2] torch.Size([10]) # b[2]
Они точно соответствуют диаграммам, за исключением размеров смещения. Это потому, что, как я кратко объяснил заранее, смещения на самом деле не матрицы, а векторы (не 100, 32, а 1, 32). Они были нарисованы таким образом, чтобы избежать дополнительной путаницы, связанной с автоматической способностью Python транслировать векторы на матрицы при выполнении матричных операций.
Давайте пока проверим нашу модель.
>> for images, labels in trainDL: print('images.shape: ' + str(images.shape)) outputs = model(images) loss = F.cross_entropy(outputs, labels) print('loss: ' + str(loss)) break images.shape: torch.Size([100, 3, 32, 32]) loss: tensor(2.3392, grad_fn=<NllLossBackward>)
Давайте рассмотрим первый пакет в нашем обучающем наборе (trainDL) - мы заканчиваем цикл с помощью оператора break
, чтобы он выполнялся только один раз.
Мы можем распечатать форму нашей партии, которая сама по себе представляет собой четырехмерную матрицу размером ([100, 3, 32, 32])
.
Затем мы можем подогнать нашу модель к 100 изображениям в первом пакете и получить наши выходные данные (матрица 100, 10 логитов). Затем мы можем рассчитать наши потери, используя кросс-энтропию.
Распечатывая наш убыток, мы видим, что это 2,3392. Это число само по себе довольно бесполезно, но, говоря о том, что мы еще не обучили нашу модель, это примерно самая большая потеря будет - когда мы правильно обучаем нашу модель, мы должны видеть, что отсюда она уменьшается. Мы можем использовать некоторую метрику точности, если хотим видеть ее в более ощутимой форме, например в точности.
>> print('outputs.shape : ', outputs.shape) >> print('sample outputs :\n', F.softmax(outputs)[:2].data) outputs.shape : torch.Size([100, 10]) sample outputs : tensor([[0.0930, 0.0877, 0.0894, 0.0983, 0.1081, 0.0955, 0.1214, 0.0973, 0.1011, 0.1083], [0.0934, 0.0964, 0.0836, 0.1114, 0.1005, 0.0903, 0.1178, 0.0988, 0.1045, 0.1032]])
Затем мы можем проверить форму наших выходных данных, чтобы увидеть, соответствует ли она нашему прогнозу. Действительно, с формой [100, 10].
Наконец, мы можем проверить некоторые образцы векторов результатов, выведенные нашей простой нейронной сетью - давайте распечатаем результаты для первых двух изображений в нашем пакете.
На самом деле они не будут логитами, поскольку PyTorch F.cross_entropy автоматически вычисляет softmax для векторов логитов перед вычислением потерь.
Теперь, когда мы подстраиваемся к более сложной модели, ЦП нашего компьютера не справляется с этой задачей - он просто не может достаточно быстро вычислять матричные операции. Вот почему нам нужно выполнить загрузку в графический процессор (GPU) перед обучением.
Загрузка в графический процессор (GPU)
Что такое графический процессор?
В 1999 году NVidia выпустила GeForce 256, первый в мире графический процессор, основной целью которого является повышение частоты кадров в 3D-играх для ПК.
Графические процессоры не лучше, чем стандартный центральный процессор (ЦП) вашего компьютера - на самом деле, они, вероятно, намного хуже, когда дело доходит до общих задач.
Графические процессоры были созданы для одной цели. Рендеринг графики для 3D-игр на ПК быстро, очень быстро. Это означало очень быструю математику. Графические процессоры выполняют вычисления параллельно ( множество вычислений одновременно) , в отличие от процессоров, которые выполняют вычисления последовательно (одно вычисление за раз ) .
Графические процессоры для машинного обучения
В основе машинного обучения лежат очень быстрые матричные операции.
Было естественно провести эту связь между графическими процессорами и машинным обучением. Это время пришло как часть возрождения интереса к машинному обучению в 2010-х годах, первопроходцами которого стали Джеффри Хинтон, Ян ЛеКун, Йошия Бенджио, Илья Суцкевер, Алекс Крижевский и Эндрю Нг и многие другие.
В 2009 году Раджат Райна, Ананд Мадхаван и Эндрю Нг из Стэнфорда опубликовали Крупномасштабное глубокое неконтролируемое обучение с использованием графических процессоров, возможно, первое официальное предложение по использованию графических процессоров для целей машинного обучения.
Вскоре последовали многие, кульминацией которых стало выступление Джеффри Хинтона, Ильи Суцкевер и Алекса Крижевского ImageNet, которое многие считают решающим поворотным моментом для реализации глубокого обучения. Частично из-за новаторского использования графических процессоров, команда испортила любую другую точность тестового набора.
Загрузка в графический процессор в PyTorch
Использовать графический процессор для ускорения собственных алгоритмов всего через десять лет после их дебюта в сообществе машинного обучения поразительно просто.
Google Colab и Kaggle (как ни странно, оба принадлежат Google) предоставляют своим пользователям бесплатные удаленно подключенные графические процессоры (и TPU) для использования.
- в Colab перейдите в раздел "Среда выполнения" ›Изменить тип среды выполнения› Графический процессор.
- в Kaggle перейдите к трем точкам «больше» в правом верхнем углу интерфейса, затем выберите «Ускоритель» ›GPU.
Во-первых, давайте определим вспомогательную функцию, которая сообщает нам, следует ли использовать графический процессор или процессор, в зависимости от доступности.
>> def getDefaultDevice(): """ Pick GPU if available, else, CPU""" if torch.cuda.is_available() == True: return torch.device('cuda') else: return torch.device('cpu')
Используя torch.cuda.is_avaliable (), мы можем получить логическое значение True или False в зависимости от того, доступен ли графический процессор. CUDA (Compute Unified Device Architecture) - это просто платформа NVIDIA для графических процессоров - CUDA позволяет работать как с графическим процессором, так и с процессором.
Если вы работаете в Colab / Kaggle и не включили графический процессор, вы должны получить следующее:
>> device = getDefaultDevice() >> device device(type='cpu')
Теперь давайте создадим функцию, которая перемещает данные (в данном случае наш обучающий набор DataLoader) в графический процессор, если он присутствует.
>> def toDevice(data, device): """ move tensors to a chosen device """ if isinstance(data, (list, tuple)): return [toDevice(x, device) for x in data] return data.to(device, non_blocking=True)
строка if isinstance(data, (list, tuple)
означает, что если наши данные, переданные в функцию, находятся в форме (таким образом, являются экземпляром) списка или кортежа, выполнить цикл по списку / кортежу и передать каждую часть данных в графический процессор. На самом деле он относится к самому себе return [toDevice(x, device) for x in data]
, и это нормально - вместо этого все содержимое списка будет проходить через последний оператор return data.to(device, non_blocking=True)
.
Затем мы можем протестировать эту функцию, передав первую партию наших изображений в графический процессор и проверив, правильно ли они перемещены.
>> for images, labels in trainDL: print(images.device) print("transfered to GPU") images = toDevice(images, device) print(images.device) break cpu transfered to GPU cuda:0
Просто переместив наши изображения в графический процессор с помощью функции toDevice()
, которую мы определили, мы увидим, что успешно передали их.
Наконец, давайте создадим загрузчик, который загружает данные в графический процессор прямо перед их обработкой [обучением]. Мы не хотим передавать все данные на GPU в начале нашей программы - нам нужен только как можно более ограниченный объем данных на GPU, чтобы ускорить время выполнения.
Давайте создадим новый класс, который сделает это.
>> class DeviceDataLoader(): """ Wrap a dataloader to move data to a device""" def __init__(self, dl, device): # add attributes. self.dl = dl self.device = device def __iter__(self): """Yield a batch of data after moving it to device""" # loop through dataloader attached to self. for b in self.dl: yield toDevice(b, self.device) def __len__(self): """ number of batches """ return len(self.dl)
Это немного сложно. Разобьем его на более мелкие части. Во-первых, заявление __init__
.
>> class DeviceDataLoader(): """ Wrap a dataloader to move data to a device""" def __init__(self, dl, device): # add attributes. self.dl = dl self.device = device ...
На самом деле нам не нужно строить родительский класс, поэтому мы оставляем class DeviceDataLoader():
без аргументов. Затем мы создаем экземпляр объекта класса DeviceDataLoader()
и определяем, что для него требуются два аргумента: dl
, некоторый DataLoader и device
, устройство, на которое вы хотите его загрузить.
Затем мы определяем два атрибута нашего объекта DeviceDataLoader()
, которые напрямую получаются из наших аргументов.
Давайте теперь определим нашу первую функцию для нашего нового типа объекта __iter__.
... def __iter__(self): """Yield a batch of data after moving it to device""" for b in self.dl: yield toDevice(b, self.device) ...
Поскольку мы начинаем с чистого листа (без родительского класса), мы должны определить способ итерации по нашему DeviceDataLoader()
объекту.
Мы делаем это, определяя __iter__
, который требует только самого себя (self)
.
Мы перебираем пакеты b
в атрибуте DataLoader dl
нашего DeviceDataLoader()
объекта, который является просто нашим старым обучающим DataLoader.
Для каждого пакета мы передаем его функции toDevice(data, device)
, которая передает его на указанное устройство (self.device
).
Урожайность - это практически итеративный возврат - каждый раз, когда запускается этот цикл for, возвращается следующий элемент в yield. В этом примере
- При первом запуске цикла for мы помещаем первый элемент в
self.dl
вself.device.
- Во второй раз, когда мы запускаем цикл for, мы помещаем второй элемент из
self.dl
вself.device
. - И так далее…
По сути, __iter__
позволяет вам перебирать объект DeviceDataLoader()
для передачи пакетов в GPU один за другим.
И, наконец, последняя и самая простая функция, которую мы должны определить в нашем новом классе, len ().
... def __len__(self): """ number of batches """ return len(self.dl)
Эта функция возвращает длину атрибута DataLoader (dl
) объекта. Поскольку DataLoader измеряется партиями, он возвращает количество партий в наших данных.
Нам не нужно отключать что-то от графического процессора, поскольку существует автоматический процесс, в котором данные, которые не обрабатываются, удаляются, хотя есть методы для ручного удаления данных, которые вызывают утомление.
Теперь мы можем обернуть наши загрузчики данных в наш новый класс и использовать наши новые методы, которые мы определили.
device = getDefaultDevice(): >> trainDL = DeviceDataLoader(trainDL, device) >> validDL = DeviceDataLoader(valDL, device)
Теперь, когда у нас есть способ эффективно выполнять вычисления, мы можем приступить к обучению нашей нейронной сети.
обучение модели
Реализация кода обучения почти идентична реализации логистической регрессии, хотя математика немного отличается. Я объясню математику обратного распространения ошибки в другой статье, на которую я дам ссылку внизу, но здесь я просто дам интуитивное объяснение и код.
Мы по-прежнему используем softmax + кросс-энтропию для расчета потерь совершенно идентичным образом (поскольку мы все еще имеем дело с 10 классами), поэтому вы можете прочитать об этом в предыдущей статье.
Основное отличие состоит в том, что у нас есть цепочка из двух алгоритмов логистической регрессии, прежде чем мы преобразуем наши логиты в вероятности и наши вероятности в общую стоимость.
Исчисление высокого уровня, объясняющее обучение нейронной сети
На высоком уровне мы используем мини-пакетный градиентный спуск для обучения нашей модели. Так же, как в логистической регрессии, мы делаем это, вычисляя отрицательный градиент нашей модели.
Отрицательный градиент - это вектор всех частных производных функции стоимости по каждому весу и смещению в модели. В многомерном исчислении градиент - это направление наискорейшего подъема функции, поэтому отрицательный градиент будет направлением наискорейшего спуска.
Для одной итерации градиентного спуска мы хотим обновить все 98666 весов и смещений с помощью соответствующей частной производной.
Мы находим производные через обратное распространение - способ использования правила цепочки в исчислении для распространения в обратном направлении по сети, вычисляя производные. Я сделаю ссылку на дополнение к мини-статье о математике обратного распространения ошибки.
Есть целый ряд интуиций, не связанных с математическим расчетом, которые я не буду здесь повторять, но ознакомьтесь с короткими сериями 3blue1brown о нейронных сетях, которые не требуют знаний в области математического анализа.
Примечание о мини-пакетном градиентном спуске
Мини-пакетный градиентный спуск по сути остается таким же, как логистическая регрессия, мы просто имеем дело с большими числами. Наша нейронная сеть выполняет обратное распространение каждого пакета для вычисления производных.
Градиентный спуск выполняется для каждого пакета из 100, что означает, что параметры обновляются с помощью аппроксимации истинного градиента каждый пакет. Одна тренировочная эпоха завершается после прохождения всех партий.
вычисление всех 98666 производных для одной партии
Код полностью идентичен коду логистической регрессии. Я кратко рассмотрю большую часть этого здесь - ознакомьтесь с разделом Подгонка модели в публикации о логистической регрессии, и это будет объяснено немного лучше.
# recall that xb is the X (a list batchSize long of 3x32x32 images) for a batch. yb is the corresponding labels for those images. >> def lossBatch(model, lossFn, xb, yb, opt=None, metric=None): # calculate the loss preds = model(xb) loss = lossFn(preds, yb) if opt is not None: # compute gradients loss.backward() # update parameters opt.step() # reset gradients to 0 opt.zero_grad()metricResult = None if metric is not None: metricResult = metric(preds, yb)return loss.item(), len(xb), metricResult
Приведенный выше код определяет функцию, которая вычисляет потери, метрику и производные по всем параметрам 98666 для одного пакета (xb, yb) с градиентным спуском [mini-batch].
оценка стоимости и точности набора для валидации
Опять тот же код, что и в предыдущей статье.
Здесь мы используем ту же функцию lossBatch без оптимизации, чтобы получить оценку потерь, длины и метрики для каждого пакета в наборе проверки. Затем мы используем их и усредняем по перекрестной проверке, чтобы получить средний убыток и средний показатель.
подгонка модели
Подбор модели также идентичен, но давайте внесем небольшие изменения, чтобы мы могли отслеживать потери и метрические показатели нашей эффективности на нашем наборе для проверки для построения графика позже.
>> def fit(epochs, model, lossFn, opt, trainDL, valDL, metric=None): losses = [] metrics = [] valList = [0.10] for epoch in range(epochs): # training - perform one step gradient descent on each batch, for all batches for xb, yb in trainDL: loss,_,lossMetric=lossBatch(model, lossFn, xb yb, opt) # evaluation on cross val dataset - after updating over all batches, technically one epoch # evaluates over all validation batches and then calculates average val loss, as well as accuracy valResult = evaluate(model, lossFn, valDL, metric) valLoss, _, valMetric = valResult losses.append(valLoss) metrics.append(valMetric) # print progress if metric is None: print('Epoch [{}/{}], Loss: {:.4f}'.format(epoch + 1 epochs, valLoss)) else: print('Epoch [{}/{}], Loss: {:.4f}, {}: {:.4f}'.format(epoch + 1, epochs, valLoss, metric.__name__, valMetric)) return losses, metrics
Здесь мы завершаем все и перебираем эпохи градиентного спуска. Поскольку мы выполняем мини-пакет, на каждой итерации мы вычисляем производные для всех пакетов (таким образом, вложенный цикл for
).
Определение нашей метрики точности
Как и раньше, нам всегда нравится пересматривать нашу метрику точности.
def accuracy(outputs, labels): _, preds = torch.max(outputs, dim=1) return torch.sum(preds==labels).item() / len(preds)
Это суммирует различия между нашими выходными данными (логит-векторами) и заданными метками.
Перенос модели на GPU
Чтобы ускорить наш алгоритм, мы должны переместить нашу модель на GPU.
# model (on GPU) device = 'cuda' model = CIFAR10model(inputSize, hiddenSize, numClasses) toDevice(model, device)
Мы можем сделать это, используя ту же функцию toDevice()
, которую мы создали.
Прежде чем мы перейдем к окончательному обучению нашей модели, давайте быстро поговорим о воспроизводимости.
Воспроизводимость
Одна из проблем нашего алгоритма состоит в том, что он не согласован. Каждый раз, когда вы запускаете его, даже если вы используете одну и ту же скорость обучения, в конце вы получите немного другую точность / потерю, поскольку наши параметры инициализируются случайным образом, каждый раз, когда мы запускаем весь алгоритм. Это означает, что кто-то другой или даже вы не можете получить точно такие же результаты с помощью одного и того же алгоритма.
Это нехорошо, особенно когда вы пытаетесь увидеть, как изменения определенных гиперпараметров влияют на общую точность (часто, ежеминутно).
Мы можем быть уверены, что всегда получаем «одинаковую» случайную инициализацию, используя случайные начальные числа. Для людей, знакомых с Minecraft, это то же самое, что использовать семя для создания карты. Если вы используете одно и то же семя (здесь число), вы получите такую же случайную инициализацию.
Добавление этого кода в начало программы, взятого у пользователя isalirezag на форумах PyTorch, гарантирует, что вы установите все возможные версии random для выбранного нами manualSeed.
manualSeed = 1 import random np.random.seed(manualSeed) random.seed(manualSeed) torch.manual_seed(manualSeed) # for GPU torch.cuda.manual_seed(manualSeed) torch.cuda.manual_seed_all(manualSeed) torch.backends.cudnn.enabled = False torch.backends.cudnn.benchmark = False torch.backends.cudnn.deterministic = True
Теперь, когда мы закончили, мы можем обучить нашу модель.
lr = 0.002 losses1, metrics1 = fit(50, lr, model, F.cross_entropy, trainDL, validDL, metric=accuracy)
Для первых 10 эпох результат должен выглядеть примерно так.
Epoch [1/100], Loss: 2.3206, accuracy: 0.1003 Epoch [2/100], Loss: 2.2711, accuracy: 0.1714 Epoch [3/100], Loss: 2.1491, accuracy: 0.1799 Epoch [4/100], Loss: 2.0796, accuracy: 0.2073 Epoch [5/100], Loss: 2.0501, accuracy: 0.2339 Epoch [6/100], Loss: 1.9828, accuracy: 0.2774 Epoch [7/100], Loss: 1.9471, accuracy: 0.2876 Epoch [8/100], Loss: 2.0135, accuracy: 0.2581 Epoch [9/100], Loss: 1.9204, accuracy: 0.3050 Epoch [10/100], Loss: 1.8904, accuracy: 0.3132 ...
По сравнению с нашими оценками, полученными с помощью логистической регрессии, мы видим небольшое повышение точности. Мы также можем отметить, что для повышения точности нашей нейронной сети требуется больше времени, поскольку, по крайней мере, в моей реализации алгоритм не достигает максимальной точности, достигнутой при логистической регрессии, до 55-й эпохи.
Давайте посмотрим, как мы получим разные результаты при использовании разных скоростей обучения. Постройте график точности при проверке, используя список, сгенерированный fit()
и через модуль matplotlib.pyplot
.
plt.plot(metrics1, ‘-x’)
axes = plt.gca() #gca means get current axes
axes.set_ylim([0,1])
plt.xlabel(‘epoch’)
plt.ylabel(‘accuracy’)
plt.title(‘accuracy vs number of epochs’)
Давайте посмотрим на наивысшую точность проверки, которую мы смогли получить при скорости обучения 0,03. Это дало нам немного более высокий балл по набору тестов, общая точность которого составила 46,43%.
Почему он все еще так плохо предсказывает?
Наша максимально возможная точность на тестовом наборе с использованием скорости обучения 0,03 составила 46% с использованием нейронной сети с одним скрытым слоем. Это всего лишь небольшой шаг (+ 6%) к точности нашего тестового набора с логистической регрессией (40%).
Это увеличение на 6% можно отнести к использованию нелинейных функций активации, в данном случае функции ReLU. Это добавляет сети нелинейность, позволяя вычислять нелинейные отношения. Может быть трудно придумать линейные отношения в нескольких измерениях, но все, что нужно знать, - это то, что они позволяют делать более сложные прогнозы. Подробнее о нелинейности я говорил в предыдущей статье.
Но почему он все еще так плохо предсказывает? Причина в том, как он принимает входные функции и как алгоритм «смотрит» на изображение.
Стандартные нейронные сети просматривают изображение по одному пикселю за раз. То есть не учитываются пиксели, окружающие рассматриваемый пиксель.
Думать об этом, предугадывать, что такое изображение, просто глядя на каждый пиксель независимо и сравнивая значения R, G, B этого пикселя с предыдущими примерами, - далеко не надежный способ узнать, что такое изображение. Представьте, что вы пытаетесь понять, что такое изображение, просто глядя на каждый пиксель - сложно, верно? Нам нужно знать не только пиксель, но и некоторую группу пикселей, которые его окружают, чтобы сделать более уверенное предположение.
Вот почему мы не используем стандартные нейронные сети для распознавания изображений.
Вместо этого мы используем алгоритмы, называемые сверточными нейронными сетями. Сверточные нейронные сети учитывают значения пикселей, окружающих каждый пиксель, что приводит к более высокой точности классификации.
Я расскажу о математике, теории и коде сверточной нейронной сети в последней части этого мини-сериала, посвященного классификации изображений с помощью CIFAR-10.
Код
Вот полный фрагмент кода (в Google Colab), на фрагменты которого я ссылался на протяжении всего задания. Это то, что я сделал, следуя этому фантастическому руководству, но применив принципы к немного другому набору данных.
Спасибо, что нашли время следовать этому чудовищно длинному руководству, или даже если вы зашли только на некоторые части. Надеюсь, вы останетесь в финале этого мини-сериала!
Адам Дхалла - ученик старшей школы из Ванкувера, Британская Колумбия, в настоящее время участвует в программе STEM и бизнес-сообществе TKS. Он увлечен миром активного отдыха и в настоящее время изучает новейшие технологии в экологических целях. Поддерживать,
Следите за его I nstagram и его LinkedIn. Чтобы получить больше похожего контента, подпишитесь на его информационный бюллетень здесь.