この記事は茨大 Advent Calendar 2019 20日目の記事です。
初めに
今回は茨大 Advent Calendar 2019に参加させてもらったのと,
ちょうどNLP100本ノックに挑戦しているので,100本ノックの最初の方の問題で使った高階関数についてまとめたいと思います。
個人的に,高階関数はあまり使っていなかったのですが,ちょうどいい機会なので
習得も含め使ってみようと思いました。
当初はPythonを使ってノックしようと思ってたのですが,ググったところ
@mono0926 さんの [[Swift 3] 言語処理100本ノック 2015 第1章: 準備運動]
(https://qiita.com/mono0926/items/c4c717cb4aeffaec8d17)
や,
@tikidunpon さんの Swift4で言語処理100本ノック 2015 第1章,第2章
など,Swiftで挑戦されている方がいらっしゃったので,私もSwfitでやっています。
コードはGithubにあげています。
間違っていたり,おかしな点があればご指摘ください!
問題を解いていて個人的によかったものをピックアップして幾つか書いていこうと思います。
100本ノックQ2
まずQ2の問題と回答を以下に書きます。
//02. 「パトカー」+「タクシー」=「パタトクカシーー」
//「パトカー」+「タクシー」の文字を先頭から交互に連結して文字列「パタトクカシーー」を得よ.
func Q2(_ input1: String, _ input2: String) -> String
return zip(input1, input2).map { String($0) + String($1) }.reduce("", +)
}
この問題は引数のinput1に「パトカー」が,input2に「タクシー」が入っています。
まず,zip(input1, input2).map { String($0) + String($1) }
の部分で,input1とinput2の先頭からの文字列が
それぞれセットになって,配列に格納されます。
この部分をデバックで出力すると以下のようになります。
([String]) $R4 = 4 values {
[0] = "パタ"
[1] = "トク"
[2] = "カシ"
[3] = "ーー"
}
この結果から,input1とinput2がmap
によって配列に格納されていることがわかります。
問題は,文字列「パタトクカシーー」を得ることが目的ですから,[String]
からString
にする必要があります。
高階関数を使わずにfor文とかを使って連結しようとしたら,次のようになると思います。
var result = ""
for line in huge {
result += line
}
別に,動くのでこれでも大丈夫です。
しかし,高階関数のreduce
を使えば,1行で,完結することができ,非常にみやすくなります。
(コードが読みやすくなるかはわかりません。)
100本ノックQ3
これもまず,問題文と解答を以下に示します。
//03. 円周率
//"Now I need a drink, alcoholic of course, after the heavy lectures involving quantum mechanics."
//という文を単語に分解し,各単語の(アルファベットの)文字数を先頭から出現順に並べたリストを作成せよ.
func Q3(_ input: String) -> [Int] {
return input.components(separatedBy: " ").map { $0.pregReplace(pattern: "[,|.]", with: "").count }
}
何も考えずに回答すれば,十数行になりそうな問題ですが,高階関数をうまく組み合わせることで1行で終わりました!
まず,inputされる文字をスペースで分ける必要があるので,compnents
を使いスペースで分けました。
デバックで出力すると以下のようなString型の配列が得ることができます。
(lldb) p input.components(separatedBy: " ")
([String]) $R0 = 15 values {
[0] = "Now"
[1] = "I"
[2] = "need"
[3] = "a"
[4] = "drink,"
[5] = "alcoholic"
[6] = "of"
[7] = "course,"
[8] = "after"
[9] = "the"
[10] = "heavy"
[11] = "lectures"
[12] = "involving"
[13] = "quantum"
[14] = "mechanics."
}
これらそれぞれに対して,文字数を得ればいいのですが,「,」や「.」を取り除く必要があります。
また,「,」や「.」が配列のどこに出現するのかはinputの内容によって変化するので,
配列の全ての要素に対して調べる必要があります。
今までなら,配列の全ての要素にアクセスするなんて聞いたらfor文を使っていました。
しかし,map
を使えば,for文を使わなくても,配列の全ての要素にアクセスできます。
map内の処理である,
$0.pregReplace(pattern: "[,|.]", with: "").count
この部分において,今回は正規表現を使って,「,」と「.」を空文字に置き換える処理をしました。
置き換える作業を行った後,count
を使って文字数を返す処理をしています。
これにより,各単語の(アルファベットの)文字数を先頭から出現順に並べたリスト(Int型の配列)を作成しています。
100本ノックQ21
問題文と解答を以下に示します。
/// 23. セクション構造
//記事中に含まれるセクション名とそのレベル(例えば"== セクション名 =="なら1)を表示せよ.
func Q23(_ input: String) -> [(secName: String, secLevel: Int)] {
let wikiUK = Q20(input)
let sectionLine = wikiUK.text.components(separatedBy: .newlines).filter{ $0.contains("==") }
let sectionName = sectionLine.map { $0.pregReplace(pattern: "[=]*", with: "")}
let sectionLevels = sectionLine.map { $0.components(separatedBy: "=")}.map{ ($0.count - 1) / 2 }
return zip(sectionName, sectionLevels).map{ ($0, $1) }
}
この問題はjsonファイルから「イギリス」に関する記事本文をQ20で取得したものに対して行う問題です。
wikiUK
は
struct WikiSet: Codable {
let title: String
let text: String
}
となっています。
まず,1行目で,イギリスに関する記事を取得しています。
2行目で,イギリスに関する記事を行ごとに分け,それに対して,==
が含まれている行を取得しています。
3行目は,セクションの名前を取得しています。正規表現を使い,==
を取り除く処理をしています。
4行目のsectionLine.map { $0.components(separatedBy: "=")}
では,=
で文を分けていきます。
例えば,===hoge===
であれば,
[0] = "="
[1] = "="
[2] = "="
[3] = "hoge"
[4] = "="
[5] = "="
[6] = "="
というふうに分割されます。これはsectionLine
それぞれの行に対して行われるので,結果として2次元配列が返されます。
次にこの2次元配列に対しての操作である.map{ ($0.count - 1) / 2 }
の部分を考えます。
セクションは必ず,
n個の「=」
+ セクション名
+ n個の「=」
で構成されることがわかります。つまり,2次元配列の個数は必ず奇数になることがわかります。
ゆえに,2次元配列の個数から1を引いて2で割れば,格セクションレベルを取り出すことができます。
まとめ
高階関数をできるだけ使いながらSwiftで100本ノックをしているのですが,結構大変ということがわかりました。
Pythonでないと解けないというわけではないのですが,
やはりPythonの方が解きやすい気はします。
Swfitで100ノックは大変なのですが,@mono0926さんも指摘しているとおり,
StringをExtentionで拡張しながら解き進めれば,Swfitでも自然言語処理はアリなのかなと思います。
調べた感じたと,Swfitで50本くらいまでいってる人はいなかったです。
挫折しなければ100本Swfitでやりきりたいと思います。