Дневник на програмиста

Написах в моята първа статия за процедурите на линейна регресия, която добавих за Neo4j. Днес искам да обясня някои от вътрешните елементи и защо избрах да ги изградя по начина, по който го направих.

Дефинираните от потребителя процедури трябва да „запомнят“ информация между повикванията, за да изградят и поддържат модел на „машинно обучение“. Това надхвърля типичната функционалност на процедурите в Neo4j. В следващата статия изследвам ключовите подробности за внедряването на набор от функции и процедури, които създават, обучават, тестват, използватисъхраняват линейни модели на графични данни. Надявам се, че това помага за вашето разбиране на моята работа или за внедряването на ваши собствени подобни дефинирани от потребителя процедури.

За да видите тези дефинирани от потребителя функции и процедури в действие, прочетете моята предишна публикация, в която използвам линейна регресия, за да създам прогнозна цена за краткосрочни наеми в Остин, Тексас.

Целта

Искам да извърша линейна регресия на данните, които съм съхранил в Neo4j. Има много библиотеки с инструменти за създаване на линейни модели (Pandas и Numpy за Python, Commons Math за Java и др.), но за да ги използвам директно, трябва да експортирам данни от моята графика в друг софтуер. За да моделирам данните си от Neo4j, трябва да разширя функционалността на езика за графични заявки Cypher.

Предпочитаното средство за разширяване на Cypher е с дефинирани от потребителя „функции и процедури“. Те са написани на Java, изградени например с Maven, внедрени в базата данни Neo4j като JAR файлове и извикани от Cypher. Трябва да напиша процедура, която извършва линейна регресия върху данни от графика.

Какво прави този проблем интересен?

Първо, нека да разгледаме типична процедура в Neo4j: apoc.meta.graph. Изпълнението на CALL apoc.meta.graph() ще върне визуално представяне на схемата на графиката (вашият модел на данни). Например, ето резултатът от тази процедура в графиката за краткосрочни наеми от моята предишна публикация:

Тази процедура осъществява достъп до информацията, съхранена в графиката, за да определи основната структура. Други процедури променят графиката, като apoc.refactor.mergeNodes, която обединява множество възли в само един. Процедурите обаче обикновено не запомнят информация между извикванията, те просто произвеждат поток от изходи или модифицират данни.

Мога да създам процедура, която, подобно на apoc.meta.graph, има достъп до информацията в графиката, за да създаде линеен модел, без да съхранява никаква външна информация. Мога да предам всички данни наведнъж, да извърша изчисления на най-малките квадрати в процедурата и да върна параметрите на линейния модел. Но ако направя ново извикване на процедурата, тя вече ще е забравила модела, който току-що е създала.

Но какво ще стане, ако след това реша да добавя още данни към модела? Какво ще стане, ако искам да използвам голямо количество данни за обучение, които изискват твърде много памет, за да бъдат въведени като аргумент за една процедура?

Идея №1: Сериализиране!

Първият ми опит за решение е малко заобиколен, защото вместо да „запомня“ информация, процедурата съхранява модела в графиката, така че да може да бъде достъпен и актуализиран по-късно. Идеята е да се сериализира Java обектът на модела и да се съхрани байтовият масив в графиката между извикванията на процедурата. Ето визуално представяне на процеса на сериализация и десериализация:

Забележка: Използвам SimpleRegression от библиотеката Apache Commons Math. SimpleRegression извършва актуализиране на изчисления на входящи точки от данни, така че да не се записват отделни точки от данни. Вместо това той съхранява определена информация като средна стойност „y“, общ брой точки от данни и т.н. и актуализира тези стойности с всяка нова точка от данни. Така с всяка допълнителна точка от данни моделът извършва изчисления и се подобрява, без да увеличава използването на паметта. Резултатът: когато сериализираме обекта SimpleRegression, съответният байтов масив не е много голям (поне не се мащабира с размера на набора от данни!).

Първо написах следните помощни функции, така че през целия си проект да мога да конвертирам обекта SimpleRegression в byte[] и обратно. Тези изискват импортиране от java.io.*.

//Serializes the object into a byte array for storage
static byte[] convertToBytes(Object object) throws IOException {
    try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
         ObjectOutput out = new ObjectOutputStream(bos)) {
        out.writeObject(object);
        return bos.toByteArray();
    }
}
//de serializes the byte array and returns the stored object
static Object convertFromBytes(byte[] bytes) throws IOException, ClassNotFoundException {
    try (ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
         ObjectInput in = new ObjectInputStream(bis)) {
        return in.readObject();
    }
}

След това, следващия път, когато исках да редактирам модела, извлякох байтовия масив от графиката и го десериализирах.

try {
    ResourceIterator<Entity> n = db.execute("MATCH (n:LinReg {ID:$ID}) RETURN n", parameters).columnAs("n");
    modelNode = n.next();
    byte[] model = (byte[])modelNode.getProperty("serializedModel");
    R = (SimpleRegression) convertFromBytes(model);
} catch (Exception e) {
    throw new RuntimeException("no existing model for specified independent and dependent variables and model ID");
}

И след редактиране на модела, съхрани новото представяне на байт [] обратно в същия възел.

try {
    byte[] byteModel = convertToBytes(R);
    modelNode.setProperty("serializedModel", byteModel);

} catch (IOException e) {
    throw new RuntimeException("something went wrong, model can't be linearized so new model not stored");
}

Ако се интересувате, вижте „пълния код“. Имайте предвид, че тези предварителни реализации са много по-различни (и по-сложни!) от окончателната версия на моите процедури за линейна регресия по-долу.

Проблеми

Какво ще стане, ако искам, в името на дизайна, да отделя create модел, add данни и remove процедури за данни? Графичните бази данни непрекъснато получават актуализации, така че трябва да създам модел, който е толкова гъвкав, колкото графиката. Сериализацията и десериализацията изискват значително време. Можете да подадете няколко точки от данни наведнъж, за да ограничите броя на извикванията на процедурите (и броя пъти, в които моделът се съхранява и извлича), но имам нужда от по-добър начин за съхраняване на междинния модел между извикванията на процедури, така че да може да се актуализира толкова пъти, колкото ми трябва.

Идея №2: Статична карта

Статичните променливи в Java класовете, прилагащи процедурата, са активни, докато базата данни продължава да работи. Следователно можем да съхраняваме моделни обекти по име в статична карта. Моделите се съхраняват в процедурата, така че всяка стъпка на линейна регресия — създаване, добавяне на данни, премахване на данни и т.н. — е изолирана в отделна процедура, но променя същия SimpleRegression модел. Нещо като процедура add може да бъде извикано веднъж за всяка точка от данни без сериозни наказания за производителност. Това създава опростения дизайн, който желаем. С всяка изолирана стъпка процедурите са ясни и потребителят има по-голям контрол върху всяка стъпка от изграждането на линейния модел.

Моделите се съхраняват в статичен ConcurrentHashMap в един от Java класовете, използвани за изпълнение на процедурите: LRModel.java. Всеки път, когато се извика процедура, която трябва да получи достъп до модела, тя се извлича от modelsпо име с помощта на метода from. Използването на тази конкретна имплементация на карта позволява едновременен достъп от множество нишки.

private static ConcurrentHashMap<String, LRModel> models = new ConcurrentHashMap<>();
static LRModel from(String name) {
    LRModel model = models.get(name);
    if (model != null) return model;
    throw new IllegalArgumentException("No valid LR-Model " + name);
}

Сега трябва само да сериализираме и съхраним модела преди изключване на базата данни и да го заредим обратно в статичната памет на процедурата, когато базата данни се рестартира. Вижте пълното изпълнение на Github.

Ограничения

  • Ако базата данни се изключи неочаквано, статичните променливи ще бъдат изчистени и моделът ще бъде загубен. Може да е добра идея да имате резервна опция, при която моделът се сериализира и записва на редовни интервали от време. След повреда на базата данни, рестартирайте базата данни и изградете отново модела.
  • Сериализацията не е най-добрият начин за запазване на модела, защото ако нещо се промени в следващата версия на Commons Math, актуализираната версия може да не разпознае сериализиран SimpleRegression обект от преди.
  • Статистическите данни за тестови данни не се съхраняват между изключване/рестартиране на базата данни. В идеалния случай бих внедрил сам актуализиращата проста регресия, вместо да използвам Commons Math, и след това бих съхранил цялата необходима информация относно данните за обучение и тестване, вместо да съхранявам сериализирано SimpleRegression.

Подобрете моята работа!
Ако имате идеи, по-стабилни от съхранение в статични карти и сериализация, уведомете ме или ги приложете сами! Предизвиквам ви да подобрите работата ми. Бих искал да обсъдим възможностите, просто ми изпратете съобщение на LinkedIn или @ML_auren. наздраве!