Статьи

Решение K-средних для проблемы машинного обучения Kaggle

В течение последних нескольких месяцев мы с Джен играли с проблемой 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 Джен , если вы заинтересованы в том , чтобы больше.