فهرست مقاله
در زنجیرهٔ تحلیل گفتار، تبدیل فوریه پایانِ تصمیمهای پیشپردازش نیست. هنوز باید مشخص کنیم کدام خانههای طیف با چه وزنی در یک ویژگی جمع شوند. بانکی که روی کاغذ چند مثلث دارد، ممکن است روی شبکهٔ فرکانسیِ انتخابشده ردیفِ کاملاً صفر بسازد. این درس بانک کوچکی را با دادهٔ مصنوعی میسازد تا همین خطا، تفاوت توان و دامنه و قرارداد مقیاس روشن شوند؛ نه برای آموزش یا ارزیابی یک مدل گفتار.
۱. شبکهٔ فرکانس و قرارداد توان

ورودی برنامه طیفِ ازپیشساختهشده است، نه فایل صوتی. هر ستون یک قاب و هر ردیف یک خانهٔ فرکانسیِ بخش یکطرفه است. طول تبدیل را زوج میگیریم؛ تعداد ردیفها نصف طول بهعلاوهٔ یک است. با فاصلهٔ زمانیِ معکوس نرخ نمونهبرداری، قرارداد rfftfreq در NumPy خانهها را از صفر تا نایکوئیستِ مثبت برحسب هرتز مشخص میکند.
برای شاهد دستی، نرخ ۵۶۰۰ نمونه در ثانیه و طول هشت را انتخاب میکنیم؛ پس فاصلهٔ خانهها ۷۰۰ هرتز و مراکز بهترتیب صفر، ۷۰۰، ۱۴۰۰، ۲۱۰۰ و ۲۸۰۰ هرتزند. این نرخ فقط شبکهٔ محاسبه را تعریف میکند؛ دادهٔ این درس از میکروفون نیامده و هیچ ویژگیِ استاندارد شبکهٔ تلفن از آن نتیجه نمیگیریم.
مقادیر ورودی، مربعِ دامنههای مصنوعیِ بیواحدند. آنها را برای اختصار توان مینامیم، اما چگالی طیفیِ توان، وات یا فشار صوتِ کالیبرهشده نیستند. ضریبِ طول تبدیل، بهرهٔ پنجره و دوبرابرکردن سهم فرکانسهای داخلی در این داده تعریف نشدهاند. بنابراین جمع وزندار نیز یک ویژگیِ بیواحد است؛ نباید برچسبِ انرژی فیزیکی یا توان واقعی تماس بگیرد.
درس بازسازی قابهای همپوشان با STFT دربارهٔ تبدیل، فاز، مرز و وزن بازسازی بود. اینجا تبدیل جلو یا معکوس اجرا نمیکنیم و فاز ورودی نداریم. مسئلهٔ تازه، نگاشتِ خانههای فرکانسی به چند جمع وزندار است؛ آزمونِ خوبِ بازسازی در درس قبلی، درستیِ این نگاشت را بهتنهایی تضمین نمیکند.
۲. مقیاس HTK و مثلثهای روی محور هرتز

در این قرارداد فقط گونهٔ HTK را برمیگزینیم: m(f)=2595*log10(1+f/700)، با فرکانس برحسب هرتز. عدد ۷۰۰ نیز مقیاسِ هرتزی دارد؛ قراردادنِ مقدارِ کیلوهرتزی بدون تبدیل واحد، همان نگاشت نیست. برای معکوس، رابطهٔ f=700*(10**(m/2595)-1) به کار میرود.
به تعداد باندها بهعلاوهٔ دو، گرهٔ همفاصله روی محور مل میچینیم و آنها را به هرتز برمیگردانیم. سپس برای هر سه گرهٔ متوالی، وزن روی محور هرتز از صفر به یک و دوباره به صفر میرسد. در نتیجه، همفاصله بودنِ گرهها روی مل به معنیِ فاصلهٔ یکسانِ هرتزی یا خطی بودنِ شیبها روی مل نیست. پیادهسازی برای کاهش حذف عددی نزدیک صفر، شکلِ همارزِ مبتنی بر log1p/expm1 را حساب میکند.
مستندات نسخهٔ ۰٫۱۱٫۰ librosa دربارهٔ فرکانسهای mel وجود گونههای متفاوت را روشن میکند؛ پیشفرضِ آن کتابخانه اسلِینی است. بنابراین نامِ مل بهتنهایی برای بازتولید کافی نیست. این درس آن کتابخانه را اجرا نکرده است و برابریِ بیتی با خروجی یا همهٔ حالتهای رابط آن ادعا نمیکند.
مثلثِ پیوسته قلهٔ یک دارد؛ وزنهای نمونهبرداریشده الزاماً به آن قله نمیرسند. هیچ ردیفی را دوباره بر بیشینه یا جمع خودش تقسیم نمیکنیم. قرارداد بانک فیلتر librosa نرمالسازیِ مساحت را از حالتِ مثلث با قلهٔ یک جدا میکند؛ اینجا حالت دوم را صریح انتخاب کردهایم و ضریبِ معکوسِ پهنای باند نداریم.
۳. کد بانک و جمع وزندار توان

فایل mel_filterbank.py با پایتون ۳٫۱۴٫۵ و نامپای ۲٫۴٫۶ واقعاً اجرا شده است. نرخ، عدد صحیح واقعیِ ۲۰۰۰ تا ۴۸۰۰۰؛ طول، عدد زوجِ چهار تا ۱۲۸؛ و تعداد باندها یک تا ۲۴ است. مقدار بولی بهجای عدد پذیرفته نمیشود. کمینه و بیشینهٔ فرکانس باید متناهی، نامنفی، مرتب و حداکثر برابر نایکوئیست باشند؛ گرههای همافتاده در دقت شناور نیز رد میشوند.
"""Original bounded HTK-mel power aggregation; no librosa or speech model."""
from dataclasses import dataclass
import json
import math
import platform
import numpy as np
@dataclass(frozen=True)
class Bank:
frequencies_hz: np.ndarray
edges_hz: np.ndarray
weights: np.ndarray
empty_bands: tuple[int, ...]
def _integer(value, lo, hi, name):
if type(value) is not int or not lo <= value <= hi:
raise ValueError(name)
def make_bank(sr, n_fft, n_mels, fmin=0.0, fmax=None):
"""Even FFT only; triangles linear in Hz and NOT area normalized."""
_integer(sr, 2000, 48000, "sr in Hz")
_integer(n_fft, 4, 128, "n_fft")
_integer(n_mels, 1, 24, "n_mels")
if n_fft % 2:
raise ValueError("even n_fft required")
if fmax is None:
fmax = sr / 2.0
for value in (fmin, fmax):
if type(value) not in (int, float) or not 0 <= value <= sr / 2:
raise ValueError("frequency bounds in Hz")
if not fmin < fmax:
raise ValueError("fmin < fmax required")
# log1p/expm1 evaluate the stated HTK formula without small-f cancellation.
c = 2595.0 / math.log(10.0)
mel_edges = np.linspace(c * math.log1p(fmin / 700.0),
c * math.log1p(fmax / 700.0), n_mels + 2)
edges = 700.0 * np.expm1(mel_edges / c)
edges[0], edges[-1] = float(fmin), float(fmax)
if not np.all(np.diff(edges) > 0):
raise ValueError("edges collapse at floating-point precision")
frequencies = np.fft.rfftfreq(n_fft, d=1.0 / sr)
left = (frequencies[None, :] - edges[:-2, None]) / np.diff(edges)[:-1, None]
right = (edges[2:, None] - frequencies[None, :]) / np.diff(edges)[1:, None]
weights = np.maximum(0.0, np.minimum(left, right))
empty = tuple(int(i) for i in np.flatnonzero(~np.any(weights > 0, axis=1)))
# These are fresh arrays. Frozen dataclass is NOT deep array immutability.
return Bank(frequencies, edges, weights, empty)
def project_power(power, sr, n_fft, n_mels, fmin=0.0, fmax=None):
"""Input shape (FFT bins, frames); synthetic squared amplitude, not PSD."""
bank = make_bank(sr, n_fft, n_mels, fmin, fmax)
if (type(power) is not np.ndarray or power.dtype != np.dtype("float64")
or power.ndim != 2 or power.shape[0] != n_fft // 2 + 1
or not 1 <= power.shape[1] <= 8):
raise ValueError("float64 power shape (bins, 1..8 frames)")
if not np.all(np.isfinite(power)) or not np.all((power >= 0) & (power <= 1e6)):
raise ValueError("finite power in [0, 1e6]")
if bank.empty_bands:
raise ValueError(f"empty discrete bands: {bank.empty_bands}")
return bank.weights @ power
def scalar_reference(sr, n_fft, n_mels, low=0.0, high=None):
"""Independent geometric interpolation of 1+f/700; scalar piecewise loops."""
if high is None:
high = sr / 2.0
edges = [700.0 * ((1 + low / 700.0) ** (1 - j / (n_mels + 1))
* (1 + high / 700.0) ** (j / (n_mels + 1)) - 1)
for j in range(n_mels + 2)]
edges[0], edges[-1] = low, high
rows = []
for a, b, c in zip(edges, edges[1:], edges[2:]):
row = []
for k in range(n_fft // 2 + 1):
f = k * sr / n_fft
if f <= a or f >= c:
w = 0.0
elif f <= b:
w = (f - a) / (b - a)
else:
w = (c - f) / (c - b)
row.append(w)
rows.append(row)
return np.array(rows, dtype=np.float64)
def rejects(call):
try:
call()
except ValueError:
return True
return False
def main():
checks = {}
bank = make_bank(5600, 8, 1, 0, 2100)
expected = np.array([[0, 1, .5, 0, 0]], dtype=np.float64)
checks["literal_frequency_grid_and_triangle"] = bool(
np.array_equal(bank.frequencies_hz, [0, 700, 1400, 2100, 2800])
and np.allclose(bank.edges_hz, [0, 700, 2100], atol=1e-12, rtol=0)
and np.allclose(bank.weights, expected, atol=1e-12, rtol=0))
power = np.array([[9], [4], [16], [25], [36]], dtype=np.float64)
score = project_power(power, 5600, 8, 1, 0, 2100)
checks["literal_weighted_power_is_twelve"] = bool(
np.allclose(score, [[12]], atol=1e-12, rtol=0))
amplitude_score = project_power(np.sqrt(power), 5600, 8, 1, 0, 2100)
checks["magnitude_is_not_power"] = bool(
np.allclose(amplitude_score, [[4]], atol=1e-12, rtol=0)
and not np.allclose(score, amplitude_score))
checks["amplitude_scale_two_means_power_scale_four"] = bool(
np.allclose(project_power(4 * power, 5600, 8, 1, 0, 2100), 4 * score,
atol=1e-12, rtol=0))
count, worst = 0, 0.0
for sr in (5600, 8000, 16000):
for n_fft in (4, 8, 16, 32, 64, 128):
for bands in (1, 2, 3, 6, 12, 24):
for lo, hi in ((0, sr / 2), (700, 2100)):
actual = make_bank(sr, n_fft, bands, lo, hi)
reference = scalar_reference(sr, n_fft, bands, lo, hi)
error = float(np.max(np.abs(actual.weights - reference)))
worst = max(worst, error)
assert np.allclose(actual.weights, reference, atol=1e-11, rtol=0)
assert actual.empty_bands == tuple(
i for i, row in enumerate(reference) if not np.any(row > 0))
count += 1
checks["scalar_geometric_piecewise_oracle_216_banks"] = count == 216
coarse = make_bank(8000, 8, 12)
checks["coarse_grid_empty_bands_are_explicit"] = bool(
coarse.empty_bands and all(not np.any(coarse.weights[i]) for i in coarse.empty_bands)
and rejects(lambda: project_power(np.ones((5, 1)), 8000, 8, 12)))
fine = make_bank(8000, 128, 12)
checks["finer_grid_has_support_for_this_fixture"] = fine.empty_bands == ()
checks["dc_nyquist_endpoint_weights_are_zero"] = bool(
np.all(fine.weights[:, 0] == 0) and np.all(fine.weights[:, -1] == 0))
checks["continuous_peak_does_not_force_sampled_peak_one"] = bool(
np.all(fine.weights >= 0) and np.all(fine.weights <= 1)
and np.any(np.max(fine.weights, axis=1) < .99))
p = np.arange(17 * 3, dtype=np.float64).reshape(17, 3)
b = make_bank(8000, 32, 3)
out = project_power(p, 8000, 32, 3)
direct = np.array([[math.fsum(float(b.weights[i, k]) * float(p[k, t])
for k in range(17)) for t in range(3)] for i in range(3)])
checks["multiple_frames_independent_scalar_weighted_sums"] = bool(
out.shape == (3, 3) and np.allclose(out, direct, atol=1e-11, rtol=0))
zero = project_power(np.zeros((17, 2)), 8000, 32, 3)
checks["zero_spectrum_and_positive_linearity"] = bool(
np.array_equal(zero, np.zeros((3, 2)))
and np.allclose(project_power(2 * p + 3, 8000, 32, 3),
2 * out + project_power(np.full_like(p, 3), 8000, 32, 3),
atol=1e-11, rtol=0))
before = p.copy()
out[0, 0] = -99
mutable_bank = make_bank(5600, 8, 1, 0, 2100)
mutable_bank.weights[0, 1] = 99
checks["no_input_alias_and_no_stale_bank_shared_arrays"] = bool(
np.array_equal(p, before)
and np.allclose(make_bank(5600, 8, 1, 0, 2100).weights, expected,
atol=1e-12, rtol=0))
checks["integer_configuration_types_and_bounds"] = all([
rejects(lambda v=v: make_bank(v, 8, 1)) for v in (True, 8000.0, 1999, 48001, 10**400)
] + [rejects(lambda v=v: make_bank(8000, v, 1)) for v in (True, 3, 5, 130)]
+ [rejects(lambda v=v: make_bank(8000, 8, v)) for v in (False, 0, 25, 1.0)])
checks["frequency_bounds_and_numerically_collapsed_edges"] = all([
rejects(lambda lo=lo, hi=hi: make_bank(8000, 8, 2, lo, hi))
for lo, hi in ((-1, 4000), (0, 4001), (1000, 1000), (True, 3000),
(0, float("nan")), (float("inf"), 4000), (0, 10**400),
(0, 5e-324))])
invalid = [p.tolist(), p.astype(np.float32), p.astype(np.int64), p.astype(complex),
np.ones(17), np.ones((3, 17)), np.ones((17, 0)), np.ones((17, 9)),
np.full((17, 1), -1.0), np.full((17, 1), float("nan")),
np.full((17, 1), float("inf")), np.full((17, 1), 1e6 + 1)]
checks["power_exact_array_dtype_shape_range_finite"] = all(
rejects(lambda x=x: project_power(x, 8000, 32, 3)) for x in invalid)
cap = project_power(np.full((65, 8), 1e6), 48000, 128, 1)
checks["inclusive_caps_produce_finite_bounded_output"] = bool(
cap.shape == (1, 8) and np.all(np.isfinite(cap)) and np.all(cap <= 65e6))
assert all(type(v) is bool and v for v in checks.values()), checks
result = {
"python": platform.python_version(), "numpy": np.__version__,
"librosa_executed": False, "scipy_executed": False,
"checks": checks, "checks_passed": len(checks),
"literal": {"sr_hz": 5600, "n_fft": 8, "fmax_hz": 2100,
"frequencies_hz": bank.frequencies_hz.tolist(),
"edges_hz": bank.edges_hz.tolist(), "weights": bank.weights.tolist(),
"power": power[:, 0].tolist(), "weighted_power": float(score[0, 0]),
"weighted_magnitude": float(amplitude_score[0, 0])},
"coarse_empty_bands_zero_based": list(coarse.empty_bands),
"fine_empty_bands_zero_based": list(fine.empty_bands),
"scalar_oracle_banks_one_group": count, "scalar_oracle_max_abs_error": worst,
"limits": "Synthetic squared amplitude; no PSD calibration, FFT/audio/model run, area normalization, MFCC, accuracy or performance guarantee."
}
print(json.dumps(result, ensure_ascii=False, indent=2, allow_nan=False))
if __name__ == "__main__":
main()
آرایهٔ توان باید واقعاً ndarray با نوع float64 و شکلِ خانههای فرکانس در یک تا هشت قاب باشد. مقادیر منفی، نامتناهی یا بزرگتر از یکمیلیون پذیرفته نمیشوند. این کرانها محدودهٔ تمریناند، نه مشخصاتِ عمومی صوت. بعد از اعتبارسنجی، ضرب ماتریسیِ وزنها در توان، خروجیِ باندها در قابها را میدهد و ورودی را تغییر نمیدهد.
طبق توضیح melspectrogram دربارهٔ ورودی آماده، باید معلوم باشد طیف از قبل توان شده یا هنوز دامنه است. تابعِ ما هیچچیز را خودکار مربع نمیکند. دوبرابر کردنِ دامنه یعنی چهاربرابر شدنِ ورودیِ توان و خروجیِ وزندار؛ دوبرابر کردنِ خودِ آرایهٔ توان فقط خروجی را دوبرابر میکند. این دو آزمایش یکسان نیستند.
۴. شاهد دستی و فیلترِ بدون پشتیبانی

یک باند با حد پایین صفر و حد بالای ۲۱۰۰ هرتز، گرههای صفر، ۷۰۰ و ۲۱۰۰ هرتز میسازد؛ زیرا عبارتِ داخل لگاریتم در این سه نقطه یک، دو و چهار است. روی شبکهٔ پنجخانهایِ بخش اول، وزنها صفر، یک، نیم، صفر و صفر میشوند. برای توانهای ۹، چهار، شانزده، ۲۵ و ۳۶، پاسخ دستی برابر دوازده است: چهار بهعلاوهٔ نصفِ شانزده.
اگر بهاشتباه دامنههای متناظر، یعنی سه، دو، چهار، پنج و شش را جمعِ وزندار کنیم، پاسخ چهار میشود. همچنین همپوشانیِ مثلثها مجوز نامیدنِ خروجی بهعنوان مجموعِ تمام توان نیست؛ لبهها و قراردادِ وزن مهماند. در حالتِ حد پایینِ صفر و حد بالای نایکوئیست، وزنِ خودِ دو خانهٔ انتهایی صفر است؛ آنها قلهٔ هیچ باندی نیستند.
برای مرجع مستقل، بهجای ساختِ گره با تبدیلِ جلو و معکوس، کمیتِ 1+f/700 را هندسی درونیابی میکنیم و وزن هر خانه را با شرطهای تکهای میسنجیم. ۲۱۶ بانک با اندازهها و حدود متفاوت، در یک گروه بررسی شدند؛ بیشترین اختلاف حدود 1.01e-14 بود. تلورانس مطلقِ 1e-11 برای همین نمونههای محدود انتخاب شده و کرانِ عمومی خطا نیست.
در نرخ ۸۰۰۰ و طول هشت با دوازده باند، اندیسهای صفر، یک، دو، سه، چهار و هفت کاملاً خالیاند. ساختِ بانک این فهرست را گزارش میکند، ولی تابعِ محاسبهٔ ویژگی آن را رد میکند. با طول ۱۲۸، همین نمونه باند خالی ندارد؛ این شاهد تضمین نمیکند که افزایش طول برای هر قرارداد کافی باشد. صفرِ ناشی از نبودِ پشتیبانی نباید با مشاهدهٔ سکوت یکی گرفته شود.
{
"python": "3.14.5",
"numpy": "2.4.6",
"librosa_executed": false,
"scipy_executed": false,
"checks": {
"literal_frequency_grid_and_triangle": true,
"literal_weighted_power_is_twelve": true,
"magnitude_is_not_power": true,
"amplitude_scale_two_means_power_scale_four": true,
"scalar_geometric_piecewise_oracle_216_banks": true,
"coarse_grid_empty_bands_are_explicit": true,
"finer_grid_has_support_for_this_fixture": true,
"dc_nyquist_endpoint_weights_are_zero": true,
"continuous_peak_does_not_force_sampled_peak_one": true,
"multiple_frames_independent_scalar_weighted_sums": true,
"zero_spectrum_and_positive_linearity": true,
"no_input_alias_and_no_stale_bank_shared_arrays": true,
"integer_configuration_types_and_bounds": true,
"frequency_bounds_and_numerically_collapsed_edges": true,
"power_exact_array_dtype_shape_range_finite": true,
"inclusive_caps_produce_finite_bounded_output": true
},
"checks_passed": 16,
"literal": {
"sr_hz": 5600,
"n_fft": 8,
"fmax_hz": 2100,
"frequencies_hz": [
0.0,
700.0,
1400.0,
2100.0,
2800.0
],
"edges_hz": [
0.0,
700.0,
2100.0
],
"weights": [
[
0.0,
1.0,
0.5,
0.0,
0.0
]
],
"power": [
9.0,
4.0,
16.0,
25.0,
36.0
],
"weighted_power": 12.0,
"weighted_magnitude": 4.0
},
"coarse_empty_bands_zero_based": [
0,
1,
2,
3,
4,
7
],
"fine_empty_bands_zero_based": [],
"scalar_oracle_banks_one_group": 216,
"scalar_oracle_max_abs_error": 1.0061396160665481e-14,
"limits": "Synthetic squared amplitude; no PSD calibration, FFT/audio/model run, area normalization, MFCC, accuracy or performance guarantee."
}
۵. چهار پرسش و مرز تفسیر ویژگیها

پرسش اول: چرا پاسخ دستی دوازده است و نه چهار؟ پاسخ: ورودیِ اعلامشده مربعِ دامنه است؛ باند فقط چهار و نیمِ شانزده را میگیرد. چهار پاسخِ جمعِ وزندارِ دامنههاست و نمایندهٔ قرارداد دیگری است.
پرسش دوم: چرا ردیفِ سالم همیشه وزنِ یک ندارد؟ پاسخ: مرکز مثلث ممکن است میان دو خانه بیفتد؛ نمونهبرداری از قله عبور نمیکند. تقسیمِ خودسرانه بر بیشینه، قراردادِ تازهای میسازد و مقایسهٔ ویژگیها را تغییر میدهد.
پرسش سوم: با باند خالی، صفر را خروجی معتبر بگیریم؟ پاسخ: در سیاستِ این تمرین خیر؛ ابتدا شبکه یا تعداد باندها را بازبینی میکنیم. برنامه همهٔ ردیفها، حتی ردیفِ شروعشده از صفر، را میسنجد. این سیاستِ پذیرشِ ماست و رفتار هشدارِ کتابخانهای دیگر معرفی نمیشود.
پرسش چهارم: این خروجی همان ضرایبِ کپستروم یا کیفیت تشخیص گفتار است؟ پاسخ: خیر؛ لگاریتم، تبدیل کسینوسی، مدلِ آموزشدیده یا دادهٔ گفتاری نداریم. درسِ مقیاس و بازسازیِ DCT نیز بهتنهایی این بانک را به زنجیرهٔ کاملِ ویژگی تبدیل نمیکند. برای کاربرد واقعی باید کل زنجیره و وظیفهٔ نهایی جدا ارزیابی شوند.
شانزده گروهِ معنادار اجرا شدهاند؛ تعدادِ بانکهای مرجع یا تکرار اجرای داور، گروهِ تازه محسوب نمیشود. منابع در ۲۶ مهر ۱۴۰۵ خوانده شدند؛ نسخهٔ صفحهها با نسخهٔ اجرای واقعی جدا ثبت شده و تاریخِ انتشارِ اولیهٔ نامعلوم حدس زده نشده است. این آموزشِ پایهٔ تألیفی، ادعای خبر تازه، اجرای لیبروزا یا متلب، بازسازی صوت، کیفیت شنیداری یا سرعت دستگاه ندارد.
منابع و مطالعهٔ بیشتر
- NumPy 2.4؛ شبکهٔ فرکانس rfftfreq
- librosa 0.11.0؛ گونهٔ HTK و فاصلهٔ گرههای mel
- librosa 0.11.0؛ مثلثها و نرمالسازی بانک
- librosa 0.11.0؛ ورودی توان آماده
بررسی منابع: ۲۶ مهر ۱۴۰۵. تحلیلها و پیشنهادهای اجرایی، جمعبندی نویسندهاند.

