📜 ⬆️ ⬇️

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 the previous post, I just started to learn the python, so everything written in this article should not be taken as the Truth. Learning is the way, and it is tortuous.

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

It seems to be a classic example, it has already been written about it many times in Habré: “What are artificial neural networks?” , “Neural networks and character recognition” , etc.
But my goal is to study python on the not so simple examples. Those. we learn at once on the complex and unfamiliar. So we will find twice as many rakes, which will allow us to dig into the depths of the language, dealing with "why does not work?".
')
Under habrakat you will find: a description of how to prepare data on PyQt4 , the use of the module argparse , and of course PyBrain !


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

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

Data preparation


Technical task

Let's clarify the task: the size of the image with the letter will be, say, 64 by 64 pixels (a total of 4096 entries from the INS).
For this we need to write a generator of these pictures. And we will write it, naturally, in python.
Training data will include:

Based on this, we will write a generator, to which the parameters are fed to the input:


Search method of working with images

To write a generator, we need information on image processing methods in python. Google didn't help us much. I suggested using either the Python Imaging Library - PIL for short, or PyGame .
But that's bad luck. The first is only for python2, and the last release was in 2009. Although on github.com there is its fork under the third python . After reading the manual, I realized that everything is not so simple.
PyGame is a more interesting option, even the manual read it a little longer. I understood that it was necessary to specifically understand the library, but something could not be done with a swoop. And using a microscope for nailing is not an option either. Not for this library is intended.
Googled more. There is 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. You want to open a bottle of beer, you want to cut a beautiful figure from a piece of wood. The main thing - to be able to use a knife. On Qt and stop.
Search in a habr gave us very few information on PyQt . Well, nothing - we'll figure it out.

We write generation and saving of the image

The first thing to do is install PyQt4. With this, I hope the reader will cope - I will not dwell on this. I will go straight to use.
We import the necessary modules, and prepare the “fish” for the program on 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()) 

The line with app = QApplication([]) very important. Do not forget it. Without it, python crashes with SIGFAULT and does not issue any warnings or errors.

Now let's do the filling of the "fish" working logic. Add the save function, which will save the image with the specified 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 the parameters of the function - everything is clear. But I will explain the contents.
First, an object of the QImage class is created that allows you to create / process your images in your program. Very powerful and flexible tool. The whole image is 64 by 64 pixels in white.
Then an object of type QPainter is created to which the img reference is passed. This class allows you to draw on the device context or, more precisely, on the outline of any class inherited from QPaintDevice. And just such a class is a QImage.
Set the black pen, font and draw the letter. The default size is 40 (which almost occupies the whole field of the image) and is centered.
Well, then save the image to a file. Everything is simple and obvious.

Finishing touches

It remains small. Parsing command line options.
This can be done in the forehead (with a bunch of if, or by hard-typing the format of the input data), or you can use any advanced modules like getopt or argparse . Last, I think we will study.
Our program at the entrance will receive the following parameters: font, font size and directory where ready-made pictures will fall down.
Alignment while we leave until better times.
Reading this manual as if 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 (az)') args = p.parse_args() 

Thus, we describe our parameters, the argparse module will take care of the rest. What I liked was the automatic showing of usage and the automatic generation of help for parameters. Moreover, argparse added one more argument (-h) to our list. For which he thanks a lot. As a real and lazy programmer, I really do not like writing 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 we add a check for the existence of the directory path and the expansion of a range of letters. To do this, we use regular expressions. They are not particularly needed in this case, but it is necessary to make the program more impressive! For this, we need the modules os , os.path and re .
  if os.path.exists(args.dir): os.mkdir(args.dir) if re.match('^([az]-[az])|([AZ]-[AZ])$', args.letters): begin = args.letters[0] end = args.letters[2] if (ord(end)-ord(begin))>26: print("Error using letters. Only AZ or az available, not Az.") 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 the cycle and transfer all the letters in turn to the drawing.

The final touch is to make a binding on top of the save () function and call it saveWrap (). How original, isn't it? Actually, it does nothing 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 am sure that the python gurus will surely find a lot of optimization possibilities. But why? Everything works, the code is quite simple and concise. Straight eye rejoices.

INS development



Now we will start work on the ins. First, let's take a look at the capabilities of PyBrain .
Lyrical digression about PyBrain and Python3
I want to clarify that I tested the program only for 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.
Very pleased comment in the place where the library fell out:
 # FIXME: the next line keeps arac from producing NaNs.  I don't
 # know why this is the __str__ method of the
 # ndarray class fixes something,
 # str (outerr)

Apparently this hack in Python3 did not work.

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

Since our network will learn letters, whose images are 64 by 64 pixels in size, the number of entries in our network will be 4096 .
We have only 5 recognizable letters, respectively, and the number of exits from the network 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 of 64 neurons in size and the type of hidden layer of SoftmaxLayer, you need to make the following call:
 net = buildNetwork(64 * 64, 8 * 8, 5, hiddenclass=SoftmaxLayer) 

Unfortunately, the article was told about this function, but no description was given. I will correct this defect.
Educational program 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 as " name = val " and include:
bias (default = True) - the displacement layer in hidden layers
outputbias (default = True) - the beginning of the offset in the output layer
hiddenclass and outclass - set types for hidden layers and output layer, respectively. Must be a descendant of the NeuronLayer class. Predefined values ​​are GaussianLayer, LinearLayer, LSTMLayer, MDLSTMLayer, SigmoidLayer, SoftmaxLayer, TanhLayer.
If the recurrent flag is set, the RecurrentNetwork network will be created, otherwise FeedForwardNetwork.
If the fast flag is set, then faster arac networks will be used, otherwise it will be the PyBrain's own implementation of the Python network.

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

Training ins


So it's time to start learning. With the help of 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 the image and converting the RGB color to grayscale let me leave it behind the scenes. There is nothing particularly interesting. To whom it is still terribly interesting how it is done - it can see itself in the brain.py file in the get_data () function.

The learning itself is done in the init_brain () function. The training sample is transferred to this function, the maximum number of epochs for training and optionally the Trainer type, and the function itself returns the object of the already trained network.
Key lines of network creation and training look like this (the full code is provided 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 that where.
ClassificationDataSet is a special type of dataset for classification purposes. The source data and the class number of the class (trans [out]) are enough for it to make a sample.
The _convertToOneOfMany () function converts these same class numbers to the 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 the training (trainUntilConvergence ()), which will train either until the network converges or until the maximum number of learning epochs is released.

findings


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

I also add that I have tested two Trainer'a - BackpropTrainer and RPropMinusTrainer .
The speed of the backpropagation algorithm (BackpropTrainer) is poor, converges very slowly. Because of this, learning takes a lot of time.
By changing one line in brain.py, you can look at the work of RPropMinusTrainer . It is much faster and shows quite good results.
I will also add that I didn’t manage to achieve 100% recognition even for a training sample, 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 for studying Python3 the task is quite good: here you can work with lists, and with dictionaries, and process command line parameters, work with images, regular expressions, work with the file system (modules os and os.path ).

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

If you have any questions - write in a personal, but I think that you yourself will understand what, where and how.
There will be time, maybe I will rewrite the prettier code and make it more customizable, I will enter more parameters.

Source codes you can take in the spoilers below.
File code gen_pic.py
 #!/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 (az)') 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('^([az]-[az])|([AZ]-[AZ])$', args.letters): begin = args.letters[0] end = args.letters[2] if (ord(end) - ord(begin)) > 26: print("Error using letters. Only AZ or az available, not Az.") 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()) 


The code of the brain.py file
 #!/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()) 



At this acquaintance with PyBrain today I consider complete. See you again!

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

Source: https://habr.com/ru/post/171937/


All Articles