본문 바로가기
딥러닝

인공지능 자동 음성 인식 모델 만들기(파이썬/딥러닝/허깅페이스/Wav2vec /STT/ASR )

by 오 복 이 2022. 10. 30.

자동 음성 인식(Automatic Speech Recognition:ASR), 음성을 텍스트로 변환(Speech-To-Text:STT) 작업에서 좋은 성능을 보이고 있는 Transformer 기반 음성 모델인 Wac2Vec을 학습 시켜 음성 인식 모델을 구축하는 방법을 공부했습니다.

Fine-tuning Wav2Vec2 for English ASR with 🤗 Transformers

Fine-tuning Wav2Vec2 for English ASR with Transformers를 한국어로 번역하고 공부한 notebook입니다. (Learning curve plot 추가)

Wav2vec 이란?

Wav2Vec2는 자동 음성 인식(ASR)을 위해 사전 훈련된 모델입니다.

September 2020에 Alexei Baevski, Michael Auli, and Alex Conneau 이 공개했습니다.

Wav2Vec2는 Using a novel contrastive pretraining objective를 사용하여 레이블이 없는 50,000 시간 이상의 음성에서 강력한 음성 표현을 학습합니다

BERT's masked language modeling과 유사하게 이 모델은 feature vector를 transformer network에 전달하기 전에 무작위로 마스킹하여 상황에 맞는 speech representation을 학습합니다.

Table 9 of the paper.

 

처음으로 사전 훈련 후 매우 적은 레이블이 지정된 음성 데이터에 대한 미세 조정만으로
최첨단 ASR 시스템의 state-of-the-art 달성 했습니다.

레이블이 지정된 10분 정도의 데이터를 사용하여 Wav2Vec2 는 LibriSpeech의 clean test set에서 5% 미만의 단어 오류율(WER)을 산출합니다.

이 notebook에서는 Wav2Vec2의 사전 훈련된 체크포인트가 모든 영어 ASR 데이터 세트에서 미세 조정되는 방법에 대해 자세히 설명합니다.

이 노트북에서는 언어 모델을 사용하지 않고 Wav2Vec2를 미세 조정합니다.

종단 간 ASR 시스템으로 언어 모델 없이 Wav2Vec2를 사용하는 것이 훨씬 간단하며
독립형 Wav2Vec2 음향 모델이 인상적인 결과를 달성하는 것으로 나타났습니다.
데모 목적으로 5h의 훈련 데이터를 포함하는 다소 작은 Timit 데이터 세트 에서 "base" 크기의 pretrained checkpoint를 미세 조정합니다.

Wav2Vec2 은 Connectionist Temporal Classification (CTC)를 사용하여 fine tuned 됩니다.

CTC는 sequence-to-sequence 문제와 주로 Automatic Speech Recognition 및 handwriting recognition에 대해 신경망을 훈련하는 데 사용되는 알고리즘입니다.

CTC 설명이 잘되어 있는 blog post : Sequence Modeling with CTC (2017)

 

Sequence Modeling with CTC

A visual guide to Connectionist Temporal Classification, an algorithm used to train deep neural networks in speech recognition, handwriting recognition and other sequence problems.

distill.pub

 

GPU 준비

colab에서 GPU 액세스 하기

Google Colab Pro를 사용하면 V100 또는 P100 GPU 사용 가능

gpu_info = !nvidia-smi
gpu_info = '\n'.join(gpu_info)
if gpu_info.find('failed') >= 0:
  print('Not connected to a GPU')
else:
  print(gpu_info)

실행 결과

 

패키지 설치하기

%%capture
!pip install datasets==1.18.3
!pip install transformers==4.11.3
!pip install librosa
!pip install jiwer

훈련 중에 훈련 체크포인트를 Hugging Face 🤗 Hub에 직접 업로드하는 것이 좋습니다. Hub는 버전 제어가 통합되어 있어 학습 중에 모델 체크포인트가 손실되지 않도록 할 수 있습니다.

이를 위해 Hugging Face 웹사이트에서 인증 토큰 받아 사용합니다.

아직 없다면 sign up here 을 통해 새롭게 받을 수 있습니다.

 

from huggingface_hub import notebook_login

notebook_login()

 

실행 결과

 

Git-LFS 설치 : model checkpoints 업로드

%%capture
!apt install git-lfs

 

Timit는 일반적으로 PER(phoneme error rate 음소 오류율)을 사용하여 평가합니다.

하지만 ASR에서 가장 일반적인 측정 기준은 WER(word error rate 단어 오류율)입니다.

일반적으로 WER을 많이 사용하기 때문에 여기서는 WER을 사용하여 모델을 평가합니다.

 

Prepare Data, Tokenizer, Feature Extractor

 

ASR 모델은 음성을 텍스트로 변환하기 때문에 feature extractor, tokenizer 둘 다 필요합니다

feature extractor는 speech signal를 모델의 입력 형식( 예: feature vector)으로 처리하는데 필요하고

tokenizer는 모델의 출력 형식을 텍스트로 처리하는데 필요합니다.

🤗 Transformers에서 Wav2Vec2 model은 tokenizer와 feature extractor를 제공합니다.

Tokenizer-Wav2Vec2CTCTokenizer

Feature extractor - Wav2Vec2FeatureExtractor.

먼저 모델의 예측 디코딩을 담당하는 토크나이저 Wav2Vec2CTCTokenizer 생성합니다.

Create Wav2Vec2CTCTokenizer

pretrained Wav2Vec2 checkpoint는 speech signal을 sequence of context representations에 매핑합니다.(위의 Fig)

fine-tuned Wav2Vec2 checkpoint는 이 sequence of context representations와 대응하는 *transcription가 매핑되고 선형 레이어가 transformer block (노란색으로 표시) 위에 추가되어야 합니다.

( *transcription(전사)란? 구어(spoken language)와 관련된 연구에서 녹음된 음성 데이터를 문자로 표현하는 작업)

이 선형 계층은 각 context representations을 token class로 분류하는 데 사용됩니다.

이는 사전 훈련 후 선형 계층이 추가 분류를 위해 BERT의 임베딩 위에 추가되는 방식과 유사합니다.

참조.blog post의 "BERT" 섹션. .

이 레이어의 출력 크기는 Wav2Vec2의 사전 훈련 작업에 의존하지 않고 미세 조정에 사용되는 레이블이 지정된 데이터 세트에만 의존하는 어휘의 토큰 수에 해당합니다.

따라서 첫 번째 단계에서 Timit를 살펴보고 데이터 세트의 전사를 기반으로 어휘를 정의합니다.

먼저 데이터 세트를 로드하고 구조를 살펴보겠습니다.

 

데이터세트를 로드하고 구조를 살펴보기

from datasets import load_dataset, load_metric

timit = load_dataset("timit_asr")

실행 결과

timit

실행 결과

많은 ASR 데이터 세트는 각 오디오 파일에 대해 대상 텍스트만 제공하지만

Timit은 'phonetic_detail' 등과 같은 각 오디오 파일에 대해 더 많은 정보를 제공합니다.

여기서는 전사된 텍스트 데이터만 미세 조정을 위해 사용합니다.

데이터 세트의 일부 랜덤 샘플을 출력하는 함수

#file', 'audio', 'text'를 제외하고 제거
timit = timit.remove_columns(["phonetic_detail", "word_detail", "dialect_region", "id", "sentence_type", "speaker_id"])

 

전사가 어떤 문장인지 확인

from datasets import ClassLabel
import random
import pandas as pd
from IPython.display import display, HTML

def show_random_elements(dataset, num_examples=10):
    assert num_examples <= len(dataset), "Can't pick more elements than there are in the dataset."
    picks = []
    for _ in range(num_examples):
        pick = random.randint(0, len(dataset)-1)
        while pick in picks:
            pick = random.randint(0, len(dataset)-1)
        picks.append(pick)
    
    df = pd.DataFrame(dataset[picks])
    display(HTML(df.to_html()))
    
    
show_random_elements(timit["train"].remove_columns(["audio", "file"]), num_examples=10)

실행 결과

내용이 대화보다는 서면 텍스트처럼 보이는데 Timit 이 read speech corpus이기 때문입니다.

전사에 ,.?!;:. 와 같은 특수 문자가 포함되어 있습니다.

언어 모델이 없으면 음성 청크를 이러한 특수 문자로 분류하는 것이 더 어렵습니다.

왜냐하면 특수 문자들은 특징적인 사운드 단위에 실제로 해당하지 않기 때문입니다.

ex) 문자 "s"는 다소 명확한 소리가 나는 반면 특수 문자 "."는 그렇지 않음

또한 음성 신호의 의미를 이해하기 위해 일반적으로 전사에 특수 문자를 포함할 필요가 없음.

--> 따라서 텍스트 특수문자를 제거하고 소문자로 정규화

전사에서 특수 문자를 제거하고 소문자로 변환

import re
chars_to_ignore_regex = '[\,\?\.\!\-\;\:\"]'
#전사를 특수문자를 제거하고 소문자로 변환하는 함수
def remove_special_characters(batch):
    batch["text"] = re.sub(chars_to_ignore_regex, '', batch["text"]).lower() + " "
    return batch
timit = timit.map(remove_special_characters)

실행 결과

 

#전처리된 전사 살펴보기
show_random_elements(timit["train"].remove_columns(["audio", "file"]))

실행 결과

 

CTC에서는 speech chunks를 문자로 분류하는 것이 일반적이므로 여기에서도 동일한 작업을 수행합니다.

훈련 및 테스트 데이터의 모든 고유한 문자를 추출하고 이 문자 집합에서 vocabulary를 구축합니다.

모든 전사를 하나의 긴 전사로 연결한 다음 문자열을 문자 집합으로 변환하는 매핑 함수를 사용합니다.

매핑 함수가 모든 전사에 한 번에 액세스 할 수 있도록 map(...) 함수에 인수 batched=True를 전달합니다.

vocabulary  구축

def extract_all_chars(batch):
  all_text = " ".join(batch["text"])
  vocab = list(set(all_text))
  return {"vocab": [vocab], "all_text": [all_text]}
vocabs = timit.map(extract_all_chars, batched=True, batch_size=-1, keep_in_memory=True, remove_columns=timit.column_names["train"])

실행 결과

vocab_list = list(set(vocabs["train"]["vocab"][0]) | set(vocabs["test"]["vocab"][0]))

vocab_dict = {v: k for k, v in enumerate(vocab_list)}
vocab_dict

실행 결과

알파벳의 모든 문자가 데이터 세트에서 발생한다는 것을 알 수 있습니다.

그리고 " "(띄어쓰기)와 특수 문자와 '도 추출되었습니다.

두 가지 이유로 이 특수문자는 제외하지 않습니다.

  • 모델은 단어가 끝나는 시점을 예측하는 방법을 학습해야 합니다. 그렇지 않으면 모델 예측은 항상 하나의 시퀀스 문자가 되어 단어를 서로 분리하는 것이 불가능합니다.
  • 영어에서는 ' 와 같이 매우 다른 의미를 가진 단어를 구별하기 위해 문자를 유지해야 합니다. ex ) "it's"와 "its"

띄어쓰기 " "자체 토큰 클래스가 있음을 더 명확하게 하기 위해 더 눈에 띄는 문자인 "|"로 치환합니다.

또한 모델이 나중에 Timit의 훈련 세트에서 만나지 못한 문자를 처리할 수 있도록 "unknown" 토큰도 추가합니다

마지막으로 CTC의 blank token"에 해당하는 패딩 토큰도 추가합니다.

blank token 은 CTC 알고리즘의 핵심 구성 요소입니다.

자세한 내용 "Alignment" section.

vocabulary에 토큰 추가

vocab_dict["|"] = vocab_dict[" "]
del vocab_dict[" "]
vocab_dict["[UNK]"] = len(vocab_dict)
vocab_dict["[PAD]"] = len(vocab_dict)
len(vocab_dict)

실행 결과

30

 

이 vacabulary는 30개의 토큰으로 구성됩니다.

즉, 사전 훈련된 Wav2Vec2 체크포인트 위에 추가할 선형 레이어의 출력 차원은 30입니다.

 

 

vacabulary를 json 파일로 저장

import json
with open('vocab.json', 'w') as vocab_file:
    json.dump(vocab_dict, vocab_file)

 

json 파일을 사용하여 Wav2Vec2CTCTokenizer클래스의 개체를 인스턴스화

from transformers import Wav2Vec2CTCTokenizer

tokenizer = Wav2Vec2CTCTokenizer("./vocab.json", unk_token="[UNK]", pad_token="[PAD]", word_delimiter_token="|")​

만든 토크나이저를 이 notebook의 미세 조정된 모델로 다시 사용하려면 tokenizer🤗 Hub에 업로드하는 것이 좋습니다.

파일을 업로드할 저장소를 호출하기 ex) "wav2vec2-large-xlsr-turkish-demo-colab".

repo_name = "wav2vec2-base-timit-demo-colab"

tokenizer를 🤗 Hub에 업로드

tokenizer.push_to_hub(repo_name)

실행 결과

맨 아래의 링크에서 방금 생성한 레포지토리를 볼 수 있습니다.

 

Create Wav2Vec2 Feature Extractor

음성은 연속적인 신호이며 컴퓨터에서 처리하려면 먼저 이산화 되어야 합니다. 이를 일반적으로 sampling 이라고 합니다.

sampling rate란 초당 측정되는 음성 신호의 데이터 포인트 수입니다.

따라서 더 높은 sampling rate로 샘플링하면 실제 음성 신호에 대한 근사치가 향상되지만 초당 더 많은 값이 필요합니다.

사전 훈련된 체크포인트는 입력 데이터가 훈련된 데이터와 동일한 분포에서 어느 정도 sampling 될 것으로 예상합니다.

2개의 상이한 rate로 sampling 된 동일한 음성 신호는 매우 상이한 분포를 가집니다 예를 들어 sampling rate를 2배로 하면 데이터 포인트의 길이가 2배가 됩니다.

따라서 ASR 모델의 사전 학습된 체크포인트를 미세 조정하기 전에

모델을 사전 학습하는데 사용된 데이터의 샘플링 속도가 모델을 미세 조정하는데 사용되는 데이터 세트의 샘플링 속도와 일치하는지 확인하는 것이 중요합니다.

Wav2Vec2는 16kHz로 샘플링된 LibriSpeech 및 LibriVox의 오디오 데이터에 대해 사전 학습되었습니다.

미세 조정 데이터 세트인 Timit 도 운 좋게도 16kHz로 샘플링되었습니다

미세 조정 데이터 세트가 16kHz보다 낮거나 높은 속도로 샘플링된 경우 먼저 사전 훈련에 사용된 데이터의 샘플링 속도와 일치하도록 음성 신호를 업 또는 다운 샘플링해야 했습니다.

Wav2Vec2 feature extractor 개체를 인스턴스 화하려면 다음 매개변수가 필요합니다.

  • feature_size: 음성 모델은 일련의 feature vectors를 입력으로 사용. 이 시퀀스의 길이는 분명히 다양하지만 feature vectors 크기는 그렇지 않아야 합니다. Wav2Vec2의 경우 모델이 raw speech signal에 대해 학습되었기 때문에 feature 크기는 1 입니다.
  • sampling_rate: 모델이 학습되는 샘플링 속도
  • padding_value: batched inference에서 더 짧은 입력은 특정 값으로 채워야함
  • do_normalize: input이 zero-mean-unit-variance 정규화 되어야하지 여부 일반적으로 speech model은 입력을 정규화할때 더 잘 수행됨
  • return_attention_mask: 모델이 일괄 추론을 사용해야 하는지 여부 . 일반적으로 모델은 패딩된 토큰을 마스크 하는 데 항상 사용해야 합니다 . 그러나 '기본' 체크포인트의 attention_mask매우 구체적인 설계 선택으로 인해 no 를 사용할 때 더 나은 결과를 얻을 수 있습니다. 다른 음성 모델에는 권장 하지 않습니다 . 자세한 내용은 이 문제를 참조하세요. 중요 이 노트북을 사용하여 large-lv60 을 미세 조정하려면 이 매개변수를 로 설정해야 합니다 .Wav2Vec2attention_maskTrue
  • return_attention_mask: 모델이 batched inference 할 때 attention_mask를 사용할지 여부. 일반적으로 모델은 패딩된 토큰을 마스크하기 위해 항상 Attention_mask를 사용해야 합니다. 그러나 Wav2Vec2의 "base" 체크포인트의 매우 구체적인 디자인 선택으로 인해 Attention_mask를 사용하지 않을 때 더 나은 결과를 얻을 수 있습니다. 다른 음성 모델에는 권장하지 않습니다 . 자세한 내용은 이 문제를 참조하세요. 중요 이 노트북을 사용하여 large-lv60 을 미세 조정하려면 이 매개변수를 True로 설정해야 합니다.

Wav2Vec2의  feature extraction pipeline 정의 

from transformers import Wav2Vec2FeatureExtractor

feature_extractor = Wav2Vec2FeatureExtractor(feature_size=1, sampling_rate=16000, padding_value=0.0, do_normalize=True, return_attention_mask=False)

 

Wav2Vec2를 최대한 사용자 친화적으로 사용하기 위해 feature extractor and tokenizer 는 하나의 Wav2Vec2 Processor 클래스로 래핑 되어processor 객체만 필요합니다.

Wav2Vec2의 Processor 정의 

from transformers import Wav2Vec2Processor

processor = Wav2Vec2Processor(feature_extractor=feature_extractor, tokenizer=tokenizer)

 

Preprocess Data

데이터셋 준비하기 지금까지 우리는 음성 신호의 실제 값을 보지 않고 전사만 보았습니다.

'text' 외에도 데이터 세트에는 두 개의 열 'file'과'audio'가 더 포함됩니다. 'file' 는 오디오 파일의 절대 경로를 나타냅니다.

file의 경로 확인

timit["train"][0]["file"]

실행 결과

Wav2Vec2은 16kHz의 1차원 배열 형식의 입력을 예상합니다.

즉, 오디오 파일을 로드하고 다시 샘플링해야 합니다.

datasets 다른 열 audio를 호출하여 이를 자동으로 수행합니다.

데이터의 audio열 확인

timit["train"][0]["audio"]

오디오 파일이 자동으로 로드되었음을 알 수 있습니다. 이 기능은 호출 시 즉석에서 오디오 파일을 로드하고 리샘플링합니다.

샘플링 속도는 Wav2Vec2입력으로 예상되는 16kHz로 설정됩니다.

데이터 세트를 더 잘 이해하고 오디오가 올바르게 로드되었는지 확인하기 위해 몇 가지 오디오 파일을 들어보겠습니다.

데이터의 오디오 파일 듣기

import IPython.display as ipd
import numpy as np
import random

rand_int = random.randint(0, len(timit["train"]))

print(timit["train"][rand_int]["text"])
ipd.Audio(data=np.asarray(timit["train"][rand_int]["audio"]["array"]), autoplay=True, rate=16000)

화자가 말하는 속도, 억양 등이 다른 것을 들을 수 있습니다.

전반적으로 녹음은 비교적 명확하게 들립니다. 이는 읽기 말뭉치에서 기대할 수 있는 것입니다.

음성 입력의 모양, 전사 및 해당 샘플링 속도를 인쇄하여 데이터가 올바르게 준비되었는지 최종 확인합니다.

 

데이터 확인

rand_int = random.randint(0, len(timit["train"]))

print("Target text:", timit["train"][rand_int]["text"])
print("Input array shape:", np.asarray(timit["train"][rand_int]["audio"]["array"]).shape)
print("Sampling rate:", timit["train"][rand_int]["audio"]["sampling_rate"])

 

실행 결과

Target text: the avalanche triggered a minor earthquake 
Input array shape: (39732,)
Sampling rate: 16000
 
 

데이터는 1차원 배열이고 샘플링 속도는 항상 16kHz에 해당하며 대상 텍스트는 정규화됩니다.

 

마지막으로 훈련을 위해 모델이 예상하는 형식으로 데이터 세트를 처리할 수 있습니다. 위의 코드가 우리는 map(...) 기능을 사용할 것입니다.

  1. batch["audio"]를 호출하여 오디오 데이터를 로드하고 resample
  2. 로드된 오디오 파일에서 input_values 추출. 여기서 Wav2Vec2Processor는 정규화를 하지만 다른 음성 모델의 경우 이 단계에서 Log-Mel 특징 추출 과 같은 더 복잡한 특징 추출이 포함될 수 있음
  3. label ids 을 지정하기 위해 전사를 인코딩

(공부 필요) Note: 이 mapping function는 Wav2Vec2Processor클래스를 사용하는 방법의 좋은 예입니다. "normal" 콘텍스트에서 processor(...)호출은 메서드 Wav2Vec2FeatureExtractor로 리디렉션 됩니다. 그러나 프로세서를 as_target_processorcontex로 래핑 할 때 동일한 메서드가 Wav2Vec2CTCTokenizer의 호출 메서드로 리디렉션 됩니다. 자세한 내용은 문서를 확인하십시오

자세한 내용 :docs.

데이터셋 준비 함수

def prepare_dataset(batch):
    audio = batch["audio"]

    # batched output is "un-batched" to ensure mapping is correct
    batch["input_values"] = processor(audio["array"], sampling_rate=audio["sampling_rate"]).input_values[0]
    batch["input_length"] = len(batch["input_values"])
    
    with processor.as_target_processor():
        batch["labels"] = processor(batch["text"]).input_ids
    return batch

모든 데이터 sample에 데이터셋 준비 함수 적용

timit = timit.map(prepare_dataset, remove_columns=timit.column_names["train"], num_proc=4)

 

Note: 현재 데이터 세트는 torchaudio 및 [librosa](https://librosa.org/ doc/latest/index.html) 오디오 로딩 및 리샘플링. 사용자 고유의 맞춤 데이터 로드/샘플링을 구현하려면 "path" 열을 대신 사용하고 "audio" 열은 무시해도 됩니다.

 

긴 입력 시퀀스에는 많은 메모리가 필요합니다. 'Wav2Vec2'는 self-attention를 기반으로 하기 때문에 긴 입력 시퀀스에 대한 입력 길이에 따라 메모리 요구 사항이 2차적으로 확장됩니다 cf.with this

이 데모에서는 훈련 데이터 세트에서 4초보다 긴 모든 시퀀스를 필터링해 보겠습니다.

긴 시퀀스 필터링

max_input_length_in_sec = 4.0
timit["train"] = timit["train"].filter(lambda x: x < max_input_length_in_sec * processor.feature_extractor.sampling_rate, input_columns=["input_length"])

실행 결과

학습할 준비 끝

 

Training & Evaluation

데이터가 처리되었고 훈련 파이프라인을 setting 합니다. 🤗의 Trainer를 사용할 것입니다.

  • Data collator를 정의합니다. 대부분의 NLP 모델과 달리 Wav2Vec2는 출력 길이보다 입력 길이가 훨씬 더 큽니다. 예: 입력 길이가 50000인 샘플의 출력 길이는 100 이하입니다. 큰 입력 크기가 주어지면 훈련 배치를 동적으로 채우는 것이 훨씬 더 효율적입니다. 즉, 모든 훈련 샘플은 전체 가장 긴 샘플이 아니라 배치에서 가장 긴 샘플에만 채워져야 합니다. 따라서 Wav2Vec2를 미세 조정하려면 아래에서 정의할 특별한 padding data collator가 필요합니다.
  • Evaluation metric 정의. 훈련하는 동안 모델은 단어 오류율로 평가되어야 합니다. 그에 따라 compute_metrics 함수를 정의해야 합니다.
  • 사전 훈련된 체크포인트를 로드합니다. 사전 훈련된 체크포인트를 로드하고 훈련을 위해 올바르게 구성해야 합니다.
  • training configuration을 정의합니다.

모델을 미세 조정한 후 테스트 데이터에서 모델을 올바르게 평가하고 실제로 음성을 올바르게 전사하는 방법을 학습했는지 확인합니다.

Set-up Trainer

data collator를 정의하는 것부터 시작하겠습니다.

data collator의 코드는 this example에서 복사

너무 많은 세부 사항에 들어가지 않고 일반적인 데이터 수집기와 달리 이 데이터 수집기는 input_values와 labels를 다르게 처리하므로 별도의 패딩 기능에 적용됩니다(다시 Wav2Vec2의context manager 사용). 이것은 음성에서 입력과 출력이 동일한 패딩 기능으로 처리되어서는 안 되는 서로 다른 양식이기 때문에 필요합니다. 일반적인 데이터 수집기와 유사하게 레이블의 패딩 토큰은 '-100'이므로 손실을 계산할 때 해당 토큰이 고려되지 않습니다.

data collator 정의

import torch

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

@dataclass
class DataCollatorCTCWithPadding:
    """
    Data collator that will dynamically pad the inputs received.
    Args:
        processor (:class:`~transformers.Wav2Vec2Processor`)
            The processor used for proccessing the data.
        padding (:obj:`bool`, :obj:`str` or :class:`~transformers.tokenization_utils_base.PaddingStrategy`, `optional`, defaults to :obj:`True`):
            Select a strategy to pad the returned sequences (according to the model's padding side and padding index)
            among:
            * :obj:`True` or :obj:`'longest'`: Pad to the longest sequence in the batch (or no padding if only a single
              sequence if provided).
            * :obj:`'max_length'`: Pad to a maximum length specified with the argument :obj:`max_length` or to the
              maximum acceptable input length for the model if that argument is not provided.
            * :obj:`False` or :obj:`'do_not_pad'` (default): No padding (i.e., can output a batch with sequences of
              different lengths).
        max_length (:obj:`int`, `optional`):
            Maximum length of the ``input_values`` of the returned list and optionally padding length (see above).
        max_length_labels (:obj:`int`, `optional`):
            Maximum length of the ``labels`` returned list and optionally padding length (see above).
        pad_to_multiple_of (:obj:`int`, `optional`):
            If set will pad the sequence to a multiple of the provided value.
            This is especially useful to enable the use of Tensor Cores on NVIDIA hardware with compute capability >=
            7.5 (Volta).
    """

    processor: Wav2Vec2Processor
    padding: Union[bool, str] = True
    max_length: Optional[int] = None
    max_length_labels: Optional[int] = None
    pad_to_multiple_of: Optional[int] = None
    pad_to_multiple_of_labels: Optional[int] = None

    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 lenghts and need
        # different padding methods
        input_features = [{"input_values": feature["input_values"]} for feature in features]
        label_features = [{"input_ids": feature["labels"]} for feature in features]

        batch = self.processor.pad(
            input_features,
            padding=self.padding,
            max_length=self.max_length,
            pad_to_multiple_of=self.pad_to_multiple_of,
            return_tensors="pt",
        )
        with self.processor.as_target_processor():
            labels_batch = self.processor.pad(
                label_features,
                padding=self.padding,
                max_length=self.max_length_labels,
                pad_to_multiple_of=self.pad_to_multiple_of_labels,
                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
data_collator = DataCollatorCTCWithPadding(processor=processor, padding=True)

다음으로 평가 메트릭이 정의됩니다. 앞서 언급했듯이, ASR에서 가장 많이 사용되는 메트릭은 WER(오류율)이라는 단어이므로 이 노트북에서도 사용할 것입니다.

평가 메트릭 정의

wer_metric = load_metric("wer")

실행 결과

 

모델은 sequence of logit vectors를 반환합니다:

logit vector y1 는 앞서 정의한 어휘의 각 단어에 대한 로그 확률을 포함하므로 len(yi)= config.vocab_size입니다.

우리는 모델의 가장 가능성 있는 예측에 관심이 있으므로 로짓의 argmax(...)를 취합니다. 또한 -100 을 pad_token_id로 바꾸고 ID를 디코딩하여 인코딩된 레이블을 원래 문자열로 다시 변환하는 동시에 연속적인 토큰이 CTC 스타일에서 동일한 토큰으로 그룹화되지 않도록 합니다.

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)
    # we do not want to group tokens when computing the metrics
    label_str = processor.batch_decode(pred.label_ids, group_tokens=False)

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

    return {"wer": wer}

이제 사전 훈련된 Wav2Vec2 체크포인트를 로드할 수 있습니다.

토크나이저의 pad_token_id는 모델의 pad_token_id 를 정의하거나 Wav2Vec2ForCTC의 경우 CTC의 blank token 도 정의해야 합니다.

GPU 메모리를 절약하기 위해 PyTorch의 gradient checkpointing를 활성화하고 손실 감소를 "mean"으로 설정합니다.

사전 훈련된 Wav2Vec2 체크포인트 불러오기

from transformers import Wav2Vec2ForCTC

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

 

Wav2Vec2의 첫 번째 구성 요소는 원시 음성 신호에서 음향적으로 의미 있지만 맥락적으로는 독립적인 특징을 추출하는 데 사용되는 CNN 레이어 스택으로 구성됩니다. 모델의 이 부분은 사전 훈련 중에 이미 충분히 훈련되었으며 paper에 명시된 바와 같이 더 이상 미세 조정할 필요가 없습니다. 따라서 feature 추출 부분의 모든 매개변수에 대해 requires_grad를 False로 설정할 수 있습니다.

 

 

마지막 단계에서 훈련과 관련된 모든 매개변수를 정의합니다. 일부 매개변수에 대한 자세한 설명은 다음과 같습니다.

  • group_by_length는 입력 길이가 유사한 훈련 샘플을 하나의 배치로 그룹화하여 훈련을 보다 효율적으로 만듭니다. 이것은 모델을 통해 전달되는 쓸모없는 패딩 토큰의 전체 수를 크게 줄여 학습 시간을 크게 단축할 수 있습니다.
  • learning_rate  weight_decay는 미세 조정이 안정될 때까지 경험적으로 조정되었습니다. 이러한 매개변수는 Timit 데이터 세트에 크게 의존하며 다른 음성 데이터 세트에는 차선책일 수 있습니다.

다른 매개변수에 대한 자세한 설명은 문서를 참조하세요.

훈련 중에 체크포인트는 400개의 훈련 단계마다 허브에 비동기식으로 업로드됩니다. 모델이 아직 훈련 중인 동안에도 데모 위젯을 가지고 놀 수 있습니다.

참고: 모델 체크포인트를 허브에 업로드하지 않으려면 push_to_hub=False로 설정하기만 하면 됩니다.

from transformers import TrainingArguments

training_args = TrainingArguments(
  output_dir=repo_name,
  group_by_length=True,
  per_device_train_batch_size=32,
  evaluation_strategy="steps",
  num_train_epochs=30,
  fp16=True,
  gradient_checkpointing=True,
  save_steps=500,
  eval_steps=500,
  logging_steps=500,
  learning_rate=1e-4,
  weight_decay=0.005,
  warmup_steps=1000,
  save_total_limit=2,
  push_to_hub=True,
)

이제 모든 인스턴스를 Trainer로 전달할 수 있으며 학습을 시작할 준비가 되었습니다!

 

from transformers import Trainer

trainer = Trainer(
    model=model,
    data_collator=data_collator,
    args=training_args,
    compute_metrics=compute_metrics,
    train_dataset=timit["train"],
    eval_dataset=timit["test"],
    tokenizer=processor.feature_extractor,
)

 

모델이 스피커 비율과 독립적이 되도록 하기 위해 CTC에서는 동일한 연속 토큰을 단일 토큰으로 그룹화합니다. 그러나 인코딩된 레이블은 모델의 예측 토큰과 일치하지 않으므로 디코딩할 때 그룹화하지 않아야 합니다. 이것이 group_tokens=False 매개변수를 전달해야 하는 이유입니다. 이 매개변수를 전달하지 않으면 "hello"와 같은 단어가 잘못 인코딩되고 "helo"로 디코딩됩니다.

blank token 사용하면 모델이 두 개의 l 사이에 빈 토큰을 삽입하도록 강제하여 "hello"와 같은 단어를 예측할 수 있습니다. 우리 모델의 "hello"에 대한 CTC 준수 예측은 [PAD] [PAD] "h" "e" "e" "l" "l" [PAD] "l" "o" "o" [PAD].

 

Training

학습은 이 노트북에 할당된 GPU에 따라 90~270분 정도 소요됩니다.

훈련된 모델은 Timit의 테스트 데이터에서 만족스러운 결과를 산출하지만 최적으로 미세 조정된 모델은 결코 아닙니다. 이 노트북의 목적은 Wav2Vec2 base, large, large-lv60에서 체크포인트를 미세 조정할 수 있는 방법을 보여주는 것입니다.

이 google colab을 사용하여 모델을 미세 조정하려는 경우 비활성으로 인해 학습이 중지되지 않는지 확인해야 합니다. 이를 방지하기 위한 간단한 해킹은 이 탭의 콘솔에 다음 코드를 붙여 넣는 것입니다(right mouse click -> inspect -> Console tab and insert code)

function ConnectButton(){
    console.log("Connect pushed"); 
    document.querySelector("#top-toolbar > colab-connect-button").shadowRoot.querySelector("#connect").click() 
}
setInterval(ConnectButton,60000);

Google colab에 할당된 GPU에 따라 여기에서 "메모리 부족" 오류가 표시될 수 있습니다. 이 경우 per_device_train_batch_size를 16 이하로 줄이고 결국 gradient_accumulation을 사용하는 것이 가장 좋습니다

trainer.train()

실행 결과

최종 WER은 약 0.3이어야 하며, 이는 최첨단 음소 오류율(PER)이 0.1 미만이라는 점을 감안할 때 합리적입니다(leaderboard. -timit) WER은 일반적으로 PER보다 나쁩니다.

이제 교육 결과를 Hub에 업로드할 수 있습니다. 다음 명령을 실행하기만 하면 됩니다.

trainer.push_to_hub()

실행 결과

이제 이 모델을 모든 친구, 가족, 좋아하는 애완동물과 공유할 수 있습니다. 그들은 모두 "your-username/the-name-you-picked" 식별자로 로드할 수 있습니다. 예를 들면 다음과 같습니다.

from transformers import AutoModelForCTC, Wav2Vec2Processor

model = AutoModelForCTC.from_pretrained("patrickvonplaten/wav2vec2-base-timit-demo-colab")
processor = Wav2Vec2Processor.from_pretrained("patrickvonplaten/wav2vec2-base-timit-demo-colab")

Evaluate

마지막 부분에서는 모델이 얼마나 잘 작동하는지 확인하기 위해 일부 validation data에 대해 모델을 실행합니다.

processor 와 model을 로드해 보겠습니다.

processor = Wav2Vec2Processor.from_pretrained("obokkkk/wav2vec2-base-timit-demo-colab")
model = Wav2Vec2ForCTC.from_pretrained("obokkkk/wav2vec2-base-timit-demo-colab")

이제 'map(...)' 함수를 사용하여 모든 테스트 샘플의 전사를 예측하고 데이터 세트 자체에 예측을 저장합니다. 결과 사전을 "results"라고 부를 것입니다.

참고: 이 issue로 인해 의도적으로 batch_size=1로 테스트 데이터 세트를 평가합니다. 패딩된 입력은 패딩 되지 않은 입력과 정확히 동일한 출력을 생성하지 않으므로 입력을 전혀 패딩 하지 않음으로써 더 나은 WER을 얻을 수 있습니다.

 

def map_to_result(batch):
  with torch.no_grad():
    input_values = torch.tensor(batch["input_values"]).unsqueeze(0)
    logits = model(input_values).logits

  pred_ids = torch.argmax(logits, dim=-1)
  batch["pred_str"] = processor.batch_decode(pred_ids)[0]
  batch["text"] = processor.decode(batch["labels"], group_tokens=False)
  
  return batch
results = timit["test"].map(map_to_result, remove_columns=timit["test"].column_names)
print("Test WER: {:.3f}".format(wer_metric.compute(predictions=results["pred_str"], references=results["text"])))

모델에서 어떤 오류가 발생하는지 보기 위해 몇 가지 예측을 살펴보겠습니다.

show_random_elements(results)

실행 결과

예측된 전사가 정답 전사와 음향적으로 매우 유사하지만 종종 철자나 문법 오류가 포함되어 있음이 분명해집니다.

우리가 언어 모델을 사용하지 않고 순수하게 Wav2Vec2에 의존한다는 점을 감안할 때 적절한 결과입니다.

 

model.to("cuda")

with torch.no_grad():
  logits = model(torch.tensor(timit["test"][:1]["input_values"], device="cuda")).logits

pred_ids = torch.argmax(logits, dim=-1)

# convert ids to tokens
" ".join(processor.tokenizer.convert_ids_to_tokens(pred_ids[0].tolist()))
 

출력은 CTC가 실제로 어떻게 작동하는지 좀 더 명확하게 해야 합니다. 모델은 분류할 음성 청크가 여전히 동일한 토큰에 해당하는 경우 동일한 토큰을 반복하는 방법을 배웠기 때문에 말하기 속도에 어느 정도 불변합니다.

이는 음성 파일의 전사가 길이와 거의 무관한 경우가 많기 때문에 CTC를 음성 인식을 위한 매우 강력한 알고리즘으로 만듭니다.

독자들에게 CTC를 더 잘 이해하기 위해 this 아주 멋진 블로그 게시물을 살펴보기를 다시 권합니다.

728x90
반응형