Курсовая работа. Глубокое обучение
Скачать 4.97 Mb.
|
106 Глава 3. Основы глубокого обучения ''' def __init__(self, neurons: int, activation: Operation = Sigmoid()) -> None: ''' Для инициализации нужна функция активации. ''' super().__init__(neurons) self.activation = activation def _setup_layer(self, input_: ndarray) -> None: ''' Определение операций для полносвязного слоя. ''' if self.seed: np.random.seed(self.seed) self.params = [] # веса self.params.append(np.random.randn(input_.shape[1], self.neurons)) # отклонения self.params.append(np.random.randn(1, self.neurons)) self.operations = [WeightMultiply(self.params[0]), BiasAdd(self.params[1]), self.activation] return None Обратите внимание, что если мы сделаем активацию по умолчанию линейной, то это то же самое, что активации нет как таковой, а на выход слоя передается то же, что и на входе. Что понадобится еще помимо Operation и Layer ? Чтобы обучить нашу модель, точно понадобится класс NeuralNetwork , которым мы обернем Layer так же, как класс Layer обернут вокруг Operation . Мы не знаем, какие еще классы понадобятся, поэтому пока сделаем NeuralNetwork , а дальше определимся по ходу дела. Класс NeuralNetwork и, возможно, другие 107 Класс NeuralNetwork и, возможно, другие Что должен делать наш класс NeuralNetwork ? Он должен учиться на дан- ных, а точнее, собирать пакеты «наблюдений» ( X ) и «правильных ответов» ( y ), и изучать взаимосвязь между X и y , подстраивая функцию, которая позволит преобразовать X в предсказания p , близкие к y Как именно будет происходить это обучение, учитывая только что опре- деленные классы Layer и Operation ? Вспоминая, как работала модель из последней главы, реализуем следующее: 1. Нейронная сеть принимает на вход набор данных X и последователь- но пропускает его через каждый слой (а на самом деле — через ряд операций), и полученным результатом будет прогноз. 2. Прогноз сравнивается с y , чтобы рассчитать потери и сгенерировать «градиент потерь», который является частной производной потери по каждому элементу в последнем слое в сети (который и создает прогноз). 3. Наконец, этот градиент проходит по сети в обратном направлении через каждый уровень и вычисляются «градиенты параметров» — частная производная потерь по каждому из параметров. Результаты сохраняются. Визуализация На рис. 3.5 показано описание нейронной сети с точки зрения слоев. x Входной слой Прогноз Обратное распространение Y Потеря Выходной слой Скрытый слой L Значения класс Градиент потерь Рис. 3.5. Обратное распространение со слоями вместо операций 108 Глава 3. Основы глубокого обучения Код Как это реализовать? Мы хотим, чтобы наша нейронная сеть в конечном итоге работала со слоями так же, как наши слои работали с операциями. То есть метод forward должен получать X в качестве входных данных и просто делать что-то вроде: for layer in self.layers: X = layer.forward (X) return X Аналогично мы хотим, чтобы обратный метод брал градиент и делал что-то вроде: for layer in reversed(self.layers): grad = layer.backward(grad) Откуда берется этот самый grad ? Он берется от потери, специальной функции, которая принимает прогноз с y : y Вычисляет одно число, представляющее собой «штраф» для сети, которая делает такой прогноз. y Отправляет назад градиент для каждого элемента прогноза относитель- но потери. Этот градиент — это то, что последний слой в сети получит как вход для своей обратной функции. В примере из предыдущей главы функция потерь была квадратом разно- сти между прогнозом и целью, и градиент прогноза относительно потери вычислялся соответствующим образом. Как это реализовать? Потери — это важно, поэтому для них надо будет сделать собственный класс. Кроме того, этот класс может быть реализован аналогично классу Layer , за исключением того, что метод forward будет выдавать число с плавающей точкой вместо объекта ndarray , которое будет передано на следующий уровень. Перейдем к коду. Класс Loss Базовый класс Loss будет похож на Layer — методы forward и backward будут проверять соответствие размерностей ndarrays и выполнять два Класс NeuralNetwork и, возможно, другие 109 метода, _output и _input_grad , которые должен реализовывать любой подкласс Loss : class Loss(object): ''' Потери нейросети. ''' def __init__(self): '''Пока ничего не делаем''' pass def forward(self, prediction: ndarray, target: ndarray) -> float: ''' Вычисление значения потерь. ''' assert_same_shape(prediction, target) self.prediction = prediction self.target = target loss_value = self._output() return loss_value def backward(self) -> ndarray: ''' Вычисление градиента потерь по входам функции потерь. ''' self.input_grad = self._input_grad() assert_same_shape(self.prediction, self.input_grad) return self.input_grad def _output(self) -> float: ''' Функция _output должна реализовываться всем подклассами класса Loss. ''' raise NotImplementedError() 110 Глава 3. Основы глубокого обучения def _input_grad(self) -> ndarray: ''' Функция _input_grad должна реализовываться всем подклассами класса Loss. ''' raise NotImplementedError() Как и в классе Operation , мы проверяем, что градиент, который потери от- правляют назад, имеет ту же форму, что и прогноз, полученный в качестве входных данных от последнего уровня сети: class MeanSquaredError(Loss): def __init__(self) '''пока ничего не делаем''' super().__init__() def _output(self) -> float: ''' вычисление среднего квадрата ошибки. ''' loss = np.sum(np.power(self.prediction — self.target, 2)) / self.prediction.shape[0] return loss def _input_grad(self) -> ndarray: ''' Вычисление градиента ошибки по входу MSE. ''' return 2.0 * (self.prediction — self.target) / self.prediction.shape[0] Здесь мы просто кодируем прямое и обратное правила формулы средне- квадратичной потери. Это последний требуемый строительный блок. Давайте рассмотрим, как эти части сочетаются друг с другом, а затем приступим к построению модели. Глубокое обучение с чистого листа 111 Глубокое обучение с чистого листа В конечном итоге мы хотим создать класс NeuralNetwork , как на рис. 3.5, и использовать эту нейросеть для моделей глубокого обучения. Прежде чем мы начнем писать код, давайте точно опишем, каким будет этот класс и как он будет взаимодействовать с классами Operation , Layer и Loss , ко- торые мы только что определили: 1. NeuralNetwork будет в качестве атрибута получать список экземпля- ров Layer . Слои будут такими, как было определено ранее — с пря- мым и обратным методами. Эти методы принимают объекты ndarray и возвращают объекты ndarray 2. Каждый Layer будет иметь список операций Operation , сохраненный в атрибуте operations слоя функцией _setup_layer 3. Эти операции, как и сам слой, имеют методы прямого и обратного преобразования, которые принимают в качестве аргументов объекты ndarray и возвращают объекты ndarray в качестве выходных данных. 4. В каждой операции форма output_grad , полученная в методе backward , должна совпадать с формой выходного атрибута Layer . То же самое верно для форм input_grad , передаваемых в обратном направлении методом backward и атрибутом input_ 5. Некоторые операции имеют параметры (которые хранятся в атри- буте param ). Эти операции наследуют от класса ParamOperation . Те же самые ограничения на входные и выходные формы применяются к слоям и их методам forward и backward — они берут объекты ndarray , и формы входных и выходных атрибутов и их соответствующие градиенты должны совпадать. 6. У класса NeuralNetwork также будет класс Loss . Этот класс берет выходные данные последней операции из NeuralNetwork и цели, проверяет, что их формы одинаковы, и, вычисляя значение потерь (число) и ndarray loss_grad , которые будут переданы в выходной слой, начинает обратное распространение. Реализация пакетного обучения Мы уже говорили о шагах обучения модели по одной партии за раз. Повторим: 112 Глава 3. Основы глубокого обучения 1. Подаем входные данные через функцию модели («прямой проход») для получения прогноза. 2. Рассчитываем потери. 3. Вычисляем градиенты потерь по параметрам с использованием цеп- ного правила и значений, вычисленных во время прямого прохода. 4. Обновляем параметры на основе этих градиентов. Затем мы передаем новый пакет данных и повторяем эти шаги. Перенести эти шаги платформу NeuralNetwork очень просто: 1. Получаем объекты ndarray X и y в качестве входных данных. 2. Передаем X по слоям. 3. Используем Loss для получения значения потерь и градиента потерь для обратного прохода. 4. Используем градиент потерь в качестве входных данных для метода backward , вычисления param_grads для каждого слоя в сети. 5. Вызываем на каждом слое функцию update_params , которая будет брать скорость обучения для NeuralNetwork , а также только что рас- считанные param_grads Наконец, у нас есть полное определение нейронной сети, на которой можно выполнять пакетное обучение. Теперь напишем код. Нейронная сеть: код Код выглядит весьма просто: class NeuralNetwork(object): ''' Класс нейронной сети. ''' def __init__(self, layers: List[Layer], loss: Loss, seed: float = 1) ''' Нейросети нужны слои и потери. ''' Глубокое обучение с чистого листа 113 self.layers = layers self.loss = loss self.seed = seed if seed: for layer in self.layers: setattr(layer, "seed", self.seed) def forward(self, x_batch: ndarray) -> ndarray: ''' Передача данных через последовательность слоев. ''' x_out = x_batch for layer in self.layers: x_out = layer.forward(x_out) return x_out def backward(self, loss_grad: ndarray) -> None: ''' Передача данных назад через последовательность слоев. ''' grad = loss_grad for layer in reversed(self.layers): grad = layer.backward(grad) return None def train_batch(self, x_batch: ndarray, y_batch: ndarray) -> float: ''' Передача данных вперед через последовательность слоев. Вычисление потерь. Передача данных назад через последовательность слоев. ''' predictions = self.forward(x_batch) loss = self.loss.forward(predictions, y_batch) self.backward(self.loss.backward()) 114 Глава 3. Основы глубокого обучения return loss def params(self): ''' Получение параметров нейросети. for layer in self.layers: yield from layer.params def param_grads(self): ''' Получение градиента потерь по отношению к параметрам нейросети. ''' for layer in self.layers: yield from layer.param_grads С помощью этого класса NeuralNetwork мы можем реализовать модели из предыдущей главы модульным, гибким способом и определить другие модели для представления сложных нелинейных отношений между входом и выходом. Например, ниже показано, как создать две модели, которые мы рассмотрели в предыдущей главе: линейную регрессию и нейронную сеть 1 : linear_regression = NeuralNetwork( layers=[Dense(neurons = 1)], loss = MeanSquaredError(), learning_rate = 0.01 ) neural_network = NeuralNetwork( layers=[Dense(neurons=13, activation=Sigmoid()), Dense(neurons=1, activation=Linear())], loss = MeanSquaredError(), learning_rate = 0.01 ) Почти готово. Теперь мы просто многократно передаем данные через сеть, чтобы модель начала учиться. Но чтобы сделать этот процесс понятнее и проще в реализации для более сложных сценариев глубокого обучения, 1 Скорость обучения 0.01 найдена оптимальной путем экспериментов. Trainer и Optimizer 115 надо определить другой класс, который будет выполнять обучение, а также дополнительный класс, который выполняет «обучение» или фактическое обновление параметров NeuralNetwork с учетом градиентов, вычисленных при обратном проходе. Давайте определим эти два класса. Trainer и Optimizer Есть сходство между этими классами и кодом, который мы использовали для обучения сети в главе 2. Там для реализации четырех шагов, опи- санных ранее для обучения модели, мы использовали следующий код: # Передаем X_batch вперед и вычисляем потери forward_info, loss = forward_loss(X_batch, y_batch, weights) # Вычисляем градиент потерь по отношению к каждому весу loss_grads = loss_gradients(forward_info, weights) # обновляем веса for key in weights.keys(): weights[key] -= learning_rate * loss_grads[key] Этот код находился внутри цикла for , который неоднократно передавал данные через функцию, определяющую и обновляющую нашу сеть. Теперь, когда у нас есть нужные классы, мы, в конечном счете, сделаем это внутри функции подгонки в классе Trainer , который в основном будет оберткой вокруг функции train , использованной в предыдущей главе. (Полный код для этой главы в можно найти на странице книги на GitHub.) Основное отличие состоит в том, что внутри этой новой функции первые две строки из предыдущего блока кода будут заменены этой строкой: neural_network.train_batch (X_batch, y_batch) Обновление параметров, которое происходит в следующих двух строках, будет происходить в отдельном классе Optimizer . И наконец, цикл for , который ранее охватывал все это, будет выполняться в классе Trainer , который оборачивает NeuralNetwork и Optimizer Далее обсудим, почему нам нужен класс Optimizer и как он должен вы- глядеть. 116 Глава 3. Основы глубокого обучения Optimizer В модели, которую мы описали в предыдущей главе, у слоев есть простое правило для обновления весов на основе параметров и их градиентов. В следующей главе мы узнаем, что есть множество других правил обнов- ления. Например, некоторые правила учитывают данные не только от текущего набора данных, но и от всех предыдущих. Создание отдельного класса Optimizer даст нам гибкость в замене одного правила обновления на другое, что мы более подробно рассмотрим в следующей главе. Описание и код Базовый класс Optimizer будет принимать NeuralNetwork , и каждый раз, когда вызывается пошаговая функция step , будет обновлять параметры сети на основе их текущих значений, их градиентов и любой другой ин- формации, хранящейся в Optimizer : class Optimizer(object): ''' Базовый класс оптимизатора нейросети. ''' def __init__(self, lr: float = 0.01): ''' У оптимизатора должна быть начальная скорость обучения. ''' self.lr = lr def step(self) -> None: ''' У оптимизатора должна быть функция "step". ''' pass И вот как это выглядит с простым правилом обновления (стохастический градиентный спуск): class SGD(Optimizer): ''' Стохастический градиентный оптимизатор. ''' def __init__(self, Trainer и Optimizer 117 lr: float = 0.01) -> None: '''пока ничего''' super().__init__(lr) def step(self): ''' Для каждого параметра настраивается направление, при этом амплитуда регулировки зависит от скорости обучения. ''' for (param, param_grad) in zip(self.net.params(), self.net.param_grads()): param -= self.lr * param_grad Обратите внимание, что хотя наш класс NeuralNetwork не имеет мето- да _update_params , мы используем методы params() и param_grads() для извлечения правильных ndarrays для оптимизации. Класс Optimizer готов, теперь нужен Trainer Trainer Помимо обучения модели класс Trainer также связывает NeuralNetwork с Optimizer , гарантируя правильность обучения. Вы, возможно, заметили в предыдущем разделе, что мы не передавали нейронную сеть при иници- ализации нашего оптимизатора. Вместо этого мы назначим NeuralNetwork атрибутом Optimizer при инициализации класса Trainer : setattr(self.optim, 'net', self.net) В следующем подразделе я покажу упрощенную, но рабочую версию класса Trainer , которая пока содержит только метод fit . Этот метод выполняет несколько эпох обучения и выводит значение потерь после некоторого заданного числа эпох. В каждую эпоху мы будем: y перемешивать данные в начале эпохи; y передавать данные через сеть в пакетном режиме, обновляя парамет ры. Эпоха заканчивается, когда мы пропускаем весь обучающий набор через Trainer 118 Глава 3. Основы глубокого обучения Код класса Trainer Ниже приведен код простой версии класса Trainer . Мы скрываем два вспо- могательных метода, использующихся во время выполнения функции fit : generate_batches , генерирующий пакеты данных из X_train и y_train для обучения, и per mute_data , перемешивающий X_train и y_train в на- чале каждой эпохи. Мы также включили аргумент restart в функцию train : если он имеет значение True (по умолчанию), то будет повторно инициализировать параметры модели в случайные значения при вызове функции train : class Trainer(object): ''' Обучение нейросети. ''' def __init__(self, net: NeuralNetwork, optim: Optimizer) ''' Для обучения нужны нейросеть и оптимизатор. Нейросеть назначается атрибутом экземпляра оптимизатора. ''' self.net = net setattr(self.optim, 'net', self.net) def fit(self, X_train: ndarray, y_train: ndarray, X_test: ndarray, y_test: ndarray, epochs: int=100, eval_every: int=10, batch_size: int=32, seed: int = 1, restart: bool = True) -> None: ''' Подгонка нейросети под обучающие данные за некоторое число эпох. Через каждые eval_every эпох выполняется оценка. ''' np.random.seed(seed) if restart: for layer in self.net.layers: layer.first = True |