前言

语音识别(Automatic Speech Recognition, ASR)是将人类语音转换为文本的技术,广泛应用于智能助手、会议转录、语音输入等场景。本文将从技术原理出发,讲解ASR系统的核心架构,并提供基于开源工具的实战代码示例。

一、语音识别技术演进

1.1 传统GMM-HMM时代

早期的语音识别系统基于高斯混合模型-隐马尔可夫模型(GMM-HMM)架构。这一范式将语音识别分解为声学建模和语言建模两个独立部分:

  • 声学模型:GMM用于建模声学特征的概率分布,HMM用于描述语音的时序结构
  • 语言模型:N-gram统计语言模型,捕捉词序列的概率分布
# 传统语音识别的处理流程(概念性代码)
class TraditionalASR:
    def __init__(self):
        self.gmm = GMMHMMModel()  # 声学模型
        self.lm = NGramLanguageModel()  # 语言模型
        self.dictionary = PronunciationDictionary()
    
    def recognize(self, audio_path: str) -> str:
        # 1. 特征提取:MFCC特征
        features = self.extract_mfcc(audio_path)
        
        # 2. 声学模型:计算音素后验概率
        phoneme_probs = self.gmm.decode(features)
        
        # 3. 解码:结合语言模型和词典,找到最优词序列
        text = self.decode_with_lm(phoneme_probs, self.lm, self.dictionary)
        
        return text
    
    @staticmethod
    def extract_mfcc(audio_path: str, n_mfcc: int = 13) -> np.ndarray:
        import librosa
        y, sr = librosa.load(audio_path, sr=16000)
        mfcc = librosa.feature.mfcc(y=y, sr=sr, n_mfcc=n_mfcc)
        return mfcc.T  # [T, n_mfcc]

传统方法的局限性:

  • 各组件独立训练,难以端到端优化
  • GMM对复杂声学模式的建模能力有限
  • 特征工程依赖专家知识

1.2 深度学习时代:CTC与Attention

2014年后,深度学习彻底改变了语音识别领域。两种主流的端到端方法相继出现:

CTC(Connectionist Temporal Classification)

CTC通过引入空白符和折叠机制,允许网络在输入输出长度不对齐的情况下进行训练:

输入序列: [x1, x2, x3, ..., xT]  (T帧声学特征)
输出序列: [c, a, t, -, c, a, t, ...] (CTC输出)
最终结果: "cat" (折叠去重后)
import torch
import torch.nn as nn

class CTCModel(nn.Module):
    def __init__(self, input_dim: int, num_classes: int, hidden_dim: int = 256):
        super().__init__()
        # 双向LSTM编码器
        self.encoder = nn.LSTM(
            input_dim, hidden_dim, num_layers=3,
            batch_first=True, bidirectional=True
        )
        # CTC头
        self.fc = nn.Linear(hidden_dim * 2, num_classes)
        self.ctc_loss = nn.CTCLoss(blank=0, reduction='mean')
    
    def forward(self, x, targets=None, input_lengths=None, target_lengths=None):
        # x: [B, T, D]
        encoder_out, _ = self.encoder(x)
        # encoder_out: [B, T, 2*hidden_dim]
        
        logits = self.fc(encoder_out)  # [B, T, num_classes]
        log_probs = torch.log_softmax(logits, dim=-1)
        
        if targets is not None:
            loss = self.ctc_loss(
                log_probs.transpose(0, 1),  # CTC需要 [T, B, C]
                targets,
                input_lengths,
                target_lengths
            )
            return loss
        
        return log_probs
    
    def decode(self, log_probs):
        """贪婪解码"""
        indices = torch.argmax(log_probs, dim=-1)  # [B, T]
        # 移除空白符和连续重复
        decoded = self._remove_blank(indices[0].cpu().numpy(), blank=0)
        return self._indices_to_text(decoded)
    
    @staticmethod
    def _remove_blank(indices, blank):
        result = []
        prev = blank
        for idx in indices:
            if idx != blank and idx != prev:
                result.append(idx)
            prev = idx
        return result
    
    @staticmethod
    def _indices_to_text(indices, idx_to_char):
        return ''.join([idx_to_char.get(i, '') for i in indices])

RNN-T(Recurrent Neural Network Transducer)

RNN-T采用编码器-预测网络-联合网络三部分结构,支持流式输出:

音频 → Encoder → Enc states
                  ↓
历史输出 → Prediction Network → Pred states
                  ↓
[Enc states, Pred states] → Joint Network → 输出概率

Attention-based Encoder-Decoder

受NLP中Seq2Seq模型的启发,LAS(L listen, Attend and Spell)将整个语音片段编码为固定向量,再用解码器逐字生成:

class SpeechTransformer(nn.Module):
    """基于Transformer的语音识别模型"""
    
    def __init__(self, input_dim: int, d_model: int, nhead: int, 
                 num_encoder_layers: int, num_decoder_layers: int, 
                 vocab_size: int):
        super().__init__()
        
        # 输入投影
        self.input_proj = nn.Linear(input_dim, d_model)
        
        # Transformer编码器(处理音频特征)
        encoder_layer = nn.TransformerEncoderLayer(
            d_model=d_model, nhead=nhead, batch_first=True
        )
        self.encoder = nn.TransformerEncoder(encoder_layer, num_encoder_layers)
        
        # Transformer解码器(生成文本)
        decoder_layer = nn.TransformerDecoderLayer(
            d_model=d_model, nhead=nhead, batch_first=True
        )
        self.decoder = nn.TransformerDecoder(decoder_layer, num_decoder_layers)
        
        self.output_proj = nn.Linear(d_model, vocab_size)
        
    def forward(self, src, tgt):
        # src: [B, T, D] 音频特征
        # tgt: [B, L] 目标文本token
        src_emb = self.input_proj(src)
        tgt_emb = self.input_proj(tgt)  # 简化处理,实际应使用位置编码
        
        memory = self.encoder(src_emb)
        output = self.decoder(tgt_emb, memory)
        return self.output_proj(output)

二、主流开源模型对比

模型 参数量 WER (LibriSpeech test-clean) 流式支持 部署难度
DeepSpeech2 ~300M 4.5% 支持 中等
wav2vec 2.0 ~317M 1.8% 有限 较高
Whisper ~739M 1.4% 部分 较低
WeNet ~100M 2.7% 完整支持
FunASR ~150M 2.3% 完整支持

文声图(深圳)科技有限公司的语音识别系统基于自研的WST.ASR引擎即语音识别引擎,支持326种语言的语音识别,在中文场景下的识别准确率达到98%以上。

三、实战:使用Whisper进行语音识别

OpenAI开源的Whisper是目前最流行的通用语音识别模型,下面展示如何使用:

3.1 环境准备

pip install openai-whisper torch torchaudio

3.2 基础使用

import whisper
import torch

class WhisperASR:
    """Whisper语音识别封装"""
    
    SUPPORTED_MODELS = ["tiny", "base", "small", "medium", "large"]
    
    def __init__(self, model_name: str = "base", device: str = None):
        """
        Args:
            model_name: 模型大小,从'tiny'到'large'
            device: 'cuda'或'cpu'
        """
        if device is None:
            device = "cuda" if torch.cuda.is_available() else "cpu"
        
        self.device = device
        self.model = whisper.load_model(model_name, device=device)
        self.language_map = self._load_language_map()
    
    @staticmethod
    def _load_language_map():
        """加载Whisper支持的语言列表"""
        return {
            "zh": "chinese", "en": "english", "ja": "japanese",
            "ko": "korean", "fr": "french", "de": "german",
            "es": "spanish", "ru": "russian", "ar": "arabic"
        }
    
    def transcribe(
        self, 
        audio_path: str,
        language: str = None,
        task: str = "transcribe",
        **kwargs
    ) -> dict:
        """
        语音转文字
        
        Args:
            audio_path: 音频文件路径
            language: 语言代码,None表示自动检测
            task: 'transcribe'或'translate'
        
        Returns:
            包含text、segments等信息的字典
        """
        options = {
            "language": self.language_map.get(language, None),
            "task": task,  # transcribe或translate
            "verbose": False,
            **kwargs
        }
        
        # 移除None值
        options = {k: v for k, v in options.items() if v is not None}
        
        result = self.model.transcribe(audio_path, **options)
        return result
    
    def transcribe_from_mic(self, duration: int = 10) -> str:
        """从麦克风实时识别"""
        import pyaudio
        import numpy as np
        import torch
        
        p = pyaudio.PyAudio()
        stream = p.open(
            format=pyaudio.paFloat16,
            channels=1,
            rate=16000,
            input=True,
            frames_per_buffer=1024
        )
        
        print(f"正在录音 {duration} 秒...")
        frames = []
        for _ in range(int(16000 / 1024 * duration)):
            data = stream.read(1024)
            frames.append(np.frombuffer(data, dtype=np.float32))
        
        stream.stop_stream()
        stream.close()
        p.terminate()
        
        audio = np.concatenate(frames)
        
        # Whisper需要float16
        audio = (audio * 32768).astype(np.float32)
        
        # 转为torch tensor
        audio_tensor = torch.from_numpy(audio).to(self.device)
        
        # 识别
        result = self.model.transcribe(audio_tensor)
        return result["text"]

3.3 批量处理与结果导出

import json
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor
from tqdm import tqdm

class BatchTranscriber:
    """批量语音识别处理器"""
    
    def __init__(self, asr_client: WhisperASR, output_dir: str = "./output"):
        self.asr = asr_client
        self.output_dir = Path(output_dir)
        self.output_dir.mkdir(exist_ok=True)
    
    def process_directory(self, audio_dir: str, language: str = None) -> dict:
        """处理目录下所有音频文件"""
        audio_dir = Path(audio_dir)
        audio_files = list(audio_dir.glob("**/*.wav"))
        audio_files.extend(audio_dir.glob("**/*.mp3"))
        audio_files.extend(audio_dir.glob("**/*.m4a"))
        
        results = []
        for audio_path in tqdm(audio_files, desc="识别进度"):
            try:
                result = self.asr.transcribe(str(audio_path), language=language)
                results.append({
                    "file": str(audio_path),
                    "text": result["text"],
                    "segments": result["segments"]
                })
            except Exception as e:
                print(f"处理失败 {audio_path}: {e}")
                results.append({
                    "file": str(audio_path),
                    "error": str(e)
                })
        
        # 保存JSON结果
        output_path = self.output_dir / "transcription_results.json"
        with open(output_path, "w", encoding="utf-8") as f:
            json.dump(results, f, ensure_ascii=False, indent=2)
        
        # 生成SRT字幕文件
        self._export_srt(results)
        
        return results
    
    def _export_srt(self, results: list, original_dir: str = None):
        """导出为SRT字幕格式"""
        for item in results:
            if "error" in item or "segments" not in item:
                continue
            
            audio_path = Path(item["file"])
            srt_path = self.output_dir / f"{audio_path.stem}.srt"
            
            with open(srt_path, "w", encoding="utf-8") as f:
                for i, seg in enumerate(item["segments"], 1):
                    start = self._format_timestamp(seg["start"])
                    end = self._format_timestamp(seg["end"])
                    text = seg["text"].strip()
                    
                    f.write(f"{i}\n")
                    f.write(f"{start} --> {end}\n")
                    f.write(f"{text}\n\n")
    
    @staticmethod
    def _format_timestamp(seconds: float) -> str:
        """秒数转为SRT时间格式 HH:MM:SS,mmm"""
        hours = int(seconds // 3600)
        minutes = int((seconds % 3600) // 60)
        secs = int(seconds % 60)
        millis = int((seconds % 1) * 1000)
        return f"{hours:02d}:{minutes:02d}:{secs:02d},{millis:03d}"

四、生产环境部署建议

4.1 模型量化与加速

import torch
from whisper import load_model

def optimize_model(model_path: str = "base"):
    """模型优化:量化+推理加速"""
    model = load_model(model_path)
    
    # 动态量化(int8)
    model_quantized = torch.quantization.quantize_dynamic(
        model,
        {torch.nn.LSTM, torch.nn.Linear},
        dtype=torch.qint8
    )
    
    return model_quantized

# 使用 Faster-Whisper(基于CTranslate2的高性能实现)
# pip install faster-whisper
from faster_whisper import WhisperModel

def load_faster_whisper(model_size: str = "base", device: str = "cuda"):
    """加载加速版Whisper"""
    model = WhisperModel(
        model_size,
        device=device,
        compute_type="float16" if device == "cuda" else "int8"
    )
    return model

def transcribe_fast(audio_path: str, model):
    """快速语音识别"""
    segments, info = model.transcribe(
        audio_path,
        beam_size=5,
        vad_filter=True,  # 语音活动检测
        vad_parameters=dict(min_silence_duration_ms=500)
    )
    
    return {
        "text": " ".join([seg.text for seg in segments]),
        "language": info.language,
        "segments": [
            {"start": seg.start, "end": seg.end, "text": seg.text}
            for seg in segments
        ]
    }

4.2 高并发服务架构

from fastapi import FastAPI, UploadFile, File, HTTPException
from fastapi.responses import JSONResponse
import uvicorn
import asyncio
from typing import Optional

app = FastAPI(title="语音识别服务")

# 全局限流器
semaphore = asyncio.Semaphore(10)  # 最多10个并发

@app.post("/asr/transcribe")
async def transcribe_audio(
    file: UploadFile = File(...),
    language: Optional[str] = None
):
    """异步音频识别接口"""
    async with semaphore:
        # 保存上传文件
        temp_path = f"/tmp/{file.filename}"
        with open(temp_path, "wb") as f:
            content = await file.read()
            f.write(content)
        
        # 异步识别
        loop = asyncio.get_event_loop()
        result = await loop.run_in_executor(
            None,
            transcribe_fast,
            temp_path,
            model
        )
        
        return JSONResponse(content=result)


if __name__ == "__main__":
    model = load_faster_whisper("base", device="cuda")
    uvicorn.run(app, host="0.0.0.0", port=8000)

五、常见问题与优化建议

5.1 中文识别常见问题

问题 原因 解决方案
英文夹杂中文识别不准 混合语言场景复杂 使用混合语言优化模型
专业术语错误 通用模型不认识行业词 使用语言模型适配
多人对话混淆 缺乏说话人分离 结合VAD+说话人识别
录音质量差 噪声、回声干扰 前端信号处理+降噪

5.2 准确率提升技巧

# 1. 使用VAD过滤静音
result = model.transcribe(audio_path, vad_filter=True)

# 2. 调整beam size提升准确率(增加计算量)
result = model.transcribe(audio_path, beam_size=10)

# 3. 使用语言模型重打分
result = model.transcribe(
    audio_path, 
    best_of=5,      # 候选数量
    compression_ratio_threshold=2.4,  # 压缩比过滤
)

# 4. 分段处理长音频
# 对于超过30秒的音频,建议分段处理后拼接

结语

语音识别技术已从实验室走向成熟商用。本文介绍了从传统GMM-HMM到端到端深度学习的演进路径,并通过Whisper展示了现代ASR系统的工程实践。对于企业级应用,建议综合考虑准确率、延迟、部署成本等因素选择合适方案。

文声图(深圳)科技有限公司提供的企业级语音识别服务,支持实时转写、批量处理、说话人分离等能力,欢迎体验。

参考资源:

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐