Closed4

今更ながら「Transformers」に入門する ④TUTORIALS: Preprocess

kun432kun432

チュートリアル: Preprocess

https://huggingface.co/docs/transformers/v4.47.1/ja/preprocessing

入力がテキスト・画像・オーディオのどれであれ、モデルに渡す前に、モデルが期待している形式、つまりテンソルのバッチに変換する必要がある。これが前処理。

  • テキスト: トークナイザー(Tokenizer)を使って、テキストを、トークンのシーケンスに変換、トークンの数値表現を作成、テンソルに変換
  • 音声: 特徴抽出器(Feature extractor)を使って、音声波形データから連続的な特徴を抽出、テンソルに変換
  • 画像: 画像処理プロセッサ(Image Processor)を使って、画像データをテンソルに変換
  • マルチモーダル: Processorを使用して、トークナイザと特徴抽出器、トークナイザと画像プロセッサ、のように複数の前処理を行う

事前準備

Colaboratory T4で。

!pip install transformers datasets evaluate accelerate
!pip install torch
!pip freeze | egrep -i "transformers|datasets|evaluate|accelerate|torch"
出力
accelerate==1.2.1
datasets==3.2.0
evaluate==0.4.3
sentence-transformers==3.3.1
tensorflow-datasets==4.9.7
torch @ https://download.pytorch.org/whl/cu121_full/torch-2.5.1%2Bcu121-cp310-cp310-linux_x86_64.whl
torchaudio @ https://download.pytorch.org/whl/cu121/torchaudio-2.5.1%2Bcu121-cp310-cp310-linux_x86_64.whl
torchsummary==1.5.1
torchvision @ https://download.pytorch.org/whl/cu121/torchvision-0.20.1%2Bcu121-cp310-cp310-linux_x86_64.whl
transformers==4.47.1
vega-datasets==0.9.0

NLP(自然言語処理)

テキストデータの前処理はトークナイザを使う。トークナイザは入力されたテキストに対して

  • 一連のルールに従ってトークンに分割
  • トークンを数値表現に変換
  • テンソルに変換

を行ってモデルに渡す。モデルが追加の入力を必要とする場合も、トークナイザがこれを追加する。

例として以下の感情分析用モデルを使う

https://huggingface.co/christian-phu/bert-finetuned-japanese-sentiment

このモデルはfugashiとunidic_liteが必要

!pip install fugashi unidic_lite

事前学習済みトークナイザをロードする。これにより、入力するテキストは、事前学習モデルが使用した学習データと同じルールで分割され、そして事前学習モデルと同じボキャブ(トークンと数値インデックスのマッピング)で、処理されることになる。

from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("christian-phu/bert-finetuned-japanese-sentiment")

テキストをトークナイザに渡す

encoded_input = tokenizer("魔法使いの問題に干渉してはなりません、というのもの、彼らは繊細ですぐに怒るからです。")
print(encoded_input)
出力
{
    'input_ids': [2, 12356, 12388, 896, 11490, 893, 18032, 873, 888, 897, 11218, 15583, 933, 828, 890, 11163, 896, 11171, 828, 2284, 923, 897, 29514, 889, 12550, 893, 29958, 11142, 12461, 829, 3],
    'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
}

トークナイザが返すのは以下の3つ。

  • input_ids: 文中の各トークンに対応するインデックス。
  • attention_mask: トークンがアテンションを受ける必要があるかどうかを示す。
  • token_type_ids: 複数のシーケンスがある場合、トークンがどのシーケンスに属しているかを識別する

input_idsをデコードすると入力したテキストが得られる。

tokenizer.decode(encoded_input["input_ids"])
出力
[CLS] 魔法 使い の 問題 に 干渉 し て は なり ませ ん 、 と いう の もの 、 彼 ら は 繊細 で すぐ に 怒る から です 。 [SEP]

入力した文章がスペースで区切られていて、トークンに分割されているのがわかる。また、[CLS](¥クラシファイア)や[SEP](セパレータ)などの特殊トークンが付与されているのがわかる。

複数のテキストの場合でも同じ。

batch_sentences = [
    "でも、2回目の朝食はどうするの?",
    "彼は2回目の朝食について知らないと思うよ、ピップ。",
    "では、11時のおやつはどう?",
]
encoded_inputs = tokenizer(batch_sentences)
print(encoded_inputs)
出力
{
    'input_ids': [
        [2, 889, 916, 828, 32, 1708, 3803, 896, 2821, 6521, 897, 12112, 11137, 896, 45, 3],
        [2, 2284, 897, 32, 1708, 3803, 896, 2821, 6521, 893, 11237, 888, 11464, 11148, 890, 15464, 922, 828, 990, 11203, 829, 3],
        [2, 889, 897, 828, 11184, 2754, 896, 25413, 6393, 897, 12112, 45, 3]
    ],
    'token_type_ids': [
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
    ],
    'attention_mask': [
        [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
        [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
        [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
    ]
}
[tokenizer.decode(i) for i in encoded_inputs["input_ids"]]
出力
[
    '[CLS] で も 、 2 回 目 の 朝食 は どう する の ? [SEP]',
    '[CLS] 彼 は 2 回 目 の 朝食 に つい て 知ら ない と 思う よ 、 ピップ 。 [SEP]',
    '[CLS] で は 、 11 時 の おやつ は どう ? [SEP]'
]

パディング

モデルへの入力テンソルは同じ長さである必要があるため、短い文章には「パディングトークン」を追加して長い文章に合わせる。一つ上の例では複数の文章をトークナイザに渡しているが、長さが異なっているのがわかると思う。

padding=Trueを付与する

batch_sentences = [
    "でも、2回目の朝食はどうするの?",
    "彼は2回目の朝食について知らないと思うよ、ピップ。",
    "では、11時のおやつはどう?",
]
encoded_inputs = tokenizer(batch_sentences, padding=True)
print(encoded_inputs)
出力
{
    'input_ids': [
        [2, 889, 916, 828, 32, 1708, 3803, 896, 2821, 6521, 897, 12112, 11137, 896, 45, 3, 0, 0, 0, 0, 0, 0],
        [2, 2284, 897, 32, 1708, 3803, 896, 2821, 6521, 893, 11237, 888, 11464, 11148, 890, 15464, 922, 828, 990, 11203, 829, 3],
        [2, 889, 897, 828, 11184, 2754, 896, 25413, 6393, 897, 12112, 45, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0]
    ],
    'token_type_ids': [
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
    ],
    'attention_mask': [
        [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0],
        [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
        [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]
    ]
}

短い文章は0で埋められて長さがおなじになっているのがわかる。

切り詰め

入力データが、モデルが処理できる長さよりも逆に長い場合はこれを切り詰める必要がある。truncation=Trueを指定する。

batch_sentences = [
    "でも、2回目の朝食はどうするの?",
    "彼は2回目の朝食について知らないと思うよ、ピップ。",
    "では、11時のおやつはどう?",
]
encoded_inputs = tokenizer(batch_sentences, padding=True, truncation=True)
print(encoded_inputs)

ただし上記の例だと以下のようなwarningが出る。

出力
Asking to truncate to max_length but no maximum length is provided and the model has no predefined maximum length. Default to no truncation.

max_lengthを設定してみる。

batch_sentences = [
    "でも、2回目の朝食はどうするの?",
    "彼は2回目の朝食について知らないと思うよ、ピップ。",
    "では、11時のおやつはどう?",
]
encoded_inputs = tokenizer(batch_sentences, padding=True, truncation=True, max_length=22)
print(encoded_inputs)
出力
{
    'input_ids': [
        [2, 889, 916, 828, 32, 1708, 3803, 896, 2821, 6521, 897, 12112, 11137, 896, 45, 3, 0, 0, 0, 0],
        [2, 2284, 897, 32, 1708, 3803, 896, 2821, 6521, 893, 11237, 888, 11464, 11148, 890, 15464, 922, 828, 990, 3],
        [2, 889, 897, 828, 11184, 2754, 896, 25413, 6393, 897, 12112, 45, 3, 0, 0, 0, 0, 0, 0, 0]
    ],
    'token_type_ids': [
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
    ],
    'attention_mask': [
        [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0],
        [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
        [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0]
    ]
}

テンソルへの変換

return_tensors="pt"でPyTorch用テンソルで返す。TensorFlowの場合は"tf"になる。

batch_sentences = [
    "でも、2回目の朝食はどうするの?",
    "彼は2回目の朝食について知らないと思うよ、ピップ。",
    "では、11時のおやつはどう?",
]
encoded_inputs = tokenizer(batch_sentences, padding=True, truncation=True, return_tensors="tf")
print(encoded_inputs)
出力
{'input_ids': <tf.Tensor: shape=(3, 22), dtype=int32, numpy=
array([[    2,   889,   916,   828,    32,  1708,  3803,   896,  2821,
         6521,   897, 12112, 11137,   896,    45,     3,     0,     0,
            0,     0,     0,     0],
       [    2,  2284,   897,    32,  1708,  3803,   896,  2821,  6521,
          893, 11237,   888, 11464, 11148,   890, 15464,   922,   828,
          990, 11203,   829,     3],
       [    2,   889,   897,   828, 11184,  2754,   896, 25413,  6393,
          897, 12112,    45,     3,     0,     0,     0,     0,     0,
            0,     0,     0,     0]], dtype=int32)>, 'token_type_ids': <tf.Tensor: shape=(3, 22), dtype=int32, numpy=
array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
       [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
       [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]],
      dtype=int32)>, 'attention_mask': <tf.Tensor: shape=(3, 22), dtype=int32, numpy=
array([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0],
       [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
       [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]],
      dtype=int32)>}

音声

音声の前処理は、特徴抽出器を使って、入力された音声データから、特徴を抽出、テンソルに変換する。

以下のデータセットおよびモデルを使用して試してみる。

https://huggingface.co/datasets/reazon-research/reazonspeech

https://huggingface.co/NTQAI/wav2vec2-large-japanese

まず、データセットをダウンロード

from datasets import load_dataset, Audio

dataset = load_dataset("reazon-research/reazonspeech", "tiny", split="train[:5]", trust_remote_code=True)

データセットをピックアップしてみる。

dataset[:2]["audio"]
出力
[{'path': '/root/.cache/huggingface/datasets/downloads/extracted/c50543a6ab5c6805e43f77f8957ae81e61f5b17cecb476837fd0dca8789b0484/000/000734dcb35d6.flac',
  'array': array([-0.01309204, -0.01068115, -0.006073  , ...,  0.00613403,
          0.00558472,  0.00674438]),
  'sampling_rate': 16000},
 {'path': '/root/.cache/huggingface/datasets/downloads/extracted/c50543a6ab5c6805e43f77f8957ae81e61f5b17cecb476837fd0dca8789b0484/000/0024ae5c517e7.flac',
  'array': array([-0.04589844, -0.0730896 , -0.09643555, ..., -0.00741577,
          0.00476074,  0.01593018]),
  'sampling_rate': 16000}]

arrayに音声信号が入っている。今回はデータもモデルもサンプリングレートはともに16kHzが前提なのだが、サンプリングレートが異なる場合はリサンプリングが必要になる。

では特徴抽出器をロードする。

from transformers import AutoFeatureExtractor

feature_extractor = AutoFeatureExtractor.from_pretrained("NTQAI/wav2vec2-large-japanese")

arrayを特徴抽出器に渡す。

audio_input = [dataset[0]["audio"]["array"]]
feature_extractor(audio_input, sampling_rate=16000)
出力
{'input_values': [array([-0.39916036, -0.32565916, -0.18516946, ...,  0.18698859,
        0.17024149,  0.2055965 ], dtype=float32)], 'attention_mask': [array([1, 1, 1, ..., 1, 1, 1], dtype=int32)]}

バッチの場合、データごとに長さが異なる場合もある。テキストと同様にパディング・切り詰めを行うことができる。データを少し見てみる。

dataset[0]["audio"]["array"].shape
出力
(22291,)
dataset[1]["audio"]["array"].shape
出力
(122372,)

これらの長さを揃えるために、特徴抽出器にパディングや切り詰め用のパラメータを付与して入力をラップする関数を書く。

def preprocess_function(examples):
    audio_arrays = [x["array"] for x in examples["audio"]]
    inputs = feature_extractor(
        audio_arrays,
        sampling_rate=16000,
        padding=True,
        max_length=100000,
        truncation=True,
    )
    return inputs

これを使って前処理する。

processed_dataset = preprocess_function(dataset)

すべてのデータに対して前処理が行われて同じ長さになっているのがわかる。

[i.shape for i in processed_dataset["input_values"]]
出力
[(100000,), (100000,), (100000,), (100000,), (100000,)]
[i[-10:] for i in processed_dataset["input_values"]]

短いものにはパディングが付与され、長いものは切り詰められていることもわかる。

出力
[array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], dtype=float32),
 array([ 0.13287929,  0.06139416,  0.10599599,  0.11577173,  0.1133278 ,
         0.04000972, -0.00031522,  0.04550857,  0.0332889 ,  0.10110812],
       dtype=float32),
 array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], dtype=float32),
 array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], dtype=float32),
 array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], dtype=float32)]

画像

画像の前処理は、画像プロセッサを使って、リサイズ・正規化などを行った上でテンソルに変換する。

以下のデータセットを使用する。

https://huggingface.co/datasets/ethz/food101

from datasets import load_dataset

dataset = load_dataset("food101", split="train[:100]")

サンプルとしてピックアップ

dataset[0]["image"]

画像プロセッサは以下のモデルのものを使用する。

https://huggingface.co/google/vit-base-patch16-224

from transformers import AutoImageProcessor

image_processor = AutoImageProcessor.from_pretrained("google/vit-base-patch16-224")

で、ちょっとここの概念がイマイチ理解できていないのだけども、画像の前処理には「前処理」と「増強」の2つの概念があるみたい。

  • 前処理: モデルが期待する入力形式に画像を合わせる。モデルがトレーニングされたときと同じ方法で行う。
  • 増強: モデルの過学習を防ぎ、堅牢性を向上するために画像を変更する。増強には様々な方法があるが、画像の意味が変わらないようにする

んー、ちょっとピンとこない・・・ChatGPTに聞いてみた。

画像の前処理(preprocessing)と画像の増強(augmentation)の違いが少しわかりづらいのは、両方とも「画像を加工する」行為に見えるからです。しかし、それぞれの目的と使用タイミングは異なります。このチュートリアルが説明したいポイントを整理してみます。

1. 画像の前処理(Preprocessing)

  • 目的:モデルが期待する形式にデータを整えること。
  • 具体的な例:
    • 画像のリサイズ(例: 224×224ピクセルに統一)
    • ピクセル値の正規化(例: [0, 255] を [0, 1] または [-1, 1] に変換)
    • カラーチャネルの調整(例: RGB 形式に統一)
    • テンソル化(PyTorchやTensorFlowで扱える形式に変換)
  • 目的のポイント: モデルが入力として扱える形式に整えることが主な目的であり、データの一貫性を保証します。
  • 例外: 画像の意味を変える操作はしません(例えば、回転や明るさの変更は行わない)。

2. 画像の増強(Augmentation)

  • 目的: データの多様性を増やしてモデルの性能を向上させること(特に汎化性能)。
  • 具体的な例:
    • 画像の回転、ズーム、ランダムクロップ
    • 明るさや色相のランダム変更
    • フリップ(左右反転)
    • ノイズの追加
  • 目的のポイント: モデルが様々な条件に対して強くなるように、入力データをランダムに変更します。これにより、過学習を防ぎます。
  • 注意: 増強の際は、画像の意味(ラベル)が変わらないことを保証する必要があります。

ふむ・・・「増強」は学習の場合の話かな?という気がする。とりあえずやってみる。

from torchvision.transforms import RandomResizedCrop, ColorJitter, Compose

size = (
    image_processor.size["shortest_edge"]
    if "shortest_edge" in image_processor.size
    else (image_processor.size["height"], image_processor.size["width"])
)

_transforms = Compose([RandomResizedCrop(size), ColorJitter(brightness=0.5, hue=0.5)])
  • 画像のリサイズに必要なサイズを取得
  • 増強: 画像をランダムにクロップしてリサイズ
  • 増強: 画像の明るさと色相をランダムに調整
  • 増強を連結

という感じらしい。

で次に、増強と前処理を組み合わせ。

def transforms(examples):
    images = [_transforms(img.convert("RGB")) for img in examples["image"]]
    examples["pixel_values"] = image_processor(images, do_resize=False, return_tensors="pt")["pixel_values"]
    return examples

dataset.set_transform(transforms)
  • _transformsで画像をランダムに増強
  • 増強済みの画像に対して、image_processorで正規化してテンソル化。
    • リサイズは増強でやっているので、前処理ではリサイズしない(do_resize=False
  • 上記の結果をpixel_valuesという新しいフィールドとして追加
  • データセット全体に適用

確認

dataset[0].keys()

pixel_valuesが追加されている

出力
dict_keys(['image', 'label', 'pixel_values'])

処理結果を見てみる。

import numpy as np
import matplotlib.pyplot as plt

img = dataset[0]["pixel_values"]
plt.imshow(img.permute(1, 2, 0))

再度実行すると以下のように異なる画像になる。

なるほど、これで画像の多様性を増やして汎化性能を上げるということね。

パディング

通常、モデルは固定サイズの入力(例: 224x224ピクセルなど)を期待しているが、DETR(オブジェクト検出モデル)など一部のモデルでは、あえて異なるサイズで学習を行うことがある。その場合に、テンソルの形が一致しなくなってしまうため、学習時にエラーになってしまう。これを避けるために小さい画像に余白となるパディングを追加して同じサイズにする事ができる。

ここは割愛。

マルチモーダル

マルチモーダルでは、複数のモダリティごとに前処理を行うことになる。ここはちょっと割愛。

kun432kun432

ちょっとQuick Tourとかと重複している箇所もあったかな。

あと、なるべく日本語が扱えるモデルで試したかったけど、良いサンプルを作れなかった。

このスクラップは5ヶ月前にクローズされました