pikesaku’s blog

個人的な勉強メモです。記載内容について一切の責任は持ちません。

sedコマンドの使い方

コマンドの役割

manでは、"stream editor for filtering and transforming text"
入力データをフィルタしたり置き換えたりする。

sedコマンドで指定する項目

①オプション

スクリプト

③読み込み対象(FILE or STDIN)

動作フロー

①1行データを読み込み、「パターンスペース」と呼ばれる記憶領域に保存
②パターンスペースに保存されているデータに対して処理を行う
③パターンスペースの内容を出力してパターンスペースを空にする
④①に戻り次の行の処理を行う
 

スクリプトの指定方法について

・-e,-E,-fオプションでスクリプトを指定
・-e,-Eはスクリプト文字列を引数に取る(違いは後述)
・-fはスクリプトが記述されたファイルを引数に取る

スクリプトの構成要素

・以下要素があり
 アドレス
 コマンド
・アドレスはコマンドの実行対象を指定。以下で指定が可能
 ①行情報
 ②正規表現
・アドレスは、範囲指定も可能
 例)n行目からm行目まで
 例)n行目からm行以降まで
 例)n行目からm行目毎
 例)正規表現Aにマッチした部分から正規表現Bにマッチした部分まで
・コマンドは処理内容を指定

実行例と動作説明

-nオプション有無の違い

例1) -nオプション無の場合
# cat ./a
a
b
# sed -e '/a/p' ./a                                                                                               
a
a
b
# 

 
①1行目(a)がパターンスペースに読み込まれる
②アドレスは正規表現で指定されている(/a/)
正規表現にマッチした1行名に対してpコマンド(結果を出力)が実行され標準出力に出力される
④パターンスペースにある1行目が標準出力に出力
⑤パターンスペースは空になる
⑥2行目(b)がパターンスペースに読み込まれる
正規表現にマッチしない為、pコマンドは実行されない
⑧パターンスペースのデータを標準出力する
⑨パターンスペースは空になる

「動作フロー」で記載したように、デフォルトでパターンスペースの行を標準出力する。
そのため、1行目が2回出力されている。

例2 -nオプション有の場合

例1の④・⑧が実行されなくなる。

# cat ./a
a
b
# sed -n -e '/a/p' ./a
a
# 

 

①1行目(a)がパターンスペースに読み込まれる
②アドレスは正規表現で指定されている(/a/)
正規表現にマッチした1行名に対してpコマンド(結果を出力)が実行され標準出力に出力される
④パターンスペースは空になる
⑤2行目(b)がパターンスペースに読み込まれる
正規表現にマッチしない為、pコマンドは実行されない
⑦パターンスペースは空になる

アドレスの指定方法

例1 行番号指定
# cat ./a
a
b
c
d
# sed -n -e '1p' ./a
a
# sed -n -e '$p' ./a
d
# 

 
"$"で最終行の指定が可能

例2 行番号範囲指定
# cat ./a
a
b
c
d
# sed -n -e '1,2p' ./a
a
b
# sed -n -e '1,+1p' ./a
a
b
# sed -n -e '0~2p' ./a
b
d
# sed -n -e '1~2p' ./a
a
c
# 

 
N,Mで行番号を指定した範囲指定が可能
N,+MでN行目からM行以降の範囲指定が可能
N~MでN行目からM行間隔で出力が可能

例3 正規表現による範囲指定
# cat ./a
a
b
c
d
a
b
c
d
# sed -n -e '/a/,/c/p' ./a
a
b
c
a
b
c
# sed -n -e '/a/~2' ./a
sed: -e expression #1, char 4: 不明なコマンド: `~'
#

 
PATTERN,PATTERNで範囲指定が可能
PATTERN~Nで行間隔指定はできない

他コマンド説明

-dコマンド

アドレスで指定された行のパターンスペースの削除を行う

# cat ./a
a
b
#
# sed -e '1d' ./a
b
#

 

①1行目(a)がパターンスペースに読み込まれる
②アドレスは1行目が指定されている
③アドレスで指定された1行目のパターンスペースに対してdコマンドが実行され、パターンスペースのデータが削除される
④パターンスペースにある1行目を標準出力するが、パターンスペースが削除済みの為、何も出力されない
⑤パターンスペースは空(既に削除済み)になる
⑥2行目(b)がパターンスペースに読み込まれる
⑦アドレスで指定されていないため、dコマンドは実行されない
⑧パターンスペースのデータを標準出力する
⑨パターンスペースは空になる

-sコマンド

アドレスで指定された行のパターンスペースの置換を行う。sコマンドは、パラメータに正規表現を指定する。

# cat ./a
a a
b b
a a
# sed -e '1s/a/b/g' ./a
b b
b b
a a
#

 

①1行目(a a)がパターンスペースに読み込まれる
②アドレスは1行目が指定されている
③アドレスで指定された1行目のパターンスペースに対してsコマンドが実行され、aがbに置換される
 /a/b/gがパラメータ。aをbに置換する。
 g指定により1行内にマッチする部分が複数ある場合、全て置換される。
 g指定がない場合は、最初の部分のみ置換
④置換されたパターンスペースを標準出力する
⑤パターンスペースは空になる
⑥2行目(b b)がパターンスペースに読み込まれる
⑦アドレスで指定されていないため、sコマンドは実行されない
⑧パターンスペースのデータを標準出力する
⑨パターンスペースは空になる
⑩3行目(a a)がパターンスペースに読み込まれる
⑪アドレスで指定されていないため、sコマンドは実行されない
⑫パターンスペースのデータを標準出力する
⑬パターンスペースは空になる

他オプション説明

-i[SUFFIX]オプション

ファイルを置換するオプション。
SUFFIX指定時は、指定した文字例を付与したファイル名で入力ファイルをバックアップする。

例) SUFFIX未指定時

# echo a > ./a
# ls -i a
52977918 a
# sed -i -e '/a/p' ./a
# ls -i a
52977919 a
# cat ./a 
a
a
# 

ポイントは入力ファイルを変更せず削除し、新たに同名ファイルで生成している点。
※元ファイルのデータは維持されない。

例) SUFFIX指定時

# echo a > ./a
# ls -i ./a      
52977918 ./a
# sed -i".hoge" -e '/a/p' ./a
# ls -i ./a*
52977919 ./a  52977918 ./a.hoge
# cat ./a
a
a
# cat ./a.hoge
a
#

ポイントは既存ファイルを削除せずにa.hogeにリネームして、新たにaを生成している点。
※元ファイルのデータは維持できる。


-cオプション

"-i"と同時指定時のみ動作するオプション。-cを指定すると元ファイルをSUFFIX付きファイルとしてコピーする。
※-iのみの場合はコピーではなくリネームする

例)

# echo a > ./a
# ls -i ./a 
52977918 ./a
# sed -c -i".hoge" -e '/a/p' ./a
# ls -i ./a ./a.hoge
52977918 ./a  52977920 ./a.hoge
# cat ./a
a
a
# cat ./a.hoge
a
# 

状況に応じて使い分けるべきだが、一般的な用途では-cを付ける必要はなさそう。
どちらも、a、a.hogeのデータは同じ。aが処理後、a.hogeが処理前のデータになる。

-Eオプション

スクリプトを指定する。

"-e"は基本、"-E"は拡張
どちらも提供する機能は同じ。以下文字をメタキャラクタとして使う場合、エスケープするかしないかの違い。

+ ? { } ( ) |

 

例)

# echo 'a' | sed -n -e '/a+/p'
# echo 'a' | sed -n -e '/a\+/p'
a
# echo 'a' | sed -n -E '/a+/p'
a
# echo 'a' | sed -n -E '/a\+/p'
# 

 

ホールドスペースの説明

パターンスペース以外の記憶領域

パターンスペース・パターンスペース操作コマンド

コマンド 説明
h パターンスペースの内容をホールドスペースにコピー
H パターンスペースの内容をホールドスペースの末尾に追加
g ホールドスペースの内容をパターンスペースにコピー
G ホールドスペースの内容をパターンスペースの末尾に追加

 

実行例

ファイル内容を逆順に出力する。

# cat ./a
a
b
c
# sed -n -e 'G' -e 'h' -e '$p' ./a
c
b
a

#

 

処理の流れ

①パターンスペースに1行目の"a"が読み込まれる

パターンスペース a
ホールドスペース


②Gコマンドでホールドスペースの内容がパターンスペースの末尾に追加される。しかし、ホールドスペースは空の為、変化はなし

パターンスペース a
ホールドスペース


③hコマンドでパターンスペースの内容がホールドスペースにコピーされる

パターンスペース a
ホールドスペース a


④pコマンドは最終行のみ実行される。まだ1行目なので何も実行されない

パターンスペース a
ホールドスペース a


⑤パターンスペースに2行目の"b"が読み込まれる

パターンスペース b
ホールドスペース a


⑥Gコマンドでホールドスペースの内容がパターンスペースの末尾に追加される

パターンスペース b
a
ホールドスペース a


⑦hコマンドでパターンスペースの内容がホールドスペースにコピーされる

パターンスペース b
a
ホールドスペース b
a


⑧pコマンドは最終行のみ実行される。まだ2行目なので何も実行されない

パターンスペース b
a
ホールドスペース b
a


⑨パターンスペースに3行目(最終行)の"c"が読み込まれる

パターンスペース c
ホールドスペース b
a


⑩Gコマンドでホールドスペースの内容がパターンスペースの末尾に追加される

パターンスペース c
b
a
ホールドスペース b
a


⑪hコマンドでパターンスペースの内容がホールドスペースにコピーされる

パターンスペース c
b
a
ホールドスペース c
b
a


⑫pコマンドが最終行の為、実行される。pコマンドは現在のパターンスペースの内容を出力する

パターンスペース c
b
a
ホールドスペース c
b
a


※上記の流れで大体説明はつく。しかし、出力結果には最後に空行が入っている。
②の時に空のホールドスペースをパターンスペースに追加する時に、改行が追加されたと想定される。

# sed -n -e 'G' -e 'p' ./a|cat -A
a$
$
b$
$
c$
$
#
# sed -n -e 'g' -e 'p' ./a|cat -A
$
$
$

 

おまけ

シングルクオートをパターンマッチ指定するのが面倒
sedでのシングルクォートの置換 | hiro345
 

# echo "''" | sed -E '/\'\'/p'
> ※エスケープが効かない
# echo "''" | sed -n -E '/'\'\''/p'
'' ※エスケープし更にシングルクオートで囲えばOK。直感的でなし。
# echo "''" | sed -n -E "/''/p"
'' ※ダブルクオートで囲えばOK

シェルの展開(EXPANSION)を学ぶ

man bashのEXPANTIONを勉強

EXPANSIONはコマンド実行前に処理されるコマンドラインの展開処理のこと。
7種類の動きがあり。

EXPANSIONの動きの確認の前に、shellで使われる括弧の英語/日本語表記について把握する。

括弧 - Wikipedia

表記 英語 日本語
( parenthesis, paren 丸括弧(小括弧)
{ brace 波括弧(中括弧)
[ bracket 角括弧(大括弧)

それでは7種類の動きを確認!!


①brace expansion

brace expansionとは中括弧{}による展開のこと。

man bashの"Brace Expansion"部分のGoogle翻訳によると、、、

ブレース拡張は、任意の文字列を生成するメカニズムです。このメカニズムはパス名拡張に似ていますが、生成されたファイル名は存在する必要はありません。中括拡張されるパターンはオプションのプリアンブルの形をとり、コンマで区切られた一連の文字列、または中括弧の間のシーケンス式の後にオプションの追記文字が続きます。プリアンブルは、中括弧内に含まれる各文字列の前に置かれ、ポストスクリプトは結果の各文字列に追加され、左から右に展開されます。

最近のGoogle翻訳すごい。。。

ひとまず以下だけ覚える。

シーケンス展開

# echo {a..c}
a b c
# echo {0..3}
0 1 2 3
# echo {0..3}a
0a 1a 2a 3a
# echo a{0..3}
a0 a1 a2 a3

カンマ区切り展開

# ls
a
# echo {a,b,1,2,*}                                                                                                                       
a b 1 2 a
# echo {a,b,1,2,*,?}
a b 1 2 a a
# echo {a,b,1,2,*,?,??}
a b 1 2 a a ??

注目すべきは*や?によるパス展開もされる点。
また*や?パス展開されない場合、そのまま文字として利用されてしまう点も注意。mkdirとかで使うと*や?のついたディレクトリが出来てしまう。


②tilde expansion

man bashの"Tilde Expansion"部分のGoogle翻訳によると、、、

単語が引用符で囲まれていないチルダ文字( `〜 ')で始まる場合、最初の引用符なしスラッシュ(または引用符なしのスラッシュがない場合はすべての文字)の前のすべての文字はチルダプレフィックスと見なされます。チルダプレフィックス内の文字のいずれも引用符で囲まれていない場合、チルダに続くチルダプレフィックス内の文字は、可能なログイン名として扱われます。このログイン名がヌル文字列の場合、チルダはシェルパラメータHOMEの値に置き換えられます。 HOMEが設定されていない場合は、シェルを実行しているユーザーのホームディレクトリが代わりに使用されます。それ以外の場合は、tilde-prefixが、指定されたログイン名に関連付けられたホームディレクトリに置き換えられます。

ひとまず以下だけ覚える。

ホームディレクトリに展開

# echo ~
/root
# echo ~/test
/root/test
# echo ~"/test"
~/test
# echo ~/"test"
/root/test
# echo a~
a~
# echo "a"~
a~

③parameter expansion

変数展開のこと。

man bashの"Parameter Expansion"部分のGoogle翻訳によると、、、

`$ '文字は、パラメータ展開、コマンド置換、または算術展開を導入します。展開されるパラメータ名またはシンボルは、省略可能な中括弧で囲むことができますが、名前の一部として解釈される直後の文字から拡張する変数を保護する役割を果たします。

色々記法・機能があるので、詳細は以下URLを参照。
シェルの変数展開 - Qiita
Bash Reference Manual: Shell Parameter Expansion

ひとまず以下だけ覚える。

{}(中括弧)を使った方がよい!(変数を明確に指定できるため)

# a="bbb"
# echo $a
bbb
# echo ${a}
bbb
# echo $ab

# echo ${a}b
bbbb

変数の文字数、配列の要素数の取り出し

${#parameter}
#を使う。

# a=1234
# echo ${#a}
4
# a=abcd
# echo ${#a}
4
# a=(a b c d)
# echo ${a}
a
# echo ${#a}
1
# echo ${a[@]}
a b c d
# echo ${#a[@]}
4
#

値の切り出し

パラメータ展開 内容
${parameter#word} 先頭から前方最短一致した位置まで取り除きます
${parameter##word} 先頭から前方最長一致した位置まで取り除きます
${parameter%word} 末尾から後方最短一致した位置まで取り除きます
${parameter%%word} 末尾から後方最長一致した位置まで取り除きます

前方一致の場合

# a=/var/log/messages
# echo ${a}
/var/log/messages
# echo ${a#/}
var/log/messages
# echo ${a##/}
var/log/messages
# echo ${a#*/}
var/log/messages
# echo ${a##*/}
messages
# echo ${a#log}
/var/log/messages
# echo ${a#*log}
/messages
# echo ${a##*log}
/messages

後方一致の場合

# a=/var/log/messages
# echo ${a%/}
/var/log/messages
# echo ${a%%/}
/var/log/messages
# echo ${a%/*}
/var/log
# echo ${a%%/*}

# echo ${a%log}
/var/log/messages
# echo ${a%%log}
/var/log/messages
# echo ${a%log*}
/var/
# echo ${a%%log*}
/var/

④command substitution

man bashの"Command Substitution"部分のGoogle翻訳によると、、、

コマンド置換は、コマンド名の代わりにコマンド出力を許可します。 2つの形式があります。
$(コマンド)
または
`コマンド`
Bashはコマンドを実行し、コマンド置換をコマンドの標準出力に置き換え、末尾に改行を削除して拡張を実行します。埋め込み改行は削除されませんが、単語分割中に削除される可能性があります。コマンド置換$(catファイル)は、同等ではあるが高速な$(<ファイル)で置き換えることができます。

ひとまず以下だけ覚える。

$(COMMAND)の形式の方が新しい使い方でよい!範囲指定が分かりやすく、ネストする時とか記述がシンプルになる。

# A=/etc/hosts
# echo $(cat $A)                                                                                                                             
127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4 ::1 localhost localhost.localdomain localhost6 localhost6.localdomain6
# echo `cat $A`
127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4 ::1 localhost localhost.localdomain localhost6 localhost6.localdomain6
# echo $(echo $(cat $A))
127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4 ::1 localhost localhost.localdomain localhost6 localhost6.localdomain6
# echo `echo `cat $A``
cat /etc/hosts
# echo `echo \`cat $A\``
127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4 ::1 localhost localhost.localdomain localhost6 localhost6.localdomain6

⑤arithmetic expansion

man bashの"Command Substitution"部分のGoogle翻訳によると、、、

算術展開では、算術式の評価と結果の置換が可能です。
算術展開の形式は次のとおりです。
$((式))
式は二重引用符で囲まれているかのように扱われますが、かっこ内の二重引用符は特別に扱われません。式のすべてのトークンは、パラメータ拡張、文字列展開、コマンド置換、および見積りの削除を受けます。算術展開はネストすることができます。

"見積りの削除"は"quote removal"のこと。
Quoteはバックスラッシュとシングル・ダブルクオーテーションのこと。
"quote removal"は
After the preceding expansions, all unquoted occurrences of the characters \, ', and " that did not result from one of the above expansions are removed.
と書いてあるが、良く分からない。。。展開処理の最後に行われう処理のよう。ひとまず目をつむる。

ひとまず以下だけ覚える。

2重括弧の中に数式を書けば展開される!

# echo $((1+1))
2
# echo $((1*1))
1
# echo $((1/1))
1
# echo $((1%1))
0
# echo $((1**1))
1

数式以外を指定した時は、こんな動き

# a=/var/log/messages
# echo $((a))
-bash: /var/log/messages: 構文エラー: オペランドが予期されます (エラーのあるトークンは "/var/log/messages")
# echo $(($a))
-bash: /var/log/messages: 構文エラー: オペランドが予期されます (エラーのあるトークンは "/var/log/messages")
# echo $(($a+1))
-bash: /var/log/messages+1: 構文エラー: オペランドが予期されます (エラーのあるトークンは "/var/log/messages+1")
# echo $(($a+a))
-bash: /var/log/messages+a: 構文エラー: オペランドが予期されます (エラーのあるトークンは "/var/log/messages+a")
# unset a
# echo $((a))
0
# echo $(($a))
0

変数展開やコマンド置換等も動く

# echo 1 > ./b
# echo $(($(cat ./b)+1))
2

⑥word splitting

man bashの"Word Splitting"部分のGoogle翻訳によると、、、

シェルは、単語の二重引用符で囲まれていないパラメータ展開、コマンド置換、および算術展開の結果をスキャンします。
シェルは、IFSの各文字を区切り文字として扱い、他の展開の結果をこれらの文字の単語に分割します。 IFSが設定されていない場合、またはその値が正確に の場合、既定値、前回の展開結果の先頭と最後に、および無視され、先頭または末尾にないIFS文字のシーケンスは単語を区切る役割を果たします。 IFSがデフォルト以外の値を持つ場合、空白文字がIFS(IFS空白文字)の値である限り、空白文字のスペースとタブのシーケンスは単語の先頭と末尾で無視されます。隣接するIFS空白文字と一緒にIFS空白ではないIFS内の文字は、フィールドを区切ります。一連のIFS空白文字も区切り文字として扱われます。 IFSの値がNULLの場合、ワード分割は行われません。
明示的なヌル引数( ""または " ')は保持されます。値を持たないパラメータの拡張の結果として、引用符で囲まれていない暗黙のヌル引数は削除されます。値のないパラメータが二重引用符で囲まれている場合、null引数が返され、保持されます。
展開が行われない場合、分割は実行されないことに注意してください。

良く分からないが、
二重引用符で囲まれていないパラメータ展開、コマンド置換、および算術展開の結果をスキャンして、結果を単語に分割する処理のことだろう。
重要そうだけど、ひとまず目をつむる。


⑦pathname expansion

man bashの"Word Splitting"部分のGoogle翻訳によると、、、

単語の分割後、-fオプションが設定されていない限り、bashは各単語に*、?、および?の文字をスキャンします。これらの文字のいずれかが表示された場合、その単語はパターンとみなされ、パターンに一致するファイル名のアルファベット順ソートリストに置き換えられます。一致するファイル名が見つからず、シェルオプションnullglobが有効でない場合、その単語は変更されません。 nullglobオプションが設定されていて一致するものが見つからない場合、その単語は削除されます。 failglobシェルオプションが設定されていて一致するものが見つからない場合、エラーメッセージが出力され、コマンドは実行されません。シェルオプションnocaseglobを有効にすると、アルファベット文字の大文字と小文字を区別せずに一致が実行されます。パス名展開にパターンを使用する場合、シェルオプションのdotglobが設定されていない限り、名前の先頭またはスラッシュの直後の文字「。」は明示的に一致させる必要があります。パス名をマッチさせる場合、スラッシュ文字は常に明示的に一致させる必要があります。他の場合には、 ``。 ''文字は特別に扱われません。 nocaseglob、nullglob、failglob、およびdotglobシェルオプションについては、SHELL BUILTIN COMMANDSのshoptの説明を参照してください。

またまた難しい。ひとまず以下を覚える

Word splitingの後に動いて*、?、[]の展開をする。デフォルトでは、マッチするパス(ファイルorディレクトリetc)がない場合、パターンマッチ文字は、そのままで認識される。

と覚える

# ls
a  b
# ls * ?
a  a  b  b
# ls ??
ls: ?? にアクセスできません: そのようなファイルやディレクトリはありません
# ls [ab]
a  b
# ls [cd]
ls: [cd] にアクセスできません: そのようなファイルやディレクトリはありません
# 

おまけ

システムによっては、process substitutionもあり。

man bashの"Process Substitution"部分のGoogle翻訳によると、、、

プロセス置換は、名前付きパイプ(FIFO)をサポートするシステム、または開いているファイルに名前を付ける/ dev / fdメソッドでサポートされています。 <(list)または>(list)の形式をとります。プロセスリストは、その入力または出力がFIFOまたは/ dev / fdのあるファイルに接続されて実行されます。このファイルの名前は、展開の結果として現在のコマンドに引数として渡されます。 >(リスト)フォームが使用されている場合、ファイルへの書き込みはリストの入力を提供します。 <(リスト)形式を使用する場合、引数として渡されたファイルは、listの出力を得るために読み込まれなければなりません。
使用可能な場合、プロセス置換は、パラメーターおよび変数の拡張、コマンド置換、および算術拡張と同時に実行されます。

Linuxシェルから辞書を呼び出す

いろんな環境で使いたいので、実装1ライナー

# JISYO="$HOME/bin/myjisyo" && JISYO_DATA="$HOME/myjisyo_data/jisyo.txt" && mkdir -p $(dirname $JISYO) $(dirname $JISYO_DATA) && wget -q -O - http://www.namazu.org/~tsuchiya/sdic/data/gene95.tar.gz | tar xzf - -O gene.txt | tr -d '\r' | iconv -c -f shift-jis -t utf-8 > $JISYO_DATA && echo "egrep -i -A 1 --color \"^\$1\$\" $JISYO_DATA" > $JISYO && chmod u+x $JISYO
# myjisyo apple
apple
リンゴ,りんご,リンゴの木

線形回帰とは?

メモ

たくさんの変数を持つ集まり (= ベクトル) についてある関数にノイズが加わったような状態が事例として観測されました。このときその関数を予測しましょうというのが回帰分析です。

関数を予測するのが線形分析?
※1次関数だけでなく対数関数も

Jubatus外れ値検知機能を使い不正ログイン検知

ためしに作ってみる!

GeoIPセットアップ

# rpm -ivh ftp://195.220.108.108/linux/centos/7.2.1511/os/x86_64/Packages/GeoIP-devel-1.5.0-9.el7.x86_64.rpm
# pip install geoip2
# wget http://geolite.maxmind.com/download/geoip/database/GeoLite2-City.mmdb.gz
# gunzip GeoLite2-City.mmdb.gz

機能

ユーザー単位で解析する。ログイン傾向から不正ログインを検知する
入力データは以下とする。
ログイン時間,接続元IP

入力データをプログラムで以下データに変換し学習データとする。

①接続元IP(ip_int)

②接続元IPの地域情報(geoid)

③接続元IPのDNSBL登録状況(dns_info)

④該当IPが新規接続元IPであるかの有無(first)

⑤対象アカウントへの過去M分以内のログイン回数(num_of_login_in_near_time)

⑥対象アカウントへの過去M分以内の接続元地域数(num_of_location_in_near_time)
 別の場所からのログインは怪しく見えるから。

誤検知をできるだけ防ぐべく、多次元データで外れ値検知をしてみる。

まだ軽くしか動かしてないので、しっかり動くか分かりません!!

プログラム

login_analyze.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import signal
import sys
import os
import json
from jubatus.anomaly import client
from jubatus.common import Datum
import time
import geoip2.database
import wget
import gzip
import dns.resolver
import syslog

DEBUG = True


def exec_study(uname, data, mes):
    stime = time.time()

    ip_int = data[0]
    geoid = data[1]
    dns_info = data[2]
    first = data[3]
    num_of_login_in_near_time = data[4]
    num_of_location_in_near_time = data[5]

    datum = Datum()

    datum.add_number("ip_int", float(ip_int))
    datum.add_number("geoid", float(geoid))
    datum.add_number("dns_info", float(dns_info))
    datum.add_number("first", float(first))
    datum.add_number("num_of_login_in_near_time", float(num_of_login_in_near_time))
    datum.add_number("num_of_location_in_near_time", float(num_of_location_in_near_time))
    anom = client.Anomaly("127.0.0.1", 9199, uname)
    ret = anom.add(datum)
    output_log(str(ret) + ": " +  mes)


def op_srv(OP):
    if OP[0] == "start":
        com = "jubaanomaly -f " + OP[1] + "> /dev/null 2>&1 &"
    else:
        com = "pkill jubaanomaly > /dev/null 2>&1"
    if os.system(com):
        err_fin("jubaanomaly " + OP[0] + " failed")


def ip2int(ip):
    o = map(int, ip.split('.'))
    ip_int = (16777216 * o[0]) + (65536 * o[1]) + (256 * o[2]) + o[3]
    return ip_int


def output_log(mes):
    syslog.openlog()
    syslog.syslog(mes)
    syslog.closelog()
    if DEBUG:
        print(mes)


def err_fin(mes):
    print("Error: " + mes)
    output_log("Error " + mes)
    op_srv(["stop"])
    exit(1)


def get_dnsinfo(ip):
    bls = ["zen.spamhaus.org", "xbl.spamhaus.org", "pbl.spamhaus.org"]
    answers = list()
    for bl in bls:
        my_resolver = dns.resolver.Resolver()
        query = '.'.join(reversed(str(ip).split("."))) + "." + bl
        try:
            answers += my_resolver.query(query, "A")
        except dns.resolver.NXDOMAIN:
            pass
        except:
            err_fin("dns loolup failed")
    return len(answers)


def get_location(ip):
    db_file = "./GeoLite2-City.mmdb"
    if not os.path.isfile(db_file):
        ret = os.system("wget -q http://geolite.maxmind.com/download/geoip/database/GeoLite2-City.mmdb.gz; gunzip -f ./GeoLite2-City.mmdb.gz")
        if ret:
            print("Error: wget failed")
    reader = geoip2.database.Reader(db_file)

    country = str()
    geoid = int()

    try:
        response = reader.city(ip)
    except:
        geoid = 0
        country = "UNKNOWN"
    else:
        geoid = response.city.geoname_id
        country = response.country.name
        if not geoid:
            geoid = 0
        if not country:
            country = "UNKNOWN"
    return geoid, country


def get_args():
    if len(sys.argv) != 3:
        err_fin(" Invalid args")

    config = sys.argv[1]
    uname = sys.argv[2]

    if not os.path.isfile(config):
        err_fin(config + " does not exist")
    return config, uname


def do_exit(sig, stack):
    print('You pressed Ctrl+C.')
    print('Stop running the job.')
    sys.exit(0)


def main():
    signal.signal(signal.SIGINT, do_exit)
    config, uname = get_args()

    ips = set()
    login_data = list()

    op_srv(["start",config])
    time.sleep(5)
 
    while True:
        try:
            line = raw_input()
        except EOFError:
            op_srv(["stop"])
            exit()

        if not line:
            next

        ltime = int(line.split(",")[0])
        ip = line.split(",")[1]
        ip_int = ip2int(ip)

        geoid, country = get_location(ip)

        dns_info = get_dnsinfo(ip)

        if ip in ips:
            first = 0
        else:
            first = 1
        ips.add(ip)

        login_data.append([ltime, geoid])

        near_time_info = [i for i in login_data if ltime - (60 * 60 * 3) <= i[0] <= ltime]
        num_of_login_in_near_time = len(near_time_info)

        tmp_set = set()
        for i in near_time_info:
            tmp_set.add(i[1])
        num_of_location_in_near_time = len(tmp_set)

        mes = "uname: " + uname + " "
        mes += "ltime: " + str(ltime) + " "
        mes += "ip: " + ip + " "
        mes += "ip_int: " + str(ip_int) + " "
        mes += "country: " + country + " "
        mes += "geoid: " + str(geoid) + " "
        mes += "dns_info: " + str(dns_info) + " "
        mes += "first: " + str(first) + " "
        mes += "num_of_login_in_near_time: " + str(num_of_login_in_near_time) + " "
        mes += "num_of_location_in_near_time: " + str(num_of_location_in_near_time) + " "

        data = [ip_int, geoid, dns_info, first, num_of_login_in_near_time, num_of_location_in_near_time]

        exec_study(uname, data, mes)


if __name__ == '__main__':
    main()

コンフィグファイル

login_analyze.json

{
 "method" : "lof",
 "parameter" : {
  "nearest_neighbor_num" : 10,
  "reverse_nearest_neighbor_num" : 30,
  "method" : "euclid_lsh",
  "ignore_kth_same_point" : true,
  "parameter" : {
   "hash_num" : 8,
   "table_num" : 16,
   "probe_num" : 64,
   "bin_width" : 10,
   "seed" : 1234
  }
 },

 "converter" : {
  "string_filter_types": {},
  "string_filter_rules": [],
  "num_filter_types": {},
  "num_filter_rules": [],
  "string_types": {},
  "string_rules": [{"key":"*", "type":"str", "global_weight" : "bin", "sample_weight" : "bin"}],
  "num_types": {},
  "num_rules": [
    {"key" : "ip_int", "type" : "str"},
    {"key" : "geoid", "type" : "str"},
    {"key" : "dns_info", "type" : "num"},
    {"key" : "first", "type" : "num"},
    {"key" : "num_of_login_in_near_time", "type" : "num"},
    {"key" : "num_of_location_in_near_time", "type" : "num"}
  ]
 }
}

入力データ作成用ちょいスクリプト

make_data.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import re
import signal
import datetime
import time
import sys
import os
from operator import itemgetter


def do_exit():
    exit(1)

signal.signal(signal.SIGINT, do_exit)

data = list()
for log in sys.argv[1:]:
    if os.path.isfile(log):
        ext = log.split(".")
        if not ext[1]:
            next
        y = (ext[1][0:4])

        for line in open(log, 'r'):
            line = line.strip()
            ptrn1 = re.compile(r'^(\S+)\s+(\d+)\s+(\d{2}:\d{2}:\d{2})(.*)$')
            mobj1 = ptrn1.match(line)
            if mobj1:
                m = mobj1.group(1)
                d = mobj1.group(2)
                t = mobj1.group(3)
                obj = datetime.datetime.strptime(str(y) + m + str(d) + t, '%Y%b%d%H:%M:%S')
                utime = int(time.mktime(obj.timetuple()))
                # print(utime)

                mes = mobj1.group(4)

                # SSHログイン
                ptrn2 = re.compile(r'^.*Accepted publickey for .* from (\S+) .*$')
                mobj2 = ptrn2.match(mes)
                if mobj2:
                    ip = mobj2.group(1)
                    data.append([utime, ip])

                # 他アプリのログインデータを追加
                #ptrn2 = re.compile(r'^.*Accepted publickey for .* from (\S+) .*$')
                #mobj2 = ptrn2.match(mes)
                # if mobj2:
                #  ip = mobj2.group(1)
                #  data.append([utime, ip])


data.sort(key=itemgetter(0))

for i in data:
    print(str(i[0]) + "," + i[1])

実行方法

# python ./make_data.py  /var/log/secure.20161010 /var/log/secure.20160918 | python ./login_analyze.py ./login_analyze.json hoge

Jubatusで外れ値検知機能(jubaanomaly)の検証

サンプルプログラム

やってること

ユーザー名と接続元IPアドレスの情報で構成されるテストデータを作成
※接続元IPアドレスはたまに仮想クラッカーのIPアドレスを利用

jubaanomalyの起動・停止

仮想クラッカーのIPの時だけ判定結果を出力

引数により学習動作が異なる。

"string1"の場合

ユーザー名:IPアドレス"の1つの文字列データにして学習させる。

"string2"の場合

ユーザー名"と"IPアドレス"の2つの文字列データにして学習させる。

ip_to_num

ユーザー名"を1つの文字データとし、"IPアドレス"を1つの数値データにして学習させる。

コンフィグファイル(anomaly.json)

num_rulesの重みづけは、以下の理由によるtypeでstrを指定する。

参考URLに以下記載あり。

"num" 与えられた数値をそのまま重みに利用する。
"num" 与えられた数値をそのまま重みに利用する。
"str" 与えられた数値を文字列として扱う。これは、例えばIDなど、数値自体の大きさに意味のないデータに対して利用する。重みは1とする。

ignore_kth_same_pointを有効にする。有効にしないとscoreがinfになってしまう為。
参考URLに以下記載あり。

登録できる重複データの件数を nearest_neighbor_num - 1 件に制限することにより、スコアが inf になることを防ぐ。 このパラメタは省略可能であり、デフォルト値は false (無効) である。 (Boolean)

{
 "method" : "lof",
 "parameter" : {
  "nearest_neighbor_num" : 10,
  "reverse_nearest_neighbor_num" : 30,
  "method" : "euclid_lsh",
  "ignore_kth_same_point" : true,
  "parameter" : {
   "hash_num" : 8,
   "table_num" : 16,
   "probe_num" : 64,
   "bin_width" : 10,
   "seed" : 1234
  }
 },

 "converter" : {
  "string_filter_types": {},
  "string_filter_rules": [],
  "num_filter_types": {},
  "num_filter_rules": [],
  "string_types": {},
  "string_rules": [{"key":"*", "type":"str", "global_weight" : "bin", "sample_weight" : "bin"}],
  "num_types": {},
  "num_rules": [{"key" : "*", "type" : "str"}]
 }
}

実行プログラム(anomaly_test.py)

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import signal
import sys
import os
import json
from jubatus.anomaly import client
from jubatus.common import Datum
import random
import time
import ipaddr


TNAMES = ["string1", "string2", "ip_to_num"]
NUM_OF_USER = 1000
NUM_OF_DATA = 10000
HUSEI = 1000


# 自分のサーバに不正ログイン施行回数の多いサーバのIP
CRACKERS = ["183.60.122.126", "188.140.127.155", "58.186.158.62", "91.224.160.184", "79.169.110.246", "46.186.241.89"]


def make_users():
    users = list()
    for i in range(NUM_OF_USER):
        users.append("user" + str(i))
    return users


def make_ip():
    num = random.randint(1, HUSEI)
    if num == HUSEI:
        ip = CRACKERS[random.randint(0, len(CRACKERS) - 1)]
    else:
        ip = "10.0.0." + str(random.randint(1, 254))
    return ip


def make_data(users):
    data = list()
    for i in range(NUM_OF_DATA):
        idx = random.randint(0, NUM_OF_USER - 1)
        ip = make_ip()
        data.append([users[idx], ip])
    return data


def do_exit(sig, stack):
    print('You pressed Ctrl+C.')
    print('Stop running the job.')
    sys.exit(0)


def ip2int(ip):
    o = map(int, ip.split('.'))
    res = (16777216 * o[0]) + (65536 * o[1]) + (256 * o[2]) + o[3]
    return res


def exec_test(tname, data):
    cnt = 1
    proc_per = 10
    cur = 0
    stime = time.time()

    for ent in data:

        datum = Datum()

        if tname == "string1":
            string = ent[0] + ":" + str(ent[1])
            datum.add_string("string", string)
        elif tname == "string2":
            datum.add_string("user", ent[0])
            datum.add_string("src_ip", ent[1])
        elif tname == "ip_to_num":
            datum.add_string("user", ent[0])
            ipnum = ip2int(ent[1])
            datum.add_number("ip", ipnum)

        anom = client.Anomaly("127.0.0.1", 9199, tname)
        ret = anom.add(datum)

        if ent[1] in CRACKERS:
            print (ret, ent)

        if not cnt % (NUM_OF_DATA / proc_per):
            cur += proc_per
            etime = int(time.time() - stime)
            print("===>" + str(cur) + "% completed (elapse: " + str(etime) + " sec)")

        cnt += 1


def op_srv(OP):
    if OP[0] == "start":
        com = "jubaanomaly -f " + OP[1] + "> /dev/null 2>&1 &"
    else:
        com = "pkill jubaanomaly > /dev/null 2>&1"
    if os.system(com):
        print("Error: jubaanomaly " + OP[0] + " failed")
        exit(1)


def get_args():
    if len(sys.argv) != 3:
        print("Error: Invalid args")
        exit(1)

    config = sys.argv[1]
    tname = sys.argv[2]

    if not os.path.isfile(config):
        print("Error: " + config + " does not exist")
        exit(1)
    if not tname in TNAMES:
        print("Error: " + tname + " is invalid test name")
        exit(1)
    return config, tname


def main():
    signal.signal(signal.SIGINT, do_exit)
    config, tname = get_args()

    users = make_users()
    print("make users complete")
    data = make_data(users)
    print("make data complete")

    op_srv(["start", config])
    time.sleep(5)
    print("Test Started")
    exec_test(tname, data)
    op_srv(["stop"])
    print("Test Finished")

if __name__ == '__main__':
    main()

実行方法&結果

./anomaly_test.py ./anomaly.json "string1"
make users complete
make data complete
Test Started
(id_with_score{id: 34, score: 1.00283694267}, ['user0', '58.186.158.62'])
(id_with_score{id: 223, score: 1.00143611431}, ['user80', '183.60.122.126'])
===>10% completed (elapse: 0 sec)
(id_with_score{id: 290, score: 0.999702095985}, ['user61', '91.224.160.184'])
(id_with_score{id: 294, score: 0.999072134495}, ['user13', '79.169.110.246'])
(id_with_score{id: 343, score: 0.99412637949}, ['user50', '58.186.158.62'])
(id_with_score{id: 405, score: 1.00214076042}, ['user12', '58.186.158.62'])
(id_with_score{id: 472, score: 1.0037945509}, ['user90', '183.60.122.126'])
(id_with_score{id: 486, score: 1.00199890137}, ['user19', '91.224.160.184'])
===>20% completed (elapse: 2 sec)
(id_with_score{id: 570, score: 1.00471949577}, ['user94', '58.186.158.62'])
(id_with_score{id: 673, score: 0.996484994888}, ['user90', '188.140.127.155'])
(id_with_score{id: 709, score: 1.00101566315}, ['user0', '183.60.122.126'])
===>30% completed (elapse: 5 sec)
(id_with_score{id: 852, score: 1.01610195637}, ['user35', '183.60.122.126'])
(id_with_score{id: 977, score: 0.999998986721}, ['user19', '46.186.241.89'])
===>40% completed (elapse: 10 sec)
(id_with_score{id: 1053, score: 1.01017296314}, ['user99', '58.186.158.62'])
(id_with_score{id: 1184, score: 0.998991131783}, ['user57', '46.186.241.89'])
(id_with_score{id: 1229, score: 0.997941493988}, ['user96', '183.60.122.126'])
===>50% completed (elapse: 15 sec)
===>60% completed (elapse: 22 sec)
===>70% completed (elapse: 29 sec)
(id_with_score{id: 1783, score: 1.00462818146}, ['user1', '46.186.241.89'])
===>80% completed (elapse: 37 sec)
(id_with_score{id: 2013, score: 1.00843286514}, ['user64', '79.169.110.246'])
(id_with_score{id: 2018, score: 1.00108575821}, ['user4', '58.186.158.62'])
(id_with_score{id: 2053, score: 1.00647413731}, ['user72', '188.140.127.155'])
(id_with_score{id: 2227, score: 1.00535583496}, ['user3', '91.224.160.184'])
(id_with_score{id: 2231, score: 1.0007673502}, ['user16', '46.186.241.89'])
===>90% completed (elapse: 46 sec)
(id_with_score{id: 2353, score: 0.995516657829}, ['user97', '58.186.158.62'])
(id_with_score{id: 2421, score: 0.9998447299}, ['user94', '188.140.127.155'])
(id_with_score{id: 2475, score: 0.992462217808}, ['user87', '183.60.122.126'])
===>100% completed (elapse: 57 sec)
Test Finished
# ./anomaly_test.py ./anomaly.json "string2"
make users complete
make data complete
Test Started
(id_with_score{id: 38, score: 1.00619769096}, ['user4', '91.224.160.184'])
(id_with_score{id: 131, score: 0.9985871315}, ['user85', '188.140.127.155'])
(id_with_score{id: 176, score: 1.06174123287}, ['user15', '91.224.160.184'])
===>10% completed (elapse: 0 sec)
(id_with_score{id: 308, score: 1.01850771904}, ['user79', '46.186.241.89'])
(id_with_score{id: 442, score: 1.02138268948}, ['user53', '58.186.158.62'])
===>20% completed (elapse: 2 sec)
(id_with_score{id: 728, score: 1.00389277935}, ['user87', '46.186.241.89'])
===>30% completed (elapse: 3 sec)
(id_with_score{id: 771, score: 0.993154287338}, ['user29', '183.60.122.126'])
(id_with_score{id: 822, score: 1.00076854229}, ['user96', '79.169.110.246'])
(id_with_score{id: 854, score: 1.01049315929}, ['user94', '183.60.122.126'])
(id_with_score{id: 880, score: 1.02746069431}, ['user20', '91.224.160.184'])
(id_with_score{id: 903, score: 1.03314602375}, ['user41', '183.60.122.126'])
(id_with_score{id: 910, score: 1.00415050983}, ['user67', '79.169.110.246'])
===>40% completed (elapse: 6 sec)
(id_with_score{id: 1006, score: 1.01191151142}, ['user84', '91.224.160.184'])
(id_with_score{id: 1175, score: 1.01555621624}, ['user93', '91.224.160.184'])
(id_with_score{id: 1243, score: 0.986846208572}, ['user96', '79.169.110.246'])
===>50% completed (elapse: 9 sec)
(id_with_score{id: 1290, score: 0.999491155148}, ['user84', '91.224.160.184'])
(id_with_score{id: 1363, score: 1.0839984417}, ['user95', '183.60.122.126'])
(id_with_score{id: 1435, score: 0.988385975361}, ['user71', '79.169.110.246'])
(id_with_score{id: 1451, score: 1.07448995113}, ['user50', '46.186.241.89'])
===>60% completed (elapse: 13 sec)
(id_with_score{id: 1506, score: 1.01763594151}, ['user22', '46.186.241.89'])
(id_with_score{id: 1659, score: 1.02214670181}, ['user27', '58.186.158.62'])
===>70% completed (elapse: 17 sec)
(id_with_score{id: 1919, score: 1.01828491688}, ['user27', '79.169.110.246'])
(id_with_score{id: 1944, score: 1.01462376118}, ['user30', '183.60.122.126'])
===>80% completed (elapse: 22 sec)
(id_with_score{id: 2030, score: 1.01424443722}, ['user30', '188.140.127.155'])
(id_with_score{id: 2203, score: 1.05265760422}, ['user54', '91.224.160.184'])
===>90% completed (elapse: 28 sec)
(id_with_score{id: 2276, score: 0.991128385067}, ['user79', '46.186.241.89'])
(id_with_score{id: 2305, score: 0.999580144882}, ['user87', '188.140.127.155'])
(id_with_score{id: 2308, score: 1.02546048164}, ['user23', '183.60.122.126'])
(id_with_score{id: 2403, score: 0.996041715145}, ['user55', '183.60.122.126'])
===>100% completed (elapse: 34 sec)
Test Finished
# ./anomaly_test.py ./anomaly.json "ip_to_num"
make users complete
make data complete
Test Started
(id_with_score{id: 11, score: 1.40382528305}, ['user79', '183.60.122.126'])
(id_with_score{id: 96, score: 1.65164899826}, ['user30', '79.169.110.246'])
===>10% completed (elapse: 0 sec)
(id_with_score{id: 302, score: 1.38489890099}, ['user77', '183.60.122.126'])
(id_with_score{id: 304, score: 1.52720057964}, ['user16', '183.60.122.126'])
(id_with_score{id: 334, score: 1.37992143631}, ['user58', '58.186.158.62'])
(id_with_score{id: 339, score: 1.53620314598}, ['user7', '58.186.158.62'])
(id_with_score{id: 375, score: 1.511734128}, ['user27', '91.224.160.184'])
===>20% completed (elapse: 2 sec)
(id_with_score{id: 524, score: 1.37163031101}, ['user22', '183.60.122.126'])
===>30% completed (elapse: 3 sec)
(id_with_score{id: 880, score: 1.40898621082}, ['user93', '58.186.158.62'])
(id_with_score{id: 891, score: 1.32244873047}, ['user49', '79.169.110.246'])
===>40% completed (elapse: 5 sec)
(id_with_score{id: 1217, score: 1.43259418011}, ['user20', '183.60.122.126'])
(id_with_score{id: 1242, score: 1.44561052322}, ['user98', '91.224.160.184'])
===>50% completed (elapse: 6 sec)
(id_with_score{id: 1300, score: 1.69272983074}, ['user14', '46.186.241.89'])
(id_with_score{id: 1382, score: 1.36963653564}, ['user78', '79.169.110.246'])
(id_with_score{id: 1426, score: 1.37384569645}, ['user77', '46.186.241.89'])
===>60% completed (elapse: 7 sec)
(id_with_score{id: 1547, score: 1.37469255924}, ['user32', '188.140.127.155'])
(id_with_score{id: 1746, score: 1.34071433544}, ['user80', '46.186.241.89'])
(id_with_score{id: 1749, score: 1.764798522}, ['user18', '91.224.160.184'])
===>70% completed (elapse: 7 sec)
(id_with_score{id: 1827, score: 1.44498622417}, ['user76', '183.60.122.126'])
(id_with_score{id: 1925, score: 1.32021319866}, ['user17', '58.186.158.62'])
===>80% completed (elapse: 8 sec)
(id_with_score{id: 2104, score: 1.12577271461}, ['user68', '188.140.127.155'])
(id_with_score{id: 2128, score: 1.35998618603}, ['user53', '58.186.158.62'])
(id_with_score{id: 2192, score: 1.13407361507}, ['user31', '79.169.110.246'])
(id_with_score{id: 2211, score: 1.47604465485}, ['user29', '188.140.127.155'])
===>90% completed (elapse: 9 sec)
(id_with_score{id: 2292, score: 1.1355766058}, ['user61', '91.224.160.184'])
(id_with_score{id: 2305, score: 1.38899683952}, ['user81', '58.186.158.62'])
(id_with_score{id: 2349, score: 1.21521854401}, ['user15', '58.186.158.62'])
(id_with_score{id: 2385, score: 1.15831208229}, ['user61', '183.60.122.126'])
(id_with_score{id: 2408, score: 1.55122351646}, ['user12', '46.186.241.89'])
(id_with_score{id: 2482, score: 1.24194133282}, ['user17', '46.186.241.89'])
===>100% completed (elapse: 9 sec)
Test Finished

分かったこと

テキストデータの違いは重みづけが弱い?
学習済みデータが多くなればなるほど、学習時間がかかる?

Jubatusチュートリアル勉強(データ変換・文字列編)

データ変換とは?

一般的に機械学習を行う場合、テキスト等の非定形データは直接扱うことはできない

その為、それらのデータから特徴抽出をして、特徴ベクトルデータを得る必要がある。これがデータ変換。

特徴ベクトルデータは、キーが文字列・値が数値のkey-value型のデータ

このデータ変換により、自然言語・画像・音声データを統一的に扱う事が可能になる

Jubatusはこの変換機能を有し、設定ファイルで柔軟にカスタマイズできる。


データ変換の流れ

①クライアントが学習データからdatum(学習データの元)を生成しサーバに渡す。

②サーバはdatumにフィルター処理をする。

③サーバはフィルター処理されたdatumに特徴抽出処理(重み付け)をして特徴ベクトルデータを得る


サーバにおけるフィルター処理について

クライアントから提供されるdatumはkey:value型データ。keyは文字列。
valueのデータは以下3種類があり。
1) 文字列
2) 数値
3) バイナリデータ

フィルター処理は、datumを入力として指定ルールに基づき新たなdatumを生成し、学習対象データに追加する。

例) 参考URLの例では、クライアントが生成したdatum(message)にHTMLタグ除去処理をして、別keyでdatum(message-detagged)を追加している。

フィルター処理は、datumの種類により異なる。本記事では文字列データの処理について記載する。(数学が苦手だから。。。)

設定ファイル内の以下パラメタで実現

stiring_filter_types

変換ルールを定義

例)

"string_filter_types": {
  "detag": { "method": "regexp", "pattern": "<[^>]*>", "replace": "" }
},

やっている事は以下の通り
①"detag"という名前でHTMLタグを除去するフィルタを定義
②"method"で正規表現をするフィルタ"regexp"を指定
③"pattern"でパターンマッチを指定
④"replace"でパターンにマッチした場合の置換ルールを指定

"detag"はユーザー定義データ
"method"、"pattern"、"replace"に指定可能なパラメタは他にもあり。詳細は参考URLを参照。
フィルタ・変換ルールのみ定義。どのdatumに適用するかは、ここでは定義しない。

string_filter_rules

stiring_filter_typesで定義されたルールをdatumに適用し学習対象となる新datumを追加する。

例)

"string_filter_rules": [
  { "key": "message", "type": "detag", "suffix": "-detagged" }
]

やっている事は以下の通り
①"key"でフィルタ処理対象にするdatumを指定。
※"message"はクライアント側プログラムで学習データからdatumを生成する時に定義されている。
②"type"でstiring_filter_typesで定義した変換ルール"detag"を指定。
③"suffix"で新しく生成されるdatumのkey値生成を生成。

結果、以下のdatumが作成され学習対象として追加される。
key: message-detagged
value: タグ除去されたデータ

上記例にはないが、"except"パラメタを利用してマッチ除外条件の指定も可能。


サーバにおける特徴抽出処理(重み付け)について

この処理をdatumに行い、特徴ベクトルデータを得る。

設定ファイル内の以下パラメタで実現

string_types

重み付けルールを定義

例)

"string_types": {
  "bigram":  { "method": "ngram", "char_num": "2" }
},

やっている事は以下の通り
①"bigram"という名前の重み付けルールを定義
②"method"で重み付けアルゴリズム("ngram")を指定
ngramは隣接するN文字を特徴量として利用するアルゴリズム(このような特徴量をN-gram特徴と呼ぶ)
③"char_num"は②で指定したアルゴリズムのオプション。ngramの隣接するN文字のNを指定。

"bigram"はユーザー定義データ。
"method"、"method"に指定可能なパラメタは他にもあり。

詳細は参考URLを参照。
重み付けルールのみ定義。どのdatumに適用するかは、ここでは定義しない。

string_rules

string_typesで定義されたルールをdatumに適用し重み付けをする。

例)

"string_rules": [
  { "key": "message",          "type": "bigram", "sample_weight": "tf",  "global_weight": "bin" },
  { "key": "message-detagged", "type": "space",  "sample_weight": "bin", "global_weight": "bin" }
]

やっている事は以下の通り
①"key"で適用するdatumを指定。
②"type"でstring_typesで定義したアルゴリズム"bigram"を指定
③"sample_weight"で重み付けする値を決定。
 "bin"の場合は常に1とする。
 "tf"の場合は、datumの文字列中の出現回数で重みづけする。
④"global_weight"で今までの通算データから算出される大域的な重み付けを指定。
 "bin"の場合は常に1とする。


今日はここまで!
特徴抽出処理あたりからよく分からない。。。。
サンプルプログラムで動きをみてみよう。