Simple classifier on PyBrain and PyQt4 (Python3)

    Studying Python3, I ported (as I could) the PyBrain library. I already wrote about this here .
    image
    Now I want to play a little with this library. As I said in a previous post, I just started to study python, so everything written in this article should not be taken as Truth. Learning is the way, and he is meandering.

    The task will be set before the artificial neural network (ANN) is very simple - classification, namely: recognition of letters of the Latin alphabet.

    It seems to be a classic example, they have already written about him on the hub several times: "What are artificial neural networks?" , "Neural networks and character recognition" , etc.
    But my goal is to study python using not the simplest examples. Those. learn immediately on the complex and unfamiliar. So we will find twice as many rakes, which will allow us to dig into the depths of the tongue, understanding "why it does not work?".

    Waiting for you under the habracat: a description of how to prepare data on PyQt4 , using the argparse module , and of course PyBrain !


    Having read the articles here on the hub and not only, you understand that it is difficult not to write / create / design an ANN, but to prepare a set of training and test data for it. Therefore, our task is divided into two subtasks:
    • prepare data for training;
    • design and train ANN.

    We will do it in that order. So to speak on the rise.

    Data preparation


    Technical task

    Let's clarify the task: the size of the image with the image of the letter will be, say, 64 by 64 pixels (a total of 4096 inputs from the ANN).
    To do this, we need to write a generator of these pictures. And we will write it, naturally, in python.
    Training data will include:
    • lowercase Latin letters
    • Latin letters in uppercase
    • letters can be of different sizes (optional)
    • the style can be different (different fonts can be used)

    Based on this, we write a generator to which parameters are input:
    • list of letters e.g. abc or fx (range)
    • font size e.g. 40
    • font used
    • path to the folder where the generated images will be added


    Finding an image manipulation method

    To write a generator, we need information on image processing methods in python. Google did not help us much. He suggested using either the Python Imaging Library - abbreviated PIL , or PyGame .
    Only here is bad luck. The first is for python2 only, and the last release was in 2009. Although on github.com there is a fork for the third python . After reading the manual, I realized that not everything is so simple.
    PyGame is a more interesting option, even the manual read it longer. I realized that I needed to specifically understand the library, but something could not be done with a snap. And using a microscope for hammering nails is also not an option. Not for that this library is intended.
    Googled yet. There is also pythonmagick, but it is only for UNIX-like systems. And then it dawned on me! PyQt4 !
    I am quite familiar with Qt4, I wrote a lot in C ++ / Qt. Yes, this library is like a Swiss knife. If you want, it will open a bottle of beer, if you want, a beautiful figure will be cut out of a piece of wood. The main thing is to be able to use a knife. On Qt and stop.
    Searching for Habré gave us very little information on PyQt . Well, nothing - we’ll figure it out.

    Writing image generation and saving

    The first thing to do is install PyQt4. With this, I hope, the reader will cope - I will not dwell on this. I will pass immediately to use.
    We will import the necessary modules, and prepare the “fish” for the program in PyQt4.
    #!/usr/bin/env python3
    import sys
    from PyQt4.QtGui import *
    from PyQt4.Qt import *
    def main():
    	app = QApplication([])
    	# some code here
    if __name__ == "__main__":
        sys.exit(main())
    

    Line c is app = QApplication([])very important. Do not forget her. Without me, python crashes with SIGFAULT'om and does not give any warnings and errors.

    Now we are going to fill the “fish” with working logic. Add the save function, which will save the image with the given parameters.
    def save(png_file, letter = 'A', font = "Arial", size = 40, align = Qt.AlignCenter):
    	img = QImage(64,64, QImage.Format_RGB32)
    	img.fill(Qt.white)
    	p = QPainter(img)
    	p.setPen(Qt.black)
    	p.setFont(QFont(font,size))
    	p.drawText(img.rect(), align, letter)
    	p.end()
    	img.save(png_file)
    


    With function parameters, everything is clear. But I’ll explain the contents.
    First, an object of the QImage class is created, which allows you to create / process your images in your program. Very powerful and flexible tool. White color is filled for the whole image of 64 by 64 pixels.
    Then an object of type QPainter is created, to which the img link is passed. This class allows you to draw on the device context or, more precisely, on the canvas of any class inherited from QPaintDevice. And just such a class is QImage.
    Set the black pen, font and draw a letter. The default is 40 (which almost occupies the entire image field) and is centered.
    Well, then save the image to a file. Everything is simple and obvious.

    Finishing touches

    There is little left. Parsing command line options.
    This can be done head-on (with a bunch of if, or by hard-setting the input data format), or you can use all sorts of advanced modules like getopt or argparse . The latter, I think, we will study.
    Our input program will receive the following parameters: font, font size, and the directory where the finished pictures will be dumped.
    Alignment for now leave until better times.
    Reading this manual tells us that we just need to use this piece of code:
    	p = argparse.ArgumentParser(description='Symbols image generator')
    	p.add_argument('-f','--font', default='Arial', help='Font name, default=Arial')
    	p.add_argument('-s','--size', type=int, default=40, help='Font size, default=40')
    	p.add_argument('-d','--dir', default='.', help='Output directory, default=current')
    	p.add_argument('letters', help='Array of letters(abc) or range (a-z)')
    	args = p.parse_args()
    

    In this way, we describe our parameters, the argparse module takes care of the rest . What I liked was the automatic display of usage and the automatic generation of help with parameters. At what argparse added one more argument (-h) to our list. Thank you very much for that. As a real and lazy programmer, I really do not like to write help and other documentation. This is a point in favor of argparse . I will use it more often.
    Help for the program, we get this:
    usage: gen_pic.py [-h] [-f FONT] [-s SIZE] [-d DIR] letters
    Symbols image generator
    positional arguments:
      letters Array of letters (abc) or range (az)
    optional arguments:
      -h, --help show this help message and exit
      -f FONT, --font FONT Font name, default = Arial
      -s SIZE, --size SIZE Font size, default = 40
      -d DIR, --dir DIR Output directory, default = current
    

    Now add a check for the existence of the directory path and expanding the range of letters. We will use regular expressions for this. They are not particularly needed in this case, but you need to make the program more impressive! To do this, we need the os , os.path and re modules .
    	if os.path.exists(args.dir):
    		os.mkdir(args.dir)
    	if re.match('^([a-z]-[a-z])|([A-Z]-[A-Z])$', args.letters):
    		begin = args.letters[0]
    		end = args.letters[2]
    		if (ord(end)-ord(begin))>26:
    			print("Error using letters. Only A-Z or a-z available, not A-z.")
    			p.print_help()
    			return
    		letters = [chr(a) for a in range(ord(begin),ord(end)+1)]
    	else:
    		letters = args.letters
    

    Here you go. It remains to organize a cycle and transfer all the letters in turn for drawing.

    The final touch is to make the binding on top of the save () function and name it saveWrap (). How original, isn't it? Actually, it does not do anything supernatural, it simply generates a name for the file based on the parameters passed to the save () function.

    In total, the entire generator took us only 55 lines (the code is given at the end of the article). Isn't that great?
    And I’m sure that the python guru will surely find a bunch of optimization opportunities. But why? Everything works, the code is quite simple and concise. Straight eye rejoices.

    ANN development



    Now let's start work on the ANN. To get started, get acquainted with the capabilities of PyBrain .
    Lyrical digression about PyBrain and Python3
    I want to clarify that I tested the program only under Python3 and used the PyBrain port, which you can find here . While debugging the program, I found a couple of jambs in the very port of the library.
    I was very pleased with the comment in the place where the library fell out:
    # FIXME: the next line keeps arac from producing NaNs. I don't
    # know why that is, but somehow the __str__ method of the
    # ndarray class fixes something,
    # str (outerr)
    

    Apparently this hack in Python3 did not work.

    At the input we have an image (in gray tones) specified by the brightness values ​​of each pixel from 0 to 1.
    To begin with, we will make an ANN that will recognize a limited set of characters and without different registers. We will train on the data with one font and see how the network recognizes these characters with a different font (test set). Take, for example, symbols A, B, C, D, Z .

    Since our network will learn letters, the images of which are 64 by 64 pixels in size , the number of inputs of our network will be 4096 .
    We will only have 5 recognized letters, respectively, and the number of network outputs is five .
    Now the question is: do we need hidden layers? And if so, how much?
    I decided to do without hidden layers, so to create a network object I make the following call:
    net = buildNetwork(64 * 64, 5)
    

    To create one hidden layer with a size of 64 neurons and a type of hidden layer SoftmaxLayer, you need to make the following call:
    net = buildNetwork(64 * 64, 8 * 8, 5, hiddenclass=SoftmaxLayer)
    

    Unfortunately, the article said about this function, but no description was given. I will correct this defect.
    Likbez about buildNetwork ()
    The buildNetwork () function is designed to quickly create a FeedForward network and has the following format:
    pybrain.tools.shortcuts.buildNetwork(*layers, **options)
    

    layers - a list or tuple of integers that contains the number of neurons in each layer.
    Options are written in the form " name = val " and include:
    bias (default = True) - beginning offset in hidden layers
    outputbias (default = True) - starting offset
    hiddenclass and outclass in the output layer - set the types for hidden layers and the output layer, respectively. Must be a descendant of the NeuronLayer class. The predefined values ​​are GaussianLayer, LinearLayer, LSTMLayer, MDLSTMLayer, SigmoidLayer, SoftmaxLayer, TanhLayer.
    If the recurrent flag is set, the RecurrentNetwork will be created, otherwise FeedForwardNetwork.
    If the flag is setfast , then the faster arac networks will be used , otherwise it will be its own implementation of the PyBrain network in python.

    Those who are interested can choose other options, edit / uncommenting function call buildNetwork () in the file brain.py .

    ANN training


    So, it's time to take up the training. Using our program gen_pic.py we generate the necessary letters.
    I did it like this:
    ./gen_pic.py -d ./learn -f FreeMono ABCDZ
    ./gen_pic.py -d ./learn -f Times ABCDZ
    ./gen_pic.py -d ./learn -f Arial ABCDZ
    ./gen_pic.py -d ./test -f DroidMono ABCDZ
    ./gen_pic.py -d ./test -f Sans ABCDZ
    

    The process of loading data from a picture and converting RGB-color to gray tones, let leave the frame. There is nothing particularly interesting. Anyone who is still terribly interested in how this is done can see it in the brain.py file in the get_data () function .

    The training itself is done in the init_brain () function . The training sample, the maximum number of epochs for training, and optionally the Trainer type are passed to this function, and the function itself returns an object of the already trained network.
    Key lines of creating and training the network look like this (full code is given at the end of the article):
    def init_brain(learn_data, epochs, TrainerClass=BackpropTrainer):
    	...
        net = buildNetwork(64 * 64, 5, hiddenclass=LinearLayer)
        # fill dataset with learn data
        ds = ClassificationDataSet(4096, nb_classes=5, class_labels=['A', 'B', 'C', 'D', 'Z'])
        for inp, out in learn_data:
            ds.appendLinked(inp, [trans[out]])
        ...
        ds._convertToOneOfMany(bounds=[0, 1])
        ...
        trainer = TrainerClass(net, verbose=True)
        trainer.setData(ds)
        trainer.trainUntilConvergence(maxEpochs=epochs)
        return net
    

    Briefly explain where.
    ClassificationDataSet - a special type of dataset for classification purposes. The source data and the class serial number (trans [out]) are enough for him to compile a sample.
    The _convertToOneOfMany () function translates these same class numbers into values ​​of the output layer.
    Next, we transfer the network to the “teacher” and say that we are interested in displaying additional information (the library will print intermediate calculations to the console).
    We give the teacher a dataset with a training set (setData ()) and start training (trainUntilConvergence ()), which will train either until the network converges, or until the maximum number of training epochs has been reached.

    conclusions


    So, the goal is achieved.
    The code is written and works. The generator, however, can do much more than the network that we built today, in its current form. But then there is an unplowed field for you, dear% username%! There is something to fix, where to edit, what to rewrite ...

    I’ll also add that I tested two Trainers — BackpropTrainer and RPropMinusTrainer .
    The speed of the BackpropTrainer algorithm is poor, converges very slowly. Because of what, training takes a lot of time. By
    changing one line in brain.py you can look at the RPropMinusTrainer . It is much quicker and shows pretty good results.
    I’ll add thatI could not achieve 100% recognition even for the training set , maybe I had to select the number of layers and the number of neurons in each - I don’t know. There is no practical sense in this program, but the task is quite good for learning Python3: here you can work with lists and dictionaries, process command line parameters, work with images, regular expressions, work with the file system (os and os.path modules )

    For those who want to play, I will say only one thing - the brain.py program will require refinement if you want to change the number of letters or change them to others. Improvements are small and uncomplicated.

    If questions arise - write in a personal, but I think that you yourself will figure out what, where and how.
    There will be time, maybe I will rewrite the code more sympathetically and make it more customizable, I will introduce more parameters.

    You can take the source code in the spoilers below.
    Gen_pic.py file code
    #!/usr/bin/env python3
    import sys
    import argparse
    import re
    import os
    import os.path
    from PyQt4.QtGui import *
    from PyQt4.Qt import *
    def saveWrap(dir='.', letter='A', font="Arial", size=40, align=Qt.AlignCenter):
        png_file = dir + "/" + font + "_" + letter + "_" + str(size) + ".png"
        save(png_file, letter, font, size, align)
    def save(png_file, letter='A', font="Arial", size=40, align=Qt.AlignCenter):
        img = QImage(64, 64, QImage.Format_RGB32)
        img.fill(Qt.white)
        p = QPainter(img)
        p.setPen(Qt.black)
        p.setFont(QFont(font, size))
        p.drawText(img.rect(), align, letter)
        p.end()
        img.save(png_file)
    def main():
        app = QApplication([])
        p = argparse.ArgumentParser(description='Symbols image generator')
        p.add_argument('-f', '--font', default='Arial', help='Font name, default=Arial')
        p.add_argument('-s', '--size', type=int, default=40, help='Font size, default=40')
        p.add_argument('-d', '--dir', default='.', help='Output directory, default=current')
        p.add_argument('letters', help='Array of letters(abc) or range (a-z)')
        args = p.parse_args()
        path = os.path.abspath(args.dir)
        if not os.path.exists(path):
            print("Directory not exists, created!")
            os.makedirs(path)
        if re.match('^([a-z]-[a-z])|([A-Z]-[A-Z])$', args.letters):
            begin = args.letters[0]
            end = args.letters[2]
            if (ord(end) - ord(begin)) > 26:
                print("Error using letters. Only A-Z or a-z available, not A-z.")
                p.print_help()
                return
            letters = [chr(a) for a in range(ord(begin), ord(end) + 1)]
        else:
            letters = args.letters
        for lett in letters:
            saveWrap(path, lett, args.font, args.size)
        return 0
    if __name__ == "__main__":
        sys.exit(main())
    


    Brain.py file code
    #!/usr/bin/env python3
    import sys
    import argparse
    import re
    import os
    import os.path
    from PyQt4.QtGui import *
    from PyQt4.Qt import *
    from pybrain.tools.shortcuts import buildNetwork
    from pybrain.datasets import ClassificationDataSet
    from pybrain.structure.modules import SigmoidLayer, SoftmaxLayer, LinearLayer
    from pybrain.supervised.trainers import BackpropTrainer
    from pybrain.supervised.trainers import RPropMinusTrainer
    def init_brain(learn_data, epochs, TrainerClass=BackpropTrainer):
        if learn_data is None:
            return None
        print ("Building network")
        # net = buildNetwork(64 * 64, 8 * 8, 5, hiddenclass=TanhLayer)
        # net = buildNetwork(64 * 64, 32 * 32, 8 * 8, 5)
        net = buildNetwork(64 * 64, 5, hiddenclass=LinearLayer)
        # fill dataset with learn data
        trans = {
            'A': 0, 'B': 1, 'C': 2, 'D': 3, 'Z': 4
        }
        ds = ClassificationDataSet(4096, nb_classes=5, class_labels=['A', 'B', 'C', 'D', 'Z'])
        for inp, out in learn_data:
            ds.appendLinked(inp, [trans[out]])
        ds.calculateStatistics()
        print ("\tNumber of classes in dataset = {0}".format(ds.nClasses))
        print ("\tOutput in dataset is ", ds.getField('target').transpose())
        ds._convertToOneOfMany(bounds=[0, 1])
        print ("\tBut after convert output in dataset is \n", ds.getField('target'))
        trainer = TrainerClass(net, verbose=True)
        trainer.setData(ds)
        print("\tEverything is ready for learning.\nPlease wait, training in progress...")
        trainer.trainUntilConvergence(maxEpochs=epochs)
        print("\tOk. We have trained our network.")
        return net
    def loadData(dir_name):
        list_dir = os.listdir(dir_name)
        list_dir.sort()
        list_for_return = []
        print ("Loading data...")
        for filename in list_dir:
            out = [None, None]
            print("Working at {0}".format(dir_name + filename))
            print("\tTrying get letter name.")
            lett = re.search("\w+_(\w)_\d+\.png", dir_name + filename)
            if lett is None:
                print ("\tFilename not matches pattern.")
                continue
            else:
                print("\tFilename matches! Letter is '{0}'. Appending...".format(lett.group(1)))
                out[1] = lett.group(1)
            print("\tTrying get letter picture.")
            out[0] = get_data(dir_name + filename)
            print("\tChecking data size.")
            if len(out[0]) == 64 * 64:
                print("\tSize is ok.")
                list_for_return.append(out)
                print("\tInput data appended. All done!")
            else:
                print("\tData size is wrong. Skipping...")
        return list_for_return
    def get_data(png_file):
        img = QImage(64, 64, QImage.Format_RGB32)
        data = []
        if img.load(png_file):
            for x in range(64):
                for y in range(64):
                    data.append(qGray(img.pixel(x, y)) / 255.0)
        else:
            print ("img.load({0}) failed!".format(png_file))
        return data
    def work_brain(net, inputs):
        rez = net.activate(inputs)
        idx = 0
        data = rez[0]
        for i in range(1, len(rez)):
            if rez[i] > data:
                idx = i
                data = rez[i]
        return (idx, data, rez)
    def test_brain(net, test_data):
        for data, right_out in test_data:
            out, rez, output = work_brain(net, data)
            print ("For '{0}' our net said that it is '{1}'. Raw = {2}".format(right_out, "ABCDZ"[out], output))
        pass
    def main():
        app = QApplication([])
        p = argparse.ArgumentParser(description='PyBrain example')
        p.add_argument('-l', '--learn-data-dir', default="./learn", help="Path to dir, containing learn data")
        p.add_argument('-t', '--test-data-dir', default="./test", help="Path to dir, containing test data")
        p.add_argument('-e', '--epochs', default="1000", help="Number of epochs for teach, use 0 for learning until convergence")
        args = p.parse_args()
        learn_path = os.path.abspath(args.learn_data_dir) + "/"
        test_path = os.path.abspath(args.test_data_dir) + "/"
        if not os.path.exists(learn_path):
            print("Error: Learn directory not exists!")
            sys.exit(1)
        if not os.path.exists(test_path):
            print("Error: Test directory not exists!")
            sys.exit(1)
        learn_data = loadData(learn_path)
        test_data = loadData(test_path)
        # net = init_brain(learn_data, int(args.epochs), TrainerClass=RPropMinusTrainer)
        net = init_brain(learn_data, int(args.epochs), TrainerClass=BackpropTrainer)
        print ("Now we get working network. Let's try to use it on learn_data.")
        print("Here comes a tests on learn-data!")
        test_brain(net, learn_data)
        print("Here comes a tests on test-data!")
        test_brain(net, test_data)
        return 0
    if __name__ == "__main__":
        sys.exit(main())
    



    On this acquaintance with PyBrain today I think is over. See you soon!

    upd: at the request of monolithed fixed the regular expression.

    Only registered users can participate in the survey. Please come in.

    What is more interesting to you? What to focus on when writing the next article?

    • 45.5% PyQt 119
    • 59% PyBrain 154
    • 32.9% Any special tricks when programming in Python3 (if something specific, then describe in the comments) 86
    • 24.1% Tutorials on Python3 modules (which modules - write in the comments) 63

    Also popular now: