среда, 24 августа 2016 г.

Моделирование вращения твёрдого тела: С++, SFML, OpenGL.

В этом посте мы рассмотрим создание компьютерной визуализации вращения твёрдого тела на основе реальных физических законов.

Довольно давно я уже сталкивался с библиотекой OpenGL и даже участвовал в проекте, где в десктопном приложении была реализована 3D визуализация с использованием этой библиотеки, но это было очень давно. Тогда последняя версия OpenGL была 2.0, а в нашем коде, если мне не изменяет память, использовалась версия 1.1. С тех пор утекло много воды и я неожиданно обнаружил, что мои знания об OpenGL сильно устарели: в уроках OpenGL так и написано "Забудьте все, что вы знали об OpenGL ранее, если ваши знания касаются glBegin() и подобных функций". Что ж, программировать трёхмерную графике по крайней мере весело! Почему бы и не обновить знания?

При создании анимации, компьютерных игр или решении инженерных задач может потребоваться промоделировать и визуализировать движение каких-то твёрдых предметов. Хотя сейчас для компьютерного моделирование механики существуют разнообразные физические движки, которые с успехом решают эти задачи, всё равно интересно и полезно представлять как движутся твёрдые тела. Здесь мы рассмотрим свободное движение твёрдого тела, на которое не действуют никакие силы и моменты. Этакий брошенный и свободно летящий "кирпич". В этом случае движение можно разделить на два компонента: поступательное движение и вращательное.

Свободное вращательное движение в общем случае более сложное чем вращение вокруг фиксированной оси: объект крутится вокруг какой-то оси, при этом сама ось вращения тоже совершает цикличекие движения.

Я воспользуюсь следующими известными из курса механики фактами:
1. Момент импульса объекта не меняется, так как на него не действуют моменты внешних сил:
2. Момент импульса связан с угловой скоростью формулой $\vec{L} = \hat{J} \vec\omega $, где $J$ - тензор инерции. Следовательно, угловую скорость в каждый момент времени можно выразить формулой $\vec\omega = \hat{J}^{-1}\left(t\right)\vec{L}$.
3. Ориентацию тела в пространстве можно описать ортогональной матрицей $\vec{Q}$ с положительным определителем.

При расчётах будем использовать неподвижную систему координат. В ней все компоненты вектора $\vec{L}$ будут постоянными, а компоненты тензора инерции $\hat{J}_{ij}$ будут меняться, так как будет меняться ориентация тела. Угловую скорость в каждый момент времени можно определить по формуле $\vec\omega = \hat{J}^{-1}\left(t\right)\vec{L}$. Зная угловую скорость можно примерно определить ориентацию тела через короткий промежуток времени $\Delta t$.


 Соединив всё это воедино полуаем такой алгоритм:
1. Инициализируем тензор инерции тела, момент имульса, матрицу ориентации тела:
$$\hat{J^{-1}_0}=\left(\begin{array}{ccc}1/J_x & 0 & 0 \\
0 & 1/J_y & 0 \\
0 & 0 & 1/J_z \end{array}\right) $$

$$Q_0=\left(\begin{array}{ccc}1 & 0 & 0 \\
0 & 1 & 0 \\
0 & 0 & 1 \end{array}\right) $$

2. Делаем следующие итерации:
$$\vec{\omega_{\tau}} = \hat{J^{-1}_\tau} \vec{L}$$
$$dQ_\tau = R\left(\vec{n}_\omega, |\vec\omega|\Delta t\right)$$
$$\hat{J^{-1}_{\tau+1}} = dQ_\tau \hat{J^{-1}_\tau} dQ_\tau^{-1}$$
$$Q_{\tau+1} = dQ_\tau Q_\tau $$


Где $\vec{n}_\omega$ единичный вектор, совпадающий с направлением $\vec{\omega}$, $|\vec \omega|$ - абсолютная величина вектора $\vec{\omega}$. Угол, на который поворачивается тело за время $\Delta t$ примерно равен $|\vec\omega|\Delta t$. Матрица трёхмерного поворота вокруг заданной оси $\vec n$ на заданный угол $\alpha$ обозначена как $R\left(\vec n, \alpha\right)$. Выражение для неё несложно найти, например, в википедии. В библиотеке glm есть готовая функция для создания матрицы поворота по заданной оси и углу: glm::rotate

Реализация. 

Я пошёл по пути наименьшего сопротивления и просто объединил два примера кода из сети: пример из туториала по OpenGL (есть русская версия), в котором создаётся трёхмерное изображение куба и код из туториала SFML, в котором показывается как инициализировать окно OpenGL.

Для работы с матрицами и векторами используем библиотеку glm. Она тесно интегрирована с OpenGL и уже и так используется в примере с кубом. В то же время вполне подходит и для любых других других применений если вам не нужны матрицы размером больше чем 4x4 и вектора размером больше чем 4. 

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

Матрица ориентации $Q$ это и есть матрица модели. В приемере со статическим кубом из OpenGL туториала матрицы модели всегда единичная, а матрицы модель-вид-проекция (MVP) вычисляется так:
В нашем случае матрица модели должна пересчитываться при каждой перерисовке кадра:
Операция glm::scale тоже часть вычисления матрицы модели. Она растягивает наш куб в прямоугольный параллелепипед в соответствии с главными моментами инерции.

Результат. 

Исходники доступны на GitHub. Хотя я создавал этот проект в Microsoft Visual Studio, код должен быть переносим на Linux и OS X, хотя я не пробовал собирать его в других системах.

 

В результате видим, что параллелепипед вращается, и движение зрительно похоже на то, что ожидаешь увидеть. Так же воспроизводится эффект гайки Джанибекова: закрученное вокруг оси близкой к оси со средним моментом инерции тело периодически "переворачивается". Однако если понаблюдать подольше, видно, что симуляция неустойчива: через какое-то время вращение уходит от оси со средним моментом инерции и стабилизируется вокруг оси с максимальным или минимальным моментом инерции. По моему убеждению это артефакт используемого вычислительного метода. В следующих постах я попробую бороться с этой неустойчивостью.

пятница, 15 апреля 2016 г.

Знакомство с микроконтроллерами ARM Cortex M на примере LPC1114. Часть 2

В предыдущем посте я рассмотрел создание простой прошивки для микроконтроллера LPC1114 полностью на ассемблере. Такой подход, без сомнения, полезен для изучения архитетуры ARM Cortex M, но весьма непрактичен. Как правило такие программы создаются с помощью языка программирования C, а ассемблеру отводится место лишь в небольшом числе "ассемблерных вставок". Нередко обходятся вообще без ассемблера. В этом посте мы перепишем программу мигания светодиодом на C.

Возможны две стратегии: небольшой код начальной инициализации на ассемблере, который передаёт управление коду на C, либо можно написать абсолютно всё на C, включая код инициализации. Здесь мы рассмотри оба варианта.

Итак, будем исполнять в контроллере такой простой код:

Для пауз используются циклы, а с помощью директивы препроцессора #define мы назначаем некоторым специальным адресам, связанным с периферией, более удобные имена.

Остаётся позаботится о том, чтобы кто-то вызвал нашу функцию main. Это сделает код начальной загрузки на ассемблере.

Всё очень просто: во время начальной загрузки управление передаётся инструкциям после метки Reset_handler. Там лежит одна единственная ассемблерная инструкция BL main, которая и вызовет сишную функция main.

Собираем:

PATH_TO_GNU_TOOLCHAIN\arm-none-eabi-as.exe -mcpu=cortex-m0 startup-lpc1114.s -o startup.o

PATH_TO_GNU_TOOLCHAIN\arm-none-eabi-gcc.exe -mcpu=cortex-m0 -mthumb blink-lpc1114.c -c -o blink.o

PATH_TO_GNU_TOOLCHAIN\arm-none-eabi-gcc -nostartfiles -Tblink-min.ld startup.o blink.o -o blink.elf

PATH_TO_GNU_TOOLCHAIN\arm-none-eabi-objcopy.exe -O binary blink.elf blink.bin

Бинарник blink.bin готов! Закидываем его в контроллер и наслажнаемся миганием светодиода. Такого же результата можно добиться и на чистом C. Для этого надо будет создать таблицу прервыаний прямо в C файле, и сказать компоновщику, что её надо разместить начиная с нулевого адреса. Вместо ассемблерного файла startup-lpc1114.s у нас теперь будет файл startup-lpc1114.c:

Собираем почти так же:

PATH_TO_GNU_TOOLCHAIN\arm-none-eabi-gcc.exe -mcpu=cortex-m0 startup-lpc1114.c -o startup.o

PATH_TO_GNU_TOOLCHAIN\arm-none-eabi-gcc.exe -mcpu=cortex-m0 -mthumb blink-lpc1114.c -c -o blink.o

PATH_TO_GNU_TOOLCHAIN\arm-none-eabi-gcc -nostartfiles -Tblink-min.ld startup.o blink.o -o blink.elf

PATH_TO_GNU_TOOLCHAIN\arm-none-eabi-objcopy.exe -O binary blink.elf blink.bin


Один из этих двух подходов можно использовать в реальных проектах. В следующиз постах я постараюсь рассказать о библиотеках, которые облегчают создание программ для микроконтроллеров ARM Cortex M.

пятница, 25 марта 2016 г.

Знакомство с микроконтроллерами ARM Cortex M на примере LPC1114

В последнее время всё большую популярность набирают микроконтроллеры построенные на основе архитектуры ARM. Есть мнение, что эра восьмиразрядных микроконтроллеров подходит к концу, так как им трудно тягаться в производительности с 32-разрядными ARM Cortex M-ядрами. Более того, ARM-чипы уже опережают 8-битных конкурентов не только по соотношение цена/производительность, но и просто по цене.

Архитектура процессоров ARM и семейство Cortex M

Понятие "архитектура ARM" объединяет довольно большое семейство микроархитектур 32 и 64-разрядных RISC-процессоров. Различные микроархитектуры имеют свои особенности и свои области применения. Поэтому при первоначальнои поиске информации нужно понимать, что "ARM ARMу рознь". В этом посте речь пойдёт о микроархитектуре ARM Cortex-M, ориентированной на микроконтроллеры, и у неё, конечно, есть свои особенности, которые надо учитывать при первоначальном поиске информации. Например, процессоры ARM могут поддерживать два набора инструкций: 32-битные инструкции ARM и 16-битные Thumb. При этом ARM Cortex-M поддерживает только Thumb инструкции. Кроме того, процедура начальной загрузки, которую мы будем тут рассматривать, отличается для микроконтроллеров Cortex-M от таковой для большинства других ARM-микроархитектур.

Микроконтроллеры Cortex-M в свою очередь делятся на несколько семейств в зависимости от ядра. Семейство на самых простых ядрах Cortex-M0 - микроконтроллеры предназначенные ситуаций, где самый важный фактор - цена. Как правило они имеют не столь богатую периферию как Cortex-M3/M4/M7. Лично мне кажется, что чем меньше навротов тем проще изучение (хотя бы потому, что datasheet короче, и досконально изучить микроконтроллер гораздо проще). С другой стороны, реализация мигания светодиодом очень похожа и для LPC1114 (Cortex-M0) и для какого-нибудь STM32F4 (Cortex-M4F). Впрочем, хотя микроконтроллеры ARM Cortex-M от разных производителей хотя и имеют одинаковое "ядро", в периферии могут иметь большое количество различий, тонкостей и своих граблей. Поэтому не стоит недооценивать опыт работы с конкретным микроконтроллером и вдумчивое чтение документации.

На рынке есть большое количество отладочных плат с этими микроконтроллерами, что сильно облегчает изучение и создание прототипов устройств. Для создания своих собственных плат может потребоваться оборудование и определённые навыки. Тем не менее это вполне под силу многим энтузиастам. Пользователь habrahabr.ru Михаил Сваричевский (@BarsMonster) убедительно показывает, что создание своей платы с микроконтроллером STM32F100C4T6B вполне поддаётся лазерно-утюжной технологии, но для многих (для меня в том числе) даже это - черезчур.

С некоторых пор Cortex-M0 микроконтроллеры существуют и в DIP-корпусах, что на мой взгляд делает создание своих собственных плат доступным даже абсолютным новичкам.
Мне известно по крайней мере о двух таких чипах: LPC810 (DIP8) и LPC1114 (DIP28). Недавно я приобрёл второй чип LPC1114. На мой взгляд это идеальный микроконтроллер для знакомства с семейсвом Cortex M. Сейчас цена eBay на них $4.50 (недёшево).

Обзор регистров и инструкций Thumb

В архитектуре ARM есть 12 регистров общего назначения. Они так и называются: R0-R12. Также имеется 2 регистра-указателя стека MSP (Main Stack Pointer) и PSP (Process Stack Pointer). Один из этих указателей отображается на регистр R13 (SP) (Stack Pointer). R13 и SP - просто взаимозаменяемые названия. В зависимости от бита 1 в регистре CONTROL SP будет соответсвовать MSP (0) или PSP (1). Link Register (LR) или R14 используется при вызове процедур и прерываний. В нём содержится адрес возврата. Program Counter (PC) или R15 - указатель текущей команды. Program Status Register (PSR).

Программа кодируется с помощью 2-байтных инструкций Thumb

Регистры общего назначения R0-R12 делятся на LO-регистры (R0-R7) и HI-регистры (R8-R12). Многие команды Thumb работают только с LO-регистрами. Согласно документации, значение может передаваться из из LO-регистра в HI-регитр и из HI-регистра в LO-регистр, с помощью специального варианта инструкции MOV. Значения в HI-регистрах могут сравниваться с значениями в LO-регистрах с помощью инструкции CMP.  Значения в HI-регистрах могут так же добавляться к значениям в LO-регистрах с помощью инструкции ADD.

Задействуем периферию

В стандартный набор периферии микроконтроллеров могут входить UART (универсальный асинхронный приёмопередатчик, тот что лежит в основе компьютерных COM-портов), шина I2C, шина SPI и, разумеется GPIO (General Purpose I/O или вводы-выводы общего назначения). Могут входить так же ADC (АЦП, аналого-цифровой преобразователь), DAC (цифро-аналоговый преобразователь), реализованный аппаратно PWM (ШИМ, широтно-импульсная модуляция). Внешние утстройства управляются с помощью регистров (не путать с регистрами процессора R0-R15), которые отображаются на определённые области в памяти.

Так, например, в LPC1114 по адресу 0x50008000 находится регистр GPIO0DIR, который определяет, какие ножки GPIO0 будут работать как вводы, а какие - как выводы. А по адресу 0x50003ffc находится регистр GPIO0DATA, с помощю которого можно установить значение на выводах (записывая значения в соответствующие биты регистра), или, соответственно, считать значения на тех ножках GPIO0, которые настрены как ввводы.

То есть в отличие от архитектуры x86 в ARM нет аналогов инструкций IN/OUT. Вместо записи значения в порт нужно просто записать значение по определённому адресу так же как мы бы записывали его в память. Это, кстати, позволяет при работе с внешними устройствами из языка C/С++ использовать только лишь адресную арифметику и указатели.

В процессоре LPC1114 (как и во многих других) можно "включать" и "выключать" периферию, оптимизируя таким образом потребляемую мощность. Как я понял, подключение/отключение блоков периферии производится с помощью подключения/отключения тактирования для этих блоков. Это делается установкой соответствующего бита в регистре SYSAHBCLKCTRL (по адресу 0x40048080). В нашем примере мы устанавливаем 6-й и 16-й биты, включая таким образом GPIO (6-й бит) и IOCON Block (блок конфигурации воода-вывода).

Начальная загрузка процессора ARM Cortex M. 

Процедура начальной загрузки одинакова по крайней мере для контроллеров Cortex-M0/3/4. Процессор считывает по адресу 0x00000000 начальное значение указателя стека и инициализирует регистр MSP (который сразу после загрузки отображается на регистр R13) Затем, начиная с адреса 0x00000004 находится таблица исключений (таблица прерываний). Самое первое прерывание - Reset. Это значит, что по адресу 0x00000004 лежит адрес кода, который начнёт выпонятся сразу после сброса микроконтроллера. Вооружившись этими знаниями, а так же таблицей комманд Thumb и манулом к LPC1114 я покажу как реализовать мигание светодиодом - аналог "Hello world" в мире микроконтроллеров.

Реализация мигания светодиодом на ассемблере

Хорошая новость о разработке на ассемблере для ARM заключается в том, что можно обойтись совсем без ассемблера, и написать всю прошивку, начиная от начальной заргрузки и заканчивая общением с периферией на языке C или даже C++. Однако для знакомства с платформой написание программ на ассемблере будет очень полезным упражнением. С этого и начнём. В мире ARM популярны как минимум два транслятора ассемблерного кода GNU AS и ARMASM, и синтаксис у них отличается. Здесь рассмотрен GNU AS.


Инструменты

  Я буду использовать набор утилит для сборки (toolchain) GNU и коммандную строку. Это позволит в максимально явном виде показать каждый этап сборки.

Сборка проекта

Сначала оттранслируем ассемблерный файл, получив на выходе объектный файл *.o :

PATH_TO_GNU_TOOLCHAIN_\arm-none-eabi-as.exe -mcpu=cortex-m0 blink-lpc1114.s -o blink-lpc1114.o

В итоге должен получиться файл blink-lpc1114.o . Следующий шаг - компоновка, или, в просторечии - линковка. Для того чтобы всё правильно слинковалось нам понадобится правильный скрипт компоновки.
Используя этот скрипт можно скомпоновать объектный файл в ELF-файл:

PATH_TO_GNU_TOOLCHAIN/arm-none-eabi-gcc -nostartfiles -Tblink-lpc1114.ld blink-lpc1114.o -o blink-lpc1114.elf

Должен сгенерироваться файл blink-lpc1114.elf . Наконец, последний шаг - конвертация ELF-файла в BIN-файл, который будет из себя представлять двоичные данные в том виде, в котором они будут лежать во флеш-памяти микроконтроллера. Это делается с помощью утилиты objcopy:

PATH_TO_GNU_TOOLCHAIN/arm-none-eabi-objcopy.exe -O binary blink-lpc1114.elf blink-lpc1114.bin
В результате получим файл blink-lpc1114.bin.

Прошивка контроллера

Здесь я немного "срезал угол". Я приобрёл чип LPC1114 вместе с mbed-совместимой платой. Она подключается к USB-разъёму, и определяется как файловая система (этакая небольшая флешка). На неё можно просто скопировать bin-файл. После перезагрузки плата "поймёт", что есть новая версия прошивки для микроконтроллера и "зальёт" бинарник в его флеш-память. Остаётся убедиться, что всё работает.


Без ткой платы LPC1114 прошивается через UART с уровнями 3.3 В. Существуют по крайней мере две утилиты для прошивки: lpc21isp (opensource) и flashmagic (бесплатная, но исходники, вроде, закрыты). Если использовать компьютерный COM-порт, то необходимо использовать адаптер уровней (COM-порт выдаёт уровни +12/-12 вольт). Я планирую попробовать использовать плату Raspberry Pi и её UART-выводы, которые как раз выдают 3.3 В, и преобразование не требуется. Я надеюсь испытать этот метод на практике в ближайшем будущем и напишу о результатах в этом блоге.

Собираем схему. Немного об электрических характеристиках LPC1114

Нам достаточно одной ножки, чтобы подключить светодиод. Из datasheet'а я понял, что среди всех "стандартных" GPIO-ножек выделяется PIO0_7 (28-я ножка микросхемы), которая служит "выводом с большим током" (High-current output driver), и может выдавать аж 20мА, что вполне достаточно для подключения светодиода через ограничивающий ток резистор. С остальными выводами ситуация такая: если через вывод вытекает ток равный 4мА, то напряжение на этом выводе падает на 0.4В, то есть выходное сопротивление этих выводов равно 0.4В/4мА = 100Ом. При этом суммарный потребляемый микроконтроллером ток (часть из которого будет выходить через выводы GPIO) не должен превышать 100мА. В общем, к другим ножкам тоже можно подключить светодиоды. Я собрал вот такую схему на макетной платe:


Плата завелась без проблем

Я использовал плату Arduino Uno в качестве источника питания на 3.3В

Заключение

Я думаю, что мне удалось показать, что работа с микроконтроллерами ARM Cortex M не обязательно сложна как с программной так и со схемотехнической точек зрения. Надеюсь, этот пост поможет кому-то вьехать в разработку устройств на этой архитектуре. В следующих постах я планирую написать про использование языка C и заголовочных файлов, предоставляемых производителем контроллера для более быстрой и удобной разработки.

Ссылки


LPC111x/LPC11Cxx User manual

Simple ARM example for LPC1114

Programming the LPC1114

STM32F4: GNU AS: Программирование на ассемблере (Часть 1)

четверг, 7 января 2016 г.

"Тепловизор" на основе Arduino и сенсора MLX90620

Некоторое время назад я приобрёл интересный прибор -- сенсор MLX90620. Это массив термостолбиков (thermopile) 16x4. По сути это такой "почти что" тепловизор, разве что с очень низким разрешением: 4 строки по 16 элементов. Другими словами он позволяет одновременно измерять поле темперутур в 64 точках на расстоянии. Мне конечно было очень интересно попробовать его в действии.

Датчик MLX90620 подключается к шине I2C, а значит его должно быть легко подключить к различным микроконтроллерам. Первым делом я попробовал соединить его с платой Raspberry Pi, но оказалось, что процессор Raspberry Pi (BCM2835) содержит аппаратный баг в реализации шины I2C, и штатными средствами подключить датчик к этой плате не получится.

Поэтому я переключился на подключение датчика к плате Arduino Uno. Для этого я собрал на макетной плате такую схему:







Датчик нужно питать напряжением 2.6 вольта. Самый простой способ подать такое напряжение -- запитать его от порта 3.3 вольта на Arduino через кремниевый диод. На диоде будет падать примерно 0.6 вольт, следовательно на ножке питания сенсора будет примерно 2.7 вольта.

С точки зрения шины I2C сенсор определяется как два устройства с адресами 0x50 и 0x60. По адресу 0x50 находится небольшое ПЗУ, в котором хранятся калибровочные константы для каждого из 64-х чувствительнх элементов-пикселей сенсора а так же для встроенного датчика внешней температуры. Из устройства по адресу 0x60 производится непосредственное чтение температур. Для перевода прочитанных значений в градусы нужно использовать формулы из спецификации к сенсору.

Само собой, я был не первым, кто пытался подружить Arduino и MLX90620. Я нашёл статью Олега Евсигнеева в которой есть ссылка на готовый работающий скетч для Arduino. К сожалению приведённая в этой статье ссылка на библиотеку i2cmaster, которая необходима для работы этого скетча, оказалась устаревшей. В итоге я использовал версию библиотеки отсюда.

Вообще-то там не один скетч, а два, которые надо залить в Arduino по очереди. Первый скетч MLX90620_alphaCalculator.ino читает из ПЗУ сенсора калибровочне константы alpha_ij и отправляет их в Serial port адруино. Эти константы следует увидеть в терминале (например, PuTTY), скопировать в буфер обмена и вставить во второй скетч: MLX90620_Example.ino. После этого надо скомпилировать второй скетч и залить его в Arduino на место первого.

Этот скетч будет выдавать в терминальное окно таблицу 4x16 с температурами в градусах Фаренгейта. Если вы, как и я, не любите градусы Фаренгейта, просто найдите такую строчку в скетче:

    Serial.print(convertToFahrenheit(temperatures[i]));

И замените её на

    Serial.print(temperatures[i]);
.

Работает! В терминале такой вывод:
25.82, 25.59, 24.78, 23.87, 26.47, 25.72, 24.15, 25.00, 26.51, 25.80, 23.65, 25.87, 27.85, 26.60, 25.93, 26.61,
27.78, 31.22, 31.05, 30.55, 28.85, 42.29, 58.05, 54.52, 28.77, 43.14, 59.59, 56.39, 29.16, 44.10, 59.48, 56.69,
29.06, 42.96, 59.27, 56.48, 28.35, 38.74, 49.77, 45.42, 26.55, 27.55, 29.37, 31.01, 26.92, 27.23, 27.23, 27.25,
25.02, 26.58, 27.24, 26.56, 26.09, 25.83, 25.13, 25.39, 26.54, 25.35, 25.76, 27.34, 26.52, 26.10, 26.11, 26.02,


Чтобы протестировать датчик я поставил горячий чайник примерно по центру поля зрения MLX90620, и получил такой вывод. Действительно, температура фона - 23-25 градусов, тогда как по центру - 50-60. Не совсем понятно откуда взялись высокие значения температуры в правом конце второй строки. Блик?

Имейте в виду, что заявленной точности сенсора 0.5К при частоте обновления "картинки" 1Гц недостаточно для "медицинских" применений. Кроме того (об этом даже упомянуто отдельно в спецификации) температура на поверхности открытого участка тела может сильно отличаться от ожидаемых 36-37 градусов цельсия.

среда, 9 декабря 2015 г.

Зацениваем многоклассовую логистическуя регрессию

Введение


Метод логистической регрессии один из простейших методов классификации в машинном обучении. Тем не менее он ряд сильных сторон и поэтому его не стоит сбрасывать со счетов. Во-первых логистическая регрессия выдаёт не только предполагаемы класс объекта, но и оценку вероятности принадлежности объекта к тому или иному классу. Причём оценка эта выдаётся в виде гладкой функции. Во-вторых этот метод очень просто реализовать и понять: вот, например, реализация из документации Apache Spark:
В третьих, натренированная модель представляет собой вектор весов, которые просто интерпретировать.
Когда объясняют логистическую регрессию, чаще всего ограничиваются бинарной классификацией (с двумя классами). Например, эта тема прекрасно раскрыта в курсе Andrew Ng на Coursera. Однако нередко нужно различать более чем 2 класса. В этом случае, например, в курсе Andrew Ng предлагается стратегия one-vs-all. То есть для различения n классов надо натренировать n бинарных классификаторов по одному на каждый класс объектов. Поэтому мне кажется интересным с самого начала рассмотреть именно вариант с произвольным числом классов.

Теория (нестрогое изложение)


Пусть наши наблюдения описываются некоторым количеством признков ${\bf f} = \{f_1...f_m\}$. Обозначим буквой $Y$ класс объекта ($Y \in 1...k$). Будем моделировать вероятность с помощью экспоненциальной функции от взвешенной суммы признаков
$$P(Y=l) \propto \exp(w^{(l)}_1 f_1 + w^{(l)}_2 f_2 + ... + w^{(l)}_m f_m ).$$
То есть для каждого класса $l$ имеется свой набор весов $\bf w^{(l)}$. Вероятность должна быть нормирована на 1: $$P(Y=l) = \frac{\exp(w^{(l)}_1 f_1 + w^{(l)}_2 f_2 + ... + w^{(l)}_m f_m )}{\sum\limits_{j=1}^k \exp(w^{(j)}_1 f_1 + w^{(j)}_2 f_2 + ... + w^{(j)}_m f_m )}.$$ Задача обучения сводится к тому, чтобы подобрать такие веса $\bf w^{(l)}$, при которых будет достигаться оптимальное разделение классов. Для этого берётся "обучающее множество": некоторое количество экземпляров данных, на которых известны $Y$ и $\bf f$. Затем изменяя значения $\bf w^{(l)}$ максимизируют функцию правдоподобия на этих данных (на практике максимизируют логарифм правдоподобия, так как в этом случае вычисления проще).
Обозначим ${\bf W} = \left( {\bf w}^{(1)}, {\bf w}^{(2)}, ... , {\bf w}^{(k)}\right)$, ${\bf F}=\left({\bf f}, {\bf f} ... {\bf f} \right)$ (всего $k$ повторений ${\bf f}$), ${\bf F}\left(Y=j\right)=\left({\bf 0}, {\bf 0} ... {\bf f} ...{\bf 0} \right)$ (здесь положены равными нулю все ${\bf f}$, кроме той, что стоит на $j$-й позиции). Такая запись позволяет представить веса модели и признаки в виде векторов рамерностью $mk$, что на мой взглят облегчает объяснения.
$$ P(Y=l|{\bf F; W}) = \frac{\exp\left({\bf W \cdot F}\left(Y=l\right)\right)}{\sum\limits_{j=1}^k\exp\left({\bf W \cdot F}\left(Y=j\right)\right)} $$ Итак, запишем логарифм функции правдоподобия: $$ L\left({\bf W}\right) = \sum\limits_{i=1}^n \ln P\left(Y=Y_i|{\bf F}_i; {\bf W}\right). $$ Здесь $i$ обозначает номер примера в тренировочном множестве, $Y_i$ -- его класс, ${\bf F}_i$ -- его набор признаков. Продиференцировав вражение для нормированной верятности $P\left(Y|{\bf F; W}\right)$ получим: $$ \frac{\partial L\left({\bf W}\right)}{\partial {\bf W}} = \sum\limits_{i=1}^n{\bf F}_i\left(Y=Y_i\right) - \sum\limits_{i=1}^n\sum\limits_{j=1}^kP\left(Y=j|{\bf F}_i; {\bf W}\right) $$ Зная градиент функции можно искать её максимум, например методом градиентного подъёма. Ещё одно замечательное свойство многоклассовой логистической регрессии -- выпуклость возникающей задачи оптимизации (см Collins, стр 16). Значит мы сразу найдём глобальный максимум.

Для того чтобы контролировать переобучение к функции правдоподобия добаволяется дополнительный член: штраф за слишком большие веса модели ${\bf W}$. Мы будем рассматривать только квадратичный штраф: $$ L\left({\bf W}\right) = \sum\limits_{i=1}^n \ln P\left(Y=Y_i|{\bf F}_i; {\bf W}\right) - \frac{\lambda}{2}{\bf W^T}\cdot{\bf W}, $$ соответственно, в производной появится дополнительный член $-\lambda {\bf W}$.

Реализация


Для экспериментов я реализовал многоклассовый классификатор на C++ в виде консольной программы. В приоритете была простота, поэтому для нахождения минимума целевой функции при обучении используется обычный градиентный спуск с постоянным шагом. Хотя на практике применяют методы получше. Из этих же соображений для представления векторов я использовал класс valarray из стандартной библиотеки C++. Мне больше нравится библиотека Eigen, но пока что я не хочу связываться с подключением внешних библиотек.

Функция featurize соответствует вычислению ${\bf F}_i\left(Y=Y_i\right)$.
Ключевой момент в обучении классификатора -- вычисление градиента целевой функции. За это отвечает такой кусок кода:


Эксперименты с обучением на реальных данных


Чтобы попробовать метод и его реализацию на реальных данных, я взял два датасета из репозитория UCI. Один из них - известнейший датасет Iris (Ирисы Фишера), второй - датасет Wine . В первом предлагается определить тип ириса, используя в качестве параметров геометрические размеры элементов цветка (всего 4 параметра). Во втором наборе днных предлагается определить происхождение вина (категориальная переменная, которая принимает значения 1, 2, 3) по 13 параметрам, полученным в результате химического анализа вина. Надо сказать, что оба набора данных не из тех, что "бросают вызов". Практически любой классификатор может показать на них неплохие результаты без какой-то особой обработки данных напильником.

Итак, сначала убедмся, что классификатор может обучиться разделять объекты в тренировочном наборе данных. Для датасета Iris будем использовать данные "как есть", слегка поменяв формат файла (заменим запятные в качестве разделителя полей на символы табуляции) и заменив названия классов на числовые значения: "Iris setosa" => 0,  "Iris virginica" => 1, "Iris versicolor" => 2. В *nix или cygwin окружении можно использовавть такую команду (в одну строчку)

>sed -e 's/,/\t/g;s/Iris-setosa/0/;s/Iris-versicolor/1/;s/Iris-virginica/2/' ./iris.data  > ./iris.tsv

или произвести замену в своём любимом текстовом редакторе.
Тренируем модель

>./mlr train --verbose --modelfile irismodel.txt --datafile iris.tsv --featureincolumns 0,1,2,3 --labelincolumns 4 --numclasses 3 --learnrate 0.2

Теперь посмотрим на матрицу неточностей (confusion matrix) на тех же самых тренировочных данных:

>./mlr eval --modelfile irismodel.txt --datafile iris.tsv --featureincolumns 0,1,2,3 --labelincolumns 4

Получим такой вывод

50      0       0
0       49      1
0       1       49

В этой матрице 3x3 число в i-й строке и j-й колонке соответствует количеству объектов класса i которым классификатор назначил класс j. "Идеальный" классификатор должен назначать всем объектам класса i только лишь класс i и никакой другой, поэтому матрица неточностей должны быть диагональной. Недиагональные элементы соответствуют ошибкам классификации.

Мы видим, что классификатор ошибся лишь два раза . Можно сделать вывод что в данном случае классификатор хорошо настроился на разделение тренировочных данных. Несмотря на линейность мы можем хорошо разделять классы ирисов в пространстве признаков. Правда, пока что рано говорить, что получился хороший классификатор: возможно мы просто переобучились, и на новых данных ошибка будет велика. Поэтому проверим теперь наш классификатор с помощью процедуры кросс-валидации. Разделим данные в пропорции 70/30. Большую часть будем использовать для тренировки, а меньшую - для проверки качества.

>sort -R ./iris.tsv > iris.shuffled.tsv
>head -n 105 iris.shuffled.tsv > ./iris.train.tsv
>tail -n 45 iris.shuffled.tsv > ./iris.test.tsv

>./mlr train --verbose --modelfile irismodel.txt --datafile iris.train.tsv --featureincolumns 0,1,2,3 --labelincolumns 4 --numclasses 3 --learnrate 0.2

>

Теперь матрица неточностей выглядит так:

16      0       0
0       15      0
0       0       14

Можно сказать, что мы научили классификатор различать ирисы.

Теперь проделаем то же самое с датасетом Wine. Сначала как и прежде заменим запятые на символы табуляции в файле данных. Кроме того класс объекта представлен числами 1,2,3. У меня всё zero-based, поэтому надо замнить в первой колонке (у неё тоже индекс 0 а не 1): 1=>0, 2=>1, 3=>2.

>sed -e 's/,/\t/g;s/^1/0/;s/^2/1/;s/^3/2/' wine.dat > wine.tsv

Взглянув на колонки признаков, можно заметить что там встречаются значения разные по порядку величины. Например ... Поэтому неплохо было бы провети сначала нормализацию данных: с помощью сдвига и масштабирования привести их значения к 0, а разброс к некоторой одинаковой величине, например, 1. Под "разбросом" можно понимать либо разницу максимального и минимального значения, либо среднеквадратичное отклонение. Для нормализации данных я написал скрипт на Perl. Скрипт можно использовать так:

>perl normalize.pl stddev --in wine.tsv  --fields 1,2,3,4,5,6,7,8,9,10,11,12,13 > wine.normalized.tsv

Тренируем:

>./mlr train --verbose --modelfile wine.model.txt --datafile wine.normalized.tsv --featureincolumns 1,2,3,4,5,6,7,8,9,10,11,12,13 --labelincolumns 0 --numclasses 3 --learnrate 0.2
Смотрим матрицу неточностей: 
>./mlr eval --modelfile wine.model.txt --datafile wine.normalized.tsv --featureincolumns 1,2,3,4,5,6,7,8,9,10,11,12,13 --labelincolumns 0

59      0       0
0       71      0
0       0       48
Видим, что классификатор научился идеально разделять объекты в тренировочных данных. Можно сказать, что чем больше рамерность пространства признаков тем больше шансов, что данные можно будет разделить линейным классификатором.
Для чистоты эксперимента опять разделим данные на тестовую и тренировочную подвыборки.
>sort -R ./wine.normalized.tsv > wine.shuffled.tsv
>head -n 124 ./wine.shuffled.tsv > ./wine.train.tsv
>tail -n 53 ./wine.shuffled.tsv > ./wine.test.tsv

Посмотрим, что получилось.

>./mlr train --verbose --modelfile wine.model.txt --datafile wine.train.tsv --featureincolumns 1,2,3,4,5,6,7,8,9,10,11,12,13 --labelincolumns 0 --numclasses 3 --learnrate 0.2


>./mlr eval --modelfile wine.model.txt --datafile wine.test.tsv --featureincolumns 1,2,3,4,5,6,7,8,9,10,11,12,13 --labelincolumns 0

20      0       0
0       18      0
0       0       15

Внедиагональных элементов вообще не оказалось. Таким образом можно сделать вывод что классификатор научился уверенно определять происхождение вина по результатам химического анализа.

Заключение


Мы рассмотрели метод многоклассовой логистической регрессии. Применили его к реальным наборам данных и оценили способность линейного классификатора, натренированного по методу многоклассовой логистической регрессии разделять классы в этих наборах данных. Можно сделать вывод, что простые алгоритмы, такие, как линейный классификатор иногда работают очень неплохо. В продолжении я собираюсь обсудить оптимизацию приведённого кода, рассмотреть обобщения метода и попробовать применить его к наборам данных посложнее.

Ссылки




воскресенье, 22 ноября 2015 г.

Делаем простой парсер химических формул с помощью JavaCC

Я продолжаю очень медленно и по чуть-чуть пилить "химический поиск" ChWiSe.Net. И хотя он пока что в стостоянии протипа, некоторый ненулевой поток запросов есть. Я заметил, что пользователи не всегда хотят использовать SMILES-нотацию. Думаю, она не особо известна за пределами сообщества професионалов-химиков и хемоинформатиков. Вместо этого пользователи пытаются найти вещества по обычной полуструктурной формуле, например, всем известный этиловый спирт пытаются найти запросом C2H5OH вместо SMILES-формулы CCO. Что ж, надо поддержать и такие запросы.

Нам понадобится создать парсер таких формул. Для формул вида (NH4)2CO3, Al2(SO4)3, (CH3)2O или CH3COOH необходимо правильно разобрать все скобочки, числовые коэфициенты, выделить отдельные химические элементы и группы атомов, такие, как сульфат-анион, и использовать эти выделенные составляющие формулы для поиска похожих веществ.

Я решил реализовать парсер формул в соответствии с очень простой грамматикой: терминалами являются круглые скобки, числа, символы химических элементов и "стандартные" группы атомов ( COOH, OH, SO4, и.т.д. ). Правил вывода всего пять:

ELEMENT -> elementToken
ELEMENTARYGROUP -> elementaryGroupToken
GROUP -> '(' FORMULA ')'
TERM ->  (GROUP|ELEMENTARYGROUP|ELEMENT) NUMBER?
FORMULA -> TERM*

Человеческим языком это можно описать так (снизу вверх):
  • Формула (FORMULA) это некоторое количество идущих друг за другом "термов" (TERM). Звёздочка после элемента обозначает некоторое (возможно, нулевое) количество повторений этого элемента.
  • Терм (TERM) это или группа (GROUP) или "элементарная группа" (ELEMENTARYGROUP), или элемент (ELEMENT) после чего может следовать число (NUMBER). Числа может и не быть: знак вопроса после NUMBER обозначает что этот элемент необязателен.
  • Группа (GROUP) - это формула (FORMULA) в скобках.
  • Элементарная группа (ELEMENTARYGROUP) - терминал elementaryGroup
  • Элемент (ELEMENT) - терминал символа химического элемента
Эта граммтика столь проста, что всю логику разбора формулы можно написать руками, но я так делать не стал: такой код будет плохо поддерживаемым, менять грамматику будет сложно. Есть специальные инструменты, которые позволяют создавать парсеры на вашем любимом языке программирования: lex/yacc, flex/bison, javacc, antlr, boost.spirit и многие другие. Я бы выделил две группы таких инструментов: в первой группе (lex/yacc, flex/bison, javacc, ANTLR) грамматика описывается в отдельном файле в специальном, удобном для описания грамматик формате, а затем специальные трансляторы преобразуют файл описания грамматики в код, который будет осуществлять разбор строк в соответствии с грамматикой. Другая группа инструментов основана на использовании возможностей соответствующего языка программирования. Так, например, в Boost.Spirit с помощью шаблонной магии и перегрузки операторов грамматика описывается прямо в C++-коде.

Для своих целей я выбрал JavaCC (Java Compiler Compiler). Этот простой генератор парсеров является рабочей лошадкой во многих проектах. Из open source могу упомянуть парсеры поисковых запросов Lucene и Solr.

Описание парсера состоит из двух основных частей: описания сканера и собственно парсера. Сканер осуществляет выделение из поступающей на вход строки отдельных терминалов. У меня он определяется так:

Секция TOKEN определяет известные парсеру типы токенов. У меня их 5 типов: открывающаяся скобка, закрывающаяся скобка, число (NUM), символ химического элемента (ELEMENT), и то, что я называю "элементарная группа" (ELEMENTARYGROUP).

Во второй важнейшей часть необходимо описать правила вывода парсера. В JavaCC эти правила описываются "декларативно" с примесью Java-кода. Пять правил, описанных выше на языке JavaCC записывются так:

Файл описания грамматики целиком можно посмотреть здесь.

Каждое такое правило сгенерирует одноимённый метод, который будет сопостовлять фрагменту строки соответствующий элемент грамматики. В случае успешного сопоставления будет вызываться пользовательский java-код. В моём случае пользовательский код строит некое синтаксическое дерево формулы. 


Вот собственно и всё, мы написали простой парсер.

Генерация парсера

JavaCC - это утилита командной строки которая обрабатывает описание грамматики и генерирует собственно парсер: Java-класс, который при конструировании получает на вход объект класса java.io.Reader. Вызвать геренацию парсера из командной строки можно так (это одна строка):

java -cp <ПУТЬ К ДИРЕКТОРИИ JavaCC>/bin/lib/javacc.jar javacc SemistrucutredFormulaParser.jj

Вызов этой команды сгенерирует несколько *.java файлов:


ParseException.java
SemistructuredFormulaParserConstants.java
SemistructuredFormulaParser.java
SemistructuredFormulaParserTokenManager.java
SimpleCharStream.java
Token.java
TokenMgrError.java

Эти файлы можно включить в свой проект руками, но обычно этот процесс автоматизируют. Например, с помощью системы сборки Maven.



Проект Maven


Шаги, описанные выше могут быть включены в проект maven. Следует положить *.jj файл в поддиректорию $PROJECT_ROOT/src/main/javacc, где $PROJECT_ROOT - корневая директория проекта. В pom.xml следует включить такую секцию:




пятница, 16 октября 2015 г.

Перевод проекта ChWiSe.Net на Backbone.js

После относительно длительного перерыва я продолжил не спеша развивать свой проект "химического поиска" ChWiSe.Net. В какой-то момент я обнаружил, что браузерный Javascript-код начинает быть запутанным и добавление новой функциональности стало нелёгкой задачей. Я решил, что пора провести мини-рефакторинг этой части. При этом возникла мысль использовать какой-либо Javascript-фреймворк. Сейчас существуюет большое количество Javascript-фреймворков, призванных облегчить и струкутрироровать клиентский код в AJAX-приложениях. При этом когда встаёт проблема выбора, если ты не знаком с парой-тройкой фреймворков, фразы типа "у каждого фреймворка своя ниша" вызывают чувство беспомощности. Совершенно непонятно, конкретно мой проект в какой нише, и какой фреймворк подойдёт лично мне лучше всего. В моём конкретно случае я остановился на Backbone.js, и, кажется, не ошибся. В этом посте я собираюсь написать о своём опыте перевода браузерной части несложного проекта с "просто javascript" на фреймворк Backbone.js. Сам по себе этот пост вряд ли годится в качестве хорошего туториала по Backbone.js (хотя я постарался дать ссылки на такие туториалы), однако тут разбира один частный случай веб-сайта, в котором Backbone.js работает хорошо.

 Одна из ключевых деталей, которых не хватало в имевшейся реализации - url hash-навигация (или просто hash навигация). Url hash навигация это такой приём в AJAX-приложении, когда ваши действия на веб-страничке, такие как поиск, центрирование карты, выбор объекта или, скажем, фильтра по колонке какой-нибудь таблицы фиксируются адресной строке браузера после символа '#'. Вы можете выполнтить какие-то действия, например поискать что-то в вашем любимом поисковике, затем скопировать URL страницы с результатами поиска и отправить ссылку вашему знакомому. Он в свою очередь, перейдёт по этой ссылке и увидит примерно то же что видели и вы: например, на страничке сайта поисковика в поисковом поле будет тот же самый запрос, а результаты будут примерно теми же (жизнь как всегда сложнее, современные поисковые движки могут давать персонализированные результаты, поэтому пользователь Б на один и тот же запрос может увидеть другие результаты, чем пользоваетль А, но сам запром будет тем же самым). Ещё одно немаловажное преймущество страничек с url hash-навигацией - адекватная работа кнопки "назад" в браузерах. Поскольку мой сайт тоже по сути дела "поисковик", для меня это фишка была обязательна.

Между делом я почитывал статьи про современные JS-фреймворки, и больше всего заинтересовался Backbone.js. Во-первых он поддерживает url hash навигацию. Хотя для реализации url hash навигации можно обойтись и без фреймворков, Backbone.js предлагает для этого логичную и легко поддерживаемую структуру кода. Во-вторых, как мне показалось, моя веб-страничка по структуре идеально вписывается в "типовую архитектуру" Backbone.js приложения (об этом ниже). Ну и, наконец, Backbone.js очень хвалили за "легковесность" и отсутствие "магии": исходный код Backbone.js относительно невелик и, как сообщают, его несложно прочесть и понять. Надо учитывать, что Backbone.js использует jQuery и Underscore.js. Поэтому при добавлении в ваш проект зависимость от Backbone.js их размер тоже стоит учесть. Если вы и так использовали эти библиотеки, то объём данных, скачиваемых браузером при заходе на вашу страницу увеличится только на размер самого скачиваемого файла Backbone.js.

В моём конкретном случае клиентская часть уже и так использовала jQuery, так что мне пришлось притянуть в проект лишь библиотеку Underscore.js. Зато я смог отказаться от шаблонной библиотеки EJS, так как в Underscore.js уже есть движок шаблонов.

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

Именно для таких страничек Backbone.js подходит очень хорошо! В общем, я решил переделать клиентскую часть с использованием Backbone.js.

Для создания таких сайтов Backbone предлагает такие абстракции:

"Model" (модель) - обычно это внутреннее представление информации о некоторой еденичной сущности: цена и характеристики товара, координаты и тип объекта на карте, "поля" некой карточи в картотеке - всё это хорошие кандидаты на то, чтобы быть смоделированными моделью Backbone.js.

"Collection" - по сути это тоже модель, которая содержит в себе другие модели. Удобно думать о коллекции как о простом массиве. Обычно Collection соответствует различным спискам, а отдельные элементы в модели-массиве - отдельным записям в этих списках. При этом модели совсем необязательно быть элементом коллекции. Например, у меня в ChWiSe.Net есть два класс моделей "нижнего уровня". Одна из них, ChWiSe.Models.SearchResultCompound отвечает за внутреннее представление одного результата поиска. Другая, ChWiSe.Models.ServerMessage отвечает за представление "сообщения от сервера", такие как ошибки, исправлнеие опечаток и.т.д. Так же есть модель-коллекция, которая содержит все результаты поиска ChWiSe.Models.SearchResults. Элементы этой коллекции - модели нижнего уровня типа ChWiSe.Models.SearchResultCompound . Сообщения от сервера класса ChWiSe.Models.SearchResultCompound  так же содержаться в этой модели-коллекции просто в виде дополнительного поля, а не элемента коллекции.

Модели Backbone.js обеспечивают не только представление ваших данных в клиентской части веб-приложения, но и взаимодействие с сервером.  Можно вносить свои коррективы в этот процесс переопределяя в ваших моделях функции url, fetch и parse.

"View" (вид) - отвечает за "рендеринг" модели. Под рендерингом подразумевается генерация html-разметки, которая будет отображаться в браузере. Я уже упомянул, что мне понадобилось 3 различных модели. Я сделал столько же представлений: ChWiSe.Views.SearchResultCompound, ChWiSe.Views.ServerMessage и ChWiSe.Views.SearchResults. Проилюстрирую роль моделей и представлений в моём проекте следующими рисунками:






"Router" (маршрутизатор). Именно этот компонент отвечает за hash-навигацию. В объекте-маршрутизаторе задаётся соответствеие шаблона hash-строки в адресной строке браузера и javascript-функции. Наприемер, у меня таблица маршрутизации очень простая и пока что выглядит так:

routes: { // sets the routes
    "" : "showWelcomePage",
    "search?*queryString" : "search",
  },

Смысл очень простой: напримре, если в строке браузера появится URL, заканчиваюзийся на search?sq=ethanol, то вызовется JS-функция search. При этом в качестве аргумента её передастся строка "sq=ethanol". В скрипте (например, в обработчике нажатия на кнопку "Search") мы не вызываем функцию, отвечающую за поиск непосредственно, а "переходим" на соотвтетствующий URL:

ChWiSe.router.navigate("search?" + encodedURIfragment, {trigger: true} );

Вот и всё!

В качестве ещё одного дополнительного украшения я сделал бесконечную прокрутку результатов (похоже на то, как сделано в Facebook, Вконтакте и DuckDuckGo) вместо щёлкания по кнопкам-ссылкам с номерами страниц. Тут мне помогла библиотека infiniScroll.js.

Заключение:

В целом Backbone.js мне понравился: большое сообщество, хорошие туториалы, относительная простота в освоении - всё это несомненные преимущества этого фреймворка. Ещё один плюс - типовая архитектура клиентского кода. Так что побочным положительным эффектом от перехода на Backbone может стать лучшая структурированность JavaScrip-кода вашего проекта.

Ссылки:

Неплохая статья для начала чтобы пощупать Backbone.js

Пожалуй наиболее всеобъемлющее введение в Backbone.ks. Рекомендую для продолжения знакомства с Backbone.js сразу послед прочтения предыдущей статьи

Конечно же, документация на официальном сайте