Posted at

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


はじめに

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

アクセス数やいいねを見て需要ない感じが半端じゃなく、更新してませんでしたが、ここにきてまさかの更新です!


解答に関して


  • 簡潔さとわかりやすさ重視で書きました。


    • 一部の問題は今までの問題より少しだけ難しめなので重視できなかったかもしれません。。



  • 文字列はなるべくString型ではなくText型を扱うようにしました。


問題と解答


第2章: UNIXコマンドの基礎


hightemp.txtは,日本の最高気温の記録を「都道府県」「地点」「℃」「日」のタブ区切り形式で格納したファイルである.以下の処理を行うプログラムを作成し,hightemp.txtを入力ファイルとして実行せよ.さらに,同様の処理をUNIXコマンドでも実行し,プログラムの実行結果を確認せよ.



15. 末尾のN行を出力


自然数Nをコマンドライン引数などの手段で受け取り,入力のうち末尾のN行だけを表示せよ.確認にはtailコマンドを用いよ.


解答はこちら


15.hs

-- 15. 末尾のN行を出力

-- 自然数Nをコマンドライン引数などの手段で受け取り,入力のうち末尾のN行だけを表示せよ.確認にはtailコマンドを用いよ.

module Main where

import qualified System.IO as IO
import qualified System.Environment as Env
import qualified Data.List as List
import qualified Data.Text as Text
import qualified Data.Text.IO as TextIO
import qualified Text.Read as Read

import System.Environment(getArgs, getProgName)

tail :: Int -> Text.Text -> IO()
tail n input = TextIO.putStr $ tail' n input
where
tail' :: Int -> Text.Text -> Text.Text
tail' n input = Text.unlines $ List.drop (List.length (Text.lines input) - n) (Text.lines input)

main :: IO()
main = do
input <- TextIO.readFile "hightemp.txt"
prog <- Env.getProgName
args <- Env.getArgs
case args of
[n] -> Main.tail (Read.read n :: Int) input
_ -> usage prog
where
usage :: String -> IO()
usage prog = IO.putStrLn $ "usage: " ++ prog ++ " N (N must be a nutural number.)"


前問のコードとほぼ同じ。

というより、headとの対称性を考えてなるべくコードの変更はしなかった。

違いはList.takeList.dropになり、引数が変わった程度。

文字列に関してはText.LazyではなくTextを用いたが特筆する話ではないだろう。


16. ファイルをN分割する


自然数Nをコマンドライン引数などの手段で受け取り,入力のファイルを行単位でN分割せよ.同様の処理をsplitコマンドで実現せよ.



16.hs

-- 16. ファイルをN分割する

-- 自然数Nをコマンドライン引数などの手段で受け取り,入力のファイルを行単位でN分割せよ.同様の処理をsplitコマンドで実現せよ.

module Main where

import qualified System.IO as IO
import qualified System.Environment as Env
import qualified Data.List as List
import qualified Data.Tuple as Tuple
import qualified Data.Text as Text
import qualified Data.Text.IO as TextIO
import qualified Text.Read as Read
import qualified Text.Show as Show
import qualified Control.Monad as Monad

import System.Environment(getArgs, getProgName)

writeLineFile :: FilePath -> [Text.Text] -> IO()
writeLineFile filepath xs = IO.withFile filepath IO.WriteMode $ \handle ->
Monad.mapM_ (TextIO.hPutStrLn handle) xs

split' :: [Text.Text] -> Int -> Int -> Int -> IO()
split' lines n m index = Main.writeLineFile filepath contents
where
filepath = "split/hightemp_split_" ++ Show.show index ++ ".txt"
contents = List.take m (List.drop (n * index) lines)

split :: Int -> Text.Text -> IO()
split n input = Monad.zipWithM_ (Main.split' (Text.lines input) n) ms [0..]
where
divmod = divMod (List.length $ Text.lines input) n
ms = List.replicate (Tuple.fst divmod) n ++ if Tuple.snd divmod == 0 then [] else [Tuple.snd divmod]

main :: IO()
main = do
input <- TextIO.readFile "hightemp.txt"
prog <- Env.getProgName
args <- Env.getArgs
case args of
[n] -> Main.split (Read.read n :: Int) input
_ -> usage prog
where
usage :: String -> IO()
usage prog = IO.putStrLn $ "usage: " ++ prog ++ " N (N must be a nutural number.)"


第2章で一番むずかしいというか、一番面倒で時間かかったのはこの問題だった。

ただ、他の言語での解答を見てもどちらにせよそこそこ面倒そうだったのでこんなもんだろうか。

Haskellでもゴリ押しのコードが書けるようになったと前向きに考えておく。:grin:

writeLineFileは前編からそのまま引用しました。

splitは前問のheadと前前問のtailの型シグネチャとわざわざ合致させるためになんとなく作った関数で、肝心な方はsplit'の方で行っている。

splitでやっていることはN分割したときにList.takeで取ってきたい行の数のリスト(msがそうです..)を作成して、何分割目かを確認するためのindex値のリスト([0..]がそうです..)と一緒にzipWithM_してしまおう!といった感じです。

zipWithM_は返り値ぽいっとしてm()とします。ここでのmApplicativeのインスタンスで今回はIOがそれに該当します。:v::v_tone1:

split'index+1分割目の役割を果たします。出力ファイル名はもちろん適当です。


17. 1列目の文字列の異なり


1列目の文字列の種類(異なる文字列の集合)を求めよ.確認にはsort, uniqコマンドを用いよ.



17.hs

-- 17. 1列目の文字列の異なり

-- 1列目の文字列の種類(異なる文字列の集合)を求めよ.確認にはsort, uniqコマンドを用いよ.

module Main where

import qualified Data.List as List
import qualified Data.Set as Set
import qualified Data.Text as Text
import qualified Data.Text.IO as TextIO
import qualified Control.Monad as Monad

main :: IO()
main = do
input <- TextIO.readFile "hightemp.txt"
let
row1 = List.map (List.head . Text.words) (Text.lines input)
uniq = Set.fromList row1
Monad.mapM_ TextIO.putStrLn uniq


row1が1列目の文字列のリストでuniqがその集合です。

このコードに関しては特に語ることがありません。。


18. 各行を3コラム目の数値の降順にソート


各行を3コラム目の数値の逆順で整列せよ(注意: 各行の内容は変更せずに並び替えよ).確認にはsortコマンドを用いよ(この問題はコマンドで実行した時の結果と合わなくてもよい).



18.hs

-- 18. 各行を3コラム目の数値の降順にソート

-- 各行を3コラム目の数値の逆順で整列せよ(注意: 各行の内容は変更せずに並び替えよ).確認にはsortコマンドを用いよ(この問題はコマンドで実行した時の結果と合わなくてもよい).

module Main where

import qualified Data.Ord as Ord
import qualified Data.List as List
import qualified Data.Text as Text
import qualified Data.Text.IO as TextIO
import qualified Control.Monad as Monad

main :: IO()
main = do
input <- TextIO.readFile "hightemp.txt"
let
words = List.map Text.words $ Text.lines input
sorted = List.sortBy (\xs ys -> Ord.compare (xs !! 2) (ys !! 2)) words
lines = List.map Text.unwords sorted
Monad.mapM_ TextIO.putStrLn lines


3カラム目でソートしてます。なんで17問目では列表記だったのに18問目でコラム表記なのでしょうか。。


19. 各行の1コラム目の文字列の出現頻度を求め,出現頻度の高い順に並べる


各行の1列目の文字列の出現頻度を求め,その高い順に並べて表示せよ.確認にはcut, uniq, sortコマンドを用いよ.


この問題に関してはコラム表記と列表記が混じってますね。なんでだろう。。:sweat_smile:


19.hs

-- 19. 各行の1コラム目の文字列の出現頻度を求め,出現頻度の高い順に並べる

-- 各行の1列目の文字列の出現頻度を求め,その高い順に並べて表示せよ.確認にはcut, uniq, sortコマンドを用いよ.

module Main where

import qualified Data.Function as Function
import qualified Data.Ord as Ord
import qualified Data.List as List
import qualified Data.Tuple as Tuple
import qualified Data.HashMap.Strict as HashMap
import qualified Data.Text as Text
import qualified Data.Text.IO as TextIO
import qualified Control.Monad as Monad

main :: IO()
main = do
input <- TextIO.readFile "hightemp.txt"
let
row1 = List.map (List.head . Text.words) (Text.lines input) -- List[都道府県(重複あり)..]
init = HashMap.fromList (List.zip row1 (repeat 0)) -- HashMap[(都道府県, 0)..]
counted = HashMap.mapWithKey (count row1) init -- HashMap[(都道府県, 出現頻度)..]
sorted = List.sortBy (Function.flip Ord.compare `Function.on` Tuple.snd) (HashMap.toList counted)
Monad.mapM_ (TextIO.putStrLn . Tuple.fst) sorted
where
count :: [Text.Text] -> Text.Text -> Int -> Int
count r k _ = List.length $ List.filter (== k) r


row1で一列目のリストを作成して、initでvalueがすべてゼロのHashMap作ります。

countedで出現頻度をvalueに入れて、sortedで出現頻度についてソートしてます。

HashMapはUnorderedなMapでKeyのOrderを気にしなくていい場合はMapよりも基本的にパフォーマンスが高いコンテナです。

まあこのレベルであれば普通にMapでも困ることはなさそうです。


さいごに

結局2章までやってしまったが、正規表現やパーサー書いてどうこうするというよくある感じのあれこれまで至っておらず、第3章もやるかもしれません。

正規表現やパーサー書くっていうのはいろいろやってきているのであまりやったことがないという理由でHaskell続投する可能性があるかも…?