text: # Optimizing a neural network with backward propagation
  
  # A. The need for optimization
  # 1. Coding how weight changes affect accuracy
  # The data point you will make a prediction for
  input_data = np.array([0, 3])
  
  # Sample weights
  weights_0 = {'node_0': [2, 1],
               'node_1': [1, 2],
               'output': [1, 1]
              }
  
  # The actual target value, used to calculate the error
  target_actual = 3
  
  # Make prediction using original weights
  model_output_0 = predict_with_network(input_data, weights_0)
  
  # Calculate error: error_0
  error_0 = model_output_0 - target_actual
  
  # Create weights that cause the network to make perfect prediction (3): weights_1
  weights_1 = {'node_0': [2, 1],
               'node_1': [1, 0],
               'output': [1, 1]
              }
  
  # Make prediction using new weights: model_output_1
  model_output_1 = predict_with_network(input_data, weights_1)
  
  # Calculate error: error_1
  error_1 = model_output_1 - target_actual
  
  # Print error_0 and error_1
  print(error_0)
  print(error_1)
  
  # 2. Scaling up to multiple data points
  from sklearn.metrics import mean_squared_error
  
  # Create model_output_0 
  model_output_0 = []
  # Create model_output_1
  model_output_1 = []
  
  # Loop over input_data
  for row in input_data:
      # Append prediction to model_output_0
      model_output_0.append(predict_with_network(row, weights_0))
      
      # Append prediction to model_output_1
      model_output_1.append(predict_with_network(row, weights_1))
  
  # Calculate the mean squared error for model_output_0: mse_0
  mse_0 = mean_squared_error(target_actuals, model_output_0)
  
  # Calculate the mean squared error for model_output_1: mse_1
  mse_1 = mean_squared_error(target_actuals, model_output_1)
  
  # Print mse_0 and mse_1
  print("Mean squared error with weights_0: %f" %mse_0)
  print("Mean squared error with weights_1: %f" %mse_1)
  
  # B. Gradient Descent
  # 1. Calculating slopes
  # Calculate the predictions: preds
  preds = (weights * input_data).sum()
  
  # Calculate the error: error
  error = preds - target
  
  # Calculate the slope: slope
  slope = input_data * error * 2
  
  # Print the slope
  print(slope)
  
  # 2. Improving model weights
  # learning_rate: by applying a smaller learning rate this means that the update to the model will be a small step forward. Then a model
  # review of the new error can take place. If there is an improvement in the error prediction then this model change can be seen as an improvement. 
  # However, if we had taken a larger step forward using a larger learning rate and this resulted in the error increasing, then the model would have
  # moved in the wrong direction and a larger correction would be required. Therefore, care is required when making model improvements
  
  # Set the learning rate: learning_rate
  learning_rate = 0.01
  
  # Calculate the predictions: preds
  preds = (weights * input_data).sum()
  
  # Calculate the error: error
  error = preds - target
  
  # Calculate the slope: slope
  slope = 2 * input_data * error
  
  # Update the weights: weights_updated
  weights_updated = weights - (learning_rate * slope)
  
  # Get updated predictions: preds_updated
  preds_updated = (weights_updated * input_data).sum()
  
  # Calculate updated error: error_updated
  error_updated = preds_updated - target
  
  # Print the original error
  print(error)
  
  # Print the updated error
  print(error_updated)
  
  # 3. Making multiple updates to weights
  # Shows how multiple updates to the weights can help to improve the model performance as the mse falls with each iteration
  n_updates = 20
  mse_hist = []
  
  # Iterate over the number of updates
  for i in range(n_updates):
      # Calculate the slope: slope
      slope = get_slope(input_data, target, weights)
      
      # Update the weights: weights
      weights = weights - 0.01 * slope
      
      # Calculate mse with new weights: mse
      mse = get_mse(input_data, target, weights)
      
      # Append the mse to mse_hist
      mse_hist.append(mse)
  
  # Plot the mse history
  plt.plot(mse_hist)
  plt.xlabel('Iterations')
  plt.ylabel('Mean Squared Error')
  plt.show()
  