Pytorch
TensorFlow

#
학습 파이프라인 디버깅

단원 7의 조언에 충실히 따라 주어진 작업에서 모델을 훈련하거나 파인튜닝하는 멋진 스크립트를 작성했습니다. 하지만 `model.fit()` 명령을 실행하면 끔찍한 일이 발생합니다. 오류가 발생했네요 😱! 또는 더 좋지 못한 일은 모든 것이 정상인 것처럼 보이고 훈련은 오류 없이 실행되었지만 결과 모델이 엉망인 경우입니다. 이 장에서는 이러한 종류의 이슈를 디버그하기 위해 수행할 수 있는 작업을 보여줍니다.

##
학습 파이프라인 디버깅

`model.fit()`에서 오류가 발생했을 때 문제는 여러 곳에서 발생할 수 있습니다. 학습은 일반적으로 시작부터 해당 에러 지점까지 많은 작업이 진행되기 때문인데, 문제는 데이터세트가 뭔가 잘못 되었거나, 데이터세트의 배치 데이터를 함께 일괄 처리하려고 할 때 발생하는 문제일 수 있습니다. 또는 모델 코드, 손실 함수 또는 옵티마이저에 문제가 있을 수 있습니다. 모든 것이 훈련에 적합하더라도 메트릭에 문제가 있는 경우 평가 중에 문제가 발생할 수 있습니다.

`model.fit()`에서 발생하는 오류를 디버그하는 가장 좋은 방법은 이 전체 파이프라인을 직접 살펴보고 문제가 발생한 부분을 확인하는 것입니다. 가끔 에러는 해결하기 쉬울 때도 있습니다.

예시를 위해서 MNLI 데이터세트에서 DistilBERT 모델을 파인튜닝(을 시도하는) 아래 스크립트를 사용합니다.:

    from datasets import load_dataset
    import evaluate
    from transformers import (
        AutoTokenizer,
        TFAutoModelForSequenceClassification,
    )

    raw_datasets = load_dataset("glue", "mnli")

    model_checkpoint = "distilbert-base-uncased"
    tokenizer = AutoTokenizer.from_pretrained(model_checkpoint)

    def preprocess_function(examples):
        return tokenizer(examples["premise"], examples["hypothesis"], truncation=True)

    tokenized_datasets = raw_datasets.map(preprocess_function, batched=True)

    train_dataset = tokenized_datasets["train"].to_tf_dataset(
        columns=["input_ids", "labels"], batch_size=16, shuffle=True
    )

    validation_dataset = tokenized_datasets["validation_matched"].to_tf_dataset(
        columns=["input_ids", "labels"], batch_size=16, shuffle=True
    )

    model = TFAutoModelForSequenceClassification.from_pretrained(model_checkpoint)

    model.compile(loss="sparse_categorical_crossentropy", optimizer="adam")

    model.fit(train_dataset)

이 코드를 실행하려고 하면 데이터 세트를 변환할 때 ‘VisibleDeprecationWarning’이 표시될 수 있습니다. 이는 알려진 UX 문제이므로 무시하시면 됩니다. 예를 들어, 2021년 11월 이후에 이 코스를 읽고 있는데 여전히 진행 중이라면 수정 할 때까지 @carrigmat로 분노의 트윗을 보내주세요.

위 문제보단 누가봐도 명백한 에러가 발생한다는 것인데, 정말 끔찍하게 깁니다:

    ValueError: No gradients provided for any variable: ['tf_distil_bert_for_sequence_classification/distilbert/embeddings/word_embeddings/weight:0', '...']

이게 무슨 뜻일까요? 데이터로 학습하려고 하는데 그라디언트가 없다니? 꽤나 당황스러운 일입니다; 이럴 경우 어떻게 디버깅을 할 수 있을까요? 에러가 발생했을 때 문제가 어디에 있는지 바로 알 수 없는 경우 가장 좋은 해결책은 각 단계에서 모든 것이 올바르게 보이는지 확인하면서 순서대로 살펴보는 것이 효과적인 경우가 있습니다. 물론 시작하는 곳은 항상…

###
데이터 확인

굳이 언급하자면, 데이터가 손상된 경우 Keras는 스스로 수정할 수 없습니다. 따라서 가장 먼저 해야 할 일은 학습 세트 내부가 어떤지 살피는 것입니다.

`raw_datasets` 및 `tokenized_datasets` 내부를 살펴보면 좋겠지만 왠만하면 모델에 들어가기 전 지점의 데이터를 확인하는 것이 좋습니다. 즉, `to_tf_dataset()` 함수로 생성한 `tf.data.Dataset`에서 출력을 확인해야 합니다! 그럼 어떻게 해야 할까요? `tf.data.Dataset` 객체는 한 번에 전체 배치를 제공하고 인덱싱을 지원하지 않으므로 `train_dataset[0]`의 방식으로 요청할 수 없습니다. 그렇지만 우리는 살며시 배치를 요청할 수 있습니다.:

    for batch in train_dataset:
        break

`break`는 한 번의 반복 후에 루프를 종료하므로 `train_dataset`에서 나오는 첫 번째 배치를 가져와 `batch`로 저장합니다. 이제 내부를 살펴보겠습니다.:

    {'attention_mask': <tf.Tensor: shape=(16, 76), dtype=int64, numpy=
     array([[1, 1, 1, ..., 0, 0, 0],
            [1, 1, 1, ..., 0, 0, 0],
            [1, 1, 1, ..., 0, 0, 0],
            ...,
            [1, 1, 1, ..., 1, 1, 1],
            [1, 1, 1, ..., 0, 0, 0],
            [1, 1, 1, ..., 0, 0, 0]])>,
     'label': <tf.Tensor: shape=(16,), dtype=int64, numpy=array([0, 2, 1, 2, 1, 1, 2, 0, 0, 0, 1, 0, 1, 2, 2, 1])>,
     'input_ids': <tf.Tensor: shape=(16, 76), dtype=int64, numpy=
     array([[ 101, 2174, 1010, ...,    0,    0,    0],
            [ 101, 3174, 2420, ...,    0,    0,    0],
            [ 101, 2044, 2048, ...,    0,    0,    0],
            ...,
            [ 101, 3398, 3398, ..., 2051, 2894,  102],
            [ 101, 1996, 4124, ...,    0,    0,    0],
            [ 101, 1999, 2070, ...,    0,    0,    0]])>}

맞는 것 같지 않나요? 모델에 ‘labels’, ‘attention_mask’, ‘input_ids’를 전달하고 있는데, 이는 출력값과 손실값을 계산하는 데 필요한 모든 것이 있어야 합니다. 그럼 왜 그래디언트가 없는 걸까요? 자세히 보면 단일 Dictionary 구조로 입력값을 전달하지만 학습 배치는 일반적으로 텐서 또는 Dictionary, 그리고 레이블 텐서입니다. Dictionary 입력에서 레이블은 키로 되어있습니다.

이것이 문제일까요? 항상 그런 것은 아닙니다! 그렇지만 TensorFlow로 Transformer 모델을 훈련할 때 접하게 되는 가장 일반적인 문제 중 하나입니다. 우리 모델은 모두 내부적으로 손실값을 계산할 수 있지만 그렇게 하려면 레이블이 입력 Dictionary에 전달되어야 합니다. 이것은 `compile()`에 손실 파라미터 값을 지정하지 않을 때 사용되는 손실값 입니다. 반면에 Keras는 일반적으로 레이블이 입력 Dictionary와 별도로 전달될 것으로 예상하며 그렇게 하지 않으면 일반적으로 손실값 계산이 실패합니다.

문제는 이제 명확해졌습니다. 우리는 ‘loss’ 파라미터값을 설정 했었습니다. 즉, Keras에게 손실값을 계산하도록 요청했지만, Keras가 예상하는 위치가 모델에다가 레이블을 직접 전달했습니다! 따라서 둘 중 하나를 선택해야 합니다. 모델의 내부 손실 함수를 사용하고 레이블을 그대로 유지하거나, Keras 손실함수를 사용하고 레이블을 Keras가 예상하는 위치로 이동해야 합니다. 심플하게 첫 번째 접근 방식을 취하겠습니다. `compile()`에 대한 호출을 다음과 같이 변경합니다.:

    model.compile(optimizer="adam")

이제 모델의 내부 손실 함수를 사용하게 될 것이고 문제가 해결 될 겁니다! Now we’ll use the model’s internal loss, and this problem should be resolved!

✏️ 여러분 차례입니다! 다른 문제를 해결 후 추가 도전으로, 이 단계로 돌아와 모델이 내부 손실 대신 원래 Keras 손실함수로 작동하도록 할 수 있습니다. 레이블이 올바르게 출력되도록 하려면 `to_tf_dataset()`의 `label_cols` 인수에 `"labels"`를 추가해야 합니다. 그러면 그래디언트가 계산됩니다. 하지만 우리가 지정한 손실함수에는 한 가지 문제가 더 있습니다. 학습은 문제가 있음에도 불구하고 계속 실행되지만 학습이 매우 느리고 높은 train 손실값에서 정체 될 수 있습니다. 왜 그런지 알아 볼까요?

만약 어렵다면 ROT13 인코드 방식의 힌트를 보세요 Transformers에서 SequenceClassification 모델의 출력을 보면 첫 번째 출력은 ‘logits’입니다. logits란 무엇일까요?(살면서 ROT13 인코딩은 처음 봤네요, 궁금하면 영어 원본으로 보세요.)

두번째 힌트: 옵티마이저, 활성함수 또는 손실함수를 문자열로 지정하면 Keras는 해당 함수의 모든 인수 값을 기본값으로 설정합니다. SparseCategoricalCrossentropy에는 어떤 인수가 있으며 기본값은 무엇일까요?

이제 훈련을 해보죠. 그라디언트를 가져와야 하므로 부디(갑자기 불길한 음악이 재생됨) `model.fit()`을 호출하면 모든 것이 잘 작동할 것입니다!

      246/24543 [..............................] - ETA: 15:52 - loss: nan

이런.

`nan`은 썩 내키지 않는 손실값 입니다. 데이터를 확인했었지만 괜찮아 보였는데 말이죠. 이게 문제가 아니라면 다음은 어떤 것을 확인해야할까요? 다음 단계는 분명히…

###
모델 확인

`model.fit()`은 Keras에서 정말 훌륭한 함수이지만 많은 일을 수행하므로 문제가 발생한 위치를 정확히 찾기가 더 어려울 수 있습니다. 모델을 디버깅하는 경우 진짜 도움이 될만한 전략으로 모델에 단일 배치를 전달하고 해당 배치에 대한 출력을 자세히 살펴보는 방법이 있습니다. 모델에서 오류가 발생하는 경우 정말 유용한 또 다른 팁은 `run_eagerly=True`로 모델을 `compile()`하는 것입니다. 이렇게 하면 속도가 훨씬 느려지지만 모델 코드에서 문제가 발생한 위치를 정확히 나타내기 때문에 에러 메시지를 훨씬 더 이해하기 쉽게 만듭니다.

하지만 지금은 아직 `run_eagerly`가 필요하지 않습니다. 모델을 통해 이전에 얻은 `batch`를 실행하고 출력이 어떻게 보이는지 봅시다: For now, though, we don’t need `run_eagerly` just yet. Let’s run the `batch` we got before through the model and see what the outputs look like:

    model(batch)

    TFSequenceClassifierOutput(loss=<tf.Tensor: shape=(16,), dtype=float32, numpy=
    array([nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan,
           nan, nan, nan], dtype=float32)>, logits=<tf.Tensor: shape=(16, 2), dtype=float32, numpy=
    array([[nan, nan],
           [nan, nan],
           [nan, nan],
           [nan, nan],
           [nan, nan],
           [nan, nan],
           [nan, nan],
           [nan, nan],
           [nan, nan],
           [nan, nan],
           [nan, nan],
           [nan, nan],
           [nan, nan],
           [nan, nan],
           [nan, nan],
           [nan, nan]], dtype=float32)>, hidden_states=None, attentions=None)

흠, 무언가 복잡하네요. 모든 것이 `nan` 입니다! 하지만 이상하지 않나요? 모든 로짓은 어떻게 `nan`이 될까요? `nan`은 “not a number.”, 즉 숫자가 아님을 뜻합니다. `nan` 값은 0으로 나누는 것과 같은 금지된 연산을 수행할 때 종종 발생합니다. 그러나 기계 학습에서 `nan`에 대해 알아야 할 매우 중요한 한 가지는 이 값이 전파되는 경향이 있다는 것입니다. 숫자에 `nan`을 곱하면 출력도 `nan`이 됩니다. 출력, 손실값 또는 그래디언트의 아무 곳에서나 `nan`을 얻으면 전체 모델에 빠르게 확산됩니다. 왜냐하면 그 `nan` 값이 네트워크를 통해 다시 전파될 때 `nan` 그래디언트를 사용하여 가중치 업데이트를 계산하면 ‘nan’ 가중치를 얻을 수 있으며 이러한 가중치는 더 많은 `nan` 출력을 계산합니다! 곧 전체 네트워크는 하나의 큰 `nan` 블록이 될 것입니다. 그런 일이 발생하면 문제가 어디에서 시작되었는지 확인하기가 매우 어렵습니다. ‘nan’이 처음 들어온 곳을 어떻게 분리할 수 있을까요?

정답은 모델을 재초기화 하는 것입니다. 한번 훈련을 시작하면서 어딘가에 `nan`이 생겼고 모델 전체로 빠르게 전파되었습니다. 따라서 체크포인트에서 모델을 로드하고 가중치 업데이트를 수행하지 않고 `nan` 값이 생기는 위치를 확인합니다.:

    model = TFAutoModelForSequenceClassification.from_pretrained(model_checkpoint)
    model(batch)

코드를 실행하면 다음의 값이 출력됩니다.:

    TFSequenceClassifierOutput(loss=<tf.Tensor: shape=(16,), dtype=float32, numpy=
    array([0.6844486 ,        nan,        nan, 0.67127866, 0.7068601 ,
                  nan, 0.69309855,        nan, 0.65531296,        nan,
                  nan,        nan, 0.675402  ,        nan,        nan,
           0.69831556], dtype=float32)>, logits=<tf.Tensor: shape=(16, 2), dtype=float32, numpy=
    array([[-0.04761693, -0.06509043],
           [-0.0481936 , -0.04556257],
           [-0.0040929 , -0.05848458],
           [-0.02417453, -0.0684005 ],
           [-0.02517801, -0.05241832],
           [-0.04514256, -0.0757378 ],
           [-0.02656011, -0.02646275],
           [ 0.00766164, -0.04350497],
           [ 0.02060014, -0.05655622],
           [-0.02615328, -0.0447021 ],
           [-0.05119278, -0.06928903],
           [-0.02859691, -0.04879177],
           [-0.02210129, -0.05791225],
           [-0.02363213, -0.05962167],
           [-0.05352269, -0.0481673 ],
           [-0.08141848, -0.07110836]], dtype=float32)>, hidden_states=None, attentions=None)

이제 `nan`값의 위치를 알았내요! 우리 로짓값에는 `nan` 값이 없습니다. 그러나 우리는 손실값에서 몇 개의 ‘nan’ 값이 보이네요. 특히 이 문제를 일으키는 샘플에 대해 뭔가가 있는 걸까요? 어떤 것들이 있는지 봅시다(이 코드를 직접 실행하면 데이터세트가 섞였기 때문에 다른 인덱스를 얻을 수 있습니다):

    import numpy as np

    loss = model(batch).loss.numpy()
    indices = np.flatnonzero(np.isnan(loss))
    indices

    array([ 1,  2,  5,  7,  9, 10, 11, 13, 14])

이 인덱스로 불러온 샘플을 살펴보겠습니다.:

    input_ids = batch["input_ids"].numpy()
    input_ids[indices]

    array([[  101,  2007,  2032,  2001,  1037, 16480,  3917,  2594,  4135,
            23212,  3070,  2214, 10170,  1010,  2012,  4356,  1997,  3183,
             6838, 12953,  2039,  2000,  1996,  6147,  1997,  2010,  2606,
             1012,   102,  6838,  2001,  3294,  6625,  3773,  1996,  2214,
             2158,  1012,   102,     0,     0,     0,     0,     0,     0,
                0,     0,     0,     0,     0,     0,     0,     0,     0,
                0,     0,     0,     0,     0,     0,     0,     0,     0,
                0,     0,     0,     0,     0,     0,     0,     0,     0,
                0,     0,     0,     0,     0,     0,     0,     0,     0,
                0,     0,     0,     0],
           [  101,  1998,  6814,  2016,  2234,  2461,  2153,  1998, 13322,
             2009,  1012,   102,  2045,  1005,  1055,  2053,  3382,  2008,
             2016,  1005,  2222,  3046,  8103,  2075,  2009,  2153,  1012,
              102,     0,     0,     0,     0,     0,     0,     0,     0,
                0,     0,     0,     0,     0,     0,     0,     0,     0,
                0,     0,     0,     0,     0,     0,     0,     0,     0,
                0,     0,     0,     0,     0,     0,     0,     0,     0,
                0,     0,     0,     0,     0,     0,     0,     0,     0,
                0,     0,     0,     0,     0,     0,     0,     0,     0,
                0,     0,     0,     0],
           [  101,  1998,  2007,  1996,  3712,  4634,  1010,  2057,  8108,
             2025,  3404,  2028,  1012,  1996,  2616, 18449,  2125,  1999,
             1037,  9666,  1997,  4100,  8663, 11020,  6313,  2791,  1998,
             2431,  1011,  4301,  1012,   102,  2028,  1005,  1055,  5177,
             2110,  1998,  3977,  2000,  2832,  2106,  2025,  2689,  2104,
             2122,  6214,  1012,   102,     0,     0,     0,     0,     0,
                0,     0,     0,     0,     0,     0,     0,     0,     0,
                0,     0,     0,     0,     0,     0,     0,     0,     0,
                0,     0,     0,     0,     0,     0,     0,     0,     0,
                0,     0,     0,     0],
           [  101,  1045,  2001,  1999,  1037, 13090,  5948,  2007,  2048,
             2308,  2006,  2026,  5001,  2043,  2026,  2171,  2001,  2170,
             1012,   102,  1045,  2001,  3564,  1999,  2277,  1012,   102,
                0,     0,     0,     0,     0,     0,     0,     0,     0,
                0,     0,     0,     0,     0,     0,     0,     0,     0,
                0,     0,     0,     0,     0,     0,     0,     0,     0,
                0,     0,     0,     0,     0,     0,     0,     0,     0,
                0,     0,     0,     0,     0,     0,     0,     0,     0,
                0,     0,     0,     0,     0,     0,     0,     0,     0,
                0,     0,     0,     0],
           [  101,  2195,  4279,  2191,  2039,  1996,  2181,  2124,  2004,
             1996,  2225,  7363,  1012,   102,  2045,  2003,  2069,  2028,
             2451,  1999,  1996,  2225,  7363,  1012,   102,     0,     0,
                0,     0,     0,     0,     0,     0,     0,     0,     0,
                0,     0,     0,     0,     0,     0,     0,     0,     0,
                0,     0,     0,     0,     0,     0,     0,     0,     0,
                0,     0,     0,     0,     0,     0,     0,     0,     0,
                0,     0,     0,     0,     0,     0,     0,     0,     0,
                0,     0,     0,     0,     0,     0,     0,     0,     0,
                0,     0,     0,     0],
           [  101,  2061,  2008,  1045,  2123,  1005,  1056,  2113,  2065,
             2009,  2428, 10654,  7347,  2030,  2009,  7126,  2256,  2495,
             2291,   102,  2009,  2003,  5094,  2256,  2495,  2291,  2035,
             2105,  1012,   102,     0,     0,     0,     0,     0,     0,
                0,     0,     0,     0,     0,     0,     0,     0,     0,
                0,     0,     0,     0,     0,     0,     0,     0,     0,
                0,     0,     0,     0,     0,     0,     0,     0,     0,
                0,     0,     0,     0,     0,     0,     0,     0,     0,
                0,     0,     0,     0,     0,     0,     0,     0,     0,
                0,     0,     0,     0],
           [  101,  2051,  1010,  2029,  3216,  2019,  2503,  3444,  1010,
             6732,  1996,  2265,  2038, 19840,  2098,  2125,  9906,  1998,
             2003,  2770,  2041,  1997,  4784,  1012,   102,  2051,  6732,
             1996,  2265,  2003,  9525,  1998,  4569,  1012,   102,     0,
                0,     0,     0,     0,     0,     0,     0,     0,     0,
                0,     0,     0,     0,     0,     0,     0,     0,     0,
                0,     0,     0,     0,     0,     0,     0,     0,     0,
                0,     0,     0,     0,     0,     0,     0,     0,     0,
                0,     0,     0,     0,     0,     0,     0,     0,     0,
                0,     0,     0,     0],
           [  101,  1996, 10556,  2140, 11515,  2058,  1010,  2010,  2162,
             2252,  5689,  2013,  2010,  7223,  1012,   102,  2043,  1996,
            10556,  2140, 11515,  2058,  1010,  2010,  2252,  3062,  2000,
             1996,  2598,  1012,   102,     0,     0,     0,     0,     0,
                0,     0,     0,     0,     0,     0,     0,     0,     0,
                0,     0,     0,     0,     0,     0,     0,     0,     0,
                0,     0,     0,     0,     0,     0,     0,     0,     0,
                0,     0,     0,     0,     0,     0,     0,     0,     0,
                0,     0,     0,     0,     0,     0,     0,     0,     0,
                0,     0,     0,     0],
           [  101, 13543,  1999,  2049,  6143,  2933,  2443,   102,  2025,
            13543,  1999,  6143,  2933,  2003,  2443,   102,     0,     0,
                0,     0,     0,     0,     0,     0,     0,     0,     0,
                0,     0,     0,     0,     0,     0,     0,     0,     0,
                0,     0,     0,     0,     0,     0,     0,     0,     0,
                0,     0,     0,     0,     0,     0,     0,     0,     0,
                0,     0,     0,     0,     0,     0,     0,     0,     0,
                0,     0,     0,     0,     0,     0,     0,     0,     0,
                0,     0,     0,     0,     0,     0,     0,     0,     0,
                0,     0,     0,     0]])

음, 여기에 많은 것이 있지만 특별한게 없습니다. 레이블을 살펴보겠습니다.:

    labels = batch['labels'].numpy()
    labels[indices]

    array([2, 2, 2, 2, 2, 2, 2, 2, 2])

아! `nan` 샘플은 모두 동일하게 레이블 2를 가지고 있습니다. 이건 강력한 힌트입니다. 레이블이 2일 때 ‘nan’손실값이 발생한다는 사실은 우리 모델의 레이블 수를 확인하기에 아주 좋은 시기임을 뜻합니다.:

    model.config.num_labels

    2

이제 문제가 보입니다. 모델은 클래스가 두 개뿐이라고 생각하지만 레이블이 최대 2까지 갑니다. 이는 실제로 세 개의 클래스가 있다는 것을 의미합니다(0도 클래스이기 때문에). 이게 `nan`값이 발생한 이유 — 존재하지 않는 클래스에 대한 손실을 계산하려고 시도 했기 때문입니다! 이를 변경하고 모델을 다시 피팅해 보겠습니다.:

    model = TFAutoModelForSequenceClassification.from_pretrained(model_checkpoint, num_labels=3)
    model.compile(optimizer='adam')
    model.fit(train_dataset)

      869/24543 [>.............................] - ETA: 15:29 - loss: 1.1032

학습이 진행되는군요! 더이상 `nan` 없으며, 손실값이 감소하는 듯 보입니다… 한참을 보다보면 손실값이 완고하게 높은 상태를 유지하기 때문에 조금 조바심이 나기 시작할지도 모릅니다. 여기서 학습을 중단하고 이 문제의 원인이 무엇인지 생각해 보겠습니다. 이 시점에서 우리는 데이터와 모델이 모두 괜찮다고 확신하지만 우리 모델은 잘 학습하지 못하고 있습니다. 무엇이 더 남았을까요? 그건 바로…

###
하이퍼파라미터 확인

위의 코드를 다시 보면 `batch_size`를 제외하고는 하이퍼파라미터를 전혀 볼 수 없는데, 이것이 범인일 가능성은 없어 보입니다. 하지만 속지 마세요. 항상 하이퍼파라미터가 있어야 하며, 이를 볼 수 없다면 이는 단지 하이퍼파라미터가 무엇으로 설정되어 있는지 모른다는 것을 의미합니다. 특히 Keras에 대한 중요한 점을 기억하세요. 손실 함수, 옵티마이저 함수 또는 활성화 함수를 문자열로 설정하면 모든 인수가 기본값으로 설정됩니다. 즉 문자열을 사용하는 것이 매우 편리하더라도 심각한 이슈 쉽게 숨길 수 있으므로 그렇게 할 때 매우 조심해야 함을 의미합니다. (이러한 선택적인 도전을 시도하는 사람은 이 사실을 염두해야 합니다.)

이 경우 문자열로 인수를 설정한 위치가 어디일까요? 처음에는 문자열로 손실 함수를 설정했지만 더 이상 그렇게 하지 않습니다. 다시보니 옵티마이저를 문자열로 설정하고 있습니다. 이게 어떤 것을 숨기고 있을까요? 옵티마이저의 인수를 살펴보겠습니다.

눈에 띄는 것이 있었나요? 맞습니다. 학습률입니다! 문자열 `'adam'`을 사용하면 기본 학습률이 0.001 또는 1e-3이 됩니다. 트랜스포머 모델에게는 상당히 높은 값입니다. 일반적으로 트랜스포머 모델 계열은 1e-5와 1e-4 사이의 학습률을 시도하는 것이 좋습니다. 이 값은 우리가 여기서 실제로 사용한 학습률보다 10배에서 100배 사이에 있습니다. 큰 문제가 될 것 같으니 줄여보겠습니다. 그렇게 하려면 실제 ‘optimizer’ 객체를 가져와야 합니다. 학습 속도가 높은 훈련이 가중치를 손상시키는 경우를 대비하여 체크포인트에서 모델을 다시 초기화 하겠습니다.:

    from tensorflow.keras.optimizers import Adam

    model = TFAutoModelForSequenceClassification.from_pretrained(model_checkpoint)
    model.compile(optimizer=Adam(5e-5))

🤗 Transformers에서 `create_optimizer()` 함수를 가져올 수도 있습니다. 이렇게 하면 적합한 가중치 감쇠(weight decay)와 학습률 워밍업과 감쇠가 포함된 AdamW 옵티마이저가 제공됩니다. 이 옵티마이저는 기본 Adam 옵티마이저로 얻은 결과보다 약간 더 나은 결과를 만들어내는 경우가 많습니다.

이제 새롭고, 개선된 학습률로 모델 학습에 도전해보겠습니다.:

    model.fit(train_dataset)

    319/24543 [..............................] - ETA: 16:07 - loss: 0.9718

이제 손실값은 정말로 움직이고 있습니다! 학습이 마침내 효과가 있는 것 같습니다. 여기에는 교훈이 있습니다. 모델이 실행 중이지만 손실값이 감소하지 않고 데이터가 괜찮다고 확신하는 경우 학습률 및 가중치 감쇠와 같은 하이퍼파라미터를 확인하는 것이 좋습니다. 둘 중 하나를 너무 높게 설정하면 학습이 높은 손실 값에서 “정지”될 가능성이 매우 높습니다.

##
다른 잠재적 이슈들

위의 스크립트에서 문제를 다루었지만 직면할 수 있는 여러가지 다른 일반적인 오류가 있습니다. (매우 불완전한) 목록을 살펴보겠습니다.

###
메모리 부족 에러 처리

메모리 부족을 알리는 신호는 “OOM when allocating tensor”와 같은 에러입니다. OOM은 “out of memory”의 줄임말입니다. 이것은 큰 언어 모델을 다룰 때 매우 흔한 위험입니다. 이 문제가 발생하면 배치 크기를 절반으로 줄이고 다시 시도하는 것이 좋습니다. 그러나 일부 모델은 매우 큽니다. 예를 들어, 최대크기 GPT-2는 1.5B개의 매개변수가 있습니다. 즉, 모델을 저장하는 데만 6GB의 메모리가 필요하고 그라디언트에는 6GB가 추가로 필요합니다! 최대 크기 GPT-2 모델을 학습하려면 사용하는 배치 크기에 관계없이 일반적으로 20GB 이상의 VRAM이 필요하며, 소수의 GPU만 해당합니다. ‘distilbert-base-cased’와 같은 더 가벼운 모델은 실행하기가 훨씬 쉽고 훨씬 빠르게 학습할 수 있습니다.

다음 장에서는 메모리 사용량을 줄이고 가장 큰 모델을 파인튜닝 할 수 있는 고급 기술을 살펴보겠습니다.

###
몹시 배고픈 TensorFlow 🦛

TensorFlow의 특별한 특징 중 하나는 모델을 로드하거나 학습을 수행하는 즉시 GPU 메모리의 모든을 자체적으로 할당한 다음 필요에 따라 해당 메모리를 분할한다는 것입니다. 이것은 PyTorch와 같은 다른 프레임워크의 동작과 다른데, 이는 내부적으로 수행하지 않고 CUDA에서 필요에 따라 메모리를 할당합니다. TensorFlow 접근 방식의 한 가지 장점은 메모리가 부족할 때 종종 유용한 오류를 제공할 수 있고 전체 CUDA 커널을 충돌시키지 않고 해당 상태에서 복구할 수 있다는 것입니다. 그러나 중요한 단점도 있습니다. 한 번에 두 개의 TensorFlow 프로세스를 실행하면 나쁜 시간을 보내게 됩니다.

Colab에서 실행하는 경우에는 이에 대해 걱정할 필요가 없지만 로컬에서 실행하는 경우에는 확실히 주의해야 합니다. 특히 노트북(주피터 노트북 등) 탭을 닫아도 해당 노트북이 반드시 종료되는 것은 아닙니다. 실행 중인 노트북(녹색 아이콘이 있는 노트북)을 선택하고 디렉토리 목록에서 수동으로 종료해야 할 수도 있습니다. TensorFlow를 사용하는 실행 중인 노트북은 여전히 ​​많은 GPU 메모리를 보유하고 있을 수 있으며, 이는 새로 시작하는 노트북에서 매우 이상한 문제가 발생할 수 있음을 의미합니다.

이전에 작동했던 코드에서 CUDA, BLAS 또는 cuBLAS에 대한 오류가 발생하기 시작하면 이 현상이 범인인 경우가 많습니다. `nvidia-smi`와 같은 명령을 사용하여 현재 노트북을 종료하거나 다시 시작할 때 대부분의 메모리가 사용 가능한지, 아니면 여전히 사용 중인지 확인할 수 있습니다. 아직 사용 중이라면 다른 무언가가 GPU를 붙잡고 있는 것입니다!

###
데이터 확인 (다시!)

모델은 데이터에서 실제로 무엇이든 학습할 수 있는 경우에만 무언가를 학습합니다. 데이터를 손상시키는 버그가 있거나 레이블이 무작위로 지정된 경우 데이터 세트에 대해 모델이 학습을 하지 못할 가능성이 매우 높습니다. 여기에서 유용한 도구 중 하나는 `tokenizer.decode()`입니다. 이렇게 하면 `input_ids`가 다시 문자열로 바뀌므로 데이터를 보고 학습 데이터가 원하는 내용을 가르치는지 확인할 수 있습니다. 예를 들어, 위에서 했던 것처럼 `tf.data.Dataset`에서 `batch`를 얻은 후 다음과 같이 첫 번째 요소를 디코딩할 수 있습니다.:

    input_ids = batch["input_ids"].numpy()
    tokenizer.decode(input_ids[0])

그리고 첫번째 레이블과 함께 다음처럼 비교해볼 수 있습니다.:

    labels = batch["labels"].numpy()
    label = labels[0]

이처럼 데이터를 볼 수 있게 되면 스스로에게 다음과 같은 질문을 할 수 있습니다.:

- 디코드된 데이터가 이해할만한가?
- 디코드된 데이터에 대한 레이블이 적합한가?
- 다른 레이블보다 더 일반적인 레이블이 있는가?
- 모델이 임의의 답변을 내거나, 같은 답변만 한다면 어떤 손실/평가 함수를 사용해야 할까?

데이터를 살펴본 후 모델의 예측을 몇 가지 살펴보세요. 모델이 토큰을 출력하는 경우 디코딩도 시도해 보세요. 모델이 항상 동일한 것을 예측하는 경우 데이터 세트가 하나의 범주(분류 문제의 경우)로 편향되어 있으므로 희귀 클래스를 오버샘플링하는 것과 같은 기술이 도움이 될 수 있습니다. 또는 잘못된 하이퍼파라미터 설정과 같은 학습 문제로 인해 발생할 수도 있습니다.

학습 전에 초기 모델에서 얻은 손실값/평가값이 무작위 예측에 대해 예상하는 손실값/평가값과 매우 다른 경우 버그가 있을 수 있으므로 손실값 또는 평가값이 계산되는 방식을 다시 확인하세요. 마지막에 여러 손실함수를 추가해 사용하는 경우 동일한 스케일의 값인지 확인하세요.

데이터가 완벽하다고 확신한다면 한 번의 간단한 테스트로 모델이 데이터를 학습할 수 있는지 확인할 수 있습니다.

###
한 배치에서 모델 과대적합

과적합은 일반적으로 학습할 때 피하려고 합니다. 이는 모델이 우리가 원하는 일반적인 기능을 인식하는 것을 학습하지 않고 대신 학습 샘플을 기억한다는 것을 의미하기 때문입니다. 그러나 한 배치에서 모델을 계속해서 훈련시키려고 시도하는 것은 학습하려는 모델로 우리가 구성한 문제를 해결할 수 있는지 확인하는 좋은 테스트입니다. 또한 초기 학습률이 너무 높은지 확인하는 데 도움이 됩니다.

‘모델’을 정의한 후에 이 작업을 수행하는 것은 정말 쉽습니다. 학습 데이터 배치를 가져온 다음 해당 `배치`를 전체 데이터 세트로 처리하고 여러 에포크에 걸쳐 학습합니다.:

    for batch in train_dataset:
        break

    # Make sure you have run model.compile() and set your optimizer,
    # and your loss/metrics if you're using them

    model.fit(batch, epochs=20)

💡 학습 데이터가 불균형한 경우 모든 레이블을 포함하는 학습 데이터 배치를 구성해야 합니다.

결과 모델은 `배치`에서 완벽에 가까운 결과를 가져야 하며 손실은 0(또는 사용 중인 손실의 최소값)으로 빠르게 감소합니다.

모델이 이와 같은 완벽한 결과를 얻도록 관리하지 못한다면 문제 또는 데이터를 구성하는 방식에 문제가 있음을 의미하므로 수정해야 합니다. 과적합 테스트를 통과해야만 모델이 실제로 무언가를 학습할 수 있다고 확신할 수 있습니다.

⚠️ 과적합 테스트 후에 모델을 다시 만들고 다시 컴파일해야 합니다. 학습 모델이 전체 데이터 세트에서 유용한 것을 복구하고 학습할 수 없기 때문입니다.

###
첫 번째 기준이 생길 때까지 아무 것도 조정하지 마세요.

강렬한 하이퍼파라미터 조정은 항상 기계 학습의 가장 어려운 부분으로 강조되지만 지표에서 약간의 이득을 얻는 데 도움이 되는 마지막 단계일 뿐입니다. Transformer 모델과 함께 기본 Adam 학습률 1e-3을 사용하는 것과 같이 하이퍼파라미터에 대한 매우 나쁜 값은 학습 진행을 매우 느리게 또는 완전히 중단하게 만들지만 대부분의 경우 다음과 같은 “합리적인” 하이퍼파라미터 1e-5에서 5e-5까지의 학습률을 사용하면 좋은 결과를 얻을 수 있습니다. 따라서 데이터 세트의 기준을 능가하는 결과를 얻을 때까지 시간과 비용이 많이 드는 하이퍼파라미터 검색을 시작하지 마세요.

적당한 모델을 갖고나면 약간 조정을 시작할 수 있습니다. 다른 하이퍼파라미터로 천 개의 실행을 시작하려고 하지 말고 하나의 하이퍼파라미터에 대해 다른 값을 가진 몇 개의 실행을 비교하여 가장 큰 영향력을 미치는 아이디어를 얻어보세요.

모델 자체를 조정하는 경우 단순하게 유지하고 합리적으로 정당화할 수 없는 것은 시도하지 마세요. 항상 과적합 테스트로 돌아가서 변경 사항이 의도하지 않은 결과를 초래하지 않았는지 확인하세요.

###
도움 요청하기

이 단원에서 문제를 해결하는 데 도움이 되는 몇 가지 조언을 찾았으면 좋겠지만, 그렇지 않은 경우 포럼에서 커뮤니티에 언제든지 질문할 수 있음을 기억하세요.

다음은 도움이 될 수 있는 몇 가지 추가 자료입니다.:

- “Reproducibility as a vehicle for engineering best practices” by Joel Grus
- “Checklist for debugging neural networks” by Cecelia Shao
- “How to unit test machine learning code” by Chase Roberts
- “A Recipe for Training Neural Networks” by Andrej Karpathy

물론 신경망을 훈련할 때 발생하는 모든 문제가 자신의 잘못은 아닙니다! 🤗 Transformers 또는 🤗 Datasets 라이브러리에서 이상한 것을 본다면 버그가 발생했을 수 있습니다. 버그를 봤을 경우 우리에게 상세하게 말해야 하며, 다음 섹션에서 그 방법을 정확히 설명할 것입니다.

< > Update on GitHub