Hayami Kaito Web
~/tech

音声認識と音声合成のしくみ:スペクトログラム・DTW・CTC から Whisper・ニューラル音声合成まで

published

スマートフォンに話しかけて文字を入力したり、会議の録音から議事録を作ったり、文章を自然な声で読み上げたりする技術が、身近になりました。声を文字にするのが 音声認識、文字を声にするのが 音声合成です。

音声は、画像や文章とは違う難しさを持っています。同じ言葉でも、話す人・速さ・声の高さによって波形がまったく違いますし、「どの時刻がどの文字に当たるか」は、どこにも書かれていません。この記事では、その難しさを どう乗り越えてきたかを、小さな計算例で確かめます。

結論を先に書くと、次のとおりです。

  • 音声は波形のままではなく、短い区間ごとの周波数の強さ(スペクトログラム)にしてから扱う。人の耳に合わせて、低い音を細かく、高い音を粗くまとめる(メル尺度)
  • 話す速さの違いは、時間軸を伸び縮みさせて対応づけて吸収する。昔の音声認識は、DTW でお手本の波形と照合していた
  • 今の音声認識は、CTC やアテンションで、文字と音の位置の対応をモデルに学ばせる。CTC は「空白」という記号を入れて、ありうる対応のしかたをすべて足し合わせる
  • 音声合成は、文字からスペクトログラムを作り、そこから波形を作る 2 段階が基本。波形に戻すには、スペクトログラムにない「位相」を補う必要がある

説明の途中に出てくる計算例は、Python と NumPy で実際に動かした結果です。

環境

計算例を動かした環境です。

項目バージョン
Python3.13
NumPy2.5

音声を数にする

波形とサンプリング

マイクが拾った音声は、空気の圧力の変化を表す 波形です。コンピューターでは、これを一定の間隔で測った数の並びとして扱います。音声認識では、1 秒に 16,000 回測る(サンプリング周波数 16 kHz)のが一般的で、1 秒の音声が 16,000 個の数になります。

ただし、この数の並びをそのまま見ても、どんな音かはほとんど分かりません。同じ「ア」でも、声の高さや大きさが違えば、数の並びはまったく違うからです。

スペクトログラム:短い区間ごとの周波数の強さ

そこで、音声を 25 ミリ秒くらいの短い区間に区切り、区間ごとに どの周波数の成分がどれだけ強いかをフーリエ変換で求めます(短時間フーリエ変換、STFT)。区間を 10 ミリ秒ずつずらしながら求めた結果を、横に時間、縦に周波数で並べたものが スペクトログラムです。

母音の違いは、スペクトログラムにはっきり表れます。声は、声帯の振動(基本周波数)とその整数倍の 倍音からできていて、口や舌の形によって特定の周波数の倍音が強められます。この強められた山を フォルマントと呼び、低いほうから 2 つの山の位置で、おおよそ母音が決まります。「ア」と「イ」に近いフォルマントを持つ音を作り、スペクトログラムで強い周波数を調べてみます(音の波そのものは、サイト内アプリの スペクトラムアナライザー や フーリエ級数 でも確かめられます)。

import numpy as np

sr = 16000                                   # 1 秒あたり 16,000 個の値(サンプリング周波数)
t = np.arange(int(0.5 * sr)) / sr            # 0.5 秒
# 「母音」のような音:120 Hz の基本周波数の倍音を、2 つの山(フォルマント)で強調する
def vowel(f1, f2, f0=120):
    wave = np.zeros_like(t)
    for k in range(1, 40):
        f = k * f0
        amp = np.exp(-((f - f1) / 150) ** 2) + np.exp(-((f - f2) / 200) ** 2) + 0.02
        wave += amp * np.sin(2 * np.pi * f * t)
    return wave / np.abs(wave).max()

a, i = vowel(800, 1200), vowel(300, 2300)    # 「ア」と「イ」に近いフォルマント
print(len(a), np.round(a[:5], 3))

def stft(x, n_fft=512, hop=160):
    # 25 ms くらいの短い区間に区切り、窓を掛けてフーリエ変換する
    win = np.hanning(n_fft)
    frames = np.stack([x[s:s + n_fft] * win for s in range(0, len(x) - n_fft + 1, hop)])
    return np.abs(np.fft.rfft(frames, axis=1))   # 大きさ(振幅スペクトル)

S = stft(a)
freqs = np.fft.rfftfreq(512, 1 / sr)
print(S.shape)
spec = S.mean(axis=0)
peaks = [int(freqs[j]) for j in range(1, len(spec) - 1) if spec[j] > spec[j - 1] and spec[j] > spec[j + 1] and spec[j] > 0.3 * spec.max()]
print("ア", peaks)
spec_i = stft(i).mean(axis=0)
print("イ", [int(freqs[j]) for j in range(1, len(spec_i) - 1) if spec_i[j] > spec_i[j - 1] and spec_i[j] > spec_i[j + 1] and spec_i[j] > 0.3 * spec_i.max()])

実行結果:

8000 [0.    0.526 0.832 0.96  1.   ]
(47, 257)
ア [718, 843, 968, 1093, 1187, 1312]
イ [250, 375, 2156, 2281, 2406]

0.5 秒の音声は 8,000 個の数で、2 行目のとおり、スペクトログラムは 47 区間 × 257 個の周波数の表になりました。3・4 行目は、強い周波数(いちばん強い成分の 30% 以上の山)です。どちらも 120 Hz の倍音が並んでいますが(1 つの周波数の幅が 31.25 Hz なので、値は少しずれます)、「ア」では 700〜1,300 Hz、「イ」では 250〜400 Hz と 2,150〜2,400 Hz の倍音が強くなっています。声の高さ(120 Hz)が同じでも、どの倍音が強いかで母音を区別できるのです。

メル尺度:耳に合わせて周波数をまとめる

人の耳は、低い音の違いには敏感で、高い音の違いには鈍感です。100 Hz と 200 Hz の違いははっきり分かりますが、7,000 Hz と 7,100 Hz の違いはほとんど分かりません。この感じ方に合わせた周波数の目盛りが メル尺度です。

音声認識では、スペクトログラムの 257 個の周波数を、メル尺度で等間隔に並べた 40〜128 個程度の三角形の重み(メルフィルタバンク)でまとめ、対数を取った ログメルスペクトログラムを使うのが一般的です。上のコードの続きです。

# メルフィルタバンク:低い音は細かく、高い音は粗くまとめる
def mel(f): return 2595 * np.log10(1 + f / 700)
def mel_inv(m): return 700 * (10 ** (m / 2595) - 1)
def mel_filterbank(n_mels=40, n_fft=512):
    edges = mel_inv(np.linspace(mel(0), mel(sr / 2), n_mels + 2))
    fb = np.zeros((n_mels, len(freqs)))
    for m in range(n_mels):
        lo, c, hi = edges[m:m + 3]
        fb[m] = np.clip(np.minimum((freqs - lo) / (c - lo), (hi - freqs) / (hi - c)), 0, None)
    return fb, edges
fb, edges = mel_filterbank()
print(np.round(edges[[1, 2, 20, 21, 39, 40]]).astype(int))
logmel = np.log(S @ fb.T + 1e-6)
print(logmel.shape)

実行結果:

[  44   92 1693 1845 6994 7481]
(47, 40)

1 行目は、40 個のフィルターの中心の周波数のうち、最初の 2 つ・真ん中の 2 つ・最後の 2 つです。低いところでは約 50 Hz おき、真ん中では約 150 Hz おき、高いところでは約 500 Hz おきと、高い音ほど粗くまとめています。これで、1 区間あたり 257 個の数が 40 個になりました。このログメルスペクトログラムが、多くの音声認識モデルの入力になります。昔は、ここからさらに離散コサイン変換をした MFCC もよく使われました。

話す速さの違いを吸収する:DTW

お手本との照合

同じ単語でも、話すたびに長さが違います。ゆっくり話せば区間の数は増え、速く話せば減ります。1970 年代の音声認識は、単語ごとにお手本の音声を録音しておき、入力の音声と 時間軸を伸び縮みさせながら照合して、いちばん近いお手本の単語を答えていました。この照合に使われたのが DTW(動的時間伸縮法)です。

DTW は、入力の i 番目の区間とお手本の j 番目の区間を対応づけるとき、「1 つ前の区間どうし」「入力だけ 1 つ進める」「お手本だけ 1 つ進める」の 3 通りから距離の合計が最小になるものを選ぶ、動的計画法です。ゆっくり話した部分は、入力の何区間かがお手本の 1 区間に対応し、速く話した部分はその逆になります。

速さを変えた発話を認識する

5 つの母音を組み合わせた単語(「アイ」「アオ」「イエ」「ウエ」「オイ」)のお手本を作り、話す速さを 0.6〜1.7 倍に変えて雑音を加えた「アオ」が、どのお手本に近いかを DTW で調べます。母音の部分は上と同じ作り方で、ここでは独立したスクリプトとして書いています。

import numpy as np

sr = 16000
formants = {"ア": (800, 1200), "イ": (300, 2300), "ウ": (300, 1400), "エ": (500, 1900), "オ": (500, 800)}

def vowel(v, sec, f0=120):
    t = np.arange(int(sec * sr)) / sr
    f1, f2 = formants[v]
    wave = np.zeros_like(t)
    for k in range(1, 40):
        f = k * f0
        wave += (np.exp(-((f - f1) / 150) ** 2) + np.exp(-((f - f2) / 200) ** 2) + 0.02) * np.sin(2 * np.pi * f * t)
    return wave / np.abs(wave).max()

def say(word, speed=1.0, noise=0.0, rng=None):
    x = np.concatenate([vowel(v, 0.2 / speed) for v in word])
    return x + noise * rng.standard_normal(len(x)) if noise else x

freqs = np.fft.rfftfreq(512, 1 / sr)
def mel(f): return 2595 * np.log10(1 + f / 700)
def mel_inv(m): return 700 * (10 ** (m / 2595) - 1)
edges = mel_inv(np.linspace(mel(0), mel(sr / 2), 42))
fb = np.array([np.clip(np.minimum((freqs - lo) / (c - lo), (hi - freqs) / (hi - c)), 0, None) for lo, c, hi in zip(edges, edges[1:], edges[2:])])

def features(x, n_fft=512, hop=160):
    win = np.hanning(n_fft)
    frames = np.stack([x[s:s + n_fft] * win for s in range(0, len(x) - n_fft + 1, hop)])
    logmel = np.log(np.abs(np.fft.rfft(frames, axis=1)) @ fb.T + 1.0)
    return logmel - logmel.mean(axis=1, keepdims=True)   # 音量の違いを打ち消す

def dtw(A, B):
    # D[i, j] = A の i 番目までと B の j 番目までを対応づけたときの、距離の合計の最小値
    D = np.full((len(A) + 1, len(B) + 1), np.inf)
    D[0, 0] = 0
    cost = np.linalg.norm(A[:, None] - B[None], axis=2)
    for i in range(1, len(A) + 1):
        for j in range(1, len(B) + 1):
            D[i, j] = cost[i - 1, j - 1] + min(D[i - 1, j], D[i, j - 1], D[i - 1, j - 1])
    return D[-1, -1] / (len(A) + len(B))

words = ["アイ", "アオ", "イエ", "ウエ", "オイ"]
templates = {w: features(say(w)) for w in words}
rng = np.random.default_rng(0)
print({w: f.shape[0] for w, f in templates.items()})

# 話す速さを変えた発話を、どの単語のお手本にいちばん近いかで認識する
for speed in [0.6, 1.0, 1.7]:
    x = features(say("アオ", speed, 0.1, rng))
    print(speed, x.shape[0], {w: round(float(dtw(x, f)), 2) for w, f in templates.items()})

# 1 音目だけ長く伸ばした「アーオ」
x = features(np.concatenate([vowel("ア", 0.5), vowel("オ", 0.1)]) + 0.1 * rng.standard_normal(9600))
print("アーオ", {w: round(float(dtw(x, f)), 2) for w, f in templates.items()})

実行結果:

{'アイ': 37, 'アオ': 37, 'イエ': 37, 'ウエ': 37, 'オイ': 37}
0.6 64 {'アイ': 4.73, 'アオ': 3.78, 'イエ': 6.18, 'ウエ': 5.84, 'オイ': 5.31}
1.0 37 {'アイ': 3.93, 'アオ': 2.96, 'イエ': 4.93, 'ウエ': 4.64, 'オイ': 4.59}
1.7 21 {'アイ': 5.0, 'アオ': 3.74, 'イエ': 6.06, 'ウエ': 5.64, 'オイ': 5.45}
アーオ {'アイ': 4.52, 'アオ': 3.9, 'イエ': 6.45, 'ウエ': 6.03, 'オイ': 5.55}

お手本はどれも 37 区間です。入力は、ゆっくり話すと 64 区間、速く話すと 21 区間と長さが違いますが、どの速さでも「アオ」のお手本との距離がいちばん小さくなり、正しく認識できました。最後の行の、「ア」だけを長く伸ばして「オ」を短くした発話も、「アオ」と認識できています。DTW が、伸びた部分と縮んだ部分をそれぞれ対応づけてくれるからです。

統計的な音声認識へ

DTW による照合は、単語の数が少なく、話す人が決まっているときはうまくいきますが、いろいろな人の声や、数万語の単語を扱うのは困難です。1980 年代からは、音素(「a」「k」のような音の単位)ごとに音響の特徴の確率分布を持たせ、音素の並びを 隠れマルコフモデル(HMM)で表す方法が主流になりました。2012 年ごろには、その確率分布の部分を深層学習のモデルに置き換えて、認識の精度が大きく上がりました。

ただし、この方法は、音響モデル・発音辞書・言語モデルを別々に作って組み合わせる必要があり、学習のためには「どの区間がどの音素か」の対応も必要でした。

CTC:文字と音の位置の対応を学ぶ

空白を入れて、まとめる

音声と書き起こしの文字の組を大量に集めることはできても、どの区間がどの文字に当たるかまで人が付けるのは大変です。2006 年に発表された CTC(Connectionist Temporal Classification)は、この対応をモデルに学ばせる方法です。

CTC では、モデルは区間ごとに「どの文字か」に加えて、「どの文字でもない」を表す 空白(ブランク、ここでは _)の確率を出します。そして、区間ごとの記号の並び(経路)を、次の規則で文字列に直します。

  1. 同じ記号が続いたら 1 つにまとめる
  2. 空白を取り除く
import itertools
import numpy as np

labels = ["_", "ア", "イ", "オ"]           # "_" は空白(ブランク)
def collapse(path):
    # 1. 同じ記号の連続を 1 つにまとめる  2. 空白を取り除く
    out = [p for k, p in enumerate(path) if k == 0 or p != path[k - 1]]
    return "".join(p for p in out if p != "_")

for p in ["アアア__イイ", "_ア_ア_イ_", "アアイイイ_", "ア_アイ"]:
    print(p, "→", collapse(p))

実行結果:

アアア__イイ → アイ
_ア_ア_イ_ → アアイ
アアイイイ_ → アイ
ア_アイ → アアイ

「アアア__イイ」も「アアイイイ_」も「アイ」になります。どの文字を何区間続けるか、間にどれだけ空白を入れるかで、同じ文字列になる経路がたくさんあるわけです。空白があるおかげで、「ア_ア」のように、同じ文字が 2 回続く言葉(「ああ」)も表せます。

ありうる経路をすべて足し合わせる

CTC の学習では、正解の文字列になる すべての経路の確率の合計を大きくします。どの経路が正しい対応かを決めなくてよいので、区間と文字の対応がないデータでも学習できます。

6 区間の音声に対して、モデルが区間ごとに 4 つの記号の確率を出したとして、「アイ」になる確率を計算してみます。まずは、4 の 6 乗 = 4,096 通りの経路をすべて調べます。上のコードの続きです。

# 音響モデルが 6 フレームそれぞれで出した、各記号の確率
rng = np.random.default_rng(0)
probs = rng.dirichlet(np.ones(4) * 0.5, size=6)
probs[:2, 1] += 1.0; probs[3:5, 2] += 1.0   # 前半は「ア」、後半は「イ」らしい
probs /= probs.sum(axis=1, keepdims=True)
print(np.round(probs, 2))

# 総当たり:4^6 = 4,096 通りの経路のうち、まとめると「アイ」になるものの確率を足す
total = 0.0
count = 0
for path in itertools.product(range(4), repeat=6):
    if collapse([labels[k] for k in path]) == "アイ":
        total += np.prod(probs[np.arange(6), path]); count += 1
print(count, total)

実行結果:

[[0.12 0.5  0.27 0.11]
 [0.05 0.66 0.2  0.1 ]
 [0.69 0.05 0.   0.26]
 [0.13 0.18 0.68 0.01]
 [0.11 0.1  0.64 0.15]
 [0.14 0.05 0.01 0.8 ]]
70 0.028700946816304817

表の列は _・ア・イ・オ の順で、行が区間です。4,096 通りのうち 70 通りの経路が「アイ」になり、その確率の合計は約 0.0287 でした。

前向きアルゴリズムで速く計算する

経路の数は、区間の数に対して指数関数的に増えるので、実際の音声(数百〜数千区間)では総当たりはできません。そこで、正解の文字列の間と両端に空白を入れた並び(_ ア _ イ _)を考え、「t 区間目までで、この並びの s 番目まで進んでいる確率」を前から順に足していきます(前向きアルゴリズム)。上のコードの続きです。

def ctc_forward(probs, target):
    # 正解の間と両端に空白を入れた並び(_ ア _ イ _)の上を、前から確率を足していく
    ext = [0]
    for c in target:
        ext += [labels.index(c), 0]
    alpha = np.zeros((len(probs), len(ext)))
    alpha[0, 0], alpha[0, 1] = probs[0, ext[0]], probs[0, ext[1]]
    for t in range(1, len(probs)):
        for s in range(len(ext)):
            a = alpha[t - 1, s] + (alpha[t - 1, s - 1] if s >= 1 else 0)
            if s >= 2 and ext[s] != 0 and ext[s] != ext[s - 2]:
                a += alpha[t - 1, s - 2]        # 違う文字どうしなら、間の空白を飛ばせる
            alpha[t, s] = a * probs[t, ext[s]]
    return alpha[-1, -1] + alpha[-1, -2]

print(ctc_forward(probs, "アイ"))
best = "".join(labels[k] for k in probs.argmax(axis=1))
print(best, "→", collapse(best))
print(ctc_forward(probs, "アイオ"), np.prod(probs.max(axis=1)))

実行結果:

0.02870094681630482
アア_イイオ → アイオ
0.18657264604407528 0.07879553673653238

1 行目は、総当たりで求めた 0.0287 と同じ値になりました。計算量は「区間の数 × 文字列の長さ」に比例するだけなので、長い音声でも速く計算できます。学習では、この確率の対数を損失にして、誤差逆伝播法でモデルを学習します。

2 行目は、区間ごとにいちばん確率の高い記号を選んで、まとめた結果です(貪欲法)。最後の区間で「オ」の確率が 0.8 と高いので、「アイオ」になりました。3 行目の左は、「アイオ」になる経路の確率の合計 0.187 で、「アイ」の 0.029 よりずっと高く、このモデルの確率では「アイオ」が最も確からしい答えです。右の 0.079 は、貪欲法で選んだ 1 本の経路だけの確率で、ほかの経路も合わせると 0.187 になります。実際の音声認識では、文字列ごとに経路の確率を合計しながら、有望な候補を何本か残して探す ビームサーチを使い、言語モデルの確率も組み合わせて、「アイオ」のような言葉として不自然な答えを避けます。

アテンションで対応を学ぶ:Whisper

CTC は、区間ごとに独立に文字を出すので、前後の文字の関係を学ぶのは苦手です。そこで、Transformer のように、音声をエンコーダーで読み、デコーダーが アテンションで音声のどこを見るかを決めながら、文字を 1 つずつ出す方法も使われます。音声と文字の対応は、アテンションの重みとして学習されます。

2022 年に OpenAI が公開した Whisper は、この方法のモデルを、ウェブから集めた約 68 万時間の音声と書き起こしの組で学習しました。ログメルスペクトログラムを入力に、書き起こし・翻訳・言語の判定などを 1 つのモデルで行います。ラベルの付いていない大量の音声で、埋め込み のように音声の表し方を先に学ぶ wav2vec 2.0(2020 年)のような自己教師あり学習も、少ないデータで精度を上げるのに使われています。

音声合成:文字から声を作る

2 段階で作る

音声合成は、音声認識の逆で、文字から波形を作ります。昔は、録音した音声を細かく切って、つなぎ合わせる方法(波形接続型)が主流でした。今のニューラル音声合成は、多くの場合、次の 2 段階で声を作ります。

  1. 音響モデル:文字(や読み・アクセント)から、ログメルスペクトログラムを作る。2017 年の Tacotron などが、アテンションで文字と時間の対応を学ぶ
  2. ボコーダー:スペクトログラムから波形を作る

位相を補う:グリフィン・リム法

2 段階目が必要なのは、スペクトログラムには周波数ごとの 大きさしかなく、波がどの位置から始まるかを表す 位相の情報がないからです。位相がでたらめなまま波形に戻すと、ガサガサした音になります。

1984 年に発表された グリフィン・リム法は、大きさを保ったまま、位相だけを少しずつ整えます。

  1. 位相をでたらめに決めて、波形に戻す
  2. その波形のスペクトログラムを計算し、位相だけを取り出して、大きさは目標のものに置き換える
  3. 波形に戻して、2 に戻る

区間を重ねながら波形に戻すので、隣り合う区間で位相のつじつまが合わないと、戻した波形のスペクトログラムは目標とずれます。くり返すうちに、つじつまの合う位相に近づいていきます。目標の大きさと、作った波形の大きさのずれ(目標の大きさに対する割合)を測ってみます。

import numpy as np

sr, n_fft, hop = 16000, 512, 128
win = np.hanning(n_fft)
t = np.arange(int(0.5 * sr)) / sr
x = sum((np.exp(-((k * 120 - 800) / 150) ** 2) + np.exp(-((k * 120 - 1200) / 200) ** 2) + 0.02) * np.sin(2 * np.pi * k * 120 * t) for k in range(1, 40))
x /= np.abs(x).max()

def stft(x):
    return np.fft.rfft(np.stack([x[s:s + n_fft] * win for s in range(0, len(x) - n_fft + 1, hop)]), axis=1)

def istft(X, length):
    # 逆フーリエ変換した区間を、窓を考慮しながら重ね合わせて波形に戻す
    frames = np.fft.irfft(X, n=n_fft, axis=1)
    y, norm = np.zeros(length), np.zeros(length)
    for k, s in enumerate(range(0, length - n_fft + 1, hop)):
        y[s:s + n_fft] += frames[k] * win
        norm[s:s + n_fft] += win ** 2
    return y / np.maximum(norm, 1e-8)

mag = np.abs(stft(x))                      # 合成で作られるのは「大きさ」だけ
def error(y):
    return np.linalg.norm(np.abs(stft(y)) - mag) / np.linalg.norm(mag)

rng = np.random.default_rng(0)
y = istft(mag * np.exp(2j * np.pi * rng.random(mag.shape)), len(x))   # 位相はでたらめ
print(0, round(error(y), 3))
for it in range(1, 101):
    # グリフィン・リム法:大きさは合わせ、位相だけ今の波形から取り直す、をくり返す
    phase = np.angle(stft(y))
    y = istft(mag * np.exp(1j * phase), len(x))
    if it in (1, 10, 50, 100):
        print(it, round(error(y), 3))

実行結果:

0 0.651
1 0.385
10 0.252
50 0.151
100 0.112

位相がでたらめなままでは、ずれが 65.1% ありましたが、くり返すたびに小さくなり、100 回で 11.2% まで下がりました。ただし、ゆっくりとしか下がらず、完全には一致しません。グリフィン・リム法で作った声は、どうしても少し機械的な響きが残ります。

ニューラルボコーダー

2016 年に DeepMind が発表した WaveNet は、波形を 1 つずつ、直前までの値から次の値の確率を出して作るモデルで、人の声に近い自然な音声を作れることを示しました。ただし、1 秒に 16,000〜24,000 個の値を 1 つずつ作るので、とても遅い方法でした。その後、画像生成 AI で説明した GAN を使って、スペクトログラムから波形を一度に作る HiFi-GAN(2020 年)などが登場し、速く高品質な合成ができるようになりました。

音声もトークンにする

最近は、音声を圧縮するニューラルネットワーク(ニューラル音声コーデック)で、音声を数千種類の記号(トークン)の並びに変え、LLM と同じように「次の音声トークンを当てる」モデルで音声を作る方法も使われています。2023 年の VALL-E は、この方法で、数秒の録音から、その人の声で任意の文章を読み上げられることを示しました。声をまねる技術はなりすましにも悪用できるので、合成された音声であることを示す透かしや、使い方の制限も議論されています。

音声認識と音声合成の手法の流れ

この記事で紹介した手法を年代順に並べます。

年手法・出来事ポイント
1937メル尺度(スティーブンスら)人が感じる音の高さの目盛り
1978DTW による単語の認識(迫江・千葉)時間軸を伸び縮みさせて、お手本と照合する
1980MFCC(デービス・マーメルスタイン)メル尺度のスペクトルを圧縮した、音声認識の定番の特徴
1980 年代隠れマルコフモデルによる音声認識音素の並びと音響の特徴を確率でモデル化する
1984グリフィン・リム法大きさだけのスペクトログラムから、位相を推定して波形に戻す
2006CTC空白を入れて、文字と区間の対応のしかたをすべて足し合わせる
2012深層学習による音響モデルHMM の確率分布をニューラルネットワークに置き換える
2016WaveNet波形を 1 つずつ予測して、自然な声を作る
2017Tacotron文字からスペクトログラムを、アテンションで作る
2020wav2vec 2.0・HiFi-GAN大量の音声で自己教師あり学習をする。GAN で速く波形を作る
2022Whisper約 68 万時間の音声で学習した、多言語の音声認識
2023VALL-E音声をトークンにして、言語モデルと同じように声を作る

まとめ

  • 音声は、短い区間ごとの周波数の強さ(スペクトログラム)にして扱う。母音は、どの倍音が強いか(フォルマント)で区別できる
  • メル尺度で低い音を細かく、高い音を粗くまとめたログメルスペクトログラムが、多くのモデルの入力になる
  • 話す速さの違いは、DTW で時間軸を伸び縮みさせて吸収できる。昔の音声認識は、この方法でお手本と照合していた
  • CTC は、空白を入れた記号の経路を考え、正解の文字列になる経路の確率をすべて足し合わせて学習する。前向きアルゴリズムで速く計算できる
  • Whisper のようなモデルは、アテンションで音声と文字の対応を学び、大量のデータで多言語の認識をする
  • 音声合成は、文字からスペクトログラムを作り、ボコーダーで波形にする。スペクトログラムにない位相は、グリフィン・リム法やニューラルボコーダーで補う

参考

  • S. S. Stevens, J. Volkmann and E. B. Newman, "A Scale for the Measurement of the Psychological Magnitude Pitch", Journal of the Acoustical Society of America 1937
  • H. Sakoe and S. Chiba, "Dynamic programming algorithm optimization for spoken word recognition", IEEE Transactions on Acoustics, Speech, and Signal Processing 1978
  • S. Davis and P. Mermelstein, "Comparison of parametric representations for monosyllabic word recognition in continuously spoken sentences", IEEE Transactions on Acoustics, Speech, and Signal Processing 1980
  • D. Griffin and J. Lim, "Signal estimation from modified short-time Fourier transform", IEEE Transactions on Acoustics, Speech, and Signal Processing 1984
  • L. R. Rabiner, "A Tutorial on Hidden Markov Models and Selected Applications in Speech Recognition", Proceedings of the IEEE 1989
  • A. Graves et al., "Connectionist Temporal Classification: Labelling Unsegmented Sequence Data with Recurrent Neural Networks", ICML 2006
  • G. Hinton et al., "Deep Neural Networks for Acoustic Modeling in Speech Recognition", IEEE Signal Processing Magazine 2012
  • A. van den Oord et al., "WaveNet: A Generative Model for Raw Audio", 2016(arXiv:1609.03499)
  • Y. Wang et al., "Tacotron: Towards End-to-End Speech Synthesis", Interspeech 2017(arXiv:1703.10135)
  • A. Baevski et al., "wav2vec 2.0: A Framework for Self-Supervised Learning of Speech Representations", NeurIPS 2020(arXiv:2006.11477)
  • J. Kong et al., "HiFi-GAN: Generative Adversarial Networks for Efficient and High Fidelity Speech Synthesis", NeurIPS 2020(arXiv:2010.05646)
  • A. Radford et al., "Robust Speech Recognition via Large-Scale Weak Supervision", 2022(arXiv:2212.04356)
  • C. Wang et al., "Neural Codec Language Models are Zero-Shot Text to Speech Synthesizers", 2023(arXiv:2301.02111)