MDL-60944 analytics: Include trained ML models

This commit is contained in:
David Monllaó 2019-02-01 06:52:42 +01:00
parent e4453adc55
commit c70a7194f4
14 changed files with 513 additions and 232 deletions

View File

@ -45,7 +45,7 @@ class import_model extends \moodleform {
$mform->addElement('header', 'settingsheader', get_string('importmodel', 'tool_analytics'));
$mform->addElement('filepicker', 'modelfile', get_string('file'), null, ['accepted_types' => '.json']);
$mform->addElement('filepicker', 'modelfile', get_string('file'), null, ['accepted_types' => '.zip']);
$mform->addRule('modelfile', null, 'required');
$mform->addElement('advcheckbox', 'ignoreversionmismatches', get_string('ignoreversionmismatches', 'tool_analytics'),

View File

@ -64,7 +64,7 @@ class helper {
*
* @param string $title
* @param \moodle_url $url
* @param \context|false $context Defaults to context_system
* @param \context|null $context Defaults to context_system
* @return null
*/
public static function set_navbar(string $title, \moodle_url $url, ?\context $context = null) {

View File

@ -54,7 +54,10 @@ if ($mform->is_cancelled()) {
// Converting option names to class names.
$targetclass = \tool_analytics\output\helper::option_to_class($data->target);
$target = \core_analytics\manager::get_target($targetclass);
if (empty($targets[$targetclass])) {
throw new \moodle_exception('errorinvalidtarget', 'analytics', '', $targetclass);
}
$target = $targets[$targetclass];
$indicators = array();
foreach ($data->indicators as $indicator) {
@ -88,4 +91,4 @@ if ($mform->is_cancelled()) {
echo $OUTPUT->header();
$mform->display();
echo $OUTPUT->footer();
echo $OUTPUT->footer();

View File

@ -40,19 +40,15 @@ if ($form->is_cancelled()) {
$modelconfig = new \core_analytics\model_config();
$json = $form->get_file_content('modelfile');
$zipfilepath = $form->save_temp_file('modelfile');
if ($error = $modelconfig->check_json_data($json)) {
// The provided file is not ok.
redirect($url, $error, 0, \core\output\notification::NOTIFY_ERROR);
}
list ($modeldata, $unused) = $modelconfig->extract_import_contents($zipfilepath);
$modeldata = json_decode($json);
if ($error = $modelconfig->check_dependencies($modeldata, $data->ignoreversionmismatches)) {
// The file is not available until the form is validated so we need an alternative method to show errors.
redirect($url, $error, 0, \core\output\notification::NOTIFY_ERROR);
}
$model = \core_analytics\model::create_from_import($modeldata, true);
\core_analytics\model::import_model($zipfilepath);
redirect($returnurl, get_string('importedsuccessfully', 'tool_analytics'), 0,
\core\output\notification::NOTIFY_SUCCESS);

View File

@ -37,21 +37,15 @@ $string['clievaluationandpredictions'] = 'A scheduled task iterates through enab
$string['clievaluationandpredictionsnoadmin'] = 'A scheduled task iterates through enabled models and gets predictions. Models evaluation via the web interface is disabled. It may be enabled by a site administrator.';
$string['createmodel'] = 'Create model';
$string['delete'] = 'Delete';
$string['deletemodelconfirmation'] = 'Are you sure you want to delete "{$a}"?';
$string['deletemodelconfirmation'] = 'Are you sure you want to delete "{$a}"? These changes can not be reverted.';
$string['disabled'] = 'Disabled';
$string['editmodel'] = 'Edit "{$a}" model';
$string['edittrainedwarning'] = 'This model has already been trained. Note that changing its indicators or its time-splitting method will delete its previous predictions and start generating new predictions.';
$string['enabled'] = 'Enabled';
$string['errorcantenablenotimesplitting'] = 'You need to select a time-splitting method before enabling the model';
$string['errorimport'] = 'Error importing the provided json file.';
$string['errorimportmissingcomponents'] = 'The provided model requires the following plugins to be installed: {$a}. Note that the versions do not necessarily need to match with the versions installed in your system. To install the same or a newer version of the plugin should be enough in most cases.';
$string['errorimportversionmismatches'] = 'The version of the following components differ from the version installed in this site: {$a}. You can use "Ignore version mismatches" option to ignore these differences.';
$string['errorimportmissingclasses'] = 'The following analytics components are not available in this site: {$a->missingclasses}. ';
$string['errornoenabledandtrainedmodels'] = 'There are no enabled and trained models to predict.';
$string['errornoenabledmodels'] = 'There are no enabled models to train.';
$string['errornoexport'] = 'Only trained models can be exported';
$string['errornoexportconfig'] = 'There was a problem exporting the model configuration.';
$string['errornoexportconfigrequirements'] = 'Only non static models with timeplitting methods can be exported.';
$string['errornostaticedit'] = 'Models based on assumptions cannot be edited.';
$string['errornostaticevaluated'] = 'Models based on assumptions cannot be evaluated. They are always 100% correct according to how they were defined.';
$string['errornostaticlog'] = 'Models based on assumptions cannot be evaluated because there is no performance log.';

View File

@ -229,10 +229,9 @@ switch ($action) {
break;
case 'exportmodel':
$downloadfilename = 'model-config.' . $model->get_id() . '.' . microtime() . '.json';
$filepath = $model->export_config($downloadfilename);
@header("Content-type: text/json; charset=UTF-8");
send_temp_file($filepath, $downloadfilename);
$zipfilename = 'model-' . $model->get_unique_id() . '-' . microtime(false) . '.zip';
$zipfilepath = $model->export_model($zipfilename);
send_temp_file($zipfilepath, $zipfilename);
break;
case 'clear':

View File

@ -338,12 +338,12 @@ class model {
*
* @param \core_analytics\local\target\base $target
* @param \core_analytics\local\indicator\base[] $indicators
* @param string $timesplittingid The time splitting method id (its fully qualified class name)
* @param string $processor The machine learning backend this model will use.
* @param string|false $timesplittingid The time splitting method id (its fully qualified class name)
* @param string|null $processor The machine learning backend this model will use.
* @return \core_analytics\model
*/
public static function create(\core_analytics\local\target\base $target, array $indicators,
$timesplittingid = false, $processor = false) {
$timesplittingid = false, $processor = null) {
global $USER, $DB;
\core_analytics\manager::check_can_manage_models();
@ -386,59 +386,6 @@ class model {
return $model;
}
/**
* Creates a new model from import configuration.
*
* It is recommended to call \core_analytics\model_config::check_dependencies first so the error message can be retrieved.
*
* @param \stdClass $modeldata Model data.
* @param bool $skipcheckdependencies Useful if you already checked the dependencies.
* @return \core_analytics\model|false False if the provided model data contain errors.
*/
public static function create_from_import(\stdClass $modeldata, ?bool $skipcheckdependencies = false) : ?\core_analytics\model {
\core_analytics\manager::check_can_manage_models();
if (!$skipcheckdependencies) {
$modelconfig = new model_config();
if ($error = $modelconfig->check_dependencies($modeldata, false)) {
return null;
}
}
// At this stage we should be 100% sure that the model data is safe and can be imported.
// If the caller explicitly set $skipcheckdependencies to false and there is a problem
// in this process we trigger a coding exception.
if (!$target = \core_analytics\manager::get_target($modeldata->target)) {
throw new \coding_exception('The provided target is not available. Ensure that model_config::check_dependencies
is called before importing the model.');
}
if (!$timesplitting = \core_analytics\manager::get_time_splitting($modeldata->timesplitting)) {
throw new \coding_exception('The provided time splitting method is not available. Ensure that
model_config::check_dependencies is called before importing the model.');
}
// Indicators.
$indicators = [];
foreach ($modeldata->indicators as $indicator) {
if (!$indicator = \core_analytics\manager::get_indicator($indicator)) {
throw new \coding_exception('The provided indicator is not available. Ensure that
model_config::check_dependencies is called before importing the model.');
}
$indicators[] = $indicator;
}
if (!empty($modeldata->processor)) {
if (!$processor = \core_analytics\manager::get_predictions_processor($modeldata->processor, false)) {
throw new \coding_exception('The provided machine learning backend is not available. Ensure that
model_config::check_dependencies is called before importing the model.');
}
} else {
$modeldata->processor = false;
}
return self::create($target, $indicators, $modeldata->timesplitting, $modeldata->processor);
}
/**
* Does this model exist?
*
@ -1363,7 +1310,7 @@ class model {
* @param bool $onlymodelid Preference over $subdirs
* @return string
*/
protected function get_output_dir($subdirs = array(), $onlymodelid = false) {
public function get_output_dir($subdirs = array(), $onlymodelid = false) {
global $CFG;
$subdirstr = '';
@ -1412,7 +1359,7 @@ class model {
}
/**
* Exports the model data.
* Exports the model data for displaying it in a template.
*
* @return \stdClass
*/
@ -1435,19 +1382,34 @@ class model {
}
/**
* Exports the model data as a JSON file.
* Exports the model data to a zip file.
*
* @param string $downloadfilename Download file name.
* @return string The filepath
* @param string $zipfilename
* @return string Zip file path
*/
public function export_config(string $downloadfilename) : string {
global $CFG;
public function export_model(string $zipfilename) : string {
\core_analytics\manager::check_can_manage_models();
$modelconfig = new model_config($this);
$modeldata = $modelconfig->export();
return $modelconfig->export_to_file($modeldata, $downloadfilename);
return $modelconfig->export($zipfilename);
}
/**
* Imports the provided model.
*
* Note that this method assumes that model_config::check_dependencies has already been called.
*
* @throws \moodle_exception
* @param string $zipfilepath Zip file path
* @return \core_analytics\model
*/
public static function import_model(string $zipfilepath) : \core_analytics\model {
\core_analytics\manager::check_can_manage_models();
$modelconfig = new \core_analytics\model_config();
return $modelconfig->import($zipfilepath);
}
/**

View File

@ -40,6 +40,11 @@ class model_config {
*/
private $model = null;
/**
* The name of the file where config is held.
*/
const CONFIG_FILE_NAME = 'model-config.json';
/**
* Constructor.
*
@ -50,100 +55,95 @@ class model_config {
}
/**
* Exports a model to a temp file using the provided file name.
* Exports a model to a zip using the provided file name.
*
* @return \stdClass
* @param string $zipfilename
* @return string
*/
public function export() : \stdClass {
public function export(string $zipfilename) : string {
if (!$this->model) {
throw new \coding_exception('No model object provided.');
}
if (!$this->model->can_export_configuration()) {
throw new \moodle_exception('errornoexportconfigrequirements', 'tool_analytics');
throw new \moodle_exception('errornoexportconfigrequirements', 'analytics');
}
$versions = \core_component::get_all_versions();
$zip = new \zip_packer();
$zipfiles = [];
$data = new \stdClass();
// Model config in JSON.
$modeldata = $this->export_model_data();
// Target.
$data->target = $this->model->get_target()->get_id();
$requiredclasses[] = $data->target;
$exporttmpdir = make_request_directory('analyticsexport');
$jsonfilepath = $exporttmpdir . DIRECTORY_SEPARATOR . 'model-config.json';
if (!file_put_contents($jsonfilepath, json_encode($modeldata))) {
print_error('errornoexportconfig', 'analytics');
}
$zipfiles[self::CONFIG_FILE_NAME] = $jsonfilepath;
// Time splitting method.
$data->timesplitting = $this->model->get_time_splitting()->get_id();
$requiredclasses[] = $data->timesplitting;
// Model indicators.
$data->indicators = [];
foreach ($this->model->get_indicators() as $indicator) {
$indicatorid = $indicator->get_id();
$data->indicators[] = $indicatorid;
$requiredclasses[] = $indicatorid;
// ML backend.
if ($this->model->is_trained()) {
$processor = $this->model->get_predictions_processor(true);
$outputdir = $this->model->get_output_dir(array('execution'));
$mlbackenddir = $processor->export($this->model->get_unique_id(), $outputdir);
$mlbackendfiles = get_directory_list($mlbackenddir);
foreach ($mlbackendfiles as $mlbackendfile) {
$fullpath = $mlbackenddir . DIRECTORY_SEPARATOR . $mlbackendfile;
// Place the ML backend files inside a mlbackend/ dir.
$zipfiles['mlbackend/' . $mlbackendfile] = $fullpath;
}
}
if ($processor = $this->model->get_model_obj()->predictionsprocessor) {
$data->processor = $processor;
}
// Add information for versioning.
$data->dependencies = [];
foreach ($requiredclasses as $fullclassname) {
$component = $this->get_class_component($fullclassname);
$data->dependencies[$component] = $versions[$component];
}
$zipfilepath = $exporttmpdir . DIRECTORY_SEPARATOR . $zipfilename;
$zip->archive_to_pathname($zipfiles, $zipfilepath);
return $data;
return $zipfilepath;
}
/**
* Packages the configuration of a model into a .json file.
* Imports the provided model configuration into a new model.
*
* @param \stdClass $data Model config data
* @param string $downloadfilename The file name.
* @return string Path to the file with the model configuration.
*/
public function export_to_file(\stdClass $data, string $downloadfilename) : string {
$modelconfig = json_encode($data);
$dir = make_temp_directory('analyticsexport');
$filepath = $dir . DIRECTORY_SEPARATOR . $downloadfilename;
if (!file_put_contents($filepath, $modelconfig)) {
print_error('errornoexportconfig', 'tool_analytics');
}
return $filepath;
}
/**
* Check the provided json string.
* Note that this method assumes that self::check_dependencies has already been called.
*
* @param string $json A json string.
* @return string|null Error string or null if all good.
* @param string $zipfilepath Path to the zip file to import
* @return \core_analytics\model
*/
public function check_json_data(string $json) : ?string {
public function import(string $zipfilepath) : \core_analytics\model {
if (!$modeldata = json_decode($json)) {
return get_string('errorimport', 'tool_analytics');
list($modeldata, $mlbackenddir) = $this->extract_import_contents($zipfilepath);
$target = \core_analytics\manager::get_target($modeldata->target);
$indicators = [];
foreach ($modeldata->indicators as $indicatorclass) {
$indicator = \core_analytics\manager::get_indicator($indicatorclass);
$indicators[$indicator->get_id()] = $indicator;
}
$model = \core_analytics\model::create($target, $indicators, $modeldata->timesplitting, $modeldata->processor);
// Import them disabled.
$model->update(false, false, false, false);
if ($mlbackenddir) {
$modeldir = $model->get_output_dir(['execution']);
if (!$model->get_predictions_processor(true)->import($model->get_unique_id(), $modeldir, $mlbackenddir)) {
throw new \moodle_exception('errorimport', 'analytics');
}
$model->mark_as_trained();
}
if (empty($modeldata->target) || empty($modeldata->timesplitting) || empty($modeldata->indicators)) {
return get_string('errorimport', 'tool_analytics');
}
return null;
return $model;
}
/**
* Check that the provided model configuration can be deployed in this site.
*
* @param \stdClass $importmodel
* @param \stdClass $modeldata
* @param bool $ignoreversionmismatches
* @return string|null Error string or null if all good.
*/
public function check_dependencies(\stdClass $importmodel, bool $ignoreversionmismatches) : ?string {
public function check_dependencies(\stdClass $modeldata, bool $ignoreversionmismatches) : ?string {
$siteversions = \core_component::get_all_versions();
@ -153,7 +153,7 @@ class model_config {
$missingclasses = [];
// We first check that this site has the required dependencies and the required versions.
foreach ($importmodel->dependencies as $component => $importversion) {
foreach ($modeldata->dependencies as $component => $importversion) {
if (empty($siteversions[$component])) {
@ -177,44 +177,42 @@ class model_config {
}
}
// Checking that the each of the components is available.
if (!$target = manager::get_target($importmodel->target)) {
$missingclasses[] = $importmodel->target;
// Checking that each of the components is available.
if (!$target = manager::get_target($modeldata->target)) {
$missingclasses[] = $modeldata->target;
}
if (!$timesplitting = manager::get_time_splitting($importmodel->timesplitting)) {
$missingclasses[] = $importmodel->timesplitting;
if (!$timesplitting = manager::get_time_splitting($modeldata->timesplitting)) {
$missingclasses[] = $modeldata->timesplitting;
}
// Indicators.
$indicators = [];
foreach ($importmodel->indicators as $indicatorclass) {
foreach ($modeldata->indicators as $indicatorclass) {
if (!$indicator = manager::get_indicator($indicatorclass)) {
$missingclasses[] = $indicatorclass;
}
}
// ML backend.
if (!empty($importmodel->processor)) {
if (!$processor = \core_analytics\manager::get_predictions_processor($importmodel->processor, false)) {
if (!empty($modeldata->processor)) {
if (!$processor = \core_analytics\manager::get_predictions_processor($modeldata->processor, false)) {
$missingclasses[] = $indicatorclass;
}
}
if (!empty($missingcomponents)) {
return get_string('errorimportmissingcomponents', 'tool_analytics', join(', ', $missingcomponents));
return get_string('errorimportmissingcomponents', 'analytics', join(', ', $missingcomponents));
}
if (!empty($versionmismatches)) {
return get_string('errorimportversionmismatches', 'tool_analytics', implode(', ', $versionmismatches));
return get_string('errorimportversionmismatches', 'analytics', implode(', ', $versionmismatches));
}
if (!empty($missingclasses)) {
$a = (object)[
'missingclasses' => implode(', ', $missingclasses),
'dependencyversions' => implode(', ', $dependencyversions)
];
return get_string('errorimportmissingclasses', 'tool_analytics', $a);
return get_string('errorimportmissingclasses', 'analytics', $a);
}
// No issues found.
@ -248,4 +246,81 @@ class model_config {
return $component;
}
/**
* Extracts the import zip contents.
*
* @param string $zipfilepath Zip file path
* @return array [0] => \stdClass, [1] => string
*/
public function extract_import_contents(string $zipfilepath) : array {
$importtempdir = make_request_directory('analyticsimport' . microtime(false));
$zip = new \zip_packer();
$filelist = $zip->extract_to_pathname($zipfilepath, $importtempdir);
if (empty($filelist[self::CONFIG_FILE_NAME])) {
// Missing required file.
throw new \moodle_exception('errorimport', 'analytics');
}
$jsonmodeldata = file_get_contents($importtempdir . DIRECTORY_SEPARATOR . self::CONFIG_FILE_NAME);
if (!$modeldata = json_decode($jsonmodeldata)) {
throw new \moodle_exception('errorimport', 'analytics');
}
if (empty($modeldata->target) || empty($modeldata->timesplitting) || empty($modeldata->indicators)) {
throw new \moodle_exception('errorimport', 'analytics');
}
$mlbackenddir = $importtempdir . DIRECTORY_SEPARATOR . 'mlbackend';
if (!is_dir($mlbackenddir)) {
$mlbackenddir = false;
}
return [$modeldata, $mlbackenddir];
}
/**
* Exports the configuration of the model.
* @return \stdClass
*/
protected function export_model_data() : \stdClass {
$versions = \core_component::get_all_versions();
$data = new \stdClass();
// Target.
$data->target = $this->model->get_target()->get_id();
$requiredclasses[] = $data->target;
// Time splitting method.
$data->timesplitting = $this->model->get_time_splitting()->get_id();
$requiredclasses[] = $data->timesplitting;
// Model indicators.
$data->indicators = [];
foreach ($this->model->get_indicators() as $indicator) {
$indicatorid = $indicator->get_id();
$data->indicators[] = $indicatorid;
$requiredclasses[] = $indicatorid;
}
// Return the predictions processor this model is using, even if no predictions processor
// was explicitly selected.
$predictionsprocessor = $this->model->get_predictions_processor();
$data->processor = '\\' . get_class($predictionsprocessor);
$requiredclasses[] = $data->processor;
// Add information for versioning.
$data->dependencies = [];
foreach ($requiredclasses as $fullclassname) {
$component = $this->get_class_component($fullclassname);
$data->dependencies[$component] = $versions[$component];
}
return $data;
}
}

View File

@ -0,0 +1,57 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Exportable machine learning backend interface.
*
* @package core_analytics
* @copyright 2019 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_analytics;
defined('MOODLE_INTERNAL') || die();
/**
* Exportable machine learning backend interface.
*
* @package core_analytics
* @copyright 2019 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
interface packable {
/**
* Exports the machine learning model.
*
* @throws \moodle_exception
* @param string $uniqueid The model unique id
* @param string $modeldir The directory that contains the trained model.
* @return string The path to the directory that contains the exported model.
*/
public function export(string $uniqueid, string $modeldir) : string;
/**
* Imports the provided machine learning model.
*
* @param string $uniqueid The model unique id
* @param string $modeldir The directory that will contain the trained model.
* @param string $importdir The directory that contains the files to import.
* @return bool Success
*/
public function import(string $uniqueid, string $modeldir, string $importdir) : bool;
}

View File

@ -337,36 +337,24 @@ class analytics_model_testcase extends advanced_testcase {
}
/**
* Test export_from_json() API.
* Test that import_model import models' configurations.
*/
public function test_create_from_import() {
public function test_import_model_config() {
$this->resetAfterTest(true);
$this->model->enable('\\core\\analytics\\time_splitting\\quarters');
$zipfilepath = $this->model->export_model('yeah-config.zip');
$this->modelobj = $this->model->get_model_obj();
$modelconfig = new \core_analytics\model_config($this->model);
$modeldata = $modelconfig->export();
$importedmodel = \core_analytics\model::create_from_import($modeldata)->get_model_obj();
$importedmodelobj = \core_analytics\model::import_model($zipfilepath)->get_model_obj();
$this->assertSame($this->modelobj->target, $importedmodel->target);
$this->assertSame($this->modelobj->indicators, $importedmodel->indicators);
$this->assertSame($this->modelobj->timesplitting, $importedmodel->timesplitting);
$this->assertEmpty($importedmodel->predictionsprocessor);
$this->assertSame($this->modelobj->target, $importedmodelobj->target);
$this->assertSame($this->modelobj->indicators, $importedmodelobj->indicators);
$this->assertSame($this->modelobj->timesplitting, $importedmodelobj->timesplitting);
$this->model->update(true, false, false, '\\mlbackend_php\\processor');
$this->modelobj = $this->model->get_model_obj();
$modelconfig = new \core_analytics\model_config($this->model);
$modeldata = $modelconfig->export();
$importedmodel = \core_analytics\model::create_from_import($modeldata)->get_model_obj();
$this->assertSame($this->modelobj->target, $importedmodel->target);
$this->assertSame($this->modelobj->indicators, $importedmodel->indicators);
$this->assertSame($this->modelobj->timesplitting, $importedmodel->timesplitting);
$this->assertSame($this->modelobj->predictionsprocessor, $importedmodel->predictionsprocessor);
$predictionsprocessor = $this->model->get_predictions_processor();
$this->assertSame('\\' . get_class($predictionsprocessor), $importedmodelobj->predictionsprocessor);
}
/**
@ -399,7 +387,11 @@ class analytics_model_testcase extends advanced_testcase {
$this->model->enable('\\core\\analytics\\time_splitting\\quarters');
$modelconfig = new \core_analytics\model_config($this->model);
$modeldata = $modelconfig->export();
$method = new ReflectionMethod('\\core_analytics\\model_config', 'export_model_data');
$method->setAccessible(true);
$modeldata = $method->invoke($modelconfig);
$this->assertArrayHasKey('core', $modeldata->dependencies);
$this->assertInternalType('float', $modeldata->dependencies['core']);
@ -409,7 +401,8 @@ class analytics_model_testcase extends advanced_testcase {
$indicators['test_indicator_max'] = \core_analytics\manager::get_indicator('test_indicator_max');
$this->model->update(true, $indicators, false);
$modeldata = $modelconfig->export();
$modeldata = $method->invoke($modelconfig);
$this->assertCount(1, $modeldata->indicators);
}
@ -443,17 +436,6 @@ class analytics_model_testcase extends advanced_testcase {
*/
class testable_model extends \core_analytics\model {
/**
* get_output_dir
*
* @param array $subdirs
* @param bool $onlymodelid
* @return string
*/
public function get_output_dir($subdirs = array(), $onlymodelid = false) {
return parent::get_output_dir($subdirs, $onlymodelid);
}
/**
* init_analyser
*

View File

@ -119,23 +119,9 @@ class core_analytics_prediction_testcase extends advanced_testcase {
$this->setAdminuser();
set_config('enabled_stores', 'logstore_standard', 'tool_log');
$ncourses = 10;
// Generate training data.
$params = array(
'startdate' => mktime(0, 0, 0, 10, 24, 2015),
'enddate' => mktime(0, 0, 0, 2, 24, 2016),
);
for ($i = 0; $i < $ncourses; $i++) {
$name = 'a' . random_string(10);
$courseparams = array('shortname' => $name, 'fullname' => $name) + $params;
$this->getDataGenerator()->create_course($courseparams);
}
for ($i = 0; $i < $ncourses; $i++) {
$name = 'b' . random_string(10);
$courseparams = array('shortname' => $name, 'fullname' => $name) + $params;
$this->getDataGenerator()->create_course($courseparams);
}
$ncourses = 10;
$this->generate_courses($ncourses);
// We repeat the test for all prediction processors.
$predictionsprocessor = \core_analytics\manager::get_predictions_processor($predictionsprocessorclass, false);
@ -171,6 +157,10 @@ class core_analytics_prediction_testcase extends advanced_testcase {
$this->assertEmpty($fs->get_directory_files(\context_system::instance()->id, 'analytics',
\core_analytics\dataset_manager::UNLABELLED_FILEAREA, $model->get_id(), '/analysable/', true, false));
$params = [
'startdate' => mktime(0, 0, 0, 10, 24, 2015),
'enddate' => mktime(0, 0, 0, 2, 24, 2016),
];
$courseparams = $params + array('shortname' => 'aaaaaa', 'fullname' => 'aaaaaa', 'visible' => 0);
$course1 = $this->getDataGenerator()->create_course($courseparams);
$courseparams = $params + array('shortname' => 'bbbbbb', 'fullname' => 'bbbbbb', 'visible' => 0);
@ -280,6 +270,67 @@ class core_analytics_prediction_testcase extends advanced_testcase {
return $this->add_prediction_processors($cases);
}
/**
* test_ml_export_import
*
* @param string $predictionsprocessorclass The class name
* @dataProvider provider_ml_export_import
*/
public function test_ml_export_import($predictionsprocessorclass) {
$this->resetAfterTest(true);
$this->setAdminuser();
set_config('enabled_stores', 'logstore_standard', 'tool_log');
// Generate training data.
$ncourses = 10;
$this->generate_courses($ncourses);
// We repeat the test for all prediction processors.
$predictionsprocessor = \core_analytics\manager::get_predictions_processor($predictionsprocessorclass, false);
if ($predictionsprocessor->is_ready() !== true) {
$this->markTestSkipped('Skipping ' . $predictionsprocessorclass . ' as the predictor is not ready.');
}
$model = $this->add_perfect_model();
$model->update(true, false, '\core\analytics\time_splitting\quarters', get_class($predictionsprocessor));
$model->train();
$this->generate_courses(10, ['visible' => 0]);
$originalresults = $model->predict();
$zipfilename = 'model-zip-' . microtime() . '.zip';
$zipfilepath = $model->export_model($zipfilename);
$importmodel = \core_analytics\model::import_model($zipfilepath);
$importmodel->enable();
// Now predict using the imported model without prior training.
$importedmodelresults = $importmodel->predict();
foreach ($originalresults->predictions as $sampleid => $prediction) {
$this->assertEquals($importedmodelresults->predictions[$sampleid]->prediction, $prediction->prediction);
}
set_config('enabled_stores', '', 'tool_log');
get_log_manager(true);
}
/**
* provider_ml_export_import
*
* @return array
*/
public function provider_ml_export_import() {
$cases = [
'case' => [],
];
// We need to test all system prediction processors.
return $this->add_prediction_processors($cases);
}
/**
* Test the system classifiers returns.
*
@ -400,20 +451,7 @@ class core_analytics_prediction_testcase extends advanced_testcase {
}
// Generate training data.
$params = array(
'startdate' => mktime(0, 0, 0, 10, 24, 2015),
'enddate' => mktime(0, 0, 0, 2, 24, 2016),
);
for ($i = 0; $i < $ncourses; $i++) {
$name = 'a' . random_string(10);
$params = array('shortname' => $name, 'fullname' => $name) + $params;
$this->getDataGenerator()->create_course($params);
}
for ($i = 0; $i < $ncourses; $i++) {
$name = 'b' . random_string(10);
$params = array('shortname' => $name, 'fullname' => $name) + $params;
$this->getDataGenerator()->create_course($params);
}
$this->generate_courses($ncourses);
// We repeat the test for all prediction processors.
$predictionsprocessor = \core_analytics\manager::get_predictions_processor($predictionsprocessorclass, false);
@ -579,6 +617,32 @@ class core_analytics_prediction_testcase extends advanced_testcase {
return new \core_analytics\model($model->get_id());
}
/**
* Generates $ncourses courses
*
* @param int $ncourses The number of courses to be generated.
* @param array $params Course params
* @return null
*/
protected function generate_courses($ncourses, array $params = []) {
$params = $params + [
'startdate' => mktime(0, 0, 0, 10, 24, 2015),
'enddate' => mktime(0, 0, 0, 2, 24, 2016),
];
for ($i = 0; $i < $ncourses; $i++) {
$name = 'a' . random_string(10);
$courseparams = array('shortname' => $name, 'fullname' => $name) + $params;
$this->getDataGenerator()->create_course($courseparams);
}
for ($i = 0; $i < $ncourses; $i++) {
$name = 'b' . random_string(10);
$courseparams = array('shortname' => $name, 'fullname' => $name) + $params;
$this->getDataGenerator()->create_course($courseparams);
}
}
/**
* add_prediction_processors
*

View File

@ -38,8 +38,16 @@ $string['erroralreadypredict'] = 'File {$a} has already been used to generate pr
$string['errorcannotreaddataset'] = 'Dataset file {$a} can not be read';
$string['errorcannotwritedataset'] = 'Dataset file {$a} cannot be written';
$string['errorendbeforestart'] = 'The end date ({$a}) is before the course start date.';
$string['errorexportmodelresult'] = 'The machine learning model can not be exported.';
$string['errorimport'] = 'Error importing the provided json file.';
$string['errorimportmissingcomponents'] = 'The provided model requires the following plugins to be installed: {$a}. Note that the versions do not necessarily need to match with the versions installed in your system. To install the same or a newer version of the plugin should be enough in most cases.';
$string['errorimportversionmismatches'] = 'The version of the following components differ from the version installed in this site: {$a}. You can use "Ignore version mismatches" option to ignore these differences.';
$string['errorimportmissingclasses'] = 'The following analytics components are not available in this site: {$a->missingclasses}. ';
$string['errorinvalidindicator'] = 'Invalid {$a} indicator';
$string['errorinvalidtarget'] = 'Invalid {$a} target';
$string['errorinvalidtimesplitting'] = 'Invalid time splitting; please ensure you add the class fully qualified class name.';
$string['errornoexportconfig'] = 'There was a problem exporting the model configuration.';
$string['errornoexportconfigrequirements'] = 'Only non static models with timeplitting methods can be exported.';
$string['errornoindicators'] = 'This model does not have any indicators.';
$string['errornopredictresults'] = 'No results returned from the predictions processor. Check the output directory contents for more information.';
$string['errornotimesplittings'] = 'This model does not have any time-splitting method.';

View File

@ -38,7 +38,7 @@ use Phpml\ModelManager;
* @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class processor implements \core_analytics\classifier, \core_analytics\regressor {
class processor implements \core_analytics\classifier, \core_analytics\regressor, \core_analytics\packable {
/**
* Size of training / prediction batches.
@ -103,8 +103,7 @@ class processor implements \core_analytics\classifier, \core_analytics\regressor
*/
public function train_classification($uniqueid, \stored_file $dataset, $outputdir) {
// Output directory is already unique to the model.
$modelfilepath = $outputdir . DIRECTORY_SEPARATOR . self::MODEL_FILENAME;
$modelfilepath = $this->get_model_filepath($outputdir);
$modelmanager = new ModelManager();
@ -175,8 +174,7 @@ class processor implements \core_analytics\classifier, \core_analytics\regressor
*/
public function classify($uniqueid, \stored_file $dataset, $outputdir) {
// Output directory is already unique to the model.
$modelfilepath = $outputdir . DIRECTORY_SEPARATOR . self::MODEL_FILENAME;
$modelfilepath = $this->get_model_filepath($outputdir);
if (!file_exists($modelfilepath)) {
throw new \moodle_exception('errorcantloadmodel', 'mlbackend_php', '', $modelfilepath);
@ -424,6 +422,77 @@ class processor implements \core_analytics\classifier, \core_analytics\regressor
throw new \coding_exception('This predictor does not support regression yet.');
}
/**
* Exports the machine learning model.
*
* @throws \moodle_exception
* @param string $uniqueid The model unique id
* @param string $modeldir The directory that contains the trained model.
* @return string The path to the directory that contains the exported model.
*/
public function export(string $uniqueid, string $modeldir) : string {
$modelfilepath = $this->get_model_filepath($modeldir);
if (!file_exists($modelfilepath)) {
throw new \moodle_exception('errorexportmodelresult', 'analytics');
}
// We can use the actual $modeldir as the directory is not modified during export, just copied into a zip.
return $modeldir;
}
/**
* Imports the provided machine learning model.
*
* @param string $uniqueid The model unique id
* @param string $modeldir The directory that will contain the trained model.
* @param string $importdir The directory that contains the files to import.
* @return bool Success
*/
public function import(string $uniqueid, string $modeldir, string $importdir) : bool {
$importmodelfilepath = $this->get_model_filepath($importdir);
$modelfilepath = $this->get_model_filepath($modeldir);
$modelmanager = new ModelManager();
// Copied from ModelManager::restoreFromFile to validate the serialised contents
// before restoring them.
$importconfig = file_get_contents($importmodelfilepath);
// Clean stuff like function calls.
$importconfig = preg_replace('/[^a-zA-Z0-9\{\}%\.\*\;\,\:\"\-\0\\\]/', '', $importconfig);
$object = unserialize($importconfig,
['allowed_classes' => ['Phpml\\Classification\\Linear\\LogisticRegression']]);
if (!$object) {
return false;
}
if (get_class($object) == '__PHP_Incomplete_Class') {
return false;
}
$classifier = $modelmanager->restoreFromFile($importmodelfilepath);
// This would override any previous classifier.
$modelmanager->saveToFile($classifier, $modelfilepath);
return true;
}
/**
* Returns the path to the serialised model file in the provided directory.
*
* @param string $modeldir The model directory
* @return string The model file
*/
protected function get_model_filepath(string $modeldir) : string {
// Output directory is already unique to the model.
return $modeldir . DIRECTORY_SEPARATOR . self::MODEL_FILENAME;
}
/**
* Returns the Phi correlation coefficient.
*

View File

@ -33,7 +33,7 @@ defined('MOODLE_INTERNAL') || die();
* @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class processor implements \core_analytics\classifier, \core_analytics\regressor {
class processor implements \core_analytics\classifier, \core_analytics\regressor, \core_analytics\packable {
/**
* The required version of the python package that performs all calculations.
@ -263,6 +263,78 @@ class processor implements \core_analytics\classifier, \core_analytics\regresso
return $resultobj;
}
/**
* Exports the machine learning model.
*
* @throws \moodle_exception
* @param string $uniqueid The model unique id
* @param string $modeldir The directory that contains the trained model.
* @return string The path to the directory that contains the exported model.
*/
public function export(string $uniqueid, string $modeldir) : string {
// We include an exporttmpdir as we want to be sure that the file is not deleted after the
// python process finishes.
$exporttmpdir = make_request_directory('mlbackend_python_export');
$cmd = "{$this->pathtopython} -m moodlemlbackend.export " .
escapeshellarg($uniqueid) . ' ' .
escapeshellarg($modeldir) . ' ' .
escapeshellarg($exporttmpdir);
if (!PHPUNIT_TEST && CLI_SCRIPT) {
debugging($cmd, DEBUG_DEVELOPER);
}
$output = null;
$exitcode = null;
$exportdir = exec($cmd, $output, $exitcode);
if ($exitcode != 0) {
throw new \moodle_exception('errorexportmodelresult', 'analytics');
}
if (!$exportdir) {
throw new \moodle_exception('errorexportmodelresult', 'analytics');
}
return $exportdir;
}
/**
* Imports the provided machine learning model.
*
* @param string $uniqueid The model unique id
* @param string $modeldir The directory that will contain the trained model.
* @param string $importdir The directory that contains the files to import.
* @return bool Success
*/
public function import(string $uniqueid, string $modeldir, string $importdir) : bool {
$cmd = "{$this->pathtopython} -m moodlemlbackend.import " .
escapeshellarg($uniqueid) . ' ' .
escapeshellarg($modeldir) . ' ' .
escapeshellarg($importdir);
if (!PHPUNIT_TEST && CLI_SCRIPT) {
debugging($cmd, DEBUG_DEVELOPER);
}
$output = null;
$exitcode = null;
$success = exec($cmd, $output, $exitcode);
if ($exitcode != 0) {
throw new \moodle_exception('errorimportmodelresult', 'analytics');
}
if (!$success) {
throw new \moodle_exception('errorimportmodelresult', 'analytics');
}
return $success;
}
/**
* Train this processor regression model using the provided supervised learning dataset.
*