Серия статей о глубокой нейронной сети с нуля в Rust

После [[3. Глубокая нейронная сеть с нуля в Rust — часть 3 — прямое распространение | Прямое распространение]] нам нужно определить функцию потерь, чтобы вычислить, насколько неверна наша модель в данный момент. Для простой задачи бинарной классификации функция потерь представлена ​​ниже.

где,

m ⇾ количество обучающих примеров

Y ⇾ Истинные обучающие метки

A[L]⇾ Прогнозируемые метки от прямого распространения

Цель функции потерь — измерить несоответствие между предсказанными метками и истинными метками. Минимизируя эту потерю, мы стремимся сделать прогнозы нашей модели как можно ближе к истине.

Чтобы обучить модель и минимизировать потери, мы используем технику, называемую обратным распространением. Этот метод вычисляет градиенты функции стоимости относительно весов и смещений, которые указывают направление и величину корректировок, необходимых для каждого параметра. Вычисления градиента выполняются с использованием следующих уравнений для каждого слоя:

После того, как мы вычислили градиенты, мы можем настроить веса и смещения, чтобы минимизировать потери. Следующие уравнения используются для обновления параметров с использованием альфа скорости обучения:

Выводы этих уравнений можно найти здесь

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

Репозиторий git для всего кода, кроме этой части, представленной по ссылке ниже. Пожалуйста, обратитесь к нему, если вы где-то застряли.

Функция стоимости

Чтобы рассчитать функцию стоимости на основе приведенного выше уравнения стоимости, нам нужно сначала предоставить характеристику журнала для Array2<f32>, поскольку вы не можете напрямую получить журнал массива в ржавчине. Мы сделаем это, написав следующий код в начале lib.rs

trait Log {
    fn log(&self) -> Array2<f32>;
}

impl Log for Array2<f32> {
    fn log(&self) -> Array2<f32> {
        self.mapv(|x| x.log(std::f32::consts::E))
    }
}

Next, in our impl DeepNeuralNetwork we will add a function to calculate the cost.

       pub fn cost(&self, al: &Array2<f32>, y: &Array2<f32>) -> f32 {
        let m = y.shape()[1] as f32;
        let cost = -(1.0 / m)
            * (y.dot(&al.clone().reversed_axes().log())
                + (1.0 - y).dot(&(1.0 - al).reversed_axes().log()));

        return cost.sum();
    }

Далее в нашем impl DeepNeuralNetwork мы добавим функцию для расчета стоимости.

Здесь мы передаем активацию последнего слоя al и истинные метки y для расчета стоимости.

Обратная активация

pub fn sigmoid_prime(z: &f32) -> f32 {
    sigmoid(z) * (1.0 - sigmoid(z))
}

pub fn relu_prime(z: &f32) -> f32 {
    match *z > 0.0 {
        true => 1.0,
        false => 0.0,
    }
}

pub fn sigmoid_backward(da: &Array2<f32>, activation_cache: ActivationCache) -> Array2<f32> {
    da * activation_cache.z.mapv(|x| sigmoid_prime(&x))
}

pub fn relu_backward(da: &Array2<f32>, activation_cache: ActivationCache) -> Array2<f32> {
    da * activation_cache.z.mapv(|x| relu_prime(&x))
}

Функция sigmoid_prime вычисляет производную сигмовидной функции активации. Он принимает входные данные z и возвращает значение производной, которое вычисляется как сигмоид z, умноженный на 1.0 минус сигмоид z.

Функция relu_prime вычисляет производную функции активации ReLU. Он принимает входные данные z и возвращает 1.0, если z больше 0, и 0.0 в противном случае.

Функция sigmoid_backward вычисляет обратное распространение для сигмовидной функции активации. Он берет производную функции стоимости по активации da и кешу активации activation_cache. Он выполняет поэлементное умножение между da и производной сигмовидной функции, примененной к значениям в кэше активации, activation_cache.z.

Функция relu_backward вычисляет обратное распространение для функции активации ReLU. Он берет производную функции стоимости по активации da и кешу активации activation_cache. Он выполняет поэлементное умножение между da и производной функции ReLU, примененной к значениям в кэше активации, activation_cache.z.

Линейный назад

pub fn linear_backward(
    dz: &Array2<f32>,
    linear_cache: LinearCache,
) -> (Array2<f32>, Array2<f32>, Array2<f32>) {
    let (a_prev, w, _b) = (linear_cache.a, linear_cache.w, linear_cache.b);
    let m = a_prev.shape()[1] as f32;
    let dw = (1.0 / m) * (dz.dot(&a_prev.reversed_axes()));
    let db_vec = ((1.0 / m) * dz.sum_axis(Axis(1))).to_vec();
    let db = Array2::from_shape_vec((db_vec.len(), 1), db_vec).unwrap();
    let da_prev = w.reversed_axes().dot(dz);

    (da_prev, dw, db)
}

Функция linear_backward вычисляет обратное распространение для линейной составляющей слоя. Он принимает градиент функции стоимости относительно линейного выхода dz и линейного кеша linear_cache. Он возвращает градиенты по отношению к активации предыдущего слоя da_prev, веса dw и смещения db.

Сначала функция извлекает активацию предыдущего слоя a_prev, матрицу весов w и матрицу смещения _b из линейного кеша. Он вычисляет количество обучающих примеров m путем доступа к форме a_prev и деления количества примеров на m.

Затем функция вычисляет градиент весов dw, используя скалярное произведение между dz и транспонированным a_prev, масштабированным на 1/m. Он вычисляет градиент смещений db, суммируя элементы dz по Axis(1) и масштабируя результат на 1/m. Наконец, он вычисляет градиент активации предыдущего слоя da_prev, выполняя скалярное произведение между транспонированными w и dz.

Функция возвращает da_prev, dw и db.

Обратное распространение

impl DeepNeuralNetwork {
    pub fn initialize_parameters(&self) -> HashMap<String, Array2<f32>> {
 // same as last part
    }
    pub fn forward(
        &self,
        x: &Array2<f32>,
        parameters: &HashMap<String, Array2<f32>>,
    ) -> (Array2<f32>, HashMap<String, (LinearCache, ActivationCache)>) {
    //same as last part
    }

 pub fn backward(
        &self,
        al: &Array2<f32>,
        y: &Array2<f32>,
        caches: HashMap<String, (LinearCache, ActivationCache)>,
    ) -> HashMap<String, Array2<f32>> {
        let mut grads = HashMap::new();
        let num_of_layers = self.layers.len() - 1;

        let dal = -(y / al - (1.0 - y) / (1.0 - al));

        let current_cache = caches[&num_of_layers.to_string()].clone();
        let (mut da_prev, mut dw, mut db) =
            linear_backward_activation(&dal, current_cache, "sigmoid");

        let weight_string = ["dW", &num_of_layers.to_string()].join("").to_string();
        let bias_string = ["db", &num_of_layers.to_string()].join("").to_string();
        let activation_string = ["dA", &num_of_layers.to_string()].join("").to_string();

        grads.insert(weight_string, dw);
        grads.insert(bias_string, db);
        grads.insert(activation_string, da_prev.clone());

        for l in (1..num_of_layers).rev() {
            let current_cache = caches[&l.to_string()].clone();
            (da_prev, dw, db) =
                linear_backward_activation(&da_prev, current_cache, "relu");

            let weight_string = ["dW", &l.to_string()].join("").to_string();
            let bias_string = ["db", &l.to_string()].join("").to_string();
            let activation_string = ["dA", &l.to_string()].join("").to_string();

            grads.insert(weight_string, dw);
            grads.insert(bias_string, db);
            grads.insert(activation_string, da_prev.clone());
        }

        grads
    }

Метод backward в структуре DeepNeuralNetwork выполняет алгоритм обратного распространения для вычисления градиентов функции стоимости относительно параметров (весов и смещений) каждого слоя.

Метод принимает окончательную активацию al, полученную из прямого распространения, истинные метки y и кэши, содержащие линейные значения и значения активации для каждого слоя.

Во-первых, он инициализирует пустой HashMap с именем grads для хранения градиентов. Он вычисляет начальную производную функции стоимости по отношению к al, используя предоставленную формулу.

Затем, начиная с последнего слоя (выходного слоя), он извлекает кэш для текущего слоя и вызывает функцию linear_backward_activation для вычисления градиентов функции стоимости по отношению к параметрам этого слоя. Используемая функция активации — «сигмовидная» для последнего слоя. Вычисленные градиенты для весов, смещений и активации хранятся на карте grads.

Далее метод перебирает оставшиеся слои в обратном порядке. Для каждого слоя он извлекает кэш, вызывает функцию linear_backward_activation для расчета градиентов и сохраняет их на карте grads.

Наконец, метод возвращает карту grads, содержащую градиенты функции стоимости по каждому параметру нейронной сети.

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

Обновить параметры

pub fn update_parameters(
        &self,
        params: &HashMap<String, Array2<f32>>,
        grads: HashMap<String, Array2<f32>>,
        m: f32, 
        learning_rate: f32,

    ) -> HashMap<String, Array2<f32>> {
        let mut parameters = params.clone();
        let num_of_layers = self.layer_dims.len() - 1;
        for l in 1..num_of_layers + 1 {
            let weight_string_grad = ["dW", &l.to_string()].join("").to_string();
            let bias_string_grad = ["db", &l.to_string()].join("").to_string();
            let weight_string = ["W", &l.to_string()].join("").to_string();
            let bias_string = ["b", &l.to_string()].join("").to_string();

            *parameters.get_mut(&weight_string).unwrap() = parameters[&weight_string].clone()
                - (learning_rate * (grads[&weight_string_grad].clone() + (self.lambda/m) *parameters[&weight_string].clone()) );
            *parameters.get_mut(&bias_string).unwrap() = parameters[&bias_string].clone()
                - (learning_rate * grads[&bias_string_grad].clone());
        }
        parameters
    }

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

В этом коде мы проходим через каждый слой и обновляем параметры в HashMap для каждого слоя, используя HashMap градиентов в этом слое. Это вернет нам обновленные параметры.

Это все для этой части. Я знаю, что это было немного сложно, но это часть глубокой нейронной сети. Вот некоторые ресурсы, которые могут помочь вам понять алгоритм более наглядно.

Обзор алгоритма обратного распространения: https://www.youtube.com/watch?v=Ilg3gGewQ5U&t=203s

Расчет позади алгоритма обратного распространения: https://www.youtube.com/watch?v=tIeHLnjs5U8

В следующей и последней части этой серии мы запустим наш цикл обучения и протестируем нашу модель на некоторых изображениях кошек 🐈.

Репозиторий GitHub: https://github.com/akshayballal95/dnn_rust_blog.git

Want to Connect?

My website
LinkedIn
Twitter

Первоначально опубликовано на https://www.akshaymakes.com.