論文コンペちょっとやりました参加記録
はじめに
probspaceにて開催されたコンペティションに参加した記録です.public/private共に19位でした.内容は「論文の引用数を予測」する問題でした.課題設定的にどのような情報が引用されるのに,起因されるのかを出題者は見たかったようです.あまり多くの時間を使うことはできませんでしたが,やった備忘録です.
やったこと
テキスト
前処理は以下をdefaultに指定.
import texthero as hero import texthero.preprocessing as heropreprocess pipline = [ heropreprocess.fillna, heropreprocess.remove_digits, heropreprocess.remove_html_tags, heropreprocess.remove_angle_brackets, heropreprocess.lowercase, heropreprocess.remove_whitespace ]
letter count 「authors,submitter, title, comments, abstract, doi, journal-ref, report-no,license」のそれぞれの文字数
word count 「 author,comments,title,abstract,journal-ref,categories_word,」の単語数.(authorとかはandとかで区切ったりした.)
GoogleNews-vectors-negative300の学習済みモデルを「abstract,title,comments」に適応(SVDで70次元にした).
- citesがあるのみでword2vecで学習「abstract,title」を作成 (SVDで70次元にした)
- journal-refやcommentsの特定の単語で0or1(ex:accept,publishなど)
- commentsのpagesやfiguresの数.
- titleとabstractの類似度
- journal-refのcharaで特徴量作成.以下コード
TfidfVectorizer(analyzer = "char",ngram_range = (1,4),max_features = 50000))
*title,abstract,commentsでtfidfをした (SVDで70次元にした)
カテゴリ
- versionsやdate情報での差分とか色々.以下コード
df["versions_datetime"] = pd.to_datetime(created_list) df["versions_year"] = df["versions_datetime"].dt.year df["versions_day"] = df["versions_datetime"].dt.day df["versions_month"] = df["versions_datetime"].dt.month df["versions_week"] = df["versions_datetime"].dt.week df["versions_weekday"] = df["versions_datetime"].dt.weekday
- doiやsubmitterなどでのaggregation.
- categories,submitter,licenseなどの特徴量でカウントエンコーディング
色々
- authors_parsed での最も多い所属機関
- LicenseがArxivかCreativecommonsか
- doi_citesを予測したものを特徴量に.
feature_importance
実装備忘録
今回はコンペ用に色々取り揃えた特徴量作成コードを作ったので,これを元にやってみようかな,という理由で参加していました.なので,あまり複雑なことはできていませんでしたが,どういう風に自分はやっているかというのだけ載せておきます.今後改良できたらいいなぐらいに考えております.
基本的にはMixinというクラスを作成して,必要なものを継承するように作っています.
class BaseFeatureMixin(object): #一部だけ載せてます. def __init__(self,encoding_word): self.encoding_word = encoding_word self.path = os.path.join(PROCESSED_ROOT,encoding_word + ".csv") def LoggerCheck(self,train_df,test_df,df): LOGGER.info("--------------------------------------") LOGGER.info(f"{self.encoding_word} OBJECT TRAIN SHAPE : {train_df.shape}") LOGGER.info(f"{self.encoding_word} OBJECT TEST SHAPE : {test_df.shape}") LOGGER.info(f"{self.encoding_word} OBJECT DF SHAPE : {df.shape}") def make(self,train_df,test_df): self.EXCEPT_COLUMNS = train_df.columns n_train = len(train_df) if os.path.exists(self.path): #既に特徴量が作られている場合はread_csvで読み込む. df = pd.read_csv(self.path,index=False) else: df = pd.concat([train_df, test_df]).reset_index(drop=True) df = self.create_features(train_df,test_df,df)#特徴量を作成 df = self.create_use_features_df(df) #不必要な特徴量を消去し必要なdfだけを返却 self.peform_create(df,self.encoding_word) #作成された特徴量は保存 train_df = df.iloc[:n_train].reset_index(drop=True) test_df = df.iloc[n_train:].reset_index(drop=True) self.LoggerCheck(train_df,test_df,df) return train_df,test_df
これのモチベーションは「作成したファイルは何度も作りたくないのと,作った特徴量を保存しておきたいこと」にあります.このMixinクラスと他のMixinクラスを重ねていくことで,汎用的な前処理あるいは特徴量作成を行っていきます.
上記のMixinの作り方はDjangoRestFrameworkに引っ張られています.peform_createとかっていうのがありますが,これはDjangoでデータベースに保存してくれるときに使うメソッドで,ここでも同じようにcsvで保存するときに使っていますwww
from utils.features.base import BaseFeatureMixin from utils.features.feature_en import FrequencyMixin class CategoricalEncode(FrequencyMixin,BaseFeatureMixin): def __init__(self): super(CategoricalEncode,self).__init__(encoding_word="CategoricalEncode") def create_features(self,train_df,test_df,df): use_columns = [ "categories", "submitter", "license" ] output_df = self.create_frequency_encoding(train_df,df,use_columns = use_columns) return output_df #ex:text from utils.features.base import BaseFeatureMixin from utils.features.feature_text import TextFeatureMixin from utils.features.feature_en import FrequencyMixin class BasicText(TextFeatureMixin,FrequencyMixin,BaseFeatureMixin): def __init__(self): super(BasicText,self).__init__(encoding_word="BasicText") def create_features(self,train_df,test_df,df): use_columns = [ "comments", "title", "abstract", "journal-ref", ] output_df = self.create_text_count_feature(df,train_df,test_df,use_columns,method_list=["word_counts","letter_counts"]) other_output_df = self.create_text_count_feature(df,train_df,test_df,["submitter","license","doi"],method_list=["letter_counts"]) other_df = self.create_other_features(df,len(train_df)) output_df = pd.concat([output_df,other_output_df,other_df],axis=1) return output_df
例えば基本的なカテゴリカルの特徴量を作りたいときは,上記のようにクラスを記載し,Mixinクラスを継承します.これらのMixinのメソッドの命名規則として,createが最初にくるメソッドは必ずDataFrameが帰ってくるようにしています. また,Mixinで作るのが難しい,特徴量に関しては,create_other_codingで記載するようにしています.return で返すdfはtrain_df,test_dfに含まれる特徴量を含んでいても含まなくてもOKのようにtrain_dfに含まれる特徴量は自動的に外されるようにしています.
from src.main.experiment.text_0001 import BasicText from src.main.experiment.date_0001 import BasicDate from src.main.experiment.categorical_0001 import CategoricalEncode from src.main.experiment.aggregate_0001 import Aggregation for FeatureObject in [BasicText,BasicDate, CategoricalEncode,Aggregation]: output_train, output_test = FeatureObject().make(train_df, test_df) feature_debug(output_train, reverse=True) train_df = pd.concat([train_df, output_train], axis=1) test_df = pd.concat([test_df, output_test], axis=1)
という感じで,必要となる特徴量のクラスを記載していき,concatで繋げていくと必要な特徴量で[train,test]が構築されていきます.
この状態でちゃんと特徴量が作られているかを確認するために,feature_debugで作らている特徴量をターミナルで出力して見えるようにしています.
-------------------------------------- RAW_W2VFeature OBJECT TRAIN SHAPE : (15117, 140) RAW_W2VFeature OBJECT TEST SHAPE : (59084, 140) RAW_W2VFeature OBJECT DF SHAPE : (74201, 140) ---------------COLUMNS EXAMPLE 30----------------------- Index(['SVD_RAW_W2V_abstract0', 'SVD_RAW_W2V_abstract1', 'SVD_RAW_W2V_abstract2', 'SVD_RAW_W2V_abstract3', 'SVD_RAW_W2V_abstract4', 'SVD_RAW_W2V_abstract5', 'SVD_RAW_W2V_abstract6', 'SVD_RAW_W2V_abstract7', 'SVD_RAW_W2V_abstract8', 'SVD_RAW_W2V_abstract9', 'SVD_RAW_W2V_abstract10', 'SVD_RAW_W2V_abstract11', 'SVD_RAW_W2V_abstract12', 'SVD_RAW_W2V_abstract13', 'SVD_RAW_W2V_abstract14', 'SVD_RAW_W2V_abstract15', 'SVD_RAW_W2V_abstract16', 'SVD_RAW_W2V_abstract17', 'SVD_RAW_W2V_abstract18', 'SVD_RAW_W2V_abstract19', 'SVD_RAW_W2V_abstract20', 'SVD_RAW_W2V_abstract21', 'SVD_RAW_W2V_abstract22', 'SVD_RAW_W2V_abstract23', 'SVD_RAW_W2V_abstract24', 'SVD_RAW_W2V_abstract25', 'SVD_RAW_W2V_abstract26', 'SVD_RAW_W2V_abstract27', 'SVD_RAW_W2V_abstract28', 'SVD_RAW_W2V_abstract29'], dtype='object')
これらの機構を作っているとあまり多くのコードを書かずに,速度をあげてコンペに参加できるようになりました.
終わりに
コンペっていいですね.
自然言語処理お勉強教室-LSTM系列振り返り(RNN,LSTM,GRU)-(6)
はじめに
この記事では,これまで勉強してきたRNN,LSTMを振り返ります.その上で,ネット上で散見しているLSTMと似た構造であるGRUを勉強します.また,これらの内容はネットでかなりの情報があるので,この記事は私が勉強したという備忘録となります.新規性とかそういうのは特にありません.
RNN
例えば,文章の
you say goodbye and I say hello
が与えられたとします.この時,RNNはyouという単語が来た場合,sayという単語を予測します.こうすることで,単語の時系列を学習することができ,単語の順序関係を考慮したモデルを構築することができます.また,過去の情報を次に渡すために,youのベクトル情報がsayに渡されるので,情報を保持することができます.
LSTM
LSTMはRNNの改良版です.RNNは文章が長くなればなるほど,最初の単語の情報量が失われます.そこで,LSTMは過去の情報をより保持するために,c,hにわけ,過去情報をうまく伝達するモデルです.短期と長期の記憶を分割している. これにより,RNNの情報が失われる欠点を補いました.
双方向LSTM
双方向LSTMは時系列の順序を逆にして,情報を後ろに流し,情報を増やす構造です.画像を作ろうと思いましたが,面倒だったので,以下のURLの画像を参考にさせていただきました.
上記の構造では,右から情報が伝わるものと左から情報が伝わるものがあります.LSTMの構造がわかっていると,難しくはないと思います.o_t-1のところで,情報をconcatして一つのベクトル表現で表します.
lstm = nn.LSTM(3, 4, batch_first=True, bidirectional=True)#bidirectionalをTrueにするだけ.
PyTorchで使う場合は,通常のLSTMとはoutputの次元が異なります.丁寧に調べてくれている参考があったので載せておきます. qiita.com
GRU
GRUの構造はLSTMと違いc_tの部分を廃止しています.なので,すっきりとした形にはなっていると思います(LSTMと比べては).この記事ではどのようなoutputがあるかを記載だけしておきます.
import torch import torch.nn as nn gru = nn.GRU(3,3,batch_first = True) sample = torch.rand(1,4,3) #(batch_size,sequence,dimention) output,h = gru(sample) print(output.size()) #torch.Size([1, 4, 3]) print(h.size())#torch.Size([1, 1, 3]) print(output[0][3] == h) # [True,True,True]
outputはそれぞれのセルで出力されたhの情報.hは最終のセルのアウトプット情報.なので,(output[0][3] == h)は一緒です.
終わりに
これまで色々なモデルを見てきました.これからはおそらく,Attentionベースの勉強をしていきたいと思います.LuongのAttentionはやったので,次はGNMTのgoogle translateのモデルを見ていこうと思います.
Javascriptのここが少しムズイ8選
はじめに
この記事はJavascript関連の技術を勉強していて,ここが少しムズイなと個人的に思ったことを記載します.基本的にはネットに乗っていることが多いので,この記事は私が勉強したという備忘録です.内容的には,Javascriptとnpmとnode関連のことを記載します.
this
thisはモジュール内で定義した変数を指し示すときによく使います.
class Hoge { constructor(name) { this.name = name; console.log(this.name) // nameが出力 } console.log(this) //window.
個人的にthisの覚え方として,一個前のものを指し示すという感じで覚えています.上の例であると,thisはコンストラクターの一個前のHogeをさします.何も書かれていない場所で,thisを記載するとwindowの内容が現れます.
class Hoge { constructor(name) { this.name = name; setTimeout(function(){ console.log(this) //window }, 1000); } }
上の例であると,windowの内容が現れます.先ほども言った通り,thisは一個前のものを指し示すので,setTimeoutが指し示されます.setTimeoutはwindowのオブジェクトなので,thisはwindowをさします.
class Hoge { constructor(name) { this.name = name; setTimeout(function(){ console.log(this) //bindを入れると,Hogeのクラス情報が出力. }.bind(this), 1000); } }
setTimeoutや他のWindowObjectの中で,コンストラクターや他の関数で定義した内容を入れたい場合は,bindを使います.bindは関数の中に〇〇というオブジェクトの内容を入れたい!と思ったときに使えばいいかと思っています.とはいえ,使い方は以下の通りです.
function名.bind(thisに対応する変数[, 引数に対応する値...])
面白い例に以下があります.
class Hoge { constructor(name) { this.name = name; const word = "hoge"; setTimeout( function () { console.log(this); //hoge }.bind(word), 1000 ); } }
これで出力されるのはwordで格納されたhogeとなります.bindはthisになりかわるものであることがわかります.thisの記載方法は他にアロー関数がありますが,他のセクションで扱います.
参考:
vueの記載の方法ですが,面白い内容です. tadaken3.hatenablog.jp
分割代入
バラバラの変数で受け取りたいときに使います.
const obj = { A: 1, B: 2, C: 3 }; const { A, B } = obj; console.log(A); //=> 1 value側が取得できる. console.log(B); //=> 2 console.log(b) ; //=> undefined
受け取りたいものがオブジェクトであったとき,key側の変数を指定することで,valueの値を取得できます.この書き方であると記載が完結になるとともに,見やすくなります.また,一括に一つの変数に格納したい場合は以下のように記載する.
const obj = { A: 1, B: 2, C: 3 }; const { A, B } = obj; const C = { ...obj }; console.log(C); // { A: 1, B: 2, C: 3 }
...と3個繋げて,入れてやると全部代入されます.フレームワークであると,gulpやvueStoreで同じような記載が見られるます.
const { src, dest } = require("gulp"); // const gulp = require("gulp") // gulp.srcと一緒.gulp.dest
gulpであると上記のようにrequireでモジュールを格納し,{src,dest}で受け取る方法がある.一方で,gulp.srcで記載する方法があります.ネットではこのような書き方が見受けられたので載せておきます.一緒です.
actions: { increment ({ commit }) { //分割代入 commit('increment') } increment (context) { //これでも一緒の書き方です context.commit('increment') } }
vue storeのaction内で第一引数に受け取るものが,contextです.その中に{commit,dispatch,}やらが入っているので,それを省略して取得するために,上記のような書き方で省略して使います.
参考:
import/export
import/exportとexports/requireの書き方があります.やっていることはほぼ同じなのに二つのやり方で外部モジュールを読み込むことができます.さて,このセクションではimport/exportです.これは,ES2015(ES6)での書き方です.nodeとか関係なく使うことができます.
//hoge.js class Hoge { constructor(name) { this.name = name; } } export default Hoge; //defaultを使う //index.js import Hoge from "./hoge.js"; const hoge = new Hoge("hogehoge");
exportしたモジュールはimport文にて受け取ることができます.defaultはその名の通り,importしたとき基本的にはこれが呼び出されるぞ!!というもの.そのため,from ./hoge.jsをしたらdefaultで記載されているものが呼び出されていると解釈してもいい,という風に私は覚えています.この場合であると,Hogeというモジュールがパッケージ化され,それがindex.jsで使えるようになっているというわけです.
//hoge.js export class Hoge { constructor(name) { this.name = name; } } //index.js import { Hoge } from "./hoge.js"; const hoge = new Hoge("hogehoge");
上の例はexportするときにdefaultをつけていない例です.from "./hoge.js"しても,importするときにどのモジュールを呼び出すかがわかりません.そのため,{}を記載して,明示的にどのモジュールを呼び出すかを記載する必要があります.
import/exportを使うことはたくさんあります.例えば,vueStoreの書き方でよく取る方法は以下の通りです.
import Vuex from "vuex"; import createPersistedState from "vuex-persistedstate"; import auth from "./auth/index"; import error from "./error/index"; Vue.use(Vuex); const store = new Vuex.Store({ modules: { auth, error }, plugins: [ createPersistedState({ key: "vuex", paths: ["auth.isLogedIn", "auth.user"], storage: window.sessionStorage, }), ], }); export default store; //exportしている.
vueStoreの場合はstoreの内容をexport defaultしているので,どこでもimport して使うことができます.
参考:
export/require
exports/require は、Node.js(CommonJS)での書き方です.
//hoge.js class Hoge { constructor(name) { this.name = name; } } module.exports = Hoge; //moduleを使って外部に渡す. //index.js const Hoge = require("./hoge.js"); const hoge = new Hoge("hogehoge"); console.log(hoge.name);
という感じでrequireを使います.import文で外部モジュールを受け取るときは,export defaultとしているところをmodule.exportsにしています.const Hogeはそのモジュールをrequireで受け取っているという内容です.ここで,注意なのが,console.log()と記載していますが,これはブラウザで見るとエラーがおきます.require構文はnode.js上で動かすことができる機能なので,ブラウザでは動作しません.実行したいときは,
node index.js
とすれば,ターミナル上で実行結果を見ることができます.exportは違う書き方があるのですが,個人的に使い道があるのか判断が難しいので,ここでは割愛します.フレームワークでrequireを使う例は以下のgulpとかで見られます.
//分割代入 const { src, dest } = require("gulp"); // const gulp = require("gulp") // gulp.srcと一緒. const rename = require("gulp-rename"); function copyFiles() { return src("./src/*.html") .pipe( rename({ prefix: "hello-", //先頭につけたい言葉 }) ) .pipe(dest("./dist")); } module.exports = copyFiles;
gulpはnode上で動くのでimport文を使わずに,requireで構文を書くことが個人的にはいいのかなと思っています.
参考:
npm install --save-dev --save global
npm install --save-dev //今のプロジェクトに記載 npm install --save npm install -g //globalで今のプロジェクト以外にも--save-devをしなくても使える.
があります.これらのコマンドは外部パッケージをインストールしている内容です.基本的にはよく使うパッケージはglobalでいいと思います.ちなみに,--save-devのdevはpackage.jsonの devDependencies に記載されます.
"dependencies": { "gulp": "^4.0.2" }, "devDependencies": { "mocha": "^3.4.2" } }
上のように二つあるんです.これらの使い道は,dependenciesがnpm installをしたときに書かれている内容全てインストールしてくれるものです.一方で,devDependenciesはnpm installしてもインストールされません.理解が難しいようであれば,脳死で,npm install --save していれば多分大丈夫です. これらの使い道は以下の通りかなーと思います.
npm install -g @vue/cli //これはよく使うのでね. npm install --save-dev @vue/cli //開発するときにこれは必要ないのでインストールしてもしゃーない. npm install --save @vue/cli //まぁとりあえず.
みたいな感じで使い分けたらいいんじゃないですかね.
参考:
アロー関数
アロー関数は関数の省略記法です.ですので,別に使わなくても問題なくjavascriptは書けます(本当だろうか?).とはいえ,その書き方が読みやすいらしいので,色々な場所で書かれています.さて,個人的には関数は「通常関数,アロー関数」の二つがあると思っています.
//通常関数 function hoge() {} //アロー関数 const hoge = () >= {}
という記載方法です.特段通常関数という名前はないですが,この記事ではfunction()ベースの関数を指し示すことにします.これら記載の違いは=>の書き方です.この場合だと,書き方が変わっただけなので,中身が変わるというわけではありません.じゃあ,何が便利かというのを記載します.
//普通 const arrow = () => { console.log("hoge"); }; //省略 const arrow = () => console.log("hoge"); //カッコを省略できる. //通常関数 const arrow = function(){console.log("hoge")}
という感じで,カッコを省略できます.言ってしまえばアロー関数ってそんなもんです.通常関数だとかっこが多くて見にくいところをアロー関数は限りなくシンプルに記載できます.しかし,これらの大きな違いは,thisを使った時,通常関数とアロー関数とで,挙動が変わるので注意するところはここです.
//通常関数 class Hoge { constructor(name) { this.name = name; const word = "hoge"; setTimeout(function () { console.log(this); //windowオブジェクト }, 1000); } } //アロー関数 class Hoge { constructor(name) { this.name = name; const word = "hoge"; setTimeout(() => { console.log(this); //Hogeオブジェクト }, 1000); } }
アロー関数のthisは内包しているクラスを指ししめます.アロー関数はレキシカルスコープであり,最初に定義したオブジェクトが優先されます.setTimeOutはHogeクラスが定義されたあとで,実行されるので,thisはHogeを指します.レキシカルスコープについては以下のサイトがわかりやすいです.
さて,アロー関数は他に違いがあるのかといえば,そこそこにあります.「newができない,argumentsがない」など通常関数とアロー関数とでは挙動が異なります.最初に「書き方が違うだけ」と記載しましたが,そんなわけないです.この記事では全て記載しませんが,以下のサイトを見ると理解が深まるので,読んでください.とはいえ,実用的に使う部分ではこの記事で書いたもので事足りることが多いです.
参考:
forEach()
for文を使うならば,forEachでできるか一回考えてみよう精神.これもアロー関数と同じように情緒な書き方にならないような方法です.
const hoge_list = ["hoge", "hogehoge"]; hoge_list.forEach((element) => { console.log(element); }); //hoge hogehogeが出力
forEachをすることで,リスト型の中身に入っているものを取得できます.for文だとごちゃごちゃ書かないといけないけど,スッキリしますね.
const hoge_list = ["hoge", "hogehoge"]; hoge_list.forEach((element, idx) => { console.log(element, idx); //hoge 0 hogehoge 1 });
idxはリストのindex値を参照します.forEachは便利ですが,ファンクション型のオブジェクト({"hoge":1}とかの内容は使えないです.)には利用できないので注意が必要.
参考:
eslintエラーが出まくる問題
eslint使っているとよくわからないエラーがよく出ます.vueとかを立ち上げるとdefaultでeslintの設定がされます.そして,エラーが出るという流れです.その時はエラーが出ていいる場所に以下のコードを描いてあげましょう!!
// eslint-disable-next-line register({}, authData) { await axios .post("api/rest-auth/ragistration/", { username: authData.username, email: authData.email, password1: authData.password1, password2: authData.password2, })
これで,{}が何も指定されなくてもエラーが出ません!便利ですね.
querySelector とか getElemetIdとかgetElementsByClassNameどっちやねん.
個人的にはquerySelectorを使う方法で統一した方がいいと思います.getElementIdはそのなの通りIDしか取得できません.またgetElementByClassNameとかありますが,これもclassのみです.一方で,querySelectorは両方取得できます.なので,こっちを使うほうが便利です.
<body> <div id="hogehoge"> <p class="hoge">hoge</p> </div> <script src="sample.js"></script> </body>
const hoge = document.querySelector(".hoge"); hoge.classList.add("hogee"); console.log(hoge); const hogehoge = document.querySelectorAll("#hogehoge"); hogehoge.forEach((element) => { element.style.color = "red"; //文字の色が赤色になる. element.classList.add("hogee"); //hogeeのクラスが付与される. });
querySelectorAllは指定したクラスを持っている全てのDOMを取得します.実用的には以下のような形でDOMを取得したり変更したりします.
class ClassObserver { constructor(entries, addoptions) { this.entries = document.querySelectorAll(entries); //targetはclass名前id名前に指定 this._init(addoptions); } //parameterの追加 _init(addoptions) { this.options = { root: null, rootMargin: "-100px", //交差点の差分(発火) }; if (addoptions !== undefined) { this.options.update(addoptions); } } cb(entries, observer) { entries.forEach((entry) => { if (entry.isIntersecting) { //要素が入っている時 entry.target.classList.add("inview"); observer.unobserve(entry.target); //targetの中にDOM要素がある。これ以降は監視対象から除外 } else { entry.target.classList.remove("inview"); } }); } run() { this.entries.forEach((entry) => { const io = new IntersectionObserver(this.cb, this.options); //画面に入った時と出た時を監視 io.observe(entry); //監視 }); } } export default ClassObserver;
終わりに
Javascriptって結構よくわからない書き方が多いですね.でも,一つ一つ見ていけば,案外ふーんというものが多かったります.
自然言語処理お勉強教室-Luong Attention-(5)
はじめに
この記事ではLuong Attentionの内容について触れたいと思います.Attentionの記事はネット上で沢山転がっているので,この記事は私が勉強したという備忘録です.
Attentionが解決したかったこと
既存の手法にはLSTMやGRUと呼ばれる手法がありました.これらの手法は多くの手法で活用され,seq2seqなどで応用されてきました.しかし,LSTMやGRUには,最初の単語の情報が後になればなるほど,失われるという問題がありました.この原因は必要でない情報をLSTMでは削ぎ落としていくので,新しい情報が最後には残ってくるようになるからです.Attentionは最初の情報も失われないように,学習できるように構築されたモデルとなります.巷では,Attention is all you needとかのTransformerとか有名なものがありますが,基本はAttentionです.この技術を学んでおくことで,応用がいくらでも効くようになります.じゃあ,いきなりTransformerを見ていくかといえば,そうではなく,古めのアイデアだが,今のトレンドの骨格となっているLuong Attentionの技術を今回は勉強します.Luong Attentionのモデルはseq2seqのEncoderとDecoderのあるモデルを利用した時に,Encoderの情報をより詳細にDecoderに渡すモデルです.
Luong Attention
Luong Attentionの論文では以下の図でモデルが表されています.
青い部分がEncoderで赤い部分がDecoderとなります.これだけであるとseq2seqと変わりません.この図の書かれているh_sについてはEncoderのLSTMのかくセル状の出力です.一方で,h_tはDecoderの各セル状の出力です.seq2seqであると,h_tを線形関数に放り込んで,次に来る単語を予測していました.さて,この図を見る限りその役目は$h^~_t$にありそうです.これは以下のように定式化されます.
) ]
となります.ここで,c_tはcontext vectorであり,h_tはDecoderの出力です.W_cはLinear関数での線形関数の重みです.急にc_tが出ましたが,これが今回の肝となる内容です.
attention weight
c_tの導出方法はまず,a_tのattention weightと呼ばれるものを導出しないと導き出せないので,まずはこいつを算出しましょう.attention weightの算出方法は色々とありますが,一番簡単なのが行列演算をする方法です.数式は以下の通りです.
という感じの数式になります.score関数は各h_sの行列全てとh_tの内積を計算しています.下にイメージ図を記載します.
上の図のh_sの部分の行はEncoderのword数を表します.Encoderしたword数が10であると,画像のような形になります.右の部分の転地したものは列がword数を表します.行列演算を行った時,赤い枠で囲まれたベクトル同士が演算されます.これは,Encoderの出力全てのh_sとDecoderの出力であるh_tが演算されるのと同値であり,演算後は(10,5)というmatrixになります.縦の列だけを見ると,h_sの全てのベクトルとh_tが掛け算されたベクトルになっていることがわかります.図にすると以下の通りです.
図に示されているh_s_1とh_t_1の情報というのはEncoderの最初のwordとDecoderの最初のwordを掛け合わせたときに出力された情報です.これらを縦にsoftmax関数でとり,各値を確率値としておきます.
context vector
上の内容でattention weightを導出することができました.a_tのベクトルは上記の画像の例であると(10,5)となっています.このベクトルをh_sを掛け算していきます.これにより,h_tにとってh_sここの情報は優位であるかどうかを計算します.a_tは確率値となっているので,h_s各wordの重要性を計算できます.イメージ図は以下の通りです.
h_sの行の内容が先ほどのsoftmaxの行列と掛け算されます.この図を見ると,0.05の値の方が小さいですが,0.2の値はでかいようです.ということは,h_s_10とh_t_1は単語同士重要な関係があるのではないか?というのがわかりますね.よく画像で相関係数のような画像がありますが,この内容が理解できると,画像の意味がわかるかと思います.これの出力matrixは(10,100)です.これを一つのmatrixにし,(1,100)とします.これは縦のベクトルの和をとったり平均することで導出することができます.この操作はh_tの出力ぶんだけ行うので,今回の例であると5回ぶんです.なので,最終的なmatrixは(5,100)となります.最初のh_tのmatrixと同じになりました.
最初に戻る.
) ]
という数式を最初に出しましたが,あとは単なる線形回帰となります.これにて単語の予測をすることができました.
実装
いつかやります.Encoderはseq2seqとほぼ変わりませんのでこれだけ載せておきます.
# Encoderクラス class Encoder(nn.Module): def __init__(self, vocab_size, embedding_dim, hidden_dim): super(Encoder, self).__init__() self.hidden_dim = hidden_dim self.word_embeddings = nn.Embedding(vocab_size, embedding_dim,) self.gru = nn.LSTM(embedding_dim, hidden_dim, batch_first=True) def forward(self, sequence): embedding = self.word_embeddings(sequence) output, state = self.LSTM(embedding) return output, state #outputにEncoderのかくセルの出力が格納されている.stateは最終出力が格納されています.
終わりに
今回はLuong Attentionを見ましたが,難しいですね.次は色々LSTM,GRUとかの振り返りでもしましょうかね.GNMTとかも見たいかもしれません.
自然言語処理お勉強教室-seq2seq-(4)
はじめに
この記事ではseq2seqを取り扱います.seq2seqはweb上で様々な情報が公開されているます.そのため,この記事は私が勉強したよという備忘録です.
seq2seqがやりたいこと
前回の記事ではLSTMを勉強しました. tatsuya-happy.hatenablog.com LSTMはRNNで取扱えなかった,長い文章を取り扱えるように,開発されたアルゴリズムです(個人的にはそう思っている).長い文章を取扱えなかったのはRNNにおいて,過去の情報がほとんど失われるためです.そこで,LSTMは情報を出来るだけ失わないように,過去の情報を伝達できるモデルとなりました.seq2seqはこのLSTMモデルを応用することで,機械翻訳,文章生成などの分野に幅を見出そうとしました.
seq2seqのモデル構造.
web上で画像を見ていて構造が,一番わかりやすかったのが,以下の画像でした. seq2seqのモデル構造には「Encoder,Decoder」と呼ばれる重要な構造があります.この画像の場合は,文章生成の分野に該当し,入力文章は「how are you ?」です.この文章をEncoderに入力し,「how are you?」が意味しているベクトルを出力します.このベクトルが,Decoderに入力されます.Decoderは「how are you?」の情報を受け取り,「I am good」の文章を生成できるように学習します.これら一連の文章の流れを可能にさせているのがLSTMです.そのため,seq2seqは二つのLSTMのモデルにより構築されたモデルとなります. seq2seqは上記の画像のような割と単純な構造で,図に表すことができます.出力であるhは前述した「how are you?」の情報を含めたベクトルとなります.実装は以下のサイトでやられている奴が一番参考になるかと思います(PyTorchですが). qiita.com
#Encode class Encoder(nn.Module): def __init__(self, vocab_size, embedding, hidden_dim): super(Encoder, self).__init__() self.hidden_dim = hidden_dim self.word_embeddings = nn.Embedding(vocab_size, embedding) self.lstm = nn.LSTM(embedding, hidden_dim, batch_first=True) def forward(self, sequence): embedding = self.word_embeddings(sequence) _, state = self.lstm(embedding) # state = (h, c) return state # Decoderクラス class Decoder(nn.Module): def __init__(self, vocab_size, embedding, hidden_dim): super(Decoder, self).__init__() self.hidden_dim = hidden_dim self.word_embeddings = nn.Embedding(vocab_size, embedding) self.lstm = nn.LSTM(embedding, hidden_dim, batch_first=True) self.hidden2linear = nn.Linear(hidden_dim, vocab_size) def forward(self, sequence, encoder_state): embedding = self.word_embeddings(sequence) output, state = self.lstm(embedding, encoder_state) output = self.hidden2linear(output) return output, state
このようにEncodeクラスとDecoderクラスを別々に作ります.中身と見ると,Encoderにはlstmがあり,確かに一つのモデルしかありません.返り値のstateは(h,c)です.LSTMの構造を見ても,事前情報で入力されるのは(h,c)でした.Encoderでは隠れ層を何次元にするか?の情報しか書かれていませんでしたが,おそらく内部的には,(h,c)でも受け取れるように構築されているのでしょう.
Decoderの部分はlstmで出力させたあと,Linear関数で次にどの単語がくるかを予測させるようにしています.nn.Linearの隠れ層の出力がvocab_sizeになっているのが確認できます.
さて,これを一つのモジュールとして表します.
class Seq2Seq(nn.Module): def __init__(self, encoder, decoder): super(Seq2Seq,self).__init__() self.encoder = encoder self.decoder = decoder def forward(self): ##まだ書いてません return outputs
こうしてしまえば,一つのモデルとして構築でき,学習させるコードを書くときにいちいちEncoderとかDecoderとかで出力する値を書く必要がなくなります.
実装
きちんとした実装はまた今後あげます.
終わりに
今回はseq2seqを勉強しました.LSTMの構造がわかっていれば,そこまで内容的には難しくないように思えます.次はこの技術を応用した,Luongが提案したAttentionの概念について勉強したいと思います.
自然言語処理お勉強教室-LSTM-(3)
はじめに
この記事ではLSTMを取り扱っていきます.LSTMの内容はネット上でたくさん上がっていると思うので,この記事は私が勉強したという備忘録を残すためにやっております.前回の記事に,RNNをやったので合わせてお読みください.
LSTMが解決したいことの問題
LSTMとRNNの大きな異なるのは情報量だと捉えています.RNNは過去の情報を多く捉えることができませんが,LSTMは言葉が離れていてもRNNと比べると,多くの情報量を捉えることができます.RNNがそれをできないのは構造上の過去の情報を表す,h_tが情報を失っていくためです.例えば,以下の
you say goodbye and I say hello.
というフレーズがあった時,最後の層でhelloを予測しようとして,h_tにyouの単語を表す情報量がどれだけ残っているのでしょうか.具体的な数値で表すことは難しいですが,文章のword数が増えるごとに前半の情報を残すというのは,RNNで厳しいかもしれません. その問題を解決するために,LSTMがあります.過去の必要でない情報を削減することで,必要な情報だけを次のニューロンに渡し,新しいデータと照らし合わせ情報を更新していきます.これをモデル化することで,今までの,過去の情報が失われる問題を解決しようとしました.
LSTMの構造
RNNとLSTMの構造が以下のような図で表せます.上の方がRNNのモデルで,下の方がLSTMです. RNNは出力である,h_t-1とx_tをtanhで足し算するような流れでしたが,LSTMは横のニューロンにつなげる時は,h_t-1,c_t-1の二つを挿入します.まぁ,構造知らずに使う分にはこのCが一つ増えたという感じです.実際にPyTorchのnn.LSTMの出力は,Outputs: output, (h_n, c_n)となっており,二つです. さて,LSTMの構造の中身ですが,ネットで色々調べたところ,下の画像があふれていました.
https://qiita.com/KojiOhki/items/89cd7b69a8a6239d67caで乗っていた画像を貼っています. 見たらわかると思いますが,ごちゃごちゃしています.ただ,LSTMの構造には「忘却ゲート,入力ゲート,出力ゲート」と呼ばれる機構があるので,それごとにみていけば把握できそうです.
忘却ゲート
数式の参考は以下です.
忘却ゲートはx_tと前の出力であるh_t-1との足し算です.ここの数式はRNNと一緒です.活性化関数がRNNはtanhでしたが,ここではシグモイド関数を利用します.シグモイド関数を利用することで,値が0~1になります.これと,c_t-1とかけ算をして,不必要な情報は忘却します(f_tの関数が0~1の値を取るため,必要でない値は0に近づき大きな値だけが情報量として残る).ここの解釈は,新しいデータxがきてその値と前の値を比べて,「過去」の必要な情報を残しましょー,というモチベーションです.
入力ゲート
入力ゲートは新しいデータのうち,「現在」の必要なデータを次に残していきましょー,というのがモチベーションです.そのために,前のh_t-1の情報と新しい情報のx_tを計算します.一つ目が,0~1の範囲で表すためにシグマで出力します.これはh_t-1の情報と新しい情報のx_tとで計算し,必要となってくるベクトルの情報だけが,値を大きくして出力します.これに,tanhで計算したものを掛け算することで,値が大きい情報だけが残り,重要な情報が残ります.
**ここの入力ゲートは色々議論されており,中にはこの図で描いた構造とは違う,ものがweb上には沢山あるかと思います.というのも.シグマだけで計算して,出力してもいいのでは?と思うかもしれません.というように,色々ここは研究されている部分でもあるので,調べる必要があるかもしれませんね.
忘却ゲートの値と先ほど計算したものを合わせ,c_tを出力します.これで,情報が更新できました.
出力ゲート
更新されたc_tの情報と過去の情報を考慮して,値がでかいものを出力しています.まず,過去の情報を0~1に正規化し,重要な値が残ります.次に,c_tにtanhを入れることで-1~1の値に正規化します.これらを掛け算することで,重要な値のみを残し,情報として次のニューロンに繋げます.
実装
class LSTM(nn.Module): def __init__(self,vocab_size,embed,hidden): super(LSTM,self).__init__() self.embed = nn.Embedding(vocab_size,embed) self.n_hid = hidden self.linear_fx = nn.Linear(embed,hidden) #W_x*x_f (x_f matrix is (1,hidden)) output = (1,hidden) self.linear_wh = nn.Linear(embed,hidden) #W_h*h_ft-1 (h_t-1 matrix is (1, hidden)) output= (1,hidden) ソフトマックスで単語を予測しているのだから,次元はhidden self.linear_ix = nn.Linear(embed,hidden) #W_x*x_i self.linear_ih = nn.Linear(embed,hidden) #W_x*h_it-1 self.linear_gx = nn.Linear(embed,hidden) #W_x*x_g self.linear_gh = nn.Linear(embed,hidden) #W_x*h_gt-1 self.linear_ox = nn.Linear(embed,hidden) self.linear_oh = nn.Linear(embed,hidden) self.linear = nn.Linear(hidden,vocab_size) def forward(self,x,h_hidden,c_hidden): x = self.embed(x) #忘却ゲート f_t = F.sigmoid(self.linear_fx(x) + self.linear_wh(h_hidden)) #入力ゲート i_t = F.sigmoid(self.linear_ix(x) + self.linear_ih(h_hidden)) g_t = F.tanh(self.linear_gx(x) + self.linear_gh(h_hidden)) c_t = (c_hidden * f_t) + (i_t * g_t) #c_t = (c_t-1*f_t) + (i_t*g_t) #出力ゲート o_t = F.sigmoid(self.linear_ox(x) + self.linear_oh(h_hidden)) h_t = F.tanh(c_t) * o_t output = self.linear(h_t) return output,h_t,c_t def initHidden(self): return torch.zeros(1, self.n_hid) def initC(self): return torch.zeros(1,self.n_hid)
終わりに
今回でRNNとLSTMに関する内容を終わります.LSTMは今でもしばしば使われる汎用モデルです.勉強しておくといいかもしれません.とはいえ,最近はAttentionやtransformerやらでてんやわんやしてます.まだ,そこには触れず,過去モデルを扱っていこうと思います.次は,seq2seqを攻めていこうかなと思います.その次ぐらいにはAttentionの最初のモデルをみていこうかなと思います.
自然言語処理お勉強教室-RNN-(2)
はじめに
この記事で取り扱うのはRNNです.RNNの記事や実装などはそこら中にあるので,これは私が勉強したよという備忘録となります.
RNN
リカレントニューラルネットワークと呼ばれるものです.こいつが役に立つのは時系列でデータを学習することができるということ.前のニューロンで学習したデータを次のニューロンに渡すことができるので便利だということです.さて,図は以下のような感じです.
X_t-1というデータがあったときに,あるh_t-1というデータを予測します.そのときに用いられた,データが次のニューロンにも利用されます.そうすると,x_t-1で利用されたデータの値が,x_tで利用されることになります.四角の部分には足し算をします. 実際にこれを自然言語処理で取り扱ったときは以下のように書くことができ,文章の生成に応用することができます. 例えば,文章の
you say goodbye and I say hello
があった時のモデルを一部作りました.最初のyouの次の単語がsayとなりますので,これを予測します.x_t_-1にyou,h_t-1に予測するsayがきます.これを続けていくと,最後のhelloまでにたどり着きます.こうすることで,とある単語が来たときに,次のどのような単語が来るのか?ということを予測することができるモデルが構築されます.重みは2種類存在し,一つ目が,隠れ層に対しての重みWであり,二つ目が新しいデータに対してのVとなります.
単語のデータの表現方法は,例えば,one-hotベクトルで入力する方法があります.これを用いて,softmax関数と交差エントロピーでloss関数を計算すると学習が進みます.
#input_line= #[ #[[0,0,1,0,0]], #[[1,0,0,0,0]] #] 3 dimention 1 dimention , word dimention #targetがsayだとすると,outputもsayとなって欲しいように学習する. n_hid = rnn.initHidden() for i in range(len(input_line)): output,h_hid = rnn(input_line[0],n_hid) #input_line[0] [[0,0,1,0,0]] loss = criterion(output,target) loss.backward()
挿入されたテキストごとにlossを計算し,最後にbackwardする感じで勾配の計算をすることができます.
数式を見てみます.
あまり込み入った計算をしないので,重要なところはh_t-1とx_tのデータを足し合わせるところです.
[tex:
ht=tanh(Wh_{t−1}+Vx_{t})
] *hatenablogの数式がうまくいかない.
まぁ,隠れ層から出力されたデータh_t-1と重みWを行列演算します.次に,新しいデータに対して,Vという重みとx_tを行列演算します.
#参考: [https://qiita.com/tsubasa_hizono/items/d3095a4dc7e4cf91ffdb] import torch import torch.nn as nn class RNN(nn.Module): def __init__(self,n_in,n_hid): super(RNN,self).__init__() self.n_hid = n_hid self.i2h=nn.Linear(n_in,n_hid) self.h2h=nn.Linear(n_hid,n_hid) def forward(self,x,n_hid): h_out=self.i2h(x)+self.h2h(n_hid) #足し算部分です. h_out=torch.tanh(h_out) #活性化関数で,勾配消失を抑制 h_out=h_out.detach() output = self.dropout(h_out) output = self.softmax(h_out) #単語を予測するときにsoftmaxをする.softmaxをするときはnn.NLLossです. return output,h_out @classmethod def initHidden(self): return torch.zeros(1, self.n_hid)
こんな感じで実装すると,RNNが学習できると思います.いづれちゃんとした実装書いてkaggleのnotebookに投稿します.
終わりに
今回は古きモデルRNNをみました.難しいですね.