B = tf.placeholder(tf.float32)
sq_deltas = tf.square(y - B)
loss_function = tf.reduce_sum(sq_deltas)

print(sess.run(loss_function, {x : [1,2,3,4], B : [0,-5,-4,-3]}))

"""
Explanation: Loss function is created and used for evaluation.
End of explanation
"""


fixM = tf.assign(M,-1.)
fixC = tf.assign(C, 1)
sess.run([fixM,fixC])

print(sess.run(loss_function, {x : [1,2,3,4], B : [0,-5,-4,-3]}))

"""
Explanation: That is totally not okay as is nearly 80 and its too high. But if we want to lower our loss_function, we need to fix the input values.
End of explanation
"""


optimizer = tf.train.GradientDescentOptimizer(0.01)
train = optimizer.minimize(loss_function)

sess.run(init) # reset values to incorrect defaults.
for i in range(1000):
  sess.run(train, {x: [1, 2, 3, 4], B: [0,-5,-4,-3]})

print(sess.run([M, C]))

"""
Explanation: See, the loss's function is lower than what we got before.
End of explanation
"""


import tensorflow as tf

# Model parameters
M = tf.Variable([.6], dtype=tf.float32)
C = tf.Variable([-.6], dtype=tf.float32)
# Model input and output
x = tf.placeholder(tf.float32)
y = M * x + C

B = tf.placeholder(tf.float32)

# loss_function
loss_function = tf.reduce_sum(tf.square(y - B)) # sum of the squares

# optimizer
optimizer = tf.train.GradientDescentOptimizer(0.01)
train = optimizer.minimize(loss_function)

# training data
x_train = [1, 2, 3, 4]
y_train = [0,-5,-4,-3]



# training loop
init = tf.global_variables_initializer()
sess = tf.Session()
sess.run(init) # reset values to wrong
for i in range(1000):
  sess.run(train, {x: x_train, B : [0,-5,-4,-3]})



# evaluate training accuracy
curr_M, curr_C, curr_loss_f = sess.run([M, C, loss_function], {x: x_train, B: y_train})
print("W: %s b: %s loss: %s"%(curr_M, curr_C, curr_loss_f))

import tensorflow as tf
# NumPy is often used to load, manipulate and preprocess data.
import numpy as np

# Declare list of features. We only have one numeric feature. There are many
# other types of columns that are more complicated and useful.
feature_cols = [tf.feature_column.numeric_column("a", shape=[1])]

# An estimator is the front end to invoke training (fitting) and evaluation
# (inference). There are many predefined types like linear regression,
# linear classification, and many neural network classifiers and regressors.
# The following code provides an estimator that does linear regression.
estimator = tf.estimator.LinearRegressor(feature_columns=feature_cols)



# TensorFlow provides many helper methods to read and set up data sets.
# Here we use two data sets: one for training and one for evaluation
# We have to tell the function how many batches
# of data (num_epochs) we want and how big each batch should be.
a_train = np.array([0., 5., 3., 4.])
y_train = np.array([0., -6., -7., -3.])
a_eval = np.array([2., 5., 8., 10.])
y_eval = np.array([-1.01, -6.1, -7., 4.])
input_fun = tf.estimator.inputs.numpy_input_fn(
    {"a": a_train}, y_train, batch_size=4, num_epochs=None, shuffle=True)
train_input_fun = tf.estimator.inputs.numpy_input_fn(
    {"a": a_train}, y_train, batch_size=4, num_epochs=1000, shuffle=False)
eval_input_fun = tf.estimator.inputs.numpy_input_fn(
    {"a": a_eval}, y_eval, batch_size=4, num_epochs=1000, shuffle=False)

# We can invoke 1000 training steps by invoking the method and passing the
# training data set.
estimator.train(input_fn=input_fun, steps=1000)



# Here we evaluate how well our model did.
train_metrics = estimator.evaluate(input_fn=train_input_fun)
eval_metrics = estimator.evaluate(input_fn=eval_input_fun)
print("train metrics: %r"% train_metrics)
print("eval metrics: %r"% eval_metrics)

"""
Explanation: We werent even close to guessing these values, but we got the exact and accurate values of the paramters defined earlier through proper training of the model. 
Lets sum up the steps of the linear model.
End of explanation
"""


