3. OES信号预处理:去噪滤波、基线校正与归一化
大家好,我是老张。在半导体干法刻蚀这行摸爬滚打了十几年,今天咱们聊聊OES信号预处理。
说实话,很多刚入行的工程师拿到OES数据,直接就开始找特征波长、设阈值。结果呢?噪声太大,终点判断一塌糊涂。我见过太多这样的案例了。
OES信号从光谱仪出来,其实挺「脏」的。有高频噪声、有基线漂移、还有不同工艺腔室间的量级差异。你不处理它,它就给你颜色看。
所以,预处理是终点检测算法的第一步,也是最重要的一步。说白了,就是给信号「洗个澡」,让它干干净净地进入后续分析。
核心观点:预处理做得好,终点检测就成功了一半。我个人的经验是,花在预处理上的时间,至少占整个算法开发的40%。
3.1 去噪滤波:把信号里的「沙子」筛掉
OES信号里的噪声主要来自哪里?光电探测器的散粒噪声、射频干扰、还有等离子体本身的波动。这些噪声如果不处理,会让你误判终点。
移动平均滤波——最简单,也最常用。我习惯用窗口大小为5~15的滑动平均。窗口太小去噪效果差,窗口太大又会把真实的终点拐点给抹平了。
# 移动平均滤波示例
import numpy as np
def moving_average(signal, window_size=7):
"""移动平均去噪"""
return np.convolve(signal, np.ones(window_size)/window_size, mode='same')
# 使用示例
raw_oes = np.random.randn(1000) + 5 # 模拟OES信号
filtered = moving_average(raw_oes, window_size=11)
我的经验:移动平均的窗口大小,我一般取工艺时间的1%~2%。比如刻蚀100秒,窗口就设1~2秒。这样既能去噪,又不会丢失终点特征。
小波去噪——这个就高级一些了。小波变换能把信号分解到不同尺度,然后只保留我们关心的频段。
我记得有一次,客户给的OES信号噪声特别大,移动平均根本搞不定。我试了小波去噪,用db4小波基,阈值设为软阈值,效果出奇的好。
# 小波去噪示例
import pywt
def wavelet_denoise(signal, wavelet='db4', level=3):
"""小波去噪"""
coeffs = pywt.wavedec(signal, wavelet, level=level)
# 软阈值处理
sigma = np.median(np.abs(coeffs[-1])) / 0.6745
threshold = sigma * np.sqrt(2 * np.log(len(signal)))
coeffs_thresh = [coeffs[0]] # 保留近似系数
for i in range(1, len(coeffs)):
coeffs_thresh.append(pywt.threshold(coeffs[i], threshold, mode='soft'))
return pywt.waverec(coeffs_thresh, wavelet)
注意:小波去噪的参数调起来比较费劲。我曾经在一个项目中,光调小波基和阈值就花了两天。建议先从db4、sym8这些小波基开始试。
3.2 基线校正:把信号「扶正」
基线漂移是OES信号的老大难问题。腔室温度变化、窗口污染、气体流量波动,都会导致基线慢慢往上飘或往下掉。
你想想看,如果基线一直在漂,终点检测的阈值怎么设?设死了肯定不行。
多项式拟合校正——这是最直观的方法。先用多项式拟合出基线,然后从原始信号中减掉。
# 多项式基线校正
def polynomial_baseline(signal, degree=3):
"""多项式拟合基线校正"""
x = np.arange(len(signal))
coeffs = np.polyfit(x, signal, degree)
baseline = np.polyval(coeffs, x)
return signal - baseline
非对称最小二乘(AsLS)——这个我强烈推荐。它通过给正负残差不同的权重,能很好地拟合出基线,尤其适合OES这种有尖锐峰值的信号。
# 非对称最小二乘基线校正
def asls_baseline(signal, lam=1e5, p=0.01):
"""非对称最小二乘基线校正"""
from scipy import sparse
from scipy.sparse.linalg import spsolve
L = len(signal)
D = sparse.diags([1, -2, 1], [0, -1, -2], shape=(L, L-2))
w = np.ones(L)
for _ in range(10):
W = sparse.spdiags(w, 0, L, L)
Z = W + lam * D.dot(D.T)
z = spsolve(Z, w * signal)
w_new = p * (signal > z) + (1-p) * (signal < z)
if np.linalg.norm(w_new - w) < 1e-3:
break
w = w_new
return signal - z
避坑指南:我曾经在调试AsLS时,lambda参数设得太小,结果基线跟着信号峰一起跑了。后来我把lambda从1e3调到1e5,效果才正常。记住,lambda越大,基线越平滑。
3.3 归一化:让不同腔室的信号「说同一种语言」
归一化解决的是量纲问题。不同腔室、不同工艺条件下,OES信号的绝对强度可能差好几倍。但终点特征往往体现在相对变化上。
Min-Max归一化——把信号映射到[0,1]区间。简单粗暴,但容易受异常值影响。
# Min-Max归一化
def minmax_normalize(signal):
"""Min-Max归一化"""
return (signal - np.min(signal)) / (np.max(signal) - np.min(signal))
Z-score标准化——减去均值,除以标准差。这个对异常值不那么敏感,我比较常用。
# Z-score标准化
def zscore_normalize(signal):
"""Z-score标准化"""
return (signal - np.mean(signal)) / np.std(signal)
面积归一化——把整个光谱的面积归一化到1。这个在OES全谱分析中特别有用。
# 面积归一化
def area_normalize(signal):
"""面积归一化"""
return signal / np.sum(np.abs(signal))
我的建议:如果你只关注单个特征波长,用Z-score就够了。如果你做全谱分析,面积归一化更合适。具体选哪个,取决于你的终点检测算法。
3.4 预处理流程总结
在实际项目中,我一般按这个顺序处理:
- 先去噪:用移动平均或小波,把高频噪声干掉
- 再校正基线:用AsLS或多项式拟合,把漂移拉回来
- 最后归一化:让不同批次的数据具有可比性
这个顺序不能乱。你想想看,如果先归一化再基线校正,基线漂移会被放大,反而更难处理。
| 预处理步骤 | 推荐方法 | 关键参数 | 我的评分 |
|---|---|---|---|
| 去噪滤波 | 移动平均 | 窗口大小:5~15 | ⭐⭐⭐⭐ |
| 去噪滤波 | 小波去噪 | 小波基:db4,阈值:软阈值 | ⭐⭐⭐⭐⭐ |
| 基线校正 | AsLS | lambda:1e5,p:0.01 | ⭐⭐⭐⭐⭐ |
| 基线校正 | 多项式拟合 | 阶数:3~5 | ⭐⭐⭐ |
| 归一化 | Z-score | 无 | ⭐⭐⭐⭐ |
| 归一化 | 面积归一化 | 无 | ⭐⭐⭐⭐ |
好了,这一章的内容就到这里。预处理这块,说白了就是「对症下药」。你的信号有什么问题,就用什么方法。多试几次,找到最适合你工艺的那一套组合拳。