LoginSignup
5
5

More than 3 years have passed since last update.

Haskellで自然言語処理100本ノックの第1章を解いてみる。【前編】

Last updated at Posted at 2018-03-04

なぜHaskellで自然言語処理か

文字列処理力を上げようと思ったのだが、いつも通りC++やらPythonやらを使うのはなんかふつうな気がしたのだ
まあC++で文字列処理ってきほんしぬとはおもうけど

それを考えると普段業務で使っていないHaskellやRustが候補にあがる。
Haskellはかなり長い間使っていなかったので今回は久しぶりにHaskellでやることにした。
ちなみに筆者のHaskell力はLYAHFGGRWHの気になる部分を読んでみた程度です。
そういう意味ではHaskellやってみたい人向けでもあるかもしれない。

とりあえずは言語処理100本ノックの第1章をやってみる。
言語処理にはあまり明るくないが最初の数章はふつうのテキスト処理問題に思える。
Python用の問題集らしいがまあ、最初の方だけならなんとかなるだろう。

解答に関して

簡潔さとわかりやすさ重視で書きました。
が、Haskell初心者がかいたものなのでたいして簡潔になってないかもしれません。
ただ、わかりやすさと天秤にかけたらわかりやすさのほうがおもいかな。
Haskellは抽象的にかけるぶんむずかしくなってしまうので。

*2018/12/12追記
解答でしょっちゅうString型を使っていますが今はText型のほうが主流のようなのでみなさんはText型を使ってみることをおすすめします。
まあ最初のうちはString型で慣れるのもよいとは思いますが。。

問題と解答

00. 文字列の逆順

文字列"stressed"の文字を逆に(末尾から先頭に向かって)並べた文字列を得よ.

00.hs
main = putStrLn $ reverse "stressed"
出力
desserts

もはやいうことはあるまい。
ただ単に文字列に対してreverseを適用しているだけ。

01. 「パタトクカシーー」

「パタトクカシーー」という文字列の1,3,5,7文字目を取り出して連結した文字列を得よ.

01.hs
main = putStrLn ["パタトクカシーー" !! index | index <- [0, 2, 4, 6]]
出力
パトカー

リスト内包表記を使用した。map使うのと大差ないだろうがこの問題に関してはリスト内包表記のほうが簡潔にかけるとおもった。
雰囲気的にはパタトクカシーー !! 0とパタトクカシーー !! 2とパタトクカシーー !! 4とパタトクカシーー !! 6が並んでいるイメージ(こなみ)

02. 「パトカー」+「タクシー」=「パタトクカシーー」

「パトカー」+「タクシー」の文字を先頭から交互に連結して文字列「パタトクカシーー」を得よ.

03.hs
import Data.List

main = do
  let objs = transpose ["パトカー", "タクシー"]  -- objs == ["パタ", "トク", "カシ", "ーー"]
  putStrLn $ foldr (++) [] objs
出力
パタトクカシーー

なんかこれに関しては一発でなんとかなる関数がありそうだが。。
畳み込みの練習になってしまった。

2018/11/9追記
foldr (++) []concat関数と同義のようだ。
なので以下のようにも書ける。

03.hs(concatを使用したバージョン)
import Data.List

main = do
  let objs = transpose ["パトカー", "タクシー"] -- objs == ["パタ", "トク", "カシ", "ーー"]
  putStrLn $ concat objs

03. 円周率

"Now I need a drink, alcoholic of course, after the heavy lectures involving quantum mechanics."という文を単語に分解し,各単語の(アルファベットの)文字数を先頭から出現順に並べたリストを作成せよ.

03.hs
module Main where

import qualified Data.List as List
import qualified Data.Char as Char
import qualified System.IO as IO

-- アルファベットのみのリストを生成し、その大きさを返す
numAlphas :: String -> Int
numAlphas = List.length . List.filter Char.isAlpha

main :: IO()
main = IO.print answer
    where
        ws      = List.words "Now I need a drink, alcoholic of course, after the heavy lectures involving quantum mechanics."
        answer  = List.map Main.numAlphas ws
出力
[3,1,4,1,5,9,2,6,5,3,5,8,9,7,9]

いろいろとコンパイラに怒られ、とうとうimport qualifiedを使用することにした。
地味にmapfilterが初登場だ。

mainの書き方はこれでいいのかなかなかわからん。
とりあえずwhereの中が短いのでここではwhereでかいた。

最初、問題の意図を勘違いしてかなり時間をかけてしまったが、
numAlphas関数はその勘違いの副産物なのでよかったとしよう。
正直、こういう問題だったら出力例みたいなのも問題と一緒にあっていい気がするが

04. 元素記号

"Hi He Lied Because Boron Could Not Oxidize Fluorine. New Nations Might Also Sign Peace Security Clause. Arthur King Can."という文を単語に分解し,1, 5, 6, 7, 8, 9, 15, 16, 19番目の単語は先頭の1文字,それ以外の単語は先頭に2文字を取り出し,取り出した文字列から単語の位置(先頭から何番目の単語か)への連想配列(辞書型もしくはマップ型)を作成せよ.

04.hs
module Main where

import qualified Data.List as List
import qualified Data.Map.Strict  as Map
import qualified System.IO as IO

makeKey :: (Eq a1, Num a1) => a1 -> [a2] -> [a2]
makeKey n = List.take (takeNum n)     -- List.take (1 or 2)
    where takeNum n' = if n' `List.elem` [1, 5, 6, 7, 8, 9, 15, 16, 19] then 1 else 2

main :: IO()
main = do
    let words   = List.words "Hi He Lied Because Boron Could Not Oxidize Fluorine. New Nations Might Also Sign Peace Security Clause. Arthur King Can."
        values  = [1..(List.length words)]               -- values == [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]
        keys    = List.zipWith Main.makeKey values words -- keys   == ["H","He","Li","Be","B","C","N","O","F","Ne","Na","Mi","Al","Si","P","S","Cl","Ar","K","Ca"]
        map     = Map.fromList $ List.zip values keys
    IO.print map
出力
fromList [(1,"H"),(2,"He"),(3,"Li"),(4,"Be"),(5,"B"),(6,"C"),(7,"N"),(8,"O"),(9,"F"),(10,"Ne"),(11,"Na"),(12,"Mi"),(13,"Al"),(14,"Si"),(15,"P"),(16,"S"),(17,"Cl"),(18,"Ar"),(19,"K"),(20,"Ca")]

これもどうするかわりと悩んだ。
1, 5, 6, 7, 8, 9, 15, 16, 19番目の単語先頭の1文字を、それ以外の2, 3, 4, 10, 11, ..., 20番目の単語2文字を取り出すので、
[1,2,2,2,1,1,1,..(中略)..,2,1,2]のようなリストを準備しておけばもっと簡潔になるかと思ったが、
変更に弱そうだし、わかりづらそうだし、やめときました。

makeKey n xsn[1, 5, 6, 7, 8, 9, 15, 16, 19]内に存在していたらtake 1 xs、それ以外ならtake 2 xsとなる関数です。

全体的にはzipzipWithでくっつけていく感じになってます。

第1章前半を終えた感想

はっきり言って結構時間がかかってしまった。
なれない言語だとゴリ押しが効かないのでなかなかキツイ。

とりあえずここまでならhoogleでData.ListとData.Mapの中を覗きまくる方針でよさそう。

1章の後半に行く前に他の言語でどれくらいの時間で最初の5問が解けるかはかってみたいところだなあ。

5
5
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
5
5