Softmax Gradients for ANN
It is common to see Artificial Neural Networks with a sofmax function in the last layer. This activation function outputs a probability distribution among the different classes that the networks is trained to predict on. Softmax, for the case of a single class looks like this: $$ \pi_{\theta}(a | s) = \frac{e^{z_{a}}}{\sum_{k}e^{z_{k}}} $$ In order to train a NN with softmax as its last activation function, the cost function known as the cross-entropy error function has to be minimized.
Cross-entropy derivatives
Let \( \mathbf{y} \) denote the output of a neural network with a softmax function as the activation of its last layer and \( \mathbf{t} \) a 'one-hot encoded' vector. Then, the error function is: $$ E = -\sum_{i} t_{i} \log{ y_{i} } $$ By estimating the gradient of the error function with respect to the parameters of a neural network, and updating these parameters with these gradients, the network can learn.

In order to estimate the gradient of the error function with respect to the parameters of the last layer of a network, \( L \) will denote the number of layers, \( \mathbf{h}^{(L)} \) the output of the previous layer and \( \mathbf{z}^{(L)} \) the result of the multiplication of \( \mathbf{h}^{(L)} \) by the weight matrix \( \mathbf{w}^{(L)} \). $$ y_{i} = \frac{e^{z_{i}^{(L)}}}{\sum_{k} e^{z_{k}^{(L)}}} \\ \\ \\ \mathbf{z}^{(L)} = \mathbf{w}^{(L)} \mathbf{h}^{(L)} \quad \small\text{where} \quad \mathbf{w}^{(L)} \in \mathbb{R}^{i\times j} $$
Therefore, for any given \( w_{ij}^{(L)} \in \mathbf{ w }^{(L)} \),
$$ \begin{align*} \frac{\partial{E}}{\partial{w_{ij}^{(L)}}} &= \sum_{i} \frac{\partial{E}}{\partial{y_{i}}} \Bigg( \sum_{k} \frac{\partial{y_{i}}}{\partial{z_{k}^{(L)}}} \frac{\partial{z_{k}^{(L)}}}{\partial{w_{ij}^{(L)}}} \Bigg) \\ &= \sum_{i} \frac{ \partial{E} }{ \partial{y_{i}} } \frac{ \partial{y_{i}} }{ \partial{z_{j}^{(L)}} } \frac{ \partial{z_{j}^{(L)}} }{ \partial{w_{ij}^{(L)}} } \quad \tiny\texttt{True since $ \frac{\partial{z_{k}^{(L)}}}{\partial{w_{ij}^{(L)}}}=0 \ \ \forall \ \ k \neq j$.} \\ &= h_{i}^{(L)} (y_{j}\sum_{i}t_{i} - t_{j}) \\ &= h_{i}^{(L)}(y_{j} - t_{j}) \quad \tiny\texttt{True since $\sum_{i}t_{i} = 1$, $\mathbf{t}$ is a 'one-hot' encoded vector.} \end{align*} $$
In vectorized notation, $$ \begin{align*} \nabla_{\mathbf{w}^{(L)}}E &= \mathbf{h}^{\intercal(L)}(\mathbf{y}-\mathbf{t}) \\ \\ &= \begin{bmatrix} h_{1}^{(L)}(y_{1} - t_{1}) & h_{1}^{(L)}(y_{2} - t_{2}) & \dots \\ \vdots & \ddots & \\ h_{i}^{(L)}(y_{1} - t_{1}) & \dots & h_{i}^{(L)}(y_{j} - t_{j}) \end{bmatrix} \end{align*} $$
And the derivative with respect to a previous weight matrix can be obtained as follows. $$ \begin{align*} \frac{\partial{E}}{\partial{w_{pq}^{(L-1)}}} &= \sum_{i} \frac{\partial{E}}{\partial{y_{i}}} \Bigg( \sum_{k} \frac{\partial{y_{i}}}{\partial{z_{k}^{(L)}}} \frac{\partial{z_{k}^{(L)}}}{\partial{h_{q}^{(L)}}} \frac{\partial{h_{q}^{(L)}}}{\partial{z_{q}^{(L-1)}}} \frac{\partial{z_{q}^{(L-1)}}}{\partial{w_{pq}^{(L-1)}}}\Bigg) \\ \\&= x_{p}h_{q}^{(L)}(1-h_{q}^{(L)})\sum_{q}w_{pq}^{(L)}(y_{q}-t_{q}) \end{align*} $$
Expressed as a a matrix, where \( h_{q}^{\prime(L)} = h_{q}^{(L)}(1 - h_{q}^{(L)}) \) and \( \delta_{q} = y_{q} - t_{q} \),
$$ \begin{align*} \nabla_{\mathbf{w}^{(L-1)}}E &= \mathbf{x}^{\intercal} \Big( \mathbf{h}^{(L)} \circ (1 - \mathbf{h}^{(L)}) \circ \big[ (\mathbf{y} - \mathbf{t})\mathbf{w}^{\intercal(L)} \big] \Big) \\ \\ &= \begin{bmatrix} x_{1}h_{1}^{\prime(L)} \sum_{q}w_{1q}^{(L)}\delta_{q} & x_{1}h_{2}^{\prime(L)}\sum_{q}w_{1q}^{(L)}\delta_{q} & \dots \\ \vdots & \ddots & \\ x_{p}h_{1}^{\prime(L)}\sum_{q}w_{pq}^{(L)}\delta_{q} & \dots & x_{p}h_{q}^{\prime(L)}\sum_{q}w_{pq}^{(L)}\delta_{q} \end{bmatrix} \end{align*} $$
Toy code
The following code aids to build a NN for classification almost from scratch.
  
from __future__ import division
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.metrics import f1_score, precision_score, recall_score


# GET DATA
X = pd.read_csv("https://archive.ics.uci.edu/ml/machine-learning-databases/iris/bezdekIris.data")

# PRE-PROCESSING
X = X.values

X = pd.DataFrame({ 'sepal_length':X[:,0], 'sepal_width':X[:,1], 'petal_length':X[:,2], 'petal_width':X[:,3], 'class':X[:,4] })

T = pd.get_dummies(X["class"]).values
X = X[["sepal_length","sepal_width","petal_length","petal_width"]].values.astype("float")

hidden_size = 40

# FEATURE NORMALIZATION
for i in range(X.shape[1]):
  X[:,i] = ( X[:,i] - np.mean(X[:,i]) ) / np.std(X[:,i])

# TRAIN-TEST SPLIT
x_train, x_test = train_test_split(X,test_size=0.2)
t_train, t_test = train_test_split(T,test_size=0.2)


# NEURAL NETWORK WEIGHTS (XAVIER INITIALIZATION)
W = {}
W['1'] = np.random.rand(X.shape[1],hidden_size) / np.sqrt(X.shape[1])
W['2'] = np.random.rand(hidden_size,3) / np.sqrt(hidden_size)


def sigmoid(x):
  return 1.0 / (1.0 + np.exp(-x))


#def softmax(x):
  #return np.exp(x) / np.sum(np.exp(x))

def softmax(w):
    a = np.max(w)
    e = np.exp(np.array(w) - a)
    dist = e / np.sum(e)
    return dist


def feedforward(X,W):
  z1 = np.dot(X,W['1'])
#  h1 = sigmoid(z1)
  h1 = z1
  h1[h1<0] = 0
  z2 = np.dot(h1,W['2'])
  y = np.array(map(softmax,z2)) if len(z2.shape) != 1 else softmax(z2)
  return y,h1


def backprop(h,d,X,W):
  dW2 = np.dot(h.T,d)
#  dW1 = np.dot( X.T,np.multiply( h*(1-h) , np.dot(d,W["2"].T) ) )
  dW1 = np.dot( X.T,np.multiply( h , np.dot(d,W["2"].T) ) )
  return { '1':dW1, '2':dW2 }


def loss(y,t):
   logProb = np.log(y)
   tagProd = t * logProb
   sums = map(lambda x:np.sum(x),tagProd) if len(tagProd.shape) != 1 else sum(tagProd)
   loss = -1 * np.mean(sums)
   return loss


def gradientDescent(alpha=0.01,iters=100000):
  history = []
  for i in range(iters):
    y,h = feedforward(x_train,W)
    dW = backprop(h,(y - t_train),x_train,W)
    W['2'] -= alpha * dW['2']
    W['1'] -= alpha * dW['1']
    if( i % 10 == 0 ):
      print np.mean(f1_score(t_test,np.around(predict(x_test,W)),average=None))
      history.append(loss(y,t_train))
  return history


def sgd(alpha=0.001,iters=10):
  history = []
  sgd_data = np.column_stack((x_train,t_train))
  np.random.shuffle(sgd_data)
  for i in xrange(iters):
   for d in sgd_data:
     X_l = x_train.shape[1]
     T_l = X_l + t_train.shape[1]
     X_ = d[0:X_l]
     T_ = d[X_l:T_l]
     y_,h_ = feedforward(X_,W)
     dif = (y_ - T_)
     dif = dif.reshape(1,dif.shape[0])
     h_ = h_.reshape(1,h_.shape[0])
     X_ = X_.reshape(1,X_.shape[0])
     dW = backprop(h_,dif,X_,W)
     W['2'] -= alpha * dW['2']
     W['1'] -= alpha * dW['1']
     history.append(loss(y_,T_))
  return history


def sgd_(alpha=0.001,iters=10):
  history = []
  sgd_data = np.column_stack((x_train,t_train))
  for i in xrange(iters):
     batch = sgd_data[np.random.randint(sgd_data.shape[0],size=20),:]
     X_l = x_train.shape[1]
     T_l = X_l + t_train.shape[1]
     X_ = d[0:X_l]
     T_ = d[X_l:T_l]
     y_,h_ = feedforward(X_,W)
     dif = (y_ - T_)
     dif = dif.reshape(1,dif.shape[0])
     h_ = h_.reshape(1,h_.shape[0])
     X_ = X_.reshape(1,X_.shape[0])
     dW = backprop(h_,dif,X_,W)
     W['2'] -= alpha * dW['2']
     W['1'] -= alpha * dW['1']
     history.append(loss(y_,T_))
  return history


def predict(x,w):
#  return np.around(feedforward(x,w)[0]).astype(int)
  return feedforward(x,w)[0]