You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

解决Matplotlib散点图Pickle报错:kiwisolver.Solver无法序列化

问题描述

我在项目里尝试把同一个Matplotlib图表嵌入到两个不同位置。因为原图表用了blitting技术,嵌入两处后关闭其中一处会导致Matplotlib的imshow图表崩溃(散点图无此问题),所以得复制图表。我试着用pickle序列化/反序列化来复制图表,但散点图报错:TypeError: cannot pickle 'kiwisolver.Solver' object。下面是嵌入函数和报错的散点图生成代码,求解决pickle问题或者绕过imshow崩溃的方案。

嵌入函数代码

def embedPlot(self, fig):
    fig = pickle.loads(pickle.dumps(fig))
    temp = self.currentEmbed if self.currentEmbed else None

    # 设置基于屏幕尺寸和dpi的最大宽高
    fig.set_size_inches(self.root.winfo_screenwidth()/self.root.winfo_fpixels('1i')-1, self.root.winfo_screenheight()/self.root.winfo_fpixels('1i')-1)
    self.currentEmbed = Frame(self.root)
    canvas = FigureCanvasTkAgg(fig, master = self.currentEmbed)
    canvas.draw()
    canvas.get_tk_widget().pack()
    self.currentEmbed.grid(row=0, column=0, padx=2, pady=2, columnspan=4)

    # 每次删除旧画布,因为创建新FigureCanvasTkAgg会导致关闭窗口时严重卡顿(可能有更好的方法)
    if temp:
        temp.destroy()

散点图生成代码

def blitgenerateTSNEPlots():
    def getTSNE(idx):
        # 获取当前示例对应所有点的最近点和距离
        norms,idxs,prediction = findNearest(exdata,exoutput,getTSNE.advdata,idx,displayEpsilon)

        # 恢复背景,清除前景并允许重绘元素
        getTSNE.fig.canvas.restore_region(getTSNE.background)
        getTSNE.fig.canvas.restore_region(getTSNE.titleBackground)

        # 更新散点图的颜色数组、最近10个点的位置偏移,以及标题文本
        getTSNE.scatterPlot.set_array(norms)
        getTSNE.cb.set_offsets([ [getTSNE.X_2d[i,0], getTSNE.X_2d[i,1]] for i in idxs])
        getTSNE.title.set_text(f"Model Prediction: {prediction}\nAverage Distance: {round(float(sum(norms))/len(norms),2)}")

        # 重绘元素
        getTSNE.ax2.draw_artist(getTSNE.title)
        getTSNE.ax2.draw_artist(getTSNE.scatterPlot)
        getTSNE.ax2.draw_artist(getTSNE.cb)

        # 对坐标轴和文本的边界框执行blit操作,更新图表
        getTSNE.fig.canvas.blit(getTSNE.ax2.bbox)
        getTSNE.fig.canvas.blit(getTSNE.title.get_window_extent())

        getTSNE.fig.canvas.flush_events()

        return getTSNE.fig
    # 创建图表并关闭所有坐标轴刻度
    getTSNE.fig, (getTSNE.ax1, getTSNE.ax2) = plt.subplots(1,2,constrained_layout=True)
    getTSNE.ax1.set_xticks([])
    getTSNE.ax1.set_yticks([])
    getTSNE.ax2.set_xticks([])
    getTSNE.ax2.set_yticks([])

    # 加载指定epsilon的对抗样本数据,并获取初始点(idx=0)的最近点
    getTSNE.advdata = get_data(npys,displayEpsilon)
    norms,idxs,prediction = findNearest(exdata,exoutput,getTSNE.advdata,0,displayEpsilon)

    # 基于原始数据生成t-SNE嵌入
    X_2d = []
    if os.path.exists("./embedding.npy"):
        X_2d = np.load('./embedding.npy').astype(np.float64)
    else:
        tsne = TSNE(n_components=2, random_state=4, perplexity=100)
        origdata = get_data(npys,'e0')
        X_2d = tsne.fit_transform(origdata)
        np.save('./embedding.npy', X_2d, allow_pickle=False)
    getTSNE.X_2d = X_2d

    # 创建按类别着色的静态散点图(无需更新)
    colors = 'r', 'g', 'b', 'c', 'm', 'y', 'k', 'aquamarine', 'orange', 'purple'
    for c, label in zip(colors, labels):
        getTSNE.ax1.scatter(X_2d[(testlabels[:] == label), 0], X_2d[(testlabels[:] == label), 1], c=c, label=label, s=3)
    getTSNE.ax1.set_title("Test Data")
    getTSNE.ax1.legend()

    # 设置空白标题(预留足够空间用于blitting背景)
    getTSNE.title = getTSNE.ax2.set_title("  \n                                                     ")

    colorLim = (4,13)

    # 在创建第二个散点图前手动生成颜色条
    getTSNE.fig.colorbar(matplotlib.cm.ScalarMappable(norm=matplotlib.colors.Normalize(vmin=colorLim[0],vmax=colorLim[1]),cmap='viridis'),ax=[getTSNE.ax1,getTSNE.ax2],label="norm")

    # 绘制图表并保存散点图和标题的背景缓存
    getTSNE.background = getTSNE.fig.canvas.copy_from_bbox(getTSNE.ax2.bbox)
    getTSNE.fig.canvas.draw()
    getTSNE.titleBackground = getTSNE.fig.canvas.copy_from_bbox(getTSNE.title.get_window_extent())

    # 创建动态散点图:所有点按与当前示例的距离着色,并标记最近10个点
    getTSNE.scatterPlot = getTSNE.ax2.scatter(X_2d[:,0], X_2d[:,1], c=norms[:], s=3, cmap='viridis', zorder=1)
    getTSNE.cb = getTSNE.ax2.scatter(X_2d[idxs,0],X_2d[idxs,1], c='red', s=7, zorder=2)
    getTSNE.scatterPlot.set_clim(colorLim[0],colorLim[1])

    return getTSNE
generateTSNEPlots = blitgenerateTSNEPlots()
解决方案

思路1:放弃pickle,手动复制图表元素

Matplotlib的Figure对象包含大量不可序列化的底层组件(比如kiwisolver的布局求解器),pickle复制根本走不通。可以手动生成新的Figure,把原图表的所有数据和配置复刻过去:

  1. 提前保存原图表的核心数据:比如t-SNE嵌入结果X_2d、标签testlabels、颜色配置、初始距离数据norms等。
  2. 创建新的Figure和Axes,完全复刻原图表的坐标轴配置、静态散点、颜色条、动态散点等元素。
  3. 如果需要保留blitting功能,给新图表重新初始化背景缓存。

示例修改后的embedPlot核心逻辑:

def embedPlot(self):
    # 创建独立的新图表
    new_fig = plt.figure(constrained_layout=True)
    new_ax1, new_ax2 = new_fig.subplots(1,2)
    
    # 复刻静态ax1的配置和内容
    new_ax1.set_xticks([])
    new_ax1.set_yticks([])
    new_ax1.set_title("Test Data")
    colors = 'r', 'g', 'b', 'c', 'm', 'y', 'k', 'aquamarine', 'orange', 'purple'
    for c, label in zip(colors, labels):
        new_ax1.scatter(X_2d[(testlabels[:] == label), 0], X_2d[(testlabels[:] == label), 1], c=c, label=label, s=3)
    new_ax1.legend()
    
    # 复刻动态ax2的初始配置和内容
    new_ax2.set_xticks([])
    new_ax2.set_yticks([])
    colorLim = (4,13)
    new_fig.colorbar(matplotlib.cm.ScalarMappable(norm=matplotlib.colors.Normalize(vmin=colorLim[0],vmax=colorLim[1]),cmap='viridis'),ax=[new_ax1, new_ax2],label="norm")
    new_scatter = new_ax2.scatter(X_2d[:,0], X_2d[:,1], c=norms[:], s=3, cmap='viridis', zorder=1)
    new_cb = new_ax2.scatter(X_2d[idxs,0],X_2d[idxs,1], c='red', s=7, zorder=2)
    new_scatter.set_clim(colorLim[0],colorLim[1])
    new_title = new_ax2.set_title(f"Model Prediction: {prediction}\nAverage Distance: {round(float(sum(norms))/len(norms),2)}")
    
    # 后续嵌入逻辑和原代码一致
    temp = self.currentEmbed if self.currentEmbed else None
    new_fig.set_size_inches(self.root.winfo_screenwidth()/self.root.winfo_fpixels('1i')-1, self.root.winfo_screenheight()/self.root.winfo_fpixels('1i')-1)
    self.currentEmbed = Frame(self.root)
    canvas = FigureCanvasTkAgg(new_fig, master = self.currentEmbed)
    canvas.draw()
    canvas.get_tk_widget().pack()
    self.currentEmbed.grid(row=0, column=0, padx=2, pady=2, columnspan=4)
    
    if temp:
        temp.destroy()

这种方法虽然繁琐,但每个嵌入的图表都是独立实例,完全不会互相影响,从根源上解决了blitting缓存冲突的问题。

思路2:修复blitting导致的imshow崩溃

imshow崩溃的本质是多个Canvas共享同一个Figure的blitting缓存,关闭一个Canvas后缓存被释放,另一个Canvas的blitting操作找不到有效资源。可以:

  1. 确保每个嵌入的图表都使用独立的Figure实例,每个实例维护自己的background和titleBackground缓存。
  2. 如果不需要同时更新两个图表的动态内容,可以只在一个图表上用blitting优化性能,另一个图表定期全量重绘。
  3. 把blitting的更新逻辑封装成通用函数,每个独立的Figure实例都能调用该函数更新自身内容。

思路3:尝试Matplotlib内置复制功能(有限支持)

Matplotlib提供了matplotlib.figure.copy函数,可尝试用它复制Figure,但该功能仅支持部分元素,对带动态blitting的图表兼容性有限,使用示例:

from matplotlib.figure import copy
new_fig = copy(original_fig)

注意:复制后需要重新初始化blitting的背景缓存,否则动态更新会失效。

内容的提问来源于stack exchange,提问作者alex_x90

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.20 06:39:34