Pytorch
TensorFlow

#
다중 시퀀스 처리

이전 섹션에서, 우리는 가장 간단한 사용 예시를 확인했습니다. 길이가 짧은 단일 시퀀스에 대한 추론을 수행했습니다. 그러나 이미 몇 가지 궁금증이 생깁니다.

- 여러 개의 시퀀스는 어떻게 처리하나요?
- 서로 다른 길이를 갖는 다중 시퀀스를 어떻게 처리하나요?
- 단어 사전의 인덱스가 모델이 잘 작동하게 하는 유일한 입력인가요?
- 엄청 긴 시퀀스를 처리하는 방법이 있나요?

이러한 질문들이 제기하는 문제에 대해 알아보고, 🤗 Transformers API를 이용해 이 문제들을 어떻게 해결할 수 있는지 살펴봅시다.

##
모델은 배치 입력을 기대합니다

우리는 이전 실습에서 시퀀스가 숫자 리스트로 어떻게 바뀌는지 확인했습니다. 이 숫자 리스트를 텐서로 바꾸고 모델로 보내봅시다.:

    import torch
    from transformers import AutoTokenizer, AutoModelForSequenceClassification

    checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"
    tokenizer = AutoTokenizer.from_pretrained(checkpoint)
    model = AutoModelForSequenceClassification.from_pretrained(checkpoint)

    sequence = "I've been waiting for a HuggingFace course my whole life."

    tokens = tokenizer.tokenize(sequence)
    ids = tokenizer.convert_tokens_to_ids(tokens)
    input_ids = torch.tensor(ids)
    # 이 코드는 실행되지 않을 것입니다.
    model(input_ids)

    IndexError: Dimension out of range (expected to be in range of [-1, 0], but got 1)

이런! 2장의 파이프라인을 순서대로 따라했는데 왜 실행되지 않는 걸까요?

🤗 Transformers 모델은 기본적으로 여러 개의 문장을 입력으로 받는데 하나의 시퀀스만을 모델에 넘겨줬기 때문에 발생하는 문제입니다. 여기서 우리는 토크나이저를 `시퀀스`에 적용했을 때 뒤에서 일어나고 있는 모든 일을 수행하려고 했습니다. 하지만 자세히 보면, 토크나이저가 입력 ID 리스트를 텐서로 바꿨을 뿐만 아니라 차원도 추가한 것을 알 수 있습니다.

    tokenized_inputs = tokenizer(sequence, return_tensors="pt")
    print(tokenized_inputs["input_ids"])

    tensor([[  101,  1045,  1005,  2310,  2042,  3403,  2005,  1037, 17662, 12172,
              2607,  2026,  2878,  2166,  1012,   102]])

새로운 차원을 추가해서 다시 시도해봅시다.

    import torch
    from transformers import AutoTokenizer, AutoModelForSequenceClassification

    checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"
    tokenizer = AutoTokenizer.from_pretrained(checkpoint)
    model = AutoModelForSequenceClassification.from_pretrained(checkpoint)

    sequence = "I've been waiting for a HuggingFace course my whole life."

    tokens = tokenizer.tokenize(sequence)
    ids = tokenizer.convert_tokens_to_ids(tokens)

    input_ids = torch.tensor([ids])
    print("Input IDs:", input_ids)

    output = model(input_ids)
    print("Logits:", output.logits)

입력 ID와 결과 로짓을 출력한 결과입니다.

    Input IDs: [[ 1045,  1005,  2310,  2042,  3403,  2005,  1037, 17662, 12172,  2607, 2026,  2878,  2166,  1012]]
    Logits: [[-2.7276,  2.8789]]

배치는 한 번에 여러 개의 문장을 모델로 보내는 방법입니다. 만약 단 한 개의 문장을 가지고 있다면 한 문장을 위한 배치를 만들 수 있습니다.

    batched_ids = [ids, ids]

동일한 문장 2개로 만든 배치입니다!

✏️ 직접 해보세요! 이 `batched_ids` 리스트를 텐서로 변환하고 모델로 전달해보세요. 이전에 얻은 로짓과 동일한 결과를 얻는지 확인해보세요. (개수는 두 개여야 합니다!)

배치는 여러 개의 문장을 모델로 넘겼을 때도 모델이 작동하게 합니다. 다중 시퀀스를 사용하는 것은 단일 시퀀스로 배치를 만드는 것만큼 간단합니다. 하지만 두 번째 문제가 있습니다. 두 개 이상의 문장을 배치로 만드려고 할 때, 그 문장들은 아마 다른 길이를 가지고 있을 것입니다. 이전에 텐서를 다뤄본 사람이라면, 텐서의 형태가 직사각형이어야 한다는 것을 알고 있습니다. 문장 길이가 다르면 입력 ID 리스트를 텐서로 바로 변환할 수 없습니다. 이 문제를 해결하기 위해, 일반적으로 입력에 패드를 추가합니다.

##
입력에 패딩 추가하기

아래 보이는 리스트는 텐서로 변환될 수 없습니다.

    batched_ids = [
        [200, 200, 200],
        [200, 200]
    ]

패딩을 이용해 텐서가 직사각형 형태를 가질 수 있게 하면 이 문제를 해결할 수 있습니다. 패딩은 길이가 짧은 문장에 패딩 토큰이라고 불리는 특별한 토큰을 추가함으로써 모든 문장이 같은 길이를 갖게 합니다. 10개의 단어로 이루어진 문장 10개와 20개의 단어로 이루어진 문장 1개를 가지고 있다고 가정한다면, 패딩은 모든 문장이 20개의 단어를 갖게 하는 역할을 합니다. 우리가 사용하는 예시에서 결과 텐서는 다음과 같습니다.

    padding_id = 100

    batched_ids = [
        [200, 200, 200],
        [200, 200, padding_id],
    ]

패딩 토큰의 ID는 `tokenizer.pad_token_id` 를 통해 확인할 수 있습니다. 이걸 사용해서 두 문장을 각각 모델로 보내고 하나의 배치로 만들어 봅시다.

    model = AutoModelForSequenceClassification.from_pretrained(checkpoint)

    sequence1_ids = [[200, 200, 200]]
    sequence2_ids = [[200, 200]]
    batched_ids = [
        [200, 200, 200],
        [200, 200, tokenizer.pad_token_id],
    ]

    print(model(torch.tensor(sequence1_ids)).logits)
    print(model(torch.tensor(sequence2_ids)).logits)
    print(model(torch.tensor(batched_ids)).logits)

    tensor([[ 1.5694, -1.3895]], grad_fn=<AddmmBackward>)
    tensor([[ 0.5803, -0.4125]], grad_fn=<AddmmBackward>)
    tensor([[ 1.5694, -1.3895],
            [ 1.3373, -1.2163]], grad_fn=<AddmmBackward>)

배치되어 있는 예측 결과의 로짓이 뭔가 잘못된 것 같습니다. 두 번째 행은 두 번째 문장의 로짓과 값이 같아야 하는데 완전히 다른 값을 얻었습니다!

이것은 Transformer 모델의 핵심 기능이 각 토큰을 문맥화하는 어텐션 레이어이기 때문입니다. 어텐션 레이어는 시퀀스 내 모든 토큰을 처리하기 때문에 패딩 토큰도 고려합니다. 서로 다른 길이를 가지는 문장 각각을 모델로 전달했을 때와 패딩이 추가되어 길이가 같아진 문장들을 배치로 전달했을 때의 결과가 같기 위해서는 이 어텐션 레이어들에게 패딩 토큰을 무시하라고 알려주어야 합니다. 이 역할을 어텐션 마스크가 수행합니다.

##
어텐션 마스크

어텐션 마스크는 입력 ID 텐서와 같은 크기를 같는 텐서로, 0과 1로 이루어져 있습니다. 1은 해당 토큰을 주의 깊게 봐야한다는 것을 의미하고 0은 해당 토큰을 신경 쓰지 않아도 된다는 의미입니다. (다시 말해, 0에 해당하는 토큰은 모델의 어텐션 레이어에서 무시되어야 한다는 뜻입니다.)

어텐션 마스크와 함께 이전 예시를 완성해봅시다.

    batched_ids = [
        [200, 200, 200],
        [200, 200, tokenizer.pad_token_id],
    ]

    attention_mask = [
        [1, 1, 1],
        [1, 1, 0],
    ]

    outputs = model(torch.tensor(batched_ids), attention_mask=torch.tensor(attention_mask))
    print(outputs.logits)

    tensor([[ 1.5694, -1.3895],
            [ 0.5803, -0.4125]], grad_fn=<AddmmBackward>)

이제 배치 내 두 번째 문장과 동일한 로짓을 얻었습니다.

두 번째 문장의 어텐션 마스크에서 마지막 값인 0은 패딩 ID라는 것을 잊지 마세요.

✏️ 직접 해보세요! 2장에서 사용한 두 개의 문장(“I’ve been waiting for a HuggingFace course my whole life.” and “I hate this so much!“)을 이용해 직접 토큰화를 적용해보세요. 토큰화 결과를 모델에 넘기고 2장에서 얻은 것과 동일한 로짓을 얻었는지 확인해보세요. 이제 Now batch them together using the padding token, then create the proper attention mask. Check that you obtain the same results when going through the model!

##
길이가 긴 시퀀스

Transformer 모델을 사용할 때, 모델에 넘겨줄 수 있는 시퀀스 길이에 제한이 있습니다. 대부분의 모델은 최대 512개나 1024개의 토큰으로 이루어진 시퀀스를 처리할 수 있으며 더 긴 길이의 시퀀스를 처리해달라는 요청을 받으면 작동하지 않습니다. 이 문제에 대한 해결 방법은 2가지가 있습니다.

- 긴 시퀀스를 처리할 수 있는 모델을 사용하세요.
- 시퀀스 길이를 최대 길이에 맞게 잘라내세요.

모델별로 지원하는 시퀀스 길이는 다르고 몇 개의 특별한 모델은 엄청나게 긴 시퀀스를 처리할 수 있습니다. Longformer 가 그 중 하나이며, LED도 해당합니다. 만약 매우 긴 길이의 시퀀스를 다뤄야 하는 태스크를 진행하고 있다면, 두 모델을 살펴보세요.

그렇지 않으면 `max_sequence_length` 파라미터를 이용해 시퀀스 길이를 잘라내는 것이 좋습니다.

    sequence = sequence[:max_sequence_length]