How to load a heavy model when using MVC design pa

2020-03-31 08:58发布

问题:

I'm building an application with wxPython and a Deep Learning model that I created using Tensorflow. The design pattern that I'm using is MVC. My problem is that the deep learning model is very heavy and it takes a very long time to load (like ~2 minutes) and in the meantime the GUI hangs. I created a sample code that describes the process. Here is what the GUI looks like while loading:

enter image description here

and this is what the GUI looks like after the loading: enter image description here

The question is how do I get the application to run while the model is loading? I would also like to add a status line to the GUI indicating that the model is loading or already loaded.

I'm adding the sample code that shows how my application is built.

import wx
import time

class Model:
    def __init__(self):
        '''This part is simulating the loading of tensorflow'''
        x = 0
        while x < 15:
            time.sleep(1)
            print(x)
            x += 1

class View(wx.Frame):
    def __init__(self, parent, title):
        super(View, self).__init__(parent, title=title, size=(400, 400))
        self.InitUI()

    def InitUI(self):
        # Defines the GUI controls
        masterPanel = wx.Panel(self)
        masterPanel.SetBackgroundColour("gold")
        self.vbox = wx.BoxSizer(wx.VERTICAL)
        self.fgs = wx.FlexGridSizer(6, 2, 10, 25)
        id = wx.StaticText(self, label="ID:")
        firstName = wx.StaticText(self, label="First name:")
        lastName = wx.StaticText(self, label="Last name:")
        self.idTc = wx.TextCtrl(self)
        self.firstNameTc = wx.TextCtrl(self)
        self.lastNameTc = wx.TextCtrl(self)
        self.fgs.AddMany([id, (self.idTc, 1, wx.EXPAND),
                         firstName, (self.firstNameTc, 1, wx.EXPAND),
                         lastName, (self.lastNameTc, 1, wx.EXPAND)])
        self.vbox.Add(self.fgs, proportion=1, flag=wx.ALL | wx.EXPAND,
   border=15)
        self.SetSizer(self.vbox)
        self.vbox.Fit(self)
        self.Layout()

class Controller:
    def __init__(self):
        self.view = View(None, title='Test')
        self.view.Show()
        self.model = Model()

def main():
    app = wx.App()
    controller = Controller()
    app.MainLoop()

if __name__ == '__main__':
    main()

回答1:

You should call wx.GetApp().Yield() after the self.view.Show() command, which releases control momentarily to the MainLoop.
If the model load code is performed in increments, you can call Yield periodically during the load as well.
Below is a simple method of informing the user that something is going on. If you wanted the option of cancelling the model load, you would have to wrap it in a dialog, assuming that it is loaded incrementally.

import wx
import time

class Model:
    def __init__(self):
        '''This part is simulating the loading of tensorflow'''
        x = 0
        #If the model load is perform in increments you could call wx.Yield
        # between the increments.
        while x < 15:
            time.sleep(1)
            wx.GetApp().Yield()
            print(x)
            x += 1

class View(wx.Frame):
    def __init__(self, parent, title):
        super(View, self).__init__(parent, title=title, size=(400, 400))
        self.InitUI()

    def InitUI(self):
        # Defines the GUI controls
        #masterPanel = wx.Panel(self)
        #masterPanel.SetBackgroundColour("gold")
        self.vbox = wx.BoxSizer(wx.VERTICAL)
        self.fgs = wx.FlexGridSizer(6, 2, 10, 25)
        id = wx.StaticText(self, label="ID:")
        firstName = wx.StaticText(self, label="First name:")
        lastName = wx.StaticText(self, label="Last name:")
        self.idTc = wx.TextCtrl(self)
        self.firstNameTc = wx.TextCtrl(self)
        self.lastNameTc = wx.TextCtrl(self)
        self.fgs.AddMany([id, (self.idTc, 1, wx.EXPAND),
                         firstName, (self.firstNameTc, 1, wx.EXPAND),
                         lastName, (self.lastNameTc, 1, wx.EXPAND)])
        self.vbox.Add(self.fgs, proportion=1, flag=wx.ALL | wx.EXPAND,
   border=15)
        self.CreateStatusBar() # A Statusbar in the bottom of the window
        self.StatusBar.SetStatusText("No model loaded", 0)
        self.SetSizer(self.vbox)
        self.vbox.Fit(self)
        self.Layout()

class Controller:
    def __init__(self):
        self.view = View(None, title='Test')
        self.view.Show()
        self.view.SetStatusText("Model loading", 0)
        wait = wx.BusyInfo("Please wait, loading model...")
        #Optionally add parent to centre message on self.view
        #wait = wx.BusyInfo("Please wait, loading model...", parent=self.view)
        wx.GetApp().Yield()
        self.model = Model()
        self.view.SetStatusText("Model loaded", 0)
        del wait

def main():
    app = wx.App()
    controller = Controller()
    app.MainLoop()

if __name__ == '__main__':
    main()


回答2:

You could use the _thread module and the PyPubSub module to keep the main thread completely functional during the loading of the model.

However, keep in mind that if you have a wx.Button in the GUI bound to method A and method A requires the full model to be loaded to properly run, then users will be able to click the wx.Button and the program will probably hang because the model is still not fully loaded. If this is the case, you could use the methods Disable() (while the model is being loaded) and Enable() (after the model is loaded) to prevent this.

Code with comments (####).

import wx
import time
import _thread
from pubsub import pub

class Model:
    def __init__(self):
        '''This part is simulating the loading of tensorflow'''
        x = 0
        while x < 15:
            time.sleep(1)
            print(x)
            #### This is how you broadcast the 'Loading' message 
            #### from a different thread.
            wx.CallAfter(pub.sendMessage, 'Loading', x=x)
            x += 1
        #### The same as before
        wx.CallAfter(pub.sendMessage, 'Loading', x=x)

class View(wx.Frame):
    def __init__(self, parent, title):
        super(View, self).__init__(parent, title=title, size=(400, 400))
        self.InitUI()

    def InitUI(self):
        # Defines the GUI controls
        #### It needed to set the size of the panel to cover the frame
        #### because it was not covering the entire frame before
        masterPanel = wx.Panel(self, size=(400, 400))
        masterPanel.SetBackgroundColour("gold")
        self.vbox = wx.BoxSizer(wx.VERTICAL)
        self.fgs = wx.FlexGridSizer(6, 2, 10, 25)
        id = wx.StaticText(self, label="ID:")
        firstName = wx.StaticText(self, label="First name:")
        lastName = wx.StaticText(self, label="Last name:")
        self.idTc = wx.TextCtrl(self)
        self.firstNameTc = wx.TextCtrl(self)
        self.lastNameTc = wx.TextCtrl(self)
        self.fgs.AddMany([id, (self.idTc, 1, wx.EXPAND),
                         firstName, (self.firstNameTc, 1, wx.EXPAND),
                         lastName, (self.lastNameTc, 1, wx.EXPAND)])
        self.vbox.Add(self.fgs, proportion=1, flag=wx.ALL | wx.EXPAND,
   border=15)
        self.SetSizer(self.vbox)
        self.vbox.Fit(self)
        self.Layout()

        #### Create status bar to show loading progress. 
        self.statusbar = self.CreateStatusBar(1)
        self.statusbar.SetStatusText('Loading model')
        #### Set the size of the window because the status bar steals space
        #### in the height direction.
        self.SetSize(wx.DefaultCoord, 160)
        #### Subscribe the class to the message 'Loading'. This means that every
        #### time the meassage 'Loading' is broadcast the method 
        #### ShowLoadProgress will be executed.
        pub.subscribe(self.ShowLoadProgress, 'Loading')
        #### Start the thread that will load the model
        _thread.start_new_thread(self.LoadModel, ('test',))

    def LoadModel(self, test):
        """
        Load the Model
        """
        #### Change depending on how exactly are you going to create/load the 
        #### model
        self.model = Model()

    def ShowLoadProgress(self, x):
        """
        Show the loading progress 
        """
        if x < 15:
            self.statusbar.SetStatusText('Loading progress: ' + str(x))
        else:
            self.statusbar.SetStatusText('All loaded')

class Controller:
    def __init__(self):
        self.view = View(None, title='Test')
        self.view.Show()
        #### The line below is not needed now because the model is 
        #### loaded now from the thread started in View.InitUI
        #self.model = Model()

def main():
    app = wx.App()
    controller = Controller()
    app.MainLoop()

if __name__ == '__main__':
    main()

If you load the model from a method inside class View then you will not need the PyPubSub module because you could just call wx.CallAfter(self.ShowLoadProgress, x)



回答3:

Just for fun and because I prefer the answer that kbr85 gave to my simplistic first answer, here's a Threaded variant with a gauge in the statusbar and a Busy cursor, although my screenshot program didn't pick it up.
There's a Stop button and the statusbar is removed once the load finishes.
Rather than use pubsub, I've used a wxpython event to communicate.

import wx
import time
from threading import Thread
import wx.lib.newevent
progress_event, EVT_PROGRESS_EVENT = wx.lib.newevent.NewEvent()
load_status=["Model Loading","Model Loaded","Model Cancelled"]

class Model(Thread):
    def __init__(self,parent):
        Thread.__init__(self)
        '''This thread simulates the loading of tensorflow'''
        self.stopthread = 0
        self.target = parent
        self.start()

    def run(self):
        while not self.stopthread:
            for i in range(20):
                if self.stopthread:
                    break
                time.sleep(0.5)
                evt = progress_event(count=i, status=self.stopthread)
                wx.PostEvent(self.target, evt)
            if self.stopthread == 0:
                self.stopthread = 1
        evt = progress_event(count=i, status=self.stopthread)
        wx.PostEvent(self.target, evt)

    def terminate(self):
        self.stopthread = 2

class View(wx.Frame):
    def __init__(self, parent, title):
        super(View, self).__init__(parent, title=title, size=(400, 400))
        self.InitUI()

    def InitUI(self):
        self.vbox = wx.BoxSizer(wx.VERTICAL)
        self.fgs = wx.FlexGridSizer(6, 2, 10, 25)
        id = wx.StaticText(self, label="ID:")
        firstName = wx.StaticText(self, label="First name:")
        lastName = wx.StaticText(self, label="Last name:")
        self.idTc = wx.TextCtrl(self)
        self.firstNameTc = wx.TextCtrl(self)
        self.lastNameTc = wx.TextCtrl(self)
        self.stop = wx.Button(self, -1, "Stop Load")

        self.fgs.AddMany([id, (self.idTc, 1, wx.EXPAND),
                         firstName, (self.firstNameTc, 1, wx.EXPAND),
                         lastName, (self.lastNameTc, 1, wx.EXPAND),
                         (self.stop,1,wx.EXPAND)])

        self.vbox.Add(self.fgs, proportion=1, flag=wx.ALL | wx.EXPAND,border=15)
        #Bind to the progress event issued by the thread
        self.Bind(EVT_PROGRESS_EVENT, self.OnProgress)
        #Bind to Stop button
        self.Bind(wx.EVT_BUTTON, self.OnStop)
        #Bind to Exit on frame close
        self.Bind(wx.EVT_CLOSE, self.OnExit)
        self.SetSizer(self.vbox)
        self.Layout()

        self.statusbar = self.CreateStatusBar(2)
        self.text = wx.StaticText(self.statusbar,-1,("No Model loaded"))
        self.progress = wx.Gauge(self.statusbar, range=20)
        sizer = wx.BoxSizer(wx.HORIZONTAL)
        sizer.Add(self.text, 0, wx.ALIGN_TOP|wx.ALL, 5)
        sizer.Add(self.progress, 1, wx.ALIGN_TOP|wx.ALL, 5)
        self.statusbar.SetSizer(sizer)
        wx.BeginBusyCursor()
        self.loadthread = Model(self)


    def OnProgress(self, event):
        self.text.SetLabel(load_status[event.status])
        #self.progress.SetValue(event.count)
        #or for indeterminate progress
        self.progress.Pulse()
        if event.status != 0:
            self.statusbar.Hide()
            wx.EndBusyCursor()
            self.Layout()

    def OnStop(self, event):
        if self.loadthread.isAlive():
            self.loadthread.terminate() # Shutdown the thread
            self.loadthread.join() # Wait for it to finish

    def OnExit(self, event):
        if self.loadthread.isAlive():
            self.loadthread.terminate() # Shutdown the thread
            self.loadthread.join() # Wait for it to finish
        self.Destroy()

class Controller:
    def __init__(self):
        self.view = View(None, title='Test')
        self.view.Show()

def main():
    app = wx.App()
    controller = Controller()
    app.MainLoop()

if __name__ == '__main__':
    main()