今更ながら「Transformers」に入門する ④TUTORIALS: Preprocess
以下の続き
チュートリアル: Preprocess
入力がテキスト・画像・オーディオのどれであれ、モデルに渡す前に、モデルが期待している形式、つまりテンソルのバッチに変換する必要がある。これが前処理。
- テキスト: トークナイザー(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(自然言語処理)
テキストデータの前処理はトークナイザを使う。トークナイザは入力されたテキストに対して
- 一連のルールに従ってトークンに分割
- トークンを数値表現に変換
- テンソルに変換
を行ってモデルに渡す。モデルが追加の入力を必要とする場合も、トークナイザがこれを追加する。
例として以下の感情分析用モデルを使う
このモデルは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)>}
音声
音声の前処理は、特徴抽出器を使って、入力された音声データから、特徴を抽出、テンソルに変換する。
以下のデータセットおよびモデルを使用して試してみる。
まず、データセットをダウンロード
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)]
画像
画像の前処理は、画像プロセッサを使って、リサイズ・正規化などを行った上でテンソルに変換する。
以下のデータセットを使用する。
from datasets import load_dataset
dataset = load_dataset("food101", split="train[:100]")
サンプルとしてピックアップ
dataset[0]["image"]
画像プロセッサは以下のモデルのものを使用する。
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(オブジェクト検出モデル)など一部のモデルでは、あえて異なるサイズで学習を行うことがある。その場合に、テンソルの形が一致しなくなってしまうため、学習時にエラーになってしまう。これを避けるために小さい画像に余白となるパディングを追加して同じサイズにする事ができる。
ここは割愛。
マルチモーダル
マルチモーダルでは、複数のモダリティごとに前処理を行うことになる。ここはちょっと割愛。
ちょっとQuick Tourとかと重複している箇所もあったかな。
あと、なるべく日本語が扱えるモデルで試したかったけど、良いサンプルを作れなかった。
学習系のチュートリアルが続くけど、一旦そこはパスで。
次