В течение последних нескольких месяцев мы с Джен играли с проблемой Kaggle Digit Recognizer — «конкурсом», созданным для ознакомления людей с машинным обучением.
Цель этого конкурса — сделать снимок одной рукописной цифры и определить, что это за цифра.
Вам предоставляется входной файл, который содержит несколько строк, каждая из которых содержит 784 пиксельных значения, представляющих изображение 28 × 28 пикселей, а также метку, указывающую, какое число фактически представляет это изображение.
Одним из алгоритмов, которые мы опробовали для этой проблемы, был вариант кластеризации k-средних, в соответствии с которым мы взяли значения в каждом пиксельном местоположении для каждой из меток и получили среднее значение для каждого пикселя.
Таким образом, мы бы получили что-то вроде этого:
Label 0: Pixel 1: 214, Pixel 2: 12, Pixel 3: 10...Pixel 784: 23 Label 1: Pixel 1: 234, Pixel 2: 0, Pixel 3: 25...Pixel 784: 0 Label 2: Pixel 1: 50, Pixel 2: 23, Pixel 3: 20...Pixel 784: 29 ... Label 9: Pixel 1: 0, Pixel 2: 2, Pixel 3: 10...Pixel 784: 1
Когда нам нужно было классифицировать новое изображение, мы рассчитывали расстояние между каждым пикселем нового изображения по отношению к эквивалентному пикселю каждой из 10-пиксельных усредненных меток, а затем определяли, к какой метке наше новое изображение было ближе всего.
Мы начали с некоторого кода для загрузки данных обучающего набора в память, чтобы мы могли поиграть с ним:
(require '[clojure.string :as string])
(use 'clojure.java.io)
(defn parse [reader]
(drop 1 (map #(string/split % #",") (line-seq reader))))
(defn get-pixels [pix] (map #( Integer/parseInt %) pix))
(defn create-tuple [[ head & rem]] {:pixels (get-pixels rem) :label head})
(defn parse-train-set [reader] (map create-tuple (parse reader)))
(defn read-train-set [n]
(with-open [train-set-rd (reader "data/train.csv")]
(vec (take n (parse-train-set train-set-rd)))))
Одна вещь, которую мы узнали, заключается в том, что полезно просто иметь возможность поместить небольшое подмножество набора данных в память, а не загружать все это сразу. В итоге я несколько раз ломал свой терминал, оценивая 40000 строковый файл в буфере слизи — не очень хорошая идея!
Чтобы получить первый ряд, мы сделаем это:
user> (first (read-train-set 1))
{:pixels (0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0...), :label "0"}
Мы написали следующий код, чтобы определить средние значения пикселей для каждой из меток:
(def all-the-data (read-train-set 1000))
(defn find-me-all-the [number] (filter #(= (str number) (:label %)) all-the-data))
(defn mean [& v]
(float
(/ (apply + v) (count v) )))
(defn averages [rows] (apply map mean (map :pixels rows)) )
(def all-the-averages
(map vector (range 0 9) (map #(averages (find-me-all-the %)) (range 0 9))))
В основном это говорит само за себя, хотя мы должны были использовать float в расчете среднего, чтобы получить десятичное значение, а не дробь.
Джен также придумал изящным способом использования применяется в среднем функции для сопоставления средней функции через каждый отдельный пиксель.
Мне было легче следить, когда мы запускали функцию с меньшим набором данных:
user> (averages [ {:pixels [1 2 3]} {:pixels [4 5 6]}])
(2.5 3.5 4.5)
Это распространяется на это:
user> (apply map mean [[1 2 3] [4 5 6]])
Что концептуально то же самое, что делать это:
user> (map mean [1 2 3] [4 5 6])
Мы можем получить средние значения для метки «0» следующим образом:
user> (first all-the-averages)
[0 (1.317757 3.3551402 6.196262 7.373831155...74767 171.61682 147.51402 96.943924 48.728973 22.299065 3.037383 )]
Чтобы определить, к какой метке подходит неподготовленный набор пикселей, мы написали следующие функции:
(defn distance-between [fo1 fo2]
(Math/sqrt (apply + (map #(* % %) (map - fo1 fo2)))))
(defn find-gap [averages unranked-value]
(vector (first averages) (distance-between (second averages) unranked-value)))
(defn which-am-i [unranked-value]
(let [all-the-gaps (map #(find-gap %1 unranked-value) all-the-averages)]
[(ffirst (sort-by second all-the-gaps)) all-the-gaps]))
distance -ween находит евклидово расстояние между значениями пикселей, затем find-gap использует его, чтобы найти расстояние от каждого обученного набора меток пикселей до набора тестовых данных, и мы можем затем вызвать which-am-i, чтобы выяснить, какое Для метки новый набор пикселей следует классифицировать как:
user> (first test-data)
(0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0...)
user> (which-am-i (first test-data))
[0 ([0 1763.5688862988827] [1 2768.1143197890624] [2 2393.9091578180937]
[3 2598.4629450761286] [4 2615.1233720558307] [5 2287.1791665580586]
[6 2470.096959417967] [7 2406.0132574502527] [8 2489.3635108564304] [9 2558.0054056506265])]
Функция which-am-i сначала возвращает свой прогноз, а затем также включает расстояние от набора тестовых данных до каждой из обученных меток, чтобы мы могли сказать, насколько близко это было к классификации как к чему-то еще.
Мы получили точность 80,657% при классификации новых значений с помощью этого алгоритма, что не очень хорошо, но не кажется слишком плохим, учитывая, насколько он прост и что мы смогли его запустить и запустить за пару часов.
Код на GitHub Джен , если вы заинтересованы в том , чтобы больше.