解决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,把原图表的所有数据和配置复刻过去:
- 提前保存原图表的核心数据:比如t-SNE嵌入结果
X_2d、标签testlabels、颜色配置、初始距离数据norms等。 - 创建新的Figure和Axes,完全复刻原图表的坐标轴配置、静态散点、颜色条、动态散点等元素。
- 如果需要保留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操作找不到有效资源。可以:
- 确保每个嵌入的图表都使用独立的Figure实例,每个实例维护自己的
background和titleBackground缓存。 - 如果不需要同时更新两个图表的动态内容,可以只在一个图表上用blitting优化性能,另一个图表定期全量重绘。
- 把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
相关产品推荐
相关产品推荐

