打ち首こくまろ

限界オタクの最終処分場

Go言語で自然言語処理100本ノック(00〜09)

モチベーション

お仕事でGo言語を使うことになった。

今まではずっとPythonを使ってうーインデンントインデント、今インデントを揃えにタブ押しまくってる僕はIT企業に勤めるごく一般的な男の子、強いて違うところをあげるとすれば人にpep8遵守を強要するってとこかナ——- みたいなことをやっていた。

Go言語が流行っていたことは知っていたけど、ifやforブロックに波カッコを使うのはPythonを使っていた僕からするとダサく見えたし、構造体だけがあってクラスも継承も無い言語使用はC言語を想起させてこれもダサい。

なんで流行っているのか全くわからなかったけど、仕事で使わなきゃいけないのなら仕方がない。勉強するしかない。

言語を勉強するには実際に書くのが一番。ということで、その筋で有名な自然言語処理100本ノックを解くことにした。簡単な問題から揃ってるし、自然言語処理を学べるということで一石二鳥一挙両得というところ。

というわけで暇な時間に解いていく。ソースコードは以下に上げる予定。

github.com

問題

00. 文字列の逆順

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

package main

import (
    "fmt"
)

func reverse(str string) string {
    str_slice := []byte(str)
    ans := make([]byte, 0, len(str))
    for i := len(str_slice) - 1; i >= 0; i-- {
        ans = append(ans, byte(str_slice[i]))
    }
    return string(ans)
}

func main() {
    str := "stressed"
    fmt.Println(reverse(str))
}
  • Python先輩はstr[::-1]で行けるが、Go君はそんなに頭が良くないので、真面目に実装する必要がある
  • make関数を使って空のスライス(配列)を確保している。キャパシティを第3引数で指定しているが、この長さを超えても容量を確保してくれる(キャパが倍になる)。メモリ意識しないといけないのは面倒だ。。。

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

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

package main

import (
    "fmt"
)

func main() {
    text := "パタトクカシーー"
    text_slice := []rune(text)
    ans := make([]rune, 0, len(text))
    ans = append(ans, text_slice[0])
    ans = append(ans, text_slice[2])
    ans = append(ans, text_slice[4])
    ans = append(ans, text_slice[6])
    fmt.Println(string(ans))
}
  • 00と同様にbyteスライスで実装しようとしていたが、日本語みたいな2バイト文字では文字化けしてしまう。当然。。
    • 文字1つを示すrune型を使う。runeとはルーン文字のrune。中二か。

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

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

package main

import (
    "fmt"
)

func main() {
    text1 := "パトカー"
    text2 := "タクシー"

    sliced_text1 := []rune(text1)
    sliced_text2 := []rune(text2)
    ans := make([]rune, 0, len(sliced_text1)+len(sliced_text2))

    for i := 0; i < len(sliced_text1); i++ {
        ans = append(ans, sliced_text1[i])
        ans = append(ans, sliced_text2[i])
    }

    fmt.Println(string(ans))
}
  • Python先輩ならzip関数で一発だが、Go君はそんなに頭が良くないので(ry

03. 円周率

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

package main

import (
    "fmt"
    "regexp"
)

func main() {
    text := "Now I need a drink, alcoholic of course, after the heavy lectures involving quantum mechanics."

    re := regexp.MustCompile("[a-zA-z]+")
    text_list := re.FindAllString(text, -1)
    ans := make([]int, 0, len(text_list))

    for _, t := range text_list {
        ans = append(ans, len(t))
    }

    fmt.Println(ans)
}
  • 正規表現コンパイル必須のようだ。
  • forrangeと組み合わせて、「このスライス全てに対して…」のように使える。添え字も一緒に返ってくるが、_等に代入することで別に使いませんよアピールしないとコンパイルエラーになる。クッソ厳しい。

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文字を取り出し,取り出した文字列から単語の位置(先頭から何番目の単語か)への連想配列(辞書型もしくはマップ型)を作成せよ.

package main

import (
    "fmt"
    "regexp"
)

func main() {
    text := "Hi He Lied Because Boron Could Not Oxidize Fluorine. New Nations Might Also Sign Peace Security Clause. Arthur King Can."
    //twos := []int{0, 4, 5, 6, 7, 8, 14, 15, 18}

    re := regexp.MustCompile("[a-zA-Z]+")
    text_list := re.FindAllString(text, -1)

    ans := make([]string, 0, len(text_list))

    for i, t := range text_list {
        switch i {
        case 0, 4, 5, 6, 7, 8, 14, 15, 18:
            ans = append(ans, string([]rune(t)[0]))
        default:
            ans = append(ans, string([]rune(t)[0:2]))
        }
    }
    fmt.Println(ans)
}

05. n-gram

与えられたシーケンス(文字列やリストなど)からn-gramを作る関数を作成せよ.この関数を用い,"I am an NLPer"という文から単語bi-gram,文字bi-gramを得よ.

package main

import (
    "flag"
    "fmt"
    "regexp"
)

var (
    text   string
    word_n int
    char_n int
)

func getWordNgram(text string, n int) [][]string {
    re := regexp.MustCompile("[a-zA-Z]+")
    text_list := re.FindAllString(text, -1)
    ans := make([][]string, 0, len(text_list)-n+1)

    for i := 0; i < cap(ans); i++ {
        sub_ans := make([]string, 0, n)
        for j := 0; j < n; j++ {
            sub_ans = append(sub_ans, text_list[i+j])
        }
        ans = append(ans, sub_ans)
    }
    return ans
}

func getCharNgram(text string, n int) []string {
    sliced_text := []rune(text)
    ans := make([]string, 0, len(sliced_text)-n+1)

    for i := 0; i < cap(ans); i++ {
        ans = append(ans, string(sliced_text[i:i+n]))
    }
    return ans
}

func main() {
    flag.StringVar(&text, "t", "", "解析する文字列")
    flag.IntVar(&word_n, "w", 2, "単語n-gram")
    flag.IntVar(&char_n, "c", 2, "文字n-gram")

    flag.Parse()

    fmt.Println(getWordNgram(text, word_n))
    fmt.Println(getCharNgram(text, char_n))
}
  • 「博多の塩」に対して、単語bi-gramは「博多の」「の塩」、文字bi-gramは「博多」「多の」「の塩」(例が悪い)
    • 単語n-gramを取得するときには、コンマなどの記号は除外するようにした。文字n-gramは無加工(どうするのが正しいんだろう。。。)
  • flagパッケージを使って、引数から解析する文字列とnを指定できるようにした。「go run 05.go -t ‘oh my got’ -w 3 -c 2」のように指定する。

06. 集合

paraparaparadise"と"paragraph"に含まれる文字bi-gramの集合を,それぞれ, XとYとして求め,XとYの和集合,積集合,差集合を求めよ.さらに,'se'というbi-gramがXおよびYに含まれるかどうかを調べよ.

package main

import (
    "fmt"
)

func getCharNgram(text string, n int) []string {
    sliced_text := []rune(text)
    ans := make([]string, 0, len(sliced_text)-n+1)

    for i := 0; i < cap(ans); i++ {
        ans = append(ans, string(sliced_text[i:i+n]))
    }
    return ans
}

func set2slice(set map[string]struct{}) []string {
    res := make([]string, 0, len(set))
    for id := range set {
        res = append(res, id)
    }
    return res
}

func slice2set(str []string) map[string]struct{} {
    set := make(map[string]struct{})
    for _, t := range str {
        set[t] = struct{}{}
    }
    return set
}

func getSet(text_list []string) []string {
    set := slice2set(text_list)
    return set2slice(set)
}

func join(set_X []string, set_Y []string) []string {
    set := slice2set(append(set_X, set_Y...))
    return set2slice(set)
}

func product(set_X []string, set_Y []string) []string {
    ans := make([]string, 0, len(set_X))
    for _, x := range set_X {
        for _, y := range set_Y {
            if x == y {
                ans = append(ans, x)
                break
            }
        }
    }
    return ans
}

func difference(set_X []string, set_Y []string) []string {
    ans := make([]string, 0, len(set_X))
    for _, x := range set_X {
        flag := false
        for _, y := range set_Y {
            if x == y {
                flag = true
                break
            }
        }
        if !flag {
            ans = append(ans, x)
        }
    }
    return ans
}

func searchSet(set []string, key string) bool {
    for _, t := range set {
        if t == key {
            return true
        }
    }
    return false
}

func main() {
    text1 := "paraparaparadise"
    text2 := "paragraph"

    set_X := getSet(getCharNgram(text1, 2))
    set_Y := getSet(getCharNgram(text2, 2))

    fmt.Println("set_X:  ", set_X)
    fmt.Println("set_Y:  ", set_Y)

    fmt.Println("X∪Y:    ", join(set_X, set_Y))
    fmt.Println("X∩Y:    ", product(set_X, set_Y))

    fmt.Println("X-Y:    ", difference(set_X, set_Y))
    fmt.Println("Y-X:    ", difference(set_Y, set_X))

    fmt.Println("se in X:", searchSet(set_X, "se"))
    fmt.Println("se in Y:", searchSet(set_Y, "se"))
}
  • Python先輩は集合型を使えば一発だが、Go君は(ry

07. テンプレートによる文生成

引数x, y, zを受け取り「x時のyはz」という文字列を返す関数を実装せよ.さらに,x=12, y=“気温”, z=22.4として,実行結果を確認せよ.

package main

import (
    "flag"
    "fmt"
)

var (
    x int
    y string
    z float64
)

func main() {
    flag.IntVar(&x, "x", 0, "時間")
    flag.StringVar(&y, "y", "気温", "文字列")
    flag.Float64Var(&z, "z", 0.0, "数字")
    flag.Parse()

    fmt.Println(x, "時の", y, "は", z)
}

08. 暗号文

与えられた文字列の各文字を,以下の仕様で変換する関数cipherを実装せよ.

  • 英小文字ならば(219 - 文字コード)の文字に置換
  • その他の文字はそのまま出力

この関数を用い,英語のメッセージを暗号化・復号化せよ.

package main

import (
    "flag"
    "fmt"
)

var text string

func cipher(text string) string {
    char_list := []rune(text)
    ans := []rune{}
    for _, char := range char_list {
        if char >= 'a' && char <= 'z' {
            char = rune(219 - int(char))
        }
        ans = append(ans, char)
    }
    return string(ans)
}

func main() {
    flag.StringVar(&text, "t", "", "input text")
    flag.Parse()

    fmt.Println(cipher(text))
}

09. Typoglycemia

スペースで区切られた単語列に対して,各単語の先頭と末尾の文字は残し,それ以外の文字の順序をランダムに並び替えるプログラムを作成せよ.ただし,長さが4以下の単語は並び替えないこととする.適当な英語の文(例えば"I couldn’t believe that I could actually understand what I was reading : the phenomenal power of the human mind .“)を与え,その実行結果を確認せよ.

package main

import (
    "fmt"
    "math/rand"
    "os"
    "strings"
    "time"
)

func shuffle(word string) string {
    if len(word) < 4 {
        return word
    }
    sliced := []rune(word)
    rand.Seed(time.Now().UnixNano())
    for i := len(sliced) - 2; i >= 1; i-- {
        j := rand.Intn(i) + 1
        sliced[i], sliced[j] = sliced[j], sliced[i]
    }
    return string(sliced)

}

func main() {
    text := os.Args[1]

    ans := []string{}
    for _, word := range strings.Fields(text) {
        shuffled := shuffle(word)
        ans = append(ans, shuffled)
    }
    fmt.Println(strings.Join(ans, " "))

}

Go君の第一印象

  • 低級言語のかほりがそこかしこから臭ってくる。make関数はC言語malloc関数のような印象を受けるし、やっぱりコードの見た目もC言語っぽい。
    • 率直に言うと「あんまり好みじゃない」
  • Python自由の翼を広げていた僕にとって、Goの強力な静的型付けは地べたに引き摺り下ろされて手錠足枷亀甲縛りされたような窮屈感を覚える
  • なるべくシンプルな言語仕様にしようとしているのは好感が持てる
    • whileを廃止してforだけで全てをさせるのはすげぇと思った。forwhileの違いって初心者はよくつまずくし。。。
  • Pythonはpep8というルールを規定して「読みやすいコードとはなんぞや」を定義していたが、Goはもっと進んでgo fmt hoge.goで「いい感じに」ソースコードを整形してくれる
    • 複数人でコード書いていてもコマンド一発で同じような見た目にしてくれるのは素晴らしいし、それを標準で用意しているのはもっと素晴らしい
    • しかしクソみたいな変数名をつけるクソやアホみたいに長いメソッド書くアホ相手にはどうしようもない(と思う)ので、やはりコーディングルールの整備は必要。
  • まぁまだこれだけ書いただけではGoの魅力にはまだまだ触れてないなと感じる。
    • スレッド処理? が楽に書けるらしいけど。。。