在深度学习和语音处理领域,如何将原始音频信号有效地表示为离散的“Token”序列,是语音识别、音频生成等任务中的关键问题。常见的方法如Mel频谱图+向量量化(VQ)、wav2vec等已经非常成熟,但这些模型通常依赖复杂的神经网络结构。

本文介绍一种轻量级、可解释性强的音频特征提取与“类Token化”编码方法,仅使用余弦相似度、线性回归和归一化技术,即可将一段WAV音频转化为一个整数序列——我们称之为“伪Token”序列。这种方法虽然不能替代现代语音模型,但非常适合用于教学、探索性数据分析或轻量级嵌入式应用。


🎯 目标

我们将实现以下功能:

  1. 读取一段 .wav 音频文件;
  2. 对音频进行预处理(去均值、标准化);
  3. 使用滑动窗口对相邻样本做线性回归分析;
  4. 提取回归斜率与拟合效果(余弦相似度)作为双特征;
  5. 将特征归一化并量化为整数,形成“Token”序列;
  6. 可视化结果并封装成函数。

🔧 核心工具与库

import numpy as np
import matplotlib.pyplot as plt
from scipy.io import wavfile

同时为了支持中文标题显示,设置 Matplotlib 的字体:

plt.rcParams['font.sans-serif'] = ['Microsoft YaHei']
plt.rcParams['axes.unicode_minus'] = False

📐 核心算法定义

1. 余弦相似度(Cosine Similarity)

衡量两个向量方向的一致性,反映线性拟合的质量。

def cosine_similarity(a, b):
    return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))

2. 最小二乘法线性回归

手动实现一元线性回归,避免依赖 sklearn。

def linear_regression(x, y):
    n = len(x)
    sum_x, sum_y = np.sum(x), np.sum(y)
    sum_xy = np.sum(x * y)
    sum_x2 = np.sum(x ** 2)
    slope = (n * sum_xy - sum_x * sum_y) / (n * sum_x2 - sum_x ** 2)
    intercept = (sum_y - slope * sum_x) / n
    return slope, intercept

3. Min-Max 归一化(防除零)

def min_max_normalize(data):
    min_val = np.min(data)
    max_val = np.max(data)
    return (data - min_val) / (max_val - min_val + 1e-8)

🧪 主流程解析:main() 函数

我们以文件 "sound/cat/1-47819-C-5.wav" 为例,演示整个流程。

1. 音频读取与预处理

_, audio = wavfile.read("sound/cat/1-47819-C-5.wav")
left = (audio - np.mean(audio)) / np.std(audio)  # 标准化

⚠️ 注意:这里假设音频是单声道。如果是立体声,请取某一通道(如 audio[:, 0])。

接着构造“左移-右移”数据对:

left = left[:left.size // 2 * 2]  # 确保长度为偶数
left = left[:-1]                  # 去最后一个元素
right = left[1:]                  # 右移一位

这相当于构建了 (x_t, x_{t+1}) 的时间序列对,可用于分析局部动态变化。


2. 滑动窗口分析

使用大小为 3200 的窗口,每步移动 1600(半重叠),进行局部线性拟合:

for i in range(0, len(left) - 1600, 1600):
    x, y = left[i:i + 3200], right[i:i + 3200]
    if len(x) > len(y): x = x[:len(y)]
    slope, intercept = linear_regression(x, y)
    sim = cosine_similarity(slope * x + intercept, y)
    sim_list.append(sim)
    slope_list.append(slope)
  • x: 当前窗口内的原始信号片段;
  • y: 对应的“下一时刻”信号(右移);
  • 使用线性模型 y ≈ slope * x + intercept 拟合;
  • 计算预测值与真实值之间的余弦相似度,评估拟合质量;
  • 记录每个窗口的 slope 和 similarity。

3. 特征归一化与“Token化”

将连续特征映射到整数空间,模拟 Token 编码过程:

sim_list = min_max_normalize(sim_list) * 2048
slope_list = min_max_normalize(slope_list) * 2048 + 2048
  • sim_list → [0, 2048]
  • slope_list → [2048, 4096]

然后转换为整型:

sim_list = sim_list.astype(np.int16)

最后将两者交错拼接成一维序列:

tokens = np.hstack([sim_list, slope_list]).reshape([2, -1]).transpose([1, 0]).reshape(-1)

这一步实现了双通道特征的交织编码。


4. 可视化 Token 序列

plt.plot(tokens)
plt.title("音频生成的伪Token序列")
plt.xlabel("Token索引")
plt.ylabel("Token值")
plt.show()

💡 封装函数:wav_to_token(path)

我们将上述逻辑封装为通用函数,适用于任意 .wav 文件:

def wav_to_token(path):
    _, audio = wavfile.read(path)
    
    # 单通道处理
    if len(audio.shape) > 1:
        audio = audio[:, 0]
        
    left = (audio - np.mean(audio)) / np.std(audio)
    left = left[:left.size // 2 * 2][:-1]
    right = left[1:]

    sim_list = []
    slope_list = []

    # 更小的窗口:1600采样点,每800步滑动
    for i in range(0, len(left) - 800, 800):
        x = left[i:i+1600]
        y = right[i:i+1600]
        if len(x) != len(y):
            min_len = min(len(x), len(y))
            x, y = x[:min_len], y[:min_len]

        slope, intercept = linear_regression(x, y)
        pred = slope * x + intercept
        sim = cosine_similarity(pred, y)

        sim_list.append(sim)
        slope_list.append(slope)

    # 归一化到 0~64 范围(2^6)
    sim_tokens = min_max_normalize(np.array(sim_list)) * 64
    slope_tokens = min_max_normalize(np.array(slope_list)) * 64

    sim_tokens = sim_tokens.astype(np.int16)
    slope_tokens = slope_tokens.astype(np.int16)

    # 合并为乘积特征(非零过滤)
    res = sim_tokens * slope_tokens
    return res[res != 0]  # 去除零值

✅ 返回的是一个整数数组,可视为该音频的“特征Token序列”。


📊 方法特点总结

项目描述
优点- 不依赖深度学习框架
- 可解释性强
- 计算开销小
- 可用于边缘设备
局限- 表达能力有限
- 无法捕捉高频语义
- 对噪声敏感
适用场景- 音频分类初筛
- 异常声音检测
- 教学演示
- 低资源环境下的特征提取

🚀 拓展思路

你可以在此基础上进一步改进:

  1. 加入频域特征:对每个窗口做FFT,提取主频作为第三Token维度;
  2. 向量量化(VQ):用KMeans对 (sim, slope) 向量聚类,真正生成离散Token;
  3. 滑动窗口自适应:根据能量或过零率动态调整窗口大小;
  4. 时间对齐编码:引入DTW对齐不同长度的Token序列;
  5. 用于对比学习:计算不同音频Token序列间的距离,做相似性匹配。

📎 完整代码下载

你可以将以下完整代码保存为 audio_tokenizer.py 并运行:

import numpy as np
import matplotlib.pyplot as plt
from scipy.io import wavfile

# 中文显示支持
plt.rcParams['font.sans-serif'] = ['Microsoft YaHei']
plt.rcParams['axes.unicode_minus'] = False


def cosine_similarity(a, b):
    """计算余弦相似度"""
    return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))


def linear_regression(x, y):
    """最小二乘法线性回归"""
    n = len(x)
    sum_x, sum_y = np.sum(x), np.sum(y)
    sum_xy = np.sum(x * y)
    sum_x2 = np.sum(x ** 2)
    slope = (n * sum_xy - sum_x * sum_y) / (n * sum_x2 - sum_x ** 2)
    intercept = (sum_y - slope * sum_x) / n
    return slope, intercept


def min_max_normalize(data):
    min_val = np.min(data)
    max_val = np.max(data)
    return (data - min_val) / (max_val - min_val + 1e-8)


def wav_to_token(path):
    _, audio = wavfile.read(path)
    if len(audio.shape) > 1:
        audio = audio[:, 0]  # 取左声道

    # 标准化
    left = (audio - np.mean(audio)) / np.std(audio)
    left = left[:len(left)//2*2][:-1]
    right = left[1:]

    sim_list = []
    slope_list = []

    for i in range(0, len(left) - 800, 800):
        x = left[i:i+1600]
        y = right[i:i+1600]
        if len(x) != len(y):
            min_len = min(len(x), len(y))
            x, y = x[:min_len], y[:min_len]

        slope, intercept = linear_regression(x, y)
        pred = slope * x + intercept
        sim = cosine_similarity(pred, y)

        sim_list.append(sim)
        slope_list.append(slope)

    sim_arr = min_max_normalize(np.array(sim_list)) * 64
    slope_arr = min_max_normalize(np.array(slope_list)) * 64

    sim_arr = sim_arr.astype(np.int16)
    slope_arr = slope_arr.astype(np.int16)

    res = sim_arr * slope_arr
    return res[res != 0]


def main():
    path = "sound/cat/1-47819-C-5.wav"
    tokens = wav_to_token(path)

    plt.figure(figsize=(10, 4))
    plt.plot(tokens)
    plt.title("音频生成的伪Token序列")
    plt.xlabel("Token索引")
    plt.ylabel("Token值")
    plt.grid(True)
    plt.tight_layout()
    plt.show()


if __name__ == "__main__":
    main()

📣 结语

本文提出了一种新颖而直观的方式,将音频信号通过线性动力学建模 + 特征量化的方式转换为离散序列。虽然它不是真正的“语音Token”,但它启发我们思考:是否可以用更简单的方法逼近复杂模型的部分能力?

这种“白盒”方法有助于理解音频特征的本质,也为轻量级系统提供了一种可行的替代方案。

🔗 后续计划:我们将尝试用这类Token训练一个RNN来“复现”原始音频,敬请期待!


📌 关键词:音频处理、Token化、线性回归、余弦相似度、滑动窗口、Python、信号处理、轻量级模型

📬 欢迎留言交流更多音频特征工程技巧!

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐