자동 음성 인식

자동 음성 인식 (Automatic speech recognition)

자동 음성 인식(ASR)은 음성 신호를 텍스트로 변환하는 작업이에요. 오디오 입력의 시퀀스를 텍스트 출력으로 매핑하죠. Siri나 Alexa 같은 가상 비서가 매일 ASR 모델을 써서 사용자를 돕고 있고, 실시간 자막이나 회의 중 메모 기록처럼 많은 유용한 사용자 대상 응용이 있어요.

이 가이드에서 보여 줄 내용은 이렇습니다:

  1. MInDS-14 데이터셋으로 Wav2Vec2를 파인튜닝해 오디오를 텍스트로 변환합니다.
  2. 파인튜닝한 모델로 추론(inference)을 수행합니다.

이 작업과 호환되는 모든 아키텍처와 체크포인트를 보려면 task-page를 확인해 보세요.

시작하기 전에 필요한 라이브러리가 모두 설치돼 있는지 확인하세요:

pip install transformers datasets evaluate jiwer soundfile librosa torchcodec

모델을 커뮤니티와 공유하고 업로드할 수 있도록 Hugging Face 계정에 로그인하길 권장해요. 요청이 오면 토큰을 입력해 로그인합니다:

>>> from huggingface_hub import notebook_login

>>> notebook_login()

MInDS-14 데이터셋 불러오기

🤗 Datasets 라이브러리에서 MInDS-14 데이터셋의 더 작은 하위 집합부터 불러와 볼게요. 전체 데이터셋으로 훈련 시간을 더 쓰기 전에, 작은 집합으로 실험해서 모든 게 잘 동작하는지 확인할 기회를 갖는 거예요.

>>> from datasets import load_dataset, Audio

>>> minds = load_dataset("PolyAI/minds14", name="en-US", split="train[:100]")

데이터셋의 train 스플릿을 [~Dataset.train_test_split] 메서드로 훈련·테스트 세트로 나눕니다:

>>> minds = minds.train_test_split(test_size=0.2)

그다음 데이터셋을 살펴볼게요:

>>> minds
DatasetDict({
    train: Dataset({
        features: ['path', 'audio', 'transcription', 'english_transcription', 'intent_class', 'lang_id'],
        num_rows: 16
    })
    test: Dataset({
        features: ['path', 'audio', 'transcription', 'english_transcription', 'intent_class', 'lang_id'],
        num_rows: 4
    })
})

데이터셋에는 lang_idenglish_transcription처럼 유용한 정보가 많지만, 이 가이드는 audiotranscription에 집중해요. [~datasets.Dataset.remove_columns] 메서드로 다른 컬럼을 제거합니다:

>>> minds = minds.remove_columns(["english_transcription", "intent_class", "lang_id"])

예시를 다시 확인해 볼게요:

>>> minds["train"][0]
{'audio': {'array': array([-0.00024414,  0.        ,  0.        , ...,  0.00024414,
          0.00024414,  0.00024414], dtype=float32),
  'path': '/root/.cache/huggingface/datasets/downloads/extracted/f14948e0e84be638dd7943ac36518a4cf3324e8b7aa331c5ab11541518e9368c/en-US~APP_ERROR/602ba9e2963e11ccd901cd4f.wav',
  'sampling_rate': 8000},
 'path': '/root/.cache/huggingface/datasets/downloads/extracted/f14948e0e84be638dd7943ac36518a4cf3324e8b7aa331c5ab11541518e9368c/en-US~APP_ERROR/602ba9e2963e11ccd901cd4f.wav',
 'transcription': "hi I'm trying to use the banking app on my phone and currently my checking and savings account balance is not refreshing"}

두 개의 필드가 있어요:

  • audio: 오디오 파일을 로드하고 리샘플링하기 위해 호출해야 하는 1차원 array의 음성 신호.
  • transcription: 타깃 텍스트.

전처리

다음 단계는 오디오 신호를 처리할 Wav2Vec2 프로세서를 로드하는 거예요:

>>> from transformers import AutoProcessor

>>> processor = AutoProcessor.from_pretrained("facebook/wav2vec2-base")

MInDS-14 데이터셋은 8000Hz 샘플링 레이트를 가져요 (이 정보는 dataset card에서 찾을 수 있어요). 즉 사전 훈련된 Wav2Vec2 모델을 쓰려면 데이터셋을 16000Hz로 리샘플링해야 해요:

>>> minds = minds.cast_column("audio", Audio(sampling_rate=16_000))
>>> minds["train"][0]
{'audio': {'array': array([-2.38064706e-04, -1.58618059e-04, -5.43987835e-06, ...,
          2.78103951e-04,  2.38446111e-04,  1.18740834e-04], dtype=float32),
  'path': '/root/.cache/huggingface/datasets/downloads/extracted/f14948e0e84be638dd7943ac36518a4cf3324e8b7aa331c5ab11541518e9368c/en-US~APP_ERROR/602ba9e2963e11ccd901cd4f.wav',
  'sampling_rate': 16000},
 'path': '/root/.cache/huggingface/datasets/downloads/extracted/f14948e0e84be638dd7943ac36518a4cf3324e8b7aa331c5ab11541518e9368c/en-US~APP_ERROR/602ba9e2963e11ccd901cd4f.wav',
 'transcription': "hi I'm trying to use the banking app on my phone and currently my checking and savings account balance is not refreshing"}

transcription에서 보듯이, 텍스트에 대문자와 소문자가 섞여 있어요. Wav2Vec2 토크나이저는 대문자로만 훈련됐기 때문에, 텍스트가 토크나이저의 어휘와 일치하도록 맞춰 줘야 합니다:

>>> def uppercase(example):
...     return {"transcription": example["transcription"].upper()}


>>> minds = minds.map(uppercase)

이제 다음과 같은 전처리 함수를 만듭니다:

  1. audio 컬럼을 호출해 오디오 파일을 로드하고 리샘플링합니다.
  2. 오디오 파일에서 input_values를 추출하고, 프로세서로 transcription 컬럼을 토크나이즈합니다.
>>> def prepare_dataset(batch):
...     audio = batch["audio"]
...     batch = processor(audio["array"], sampling_rate=audio["sampling_rate"], text=batch["transcription"])
...     batch["input_length"] = len(batch["input_values"][0])
...     return batch

전체 데이터셋에 전처리 함수를 적용하려면 🤗 Datasets의 [~datasets.Dataset.map] 함수를 쓰세요. num_proc 파라미터로 프로세스 수를 늘리면 map을 가속화할 수 있어요. [~datasets.Dataset.remove_columns] 메서드로 필요 없는 컬럼을 제거합니다:

>>> encoded_minds = minds.map(prepare_dataset, remove_columns=minds.column_names["train"], num_proc=4)

🤗 Transformers에는 ASR용 데이터 콜레이터가 없어서, [DataCollatorWithPadding]을 적응시켜 예시 배치를 만들어야 해요. 이렇게 하면 텍스트와 라벨을 (전체 데이터셋이 아니라) 배치에서 가장 긴 요소의 길이로 동적으로 패딩해 균일한 길이로 맞춰 줍니다. tokenizer 함수에서 padding=True로 설정해 텍스트를 패딩하는 것도 가능하지만, 동적 패딩이 더 효율적이에요.

다른 데이터 콜레이터와 달리, 이 특정 데이터 콜레이터는 input_valueslabels에 다른 패딩 방법을 적용해야 해요:

>>> import torch

>>> from dataclasses import dataclass, field
>>> from typing import Any, Dict, List, Optional, Union


>>> @dataclass
... class DataCollatorCTCWithPadding:
...     processor: AutoProcessor
...     padding: Union[bool, str] = "longest"

...     def __call__(self, features: list[dict[str, Union[list[int], torch.Tensor]]]) -> dict[str, torch.Tensor]:
...         # split inputs and labels since they have to be of different lengths and need
...         # different padding methods
...         input_features = [{"input_values": feature["input_values"][0]} for feature in features]
...         label_features = [{"input_ids": feature["labels"]} for feature in features]

...         batch = self.processor.pad(input_features, padding=self.padding, return_tensors="pt")

...         labels_batch = self.processor.pad(labels=label_features, padding=self.padding, return_tensors="pt")

...         # replace padding with -100 to ignore loss correctly
...         labels = labels_batch["input_ids"].masked_fill(labels_batch.attention_mask.ne(1), -100)

...         batch["labels"] = labels

...         return batch

이제 DataCollatorCTCWithPadding을 인스턴스화합니다:

>>> data_collator = DataCollatorCTCWithPadding(processor=processor, padding="longest")

평가

훈련 중에 지표를 포함하면 모델 성능을 평가하는 데 자주 도움이 돼요. 🤗 Evaluate 라이브러리로 평가 방법을 빠르게 불러올 수 있습니다. 이 작업에서는 word error rate (WER) 지표를 로드합니다 (지표를 로드하고 계산하는 방법을 더 배우려면 🤗 Evaluate quick tour를 확인해 보세요):

>>> import evaluate

>>> wer = evaluate.load("wer")

그다음 예측과 라벨을 [~evaluate.EvaluationModule.compute]에 넘겨 WER을 계산하는 함수를 만듭니다:

>>> import numpy as np


>>> def compute_metrics(pred):
...     pred_logits = pred.predictions
...     pred_ids = np.argmax(pred_logits, axis=-1)

...     pred.label_ids[pred.label_ids == -100] = processor.tokenizer.pad_token_id

...     pred_str = processor.batch_decode(pred_ids)
...     label_str = processor.batch_decode(pred.label_ids, group_tokens=False)

...     wer_score = wer.compute(predictions=pred_str, references=label_str)

...     return {"wer": wer_score}

compute_metrics 함수가 준비됐으니, 훈련을 설정할 때 다시 쓰게 될 거예요.

훈련

[Trainer]로 모델을 파인튜닝하는 방법이 익숙하지 않다면 여기의 기본 튜토리얼을 먼저 확인해 보세요!

이제 모델 훈련을 시작할 준비가 됐어요! [AutoModelForCTC]로 Wav2Vec2를 로드합니다. ctc_loss_reduction 파라미터로 적용할 reduction을 지정해 주세요. 기본 합산(sum)보다 평균을 쓰는 게 보통 더 좋아요:

>>> from transformers import AutoModelForCTC, TrainingArguments, Trainer

>>> model = AutoModelForCTC.from_pretrained(
...     "facebook/wav2vec2-base",
...     ctc_loss_reduction="mean",
...     pad_token_id=processor.tokenizer.pad_token_id,
... )

이 시점에 남은 단계는 세 개뿐이에요:

  1. [TrainingArguments]에 훈련 하이퍼파라미터를 정의합니다. 유일한 필수 파라미터는 모델을 저장할 위치를 정하는 output_dir이에요. push_to_hub=True로 설정하면 이 모델을 Hub에 push합니다 (모델을 업로드하려면 Hugging Face에 로그인해야 해요). [Trainer]는 각 에폭이 끝날 때 WER을 평가하고 훈련 체크포인트를 저장해요.
  2. 훈련 인자를 모델, 데이터셋, 토크나이저, 데이터 콜레이터, compute_metrics 함수와 함께 [Trainer]로 넘깁니다.
  3. [~Trainer.train]을 호출해 모델을 파인튜닝합니다.
>>> training_args = TrainingArguments(
...     output_dir="my_awesome_asr_mind_model",
...     per_device_train_batch_size=8,
...     gradient_accumulation_steps=2,
...     learning_rate=1e-5,
...     warmup_steps=500,
...     max_steps=2000,
...     gradient_checkpointing=True,
...     fp16=True,
...     train_sampling_strategy="group_by_length",
...     eval_strategy="steps",
...     per_device_eval_batch_size=8,
...     save_steps=1000,
...     eval_steps=1000,
...     logging_steps=25,
...     load_best_model_at_end=True,
...     metric_for_best_model="wer",
...     greater_is_better=False,
...     push_to_hub=True,
...     report_to="trackio",
... )

>>> trainer = Trainer(
...     model=model,
...     args=training_args,
...     train_dataset=encoded_minds["train"],
...     eval_dataset=encoded_minds["test"],
...     processing_class=processor,
...     data_collator=data_collator,
...     compute_metrics=compute_metrics,
... )

>>> trainer.train()

훈련이 끝나면 [~transformers.Trainer.push_to_hub] 메서드로 모델을 Hub에 공유해 모두가 접근할 수 있게 하세요:

>>> trainer.push_to_hub()

자동 음성 인식용 모델을 파인튜닝하는 더 자세한 예시는 영어 ASR용 이 블로그 post와 다국어 ASR용 이 post를 확인해 보세요.

추론

좋아요, 이제 모델을 파인튜닝했으니 추론에 쓸 수 있어요!

추론을 실행할 오디오 파일을 불러옵니다. 필요하면 오디오 파일의 샘플링 레이트를 모델의 샘플링 레이트에 맞게 리샘플링하는 걸 기억하세요!

>>> from datasets import load_dataset, Audio

>>> dataset = load_dataset("PolyAI/minds14", "en-US", split="train")
>>> dataset = dataset.cast_column("audio", Audio(sampling_rate=16000))
>>> sampling_rate = dataset.features["audio"].sampling_rate
>>> audio_file = dataset[0]["audio"]["path"]

파인튜닝한 모델을 추론에 시험해 보는 가장 간단한 방법은 [pipeline]에 쓰는 거예요. 모델로 자동 음성 인식용 pipeline을 만들고 오디오 파일을 넘겨 보세요:

>>> from transformers import pipeline

>>> transcriber = pipeline("automatic-speech-recognition", model="stevhliu/my_awesome_asr_minds_model")
>>> transcriber(audio_file)
{'text': 'I WOUD LIKE O SET UP JOINT ACOUNT WTH Y PARTNER'}

전사(transcription) 결과가 괜찮지만 더 좋을 수 있어요! 더 많은 예시로 모델을 파인튜닝하면 더 나은 결과를 얻을 수 있습니다!

원한다면 pipeline의 결과를 수동으로 재현할 수도 있어요:

오디오 파일과 전사를 전처리하고 input을 PyTorch 텐서로 반환할 프로세서를 로드합니다:

>>> from transformers import AutoProcessor

>>> processor = AutoProcessor.from_pretrained("stevhliu/my_awesome_asr_mind_model")
>>> inputs = processor(dataset[0]["audio"]["array"], sampling_rate=sampling_rate, return_tensors="pt")

입력을 모델에 넘기고 로짓을 얻습니다:

>>> from transformers import AutoModelForCTC

>>> model = AutoModelForCTC.from_pretrained("stevhliu/my_awesome_asr_mind_model")
>>> with torch.no_grad():
...     logits = model(**inputs).logits

가장 높은 확률의 예측 input_ids를 얻고, 프로세서로 예측 input_ids를 텍스트로 다시 디코딩합니다:

>>> import torch

>>> predicted_ids = torch.argmax(logits, dim=-1)
>>> transcription = processor.batch_decode(predicted_ids)
>>> transcription
['I WOUL LIKE O SET UP JOINT ACOUNT WTH Y PARTNER']

더 알아보기 (Learn more)