기록하는삶

[파이썬/Python] 한국어 STT, kospeech 활용기(1) _ 글자 사전 및 transcripts.txt 생성하기 본문

AI/kospeech(한국어 STT)

[파이썬/Python] 한국어 STT, kospeech 활용기(1) _ 글자 사전 및 transcripts.txt 생성하기

mingchin 2021. 12. 20. 23:05
728x90
반응형

오늘부터 몇 개로 나누어 작성할 글은 kospeech가 제공하는 모델 중 deepspeech2 기반 & 3가지 방법 중 character unit의 전처리를 가지고 진행했던 프로젝트를 복기하고 정리하는 글이다. 혹여나 나의 글이 참고가 될 분들을 위해, 그리고 나 스스로의 복습을 위해 최대한 꼼꼼히 다루어보려고 한다. kospeech는 아주 훌륭한 오픈 소스임에는 틀림없지만, 생각보다 많은 디버깅이 필요했기 때문에 분명 사용하려는 경우에 공통분모가 있을 것이라 생각한다.

 

0) 환경, 기술 스택

나는 Window 10에서 anaconda prompt를 활용했고, 파이썬 3.8 기반의 가상환경에서 프로젝트를 진행했다. gpu를 가지고 있지 않아 로컬 환경에서 모델 학습이 가능한 것을 확인한 후 실제 학습의 경우 월 12000원인 colab pro를 팀원들과 결제해 사용하였다.

 

1) 전처리 & 학습 준비

결과적으로 올바른 길을 찾긴 했지만, 나는 많은 길을 돌아돌아 맨 첫 관문에 도착했는데, 그건 바로 전처리 및 단어 사전 준비, 그리고 전사자료의 벡터화이다. 일단 기본적으로 음성 인식 알고리즘을 만들기 위해서는 음성 데이터(wav, pcm 등)와 전사 데이터(해당 음성이 무엇을 말하는지 그 내용을 사람이 적어놓은 문자열)가 필요하다. 보통 전사 데이터의 경우 해당 데이터가 만들어진 전사 규칙에 따라 한글 외에도 '+, un/, sn/' 등등의 전사 기호들을 포함할텐데, 목적에 맞게 필요하다면 해당 전사 기호나 문장 부호들을 삭제해야한다.

def rule(x):
    # 괄호
    a = re.compile(r'\([^)]*\)')
    # 문장 부호
    b = re.compile('[^가-힣 ]')
    x = re.sub(pattern=a, repl='', string= x)
    x = re.sub(pattern=b, repl='', string= x)
    return x

나는 나의 목적에 맞게 한글과 띄어쓰기를 제외한 모든 것들을 삭제한 label을 생성해주었다. 이는 최종적으로 음성을 통해 예측하고자 하는 문장에 구성과 일치하며, 이것을 활용해 단어 사전과 단어 사전을 바탕으로한 정답 벡터(문장으로 복원되기 직전의 예측값)를 만들어낼 것이다.

 

이러한 전처리 및 단어 사전 만들기와 관련된 파일은 모두 'kospeech-latest/dataset' 안에 포함돼있다. 나는 kspon 데이터로 학습된 과정만 계속 따라가며 프로젝트를 진행했다. 'kospeech-latest/dataset/kspon'에 readme를 읽어보면 어떤 데이터를 사용했는지, 전처리는 어떤식으로 진행하면 되는지가 설명이 꽤 자세히 되어있다. 결국 알아야하는 내용은 두 가지 정도다.

 

① requirement.txt를 실행해 필요한 모듈을 다운 받는다.

② 상황에 맞게 적절한 argument 들을 포함해 main.py를 실행한다.

 

conda create -n kospeech python==3.8

먼저 파이썬 3.8의 가상환경을 준비한다. 그 다음 'kospeech-latest/dataset/kspon' 폴더로 이동 후에

pip install -r requirements.txt

를 입력해 필요한 모듈을 다운받으면,

빨갛게 물든 cmd 창을 만날 수 있다. 괜찮다 가볍게 무시해주자, 앞으로 자주 일어날 일이다. 원인은 모르겠지만, 여기서도 학습에 필요한 모듈을 다운받을 때에도 원작자가 제공한 파일에서 버그가 발생하는 것 같은데 별 문제는 발생하지 않았다. 다시 kspon 폴더 안에 preprocess.sh 파일 안을 살펴보면 main.py에 던져줘야하는 args들이 나오는데 다음과 같다.

저 친구들의 역할은 main.py와 그 안에서 실행되는 preprocess.py, 그리고 character.py(다른 전처리를 택한다면 graphme.py 혹은 subword.py - 차이에 대해서는 첫 글에 있는 개발자 영상 참고)를 살펴보면 자세히 알 수 있다. 간략하게 정리해보면 아래와 같다.

--dataset_path: 오디오 파일을 포함하는 폴더의 경로

--vocab_dest: 전처리의 단어 사전의 저장 경로

--output_unit: 택할 전처리 방법(필자는 character unit _ 글자 단위 _ 선택)

--preprocess_mode: phonetic인지 spelling인지 원하는 것 선택 -> 칠 십 퍼센트 or 70% (필자는 phonetic)

--vocab_size: 단어 사전의 크기, 미입력시 5000

 

main.py가 정상 실행되면 만들어지는 산출물은 2개다.

① 한글 전사자료가 포함하는 글자로 만들어지는 단어 사전

② ①의 단어 사전을 이용한 벡터화된 전사자료를 포함하는 transcript.txt - 학습에 필요함

("audio_path + 탭 + korean transcript + 탭 + (벡터화된) transcript"의 구조) 

 

preprocess.py를 포함해 여기에서 실행되는 코드들이 왜 그렇게 짜여졌는지를 이해하기 위해서는 원작자의 유튜브 영상을 참고해 kspon 데이터가 원래 어떤 구조로 주어졌는지에 대한 이해가 필요하다. 내가 이해한 바로는 kospeech가 활용했던 kspon 데이터는 원래 다음과 같은 구조다.

/오디오1(폴더)
ㄴ 오디오1.pcm
ㄴ 오디오1.txt

/오디오2(폴더)
ㄴ 오디오2.pcm
ㄴ 오디오2.txt

이런 식으로 하나의 음성 파일과 해당 음성 파일에 대한 전사 자료가 한 폴더 안에 비슷한 이름으로 존재하는 상황이고, 해당 전사 자료가 '%'를 어떻게 읽었는지가 .txt파일의 제목에 무언가 표현이 되어있는 형태였던 것 같다.('10%'는 누군가에게는 '십 프로', 누군가에게는 '십 퍼센트') 그래서 preprocess.py 라는 파일에 아래와 같은 코드가 등장한다.

 

def preprocess(dataset_path, mode='phonetic'):
    print('preprocess started..')

    audio_paths = list()
    transcripts = list()

    percent_files = {
        '087797': '퍼센트',
        '215401': '퍼센트',
        '284574': '퍼센트',
        '397184': '퍼센트',
        '501006': '프로',
        '502173': '프로',
        '542363': '프로',
        '581483': '퍼센트'
    }

    for folder in os.listdir(dataset_path):
        # folder : {KsponSpeech_01, ..., KsponSpeech_05}
        if not folder.startswith('KsponSpeech'):
            continue
        path = os.path.join(dataset_path, folder)
        for idx, subfolder in enumerate(os.listdir(path)):
            path = os.path.join(dataset_path, folder, subfolder)

            for jdx, file in enumerate(os.listdir(path)):
                if file.endswith('.txt'):
                    with open(os.path.join(path, file), "r", encoding='cp949') as f:
                        raw_sentence = f.read()
                        if file[12:18] in percent_files.keys():
                            new_sentence = sentence_filter(raw_sentence, mode, percent_files[file[12:18]])
                        else:
                            new_sentence = sentence_filter(raw_sentence, mode=mode)

                    audio_paths.append(os.path.join(folder, subfolder, file))
                    transcripts.append(new_sentence)

                else:
                    continue

    return audio_paths, transcripts

.txt 파일의 제목에 특정 부분([12:18])에 해당 숫자가 등장하면 전사 자료의 일부를 변환하며 그것을 리스트에 담아 반환하고, 해당 두 개의 리스트(audio_paths와 transcripts)는 transcript.txt를 만드는데 활용된다. 이 부분은 각자 보유하고 있는 데이터에 맞게, 변형이 필요한 부분이다.

필자의 경우 앞서 정의한 rule() 함수를 이용해 전처리한 문장을 train.txt라는 파일에 'audio_path + 탭 + korean transcript'의 구조로 이미 저장해 둔 상태였기에, 아래와 같이 바꾸어 사용하였다. (상황이 다르다면 이 역시 각자의 상황에 맞게 변형이 필요하다.)

def preprocess(dataset_path, mode='phonetic'):
    print('preprocess started..')

    audio_paths = list()
    transcripts = list()

    with open("train.txt") as f:
        for idx, line in enumerate(f.readlines()):
            audio_path, transcript = line.split('\t')
            transcript = transcript.replace('\n', '')

            audio_paths.append(audio_path)
            transcripts.append(transcript)
        print("성공")

    return audio_paths, transcripts

또한 character.py에 다음과 같은 부분이 있다.

def generate_character_labels(transcripts, labels_dest):
    print('create_char_labels started..')

    label_list = list()
    label_freq = list()

    for transcript in transcripts:
        for ch in transcript:
            if ch not in label_list:
                label_list.append(ch)
                label_freq.append(1)
            else:
                label_freq[label_list.index(ch)] += 1

    # sort together Using zip
    label_freq, label_list = zip(*sorted(zip(label_freq, label_list), reverse=True))
    label = {'id': [0, 1, 2], 'char': ['<pad>', '<sos>', '<eos>'], 'freq': [0, 0, 0]}

    for idx, (ch, freq) in enumerate(zip(label_list, label_freq)):
        label['id'].append(idx + 3)
        label['char'].append(ch)
        label['freq'].append(freq)

    label['id'] = label['id'][:2000]
    label['char'] = label['char'][:2000]
    label['freq'] = label['freq'][:2000]

    label_df = pd.DataFrame(label)
    label_df.to_csv(os.path.join(labels_dest, "aihub_labels.csv"), encoding="utf-8", index=False)


def generate_character_script(audio_paths, transcripts, labels_dest):
    print('create_script started..')
    char2id, id2char = load_label(os.path.join(labels_dest, "aihub_labels.csv"))

    with open(os.path.join("transcripts.txt"), "w") as f:
        for audio_path, transcript in zip(audio_paths, transcripts):
            char_id_transcript = sentence_to_target(transcript, char2id)
            audio_path = audio_path.replace('txt', 'pcm')
            f.write(f'{audio_path}\t{transcript}\t{char_id_transcript}\n')

각각 글자 사전과 그것을 바탕으로 한 transcript를 저장하는 부분인데, 이때 글자 사전의 이름 부분은 자유롭게 변경하고 중간 부분의 2000이라는 숫자를 각자 상황에 맞게 변경해야한다. 필자의 경우 전체 글자가 1000개 남짓이었고, 글자 사전을 만든 뒤 살펴보니 너무 적게 등장한 글자들이 있어 986 정도로 설정했다. 원 개발자 역시 임의로 2000이라는 숫자를 지정한 것이기 때문에, 각자 데이터를 잘 살펴보고 판단하여 정해야하는 부분이다. 그리고 아래에서 두번째 줄은 원래 데이터 셋의 형태 때문에 있는 코드 같은데, 없어도 될 것 같다. 계속해서 이런 느낌으로, 각자 상황에 맞게 코드를 수정 보완해가며 사용할 수 밖에 없다.

 

이렇게 필요한 부분을 수정한 뒤, 아래 예시처럼 형식에 맞게 명령어를 입력하면

python main.py --dataset_path "D:\code\train wav" --vocab_dest "D:\kospeech-latest" --output_unit "character" --preprocess_mode 'phonetic'

약간의 시간이 소요된 뒤에 지정한 경로에, 지정한 이름으로 글자 사전이 만들어지고 학습에 필요한 transcripts.txt 파일 역시 만들어지는 것을 확인할 수 있다. 이 파일을 이후 다른 곳에 붙여넣고 활용할 것인데, encoding 방식으로 utf-8이 기본적으로 사용되니, 파일을 열어 다른 이름으로 저장에서 인코딩 방법을 utf-8로 변경하여 다시 저장해두자.

데이터가 공개되면 안돼서 올릴 수는 없지만, transcript.txt 파일 구성의 예를 들자면 아래와 같다.

# 오디오 파일 이름 tab 한글 전사 tab 숫자 전사
audio1.wav	점심 맛있게 먹었니	320 1 3 24 56 88 3 98 124 76

위의 숫자 전사에서 3은 띄어쓰기를 의미한다. 위의 구조로 오디오 파일의 갯수 만큼 .txt 파일에 줄마다 적혀 있다면 모델을 학습할 준비가 된 것이다.

 

기억했던 것보다 시행착오를 겪는데 훨씬 더 많은 시간을 들였던 것 같다는 생각이 든다,, 언제 완성하려나 ㅎㅎ

728x90
반응형