Jelajahi Sumber

Add LightGBM .txt prototype (#669)

Lutz Roeder 5 tahun lalu
induk
melakukan
cc47bafb98
8 mengubah file dengan 494 tambahan dan 180 penghapusan
  1. 1 0
      source/darknet.js
  2. 2 0
      source/index.html
  3. 297 0
      source/lightgbm.js
  4. 148 149
      source/npz.js
  5. 14 15
      source/pytorch.js
  6. 24 16
      source/sklearn.js
  7. 1 0
      source/view.js
  8. 7 0
      test/models.json

+ 1 - 0
source/darknet.js

@@ -29,6 +29,7 @@ darknet.ModelFactory = class {
                         if (text.startsWith('[') && text.endsWith(']')) {
                             return true;
                         }
+                        return false;
                     }
                 }
                 catch (err) {

+ 2 - 0
source/index.html

@@ -17,6 +17,8 @@
 <script type="text/javascript" src="dagre.js"></script>
 <script type="text/javascript" src="base.js"></script>
 <script type="text/javascript" src="json.js"></script>
+<script type="text/javascript" src="pickle.js"></script>
+<script type="text/javascript" src="python.js"></script>
 <script type="text/javascript" src="protobuf.js"></script>
 <script type="text/javascript" src="flatbuffers.js"></script>
 <script type="text/javascript" src="zip.js"></script>

+ 297 - 0
source/lightgbm.js

@@ -0,0 +1,297 @@
+/* jshint esversion: 6 */
+
+var lightgbm = lightgbm || {};
+var base = base || require('./base');
+
+lightgbm.ModelFactory = class {
+
+    match(context) {
+        try {
+            const stream = context.stream;
+            const reader = base.TextReader.create(stream.peek(), 65536);
+            const line = reader.read();
+            if (line === 'tree') {
+                return true;
+            }
+        }
+        catch (err) {
+            // continue regardless of error
+        }
+        return false;
+    }
+
+    open(context) {
+        return new Promise((resolve, reject) => {
+            try {
+                const booster = new lightgbm.basic.Booster(context.stream);
+                resolve(new lightgbm.Model(booster));
+            }
+            catch (err) {
+                reject(err);
+            }
+        });
+    }
+};
+
+lightgbm.Model = class {
+
+    constructor(model) {
+        this._version = model.meta.version;
+        this._graphs = [ new lightgbm.Graph(model) ];
+    }
+
+    get format() {
+        return 'LightGBM' + (this._version ? ' ' + this._version : '');
+    }
+
+    get graphs() {
+        return this._graphs;
+    }
+};
+
+lightgbm.Graph = class {
+
+    constructor(model) {
+        this._inputs = [];
+        this._outputs = [];
+        this._nodes = [];
+
+        const args = [];
+        if (model.meta.feature_names) {
+            const feature_names = model.meta.feature_names.split(' ').map((item) => item.trim());
+            for (const feature_name of feature_names) {
+                const arg = new lightgbm.Argument(feature_name);
+                args.push(arg);
+                this._inputs.push(new lightgbm.Parameter(feature_name, [ arg ]));
+            }
+        }
+
+        this._nodes.push(new lightgbm.Node(model, args));
+    }
+
+    get inputs() {
+        return this._inputs;
+    }
+
+    get outputs() {
+        return this._outputs;
+    }
+
+    get nodes() {
+        return this._nodes;
+    }
+};
+
+lightgbm.Parameter = class {
+
+    constructor(name, args) {
+        this._name = name;
+        this._arguments = args;
+    }
+
+    get name() {
+        return this._name;
+    }
+
+    get visible() {
+        return true;
+    }
+
+    get arguments() {
+        return this._arguments;
+    }
+};
+
+lightgbm.Argument = class {
+
+    constructor(name) {
+        if (typeof name !== 'string') {
+            throw new lightgbm.Error("Invalid argument identifier '" + JSON.stringify(name) + "'.");
+        }
+        this._name = name;
+    }
+
+    get name() {
+        return this._name;
+    }
+
+    get type() {
+        return null;
+    }
+
+    get initializer() {
+        return null;
+    }
+};
+
+lightgbm.Node = class {
+
+    constructor(model, args) {
+        this._type = model.__module__ + '.' + model.__name__;
+        this._inputs = [];
+        this._outputs = [];
+        this._attributes = [];
+
+        this._inputs.push(new lightgbm.Parameter('features', args));
+
+        for (const key of Object.keys(model.params)) {
+            this._attributes.push(new lightgbm.Attribute(key, model.params[key]));
+        }
+    }
+
+    get type() {
+        return this._type;
+    }
+
+    get name() {
+        return '';
+    }
+
+    get inputs() {
+        return this._inputs;
+    }
+
+    get outputs() {
+        return this._outputs;
+    }
+
+    get attributes() {
+        return this._attributes;
+    }
+};
+
+lightgbm.Attribute = class {
+
+    constructor(name, value) {
+        this._name = name;
+        this._value = value;
+    }
+
+    get name() {
+        return this._name;
+    }
+
+    get value() {
+        return this._value;
+    }
+};
+
+lightgbm.basic = {};
+
+lightgbm.basic.Booster = class {
+
+    constructor(stream) {
+
+        this.__module__ = 'lightgbm.basic';
+        this.__name__ = 'Booster';
+
+        this.params = {};
+        this.feature_importances = {};
+        this.meta = {};
+        this.trees = [];
+
+        // GBDT::LoadModelFromString() in https://github.com/microsoft/LightGBM/blob/master/src/boosting/gbdt_model_text.cpp
+        const reader = base.TextReader.create(stream.peek());
+
+        const signature = reader.read();
+        if (!signature || signature.trim() !== 'tree') {
+            throw new lightgbm.Error("Invalid signature '" + signature.trim() + "'.");
+        }
+        let state = '';
+        let tree = null;
+        // let lineNumber = 0;
+        for (;;) {
+            // lineNumber++;
+            const text = reader.read();
+            if (text === undefined) {
+                break;
+            }
+            const line = text.trim();
+            if (line.length === 0) {
+                continue;
+            }
+            if (line.startsWith('Tree=')) {
+                state = 'tree';
+                tree = { index: parseInt(line.split('=').pop(), 10) };
+                this.trees.push(tree);
+                continue;
+            }
+            else if (line === 'parameters:') {
+                state = 'param';
+                continue;
+            }
+            else if (line === 'feature_importances:') {
+                state = 'feature_importances';
+                continue;
+            }
+            else if (line === 'end of trees' || line === 'end of parameters') {
+                state = '';
+                continue;
+            }
+            else if (line.startsWith('pandas_categorical:')) {
+                state = 'pandas_categorical';
+                continue;
+            }
+            switch (state) {
+                case '': {
+                    const param = line.split('=');
+                    if (param.length !== 2) {
+                        throw new lightgbm.Error("Invalid property '" + line + "'.");
+                    }
+                    const name = param[0].trim();
+                    const value = param[1].trim();
+                    this.meta[name] = value;
+                    break;
+                }
+                case 'param': {
+                    if (!line.startsWith('[') || !line.endsWith(']')) {
+                        throw new lightgbm.Error("Invalid parameter '" + line + "'.");
+                    }
+                    const param = line.substring(1, line.length - 2).split(':');
+                    if (param.length !== 2) {
+                        throw new lightgbm.Error("Invalid param '" + line + "'.");
+                    }
+                    const name = param[0].trim();
+                    const value = param[1].trim();
+                    this.params[name] = value;
+                    break;
+                }
+                case 'tree': {
+                    const param = line.split('=');
+                    if (param.length !== 2) {
+                        throw new lightgbm.Error("Invalid property '" + line + "'.");
+                    }
+                    const name = param[0].trim();
+                    const value = param[1].trim();
+                    tree[name] = value;
+                    break;
+                }
+                case 'feature_importances': {
+                    const param = line.split('=');
+                    if (param.length !== 2) {
+                        throw new lightgbm.Error("Invalid feature importance '" + line + "'.");
+                    }
+                    const name = param[0].trim();
+                    const value = param[1].trim();
+                    this.feature_importances[name] = value;
+                    break;
+                }
+                case 'pandas_categorical': {
+                    break;
+                }
+            }
+        }
+    }
+};
+
+lightgbm.Error = class extends Error {
+
+    constructor(message) {
+        super(message);
+        this.name = 'Error loading LightGBM model.';
+    }
+};
+
+if (typeof module !== 'undefined' && typeof module.exports === 'object') {
+    module.exports.ModelFactory = lightgbm.ModelFactory;
+}

+ 148 - 149
source/npz.js

@@ -3,6 +3,7 @@
 // Experimental
 
 var npz = npz || {};
+var pickle = pickle || require('./pickle');
 
 npz.ModelFactory = class {
 
@@ -13,164 +14,162 @@ npz.ModelFactory = class {
 
     open(context) {
         return context.require('./numpy').then((numpy) => {
-            return context.require('./pickle').then((pickle) => {
-                const modules = [];
-                const modulesMap = new Map();
-                const functionTable = new Map();
-                const constructorTable = new Map();
-                functionTable.set('_codecs.encode', function(obj /*, econding */) {
-                    return obj;
-                });
-                constructorTable.set('numpy.core.multiarray._reconstruct', function(subtype, shape, dtype) {
-                    this.subtype = subtype;
-                    this.shape = shape;
-                    this.dtype = dtype;
-                    this.__setstate__ = function(state) {
-                        this.version = state[0];
-                        this.shape = state[1];
-                        this.typecode = state[2];
-                        this.is_f_order = state[3];
-                        this.rawdata = state[4];
-                    };
-                    this.__read__ = function(unpickler) {
-                        const array = {};
-                        array.__type__ = this.subtype;
-                        array.dtype = this.typecode;
-                        array.shape = this.shape;
-                        let size = array.dtype.itemsize;
-                        for (let i = 0; i < array.shape.length; i++) {
-                            size = size * array.shape[i];
-                        }
-                        if (typeof this.rawdata == 'string') {
-                            array.data = unpickler.unescape(this.rawdata, size);
-                            if (array.data.length != size) {
-                                throw new npz.Error('Invalid string array data size.');
-                            }
-                        }
-                        else {
-                            array.data = this.rawdata;
-                            if (array.data.length != size) {
-                                // TODO
-                                // throw new npz.Error('Invalid array data size.');
-                            }
-                        }
-                        return array;
-                    };
-                });
-                constructorTable.set('numpy.dtype', function(obj, align, copy) {
-                    switch (obj) {
-                        case 'i1': this.name = 'int8'; this.itemsize = 1; break;
-                        case 'i2': this.name = 'int16'; this.itemsize = 2; break;
-                        case 'i4': this.name = 'int32'; this.itemsize = 4; break;
-                        case 'i8': this.name = 'int64'; this.itemsize = 8; break;
-                        case 'u1': this.name = 'uint8'; this.itemsize = 1; break;
-                        case 'u2': this.name = 'uint16'; this.itemsize = 2; break;
-                        case 'u4': this.name = 'uint32'; this.itemsize = 4; break;
-                        case 'u8': this.name = 'uint64'; this.itemsize = 8; break;
-                        case 'f4': this.name = 'float32'; this.itemsize = 4; break;
-                        case 'f8': this.name = 'float64'; this.itemsize = 8; break;
-                        default:
-                            if (obj.startsWith('V')) {
-                                this.itemsize = Number(obj.substring(1));
-                                this.name = 'void' + (this.itemsize * 8).toString();
-                            }
-                            else if (obj.startsWith('O')) {
-                                this.itemsize = Number(obj.substring(1));
-                                this.name = 'object';
-                            }
-                            else if (obj.startsWith('S')) {
-                                this.itemsize = Number(obj.substring(1));
-                                this.name = 'string';
-                            }
-                            else if (obj.startsWith('U')) {
-                                this.itemsize = Number(obj.substring(1));
-                                this.name = 'string';
-                            }
-                            else if (obj.startsWith('M')) {
-                                this.itemsize = Number(obj.substring(1));
-                                this.name = 'datetime';
-                            }
-                            else {
-                                throw new npz.Error("Unknown dtype '" + obj.toString() + "'.");
-                            }
-                            break;
+            const modules = [];
+            const modulesMap = new Map();
+            const functionTable = new Map();
+            const constructorTable = new Map();
+            functionTable.set('_codecs.encode', function(obj /*, econding */) {
+                return obj;
+            });
+            constructorTable.set('numpy.core.multiarray._reconstruct', function(subtype, shape, dtype) {
+                this.subtype = subtype;
+                this.shape = shape;
+                this.dtype = dtype;
+                this.__setstate__ = function(state) {
+                    this.version = state[0];
+                    this.shape = state[1];
+                    this.typecode = state[2];
+                    this.is_f_order = state[3];
+                    this.rawdata = state[4];
+                };
+                this.__read__ = function(unpickler) {
+                    const array = {};
+                    array.__type__ = this.subtype;
+                    array.dtype = this.typecode;
+                    array.shape = this.shape;
+                    let size = array.dtype.itemsize;
+                    for (let i = 0; i < array.shape.length; i++) {
+                        size = size * array.shape[i];
                     }
-                    this.align = align;
-                    this.copy = copy;
-                    this.__setstate__ = function(state) {
-                        switch (state.length) {
-                            case 8:
-                                this.version = state[0];
-                                this.byteorder = state[1];
-                                this.subarray = state[2];
-                                this.names = state[3];
-                                this.fields = state[4];
-                                this.elsize = state[5];
-                                this.alignment = state[6];
-                                this.int_dtypeflags = state[7];
-                                break;
-                            default:
-                                throw new npz.Error("Unknown numpy.dtype setstate length '" + state.length.toString() + "'.");
+                    if (typeof this.rawdata == 'string') {
+                        array.data = unpickler.unescape(this.rawdata, size);
+                        if (array.data.length != size) {
+                            throw new npz.Error('Invalid string array data size.');
                         }
-                    };
-                });
-                const function_call = (name, args) => {
-                    if (functionTable.has(name)) {
-                        const func = functionTable.get(name);
-                        return func.apply(null, args);
-                    }
-                    const obj = { __type__: name };
-                    if (constructorTable.has(name)) {
-                        const constructor = constructorTable.get(name);
-                        constructor.apply(obj, args);
                     }
                     else {
-                        throw new npz.Error("Unknown function '" + name + "'.");
+                        array.data = this.rawdata;
+                        if (array.data.length != size) {
+                            // TODO
+                            // throw new npz.Error('Invalid array data size.');
+                        }
                     }
-                    return obj;
+                    return array;
                 };
-
-                const dataTypeMap = new Map([
-                    [ 'i1', 'int8'], [ 'i2', 'int16' ], [ 'i4', 'int32'], [ 'i8', 'int64' ],
-                    [ 'u1', 'uint8'], [ 'u2', 'uint16' ], [ 'u4', 'uint32'], [ 'u8', 'uint64' ],
-                    [ 'f2', 'float16'], [ 'f4', 'float32' ], [ 'f8', 'float64']
-                ]);
-
-                for (const entry of context.entries('zip')) {
-                    if (!entry.name.endsWith('.npy')) {
-                        throw new npz.Error("Invalid file name '" + entry.name + "'.");
-                    }
-                    const name = entry.name.replace(/\.npy$/, '');
-                    const parts = name.split('/');
-                    const parameterName = parts.pop();
-                    const moduleName = parts.join('/');
-                    if (!modulesMap.has(moduleName)) {
-                        const newModule = { name: moduleName, parameters: [] };
-                        modules.push(newModule);
-                        modulesMap.set(moduleName, newModule);
-                    }
-                    const module = modulesMap.get(moduleName);
-                    const data = entry.data;
-                    let array = new numpy.Array(data);
-                    if (array.byteOrder === '|') {
-                        if (array.dataType !== 'O') {
-                            throw new npz.Error("Invalid data type '" + array.dataType + "'.");
+            });
+            constructorTable.set('numpy.dtype', function(obj, align, copy) {
+                switch (obj) {
+                    case 'i1': this.name = 'int8'; this.itemsize = 1; break;
+                    case 'i2': this.name = 'int16'; this.itemsize = 2; break;
+                    case 'i4': this.name = 'int32'; this.itemsize = 4; break;
+                    case 'i8': this.name = 'int64'; this.itemsize = 8; break;
+                    case 'u1': this.name = 'uint8'; this.itemsize = 1; break;
+                    case 'u2': this.name = 'uint16'; this.itemsize = 2; break;
+                    case 'u4': this.name = 'uint32'; this.itemsize = 4; break;
+                    case 'u8': this.name = 'uint64'; this.itemsize = 8; break;
+                    case 'f4': this.name = 'float32'; this.itemsize = 4; break;
+                    case 'f8': this.name = 'float64'; this.itemsize = 8; break;
+                    default:
+                        if (obj.startsWith('V')) {
+                            this.itemsize = Number(obj.substring(1));
+                            this.name = 'void' + (this.itemsize * 8).toString();
                         }
-                        const unpickler = new pickle.Unpickler(array.data);
-                        const root = unpickler.load(function_call);
-                        array = { dataType: root.dtype.name, shape: null, data: null, byteOrder: '|' };
-                    }
-
-                    module.parameters.push({
-                        name: parameterName,
-                        dataType: dataTypeMap.has(array.dataType) ? dataTypeMap.get(array.dataType) : array.dataType,
-                        shape: array.shape,
-                        data: array.data,
-                        byteOrder: array.byteOrder
-                    });
+                        else if (obj.startsWith('O')) {
+                            this.itemsize = Number(obj.substring(1));
+                            this.name = 'object';
+                        }
+                        else if (obj.startsWith('S')) {
+                            this.itemsize = Number(obj.substring(1));
+                            this.name = 'string';
+                        }
+                        else if (obj.startsWith('U')) {
+                            this.itemsize = Number(obj.substring(1));
+                            this.name = 'string';
+                        }
+                        else if (obj.startsWith('M')) {
+                            this.itemsize = Number(obj.substring(1));
+                            this.name = 'datetime';
+                        }
+                        else {
+                            throw new npz.Error("Unknown dtype '" + obj.toString() + "'.");
+                        }
+                        break;
                 }
-                return new npz.Model(modules, 'NumPy Zip');
+                this.align = align;
+                this.copy = copy;
+                this.__setstate__ = function(state) {
+                    switch (state.length) {
+                        case 8:
+                            this.version = state[0];
+                            this.byteorder = state[1];
+                            this.subarray = state[2];
+                            this.names = state[3];
+                            this.fields = state[4];
+                            this.elsize = state[5];
+                            this.alignment = state[6];
+                            this.int_dtypeflags = state[7];
+                            break;
+                        default:
+                            throw new npz.Error("Unknown numpy.dtype setstate length '" + state.length.toString() + "'.");
+                    }
+                };
             });
+            const function_call = (name, args) => {
+                if (functionTable.has(name)) {
+                    const func = functionTable.get(name);
+                    return func.apply(null, args);
+                }
+                const obj = { __type__: name };
+                if (constructorTable.has(name)) {
+                    const constructor = constructorTable.get(name);
+                    constructor.apply(obj, args);
+                }
+                else {
+                    throw new npz.Error("Unknown function '" + name + "'.");
+                }
+                return obj;
+            };
+
+            const dataTypeMap = new Map([
+                [ 'i1', 'int8'], [ 'i2', 'int16' ], [ 'i4', 'int32'], [ 'i8', 'int64' ],
+                [ 'u1', 'uint8'], [ 'u2', 'uint16' ], [ 'u4', 'uint32'], [ 'u8', 'uint64' ],
+                [ 'f2', 'float16'], [ 'f4', 'float32' ], [ 'f8', 'float64']
+            ]);
+
+            for (const entry of context.entries('zip')) {
+                if (!entry.name.endsWith('.npy')) {
+                    throw new npz.Error("Invalid file name '" + entry.name + "'.");
+                }
+                const name = entry.name.replace(/\.npy$/, '');
+                const parts = name.split('/');
+                const parameterName = parts.pop();
+                const moduleName = parts.join('/');
+                if (!modulesMap.has(moduleName)) {
+                    const newModule = { name: moduleName, parameters: [] };
+                    modules.push(newModule);
+                    modulesMap.set(moduleName, newModule);
+                }
+                const module = modulesMap.get(moduleName);
+                const data = entry.data;
+                let array = new numpy.Array(data);
+                if (array.byteOrder === '|') {
+                    if (array.dataType !== 'O') {
+                        throw new npz.Error("Invalid data type '" + array.dataType + "'.");
+                    }
+                    const unpickler = new pickle.Unpickler(array.data);
+                    const root = unpickler.load(function_call);
+                    array = { dataType: root.dtype.name, shape: null, data: null, byteOrder: '|' };
+                }
+
+                module.parameters.push({
+                    name: parameterName,
+                    dataType: dataTypeMap.has(array.dataType) ? dataTypeMap.get(array.dataType) : array.dataType,
+                    shape: array.shape,
+                    data: array.data,
+                    byteOrder: array.byteOrder
+                });
+            }
+            return new npz.Model(modules, 'NumPy Zip');
         });
     }
 };

+ 14 - 15
source/pytorch.js

@@ -3,6 +3,7 @@
 // Experimental
 
 var pytorch = pytorch || {};
+var pickle = pickle || require('./pickle');
 var base = base || require('./base');
 
 pytorch.ModelFactory = class {
@@ -16,22 +17,20 @@ pytorch.ModelFactory = class {
 
     open(context) {
         const identifier = context.identifier;
-        return context.require('./pickle').then((pickle) => {
-            return context.require('./python').then((python) => {
-                return pytorch.Metadata.open(context).then((metadata) => {
-                    let container = null;
-                    try {
-                        container = pytorch.Container.open(context, metadata, pickle, python, (error, fatal) => {
-                            const message = error && error.message ? error.message : error.toString();
-                            context.exception(new pytorch.Error(message.replace(/\.$/, '') + " in '" + identifier + "'."), fatal);
-                        });
-                    }
-                    catch (error) {
+        return context.require('./python').then((python) => {
+            return pytorch.Metadata.open(context).then((metadata) => {
+                let container = null;
+                try {
+                    container = pytorch.Container.open(context, metadata, pickle, python, (error, fatal) => {
                         const message = error && error.message ? error.message : error.toString();
-                        throw new pytorch.Error('File format is not PyTorch (' + message.replace(/\.$/, '') + ').');
-                    }
-                    return new pytorch.Model(metadata, container);
-                });
+                        context.exception(new pytorch.Error(message.replace(/\.$/, '') + " in '" + identifier + "'."), fatal);
+                    });
+                }
+                catch (error) {
+                    const message = error && error.message ? error.message : error.toString();
+                    throw new pytorch.Error('File format is not PyTorch (' + message.replace(/\.$/, '') + ').');
+                }
+                return new pytorch.Model(metadata, container);
             });
         });
     }

+ 24 - 16
source/sklearn.js

@@ -3,6 +3,7 @@
 // Experimental
 
 var sklearn = sklearn || {};
+var pickle = pickle || require('./pickle');
 var zip = zip || require('./zip');
 
 sklearn.ModelFactory = class {
@@ -35,23 +36,21 @@ sklearn.ModelFactory = class {
     }
 
     open(context) {
-        return context.require('./pickle').then((pickle) => {
-            const identifier = context.identifier;
-            return sklearn.Metadata.open(context).then((metadata) => {
-                let container;
-                try {
-                    const buffer = context.stream.peek();
-                    container = new sklearn.Container(buffer, pickle, (error, fatal) => {
-                        const message = error && error.message ? error.message : error.toString();
-                        context.exception(new sklearn.Error(message.replace(/\.$/, '') + " in '" + identifier + "'."), fatal);
-                    });
-                }
-                catch (error) {
+        const identifier = context.identifier;
+        return sklearn.Metadata.open(context).then((metadata) => {
+            let container;
+            try {
+                const buffer = context.stream.peek();
+                container = new sklearn.Container(buffer, pickle, (error, fatal) => {
                     const message = error && error.message ? error.message : error.toString();
-                    throw new sklearn.Error('File is not scikit-learn (' + message.replace(/\.$/, '') + ').');
-                }
-                return new sklearn.Model(metadata, container);
-            });
+                    context.exception(new sklearn.Error(message.replace(/\.$/, '') + " in '" + identifier + "'."), fatal);
+                });
+            }
+            catch (error) {
+                const message = error && error.message ? error.message : error.toString();
+                throw new sklearn.Error('File is not scikit-learn (' + message.replace(/\.$/, '') + ').');
+            }
+            return new sklearn.Model(metadata, container);
         });
     }
 };
@@ -1114,6 +1113,12 @@ sklearn.Container = class {
         functionTable['__builtin__.frozenset'] = function(iterable) {
             return iterable ? iterable : [];
         };
+        functionTable['__builtin__.getattr'] = function(obj, name, defaultValue) {
+            if (Object.prototype.hasOwnProperty.call(obj, name)) {
+                return obj[name];
+            }
+            return defaultValue;
+        };
         functionTable['_codecs.encode'] = function(obj /*, econding */) {
             return obj;
         };
@@ -1289,6 +1294,9 @@ sklearn.Container = class {
                         else if (obj.__module__.startsWith('xgboost.')) {
                             return 'XGBoost' + (obj._sklearn_version ? ' v' + obj._sklearn_version.toString() : '');
                         }
+                        else if (obj.__module__.startsWith('lightgbm.')) {
+                            return 'LightGBM Pickle';
+                        }
                         else if (obj.__module__.startsWith('nolearn.lasagne.')) {
                             return 'Lasagne';
                         }

+ 1 - 0
source/view.js

@@ -1249,6 +1249,7 @@ view.ModelFactoryService = class {
         this.register('./dl4j', [ '.zip' ]);
         this.register('./mlnet', [ '.zip' ]);
         this.register('./acuity', [ '.json' ]);
+        this.register('./lightgbm', [ '.txt' ]);
     }
 
     register(id, extensions) {

+ 7 - 0
test/models.json

@@ -2337,6 +2337,13 @@
     "script": "./tools/tf sync install zoo",
     "link":   "https://www.tensorflow.org/api_docs/python/tf/keras/applications"
   },
+  {
+    "type":   "lightgbm",
+    "target": "simple_example.txt",
+    "source": "https://github.com/lutzroeder/netron/files/5832361/simple_example.txt.zip[simple_example.txt]",
+    "format": "LightGBM v3",
+    "link":   "https://github.com/lutzroeder/netron/issues/669"
+  },
   {
     "type":   "mediapipe",
     "target": "clipped_images_from_file_at_24fps.pbtxt",