fd873630 / RNN-Transducer

RNN-Transducer for korean
39 stars 3 forks source link

Transducer -> recognize -> decode 관련 질문 #2

Open hccho2 opened 2 years ago

hccho2 commented 2 years ago

Transducer class 코드에서. Q1. pred != 0 ---> 이 의미는 blank가 아닌 character를 예측한 것으로 transducer 구조(첨부한 그림 참조)에서 수직으로 이동한 것에 해당합니다. 이 경우에는 (encoder time step) t가 증가하면 안되는 것 아닌가요? image

Q2. zero_token = torch.LongTensor([[0]]) 시작 token을 0으로 잡았는데, 0은 blank입니다. blank로 하는 것보다는 sos token으로 하는 것이 맞지 않을까요? sos token으로 하려면, train data도 모두 sos token으로 시작하게 만들어야 하는데, README에 보면, 그렇게 하지는 않은 것 같습니다.

fd873630 commented 2 years ago

Q1. def recognize(self, inputs, inputs_length):

    batch_size = inputs.size(0)

    enc_states, _ = self.encoder(inputs, inputs_length)

    zero_token = torch.LongTensor([[0]])

    if inputs.is_cuda:
        zero_token = zero_token.cuda()

    def decode(enc_state, lengths):
        token_list = []
        dec_state, hidden = self.decoder(zero_token)

        #print(len(hidden))
        for t in range(lengths):
            logits = self.joint(enc_state[t].view(-1), dec_state.view(-1))
            out = F.softmax(logits, dim=0).detach()
            pred = torch.argmax(out, dim=0)
            pred = int(pred.item())

            if pred != 0:
                token_list.append(pred)
                token = torch.LongTensor([[pred]])

                if enc_state.is_cuda:
                    token = token.cuda()

                dec_state, hidden = self.decoder(token, hidden=hidden)

        return token_list

inference코드인 recognize부분인 것으로 알고 있습니다.

inference code의 의미는 RNN-T의 수직방향만 뽑아내겠다는 의미입니다. 수평방향은 blank 함수이기 때문에 생략이 가능하기 때문입니다.

즉, 다시 말하자면 encoder의 output이 한 라인씩 들어가기 때문에 t는 계속 증가하고 blank함수가 출력으로 나올때는 그것을 생략하는 의미입니다.

Q2. seq2seq 기반의 모델들과 다르게 RNN-T는 sos token eos token이 없습니다.

제가 sos, eos를 추가한 이유는 추후에 Two pass speech recognition model을 위해서 미리 추가해놨습니다.

정리하자면 RNN-T는 encoder와 decoder의 output이 joint되어서 계산되는데 decoder의 initialize가 필요하기 때문에 맨 처음 blank 함수를 사용합니다.

궁금한점이 해결되었기를 바랍니다.

감사합니다.

hccho2 commented 2 years ago

"encoder의 output이 한 라인씩 들어가기 때문에 t는 계속 증가하고 blank함수가 출력으로 나올때는 그것을 생략하는 의미입니다."

이 부분이 좀 이해가 되지 않는데요. blank가 나올 때까지 증가하는 것은 encoder time step t가 아니고, decoder time step u가 증가해야 하는 것 같습니다.

아래 코드를 한번 참고해 보세요. https://github.com/iceychris/LibreASR/blob/8a12c2f7cdccf42a5df47b63b2d60e97d389eba6/libreasr/lib/models.py#L405

fd873630 commented 2 years ago

제가 질문 이해를 잘못한것 같습니다.

pred != 0 ---> 이 의미는 blank가 아닌 character를 예측한 것으로 transducer 구조(첨부한 그림 참조)에서 수직으로 이동한 것에 해당합니다. 이 경우에는 (encoder time step) t가 증가하면 안되는 것 아닌가요?

-> 위에 첨부해주신 코드가 맞는것 같습니다. 말씀하신 내용을 다시 한번 정리하자면 하나의 encoder frame에 1 ~ x개의 output이 포함 될 수 있는데 이 코드에서는 "encoder frame에서 하나의 character만 나올 것이다" 라는 가정을 하고 코딩을 했던것 같습니다. 코드 작성할때 speech recognition 기준으로 time reduction layer를 안쓰는 이상 input sequence가 output보다 상당히 길어서 하나의 encoder frame에서 두개 이상의 character가 나올 가능성이 없을 것이다라고 생각했던 것 같습니다.

부족한점 알려주셔서 감사합니다.

최대한 빠른 시일 내에 수정하겠습니다.

감사합니다

YooSungHyun commented 1 year ago

혹시 작성된 소스에서 잘못될 여지가 있는게 blank_token이 아닌경우에 t와 u가 동시에 증가하기 때문일까요? 이거 뭔가 for문으로 돌릴게 아니고 while로 처리해야되나 싶기도 하고요...? @fd873630 @hccho2

fd873630 commented 1 year ago

@YooSungHyun 혹시 u < beam size는 왜 사용하셨는지 알려주실수 있으신가요? 한 프레임에서 여러 label이 나올 가능성을 염두하시고 작성하신걸로 이해하였는데 맞나요?

fd873630 commented 1 year ago

@YooSungHyun

t=1일때, encode time sequence 1에 대해 blank가 아닐때까지 예측을 진행하며, 모든 디코딩 가능한 경우의 수를 뽑습니다. 안, 얀, 야, 아, 안.... (블랭크가 영영 나오지 않는다면, 무한 루프에 빠질 수 있으므로 beam_size와 같은 특정 파라미터로, 개수를 제한해야합니다)

혹시 이부분이 잘 이해가 되지 않아서 t=1일때 왜 "안, 얀, 야, 아, 안..." 이런 식으로 나올까요? argmax하게 되면 프레임별 하나만 나오지 않나요?

fd873630 commented 1 year ago

@YooSungHyun 아 이 그림은 infernece 그림이 아니라 best path의 probability를 찾는겁니다. CTC 그래프를 생각하시면 편할것 같습니다.

YooSungHyun commented 1 year ago

RNNT Loss로 학습되는 모델과 그리디 서치에 대해서 이해가 된 것 같습니다. 제가 현재 이전에 쓴 해당 이슈의 모든 글들은 이해를 30%정도밖에 못하고 작성한 것이었어서 그냥 삭제했습니다.

https://www.youtube.com/watch?v=dgsDIuJLoJU 일단 1프레임에 1 단어만 생긴다 = 오류 입니다. 1프레임에 2단어 이상 발생할 수 있습니다. 예를 들어, t1의 해당하는 프레임이 매~~~우 작게 잘린 경우가 아니므로, bee이라는 단어라고하면, t1=be t2=e이 될 수 있습니다. 즉 현재 디코딩 방식대로, text가 검출되었더라도 t를 +1해버리는 것은, 짧은 음성에 더 많은 정보가 있을때, 유실될 여지를 내포합니다. 텍스트가 검출되었을때는 t+1을 하면 안되고, 그대로 u를 추가로 진행시켜야 하는 것이 맞습니다.

이러면 CTC를 알고있던 우리에겐 두가지 질문이 생길 수 있습니다.

  1. u가 무한대로 증가하는 경우가 생길 수 있지 않겠는가?
  2. CTC는 무조건 time frame을 증가시키면서 계산하는데, 한 프레임에 be가 존재한다는게 가능은 한 것인가?

1번에 대한 해설 u가 무한대로 증가하는 경우는, 모델이 발산하는 경우로 사료됩니다. 정상적인 RNNTLoss가 낮아지는 방향으로 모델이 학습된다면, 해당 프레임에 있는 단어만 정확하게 검출되고 blank 처리되어야 합니다. 마치, '아이' 라는 단어가 1프레임에 들어있었는데, 그 음성을 듣고, 이게 '아'인지, '이'인지? 모델이 제대로 결정을 못 내릴경우 u가 무한대로 증가할 여지가 생깁니다. 만약에 모델이 잘 학습되었다면, 직전 decode의 hidden_state를 계속 넣으면서 진행하므로, 언젠가는 매칭되지 않는 blank가 발생하며, 다음 t+1을 진행해야 합니다.

2번에 대한 해설 CTC의 Input Length가 Output Length 보다 길어야하는 이유도 어쩌면 이 때문일까 조심스럽게 생각해봤습니다. 앞 뒤 순서가 모순일 수 있으나, 무조건 진행해야하는 CTC 입장에서 Output Length가 Input Length보다 길다는 것은, 1개의 Input에 여러 Output이랑 매칭될 수 있다는 의미이므로, CTC사상에 맞지 않습니다. 때문에 CTC loss로 학습될때는 음성 sequence를 라벨 sequence보다 긴 길이로 세밀하게 구성해서 최대한 겹치지 않을 상황을 만들어주는게 아닐까 생각되구요. RNNTLoss는 진행 방식이 CTC loss와는 전혀 다르기 때문에, 이런 부분들이 해소되지 않나 생각해봤습니다.

마지막으론 이런 질문도 생길 수 있겠네요 그렇다면 왜 기존 소스들은 대체적으로 blank가 아닐때도 t를 증가시키는데 학습이 잘되느냐? 음성학 기준으로 사람이 분간 가능한 최소단위 시간은 25ms인데, 음성 전처리를 이미 그렇게 해놨으니, RNNT Loss로 학습시키더라도 한 음성프레임이 사람이 인지 가능한 정보량이 2개이상 있을 가능성이 매우 적어질거라고 봅니다. 그래서 그냥 t가 계속 진행되기만 하여도 잘 되는 것 같구요.

FM 대로 짤거면 blank일때만 t가 진행되어야 하고, 모델 학습이 잘 됐다면 u가 무한히 진행되지 않아야 하나, 초반에 모델 학습이 잘 안되었는데 metric을 위해 greedy search를 채용한다면, u가 무한으로 진행되는 상황이 생길지도 모르겠습니다. 때문에, @hccho2 님이 올려주신 예제 소스에는 max_iter를 default 3으로 잡아놓은게 그 때문일 것으로 생각되네요.

맘같아선 이부분 수정해서 컨트리뷰트 하고싶긴한데, greedy 뿐만 아니라 빔서치도 짜야해서, 정말 나중에나 컨트리뷰트 도전하거나 하게될 것 같습니다.

그리고 어제 @fd873630 님과 이야기된 '비비디 바비디 부 문제'는 제가 토크나이징 디코드 잘못하고있었더군요, blank에 맞춰 그냥 디코드에서 중복 처리하도록 하던가 해야될 것 같습니다. 정신 깨어지게 도와주셔서 감사합니다.

YooSungHyun commented 1 year ago

아 그리고 한가지 더 추가하면, greedy search에서 u가 진행되면서 나오는 토큰은 모두 append되어야 합니다. 1) t1에서 u1=안 -> u2=안: blank가 나오기 전에 중복이니 pred=안 2) t1에서 u1=안 -> u2=녕: pred=안녕

초기에 같은 t에서 나온 모든 u에 대해서도 argmax를 해서 t1에 대한 토큰 1개만을 선별해야하나 생각했던 적이 있었는데,

ex) 2번의 경우 '안'의 prob이 있고, '녕'의 prob이 있어서 두개중에서도 max를 뽑아서 t1에 대한 토큰은 무조건 1개로 매핑해야하나라고 생각함.

잘못생각하고있었던겁니다. 혹시나 저와 비슷하게 헷깔리시는 분 계실수도 있어서 말씀드립니다.

YooSungHyun commented 1 year ago

구현해놓으신 BeamSearch도 위의 사상과 같은 맥락으로 작성되어져있는 듯 합니다. 통상 Beam=1 BeamSearch는 GreedySearch와 동치로 보는 것으로 알고있습니다.

빔서치 로직을 보아하니, prefix는 뭔지 잘 모르겠지만(쓰긴 쓰나요?) 특정 타임시점 t1 ~ t3에 따라

(t1,u0) 에선 blank밖에 없으므로 최초 디코딩 이후,
    joint하여 blank면 B에 넣고 (t2,u0)으로 진행되게 될 것이고
    blank가 아니면, (t1,u1)를 진행시키기 위해 non blank vocab prob을 A에 저장하게 됩니다.
        (t1,u1)에선 non blank vocab prob A중에 가장 확률적으로 선택될만한놈 y_star를 뽑아서 디코딩+joint를 진행,
        """ 상단 부분이 argmax를 뽑은다음 t를 +1하지 않고, 또 디코딩+joint하는 내용과 일맥상통합니다 """
        다음 토큰이 blank라면, 현재까지 완성된 y_star를 B에 저장하고, t2 진행
        다음 토큰이 blank가 아니라면, y_star + joint결과 vocab 확률 (log prob의 +니까 결국 확률곱)
--- 이하 생략 ---

이런식으로 현재 t에서 가장 높은 확률이 blank가 되어 t2를 보러가기 전까지 u만 증가시키는 형태로 코드가 작성되어있네요 그리고 논문 수도코드도 y_star라는 갠적으로 개떡같은 표현을 써놔서 그렇지, 위와 로직이 동일한 것 같구요

때문에 greedy search를 beam search beam=1과 동치로 진행시키려면, 최초 시작점부터, u가 blank가 나오지 않으면, 그 외의 어떤 여러가지 텍스트가 나올 여지가 있으므로, t는 +되지 않는 것이 맞습니다.

그나저나 이거 수도코드도 blank가 tn에서 무한하게 나오지 않고 계속 u만 증가되는 경우, 모든 A의 최악의 prob까지 전부 재귀적으로 확인해보고 A가 텅텅 비어서 에러가 나지 않으면 코드가 아마 무한루프 돌아갈 것 같습니다. blank를 마주할일이 없으니 B에 채워질리도 없고, 때문에 뭔가 기존에 만들어진 완성형 문장 B가 A보다 확률이 높아질일이 영영 없기 때문이죠.

어쩌면 모델이 잘 학습되면, u가 무한하게 증가되는 경우는 없는걸까요? 허허...

YooSungHyun commented 1 year ago

@fd873630 코드가 지극히 개인적이다보니, 테스트를 해보고 커밋을 치긴 어려울 것 같은데, 가능하면 beam_search와 greedy_search 쪽 수정해도 될까요?

fork하고 pr 날려야할지, 아니면 branch 따서 바로 작업해도 될지, 원하시는 방법 있으시면 알려주심 감사하겠습니다.

pyctcdecode의 beam_search 진행 간 lm이 처리되는 로직을 참고하여, beam_search with kenlm이 가능한 코드까지 구현 완료하였네요.

https://github.com/YooSungHyun/RNNTransducer/blob/main/networks/transducer.py

수 많은 디버깅과, 핸드 트레이싱을 통해서 수도코드와 다르지 않으면서도, pyctcdecode의 kenlm 사용방식과 거의 동일하게 짜내지 않았나 생각됩니다. (오로지 우려스러운 점은, 스트리밍 진행 간 정상 동작할 지 정도 우려스러운데(테스트는 전체 음성을 넣는 case로만 진행되었음), 아마 text는 계속 저장하면서 진행하게 될테니 정상 동작하지 않을까 사료됩니다.) lm 넣어서 해보면, 짧은 음성에는 매우 정확하게 나오는데, 긴 음성은 문장이 짤리는 경향이 있네요, 아마 확률의 대소비교로 반복문 out 조건이나 이런게 있다보니, 길어질수록 확률값이 작아져서 더 민감하게 반응하나 싶기도 하고...

지금 상당히 알고리즘 이해도에 물이 올라있는 시점이라, 아마 제가 짰던 코드를 기반으로 greedy search와 beam search 정도의 무한 loop 돌지 않을 조건 및 t와 u가 같이 진행되지 않는 greedy search를 수정해놓을 수 있을 것으로 판단됩니다. (lm은 위에 우려스러움에 대한 이유로, 건들지 않겠습니다.)

해당 코드에 맞는 모델이 없어서 테스트는 힘들겠지만, 해당 코드를 직접 수정하지않고, python file을 하나 추가하여 example 처럼 만들어놓는 것도 가능하니, 괜찮으시면 말씀해주시면 좋겠습니다. 개인적으로 참고도 많이되었고 도움도 많이 주셨는데, 32 star 박혀있는 프로젝트에 기여하고 싶긴 하네요

fd873630 commented 1 year ago

@YooSungHyun python file을 하나 추가해서 example 처럼 만들어놓는것이 좋을것 같습니다!