使用 MVC 设计模式时如何加载重型模型

How to load a heavy model when using MVC design pattern

我正在使用 wxPython 和我使用 Tensorflow 创建的深度学习模型构建应用程序。我使用的设计模式是 MVC。 我的问题是深度学习模型非常繁重,加载需要很长时间(大约 2 分钟),同时 GUI 挂起。 我创建了一个描述该过程的示例代码。 这是加载时 GUI 的样子:

enter image description here

这是加载后 GUI 的样子: enter image description here

问题是如何在加载模型时将应用程序获取到 运行? 我还想向 GUI 添加一个状态行,指示模型正在加载或已经加载。

我正在添加展示我的应用程序构建方式的示例代码。

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()

您应该在 self.view.Show() 命令之后调用 wx.GetApp().Yield(),这会暂时将控制权交给 MainLoop
如果模型加载代码是增量执行的,你也可以在加载过程中定期调用Yield。
下面是一种通知用户某事正在发生的简单方法。如果你想要取消模型加载的选项,你必须将它包装在一个对话框中,假设它是增量加载的。

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()

您可以使用 _thread 模块和 PyPubSub 模块在模型加载期间保持主线程完全正常运行。

但是,请记住,如果您在 GUI 中有一个 wx.Button 绑定到 method A 并且 method A 需要正确加载完整模型 运行 , 然后用户将能够单击 wx.Button 并且程序可能会挂起,因为模型仍未完全加载。如果是这种情况,您可以使用方法 Disable()(在加载模型时)和 Enable()(在加载模型后)来防止这种情况发生。

带注释的代码 (####)。

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()

如果您从 class View 中的方法加载模型,那么您将不需要 PyPubSub 模块,因为您只需调用 wx.CallAfter(self.ShowLoadProgress, x)

只是为了好玩,因为我更喜欢 kbr85 给我的第一个简单答案的答案,这里有一个线程变体,在 statusbar 中有一个 gauge 和一个忙碌光标,尽管我的屏幕截图程序没捡到。
有一个 Stop 按钮,加载完成后 statusbar 将被删除。
我没有使用 pubsub,而是使用 wxpython event 进行通信。

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()