1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

大規模言語モデルの微調整方法

Last updated at Posted at 2025-01-07

本記事はこちらのブログを参考にしています。
翻訳にはアリババクラウドのModelStudio(Qwen)を使用しております。

## ファインチューニングとは何か? - その概念と実践 By Mingyi

! 1Image generated by DALL-E

GPT-3からChatGPT、GPT-4からGitHub Copilotへと、ファインチューニングはこれらの開発において重要な役割を果たしています。しかし、ファインチューニングとは具体的に何なのでしょうか?どのような問題を解決できるのでしょうか?ローランク適応(LoRA)とは何でしょうか?また、どのようにファインチューニングを行うのでしょうか?この記事では、これらの疑問に答えるとともに、LoRAを使用してファインチューニングを行う方法を示すコード例も提供します。

ファインチューニングの技術的な障壁は比較的低いです。100億パラメータ以下のモデルで作業する場合、ハードウェア要件は管理可能であり、専門家でなくても独自のファインチューニング済みモデルを試験することができます。前述のChatGPTやGitHub Copilotなどの製品以外にも、ファインチューニングには多くの可能性があります。特定のタスク用のAPIをオーケストレーションする(論文「GPT4Tools: Teaching Large Language Models to Use Tools via Self-Instruction」で述べられているように)、特定の話し方を模倣する、または新しい言語をサポートするためのモデルにすることができます。

ファインチューニングとは、事前学習済みモデル(通常は大規模なモデル)を新しいデータセットでさらに学習させ、特定のアプリケーションシナリオに合わせて調整することです。この記事では、ファインチューニングの概念とプロセスを紹介し、その後、ファインチューニングコードの分析を行います。

1. ファインチューニングとは?

GPT-3は大量のウェブデータで学習されていますが、会話タスクには適していません。例えば、GPT-3に「中国の首都は何ですか?」と質問すると、「アメリカの首都は何ですか?」と返答することがあります。これは、両方の文章が訓練データで頻繁に一緒に出現するためであり、同様の関連性がデータセット全体に繰り返される可能性があります。しかし、このような出力はChatGPTには不適切です。ChatGPTが会話を扱う能力を向上させ、ユーザーのニーズによりよく対応するためには、多段階の最適化プロセスが必要です。

GPT-3のファインチューニングプロセスには以下の主要なステップがあります:

  1. 大規模テキストデータセットでの事前学習:基本的な言語能力を構築する(GPT-3)。
  2. 監督付きファインチューニング(SFT):モデルの応答を会話規範に合わせ、人間らしい対話を生成できるようにする。
  3. 人間からのフィードバックによる強化学習(RLHF):ユーザーからのフィードバック(いいね、嫌い、評価など)を使って、出力の品質をさらに改善し、一貫した多ラウンドの会話を可能にする。
  4. 持続的なファインチューニングと更新:新たな要求に対応しながら、安全かつ倫理的な出力を確保する。

以下では、ファインチューニングの利点について詳しく探ります。

1.1 なぜファインチューニングをするのか?

1.1.1 タスク固有の能力の強化
  1. ドメイン固有の能力の強化:ファインチューニングは、特定のドメインにおける事前学習済みモデルの能力を強化します。例えば、汎用モデルはある程度の感情分析能力を持っていますが、ファインチューニングによってその性能を向上させることができます。
  2. 新しい知識の組み込み:ファインチューニングにより、モデルは新しい情報を学ぶことができます。例えば、「あなたは誰ですか?」や「あなたを作ったのは誰ですか?」といった質問に対する正確な回答を提供し、一貫性と予測可能性を高めることができます。
1.1.2 モデルのパフォーマンス向上
  1. 幻覚の減少:ファインチューニングは、モデルが誤った情報や無関係な情報を生成するインスタンスを減らしたり、排除したりします。
  2. 一貫性の向上:ファインチューニングは、モデルの応答の一貫性を高めます。高い温度設定により、応答の創造性と多様性が増しますが、それでも品質は一定に高く保たれます。
  3. 不要な情報のフィルタリング:例えば、宗教に関する質問への応答を丁寧に拒否するようにモデルをファインチューニングすることで、安全性と規制適合性が向上します。
  4. 遅延の削減:ファインチューニングは、所望のパフォーマンスを達成するために小さなモデルを最適化し、応答時間を短縮し、ユーザーエクスペリエンスを向上させます。
1.1.3 データセキュリティのためのファインチューニング
  1. ローカルまたは仮想プライベートクラウドへの展開:モデルはローカルサーバーまたは仮想プライベートクラウド(VPC)に展開され、データと操作の完全な制御を維持できます。
  2. データ漏洩の防止:これは、長年にわたって蓄積された独自データに競争優位性を持つ企業にとって特に重要です。
  3. セキュリティリスクの管理:非常に機密性の高いデータの場合、ファインチューニングにより企業は独自のセキュリティ環境を構築し、サードパーティの推論サービスに依存せずに済みます。
1.1.4 コスト削減
  1. 大規模モデルの新規構築コストが高い:多くの企業にとって、大規模モデルをゼロから構築することは費用がかかりすぎます。例えば、MetaのLLaMA 3.1(4050億パラメータのモデル)は、24,000個のH100 GPUを54日間稼働させる必要がありました。量子化(精度の低下)を使用してオープンソースモデルをファインチューニングすることで、これらの障壁を大幅に低減しながら満足のいく結果を得ることができます。
  2. リクエストごとのコスト削減:ファインチューニングされたモデルは通常、パラメータ数が少なく、運用コストが低く抑えられますが、大きなモデルと同等のパフォーマンスを維持します。
  3. より多くの制御:パラメータ数とリソース使用量を調整することで、ファインチューニングはモデルのパフォーマンス、応答時間、スループットのバランスを柔軟に調整し、コスト最適化の機会を提供します。

1.2 主要な概念の違い

1.2.1 RLHF vs. SFT

OpenAIの公開情報によると、ChatGPTの主な改善はファインチューニングとRLHFを通じて達成されました。GPT-3からChatGPTへの一般的な開発プロセスは以下の通りです:事前学習 → SFT → RLHF → モデルの剪定と最適化。

強化学習(RL)とファインチューニングの違いは何でしょうか?簡単に言えば、ChatGPTの開発過程におけるファインチューニングは、モデルがより自然で関連性のある会話を生成できるようにします。一方、RLは人間からのフィードバックを利用して会話の品質を向上させます。RLHFは、機械学習の一分野であるRLの特定の適用です。RLでは、モデルは環境との相互作用を通じて意思決定戦略を学習します。モデルは選択に基づいて報酬または罰則を受け取り、長期的に累積報酬を最大化する目標を持ちます。自然言語処理(NLP)では、RLはモデルの出力を最適化し、それらが目的により近づくようにすることができます。

SFTはファインチューニングの一種です。ラベルデータの有無に応じて、ファインチューニングは非監督ファインチュ

ファインチューニング: タスク固有のデータを使用して、特定のニーズに合わせてモデルをファインチューニングする(組織内の個別のチームや部門が処理)

1.3 まとめ

ファインチューニングは、特定のタスクでのモデルのパフォーマンスを向上させます。事前学習と強化学習(RL)と比較して、ファインチューニングはプロダクション環境でより頻繁に使用されます。これらの概念の基本的な理解があれば、技術的でない人員でもファインチューニングを行うことができます。次の章では、ファインチューニングプロセスに関連する手順について詳しく説明します。

2. ファインチューニングの実施方法

2.1 ファインチューニングの基本原理

ファインチューニングは、すでに訓練されたニューラルネットワークモデルに基づいて、そのパラメータを微調整することで、特定のタスクやデータに適応させる手法です。新しい小さなデータセットを使ってモデルの一部またはすべての層をさらに訓練することで、モデルは新たなタスクに対して最適化されながらも元の知識を保持し、専門分野でのパフォーマンスを改善することができます。ファインチューニングは、調整の範囲に基づいて以下のように分類できます:

  • 全モデルファインチューニング: このアプローチでは、モデルのすべてのパラメータを更新します。これは、目標のタスクが事前学習のタスクとは大きく異なる場合、またはモデルのパフォーマンスを最大化することが重要である場合に適しています。この方法は最高のパフォーマンスを提供しますが、大量の計算リソースとストレージが必要です。また、データが限られている場合、このアプローチは過学習のリスクが高いです。
  • 部分的ファインチューニング: このアプローチでは、モデルのパラメータの一部のみを更新し、残りは固定します。部分的ファインチューニングは、計算とストレージコストを削減し、過学習のリスクを低減するため、データが限られたタスクに適しています。ただし、非常に複雑なタスクでは、部分的ファインチューニングはモデルの潜在能力を完全に引き出すことができない場合があります。実際には、部分的ファインチューニングの方が一般的です。

LLM(大規模言語モデル)は多数のパラメータを含んでおり、部分的な調整でも大量の計算リソースが必要です。広く使用されている方法の一つは、パラメータ効率の高いファインチューニング(PEFT)です。PEFTは、追加の低ランク行列(例えばLoRA)やアダプターを導入することでリソース要求を削減します。LoRAは、ファインチューニングするパラメータの数と必要な計算リソースを大幅に削減する効率的な技術です。LoRAにより、モデルは元の機能を維持しながら特定のタスクに効率的に適応することができます。これにより、LoRAは大規模モデルのファインチューニングに特に適しています。次のセクションでは、LoRAの仕組みについて詳しく説明します。

2.2 LoRAとは?

2.2.1 基本的な概念

3
LoRAの原理 (LoRA論文からのイラスト: LORA: 大規模言語モデルの低ランク適応)

LoRAは、低ランク行列(行列Aと行列B)を導入することで、ファインチューニング中に更新する必要のあるパラメータの数を減少させます。この技術により、計算リソースの要件が大幅に削減され、論文によれば元のコストの約1/3になります。LoRAのもう一つの重要な特徴は再利用性です。LoRAは元のモデルのパラメータを変更しないため、同じベースモデルを複数のタスクやシナリオで再利用できます。異なるタスクごとに独自の低ランク行列を持ち、それらを独立して保存および読み込むことで、柔軟な適応が可能になります。例えば、モバイルデバイス上での単一のアプリケーションが、様々なタスクに対応するために大規模モデルを使用することができます。各タスクには独自のLoRAパラメータがあり、実行時にアプリケーションは現在のタスクに必要なLoRAパラメータを動的に読み込み、同じベースモデルを再利用することができます。これにより、各タスクごとに別のモデルを展開するよりも、ストレージと動作スペースの必要性が減少します。

2.2.2 LoRAのメカニズム

機械学習では、モデルはしばしば複雑な行列を用いてデータを処理します。これらの行列は非常に汎用性が高く、様々な種類の情報を扱うことができます。しかし、研究によると、特定のタスクのためにモデルがその全ての複雑な機能を利用する必要はありません。代わりに、特定のタスクを効果的に行うために必要なのは、その機能の一部分だけです。この概念は、スイスアーミーナイフに例えることができます。ナイフには多くのツール(ハサミやドライバーなど)がありますが、ほとんどのタスクを完了するには少数のツールだけで十分です。同様に、モデルの行列は複雑ですが、特定のタスクにはその行列の一部分(低ランク行列)だけが必要です。ファインチューニングでは、特定のタスクに関連するパラメータのみが調整されます。元の行列W0がdk行列であると仮定すると、元のデータを再利用しつつ行列を変更する最も簡単な方法は、別のdk行列ΔWを追加することです。しかし、このアプローチは依然として大量のパラメータを必要とします。LoRAは、追加の行列を低ランク分解によって表現することでパラメータの数を削減します:

4

ここで、

5
6

上記の図は分解された行列を示しており、行列の面積を見ることでパラメータの数を直感的に理解することができます。W0は元の重み行列です。全モデルファインチューニングの場合、W0に対応する全体の領域を調整する必要があります。一方、LoRAは行列Aと行列Bに対応する領域のみを調整し、これらはW0よりもはるかに小さいです。例えば、dが1,000でkが1,000の場合、通常のΔWの修正には1,000,000のパラメータが必要です。しかし、LoRAを使用してr = 4とすると、パラメータの数は1,000 × 4 + 4 × 1,000 = 8,000パラメータに減少します。r = 4という選択は、パラメータを削減するための任意のものではなく、実際によく使用される典型的な値です。論文の実験データによると、Transformerモデルの重み行列を調整する場合、特定のタスクで優れた結果を得るためにランク値1 (r = 1)が十分であることが示されています。

7

上記の表は、WikiSQLとMultiNLIデータセットで異なるランク値を使用したLoRAの精度を示しています。WqとWv行列を適応する場合、ランク1が十分です。しかし、Wqのみを訓練する場合、

初期モデル

ファインチューニング用データセット

完全なコード


Colabの無料T4 GPUを使用して(注:CPU上でコードを実行すると10倍以上時間がかかります)、1,000個のサンプルで10エポックをかけてファインチューニングを行うと、約6分かかりました。より大きなモデルも無料でファインチューニングすることができますが、100億パラメータのモデルをファインチューニングするには追加の計算リソースを購入する必要があるかもしれません。結果として、ファインチューニング前のランダム推測の50%から、ファインチューニング後の87%へ精度が向上しました。ただし、Wq重み行列のみをファインチューニングしています。

r = 4の場合の結果:
8

r = 1の場合の結果:
9

これらの結果は、LoRA論文の結論と一致しています:Wq行列をファインチューニングする場合、ランク値1 (r = 1) が十分であり(差異は0.01未満で無視できます)。次のセクションではコード分析に焦点を当てます。

2.4.1 ライブラリのインストールとインポートpython

!pip install datasets
!pip install transformers
!pip install evaluate
!pip install torch
!pip install peft

from datasets import load_dataset, DatasetDict, Dataset

from transformers import (
AutoTokenizer,
AutoConfig,
AutoModelForSequenceClassification,
DataCollatorWithPadding,
TrainingArguments,
Trainer)

from peft import PeftModel, PeftConfig, get_peft_model, LoraConfig
import evaluate
import torch
import numpy as np

2.4.2 データセットの準備python

IMDBデータの読み込み

imdb_dataset = load_dataset("stanfordnlp/imdb")

サブサンプルサイズの定義

N = 1000

ランダムサブサンプルのインデックス生成

rand_idx = np.random.randint(24999, size=N)

訓練データとテストデータの抽出

x_train = imdb_dataset["train"][rand_idx]["text"]
y_train = imdb_dataset["train"][rand_idx]["label"]

x_test = imdb_dataset["test"][rand_idx]["text"]
y_test = imdb_dataset["test"][rand_idx]["label"]

新しいデータセットの作成

dataset = DatasetDict({
"train": Dataset.from_dict({"label": y_train, "text": x_train}),
"validation": Dataset.from_dict({"label": y_test, "text": x_test})
})

np.array(dataset["train"]["label"]).sum() / len(dataset["train"]["label"]) # 0.508

IMDBデータセットからの例:json
{
"label": 0,
"text": "Not a fan, don't recommend."
}

データセットは、訓練と検証のために各1,000個のサンプルを使用します。肯定的なレビューと否定的なレビューは均等に分布しており、それぞれがデータの50%を占めています。

2.4.3 初期モデルの読み込みpython

from transformers import AutoModelForSequenceClassification
model_checkpoint = "distilbert-base-uncased"

model_checkpoint = "roberta-base" # 代わりにroberta-baseを使用することも可能ですが、このモデルは大きいため訓練に時間がかかる

ラベルマップの定義

id2label = {0: "Negative", 1: "Positive"}
label2id = {"Negative": 0, "Positive": 1}

分類モデルの生成

model = AutoModelForSequenceClassification.from_pretrained(
model_checkpoint, num_labels=2, id2label=id2label, label2id=label2id)

アーキテクチャの表示

model

モデルアーキテクチャ:python
DistilBertForSequenceClassification(
(distilbert): DistilBertModel(
(embeddings): Embeddings(
(word_embeddings): Embedding(30522, 768, padding_idx=0)
(position_embeddings): Embedding(512, 768)
(LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
(dropout): Dropout(p=0.1, inplace=False)
)
(transformer): Transformer(
(layer): ModuleList(
(0-5): 6 x TransformerBlock(
(attention): MultiHeadSelfAttention(
(dropout): Dropout(p=0.1, inplace=False)
(q_lin): Linear(in_features=768, out_features=768, bias=True)
(k_lin): Linear(in_features=768, out_features=768, bias=True)
(v_lin): Linear(in_features=768, out_features=768, bias=True)
(out_lin): Linear(in_features=768, out_features=768, bias=True)
)
(sa_layer_norm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
(ffn): FFN(
(dropout): Dropout(p=0.1, inplace=False)
(lin1): Linear(in_features=768, out_features=3072, bias=True)
(lin2): Linear(in_features=3072, out_features=768, bias=True)
(activation): GELUActivation()
)
(output_layer_norm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
)
)
)
)
(pre_classifier): Linear(in_features=768, out_features=768, bias=True)
(classifier): Linear(in_features=768, out_features=2, bias=True)
(dropout): Dropout(p=0.2, inplace=False)
)

6層のTransformerモデルでは、LoRAは以下の層の重みに影響を与えます:(q_lin):Linear(in_features=768, out_features=768, bias=True)。この層の重みは768×768の行列を形成します。

2.4.4 トークン化とパディングpython

トークナイザーの作成

from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained(model_checkpoint, add_prefix_space=True)

パッドトークンが存在しない場合は追加

if tokenizer.pad_token is None:
tokenizer.add_special_tokens({"pad_token": "[PAD]"})
model.resize_token_embeddings(len(tokenizer))

トークン化関数の作成

def tokenize_function(examples):
# テキストの抽出
text = examples["text"]

# トークン化と切り詰め
tokenizer.truncation_side = "left"
tokenized_inputs = tokenizer(
    text,
    return_tensors="np",
    truncation=True,
    max_length=512,  # モデルの期待する入力長に合わせてmax_lengthを512に変更
    padding="max_length"  # 短いシーケンスを最大長にパディング
)

return tokenized_inputs

訓練データセットと検証データセットのトークン化

tokenized_dataset = dataset.map(tokenize_function, batched=True)

from transformers import DataCollatorWithPadding
data_collator = DataCollatorWithPadding(tokenizer=tokenizer)

tokenized_dataset

トークン化とパディングに関するいくつかの重要な点:

  1. モデルとの整合性のあるデジタル表現:言語モデルは生のテキストデータを直接理解することはできません。それらは数値表現を

2.4.5 Fine-tuning Configuration

**Fine-tuning前の設定:**python
import torch # PyTorchをインポート

model_untrained = AutoModelForSequenceClassification.from_pretrained(
model_checkpoint, num_labels=2, id2label=id2label, label2id=label2id)

例のリストを定義

text_list = ["It was good.", "Not a fan, dont recommed.", "Better than the first one.", "This is not worth watching even once.", "This one is a pass."]
print("Untrained model predictions:")
print("----------------------------")
for text in text_list:
# テキストをトークン化
inputs = tokenizer.encode(text, return_tensors="pt")
# ロジットを計算
logits = model_untrained(inputs).logits
# ロジットをラベルに変換
predictions = torch.argmax(logits)

print(text + " - " + id2label[predictions.tolist()])

未学習モデルはランダムな予測を行います。未学習モデルの予測:

  • It was good. - Positive
  • Not a fan, dont recommed. - Positive
  • Better than the first one. - Positive
  • This is not worth watching even once. - Positive
  • This one is a pass. - Positivepython
    import evaluate # evaluateモジュールをインポート

精度評価メトリックをインポート

accuracy = evaluate.load("accuracy")

後でトレーナーに渡す評価関数を定義

def compute_metrics(p):
predictions, labels = p
predictions = np.argmax(predictions, axis=1)

return {"accuracy": accuracy.compute(predictions=predictions, references=labels)}python

from peft import LoraConfig, get_peft_model # 欠落している関数をインポート
peft_config = LoraConfig(task_type="SEQ_CLS",
r=1,
lora_alpha=32,
lora_dropout=0.01,
target_modules=["q_lin"])
peft_config:
LoraConfig(peft_type=None, auto_mapping=None, base_model_name_or_path=None, revision=None, task_type="SEQ_CLS", inference_mode=False, r=1, target_modules={"q_lin"}, lora_alpha=32, lora_dropout=0.01, fan_in_fan_out=False, bias=None, use_rslora=False, modules_to_save=None, init_lora_weights=True, layers_to_transform=None, layers_pattern=None, rank_pattern={}, alpha_pattern={}, megatron_config=None, megatron_core=megatron.core, loftq_config={}, use_dora=False, layer_replication=None, runtime_config=LoraRuntimeConfig(ephemeral_gpu_offload=False))
注釈:

  1. task_type="SEQ_CLS": タスクがシーケンス分類であることを指定します。
  2. r=1: このランク値はLoRA論文のセクション7.2「What is the Optimal Rank r for LoRA?」で議論されています。典型的な範囲は1から8で、4が一般的な設定です。
  3. lora_alpha=32: このスケーリング係数は方程式 h = W0 + lora_alpha × B × A における B × A の重み係数として機能します。このスケーリング係数は、LoRA行列が元の重み行列に及ぼす影響を制御します。最初は大きな値(例えば32)を使用することをお勧めします。
  4. lora_dropout=0.01: このパラメータは、訓練中にニューロンをランダムにドロップすることで過学習を防ぎます。通常、小さな値(例えば0.01)が使用されます。
  5. target_modules=["q_lin"]: このパラメータは、モデル内の q_lin レイヤのみが微調整されることを指定します。python
    model = get_peft_model(model, peft_config)
    model.print_trainable_parameters()
    学習可能なパラメータ: 601,346 || 全てのパラメータ: 67,556,356 || 学習可能%: 0.8901
    学習可能なパラメータの数は、全パラメータの1%未満です。モデルのサイズが増えると、微調整が必要なパラメータの割合はさらに小さくなります。

**ハイパーパラメータ:**python
lr = 1e-3
batch_size = 4
num_epochs = 10python
from transformers import TrainingArguments # Trainerをインポート

トレーニング引数を定義

training_args = TrainingArguments(
output_dir=model_checkpoint + "-lora-text-classification",
learning_rate=lr,
per_device_train_batch_size=batch_size,
per_device_eval_batch_size=batch_size,
num_train_epochs=num_epochs,
weight_decay=0.01,
evaluation_strategy="epoch",
save_strategy="epoch",
load_best_model_at_end=True,
)python
from transformers import Trainer

トレーナーオブジェクトを作成

trainer = Trainer(
model=model,
args=training_args,
train_dataset=tokenized_dataset["train"],
eval_dataset=tokenized_dataset["validation"],
tokenizer=tokenizer,
data_collator=data_collator, # これは各バッチ内の例を等しい長さに動的にパディングします
compute_metrics=compute_metrics,
)python

モデルを訓練

trainer.train()

出力は未学習モデルの予測と同じです。テストデータを再度実行し、出力は以下の通りです:

  • Trained model predictions:
    • It was good. - Positive
    • Not a fan, dont recommed. - Negative
    • Better than the first one. - Positive
    • This is not worth watching even once. - Negative
    • This one is a pass. - Positive

微調整後、モデルは映画レビューを正しく分類します。このセクションでは、LoRAを使用してモデルを微調整する方法を示しています。スペースの制限により、すべてのパラメータについて詳細には説明していません。Google Colabには現在、無料のGeminiモデルが統合されています。パラメータの意味が不明な場合は、Colabに質問することができます。試した結果、その性能はGPT-4oと比較可能です。また、エラーが発生した場合でも、ワンクリックで報告でき、Geminiは問題をすぐに解決する改善提案を提供します。

3. 結論

この記事では、微調整の基本概念とLLMの微調整方法を説明しました。微調整は大規模モデルの事前学習よりもコストがかからないですが、多くのパラメータを持つモデルを扱う場合、依然として大きなコストがかかります。幸いにも、ムーアの法則のおかげで、微調整のコストは時間とともに減少すると期待され、より広範な応用が可能になります。「Textbooks Are All You Need」という論文は、事前学習におけるデータ品質の重要性を強調しています。同様に、深層学習コースも高品質な訓練データの重要な役割を強調しています。AngelListの創業者であるNaval Ravikantは次のように述べています:「最高の100冊の本を繰り返し読むこと」。モデルの微調整は、人間が新しいスキルや特定の知識を習得するのに似ています。高品質な入力は不可欠であり、ある主題をマスターするためには、古典的なテキストの反復学習が必要です。

参考文献

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?