ホーム » HuggingFace Transformers » HuggingFace Transformers 4.17 : ガイド : 下流タスク用にモデルを再調整する方法

HuggingFace Transformers 4.17 : ガイド : 下流タスク用にモデルを再調整する方法

HuggingFace Transformers 4.17 : ガイド : 下流タスク用にモデルを再調整する方法 (翻訳/解説)

翻訳 : (株)クラスキャット セールスインフォメーション
作成日時 : 04/26/2022 (v4.17.0)

* 本ページは、HuggingFace Transformers の以下のドキュメントを翻訳した上で適宜、補足説明したものです:

* サンプルコードの動作確認はしておりますが、必要な場合には適宜、追加改変しています。
* ご自由にリンクを張って頂いてかまいませんが、sales-info@classcat.com までご一報いただけると嬉しいです。

 

クラスキャット 人工知能 研究開発支援サービス

クラスキャット は人工知能・テレワークに関する各種サービスを提供しています。お気軽にご相談ください :

◆ 人工知能とビジネスをテーマに WEB セミナーを定期的に開催しています。スケジュール
  • お住まいの地域に関係なく Web ブラウザからご参加頂けます。事前登録 が必要ですのでご注意ください。

お問合せ : 本件に関するお問い合わせ先は下記までお願いいたします。

  • 株式会社クラスキャット セールス・マーケティング本部 セールス・インフォメーション
  • sales-info@classcat.com  ;  Web: www.classcat.com  ;   ClassCatJP

 

HuggingFace Transformers : ガイド : 下流タスク用にモデルを再調整する方法

このガイドは一般的な下流タスクに対して Transformers モデルを再調整する方法を示します。データセットを素早くロードして前処理するために Datasets ライブラリを使用し、PyTorch と TensorFlow による訓練のためにそれらを準備します。

始める前に、 Datasets ライブラリがインストールされていることを確認してください。詳細なインストール手順については、 Datasets インストール・ページ を参照してください。このガイドのサンプルの総てはデータセットをロードして前処理するために を使用します。

pip install datasets

以下のためのモデルを再調整する方法を学習します :

 

IMDb レビューによるシークエンス分類

シークエンス分類は与えられた数のクラスに従ってテキストのシークエンスを分類するタスクを指します。この例では、レビューがポジティブかネガティブかを決定するために IMDb データセット でモデルを再調整する方法を学習します。

Note : テキスト分類のためにモデルを再調整する方法の詳細なサンプルについては、対応する PyTorch ノートブックTensorFlow ノートブック を見てください。

 

IMDb データセットのロード

Datasets ライブラリはデータセットのロードを簡単します :

from datasets import load_dataset

imdb = load_dataset("imdb")

これは DatasetDict オブジェクトをロードします、これに対してサンプルを見るためにインデックスできます :

imdb["train"][0]
{
    "label": 1,
    "text": "Bromwell High is a cartoon comedy. It ran at the same time as some other programs about school life, such as \"Teachers\". My 35 years in the teaching profession lead me to believe that Bromwell High's satire is much closer to reality than is \"Teachers\". The scramble to survive financially, the insightful students who can see right through their pathetic teachers' pomp, the pettiness of the whole situation, all remind me of the schools I knew and their students. When I saw the episode in which a student repeatedly tried to burn down the school, I immediately recalled ......... at .......... High. A classic line: INSPECTOR: I'm here to sack one of your teachers. STUDENT: Welcome to Bromwell High. I expect that many adults of my age think that Bromwell High is far fetched. What a pity that it isn't!",
}

 

前処理

次のステップはテキストをモデルにより可読な形式にトークン化することです。適切にトークン化された単語を確実にするためにモデルがそれで訓練されたのと同じトークナイザーをロードすることは重要です。AutoTokenizer で DistilBERT トークナイザーをロードします、何故ならば結局は、事前訓練済み DistilBERT モデルを使用して分類器を訓練するからです。

from transformers import AutoTokenizer

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

トークナイザーをインスタンス化したので、テキストをトークン化する関数を作成します。また、テキストの長いシークエンスはモデルの最大入力長よりも長くならないように切り詰める必要があります :

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

前処理関数をデータセット全体に適用するために Datasets map 関数を使用します。より高速な前処理のためにデータセットの複数の要素に一度に前処理関数を適用するためには batched=True を設定することもできます :

tokenized_imdb = imdb.map(preprocess_function, batched=True)
type(tokenized_imdb), tokenized_imdb.keys()
(datasets.dataset_dict.DatasetDict, dict_keys(['train', 'test', 'unsupervised']))
print(tokenized_imdb['train'][0])
{'text': 'I rented I AM CURIOUS-YELLOW from my video store because of all the controversy that surrounded it when it was first released in 1967. I also heard that at first it was seized by U.S. customs if it ever tried to enter this country, therefore being a fan of films considered "controversial" I really had to see this for myself.

The plot is centered around a young Swedish drama student named Lena who wants to learn everything she can about life. In particular she wants to focus her attentions to making some sort of documentary on what the average Swede thought about certain political issues such as the Vietnam War and race issues in the United States. In between asking politicians and ordinary denizens of Stockholm about their opinions on politics, she has sex with her drama teacher, classmates, and married men.

What kills me about I AM CURIOUS-YELLOW is that 40 years ago, this was considered pornographic. Really, the sex and nudity scenes are few and far between, even then it\'s not shot like some cheaply made porno. While my countrymen mind find it shocking, in reality sex and nudity are a major staple in Swedish cinema. Even Ingmar Bergman, arguably their answer to good old boy John Ford, had sex scenes in his films.

I do commend the filmmakers for the fact that any sex shown in the film is shown for artistic purposes rather than just to shock people and make money to be shown in pornographic theaters in America. I AM CURIOUS-YELLOW is a good film for anyone wanting to study the meat and potatoes (no pun intended) of Swedish cinema. But really, this film doesn\'t have much of a plot.', 'label': 0, 'input_ids': [101, 1045, 12524, 1045, 2572, 8025, 1011, 3756, 2013, 2026, 2678, 3573, 2138, 1997, 2035, 1996, 6704, 2008, 5129, 2009, 2043, 2009, 2001, 2034, 2207, 1999, 3476, 1012, 1045, 2036, 2657, 2008, 2012, 2034, 2009, 2001, 8243, 2011, 1057, 1012, 1055, 1012, 8205, 2065, 2009, 2412, 2699, 2000, 4607, 2023, 2406, 1010, 3568, 2108, 1037, 5470, 1997, 3152, 2641, 1000, 6801, 1000, 1045, 2428, 2018, 2000, 2156, 2023, 2005, 2870, 1012, 1026, 7987, 1013, 1028, 1026, 7987, 1013, 1028, 1996, 5436, 2003, 8857, 2105, 1037, 2402, 4467, 3689, 3076, 2315, 14229, 2040, 4122, 2000, 4553, 2673, 2016, 2064, 2055, 2166, 1012, 1999, 3327, 2016, 4122, 2000, 3579, 2014, 3086, 2015, 2000, 2437, 2070, 4066, 1997, 4516, 2006, 2054, 1996, 2779, 25430, 14728, 2245, 2055, 3056, 2576, 3314, 2107, 2004, 1996, 5148, 2162, 1998, 2679, 3314, 1999, 1996, 2142, 2163, 1012, 1999, 2090, 4851, 8801, 1998, 6623, 7939, 4697, 3619, 1997, 8947, 2055, 2037, 10740, 2006, 4331, 1010, 2016, 2038, 3348, 2007, 2014, 3689, 3836, 1010, 19846, 1010, 1998, 2496, 2273, 1012, 1026, 7987, 1013, 1028, 1026, 7987, 1013, 1028, 2054, 8563, 2033, 2055, 1045, 2572, 8025, 1011, 3756, 2003, 2008, 2871, 2086, 3283, 1010, 2023, 2001, 2641, 26932, 1012, 2428, 1010, 1996, 3348, 1998, 16371, 25469, 5019, 2024, 2261, 1998, 2521, 2090, 1010, 2130, 2059, 2009, 1005, 1055, 2025, 2915, 2066, 2070, 10036, 2135, 2081, 22555, 2080, 1012, 2096, 2026, 2406, 3549, 2568, 2424, 2009, 16880, 1010, 1999, 4507, 3348, 1998, 16371, 25469, 2024, 1037, 2350, 18785, 1999, 4467, 5988, 1012, 2130, 13749, 7849, 24544, 1010, 15835, 2037, 3437, 2000, 2204, 2214, 2879, 2198, 4811, 1010, 2018, 3348, 5019, 1999, 2010, 3152, 1012, 1026, 7987, 1013, 1028, 1026, 7987, 1013, 1028, 1045, 2079, 4012, 3549, 2094, 1996, 16587, 2005, 1996, 2755, 2008, 2151, 3348, 3491, 1999, 1996, 2143, 2003, 3491, 2005, 6018, 5682, 2738, 2084, 2074, 2000, 5213, 2111, 1998, 2191, 2769, 2000, 2022, 3491, 1999, 26932, 12370, 1999, 2637, 1012, 1045, 2572, 8025, 1011, 3756, 2003, 1037, 2204, 2143, 2005, 3087, 5782, 2000, 2817, 1996, 6240, 1998, 14629, 1006, 2053, 26136, 3832, 1007, 1997, 4467, 5988, 1012, 2021, 2428, 1010, 2023, 2143, 2987, 1005, 1056, 2031, 2172, 1997, 1037, 5436, 1012, 102], '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, 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, 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, 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, 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, 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, 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, 1, 1, 1, 1, 1, 1]}

最後に、テキストをそれらが均一な長さになるようにパディングします。トークナイザーの関数で padding=True を設定することによりテキストをパディングすることが可能である一方で、テキストをそのバッチ内の最長要素の長さにパディングするだけのほうがより効率的です。これは 動的パディング として知られています。DataCollatorWithPadding 関数でこれを行なうことができます :

from transformers import DataCollatorWithPadding

data_collator = DataCollatorWithPadding(tokenizer=tokenizer)

 

Trainer API で再調整

次に AutoModelForSequenceClassification クラスのモデルを想定されるラベル数でロードします :

from transformers import AutoModelForSequenceClassification

model = AutoModelForSequenceClassification.from_pretrained("distilbert-base-uncased", num_labels=2)

この時点で、3 つのステップだけが残っています :

  1. TrainingArguments で訓練ハイパーパラメータを定義する。

  2. 訓練引数をモデル, データセット, トークナイザーとデータ collator と一緒に Trainer に渡します。

  3. モデルを再調整するために Trainer.train() を呼び出します。
from transformers import TrainingArguments, Trainer

training_args = TrainingArguments(
    output_dir="./results",
    learning_rate=2e-5,
    per_device_train_batch_size=16,
    per_device_eval_batch_size=16,
    num_train_epochs=5,
    weight_decay=0.01,
)

trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=tokenized_imdb["train"],
    eval_dataset=tokenized_imdb["test"],
    tokenizer=tokenizer,
    data_collator=data_collator,
)

trainer.train()

 

TensorFlow による再調整

(訳注: 原文 参照)

 

WNUT の新たに出現した (= emerging) エンティティによるトークン分類

トークン分類はセンテンスの個々のトークンを分類するタスクを指します。最も一般的なトークン分類タスクの一つは固有表現認識 (NER, Named Entity Recognition) です。NER は、人, 場所や組織のような、センテンスの各エンティティに対してラベルを見つけることを試みます。この例では、新しいエンティティを検出するために WNUT 17 データセットでモデルを再調整する方法を学習します。

Note : トークン分類のためにモデルを再調整する方法の詳細なサンプルについては、対応する PyTorch ノートブックTensorFlow ノートブック を見てください。

 

WNUT 17 データセットのロード

Datasets ライブラリから WNUT 17 データセットをロードします :

from datasets import load_dataset

wnut = load_dataset("wnut_17")

データセットを素早く見るとセンテンスの各単語に関連付けされたラベルが示されています :

wnut["train"][0]
{'id': '0',
 'ner_tags': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 8, 8, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0],
 'tokens': ['@paulwalk', 'It', "'s", 'the', 'view', 'from', 'where', 'I', "'m", 'living', 'for', 'two', 'weeks', '.', 'Empire', 'State', 'Building', '=', 'ESB', '.', 'Pretty', 'bad', 'storm', 'here', 'last', 'evening', '.']
}

次により固有の NER タグを閲覧します :

label_list = wnut["train"].features[f"ner_tags"].feature.names
label_list
[
    "O",
    "B-corporation",
    "I-corporation",
    "B-creative-work",
    "I-creative-work",
    "B-group",
    "I-group",
    "B-location",
    "I-location",
    "B-person",
    "I-person",
    "B-product",
    "I-product",
]

各 NER タグを prefix する文字は以下を意味します :

  • B- はエンティティの始まりを示します。

  • I- は、トークンが同じエンティティ内に含まれていることを示しています (e.g., State トークンは Empire State Building のようなエンティティの一部です)。

  • 0 はトークンがどのエンティティにも対応していないことを示します。

 

前処理

次にテキストをトークン化する必要があります。AutoTokenizer で DistilBERT トークナイザーをロードします :

from transformers import AutoTokenizer

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

入力は既に単語に分割されていますので、単語をサブワードにトークン化するために is_split_into_words=True を設定します :

#example = wnut["train"][0]

tokenized_input = tokenizer(example["tokens"], is_split_into_words=True)
tokens = tokenizer.convert_ids_to_tokens(tokenized_input["input_ids"])
tokens
['[CLS]', '@', 'paul', '##walk', 'it', "'", 's', 'the', 'view', 'from', 'where', 'i', "'", 'm', 'living', 'for', 'two', 'weeks', '.', 'empire', 'state', 'building', '=', 'es', '##b', '.', 'pretty', 'bad', 'storm', 'here', 'last', 'evening', '.', '[SEP]']

特殊トークン [CLS] と [SEP] の追加とサブワードのトークン化は入力とラベル間の不一致を起こします。以下によりラベルとトークンを再調整 (= realign) します :

  1. word_ids メソッドで総てのトークンを対応する単語にマップします。

  2. 特殊トークン [CLS] と [SEP] にラベル -100 を割当てると、PyTorch 損失関数はそれらを無視します。

  3. 与えられた単語の最初のトークンにだけラベル付けします。同じ単語の他のサブトークンには -100 を割当てます。

ここにラベルとトークンを再調整する関数を作成する方法があります :

def tokenize_and_align_labels(examples):
    tokenized_inputs = tokenizer(examples["tokens"], truncation=True, is_split_into_words=True)

    labels = []
    for i, label in enumerate(examples[f"ner_tags"]):
        word_ids = tokenized_inputs.word_ids(batch_index=i)  # Map tokens to their respective word.
        previous_word_idx = None
        label_ids = []
        for word_idx in word_ids:  # Set the special tokens to -100.
            if word_idx is None:
                label_ids.append(-100)
            elif word_idx != previous_word_idx:  # Only label the first token of a given word.
                label_ids.append(label[word_idx])
            else:
                label_ids.append(-100)
            previous_word_idx = word_idx
        labels.append(label_ids)

    tokenized_inputs["labels"] = labels
    return tokenized_inputs

そして Datasets map 関数でデータセット全体に対してラベルをトークン化してアラインします :

tokenized_wnut = wnut.map(tokenize_and_align_labels, batched=True)

最後に、テキストとラベルをそれらが均一な長さになるようにパディングします :

from transformers import DataCollatorForTokenClassification

data_collator = DataCollatorForTokenClassification(tokenizer)

 

Trainer API で再調整する

想定されるラベル数と共に AutoModelForTokenClassification クラスでモデルをロードします :

from transformers import AutoModelForTokenClassification, TrainingArguments, Trainer

model = AutoModelForTokenClassification.from_pretrained("distilbert-base-uncased", num_labels=len(label_list))

TrainingArguments 内に訓練引数を集めます :

training_args = TrainingArguments(
    output_dir="./results",
    evaluation_strategy="epoch",
    learning_rate=2e-5,
    per_device_train_batch_size=16,
    per_device_eval_batch_size=16,
    num_train_epochs=3,
    weight_decay=0.01,
)

モデル, 訓練引数, データセット, データ collator, そしてトークナイザーを Trainer に集めます :

trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=tokenized_wnut["train"],
    eval_dataset=tokenized_wnut["test"],
    data_collator=data_collator,
    tokenizer=tokenizer,
)

モデルを再調整します :

trainer.train()

 

TensorFlow による再調整

(訳注: 原文 参照)

 

SQuAD による質問応答

多くのタイプの質問応答 (QA) タスクがあります。Extractive QA は質問が与えられたときテキストから答えを識別することにフォーカスします。この例では、SQuAD データセットでモデルを再調整する方法を学習します。

Note : 質問応答のためにモデルを再調整する方法の詳細なサンプルについては、対応する PyTorch ノートブックTensorFlow ノートブック を見てください。

 

SQuAD データセットのロード

Datasets ライブラリから SQuAD データセットをロードします :

from datasets import load_dataset

squad = load_dataset("squad")

データセットのサンプルを見ましょう :

squad["train"][0]
{'answers': {'answer_start': [515], 'text': ['Saint Bernadette Soubirous']},
 'context': 'Architecturally, the school has a Catholic character. Atop the Main Building\'s gold dome is a golden statue of the Virgin Mary. Immediately in front of the Main Building and facing it, is a copper statue of Christ with arms upraised with the legend "Venite Ad Me Omnes". Next to the Main Building is the Basilica of the Sacred Heart. Immediately behind the basilica is the Grotto, a Marian place of prayer and reflection. It is a replica of the grotto at Lourdes, France where the Virgin Mary reputedly appeared to Saint Bernadette Soubirous in 1858. At the end of the main drive (and in a direct line that connects through 3 statues and the Gold Dome), is a simple, modern stone statue of Mary.',
 'id': '5733be284776f41900661182',
 'question': 'To whom did the Virgin Mary allegedly appear in 1858 in Lourdes France?',
 'title': 'University_of_Notre_Dame'
}

 

前処理

AutoTokenizer で DistilBERT トークナイザーをロードします :

from transformers import AutoTokenizer

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

質問応答のためにテキストを前処理するとき、知るべき幾つかのことがあります :

  1. データセットの幾つかのサンプルは、モデルの最大入力長を超える非常に長いコンテキストを持っている可能性があります。コンテキストを切り詰めて truncation=”only_second” を設定することによりこれに対応できます。

  2. 次に、答えの開始位置と終了位置を元のコンテキストにマップする必要があります。これを処理するために return_offset_mapping=True を設定します。

  3. 手動でマッピングすることにより、答えの開始と終了トークンを見つけられます。オフセットのどの部分が質問に対応し、オフセットのどの部分がコンテキストに対応するかを見つけるために sequence_ids メソッドを使用します。

下で示されるように総てを前処理関数に集めます :

def preprocess_function(examples):
    questions = [q.strip() for q in examples["question"]]
    inputs = tokenizer(
        questions,
        examples["context"],
        max_length=384,
        truncation="only_second",
        return_offsets_mapping=True,
        padding="max_length",
    )

    offset_mapping = inputs.pop("offset_mapping")
    answers = examples["answers"]
    start_positions = []
    end_positions = []

    for i, offset in enumerate(offset_mapping):
        answer = answers[i]
        start_char = answer["answer_start"][0]
        end_char = answer["answer_start"][0] + len(answer["text"][0])
        sequence_ids = inputs.sequence_ids(i)

        # Find the start and end of the context
        idx = 0
        while sequence_ids[idx] != 1:
            idx += 1
        context_start = idx
        while sequence_ids[idx] == 1:
            idx += 1
        context_end = idx - 1

        # If the answer is not fully inside the context, label it (0, 0)
        if offset[context_start][0] > end_char or offset[context_end][1] < start_char:
            start_positions.append(0)
            end_positions.append(0)
        else:
            # Otherwise it's the start and end token positions
            idx = context_start
            while idx <= context_end and offset[idx][0] <= start_char:
                idx += 1
            start_positions.append(idx - 1)

            idx = context_end
            while idx >= context_start and offset[idx][1] >= end_char:
                idx -= 1
            end_positions.append(idx + 1)

    inputs["start_positions"] = start_positions
    inputs["end_positions"] = end_positions
    return inputs

Datasets map 関数で前処理関数をデータセット全体に対して適用します :

tokenized_squad = squad.map(preprocess_function, batched=True, remove_columns=squad["train"].column_names)

前処理されたサンプルをまとめてバッチ化します :

from transformers import default_data_collator

data_collator = default_data_collator

 

Trainer API で再調整する

AutoModelForQuestionAnswering クラスでモデルをロードする :

from transformers import AutoModelForQuestionAnswering, TrainingArguments, Trainer

model = AutoModelForQuestionAnswering.from_pretrained("distilbert-base-uncased")

TrainingArguments で訓練引数を集めます :

training_args = TrainingArguments(
    output_dir="./results",
    evaluation_strategy="epoch",
    learning_rate=2e-5,
    per_device_train_batch_size=16,
    per_device_eval_batch_size=16,
    num_train_epochs=3,
    weight_decay=0.01,
)

Trainer にモデル, 訓練引数, データセット, データ collator, とトークナイザーを集めます :

trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=tokenized_squad["train"],
    eval_dataset=tokenized_squad["validation"],
    data_collator=data_collator,
    tokenizer=tokenizer,
)

モデルを再調整します :

trainer.train()

 

TensorFlow による再調整

(訳注: 原文 参照)

 

以上



ClassCat® Chatbot

人工知能開発支援
クラスキャットは 人工知能研究開発支援 サービスを提供しています :
  • テクニカルコンサルティングサービス
  • 実証実験 (プロトタイプ構築)
  • アプリケーションへの実装
  • 人工知能研修サービス
◆ お問合せ先 ◆
(株)クラスキャット
セールス・インフォメーション
E-Mail:sales-info@classcat.com

カテゴリー