воскресенье, 13 ноября 2016 г.

"Низкоуровневое" программирование Arduino DUE без использования среды Arduino

Если вы работаете с платой Arduino Due, у вас может возникнуть необходимость или желание избавится либо от среды разработки либо от библиотеки Arduino. Причины могут быть разными: например, вам нужно использовать язык C, а не Arduino/C++. Или плата Arduino Due подключена к машине, к которой у вас есть доступ только через терминал и командную строку. А может быть вы просто хотите написать весь низкоуровневый код самостоятельно чтобы получше разобраться в тонкостях работы микроконтроллера Atmel sam3x8e.

Моя "среда разработки" состоит из платы Raspberry Pi к которой USB-кабелем может подключаться плата Arduino Due. Клавиатура и монитор отсутствуют, всё взаимодействие через SSH и коммандную строку. На Raspberry Pi установлена операционная система Raspbian (адаптированный под RPi Debian Linux). Raspberry Pi обеспечивает ряд удобств: во-первых, если что-то пойдёт не так, вы скорее всего сожжёте относительно недорогой RPi а не свой основной компьютер. Во-вторых RPi имеет на борту разные интерфейсы, такие как I2C, GPIO, UART итд, которые можно использовать для свзяи с Arduino DUE. При этом платы совместимы по уровню напряжения 3.3В. В третьих репозитории raspbian содержат всё необходимое для работы с Arduino Due.

Для работы нам необходим в первую очередь подходящий "тулчейн" -- набор инструментов состоящий из компилятора gcc и сопутствующих программ.  Установим его из репозитория Raspbian:

>sudo apt-get install gcc-arm-none-eabi

Также нам понадобится утилита коммандной строки bossac, которая служит для прошивания ARM-контроллеров фирмы Atmel. Ставим из репозитория и её:

>sudo apt-get install bossa-cli

В принципе у нас теперь есть необходимый минимум для того, чтобы написать какую-нибудь программу для sam3x8e/Arduino DUE собрать её и закинуть бинарник в память микроконтроллера, примерно так же как я описывал в предыдущих постах (раз, два) про lpc1114. Наш контроллер sam3x8e как и lpc1114 построен по архитектуре ARM Cortex M (только теперь у нас более навороченный Cortex M3, а в lpc1114 был Cortex M0). Нам придётся найти в даташите адреса регистров соответствующей периферии в sam3x8e и управлять ей записывая туда и считывая оттуда данные.

Заметно облегчить задачу может библиотека от производителя микроконтроллера. В простейшем случае она может предствалять из себя набор заголовочных файлоа (*.h файлов), в которых адресам регистров и числовым значениям, зоответствющим различным служебным маскам сопоставляется мнемонические константы. Кроме этого в библиотеку может входить набор относительно низкоуровневых функций и типов, сильно облегчающий и ускоряющий разработку. Библиотека Arduino построена "поверх" такой низкоуровневой библиотеки libsam. На сайте Atmel предлагается скачать библиотеку ASF (Atmel Software Foundation). Сравнивая эти две библиотеки я пришёл к выводу, что libsam либо предшественник либо старая версия ASF. В них довольно много одинакового кода.

Лично мне оказалось проще стартовать с библиотекой libsam, наверно потому что я "выкидывал лишнее" из библиотеки Arduino. Лучше всего воспользоваться готовым "скелетом" C/C++ программы для Ардуино. Мне удалось найти несколько таких "заготовок-скелетов".

http://www.atwillys.de/content/cc/using-custom-ide-and-system-library-on-arduino-due-sam3x8e/?lang=en -- статья о программировании платы Arduino Due на С. С этой странички можно скачать файлы для создания структуры директорий необходимой для сборки ваших C-файлов. Необходимо иметь установленную среду Arduino, сконфигурированную для работы с Arduino Due и интерпретатор php. Скрипт на php "наполняет" директорию проекта копирую в неё из директорий Arduino программы для сборки, закачивание прошивки а так же библиотеки sam3x8e.

https://github.com/sethm/arm_skeleton -- содержит libsam (заголовочные файлы и исходный код), Makefile и файл main.c, в который вам просто нужно поместить свой код. Об инструментах (arm-none-eabi-gcc и bossac) нужно позаботиться самостоятельно, но это не проблема так как они есть в репозитории Raspbian. Нет никаких внешних зависимостей, поэтому всё заводится без проблем. Я остановился на этом "каркасе" для своих проектов и здесь будем рассматривать именно его.

https://github.com/pauldreik/arduino-due-makefile -- по сути просто один Makefile, который позволяет собирать Arduino-проекты (из файлов *.ino) используя лишь командную строку.

Итак, устанавливает "каркасс" по второй ссылке.

>git clone https://github.com/sethm/arm_skeleton.git

в файле main.c реализована программа "плавного" мигания светодиодом с помощью ШИМ. Меняем на свою, супер-примитивную программу мигания светодиодом без всякого ШИМ и плавности.



Для редактирования исходного кода через ssh-подключение я использую редактор vim.


Собираем:

>make

Если сборка прошла без ошибок, то в директории проекта появится файл main.c.bin. Подключаем "programming port" Arduino Due к USB-выходу Raspberry Pi. Прошиваем:

>make prog

После того, как прошивка будет отправлена в контроллер, он будет автоматически сброшен и светодиод должен начать мигать в соответствии с нашей программой.

Итог: мы научились программировать Arduino Due "на низком уровне" без среды и библиотек Arduino.

среда, 21 сентября 2016 г.

Обучаем нейросеть распознавать рукописные цифры с помощью Torch7


В "песочнице" kaggle.com есть задача на классификацию рукописных цифр с использованием набора данных MNIST. Нужно обучить классификатор, которые по изображению написанной цифры размером 28x28 определит, что это за цифра. Это неплохая возможность для знакомства со свёрточными нейросетями, тем более, что именно они дают самую высокую точность на этой задаче.

Итак, имеется датасет размером 70000 цифр. Он условно разбит на тренировочное (42000 цифр) и тестовое (28000 цифр) множества. "Условно", потому что этот набор данных опубликован полностью, поэтому при желании можно найти разметку для тестовых данных, но мы не будем читить.

В качестве baseline возьмём такое простое, но очень эффективное решение пользователя Zhao Hanguang. Это решение использует связку метода главных компонент и SVM, работает очень быстро и всего на 35 главных компонентах даёт впечатляющие 98.243% точности. Задача минимум была побить это решение. Задача максимум - достичь 99% точности. Главной целью этих упражнений было знакомство с пакетом Torch7 и его возможностями "глубинного обучения". Сложным моментом было отсутствие в моём распоряжении машины с поддержкой CUDA, так что приходилось ограничиваться небольшими нейросетями и ждать результатов по несколько часов.

Torch предоставляет интерактивную среду с интерпретатором Lua: всё, что вы вводите интерпретируется как строка Lua и сразу выполняется. В ваших Lua-командах вы можете манипулировать объектами-тензорами (по сути просто многомерными массивами). Операции с тензорами написаны на C (есть реализации, которые используют GPU). Создатели Torch позиционируют его как "матлаб-подобную среду для машинного обучения".

Удобнее всего работать с Torch через веб-оболочку iTorch (аналогичную iPython).

Для подобных задач Torch содержит несколько пакетов: optim для поиска максимумов и минимумов, image работы и изображениями и nn для работы с нейросетями. За основу я взял пример из GitHub проекта Torch.

Итак, подключаем все модули:

--Import all dependencies
require 'nn'
require 'optim'
require 'csvigo'
require 'image'

Модуль csvigo помогает загружать данные в формате csv.

Загружаем данные

--Read datasets
train_data = csvigo.load({path = "~/data/train.csv", mode = "large"})
test_data = csvigo.load({path = "~/data/test.csv", mode = "large"})        

Извлекаем из данных отдельные колонки: признаки и разметку для тренировочных данных, а так же конвертируем таблицы Lua в тензоры Torch

--Create tensors for train and test data
train_feature_tensor = torch.Tensor(#train_data-1, 784)
train_label_tensor = torch.Tensor(#train_data-1, 1)
test_feature_tensor = torch.Tensor(#test_data-1, 784)
--Fill tensor with train and test data from file
for i=2,#train_data do
    train_feature_tensor[{i-1,{}}] = torch.Tensor(train_data[i]):narrow(1,2,784)
    train_label_tensor[i-1] = train_data[i][1]
end
 
for i=2,#test_data do
    test_feature_tensor[{i-1,{}}] = torch.Tensor(test_data[i])
end

Модуль image позволяет легко визуализировать картинки, представленные тензорами. Проверим, что наши цифры загрузились как ожидаолсь...

--Check data. We should see handwritten digits
itorch.image(train_feature_tensor[524]:resize(28,28))
itorch.image(test_feature_tensor[231]:resize(28,28))
 


Теперь задаём нейросеть.


--Create Neural network model
model = nn.Sequential()
 
model:add(nn.SpatialConvolution(1, 16, 5, 5)) --28x28x1 goes in, 24x24x16 goes out
model:add(nn.ReLU()) -- 
model:add(nn.SpatialMaxPooling(2, 2, 2, 2)) --24x24x16 goes in, 12x12x16 goes out
model:add(nn.Dropout(0.2))
 
model:add(nn.SpatialConvolution(16, 32, 5, 5)) --12x12x16 goes in, 8x8x32 goes out
model:add(nn.ReLU()) --
model:add(nn.SpatialMaxPooling(2, 2, 2, 2)) --8x8x32 goes in, 4x4x32 goes out
model:add(nn.Dropout(0.2))
 
model:add(nn.View(4*4*32))
model:add(nn.Linear(4*4*32, 64))
model:add(nn.ReLU()) --
model:add(nn.Dropout(0.2))
model:add(nn.Linear(64, 20))
model:add(nn.ReLU())
model:add(nn.Linear(20, 10))
model:add(nn.LogSoftMax())
 

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

In [14]:
model:getParameters():size()
Out[14]:
 47590
[torch.LongStorage of size 1]


Видим, что при обучении нам надо будет подобрать 47590 параметров. В Torch индексы отсчитываются от 1. Классы в классификации тоже нумеруются начиная от 1, а у нас из csv приходят 0-based метки классов. Поправим...

train_label_tensor = train_label_tensor + 1 

Во время своих первых экспериментов я столкнулся с такими проблемами: нейросети более простой архитектуры чуть-чуть не дотягивали до baseline, более сложные нейросети (например такая, как описана выше) переобучались: на тренировочном множестве они могли достигать 99%, но на тестовом отставали примерно на полпроцента. Переобучение налицо. Сначала я пытался решить проблему подбором параметра регуляризации и dropout-слоями, но 99% на тестовой выборке не достиг. Тогда я стал "деформировать" цифры из тренировочного множества чтобы увеличить разнообразие обучаюзих примеров. Для этого я нашёл готовую функцию на GitHub (автор - пользователь chsasank).

Кстати, это очень сильно увеличило время тренировки, и оно достигло нескольких часов.Но именно это дало в итоге последние доли процента на тестовых данных.

Обучение нейросети делал "явно". То есть в модуле nn нет "высокоуровневой" операции "тренировать нейросеть". Вместо этого приходится определять градиент функции стоимости и передвать его методам пакета optim. К счастью для этого не надо руками писать метод обратного распространения ошибки, он уже есть в пакете nn (nn,backward).

batchSize = 512
trainSize = train_feature_tensor:size()[1]
batchInputs = torch.Tensor(batchSize, 1, 28, 28)
batchLabels = torch.Tensor(batchSize)
lambda = 0.0005
 
-- this matrix records the current confusion across classes
confusion = optim.ConfusionMatrix(classes)
 
local params, gradParams = model:getParameters()
local optimState = {learningRate=0.04}
for epoch=1,450 do
  --local optimState = {learningRate=0.04 - epoch/1000.0 * 0.03}
  print("Epoch:"..epoch)
  for b = 1,math.ceil(trainSize/batchSize) do
    for i=1,batchSize do
      local originalImage = torch.Tensor(1, 28, 28)
      originalImage:copy(train_feature_tensor[(b*batchSize + i - 1) % trainSize + 1])
      batchInputs[i] = ElasticTransform(originalImage, 100, 10) 
      batchLabels[i] = train_label_tensor[(b*batchSize + i - 1) % trainSize + 1]
    end
 
    --Differentiation
    local function feval(params)
      gradParams:zero()
 
      local outputs = model:forward(batchInputs)
      local loss = criterion:forward(outputs, batchLabels)
      local dloss_doutput = criterion:backward(outputs, batchLabels)
      model:backward(batchInputs, dloss_doutput)
 
      --Regularization\n",
      loss = loss + 0.5 * lambda * torch.norm(params,2)^2 / batchSize;
      gradParams:add( params:clone():mul(lambda) )
 
      -- update confusion
      for i = 1,batchSize do
        confusion:add(outputs[i], batchLabels[i])
      end
 
      return loss,gradParams
    end
    optim.sgd(feval, params, optimState)
 
 -- Too big output
 --   print(confusion)
 --   print("Total valid: "..confusion.totalValid * 100)
    confusion:zero()
  end
end
 

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

confusion = optim.ConfusionMatrix(classes)
-- test function
function test(eval_features, eval_labels)
  print(eval_features:size())
  print(eval_labels:size())
  confusion:zero()
  -- test samples
  local preds = model:forward(eval_features)
 
  local maxval, pred_idx = torch.max(preds, 2)
 
  -- confusion:
  for i = 1,eval_features:size()[1] do
   -- print("Add: ", pred_idx[i][1] , eval_labels[i][1])  
    confusion:add(pred_idx[i][1], eval_labels[i][1])
  end
 
   -- print confusion matrix
   print(confusion)
   --confusion:zero()
end

train_features_resized = train_feature_tensor:resize(42000,1,28,28)
test(train_features_resized:narrow(1,1,20000), train_label_tensor:narrow(1,1,20000))

Функция test выведет матрицу ошибок. У меня в машине не хватало памяти для того, чтобы пропустить через сеть больше примрно 20000 примеров за раз, так что для матрицы ошибок я взял первые 20000 примеров. В тестовой выборке 28000 примеров, поэтому её пришлось разбить на два куска. Классификация осуществляется методом forward объекта-модели. Насколько я разобрался, метод forward меняет "на месте" (in-place) внутреннее поле объекта и возвращает ссылку на него, поэтому в результате выполнения такого кода

A = model:forward(F1)
B = model:forward(F2)

Объекты A и B будут одинаковы и равны результату model:forward(F2).
"Тетрадка" iTorch выложена на GitHub.

среда, 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

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

Заключение


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

Ссылки