Open GENZITSU opened 1 year ago
最近の自然言語処理コンペで度々用いられる敵対的学習手法、Fast Gradient MethodとAdversarial Weight Perturbationの解説及び実装例が紹介されているブログ
FGMは現在のモデルの精度を最も悪くする方向に入力に摂動を与えたときでもコスト関数が小さくなるような制約をモデルに与える。
この摂動をDeepなモデルに対して得ることは困難なのだが、誤差逆伝搬を利用する以下の方式で求められることがExplaining and Harnessing Adversarial Examplesにて提案されている。
下記は実装例
# reference: https://www.kaggle.com/c/tweet-sentiment-extraction/discussion/143764
class FGM():
def __init__(self, model):
self.model = model
self.backup = {}
def attack(self, epsilon=1., emb_name='word_embeddings'):
"""
敵対的な摂動を求め、現在のembedding layerに摂動を加える
"""
for name, param in self.model.named_parameters():
if param.requires_grad and emb_name in name:
self.backup[name] = param.data.clone()
norm = torch.norm(param.grad)
if norm != 0:
r_at = epsilon * param.grad / norm
param.data.add_(r_at)
def restore(self, emb_name='word_embeddings'):
"""
敵対的な摂動を求める際に変更してしまったembedding layerのパラメータについて
元のパラメータを代入する
"""
for name, param in self.model.named_parameters():
if param.requires_grad and emb_name in name:
assert name in self.backup
param.data = self.backup[name]
self.backup = {}
使い方
# from https://blog.brainpad.co.jp/entry/2022/08/23/153001
fgm = FGM(model)
for batch_input, batch_label in data:
# オリジナルのサンプルについての損失を計算
loss = model(batch_input, batch_label)
loss.backward()
# adversarial training
# embedding layerに敵対的な摂動を加える
fgm.attack()
# 敵対的な摂動を加えられた状態での損失を計算
loss_adv = model(batch_input, batch_label)
loss_adv.backward()
fgm.restore()
optimizer.step()
model.zero_grad()
モデルの入力に対して摂動を加えるのではなく、モデルの重みに摂動を加えるという手法
実装は以下
# reference: https://www.kaggle.com/code/wht1996/feedback-nn-train/notebook
class AWP:
def __init__(
self,
model,
optimizer,
adv_param="weight",
adv_lr=1,
adv_eps=0.2,
start_epoch=0,
adv_step=1,
scaler=None
):
self.model = model
self.optimizer = optimizer
self.adv_param = adv_param
self.adv_lr = adv_lr
self.adv_eps = adv_eps
self.start_epoch = start_epoch
self.adv_step = adv_step
self.backup = {}
self.backup_eps = {}
self.scaler = scaler
def attack_backward(self, x, y, attention_mask,epoch):
"""
敵対的な摂動を加えた損失を計算し、パラメータを更新する
"""
if (self.adv_lr == 0) or (epoch < self.start_epoch):
return None
self._save()
for i in range(self.adv_step):
self._attack_step()
with torch.cuda.amp.autocast():
adv_loss, tr_logits = self.model(input_ids=x, attention_mask=attention_mask, labels=y)
adv_loss = adv_loss.mean()
self.optimizer.zero_grad()
self.scaler.scale(adv_loss).backward()
self._restore()
def _attack_step(self):
"""
敵対的な摂動を求め、重みに加える
重みの範囲をbackup_epsで制限している
"""
e = 1e-6
for name, param in self.model.named_parameters():
if param.requires_grad and param.grad is not None and self.adv_param in name:
norm1 = torch.norm(param.grad)
norm2 = torch.norm(param.data.detach())
if norm1 != 0 and not torch.isnan(norm1):
r_at = self.adv_lr * param.grad / (norm1 + e) * (norm2 + e)
param.data.add_(r_at)
param.data = torch.min(
torch.max(param.data, self.backup_eps[name][0]), self.backup_eps[name][1]
)
def _save(self):
"""
重みのバックアップと、重みの範囲を取得する
重みの範囲はパラメータの絶対値とadv_epsによって決定する
"""
for name, param in self.model.named_parameters():
if param.requires_grad and param.grad is not None and self.adv_param in name:
if name not in self.backup:
self.backup[name] = param.data.clone()
grad_eps = self.adv_eps * param.abs().detach()
self.backup_eps[name] = (
self.backup[name] - grad_eps,
self.backup[name] + grad_eps,
)
def _restore(self):
"""
バックアップを取っていたパラメータを代入するとともに初期化する
"""
for name, param in self.model.named_parameters():
if name in self.backup:
param.data = self.backup[name]
self.backup = {}
self.backup_eps = {}
このクラスをインスタンスかして以下のように使用する。
# reference: https://www.kaggle.com/code/wht1996/feedback-nn-train/notebook
with torch.cuda.amp.autocast():
loss, tr_logits = model(input_ids=input_ids, attention_mask=attention_mask, labels=labels)
loss = loss.mean()
optimizer.zero_grad()
# loss.backward()
# optimizer.step()
scaler.scale(loss).backward()
if f1_score > 0.64:
awp.attack_backward(input_ids,labels,attention_mask,step)
# gradient clipping
torch.nn.utils.clip_grad_norm_(
parameters=model.parameters(), max_norm=10
)
scaler.step(optimizer)
scaler.update()
scheduler.step()
実装のイメージがあまり沸いてなかった手法達だったが、元の入力/重みを一回バックアップしておいて摂動を与えてロスを算出しているということがわかり勉強になった。
FGMはあまり使われているということを知らなかっただ、AWPの方がモデル全体に正則化をかけられるから精度を上げやすいのかなと想像。
文書からの情報抽出を検出したテキストからのグラフ生成問題として解く手法の提案 from NAVER @ ACL 2021
文書からの情報抽出はしばしばテキストの検出 → serialization (bboxの並び替え)→ 固有表現抽出という流れで解かれることが多いが、文書の構造が複雑になってくるとserializationのアルゴリズムが複雑になるため、様々な文書に応用することを考えると最適とは言えない。
また、固有表現抽出で問題を解く場合、情報の階層性をうまく扱うことができない。
※ 情報の階層性とはたとえば、レシートの情報抽出を行う際の、品目→品目の個数, 品目→品目の単価などの関係のこと
ただし、文書の画像が基本的に横書き縦書きの形式になっており、抽出すべき情報に階層性がない場合はBERTなどで固有表現抽出を行うので十分とも言っている。
In the simplest case, both of the two factors are minimally present, where the text is strictly a linear sequence, and the desired output is simply a list of fields, similar to Named Entity Recognition (NER) task.
この論文では、以下のようにOCR検出したBBoxから情報の依存関係グラフを生成することで情報抽出を行うSPADEという方法を提案
ここでrel-sはある情報を構成するトークンの順番を司るもので、rel-gは情報の階層性を司るものである。
(1) rel-s for the ordering and grouping of tokens belonging to the same information category (blue arrows in Fig. 1b), and (2) rel-g for the inter-group relation between grouped tokens or groups (orange arrows in the same figure).
SPADEはbboxの位置とbbox内のテキストをencodeする spacial text encoderと、そこからグラフを生成する graph generator, 生成されたグラフから情報をparseする graph decoderの3つのパーツからなる。
graph decoderはパラメータがないルールベースのdecoderである。
spacial text encoderはtokenとbboxペアの相対的な位置関係から生成されるspacial vectorを用いてbboxのembeddingを生成するもの。
graph generatorはspacial text encoderから生成されたノードと情報項目のノードからgraph matrixに 0 or 1を入れていくモデルで、rel-s用のものとrel-g用の2つを用意する。
それぞれのマトリックスの0/1は以下のように計算していく。
以下のようなデータで実験
CORDはclovaaiがこのgithubで公開しているレシートでのデータで+が増えるごとにaugmentationが激しくなっているとのこと。
精度の結果が以下
テストデータのOCR結果が真値かつbboxのグルーピングも真値が与えられるのであれば、BERTによる固有表現抽出タスクがよいが、そうでなければSPADEの方が精度が良い。
また、文書が曲がっていたり歪んでいたりする場合もSPADEの方が精度が高い。
表の中にあるTCAはtail colision avoidanceアルゴリズムと呼ばれるもので、グラフ生成の際に同じトークンが別々の情報の末端となら内容にするためのルールベースの手法。
以下が定性的な評価で、serializaitonがうまくいかないと普通の固有表現抽出手法では間違えてしまうことを言っている。
精度の上がり幅を見てみると、そんなに階層構造がそこまで複雑じゃない文書に対してはserializatioz + 固有表現抽出の方が筋がいいのかもしれない。
また、この手法も結局文書ごとに別々にモデルを作って評価しているので、実務で使うには少しハードルが高いかもしれない...。
training dataの作り方もちょっと大変そうだし...
Spatial Dependency Parsing for Semi-Structured Document Information Extraction
spacyで提供されている固有標抽出器に新しいクラスを追加して学習する方法の紹介記事
step 1 データセットの準備
# from https://tech.mof-mof.co.jp/blog/spacy-ner/
TRAIN_DATA = [
('時は大正。主人公・竈門炭治郎は亡き父親の跡を継ぎ、炭焼きをして家族の暮らしを支えていた。', {'entities': [(9, 14, 'CHARACTER')]}),
('禰󠄀豆子に襲われかけた炭治郎を救ったのは冨岡義勇と名乗る剣士だった。', {'entities': [(20, 24, 'CHARACTER')]}),
('鬼化して辛うじて生き残った禰󠄀豆子を人間に戻すため、冨岡義勇の紹介で鱗滝の元を訪れる', {'entities': [(26, 30, 'CHARACTER')]}),
('修得者は竈門炭治郎・冨岡義勇・鱗滝左近次。基本型は10つ。日輪刀の色は青色', {'entities': [(4, 9, 'CHARACTER'), (10, 14, 'CHARACTER')]})
]
step 2: 学習ループの作成
# from https://tech.mof-mof.co.jp/blog/spacy-ner/
# get names of other pipes to disable them during training
other_pipes = [pipe for pipe in nlp.pipe_names if pipe != 'ner']
with nlp.disable_pipes(*other_pipes): # only train NER
optimizer = nlp.begin_training()
for itn in range(iterations):
print("Statring iteration " + str(itn))
random.shuffle(TRAIN_DATA)
losses = {}
for text, annotations in TRAIN_DATA:
nlp.update(
[text], # batch of texts
[annotations], # batch of annotations
drop=0.2, # dropout - make it harder to memorise data
sgd=optimizer, # callable to update weights
losses=losses)
ここまで簡単にできるのは知らなかった。
2022年6月時点でMVTec ADでSOTAを達成した学習不要な異常検知アルゴリズムの紹介記事。
この手法は特徴抽出部分と特徴分布からのコアセットサンプリングが肝となっている。
以下の図のように、学習済みbackboneの間の層から特徴特徴を抽出してそれらをconcatすることで、各pixelの特徴量とみなしている。
特徴マップのサイズが入力画像と合わないところはaverage poolingを用いて整合性をとっている。
最初と最後の層を使わないのは、より一タスクに意味のある特徴を取りたいからで、最初の層は一般的すぎる特徴で最後の層はImagenetのタスクに寄りすぎているからとしている。 (ここがPaDiMとの大きな違いか)
この手法ではマハラノビス距離や特徴量の統計量から作成した正規分布などを利用することは常に最適なわけではないということで、近傍法による距離を用いて異常度のスコアを算出する。
以下が、特徴量分布が正規分布ではないのに無理やり当てはめて失敗する例。
ただし近傍方法を用いるにはサンプルサイズがかなり大きいので計算時間が現実的でなくなってしまうので、正常画像から求めた特徴量を適宜間引く必要がある。
そこで考えられたアルゴリズムが以下で、コアセット内の点からの距離が最も遠いものを入れていく。
(正確にはコアセットの各点との距離のうち最小のものが、残りの点の中のうちで最大のもの)
このコアセット選定の際に一時的にrandom projectionで次元を削減するが実際に使うときは特に次元削減を行わなっていないことに注意
イメージ図を見ると何をやっているかがわかりやすい
このアルゴリズムによりランダムサンプリングよりも分布を広くカバーした特徴量をサンプリングできる
実験では元の特徴量数の1%程度まで削減しても精度が出ることを確認している。 とはいえ 224 × 224 * 5354サンプルの1%なので、それなりにでかいのだけど...
最終的に異常度を算出する際は各ピクセルの特徴量と最も近傍の点を見つけその点の周囲の点との距離も鑑みてスコアリングを実施するのだが、以下のablationで2~4程度が良いとされている。 このablationの下半分は特徴抽出の際に何番目のレイヤーまでを使うかの比較。
またcoresetによる比較特徴料の母数の削減により、推論時間も早い。その下半分はサンプリングの%ごとの精度比較。
さらにこの手法はサンプル効率も他の手法よりも良いのだとか...
学習済みCNNの取ってくる特徴量のレイヤーを変えるだけ大きく精度が変わってしまうのはかなり驚き。
スコアリング関数を正規分布系から近傍法ベースにかえたというのも納得だし、PaDiMのようなピクセル方向での比較ではなく、特徴量セット全体を見て異常度を算出するので、よりロバストかつ撮影環境のブレに強くなりそうに見える。
2020年10月時点でMVTec ADでSOTAを達成した学習不要な異常検知アルゴリズムの紹介記事
その手法は非常にシンプルで学習済みCNNからピクセルごとの特徴量を作成し、正例サンプルを用いて特徴量の平均と共分散行列を作ることでそれをパラメータに持つ正規分布を作成する。
推論時は同じく学習済みCNNからピクセルごとの特徴量を作成し、先ほど作成した正規分布の関数にいれて異常度を求める。
ちなみに、CNNから得られる特徴量をそのまま使用すると計算量が重たくなるので、ランダムに次元を選択して特徴量サイズを縮小すると良いらしい。これはPCAを用いた次元削減よりも性能がでるとのこと。
PaDiMの再現実装においては、resnet18では448次元の特徴ベクトルを100次元に、wide_resnet50_2では1792次元の特徴ベクトルを550次元に、ランダム抽出によって削減しています。
結果は以下
ここまでサチったデータセットにもかかわらず、+1%の改善を見せている。
かなりシンプルな手法なのに精度が出ていてすごい。
MVTecのように撮影環境が統制されているとこれで良い感じなのか...?
AIシンガーの学習データとして声データを提供したクリエイターに収益を還元するためCeVIO AIがサブスクリプションサービスを9/1から開始するという。
AIに食わせるためのデータを生み出すものとそれを使用してAIを作成しようとしていくものとの一つの共生関係として参考になりそうだ。
以下記事で面白かった部分
最近では、バーチャルYouTuberの「花譜」さんが「CeVIO AI」というブランドのAIシンガーになり、ヒット曲もたくさんリリースされている。8月8日にはVTuberのキズナアイさんの歌声を再現したAI「#kzn」が先行販売された。
テクノスピーチは22年6月、新ブランド「VoiSona」を発表した。CeVIOブランドとは異なる同社単独のプロジェクトで、使っている要素技術はほぼ同じだが、別のビジネスモデルを採用している。それがサブスクリプションだ。
VoiSonaは楽譜を入力すると、AIが人間らしい歌声を自動生成するソフトウェア。歌声を加工する編集ソフト(エディター)は無償で提供し、人間の歌声を学習したAIモデルを月額880円、年額6600円で提供する。
AIを活用した音声合成技術は品質向上を続け、特に話し声の再現では人間と聞き分けられないレベルのものも登場している。そうなると、ユーザーからすれば一度だけ1万円前後でソフトウェアを買ってしまえば、声優に仕事を頼まなくて済むような状況になってしまう。 「そうすると、いつか本当にAIが演者さんの敵になってしまう。そこで、新しいビジネスモデルとして、サブスクリプションによって定常的に収益を上げ、それを演者さんに還元する仕組みを作りました」(大浦代表) 音源の提供で得た収益の一部を演者(の所属事務所など)にフィードバックする。利用されればされるほど演者に収入が入る印税のような仕組みだ。
「レコードが登場したとき、演奏家はコンサートに人が来なくなるんじゃないかと反対したらしいです。実際はレコードから収益を得たり、コンサートへの集客につながったりと双方にメリットがありました。音声合成と演者もそういうWin-Winの関係になれるんじゃないかと思っています」(徳田教授)
以下はオリジナルの花譜とそれをコピーした可不さんのコラボMV 「【音楽的同位体可不】フォニイ / 花譜 feat. 可不(KAFU)
」
コピーロボットをうまく使ってファンをさらに拡大させることもできそうだ。
https://www.youtube.com/watch?v=qzUU5tfFAeA&feature=youtu.be
stable dissusionが生み出されたことで著作権周りの議論が再燃しているが、この事例のようにコピー〇〇風画像を作られることは不可避なので、公式がコピー〇〇風画像AIに食わせるデータを進んで提供しその対価としてサブスクリプション料を受け取るというモデルもあり得そうな気がする。
AIの社会実装でいうとデータを関係部門からうまく収集できないことで進まなくなることががままあったりするが、学習データの利用権を還元する仕組みなどを整えると話が進みやすいかも?? (じっさいはもっと感情的な部分で阻まれることの方が多そうですが)
AIを用いたIT技術教育を受けた海軍の新人は、ベテランの教師の授業を受けた新人よりも高いパフォーマンスを発揮したという研究結果が2014年にDARPAより発表されていたらしい。
ただし、ベテラン教師の授業については1対多形式の授業で、AIによる教育は一人一人に最適化された個別授業である。
一般的に個別学習の方が1対多形式よりも圧倒的に学習効果が高いことは1982年の研究で明らかになっていたのだが、これを一人一人の新人に行うことはリソース的に不可能であるということで、AIを作成することになった。
教師となるIT技術者が持つ専門的な知識の収集しデータベース化 これらのデータは「黒板」や「プロジェクター」に書かれる内容だけでなく、関連する参考資料や知識やスキルをチェックする問題文にも及ぶ
また教師と生徒の1対1の個別指導を実際に行ってもらい、教師と生徒の間で行われた全ての説明・質問などの会話音声を記録・データ化
発表によれば、研究予算の半分が専門家からの知識収集の対価に当てられたと報告されています。
Aiが実施するタスクは、基本的な知識や法則の説明と理解度を確認用の練習問題の提示
これを実現するためのモジュールがそれぞれ「推論エンジン」「命令エンジン」「会話モジュール」の3つからなる。
「推論エンジン」は個別指導型AI教師の肝となる部分であり、生徒が問題を解決する過程を観察して生徒の実力を推測する機能が与えられています。
次に推測エンジンによる判断結果は「命令エンジン」に送られ、生徒の実力に合致する新たな問題が提示されます。
そして「会話モジュール」では生徒がAI教師に対して自然な言語での質問を行える機能が追加されました。 この会話機能は「今日の天気」や「恋バナ」など自由度の高い会話はできませんが、授業内容にかんする質問に的確に答えられるように設計されています。
常にちょうどいい難易度の問題を出し続けることで、より集中した状態(フロー状態)になりやすいからと考察
AI教師は単に基本的な説明や問題の提示を繰り返すだけでなく、常に生徒の実力を測定し、学びの速度が速い時にはより高いレベルや関連トピックのに移行する一方で、学びが不十分である場合には、より詳しい説明を行うことが可能になります。
どれくらいの量のデータベースを作成したかの記載は元の報告書を辿っても見つからなかったが、訓練の期間は16週間に及ぶので相当な量であることが伺える。
結論自体は当たり前ではあるのだが、2014年時点でこの実験を行ったDARPAまじで先見の明がある。
逆翻訳を使って日本語テキストの学習データを拡張(水増し)してみた
みらい翻訳APIを用いてテキストデータのが逆翻訳を実施し、文書分類精度が向上するかを検討した記事。
リクルートが公開しているじゃらんnetに投稿されたポジネガラベル付きの宿クチコミのコーパスを用いて検証。
逆翻訳の様子 逆翻訳の成功確率は50%くらいといった感じか
水増しで1.5枚程度にしたものと、水増しなしで比較、使用モデルは
"cl-tohoku/bert-base-japanese-whole-word-masking
全てのカテゴリで精度が上昇しているが、テキスト量が少なかったラベル2での向上が著しい
コメント
最近あまり見聞きしてなかった古のテクだけど、まだ効果があるみたい
ちなみにみら翻訳の価格は以下の通りで、学習データ増強につかうのであれば定額プランを使いたいところ。
DeepL翻訳の場合は以下の通りで、こっちの方が安そう?
出典
逆翻訳を使って日本語テキストの学習データを拡張(水増し)してみた