Простой классификатор на PyBrain и PyQt4 (Python3)

    Изучая Python3, я портировал (как смог) библиотечку PyBrain. Об этом я уже писал здесь.
    image
    Теперь же я хочу немного «поиграть» с данной библиотечкой. Как я уже говорил в предыдущем посте, питон я только начал изучать, так что все написанное в этой статье не стоит воспринимать как Истину. Изучение — это путь, и он извилист.

    Задачу поставим перед искусственной нейронной сетью (ИНС) весьма простую — классификацию, а именно: распознавание букв латинского алфавита.

    Вроде бы классический пример, про него уже писали на хабре неоднократно: «Что такое искусственные нейронные сети?», «Нейронные сети и распознавание символов» и т.д.
    Но моей целью стоит изучение питона на не самых простых примерах. Т.е. учимся сразу на сложном и незнакомом. Так мы найдем в два раза больше граблей, что позволит нам копнуть в глубины языка, разбираясь с «почему не работает?».

    Под хабракатом вас ждёт: описание способа подготовки данных на PyQt4, использование модуля argparse, ну и конечно же PyBrain!


    Почитав статьи здесь на хабре и не только, понимаешь, что сложно не написать/создать/спроектировать ИНС, а подготовить для неё набор обучающих и тестовых данных. Поэтому наша задача разбивается на две подзадачи:
    • подготовить данные для обучения;
    • спроектировать и обучить ИНС.

    Делать будем именно в этом порядке. Так сказать по нарастающей.

    Подготовка данных


    Техническое задание

    Давайте уточним задание: размер картинки с изображением буквы будет, скажем, 64 на 64 пиксела (итого 4096 входа у ИНС).
    Для этого нам потребуется написать генератор этих картинок. И писать его мы будем, естесственно, на python'е.
    Данные для обучения будут включать в себя:
    • буквы латиницы в нижнем регистре
    • буквы латиницы в верхнем регистре
    • буквы могут быть разного размера (опционально)
    • начертание может быть различным (могут использоваться разные шрифты)

    Исходя из этого напишем генератор, которому на вход подаются параметры:
    • список букв, например, abc или f-x(диапазон)
    • размер шрифта, например, 40
    • используемый шрифт
    • путь к папке, куда будут складываться сгенерированные изображения


    Поиск метода работы с изображениями

    Для написания генератора нам понадобится информация по методам обработки изображений в python'е. Google нам помог не сильно. Предложил использовать либо Python Imaging Library — сокращённо PIL, либо PyGame.
    Только вот незадача. Первый — только для python2, и последний релиз был в 2009 году. Хотя на github.com есть его форк под третий питон. Почитав мануал я понял, что не все так просто.
    PyGame — вариант более интересный, даже мануал его почитал подольше. Понял, что надо конкретно разбираться в библиотеке, а с наскока что-то сделать не получится. Да и использовать микроскоп для забивания гвоздей — тоже не вариант. Не для того эта библиотека предназначена.
    Погуглил ещё. Есть ещё pythonmagick, но он только для UNIX-like систем. И тут меня осенило! PyQt4!
    C Qt4 я неплохо знаком, на С++/Qt много писал. Да библиотечка эта — как швейцарский нож. Хочешь — бутылку пива откроет, хочешь — из куска деревяшки красивую фигурку вырежет. Главное — уметь ножом пользоваться. На Qt и остановимся.
    Поиск по хабру дал нам совсем немного информации по PyQt. Ну да ничего — разберёмся.

    Пишем генерацию и сохранение изображения

    Первое, что требуется — это установить PyQt4. С этим, я надеюсь, читатель справится — останавливаться на этом не буду. Перейду сразу к использованию.
    Сделаем импорт необходимых модулей, и приготовим «рыбу» для программы на 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())
    

    Строка с app = QApplication([]) очень важна. Не забывайте её. У меня без неё python вылетает с SIGFAULT'ом и не выдает никаких предупреждений и ошибок.

    Теперь займемся наполнением «рыбы» рабочей логикой. Добавим функцию save, которая будет сохранять картинку с заданными параметрами.
    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)
    


    С параметрами функции — всё ясно. А вот по содержимому поясню.
    Сначала создается объект класса QImage, который позволяет создавать/обрабатывать свои изображения у себя в программе. Очень мощный и гибкий инструмент. Производится закраска белым цветом всего изображения размером 64 на 64 пиксела.
    Затем создается объект типа QPainter, которому передается ссылка img. Этот класс позволяет рисовать на контексте устройства или, если точнее, на канве любого класса, унаследованного от QPaintDevice. А как раз таким классом и явлется QImage.
    Устанавливаем черное перо, шрифт и рисуем буковку. По умолчанию — размером 40 (что почти занимает всё поле изображения) и с размещением по центру.
    Ну а затем сохраняем изображение в файл. Всё просто и очевидно.

    Последние штрихи

    Осталось малое. Разбор параметров командной строки.
    Это можно делать в лоб (с кучей if, либо жёстко задав формат входных данных), а можно с использованием всяких продвинутых модулей типа getopt или argparse. Последний, думаю, мы и изучим.
    Программа наша на вход будет получать следующие параметры: шрифт, размер шрифта и директория, куда будут сваливаться готовые картинки.
    Выравнивание пока оставим до лучших времен.
    Чтение этого мануала как бы подсказывает нам, что надо просто использовать вот такой кусок кода:
    	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()
    

    Таким образом мы описываем наши параметры, обо всём остальном позаботится модуль argparse. Что мне понравилось, так это автоматический показ usage и автоматическая генерация помощи по параметрам. При чём argparse ещё один аргумент (-h) в наш список сам добавил. За что ему большое спасибо. Как настоящий и ленивый программист, я очень не люблю писать хелпы и прочую документацию. Это очко в пользу argparse. Буду чаще им пользоваться.
    Справка по программе у нас получается такой:
    usage: gen_pic.py [-h] [-f FONT] [-s SIZE] [-d DIR] letters
    
    Symbols image generator
    
    positional arguments:
      letters               Array of letters(abc) or range (a-z)
    
    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
    

    Теперь добавим проверку на существование пути директории и разворачивание диапазона букв. Для этого воспользуемся регулярными выражениями. Они не особо нужны в данном случае, но надо же программу посолидней сделать! Для этого нам потребуются модули os, os.path и re.
    	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
    

    Ну вот. Осталось организовать цикл и передать все буковки по очереди на отрисовку.

    Последний штрих — сделаем обвязочку поверх функции save() и назовём её saveWrap(). Как оригинально, не правда ли? Собственно она не делает ничего сверхестесственного, просто генерирует имя для файла исходя из передавваемых в функцию save() параметров.

    Итого весь генератор занял у нас всего 55 строк (код приведён в конце статьи). Это ли не прекрасно?
    Причем я уверен, что гуру питона наверняка найдут ещё кучу возможностей для оптимизации. Но зачем? Всё работает, код достаточно прост и лаконичен. Прям глаз радуется.

    Разработка ИНС



    Теперь начнем работу над ИНС. Для начала ознакомимся с возможностями PyBrain.
    Лирическое отступление про PyBrain и Python3
    Хочу уточнить, что я тестировал программу только под Python3 и пользовался портом PyBrain, который вы можете найти здесь. Пока отлаживал программу нашёл пару косяков в самом порте библиотеки.
    Очень порадовал комментарий в том месте, где вываливалась библиотека:
    # 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)
    

    Видимо этот хак в Python3 не сработал.

    На вход у нас подается изображение (в серых тонах), задаваемое значениями яркости каждого пикселя от 0 до 1.
    Для начала сделаем ИНС, которая будет распознавать ограниченный набор символов и без разных регистров. Обучим на данных с одним шрифтом и посмотрим, как сеть распознает эти символы с другим шрифтом (тестовый набор). Возьмем, например, символы A, B, C, D, Z.

    Поскольку у нас сеть будет учиться буковкам, изображения которых имеют размер 64 на 64 пиксела, то количество входов нашей сети будет равно 4096.
    Самих распознаваемых букв у нас будет только 5, соответственно, и количество выходов из сети равно пяти.
    Теперь встаёт вопрос: нужны ли нам скрытые слои? И если да, то сколько?
    Я решил обойтись без скрытых слоёв, поэтому для создания объекта сети делаю следующий вызов:
    net = buildNetwork(64 * 64, 5)
    

    Для создания одного скрытого слоя размером в 64 нейрона и типом скрытого слоя SoftmaxLayer надо выполнить следующий вызов:
    net = buildNetwork(64 * 64, 8 * 8, 5, hiddenclass=SoftmaxLayer)
    

    К сожалению, в статье было сказано про данную функцию, но не было дано описание. Исправлю этот недочёт.
    Ликбез про buildNetwork()
    Функция buildNetwork() предназначена для быстрого создания FeedForward-сети и имеет следующий формат:
    pybrain.tools.shortcuts.buildNetwork(*layers, **options)
    

    layers — список или кортеж целых чисел, котоый содержит количество нейронов в каждом слое
    Опции записываются в виде "name = val" и включают в себя:
    bias (default = True) — начилие смещения в скрытых слоях
    outputbias (default = True) — начилие смещения в выходном слое
    hiddenclass и outclass — задают типы для скрытых слоёв и выходного слоя соответственно. Должны быть потомком класса NeuronLayer. Предопределенные значения — это GaussianLayer, LinearLayer, LSTMLayer, MDLSTMLayer, SigmoidLayer, SoftmaxLayer, TanhLayer.
    Если установлен флаг recurrent, то будет создана сеть RecurrentNetwork, иначе FeedForwardNetwork.
    Если установлен флаг fast, то будет использоваться более быстрые сети arac, в противном же случае — это будет собственная реализация PyBrain сети на питоне.

    Те, кому интересно, могут выбрать другие варианты, исправив/раскомментировав вызов функции buildNetwork() в файле brain.py.

    Обучение ИНС


    Итак, пришло время взяться за обучение. При помощи нашей программки gen_pic.py генерируем нужные буквы.
    Я сделал это так:
    ./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
    

    Процесс загрузки из картинки данных и преобразование RGB-цвета в тона серого позвольте оставить за кадром. Там ничего особо интересного. Кому всё-таки жутко интересно как же это сделано — может увидеть сам в файле brain.py в функции get_data().

    Само обучение производится в функции init_brain(). В эту функцию передается обучающая выборка, максимальное количество эпох для обучения и опционально тип Trainer'a, а сама функция возвращает объект уже обученной сети.
    Ключевые строки создания и обучения сети выглядят так (полный код приведён к конце статьи):
    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
    

    Кратко поясню, что где.
    ClassificationDataSet — специальный вид датасетов для целей классификации. Ему достаточно исходных данных и порядкового номера класса (trans[out]) для составления выборки.
    Функция _convertToOneOfMany() переводит эти самые номера классов в значения выходного слоя.
    Далее передаём «учителю» сеть и говорим, что нас интересует вывод дополнительной информации (библиотека будет печатать в консоль промежуточные вычисления).
    Даём учителю датасет с обучающей выборкой (setData()) и запускаем обучение (trainUntilConvergence()), которое будет обучать либо пока сеть не сойдётся, либо пока не выйдет максимальное количество эпох обучения.

    Выводы


    Итак, цель достигнута.
    Код написан и работает. Генератор, правда, может намного больше, чем сеть, нами сегодня построенная, в текущем её виде. Но зато осталось непаханное поле для Вас, дорогой %username%! Есть чего поправить, где подредактировать, что переписать…

    Добавлю ещё, что я потестировал два Trainer'a — BackpropTrainer и RPropMinusTrainer.
    Скорость работы у алгоритма обратного распространения ошибки (BackpropTrainer) плохая, сходится очень медленно. Из-за чего обучение занимает много времени.
    Поменяв одну строку в brain.py можно посмотреть на работу RPropMinusTrainer. Он значительно шустрее и показывает довольно неплохие результаты.
    Добавлю ещё, что добиться 100% распознавания даже для обучающей выборки мне не удалось, может надо было подбирать количество слоёв и количество нейронов в каждом — не знаю. Практического смылса в данной программе особо нету, но для изучения Python3 задача весьма неплоха: здесь и работа со списками, и со словарями, и обработка параметров командной строки, работа с изображениями, регулярные выражения, работа с файловой системой (модули os и os.path).

    Для желающих поиграться скажу лишь одно — программа brain.py потребует доработки, если вы захотите изменить количество букв или поменять их на другие. Доработки небольшие и несложные.

    Если будут возникать вопросы — пишите в личку, но думаю, что вы и сами разберётесь что, где и как.
    Будет время, может перепишу код посимпатичнее и сделаю его более настраиваемым, введу больше параметров.

    Исходные тексты Вы можете взять в спойлерах ниже.
    Код файла 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 (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
    #!/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())
    



    На этом знакомство с PyBrain на сегодня считаю законченным. До новых встреч!

    upd: по просьбе monolithed исправил регулярное выражение.
    Что вам интереснее больше? На чём сконцентрироваться при написании следующей статьи?

    Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

    Метки:
    Поделиться публикацией
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 23
    • +2
      «if re.match('\w-\w', args.letters):»
      неправильно же! вы проверяете что есть минус в строке, а если там будет, например, abcd-efgh?
      вы будете делать только a-c!
      • 0
        а по опросу — хотелось бы осмотр главных плюсов, ради которых надо с пы2 на пы3 переходить. вот не вижу пока, даже под лупой.
        скорость? разницы нет.
        стабильность? разницы нет.
        да вообще — разница? разницы ж нет…
        • 0
          По-моему, он местами чуточку логичнее.
          Простой пример:
          в py2 print — это что? По идее функция, но вызывается без ()
          в py3 логично делается вызов print()
          Да и не только это. Но не могу я с вами на эту тему спорить, ибо python2 я не знаю. Я просто начал изучать python, а зачем вникать в суть устаревающего языка, если всё равно идёт медленная миграция на python3.
          Да и с точки зрения финансовой, будет повальный переход на py3, потребуются спецы, а большинство будет только py2 хорошо знать, ведь зачем учить новое, если есть старое?
        • 0
          Да, я это заметил, когда проводил финальную вёрстку, но исправлять не стал. Я знаю, как вызывать правильно, а неправильно — это домашняя работа для хабрачитателей.
          В принципе, я это даже в самой статье подчеркнул.
          Правильно будет так:
          if re.match('^\w-\w$', args.letters):
          
          • 0
            то есть можно попросить при этом "_-w"? ;) и каков эффект будет? ;)
            • 0
              Хм. Думал, что \w — это любая буква, а \W — любая не-буква. Но ваш вариант даёт True.
              Буду копать. Видимо есть различие в регулярных выражениях для перла и питона.
              Регулярные я в перле учил когда-то.
              • 0
                Вот документация на модуль re. Там действительно написано \w = [A-Za-z0-9_]
                Всё равно — вот так вот точно правильно:
                if re.match('^([a-z]-[a-z])|([A-Z]-[A-Z])$', args.letters):
                
                • 0
                  вот это — самый правильный вариант :)
                  • 0
                    Т.е. a-A не должно матчиться?

                    • 0
                      дык выше, в хелпе: print(«Error using letters. Only A-Z or a-z available, not A-z.»)
                      • 0
                        Ну тогда нужно регулярку в статье поправить :)
                      • 0
                        Нет. Об этом даже в статье написано. Правда, в коде:
                        print("Error using letters. Only A-Z or a-z available, not A-z.")
                        

                        Хотя программа и способна отрисовать не только буквы, но и цифры, спец-символы и т.д., но тогда требуется её доработка.
                  • 0
                    \w — это [a-zA-Z0-9_]. Причем это везде. И в перле тоже.
                    а вам надо [a-zA-Z] либо [:alpha:]
                    Если 0-9 надо — то [:alnum:]

                    en.wikipedia.org/wiki/Regular_expression#POSIX_character_classes
                    • 0
                      Ну давно это было. Подзабылось.
                      Защиты от дурака в программе особо-то и не реализовано…
                      • 0
                        [:alpha:] и \p{L} есть только альтернативной реализации re
              • 0
                По поводу опроса.

                Народ, вы молодцы, что голосуете, но хоть какие-нибудь пожелания-то оставляйте!
                • 0
                  Картинок не хватает.
                  Можете считать меня дитём необразованным, но мне в статьях катострофически не хватает картинок, за которые можно зацепиться взглядом.
                  Дело в том, что в наш век информации в интернете очень много и становится ещё больше. И при наличии хорошей, годной статьи, бывает взглянешь на обилие буков и грустно становится. Я всеми фибрами ощущаю нехватку правильного способа переварить всю нужную мне информацию. Наличие картинок, скриншотов, диаграм, схем — в-общем каких-либо графических изображений ключевых моментов статьи, как мне думается, может поправить положение.
                  Таково моё мнение.
                  • 0
                    Понятно. Только в контексте данной статьи картинками могут быть разве что отрендеренные буквы.
                    А так — на будущее, буду иметь в виду. Спасибо.
                    • 0
                      Ну почему, как минимум график уменьшения ошибки, кросс-валидация. Структура сети (пусть и упрощенно).
                • 0
                  Причем я уверен, что гуру питона наверняка найдут ещё кучу возможностей для оптимизации. Но зачем? Всё работает, код достаточно прост и лаконичен. Прям глаз радуется.

                  Это молодость, это пройдет. :)

                  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)


                  имя в нестандартной нотации. www.python.org/dev/peps/pep-0008/#method-names-and-instance-variables
                  Складывание кучи строк некрасиво. docs.python.org/3.3/library/string.html?highlight=string#format-examples
                  Функция совсем не нужна. Можно всю логику перенести в save

                  # 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)

                  Перед публикацией вычитывайте код. Коментированные строки излишни.

                  print («img.load({0}) failed!».format(png_file))

                  Вначале подумал что атавизм из второго питона. Потому что данная строка будет там работать точно так же. Если ставите пробел между принтом и вызовом то ставьте его везде.

                  learn_path = os.path.abspath(args.learn_data_dir) + "/"

                  Попробуйте без слэша.

                  list_dir = os.listdir(dir_name)

                  Гляньте глоб docs.python.org/3.3/library/glob.html?highlight=glob.glob#glob.glob

                  Спасибо за библиотеку работы с картинками, а то PIL у меня под 7-64 заводится с бубнами.
                  • 0
                    1) про стандартные нотации был не в курсе, я же изучаю. Но про PEP8 слышал, код писал в sublime-text с модулем PEP8 autoformat. Редактор мне много всего подчеркивал и исправлял. А такая нотация именования у меня сохранилась из Qt.
                    Кстати, если обратите внимание, то в самой библиотеке PyBrain народ не придерживается этого правила.
                    2) сложение строк некрасиво, согласен
                    3) внедрение функционала генерации имени в саму функцию save() считаю неправильным архитектурно, ибо функция отвечает за сохранение. А вот saveWrap() надо было назвать generate_filename_and_save_it().
                    4)
                    Перед публикацией вычитывайте код. Коментированные строки излишни.

                    Строки оставлены специально. Если прочитать текст статьи, то там есть
                    Те, кому интересно, могут выбрать другие варианты, исправив/раскомментировав вызов функции buildNetwork() в файле brain.py.

                    5) про print: рефлекторная опечатка, если посмотрите код, то там где как — где с пробелом, где без.
                    6)
                    Попробуйте без слэша.
                    Пробовал. Программа будет работать некорректно. Надо тогда и функцию loadData() в нескольких местах править. Здесь уже лень сработала. Зачем править несколько мест, если можно поправить в одном.
                    7) гляну, спасибо. В книге по питону мне такого пока не попадалось.

                    И в заключение хочу добавить — большую часть замечаний можно было устранить просто написав в личку.
                    Но с другой стороны, кто-то ещё научится на моём примере.

                    Спасибо за развёрнутый, а главное — конструктивный комментарий.
                  • 0
                    Pillow is the «friendly» PIL fork by Alex Clark and Contributors. PIL is the Python Imaging Library by Fredrik Lundh and Contributors.

                    Pillow >= 2.0.0 supports Python versions: 2.6, 2.7, 3.2, 3.3;

                    pypi.python.org/pypi/Pillow/2.0.0
                    • 0
                      Спасибо. Изучим.

                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.