ML-Блиц: разбор задач первого квалификационного раунда

В реализации я буду использовать метод Уэлфорда, т.к. он нравится мне больше, чем вычисления по «стандартным» формулам. Кроме того, я не буду использовать numpy и какие-либо другие библиотеки, чтобы продемонстрировать, что для получения решения достаточно знания элементарных конструкций языка python.

Итак, нам понадобится уметь инкрементально вычислять среднее и сумму квадратов отклонений. Для этого опишем два класса:

class MeanCalculator:
    def __init__(self):
        self.Count = 0.
        self.Mean = 0.

    def Add(self, value, weight = 1.):
        self.Count += weight
        self.Mean += weight * (value - self.Mean) / self.Count

    def Remove(self, value, weight = 1.):
        self.Add(value, -weight)

class SumSquaredErrorsCalculator:
    def __init__(self):
        self.MeanCalculator = MeanCalculator()
        self.SSE = 0.

    def Add(self, value, weight = 1.):
        curDiff = value - self.MeanCalculator.Mean
        self.MeanCalculator.Add(value, weight)
        self.SSE += weight * curDiff * (value - self.MeanCalculator.Mean)

    def Remove(self, value, weight = 1.):
        self.Add(value, -weight)

Теперь нужен класс для хранения и обработки обучающей выборки. Для начала опишем его поля и метод ввода:

class Instances:
    def __init__(self):
        self.Items = []
        self.OverAllSSE = SumSquaredErrorsCalculator()

    def Read(self):
        fin = open('stump.in')

        for line in fin.readlines()[1:]:
            x, y = map(float, line.strip().split())
            self.Items.append([x, y])
            self.OverAllSSE.Add(y)
        self.Items.sort()

Одновременно с вводом данных мы сразу формируем структуру SumSquaredErrorsCalculator для всех объектов выборки. После загрузки всей выборки мы сортируем её по неубыванию значений признаков.

Теперь самое интересное: метод для нахождения оптимальных значений параметров.

Начнём с инициализации. В начальном состоянии все объекты находятся в правой подвыборке. Тогда параметр $a$ может быть равен чему угодно, параметр $b$ равен средней величине ответов во всей выборке, а параметр $c$ таков, чтобы все объекты выборки находились правее него.

Кроме того, инициализируем переменные left и right — они будут хранить статистики для левой и правой подвыборок, соответственно.

left = SumSquaredErrorsCalculator()
right = self.OverAllSSE

bestA = 0
bestB = right.MeanCalculator.Mean
bestC = self.Items[0][0]

bestQ = right.SSE

Теперь переходим к инкрементальному алгоритму. Будем обрабатывать элементы выборки по одному. Каждый элемент переносится в левое подмножество. Затем нужно проверить, что соответствующее разбиение действительно существует — то есть, значение признака отличается от значения признака следующего объекта.

for i in range(len(self.Items) - 1):
    item = self.Items[i]
    nextItem = self.Items[i + 1]

    left.Add(item[1])
    right.Remove(item[1])

    if item[0] == nextItem[0]:
        continue

    a = left.MeanCalculator.Mean
    b = right.MeanCalculator.Mean
    c = (item[0] + nextItem[0]) / 2

    q = left.SSE + right.SSE

    if q < bestQ:
        bestA = a
        bestB = b
        bestC = c
        bestQ = q

Теперь осталось только скомпоновать код для получения решения:

instances = Instances()
instances.Read()
a, b, c = instances.BestSplit()
print >> open('stump.out', 'w'), a, b, c

Замечу, что представленное решение на python действительно принимается системой, но оно подходит к верхней границе по времени решения: самые большие тесты требует порядка 800 миллисекунд на исполнение. Конечно, с использованием дополнительных библиотек можно добиться намного более впечатляющей производительности.

Поэтому я также реализовал предложенный алгоритм на C++. Это решение затратило в худшем случае 60 миллисекунд на один из тестов.

© Habrahabr.ru