Friday, January 15, 2021

Learn From Scratch Neural Networks Using PyQt: Part 1

This content is powered by Balige Publishing. Visit this link (collaboration with Rismon Hasiholan Sianipar)

In this tutorial, you will learn how to use Pandas, NumPy and other libraries to perform simple classification using perceptron and Adaline (adaptive linear neuron). The dataset used is Iris dataset directly from the UCI Machine Learning Repository.

Step 1: Open Qt Designer. Create the form using the Main Window template, as shown in Figure below.


Step 2: Click the Create button. Then, place a Push Button widget on the form. Specify the text property of the Push Button widget in the Property Editor window to Load Data. Then, set the property of the objectName of the Push Button widget in the Property Editor window to pbLoad.

Step 3: Place a Widget from the Containers panel on the form. Set the objectName property to widgetData.

Step 4: Save the form as gui_perceptron.ui. Now, the form looks as shown in Figure below.


Step 5: Next, right-click on the Widget and from the context menu displayed select Promote to .... Set the Promoted class name as graphics_perceptron as shown in Figure below.


Step 6: Then, click the Add button and click the Promote button. In the Object Inspector window, you can see that widgetData (graphics_perceptron class) along with the pbLoad object (QPushButton class) are now in the centralwidget object (QWidget class) as shown in Figure below.


Step 7: Define the graphics_perceptron class (according to the class name from widgetData) in a Python file. Save it as graphics_perceptron.py as follows:

#graphics_perceptron.py

from PyQt5.QtWidgets import*
from matplotlib.backends.backend_qt5agg import FigureCanvas
from matplotlib.figure import Figure
    
class graphics_perceptron(QWidget):    
    def __init__(self, parent = None):
        QWidget.__init__(self, parent)        
        self.canvas = FigureCanvas(Figure())
       
        vertical_layout = QVBoxLayout()
        vertical_layout.addWidget(self.canvas)
        
        self.canvas.axis1 = self.canvas.figure.add_subplot(111)
        self.canvas.figure.set_facecolor("xkcd:beige")
        self.setLayout(vertical_layout)
 

Step 8: Create a new class, Perceptron, save it as Perceptron_Class.py as follows:

#Perceptron_Class.py
import numpy as np

class Perceptron(object):
    """Perceptron classifier.
    Parameters
    ------------
    eta : float
        Learning rate (between 0.0 and 1.0)
    n_iter : int
        Passes over the training dataset.
    random_state : int
        Random number generator seed for random weight
        initialization.
    
    Attributes
    -----------
    w_ : 1d-array
        Weights after training.
    errors_ : list
        Number of misclassifications (updates) in each epoch.
    """
    
    def __init__(self, eta=0.01, n_iter=50, random_state=1):
        self.eta = eta
        self.n_iter = n_iter
        self.random_state = random_state
    
    def train(self, X, y):
        """Trains the perceptron.
        Parameters
        ----------
        X : {array-like}, shape = [n_samples, n_features]
            Training vectors, where n_samples is the number of
            samples and
            n_features is the number of features.
        y : array-like, shape = [n_samples]
            Target values.
        Returns
        -------
        self : object
        
        """
        rgen = np.random.RandomState(self.random_state)
        self.w_ = rgen.normal(loc=0.0, scale=0.01,
                              size=1 + X.shape[1])
        self.errors_ = []
        
        for _ in range(self.n_iter):
            errors = 0
            for xi, target in zip(X, y):
                update = self.eta * (target - self.process(xi))
                self.w_[1:] += update * xi
                self.w_[0] += update
                errors += int(update != 0.0)
            self.errors_.append(errors)
        return self
    
    def NN_input(self, X):
        #Calculate net input
        return np.dot(X, self.w_[1:]) + self.w_[0]

    def process(self, X):
        #passing training data to net to get output   
        #Return class label after unit step"""
        return np.where(self.NN_input(X) >= 0.0, 1, -1)

Step 9: Define the new Python script, Perceptron.py, defines the method display_data() and connect it with the clicked() event from pbData widget as follows:

#Perceptron.py
from PyQt5.QtWidgets import *
from PyQt5.uic import loadUi
from matplotlib.backends.backend_qt5agg import (NavigationToolbar2QT as NavigationToolbar)
from matplotlib.colors import ListedColormap
from Perceptron_Class import Perceptron
import numpy as np
import pandas as pd 

class DemoGUIPerceptron(QMainWindow):   
    def __init__(self):        
        QMainWindow.__init__(self)
        loadUi("gui_perceptron.ui",self)

        self.setWindowTitle("GUI Demo of Perceptron")
        self.pbLoad.clicked.connect(self.display_data)
        self.addToolBar(NavigationToolbar(self.widgetData.canvas, self))

    def load_data(self):
        df = pd.read_csv('https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data',header=None)
        
        # select setosa and versicolor
        y = df.iloc[0:100, 4].values
        self.y = np.where(y == 'Iris-setosa', -1, 1)

        # extract sepal length and petal length
        self.X = df.iloc[0:100, [0, 2]].values

    def display_data(self): 
        self.load_data()
        
        # plot data
        self.widgetData.canvas.axis1.scatter(self.X[:50, 0], \
            self.X[:50, 1],
            color='red', marker='o', label='setosa')
        self.widgetData.canvas.axis1.scatter(self.X[50:100, 0], \
            self.X[50:100, 1],
            color='blue', marker='x', label='versicolor')
        self.widgetData.canvas.axis1.set_xlabel('sepal length [cm]')
        self.widgetData.canvas.axis1.set_ylabel('petal length [cm]')
        self.widgetData.canvas.axis1.legend(loc='upper left')
        self.widgetData.canvas.draw()
               
if __name__ == '__main__':
    import sys
    app = QApplication(sys.argv)
    ex = DemoGUIPerceptron()
    ex.show()
    sys.exit(app.exec_())

Step 10: Run Perceptron.py script and click the Load Data button. The scatter plot of extracted data will be displayed as shown in Figure below.


Step 11: On gui_perceptron.ui form, add a Spin Box widget and set its minimum, maximum, singleStep, and value properties to 10, 100, 2, and 100. Set its objectName property to sbLength.

Step 12: Then, add a Table Widget onto form and set its objectName property as tableData. Now the form looks as shown in Figure below.


Step 13: Modify load_data() and display_data() functions so that the length of data extracted is determined by value property of sbLength widget as follows:

def load_data(self):
    df = pd.read_csv('https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data',header=None)
        
    self.dataLength = self.sbLength.value()
        
    # select setosa and versicolor
    y = df.iloc[0:self.dataLength, 4].values
    self.y = np.where(y == 'Iris-setosa', -1, 1)

    # extract sepal length and petal length
    self.X = df.iloc[0:self.dataLength, [0, 2]].values        
        
def display_data(self): 
    self.load_data()        
    dataHalf = int(self.dataLength/2)

    # plot data
    self.widgetData.canvas.axis1.clear()
    self.widgetData.canvas.axis1.scatter(self.X[:dataHalf, 0], \
        self.X[:dataHalf, 1],
        color='red', marker='o', label='setosa')
    self.widgetData.canvas.axis1.scatter(\
        self.X[dataHalf:self.dataLength, 0], \
        self.X[dataHalf:self.dataLength, 1],
        color='blue', marker='x', label='versicolor')
    self.widgetData.canvas.axis1.set_xlabel('sepal length [cm]')
    self.widgetData.canvas.axis1.set_ylabel('petal length [cm]')
    self.widgetData.canvas.axis1.legend(loc='upper left')
    self.widgetData.canvas.draw()

Step 14: Connect valueChanged() signal of sbLength with display_data() function and add it inside __init__() function:

self.sbLength.valueChanged.connect(self.display_data)

Step 15: Define write_df_to_qtable() and display_table() functions to read data frame and display it on table widget.

def display_table(self,df):
    # show data on table widget
    self.write_df_to_qtable(df,self.tableData)
    self.tableData.setHorizontalHeaderLabels(['0',\
        '1', '2', '3', 'Name'])
    self.tableData.setColumnWidth(0, 40)
    self.tableData.setColumnWidth(1, 40)
    self.tableData.setColumnWidth(2, 40)
    self.tableData.setColumnWidth(3, 40)
        
    styleH = "::section {""background-color: red; }"
    self.tableData.horizontalHeader().setStyleSheet(styleH)

    styleV = "::section {""background-color: yellow; }"
    self.tableData.verticalHeader().setStyleSheet(styleV)        

        
# Takes a df and writes it to a qtable provided. df headers become qtable headers
@staticmethod
def write_df_to_qtable(df,table):
    table.setRowCount(df.shape[0])
    table.setColumnCount(df.shape[1])       

    # getting data from df is computationally costly so convert it to array first
    df_array = df.values
    for row in range(df.shape[0]):
        for col in range(df.shape[1]):
            table.setItem(row, col, \
                QTableWidgetItem(str(df_array[row,col])))

Step 16: Invoke display_table() from load_data() function as shown in line 14:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
def load_data(self):
    df = pd.read_csv('https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data',header=None)
        
    self.dataLength = self.sbLength.value()
        
    # select setosa and versicolor
    y = df.iloc[0:self.dataLength, 4].values
    self.y = np.where(y == 'Iris-setosa', -1, 1)

    # extract sepal length and petal length
    self.X = df.iloc[0:self.dataLength, [0, 2]].values   
     
    #display data on table
    self.display_table(df)

Step 17: Run Perceptron.py to see the scatter plot of extracted data and its representation of table as shown in Figure below.


Step 18: Next, add two Widgets onto form and set their objectName properties as widgetDecision and widgetEpoch. Now, the form looks as shown in Figure below.


Step 19: Promote widgetDecision and widgetEpoch to graphics_perceptron class by doing as shown in Step 5 and Step 6. 

In the Object Inspector window, you can see that widgetData, widgetDecision and widgetEpoch (graphics_perceptron class) along with the pbLoad object (QPushButton class) are now in the centralwidget object (QWidget class) as shown in Figure below.


Step 20: Define display_epoch() and display_decision() functions to display both error in every epoch (iteration) in widgetEpoch  and the decision regions in widgetDecision as follows:

def display_epoch(self,ppn):   
    ppn.train(self.X, self.y)
    self.widgetEpoch.canvas.axis1.clear()
    self.widgetEpoch.canvas.axis1.plot(range(1, \
        len(ppn.errors_) + 1),ppn.errors_, marker='o')
    self.widgetEpoch.canvas.axis1.set_xlabel('Epochs')
    self.widgetEpoch.canvas.axis1.set_ylabel('Number of updates')
    self.widgetEpoch.canvas.draw()
 
def display_decision(self,ppn):     
    self.widgetDecision.canvas.axis1.clear()
    # setup marker generator and color map
    markers = ('s', 'x', 'o', '^', 'v')
    colors = ('red', 'blue', 'lightgreen', 'gray', 'cyan')
    cmap = ListedColormap(colors[:len(np.unique(self.y))])
        
    # plot the decision surface
    x1_min, x1_max = self.X[:, 0].min() - 1, self.X[:, 0].max() + 1
    x2_min, x2_max = self.X[:, 1].min() - 1, self.X[:, 1].max() + 1
    xx1, xx2 = np.meshgrid(np.arange(x1_min, x1_max, 0.01),
                           np.arange(x2_min, x2_max, 0.01))
    Z = ppn.process(np.array([xx1.ravel(), xx2.ravel()]).T)
    Z = Z.reshape(xx1.shape)
    self.widgetDecision.canvas.axis1.contourf(xx1, xx2, Z, \
        alpha=0.5, cmap=cmap)
    self.widgetDecision.canvas.axis1.set_xlim(xx1.min(), xx1.max())
    self.widgetDecision.canvas.axis1.set_ylim(xx2.min(), xx2.max())
        
    # plot class samples
    for idx, cl in enumerate(np.unique(self.y)):
        self.widgetDecision.canvas.axis1.scatter(\
            x=self.X[self.y == cl, 0],
            y=self.X[self.y == cl, 1],
            alpha=0.8,
            c=colors[idx],
            marker=markers[idx],
            label=cl,
            edgecolor='black')
        
    self.widgetDecision.canvas.axis1.set_xlabel('sepal length [cm]')
    self.widgetDecision.canvas.axis1.set_ylabel('petal length [cm]')
    self.widgetDecision.canvas.axis1.legend(loc='upper left')
    self.widgetDecision.canvas.draw()

Step 21: Invoke display_epoch() and display_decision() functions in load_data() as shown in line:

def load_data(self):
    df = pd.read_csv('https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data',header=None)
        
    self.dataLength = self.sbLength.value()
        
    # select setosa and versicolor
    y = df.iloc[0:self.dataLength, 4].values
    self.y = np.where(y == 'Iris-setosa', -1, 1)

    # extract sepal length and petal length
    self.X = df.iloc[0:self.dataLength, [0, 2]].values        
        
    #display data on table
    self.display_table(df)
        
    #display errors
    ppn = Perceptron(eta=0.1, n_iter=10)
    self.display_epoch(ppn)
        
    #display decision
    self.display_decision(ppn)

Step 22: Run Perceptron.py and click on Load Data button. You will see error in every iteration graph and decision regions as shown in Figure below.


Step 23: Change data length and you will get the result as shown in Figure below.


Below is the full script of Perceptron.py:

#Perceptron.py
from PyQt5.QtWidgets import *
from PyQt5.uic import loadUi
from matplotlib.backends.backend_qt5agg import (NavigationToolbar2QT as NavigationToolbar)
from matplotlib.colors import ListedColormap
from Perceptron_Class import Perceptron
import numpy as np
import pandas as pd 

class DemoGUIPerceptron(QMainWindow):   
    def __init__(self):       
        QMainWindow.__init__(self)
        loadUi("gui_perceptron.ui",self)

        self.setWindowTitle("GUI Demo of Perceptron")
        self.pbLoad.clicked.connect(self.display_data)
        self.addToolBar(NavigationToolbar(self.widgetData.canvas, self))
        self.sbLength.valueChanged.connect(self.display_data)       

    def load_data(self):
        df = pd.read_csv('https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data',header=None)
        
        self.dataLength = self.sbLength.value()
        
        # select setosa and versicolor
        y = df.iloc[0:self.dataLength, 4].values
        self.y = np.where(y == 'Iris-setosa', -1, 1)

        # extract sepal length and petal length
        self.X = df.iloc[0:self.dataLength, [0, 2]].values        
        
        #display data on table
        self.display_table(df)
        
        #display errors
        ppn = Perceptron(eta=0.1, n_iter=10)
        self.display_epoch(ppn)
        
        #display decision
        self.display_decision(ppn)
        
    def display_data(self): 
        self.load_data()        
        dataHalf = int(self.dataLength/2)

        # plot data
        self.widgetData.canvas.axis1.clear()
        self.widgetData.canvas.axis1.scatter(self.X[:dataHalf, 0],\
            self.X[:dataHalf, 1],
            color='red', marker='o', label='setosa')
        self.widgetData.canvas.axis1.scatter(\
            self.X[dataHalf:self.dataLength, 0], \
            self.X[dataHalf:self.dataLength, 1],
            color='blue', marker='x', label='versicolor')
        self.widgetData.canvas.axis1.set_xlabel('sepal length [cm]')
        self.widgetData.canvas.axis1.set_ylabel('petal length [cm]')
        self.widgetData.canvas.axis1.legend(loc='upper left')
        self.widgetData.canvas.draw()

    def display_table(self,df):
        # show data on table widget
        self.write_df_to_qtable(df,self.tableData)
        self.tableData.setHorizontalHeaderLabels(['0', '1', '2', '3', 'Name'])
        self.tableData.setColumnWidth(0, 40)
        self.tableData.setColumnWidth(1, 40)
        self.tableData.setColumnWidth(2, 40)
        self.tableData.setColumnWidth(3, 40)
        
        styleH = "::section {""background-color: red; }"
        self.tableData.horizontalHeader().setStyleSheet(styleH)

        styleV = "::section {""background-color: yellow; }"
        self.tableData.verticalHeader().setStyleSheet(styleV)        

        
    # Takes a df and writes it to a qtable provided. df headers become qtable headers
    @staticmethod
    def write_df_to_qtable(df,table):
        table.setRowCount(df.shape[0])
        table.setColumnCount(df.shape[1])       

        # getting data from df is computationally costly so convert it to array first
        df_array = df.values
        for row in range(df.shape[0]):
            for col in range(df.shape[1]):
                table.setItem(row, col, \
                    QTableWidgetItem(str(df_array[row,col])))

    def display_epoch(self,ppn):   
        ppn.train(self.X, self.y)
        self.widgetEpoch.canvas.axis1.clear()
        self.widgetEpoch.canvas.axis1.plot(range(1, \
            len(ppn.errors_) + 1),ppn.errors_, marker='o')
        self.widgetEpoch.canvas.axis1.set_xlabel('Epochs')
        self.widgetEpoch.canvas.axis1.set_ylabel('Number of updates')
        self.widgetEpoch.canvas.draw()
 
    def display_decision(self,ppn):     
        self.widgetDecision.canvas.axis1.clear()
        # setup marker generator and color map
        markers = ('s', 'x', 'o', '^', 'v')
        colors = ('red', 'blue', 'lightgreen', 'gray', 'cyan')
        cmap = ListedColormap(colors[:len(np.unique(self.y))])
        
        # plot the decision surface
        x1_min, x1_max = self.X[:, 0].min() - 1, self.X[:, 0].max() + 1
        x2_min, x2_max = self.X[:, 1].min() - 1, self.X[:, 1].max() + 1
        xx1, xx2 = np.meshgrid(np.arange(x1_min, x1_max, 0.01),
                           np.arange(x2_min, x2_max, 0.01))
        Z = ppn.process(np.array([xx1.ravel(), xx2.ravel()]).T)
        Z = Z.reshape(xx1.shape)
        self.widgetDecision.canvas.axis1.contourf(xx1, xx2, Z, \
            alpha=0.5, cmap=cmap)
        self.widgetDecision.canvas.axis1.set_xlim(xx1.min(), xx1.max())
        self.widgetDecision.canvas.axis1.set_ylim(xx2.min(), xx2.max())
        
        # plot class samples
        for idx, cl in enumerate(np.unique(self.y)):
            self.widgetDecision.canvas.axis1.scatter(x=self.X[self.y == cl, 0],
                    y=self.X[self.y == cl, 1],
                    alpha=0.8,
                    c=colors[idx],
                    marker=markers[idx],
                    label=cl,
                    edgecolor='black')
        
        self.widgetDecision.canvas.axis1.set_xlabel('sepal length [cm]')
        self.widgetDecision.canvas.axis1.set_ylabel('petal length [cm]')
        self.widgetDecision.canvas.axis1.legend(loc='upper left')
        self.widgetDecision.canvas.draw()

if __name__ == '__main__':
    import sys
    app = QApplication(sys.argv)
    ex = DemoGUIPerceptron()
    ex.show()
    sys.exit(app.exec_())

Learn From Scratch Neural Networks Using PyQt: Part 2

No comments:

Post a Comment