0


基于GMM的一维时序数据平滑算法

本文将介绍我们使用高斯混合模型(GMM)算法作为一维数据的平滑和去噪算法。

假设我们想要在音频记录中检测一个特定的人的声音,并获得每个声音片段的时间边界。例如,给定一小时的流,管道预测前10分钟是前景(我们感兴趣的人说话),然后接下来的20分钟是背景(其他人或没有人说话),然后接下来的20分钟是前景段,最后10分钟属于背景段。

有一种方法是预测每个语音段的边界,然后对语音段进行分类。但是如果我们错过了一个片段,那么这个错误将会使整个片段产生错误。想要解决这题我们可以使用GMM smooth,音频检测器生成时间范围片段和每个片段的标签。GMM smooth的输入数据是这些段,它可以帮助我们来降低最终预测中的噪声。

高斯混合模型

在深入GMM之前,必须首先了解高斯分布。高斯分布是一种概率分布,由两个参数定义:平均值(或期望)和标准差(STD)。在统计学中,平均值是指数据集的平均值,而标准偏差(STD)衡量数据的变化或分散程度。STD表示每个数据点与平均值之间的距离,在高斯分布中,大约68%的数据落在平均值的一个STD内。

GMM是一种参数概率模型。它假设在给定的一组数据点中,每一个单点都是由一个高斯分布产生的,给定一组K个高斯分布[7]。GMM的目标是从K个分布中为每个数据点分配一个高斯分布。换句话说,GMM解决的任务是聚类任务或无监督任务。

GMMs通常用作生物识别系统中连续测量或特征的概率分布的参数模型,例如说话人识别系统中与声道相关的频谱特征。使用迭代期望最大化(EM)算法或来自训练良好的先验模型的最大后验(MAP)估计从训练数据中估计GMM参数[8]。

基于 GMM 的平滑器

我们的目标是解决时间概念定位问题,比如输出如下所示:[[StartTime1, EndTime1, Class1], [StartTime2, EndTime2, Class2], …]。 如果我们想直观地展示一下,可以像下图这样:

但是因为误差而产生很大的噪声,如下所示:

我们的目标只是减少噪声(并使用本文后面描述的方法测量噪声)。可以看到背景预测更常见(橙色),也就是说我们正在寻找的说话者的“标记”音频片段更频繁地被预测为“其他说话者”或“没有说话”。

可以看到噪声预测与真实预测相比具有较小的长度,所以可以得出结论,噪声预测是可以与真实预测分离的。我们将预测的长度建模为高斯分布的混合,使用GMM作为噪声平滑算法来解决这个问题。

代码和解释

完整的代码可以在下面的代码块中看到:

  1. fromcopyimportdeepcopy
  2. importnumpyasnp
  3. frommatplotlibimportpyplotasplt
  4. importpandasaspd
  5. fromsklearn.mixtureimportGaussianMixture
  6. importlogging
  7. logger=logging.getLogger()
  8. logger.setLevel(logging.INFO)
  9. logger.addHandler(logging.StreamHandler())
  10. classGMMSmoother:
  11. """
  12. This class is the main class of the Smoother. It performs a smoothing to joint segments
  13. """
  14. def__init__(self, min_samples=10):
  15. # The minimum number of samples for applying GMM
  16. self.min_samples=min_samples
  17. # Logger instance
  18. self.logger=logger
  19. defsmooth_segments_gmm(self, segments, gmm_segment_class='background', bg_segment_class='foreground'):
  20. """
  21. This method performs the smoothing using Gaussian Mixture Model (GMM) (for more information about GMM
  22. please visit: https://scikit-learn.org/stable/modules/mixture.html). It calculates two GMMs: first with one
  23. gaussian component and the second with two components. Then, it selects the best model using AIC, and BIC metrics.
  24. After we choose the best model, we perform a clustering of tew clusters: real or fake
  25. Please note that the GMMs don't use the first and last segments because in our case
  26. the stream's time limit is an hour and we don't have complete statistics on
  27. the lengths of the first and last segments.
  28. :param segments: a list of dictionaries, each dict represents a segment
  29. :param gmm_segment_class: the segment class of the "reals"
  30. :param bg_segment_class: the segment class of the "fakes"
  31. :return:
  32. segments_copy: the smoothed version of segments
  33. """
  34. self.logger.info("Begin smoothing using Gaussian Mixture Model (GMM)")
  35. # Some instancing
  36. preds_map= {0: bg_segment_class, 1: gmm_segment_class}
  37. gmms_results_dict= {}
  38. # Copy segments to a new variable
  39. segments_copy=deepcopy(segments)
  40. self.logger.info("Create input data for GMM")
  41. # Keep the gmm_segment_class data points and perform GMM on them.
  42. # For example: gmm_segment_class = 'background'
  43. segments_filtered= {i: sfori, sinenumerate(segments_copy) if
  44. s['segment'] ==gmm_segment_classand (i>0andi<len(segments_copy) -1)}
  45. # Calcualte the length of each segment
  46. X=np.array([[(s['endTime'] -s['startTime']).total_seconds()] for_, sinsegments_filtered.items()])
  47. # Check if the length of data points is less than the minimum.
  48. # If it is, do not apply GMM!
  49. iflen(X) <=self.min_samples:
  50. self.logger.warning(f"Size of input ({len(X)} smaller than min simples ({self.min_samples}). Do not perform smoothing.)")
  51. returnsegments
  52. # Go over 1 and 2 components and calculate statistics
  53. best_fitting_score=np.Inf
  54. self.logger.info("Begin to fit GMMs with 1 and 2 components.")
  55. foriin [1, 2]:
  56. # For each number of component (1 or 2), fit GMM
  57. gmm=GaussianMixture(n_components=i, random_state=0, tol=10**-6).fit(X)
  58. # Calculate AIC and BIC and the average between them
  59. aic, bic=gmm.aic(X), gmm.bic(X)
  60. fitting_score= (aic+bic) /2
  61. # If the average is less than the best score, replace them
  62. iffitting_score<best_fitting_score:
  63. best_model=gmm
  64. best_fitting_score=fitting_score
  65. gmms_results_dict[i] = {"model": gmm, "fitting_score": fitting_score, "aic": aic, "bic": bic}
  66. self.logger.info(f"GMM with {best_model.n_components} components was selected")
  67. # If the number of components is 1, change the label to the points that
  68. # have distance from the mean that is bigger than 2*STD
  69. ifbest_model.n_components==1:
  70. mean=best_model.means_[0, 0]
  71. std=np.sqrt(best_model.covariances_[0, 0])
  72. model_preds= [0ifx<mean-2*stdelse1forxinrange(len(X))]
  73. # If the number of components is 2, assign a label to each data point,
  74. # and replace the label to the points that assigned to the low mean Gaussian
  75. else:
  76. ifnp.linalg.norm(best_model.means_[0]) >np.linalg.norm(best_model.means_[1]):
  77. preds_map= {1: bg_segment_class, 0: gmm_segment_class}
  78. model_preds=best_model.predict(X)
  79. self.logger.info("Replace previous predictions with GMM predictions")
  80. # Perform smoothing
  81. fori, (k, s) inenumerate(segments_filtered.items()):
  82. ifs['segment'] !=preds_map[model_preds[i]]:
  83. s['segment'] =preds_map[model_preds[i]]
  84. segments_copy[k] =s
  85. self.logger.info("Merge segments")
  86. # Join consecutive segments after the processing
  87. segments_copy=join_consecutive_segments(segments_copy)
  88. returnsegments_copy
  89. @staticmethod
  90. defplot_bars(res_dict_objs, color_dict={"foreground": "#DADDFC", "background": '#FC997C', "null": "#808080"}, channel="",
  91. start_time="", end_time="", snrs=None, titles=['orig', 'smoothed'],
  92. save=False, save_path="", show=True):
  93. """
  94. Inspired by https://stackoverflow.com/questions/70142098/stacked-horizontal-bar-showing-datetime-areas
  95. This function is for visualizing the smoothing results
  96. of multiple segments' lists
  97. :param res_dict_objs: a list of lists. Each list is a segments list to plot
  98. :param color_dict: dictionary which represents the mapping between class to color in the plot
  99. :param channel: channel number
  100. :param start_time: absolute start time
  101. :param end_time: absolute end time
  102. :param snrs: list of snrs to display in the title
  103. :param titles: title to each subplot
  104. :param save: flag to save the figure into a png file
  105. :param save_path: save path of the figure
  106. :param show: flag to show the figure
  107. """
  108. ifsnrs==None:
  109. snrs= [''] *len(res_dict_objs)
  110. iftype(res_dict_objs) !=list:
  111. res_dict_objs= [res_dict_objs]
  112. fig, ax=plt.subplots(len(res_dict_objs), 1, figsize=(20, 10))
  113. fig.suptitle(f"Channel {channel}, {start_time}-{end_time}\n{snrs[0]}\n{snrs[1]}")
  114. fordict_idx, res_dictinenumerate(res_dict_objs):
  115. date_from= [a['startTime'] forainres_dict]
  116. date_to= [a['endTime'] forainres_dict]
  117. segment= [a['segment'] forainres_dict]
  118. df=pd.DataFrame({'date_from': date_from, 'date_to': date_to,
  119. 'segment': segment})
  120. foriinrange(df.shape[0]):
  121. ax[dict_idx].plot([df['date_from'][i], df['date_to'][i]], [1, 1],
  122. linewidth=50, c=color_dict[df['segment'][i]])
  123. ax[dict_idx].set_yticks([])
  124. ax[dict_idx].set_yticklabels([])
  125. ax[dict_idx].set(frame_on=False)
  126. ax[dict_idx].title.set_text(titles[dict_idx])
  127. ifshow:
  128. plt.show()
  129. ifsave:
  130. plt.savefig(save_path)
  131. defjoin_consecutive_segments(seg_list):
  132. """
  133. This function is merged consecutive segments if they
  134. have the same segment class and create one segment. It also changes the
  135. start and the end times with respect to the joined segments
  136. :param seg_list: a list of dictionaries. Each dict represents a segment
  137. :return: joined_segments: a list of dictionaries, where the segments are merged
  138. """
  139. joined_segments=list()
  140. init_seg= {
  141. 'startTime': seg_list[0]['startTime'],
  142. 'endTime': seg_list[0]['endTime'],
  143. 'segment': seg_list[0]['segment']
  144. }
  145. collector=init_seg
  146. last_segment=init_seg
  147. last_segment=last_segment['segment']
  148. forseginseg_list:
  149. segment=seg['segment']
  150. start_dt=seg['startTime']
  151. end_dt=seg['endTime']
  152. prefiltered_type=segment
  153. ifprefiltered_type==last_segment:
  154. collector['endTime'] =end_dt
  155. else:
  156. joined_segments.append(collector)
  157. init_seg= {
  158. 'startTime': start_dt,
  159. 'endTime': end_dt,
  160. 'segment': prefiltered_type
  161. }
  162. collector=init_seg
  163. last_segment=init_seg
  164. last_segment=last_segment['segment']
  165. joined_segments.append(collector)
  166. returnjoined_segments
  167. defmain(seg_list):
  168. # Create GMMSmoother instance
  169. gmm_smoother=GMMSmoother()
  170. # Join consecutive segments that have the same segment label
  171. seg_list_joined=join_consecutive_segments(seg_list)
  172. # Perform smoothing on background class
  173. smoothed_segs_tmp=gmm_smoother.smooth_segments_gmm(seg_list_joined)
  174. # Perform smoothing on foreground class
  175. smoothed_segs_final=gmm_smoother.smooth_segments_gmm(smoothed_segs_tmp, gmm_segment_class='foreground', bg_segment_class='background') iflen(
  176. smoothed_segs_tmp) !=len(seg_list_joined) elsesmoothed_segs_tmp
  177. returnsmoothed_segs_final
  178. if__name__=="__main__":
  179. # The read_data_func should be implemented by the user,
  180. # depending on his needs.
  181. seg_list=read_data_func()
  182. res=main(seg_list)

下面我们解释关键块以及如何使用GMM来执行平滑:

1、输入数据

数据结构是一个字典列表。每个字典代表一个段预测,具有以下键值对: “startTime”,“endTime”和“segment”。下面是一个例子:

  1. {"startTime": ISODate("%Y-%m-%dT%H:%M:%S%z"), "endTime": ISODate("%Y-%m-%dT%H:%M:%S%z"), "segment": "background/foreground"}

“startTime”和“endTime”是段的时间边界,“segment”是它的类型。

2、连接连续段

假设输入数据具有相同标签的连续预测(并非所有输入数据都必须需要此阶段)。例如:

  1. # Input segments list
  2. seg_list = [{"startTime": ISODate("2022-11-19T00:00:00Z"), "endTime": ISODate("2022-11-19T01:00:00Z"), "segment": "background"},
  3. {"startTime": ISODate("2022-11-19T01:00:00Z"), "endTime": ISODate("2022-11-19T02:00:00Z"), "segment": "background"}]
  4. # Apply join_consecutive_segments on seg_list to join consecutive segments
  5. seg_list_joined = join_consecutive_segments(seg_list)
  6. # After applying the function, the new list should look like the following:
  7. # seg_list_joined = [{"startTime": ISODate("2022-11-19T00:00:00Z"), "endTime": ISODate("2022-11-19T02:00:00Z"), "segment": "background"}]

使用的join_consecutive_segments的代码如下:

  1. defjoin_consecutive_segments(seg_list):
  2. """
  3. This function is merged consecutive segments if they
  4. have the same segment class and create one segment. It also changes the
  5. start and the end times with respect to the joined segments
  6. :param seg_list: a list of dictionaries. Each dict represents a segment
  7. :return: joined_segments: a list of dictionaries, where the segments are merged
  8. """
  9. joined_segments=list()
  10. init_seg= {
  11. 'startTime': seg_list[0]['startTime'],
  12. 'endTime': seg_list[0]['endTime'],
  13. 'segment': seg_list[0]['segment']
  14. }
  15. collector=init_seg
  16. last_segment=init_seg
  17. last_segment=last_segment['segment']
  18. forseginseg_list:
  19. segment=seg['segment']
  20. start_dt=seg['startTime']
  21. end_dt=seg['endTime']
  22. prefiltered_type=segment
  23. ifprefiltered_type==last_segment:
  24. collector['endTime'] =end_dt
  25. else:
  26. joined_segments.append(collector)
  27. init_seg= {
  28. 'startTime': start_dt,
  29. 'endTime': end_dt,
  30. 'segment': prefiltered_type
  31. }
  32. collector=init_seg
  33. last_segment=init_seg
  34. last_segment=last_segment['segment']
  35. joined_segments.append(collector)
  36. returnjoined_segments

join_consecutive_segments将两个或多个具有相同预测的连续片段连接为一个片段。

3、删除当前迭代的不相关片段

我们的预测有更多的噪声,所以首先需要对它们进行平滑处理。从数据中过滤掉前景部分:

  1. # Copy segments to a new variable
  2. segments_copy=deepcopy(segments)
  3. # Keep the gmm_segment_class data points and perform GMM on them.
  4. # For example: gmm_segment_class = 'background'
  5. segments_filtered= {i: sfori, sinenumerate(segments_copy) ifs['segment'] ==gmm_segment_classand (i>0andi<len(segments_copy) -1)}

4、计算段的长度

以秒为单位计算所有段的长度。

  1. # Calcualte the length of each segment
  2. X=np.array([[(s['endTime'] -s['startTime']).total_seconds()] for_, sinsegments_filtered.items()])

5、GMM

仅获取背景片段的长度并将 GMM 应用于长度数据。 如果有足够的数据点(预定义数量——超参数),我们这里使用两个GMM:一个分量模型和两个分量模型。 然后使用贝叶斯信息准则 (BIC) 和 Akaike 信息准则 (AIC) 之间的平均值来选择最适合的 GMM。

  1. # Check if the length of data points is less than the minimum.
  2. # If it is, do not apply GMM!
  3. iflen(X) <=self.min_samples:
  4. self.logger.warning(f"Size of input ({len(X)} smaller than min simples ({self.min_samples}). Do not perform smoothing.)")
  5. returnsegments
  6. # Go over 1 and 2 number of components and calculate statistics
  7. best_fitting_score=np.Inf
  8. self.logger.info("Begin to fit GMMs with 1 and 2 components.")
  9. foriinrange(1, 3):
  10. # For each number of component (1 or 2), fit GMM
  11. gmm=GaussianMixture(n_components=i, random_state=0, tol=10**-6).fit(X)
  12. # Calculate AIC and BIC and the average between them
  13. aic, bic=gmm.aic(X), gmm.bic(X)
  14. fitting_score= (aic+bic) /2
  15. # If the average is less than the best score, replace them
  16. iffitting_score<best_fitting_score:
  17. best_model=gmm
  18. best_fitting_score=fitting_score
  19. gmms_results_dict[i] = {"model": gmm, "fitting_score": fitting_score, "aic": aic, "bic": bic}

6、选择最佳模型并进行平滑

如果选择了一个分量:将距离均值大于 2-STD 的数据点标记为前景,其余数据点保留为背景点。

如果选择了两个分量:将分配给低均值高斯的点标记为前景,将高均值高斯标记为背景。

  1. # If the number of components is 1, change the label to the points that
  2. # have distance from the mean that is bigger than 2*STD
  3. ifbest_model.n_components==1:
  4. mean=best_model.means_[0, 0]
  5. std=np.sqrt(best_model.covariances_[0, 0])
  6. model_preds= [0ifx<mean-2*stdelse1forxinrange(len(X))]
  7. # If the number of components is 2, assign a label to each data point,
  8. # and replace the label to the points that assigned to the low mean Gaussian
  9. else:
  10. ifnp.linalg.norm(best_model.means_[0]) >np.linalg.norm(best_model.means_[1]):
  11. preds_map= {1: bg_segment_class, 0: gmm_segment_class}
  12. model_preds=best_model.predict(X)
  13. self.logger.info("Replace previous predictions with GMM predictions")
  14. # Perform smoothing
  15. fori, (k, s) inenumerate(segments_filtered.items()):
  16. ifs['segment'] !=preds_map[model_preds[i]]:
  17. s['segment'] =preds_map[model_preds[i]]
  18. segments_copy[k] =s
  19. self.logger.info("Merge segments")

7、后处理

再次连接连续的片段产生并返回最终结果。

  1. # Join consecutive segments after the processing
  2. segments_copy=join_consecutive_segments(segments_copy)

8、重复这个过程

这是一个迭代的过程我们可以重复这个过程几次,来找到最佳结果

9、可视化

使用下面方法可以可视化我们的中间和最终的结果,并方便调试

  1. defplot_bars(res_dict_objs, color_dict={"foreground": "#DADDFC", "background": '#FC997C', "null": "#808080"}, channel="",
  2. start_time="", end_time="", snrs=None, titles=['orig', 'smoothed'],
  3. save=False, save_path="", show=True):
  4. """
  5. This function is for visualizing the smoothing results of multiple segments lists
  6. :param res_dict_objs: a list of lists. Each list is a segments list to plot
  7. :param color_dict: dictionary which represents the mapping between class to color in the plot
  8. :param channel: channel number
  9. :param start_time: absolute start time
  10. :param end_time: absolute end time
  11. :param snrs: list of snrs to display in the title
  12. :param titles: title to each subplot
  13. :param save: flag to save the figure into a png file
  14. :param save_path: save path of the figure
  15. :param show: flag to show the figure
  16. """
  17. ifsnrs==None:
  18. snrs= [''] *len(res_dict_objs)
  19. iftype(res_dict_objs) !=list:
  20. res_dict_objs= [res_dict_objs]
  21. fig, ax=plt.subplots(len(res_dict_objs), 1, figsize=(20, 10))
  22. fig.suptitle(f"Channel {channel}, {start_time}-{end_time}\n{snrs[0]}\n{snrs[1]}")
  23. fordict_idx, res_dictinenumerate(res_dict_objs):
  24. date_from= [a['startTime'] forainres_dict]
  25. date_to= [a['endTime'] forainres_dict]
  26. segment= [a['segment'] forainres_dict]
  27. df=pd.DataFrame({'date_from': date_from, 'date_to': date_to,
  28. 'segment': segment})
  29. foriinrange(df.shape[0]):
  30. ax[dict_idx].plot([df['date_from'][i], df['date_to'][i]], [1, 1],
  31. linewidth=50, c=color_dict[df['segment'][i]])
  32. ax[dict_idx].set_yticks([])
  33. ax[dict_idx].set_yticklabels([])
  34. ax[dict_idx].set(frame_on=False)
  35. ax[dict_idx].title.set_text(titles[dict_idx])
  36. ifshow:
  37. plt.show()
  38. ifsave:
  39. plt.savefig(save_path)

可视化结果如下图所示:

可以看到,在第一次迭代之后减少了背景类中的噪声,第二次迭代之后减少了前景类中的噪声。

结果展示

下面我们展示平滑算法的一些结果。并且还测量了信噪比(SNR)[10],得到了一些数值结果来评估算法。比较平滑前后,对前景类和背景类进行了两次信噪比。这里的淡紫色部分代表前景部分,橙色部分代表背景部分。

总结

在本文中探讨GMM作为时间数据平滑算法的使用。GMM(Gaussian Mixture Model)是一种统计模型,常用于数据聚类和密度估计。虽然它主要用于聚类任务,但也可以在一定程度上用作时间数据平滑算法。虽然它并不是专门为此任务设计的,但是对于这种类别相关的数据平滑,GMM在降噪和结果改善方面表现非常好(信噪比参数)。

引用:

[1] Girshick, R., Donahue, J., Darrell, T. and Malik, J., 2014. Rich feature hierarchies for accurate object detection and semantic segmentation. In Proceedings of the IEEE conference on computer vision and pattern recognition (pp. 580–587).

[2] Girshick, R., 2015. Fast r-cnn. In Proceedings of the IEEE international conference on computer vision (pp. 1440–1448).

[3] Ren, S., He, K., Girshick, R. and Sun, J., 2015. Faster r-cnn: Towards real-time object detection with region proposal networks. Advances in neural information processing systems, 28.

[4] Feichtenhofer, Christoph, Haoqi Fan, Jitendra Malik, and Kaiming He. “Slowfast networks for video recognition.” In Proceedings of the IEEE/CVF international conference on computer vision, pp. 6202–6211. 2019.

[5] Normal distribution, *Wikipedia,*https://en.wikipedia.org/wiki/Normal_distribution

[6] Normal Distribution, Feldman K., https://www.isixsigma.com/dictionary/normal-distribution/

[7] Scikit-learn: Machine Learning in Python, Pedregosa, et al., JMLR 12, pp. 2825–2830, 2011.

[8] Reynolds, D.A., 2009. Gaussian mixture models. Encyclopedia of biometrics, 741(659–663).

[9] Kireeva A., 2001, Gaussian Mixture Models Visually Explained, https://aabkn.github.io/GMM_visually_explained

[10] Signal-to-noise ratio, *Wikipedia,*https://en.wikipedia.org/wiki/Signal-to-noise_ratio

作者:Tal Goldfryd

“基于GMM的一维时序数据平滑算法”的评论:

还没有评论