news 2026/9/15 15:57:59

语音预处理关键:分帧加窗原理、参数与Python实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
语音预处理关键:分帧加窗原理、参数与Python实战

语音处理里干了大半年,最容易被忽略、却又最影响后续效果的一步,就是分帧和加窗。很多初学者把网络上下好的音频直接丢给模型,出来的结果乱七八糟,回头怀疑是模型不行,实际上一大半问题出在预处理没做好。语音信号的分帧、加窗、预处理,听起来像个基础工具,实际上它决定了你后面提特征、训练模型、做识别推理的上限。这篇文章我想把这块内容彻底讲透,从为什么需要分帧加窗、参数怎么定,到Python代码怎么落地,再到我自己踩过的坑,一次性说清楚,适合刚入门语音方向、或者已经写了几个模型但始终觉得效果不稳定的朋友参考。

1. 分帧加窗到底在解决什么问题

1.1 语音信号的"短时平稳性"是关键

先问个最朴素的问题:处理图像时,每个像素就是数据点,简单直观。但语音信号呢?本质上是一维时间序列,如果直接对整个信号做傅里叶变换,算出来的频谱是整段音频的平均结果——这相当于把一句话里所有的音素混在一起,什么东西都丢了。

为什么会这样?因为语音信号是典型的非平稳信号。发音时声带振动频率在变、音量在变、口腔形状在变,整个信号的统计特性随时间剧烈变化。上一毫秒是清音,下一毫秒可能就变成浊音。如果拿全局视角去分析,等于你把完全不同统计特性的数据放到同一个篮子里,提取到的特征自然没有区分度。

但语音信号有一个非常重要的性质:短时平稳性。在极短的时间范围内(一般是10到50毫秒),人的发声器官运动速度远跟不上声波振动速度,这段时间内信号可以近似看作平稳的。也就是说,你可以把长语音切成一小段一小段,每一段单独分析,再按时间顺序把分析结果串起来,就能还原出语音随时间变化的过程。

这就是分帧的理论基础。分帧的本质是:用局部平稳近似替代全局非平稳,把一个长时序列变成一组短时序列的集合。

1.2 帧长和帧移怎么定,为什么没人告诉你标准答案

分帧有两个核心参数:帧长(frame size)帧移(frame shift)

帧长是指每一帧包含多少个采样点,帧移是指相邻两帧起点之间的距离。帧长对应时间长度,帧移对应时间步长。这两个参数没有绝对正确的值,但行业里有一组被反复验证的常用配置,直接记下来就能用:

参数常见值换算关系
采样率16 kHz每秒16000个采样点
帧长25 ms25/1000 × 16000 = 400 个采样点
帧移10 ms10/1000 × 16000 = 160 个采样点
每帧有效点数400帧长与帧移的比例通常为 2 到 3 倍

帧长为什么选25毫秒?这个时间足够覆盖几个基音周期(成年男性基频大约100到150 Hz,周期为7到10毫秒;女性基频大约200到300 Hz,周期为3到5毫秒),能捕捉到完整的声门周期信息,同时又足够短,能满足平稳性假设。

帧移为什么选10毫秒?相邻帧要有重叠。如果不重叠,帧与帧之间的信息跳跃太大,提取的特征序列不平滑,后续做动态特征等操作时效果会打折扣。10毫秒的帧移意味着相邻帧有15毫秒的重叠(25毫秒帧长),重叠率60%,既保证了时间分辨率,又不会产生大量重复计算。

实际项目中我见过有人直接把帧长设成512点、帧移设成256点(在16 kHz下是32毫秒和16毫秒),也能跑通,但识别效果普遍不如25/10毫秒的配置。这不是玄学,是因为25/10毫秒的组合经过了几十年语音识别研究的验证,适合大部分人的语音特性。如果你做的是特定领域的音频(比如动物声、声呐信号),那需要根据信号本身的特性重新标定,但刚起步时无脑用25/10毫秒不会错。

1.3 加窗是为了避免频谱泄漏,不是强迫症

分帧之后,下一步是加窗。很多教程只说"对每帧乘一个窗函数",却没说为什么要乘。

直接对截取出来的一段信号做傅里叶变换会有一个问题:你切出来的这帧信号,在边界处是不连续的——第一个采样点的值和前一个点没有关系,最后一个点的值也和后一个点没有关系。这种人为造成的不连续,在频域上表现为高频分量的能量泄漏,叫做频谱泄漏

打个比方:你的信号本来只有100 Hz一个频率成分,但因为你硬生生截断,傅里叶变换看到的就不只是100 Hz,还出现了不少虚假的高频分量,频谱变"脏"了。

窗函数的作用就是平滑地衰减帧两端的幅度,让边界处的幅度渐渐趋向于0,消除人为截断带来的不连续。乘上窗函数后,帧中心区域的信号被保留,两端的信号被压下去,这样再做傅里叶变换,频谱泄漏就会被明显抑制。

所以加窗不是可有可无的仪式感,它是保证频域分析准确性的必要操作。

2. Python实现分帧的三种方式

2.1 最直观的循环分帧:适合讲清楚逻辑

先看最简单、最符合思维直觉的版本。假设你已经读入了一段信号,采样率为16 kHz,现在要按帧长400、帧移160进行分帧:

import numpy as np def frame_signal_loop(signal, frame_size=400, frame_shift=160): """ 循环分帧实现方式 :param signal: 一维numpy数组,语音信号 :param frame_size: 帧长(采样点个数) :param frame_shift: 帧移(采样点个数) :return: 二维数组,形状为 (num_frames, frame_size) """ signal_len = len(signal) # 帧数的计算方法 num_frames = (signal_len - frame_size) // frame_shift + 1 print(f"信号长度: {signal_len}, 帧数: {num_frames}") frames = np.zeros((num_frames, frame_size)) for i in range(num_frames): start = i * frame_shift frames[i] = signal[start:start + frame_size] return frames

这段代码里最关键的是帧数计算:

num_frames = (signal_len - frame_size) // frame_shift + 1

为什么要减frame_size?因为分帧要求每一帧都是完整的frame_size长度。如果信号长度不足以填满最后一帧,最简单的方式是直接丢弃。加1是考虑到起始位置可以是从0开始的整数倍帧移,保证最后一帧的结束位置不超过信号末尾。

打个比方:你有100个苹果,每盒装25个,每走一步跨10个位置。第一盒从位置0拿到24,第二盒从位置10拿到34,第三盒从20拿到44……直到最后一次,起始位置不能超过100-25=75,所以能拿的盒数就是(100-25)//10+1 = 8盒。

循环方式的优点是逻辑清楚,适合学习和调试;缺点是慢。如果你处理几秒钟的音频,帧数上千,Python循环开销可不小。我做实验时拿10秒音频跑了100遍循环版分帧,用时大约1秒多,对实时性要求高的场景不太行。

2.2 向量化索引分帧:兼顾速度和可读性

实际项目中我更喜欢用向量化方式,利用numpy的广播机制,一次性生成所有帧的索引矩阵,再一次性取数据:

def frame_signal_vectorized(signal, frame_size=400, frame_shift=160): """ 向量化分帧,无显式循环 """ signal_len = len(signal) num_frames = (signal_len - frame_size) // frame_shift + 1 # 生成每一帧的起始位置偏移 frame_indices = np.arange(num_frames) * frame_shift # 生成每一帧内部的相对偏移 frame_offsets = np.arange(frame_size) # 利用广播生成索引矩阵 (num_frames, frame_size) indices = frame_indices[:, None] + frame_offsets[None, :] # 一行代码完成所有帧的截取 frames = signal[indices] return frames

这里的核心是:

indices = frame_indices[:, None] + frame_offsets[None, :]

frame_indices形状是(num_frames, 1)frame_offsets形状是(1, frame_size),相加后得到一个(num_frames, frame_size)的索引矩阵。比如第3行第第100列,对应的就是起始位置2 * frame_shift + 99这个采样点的索引。

用这个方式分帧,10秒音频的分帧操作耗时会从1秒多降到几毫秒,100倍以上的速度提升。实际做数据预处理时,我还有过对长达几小时的音频做分帧,如果不用向量化,等到天荒地老也跑不完。

2.3 stride_tricks无损分帧:内存效率最优但容易踩坑

再进阶一步,numpy提供了as_strided函数,可以让你不复制数据地创建分帧视图:

from numpy.lib.stride_tricks import as_strided def frame_signal_strided(signal, frame_size=400, frame_shift=160): """ 使用as_strided实现零拷贝分帧 注意:返回的是原始数据的视图,修改会改变原始数据 """ signal_len = len(signal) num_frames = (signal_len - frame_size) // frame_shift + 1 if num_frames <= 0: return np.zeros((0, frame_size)) stride = signal.strides[0] # 每个采样点的字节步长 frames = as_strided( signal, shape=(num_frames, frame_size), strides=(frame_shift * stride, stride) ) return frames

as_strided的思路是告诉numpy:"这些数据,你按什么形状、什么步长去读"。因为每一帧的数据是重叠的(帧移小于帧长),所以as_strided可以共享底层内存,不需要额外占用空间。

这个方式的优点是一旦大数据量下内存开销极小,缺点也很明显:返回的视图改一个值,原始信号也会被改;而且as_strided调用不当极易越界读内存,轻则数据错误,重则程序崩溃。我自己在工作中很少用as_strided做语音分帧,因为向量化方式的内存开销对绝大多数场景完全够用,没必要为了那点优化引入崩溃风险。如果你想追求极致性能,我的建议是先用向量化版本跑通流程,再考虑是否需要换成as_strided

3. 加窗:为什么不能直接对分帧后的信号做FFT

3.1 矩形窗的问题,用一段代码就能直观感受到

很多人心想,我分完帧直接对每一帧做FFT不就行了?我把帧乘上一个全为1的窗口,也就是矩形窗,不是等于没乘吗?

对,等于没乘。但这样做频谱泄漏很严重。我用一个例子来说明。

生成一个频率为100 Hz的正弦波,采样率8000 Hz,取400个点(50毫秒),分别不加窗和加汉明窗,然后做FFT对比:

import numpy as np import matplotlib.pyplot as plt fs = 8000 t = np.arange(400) / fs signal = np.sin(2 * np.pi * 100 * t) # 不加窗(矩形窗) fft_raw = np.fft.rfft(signal) freqs = np.fft.rfftfreq(400, 1/fs) # 加汉明窗 window = np.hamming(400) fft_windowed = np.fft.rfft(signal * window) plt.figure(figsize=(10, 5)) plt.plot(freqs, 20*np.log10(np.abs(fft_raw) + 1e-10), label='矩形窗') plt.plot(freqs, 20*np.log10(np.abs(fft_windowed) + 1e-10), label='汉明窗')

运行这段代码你会发现:矩形窗的情况下,100 Hz 附近频谱底部抬高了很多,旁瓣非常明显;加汉明窗后,主瓣更集中,旁瓣明显下降。这就是频谱泄漏的直观体现。

3.2 常见窗函数的选择与对比

语音处理里最常见的窗函数是汉明窗和汉宁窗,另外还有布莱克曼窗、矩形窗。它们各有侧重:

窗函数主瓣宽度旁瓣衰减适用范围
矩形窗最窄最差(-13 dB)频率分辨率优先,不考虑泄漏
汉宁窗较宽较好(-31 dB)通用信号分析
汉明窗较宽较好(-43 dB)语音识别特征提取中最常用
布莱克曼窗最宽最好(-58 dB)需要极低旁瓣的场合

汉明窗在语音特征提取里特别流行,是因为它在主瓣宽度和旁瓣衰减之间取得了很好的平衡。语音信号的频谱本身是宽带的,不需要极窄的主瓣,但对旁瓣泄漏敏感,所以汉明窗很合适。

在Python里,numpy直接提供了窗函数接口,不需要自己手写公式:

import numpy as np frame_size = 400 window_hamming = np.hamming(frame_size) window_hanning = np.hanning(frame_size) window_blackman = np.blackman(frame_size) # 自实现汉明窗,公式为 0.54 - 0.46 * cos(2πn/(N-1)) n = np.arange(frame_size) window_custom = 0.54 - 0.46 * np.cos(2 * np.pi * n / (frame_size - 1))

np.hamming的返回值已经是归一化好的,直接和帧数据相乘即可。

3.3 加窗操作在完整流程中的位置

加窗是分帧的下一步操作,放在分帧之后、FFT之前。

# 延续之前的分帧代码 frames, num_frames = frame_signal_vectorized(signal, frame_size=400, frame_shift=160) # 生成汉明窗 window = np.hamming(400) # 对每一帧做加窗 windowed_frames = frames * window # 利用numpy广播,逐行相乘

这里有一个细节:frames形状是(num_frames, 400)window形状是(400,),numpy广播会将window沿第一维自动扩展,等价于每一帧都乘以同一个窗函数。代码看起来就一行,但理解了广播机制你才知道它为什么能work。

加窗后的帧再去做FFT,得到的就是干净的短时频谱,这也是后面提取MFCC、Filter-Bank特征的基础。如果你做的是语音识别,这一步做没做好,直接影响特征质量和最终识别准确率。

4. 完整预处理流程:从原始wav到干净的数据矩阵

4.1 读取音频:wav文件处理与归一化

预处理的第一步是读取音频文件。语音领域最常用的是wav格式,因为它未压缩、处理简单。Python里可以直接用标准库wave读取,也可以用scipy.io.wavfile

import numpy as np import wave def read_wav(file_path): """ 读取wav文件并返回归一化后的单声道信号和采样率 """ with wave.open(file_path, 'rb') as wf: params = wf.getparams() n_channels, sampwidth, fs, n_frames = params[:4] print(f"声道数: {n_channels}, 采样位深: {sampwidth*8}bit, 采样率: {fs}, 帧数: {n_frames}") # 读出原始数据 raw_data = wf.readframes(n_frames) # 根据位深选择转换类型 if sampwidth == 2: dtype = np.int16 elif sampwidth == 4: dtype = np.int32 else: raise ValueError(f"不支持的采样位深: {sampwidth*8}bit") audio = np.frombuffer(raw_data, dtype=dtype).astype(np.float32) # 如果是多声道,取平均转单声道 if n_channels > 1: audio = audio.reshape(-1, n_channels).mean(axis=1) # 归一化到[-1, 1]区间 max_val = np.iinfo(dtype).max audio = audio / max_val return audio, fs

归一化这一步很多人不做或者做错。wav文件的原始数据是整数类型(16位wav是-32768到32767),直接处理会导致数值范围极大,给后续浮点运算带来麻烦。归一化到[-1, 1]后,不仅能统一数值尺度,还能避免后续计算溢出。

这里有一个细节:如果原始wav本来就是双声道的,简单取平均可能会引起相位抵消。如果两个声道内容差异很大(比如立体声音乐),更好的做法是只取左声道。但语音数据大多数是单声道,这个问题遇到再说。

4.2 预加重:语音信号预处理里最容易被忽视的一步

分帧加窗之前,一般还要做一步预加重。为什么要做?因为语音信号存在一个物理特性:高频成分的能量普遍低于低频成分。人发声时,声门激励的频谱大致按12 dB/倍频程的规律下降,这种趋势会让高频信息在特征提取时被淹没。

预加重的做法是加一个一阶高通滤波器,公式为:

y[n] = x[n] - α * x[n-1]

其中 α 通常取0.95到0.98之间,语音识别里最常用0.97。这个操作的效果是让信号的高频部分提升约20 dB/倍频程,补偿声门激励带来的频谱倾斜,让各个频段的能量分布更加均衡。

def pre_emphasis(signal, alpha=0.97): """ 预加重 :param signal: 输入信号 :param alpha: 预加重系数,常用0.95~0.98 :return: 预加重后的信号 """ emphasized = np.zeros_like(signal) emphasized[0] = signal[0] emphasized[1:] = signal[1:] - alpha * signal[:-1] return emphasized

预加重的位置是:读入原始信号 → 预加重 → 分帧 → 加窗 → FFT。

实测下来,预加重对语音识别任务的影响可能不如分帧加窗那么直观,但对声学特征的质量提升是实打实的。我自己对比过是否做预加重的两个系统,在同样的模型结构下,做了预加重的系统在噪声环境里的识别准确率能高出几个百分点。

4.3 静音检测与端点检测:怎么把无用的空白去掉

很多实际采集的语音文件,开头和结尾都有一段静音或环境噪声。直接把这些静音帧也送进模型,不仅浪费计算资源,还会干扰模型学习。所以预处理中经常要加入静音去除或端点检测。

最简单的静音检测方法是基于短时能量的阈值判断。计算每一帧的短时能量:

def frame_energy(frames): """计算每一帧的短时能量""" return np.sum(frames ** 2, axis=1) def remove_silence(signal, fs=16000, frame_size=400, frame_shift=160, energy_thresh=0.005): """ 基于短时能量的静音去除 """ frames = frame_signal_vectorized(signal, frame_size, frame_shift) energies = frame_energy(frames) # 找到能量超过阈值的帧 voiced_frames = energies > energy_thresh # 重建去除静音后的信号 voiced_signal = [] for i, is_voiced in enumerate(voiced_frames): if is_voiced: voiced_signal.append(frames[i]) return np.concatenate(voiced_signal) if len(voiced_signal) > 0 else signal

阈值怎么定?需要根据实际数据来标定。一个经验方法:先算整段信号能量分布,取一个小比例分位数作为阈值。比如:

# 用能量的20%分位数作为阈值 energy_threshold = np.percentile(energies, 20)

如果你只是粗略处理一下静音,用固定阈值也能凑合。但要注意,阈值设置太高会误删有效的清音部分,太低又去不掉静音。所以更稳妥的做法是先用一个宽松的阈值选出有声段,再根据有声段边界向外扩展几帧,保证清音和弱音不被误删。

4.4 把整个预处理流程串起来

到这里,你已经有了读取、预加重、分帧、加窗、静音去除的所有基础模块。把它们组合成一个完整的预处理函数:

def audio_preprocess(file_path, frame_size=400, frame_shift=160, window_type='hamming', pre_emph_alpha=0.97, remove_silence=True): """ 完整语音预处理流程 """ # 1. 读取音频 signal, fs = read_wav(file_path) # 2. 预加重 signal = pre_emphasis(signal, pre_emph_alpha) # 3. 分帧 frames = frame_signal_vectorized(signal, frame_size, frame_shift) if frames.shape[0] == 0: raise ValueError("音频过短,无法分帧") # 4. 加窗 if window_type == 'hamming': window = np.hamming(frame_size) elif window_type == 'hanning': window = np.hanning(frame_size) else: window = np.ones(frame_size) windowed_frames = frames * window # 5. 静音去除(可选,基于加窗前后的帧能量) if remove_silence: energies = frame_energy(windowed_frames) threshold = np.percentile(energies, 20) mask = energies > threshold windowed_frames = windowed_frames[mask] return windowed_frames, fs

这个函数可以直接用于后续的特征提取。你只要传入wav路径,就能得到预处理后的帧数据矩阵,形状为(有效帧数, frame_size)

5. 工程实战中的常见问题与调优经验

5.1 参数选择的匹配问题:采样率变了,帧长必须跟着变

上面提到的400点和160点,都是针对16 kHz采样率说的。如果你手里的音频采样率是8 kHz或48 kHz,直接用固定点数就会出问题。

换算公式很简单:

帧长(点) = 帧长(毫秒) × 采样率 / 1000

比如8 kHz采样率,25毫秒帧长就是200点,10毫秒帧移就是80点。48 kHz采样率,25毫秒帧长就是1200点,帧移480点。我见过不少同学把16 kHz的配置原封不动用到8 kHz音频上,结果帧长变成了50毫秒,平稳性假设不再成立,后续特征质量明显下降。

最好的做法是写成参数化方式:

def get_frame_params(fs, frame_ms=25, shift_ms=10): frame_size = int(fs * frame_ms / 1000) frame_shift = int(fs * shift_ms / 1000) return frame_size, frame_shift

这样不管你输入什么采样率的音频,都能自动计算出正确的帧参数。

5.2 音频过短或异常时:分帧代码不能崩

实际工程里,你永远不知道读进来的文件是什么情况。有可能是一段静音,有可能是0.1秒的极短音频,也有可能是采样率标注错误的音频。所以分帧函数一定要做边界检查。

我自己常用的保护写法:

def safe_frame_signal(signal, frame_size=400, frame_shift=160): signal_len = len(signal) if signal_len < frame_size: # 信号太短,padding到帧长 padded = np.zeros(frame_size) padded[:signal_len] = signal return padded.reshape(1, -1) num_frames = (signal_len - frame_size) // frame_shift + 1 if num_frames <= 0: num_frames = 1 # 正常分帧 frames = frame_signal_vectorized(signal[: signal_len], frame_size, frame_shift) return frames

信号短于帧长时直接补零而不是报错,是更稳妥的做法。这样下游的特征提取代码不需要判断特殊情况,流程更健壮。

5.3 实时性与性能:批量处理时的优化思路

如果你要处理成百上千条语音数据,分帧加窗的性能就会成为瓶颈。除了用向量化替代循环外,还有几个实用的优化点:

  1. 避免在循环里重复创建窗函数。窗函数只和帧长有关,和音频无关。可以在初始化时生成一次,全局复用。

  2. 批量处理多个文件时,使用numpy的矩阵运算。把多个wav文件读取后padding到相同长度,堆叠成一个大的矩阵,一次性分帧。这比逐文件循环快很多。

  3. 如果只需要特征而不需要重建信号,考虑用librosalibrosa.stft内部把分帧、加窗、FFT一步到位,而且用C扩展实现,性能极好。自己实现分帧加窗的意义在于理解原理和应对定制需求,但生产环境中用成熟库更省心。

import librosa # 一行代码完成分帧+加窗+STFT stft_matrix = librosa.stft(y=signal, n_fft=400, hop_length=160, window='hamming')

5.4 常见报错和排查思路

做分帧加窗时,高频报错基本集中在几个地方:

报错1:IndexError: index X is out of bounds for axis 0 with size Y

原因:分帧时最后一帧越界。通常是帧数计算多算了一帧,或者信号长度恰好比预期短。解决办法:在分帧前打印signal_lennum_frames,检查最后一帧的start + frame_size是否小于等于signal_len

报错2:ValueError: operands could not be broadcast together with shapes (100,400) (401,)

原因:窗函数长度和帧长不一致。常见于你修改了frame_size但忘了同步修改窗函数生成时的参数。窗函数长度必须严格等于帧长,否则numpy广播会报错。

报错3:音频全是NaN或Inf

原因:读取wav后没有做归一化,或者原始数据有问题。排查方法:打印signal.min()signal.max(),如果出现NaN,需要检查wav文件本身是否损坏。

报错4:提完MFCC特征维度不对

原因:可能是在分帧前就做了FFT,或者窗函数加在了错误的位置。标准顺序一定是:预加重 → 分帧 → 加窗 → FFT → 取对数 → DCT。任何一步顺序错了,特征维度都可能对不上。

6. 分帧加窗做完之后还能做什么

预处理只是语音信号处理的第一步。拿到分帧加窗后的数据矩阵,你可以顺势做后续工作:

  • 对每一帧加窗后做FFT,得到短时幅度谱或功率谱;
  • 基于功率谱计算Mel滤波器组,再取对数得到Filter-Bank特征;
  • 对FBank特征做DCT,得到MFCC特征;
  • 还可以进一步计算差分特征,提取帧间动态信息。

这些是语音识别和声学特征提取的完整链路。分帧加窗作为第一步,虽然简单,却是整个链路里最基础也最关键的地基。地基打歪了,后面无论用多先进的模型都救不回来。

从我个人的实践经验来看,很多项目跑出的结果不对,返工排查到最后,发现的居然是某个wav文件的采样率不一致,导致分帧参数在部分数据上彻底失效。所以强烈建议在预处理阶段就把数据检查、参数管理、边界处理做扎实,这样后续的模型训练和调优才能把真正的精力放在模型本身。

最后分享一个小技巧:每次分帧加窗后,顺手把帧数和有效帧数打印出来,和原始信号时长对比一下,十几秒的数据看到帧数不对,能帮你提前发现很多隐藏问题。这套流程我用了很久,稳定可靠,直接复制到你的项目里改改参数就能用。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/15 15:55:31

ORB-SLAM3 Euroc数据集测试实战:编译、运行与精度评估

第一次把ORB-SLAM3跑在Euroc的MH01序列上&#xff0c;我看着屏幕上不断刷新的地图点和轨迹线&#xff0c;第一反应是“终于跑通了”&#xff0c;第二反应是“这轨迹怎么和真值差了这么多”。后来仔细一查&#xff0c;问题居然出在一个非常不起眼的地方——我给单目模式传的yaml…

作者头像 李华
网站建设 2026/9/15 15:54:46

VGG19实战:从结构解析到PyTorch实现与调参全指南

前一段时间整理自己深度学习系列的学习笔记&#xff0c;正好写到“深度学习12—VGG19实现”这一篇。VGG19这个模型&#xff0c;现在看起来已经不算最前沿&#xff0c;但它在视觉模型演进里的位置非常特殊&#xff1a;它是“深度”这个概念真正被做到极致的代表作&#xff0c;也…

作者头像 李华
网站建设 2026/9/15 15:54:12

三步用 ipatool 下载 iOS IPA 包:新手完整的上手指南

三步用 ipatool 下载 iOS IPA 包&#xff1a;新手完整的上手指南 【免费下载链接】ipatool Command-line tool that allows you to search for iOS, iPadOS, tvOS, visionOS, and macOS apps on the App Store, and download .ipa or macOS .pkg app packages. 项目地址: htt…

作者头像 李华
网站建设 2026/9/15 15:52:26

KMP跨平台瀑布流实战:基于Compose Multiplatform的LazyVerticalStaggeredGrid

最近在做KMP项目的瀑布流时踩了不少坑&#xff0c;整理成这篇东西&#xff0c;希望能让后来的人少走弯路。先说清楚&#xff0c;这里的KMP是Kotlin Multiplatform的缩写&#xff0c;不是数据结构里那个KMP字符串匹配算法。如果你看到标题第一反应是“求next数组”&#xff0c;那…

作者头像 李华